diff --git a/.github/ISSUE_TEMPLATE/---bug-report.md b/.github/ISSUE_TEMPLATE/---bug-report.md new file mode 100644 index 0000000000..bffb88ebb7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/---bug-report.md @@ -0,0 +1,60 @@ +--- +name: "\U0001F41E Bug report" +about: Create a report to help us improve +title: "[Bug] " +labels: bug +assignees: ashvayka, vvlladd28 + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**Your Server Environment** + +* demo.thingsboard.io +* cloud.thingsboard.io +* own setup + * cloud or local infrastructure or docker deployment + * ThingsBoard Version + * OS Name and Version + +**Your Client Environment** + +**Desktop (please complete the following information):** + +* OS: [e.g. iOS] +* Browser [e.g. chrome, safari] +* Version [e.g. 22] + +**Smartphone (please complete the following information):** +* Device: [e.g. iPhone6] +* OS: [e.g. iOS8.1] +* Browser [e.g. stock browser, safari] +* Version [e.g. 22] + +**Your Device** + +* Connectivity + * MQTT + * HTTP + * CoAP + * Gateway + * Integration: (Specify name) +* Device vendor and model + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..8730336bba --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[Feature Request]" +labels: feature +assignees: ikulikov + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000000..99d82ca698 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,25 @@ +--- +name: Question +about: Describe your questions in details +title: "[Question] your title here" +labels: question +assignees: ashvayka + +--- + +**Component** + + + * UI + * Rule Engine + * Installation + * Generic + +**Description** +A clear and concise details. + +**Environment** + + * OS: name and version + * ThingsBoard: version + * Browser: name and version diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f635da5a55..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,12 +0,0 @@ -before_install: - - sudo rm -f /etc/mavenrc - - export M2_HOME=/usr/local/maven - - export MAVEN_OPTS="-Dmaven.repo.local=$HOME/.m2/repository -Xms1024m -Xmx3072m" - - export HTTP_LOG_CONTROLLER_ERROR_STACK_TRACE=false -jdk: - - openjdk8 -language: java -sudo: required -services: - - docker -script: mvn clean verify -Ddockerfile.skip=false diff --git a/README.md b/README.md index 04d2e5fede..5520645541 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ThingsBoard [![Join the chat at https://gitter.im/thingsboard/chat](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/thingsboard/chat?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![Build Status](https://travis-ci.org/thingsboard/thingsboard.svg?branch=master)](https://travis-ci.org/thingsboard/thingsboard) +[![ThingsBoard Builds Server Status](https://img.shields.io/teamcity/build/e/ThingsBoard_Build?label=TB%20builds%20server&server=https%3A%2F%2Fbuilds.thingsboard.io&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAALzAAAC8wHS6QoqAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAB9FJREFUeJzVm3+MXUUVx7+zWwqEtnRLWisQ2lKVUisIQmsqYCohpUhpEGsFKSJJTS0qGiGIISJ/8CNGYzSaEKBQEZUiP7RgVbCVdpE0xYKBWgI2rFLZJZQWtFKobPfjH3Pfdu7s3Pvmzntv3/JNNr3bOXPO+Z6ZO3PumVmjFgEYJWmWpDmSZks6VtIESV3Zv29LWmGMubdVPgw7gEOBJcAaYC/18fd2+zyqngAwXdL7M9keSduMMXgyH5R0laRPSRpbwf62CrLDB8AAS4HnAqP2EvA1YBTwPuBnwP46I70H+DPwALAS+B5wBTCu3VyHIJvG98dMX+B/BW1vAvcAnwdmAp3t5hWFbORXR5AvwmPARcCYdnNJAnCBR+gd7HQ9HZgLfAt4PUB8AzCv3f43DGCTQ6o/RAo43gtCL2Da4W9TAUwEBhxiPymRvcabAR8eTl+biQ7neYokdyTXlvR7xPt9etM8GmZ0FDxL+WD42FdBdkTDJd0jyU1wzi7pd473e0+qA8AM4AbgkrK1BDgOWAc8ChyTaq+eM5ud93ofcHpAZiY2sanhZaDDaTfAZ7HJUmlWCJzm6bqLQM6QBanXkfthcxgPNbTEW9z2AT8AzgTmANdikxwXX/d0XOi0bQEmFNj6GPAfhuKnXkB98kNsNjsITwacKkI3MNrrf4UnswXoiiRfwyqgo4D8L2hVZglMw456DDYCRwR0jCH/KuWCgE2oysjX8KsA+V+2jHzm3CrP4PMBx/4JfAU4qETP+EAQ/gKcA/w7gnwNbl5yD7bG0DLyM7DZXw3d2f9PA+YD5wIzK+gLBSEFA/XIA2cAVwLvbSQAt3mGP5Gs7IDO8dg1ZYDGcAfOwujZuIwDn+ObUx09hHx+v7Eh5nndCyIIDgBbgd0lMiv9IABfIF+LeDnVyU97xj5XR/6bwI5sZEaXyH2UuHd+WSbfRXktYjAIAfL9wGdSA/Cgo+gtSio12IKJa3hNKAgZ+TciyL+AlwECKzI/ioLgTvsa+YtTyXeSz8ZW15E3wN88p3JBwCZNMeShIKkBTsRmmSG4a0o/sDSJfGboBE/5pRF9pgI9oSBUJP8mXpLk2bm6pO9Aw+QzI8s8xVFbXRaEf3h911cgD7Cyjg0/L/GxnoLdoUoA3O1vDxUyLWyO4AehCpYX6D2L/LpUhtsaCkIWxRoeT+g/DVsqT8EWYDowC5jh6FxUUc+tJJblOmSPqWp4JUFHl6TDUoxLOlnSdknPSnK3sA2S9lfQs0zS7SkzwQ/A61U6A6dKWufpSMVg5mmMeUPSXyv2v0zSN6oa7ZAdwRqiA5CRf0TS+KpGAxiQ1OFN4z8l6PErVXUxSvmp1hvTqUnk35adPWskPWSM6fPaq84ASXqscg/gi9gcvJuC6o0nfwrhw5EYvIpNn88HStcN4M6KulfTys/lzKlO0lb8P2Lrf6VbLDAF+DLweEX998aSx372bwP6gPlVA3BEAvm9FJwVYtPqjwDXA08n6AZbOYoeeeAWp++mSlPGGLMLeFjSuRW6Iektx4GDJc2TdJ6khZKOruKDh/skXWSM6a/Q5yjn+dDKFrE1vw0VR2m2039x4kj7uJ+SslyJ/+7rtaly4mCM+a+kBaq2TbnVpfWy216jmCzpkIR+7kK/MymHNsbslX0NYoMweMpsjNklaWuKXQ9zJf2eOocvAbzHee5N/ojIgvBVxY3madh3v4b1iWZ/o3zw5kpaS+SFDGCq8jPguUQ/CmsCZfi403dhwjv/AHAQMAl41mvbGBMEhq4/c1PJTwmQr1f7u97pfzj5EnwUead/KAg/ivD7Zkf+HSBpFwiRfwibI3SXkOj29PgEivAggdU+C8JWR+6+CN9dm1tSyHcBLwbIj87ax1Kcxe0DJmVyY4CdEeR/TXnVeRLwc+C3wHF1fP+Qp/uGlABc6Cl5mPziVi8IzwDfAZ6KIN9LyhQt9v1GT/+sFCXTOVBBXuOTd+TGkp+eqWjKSTBwMPAvR+9TjSibjK35l93mWIxdZFKOxPzFseEgAJd7Olt6v+AC8jdIqwRhLbZM758HRH3tYa/vnoqtKZ4JHIk99tvh6HqNVl3RLSB/JfBEBPnBwxXsJ2uf176qxO7hwE3ALq/PfuyVXhdXt4r8+QHyK7K2cXWCMLiTOPqODwTh2IDdD2CP12LwCnUKMankO8kfiAySd2SKgjCEfEEQ+nznsZc7eyLJA9zddPKZIx0c2NcHgMsL5MZhr83XULiTeCSXAEcG2m4PjPCXsEWWBdhbZ/4h6knN4u07Mxv4MbCojtxo7DW6RTRwopMFxt0xeoCJAblLvCDdlWpzRAG42CO2sET2UUfuVbetsYPF9mKq8zwg6Q8lsm7bRJxt8N0cAPdar5FUupYU9X03B2C782wknVUi+0nneacxZk9rXBpGABO8RXA72demJ7fcWyvubIe/TQN2y11MuJ6wA5v3z8HeMbjba+8n5StwJCDb9lYUEI/Fde3mEQ1svnBKRvp32K/LEPYQd1z3XQJfsG3/Sw/gKElLZev8tb8rnizpBEmF1SDZ06ZbJN0saa+kayQtV77qi6QnJF1njFnXdOebAcIXssvQB3yfcGrcCZwEnAfMC8mMKGArNUVT28VubF4/nyZflx8Jr8BVkr4tm83tzn5ek/S8pM2SnpT0gv8H283C/wGTFfhGtexQwQAAAABJRU5ErkJggg==&labelColor=305680)](https://builds.thingsboard.io/viewType.html?buildTypeId=ThingsBoard_Build&guest=1) ThingsBoard is an open-source IoT platform for data collection, processing, visualization, and device management. diff --git a/application/pom.xml b/application/pom.xml index 1730d7a5a5..c2ae7eccc2 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT thingsboard application @@ -46,10 +46,6 @@ - - de.ruedigermoeller - fst - io.netty netty-transport-native-epoll @@ -124,12 +120,16 @@ org.slf4j slf4j-log4j12 + + org.hibernate + hibernate-validator + test org.thingsboard - ui + ui-ngx ${project.version} runtime @@ -162,12 +162,8 @@ jjwt - org.apache.velocity - velocity - - - org.apache.velocity - velocity-tools + org.freemarker + freemarker commons-io @@ -246,6 +242,12 @@ org.springframework.boot spring-boot-starter-test test + + + com.vaadin.external.google + android-json + + org.springframework.security @@ -302,6 +304,11 @@ com.github.ua-parser uap-java + + org.java-websocket + Java-WebSocket + test + diff --git a/application/src/main/data/json/demo/dashboards/gateways.json b/application/src/main/data/json/demo/dashboards/gateways.json index 7d75908229..34474ea890 100644 --- a/application/src/main/data/json/demo/dashboards/gateways.json +++ b/application/src/main/data/json/demo/dashboards/gateways.json @@ -128,13 +128,14 @@ "actions": { "headerButton": [ { - "id": "70837a9d-c3de-a9a7-03c5-dccd14998758", "name": "Add device", "icon": "add", "type": "customPretty", - "customHtml": "\n
\n \n
\n

Add device

\n \n \n \n \n
\n
\n \n \n \n
\n
\n \n \n \n
\n
Device name is required.
\n
\n
\n
\n \n \n \n \n \n \n \n \n
\n \n \n \n \n
\n
\n
\n \n Create\n Cancel\n \n
\n
\n", + "customHtml": "
\n \n

Add device

\n \n \n
\n \n \n
\n
\n
\n \n Device name\n \n \n Device name is required.\n \n \n
\n \n Latitude\n \n \n \n Longitude\n \n \n
\n \n Label\n \n \n
\n
\n
\n \n \n \n
\n
\n", "customCss": "", - "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet $mdDialog = $injector.get('$mdDialog'),\n $document = $injector.get('$document'),\n $q = $injector.get('$q'),\n $rootScope = $injector.get('$rootScope'),\n types = $injector.get('types'),\n deviceService = $injector.get('deviceService'),\n attributeService = $injector.get('attributeService');\n\nopenAddDeviceDialog();\n\nfunction openAddDeviceDialog() {\n $mdDialog.show({\n controller: ['$scope', '$mdDialog',\n AddDeviceDialogController\n ],\n controllerAs: 'vm',\n template: htmlTemplate,\n parent: angular.element($document[0].body),\n targetEvent: $event,\n multiple: true,\n clickOutsideToClose: false\n });\n}\n\nfunction AddDeviceDialogController($scope, $mdDialog) {\n let vm = this;\n vm.types = types;\n vm.attributes = {};\n vm.deviceType = \"gateway\";\n\n vm.cancel = () => {\n $mdDialog.hide();\n };\n\n vm.save = () => {\n vm.loading = true;\n $scope.addDeviceForm.$setPristine();\n let device = {\n additionalInfo: {gateway: true},\n name: vm.deviceName,\n type: vm.deviceType,\n label: vm.deviceLabel\n };\n deviceService.saveDevice(device).then(\n (device) => {\n saveAttributes(device.id).then(\n () => {\n vm.loading = false;\n updateAliasData();\n $mdDialog.hide();\n }\n );\n },\n () => {\n vm.loading = false;\n }\n );\n };\n\n function saveAttributes(entityId) {\n let attributesArray = [];\n for (let key in vm.attributes) {\n attributesArray.push({\n key: key,\n value: vm.attributes[key]\n });\n }\n if (attributesArray.length > 0) {\n return attributeService.saveEntityAttributes(\n entityId.entityType, entityId.id,\n \"SERVER_SCOPE\", attributesArray);\n } else {\n return $q.when([]);\n }\n }\n\n function updateAliasData() {\n let aliasIds = [];\n for (let id in widgetContext.aliasController\n .resolvedAliases) {\n aliasIds.push(id);\n }\n let tasks = [];\n aliasIds.forEach((aliasId) => {\n widgetContext.aliasController\n .setAliasUnresolved(aliasId);\n tasks.push(widgetContext.aliasController\n .getAliasInfo(aliasId));\n });\n $q.all(tasks).then(() => {\n $rootScope.$broadcast(\n 'widgetForceReInit');\n });\n }\n}" + "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\n\nopenAddDeviceDialog();\n\nfunction openAddDeviceDialog() {\n customDialog.customDialog(htmlTemplate, AddDeviceDialogController).subscribe();\n}\n\nfunction AddDeviceDialogController(instance) {\n let vm = instance;\n \n vm.addDeviceFormGroup = vm.fb.group({\n deviceName: ['', [vm.validators.required]],\n deviceLabel: [''],\n attributes: vm.fb.group({\n latitude: [null],\n longitude: [null]\n }) \n });\n \n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n \n vm.save = function() {\n vm.addDeviceFormGroup.markAsPristine();\n let device = {\n additionalInfo: {gateway: true},\n name: vm.addDeviceFormGroup.get('deviceName').value,\n type: 'gateway',\n label: vm.addDeviceFormGroup.get('deviceLabel').value\n };\n deviceService.saveDevice(device).subscribe(\n function (device) {\n saveAttributes(device.id).subscribe(\n function () {\n widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n }\n );\n };\n \n function saveAttributes(entityId) {\n let attributes = vm.addDeviceFormGroup.get('attributes').value;\n let attributesArray = [];\n for (let key in attributes) {\n attributesArray.push({key: key, value: attributes[key]});\n }\n if (attributesArray.length > 0) {\n return attributeService.saveEntityAttributes(entityId, \"SERVER_SCOPE\", attributesArray);\n } else {\n return widgetContext.rxjs.of([]);\n }\n }\n}\n", + "customResources": [], + "id": "70837a9d-c3de-a9a7-03c5-dccd14998758" } ], "actionCellButton": [ @@ -156,20 +157,21 @@ "setEntityId": true }, { - "id": "242671f3-76c6-6982-7acc-6f12addf0ccc", "name": "Edit device", "icon": "edit", "type": "customPretty", - "customHtml": "\n
\n \n
\n

Edit device

\n \n \n \n \n
\n
\n \n \n \n
\n
\n \n \n \n
\n
Device name is required.
\n
\n
\n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n \n \n \n \n \n \n \n \n
\n \n \n \n \n
\n
\n
\n \n Create\n Cancel\n \n
\n
", + "customHtml": "
\n \n

Edit device

\n \n \n
\n \n \n
\n
\n
\n \n Device name\n \n \n Device name is required.\n \n \n
\n \n Latitude\n \n \n \n Longitude\n \n \n
\n \n Label\n \n \n
\n
\n
\n \n \n \n
\n
\n", "customCss": "/*=======================================================================*/\n/*========== There are two examples: for edit and add entity ==========*/\n/*=======================================================================*/\n/*======================== Edit entity example ========================*/\n/*=======================================================================*/\n/*\n.edit-entity-form md-input-container {\n padding-right: 10px;\n}\n\n.edit-entity-form .boolean-value-input {\n padding-left: 5px;\n}\n\n.edit-entity-form .boolean-value-input .checkbox-label {\n margin-bottom: 8px;\n color: rgba(0,0,0,0.54);\n font-size: 12px;\n}\n\n.relations-list .header {\n padding-right: 5px;\n padding-bottom: 5px;\n padding-left: 5px;\n}\n\n.relations-list .header .cell {\n padding-right: 5px;\n padding-left: 5px;\n font-size: 12px;\n font-weight: 700;\n color: rgba(0, 0, 0, .54);\n white-space: nowrap;\n}\n\n.relations-list .body {\n padding-right: 5px;\n padding-bottom: 15px;\n padding-left: 5px;\n}\n\n.relations-list .body .row {\n padding-top: 5px;\n}\n\n.relations-list .body .cell {\n padding-right: 5px;\n padding-left: 5px;\n}\n\n.relations-list .body md-autocomplete-wrap md-input-container {\n height: 30px;\n}\n\n.relations-list .body .md-button {\n margin: 0;\n}\n\n.relations-list.old-relations tb-entity-select tb-entity-autocomplete button {\n display: none;\n} \n*/\n/*========================================================================*/\n/*========================= Add entity example =========================*/\n/*========================================================================*/\n/*\n.add-entity-form md-input-container {\n padding-right: 10px;\n}\n\n.add-entity-form .boolean-value-input {\n padding-left: 5px;\n}\n\n.add-entity-form .boolean-value-input .checkbox-label {\n margin-bottom: 8px;\n color: rgba(0,0,0,0.54);\n font-size: 12px;\n}\n\n.relations-list .header {\n padding-right: 5px;\n padding-bottom: 5px;\n padding-left: 5px;\n}\n\n.relations-list .header .cell {\n padding-right: 5px;\n padding-left: 5px;\n font-size: 12px;\n font-weight: 700;\n color: rgba(0, 0, 0, .54);\n white-space: nowrap;\n}\n\n.relations-list .body {\n padding-right: 5px;\n padding-bottom: 15px;\n padding-left: 5px;\n}\n\n.relations-list .body .row {\n padding-top: 5px;\n}\n\n.relations-list .body .cell {\n padding-right: 5px;\n padding-left: 5px;\n}\n\n.relations-list .body md-autocomplete-wrap md-input-container {\n height: 30px;\n}\n\n.relations-list .body .md-button {\n margin: 0;\n}\n*/\n", - "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet $mdDialog = $injector.get('$mdDialog'),\n $document = $injector.get('$document'),\n $q = $injector.get('$q'),\n $rootScope = $injector.get('$rootScope'),\n types = $injector.get('types'),\n deviceService = $injector.get('deviceService'),\n attributeService = $injector.get('attributeService');\n \nopenEditDeviceDialog();\n\nfunction openEditDeviceDialog() {\n $mdDialog.show({\n controller: ['$scope','$mdDialog', EditDeviceDialogController],\n controllerAs: 'vm',\n template: htmlTemplate,\n parent: angular.element($document[0].body),\n targetEvent: $event,\n multiple: true,\n clickOutsideToClose: false\n });\n}\n\nfunction EditDeviceDialogController($scope,$mdDialog) {\n let vm = this;\n vm.types = types;\n vm.loading = false;\n vm.attributes = {};\n \n getEntityInfo();\n \n function getEntityInfo() {\n vm.loading = true;\n deviceService.getDevice(entityId.id).then(\n (device) => {\n attributeService.getEntityAttributesValues(entityId.entityType, entityId.id, 'SERVER_SCOPE').then(\n (data) => {\n if (data.length) {\n getEntityAttributes(data);\n }\n vm.device = device;\n vm.loading = false;\n } \n );\n }\n )\n }\n \n vm.cancel = function() {\n $mdDialog.hide();\n };\n \n vm.save = () => {\n vm.loading = true;\n $scope.editDeviceForm.$setPristine();\n deviceService.saveDevice(vm.device).then(\n () => {\n saveAttributes().then(\n () => {\n updateAliasData();\n vm.loading = false;\n $mdDialog.hide();\n }\n );\n },\n () => {\n vm.loading = false;\n }\n );\n }\n \n function getEntityAttributes(attributes) {\n for (let i = 0; i < attributes.length; i++) {\n vm.attributes[attributes[i].key] = attributes[i].value; \n }\n }\n \n function saveAttributes() {\n let attributesArray = [];\n for (let key in vm.attributes) {\n attributesArray.push({key: key, value: vm.attributes[key]});\n }\n if (attributesArray.length > 0) {\n return attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \"SERVER_SCOPE\", attributesArray);\n } else {\n return $q.when([]);\n }\n }\n \n function updateAliasData() {\n let aliasIds = [];\n for (let id in widgetContext.aliasController.resolvedAliases) {\n aliasIds.push(id);\n }\n let tasks = [];\n aliasIds.forEach((aliasId) => {\n widgetContext.aliasController.setAliasUnresolved(aliasId);\n tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\n });\n console.log(widgetContext);\n $q.all(tasks).then(() => {\n $rootScope.$broadcast('widgetForceReInit');\n });\n }\n}\n" + "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\n\nopenEditDeviceDialog();\n\nfunction openEditDeviceDialog() {\n customDialog.customDialog(htmlTemplate, EditDeviceDialogController).subscribe();\n}\n\nfunction EditDeviceDialogController(instance) {\n let vm = instance;\n \n vm.device = null;\n vm.attributes = {};\n \n vm.editDeviceFormGroup = vm.fb.group({\n deviceName: ['', [vm.validators.required]],\n deviceLabel: [''],\n attributes: vm.fb.group({\n latitude: [null],\n longitude: [null]\n }) \n });\n \n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n \n vm.save = function() {\n vm.editDeviceFormGroup.markAsPristine();\n vm.device.name = vm.editDeviceFormGroup.get('deviceName').value;\n vm.device.label = vm.editDeviceFormGroup.get('deviceLabel').value;\n deviceService.saveDevice(vm.device).subscribe(\n function () {\n saveAttributes().subscribe(\n function () {\n widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n }\n );\n };\n \n getEntityInfo();\n \n function getEntityInfo() {\n deviceService.getDevice(entityId.id).subscribe(\n function (device) {\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE',\n ['latitude', 'longitude']).subscribe(\n function (attributes) {\n for (let i = 0; i < attributes.length; i++) {\n vm.attributes[attributes[i].key] = attributes[i].value; \n }\n vm.device = device;\n vm.editDeviceFormGroup.patchValue(\n {\n deviceName: vm.device.name,\n deviceLabel: vm.device.label,\n attributes: {\n latitude: vm.attributes.latitude,\n longitude: vm.attributes.longitude\n }\n }, {emitEvent: false}\n );\n } \n );\n }\n ); \n }\n \n function saveAttributes() {\n let attributes = vm.editDeviceFormGroup.get('attributes').value;\n let attributesArray = [];\n for (let key in attributes) {\n attributesArray.push({key: key, value: attributes[key]});\n }\n if (attributesArray.length > 0) {\n return attributeService.saveEntityAttributes(entityId, 'SERVER_SCOPE', attributesArray);\n } else {\n return widgetContext.rxjs.of([]);\n }\n }\n}\n", + "customResources": [], + "id": "242671f3-76c6-6982-7acc-6f12addf0ccc" }, { - "id": "862ec2b7-fbcf-376e-f85f-b77c07f36efa", "name": "Delete device", "icon": "delete", "type": "custom", - "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet $mdDialog = $injector.get('$mdDialog'),\n $document = $injector.get('$document'),\n types = $injector.get('types'),\n deviceService = $injector.get('deviceService'),\n $rootScope = $injector.get('$rootScope'),\n $q = $injector.get('$q');\n\nopenDeleteDeviceDialog();\n\nfunction openDeleteDeviceDialog() {\n let title = \"Are you sure you want to delete the device \" + entityName + \"?\";\n let content = \"Be careful, after the confirmation, the device and all related data will become unrecoverable!\";\n let confirm = $mdDialog.confirm()\n .targetEvent($event)\n .title(title)\n .htmlContent(content)\n .ariaLabel(title)\n .cancel('Cancel')\n .ok('Delete');\n $mdDialog.show(confirm).then(() => {\n deleteDevice();\n })\n}\n\nfunction deleteDevice() {\n deviceService.deleteDevice(entityId.id).then(\n () => {\n updateAliasData();\n }\n );\n}\n\nfunction updateAliasData() {\n let aliasIds = [];\n for (let id in widgetContext.aliasController.resolvedAliases) {\n aliasIds.push(id);\n }\n let tasks = [];\n aliasIds.forEach((aliasId) => {\n widgetContext.aliasController.setAliasUnresolved(aliasId);\n tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\n });\n $q.all(tasks).then(() => {\n $rootScope.$broadcast('entityAliasesChanged', aliasIds);\n });\n}" + "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet dialogs = $injector.get(widgetContext.servicesMap.get('dialogs'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenDeleteDeviceDialog();\n\nfunction openDeleteDeviceDialog() {\n let title = \"Are you sure you want to delete the device \" + entityName + \"?\";\n let content = \"Be careful, after the confirmation, the device and all related data will become unrecoverable!\";\n dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe(\n function (result) {\n if (result) {\n deleteDevice();\n }\n }\n );\n}\n\nfunction deleteDevice() {\n deviceService.deleteDevice(entityId.id).subscribe(\n function () {\n widgetContext.updateAliases();\n }\n );\n}\n", + "id": "862ec2b7-fbcf-376e-f85f-b77c07f36efa" } ], "rowClick": [ @@ -1102,14 +1104,11 @@ "backgroundColor": "#eeeeee", "color": "rgba(0,0,0,0.870588)", "columns": 24, - "margins": [ - 10, - 10 - ], "backgroundSizeMode": "100%", "autoFillHeight": true, "mobileAutoFillHeight": false, - "mobileRowHeight": 70 + "mobileRowHeight": 70, + "margin": 10 } } } @@ -1155,14 +1154,11 @@ "backgroundColor": "#eeeeee", "color": "rgba(0,0,0,0.870588)", "columns": 24, - "margins": [ - 10, - 10 - ], "backgroundSizeMode": "100%", "autoFillHeight": true, "mobileAutoFillHeight": false, - "mobileRowHeight": 70 + "mobileRowHeight": 70, + "margin": 10 } } } @@ -1211,14 +1207,11 @@ "backgroundColor": "#eeeeee", "color": "rgba(0,0,0,0.870588)", "columns": 24, - "margins": [ - 10, - 10 - ], "backgroundSizeMode": "auto 100%", "autoFillHeight": true, "mobileAutoFillHeight": true, - "mobileRowHeight": 70 + "mobileRowHeight": 70, + "margin": 10 } } } diff --git a/application/src/main/data/json/demo/dashboards/thermostats.json b/application/src/main/data/json/demo/dashboards/thermostats.json index f91d987447..06edbcbef0 100644 --- a/application/src/main/data/json/demo/dashboards/thermostats.json +++ b/application/src/main/data/json/demo/dashboards/thermostats.json @@ -50,6 +50,8 @@ "datasources": [ { "type": "entity", + "name": null, + "entityAliasId": "68a058e1-fdda-8482-715b-3ae4a488568e", "dataKeys": [ { "name": "active", @@ -92,9 +94,36 @@ "_hash": 0.5726727868178358, "units": "%", "decimals": 0 + }, + { + "name": "latitude", + "type": "attribute", + "label": "latitude", + "color": "#ffc107", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "cellContentFunction": "" + }, + "_hash": 0.16055765877264894 + }, + { + "name": "longitude", + "type": "attribute", + "label": "longitude", + "color": "#607d8b", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "cellContentFunction": "" + }, + "_hash": 0.10969512220289346 } - ], - "entityAliasId": "68a058e1-fdda-8482-715b-3ae4a488568e" + ] } ], "showTitleIcon": false, @@ -115,13 +144,14 @@ "setEntityId": false }, { - "id": "8ab5a518-67d2-b6a2-956d-81fd512294b2", "name": "Add", "icon": "add", "type": "customPretty", - "customHtml": "\n
\n \n
\n

Add thermostat

\n \n \n \n \n
\n
\n \n
\n \n \n \n
\n
Thermostat name is required.
\n
\n
\n \n High temperature alarm\n \n \n \n \n
\n
High temperature threshold is required.
\n
\n
\n \n Low humidity alarm\n \n \n \n \n
\n
Low humidity threshold is required.
\n
\n
\n
\n
\n \n Create\n Cancel\n \n
\n
", - "customCss": ".add-entity-form md-input-container {\n padding-right: 10px;\n}\n\n.add-entity-form .boolean-value-input {\n padding-left: 5px;\n}\n\n.add-entity-form .boolean-value-input .checkbox-label {\n margin-bottom: 8px;\n color: rgba(0,0,0,0.54);\n font-size: 12px;\n}\n\n", - "customFunction": "var $injector = widgetContext.$scope.$injector;\nvar $mdDialog = $injector.get('$mdDialog'),\n $document = $injector.get('$document'),\n $q = $injector.get('$q'),\n $rootScope = $injector.get('$rootScope'),\n deviceService = $injector.get('deviceService'),\n attributeService = $injector.get('attributeService');\n\nopenAddEntityDialog();\n\nfunction openAddEntityDialog() {\n $mdDialog.show({\n controller: ['$scope','$mdDialog', AddEntityDialogController],\n controllerAs: 'vm',\n template: htmlTemplate,\n locals: {\n entityId: entityId\n },\n parent: angular.element($document[0].body),\n targetEvent: $event,\n multiple: true,\n clickOutsideToClose: false\n });\n}\n\nfunction AddEntityDialogController($scope, $mdDialog) {\n var vm = this;\n vm.attributes = {};\n\n vm.save = function() {\n $scope.addEntityForm.$setPristine();\n saveEntityPromise().then(\n function (entity) {\n saveAttributes(entity.id).then(() => {\n updateAliasData();\n $mdDialog.hide();\n });\n }\n );\n };\n vm.cancel = function() {\n $mdDialog.hide();\n };\n \n \n function saveEntityPromise() {\n var entity = {\n name: vm.entityName,\n type: \"thermostat\"\n };\n return deviceService.saveDevice(entity);\n }\n \n function saveAttributes(entityId) {\n var attributesArray = [];\n for (var key in vm.attributes) {\n attributesArray.push({key: key, value: vm.attributes[key]});\n }\n if (attributesArray.length > 0) {\n return attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \"SERVER_SCOPE\", attributesArray);\n } else {\n return $q.when(null);\n } \n }\n \n function updateAliasData() {\n var aliasIds = [];\n for (var id in widgetContext.aliasController.resolvedAliases) {\n aliasIds.push(id);\n }\n var tasks = [];\n aliasIds.forEach(function(aliasId) {\n widgetContext.aliasController.setAliasUnresolved(aliasId);\n tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\n });\n $q.all(tasks).then(function() {\n $rootScope.$broadcast('widgetForceReInit');\n });\n }\n}" + "customHtml": "
\n \n

Add thermostat

\n \n \n
\n \n \n
\n
\n \n Thermostat name\n \n \n Thermostat name is required.\n \n \n
\n \n High temperature alarm\n \n \n High temperature threshold, °C\n \n \n High temperature threshold is required.\n \n \n \n \n Low humidity alarm\n \n \n \n Low humidity threshold, %\n \n \n Low humidity threshold is required.\n \n \n
\n
\n
\n \n \n
\n
", + "customCss": ".add-entity-form{\n width: 300px;\n}\n", + "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\n\nopenAddEntityDialog();\n\nfunction openAddEntityDialog() {\n customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();\n}\n\nfunction AddEntityDialogController(instance) {\n let vm = instance;\n \n vm.addEntityFormGroup = vm.fb.group({\n entityName: ['', [vm.validators.required]],\n attributes: vm.fb.group({\n alarmTemperature: [false],\n thresholdTemperature: [{value: null, disabled: true}],\n alarmHumidity: [false],\n thresholdHumidity: [{value: null, disabled: true}]\n })\n });\n \n vm.addEntityFormGroup.get('attributes').get('alarmTemperature').valueChanges\n .subscribe(activate => {\n if (activate) {\n vm.addEntityFormGroup.get('attributes').get('thresholdTemperature').enable();\n } else {\n vm.addEntityFormGroup.get('attributes').get('thresholdTemperature').disable();\n }\n });\n \n vm.addEntityFormGroup.get('attributes').get('alarmHumidity').valueChanges\n .subscribe(activate => {\n if (activate) {\n vm.addEntityFormGroup.get('attributes').get('thresholdHumidity').enable();\n } else {\n vm.addEntityFormGroup.get('attributes').get('thresholdHumidity').disable();\n }\n });\n\n vm.save = function() {\n vm.addEntityFormGroup.markAsPristine();\n saveEntityObservable().subscribe(\n function (entity) {\n saveAttributes(entity.id).subscribe(\n function () {\n widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n }\n );\n };\n \n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n \n function saveEntityObservable() {\n const formValues = vm.addEntityFormGroup.value;\n let entity = {\n name: formValues.entityName,\n type: \"thermostat\"\n };\n return deviceService.saveDevice(entity);\n }\n \n function saveAttributes(entityId) {\n let attributes = vm.addEntityFormGroup.get('attributes').value;\n let attributesArray = [];\n for (let key in attributes) {\n if(attributes[key] !== null) {\n attributesArray.push({key: key, value: attributes[key]});\n }\n }\n if (attributesArray.length > 0) {\n return attributeService.saveEntityAttributes(entityId, \"SERVER_SCOPE\", attributesArray);\n } else {\n return widgetContext.rxjs.of([]);\n }\n }\n}", + "customResources": [], + "id": "8ab5a518-67d2-b6a2-956d-81fd512294b2" } ], "actionCellButton": [ @@ -134,20 +164,21 @@ "setEntityId": true }, { - "id": "7506576f-87ba-d3a0-88fb-e304d451776d", "name": "Edit", "icon": "edit", "type": "customPretty", - "customHtml": "\n
\n \n
\n

Edit thermostat {{vm.entityName}}

\n \n \n \n \n
\n
\n \n
\n \n \n \n \n \n High temperature alarm\n \n \n \n \n
\n
High temperature threshold is required.
\n
\n
\n \n Low humidity alarm\n \n \n \n \n
\n
Low humidity threshold is required.
\n
\n
\n
\n
\n \n Save\n Cancel\n \n
\n
", - "customCss": ".edit-entity-form md-input-container {\n padding-right: 10px;\n}\n\n.edit-entity-form .boolean-value-input {\n padding-left: 5px;\n}\n\n.edit-entity-form .boolean-value-input .checkbox-label {\n margin-bottom: 8px;\n color: rgba(0,0,0,0.54);\n font-size: 12px;\n}\n", - "customFunction": "var $injector = widgetContext.$scope.$injector;\nvar $mdDialog = $injector.get('$mdDialog'),\n $document = $injector.get('$document'),\n attributeService = $injector.get('attributeService');\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n $mdDialog.show({\n controller: ['$scope','$mdDialog', EditEntityDialogController],\n controllerAs: 'vm',\n template: htmlTemplate,\n locals: {\n entityId: entityId\n },\n parent: angular.element($document[0].body),\n targetEvent: $event,\n multiple: true,\n clickOutsideToClose: false\n });\n}\n\nfunction EditEntityDialogController($scope,$mdDialog) {\n var vm = this;\n vm.entityId = entityId;\n vm.entityName = entityName;\n vm.entityType = entityId.entityType;\n vm.attributes = {};\n vm.serverAttributes = {};\n getEntityInfo();\n \n vm.save = function() {\n saveAttributes();\n $mdDialog.hide();\n };\n vm.cancel = function() {\n $mdDialog.hide();\n };\n \n function getEntityAttributes(attributes) {\n for (var i = 0; i < attributes.length; i++) {\n vm.attributes[attributes[i].key] = attributes[i].value; \n }\n vm.serverAttributes = angular.copy(vm.attributes);\n }\n \n function getEntityInfo() {\n attributeService.getEntityAttributesValues(entityId.entityType, entityId.id, 'SERVER_SCOPE').then(\n function(data){\n if (data.length) {\n getEntityAttributes(data);\n }\n });\n }\n \n function saveAttributes() {\n var attributesArray = [];\n for (var key in vm.attributes) {\n if (vm.attributes[key] !== vm.serverAttributes[key]) {\n attributesArray.push({key: key, value: vm.attributes[key]});\n }\n }\n if (attributesArray.length > 0) {\n attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \"SERVER_SCOPE\", attributesArray);\n } \n }\n}" + "customHtml": "
\n \n

Edit thermostat {{entityName}}

\n \n \n
\n \n \n
\n
\n \n Thermostat name\n \n \n
\n \n High temperature alarm\n \n \n High temperature threshold, °C\n \n \n High temperature threshold is required.\n \n \n\n \n Low humidity alarm\n \n\n \n Low humidity threshold, %\n \n \n Low humidity threshold is required.\n \n \n
\n
\n
\n \n \n
\n
", + "customCss": ".edit-entity-form{\n width: 300px;\n}", + "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n \n vm.entityId = entityId;\n vm.entityName = entityName;\n vm.attributes = {};\n \n vm.editEntityFormGroup = vm.fb.group({\n entityName: [''],\n attributes: vm.fb.group({\n alarmTemperature: [false],\n thresholdTemperature: [{value: null, disabled: true}],\n alarmHumidity: [false],\n thresholdHumidity: [{value: null, disabled: true}]\n })\n });\n \n vm.editEntityFormGroup.get('attributes').get('alarmTemperature').valueChanges\n .subscribe(activate => {\n if (activate) {\n vm.editEntityFormGroup.get('attributes').get('thresholdTemperature').enable();\n } else {\n vm.editEntityFormGroup.get('attributes').get('thresholdTemperature').disable();\n }\n });\n \n vm.editEntityFormGroup.get('attributes').get('alarmHumidity').valueChanges\n .subscribe(activate => {\n if (activate) {\n vm.editEntityFormGroup.get('attributes').get('thresholdHumidity').enable();\n } else {\n vm.editEntityFormGroup.get('attributes').get('thresholdHumidity').disable();\n }\n });\n \n \n getEntityInfo();\n \n \n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveAttributes(entityId).subscribe(\n function () {\n vm.dialogRef.close(null);\n }\n );\n };\n \n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n \n function getEntityAttributes(attributes) {\n for (var i = 0; i < attributes.length; i++) {\n vm.attributes[attributes[i].key] = attributes[i].value;\n }\n }\n \n function getEntityInfo() {\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE').subscribe(\n function (attributes) {\n getEntityAttributes(attributes);\n vm.editEntityFormGroup.patchValue({\n entityName: vm.entityName,\n attributes: vm.attributes\n });\n // if(vm.attributes.alarmTemperature) {\n // vm.editEntityFormGroup.get('attributes').get('thresholdTemperature').enable();\n // }\n // if(vm.attributes.alarmHumidity) {\n // vm.editEntityFormGroup.get('attributes').get('thresholdHumidity').enable();\n // }\n }\n );\n }\n \n function saveAttributes(entityId) {\n let attributes = vm.editEntityFormGroup.get('attributes').value;\n let attributesArray = [];\n for (let key in attributes) {\n if (attributes[key] !== vm.attributes[key]) {\n attributesArray.push({key: key, value: attributes[key]});\n }\n }\n if (attributesArray.length > 0) {\n return attributeService.saveEntityAttributes(entityId, \"SERVER_SCOPE\", attributesArray);\n } else {\n return widgetContext.rxjs.of([]);\n }\n }\n}", + "customResources": [], + "id": "7506576f-87ba-d3a0-88fb-e304d451776d" }, { - "id": "3488848b-e47d-6af6-659f-5d78369ece5e", "name": "Delete", "icon": "delete", "type": "custom", - "customFunction": "var $injector = widgetContext.$scope.$injector;\nvar $mdDialog = $injector.get('$mdDialog'),\n $document = $injector.get('$document'),\n deviceService = $injector.get('deviceService')\n $rootScope = $injector.get('$rootScope'),\n $q = $injector.get('$q');\n\nopenDeleteEntityDialog();\n\nfunction openDeleteEntityDialog() {\n var title = 'Delete thermostat \"' + entityName + '\"';\n var content = 'Are you sure you want to delete the thermostat \"' +\n entityName + '\"?';\n var confirm = $mdDialog.confirm()\n .targetEvent($event)\n .title(title)\n .htmlContent(content)\n .ariaLabel(title)\n .cancel('Cancel')\n .ok('Delete');\n $mdDialog.show(confirm).then(function() {\n deleteEntity();\n })\n}\n\nfunction deleteEntity() {\n deviceService.deleteDevice(entityId.id).then(\n function success() {\n updateAliasData();\n },\n function fail() {\n showErrorDialog();\n }\n );\n}\n\nfunction updateAliasData() {\n var aliasIds = [];\n for (var id in widgetContext.aliasController.resolvedAliases) {\n aliasIds.push(id);\n }\n var tasks = [];\n aliasIds.forEach(function(aliasId) {\n widgetContext.aliasController.setAliasUnresolved(aliasId);\n tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\n });\n $q.all(tasks).then(function() {\n $rootScope.$broadcast('entityAliasesChanged', aliasIds);\n });\n}\n\nfunction showErrorDialog() {\n var title = 'Error';\n var content = 'An error occurred while deleting the thermostat. Please try again.';\n var alert = $mdDialog.alert()\n .title(title)\n .htmlContent(content)\n .ariaLabel(title)\n .parent(angular.element($document[0].body))\n .targetEvent($event)\n .multiple(true)\n .clickOutsideToClose(true)\n .ok('CLOSE');\n $mdDialog.show(alert);\n}" + "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet dialogs = $injector.get(widgetContext.servicesMap.get('dialogs'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenDeleteEntityDialog();\n\nfunction openDeleteEntityDialog() {\n let title = 'Delete thermostat \"' + entityName + '\"';\n let content = 'Are you sure you want to delete the thermostat \"' +\n entityName + '\"?';\n dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe(\n function(result) {\n if (result) {\n deleteEntity();\n }\n }\n );\n}\n\nfunction deleteEntity() {\n deviceService.deleteDevice(entityId.id).subscribe(\n function success() {\n widgetContext.updateAliases();\n },\n function fail() {\n showErrorDialog();\n }\n );\n}\n\nfunction showErrorDialog() {\n let title = 'Error';\n let content = 'An error occurred while deleting the thermostat. Please try again.';\n dialogs.alert(title, content, 'CLOSE').subscribe(\n function(result) {}\n );\n}", + "id": "3488848b-e47d-6af6-659f-5d78369ece5e" } ], "rowClick": [] @@ -848,10 +879,7 @@ "mapProvider": "OpenStreetMap.HOT", "showTooltip": true, "autocloseTooltip": true, - "defaultCenterPosition": [ - 0, - 0 - ], + "defaultCenterPosition": "0,0", "customProviderTileUrl": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", "showTooltipAction": "click", "polygonKeyName": "coordinates", @@ -866,7 +894,9 @@ "defaultZoomLevel": 12, "labelFunction": "var color;\nif(dsData[dsIndex].active !== \"true\"){\n color = 'rgb(255, 0, 0)';\n} else {\n color = 'rgb(39, 134, 34)';\n}\nreturn '' + \n '${entityLabel}' + \n ''", "markerImageFunction": "var res;\nif(dsData[dsIndex].active !== \"true\"){\n\tvar res = {\n\t url: images[0],\n\t size: 48\n\t}\n} else {\n var res = {\n\t url: images[1],\n\t size: 48\n\t}\n}\nreturn res;", - "useLabelFunction": true + "useLabelFunction": true, + "provider": "openstreet-map", + "draggableMarker": true }, "title": "New Markers Placement - OpenStreetMap", "dropShadow": true, @@ -881,11 +911,11 @@ "actions": { "tooltipAction": [ { - "id": "54c293c4-9ca6-e34f-dc6a-0271944c1c66", "name": "delete", "icon": "more_horiz", "type": "custom", - "customFunction": "var $rootScope = widgetContext.$scope.$injector.get('$rootScope');\nvar entityDatasource = widgetContext.map.subscription.datasources.filter(\n function(entity) {\n return entity.entityId === entityId.id\n });\n\nwidgetContext.map.saveMarkerLocation(entityDatasource[0],\n widgetContext.map.locations[0], {\n \"lat\": null,\n \"lng\": null\n }).then(function succes() {\n $rootScope.$broadcast('widgetForceReInit');\n });" + "customFunction": "var entityDatasource = widgetContext.mapInstance.datasources.filter(\n function(entity) {\n return entity.entityId === entityId.id\n });\n\nwidgetContext.mapInstance.saveMarkerLocation(entityDatasource[0], null, null).subscribe(function success() {\n widgetContext.updateAliases();\n});", + "id": "54c293c4-9ca6-e34f-dc6a-0271944c1c66" } ] }, @@ -984,10 +1014,7 @@ "mapProvider": "OpenStreetMap.HOT", "showTooltip": true, "autocloseTooltip": true, - "defaultCenterPosition": [ - 37.7749, - -122.4194 - ], + "defaultCenterPosition": "0,0", "customProviderTileUrl": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", "showTooltipAction": "click", "polygonKeyName": "coordinates", @@ -1003,7 +1030,8 @@ "labelFunction": "var color;\nif(dsData[dsIndex].active !== \"true\"){\n color = 'rgb(255, 0, 0)';\n} else {\n color = 'rgb(39, 134, 34)';\n}\nreturn '' + \n '${entityLabel}' + \n ''", "markerImageFunction": "var res;\nif(dsData[dsIndex].active !== \"true\"){\n\tvar res = {\n\t url: images[0],\n\t size: 48\n\t}\n} else {\n var res = {\n\t url: images[1],\n\t size: 48\n\t}\n}\nreturn res;", "useLabelFunction": true, - "useDefaultCenterPosition": true + "provider": "openstreet-map", + "draggableMarker": true }, "title": "New Markers Placement - OpenStreetMap", "dropShadow": true, @@ -1018,11 +1046,11 @@ "actions": { "tooltipAction": [ { - "id": "54c293c4-9ca6-e34f-dc6a-0271944c1c66", "name": "delete", "icon": "more_horiz", "type": "custom", - "customFunction": "var $rootScope = widgetContext.$scope.$injector.get('$rootScope');\nvar entityDatasource = widgetContext.map.subscription.datasources.filter(\n function(entity) {\n return entity.entityId === entityId.id\n });\n\nwidgetContext.map.saveMarkerLocation(entityDatasource[0],\n widgetContext.map.locations[0], {\n \"lat\": null,\n \"lng\": null\n }).then(function succes() {\n $rootScope.$broadcast('widgetForceReInit');\n });" + "customFunction": "var entityDatasource = widgetContext.mapInstance.datasources.filter(\n function(entity) {\n return entity.entityId === entityId.id\n });\n\nwidgetContext.mapInstance.saveMarkerLocation(entityDatasource[0], null, null).subscribe(function success() {\n widgetContext.updateAliases();\n});", + "id": "54c293c4-9ca6-e34f-dc6a-0271944c1c66" } ] }, @@ -1034,6 +1062,61 @@ "displayTimewindow": true }, "id": "0a430429-9078-9ae6-2b67-e4a15a2bf8bf" + }, + "f4bb2f2d-0164-60bc-f3e8-9b1e7b5a59b3": { + "isSystemType": true, + "bundleAlias": "input_widgets", + "typeAlias": "update_double_timeseries", + "type": "latest", + "title": "New widget", + "sizeX": 7.5, + "sizeY": 3, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "12ae98c7-1ea2-52cf-64d5-763e9d993547", + "dataKeys": [ + { + "name": "temperature", + "type": "timeseries", + "label": "temperature", + "color": "#2196f3", + "settings": {}, + "_hash": 0.4164505192982848 + } + ] + } + ], + "timewindow": { + "realtime": { + "timewindowMs": 60000 + } + }, + "showTitle": true, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "8px", + "settings": { + "showResultMessage": true, + "showLabel": true + }, + "title": "New Update double timeseries", + "dropShadow": true, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "useDashboardTimewindow": true, + "showLegend": false, + "actions": {} + }, + "row": 0, + "col": 0, + "id": "f4bb2f2d-0164-60bc-f3e8-9b1e7b5a59b3" } }, "states": { @@ -1066,14 +1149,11 @@ "backgroundColor": "#eeeeee", "color": "rgba(0,0,0,0.870588)", "columns": 24, - "margins": [ - 10, - 10 - ], "backgroundSizeMode": "100%", "autoFillHeight": true, "mobileAutoFillHeight": false, - "mobileRowHeight": 70 + "mobileRowHeight": 70, + "margin": 10 } } } @@ -1095,14 +1175,11 @@ "backgroundColor": "#eeeeee", "color": "rgba(0,0,0,0.870588)", "columns": 24, - "margins": [ - 10, - 10 - ], "backgroundSizeMode": "100%", "autoFillHeight": true, "mobileAutoFillHeight": false, - "mobileRowHeight": 70 + "mobileRowHeight": 70, + "margin": 10 } } } @@ -1138,20 +1215,23 @@ "sizeY": 6, "row": 6, "col": 0 + }, + "f4bb2f2d-0164-60bc-f3e8-9b1e7b5a59b3": { + "sizeX": 7.5, + "sizeY": 3, + "row": 12, + "col": 0 } }, "gridSettings": { "backgroundColor": "#eeeeee", "color": "rgba(0,0,0,0.870588)", "columns": 24, - "margins": [ - 10, - 10 - ], "backgroundSizeMode": "100%", "autoFillHeight": true, "mobileAutoFillHeight": false, - "mobileRowHeight": 70 + "mobileRowHeight": 70, + "margin": 10 } } } diff --git a/application/src/main/data/json/system/oauth2_config_templates/facebook_config.json b/application/src/main/data/json/system/oauth2_config_templates/facebook_config.json new file mode 100644 index 0000000000..04847a9da6 --- /dev/null +++ b/application/src/main/data/json/system/oauth2_config_templates/facebook_config.json @@ -0,0 +1,23 @@ +{ + "providerId": "Facebook", + "accessTokenUri": "https://graph.facebook.com/v2.8/oauth/access_token", + "authorizationUri": "https://www.facebook.com/v2.8/dialog/oauth", + "scope": ["email","public_profile"], + "jwkSetUri": null, + "userInfoUri": "https://graph.facebook.com/me?fields=id,name,first_name,last_name,email", + "clientAuthenticationMethod": "BASIC", + "userNameAttributeName": "email", + "mapperConfig": { + "type": "BASIC", + "basic": { + "emailAttributeKey": "email", + "firstNameAttributeKey": "first_name", + "lastNameAttributeKey": "last_name", + "tenantNameStrategy": "DOMAIN" + } + }, + "comment": null, + "loginButtonIcon": "facebook-logo", + "loginButtonLabel": "Facebook", + "helpLink": "https://developers.facebook.com/docs/facebook-login/web#logindialog" +} diff --git a/application/src/main/data/json/system/oauth2_config_templates/github_config.json b/application/src/main/data/json/system/oauth2_config_templates/github_config.json new file mode 100644 index 0000000000..439043d961 --- /dev/null +++ b/application/src/main/data/json/system/oauth2_config_templates/github_config.json @@ -0,0 +1,21 @@ +{ + "providerId": "Github", + "accessTokenUri": "https://github.com/login/oauth/access_token", + "authorizationUri": "https://github.com/login/oauth/authorize", + "scope": ["read:user","user:email"], + "jwkSetUri": null, + "userInfoUri": "https://api.github.com/user", + "clientAuthenticationMethod": "BASIC", + "userNameAttributeName": "login", + "mapperConfig": { + "type": "GITHUB", + "basic": { + "firstNameAttributeKey": "name", + "tenantNameStrategy": "DOMAIN" + } + }, + "comment": "In order to log into ThingsBoard you need to have user's email. You may configure and use Custom OAuth2 Mapper to get email information. Please refer to Github Documentation", + "loginButtonIcon": "github-logo", + "loginButtonLabel": "Github", + "helpLink": "https://docs.github.com/en/developers/apps/creating-an-oauth-app" +} diff --git a/application/src/main/data/json/system/oauth2_config_templates/google_config.json b/application/src/main/data/json/system/oauth2_config_templates/google_config.json new file mode 100644 index 0000000000..f8626439ba --- /dev/null +++ b/application/src/main/data/json/system/oauth2_config_templates/google_config.json @@ -0,0 +1,24 @@ +{ + "providerId": "Google", + "additionalInfo": null, + "accessTokenUri": "https://oauth2.googleapis.com/token", + "authorizationUri": "https://accounts.google.com/o/oauth2/v2/auth", + "scope": ["email","openid","profile"], + "jwkSetUri": "https://www.googleapis.com/oauth2/v3/certs", + "userInfoUri": "https://openidconnect.googleapis.com/v1/userinfo", + "clientAuthenticationMethod": "BASIC", + "userNameAttributeName": "email", + "mapperConfig": { + "type": "BASIC", + "basic": { + "emailAttributeKey": "email", + "firstNameAttributeKey": "given_name", + "lastNameAttributeKey": "family_name", + "tenantNameStrategy": "DOMAIN" + } + }, + "comment": null, + "loginButtonIcon": "google-logo", + "loginButtonLabel": "Google", + "helpLink": "https://developers.google.com/adwords/api/docs/guides/authentication" +} diff --git a/application/src/main/data/json/system/widget_bundles/alarm_widgets.json b/application/src/main/data/json/system/widget_bundles/alarm_widgets.json index 2daf3e27c8..697ce4bbae 100644 --- a/application/src/main/data/json/system/widget_bundles/alarm_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/alarm_widgets.json @@ -13,13 +13,13 @@ "sizeX": 10.5, "sizeY": 6.5, "resources": [], - "templateHtml": "\n", + "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('alarms-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"AlarmTableSettings\",\n \"properties\": {\n \"alarmsTitle\": {\n \"title\": \"Alarms table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSelection\": {\n \"title\": \"Enable alarms selection\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSearch\": {\n \"title\": \"Enable alarms search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStatusFilter\": {\n \"title\": \"Enable alarm status filter\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayDetails\": {\n \"title\": \"Display alarm details\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowAcknowledgment\": {\n \"title\": \"Allow alarms acknowledgment\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowClear\": {\n \"title\": \"Allow alarms clear\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"-createdTime\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"alarmsTitle\",\n \"enableSelection\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"enableStatusFilter\",\n \"displayDetails\",\n \"allowAcknowledgment\",\n \"allowClear\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.alarmsTableWidget.onDataUpdated();\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"AlarmTableSettings\",\n \"properties\": {\n \"alarmsTitle\": {\n \"title\": \"Alarms table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSelection\": {\n \"title\": \"Enable alarms selection\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSearch\": {\n \"title\": \"Enable alarms search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableFilter\": {\n \"title\": \"Enable alarm filter\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyAction\": {\n \"title\": \"Always display actions column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"displayDetails\": {\n \"title\": \"Display alarm details\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowAcknowledgment\": {\n \"title\": \"Allow alarms acknowledgment\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowClear\": {\n \"title\": \"Allow alarms clear\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"-createdTime\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"alarmsTitle\",\n \"enableSelection\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"enableFilter\",\n \"enableStickyAction\",\n \"displayDetails\",\n \"allowAcknowledgment\",\n \"allowClear\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}", "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, alarm, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}", - "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStatusFilter\":true},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{}}" + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStickyAction\":false,\"enableFilter\":true},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{},\"alarmStatusList\":[],\"alarmSeverityList\":[],\"alarmTypeList\":[],\"searchPropagatedAlarms\":false}" } } ] -} +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/analogue_gauges.json b/application/src/main/data/json/system/widget_bundles/analogue_gauges.json index 4ecbc73429..95845bad1b 100644 --- a/application/src/main/data/json/system/widget_bundles/analogue_gauges.json +++ b/application/src/main/data/json/system/widget_bundles/analogue_gauges.json @@ -6,35 +6,19 @@ }, "widgetTypes": [ { - "alias": "radial_gauge_canvas_gauges", - "name": "Radial gauge - Canvas Gauges", + "alias": "analogue_compass", + "name": "Analogue Compass", "descriptor": { "type": "latest", "sizeX": 6, "sizeY": 5, "resources": [], - "templateHtml": "\n", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbAnalogueRadialGauge(self.ctx, 'radialGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.getSettingsSchema = function() {\n return TbAnalogueRadialGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nif (value < -100) {\\n\\tvalue = -100;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":100,\"startAngle\":45,\"ticksAngle\":270,\"showBorder\":true,\"defaultColor\":\"#e65100\",\"needleCircleSize\":10,\"highlights\":[],\"showUnitTitle\":true,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":10,\"valueInt\":3,\"valueDec\":0,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"numbersFont\":{\"family\":\"Roboto\",\"size\":18,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":22,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"size\":36,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\"},\"minValue\":-100,\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\"},\"title\":\"Radial gauge - Canvas Gauges\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" - } - }, - { - "alias": "speed_gauge_canvas_gauges", - "name": "Speed gauge - Canvas Gauges", - "descriptor": { - "type": "latest", - "sizeX": 7, - "sizeY": 5, - "resources": [], - "templateHtml": "\n", + "templateHtml": "", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbAnalogueRadialGauge(self.ctx, 'radialGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.getSettingsSchema = function() {\n return TbAnalogueRadialGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n };\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbAnalogueCompass(self.ctx, 'compass');\n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.getSettingsSchema = function() {\n return TbAnalogueCompass.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 220) {\\n\\tvalue = 220;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":180,\"startAngle\":45,\"ticksAngle\":270,\"showBorder\":false,\"defaultColor\":\"#e65100\",\"needleCircleSize\":7,\"highlights\":[{\"from\":80,\"to\":120,\"color\":\"#fdd835\"},{\"color\":\"#e57373\",\"from\":120,\"to\":180}],\"showUnitTitle\":false,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":2,\"valueInt\":3,\"minValue\":0,\"valueDec\":0,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":1500,\"animationRule\":\"linear\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188, 143, 143, 0.78)\",\"units\":\"MPH\",\"majorTicksCount\":9,\"numbersFont\":{\"family\":\"Roboto\",\"size\":22,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":28,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"size\":32,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\",\"family\":\"Segment7Standard\"},\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\"},\"title\":\"Speed gauge - Canvas Gauges\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "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\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"minorTicks\":22,\"needleCircleSize\":15,\"showBorder\":true,\"borderOuterWidth\":10,\"colorPlate\":\"#222\",\"colorMajorTicks\":\"#f5f5f5\",\"colorMinorTicks\":\"#ddd\",\"colorNeedle\":\"#f08080\",\"colorNeedleCircle\":\"#e8e8e8\",\"colorBorder\":\"#ccc\",\"majorTickFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#ccc\"},\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\",\"animationTarget\":\"needle\",\"majorTicks\":[\"N\",\"NE\",\"E\",\"SE\",\"S\",\"SW\",\"W\",\"NW\"]},\"title\":\"Analogue Compass\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { @@ -47,7 +31,7 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbAnalogueLinearGauge(self.ctx, 'linearGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.getSettingsSchema = function() {\n return TbAnalogueLinearGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n };\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbAnalogueLinearGauge(self.ctx, 'linearGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.getSettingsSchema = function() {\n return TbAnalogueLinearGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 30 - 15;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":100,\"defaultColor\":\"#e64a19\",\"barStrokeWidth\":2.5,\"colorBar\":\"rgba(255, 255, 255, 0.4)\",\"colorBarEnd\":\"rgba(221, 221, 221, 0.38)\",\"showUnitTitle\":true,\"minorTicks\":2,\"valueBox\":true,\"valueInt\":3,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"colorNeedleShadowUp\":\"rgba(2,255,255,0.2)\",\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\",\"highlightsWidth\":10,\"animation\":true,\"animationDuration\":1500,\"animationRule\":\"linear\",\"showBorder\":false,\"majorTicksCount\":8,\"numbersFont\":{\"family\":\"Arial\",\"size\":18,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#78909c\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":26,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#37474f\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":40,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#444\",\"shadowColor\":\"rgba(0,0,0,0.3)\"},\"minValue\":-60,\"highlights\":[{\"from\":-60,\"to\":-40,\"color\":\"#90caf9\"},{\"from\":-40,\"to\":-20,\"color\":\"rgba(144, 202, 249, 0.66)\"},{\"from\":-20,\"to\":0,\"color\":\"rgba(144, 202, 249, 0.33)\"},{\"from\":0,\"to\":20,\"color\":\"rgba(244, 67, 54, 0.2)\"},{\"from\":20,\"to\":40,\"color\":\"rgba(244, 67, 54, 0.4)\"},{\"from\":40,\"to\":60,\"color\":\"rgba(244, 67, 54, 0.6)\"},{\"from\":60,\"to\":80,\"color\":\"rgba(244, 67, 54, 0.8)\"},{\"from\":80,\"to\":100,\"color\":\"#f44336\"}],\"unitTitle\":\"Temperature\",\"units\":\"°C\",\"colorBarProgress\":\"#90caf9\",\"colorBarProgressEnd\":\"#f44336\",\"colorBarStroke\":\"#b0bec5\",\"valueDec\":1},\"title\":\"Temperature gauge - Canvas Gauges\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" @@ -63,26 +47,42 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbAnalogueRadialGauge(self.ctx, 'radialGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.getSettingsSchema = function() {\n return TbAnalogueRadialGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n };\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbAnalogueRadialGauge(self.ctx, 'radialGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.getSettingsSchema = function() {\n return TbAnalogueRadialGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":60,\"startAngle\":67.5,\"ticksAngle\":225,\"showBorder\":true,\"defaultColor\":\"#e65100\",\"needleCircleSize\":7,\"highlights\":[{\"from\":-60,\"to\":-50,\"color\":\"#42a5f5\"},{\"from\":-50,\"to\":-40,\"color\":\"rgba(66, 165, 245, 0.83)\"},{\"from\":-40,\"to\":-30,\"color\":\"rgba(66, 165, 245, 0.66)\"},{\"from\":-30,\"to\":-20,\"color\":\"rgba(66, 165, 245, 0.5)\"},{\"from\":-20,\"to\":-10,\"color\":\"rgba(66, 165, 245, 0.33)\"},{\"from\":-10,\"to\":0,\"color\":\"rgba(66, 165, 245, 0.16)\"},{\"from\":0,\"to\":10,\"color\":\"rgba(229, 115, 115, 0.16)\"},{\"from\":10,\"to\":20,\"color\":\"rgba(229, 115, 115, 0.33)\"},{\"from\":20,\"to\":30,\"color\":\"rgba(229, 115, 115, 0.5)\"},{\"from\":30,\"to\":40,\"color\":\"rgba(229, 115, 115, 0.66)\"},{\"from\":40,\"to\":50,\"color\":\"rgba(229, 115, 115, 0.83)\"},{\"from\":50,\"to\":60,\"color\":\"#e57373\"}],\"showUnitTitle\":true,\"colorPlate\":\"#cfd8dc\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":2,\"valueInt\":3,\"valueDec\":1,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":1000,\"animationRule\":\"bounce\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188, 143, 143, 0.78)\",\"units\":\"°C\",\"majorTicksCount\":12,\"numbersFont\":{\"family\":\"Roboto\",\"size\":20,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":28,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"size\":30,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\"},\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\",\"unitTitle\":\"Temperature\",\"minValue\":-60},\"title\":\"Temperature radial gauge - Canvas Gauges\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" } }, { - "alias": "analogue_compass", - "name": "Analogue Compass", + "alias": "speed_gauge_canvas_gauges", + "name": "Speed gauge - Canvas Gauges", + "descriptor": { + "type": "latest", + "sizeX": 7, + "sizeY": 5, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbAnalogueRadialGauge(self.ctx, 'radialGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.getSettingsSchema = function() {\n return TbAnalogueRadialGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}\n", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 220) {\\n\\tvalue = 220;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":180,\"startAngle\":45,\"ticksAngle\":270,\"showBorder\":false,\"defaultColor\":\"#e65100\",\"needleCircleSize\":7,\"highlights\":[{\"from\":80,\"to\":120,\"color\":\"#fdd835\"},{\"color\":\"#e57373\",\"from\":120,\"to\":180}],\"showUnitTitle\":false,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":2,\"valueInt\":3,\"minValue\":0,\"valueDec\":0,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":1500,\"animationRule\":\"linear\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188, 143, 143, 0.78)\",\"units\":\"MPH\",\"majorTicksCount\":9,\"numbersFont\":{\"family\":\"Roboto\",\"size\":22,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":28,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"size\":32,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\",\"family\":\"Segment7Standard\"},\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\"},\"title\":\"Speed gauge - Canvas Gauges\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + } + }, + { + "alias": "radial_gauge_canvas_gauges", + "name": "Radial gauge - Canvas Gauges", "descriptor": { "type": "latest", "sizeX": 6, "sizeY": 5, "resources": [], - "templateHtml": "", + "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbAnalogueCompass(self.ctx, 'compass');\n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.getSettingsSchema = function() {\n return TbAnalogueCompass.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n };\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbAnalogueRadialGauge(self.ctx, 'radialGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.getSettingsSchema = function() {\n return TbAnalogueRadialGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", - "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\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"minorTicks\":22,\"needleCircleSize\":15,\"showBorder\":true,\"borderOuterWidth\":10,\"colorPlate\":\"#222\",\"colorMajorTicks\":\"#f5f5f5\",\"colorMinorTicks\":\"#ddd\",\"colorNeedle\":\"#f08080\",\"colorNeedleCircle\":\"#e8e8e8\",\"colorBorder\":\"#ccc\",\"majorTickFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#ccc\"},\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\",\"animationTarget\":\"needle\",\"majorTicks\":[\"N\",\"NE\",\"E\",\"SE\",\"S\",\"SW\",\"W\",\"NW\"]},\"title\":\"Analogue Compass\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nif (value < -100) {\\n\\tvalue = -100;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":100,\"startAngle\":45,\"ticksAngle\":270,\"showBorder\":true,\"defaultColor\":\"#e65100\",\"needleCircleSize\":10,\"highlights\":[],\"showUnitTitle\":true,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":10,\"valueInt\":3,\"valueDec\":0,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"numbersFont\":{\"family\":\"Roboto\",\"size\":18,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":22,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"size\":36,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\"},\"minValue\":-100,\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\"},\"title\":\"Radial gauge - Canvas Gauges\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" } } ] diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index d07186fd42..0d97afd5e7 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -21,22 +21,6 @@ "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\":\"Attributes card\"}" } }, - { - "alias": "entities_table", - "name": "Entities table", - "descriptor": { - "type": "latest", - "sizeX": 7.5, - "sizeY": 6.5, - "resources": [], - "templateHtml": "\n", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('entities-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}", - "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, entity, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}", - "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"enableSelectColumnDisplay\":true},\"title\":\"Entity table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{}}" - } - }, { "alias": "html_card", "name": "HTML Card", @@ -53,6 +37,22 @@ "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"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\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"cardHtml\":\"
HTML code here
\",\"cardCss\":\".card {\\n font-weight: bold;\\n font-size: 32px;\\n color: #999;\\n width: 100%;\\n height: 100%;\\n display: flex;\\n align-items: center;\\n justify-content: center;\\n}\"},\"title\":\"HTML Card\",\"dropShadow\":true}" } }, + { + "alias": "timeseries_table", + "name": "Timeseries table", + "descriptor": { + "type": "timeseries", + "sizeX": 8, + "sizeY": 6.5, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeseriesTableWidget.onDataUpdated();\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"TimeseriesTableSettings\",\n \"properties\": {\n \"showTimestamp\": {\n \"title\": \"Display timestamp column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"showMilliseconds\": {\n \"title\": \"Display timestamp milliseconds\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"hideEmptyLines\": {\n \"title\": \"Hide empty lines\",\n \"type\": \"boolean\",\n \"default\": false\n }\n },\n \"required\": []\n },\n \"form\": [\n \"showTimestamp\",\n \"showMilliseconds\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"hideEmptyLines\"\n ]\n}", + "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, rowData, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', amount = percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\"},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true,\"displayPagination\":true,\"defaultPageSize\":10},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\"}" + } + }, { "alias": "html_value_card", "name": "HTML Value Card", @@ -63,58 +63,58 @@ "resources": [], "templateHtml": "", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.varsRegex = /\\$\\{([^\\}]*)\\}/g;\n self.ctx.htmlSet = false;\n \n var cssParser = new cssjs();\n cssParser.testMode = false;\n var namespace = 'html-value-card-' + hashCode(self.ctx.settings.cardCss);\n cssParser.cssPreviewNamespace = namespace;\n cssParser.createStyleElement(namespace, self.ctx.settings.cardCss);\n self.ctx.$container.addClass(namespace);\n var evtFnPrefix = 'htmlValueCard_' + Math.abs(hashCode(self.ctx.settings.cardCss + self.ctx.settings.cardHtml));\n self.ctx.html = '
' + \n self.ctx.settings.cardHtml + \n '
';\n\n self.ctx.replaceInfo = processHtmlPattern(self.ctx.html, self.ctx.data);\n \n updateHtml();\n \n window[evtFnPrefix + '_onClickFn'] = function (event) {\n self.ctx.actionsApi.elementClick(event);\n }\n\n function hashCode(str) {\n var hash = 0;\n var i, char;\n if (str.length === 0) return hash;\n for (i = 0; i < str.length; i++) {\n char = str.charCodeAt(i);\n hash = ((hash << 5) - hash) + char;\n hash = hash & hash;\n }\n return hash;\n }\n \n function processHtmlPattern(pattern, data) {\n var match = self.ctx.varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split(':');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n if (label == 'entityName') {\n variableInfo.isEntityName = true;\n } else if (label == 'entityLabel') {\n variableInfo.isEntityLabel = true;\n } else if (label.startsWith('#')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = n;\n }\n }\n if (!variableInfo.isEntityName && !variableInfo.isEntityLabel && variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = self.ctx.varsRegex.exec(pattern);\n }\n return replaceInfo;\n } \n}\n\nself.onDataUpdated = function() {\n updateHtml();\n}\n\nself.actionSources = function() {\n return {\n 'elementClick': {\n name: 'widget-action.element-click',\n multiple: true\n }\n };\n}\n\nself.onDestroy = function() {\n}\n\nfunction isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n}\n\nfunction padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n\n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n\n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split('.');\n s = int - strVal[0].length;\n\n for (; i < s; ++i) {\n strVal[0] = '0' + strVal[0];\n }\n\n strVal = (n ? '-' : '') + strVal[0] + '.' + strVal[1];\n }\n\n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n\n for (; i < s; ++i) {\n strVal = '0' + strVal;\n }\n\n strVal = (n ? '-' : '') + strVal;\n }\n\n return strVal;\n}\n\nfunction updateHtml() {\n var $injector = self.ctx.$scope.$injector;\n var utils = $injector.get('utils');\n var types = $injector.get('types');\n var text = self.ctx.html;\n var updated = false;\n for (var v in self.ctx.replaceInfo.variables) {\n var variableInfo = self.ctx.replaceInfo.variables[v];\n var txtVal = '';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = self.ctx.data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n } else {\n txtVal = val;\n }\n }\n } else if (variableInfo.isEntityName) {\n if (self.ctx.defaultSubscription.datasources.length) {\n txtVal = self.ctx.defaultSubscription.datasources[0].entityName;\n } else {\n txtVal = 'Unknown';\n }\n } else if (variableInfo.isEntityLabel) {\n if (self.ctx.defaultSubscription.datasources.length) {\n txtVal = self.ctx.defaultSubscription.datasources[0].entityLabel || self.ctx.defaultSubscription.datasources[0].entityName;\n } else {\n txtVal = 'Unknown';\n }\n }\n if (typeof variableInfo.lastVal === undefined ||\n variableInfo.lastVal !== txtVal) {\n updated = true;\n variableInfo.lastVal = txtVal;\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n if (updated || !self.ctx.htmlSet) {\n text = replaceCustomTranslations(text);\n self.ctx.$container.html(text);\n if (!self.ctx.htmlSet) {\n self.ctx.htmlSet = true;\n }\n }\n \n function replaceCustomTranslations (pattern) {\n var customTranslationRegex = new RegExp('{' + types.translate.i18nPrefix + ':[^{}]+}', 'g');\n pattern = pattern.replace(customTranslationRegex, getTranslationText);\n return pattern;\n }\n \n function getTranslationText (variable) {\n return utils.customTranslation(variable, variable);\n \n }\n}\n\n", + "controllerScript": "self.onInit = function() {\n self.ctx.varsRegex = /\\$\\{([^\\}]*)\\}/g;\n self.ctx.htmlSet = false;\n \n var cssParser = new cssjs();\n cssParser.testMode = false;\n var namespace = 'html-value-card-' + hashCode(self.ctx.settings.cardCss);\n cssParser.cssPreviewNamespace = namespace;\n cssParser.createStyleElement(namespace, self.ctx.settings.cardCss);\n self.ctx.$container.addClass(namespace);\n var evtFnPrefix = 'htmlValueCard_' + Math.abs(hashCode(self.ctx.settings.cardCss + self.ctx.settings.cardHtml));\n self.ctx.html = '
' + \n self.ctx.settings.cardHtml + \n '
';\n\n self.ctx.replaceInfo = processHtmlPattern(self.ctx.html, self.ctx.data);\n \n updateHtml();\n \n window[evtFnPrefix + '_onClickFn'] = function (event) {\n self.ctx.actionsApi.elementClick(event);\n }\n\n function hashCode(str) {\n var hash = 0;\n var i, char;\n if (str.length === 0) return hash;\n for (i = 0; i < str.length; i++) {\n char = str.charCodeAt(i);\n hash = ((hash << 5) - hash) + char;\n hash = hash & hash;\n }\n return hash;\n }\n \n function processHtmlPattern(pattern, data) {\n var match = self.ctx.varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split(':');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n if (label == 'entityName') {\n variableInfo.isEntityName = true;\n } else if (label == 'entityLabel') {\n variableInfo.isEntityLabel = true;\n } else if (label.startsWith('#')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = n;\n }\n }\n if (!variableInfo.isEntityName && !variableInfo.isEntityLabel && variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = self.ctx.varsRegex.exec(pattern);\n }\n return replaceInfo;\n } \n}\n\nself.onDataUpdated = function() {\n updateHtml();\n}\n\nself.actionSources = function() {\n return {\n 'elementClick': {\n name: 'widget-action.element-click',\n multiple: true\n }\n };\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n singleEntity: true,\n dataKeysOptional: true\n };\n}\n\n\nself.onDestroy = function() {\n}\n\nfunction isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n}\n\nfunction padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n\n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n\n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split('.');\n s = int - strVal[0].length;\n\n for (; i < s; ++i) {\n strVal[0] = '0' + strVal[0];\n }\n\n strVal = (n ? '-' : '') + strVal[0] + '.' + strVal[1];\n }\n\n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n\n for (; i < s; ++i) {\n strVal = '0' + strVal;\n }\n\n strVal = (n ? '-' : '') + strVal;\n }\n\n return strVal;\n}\n\nfunction updateHtml() {\n var $injector = self.ctx.$scope.$injector;\n var utils = $injector.get(self.ctx.servicesMap.get('utils'));\n var text = self.ctx.html;\n var updated = false;\n for (var v in self.ctx.replaceInfo.variables) {\n var variableInfo = self.ctx.replaceInfo.variables[v];\n var txtVal = '';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = self.ctx.data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n } else {\n txtVal = val;\n }\n }\n } else if (variableInfo.isEntityName) {\n if (self.ctx.defaultSubscription.datasources.length) {\n txtVal = self.ctx.defaultSubscription.datasources[0].entityName;\n } else {\n txtVal = 'Unknown';\n }\n } else if (variableInfo.isEntityLabel) {\n if (self.ctx.defaultSubscription.datasources.length) {\n txtVal = self.ctx.defaultSubscription.datasources[0].entityLabel || self.ctx.defaultSubscription.datasources[0].entityName;\n } else {\n txtVal = 'Unknown';\n }\n }\n if (typeof variableInfo.lastVal === undefined ||\n variableInfo.lastVal !== txtVal) {\n updated = true;\n variableInfo.lastVal = txtVal;\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n if (updated || !self.ctx.htmlSet) {\n text = replaceCustomTranslations(text);\n self.ctx.$container.html(text);\n if (!self.ctx.htmlSet) {\n self.ctx.htmlSet = true;\n }\n }\n \n function replaceCustomTranslations (pattern) {\n var customTranslationRegex = new RegExp('{i18n:[^{}]+}', 'g');\n pattern = pattern.replace(customTranslationRegex, getTranslationText);\n return pattern;\n }\n \n function getTranslationText (variable) {\n return utils.customTranslation(variable, variable);\n \n }\n}\n\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"required\": [\"cardHtml\"],\n \"properties\": {\n \"cardCss\": {\n \"title\": \"CSS\",\n \"type\": \"string\",\n \"default\": \".card {\\n font-weight: bold; \\n}\"\n },\n \"cardHtml\": {\n \"title\": \"HTML\",\n \"type\": \"string\",\n \"default\": \"
HTML code here
\"\n }\n }\n },\n \"form\": [\n {\n \"key\": \"cardCss\",\n \"type\": \"css\"\n }, \n {\n \"key\": \"cardHtml\",\n \"type\": \"html\"\n } \n ]\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"My value\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.random() * 5.45;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"cardCss\":\".card {\\n width: 100%;\\n height: 100%;\\n border: 2px solid #ccc;\\n box-sizing: border-box;\\n}\\n\\n.card .content {\\n padding: 20px;\\n display: flex;\\n flex-direction: row;\\n align-items: center;\\n justify-content: space-around;\\n height: 100%;\\n box-sizing: border-box;\\n}\\n\\n.card .content .column {\\n display: flex;\\n flex-direction: column; \\n justify-content: space-around;\\n height: 100%;\\n}\\n\\n.card h1 {\\n text-transform: uppercase;\\n color: #999;\\n font-size: 20px;\\n font-weight: bold;\\n margin: 0;\\n padding-bottom: 10px;\\n line-height: 32px;\\n}\\n\\n.card .value {\\n font-size: 38px;\\n font-weight: 200;\\n}\\n\\n.card .description {\\n font-size: 20px;\\n color: #999;\\n}\\n\",\"cardHtml\":\"
\\n
\\n
\\n

Value title

\\n
\\n ${My value:2} units.\\n
\\n
\\n Value description text\\n
\\n
\\n \\n
\\n
\"},\"title\":\"HTML Value Card\",\"dropShadow\":false,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { - "alias": "label_widget", - "name": "Label widget", + "alias": "simple_card", + "name": "Simple card", "descriptor": { "type": "latest", - "sizeX": 4.5, - "sizeY": 5, + "sizeX": 5, + "sizeY": 3, "resources": [], "templateHtml": "", - "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}", - "controllerScript": "self.onInit = function() {\n self.ctx.varsRegex = /\\$\\{([^\\}]*)\\}/g;\n \n var imageUrl = self.ctx.settings.backgroundImageUrl ? self.ctx.settings.backgroundImageUrl :\n 'data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==';\n\n self.ctx.$container.css('background', 'url(\"'+imageUrl+'\") no-repeat');\n self.ctx.$container.css('backgroundSize', 'contain');\n self.ctx.$container.css('backgroundPosition', '50% 50%');\n \n function processLabelPattern(pattern, data) {\n var match = self.ctx.varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split(':');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n \n if (label.startsWith('#')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = n;\n }\n }\n if (variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = self.ctx.varsRegex.exec(pattern);\n }\n return replaceInfo;\n }\n\n var configuredLabels = self.ctx.settings.labels;\n if (!configuredLabels) {\n configuredLabels = [];\n }\n \n self.ctx.labels = [];\n\n for (var l = 0; l < configuredLabels.length; l++) {\n var labelConfig = configuredLabels[l];\n var localConfig = {};\n localConfig.font = {};\n \n localConfig.pattern = labelConfig.pattern ? labelConfig.pattern : '${#0}';\n localConfig.x = labelConfig.x ? labelConfig.x : 0;\n localConfig.y = labelConfig.y ? labelConfig.y : 0;\n localConfig.backgroundColor = labelConfig.backgroundColor ? labelConfig.backgroundColor : 'rgba(0,0,0,0)';\n \n var settingsFont = labelConfig.font;\n if (!settingsFont) {\n settingsFont = {};\n }\n \n localConfig.font.family = settingsFont.family || 'Roboto';\n localConfig.font.size = settingsFont.size ? settingsFont.size : 6;\n localConfig.font.style = settingsFont.style ? settingsFont.style : 'normal';\n localConfig.font.weight = settingsFont.weight ? settingsFont.weight : '500';\n localConfig.font.color = settingsFont.color ? settingsFont.color : '#fff';\n \n localConfig.replaceInfo = processLabelPattern(localConfig.pattern, self.ctx.data);\n \n var label = {};\n var labelElement = $('
');\n labelElement.css('position', 'absolute');\n labelElement.css('display', 'none');\n labelElement.css('top', '0');\n labelElement.css('left', '0');\n labelElement.css('backgroundColor', localConfig.backgroundColor);\n labelElement.css('color', localConfig.font.color);\n labelElement.css('fontFamily', localConfig.font.family);\n labelElement.css('fontStyle', localConfig.font.style);\n labelElement.css('fontWeight', localConfig.font.weight);\n \n labelElement.html(localConfig.pattern);\n self.ctx.$container.append(labelElement);\n label.element = labelElement;\n label.config = localConfig;\n label.htmlSet = false;\n label.visible = false;\n self.ctx.labels.push(label);\n }\n\n var bgImg = $('');\n bgImg.hide();\n bgImg.bind('load', function()\n {\n self.ctx.bImageHeight = $(this).height();\n self.ctx.bImageWidth = $(this).width();\n self.onResize();\n });\n self.ctx.$container.append(bgImg);\n bgImg.attr('src', imageUrl);\n \n self.onDataUpdated();\n}\n\nself.onDataUpdated = function() {\n updateLabels();\n}\n\nself.onResize = function() {\n if (self.ctx.bImageHeight && self.ctx.bImageWidth) {\n var backgroundRect = {};\n var imageRatio = self.ctx.bImageWidth / self.ctx.bImageHeight;\n var componentRatio = self.ctx.width / self.ctx.height;\n if (componentRatio >= imageRatio) {\n backgroundRect.top = 0;\n backgroundRect.bottom = 1.0;\n backgroundRect.xRatio = imageRatio / componentRatio;\n backgroundRect.yRatio = 1;\n var offset = (1 - backgroundRect.xRatio) / 2;\n backgroundRect.left = offset;\n backgroundRect.right = 1 - offset;\n } else {\n backgroundRect.left = 0;\n backgroundRect.right = 1.0;\n backgroundRect.xRatio = 1;\n backgroundRect.yRatio = componentRatio / imageRatio;\n var offset = (1 - backgroundRect.yRatio) / 2;\n backgroundRect.top = offset;\n backgroundRect.bottom = 1 - offset;\n }\n for (var l = 0; l < self.ctx.labels.length; l++) {\n var label = self.ctx.labels[l];\n var labelLeft = backgroundRect.left*100 + (label.config.x*backgroundRect.xRatio);\n var labelTop = backgroundRect.top*100 + (label.config.y*backgroundRect.yRatio);\n var fontSize = self.ctx.height * backgroundRect.yRatio * label.config.font.size / 100;\n label.element.css('top', labelTop + '%');\n label.element.css('left', labelLeft + '%');\n label.element.css('fontSize', fontSize + 'px');\n if (!label.visible) {\n label.element.css('display', 'block');\n label.visible = true;\n }\n }\n } \n}\n\n\nfunction isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n}\n\nfunction padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n\n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n\n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split('.');\n s = int - strVal[0].length;\n\n for (; i < s; ++i) {\n strVal[0] = '0' + strVal[0];\n }\n\n strVal = (n ? '-' : '') + strVal[0] + '.' + strVal[1];\n }\n\n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n\n for (; i < s; ++i) {\n strVal = '0' + strVal;\n }\n\n strVal = (n ? '-' : '') + strVal;\n }\n\n return strVal;\n}\n\nfunction updateLabels() {\n for (var l = 0; l < self.ctx.labels.length; l++) {\n var label = self.ctx.labels[l];\n var text = label.config.pattern;\n var replaceInfo = label.config.replaceInfo;\n var updated = false;\n for (var v = 0; v < replaceInfo.variables.length; v++) {\n var variableInfo = replaceInfo.variables[v];\n var txtVal = '';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = self.ctx.data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n updated = true;\n } else {\n txtVal = val;\n updated = true;\n }\n }\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n if (updated || !label.htmlSet) {\n label.element.html(text);\n if (!label.htmlSet) {\n label.htmlSet = true;\n }\n }\n }\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"required\": [\"backgroundImageUrl\"],\n \"properties\": {\n \"backgroundImageUrl\": {\n \"title\": \"Background image\",\n \"type\": \"string\",\n \"default\": \"data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==\"\n },\n \"labels\": {\n \"title\": \"Labels\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Label\",\n \"type\": \"object\",\n \"required\": [\"pattern\"],\n \"properties\": {\n \"pattern\": {\n \"title\": \"Pattern ( for ex. 'Text ${keyName} units.' or '${#} units' )\",\n \"type\": \"string\",\n \"default\": \"${#0}\"\n },\n \"x\": {\n \"title\": \"X (Percentage relative to background)\",\n \"type\": \"number\",\n \"default\": 50\n },\n \"y\": {\n \"title\": \"Y (Percentage relative to background)\",\n \"type\": \"number\",\n \"default\": 50\n },\n \"backgroundColor\": {\n \"title\": \"Backround color\",\n \"type\": \"string\",\n \"default\": \"rgba(0,0,0,0)\"\n },\n \"font\": {\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"Roboto\"\n },\n \"size\": {\n \"title\": \"Relative font size (percents)\",\n \"type\": \"number\",\n \"default\": 6\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": \"#fff\"\n }\n }\n }\n }\n }\n }\n }\n },\n \"form\": [\n {\n \"key\": \"backgroundImageUrl\",\n \"type\": \"image\"\n },\n {\n \"key\": \"labels\",\n \"items\": [\n \"labels[].pattern\",\n \"labels[].x\",\n \"labels[].y\",\n {\n \"key\": \"labels[].backgroundColor\",\n \"type\": \"color\"\n },\n \"labels[].font.family\",\n \"labels[].font.size\",\n {\n \"key\": \"labels[].font.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n\n },\n {\n \"key\": \"labels[].font.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"labels[].font.color\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}", + "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n\n.tbDatasource-table {\n width: 100%;\n height: 100%;\n border-collapse: collapse;\n white-space: nowrap;\n font-weight: 100;\n text-align: right;\n}\n\n.tbDatasource-table td {\n padding: 12px;\n position: relative;\n box-sizing: border-box;\n}\n\n.tbDatasource-data-key {\n opacity: 0.7;\n font-weight: 400;\n font-size: 3.500rem;\n}\n\n.tbDatasource-value {\n font-size: 5.000rem;\n}", + "controllerScript": "self.onInit = function() {\n\n self.ctx.labelPosition = self.ctx.settings.labelPosition || 'left';\n \n if (self.ctx.datasources.length > 0) {\n var tbDatasource = self.ctx.datasources[0];\n var datasourceId = 'tbDatasource' + 0;\n self.ctx.$container.append(\n \"
\"\n );\n \n self.ctx.datasourceContainer = $('#' + datasourceId,\n self.ctx.$container);\n \n var tableId = 'table' + 0;\n self.ctx.datasourceContainer.append(\n \"
\"\n );\n var table = $('#' + tableId, self.ctx.$container);\n if (self.ctx.labelPosition === 'top') {\n table.css('text-align', 'left');\n }\n \n if (tbDatasource.dataKeys.length > 0) {\n var dataKey = tbDatasource.dataKeys[0];\n var labelCellId = 'labelCell' + 0;\n var cellId = 'cell' + 0;\n if (self.ctx.labelPosition === 'left') {\n table.append(\n \"\" +\n dataKey.label +\n \"\");\n } else {\n table.append(\n \"\" +\n dataKey.label +\n \"\");\n }\n self.ctx.labelCell = $('#' + labelCellId, table);\n self.ctx.valueCell = $('#' + cellId, table);\n self.ctx.valueCell.html(0 + ' ' + self.ctx.units);\n }\n }\n \n $.fn.textWidth = function(){\n var html_org = $(this).html();\n var html_calc = '' + html_org + '';\n $(this).html(html_calc);\n var width = $(this).find('span:first').width();\n $(this).html(html_org);\n return width;\n }; \n \n self.onResize();\n};\n\nself.onDataUpdated = function() {\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n\n if (self.ctx.valueCell && self.ctx.data.length > 0) {\n var cellData = self.ctx.data[0];\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length -\n 1];\n var value = tvPair[1];\n var txtValue;\n if (isNumber(value)) {\n var decimals = self.ctx.decimals;\n var units = self.ctx.units;\n if (self.ctx.datasources.length > 0 && self.ctx.datasources[0].dataKeys.length > 0) {\n dataKey = self.ctx.datasources[0].dataKeys[0];\n if (dataKey.decimals || dataKey.decimals === 0) {\n decimals = dataKey.decimals;\n }\n if (dataKey.units) {\n units = dataKey.units;\n }\n }\n txtValue = self.ctx.utils.formatValue(value, decimals, units, true);\n } else {\n txtValue = value;\n }\n self.ctx.valueCell.html(txtValue);\n var targetWidth;\n var minDelta;\n if (self.ctx.labelPosition === 'left') {\n targetWidth = self.ctx.datasourceContainer.width() - self.ctx.labelCell.width();\n minDelta = self.ctx.width/16 + self.ctx.padding;\n } else {\n targetWidth = self.ctx.datasourceContainer.width();\n minDelta = self.ctx.padding;\n }\n var delta = targetWidth - self.ctx.valueCell.textWidth();\n var fontSize = self.ctx.valueFontSize;\n if (targetWidth > minDelta) {\n while (delta < minDelta && fontSize > 6) {\n fontSize--;\n self.ctx.valueCell.css('font-size', fontSize+'px');\n delta = targetWidth - self.ctx.valueCell.textWidth();\n }\n }\n }\n } \n \n};\n\nself.onResize = function() {\n var labelFontSize;\n if (self.ctx.labelPosition === 'top') {\n self.ctx.padding = self.ctx.height/20;\n labelFontSize = self.ctx.height/4;\n self.ctx.valueFontSize = self.ctx.height/2;\n } else {\n self.ctx.padding = self.ctx.width/50;\n labelFontSize = self.ctx.height/2.5;\n self.ctx.valueFontSize = self.ctx.height/2;\n if (self.ctx.width/self.ctx.height <= 2.7) {\n labelFontSize = self.ctx.width/7;\n self.ctx.valueFontSize = self.ctx.width/6;\n }\n }\n self.ctx.padding = Math.min(12, self.ctx.padding);\n \n if (self.ctx.labelCell) {\n self.ctx.labelCell.css('font-size', labelFontSize+'px');\n self.ctx.labelCell.css('padding', self.ctx.padding+'px');\n }\n if (self.ctx.valueCell) {\n self.ctx.valueCell.css('font-size', self.ctx.valueFontSize+'px');\n self.ctx.valueCell.css('padding', self.ctx.padding+'px');\n } \n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n};\n\n\nself.onDestroy = function() {\n};\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"labelPosition\": {\n \"title\": \"Label position\",\n \"type\": \"string\",\n \"default\": \"left\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"labelPosition\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"left\",\n \"label\": \"Left\"\n },\n {\n \"value\": \"top\",\n \"label\": \"Top\"\n }\n ]\n }\n ]\n}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"var\",\"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\":{\"backgroundImageUrl\":\"data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==\",\"labels\":[{\"pattern\":\"Value: ${#0:2} units.\",\"x\":20,\"y\":47,\"font\":{\"color\":\"#515151\",\"family\":\"Roboto\",\"size\":6,\"style\":\"normal\",\"weight\":\"500\"}}]},\"title\":\"Label widget\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ff5722\",\"color\":\"rgba(255, 255, 255, 0.87)\",\"padding\":\"16px\",\"settings\":{\"labelPosition\":\"top\"},\"title\":\"Simple card\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" } }, { - "alias": "simple_card", - "name": "Simple card", + "alias": "label_widget", + "name": "Label widget", "descriptor": { "type": "latest", - "sizeX": 5, - "sizeY": 3, + "sizeX": 4.5, + "sizeY": 5, "resources": [], "templateHtml": "", - "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n\n.tbDatasource-table {\n width: 100%;\n height: 100%;\n border-collapse: collapse;\n white-space: nowrap;\n font-weight: 100;\n text-align: right;\n}\n\n.tbDatasource-table td {\n padding: 12px;\n position: relative;\n box-sizing: border-box;\n}\n\n.tbDatasource-data-key {\n opacity: 0.7;\n font-weight: 400;\n font-size: 3.500rem;\n}\n\n.tbDatasource-value {\n font-size: 5.000rem;\n}", - "controllerScript": "self.onInit = function() {\n\n self.ctx.labelPosition = self.ctx.settings.labelPosition || 'left';\n \n if (self.ctx.datasources.length > 0) {\n var tbDatasource = self.ctx.datasources[0];\n var datasourceId = 'tbDatasource' + 0;\n self.ctx.$container.append(\n \"
\"\n );\n \n self.ctx.datasourceContainer = $('#' + datasourceId,\n self.ctx.$container);\n \n var tableId = 'table' + 0;\n self.ctx.datasourceContainer.append(\n \"
\"\n );\n var table = $('#' + tableId, self.ctx.$container);\n if (self.ctx.labelPosition === 'top') {\n table.css('text-align', 'left');\n }\n \n if (tbDatasource.dataKeys.length > 0) {\n var dataKey = tbDatasource.dataKeys[0];\n var labelCellId = 'labelCell' + 0;\n var cellId = 'cell' + 0;\n if (self.ctx.labelPosition === 'left') {\n table.append(\n \"\" +\n dataKey.label +\n \"\");\n } else {\n table.append(\n \"\" +\n dataKey.label +\n \"\");\n }\n self.ctx.labelCell = $('#' + labelCellId, table);\n self.ctx.valueCell = $('#' + cellId, table);\n self.ctx.valueCell.html(0 + ' ' + self.ctx.units);\n }\n }\n \n $.fn.textWidth = function(){\n var html_org = $(this).html();\n var html_calc = '' + html_org + '';\n $(this).html(html_calc);\n var width = $(this).find('span:first').width();\n $(this).html(html_org);\n return width;\n }; \n \n self.onResize();\n};\n\nself.onDataUpdated = function() {\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n\n if (self.ctx.valueCell && self.ctx.data.length > 0) {\n var cellData = self.ctx.data[0];\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length -\n 1];\n var value = tvPair[1];\n var txtValue;\n if (isNumber(value)) {\n var decimals = self.ctx.decimals;\n var units = self.ctx.units;\n if (self.ctx.datasources.length > 0 && self.ctx.datasources[0].dataKeys.length > 0) {\n dataKey = self.ctx.datasources[0].dataKeys[0];\n if (dataKey.decimals || dataKey.decimals === 0) {\n decimals = dataKey.decimals;\n }\n if (dataKey.units) {\n units = dataKey.units;\n }\n }\n txtValue = self.ctx.utils.formatValue(value, decimals, units, true);\n } else {\n txtValue = value;\n }\n self.ctx.valueCell.html(txtValue);\n var targetWidth;\n var minDelta;\n if (self.ctx.labelPosition === 'left') {\n targetWidth = self.ctx.datasourceContainer.width() - self.ctx.labelCell.width();\n minDelta = self.ctx.width/16 + self.ctx.padding;\n } else {\n targetWidth = self.ctx.datasourceContainer.width();\n minDelta = self.ctx.padding;\n }\n var delta = targetWidth - self.ctx.valueCell.textWidth();\n var fontSize = self.ctx.valueFontSize;\n if (targetWidth > minDelta) {\n while (delta < minDelta && fontSize > 6) {\n fontSize--;\n self.ctx.valueCell.css('font-size', fontSize+'px');\n delta = targetWidth - self.ctx.valueCell.textWidth();\n }\n }\n }\n } \n \n};\n\nself.onResize = function() {\n var labelFontSize;\n if (self.ctx.labelPosition === 'top') {\n self.ctx.padding = self.ctx.height/20;\n labelFontSize = self.ctx.height/4;\n self.ctx.valueFontSize = self.ctx.height/2;\n } else {\n self.ctx.padding = self.ctx.width/50;\n labelFontSize = self.ctx.height/2.5;\n self.ctx.valueFontSize = self.ctx.height/2;\n if (self.ctx.width/self.ctx.height <= 2.7) {\n labelFontSize = self.ctx.width/7;\n self.ctx.valueFontSize = self.ctx.width/6;\n }\n }\n self.ctx.padding = Math.min(12, self.ctx.padding);\n \n if (self.ctx.labelCell) {\n self.ctx.labelCell.css('font-size', labelFontSize+'px');\n self.ctx.labelCell.css('padding', self.ctx.padding+'px');\n }\n if (self.ctx.valueCell) {\n self.ctx.valueCell.css('font-size', self.ctx.valueFontSize+'px');\n self.ctx.valueCell.css('padding', self.ctx.padding+'px');\n } \n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n };\n};\n\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"labelPosition\": {\n \"title\": \"Label position\",\n \"type\": \"string\",\n \"default\": \"left\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"labelPosition\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"left\",\n \"label\": \"Left\"\n },\n {\n \"value\": \"top\",\n \"label\": \"Top\"\n }\n ]\n }\n ]\n}", + "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}", + "controllerScript": "self.onInit = function() {\n self.ctx.varsRegex = /\\$\\{([^\\}]*)\\}/g;\n \n var imageUrl = self.ctx.settings.backgroundImageUrl ? self.ctx.settings.backgroundImageUrl :\n 'data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==';\n\n self.ctx.$container.css('background', 'url(\"'+imageUrl+'\") no-repeat');\n self.ctx.$container.css('backgroundSize', 'contain');\n self.ctx.$container.css('backgroundPosition', '50% 50%');\n \n function processLabelPattern(pattern, data) {\n var match = self.ctx.varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split(':');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n \n if (label.startsWith('#')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = n;\n }\n }\n if (variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = self.ctx.varsRegex.exec(pattern);\n }\n return replaceInfo;\n }\n\n var configuredLabels = self.ctx.settings.labels;\n if (!configuredLabels) {\n configuredLabels = [];\n }\n \n self.ctx.labels = [];\n\n for (var l = 0; l < configuredLabels.length; l++) {\n var labelConfig = configuredLabels[l];\n var localConfig = {};\n localConfig.font = {};\n \n localConfig.pattern = labelConfig.pattern ? labelConfig.pattern : '${#0}';\n localConfig.x = labelConfig.x ? labelConfig.x : 0;\n localConfig.y = labelConfig.y ? labelConfig.y : 0;\n localConfig.backgroundColor = labelConfig.backgroundColor ? labelConfig.backgroundColor : 'rgba(0,0,0,0)';\n \n var settingsFont = labelConfig.font;\n if (!settingsFont) {\n settingsFont = {};\n }\n \n localConfig.font.family = settingsFont.family || 'Roboto';\n localConfig.font.size = settingsFont.size ? settingsFont.size : 6;\n localConfig.font.style = settingsFont.style ? settingsFont.style : 'normal';\n localConfig.font.weight = settingsFont.weight ? settingsFont.weight : '500';\n localConfig.font.color = settingsFont.color ? settingsFont.color : '#fff';\n \n localConfig.replaceInfo = processLabelPattern(localConfig.pattern, self.ctx.data);\n \n var label = {};\n var labelElement = $('
');\n labelElement.css('position', 'absolute');\n labelElement.css('display', 'none');\n labelElement.css('top', '0');\n labelElement.css('left', '0');\n labelElement.css('backgroundColor', localConfig.backgroundColor);\n labelElement.css('color', localConfig.font.color);\n labelElement.css('fontFamily', localConfig.font.family);\n labelElement.css('fontStyle', localConfig.font.style);\n labelElement.css('fontWeight', localConfig.font.weight);\n \n labelElement.html(localConfig.pattern);\n self.ctx.$container.append(labelElement);\n label.element = labelElement;\n label.config = localConfig;\n label.htmlSet = false;\n label.visible = false;\n self.ctx.labels.push(label);\n }\n\n var bgImg = $('');\n bgImg.hide();\n bgImg.bind('load', function()\n {\n self.ctx.bImageHeight = $(this).height();\n self.ctx.bImageWidth = $(this).width();\n self.onResize();\n });\n self.ctx.$container.append(bgImg);\n bgImg.attr('src', imageUrl);\n \n self.onDataUpdated();\n}\n\nself.onDataUpdated = function() {\n updateLabels();\n}\n\nself.onResize = function() {\n if (self.ctx.bImageHeight && self.ctx.bImageWidth) {\n var backgroundRect = {};\n var imageRatio = self.ctx.bImageWidth / self.ctx.bImageHeight;\n var componentRatio = self.ctx.width / self.ctx.height;\n if (componentRatio >= imageRatio) {\n backgroundRect.top = 0;\n backgroundRect.bottom = 1.0;\n backgroundRect.xRatio = imageRatio / componentRatio;\n backgroundRect.yRatio = 1;\n var offset = (1 - backgroundRect.xRatio) / 2;\n backgroundRect.left = offset;\n backgroundRect.right = 1 - offset;\n } else {\n backgroundRect.left = 0;\n backgroundRect.right = 1.0;\n backgroundRect.xRatio = 1;\n backgroundRect.yRatio = componentRatio / imageRatio;\n var offset = (1 - backgroundRect.yRatio) / 2;\n backgroundRect.top = offset;\n backgroundRect.bottom = 1 - offset;\n }\n for (var l = 0; l < self.ctx.labels.length; l++) {\n var label = self.ctx.labels[l];\n var labelLeft = backgroundRect.left*100 + (label.config.x*backgroundRect.xRatio);\n var labelTop = backgroundRect.top*100 + (label.config.y*backgroundRect.yRatio);\n var fontSize = self.ctx.height * backgroundRect.yRatio * label.config.font.size / 100;\n label.element.css('top', labelTop + '%');\n label.element.css('left', labelLeft + '%');\n label.element.css('fontSize', fontSize + 'px');\n if (!label.visible) {\n label.element.css('display', 'block');\n label.visible = true;\n }\n }\n } \n}\n\n\nfunction isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n}\n\nfunction padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n\n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n\n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split('.');\n s = int - strVal[0].length;\n\n for (; i < s; ++i) {\n strVal[0] = '0' + strVal[0];\n }\n\n strVal = (n ? '-' : '') + strVal[0] + '.' + strVal[1];\n }\n\n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n\n for (; i < s; ++i) {\n strVal = '0' + strVal;\n }\n\n strVal = (n ? '-' : '') + strVal;\n }\n\n return strVal;\n}\n\nfunction updateLabels() {\n for (var l = 0; l < self.ctx.labels.length; l++) {\n var label = self.ctx.labels[l];\n var text = label.config.pattern;\n var replaceInfo = label.config.replaceInfo;\n var updated = false;\n for (var v = 0; v < replaceInfo.variables.length; v++) {\n var variableInfo = replaceInfo.variables[v];\n var txtVal = '';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = self.ctx.data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n updated = true;\n } else {\n txtVal = val;\n updated = true;\n }\n }\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n if (updated || !label.htmlSet) {\n label.element.html(text);\n if (!label.htmlSet) {\n label.htmlSet = true;\n }\n }\n }\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n singleEntity: true\n };\n};\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"required\": [\"backgroundImageUrl\"],\n \"properties\": {\n \"backgroundImageUrl\": {\n \"title\": \"Background image\",\n \"type\": \"string\",\n \"default\": \"data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==\"\n },\n \"labels\": {\n \"title\": \"Labels\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Label\",\n \"type\": \"object\",\n \"required\": [\"pattern\"],\n \"properties\": {\n \"pattern\": {\n \"title\": \"Pattern ( for ex. 'Text ${keyName} units.' or '${#} units' )\",\n \"type\": \"string\",\n \"default\": \"${#0}\"\n },\n \"x\": {\n \"title\": \"X (Percentage relative to background)\",\n \"type\": \"number\",\n \"default\": 50\n },\n \"y\": {\n \"title\": \"Y (Percentage relative to background)\",\n \"type\": \"number\",\n \"default\": 50\n },\n \"backgroundColor\": {\n \"title\": \"Backround color\",\n \"type\": \"string\",\n \"default\": \"rgba(0,0,0,0)\"\n },\n \"font\": {\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"Roboto\"\n },\n \"size\": {\n \"title\": \"Relative font size (percents)\",\n \"type\": \"number\",\n \"default\": 6\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": \"#fff\"\n }\n }\n }\n }\n }\n }\n }\n },\n \"form\": [\n {\n \"key\": \"backgroundImageUrl\",\n \"type\": \"image\"\n },\n {\n \"key\": \"labels\",\n \"items\": [\n \"labels[].pattern\",\n \"labels[].x\",\n \"labels[].y\",\n {\n \"key\": \"labels[].backgroundColor\",\n \"type\": \"color\"\n },\n \"labels[].font.family\",\n \"labels[].font.size\",\n {\n \"key\": \"labels[].font.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n\n },\n {\n \"key\": \"labels[].font.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"labels[].font.color\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ff5722\",\"color\":\"rgba(255, 255, 255, 0.87)\",\"padding\":\"16px\",\"settings\":{\"labelPosition\":\"top\"},\"title\":\"Simple card\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"var\",\"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\":{\"backgroundImageUrl\":\"data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==\",\"labels\":[{\"pattern\":\"Value: ${#0:2} units.\",\"x\":20,\"y\":47,\"font\":{\"color\":\"#515151\",\"family\":\"Roboto\",\"size\":6,\"style\":\"normal\",\"weight\":\"500\"}}]},\"title\":\"Label widget\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" } }, { - "alias": "timeseries_table", - "name": "Timeseries table", + "alias": "entities_table", + "name": "Entities table", "descriptor": { - "type": "timeseries", - "sizeX": 8, + "type": "latest", + "sizeX": 7.5, "sizeY": 6.5, "resources": [], - "templateHtml": "\n", + "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('timeseries-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"TimeseriesTableSettings\",\n \"properties\": {\n \"showTimestamp\": {\n \"title\": \"Display timestamp column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"showMilliseconds\": {\n \"title\": \"Display timestamp milliseconds\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"hideEmptyLines\": {\n \"title\": \"Hide empty lines\",\n \"type\": \"boolean\",\n \"default\": false\n }\n },\n \"required\": []\n },\n \"form\": [\n \"showTimestamp\",\n \"showMilliseconds\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"hideEmptyLines\"\n ]\n}", - "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, rowData, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', amount = percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\"},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true,\"displayPagination\":true,\"defaultPageSize\":10},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}", + "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}", + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}]}" } }, { @@ -125,13 +125,13 @@ "sizeX": 7.5, "sizeY": 3.5, "resources": [], - "templateHtml": "\n", + "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.hierarchyId = \"hierarchy-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('entities-hierarchy-data-updated', self.ctx.$scope.hierarchyId);\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'nodeSelected': {\n name: 'widget-action.node-selected',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesHierarchyWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'nodeSelected': {\n name: 'widget-action.node-selected',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesHierarchySettings\",\n \"properties\": {\n \"nodeRelationQueryFunction\": {\n \"title\": \"Node relations query function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeHasChildrenFunction\": {\n \"title\": \"Node has children function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeOpenedFunction\": {\n \"title\": \"Default node opened function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeDisabledFunction\": {\n \"title\": \"Node disabled function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeIconFunction\": {\n \"title\": \"Node icon function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeTextFunction\": {\n \"title\": \"Node text function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodesSortFunction\": {\n \"title\": \"Nodes sort function: f(nodeCtx1, nodeCtx2)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"nodeRelationQueryFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeHasChildrenFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeOpenedFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeDisabledFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeIconFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeTextFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodesSortFunction\",\n \"type\": \"javascript\"\n }\n ]\n}", "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {},\n \"required\": []\n },\n \"form\": []\n}", - "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"nodeRelationQueryFunction\":\"/**\\n\\n// Function should return relations query object for current node used to fetch entity children.\\n// Function can return 'default' string value. In this case default relations query will be used.\\n\\n// The following example code will construct simple relations query that will fetch relations of type 'Contains'\\n// from the current entity.\\n\\nvar entity = nodeCtx.entity;\\nvar query = {\\n parameters: {\\n rootId: entity.id.id,\\n rootType: entity.id.entityType,\\n direction: types.entitySearchDirection.from,\\n relationTypeGroup: \\\"COMMON\\\",\\n maxLevel: 1\\n },\\n filters: [{\\n relationType: \\\"Contains\\\",\\n entityTypes: []\\n }]\\n};\\nreturn query;\\n\\n**/\\n\",\"nodeHasChildrenFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node has children (whether it can be expanded).\\n\\n// The following example code will restrict entities hierarchy expansion up to third level.\\n\\nreturn nodeCtx.level <= 2;\\n\\n// The next example code will restrict entities expansion according to the value of example 'nodeHasChildren' attribute.\\n\\nvar data = nodeCtx.data;\\nif (data.hasOwnProperty('nodeHasChildren') && data['nodeHasChildren'] !== null) {\\n return data['nodeHasChildren'] === 'true';\\n} else {\\n return true;\\n}\\n \\n**/\\n \",\"nodeTextFunction\":\"/**\\n\\n// Function should return text (can be HTML code) for the current node.\\n\\n// The following example code will generate node text consisting of entity name and temperature if temperature value is present in entity attributes/timeseries.\\n\\nvar data = nodeCtx.data;\\nvar entity = nodeCtx.entity;\\nvar text = entity.name;\\nif (data.hasOwnProperty('temperature') && data['temperature'] !== null) {\\n text += \\\" \\\"+ data['temperature'] +\\\" °C\\\";\\n}\\nreturn text;\\n\\n**/\",\"nodeIconFunction\":\"/** \\n\\n// Function should return node icon info object.\\n// Resulting object should contain either 'materialIcon' or 'iconUrl' property. \\n// Where:\\n - 'materialIcon' - name of the material icon to be used from the Material Icons Library (https://material.io/tools/icons);\\n - 'iconUrl' - url of the external image to be used as node icon.\\n// Function can return 'default' string value. In this case default icons according to entity type will be used.\\n\\n// The following example code shows how to use external image for devices which name starts with 'Test' and use \\n// default icons for the rest of entities.\\n\\nvar entity = nodeCtx.entity;\\nif (entity.id.entityType === 'DEVICE' && entity.name.startsWith('Test')) {\\n return {iconUrl: 'https://avatars1.githubusercontent.com/u/14793288?v=4&s=117'};\\n} else {\\n return 'default';\\n}\\n \\n**/\",\"nodeDisabledFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node should be disabled (not selectable).\\n\\n// The following example code will disable current node according to the value of example 'nodeDisabled' attribute.\\n\\nvar data = nodeCtx.data;\\nif (data.hasOwnProperty('nodeDisabled') && data['nodeDisabled'] !== null) {\\n return data['nodeDisabled'] === 'true';\\n} else {\\n return false;\\n}\\n \\n**/\\n\",\"nodesSortFunction\":\"/**\\n\\n// This function is used to sort nodes of the same level. Function should compare two nodes and return \\n// integer value: \\n// - less than 0 - sort nodeCtx1 to an index lower than nodeCtx2\\n// - 0 - leave nodeCtx1 and nodeCtx2 unchanged with respect to each other\\n// - greater than 0 - sort nodeCtx2 to an index lower than nodeCtx1\\n\\n// The following example code will sort entities first by entity type in alphabetical order then\\n// by entity name in alphabetical order.\\n\\nvar result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType);\\nif (result === 0) {\\n result = nodeCtx1.entity.name.localeCompare(nodeCtx2.entity.name);\\n}\\nreturn result;\\n \\n**/\",\"nodeOpenedFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node should be opened (expanded) when it first loaded.\\n\\n// The following example code will open by default nodes up to third level.\\n\\nreturn nodeCtx.level <= 2;\\n\\n**/\\n \"},\"title\":\"Entities hierarchy\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"widgetStyle\":{},\"actions\":{}}" + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"nodeRelationQueryFunction\":\"/**\\n\\n// Function should return relations query object for current node used to fetch entity children.\\n// Function can return 'default' string value. In this case default relations query will be used.\\n\\n// The following example code will construct simple relations query that will fetch relations of type 'Contains'\\n// from the current entity.\\n\\nvar entity = nodeCtx.entity;\\nvar query = {\\n parameters: {\\n rootId: entity.id.id,\\n rootType: entity.id.entityType,\\n direction: \\\"FROM\\\",\\n maxLevel: 1\\n },\\n filters: [{\\n relationType: \\\"Contains\\\",\\n entityTypes: []\\n }]\\n};\\nreturn query;\\n\\n**/\\n\",\"nodeHasChildrenFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node has children (whether it can be expanded).\\n\\n// The following example code will restrict entities hierarchy expansion up to third level.\\n\\nreturn nodeCtx.level <= 2;\\n\\n// The next example code will restrict entities expansion according to the value of example 'nodeHasChildren' attribute.\\n\\nvar data = nodeCtx.data;\\nif (data.hasOwnProperty('nodeHasChildren') && data['nodeHasChildren'] !== null) {\\n return data['nodeHasChildren'] === 'true';\\n} else {\\n return true;\\n}\\n \\n**/\\n \",\"nodeTextFunction\":\"/**\\n\\n// Function should return text (can be HTML code) for the current node.\\n\\n// The following example code will generate node text consisting of entity name and temperature if temperature value is present in entity attributes/timeseries.\\n\\nvar data = nodeCtx.data;\\nvar entity = nodeCtx.entity;\\nvar text = entity.name;\\nif (data.hasOwnProperty('temperature') && data['temperature'] !== null) {\\n text += \\\" \\\"+ data['temperature'] +\\\" °C\\\";\\n}\\nreturn text;\\n\\n**/\",\"nodeIconFunction\":\"/** \\n\\n// Function should return node icon info object.\\n// Resulting object should contain either 'materialIcon' or 'iconUrl' property. \\n// Where:\\n - 'materialIcon' - name of the material icon to be used from the Material Icons Library (https://material.io/tools/icons);\\n - 'iconUrl' - url of the external image to be used as node icon.\\n// Function can return 'default' string value. In this case default icons according to entity type will be used.\\n\\n// The following example code shows how to use external image for devices which name starts with 'Test' and use \\n// default icons for the rest of entities.\\n\\nvar entity = nodeCtx.entity;\\nif (entity.id.entityType === 'DEVICE' && entity.name.startsWith('Test')) {\\n return {iconUrl: 'https://avatars1.githubusercontent.com/u/14793288?v=4&s=117'};\\n} else {\\n return 'default';\\n}\\n \\n**/\",\"nodeDisabledFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node should be disabled (not selectable).\\n\\n// The following example code will disable current node according to the value of example 'nodeDisabled' attribute.\\n\\nvar data = nodeCtx.data;\\nif (data.hasOwnProperty('nodeDisabled') && data['nodeDisabled'] !== null) {\\n return data['nodeDisabled'] === 'true';\\n} else {\\n return false;\\n}\\n \\n**/\\n\",\"nodesSortFunction\":\"/**\\n\\n// This function is used to sort nodes of the same level. Function should compare two nodes and return \\n// integer value: \\n// - less than 0 - sort nodeCtx1 to an index lower than nodeCtx2\\n// - 0 - leave nodeCtx1 and nodeCtx2 unchanged with respect to each other\\n// - greater than 0 - sort nodeCtx2 to an index lower than nodeCtx1\\n\\n// The following example code will sort entities first by entity type in alphabetical order then\\n// by entity name in alphabetical order.\\n\\nvar result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType);\\nif (result === 0) {\\n result = nodeCtx1.entity.name.localeCompare(nodeCtx2.entity.name);\\n}\\nreturn result;\\n \\n**/\",\"nodeOpenedFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node should be opened (expanded) when it first loaded.\\n\\n// The following example code will open by default nodes up to third level.\\n\\nreturn nodeCtx.level <= 2;\\n\\n**/\\n \"},\"title\":\"Entities hierarchy\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"widgetStyle\":{},\"actions\":{}}" } } ] -} +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/charts.json b/application/src/main/data/json/system/widget_bundles/charts.json index 9c97a03084..1010013689 100644 --- a/application/src/main/data/json/system/widget_bundles/charts.json +++ b/application/src/main/data/json/system/widget_bundles/charts.json @@ -19,7 +19,7 @@ ], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n var barData = {\n labels: [],\n datasets: []\n };\n \n for (var i = 0; i < self.ctx.datasources.length; i++) {\n var datasource = self.ctx.datasources[i];\n for (var d = 0; d < datasource.dataKeys.length; d++) {\n var dataset = {\n label: datasource.dataKeys[d].label,\n data: [0],\n backgroundColor: [datasource.dataKeys[d].color],\n borderColor: [datasource.dataKeys[d].color],\n borderWidth: 1\n }\n barData.datasets.push(dataset);\n }\n }\n\n var ctx = $('#barChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'bar',\n data: barData,\n options: {\n responsive: false,\n maintainAspectRatio: false,\n scales: {\n yAxes: [{\n ticks: {\n beginAtZero:true\n }\n }]\n }\n }\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n var c = 0;\n for (var i = 0; i < self.ctx.chart.data.datasets.length; i++) {\n var dataset = self.ctx.chart.data.datasets[i];\n var cellData = self.ctx.data[i]; \n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = tvPair[1];\n dataset.data[0] = parseFloat(value);\n }\n }\n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n self.ctx.chart.resize();\n}\n\n", + "controllerScript": "self.onInit = function() {\n var barData = {\n labels: [],\n datasets: []\n };\n \n for (var i = 0; i < self.ctx.datasources.length; i++) {\n var datasource = self.ctx.datasources[i];\n for (var d = 0; d < datasource.dataKeys.length; d++) {\n var dataKey = datasource.dataKeys[d];\n var units = dataKey.units && dataKey.units.length ? dataKey.units : self.ctx.units;\n units = units ? (' (' + units + ')') : '';\n var dataset = {\n label: dataKey.label + units,\n data: [0],\n backgroundColor: [dataKey.color],\n borderColor: [dataKey.color],\n borderWidth: 1\n }\n barData.datasets.push(dataset);\n }\n }\n\n var ctx = $('#barChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'bar',\n data: barData,\n options: {\n responsive: false,\n maintainAspectRatio: false,\n scales: {\n yAxes: [{\n ticks: {\n beginAtZero:true\n }\n }]\n }\n }\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n var c = 0;\n for (var i = 0; i < self.ctx.chart.data.datasets.length; i++) {\n var dataset = self.ctx.chart.data.datasets[i];\n var cellData = self.ctx.data[i]; \n if (cellData.data.length > 0) {\n var decimals;\n if (typeof cellData.dataKey.decimals !== 'undefined' \n && cellData.dataKey.decimals !== null ) {\n decimals = cellData.dataKey.decimals; \n } else {\n decimals = self.ctx.decimals;\n }\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = self.ctx.utils.formatValue(tvPair[1], decimals);\n dataset.data[0] = parseFloat(value);\n }\n }\n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n self.ctx.chart.resize();\n}\n\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Bars - Chart.js\"}" @@ -55,7 +55,7 @@ ], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n \n var borderColor = self.ctx.settings.borderColor || '#fff';\n var borderWidth = angular.isDefined(self.ctx.settings.borderWidth) ? self.ctx.settings.borderWidth : 5;\n \n pieData.datasets.push(dataset);\n \n for (var i=0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n pieData.labels.push(dataKey.label);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push(borderColor);\n dataset.borderWidth.push(borderWidth);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n\n var options = {\n responsive: false,\n maintainAspectRatio: false,\n legend: {\n display: true,\n labels: {\n fontColor: '#666'\n }\n },\n tooltips: {\n callbacks: {\n label: function(tooltipItem, data) {\n var label = data.labels[tooltipItem.index];\n var value = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];\n var content = label + ': ' + value;\n var units = self.ctx.settings.units ? self.ctx.settings.units : self.ctx.units;\n if (units) {\n content += ' ' + units;\n } \n return content;\n }\n }\n }\n };\n\n if (self.ctx.settings.legend) {\n options.legend.display = self.ctx.settings.legend.display !== false;\n options.legend.labels.fontColor = self.ctx.settings.legend.labelsFontColor || '#666';\n }\n\n var ctx = $('#pieChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'doughnut',\n data: pieData,\n options: options\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = tvPair[1];\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n self.ctx.chart.resize();\n}\n\n", + "controllerScript": "self.onInit = function() {\n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n \n var borderColor = self.ctx.settings.borderColor || '#fff';\n var borderWidth = typeof self.ctx.settings.borderWidth !== 'undefined' ? self.ctx.settings.borderWidth : 5;\n \n pieData.datasets.push(dataset);\n \n for (var i=0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n var units = dataKey.units && dataKey.units.length ? dataKey.units : self.ctx.units;\n units = units ? (' (' + units + ')') : '';\n pieData.labels.push(dataKey.label + units);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push(borderColor);\n dataset.borderWidth.push(borderWidth);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n\n var options = {\n responsive: false,\n maintainAspectRatio: false,\n legend: {\n display: true,\n labels: {\n fontColor: '#666'\n }\n },\n tooltips: {\n callbacks: {\n label: function(tooltipItem, data) {\n var label = data.labels[tooltipItem.index];\n var value = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];\n var content = label + ': ' + value;\n var units = self.ctx.settings.units ? self.ctx.settings.units : self.ctx.units;\n if (units) {\n content += ' ' + units;\n } \n return content;\n }\n }\n }\n };\n\n if (self.ctx.settings.legend) {\n options.legend.display = self.ctx.settings.legend.display !== false;\n options.legend.labels.fontColor = self.ctx.settings.legend.labelsFontColor || '#666';\n }\n\n var ctx = $('#pieChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'doughnut',\n data: pieData,\n options: options\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var decimals;\n if (typeof cellData.dataKey.decimals !== 'undefined' \n && cellData.dataKey.decimals !== null ) {\n decimals = cellData.dataKey.decimals; \n } else {\n decimals = self.ctx.decimals;\n }\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = self.ctx.utils.formatValue(tvPair[1], decimals);\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n self.ctx.chart.resize();\n}\n\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"borderWidth\": {\n \"title\": \"Border width\",\n \"type\": \"number\",\n \"default\": 5\n },\n \"borderColor\": {\n \"title\": \"Border color\",\n \"type\": \"string\",\n \"default\": \"#fff\"\n },\n \"legend\": {\n \"title\": \"Legend settings\",\n \"type\": \"object\",\n \"properties\": {\n \"display\": {\n \"title\": \"Display legend\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"labelsFontColor\": {\n \"title\": \"Labels font color\",\n \"type\": \"string\",\n \"default\": \"#666\"\n }\n }\n }\n },\n \"required\": []\n },\n \"form\": [\n \"borderWidth\", \n {\n \"key\": \"borderColor\",\n \"type\": \"color\"\n }, \n {\n \"key\": \"legend\",\n \"items\": [\n \"legend.display\",\n {\n \"key\": \"legend.labelsFontColor\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#26a69a\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#f57c00\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#afb42b\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#673ab7\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"borderWidth\":5,\"borderColor\":\"#fff\",\"legend\":{\"display\":true,\"labelsFontColor\":\"#666666\"}},\"title\":\"Doughnut - Chart.js\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" @@ -71,7 +71,7 @@ "resources": [], "templateHtml": "", "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.pie-label {\n font-size: 12px;\n font-family: 'Roboto';\n font-weight: bold;\n text-align: center;\n padding: 2px;\n color: white;\n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'pie'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.pieSettingsSchema;\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.pieDatakeySettingsSchema;\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\nself.actionSources = function() {\n return {\n 'sliceClick': {\n name: 'widget-action.pie-slice-click',\n multiple: false\n }\n };\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'pie'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.pieSettingsSchema();\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.pieDatakeySettingsSchema();\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\nself.actionSources = function() {\n return {\n 'sliceClick': {\n name: 'widget-action.pie-slice-click',\n multiple: false\n }\n };\n}\n", "settingsSchema": "{}\n", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.6114638304362894,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.9955906536344441,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.9430835931647599,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"radius\":1,\"fontColor\":\"#545454\",\"fontSize\":10,\"decimals\":1,\"legend\":{\"show\":true,\"position\":\"nw\",\"labelBoxBorderColor\":\"#CCCCCC\",\"backgroundColor\":\"#F0F0F0\",\"backgroundOpacity\":0.85},\"innerRadius\":0,\"showLabels\":true,\"showPercentages\":true,\"stroke\":{\"width\":5},\"tilt\":1,\"animatedPie\":false},\"title\":\"Pie - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" @@ -91,7 +91,7 @@ ], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n \n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n \n pieData.datasets.push(dataset);\n \n for (var i=0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n pieData.labels.push(dataKey.label);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n var borderColor = tinycolor(dataKey.color).darken();\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push('#fff');\n dataset.borderWidth.push(5);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n\n var ctx = $('#pieChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'pie',\n data: pieData,\n options: {\n responsive: false,\n maintainAspectRatio: false\n }\n }); \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = tvPair[1];\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n self.ctx.chart.resize();\n}\n", + "controllerScript": "self.onInit = function() {\n \n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n \n pieData.datasets.push(dataset);\n \n for (var i=0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n var units = dataKey.units && dataKey.units.length ? dataKey.units : self.ctx.units;\n units = units ? (' (' + units + ')') : '';\n pieData.labels.push(dataKey.label + units);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n var borderColor = tinycolor(dataKey.color).darken();\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push('#fff');\n dataset.borderWidth.push(5);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n\n var ctx = $('#pieChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'pie',\n data: pieData,\n options: {\n responsive: false,\n maintainAspectRatio: false\n }\n }); \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var decimals;\n if (typeof cellData.dataKey.decimals !== 'undefined' \n && cellData.dataKey.decimals !== null ) {\n decimals = cellData.dataKey.decimals; \n } else {\n decimals = self.ctx.decimals;\n }\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = self.ctx.utils.formatValue(tvPair[1], decimals);\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n self.ctx.chart.resize();\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Pie - Chart.js\"}" @@ -111,7 +111,7 @@ ], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n \n pieData.datasets.push(dataset);\n \n for (var i = 0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n pieData.labels.push(dataKey.label);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n var borderColor = tinycolor(dataKey.color).darken();\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push('#fff');\n dataset.borderWidth.push(5);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n\n var ctx = $('#pieChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'polarArea',\n data: pieData,\n options: {\n responsive: false,\n maintainAspectRatio: false\n }\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = tvPair[1];\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n \n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n if (self.ctx.height >= 70) {\n try {\n self.ctx.chart.resize();\n } catch (e) {}\n }\n}\n", + "controllerScript": "self.onInit = function() {\n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n \n pieData.datasets.push(dataset);\n \n for (var i = 0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n var units = dataKey.units && dataKey.units.length ? dataKey.units : self.ctx.units;\n units = units ? (' (' + units + ')') : '';\n pieData.labels.push(dataKey.label + units);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n var borderColor = tinycolor(dataKey.color).darken();\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push('#fff');\n dataset.borderWidth.push(5);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n\n var ctx = $('#pieChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'polarArea',\n data: pieData,\n options: {\n responsive: false,\n maintainAspectRatio: false\n }\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var decimals;\n if (typeof cellData.dataKey.decimals !== 'undefined' \n && cellData.dataKey.decimals !== null ) {\n decimals = cellData.dataKey.decimals; \n } else {\n decimals = self.ctx.decimals;\n }\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = self.ctx.utils.formatValue(tvPair[1], decimals);\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n \n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n if (self.ctx.height >= 70) {\n try {\n self.ctx.chart.resize();\n } catch (e) {}\n }\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fifth\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.2074391823443591,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Polar Area - Chart.js\"}" @@ -131,7 +131,7 @@ ], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n var barData = {\n labels: [],\n datasets: []\n };\n\n var backgroundColor = tinycolor(self.ctx.data[0].dataKey.color);\n backgroundColor.setAlpha(0.2);\n var borderColor = tinycolor(self.ctx.data[0].dataKey.color);\n borderColor.setAlpha(1);\n var dataset = {\n label: self.ctx.datasources[0].name,\n data: [],\n backgroundColor: backgroundColor.toRgbString(),\n borderColor: borderColor.toRgbString(),\n pointBackgroundColor: borderColor.toRgbString(),\n pointBorderColor: borderColor.darken().toRgbString(),\n borderWidth: 1\n }\n \n barData.datasets.push(dataset);\n \n for (var i = 0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n barData.labels.push(dataKey.label);\n dataset.data.push(0);\n }\n\n var ctx = $('#radarChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'radar',\n data: barData,\n options: {\n responsive: false,\n maintainAspectRatio: false\n }\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = tvPair[1];\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n } \n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n if (self.ctx.height >= 70) {\n self.ctx.chart.resize();\n }\n}\n", + "controllerScript": "self.onInit = function() {\n var barData = {\n labels: [],\n datasets: []\n };\n\n var backgroundColor = tinycolor(self.ctx.data[0].dataKey.color);\n backgroundColor.setAlpha(0.2);\n var borderColor = tinycolor(self.ctx.data[0].dataKey.color);\n borderColor.setAlpha(1);\n var dataset = {\n label: self.ctx.datasources[0].name,\n data: [],\n backgroundColor: backgroundColor.toRgbString(),\n borderColor: borderColor.toRgbString(),\n pointBackgroundColor: borderColor.toRgbString(),\n pointBorderColor: borderColor.darken().toRgbString(),\n borderWidth: 1\n }\n \n barData.datasets.push(dataset);\n \n for (var i = 0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n var units = dataKey.units && dataKey.units.length ? dataKey.units : self.ctx.units;\n units = units ? (' (' + units + ')') : '';\n barData.labels.push(dataKey.label + units);\n dataset.data.push(0);\n }\n\n var ctx = $('#radarChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'radar',\n data: barData,\n options: {\n responsive: false,\n maintainAspectRatio: false\n }\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var decimals;\n if (typeof cellData.dataKey.decimals !== 'undefined' \n && cellData.dataKey.decimals !== null ) {\n decimals = cellData.dataKey.decimals; \n } else {\n decimals = self.ctx.decimals;\n }\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = self.ctx.utils.formatValue(tvPair[1], decimals);\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n } \n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n if (self.ctx.height >= 70) {\n self.ctx.chart.resize();\n }\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Radar - Chart.js\"}" @@ -166,7 +166,7 @@ "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'state'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true\n };\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\",\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":false,\"tooltipIndividual\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"smoothLines\":false},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\",\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":false,\"tooltipIndividual\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"smoothLines\":false},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"direction\":\"column\",\",position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}" } } ] diff --git a/application/src/main/data/json/system/widget_bundles/control_widgets.json b/application/src/main/data/json/system/widget_bundles/control_widgets.json index 9f1b8cea10..630b9f830a 100644 --- a/application/src/main/data/json/system/widget_bundles/control_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/control_widgets.json @@ -15,7 +15,7 @@ "resources": [], "templateHtml": "
", "templateCss": ".cmd .cursor.blink {\n -webkit-animation-name: terminal-underline;\n -moz-animation-name: terminal-underline;\n -ms-animation-name: terminal-underline;\n animation-name: terminal-underline;\n}\n.terminal .inverted, .cmd .inverted {\n border-bottom-color: #aaa;\n}\n", - "controllerScript": "var requestTimeout = 500;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetDeviceName && subscription.targetDeviceName.length) {\n deviceName = subscription.targetDeviceName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n var greetings = 'Welcome to ThingsBoard RPC debug terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = angular.copy(command).trim();\n if (localCommand == 'help') {\n printUsage(this);\n } else {\n var cmdObj = $.terminal.parse_command(localCommand);\n if (cmdObj.args.length > 1) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (cmdObj.args.length && cmdObj.args[0]) {\n try {\n params = JSON.parse(cmdObj.args[0]);\n } catch (e) {\n params = cmdObj.args[0];\n }\n }\n performRpc(this, cmdObj.name, params);\n }\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt\n });\n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' [params body]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1:]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2:]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": 2, \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\nfunction performRpc(terminal, method, params) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout).then(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\n \nself.onDestroy = function() {\n}\n", + "controllerScript": "var requestTimeout = 500;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetDeviceName && subscription.targetDeviceName.length) {\n deviceName = subscription.targetDeviceName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n var greetings = 'Welcome to ThingsBoard RPC debug terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = command.trim();\n if (localCommand === 'help') {\n printUsage(this);\n } else {\n var cmdObj = $.terminal.parse_command(localCommand);\n if (cmdObj.args.length > 1) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (cmdObj.args.length && cmdObj.args[0]) {\n try {\n params = JSON.parse(cmdObj.args[0]);\n } catch (e) {\n params = cmdObj.args[0];\n }\n }\n performRpc(this, cmdObj.name, params);\n }\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt,\n enabled: rpcEnabled\n });\n \n \n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' [params body]]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1:]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2:]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": 2, \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\nfunction performRpc(terminal, method, params) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout).subscribe(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\n \nself.onDestroy = function() {\n}\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"requestTimeout\": {\n \"title\": \"RPC request timeout (ms)\",\n \"type\": \"number\",\n \"default\": 500\n }\n },\n \"required\": [\"requestTimeout\"]\n },\n \"form\": [\n \"requestTimeout\"\n ]\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":true,\"backgroundColor\":\"#010101\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"parseGpioStatusFunction\":\"return body[pin] === true;\",\"gpioStatusChangeRequest\":{\"method\":\"setGpioStatus\",\"paramsBody\":\"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"},\"requestTimeout\":500,\"switchPanelBackgroundColor\":\"#b71c1c\",\"gpioStatusRequest\":{\"method\":\"getGpioStatus\",\"paramsBody\":\"{}\"},\"gpioList\":[{\"pin\":1,\"label\":\"GPIO 1\",\"row\":0,\"col\":0,\"_uniqueKey\":0},{\"pin\":2,\"label\":\"GPIO 2\",\"row\":0,\"col\":1,\"_uniqueKey\":1},{\"pin\":3,\"label\":\"GPIO 3\",\"row\":1,\"col\":0,\"_uniqueKey\":2}]},\"title\":\"RPC debug terminal\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" @@ -31,7 +31,7 @@ "resources": [], "templateHtml": "
", "templateCss": ".cmd .cursor.blink {\n -webkit-animation-name: terminal-underline;\n -moz-animation-name: terminal-underline;\n -ms-animation-name: terminal-underline;\n animation-name: terminal-underline;\n}\n.terminal .inverted, .cmd .inverted {\n border-bottom-color: #aaa;\n}\n", - "controllerScript": "var requestTimeout = 500;\nconst commandStatusPollingInterval = 200;\n\nconst welcome = 'Welcome to ThingsBoard RPC remote shell.\\n';\n\nvar terminal, rpcEnabled, simulated, deviceName, cwd;\nvar commandExecuting = false;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n rpcEnabled = subscription.rpcEnabled;\n if (subscription.targetDeviceName && subscription.targetDeviceName.length) {\n deviceName = subscription.targetDeviceName;\n } else {\n deviceName = 'Simulated';\n simulated = true;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n \n terminal = $('#device-terminal', self.ctx.$container).terminal(\n function (command) {\n if (command && command.trim().length) {\n try {\n if (simulated) {\n this.echo(command);\n } else {\n sendCommand(this, command);\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: false,\n prompt: rpcEnabled ? currentPrompt : '',\n name: 'shell',\n pauseEvents: false,\n keydown: (e, term) => {\n if ((e.which == 67 || e.which == 68) && e.ctrlKey) { // CTRL+C || CTRL+D\n if (commandExecuting) {\n terminateCommand(term);\n return false;\n }\n }\n },\n onInit: initTerm\n }\n );\n}\n\nfunction initTerm(terminal) {\n terminal.echo(welcome);\n if (!rpcEnabled) {\n terminal.error('Target device is not set!\\n');\n } else {\n terminal.echo(`Current target device for RPC terminal: [[b;#fff;]${deviceName}]\\n`);\n if (!simulated) {\n terminal.pause();\n getTermInfo(terminal,\n (remoteTermInfo) => {\n if (remoteTermInfo) {\n terminal.echo(`Remote platform info:`);\n terminal.echo(`OS: [[b;#fff;]${remoteTermInfo.platform}]`);\n if (remoteTermInfo.release) {\n terminal.echo(`OS release: [[b;#fff;]${remoteTermInfo.release}]`);\n }\n terminal.echo('\\r');\n } else {\n terminal.echo('[[;#f00;]Unable to get remote platform info.\\nDevice is not responding.]\\n');\n }\n terminal.resume();\n });\n }\n }\n}\n\nfunction currentPrompt(callback) {\n if (cwd) {\n callback('[[b;#2196f3;]' + deviceName +']: [[b;#8bc34a;]' + cwd +']> ');\n } else {\n callback('[[b;#8bc34a;]' + deviceName +']> ');\n }\n}\n\nfunction getTermInfo(terminal, callback) {\n self.ctx.controlApi.sendTwoWayCommand('getTermInfo', null, requestTimeout).then(\n (termInfo) => {\n cwd = termInfo.cwd;\n if (callback) {\n callback(termInfo);\n } \n },\n () => {\n if (callback) {\n callback(null);\n }\n }\n );\n}\n\nfunction sendCommand(terminal, command) {\n terminal.pause();\n var sendCommandRequest = {\n command: command,\n cwd: cwd\n };\n self.ctx.controlApi.sendTwoWayCommand('sendCommand', sendCommandRequest, requestTimeout).then(\n (responseBody) => {\n if (responseBody && responseBody.ok) {\n commandExecuting = true;\n setTimeout( pollCommandStatus.bind(null,terminal), commandStatusPollingInterval );\n } else {\n var error = responseBody ? responseBody.error : 'Unhandled error.';\n terminal.error(error);\n terminal.resume();\n }\n },\n () => {\n onRpcError(terminal);\n }\n );\n}\n\nfunction terminateCommand(terminal) {\n self.ctx.controlApi.sendTwoWayCommand('terminateCommand', null, requestTimeout).then(\n (responseBody) => {\n if (!responseBody.ok) {\n commandExecuting = false;\n terminal.error(responseBody.error);\n terminal.resume();\n } \n },\n () => {\n onRpcError(terminal);\n }\n ); \n}\n\nfunction onRpcError(terminal) {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.resume();\n}\n\nfunction pollCommandStatus(terminal) {\n self.ctx.controlApi.sendTwoWayCommand('getCommandStatus', null, requestTimeout).then(\n (commandStatusResponse) => {\n commandStatusResponse.data.forEach((dataElement) => {\n if (dataElement.stdout) {\n terminal.echo(dataElement.stdout);\n }\n if (dataElement.stderr) {\n terminal.error(dataElement.stderr);\n }\n }); \n if (commandStatusResponse.done) {\n commandExecuting = false;\n cwd = commandStatusResponse.cwd;\n terminal.resume();\n } else {\n var interval = commandStatusPollingInterval;\n if (!commandStatusResponse.data.length) {\n interval *=5;\n }\n setTimeout( pollCommandStatus.bind(null,terminal), interval );\n }\n },\n () => {\n commandExecuting = false;\n onRpcError(terminal);\n }\n );\n}\n\nself.onResize = function () {\n if (terminal) {\n terminal.resize(self.ctx.width, self.ctx.height);\n }\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "var requestTimeout = 500;\nvar commandStatusPollingInterval = 200;\n\nvar welcome = 'Welcome to ThingsBoard RPC remote shell.\\n';\n\nvar terminal, rpcEnabled, simulated, deviceName, cwd;\nvar commandExecuting = false;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n rpcEnabled = subscription.rpcEnabled;\n if (subscription.targetDeviceName && subscription.targetDeviceName.length) {\n deviceName = subscription.targetDeviceName;\n } else {\n deviceName = 'Simulated';\n simulated = true;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n \n terminal = $('#device-terminal', self.ctx.$container).terminal(\n function (command) {\n if (command && command.trim().length) {\n try {\n if (simulated) {\n this.echo(command);\n } else {\n sendCommand(this, command);\n }\n } catch(e) {\n this.error(e + '');\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: false,\n enabled: rpcEnabled,\n prompt: rpcEnabled ? currentPrompt : '',\n name: 'shell',\n pauseEvents: false,\n keydown: function (e, term) {\n if ((e.which == 67 || e.which == 68) && e.ctrlKey) { // CTRL+C || CTRL+D\n if (commandExecuting) {\n terminateCommand(term);\n return false;\n }\n }\n },\n onInit: initTerm\n }\n );\n \n};\n\nfunction initTerm(terminal) {\n terminal.echo(welcome);\n if (!rpcEnabled) {\n terminal.error('Target device is not set!\\n');\n } else {\n terminal.echo('Current target device for RPC terminal: [[b;#fff;]' + deviceName + ']\\n');\n if (!simulated) {\n terminal.pause();\n getTermInfo(terminal,\n function (remoteTermInfo) {\n if (remoteTermInfo) {\n terminal.echo('Remote platform info:');\n terminal.echo('OS: [[b;#fff;]' + remoteTermInfo.platform + ']');\n if (remoteTermInfo.release) {\n terminal.echo('OS release: [[b;#fff;]' + remoteTermInfo.release + ']');\n }\n terminal.echo('\\r');\n } else {\n terminal.echo('[[;#f00;]Unable to get remote platform info.\\nDevice is not responding.]\\n');\n }\n terminal.resume();\n });\n }\n }\n}\n\nfunction currentPrompt(callback) {\n if (cwd) {\n callback('[[b;#2196f3;]' + deviceName +']: [[b;#8bc34a;]' + cwd +']> ');\n } else {\n callback('[[b;#8bc34a;]' + deviceName +']> ');\n }\n}\n\nfunction getTermInfo(terminal, callback) {\n self.ctx.controlApi.sendTwoWayCommand('getTermInfo', null, requestTimeout).subscribe(\n function (termInfo) {\n cwd = termInfo.cwd;\n if (callback) {\n callback(termInfo);\n } \n },\n function () {\n if (callback) {\n callback(null);\n }\n }\n );\n}\n\nfunction sendCommand(terminal, command) {\n terminal.pause();\n var sendCommandRequest = {\n command: command,\n cwd: cwd\n };\n self.ctx.controlApi.sendTwoWayCommand('sendCommand', sendCommandRequest, requestTimeout).subscribe(\n function (responseBody) {\n if (responseBody && responseBody.ok) {\n commandExecuting = true;\n setTimeout( pollCommandStatus.bind(null,terminal), commandStatusPollingInterval );\n } else {\n var error = responseBody ? responseBody.error : 'Unhandled error.';\n terminal.error(error);\n terminal.resume();\n }\n },\n function () {\n onRpcError(terminal);\n }\n );\n}\n\nfunction terminateCommand(terminal) {\n self.ctx.controlApi.sendTwoWayCommand('terminateCommand', null, requestTimeout).subscribe(\n function (responseBody) {\n if (!responseBody.ok) {\n commandExecuting = false;\n terminal.error(responseBody.error);\n terminal.resume();\n } \n },\n function () {\n onRpcError(terminal);\n }\n ); \n}\n\nfunction onRpcError(terminal) {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.resume();\n}\n\nfunction pollCommandStatus(terminal) {\n self.ctx.controlApi.sendTwoWayCommand('getCommandStatus', null, requestTimeout).subscribe(\n function (commandStatusResponse) {\n for (var i=0;i", + "templateHtml": "", "templateCss": "", - "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n scope.ctx = self.ctx;\n}\n\nself.onResize = function() {\n if (self.ctx.resize) {\n self.ctx.resize();\n }\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\",\n \"default\": 100\n },\n \"initialValue\": {\n \"title\": \"Initial value\",\n \"type\": \"number\",\n \"default\": 50\n },\n \"title\": {\n \"title\": \"Knob title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"getValueMethod\": {\n \"title\": \"Get value method\",\n \"type\": \"string\",\n \"default\": \"getValue\"\n },\n \"setValueMethod\": {\n \"title\": \"Set value method\",\n \"type\": \"string\",\n \"default\": \"setValue\"\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout\",\n \"type\": \"number\",\n \"default\": 500\n }\n },\n \"required\": [\"minValue\", \"maxValue\", \"getValueMethod\", \"setValueMethod\", \"requestTimeout\"]\n },\n \"form\": [\n \"minValue\",\n \"maxValue\",\n \"initialValue\",\n \"title\",\n \"getValueMethod\",\n \"setValueMethod\",\n \"requestTimeout\"\n ]\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":500,\"maxValue\":100,\"initialValue\":50,\"minValue\":0,\"title\":\"Knob control\",\"getValueMethod\":\"getValue\",\"setValueMethod\":\"setValue\"},\"title\":\"Knob Control\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"decimals\":2}" @@ -61,9 +61,9 @@ "sizeX": 4, "sizeY": 2.5, "resources": [], - "templateHtml": "", + "templateHtml": "", "templateCss": "", - "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n scope.ctx = self.ctx;\n}\n\nself.onResize = function() {\n if (self.ctx.resize) {\n self.ctx.resize();\n }\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"initialValue\": {\n \"title\": \"Initial value\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"title\": {\n \"title\": \"Switch title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showOnOffLabels\": {\n \"title\": \"Show on/off labels\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"retrieveValueMethod\": {\n \"title\": \"Retrieve on/off value using method\",\n \"type\": \"string\",\n \"default\": \"rpc\"\n },\n \"valueKey\": {\n \"title\": \"Attribute/Timeseries value key (only when subscribe for attribute/timeseries method)\",\n \"type\": \"string\",\n \"default\": \"value\"\n },\n \"getValueMethod\": {\n \"title\": \"RPC get value method\",\n \"type\": \"string\",\n \"default\": \"getValue\"\n },\n \"setValueMethod\": {\n \"title\": \"RPC set value method\",\n \"type\": \"string\",\n \"default\": \"setValue\"\n },\n \"parseValueFunction\": {\n \"title\": \"Parse value function, f(data), returns boolean\",\n \"type\": \"string\",\n \"default\": \"return data ? true : false;\"\n },\n \"convertValueFunction\": {\n \"title\": \"Convert value function, f(value), returns payload used by RPC set value method\",\n \"type\": \"string\",\n \"default\": \"return value;\"\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout\",\n \"type\": \"number\",\n \"default\": 500\n }\n },\n \"required\": [\"requestTimeout\"]\n },\n \"form\": [\n \"initialValue\",\n \"title\",\n \"showOnOffLabels\",\n {\n \"key\": \"retrieveValueMethod\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"none\",\n \"label\": \"Don't retrieve\"\n },\n {\n \"value\": \"rpc\",\n \"label\": \"Call RPC get value method\"\n },\n {\n \"value\": \"attribute\",\n \"label\": \"Subscribe for attribute\"\n },\n {\n \"value\": \"timeseries\",\n \"label\": \"Subscribe for timeseries\"\n }\n ]\n },\n \"valueKey\",\n \"getValueMethod\",\n \"setValueMethod\",\n {\n \"key\": \"parseValueFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"convertValueFunction\",\n \"type\": \"javascript\"\n },\n \"requestTimeout\"\n ]\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":500,\"initialValue\":false,\"getValueMethod\":\"getValue\",\"setValueMethod\":\"setValue\",\"showOnOffLabels\":true,\"title\":\"Switch control\"},\"title\":\"Switch Control\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"decimals\":2}" @@ -77,9 +77,9 @@ "sizeX": 2.5, "sizeY": 2, "resources": [], - "templateHtml": "", + "templateHtml": "", "templateCss": "", - "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n scope.ctx = self.ctx;\n}\n\nself.onResize = function() {\n if (self.ctx.resize) {\n self.ctx.resize();\n }\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"initialValue\": {\n \"title\": \"Initial value\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"title\": {\n \"title\": \"Switch title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"retrieveValueMethod\": {\n \"title\": \"Retrieve on/off value using method\",\n \"type\": \"string\",\n \"default\": \"rpc\"\n },\n \"valueKey\": {\n \"title\": \"Attribute/Timeseries value key (only when subscribe for attribute/timeseries method)\",\n \"type\": \"string\",\n \"default\": \"value\"\n },\n \"getValueMethod\": {\n \"title\": \"RPC get value method\",\n \"type\": \"string\",\n \"default\": \"getValue\"\n },\n \"setValueMethod\": {\n \"title\": \"RPC set value method\",\n \"type\": \"string\",\n \"default\": \"setValue\"\n },\n \"parseValueFunction\": {\n \"title\": \"Parse value function, f(data), returns boolean\",\n \"type\": \"string\",\n \"default\": \"return data ? true : false;\"\n },\n \"convertValueFunction\": {\n \"title\": \"Convert value function, f(value), returns payload used by RPC set value method\",\n \"type\": \"string\",\n \"default\": \"return value;\"\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout\",\n \"type\": \"number\",\n \"default\": 500\n }\n },\n \"required\": [\"requestTimeout\"]\n },\n \"form\": [\n \"initialValue\",\n \"title\",\n {\n \"key\": \"retrieveValueMethod\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"none\",\n \"label\": \"Don't retrieve\"\n },\n {\n \"value\": \"rpc\",\n \"label\": \"Call RPC get value method\"\n },\n {\n \"value\": \"attribute\",\n \"label\": \"Subscribe for attribute\"\n },\n {\n \"value\": \"timeseries\",\n \"label\": \"Subscribe for timeseries\"\n }\n ]\n },\n \"valueKey\",\n \"getValueMethod\",\n \"setValueMethod\",\n {\n \"key\": \"parseValueFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"convertValueFunction\",\n \"type\": \"javascript\"\n },\n \"requestTimeout\"\n ]\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":500,\"initialValue\":false,\"getValueMethod\":\"getValue\",\"setValueMethod\":\"setValue\",\"title\":\"Round switch\",\"retrieveValueMethod\":\"rpc\",\"valueKey\":\"value\",\"parseValueFunction\":\"return data ? true : false;\",\"convertValueFunction\":\"return value;\"},\"title\":\"Round switch\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"decimals\":2}" @@ -93,9 +93,9 @@ "sizeX": 2.5, "sizeY": 2.5, "resources": [], - "templateHtml": "", + "templateHtml": "", "templateCss": "", - "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n scope.ctx = self.ctx;\n}\n\nself.onResize = function() {\n if (self.ctx.resize) {\n self.ctx.resize();\n }\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"initialValue\": {\n \"title\": \"Initial value\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"title\": {\n \"title\": \"LED title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"ledColor\": {\n \"title\": \"LED Color\",\n \"type\": \"string\",\n \"default\": \"green\"\n },\n \"performCheckStatus\": {\n \"title\": \"Perform RPC device status check\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"checkStatusMethod\": {\n \"title\": \"RPC check device status method\",\n \"type\": \"string\",\n \"default\": \"checkStatus\"\n },\n \"retrieveValueMethod\": {\n \"title\": \"Retrieve led status value using method\",\n \"type\": \"string\",\n \"default\": \"attribute\"\n },\n \"valueAttribute\": {\n \"title\": \"Device attribute/timeseries containing led status value\",\n \"type\": \"string\",\n \"default\": \"value\"\n },\n \"parseValueFunction\": {\n \"title\": \"Parse led status value function, f(data), returns boolean\",\n \"type\": \"string\",\n \"default\": \"return data ? true : false;\"\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout (ms)\",\n \"type\": \"number\",\n \"default\": 500\n }\n },\n \"required\": [\"valueAttribute\", \"requestTimeout\"]\n },\n \"form\": [\n \"initialValue\",\n \"title\",\n {\n \"key\": \"ledColor\",\n \"type\": \"color\"\n },\n \"performCheckStatus\",\n \"checkStatusMethod\",\n {\n \"key\": \"retrieveValueMethod\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"attribute\",\n \"label\": \"Subscribe for attribute\"\n },\n {\n \"value\": \"timeseries\",\n \"label\": \"Subscribe for timeseries\"\n }\n ]\n },\n \"valueAttribute\",\n {\n \"key\": \"parseValueFunction\",\n \"type\": \"javascript\"\n },\n \"requestTimeout\"\n ]\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":500,\"initialValue\":true,\"title\":\"Led indicator\",\"ledColor\":\"#4caf50\",\"valueAttribute\":\"value\",\"retrieveValueMethod\":\"attribute\",\"parseValueFunction\":\"return data ? true : false;\",\"performCheckStatus\":true,\"checkStatusMethod\":\"checkStatus\"},\"title\":\"Led indicator\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"decimals\":2}" @@ -109,9 +109,9 @@ "sizeX": 4, "sizeY": 2, "resources": [], - "templateHtml": "
\n
\n {{title}}\n
\n
\n
\n \n {{buttonLabel}}\n \n
\n
\n
\n {{ error }}\n
\n
", - "templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .md-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}", - "controllerScript": "self.onInit = function() {\n let rpcEnabled = self.ctx.defaultSubscription.rpcEnabled;\n\n self.ctx.$scope.buttonLabel = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton.bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n if (!rpcEnabled) {\n self.ctx.$scope.error =\n 'Target device is not set!';\n }\n\n self.ctx.$scope.sendCommand = function() {\n var rpcMethod = self.ctx.settings.methodName;\n var rpcParams = self.ctx.settings.methodParams;\n var timeout = self.ctx.settings.requestTimeout;\n var oneWayElseTwoWay = self.ctx.settings.oneWayElseTwoWay ?\n true : false;\n\n var commandPromise;\n if (oneWayElseTwoWay) {\n commandPromise = self.ctx.controlApi.sendOneWayCommand(\n rpcMethod, rpcParams, timeout);\n } else {\n commandPromise = self.ctx.controlApi.sendTwoWayCommand(\n rpcMethod, rpcParams, timeout);\n }\n commandPromise.then(\n function success() {\n self.ctx.$scope.error = \"\";\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n }\n }\n );\n };\n\n};", + "templateHtml": "
\n
\n {{title}}\n
\n
\n
\n \n
\n
\n
\n {{ error }}\n
\n
", + "templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .mat-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}", + "controllerScript": "self.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges();\n });\n};\n\nfunction init() {\n let rpcEnabled = self.ctx.defaultSubscription.rpcEnabled;\n\n self.ctx.$scope.buttonLable = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton.bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n if (!rpcEnabled) {\n self.ctx.$scope.error =\n 'Target device is not set!';\n }\n\n self.ctx.$scope.sendCommand = function() {\n var rpcMethod = self.ctx.settings.methodName;\n var rpcParams = self.ctx.settings.methodParams;\n var timeout = self.ctx.settings.requestTimeout;\n var oneWayElseTwoWay = self.ctx.settings.oneWayElseTwoWay ?\n true : false;\n\n var commandPromise;\n if (oneWayElseTwoWay) {\n commandPromise = self.ctx.controlApi.sendOneWayCommand(\n rpcMethod, rpcParams, timeout);\n } else {\n commandPromise = self.ctx.controlApi.sendTwoWayCommand(\n rpcMethod, rpcParams, timeout);\n }\n commandPromise.subscribe(\n function success() {\n self.ctx.$scope.error = \"\";\n self.ctx.detectChanges();\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n self.ctx.detectChanges();\n }\n }\n );\n };\n}\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"title\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"buttonText\": {\n \"title\": \"Button label\",\n \"type\": \"string\",\n \"default\": \"Send RPC\"\n },\n \"oneWayElseTwoWay\": {\n \"title\": \"Is One Way Command\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"showError\": {\n \"title\": \"Show RPC command execution error\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"methodName\": {\n \"title\": \"RPC method\",\n \"type\": \"string\",\n \"default\": \"rpcCommand\"\n },\n \"methodParams\": {\n \"title\": \"RPC method params\",\n \"type\": \"string\",\n \"default\": \"{}\"\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout\",\n \"type\": \"number\",\n \"default\": 5000\n },\n \"styleButton\": {\n \"type\": \"object\",\n \"title\": \"Button Style\",\n \"properties\": {\n \"isRaised\": {\n \"type\": \"boolean\",\n \"title\": \"Raised\",\n \"default\": true\n },\n \"isPrimary\": {\n \"type\": \"boolean\",\n \"title\": \"Primary color\",\n \"default\": false\n },\n \"bgColor\": {\n \"type\": \"string\",\n \"title\": \"Button background color\",\n \"default\": null\n },\n \"textColor\": {\n \"type\": \"string\",\n \"title\": \"Button text color\",\n \"default\": null\n }\n }\n },\n \"required\": []\n }\n },\n \"form\": [\n \"title\",\n \"buttonText\",\n \"oneWayElseTwoWay\",\n \"showError\",\n \"methodName\",\n {\n \"key\": \"methodParams\",\n \"type\": \"json\"\n },\n \"requestTimeout\",\n {\n \"key\": \"styleButton\",\n \"items\": [\n \"styleButton.isRaised\",\n \"styleButton.isPrimary\",\n {\n \"key\": \"styleButton.bgColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"styleButton.textColor\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":5000,\"oneWayElseTwoWay\":true,\"buttonText\":\"Send RPC\",\"styleButton\":{\"isRaised\":true,\"isPrimary\":false},\"methodName\":\"rpcCommand\",\"methodParams\":\"{}\"},\"title\":\"RPC Button\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" @@ -125,13 +125,13 @@ "sizeX": 4, "sizeY": 2, "resources": [], - "templateHtml": "
\n
\n {{title}}\n
\n
\n
\n \n {{buttonLabel}}\n \n
\n
\n
\n {{ error }}\n
\n
", - "templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .md-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.buttonLabel = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n let entityAttributeType = self.ctx.settings.entityAttributeType;\n let entityParameters = JSON.parse(self.ctx.settings.entityParameters);\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton\n .bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n console.log(self.ctx);\n\n let attributeService = self.ctx.$scope.$injector.get('attributeService');\n\n self.ctx.$scope.sendUpdate = function() {\n let attributes = [];\n for (let key in entityParameters) {\n attributes.push({\n \"key\": key,\n \"value\": entityParameters[key]\n });\n }\n \n \n attributeService.saveEntityAttributes(\"DEVICE\", self.ctx.defaultSubscription.targetDeviceId,\n entityAttributeType, attributes).then(\n function success() {\n self.ctx.$scope.error = \"\";\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n }\n console.log(rejection);\n }\n\n );\n };\n\n};", + "templateHtml": "
\n
\n {{title}}\n
\n
\n
\n \n
\n
\n
\n {{ error }}\n
\n
", + "templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .mat-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}", + "controllerScript": "self.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges();\n });\n};\n\nfunction init() {\n self.ctx.$scope.buttonLable = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n let entityAttributeType = self.ctx.settings.entityAttributeType;\n let entityParameters = JSON.parse(self.ctx.settings.entityParameters);\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton\n .bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n let attributeService = self.ctx.$scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n\n self.ctx.$scope.sendUpdate = function() {\n let attributes = [];\n for (let key in entityParameters) {\n attributes.push({\n \"key\": key,\n \"value\": entityParameters[key]\n });\n }\n \n let entityId = {\n entityType: \"DEVICE\",\n id: self.ctx.defaultSubscription.targetDeviceId\n };\n attributeService.saveEntityAttributes(entityId,\n entityAttributeType, attributes).subscribe(\n function success() {\n self.ctx.$scope.error = \"\";\n self.ctx.detectChanges();\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n self.ctx.detectChanges();\n }\n }\n\n );\n };\n}\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"title\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"buttonText\": {\n \"title\": \"Button label\",\n \"type\": \"string\",\n \"default\": \"Update device attribute\"\n },\n \"entityAttributeType\": {\n \"title\": \"Device attribute scope\",\n \"type\": \"string\",\n \"default\": \"SERVER_SCOPE\"\n },\n \"entityParameters\": {\n \"title\": \"Device attribute parameters\",\n \"type\": \"string\",\n \"default\": \"{}\"\n },\n \"styleButton\": {\n \"type\": \"object\",\n \"title\": \"Button Style\",\n \"properties\": {\n \"isRaised\": {\n \"type\": \"boolean\",\n \"title\": \"Raised\",\n \"default\": true\n },\n \"isPrimary\": {\n \"type\": \"boolean\",\n \"title\": \"Primary color\",\n \"default\": false\n },\n \"bgColor\": {\n \"type\": \"string\",\n \"title\": \"Button background color\",\n \"default\": null\n },\n \"textColor\": {\n \"type\": \"string\",\n \"title\": \"Button text color\",\n \"default\": null\n }\n }\n },\n \"required\": []\n }\n },\n \"form\": [\n \"title\",\n \"buttonText\",\n {\n \"key\": \"entityAttributeType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [{\n \"value\": \"SERVER_SCOPE\",\n \"label\": \"Server attribute\"\n }, {\n \"value\": \"SHARED_SCOPE\",\n \"label\": \"Shared attribute\"\n }]\n }, {\n \"key\": \"entityParameters\",\n \"type\": \"json\"\n },\n {\n \"key\": \"styleButton\",\n \"items\": [\n \"styleButton.isRaised\",\n \"styleButton.isPrimary\",\n {\n \"key\": \"styleButton.bgColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"styleButton.textColor\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"styleButton\":{\"isRaised\":true,\"isPrimary\":false},\"entityParameters\":\"{}\",\"entityAttributeType\":\"SERVER_SCOPE\",\"buttonText\":\"Update device attribute\"},\"title\":\"Update device attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"targetDeviceAliases\":[]}" } } ] -} \ No newline at end of file +} diff --git a/application/src/main/data/json/system/widget_bundles/date.json b/application/src/main/data/json/system/widget_bundles/date.json index cdde7bcad3..da05b4e53c 100644 --- a/application/src/main/data/json/system/widget_bundles/date.json +++ b/application/src/main/data/json/system/widget_bundles/date.json @@ -13,9 +13,9 @@ "sizeX": 5, "sizeY": 5.5, "resources": [], - "templateHtml": "", + "templateHtml": "", "templateCss": "", - "controllerScript": "self.onInit = function() {\n scope = self.ctx.$scope;\n scope.ctx = self.ctx;\n}", + "controllerScript": "self.onInit = function() {\n}\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"hidePicker\": {\n \"title\": \"Hide date range picker\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"onePanel\": {\n \"title\": \"Date range picker one panel\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"autoConfirm\": {\n \"title\": \"Date range picker auto confirm\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showTemplate\": {\n \"title\": \"Date range picker show template\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"firstDayOfWeek\": {\n \"title\": \"First day of the week\",\n \"type\": \"number\",\n \"default\": 1\n },\n \"hideInterval\": {\n \"title\": \"Hide interval\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"initialInterval\": {\n\t\t\t\t\"title\": \"Initial interval\",\n\t\t\t\t\"type\": \"string\",\n\t\t\t\t\"default\": \"week\"\n\t\t\t},\n \"hideStepSize\": {\n \"title\": \"Hide step size\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"stepSize\": {\n\t\t\t\t\"title\": \"Initial step size\",\n\t\t\t\t\"type\": \"string\",\n\t\t\t\t\"default\": \"day\"\n\t\t\t},\n \"hideLabels\": {\n \"title\": \"Hide labels\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"useSessionStorage\": {\n \"title\": \"Use session storage\",\n \"type\": \"boolean\",\n \"default\": true\n }\n }\n },\n \"form\": [\n \"hidePicker\",\n\t\t\"onePanel\",\n\t\t\"autoConfirm\",\n\t\t\"showTemplate\",\n\t\t\"firstDayOfWeek\",\n \"hideInterval\",\n {\n\t\t\t\"key\": \"initialInterval\",\n\t\t\t\"type\": \"rc-select\",\n\t\t\t\"multiple\": false,\n\t\t\t\"items\": [\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"hour\",\n\t\t\t\t\t\"label\": \"Hour\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"day\",\n\t\t\t\t\t\"label\": \"Day\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"week\",\n\t\t\t\t\t\"label\": \"Week\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"twoWeeks\",\n\t\t\t\t\t\"label\": \"2 weeks\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"month\",\n\t\t\t\t\t\"label\": \"Month\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"threeMonths\",\n\t\t\t\t\t\"label\": \"3 months\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"sixMonths\",\n\t\t\t\t\t\"label\": \"6 months\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n \"hideStepSize\",\n {\n\t\t\t\"key\": \"stepSize\",\n\t\t\t\"type\": \"rc-select\",\n\t\t\t\"multiple\": false,\n\t\t\t\"items\": [\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"hour\",\n\t\t\t\t\t\"label\": \"Hour\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"day\",\n\t\t\t\t\t\"label\": \"Day\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"week\",\n\t\t\t\t\t\"label\": \"Week\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"twoWeeks\",\n\t\t\t\t\t\"label\": \"2 weeks\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"month\",\n\t\t\t\t\t\"label\": \"Month\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"threeMonths\",\n\t\t\t\t\t\"label\": \"3 months\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"sixMonths\",\n\t\t\t\t\t\"label\": \"6 months\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t\"hideLabels\",\n\t\t\"useSessionStorage\"\n ]\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"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\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"defaultInterval\":\"week\",\"stepSize\":\"day\"},\"title\":\"Date-range-navigator\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" diff --git a/application/src/main/data/json/system/widget_bundles/digital_gauges.json b/application/src/main/data/json/system/widget_bundles/digital_gauges.json index a0ad05de95..116b5c9c02 100644 --- a/application/src/main/data/json/system/widget_bundles/digital_gauges.json +++ b/application/src/main/data/json/system/widget_bundles/digital_gauges.json @@ -6,35 +6,19 @@ }, "widgetTypes": [ { - "alias": "digital_bar", - "name": "Digital horizontal bar", - "descriptor": { - "type": "latest", - "sizeX": 6, - "sizeY": 2.5, - "resources": [], - "templateHtml": "", - "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n}", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 80) {\\n\\tvalue = 80;\\n} else if (value > 160) {\\n\\tvalue = 160;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":180,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[\"#008000\",\"#fbc02d\",\"#f44336\"],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":18},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#ffffff\"},\"neonGlowBrightness\":40,\"dashThickness\":1.5,\"unitTitle\":\"MPH\",\"showUnitTitle\":true,\"gaugeColor\":\"#171a1c\",\"gaugeType\":\"horizontalBar\",\"showTitle\":false,\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Digital horizontal bar\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "digital_speedometer", - "name": "Digital speedometer", + "alias": "gauge_justgage", + "name": "Gauge - justGage", "descriptor": { "type": "latest", - "sizeX": 5, + "sizeX": 4, "sizeY": 3, "resources": [], "templateHtml": "", "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n}\n\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 45) {\\n\\tvalue = 45;\\n} else if (value > 130) {\\n\\tvalue = 130;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":180,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[\"#008000\",\"#fbc02d\",\"#f44336\"],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#ffffff\"},\"neonGlowBrightness\":40,\"dashThickness\":1.5,\"unitTitle\":\"MPH\",\"showUnitTitle\":true,\"gaugeColor\":\"#171a1c\",\"gaugeType\":\"arc\",\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Digital speedometer\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#999999\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"size\":36,\"color\":\"#666666\"},\"minMaxFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#666666\"},\"neonGlowBrightness\":0,\"decimals\":0,\"dashThickness\":0,\"gaugeColor\":\"#eeeeee\",\"showTitle\":true,\"gaugeType\":\"arc\"},\"title\":\"Gauge - justGage\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" } }, { @@ -47,58 +31,58 @@ "resources": [], "templateHtml": "", "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n}\n\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n}\n\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < -60) {\\n\\tvalue = 60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":60,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":1,\"levelColors\":[\"#304ffe\",\"#7e57c2\",\"#ff4081\",\"#d32f2f\"],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":18},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"dashThickness\":1.5,\"minValue\":-60,\"gaugeColor\":\"#333333\",\"neonGlowBrightness\":35,\"gaugeType\":\"donut\",\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Digital thermometer\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { - "alias": "digital_vertical_bar", - "name": "Digital vertical bar", + "alias": "digital_speedometer", + "name": "Digital speedometer", "descriptor": { "type": "latest", - "sizeX": 2.5, - "sizeY": 4.5, + "sizeX": 5, + "sizeY": 3, "resources": [], "templateHtml": "", "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n}\n\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":60,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[\"#3d5afe\",\"#f44336\"],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":14},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":8,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#cccccc\"},\"neonGlowBrightness\":20,\"showUnitTitle\":true,\"gaugeColor\":\"#171a1c\",\"gaugeType\":\"verticalBar\",\"showTitle\":false,\"minValue\":-60,\"dashThickness\":1.2,\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Digital vertical bar\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 45) {\\n\\tvalue = 45;\\n} else if (value > 130) {\\n\\tvalue = 130;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":180,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[\"#008000\",\"#fbc02d\",\"#f44336\"],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#ffffff\"},\"neonGlowBrightness\":40,\"dashThickness\":1.5,\"unitTitle\":\"MPH\",\"showUnitTitle\":true,\"gaugeColor\":\"#171a1c\",\"gaugeType\":\"arc\",\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Digital speedometer\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { - "alias": "gauge_justgage", - "name": "Gauge - justGage", + "alias": "neon_gauge_justgage", + "name": "Neon gauge - justGage", "descriptor": { "type": "latest", - "sizeX": 4, + "sizeX": 5, "sizeY": 3, "resources": [], "templateHtml": "", "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n}\n\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n}\n\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#999999\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"size\":36,\"color\":\"#666666\"},\"minMaxFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#666666\"},\"neonGlowBrightness\":0,\"decimals\":0,\"dashThickness\":0,\"gaugeColor\":\"#eeeeee\",\"showTitle\":true,\"gaugeType\":\"arc\"},\"title\":\"Gauge - justGage\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":70,\"dashThickness\":1,\"gaugeType\":\"arc\",\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Neon gauge - justGage\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { - "alias": "horizontal_bar_justgage", - "name": "Horizontal bar - justGage", + "alias": "lcd_gauge", + "name": "LCD gauge", "descriptor": { "type": "latest", - "sizeX": 7, + "sizeX": 5, "sizeY": 3, "resources": [], - "templateHtml": "\n", + "templateHtml": "", "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n}\n\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#999999\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"size\":18,\"color\":\"#666666\"},\"minMaxFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#666666\"},\"neonGlowBrightness\":0,\"decimals\":0,\"dashThickness\":0,\"gaugeColor\":\"#eeeeee\",\"showTitle\":true,\"gaugeType\":\"horizontalBar\"},\"title\":\"Horizontal bar - justGage\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 180) {\\n\\tvalue = 180;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#babab2\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":180,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\"linear\",\"refreshAnimationTime\":700,\"startAnimationType\":\"linear\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":0,\"dashThickness\":1.5,\"decimals\":0,\"unitTitle\":\"MPH\",\"showUnitTitle\":true,\"defaultColor\":\"#444444\",\"gaugeType\":\"arc\"},\"title\":\"LCD gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" } }, { @@ -111,106 +95,122 @@ "resources": [], "templateHtml": "", "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#babab2\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\"linear\",\"refreshAnimationTime\":700,\"startAnimationType\":\"linear\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"400\",\"size\":16},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":0,\"dashThickness\":1.5,\"decimals\":0,\"showUnitTitle\":true,\"defaultColor\":\"#444444\",\"gaugeType\":\"verticalBar\",\"units\":\"%\"},\"title\":\"LCD bar gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" } }, { - "alias": "lcd_gauge", - "name": "LCD gauge", + "alias": "simple_neon_gauge_justgage", + "name": "Simple neon gauge - justGage", "descriptor": { "type": "latest", - "sizeX": 5, + "sizeX": 3, "sizeY": 3, "resources": [], "templateHtml": "", "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n}\n\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 180) {\\n\\tvalue = 180;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#babab2\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":180,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\"linear\",\"refreshAnimationTime\":700,\"startAnimationType\":\"linear\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":0,\"dashThickness\":1.5,\"decimals\":0,\"unitTitle\":\"MPH\",\"showUnitTitle\":true,\"defaultColor\":\"#444444\",\"gaugeType\":\"arc\"},\"title\":\"LCD gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#388e3c\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":1,\"levelColors\":[],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":40,\"dashThickness\":1.5,\"gaugeType\":\"donut\",\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Simple neon gauge - justGage\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { - "alias": "mini_gauge_justgage", - "name": "Mini gauge - justGage", + "alias": "vertical_bar_justgage", + "name": "Vertical bar - justGage", "descriptor": { "type": "latest", "sizeX": 2, - "sizeY": 2, + "sizeY": 3.5, "resources": [], "templateHtml": "", "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n}\n\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n}\n\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#7cb342\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":0,\"dashThickness\":0,\"decimals\":0,\"roundedLineCap\":true,\"gaugeType\":\"donut\"},\"title\":\"Mini gauge - justGage\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#f57c00\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#999999\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"size\":12,\"color\":\"#666666\"},\"minMaxFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#666666\"},\"neonGlowBrightness\":0,\"decimals\":0,\"dashThickness\":1.5,\"gaugeColor\":\"#eeeeee\",\"showTitle\":false,\"gaugeType\":\"verticalBar\"},\"title\":\"Vertical bar - justGage\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" } }, { - "alias": "neon_gauge_justgage", - "name": "Neon gauge - justGage", + "alias": "simple_gauge_justgage", + "name": "Simple gauge - justGage", "descriptor": { "type": "latest", - "sizeX": 5, - "sizeY": 3, + "sizeX": 2, + "sizeY": 2, + "resources": [], + "templateHtml": "\n", + "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", + "controllerScript": "\nself.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n}\n\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}\n", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#ef6c00\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32,\"color\":\"#666666\"},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":0,\"dashThickness\":0,\"decimals\":0,\"gaugeColor\":\"#eeeeee\",\"gaugeType\":\"donut\"},\"title\":\"Simple gauge - justGage\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + } + }, + { + "alias": "digital_bar", + "name": "Digital horizontal bar", + "descriptor": { + "type": "latest", + "sizeX": 6, + "sizeY": 2.5, "resources": [], "templateHtml": "", "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n}\n\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n}", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":70,\"dashThickness\":1,\"gaugeType\":\"arc\",\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Neon gauge - justGage\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 80) {\\n\\tvalue = 80;\\n} else if (value > 160) {\\n\\tvalue = 160;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":180,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[\"#008000\",\"#fbc02d\",\"#f44336\"],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":18},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#ffffff\"},\"neonGlowBrightness\":40,\"dashThickness\":1.5,\"unitTitle\":\"MPH\",\"showUnitTitle\":true,\"gaugeColor\":\"#171a1c\",\"gaugeType\":\"horizontalBar\",\"showTitle\":false,\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Digital horizontal bar\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { - "alias": "simple_gauge_justgage", - "name": "Simple gauge - justGage", + "alias": "mini_gauge_justgage", + "name": "Mini gauge - justGage", "descriptor": { "type": "latest", "sizeX": 2, "sizeY": 2, "resources": [], - "templateHtml": "\n", + "templateHtml": "", "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", - "controllerScript": "\nself.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n}\n\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n}\n\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#ef6c00\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32,\"color\":\"#666666\"},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":0,\"dashThickness\":0,\"decimals\":0,\"gaugeColor\":\"#eeeeee\",\"gaugeType\":\"donut\"},\"title\":\"Simple gauge - justGage\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#7cb342\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":0,\"dashThickness\":0,\"decimals\":0,\"roundedLineCap\":true,\"gaugeType\":\"donut\"},\"title\":\"Mini gauge - justGage\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" } }, { - "alias": "simple_neon_gauge_justgage", - "name": "Simple neon gauge - justGage", + "alias": "horizontal_bar_justgage", + "name": "Horizontal bar - justGage", "descriptor": { "type": "latest", - "sizeX": 3, + "sizeX": 7, "sizeY": 3, "resources": [], - "templateHtml": "", + "templateHtml": "\n", "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n}\n\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n}\n\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#388e3c\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":1,\"levelColors\":[],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":40,\"dashThickness\":1.5,\"gaugeType\":\"donut\",\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Simple neon gauge - justGage\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#999999\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"size\":18,\"color\":\"#666666\"},\"minMaxFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#666666\"},\"neonGlowBrightness\":0,\"decimals\":0,\"dashThickness\":0,\"gaugeColor\":\"#eeeeee\",\"showTitle\":true,\"gaugeType\":\"horizontalBar\"},\"title\":\"Horizontal bar - justGage\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" } }, { - "alias": "vertical_bar_justgage", - "name": "Vertical bar - justGage", + "alias": "digital_vertical_bar", + "name": "Digital vertical bar", "descriptor": { "type": "latest", - "sizeX": 2, - "sizeY": 3.5, + "sizeX": 2.5, + "sizeY": 4.5, "resources": [], "templateHtml": "", "templateCss": "#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n}\n\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbCanvasDigitalGauge(self.ctx, 'digitalGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.onDestroy = function() {\n}\n\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#f57c00\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#999999\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"size\":12,\"color\":\"#666666\"},\"minMaxFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#666666\"},\"neonGlowBrightness\":0,\"decimals\":0,\"dashThickness\":1.5,\"gaugeColor\":\"#eeeeee\",\"showTitle\":false,\"gaugeType\":\"verticalBar\"},\"title\":\"Vertical bar - justGage\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":60,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[\"#3d5afe\",\"#f44336\"],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":14},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":8,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#cccccc\"},\"neonGlowBrightness\":20,\"showUnitTitle\":true,\"gaugeColor\":\"#171a1c\",\"gaugeType\":\"verticalBar\",\"showTitle\":false,\"minValue\":-60,\"dashThickness\":1.2,\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Digital vertical bar\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } } ] diff --git a/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json b/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json index b65445f3ca..f9d05d83f3 100644 --- a/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json @@ -13,12 +13,12 @@ "sizeX": 7.5, "sizeY": 6.5, "resources": [], - "templateHtml": "\n", + "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('entities-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}", "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, entity, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}", - "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Device admin table\",\"enableSelectColumnDisplay\":true},\"title\":\"device\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{\"headerButton\":[{\"id\":\"70837a9d-c3de-a9a7-03c5-dccd14998758\",\"name\":\"Add device\",\"icon\":\"add\",\"type\":\"customPretty\",\"customHtml\":\"\\n
\\n \\n
\\n

Add device

\\n \\n \\n \\n \\n
\\n
\\n \\n \\n \\n
\\n
\\n \\n \\n \\n
\\n
Device name is required.
\\n
\\n
\\n
\\n \\n \\n \\n \\n \\n \\n
\\n
\\n \\n \\n \\n \\n \\n \\n \\n \\n
\\n
\\n
\\n
\\n \\n Create\\n Cancel\\n \\n
\\n
\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet $mdDialog = $injector.get('$mdDialog'),\\n $document = $injector.get('$document'),\\n $q = $injector.get('$q'),\\n $rootScope = $injector.get('$rootScope'),\\n types = $injector.get('types'),\\n deviceService = $injector.get('deviceService'),\\n attributeService = $injector.get('attributeService');\\n \\nopenAddDeviceDialog();\\n\\nfunction openAddDeviceDialog() {\\n $mdDialog.show({\\n controller: ['$scope','$mdDialog', AddDeviceDialogController],\\n controllerAs: 'vm',\\n template: htmlTemplate,\\n parent: angular.element($document[0].body),\\n targetEvent: $event,\\n multiple: true,\\n clickOutsideToClose: false\\n });\\n}\\n\\nfunction AddDeviceDialogController($scope, $mdDialog) {\\n let vm = this;\\n vm.types = types;\\n vm.attributes = {};\\n \\n vm.cancel = () => {\\n $mdDialog.hide();\\n };\\n \\n vm.save = () => {\\n vm.loading = true;\\n $scope.addDeviceForm.$setPristine();\\n let device = {\\n name: vm.deviceName,\\n type: vm.deviceType,\\n label: vm.deviceLabel\\n };\\n deviceService.saveDevice(device).then(\\n (device) => {\\n saveAttributes(device.id).then(\\n () => {\\n vm.loading = false;\\n updateAliasData();\\n $mdDialog.hide();\\n }\\n );\\n },\\n () => {\\n vm.loading = false;\\n }\\n );\\n };\\n \\n function saveAttributes(entityId) {\\n let attributesArray = [];\\n for (let key in vm.attributes) {\\n attributesArray.push({key: key, value: vm.attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return $q.when([]);\\n }\\n }\\n \\n function updateAliasData() {\\n let aliasIds = [];\\n for (let id in widgetContext.aliasController.resolvedAliases) {\\n aliasIds.push(id);\\n }\\n let tasks = [];\\n aliasIds.forEach((aliasId) => {\\n widgetContext.aliasController.setAliasUnresolved(aliasId);\\n tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\\n });\\n $q.all(tasks).then(() => {\\n $rootScope.$broadcast('widgetForceReInit');\\n });\\n }\\n}\"}],\"actionCellButton\":[{\"id\":\"93931e52-5d7c-903e-67aa-b9435df44ff4\",\"name\":\"Edit device\",\"icon\":\"edit\",\"type\":\"customPretty\",\"customHtml\":\"\\n
\\n \\n
\\n

Edit device

\\n \\n \\n \\n \\n
\\n
\\n \\n \\n \\n
\\n
\\n \\n \\n \\n
\\n
Device name is required.
\\n
\\n
\\n
\\n \\n \\n \\n \\n \\n \\n
\\n
\\n \\n \\n \\n \\n \\n \\n \\n \\n
\\n
\\n
\\n
\\n \\n Create\\n Cancel\\n \\n
\\n
\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet $mdDialog = $injector.get('$mdDialog'),\\n $document = $injector.get('$document'),\\n $q = $injector.get('$q'),\\n $rootScope = $injector.get('$rootScope'),\\n types = $injector.get('types'),\\n deviceService = $injector.get('deviceService'),\\n attributeService = $injector.get('attributeService');\\n \\nopenEditDeviceDialog();\\n\\nfunction openEditDeviceDialog() {\\n $mdDialog.show({\\n controller: ['$scope','$mdDialog', EditDeviceDialogController],\\n controllerAs: 'vm',\\n template: htmlTemplate,\\n parent: angular.element($document[0].body),\\n targetEvent: $event,\\n multiple: true,\\n clickOutsideToClose: false\\n });\\n}\\n\\nfunction EditDeviceDialogController($scope,$mdDialog) {\\n let vm = this;\\n vm.types = types;\\n vm.loading = false;\\n vm.attributes = {};\\n \\n getEntityInfo();\\n \\n function getEntityInfo() {\\n vm.loading = true;\\n deviceService.getDevice(entityId.id).then(\\n (device) => {\\n attributeService.getEntityAttributesValues(entityId.entityType, entityId.id, 'SERVER_SCOPE').then(\\n (data) => {\\n if (data.length) {\\n getEntityAttributes(data);\\n }\\n vm.device = device;\\n vm.loading = false;\\n } \\n );\\n }\\n )\\n }\\n \\n vm.cancel = function() {\\n $mdDialog.hide();\\n };\\n \\n vm.save = () => {\\n vm.loading = true;\\n $scope.editDeviceForm.$setPristine();\\n deviceService.saveDevice(vm.device).then(\\n () => {\\n saveAttributes().then(\\n () => {\\n updateAliasData();\\n vm.loading = false;\\n $mdDialog.hide();\\n }\\n );\\n },\\n () => {\\n vm.loading = false;\\n }\\n );\\n }\\n \\n function getEntityAttributes(attributes) {\\n for (let i = 0; i < attributes.length; i++) {\\n vm.attributes[attributes[i].key] = attributes[i].value; \\n }\\n }\\n \\n function saveAttributes() {\\n let attributesArray = [];\\n for (let key in vm.attributes) {\\n attributesArray.push({key: key, value: vm.attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return $q.when([]);\\n }\\n }\\n \\n function updateAliasData() {\\n let aliasIds = [];\\n for (let id in widgetContext.aliasController.resolvedAliases) {\\n aliasIds.push(id);\\n }\\n let tasks = [];\\n aliasIds.forEach((aliasId) => {\\n widgetContext.aliasController.setAliasUnresolved(aliasId);\\n tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\\n });\\n console.log(widgetContext);\\n $q.all(tasks).then(() => {\\n $rootScope.$broadcast('widgetForceReInit');\\n });\\n }\\n}\\n\"},{\"id\":\"ec2708f6-9ff0-186b-e4fc-7635ebfa3074\",\"name\":\"Delete device\",\"icon\":\"delete\",\"type\":\"custom\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet $mdDialog = $injector.get('$mdDialog'),\\n $document = $injector.get('$document'),\\n types = $injector.get('types'),\\n deviceService = $injector.get('deviceService'),\\n $rootScope = $injector.get('$rootScope'),\\n $q = $injector.get('$q');\\n\\nopenDeleteDeviceDialog();\\n\\nfunction openDeleteDeviceDialog() {\\n let title = \\\"Are you sure you want to delete the device \\\" + entityName + \\\"?\\\";\\n let content = \\\"Be careful, after the confirmation, the device and all related data will become unrecoverable!\\\";\\n let confirm = $mdDialog.confirm()\\n .targetEvent($event)\\n .title(title)\\n .htmlContent(content)\\n .ariaLabel(title)\\n .cancel('Cancel')\\n .ok('Delete');\\n $mdDialog.show(confirm).then(() => {\\n deleteDevice();\\n })\\n}\\n\\nfunction deleteDevice() {\\n deviceService.deleteDevice(entityId.id).then(\\n () => {\\n updateAliasData();\\n }\\n );\\n}\\n\\nfunction updateAliasData() {\\n let aliasIds = [];\\n for (let id in widgetContext.aliasController.resolvedAliases) {\\n aliasIds.push(id);\\n }\\n let tasks = [];\\n aliasIds.forEach((aliasId) => {\\n widgetContext.aliasController.setAliasUnresolved(aliasId);\\n tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\\n });\\n $q.all(tasks).then(() => {\\n $rootScope.$broadcast('entityAliasesChanged', aliasIds);\\n });\\n}\"}]}}" + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Device admin table\",\"enableSelectColumnDisplay\":true},\"title\":\"Device admin table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{\"headerButton\":[{\"name\":\"Add device\",\"icon\":\"add\",\"type\":\"customPretty\",\"customHtml\":\"
\\n \\n

Add device

\\n \\n \\n
\\n \\n \\n
\\n
\\n
\\n \\n Device name\\n \\n \\n Device name is required.\\n \\n \\n
\\n \\n \\n Label\\n \\n \\n
\\n
\\n \\n Latitude\\n \\n \\n \\n Longitude\\n \\n \\n
\\n
\\n
\\n
\\n \\n \\n \\n
\\n
\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenAddDeviceDialog();\\n\\nfunction openAddDeviceDialog() {\\n customDialog.customDialog(htmlTemplate, AddDeviceDialogController).subscribe();\\n}\\n\\nfunction AddDeviceDialogController(instance) {\\n let vm = instance;\\n \\n vm.addDeviceFormGroup = vm.fb.group({\\n deviceName: ['', [vm.validators.required]],\\n deviceType: ['', [vm.validators.required]],\\n deviceLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.addDeviceFormGroup.markAsPristine();\\n let device = {\\n name: vm.addDeviceFormGroup.get('deviceName').value,\\n type: vm.addDeviceFormGroup.get('deviceType').value,\\n label: vm.addDeviceFormGroup.get('deviceLabel').value\\n };\\n deviceService.saveDevice(device).subscribe(\\n function (device) {\\n saveAttributes(device.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n function saveAttributes(entityId) {\\n let attributes = vm.addDeviceFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"70837a9d-c3de-a9a7-03c5-dccd14998758\"}],\"actionCellButton\":[{\"name\":\"Edit device\",\"icon\":\"edit\",\"type\":\"customPretty\",\"customHtml\":\"
\\n \\n

Edit device

\\n \\n \\n
\\n \\n \\n
\\n
\\n
\\n \\n Device name\\n \\n \\n Device name is required.\\n \\n \\n
\\n \\n \\n Label\\n \\n \\n
\\n
\\n \\n Latitude\\n \\n \\n \\n Longitude\\n \\n \\n
\\n
\\n
\\n
\\n \\n \\n \\n
\\n
\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenEditDeviceDialog();\\n\\nfunction openEditDeviceDialog() {\\n customDialog.customDialog(htmlTemplate, EditDeviceDialogController).subscribe();\\n}\\n\\nfunction EditDeviceDialogController(instance) {\\n let vm = instance;\\n \\n vm.device = null;\\n vm.attributes = {};\\n \\n vm.editDeviceFormGroup = vm.fb.group({\\n deviceName: ['', [vm.validators.required]],\\n deviceType: ['', [vm.validators.required]],\\n deviceLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.editDeviceFormGroup.markAsPristine();\\n vm.device.name = vm.editDeviceFormGroup.get('deviceName').value,\\n vm.device.type = vm.editDeviceFormGroup.get('deviceType').value,\\n vm.device.label = vm.editDeviceFormGroup.get('deviceLabel').value\\n deviceService.saveDevice(vm.device).subscribe(\\n function () {\\n saveAttributes().subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n getEntityInfo();\\n \\n function getEntityInfo() {\\n deviceService.getDevice(entityId.id).subscribe(\\n function (device) {\\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE',\\n ['latitude', 'longitude']).subscribe(\\n function (attributes) {\\n for (let i = 0; i < attributes.length; i++) {\\n vm.attributes[attributes[i].key] = attributes[i].value; \\n }\\n vm.device = device;\\n vm.editDeviceFormGroup.patchValue(\\n {\\n deviceName: vm.device.name,\\n deviceType: vm.device.type,\\n deviceLabel: vm.device.label,\\n attributes: {\\n latitude: vm.attributes.latitude,\\n longitude: vm.attributes.longitude\\n }\\n }, {emitEvent: false}\\n );\\n } \\n );\\n }\\n ); \\n }\\n \\n function saveAttributes() {\\n let attributes = vm.editDeviceFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, 'SERVER_SCOPE', attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"93931e52-5d7c-903e-67aa-b9435df44ff4\"},{\"name\":\"Delete device\",\"icon\":\"delete\",\"type\":\"custom\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet dialogs = $injector.get(widgetContext.servicesMap.get('dialogs'));\\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\\n\\nopenDeleteDeviceDialog();\\n\\nfunction openDeleteDeviceDialog() {\\n let title = \\\"Are you sure you want to delete the device \\\" + entityName + \\\"?\\\";\\n let content = \\\"Be careful, after the confirmation, the device and all related data will become unrecoverable!\\\";\\n dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe(\\n function (result) {\\n if (result) {\\n deleteDevice();\\n }\\n }\\n );\\n}\\n\\nfunction deleteDevice() {\\n deviceService.deleteDevice(entityId.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n }\\n );\\n}\\n\",\"id\":\"ec2708f6-9ff0-186b-e4fc-7635ebfa3074\"}]}}" } }, { @@ -29,13 +29,13 @@ "sizeX": 7.5, "sizeY": 6.5, "resources": [], - "templateHtml": "\n", + "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('entities-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}", "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, entity, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}", - "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Asset admin table\",\"enableSelectColumnDisplay\":true},\"title\":\"asset\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{\"headerButton\":[{\"id\":\"70837a9d-c3de-a9a7-03c5-dccd14998758\",\"name\":\"Add asset\",\"icon\":\"add\",\"type\":\"customPretty\",\"customHtml\":\"\\n
\\n \\n
\\n

Add asset

\\n \\n \\n \\n \\n
\\n
\\n \\n \\n \\n
\\n
\\n \\n \\n \\n
\\n
Asset name is required.
\\n
\\n
\\n \\n \\n
\\n \\n \\n \\n \\n \\n \\n \\n \\n
\\n
\\n
\\n
\\n \\n Create\\n Cancel\\n \\n
\\n
\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet $mdDialog = $injector.get('$mdDialog'),\\n $document = $injector.get('$document'),\\n $q = $injector.get('$q'),\\n $rootScope = $injector.get('$rootScope'),\\n types = $injector.get('types'),\\n assetService = $injector.get('assetService'),\\n attributeService = $injector.get('attributeService');\\n \\nopenAddAssetDialog();\\n\\nfunction openAddAssetDialog() {\\n $mdDialog.show({\\n controller: ['$scope','$mdDialog', AddAssetDialogController],\\n controllerAs: 'vm',\\n template: htmlTemplate,\\n parent: angular.element($document[0].body),\\n targetEvent: $event,\\n multiple: true,\\n clickOutsideToClose: false\\n });\\n}\\n\\nfunction AddAssetDialogController($scope, $mdDialog) {\\n let vm = this;\\n vm.types = types;\\n vm.attributes = {};\\n \\n vm.cancel = () => {\\n $mdDialog.hide();\\n };\\n \\n vm.save = () => {\\n vm.loading = true;\\n $scope.addAssetForm.$setPristine();\\n let asset = {\\n name: vm.assetName,\\n type: vm.assetType\\n };\\n assetService.saveAsset(asset).then(\\n (asset) => {\\n saveAttributes(asset.id).then(\\n () => {\\n vm.loading = false;\\n updateAliasData();\\n $mdDialog.hide();\\n }\\n );\\n },\\n () => {\\n vm.loading = false;\\n }\\n );\\n };\\n \\n function saveAttributes(entityId) {\\n let attributesArray = [];\\n for (let key in vm.attributes) {\\n attributesArray.push({key: key, value: vm.attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return $q.when([]);\\n }\\n }\\n \\n function updateAliasData() {\\n let aliasIds = [];\\n for (let id in widgetContext.aliasController.resolvedAliases) {\\n aliasIds.push(id);\\n }\\n let tasks = [];\\n aliasIds.forEach((aliasId) => {\\n widgetContext.aliasController.setAliasUnresolved(aliasId);\\n tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\\n });\\n $q.all(tasks).then(() => {\\n $rootScope.$broadcast('widgetForceReInit');\\n });\\n }\\n}\"}],\"actionCellButton\":[{\"id\":\"93931e52-5d7c-903e-67aa-b9435df44ff4\",\"name\":\"Edit asset\",\"icon\":\"edit\",\"type\":\"customPretty\",\"customHtml\":\"\\n
\\n \\n
\\n

Edit asset

\\n \\n \\n \\n \\n
\\n
\\n \\n \\n \\n
\\n
\\n \\n \\n \\n
\\n
Asset name is required.
\\n
\\n
\\n \\n \\n
\\n \\n \\n \\n \\n \\n \\n \\n \\n
\\n
\\n
\\n
\\n \\n Create\\n Cancel\\n \\n
\\n
\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet $mdDialog = $injector.get('$mdDialog'),\\n $document = $injector.get('$document'),\\n $q = $injector.get('$q'),\\n $rootScope = $injector.get('$rootScope'),\\n types = $injector.get('types'),\\n assetService = $injector.get('assetService'),\\n attributeService = $injector.get('attributeService');\\n \\nopenEditAssetDialog();\\n\\nfunction openEditAssetDialog() {\\n $mdDialog.show({\\n controller: ['$scope','$mdDialog', EditAssetDialogController],\\n controllerAs: 'vm',\\n template: htmlTemplate,\\n parent: angular.element($document[0].body),\\n targetEvent: $event,\\n multiple: true,\\n clickOutsideToClose: false\\n });\\n}\\n\\nfunction EditAssetDialogController($scope,$mdDialog) {\\n let vm = this;\\n vm.types = types;\\n vm.loading = false;\\n vm.attributes = {};\\n \\n getEntityInfo();\\n \\n function getEntityInfo() {\\n vm.loading = true;\\n assetService.getAsset(entityId.id).then(\\n (asset) => {\\n attributeService.getEntityAttributesValues(entityId.entityType, entityId.id, 'SERVER_SCOPE').then(\\n (data) => {\\n if (data.length) {\\n getEntityAttributes(data);\\n }\\n vm.asset = asset;\\n vm.loading = false;\\n } \\n );\\n }\\n )\\n }\\n \\n vm.cancel = function() {\\n $mdDialog.hide();\\n };\\n \\n vm.save = () => {\\n vm.loading = true;\\n $scope.editAssetForm.$setPristine();\\n assetService.saveAsset(vm.asset).then(\\n () => {\\n saveAttributes().then(\\n () => {\\n updateAliasData();\\n vm.loading = false;\\n $mdDialog.hide();\\n }\\n );\\n },\\n () => {\\n vm.loading = false;\\n }\\n );\\n }\\n \\n function getEntityAttributes(attributes) {\\n for (let i = 0; i < attributes.length; i++) {\\n vm.attributes[attributes[i].key] = attributes[i].value; \\n }\\n }\\n \\n function saveAttributes() {\\n let attributesArray = [];\\n for (let key in vm.attributes) {\\n attributesArray.push({key: key, value: vm.attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return $q.when([]);\\n }\\n }\\n \\n function updateAliasData() {\\n let aliasIds = [];\\n for (let id in widgetContext.aliasController.resolvedAliases) {\\n aliasIds.push(id);\\n }\\n let tasks = [];\\n aliasIds.forEach((aliasId) => {\\n widgetContext.aliasController.setAliasUnresolved(aliasId);\\n tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\\n });\\n console.log(widgetContext);\\n $q.all(tasks).then(() => {\\n $rootScope.$broadcast('widgetForceReInit');\\n });\\n }\\n}\\n\"},{\"id\":\"ec2708f6-9ff0-186b-e4fc-7635ebfa3074\",\"name\":\"Delete asset\",\"icon\":\"delete\",\"type\":\"custom\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet $mdDialog = $injector.get('$mdDialog'),\\n $document = $injector.get('$document'),\\n types = $injector.get('types'),\\n assetService = $injector.get('assetService'),\\n $rootScope = $injector.get('$rootScope'),\\n $q = $injector.get('$q');\\n\\nopenDeleteAssetDialog();\\n\\nfunction openDeleteAssetDialog() {\\n let title = \\\"Are you sure you want to delete the asset \\\" + entityName + \\\"?\\\";\\n let content = \\\"Be careful, after the confirmation, the asset and all related data will become unrecoverable!\\\";\\n let confirm = $mdDialog.confirm()\\n .targetEvent($event)\\n .title(title)\\n .htmlContent(content)\\n .ariaLabel(title)\\n .cancel('Cancel')\\n .ok('Delete');\\n $mdDialog.show(confirm).then(() => {\\n deleteAsset();\\n })\\n}\\n\\nfunction deleteAsset() {\\n assetService.deleteAsset(entityId.id).then(\\n () => {\\n updateAliasData();\\n }\\n );\\n}\\n\\nfunction updateAliasData() {\\n let aliasIds = [];\\n for (let id in widgetContext.aliasController.resolvedAliases) {\\n aliasIds.push(id);\\n }\\n let tasks = [];\\n aliasIds.forEach((aliasId) => {\\n widgetContext.aliasController.setAliasUnresolved(aliasId);\\n tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\\n });\\n $q.all(tasks).then(() => {\\n $rootScope.$broadcast('entityAliasesChanged', aliasIds);\\n });\\n}\"}]}}" + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Asset admin table\",\"enableSelectColumnDisplay\":true},\"title\":\"Asset admin table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{\"headerButton\":[{\"name\":\"Add asset\",\"icon\":\"add\",\"type\":\"customPretty\",\"customHtml\":\"
\\n \\n

Add asset

\\n \\n \\n
\\n \\n \\n
\\n
\\n
\\n \\n Asset name\\n \\n \\n Asset name is required.\\n \\n \\n
\\n \\n \\n Label\\n \\n \\n
\\n
\\n \\n Latitude\\n \\n \\n \\n Longitude\\n \\n \\n
\\n
\\n
\\n
\\n \\n \\n \\n
\\n
\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenAddAssetDialog();\\n\\nfunction openAddAssetDialog() {\\n customDialog.customDialog(htmlTemplate, AddAssetDialogController).subscribe();\\n}\\n\\nfunction AddAssetDialogController(instance) {\\n let vm = instance;\\n \\n vm.addAssetFormGroup = vm.fb.group({\\n assetName: ['', [vm.validators.required]],\\n assetType: ['', [vm.validators.required]],\\n assetLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.addAssetFormGroup.markAsPristine();\\n let asset = {\\n name: vm.addAssetFormGroup.get('assetName').value,\\n type: vm.addAssetFormGroup.get('assetType').value,\\n label: vm.addAssetFormGroup.get('assetLabel').value\\n };\\n assetService.saveAsset(asset).subscribe(\\n function (asset) {\\n saveAttributes(asset.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n function saveAttributes(entityId) {\\n let attributes = vm.addAssetFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"70837a9d-c3de-a9a7-03c5-dccd14998758\"}],\"actionCellButton\":[{\"name\":\"Edit asset\",\"icon\":\"edit\",\"type\":\"customPretty\",\"customHtml\":\"
\\n \\n

Edit asset

\\n \\n \\n
\\n \\n \\n
\\n
\\n
\\n \\n Asset name\\n \\n \\n Asset name is required.\\n \\n \\n
\\n \\n \\n Label\\n \\n \\n
\\n
\\n \\n Latitude\\n \\n \\n \\n Longitude\\n \\n \\n
\\n
\\n
\\n
\\n \\n \\n \\n
\\n
\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenEditAssetDialog();\\n\\nfunction openEditAssetDialog() {\\n customDialog.customDialog(htmlTemplate, EditAssetDialogController).subscribe();\\n}\\n\\nfunction EditAssetDialogController(instance) {\\n let vm = instance;\\n \\n vm.asset = null;\\n vm.attributes = {};\\n \\n vm.editAssetFormGroup = vm.fb.group({\\n assetName: ['', [vm.validators.required]],\\n assetType: ['', [vm.validators.required]],\\n assetLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.editAssetFormGroup.markAsPristine();\\n vm.asset.name = vm.editAssetFormGroup.get('assetName').value,\\n vm.asset.type = vm.editAssetFormGroup.get('assetType').value,\\n vm.asset.label = vm.editAssetFormGroup.get('assetLabel').value\\n assetService.saveAsset(vm.asset).subscribe(\\n function () {\\n saveAttributes().subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n getEntityInfo();\\n \\n function getEntityInfo() {\\n assetService.getAsset(entityId.id).subscribe(\\n function (asset) {\\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE',\\n ['latitude', 'longitude']).subscribe(\\n function (attributes) {\\n for (let i = 0; i < attributes.length; i++) {\\n vm.attributes[attributes[i].key] = attributes[i].value; \\n }\\n vm.asset = asset;\\n vm.editAssetFormGroup.patchValue(\\n {\\n assetName: vm.asset.name,\\n assetType: vm.asset.type,\\n assetLabel: vm.asset.label,\\n attributes: {\\n latitude: vm.attributes.latitude,\\n longitude: vm.attributes.longitude\\n }\\n }, {emitEvent: false}\\n );\\n } \\n );\\n }\\n ); \\n }\\n \\n function saveAttributes() {\\n let attributes = vm.editAssetFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, 'SERVER_SCOPE', attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"93931e52-5d7c-903e-67aa-b9435df44ff4\"},{\"name\":\"Delete asset\",\"icon\":\"delete\",\"type\":\"custom\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet dialogs = $injector.get(widgetContext.servicesMap.get('dialogs'));\\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\\n\\nopenDeleteAssetDialog();\\n\\nfunction openDeleteAssetDialog() {\\n let title = \\\"Are you sure you want to delete the asset \\\" + entityName + \\\"?\\\";\\n let content = \\\"Be careful, after the confirmation, the asset and all related data will become unrecoverable!\\\";\\n dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe(\\n function (result) {\\n if (result) {\\n deleteAsset();\\n }\\n }\\n );\\n}\\n\\nfunction deleteAsset() {\\n assetService.deleteAsset(entityId.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n }\\n );\\n}\\n\",\"id\":\"ec2708f6-9ff0-186b-e4fc-7635ebfa3074\"}]}}" } } ] -} +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/gateway_widgets.json b/application/src/main/data/json/system/widget_bundles/gateway_widgets.json index 7d5dd46086..22cbc82beb 100644 --- a/application/src/main/data/json/system/widget_bundles/gateway_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/gateway_widgets.json @@ -5,6 +5,22 @@ "image": null }, "widgetTypes": [ + { + "alias": "gateway_configuration", + "name": "Gateway Configuration", + "descriptor": { + "type": "static", + "sizeX": 8, + "sizeY": 6.5, + "resources": [], + "templateHtml": "\n\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"Gateway Configuration\"\n },\n \"archiveFileName\": {\n \"title\": \"Default archive file name\",\n \"type\": \"string\",\n \"default\": \"gatewayConfiguration\"\n },\n \"gatewayType\": {\n \"title\": \"Device type for new gateway\",\n \"type\": \"string\",\n \"default\": \"Gateway\"\n },\n \"successfulSave\": {\n \"title\": \"Text message about successfully saved gateway configuration\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"gatewayNameExists\": {\n \"title\": \"Text message when device with entered name is already exists\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n [\n \"widgetTitle\",\n \"archiveFileName\",\n \"gatewayType\"\n ],\n [\n \"successfulSave\",\n \"gatewayNameExists\"\n ]\n ],\n \"groupInfoes\": [{\n \"formIndex\": 0,\n \"GroupTitle\": \"General settings\"\n }, {\n \"formIndex\": 1,\n \"GroupTitle\": \"Messages settings\"\n }]\n}", + "dataKeySettingsSchema": "{}\n", + "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"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\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"widgetTitle\":\"Gateway Configuration\",\"archiveFileName\":\"configurationGateway\"},\"title\":\"Gateway Configuration\",\"dropShadow\":true,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + } + }, { "alias": "attributes_card", "name": "Gateway events", @@ -15,7 +31,7 @@ "resources": [], "templateHtml": "", "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}", - "controllerScript": "let types;\nlet eventsReg = \"eventsReg\";\n\nself.onInit = function() {\n \n self.ctx.datasourceTitleCells = [];\n self.ctx.valueCells = [];\n self.ctx.labelCells = [];\n types = self.ctx.$scope.$injector.get('types');\n \n if (self.ctx.datasources.length && self.ctx.datasources[0].type === types.datasourceType.entity) {\n getDatasourceKeys(self.ctx.datasources[0]);\n } else {\n processDatasources(self.ctx.datasources);\n }\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.valueCells.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData && cellData.data && cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length -\n 1];\n var value = tvPair[1];\n var textValue;\n //toDo -> + IsNumber\n \n if (isNumber(value)) {\n var decimals = self.ctx.decimals;\n var units = self.ctx.units;\n if (cellData.dataKey.decimals || cellData.dataKey.decimals === 0) {\n decimals = cellData.dataKey.decimals;\n }\n if (cellData.dataKey.units) {\n units = cellData.dataKey.units;\n }\n txtValue = self.ctx.utils.formatValue(value, decimals, units, false);\n }\n else {\n txtValue = value;\n }\n self.ctx.valueCells[i].html(txtValue);\n }\n }\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n}\n\nself.onResize = function() {\n var datasourceTitleFontSize = self.ctx.height/8;\n if (self.ctx.width/self.ctx.height <= 1.5) {\n datasourceTitleFontSize = self.ctx.width/12;\n }\n datasourceTitleFontSize = Math.min(datasourceTitleFontSize, 20);\n for (var i = 0; i < self.ctx.datasourceTitleCells.length; i++) {\n self.ctx.datasourceTitleCells[i].css('font-size', datasourceTitleFontSize+'px');\n }\n var valueFontSize = self.ctx.height/9;\n var labelFontSize = self.ctx.height/9;\n if (self.ctx.width/self.ctx.height <= 1.5) {\n valueFontSize = self.ctx.width/15;\n labelFontSize = self.ctx.width/15;\n }\n valueFontSize = Math.min(valueFontSize, 18);\n labelFontSize = Math.min(labelFontSize, 18);\n\n for (i = 0; i < self.ctx.valueCells; i++) {\n self.ctx.valueCells[i].css('font-size', valueFontSize+'px');\n self.ctx.valueCells[i].css('height', valueFontSize*2.5+'px');\n self.ctx.valueCells[i].css('padding', '0px ' + valueFontSize + 'px');\n self.ctx.labelCells[i].css('font-size', labelFontSize+'px');\n self.ctx.labelCells[i].css('height', labelFontSize*2.5+'px');\n self.ctx.labelCells[i].css('padding', '0px ' + labelFontSize + 'px');\n } \n}\n\nfunction processDatasources(datasources) {\n var i = 0;\n var tbDatasource = datasources[i];\n var datasourceId = 'tbDatasource' + i;\n self.ctx.$container.append(\n \"
\"\n );\n\n var datasourceContainer = $('#' + datasourceId,\n self.ctx.$container);\n\n datasourceContainer.append(\n \"
\" +\n tbDatasource.name + \"
\"\n );\n \n var datasourceTitleCell = $('.tbDatasource-title', datasourceContainer);\n self.ctx.datasourceTitleCells.push(datasourceTitleCell);\n \n var tableId = 'table' + i;\n datasourceContainer.append(\n \"
\"\n );\n var table = $('#' + tableId, self.ctx.$container);\n\n for (var a = 0; a < tbDatasource.dataKeys.length; a++) {\n var dataKey = tbDatasource.dataKeys[a];\n var labelCellId = 'labelCell' + a;\n var cellId = 'cell' + a;\n table.append(\"\" + dataKey.label +\n \"\");\n var labelCell = $('#' + labelCellId, table);\n self.ctx.labelCells.push(labelCell);\n var valueCell = $('#' + cellId, table);\n self.ctx.valueCells.push(valueCell);\n }\n self.onResize();\n}\n\nfunction getDatasourceKeys (datasource) {\n let attributeService = self.ctx.$scope.$injector.get('attributeService');\n if (datasource.entityId && datasource.entityType) {\n attributeService.getEntityKeys(datasource.entityType, datasource.entityId, \"\" , types.dataKeyType.timeseries).then(\n function(data){\n if (data.length) {\n subscribeForKeys (datasource, data);\n }\n });\n }\n}\n\nfunction subscribeForKeys (datasource, data) {\n let eventsRegVals = self.ctx.settings[eventsReg];\n if (eventsRegVals && eventsRegVals.length > 0) {\n var dataKeys = [];\n data.sort();\n data.forEach(dataValue => {eventsRegVals.forEach(event => {\n if (dataValue.toLowerCase().includes(event.toLowerCase())) {\n var dataKey = {\n type: types.dataKeyType.timeseries,\n name: dataValue,\n label: dataValue,\n settings: {},\n _hash: Math.random()\n };\n dataKeys.push(dataKey);\n }\n })});\n\n if (dataKeys.length) {\n updateSubscription (datasource, dataKeys);\n }\n }\n}\n\nfunction updateSubscription (datasource, dataKeys) {\n var datasources = [\n {\n type: types.datasourceType.entity,\n name: datasource.aliasName,\n aliasName: datasource.aliasName,\n entityAliasId: datasource.entityAliasId,\n dataKeys: dataKeys\n }\n ];\n \n var subscriptionOptions = {\n datasources: datasources,\n useDashboardTimewindow: false,\n type: types.widgetType.latest.value,\n callbacks: {\n onDataUpdated: (subscription) => {\n self.ctx.data = subscription.data;\n self.onDataUpdated();\n }\n }\n };\n \n processDatasources(datasources);\n self.ctx.subscriptionApi.createSubscription(subscriptionOptions, true).then(\n (subscription) => {\n self.ctx.defaultSubscription = subscription;\n }\n );\n}\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\t\n dataKeysOptional: true\n };\n}\n\n", + "controllerScript": "let types;\nlet eventsReg = \"eventsReg\";\n\nself.onInit = function() {\n \n self.ctx.datasourceTitleCells = [];\n self.ctx.valueCells = [];\n self.ctx.labelCells = [];\n\n if (self.ctx.datasources.length && self.ctx.datasources[0].type === 'entity') {\n getDatasourceKeys(self.ctx.datasources[0]);\n } else {\n processDatasources(self.ctx.datasources);\n }\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.valueCells.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData && cellData.data && cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length -\n 1];\n var value = tvPair[1];\n var textValue;\n //toDo -> + IsNumber\n \n if (isNumber(value)) {\n var decimals = self.ctx.decimals;\n var units = self.ctx.units;\n if (cellData.dataKey.decimals || cellData.dataKey.decimals === 0) {\n decimals = cellData.dataKey.decimals;\n }\n if (cellData.dataKey.units) {\n units = cellData.dataKey.units;\n }\n txtValue = self.ctx.utils.formatValue(value, decimals, units, false);\n }\n else {\n txtValue = value;\n }\n self.ctx.valueCells[i].html(txtValue);\n }\n }\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n}\n\nself.onResize = function() {\n var datasourceTitleFontSize = self.ctx.height/8;\n if (self.ctx.width/self.ctx.height <= 1.5) {\n datasourceTitleFontSize = self.ctx.width/12;\n }\n datasourceTitleFontSize = Math.min(datasourceTitleFontSize, 20);\n for (var i = 0; i < self.ctx.datasourceTitleCells.length; i++) {\n self.ctx.datasourceTitleCells[i].css('font-size', datasourceTitleFontSize+'px');\n }\n var valueFontSize = self.ctx.height/9;\n var labelFontSize = self.ctx.height/9;\n if (self.ctx.width/self.ctx.height <= 1.5) {\n valueFontSize = self.ctx.width/15;\n labelFontSize = self.ctx.width/15;\n }\n valueFontSize = Math.min(valueFontSize, 18);\n labelFontSize = Math.min(labelFontSize, 18);\n\n for (i = 0; i < self.ctx.valueCells; i++) {\n self.ctx.valueCells[i].css('font-size', valueFontSize+'px');\n self.ctx.valueCells[i].css('height', valueFontSize*2.5+'px');\n self.ctx.valueCells[i].css('padding', '0px ' + valueFontSize + 'px');\n self.ctx.labelCells[i].css('font-size', labelFontSize+'px');\n self.ctx.labelCells[i].css('height', labelFontSize*2.5+'px');\n self.ctx.labelCells[i].css('padding', '0px ' + labelFontSize + 'px');\n } \n}\n\nfunction processDatasources(datasources) {\n var i = 0;\n var tbDatasource = datasources[i];\n var datasourceId = 'tbDatasource' + i;\n self.ctx.$container.append(\n \"
\"\n );\n\n var datasourceContainer = $('#' + datasourceId,\n self.ctx.$container);\n\n datasourceContainer.append(\n \"
\" +\n tbDatasource.name + \"
\"\n );\n \n var datasourceTitleCell = $('.tbDatasource-title', datasourceContainer);\n self.ctx.datasourceTitleCells.push(datasourceTitleCell);\n \n var tableId = 'table' + i;\n datasourceContainer.append(\n \"
\"\n );\n var table = $('#' + tableId, self.ctx.$container);\n\n for (var a = 0; a < tbDatasource.dataKeys.length; a++) {\n var dataKey = tbDatasource.dataKeys[a];\n var labelCellId = 'labelCell' + a;\n var cellId = 'cell' + a;\n table.append(\"\" + dataKey.label +\n \"\");\n var labelCell = $('#' + labelCellId, table);\n self.ctx.labelCells.push(labelCell);\n var valueCell = $('#' + cellId, table);\n self.ctx.valueCells.push(valueCell);\n }\n self.onResize();\n}\n\nfunction getDatasourceKeys (datasource) {\n let entityService = self.ctx.$scope.$injector.get(self.ctx.servicesMap.get('entityService'));\n if (datasource.entityId && datasource.entityType) {\n entityService.getEntityKeys({entityType: datasource.entityType, id: datasource.entityId}, '', 'timeseries').subscribe(\n function(data){\n if (data.length) {\n subscribeForKeys (datasource, data);\n }\n });\n }\n}\n\nfunction subscribeForKeys (datasource, data) {\n let eventsRegVals = self.ctx.settings[eventsReg];\n if (eventsRegVals && eventsRegVals.length > 0) {\n var dataKeys = [];\n data.sort();\n data.forEach(dataValue => {eventsRegVals.forEach(event => {\n if (dataValue.toLowerCase().includes(event.toLowerCase())) {\n var dataKey = {\n type: 'timeseries',\n name: dataValue,\n label: dataValue,\n settings: {},\n _hash: Math.random()\n };\n dataKeys.push(dataKey);\n }\n })});\n\n if (dataKeys.length) {\n updateSubscription (datasource, dataKeys);\n }\n }\n}\n\nfunction updateSubscription (datasource, dataKeys) {\n var datasources = [\n {\n type: 'entity',\n name: datasource.aliasName,\n aliasName: datasource.aliasName,\n entityAliasId: datasource.entityAliasId,\n dataKeys: dataKeys\n }\n ];\n \n var subscriptionOptions = {\n datasources: datasources,\n useDashboardTimewindow: false,\n type: 'latest',\n callbacks: {\n onDataUpdated: (subscription) => {\n self.ctx.data = subscription.data;\n self.onDataUpdated();\n }\n }\n };\n \n processDatasources(datasources);\n self.ctx.subscriptionApi.createSubscription(subscriptionOptions, true).subscribe(\n (subscription) => {\n self.ctx.defaultSubscription = subscription;\n }\n );\n}\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\t\n dataKeysOptional: true,\n singleEntity: true\n };\n}\n\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"GatewayEventsForm\",\n \"properties\": {\n \"eventsTitle\": {\n \"title\": \"Gateway events form title\",\n \"type\": \"string\",\n \"default\": \"Gateway Events Form\"\n },\n \"eventsReg\": {\n \"title\": \"Events filten.\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Event key contains\",\n \"type\": \"string\"\n }\n }\n }\n },\n \"form\": [\n \"eventsTitle\",\n \"eventsReg\"\n ]\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Function Math.round\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.826503672916844,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"eventsTitle\":\"Gateway Events Form\",\"eventsReg\":[]},\"title\":\"Gateway events\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" @@ -29,45 +45,13 @@ "sizeX": 7.5, "sizeY": 9, "resources": [], - "templateHtml": "\n", + "templateHtml": "\n", "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}", - "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.formId = \"form-\"+id;\n scope.ctx = self.ctx;\n scope.isStateForm = true;\n}\n\nself.onResize = function() {\n self.ctx.$scope.$broadcast('gateway-form-resize', self.ctx.$scope.formId);\n}\n\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\t\t\t\n dataKeysOptional: true\t\t\n };\n}\n\n", + "controllerScript": "self.onInit = function() {\n}\n\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\t\t\t\n dataKeysOptional: true,\n singleEntity: true\n };\n}\n\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"GatewayConfigForm\",\n \"properties\": {\n \"gatewayTitle\": {\n \"title\": \"Gateway form\",\n \"type\": \"string\",\n \"default\": \"Gateway configuration (Single device)\"\n },\n \"readOnly\": {\n \"title\": \"Read Only\",\n \"type\": \"boolean\",\n \"default\": false\n }\n }\n },\n \"form\": [\n \"gatewayTitle\",\n \"readOnly\"\n ]\n}\n", "dataKeySettingsSchema": "{}\n", - "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\":\"Gateway configuration (Single device)\"}" - } - }, - { - "alias": "extension_configuration_widget", - "name": "Extensions table", - "descriptor": { - "type": "latest", - "sizeX": 9, - "sizeY": 6.5, - "resources": [], - "templateHtml": "\n", - "templateCss": "#container {\n overflow: auto;\n}", - "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n}\n\nself.onResize = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"ExtensionTableSettings\",\n \"properties\": {\n \"extensionsTitle\": {\n \"title\": \"Extension table title\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"extensionsTitle\"\n ]\n}", - "dataKeySettingsSchema": "{}\n", - "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\":\"4px\",\"settings\":{},\"title\":\"Extensions table\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"18px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" - } - }, - { - "alias": "gateway_configuration", - "name": "Gateway Configuration", - "descriptor": { - "type": "static", - "sizeX": 8, - "sizeY": 6.5, - "resources": [], - "templateHtml": "\n\n", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.formId = \"form-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onResize = function() {\n self.ctx.$scope.$broadcast('gateway-form-resize', self.ctx.$scope.formId);\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"Gateway Configuration\"\n },\n \"archiveFileName\": {\n \"title\": \"Default archive file name\",\n \"type\": \"string\",\n \"default\": \"gatewayConfiguration\"\n },\n \"gatewayType\": {\n \"title\": \"Device type for new gateway\",\n \"type\": \"string\",\n \"default\": \"Gateway\"\n },\n \"successfulSave\": {\n \"title\": \"Text message about successfully saved gateway configuration\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"gatewayNameExists\": {\n \"title\": \"Text message when device with entered name is already exists\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n [\n \"widgetTitle\",\n \"archiveFileName\",\n \"gatewayType\"\n ],\n [\n \"successfulSave\",\n \"gatewayNameExists\"\n ]\n ],\n \"groupInfoes\": [{\n \"formIndex\": 0,\n \"GroupTitle\": \"General settings\"\n }, {\n \"formIndex\": 1,\n \"GroupTitle\": \"Messages settings\"\n }]\n}", - "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"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\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"widgetTitle\":\"Gateway Configuration\",\"archiveFileName\":\"configurationGateway\"},\"title\":\"Gateway Configuration\",\"dropShadow\":true,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "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\":{\"gatewayTitle\":\"Gateway configuration (Single device)\"},\"title\":\"Gateway configuration (Single device)\"}" } } ] -} +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/gpio_widgets.json b/application/src/main/data/json/system/widget_bundles/gpio_widgets.json index fd8d0538b6..00a3c7b3f3 100644 --- a/application/src/main/data/json/system/widget_bundles/gpio_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/gpio_widgets.json @@ -13,9 +13,9 @@ "sizeX": 4, "sizeY": 2, "resources": [], - "templateHtml": "
\n
\n
\n
\n {{ cell.label }}\n
\n {{cell.pin}}\n \n \n \n \n {{cell.pin}}\n
\n {{ cell.label }}\n
\n
\n \n \n \n
\n
\n
\n {{rpcErrorText}}\n \n
", - "templateCss": ".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.switch-panel {\n margin: 0;\n height: 32px;\n width: 66px;\n min-width: 66px;\n}\n\n.switch-panel md-switch {\n margin: 0;\n width: 36px;\n min-width: 36px;\n}\n\n.switch-panel md-switch > div.md-container {\n margin: 0;\n}\n\n.switch-panel.col-0 md-switch {\n padding-left: 8px;\n padding-right: 4px;\n}\n\n.switch-panel.col-1 md-switch {\n padding-left: 4px;\n padding-right: 8px;\n}\n\n.gpio-row {\n height: 32px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.switch-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.switch-panel.col-1 .pin {\n margin-right: auto;\n \n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}", - "controllerScript": "self.onInit = function() {\n \n var i, gpio;\n var scope = self.ctx.$scope;\n var settings = self.ctx.settings;\n scope.gpioList = [];\n for (var g = 0; g < settings.gpioList.length; g++) {\n gpio = settings.gpioList[g];\n scope.gpioList.push(\n {\n row: gpio.row,\n col: gpio.col,\n pin: gpio.pin,\n label: gpio.label,\n enabled: false\n }\n );\n }\n\n scope.requestTimeout = settings.requestTimeout || 1000;\n\n scope.switchPanelBackgroundColor = settings.switchPanelBackgroundColor || tinycolor('green').lighten(2).toRgbString();\n\n scope.gpioStatusRequest = {\n method: \"getGpioStatus\",\n paramsBody: \"{}\"\n };\n \n if (settings.gpioStatusRequest) {\n scope.gpioStatusRequest.method = settings.gpioStatusRequest.method || scope.gpioStatusRequest.method;\n scope.gpioStatusRequest.paramsBody = settings.gpioStatusRequest.paramsBody || scope.gpioStatusRequest.paramsBody;\n }\n \n scope.gpioStatusChangeRequest = {\n method: \"setGpioStatus\",\n paramsBody: \"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"\n };\n \n if (settings.gpioStatusChangeRequest) {\n scope.gpioStatusChangeRequest.method = settings.gpioStatusChangeRequest.method || scope.gpioStatusChangeRequest.method;\n scope.gpioStatusChangeRequest.paramsBody = settings.gpioStatusChangeRequest.paramsBody || scope.gpioStatusChangeRequest.paramsBody;\n }\n \n scope.parseGpioStatusFunction = \"return body[pin] === true;\";\n \n if (settings.parseGpioStatusFunction && settings.parseGpioStatusFunction.length > 0) {\n scope.parseGpioStatusFunction = settings.parseGpioStatusFunction;\n }\n \n scope.parseGpioStatusFunction = new Function(\"body, pin\", scope.parseGpioStatusFunction);\n \n function requestGpioStatus() {\n self.ctx.controlApi.sendTwoWayCommand(scope.gpioStatusRequest.method, \n scope.gpioStatusRequest.paramsBody, \n scope.requestTimeout)\n .then(\n function success(responseBody) {\n for (var g = 0; g < scope.gpioList.length; g++) {\n var gpio = scope.gpioList[g];\n var enabled = scope.parseGpioStatusFunction.apply(this, [responseBody, gpio.pin]);\n gpio.enabled = enabled; \n }\n }\n );\n }\n \n function changeGpioStatus(gpio) {\n var pin = gpio.pin + '';\n var enabled = !gpio.enabled;\n enabled = enabled === true ? 'true' : 'false';\n var paramsBody = scope.gpioStatusChangeRequest.paramsBody;\n var requestBody = JSON.parse(paramsBody.replace(\"\\\"{$pin}\\\"\", pin).replace(\"\\\"{$enabled}\\\"\", enabled));\n self.ctx.controlApi.sendTwoWayCommand(scope.gpioStatusChangeRequest.method, \n requestBody, scope.requestTimeout)\n .then(\n function success(responseBody) {\n var enabled = scope.parseGpioStatusFunction.apply(this, [responseBody, gpio.pin]);\n gpio.enabled = enabled;\n }\n );\n }\n \n scope.gpioCells = {};\n var rowCount = 0;\n for (i = 0; i < scope.gpioList.length; i++) {\n gpio = scope.gpioList[i];\n scope.gpioCells[gpio.row+'_'+gpio.col] = gpio;\n rowCount = Math.max(rowCount, gpio.row+1);\n }\n \n scope.prefferedRowHeight = 32;\n scope.rows = [];\n for (i = 0; i < rowCount; i++) {\n var row = [];\n for (var c =0; c<2;c++) {\n if (scope.gpioCells[i+'_'+c]) {\n row[c] = scope.gpioCells[i+'_'+c];\n } else {\n row[c] = null;\n }\n }\n scope.rows.push(row);\n }\n\n scope.gpioClick = function($event, gpio) {\n changeGpioStatus(gpio);\n };\n\n requestGpioStatus(); \n \n self.onResize();\n}\n\nself.onResize = function() {\n var scope = self.ctx.$scope;\n var rowCount = scope.rows.length;\n var prefferedRowHeight = (self.ctx.height - 35)/rowCount;\n prefferedRowHeight = Math.min(32, prefferedRowHeight);\n prefferedRowHeight = Math.max(12, prefferedRowHeight);\n scope.prefferedRowHeight = prefferedRowHeight;\n var ratio = prefferedRowHeight/32;\n var switches = $('md-switch', self.ctx.$container);\n switches.css('height', 30*ratio+'px');\n switches.css('width', 36*ratio+'px');\n switches.css('min-width', 36*ratio+'px');\n $('.md-container', switches).css('height', 24*ratio+'px');\n $('.md-container', switches).css('width', 36*ratio+'px');\n var bars = $('.md-bar', self.ctx.$container);\n bars.css('height', 14*ratio+'px');\n bars.css('width', 34*ratio+'px');\n var thumbs = $('.md-thumb', self.ctx.$container);\n thumbs.css('height', 20*ratio+'px');\n thumbs.css('width', 20*ratio+'px');\n \n var leftLabels = $('.gpio-left-label', self.ctx.$container);\n leftLabels.css('font-size', 16*ratio+'px');\n var rightLabels = $('.gpio-right-label', self.ctx.$container);\n rightLabels.css('font-size', 16*ratio+'px');\n var pins = $('.pin', self.ctx.$container);\n var pinsFontSize = Math.max(9, 12*ratio);\n pins.css('font-size', pinsFontSize+'px'); \n}\n\nself.onDestroy = function() {\n}\n", + "templateHtml": "
\n
\n
\n
\n {{ cell.label }}\n
\n {{cell.pin}}\n \n \n \n \n {{cell.pin}}\n
\n {{ cell.label }}\n
\n
\n \n \n \n
\n
\n
\n {{rpcErrorText}}\n \n
", + "templateCss": ".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.gpio-panel section[fxflex] {\n min-width: 0px;\n}\n\n\n.switch-panel {\n margin: 0;\n height: 32px;\n width: 66px;\n min-width: 66px;\n}\n\n.switch-panel mat-slide-toggle {\n margin: 0;\n width: 36px;\n min-width: 36px;\n}\n\n.switch-panel.col-0 mat-slide-toggle {\n margin-left: 8px;\n margin-right: 4px;\n}\n\n.switch-panel.col-1 mat-slide-toggle {\n margin-left: 4px;\n margin-right: 8px;\n}\n\n.gpio-row {\n height: 32px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.switch-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.switch-panel.col-1 .pin {\n margin-right: auto;\n \n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}", + "controllerScript": "var namespace;\nvar cssParser = new cssjs();\n\nself.onInit = function() {\n var utils = self.ctx.$injector.get(self.ctx.servicesMap.get('utils'));\n namespace = 'gpio-control-' + utils.guid();\n cssParser.testMode = false;\n cssParser.cssPreviewNamespace = namespace;\n self.ctx.$container.addClass(namespace);\n self.ctx.ngZone.run(function() {\n init(); \n });\n}\n\nfunction init() {\n \n var i, gpio;\n var scope = self.ctx.$scope;\n var settings = self.ctx.settings;\n scope.gpioList = [];\n for (var g = 0; g < settings.gpioList.length; g++) {\n gpio = settings.gpioList[g];\n scope.gpioList.push(\n {\n row: gpio.row,\n col: gpio.col,\n pin: gpio.pin,\n label: gpio.label,\n enabled: false\n }\n );\n }\n\n scope.requestTimeout = settings.requestTimeout || 1000;\n\n scope.switchPanelBackgroundColor = settings.switchPanelBackgroundColor || tinycolor('green').lighten(2).toRgbString();\n\n scope.gpioStatusRequest = {\n method: \"getGpioStatus\",\n paramsBody: \"{}\"\n };\n \n if (settings.gpioStatusRequest) {\n scope.gpioStatusRequest.method = settings.gpioStatusRequest.method || scope.gpioStatusRequest.method;\n scope.gpioStatusRequest.paramsBody = settings.gpioStatusRequest.paramsBody || scope.gpioStatusRequest.paramsBody;\n }\n \n scope.gpioStatusChangeRequest = {\n method: \"setGpioStatus\",\n paramsBody: \"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"\n };\n \n if (settings.gpioStatusChangeRequest) {\n scope.gpioStatusChangeRequest.method = settings.gpioStatusChangeRequest.method || scope.gpioStatusChangeRequest.method;\n scope.gpioStatusChangeRequest.paramsBody = settings.gpioStatusChangeRequest.paramsBody || scope.gpioStatusChangeRequest.paramsBody;\n }\n \n scope.parseGpioStatusFunction = \"return body[pin] === true;\";\n \n if (settings.parseGpioStatusFunction && settings.parseGpioStatusFunction.length > 0) {\n scope.parseGpioStatusFunction = settings.parseGpioStatusFunction;\n }\n \n scope.parseGpioStatusFunction = new Function(\"body, pin\", scope.parseGpioStatusFunction);\n \n function requestGpioStatus() {\n self.ctx.controlApi.sendTwoWayCommand(scope.gpioStatusRequest.method, \n scope.gpioStatusRequest.paramsBody, \n scope.requestTimeout)\n .subscribe(\n function success(responseBody) {\n for (var g = 0; g < scope.gpioList.length; g++) {\n var gpio = scope.gpioList[g];\n var enabled = scope.parseGpioStatusFunction.apply(this, [responseBody, gpio.pin]);\n gpio.enabled = enabled; \n self.ctx.detectChanges();\n }\n }\n );\n }\n \n function changeGpioStatus(gpio) {\n var pin = gpio.pin + '';\n var enabled = !gpio.enabled;\n enabled = enabled === true ? 'true' : 'false';\n var paramsBody = scope.gpioStatusChangeRequest.paramsBody;\n var requestBody = JSON.parse(paramsBody.replace(\"\\\"{$pin}\\\"\", pin).replace(\"\\\"{$enabled}\\\"\", enabled));\n self.ctx.controlApi.sendTwoWayCommand(scope.gpioStatusChangeRequest.method, \n requestBody, scope.requestTimeout)\n .subscribe(\n function success(responseBody) {\n var enabled = scope.parseGpioStatusFunction.apply(this, [responseBody, gpio.pin]);\n gpio.enabled = enabled;\n self.ctx.detectChanges();\n }\n );\n }\n \n scope.gpioCells = {};\n var rowCount = 0;\n for (i = 0; i < scope.gpioList.length; i++) {\n gpio = scope.gpioList[i];\n scope.gpioCells[gpio.row+'_'+gpio.col] = gpio;\n rowCount = Math.max(rowCount, gpio.row+1);\n }\n \n scope.prefferedRowHeight = 32;\n scope.rows = [];\n for (i = 0; i < rowCount; i++) {\n var row = [];\n for (var c =0; c<2;c++) {\n if (scope.gpioCells[i+'_'+c]) {\n row[c] = scope.gpioCells[i+'_'+c];\n } else {\n row[c] = null;\n }\n }\n scope.rows.push(row);\n }\n\n scope.gpioClick = function($event, gpio) {\n if (scope.rpcEnabled && !scope.executingRpcRequest) {\n changeGpioStatus(gpio);\n }\n };\n \n scope.gpioToggleChange = function($event, gpio) {\n gpio.enabled = !$event.checked;\n $event.source.toggle();\n self.ctx.detectChanges();\n }\n \n if (scope.rpcEnabled) {\n requestGpioStatus(); \n }\n \n self.onResize();\n}\n\nself.onResize = function() {\n var scope = self.ctx.$scope;\n var rowCount = scope.rows.length;\n var prefferedRowHeight = (self.ctx.height - 35)/rowCount;\n prefferedRowHeight = Math.min(32, prefferedRowHeight);\n prefferedRowHeight = Math.max(12, prefferedRowHeight);\n scope.prefferedRowHeight = prefferedRowHeight;\n var ratio = prefferedRowHeight/32;\n \n var css = '.mat-slide-toggle .mat-slide-toggle-bar {\\n' +\n ' height: ' + 14*ratio+'px;\\n'+\n ' width: ' + 36*ratio+'px;\\n'+\n '}\\n';\n css += '.mat-slide-toggle .mat-slide-toggle-thumb-container {\\n' +\n ' height: ' + 20*ratio+'px;\\n'+\n ' width: ' + 20*ratio+'px;\\n'+\n '}\\n';\n css += '.mat-slide-toggle .mat-slide-toggle-thumb {\\n' +\n ' height: ' + 20*ratio+'px;\\n'+\n ' width: ' + 20*ratio+'px;\\n'+\n '}\\n';\n css += '.mat-slide-toggle .mat-slide-toggle-ripple {\\n' +\n ' height: ' + 40*ratio+'px;\\n'+\n ' width: ' + 40*ratio+'px;\\n'+\n ' top: calc(50% - '+20*ratio+'px);\\n'+\n ' left: calc(50% - '+20*ratio+'px);\\n'+\n '}\\n';\n css += '.gpio-left-label, .gpio-right-label {\\n' +\n ' font-size: ' + 16*ratio+'px;\\n'+\n '}\\n';\n var pinsFontSize = Math.max(9, 12*ratio);\n css += '.pin {\\n' +\n ' font-size: ' + pinsFontSize+'px;\\n'+\n '}\\n';\n\n cssParser.createStyleElement(namespace, css);\n \n self.ctx.detectChanges();\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"gpioList\": {\n \"title\": \"Gpio switches\",\n \"type\": \"array\",\n \"minItems\" : 1,\n \"items\": {\n \"title\": \"Gpio switch\",\n \"type\": \"object\",\n \"properties\": {\n \"pin\": {\n \"title\": \"Pin\",\n \"type\": \"number\"\n },\n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"row\": {\n \"title\": \"Row\",\n \"type\": \"number\"\n },\n \"col\": {\n \"title\": \"Column\",\n \"type\": \"number\"\n }\n },\n \"required\": [\"pin\", \"label\", \"row\", \"col\"]\n }\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"switchPanelBackgroundColor\": {\n \"title\": \"Switches panel background color\",\n \"type\": \"string\",\n \"default\": \"#008a00\"\n },\n \"gpioStatusRequest\": {\n \"title\": \"GPIO status request\",\n \"type\": \"object\",\n \"properties\": {\n \"method\": {\n \"title\": \"Method name\",\n \"type\": \"string\",\n \"default\": \"getGpioStatus\"\n },\n \"paramsBody\": {\n \"title\": \"Method body\",\n \"type\": \"string\",\n \"default\": \"{}\"\n }\n },\n \"required\": [\"method\", \"paramsBody\"]\n },\n \"gpioStatusChangeRequest\": {\n \"title\": \"GPIO status change request\",\n \"type\": \"object\",\n \"properties\": {\n \"method\": {\n \"title\": \"Method name\",\n \"type\": \"string\",\n \"default\": \"setGpioStatus\"\n },\n \"paramsBody\": {\n \"title\": \"Method body\",\n \"type\": \"string\",\n \"default\": \"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"\n }\n },\n \"required\": [\"method\", \"paramsBody\"]\n },\n \"parseGpioStatusFunction\": {\n \"title\": \"Parse gpio status function\",\n \"type\": \"string\",\n \"default\": \"return body[pin] === true;\"\n } \n },\n \"required\": [\"gpioList\", \n \"requestTimeout\",\n \"switchPanelBackgroundColor\",\n \"gpioStatusRequest\",\n \"gpioStatusChangeRequest\",\n \"parseGpioStatusFunction\"]\n },\n \"form\": [\n \"gpioList\",\n \"requestTimeout\",\n {\n \"key\": \"switchPanelBackgroundColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"gpioStatusRequest\",\n \"items\": [\n \"gpioStatusRequest.method\",\n {\n \"key\": \"gpioStatusRequest.paramsBody\",\n \"type\": \"json\"\n }\n ]\n },\n {\n \"key\": \"gpioStatusChangeRequest\",\n \"items\": [\n \"gpioStatusChangeRequest.method\",\n {\n \"key\": \"gpioStatusChangeRequest.paramsBody\",\n \"type\": \"json\"\n }\n ]\n },\n {\n \"key\": \"parseGpioStatusFunction\",\n \"type\": \"javascript\"\n }\n ]\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"parseGpioStatusFunction\":\"return body[pin] === true;\",\"gpioStatusChangeRequest\":{\"method\":\"setGpioStatus\",\"paramsBody\":\"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"},\"requestTimeout\":500,\"switchPanelBackgroundColor\":\"#b71c1c\",\"gpioStatusRequest\":{\"method\":\"getGpioStatus\",\"paramsBody\":\"{}\"},\"gpioList\":[{\"pin\":1,\"label\":\"GPIO 1\",\"row\":0,\"col\":0,\"_uniqueKey\":0},{\"pin\":2,\"label\":\"GPIO 2\",\"row\":0,\"col\":1,\"_uniqueKey\":1},{\"pin\":3,\"label\":\"GPIO 3\",\"row\":1,\"col\":0,\"_uniqueKey\":2}]},\"title\":\"Basic GPIO Control\"}" @@ -29,44 +29,44 @@ "sizeX": 5, "sizeY": 2, "resources": [], - "templateHtml": "
\n
\n
\n
\n {{ cell.label }}\n
\n {{cell.pin}}\n \n \n \n \n {{cell.pin}}\n
\n {{ cell.label }}\n
\n
\n \n \n \n
\n
\n
\n
", - "templateCss": ".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.gpio-panel tb-led-light > div {\n margin: auto;\n}\n\n.led-panel {\n margin: 0;\n width: 66px;\n min-width: 66px;\n}\n\n.led-container {\n width: 48px;\n min-width: 48px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.led-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.led-panel.col-1 .pin {\n margin-right: auto;\n \n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}", - "controllerScript": "self.onInit = function() {\n var i, gpio;\n \n var scope = self.ctx.$scope;\n var settings = self.ctx.settings;\n \n scope.gpioList = [];\n scope.gpioByPin = {};\n for (var g = 0; g < settings.gpioList.length; g++) {\n gpio = settings.gpioList[g];\n scope.gpioList.push(\n {\n row: gpio.row,\n col: gpio.col,\n pin: gpio.pin,\n label: gpio.label,\n enabled: false,\n colorOn: tinycolor(gpio.color).lighten(20).toHexString(),\n colorOff: tinycolor(gpio.color).darken().toHexString()\n }\n );\n scope.gpioByPin[gpio.pin] = scope.gpioList[scope.gpioList.length-1];\n }\n\n scope.ledPanelBackgroundColor = settings.ledPanelBackgroundColor || tinycolor('green').lighten(2).toRgbString();\n\n scope.gpioCells = {};\n var rowCount = 0;\n for (i = 0; i < scope.gpioList.length; i++) {\n gpio = scope.gpioList[i];\n scope.gpioCells[gpio.row+'_'+gpio.col] = gpio;\n rowCount = Math.max(rowCount, gpio.row+1);\n }\n \n scope.prefferedRowHeight = 32;\n scope.rows = [];\n for (i = 0; i < rowCount; i++) {\n var row = [];\n for (var c =0; c<2;c++) {\n if (scope.gpioCells[i+'_'+c]) {\n row[c] = scope.gpioCells[i+'_'+c];\n } else {\n row[c] = null;\n }\n }\n scope.rows.push(row);\n } \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n var changed = false;\n for (var d = 0; d < self.ctx.data.length; d++) {\n var cellData = self.ctx.data[d];\n var dataKey = cellData.dataKey;\n var gpio = self.ctx.$scope.gpioByPin[dataKey.label];\n if (gpio) {\n var enabled = false;\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n enabled = (tvPair[1] === true || tvPair[1] === 'true');\n }\n if (gpio.enabled != enabled) {\n changed = true;\n gpio.enabled = enabled;\n }\n }\n }\n if (changed) {\n self.ctx.$scope.$digest();\n } \n}\n\nself.onResize = function() {\n var rowCount = self.ctx.$scope.rows.length;\n var prefferedRowHeight = (self.ctx.height - 35)/rowCount;\n prefferedRowHeight = Math.min(32, prefferedRowHeight);\n prefferedRowHeight = Math.max(12, prefferedRowHeight);\n self.ctx.$scope.prefferedRowHeight = prefferedRowHeight;\n \n var ratio = prefferedRowHeight/32;\n \n var leftLabels = $('.gpio-left-label', self.ctx.$container);\n leftLabels.css('font-size', 16*ratio+'px');\n var rightLabels = $('.gpio-right-label', self.ctx.$container);\n rightLabels.css('font-size', 16*ratio+'px');\n var pins = $('.pin', self.ctx.$container);\n var pinsFontSize = Math.max(9, 12*ratio);\n pins.css('font-size', pinsFontSize+'px'); \n}\n\nself.onDestroy = function() {\n}\n", + "templateHtml": "
\n
\n
\n
\n {{ cell.label }}\n
\n {{cell.pin}}\n \n \n \n \n {{cell.pin}}\n
\n {{ cell.label }}\n
\n
\n \n \n \n
\n
\n
\n
", + "templateCss": ".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.gpio-panel section[fxflex] {\n min-width: 0px;\n}\n\n\n.gpio-panel tb-led-light > div {\n margin: auto;\n}\n\n.led-panel {\n margin: 0;\n width: 66px;\n min-width: 66px;\n}\n\n.led-container {\n width: 48px;\n min-width: 48px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.led-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.led-panel.col-1 .pin {\n margin-right: auto;\n \n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}", + "controllerScript": "var namespace;\nvar cssParser = new cssjs();\n\nself.onInit = function() {\n var utils = self.ctx.$injector.get(self.ctx.servicesMap.get('utils'));\n namespace = 'gpio-panel-' + utils.guid();\n cssParser.testMode = false;\n cssParser.cssPreviewNamespace = namespace;\n self.ctx.$container.addClass(namespace);\n self.ctx.ngZone.run(function() {\n init(); \n });\n}\n\nfunction init() {\n var i, gpio;\n \n var scope = self.ctx.$scope;\n var settings = self.ctx.settings;\n \n scope.gpioList = [];\n scope.gpioByPin = {};\n for (var g = 0; g < settings.gpioList.length; g++) {\n gpio = settings.gpioList[g];\n scope.gpioList.push(\n {\n row: gpio.row,\n col: gpio.col,\n pin: gpio.pin,\n label: gpio.label,\n enabled: false,\n colorOn: tinycolor(gpio.color).lighten(20).toHexString(),\n colorOff: tinycolor(gpio.color).darken().toHexString()\n }\n );\n scope.gpioByPin[gpio.pin] = scope.gpioList[scope.gpioList.length-1];\n }\n\n scope.ledPanelBackgroundColor = settings.ledPanelBackgroundColor || tinycolor('green').lighten(2).toRgbString();\n\n scope.gpioCells = {};\n var rowCount = 0;\n for (i = 0; i < scope.gpioList.length; i++) {\n gpio = scope.gpioList[i];\n scope.gpioCells[gpio.row+'_'+gpio.col] = gpio;\n rowCount = Math.max(rowCount, gpio.row+1);\n }\n \n scope.prefferedRowHeight = 32;\n scope.rows = [];\n for (i = 0; i < rowCount; i++) {\n var row = [];\n for (var c =0; c<2;c++) {\n if (scope.gpioCells[i+'_'+c]) {\n row[c] = scope.gpioCells[i+'_'+c];\n } else {\n row[c] = null;\n }\n }\n scope.rows.push(row);\n } \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n var changed = false;\n for (var d = 0; d < self.ctx.data.length; d++) {\n var cellData = self.ctx.data[d];\n var dataKey = cellData.dataKey;\n var gpio = self.ctx.$scope.gpioByPin[dataKey.label];\n if (gpio) {\n var enabled = false;\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n enabled = (tvPair[1] === true || tvPair[1] === 'true');\n }\n if (gpio.enabled != enabled) {\n changed = true;\n gpio.enabled = enabled;\n }\n }\n }\n if (changed) {\n self.ctx.detectChanges();\n } \n}\n\nself.onResize = function() {\n var rowCount = self.ctx.$scope.rows.length;\n var prefferedRowHeight = (self.ctx.height - 35)/rowCount;\n prefferedRowHeight = Math.min(32, prefferedRowHeight);\n prefferedRowHeight = Math.max(12, prefferedRowHeight);\n self.ctx.$scope.prefferedRowHeight = prefferedRowHeight;\n \n var ratio = prefferedRowHeight/32;\n \n var css = '.gpio-left-label, .gpio-right-label {\\n' +\n ' font-size: ' + 16*ratio+'px;\\n'+\n '}\\n';\n var pinsFontSize = Math.max(9, 12*ratio);\n css += '.pin {\\n' +\n ' font-size: ' + pinsFontSize+'px;\\n'+\n '}\\n';\n \n cssParser.createStyleElement(namespace, css); \n \n self.ctx.detectChanges();\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"gpioList\": {\n \"title\": \"Gpio leds\",\n \"type\": \"array\",\n \"minItems\" : 1,\n \"items\": {\n \"title\": \"Gpio led\",\n \"type\": \"object\",\n \"properties\": {\n \"pin\": {\n \"title\": \"Pin\",\n \"type\": \"number\"\n },\n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"row\": {\n \"title\": \"Row\",\n \"type\": \"number\"\n },\n \"col\": {\n \"title\": \"Column\",\n \"type\": \"number\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\",\n \"default\": \"red\"\n }\n },\n \"required\": [\"pin\", \"label\", \"row\", \"col\", \"color\"]\n }\n },\n \"ledPanelBackgroundColor\": {\n \"title\": \"LED panel background color\",\n \"type\": \"string\",\n \"default\": \"#008a00\"\n } \n },\n \"required\": [\"gpioList\", \n \"ledPanelBackgroundColor\"]\n },\n \"form\": [\n {\n \"key\": \"gpioList\",\n \"items\": [\n \"gpioList[].pin\",\n \"gpioList[].label\",\n \"gpioList[].row\",\n \"gpioList[].col\",\n {\n \"key\": \"gpioList[].color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"ledPanelBackgroundColor\",\n \"type\": \"color\"\n }\n ]\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"gpioList\":[{\"pin\":1,\"label\":\"GPIO 1\",\"row\":0,\"col\":0,\"color\":\"#008000\",\"_uniqueKey\":0},{\"pin\":2,\"label\":\"GPIO 2\",\"row\":0,\"col\":1,\"color\":\"#ffff00\",\"_uniqueKey\":1},{\"pin\":3,\"label\":\"GPIO 3\",\"row\":1,\"col\":0,\"color\":\"#cf006f\",\"_uniqueKey\":2}],\"ledPanelBackgroundColor\":\"#b71c1c\"},\"title\":\"Basic GPIO Panel\",\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"1\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.22518255793320163,\"funcBody\":\"var period = time % 1500;\\nreturn period < 500;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"2\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.7008206860666621,\"funcBody\":\"var period = time % 1500;\\nreturn period >= 500 && period < 1000;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"3\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.42600325102193426,\"funcBody\":\"var period = time % 1500;\\nreturn period >= 1000;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}}}" } }, { - "alias": "raspberry_pi_gpio_control", - "name": "Raspberry Pi GPIO Control", + "alias": "raspberry_pi_gpio_panel", + "name": "Raspberry Pi GPIO Panel", "descriptor": { - "type": "rpc", - "sizeX": 6, + "type": "latest", + "sizeX": 7, "sizeY": 10.5, "resources": [], - "templateHtml": "
\n
\n
\n
\n {{ cell.label }}\n
\n {{cell.pin}}\n \n \n \n \n {{cell.pin}}\n
\n {{ cell.label }}\n
\n
\n \n \n \n
\n
\n
\n {{rpcErrorText}}\n \n
", - "templateCss": ".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.switch-panel {\n margin: 0;\n height: 32px;\n width: 66px;\n min-width: 66px;\n}\n\n.switch-panel md-switch {\n margin: 0;\n width: 36px;\n min-width: 36px;\n}\n\n.switch-panel md-switch > div.md-container {\n margin: 0;\n}\n\n.switch-panel.col-0 md-switch {\n margin-left: 8px;\n margin-right: 4px;\n}\n\n.switch-panel.col-1 md-switch {\n margin-left: 4px;\n margin-right: 8px;\n}\n\n.gpio-row {\n height: 32px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.switch-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.switch-panel.col-1 .pin {\n margin-right: auto;\n \n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}", - "controllerScript": "self.onInit = function() {\n \n var i, gpio;\n var scope = self.ctx.$scope;\n var settings = self.ctx.settings;\n scope.gpioList = [];\n for (var g = 0; g < settings.gpioList.length; g++) {\n gpio = settings.gpioList[g];\n scope.gpioList.push(\n {\n row: gpio.row,\n col: gpio.col,\n pin: gpio.pin,\n label: gpio.label,\n enabled: false\n }\n );\n }\n\n scope.requestTimeout = settings.requestTimeout || 1000;\n\n scope.switchPanelBackgroundColor = settings.switchPanelBackgroundColor || tinycolor('green').lighten(2).toRgbString();\n\n scope.gpioStatusRequest = {\n method: \"getGpioStatus\",\n paramsBody: \"{}\"\n };\n \n if (settings.gpioStatusRequest) {\n scope.gpioStatusRequest.method = settings.gpioStatusRequest.method || scope.gpioStatusRequest.method;\n scope.gpioStatusRequest.paramsBody = settings.gpioStatusRequest.paramsBody || scope.gpioStatusRequest.paramsBody;\n }\n \n scope.gpioStatusChangeRequest = {\n method: \"setGpioStatus\",\n paramsBody: \"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"\n };\n \n if (settings.gpioStatusChangeRequest) {\n scope.gpioStatusChangeRequest.method = settings.gpioStatusChangeRequest.method || scope.gpioStatusChangeRequest.method;\n scope.gpioStatusChangeRequest.paramsBody = settings.gpioStatusChangeRequest.paramsBody || scope.gpioStatusChangeRequest.paramsBody;\n }\n \n scope.parseGpioStatusFunction = \"return body[pin] === true;\";\n \n if (settings.parseGpioStatusFunction && settings.parseGpioStatusFunction.length > 0) {\n scope.parseGpioStatusFunction = settings.parseGpioStatusFunction;\n }\n \n scope.parseGpioStatusFunction = new Function(\"body, pin\", scope.parseGpioStatusFunction);\n \n function requestGpioStatus() {\n self.ctx.controlApi.sendTwoWayCommand(scope.gpioStatusRequest.method, \n scope.gpioStatusRequest.paramsBody, \n scope.requestTimeout)\n .then(\n function success(responseBody) {\n for (var g = 0; g < scope.gpioList.length; g++) {\n var gpio = scope.gpioList[g];\n var enabled = scope.parseGpioStatusFunction.apply(this, [responseBody, gpio.pin]);\n gpio.enabled = enabled; \n }\n }\n );\n }\n \n function changeGpioStatus(gpio) {\n var pin = gpio.pin + '';\n var enabled = !gpio.enabled;\n enabled = enabled === true ? 'true' : 'false';\n var paramsBody = scope.gpioStatusChangeRequest.paramsBody;\n var requestBody = JSON.parse(paramsBody.replace(\"\\\"{$pin}\\\"\", pin).replace(\"\\\"{$enabled}\\\"\", enabled));\n self.ctx.controlApi.sendTwoWayCommand(scope.gpioStatusChangeRequest.method, \n requestBody, scope.requestTimeout)\n .then(\n function success(responseBody) {\n var enabled = scope.parseGpioStatusFunction.apply(this, [responseBody, gpio.pin]);\n gpio.enabled = enabled;\n }\n );\n }\n \n scope.gpioCells = {};\n var rowCount = 0;\n for (i = 0; i < scope.gpioList.length; i++) {\n gpio = scope.gpioList[i];\n scope.gpioCells[gpio.row+'_'+gpio.col] = gpio;\n rowCount = Math.max(rowCount, gpio.row+1);\n }\n \n scope.prefferedRowHeight = 32;\n scope.rows = [];\n for (i = 0; i < rowCount; i++) {\n var row = [];\n for (var c =0; c<2;c++) {\n if (scope.gpioCells[i+'_'+c]) {\n row[c] = scope.gpioCells[i+'_'+c];\n } else {\n row[c] = null;\n }\n }\n scope.rows.push(row);\n }\n\n scope.gpioClick = function($event, gpio) {\n changeGpioStatus(gpio);\n };\n\n requestGpioStatus(); \n \n self.onResize();\n}\n\nself.onResize = function() {\n var scope = self.ctx.$scope;\n var rowCount = scope.rows.length;\n var prefferedRowHeight = (self.ctx.height - 35)/rowCount;\n prefferedRowHeight = Math.min(32, prefferedRowHeight);\n prefferedRowHeight = Math.max(12, prefferedRowHeight);\n scope.prefferedRowHeight = prefferedRowHeight;\n var ratio = prefferedRowHeight/32;\n var switches = $('md-switch', self.ctx.$container);\n switches.css('height', 30*ratio+'px');\n switches.css('width', 36*ratio+'px');\n switches.css('min-width', 36*ratio+'px');\n $('.md-container', switches).css('height', 24*ratio+'px');\n $('.md-container', switches).css('width', 36*ratio+'px');\n var bars = $('.md-bar', self.ctx.$container);\n bars.css('height', 14*ratio+'px');\n bars.css('width', 34*ratio+'px');\n var thumbs = $('.md-thumb', self.ctx.$container);\n thumbs.css('height', 20*ratio+'px');\n thumbs.css('width', 20*ratio+'px');\n \n var leftLabels = $('.gpio-left-label', self.ctx.$container);\n leftLabels.css('font-size', 16*ratio+'px');\n var rightLabels = $('.gpio-right-label', self.ctx.$container);\n rightLabels.css('font-size', 16*ratio+'px');\n var pins = $('.pin', self.ctx.$container);\n var pinsFontSize = Math.max(9, 12*ratio);\n pins.css('font-size', pinsFontSize+'px'); \n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"gpioList\": {\n \"title\": \"Gpio switches\",\n \"type\": \"array\",\n \"minItems\" : 1,\n \"items\": {\n \"title\": \"Gpio switch\",\n \"type\": \"object\",\n \"properties\": {\n \"pin\": {\n \"title\": \"Pin\",\n \"type\": \"number\"\n },\n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"row\": {\n \"title\": \"Row\",\n \"type\": \"number\"\n },\n \"col\": {\n \"title\": \"Column\",\n \"type\": \"number\"\n }\n },\n \"required\": [\"pin\", \"label\", \"row\", \"col\"]\n }\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"switchPanelBackgroundColor\": {\n \"title\": \"Switches panel background color\",\n \"type\": \"string\",\n \"default\": \"#008a00\"\n },\n \"gpioStatusRequest\": {\n \"title\": \"GPIO status request\",\n \"type\": \"object\",\n \"properties\": {\n \"method\": {\n \"title\": \"Method name\",\n \"type\": \"string\",\n \"default\": \"getGpioStatus\"\n },\n \"paramsBody\": {\n \"title\": \"Method body\",\n \"type\": \"string\",\n \"default\": \"{}\"\n }\n },\n \"required\": [\"method\", \"paramsBody\"]\n },\n \"gpioStatusChangeRequest\": {\n \"title\": \"GPIO status change request\",\n \"type\": \"object\",\n \"properties\": {\n \"method\": {\n \"title\": \"Method name\",\n \"type\": \"string\",\n \"default\": \"setGpioStatus\"\n },\n \"paramsBody\": {\n \"title\": \"Method body\",\n \"type\": \"string\",\n \"default\": \"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"\n }\n },\n \"required\": [\"method\", \"paramsBody\"]\n },\n \"parseGpioStatusFunction\": {\n \"title\": \"Parse gpio status function\",\n \"type\": \"string\",\n \"default\": \"return body[pin] === true;\"\n } \n },\n \"required\": [\"gpioList\", \n \"requestTimeout\",\n \"switchPanelBackgroundColor\",\n \"gpioStatusRequest\",\n \"gpioStatusChangeRequest\",\n \"parseGpioStatusFunction\"]\n },\n \"form\": [\n \"gpioList\",\n \"requestTimeout\",\n {\n \"key\": \"switchPanelBackgroundColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"gpioStatusRequest\",\n \"items\": [\n \"gpioStatusRequest.method\",\n {\n \"key\": \"gpioStatusRequest.paramsBody\",\n \"type\": \"json\"\n }\n ]\n },\n {\n \"key\": \"gpioStatusChangeRequest\",\n \"items\": [\n \"gpioStatusChangeRequest.method\",\n {\n \"key\": \"gpioStatusChangeRequest.paramsBody\",\n \"type\": \"json\"\n }\n ]\n },\n {\n \"key\": \"parseGpioStatusFunction\",\n \"type\": \"javascript\"\n }\n ]\n}", + "templateHtml": "
\n
\n
\n
\n {{ cell.label }}\n
\n {{cell.pin}}\n \n \n \n \n {{cell.pin}}\n
\n {{ cell.label }}\n
\n
\n \n \n \n
\n
\n
\n
", + "templateCss": ".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.gpio-panel section[fxflex] {\n min-width: 0px;\n}\n\n.gpio-panel tb-led-light > div {\n margin: auto;\n}\n\n.led-panel {\n margin: 0;\n width: 66px;\n min-width: 66px;\n}\n\n.led-container {\n width: 48px;\n min-width: 48px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.led-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.led-panel.col-1 .pin {\n margin-right: auto;\n \n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}", + "controllerScript": "var namespace;\nvar cssParser = new cssjs();\n\nself.onInit = function() {\n var utils = self.ctx.$injector.get(self.ctx.servicesMap.get('utils'));\n namespace = 'gpio-panel-' + utils.guid();\n cssParser.testMode = false;\n cssParser.cssPreviewNamespace = namespace;\n self.ctx.$container.addClass(namespace);\n self.ctx.ngZone.run(function() {\n init(); \n });\n}\n\nfunction init() {\n var i, gpio;\n \n var scope = self.ctx.$scope;\n var settings = self.ctx.settings;\n \n scope.gpioList = [];\n scope.gpioByPin = {};\n for (var g = 0; g < settings.gpioList.length; g++) {\n gpio = settings.gpioList[g];\n scope.gpioList.push(\n {\n row: gpio.row,\n col: gpio.col,\n pin: gpio.pin,\n label: gpio.label,\n enabled: false,\n colorOn: tinycolor(gpio.color).lighten(20).toHexString(),\n colorOff: tinycolor(gpio.color).darken().toHexString()\n }\n );\n scope.gpioByPin[gpio.pin] = scope.gpioList[scope.gpioList.length-1];\n }\n\n scope.ledPanelBackgroundColor = settings.ledPanelBackgroundColor || tinycolor('green').lighten(2).toRgbString();\n\n scope.gpioCells = {};\n var rowCount = 0;\n for (i = 0; i < scope.gpioList.length; i++) {\n gpio = scope.gpioList[i];\n scope.gpioCells[gpio.row+'_'+gpio.col] = gpio;\n rowCount = Math.max(rowCount, gpio.row+1);\n }\n \n scope.prefferedRowHeight = 32;\n scope.rows = [];\n for (i = 0; i < rowCount; i++) {\n var row = [];\n for (var c =0; c<2;c++) {\n if (scope.gpioCells[i+'_'+c]) {\n row[c] = scope.gpioCells[i+'_'+c];\n } else {\n row[c] = null;\n }\n }\n scope.rows.push(row);\n } \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n var changed = false;\n for (var d = 0; d < self.ctx.data.length; d++) {\n var cellData = self.ctx.data[d];\n var dataKey = cellData.dataKey;\n var gpio = self.ctx.$scope.gpioByPin[dataKey.label];\n if (gpio) {\n var enabled = false;\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n enabled = (tvPair[1] === true || tvPair[1] === 'true');\n }\n if (gpio.enabled != enabled) {\n changed = true;\n gpio.enabled = enabled;\n }\n }\n }\n if (changed) {\n self.ctx.detectChanges();\n } \n}\n\nself.onResize = function() {\n var rowCount = self.ctx.$scope.rows.length;\n var prefferedRowHeight = (self.ctx.height - 35)/rowCount;\n prefferedRowHeight = Math.min(32, prefferedRowHeight);\n prefferedRowHeight = Math.max(12, prefferedRowHeight);\n self.ctx.$scope.prefferedRowHeight = prefferedRowHeight;\n \n var ratio = prefferedRowHeight/32;\n \n var css = '.gpio-left-label, .gpio-right-label {\\n' +\n ' font-size: ' + 16*ratio+'px;\\n'+\n '}\\n';\n var pinsFontSize = Math.max(9, 12*ratio);\n css += '.pin {\\n' +\n ' font-size: ' + pinsFontSize+'px;\\n'+\n '}\\n';\n \n cssParser.createStyleElement(namespace, css); \n \n self.ctx.detectChanges();\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"gpioList\": {\n \"title\": \"Gpio leds\",\n \"type\": \"array\",\n \"minItems\" : 1,\n \"items\": {\n \"title\": \"Gpio led\",\n \"type\": \"object\",\n \"properties\": {\n \"pin\": {\n \"title\": \"Pin\",\n \"type\": \"number\"\n },\n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"row\": {\n \"title\": \"Row\",\n \"type\": \"number\"\n },\n \"col\": {\n \"title\": \"Column\",\n \"type\": \"number\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\",\n \"default\": \"red\"\n }\n },\n \"required\": [\"pin\", \"label\", \"row\", \"col\", \"color\"]\n }\n },\n \"ledPanelBackgroundColor\": {\n \"title\": \"LED panel background color\",\n \"type\": \"string\",\n \"default\": \"#008a00\"\n } \n },\n \"required\": [\"gpioList\", \n \"ledPanelBackgroundColor\"]\n },\n \"form\": [\n {\n \"key\": \"gpioList\",\n \"items\": [\n \"gpioList[].pin\",\n \"gpioList[].label\",\n \"gpioList[].row\",\n \"gpioList[].col\",\n {\n \"key\": \"gpioList[].color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"ledPanelBackgroundColor\",\n \"type\": \"color\"\n }\n ]\n}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"parseGpioStatusFunction\":\"return body[pin] === true;\",\"gpioStatusChangeRequest\":{\"method\":\"setGpioStatus\",\"paramsBody\":\"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"},\"requestTimeout\":500,\"switchPanelBackgroundColor\":\"#008a00\",\"gpioStatusRequest\":{\"method\":\"getGpioStatus\",\"paramsBody\":\"{}\"},\"gpioList\":[{\"pin\":7,\"label\":\"GPIO 4 (GPCLK0)\",\"row\":3,\"col\":0,\"_uniqueKey\":0},{\"pin\":11,\"label\":\"GPIO 17\",\"row\":5,\"col\":0,\"_uniqueKey\":1},{\"pin\":12,\"label\":\"GPIO 18\",\"row\":5,\"col\":1,\"_uniqueKey\":2},{\"_uniqueKey\":3,\"pin\":13,\"label\":\"GPIO 27\",\"row\":6,\"col\":0},{\"_uniqueKey\":4,\"pin\":15,\"label\":\"GPIO 22\",\"row\":7,\"col\":0},{\"_uniqueKey\":5,\"pin\":16,\"label\":\"GPIO 23\",\"row\":7,\"col\":1},{\"_uniqueKey\":6,\"pin\":18,\"label\":\"GPIO 24\",\"row\":8,\"col\":1},{\"_uniqueKey\":7,\"pin\":22,\"label\":\"GPIO 25\",\"row\":10,\"col\":1},{\"_uniqueKey\":8,\"pin\":29,\"label\":\"GPIO 5\",\"row\":14,\"col\":0},{\"_uniqueKey\":9,\"pin\":31,\"label\":\"GPIO 6\",\"row\":15,\"col\":0},{\"_uniqueKey\":10,\"pin\":32,\"label\":\"GPIO 12\",\"row\":15,\"col\":1},{\"_uniqueKey\":11,\"pin\":33,\"label\":\"GPIO 13\",\"row\":16,\"col\":0},{\"_uniqueKey\":12,\"pin\":35,\"label\":\"GPIO 19\",\"row\":17,\"col\":0},{\"_uniqueKey\":13,\"pin\":36,\"label\":\"GPIO 16\",\"row\":17,\"col\":1},{\"_uniqueKey\":14,\"pin\":37,\"label\":\"GPIO 26\",\"row\":18,\"col\":0},{\"_uniqueKey\":15,\"pin\":38,\"label\":\"GPIO 20\",\"row\":18,\"col\":1},{\"_uniqueKey\":16,\"pin\":40,\"label\":\"GPIO 21\",\"row\":19,\"col\":1}]},\"title\":\"Raspberry Pi GPIO Control\"}" + "defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"gpioList\":[{\"pin\":1,\"label\":\"3.3V\",\"row\":0,\"col\":0,\"color\":\"#fc9700\",\"_uniqueKey\":0},{\"pin\":2,\"label\":\"5V\",\"row\":0,\"col\":1,\"color\":\"#fb0000\",\"_uniqueKey\":1},{\"pin\":3,\"label\":\"GPIO 2 (I2C1_SDA)\",\"row\":1,\"col\":0,\"color\":\"#02fefb\",\"_uniqueKey\":2},{\"color\":\"#fb0000\",\"pin\":4,\"label\":\"5V\",\"row\":1,\"col\":1},{\"color\":\"#02fefb\",\"pin\":5,\"label\":\"GPIO 3 (I2C1_SCL)\",\"row\":2,\"col\":0},{\"color\":\"#000000\",\"pin\":6,\"label\":\"GND\",\"row\":2,\"col\":1},{\"color\":\"#00fd00\",\"pin\":7,\"label\":\"GPIO 4 (GPCLK0)\",\"row\":3,\"col\":0},{\"color\":\"#fdfb00\",\"pin\":8,\"label\":\"GPIO 14 (UART_TXD)\",\"row\":3,\"col\":1},{\"color\":\"#000000\",\"pin\":9,\"label\":\"GND\",\"row\":4,\"col\":0},{\"color\":\"#fdfb00\",\"pin\":10,\"label\":\"GPIO 15 (UART_RXD)\",\"row\":4,\"col\":1},{\"color\":\"#00fd00\",\"pin\":11,\"label\":\"GPIO 17\",\"row\":5,\"col\":0},{\"color\":\"#00fd00\",\"pin\":12,\"label\":\"GPIO 18\",\"row\":5,\"col\":1},{\"color\":\"#00fd00\",\"pin\":13,\"label\":\"GPIO 27\",\"row\":6,\"col\":0},{\"color\":\"#000000\",\"pin\":14,\"label\":\"GND\",\"row\":6,\"col\":1},{\"color\":\"#00fd00\",\"pin\":15,\"label\":\"GPIO 22\",\"row\":7,\"col\":0},{\"color\":\"#00fd00\",\"pin\":16,\"label\":\"GPIO 23\",\"row\":7,\"col\":1},{\"color\":\"#fc9700\",\"pin\":17,\"label\":\"3.3V\",\"row\":8,\"col\":0},{\"color\":\"#00fd00\",\"pin\":18,\"label\":\"GPIO 24\",\"row\":8,\"col\":1},{\"color\":\"#fd01fd\",\"pin\":19,\"label\":\"GPIO 10 (SPI_MOSI)\",\"row\":9,\"col\":0},{\"color\":\"#000000\",\"pin\":20,\"label\":\"GND\",\"row\":9,\"col\":1},{\"color\":\"#fd01fd\",\"pin\":21,\"label\":\"GPIO 9 (SPI_MISO)\",\"row\":10,\"col\":0},{\"color\":\"#00fd00\",\"pin\":22,\"label\":\"GPIO 25\",\"row\":10,\"col\":1},{\"color\":\"#fd01fd\",\"pin\":23,\"label\":\"GPIO 11 (SPI_SCLK)\",\"row\":11,\"col\":0},{\"color\":\"#fd01fd\",\"pin\":24,\"label\":\"GPIO 8 (SPI_CE0)\",\"row\":11,\"col\":1},{\"color\":\"#000000\",\"pin\":25,\"label\":\"GND\",\"row\":12,\"col\":0},{\"color\":\"#fd01fd\",\"pin\":26,\"label\":\"GPIO 7 (SPI_CE1)\",\"row\":12,\"col\":1},{\"color\":\"#ffffff\",\"pin\":27,\"label\":\"ID_SD\",\"row\":13,\"col\":0},{\"color\":\"#ffffff\",\"pin\":28,\"label\":\"ID_SC\",\"row\":13,\"col\":1},{\"color\":\"#00fd00\",\"pin\":29,\"label\":\"GPIO 5\",\"row\":14,\"col\":0},{\"color\":\"#000000\",\"pin\":30,\"label\":\"GND\",\"row\":14,\"col\":1},{\"color\":\"#00fd00\",\"pin\":31,\"label\":\"GPIO 6\",\"row\":15,\"col\":0},{\"color\":\"#00fd00\",\"pin\":32,\"label\":\"GPIO 12\",\"row\":15,\"col\":1},{\"color\":\"#00fd00\",\"pin\":33,\"label\":\"GPIO 13\",\"row\":16,\"col\":0},{\"color\":\"#000000\",\"pin\":34,\"label\":\"GND\",\"row\":16,\"col\":1},{\"color\":\"#00fd00\",\"pin\":35,\"label\":\"GPIO 19\",\"row\":17,\"col\":0},{\"color\":\"#00fd00\",\"pin\":36,\"label\":\"GPIO 16\",\"row\":17,\"col\":1},{\"color\":\"#00fd00\",\"pin\":37,\"label\":\"GPIO 26\",\"row\":18,\"col\":0},{\"color\":\"#00fd00\",\"pin\":38,\"label\":\"GPIO 20\",\"row\":18,\"col\":1},{\"color\":\"#000000\",\"pin\":39,\"label\":\"GND\",\"row\":19,\"col\":0},{\"color\":\"#00fd00\",\"pin\":40,\"label\":\"GPIO 21\",\"row\":19,\"col\":1}],\"ledPanelBackgroundColor\":\"#008a00\"},\"title\":\"Raspberry Pi GPIO Panel\",\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"7\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.22518255793320163,\"funcBody\":\"var period = time % 1500;\\nreturn period < 500;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"11\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.7008206860666621,\"funcBody\":\"var period = time % 1500;\\nreturn period >= 500 && period < 1000;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"12\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.42600325102193426,\"funcBody\":\"var period = time % 1500;\\nreturn period >= 1000;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"13\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.48362241571415243,\"funcBody\":\"var period = time % 1500;\\nreturn period < 500;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"29\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.7217670147518815,\"funcBody\":\"var period = time % 1500;\\nreturn period >= 500 && period < 1000;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}}}" } }, { - "alias": "raspberry_pi_gpio_panel", - "name": "Raspberry Pi GPIO Panel", + "alias": "raspberry_pi_gpio_control", + "name": "Raspberry Pi GPIO Control", "descriptor": { - "type": "latest", - "sizeX": 7, + "type": "rpc", + "sizeX": 6, "sizeY": 10.5, "resources": [], - "templateHtml": "
\n
\n
\n
\n {{ cell.label }}\n
\n {{cell.pin}}\n \n \n \n \n {{cell.pin}}\n
\n {{ cell.label }}\n
\n
\n \n \n \n
\n
\n
\n
", - "templateCss": ".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.gpio-panel tb-led-light > div {\n margin: auto;\n}\n\n.led-panel {\n margin: 0;\n width: 66px;\n min-width: 66px;\n}\n\n.led-container {\n width: 48px;\n min-width: 48px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.led-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.led-panel.col-1 .pin {\n margin-right: auto;\n \n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}", - "controllerScript": "self.onInit = function() {\n var i, gpio;\n \n var scope = self.ctx.$scope;\n var settings = self.ctx.settings;\n \n scope.gpioList = [];\n scope.gpioByPin = {};\n for (var g = 0; g < settings.gpioList.length; g++) {\n gpio = settings.gpioList[g];\n scope.gpioList.push(\n {\n row: gpio.row,\n col: gpio.col,\n pin: gpio.pin,\n label: gpio.label,\n enabled: false,\n colorOn: tinycolor(gpio.color).lighten(20).toHexString(),\n colorOff: tinycolor(gpio.color).darken().toHexString()\n }\n );\n scope.gpioByPin[gpio.pin] = scope.gpioList[scope.gpioList.length-1];\n }\n\n scope.ledPanelBackgroundColor = settings.ledPanelBackgroundColor || tinycolor('green').lighten(2).toRgbString();\n\n scope.gpioCells = {};\n var rowCount = 0;\n for (i = 0; i < scope.gpioList.length; i++) {\n gpio = scope.gpioList[i];\n scope.gpioCells[gpio.row+'_'+gpio.col] = gpio;\n rowCount = Math.max(rowCount, gpio.row+1);\n }\n \n scope.prefferedRowHeight = 32;\n scope.rows = [];\n for (i = 0; i < rowCount; i++) {\n var row = [];\n for (var c =0; c<2;c++) {\n if (scope.gpioCells[i+'_'+c]) {\n row[c] = scope.gpioCells[i+'_'+c];\n } else {\n row[c] = null;\n }\n }\n scope.rows.push(row);\n } \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n var changed = false;\n for (var d = 0; d < self.ctx.data.length; d++) {\n var cellData = self.ctx.data[d];\n var dataKey = cellData.dataKey;\n var gpio = self.ctx.$scope.gpioByPin[dataKey.label];\n if (gpio) {\n var enabled = false;\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n enabled = (tvPair[1] === true || tvPair[1] === 'true');\n }\n if (gpio.enabled != enabled) {\n changed = true;\n gpio.enabled = enabled;\n }\n }\n }\n if (changed) {\n self.ctx.$scope.$digest();\n } \n}\n\nself.onResize = function() {\n var rowCount = self.ctx.$scope.rows.length;\n var prefferedRowHeight = (self.ctx.height - 35)/rowCount;\n prefferedRowHeight = Math.min(32, prefferedRowHeight);\n prefferedRowHeight = Math.max(12, prefferedRowHeight);\n self.ctx.$scope.prefferedRowHeight = prefferedRowHeight;\n \n var ratio = prefferedRowHeight/32;\n \n var leftLabels = $('.gpio-left-label', self.ctx.$container);\n leftLabels.css('font-size', 16*ratio+'px');\n var rightLabels = $('.gpio-right-label', self.ctx.$container);\n rightLabels.css('font-size', 16*ratio+'px');\n var pins = $('.pin', self.ctx.$container);\n var pinsFontSize = Math.max(9, 12*ratio);\n pins.css('font-size', pinsFontSize+'px'); \n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"gpioList\": {\n \"title\": \"Gpio leds\",\n \"type\": \"array\",\n \"minItems\" : 1,\n \"items\": {\n \"title\": \"Gpio led\",\n \"type\": \"object\",\n \"properties\": {\n \"pin\": {\n \"title\": \"Pin\",\n \"type\": \"number\"\n },\n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"row\": {\n \"title\": \"Row\",\n \"type\": \"number\"\n },\n \"col\": {\n \"title\": \"Column\",\n \"type\": \"number\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\",\n \"default\": \"red\"\n }\n },\n \"required\": [\"pin\", \"label\", \"row\", \"col\", \"color\"]\n }\n },\n \"ledPanelBackgroundColor\": {\n \"title\": \"LED panel background color\",\n \"type\": \"string\",\n \"default\": \"#008a00\"\n } \n },\n \"required\": [\"gpioList\", \n \"ledPanelBackgroundColor\"]\n },\n \"form\": [\n {\n \"key\": \"gpioList\",\n \"items\": [\n \"gpioList[].pin\",\n \"gpioList[].label\",\n \"gpioList[].row\",\n \"gpioList[].col\",\n {\n \"key\": \"gpioList[].color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"ledPanelBackgroundColor\",\n \"type\": \"color\"\n }\n ]\n}", + "templateHtml": "
\n
\n
\n
\n {{ cell.label }}\n
\n {{cell.pin}}\n \n \n \n \n {{cell.pin}}\n
\n {{ cell.label }}\n
\n
\n \n \n \n
\n
\n
\n {{rpcErrorText}}\n \n
", + "templateCss": ".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.gpio-panel section[fxflex] {\n min-width: 0px;\n}\n\n.switch-panel {\n margin: 0;\n height: 32px;\n width: 66px;\n min-width: 66px;\n}\n\n.switch-panel mat-slide-toggle {\n margin: 0;\n width: 36px;\n min-width: 36px;\n}\n\n.switch-panel.col-0 mat-slide-toggle {\n margin-left: 8px;\n margin-right: 4px;\n}\n\n.switch-panel.col-1 mat-slide-toggle {\n margin-left: 4px;\n margin-right: 8px;\n}\n\n.gpio-row {\n height: 32px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.switch-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.switch-panel.col-1 .pin {\n margin-right: auto;\n \n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}", + "controllerScript": "var namespace;\nvar cssParser = new cssjs();\n\nself.onInit = function() {\n var utils = self.ctx.$injector.get(self.ctx.servicesMap.get('utils'));\n namespace = 'gpio-control-' + utils.guid();\n cssParser.testMode = false;\n cssParser.cssPreviewNamespace = namespace;\n self.ctx.$container.addClass(namespace);\n self.ctx.ngZone.run(function() {\n init(); \n });\n}\n\nfunction init() {\n \n var i, gpio;\n var scope = self.ctx.$scope;\n var settings = self.ctx.settings;\n scope.gpioList = [];\n for (var g = 0; g < settings.gpioList.length; g++) {\n gpio = settings.gpioList[g];\n scope.gpioList.push(\n {\n row: gpio.row,\n col: gpio.col,\n pin: gpio.pin,\n label: gpio.label,\n enabled: false\n }\n );\n }\n\n scope.requestTimeout = settings.requestTimeout || 1000;\n\n scope.switchPanelBackgroundColor = settings.switchPanelBackgroundColor || tinycolor('green').lighten(2).toRgbString();\n\n scope.gpioStatusRequest = {\n method: \"getGpioStatus\",\n paramsBody: \"{}\"\n };\n \n if (settings.gpioStatusRequest) {\n scope.gpioStatusRequest.method = settings.gpioStatusRequest.method || scope.gpioStatusRequest.method;\n scope.gpioStatusRequest.paramsBody = settings.gpioStatusRequest.paramsBody || scope.gpioStatusRequest.paramsBody;\n }\n \n scope.gpioStatusChangeRequest = {\n method: \"setGpioStatus\",\n paramsBody: \"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"\n };\n \n if (settings.gpioStatusChangeRequest) {\n scope.gpioStatusChangeRequest.method = settings.gpioStatusChangeRequest.method || scope.gpioStatusChangeRequest.method;\n scope.gpioStatusChangeRequest.paramsBody = settings.gpioStatusChangeRequest.paramsBody || scope.gpioStatusChangeRequest.paramsBody;\n }\n \n scope.parseGpioStatusFunction = \"return body[pin] === true;\";\n \n if (settings.parseGpioStatusFunction && settings.parseGpioStatusFunction.length > 0) {\n scope.parseGpioStatusFunction = settings.parseGpioStatusFunction;\n }\n \n scope.parseGpioStatusFunction = new Function(\"body, pin\", scope.parseGpioStatusFunction);\n \n function requestGpioStatus() {\n self.ctx.controlApi.sendTwoWayCommand(scope.gpioStatusRequest.method, \n scope.gpioStatusRequest.paramsBody, \n scope.requestTimeout)\n .subscribe(\n function success(responseBody) {\n for (var g = 0; g < scope.gpioList.length; g++) {\n var gpio = scope.gpioList[g];\n var enabled = scope.parseGpioStatusFunction.apply(this, [responseBody, gpio.pin]);\n gpio.enabled = enabled; \n self.ctx.detectChanges();\n }\n }\n );\n }\n \n function changeGpioStatus(gpio) {\n var pin = gpio.pin + '';\n var enabled = !gpio.enabled;\n enabled = enabled === true ? 'true' : 'false';\n var paramsBody = scope.gpioStatusChangeRequest.paramsBody;\n var requestBody = JSON.parse(paramsBody.replace(\"\\\"{$pin}\\\"\", pin).replace(\"\\\"{$enabled}\\\"\", enabled));\n self.ctx.controlApi.sendTwoWayCommand(scope.gpioStatusChangeRequest.method, \n requestBody, scope.requestTimeout)\n .subscribe(\n function success(responseBody) {\n var enabled = scope.parseGpioStatusFunction.apply(this, [responseBody, gpio.pin]);\n gpio.enabled = enabled;\n self.ctx.detectChanges();\n }\n );\n }\n \n scope.gpioCells = {};\n var rowCount = 0;\n for (i = 0; i < scope.gpioList.length; i++) {\n gpio = scope.gpioList[i];\n scope.gpioCells[gpio.row+'_'+gpio.col] = gpio;\n rowCount = Math.max(rowCount, gpio.row+1);\n }\n \n scope.prefferedRowHeight = 32;\n scope.rows = [];\n for (i = 0; i < rowCount; i++) {\n var row = [];\n for (var c =0; c<2;c++) {\n if (scope.gpioCells[i+'_'+c]) {\n row[c] = scope.gpioCells[i+'_'+c];\n } else {\n row[c] = null;\n }\n }\n scope.rows.push(row);\n }\n\n scope.gpioClick = function($event, gpio) {\n if (scope.rpcEnabled && !scope.executingRpcRequest) {\n changeGpioStatus(gpio);\n }\n };\n \n scope.gpioToggleChange = function($event, gpio) {\n gpio.enabled = !$event.checked;\n $event.source.toggle();\n self.ctx.detectChanges();\n }\n \n if (scope.rpcEnabled) {\n requestGpioStatus(); \n }\n \n self.onResize();\n}\n\nself.onResize = function() {\n var scope = self.ctx.$scope;\n var rowCount = scope.rows.length;\n var prefferedRowHeight = (self.ctx.height - 35)/rowCount;\n prefferedRowHeight = Math.min(32, prefferedRowHeight);\n prefferedRowHeight = Math.max(12, prefferedRowHeight);\n scope.prefferedRowHeight = prefferedRowHeight;\n var ratio = prefferedRowHeight/32;\n \n var css = '.mat-slide-toggle .mat-slide-toggle-bar {\\n' +\n ' height: ' + 14*ratio+'px;\\n'+\n ' width: ' + 36*ratio+'px;\\n'+\n '}\\n';\n css += '.mat-slide-toggle .mat-slide-toggle-thumb-container {\\n' +\n ' height: ' + 20*ratio+'px;\\n'+\n ' width: ' + 20*ratio+'px;\\n'+\n '}\\n';\n css += '.mat-slide-toggle .mat-slide-toggle-thumb {\\n' +\n ' height: ' + 20*ratio+'px;\\n'+\n ' width: ' + 20*ratio+'px;\\n'+\n '}\\n';\n css += '.mat-slide-toggle .mat-slide-toggle-ripple {\\n' +\n ' height: ' + 40*ratio+'px;\\n'+\n ' width: ' + 40*ratio+'px;\\n'+\n ' top: calc(50% - '+20*ratio+'px);\\n'+\n ' left: calc(50% - '+20*ratio+'px);\\n'+\n '}\\n';\n css += '.gpio-left-label, .gpio-right-label {\\n' +\n ' font-size: ' + 16*ratio+'px;\\n'+\n '}\\n';\n var pinsFontSize = Math.max(9, 12*ratio);\n css += '.pin {\\n' +\n ' font-size: ' + pinsFontSize+'px;\\n'+\n '}\\n';\n\n cssParser.createStyleElement(namespace, css);\n \n self.ctx.detectChanges();\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"gpioList\": {\n \"title\": \"Gpio switches\",\n \"type\": \"array\",\n \"minItems\" : 1,\n \"items\": {\n \"title\": \"Gpio switch\",\n \"type\": \"object\",\n \"properties\": {\n \"pin\": {\n \"title\": \"Pin\",\n \"type\": \"number\"\n },\n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"row\": {\n \"title\": \"Row\",\n \"type\": \"number\"\n },\n \"col\": {\n \"title\": \"Column\",\n \"type\": \"number\"\n }\n },\n \"required\": [\"pin\", \"label\", \"row\", \"col\"]\n }\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"switchPanelBackgroundColor\": {\n \"title\": \"Switches panel background color\",\n \"type\": \"string\",\n \"default\": \"#008a00\"\n },\n \"gpioStatusRequest\": {\n \"title\": \"GPIO status request\",\n \"type\": \"object\",\n \"properties\": {\n \"method\": {\n \"title\": \"Method name\",\n \"type\": \"string\",\n \"default\": \"getGpioStatus\"\n },\n \"paramsBody\": {\n \"title\": \"Method body\",\n \"type\": \"string\",\n \"default\": \"{}\"\n }\n },\n \"required\": [\"method\", \"paramsBody\"]\n },\n \"gpioStatusChangeRequest\": {\n \"title\": \"GPIO status change request\",\n \"type\": \"object\",\n \"properties\": {\n \"method\": {\n \"title\": \"Method name\",\n \"type\": \"string\",\n \"default\": \"setGpioStatus\"\n },\n \"paramsBody\": {\n \"title\": \"Method body\",\n \"type\": \"string\",\n \"default\": \"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"\n }\n },\n \"required\": [\"method\", \"paramsBody\"]\n },\n \"parseGpioStatusFunction\": {\n \"title\": \"Parse gpio status function\",\n \"type\": \"string\",\n \"default\": \"return body[pin] === true;\"\n } \n },\n \"required\": [\"gpioList\", \n \"requestTimeout\",\n \"switchPanelBackgroundColor\",\n \"gpioStatusRequest\",\n \"gpioStatusChangeRequest\",\n \"parseGpioStatusFunction\"]\n },\n \"form\": [\n \"gpioList\",\n \"requestTimeout\",\n {\n \"key\": \"switchPanelBackgroundColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"gpioStatusRequest\",\n \"items\": [\n \"gpioStatusRequest.method\",\n {\n \"key\": \"gpioStatusRequest.paramsBody\",\n \"type\": \"json\"\n }\n ]\n },\n {\n \"key\": \"gpioStatusChangeRequest\",\n \"items\": [\n \"gpioStatusChangeRequest.method\",\n {\n \"key\": \"gpioStatusChangeRequest.paramsBody\",\n \"type\": \"json\"\n }\n ]\n },\n {\n \"key\": \"parseGpioStatusFunction\",\n \"type\": \"javascript\"\n }\n ]\n}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"gpioList\":[{\"pin\":1,\"label\":\"3.3V\",\"row\":0,\"col\":0,\"color\":\"#fc9700\",\"_uniqueKey\":0},{\"pin\":2,\"label\":\"5V\",\"row\":0,\"col\":1,\"color\":\"#fb0000\",\"_uniqueKey\":1},{\"pin\":3,\"label\":\"GPIO 2 (I2C1_SDA)\",\"row\":1,\"col\":0,\"color\":\"#02fefb\",\"_uniqueKey\":2},{\"color\":\"#fb0000\",\"pin\":4,\"label\":\"5V\",\"row\":1,\"col\":1},{\"color\":\"#02fefb\",\"pin\":5,\"label\":\"GPIO 3 (I2C1_SCL)\",\"row\":2,\"col\":0},{\"color\":\"#000000\",\"pin\":6,\"label\":\"GND\",\"row\":2,\"col\":1},{\"color\":\"#00fd00\",\"pin\":7,\"label\":\"GPIO 4 (GPCLK0)\",\"row\":3,\"col\":0},{\"color\":\"#fdfb00\",\"pin\":8,\"label\":\"GPIO 14 (UART_TXD)\",\"row\":3,\"col\":1},{\"color\":\"#000000\",\"pin\":9,\"label\":\"GND\",\"row\":4,\"col\":0},{\"color\":\"#fdfb00\",\"pin\":10,\"label\":\"GPIO 15 (UART_RXD)\",\"row\":4,\"col\":1},{\"color\":\"#00fd00\",\"pin\":11,\"label\":\"GPIO 17\",\"row\":5,\"col\":0},{\"color\":\"#00fd00\",\"pin\":12,\"label\":\"GPIO 18\",\"row\":5,\"col\":1},{\"color\":\"#00fd00\",\"pin\":13,\"label\":\"GPIO 27\",\"row\":6,\"col\":0},{\"color\":\"#000000\",\"pin\":14,\"label\":\"GND\",\"row\":6,\"col\":1},{\"color\":\"#00fd00\",\"pin\":15,\"label\":\"GPIO 22\",\"row\":7,\"col\":0},{\"color\":\"#00fd00\",\"pin\":16,\"label\":\"GPIO 23\",\"row\":7,\"col\":1},{\"color\":\"#fc9700\",\"pin\":17,\"label\":\"3.3V\",\"row\":8,\"col\":0},{\"color\":\"#00fd00\",\"pin\":18,\"label\":\"GPIO 24\",\"row\":8,\"col\":1},{\"color\":\"#fd01fd\",\"pin\":19,\"label\":\"GPIO 10 (SPI_MOSI)\",\"row\":9,\"col\":0},{\"color\":\"#000000\",\"pin\":20,\"label\":\"GND\",\"row\":9,\"col\":1},{\"color\":\"#fd01fd\",\"pin\":21,\"label\":\"GPIO 9 (SPI_MISO)\",\"row\":10,\"col\":0},{\"color\":\"#00fd00\",\"pin\":22,\"label\":\"GPIO 25\",\"row\":10,\"col\":1},{\"color\":\"#fd01fd\",\"pin\":23,\"label\":\"GPIO 11 (SPI_SCLK)\",\"row\":11,\"col\":0},{\"color\":\"#fd01fd\",\"pin\":24,\"label\":\"GPIO 8 (SPI_CE0)\",\"row\":11,\"col\":1},{\"color\":\"#000000\",\"pin\":25,\"label\":\"GND\",\"row\":12,\"col\":0},{\"color\":\"#fd01fd\",\"pin\":26,\"label\":\"GPIO 7 (SPI_CE1)\",\"row\":12,\"col\":1},{\"color\":\"#ffffff\",\"pin\":27,\"label\":\"ID_SD\",\"row\":13,\"col\":0},{\"color\":\"#ffffff\",\"pin\":28,\"label\":\"ID_SC\",\"row\":13,\"col\":1},{\"color\":\"#00fd00\",\"pin\":29,\"label\":\"GPIO 5\",\"row\":14,\"col\":0},{\"color\":\"#000000\",\"pin\":30,\"label\":\"GND\",\"row\":14,\"col\":1},{\"color\":\"#00fd00\",\"pin\":31,\"label\":\"GPIO 6\",\"row\":15,\"col\":0},{\"color\":\"#00fd00\",\"pin\":32,\"label\":\"GPIO 12\",\"row\":15,\"col\":1},{\"color\":\"#00fd00\",\"pin\":33,\"label\":\"GPIO 13\",\"row\":16,\"col\":0},{\"color\":\"#000000\",\"pin\":34,\"label\":\"GND\",\"row\":16,\"col\":1},{\"color\":\"#00fd00\",\"pin\":35,\"label\":\"GPIO 19\",\"row\":17,\"col\":0},{\"color\":\"#00fd00\",\"pin\":36,\"label\":\"GPIO 16\",\"row\":17,\"col\":1},{\"color\":\"#00fd00\",\"pin\":37,\"label\":\"GPIO 26\",\"row\":18,\"col\":0},{\"color\":\"#00fd00\",\"pin\":38,\"label\":\"GPIO 20\",\"row\":18,\"col\":1},{\"color\":\"#000000\",\"pin\":39,\"label\":\"GND\",\"row\":19,\"col\":0},{\"color\":\"#00fd00\",\"pin\":40,\"label\":\"GPIO 21\",\"row\":19,\"col\":1}],\"ledPanelBackgroundColor\":\"#008a00\"},\"title\":\"Raspberry Pi GPIO Panel\",\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"7\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.22518255793320163,\"funcBody\":\"var period = time % 1500;\\nreturn period < 500;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"11\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.7008206860666621,\"funcBody\":\"var period = time % 1500;\\nreturn period >= 500 && period < 1000;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"12\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.42600325102193426,\"funcBody\":\"var period = time % 1500;\\nreturn period >= 1000;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"13\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.48362241571415243,\"funcBody\":\"var period = time % 1500;\\nreturn period < 500;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"29\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.7217670147518815,\"funcBody\":\"var period = time % 1500;\\nreturn period >= 500 && period < 1000;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}}}" + "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"parseGpioStatusFunction\":\"return body[pin] === true;\",\"gpioStatusChangeRequest\":{\"method\":\"setGpioStatus\",\"paramsBody\":\"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"},\"requestTimeout\":500,\"switchPanelBackgroundColor\":\"#008a00\",\"gpioStatusRequest\":{\"method\":\"getGpioStatus\",\"paramsBody\":\"{}\"},\"gpioList\":[{\"pin\":7,\"label\":\"GPIO 4 (GPCLK0)\",\"row\":3,\"col\":0,\"_uniqueKey\":0},{\"pin\":11,\"label\":\"GPIO 17\",\"row\":5,\"col\":0,\"_uniqueKey\":1},{\"pin\":12,\"label\":\"GPIO 18\",\"row\":5,\"col\":1,\"_uniqueKey\":2},{\"_uniqueKey\":3,\"pin\":13,\"label\":\"GPIO 27\",\"row\":6,\"col\":0},{\"_uniqueKey\":4,\"pin\":15,\"label\":\"GPIO 22\",\"row\":7,\"col\":0},{\"_uniqueKey\":5,\"pin\":16,\"label\":\"GPIO 23\",\"row\":7,\"col\":1},{\"_uniqueKey\":6,\"pin\":18,\"label\":\"GPIO 24\",\"row\":8,\"col\":1},{\"_uniqueKey\":7,\"pin\":22,\"label\":\"GPIO 25\",\"row\":10,\"col\":1},{\"_uniqueKey\":8,\"pin\":29,\"label\":\"GPIO 5\",\"row\":14,\"col\":0},{\"_uniqueKey\":9,\"pin\":31,\"label\":\"GPIO 6\",\"row\":15,\"col\":0},{\"_uniqueKey\":10,\"pin\":32,\"label\":\"GPIO 12\",\"row\":15,\"col\":1},{\"_uniqueKey\":11,\"pin\":33,\"label\":\"GPIO 13\",\"row\":16,\"col\":0},{\"_uniqueKey\":12,\"pin\":35,\"label\":\"GPIO 19\",\"row\":17,\"col\":0},{\"_uniqueKey\":13,\"pin\":36,\"label\":\"GPIO 16\",\"row\":17,\"col\":1},{\"_uniqueKey\":14,\"pin\":37,\"label\":\"GPIO 26\",\"row\":18,\"col\":0},{\"_uniqueKey\":15,\"pin\":38,\"label\":\"GPIO 20\",\"row\":18,\"col\":1},{\"_uniqueKey\":16,\"pin\":40,\"label\":\"GPIO 21\",\"row\":19,\"col\":1}]},\"title\":\"Raspberry Pi GPIO Control\"}" } } ] diff --git a/application/src/main/data/json/system/widget_bundles/input_widgets.json b/application/src/main/data/json/system/widget_bundles/input_widgets.json index 18765a8b54..097c31dac4 100644 --- a/application/src/main/data/json/system/widget_bundles/input_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/input_widgets.json @@ -6,195 +6,195 @@ }, "widgetTypes": [ { - "alias": "update_server_string_attribute", - "name": "Update server string attribute", + "alias": "markers_placement_openstreetmap", + "name": "Markers Placement - OpenStreetMap", "descriptor": { "type": "latest", - "sizeX": 7.5, - "sizeY": 3.5, + "sizeX": 8.5, + "sizeY": 6.5, "resources": [], - "templateHtml": "
\n
\n
\n
\n \n \n \n
\n
{{requiredErrorMessage}}
\n
\n
\n
\n\n
\n \n check\n {{ 'widgets.input-widgets.update-attribute' | translate }}\n \n \n close\n {{ 'widgets.input-widgets.discard-changes' | translate }}\n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\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 .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-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 .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet toast;\r\nlet utils;\r\nlet types;\r\nlet $translate;\r\n\r\nself.onInit = function() {\r\n\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get('attributeService');\r\n toast = $scope.$injector.get('toast');\r\n utils = $scope.$injector.get('utils');\r\n types = $scope.$injector.get('types');\r\n $translate = $scope.$injector.get('$translate');\r\n settings = self.ctx.settings || {};\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false;\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-attribute-required');\r\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || $translate.instant('widgets.input-widgets.value');\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === types.datasourceType.entity) {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length) {\r\n if (datasource.dataKeys[0].type != types.dataKeyType.attribute) {\r\n $scope.isValidParameter = false;\r\n } else {\r\n $scope.currentKey = datasource.dataKeys[0].name;\r\n $scope.dataKeyType = datasource.dataKeys[0].type;\r\n $scope.dataKeyDetected = true;\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function () {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entityType,\r\n datasource.entityId,\r\n types.attributesScope.server.value,\r\n [\r\n {\r\n key: $scope.currentKey,\r\n value: $scope.currentValue\r\n }\r\n ]\r\n ).then(\r\n function success() {\r\n $scope.originalValue = $scope.currentValue;\r\n if (settings.showResultMessage) {\r\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n }\r\n );\r\n }\r\n };\r\n\r\n $scope.changeFocus = function () {\r\n if ($scope.currentValue === $scope.originalValue) {\r\n $scope.isFocused = false;\r\n }\r\n }\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\r\n $scope.$digest();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n}\r\n\r\nself.onResize = function() {\r\n\r\n}\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 1\r\n }\r\n}\r\n\r\nself.onDestroy = function() {\r\n\r\n}\r\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\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 \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxLength\": {\n \"title\": \"Max length\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minLength\": {\n \"title\": \"Min length\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxLength\",\n \"minLength\"\n ]\n}", + "templateHtml": "", + "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('openstreet-map', false, self.ctx, null, true);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('openstreet-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('openstreet-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", + "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update server string attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.7867521952070078,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.7040053227577256,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}

Delete\",\"markerImageSize\":34,\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"color\":\"#fe7569\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"showTooltip\":true,\"autocloseTooltip\":true,\"defaultCenterPosition\":\"0,0\",\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"showTooltipAction\":\"click\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"zoomOnClick\":true,\"showCoverageOnHover\":true,\"animate\":true,\"maxClusterRadius\":80,\"removeOutsideVisibleBounds\":true,\"defaultZoomLevel\":5,\"provider\":\"openstreet-map\",\"draggableMarker\":true,\"editablePolygon\":true,\"mapPageSize\":16384,\"showPolygon\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${coordinates|ts:7}

Delete\"},\"title\":\"Markers Placement - OpenStreetMap\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id;\\n });\\n\\nwidgetContext.map.setMarkerLocation(entityDatasource[0], null, null).subscribe();\",\"id\":\"54c293c4-9ca6-e34f-dc6a-0271944c1c66\"},{\"name\":\"delete_polygon\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.savePolygonLocation(entityDatasource[0], null).subscribe();\",\"id\":\"6beb7bed-dfd8-388d-b60c-82988ab52f06\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}" } }, { - "alias": "update_server_integer_attribute", - "name": "Update server integer attribute", + "alias": "update_multiple_attributes", + "name": "Update Multiple Attributes", "descriptor": { "type": "latest", "sizeX": 7.5, "sizeY": 3, "resources": [], - "templateHtml": "
\n
\n\n
\n
\n \n \n \n
\n
{{requiredErrorMessage}}
\n
\n
\n
\n\n
\n \n check\n {{ 'widgets.input-widgets.update-attribute' | translate }}\n \n \n close\n {{ 'widgets.input-widgets.discard-changes' | translate }}\n \n
\n
\n\n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\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 .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-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 .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet $translate;\n\nself.onInit = function() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n $translate = $scope.$injector.get('$translate');\n settings = angular.copy(self.ctx.settings) || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || $translate.instant('widgets.input-widgets.value');\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === types.datasourceType.entity) {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != types.dataKeyType.attribute) {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entityType,\n datasource.entityId,\n types.attributesScope.server.value,\n [\n {\n key: $scope.currentKey,\n value: $scope.currentValue\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.currentValue === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n correctValue($scope.currentValue);\n $scope.$digest();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n $scope.currentValue = 0;\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onDestroy = function() {\n\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\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 \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValue\": {\n \"title\": \"Max value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minValue\": {\n \"title\": \"Min value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxValue\",\n \"minValue\"\n ]\n}", - "dataKeySettingsSchema": "{}\n", - "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\":\"Update server integer attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "templateHtml": "\n", + "templateCss": ".tb-toast {\n min-width: 0;\n font-size: 14px !important;\n}", + "controllerScript": "self.onInit = function() {\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n self.ctx.$scope.multipleInputWidget.onDataUpdated();\r\n}\r\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"MultipleInput\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showActionButtons\":{\n \"title\":\"Show action buttons\",\n \"type\":\"boolean\",\n \"default\": true\n },\n \"updateAllValues\": {\n \"title\":\"Update all values, not only modified\",\n \"type\":\"boolean\",\n \"default\": false\n },\n \"saveButtonLabel\": {\n \"title\": \"'SAVE' button label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"resetButtonLabel\": {\n \"title\": \"'UNDO' button label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\": true\n },\n \"showGroupTitle\": {\n \"title\":\"Show title for group of fields, related to different entities\",\n \"type\":\"boolean\",\n \"default\": false\n },\n \"groupTitle\": {\n \"title\": \"Group title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"fieldsAlignment\": {\n \"title\": \"Fields alignment\",\n \"type\": \"string\",\n \"default\": \"row\"\n },\n \"fieldsInRow\": {\n \"title\": \"Number of fields in the row\",\n \"type\": \"number\",\n \"default\": \"2\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showActionButtons\",\n {\n \"key\": \"updateAllValues\",\n \"condition\": \"model.showActionButtons === true\"\n },\n {\n \"key\": \"saveButtonLabel\",\n \"condition\": \"model.showActionButtons === true\"\n },\n {\n \"key\": \"resetButtonLabel\",\n \"condition\": \"model.showActionButtons === true\"\n },\n \"showResultMessage\",\n \"showGroupTitle\",\n \"groupTitle\",\n {\n \"key\": \"fieldsAlignment\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"row\",\n \"label\": \"Row (default)\"\n },\n {\n \"value\": \"column\",\n \"label\": \"Column\"\n }\n ]\n },\n {\n \"key\": \"fieldsInRow\",\n \"condition\": \"model.fieldsAlignment === 'row'\"\n }\n ]\n}", + "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"dataKeyType\": {\n \"title\": \"Datakey type\",\n \"type\": \"string\",\n \"default\": \"server\"\n },\n \"dataKeyValueType\": {\n \"title\": \"Datakey value type\",\n \"type\": \"string\",\n \"default\": \"string\"\n },\n \"step\": {\n \"title\": \"Step interval between values\",\n \"type\": \"number\",\n \"default\": \"1\"\n },\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\"\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\"\n },\n \"required\": {\n \"title\": \"Value is required\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"minValueErrorMessage\": {\n \"title\": \"'Min Value' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValueErrorMessage\": {\n \"title\": \"'Max Value' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"invalidDateErrorMessage\": {\n \"title\": \"'Invalid Date' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"selectOptionsType\": {\n \"title\": \"Select options type\",\n \"type\": \"string\",\n \"default\": \"valueWithLabel\"\n },\n \"selectOptionsList\": {\n \"title\": \"Select options list\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"value\": {\n \"title\": \"Value\",\n \"type\": \"string\"\n },\n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n }\n },\n \"required\": [\"value\", \"label\"]\n }\n },\n \"isEditable\": {\n \"title\": \"Ability to edit attribute\",\n \"type\": \"string\",\n \"default\": \"editable\"\n },\n \"disabledOnDataKey\": {\n \"title\": \"Disable on false value of another datakey (specify datakey name)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"dataKeyHidden\": {\n \"title\": \"Hide input field\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"icon\": {\n \"title\": \"Icon to show before input cell\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"dataKeyType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"server\",\n \"label\": \"Server attribute (default)\"\n },\n {\n \"value\": \"shared\",\n \"label\": \"Shared attribute\"\n },\n {\n \"value\": \"timeseries\",\n \"label\": \"Timeseries\"\n }\n ]\n },\n {\n \"key\": \"dataKeyValueType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"string\",\n \"label\": \"String\"\n },\n {\n \"value\": \"double\",\n \"label\": \"Double\"\n },\n {\n \"value\": \"integer\",\n \"label\": \"Integer\"\n },\n {\n \"value\": \"booleanCheckbox\",\n \"label\": \"Boolean (Checkbox)\"\n },\n {\n \"value\": \"booleanSwitch\",\n \"label\": \"Boolean (Switch)\"\n },\n {\n \"value\": \"dateTime\",\n \"label\": \"Date & Time\"\n },\n {\n \"value\": \"date\",\n \"label\": \"Date\"\n },\n {\n \"value\": \"time\",\n \"label\": \"Time\"\n },\n {\n \"value\": \"selectOption\",\n \"label\": \"Selectable option\"\n }\n ]\n },\n {\n \"key\": \"selectOptionsType\",\n \"condition\": \"model.dataKeyValueType === 'selectOption'\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"valueWithLabel\",\n \"label\": \"Values with labels\"\n },\n {\n \"value\": \"rawValue\",\n \"label\": \"Raw values\"\n }\n ]\n },\n {\n \"key\": \"selectOptionsList\",\n \"type\": \"array\",\n \"condition\": \"model.dataKeyValueType === 'selectOption'\",\n \"items\": [\n \"selectOptionsList[].value\",\n {\n \"key\": \"selectOptionsList[].label\",\n \"condition\": \"model.selectOptionsType === 'valueWithLabel'\"\n }\n ]\n },\n {\n \"key\": \"step\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n {\n \"key\": \"minValue\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n {\n \"key\": \"maxValue\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n \"required\",\n {\n \"key\": \"requiredErrorMessage\",\n \"condition\": \"model.required === true\"\n },\n {\n \"key\": \"invalidDateErrorMessage\",\n \"condition\": \"model.dataKeyValueType === 'dateTime' || model.dataKeyValueType === 'date' || model.dataKeyValueType === 'time'\"\n },\n {\n \"key\": \"minValueErrorMessage\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n {\n \"key\": \"maxValueErrorMessage\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n {\n \"key\": \"isEditable\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"editable\",\n \"label\": \"Editable (default)\"\n },\n {\n \"value\": \"disabled\",\n \"label\": \"Disabled\"\n },\n {\n \"value\": \"readonly\",\n \"label\": \"Read-only\"\n }\n ]\n },\n \"disabledOnDataKey\",\n \"dataKeyHidden\",\n\t\t{\n \t\t\"key\": \"icon\",\n\t\t\t\"type\": \"icon\"\n\t\t}\n ]\n}\n", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update Multiple Attributes\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { - "alias": "update_server_double_attribute", - "name": "Update server double attribute", + "alias": "device_claiming_widget", + "name": "Device claiming widget", "descriptor": { - "type": "latest", + "type": "static", "sizeX": 7.5, - "sizeY": 3, + "sizeY": 4.5, "resources": [], - "templateHtml": "
\n
\n\n
\n
\n \n \n \n
\n
{{requiredErrorMessage}}
\n
\n
\n
\n\n
\n \n check\n {{ 'widgets.input-widgets.update-attribute' | translate }}\n \n \n close\n {{ 'widgets.input-widgets.discard-changes' | translate }}\n \n
\n
\n\n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\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 .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-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 .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet $translate;\n\nself.onInit = function() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n $translate = $scope.$injector.get('$translate');\n settings = angular.copy(self.ctx.settings) || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || $translate.instant('widgets.input-widgets.value');\n \n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === types.datasourceType.entity) {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != types.dataKeyType.attribute) {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entityType,\n datasource.entityId,\n types.attributesScope.server.value,\n [\n {\n key: $scope.currentKey,\n value: $scope.currentValue\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.currentValue === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n correctValue($scope.currentValue);\n $scope.$digest();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n $scope.currentValue = 0;\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onDestroy = function() {\n\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\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 \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValue\": {\n \"title\": \"Max value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minValue\": {\n \"title\": \"Min value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxValue\",\n \"minValue\"\n ]\n}", + "templateHtml": "
\n
\n \n {{deviceLabel}}\n \n \n {{requiredErrorDevice}}\n \n \n \n {{secretKeyLabel}}\n \n \n {{requiredErrorSecretKey}}\n \n \n
\n
\n \n
\n
\n", + "templateCss": ".claim-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n", + "controllerScript": "let $scope;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n}\n\nfunction init() {\n $scope = self.ctx.$scope;\n let $injector = $scope.$injector;\n let utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n let $translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n let deviceService = $scope.$injector.get(self.ctx.servicesMap.get('deviceService'));\n let settings = self.ctx.settings || {};\n \n $scope.toastTargetId = 'device-claiming-widget' + utils.guid();\n $scope.secretKeyField = settings.deviceSecret;\n $scope.showLabel = settings.showLabel;\n\n let titleTemplate = \"\";\n let successfulClaim = utils.customTranslation(settings.successfulClaimDevice, settings.successfulClaimDevice) || $translate.instant('widgets.input-widgets.claim-successful');\n let failedClaimDevice = utils.customTranslation(settings.failedClaimDevice, settings.failedClaimDevice) || $translate.instant('widgets.input-widgets.claim-failed');\n let deviceNotFound = utils.customTranslation(settings.deviceNotFound, settings.deviceNotFound) || $translate.instant('widgets.input-widgets.claim-not-found');\n \n if (settings.widgetTitle && settings.widgetTitle.length) {\n titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n titleTemplate = self.ctx.widgetConfig.title;\n }\n self.ctx.widgetTitle = titleTemplate;\n \n $scope.deviceLabel = utils.customTranslation(settings.deviceLabel, settings.deviceLabel) || $translate.instant('widgets.input-widgets.device-name');\n $scope.requiredErrorDevice= utils.customTranslation(settings.requiredErrorDevice, settings.requiredErrorDevice) || $translate.instant('widgets.input-widgets.device-name-required');\n \n $scope.secretKeyLabel = utils.customTranslation(settings.secretKeyLabel, settings.secretKeyLabel) || $translate.instant('widgets.input-widgets.secret-key');\n $scope.requiredErrorSecretKey= utils.customTranslation(settings.requiredErrorSecretKey, settings.requiredErrorSecretKey) || $translate.instant('widgets.input-widgets.secret-key-required');\n \n $scope.labelClaimButon = utils.customTranslation(settings.labelClaimButon, settings.labelClaimButon) || $translate.instant('widgets.input-widgets.claim-device');\n \n $scope.claimDeviceFormGroup = $scope.fb.group(\n {deviceName: ['', [$scope.validators.required]]}\n );\n if ($scope.secretKeyField) {\n $scope.claimDeviceFormGroup.addControl('deviceSecret', $scope.fb.control('', [$scope.validators.required]));\n }\n \n $scope.claim = function(claimDeviceForm) {\n $scope.loading = true;\n\n let deviceName = $scope.claimDeviceFormGroup.get('deviceName').value;\n let claimRequest = {};\n if ($scope.secretKeyField) {\n claimRequest.secretKey = $scope.claimDeviceFormGroup.get('deviceSecret').value;\n }\n deviceService.claimDevice(deviceName, claimRequest, { ignoreErrors: true }).subscribe(\n function (data) {\n successClaim(claimDeviceForm);\n self.ctx.detectChanges();\n },\n function (error) {\n $scope.loading = false;\n if(error.status == 404) {\n $scope.showErrorToast(deviceNotFound, 'bottom', 'left', $scope.toastTargetId);\n } else {\n let errorMessage = failedClaimDevice;\n if (error.status !== 400) {\n if (error.error && error.error.message) {\n errorMessage = error.error.message;\n }\n }\n $scope.showErrorToast(errorMessage, 'bottom', 'left', $scope.toastTargetId);\n } \n self.ctx.detectChanges();\n }\n );\n }\n\n function successClaim(claimDeviceForm) {\n let deviceObj = {\n deviceName: ''\n };\n if ($scope.secretKeyField) {\n deviceObj.deviceSecret = '';\n } \n claimDeviceForm.resetForm(); \n $scope.claimDeviceFormGroup.reset(deviceObj);\n $scope.loading = false;\n $scope.showSuccessToast(successfulClaim, 2000, 'bottom', 'left', $scope.toastTargetId);\n self.ctx.updateAliases();\n }\n \n}\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"deviceSecret\": {\n \"title\": \"Show 'Secret key' input field\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"deviceLabel\": {\n \"title\": \"Label for device name\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorDevice\": {\n \"title\": \"'Device name required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"secretKeyLabel\": {\n \"title\": \"Label for secret key\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorSecretKey\": {\n \"title\": \"'Secret key required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"labelClaimButon\": {\n \"title\": \"Label for claiming button\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"successfulClaimDevice\": {\n \"title\": \"Text message of successful device claiming\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"deviceNotFound\": {\n \"title\": \"Text message when device not found\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"failedClaimDevice\": {\n \"title\": \"Text message of failed device claiming\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n [\n \"widgetTitle\",\n \"labelClaimButon\",\n \"deviceSecret\",\n \"showLabel\",\n \"deviceLabel\",\n \"secretKeyLabel\"\n ],\n [\n \"deviceNotFound\",\n \"failedClaimDevice\",\n \"successfulClaimDevice\",\n \"requiredErrorDevice\",\n \"requiredErrorSecretKey\"\n ]\n ],\n \"groupInfoes\": [{\n \"formIndex\": 0,\n \"GroupTitle\": \"General settings\"\n }, {\n \"formIndex\": 1,\n \"GroupTitle\": \"Message settings\"\n }]\n}", "dataKeySettingsSchema": "{}\n", - "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\":\"Update server double attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"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\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"deviceSecret\":true,\"showLabel\":true},\"title\":\"Device claiming widget\",\"dropShadow\":true,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"enableDataExport\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { - "alias": "update_server_boolean_attribute", - "name": "Update server boolean attribute", + "alias": "markers_placement_image_map", + "name": "Markers Placement - Image Map", "descriptor": { "type": "latest", - "sizeX": 7.5, - "sizeY": 3, + "sizeX": 8.5, + "sizeY": 6.5, "resources": [], - "templateHtml": "
\n
\n
\n
\n \n {{currentValue}}\n \n
\n
\n\n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-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 .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet $translate;\nlet map;\nlet mapReverse;\n\nself.onInit = function() {\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n $translate = $scope.$injector.get('$translate');\n settings = angular.copy(self.ctx.settings) || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n\n settings.trueValue = utils.customTranslation(settings.trueValue, settings.trueValue) || true;\n settings.falseValue = utils.customTranslation(settings.falseValue, settings.falseValue) || false;\n\n map = {\"true\":settings.trueValue, \"false\": settings.falseValue};\n mapReverse = {[settings.trueValue]:true, [settings.falseValue]:false};\n $scope.checkboxValue = \"false\";\n $scope.currentValue = map[$scope.checkboxValue];\n\n $scope.changed = function () {\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.updateAttribute();\n }\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === types.datasourceType.entity) {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != types.dataKeyType.attribute) {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entityType,\n datasource.entityId,\n types.attributesScope.server.value,\n [\n {\n key: $scope.currentKey,\n value: mapReverse[$scope.currentValue] || false\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n $scope.checkboxValue = ($scope.originalValue = self.ctx.data[0].data[0][1]) || false;\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.$digest();\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onResize = function() {}\nself.onDestroy = function() {}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"trueValue\": {\n \"title\": \"True value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"falseValue\": {\n \"title\": \"False value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"trueValue\",\n \"falseValue\"\n ]\n}", + "templateHtml": "", + "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('image-map', false, self.ctx, null, true);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('image-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('image-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", + "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", - "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\":\"Update server boolean attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}

Delete\",\"markerImageSize\":34,\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"color\":\"#fe7569\",\"mapImageUrl\":\"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiCiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiCiAgIHhtbG5zOmlua3NjYXBlPSJodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy9uYW1lc3BhY2VzL2lua3NjYXBlIgogICB3aWR0aD0iMTEzNC41MTgzIgogICBoZWlnaHQ9Ijc2Mi43ODI0MSIKICAgaWQ9InN2ZzIiCiAgIHZlcnNpb249IjEuMSIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMC40OC41IHIxMDA0MCIKICAgc29kaXBvZGk6ZG9jbmFtZT0id2ljaGl0YW1hcC1ub2xpYi5zdmciPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM0IiAvPgogIDxzb2RpcG9kaTpuYW1lZHZpZXcKICAgICBpZD0iYmFzZSIKICAgICBwYWdlY29sb3I9IiNmZmZmZmYiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgYm9yZGVyb3BhY2l0eT0iMS4wIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTp6b29tPSIwLjM1IgogICAgIGlua3NjYXBlOmN4PSI4OS45MDc4NTciCiAgICAgaW5rc2NhcGU6Y3k9IjQ1My43ODI0MSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0icHgiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ibGF5ZXIxIgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEzNjYiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iNzIxIgogICAgIGlua3NjYXBlOndpbmRvdy14PSItNCIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iLTQiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgICBpbmtzY2FwZTpvYmplY3QtcGF0aHM9InRydWUiCiAgICAgaW5rc2NhcGU6c25hcC1nbG9iYWw9ImZhbHNlIgogICAgIHNob3dndWlkZXM9InRydWUiCiAgICAgaW5rc2NhcGU6Z3VpZGUtYmJveD0idHJ1ZSIKICAgICBmaXQtbWFyZ2luLXRvcD0iMCIKICAgICBmaXQtbWFyZ2luLWxlZnQ9IjAiCiAgICAgZml0LW1hcmdpbi1yaWdodD0iMCIKICAgICBmaXQtbWFyZ2luLWJvdHRvbT0iMCIgLz4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE3Ij4KICAgIDxyZGY6UkRGPgogICAgICA8Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+CiAgICAgICAgPGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+CiAgICAgICAgPGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPgogICAgICAgIDxkYzp0aXRsZT48L2RjOnRpdGxlPgogICAgICA8L2NjOldvcms+CiAgICA8L3JkZjpSREY+CiAgPC9tZXRhZGF0YT4KICA8ZwogICAgIGlua3NjYXBlOmxhYmVsPSJMYXllciAxIgogICAgIGlua3NjYXBlOmdyb3VwbW9kZT0ibGF5ZXIiCiAgICAgaWQ9ImxheWVyMSIKICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjcuMDcxNDI4LC0zMDcuOTAyOTkpIj4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDM3ODciCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMzY0ZTU5O3N0cm9rZS13aWR0aDoyLjk5OTk5OTc2O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmUiCiAgICAgICBkPSJtIDkwNi4wMzMxNSw3MDYuMTMzNjcgMy40MjkyLDE3Ljc5NTUyIE0gMjguNTcxNDI4LDc2NS4wNTA2NyBjIDE1MC40MzUyMDIsNi44MzM0MiAxNDYuMzkyMzIyLC0yNi4zMzQxNSAxNjYuNDM0NTQyLC0yOS4zMjAwOSAzNi4xNDM3NSwtNS4zODQ3NiAxMTQuMjg2NzYsLTYuNTI1NCAxNDguMzI1MDgsLTguNjIzNTQgNDMuMzc4MDgsLTIuNjczODUgMTQxLjc2MjIxLC0xMS4yMzA5OSAxODguODU1NzgsLTE5LjgzNDE4IDM5LjgxMTM4LC03LjI3Mjg0IDIyMS4zNjk5MSwtMC44NjIzNSAzMTkuMDcxNDEsLTAuODYyMzUgNzAuODI3MzUsMCAxNDYuOTE4NjcsLTEuNzI0NyAyMTguMTc1ODYsLTEuNzI0NyAtMzEuNjE5NywwIDExNy44NTUyLC0yLjU4NzA3IDg2LjIzNTUsLTIuNTg3MDcgbSAtMjUuMDkwNywtNjguMTI2MDYgYyAtNTIuNzk5NiwzNC43ODQ4NCAtNjUuODk1MSw1MS43NDg2NSAtOTUuNjM5LDgxLjQ5MjU4IC0yNC45MzEzLDI0LjkzMTI3IC0xNDAuMzk2NTMsLTE5LjEzOTIgLTE3OC45Mzg3MSwzNi42NTAwNyAtMTIuMjgxNCwxNy43NzcxNSAtNDcuMDAyNTcsNDYuNTQ2NTMgLTY1LjEwNzgzLDU5LjA3MTMzIC0yMC4xMDUsMTMuOTA4MTggLTU2LjAzNjcyLDQ0Ljk1NjY0IC02Ny43Njg4NSw3My4wNzgyNyAtNC44MDE0NywxMS41MDkwMiAtMTMuMzgwNDYsMzUuOTkyOTggLTIzLjQ0OTQ5LDQ2LjA2MjAxIC0xMC40OTY5OSwxMC40OTY5OSAtMzguMzc3MzMsNi4zODU2OSAtNDQuMDIzNDUsMTcuNjQ3NjQgLTE5LjAwNTAyLDM3LjkwODEyIC0yNS40NjUzLDEwMC45MjM1MiAtNjcuNjE3ODksMTAyLjA1MTAyIG0gMTkuMjgxNTEsLTYyNC4wMTQ2NCBjIDM0LjY1OTM0LC0xLjg3MzgyIDg0LjAyNzMzLDcuMzkxMzEgMTA5LjkwMDcxLC00LjI4NTQ1IDEzLjI4MTcyLC01Ljk5NDA4IDQxLjQwNzIxLC0yLjQ2MTM1IDY2LjgyODY2LC0yLjMyMDQ2IDM1LjMyMjM4LDAuMTk1NzggNjQuMzgyNDksMC42MzQ3NyAxMDEuOTE2Nyw1LjAyMzIgMjUuMDMwMzYsMi45MjY1IDQ0LjY2MjczLDM0LjI4NzIyIDU4LjUyNjk4LDUwLjY0MzkgMTcuMDk4NzgsMjAuMTcyNjggNjIuNzYzODYsLTEuNzE0NjcgNjYuMzA1NjYsMzIuMTM0MzMgNS4xMDI3LDQ4Ljc2NTg3IC02LjMyODQsNzguNjM3MjUgNi4xNDExLDk3LjM0MTUgMTkuOTY5MiwyOS45NTM3OSA1MC40ODY0LDE3Ljg1NTc5IDQ0LjYxOTMsODMuOTcxMTkgTSA1ODkuMTAyMjcsMzA5LjcyNzE1IGMgNC42NDM0NiwyMy43MjkyMyAxNS4wNjkwNCw3Mi43NzU3NSAxOS4wNjEyOCwxMzAuNjQyODggMC44NzIwNiwxMi42NDA0OCA1LjQ0NzE4LDI0Ljk5MjUzIDQuMjIyMzEsNDUuMjc3NTcgLTIuNTE3MjEsNDEuNjg3NSAtMTUuNzE3MDYsNDMuNjc3MjcgLTE1LjA5MTIyLDYwLjM2NDg2IDEuNDMxOTUsMzguMTgyMjQgMzAuNjEzNjEsOTMuODM3MTkgMzAuNjEzNjEsMTM5LjcwMTU0IDAsMjQuMTgwOCAtMi42Njk2NCwxMTUuMzkwNDUgNy4zMzAwMSwxMzUuMzg5NzYgMC4xNTkxMSwwLjMxODIxIDEwLjA2NDc2LDM1Ljg4MzMyIDEwLjc3OTQ1LDQ5LjE1NDI0IDAuOTQzNzgsMTcuNTI0NjkgLTI0LjQ3OCwzOS40NzAwOCAtMjguMDI2NTUsNDYuNTY3MTYgLTUuNDc3NywxMC45NTUzOSAtMzYuOTczMjQsMTAuODgxOTcgLTQwLjA5OTUsMjQuMTQ1OTUgLTMuODY4ODQsMTYuNDE0NTEgLTMuODY2Myw0My43OTczNSA0LjA0NjQ3LDU5LjQ0MTI5IG0gOTcuMzM3MzQsLTY5MS4wMDk0MSBjIC01LjAxMzMyLDM1LjUxNTk1IC00My42NTkwMSwxMS4zMTY1MiAtNTguNTM4NjEsMjMuNzgxMzEgLTIxLjMzMDE5LDE3Ljg2ODUyIC02Mi40OTk2NCwzMS40MzIxMiAtNzAuMTI0MzcsMzUuMzY3MDggLTM1LjA4NzYzLDE4LjEwNzkzIC0xMTAuNDcyMTUsLTE1LjE0MTk2IC0xMjUuNjE0MSw0LjI2ODQzIC0xNS45NTA2MywyMC40NDcwMyAtMC4wNzM1LDYxLjQ2NjQ4IC05LjE0NjY2LDg0LjE0OTI0IC02LjAzNTcsMTUuMDg5MjYgLTE4Ljg3NjcsMjMuMDE3MzQgLTI3LjQzOTk3LDMyLjkyNzk4IC0xOS43NDgyOSwyMi44NTU1NSAtNjkuOTc0MjgsNjkuODI0MTkgLTg0Ljc1OTA0LDEwMC4wMDM0NiAtNy40OTc0MSwxNS4zMDQwNCAtMy4yODQyNiw0NC40MjA0MSAtMy40NzA1Myw2My4zNDI4NCAtMC4xMjc5MywxMi45OTQxNCAtMC44MTAxNSwyMy4xMDM4NSAyLjQwMzQzLDI4LjI3NjE4IDQuOTYxNTgsNy45ODU4MSAyMy43MjA1LDI4LjExMjA3IDI0LjIzODY1LDUwLjYxMTQ5IDAuMjk0MTEsMTIuNzcxNDYgMC4wMTMzLDc4LjU5MTAxIDMuMDQ4ODgsODcuNjU1NDkgMi4zMTI1Niw2LjkwNTQ2IDQuMjIwMDQsMjYuNTY0OTcgMTAuMjEzNzcsMzYuNTg2NjIgMTEuMzU0MDEsMTguOTg0MTUgNC4zODczNyw0MC4xNTY2MiAyNy44OTczLDUzLjUwNzk1IDE5LjA1MDEyLDEwLjgxODU5IDQ2Ljg3NzgxLDEyLjIxODYyIDgxLjkyNjE4LDE0LjQ2MDU0IDMzLjcwMzQ1LDIuMTU1ODkgNjEuNTEyMTcsLTEuNDMwMzUgNzYuOTIwNzcsNi4xNDExIDExLjU4NTA4LDUuNjkyNjYgOC41ODE1MSwxNy45MzM0NCAxNC4yOTU0MSwyOS4zNjEyMyA1LjY0MDQyLDExLjI4MDg1IDMxLjUwMjYzLDExLjE1NjI3IDQxLjgwNDA5LDQzLjQ1NDg3IDcuNjA1OSwyMy44NDcxIDMuMDg1OTMsNDQuMTU2OSA2LjcwNzU1LDY1Ljg4NjYiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjY2Nzc3NzY2Njc3Nzc3NzY2Nzc3Nzc3NjY3Nzc3Njc3NzY2Nzc3Nzc3Nzc3Nzc3Nzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Im0gNDMuMjc3ODgxLDUxNy45NDY3OSBjIDAsMCAyMzAuODQ4Mjg5LC0zLjYzODA1IDI1MC4wMDg2MzksLTMuNjU4NjcgNy40ODIyMiwtMC4wMDggOC42MTk1NCw1LjE1MTk0IDE0LjAyMDksMTEuNDU4NjkgMjQuNTk2MDgsMjguNzE4OTMgOTMuOTA5NjYsMTEyLjkzNTg1IDkzLjkwOTY2LDExMi45MzU4NSIKICAgICAgIGlkPSJwYXRoMzc4OSIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJtIDM1Ljk2MDU1NSw1NzcuNzA0OTQgYyAwLDAgMTY1LjUyNDU2NSwtMS42ODQ1NCAyNDguNzc5NTY1LC0xLjY4NDU0IDQuOTQ3NDksMCA3LjcyOTkzLC0yLjg4MzMgMTAuNTM3NzEsLTUuNzI5NzcgOS42NjEwNywtOS43OTQxNiAyNS42MzE5OSwtMjguNTg5OTUgMjUuNjMxOTksLTI4LjU4OTk1IgogICAgICAgaWQ9InBhdGgzNzkxIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOiMzMzMzNjY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gMzguMzk5NjYzLDY0MS43MzE1NSA0MzEuNzA1OTMsNjM3LjQ2MzExIgogICAgICAgaWQ9InBhdGgzNzk1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOiMzMzMzNjY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gMzkuMDA5NDQyLDcwNC41Mzg1OSA1MjMuMTcyNTMsNjk3LjgzMTA0IgogICAgICAgaWQ9InBhdGgzNzk3IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzAzLjk1NzYyLDY4Mi41ODY2MSAxNDYuNzk1NDIsMS44MjkzMyBjIDEwLjUzNDAzLDAuMTMxMjcgMTQuMzQzNzQsLTIuNjM3MzkgMjUuNDg3MTUsLTYuMzcyOCAxMC40MTIxMiwtMy40OTAyNyAzMS40MjQxNSwtMi42OTg5NiA0MS4zODUzOCwtMi43NzM4NSBsIDQwNS41NjA3OSwtMy4wNDg5IgogICAgICAgaWQ9InBhdGgzNzk5IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgaWQ9InBhdGgzODA0IgogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDQyNi4yMTc5NCwzMTQuODkwOTggYyAyLjA2NzU0LDkuMDUyNzMgMS44NDE3Nyw1MS43Mjc3NyA2LjUwNzk0LDc0LjgzNDY2IDEuNjc0NzUsOC4yOTMzNiA4LjY3NTA4LDE0LjA2NTk4IDEwLjA1NTQxLDE0Ljg1ODYyIDQuOTAxNDcsMi44MTQ2MyAxMC44MTQ3OSw4LjE0OTgyIDEzLjA0NTc5LDE2LjA4ODMxIDYuNzU3NzksMjQuMDQ1OTEgMC44Nzk3Miw2OC40NTIxMiAwLjg3OTcyLDExMC42ODkzIDAsNi4wOTc4MiAxLjY2MDEsMzAuMTQ2NiAtMi4xNTU4OCwzMy45NjI1OSAtMi41NDA4NSwyLjU0MDgzIC0wLjI4MTYzLDEyLjk5MDY5IC0zLjQzNjc1LDE2LjE0Mzc3IGwgLTkuODQ5NDQsOS44NDMxMSBjIC0xMC4zNjcxNSwxMC4zNjA0NyAtMTEuNTkwMTcsNi41MjYxNCAtMTcuNzM4NDgsMTguODIyNzYgLTMuNTY3NzIsNy4xMzU0MyA1LjQwMjM1LDIwLjY3MjEgNy4zNTQzMiwyNC41NzYwMiAxLjkzMjE0LDMuODY0MyAtMS44NDIxNiw0Ljc3NzczIC0xLjc5MjM1LDcuNDQ2MjYgMC4yNTI4NiwxMy41NDQ4MyAyLjI5NzUsMzczLjkyNzEyIDIuMjk3NSwzNzMuOTI3MTIiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDM2NS4yNDAyMiw1MTkuNzc2MTIgNC4xMTU5OSw1MDIuMTUxNTgiCiAgICAgICBpZD0icGF0aDM4MDYiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAxMTYuNTMxNjUsNTA0LjE4Njk5IDMuODgwNTksMzEwLjk2NDM2IgogICAgICAgaWQ9InBhdGgzODMxIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDM4ODkiCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzE3LjY3NzYsNTc2LjQ4NTM5IDEzMC4xODc0MiwxLjUyNDQ0IGMgNC41MTA3OSwzLjI0MTY5IDIwLjM0NDcxLDcuOTY4NTMgMjcuNzQ0ODYsNC4yNjg0NCAzLjE1NTQ2LC0xLjU3NzcyIDkuNDE5LC01LjM4ODE3IDE0LjAyNDg5LC0zLjk2MzU1IDQuMjY2OTgsMS4zMTk4MSA2LjAxNjg5LDMuMTE2MzIgMTAuMzY2MjEsMy4wNDg4OSAxMC4zMDQwMywtMC4xNTk3NSAyMC4yMTE3LDAuMzg3NDEgMzAuNDg4ODYsMC4zMDQ4OSAxNzcuODkwOCwtMS40MjgyNyAzNTYuNTkwMzUsLTIuMTMyNDcgNTM0Ljc3NDU2LC0zLjA0ODg4IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2Nzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDc1LjMwNTAxLDU4Mi44ODgwNSBjIC0zLjQ0NDE4LDExLjM1MDY2IC0yLjEwMzQzLDEyLjQzMzczIDMuNjU4NjUsMjEuMDM3MzEgMy43OTQ0NSw1LjY2NTY0IDUwLjg2MjYxLDEzLjAzODQ1IDQxLjQ2NDg1LDI3LjEzNTA5IC0xMC41MzY5NywxNS44MDU0NyAtMjIuODk3NDUsLTUuNDc3NzIgLTMzLjg0MjYzLC0xLjgyOTMzIC01LjQ1MjM2LDEuODE3NDUgLTcuMzQ5MDEsNS40NTYzMSAtMy42NTg2Niw5LjE0NjY1IDIuODA2ODMsMi44MDY4NCA0LjA0OCwxLjgwMzk2IDYuNTIwMzQsNS4xMDA0MSIKICAgICAgIGlkPSJwYXRoMzkxMCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDMyLjAxMDgyLDYzNi44NTMzMyBjIDguMzE4OTksMTMuMTEwMTYgMTguODQ2MjEsMTQuNjM0NjUgMzUuNjcxOTYsMTQuNjM0NjUgMi45Mzg2NSwwIDcuODY5OTgsLTAuOTMzNzEgMTAuNjcxMTEsMCAxMS4zNTkxNywzLjc4NjM5IDI3LjE5Mzk4LDEwLjI3NTc3IDM2LjIwMTkzLDIxLjEyOTQ4IDguMjgwMDIsOS45NzY2MSAxMC4yNTI3OCwyMy44ODMwOCA3LjcwMjAyLDM3LjEwNDI0IC02LjE2OTg5LDMxLjk3OTk4IC0xNi43MTQzMSw1Ni45ODg1MyAtMTkuMDQzNTUsODYuNTY5MDUgLTEuMzQ3OTgsMTcuMTE4OCA0LjUwOTU3LDIyLjUzNTIyIDExLjA3MTQzLDMzLjkyODU3IDEwLjY3MDIzLDE4LjUyNjcyIDguNzI0NTMsMTQuMTk5NTUgOC41NzE0MywzNC4yODU3MiAtMC4xMzk2MywxOC4zMTk0NCAwLDYwLjI2Mzg1IDAsODAuNzE0MjkiCiAgICAgICBpZD0icGF0aDM5MTIiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDUyOC41MDgwNiw2NTguOTU3NzYgYyAtMTAuNjgxMjMsMC45MDQ1NCAtNy4xMDgwNCwtNS42MDI1NSAtMTAuODIzNTQsLTguMDc5NTYgLTQuNzg0NTQsLTMuMTg5NjkgLTEyLjIyNzA0LC0xLjI1MTA0IC0xNi43Njg4OCwtNS43OTI4OCAtMC42NjYxMiwtMC42NjYxMiAtOC44MDk2OSwtNC4xMDg3NyAtMTAuMTc0NDcsLTIuNzQzOTkgLTguMzY0NTksOC4zNjQ1OSAtMy4wNDg4OCwyMC41NTE4OCAtMy4wNDg4OCwzMy41Mzc3NCBsIDMuMDIyLDMzOS42OTc0MyIKICAgICAgIGlkPSJwYXRoMzkxNCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA1MTcuOTg5NDEsNjUxLjAzMDY1IGMgLTAuMjIxNzEsLTIuNzAxODQgMS45MDM0NiwtNS41NjIxMyAzLjM1Mzc3LC03LjAxMjQ1IDEuNzk5NDMsLTEuNzk5NDIgNi45MjI5NCwxLjAwNDE5IDguODQxNzgsLTAuOTE0NjYgMC4yODc2NSwtMC4yODc2NiAwLjg0MzI5LC0xMS4xNjQxIDAuMjI4NjYsLTEzLjU2NzUzIC0yLjA2NDgzLC04LjA3NDE2IC0yLjA1ODAxLC0yOC42NTY1OCAtMi4wNTgwMSwtMzguNzIwODYgbCAwLC03My4xNzMyNiIKICAgICAgIGlkPSJwYXRoMzkxNiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNTI4LjY2MDUsNjc1LjQyMTczIC0wLjQ1NzMzLC0zMS41NTU5NiIKICAgICAgIGlkPSJwYXRoMzk3NCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDc2Ni4zMTYyNSw1NzkuNjQ0MzEgMC40MzExOCwxMy43OTc2OCBjIDMuMTM2NDMsNC42NjkxNSAzLjAxODI0LDkuNjAwNjggMy4wMTgyNCwxNi4zODQ3NSBsIDAsMTU3LjM3OTgxIgogICAgICAgaWQ9InBhdGgzOTgyIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMTEyMi45MDAxLDc2NS45MTMwMyBjIC0yMDIuMzA2NjksNC42OTA1IC00MDMuNzQ0MDUsLTEuMTEzODEgLTYwNS45NTQ1NCwzLjM1MzkgLTEwLjg2MzYyLDAuMjQwMDIgLTMuMzYxNDcsLTguNTg2MyAtMjguNTM2OCwtOC41ODYzIgogICAgICAgaWQ9InBhdGgzOTg0IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA4NjAuMDA4MDUsNzM3LjA2NjUxIGMgMCwwIC05Ny40NDc1LDAuODU4MDYgLTE0Ny41Njg5MiwwLjg1ODA2IC01LjI2ODYxLDAgLTQuNTE1NDYsLTguMzI5ODYgLTcuMzAwODksLTguMzI5ODYgLTMuOTc0MzUsMCAtOC42MjkyNSwwLjAyMDEgLTEwLjUwOTQ4LDAuMDM1OSAtMi4zMzQ3NywwLjAxOTcgLTEuODEwOTQsOC4zNjU5NyAtNC4xNDU4LDguMzY2OTIgLTQ2LjE2ODk5LDAuMDE4OCAtMTY3LjQwNzY3LC0xLjMwNzk5IC0xNzUuMDUyNjMsLTEuMzA3OTkgLTQuNDI5NTUsMCAtOC41NzYyNywtNi40Mzk3MiAtMTMuMTMxOTgsLTYuNDM5NzIgLTEuMzYxMTUsMCAtNi4yMzg3MywwIC0xNC4zOTQ2NywwIgogICAgICAgaWQ9InBhdGgzOTg2IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJNIDY3NS4wMDcwMyw4MzEuMTc0MDIgNjc0LjM5NzI1LDMwOS40MDI5OSIKICAgICAgIGlkPSJwYXRoMzk4OCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDc5OS40MDE1NywzMTMuMDYxNjUgMS4yMTk1NSw0OTUuODY2NTMiCiAgICAgICBpZD0icGF0aDM5OTAiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA3MzYuNTk0NTIsMzEyLjQ1MTg4IC0xLjIxOTU1LDcxNi40ODgyMiIKICAgICAgIGlkPSJwYXRoMzk5MiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDUzMC4wMzA5NCw2NDMuNDU4NTkgMzkyLjM3MTU5LC0zLjAxODI1IgogICAgICAgaWQ9InBhdGg0MDQ4IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gODU5LjQ1MDYsMzE0LjkwMTI4IDEuMjkzNTQsNTA3Ljk4MDU4IgogICAgICAgaWQ9InBhdGg0MDUwIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjAuOTk5OTk5OTRweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gOTIxLjU0MDE3LDMxMC41ODk0OSAxLjcyNDcxLDUzMS43NTIyNyIKICAgICAgIGlkPSJwYXRoNDA1MiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDczNi4yODk2Myw0NTMuMzEwNCAxODUuNjc3MTUsLTAuMzA0ODkiCiAgICAgICBpZD0icGF0aDQxODciCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAxMDYwLjgxMDUsNTE0Ljk2NzY3IGMgMCwwIC0zNjMuMjgxMjYsLTUuNjI2MTggLTU0NC42NTA0MiwyLjUyMTc4IC00LjE3Nzc2LDAuMTg3NjkgLTEyLjUwMDQ0LDEuMDY3MTEgLTEyLjUwMDQ0LDEuMDY3MTEgLTEuNTcwOTUsMC4xMzQxIC0yLjAwMDkzLC0yLjMyNDk1IC0yLjU5MTU1LC0zLjUwNjIzIC0wLjA5NjcsLTAuMTkzNDMgLTcuMDYwODEsLTEuOTMzNCAtNy42MjIyMSwtMS4zNzE5OSAtMi44OTMxNCwyLjg5MzE0IC03LjYzMTY3LDQuMjQ4NjkgLTEyLjE5NTU1LDQuMTE2IEwgMzY5LjIwMTcsNTE0LjUzNjUiCiAgICAgICBpZD0icGF0aDQyNjEiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAzOTkuODE1MzEsNDc5LjYxMTEyIDExLjY0MTgsNS42MDUzIGMgMi45ODQxMiwxLjQzNjc5IDYuNTI4NzgsLTAuNDc3MTIgOS45MTcwOCwtMC40MzExOCBsIDEyNy4xOTczOSwxLjcyNDcxIgogICAgICAgaWQ9InBhdGg0MjYzIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gNTE5LjI1MTUxLDUxNy4xMjM1NyA1MTguODIwMzIsMzA4LjQzMzYyIgogICAgICAgaWQ9InBhdGg0MjY1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDMyLjkyNTQ5LDM4OS43MTQ5OCBjIDExLjA0NDk2LDAgMzUuNTMzMDcsMC42MTkyNyA0Mi41Nzk3OCwtMS4wMDM5NyA4LjQwNTIyLC0xLjkzNjE4IDcuMDY2LC02Ljk1Mzc4IDE0LjE5NzEyLC02Ljk1Mzc4IDcuODA5NSwwIDYuNTQyOTEsOC4wNjIzNyAyMC4xNDE3LDguMDYyMzcgMTMuOTkwNjgsMCA0NC45NzY4OSwwLjM3ODg2IDYzLjkzOTkyLDAuMzc4ODYgMTIuMDgzOTUsMCA4Mi4wMDI2NiwwLjMwNDg5IDkzLjYwMDgxLDAuMzA0ODkgOC43NjA0NywwIDEzLjE1OTcsLTIuMjg4MjcgMjEuMzQyMTksLTcuMDEyNDMgNy4xOTUxNSwtNC4xNTQxMyAyLjA1NDU5LC05LjQ5MTM3IDIwLjQyNzU0LC04Ljg0MTc3IDIzLjE0NTQsMC44MTgzMyAxMi42NDMzNCwxNC4wMjQ4NyAzMi4zMTgxOSwxNC4wMjQ4NyAyNS4zNTk1NCwwIDEzMC45OTkwMiwwIDE1MC45MTk4NSwwIDE0LjMzMjQ0LDAgLTQuMTE5MTEsLTEzLjExMDIxIDI5LjI2OTMsLTEzLjQxNTEiCiAgICAgICBpZD0icGF0aDQyNjkiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc3NzYyIgLz4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjU4OC42Nzk1NyIKICAgICAgIHk9IjczNS44MDQ2MyIKICAgICAgIGlkPSJ0ZXh0NDMxMCIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMiIKICAgICAgICAgeD0iNTg4LjY3OTU3IgogICAgICAgICB5PSI3MzUuODA0NjMiPkxpbmNvbG48L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjY4Ni4zOTg1IgogICAgICAgeT0iNzY1LjYyODQyIgogICAgICAgaWQ9InRleHQ0MzEwLTciCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNiIKICAgICAgICAgeD0iNjg2LjM5ODUiCiAgICAgICAgIHk9Ijc2NS42Mjg0MiI+SGFycnk8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjcwOS44NzE4MyIKICAgICAgIHk9Ii04MDIuMzc3MzgiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMi02LTgiCiAgICAgICAgIHg9IjcwOS44NzE4MyIKICAgICAgICAgeT0iLTgwMi4zNzczOCI+V29vZGxhd248L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjU2Mi4xMTkyNiIKICAgICAgIHk9Ii03NzEuOTY4MTQiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xLTkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtOC0yIgogICAgICAgICB4PSI1NjIuMTE5MjYiCiAgICAgICAgIHk9Ii03NzEuOTY4MTQiPkVkZ2Vtb29yPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTguMzA0ODciCiAgICAgICB5PSItNzM4LjM2NjQ2IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTciCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtOC0yLTkiCiAgICAgICAgIHg9IjU5OC4zMDQ4NyIKICAgICAgICAgeT0iLTczOC4zNjY0NiI+T2xpdmVyPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTIuMTIyODYiCiAgICAgICB5PSItNjc3LjIwMzk4IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTctNSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNi04LTItOS00IgogICAgICAgICB4PSI1OTIuMTIyODYiCiAgICAgICAgIHk9Ii02NzcuMjAzOTgiPkhpbGxzaWRlPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTcuMzI3MDkiCiAgICAgICB5PSItODYyLjYxNDA3IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTctNS0zIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMi02LTgtMi05LTQtMSIKICAgICAgICAgeD0iNTk3LjMyNzA5IgogICAgICAgICB5PSItODYyLjYxNDA3Ij5Sb2NrPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1ODcuMzcwMTgiCiAgICAgICB5PSItOTI2LjEzNjYiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xLTktNy01LTMtMiIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNi04LTItOS00LTEtMyIKICAgICAgICAgeD0iNTg3LjM3MDE4IgogICAgICAgICB5PSItOTI2LjEzNjYiPldlYmI8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9Ijg3MS4xNjEwMSIKICAgICAgIHk9IjYzNy41NzUyIgogICAgICAgaWQ9InRleHQ0NDY1IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NDY3IgogICAgICAgICB4PSI4NzEuMTYxMDEiCiAgICAgICAgIHk9IjYzNy41NzUyIj5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI4NzMuODMyMjgiCiAgICAgICB5PSI1NzcuMDMyNDciCiAgICAgICBpZD0idGV4dDQ0NjUtMyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDQ2Ny00IgogICAgICAgICB4PSI4NzMuODMyMjgiCiAgICAgICAgIHk9IjU3Ny4wMzI0NyI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgaWQ9InRleHQ0NDkwIgogICAgICAgeT0iNTEwLjI2MTgxIgogICAgICAgeD0iODc1Ljk2NjQ5IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbgogICAgICAgICB5PSI1MTAuMjYxODEiCiAgICAgICAgIHg9Ijg3NS45NjY0OSIKICAgICAgICAgaWQ9InRzcGFuNDQ5MiIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+MjFzdDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iODgxLjMxNjU5IgogICAgICAgeT0iNDUwLjE5ODc2IgogICAgICAgaWQ9InRleHQ0NDk0IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NDk2IgogICAgICAgICB4PSI4ODEuMzE2NTkiCiAgICAgICAgIHk9IjQ1MC4xOTg3NiI+Mjl0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNjE1Ljc5MjQ4IgogICAgICAgeT0iMzg3Ljc0NzE2IgogICAgICAgaWQ9InRleHQ0NDY1LTMtMSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDQ2Ny00LTEiCiAgICAgICAgIHg9IjYxNS43OTI0OCIKICAgICAgICAgeT0iMzg3Ljc0NzE2Ij4zN3RoPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ1MTkiCiAgICAgICB5PSI0ODEuNjUyODYiCiAgICAgICB4PSI0ODQuNjkwMzciCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9IjQ4MS42NTI4NiIKICAgICAgICAgeD0iNDg0LjY5MDM3IgogICAgICAgICBpZD0idHNwYW40NTIxIgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIj4yNXRoPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1NjMuMDQ2NzUiCiAgICAgICB5PSI1MTMuMzYxMzMiCiAgICAgICBpZD0idGV4dDQ1MjMiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1MjUiCiAgICAgICAgIHg9IjU2My4wNDY3NSIKICAgICAgICAgeT0iNTEzLjM2MTMzIj4yMXN0PC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ1MjciCiAgICAgICB5PSI1NzcuODk0ODQiCiAgICAgICB4PSI1NjUuOTcxNSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNTc3Ljg5NDg0IgogICAgICAgICB4PSI1NjUuOTcxNSIKICAgICAgICAgaWQ9InRzcGFuNDUyOSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDUzMSIKICAgICAgIHk9Ii00NjAuNzMzMTIiCiAgICAgICB4PSI0MzMuNTgwNzUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii00NjAuNzMzMTIiCiAgICAgICAgIHg9IjQzMy41ODA3NSIKICAgICAgICAgaWQ9InRzcGFuNDUzMyIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+QW1pZG9uPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI0MDUuNTMwOTgiCiAgICAgICB5PSItNTIzLjU0MDE2IgogICAgICAgaWQ9InRleHQ0NTM1IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDUzNyIKICAgICAgICAgeD0iNDA1LjUzMDk4IgogICAgICAgICB5PSItNTIzLjU0MDE2Ij5BcmthbnNhczwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDUzOSIKICAgICAgIHk9Ii0zNzIuNTg1OTQiCiAgICAgICB4PSI3NDUuNDg0NjIiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii0zNzIuNTg1OTQiCiAgICAgICAgIHg9Ijc0NS40ODQ2MiIKICAgICAgICAgaWQ9InRzcGFuNDU0MSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+V2VzdDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNTk2LjcyODMzIgogICAgICAgeT0iLTUzMS4yNTkyOCIKICAgICAgIGlkPSJ0ZXh0NDU0MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NDUiCiAgICAgICAgIHg9IjU5Ni43MjgzMyIKICAgICAgICAgeT0iLTUzMS4yNTkyOCI+V2FjbzwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU1NSIKICAgICAgIHk9Ii0xMjIuNTAyOTUiCiAgICAgICB4PSI1OTUuNDM0ODEiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii0xMjIuNTAyOTUiCiAgICAgICAgIHg9IjU5NS40MzQ4MSIKICAgICAgICAgaWQ9InRzcGFuNDU1NyIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+TWF6aWU8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjY5NS43NzI5NSIKICAgICAgIHk9IjE2Mi4wNjg3NyIKICAgICAgIGlkPSJ0ZXh0NDU1OSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMC43MDcxMDY3OCwwLjcwNzEwNjc4LC0wLjcwNzEwNjc4LDAuNzA3MTA2NzgsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NjEiCiAgICAgICAgIHg9IjY5NS43NzI5NSIKICAgICAgICAgeT0iMTYyLjA2ODc3Ij5ab288L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjI0MC41ODk5NyIKICAgICAgIHk9IjU3NC40NDU0MyIKICAgICAgIGlkPSJ0ZXh0NDU2MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDU2NSIKICAgICAgICAgeD0iMjQwLjU4OTk3IgogICAgICAgICB5PSI1NzQuNDQ1NDMiPjEzdGg8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU2NyIKICAgICAgIHk9IjUxMS42MzY2MyIKICAgICAgIHg9IjIwNi4wMzE3NSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNTExLjYzNjYzIgogICAgICAgICB4PSIyMDYuMDMxNzUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NjkiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPjIxc3Q8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjYyMC40NDMxMiIKICAgICAgIHk9Ii01MDYuNjgyMTkiCiAgICAgICBpZD0idGV4dDQ1NzEiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NTczIgogICAgICAgICB4PSI2MjAuNDQzMTIiCiAgICAgICAgIHk9Ii01MDYuNjgyMTkiPk5pbXM8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU4MyIKICAgICAgIHk9IjY5OC44NDAwOSIKICAgICAgIHg9IjM3MC4yMTY4NiIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNjk4Ljg0MDA5IgogICAgICAgICB4PSIzNzAuMjE2ODYiCiAgICAgICAgIGlkPSJ0c3BhbjQ1ODUiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1hcGxlPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSIzODQuMDg0MiIKICAgICAgIHk9IjY4MC44NTEzOCIKICAgICAgIGlkPSJ0ZXh0NDU5OSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDYwMSIKICAgICAgICAgeD0iMzg0LjA4NDIiCiAgICAgICAgIHk9IjY4MC44NTEzOCI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAzNjcuOTA4MTcsMTAwOS45NTk2IDI2My4wMTgzMywwIgogICAgICAgaWQ9InBhdGg0NjA1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDx0ZXh0CiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ2MDciCiAgICAgICB5PSItNDMzLjEzNzc2IgogICAgICAgeD0iNzM2LjI2NzQ2IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbgogICAgICAgICB5PSItNDMzLjEzNzc2IgogICAgICAgICB4PSI3MzYuMjY3NDYiCiAgICAgICAgIGlkPSJ0c3BhbjQ2MDkiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1lcmlkaWFuPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ5NzkiCiAgICAgICB5PSI2NDAuMjA1MjYiCiAgICAgICB4PSI1NzIuODMyMTUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9IjY0MC4yMDUyNiIKICAgICAgICAgeD0iNTcyLjgzMjE1IgogICAgICAgICBpZD0idHNwYW40OTgxIgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIj5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1NzUuMDg5NjYiCiAgICAgICB5PSI2NzAuOTAzNSIKICAgICAgIGlkPSJ0ZXh0NDk4MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDk4NSIKICAgICAgICAgeD0iNTc1LjA4OTY2IgogICAgICAgICB5PSI2NzAuOTAzNSI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNDk5LjQ4OTYyIgogICAgICAgeT0iMTAwOC42MDY5IgogICAgICAgaWQ9InRleHQ1MDQ3IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDQ5IgogICAgICAgICB4PSI0OTkuNDg5NjIiCiAgICAgICAgIHk9IjEwMDguNjA2OSI+NDd0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iMjE2LjY0NTQzIgogICAgICAgeT0iNzI1Ljk4Mjk3IgogICAgICAgaWQ9InRleHQ1MDUxIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDUzIgogICAgICAgICB4PSIyMTYuNjQ1NDMiCiAgICAgICAgIHk9IjcyNS45ODI5NyI+S2VsbG9nZzwvdHNwYW4+PC90ZXh0PgogICAgPGZsb3dSb290CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgaWQ9ImZsb3dSb290NTA1NSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6MThweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwyODcuMzYyMTgpIj48Zmxvd1JlZ2lvbgogICAgICAgICBpZD0iZmxvd1JlZ2lvbjUwNTciPjxyZWN0CiAgICAgICAgICAgaWQ9InJlY3Q1MDU5IgogICAgICAgICAgIHdpZHRoPSIzNDMuNTcxNDQiCiAgICAgICAgICAgaGVpZ2h0PSIxMDMuNTcxNDMiCiAgICAgICAgICAgeD0iMTkuMjg1NzE1IgogICAgICAgICAgIHk9IjE3LjE0Mjg1NyIKICAgICAgICAgICBzdHlsZT0iZm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIgLz48L2Zsb3dSZWdpb24+PGZsb3dQYXJhCiAgICAgICAgIGlkPSJmbG93UGFyYTUwNjEiPjwvZmxvd1BhcmE+PC9mbG93Um9vdD4gICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDYwNy03IgogICAgICAgeT0iLTUwOC4xODk3MyIKICAgICAgIHg9Ijc3NC44NzU2MSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iLTUwOC4xODk3MyIKICAgICAgICAgeD0iNzc0Ljg3NTYxIgogICAgICAgICBpZD0idHNwYW40NjA5LTciCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1jQ2xlYW48L3RzcGFuPjwvdGV4dD4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzY0LjE1OTk5LDY1OC40Mjg5MSAyOTkuNTEwMjMsLTEuMDEwMTYgYyA2LjQ5ODcyLC0wLjAyMTkgNi45NzcxOSw5LjI1NDEyIDE2LjU5NjMxLDkuMzkyNDcgMTIuMDU0MjcsMC4xNzMzOSAyOS4xMTA4MywtMC41MzU3MiA1NC4xMTQzNywtMC4zMDExIgogICAgICAgaWQ9InBhdGg1NDQwIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjg3LjM2MjE4KSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjM3My45OTMwNCIKICAgICAgIHk9Ijk0NC4zNTc1NCIKICAgICAgIGlkPSJ0ZXh0NTA0Ny05IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDQ5LTMiCiAgICAgICAgIHg9IjM3My45OTMwNCIKICAgICAgICAgeT0iOTQ0LjM1NzU0Ij5NYWNBcnRodXI8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ2MDctNy0xIgogICAgICAgeT0iLTQ5MC4yNDU5NyIKICAgICAgIHg9Ijc4MC44NDYwNyIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iLTQ5MC4yNDU5NyIKICAgICAgICAgeD0iNzgwLjg0NjA3IgogICAgICAgICBpZD0idHNwYW40NjA5LTctOSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+U2VuZWNhPC90c3Bhbj48L3RleHQ+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDM2Ny42OTU1Myw1MzcuMjEwNiAxNDEuMjgzMDMsLTEuMDEwMTUgYyA2LjQ4OTk5LC0wLjA0NjQgMTIuNzgxMTQsNy4yMzU0NSAxOS4xOTI5LDcuMzIzNiA1NS45MjM2MiwwLjc2ODkgMTU4LjY4OTk3LC0wLjE3MzMzIDIzNi41MTQwMiwtMS4wMTAxNSA3LjgzOTU2LC0wLjA4NDMgMjIuNjMxNDcsLTE5Ljg1MzU1IDMwLjMwNDU3LC0yMC40NTU1OSAyMi4yNjU4OSwtMS4zNTE4MSA0NS4xNzk0NSwtMC41MDUwNyA2Ny42ODAyMiwtMC41MDUwNyAxNi4xNDczMSwtMC42MzI0MSAzLjYxMDE2LDIwLjcwODEzIDI2Ljc2OTA0LDIwLjcwODEzIGwgMjQzLjQ0Njc5LC0xLjAxMDE2IgogICAgICAgaWQ9InBhdGg1NDk2IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjg3LjM2MjE4KSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzY2NjY2MiIC8+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI2ODUuMjA4MTMiCiAgICAgICB5PSI4MjcuNTMwODIiCiAgICAgICBpZD0idGV4dDQzMTAtNy04IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtNiIKICAgICAgICAgeD0iNjg1LjIwODEzIgogICAgICAgICB5PSI4MjcuNTMwODIiPlBhd25lZTwvdHNwYW4+PC90ZXh0PgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0iTSA1NTQuMjg1NzIsNzIxLjQyODU3IDU1MCw1NDMuMjE0MjkgNTQ3LjE0Mjg2LDEwMi41IDU0Ni43ODU3MiwyMy4yMTQyODUiCiAgICAgICBpZD0icGF0aDU1MTkiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwyODcuMzYyMTgpIiAvPgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNTI5LjYyNTMxIgogICAgICAgeT0iLTU1MC44NDc3OCIKICAgICAgIGlkPSJ0ZXh0NDU0My01IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDU0NS0wIgogICAgICAgICB4PSI1MjkuNjI1MzEiCiAgICAgICAgIHk9Ii01NTAuODQ3NzgiPkJyb2Fkd2F5PC90c3Bhbj48L3RleHQ+CiAgPC9nPgo8L3N2Zz4K\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"showTooltip\":true,\"autocloseTooltip\":true,\"showTooltipAction\":\"click\",\"defaultCenterPosition\":\"0,0\",\"provider\":\"image-map\",\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"mapProvider\":\"HERE.normalDay\",\"draggableMarker\":true,\"editablePolygon\":true,\"mapPageSize\":16384,\"showPolygon\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${coordinates|ts:7}

Delete\"},\"title\":\"Markers Placement - Image Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id;\\n });\\n\\nwidgetContext.map.setMarkerLocation(entityDatasource[0], null, null).subscribe();\",\"id\":\"c39f512a-21c6-6b06-3aa1-715262c6553d\"},{\"name\":\"delete_polygon\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.savePolygonLocation(entityDatasource[0], null).subscribe();\",\"id\":\"94bf5ffd-b526-c6c3-ae3b-ab42191217d9\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}" } }, { - "alias": "update_server_date_attribute", - "name": "Update server date attribute", + "alias": "update_integer_timeseries", + "name": "Update integer timeseries", "descriptor": { "type": "latest", "sizeX": 7.5, - "sizeY": 3.5, + "sizeY": 3, "resources": [], - "templateHtml": "
\n
\n
\n
\n \n
\n
{{requiredErrorMessage}}
\n
\n
\n\t \n\t
\n
{{requiredErrorMessage}}
\n
\n\t
\n
\n\n
\n \n check\n {{ 'widgets.input-widgets.update-attribute' | translate }}\n \n \n close\n {{ 'widgets.input-widgets.discard-changes' | translate }}\n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex-direction: column;\n flex: 1;\n}\n.grid__element.horizontal-alignment {\n flex-direction: row;\n}\n.grid__element:last-child {\n align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .action-button {\n margin: 0;\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 .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.attribute-update-form mdp-date-picker,\n.attribute-update-form mdp-time-picker {\n width: 100%;\n}\n\n.attribute-update-form mdp-date-picker md-input-container,\n.attribute-update-form mdp-time-picker md-input-container {\n margin: 18px 0 5px;\n width: 100%;\n}\n\n.attribute-update-form.small-width mdp-date-picker md-input-container,\n.attribute-update-form.small-width mdp-time-picker md-input-container {\n width: 150px;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet toast;\r\nlet utils;\r\nlet types;\r\nlet $translate;\r\n\r\nself.onInit = function() {\r\n\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get('attributeService');\r\n toast = $scope.$injector.get('toast');\r\n utils = $scope.$injector.get('utils');\r\n types = $scope.$injector.get('types');\r\n $translate = $scope.$injector.get('$translate');\r\n settings = self.ctx.settings || {};\r\n $scope.settings = settings;\r\n $scope.isHorizontal = (settings.inputFieldsAlignment === 'row') ? true : false;\r\n $scope.isValidParameter = true;\r\n $scope.entityDetected = false;\r\n $scope.dataKeyDetected = false;\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-attribute-required');\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === types.datasourceType.entity) {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length) {\r\n if (datasource.dataKeys[0].type != types.dataKeyType.attribute) {\r\n $scope.isValidParameter = false;\r\n } else {\r\n $scope.currentKey = datasource.dataKeys[0].name;\r\n $scope.dataKeyType = datasource.dataKeys[0].type;\r\n $scope.dataKeyDetected = true;\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function () {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n var currentValueInMilliseconds = $scope.currentValue.getTime();\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entityType,\r\n datasource.entityId,\r\n types.attributesScope.server.value,\r\n [\r\n {\r\n key: $scope.currentKey,\r\n value: $scope.currentValue\r\n }\r\n ]\r\n ).then(\r\n function success() {\r\n $scope.originalValue = $scope.currentValue;\r\n if (settings.showResultMessage) {\r\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n }\r\n );\r\n }\r\n };\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n $scope.currentValue = $scope.originalValue = moment(self.ctx.data[0].data[0][1]).toDate();\r\n $scope.$digest();\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n}\r\n\r\nself.onResize = function() {\r\n $scope.smallWidthContainer = (self.ctx.$container[0].offsetWidth < 320) ? true : false;\r\n $scope.changeAlignment = ($scope.isHorizontal && (self.ctx.$container[0].offsetWidth < 480)) ? true : false;\r\n}\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 1\r\n }\r\n}\r\n\r\nself.onDestroy = function() {\r\n\r\n}\r\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"UpdateDateAttributeSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showLabel\":{\n \"title\":\"Show label\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"showTimeInput\":{\n \"title\":\"Show time input field\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"inputFieldsAlignment\": {\n \"title\": \"Input fields alignment\",\n \"type\": \"string\",\n \"default\": \"column\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"showTimeInput\",\n {\n \"key\": \"inputFieldsAlignment\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"column\",\n \"label\": \"Column (default)\"\n },\n {\n \"value\": \"row\",\n \"label\": \"Row\"\n }\n ]\n },\n \"requiredErrorMessage\"\n ]\n}", + "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n\n
\n \n \n
\n
\n\n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-timeseries-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.attribute-not-allowed' | translate }}\n
\n
\n
\n
", + "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}\n", + "controllerScript": "let $scope;\nlet settings;\nlet utils;\nlet translate;\nlet http;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n http = $scope.$injector.get(self.ctx.servicesMap.get('http'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-timeseries-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n\n $scope.attributeUpdateFormGroup = $scope.fb.group(\n {currentValue: [undefined, [$scope.validators.required,\n $scope.validators.min(settings.minValue),\n $scope.validators.max(settings.maxValue),\n $scope.validators.pattern(/^-?[0-9]+$/)]]}\n );\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"timeseries\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n let observable = saveEntityTimeseries(\n datasource.entityType,\n datasource.entityId,\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n );\n if (observable) {\n observable.subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n\n function saveEntityTimeseries(entityType, entityId, telemetries) {\n var telemetriesData = {};\n for (var a = 0; a < telemetries.length; a++) {\n if (typeof telemetries[a].value !== 'undefined' && telemetries[a].value !== null) {\n telemetriesData[telemetries[a].key] = telemetries[a].value;\n }\n }\n if (Object.keys(telemetriesData).length) {\n var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\n return http.post(url, telemetriesData);\n }\n return null;\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(correctValue($scope.originalValue));\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n return 0;\n }\n return value;\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n dataKeyOptional: true\n }\n}\n\nself.onDestroy = function() {\n\n}\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\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 \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValue\": {\n \"title\": \"Max value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minValue\": {\n \"title\": \"Min value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxValue\",\n \"minValue\"\n ]\n}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update server date attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "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\":\"Update integer timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { - "alias": "update_server_image_attribute", - "name": "Update server image attribute", + "alias": "update_double_timeseries", + "name": "Update double timeseries", "descriptor": { "type": "latest", "sizeX": 7.5, - "sizeY": 3.5, + "sizeY": 3, "resources": [], - "templateHtml": "
\n
\n
\n
\n
\n
\n
\n
dashboard.no-image
\n \n
\n
\n \n {{ 'action.remove' | translate }}\n close\n \n
\n
\n \n \n
\n
\n
\n
\n\n
\n \n check\n {{ 'widgets.input-widgets.update-attribute' | translate }}\n \n \n close\n {{ 'widgets.input-widgets.discard-changes' | translate }}\n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\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 align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-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 .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-image-select-wrapper {\n width: 100%;\n}\n\n.tb-image-select-wrapper>label {\n display: none;\n}\n\n.tb-image-select-wrapper>label.show-label {\n display: block;\n}\n\n.tb-image-preview-container,\n.tb-flow-drop {\n box-sizing: border-box;\n}\n\n.tb-image-preview {\n max-width: 98px;\n max-height: 98px;\n}\n\n.tb-image-preview-container div,\n.tb-flow-drop label {\n font-size: 16px;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet $translate;\n\nself.onInit = function() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n $translate = $scope.$injector.get('$translate');\n settings = self.ctx.settings || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === types.datasourceType.entity) {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != types.dataKeyType.attribute) {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entityType,\n datasource.entityId,\n types.attributesScope.server.value,\n [\n {\n key: $scope.currentKey,\n value: $scope.currentValue\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n \n $scope.imageAdded = function ($file) {\n var reader = new FileReader();\n reader.onload = function(event) {\n $scope.$apply(function() {\n if (event.target.result && event.target.result.startsWith('data:image/')) {\n $scope.attrUpdateForm.$setDirty();\n $scope.currentValue = event.target.result;\n } \n });\n };\n reader.readAsDataURL($file.file);\n };\n \n $scope.clearImage = function () {\n $scope.attrUpdateForm.$setDirty();\n $scope.currentValue = null;\n };\n \n};\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.$digest();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onDestroy = function() {\n\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"UpdateImageAttributeSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"displayPreview\":{\n \"title\":\"Display preview\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"displayClearButton\":{\n \"title\":\"Display clear button\",\n \"type\":\"boolean\",\n \"default\":false\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"displayPreview\",\n \"displayClearButton\"\n ]\n}", + "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-timeseries-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.attribute-not-allowed' | translate }}\n
\n
\n
\n
", + "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}", + "controllerScript": "let $scope;\nlet settings;\nlet utils;\nlet translate;\nlet http;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n http = $scope.$injector.get(self.ctx.servicesMap.get('http'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-timeseries-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n\n $scope.attributeUpdateFormGroup = $scope.fb.group(\n {currentValue: [undefined, [$scope.validators.required,\n $scope.validators.min(settings.minValue),\n $scope.validators.max(settings.maxValue)]]}\n );\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"timeseries\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n let observable = saveEntityTimeseries(\n datasource.entityType,\n datasource.entityId,\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n );\n if (observable) {\n observable.subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n\n function saveEntityTimeseries(entityType, entityId, telemetries) {\n var telemetriesData = {};\n for (var a = 0; a < telemetries.length; a++) {\n if (typeof telemetries[a].value !== 'undefined' && telemetries[a].value !== null) {\n telemetriesData[telemetries[a].key] = telemetries[a].value;\n }\n }\n if (Object.keys(telemetriesData).length) {\n var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\n return http.post(url, telemetriesData);\n }\n return null;\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(correctValue($scope.originalValue));\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n return 0;\n }\n return value;\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n dataKeyOptional: true\n }\n}\n\nself.onDestroy = function() {\n\n}", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\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 \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValue\": {\n \"title\": \"Max value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minValue\": {\n \"title\": \"Min value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxValue\",\n \"minValue\"\n ]\n}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update server image attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "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\":\"Update double timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { - "alias": "update_server_location_attribute", - "name": "Update server location attribute", + "alias": "update_boolean_timeseries", + "name": "Update boolean timeseries", "descriptor": { "type": "latest", "sizeX": 7.5, "sizeY": 3, "resources": [], - "templateHtml": "
\n
\n\n
\n
\n \n \n \n
\n
{{requiredErrorMessage}}
\n
\n
\n \n \n \n
\n
{{requiredErrorMessage}}
\n
\n
\n
\n\n
\n \n my_location\n {{ 'widgets.input-widgets.get-location' | translate }}\n \n \n \n check\n {{ 'widgets.input-widgets.update-timeseries' | translate }}\n \n \n close\n {{ 'widgets.input-widgets.discard-changes' | translate }}\n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-coordinate-specified' | translate }}\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex-direction: column;\n flex: 1;\n}\n\n.grid__element.horizontal-alignment {\n flex-direction: row;\n}\n\n.grid__element:last-child {\n align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0;\n line-height: 20px;\n}\n\n.attribute-update-form .md-button.getLocation {\n margin-right: 10px;\n}\n\n.attribute-update-form .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.attribute-update-form md-input-container{\n width: 100%;\n margin: 18px 0 5px;\n}\n\n.attribute-update-form.small-width md-input-container{\n width: 150px;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet toast;\r\nlet utils;\r\nlet types;\r\nlet $translate;\r\n\r\nself.onInit = function() {\r\n console.log(self.ctx);\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get('attributeService');\r\n toast = $scope.$injector.get('toast');\r\n utils = $scope.$injector.get('utils');\r\n types = $scope.$injector.get('types');\r\n $translate = $scope.$injector.get('$translate');\r\n settings = self.ctx.settings || {};\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false;\r\n $scope.isHorizontal = (settings.inputFieldsAlignment === 'row') ? true : false;\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-coordinate-required');\r\n $scope.latLabel = utils.customTranslation(settings.latLabel, settings.latLabel) || $translate.instant('widgets.input-widgets.latitude');\r\n $scope.lngLabel = utils.customTranslation(settings.lngLabel, settings.lngLabel) || $translate.instant('widgets.input-widgets.longitude');\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === types.datasourceType.entity) {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length > 1) {\r\n $scope.dataKeyDetected = true;\r\n for (let i = 0; i < datasource.dataKeys.length; i++) {\r\n if (datasource.dataKeys[i].type != types.dataKeyType.attribute){\r\n $scope.isValidParameter = false;\r\n }\r\n if (datasource.dataKeys[i].name !== settings.latKeyName && datasource.dataKeys[i].name !== settings.lngKeyName){\r\n $scope.dataKeyDetected = false;\r\n }\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function() {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entityType,\r\n datasource.entityId,\r\n types.attributesScope.server.value,\r\n [{\r\n key: settings.latKeyName,\r\n value: $scope.currentLat\r\n },{\r\n key: settings.lngKeyName,\r\n value: $scope.currentLng\r\n }]\r\n ).then(\r\n function success() {\r\n $scope.originalLat = $scope.currentLat;\r\n $scope.originalLng = $scope.currentLng;\r\n if (settings.showResultMessage) {\r\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000,\r\n angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), \r\n angular.element(self.ctx.$container),'bottom left');\r\n }\r\n }\r\n );\r\n }\r\n };\r\n\r\n $scope.changeFocus = function() {\r\n if ($scope.currentLat === $scope.originalLat || $scope.currentLng ===$scope.originalLng) {\r\n $scope.isFocused = false;\r\n }\r\n }\r\n\r\n $scope.discardChange = function() {\r\n $scope.currentLat = $scope.originalLat;\r\n $scope.currentLng = $scope.originalLng;\r\n scope.isFocused = false;\r\n }\r\n \r\n $scope.disableButton = function () {\r\n return $scope.currentLat === $scope.originalLat && $scope.currentLng === $scope.originalLng || $scope.currentLng === null || $scope.currentLat === null ;\r\n }\r\n \r\n $scope.getCoordinate = function() {\r\n if (navigator.geolocation) {\r\n navigator.geolocation.getCurrentPosition(showPosition, function (){\r\n toast.showError($translate.instant('widgets.input-widgets.blocked-location'), \r\n angular.element(self.ctx.$container),'bottom left');\r\n }, {\r\n enableHighAccuracy: settings.enableHighAccuracy\r\n });\r\n } else {\r\n toast.showError($translate.instant('widgets.input-widgets.no-support-geolocation'), \r\n angular.element(self.ctx.$container),'bottom left');\r\n }\r\n }\r\n \r\n function showPosition(position) {\r\n $scope.currentLat = position.coords.latitude;\r\n $scope.currentLng = position.coords.longitude;\r\n }\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n for(let i = 0; i < self.typeParameters().maxDataKeys; i++){\r\n if(self.ctx.data[i].dataKey.name === self.ctx.settings.latKeyName){\r\n $scope.currentLat = $scope.originalLat = self.ctx.data[i].data[0][1];\r\n } else if(self.ctx.data[i].dataKey.name === self.ctx.settings.lngKeyName){\r\n $scope.currentLng = $scope.originalLng = self.ctx.data[i].data[0][1];\r\n }\r\n }\r\n $scope.$digest();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n}\r\n\r\nself.onResize = function() {\r\n $scope.smallWidthContainer = (self.ctx.$container[0].offsetWidth < 320) ? true : false;\r\n $scope.changeAlignment = ($scope.isHorizontal && (self.ctx.$container[0].offsetWidth < 480)) ? true : false;\r\n}\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 2\r\n }\r\n}\r\n\r\nself.onDestroy = function() {\r\n\r\n}", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"latitude\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"longitude\"\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"latLabel\": {\n \"title\": \"Label for latitude\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"lngLabel\": {\n \"title\": \"Label for longitude\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\": {\n \"title\": \"Show result message\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableHighAccuracy\": {\n \"title\": \"Use high accuracy\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showGetLocation\": {\n \"title\": \"Show button 'Get current location'\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"inputFieldsAlignment\": {\n \"title\": \"Input fields alignment\",\n \"type\": \"string\",\n \"default\": \"column\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"latKeyName\",\n \"lngKeyName\",\n \"enableHighAccuracy\",\n \"showGetLocation\",\n \"showResultMessage\",\n {\n \"key\": \"inputFieldsAlignment\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"column\",\n \"label\": \"Column (default)\"\n },\n {\n \"value\": \"row\",\n \"label\": \"Row\"\n }\n ]\n },\n \"showLabel\",\n \"latLabel\",\n \"lngLabel\",\n \"requiredErrorMessage\"\n ]\n}", + "templateHtml": "
\n
\n
\n
\n
\n \n {{currentValue}}\n \n
\n
\n\n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-timeseries-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.attribute-not-allowed' | translate }}\n
\n
\n
\n
", + "templateCss": ".attribute-update-form {\r\n overflow: hidden;\r\n height: 100%;\r\n display: flex;\r\n flex-direction: column;\r\n}\r\n\r\n.attribute-update-form__grid {\r\n display: flex;\r\n}\r\n.grid__element:first-child {\r\n flex: 1;\r\n}\r\n\r\n.grid__element {\r\n display: flex;\r\n}\r\n\r\n.attribute-update-form .mat-button.mat-icon-button {\r\n width: 32px;\r\n min-width: 32px;\r\n height: 32px;\r\n min-height: 32px;\r\n padding: 0 !important;\r\n margin: 0 !important;\r\n line-height: 20px;\r\n}\r\n\r\n.attribute-update-form .mat-icon-button mat-icon {\r\n width: 20px;\r\n min-width: 20px;\r\n height: 20px;\r\n min-height: 20px;\r\n font-size: 20px;\r\n}\r\n\r\n.tb-toast {\r\n font-size: 14px!important;\r\n}", + "controllerScript": "let settings;\nlet utils;\nlet translate;\nlet http;\nlet $scope;\nlet map;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init();\n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n http = $scope.$injector.get(self.ctx.servicesMap.get('http'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n\n settings.trueValue = utils.defaultValue(utils.customTranslation(settings.trueValue, settings.trueValue), true);\n settings.falseValue = utils.defaultValue(utils.customTranslation(settings.falseValue, settings.falseValue), false);\n\n map = {\n true: settings.trueValue,\n false: settings.falseValue\n };\n \n $scope.checkboxValue = false;\n $scope.currentValue = map[$scope.checkboxValue];\n\n $scope.attributeUpdateFormGroup = $scope.fb.group({checkboxValue: [$scope.checkboxValue]});\n\n $scope.changed = function() {\n $scope.checkboxValue = $scope.attributeUpdateFormGroup.get('checkboxValue').value;\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.updateAttribute();\n };\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"timeseries\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function() {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n let observable = saveEntityTimeseries(\n datasource.entityType,\n datasource.entityId,\n [{\n key: $scope.currentKey,\n value: $scope.checkboxValue\n }]\n );\n\n if (observable) {\n observable.subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('checkboxValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n }\n };\n\n function saveEntityTimeseries(entityType, entityId, telemetries) {\n var telemetriesData = {};\n for (var a = 0; a < telemetries.length; a++) {\n if (typeof telemetries[a].value !== 'undefined' && telemetries[a].value !== null) {\n telemetriesData[telemetries[a].key] = telemetries[a].value;\n }\n }\n if (Object.keys(telemetriesData).length) {\n var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\n return http.post(url, telemetriesData);\n }\n return null;\n }\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n $scope.checkboxValue = self.ctx.data[0].data[0][1] === 'true';\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.attributeUpdateFormGroup.get('checkboxValue').patchValue($scope.checkboxValue);\n self.ctx.detectChanges();\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {}", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"trueValue\": {\n \"title\": \"True value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"falseValue\": {\n \"title\": \"False value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"trueValue\",\n \"falseValue\"\n ]\n}", "dataKeySettingsSchema": "{}\n", - "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\":\"Update server location attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "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\":\"Update boolean timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { - "alias": "update_shared_string_attribute", - "name": "Update shared string attribute", + "alias": "update_string_timeseries", + "name": "Update string timeseries", "descriptor": { "type": "latest", "sizeX": 7.5, - "sizeY": 3.5, + "sizeY": 3, "resources": [], - "templateHtml": "
\n
\n\n
\n
\n \n \n \n
\n
{{requiredErrorMessage}}
\n
\n
\n
\n\n
\n \n check\n {{ 'widgets.input-widgets.update-attribute' | translate }}\n \n \n close\n {{ 'widgets.input-widgets.discard-changes' | translate }}\n \n
\n
\n\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\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 .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-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 .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet $translate;\n\nself.onInit = function() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n $translate = $scope.$injector.get('$translate');\n settings = angular.copy(self.ctx.settings) || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.message = $translate.instant('widgets.input-widgets.no-entity-selected');\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || $translate.instant('widgets.input-widgets.value');\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === types.datasourceType.entity) {\n if (datasource.entityType === types.entityType.device) {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n } else {\n $scope.message = $translate.instant('widgets.input-widgets.not-allowed-entity');\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != types.dataKeyType.attribute) {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entityType,\n datasource.entityId,\n types.attributesScope.shared.value,\n [\n {\n key: $scope.currentKey,\n value: $scope.currentValue\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.currentValue === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.$digest();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onDestroy = function() {\n\n}\n", + "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-timeseries-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.attribute-not-allowed' | translate }}\n
\n
\n
\n
", + "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}", + "controllerScript": "let $scope;\nlet settings;\nlet utils;\nlet translate;\nlet http;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n http = $scope.$injector.get(self.ctx.servicesMap.get('http'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-timeseries-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n\n $scope.attributeUpdateFormGroup = $scope.fb.group(\n {currentValue: [undefined, [$scope.validators.required,\n $scope.validators.minLength(settings.minLength),\n $scope.validators.maxLength(settings.maxLength)]]}\n );\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"timeseries\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n let observable = saveEntityTimeseries(\n datasource.entityType,\n datasource.entityId,\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n );\n if (observable) {\n observable.subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n\n function saveEntityTimeseries(entityType, entityId, telemetries) {\n var telemetriesData = {};\n for (var a = 0; a < telemetries.length; a++) {\n if (typeof telemetries[a].value !== 'undefined' && telemetries[a].value !== null) {\n telemetriesData[telemetries[a].key] = telemetries[a].value;\n }\n }\n if (Object.keys(telemetriesData).length) {\n var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\n return http.post(url, telemetriesData);\n }\n return null;\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue($scope.originalValue);\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\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 \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxLength\": {\n \"title\": \"Max length\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minLength\": {\n \"title\": \"Min length\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxLength\",\n \"minLength\"\n ]\n}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update shared string attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "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\":\"Update string timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { - "alias": "update_shared_integer_attribute", - "name": "Update shared integer attribute", + "alias": "update_server_image_attribute", + "name": "Update server image attribute", "descriptor": { "type": "latest", "sizeX": 7.5, - "sizeY": 3, + "sizeY": 3.5, "resources": [], - "templateHtml": "
\n
\n\n
\n
\n \n \n \n
\n
{{requiredErrorMessage}}
\n
\n
\n
\n\n
\n \n check\n {{ 'widgets.input-widgets.update-attribute' | translate }}\n \n \n close\n {{ 'widgets.input-widgets.discard-changes' | translate }}\n \n
\n
\n\n
\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\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 .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-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 .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet $translate;\n\nself.onInit = function() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n $translate = $scope.$injector.get('$translate');\n settings = angular.copy(self.ctx.settings) || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || $translate.instant('widgets.input-widgets.value');\n $scope.message = $translate.instant('widgets.input-widgets.no-entity-selected');\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === types.datasourceType.entity) {\n if (datasource.entityType === types.entityType.device) {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n } else {\n $scope.message = $translate.instant('widgets.input-widgets.not-allowed-entity');\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != types.dataKeyType.attribute) {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entityType,\n datasource.entityId,\n types.attributesScope.shared.value,\n [\n {\n key: $scope.currentKey,\n value: $scope.currentValue\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.currentValue === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n correctValue($scope.currentValue);\n $scope.$digest();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n $scope.currentValue = 0;\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onDestroy = function() {\n\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\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 \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValue\": {\n \"title\": \"Max value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minValue\": {\n \"title\": \"Min value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxValue\",\n \"minValue\"\n ]\n}", + "templateHtml": "
\n
\n
\n
\n
\n \n \n
\n \n
\n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", + "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 align-items: center;\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.tb-image-preview-container div,\n.tb-flow-drop label {\n font-size: 16px !important;\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}", + "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet utils;\r\nlet translate;\r\n\r\nself.onInit = function() {\r\n self.ctx.ngZone.run(function() {\r\n init(); \r\n self.ctx.detectChanges(true);\r\n });\r\n};\r\n\r\nfunction init() {\r\n\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\r\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\r\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\r\n $scope.toastTargetId = 'input-widget' + utils.guid();\r\n settings = utils.deepClone(self.ctx.settings) || {};\r\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\r\n settings.displayPreview = utils.defaultValue(settings.displayPreview, true);\r\n settings.displayClearButton = utils.defaultValue(settings.displayClearButton, false);\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false;\r\n $scope.entityDetected = false;\r\n \r\n $scope.attributeUpdateFormGroup = $scope.fb.group(\r\n {currentValue: [undefined, []]}\r\n );\r\n \r\n $scope.attributeUpdateFormGroup.valueChanges.subscribe( () => {\r\n self.ctx.detectChanges();\r\n });\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n \r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length) {\r\n if (datasource.dataKeys[0].type !== \"attribute\") {\r\n $scope.isValidParameter = false;\r\n } else {\r\n $scope.currentKey = datasource.dataKeys[0].name;\r\n $scope.dataKeyType = datasource.dataKeys[0].type;\r\n $scope.dataKeyDetected = true;\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n \r\n $scope.updateAttribute = function () {\r\n console.log(\"Work\");\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entity.id,\r\n 'SERVER_SCOPE',\r\n [\r\n {\r\n key: $scope.currentKey,\r\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\r\n }\r\n ]\r\n ).subscribe(\r\n function success() {\r\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\r\n if (settings.showResultMessage) {\r\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n }\r\n );\r\n }\r\n };\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n $scope.originalValue = self.ctx.data[0].data[0][1];\r\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue($scope.originalValue);\r\n self.ctx.detectChanges();\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n};\r\n\r\nself.onResize = function() {\r\n\r\n};\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 1,\r\n singleEntity: true\r\n };\r\n};\r\n\r\nself.onDestroy = function() {\r\n $scope.attributeUpdateFormGroup.valueChanges.unsubscribe();\r\n}", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"UpdateImageAttributeSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"displayPreview\":{\n \"title\":\"Display preview\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"displayClearButton\":{\n \"title\":\"Display clear button\",\n \"type\":\"boolean\",\n \"default\":false\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"displayPreview\",\n \"displayClearButton\"\n ]\n}", "dataKeySettingsSchema": "{}\n", - "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\":\"Update shared integer attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update server image attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { - "alias": "update_shared_double_attribute", - "name": "Update shared double attribute", + "alias": "update_server_date_attribute", + "name": "Update server date attribute", "descriptor": { "type": "latest", "sizeX": 7.5, - "sizeY": 3, + "sizeY": 3.5, "resources": [], - "templateHtml": "
\n
\n\n
\n
\n \n \n \n
\n
{{requiredErrorMessage}}
\n
\n
\n
\n\n
\n \n check\n {{ 'widgets.input-widgets.update-attribute' | translate }}\n \n \n close\n {{ 'widgets.input-widgets.discard-changes' | translate }}\n \n
\n
\n\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\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 .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-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 .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet $translate;\n\nself.onInit = function() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n $translate = $scope.$injector.get('$translate');\n settings = angular.copy(self.ctx.settings) || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || $translate.instant('widgets.input-widgets.value');\n $scope.message = $translate.instant('widgets.input-widgets.no-entity-selected');\n \n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === types.datasourceType.entity) {\n if (datasource.entityType === types.entityType.device) {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n } else {\n $scope.message = $translate.instant('widgets.input-widgets.not-allowed-entity');\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != types.dataKeyType.attribute) {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entityType,\n datasource.entityId,\n types.attributesScope.shared.value,\n [\n {\n key: $scope.currentKey,\n value: $scope.currentValue\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.currentValue === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n correctValue($scope.currentValue);\n $scope.$digest();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n $scope.currentValue = 0;\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onDestroy = function() {\n\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\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 \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValue\": {\n \"title\": \"Max value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minValue\": {\n \"title\": \"Min value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxValue\",\n \"minValue\"\n ]\n}", + "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", + "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}", + "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet utils;\r\nlet translate;\r\n\r\nself.onInit = function() {\r\n self.ctx.ngZone.run(function() {\r\n init(); \r\n self.ctx.detectChanges(true);\r\n });\r\n};\r\n\r\nfunction init() {\r\n\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\r\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\r\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\r\n $scope.toastTargetId = 'input-widget' + utils.guid();\r\n settings = utils.deepClone(self.ctx.settings) || {};\r\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\r\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false;\r\n $scope.entityDetected = false;\r\n\r\n $scope.datePickerType = settings.showTimeInput ? 'datetime' : 'date'; \r\n \r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\r\n $scope.labelValue = translate.instant('widgets.input-widgets.date');\r\n \r\n if (settings.showTimeInput) {\r\n $scope.labelValue += \" & \" + translate.instant('widgets.input-widgets.time');\r\n }\r\n \r\n $scope.attributeUpdateFormGroup = $scope.fb.group(\r\n {currentValue: [undefined, [$scope.validators.required]]}\r\n );\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n \r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length) {\r\n if (datasource.dataKeys[0].type !== \"attribute\") {\r\n $scope.isValidParameter = false;\r\n } else {\r\n $scope.currentKey = datasource.dataKeys[0].name;\r\n $scope.dataKeyType = datasource.dataKeys[0].type;\r\n $scope.dataKeyDetected = true;\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n \r\n $scope.updateAttribute = function () {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n var currentValueInMilliseconds = $scope.attributeUpdateFormGroup.get('currentValue').value.getTime();\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entity.id,\r\n 'SERVER_SCOPE',\r\n [\r\n {\r\n key: $scope.currentKey,\r\n value: currentValueInMilliseconds\r\n }\r\n ]\r\n ).subscribe(\r\n function success() {\r\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\r\n if (settings.showResultMessage) {\r\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n }\r\n );\r\n }\r\n };\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n $scope.originalValue = moment(self.ctx.data[0].data[0][1]).toDate();\r\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue($scope.originalValue);\r\n self.ctx.detectChanges();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n};\r\n\r\nself.onResize = function() {\r\n\r\n};\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 1,\r\n singleEntity: true\r\n };\r\n};\r\n\r\nself.onDestroy = function() {\r\n\r\n}\r\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"UpdateDateAttributeSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showLabel\":{\n \"title\":\"Show label\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"showTimeInput\":{\n \"title\":\"Show select time\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"inputFieldsAlignment\": {\n \"title\": \"Input fields alignment\",\n \"type\": \"string\",\n \"default\": \"column\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"showTimeInput\",\n \"requiredErrorMessage\"\n ]\n}", "dataKeySettingsSchema": "{}\n", - "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\":\"Update shared double attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update server date attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { - "alias": "update_shared_boolean_attribute", - "name": "Update shared boolean attribute", + "alias": "update_server_boolean_attribute", + "name": "Update server boolean attribute", "descriptor": { "type": "latest", "sizeX": 7.5, "sizeY": 3, "resources": [], - "templateHtml": "
\n
\n
\n
\n \n {{currentValue}}\n \n
\n
\n\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\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 .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-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 .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet $translate;\nlet map;\nlet mapReverse;\n\nself.onInit = function() {\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n $translate = $scope.$injector.get('$translate');\n settings = angular.copy(self.ctx.settings) || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.message = $translate.instant('widgets.input-widgets.no-entity-selected');\n\n settings.trueValue = utils.customTranslation(settings.trueValue, settings.trueValue) || true;\n settings.falseValue = utils.customTranslation(settings.falseValue, settings.falseValue) || false;\n\n map = {\"true\":settings.trueValue, \"false\": settings.falseValue};\n mapReverse = {[settings.trueValue]:true, [settings.falseValue]:false};\n $scope.checkboxValue = \"false\";\n $scope.currentValue = map[$scope.checkboxValue];\n\n $scope.changed = function () {\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.updateAttribute();\n }\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === types.datasourceType.entity) {\n if (datasource.entityType === types.entityType.device) {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n } else {\n $scope.message = $translate.instant('widgets.input-widgets.not-allowed-entity');\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != types.dataKeyType.attribute) {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entityType,\n datasource.entityId,\n types.attributesScope.shared.value,\n [\n {\n key: $scope.currentKey,\n value: mapReverse[$scope.currentValue] || false\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n}\n\nself.onDataUpdated = function() {\n try {\n $scope.checkboxValue = ($scope.originalValue = self.ctx.data[0].data[0][1]) || 'false';\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.$digest();\n\n } catch (e) {\n console.log(e);\n }\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onResize = function() {}\nself.onDestroy = function() {}\n", + "templateHtml": "
\r\n
\r\n
\r\n
\r\n
\r\n \r\n {{currentValue}}\r\n \r\n
\r\n
\r\n\r\n
\r\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\r\n
\r\n
\r\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\r\n
\r\n
\r\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\r\n
\r\n
\r\n
\r\n
", + "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}", + "controllerScript": "let settings;\nlet attributeService;\nlet utils;\nlet translate;\nlet $scope;\nlet map;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init();\n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n\n settings.trueValue = utils.defaultValue(utils.customTranslation(settings.trueValue, settings.trueValue), true);\n settings.falseValue = utils.defaultValue(utils.customTranslation(settings.falseValue, settings.falseValue), false);\n\n map = {\n true: settings.trueValue,\n false: settings.falseValue\n };\n \n $scope.checkboxValue = false;\n $scope.currentValue = map[$scope.checkboxValue];\n\n $scope.attributeUpdateFormGroup = $scope.fb.group({checkboxValue: [$scope.checkboxValue]});\n\n $scope.changed = function() {\n $scope.checkboxValue = $scope.attributeUpdateFormGroup.get('checkboxValue').value;\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.updateAttribute();\n };\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function() {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SERVER_SCOPE',\n [\n {\n key: $scope.currentKey,\n value: $scope.checkboxValue\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('checkboxValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n $scope.checkboxValue = self.ctx.data[0].data[0][1] === 'true';\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.attributeUpdateFormGroup.get('checkboxValue').patchValue($scope.checkboxValue);\n self.ctx.detectChanges();\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {}", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"trueValue\": {\n \"title\": \"True value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"falseValue\": {\n \"title\": \"False value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"trueValue\",\n \"falseValue\"\n ]\n}", "dataKeySettingsSchema": "{}\n", - "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\":{\"trueValue\":\"active\",\"falseValue\":\"inactive\"},\"title\":\"Update shared boolean attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "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\":\"Update server boolean attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { - "alias": "update_shared_date_attribute", - "name": "Update shared date attribute", + "alias": "update_server_double_attribute", + "name": "Update server double attribute", "descriptor": { "type": "latest", "sizeX": 7.5, - "sizeY": 3.5, + "sizeY": 3, "resources": [], - "templateHtml": "
\n
\n
\n
\n \n
\n
{{requiredErrorMessage}}
\n
\n
\n\t \n\t
\n
{{requiredErrorMessage}}
\n
\n\t
\n
\n\n
\n \n check\n {{ 'widgets.input-widgets.update-attribute' | translate }}\n \n \n close\n {{ 'widgets.input-widgets.discard-changes' | translate }}\n \n
\n
\n \n
\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex-direction: column;\n flex: 1;\n}\n.grid__element.horizontal-alignment {\n flex-direction: row;\n}\n.grid__element:last-child {\n align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .action-button {\n margin: 0;\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 .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.attribute-update-form mdp-date-picker,\n.attribute-update-form mdp-time-picker {\n width: 100%;\n}\n\n.attribute-update-form mdp-date-picker md-input-container,\n.attribute-update-form mdp-time-picker md-input-container {\n margin: 18px 0 5px;\n width: 100%;\n}\n\n.attribute-update-form.small-width mdp-date-picker md-input-container,\n.attribute-update-form.small-width mdp-time-picker md-input-container {\n width: 150px;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet toast;\r\nlet utils;\r\nlet types;\r\nlet $translate;\r\n\r\nself.onInit = function() {\r\n\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get('attributeService');\r\n toast = $scope.$injector.get('toast');\r\n utils = $scope.$injector.get('utils');\r\n types = $scope.$injector.get('types');\r\n $translate = $scope.$injector.get('$translate');\r\n settings = self.ctx.settings || {};\r\n $scope.settings = settings;\r\n $scope.isHorizontal = (settings.inputFieldsAlignment === 'row') ? true : false;\r\n $scope.isValidParameter = true;\r\n $scope.entityDetected = false;\r\n $scope.dataKeyDetected = false;\r\n $scope.message = $translate.instant('widgets.input-widgets.no-entity-selected');\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-attribute-required');\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === types.datasourceType.entity) {\r\n if (datasource.entityType === types.entityType.device) {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n } else {\r\n $scope.message = $translate.instant('widgets.input-widgets.not-allowed-entity');\r\n }\r\n }\r\n if (datasource.dataKeys.length) {\r\n if (datasource.dataKeys[0].type != types.dataKeyType.attribute) {\r\n $scope.isValidParameter = false;\r\n } else {\r\n $scope.currentKey = datasource.dataKeys[0].name;\r\n $scope.dataKeyType = datasource.dataKeys[0].type;\r\n $scope.dataKeyDetected = true;\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function () {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n var currentValueInMilliseconds = $scope.currentValue.getTime();\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entityType,\r\n datasource.entityId,\r\n types.attributesScope.shared.value,\r\n [\r\n {\r\n key: $scope.currentKey,\r\n value: $scope.currentValue\r\n }\r\n ]\r\n ).then(\r\n function success() {\r\n $scope.originalValue = $scope.currentValue;\r\n if (settings.showResultMessage) {\r\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n }\r\n );\r\n }\r\n };\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n $scope.currentValue = $scope.originalValue = moment(self.ctx.data[0].data[0][1]).toDate();\r\n $scope.$digest();\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n}\r\n\r\nself.onResize = function() {\r\n $scope.smallWidthContainer = (self.ctx.$container[0].offsetWidth < 320) ? true : false;\r\n $scope.changeAlignment = ($scope.isHorizontal && (self.ctx.$container[0].offsetWidth < 480)) ? true : false;\r\n}\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 1\r\n }\r\n}\r\n\r\nself.onDestroy = function() {\r\n\r\n}\r\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"UpdateDateAttributeSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showLabel\":{\n \"title\":\"Show label\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"showTimeInput\":{\n \"title\":\"Show time input field\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"inputFieldsAlignment\": {\n \"title\": \"Input fields alignment\",\n \"type\": \"string\",\n \"default\": \"column\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"showTimeInput\",\n {\n \"key\": \"inputFieldsAlignment\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"column\",\n \"label\": \"Column (default)\"\n },\n {\n \"value\": \"row\",\n \"label\": \"Row\"\n }\n ]\n },\n \"requiredErrorMessage\"\n ]\n}", + "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", + "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}", + "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet utils;\nlet translate;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false; \n\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n\n $scope.attributeUpdateFormGroup = $scope.fb.group(\n {currentValue: [undefined, [$scope.validators.required,\n $scope.validators.min(settings.minValue),\n $scope.validators.max(settings.maxValue)]]}\n );\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SERVER_SCOPE',\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(correctValue($scope.originalValue));\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n return 0;\n }\n return value;\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\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 \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValue\": {\n \"title\": \"Max value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minValue\": {\n \"title\": \"Min value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxValue\",\n \"minValue\"\n ]\n}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update shared date attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "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\":\"Update server double attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { @@ -205,204 +205,204 @@ "sizeX": 7.5, "sizeY": 3.5, "resources": [], - "templateHtml": "
\n
\n
\n
\n
\n
\n
\n
dashboard.no-image
\n \n
\n
\n \n {{ 'action.remove' | translate }}\n close\n \n
\n
\n \n \n
\n
\n
\n
\n\n
\n \n check\n {{ 'widgets.input-widgets.update-attribute' | translate }}\n \n \n close\n {{ 'widgets.input-widgets.discard-changes' | translate }}\n \n
\n
\n \n
\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\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 align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-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 .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-image-select-wrapper {\n width: 100%;\n}\n\n.tb-image-select-wrapper>label {\n display: none;\n}\n\n.tb-image-select-wrapper>label.show-label {\n display: block;\n}\n\n.tb-image-preview-container,\n.tb-flow-drop {\n box-sizing: border-box;\n}\n\n.tb-image-preview {\n max-width: 98px;\n max-height: 98px;\n}\n\n.tb-image-preview-container div,\n.tb-flow-drop label {\n font-size: 16px;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet $translate;\n\nself.onInit = function() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n $translate = $scope.$injector.get('$translate');\n settings = self.ctx.settings || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.message = $translate.instant('widgets.input-widgets.no-entity-selected');\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === types.datasourceType.entity) {\n if (datasource.entityType === types.entityType.device) {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n } else {\n $scope.message = $translate.instant('widgets.input-widgets.not-allowed-entity');\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != types.dataKeyType.attribute) {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entityType,\n datasource.entityId,\n types.attributesScope.shared.value,\n [\n {\n key: $scope.currentKey,\n value: $scope.currentValue\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n \n $scope.imageAdded = function ($file) {\n var reader = new FileReader();\n reader.onload = function(event) {\n $scope.$apply(function() {\n if (event.target.result && event.target.result.startsWith('data:image/')) {\n $scope.attrUpdateForm.$setDirty();\n $scope.currentValue = event.target.result;\n } \n });\n };\n reader.readAsDataURL($file.file);\n };\n \n $scope.clearImage = function () {\n $scope.attrUpdateForm.$setDirty();\n $scope.currentValue = null;\n };\n \n};\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.$digest();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onDestroy = function() {\n\n}\n", + "templateHtml": "
\n
\n
\n
\n
\n \n \n
\n \n
\n \n \n
\n
\n \n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", + "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 align-items: center;\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.tb-image-preview-container div,\n.tb-flow-drop label {\n font-size: 16px !important;\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}", + "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet utils;\r\nlet translate;\r\n\r\nself.onInit = function() {\r\n self.ctx.ngZone.run(function() {\r\n init(); \r\n self.ctx.detectChanges(true);\r\n });\r\n};\r\n\r\nfunction init() {\r\n\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\r\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\r\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\r\n $scope.toastTargetId = 'input-widget' + utils.guid();\r\n settings = utils.deepClone(self.ctx.settings) || {};\r\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\r\n settings.displayPreview = utils.defaultValue(settings.displayPreview, true);\r\n settings.displayClearButton = utils.defaultValue(settings.displayClearButton, false);\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false;\r\n $scope.entityDetected = false;\r\n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\r\n \r\n $scope.attributeUpdateFormGroup = $scope.fb.group(\r\n {currentValue: [undefined, []]}\r\n );\r\n \r\n $scope.attributeUpdateFormGroup.valueChanges.subscribe( () => {\r\n self.ctx.detectChanges();\r\n });\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n \r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType === 'DEVICE') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n \r\n $scope.entityDetected = true;\r\n }\r\n } else {\r\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\r\n }\r\n }\r\n if (datasource.dataKeys.length) {\r\n if (datasource.dataKeys[0].type !== \"attribute\") {\r\n $scope.isValidParameter = false;\r\n } else {\r\n $scope.currentKey = datasource.dataKeys[0].name;\r\n $scope.dataKeyType = datasource.dataKeys[0].type;\r\n $scope.dataKeyDetected = true;\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n \r\n $scope.updateAttribute = function () {\r\n console.log(\"Work\");\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entity.id,\r\n 'SHARED_SCOPE',\r\n [\r\n {\r\n key: $scope.currentKey,\r\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\r\n }\r\n ]\r\n ).subscribe(\r\n function success() {\r\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\r\n if (settings.showResultMessage) {\r\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n }\r\n );\r\n }\r\n };\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n $scope.originalValue = self.ctx.data[0].data[0][1];\r\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue($scope.originalValue);\r\n self.ctx.detectChanges();\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n};\r\n\r\nself.onResize = function() {\r\n\r\n};\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 1,\r\n singleEntity: true\r\n };\r\n};\r\n\r\nself.onDestroy = function() {\r\n $scope.attributeUpdateFormGroup.valueChanges.unsubscribe();\r\n}", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"UpdateImageAttributeSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"displayPreview\":{\n \"title\":\"Display preview\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"displayClearButton\":{\n \"title\":\"Display clear button\",\n \"type\":\"boolean\",\n \"default\":false\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"displayPreview\",\n \"displayClearButton\"\n ]\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update shared image attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { - "alias": "update_shared_location_attribute", - "name": "Update shared location attribute", + "alias": "update_shared_date_attribute", + "name": "Update shared date attribute", "descriptor": { "type": "latest", "sizeX": 7.5, - "sizeY": 3, + "sizeY": 3.5, "resources": [], - "templateHtml": "
\n
\n\n
\n
\n \n \n \n
\n
{{requiredErrorMessage}}
\n
\n
\n \n \n \n
\n
{{requiredErrorMessage}}
\n
\n
\n
\n\n
\n \n my_location\n {{ 'widgets.input-widgets.get-location' | translate }}\n \n \n \n check\n {{ 'widgets.input-widgets.update-timeseries' | translate }}\n \n \n close\n {{ 'widgets.input-widgets.discard-changes' | translate }}\n \n
\n
\n \n
\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-coordinate-specified' | translate }}\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex-direction: column;\n flex: 1;\n}\n\n.grid__element.horizontal-alignment {\n flex-direction: row;\n}\n\n.grid__element:last-child {\n align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0;\n line-height: 20px;\n}\n\n.attribute-update-form .md-button.getLocation {\n margin-right: 10px;\n}\n\n.attribute-update-form .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.attribute-update-form md-input-container{\n width: 100%;\n margin: 18px 0 5px;\n}\n\n.attribute-update-form.small-width md-input-container{\n width: 150px;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet toast;\r\nlet utils;\r\nlet types;\r\nlet $translate;\r\n\r\nself.onInit = function() {\r\n console.log(self.ctx);\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get('attributeService');\r\n toast = $scope.$injector.get('toast');\r\n utils = $scope.$injector.get('utils');\r\n types = $scope.$injector.get('types');\r\n $translate = $scope.$injector.get('$translate');\r\n settings = self.ctx.settings || {};\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false;\r\n $scope.isHorizontal = (settings.inputFieldsAlignment === 'row') ? true : false;\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-coordinate-required');\r\n $scope.latLabel = utils.customTranslation(settings.latLabel, settings.latLabel) || $translate.instant('widgets.input-widgets.latitude');\r\n $scope.lngLabel = utils.customTranslation(settings.lngLabel, settings.lngLabel) || $translate.instant('widgets.input-widgets.longitude');\r\n $scope.message = $translate.instant('widgets.input-widgets.no-entity-selected');\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === types.datasourceType.entity) {\r\n if (datasource.entityType === types.entityType.device) {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n \r\n $scope.entityDetected = true;\r\n }\r\n } else {\r\n $scope.message = $translate.instant('widgets.input-widgets.not-allowed-entity');\r\n }\r\n }\r\n if (datasource.dataKeys.length > 1) {\r\n $scope.dataKeyDetected = true;\r\n for (let i = 0; i < datasource.dataKeys.length; i++) {\r\n if (datasource.dataKeys[i].type != types.dataKeyType.attribute){\r\n $scope.isValidParameter = false;\r\n }\r\n if (datasource.dataKeys[i].name !== settings.latKeyName && datasource.dataKeys[i].name !== settings.lngKeyName){\r\n $scope.dataKeyDetected = false;\r\n }\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function() {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entityType,\r\n datasource.entityId,\r\n types.attributesScope.server.value,\r\n [{\r\n key: settings.latKeyName,\r\n value: $scope.currentLat\r\n },{\r\n key: settings.lngKeyName,\r\n value: $scope.currentLng\r\n }]\r\n ).then(\r\n function success() {\r\n $scope.originalLat = $scope.currentLat;\r\n $scope.originalLng = $scope.currentLng;\r\n if (settings.showResultMessage) {\r\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000,\r\n angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), \r\n angular.element(self.ctx.$container),'bottom left');\r\n }\r\n }\r\n );\r\n }\r\n };\r\n\r\n $scope.changeFocus = function() {\r\n if ($scope.currentLat === $scope.originalLat || $scope.currentLng ===$scope.originalLng) {\r\n $scope.isFocused = false;\r\n }\r\n }\r\n\r\n $scope.discardChange = function() {\r\n $scope.currentLat = $scope.originalLat;\r\n $scope.currentLng = $scope.originalLng;\r\n scope.isFocused = false;\r\n }\r\n \r\n $scope.disableButton = function () {\r\n return $scope.currentLat === $scope.originalLat && $scope.currentLng === $scope.originalLng || $scope.currentLng === null || $scope.currentLat === null ;\r\n }\r\n \r\n $scope.getCoordinate = function() {\r\n if (navigator.geolocation) {\r\n navigator.geolocation.getCurrentPosition(showPosition, function (){\r\n toast.showError($translate.instant('widgets.input-widgets.blocked-location'), \r\n angular.element(self.ctx.$container),'bottom left');\r\n }, {\r\n enableHighAccuracy: settings.enableHighAccuracy\r\n });\r\n } else {\r\n toast.showError($translate.instant('widgets.input-widgets.no-support-geolocation'), \r\n angular.element(self.ctx.$container),'bottom left');\r\n }\r\n }\r\n \r\n function showPosition(position) {\r\n $scope.currentLat = position.coords.latitude;\r\n $scope.currentLng = position.coords.longitude;\r\n }\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n for(let i = 0; i < self.typeParameters().maxDataKeys; i++){\r\n if(self.ctx.data[i].dataKey.name === self.ctx.settings.latKeyName){\r\n $scope.currentLat = $scope.originalLat = self.ctx.data[i].data[0][1];\r\n } else if(self.ctx.data[i].dataKey.name === self.ctx.settings.lngKeyName){\r\n $scope.currentLng = $scope.originalLng = self.ctx.data[i].data[0][1];\r\n }\r\n }\r\n $scope.$digest();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n}\r\n\r\nself.onResize = function() {\r\n $scope.smallWidthContainer = (self.ctx.$container[0].offsetWidth < 320) ? true : false;\r\n $scope.changeAlignment = ($scope.isHorizontal && (self.ctx.$container[0].offsetWidth < 480)) ? true : false;\r\n}\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 2\r\n }\r\n}\r\n\r\nself.onDestroy = function() {\r\n\r\n}", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"latitude\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"longitude\"\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"latLabel\": {\n \"title\": \"Label for latitude\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"lngLabel\": {\n \"title\": \"Label for longitude\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\": {\n \"title\": \"Show result message\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableHighAccuracy\": {\n \"title\": \"Use high accuracy\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showGetLocation\": {\n \"title\": \"Show button 'Get current location'\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"inputFieldsAlignment\": {\n \"title\": \"Input fields alignment\",\n \"type\": \"string\",\n \"default\": \"column\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"latKeyName\",\n \"lngKeyName\",\n \"enableHighAccuracy\",\n \"showGetLocation\",\n \"showResultMessage\",\n {\n \"key\": \"inputFieldsAlignment\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"column\",\n \"label\": \"Column (default)\"\n },\n {\n \"value\": \"row\",\n \"label\": \"Row\"\n }\n ]\n },\n \"showLabel\",\n \"latLabel\",\n \"lngLabel\",\n \"requiredErrorMessage\"\n ]\n}", + "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", + "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}", + "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet utils;\r\nlet translate;\r\n\r\nself.onInit = function() {\r\n self.ctx.ngZone.run(function() {\r\n init(); \r\n self.ctx.detectChanges(true);\r\n });\r\n};\r\n\r\nfunction init() {\r\n\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\r\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\r\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\r\n $scope.toastTargetId = 'input-widget' + utils.guid();\r\n settings = utils.deepClone(self.ctx.settings) || {};\r\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\r\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false;\r\n $scope.entityDetected = false;\r\n\r\n $scope.datePickerType = settings.showTimeInput ? 'datetime' : 'date'; \r\n \r\n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\r\n $scope.labelValue = translate.instant('widgets.input-widgets.date');\r\n \r\n \r\n if (settings.showTimeInput) {\r\n $scope.labelValue += \" & \" + translate.instant('widgets.input-widgets.time');\r\n }\r\n \r\n $scope.attributeUpdateFormGroup = $scope.fb.group(\r\n {currentValue: [undefined, [$scope.validators.required]]}\r\n );\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n \r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType === 'DEVICE') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n \r\n $scope.entityDetected = true;\r\n }\r\n } else {\r\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\r\n }\r\n }\r\n if (datasource.dataKeys.length) {\r\n if (datasource.dataKeys[0].type !== \"attribute\") {\r\n $scope.isValidParameter = false;\r\n } else {\r\n $scope.currentKey = datasource.dataKeys[0].name;\r\n $scope.dataKeyType = datasource.dataKeys[0].type;\r\n $scope.dataKeyDetected = true;\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n \r\n $scope.updateAttribute = function () {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n var currentValueInMilliseconds = $scope.attributeUpdateFormGroup.get('currentValue').value.getTime();\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entity.id,\r\n 'SHARED_SCOPE',\r\n [\r\n {\r\n key: $scope.currentKey,\r\n value: currentValueInMilliseconds\r\n }\r\n ]\r\n ).subscribe(\r\n function success() {\r\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\r\n if (settings.showResultMessage) {\r\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n }\r\n );\r\n }\r\n };\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n $scope.originalValue = moment(self.ctx.data[0].data[0][1]).toDate();\r\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue($scope.originalValue);\r\n self.ctx.detectChanges();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n};\r\n\r\nself.onResize = function() {\r\n\r\n};\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 1,\r\n singleEntity: true\r\n };\r\n};\r\n\r\nself.onDestroy = function() {\r\n\r\n}\r\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"UpdateDateAttributeSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showLabel\":{\n \"title\":\"Show label\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"showTimeInput\":{\n \"title\":\"Show select time\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"inputFieldsAlignment\": {\n \"title\": \"Input fields alignment\",\n \"type\": \"string\",\n \"default\": \"column\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"showTimeInput\",\n \"requiredErrorMessage\"\n ]\n}", "dataKeySettingsSchema": "{}\n", - "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\":\"Update shared location attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update shared date attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { - "alias": "update_string_timeseries", - "name": "Update string timeseries", + "alias": "update_shared_boolean_attribute", + "name": "Update shared boolean attribute", "descriptor": { "type": "latest", "sizeX": 7.5, "sizeY": 3, "resources": [], - "templateHtml": "
\n
\n\n
\n
\n \n \n \n
\n
{{requiredErrorMessage}}
\n
\n
\n
\n\n
\n \n check\n {{ 'widgets.input-widgets.update-timeseries' | translate }}\n \n \n close\n {{ 'widgets.input-widgets.discard-changes' | translate }}\n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-timeseries-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.attribute-not-allowed' | translate }}\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\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 .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-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 .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet toast;\r\nlet utils;\r\nlet types;\r\nlet $translate;\r\nlet $q\r\nlet $http;\r\n\r\nself.onInit = function() {\r\n\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get('attributeService');\r\n toast = $scope.$injector.get('toast');\r\n utils = $scope.$injector.get('utils');\r\n types = $scope.$injector.get('types');\r\n $translate = $scope.$injector.get('$translate');\r\n $q = $scope.$injector.get('$q');\r\n $http = $scope.$injector.get('$http');\r\n settings = self.ctx.settings || {};\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false;\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-timeseries-required');\r\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || $translate.instant('widgets.input-widgets.value');\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === types.datasourceType.entity) {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length) {\r\n if (datasource.dataKeys[0].type != types.dataKeyType.timeseries) {\r\n $scope.isValidParameter = false;\r\n } else {\r\n $scope.currentKey = datasource.dataKeys[0].name;\r\n $scope.dataKeyType = datasource.dataKeys[0].type;\r\n $scope.dataKeyDetected = true;\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function () {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n saveEntityTimeseries(\r\n datasource.entityType,\r\n datasource.entityId,\r\n [\r\n {\r\n key: $scope.currentKey,\r\n value: $scope.currentValue\r\n }\r\n ]\r\n ).then(\r\n function success() {\r\n $scope.originalValue = $scope.currentValue;\r\n if (settings.showResultMessage) {\r\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n }\r\n );\r\n }\r\n };\r\n\r\n $scope.changeFocus = function () {\r\n if ($scope.currentValue === $scope.originalValue) {\r\n $scope.isFocused = false;\r\n }\r\n }\r\n\r\n function saveEntityTimeseries(entityType, entityId, telemetries) {\r\n var deferred = $q.defer();\r\n var telemetriesData = {};\r\n for (var a = 0; a < telemetries.length; a++) {\r\n if (angular.isDefined(telemetries[a].value) && telemetries[a].value !== null) {\r\n telemetriesData[telemetries[a].key] = telemetries[a].value;\r\n }\r\n }\r\n if (Object.keys(telemetriesData).length) {\r\n var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\r\n $http.post(url, telemetriesData).then(\r\n function(response) {\r\n deferred.resolve(response.data);\r\n },\r\n function() {\r\n deferred.reject();\r\n }\r\n );\r\n }\r\n return deferred.promise;\r\n }\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\r\n $scope.$digest();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n}\r\n\r\nself.onResize = function() {\r\n\r\n}\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 1\r\n }\r\n}\r\n\r\nself.onDestroy = function() {\r\n\r\n}\r\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\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 \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxLength\": {\n \"title\": \"Max length\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minLength\": {\n \"title\": \"Min length\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxLength\",\n \"minLength\"\n ]\n}", + "templateHtml": "
\r\n
\r\n
\r\n
\r\n
\r\n \r\n {{currentValue}}\r\n \r\n
\r\n
\r\n\r\n
\r\n
\r\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\r\n
\r\n
\r\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\r\n
\r\n
\r\n
\r\n
", + "templateCss": ".attribute-update-form {\r\n overflow: hidden;\r\n height: 100%;\r\n display: flex;\r\n flex-direction: column;\r\n}\r\n\r\n.attribute-update-form__grid {\r\n display: flex;\r\n}\r\n.grid__element:first-child {\r\n flex: 1;\r\n}\r\n\r\n.grid__element {\r\n display: flex;\r\n}\r\n\r\n.attribute-update-form .mat-button.mat-icon-button {\r\n width: 32px;\r\n min-width: 32px;\r\n height: 32px;\r\n min-height: 32px;\r\n padding: 0 !important;\r\n margin: 0 !important;\r\n line-height: 20px;\r\n}\r\n\r\n.attribute-update-form .mat-icon-button mat-icon {\r\n width: 20px;\r\n min-width: 20px;\r\n height: 20px;\r\n min-height: 20px;\r\n font-size: 20px;\r\n}\r\n\r\n.tb-toast {\r\n font-size: 14px!important;\r\n}", + "controllerScript": "let settings;\nlet attributeService;\nlet utils;\nlet translate;\nlet $scope;\nlet map;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init();\n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\n\n settings.trueValue = utils.defaultValue(utils.customTranslation(settings.trueValue, settings.trueValue), true);\n settings.falseValue = utils.defaultValue(utils.customTranslation(settings.falseValue, settings.falseValue), false);\n\n map = {\n true: settings.trueValue,\n false: settings.falseValue\n };\n \n $scope.checkboxValue = false;\n $scope.currentValue = map[$scope.checkboxValue];\n\n $scope.attributeUpdateFormGroup = $scope.fb.group({checkboxValue: [$scope.checkboxValue]});\n\n $scope.changed = function() {\n $scope.checkboxValue = $scope.attributeUpdateFormGroup.get('checkboxValue').value;\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.updateAttribute();\n };\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType === 'DEVICE') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n \n $scope.entityDetected = true;\n }\n } else {\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function() {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SHARED_SCOPE',\n [\n {\n key: $scope.currentKey,\n value: $scope.checkboxValue || false\n }\n ]\n ).subscribe(\n function success() {\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n $scope.checkboxValue = self.ctx.data[0].data[0][1] === 'true';\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.attributeUpdateFormGroup.get('checkboxValue').patchValue($scope.checkboxValue);\n self.ctx.detectChanges();\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {}", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"trueValue\": {\n \"title\": \"True value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"falseValue\": {\n \"title\": \"False value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"trueValue\",\n \"falseValue\"\n ]\n}", "dataKeySettingsSchema": "{}\n", - "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\":\"Update string timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "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\":\"Update shared boolean attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { - "alias": "update_boolean_timeseries", - "name": "Update boolean timeseries", + "alias": "update_server_integer_attribute", + "name": "Update server integer attribute", "descriptor": { "type": "latest", "sizeX": 7.5, "sizeY": 3, "resources": [], - "templateHtml": "
\n
\n
\n
\n \n {{currentValue}}\n \n
\n
\n\n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-timeseries-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.attribute-not-allowed' | translate }}\n
\n
\n
", - "templateCss": ".attribute-update-form {\r\n overflow: hidden;\r\n height: 100%;\r\n display: flex;\r\n flex-direction: column;\r\n}\r\n\r\n.entity-title {\r\n font-weight: bold;\r\n font-size: 22px;\r\n padding-top: 12px;\r\n padding-bottom: 6px;\r\n color: #666;\r\n}\r\n\r\n.attribute-update-form__grid {\r\n display: flex;\r\n}\r\n.grid__element:first-child {\r\n flex: 1;\r\n}\r\n\r\n.grid__element {\r\n display: flex;\r\n}\r\n\r\n.attribute-update-form .md-button.md-icon-button {\r\n margin: 0;\r\n}\r\n\r\n.attribute-update-form .md-button.md-icon-button {\r\n width: 32px;\r\n min-width: 32px;\r\n height: 32px;\r\n min-height: 32px;\r\n padding: 0 !important;\r\n margin: 0 !important;\r\n line-height: 20px;\r\n}\r\n\r\n.attribute-update-form .md-icon-button md-icon {\r\n width: 20px;\r\n min-width: 20px;\r\n height: 20px;\r\n min-height: 20px;\r\n font-size: 20px;\r\n}\r\n\r\n\r\nmd-toast{\r\n min-width: 0;\r\n}\r\nmd-toast .md-toast-content {\r\n font-size: 14px!important;\r\n}", - "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet $translate;\nlet $q\nlet $http;\nlet map;\nlet mapReverse;\n\nself.onInit = function() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n $translate = $scope.$injector.get('$translate');\n $q = $scope.$injector.get('$q');\n $http = $scope.$injector.get('$http');\n settings = angular.copy(self.ctx.settings) || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n\n settings.trueValue = utils.customTranslation(settings.trueValue, settings.trueValue) || true;\n settings.falseValue = utils.customTranslation(settings.falseValue, settings.falseValue) || false;\n \n map = {\"true\":settings.trueValue, \"false\": settings.falseValue};\n mapReverse = {[settings.trueValue]:\"true\", [settings.falseValue]:\"false\"};\n $scope.checkboxValue = \"false\";\n $scope.currentValue = map[$scope.checkboxValue];\n\n $scope.changed = function () {\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.updateAttribute();\n }\n \n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === types.datasourceType.entity) {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != types.dataKeyType.timeseries) {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n saveEntityTimeseries(\n datasource.entityType,\n datasource.entityId,\n [\n {\n key: $scope.currentKey,\n value: $scope.currentValue\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n\n function saveEntityTimeseries(entityType, entityId, telemetries) {\n var deferred = $q.defer();\n var telemetriesData = {};\n for (var a = 0; a < telemetries.length; a++) {\n if (angular.isDefined(telemetries[a].value) && telemetries[a].value !== null) {\n telemetriesData[telemetries[a].key] = telemetries[a].value;\n }\n }\n if (Object.keys(telemetriesData).length) {\n var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\n $http.post(url, telemetriesData).then(\n function(response) {\n deferred.resolve(response.data);\n },\n function() {\n deferred.reject();\n }\n );\n }\n return deferred.promise;\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n $scope.checkboxValue = mapReverse[$scope.originalValue = self.ctx.data[0].data[0][1]] || 'false';\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.$digest();\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onDestroy = function() {\n\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"trueValue\": {\n \"title\": \"True value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"falseValue\": {\n \"title\": \"False value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"trueValue\",\n \"falseValue\"\n ]\n}", + "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", + "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}", + "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet utils;\nlet translate;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false; \n\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n\n $scope.attributeUpdateFormGroup = $scope.fb.group(\n {currentValue: [undefined, [$scope.validators.required,\n $scope.validators.min(settings.minValue),\n $scope.validators.max(settings.maxValue),\n $scope.validators.pattern(/^-?[0-9]+$/)]]}\n );\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n \n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SERVER_SCOPE',\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(correctValue($scope.originalValue));\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n return 0;\n }\n return value;\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\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 \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValue\": {\n \"title\": \"Max value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minValue\": {\n \"title\": \"Min value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxValue\",\n \"minValue\"\n ]\n}", "dataKeySettingsSchema": "{}\n", - "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\":{\"trueValue\":\"active\",\"falseValue\":\"inactive\"},\"title\":\"Update boolean timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "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\":\"Update server integer attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { - "alias": "update_double_timeseries", - "name": "Update double timeseries", + "alias": "update_server_string_attribute", + "name": "Update server string attribute", "descriptor": { "type": "latest", "sizeX": 7.5, "sizeY": 3, "resources": [], - "templateHtml": "
\n
\n\n
\n
\n \n \n \n
\n
{{requiredErrorMessage}}
\n
\n
\n
\n\n
\n \n check\n {{ 'widgets.input-widgets.update-timeseries' | translate }}\n \n \n close\n {{ 'widgets.input-widgets.discard-changes' | translate }}\n \n
\n
\n\n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-timeseries-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.attribute-not-allowed' | translate }}\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\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 .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-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 .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet toast;\r\nlet utils;\r\nlet types;\r\nlet $translate;\r\nlet $q;\r\nlet $http;\r\n\r\nself.onInit = function() {\r\n\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get('attributeService');\r\n toast = $scope.$injector.get('toast');\r\n utils = $scope.$injector.get('utils');\r\n types = $scope.$injector.get('types');\r\n $translate = $scope.$injector.get('$translate');\r\n $q = $scope.$injector.get('$q');\r\n $http = $scope.$injector.get('$http');\r\n settings = angular.copy(self.ctx.settings) || {};\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false;\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-timeseries-required');\r\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || $translate.instant('widgets.input-widgets.value');\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === types.datasourceType.entity) {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length) {\r\n if (datasource.dataKeys[0].type != types.dataKeyType.timeseries) {\r\n $scope.isValidParameter = false;\r\n } else {\r\n $scope.currentKey = datasource.dataKeys[0].name;\r\n $scope.dataKeyType = datasource.dataKeys[0].type;\r\n $scope.dataKeyDetected = true;\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function () {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n saveEntityTimeseries(\r\n datasource.entityType,\r\n datasource.entityId,\r\n [\r\n {\r\n key: $scope.currentKey,\r\n value: $scope.currentValue\r\n }\r\n ]\r\n ).then(\r\n function success() {\r\n $scope.originalValue = $scope.currentValue;\r\n if (settings.showResultMessage) {\r\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n }\r\n );\r\n }\r\n };\r\n\r\n $scope.changeFocus = function () {\r\n if ($scope.currentValue === $scope.originalValue) {\r\n $scope.isFocused = false;\r\n }\r\n }\r\n\r\n function saveEntityTimeseries(entityType, entityId, telemetries) {\r\n var deferred = $q.defer();\r\n var telemetriesData = {};\r\n for (var a = 0; a < telemetries.length; a++) {\r\n if (angular.isDefined(telemetries[a].value) && telemetries[a].value !== null) {\r\n telemetriesData[telemetries[a].key] = telemetries[a].value;\r\n }\r\n }\r\n if (Object.keys(telemetriesData).length) {\r\n var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\r\n $http.post(url, telemetriesData).then(\r\n function(response) {\r\n deferred.resolve(response.data);\r\n },\r\n function() {\r\n deferred.reject();\r\n }\r\n );\r\n }\r\n return deferred.promise;\r\n }\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\r\n correctValue($scope.currentValue);\r\n $scope.$digest();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n}\r\n\r\nfunction correctValue(value) {\r\n if (typeof value !== \"number\") {\r\n $scope.currentValue = 0;\r\n }\r\n}\r\n\r\nself.onResize = function() {\r\n\r\n}\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 1\r\n }\r\n}\r\n\r\nself.onDestroy = function() {\r\n\r\n}\r\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\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 \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValue\": {\n \"title\": \"Max value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minValue\": {\n \"title\": \"Min value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxValue\",\n \"minValue\"\n ]\n}", + "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", + "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}", + "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet utils;\nlet translate;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false; \n\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n\n $scope.attributeUpdateFormGroup = $scope.fb.group(\n {currentValue: [undefined, [$scope.validators.required,\n $scope.validators.minLength(settings.minLength),\n $scope.validators.maxLength(settings.maxLength)]]}\n );\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SERVER_SCOPE',\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue($scope.originalValue);\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\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 \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxLength\": {\n \"title\": \"Max length\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minLength\": {\n \"title\": \"Min length\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxLength\",\n \"minLength\"\n ]\n}", "dataKeySettingsSchema": "{}\n", - "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\":\"Update double timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update server string attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { - "alias": "update_integer_timeseries", - "name": "Update integer timeseries", + "alias": "update_location_timeseries", + "name": "Update location timeseries", "descriptor": { "type": "latest", "sizeX": 7.5, "sizeY": 3, "resources": [], - "templateHtml": "
\n
\n\n
\n
\n \n \n \n
\n
{{requiredErrorMessage}}
\n
\n
\n
\n\n
\n \n check\n {{ 'widgets.input-widgets.update-timeseries' | translate }}\n \n \n close\n {{ 'widgets.input-widgets.discard-changes' | translate }}\n \n
\n
\n\n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-timeseries-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.attribute-not-allowed' | translate }}\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\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 .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-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 .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet $translate;\nlet $q;\nlet $http;\n\nself.onInit = function() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n $translate = $scope.$injector.get('$translate');\n $q = $scope.$injector.get('$q');\n $http = $scope.$injector.get('$http');\n settings = angular.copy(self.ctx.settings) || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-timeseries-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || $translate.instant('widgets.input-widgets.value');\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === types.datasourceType.entity) {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != types.dataKeyType.timeseries) {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n saveEntityTimeseries(\n datasource.entityType,\n datasource.entityId,\n [\n {\n key: $scope.currentKey,\n value: $scope.currentValue\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.currentValue === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n\n function saveEntityTimeseries(entityType, entityId, telemetries) {\n var deferred = $q.defer();\n var telemetriesData = {};\n for (var a = 0; a < telemetries.length; a++) {\n if (angular.isDefined(telemetries[a].value) && telemetries[a].value !== null) {\n telemetriesData[telemetries[a].key] = telemetries[a].value;\n }\n }\n if (Object.keys(telemetriesData).length) {\n var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\n $http.post(url, telemetriesData).then(\n function(response) {\n deferred.resolve(response.data);\n },\n function() {\n deferred.reject();\n }\n );\n }\n return deferred.promise;\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n correctValue($scope.currentValue);\n $scope.$digest();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n $scope.currentValue = 0;\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onDestroy = function() {\n\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\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 \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValue\": {\n \"title\": \"Max value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minValue\": {\n \"title\": \"Min value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxValue\",\n \"minValue\"\n ]\n}", + "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? latLabel : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n \n \n {{ settings.showLabel ? lngLabel : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-timeseries-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-coordinate-specified' | translate }}\n
\n
\n
\n
", + "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-direction: column;\n flex: 1;\n}\n\n.grid__element.horizontal-alignment {\n flex-direction: row;\n}\n\n.grid__element:last-child {\n align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n margin: 0;\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;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-button.getLocation {\n margin-right: 10px;\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.attribute-update-form mat-form-field{\n width: 100%;\n padding-right: 5px;\n}\n\n.attribute-update-form.small-width mat-form-field{\n width: 150px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", + "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet utils;\r\nlet translate;\r\n\r\nself.onInit = function() {\r\n self.ctx.ngZone.run(function() {\r\n init(); \r\n self.ctx.detectChanges(true);\r\n });\r\n};\r\n\r\n\r\nfunction init() {\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\r\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\r\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\r\n $scope.toastTargetId = 'input-widget' + utils.guid();\r\n settings = utils.deepClone(self.ctx.settings) || {};\r\n \r\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\r\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\r\n settings.showGetLocation = utils.defaultValue(settings.showGetLocation, true);\r\n settings.enableHighAccuracy = utils.defaultValue(settings.enableHighAccuracy, false);\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false; \r\n\r\n $scope.isHorizontal = (settings.inputFieldsAlignment === 'row');\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-coordinate-required');\r\n $scope.latLabel = utils.customTranslation(settings.latLabel, settings.latLabel) || translate.instant('widgets.input-widgets.latitude');\r\n $scope.lngLabel = utils.customTranslation(settings.lngLabel, settings.lngLabel) || translate.instant('widgets.input-widgets.longitude');\r\n\r\n $scope.attributeUpdateFormGroup = $scope.fb.group(\r\n {currentLat: [undefined, [$scope.validators.required,\r\n $scope.validators.min(-90),\r\n $scope.validators.max(90)]],\r\n currentLng: [undefined, [$scope.validators.required,\r\n $scope.validators.min(-180),\r\n $scope.validators.max(180)]]}\r\n );\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length > 1) {\r\n $scope.dataKeyDetected = true;\r\n for (let i = 0; i < datasource.dataKeys.length; i++) {\r\n if (datasource.dataKeys[i].type != \"timeseries\"){\r\n $scope.isValidParameter = false;\r\n }\r\n if (datasource.dataKeys[i].name !== settings.latKeyName && datasource.dataKeys[i].name !== settings.lngKeyName){\r\n $scope.dataKeyDetected = false;\r\n }\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function () {\r\n $scope.isFocused = false;\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n attributeService.saveEntityTimeseries(\r\n datasource.entity.id,\r\n 'scope',\r\n [\r\n {\r\n key: settings.latKeyName,\r\n value: $scope.attributeUpdateFormGroup.get('currentLat').value\r\n },{\r\n key: settings.lngKeyName,\r\n value: $scope.attributeUpdateFormGroup.get('currentLng').value\r\n }\r\n ]\r\n ).subscribe(\r\n function success() {\r\n $scope.originalLat = $scope.attributeUpdateFormGroup.get('currentLat').value;\r\n $scope.originalLng = $scope.attributeUpdateFormGroup.get('currentLng').value;\r\n if (settings.showResultMessage) {\r\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n }\r\n );\r\n }\r\n };\r\n\r\n $scope.changeFocus = function () {\r\n if ($scope.attributeUpdateFormGroup.get('currentLat').value === $scope.originalLat && $scope.attributeUpdateFormGroup.get('currentLng').value === $scope.originalLng) {\r\n $scope.isFocused = false;\r\n }\r\n };\r\n \r\n $scope.discardChange = function() {\r\n $scope.attributeUpdateFormGroup.setValue({\r\n 'currentLat': $scope.originalLat,\r\n 'currentLng': $scope.originalLng\r\n });\r\n $scope.isFocused = false;\r\n $scope.attributeUpdateFormGroup.markAsPristine();\r\n self.onDataUpdated();\r\n };\r\n \r\n $scope.disableButton = function () {\r\n return $scope.attributeUpdateFormGroup.get('currentLat').value === $scope.originalLat && $scope.attributeUpdateFormGroup.get('currentLng').value === $scope.originalLng || $scope.currentLng === null || $scope.currentLat === null;\r\n };\r\n \r\n $scope.getCoordinate = function() {\r\n if (navigator.geolocation) {\r\n navigator.geolocation.getCurrentPosition(showPosition, function (){\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.blocked-location'), \r\n 'bottom', 'left', $scope.toastTargetId);\r\n }, {\r\n enableHighAccuracy: settings.enableHighAccuracy\r\n });\r\n } else {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.no-support-geolocation'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n };\r\n \r\n function showPosition(position) {\r\n $scope.attributeUpdateFormGroup.setValue({\r\n currentLat: correctValue(position.coords.latitude),\r\n currentLng: correctValue(position.coords.longitude)\r\n });\r\n $scope.attributeUpdateFormGroup.markAsDirty();\r\n $scope.isFocused = true;\r\n }\r\n \r\n self.onResize();\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n for(let i = 0; i < self.typeParameters().maxDataKeys; i++){\r\n if(self.ctx.data[i].dataKey.name === self.ctx.settings.latKeyName && $scope.attributeUpdateFormGroup.get('currentLat').pristine){\r\n $scope.originalLat = self.ctx.data[i].data[0][1];\r\n $scope.attributeUpdateFormGroup.get('currentLat').patchValue(correctValue($scope.originalLat));\r\n } else if(self.ctx.data[i].dataKey.name === self.ctx.settings.lngKeyName && $scope.attributeUpdateFormGroup.get('currentLng').pristine){\r\n $scope.originalLng = self.ctx.data[i].data[0][1];\r\n $scope.attributeUpdateFormGroup.get('currentLng').patchValue(correctValue($scope.originalLng));\r\n }\r\n }\r\n self.ctx.detectChanges();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n};\r\n\r\nfunction correctValue(value) {\r\n if (typeof value !== \"number\") {\r\n return 0;\r\n }\r\n return value;\r\n}\r\n\r\nself.onResize = function() {\r\n $scope.smallWidthContainer = (self.ctx.$container && self.ctx.$container[0].offsetWidth < 320);\r\n $scope.changeAlignment = ($scope.isHorizontal && self.ctx.$container && self.ctx.$container[0].offsetWidth < 480);\r\n self.ctx.detectChanges();\r\n};\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 2,\r\n singleEntity: true\r\n };\r\n};\r\n\r\nself.onDestroy = function() {\r\n\r\n};", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"latitude\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"longitude\"\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"latLabel\": {\n \"title\": \"Label for latitude\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"lngLabel\": {\n \"title\": \"Label for longitude\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\": {\n \"title\": \"Show result message\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableHighAccuracy\": {\n \"title\": \"Use high accuracy\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showGetLocation\": {\n \"title\": \"Show button 'Get current location'\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"inputFieldsAlignment\": {\n \"title\": \"Input fields alignment\",\n \"type\": \"string\",\n \"default\": \"column\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"latKeyName\",\n \"lngKeyName\",\n \"enableHighAccuracy\",\n \"showGetLocation\",\n \"showResultMessage\",\n {\n \"key\": \"inputFieldsAlignment\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"column\",\n \"label\": \"Column (default)\"\n },\n {\n \"value\": \"row\",\n \"label\": \"Row\"\n }\n ]\n },\n \"showLabel\",\n \"latLabel\",\n \"lngLabel\",\n \"requiredErrorMessage\"\n ]\n}", "dataKeySettingsSchema": "{}\n", - "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\":\"Update integer timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "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\":\"Update location timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { - "alias": "update_location_timeseries", - "name": "Update location timeseries", + "alias": "update_shared_double_attribute", + "name": "Update shared double attribute", "descriptor": { "type": "latest", "sizeX": 7.5, "sizeY": 3, "resources": [], - "templateHtml": "
\n
\n\n
\n
\n \n \n \n
\n
{{requiredErrorMessage}}
\n
\n
\n \n \n \n
\n
{{requiredErrorMessage}}
\n
\n
\n
\n\n
\n \n my_location\n {{ 'widgets.input-widgets.get-location' | translate }}\n \n \n \n check\n {{ 'widgets.input-widgets.update-timeseries' | translate }}\n \n \n close\n {{ 'widgets.input-widgets.discard-changes' | translate }}\n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.attribute-not-allowed' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-coordinate-specified' | translate }}\n
\n
\n
", - "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex-direction: column;\n flex: 1;\n}\n\n.grid__element.horizontal-alignment {\n flex-direction: row;\n}\n\n.grid__element:last-child {\n align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0;\n line-height: 20px;\n}\n\n.attribute-update-form .md-button.getLocation {\n margin-right: 10px;\n}\n\n.attribute-update-form .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.attribute-update-form md-input-container{\n width: 100%;\n margin: 18px 0 5px;\n}\n\n.attribute-update-form.small-width md-input-container{\n width: 150px;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet toast;\r\nlet utils;\r\nlet types;\r\nlet $translate;\r\n\r\nself.onInit = function() {\r\n console.log(self.ctx);\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get('attributeService');\r\n toast = $scope.$injector.get('toast');\r\n utils = $scope.$injector.get('utils');\r\n types = $scope.$injector.get('types');\r\n $translate = $scope.$injector.get('$translate');\r\n settings = self.ctx.settings || {};\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false;\r\n $scope.isHorizontal = (settings.inputFieldsAlignment === 'row') ? true : false;\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-coordinate-required');\r\n $scope.latLabel = utils.customTranslation(settings.latLabel, settings.latLabel) || $translate.instant('widgets.input-widgets.latitude');\r\n $scope.lngLabel = utils.customTranslation(settings.lngLabel, settings.lngLabel) || $translate.instant('widgets.input-widgets.longitude');\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === types.datasourceType.entity) {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length > 1) {\r\n $scope.dataKeyDetected = true;\r\n for (let i = 0; i < datasource.dataKeys.length; i++) {\r\n if (datasource.dataKeys[i].type != types.dataKeyType.timeseries){\r\n $scope.isValidParameter = false;\r\n }\r\n if (datasource.dataKeys[i].name !== settings.latKeyName && datasource.dataKeys[i].name !== settings.lngKeyName){\r\n $scope.dataKeyDetected = false;\r\n }\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function() {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n attributeService.saveEntityTimeseries(\r\n datasource.entityType,\r\n datasource.entityId,\r\n 'scope',\r\n [{\r\n key: settings.latKeyName,\r\n value: $scope.currentLat\r\n },{\r\n key: settings.lngKeyName,\r\n value: $scope.currentLng\r\n }]\r\n ).then(\r\n function success() {\r\n $scope.originalLat = $scope.currentLat;\r\n $scope.originalLng = $scope.currentLng;\r\n if (settings.showResultMessage) {\r\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000,\r\n angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), \r\n angular.element(self.ctx.$container),'bottom left');\r\n }\r\n }\r\n );\r\n }\r\n };\r\n\r\n $scope.changeFocus = function() {\r\n if ($scope.currentLat === $scope.originalLat || $scope.currentLng ===$scope.originalLng) {\r\n $scope.isFocused = false;\r\n }\r\n }\r\n\r\n $scope.discardChange = function() {\r\n $scope.currentLat = $scope.originalLat;\r\n $scope.currentLng = $scope.originalLng;\r\n scope.isFocused = false;\r\n }\r\n \r\n $scope.disableButton = function () {\r\n return $scope.currentLat === $scope.originalLat && $scope.currentLng === $scope.originalLng || $scope.currentLng === null || $scope.currentLat === null ;\r\n }\r\n \r\n $scope.getCoordinate = function() {\r\n if (navigator.geolocation) {\r\n navigator.geolocation.getCurrentPosition(showPosition, function (){\r\n toast.showError($translate.instant('widgets.input-widgets.blocked-location'), \r\n angular.element(self.ctx.$container),'bottom left');\r\n }, {\r\n enableHighAccuracy: settings.enableHighAccuracy\r\n });\r\n } else {\r\n toast.showError($translate.instant('widgets.input-widgets.no-support-geolocation'), \r\n angular.element(self.ctx.$container),'bottom left');\r\n }\r\n }\r\n \r\n function showPosition(position) {\r\n $scope.currentLat = position.coords.latitude;\r\n $scope.currentLng = position.coords.longitude;\r\n }\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n for(let i = 0; i < self.typeParameters().maxDataKeys; i++){\r\n if(self.ctx.data[i].dataKey.name === self.ctx.settings.latKeyName){\r\n $scope.currentLat = $scope.originalLat = self.ctx.data[i].data[0][1];\r\n } else if(self.ctx.data[i].dataKey.name === self.ctx.settings.lngKeyName){\r\n $scope.currentLng = $scope.originalLng = self.ctx.data[i].data[0][1];\r\n }\r\n }\r\n $scope.$digest();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n}\r\n\r\nself.onResize = function() {\r\n $scope.smallWidthContainer = (self.ctx.$container[0].offsetWidth < 320) ? true : false;\r\n $scope.changeAlignment = ($scope.isHorizontal && (self.ctx.$container[0].offsetWidth < 480)) ? true : false;\r\n}\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 2\r\n }\r\n}\r\n\r\nself.onDestroy = function() {\r\n\r\n}", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"latitude\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"longitude\"\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"latLabel\": {\n \"title\": \"Label for latitude\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"lngLabel\": {\n \"title\": \"Label for longitude\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\": {\n \"title\": \"Show result message\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableHighAccuracy\": {\n \"title\": \"Use high accuracy\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showGetLocation\": {\n \"title\": \"Show button 'Get current location'\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"inputFieldsAlignment\": {\n \"title\": \"Input fields alignment\",\n \"type\": \"string\",\n \"default\": \"column\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"latKeyName\",\n \"lngKeyName\",\n \"enableHighAccuracy\",\n \"showGetLocation\",\n \"showResultMessage\",\n {\n \"key\": \"inputFieldsAlignment\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"column\",\n \"label\": \"Column (default)\"\n },\n {\n \"value\": \"row\",\n \"label\": \"Row\"\n }\n ]\n },\n \"showLabel\",\n \"latLabel\",\n \"lngLabel\",\n \"requiredErrorMessage\"\n ]\n}", + "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", + "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}", + "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet utils;\nlet translate;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false; \n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\n \n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n\n $scope.attributeUpdateFormGroup = $scope.fb.group(\n {currentValue: [undefined, [$scope.validators.required,\n $scope.validators.min(settings.minValue),\n $scope.validators.max(settings.maxValue)]]}\n );\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType === 'DEVICE') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n \n $scope.entityDetected = true;\n }\n } else {\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SHARED_SCOPE',\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(correctValue($scope.originalValue));\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n return 0;\n }\n return value;\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\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 \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValue\": {\n \"title\": \"Max value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minValue\": {\n \"title\": \"Min value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxValue\",\n \"minValue\"\n ]\n}", "dataKeySettingsSchema": "{}\n", - "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\":\"Update location timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "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\":\"Update shared double attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { - "alias": "update_multiple_attributes", - "name": "Update Multiple Attributes", + "alias": "update_shared_integer_attribute", + "name": "Update shared integer attribute", "descriptor": { "type": "latest", "sizeX": 7.5, - "sizeY": 3.5, + "sizeY": 3, "resources": [], - "templateHtml": "\n", - "templateCss": "", - "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet toast;\r\nlet utils;\r\nlet types;\r\n\r\nself.onInit = function() {\r\n var scope = self.ctx.$scope;\r\n var id = self.ctx.$scope.$injector.get('utils').guid();\r\n scope.formId = \"form-\"+id;\r\n scope.ctx = self.ctx;\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n self.ctx.$scope.$broadcast('multiple-input-data-updated', self.ctx.$scope.formId);\r\n}\r\n\r\nself.onResize = function() {\r\n self.ctx.$scope.$broadcast('multiple-input-resize', self.ctx.$scope.formId);\r\n}\r\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"MultipleInput\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showActionButtons\":{\n \"title\":\"Show action buttons\",\n \"type\":\"boolean\",\n \"default\": true\n },\n \"updateAllValues\": {\n \"title\":\"Update all values, not only modified (only if action buttons are visible)\",\n \"type\":\"boolean\",\n \"default\": false\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\": true\n },\n \"showGroupTitle\": {\n \"title\":\"Show title for group of fields, related to different entities\",\n \"type\":\"boolean\",\n \"default\": false\n },\n \"groupTitle\": {\n \"title\": \"Group title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"fieldsAlignment\": {\n \"title\": \"Fields alignment\",\n \"type\": \"string\",\n \"default\": \"row\"\n },\n \"fieldsInRow\": {\n \"title\": \"Number of fields in the row\",\n \"type\": \"number\",\n \"default\": \"2\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showActionButtons\",\n \"updateAllValues\",\n \"showResultMessage\",\n \"showGroupTitle\",\n \"groupTitle\",\n {\n \"key\": \"fieldsAlignment\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"row\",\n \"label\": \"Row (default)\"\n },\n {\n \"value\": \"column\",\n \"label\": \"Column\"\n }\n ]\n },\n \"fieldsInRow\"\n ]\n}", - "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"dataKeyType\": {\n \"title\": \"Datakey type\",\n \"type\": \"string\",\n \"default\": \"server\"\n },\n \"dataKeyValueType\": {\n \"title\": \"Datakey value type\",\n \"type\": \"string\",\n \"default\": \"string\"\n },\n \"required\": {\n \"title\": \"Value is required\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"isEditable\": {\n \"title\": \"Ability to edit attribute\",\n \"type\": \"string\",\n \"default\": \"editable\"\n },\n \"disabledOnDataKey\": {\n \"title\": \"Disable on false value of another datakey (specify datakey name)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"dataKeyHidden\": {\n \"title\": \"Hide input field\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"step\": {\n \"title\": \"Step interval between values (only for numbers)\",\n \"type\": \"number\",\n \"default\": \"1\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"icon\": {\n \"title\": \"Icon to show before input cell\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"dataKeyType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"server\",\n \"label\": \"Server attribute (default)\"\n },\n {\n \"value\": \"shared\",\n \"label\": \"Shared attribute\"\n },\n {\n \"value\": \"timeseries\",\n \"label\": \"Timeseries\"\n }\n ]\n },\n {\n \"key\": \"dataKeyValueType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"string\",\n \"label\": \"String\"\n },\n {\n \"value\": \"double\",\n \"label\": \"Double\"\n },\n {\n \"value\": \"integer\",\n \"label\": \"Integer\"\n },\n {\n \"value\": \"booleanCheckbox\",\n \"label\": \"Boolean (Checkbox)\"\n },\n {\n \"value\": \"booleanSwitch\",\n \"label\": \"Boolean (Switch)\"\n },\n {\n \"value\": \"dateTime\",\n \"label\": \"Date & Time\"\n },\n {\n \"value\": \"date\",\n \"label\": \"Date\"\n },\n {\n \"value\": \"time\",\n \"label\": \"Time\"\n }\n ]\n },\n \"required\",\n {\n \"key\": \"isEditable\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"editable\",\n \"label\": \"Editable (default)\"\n },\n {\n \"value\": \"disabled\",\n \"label\": \"Disabled\"\n },\n {\n \"value\": \"readonly\",\n \"label\": \"Read-only\"\n }\n ]\n },\n \"disabledOnDataKey\",\n \"dataKeyHidden\",\n \"step\",\n \"requiredErrorMessage\",\n\t\t{\n \t\t\"key\": \"icon\",\n\t\t\t\"type\": \"icon\"\n\t\t}\n ]\n}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update Multiple Attributes\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", + "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}", + "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet utils;\nlet translate;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false; \n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\n \n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n\n $scope.attributeUpdateFormGroup = $scope.fb.group(\n {currentValue: [undefined, [$scope.validators.required,\n $scope.validators.min(settings.minValue),\n $scope.validators.max(settings.maxValue),\n $scope.validators.pattern(/^-?[0-9]+$/)]]}\n );\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType === 'DEVICE') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n \n $scope.entityDetected = true;\n }\n } else {\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SHARED_SCOPE',\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(correctValue($scope.originalValue));\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n return 0;\n }\n return value;\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\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 \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValue\": {\n \"title\": \"Max value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minValue\": {\n \"title\": \"Min value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxValue\",\n \"minValue\"\n ]\n}", + "dataKeySettingsSchema": "{}\n", + "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\":\"Update shared integer attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { - "alias": "web_camera_input", - "name": "Web Camera Input", + "alias": "update_shared_string_attribute", + "name": "Update shared string attribute", "descriptor": { "type": "latest", "sizeX": 7.5, "sizeY": 3, "resources": [], - "templateHtml": "\n", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Web Camera\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"imageFormat\": {\n \"title\": \"Image Format\",\n \"type\": \"string\",\n \"default\": \"image/png\"\n },\n \"imageQuality\":{\n \"title\":\"Image quality that use lossy compression such as jpeg and webp\",\n \"type\":\"number\",\n \"default\": 0.92,\n \"min\": 0,\n \"max\": 1\n },\n \"maxWidth\": {\n \"title\": \"The maximal image width\",\n \"type\": \"number\",\n \"default\": 640\n }, \n \"maxHeight\": {\n \"title\": \"The maximal image heigth\",\n \"type\": \"number\",\n \"default\": 480\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n {\n \"key\": \"imageFormat\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"image/jpeg\",\n \"label\": \"JPEG\"\n },\n {\n \"value\": \"image/png\",\n \"label\": \"PNG\"\n },\n {\n \"value\": \"image/webp\",\n \"label\": \"WEBP\"\n }\n ]\n },\n \"imageQuality\",\n \"maxWidth\",\n \"maxHeight\"\n ]\n}", + "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", + "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}", + "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet utils;\nlet translate;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false; \n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\n \n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n\n $scope.attributeUpdateFormGroup = $scope.fb.group(\n {currentValue: [undefined, [$scope.validators.required,\n $scope.validators.minLength(settings.minLength),\n $scope.validators.maxLength(settings.maxLength)]]}\n );\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType === 'DEVICE') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n \n $scope.entityDetected = true;\n }\n } else {\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SHARED_SCOPE',\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue($scope.originalValue);\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\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 \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxLength\": {\n \"title\": \"Max length\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minLength\": {\n \"title\": \"Min length\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxLength\",\n \"minLength\"\n ]\n}", "dataKeySettingsSchema": "{}\n", - "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\":\"Web 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\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update shared string attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { - "alias": "device_claiming_widget", - "name": "Device claiming widget", + "alias": "update_server_location_attribute", + "name": "Update server location attribute", "descriptor": { - "type": "static", + "type": "latest", "sizeX": 7.5, - "sizeY": 4.5, + "sizeY": 3, "resources": [], - "templateHtml": "
\n
\n \n \n \n
\n
{{requiredErrorDevice}}
\n
\n
\n \n \n \n
\n
{{requiredErrorSecretKey}}
\n
\n
\n
\n
\n {{labelClaimButon}}\n
\n
", - "templateCss": ".claim-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}", - "controllerScript": "let $scope;\n\nself.onInit = function() {\n $scope = self.ctx.$scope;\n let $injector = $scope.$injector;\n let $q = $injector.get('$q');\n let $http = $injector.get('$http');\n let toast = $scope.$injector.get('toast');\n let utils = $scope.$injector.get('utils');\n let $translate = $scope.$injector.get('$translate');\n let $rootScope = $scope.$injector.get('$rootScope');\n let settings = self.ctx.settings || {};\n $scope.secretKeyField = settings.deviceSecret;\n $scope.showLabel = settings.showLabel;\n $scope.deviceObj = {};\n \n const config = {\n ignoreErrors: true \n };\n \n let titleTemplate = \"\";\n let successfulClaim = utils.customTranslation(settings.successfulClaimDevice, settings.successfulClaimDevice) || $translate.instant('widgets.input-widgets.claim-successful');\n let failedClaimDevice = utils.customTranslation(settings.failedClaimDevice, settings.failedClaimDevice) || $translate.instant('widgets.input-widgets.claim-failed');\n let deviceNotFound = utils.customTranslation(settings.deviceNotFound, settings.deviceNotFound) || $translate.instant('widgets.input-widgets.claim-not-found');\n \n if (settings.widgetTitle && settings.widgetTitle.length) {\n titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n titleTemplate = self.ctx.widgetConfig.title;\n }\n self.ctx.widgetTitle = titleTemplate;\n \n $scope.deviceLabel = utils.customTranslation(settings.deviceLabel, settings.deviceLabel) || $translate.instant('widgets.input-widgets.device-name');\n $scope.requiredErrorDevice= utils.customTranslation(settings.requiredErrorDevice, settings.requiredErrorDevice) || $translate.instant('widgets.input-widgets.device-name-required');\n \n $scope.secretKeyLabel = utils.customTranslation(settings.secretKeyLabel, settings.secretKeyLabel) || $translate.instant('widgets.input-widgets.secret-key');\n $scope.requiredErrorSecretKey= utils.customTranslation(settings.requiredErrorSecretKey, settings.requiredErrorSecretKey) || $translate.instant('widgets.input-widgets.secret-key-required');\n \n $scope.labelClaimButon = utils.customTranslation(settings.labelClaimButon, settings.labelClaimButon) || $translate.instant('widgets.input-widgets.claim-device');\n \n $scope.claim = () => {\n $scope.loading = true;\n claimDevice($scope.deviceObj.deviceName, $scope.deviceObj.deviceSecret, config).then(\n (data) => {\n successClaim();\n },\n (error) => {\n $scope.loading = false;\n if(error.status == 404) {\n toast.showError(deviceNotFound, angular.element('.claim-form'),'bottom left');\n } else if(error.status == 400) {\n toast.showError(failedClaimDevice, angular.element('.claim-form'),'bottom left');\n }\n }\n );\n }\n \n function claimDevice(deviceName, deviceSecret, config) {\n let deferred = $q.defer();\n let url = \"/api/customer/device/\" + deviceName + \"/claim\";\n let obj = deviceSecret ? { secretKey: deviceSecret } : {};\n $http.post(url, obj, config).then(\n (payload) => {\n deferred.resolve(payload.data); \n },\n (error) => {\n deferred.reject(error);\n }\n );\n return deferred.promise;\n }\n \n function updateAliasData() {\n var aliasIds = [];\n for (var id in self.ctx.aliasController.resolvedAliases) {\n aliasIds.push(id);\n }\n var tasks = [];\n aliasIds.forEach(function(aliasId) {\n self.ctx.aliasController.setAliasUnresolved(aliasId);\n tasks.push(self.ctx.aliasController.getAliasInfo(aliasId));\n });\n $q.all(tasks).then(function() {\n $rootScope.$broadcast('widgetForceReInit');\n });\n }\n \n function successClaim() {\n resetForm();\n $scope.claimDeviceForm.$setPristine();\n $scope.claimDeviceForm.$setUntouched();\n $scope.loading = false;\n toast.showSuccess(successfulClaim, 2000);\n updateAliasData();\n }\n \n function resetForm() {\n $scope.deviceObj = {};\n }\n}\n\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"deviceSecret\": {\n \"title\": \"Show 'Secret key' input field\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"deviceLabel\": {\n \"title\": \"Label for device name\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorDevice\": {\n \"title\": \"'Device name required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"secretKeyLabel\": {\n \"title\": \"Label for secret key\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorSecretKey\": {\n \"title\": \"'Secret key required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"labelClaimButon\": {\n \"title\": \"Label for claiming button\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"successfulClaimDevice\": {\n \"title\": \"Text message of successful device claiming\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"deviceNotFound\": {\n \"title\": \"Text message when device not found\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"failedClaimDevice\": {\n \"title\": \"Text message of failed device claiming\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n [\n \"widgetTitle\",\n \"labelClaimButon\",\n \"deviceSecret\",\n \"showLabel\",\n \"deviceLabel\",\n \"secretKeyLabel\"\n ],\n [\n \"deviceNotFound\",\n \"failedClaimDevice\",\n \"successfulClaimDevice\",\n \"requiredErrorDevice\",\n \"requiredErrorSecretKey\"\n ]\n ],\n \"groupInfoes\": [{\n \"formIndex\": 0,\n \"GroupTitle\": \"General settings\"\n }, {\n \"formIndex\": 1,\n \"GroupTitle\": \"Message settings\"\n }]\n}", + "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? latLabel : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n \n \n {{ settings.showLabel ? lngLabel : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-coordinate-specified' | translate }}\n
\n
\n
\n
", + "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-direction: column;\n flex: 1;\n}\n\n.grid__element.horizontal-alignment {\n flex-direction: row;\n}\n\n.grid__element:last-child {\n align-items: center;\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;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-button.getLocation {\n margin-right: 10px;\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.attribute-update-form mat-form-field{\n width: 100%;\n padding-right: 5px;\n}\n\n.attribute-update-form.small-width mat-form-field{\n width: 150px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", + "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet utils;\r\nlet translate;\r\n\r\nself.onInit = function() {\r\n self.ctx.ngZone.run(function() {\r\n init(); \r\n self.ctx.detectChanges(true);\r\n });\r\n};\r\n\r\n\r\nfunction init() {\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\r\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\r\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\r\n $scope.toastTargetId = 'input-widget' + utils.guid();\r\n settings = utils.deepClone(self.ctx.settings) || {};\r\n \r\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\r\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\r\n settings.showGetLocation = utils.defaultValue(settings.showGetLocation, true);\r\n settings.enableHighAccuracy = utils.defaultValue(settings.enableHighAccuracy, false);\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false; \r\n\r\n $scope.isHorizontal = (settings.inputFieldsAlignment === 'row');\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-coordinate-required');\r\n $scope.latLabel = utils.customTranslation(settings.latLabel, settings.latLabel) || translate.instant('widgets.input-widgets.latitude');\r\n $scope.lngLabel = utils.customTranslation(settings.lngLabel, settings.lngLabel) || translate.instant('widgets.input-widgets.longitude');\r\n\r\n $scope.attributeUpdateFormGroup = $scope.fb.group(\r\n {currentLat: [undefined, [$scope.validators.required,\r\n $scope.validators.min(-90),\r\n $scope.validators.max(90)]],\r\n currentLng: [undefined, [$scope.validators.required,\r\n $scope.validators.min(-180),\r\n $scope.validators.max(180)]]}\r\n );\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length > 1) {\r\n $scope.dataKeyDetected = true;\r\n for (let i = 0; i < datasource.dataKeys.length; i++) {\r\n if (datasource.dataKeys[i].type != \"attribute\"){\r\n $scope.isValidParameter = false;\r\n }\r\n if (datasource.dataKeys[i].name !== settings.latKeyName && datasource.dataKeys[i].name !== settings.lngKeyName){\r\n $scope.dataKeyDetected = false;\r\n }\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function () {\r\n $scope.isFocused = false;\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entity.id,\r\n 'SERVER_SCOPE',\r\n [\r\n {\r\n key: settings.latKeyName,\r\n value: $scope.attributeUpdateFormGroup.get('currentLat').value\r\n },{\r\n key: settings.lngKeyName,\r\n value: $scope.attributeUpdateFormGroup.get('currentLng').value\r\n }\r\n ]\r\n ).subscribe(\r\n function success() {\r\n $scope.originalLat = $scope.attributeUpdateFormGroup.get('currentLat').value;\r\n $scope.originalLng = $scope.attributeUpdateFormGroup.get('currentLng').value;\r\n if (settings.showResultMessage) {\r\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n }\r\n );\r\n }\r\n };\r\n\r\n $scope.changeFocus = function () {\r\n if ($scope.attributeUpdateFormGroup.get('currentLat').value === $scope.originalLat && $scope.attributeUpdateFormGroup.get('currentLng').value === $scope.originalLng) {\r\n $scope.isFocused = false;\r\n }\r\n };\r\n \r\n $scope.discardChange = function() {\r\n $scope.attributeUpdateFormGroup.setValue({\r\n 'currentLat': $scope.originalLat,\r\n 'currentLng': $scope.originalLng\r\n });\r\n $scope.isFocused = false;\r\n $scope.attributeUpdateFormGroup.markAsPristine();\r\n self.onDataUpdated();\r\n };\r\n \r\n $scope.disableButton = function () {\r\n return $scope.attributeUpdateFormGroup.get('currentLat').value === $scope.originalLat && $scope.attributeUpdateFormGroup.get('currentLng').value === $scope.originalLng || $scope.currentLng === null || $scope.currentLat === null;\r\n };\r\n \r\n $scope.getCoordinate = function() {\r\n if (navigator.geolocation) {\r\n navigator.geolocation.getCurrentPosition(showPosition, function (){\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.blocked-location'), \r\n 'bottom', 'left', $scope.toastTargetId);\r\n }, {\r\n enableHighAccuracy: settings.enableHighAccuracy\r\n });\r\n } else {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.no-support-geolocation'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n };\r\n \r\n function showPosition(position) {\r\n $scope.attributeUpdateFormGroup.setValue({\r\n currentLat: correctValue(position.coords.latitude),\r\n currentLng: correctValue(position.coords.longitude)\r\n });\r\n $scope.attributeUpdateFormGroup.markAsDirty();\r\n $scope.isFocused = true;\r\n }\r\n \r\n self.onResize();\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n for(let i = 0; i < self.typeParameters().maxDataKeys; i++){\r\n if(self.ctx.data[i].dataKey.name === self.ctx.settings.latKeyName && $scope.attributeUpdateFormGroup.get('currentLat').pristine){\r\n $scope.originalLat = self.ctx.data[i].data[0][1];\r\n $scope.attributeUpdateFormGroup.get('currentLat').patchValue(correctValue($scope.originalLat));\r\n } else if(self.ctx.data[i].dataKey.name === self.ctx.settings.lngKeyName && $scope.attributeUpdateFormGroup.get('currentLng').pristine){\r\n $scope.originalLng = self.ctx.data[i].data[0][1];\r\n $scope.attributeUpdateFormGroup.get('currentLng').patchValue(correctValue($scope.originalLng));\r\n }\r\n }\r\n self.ctx.detectChanges();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n};\r\n\r\nfunction correctValue(value) {\r\n if (typeof value !== \"number\") {\r\n return 0;\r\n }\r\n return value;\r\n}\r\n\r\nself.onResize = function() {\r\n $scope.smallWidthContainer = (self.ctx.$container && self.ctx.$container[0].offsetWidth < 320);\r\n $scope.changeAlignment = ($scope.isHorizontal && self.ctx.$container && self.ctx.$container[0].offsetWidth < 480);\r\n self.ctx.detectChanges();\r\n};\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 2,\r\n singleEntity: true\r\n };\r\n};\r\n\r\nself.onDestroy = function() {\r\n\r\n};", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"latitude\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"longitude\"\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"latLabel\": {\n \"title\": \"Label for latitude\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"lngLabel\": {\n \"title\": \"Label for longitude\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\": {\n \"title\": \"Show result message\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableHighAccuracy\": {\n \"title\": \"Use high accuracy\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showGetLocation\": {\n \"title\": \"Show button 'Get current location'\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"inputFieldsAlignment\": {\n \"title\": \"Input fields alignment\",\n \"type\": \"string\",\n \"default\": \"column\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"latKeyName\",\n \"lngKeyName\",\n \"enableHighAccuracy\",\n \"showGetLocation\",\n \"showResultMessage\",\n {\n \"key\": \"inputFieldsAlignment\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"column\",\n \"label\": \"Column (default)\"\n },\n {\n \"value\": \"row\",\n \"label\": \"Row\"\n }\n ]\n },\n \"showLabel\",\n \"latLabel\",\n \"lngLabel\",\n \"requiredErrorMessage\"\n ]\n}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"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\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"deviceSecret\":true,\"showLabel\":true},\"title\":\"Device claiming widget\",\"dropShadow\":true,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"enableDataExport\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "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\":\"Update server location attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { - "alias": "markers_placement_image_map", - "name": "Markers Placement - Image Map", + "alias": "markers_placement_google_maps", + "name": "Markers Placement - Google Maps", "descriptor": { "type": "latest", "sizeX": 8.5, - "sizeY": 6.5, + "sizeY": 6, "resources": [], "templateHtml": "", - "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('image-map', false, self.ctx, null, null, true);\n var createEntityLocation = {\n name: 'action.add',\n show: true,\n onAction: function($event) {\n self.ctx.map.selectEntity($event);\n },\n icon: 'add_location'\n };\n self.ctx.widgetActions = [createEntityLocation];\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('image-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('image-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n", + "templateCss": ".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 200px;\n white-space: nowrap;\n}", + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('google-map', false, self.ctx, null, true);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('google-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('google-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}

Delete\",\"markerImageSize\":34,\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"color\":\"#fe7569\",\"mapImageUrl\":\"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiCiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiCiAgIHhtbG5zOmlua3NjYXBlPSJodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy9uYW1lc3BhY2VzL2lua3NjYXBlIgogICB3aWR0aD0iMTEzNC41MTgzIgogICBoZWlnaHQ9Ijc2Mi43ODI0MSIKICAgaWQ9InN2ZzIiCiAgIHZlcnNpb249IjEuMSIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMC40OC41IHIxMDA0MCIKICAgc29kaXBvZGk6ZG9jbmFtZT0id2ljaGl0YW1hcC1ub2xpYi5zdmciPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM0IiAvPgogIDxzb2RpcG9kaTpuYW1lZHZpZXcKICAgICBpZD0iYmFzZSIKICAgICBwYWdlY29sb3I9IiNmZmZmZmYiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgYm9yZGVyb3BhY2l0eT0iMS4wIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTp6b29tPSIwLjM1IgogICAgIGlua3NjYXBlOmN4PSI4OS45MDc4NTciCiAgICAgaW5rc2NhcGU6Y3k9IjQ1My43ODI0MSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0icHgiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ibGF5ZXIxIgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEzNjYiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iNzIxIgogICAgIGlua3NjYXBlOndpbmRvdy14PSItNCIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iLTQiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgICBpbmtzY2FwZTpvYmplY3QtcGF0aHM9InRydWUiCiAgICAgaW5rc2NhcGU6c25hcC1nbG9iYWw9ImZhbHNlIgogICAgIHNob3dndWlkZXM9InRydWUiCiAgICAgaW5rc2NhcGU6Z3VpZGUtYmJveD0idHJ1ZSIKICAgICBmaXQtbWFyZ2luLXRvcD0iMCIKICAgICBmaXQtbWFyZ2luLWxlZnQ9IjAiCiAgICAgZml0LW1hcmdpbi1yaWdodD0iMCIKICAgICBmaXQtbWFyZ2luLWJvdHRvbT0iMCIgLz4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE3Ij4KICAgIDxyZGY6UkRGPgogICAgICA8Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+CiAgICAgICAgPGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+CiAgICAgICAgPGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPgogICAgICAgIDxkYzp0aXRsZT48L2RjOnRpdGxlPgogICAgICA8L2NjOldvcms+CiAgICA8L3JkZjpSREY+CiAgPC9tZXRhZGF0YT4KICA8ZwogICAgIGlua3NjYXBlOmxhYmVsPSJMYXllciAxIgogICAgIGlua3NjYXBlOmdyb3VwbW9kZT0ibGF5ZXIiCiAgICAgaWQ9ImxheWVyMSIKICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjcuMDcxNDI4LC0zMDcuOTAyOTkpIj4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDM3ODciCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMzY0ZTU5O3N0cm9rZS13aWR0aDoyLjk5OTk5OTc2O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmUiCiAgICAgICBkPSJtIDkwNi4wMzMxNSw3MDYuMTMzNjcgMy40MjkyLDE3Ljc5NTUyIE0gMjguNTcxNDI4LDc2NS4wNTA2NyBjIDE1MC40MzUyMDIsNi44MzM0MiAxNDYuMzkyMzIyLC0yNi4zMzQxNSAxNjYuNDM0NTQyLC0yOS4zMjAwOSAzNi4xNDM3NSwtNS4zODQ3NiAxMTQuMjg2NzYsLTYuNTI1NCAxNDguMzI1MDgsLTguNjIzNTQgNDMuMzc4MDgsLTIuNjczODUgMTQxLjc2MjIxLC0xMS4yMzA5OSAxODguODU1NzgsLTE5LjgzNDE4IDM5LjgxMTM4LC03LjI3Mjg0IDIyMS4zNjk5MSwtMC44NjIzNSAzMTkuMDcxNDEsLTAuODYyMzUgNzAuODI3MzUsMCAxNDYuOTE4NjcsLTEuNzI0NyAyMTguMTc1ODYsLTEuNzI0NyAtMzEuNjE5NywwIDExNy44NTUyLC0yLjU4NzA3IDg2LjIzNTUsLTIuNTg3MDcgbSAtMjUuMDkwNywtNjguMTI2MDYgYyAtNTIuNzk5NiwzNC43ODQ4NCAtNjUuODk1MSw1MS43NDg2NSAtOTUuNjM5LDgxLjQ5MjU4IC0yNC45MzEzLDI0LjkzMTI3IC0xNDAuMzk2NTMsLTE5LjEzOTIgLTE3OC45Mzg3MSwzNi42NTAwNyAtMTIuMjgxNCwxNy43NzcxNSAtNDcuMDAyNTcsNDYuNTQ2NTMgLTY1LjEwNzgzLDU5LjA3MTMzIC0yMC4xMDUsMTMuOTA4MTggLTU2LjAzNjcyLDQ0Ljk1NjY0IC02Ny43Njg4NSw3My4wNzgyNyAtNC44MDE0NywxMS41MDkwMiAtMTMuMzgwNDYsMzUuOTkyOTggLTIzLjQ0OTQ5LDQ2LjA2MjAxIC0xMC40OTY5OSwxMC40OTY5OSAtMzguMzc3MzMsNi4zODU2OSAtNDQuMDIzNDUsMTcuNjQ3NjQgLTE5LjAwNTAyLDM3LjkwODEyIC0yNS40NjUzLDEwMC45MjM1MiAtNjcuNjE3ODksMTAyLjA1MTAyIG0gMTkuMjgxNTEsLTYyNC4wMTQ2NCBjIDM0LjY1OTM0LC0xLjg3MzgyIDg0LjAyNzMzLDcuMzkxMzEgMTA5LjkwMDcxLC00LjI4NTQ1IDEzLjI4MTcyLC01Ljk5NDA4IDQxLjQwNzIxLC0yLjQ2MTM1IDY2LjgyODY2LC0yLjMyMDQ2IDM1LjMyMjM4LDAuMTk1NzggNjQuMzgyNDksMC42MzQ3NyAxMDEuOTE2Nyw1LjAyMzIgMjUuMDMwMzYsMi45MjY1IDQ0LjY2MjczLDM0LjI4NzIyIDU4LjUyNjk4LDUwLjY0MzkgMTcuMDk4NzgsMjAuMTcyNjggNjIuNzYzODYsLTEuNzE0NjcgNjYuMzA1NjYsMzIuMTM0MzMgNS4xMDI3LDQ4Ljc2NTg3IC02LjMyODQsNzguNjM3MjUgNi4xNDExLDk3LjM0MTUgMTkuOTY5MiwyOS45NTM3OSA1MC40ODY0LDE3Ljg1NTc5IDQ0LjYxOTMsODMuOTcxMTkgTSA1ODkuMTAyMjcsMzA5LjcyNzE1IGMgNC42NDM0NiwyMy43MjkyMyAxNS4wNjkwNCw3Mi43NzU3NSAxOS4wNjEyOCwxMzAuNjQyODggMC44NzIwNiwxMi42NDA0OCA1LjQ0NzE4LDI0Ljk5MjUzIDQuMjIyMzEsNDUuMjc3NTcgLTIuNTE3MjEsNDEuNjg3NSAtMTUuNzE3MDYsNDMuNjc3MjcgLTE1LjA5MTIyLDYwLjM2NDg2IDEuNDMxOTUsMzguMTgyMjQgMzAuNjEzNjEsOTMuODM3MTkgMzAuNjEzNjEsMTM5LjcwMTU0IDAsMjQuMTgwOCAtMi42Njk2NCwxMTUuMzkwNDUgNy4zMzAwMSwxMzUuMzg5NzYgMC4xNTkxMSwwLjMxODIxIDEwLjA2NDc2LDM1Ljg4MzMyIDEwLjc3OTQ1LDQ5LjE1NDI0IDAuOTQzNzgsMTcuNTI0NjkgLTI0LjQ3OCwzOS40NzAwOCAtMjguMDI2NTUsNDYuNTY3MTYgLTUuNDc3NywxMC45NTUzOSAtMzYuOTczMjQsMTAuODgxOTcgLTQwLjA5OTUsMjQuMTQ1OTUgLTMuODY4ODQsMTYuNDE0NTEgLTMuODY2Myw0My43OTczNSA0LjA0NjQ3LDU5LjQ0MTI5IG0gOTcuMzM3MzQsLTY5MS4wMDk0MSBjIC01LjAxMzMyLDM1LjUxNTk1IC00My42NTkwMSwxMS4zMTY1MiAtNTguNTM4NjEsMjMuNzgxMzEgLTIxLjMzMDE5LDE3Ljg2ODUyIC02Mi40OTk2NCwzMS40MzIxMiAtNzAuMTI0MzcsMzUuMzY3MDggLTM1LjA4NzYzLDE4LjEwNzkzIC0xMTAuNDcyMTUsLTE1LjE0MTk2IC0xMjUuNjE0MSw0LjI2ODQzIC0xNS45NTA2MywyMC40NDcwMyAtMC4wNzM1LDYxLjQ2NjQ4IC05LjE0NjY2LDg0LjE0OTI0IC02LjAzNTcsMTUuMDg5MjYgLTE4Ljg3NjcsMjMuMDE3MzQgLTI3LjQzOTk3LDMyLjkyNzk4IC0xOS43NDgyOSwyMi44NTU1NSAtNjkuOTc0MjgsNjkuODI0MTkgLTg0Ljc1OTA0LDEwMC4wMDM0NiAtNy40OTc0MSwxNS4zMDQwNCAtMy4yODQyNiw0NC40MjA0MSAtMy40NzA1Myw2My4zNDI4NCAtMC4xMjc5MywxMi45OTQxNCAtMC44MTAxNSwyMy4xMDM4NSAyLjQwMzQzLDI4LjI3NjE4IDQuOTYxNTgsNy45ODU4MSAyMy43MjA1LDI4LjExMjA3IDI0LjIzODY1LDUwLjYxMTQ5IDAuMjk0MTEsMTIuNzcxNDYgMC4wMTMzLDc4LjU5MTAxIDMuMDQ4ODgsODcuNjU1NDkgMi4zMTI1Niw2LjkwNTQ2IDQuMjIwMDQsMjYuNTY0OTcgMTAuMjEzNzcsMzYuNTg2NjIgMTEuMzU0MDEsMTguOTg0MTUgNC4zODczNyw0MC4xNTY2MiAyNy44OTczLDUzLjUwNzk1IDE5LjA1MDEyLDEwLjgxODU5IDQ2Ljg3NzgxLDEyLjIxODYyIDgxLjkyNjE4LDE0LjQ2MDU0IDMzLjcwMzQ1LDIuMTU1ODkgNjEuNTEyMTcsLTEuNDMwMzUgNzYuOTIwNzcsNi4xNDExIDExLjU4NTA4LDUuNjkyNjYgOC41ODE1MSwxNy45MzM0NCAxNC4yOTU0MSwyOS4zNjEyMyA1LjY0MDQyLDExLjI4MDg1IDMxLjUwMjYzLDExLjE1NjI3IDQxLjgwNDA5LDQzLjQ1NDg3IDcuNjA1OSwyMy44NDcxIDMuMDg1OTMsNDQuMTU2OSA2LjcwNzU1LDY1Ljg4NjYiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjY2Nzc3NzY2Njc3Nzc3NzY2Nzc3Nzc3NjY3Nzc3Njc3NzY2Nzc3Nzc3Nzc3Nzc3Nzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Im0gNDMuMjc3ODgxLDUxNy45NDY3OSBjIDAsMCAyMzAuODQ4Mjg5LC0zLjYzODA1IDI1MC4wMDg2MzksLTMuNjU4NjcgNy40ODIyMiwtMC4wMDggOC42MTk1NCw1LjE1MTk0IDE0LjAyMDksMTEuNDU4NjkgMjQuNTk2MDgsMjguNzE4OTMgOTMuOTA5NjYsMTEyLjkzNTg1IDkzLjkwOTY2LDExMi45MzU4NSIKICAgICAgIGlkPSJwYXRoMzc4OSIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJtIDM1Ljk2MDU1NSw1NzcuNzA0OTQgYyAwLDAgMTY1LjUyNDU2NSwtMS42ODQ1NCAyNDguNzc5NTY1LC0xLjY4NDU0IDQuOTQ3NDksMCA3LjcyOTkzLC0yLjg4MzMgMTAuNTM3NzEsLTUuNzI5NzcgOS42NjEwNywtOS43OTQxNiAyNS42MzE5OSwtMjguNTg5OTUgMjUuNjMxOTksLTI4LjU4OTk1IgogICAgICAgaWQ9InBhdGgzNzkxIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOiMzMzMzNjY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gMzguMzk5NjYzLDY0MS43MzE1NSA0MzEuNzA1OTMsNjM3LjQ2MzExIgogICAgICAgaWQ9InBhdGgzNzk1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOiMzMzMzNjY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gMzkuMDA5NDQyLDcwNC41Mzg1OSA1MjMuMTcyNTMsNjk3LjgzMTA0IgogICAgICAgaWQ9InBhdGgzNzk3IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzAzLjk1NzYyLDY4Mi41ODY2MSAxNDYuNzk1NDIsMS44MjkzMyBjIDEwLjUzNDAzLDAuMTMxMjcgMTQuMzQzNzQsLTIuNjM3MzkgMjUuNDg3MTUsLTYuMzcyOCAxMC40MTIxMiwtMy40OTAyNyAzMS40MjQxNSwtMi42OTg5NiA0MS4zODUzOCwtMi43NzM4NSBsIDQwNS41NjA3OSwtMy4wNDg5IgogICAgICAgaWQ9InBhdGgzNzk5IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgaWQ9InBhdGgzODA0IgogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDQyNi4yMTc5NCwzMTQuODkwOTggYyAyLjA2NzU0LDkuMDUyNzMgMS44NDE3Nyw1MS43Mjc3NyA2LjUwNzk0LDc0LjgzNDY2IDEuNjc0NzUsOC4yOTMzNiA4LjY3NTA4LDE0LjA2NTk4IDEwLjA1NTQxLDE0Ljg1ODYyIDQuOTAxNDcsMi44MTQ2MyAxMC44MTQ3OSw4LjE0OTgyIDEzLjA0NTc5LDE2LjA4ODMxIDYuNzU3NzksMjQuMDQ1OTEgMC44Nzk3Miw2OC40NTIxMiAwLjg3OTcyLDExMC42ODkzIDAsNi4wOTc4MiAxLjY2MDEsMzAuMTQ2NiAtMi4xNTU4OCwzMy45NjI1OSAtMi41NDA4NSwyLjU0MDgzIC0wLjI4MTYzLDEyLjk5MDY5IC0zLjQzNjc1LDE2LjE0Mzc3IGwgLTkuODQ5NDQsOS44NDMxMSBjIC0xMC4zNjcxNSwxMC4zNjA0NyAtMTEuNTkwMTcsNi41MjYxNCAtMTcuNzM4NDgsMTguODIyNzYgLTMuNTY3NzIsNy4xMzU0MyA1LjQwMjM1LDIwLjY3MjEgNy4zNTQzMiwyNC41NzYwMiAxLjkzMjE0LDMuODY0MyAtMS44NDIxNiw0Ljc3NzczIC0xLjc5MjM1LDcuNDQ2MjYgMC4yNTI4NiwxMy41NDQ4MyAyLjI5NzUsMzczLjkyNzEyIDIuMjk3NSwzNzMuOTI3MTIiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDM2NS4yNDAyMiw1MTkuNzc2MTIgNC4xMTU5OSw1MDIuMTUxNTgiCiAgICAgICBpZD0icGF0aDM4MDYiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAxMTYuNTMxNjUsNTA0LjE4Njk5IDMuODgwNTksMzEwLjk2NDM2IgogICAgICAgaWQ9InBhdGgzODMxIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDM4ODkiCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzE3LjY3NzYsNTc2LjQ4NTM5IDEzMC4xODc0MiwxLjUyNDQ0IGMgNC41MTA3OSwzLjI0MTY5IDIwLjM0NDcxLDcuOTY4NTMgMjcuNzQ0ODYsNC4yNjg0NCAzLjE1NTQ2LC0xLjU3NzcyIDkuNDE5LC01LjM4ODE3IDE0LjAyNDg5LC0zLjk2MzU1IDQuMjY2OTgsMS4zMTk4MSA2LjAxNjg5LDMuMTE2MzIgMTAuMzY2MjEsMy4wNDg4OSAxMC4zMDQwMywtMC4xNTk3NSAyMC4yMTE3LDAuMzg3NDEgMzAuNDg4ODYsMC4zMDQ4OSAxNzcuODkwOCwtMS40MjgyNyAzNTYuNTkwMzUsLTIuMTMyNDcgNTM0Ljc3NDU2LC0zLjA0ODg4IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2Nzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDc1LjMwNTAxLDU4Mi44ODgwNSBjIC0zLjQ0NDE4LDExLjM1MDY2IC0yLjEwMzQzLDEyLjQzMzczIDMuNjU4NjUsMjEuMDM3MzEgMy43OTQ0NSw1LjY2NTY0IDUwLjg2MjYxLDEzLjAzODQ1IDQxLjQ2NDg1LDI3LjEzNTA5IC0xMC41MzY5NywxNS44MDU0NyAtMjIuODk3NDUsLTUuNDc3NzIgLTMzLjg0MjYzLC0xLjgyOTMzIC01LjQ1MjM2LDEuODE3NDUgLTcuMzQ5MDEsNS40NTYzMSAtMy42NTg2Niw5LjE0NjY1IDIuODA2ODMsMi44MDY4NCA0LjA0OCwxLjgwMzk2IDYuNTIwMzQsNS4xMDA0MSIKICAgICAgIGlkPSJwYXRoMzkxMCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDMyLjAxMDgyLDYzNi44NTMzMyBjIDguMzE4OTksMTMuMTEwMTYgMTguODQ2MjEsMTQuNjM0NjUgMzUuNjcxOTYsMTQuNjM0NjUgMi45Mzg2NSwwIDcuODY5OTgsLTAuOTMzNzEgMTAuNjcxMTEsMCAxMS4zNTkxNywzLjc4NjM5IDI3LjE5Mzk4LDEwLjI3NTc3IDM2LjIwMTkzLDIxLjEyOTQ4IDguMjgwMDIsOS45NzY2MSAxMC4yNTI3OCwyMy44ODMwOCA3LjcwMjAyLDM3LjEwNDI0IC02LjE2OTg5LDMxLjk3OTk4IC0xNi43MTQzMSw1Ni45ODg1MyAtMTkuMDQzNTUsODYuNTY5MDUgLTEuMzQ3OTgsMTcuMTE4OCA0LjUwOTU3LDIyLjUzNTIyIDExLjA3MTQzLDMzLjkyODU3IDEwLjY3MDIzLDE4LjUyNjcyIDguNzI0NTMsMTQuMTk5NTUgOC41NzE0MywzNC4yODU3MiAtMC4xMzk2MywxOC4zMTk0NCAwLDYwLjI2Mzg1IDAsODAuNzE0MjkiCiAgICAgICBpZD0icGF0aDM5MTIiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDUyOC41MDgwNiw2NTguOTU3NzYgYyAtMTAuNjgxMjMsMC45MDQ1NCAtNy4xMDgwNCwtNS42MDI1NSAtMTAuODIzNTQsLTguMDc5NTYgLTQuNzg0NTQsLTMuMTg5NjkgLTEyLjIyNzA0LC0xLjI1MTA0IC0xNi43Njg4OCwtNS43OTI4OCAtMC42NjYxMiwtMC42NjYxMiAtOC44MDk2OSwtNC4xMDg3NyAtMTAuMTc0NDcsLTIuNzQzOTkgLTguMzY0NTksOC4zNjQ1OSAtMy4wNDg4OCwyMC41NTE4OCAtMy4wNDg4OCwzMy41Mzc3NCBsIDMuMDIyLDMzOS42OTc0MyIKICAgICAgIGlkPSJwYXRoMzkxNCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA1MTcuOTg5NDEsNjUxLjAzMDY1IGMgLTAuMjIxNzEsLTIuNzAxODQgMS45MDM0NiwtNS41NjIxMyAzLjM1Mzc3LC03LjAxMjQ1IDEuNzk5NDMsLTEuNzk5NDIgNi45MjI5NCwxLjAwNDE5IDguODQxNzgsLTAuOTE0NjYgMC4yODc2NSwtMC4yODc2NiAwLjg0MzI5LC0xMS4xNjQxIDAuMjI4NjYsLTEzLjU2NzUzIC0yLjA2NDgzLC04LjA3NDE2IC0yLjA1ODAxLC0yOC42NTY1OCAtMi4wNTgwMSwtMzguNzIwODYgbCAwLC03My4xNzMyNiIKICAgICAgIGlkPSJwYXRoMzkxNiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNTI4LjY2MDUsNjc1LjQyMTczIC0wLjQ1NzMzLC0zMS41NTU5NiIKICAgICAgIGlkPSJwYXRoMzk3NCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDc2Ni4zMTYyNSw1NzkuNjQ0MzEgMC40MzExOCwxMy43OTc2OCBjIDMuMTM2NDMsNC42NjkxNSAzLjAxODI0LDkuNjAwNjggMy4wMTgyNCwxNi4zODQ3NSBsIDAsMTU3LjM3OTgxIgogICAgICAgaWQ9InBhdGgzOTgyIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMTEyMi45MDAxLDc2NS45MTMwMyBjIC0yMDIuMzA2NjksNC42OTA1IC00MDMuNzQ0MDUsLTEuMTEzODEgLTYwNS45NTQ1NCwzLjM1MzkgLTEwLjg2MzYyLDAuMjQwMDIgLTMuMzYxNDcsLTguNTg2MyAtMjguNTM2OCwtOC41ODYzIgogICAgICAgaWQ9InBhdGgzOTg0IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA4NjAuMDA4MDUsNzM3LjA2NjUxIGMgMCwwIC05Ny40NDc1LDAuODU4MDYgLTE0Ny41Njg5MiwwLjg1ODA2IC01LjI2ODYxLDAgLTQuNTE1NDYsLTguMzI5ODYgLTcuMzAwODksLTguMzI5ODYgLTMuOTc0MzUsMCAtOC42MjkyNSwwLjAyMDEgLTEwLjUwOTQ4LDAuMDM1OSAtMi4zMzQ3NywwLjAxOTcgLTEuODEwOTQsOC4zNjU5NyAtNC4xNDU4LDguMzY2OTIgLTQ2LjE2ODk5LDAuMDE4OCAtMTY3LjQwNzY3LC0xLjMwNzk5IC0xNzUuMDUyNjMsLTEuMzA3OTkgLTQuNDI5NTUsMCAtOC41NzYyNywtNi40Mzk3MiAtMTMuMTMxOTgsLTYuNDM5NzIgLTEuMzYxMTUsMCAtNi4yMzg3MywwIC0xNC4zOTQ2NywwIgogICAgICAgaWQ9InBhdGgzOTg2IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJNIDY3NS4wMDcwMyw4MzEuMTc0MDIgNjc0LjM5NzI1LDMwOS40MDI5OSIKICAgICAgIGlkPSJwYXRoMzk4OCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDc5OS40MDE1NywzMTMuMDYxNjUgMS4yMTk1NSw0OTUuODY2NTMiCiAgICAgICBpZD0icGF0aDM5OTAiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA3MzYuNTk0NTIsMzEyLjQ1MTg4IC0xLjIxOTU1LDcxNi40ODgyMiIKICAgICAgIGlkPSJwYXRoMzk5MiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDUzMC4wMzA5NCw2NDMuNDU4NTkgMzkyLjM3MTU5LC0zLjAxODI1IgogICAgICAgaWQ9InBhdGg0MDQ4IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gODU5LjQ1MDYsMzE0LjkwMTI4IDEuMjkzNTQsNTA3Ljk4MDU4IgogICAgICAgaWQ9InBhdGg0MDUwIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjAuOTk5OTk5OTRweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gOTIxLjU0MDE3LDMxMC41ODk0OSAxLjcyNDcxLDUzMS43NTIyNyIKICAgICAgIGlkPSJwYXRoNDA1MiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDczNi4yODk2Myw0NTMuMzEwNCAxODUuNjc3MTUsLTAuMzA0ODkiCiAgICAgICBpZD0icGF0aDQxODciCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAxMDYwLjgxMDUsNTE0Ljk2NzY3IGMgMCwwIC0zNjMuMjgxMjYsLTUuNjI2MTggLTU0NC42NTA0MiwyLjUyMTc4IC00LjE3Nzc2LDAuMTg3NjkgLTEyLjUwMDQ0LDEuMDY3MTEgLTEyLjUwMDQ0LDEuMDY3MTEgLTEuNTcwOTUsMC4xMzQxIC0yLjAwMDkzLC0yLjMyNDk1IC0yLjU5MTU1LC0zLjUwNjIzIC0wLjA5NjcsLTAuMTkzNDMgLTcuMDYwODEsLTEuOTMzNCAtNy42MjIyMSwtMS4zNzE5OSAtMi44OTMxNCwyLjg5MzE0IC03LjYzMTY3LDQuMjQ4NjkgLTEyLjE5NTU1LDQuMTE2IEwgMzY5LjIwMTcsNTE0LjUzNjUiCiAgICAgICBpZD0icGF0aDQyNjEiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAzOTkuODE1MzEsNDc5LjYxMTEyIDExLjY0MTgsNS42MDUzIGMgMi45ODQxMiwxLjQzNjc5IDYuNTI4NzgsLTAuNDc3MTIgOS45MTcwOCwtMC40MzExOCBsIDEyNy4xOTczOSwxLjcyNDcxIgogICAgICAgaWQ9InBhdGg0MjYzIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gNTE5LjI1MTUxLDUxNy4xMjM1NyA1MTguODIwMzIsMzA4LjQzMzYyIgogICAgICAgaWQ9InBhdGg0MjY1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDMyLjkyNTQ5LDM4OS43MTQ5OCBjIDExLjA0NDk2LDAgMzUuNTMzMDcsMC42MTkyNyA0Mi41Nzk3OCwtMS4wMDM5NyA4LjQwNTIyLC0xLjkzNjE4IDcuMDY2LC02Ljk1Mzc4IDE0LjE5NzEyLC02Ljk1Mzc4IDcuODA5NSwwIDYuNTQyOTEsOC4wNjIzNyAyMC4xNDE3LDguMDYyMzcgMTMuOTkwNjgsMCA0NC45NzY4OSwwLjM3ODg2IDYzLjkzOTkyLDAuMzc4ODYgMTIuMDgzOTUsMCA4Mi4wMDI2NiwwLjMwNDg5IDkzLjYwMDgxLDAuMzA0ODkgOC43NjA0NywwIDEzLjE1OTcsLTIuMjg4MjcgMjEuMzQyMTksLTcuMDEyNDMgNy4xOTUxNSwtNC4xNTQxMyAyLjA1NDU5LC05LjQ5MTM3IDIwLjQyNzU0LC04Ljg0MTc3IDIzLjE0NTQsMC44MTgzMyAxMi42NDMzNCwxNC4wMjQ4NyAzMi4zMTgxOSwxNC4wMjQ4NyAyNS4zNTk1NCwwIDEzMC45OTkwMiwwIDE1MC45MTk4NSwwIDE0LjMzMjQ0LDAgLTQuMTE5MTEsLTEzLjExMDIxIDI5LjI2OTMsLTEzLjQxNTEiCiAgICAgICBpZD0icGF0aDQyNjkiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc3NzYyIgLz4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjU4OC42Nzk1NyIKICAgICAgIHk9IjczNS44MDQ2MyIKICAgICAgIGlkPSJ0ZXh0NDMxMCIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMiIKICAgICAgICAgeD0iNTg4LjY3OTU3IgogICAgICAgICB5PSI3MzUuODA0NjMiPkxpbmNvbG48L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjY4Ni4zOTg1IgogICAgICAgeT0iNzY1LjYyODQyIgogICAgICAgaWQ9InRleHQ0MzEwLTciCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNiIKICAgICAgICAgeD0iNjg2LjM5ODUiCiAgICAgICAgIHk9Ijc2NS42Mjg0MiI+SGFycnk8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjcwOS44NzE4MyIKICAgICAgIHk9Ii04MDIuMzc3MzgiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMi02LTgiCiAgICAgICAgIHg9IjcwOS44NzE4MyIKICAgICAgICAgeT0iLTgwMi4zNzczOCI+V29vZGxhd248L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjU2Mi4xMTkyNiIKICAgICAgIHk9Ii03NzEuOTY4MTQiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xLTkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtOC0yIgogICAgICAgICB4PSI1NjIuMTE5MjYiCiAgICAgICAgIHk9Ii03NzEuOTY4MTQiPkVkZ2Vtb29yPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTguMzA0ODciCiAgICAgICB5PSItNzM4LjM2NjQ2IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTciCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtOC0yLTkiCiAgICAgICAgIHg9IjU5OC4zMDQ4NyIKICAgICAgICAgeT0iLTczOC4zNjY0NiI+T2xpdmVyPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTIuMTIyODYiCiAgICAgICB5PSItNjc3LjIwMzk4IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTctNSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNi04LTItOS00IgogICAgICAgICB4PSI1OTIuMTIyODYiCiAgICAgICAgIHk9Ii02NzcuMjAzOTgiPkhpbGxzaWRlPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTcuMzI3MDkiCiAgICAgICB5PSItODYyLjYxNDA3IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTctNS0zIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMi02LTgtMi05LTQtMSIKICAgICAgICAgeD0iNTk3LjMyNzA5IgogICAgICAgICB5PSItODYyLjYxNDA3Ij5Sb2NrPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1ODcuMzcwMTgiCiAgICAgICB5PSItOTI2LjEzNjYiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xLTktNy01LTMtMiIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNi04LTItOS00LTEtMyIKICAgICAgICAgeD0iNTg3LjM3MDE4IgogICAgICAgICB5PSItOTI2LjEzNjYiPldlYmI8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9Ijg3MS4xNjEwMSIKICAgICAgIHk9IjYzNy41NzUyIgogICAgICAgaWQ9InRleHQ0NDY1IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NDY3IgogICAgICAgICB4PSI4NzEuMTYxMDEiCiAgICAgICAgIHk9IjYzNy41NzUyIj5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI4NzMuODMyMjgiCiAgICAgICB5PSI1NzcuMDMyNDciCiAgICAgICBpZD0idGV4dDQ0NjUtMyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDQ2Ny00IgogICAgICAgICB4PSI4NzMuODMyMjgiCiAgICAgICAgIHk9IjU3Ny4wMzI0NyI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgaWQ9InRleHQ0NDkwIgogICAgICAgeT0iNTEwLjI2MTgxIgogICAgICAgeD0iODc1Ljk2NjQ5IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbgogICAgICAgICB5PSI1MTAuMjYxODEiCiAgICAgICAgIHg9Ijg3NS45NjY0OSIKICAgICAgICAgaWQ9InRzcGFuNDQ5MiIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+MjFzdDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iODgxLjMxNjU5IgogICAgICAgeT0iNDUwLjE5ODc2IgogICAgICAgaWQ9InRleHQ0NDk0IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NDk2IgogICAgICAgICB4PSI4ODEuMzE2NTkiCiAgICAgICAgIHk9IjQ1MC4xOTg3NiI+Mjl0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNjE1Ljc5MjQ4IgogICAgICAgeT0iMzg3Ljc0NzE2IgogICAgICAgaWQ9InRleHQ0NDY1LTMtMSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDQ2Ny00LTEiCiAgICAgICAgIHg9IjYxNS43OTI0OCIKICAgICAgICAgeT0iMzg3Ljc0NzE2Ij4zN3RoPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ1MTkiCiAgICAgICB5PSI0ODEuNjUyODYiCiAgICAgICB4PSI0ODQuNjkwMzciCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9IjQ4MS42NTI4NiIKICAgICAgICAgeD0iNDg0LjY5MDM3IgogICAgICAgICBpZD0idHNwYW40NTIxIgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIj4yNXRoPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1NjMuMDQ2NzUiCiAgICAgICB5PSI1MTMuMzYxMzMiCiAgICAgICBpZD0idGV4dDQ1MjMiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1MjUiCiAgICAgICAgIHg9IjU2My4wNDY3NSIKICAgICAgICAgeT0iNTEzLjM2MTMzIj4yMXN0PC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ1MjciCiAgICAgICB5PSI1NzcuODk0ODQiCiAgICAgICB4PSI1NjUuOTcxNSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNTc3Ljg5NDg0IgogICAgICAgICB4PSI1NjUuOTcxNSIKICAgICAgICAgaWQ9InRzcGFuNDUyOSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDUzMSIKICAgICAgIHk9Ii00NjAuNzMzMTIiCiAgICAgICB4PSI0MzMuNTgwNzUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii00NjAuNzMzMTIiCiAgICAgICAgIHg9IjQzMy41ODA3NSIKICAgICAgICAgaWQ9InRzcGFuNDUzMyIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+QW1pZG9uPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI0MDUuNTMwOTgiCiAgICAgICB5PSItNTIzLjU0MDE2IgogICAgICAgaWQ9InRleHQ0NTM1IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDUzNyIKICAgICAgICAgeD0iNDA1LjUzMDk4IgogICAgICAgICB5PSItNTIzLjU0MDE2Ij5BcmthbnNhczwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDUzOSIKICAgICAgIHk9Ii0zNzIuNTg1OTQiCiAgICAgICB4PSI3NDUuNDg0NjIiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii0zNzIuNTg1OTQiCiAgICAgICAgIHg9Ijc0NS40ODQ2MiIKICAgICAgICAgaWQ9InRzcGFuNDU0MSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+V2VzdDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNTk2LjcyODMzIgogICAgICAgeT0iLTUzMS4yNTkyOCIKICAgICAgIGlkPSJ0ZXh0NDU0MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NDUiCiAgICAgICAgIHg9IjU5Ni43MjgzMyIKICAgICAgICAgeT0iLTUzMS4yNTkyOCI+V2FjbzwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU1NSIKICAgICAgIHk9Ii0xMjIuNTAyOTUiCiAgICAgICB4PSI1OTUuNDM0ODEiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii0xMjIuNTAyOTUiCiAgICAgICAgIHg9IjU5NS40MzQ4MSIKICAgICAgICAgaWQ9InRzcGFuNDU1NyIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+TWF6aWU8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjY5NS43NzI5NSIKICAgICAgIHk9IjE2Mi4wNjg3NyIKICAgICAgIGlkPSJ0ZXh0NDU1OSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMC43MDcxMDY3OCwwLjcwNzEwNjc4LC0wLjcwNzEwNjc4LDAuNzA3MTA2NzgsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NjEiCiAgICAgICAgIHg9IjY5NS43NzI5NSIKICAgICAgICAgeT0iMTYyLjA2ODc3Ij5ab288L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjI0MC41ODk5NyIKICAgICAgIHk9IjU3NC40NDU0MyIKICAgICAgIGlkPSJ0ZXh0NDU2MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDU2NSIKICAgICAgICAgeD0iMjQwLjU4OTk3IgogICAgICAgICB5PSI1NzQuNDQ1NDMiPjEzdGg8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU2NyIKICAgICAgIHk9IjUxMS42MzY2MyIKICAgICAgIHg9IjIwNi4wMzE3NSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNTExLjYzNjYzIgogICAgICAgICB4PSIyMDYuMDMxNzUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NjkiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPjIxc3Q8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjYyMC40NDMxMiIKICAgICAgIHk9Ii01MDYuNjgyMTkiCiAgICAgICBpZD0idGV4dDQ1NzEiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NTczIgogICAgICAgICB4PSI2MjAuNDQzMTIiCiAgICAgICAgIHk9Ii01MDYuNjgyMTkiPk5pbXM8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU4MyIKICAgICAgIHk9IjY5OC44NDAwOSIKICAgICAgIHg9IjM3MC4yMTY4NiIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNjk4Ljg0MDA5IgogICAgICAgICB4PSIzNzAuMjE2ODYiCiAgICAgICAgIGlkPSJ0c3BhbjQ1ODUiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1hcGxlPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSIzODQuMDg0MiIKICAgICAgIHk9IjY4MC44NTEzOCIKICAgICAgIGlkPSJ0ZXh0NDU5OSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDYwMSIKICAgICAgICAgeD0iMzg0LjA4NDIiCiAgICAgICAgIHk9IjY4MC44NTEzOCI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAzNjcuOTA4MTcsMTAwOS45NTk2IDI2My4wMTgzMywwIgogICAgICAgaWQ9InBhdGg0NjA1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDx0ZXh0CiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ2MDciCiAgICAgICB5PSItNDMzLjEzNzc2IgogICAgICAgeD0iNzM2LjI2NzQ2IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbgogICAgICAgICB5PSItNDMzLjEzNzc2IgogICAgICAgICB4PSI3MzYuMjY3NDYiCiAgICAgICAgIGlkPSJ0c3BhbjQ2MDkiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1lcmlkaWFuPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ5NzkiCiAgICAgICB5PSI2NDAuMjA1MjYiCiAgICAgICB4PSI1NzIuODMyMTUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9IjY0MC4yMDUyNiIKICAgICAgICAgeD0iNTcyLjgzMjE1IgogICAgICAgICBpZD0idHNwYW40OTgxIgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIj5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1NzUuMDg5NjYiCiAgICAgICB5PSI2NzAuOTAzNSIKICAgICAgIGlkPSJ0ZXh0NDk4MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDk4NSIKICAgICAgICAgeD0iNTc1LjA4OTY2IgogICAgICAgICB5PSI2NzAuOTAzNSI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNDk5LjQ4OTYyIgogICAgICAgeT0iMTAwOC42MDY5IgogICAgICAgaWQ9InRleHQ1MDQ3IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDQ5IgogICAgICAgICB4PSI0OTkuNDg5NjIiCiAgICAgICAgIHk9IjEwMDguNjA2OSI+NDd0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iMjE2LjY0NTQzIgogICAgICAgeT0iNzI1Ljk4Mjk3IgogICAgICAgaWQ9InRleHQ1MDUxIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDUzIgogICAgICAgICB4PSIyMTYuNjQ1NDMiCiAgICAgICAgIHk9IjcyNS45ODI5NyI+S2VsbG9nZzwvdHNwYW4+PC90ZXh0PgogICAgPGZsb3dSb290CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgaWQ9ImZsb3dSb290NTA1NSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6MThweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwyODcuMzYyMTgpIj48Zmxvd1JlZ2lvbgogICAgICAgICBpZD0iZmxvd1JlZ2lvbjUwNTciPjxyZWN0CiAgICAgICAgICAgaWQ9InJlY3Q1MDU5IgogICAgICAgICAgIHdpZHRoPSIzNDMuNTcxNDQiCiAgICAgICAgICAgaGVpZ2h0PSIxMDMuNTcxNDMiCiAgICAgICAgICAgeD0iMTkuMjg1NzE1IgogICAgICAgICAgIHk9IjE3LjE0Mjg1NyIKICAgICAgICAgICBzdHlsZT0iZm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIgLz48L2Zsb3dSZWdpb24+PGZsb3dQYXJhCiAgICAgICAgIGlkPSJmbG93UGFyYTUwNjEiPjwvZmxvd1BhcmE+PC9mbG93Um9vdD4gICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDYwNy03IgogICAgICAgeT0iLTUwOC4xODk3MyIKICAgICAgIHg9Ijc3NC44NzU2MSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iLTUwOC4xODk3MyIKICAgICAgICAgeD0iNzc0Ljg3NTYxIgogICAgICAgICBpZD0idHNwYW40NjA5LTciCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1jQ2xlYW48L3RzcGFuPjwvdGV4dD4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzY0LjE1OTk5LDY1OC40Mjg5MSAyOTkuNTEwMjMsLTEuMDEwMTYgYyA2LjQ5ODcyLC0wLjAyMTkgNi45NzcxOSw5LjI1NDEyIDE2LjU5NjMxLDkuMzkyNDcgMTIuMDU0MjcsMC4xNzMzOSAyOS4xMTA4MywtMC41MzU3MiA1NC4xMTQzNywtMC4zMDExIgogICAgICAgaWQ9InBhdGg1NDQwIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjg3LjM2MjE4KSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjM3My45OTMwNCIKICAgICAgIHk9Ijk0NC4zNTc1NCIKICAgICAgIGlkPSJ0ZXh0NTA0Ny05IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDQ5LTMiCiAgICAgICAgIHg9IjM3My45OTMwNCIKICAgICAgICAgeT0iOTQ0LjM1NzU0Ij5NYWNBcnRodXI8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ2MDctNy0xIgogICAgICAgeT0iLTQ5MC4yNDU5NyIKICAgICAgIHg9Ijc4MC44NDYwNyIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iLTQ5MC4yNDU5NyIKICAgICAgICAgeD0iNzgwLjg0NjA3IgogICAgICAgICBpZD0idHNwYW40NjA5LTctOSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+U2VuZWNhPC90c3Bhbj48L3RleHQ+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDM2Ny42OTU1Myw1MzcuMjEwNiAxNDEuMjgzMDMsLTEuMDEwMTUgYyA2LjQ4OTk5LC0wLjA0NjQgMTIuNzgxMTQsNy4yMzU0NSAxOS4xOTI5LDcuMzIzNiA1NS45MjM2MiwwLjc2ODkgMTU4LjY4OTk3LC0wLjE3MzMzIDIzNi41MTQwMiwtMS4wMTAxNSA3LjgzOTU2LC0wLjA4NDMgMjIuNjMxNDcsLTE5Ljg1MzU1IDMwLjMwNDU3LC0yMC40NTU1OSAyMi4yNjU4OSwtMS4zNTE4MSA0NS4xNzk0NSwtMC41MDUwNyA2Ny42ODAyMiwtMC41MDUwNyAxNi4xNDczMSwtMC42MzI0MSAzLjYxMDE2LDIwLjcwODEzIDI2Ljc2OTA0LDIwLjcwODEzIGwgMjQzLjQ0Njc5LC0xLjAxMDE2IgogICAgICAgaWQ9InBhdGg1NDk2IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjg3LjM2MjE4KSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzY2NjY2MiIC8+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI2ODUuMjA4MTMiCiAgICAgICB5PSI4MjcuNTMwODIiCiAgICAgICBpZD0idGV4dDQzMTAtNy04IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtNiIKICAgICAgICAgeD0iNjg1LjIwODEzIgogICAgICAgICB5PSI4MjcuNTMwODIiPlBhd25lZTwvdHNwYW4+PC90ZXh0PgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0iTSA1NTQuMjg1NzIsNzIxLjQyODU3IDU1MCw1NDMuMjE0MjkgNTQ3LjE0Mjg2LDEwMi41IDU0Ni43ODU3MiwyMy4yMTQyODUiCiAgICAgICBpZD0icGF0aDU1MTkiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwyODcuMzYyMTgpIiAvPgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNTI5LjYyNTMxIgogICAgICAgeT0iLTU1MC44NDc3OCIKICAgICAgIGlkPSJ0ZXh0NDU0My01IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDU0NS0wIgogICAgICAgICB4PSI1MjkuNjI1MzEiCiAgICAgICAgIHk9Ii01NTAuODQ3NzgiPkJyb2Fkd2F5PC90c3Bhbj48L3RleHQ+CiAgPC9nPgo8L3N2Zz4K\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"showTooltip\":true,\"autocloseTooltip\":true,\"showTooltipAction\":\"click\",\"defaultCenterPosition\":[0,0]},\"title\":\"Markers Placement - Image Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"id\":\"c39f512a-21c6-6b06-3aa1-715262c6553d\",\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var $rootScope = widgetContext.$scope.$injector.get('$rootScope');\\nvar entityDatasource = widgetContext.map.subscription.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.saveMarkerLocation(entityDatasource[0],\\n widgetContext.map.locations[0], {\\n \\\"lat\\\": null,\\n \\\"lng\\\": null\\n }).then(function succes() {\\n $rootScope.$broadcast('widgetForceReInit');\\n });\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}

Delete\",\"markerImageSize\":34,\"gmDefaultMapType\":\"roadmap\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"colorFunction\":\"\\n\",\"color\":\"#fe7569\",\"showTooltip\":true,\"autocloseTooltip\":true,\"defaultCenterPosition\":\"0,0\",\"showTooltipAction\":\"click\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"zoomOnClick\":true,\"defaultZoomLevel\":5,\"provider\":\"google-map\",\"showCoverageOnHover\":true,\"animate\":true,\"maxClusterRadius\":80,\"removeOutsideVisibleBounds\":true,\"mapProvider\":\"HERE.normalDay\",\"draggableMarker\":true,\"editablePolygon\":true,\"mapPageSize\":16384,\"showPolygon\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${coordinates|ts:7}

Delete\",\"showPolygonTooltip\":false},\"title\":\"Markers Placement - Google Maps\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id;\\n });\\n\\nwidgetContext.map.setMarkerLocation(entityDatasource[0], null, null).subscribe();\",\"id\":\"8d3c0156-0a14-7a6f-0ddd-0ec16b9ffc91\"},{\"name\":\"delete_polygon\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.savePolygonLocation(entityDatasource[0], null).subscribe();\",\"id\":\"46bf69cd-8906-234c-a879-e2e4c92f5b67\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}" } }, { - "alias": "markers_placement_openstreetmap", - "name": "Markers Placement - OpenStreetMap", + "alias": "update_shared_location_attribute", + "name": "Update shared location attribute", "descriptor": { "type": "latest", - "sizeX": 8.5, - "sizeY": 6, + "sizeX": 7.5, + "sizeY": 3, "resources": [], - "templateHtml": "", - "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('openstreet-map', false, self.ctx, null, null, true);\n var createEntityLocation = {\n name: 'action.add',\n show: true,\n onAction: function($event) {\n self.ctx.map.selectEntity($event);\n },\n icon: 'add_location'\n };\n self.ctx.widgetActions = [createEntityLocation];\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('openstreet-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('openstreet-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{}", + "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? latLabel : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n \n \n {{ settings.showLabel ? lngLabel : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n \n
\n
\n \n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-coordinate-specified' | translate }}\n
\n
\n
\n
", + "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-direction: column;\n flex: 1;\n}\n\n.grid__element.horizontal-alignment {\n flex-direction: row;\n}\n\n.grid__element:last-child {\n align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n margin: 0;\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;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-button.getLocation {\n margin-right: 10px;\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.attribute-update-form mat-form-field{\n width: 100%;\n padding-right: 5px;\n}\n\n.attribute-update-form.small-width mat-form-field{\n width: 150px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", + "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet utils;\r\nlet translate;\r\n\r\nself.onInit = function() {\r\n self.ctx.ngZone.run(function() {\r\n init(); \r\n self.ctx.detectChanges(true);\r\n });\r\n};\r\n\r\n\r\nfunction init() {\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\r\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\r\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\r\n $scope.toastTargetId = 'input-widget' + utils.guid();\r\n settings = utils.deepClone(self.ctx.settings) || {};\r\n \r\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\r\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\r\n settings.showGetLocation = utils.defaultValue(settings.showGetLocation, true);\r\n settings.enableHighAccuracy = utils.defaultValue(settings.enableHighAccuracy, false);\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false; \r\n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\r\n\r\n $scope.isHorizontal = (settings.inputFieldsAlignment === 'row');\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-coordinate-required');\r\n $scope.latLabel = utils.customTranslation(settings.latLabel, settings.latLabel) || translate.instant('widgets.input-widgets.latitude');\r\n $scope.lngLabel = utils.customTranslation(settings.lngLabel, settings.lngLabel) || translate.instant('widgets.input-widgets.longitude');\r\n\r\n $scope.attributeUpdateFormGroup = $scope.fb.group(\r\n {currentLat: [undefined, [$scope.validators.required,\r\n $scope.validators.min(-90),\r\n $scope.validators.max(90)]],\r\n currentLng: [undefined, [$scope.validators.required,\r\n $scope.validators.min(-180),\r\n $scope.validators.max(180)]]}\r\n );\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType === 'DEVICE') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n \r\n $scope.entityDetected = true;\r\n }\r\n } else {\r\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\r\n }\r\n }\r\n if (datasource.dataKeys.length > 1) {\r\n $scope.dataKeyDetected = true;\r\n for (let i = 0; i < datasource.dataKeys.length; i++) {\r\n if (datasource.dataKeys[i].type != \"attribute\"){\r\n $scope.isValidParameter = false;\r\n }\r\n if (datasource.dataKeys[i].name !== settings.latKeyName && datasource.dataKeys[i].name !== settings.lngKeyName){\r\n $scope.dataKeyDetected = false;\r\n }\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function () {\r\n $scope.isFocused = false;\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entity.id,\r\n 'SHARED_SCOPE',\r\n [\r\n {\r\n key: settings.latKeyName,\r\n value: $scope.attributeUpdateFormGroup.get('currentLat').value\r\n },{\r\n key: settings.lngKeyName,\r\n value: $scope.attributeUpdateFormGroup.get('currentLng').value\r\n }\r\n ]\r\n ).subscribe(\r\n function success() {\r\n $scope.originalLat = $scope.attributeUpdateFormGroup.get('currentLat').value;\r\n $scope.originalLng = $scope.attributeUpdateFormGroup.get('currentLng').value;\r\n if (settings.showResultMessage) {\r\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n }\r\n );\r\n }\r\n };\r\n\r\n $scope.changeFocus = function () {\r\n if ($scope.attributeUpdateFormGroup.get('currentLat').value === $scope.originalLat && $scope.attributeUpdateFormGroup.get('currentLng').value === $scope.originalLng) {\r\n $scope.isFocused = false;\r\n }\r\n };\r\n \r\n $scope.discardChange = function() {\r\n $scope.attributeUpdateFormGroup.setValue({\r\n 'currentLat': $scope.originalLat,\r\n 'currentLng': $scope.originalLng\r\n });\r\n $scope.isFocused = false;\r\n $scope.attributeUpdateFormGroup.markAsPristine();\r\n self.onDataUpdated();\r\n };\r\n \r\n $scope.disableButton = function () {\r\n return $scope.attributeUpdateFormGroup.get('currentLat').value === $scope.originalLat && $scope.attributeUpdateFormGroup.get('currentLng').value === $scope.originalLng || $scope.currentLng === null || $scope.currentLat === null;\r\n };\r\n \r\n $scope.getCoordinate = function() {\r\n if (navigator.geolocation) {\r\n navigator.geolocation.getCurrentPosition(showPosition, function (){\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.blocked-location'), \r\n 'bottom', 'left', $scope.toastTargetId);\r\n }, {\r\n enableHighAccuracy: settings.enableHighAccuracy\r\n });\r\n } else {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.no-support-geolocation'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n };\r\n \r\n function showPosition(position) {\r\n $scope.attributeUpdateFormGroup.setValue({\r\n currentLat: correctValue(position.coords.latitude),\r\n currentLng: correctValue(position.coords.longitude)\r\n });\r\n $scope.attributeUpdateFormGroup.markAsDirty();\r\n $scope.isFocused = true;\r\n }\r\n \r\n self.onResize();\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n for(let i = 0; i < self.typeParameters().maxDataKeys; i++){\r\n if(self.ctx.data[i].dataKey.name === self.ctx.settings.latKeyName && $scope.attributeUpdateFormGroup.get('currentLat').pristine){\r\n $scope.originalLat = self.ctx.data[i].data[0][1];\r\n $scope.attributeUpdateFormGroup.get('currentLat').patchValue(correctValue($scope.originalLat));\r\n } else if(self.ctx.data[i].dataKey.name === self.ctx.settings.lngKeyName && $scope.attributeUpdateFormGroup.get('currentLng').pristine){\r\n $scope.originalLng = self.ctx.data[i].data[0][1];\r\n $scope.attributeUpdateFormGroup.get('currentLng').patchValue(correctValue($scope.originalLng));\r\n }\r\n }\r\n self.ctx.detectChanges();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n};\r\n\r\nfunction correctValue(value) {\r\n if (typeof value !== \"number\") {\r\n return 0;\r\n }\r\n return value;\r\n}\r\n\r\nself.onResize = function() {\r\n $scope.smallWidthContainer = (self.ctx.$container && self.ctx.$container[0].offsetWidth < 320);\r\n $scope.changeAlignment = ($scope.isHorizontal && self.ctx.$container && self.ctx.$container[0].offsetWidth < 480);\r\n self.ctx.detectChanges();\r\n};\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 2,\r\n singleEntity: true\r\n };\r\n};\r\n\r\nself.onDestroy = function() {\r\n\r\n};", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"latitude\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"longitude\"\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"latLabel\": {\n \"title\": \"Label for latitude\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"lngLabel\": {\n \"title\": \"Label for longitude\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\": {\n \"title\": \"Show result message\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableHighAccuracy\": {\n \"title\": \"Use high accuracy\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showGetLocation\": {\n \"title\": \"Show button 'Get current location'\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"inputFieldsAlignment\": {\n \"title\": \"Input fields alignment\",\n \"type\": \"string\",\n \"default\": \"column\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"latKeyName\",\n \"lngKeyName\",\n \"enableHighAccuracy\",\n \"showGetLocation\",\n \"showResultMessage\",\n {\n \"key\": \"inputFieldsAlignment\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"column\",\n \"label\": \"Column (default)\"\n },\n {\n \"value\": \"row\",\n \"label\": \"Row\"\n }\n ]\n },\n \"showLabel\",\n \"latLabel\",\n \"lngLabel\",\n \"requiredErrorMessage\"\n ]\n}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.7867521952070078,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.7040053227577256,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}

Delete\",\"markerImageSize\":34,\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"color\":\"#fe7569\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"showTooltip\":true,\"autocloseTooltip\":true,\"defaultCenterPosition\":[0,0],\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"showTooltipAction\":\"click\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"zoomOnClick\":true,\"showCoverageOnHover\":true,\"animate\":true,\"maxClusterRadius\":80,\"removeOutsideVisibleBounds\":true,\"defaultZoomLevel\":5},\"title\":\"Markers Placement - OpenStreetMap\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"id\":\"54c293c4-9ca6-e34f-dc6a-0271944c1c66\",\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var $rootScope = widgetContext.$scope.$injector.get('$rootScope');\\nvar entityDatasource = widgetContext.map.subscription.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.saveMarkerLocation(entityDatasource[0],\\n widgetContext.map.locations[0], {\\n \\\"lat\\\": null,\\n \\\"lng\\\": null\\n }).then(function succes() {\\n $rootScope.$broadcast('widgetForceReInit');\\n });\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}" + "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\":\"Update shared location attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { - "alias": "markers_placement_google_maps", - "name": "Markers Placement - Google Maps", + "alias": "web_camera_input", + "name": "Photo camera input", "descriptor": { "type": "latest", - "sizeX": 8.5, - "sizeY": 6, + "sizeX": 7.5, + "sizeY": 3, "resources": [], - "templateHtml": "", - "templateCss": ".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 200px;\n white-space: nowrap;\n}", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('google-map', false, self.ctx, null, null, true);\n var createEntityLocation = {\n name: 'action.add',\n show: true,\n onAction: function($event) {\n self.ctx.map.selectEntity($event);\n },\n icon: 'add_location'\n };\n self.ctx.widgetActions = [createEntityLocation];\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('google-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('google-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{}", + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.photoCameraInputWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Photo Camera\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"imageFormat\": {\n \"title\": \"Image Format\",\n \"type\": \"string\",\n \"default\": \"image/png\"\n },\n \"imageQuality\":{\n \"title\":\"Image quality that use lossy compression such as jpeg and webp\",\n \"type\":\"number\",\n \"default\": 0.92,\n \"min\": 0,\n \"max\": 1\n },\n \"maxWidth\": {\n \"title\": \"The maximal image width\",\n \"type\": \"number\",\n \"default\": 640\n }, \n \"maxHeight\": {\n \"title\": \"The maximal image heigth\",\n \"type\": \"number\",\n \"default\": 480\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n {\n \"key\": \"imageFormat\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"image/jpeg\",\n \"label\": \"JPEG\"\n },\n {\n \"value\": \"image/png\",\n \"label\": \"PNG\"\n },\n {\n \"value\": \"image/webp\",\n \"label\": \"WEBP\"\n }\n ]\n },\n \"imageQuality\",\n \"maxWidth\",\n \"maxHeight\"\n ]\n}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}

Delete\",\"markerImageSize\":34,\"gmDefaultMapType\":\"roadmap\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"colorFunction\":\"\\n\",\"color\":\"#fe7569\",\"showTooltip\":true,\"autocloseTooltip\":true,\"defaultCenterPosition\":[0,0],\"showTooltipAction\":\"click\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"zoomOnClick\":true,\"gridSize\":60,\"defaultZoomLevel\":5},\"title\":\"Markers Placement - Google Maps\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"id\":\"8d3c0156-0a14-7a6f-0ddd-0ec16b9ffc91\",\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var $rootScope = widgetContext.$scope.$injector.get('$rootScope');\\nvar entityDatasource = widgetContext.map.subscription.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.saveMarkerLocation(entityDatasource[0],\\n widgetContext.map.locations[0], {\\n \\\"lat\\\": null,\\n \\\"lng\\\": null\\n }).then(function succes() {\\n $rootScope.$broadcast('widgetForceReInit');\\n });\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}" + "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\":\"Web 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\":{}}" } } ] diff --git a/application/src/main/data/json/system/widget_bundles/maps.json b/application/src/main/data/json/system/widget_bundles/maps.json index 82da974cba..7e895b7bdd 100644 --- a/application/src/main/data/json/system/widget_bundles/maps.json +++ b/application/src/main/data/json/system/widget_bundles/maps.json @@ -6,131 +6,147 @@ }, "widgetTypes": [ { - "alias": "google_maps", - "name": "Google Maps", + "alias": "route_map_tencent_maps", + "name": "Route Map - Tencent Maps", "descriptor": { - "type": "latest", + "type": "timeseries", "sizeX": 8.5, "sizeY": 6, "resources": [], "templateHtml": "", "templateCss": ".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 200px;\n white-space: nowrap;\n}", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('google-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('google-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('google-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('tencent-map', true, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('tencent-map', true);\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('tencent-map', true);\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermomether\\\";\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"markerImageSize\":34,\"gmDefaultMapType\":\"roadmap\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"useColorFunction\":true,\"markerImages\":[\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAwgSURBVGiB7Zt5cBT3lce/v18fc89oRoPEIRBCHIUxp2ywCAgIxLExvoidZIFNxXE2VXHirIO3aqtSseM43qpNeZfYKecox3bhpJykYgdjDkU2mBAB5vCamMNYAgQyURBCoxnNPd39O/aP7hGSEUR24L/uqqf+zfR77/Pe69/Rv6kWwcgPLRIJfZUAa7xez2xd90QBwDSNZKlkHJHAK+l09mUA7BP4vPpRUVExMVoRef+L998njxx9X57vPi/PnTsnO850yPaT7XLXrrflqjtWymhF+HA0Gp0wEp/kHymEQqG4ptJDGzf+um5RUxMSiV7Z3Lyt88L5nozgHJWj4pGmpqZav99PWve04onHHuswmViQzWb7ruZX+Udgv8/z3A+f/NGye1evxssvb+wo5PMfTZs6bfqcuXNHL7hlweh58+ZVAOTUpk2b0p9dvjyqqmrs/b8ejpUMc+unzjgUCsXjsYruE+2n1JY/NedM0zCi0VjA7/d7/f4AAgE//H4/vF4fOjvP9h5695C/oaEhcN/q1SyTzVdnMpnklXzTq4EplUsXfmaRCgC7du3cOn78+KfGj59Add3z1Md1vV7vqPa2D1sA4MYbZ6qUiqVX9X21i4TQcfX19QCA6urquN/vn0kAPRQKpYbTnzRpUhgAampqAEFrPjVYSql7fD4AgK5r2tV0AcDj8WkAoOk6JJGeTw2+nocLdsEu2AW7YBfsgl2wC3bBLtgFu2AX7IJdsAt2wS7YBbtgF+yCXbALdsEu2AW7YBfsgl2wC76mh/ppjIQgXVloPxVSBRV0rBe455P6+kTKBYF3tonxY/IWarry7DvI298Tgp0PR9RzACaN1NeIS100+EdvKXW3cMZvF8wCK10Sq2it2NAzakmukP/wmoP/KuId3BRUMg5uCfCSNVSKVn1rNto7Un8jLrUVqJ4Fi2eEQiEYBzOsy3SYL37TNQdzi8Q5FxkqJIQBsNLlYMGF/zqAJWBxSEogDAY+DJibYqTuRg4WFgO3OKhCYTExbKk5G/mbkSPP2DQhLA5IO/NhSz1MMP882BDgnAFQwdiVSs2vPVhYDIJLUMkBgw1favM6lJoZDDAYhKbAYsOX+rqAhcXAuQSIAKzhSy2vS8YmB7NYH4WCfM7kw5VaWtdpOO3bfWZJZVXgPxMX898bVsm6RhkTIseX29yyIErm/J5z5vwr6pvmsLYjBgeDwSpVJS/OmT1n1de+9qANZgLc4q9Dyj2qQhUhSSUAUCL7GBcchCymTEYBYNWqVXj30MGHT586PZEJ+WAul7ts8bjspd9QKDRNU2nz4z94YtI3H3oI+XwB//3j/9m77eRUUJ9/0eh4APGoDz6vCi4ksgUTmYyBC4k8RLGwtzF+EGu+tHqRqqrYtm0rXnzhhQ7G5cpsNnvyiuBIJFKnqvSd55772eilS5fhwIH9ye+/dPaEf1T9otW3T8GtiyYgGNBBymYEgLSbvakidu8/h01vnkYhcab1gcVs5tx5c6PHjh7DU0/9qFsINPb3939UZg28X11dXR0Qwtr9g8efqGtc+Bn89re/O7FhR9BXNaFm+n98uxHTZ1SDKQqKAihweZlITUVtXQwNs8fg+Bmzdk+bnmPdf/7bwsbGeO2ECaED+9/5XCxWuTGbzVpDwJpGNtx+28o77rr7bmzZsu3k7z+cMlHzeiPrvnoTwtVhFAVQHAZY4HBEoiAAeDXUjI/gyJGeQEd6TFj2tHYuXNgYy2azVe0fngiWDLNloHNFo4FZkXDsoTVr1+KD4x8U/3Ci1qP5PV7N74FeFUbClKDEriy57A5JANL5a68hnqoINL8OAPqbXbNp7clTxTVr1/oOHjr0MFXxq2Qy9wEFACnoY//6la9QAHj+9Q/eUL2RWkVXoWgqkhZBypRImkDKBFIWkLIk+h1JWdL+zrmeNCWSDFB0DYquQvWG637TcnozAKxbt45yTr8PAGowGBwVDAbvmT9/Pvbu3dddijV9WdUUUE0BUQm6kwaCYe+ljK/w8ruUdsYCBLlMEUQhoJoCygWM+LIvHTx4sGfevIbqYMD3BSFkJVUUrG5oaFABoPXwhd1UVUBVahtpKtoOnEV/gSHHgBwDso5c6XO6yNF24CNQTbV9qBRUUenuwz1/BoCZM2dplOJeSggWL1myFEII9IeXziIKBVUUW1QKo2Ci41Anei9kkWcY6Ex5R8qfc0wi0ZPF6QNnYeQNB2j7IQpFOtg0WwiBxoWNIBKLVQI6Z8rUqTh69FiWaFNmEIWgLFShoM5TZbIzgVxvFp6ID5rfA6JQgBAIxsGLJkrpAsycAcH4gN1gX0QPTW9vP5Grr58cJJTOpbqmjgWAnp6ei4QSEEJAKAGh1BbHCS2DLAFmMAgmICwObjDnyYMMAtJL9oN89vRc7KWUQtOUsSqhSggA8sWivSEh9qBxTiCEAGRwQARUVaB67Hf5pZAQlA0Ayrq2LTCogVyhlLURNEw55yYABP2+4ED3vHSClBKQ9jiFdHqvEBCMQzAOKYSt6/RqSGnbDPJRbgT93hAAcM4NyhjrBYDKylhswEEZJgYJFxDchnGTwSqasIomuMnsIDiH5GKIzUAQTsCVlZUxB9xLIUVbKpVEff3kiLTMfimEA7HP5bZgHMJ07mnJAiuaYEXT3jcZDMLkTgBD7exgBKRp9NfVTQwnk0kIKduoJGRH8/ZmhMNh4skc3DnEkDlAi4GbtjDDguVAmZM1M6yB68JyKsCGBqD373s7GAySnTt3gBDyFhWCvPHee/8HAJhTU5g0BMg4uMXBTT4AZSUTrGjBKpiwCnablQbDbZuyfTmAuRPMegA4euQopCRbaCaTOd2XSLzX3d2Nu+64bR7PnP3LJSCDMBm4YW9FWcmyQYMytsW+Zpfdsm1MdimAdMc7K29bMedCdzeSyeS76XT6jLNI4PGf/+w5aLqOu25IjOOWKcSg0jJjcLZ2ecsZD5TdybqsOxC0ZYpbJ58frek6nn/+eVBJHgecjXkqk2nu7Ozcdfz4cdx556rJN5C3m8v3jBt2xpdnazjysawNy5lUbKkrbmtZsWL5pGNHj6Or62+7k5lMy5CFNRQKTfN6tAMvvvhSRe3EOqx/4oXXLvia7qO6CsVZrey5154KB5YpKSG5tHs+5/ZsZnEIk6Ei1fLH73373i/09fXi0fWPpgyTLchkMqeGgAEgHA5/vjJWsf2PmzYr1dXV+K8fP7vjLxduWkY8ilpetQZPg+UJxh63lzqlNDi7gTa3fuPraz6bzxXw79/5FutP51am0+kdZdaQ/2kzDKNDUci51179w8pbP3er8sAD6+pnVCWy+/fs21LAqBnlMT50qJXFLq2a2L/5gaVy7N133j69u7sb67/7iFHIFf4tlU6/Ppg1kLGU8hYAywBMeOWV33gfXb9+1Q+ffDL+4Ne/AcYY/tS8PbV5++4Dhy+MopY2ZrLiidQDgDBSp5TS+Y7psS65ZOHsW26++eYosxje2PwGNm586eKzz/x027+sXWsBOAfgbULIQQAgUspaAA8BGAfnsamrq4u0tZ0Q333kkdGmZS3f8JNnlBXLV0AOilRKCS7sWYlxjlKxgHw+j5Y3W/C/Tz/NQ6Hgjp9seKZ31py5ajwe4wAtz9zdAH5OpJTPAqgEgL5USkpu4eLFHloqFXniYh9t3bunauuWrStisSi5//4vYnHTEkyZOhWqokBICcuy0N7ehr2trXjt1VeRzqTl3ffc81bjgsZELF4pQ6EAqa4eI6UEicfj5dhTKoCikynx6Bop5C14dJ2XcjmouipvvGFGoSJaWfr738/7tmzdjl/88pfIZjKwnH2SpmkIhSMYW1ODhvmNGFcztjhudFXR69Wgck58Hg+XEorH5ylDJYA8kVKOckpdB0ADIBOJhOzv70OhUFILuTzPZLNcSE6SfSlvJp0O5A1DN0qGDxLS4/OUAh6PGQqHC5XxeJEQgkgoRH1+L/wBP6LRuIjH4+Uf8gSAUwB+MbhzzQSwCMA0p/QUQADgNJ/PJ/v7+wnnnFiWkJZhKCYzKADoqiZUXeW67iGcSxKPx2QoFAo7AybnuE8COAZgHyHkxGXjeFAQEQCzANQCqAIQBeAH4AXgcex052w45TMcyQHIAOgBcBbAUUJI5uOM/wcaHmf3g9UM7QAAAABJRU5ErkJggg==\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAA3vSURBVGiB7Vt7cFzVef+dc+/d90OrJyO/JSO/4ncxxfULMCYIAyEW08amJJgmM4GmnZjJdNq4gcSGzLQxk3bsaWcaaIHyR8CJrWAbpjgG/AhINsbYxkaSDY6xJFvSrrS7Wu3uvfecr3+cu1pbXhkJs/4nujNndufec77f+d7fd+4uw8gvIxwOfocBaz0e91yXyx0BgKyZiWUz5kcEvBKPJ18EYI+C5rWvkpKSyZGS8LGHGtbQR8ePUUdnB50/f57OfnqWWlpbaN++39O99fdQpCR0NBKJTBwJTfZFE4LBYLmh8+YXXvifKctWrEBPTze9+cbu8/3JVMoWNjwer3/ZsuUTvV4P239gP36yceNZW9CtyWQyei262hcB+7zurU/99Ge3r1nTgJdfevFsqr8/Wlc3rWbGzFkV8+fPr1iwYEEJgLadO3cmbr/jjohh6KXHPjxamsmar39pjoPBYHl5aUnnqZY2/b1Dh9LdPd39kUgk6PP5PD6fH36/Dz6fDx6PF+fOfdZ9+pPTgbq6Ou+aBx+0k/0DVYlEIjYcbX4tYM5pxeK/WKIDwM7Gxt0TJox/dtLESXC53JuHzvV4PBVHDjfvAYDZs+fonMsV16R9rYeM8XG1tbUAgMrKsrDP659DRJ5gMNhbaH5NTU0IAMaPHw9IPv5LAxORy+31AgBcLsO41lwAcLu9BgAYLheIkftLAxfzGgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4D/lME1ke7gDF8ltbOHe3W923oEwYi1jxftWfZWgAziwacZkd2pfyN96XN5IIu7dMtIKA9/TI+zqCnFps2Alg5UlojFnVqIHZUlO2sl4RyC4CU+SEEylux8Z/iyc7mrxw4U7UnYwvGpXMYKIgNGdwXC/76C48oRw3sDWfnCgIkARJXcpwbvpA1e6T0Rq5jDr8EAHKA6OpjUOJwfeXAJAEhAXAGgEPKq+dIMVJqowDO4RAAC0rHV21u5LijAJaABAOIAY5Oh15iFMgj1zEpcUuuXjpIWeCouxjAtnIZcGKA5AVFbRfazPUC50QrKe8+Qy8qiqjBYIODA5DgBd1pBO9WRg9sy7yOhXBca+icYrgTOUGOiKnIVdCdisAxJGBTPsYW0nHRrJqgfNmGVtiqaeR1xchF7Vgz40q/BUNmISlcL7CUgJAMnOUiVwEdF0PURIAAVHaC8ucbAiwcQAb1KQpwXMjFrhtYMcOVO8lhOB457ujcKZd9hBguSYwcelTupKyaQWKYJFEU4xJw/Dhfcw29ilSBcNjEoTucFnSnkeOOvvTJpcVC1cYoGB5NAGEQTukjMAzHoghJghyWCRjenYoTuZjKx8xJiwU4LrSZ6waWpIoBjTuRqxDHRUkSUMWAJAZp6QU5FqOw65HHapG3bGVcBTZXDI5VnFaFgBL1yC34uoBJqEJeIwD2MMY1ilZidAFEMlDOqm9UdpJ0ZawumI+LU9ArwhyqWxyNz14XsBAMUnLVH0ttGB0XococdCGWE3XhOV85MF1WV2OY3omK0S2SkxgYAZYYJoAUpcqEEjG/Ru80isA1ysMXYNCnCum4aKUPgTu90w3sFinXL6nO/MadCAhiKloxBjFMeSuK0S1Kylv1cE1bUVoYyHwhoI6bCswpjjuxK5u2G2lcti2jzNCRTluioHEVw52EBA5/2LKsLBL+h2gs/o+Fjpa+MqtmjCbkqQJSYFF3T3zRsPMvA75i7UiBA4FApa6z5+fNnbd6/frHADghk7QdlhAHdMY0KXkZAHAuozaRMDRtKYMdAYDVq1fjcHPTD860nZlsS3qsv7+/+6pNDr0RDAanGTrf85Onnq75/uNPIJ1O4+dbnj34Ot6B4eFLqksqUeEvgcflAREhZabR09+Li/EorLQ4eFv317D2oW8t0XUdu3a9jud/9auztqD6ZDLZOixwOByeouv8D1u3brtpxYrb0XS4Kfbj3//8VHC8d0nDLXfj67OWIeQJgDGADfoOAxHQl05i14l92PHBXiTPp/c/OrFh9vwF8yMnjp/A5s2bOqXEbX19fX+8CriqqspvmunDTz/10xkr71qFnY07Tr1i7aqsLg2Vb6h/GOPCpdAYgTPlNLmF5AzpvBRp74viX3a/hO6+ge47+hZG61fVTz9y+DCee27Lx15fYFFHR8cAcNkPuw2DPXfP1+vvvf+BB7Br967WX9Mbk70eCn33zlWoCrsgKAFBCdgy/2nLBCyZgCUSMGUSpkzC0G1MrKzE0XMt/la9I0QnM+cWL15cmkwmK1tOnwpksuabg8YVifjnhEOlj69dtw6nT51Kv2q96fYG4fG7gbJwFhn7cxicIJgEZwAfEiokGASpWG1KhvIwg1/91ti1N9DEJ7ZOzKxdt87T1Nz8A67jv2Kx/o85AJDk//zXjzzCAeA/D7zU6PZjkkuXcBuEjN2OrGiHabfDFB2w7HZYoh3mVaMDWWdu1m6Hy5Bw6RIuP6b87+HXdgDAww8/zIXgGwFADwQCFYFA4BuLFi3CoUN/6LRmyL/y6gSXTtC4QDTVgQo/B5iEJFJ6Rt64lI6Vfi3JYBFHd1JA5wIunUNIQvpr/C+bm5u65s9fWBnwe9dISWVc0/DNhQsX6gDwTuuhd3WNYOSGTjjSehGp7EVYsguWuJQfssu51wVTXIIpLsGWlzBgXsSRM5dg6Hk6uk787Zb39gHA7NlzDM7xoM4Yli5fvgJSSiRmmbP9HNA0Qm4D6axEc6uJ6eOzuCloQuOOjlneqiUx2BK4lDBwut2DTFaHoXFYGilaHEjMMOdKKXHb4tvw/nvvL9UZ+Lyb6+pw/PjxpOZhsziX0DigcYLG1QaEBD69ZKA7wRHx2/C7BDSNwEi9AEmZGmJJA/1Z9SJM12hwvcYBzgmaj89obW3pr62dGmCcz+cuQ68GgEtdl7oYU40CZwSeW+As1rmy5KzNkbY1WILDlOp71ubgnKA7czVO4NyhwQhcFS7o6urq5pzDMLRqnXEtCACpdCrFHOHlAsTgYEq0nCnj0jnBY6i8KCTLBxbmzB2yPkczmU4lAYAxHtKFECYAPeDzBQZD4GU+motMueXklECWc7QkSaVDGoTAVetz8AGfLwQAQoisbtt2N4BJZaVlpZQjkntdS8w5UFOFni0YLMGhWfny1rbVPVuoOVKyK9ZeTrMsUl7qAHdzkPyktzeG2tqbw8KihCQlPjVUl2hLBkswmDZD1mJIWxwDWTXSFkfWUs8sZ64QzlqHjiRA2tQ7ZcqUYCwWgyT6hBNjb+3ZvQehUIi52tje3M6FyHHIYNkOqM2RsTjS2cuAs+pe1uYKPLcBkduA+m60sH1+v5/t3fsWGGP/x6VkjR98cAQAMNc7bXJepAyWzWHaimjW4siYDGmTY8DkGMhqapgcaVM9yw5ugMOyeX4DkmGub1otABz/6DiI2O94IpE4E+3p+aCzsxP333PfAvOi2G8JBtMRbU68GZMj44Ao0BzXmgOsRk7spq1oWILB6rQP3nt3/byLnZ2IxWKH4/H4pxoAeFzuC21tretW3rUKnk5mtWiflzAGxhgDQ66IYyrnOnqzBFfDZjAdLk1HMnkpMWRNLldmFomamtrIL/71F+iPJ/8mnc2e4QDQm0jsOXfu3L6TJ0/ivtX3T607M26P6SzMWI5eB7ktPHLPc/MV5xwTjpe9sfLOu2pOHD+JCxc+fyeWSLyZdzCoWsvjNpqef/6F8KTJU/DDLT/a3jM90eDWCS5dqmDvxF7NCRSAOikQhCuMUXHMEDjm3v7jb/+oIRrtxpMbnuzNmvatiUSi7QpgAAiFQneXlZbs3rGjUauorMSmLc+8dShy7HbDELqeA3bC4GCScHxWSMDOgVuaPb2t+t3vPfK9O1P9A/j7v3vC7ov318fj8bdyWFf8YCSbzZ7VNHb+tVdfrV911ypt/bcfq52J2uTBg+//LhWwZ0nJYTtWf6WrcccDGFgLdn5nwkPVD9Q/MLOzsxNPbvhhNpUc+G5vPL7jcqxBjonozwEsBzD5lVde9jy5YcPqTZufKX90/WOwbRv7330nsffDt08dSB41EkZyHPfwmwBAZuTFsBm48GeuWfai2oUzp02fFjKzJhp3NuLFF/+765e//Pfd31q71gLwGYC3GWNNAMCIaBKAJwBUO3uQnZ2d/MyZNv1vn/j+LUuXLq/Z/MyzCIfDTmxW8Y+IVFyWqjKRQkDYNqKxGDb97GkcOXLk7LZt/9F8c12dqKqqYM4LYALQCWAbI6J/A1AGgKK9vSBhoa8vEe+N9TwejcZYU1MTfrN9O6puqkJDw0NYtnwFpk6dCsZUMrFtG22trTiw/11s3/4aotEo1jQ04NZFt6KsrJTCoZKtJaWRiGG4KBKJ5BJWnw4gDedAx+0yMJCywLnQGWOSMabV1NbikUfX40J7B367sxFbt25DMhGHZZkgAC7DhWAojOpx4zF3wS0YP64aVZUVYCoQSN2la4bhIsNlcOS73H5GRBUAHgcwBYABAD09PZROp1gq2V8WTybq4vH4xEQ8oSWSSfSnUkinM7As9RdUw9Dh9XoR8PsQCgYRCodESTj0x1Aw2OrxBXsDgYBdXl6eM2IB4CyAbZcb12wASwBMB1Dq7C4ACJZIJHstM5PWdC2TTmcom80wEtySAFwupum6wbxeDxeCuT0et8/v94UBTTrSJABRAKcAHGCMnbrKjy/bRBjAHAATAFQ5NuAF4IFqAtyOKzKo83MLgAkgA2AAQB+ADgCfAzjBGIsPxfh/6wbDK7xbMFYAAAAASUVORK5CYII=\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAyUSURBVGiB7Zp7kFRVesB/5/S9PdMz/ZoHMwo4MICDuoGVIYICIuzGcn0vC+oWGuNjs8mua9ySP4wpgyaiVVupbHYTsLJmNT7WNXExwqqzrq8g4oNxdXUgyEMQARmZd3fPTE/3vfd8+ePenhlgBsFlrFSqb9Wpvn3vd77f+b7zne87ffsqjv+wE4nYDQqWl5aWfDUcLqkAyOUHunID+Q8EnkilMo8C7gnoPPaRTCYnVyQT71+1bKl80PK+HGw9KPv27ZPde3bLjp075NVXX5FLL7lYKpLx9yoqKuqOR6f6PIFYLFZtW7r54YcfqV+4aBEdHe3ywm+e39eb6etzPZfS0kj5woUX1EUipWrj6xtZedddu11P5mYymc5j6Q19HrgsUrL67r/7+8VLly7j8cce3d3X29vZ0DB9yplnfWXcrFmzxjU2NiaBXevWrUsv/trXKmzbqnz/9+9VDuTyz35hi2OxWHV1ZbJ1245d1ltvvpFtb293Kyoq7LKystKysnLKy8soKyujtDTCxx/vSW3fsT3c0NAQWbpkiZvp7a9Np9Ndo+nWxwJrLYvmzV9gAaxbt/75urrxd592Wp0Oh0tWHSkbiUQSv3unuQlgxoyZltZm0TF1H+umUnrC1KlTAaipqUpESmMzFIRjsVj3SPJTpkyJA0ycOBGMnviFwSISLolEAAiHbftYsgAlJREbwA6HESUlXxg8lkcRXAQXwUVwEVwEF8FFcBH8/xhsnZC0ksw49eQPI5mmNtP54ccAIvqgqbz4aYn8zYoTUXXcFnueyZ8eXtleZt75iQnpU0VUvYiqB5mvu5p+XH9w8RtgnJMOLut/7rd4+fpRBcS52hz65csnHdxQ8clZnyuT3NV40sHRUnfq58mUWFJ70sEn+yiCi+AiuAgugovgIrgILoKL4CK4CC6Ci+D/Q+Djf/higk8Jzs0IMjIGYDGAp0AUeBbiHf3Xs/HGAHyYlYaRX0EYC4txNeIFugvWHyXzua8cnDjYGMBoQIFhRFfLmLjaCxqAw8iuHing/nCwGlLuMrKrveNfnccPFnyLtQ8c0a1jElye8sGFAYwUSCN54Q8GB4ljKKpHkBmLOZbB4FLgjhLVYxNcDFnkMXJUj03m0kOKR0sgYzLHRvlwpcDYI7oaGYvl5HB4ZRrJ1cf9fP5E/5NwQUKM7uoTOI4/ql38kmgUOCMnEHMCL819sag2jJJAxgIs+HNY6PGlpUxXDQWXw5dXjxH8SFZBPf7SyqKrMQLKG7b/OkpmTBJI0BSjbwTGYo6Ni5+ZjMJDj1wkxmQ5iV+VsBh9BzImKbNQFhWjp8wx21c7dKIV9A94IxaJsdplZt9574JQVcUdpr3rzlEHdzLASslpg19EofLMMa3dc0Z9c9YMXT+s7/GCo9FojWWph87+6tmX3XTTzT7XA/F4xutXr4fyOuQZVQUQ0tLphY1nlcn5YqgAuOyyy3inefOtH+36aLJr5Obe3t72o4w68kIsFptuW7pp5d33TPne928hm83yLz+6b9PVb/4niRK9QNfUoquqUaUREEEG+jGd7Zi2Dnpy3qYHGr7OFdcsX2BZFs899ywP/fznu11PLslkMjtHBScSiXrL0m+uXr3mlEWLFrN58+auxD+u2HZWhb0gcvkyShZ/Ax2N+70KPcVvJpMm999NZJ99mi1dzsb3rviLGbNmz6rY0rKFVavubTWG83p6ej4psAbfr66trS03xtlw98p76s+bN5+nnvzFtouevK/s1AnJM+I/vB37j6aDziJeCtxhzUkhTgoYwJpchz3zbJI7fj/pzA829f6iR/bPPW9e9aS6utjbb715YWVl1SOZTMY5DGzb6scXf+OSS6+48kqanntu55+99shkOyLx8uuvIjSuDEzq6Ob5TdzgPJ9GhT2sCbV4W1vK57R+FP9lOrT33PnzKjOZTM2OD7dFB3L5FwaDq6KifGYiXvn95ddey4fbtmWv2fhIiVUqpbpMEao2SH4fiKCMgAbRggSuVkKwEQz22q4iVKtQEYUtJvzdlvX6+bq67PJrr41sbm6+VVv8W1dX7/9oADH6b//0+us1QO/jD6xPhGWSCgsqLJj8PsTdjzj7Ma7fxDkAzn5wjry+H3H2YfL7UGGDCguJEqnPPf3YOoDrrrtOe56+C8CKRqPjotHoN+fMmcObb7zRelsk9W1lC4QFCRlM9yfoKnsoEgOLVWCxDLfYBRwwnXmwDIQVyoMbo6lrfrq5+dCsxsbaaHlkqTFSpUMhvjV79mwLwHvjldewBGxQlqBswXn3Y6T/EDhtiNOGuG2I2444QXPb/WtOGzhtmL7PcN7di7IFFegiJDq3+ZVXAWbMmGlrzRJLKc6/4IJFGGO4MdQ+gxAQEn/2LcH0u+Sa27HO0IRq/V+MSqnBOUZARMAD75DB2w4mq8AKWkggpPiOtJ3dYgznzTuPt996+3xLoc8+vaGBlpaWzFybrygtqCPgeODtcTFtBl1hUBHfGgl+wNGv8FIayWjE6KCfD1UhBVqotPWZO3Zs7506dVpUaT1Lh21rPED7oUNtKH8OUYLSoHTwWRiEAsmBDIA4gCPIAJh8YL3lyw7vi5JAJ7QdamvXWmPbofGW0qEYQL4/0zeYjdTRTQ0Oxp9/Svx9jvKAkBocsCh1dP9AZ76vNwOglI5bnuflAaukPBo9bM8UpMIjvxeiWAUbATHK3/yNJM/h30vKozEAz/Ny2nXddoCKyqrKwc5GDYFMUJmM8peLqyCvkH6FZP1zXP+eGBXIFvQcrquyqroyALdrxGzv7u5i6rTTE3lX0gUL/DIYPPfwFDh+k5xCBhSS1Ui/9s9zQ/cLz0rEGxqEGMWAK92T6yfHu7q6MCLbtSj1UtPzTcTjcfW0E3t5EBSkv0FgPgAMQgtWa/9azpcZHICrhvR48B+52CvRaFS9/PJLKKVe1Mao9e+++zsAtk9rnIwbLBFHIQ5IACWvkJxGBjSSDeDZ4HxAIznty+SV38chGIA/PXumzZoK0PJBCyLq1zqdTn/U2dHxbmtrKxddfmXj1r7QRr9jMH/5Ye4d8OdV+odZ3F+AqyG3F/oFelr62PQnl14667PWVrq6ut5JpVJ7giLBygfWrMYOh3ll/pLx4iojR7p3QMGgpQX4kPUE8OFuF0chrjIvzL78VDsc5sEHH0SLWkmQLuhOp5v27t376tatW7nk8iun/UN8VhM5BblASS5w53BowdXD4L7Lg8EG7Z6SM36z+MILp25p2cqBA/s3dKXTLxRSBeDvtUpL7M0PPfRwYtLken791z9Y++fevmWE/WJBIelbgJbDtz4mePblBksrcPU/ubVrF65Yuayzs50Vt6/ozuXduel0etdhYIB4PH5RVWXy+WeeWR8aV1PDz+6/56W//PDFxbpELGULgwVEcwSYoWXkKExOuatqGl9b8p3vfb2vt5/b/uoWtyfVe0kqlXqpwDpql1lVlbwhUhr52VNPrQ3PPuccNm16PbXrR3f+9pvm0NV+pWEwhQKIqKHnm57iV9nydc6Smxc1zm5MHvj0AHfecUeuv7f/u509PY8N5wyCReRcYCEw6YknHi9bcfvtl9276r7qG2+6Gdd12bhhQ/rghhe3TdmywT4l2zkhEeIUgJTLZ62RygPbT5/rlv/xvLOmnzE9ns/lWb9uPY8++u9tP/3JPzd9e/nyLLAXeE0ptRlAicgk4BZgfDAGc/DgQb1790fWrT+45Zz58xdMue+++0kkk/5N8RO2iPiZ0BiMCMbz8FyXzq4u7l91L5ub3969Zs2/Np/eMM2rrT21YKQBPgPWKBFZAyQA093drTzPobu7uyPV3XNbR2enam5uZu3atdTW1LDsqqtYeMEipk2b5m8GANd12bVzJ69vfI2n1/6Kjo5OvrVsKefOPZeqqkpJJCtXJ5OJinBpRJLxeOF3bI8FZIAYoEN2SHmeJ6GQ2CiMUipUP2UK199wI59+2sp/rVvP6tVryKRTOE4eAcJ2mFg8wfgJE5nZeA4TJ4yntmYcSimUUsaydMi2wxIKKTXM6n4lIuMCV08m2O52dHSQzfbpvkxvZSqTbkinUnWpVDqUzvTS29dHNpvFcfy6aNsWkUgp0fJyYrEYiUTcSybin8RjiZ2lZeXd0WjUra6uDg2L/z3A6uHBNQNYAEwHqvAXTTl4Kp3O9HhOvk+FGMhmHXHdHGLEE8CytNY6rCKRsPY8VRoOh8tisfIkhFxgIAB2AtuA15VS20ZcTsEgEsBM4DTgFKASiAClQAnBig7EC8/8BoAc0AekgE+B/cAWpVTqSMb/AlY1WXIncMcxAAAAAElFTkSuQmCC\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAxNSURBVGiB7Zp7kFTllcB/5/a93dMz3T0PemYIDgoCPhZ5iaD4wNkFjQjRRMlLTNbSlKlyzZpobSVbFRPUbNVWSRCWuKvlxqybtbIrukp4SATZCAgospEBgeElj4EZ5t3d0+++37d/9O2ZnqEHQZzZSlXfqlMz/c253+875zvfOefeHuH8L6u83P+AwH0lJZ4pbrenEiCVSnYmEsndGl4NhSKvAJkLmPPcV0VFxZjKivKPv77wXr274WN9uvm0PnHihD5y9IhuPNioN216Vy+Yf6eurAj8b2Vl5aXnM6d8loLf7w9apvHhyy//29jZ9fW0t7fpdWtWN7Wdao4qpaiqDpbdXF9fV1paKpu3bGbxk08eSWXU9ZFIpOPirC33v7xs+TIdiUT0Pz239NjeaTOTHXXjdb4cuP6W5DOLFx/7aNdH+oknfqQryv0vXZTFfr8/GKyqaN7XeMhc//ba6NSfPFXqS6fESJ29jdGAX69+9KHY9OnTyxbec08mHInWhsPhzsHmNs4FNgxdf+NNN5sAh3/7n40dCxeKedUsOr6x8CzdsnBEQu9sPABwzTWTTMNQ9eec+1x/FDEuGTduHABXtreOKutJYyiFqq4tqD+5O3wJQF1dHSij7nODtdZuj9cLgMfGOpcuQInSFoDldqNFez43eCivIrgILoKL4CK4CC6Ci+AiuAgugovgIrgILoKL4CK4CC6Ci+A/B7B5vor6Mz4PNnbRYAAtoCQLUMMFVobuBWOALWdjVIGxiwbbZC3WkrXWLqAzJBZrR5T0LWTgdSHfdF1YcIlG57t8oM5nfov1OcCKPmDW1Rfi2IsA5yI5F9WFXF0o0i8arARwggsBu4BbhwaM6g0ujXY+9b+GLqrzLR5E5wsH2ziB5QRXoW8lCy3mosH553iwlDlEe9znai2DpMyhAJ+PxUNTJMhZm51+WM9xvsWFXD2kx0nl9rjQ4oYC3C+4BoEMnasl39Vn6wxRdcqbXApXpwupWBcEVgLKGLw6DU1w5bkaCjcChcYuHozuLYtqEFfroXC1TZ67GcbjlEuZWjSIHr6ozjZ7/y/VSWOLdgJIF9zjQl3JFwDOXn1lsYDOULm6X+YaROcLB6s8+LC2tzqvoc+Wx0L2nT/6wlIm5y6LQ9bs5TLXsO5x7jG192lxuJq9bCOg0aIRGcYEkt9lCsPp6lxlMsBlFE4ghcYuGoxznHKFYNjKYq7Zy5XFYW32lMtCBGzbLlwWLwB83m/2NNC44R0iFaP503+8jO1UqHz5wiwW0aNzvysgdPJTQr/7dFD9fHD+vecN9vl8NaYpv546ZeqCBx98CMhGbPXEqZRfcTWmyySTjuO2TMora/B4Sji+832OnWoGYMGCBez88IMfHD50eExG6Yd6enraBjJcAwf8fv+Vbsv1Pz9f/NT1y1esQCnNPz6zeGuy6WBN+MRRrwp1YMR6MOIJMqEuOj49xNFd2zh5aD9SVpr44PCJXVOmXXvpHfPm4fP7rtz98Z/usSz3+lQq1e/fnvuFSHl5+VjTNLb96lfPj6yv/0t2bN/eufJnj+37Uql1c/1Xv8WM279CaZn/rJcBGoj1hNm+7k22rF5JcyK1edp3Hps0bfq0yj0Ne/jFL55pVopZ3d3dx88C19bWlqVS8Z2Lf/7U1XNvu51Vb72x7/irz9fUBEcEv/03PyFYPRJDgZHt9XpvzG8QlAFnWppY+S9LaOnsaPPOWdhxx7z5V320cydLl/7yE2+pb+bp06dj/VxtWbJ03h13zr/r7rtZu2bNwVP/9cKYMiHwtW8+QNAbwOiOIN09SCiChCKQL+EIKhxBhcN4EGpGjuJww66yxNH9gePac+zGm26sikQiNY379/kSydT63uCqrCybXB6oeuS+RYvYv29f/OTKFz1+dIlXXFQrCznRjNhkRfdJzmIMEAExsqbUmh68holWGXf43deMg6NHJ+5btKjkgw8//IFh8lJnZ88nBoBWxpPf+e53DYC1Ly5bVSb6Mo8WSrQgx5uRY6cHSDMcz0q/vx/PSTNeJXi04EOPfe93L70JcP/99xu2bfwUwPT5fNU+n++rM2fO5P3332+uS3V9y9KCG8FSmtjRo3iN0uz+qqylemDnLhpDQDsFJGrHMG2F2xAyGi5Nhr65Y8f21unTZ9T4yrz3KqVHGC4X91x33XUmwN7N775nApbuk90nD5BpbUbaWqG9Dd3eju5o6y/t7dDehrS1kmltYffJ/ViA25nDBcbeLZs2AUyaNNkyDL5minDL7Nm3opSiNtQ0yUQwESydlXg6xc70Sf5CewliYSD9TqHu/anpIMUnJIiLjSVCGjAFTA21odNTlFLMunEWO7bvuMUUjKkTrriCvXv3RDyiJxpacGVXSc56W2uO6DhtKkmFFsocHchmtKhoukURNrJPG5YDdAEuDYaAV/TVjY0HesaNG+8Tw5hmuC1zFEBLS0urkQ3QPtFgILgQTC0IkAZSgEJQCClnTBwdF4KBOPf2iQBnzrS2GYaBZblGmWK4/ADxWCzqoS85iDOZDFiMS2ddV5Kz2EkGhgwECYLOzqOzxy0W7YkAiBgBw7btFIC3tMw/2JsrnS9OI5B2pPdt0AC9gdVZZxkBANu2k0Ymk2kDCI6oqsw1c/nNu8rVW8l+2ZFCkxRNzMhKUjQpNBlnv23nXfbAeTRQHayudMBtBlod6OrqZNz4CeVprcKqd4KsZBxgGk1KNEmBmGiijsScsZRo0s4CMnn3284CMqJCY8aOCXR2dqK0PmBokQ3r1q7D7/dLq7tyY8axMCOatDNZFqhJiCbuWNsLNrJjCUcnt4C0ZOew0WTQnDYr3/X5fLJx4wZE5B1DKVm1a9dHAIyYesPYjEBa+vYwJZAUSAgkHAtjookaWcl9Togm4eim8u5PS9YDNVNmXg7QsLsBreX3RjgcPtzW1rarubmZ+QvumtahXJvzrUzmWRvrZ61yxNnvPKuTA6xvt13bvjxv/tSW5mY6Ozt3hkKhoy4Ar6ek6dChg4vm3nY7oZJAJnG4oUIQESdD5Ud0v30XSBlZC1OGdjyTA/darwK3LcxcPm585ZJnl9ATinwvnkweNgC6wuF1x44d27R3714WfOWucZGrb3g7kee+eJ6LewPLcXU0bzwuuf2G3P3NoyevnzP3tsv3NOylqenkHzvD4fWQ197aikeW/nJJd1dnJ4//9On57V+a8Hoib7K4kQeUAWL0D7RcsJ2oqHv9wUcfu7Orq5MVK5Z3KS0P53j96lsgEPjyiKqKtW/891uu2tpalvzDMxsTW96s9yhMC8HUOCkxm07JO/fZk5A9dkmDTOSqWe/99fcfmRPtifHY3z6a6Q5F7gyFQhsKggFGjKh4wFviffG11153T59xHVu3bg3968/+7g9V3ae+0Zv0kX49l3ISjA2ccpe/NXvR9+uvnX5tRdOpJv7+xz9OxnpiD3d0d/97PqcXrLWeBcwGLnv11d96n3j88QVPPf108KHvPUwmk+HttWu71q96Y0dozzajJBUfXyqMA4gpfShmeY54JkzX19/6VzfMmDmjMpPOsOqtVbzyym9alz23fM23Fy1KACeAP4rIBwCitb4MeAQY5SxEt7a2qIaGBn70wx+OTKXTc5Y+t8w1d85cdN5KtdbYSqGVImPbJOIxotEo6/+wniXPPmsH/L4Ny5etaJk46Rqprq7JPTgooBn4Z9FaPw9UAHR1dSnbTsuZMy1GMpnItLZ2GFu3bq5d/fvVc0ZUjZB7F36d2fW3MmHCFZguF0pr0uk0Bxsb2bL5PV5fuZLuUEjfdffdG2+66ebW6mCVLvP5qa4OAoYEg8Gcg7tNIAIEADHdJnbcxmNZ6UQ05nK7TT1x4sRYRVV1/FTTqdLVa9bywgsvEImESKfSAFiWhT9QzqhL6rh25g3UjbokPnJkTaKkxFRaa8NtGbaIy+Up8eS2VgEx0VpXO66+HKfdbW9vV93d7RKNJl3xeNQOd4d1Mp0i3B3yRCKRsmgiYSVTaa9orS23lfR5vany8vKYLxCIeyxLKqoqtddbKh6PSVVVtQ4Gg5IHPQI8nx9ck4CbgSuBarJnvARsiUai4XBPmGQyqbWGRCxh2VrZAKYYLtNjZUyXSxsuU6oqyg1fwO91nhUSzvQdwB5gm4h8UvA4OYsoByYDY4EaoBLwAN7sYiDvZ4LsqUo60uNIK3AY2CMioYGM/wPREY0iGUY58wAAAABJRU5ErkJggg==\"],\"useMarkerImageFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', amount = percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"markerImageFunction\":\"var type = dsData[dsIndex]['type'];\\nif (type == 'thermomether') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"color\":\"#fe7569\",\"showTooltip\":true,\"autocloseTooltip\":true,\"labelFunction\":\"var deviceType = dsData[dsIndex]['deviceType'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}, ${energy:2} kWt';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}, ${temperature:2} °C';\\r\\n }\\r\\n}\",\"tooltipFunction\":\"var deviceType = dsData[dsIndex]['deviceType'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}
Energy: ${energy:2} kWt
';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}
Temperature: ${temperature:2} °C
';\\r\\n }\\r\\n}\"},\"title\":\"Google Maps\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First route\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.5851719234007373,\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.9015113051937396,\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.7253460349565717,\"funcBody\":\"var value = prevValue;\\nif (time % 500 < 100) {\\n value = value + Math.random() * 40 - 20;\\n if (value < 45) {\\n \\tvalue = 45;\\n } else if (value > 130) {\\n \\tvalue = 130;\\n }\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"
${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH
See advanced settings for details
\",\"markerImageSize\":34,\"useColorFunction\":true,\"markerImages\":[\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic7b13uB3VdTb+rrX3zJx6i7qQUAEJIQlRBAZc6BgLDDYmIIExLjgJcQk/YkKc4gIGHH+fDSHg2CGOHRuCQ4ltbBODJIroIIoQIJCQdNXLvVe3nT4ze6/1/XHOlYWQAJuWP37refYz58yd3d6zyt5rr1mX8B7S5Xo5/0nPYaNFM1PY0gGqOhfAgQCNBGlWFFUAYEIeihigbhFdZQwt85BV5Gj9r/718R2XX365vFdzoHe7w6d77xnPkn4YpAtU0YiizNJcmPNkMQFkDiSlowHt2HNtGlTSJ6B+pTpsKTfKgTj3Pi8SMtFtEZnFs8d8dPu7OZ93BcCHtt0+OiL+FJjOiqy5K5dtLwD4PBHGvy0dKLYo8B+1+lAldv50FfmFzWX+84i2M3a8Le2/Dr1jAKqCHtl2y1wC/pEMP9ZRLBaYzF8CCN+pPluUkOKfB6qlmk/dBwTyt8eOv2AZCPpOdPaOAPjA1h9/SJX+TyGXuz0TZi4EcPBeOk+U+RErZh2YyMAyQJEoZUjFgtkCAEScgDyx1hmInTglqDj2U1X0WILaPbWvwHO1WummeuLONhaXHTf2wsfe7rm+rQDe133j/i5xPyrmCr+OouhSKPbdQ5fLiezTIYUBQGMJBgYWxMYSISZhbxgQT8wGAgDiwWxUvCiBxKhSKOqdh4OyV5+6XiEfK/kjVOXQ13apG+I0+adKpXaG0/Si0yZdvPbtmvPbAuCNT98YTBhT/8fAmEpHoXgKgPe/6gFGP0nwG8s2YykcaRCAYYQ5tKTkDVuArDEwMRF5AICS4VZ1AQBSr6oEgL36CBAvlKqIsyLOKQl5TZH4uN+TawDuY6o64lWTJX20v1S633uJNvfmvnbRERelb3XubxnAX26+5gDy6Y9HtrU/wERff1XjSt0WwULDmZEMawPOgilgQ4FaGCEygaXQMQyRMaxiUijUkAEAImIGAFURAOrVA1AmI1ZExGuqoqkVFefhyGtKDql4X4eHc6LxJof0VIVM3nVc4uXaHUPlo0Tpc2fv/zer38r83xKAd6y74iImO31EMf9REA7cpdVBY8NbA5+dFNqsCTQipkitBjAUsLUZNd4qm8AyjDMmJAIRhDzDEBEbJkBVAyJWQJ14AEaciIeSGicOgBeBWNHEeXLkXIM8UvFI4bVBCVJNfdk7STd5xOcp0LZzjIqV/eXq/4i61edM/eaN7yqAqpfzf62Nf5LP5lbko/DbCuxU4saEN1mN2kKTzQbIkuEIEWfVagRDEVkOyXCkVq0aDg2p9YYNAySVerU0WN1R27Jjo6ulMQ1V+ggAOgsjNRNEus/IiUFnYUy2kM23AcrivXh2RiTxjhx5iSmVWEWdpmhQ4qvwSBBrXVPfqDmuVsT7C3aZvKslyZcr9dpxdr81F8ynO/w7DuD1q/8y6kDw2872ticN0deG7wvQHXHmdxGK+1ibQag5ikweliIElNUAEayNYBCSRQRiYzf2rNtx11O/rC5d9dj+1aQyM2Pyz3WGozaNisYNWY7SYtgWA0A5KUVO4qAn3t4+lOzYt+Grh+bDwstHzvjA2tPfd1Z+39FTRhGpi7VBKrE4nyBFDKcNJL5OCerqUEXdVeEQb0mk8lECjR0euxe9cqBUOnoQ6RkXT78hfscAvH71X0Z5kf8Z0dH2CgNf2NkI0d0ZbmtElMtFVEAQ5BFIlkKb00AzFJqCGooQcJjv7t868P3/ubayZvua48ZlJt57xLjjB/cpTssXokK7IQNrbeoZ3pIRJm1aYSUW9cwixglZ7xNU40ppY7mr+sy2ezt7G1s+vP+EGfd/+fS/Ko5pH9/pJK04X6MUDSRapcTXkXJN46QKp1UkqNVqvpxVyLzhOajihh1DpVkmrJ7+uak/bbztAF6/+i8j62p3j20vbgXR+cP3LYU/Djg/KcsdEnIWERcRIk+hzWtEOYSch2U76tk1T6+84Tf/NCdni2tOmbRgy6T26WOiKDBhGFEQhrBhiNAyjDGiQp4DFgI8AChg1BGBXOC9p8QJ0kas3jvEcUxxnLgNpTW9izfdOqGWlve7+OOXrThk6qEHKtKehq9xIlWkvoaYytrwFYqlglgrcZxW+oXSz+ycpOLmnsHypDTIfuTNcuKbAvD2288x22dn7hrVnt/ATBftBE/CH2aCtqkZU6CI2hHZomS4YCPK+5AKHFB2ZNe2Nev/739/e9qY3KRnPzHtQp/LtnfkMhnKZDMa2oDCTIjQhghDC2MCCQITAyYxpmkhAIAZDDA7l4bOSeR9YpLEwfkUjXqMOE0QN2LU4waq9aGBX6/+d7O9sXnu3579jbVTx02dlEilL0FDG1pJG64cJX5IGr6MupY5duU1npIv7sTQ4196ytUDx8+sf+TN6MQ3AyBd8+L8W0a15zYw0d8O3ww4vC7ijlkZU5QctVPE7QhNEVlTRNYUjHcy7tu3fuuVSqXBF8z66962fMeIfDaHfD4nmUyWsrk8BdaYIAh9EFoxzExEysYoAQ5A0ioAEIpIBGZmAM459iKaJo6cT209TnyjWkOSNLRWi1GtV9A3sGPg56uvG1vIZ9N/OO9rM8jS9oavSOwqaEhZYh3khq9K3fdpXWsbvdR3MoYCV/UOVadcOvv2C/AG9IYAfue5j1/U0R5mIhNctxM8yvxLyMVpOduJyLRRnto1MkXK23axlB27sXtT1z//8vqDTt3vk/fMGnX4xGyhiEI2Qi6X1Ww2S7lCIQ3DkCxzQEQKYADANgCbW6UHvwcRaO6fAwCjAewLYAKAcao6UkRIBEniEtRqNVOrVKjeSFCP61oaqurKvqe237P2lnkXn/X/PT9l3OT9Eql2V90QN1wZdRqSuhukhi9T3Q2s9ki+NDzHWppeUqnG/qsH/+b7fzSA33ruI7ODIDh/RCH6KkEZAEINfhia4n4ZO0KzphN5005Z06aRaeOAcjP++4Ff3P/86hWTLjr08i3FfEeurS3LUTanhVwe+XxOwjAw1loLoB/ASgBrAdSAV232Gc0NyJGt70+27mlrzNT6nAEwDcBMACO892kcx1KvN6hUqWu9Xka9XsfgUP/Qjcu+Nf3g6bO7zj7urBNT1F+quxLXfUkaMmDrviQ13+8THdqYqvuLZpfq+qrJNXFDbrp87t0v/cEAXr5iduiTMQvHd2QnKDC9+bC9NUfF9kwwgvNmBGW5Q3O2SFkzAkaCg/71Nz9+2MTZ6rlzLs4Vi0WbyWS5o63N5fM5G0VRaoxpA7ChBVw3ANMq1AKoHUAewCwARwHYvzWctQCeaNUrt4pvgeha17Gtevt47+M4jrVSqZlSqepqjQpVyyX/8xU3VBHF2T//+OeOFbgXaq5fa75ENR3SarzDxDToYz846FTORbPRV7oHG9sm+qEPX3TEM3vc9pm9AfiBP53+T6Pbwo0Cd4aog4p/yXK+lDX5IDIFZDinGS7CckEM+JB//u9/e3Z8NGPTgjl/Maq9s8N2FNtcPpc1bW1tFIZhaIxJATwFYA2AtAVWh4hERBQByIgIE1Gsql8gou8AeAjAfQAeVdUvEtE9reFFIpIloiyATgARgCqALQAGmHmUtTYTRWHDhhaGYE0YYmbHEXZj//rBRc/fXTly5qGHEus2FUceCbxP4DShRJ2mvuIFboyqG5kNcNuWVM965MbNd71pAC99+vADA+MnR6F+TeAg6h1TeE/I2bbAFjVLBbJcpIDzZNke8qNf//yxKblZWz42+9Pj2opFbutop7ZCQdva2hAEQZGZXwGwDEBDRCJV7VTVfVV1BDNPUtXZqnomER2tqi8S0REAzgJwUqvMI6JBAM+p6pdU9f1ElGu1E6lqUVVZVYWI6gA2EFFijJmSiUIPsDbXmGT3b59V6Kv0dd334uLGYTPmHK7Q7lRi65DCawqviXWSrEm1PlvgWMh9KPbut+/77Ohtj/97d98bA6igo7aM+O/Ogp0l8BNFPQhyY2RyE0MqcC7Ia2jyGpksBYj2//WDCx9uk/EDZ8783JhiW5HbigXpaG9HNpvNMXMGwAoR6SWiUKS5KhERS0QqIgmAHcz8sqrOA7AdwCcB9AK4CcBvAdwP4EVV3V9VPwGgC8B4Zv4PIqqoqgPQYObEOadExC1A60RUJaLxURQaZqoRW0NEsm/xgI6u7rV9L295vmvGlKmHQ32vk0QdxfA+oYTq+Vgbi70mR4p6BEaKlTid98S/9f4MV7wBgF/66AEnFbPUz+z/VNTBiywLgxxCFDgwGQqR5wznOeR8+6p1657r6uopfu7wv4mKbW0oFvIoFovIZDIBEXkReUlVG6o6Fs2N/EjvfSczj2Hm/YnoY6r6Ae/9w0T0cVXdSkTfE5FsC8iTAZwI4DAAjxDRj0TkUABTACxS1csAzG39MHlmzqvqGCLKt1xZA0Q0QERtQRBkDZMngrcmNAeMmB08uHpxNsrz2pFtbft4TWInDZtSLE5T8i7uSKRS8XDjBX4fYbnusI2jMkt/tGP9rnjxrl+gICP4Riagrzb1ssKa4CkrYRhwwBFHYGSUOZJKo8oPP/vCoV846opSoZCnQj7HxUJRMplMgGblR5h5wHtfbE1oZAvIHBFtVtX7RKTQ4pSrnHOXAThQRK4BcIaqNkTkRRF5UVUTVf1462/TVPVSEfm2974qIm3MvBhAl6pGAEYAaBcR45zLiUiPiDxKRC6bzZpsNhtGUaj5fIG/dNTltYeeWja3ltbVcGgMZX1IWbUUqDUBbBA+OYxDPuDLSORq6KsN76s48MvzZnwwlzNDgaFzAIBAi0LKtGVtEQHlOaQCQpOHoWDWL+9+ZODCuV99cnTbmM5cIY+2JudZIpronHukxUWemavOuZIxpuG9H8fM8wDMJaJHVfV0ANcDOIyIPg5ghTHm+0S0UETWq2oCoA/AI6r6C2PMgyKyD4BPM/MggJ8COIGIFqnqV1T1YADbVXUjEfUaYxrOOcPMBVXdCmCutbZirQGIlIBwavucl2577NaJM6ftO1nJ9aY+YfEpvDryknamSNdAMQ1AGwxdc/DqDjz9k/7Nw5i96ixBSK/MhTRxJ7oUbracmWAoVGNCtRSCYOxLazfcN7VjdjK+beK4KAqpkMtpJpNRABNVdT2AowHUvffjAYgxZpNz7hUiuk9VT1LVWFX/iojuBfA1IrpfVRcS0Xne+6tUX33+M/zdew8AzxljLvPefxTA3xPRIufcpQA8EYUAFhPRSCKaKSL7EFGgqjtU1RDRZmaeGIbh1sh78s7LxM59R09um7585fqNdtqUMZOMMc4igE0DthSppcYWL80VTNbyX1QCPgNN1fJqDvzi0tnjQviObGia3Ee0JEAml+E8DOUo4pxaE4GUJz3yxJr9/vSIv+8uFAu2kM8jl8vBGNNJRE+q6grn3AZV3QRgi6q2AZjHzHNE5FEAp3vvv8HM8wFQSywvADAPwDgAi0TkPwDcBWDhcFHVh9FcXH9ARE4BMI6ZvyEiHwYwSVW/CeB0IlpERJeo6hwiepmIlnrvVzLzemZex8yDzDwZqlUikGGm6R0H66+evuPYafuNynvFkCCF4xjiBd67otN4C4GmEDAqTuVnR3++beWT/z5YfRUHio8/0dEe7DynJTUvswmmEiwxWcCDwGyee37j4ydNO6ucy+YmZMJQM5kMWWvHqmqPc24eADCzENEGAMvTNH2AiM5Q1W1E9GkR2cLM3yOiS0TkO0R0lao+zMy/8N7PBHAmEZ2C3YiIoKrdqnqjqq5i5j/x3n8bTQt8iapeKyKbjDGfFpEhAGOccw8EQdBhjPmQqk723rP3PrTWvhxF0Xgi6vHeayaTyx075fS7nlvxcPGgg8ZNIjHeSKRMdbEUIEHwEuCOA4DOvB25vSRnAfghMGxEFNRb7ZoM0HFNadFeIjvRgMFkhEDKbEl8Oqq7u3bs+/c9cXQUWo2iCGEYsqrG3vvHAPwEwL2qulZETnXO/Zm1FqoKVf2Bqh6qqr8SkW3e++tU9T4i+ntVnem9vw7ARQA6ReQ5AL9yzl3vnLsewK8APIfmovkiIrpWVWeo6t977x/w3l8nIluI6Dcicqiq/quqgpnJOfdnIvJR59wmEVlCRD9S1QeJKLHWmmw2hyAM9bhpp47q7q4d733aSVBlkBoNQGxgYPdVRZ82N5In9lS7dp42GgA483hMyUY0RXgwXzAjQgUtshp1WhOR5YgDzoiB0U2baqsPLB7z0oxxBxWz2Rxls1lh5gNVdbn3/rwWR68moi5VPZWZt4nIvgBGquoRAH5BRH+OprH4oYh8XlVPQXMvfIOI/BJAFxF1qupxRPRBIjpKVSe3dOtdInKbqj5PRIe3RHayiHydiMYDOIuZfyIin0HTfI4kIgAYa4y5UUQaAI4QkY8ZY5YR0aGq0kcE8k5NNS4t665u6G9r47xDCi8pqabsNbFe9WkoRvU0upYl8GunnqebX7kZQ00O9DipLbKjRfQTPWnXYyBTBxMBBiIML2IVkt20sf6B46d9rJjJ5chaQ0EQRAC2pWm6VlVXq+rZIvIXSZKELcX/Y1U9RlW/AWC8iJyqql9V1aOcc99W1SXMfAmAh1X1qy3O+rKIHCMiGRGptUqude9iIrqWiC4brisiDxHRt1X1KFX9qnPuowDGe++vUNUPishNLQkIiOjPVPVs7/02EVkLYHsYhtYYg0wm1FNmnZPftKF2lFPJisCIkhE1DFiFaNLr1i5R+PntGR5lFMcBLWfCxxbhrgkjgqMAjCKgkrWFX48KZ7RHJm8CziJLOXJpUNu4omAuOfbKOMxkKBOGHIbhHBG576qrrtLHH3/8QmaOdtdd/5tIROLTTjvtyc9//vN3BUGQs9aOA3CyiDxXr9dRrzfo2gf/Ljt1TpyYIMnWtQ4nVW2kNd+bri41fOlMADkQerb1p4/f+WGcaS9X8HOLUQIwCgCUdFGi6ehBt7k+3k4DqQ8cOd2+mQdPnP6xijHB+MAYhGEoqppL03T/J5544iRmpvnz5z+4Zs2a1dOnT5/+8ssvr5o5c+aMWq1WSdM0VdXORYsWHW+tXXbmmWcONV2jQG9v744dO3b0jR07dvSIESNG3HbbbbNFpHPBggWPtMTvVUREWL58ee2VV145bcSIEU+ddNJJ1RY4unLlytXTpk2bEoZh2N/f37dw4cKTrLUdxWLxvnnz5pnf/e53unDhwhPa2tpWnnfeecekabopCIIMEYGIyBjGCfufvmbpltuKY6a4LKkzCh8PpZu913g0oIsAOhOKMQTElyvYPrsY43IRP6uK8wCAYHrUo+gpiXoaG+LR0X5VaNgxNEAHz5pz6PIgMGBmBTCKiJZVKpUjjDEmTdPG/PnzPwSgLCJHoLlY/omqXgLgWSJauHjx4uNPP/30obPPPnsAwGNoLl+O32Xdt/a3v/3txnK5HM6fP/+3aJ2JAAi89zkAUwGcdOqpp+YvvPBCnH322fEJJ5yQA3CH9/5YY8yft0C+SkTmP/roo72NRqPjhhtuODCTyRTPOuusRy+88MJVd9xxx8cWLFiwiog+oqp3ARgVBMEO7xVzJ70/v2jdHbNGqu/16uq98WakmuQgANhsU98MRQwMP7N0iYxhUuybD/n3WzqlAMROROElzfY3NrXHrtTNFHTkMvkiGQNiZhGZ7ZzbPDx5IoKIXK2qZzDzd9F0T/0pEV2qqoeKyN8BwLZt27ap6hmq+l0RmQXgZhH5iohcpaqrwzA0RATn3DXOueta5buqeoWqnqWqT9dqte8DwPbt2zeKyBGq+l1m/giA7wL4map+jYj2S5LEA0AYhp0AvsvMp5577rn3Axi/YcOGxaoKEdkCYBYzqzGEMMgUWILRjXSopzfekFUf5wUKYXYQCoZhykcM08C+DMUMw7Rva8sHqHZCJFD1VtTDaYLuoe3xrLGH/Yu1NiZVtcYAQEVVy7vpmPNU9VHv/RUArgZQ9d5f473/qYj8OwBMmDBhPIBnnXNfAfAj59w5AK4F8DURmcfM1JrY/4jIrSJyq/f+XlV9vmVMPlEoFC4GgM7OznEicmPrB3hJRC4Tkc+IyI+897cFQWBay5lrVfVKVX30lFNOOUZV/aJFiz7YMi79RFQiIgbg2NrazHEHf7+70q1eGiwkROoteQkhOmIYp8DQBGUcYIVwOJMepCCAkBCooCAnUPVwXoU1rrXVoyi7nwgoDO1QyymwzTn34d7e3p8B+NsWFx4AYLP3/l4iuoKIHhaR/yaiLw1z6rp169Z57+cR0bUiAiIaVNU7ReR5Y0xcrVbPbf0ek1U1DwCq2qOqG4jofhHZUi6XAeC7IkIAvqCqIKItaG4LZ4jInxERvPevtK5fY+b7W+0eBGD78uXLx6nqd51z85i5G0Bore1rNJJsxuan1EumFo3w3mtKSupAMASNRJEACBk6ixWphWCaKs1tqegVUIWyiBcPIYhRQlLKhQccNDtW9YEIh0TkiciJyGFtbW29LfCCxx577PtHHHHEhdbabd77bzLzFap6jPf+X5o46Jf333//qWh6kP+P934HMx8F4HQA53rvkc/nl9frdYjIQbsw99SWy6opPvl8BQC6u7u3ENFfq+poVb1IRK4iIvHeX7dy5UpKkuR8Zka9Xv9WNps9n4j2B/DNkSNHnrV9+/ZRIvIhIjpMVZeoqlfVEcyQ6WNmpQ8+nyva9m4IO/XeQ1XFE6UKfYkUhyrTEVDEFkAWO4NuZAuAsPnDKlgFzih8ku0cU5y4NQiCxFrLAPYDUCOizxpjrgAAY4y54YYbvtwS5f1E5B9UdSgIgloURR8BIESEO++8c8qmTZtetNYeHYahdnR0wHv/pIhsrVarvX19fQsA5H71q1/dYq01pVKpkCRJXCqVaGBgwDcaDdfX1zcRwDELFy788JIlS96XJEnBOQcADSIKmfkSIsKwpXfO/bmItBljLlHVa6dNm/bIE088sR+AMUT0WRG5kIgmWWtfIWPcuPZJDJ9r90hIRVTEq5KAlBIIdYH0UCg6FMhZUvDvjSDVnZBhUhUSUijICxHCbDFXZGOMqKoH0KmqQ/l8/ptdXV0/rlar38rn8zs5hJmJmUM0jyPb4/j3h/ze+ylLly6dgr2QaepX3Hnnnefv7ZmdoyUamyTJWABoHvTtmbq6un4xa9asSQCuA7DSWvtSo9E4zHt/dbFYvKLRaKwF0E5EwoBENlKVMOPFkcJDCRBVUlEloLQTLgWz1987FAhImCECJVEh8Z6cdzBk20ITkIg4Y4xX1ZFoHuJM3XfffT/S29uLLVu2oFKp7HQ9/W8ia+2RzHyGqv6TiPzjsccei97e3kxbW9uZACYTURVNb7mIiIYmJIOwLUWqTqQVIqFEDFHV6nC7orDMBB22LOzhWbRC0LJRLalqGYqyQWAJVDPGVJIkqQPYrKq9AGCMmQoAaZpix44d2Lx5M/r7+5Gmbzn4822jVatWvei9/9M0Ted77/9j5syZawAk27ZtswCgqt0AtohIzRhTssZWDdvQkA4RtETaxAOqZSWWnXgR1Kr8/kTbG2ThtaAE9QQSZWIQ2EilFteyhoJCa4lxYMvf9xry3qNUKqFUKiEMQxQKBeRyudcVsXeC0jRFrVZDtVrFzTffnOnp6Tl2/Pjx944ePXrt9OnTzyGirY888sjLCxYsOERExhPRDGvtswACrz4m60pOqIMIBIX4ZqCYAWsZLXumAtid6z8A5DSvlgkKFkcMiBERqHUDiUu8994SkQCoEFF+jyPfhZIkQX9/P/r7+xEEAbLZLKIoQhRFbzugzjnEcYxGo4FGo/EqCejp6Tnv5ptvfk2dH/zgB8sWLFgAVS0CqHjvyTlnq2mFYF3VORnJICKwI2IFI0Qi7TCtLaYCVgnbAdoA6GRhaoPXhipIVJkEUCXP7CrleBAd2RHsvYcxpopmfMreaICZN6LpQWYRmZSmaeeuk7LWIggCWGsRhiGstWBmWGuxqwUFABEZ9ilCROCcQ5qmcM7BOYckSYbd/XuiTczcT80YHHjvZ6MZZ4O+vr5hx+14Va1Qa/M9WB0Asa+SUCcIRuAtg5QEBKDYrEJrwdhiIXhBRQyIJkMxQxQvkELh4RUq4kCJ2VHdOLiOx+YmmTC0trWwnQOgsvtoiegFInKdnZ3rRo0aJT09PTw0NAQAm0VkzvBzw5N/B0mMMU+pqhk7dmxXsVjkzZs35xuNhojICDSPRpPt27c/WSgU5hLRC95722g0aOPgWnbcW5VUBYCSJYBBChgQzWnt2J4BsJyheFkVr7Q6Hc2kZYU6ARSejCjZFN259UOrc6reOucMEfWpqnXOPQIAhULhN8PgMXNl3rx5Y4IgOIuZz46i6KyTTz55JBFVmXnFO4nYrmSMeTKKooEPfvCDs40x8621Z3d2dp566qmnxsxcArC1s7PzkVWrVi1X1QBAv/eeiYg2DK0upOgpiCBQIlIBBOrBOgTCCAAQ0jUQrGS1WF1vUPewLlTlKoQCOARewOqVUgzmtlXWTWuKiqiIVAAgjuOtuy1bgtNOO21ET0/PhO9973sQEXznO99BT0/PxJNPPrkDQAO/97C8k7RBVaO5c+ce19nZmb3yyisxZcoU/NVf/RVWrFjx/kMOOWQ9M3dXKpVRjUYjbKmGinOOnPPYWt04PZGhjHoQCZigAQsFpFwbxqlRpx6k6LI6gK5Kpz8zm20d0JHWQFAYTSUlALDexSNdEB+Y+nQxpZRlppSZ4ZybdPvttz9QqVSOt9Y+SkR+xYoVxx522GF4/PHHceCBB2LZsmWYPn06nnrqqQOZ+REiekZERr+T6BFR37hx47rWr18/NwxDvPLKKygWi3jhhRdw5JFHolarzXvuuee60jSdYFordxFJnHNI0rghiGc4jb3xUDEQEngyYEBrwx7KcuJHZzux1t79KZQ++iv5AHTnCadVBZGQhULh1SsIMfoe7KlsGRqTm5Q1xmkQBJtV9dijjz766f06bwAAEgVJREFUnpUrVy4EgIMPPjh300034bjjjsOaNWtQqVQgIjjqqKOwZMkSzJs3b/Xy5cstgFUA3rZF954cr6eccsrYxx57DJ/85CexcOFCDA0N4cQTT0S1WsWjjz4azp49+4l6vc5Tp049TVU3eu/hVXVbZUN/TH33k8c4DVRIiMFEohCjCIdXLC6VY+44DV+zACCEXiiWgnCkEp1EpKsEqqTEIsTq1Axg+eCy/kczp+QmqDZfuXpRVedNmjRpx9VXX32hiEBEsHTpUtx5551YsGABnHM47LDDcNNNN+GAAw7Al770pc8NPzdsUXe1rsOA7n4dBmjXK3NzgbHrZ2beWQDg7rvvxq233oqLL74YS5YswY4dO/Dkk09i7ty5uOCCCz4bx/FPRGSUiNydph71ap2W9T9eGGgsr4iqZSVVsLJ6Z5lIlU5srfmWAlgHtE7lDjgP5SjgAWb6MBTtoroMgpwoERTwniiJhwq5aPrxB+YOWwuQIaKEmWd573NBEHSoKosIpk+fjltvvRWqitWrV6O7uxvLli3DV77yFRQKhVeBtzcgd/2+exmm3bl3dy4kIowfPx4LFy5EpVLBpk2b0Nvbi+7ublx22WWw1ro4jgsARgJYVq/XUG/Uk2fK95+ypXxfrESGGUIEMhYGTP1ovQOYOr2+kcjvVt+K9c130cp4slyX4nDnBqYbRCAGkTZXUELIVtPeezeUu3rjOEaSJFDVpwEcmKbpLcMTnDhxIm644QYEQQDTPDvBNddcg3322ec1IL1e8d6/qryZOruDffTRR+PrX/866vU6kiTBAQccgOuvvx5hGKI15hki8lTz76lura/fUUt6F4siJIKCiREAakhB6BnGp1ST9lwbngJ2CfE99Zd4cPzIcDqg4xl4wQl64EE+BlyicCnYanHz4RMumviR9vO7C4UC5fN5JqKzVfXlKIomtzzGr5nwGwGwOxe+ngi/ntjuXowxe/s+0Gg0+ohofxG5o1KpoFqv6+LBn496dssPt6dcmWAtlCOCNRDKgJgxEopDoLRl60Cy5p5P4Hhgl/A2NbgmTuUGBeCBOUTokVZAtyiIFJSk5QmJlJKeyvaeer2u9XpdVPVxVZ1Zr9dv25PI7Q7M3sDbEwe+0Q+wt/b21vdwqdVqv1XVaar6eJwkqNdj9JY3bW9IKU5cZRwUDNPcuagBE2G7Kg5RAKnI9SD832HcdgJIARYOVdyknXtjoTpBoaRsTPOMHQy7fMutQy/qQzOr1arW63VNvd+kTc/NfO/9I3vTXXub0N5E9/U+v57Yvp7+VFWkabpYVc8DMJSm6aZyqcSNRk1fxOMHPb/5v+pQtWwgUBCxErGCiOJhXHYMuRkU4r7XAHj3aYhTAaC4rakI9dNkMMSWPBhMSsRKmjRKIyuuZ3Bzfe32crnGlVJJReQ+Vc3HcdyuqgPD4re3ib1ZHfhmVcDuYO4JxNaYetI0HYvmMen91WqVqo1YNqVdW2uutz9NSp3KTNpcxMEYgjEYVNULmvVxiwLVu09D/BoAAcAZXL6j7F9SBVRgiUwPkRJYCQaqrEoMWrrqp4WN2ZfmxXGtWq7UqFwuJyJyP4A5cRw/qKryelywNw7ck+58I336ZvtR1Uaj0XgewMEicl+5XPblcpXqtXJtk33x1KUr/6MAbnKdgQKsDFUVMTtUYFWBvpLvohRX7orZqyJU192K6tSz9Qv5HPcQaCpBZyvjRSiyEFIVkDioiBbL1W3LglGduWJ9LKDExnAtCIJEVU/w3t/MzIfsbiD2dn0jHbkrF+1qSPZkXHY3MMNX59ydaB5ePdNoNLZUqlVfrpSxOvO4earr5xvqvm8iGfggBFNIyiGYQwwQ4xwABqqLhmo+c885eJVf7NUx0gDE4iv9Q/JYc1+MDABvDJQs2DDYhlBmxD2Da6YNxOulW9dsr1TLWiqVtF6vrwawXFU/7Zz7TwB/FCf+MUuW1ylJmqY/F5GzVXVZvV5fWy6XaahU5q26asuA22L7hlbvR4a8NVAYKFsgMBACJZDm7mNHSZ41HpfujtdrovS7bkV58p/oRwpZ8zIIhwM0C0SLoBipCmqNnaHAhq3L7MT9D9mfhjIrrYRt3nu0fG9VAKd673+Npq8t82a5cW9ADdOb4bZdljfbRWSpNt9BeSJJknVDQ0MYHBqiwXRHd9+IriPvffpa4YBCE0I5grCFMRlSGFoF4DMt3ffDUtXLPfPxyzcEEADGnoNH01gWFLNmChQhgTJEOqiKQIQEAiPNU09+Zf3jfZNnH3yY9mVWasoFL16sMWVm3gzgNO/9KiJaq6qTdlfyewNv9+f+QNCGPz8qIgLgaFVdVK83egcGBk25UtWBel9f/4Q1x931yFUbYLWNIxgOoDYgDSJYE6IB8CEEjFKg1D2QdscVfHn9r/EaB+YeAdx8B9z0+Sgz8HxgeR6AMVB6hgzaVMk3Q/2JSQHvJOra+GTXlMPmfEi6o+d87NpTLyTeN5j5ZWae6b3fV0RuIaKZqmr3ZJ33BNzuAO4G0B7vMfOQiNyqzcBN8t7fN1QuN0pDJVQqJe2v9u2oTt9w0l0P/uNz3iQjghA2CMmEGXgOCSYDIqJuAk4AgHrDf7We6u/uPx97zO6x13fl1tyOtfucqRcXM+ZFAHNAmA2iu4gwRkBKos0jAVXy4vKvrHvslWlHHHZk2m1eQKJ5VfXOOauqG4Mg6FXVj4nIalVdpKoHqSrtsrzYed1VXAHsDaQ9caAQ0S0iMoqIPkBEDzWSZHWlXI6HBkvBUKWsQ2nf5uSA7SfeueTqFxPUxtpQAxMSmxBqAhKTBZhoBYALAUCBW3ZU/D6Lz8E1e8NprwACwKQv4nf1fvlUMWsJwEgC5oDpIVJ0EhGrJ6sAICCXuvYVqx8uzXj/YZPSWFbWelyHeA/nPRLvqwxa3XRN4COqugrNKPwx2ozifxVww1y3K4CvA95WAHdQ8xWHDwJY4b1/tlwupwNDVVTKQ9rfP6j19h3dsv+Ow29bdEWvUmO0CWBshowJCTZL3kQAW1pPTb1noPTK9oG0no7Cp9b/7LWi+6YAXP8zuMnn4rFG4kfnQ3MYgIgIU5jxDCmKCigBpE1xZlEfvPDSErffrFkU7BNQpSutxQ1PLo6zSerFi9RV/CvMXFXVQ1R1H1VdhGaIbxnAzgQ5u4vtLsUx8yMA7mPmbQAOJKI2VV2XJMlLtVqtViqVaLBUlUqpn0vloTofOhBVMptzv1h4dd4Yn7cR1GSJwwhiQhIbIjUBthBwJoC8ElzvUHqzKL5+/+l4zQuGu9Kbyplw4m04Ix/xjI68+W6r2gZifdI1dFSaEEtdOW2AJYG6hnqXEMaOnL7ptGO/+L5kjVks2/JjM5nIZKJAoihLmUyIIAjIGANjTEBEHSIyWUQ6RWSdqm5V1YqIpC3RDImoQETjiGgKM5eIaKOIDKpq4r2Hcw6NRgO1egzvUq3V6l5Hxhuys9OPP7T0lke7tj41nQNiG0FtBmojeBMR2yzIRNhKQh9U6L6kkMGq/7t6Ii8uXoDfvRE2bzprx0n/hc93FLiQi8x1zYq0CdAHvcdkV4V3Dupi9b6OgosR+wRGvU3PPuXSHcXcPiMGnvAvcJIZlwsjG2UzMESUzWa16SExZGxLGFS9sVbFK5SUAGBYWYoIMzN5BbnUgSCaph5xXCfvvSZJouVaw1NWejrfL3NK1a07frHwmpFsXcgRvA3hTRahNeRsHmKaXpZtIDoa0P0AoBb7SwZqEt+/AP/6ZnD5g/LGnHwbvtlZCAYzAYbzJwwo4U5xOl0aUB8jcDHUxUSuoQ4pJE0gmbCt9vFTLm4UM2NHDCxNlidDweiQOAyCUDkwFLBBEFhSZrVEqkDzHLEVAiA6PFBFE0pFkjhS9YjjVJ1Lkfg0sZ3SO+rI8NBSo7vvznuuz8S+lDMhwBbWhmRtVr3JgmwAmAhqAlolij+h5svfqMW4ZKiaFu49F1e/WUz+4MxFJ92GS3MR246M+bYSGEAizD8mJ4d6p+oa8L4OcQnUJzA+hhWnqU+gUdA2cPKxnylNHj/rmOrW9N7+F5JGOiQjyXIYcgC2zRejiVXFw5Np5Y3xMGxgxBMJPMSlFHtPUI1NG/eNmhNm8uODUzZse+nB+x78WVs9KXXaDMgYspyBNyG8iQATwIRZwIawYPOCQj4LICSFDNX9V6qJ5O5bgH/8Q/D4o3JnnfhzfC6yvM/IdvPXADpaLd0KoaJPNS+xmjSF1QYkTeEkVfYpGR8j9Q5WRKvjRkztPf5DC3j0iCkn+AQvlDdUu6rbXaPWn5KrCEEErTwXTTKALbDmRgSaGxNk26bmppoQc7p7ux546PE7ZHvfutHGUJ4DOGMRmEi9sSQcwgYR2GTgOCRvDFXVaJUU81sA9PcM+X92Trru+yT+8w/F4o/O3nbyrTiaGF8cUwgOIMZRreZegerDgB6YJiQSw0uqgYsh3sFrjMB5eE1gfAovHka9pjaM+ke2TxiaNnWujBkzOcxnO/KFXKHNBpnRAODSRm+lVh6q1odqPT0bkjXrnuW+oS3tLo1HsKGADIQDsAnhjEFAFgmHsDYCmYBSG4BMRgMQvQTQcYBOBwBVPN5TStd6hxvuPx9L/xgc3lL6u5N+hpGwuHl0u33a2N/nDiTSXxBIRHWCNMilMdQ7DSVF6h1YUxXvyKhD6h0CCKCCVLxa9YASKYlyK/AOIJAyCUFBDGImB4KlEEoMbywCCtQbQ8QhxFiEJqDYWLDJakBEm4g1UKFPDI/Rq16xY9AdZQzOXzgf/X8sBm85AeM5t8P0eXwtItYRbfZToOavCyDxKj81RCPgaKJ3iL1TAw9xCVgdvHcw6uBVm/pNvQIKpwJV2pkKBQCEFKoMYoKFITVGQQxPBsZYeLIwNoQQw3BAjiNEzNioQKzAebQzkJRW9lXcbXEqctx5uOryYUv1R9LblkP1+JsxjS1+MDJn7wkDuhKEHACQQqD4OUgExJPFq/EpqTglcXDqEXoPJYETDwbgROBVAQY7ABCIJQKYYQBYZogaWGMAMkhhEJiQPLMaG5BTlvWUsgXjvJahAxS1RqpfH6i5eYjxhfs/i7clj+rbm8VXQSf/HB8T4LOj2uwzgaF/0GZ2oeHuVqjq48zIQzHee4QiSLUZgwN4kDYdt0Kkqq38BM1XhYnAMMwKGDQ979y0rERIRbENQJWIPgDorF0m2Ei9Xt0/5N4njH+//zzc9XamRH5H0iAffiOC9gLOVeD8kXl7bxjyxYC+OqMv0VaoPsCEukAigNqg1EEEFlWBQKHUFC9SBoOYiEUhRDoIaInBiSgyBDpJoeN2m9qG2Mv1/SV3iir+s1zFbc9chLc97vgdzWR+uYIfugUnC/C3keUlHQXTaQiX7LUCox9en1XwIBENCqTcvM1FVe0gSAcMzYVgxN6a8IrrBit+IHFyrCF850Orcf/ll781Pfd69K7l0j/mJxhtLb4+ot2uDy3t1T30Vihxeml/2U1WxpVLPol3PA088O7/MwI6/ib819j2YDOb154vvBVSxfXdA+nEBz6Ns4G3T8e9Eb3mUOkdJsW++NT2UjpHVO/V5vrvrRfVh7f3pTNLdZyLdxE84N0HEEtOgMsRzukdcBUV2vRWwYOnbTuG3HZXw4J3wki8Eb2uQ/WdojW/RLz/n+CluKaZTMhzm4eJwB9aFHADFf1X7+X6h/4MG9+LubzrHDhM934KLyhoaSPB3/yx3Nco42+811UPfBbvWvD67vSu/0eb3enEn/K17RkeNExXvPHTvyfxeuVQQ0be9zn50hs//c7Re8aBw3T/Z+TScl3niuBm9cCbLLeXGjr3mA3yl+/1+N9zAEHQ6oA/rxLLBPF49o1Fl54vxVJ08Ge/kwvkN0vvPYAAHv8K6ur8BbVEnlNF6XUArNQS/ziJv2jJ5/Cm07W/k/SeWOE9UddvUJ5+pimpYhODTtyT1Y29fsOrv2fxhXj+vR3t7+l/BQcO0z2fc0ucEyeil+7OfV7xFYXI4gvx4Hs9zl3pPbfCeyA67cfmFiaziVX/BgCUcL1XGf27z/vz8S7vNN6I3t23oN8caW0//+lcF/0PC+4VIBJgZm2aPw3/y8AD/peJ8DAtOQEuZLfAQ0sK7Q0rbv6SE/Yen/L/017ojH8LZ5/xb+Hs93ocr0f/D6s769KBP+5xAAAAAElFTkSuQmCC\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic3bx5uF1Flff/WVV77zPeIQMJIYQxYRRBpBGcQFEEbVQQUXB6xW5tWx9+Cm07IYIitiJog2P7qu3UCN22aDs0KIIyg0CYyUhCyHiTmzucce+qtd4/zrkhQIIogz6/9Tz1nHP22buG715D1VpVS/gLktnZjg3P2wGz3RC/N9hBCHtjMgOxGjDZv3UAkyZim4EHQO4i2j3UkxXUb9kkcrb+pcYgz3aDNvLjOah7BSZvRnwLX/8D2axILM0Dtx9ODgGGt/P4GNgtqD6A764i3+iJ44dC9CD/jaRXyqzXrHs2x/OsAGhrL9sB8W/B7ARKwz/H7TAE2btAZj89DbAW6X4HHRknnzgW7KdU5fsyeMKmp6X+J6BnDEAzhLWXHYz4c3GlG8l2HgL3frDsmWqzR5KDfpnOykkIL8D4OHPecIcI9oy09kxUaut//CKCfp7qtMuwwb8DnrOd5nOcXIfJCryAOgEpASUwh5HiBMwKEAW6YF1chIghthtqL97uSxG5C8a/TXvsBLx9VGa/8Yane6xPK4C2/kd7EuWblIZ+itU+BMx93E1OFmLchqQpTmaDA0kAlyDSwSwizkAFfN84RAfOQASLCVACDVgAESOGDZjmSDwEtYO20bVVaPMCionjiPoe2eXNy56uMT8tAJp9I2X10GfxyQTJ4GtADn3UDd6NYv5niKtgyQxcYjinkCSYi3gHikeSLhDwXjHzTNlWB4hEYnRAgoUSmIIqxAQsYEFQA/JRNHZw+lqiTn90R/VGYuNKQl5j/cTH5JD3FE917E8ZQFv23b0oJf9GNnQd8PFH1y7rkORKLJ2BTxJcAqQOSQyXGEiC+IB4wZxHXMABeIeZQ3q/MBQRhdiDNGqCaMSiYTFBKIiF64FYKBbAigKLD6PFsYjt+uhOcwHdiUMRe5fMe8uSpzL+pwSgrfje+/CyK1n1dcBeW/7wMoZll4Kfhy95SARXMsSDSxxkIInifIK4gHpH6kAlggjOOwTBJEFV8FIQDcQCGIh6ighOFdMELQKYYF1Bg0KgB2ZuxG4kFqvw+cmoDG7V/cXk7Z9iulx2efvX/1wM/iwA7bLLPIe1v4m4RSTJuWDJI/+675OUB3ClCmSC9yA1cKn1dF3mSFLDRJEsAYk9wJxSTEzQGW3TXlEQC6G7sde/0gzDZ0Zlt5Ty9Arp4GBfnBWCgxghOkIhkBsxGK4QYgs0gHZAQxPtNLH41q2GH4jhNFRfwrzK20ROis84gLbkohLp4C/IuAXso1v+UFmLlK4gLc/Bl4AMfEWQDHzZIAFJhKQMLgWzhMayjTz0kyYbrt2T2Nq3a5WFE3HWqgYzNxeU81ao5ADVpJ2ldLI6G6YN+o3zStI+CF+9n1kvWcYub6hR330mIgEtIHSAYFgOVkDREegYmoO2QfOHCJ3X4thqDirn0rUX0Kr9rex/Uv6MAWhLLiqRDPwPafdBxN79SC3yS1y9S5JVoQpSEnylB5wrgSvTAzCt0Vqzmfs/32By8cs2xZ2vXGJHjo/KnlVz1aEsLZsXKVQkpIlXTHo6T8wFjU5iTELULFpEQmN8Gsub87lq2gy/5pUM7ftb9jtjgNJO07DQQHPBerMeYqsnztaGmBvabhNbVYivemRwfJWitADktbL7OztPO4C25KISvnolWWMN8OZHasj+L5Luih9QfAWSmvVEtwquAi4DyWay6aYHuO/8A1phYOkd9sbVTbdgVlYqJVk5wycpWZaRJY7E+2i44L2Y0Zv8CkgMCMQ0xOCKGAndnCJG8m5ueacba7pkw/Pksp2rvrkH+3/kXqY9fx+cbiA0HbEDdIzYBmvRE+12l7y1GRffsRUj/JBubR6xdbQsOK37tAFol13mOXjzzymNrQL+/pGnS1/FZXvg64IfAKkqSTnBVRRXEaQ8g7HFK7jnU/NHde7tC5N3RpKB4WqpKuVq2cpZSpJlkvrMSqVEvE8ty9JClSJJJIr44BwWY0xjNC9iWZ4HH2OUdrsjzpm1Wi3X6RTW7XZpdNviwsToQfodP92tPpj9z1nG8Pxd0M4mYtugEygaGdpWQgO05aCxFI3/+Mhg5WsUQ3vx0Npj5GVnh6cHwCVfv4x0ZAViH9py0WcXIpUDcPVIOiBIFZIauKrgKg6z2Sw8c0m72XK3uA+MuMq06dVqzSqlTGq1uqVp6iuVsqRpGtIsM++ceO8NEQQi0AGmuKAElAEfYxTAQowUeZBu0U3zTkc7nVzzvGOtVpdGqyHa3rTpUPvSjpVyreDgc/dGZB3aVrRlFE3QlmBtoxhXtPUQlr/nEVTceRQzd5c9/+GUpwygLf7Ke0jHykjrS1suavpV0vqeuDqkdUEGlKTsSQYUqcxmcvky7v38c+6yt/xqvHTwvEp90OqVTKrVitVqNcrVasiyTBLnUhEBGAXW9ssqYD2QA1MT3bQP4g7APGBOv0w3M4kxxm5RxG67nUxOTkq7ndPO29YYb9pQ55a1z3U/Opb9P3wXA7vtgbbXow1HbBk6aYSGoA2hmFgK+ggnxoEPYENR5r/3y382gHb/xQeQFm/Gr/0ImOuD9zXS6p4kg4ofBKkJyYD1gCzty9IfXlVsumuXm7NPrC6Xh6sDA1XJyhWmDQ1ampa0XM689z4FNgP3A0uBdr8vSk/veXpceFj/9y39/6ccAq7/vQLsCewHDMUYQ7fbtWaz6VqtXJutCWt3OtJubBo7rPjMXsmMA5cz/60vJ3TuJzQEm1TiREJsRsKEElqrcEWfE0UJcz4P2Q9kwfvv/ZMBtD98I6XW+QXZkj0Q9uzdLZcgA8OkdYdMF5KakgwK1AWfPof7Lr52vJk27qufVq0PDKTltOoGBqo2OFi3crmszrkB4CHgAXpc5vpgSf/7MFAD9gVe0AcHYAlwK3Af0AQm+gDrVmV2H8i5ZtZpt9s2MdGwRqNJq9WUVqsZ9m1f1BqqxAr7v++laH43sWHEhhAaRhjzMBkJExNgJ/XhWUK+YDVx9FWy/9nbnN4k27oIQK39FUqLFmLhlX1beB+U67jMIRlIGiF1kApen8PCzy0cYZ91Dw2/dadp9ZqrVWtFrVZLa7UyWZaVRGQc+D2wiR73zARKqhqccwqIquKcmzSz14rImUC135uWmZ0nIrf0fw+oqjjnhJ54d4AGPU6dJSL7VqvVoSRJOlmWSKmU+KxSssVjH6zOa/5g8453nn8bzz3tEHzpDrTrECeQKnjBJSmhdQ+izwEWkNx/Oez9ReB924Jpmxxo91+wl0rjjc4vO7d3wQKu8kP84CyyIUHqgh82/ICQVA7k3m9evybstXb98FtnD9Xrrj5Qp5RlWq/Xnfd+AFgILANSVfVAHZjVf4E1EZlhZs8VEWdm3xCRD/QBfqSjIhtU9SIR+XszUxG5T0Q2quokPV25EZjsv4wA7AI838wajUaDZrujk+NjyWSzyZzJ/3pwTnrffPb5u79B2wsJEylxUonjkTAhxPENaOcURBIADfM/6bT+H7L/6Uv/KIBmiN1z/tWS3TqESM81JMlXSQb3QIYgGwQ/CH5QoDaflT+7cXNjYGLl4Lt3qA/W3WC9rtVqxdfr9QxIVPVeYKNzzoCOqjpVLSdJUg0huCRJusC4qp5FT7wPAlYCP1XVkf5zs4DXAbsCtwN7OOfO7nNiRk+EWzHGwntvzrlSn0N3APYzszjZbE50Wu1sfGKcZqNtu05+ffO0aitlj+NeSphcTGyATkCcgDhmhInlWJziutst/E1D9v2nIx/rmH08gPd+7ijSpc/BRnpW1+RWXGUd6WCGH+5xXjJo+MFhRpcu6q6+a2jJzHO65UpdBgeqbmBgwMrlciYiqqqLnXOFqk7vc4WPMRpQTZJkB1U9FEhF5Fwzu9DMbnfO/VBEXqyqrwGmHKW5c+4XZnatqr5dRA7y3p+uqh8DVFVvE5H1QAtwIhLo6dSNzrkKsEeMMQ0h5GNjY9rpFH5iYizutemTrjTvuS0G91iANcYJE544HoljQpyMxOZcsAN7SM3+AHHPhbLvP/9ua7zc47jP7OPEhz+KdcA64MIdSJKBBxFFpPcGigmx1Tc/f9G0syZrtZofHKi5en3AyuVy0gfveufc5qIo6mZWAWaZ2Rzvfdl7/3AI4SozGzCztqqeG0L4ELCPql4QYzzezFRVH1DVB8xMY4zHq+qFwHwzOyOEcF6Msamqg865K2KMK0XE05vaTDOzUgihqqojqnqD9z5kWZYMDAwmpVJGvT4gS6af1baHbzyY0FQwD67nzBVngMNz4xYcbOXHLMRzzEy2CyB3nfdicQ/8FOvMRrsQu78A2xWJYCaY6zGwxf1Y+quwbNrp19QHB6u1Wo16vUalUk5EZGdVXaiqQ3meO+/9BhFZrqqr6OnAE83sH/ttV4HvAB3n3Plmttg5d6Zz7sNm9i0zu69fvqWqH3bOnWlmy83sAhFpiMh/0JtgO+/9e1X1TTHGATNbb2YPOefGVbUEDKvqQmBOqZS5wcE6lXpdqgODlWXTP/Iblv48RePeOOt5wrUvpZrvDt3/7WMxS9zi/+aezx2+NWSPssImeqbY4j23XJDSeszvBAJODFHBWcrIA1c1sr1zN7DHjqVKhUqlQqlUcsA8M3tQVV8MNEVkjqoGEVkLPKCqVwEvA3Iz+yDwG+BMEfmtmV0hIifHGM81e3T8Z+p3jBHgTu/9h4qiOM4591ERuTKEcIb0JCMTkSuAGWa2v3Nujpk5MxsFEhFZ7ZybWyqVHq6pOrQaJuPOcxvNve6sjy2+l+Gdd8EkIAJ9LFHWYFMLokXvN9vzQWCLE2ILgPbA53bS7qrfi3WP7l+6GvxcJPRG4Kwn5LE1l/FVu63e4fM31CsV6pWKZVlm3vshVb0S2BBjTAG890PAAar6ahE5EjjPzOohhLO89xeaWQ6caWZnAS/vA3Wlqv5eRFqPAbEmIkeIyCtCCAeKSEdEPqaqfw/MVdXTReRCEXHAe4B6COEqEbk7y7LNIQRCCEWaprO894eWsmyTxuhDCG7NzH8s77X+jBcyML2AsBIxQ0xwKgTdCcl/h9kRwAJh/fds4blz5aAzVz+aA7vtE527fi7WXz87/wCwO+ZAVIgFuODZuOqmkfrrJirV+k5ZkkiWZWRZtqOZbYgxHglUnXOJiKwMIdyRZdnVIYTXmdl6EXm7qq52zn1BRD6gqv8CnAtc65z7sarua2avF5GjeQyJCH3R/IaZLXLOvSHGeB498f+AmV2oqqu89/8nxjgmIrPSNL0qhDAjhPBiM9sdiCGEpvd+JE3TOTHGDZVyOSpUNtVe/fMZI9cNMn32LliMmBnmIs4ghnshHtHrybU7Iq9/A3DRFgDNEL1l/U6uUhzRN8wbUD+vF8PAepecoHEandburbkvv7mSpZTLFSuXywnQCSFc1xezIefcc83sOOCQoig+18fgy2Z2gZl92cyON7MvAb9wzl2vqqfHGKfW2rmqLnTOPaiqK1XVJUkyD9iD3grlPX0wNwIfU9WXmNmXzGyVmf1cRN7rnDtdVS+MMTpVfTcwA/gVsFBExsyscM4dVy6XnRmxiGqTw6/cYcbqKw9nmo6CbEREURFIDPW7IGETyAwIL9PO+oZZ7516gLOP/budTZfuIXp3FT89Q9yvcaUhpNTzKLuy4TJlcmLZWPqi+2P9wIFqtUq5XDLv/d5mdqeqntLXQaNm9gBwrIisMbN5fZ10sHPuJ8C7gbu9919X1XeZ2dH0ph8Xq+p/A8tFZJr1RObFIvICM9vVzB4Efq6ql5rZXSLyfOBvRWRXVf2EiMxxzh3vnPt2jPEdgJjZcF+kZznnvuGcK6vqwar6ehG5XUSeZ6YbBcOMxIrx20v5is2UXA0NPYesRgfqIf4Bs5l0H/yDWHUZq45eec63/jDpALTg1c7dNouox9Nafh1KgKRnrhWPakKINZrtlzSnv7ZWq5UlTb1kWVYG1hZFsczMFpnZ61X1VBFJrfeKvm1mL+nruLkhhKPN7MNmdngI4Twzu8Y59wHgWjP7sIhcaGbvV9WXqGpZVVv9UlXVl6rqaX0996GpZ/v68jwze4GZfTiE8BpgjpmdZWYvCSF818wwszSE8E4zO9HMVqnqMhFZm2VZ4n0qWVay1ow31Gg0DydaBTMH3qHiEGeoy2kuv4YY3wy3zUL1WOjLq133ritxP3w+2HSgQTrwc8p7DeBqDl/ueVxi1o7Nimza5VNFqVqlkmWSpulBqvrrT3/603bTTTed6pwrPVZ3/TWRqnZf/epX3/ye97znZzHGwVKpNEtEXhVCuL3b7dLpdGzmio9XZKBb4PIKoQXSNEKrS3dJh2LytfRiFqMWTvmDe+m3X5WYne30+kUbnFkvCC38L0U+HdZ2KO9uSMxw0Wh3xyanvXHCe79TImLee8ys1O125998881HOec46aSTfrd06dIlCxYsWHDvvfcu2n///fdutVqNPM8LYNqvf/3rI733d5xwwgnjU4MaGRnZuHHjxk2zZ8/eYfr06dMvvfTS/VV12pve9KbrZWrSvhWJCHfeeWdz8eLFr5kxY8YtL3/5y1t9cOyBBx5YMn/+/N2yLMtGR0c3XXHFFUclSTI8MDBw1THHHON/+ctf2hVXXPGyer1+/ymnnHJkURSrsiwrpWlqeZ6Lc07Gh49bOty4ZJB6UcXFnh+gvcbQfAbGlcDrwaabdcfMznYJt66Yha2+A3hLr4tuBGQIbWe0V7Yp7xlJY5mQPjfUD16YJok453DOzVTVmycnJ1/kvXdFUXROOumkF8cYJ0XkkBNPPPEgM/secBpwu4hc8etf//rI4447bvyEE07YDNwAvA04cqt535L/+Z//WTk5OZmedNJJP6PnsgJIY4xVYHfgqGOPPbZ26qmn8sY3vjE/4ogjqsB/xhhf6r1/N4D3/lxVPfG6667b0O12hy+++OJ9yuXywAknnHD9qaeeuujHP/7x604++eQlIvJKVf2ZiMzy3m/IshJh2iE1Ri/dn8I2o/lGOg8NQLeKCuDWYr04l0tX38rNuoOjU+zpdHHSW2EAKiWE0PO2FTW6K0qE8fWmyXBWqdfTNLM0TUVV91fVDVtzhqp+xjl3HHC+mVXN7FQz+5CZHaSqHwVYt27dGjN7jZmdr6r7hBC+r6qnq+q5ZrYsy7LEOSchhAtCCF/ql/PN7BwzO8HM/tBut7/Sr2uVqh5iZuc7514FnA98N8Z4ppnNz/M8AmRZNg04X0SOPeWUU64WkTkrVqy4sq8bVwP7eO/FewdpdQjxMwgTG2g9tAMxr6BqGBGTdAtO4QFH7ua7qLoXrjHvEQBtGmopph6LghZCc023M/yCr5mZgk2tCBpm1th61aCqJ5vZ9WZ2DvAZoGlm58cY/11V/y/ATjvtNBe4S1VPB74FvBG4EDhTVY9xzomqmpn9j6r+SFV/FGP8jZnd1Tcmx1er1dMAhoaGZqvqN/ov4D5V/ZCqvkNVvxljvDTLMm9mOOcuNLNPm9mNr3zlK1+oqvHKK698oZmpmY2LyIRzzkTEvEinW33ORbTXdqEbUQUjwUiINn0LTtaYi9meiWg8ACkO6GPQQaRCtBwfCyIlEEOHO6jf1bmkSJIkAENmtjaE8KrR0dHvAh/pc+FewMMxxt+IyDkicq2q/peIvK//tlm+fPnyGOMxwIV9Sz1mZpc75xYCRbPZfDO9KcjuZlYDMLMNZrZSRH6rqqsnJiYAzldVAd7br2d1COHMNE33VtW/FxFijIv6n2c6537rnFNVPRxYd9ddd80xs/NDCMc659aratk5twFI1dUXEJM2lnchRlwUMI+TKpCjZEjYT0PsJBJ1fxwHA2ByD6KG8wENAWcZBYKUM4b2ayeJK8eolqZJbma5qh5YrVbX98FLb7jhhi8fcsghpyZJsjbG+Enn3Dn9acyXVRURef+ee+65B70A0b/EGDc5514A/G0I4c0A1Wr1rna7japuvadwd9VHtkEPDAxM9EV4tYicEULY0Xv/9yJyboyRGOMX77vvPpfn+SnOObrd7qdKpdJbzGxP4JMzZsw4ft26dTNijEc65w4ErnbOdfO8GEqSxJLpB25ktFzDaUSDoNERDEQdjvsxDgQ7VFRDglgZo95TZPogJlUsRpCIRkEAqQ672ryuiQQRSVV1DzObEJH/k2XZJ/uK21988cXv74vyHqr6MTMbT9O0VSqVjqHn9OTyyy/fddWqVfckSXJ4lmU2PDxMjPFmVV3TbDZHNm3a9Gag+pOf/OSHSZL4iYmJep7n3YmJCdm8eXPsdDph06ZNOwMvufLKK1/5+9///tA8z2tFUQB0RCTz3n8QwLmesynP83enaTrovf+AmV04f/7862666aY9RWSWiLwjxniqiOyRJH5pURQastkxteowRW6g1tsVZgZiRHsIOBBjEI2VBNXe3kUAtSaQoma4QsEJFoQ0rbq0hjmX9yTKhoHRWq32yRUrVnyn2Wx+qlqt0g9R4pyT/pywBAx1u48E+WOMu91yyy27sR1Kkt7y/PLLL3/L9u6ZIhGZ3e12Z2/93LZo2bJl//2c5zxnLvAl4L4sy+7tdDoHxxg/MzAw8KlOp7PUzIaT3to+NwYMyUpYXiBqRAOHEdWDTiJTLkHFoYVsUYxCCwgQlRiFEAQtDEmHVBLnvS9UNdCLV7SA3efOnfuqkZER1qxZQ6PR2OJ6+muiJEkOFZHXmdmFIvK5I488UkZGRkoDAwOvA3YVkWY/LlOYmSVpDciGevtrQi/Or7FvPGg/YnCDc72NnvRKdEqkidDEaKK2DmMN4hLvk2az2eyISINe7GIDgPd+N4CiKNi4cSMPP/wwo6Oj9EXqr4IWLVp0D/B3RVG8Kc/z7+y9995Lgc7atWt9/5YNwKoYY8PMmnnezXGuRKBFpAk0UBqYTqDoFrxMSTDskTCJ1VCGCQgmZg4vZoZnMoa8UqkMls0siTHuY2YPbauzMUYmJiaYmJggyzJqtRq1Wu0JReyZoKIoaLVaNJtNfvCDH5RGRkZeOmfOnN/ssMMOyxcsWPBGEVl37bXXPnDyySc/L8Y4R0T2EZGFIhLE2ySiE5jMwBCi9QL+Ig7byotvWALxkXi/yRCQRMU5jFhYCXGaxDgeuk2DctL39TXpBcCfkPI8J89zNm/eTJqmU55rSqXS0w5oCIH+epZOp/MoCdiwYcPJ3//+9x/3zNe//vWFJ598MmY2ADRU1RVF4V0+TmahEYxpBBVBAog6ByJWe4ThIglqG3CyCmwepkNmKIZEwUV1DlTF8gZx3GBGGmN0fZ3x+B34j9Bm59xD9BjdqequRVEMbz2oJElI05QkSciyjCRJcM6RJAkissWCAqgqU/NIVSWEQFEU9L3M5Hk+NbnfFq1yzo1OratjjPvTC8azadOmV/bvmWNmDeecxBgTjZMSi9CIQYcR5zykeFUiINQQAZEHzeLaxDTeLUYKzEPcAWLcY6ZmipppjMFI8tEGzeXW9TtnWZYCrFfVA+jtBngUOefuEhEdHh5ePnPmTN2wYYMbHx8HWNV/BmDL4J9BUu/9LWaWzJ49e/nAwIB7+OGHa51OR60XtdsdyNevX39zrVY72Dl3dwghiTGaH1+UabGpFQszEg3OgSgizjnE9u8bkdscdqczC/ejyeL+Mm6WYQ3MopmoBefMJOk21laTxuKKmbrY83aPmlkSQrgeoF6v/wxARO4WkearXvWqHdI0PcE5d2KpVDrhqKOOmiEiLefcfc8kYluT9/7mUqk0/qIXvWh/7/1JaZqeOG3atGOPPfbYrohMAmumTZt23ZIlSxaaWaqqm83MVFVKnQdrne66ajRJXHSiEcQkGjaO0lvOkSxB7QHnE1lCHFyLWc+3H2l5xKtapqYuRpNue6ySFSsXhKCxv05tAHS73dWPEZ3k2GOPnT4yMjL3C1/4AqVSic9+9rOMjIzsfNRRRw3S24X1bJysXGlmpec973kvnT59euUTn/gE8+bN4/TTT+fee+89/MADD1zhnFvfaDRmttvtrK8eGj3VECzLVy0IjfGKRiOqOtRSU0vFaE3hRJi2nsKWJ2xuL9fa7Nc7t7HXtLNGjOwgaoWpYEoSY3emaPeAPG//CkoVEcmdc4QQ5l166aVXNxqNI5MkuQ6we++99yWHHXYYt912G7vvvjs33XQTCxYs4NZbb91XRK53zt1mZjOfcPhPkURk0+zZsx9cuXLlwcPDw6xcuZKhoSHuuusuDj30UFqt1jELFy5cVhTFXO990teteVEUxJi3he4+MXYjgDnUjAg4orWm9nJo2GGmm2bLEnnrzRP6k8MPe8QS41EwkwTFFIsaRIr25qt8vmY8uHlV770lSbIaOOKFL3zh/y5evPhKgOc+97nV733vexx++OEsXbqUiYkJ5s2bx2GHHcbvfvc7jj322MV33nnnI6HUp2nSLfL4PVJHH3307BtuuIHjjz+eK664gvHxcV7xilcwOTnJ9ddfnx1wwAE3NZtNt+uuu74GeKgoCosxGq1VY3lr9CoN7CjO1KI4ExEUxZNN4SSUXiwvu+YT/bCbbRLldoSDMTnSO1seogU1cTEXb2YyvvaOscHB31dG0zdrjFG893cDx+yyyy4bP/OZz5yqqqgqt9xyCz/72c94xzveQQiBgw46iO9973ssWLCA973vfe+cum/Kom5tXacAfeznFEBbfzrnEJFHfe87erdY8F/96lf86Ec/4rTTTuOaa66h0WhwzTXXcPDBB/O2t73tnd1u99uqOlNVfwVYu92VOe3rapMjCyei2lwfxXDOnMTgRQSTl4OB8QczVkJvcyPnvGnuJDI+ihWvAKaJ2kIzqmoiqhBUpNMZr8/ace8j1snzljrnvHMud87tF2Ospmk6bGZOVVmwYAGXXHIJeZ6zdOlS1q9fzx133MEZZ5xBrVZ7FHjbA3Lr348t2+Pex3KhiDBnzhyuuOIKGo0Gq1atYs2aNaxfv54PV+O33AAAECZJREFUfehDJEkSut1uDZihqre3Wm2X5+3unPy3x2548Kpu6sV7QROPpIL3XkYxDu8teev/Kjrjl+dctnpF71W1uFnjzvVHnIV+rSBCRDDBDDWl3GmM/LrUWTbS6XQkxqiqehuwT7fb/Y+pAe68885cfPHFpGmKc45SqcQFF1zAnDlzHgfSE5W+W2pLeTLPPBbsww47jLPOOot2u02e5+y1115cdNFFZFlGv8/7qOqt3W5Xut3cyvmDGzutkSs0UlLFMHEoiIlhbJjCR8PcIWLj1p4o90kvPegaSe/bC2wOcLsZY50CaRdYNzfJI6gbWL3LIe+euyh9+4ZSqSKDg3UnIieq6n3lcnm3vsf4cQP+YwA8lgufSISfSGwfW7z32/s93ul0NojIfFX9z0ajRbPZtP35wZyVt/zbKomTcyspVkqFSoaWUkSEGcCBmKy2uN9id9LCl8NWu7NU9UKsdHEf5YMFNgjgpLcBFpCiPbkTYbKgvXp9nnes3W6rmd0I7Nduty/dlsg9FpjtgbctDvxjL2B79W2v7anS6XQuN7MFwPV5nlur1RLXfXi9FRPtojM5B3D9bXwighNYh3Fgb65culgtfmEKty0A+qHWFZrvNG/LPEddI3WGiLkE8JiKt/TB2340viD9/X7tdtva7bZ18/xhM5swszfGGK/bnu7a3oC2J7pP9P2JxPaJ9KeZEUL4dYzxFBEZy/N89cTEpO902rpP6boDlt98adNjPsE0wSQRIxEDle4ULhp3nu/Xt696HIDy6qVd1Asql/ZMdXy7F5ssiUURvDcRr1i3NT5DWxsmKvnitZONhms1m1oUxdVmVu92u0NmNj4lftsb2JPVgU9WBTwWzG2B2O/ThjzPZ5tZmuf5NZONhmu22jpkS9fE9sho0R4fRhDvRRMxSxLDY2OIvq0nmXIJKm05bWn3cQACOHFna5hzX1+MM8ytS5yId+AEBMErsui671b3Gbjn2G6n02w0GrRarY6ZXQUc0Ol0rrZetOsJOWFbYG5Ld/4xffpk2zGzTp7nd5rZc4HfdDqdvNloWrc12dqrfs+rF1373YokvU2ECeC9+FTEsGQjPbcfxLlLXJJ/+lGYPcr0n3LPerS8D/DbHhfaWxOxsVKCeYdkHhMvRAvDS2/58Y0H1X9fGZ9o0Gw26Xa7m+htAH99nuc/2NoIbP35ZET6sRZ4ayCfTB3barvb7f48xvhK4A+NRmt0YmLSJpsNe/70m+tLbrrsRrUwLe0fB08F6Z2rtzGI7+jpPq6MoTRfTlo6sl0AATq+/U+az76h/1AVlZg6o5xg3uNSj6WefHz9kj1orrCdSsvWTUxMyOjoZtrt9lLgTjN7ewjhB8CfxYl/zpTlCUpeFMUlZnYicHur1Vo+OTnpGo0GOyVLVkvzIR1bt3yPzBNTJ5Y5LHGQelPE5T1JBHTObb7onvFYvB4HYO3kVWuIpRSTb/aXLSd6WJQ6c5nHSg4p9xqS+67+9tCe9QcPk+66NZOTEzI6Okqz1VpsZjer6luLovjZ1jrxyXLlE80Dnwy3TX0C62KMv1PVk4GbGo32srGxMRkd22x0143sOfTQYXf+5t/qpQQyJ5qmWOKQLDHxxiLM3tTXfV/TIknlnSselxXpcQACuEp+Tsx3HERp9Pz/7kWZp1tySEkg8bjUiROscsvln1v7kl2Wvrzb2LhhbGxSxsfGtNlsPqSqV5jZ60IIK1X1uq3r/1PB/BNBm6Lr8zzfqKqvUNX/nZxsPjw2ttmNj08Smps2vnju4iNv+cnn1qVi1VRESg5KhpQTXObIUXdEP/bRiPnsHZzaJ7aJ1bYuykkPt73xbbR+Zu8N2P6ibqycQrmMlZ1ZKYVyIi6VOHzzjz9/99ELlh8dWhvXjI6OsmnTJhsbG5sIIfwXsIOqHhRj/Da9I1mPom0BsD0An+iZrWg8hPDdEMLzgel5nv98fHx8cnRs1MbHN2unuWHkmL0ffMXNl3/+LtEwVErFlRJcmpiVykgpRcXcJrB9eqJbP9OL/5q8c8U2T7E/4WnN8J2df+iTtQKc3If7KyGyf7ONNbqUOoXQbFtoBaJKZc3hJ33igF89sOPVOcNzhoYGQpqWy/V6RUul8qCZHqGqS4CFMcZTVHvO2a0t67asLvC41cTUiuIxn+qc+w/ghc65nUTkd3mej7dardhqtbLJZlMzHXv41c/Z8MqbL/nMIrHG9EqCr1TEVVOjVibUM0g89wFTx14viWHHkLxz9du3h5Hf3h8A57x++i9jrLzV+ZZgzACe6+B3HqYhaIw4J+JMkbwIQyvuvHryZS9//i6tXB9YuSFMC0Xs5W2KoeVElgCY2dFmttjMfk7v8M1g//qWds1sm56XKRAfc20N8J/0gvgvBO4piuKOZrMZx8YmmZycYPPmUXYb3LT+pXtu+ptrf/iJEbHujEpKUstEaplRKxNrKaTCCoR3AB6RJTGfPekle/s5Px3bbuzhjx64bn9tx92yrPigS8b+AcgQNqP8ohuZ28zxrQ7SynHNDtIulE5wxaEn/PP6WJnDT24faJfSSlKtlsvV6oCVKxmlXgCpDOypqrur6m/NbF2Mcb6qvlh7Z+keJbZbr3+dc8E5d4OILPXe7ygiR3rvV5jZgzHGVp7ntNtt2p3cmq2m73ZbjeOfN1nz3TXx1h+fv2PJaVYtOStnSK2C1lJirUxeSlmPcQwwo7e9b+gifPlf5e1rthm+fdIAAoRvzXytSHcv51rn959aCdzcDcxsdPCtHNfuIO0ca+UaWwEGZu+16pDj3vc3Ny1Nfr1oXW1WuVz2pSyxUqki5XK2dSQuU9VhM9vFzKap6gozW2NmDVUt+qcvMxGpi8iOwK5JkkyIyEOqOm5mRVC1kOd0Ojndbod2u0On0427zypWHLlv53X3XfWD69ctv3V+NcPXMmeVBKplQqWEq5eQUsZalBfSOw2PhuqHTct3J+8e+dUfw+bJZ+345vR3kbTrkE8dR1iF8LsisGujQ2xFrNUmtrvUmwXdboHP1RUvPOmfNyYDO02//BbuGu9k8yqlMlk5w7uEUlYy55A0TcQliTkRw0xxHouxd2Cot3PT+kcbxEzEQIIG0SISo1qet6UoAt08aLvb1uGyrj/hMD0gTK7ZeP2PPj+zlGhazoiVBK1lZJUSRb2CVlMk9ayldzJ+DwBi9gG01pV3b3xS2Yz+pLwx8RvDn3RZdwzrgyhsxvhJMPZq52izLVmzwFq5SbdLaEeNndyZlOutw97wwU5anz39ZzeHO9dPMCvxaZp4j08z0iQlSz1Kz/XhxHdxRFR7ESvnPIqPUTPV6LwX63aDKLEXEy6ChVDkc4Z15G8Pyw4qJtZvvOm/v1ixTqNaTpRSySXVFF/NROslpJwY1RKWeBZjnIAw1BtP9gGKclXevfmzTxaTPzlzUfzawBnOhwSXn9dPDpZj9i1DDmp2oR0Iza5qJzhrFfhuR5NOQdENTpPKwNiBx5w6MXOXfY9YtiZcef097e7IhE2XLMlSvDjn8IngncfMgllvQ7JzIoqkGsRUC4kWpCiCodadPuw2vXi/cmXBTslRIyvvvfauX/37YNGdnFbNVDJHUi67WMmIlVSpJs5XMqRWwouzuzB5J5BhqMbkDGflivzD+JMG788CECB8tfZO8cVOzsd/4pF8pz9CGOjm1DoB1+yStgq1dk6RF851g/puTtE10iLSGNpx95EDjjrFTZu928taOXcvebizfMWabvfhTV3f6UI3qqC9o6WC0ywVyiXYeUYp7rZTWpq/c3WPWsYBm9c9ePU9v7lEx9Y/uEOaUMs8oZSQVTJXZF6tZ22dK2eESkYsJf2NU0I/LwKjhPSLEb8i+YfmD/5ULP7s7G321cphEXuv98XeiL2gf3kxwu8N269VENtdF9sFaadQ6/aSDaXdSMgLfKFoEUhiJCcpjw7OnDu+497P1xk77pqV6tNr5drAoE/THUSchLy7odOcnOg2Rpub1q3M1y26zU1sXD1E6ExPElLv0MzjspRQ8iSlhKKSkpRTJ6WUolZSygmZiNyH8VKmMs2Z3BhDtlyRf83e17r1z8HhKaW/m/jywIy65N+XJP4B9JGljsiPMTSiO3cCRTt32im01A3keeF8oap57nxhWsRA0lEwJQQlMcNCz502ldqk109BEwERJHEEcSQlB6knJN6lmdOYlpzLnMZKQpZlrltJ1ZVLpB63CiPF7PgtfTT3KYv+b0TKb5F/HN/852Lw1BMwXobX9dmZmJrL9K3Agv5fOSr/jmdGVJ2bF3Q76nxRqHZy52LUmCsuj06jqu8qgjmiEtTUmEqF0vumgDlx4h2Jd2qJgHcuJk592ROT1PlSopomzpW9xiwl896tQumAvZlH0gcs1sJd4oTAxnCenP3Udko8bTlU7SvMN/NfFW9XAJ9iKmVJL/vkDzEM0V0Ldb5bqObRuagaikBWmLMQCKAuGAFDowKO3gpASbwDBJcICeI08SSJU8kceZKSpGCl1EnqNWJuBYLD7C1bsmBCC+MTMcqrfIjvlQ+y/OkY99ObhNaQ+K8ch+OdPnG3KXwco7xVY/eqyY3iqRk6xyJZUFeEqC4oFOYExaKh9JzaPSMiGOLECw6HpKKWeMyLI/XqxVMIbq1Fmk7scIP9txphxymfiUGfD3zL/3/84ulMifzMpEH+Bmls8yaMU8y535rnNOnP8rdqeA3I1eKsbSolB4NRdFgMb0Y0wcR64mXSSzggggeCw40rTIizrqlUwF5msNOj+gArPVykhR6t8P27q1x2yHt42vcdP6OZzO1sXBjmKODDqvxeEjfN4ANP0JlRRG43GBdljN5OWDCrmWNYYAizgw2mP0EdXzJ0s1NeivDZZDNXP1U990T0rOXSty8wsyuchXMrTLjgmWhDjDNQ3a1kfEr+iY3PRBuPa/PZaGSKDKR9AZc47x6KulUuwqelbrnIqe5c+SdOFJ4+HffHaJse6WeKBKwyyVtDoQca/Eb7qbSfarHItVbovpUB3vxsggfPMoAAcjahW+KNVlhDlZVPGcDIWjFbF5U3yTNgJP7oeJ7tBqdo9F84wMOpivwjsmWS+6dSMLUvpsJ3Bz7CdpMkPpP0FwMQYPyznByUHXFy4Z9VgdrpzjE+7aN8+2nu2pOmvyiAAJvO44tmboNh5/1pT8qnPTpj+se3nRjx2aK/OIBmyKbP8FMzN6bY257MM+LkMjGtzQy89pmc4z0ZetaNyGNJBGtv4k2gczVy+5MwHHcRdVoROOkvDR78FQAIMO+LtIm8zYndr8bodqcrRkuwG73jXTudTeuP1/zM019chLemtWdzpCkvir2EZI8jBx9xCTfNOYvfbev/vwT9VXDgFM05m2sMgiinP477lNMd6F8TePBXxoHQW+6tOYsfFsrDpnyof/GiJGHmzp/mrc/2SuOP0bN7CvpJkICZ4+2rjF8G5RcmDDrHvjt7Xv3XBh78lYnwFMnZhOg5SRxF6hmhyUlyNs/o2dj/X9LKT7D/yo+y31+6H09E/w/wHJVcjfUH5AAAAABJRU5ErkJggg==\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHiczZ15vF1Fle9/a1Xtvc98780cEjJCAgkgiUwiIghCgqLIQyK2qI0D2g6PlmfbbaONCmo/habB4dF+tBX0KdC2PFGZB4GEgECYIQmZp5vc+Yx7qFrr/XHODSEkiDKuz6c+Z9+z9zlV9T2ratWwal3C6yh64YW8Y8Ex4431M4yhOQAtVMUBTOgRQRGEWvtBlJnRgGJABSthdIWHrIyI1l9y3339F154obxedaDXOsPGr2+anFL2TgXOJKZWEIYP5oslL0z7EtE8Zj4M0O49f5qGofKAF32GRTe1GnWTZOkR5DUi0muC0Nxaete7el/L+rwmALdde+34nOcPgei0ILK/j4rlLmbztyBMfkUyUGwT8f+ZNGojWeLeBchvjOR+Xvngqf2vyPe/iLxqABWg/p/+YqFR+WYQREsLXeUuMH8WQPhq5dmRVMRfUR8eqquTt7Din7rOOXsFAfpqZPaqABy88spjlPhfi8XytSbKfZxAB+3l0ZSZ7hXD6wkWCAggClURGWJSggUAUjivokRIoJpq6gkkyl5miOJYqNq9VO6xer3+UxfHp4P9l8Z+8pPLXum6vqIAhy+/fLYXXFksdd1go9wXQZjyggyJH1HDDyEIQ1iawMZAjAWTsWS55QHHbEREGQwPABAYZhLxzjDBIpOcQBx7BwhUvOtDkiXk/WGqcugeirbJxc1LGvXaqY5x7sTPf37NK1XnVwTgg1deGcwcHvkWOKpXuiqLiPjI3XIZJBv91lub59CORWBVAysmDKyQ8QgsQGSNsakaUhAYIAPqlE+hgHpRVeMB730IFcfOK8RbSVPHTghZCsn8IJI0hk/fA8WYXYuhovfVa8O3eudy69eWLzjsP87NXm7dXzbA6oXfnJNBflwaM+4uJrpgt6/fTlFwC+dyY7w1Fvk8KAwYHChHRsQGAVvrEBgSsGE2DtYATAwFE4MBQAUCgkBU4D3EewtRD3WK1FmkzsE7gstI00zQaoEynyFNNkucLCZg+q6lUpVLRoYGj1KWv53wla+sfjn1f1kA+750wblBYGcXunpOJcUBu3zrsIbBryhfnGaiyEghIoSRahiAcgEjyCkCqxwEFsY4BCHBEAnIIzAEsGEGRDVgYgXUiQAMcfAeEDXwqshSFe8t0syxcyRZTIgz0TSDacYkaapoNb2kySaK07MAVEaLqIRnGkNDv3ferR7/rxdd+ZoC1Asv5L6R+k9yudJTuXz+YgA7O3EOwquQjyoo5POI8oRCBC7m1YcRKIgIUUgcRSqhVUSRARtvrGEPkiRuVBsDw83B3m3Ot2KqDQ8TAJR7utXkcjpm4qSgOK4nn88XK1BlOC8iziBOPWeOkCTkk0SROqU0Jm00QEkKtFqqzVZLG406vHxol6q4pNn6XBw3jh23Zf3ZdN11/lUHuPpzn4t6Mr2h3DX2T8T85eeoYjuKuT9QobwPFfPQYp4oX4Tmc0AxD+RyanIBxIZk8hE8sx3cuLnv4ZtubD774MOz01bzQI4KjwTdXRt57NhhDYO00FXOAKA5UgsozUIZGOjOhkemSdI8NMwXnt7vsIVrDl20uDhu2tRxRtT5OCZOUvFJqhTHhFYMbbSIkpai3oSvN0Ct1lY0mqeAMHFn0UUuqo0MHDkU0Kn7X3FF8qoBXL34c1HXxNYNlZ5xawj0qZ03DN3I5UqMYqFgSiX4UhEo5IkKBUUhDxTy4FweEobF4R19g7f+6D8a29etPz6cPPGWniOOHM5Nn1qKyuUKk+UgCMSwigoLMwQARNr9ocucEUC9TzWu1WqNDRvrw8sf6HHbd7xz0uxZd5z0iY+VK+Mm9HCa1aXZJG01iZJYfbVJHDfV1BvwtQbQbDSlVsvB6+JdQHxvZLDvwDq5d8/86U/jVxygfu5zUV//4I1dXeN7QXTWzvdN+GNTKU5DpSJUKYKLRaBShi8UCaWCUr4Ijuy4NY8/9syNP/rPg6mYf3bSu9+zpTJj3/FhaG0YRWRsiCC0yFkLgAWgzFgSMkYBQL0n74iZJRDxnDiHLM3Ue4eklVCaZVlt3fr+3j/8YYrWG7NOOfeTT86cP+8AybIdaLRI63Uy9ZZKowodqZOv10G1WiLVxiD77CO7VPPqkaG+aSMjAyfvf+ONL0kTXxLAa9//fvN2p7+rlMdsYDbn7oQXhj+kSnkmd5cJlS5QpSwoly2KBUGpRFTIj92+YcP663/4g/2CSZMfnnrG6T6sdHUXopByuYKGoaEwDBGEIaIwJGOMhGHovKfEGPVBEGUAkGVJoEqGCGGaJoH3npPUiXcpxXGKJEmRpC3EcapxbWR463X/bZLebQvf95nPrpm4777TpNUYQD1WrtWdr9dCDI8IqjVItcpUra3WNPvMzjqpfH9kZOCACQGd/FL6xD8LUAHaccq7flEuj9/ARP+480YuvIzK3fO4uyzo6iLT1QXpKkMrZZhSyWTQiddd/r1n62lMsz/+sb6ouzKmmC+gWCxImMtTFAQmn89REEQuzIXKABtjhIgUAAPIOgkAAnQMlarCe8+i6tMkYeecbSapxs2WZGmszWaCRquOWt/g0Iaf/WxiJcxl7//8Z+ayoldrdaFaXTEyDD9cI1NtiBseUNtobHDN+FPP1Vkuqg4Pzph40w1nv2yAW4874dxKqZyzJnfZ6Hucz31fyuX9TM9YoKdC6KkIlbuYuyuiheLEbZu3rL3h5z87aNJp77lp7PyDpuZLZZTyEQqFvBaLRQRRzufzeVimoANsAMB2AFsAbAbQByDZDWAEYAKAKQD2ATAZwBhVhXMuSZ3nZrNhm/U6teIUraSl1ZGGDj3xWO/263+76D0f/chjEydPmaWN+nYdqTKGa0B1WGRomDA8QjpYfRZZ/HejdXRZfF6tWfeT77rte381wIHD3zpfQ/s3xfKYL0GVAQA2+iF3lWfpuDFK3V2Erh6Ysd2qlRJToTD3jzffcseza9dMm/s/P7clX6wUKpU8R/mCVkpFLRSKEgTWWmstgEEATwNYC6Cxh3IJgKM6fy9HWyu1c4861wUAswDMA9DtvXdpmvp6vW6arVQazRparRZqQ0Mjq//9+/vvt/9+a4898Z3v0Li1CsNV9cNVz4ODVoeqIsNDHsNDG5G5tiaqukZ96JLM61WT77/nqb8Y4JPz54djw8LN3eVxUxS6PwCA7a+op9JF3V3MEyYQuiqCMV2M7m4gCg/6f7/573uauUJj9t+eXSiXyzaXK3JXuaiFQh6FQsExcxnAJgDPoK1xhHbTpA6g7g6UgwAcDmB2pzhrANwP4KkO7CoA34HoOq9jOp/b13ufJEmi9XpDq9Um4rhOtVrVr/np1Y1CKy68532nH0NZ+gQGhtQPjxCGhlX7Bw2GB70OD4/A65JOvqtGagPbJrK8kx56aI/TPrM3gF8ZO/HfugpdG9W5U+EcyPmnqFioarEYULkCKhWEymVGpUxgc9A11123ws7cb+P+HzprXHdPt+0qV1y5VOCuroqGYRgZYzIADwJ4Fu2mWQHQIyIREUUAciJCRJSo6qeI6NsA7gZwO4ClqvppIrqpU7xIRPJElAfQg/YSWQJgG4AhZh5rrc1HUZgGATMRGRuGKM87yA5v2Tz02N33NOfPPeBQUfSyS1nTDEidauqIUie+Uffk3AQ4NzYy9prBZv307/bt+N1LBrh1zpwDQoTTAzIXtKdO4jSMbuJysULlippKCShXiIolImsPuf6m3y+LDpy7ZeYZp0+qlMvc091FpVJJy+UyBUFQYeanADwCIBWRkIjGiMg+qjqWmaep6nxVPY2IjlLVJ4joMACnAzihkxYR0TCAR1T1M6r6FiIqEVFFRHKqWlFVUlUhohjAWiLKjDEzoyhyxhhlMrCWbGn//UrVoaG1Ty9dGs+fvf+bSWgL0tSqy0BxBmSppSx7FnE8H84zvBzjnLvhf47r2XZpf//AnwWoAFXHTfivclSeR0RTAYA4uJIqpamolJkrJUi5ApTLxLlw9u0P3HdPOnnS0KwzzpjQVSlTsVhAuVxGoVAoMHMOwOMiMkBEkYgAABMRqSqJSAqgn5mfVtVFALYC+Bu0jchVAG4AcAeAJ1R1tqq+T1WfJaJ9mPknABqq6gDEABLvPYjIEFFJRFJVbTDz5CAIDDO1mNmoshRnzOgeWr9+oPfZZ9fOnDr1MFHpJ+cUzqv6lJH5IpL0VqgcAQChCcstFy+6ZKD/Z1/7cwA/PWfeCcWwMMhsP94mqiu4XAJVykzlElG5i6irCCrku9f2bnnk2aGB8rxzz43KlQrKpSKVyxUtFAqWiLyIPKWqCYBJqlpBu5/qEZGJzDybiN6jqkd77+8hoveq6nYi+g4RBar6QQDvBPAOAAuY+W4APyKiBao6A8AtqvpFAAtFBERUZOYKgPGqWlTVHiIa6qRyEAR5a61T9T4IIi4fOCdcd+/SfJGwprvctQ9EEnZi1XmFc6Qu60Ka1gGaDMI+AfFlQ2PG5i7p37F+V168u/YJ9KvWRl/a+UCY+xNyUai5HFMxUuSNIoyQthK6f+XKhQed9/fVUqlIpWKBS6WSz+dzQfurcC+AQe99WVXzqtqtqhNVNSKizap6u4iUVLVFRBc5574IYI6IXOK9f5+qiog8IyLPqKp4798nIpeKyH6qer6IfNN73xCRCjPfCmCt954BdAHoIiIjIkUR6RORZUSURVFki8VykM9HWiyW+KC//3zz/pXPLEySRCm0RiIryAVKuZyafJ45Ch4Y5WBN9EURf7HuZnifp4F/N3f+WwthfoTJvL/9Dt2ihahiKmVoscDIFxWFAlEQzvvtI38anvPpTy/vmjC+p1AqoburolEUBUQ01Tl3D9rW1DNzQ1VrzJyo6j4AFgFYSERLVfXdAC4HsICI3gvgSWPM94joZhFZq6pxpznfraq/Nsb8UUT2AfBhZh4G8FMAxxPRLar6BQBv8t73qeomADuYuQmARKSMdvew0FrTMIYVRAAQFvaf89T9N/x26gGTpkwn7/uQesCnUPHkY99NLlkNYH8AFct6SX+lB5cO9m/eCXZXgF79N4wJp47uvpgo2CxhYYoEgSIMlYKANLB2Ze+W28v7z0m7pk6elM/nUCzkuT20w1Tv/UYAbxWRhqqOrnhscs6tIqJbARyvqomq/j0R3QbgAiK6Q1VvJqKzvPcXqT5//2f0b+89ADxijPmi9/5dAL5MRLc4584H4IkoJKKbiWiMqh4gIlNUNWDmfhExxpgtzDwlDMMtBRHyzvvuqVPGl/af9ejqbVvs3DFjp0kQOAojFROQKQQqLtymSdrmwflPESenAjjxBQDXzZ8/iV1wO4CLAEAJd4kNpyCygAmI2KgGBkjdlCd3bJ+54NOfWFbI5xFFEedzOW+M6QFwFxFtUdXAew9jTMV7fygzv5uZ6977bxNRwXt/gTHmUlV1InIBM38VwDs6oG4RkbuJqLkbxAIRHUdEJzrnDgUQM/OXReQTAKZ0NPBSImIAnwJQAnAnET3mnBs0xkBVM2aeYK09wlo7kM/nDaA44MNnRw9dePFb9ytVMmbaAGtgQqPehIAJ9oFmfwTp20E4gNn+cu3MgybOWvfE9ucBDBJ5X7EY7dynZWOfhjEzjbWkAUOJiRRmxY5t901bfHItly9MyefzUiwW1Vo7SVV3OOcWoz2wtcaYDQBWiMidRHSqqm4jog+LyBZm/i4RnSci3yaii1T1Hmb+tff+QACnEdFJ2E2ICKq6XVWvVNWVzPw/vPff7IA8T1UvFZFNxpgPi8gIgAnOuduDIOgGcKyq7uu9Z+993Vq7PYqifYhou4jXLPOFKSce9/tH7vtTeUHPuGlK7MFWOTAiQQAT8FPe+bcDQCksjvVu5HQAPwQ6RkQB8mtXTwf47e3GQn1gM1WtgbckwqywRN6l4zYn8dsnvfWt46PQahAEFIYhq2rivV8G4Ceq+v9UdbWInOyc+4S1FqoKVf2Bqh6qqr8RkW3e+8tU9XYi+rKqHui9vwzAuWhb6UcA/MY5d7lz7nIAv0F7HNkD4FwiulRV91fVL3vv7/TeXyYiW4jotyJyqKr+H1UFM5Nz7hNEtNg5t0lE7iKiX6vqnUQUW2tNEIQU5iLd9x3vGLclTY7zWdoDqLbrzGBr4GH3VcWAAgDTcbJ29b6jP6xBW99nGJOfEYy0ijSmO1TCLRREPchFZIKANQyFQqPPpunq8C1HPjl+3oHlKMpRsVgQZj5AVR/z3p/V0egBVV0JYDEzbxORfQGMVdXDAPyaiD6JtrH4oYh8TFVPQnscdzkz/5f3fnVnnDiLiBYR0WGqKqq6XkRuNcb8ynv/eGew/W4imi4iXyGiyQBOZ+afiMhH2nqBsdQ2FhONMVeKSAzgMBF5LxGtYOZDiagPquS8mrReWxFv3jw4hrlIaQpJM1LvGN4ZdemDrDRG1qx9VLys+TvJNl8OjFgASGBO6Db58ar+fbxu3dUye78WDBMRVNgAAguHYJ1Lj15w8onLC4UchWGo1tqIiLap6urO2OwMVd3CzFd0xmY/VtVvA/gCgEtFZDERfUlVv+WcewuAW4wxfxCRt6vqlzpGAp0BN9BeUACAQzoJncEyVPW/jTF3O+feTUTfVFUB8CXn3BeIaKL3/nxmvkRV/5GIvq2qARF9QlWnA1gqImuMMbOCIAicc1k+r5h16ruKDz348JGzDe0gIAUzQExEVmBMitVr7obXsyObW5e6+O2Av5oAYC3s78YVxhypinEA1blS/i3PmlnWUtFSPgcuFCjOBc3l3RXz5i9/MQnDkHO5HIVheLCI3P6Nb3xDly9ffg4zR7v3XW8kEZHklFNOuf9jH/vY74IgKFhrJwE40Tn3aJIk2opjevDib+WPGm6lhTTNS6OpiJvQetPrug01P1x9L0ELRNgx0By4byb8aVYBXg+qimIcACj0FsTJOOzojbk40wMSiM90HezwtFNOrhtjJodhKNZaUtVCkiSz7r///hOZmc4888w/rl69etWcOXPmPP300ysPPPDAuc1ms56maQag55ZbbjnOWrvitNNOG2Fuj+H7+vr6+/v7ByZOnDh+zJgxY6655pr5ItKzZMmSezvN73lCRHj00Ucbq1ateteYMWP+dMIJJzQ6cPSZZ55Zvd9++80IwzAcHBwcuPnmm0+w1naXy+XbFy1aZP7whz/ozTfffHylUnnmrLPOeluWZZuCIMgZY5SIyDBj2knvfHbDdb8pz3U+r+otvCayfYeXZnOcQm8BcJoqJgCcKDzbTcCknA0fBnAWAKjyDiWUtZmEfv2mxM6a2VAbdG8L7SGHHzTv0SAIgPZofJyqPtJsNt9sjDFZlsVnnnnmMara6PR3C4noJ6p6HoCHmfmmW2+99bh3v/vdI2ecccYQgGUAzgZw3C7jvjU33HDDxlqtFpx55pk3ABhdUg+89wUAMwGcsHjx4uI555yDM844Izn++OMLAK7z3h9rjPlkB/JFInLm0qVL++I47r7iiisOyOVy5dNPP33pOeecs/K66657z5IlS54mopNV9XcAxllr+7xX2ufNC4sPXH/DvLnS6JPEx1i/UaXZKrR/S9422qtYGz603mUT2AP7Rhxyu89VcOADYnWqHuLSfLpxY5dWa9vZmO6wUCiTseC2+swTkY2jlSciiMjFqvouEbkEQE5VP6mq56vqId77LwPAtm3btqnqqar6HRGZB+BqEfmCiFykqqvCMDRERM65S5xzl3XSd1T1a6p6uqo+2Gw2vwcAvb29G0XkMFX9DjOfDOA7AH6mqhcQ0aw0TT0AhGHYA+A7RLT4Ax/4wB1ENHnDhg23qSpEZDOAedZaMobI5HIltcF4DNd28IZ1OfGuCCiUycP4YJRTyCEE2Jc9zFwY3rf9NqDe9DjhQAUW3oNcipH+HcmEgw/5PhEnDIWIKIC6qtZ362POArCUiP5FRL6lqjVVvURVfy4iPwKAKVOmTAbwsHPuCwB+5Jx7P4BLAVwgIouZmbQtN4jIr0TkV97721T1MREpiMj7SqXS5wGgp6dnkohc2fkBnhKRL3Ys8JUicl0QBKYzhLpMVb8BYOlJJ530NhHxt9xyyzGde0NEVG3rAKVgbk6cP/97IwPb1WWO4T2p91aFAxUaM8rJGp4CmDkWoDcTuON+RqmSlgyJI/Uq3nhKM9SisKX5cJYxrNbamjFmnKpuc869s6+v72cA/rGjhXMAbPbe30ZE/wLgHhG5jog+O6qp69atWy8i7ySiSzuWelhVrxeRx4wxSaPR+EC7K9GZqlrsXO9Q1Q1EdIeIbKnVagDwHVUlAJ9WVRDRJufcBcaYA7335xIRvPerOv3ol9FeFgPaq9a9jz766CTv/XcALGLm7QBCIhokIOJibkY1CJs5X/fshYyqcyQGRBEpUkBDEM0jaGYB7KekC6EEgj7JAOC9aHvnQSAW/cVSuO/cuYn3PmLmnDEm7UzDFlQqlb4OvGDZsmXfO+yww86x1m7z3v8LM39NVd/mvf9+54f77OzZs2eoatF7/x1rba+IHAngFAAf8N6jWCw+2mq1SER29SmcucvQBsVisQ4AW7du3UxE56vqJBE5tzOrEefcZStXruQ0TT/IzGi1Wl/P5/N/Q0SzmfkrY8eOPaO3t3ccgGNEZIGq3sXMKRH1QFXGzp2bbSyWypPTXqgXdd6BvRdVZEr6FBSHMnCYgBMLIA+lCgAo6RZRhFAFeQ8IQSwQR2FPYZ9JW4MgSK21LCJzRaRJRB81xnwNAIwx5oorrvhspynPEpF/BlC11jaiKDoZnd73+uuvn7Fp06bHrLVHhGGo3d3dEJGHsizbGsfxjoGBgTMBFK6//vqrjTFBtVottVqtuNFooL+/X7Msc0NDQ1MBvO3WW289aenSpUfEcVzy3ouIxEQUGmPOA4BRS++c+6SIVIwx53nvL91vv/3uXb58+SwAE4jooyJyjqrOtNauIiJXnLoPJ8Woy2cZwYuyqIoqoEgFWMvAoarUDaBgAfCo96sqWiA15L1CmMApICDJh/l8qcTGGFFVj/aUaqRYLP7L2rVrf9xoNL5eLBZ3aggzEzOHaO9VVJLkuU1+7/2MBx54YAb2Isa0V9h+85vf/Pk9WaKJrVZr4iisUWC7y9q1a389b968aQAuA/CMtfapOI4XeO8vLpfLX4vjeA3aa4gCQE0QqURRDmlKgEK8AAqCihK0iueGV8yA2tGOkYAUIIEoqfckzpPLHBAEFY4iEhFnjPGqOhbt3bGZ++6778l9fX3YsmUL6vX6zqWnN5JYa49g5lNV9d9E5FvHHnss+vr6cpVK5X0AphNRA8BY770AEJsLSYytqMtUUwd4bbMDRBWNUV6AWn6e87WSV9KmGtRhUGdwPYCpE1srzmVE1ErTNFHVLd77AQAwxswEgCzL0N/fj82bN2NwcBBZ9rKdP18xWbVq1ZPe+49nWXam9/4/DzzwwGcBpNu2bTMA0FmE3UpELWNM3RiTsA1CgOpgqpNBnRk1z9QU2J28aFT7dr5BmldFyQIqIPKq5OFh4evqfaiq+c48dC6AjXsqrPce1WoV1WoVYRiiVCqhUCigs+D6mkmWZWg2m2g0Grjqqqui7du3Hzt58uTbJk6cuHb27NnvJ6Kt995779NLlix5E4CJqjqHmR8CYLMkSQP4qgrKBJAwhKBgJVZyFjs9jwEreE4FhajIopRAGSAGkRG1CJwfcnEqlMtZIhIiqhNRcc9Ff07SNMXg4CAGBwcRBAHy7QVYRFH0igN1ziFJEsRxjDiOn9cCduzYcdbVV1/9gs/84Ac/WLFkyRKoahlA3XtPzjmbtVrMiWs66BgQiIWcQpVZQwJ17eQFwBLQC9UNIEyHoqJGYwhIIO21QvbepGktHhpyQXeFOyvNDeCFHvi7yBAzb+zkwSIyLcuynl0rZa1FEASw1iIMQ1hrwcyw1oKInmcQRGR0TREiAuccsiyDcw7OOaRpOrrcvyfZxMyD1PbBgfd+Ptq+NhgYGDgJAIhoHwB1dFQrHhlxuSyteUg3g4xv93VKYFJomaCA0hoCtliBPi4EQ8B0IZ1LgscZUKfwgIoQo7S9v9nYtDGMpkwxYWhtZ2B7SCfT5wkRPU5ErqenZ924ceNkx44dPDIyAgCbReTg0edGK/8qihhj/qSqZuLEiWvL5TJv3ry5GMexV9UxqjoTQNrb27u8VCq9mYgeFxEbxzE11m8Mcn39Dc8QFaiFAkSkKoZID1YlgOQhQB9lBT2tKqsAgBTjhaTmSb0C6kDGiQa8dWuhsWZ9TtVb55wBMKiqxjl3LwCUSqXfjsJj5vqiRYsmBEFwOjOfEUXR6SeeeOJYImow85OvJrFdxRhzfxRFQ0cfffR8a+2ZQRCc0dPTs3jx4sVpZ+q2paen596VK1c+pqoBgMHMeyYiaqxdVwy2bCtnQoEA5Nu+TJ6gI6o0BgC86rMCeoYZfnXm0+0Ytc3CDXiygASqYK9MNFwrNNavnZ2mKURERaQOAEmSbN1t2GIXL148pq+vb8p3v/tdiAi+/e1vo6+vb+qJJ57YjfbK81/syP1XyAZVjRYsWHDc2LFj81//+tcxffp0nHfeeXjyySffsmDBgg3MvKNer49LkiTqdA11FYFzHsn6TbN4qJYnBYmCFQjEU6BKzVFO4tMdgF9rCVib+Oy00OQAAMTUVFEVpUyhUKhFko5F4g5Mk+QOIgqIKGVmOOemX3vttXfW6/XjrLX3EpE89dRTxy5cuBDLly/HvHnz8MADD2D69Ol45JFHDmDmewE8rKrjXk16RDQwadKktRs2bFgYRRFWrVqFSqWCe+65B4cffjhardaihx9+eG2WZVOMMRYARCTN0hQuzRLN0nk+i58ASAOwCOANgcHaVGlb4KbPxjtgjd0fqK4Uf/ROWyxqASYlsVBWBbwCCPv778x6dwzQPvtMMsZoEASbVfVtRx111E3PPPPMzQBwyCGHFK666iocwXXn3QAAEZlJREFUe+yxePbZZ1GtVjF16lQcc8wxWLp0KRYtWrT60Ucf3Wl+X6lB954WXk866aSJy5Ytwwc/+EHcfPPNGBkZwTve8Q40Gg0sXbo0nD9//vJWq8UzZsw4RUQ2dgyVtjZtGgj6+m9X8EQAQlBWKBFYSBBqh1Mq/m3zgQvabrNAnwIPEHAEgBMIspJUIKTkARaFMQ89OlJbvjw/5vTT1DlHxpgnACyaNm1a38UXX/wxEYGI4IEHHsD111+PJUuWgJlxyCGH4Oqrr8acOXPwmc985m9Hnxu1qLta11Ggu7+OAtr1lZlBRM+7Hp3OjVrwG2+8Eddccw3OO+883HXXXRgYGMD999+PBQsW4Oyzz/5okiT/KSLjVPVGL6KNRoOaD64o24cfq3nIFEOkAlIDdYAnAb+jw+sBAtYBnV25v4OphWyHmPidALqgtEIIxbbnIkOg5KrDJT3ggLebgw9ew4aNtTYlonne+2IQBN2qyiKCOXPm4Je//CVEBKtWrUJvby9WrFiB888/H8Vi8Xnw9gZy1793T3vT3t21kIgwefJk3HzzzajX69iwYQN27NiB7du344tf/CKstS5JkhKAsSKyIm61qNVspcnd95yst92RWMAYYjEABYCxsIMKfQsAOPGXpz77w/cg6xkAArj7W65VHs1cGNsZpBYMQKAgiFIevX23tTas64vjmFqtFlT1QQBzkyT5xWgFp0yZgiuuuAJRFIGIEEURLrnkEkyePPkFkF4see+fl17KZ3aHfdRRR+GrX/0qms0m0jTF3LlzcfnllyMMQ2RZ9gtVnauqf0rTVFutBK11m/pNb98tqhIqoKTC3LbAqup3jPJpuVaXh/sTsIun0dMwf+zJd+0PxWRVPA7CjhSglgIJhDxAaSm/OffZT0+17z+9t1AscqlYZCI6Q1WfiaJoWmfF+AUV/nMAdtfCF2vCL9Zsd0/GmL39PRTH8QARzfYi1zXqdbRaseh//fe41uXf326ayZQA0BwMAlLJA8SKsUp4EwhbBlsjz86DPw7Yxb1NgEuc91e0C4qDFboDECK0zY4KyNdbU2iknqT9/X1JHEur1VIRuU9VD0iS5Jo9NbndwewN3p408M/9AHv7vr3lPZriOL5BVfcDsCxNErRaLcRbt/XpSD1zzXiSgtgAyvAwbSPSq4Q3AYB6fzmA/z3KbSfAEP7melqfNrppQqKtAIAhUAiAWD1BbO2XvxyJ7l52QL1eR6vVEieySVWr3vszvff37K3v2luF9tZ0X+z6xZrti/Wfqoosy2713p9FRMNpmm6u1eucpqkU7n/wwPov/m+DAGsgQgAZAhkoWDQZ5TKY1ueuh7/9BQD3BxLfNtHXdHxAPgyiEduZzzJAEFI3PDxO+3aMYP3GvlqtybWRKrz3d6hqMcuyblUdGm1+e6vYi/V7f8n13mDuCWKnTL1pmk4CEKRpeme90aBGKxa/ZsN237d9OKnWxrQdKEm4c9qbCcPKdLa2rcEvAG2c0nZofz7AdjOWC0fSxuiZCMuCHQYg204IIEoA9f7kqlLlqZXvTONGrd5ool6vp9r2OD04juO7te3L8qKa8FIMyksxIC81H1WNW63WU0R0sIjcFsdxVq/VEdfr9a6nn17c95OfFRhCFkohFBZqLKAQ7kfHi62aNtcmkG/syux5AA8GtntxBwB0BwAo0YcD5WFDpAYEA4YBQzPXvf26/75vzP0PlhuNmtZqDWo2m4MAHgLw3iRJfrWrEdj19aU06d0t8K4gX8p37CnvJEl+h7YP4oOtOB6oVqtaq9do3EMrituvuW4ZMt8TgikAwxBgicDKwyD9CAAocIsTN3th22N2zwABIIb8r6G0vqzd4jUnLD4E1HS2OgJAQ6KkuXrlfn79Rp9fs663Xq9ptVbzjUZjjao+AuCDzrlfAHhFNPFlal6aJMkvVfUMAA83m821tWrVjNTqlHt27WbevJmba9bMMgRvALWAsBIFgIA1BRAqgKG08TBBzt+d1wsALgS2irhAIT8CFFCcYYFVAUHzgIYAWRKyUF37o590j9u89ah0247eWrXKw8MjaLZaq1V1uYj8TZZlN6jqyN60cW9a+WLjwJeibaOvALamaXoPgLMUWN5KkjXDw8M0ODSk0rejf0LvjiPX/OA/ihaAhUoHIIekxJCVpLqkzUB+KOLsfOAFUZH2eNDmQ9ClcG5J3uZmKBBCKWcNDQsQCEQ8wSiIRMT0Llvef9CCQxZsjIKnHbikIgKgGQTBJlU9xTm3GsBqANN2b3Z7et2TMdh1/Lf7GHBPY0IigjHmHu89EdERAG6O47hvcGjI1OtNifsHBg5cv/HYJy765gbrfaVAxCGBilDNEWyOTaxCbwJhHIGqQ0ltex/kcz9re9/+eYA/BtynYaqG6HHDZhEIE6D6EClXAHgHYgAsBPLqo833P7Bm4cELjt1A9GhGWnaqEOdbzPS0MWauiExX1Z9re+Qf7KkP2xO43QHuCmhP0Dpz4CERuVZVTyYicc7dPlStJrWRmtZrVY37h/oXbu094bFvfXuFSZOxEZGNFDYCfA6MHDGp6nYiHA8Aqc++lIn//RGQPUb32OtZuR9C1nxc/GcjGz5JoIMBmk+svyPQBAACUqiSVVJIlhW3LLt/5eGHLzx8o8NTKUkh88555wNAN1pr+1T1VBFZx8w3icjB2j6a9bwmt2uzHJU9QdqLBgoz/1xVJxDRUQD+2Izj1fVaLalXq+FwraZZ//DWI/r7j3/4m998gprNSTkgyIE4YmgBLDkiWKUniXAOAAjwi2ra2Ocg+Ev3xmmvAAHgH6A31lz6oZzJEYCxUD6YCHczoUeU2DMMQCBSylzWtfHOu2tHHnXk9DjLHu/1bqzKzj4sZrarARUROUnbHq2/Q9tFrmtXiKPXe1p5GZ2K7QZvC4D/ApADcDSAJ0RkRa1Wy2rVBtXqVQwODMvkoZGtbxoZPuyBr3ytj5JkfJ5gQmWTJ0IR7AsEWKb1qvhIh8uq4WSkVYJ+6N/30HRfEsB/B9zHgWVO3fjIBAsIGhHRDIY+xNAygRVQgjBDlcW7YN0dd2bzDj7YTjXWPJVltVbqOUvTXOYz8c63VGU1M9cBHKKqU1T1NgB3S9uzfho68/Pdm+0uyRHR3UR0B4DtRHQgM5dUdV2apk83m83myMgI1RpNHR4aMLVavX5stV6sbNpaWP71i4qBl2IEaF6ZCyxSAEsemgXEW1RwGkGLANxI1riaVL4yp30YfK/ykmImPApzap7t3EKQ+067ctigwP2xYlwq4AYLN6AcgzQW9S0oeubM2fTmv/v0kY8Q3bUxF3bnCpHJBYFEUZ7CMEQYtnfjmDnsaOE07/047/16Vd3S8RZIvPdgZgugQkSTmHm6MWaYiDZ2oKejO3Rx6rXValAaJ9qMWzohTre9FXTy4z//5dLtDz64fx7gHLdHE3kYXxDhHINyxFsVeCsU+wKQetb6p1T844fA3/jn2LzkqB2PAOeUOSqHQefoP2EThP6YkUxvgnwLoi2QT1RLTZUkUTXehukxXzp/MJw8acyt3j1RZTOhkM/bMBeQNYHmcyEAAxMwhUEgaLurdcJmAUTtU/LS9kfU9jUABrIkJS8eBGiSJJRlmaZpqtV6S3oEAydZO6+1bVvfsv99yVjj0jBH5AsgHzKFecAVwFIAyCi2EeMoKGYBQJrF5zUlSQ4G/s9L4fIXxY15BPhqV5AfsRx2INIQAdd7yP4tQJseQQvQJpRarC4VlRQQU+5qHP2Fz6e5CRPG3dGsr9geBOMtIQzDnLBlCkyAIGBSJTWGoKoZGePEOQ8AbK1R7y0RWSfKDEWaeiiJpnEK5zJkzmfjVftOyOXfnG7v237vv12Wk1qtEAKImGxOyOZBvgClvAHyIDWglar0PwjtfjiV9Lx61iq8CfjWS2XyF0cuegz4+zyHYRTkvwmAFUiJ8WNRPTSGaqzwTYEkDI2hpuVhHSFLoGq7K4NHfvQjtfEHHHj8mlZ828NJq9HnsnHGhNYGhgwD1rYNhaq6dgwZoDM5sCKs4jLy4uEyp+KzrDsIB95aKJSmBdEJ2598+s4HfvaziqtWe3IgChQ2b8hHUJ8TQp5h8gREIEtKjwP4KLU9yKSZxV9IJPmL4P1VAAHgUeAjAduppaD0v9CJd2oIvxKlcgJfjAHTFLIxqcaKLGXlTGBa0CxTsWK40TNjZt9hH1hiumfMeEcs+sS6VmP1hjh221KniXrK4I2gHXiH4cnA+DwZnRQEPCOfs7ML+f0j4gMH1qy/88Hrfikj69aPZ9FijthZUBAxfCQkOYYtClHI6nNgH4AalrThFWd2CAwOp41/F3FrDwV+/pey+Kujtz0EHMXgT48NS3NBGI0XuAqge5TkgFhJEqiPgSCBaiJwKSRogbxTNY7IZ6rGkWYmFw10T506MnXBm3X89Olhvru7GJXLlSCKxgNAliR9rWp1JBkZafZt2JBufPghHtmyuUviZEygFAREYlXZErkIGoTgNMewEYhCIMsBlCMERvkpJX07FPsDACnuG0rrazzk8gXAn/4aDi8r/N2TwJgU+HlPWHqQiL/y3B39NROJE0yJiVwM0VQ0TIE0ZVgH+EzEZKAsgwYegIdmClivnCl5B2KFSHv8xWyhQlBjLElIgCOQDUFqAR9AA8vsQ4ADgc8BgWVKc2DOqQbM2KSqAYHeN1pCUflaNa0fTsCHDgGG/loGLzsA47WA2Q/4Z8sWXUHhQ9o+nAwFUkB/ysAYrzQ1IU0cqYmVxAGcifoU4IwhHmIgTI4FIuQEUAWI2lNGKFQIHZcxVmuElVgQgL0RmBDwAZOxgIQEEylcpBQxY6MRJI4weo4PAJ5pZM1rMnHuTcA36bnjZH+VvGIxVB8HZjvghyVbvCkw9hsKLXRuCSn+rzIEqtMzgnEKTQnkPJwzCJwCHuqkbTUcAeIERNSeAajCWoZqe2XcctuqBJahgUdmGUEAeEswgcIR0XoVWGqD43ZFqZn49CtN11rkgE8d3g7487LlFQ1CqwCtgDmVIH9bDIsPWeJ/1vYUa/T+kwq9j5WKRDQ5UwmFkHmAPRSOQOpJhSHtlcT2fI6gCiYigWGjsAplkBoAAWAMOBPVbSBtEOhotCMZjVYwdioXN9LGmwX844Xwv38lQyK/KmGQHwQCMmYJRD6UM7nbQms+D6V9n/cQ6VYAdwLUIiBSRQVAt5IaArwHgbQ9jFESNu1ItKbtLIFhIlQ73UQOwAlQmrRbzTaIc5c3fHyiMv9Cvb/2sOdicb1i8qpGMleAH7T2BPL6T9aYu3Im7CHQeXsvDQ1CZQVAI6QY0fZ0DqRaVEIXoF0gXgDVMXv7CoVeFvt0KPP+WGPoWwucu/Pl9nMvJq9ZLP0HgXFg89U8h+uZ7SWvRh4i7vyGZNNZ3DcOA171MPDAawhwNL8/sfllzuQ2M/EL9hdejojq5bFrTT1M/RmvVtj3Pclr/t8c7gRswdjfFzjHoOfCh7wcIcU9dYlj6927Xo1+7sVkz0d7XkU5HnDq3ftjadWVdNPOU6J/bSLd1pBWb+bdktcaHvA6AASAo4Bq5s1Xmz79tQKpoN3L/6XJAy7x2VXkzdfe9jJmEy9HXheAAHA00sdJ6QFR/Ye/FmCq+g9edeURSF8z5/Xd5TXvA3eXZRxcam00zMDukeX+nHwj88nYt/jnIvC+HvK6A1SA7jPhb4wJqtSOofBS5NpMsuKtLn3Pha/iGO+lyOsOEACWAXm1we+Ywm4iLPwzjz/mJNuSufT049vHJl5Xed36wF3laKClLjhbJHtEQdW9Gw2qZ97f5505940AD3iDaOCo3GNzx4HxVlZz0Z7ui8o/KrLlxzr3x9e6bHuTN4QGjsrbXHwXRJywnP8C7WP5AkHkjQQPeINpYEfoHpP7hRrapEr/0H5LL2fR8W/z8d/gNZymvRR5IwLEnYBlW/i9gJiACNBYXfOU41/ExeL1kjdUEx6V4wEnLlxCkCogfeqaZ74R4b3h5d6wNP/esDT/9S7Hi8n/B3LrBEUxxEM2AAAAAElFTkSuQmCC\"],\"useMarkerImageFunction\":true,\"colorFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n if (percent < 0.5) {\\n percent *=2*100; \\n return tinycolor.mix('green', 'yellow', amount = percent).toHexString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', amount = percent).toHexString();\\n }\\n}\",\"markerImageFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nvar res = {\\n url: images[0],\\n size: 55\\n};\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n var index = Math.min(2, Math.floor(3 * percent));\\n res.url = images[index];\\n}\\nreturn res;\",\"strokeWeight\":4,\"strokeOpacity\":0.65,\"color\":\"#1976d3\",\"tmDefaultMapType\":\"roadmap\",\"showTooltip\":true,\"autocloseTooltip\":true,\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"labelFunction\":\"var vehicleType = dsData[dsIndex]['vehicleType'];\\r\\nif (typeof vehicleType !== undefined) {\\r\\n if (vehicleType == \\\"bus\\\") {\\r\\n return 'Bus: ${entityName}';\\r\\n } else if (vehicleType == \\\"car\\\") {\\r\\n return 'Car: ${entityName}';\\r\\n }\\r\\n}\",\"tooltipFunction\":\"var vehicleType = dsData[dsIndex]['vehicleType'];\\r\\nif (typeof vehicleType !== undefined) {\\r\\n if (vehicleType == \\\"bus\\\") {\\r\\n return 'Bus: ${entityName}
Bus route: ${busRoute}
';\\r\\n } else if (vehicleType == \\\"car\\\") {\\r\\n return 'Car: ${entityName}
Current destination: ${destination}
';\\r\\n }\\r\\n}\",\"provider\":\"tencent-map\",\"defaultCenterPosition\":\"0,0\",\"showTooltipAction\":\"click\"},\"title\":\"Route Map - Tencent Maps\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" } }, { - "alias": "image_map", - "name": "Image Map", + "alias": "test", + "name": "Trip Animation", "descriptor": { - "type": "latest", - "sizeX": 8.5, + "type": "timeseries", + "sizeX": 10, "sizeY": 6.5, "resources": [], + "templateHtml": "", + "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", + "controllerScript": "self.onInit = function() {\n var $scope = self.ctx.$scope;\n $scope.self = self;\n}\n\nself.actionSources = function() {\n return {\n 'tooltipAction': {\n name: 'widget-action.tooltip-tag-action',\n multiple: false\n }\n }\n};\n\nself.getSettingsSchema = function() {\n return TbTripAnimationWidget.getSettingsSchema();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "{}", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var gpsData = [\\n37.771210000, -122.510960000,\\n 37.771990000, -122.497070000,\\n 37.772730000, -122.480740000,\\n 37.773360000, -122.466870000,\\n 37.774270000, -122.458520000,\\n 37.771980000, -122.454110000,\\n 37.768250000, -122.453380000,\\n 37.765920000, -122.456810000,\\n 37.765930000, -122.467680000,\\n 37.765500000, -122.477180000,\\n 37.765300000, -122.481660000,\\n 37.764780000, -122.493350000,\\n 37.764120000, -122.508360000,\\n 37.766410000, -122.510260000,\\n 37.770010000, -122.510830000,\\n 37.770980000, -122.510930000\\n];\\n let value = gpsData.indexOf(prevValue); \\nreturn gpsData[(value == -1 ? 0 : (value + 2) % gpsData.length)];\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var gpsData = [\\n37.771210000, -122.510960000,\\n 37.771990000, -122.497070000,\\n 37.772730000, -122.480740000,\\n 37.773360000, -122.466870000,\\n 37.774270000, -122.458520000,\\n 37.771980000, -122.454110000,\\n 37.768250000, -122.453380000,\\n 37.765920000, -122.456810000,\\n 37.765930000, -122.467680000,\\n 37.765500000, -122.477180000,\\n 37.765300000, -122.481660000,\\n 37.764780000, -122.493350000,\\n 37.764120000, -122.508360000,\\n 37.766410000, -122.510260000,\\n 37.770010000, -122.510830000,\\n 37.770980000, -122.510930000\\n];\\n let value = gpsData.indexOf(prevValue); \\nreturn gpsData[(value == -1 ? 1 : (value + 2) % gpsData.length)];\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"history\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":500}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"mapProvider\":\"OpenStreetMap.Mapnik\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"showTooltip\":true,\"tooltipColor\":\"#fff\",\"tooltipFontColor\":\"#000\",\"tooltipOpacity\":1,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
End Time: ${maxTime}
Start Time: ${minTime}\",\"strokeWeight\":2,\"strokeOpacity\":1,\"pointSize\":10,\"markerImageSize\":34,\"rotationAngle\":180,\"provider\":\"openstreet-map\",\"normalizationStep\":1000,\"polKeyName\":\"coordinates\",\"decoratorSymbol\":\"arrowHead\",\"decoratorSymbolSize\":10,\"decoratorCustomColor\":\"#000\",\"decoratorOffset\":\"20px\",\"endDecoratorOffset\":\"20px\",\"decoratorRepeat\":\"20px\",\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"pointTooltipOnRightPanel\":true,\"autocloseTooltip\":true},\"title\":\"Trip Animation\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":false,\"showLegend\":false,\"actions\":{},\"legendConfig\":{\"position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false},\"displayTimewindow\":true}" + } + }, + { + "alias": "route_map", + "name": "Route Map", + "descriptor": { + "type": "timeseries", + "sizeX": 8.5, + "sizeY": 6, + "resources": [], "templateHtml": "", - "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('image-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('image-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('image-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n", + "templateCss": ".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 200px;\n white-space: nowrap;\n}", + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('google-map', true, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('google-map', true);\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('google-map', true);\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermomether\\\";\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See advanced settings for details\",\"markerImageSize\":34,\"useColorFunction\":true,\"markerImages\":[\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAwgSURBVGiB7Zt5cBT3lce/v18fc89oRoPEIRBCHIUxp2ywCAgIxLExvoidZIFNxXE2VXHirIO3aqtSseM43qpNeZfYKecox3bhpJykYgdjDkU2mBAB5vCamMNYAgQyURBCoxnNPd39O/aP7hGSEUR24L/uqqf+zfR77/Pe69/Rv6kWwcgPLRIJfZUAa7xez2xd90QBwDSNZKlkHJHAK+l09mUA7BP4vPpRUVExMVoRef+L998njxx9X57vPi/PnTsnO850yPaT7XLXrrflqjtWymhF+HA0Gp0wEp/kHymEQqG4ptJDGzf+um5RUxMSiV7Z3Lyt88L5nozgHJWj4pGmpqZav99PWve04onHHuswmViQzWb7ruZX+Udgv8/z3A+f/NGye1evxssvb+wo5PMfTZs6bfqcuXNHL7hlweh58+ZVAOTUpk2b0p9dvjyqqmrs/b8ejpUMc+unzjgUCsXjsYruE+2n1JY/NedM0zCi0VjA7/d7/f4AAgE//H4/vF4fOjvP9h5695C/oaEhcN/q1SyTzVdnMpnklXzTq4EplUsXfmaRCgC7du3cOn78+KfGj59Add3z1Md1vV7vqPa2D1sA4MYbZ6qUiqVX9X21i4TQcfX19QCA6urquN/vn0kAPRQKpYbTnzRpUhgAampqAEFrPjVYSql7fD4AgK5r2tV0AcDj8WkAoOk6JJGeTw2+nocLdsEu2AW7YBfsgl2wC3bBLtgFu2AX7IJdsAt2wS7YBbtgF+yCXbALdsEu2AW7YBfsgl2wC76mh/ppjIQgXVloPxVSBRV0rBe455P6+kTKBYF3tonxY/IWarry7DvI298Tgp0PR9RzACaN1NeIS100+EdvKXW3cMZvF8wCK10Sq2it2NAzakmukP/wmoP/KuId3BRUMg5uCfCSNVSKVn1rNto7Un8jLrUVqJ4Fi2eEQiEYBzOsy3SYL37TNQdzi8Q5FxkqJIQBsNLlYMGF/zqAJWBxSEogDAY+DJibYqTuRg4WFgO3OKhCYTExbKk5G/mbkSPP2DQhLA5IO/NhSz1MMP882BDgnAFQwdiVSs2vPVhYDIJLUMkBgw1favM6lJoZDDAYhKbAYsOX+rqAhcXAuQSIAKzhSy2vS8YmB7NYH4WCfM7kw5VaWtdpOO3bfWZJZVXgPxMX898bVsm6RhkTIseX29yyIErm/J5z5vwr6pvmsLYjBgeDwSpVJS/OmT1n1de+9qANZgLc4q9Dyj2qQhUhSSUAUCL7GBcchCymTEYBYNWqVXj30MGHT586PZEJ+WAul7ts8bjspd9QKDRNU2nz4z94YtI3H3oI+XwB//3j/9m77eRUUJ9/0eh4APGoDz6vCi4ksgUTmYyBC4k8RLGwtzF+EGu+tHqRqqrYtm0rXnzhhQ7G5cpsNnvyiuBIJFKnqvSd55772eilS5fhwIH9ye+/dPaEf1T9otW3T8GtiyYgGNBBymYEgLSbvakidu8/h01vnkYhcab1gcVs5tx5c6PHjh7DU0/9qFsINPb3939UZg28X11dXR0Qwtr9g8efqGtc+Bn89re/O7FhR9BXNaFm+n98uxHTZ1SDKQqKAihweZlITUVtXQwNs8fg+Bmzdk+bnmPdf/7bwsbGeO2ECaED+9/5XCxWuTGbzVpDwJpGNtx+28o77rr7bmzZsu3k7z+cMlHzeiPrvnoTwtVhFAVQHAZY4HBEoiAAeDXUjI/gyJGeQEd6TFj2tHYuXNgYy2azVe0fngiWDLNloHNFo4FZkXDsoTVr1+KD4x8U/3Ci1qP5PV7N74FeFUbClKDEriy57A5JANL5a68hnqoINL8OAPqbXbNp7clTxTVr1/oOHjr0MFXxq2Qy9wEFACnoY//6la9QAHj+9Q/eUL2RWkVXoWgqkhZBypRImkDKBFIWkLIk+h1JWdL+zrmeNCWSDFB0DYquQvWG637TcnozAKxbt45yTr8PAGowGBwVDAbvmT9/Pvbu3dddijV9WdUUUE0BUQm6kwaCYe+ljK/w8ruUdsYCBLlMEUQhoJoCygWM+LIvHTx4sGfevIbqYMD3BSFkJVUUrG5oaFABoPXwhd1UVUBVahtpKtoOnEV/gSHHgBwDso5c6XO6yNF24CNQTbV9qBRUUenuwz1/BoCZM2dplOJeSggWL1myFEII9IeXziIKBVUUW1QKo2Ci41Anei9kkWcY6Ex5R8qfc0wi0ZPF6QNnYeQNB2j7IQpFOtg0WwiBxoWNIBKLVQI6Z8rUqTh69FiWaFNmEIWgLFShoM5TZbIzgVxvFp6ID5rfA6JQgBAIxsGLJkrpAsycAcH4gN1gX0QPTW9vP5Grr58cJJTOpbqmjgWAnp6ei4QSEEJAKAGh1BbHCS2DLAFmMAgmICwObjDnyYMMAtJL9oN89vRc7KWUQtOUsSqhSggA8sWivSEh9qBxTiCEAGRwQARUVaB67Hf5pZAQlA0Ayrq2LTCogVyhlLURNEw55yYABP2+4ED3vHSClBKQ9jiFdHqvEBCMQzAOKYSt6/RqSGnbDPJRbgT93hAAcM4NyhjrBYDKylhswEEZJgYJFxDchnGTwSqasIomuMnsIDiH5GKIzUAQTsCVlZUxB9xLIUVbKpVEff3kiLTMfimEA7HP5bZgHMJ07mnJAiuaYEXT3jcZDMLkTgBD7exgBKRp9NfVTQwnk0kIKduoJGRH8/ZmhMNh4skc3DnEkDlAi4GbtjDDguVAmZM1M6yB68JyKsCGBqD373s7GAySnTt3gBDyFhWCvPHee/8HAJhTU5g0BMg4uMXBTT4AZSUTrGjBKpiwCnablQbDbZuyfTmAuRPMegA4euQopCRbaCaTOd2XSLzX3d2Nu+64bR7PnP3LJSCDMBm4YW9FWcmyQYMytsW+Zpfdsm1MdimAdMc7K29bMedCdzeSyeS76XT6jLNI4PGf/+w5aLqOu25IjOOWKcSg0jJjcLZ2ecsZD5TdybqsOxC0ZYpbJ58frek6nn/+eVBJHgecjXkqk2nu7Ozcdfz4cdx556rJN5C3m8v3jBt2xpdnazjysawNy5lUbKkrbmtZsWL5pGNHj6Or62+7k5lMy5CFNRQKTfN6tAMvvvhSRe3EOqx/4oXXLvia7qO6CsVZrey5154KB5YpKSG5tHs+5/ZsZnEIk6Ei1fLH73373i/09fXi0fWPpgyTLchkMqeGgAEgHA5/vjJWsf2PmzYr1dXV+K8fP7vjLxduWkY8ilpetQZPg+UJxh63lzqlNDi7gTa3fuPraz6bzxXw79/5FutP51am0+kdZdaQ/2kzDKNDUci51179w8pbP3er8sAD6+pnVCWy+/fs21LAqBnlMT50qJXFLq2a2L/5gaVy7N133j69u7sb67/7iFHIFf4tlU6/Ppg1kLGU8hYAywBMeOWV33gfXb9+1Q+ffDL+4Ne/AcYY/tS8PbV5++4Dhy+MopY2ZrLiidQDgDBSp5TS+Y7psS65ZOHsW26++eYosxje2PwGNm586eKzz/x027+sXWsBOAfgbULIQQAgUspaAA8BGAfnsamrq4u0tZ0Q333kkdGmZS3f8JNnlBXLV0AOilRKCS7sWYlxjlKxgHw+j5Y3W/C/Tz/NQ6Hgjp9seKZ31py5ajwe4wAtz9zdAH5OpJTPAqgEgL5USkpu4eLFHloqFXniYh9t3bunauuWrStisSi5//4vYnHTEkyZOhWqokBICcuy0N7ehr2trXjt1VeRzqTl3ffc81bjgsZELF4pQ6EAqa4eI6UEicfj5dhTKoCikynx6Bop5C14dJ2XcjmouipvvGFGoSJaWfr738/7tmzdjl/88pfIZjKwnH2SpmkIhSMYW1ODhvmNGFcztjhudFXR69Wgck58Hg+XEorH5ylDJYA8kVKOckpdB0ADIBOJhOzv70OhUFILuTzPZLNcSE6SfSlvJp0O5A1DN0qGDxLS4/OUAh6PGQqHC5XxeJEQgkgoRH1+L/wBP6LRuIjH4+Uf8gSAUwB+MbhzzQSwCMA0p/QUQADgNJ/PJ/v7+wnnnFiWkJZhKCYzKADoqiZUXeW67iGcSxKPx2QoFAo7AybnuE8COAZgHyHkxGXjeFAQEQCzANQCqAIQBeAH4AXgcex052w45TMcyQHIAOgBcBbAUUJI5uOM/wcaHmf3g9UM7QAAAABJRU5ErkJggg==\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAA3vSURBVGiB7Vt7cFzVef+dc+/d90OrJyO/JSO/4ncxxfULMCYIAyEW08amJJgmM4GmnZjJdNq4gcSGzLQxk3bsaWcaaIHyR8CJrWAbpjgG/AhINsbYxkaSDY6xJFvSrrS7Wu3uvfecr3+cu1pbXhkJs/4nujNndufec77f+d7fd+4uw8gvIxwOfocBaz0e91yXyx0BgKyZiWUz5kcEvBKPJ18EYI+C5rWvkpKSyZGS8LGHGtbQR8ePUUdnB50/f57OfnqWWlpbaN++39O99fdQpCR0NBKJTBwJTfZFE4LBYLmh8+YXXvifKctWrEBPTze9+cbu8/3JVMoWNjwer3/ZsuUTvV4P239gP36yceNZW9CtyWQyei262hcB+7zurU/99Ge3r1nTgJdfevFsqr8/Wlc3rWbGzFkV8+fPr1iwYEEJgLadO3cmbr/jjohh6KXHPjxamsmar39pjoPBYHl5aUnnqZY2/b1Dh9LdPd39kUgk6PP5PD6fH36/Dz6fDx6PF+fOfdZ9+pPTgbq6Ou+aBx+0k/0DVYlEIjYcbX4tYM5pxeK/WKIDwM7Gxt0TJox/dtLESXC53JuHzvV4PBVHDjfvAYDZs+fonMsV16R9rYeM8XG1tbUAgMrKsrDP659DRJ5gMNhbaH5NTU0IAMaPHw9IPv5LAxORy+31AgBcLsO41lwAcLu9BgAYLheIkftLAxfzGgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4D/lME1ke7gDF8ltbOHe3W923oEwYi1jxftWfZWgAziwacZkd2pfyN96XN5IIu7dMtIKA9/TI+zqCnFps2Alg5UlojFnVqIHZUlO2sl4RyC4CU+SEEylux8Z/iyc7mrxw4U7UnYwvGpXMYKIgNGdwXC/76C48oRw3sDWfnCgIkARJXcpwbvpA1e6T0Rq5jDr8EAHKA6OpjUOJwfeXAJAEhAXAGgEPKq+dIMVJqowDO4RAAC0rHV21u5LijAJaABAOIAY5Oh15iFMgj1zEpcUuuXjpIWeCouxjAtnIZcGKA5AVFbRfazPUC50QrKe8+Qy8qiqjBYIODA5DgBd1pBO9WRg9sy7yOhXBca+icYrgTOUGOiKnIVdCdisAxJGBTPsYW0nHRrJqgfNmGVtiqaeR1xchF7Vgz40q/BUNmISlcL7CUgJAMnOUiVwEdF0PURIAAVHaC8ucbAiwcQAb1KQpwXMjFrhtYMcOVO8lhOB457ujcKZd9hBguSYwcelTupKyaQWKYJFEU4xJw/Dhfcw29ilSBcNjEoTucFnSnkeOOvvTJpcVC1cYoGB5NAGEQTukjMAzHoghJghyWCRjenYoTuZjKx8xJiwU4LrSZ6waWpIoBjTuRqxDHRUkSUMWAJAZp6QU5FqOw65HHapG3bGVcBTZXDI5VnFaFgBL1yC34uoBJqEJeIwD2MMY1ilZidAFEMlDOqm9UdpJ0ZawumI+LU9ArwhyqWxyNz14XsBAMUnLVH0ttGB0XococdCGWE3XhOV85MF1WV2OY3omK0S2SkxgYAZYYJoAUpcqEEjG/Ru80isA1ysMXYNCnCum4aKUPgTu90w3sFinXL6nO/MadCAhiKloxBjFMeSuK0S1Kylv1cE1bUVoYyHwhoI6bCswpjjuxK5u2G2lcti2jzNCRTluioHEVw52EBA5/2LKsLBL+h2gs/o+Fjpa+MqtmjCbkqQJSYFF3T3zRsPMvA75i7UiBA4FApa6z5+fNnbd6/frHADghk7QdlhAHdMY0KXkZAHAuozaRMDRtKYMdAYDVq1fjcHPTD860nZlsS3qsv7+/+6pNDr0RDAanGTrf85Onnq75/uNPIJ1O4+dbnj34Ot6B4eFLqksqUeEvgcflAREhZabR09+Li/EorLQ4eFv317D2oW8t0XUdu3a9jud/9auztqD6ZDLZOixwOByeouv8D1u3brtpxYrb0XS4Kfbj3//8VHC8d0nDLXfj67OWIeQJgDGADfoOAxHQl05i14l92PHBXiTPp/c/OrFh9vwF8yMnjp/A5s2bOqXEbX19fX+8CriqqspvmunDTz/10xkr71qFnY07Tr1i7aqsLg2Vb6h/GOPCpdAYgTPlNLmF5AzpvBRp74viX3a/hO6+ge47+hZG61fVTz9y+DCee27Lx15fYFFHR8cAcNkPuw2DPXfP1+vvvf+BB7Br967WX9Mbk70eCn33zlWoCrsgKAFBCdgy/2nLBCyZgCUSMGUSpkzC0G1MrKzE0XMt/la9I0QnM+cWL15cmkwmK1tOnwpksuabg8YVifjnhEOlj69dtw6nT51Kv2q96fYG4fG7gbJwFhn7cxicIJgEZwAfEiokGASpWG1KhvIwg1/91ti1N9DEJ7ZOzKxdt87T1Nz8A67jv2Kx/o85AJDk//zXjzzCAeA/D7zU6PZjkkuXcBuEjN2OrGiHabfDFB2w7HZYoh3mVaMDWWdu1m6Hy5Bw6RIuP6b87+HXdgDAww8/zIXgGwFADwQCFYFA4BuLFi3CoUN/6LRmyL/y6gSXTtC4QDTVgQo/B5iEJFJ6Rt64lI6Vfi3JYBFHd1JA5wIunUNIQvpr/C+bm5u65s9fWBnwe9dISWVc0/DNhQsX6gDwTuuhd3WNYOSGTjjSehGp7EVYsguWuJQfssu51wVTXIIpLsGWlzBgXsSRM5dg6Hk6uk787Zb39gHA7NlzDM7xoM4Yli5fvgJSSiRmmbP9HNA0Qm4D6axEc6uJ6eOzuCloQuOOjlneqiUx2BK4lDBwut2DTFaHoXFYGilaHEjMMOdKKXHb4tvw/nvvL9UZ+Lyb6+pw/PjxpOZhsziX0DigcYLG1QaEBD69ZKA7wRHx2/C7BDSNwEi9AEmZGmJJA/1Z9SJM12hwvcYBzgmaj89obW3pr62dGmCcz+cuQ68GgEtdl7oYU40CZwSeW+As1rmy5KzNkbY1WILDlOp71ubgnKA7czVO4NyhwQhcFS7o6urq5pzDMLRqnXEtCACpdCrFHOHlAsTgYEq0nCnj0jnBY6i8KCTLBxbmzB2yPkczmU4lAYAxHtKFECYAPeDzBQZD4GU+motMueXklECWc7QkSaVDGoTAVetz8AGfLwQAQoisbtt2N4BJZaVlpZQjkntdS8w5UFOFni0YLMGhWfny1rbVPVuoOVKyK9ZeTrMsUl7qAHdzkPyktzeG2tqbw8KihCQlPjVUl2hLBkswmDZD1mJIWxwDWTXSFkfWUs8sZ64QzlqHjiRA2tQ7ZcqUYCwWgyT6hBNjb+3ZvQehUIi52tje3M6FyHHIYNkOqM2RsTjS2cuAs+pe1uYKPLcBkduA+m60sH1+v5/t3fsWGGP/x6VkjR98cAQAMNc7bXJepAyWzWHaimjW4siYDGmTY8DkGMhqapgcaVM9yw5ugMOyeX4DkmGub1otABz/6DiI2O94IpE4E+3p+aCzsxP333PfAvOi2G8JBtMRbU68GZMj44Ao0BzXmgOsRk7spq1oWILB6rQP3nt3/byLnZ2IxWKH4/H4pxoAeFzuC21tretW3rUKnk5mtWiflzAGxhgDQ66IYyrnOnqzBFfDZjAdLk1HMnkpMWRNLldmFomamtrIL/71F+iPJ/8mnc2e4QDQm0jsOXfu3L6TJ0/ivtX3T607M26P6SzMWI5eB7ktPHLPc/MV5xwTjpe9sfLOu2pOHD+JCxc+fyeWSLyZdzCoWsvjNpqef/6F8KTJU/DDLT/a3jM90eDWCS5dqmDvxF7NCRSAOikQhCuMUXHMEDjm3v7jb/+oIRrtxpMbnuzNmvatiUSi7QpgAAiFQneXlZbs3rGjUauorMSmLc+8dShy7HbDELqeA3bC4GCScHxWSMDOgVuaPb2t+t3vPfK9O1P9A/j7v3vC7ov318fj8bdyWFf8YCSbzZ7VNHb+tVdfrV911ypt/bcfq52J2uTBg+//LhWwZ0nJYTtWf6WrcccDGFgLdn5nwkPVD9Q/MLOzsxNPbvhhNpUc+G5vPL7jcqxBjonozwEsBzD5lVde9jy5YcPqTZufKX90/WOwbRv7330nsffDt08dSB41EkZyHPfwmwBAZuTFsBm48GeuWfai2oUzp02fFjKzJhp3NuLFF/+765e//Pfd31q71gLwGYC3GWNNAMCIaBKAJwBUO3uQnZ2d/MyZNv1vn/j+LUuXLq/Z/MyzCIfDTmxW8Y+IVFyWqjKRQkDYNqKxGDb97GkcOXLk7LZt/9F8c12dqKqqYM4LYALQCWAbI6J/A1AGgKK9vSBhoa8vEe+N9TwejcZYU1MTfrN9O6puqkJDw0NYtnwFpk6dCsZUMrFtG22trTiw/11s3/4aotEo1jQ04NZFt6KsrJTCoZKtJaWRiGG4KBKJ5BJWnw4gDedAx+0yMJCywLnQGWOSMabV1NbikUfX40J7B367sxFbt25DMhGHZZkgAC7DhWAojOpx4zF3wS0YP64aVZUVYCoQSN2la4bhIsNlcOS73H5GRBUAHgcwBYABAD09PZROp1gq2V8WTybq4vH4xEQ8oSWSSfSnUkinM7As9RdUw9Dh9XoR8PsQCgYRCodESTj0x1Aw2OrxBXsDgYBdXl6eM2IB4CyAbZcb12wASwBMB1Dq7C4ACJZIJHstM5PWdC2TTmcom80wEtySAFwupum6wbxeDxeCuT0et8/v94UBTTrSJABRAKcAHGCMnbrKjy/bRBjAHAATAFQ5NuAF4IFqAtyOKzKo83MLgAkgA2AAQB+ADgCfAzjBGIsPxfh/6wbDK7xbMFYAAAAASUVORK5CYII=\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAyUSURBVGiB7Zp7kFRVesB/5/S9PdMz/ZoHMwo4MICDuoGVIYICIuzGcn0vC+oWGuNjs8mua9ySP4wpgyaiVVupbHYTsLJmNT7WNXExwqqzrq8g4oNxdXUgyEMQARmZd3fPTE/3vfd8+ePenhlgBsFlrFSqb9Wpvn3vd77f+b7zne87ffsqjv+wE4nYDQqWl5aWfDUcLqkAyOUHunID+Q8EnkilMo8C7gnoPPaRTCYnVyQT71+1bKl80PK+HGw9KPv27ZPde3bLjp075NVXX5FLL7lYKpLx9yoqKuqOR6f6PIFYLFZtW7r54YcfqV+4aBEdHe3ywm+e39eb6etzPZfS0kj5woUX1EUipWrj6xtZedddu11P5mYymc5j6Q19HrgsUrL67r/7+8VLly7j8cce3d3X29vZ0DB9yplnfWXcrFmzxjU2NiaBXevWrUsv/trXKmzbqnz/9+9VDuTyz35hi2OxWHV1ZbJ1245d1ltvvpFtb293Kyoq7LKystKysnLKy8soKyujtDTCxx/vSW3fsT3c0NAQWbpkiZvp7a9Np9Ndo+nWxwJrLYvmzV9gAaxbt/75urrxd592Wp0Oh0tWHSkbiUQSv3unuQlgxoyZltZm0TF1H+umUnrC1KlTAaipqUpESmMzFIRjsVj3SPJTpkyJA0ycOBGMnviFwSISLolEAAiHbftYsgAlJREbwA6HESUlXxg8lkcRXAQXwUVwEVwEF8FFcBH8/xhsnZC0ksw49eQPI5mmNtP54ccAIvqgqbz4aYn8zYoTUXXcFnueyZ8eXtleZt75iQnpU0VUvYiqB5mvu5p+XH9w8RtgnJMOLut/7rd4+fpRBcS52hz65csnHdxQ8clZnyuT3NV40sHRUnfq58mUWFJ70sEn+yiCi+AiuAgugovgIrgILoKL4CK4CC6Ci+D/Q+Djf/higk8Jzs0IMjIGYDGAp0AUeBbiHf3Xs/HGAHyYlYaRX0EYC4txNeIFugvWHyXzua8cnDjYGMBoQIFhRFfLmLjaCxqAw8iuHing/nCwGlLuMrKrveNfnccPFnyLtQ8c0a1jElye8sGFAYwUSCN54Q8GB4ljKKpHkBmLOZbB4FLgjhLVYxNcDFnkMXJUj03m0kOKR0sgYzLHRvlwpcDYI7oaGYvl5HB4ZRrJ1cf9fP5E/5NwQUKM7uoTOI4/ql38kmgUOCMnEHMCL819sag2jJJAxgIs+HNY6PGlpUxXDQWXw5dXjxH8SFZBPf7SyqKrMQLKG7b/OkpmTBJI0BSjbwTGYo6Ni5+ZjMJDj1wkxmQ5iV+VsBh9BzImKbNQFhWjp8wx21c7dKIV9A94IxaJsdplZt9574JQVcUdpr3rzlEHdzLASslpg19EofLMMa3dc0Z9c9YMXT+s7/GCo9FojWWph87+6tmX3XTTzT7XA/F4xutXr4fyOuQZVQUQ0tLphY1nlcn5YqgAuOyyy3inefOtH+36aLJr5Obe3t72o4w68kIsFptuW7pp5d33TPne928hm83yLz+6b9PVb/4niRK9QNfUoquqUaUREEEG+jGd7Zi2Dnpy3qYHGr7OFdcsX2BZFs899ywP/fznu11PLslkMjtHBScSiXrL0m+uXr3mlEWLFrN58+auxD+u2HZWhb0gcvkyShZ/Ax2N+70KPcVvJpMm999NZJ99mi1dzsb3rviLGbNmz6rY0rKFVavubTWG83p6ej4psAbfr66trS03xtlw98p76s+bN5+nnvzFtouevK/s1AnJM+I/vB37j6aDziJeCtxhzUkhTgoYwJpchz3zbJI7fj/pzA829f6iR/bPPW9e9aS6utjbb715YWVl1SOZTMY5DGzb6scXf+OSS6+48kqanntu55+99shkOyLx8uuvIjSuDEzq6Ob5TdzgPJ9GhT2sCbV4W1vK57R+FP9lOrT33PnzKjOZTM2OD7dFB3L5FwaDq6KifGYiXvn95ddey4fbtmWv2fhIiVUqpbpMEao2SH4fiKCMgAbRggSuVkKwEQz22q4iVKtQEYUtJvzdlvX6+bq67PJrr41sbm6+VVv8W1dX7/9oADH6b//0+us1QO/jD6xPhGWSCgsqLJj8PsTdjzj7Ma7fxDkAzn5wjry+H3H2YfL7UGGDCguJEqnPPf3YOoDrrrtOe56+C8CKRqPjotHoN+fMmcObb7zRelsk9W1lC4QFCRlM9yfoKnsoEgOLVWCxDLfYBRwwnXmwDIQVyoMbo6lrfrq5+dCsxsbaaHlkqTFSpUMhvjV79mwLwHvjldewBGxQlqBswXn3Y6T/EDhtiNOGuG2I2444QXPb/WtOGzhtmL7PcN7di7IFFegiJDq3+ZVXAWbMmGlrzRJLKc6/4IJFGGO4MdQ+gxAQEn/2LcH0u+Sa27HO0IRq/V+MSqnBOUZARMAD75DB2w4mq8AKWkggpPiOtJ3dYgznzTuPt996+3xLoc8+vaGBlpaWzFybrygtqCPgeODtcTFtBl1hUBHfGgl+wNGv8FIayWjE6KCfD1UhBVqotPWZO3Zs7506dVpUaT1Lh21rPED7oUNtKH8OUYLSoHTwWRiEAsmBDIA4gCPIAJh8YL3lyw7vi5JAJ7QdamvXWmPbofGW0qEYQL4/0zeYjdTRTQ0Oxp9/Svx9jvKAkBocsCh1dP9AZ76vNwOglI5bnuflAaukPBo9bM8UpMIjvxeiWAUbATHK3/yNJM/h30vKozEAz/Ny2nXddoCKyqrKwc5GDYFMUJmM8peLqyCvkH6FZP1zXP+eGBXIFvQcrquyqroyALdrxGzv7u5i6rTTE3lX0gUL/DIYPPfwFDh+k5xCBhSS1Ui/9s9zQ/cLz0rEGxqEGMWAK92T6yfHu7q6MCLbtSj1UtPzTcTjcfW0E3t5EBSkv0FgPgAMQgtWa/9azpcZHICrhvR48B+52CvRaFS9/PJLKKVe1Mao9e+++zsAtk9rnIwbLBFHIQ5IACWvkJxGBjSSDeDZ4HxAIznty+SV38chGIA/PXumzZoK0PJBCyLq1zqdTn/U2dHxbmtrKxddfmXj1r7QRr9jMH/5Ye4d8OdV+odZ3F+AqyG3F/oFelr62PQnl14667PWVrq6ut5JpVJ7giLBygfWrMYOh3ll/pLx4iojR7p3QMGgpQX4kPUE8OFuF0chrjIvzL78VDsc5sEHH0SLWkmQLuhOp5v27t376tatW7nk8iun/UN8VhM5BblASS5w53BowdXD4L7Lg8EG7Z6SM36z+MILp25p2cqBA/s3dKXTLxRSBeDvtUpL7M0PPfRwYtLken791z9Y++fevmWE/WJBIelbgJbDtz4mePblBksrcPU/ubVrF65Yuayzs50Vt6/ozuXduel0etdhYIB4PH5RVWXy+WeeWR8aV1PDz+6/56W//PDFxbpELGULgwVEcwSYoWXkKExOuatqGl9b8p3vfb2vt5/b/uoWtyfVe0kqlXqpwDpql1lVlbwhUhr52VNPrQ3PPuccNm16PbXrR3f+9pvm0NV+pWEwhQKIqKHnm57iV9nydc6Smxc1zm5MHvj0AHfecUeuv7f/u509PY8N5wyCReRcYCEw6YknHi9bcfvtl9276r7qG2+6Gdd12bhhQ/rghhe3TdmywT4l2zkhEeIUgJTLZ62RygPbT5/rlv/xvLOmnzE9ns/lWb9uPY8++u9tP/3JPzd9e/nyLLAXeE0ptRlAicgk4BZgfDAGc/DgQb1790fWrT+45Zz58xdMue+++0kkk/5N8RO2iPiZ0BiMCMbz8FyXzq4u7l91L5ub3969Zs2/Np/eMM2rrT21YKQBPgPWKBFZAyQA093drTzPobu7uyPV3XNbR2enam5uZu3atdTW1LDsqqtYeMEipk2b5m8GANd12bVzJ69vfI2n1/6Kjo5OvrVsKefOPZeqqkpJJCtXJ5OJinBpRJLxeOF3bI8FZIAYoEN2SHmeJ6GQ2CiMUipUP2UK199wI59+2sp/rVvP6tVryKRTOE4eAcJ2mFg8wfgJE5nZeA4TJ4yntmYcSimUUsaydMi2wxIKKTXM6n4lIuMCV08m2O52dHSQzfbpvkxvZSqTbkinUnWpVDqUzvTS29dHNpvFcfy6aNsWkUgp0fJyYrEYiUTcSybin8RjiZ2lZeXd0WjUra6uDg2L/z3A6uHBNQNYAEwHqvAXTTl4Kp3O9HhOvk+FGMhmHXHdHGLEE8CytNY6rCKRsPY8VRoOh8tisfIkhFxgIAB2AtuA15VS20ZcTsEgEsBM4DTgFKASiAClQAnBig7EC8/8BoAc0AekgE+B/cAWpVTqSMb/AlY1WXIncMcxAAAAAElFTkSuQmCC\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAxNSURBVGiB7Zp7kFTllcB/5/a93dMz3T0PemYIDgoCPhZ5iaD4wNkFjQjRRMlLTNbSlKlyzZpobSVbFRPUbNVWSRCWuKvlxqybtbIrukp4SATZCAgospEBgeElj4EZ5t3d0+++37d/9O2ZnqEHQZzZSlXfqlMz/c253+875zvfOefeHuH8L6u83P+AwH0lJZ4pbrenEiCVSnYmEsndGl4NhSKvAJkLmPPcV0VFxZjKivKPv77wXr274WN9uvm0PnHihD5y9IhuPNioN216Vy+Yf6eurAj8b2Vl5aXnM6d8loLf7w9apvHhyy//29jZ9fW0t7fpdWtWN7Wdao4qpaiqDpbdXF9fV1paKpu3bGbxk08eSWXU9ZFIpOPirC33v7xs+TIdiUT0Pz239NjeaTOTHXXjdb4cuP6W5DOLFx/7aNdH+oknfqQryv0vXZTFfr8/GKyqaN7XeMhc//ba6NSfPFXqS6fESJ29jdGAX69+9KHY9OnTyxbec08mHInWhsPhzsHmNs4FNgxdf+NNN5sAh3/7n40dCxeKedUsOr6x8CzdsnBEQu9sPABwzTWTTMNQ9eec+1x/FDEuGTduHABXtreOKutJYyiFqq4tqD+5O3wJQF1dHSij7nODtdZuj9cLgMfGOpcuQInSFoDldqNFez43eCivIrgILoKL4CK4CC6Ci+AiuAgugovgIrgILoKL4CK4CC6Ci+A/B7B5vor6Mz4PNnbRYAAtoCQLUMMFVobuBWOALWdjVIGxiwbbZC3WkrXWLqAzJBZrR5T0LWTgdSHfdF1YcIlG57t8oM5nfov1OcCKPmDW1Rfi2IsA5yI5F9WFXF0o0i8arARwggsBu4BbhwaM6g0ujXY+9b+GLqrzLR5E5wsH2ziB5QRXoW8lCy3mosH553iwlDlEe9znai2DpMyhAJ+PxUNTJMhZm51+WM9xvsWFXD2kx0nl9rjQ4oYC3C+4BoEMnasl39Vn6wxRdcqbXApXpwupWBcEVgLKGLw6DU1w5bkaCjcChcYuHozuLYtqEFfroXC1TZ67GcbjlEuZWjSIHr6ozjZ7/y/VSWOLdgJIF9zjQl3JFwDOXn1lsYDOULm6X+YaROcLB6s8+LC2tzqvoc+Wx0L2nT/6wlIm5y6LQ9bs5TLXsO5x7jG192lxuJq9bCOg0aIRGcYEkt9lCsPp6lxlMsBlFE4ghcYuGoxznHKFYNjKYq7Zy5XFYW32lMtCBGzbLlwWLwB83m/2NNC44R0iFaP503+8jO1UqHz5wiwW0aNzvysgdPJTQr/7dFD9fHD+vecN9vl8NaYpv546ZeqCBx98CMhGbPXEqZRfcTWmyySTjuO2TMora/B4Sji+832OnWoGYMGCBez88IMfHD50eExG6Yd6enraBjJcAwf8fv+Vbsv1Pz9f/NT1y1esQCnNPz6zeGuy6WBN+MRRrwp1YMR6MOIJMqEuOj49xNFd2zh5aD9SVpr44PCJXVOmXXvpHfPm4fP7rtz98Z/usSz3+lQq1e/fnvuFSHl5+VjTNLb96lfPj6yv/0t2bN/eufJnj+37Uql1c/1Xv8WM279CaZn/rJcBGoj1hNm+7k22rF5JcyK1edp3Hps0bfq0yj0Ne/jFL55pVopZ3d3dx88C19bWlqVS8Z2Lf/7U1XNvu51Vb72x7/irz9fUBEcEv/03PyFYPRJDgZHt9XpvzG8QlAFnWppY+S9LaOnsaPPOWdhxx7z5V320cydLl/7yE2+pb+bp06dj/VxtWbJ03h13zr/r7rtZu2bNwVP/9cKYMiHwtW8+QNAbwOiOIN09SCiChCKQL+EIKhxBhcN4EGpGjuJww66yxNH9gePac+zGm26sikQiNY379/kSydT63uCqrCybXB6oeuS+RYvYv29f/OTKFz1+dIlXXFQrCznRjNhkRfdJzmIMEAExsqbUmh68holWGXf43deMg6NHJ+5btKjkgw8//IFh8lJnZ88nBoBWxpPf+e53DYC1Ly5bVSb6Mo8WSrQgx5uRY6cHSDMcz0q/vx/PSTNeJXi04EOPfe93L70JcP/99xu2bfwUwPT5fNU+n++rM2fO5P3332+uS3V9y9KCG8FSmtjRo3iN0uz+qqylemDnLhpDQDsFJGrHMG2F2xAyGi5Nhr65Y8f21unTZ9T4yrz3KqVHGC4X91x33XUmwN7N775nApbuk90nD5BpbUbaWqG9Dd3eju5o6y/t7dDehrS1kmltYffJ/ViA25nDBcbeLZs2AUyaNNkyDL5minDL7Nm3opSiNtQ0yUQwESydlXg6xc70Sf5CewliYSD9TqHu/anpIMUnJIiLjSVCGjAFTA21odNTlFLMunEWO7bvuMUUjKkTrriCvXv3RDyiJxpacGVXSc56W2uO6DhtKkmFFsocHchmtKhoukURNrJPG5YDdAEuDYaAV/TVjY0HesaNG+8Tw5hmuC1zFEBLS0urkQ3QPtFgILgQTC0IkAZSgEJQCClnTBwdF4KBOPf2iQBnzrS2GYaBZblGmWK4/ADxWCzqoS85iDOZDFiMS2ddV5Kz2EkGhgwECYLOzqOzxy0W7YkAiBgBw7btFIC3tMw/2JsrnS9OI5B2pPdt0AC9gdVZZxkBANu2k0Ymk2kDCI6oqsw1c/nNu8rVW8l+2ZFCkxRNzMhKUjQpNBlnv23nXfbAeTRQHayudMBtBlod6OrqZNz4CeVprcKqd4KsZBxgGk1KNEmBmGiijsScsZRo0s4CMnn3284CMqJCY8aOCXR2dqK0PmBokQ3r1q7D7/dLq7tyY8axMCOatDNZFqhJiCbuWNsLNrJjCUcnt4C0ZOew0WTQnDYr3/X5fLJx4wZE5B1DKVm1a9dHAIyYesPYjEBa+vYwJZAUSAgkHAtjookaWcl9Togm4eim8u5PS9YDNVNmXg7QsLsBreX3RjgcPtzW1rarubmZ+QvumtahXJvzrUzmWRvrZ61yxNnvPKuTA6xvt13bvjxv/tSW5mY6Ozt3hkKhoy4Ar6ek6dChg4vm3nY7oZJAJnG4oUIQESdD5Ud0v30XSBlZC1OGdjyTA/darwK3LcxcPm585ZJnl9ATinwvnkweNgC6wuF1x44d27R3714WfOWucZGrb3g7kee+eJ6LewPLcXU0bzwuuf2G3P3NoyevnzP3tsv3NOylqenkHzvD4fWQ197aikeW/nJJd1dnJ4//9On57V+a8Hoib7K4kQeUAWL0D7RcsJ2oqHv9wUcfu7Orq5MVK5Z3KS0P53j96lsgEPjyiKqKtW/891uu2tpalvzDMxsTW96s9yhMC8HUOCkxm07JO/fZk5A9dkmDTOSqWe/99fcfmRPtifHY3z6a6Q5F7gyFQhsKggFGjKh4wFviffG11153T59xHVu3bg3968/+7g9V3ae+0Zv0kX49l3ISjA2ccpe/NXvR9+uvnX5tRdOpJv7+xz9OxnpiD3d0d/97PqcXrLWeBcwGLnv11d96n3j88QVPPf108KHvPUwmk+HttWu71q96Y0dozzajJBUfXyqMA4gpfShmeY54JkzX19/6VzfMmDmjMpPOsOqtVbzyym9alz23fM23Fy1KACeAP4rIBwCitb4MeAQY5SxEt7a2qIaGBn70wx+OTKXTc5Y+t8w1d85cdN5KtdbYSqGVImPbJOIxotEo6/+wniXPPmsH/L4Ny5etaJk46Rqprq7JPTgooBn4Z9FaPw9UAHR1dSnbTsuZMy1GMpnItLZ2GFu3bq5d/fvVc0ZUjZB7F36d2fW3MmHCFZguF0pr0uk0Bxsb2bL5PV5fuZLuUEjfdffdG2+66ebW6mCVLvP5qa4OAoYEg8Gcg7tNIAIEADHdJnbcxmNZ6UQ05nK7TT1x4sRYRVV1/FTTqdLVa9bywgsvEImESKfSAFiWhT9QzqhL6rh25g3UjbokPnJkTaKkxFRaa8NtGbaIy+Up8eS2VgEx0VpXO66+HKfdbW9vV93d7RKNJl3xeNQOd4d1Mp0i3B3yRCKRsmgiYSVTaa9orS23lfR5vany8vKYLxCIeyxLKqoqtddbKh6PSVVVtQ4Gg5IHPQI8nx9ck4CbgSuBarJnvARsiUai4XBPmGQyqbWGRCxh2VrZAKYYLtNjZUyXSxsuU6oqyg1fwO91nhUSzvQdwB5gm4h8UvA4OYsoByYDY4EaoBLwAN7sYiDvZ4LsqUo60uNIK3AY2CMioYGM/wPREY0iGUY58wAAAABJRU5ErkJggg==\"],\"useMarkerImageFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', amount = percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"markerImageFunction\":\"var type = dsData[dsIndex]['type'];\\nif (type == 'thermomether') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"color\":\"#fe7569\",\"mapImageUrl\":\"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiCiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiCiAgIHhtbG5zOmlua3NjYXBlPSJodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy9uYW1lc3BhY2VzL2lua3NjYXBlIgogICB3aWR0aD0iMTEzNC41MTgzIgogICBoZWlnaHQ9Ijc2Mi43ODI0MSIKICAgaWQ9InN2ZzIiCiAgIHZlcnNpb249IjEuMSIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMC40OC41IHIxMDA0MCIKICAgc29kaXBvZGk6ZG9jbmFtZT0id2ljaGl0YW1hcC1ub2xpYi5zdmciPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM0IiAvPgogIDxzb2RpcG9kaTpuYW1lZHZpZXcKICAgICBpZD0iYmFzZSIKICAgICBwYWdlY29sb3I9IiNmZmZmZmYiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgYm9yZGVyb3BhY2l0eT0iMS4wIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTp6b29tPSIwLjM1IgogICAgIGlua3NjYXBlOmN4PSI4OS45MDc4NTciCiAgICAgaW5rc2NhcGU6Y3k9IjQ1My43ODI0MSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0icHgiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ibGF5ZXIxIgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEzNjYiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iNzIxIgogICAgIGlua3NjYXBlOndpbmRvdy14PSItNCIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iLTQiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgICBpbmtzY2FwZTpvYmplY3QtcGF0aHM9InRydWUiCiAgICAgaW5rc2NhcGU6c25hcC1nbG9iYWw9ImZhbHNlIgogICAgIHNob3dndWlkZXM9InRydWUiCiAgICAgaW5rc2NhcGU6Z3VpZGUtYmJveD0idHJ1ZSIKICAgICBmaXQtbWFyZ2luLXRvcD0iMCIKICAgICBmaXQtbWFyZ2luLWxlZnQ9IjAiCiAgICAgZml0LW1hcmdpbi1yaWdodD0iMCIKICAgICBmaXQtbWFyZ2luLWJvdHRvbT0iMCIgLz4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE3Ij4KICAgIDxyZGY6UkRGPgogICAgICA8Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+CiAgICAgICAgPGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+CiAgICAgICAgPGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPgogICAgICAgIDxkYzp0aXRsZT48L2RjOnRpdGxlPgogICAgICA8L2NjOldvcms+CiAgICA8L3JkZjpSREY+CiAgPC9tZXRhZGF0YT4KICA8ZwogICAgIGlua3NjYXBlOmxhYmVsPSJMYXllciAxIgogICAgIGlua3NjYXBlOmdyb3VwbW9kZT0ibGF5ZXIiCiAgICAgaWQ9ImxheWVyMSIKICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjcuMDcxNDI4LC0zMDcuOTAyOTkpIj4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDM3ODciCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMzY0ZTU5O3N0cm9rZS13aWR0aDoyLjk5OTk5OTc2O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmUiCiAgICAgICBkPSJtIDkwNi4wMzMxNSw3MDYuMTMzNjcgMy40MjkyLDE3Ljc5NTUyIE0gMjguNTcxNDI4LDc2NS4wNTA2NyBjIDE1MC40MzUyMDIsNi44MzM0MiAxNDYuMzkyMzIyLC0yNi4zMzQxNSAxNjYuNDM0NTQyLC0yOS4zMjAwOSAzNi4xNDM3NSwtNS4zODQ3NiAxMTQuMjg2NzYsLTYuNTI1NCAxNDguMzI1MDgsLTguNjIzNTQgNDMuMzc4MDgsLTIuNjczODUgMTQxLjc2MjIxLC0xMS4yMzA5OSAxODguODU1NzgsLTE5LjgzNDE4IDM5LjgxMTM4LC03LjI3Mjg0IDIyMS4zNjk5MSwtMC44NjIzNSAzMTkuMDcxNDEsLTAuODYyMzUgNzAuODI3MzUsMCAxNDYuOTE4NjcsLTEuNzI0NyAyMTguMTc1ODYsLTEuNzI0NyAtMzEuNjE5NywwIDExNy44NTUyLC0yLjU4NzA3IDg2LjIzNTUsLTIuNTg3MDcgbSAtMjUuMDkwNywtNjguMTI2MDYgYyAtNTIuNzk5NiwzNC43ODQ4NCAtNjUuODk1MSw1MS43NDg2NSAtOTUuNjM5LDgxLjQ5MjU4IC0yNC45MzEzLDI0LjkzMTI3IC0xNDAuMzk2NTMsLTE5LjEzOTIgLTE3OC45Mzg3MSwzNi42NTAwNyAtMTIuMjgxNCwxNy43NzcxNSAtNDcuMDAyNTcsNDYuNTQ2NTMgLTY1LjEwNzgzLDU5LjA3MTMzIC0yMC4xMDUsMTMuOTA4MTggLTU2LjAzNjcyLDQ0Ljk1NjY0IC02Ny43Njg4NSw3My4wNzgyNyAtNC44MDE0NywxMS41MDkwMiAtMTMuMzgwNDYsMzUuOTkyOTggLTIzLjQ0OTQ5LDQ2LjA2MjAxIC0xMC40OTY5OSwxMC40OTY5OSAtMzguMzc3MzMsNi4zODU2OSAtNDQuMDIzNDUsMTcuNjQ3NjQgLTE5LjAwNTAyLDM3LjkwODEyIC0yNS40NjUzLDEwMC45MjM1MiAtNjcuNjE3ODksMTAyLjA1MTAyIG0gMTkuMjgxNTEsLTYyNC4wMTQ2NCBjIDM0LjY1OTM0LC0xLjg3MzgyIDg0LjAyNzMzLDcuMzkxMzEgMTA5LjkwMDcxLC00LjI4NTQ1IDEzLjI4MTcyLC01Ljk5NDA4IDQxLjQwNzIxLC0yLjQ2MTM1IDY2LjgyODY2LC0yLjMyMDQ2IDM1LjMyMjM4LDAuMTk1NzggNjQuMzgyNDksMC42MzQ3NyAxMDEuOTE2Nyw1LjAyMzIgMjUuMDMwMzYsMi45MjY1IDQ0LjY2MjczLDM0LjI4NzIyIDU4LjUyNjk4LDUwLjY0MzkgMTcuMDk4NzgsMjAuMTcyNjggNjIuNzYzODYsLTEuNzE0NjcgNjYuMzA1NjYsMzIuMTM0MzMgNS4xMDI3LDQ4Ljc2NTg3IC02LjMyODQsNzguNjM3MjUgNi4xNDExLDk3LjM0MTUgMTkuOTY5MiwyOS45NTM3OSA1MC40ODY0LDE3Ljg1NTc5IDQ0LjYxOTMsODMuOTcxMTkgTSA1ODkuMTAyMjcsMzA5LjcyNzE1IGMgNC42NDM0NiwyMy43MjkyMyAxNS4wNjkwNCw3Mi43NzU3NSAxOS4wNjEyOCwxMzAuNjQyODggMC44NzIwNiwxMi42NDA0OCA1LjQ0NzE4LDI0Ljk5MjUzIDQuMjIyMzEsNDUuMjc3NTcgLTIuNTE3MjEsNDEuNjg3NSAtMTUuNzE3MDYsNDMuNjc3MjcgLTE1LjA5MTIyLDYwLjM2NDg2IDEuNDMxOTUsMzguMTgyMjQgMzAuNjEzNjEsOTMuODM3MTkgMzAuNjEzNjEsMTM5LjcwMTU0IDAsMjQuMTgwOCAtMi42Njk2NCwxMTUuMzkwNDUgNy4zMzAwMSwxMzUuMzg5NzYgMC4xNTkxMSwwLjMxODIxIDEwLjA2NDc2LDM1Ljg4MzMyIDEwLjc3OTQ1LDQ5LjE1NDI0IDAuOTQzNzgsMTcuNTI0NjkgLTI0LjQ3OCwzOS40NzAwOCAtMjguMDI2NTUsNDYuNTY3MTYgLTUuNDc3NywxMC45NTUzOSAtMzYuOTczMjQsMTAuODgxOTcgLTQwLjA5OTUsMjQuMTQ1OTUgLTMuODY4ODQsMTYuNDE0NTEgLTMuODY2Myw0My43OTczNSA0LjA0NjQ3LDU5LjQ0MTI5IG0gOTcuMzM3MzQsLTY5MS4wMDk0MSBjIC01LjAxMzMyLDM1LjUxNTk1IC00My42NTkwMSwxMS4zMTY1MiAtNTguNTM4NjEsMjMuNzgxMzEgLTIxLjMzMDE5LDE3Ljg2ODUyIC02Mi40OTk2NCwzMS40MzIxMiAtNzAuMTI0MzcsMzUuMzY3MDggLTM1LjA4NzYzLDE4LjEwNzkzIC0xMTAuNDcyMTUsLTE1LjE0MTk2IC0xMjUuNjE0MSw0LjI2ODQzIC0xNS45NTA2MywyMC40NDcwMyAtMC4wNzM1LDYxLjQ2NjQ4IC05LjE0NjY2LDg0LjE0OTI0IC02LjAzNTcsMTUuMDg5MjYgLTE4Ljg3NjcsMjMuMDE3MzQgLTI3LjQzOTk3LDMyLjkyNzk4IC0xOS43NDgyOSwyMi44NTU1NSAtNjkuOTc0MjgsNjkuODI0MTkgLTg0Ljc1OTA0LDEwMC4wMDM0NiAtNy40OTc0MSwxNS4zMDQwNCAtMy4yODQyNiw0NC40MjA0MSAtMy40NzA1Myw2My4zNDI4NCAtMC4xMjc5MywxMi45OTQxNCAtMC44MTAxNSwyMy4xMDM4NSAyLjQwMzQzLDI4LjI3NjE4IDQuOTYxNTgsNy45ODU4MSAyMy43MjA1LDI4LjExMjA3IDI0LjIzODY1LDUwLjYxMTQ5IDAuMjk0MTEsMTIuNzcxNDYgMC4wMTMzLDc4LjU5MTAxIDMuMDQ4ODgsODcuNjU1NDkgMi4zMTI1Niw2LjkwNTQ2IDQuMjIwMDQsMjYuNTY0OTcgMTAuMjEzNzcsMzYuNTg2NjIgMTEuMzU0MDEsMTguOTg0MTUgNC4zODczNyw0MC4xNTY2MiAyNy44OTczLDUzLjUwNzk1IDE5LjA1MDEyLDEwLjgxODU5IDQ2Ljg3NzgxLDEyLjIxODYyIDgxLjkyNjE4LDE0LjQ2MDU0IDMzLjcwMzQ1LDIuMTU1ODkgNjEuNTEyMTcsLTEuNDMwMzUgNzYuOTIwNzcsNi4xNDExIDExLjU4NTA4LDUuNjkyNjYgOC41ODE1MSwxNy45MzM0NCAxNC4yOTU0MSwyOS4zNjEyMyA1LjY0MDQyLDExLjI4MDg1IDMxLjUwMjYzLDExLjE1NjI3IDQxLjgwNDA5LDQzLjQ1NDg3IDcuNjA1OSwyMy44NDcxIDMuMDg1OTMsNDQuMTU2OSA2LjcwNzU1LDY1Ljg4NjYiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjY2Nzc3NzY2Njc3Nzc3NzY2Nzc3Nzc3NjY3Nzc3Njc3NzY2Nzc3Nzc3Nzc3Nzc3Nzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Im0gNDMuMjc3ODgxLDUxNy45NDY3OSBjIDAsMCAyMzAuODQ4Mjg5LC0zLjYzODA1IDI1MC4wMDg2MzksLTMuNjU4NjcgNy40ODIyMiwtMC4wMDggOC42MTk1NCw1LjE1MTk0IDE0LjAyMDksMTEuNDU4NjkgMjQuNTk2MDgsMjguNzE4OTMgOTMuOTA5NjYsMTEyLjkzNTg1IDkzLjkwOTY2LDExMi45MzU4NSIKICAgICAgIGlkPSJwYXRoMzc4OSIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJtIDM1Ljk2MDU1NSw1NzcuNzA0OTQgYyAwLDAgMTY1LjUyNDU2NSwtMS42ODQ1NCAyNDguNzc5NTY1LC0xLjY4NDU0IDQuOTQ3NDksMCA3LjcyOTkzLC0yLjg4MzMgMTAuNTM3NzEsLTUuNzI5NzcgOS42NjEwNywtOS43OTQxNiAyNS42MzE5OSwtMjguNTg5OTUgMjUuNjMxOTksLTI4LjU4OTk1IgogICAgICAgaWQ9InBhdGgzNzkxIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOiMzMzMzNjY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gMzguMzk5NjYzLDY0MS43MzE1NSA0MzEuNzA1OTMsNjM3LjQ2MzExIgogICAgICAgaWQ9InBhdGgzNzk1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOiMzMzMzNjY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gMzkuMDA5NDQyLDcwNC41Mzg1OSA1MjMuMTcyNTMsNjk3LjgzMTA0IgogICAgICAgaWQ9InBhdGgzNzk3IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzAzLjk1NzYyLDY4Mi41ODY2MSAxNDYuNzk1NDIsMS44MjkzMyBjIDEwLjUzNDAzLDAuMTMxMjcgMTQuMzQzNzQsLTIuNjM3MzkgMjUuNDg3MTUsLTYuMzcyOCAxMC40MTIxMiwtMy40OTAyNyAzMS40MjQxNSwtMi42OTg5NiA0MS4zODUzOCwtMi43NzM4NSBsIDQwNS41NjA3OSwtMy4wNDg5IgogICAgICAgaWQ9InBhdGgzNzk5IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgaWQ9InBhdGgzODA0IgogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDQyNi4yMTc5NCwzMTQuODkwOTggYyAyLjA2NzU0LDkuMDUyNzMgMS44NDE3Nyw1MS43Mjc3NyA2LjUwNzk0LDc0LjgzNDY2IDEuNjc0NzUsOC4yOTMzNiA4LjY3NTA4LDE0LjA2NTk4IDEwLjA1NTQxLDE0Ljg1ODYyIDQuOTAxNDcsMi44MTQ2MyAxMC44MTQ3OSw4LjE0OTgyIDEzLjA0NTc5LDE2LjA4ODMxIDYuNzU3NzksMjQuMDQ1OTEgMC44Nzk3Miw2OC40NTIxMiAwLjg3OTcyLDExMC42ODkzIDAsNi4wOTc4MiAxLjY2MDEsMzAuMTQ2NiAtMi4xNTU4OCwzMy45NjI1OSAtMi41NDA4NSwyLjU0MDgzIC0wLjI4MTYzLDEyLjk5MDY5IC0zLjQzNjc1LDE2LjE0Mzc3IGwgLTkuODQ5NDQsOS44NDMxMSBjIC0xMC4zNjcxNSwxMC4zNjA0NyAtMTEuNTkwMTcsNi41MjYxNCAtMTcuNzM4NDgsMTguODIyNzYgLTMuNTY3NzIsNy4xMzU0MyA1LjQwMjM1LDIwLjY3MjEgNy4zNTQzMiwyNC41NzYwMiAxLjkzMjE0LDMuODY0MyAtMS44NDIxNiw0Ljc3NzczIC0xLjc5MjM1LDcuNDQ2MjYgMC4yNTI4NiwxMy41NDQ4MyAyLjI5NzUsMzczLjkyNzEyIDIuMjk3NSwzNzMuOTI3MTIiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDM2NS4yNDAyMiw1MTkuNzc2MTIgNC4xMTU5OSw1MDIuMTUxNTgiCiAgICAgICBpZD0icGF0aDM4MDYiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAxMTYuNTMxNjUsNTA0LjE4Njk5IDMuODgwNTksMzEwLjk2NDM2IgogICAgICAgaWQ9InBhdGgzODMxIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDM4ODkiCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzE3LjY3NzYsNTc2LjQ4NTM5IDEzMC4xODc0MiwxLjUyNDQ0IGMgNC41MTA3OSwzLjI0MTY5IDIwLjM0NDcxLDcuOTY4NTMgMjcuNzQ0ODYsNC4yNjg0NCAzLjE1NTQ2LC0xLjU3NzcyIDkuNDE5LC01LjM4ODE3IDE0LjAyNDg5LC0zLjk2MzU1IDQuMjY2OTgsMS4zMTk4MSA2LjAxNjg5LDMuMTE2MzIgMTAuMzY2MjEsMy4wNDg4OSAxMC4zMDQwMywtMC4xNTk3NSAyMC4yMTE3LDAuMzg3NDEgMzAuNDg4ODYsMC4zMDQ4OSAxNzcuODkwOCwtMS40MjgyNyAzNTYuNTkwMzUsLTIuMTMyNDcgNTM0Ljc3NDU2LC0zLjA0ODg4IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2Nzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDc1LjMwNTAxLDU4Mi44ODgwNSBjIC0zLjQ0NDE4LDExLjM1MDY2IC0yLjEwMzQzLDEyLjQzMzczIDMuNjU4NjUsMjEuMDM3MzEgMy43OTQ0NSw1LjY2NTY0IDUwLjg2MjYxLDEzLjAzODQ1IDQxLjQ2NDg1LDI3LjEzNTA5IC0xMC41MzY5NywxNS44MDU0NyAtMjIuODk3NDUsLTUuNDc3NzIgLTMzLjg0MjYzLC0xLjgyOTMzIC01LjQ1MjM2LDEuODE3NDUgLTcuMzQ5MDEsNS40NTYzMSAtMy42NTg2Niw5LjE0NjY1IDIuODA2ODMsMi44MDY4NCA0LjA0OCwxLjgwMzk2IDYuNTIwMzQsNS4xMDA0MSIKICAgICAgIGlkPSJwYXRoMzkxMCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDMyLjAxMDgyLDYzNi44NTMzMyBjIDguMzE4OTksMTMuMTEwMTYgMTguODQ2MjEsMTQuNjM0NjUgMzUuNjcxOTYsMTQuNjM0NjUgMi45Mzg2NSwwIDcuODY5OTgsLTAuOTMzNzEgMTAuNjcxMTEsMCAxMS4zNTkxNywzLjc4NjM5IDI3LjE5Mzk4LDEwLjI3NTc3IDM2LjIwMTkzLDIxLjEyOTQ4IDguMjgwMDIsOS45NzY2MSAxMC4yNTI3OCwyMy44ODMwOCA3LjcwMjAyLDM3LjEwNDI0IC02LjE2OTg5LDMxLjk3OTk4IC0xNi43MTQzMSw1Ni45ODg1MyAtMTkuMDQzNTUsODYuNTY5MDUgLTEuMzQ3OTgsMTcuMTE4OCA0LjUwOTU3LDIyLjUzNTIyIDExLjA3MTQzLDMzLjkyODU3IDEwLjY3MDIzLDE4LjUyNjcyIDguNzI0NTMsMTQuMTk5NTUgOC41NzE0MywzNC4yODU3MiAtMC4xMzk2MywxOC4zMTk0NCAwLDYwLjI2Mzg1IDAsODAuNzE0MjkiCiAgICAgICBpZD0icGF0aDM5MTIiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDUyOC41MDgwNiw2NTguOTU3NzYgYyAtMTAuNjgxMjMsMC45MDQ1NCAtNy4xMDgwNCwtNS42MDI1NSAtMTAuODIzNTQsLTguMDc5NTYgLTQuNzg0NTQsLTMuMTg5NjkgLTEyLjIyNzA0LC0xLjI1MTA0IC0xNi43Njg4OCwtNS43OTI4OCAtMC42NjYxMiwtMC42NjYxMiAtOC44MDk2OSwtNC4xMDg3NyAtMTAuMTc0NDcsLTIuNzQzOTkgLTguMzY0NTksOC4zNjQ1OSAtMy4wNDg4OCwyMC41NTE4OCAtMy4wNDg4OCwzMy41Mzc3NCBsIDMuMDIyLDMzOS42OTc0MyIKICAgICAgIGlkPSJwYXRoMzkxNCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA1MTcuOTg5NDEsNjUxLjAzMDY1IGMgLTAuMjIxNzEsLTIuNzAxODQgMS45MDM0NiwtNS41NjIxMyAzLjM1Mzc3LC03LjAxMjQ1IDEuNzk5NDMsLTEuNzk5NDIgNi45MjI5NCwxLjAwNDE5IDguODQxNzgsLTAuOTE0NjYgMC4yODc2NSwtMC4yODc2NiAwLjg0MzI5LC0xMS4xNjQxIDAuMjI4NjYsLTEzLjU2NzUzIC0yLjA2NDgzLC04LjA3NDE2IC0yLjA1ODAxLC0yOC42NTY1OCAtMi4wNTgwMSwtMzguNzIwODYgbCAwLC03My4xNzMyNiIKICAgICAgIGlkPSJwYXRoMzkxNiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNTI4LjY2MDUsNjc1LjQyMTczIC0wLjQ1NzMzLC0zMS41NTU5NiIKICAgICAgIGlkPSJwYXRoMzk3NCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDc2Ni4zMTYyNSw1NzkuNjQ0MzEgMC40MzExOCwxMy43OTc2OCBjIDMuMTM2NDMsNC42NjkxNSAzLjAxODI0LDkuNjAwNjggMy4wMTgyNCwxNi4zODQ3NSBsIDAsMTU3LjM3OTgxIgogICAgICAgaWQ9InBhdGgzOTgyIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMTEyMi45MDAxLDc2NS45MTMwMyBjIC0yMDIuMzA2NjksNC42OTA1IC00MDMuNzQ0MDUsLTEuMTEzODEgLTYwNS45NTQ1NCwzLjM1MzkgLTEwLjg2MzYyLDAuMjQwMDIgLTMuMzYxNDcsLTguNTg2MyAtMjguNTM2OCwtOC41ODYzIgogICAgICAgaWQ9InBhdGgzOTg0IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA4NjAuMDA4MDUsNzM3LjA2NjUxIGMgMCwwIC05Ny40NDc1LDAuODU4MDYgLTE0Ny41Njg5MiwwLjg1ODA2IC01LjI2ODYxLDAgLTQuNTE1NDYsLTguMzI5ODYgLTcuMzAwODksLTguMzI5ODYgLTMuOTc0MzUsMCAtOC42MjkyNSwwLjAyMDEgLTEwLjUwOTQ4LDAuMDM1OSAtMi4zMzQ3NywwLjAxOTcgLTEuODEwOTQsOC4zNjU5NyAtNC4xNDU4LDguMzY2OTIgLTQ2LjE2ODk5LDAuMDE4OCAtMTY3LjQwNzY3LC0xLjMwNzk5IC0xNzUuMDUyNjMsLTEuMzA3OTkgLTQuNDI5NTUsMCAtOC41NzYyNywtNi40Mzk3MiAtMTMuMTMxOTgsLTYuNDM5NzIgLTEuMzYxMTUsMCAtNi4yMzg3MywwIC0xNC4zOTQ2NywwIgogICAgICAgaWQ9InBhdGgzOTg2IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJNIDY3NS4wMDcwMyw4MzEuMTc0MDIgNjc0LjM5NzI1LDMwOS40MDI5OSIKICAgICAgIGlkPSJwYXRoMzk4OCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDc5OS40MDE1NywzMTMuMDYxNjUgMS4yMTk1NSw0OTUuODY2NTMiCiAgICAgICBpZD0icGF0aDM5OTAiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA3MzYuNTk0NTIsMzEyLjQ1MTg4IC0xLjIxOTU1LDcxNi40ODgyMiIKICAgICAgIGlkPSJwYXRoMzk5MiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDUzMC4wMzA5NCw2NDMuNDU4NTkgMzkyLjM3MTU5LC0zLjAxODI1IgogICAgICAgaWQ9InBhdGg0MDQ4IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gODU5LjQ1MDYsMzE0LjkwMTI4IDEuMjkzNTQsNTA3Ljk4MDU4IgogICAgICAgaWQ9InBhdGg0MDUwIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjAuOTk5OTk5OTRweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gOTIxLjU0MDE3LDMxMC41ODk0OSAxLjcyNDcxLDUzMS43NTIyNyIKICAgICAgIGlkPSJwYXRoNDA1MiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDczNi4yODk2Myw0NTMuMzEwNCAxODUuNjc3MTUsLTAuMzA0ODkiCiAgICAgICBpZD0icGF0aDQxODciCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAxMDYwLjgxMDUsNTE0Ljk2NzY3IGMgMCwwIC0zNjMuMjgxMjYsLTUuNjI2MTggLTU0NC42NTA0MiwyLjUyMTc4IC00LjE3Nzc2LDAuMTg3NjkgLTEyLjUwMDQ0LDEuMDY3MTEgLTEyLjUwMDQ0LDEuMDY3MTEgLTEuNTcwOTUsMC4xMzQxIC0yLjAwMDkzLC0yLjMyNDk1IC0yLjU5MTU1LC0zLjUwNjIzIC0wLjA5NjcsLTAuMTkzNDMgLTcuMDYwODEsLTEuOTMzNCAtNy42MjIyMSwtMS4zNzE5OSAtMi44OTMxNCwyLjg5MzE0IC03LjYzMTY3LDQuMjQ4NjkgLTEyLjE5NTU1LDQuMTE2IEwgMzY5LjIwMTcsNTE0LjUzNjUiCiAgICAgICBpZD0icGF0aDQyNjEiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAzOTkuODE1MzEsNDc5LjYxMTEyIDExLjY0MTgsNS42MDUzIGMgMi45ODQxMiwxLjQzNjc5IDYuNTI4NzgsLTAuNDc3MTIgOS45MTcwOCwtMC40MzExOCBsIDEyNy4xOTczOSwxLjcyNDcxIgogICAgICAgaWQ9InBhdGg0MjYzIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gNTE5LjI1MTUxLDUxNy4xMjM1NyA1MTguODIwMzIsMzA4LjQzMzYyIgogICAgICAgaWQ9InBhdGg0MjY1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDMyLjkyNTQ5LDM4OS43MTQ5OCBjIDExLjA0NDk2LDAgMzUuNTMzMDcsMC42MTkyNyA0Mi41Nzk3OCwtMS4wMDM5NyA4LjQwNTIyLC0xLjkzNjE4IDcuMDY2LC02Ljk1Mzc4IDE0LjE5NzEyLC02Ljk1Mzc4IDcuODA5NSwwIDYuNTQyOTEsOC4wNjIzNyAyMC4xNDE3LDguMDYyMzcgMTMuOTkwNjgsMCA0NC45NzY4OSwwLjM3ODg2IDYzLjkzOTkyLDAuMzc4ODYgMTIuMDgzOTUsMCA4Mi4wMDI2NiwwLjMwNDg5IDkzLjYwMDgxLDAuMzA0ODkgOC43NjA0NywwIDEzLjE1OTcsLTIuMjg4MjcgMjEuMzQyMTksLTcuMDEyNDMgNy4xOTUxNSwtNC4xNTQxMyAyLjA1NDU5LC05LjQ5MTM3IDIwLjQyNzU0LC04Ljg0MTc3IDIzLjE0NTQsMC44MTgzMyAxMi42NDMzNCwxNC4wMjQ4NyAzMi4zMTgxOSwxNC4wMjQ4NyAyNS4zNTk1NCwwIDEzMC45OTkwMiwwIDE1MC45MTk4NSwwIDE0LjMzMjQ0LDAgLTQuMTE5MTEsLTEzLjExMDIxIDI5LjI2OTMsLTEzLjQxNTEiCiAgICAgICBpZD0icGF0aDQyNjkiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc3NzYyIgLz4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjU4OC42Nzk1NyIKICAgICAgIHk9IjczNS44MDQ2MyIKICAgICAgIGlkPSJ0ZXh0NDMxMCIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMiIKICAgICAgICAgeD0iNTg4LjY3OTU3IgogICAgICAgICB5PSI3MzUuODA0NjMiPkxpbmNvbG48L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjY4Ni4zOTg1IgogICAgICAgeT0iNzY1LjYyODQyIgogICAgICAgaWQ9InRleHQ0MzEwLTciCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNiIKICAgICAgICAgeD0iNjg2LjM5ODUiCiAgICAgICAgIHk9Ijc2NS42Mjg0MiI+SGFycnk8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjcwOS44NzE4MyIKICAgICAgIHk9Ii04MDIuMzc3MzgiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMi02LTgiCiAgICAgICAgIHg9IjcwOS44NzE4MyIKICAgICAgICAgeT0iLTgwMi4zNzczOCI+V29vZGxhd248L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjU2Mi4xMTkyNiIKICAgICAgIHk9Ii03NzEuOTY4MTQiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xLTkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtOC0yIgogICAgICAgICB4PSI1NjIuMTE5MjYiCiAgICAgICAgIHk9Ii03NzEuOTY4MTQiPkVkZ2Vtb29yPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTguMzA0ODciCiAgICAgICB5PSItNzM4LjM2NjQ2IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTciCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtOC0yLTkiCiAgICAgICAgIHg9IjU5OC4zMDQ4NyIKICAgICAgICAgeT0iLTczOC4zNjY0NiI+T2xpdmVyPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTIuMTIyODYiCiAgICAgICB5PSItNjc3LjIwMzk4IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTctNSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNi04LTItOS00IgogICAgICAgICB4PSI1OTIuMTIyODYiCiAgICAgICAgIHk9Ii02NzcuMjAzOTgiPkhpbGxzaWRlPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTcuMzI3MDkiCiAgICAgICB5PSItODYyLjYxNDA3IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTctNS0zIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMi02LTgtMi05LTQtMSIKICAgICAgICAgeD0iNTk3LjMyNzA5IgogICAgICAgICB5PSItODYyLjYxNDA3Ij5Sb2NrPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1ODcuMzcwMTgiCiAgICAgICB5PSItOTI2LjEzNjYiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xLTktNy01LTMtMiIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNi04LTItOS00LTEtMyIKICAgICAgICAgeD0iNTg3LjM3MDE4IgogICAgICAgICB5PSItOTI2LjEzNjYiPldlYmI8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9Ijg3MS4xNjEwMSIKICAgICAgIHk9IjYzNy41NzUyIgogICAgICAgaWQ9InRleHQ0NDY1IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NDY3IgogICAgICAgICB4PSI4NzEuMTYxMDEiCiAgICAgICAgIHk9IjYzNy41NzUyIj5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI4NzMuODMyMjgiCiAgICAgICB5PSI1NzcuMDMyNDciCiAgICAgICBpZD0idGV4dDQ0NjUtMyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDQ2Ny00IgogICAgICAgICB4PSI4NzMuODMyMjgiCiAgICAgICAgIHk9IjU3Ny4wMzI0NyI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgaWQ9InRleHQ0NDkwIgogICAgICAgeT0iNTEwLjI2MTgxIgogICAgICAgeD0iODc1Ljk2NjQ5IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbgogICAgICAgICB5PSI1MTAuMjYxODEiCiAgICAgICAgIHg9Ijg3NS45NjY0OSIKICAgICAgICAgaWQ9InRzcGFuNDQ5MiIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+MjFzdDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iODgxLjMxNjU5IgogICAgICAgeT0iNDUwLjE5ODc2IgogICAgICAgaWQ9InRleHQ0NDk0IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NDk2IgogICAgICAgICB4PSI4ODEuMzE2NTkiCiAgICAgICAgIHk9IjQ1MC4xOTg3NiI+Mjl0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNjE1Ljc5MjQ4IgogICAgICAgeT0iMzg3Ljc0NzE2IgogICAgICAgaWQ9InRleHQ0NDY1LTMtMSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDQ2Ny00LTEiCiAgICAgICAgIHg9IjYxNS43OTI0OCIKICAgICAgICAgeT0iMzg3Ljc0NzE2Ij4zN3RoPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ1MTkiCiAgICAgICB5PSI0ODEuNjUyODYiCiAgICAgICB4PSI0ODQuNjkwMzciCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9IjQ4MS42NTI4NiIKICAgICAgICAgeD0iNDg0LjY5MDM3IgogICAgICAgICBpZD0idHNwYW40NTIxIgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIj4yNXRoPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1NjMuMDQ2NzUiCiAgICAgICB5PSI1MTMuMzYxMzMiCiAgICAgICBpZD0idGV4dDQ1MjMiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1MjUiCiAgICAgICAgIHg9IjU2My4wNDY3NSIKICAgICAgICAgeT0iNTEzLjM2MTMzIj4yMXN0PC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ1MjciCiAgICAgICB5PSI1NzcuODk0ODQiCiAgICAgICB4PSI1NjUuOTcxNSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNTc3Ljg5NDg0IgogICAgICAgICB4PSI1NjUuOTcxNSIKICAgICAgICAgaWQ9InRzcGFuNDUyOSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDUzMSIKICAgICAgIHk9Ii00NjAuNzMzMTIiCiAgICAgICB4PSI0MzMuNTgwNzUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii00NjAuNzMzMTIiCiAgICAgICAgIHg9IjQzMy41ODA3NSIKICAgICAgICAgaWQ9InRzcGFuNDUzMyIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+QW1pZG9uPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI0MDUuNTMwOTgiCiAgICAgICB5PSItNTIzLjU0MDE2IgogICAgICAgaWQ9InRleHQ0NTM1IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDUzNyIKICAgICAgICAgeD0iNDA1LjUzMDk4IgogICAgICAgICB5PSItNTIzLjU0MDE2Ij5BcmthbnNhczwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDUzOSIKICAgICAgIHk9Ii0zNzIuNTg1OTQiCiAgICAgICB4PSI3NDUuNDg0NjIiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii0zNzIuNTg1OTQiCiAgICAgICAgIHg9Ijc0NS40ODQ2MiIKICAgICAgICAgaWQ9InRzcGFuNDU0MSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+V2VzdDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNTk2LjcyODMzIgogICAgICAgeT0iLTUzMS4yNTkyOCIKICAgICAgIGlkPSJ0ZXh0NDU0MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NDUiCiAgICAgICAgIHg9IjU5Ni43MjgzMyIKICAgICAgICAgeT0iLTUzMS4yNTkyOCI+V2FjbzwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU1NSIKICAgICAgIHk9Ii0xMjIuNTAyOTUiCiAgICAgICB4PSI1OTUuNDM0ODEiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii0xMjIuNTAyOTUiCiAgICAgICAgIHg9IjU5NS40MzQ4MSIKICAgICAgICAgaWQ9InRzcGFuNDU1NyIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+TWF6aWU8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjY5NS43NzI5NSIKICAgICAgIHk9IjE2Mi4wNjg3NyIKICAgICAgIGlkPSJ0ZXh0NDU1OSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMC43MDcxMDY3OCwwLjcwNzEwNjc4LC0wLjcwNzEwNjc4LDAuNzA3MTA2NzgsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NjEiCiAgICAgICAgIHg9IjY5NS43NzI5NSIKICAgICAgICAgeT0iMTYyLjA2ODc3Ij5ab288L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjI0MC41ODk5NyIKICAgICAgIHk9IjU3NC40NDU0MyIKICAgICAgIGlkPSJ0ZXh0NDU2MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDU2NSIKICAgICAgICAgeD0iMjQwLjU4OTk3IgogICAgICAgICB5PSI1NzQuNDQ1NDMiPjEzdGg8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU2NyIKICAgICAgIHk9IjUxMS42MzY2MyIKICAgICAgIHg9IjIwNi4wMzE3NSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNTExLjYzNjYzIgogICAgICAgICB4PSIyMDYuMDMxNzUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NjkiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPjIxc3Q8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjYyMC40NDMxMiIKICAgICAgIHk9Ii01MDYuNjgyMTkiCiAgICAgICBpZD0idGV4dDQ1NzEiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NTczIgogICAgICAgICB4PSI2MjAuNDQzMTIiCiAgICAgICAgIHk9Ii01MDYuNjgyMTkiPk5pbXM8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU4MyIKICAgICAgIHk9IjY5OC44NDAwOSIKICAgICAgIHg9IjM3MC4yMTY4NiIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNjk4Ljg0MDA5IgogICAgICAgICB4PSIzNzAuMjE2ODYiCiAgICAgICAgIGlkPSJ0c3BhbjQ1ODUiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1hcGxlPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSIzODQuMDg0MiIKICAgICAgIHk9IjY4MC44NTEzOCIKICAgICAgIGlkPSJ0ZXh0NDU5OSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDYwMSIKICAgICAgICAgeD0iMzg0LjA4NDIiCiAgICAgICAgIHk9IjY4MC44NTEzOCI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAzNjcuOTA4MTcsMTAwOS45NTk2IDI2My4wMTgzMywwIgogICAgICAgaWQ9InBhdGg0NjA1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDx0ZXh0CiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ2MDciCiAgICAgICB5PSItNDMzLjEzNzc2IgogICAgICAgeD0iNzM2LjI2NzQ2IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbgogICAgICAgICB5PSItNDMzLjEzNzc2IgogICAgICAgICB4PSI3MzYuMjY3NDYiCiAgICAgICAgIGlkPSJ0c3BhbjQ2MDkiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1lcmlkaWFuPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ5NzkiCiAgICAgICB5PSI2NDAuMjA1MjYiCiAgICAgICB4PSI1NzIuODMyMTUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9IjY0MC4yMDUyNiIKICAgICAgICAgeD0iNTcyLjgzMjE1IgogICAgICAgICBpZD0idHNwYW40OTgxIgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIj5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1NzUuMDg5NjYiCiAgICAgICB5PSI2NzAuOTAzNSIKICAgICAgIGlkPSJ0ZXh0NDk4MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDk4NSIKICAgICAgICAgeD0iNTc1LjA4OTY2IgogICAgICAgICB5PSI2NzAuOTAzNSI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNDk5LjQ4OTYyIgogICAgICAgeT0iMTAwOC42MDY5IgogICAgICAgaWQ9InRleHQ1MDQ3IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDQ5IgogICAgICAgICB4PSI0OTkuNDg5NjIiCiAgICAgICAgIHk9IjEwMDguNjA2OSI+NDd0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iMjE2LjY0NTQzIgogICAgICAgeT0iNzI1Ljk4Mjk3IgogICAgICAgaWQ9InRleHQ1MDUxIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDUzIgogICAgICAgICB4PSIyMTYuNjQ1NDMiCiAgICAgICAgIHk9IjcyNS45ODI5NyI+S2VsbG9nZzwvdHNwYW4+PC90ZXh0PgogICAgPGZsb3dSb290CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgaWQ9ImZsb3dSb290NTA1NSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6MThweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwyODcuMzYyMTgpIj48Zmxvd1JlZ2lvbgogICAgICAgICBpZD0iZmxvd1JlZ2lvbjUwNTciPjxyZWN0CiAgICAgICAgICAgaWQ9InJlY3Q1MDU5IgogICAgICAgICAgIHdpZHRoPSIzNDMuNTcxNDQiCiAgICAgICAgICAgaGVpZ2h0PSIxMDMuNTcxNDMiCiAgICAgICAgICAgeD0iMTkuMjg1NzE1IgogICAgICAgICAgIHk9IjE3LjE0Mjg1NyIKICAgICAgICAgICBzdHlsZT0iZm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIgLz48L2Zsb3dSZWdpb24+PGZsb3dQYXJhCiAgICAgICAgIGlkPSJmbG93UGFyYTUwNjEiPjwvZmxvd1BhcmE+PC9mbG93Um9vdD4gICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDYwNy03IgogICAgICAgeT0iLTUwOC4xODk3MyIKICAgICAgIHg9Ijc3NC44NzU2MSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iLTUwOC4xODk3MyIKICAgICAgICAgeD0iNzc0Ljg3NTYxIgogICAgICAgICBpZD0idHNwYW40NjA5LTciCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1jQ2xlYW48L3RzcGFuPjwvdGV4dD4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzY0LjE1OTk5LDY1OC40Mjg5MSAyOTkuNTEwMjMsLTEuMDEwMTYgYyA2LjQ5ODcyLC0wLjAyMTkgNi45NzcxOSw5LjI1NDEyIDE2LjU5NjMxLDkuMzkyNDcgMTIuMDU0MjcsMC4xNzMzOSAyOS4xMTA4MywtMC41MzU3MiA1NC4xMTQzNywtMC4zMDExIgogICAgICAgaWQ9InBhdGg1NDQwIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjg3LjM2MjE4KSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjM3My45OTMwNCIKICAgICAgIHk9Ijk0NC4zNTc1NCIKICAgICAgIGlkPSJ0ZXh0NTA0Ny05IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDQ5LTMiCiAgICAgICAgIHg9IjM3My45OTMwNCIKICAgICAgICAgeT0iOTQ0LjM1NzU0Ij5NYWNBcnRodXI8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ2MDctNy0xIgogICAgICAgeT0iLTQ5MC4yNDU5NyIKICAgICAgIHg9Ijc4MC44NDYwNyIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iLTQ5MC4yNDU5NyIKICAgICAgICAgeD0iNzgwLjg0NjA3IgogICAgICAgICBpZD0idHNwYW40NjA5LTctOSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+U2VuZWNhPC90c3Bhbj48L3RleHQ+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDM2Ny42OTU1Myw1MzcuMjEwNiAxNDEuMjgzMDMsLTEuMDEwMTUgYyA2LjQ4OTk5LC0wLjA0NjQgMTIuNzgxMTQsNy4yMzU0NSAxOS4xOTI5LDcuMzIzNiA1NS45MjM2MiwwLjc2ODkgMTU4LjY4OTk3LC0wLjE3MzMzIDIzNi41MTQwMiwtMS4wMTAxNSA3LjgzOTU2LC0wLjA4NDMgMjIuNjMxNDcsLTE5Ljg1MzU1IDMwLjMwNDU3LC0yMC40NTU1OSAyMi4yNjU4OSwtMS4zNTE4MSA0NS4xNzk0NSwtMC41MDUwNyA2Ny42ODAyMiwtMC41MDUwNyAxNi4xNDczMSwtMC42MzI0MSAzLjYxMDE2LDIwLjcwODEzIDI2Ljc2OTA0LDIwLjcwODEzIGwgMjQzLjQ0Njc5LC0xLjAxMDE2IgogICAgICAgaWQ9InBhdGg1NDk2IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjg3LjM2MjE4KSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzY2NjY2MiIC8+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI2ODUuMjA4MTMiCiAgICAgICB5PSI4MjcuNTMwODIiCiAgICAgICBpZD0idGV4dDQzMTAtNy04IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtNiIKICAgICAgICAgeD0iNjg1LjIwODEzIgogICAgICAgICB5PSI4MjcuNTMwODIiPlBhd25lZTwvdHNwYW4+PC90ZXh0PgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0iTSA1NTQuMjg1NzIsNzIxLjQyODU3IDU1MCw1NDMuMjE0MjkgNTQ3LjE0Mjg2LDEwMi41IDU0Ni43ODU3MiwyMy4yMTQyODUiCiAgICAgICBpZD0icGF0aDU1MTkiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwyODcuMzYyMTgpIiAvPgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNTI5LjYyNTMxIgogICAgICAgeT0iLTU1MC44NDc3OCIKICAgICAgIGlkPSJ0ZXh0NDU0My01IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDU0NS0wIgogICAgICAgICB4PSI1MjkuNjI1MzEiCiAgICAgICAgIHk9Ii01NTAuODQ3NzgiPkJyb2Fkd2F5PC90c3Bhbj48L3RleHQ+CiAgPC9nPgo8L3N2Zz4K\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"showTooltip\":true,\"autocloseTooltip\":true,\"labelFunction\":\"var deviceType = dsData[dsIndex]['deviceType'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}, ${energy:2} kWt';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}, ${temperature:2} °C';\\r\\n }\\r\\n}\",\"tooltipFunction\":\"var deviceType = dsData[dsIndex]['deviceType'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}
Energy: ${energy:2} kWt
';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}
Temperature: ${temperature:2} °C
';\\r\\n }\\r\\n}\"},\"title\":\"Image Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First route\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.5851719234007373,\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.9015113051937396,\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.7253460349565717,\"funcBody\":\"var value = prevValue;\\nif (time % 500 < 100) {\\n value = value + Math.random() * 40 - 20;\\n if (value < 45) {\\n \\tvalue = 45;\\n } else if (value > 130) {\\n \\tvalue = 130;\\n }\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH
See advanced settings for details\",\"markerImageSize\":34,\"gmDefaultMapType\":\"roadmap\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"useColorFunction\":true,\"markerImages\":[\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic7b13uB3VdTb+rrX3zJx6i7qQUAEJIQlRBAZc6BgLDDYmIIExLjgJcQk/YkKc4gIGHH+fDSHg2CGOHRuCQ4ltbBODJIroIIoQIJCQdNXLvVe3nT4ze6/1/XHOlYWQAJuWP37refYz58yd3d6zyt5rr1mX8B7S5Xo5/0nPYaNFM1PY0gGqOhfAgQCNBGlWFFUAYEIeihigbhFdZQwt85BV5Gj9r/718R2XX365vFdzoHe7w6d77xnPkn4YpAtU0YiizNJcmPNkMQFkDiSlowHt2HNtGlTSJ6B+pTpsKTfKgTj3Pi8SMtFtEZnFs8d8dPu7OZ93BcCHtt0+OiL+FJjOiqy5K5dtLwD4PBHGvy0dKLYo8B+1+lAldv50FfmFzWX+84i2M3a8Le2/Dr1jAKqCHtl2y1wC/pEMP9ZRLBaYzF8CCN+pPluUkOKfB6qlmk/dBwTyt8eOv2AZCPpOdPaOAPjA1h9/SJX+TyGXuz0TZi4EcPBeOk+U+RErZh2YyMAyQJEoZUjFgtkCAEScgDyx1hmInTglqDj2U1X0WILaPbWvwHO1WummeuLONhaXHTf2wsfe7rm+rQDe133j/i5xPyrmCr+OouhSKPbdQ5fLiezTIYUBQGMJBgYWxMYSISZhbxgQT8wGAgDiwWxUvCiBxKhSKOqdh4OyV5+6XiEfK/kjVOXQ13apG+I0+adKpXaG0/Si0yZdvPbtmvPbAuCNT98YTBhT/8fAmEpHoXgKgPe/6gFGP0nwG8s2YykcaRCAYYQ5tKTkDVuArDEwMRF5AICS4VZ1AQBSr6oEgL36CBAvlKqIsyLOKQl5TZH4uN+TawDuY6o64lWTJX20v1S633uJNvfmvnbRERelb3XubxnAX26+5gDy6Y9HtrU/wERff1XjSt0WwULDmZEMawPOgilgQ4FaGCEygaXQMQyRMaxiUijUkAEAImIGAFURAOrVA1AmI1ZExGuqoqkVFefhyGtKDql4X4eHc6LxJof0VIVM3nVc4uXaHUPlo0Tpc2fv/zer38r83xKAd6y74iImO31EMf9REA7cpdVBY8NbA5+dFNqsCTQipkitBjAUsLUZNd4qm8AyjDMmJAIRhDzDEBEbJkBVAyJWQJ14AEaciIeSGicOgBeBWNHEeXLkXIM8UvFI4bVBCVJNfdk7STd5xOcp0LZzjIqV/eXq/4i61edM/eaN7yqAqpfzf62Nf5LP5lbko/DbCuxU4saEN1mN2kKTzQbIkuEIEWfVagRDEVkOyXCkVq0aDg2p9YYNAySVerU0WN1R27Jjo6ulMQ1V+ggAOgsjNRNEus/IiUFnYUy2kM23AcrivXh2RiTxjhx5iSmVWEWdpmhQ4qvwSBBrXVPfqDmuVsT7C3aZvKslyZcr9dpxdr81F8ynO/w7DuD1q/8y6kDw2872ticN0deG7wvQHXHmdxGK+1ibQag5ikweliIElNUAEayNYBCSRQRiYzf2rNtx11O/rC5d9dj+1aQyM2Pyz3WGozaNisYNWY7SYtgWA0A5KUVO4qAn3t4+lOzYt+Grh+bDwstHzvjA2tPfd1Z+39FTRhGpi7VBKrE4nyBFDKcNJL5OCerqUEXdVeEQb0mk8lECjR0euxe9cqBUOnoQ6RkXT78hfscAvH71X0Z5kf8Z0dH2CgNf2NkI0d0ZbmtElMtFVEAQ5BFIlkKb00AzFJqCGooQcJjv7t868P3/ubayZvua48ZlJt57xLjjB/cpTssXokK7IQNrbeoZ3pIRJm1aYSUW9cwixglZ7xNU40ppY7mr+sy2ezt7G1s+vP+EGfd/+fS/Ko5pH9/pJK04X6MUDSRapcTXkXJN46QKp1UkqNVqvpxVyLzhOajihh1DpVkmrJ7+uak/bbztAF6/+i8j62p3j20vbgXR+cP3LYU/Djg/KcsdEnIWERcRIk+hzWtEOYSch2U76tk1T6+84Tf/NCdni2tOmbRgy6T26WOiKDBhGFEQhrBhiNAyjDGiQp4DFgI8AChg1BGBXOC9p8QJ0kas3jvEcUxxnLgNpTW9izfdOqGWlve7+OOXrThk6qEHKtKehq9xIlWkvoaYytrwFYqlglgrcZxW+oXSz+ycpOLmnsHypDTIfuTNcuKbAvD2288x22dn7hrVnt/ATBftBE/CH2aCtqkZU6CI2hHZomS4YCPK+5AKHFB2ZNe2Nev/739/e9qY3KRnPzHtQp/LtnfkMhnKZDMa2oDCTIjQhghDC2MCCQITAyYxpmkhAIAZDDA7l4bOSeR9YpLEwfkUjXqMOE0QN2LU4waq9aGBX6/+d7O9sXnu3579jbVTx02dlEilL0FDG1pJG64cJX5IGr6MupY5duU1npIv7sTQ4196ytUDx8+sf+TN6MQ3AyBd8+L8W0a15zYw0d8O3ww4vC7ijlkZU5QctVPE7QhNEVlTRNYUjHcy7tu3fuuVSqXBF8z66962fMeIfDaHfD4nmUyWsrk8BdaYIAh9EFoxzExEysYoAQ5A0ioAEIpIBGZmAM459iKaJo6cT209TnyjWkOSNLRWi1GtV9A3sGPg56uvG1vIZ9N/OO9rM8jS9oavSOwqaEhZYh3khq9K3fdpXWsbvdR3MoYCV/UOVadcOvv2C/AG9IYAfue5j1/U0R5mIhNctxM8yvxLyMVpOduJyLRRnto1MkXK23axlB27sXtT1z//8vqDTt3vk/fMGnX4xGyhiEI2Qi6X1Ww2S7lCIQ3DkCxzQEQKYADANgCbW6UHvwcRaO6fAwCjAewLYAKAcao6UkRIBEniEtRqNVOrVKjeSFCP61oaqurKvqe237P2lnkXn/X/PT9l3OT9Eql2V90QN1wZdRqSuhukhi9T3Q2s9ki+NDzHWppeUqnG/qsH/+b7fzSA33ruI7ODIDh/RCH6KkEZAEINfhia4n4ZO0KzphN5005Z06aRaeOAcjP++4Ff3P/86hWTLjr08i3FfEeurS3LUTanhVwe+XxOwjAw1loLoB/ASgBrAdSAV232Gc0NyJGt70+27mlrzNT6nAEwDcBMACO892kcx1KvN6hUqWu9Xka9XsfgUP/Qjcu+Nf3g6bO7zj7urBNT1F+quxLXfUkaMmDrviQ13+8THdqYqvuLZpfq+qrJNXFDbrp87t0v/cEAXr5iduiTMQvHd2QnKDC9+bC9NUfF9kwwgvNmBGW5Q3O2SFkzAkaCg/71Nz9+2MTZ6rlzLs4Vi0WbyWS5o63N5fM5G0VRaoxpA7ChBVw3ANMq1AKoHUAewCwARwHYvzWctQCeaNUrt4pvgeha17Gtevt47+M4jrVSqZlSqepqjQpVyyX/8xU3VBHF2T//+OeOFbgXaq5fa75ENR3SarzDxDToYz846FTORbPRV7oHG9sm+qEPX3TEM3vc9pm9AfiBP53+T6Pbwo0Cd4aog4p/yXK+lDX5IDIFZDinGS7CckEM+JB//u9/e3Z8NGPTgjl/Maq9s8N2FNtcPpc1bW1tFIZhaIxJATwFYA2AtAVWh4hERBQByIgIE1Gsql8gou8AeAjAfQAeVdUvEtE9reFFIpIloiyATgARgCqALQAGmHmUtTYTRWHDhhaGYE0YYmbHEXZj//rBRc/fXTly5qGHEus2FUceCbxP4DShRJ2mvuIFboyqG5kNcNuWVM965MbNd71pAC99+vADA+MnR6F+TeAg6h1TeE/I2bbAFjVLBbJcpIDzZNke8qNf//yxKblZWz42+9Pj2opFbutop7ZCQdva2hAEQZGZXwGwDEBDRCJV7VTVfVV1BDNPUtXZqnomER2tqi8S0REAzgJwUqvMI6JBAM+p6pdU9f1ElGu1E6lqUVVZVYWI6gA2EFFijJmSiUIPsDbXmGT3b59V6Kv0dd334uLGYTPmHK7Q7lRi65DCawqviXWSrEm1PlvgWMh9KPbut+/77Ohtj/97d98bA6igo7aM+O/Ogp0l8BNFPQhyY2RyE0MqcC7Ia2jyGpksBYj2//WDCx9uk/EDZ8783JhiW5HbigXpaG9HNpvNMXMGwAoR6SWiUKS5KhERS0QqIgmAHcz8sqrOA7AdwCcB9AK4CcBvAdwP4EVV3V9VPwGgC8B4Zv4PIqqoqgPQYObEOadExC1A60RUJaLxURQaZqoRW0NEsm/xgI6u7rV9L295vmvGlKmHQ32vk0QdxfA+oYTq+Vgbi70mR4p6BEaKlTid98S/9f4MV7wBgF/66AEnFbPUz+z/VNTBiywLgxxCFDgwGQqR5wznOeR8+6p1657r6uopfu7wv4mKbW0oFvIoFovIZDIBEXkReUlVG6o6Fs2N/EjvfSczj2Hm/YnoY6r6Ae/9w0T0cVXdSkTfE5FsC8iTAZwI4DAAjxDRj0TkUABTACxS1csAzG39MHlmzqvqGCLKt1xZA0Q0QERtQRBkDZMngrcmNAeMmB08uHpxNsrz2pFtbft4TWInDZtSLE5T8i7uSKRS8XDjBX4fYbnusI2jMkt/tGP9rnjxrl+gICP4Riagrzb1ssKa4CkrYRhwwBFHYGSUOZJKo8oPP/vCoV846opSoZCnQj7HxUJRMplMgGblR5h5wHtfbE1oZAvIHBFtVtX7RKTQ4pSrnHOXAThQRK4BcIaqNkTkRRF5UVUTVf1462/TVPVSEfm2974qIm3MvBhAl6pGAEYAaBcR45zLiUiPiDxKRC6bzZpsNhtGUaj5fIG/dNTltYeeWja3ltbVcGgMZX1IWbUUqDUBbBA+OYxDPuDLSORq6KsN76s48MvzZnwwlzNDgaFzAIBAi0LKtGVtEQHlOaQCQpOHoWDWL+9+ZODCuV99cnTbmM5cIY+2JudZIpronHukxUWemavOuZIxpuG9H8fM8wDMJaJHVfV0ANcDOIyIPg5ghTHm+0S0UETWq2oCoA/AI6r6C2PMgyKyD4BPM/MggJ8COIGIFqnqV1T1YADbVXUjEfUaYxrOOcPMBVXdCmCutbZirQGIlIBwavucl2577NaJM6ftO1nJ9aY+YfEpvDryknamSNdAMQ1AGwxdc/DqDjz9k/7Nw5i96ixBSK/MhTRxJ7oUbracmWAoVGNCtRSCYOxLazfcN7VjdjK+beK4KAqpkMtpJpNRABNVdT2AowHUvffjAYgxZpNz7hUiuk9VT1LVWFX/iojuBfA1IrpfVRcS0Xne+6tUX33+M/zdew8AzxljLvPefxTA3xPRIufcpQA8EYUAFhPRSCKaKSL7EFGgqjtU1RDRZmaeGIbh1sh78s7LxM59R09um7585fqNdtqUMZOMMc4igE0DthSppcYWL80VTNbyX1QCPgNN1fJqDvzi0tnjQviObGia3Ee0JEAml+E8DOUo4pxaE4GUJz3yxJr9/vSIv+8uFAu2kM8jl8vBGNNJRE+q6grn3AZV3QRgi6q2AZjHzHNE5FEAp3vvv8HM8wFQSywvADAPwDgAi0TkPwDcBWDhcFHVh9FcXH9ARE4BMI6ZvyEiHwYwSVW/CeB0IlpERJeo6hwiepmIlnrvVzLzemZex8yDzDwZqlUikGGm6R0H66+evuPYafuNynvFkCCF4xjiBd67otN4C4GmEDAqTuVnR3++beWT/z5YfRUHio8/0dEe7DynJTUvswmmEiwxWcCDwGyee37j4ydNO6ucy+YmZMJQM5kMWWvHqmqPc24eADCzENEGAMvTNH2AiM5Q1W1E9GkR2cLM3yOiS0TkO0R0lao+zMy/8N7PBHAmEZ2C3YiIoKrdqnqjqq5i5j/x3n8bTQt8iapeKyKbjDGfFpEhAGOccw8EQdBhjPmQqk723rP3PrTWvhxF0Xgi6vHeayaTyx075fS7nlvxcPGgg8ZNIjHeSKRMdbEUIEHwEuCOA4DOvB25vSRnAfghMGxEFNRb7ZoM0HFNadFeIjvRgMFkhEDKbEl8Oqq7u3bs+/c9cXQUWo2iCGEYsqrG3vvHAPwEwL2qulZETnXO/Zm1FqoKVf2Bqh6qqr8SkW3e++tU9T4i+ntVnem9vw7ARQA6ReQ5AL9yzl3vnLsewK8APIfmovkiIrpWVWeo6t977x/w3l8nIluI6Dcicqiq/quqgpnJOfdnIvJR59wmEVlCRD9S1QeJKLHWmmw2hyAM9bhpp47q7q4d733aSVBlkBoNQGxgYPdVRZ82N5In9lS7dp42GgA483hMyUY0RXgwXzAjQgUtshp1WhOR5YgDzoiB0U2baqsPLB7z0oxxBxWz2Rxls1lh5gNVdbn3/rwWR68moi5VPZWZt4nIvgBGquoRAH5BRH+OprH4oYh8XlVPQXMvfIOI/BJAFxF1qupxRPRBIjpKVSe3dOtdInKbqj5PRIe3RHayiHydiMYDOIuZfyIin0HTfI4kIgAYa4y5UUQaAI4QkY8ZY5YR0aGq0kcE8k5NNS4t665u6G9r47xDCi8pqabsNbFe9WkoRvU0upYl8GunnqebX7kZQ00O9DipLbKjRfQTPWnXYyBTBxMBBiIML2IVkt20sf6B46d9rJjJ5chaQ0EQRAC2pWm6VlVXq+rZIvIXSZKELcX/Y1U9RlW/AWC8iJyqql9V1aOcc99W1SXMfAmAh1X1qy3O+rKIHCMiGRGptUqude9iIrqWiC4brisiDxHRt1X1KFX9qnPuowDGe++vUNUPishNLQkIiOjPVPVs7/02EVkLYHsYhtYYg0wm1FNmnZPftKF2lFPJisCIkhE1DFiFaNLr1i5R+PntGR5lFMcBLWfCxxbhrgkjgqMAjCKgkrWFX48KZ7RHJm8CziJLOXJpUNu4omAuOfbKOMxkKBOGHIbhHBG576qrrtLHH3/8QmaOdtdd/5tIROLTTjvtyc9//vN3BUGQs9aOA3CyiDxXr9dRrzfo2gf/Ljt1TpyYIMnWtQ4nVW2kNd+bri41fOlMADkQerb1p4/f+WGcaS9X8HOLUQIwCgCUdFGi6ehBt7k+3k4DqQ8cOd2+mQdPnP6xijHB+MAYhGEoqppL03T/J5544iRmpvnz5z+4Zs2a1dOnT5/+8ssvr5o5c+aMWq1WSdM0VdXORYsWHW+tXXbmmWcONV2jQG9v744dO3b0jR07dvSIESNG3HbbbbNFpHPBggWPtMTvVUREWL58ee2VV145bcSIEU+ddNJJ1RY4unLlytXTpk2bEoZh2N/f37dw4cKTrLUdxWLxvnnz5pnf/e53unDhwhPa2tpWnnfeecekabopCIIMEYGIyBjGCfufvmbpltuKY6a4LKkzCh8PpZu913g0oIsAOhOKMQTElyvYPrsY43IRP6uK8wCAYHrUo+gpiXoaG+LR0X5VaNgxNEAHz5pz6PIgMGBmBTCKiJZVKpUjjDEmTdPG/PnzPwSgLCJHoLlY/omqXgLgWSJauHjx4uNPP/30obPPPnsAwGNoLl+O32Xdt/a3v/3txnK5HM6fP/+3aJ2JAAi89zkAUwGcdOqpp+YvvPBCnH322fEJJ5yQA3CH9/5YY8yft0C+SkTmP/roo72NRqPjhhtuODCTyRTPOuusRy+88MJVd9xxx8cWLFiwiog+oqp3ARgVBMEO7xVzJ70/v2jdHbNGqu/16uq98WakmuQgANhsU98MRQwMP7N0iYxhUuybD/n3WzqlAMROROElzfY3NrXHrtTNFHTkMvkiGQNiZhGZ7ZzbPDx5IoKIXK2qZzDzd9F0T/0pEV2qqoeKyN8BwLZt27ap6hmq+l0RmQXgZhH5iohcpaqrwzA0RATn3DXOueta5buqeoWqnqWqT9dqte8DwPbt2zeKyBGq+l1m/giA7wL4map+jYj2S5LEA0AYhp0AvsvMp5577rn3Axi/YcOGxaoKEdkCYBYzqzGEMMgUWILRjXSopzfekFUf5wUKYXYQCoZhykcM08C+DMUMw7Rva8sHqHZCJFD1VtTDaYLuoe3xrLGH/Yu1NiZVtcYAQEVVy7vpmPNU9VHv/RUArgZQ9d5f473/qYj8OwBMmDBhPIBnnXNfAfAj59w5AK4F8DURmcfM1JrY/4jIrSJyq/f+XlV9vmVMPlEoFC4GgM7OznEicmPrB3hJRC4Tkc+IyI+897cFQWBay5lrVfVKVX30lFNOOUZV/aJFiz7YMi79RFQiIgbg2NrazHEHf7+70q1eGiwkROoteQkhOmIYp8DQBGUcYIVwOJMepCCAkBCooCAnUPVwXoU1rrXVoyi7nwgoDO1QyymwzTn34d7e3p8B+NsWFx4AYLP3/l4iuoKIHhaR/yaiLw1z6rp169Z57+cR0bUiAiIaVNU7ReR5Y0xcrVbPbf0ek1U1DwCq2qOqG4jofhHZUi6XAeC7IkIAvqCqIKItaG4LZ4jInxERvPevtK5fY+b7W+0eBGD78uXLx6nqd51z85i5G0Bore1rNJJsxuan1EumFo3w3mtKSupAMASNRJEACBk6ixWphWCaKs1tqegVUIWyiBcPIYhRQlLKhQccNDtW9YEIh0TkiciJyGFtbW29LfCCxx577PtHHHHEhdbabd77bzLzFap6jPf+X5o46Jf333//qWh6kP+P934HMx8F4HQA53rvkc/nl9frdYjIQbsw99SWy6opPvl8BQC6u7u3ENFfq+poVb1IRK4iIvHeX7dy5UpKkuR8Zka9Xv9WNps9n4j2B/DNkSNHnrV9+/ZRIvIhIjpMVZeoqlfVEcyQ6WNmpQ8+nyva9m4IO/XeQ1XFE6UKfYkUhyrTEVDEFkAWO4NuZAuAsPnDKlgFzih8ku0cU5y4NQiCxFrLAPYDUCOizxpjrgAAY4y54YYbvtwS5f1E5B9UdSgIgloURR8BIESEO++8c8qmTZtetNYeHYahdnR0wHv/pIhsrVarvX19fQsA5H71q1/dYq01pVKpkCRJXCqVaGBgwDcaDdfX1zcRwDELFy788JIlS96XJEnBOQcADSIKmfkSIsKwpXfO/bmItBljLlHVa6dNm/bIE088sR+AMUT0WRG5kIgmWWtfIWPcuPZJDJ9r90hIRVTEq5KAlBIIdYH0UCg6FMhZUvDvjSDVnZBhUhUSUijICxHCbDFXZGOMqKoH0KmqQ/l8/ptdXV0/rlar38rn8zs5hJmJmUM0jyPb4/j3h/ze+ylLly6dgr2QaepX3Hnnnefv7ZmdoyUamyTJWABoHvTtmbq6un4xa9asSQCuA7DSWvtSo9E4zHt/dbFYvKLRaKwF0E5EwoBENlKVMOPFkcJDCRBVUlEloLQTLgWz1987FAhImCECJVEh8Z6cdzBk20ITkIg4Y4xX1ZFoHuJM3XfffT/S29uLLVu2oFKp7HQ9/W8ia+2RzHyGqv6TiPzjsccei97e3kxbW9uZACYTURVNb7mIiIYmJIOwLUWqTqQVIqFEDFHV6nC7orDMBB22LOzhWbRC0LJRLalqGYqyQWAJVDPGVJIkqQPYrKq9AGCMmQoAaZpix44d2Lx5M/r7+5Gmbzn4822jVatWvei9/9M0Ted77/9j5syZawAk27ZtswCgqt0AtohIzRhTssZWDdvQkA4RtETaxAOqZSWWnXgR1Kr8/kTbG2ThtaAE9QQSZWIQ2EilFteyhoJCa4lxYMvf9xry3qNUKqFUKiEMQxQKBeRyudcVsXeC0jRFrVZDtVrFzTffnOnp6Tl2/Pjx944ePXrt9OnTzyGirY888sjLCxYsOERExhPRDGvtswACrz4m60pOqIMIBIX4ZqCYAWsZLXumAtid6z8A5DSvlgkKFkcMiBERqHUDiUu8994SkQCoEFF+jyPfhZIkQX9/P/r7+xEEAbLZLKIoQhRFbzugzjnEcYxGo4FGo/EqCejp6Tnv5ptvfk2dH/zgB8sWLFgAVS0CqHjvyTlnq2mFYF3VORnJICKwI2IFI0Qi7TCtLaYCVgnbAdoA6GRhaoPXhipIVJkEUCXP7CrleBAd2RHsvYcxpopmfMreaICZN6LpQWYRmZSmaeeuk7LWIggCWGsRhiGstWBmWGuxqwUFABEZ9ilCROCcQ5qmcM7BOYckSYbd/XuiTczcT80YHHjvZ6MZZ4O+vr5hx+14Va1Qa/M9WB0Asa+SUCcIRuAtg5QEBKDYrEJrwdhiIXhBRQyIJkMxQxQvkELh4RUq4kCJ2VHdOLiOx+YmmTC0trWwnQOgsvtoiegFInKdnZ3rRo0aJT09PTw0NAQAm0VkzvBzw5N/B0mMMU+pqhk7dmxXsVjkzZs35xuNhojICDSPRpPt27c/WSgU5hLRC95722g0aOPgWnbcW5VUBYCSJYBBChgQzWnt2J4BsJyheFkVr7Q6Hc2kZYU6ARSejCjZFN259UOrc6reOucMEfWpqnXOPQIAhULhN8PgMXNl3rx5Y4IgOIuZz46i6KyTTz55JBFVmXnFO4nYrmSMeTKKooEPfvCDs40x8621Z3d2dp566qmnxsxcArC1s7PzkVWrVi1X1QBAv/eeiYg2DK0upOgpiCBQIlIBBOrBOgTCCAAQ0jUQrGS1WF1vUPewLlTlKoQCOARewOqVUgzmtlXWTWuKiqiIVAAgjuOtuy1bgtNOO21ET0/PhO9973sQEXznO99BT0/PxJNPPrkDQAO/97C8k7RBVaO5c+ce19nZmb3yyisxZcoU/NVf/RVWrFjx/kMOOWQ9M3dXKpVRjUYjbKmGinOOnPPYWt04PZGhjHoQCZigAQsFpFwbxqlRpx6k6LI6gK5Kpz8zm20d0JHWQFAYTSUlALDexSNdEB+Y+nQxpZRlppSZ4ZybdPvttz9QqVSOt9Y+SkR+xYoVxx522GF4/PHHceCBB2LZsmWYPn06nnrqqQOZ+REiekZERr+T6BFR37hx47rWr18/NwxDvPLKKygWi3jhhRdw5JFHolarzXvuuee60jSdYFordxFJnHNI0rghiGc4jb3xUDEQEngyYEBrwx7KcuJHZzux1t79KZQ++iv5AHTnCadVBZGQhULh1SsIMfoe7KlsGRqTm5Q1xmkQBJtV9dijjz766f06bwAAEgVJREFUnpUrVy4EgIMPPjh300034bjjjsOaNWtQqVQgIjjqqKOwZMkSzJs3b/Xy5cstgFUA3rZF954cr6eccsrYxx57DJ/85CexcOFCDA0N4cQTT0S1WsWjjz4azp49+4l6vc5Tp049TVU3eu/hVXVbZUN/TH33k8c4DVRIiMFEohCjCIdXLC6VY+44DV+zACCEXiiWgnCkEp1EpKsEqqTEIsTq1Axg+eCy/kczp+QmqDZfuXpRVedNmjRpx9VXX32hiEBEsHTpUtx5551YsGABnHM47LDDcNNNN+GAAw7Al770pc8NPzdsUXe1rsOA7n4dBmjXK3NzgbHrZ2beWQDg7rvvxq233oqLL74YS5YswY4dO/Dkk09i7ty5uOCCCz4bx/FPRGSUiNydph71ap2W9T9eGGgsr4iqZSVVsLJ6Z5lIlU5srfmWAlgHtE7lDjgP5SjgAWb6MBTtoroMgpwoERTwniiJhwq5aPrxB+YOWwuQIaKEmWd573NBEHSoKosIpk+fjltvvRWqitWrV6O7uxvLli3DV77yFRQKhVeBtzcgd/2+exmm3bl3dy4kIowfPx4LFy5EpVLBpk2b0Nvbi+7ublx22WWw1ro4jgsARgJYVq/XUG/Uk2fK95+ypXxfrESGGUIEMhYGTP1ovQOYOr2+kcjvVt+K9c130cp4slyX4nDnBqYbRCAGkTZXUELIVtPeezeUu3rjOEaSJFDVpwEcmKbpLcMTnDhxIm644QYEQQDTPDvBNddcg3322ec1IL1e8d6/qryZOruDffTRR+PrX/866vU6kiTBAQccgOuvvx5hGKI15hki8lTz76lura/fUUt6F4siJIKCiREAakhB6BnGp1ST9lwbngJ2CfE99Zd4cPzIcDqg4xl4wQl64EE+BlyicCnYanHz4RMumviR9vO7C4UC5fN5JqKzVfXlKIomtzzGr5nwGwGwOxe+ngi/ntjuXowxe/s+0Gg0+ohofxG5o1KpoFqv6+LBn496dssPt6dcmWAtlCOCNRDKgJgxEopDoLRl60Cy5p5P4Hhgl/A2NbgmTuUGBeCBOUTokVZAtyiIFJSk5QmJlJKeyvaeer2u9XpdVPVxVZ1Zr9dv25PI7Q7M3sDbEwe+0Q+wt/b21vdwqdVqv1XVaar6eJwkqNdj9JY3bW9IKU5cZRwUDNPcuagBE2G7Kg5RAKnI9SD832HcdgJIARYOVdyknXtjoTpBoaRsTPOMHQy7fMutQy/qQzOr1arW63VNvd+kTc/NfO/9I3vTXXub0N5E9/U+v57Yvp7+VFWkabpYVc8DMJSm6aZyqcSNRk1fxOMHPb/5v+pQtWwgUBCxErGCiOJhXHYMuRkU4r7XAHj3aYhTAaC4rakI9dNkMMSWPBhMSsRKmjRKIyuuZ3Bzfe32crnGlVJJReQ+Vc3HcdyuqgPD4re3ib1ZHfhmVcDuYO4JxNaYetI0HYvmMen91WqVqo1YNqVdW2uutz9NSp3KTNpcxMEYgjEYVNULmvVxiwLVu09D/BoAAcAZXL6j7F9SBVRgiUwPkRJYCQaqrEoMWrrqp4WN2ZfmxXGtWq7UqFwuJyJyP4A5cRw/qKryelywNw7ck+58I336ZvtR1Uaj0XgewMEicl+5XPblcpXqtXJtk33x1KUr/6MAbnKdgQKsDFUVMTtUYFWBvpLvohRX7orZqyJU192K6tSz9Qv5HPcQaCpBZyvjRSiyEFIVkDioiBbL1W3LglGduWJ9LKDExnAtCIJEVU/w3t/MzIfsbiD2dn0jHbkrF+1qSPZkXHY3MMNX59ydaB5ePdNoNLZUqlVfrpSxOvO4earr5xvqvm8iGfggBFNIyiGYQwwQ4xwABqqLhmo+c885eJVf7NUx0gDE4iv9Q/JYc1+MDABvDJQs2DDYhlBmxD2Da6YNxOulW9dsr1TLWiqVtF6vrwawXFU/7Zz7TwB/FCf+MUuW1ylJmqY/F5GzVXVZvV5fWy6XaahU5q26asuA22L7hlbvR4a8NVAYKFsgMBACJZDm7mNHSZ41HpfujtdrovS7bkV58p/oRwpZ8zIIhwM0C0SLoBipCmqNnaHAhq3L7MT9D9mfhjIrrYRt3nu0fG9VAKd673+Npq8t82a5cW9ADdOb4bZdljfbRWSpNt9BeSJJknVDQ0MYHBqiwXRHd9+IriPvffpa4YBCE0I5grCFMRlSGFoF4DMt3ffDUtXLPfPxyzcEEADGnoNH01gWFLNmChQhgTJEOqiKQIQEAiPNU09+Zf3jfZNnH3yY9mVWasoFL16sMWVm3gzgNO/9KiJaq6qTdlfyewNv9+f+QNCGPz8qIgLgaFVdVK83egcGBk25UtWBel9f/4Q1x931yFUbYLWNIxgOoDYgDSJYE6IB8CEEjFKg1D2QdscVfHn9r/EaB+YeAdx8B9z0+Sgz8HxgeR6AMVB6hgzaVMk3Q/2JSQHvJOra+GTXlMPmfEi6o+d87NpTLyTeN5j5ZWae6b3fV0RuIaKZqmr3ZJ33BNzuAO4G0B7vMfOQiNyqzcBN8t7fN1QuN0pDJVQqJe2v9u2oTt9w0l0P/uNz3iQjghA2CMmEGXgOCSYDIqJuAk4AgHrDf7We6u/uPx97zO6x13fl1tyOtfucqRcXM+ZFAHNAmA2iu4gwRkBKos0jAVXy4vKvrHvslWlHHHZk2m1eQKJ5VfXOOauqG4Mg6FXVj4nIalVdpKoHqSrtsrzYed1VXAHsDaQ9caAQ0S0iMoqIPkBEDzWSZHWlXI6HBkvBUKWsQ2nf5uSA7SfeueTqFxPUxtpQAxMSmxBqAhKTBZhoBYALAUCBW3ZU/D6Lz8E1e8NprwACwKQv4nf1fvlUMWsJwEgC5oDpIVJ0EhGrJ6sAICCXuvYVqx8uzXj/YZPSWFbWelyHeA/nPRLvqwxa3XRN4COqugrNKPwx2ozifxVww1y3K4CvA95WAHdQ8xWHDwJY4b1/tlwupwNDVVTKQ9rfP6j19h3dsv+Ow29bdEWvUmO0CWBshowJCTZL3kQAW1pPTb1noPTK9oG0no7Cp9b/7LWi+6YAXP8zuMnn4rFG4kfnQ3MYgIgIU5jxDCmKCigBpE1xZlEfvPDSErffrFkU7BNQpSutxQ1PLo6zSerFi9RV/CvMXFXVQ1R1H1VdhGaIbxnAzgQ5u4vtLsUx8yMA7mPmbQAOJKI2VV2XJMlLtVqtViqVaLBUlUqpn0vloTofOhBVMptzv1h4dd4Yn7cR1GSJwwhiQhIbIjUBthBwJoC8ElzvUHqzKL5+/+l4zQuGu9Kbyplw4m04Ix/xjI68+W6r2gZifdI1dFSaEEtdOW2AJYG6hnqXEMaOnL7ptGO/+L5kjVks2/JjM5nIZKJAoihLmUyIIAjIGANjTEBEHSIyWUQ6RWSdqm5V1YqIpC3RDImoQETjiGgKM5eIaKOIDKpq4r2Hcw6NRgO1egzvUq3V6l5Hxhuys9OPP7T0lke7tj41nQNiG0FtBmojeBMR2yzIRNhKQh9U6L6kkMGq/7t6Ii8uXoDfvRE2bzprx0n/hc93FLiQi8x1zYq0CdAHvcdkV4V3Dupi9b6OgosR+wRGvU3PPuXSHcXcPiMGnvAvcJIZlwsjG2UzMESUzWa16SExZGxLGFS9sVbFK5SUAGBYWYoIMzN5BbnUgSCaph5xXCfvvSZJouVaw1NWejrfL3NK1a07frHwmpFsXcgRvA3hTRahNeRsHmKaXpZtIDoa0P0AoBb7SwZqEt+/AP/6ZnD5g/LGnHwbvtlZCAYzAYbzJwwo4U5xOl0aUB8jcDHUxUSuoQ4pJE0gmbCt9vFTLm4UM2NHDCxNlidDweiQOAyCUDkwFLBBEFhSZrVEqkDzHLEVAiA6PFBFE0pFkjhS9YjjVJ1Lkfg0sZ3SO+rI8NBSo7vvznuuz8S+lDMhwBbWhmRtVr3JgmwAmAhqAlolij+h5svfqMW4ZKiaFu49F1e/WUz+4MxFJ92GS3MR246M+bYSGEAizD8mJ4d6p+oa8L4OcQnUJzA+hhWnqU+gUdA2cPKxnylNHj/rmOrW9N7+F5JGOiQjyXIYcgC2zRejiVXFw5Np5Y3xMGxgxBMJPMSlFHtPUI1NG/eNmhNm8uODUzZse+nB+x78WVs9KXXaDMgYspyBNyG8iQATwIRZwIawYPOCQj4LICSFDNX9V6qJ5O5bgH/8Q/D4o3JnnfhzfC6yvM/IdvPXADpaLd0KoaJPNS+xmjSF1QYkTeEkVfYpGR8j9Q5WRKvjRkztPf5DC3j0iCkn+AQvlDdUu6rbXaPWn5KrCEEErTwXTTKALbDmRgSaGxNk26bmppoQc7p7ux546PE7ZHvfutHGUJ4DOGMRmEi9sSQcwgYR2GTgOCRvDFXVaJUU81sA9PcM+X92Trru+yT+8w/F4o/O3nbyrTiaGF8cUwgOIMZRreZegerDgB6YJiQSw0uqgYsh3sFrjMB5eE1gfAovHka9pjaM+ke2TxiaNnWujBkzOcxnO/KFXKHNBpnRAODSRm+lVh6q1odqPT0bkjXrnuW+oS3tLo1HsKGADIQDsAnhjEFAFgmHsDYCmYBSG4BMRgMQvQTQcYBOBwBVPN5TStd6hxvuPx9L/xgc3lL6u5N+hpGwuHl0u33a2N/nDiTSXxBIRHWCNMilMdQ7DSVF6h1YUxXvyKhD6h0CCKCCVLxa9YASKYlyK/AOIJAyCUFBDGImB4KlEEoMbywCCtQbQ8QhxFiEJqDYWLDJakBEm4g1UKFPDI/Rq16xY9AdZQzOXzgf/X8sBm85AeM5t8P0eXwtItYRbfZToOavCyDxKj81RCPgaKJ3iL1TAw9xCVgdvHcw6uBVm/pNvQIKpwJV2pkKBQCEFKoMYoKFITVGQQxPBsZYeLIwNoQQw3BAjiNEzNioQKzAebQzkJRW9lXcbXEqctx5uOryYUv1R9LblkP1+JsxjS1+MDJn7wkDuhKEHACQQqD4OUgExJPFq/EpqTglcXDqEXoPJYETDwbgROBVAQY7ABCIJQKYYQBYZogaWGMAMkhhEJiQPLMaG5BTlvWUsgXjvJahAxS1RqpfH6i5eYjxhfs/i7clj+rbm8VXQSf/HB8T4LOj2uwzgaF/0GZ2oeHuVqjq48zIQzHee4QiSLUZgwN4kDYdt0Kkqq38BM1XhYnAMMwKGDQ979y0rERIRbENQJWIPgDorF0m2Ei9Xt0/5N4njH+//zzc9XamRH5H0iAffiOC9gLOVeD8kXl7bxjyxYC+OqMv0VaoPsCEukAigNqg1EEEFlWBQKHUFC9SBoOYiEUhRDoIaInBiSgyBDpJoeN2m9qG2Mv1/SV3iir+s1zFbc9chLc97vgdzWR+uYIfugUnC/C3keUlHQXTaQiX7LUCox9en1XwIBENCqTcvM1FVe0gSAcMzYVgxN6a8IrrBit+IHFyrCF850Orcf/ll781Pfd69K7l0j/mJxhtLb4+ot2uDy3t1T30Vihxeml/2U1WxpVLPol3PA088O7/MwI6/ib819j2YDOb154vvBVSxfXdA+nEBz6Ns4G3T8e9Eb3mUOkdJsW++NT2UjpHVO/V5vrvrRfVh7f3pTNLdZyLdxE84N0HEEtOgMsRzukdcBUV2vRWwYOnbTuG3HZXw4J3wki8Eb2uQ/WdojW/RLz/n+CluKaZTMhzm4eJwB9aFHADFf1X7+X6h/4MG9+LubzrHDhM934KLyhoaSPB3/yx3Nco42+811UPfBbvWvD67vSu/0eb3enEn/K17RkeNExXvPHTvyfxeuVQQ0be9zn50hs//c7Re8aBw3T/Z+TScl3niuBm9cCbLLeXGjr3mA3yl+/1+N9zAEHQ6oA/rxLLBPF49o1Fl54vxVJ08Ge/kwvkN0vvPYAAHv8K6ur8BbVEnlNF6XUArNQS/ziJv2jJ5/Cm07W/k/SeWOE9UddvUJ5+pimpYhODTtyT1Y29fsOrv2fxhXj+vR3t7+l/BQcO0z2fc0ucEyeil+7OfV7xFYXI4gvx4Hs9zl3pPbfCeyA67cfmFiaziVX/BgCUcL1XGf27z/vz8S7vNN6I3t23oN8caW0//+lcF/0PC+4VIBJgZm2aPw3/y8AD/peJ8DAtOQEuZLfAQ0sK7Q0rbv6SE/Yen/L/017ojH8LZ5/xb+Hs93ocr0f/D6s769KBP+5xAAAAAElFTkSuQmCC\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic3bx5uF1Flff/WVV77zPeIQMJIYQxYRRBpBGcQFEEbVQQUXB6xW5tWx9+Cm07IYIitiJog2P7qu3UCN22aDs0KIIyg0CYyUhCyHiTmzucce+qtd4/zrkhQIIogz6/9Tz1nHP22buG715D1VpVS/gLktnZjg3P2wGz3RC/N9hBCHtjMgOxGjDZv3UAkyZim4EHQO4i2j3UkxXUb9kkcrb+pcYgz3aDNvLjOah7BSZvRnwLX/8D2axILM0Dtx9ODgGGt/P4GNgtqD6A764i3+iJ44dC9CD/jaRXyqzXrHs2x/OsAGhrL9sB8W/B7ARKwz/H7TAE2btAZj89DbAW6X4HHRknnzgW7KdU5fsyeMKmp6X+J6BnDEAzhLWXHYz4c3GlG8l2HgL3frDsmWqzR5KDfpnOykkIL8D4OHPecIcI9oy09kxUaut//CKCfp7qtMuwwb8DnrOd5nOcXIfJCryAOgEpASUwh5HiBMwKEAW6YF1chIghthtqL97uSxG5C8a/TXvsBLx9VGa/8Yane6xPK4C2/kd7EuWblIZ+itU+BMx93E1OFmLchqQpTmaDA0kAlyDSwSwizkAFfN84RAfOQASLCVACDVgAESOGDZjmSDwEtYO20bVVaPMCionjiPoe2eXNy56uMT8tAJp9I2X10GfxyQTJ4GtADn3UDd6NYv5niKtgyQxcYjinkCSYi3gHikeSLhDwXjHzTNlWB4hEYnRAgoUSmIIqxAQsYEFQA/JRNHZw+lqiTn90R/VGYuNKQl5j/cTH5JD3FE917E8ZQFv23b0oJf9GNnQd8PFH1y7rkORKLJ2BTxJcAqQOSQyXGEiC+IB4wZxHXMABeIeZQ3q/MBQRhdiDNGqCaMSiYTFBKIiF64FYKBbAigKLD6PFsYjt+uhOcwHdiUMRe5fMe8uSpzL+pwSgrfje+/CyK1n1dcBeW/7wMoZll4Kfhy95SARXMsSDSxxkIInifIK4gHpH6kAlggjOOwTBJEFV8FIQDcQCGIh6ighOFdMELQKYYF1Bg0KgB2ZuxG4kFqvw+cmoDG7V/cXk7Z9iulx2efvX/1wM/iwA7bLLPIe1v4m4RSTJuWDJI/+675OUB3ClCmSC9yA1cKn1dF3mSFLDRJEsAYk9wJxSTEzQGW3TXlEQC6G7sde/0gzDZ0Zlt5Ty9Arp4GBfnBWCgxghOkIhkBsxGK4QYgs0gHZAQxPtNLH41q2GH4jhNFRfwrzK20ROis84gLbkohLp4C/IuAXso1v+UFmLlK4gLc/Bl4AMfEWQDHzZIAFJhKQMLgWzhMayjTz0kyYbrt2T2Nq3a5WFE3HWqgYzNxeU81ao5ADVpJ2ldLI6G6YN+o3zStI+CF+9n1kvWcYub6hR330mIgEtIHSAYFgOVkDREegYmoO2QfOHCJ3X4thqDirn0rUX0Kr9rex/Uv6MAWhLLiqRDPwPafdBxN79SC3yS1y9S5JVoQpSEnylB5wrgSvTAzCt0Vqzmfs/32By8cs2xZ2vXGJHjo/KnlVz1aEsLZsXKVQkpIlXTHo6T8wFjU5iTELULFpEQmN8Gsub87lq2gy/5pUM7ftb9jtjgNJO07DQQHPBerMeYqsnztaGmBvabhNbVYivemRwfJWitADktbL7OztPO4C25KISvnolWWMN8OZHasj+L5Luih9QfAWSmvVEtwquAi4DyWay6aYHuO/8A1phYOkd9sbVTbdgVlYqJVk5wycpWZaRJY7E+2i44L2Y0Zv8CkgMCMQ0xOCKGAndnCJG8m5ueacba7pkw/Pksp2rvrkH+3/kXqY9fx+cbiA0HbEDdIzYBmvRE+12l7y1GRffsRUj/JBubR6xdbQsOK37tAFol13mOXjzzymNrQL+/pGnS1/FZXvg64IfAKkqSTnBVRRXEaQ8g7HFK7jnU/NHde7tC5N3RpKB4WqpKuVq2cpZSpJlkvrMSqVEvE8ty9JClSJJJIr44BwWY0xjNC9iWZ4HH2OUdrsjzpm1Wi3X6RTW7XZpdNviwsToQfodP92tPpj9z1nG8Pxd0M4mYtugEygaGdpWQgO05aCxFI3/+Mhg5WsUQ3vx0Npj5GVnh6cHwCVfv4x0ZAViH9py0WcXIpUDcPVIOiBIFZIauKrgKg6z2Sw8c0m72XK3uA+MuMq06dVqzSqlTGq1uqVp6iuVsqRpGtIsM++ceO8NEQQi0AGmuKAElAEfYxTAQowUeZBu0U3zTkc7nVzzvGOtVpdGqyHa3rTpUPvSjpVyreDgc/dGZB3aVrRlFE3QlmBtoxhXtPUQlr/nEVTceRQzd5c9/+GUpwygLf7Ke0jHykjrS1suavpV0vqeuDqkdUEGlKTsSQYUqcxmcvky7v38c+6yt/xqvHTwvEp90OqVTKrVitVqNcrVasiyTBLnUhEBGAXW9ssqYD2QA1MT3bQP4g7APGBOv0w3M4kxxm5RxG67nUxOTkq7ndPO29YYb9pQ55a1z3U/Opb9P3wXA7vtgbbXow1HbBk6aYSGoA2hmFgK+ggnxoEPYENR5r/3y382gHb/xQeQFm/Gr/0ImOuD9zXS6p4kg4ofBKkJyYD1gCzty9IfXlVsumuXm7NPrC6Xh6sDA1XJyhWmDQ1ampa0XM689z4FNgP3A0uBdr8vSk/veXpceFj/9y39/6ccAq7/vQLsCewHDMUYQ7fbtWaz6VqtXJutCWt3OtJubBo7rPjMXsmMA5cz/60vJ3TuJzQEm1TiREJsRsKEElqrcEWfE0UJcz4P2Q9kwfvv/ZMBtD98I6XW+QXZkj0Q9uzdLZcgA8OkdYdMF5KakgwK1AWfPof7Lr52vJk27qufVq0PDKTltOoGBqo2OFi3crmszrkB4CHgAXpc5vpgSf/7MFAD9gVe0AcHYAlwK3Af0AQm+gDrVmV2H8i5ZtZpt9s2MdGwRqNJq9WUVqsZ9m1f1BqqxAr7v++laH43sWHEhhAaRhjzMBkJExNgJ/XhWUK+YDVx9FWy/9nbnN4k27oIQK39FUqLFmLhlX1beB+U67jMIRlIGiF1kApen8PCzy0cYZ91Dw2/dadp9ZqrVWtFrVZLa7UyWZaVRGQc+D2wiR73zARKqhqccwqIquKcmzSz14rImUC135uWmZ0nIrf0fw+oqjjnhJ54d4AGPU6dJSL7VqvVoSRJOlmWSKmU+KxSssVjH6zOa/5g8453nn8bzz3tEHzpDrTrECeQKnjBJSmhdQ+izwEWkNx/Oez9ReB924Jpmxxo91+wl0rjjc4vO7d3wQKu8kP84CyyIUHqgh82/ICQVA7k3m9evybstXb98FtnD9Xrrj5Qp5RlWq/Xnfd+AFgILANSVfVAHZjVf4E1EZlhZs8VEWdm3xCRD/QBfqSjIhtU9SIR+XszUxG5T0Q2quokPV25EZjsv4wA7AI838wajUaDZrujk+NjyWSzyZzJ/3pwTnrffPb5u79B2wsJEylxUonjkTAhxPENaOcURBIADfM/6bT+H7L/6Uv/KIBmiN1z/tWS3TqESM81JMlXSQb3QIYgGwQ/CH5QoDaflT+7cXNjYGLl4Lt3qA/W3WC9rtVqxdfr9QxIVPVeYKNzzoCOqjpVLSdJUg0huCRJusC4qp5FT7wPAlYCP1XVkf5zs4DXAbsCtwN7OOfO7nNiRk+EWzHGwntvzrlSn0N3APYzszjZbE50Wu1sfGKcZqNtu05+ffO0aitlj+NeSphcTGyATkCcgDhmhInlWJziutst/E1D9v2nIx/rmH08gPd+7ijSpc/BRnpW1+RWXGUd6WCGH+5xXjJo+MFhRpcu6q6+a2jJzHO65UpdBgeqbmBgwMrlciYiqqqLnXOFqk7vc4WPMRpQTZJkB1U9FEhF5Fwzu9DMbnfO/VBEXqyqrwGmHKW5c+4XZnatqr5dRA7y3p+uqh8DVFVvE5H1QAtwIhLo6dSNzrkKsEeMMQ0h5GNjY9rpFH5iYizutemTrjTvuS0G91iANcYJE544HoljQpyMxOZcsAN7SM3+AHHPhbLvP/9ua7zc47jP7OPEhz+KdcA64MIdSJKBBxFFpPcGigmx1Tc/f9G0syZrtZofHKi5en3AyuVy0gfveufc5qIo6mZWAWaZ2Rzvfdl7/3AI4SozGzCztqqeG0L4ELCPql4QYzzezFRVH1DVB8xMY4zHq+qFwHwzOyOEcF6Msamqg865K2KMK0XE05vaTDOzUgihqqojqnqD9z5kWZYMDAwmpVJGvT4gS6af1baHbzyY0FQwD67nzBVngMNz4xYcbOXHLMRzzEy2CyB3nfdicQ/8FOvMRrsQu78A2xWJYCaY6zGwxf1Y+quwbNrp19QHB6u1Wo16vUalUk5EZGdVXaiqQ3meO+/9BhFZrqqr6OnAE83sH/ttV4HvAB3n3Plmttg5d6Zz7sNm9i0zu69fvqWqH3bOnWlmy83sAhFpiMh/0JtgO+/9e1X1TTHGATNbb2YPOefGVbUEDKvqQmBOqZS5wcE6lXpdqgODlWXTP/Iblv48RePeOOt5wrUvpZrvDt3/7WMxS9zi/+aezx2+NWSPssImeqbY4j23XJDSeszvBAJODFHBWcrIA1c1sr1zN7DHjqVKhUqlQqlUcsA8M3tQVV8MNEVkjqoGEVkLPKCqVwEvA3Iz+yDwG+BMEfmtmV0hIifHGM81e3T8Z+p3jBHgTu/9h4qiOM4591ERuTKEcIb0JCMTkSuAGWa2v3Nujpk5MxsFEhFZ7ZybWyqVHq6pOrQaJuPOcxvNve6sjy2+l+Gdd8EkIAJ9LFHWYFMLokXvN9vzQWCLE2ILgPbA53bS7qrfi3WP7l+6GvxcJPRG4Kwn5LE1l/FVu63e4fM31CsV6pWKZVlm3vshVb0S2BBjTAG890PAAar6ahE5EjjPzOohhLO89xeaWQ6caWZnAS/vA3Wlqv5eRFqPAbEmIkeIyCtCCAeKSEdEPqaqfw/MVdXTReRCEXHAe4B6COEqEbk7y7LNIQRCCEWaprO894eWsmyTxuhDCG7NzH8s77X+jBcyML2AsBIxQ0xwKgTdCcl/h9kRwAJh/fds4blz5aAzVz+aA7vtE527fi7WXz87/wCwO+ZAVIgFuODZuOqmkfrrJirV+k5ZkkiWZWRZtqOZbYgxHglUnXOJiKwMIdyRZdnVIYTXmdl6EXm7qq52zn1BRD6gqv8CnAtc65z7sarua2avF5GjeQyJCH3R/IaZLXLOvSHGeB498f+AmV2oqqu89/8nxjgmIrPSNL0qhDAjhPBiM9sdiCGEpvd+JE3TOTHGDZVyOSpUNtVe/fMZI9cNMn32LliMmBnmIs4ghnshHtHrybU7Iq9/A3DRFgDNEL1l/U6uUhzRN8wbUD+vF8PAepecoHEandburbkvv7mSpZTLFSuXywnQCSFc1xezIefcc83sOOCQoig+18fgy2Z2gZl92cyON7MvAb9wzl2vqqfHGKfW2rmqLnTOPaiqK1XVJUkyD9iD3grlPX0wNwIfU9WXmNmXzGyVmf1cRN7rnDtdVS+MMTpVfTcwA/gVsFBExsyscM4dVy6XnRmxiGqTw6/cYcbqKw9nmo6CbEREURFIDPW7IGETyAwIL9PO+oZZ7516gLOP/budTZfuIXp3FT89Q9yvcaUhpNTzKLuy4TJlcmLZWPqi+2P9wIFqtUq5XDLv/d5mdqeqntLXQaNm9gBwrIisMbN5fZ10sHPuJ8C7gbu9919X1XeZ2dH0ph8Xq+p/A8tFZJr1RObFIvICM9vVzB4Efq6ql5rZXSLyfOBvRWRXVf2EiMxxzh3vnPt2jPEdgJjZcF+kZznnvuGcK6vqwar6ehG5XUSeZ6YbBcOMxIrx20v5is2UXA0NPYesRgfqIf4Bs5l0H/yDWHUZq45eec63/jDpALTg1c7dNouox9Nafh1KgKRnrhWPakKINZrtlzSnv7ZWq5UlTb1kWVYG1hZFsczMFpnZ61X1VBFJrfeKvm1mL+nruLkhhKPN7MNmdngI4Twzu8Y59wHgWjP7sIhcaGbvV9WXqGpZVVv9UlXVl6rqaX0996GpZ/v68jwze4GZfTiE8BpgjpmdZWYvCSF818wwszSE8E4zO9HMVqnqMhFZm2VZ4n0qWVay1ow31Gg0DydaBTMH3qHiEGeoy2kuv4YY3wy3zUL1WOjLq133ritxP3w+2HSgQTrwc8p7DeBqDl/ueVxi1o7Nimza5VNFqVqlkmWSpulBqvrrT3/603bTTTed6pwrPVZ3/TWRqnZf/epX3/ye97znZzHGwVKpNEtEXhVCuL3b7dLpdGzmio9XZKBb4PIKoQXSNEKrS3dJh2LytfRiFqMWTvmDe+m3X5WYne30+kUbnFkvCC38L0U+HdZ2KO9uSMxw0Wh3xyanvXHCe79TImLee8ys1O125998881HOec46aSTfrd06dIlCxYsWHDvvfcu2n///fdutVqNPM8LYNqvf/3rI733d5xwwgnjU4MaGRnZuHHjxk2zZ8/eYfr06dMvvfTS/VV12pve9KbrZWrSvhWJCHfeeWdz8eLFr5kxY8YtL3/5y1t9cOyBBx5YMn/+/N2yLMtGR0c3XXHFFUclSTI8MDBw1THHHON/+ctf2hVXXPGyer1+/ymnnHJkURSrsiwrpWlqeZ6Lc07Gh49bOty4ZJB6UcXFnh+gvcbQfAbGlcDrwaabdcfMznYJt66Yha2+A3hLr4tuBGQIbWe0V7Yp7xlJY5mQPjfUD16YJok453DOzVTVmycnJ1/kvXdFUXROOumkF8cYJ0XkkBNPPPEgM/secBpwu4hc8etf//rI4447bvyEE07YDNwAvA04cqt535L/+Z//WTk5OZmedNJJP6PnsgJIY4xVYHfgqGOPPbZ26qmn8sY3vjE/4ogjqsB/xhhf6r1/N4D3/lxVPfG6667b0O12hy+++OJ9yuXywAknnHD9qaeeuujHP/7x604++eQlIvJKVf2ZiMzy3m/IshJh2iE1Ri/dn8I2o/lGOg8NQLeKCuDWYr04l0tX38rNuoOjU+zpdHHSW2EAKiWE0PO2FTW6K0qE8fWmyXBWqdfTNLM0TUVV91fVDVtzhqp+xjl3HHC+mVXN7FQz+5CZHaSqHwVYt27dGjN7jZmdr6r7hBC+r6qnq+q5ZrYsy7LEOSchhAtCCF/ql/PN7BwzO8HM/tBut7/Sr2uVqh5iZuc7514FnA98N8Z4ppnNz/M8AmRZNg04X0SOPeWUU64WkTkrVqy4sq8bVwP7eO/FewdpdQjxMwgTG2g9tAMxr6BqGBGTdAtO4QFH7ua7qLoXrjHvEQBtGmopph6LghZCc023M/yCr5mZgk2tCBpm1th61aCqJ5vZ9WZ2DvAZoGlm58cY/11V/y/ATjvtNBe4S1VPB74FvBG4EDhTVY9xzomqmpn9j6r+SFV/FGP8jZnd1Tcmx1er1dMAhoaGZqvqN/ov4D5V/ZCqvkNVvxljvDTLMm9mOOcuNLNPm9mNr3zlK1+oqvHKK698oZmpmY2LyIRzzkTEvEinW33ORbTXdqEbUQUjwUiINn0LTtaYi9meiWg8ACkO6GPQQaRCtBwfCyIlEEOHO6jf1bmkSJIkAENmtjaE8KrR0dHvAh/pc+FewMMxxt+IyDkicq2q/peIvK//tlm+fPnyGOMxwIV9Sz1mZpc75xYCRbPZfDO9KcjuZlYDMLMNZrZSRH6rqqsnJiYAzldVAd7br2d1COHMNE33VtW/FxFijIv6n2c6537rnFNVPRxYd9ddd80xs/NDCMc659aratk5twFI1dUXEJM2lnchRlwUMI+TKpCjZEjYT0PsJBJ1fxwHA2ByD6KG8wENAWcZBYKUM4b2ayeJK8eolqZJbma5qh5YrVbX98FLb7jhhi8fcsghpyZJsjbG+Enn3Dn9acyXVRURef+ee+65B70A0b/EGDc5514A/G0I4c0A1Wr1rna7japuvadwd9VHtkEPDAxM9EV4tYicEULY0Xv/9yJyboyRGOMX77vvPpfn+SnOObrd7qdKpdJbzGxP4JMzZsw4ft26dTNijEc65w4ErnbOdfO8GEqSxJLpB25ktFzDaUSDoNERDEQdjvsxDgQ7VFRDglgZo95TZPogJlUsRpCIRkEAqQ672ryuiQQRSVV1DzObEJH/k2XZJ/uK21988cXv74vyHqr6MTMbT9O0VSqVjqHn9OTyyy/fddWqVfckSXJ4lmU2PDxMjPFmVV3TbDZHNm3a9Gag+pOf/OSHSZL4iYmJep7n3YmJCdm8eXPsdDph06ZNOwMvufLKK1/5+9///tA8z2tFUQB0RCTz3n8QwLmesynP83enaTrovf+AmV04f/7862666aY9RWSWiLwjxniqiOyRJH5pURQastkxteowRW6g1tsVZgZiRHsIOBBjEI2VBNXe3kUAtSaQoma4QsEJFoQ0rbq0hjmX9yTKhoHRWq32yRUrVnyn2Wx+qlqt0g9R4pyT/pywBAx1u48E+WOMu91yyy27sR1Kkt7y/PLLL3/L9u6ZIhGZ3e12Z2/93LZo2bJl//2c5zxnLvAl4L4sy+7tdDoHxxg/MzAw8KlOp7PUzIaT3to+NwYMyUpYXiBqRAOHEdWDTiJTLkHFoYVsUYxCCwgQlRiFEAQtDEmHVBLnvS9UNdCLV7SA3efOnfuqkZER1qxZQ6PR2OJ6+muiJEkOFZHXmdmFIvK5I488UkZGRkoDAwOvA3YVkWY/LlOYmSVpDciGevtrQi/Or7FvPGg/YnCDc72NnvRKdEqkidDEaKK2DmMN4hLvk2az2eyISINe7GIDgPd+N4CiKNi4cSMPP/wwo6Oj9EXqr4IWLVp0D/B3RVG8Kc/z7+y9995Lgc7atWt9/5YNwKoYY8PMmnnezXGuRKBFpAk0UBqYTqDoFrxMSTDskTCJ1VCGCQgmZg4vZoZnMoa8UqkMls0siTHuY2YPbauzMUYmJiaYmJggyzJqtRq1Wu0JReyZoKIoaLVaNJtNfvCDH5RGRkZeOmfOnN/ssMMOyxcsWPBGEVl37bXXPnDyySc/L8Y4R0T2EZGFIhLE2ySiE5jMwBCi9QL+Ig7byotvWALxkXi/yRCQRMU5jFhYCXGaxDgeuk2DctL39TXpBcCfkPI8J89zNm/eTJqmU55rSqXS0w5oCIH+epZOp/MoCdiwYcPJ3//+9x/3zNe//vWFJ598MmY2ADRU1RVF4V0+TmahEYxpBBVBAog6ByJWe4ThIglqG3CyCmwepkNmKIZEwUV1DlTF8gZx3GBGGmN0fZ3x+B34j9Bm59xD9BjdqequRVEMbz2oJElI05QkSciyjCRJcM6RJAkissWCAqgqU/NIVSWEQFEU9L3M5Hk+NbnfFq1yzo1OratjjPvTC8azadOmV/bvmWNmDeecxBgTjZMSi9CIQYcR5zykeFUiINQQAZEHzeLaxDTeLUYKzEPcAWLcY6ZmipppjMFI8tEGzeXW9TtnWZYCrFfVA+jtBngUOefuEhEdHh5ePnPmTN2wYYMbHx8HWNV/BmDL4J9BUu/9LWaWzJ49e/nAwIB7+OGHa51OR60XtdsdyNevX39zrVY72Dl3dwghiTGaH1+UabGpFQszEg3OgSgizjnE9u8bkdscdqczC/ejyeL+Mm6WYQ3MopmoBefMJOk21laTxuKKmbrY83aPmlkSQrgeoF6v/wxARO4WkearXvWqHdI0PcE5d2KpVDrhqKOOmiEiLefcfc8kYluT9/7mUqk0/qIXvWh/7/1JaZqeOG3atGOPPfbYrohMAmumTZt23ZIlSxaaWaqqm83MVFVKnQdrne66ajRJXHSiEcQkGjaO0lvOkSxB7QHnE1lCHFyLWc+3H2l5xKtapqYuRpNue6ySFSsXhKCxv05tAHS73dWPEZ3k2GOPnT4yMjL3C1/4AqVSic9+9rOMjIzsfNRRRw3S24X1bJysXGlmpec973kvnT59euUTn/gE8+bN4/TTT+fee+89/MADD1zhnFvfaDRmttvtrK8eGj3VECzLVy0IjfGKRiOqOtRSU0vFaE3hRJi2nsKWJ2xuL9fa7Nc7t7HXtLNGjOwgaoWpYEoSY3emaPeAPG//CkoVEcmdc4QQ5l166aVXNxqNI5MkuQ6we++99yWHHXYYt912G7vvvjs33XQTCxYs4NZbb91XRK53zt1mZjOfcPhPkURk0+zZsx9cuXLlwcPDw6xcuZKhoSHuuusuDj30UFqt1jELFy5cVhTFXO990teteVEUxJi3he4+MXYjgDnUjAg4orWm9nJo2GGmm2bLEnnrzRP6k8MPe8QS41EwkwTFFIsaRIr25qt8vmY8uHlV770lSbIaOOKFL3zh/y5evPhKgOc+97nV733vexx++OEsXbqUiYkJ5s2bx2GHHcbvfvc7jj322MV33nnnI6HUp2nSLfL4PVJHH3307BtuuIHjjz+eK664gvHxcV7xilcwOTnJ9ddfnx1wwAE3NZtNt+uuu74GeKgoCosxGq1VY3lr9CoN7CjO1KI4ExEUxZNN4SSUXiwvu+YT/bCbbRLldoSDMTnSO1seogU1cTEXb2YyvvaOscHB31dG0zdrjFG893cDx+yyyy4bP/OZz5yqqqgqt9xyCz/72c94xzveQQiBgw46iO9973ssWLCA973vfe+cum/Kom5tXacAfeznFEBbfzrnEJFHfe87erdY8F/96lf86Ec/4rTTTuOaa66h0WhwzTXXcPDBB/O2t73tnd1u99uqOlNVfwVYu92VOe3rapMjCyei2lwfxXDOnMTgRQSTl4OB8QczVkJvcyPnvGnuJDI+ihWvAKaJ2kIzqmoiqhBUpNMZr8/ace8j1snzljrnvHMud87tF2Ospmk6bGZOVVmwYAGXXHIJeZ6zdOlS1q9fzx133MEZZ5xBrVZ7FHjbA3Lr348t2+Pex3KhiDBnzhyuuOIKGo0Gq1atYs2aNaxfv54PV+O33AAAECZJREFUfehDJEkSut1uDZihqre3Wm2X5+3unPy3x2548Kpu6sV7QROPpIL3XkYxDu8teev/Kjrjl+dctnpF71W1uFnjzvVHnIV+rSBCRDDBDDWl3GmM/LrUWTbS6XQkxqiqehuwT7fb/Y+pAe68885cfPHFpGmKc45SqcQFF1zAnDlzHgfSE5W+W2pLeTLPPBbsww47jLPOOot2u02e5+y1115cdNFFZFlGv8/7qOqt3W5Xut3cyvmDGzutkSs0UlLFMHEoiIlhbJjCR8PcIWLj1p4o90kvPegaSe/bC2wOcLsZY50CaRdYNzfJI6gbWL3LIe+euyh9+4ZSqSKDg3UnIieq6n3lcnm3vsf4cQP+YwA8lgufSISfSGwfW7z32/s93ul0NojIfFX9z0ajRbPZtP35wZyVt/zbKomTcyspVkqFSoaWUkSEGcCBmKy2uN9id9LCl8NWu7NU9UKsdHEf5YMFNgjgpLcBFpCiPbkTYbKgvXp9nnes3W6rmd0I7Nduty/dlsg9FpjtgbctDvxjL2B79W2v7anS6XQuN7MFwPV5nlur1RLXfXi9FRPtojM5B3D9bXwighNYh3Fgb65culgtfmEKty0A+qHWFZrvNG/LPEddI3WGiLkE8JiKt/TB2340viD9/X7tdtva7bZ18/xhM5swszfGGK/bnu7a3oC2J7pP9P2JxPaJ9KeZEUL4dYzxFBEZy/N89cTEpO902rpP6boDlt98adNjPsE0wSQRIxEDle4ULhp3nu/Xt696HIDy6qVd1Asql/ZMdXy7F5ssiUURvDcRr1i3NT5DWxsmKvnitZONhms1m1oUxdVmVu92u0NmNj4lftsb2JPVgU9WBTwWzG2B2O/ThjzPZ5tZmuf5NZONhmu22jpkS9fE9sho0R4fRhDvRRMxSxLDY2OIvq0nmXIJKm05bWn3cQACOHFna5hzX1+MM8ytS5yId+AEBMErsui671b3Gbjn2G6n02w0GrRarY6ZXQUc0Ol0rrZetOsJOWFbYG5Ld/4xffpk2zGzTp7nd5rZc4HfdDqdvNloWrc12dqrfs+rF1373YokvU2ECeC9+FTEsGQjPbcfxLlLXJJ/+lGYPcr0n3LPerS8D/DbHhfaWxOxsVKCeYdkHhMvRAvDS2/58Y0H1X9fGZ9o0Gw26Xa7m+htAH99nuc/2NoIbP35ZET6sRZ4ayCfTB3barvb7f48xvhK4A+NRmt0YmLSJpsNe/70m+tLbrrsRrUwLe0fB08F6Z2rtzGI7+jpPq6MoTRfTlo6sl0AATq+/U+az76h/1AVlZg6o5xg3uNSj6WefHz9kj1orrCdSsvWTUxMyOjoZtrt9lLgTjN7ewjhB8CfxYl/zpTlCUpeFMUlZnYicHur1Vo+OTnpGo0GOyVLVkvzIR1bt3yPzBNTJ5Y5LHGQelPE5T1JBHTObb7onvFYvB4HYO3kVWuIpRSTb/aXLSd6WJQ6c5nHSg4p9xqS+67+9tCe9QcPk+66NZOTEzI6Okqz1VpsZjer6luLovjZ1jrxyXLlE80Dnwy3TX0C62KMv1PVk4GbGo32srGxMRkd22x0143sOfTQYXf+5t/qpQQyJ5qmWOKQLDHxxiLM3tTXfV/TIknlnSselxXpcQACuEp+Tsx3HERp9Pz/7kWZp1tySEkg8bjUiROscsvln1v7kl2Wvrzb2LhhbGxSxsfGtNlsPqSqV5jZ60IIK1X1uq3r/1PB/BNBm6Lr8zzfqKqvUNX/nZxsPjw2ttmNj08Smps2vnju4iNv+cnn1qVi1VRESg5KhpQTXObIUXdEP/bRiPnsHZzaJ7aJ1bYuykkPt73xbbR+Zu8N2P6ibqycQrmMlZ1ZKYVyIi6VOHzzjz9/99ELlh8dWhvXjI6OsmnTJhsbG5sIIfwXsIOqHhRj/Da9I1mPom0BsD0An+iZrWg8hPDdEMLzgel5nv98fHx8cnRs1MbHN2unuWHkmL0ffMXNl3/+LtEwVErFlRJcmpiVykgpRcXcJrB9eqJbP9OL/5q8c8U2T7E/4WnN8J2df+iTtQKc3If7KyGyf7ONNbqUOoXQbFtoBaJKZc3hJ33igF89sOPVOcNzhoYGQpqWy/V6RUul8qCZHqGqS4CFMcZTVHvO2a0t67asLvC41cTUiuIxn+qc+w/ghc65nUTkd3mej7dardhqtbLJZlMzHXv41c/Z8MqbL/nMIrHG9EqCr1TEVVOjVibUM0g89wFTx14viWHHkLxz9du3h5Hf3h8A57x++i9jrLzV+ZZgzACe6+B3HqYhaIw4J+JMkbwIQyvuvHryZS9//i6tXB9YuSFMC0Xs5W2KoeVElgCY2dFmttjMfk7v8M1g//qWds1sm56XKRAfc20N8J/0gvgvBO4piuKOZrMZx8YmmZycYPPmUXYb3LT+pXtu+ptrf/iJEbHujEpKUstEaplRKxNrKaTCCoR3AB6RJTGfPekle/s5Px3bbuzhjx64bn9tx92yrPigS8b+AcgQNqP8ohuZ28zxrQ7SynHNDtIulE5wxaEn/PP6WJnDT24faJfSSlKtlsvV6oCVKxmlXgCpDOypqrur6m/NbF2Mcb6qvlh7Z+keJbZbr3+dc8E5d4OILPXe7ygiR3rvV5jZgzHGVp7ntNtt2p3cmq2m73ZbjeOfN1nz3TXx1h+fv2PJaVYtOStnSK2C1lJirUxeSlmPcQwwo7e9b+gifPlf5e1rthm+fdIAAoRvzXytSHcv51rn959aCdzcDcxsdPCtHNfuIO0ca+UaWwEGZu+16pDj3vc3Ny1Nfr1oXW1WuVz2pSyxUqki5XK2dSQuU9VhM9vFzKap6gozW2NmDVUt+qcvMxGpi8iOwK5JkkyIyEOqOm5mRVC1kOd0Ojndbod2u0On0427zypWHLlv53X3XfWD69ctv3V+NcPXMmeVBKplQqWEq5eQUsZalBfSOw2PhuqHTct3J+8e+dUfw+bJZ+345vR3kbTrkE8dR1iF8LsisGujQ2xFrNUmtrvUmwXdboHP1RUvPOmfNyYDO02//BbuGu9k8yqlMlk5w7uEUlYy55A0TcQliTkRw0xxHouxd2Cot3PT+kcbxEzEQIIG0SISo1qet6UoAt08aLvb1uGyrj/hMD0gTK7ZeP2PPj+zlGhazoiVBK1lZJUSRb2CVlMk9ayldzJ+DwBi9gG01pV3b3xS2Yz+pLwx8RvDn3RZdwzrgyhsxvhJMPZq52izLVmzwFq5SbdLaEeNndyZlOutw97wwU5anz39ZzeHO9dPMCvxaZp4j08z0iQlSz1Kz/XhxHdxRFR7ESvnPIqPUTPV6LwX63aDKLEXEy6ChVDkc4Z15G8Pyw4qJtZvvOm/v1ixTqNaTpRSySXVFF/NROslpJwY1RKWeBZjnIAw1BtP9gGKclXevfmzTxaTPzlzUfzawBnOhwSXn9dPDpZj9i1DDmp2oR0Iza5qJzhrFfhuR5NOQdENTpPKwNiBx5w6MXOXfY9YtiZcef097e7IhE2XLMlSvDjn8IngncfMgllvQ7JzIoqkGsRUC4kWpCiCodadPuw2vXi/cmXBTslRIyvvvfauX/37YNGdnFbNVDJHUi67WMmIlVSpJs5XMqRWwouzuzB5J5BhqMbkDGflivzD+JMG788CECB8tfZO8cVOzsd/4pF8pz9CGOjm1DoB1+yStgq1dk6RF851g/puTtE10iLSGNpx95EDjjrFTZu928taOXcvebizfMWabvfhTV3f6UI3qqC9o6WC0ywVyiXYeUYp7rZTWpq/c3WPWsYBm9c9ePU9v7lEx9Y/uEOaUMs8oZSQVTJXZF6tZ22dK2eESkYsJf2NU0I/LwKjhPSLEb8i+YfmD/5ULP7s7G321cphEXuv98XeiL2gf3kxwu8N269VENtdF9sFaadQ6/aSDaXdSMgLfKFoEUhiJCcpjw7OnDu+497P1xk77pqV6tNr5drAoE/THUSchLy7odOcnOg2Rpub1q3M1y26zU1sXD1E6ExPElLv0MzjspRQ8iSlhKKSkpRTJ6WUolZSygmZiNyH8VKmMs2Z3BhDtlyRf83e17r1z8HhKaW/m/jywIy65N+XJP4B9JGljsiPMTSiO3cCRTt32im01A3keeF8oap57nxhWsRA0lEwJQQlMcNCz502ldqk109BEwERJHEEcSQlB6knJN6lmdOYlpzLnMZKQpZlrltJ1ZVLpB63CiPF7PgtfTT3KYv+b0TKb5F/HN/852Lw1BMwXobX9dmZmJrL9K3Agv5fOSr/jmdGVJ2bF3Q76nxRqHZy52LUmCsuj06jqu8qgjmiEtTUmEqF0vumgDlx4h2Jd2qJgHcuJk592ROT1PlSopomzpW9xiwl896tQumAvZlH0gcs1sJd4oTAxnCenP3Udko8bTlU7SvMN/NfFW9XAJ9iKmVJL/vkDzEM0V0Ldb5bqObRuagaikBWmLMQCKAuGAFDowKO3gpASbwDBJcICeI08SSJU8kceZKSpGCl1EnqNWJuBYLD7C1bsmBCC+MTMcqrfIjvlQ+y/OkY99ObhNaQ+K8ch+OdPnG3KXwco7xVY/eqyY3iqRk6xyJZUFeEqC4oFOYExaKh9JzaPSMiGOLECw6HpKKWeMyLI/XqxVMIbq1Fmk7scIP9txphxymfiUGfD3zL/3/84ulMifzMpEH+Bmls8yaMU8y535rnNOnP8rdqeA3I1eKsbSolB4NRdFgMb0Y0wcR64mXSSzggggeCw40rTIizrqlUwF5msNOj+gArPVykhR6t8P27q1x2yHt42vcdP6OZzO1sXBjmKODDqvxeEjfN4ANP0JlRRG43GBdljN5OWDCrmWNYYAizgw2mP0EdXzJ0s1NeivDZZDNXP1U990T0rOXSty8wsyuchXMrTLjgmWhDjDNQ3a1kfEr+iY3PRBuPa/PZaGSKDKR9AZc47x6KulUuwqelbrnIqe5c+SdOFJ4+HffHaJse6WeKBKwyyVtDoQca/Eb7qbSfarHItVbovpUB3vxsggfPMoAAcjahW+KNVlhDlZVPGcDIWjFbF5U3yTNgJP7oeJ7tBqdo9F84wMOpivwjsmWS+6dSMLUvpsJ3Bz7CdpMkPpP0FwMQYPyznByUHXFy4Z9VgdrpzjE+7aN8+2nu2pOmvyiAAJvO44tmboNh5/1pT8qnPTpj+se3nRjx2aK/OIBmyKbP8FMzN6bY257MM+LkMjGtzQy89pmc4z0ZetaNyGNJBGtv4k2gczVy+5MwHHcRdVoROOkvDR78FQAIMO+LtIm8zYndr8bodqcrRkuwG73jXTudTeuP1/zM019chLemtWdzpCkvir2EZI8jBx9xCTfNOYvfbev/vwT9VXDgFM05m2sMgiinP477lNMd6F8TePBXxoHQW+6tOYsfFsrDpnyof/GiJGHmzp/mrc/2SuOP0bN7CvpJkICZ4+2rjF8G5RcmDDrHvjt7Xv3XBh78lYnwFMnZhOg5SRxF6hmhyUlyNs/o2dj/X9LKT7D/yo+y31+6H09E/w/wHJVcjfUH5AAAAABJRU5ErkJggg==\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHiczZ15vF1Fle9/a1Xtvc98780cEjJCAgkgiUwiIghCgqLIQyK2qI0D2g6PlmfbbaONCmo/habB4dF+tBX0KdC2PFGZB4GEgECYIQmZp5vc+Yx7qFrr/XHODSEkiDKuz6c+Z9+z9zlV9T2ratWwal3C6yh64YW8Y8Ex4431M4yhOQAtVMUBTOgRQRGEWvtBlJnRgGJABSthdIWHrIyI1l9y3339F154obxedaDXOsPGr2+anFL2TgXOJKZWEIYP5oslL0z7EtE8Zj4M0O49f5qGofKAF32GRTe1GnWTZOkR5DUi0muC0Nxaete7el/L+rwmALdde+34nOcPgei0ILK/j4rlLmbztyBMfkUyUGwT8f+ZNGojWeLeBchvjOR+Xvngqf2vyPe/iLxqABWg/p/+YqFR+WYQREsLXeUuMH8WQPhq5dmRVMRfUR8eqquTt7Din7rOOXsFAfpqZPaqABy88spjlPhfi8XytSbKfZxAB+3l0ZSZ7hXD6wkWCAggClURGWJSggUAUjivokRIoJpq6gkkyl5miOJYqNq9VO6xer3+UxfHp4P9l8Z+8pPLXum6vqIAhy+/fLYXXFksdd1go9wXQZjyggyJH1HDDyEIQ1iawMZAjAWTsWS55QHHbEREGQwPABAYZhLxzjDBIpOcQBx7BwhUvOtDkiXk/WGqcugeirbJxc1LGvXaqY5x7sTPf37NK1XnVwTgg1deGcwcHvkWOKpXuiqLiPjI3XIZJBv91lub59CORWBVAysmDKyQ8QgsQGSNsakaUhAYIAPqlE+hgHpRVeMB730IFcfOK8RbSVPHTghZCsn8IJI0hk/fA8WYXYuhovfVa8O3eudy69eWLzjsP87NXm7dXzbA6oXfnJNBflwaM+4uJrpgt6/fTlFwC+dyY7w1Fvk8KAwYHChHRsQGAVvrEBgSsGE2DtYATAwFE4MBQAUCgkBU4D3EewtRD3WK1FmkzsE7gstI00zQaoEynyFNNkucLCZg+q6lUpVLRoYGj1KWv53wla+sfjn1f1kA+750wblBYGcXunpOJcUBu3zrsIbBryhfnGaiyEghIoSRahiAcgEjyCkCqxwEFsY4BCHBEAnIIzAEsGEGRDVgYgXUiQAMcfAeEDXwqshSFe8t0syxcyRZTIgz0TSDacYkaapoNb2kySaK07MAVEaLqIRnGkNDv3ferR7/rxdd+ZoC1Asv5L6R+k9yudJTuXz+YgA7O3EOwquQjyoo5POI8oRCBC7m1YcRKIgIUUgcRSqhVUSRARtvrGEPkiRuVBsDw83B3m3Ot2KqDQ8TAJR7utXkcjpm4qSgOK4nn88XK1BlOC8iziBOPWeOkCTkk0SROqU0Jm00QEkKtFqqzVZLG406vHxol6q4pNn6XBw3jh23Zf3ZdN11/lUHuPpzn4t6Mr2h3DX2T8T85eeoYjuKuT9QobwPFfPQYp4oX4Tmc0AxD+RyanIBxIZk8hE8sx3cuLnv4ZtubD774MOz01bzQI4KjwTdXRt57NhhDYO00FXOAKA5UgsozUIZGOjOhkemSdI8NMwXnt7vsIVrDl20uDhu2tRxRtT5OCZOUvFJqhTHhFYMbbSIkpai3oSvN0Ct1lY0mqeAMHFn0UUuqo0MHDkU0Kn7X3FF8qoBXL34c1HXxNYNlZ5xawj0qZ03DN3I5UqMYqFgSiX4UhEo5IkKBUUhDxTy4FweEobF4R19g7f+6D8a29etPz6cPPGWniOOHM5Nn1qKyuUKk+UgCMSwigoLMwQARNr9ocucEUC9TzWu1WqNDRvrw8sf6HHbd7xz0uxZd5z0iY+VK+Mm9HCa1aXZJG01iZJYfbVJHDfV1BvwtQbQbDSlVsvB6+JdQHxvZLDvwDq5d8/86U/jVxygfu5zUV//4I1dXeN7QXTWzvdN+GNTKU5DpSJUKYKLRaBShi8UCaWCUr4Ijuy4NY8/9syNP/rPg6mYf3bSu9+zpTJj3/FhaG0YRWRsiCC0yFkLgAWgzFgSMkYBQL0n74iZJRDxnDiHLM3Ue4eklVCaZVlt3fr+3j/8YYrWG7NOOfeTT86cP+8AybIdaLRI63Uy9ZZKowodqZOv10G1WiLVxiD77CO7VPPqkaG+aSMjAyfvf+ONL0kTXxLAa9//fvN2p7+rlMdsYDbn7oQXhj+kSnkmd5cJlS5QpSwoly2KBUGpRFTIj92+YcP663/4g/2CSZMfnnrG6T6sdHUXopByuYKGoaEwDBGEIaIwJGOMhGHovKfEGPVBEGUAkGVJoEqGCGGaJoH3npPUiXcpxXGKJEmRpC3EcapxbWR463X/bZLebQvf95nPrpm4777TpNUYQD1WrtWdr9dCDI8IqjVItcpUra3WNPvMzjqpfH9kZOCACQGd/FL6xD8LUAHaccq7flEuj9/ARP+480YuvIzK3fO4uyzo6iLT1QXpKkMrZZhSyWTQiddd/r1n62lMsz/+sb6ouzKmmC+gWCxImMtTFAQmn89REEQuzIXKABtjhIgUAAPIOgkAAnQMlarCe8+i6tMkYeecbSapxs2WZGmszWaCRquOWt/g0Iaf/WxiJcxl7//8Z+ayoldrdaFaXTEyDD9cI1NtiBseUNtobHDN+FPP1Vkuqg4Pzph40w1nv2yAW4874dxKqZyzJnfZ6Hucz31fyuX9TM9YoKdC6KkIlbuYuyuiheLEbZu3rL3h5z87aNJp77lp7PyDpuZLZZTyEQqFvBaLRQRRzufzeVimoANsAMB2AFsAbAbQByDZDWAEYAKAKQD2ATAZwBhVhXMuSZ3nZrNhm/U6teIUraSl1ZGGDj3xWO/263+76D0f/chjEydPmaWN+nYdqTKGa0B1WGRomDA8QjpYfRZZ/HejdXRZfF6tWfeT77rte381wIHD3zpfQ/s3xfKYL0GVAQA2+iF3lWfpuDFK3V2Erh6Ysd2qlRJToTD3jzffcseza9dMm/s/P7clX6wUKpU8R/mCVkpFLRSKEgTWWmstgEEATwNYC6Cxh3IJgKM6fy9HWyu1c4861wUAswDMA9DtvXdpmvp6vW6arVQazRparRZqQ0Mjq//9+/vvt/9+a4898Z3v0Li1CsNV9cNVz4ODVoeqIsNDHsNDG5G5tiaqukZ96JLM61WT77/nqb8Y4JPz54djw8LN3eVxUxS6PwCA7a+op9JF3V3MEyYQuiqCMV2M7m4gCg/6f7/573uauUJj9t+eXSiXyzaXK3JXuaiFQh6FQsExcxnAJgDPoK1xhHbTpA6g7g6UgwAcDmB2pzhrANwP4KkO7CoA34HoOq9jOp/b13ufJEmi9XpDq9Um4rhOtVrVr/np1Y1CKy68532nH0NZ+gQGhtQPjxCGhlX7Bw2GB70OD4/A65JOvqtGagPbJrK8kx56aI/TPrM3gF8ZO/HfugpdG9W5U+EcyPmnqFioarEYULkCKhWEymVGpUxgc9A11123ws7cb+P+HzprXHdPt+0qV1y5VOCuroqGYRgZYzIADwJ4Fu2mWQHQIyIREUUAciJCRJSo6qeI6NsA7gZwO4ClqvppIrqpU7xIRPJElAfQg/YSWQJgG4AhZh5rrc1HUZgGATMRGRuGKM87yA5v2Tz02N33NOfPPeBQUfSyS1nTDEidauqIUie+Uffk3AQ4NzYy9prBZv307/bt+N1LBrh1zpwDQoTTAzIXtKdO4jSMbuJysULlippKCShXiIolImsPuf6m3y+LDpy7ZeYZp0+qlMvc091FpVJJy+UyBUFQYeanADwCIBWRkIjGiMg+qjqWmaep6nxVPY2IjlLVJ4joMACnAzihkxYR0TCAR1T1M6r6FiIqEVFFRHKqWlFVUlUhohjAWiLKjDEzoyhyxhhlMrCWbGn//UrVoaG1Ty9dGs+fvf+bSWgL0tSqy0BxBmSppSx7FnE8H84zvBzjnLvhf47r2XZpf//AnwWoAFXHTfivclSeR0RTAYA4uJIqpamolJkrJUi5ApTLxLlw9u0P3HdPOnnS0KwzzpjQVSlTsVhAuVxGoVAoMHMOwOMiMkBEkYgAABMRqSqJSAqgn5mfVtVFALYC+Bu0jchVAG4AcAeAJ1R1tqq+T1WfJaJ9mPknABqq6gDEABLvPYjIEFFJRFJVbTDz5CAIDDO1mNmoshRnzOgeWr9+oPfZZ9fOnDr1MFHpJ+cUzqv6lJH5IpL0VqgcAQChCcstFy+6ZKD/Z1/7cwA/PWfeCcWwMMhsP94mqiu4XAJVykzlElG5i6irCCrku9f2bnnk2aGB8rxzz43KlQrKpSKVyxUtFAqWiLyIPKWqCYBJqlpBu5/qEZGJzDybiN6jqkd77+8hoveq6nYi+g4RBar6QQDvBPAOAAuY+W4APyKiBao6A8AtqvpFAAtFBERUZOYKgPGqWlTVHiIa6qRyEAR5a61T9T4IIi4fOCdcd+/SfJGwprvctQ9EEnZi1XmFc6Qu60Ka1gGaDMI+AfFlQ2PG5i7p37F+V168u/YJ9KvWRl/a+UCY+xNyUai5HFMxUuSNIoyQthK6f+XKhQed9/fVUqlIpWKBS6WSz+dzQfurcC+AQe99WVXzqtqtqhNVNSKizap6u4iUVLVFRBc5574IYI6IXOK9f5+qiog8IyLPqKp4798nIpeKyH6qer6IfNN73xCRCjPfCmCt954BdAHoIiIjIkUR6RORZUSURVFki8VykM9HWiyW+KC//3zz/pXPLEySRCm0RiIryAVKuZyafJ45Ch4Y5WBN9EURf7HuZnifp4F/N3f+WwthfoTJvL/9Dt2ihahiKmVoscDIFxWFAlEQzvvtI38anvPpTy/vmjC+p1AqoburolEUBUQ01Tl3D9rW1DNzQ1VrzJyo6j4AFgFYSERLVfXdAC4HsICI3gvgSWPM94joZhFZq6pxpznfraq/Nsb8UUT2AfBhZh4G8FMAxxPRLar6BQBv8t73qeomADuYuQmARKSMdvew0FrTMIYVRAAQFvaf89T9N/x26gGTpkwn7/uQesCnUPHkY99NLlkNYH8AFct6SX+lB5cO9m/eCXZXgF79N4wJp47uvpgo2CxhYYoEgSIMlYKANLB2Ze+W28v7z0m7pk6elM/nUCzkuT20w1Tv/UYAbxWRhqqOrnhscs6tIqJbARyvqomq/j0R3QbgAiK6Q1VvJqKzvPcXqT5//2f0b+89ADxijPmi9/5dAL5MRLc4584H4IkoJKKbiWiMqh4gIlNUNWDmfhExxpgtzDwlDMMtBRHyzvvuqVPGl/af9ejqbVvs3DFjp0kQOAojFROQKQQqLtymSdrmwflPESenAjjxBQDXzZ8/iV1wO4CLAEAJd4kNpyCygAmI2KgGBkjdlCd3bJ+54NOfWFbI5xFFEedzOW+M6QFwFxFtUdXAew9jTMV7fygzv5uZ6977bxNRwXt/gTHmUlV1InIBM38VwDs6oG4RkbuJqLkbxAIRHUdEJzrnDgUQM/OXReQTAKZ0NPBSImIAnwJQAnAnET3mnBs0xkBVM2aeYK09wlo7kM/nDaA44MNnRw9dePFb9ytVMmbaAGtgQqPehIAJ9oFmfwTp20E4gNn+cu3MgybOWvfE9ucBDBJ5X7EY7dynZWOfhjEzjbWkAUOJiRRmxY5t901bfHItly9MyefzUiwW1Vo7SVV3OOcWoz2wtcaYDQBWiMidRHSqqm4jog+LyBZm/i4RnSci3yaii1T1Hmb+tff+QACnEdFJ2E2ICKq6XVWvVNWVzPw/vPff7IA8T1UvFZFNxpgPi8gIgAnOuduDIOgGcKyq7uu9Z+993Vq7PYqifYhou4jXLPOFKSce9/tH7vtTeUHPuGlK7MFWOTAiQQAT8FPe+bcDQCksjvVu5HQAPwQ6RkQB8mtXTwf47e3GQn1gM1WtgbckwqywRN6l4zYn8dsnvfWt46PQahAEFIYhq2rivV8G4Ceq+v9UdbWInOyc+4S1FqoKVf2Bqh6qqr8RkW3e+8tU9XYi+rKqHui9vwzAuWhb6UcA/MY5d7lz7nIAv0F7HNkD4FwiulRV91fVL3vv7/TeXyYiW4jotyJyqKr+H1UFM5Nz7hNEtNg5t0lE7iKiX6vqnUQUW2tNEIQU5iLd9x3vGLclTY7zWdoDqLbrzGBr4GH3VcWAAgDTcbJ29b6jP6xBW99nGJOfEYy0ijSmO1TCLRREPchFZIKANQyFQqPPpunq8C1HPjl+3oHlKMpRsVgQZj5AVR/z3p/V0egBVV0JYDEzbxORfQGMVdXDAPyaiD6JtrH4oYh8TFVPQnscdzkz/5f3fnVnnDiLiBYR0WGqKqq6XkRuNcb8ynv/eGew/W4imi4iXyGiyQBOZ+afiMhH2nqBsdQ2FhONMVeKSAzgMBF5LxGtYOZDiagPquS8mrReWxFv3jw4hrlIaQpJM1LvGN4ZdemDrDRG1qx9VLys+TvJNl8OjFgASGBO6Db58ar+fbxu3dUye78WDBMRVNgAAguHYJ1Lj15w8onLC4UchWGo1tqIiLap6urO2OwMVd3CzFd0xmY/VtVvA/gCgEtFZDERfUlVv+WcewuAW4wxfxCRt6vqlzpGAp0BN9BeUACAQzoJncEyVPW/jTF3O+feTUTfVFUB8CXn3BeIaKL3/nxmvkRV/5GIvq2qARF9QlWnA1gqImuMMbOCIAicc1k+r5h16ruKDz348JGzDe0gIAUzQExEVmBMitVr7obXsyObW5e6+O2Av5oAYC3s78YVxhypinEA1blS/i3PmlnWUtFSPgcuFCjOBc3l3RXz5i9/MQnDkHO5HIVheLCI3P6Nb3xDly9ffg4zR7v3XW8kEZHklFNOuf9jH/vY74IgKFhrJwE40Tn3aJIk2opjevDib+WPGm6lhTTNS6OpiJvQetPrug01P1x9L0ELRNgx0By4byb8aVYBXg+qimIcACj0FsTJOOzojbk40wMSiM90HezwtFNOrhtjJodhKNZaUtVCkiSz7r///hOZmc4888w/rl69etWcOXPmPP300ysPPPDAuc1ms56maQag55ZbbjnOWrvitNNOG2Fuj+H7+vr6+/v7ByZOnDh+zJgxY6655pr5ItKzZMmSezvN73lCRHj00Ucbq1ateteYMWP+dMIJJzQ6cPSZZ55Zvd9++80IwzAcHBwcuPnmm0+w1naXy+XbFy1aZP7whz/ozTfffHylUnnmrLPOeluWZZuCIMgZY5SIyDBj2knvfHbDdb8pz3U+r+otvCayfYeXZnOcQm8BcJoqJgCcKDzbTcCknA0fBnAWAKjyDiWUtZmEfv2mxM6a2VAbdG8L7SGHHzTv0SAIgPZofJyqPtJsNt9sjDFZlsVnnnnmMara6PR3C4noJ6p6HoCHmfmmW2+99bh3v/vdI2ecccYQgGUAzgZw3C7jvjU33HDDxlqtFpx55pk3ABhdUg+89wUAMwGcsHjx4uI555yDM844Izn++OMLAK7z3h9rjPlkB/JFInLm0qVL++I47r7iiisOyOVy5dNPP33pOeecs/K66657z5IlS54mopNV9XcAxllr+7xX2ufNC4sPXH/DvLnS6JPEx1i/UaXZKrR/S9422qtYGz603mUT2AP7Rhxyu89VcOADYnWqHuLSfLpxY5dWa9vZmO6wUCiTseC2+swTkY2jlSciiMjFqvouEbkEQE5VP6mq56vqId77LwPAtm3btqnqqar6HRGZB+BqEfmCiFykqqvCMDRERM65S5xzl3XSd1T1a6p6uqo+2Gw2vwcAvb29G0XkMFX9DjOfDOA7AH6mqhcQ0aw0TT0AhGHYA+A7RLT4Ax/4wB1ENHnDhg23qSpEZDOAedZaMobI5HIltcF4DNd28IZ1OfGuCCiUycP4YJRTyCEE2Jc9zFwY3rf9NqDe9DjhQAUW3oNcipH+HcmEgw/5PhEnDIWIKIC6qtZ362POArCUiP5FRL6lqjVVvURVfy4iPwKAKVOmTAbwsHPuCwB+5Jx7P4BLAVwgIouZmbQtN4jIr0TkV97721T1MREpiMj7SqXS5wGgp6dnkohc2fkBnhKRL3Ys8JUicl0QBKYzhLpMVb8BYOlJJ530NhHxt9xyyzGde0NEVG3rAKVgbk6cP/97IwPb1WWO4T2p91aFAxUaM8rJGp4CmDkWoDcTuON+RqmSlgyJI/Uq3nhKM9SisKX5cJYxrNbamjFmnKpuc869s6+v72cA/rGjhXMAbPbe30ZE/wLgHhG5jog+O6qp69atWy8i7ySiSzuWelhVrxeRx4wxSaPR+EC7K9GZqlrsXO9Q1Q1EdIeIbKnVagDwHVUlAJ9WVRDRJufcBcaYA7335xIRvPerOv3ol9FeFgPaq9a9jz766CTv/XcALGLm7QBCIhokIOJibkY1CJs5X/fshYyqcyQGRBEpUkBDEM0jaGYB7KekC6EEgj7JAOC9aHvnQSAW/cVSuO/cuYn3PmLmnDEm7UzDFlQqlb4OvGDZsmXfO+yww86x1m7z3v8LM39NVd/mvf9+54f77OzZs2eoatF7/x1rba+IHAngFAAf8N6jWCw+2mq1SER29SmcucvQBsVisQ4AW7du3UxE56vqJBE5tzOrEefcZStXruQ0TT/IzGi1Wl/P5/N/Q0SzmfkrY8eOPaO3t3ccgGNEZIGq3sXMKRH1QFXGzp2bbSyWypPTXqgXdd6BvRdVZEr6FBSHMnCYgBMLIA+lCgAo6RZRhFAFeQ8IQSwQR2FPYZ9JW4MgSK21LCJzRaRJRB81xnwNAIwx5oorrvhspynPEpF/BlC11jaiKDoZnd73+uuvn7Fp06bHrLVHhGGo3d3dEJGHsizbGsfxjoGBgTMBFK6//vqrjTFBtVottVqtuNFooL+/X7Msc0NDQ1MBvO3WW289aenSpUfEcVzy3ouIxEQUGmPOA4BRS++c+6SIVIwx53nvL91vv/3uXb58+SwAE4jooyJyjqrOtNauIiJXnLoPJ8Woy2cZwYuyqIoqoEgFWMvAoarUDaBgAfCo96sqWiA15L1CmMApICDJh/l8qcTGGFFVj/aUaqRYLP7L2rVrf9xoNL5eLBZ3aggzEzOHaO9VVJLkuU1+7/2MBx54YAb2Isa0V9h+85vf/Pk9WaKJrVZr4iisUWC7y9q1a389b968aQAuA/CMtfapOI4XeO8vLpfLX4vjeA3aa4gCQE0QqURRDmlKgEK8AAqCihK0iueGV8yA2tGOkYAUIIEoqfckzpPLHBAEFY4iEhFnjPGqOhbt3bGZ++6778l9fX3YsmUL6vX6zqWnN5JYa49g5lNV9d9E5FvHHnss+vr6cpVK5X0AphNRA8BY770AEJsLSYytqMtUUwd4bbMDRBWNUV6AWn6e87WSV9KmGtRhUGdwPYCpE1srzmVE1ErTNFHVLd77AQAwxswEgCzL0N/fj82bN2NwcBBZ9rKdP18xWbVq1ZPe+49nWXam9/4/DzzwwGcBpNu2bTMA0FmE3UpELWNM3RiTsA1CgOpgqpNBnRk1z9QU2J28aFT7dr5BmldFyQIqIPKq5OFh4evqfaiq+c48dC6AjXsqrPce1WoV1WoVYRiiVCqhUCigs+D6mkmWZWg2m2g0Grjqqqui7du3Hzt58uTbJk6cuHb27NnvJ6Kt995779NLlix5E4CJqjqHmR8CYLMkSQP4qgrKBJAwhKBgJVZyFjs9jwEreE4FhajIopRAGSAGkRG1CJwfcnEqlMtZIhIiqhNRcc9Ff07SNMXg4CAGBwcRBAHy7QVYRFH0igN1ziFJEsRxjDiOn9cCduzYcdbVV1/9gs/84Ac/WLFkyRKoahlA3XtPzjmbtVrMiWs66BgQiIWcQpVZQwJ17eQFwBLQC9UNIEyHoqJGYwhIIO21QvbepGktHhpyQXeFOyvNDeCFHvi7yBAzb+zkwSIyLcuynl0rZa1FEASw1iIMQ1hrwcyw1oKInmcQRGR0TREiAuccsiyDcw7OOaRpOrrcvyfZxMyD1PbBgfd+Ptq+NhgYGDgJAIhoHwB1dFQrHhlxuSyteUg3g4xv93VKYFJomaCA0hoCtliBPi4EQ8B0IZ1LgscZUKfwgIoQo7S9v9nYtDGMpkwxYWhtZ2B7SCfT5wkRPU5ErqenZ924ceNkx44dPDIyAgCbReTg0edGK/8qihhj/qSqZuLEiWvL5TJv3ry5GMexV9UxqjoTQNrb27u8VCq9mYgeFxEbxzE11m8Mcn39Dc8QFaiFAkSkKoZID1YlgOQhQB9lBT2tKqsAgBTjhaTmSb0C6kDGiQa8dWuhsWZ9TtVb55wBMKiqxjl3LwCUSqXfjsJj5vqiRYsmBEFwOjOfEUXR6SeeeOJYImow85OvJrFdxRhzfxRFQ0cfffR8a+2ZQRCc0dPTs3jx4sVpZ+q2paen596VK1c+pqoBgMHMeyYiaqxdVwy2bCtnQoEA5Nu+TJ6gI6o0BgC86rMCeoYZfnXm0+0Ytc3CDXiygASqYK9MNFwrNNavnZ2mKURERaQOAEmSbN1t2GIXL148pq+vb8p3v/tdiAi+/e1vo6+vb+qJJ57YjfbK81/syP1XyAZVjRYsWHDc2LFj81//+tcxffp0nHfeeXjyySffsmDBgg3MvKNer49LkiTqdA11FYFzHsn6TbN4qJYnBYmCFQjEU6BKzVFO4tMdgF9rCVib+Oy00OQAAMTUVFEVpUyhUKhFko5F4g5Mk+QOIgqIKGVmOOemX3vttXfW6/XjrLX3EpE89dRTxy5cuBDLly/HvHnz8MADD2D69Ol45JFHDmDmewE8rKrjXk16RDQwadKktRs2bFgYRRFWrVqFSqWCe+65B4cffjhardaihx9+eG2WZVOMMRYARCTN0hQuzRLN0nk+i58ASAOwCOANgcHaVGlb4KbPxjtgjd0fqK4Uf/ROWyxqASYlsVBWBbwCCPv778x6dwzQPvtMMsZoEASbVfVtRx111E3PPPPMzQBwyCGHFK666iocwXXn3QAAEZlJREFUe+yxePbZZ1GtVjF16lQcc8wxWLp0KRYtWrT60Ucf3Wl+X6lB954WXk866aSJy5Ytwwc/+EHcfPPNGBkZwTve8Q40Gg0sXbo0nD9//vJWq8UzZsw4RUQ2dgyVtjZtGgj6+m9X8EQAQlBWKBFYSBBqh1Mq/m3zgQvabrNAnwIPEHAEgBMIspJUIKTkARaFMQ89OlJbvjw/5vTT1DlHxpgnACyaNm1a38UXX/wxEYGI4IEHHsD111+PJUuWgJlxyCGH4Oqrr8acOXPwmc985m9Hnxu1qLta11Ggu7+OAtr1lZlBRM+7Hp3OjVrwG2+8Eddccw3OO+883HXXXRgYGMD999+PBQsW4Oyzz/5okiT/KSLjVPVGL6KNRoOaD64o24cfq3nIFEOkAlIDdYAnAb+jw+sBAtYBnV25v4OphWyHmPidALqgtEIIxbbnIkOg5KrDJT3ggLebgw9ew4aNtTYlonne+2IQBN2qyiKCOXPm4Je//CVEBKtWrUJvby9WrFiB888/H8Vi8Xnw9gZy1793T3vT3t21kIgwefJk3HzzzajX69iwYQN27NiB7du344tf/CKstS5JkhKAsSKyIm61qNVspcnd95yst92RWMAYYjEABYCxsIMKfQsAOPGXpz77w/cg6xkAArj7W65VHs1cGNsZpBYMQKAgiFIevX23tTas64vjmFqtFlT1QQBzkyT5xWgFp0yZgiuuuAJRFIGIEEURLrnkEkyePPkFkF4see+fl17KZ3aHfdRRR+GrX/0qms0m0jTF3LlzcfnllyMMQ2RZ9gtVnauqf0rTVFutBK11m/pNb98tqhIqoKTC3LbAqup3jPJpuVaXh/sTsIun0dMwf+zJd+0PxWRVPA7CjhSglgIJhDxAaSm/OffZT0+17z+9t1AscqlYZCI6Q1WfiaJoWmfF+AUV/nMAdtfCF2vCL9Zsd0/GmL39PRTH8QARzfYi1zXqdbRaseh//fe41uXf326ayZQA0BwMAlLJA8SKsUp4EwhbBlsjz86DPw7Yxb1NgEuc91e0C4qDFboDECK0zY4KyNdbU2iknqT9/X1JHEur1VIRuU9VD0iS5Jo9NbndwewN3p408M/9AHv7vr3lPZriOL5BVfcDsCxNErRaLcRbt/XpSD1zzXiSgtgAyvAwbSPSq4Q3AYB6fzmA/z3KbSfAEP7melqfNrppQqKtAIAhUAiAWD1BbO2XvxyJ7l52QL1eR6vVEieySVWr3vszvff37K3v2luF9tZ0X+z6xZrti/Wfqoosy2713p9FRMNpmm6u1eucpqkU7n/wwPov/m+DAGsgQgAZAhkoWDQZ5TKY1ueuh7/9BQD3BxLfNtHXdHxAPgyiEduZzzJAEFI3PDxO+3aMYP3GvlqtybWRKrz3d6hqMcuyblUdGm1+e6vYi/V7f8n13mDuCWKnTL1pmk4CEKRpeme90aBGKxa/ZsN237d9OKnWxrQdKEm4c9qbCcPKdLa2rcEvAG2c0nZofz7AdjOWC0fSxuiZCMuCHQYg204IIEoA9f7kqlLlqZXvTONGrd5ool6vp9r2OD04juO7te3L8qKa8FIMyksxIC81H1WNW63WU0R0sIjcFsdxVq/VEdfr9a6nn17c95OfFRhCFkohFBZqLKAQ7kfHi62aNtcmkG/syux5AA8GtntxBwB0BwAo0YcD5WFDpAYEA4YBQzPXvf26/75vzP0PlhuNmtZqDWo2m4MAHgLw3iRJfrWrEdj19aU06d0t8K4gX8p37CnvJEl+h7YP4oOtOB6oVqtaq9do3EMrituvuW4ZMt8TgikAwxBgicDKwyD9CAAocIsTN3th22N2zwABIIb8r6G0vqzd4jUnLD4E1HS2OgJAQ6KkuXrlfn79Rp9fs663Xq9ptVbzjUZjjao+AuCDzrlfAHhFNPFlal6aJMkvVfUMAA83m821tWrVjNTqlHt27WbevJmba9bMMgRvALWAsBIFgIA1BRAqgKG08TBBzt+d1wsALgS2irhAIT8CFFCcYYFVAUHzgIYAWRKyUF37o590j9u89ah0247eWrXKw8MjaLZaq1V1uYj8TZZlN6jqyN60cW9a+WLjwJeibaOvALamaXoPgLMUWN5KkjXDw8M0ODSk0rejf0LvjiPX/OA/ihaAhUoHIIekxJCVpLqkzUB+KOLsfOAFUZH2eNDmQ9ClcG5J3uZmKBBCKWcNDQsQCEQ8wSiIRMT0Llvef9CCQxZsjIKnHbikIgKgGQTBJlU9xTm3GsBqANN2b3Z7et2TMdh1/Lf7GHBPY0IigjHmHu89EdERAG6O47hvcGjI1OtNifsHBg5cv/HYJy765gbrfaVAxCGBilDNEWyOTaxCbwJhHIGqQ0ltex/kcz9re9/+eYA/BtynYaqG6HHDZhEIE6D6EClXAHgHYgAsBPLqo833P7Bm4cELjt1A9GhGWnaqEOdbzPS0MWauiExX1Z9re+Qf7KkP2xO43QHuCmhP0Dpz4CERuVZVTyYicc7dPlStJrWRmtZrVY37h/oXbu094bFvfXuFSZOxEZGNFDYCfA6MHDGp6nYiHA8Aqc++lIn//RGQPUb32OtZuR9C1nxc/GcjGz5JoIMBmk+svyPQBAACUqiSVVJIlhW3LLt/5eGHLzx8o8NTKUkh88555wNAN1pr+1T1VBFZx8w3icjB2j6a9bwmt2uzHJU9QdqLBgoz/1xVJxDRUQD+2Izj1fVaLalXq+FwraZZ//DWI/r7j3/4m998gprNSTkgyIE4YmgBLDkiWKUniXAOAAjwi2ra2Ocg+Ev3xmmvAAHgH6A31lz6oZzJEYCxUD6YCHczoUeU2DMMQCBSylzWtfHOu2tHHnXk9DjLHu/1bqzKzj4sZrarARUROUnbHq2/Q9tFrmtXiKPXe1p5GZ2K7QZvC4D/ApADcDSAJ0RkRa1Wy2rVBtXqVQwODMvkoZGtbxoZPuyBr3ytj5JkfJ5gQmWTJ0IR7AsEWKb1qvhIh8uq4WSkVYJ+6N/30HRfEsB/B9zHgWVO3fjIBAsIGhHRDIY+xNAygRVQgjBDlcW7YN0dd2bzDj7YTjXWPJVltVbqOUvTXOYz8c63VGU1M9cBHKKqU1T1NgB3S9uzfho68/Pdm+0uyRHR3UR0B4DtRHQgM5dUdV2apk83m83myMgI1RpNHR4aMLVavX5stV6sbNpaWP71i4qBl2IEaF6ZCyxSAEsemgXEW1RwGkGLANxI1riaVL4yp30YfK/ykmImPApzap7t3EKQ+067ctigwP2xYlwq4AYLN6AcgzQW9S0oeubM2fTmv/v0kY8Q3bUxF3bnCpHJBYFEUZ7CMEQYtnfjmDnsaOE07/047/16Vd3S8RZIvPdgZgugQkSTmHm6MWaYiDZ2oKejO3Rx6rXValAaJ9qMWzohTre9FXTy4z//5dLtDz64fx7gHLdHE3kYXxDhHINyxFsVeCsU+wKQetb6p1T844fA3/jn2LzkqB2PAOeUOSqHQefoP2EThP6YkUxvgnwLoi2QT1RLTZUkUTXehukxXzp/MJw8acyt3j1RZTOhkM/bMBeQNYHmcyEAAxMwhUEgaLurdcJmAUTtU/LS9kfU9jUABrIkJS8eBGiSJJRlmaZpqtV6S3oEAydZO6+1bVvfsv99yVjj0jBH5AsgHzKFecAVwFIAyCi2EeMoKGYBQJrF5zUlSQ4G/s9L4fIXxY15BPhqV5AfsRx2INIQAdd7yP4tQJseQQvQJpRarC4VlRQQU+5qHP2Fz6e5CRPG3dGsr9geBOMtIQzDnLBlCkyAIGBSJTWGoKoZGePEOQ8AbK1R7y0RWSfKDEWaeiiJpnEK5zJkzmfjVftOyOXfnG7v237vv12Wk1qtEAKImGxOyOZBvgClvAHyIDWglar0PwjtfjiV9Lx61iq8CfjWS2XyF0cuegz4+zyHYRTkvwmAFUiJ8WNRPTSGaqzwTYEkDI2hpuVhHSFLoGq7K4NHfvQjtfEHHHj8mlZ828NJq9HnsnHGhNYGhgwD1rYNhaq6dgwZoDM5sCKs4jLy4uEyp+KzrDsIB95aKJSmBdEJ2598+s4HfvaziqtWe3IgChQ2b8hHUJ8TQp5h8gREIEtKjwP4KLU9yKSZxV9IJPmL4P1VAAHgUeAjAduppaD0v9CJd2oIvxKlcgJfjAHTFLIxqcaKLGXlTGBa0CxTsWK40TNjZt9hH1hiumfMeEcs+sS6VmP1hjh221KniXrK4I2gHXiH4cnA+DwZnRQEPCOfs7ML+f0j4gMH1qy/88Hrfikj69aPZ9FijthZUBAxfCQkOYYtClHI6nNgH4AalrThFWd2CAwOp41/F3FrDwV+/pey+Kujtz0EHMXgT48NS3NBGI0XuAqge5TkgFhJEqiPgSCBaiJwKSRogbxTNY7IZ6rGkWYmFw10T506MnXBm3X89Olhvru7GJXLlSCKxgNAliR9rWp1JBkZafZt2JBufPghHtmyuUviZEygFAREYlXZErkIGoTgNMewEYhCIMsBlCMERvkpJX07FPsDACnuG0rrazzk8gXAn/4aDi8r/N2TwJgU+HlPWHqQiL/y3B39NROJE0yJiVwM0VQ0TIE0ZVgH+EzEZKAsgwYegIdmClivnCl5B2KFSHv8xWyhQlBjLElIgCOQDUFqAR9AA8vsQ4ADgc8BgWVKc2DOqQbM2KSqAYHeN1pCUflaNa0fTsCHDgGG/loGLzsA47WA2Q/4Z8sWXUHhQ9o+nAwFUkB/ysAYrzQ1IU0cqYmVxAGcifoU4IwhHmIgTI4FIuQEUAWI2lNGKFQIHZcxVmuElVgQgL0RmBDwAZOxgIQEEylcpBQxY6MRJI4weo4PAJ5pZM1rMnHuTcA36bnjZH+VvGIxVB8HZjvghyVbvCkw9hsKLXRuCSn+rzIEqtMzgnEKTQnkPJwzCJwCHuqkbTUcAeIERNSeAajCWoZqe2XcctuqBJahgUdmGUEAeEswgcIR0XoVWGqD43ZFqZn49CtN11rkgE8d3g7487LlFQ1CqwCtgDmVIH9bDIsPWeJ/1vYUa/T+kwq9j5WKRDQ5UwmFkHmAPRSOQOpJhSHtlcT2fI6gCiYigWGjsAplkBoAAWAMOBPVbSBtEOhotCMZjVYwdioXN9LGmwX844Xwv38lQyK/KmGQHwQCMmYJRD6UM7nbQms+D6V9n/cQ6VYAdwLUIiBSRQVAt5IaArwHgbQ9jFESNu1ItKbtLIFhIlQ73UQOwAlQmrRbzTaIc5c3fHyiMv9Cvb/2sOdicb1i8qpGMleAH7T2BPL6T9aYu3Im7CHQeXsvDQ1CZQVAI6QY0fZ0DqRaVEIXoF0gXgDVMXv7CoVeFvt0KPP+WGPoWwucu/Pl9nMvJq9ZLP0HgXFg89U8h+uZ7SWvRh4i7vyGZNNZ3DcOA171MPDAawhwNL8/sfllzuQ2M/EL9hdejojq5bFrTT1M/RmvVtj3Pclr/t8c7gRswdjfFzjHoOfCh7wcIcU9dYlj6927Xo1+7sVkz0d7XkU5HnDq3ftjadWVdNPOU6J/bSLd1pBWb+bdktcaHvA6AASAo4Bq5s1Xmz79tQKpoN3L/6XJAy7x2VXkzdfe9jJmEy9HXheAAHA00sdJ6QFR/Ye/FmCq+g9edeURSF8z5/Xd5TXvA3eXZRxcam00zMDukeX+nHwj88nYt/jnIvC+HvK6A1SA7jPhb4wJqtSOofBS5NpMsuKtLn3Pha/iGO+lyOsOEACWAXm1we+Ywm4iLPwzjz/mJNuSufT049vHJl5Xed36wF3laKClLjhbJHtEQdW9Gw2qZ97f5505940AD3iDaOCo3GNzx4HxVlZz0Z7ui8o/KrLlxzr3x9e6bHuTN4QGjsrbXHwXRJywnP8C7WP5AkHkjQQPeINpYEfoHpP7hRrapEr/0H5LL2fR8W/z8d/gNZymvRR5IwLEnYBlW/i9gJiACNBYXfOU41/ExeL1kjdUEx6V4wEnLlxCkCogfeqaZ74R4b3h5d6wNP/esDT/9S7Hi8n/B3LrBEUxxEM2AAAAAElFTkSuQmCC\"],\"useMarkerImageFunction\":true,\"colorFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n if (percent < 0.5) {\\n percent *=2*100; \\n return tinycolor.mix('green', 'yellow', amount = percent).toHexString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', amount = percent).toHexString();\\n }\\n}\",\"markerImageFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nvar res = {\\n url: images[0],\\n size: 55\\n};\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n var index = Math.min(2, Math.floor(3 * percent));\\n res.url = images[index];\\n}\\nreturn res;\",\"strokeWeight\":4,\"strokeOpacity\":0.65,\"color\":\"#1976d2\",\"showTooltip\":true,\"autocloseTooltip\":true,\"labelFunction\":\"var vehicleType = dsData[dsIndex]['vehicleType'];\\r\\nif (typeof vehicleType !== undefined) {\\r\\n if (vehicleType == \\\"bus\\\") {\\r\\n return 'Bus: ${entityName}';\\r\\n } else if (vehicleType == \\\"car\\\") {\\r\\n return 'Car: ${entityName}';\\r\\n }\\r\\n}\",\"tooltipFunction\":\"var vehicleType = dsData[dsIndex]['vehicleType'];\\r\\nif (typeof vehicleType !== undefined) {\\r\\n if (vehicleType == \\\"bus\\\") {\\r\\n return 'Bus: ${entityName}
Bus route: ${busRoute}
';\\r\\n } else if (vehicleType == \\\"car\\\") {\\r\\n return 'Car: ${entityName}
Current destination: ${destination}
';\\r\\n }\\r\\n}\",\"provider\":\"google-map\",\"defaultCenterPosition\":\"0,0\",\"showTooltipAction\":\"click\"},\"title\":\"Route Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" } }, { - "alias": "openstreetmap", - "name": "OpenStreetMap", + "alias": "route_map_openstreetmap", + "name": "Route Map - OpenStreetMap", "descriptor": { - "type": "latest", + "type": "timeseries", "sizeX": 8.5, "sizeY": 6, "resources": [], "templateHtml": "", "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('openstreet-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('openstreet-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('openstreet-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('openstreet-map', true, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('openstreet-map', true);\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('openstreet-map', true);\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermomether\\\";\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"markerImageSize\":34,\"useColorFunction\":true,\"markerImages\":[\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAwgSURBVGiB7Zt5cBT3lce/v18fc89oRoPEIRBCHIUxp2ywCAgIxLExvoidZIFNxXE2VXHirIO3aqtSseM43qpNeZfYKecox3bhpJykYgdjDkU2mBAB5vCamMNYAgQyURBCoxnNPd39O/aP7hGSEUR24L/uqqf+zfR77/Pe69/Rv6kWwcgPLRIJfZUAa7xez2xd90QBwDSNZKlkHJHAK+l09mUA7BP4vPpRUVExMVoRef+L998njxx9X57vPi/PnTsnO850yPaT7XLXrrflqjtWymhF+HA0Gp0wEp/kHymEQqG4ptJDGzf+um5RUxMSiV7Z3Lyt88L5nozgHJWj4pGmpqZav99PWve04onHHuswmViQzWb7ruZX+Udgv8/z3A+f/NGye1evxssvb+wo5PMfTZs6bfqcuXNHL7hlweh58+ZVAOTUpk2b0p9dvjyqqmrs/b8ejpUMc+unzjgUCsXjsYruE+2n1JY/NedM0zCi0VjA7/d7/f4AAgE//H4/vF4fOjvP9h5695C/oaEhcN/q1SyTzVdnMpnklXzTq4EplUsXfmaRCgC7du3cOn78+KfGj59Add3z1Md1vV7vqPa2D1sA4MYbZ6qUiqVX9X21i4TQcfX19QCA6urquN/vn0kAPRQKpYbTnzRpUhgAampqAEFrPjVYSql7fD4AgK5r2tV0AcDj8WkAoOk6JJGeTw2+nocLdsEu2AW7YBfsgl2wC3bBLtgFu2AX7IJdsAt2wS7YBbtgF+yCXbALdsEu2AW7YBfsgl2wC76mh/ppjIQgXVloPxVSBRV0rBe455P6+kTKBYF3tonxY/IWarry7DvI298Tgp0PR9RzACaN1NeIS100+EdvKXW3cMZvF8wCK10Sq2it2NAzakmukP/wmoP/KuId3BRUMg5uCfCSNVSKVn1rNto7Un8jLrUVqJ4Fi2eEQiEYBzOsy3SYL37TNQdzi8Q5FxkqJIQBsNLlYMGF/zqAJWBxSEogDAY+DJibYqTuRg4WFgO3OKhCYTExbKk5G/mbkSPP2DQhLA5IO/NhSz1MMP882BDgnAFQwdiVSs2vPVhYDIJLUMkBgw1favM6lJoZDDAYhKbAYsOX+rqAhcXAuQSIAKzhSy2vS8YmB7NYH4WCfM7kw5VaWtdpOO3bfWZJZVXgPxMX898bVsm6RhkTIseX29yyIErm/J5z5vwr6pvmsLYjBgeDwSpVJS/OmT1n1de+9qANZgLc4q9Dyj2qQhUhSSUAUCL7GBcchCymTEYBYNWqVXj30MGHT586PZEJ+WAul7ts8bjspd9QKDRNU2nz4z94YtI3H3oI+XwB//3j/9m77eRUUJ9/0eh4APGoDz6vCi4ksgUTmYyBC4k8RLGwtzF+EGu+tHqRqqrYtm0rXnzhhQ7G5cpsNnvyiuBIJFKnqvSd55772eilS5fhwIH9ye+/dPaEf1T9otW3T8GtiyYgGNBBymYEgLSbvakidu8/h01vnkYhcab1gcVs5tx5c6PHjh7DU0/9qFsINPb3939UZg28X11dXR0Qwtr9g8efqGtc+Bn89re/O7FhR9BXNaFm+n98uxHTZ1SDKQqKAihweZlITUVtXQwNs8fg+Bmzdk+bnmPdf/7bwsbGeO2ECaED+9/5XCxWuTGbzVpDwJpGNtx+28o77rr7bmzZsu3k7z+cMlHzeiPrvnoTwtVhFAVQHAZY4HBEoiAAeDXUjI/gyJGeQEd6TFj2tHYuXNgYy2azVe0fngiWDLNloHNFo4FZkXDsoTVr1+KD4x8U/3Ci1qP5PV7N74FeFUbClKDEriy57A5JANL5a68hnqoINL8OAPqbXbNp7clTxTVr1/oOHjr0MFXxq2Qy9wEFACnoY//6la9QAHj+9Q/eUL2RWkVXoWgqkhZBypRImkDKBFIWkLIk+h1JWdL+zrmeNCWSDFB0DYquQvWG637TcnozAKxbt45yTr8PAGowGBwVDAbvmT9/Pvbu3dddijV9WdUUUE0BUQm6kwaCYe+ljK/w8ruUdsYCBLlMEUQhoJoCygWM+LIvHTx4sGfevIbqYMD3BSFkJVUUrG5oaFABoPXwhd1UVUBVahtpKtoOnEV/gSHHgBwDso5c6XO6yNF24CNQTbV9qBRUUenuwz1/BoCZM2dplOJeSggWL1myFEII9IeXziIKBVUUW1QKo2Ci41Anei9kkWcY6Ex5R8qfc0wi0ZPF6QNnYeQNB2j7IQpFOtg0WwiBxoWNIBKLVQI6Z8rUqTh69FiWaFNmEIWgLFShoM5TZbIzgVxvFp6ID5rfA6JQgBAIxsGLJkrpAsycAcH4gN1gX0QPTW9vP5Grr58cJJTOpbqmjgWAnp6ei4QSEEJAKAGh1BbHCS2DLAFmMAgmICwObjDnyYMMAtJL9oN89vRc7KWUQtOUsSqhSggA8sWivSEh9qBxTiCEAGRwQARUVaB67Hf5pZAQlA0Ayrq2LTCogVyhlLURNEw55yYABP2+4ED3vHSClBKQ9jiFdHqvEBCMQzAOKYSt6/RqSGnbDPJRbgT93hAAcM4NyhjrBYDKylhswEEZJgYJFxDchnGTwSqasIomuMnsIDiH5GKIzUAQTsCVlZUxB9xLIUVbKpVEff3kiLTMfimEA7HP5bZgHMJ07mnJAiuaYEXT3jcZDMLkTgBD7exgBKRp9NfVTQwnk0kIKduoJGRH8/ZmhMNh4skc3DnEkDlAi4GbtjDDguVAmZM1M6yB68JyKsCGBqD373s7GAySnTt3gBDyFhWCvPHee/8HAJhTU5g0BMg4uMXBTT4AZSUTrGjBKpiwCnablQbDbZuyfTmAuRPMegA4euQopCRbaCaTOd2XSLzX3d2Nu+64bR7PnP3LJSCDMBm4YW9FWcmyQYMytsW+Zpfdsm1MdimAdMc7K29bMedCdzeSyeS76XT6jLNI4PGf/+w5aLqOu25IjOOWKcSg0jJjcLZ2ecsZD5TdybqsOxC0ZYpbJ58frek6nn/+eVBJHgecjXkqk2nu7Ozcdfz4cdx556rJN5C3m8v3jBt2xpdnazjysawNy5lUbKkrbmtZsWL5pGNHj6Or62+7k5lMy5CFNRQKTfN6tAMvvvhSRe3EOqx/4oXXLvia7qO6CsVZrey5154KB5YpKSG5tHs+5/ZsZnEIk6Ei1fLH73373i/09fXi0fWPpgyTLchkMqeGgAEgHA5/vjJWsf2PmzYr1dXV+K8fP7vjLxduWkY8ilpetQZPg+UJxh63lzqlNDi7gTa3fuPraz6bzxXw79/5FutP51am0+kdZdaQ/2kzDKNDUci51179w8pbP3er8sAD6+pnVCWy+/fs21LAqBnlMT50qJXFLq2a2L/5gaVy7N133j69u7sb67/7iFHIFf4tlU6/Ppg1kLGU8hYAywBMeOWV33gfXb9+1Q+ffDL+4Ne/AcYY/tS8PbV5++4Dhy+MopY2ZrLiidQDgDBSp5TS+Y7psS65ZOHsW26++eYosxje2PwGNm586eKzz/x027+sXWsBOAfgbULIQQAgUspaAA8BGAfnsamrq4u0tZ0Q333kkdGmZS3f8JNnlBXLV0AOilRKCS7sWYlxjlKxgHw+j5Y3W/C/Tz/NQ6Hgjp9seKZ31py5ajwe4wAtz9zdAH5OpJTPAqgEgL5USkpu4eLFHloqFXniYh9t3bunauuWrStisSi5//4vYnHTEkyZOhWqokBICcuy0N7ehr2trXjt1VeRzqTl3ffc81bjgsZELF4pQ6EAqa4eI6UEicfj5dhTKoCikynx6Bop5C14dJ2XcjmouipvvGFGoSJaWfr738/7tmzdjl/88pfIZjKwnH2SpmkIhSMYW1ODhvmNGFcztjhudFXR69Wgck58Hg+XEorH5ylDJYA8kVKOckpdB0ADIBOJhOzv70OhUFILuTzPZLNcSE6SfSlvJp0O5A1DN0qGDxLS4/OUAh6PGQqHC5XxeJEQgkgoRH1+L/wBP6LRuIjH4+Uf8gSAUwB+MbhzzQSwCMA0p/QUQADgNJ/PJ/v7+wnnnFiWkJZhKCYzKADoqiZUXeW67iGcSxKPx2QoFAo7AybnuE8COAZgHyHkxGXjeFAQEQCzANQCqAIQBeAH4AXgcex052w45TMcyQHIAOgBcBbAUUJI5uOM/wcaHmf3g9UM7QAAAABJRU5ErkJggg==\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAA3vSURBVGiB7Vt7cFzVef+dc+/d90OrJyO/JSO/4ncxxfULMCYIAyEW08amJJgmM4GmnZjJdNq4gcSGzLQxk3bsaWcaaIHyR8CJrWAbpjgG/AhINsbYxkaSDY6xJFvSrrS7Wu3uvfecr3+cu1pbXhkJs/4nujNndufec77f+d7fd+4uw8gvIxwOfocBaz0e91yXyx0BgKyZiWUz5kcEvBKPJ18EYI+C5rWvkpKSyZGS8LGHGtbQR8ePUUdnB50/f57OfnqWWlpbaN++39O99fdQpCR0NBKJTBwJTfZFE4LBYLmh8+YXXvifKctWrEBPTze9+cbu8/3JVMoWNjwer3/ZsuUTvV4P239gP36yceNZW9CtyWQyei262hcB+7zurU/99Ge3r1nTgJdfevFsqr8/Wlc3rWbGzFkV8+fPr1iwYEEJgLadO3cmbr/jjohh6KXHPjxamsmar39pjoPBYHl5aUnnqZY2/b1Dh9LdPd39kUgk6PP5PD6fH36/Dz6fDx6PF+fOfdZ9+pPTgbq6Ou+aBx+0k/0DVYlEIjYcbX4tYM5pxeK/WKIDwM7Gxt0TJox/dtLESXC53JuHzvV4PBVHDjfvAYDZs+fonMsV16R9rYeM8XG1tbUAgMrKsrDP659DRJ5gMNhbaH5NTU0IAMaPHw9IPv5LAxORy+31AgBcLsO41lwAcLu9BgAYLheIkftLAxfzGgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4D/lME1ke7gDF8ltbOHe3W923oEwYi1jxftWfZWgAziwacZkd2pfyN96XN5IIu7dMtIKA9/TI+zqCnFps2Alg5UlojFnVqIHZUlO2sl4RyC4CU+SEEylux8Z/iyc7mrxw4U7UnYwvGpXMYKIgNGdwXC/76C48oRw3sDWfnCgIkARJXcpwbvpA1e6T0Rq5jDr8EAHKA6OpjUOJwfeXAJAEhAXAGgEPKq+dIMVJqowDO4RAAC0rHV21u5LijAJaABAOIAY5Oh15iFMgj1zEpcUuuXjpIWeCouxjAtnIZcGKA5AVFbRfazPUC50QrKe8+Qy8qiqjBYIODA5DgBd1pBO9WRg9sy7yOhXBca+icYrgTOUGOiKnIVdCdisAxJGBTPsYW0nHRrJqgfNmGVtiqaeR1xchF7Vgz40q/BUNmISlcL7CUgJAMnOUiVwEdF0PURIAAVHaC8ucbAiwcQAb1KQpwXMjFrhtYMcOVO8lhOB457ujcKZd9hBguSYwcelTupKyaQWKYJFEU4xJw/Dhfcw29ilSBcNjEoTucFnSnkeOOvvTJpcVC1cYoGB5NAGEQTukjMAzHoghJghyWCRjenYoTuZjKx8xJiwU4LrSZ6waWpIoBjTuRqxDHRUkSUMWAJAZp6QU5FqOw65HHapG3bGVcBTZXDI5VnFaFgBL1yC34uoBJqEJeIwD2MMY1ilZidAFEMlDOqm9UdpJ0ZawumI+LU9ArwhyqWxyNz14XsBAMUnLVH0ttGB0XococdCGWE3XhOV85MF1WV2OY3omK0S2SkxgYAZYYJoAUpcqEEjG/Ru80isA1ysMXYNCnCum4aKUPgTu90w3sFinXL6nO/MadCAhiKloxBjFMeSuK0S1Kylv1cE1bUVoYyHwhoI6bCswpjjuxK5u2G2lcti2jzNCRTluioHEVw52EBA5/2LKsLBL+h2gs/o+Fjpa+MqtmjCbkqQJSYFF3T3zRsPMvA75i7UiBA4FApa6z5+fNnbd6/frHADghk7QdlhAHdMY0KXkZAHAuozaRMDRtKYMdAYDVq1fjcHPTD860nZlsS3qsv7+/+6pNDr0RDAanGTrf85Onnq75/uNPIJ1O4+dbnj34Ot6B4eFLqksqUeEvgcflAREhZabR09+Li/EorLQ4eFv317D2oW8t0XUdu3a9jud/9auztqD6ZDLZOixwOByeouv8D1u3brtpxYrb0XS4Kfbj3//8VHC8d0nDLXfj67OWIeQJgDGADfoOAxHQl05i14l92PHBXiTPp/c/OrFh9vwF8yMnjp/A5s2bOqXEbX19fX+8CriqqspvmunDTz/10xkr71qFnY07Tr1i7aqsLg2Vb6h/GOPCpdAYgTPlNLmF5AzpvBRp74viX3a/hO6+ge47+hZG61fVTz9y+DCee27Lx15fYFFHR8cAcNkPuw2DPXfP1+vvvf+BB7Br967WX9Mbk70eCn33zlWoCrsgKAFBCdgy/2nLBCyZgCUSMGUSpkzC0G1MrKzE0XMt/la9I0QnM+cWL15cmkwmK1tOnwpksuabg8YVifjnhEOlj69dtw6nT51Kv2q96fYG4fG7gbJwFhn7cxicIJgEZwAfEiokGASpWG1KhvIwg1/91ti1N9DEJ7ZOzKxdt87T1Nz8A67jv2Kx/o85AJDk//zXjzzCAeA/D7zU6PZjkkuXcBuEjN2OrGiHabfDFB2w7HZYoh3mVaMDWWdu1m6Hy5Bw6RIuP6b87+HXdgDAww8/zIXgGwFADwQCFYFA4BuLFi3CoUN/6LRmyL/y6gSXTtC4QDTVgQo/B5iEJFJ6Rt64lI6Vfi3JYBFHd1JA5wIunUNIQvpr/C+bm5u65s9fWBnwe9dISWVc0/DNhQsX6gDwTuuhd3WNYOSGTjjSehGp7EVYsguWuJQfssu51wVTXIIpLsGWlzBgXsSRM5dg6Hk6uk787Zb39gHA7NlzDM7xoM4Yli5fvgJSSiRmmbP9HNA0Qm4D6axEc6uJ6eOzuCloQuOOjlneqiUx2BK4lDBwut2DTFaHoXFYGilaHEjMMOdKKXHb4tvw/nvvL9UZ+Lyb6+pw/PjxpOZhsziX0DigcYLG1QaEBD69ZKA7wRHx2/C7BDSNwEi9AEmZGmJJA/1Z9SJM12hwvcYBzgmaj89obW3pr62dGmCcz+cuQ68GgEtdl7oYU40CZwSeW+As1rmy5KzNkbY1WILDlOp71ubgnKA7czVO4NyhwQhcFS7o6urq5pzDMLRqnXEtCACpdCrFHOHlAsTgYEq0nCnj0jnBY6i8KCTLBxbmzB2yPkczmU4lAYAxHtKFECYAPeDzBQZD4GU+motMueXklECWc7QkSaVDGoTAVetz8AGfLwQAQoisbtt2N4BJZaVlpZQjkntdS8w5UFOFni0YLMGhWfny1rbVPVuoOVKyK9ZeTrMsUl7qAHdzkPyktzeG2tqbw8KihCQlPjVUl2hLBkswmDZD1mJIWxwDWTXSFkfWUs8sZ64QzlqHjiRA2tQ7ZcqUYCwWgyT6hBNjb+3ZvQehUIi52tje3M6FyHHIYNkOqM2RsTjS2cuAs+pe1uYKPLcBkduA+m60sH1+v5/t3fsWGGP/x6VkjR98cAQAMNc7bXJepAyWzWHaimjW4siYDGmTY8DkGMhqapgcaVM9yw5ugMOyeX4DkmGub1otABz/6DiI2O94IpE4E+3p+aCzsxP333PfAvOi2G8JBtMRbU68GZMj44Ao0BzXmgOsRk7spq1oWILB6rQP3nt3/byLnZ2IxWKH4/H4pxoAeFzuC21tretW3rUKnk5mtWiflzAGxhgDQ66IYyrnOnqzBFfDZjAdLk1HMnkpMWRNLldmFomamtrIL/71F+iPJ/8mnc2e4QDQm0jsOXfu3L6TJ0/ivtX3T607M26P6SzMWI5eB7ktPHLPc/MV5xwTjpe9sfLOu2pOHD+JCxc+fyeWSLyZdzCoWsvjNpqef/6F8KTJU/DDLT/a3jM90eDWCS5dqmDvxF7NCRSAOikQhCuMUXHMEDjm3v7jb/+oIRrtxpMbnuzNmvatiUSi7QpgAAiFQneXlZbs3rGjUauorMSmLc+8dShy7HbDELqeA3bC4GCScHxWSMDOgVuaPb2t+t3vPfK9O1P9A/j7v3vC7ov318fj8bdyWFf8YCSbzZ7VNHb+tVdfrV911ypt/bcfq52J2uTBg+//LhWwZ0nJYTtWf6WrcccDGFgLdn5nwkPVD9Q/MLOzsxNPbvhhNpUc+G5vPL7jcqxBjonozwEsBzD5lVde9jy5YcPqTZufKX90/WOwbRv7330nsffDt08dSB41EkZyHPfwmwBAZuTFsBm48GeuWfai2oUzp02fFjKzJhp3NuLFF/+765e//Pfd31q71gLwGYC3GWNNAMCIaBKAJwBUO3uQnZ2d/MyZNv1vn/j+LUuXLq/Z/MyzCIfDTmxW8Y+IVFyWqjKRQkDYNqKxGDb97GkcOXLk7LZt/9F8c12dqKqqYM4LYALQCWAbI6J/A1AGgKK9vSBhoa8vEe+N9TwejcZYU1MTfrN9O6puqkJDw0NYtnwFpk6dCsZUMrFtG22trTiw/11s3/4aotEo1jQ04NZFt6KsrJTCoZKtJaWRiGG4KBKJ5BJWnw4gDedAx+0yMJCywLnQGWOSMabV1NbikUfX40J7B367sxFbt25DMhGHZZkgAC7DhWAojOpx4zF3wS0YP64aVZUVYCoQSN2la4bhIsNlcOS73H5GRBUAHgcwBYABAD09PZROp1gq2V8WTybq4vH4xEQ8oSWSSfSnUkinM7As9RdUw9Dh9XoR8PsQCgYRCodESTj0x1Aw2OrxBXsDgYBdXl6eM2IB4CyAbZcb12wASwBMB1Dq7C4ACJZIJHstM5PWdC2TTmcom80wEtySAFwupum6wbxeDxeCuT0et8/v94UBTTrSJABRAKcAHGCMnbrKjy/bRBjAHAATAFQ5NuAF4IFqAtyOKzKo83MLgAkgA2AAQB+ADgCfAzjBGIsPxfh/6wbDK7xbMFYAAAAASUVORK5CYII=\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAyUSURBVGiB7Zp7kFRVesB/5/S9PdMz/ZoHMwo4MICDuoGVIYICIuzGcn0vC+oWGuNjs8mua9ySP4wpgyaiVVupbHYTsLJmNT7WNXExwqqzrq8g4oNxdXUgyEMQARmZd3fPTE/3vfd8+ePenhlgBsFlrFSqb9Wpvn3vd77f+b7zne87ffsqjv+wE4nYDQqWl5aWfDUcLqkAyOUHunID+Q8EnkilMo8C7gnoPPaRTCYnVyQT71+1bKl80PK+HGw9KPv27ZPde3bLjp075NVXX5FLL7lYKpLx9yoqKuqOR6f6PIFYLFZtW7r54YcfqV+4aBEdHe3ywm+e39eb6etzPZfS0kj5woUX1EUipWrj6xtZedddu11P5mYymc5j6Q19HrgsUrL67r/7+8VLly7j8cce3d3X29vZ0DB9yplnfWXcrFmzxjU2NiaBXevWrUsv/trXKmzbqnz/9+9VDuTyz35hi2OxWHV1ZbJ1245d1ltvvpFtb293Kyoq7LKystKysnLKy8soKyujtDTCxx/vSW3fsT3c0NAQWbpkiZvp7a9Np9Ndo+nWxwJrLYvmzV9gAaxbt/75urrxd592Wp0Oh0tWHSkbiUQSv3unuQlgxoyZltZm0TF1H+umUnrC1KlTAaipqUpESmMzFIRjsVj3SPJTpkyJA0ycOBGMnviFwSISLolEAAiHbftYsgAlJREbwA6HESUlXxg8lkcRXAQXwUVwEVwEF8FFcBH8/xhsnZC0ksw49eQPI5mmNtP54ccAIvqgqbz4aYn8zYoTUXXcFnueyZ8eXtleZt75iQnpU0VUvYiqB5mvu5p+XH9w8RtgnJMOLut/7rd4+fpRBcS52hz65csnHdxQ8clZnyuT3NV40sHRUnfq58mUWFJ70sEn+yiCi+AiuAgugovgIrgILoKL4CK4CC6Ci+D/Q+Djf/higk8Jzs0IMjIGYDGAp0AUeBbiHf3Xs/HGAHyYlYaRX0EYC4txNeIFugvWHyXzua8cnDjYGMBoQIFhRFfLmLjaCxqAw8iuHing/nCwGlLuMrKrveNfnccPFnyLtQ8c0a1jElye8sGFAYwUSCN54Q8GB4ljKKpHkBmLOZbB4FLgjhLVYxNcDFnkMXJUj03m0kOKR0sgYzLHRvlwpcDYI7oaGYvl5HB4ZRrJ1cf9fP5E/5NwQUKM7uoTOI4/ql38kmgUOCMnEHMCL819sag2jJJAxgIs+HNY6PGlpUxXDQWXw5dXjxH8SFZBPf7SyqKrMQLKG7b/OkpmTBJI0BSjbwTGYo6Ni5+ZjMJDj1wkxmQ5iV+VsBh9BzImKbNQFhWjp8wx21c7dKIV9A94IxaJsdplZt9574JQVcUdpr3rzlEHdzLASslpg19EofLMMa3dc0Z9c9YMXT+s7/GCo9FojWWph87+6tmX3XTTzT7XA/F4xutXr4fyOuQZVQUQ0tLphY1nlcn5YqgAuOyyy3inefOtH+36aLJr5Obe3t72o4w68kIsFptuW7pp5d33TPne928hm83yLz+6b9PVb/4niRK9QNfUoquqUaUREEEG+jGd7Zi2Dnpy3qYHGr7OFdcsX2BZFs899ywP/fznu11PLslkMjtHBScSiXrL0m+uXr3mlEWLFrN58+auxD+u2HZWhb0gcvkyShZ/Ax2N+70KPcVvJpMm999NZJ99mi1dzsb3rviLGbNmz6rY0rKFVavubTWG83p6ej4psAbfr66trS03xtlw98p76s+bN5+nnvzFtouevK/s1AnJM+I/vB37j6aDziJeCtxhzUkhTgoYwJpchz3zbJI7fj/pzA829f6iR/bPPW9e9aS6utjbb715YWVl1SOZTMY5DGzb6scXf+OSS6+48kqanntu55+99shkOyLx8uuvIjSuDEzq6Ob5TdzgPJ9GhT2sCbV4W1vK57R+FP9lOrT33PnzKjOZTM2OD7dFB3L5FwaDq6KifGYiXvn95ddey4fbtmWv2fhIiVUqpbpMEao2SH4fiKCMgAbRggSuVkKwEQz22q4iVKtQEYUtJvzdlvX6+bq67PJrr41sbm6+VVv8W1dX7/9oADH6b//0+us1QO/jD6xPhGWSCgsqLJj8PsTdjzj7Ma7fxDkAzn5wjry+H3H2YfL7UGGDCguJEqnPPf3YOoDrrrtOe56+C8CKRqPjotHoN+fMmcObb7zRelsk9W1lC4QFCRlM9yfoKnsoEgOLVWCxDLfYBRwwnXmwDIQVyoMbo6lrfrq5+dCsxsbaaHlkqTFSpUMhvjV79mwLwHvjldewBGxQlqBswXn3Y6T/EDhtiNOGuG2I2444QXPb/WtOGzhtmL7PcN7di7IFFegiJDq3+ZVXAWbMmGlrzRJLKc6/4IJFGGO4MdQ+gxAQEn/2LcH0u+Sa27HO0IRq/V+MSqnBOUZARMAD75DB2w4mq8AKWkggpPiOtJ3dYgznzTuPt996+3xLoc8+vaGBlpaWzFybrygtqCPgeODtcTFtBl1hUBHfGgl+wNGv8FIayWjE6KCfD1UhBVqotPWZO3Zs7506dVpUaT1Lh21rPED7oUNtKH8OUYLSoHTwWRiEAsmBDIA4gCPIAJh8YL3lyw7vi5JAJ7QdamvXWmPbofGW0qEYQL4/0zeYjdTRTQ0Oxp9/Svx9jvKAkBocsCh1dP9AZ76vNwOglI5bnuflAaukPBo9bM8UpMIjvxeiWAUbATHK3/yNJM/h30vKozEAz/Ny2nXddoCKyqrKwc5GDYFMUJmM8peLqyCvkH6FZP1zXP+eGBXIFvQcrquyqroyALdrxGzv7u5i6rTTE3lX0gUL/DIYPPfwFDh+k5xCBhSS1Ui/9s9zQ/cLz0rEGxqEGMWAK92T6yfHu7q6MCLbtSj1UtPzTcTjcfW0E3t5EBSkv0FgPgAMQgtWa/9azpcZHICrhvR48B+52CvRaFS9/PJLKKVe1Mao9e+++zsAtk9rnIwbLBFHIQ5IACWvkJxGBjSSDeDZ4HxAIznty+SV38chGIA/PXumzZoK0PJBCyLq1zqdTn/U2dHxbmtrKxddfmXj1r7QRr9jMH/5Ye4d8OdV+odZ3F+AqyG3F/oFelr62PQnl14667PWVrq6ut5JpVJ7giLBygfWrMYOh3ll/pLx4iojR7p3QMGgpQX4kPUE8OFuF0chrjIvzL78VDsc5sEHH0SLWkmQLuhOp5v27t376tatW7nk8iun/UN8VhM5BblASS5w53BowdXD4L7Lg8EG7Z6SM36z+MILp25p2cqBA/s3dKXTLxRSBeDvtUpL7M0PPfRwYtLken791z9Y++fevmWE/WJBIelbgJbDtz4mePblBksrcPU/ubVrF65Yuayzs50Vt6/ozuXduel0etdhYIB4PH5RVWXy+WeeWR8aV1PDz+6/56W//PDFxbpELGULgwVEcwSYoWXkKExOuatqGl9b8p3vfb2vt5/b/uoWtyfVe0kqlXqpwDpql1lVlbwhUhr52VNPrQ3PPuccNm16PbXrR3f+9pvm0NV+pWEwhQKIqKHnm57iV9nydc6Smxc1zm5MHvj0AHfecUeuv7f/u509PY8N5wyCReRcYCEw6YknHi9bcfvtl9276r7qG2+6Gdd12bhhQ/rghhe3TdmywT4l2zkhEeIUgJTLZ62RygPbT5/rlv/xvLOmnzE9ns/lWb9uPY8++u9tP/3JPzd9e/nyLLAXeE0ptRlAicgk4BZgfDAGc/DgQb1790fWrT+45Zz58xdMue+++0kkk/5N8RO2iPiZ0BiMCMbz8FyXzq4u7l91L5ub3969Zs2/Np/eMM2rrT21YKQBPgPWKBFZAyQA093drTzPobu7uyPV3XNbR2enam5uZu3atdTW1LDsqqtYeMEipk2b5m8GANd12bVzJ69vfI2n1/6Kjo5OvrVsKefOPZeqqkpJJCtXJ5OJinBpRJLxeOF3bI8FZIAYoEN2SHmeJ6GQ2CiMUipUP2UK199wI59+2sp/rVvP6tVryKRTOE4eAcJ2mFg8wfgJE5nZeA4TJ4yntmYcSimUUsaydMi2wxIKKTXM6n4lIuMCV08m2O52dHSQzfbpvkxvZSqTbkinUnWpVDqUzvTS29dHNpvFcfy6aNsWkUgp0fJyYrEYiUTcSybin8RjiZ2lZeXd0WjUra6uDg2L/z3A6uHBNQNYAEwHqvAXTTl4Kp3O9HhOvk+FGMhmHXHdHGLEE8CytNY6rCKRsPY8VRoOh8tisfIkhFxgIAB2AtuA15VS20ZcTsEgEsBM4DTgFKASiAClQAnBig7EC8/8BoAc0AekgE+B/cAWpVTqSMb/AlY1WXIncMcxAAAAAElFTkSuQmCC\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAxNSURBVGiB7Zp7kFTllcB/5/a93dMz3T0PemYIDgoCPhZ5iaD4wNkFjQjRRMlLTNbSlKlyzZpobSVbFRPUbNVWSRCWuKvlxqybtbIrukp4SATZCAgospEBgeElj4EZ5t3d0+++37d/9O2ZnqEHQZzZSlXfqlMz/c253+875zvfOefeHuH8L6u83P+AwH0lJZ4pbrenEiCVSnYmEsndGl4NhSKvAJkLmPPcV0VFxZjKivKPv77wXr274WN9uvm0PnHihD5y9IhuPNioN216Vy+Yf6eurAj8b2Vl5aXnM6d8loLf7w9apvHhyy//29jZ9fW0t7fpdWtWN7Wdao4qpaiqDpbdXF9fV1paKpu3bGbxk08eSWXU9ZFIpOPirC33v7xs+TIdiUT0Pz239NjeaTOTHXXjdb4cuP6W5DOLFx/7aNdH+oknfqQryv0vXZTFfr8/GKyqaN7XeMhc//ba6NSfPFXqS6fESJ29jdGAX69+9KHY9OnTyxbec08mHInWhsPhzsHmNs4FNgxdf+NNN5sAh3/7n40dCxeKedUsOr6x8CzdsnBEQu9sPABwzTWTTMNQ9eec+1x/FDEuGTduHABXtreOKutJYyiFqq4tqD+5O3wJQF1dHSij7nODtdZuj9cLgMfGOpcuQInSFoDldqNFez43eCivIrgILoKL4CK4CC6Ci+AiuAgugovgIrgILoKL4CK4CC6Ci+A/B7B5vor6Mz4PNnbRYAAtoCQLUMMFVobuBWOALWdjVIGxiwbbZC3WkrXWLqAzJBZrR5T0LWTgdSHfdF1YcIlG57t8oM5nfov1OcCKPmDW1Rfi2IsA5yI5F9WFXF0o0i8arARwggsBu4BbhwaM6g0ujXY+9b+GLqrzLR5E5wsH2ziB5QRXoW8lCy3mosH553iwlDlEe9znai2DpMyhAJ+PxUNTJMhZm51+WM9xvsWFXD2kx0nl9rjQ4oYC3C+4BoEMnasl39Vn6wxRdcqbXApXpwupWBcEVgLKGLw6DU1w5bkaCjcChcYuHozuLYtqEFfroXC1TZ67GcbjlEuZWjSIHr6ozjZ7/y/VSWOLdgJIF9zjQl3JFwDOXn1lsYDOULm6X+YaROcLB6s8+LC2tzqvoc+Wx0L2nT/6wlIm5y6LQ9bs5TLXsO5x7jG192lxuJq9bCOg0aIRGcYEkt9lCsPp6lxlMsBlFE4ghcYuGoxznHKFYNjKYq7Zy5XFYW32lMtCBGzbLlwWLwB83m/2NNC44R0iFaP503+8jO1UqHz5wiwW0aNzvysgdPJTQr/7dFD9fHD+vecN9vl8NaYpv546ZeqCBx98CMhGbPXEqZRfcTWmyySTjuO2TMora/B4Sji+832OnWoGYMGCBez88IMfHD50eExG6Yd6enraBjJcAwf8fv+Vbsv1Pz9f/NT1y1esQCnNPz6zeGuy6WBN+MRRrwp1YMR6MOIJMqEuOj49xNFd2zh5aD9SVpr44PCJXVOmXXvpHfPm4fP7rtz98Z/usSz3+lQq1e/fnvuFSHl5+VjTNLb96lfPj6yv/0t2bN/eufJnj+37Uql1c/1Xv8WM279CaZn/rJcBGoj1hNm+7k22rF5JcyK1edp3Hps0bfq0yj0Ne/jFL55pVopZ3d3dx88C19bWlqVS8Z2Lf/7U1XNvu51Vb72x7/irz9fUBEcEv/03PyFYPRJDgZHt9XpvzG8QlAFnWppY+S9LaOnsaPPOWdhxx7z5V320cydLl/7yE2+pb+bp06dj/VxtWbJ03h13zr/r7rtZu2bNwVP/9cKYMiHwtW8+QNAbwOiOIN09SCiChCKQL+EIKhxBhcN4EGpGjuJww66yxNH9gePac+zGm26sikQiNY379/kSydT63uCqrCybXB6oeuS+RYvYv29f/OTKFz1+dIlXXFQrCznRjNhkRfdJzmIMEAExsqbUmh68holWGXf43deMg6NHJ+5btKjkgw8//IFh8lJnZ88nBoBWxpPf+e53DYC1Ly5bVSb6Mo8WSrQgx5uRY6cHSDMcz0q/vx/PSTNeJXi04EOPfe93L70JcP/99xu2bfwUwPT5fNU+n++rM2fO5P3332+uS3V9y9KCG8FSmtjRo3iN0uz+qqylemDnLhpDQDsFJGrHMG2F2xAyGi5Nhr65Y8f21unTZ9T4yrz3KqVHGC4X91x33XUmwN7N775nApbuk90nD5BpbUbaWqG9Dd3eju5o6y/t7dDehrS1kmltYffJ/ViA25nDBcbeLZs2AUyaNNkyDL5minDL7Nm3opSiNtQ0yUQwESydlXg6xc70Sf5CewliYSD9TqHu/anpIMUnJIiLjSVCGjAFTA21odNTlFLMunEWO7bvuMUUjKkTrriCvXv3RDyiJxpacGVXSc56W2uO6DhtKkmFFsocHchmtKhoukURNrJPG5YDdAEuDYaAV/TVjY0HesaNG+8Tw5hmuC1zFEBLS0urkQ3QPtFgILgQTC0IkAZSgEJQCClnTBwdF4KBOPf2iQBnzrS2GYaBZblGmWK4/ADxWCzqoS85iDOZDFiMS2ddV5Kz2EkGhgwECYLOzqOzxy0W7YkAiBgBw7btFIC3tMw/2JsrnS9OI5B2pPdt0AC9gdVZZxkBANu2k0Ymk2kDCI6oqsw1c/nNu8rVW8l+2ZFCkxRNzMhKUjQpNBlnv23nXfbAeTRQHayudMBtBlod6OrqZNz4CeVprcKqd4KsZBxgGk1KNEmBmGiijsScsZRo0s4CMnn3284CMqJCY8aOCXR2dqK0PmBokQ3r1q7D7/dLq7tyY8axMCOatDNZFqhJiCbuWNsLNrJjCUcnt4C0ZOew0WTQnDYr3/X5fLJx4wZE5B1DKVm1a9dHAIyYesPYjEBa+vYwJZAUSAgkHAtjookaWcl9Togm4eim8u5PS9YDNVNmXg7QsLsBreX3RjgcPtzW1rarubmZ+QvumtahXJvzrUzmWRvrZ61yxNnvPKuTA6xvt13bvjxv/tSW5mY6Ozt3hkKhoy4Ar6ek6dChg4vm3nY7oZJAJnG4oUIQESdD5Ud0v30XSBlZC1OGdjyTA/darwK3LcxcPm585ZJnl9ATinwvnkweNgC6wuF1x44d27R3714WfOWucZGrb3g7kee+eJ6LewPLcXU0bzwuuf2G3P3NoyevnzP3tsv3NOylqenkHzvD4fWQ197aikeW/nJJd1dnJ4//9On57V+a8Hoib7K4kQeUAWL0D7RcsJ2oqHv9wUcfu7Orq5MVK5Z3KS0P53j96lsgEPjyiKqKtW/891uu2tpalvzDMxsTW96s9yhMC8HUOCkxm07JO/fZk5A9dkmDTOSqWe/99fcfmRPtifHY3z6a6Q5F7gyFQhsKggFGjKh4wFviffG11153T59xHVu3bg3968/+7g9V3ae+0Zv0kX49l3ISjA2ccpe/NXvR9+uvnX5tRdOpJv7+xz9OxnpiD3d0d/97PqcXrLWeBcwGLnv11d96n3j88QVPPf108KHvPUwmk+HttWu71q96Y0dozzajJBUfXyqMA4gpfShmeY54JkzX19/6VzfMmDmjMpPOsOqtVbzyym9alz23fM23Fy1KACeAP4rIBwCitb4MeAQY5SxEt7a2qIaGBn70wx+OTKXTc5Y+t8w1d85cdN5KtdbYSqGVImPbJOIxotEo6/+wniXPPmsH/L4Ny5etaJk46Rqprq7JPTgooBn4Z9FaPw9UAHR1dSnbTsuZMy1GMpnItLZ2GFu3bq5d/fvVc0ZUjZB7F36d2fW3MmHCFZguF0pr0uk0Bxsb2bL5PV5fuZLuUEjfdffdG2+66ebW6mCVLvP5qa4OAoYEg8Gcg7tNIAIEADHdJnbcxmNZ6UQ05nK7TT1x4sRYRVV1/FTTqdLVa9bywgsvEImESKfSAFiWhT9QzqhL6rh25g3UjbokPnJkTaKkxFRaa8NtGbaIy+Up8eS2VgEx0VpXO66+HKfdbW9vV93d7RKNJl3xeNQOd4d1Mp0i3B3yRCKRsmgiYSVTaa9orS23lfR5vany8vKYLxCIeyxLKqoqtddbKh6PSVVVtQ4Gg5IHPQI8nx9ck4CbgSuBarJnvARsiUai4XBPmGQyqbWGRCxh2VrZAKYYLtNjZUyXSxsuU6oqyg1fwO91nhUSzvQdwB5gm4h8UvA4OYsoByYDY4EaoBLwAN7sYiDvZ4LsqUo60uNIK3AY2CMioYGM/wPREY0iGUY58wAAAABJRU5ErkJggg==\"],\"useMarkerImageFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', amount = percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"markerImageFunction\":\"var type = dsData[dsIndex]['type'];\\nif (type == 'thermomether') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"color\":\"#fe7569\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"showTooltip\":true,\"autocloseTooltip\":true,\"tooltipFunction\":\"var deviceType = dsData[dsIndex]['deviceType'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}
Energy: ${energy:2} kWt
';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}
Temperature: ${temperature:2} °C
';\\r\\n }\\r\\n}\",\"labelFunction\":\"var deviceType = dsData[dsIndex]['deviceType'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}, ${energy:2} kWt';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}, ${temperature:2} °C';\\r\\n }\\r\\n}\"},\"title\":\"OpenStreetMap\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First route\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.5851719234007373,\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.9015113051937396,\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.7253460349565717,\"funcBody\":\"var value = prevValue;\\nif (time % 500 < 100) {\\n value = value + Math.random() * 40 - 20;\\n if (value < 45) {\\n \\tvalue = 45;\\n } else if (value > 130) {\\n \\tvalue = 130;\\n }\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH
See advanced settings for details\",\"markerImageSize\":34,\"useColorFunction\":true,\"markerImages\":[\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic7b13uB3VdTb+rrX3zJx6i7qQUAEJIQlRBAZc6BgLDDYmIIExLjgJcQk/YkKc4gIGHH+fDSHg2CGOHRuCQ4ltbBODJIroIIoQIJCQdNXLvVe3nT4ze6/1/XHOlYWQAJuWP37refYz58yd3d6zyt5rr1mX8B7S5Xo5/0nPYaNFM1PY0gGqOhfAgQCNBGlWFFUAYEIeihigbhFdZQwt85BV5Gj9r/718R2XX365vFdzoHe7w6d77xnPkn4YpAtU0YiizNJcmPNkMQFkDiSlowHt2HNtGlTSJ6B+pTpsKTfKgTj3Pi8SMtFtEZnFs8d8dPu7OZ93BcCHtt0+OiL+FJjOiqy5K5dtLwD4PBHGvy0dKLYo8B+1+lAldv50FfmFzWX+84i2M3a8Le2/Dr1jAKqCHtl2y1wC/pEMP9ZRLBaYzF8CCN+pPluUkOKfB6qlmk/dBwTyt8eOv2AZCPpOdPaOAPjA1h9/SJX+TyGXuz0TZi4EcPBeOk+U+RErZh2YyMAyQJEoZUjFgtkCAEScgDyx1hmInTglqDj2U1X0WILaPbWvwHO1WummeuLONhaXHTf2wsfe7rm+rQDe133j/i5xPyrmCr+OouhSKPbdQ5fLiezTIYUBQGMJBgYWxMYSISZhbxgQT8wGAgDiwWxUvCiBxKhSKOqdh4OyV5+6XiEfK/kjVOXQ13apG+I0+adKpXaG0/Si0yZdvPbtmvPbAuCNT98YTBhT/8fAmEpHoXgKgPe/6gFGP0nwG8s2YykcaRCAYYQ5tKTkDVuArDEwMRF5AICS4VZ1AQBSr6oEgL36CBAvlKqIsyLOKQl5TZH4uN+TawDuY6o64lWTJX20v1S633uJNvfmvnbRERelb3XubxnAX26+5gDy6Y9HtrU/wERff1XjSt0WwULDmZEMawPOgilgQ4FaGCEygaXQMQyRMaxiUijUkAEAImIGAFURAOrVA1AmI1ZExGuqoqkVFefhyGtKDql4X4eHc6LxJof0VIVM3nVc4uXaHUPlo0Tpc2fv/zer38r83xKAd6y74iImO31EMf9REA7cpdVBY8NbA5+dFNqsCTQipkitBjAUsLUZNd4qm8AyjDMmJAIRhDzDEBEbJkBVAyJWQJ14AEaciIeSGicOgBeBWNHEeXLkXIM8UvFI4bVBCVJNfdk7STd5xOcp0LZzjIqV/eXq/4i61edM/eaN7yqAqpfzf62Nf5LP5lbko/DbCuxU4saEN1mN2kKTzQbIkuEIEWfVagRDEVkOyXCkVq0aDg2p9YYNAySVerU0WN1R27Jjo6ulMQ1V+ggAOgsjNRNEus/IiUFnYUy2kM23AcrivXh2RiTxjhx5iSmVWEWdpmhQ4qvwSBBrXVPfqDmuVsT7C3aZvKslyZcr9dpxdr81F8ynO/w7DuD1q/8y6kDw2872ticN0deG7wvQHXHmdxGK+1ibQag5ikweliIElNUAEayNYBCSRQRiYzf2rNtx11O/rC5d9dj+1aQyM2Pyz3WGozaNisYNWY7SYtgWA0A5KUVO4qAn3t4+lOzYt+Grh+bDwstHzvjA2tPfd1Z+39FTRhGpi7VBKrE4nyBFDKcNJL5OCerqUEXdVeEQb0mk8lECjR0euxe9cqBUOnoQ6RkXT78hfscAvH71X0Z5kf8Z0dH2CgNf2NkI0d0ZbmtElMtFVEAQ5BFIlkKb00AzFJqCGooQcJjv7t868P3/ubayZvua48ZlJt57xLjjB/cpTssXokK7IQNrbeoZ3pIRJm1aYSUW9cwixglZ7xNU40ppY7mr+sy2ezt7G1s+vP+EGfd/+fS/Ko5pH9/pJK04X6MUDSRapcTXkXJN46QKp1UkqNVqvpxVyLzhOajihh1DpVkmrJ7+uak/bbztAF6/+i8j62p3j20vbgXR+cP3LYU/Djg/KcsdEnIWERcRIk+hzWtEOYSch2U76tk1T6+84Tf/NCdni2tOmbRgy6T26WOiKDBhGFEQhrBhiNAyjDGiQp4DFgI8AChg1BGBXOC9p8QJ0kas3jvEcUxxnLgNpTW9izfdOqGWlve7+OOXrThk6qEHKtKehq9xIlWkvoaYytrwFYqlglgrcZxW+oXSz+ycpOLmnsHypDTIfuTNcuKbAvD2288x22dn7hrVnt/ATBftBE/CH2aCtqkZU6CI2hHZomS4YCPK+5AKHFB2ZNe2Nev/739/e9qY3KRnPzHtQp/LtnfkMhnKZDMa2oDCTIjQhghDC2MCCQITAyYxpmkhAIAZDDA7l4bOSeR9YpLEwfkUjXqMOE0QN2LU4waq9aGBX6/+d7O9sXnu3579jbVTx02dlEilL0FDG1pJG64cJX5IGr6MupY5duU1npIv7sTQ4196ytUDx8+sf+TN6MQ3AyBd8+L8W0a15zYw0d8O3ww4vC7ijlkZU5QctVPE7QhNEVlTRNYUjHcy7tu3fuuVSqXBF8z66962fMeIfDaHfD4nmUyWsrk8BdaYIAh9EFoxzExEysYoAQ5A0ioAEIpIBGZmAM459iKaJo6cT209TnyjWkOSNLRWi1GtV9A3sGPg56uvG1vIZ9N/OO9rM8jS9oavSOwqaEhZYh3khq9K3fdpXWsbvdR3MoYCV/UOVadcOvv2C/AG9IYAfue5j1/U0R5mIhNctxM8yvxLyMVpOduJyLRRnto1MkXK23axlB27sXtT1z//8vqDTt3vk/fMGnX4xGyhiEI2Qi6X1Ww2S7lCIQ3DkCxzQEQKYADANgCbW6UHvwcRaO6fAwCjAewLYAKAcao6UkRIBEniEtRqNVOrVKjeSFCP61oaqurKvqe237P2lnkXn/X/PT9l3OT9Eql2V90QN1wZdRqSuhukhi9T3Q2s9ki+NDzHWppeUqnG/qsH/+b7fzSA33ruI7ODIDh/RCH6KkEZAEINfhia4n4ZO0KzphN5005Z06aRaeOAcjP++4Ff3P/86hWTLjr08i3FfEeurS3LUTanhVwe+XxOwjAw1loLoB/ASgBrAdSAV232Gc0NyJGt70+27mlrzNT6nAEwDcBMACO892kcx1KvN6hUqWu9Xka9XsfgUP/Qjcu+Nf3g6bO7zj7urBNT1F+quxLXfUkaMmDrviQ13+8THdqYqvuLZpfq+qrJNXFDbrp87t0v/cEAXr5iduiTMQvHd2QnKDC9+bC9NUfF9kwwgvNmBGW5Q3O2SFkzAkaCg/71Nz9+2MTZ6rlzLs4Vi0WbyWS5o63N5fM5G0VRaoxpA7ChBVw3ANMq1AKoHUAewCwARwHYvzWctQCeaNUrt4pvgeha17Gtevt47+M4jrVSqZlSqepqjQpVyyX/8xU3VBHF2T//+OeOFbgXaq5fa75ENR3SarzDxDToYz846FTORbPRV7oHG9sm+qEPX3TEM3vc9pm9AfiBP53+T6Pbwo0Cd4aog4p/yXK+lDX5IDIFZDinGS7CckEM+JB//u9/e3Z8NGPTgjl/Maq9s8N2FNtcPpc1bW1tFIZhaIxJATwFYA2AtAVWh4hERBQByIgIE1Gsql8gou8AeAjAfQAeVdUvEtE9reFFIpIloiyATgARgCqALQAGmHmUtTYTRWHDhhaGYE0YYmbHEXZj//rBRc/fXTly5qGHEus2FUceCbxP4DShRJ2mvuIFboyqG5kNcNuWVM965MbNd71pAC99+vADA+MnR6F+TeAg6h1TeE/I2bbAFjVLBbJcpIDzZNke8qNf//yxKblZWz42+9Pj2opFbutop7ZCQdva2hAEQZGZXwGwDEBDRCJV7VTVfVV1BDNPUtXZqnomER2tqi8S0REAzgJwUqvMI6JBAM+p6pdU9f1ElGu1E6lqUVVZVYWI6gA2EFFijJmSiUIPsDbXmGT3b59V6Kv0dd334uLGYTPmHK7Q7lRi65DCawqviXWSrEm1PlvgWMh9KPbut+/77Ohtj/97d98bA6igo7aM+O/Ogp0l8BNFPQhyY2RyE0MqcC7Ia2jyGpksBYj2//WDCx9uk/EDZ8783JhiW5HbigXpaG9HNpvNMXMGwAoR6SWiUKS5KhERS0QqIgmAHcz8sqrOA7AdwCcB9AK4CcBvAdwP4EVV3V9VPwGgC8B4Zv4PIqqoqgPQYObEOadExC1A60RUJaLxURQaZqoRW0NEsm/xgI6u7rV9L295vmvGlKmHQ32vk0QdxfA+oYTq+Vgbi70mR4p6BEaKlTid98S/9f4MV7wBgF/66AEnFbPUz+z/VNTBiywLgxxCFDgwGQqR5wznOeR8+6p1657r6uopfu7wv4mKbW0oFvIoFovIZDIBEXkReUlVG6o6Fs2N/EjvfSczj2Hm/YnoY6r6Ae/9w0T0cVXdSkTfE5FsC8iTAZwI4DAAjxDRj0TkUABTACxS1csAzG39MHlmzqvqGCLKt1xZA0Q0QERtQRBkDZMngrcmNAeMmB08uHpxNsrz2pFtbft4TWInDZtSLE5T8i7uSKRS8XDjBX4fYbnusI2jMkt/tGP9rnjxrl+gICP4Riagrzb1ssKa4CkrYRhwwBFHYGSUOZJKo8oPP/vCoV846opSoZCnQj7HxUJRMplMgGblR5h5wHtfbE1oZAvIHBFtVtX7RKTQ4pSrnHOXAThQRK4BcIaqNkTkRRF5UVUTVf1462/TVPVSEfm2974qIm3MvBhAl6pGAEYAaBcR45zLiUiPiDxKRC6bzZpsNhtGUaj5fIG/dNTltYeeWja3ltbVcGgMZX1IWbUUqDUBbBA+OYxDPuDLSORq6KsN76s48MvzZnwwlzNDgaFzAIBAi0LKtGVtEQHlOaQCQpOHoWDWL+9+ZODCuV99cnTbmM5cIY+2JudZIpronHukxUWemavOuZIxpuG9H8fM8wDMJaJHVfV0ANcDOIyIPg5ghTHm+0S0UETWq2oCoA/AI6r6C2PMgyKyD4BPM/MggJ8COIGIFqnqV1T1YADbVXUjEfUaYxrOOcPMBVXdCmCutbZirQGIlIBwavucl2577NaJM6ftO1nJ9aY+YfEpvDryknamSNdAMQ1AGwxdc/DqDjz9k/7Nw5i96ixBSK/MhTRxJ7oUbracmWAoVGNCtRSCYOxLazfcN7VjdjK+beK4KAqpkMtpJpNRABNVdT2AowHUvffjAYgxZpNz7hUiuk9VT1LVWFX/iojuBfA1IrpfVRcS0Xne+6tUX33+M/zdew8AzxljLvPefxTA3xPRIufcpQA8EYUAFhPRSCKaKSL7EFGgqjtU1RDRZmaeGIbh1sh78s7LxM59R09um7585fqNdtqUMZOMMc4igE0DthSppcYWL80VTNbyX1QCPgNN1fJqDvzi0tnjQviObGia3Ee0JEAml+E8DOUo4pxaE4GUJz3yxJr9/vSIv+8uFAu2kM8jl8vBGNNJRE+q6grn3AZV3QRgi6q2AZjHzHNE5FEAp3vvv8HM8wFQSywvADAPwDgAi0TkPwDcBWDhcFHVh9FcXH9ARE4BMI6ZvyEiHwYwSVW/CeB0IlpERJeo6hwiepmIlnrvVzLzemZex8yDzDwZqlUikGGm6R0H66+evuPYafuNynvFkCCF4xjiBd67otN4C4GmEDAqTuVnR3++beWT/z5YfRUHio8/0dEe7DynJTUvswmmEiwxWcCDwGyee37j4ydNO6ucy+YmZMJQM5kMWWvHqmqPc24eADCzENEGAMvTNH2AiM5Q1W1E9GkR2cLM3yOiS0TkO0R0lao+zMy/8N7PBHAmEZ2C3YiIoKrdqnqjqq5i5j/x3n8bTQt8iapeKyKbjDGfFpEhAGOccw8EQdBhjPmQqk723rP3PrTWvhxF0Xgi6vHeayaTyx075fS7nlvxcPGgg8ZNIjHeSKRMdbEUIEHwEuCOA4DOvB25vSRnAfghMGxEFNRb7ZoM0HFNadFeIjvRgMFkhEDKbEl8Oqq7u3bs+/c9cXQUWo2iCGEYsqrG3vvHAPwEwL2qulZETnXO/Zm1FqoKVf2Bqh6qqr8SkW3e++tU9T4i+ntVnem9vw7ARQA6ReQ5AL9yzl3vnLsewK8APIfmovkiIrpWVWeo6t977x/w3l8nIluI6Dcicqiq/quqgpnJOfdnIvJR59wmEVlCRD9S1QeJKLHWmmw2hyAM9bhpp47q7q4d733aSVBlkBoNQGxgYPdVRZ82N5In9lS7dp42GgA483hMyUY0RXgwXzAjQgUtshp1WhOR5YgDzoiB0U2baqsPLB7z0oxxBxWz2Rxls1lh5gNVdbn3/rwWR68moi5VPZWZt4nIvgBGquoRAH5BRH+OprH4oYh8XlVPQXMvfIOI/BJAFxF1qupxRPRBIjpKVSe3dOtdInKbqj5PRIe3RHayiHydiMYDOIuZfyIin0HTfI4kIgAYa4y5UUQaAI4QkY8ZY5YR0aGq0kcE8k5NNS4t665u6G9r47xDCi8pqabsNbFe9WkoRvU0upYl8GunnqebX7kZQ00O9DipLbKjRfQTPWnXYyBTBxMBBiIML2IVkt20sf6B46d9rJjJ5chaQ0EQRAC2pWm6VlVXq+rZIvIXSZKELcX/Y1U9RlW/AWC8iJyqql9V1aOcc99W1SXMfAmAh1X1qy3O+rKIHCMiGRGptUqude9iIrqWiC4brisiDxHRt1X1KFX9qnPuowDGe++vUNUPishNLQkIiOjPVPVs7/02EVkLYHsYhtYYg0wm1FNmnZPftKF2lFPJisCIkhE1DFiFaNLr1i5R+PntGR5lFMcBLWfCxxbhrgkjgqMAjCKgkrWFX48KZ7RHJm8CziJLOXJpUNu4omAuOfbKOMxkKBOGHIbhHBG576qrrtLHH3/8QmaOdtdd/5tIROLTTjvtyc9//vN3BUGQs9aOA3CyiDxXr9dRrzfo2gf/Ljt1TpyYIMnWtQ4nVW2kNd+bri41fOlMADkQerb1p4/f+WGcaS9X8HOLUQIwCgCUdFGi6ehBt7k+3k4DqQ8cOd2+mQdPnP6xijHB+MAYhGEoqppL03T/J5544iRmpvnz5z+4Zs2a1dOnT5/+8ssvr5o5c+aMWq1WSdM0VdXORYsWHW+tXXbmmWcONV2jQG9v744dO3b0jR07dvSIESNG3HbbbbNFpHPBggWPtMTvVUREWL58ee2VV145bcSIEU+ddNJJ1RY4unLlytXTpk2bEoZh2N/f37dw4cKTrLUdxWLxvnnz5pnf/e53unDhwhPa2tpWnnfeecekabopCIIMEYGIyBjGCfufvmbpltuKY6a4LKkzCh8PpZu913g0oIsAOhOKMQTElyvYPrsY43IRP6uK8wCAYHrUo+gpiXoaG+LR0X5VaNgxNEAHz5pz6PIgMGBmBTCKiJZVKpUjjDEmTdPG/PnzPwSgLCJHoLlY/omqXgLgWSJauHjx4uNPP/30obPPPnsAwGNoLl+O32Xdt/a3v/3txnK5HM6fP/+3aJ2JAAi89zkAUwGcdOqpp+YvvPBCnH322fEJJ5yQA3CH9/5YY8yft0C+SkTmP/roo72NRqPjhhtuODCTyRTPOuusRy+88MJVd9xxx8cWLFiwiog+oqp3ARgVBMEO7xVzJ70/v2jdHbNGqu/16uq98WakmuQgANhsU98MRQwMP7N0iYxhUuybD/n3WzqlAMROROElzfY3NrXHrtTNFHTkMvkiGQNiZhGZ7ZzbPDx5IoKIXK2qZzDzd9F0T/0pEV2qqoeKyN8BwLZt27ap6hmq+l0RmQXgZhH5iohcpaqrwzA0RATn3DXOueta5buqeoWqnqWqT9dqte8DwPbt2zeKyBGq+l1m/giA7wL4map+jYj2S5LEA0AYhp0AvsvMp5577rn3Axi/YcOGxaoKEdkCYBYzqzGEMMgUWILRjXSopzfekFUf5wUKYXYQCoZhykcM08C+DMUMw7Rva8sHqHZCJFD1VtTDaYLuoe3xrLGH/Yu1NiZVtcYAQEVVy7vpmPNU9VHv/RUArgZQ9d5f473/qYj8OwBMmDBhPIBnnXNfAfAj59w5AK4F8DURmcfM1JrY/4jIrSJyq/f+XlV9vmVMPlEoFC4GgM7OznEicmPrB3hJRC4Tkc+IyI+897cFQWBay5lrVfVKVX30lFNOOUZV/aJFiz7YMi79RFQiIgbg2NrazHEHf7+70q1eGiwkROoteQkhOmIYp8DQBGUcYIVwOJMepCCAkBCooCAnUPVwXoU1rrXVoyi7nwgoDO1QyymwzTn34d7e3p8B+NsWFx4AYLP3/l4iuoKIHhaR/yaiLw1z6rp169Z57+cR0bUiAiIaVNU7ReR5Y0xcrVbPbf0ek1U1DwCq2qOqG4jofhHZUi6XAeC7IkIAvqCqIKItaG4LZ4jInxERvPevtK5fY+b7W+0eBGD78uXLx6nqd51z85i5G0Bore1rNJJsxuan1EumFo3w3mtKSupAMASNRJEACBk6ixWphWCaKs1tqegVUIWyiBcPIYhRQlLKhQccNDtW9YEIh0TkiciJyGFtbW29LfCCxx577PtHHHHEhdbabd77bzLzFap6jPf+X5o46Jf333//qWh6kP+P934HMx8F4HQA53rvkc/nl9frdYjIQbsw99SWy6opPvl8BQC6u7u3ENFfq+poVb1IRK4iIvHeX7dy5UpKkuR8Zka9Xv9WNps9n4j2B/DNkSNHnrV9+/ZRIvIhIjpMVZeoqlfVEcyQ6WNmpQ8+nyva9m4IO/XeQ1XFE6UKfYkUhyrTEVDEFkAWO4NuZAuAsPnDKlgFzih8ku0cU5y4NQiCxFrLAPYDUCOizxpjrgAAY4y54YYbvtwS5f1E5B9UdSgIgloURR8BIESEO++8c8qmTZtetNYeHYahdnR0wHv/pIhsrVarvX19fQsA5H71q1/dYq01pVKpkCRJXCqVaGBgwDcaDdfX1zcRwDELFy788JIlS96XJEnBOQcADSIKmfkSIsKwpXfO/bmItBljLlHVa6dNm/bIE088sR+AMUT0WRG5kIgmWWtfIWPcuPZJDJ9r90hIRVTEq5KAlBIIdYH0UCg6FMhZUvDvjSDVnZBhUhUSUijICxHCbDFXZGOMqKoH0KmqQ/l8/ptdXV0/rlar38rn8zs5hJmJmUM0jyPb4/j3h/ze+ylLly6dgr2QaepX3Hnnnefv7ZmdoyUamyTJWABoHvTtmbq6un4xa9asSQCuA7DSWvtSo9E4zHt/dbFYvKLRaKwF0E5EwoBENlKVMOPFkcJDCRBVUlEloLQTLgWz1987FAhImCECJVEh8Z6cdzBk20ITkIg4Y4xX1ZFoHuJM3XfffT/S29uLLVu2oFKp7HQ9/W8ia+2RzHyGqv6TiPzjsccei97e3kxbW9uZACYTURVNb7mIiIYmJIOwLUWqTqQVIqFEDFHV6nC7orDMBB22LOzhWbRC0LJRLalqGYqyQWAJVDPGVJIkqQPYrKq9AGCMmQoAaZpix44d2Lx5M/r7+5Gmbzn4822jVatWvei9/9M0Ted77/9j5syZawAk27ZtswCgqt0AtohIzRhTssZWDdvQkA4RtETaxAOqZSWWnXgR1Kr8/kTbG2ThtaAE9QQSZWIQ2EilFteyhoJCa4lxYMvf9xry3qNUKqFUKiEMQxQKBeRyudcVsXeC0jRFrVZDtVrFzTffnOnp6Tl2/Pjx944ePXrt9OnTzyGirY888sjLCxYsOERExhPRDGvtswACrz4m60pOqIMIBIX4ZqCYAWsZLXumAtid6z8A5DSvlgkKFkcMiBERqHUDiUu8994SkQCoEFF+jyPfhZIkQX9/P/r7+xEEAbLZLKIoQhRFbzugzjnEcYxGo4FGo/EqCejp6Tnv5ptvfk2dH/zgB8sWLFgAVS0CqHjvyTlnq2mFYF3VORnJICKwI2IFI0Qi7TCtLaYCVgnbAdoA6GRhaoPXhipIVJkEUCXP7CrleBAd2RHsvYcxpopmfMreaICZN6LpQWYRmZSmaeeuk7LWIggCWGsRhiGstWBmWGuxqwUFABEZ9ilCROCcQ5qmcM7BOYckSYbd/XuiTczcT80YHHjvZ6MZZ4O+vr5hx+14Va1Qa/M9WB0Asa+SUCcIRuAtg5QEBKDYrEJrwdhiIXhBRQyIJkMxQxQvkELh4RUq4kCJ2VHdOLiOx+YmmTC0trWwnQOgsvtoiegFInKdnZ3rRo0aJT09PTw0NAQAm0VkzvBzw5N/B0mMMU+pqhk7dmxXsVjkzZs35xuNhojICDSPRpPt27c/WSgU5hLRC95722g0aOPgWnbcW5VUBYCSJYBBChgQzWnt2J4BsJyheFkVr7Q6Hc2kZYU6ARSejCjZFN259UOrc6reOucMEfWpqnXOPQIAhULhN8PgMXNl3rx5Y4IgOIuZz46i6KyTTz55JBFVmXnFO4nYrmSMeTKKooEPfvCDs40x8621Z3d2dp566qmnxsxcArC1s7PzkVWrVi1X1QBAv/eeiYg2DK0upOgpiCBQIlIBBOrBOgTCCAAQ0jUQrGS1WF1vUPewLlTlKoQCOARewOqVUgzmtlXWTWuKiqiIVAAgjuOtuy1bgtNOO21ET0/PhO9973sQEXznO99BT0/PxJNPPrkDQAO/97C8k7RBVaO5c+ce19nZmb3yyisxZcoU/NVf/RVWrFjx/kMOOWQ9M3dXKpVRjUYjbKmGinOOnPPYWt04PZGhjHoQCZigAQsFpFwbxqlRpx6k6LI6gK5Kpz8zm20d0JHWQFAYTSUlALDexSNdEB+Y+nQxpZRlppSZ4ZybdPvttz9QqVSOt9Y+SkR+xYoVxx522GF4/PHHceCBB2LZsmWYPn06nnrqqQOZ+REiekZERr+T6BFR37hx47rWr18/NwxDvPLKKygWi3jhhRdw5JFHolarzXvuuee60jSdYFordxFJnHNI0rghiGc4jb3xUDEQEngyYEBrwx7KcuJHZzux1t79KZQ++iv5AHTnCadVBZGQhULh1SsIMfoe7KlsGRqTm5Q1xmkQBJtV9dijjz766f06bwAAEgVJREFUnpUrVy4EgIMPPjh300034bjjjsOaNWtQqVQgIjjqqKOwZMkSzJs3b/Xy5cstgFUA3rZF954cr6eccsrYxx57DJ/85CexcOFCDA0N4cQTT0S1WsWjjz4azp49+4l6vc5Tp049TVU3eu/hVXVbZUN/TH33k8c4DVRIiMFEohCjCIdXLC6VY+44DV+zACCEXiiWgnCkEp1EpKsEqqTEIsTq1Axg+eCy/kczp+QmqDZfuXpRVedNmjRpx9VXX32hiEBEsHTpUtx5551YsGABnHM47LDDcNNNN+GAAw7Al770pc8NPzdsUXe1rsOA7n4dBmjXK3NzgbHrZ2beWQDg7rvvxq233oqLL74YS5YswY4dO/Dkk09i7ty5uOCCCz4bx/FPRGSUiNydph71ap2W9T9eGGgsr4iqZSVVsLJ6Z5lIlU5srfmWAlgHtE7lDjgP5SjgAWb6MBTtoroMgpwoERTwniiJhwq5aPrxB+YOWwuQIaKEmWd573NBEHSoKosIpk+fjltvvRWqitWrV6O7uxvLli3DV77yFRQKhVeBtzcgd/2+exmm3bl3dy4kIowfPx4LFy5EpVLBpk2b0Nvbi+7ublx22WWw1ro4jgsARgJYVq/XUG/Uk2fK95+ypXxfrESGGUIEMhYGTP1ovQOYOr2+kcjvVt+K9c130cp4slyX4nDnBqYbRCAGkTZXUELIVtPeezeUu3rjOEaSJFDVpwEcmKbpLcMTnDhxIm644QYEQQDTPDvBNddcg3322ec1IL1e8d6/qryZOruDffTRR+PrX/866vU6kiTBAQccgOuvvx5hGKI15hki8lTz76lura/fUUt6F4siJIKCiREAakhB6BnGp1ST9lwbngJ2CfE99Zd4cPzIcDqg4xl4wQl64EE+BlyicCnYanHz4RMumviR9vO7C4UC5fN5JqKzVfXlKIomtzzGr5nwGwGwOxe+ngi/ntjuXowxe/s+0Gg0+ohofxG5o1KpoFqv6+LBn496dssPt6dcmWAtlCOCNRDKgJgxEopDoLRl60Cy5p5P4Hhgl/A2NbgmTuUGBeCBOUTokVZAtyiIFJSk5QmJlJKeyvaeer2u9XpdVPVxVZ1Zr9dv25PI7Q7M3sDbEwe+0Q+wt/b21vdwqdVqv1XVaar6eJwkqNdj9JY3bW9IKU5cZRwUDNPcuagBE2G7Kg5RAKnI9SD832HcdgJIARYOVdyknXtjoTpBoaRsTPOMHQy7fMutQy/qQzOr1arW63VNvd+kTc/NfO/9I3vTXXub0N5E9/U+v57Yvp7+VFWkabpYVc8DMJSm6aZyqcSNRk1fxOMHPb/5v+pQtWwgUBCxErGCiOJhXHYMuRkU4r7XAHj3aYhTAaC4rakI9dNkMMSWPBhMSsRKmjRKIyuuZ3Bzfe32crnGlVJJReQ+Vc3HcdyuqgPD4re3ib1ZHfhmVcDuYO4JxNaYetI0HYvmMen91WqVqo1YNqVdW2uutz9NSp3KTNpcxMEYgjEYVNULmvVxiwLVu09D/BoAAcAZXL6j7F9SBVRgiUwPkRJYCQaqrEoMWrrqp4WN2ZfmxXGtWq7UqFwuJyJyP4A5cRw/qKryelywNw7ck+58I336ZvtR1Uaj0XgewMEicl+5XPblcpXqtXJtk33x1KUr/6MAbnKdgQKsDFUVMTtUYFWBvpLvohRX7orZqyJU192K6tSz9Qv5HPcQaCpBZyvjRSiyEFIVkDioiBbL1W3LglGduWJ9LKDExnAtCIJEVU/w3t/MzIfsbiD2dn0jHbkrF+1qSPZkXHY3MMNX59ydaB5ePdNoNLZUqlVfrpSxOvO4earr5xvqvm8iGfggBFNIyiGYQwwQ4xwABqqLhmo+c885eJVf7NUx0gDE4iv9Q/JYc1+MDABvDJQs2DDYhlBmxD2Da6YNxOulW9dsr1TLWiqVtF6vrwawXFU/7Zz7TwB/FCf+MUuW1ylJmqY/F5GzVXVZvV5fWy6XaahU5q26asuA22L7hlbvR4a8NVAYKFsgMBACJZDm7mNHSZ41HpfujtdrovS7bkV58p/oRwpZ8zIIhwM0C0SLoBipCmqNnaHAhq3L7MT9D9mfhjIrrYRt3nu0fG9VAKd673+Npq8t82a5cW9ADdOb4bZdljfbRWSpNt9BeSJJknVDQ0MYHBqiwXRHd9+IriPvffpa4YBCE0I5grCFMRlSGFoF4DMt3ffDUtXLPfPxyzcEEADGnoNH01gWFLNmChQhgTJEOqiKQIQEAiPNU09+Zf3jfZNnH3yY9mVWasoFL16sMWVm3gzgNO/9KiJaq6qTdlfyewNv9+f+QNCGPz8qIgLgaFVdVK83egcGBk25UtWBel9f/4Q1x931yFUbYLWNIxgOoDYgDSJYE6IB8CEEjFKg1D2QdscVfHn9r/EaB+YeAdx8B9z0+Sgz8HxgeR6AMVB6hgzaVMk3Q/2JSQHvJOra+GTXlMPmfEi6o+d87NpTLyTeN5j5ZWae6b3fV0RuIaKZqmr3ZJ33BNzuAO4G0B7vMfOQiNyqzcBN8t7fN1QuN0pDJVQqJe2v9u2oTt9w0l0P/uNz3iQjghA2CMmEGXgOCSYDIqJuAk4AgHrDf7We6u/uPx97zO6x13fl1tyOtfucqRcXM+ZFAHNAmA2iu4gwRkBKos0jAVXy4vKvrHvslWlHHHZk2m1eQKJ5VfXOOauqG4Mg6FXVj4nIalVdpKoHqSrtsrzYed1VXAHsDaQ9caAQ0S0iMoqIPkBEDzWSZHWlXI6HBkvBUKWsQ2nf5uSA7SfeueTqFxPUxtpQAxMSmxBqAhKTBZhoBYALAUCBW3ZU/D6Lz8E1e8NprwACwKQv4nf1fvlUMWsJwEgC5oDpIVJ0EhGrJ6sAICCXuvYVqx8uzXj/YZPSWFbWelyHeA/nPRLvqwxa3XRN4COqugrNKPwx2ozifxVww1y3K4CvA95WAHdQ8xWHDwJY4b1/tlwupwNDVVTKQ9rfP6j19h3dsv+Ow29bdEWvUmO0CWBshowJCTZL3kQAW1pPTb1noPTK9oG0no7Cp9b/7LWi+6YAXP8zuMnn4rFG4kfnQ3MYgIgIU5jxDCmKCigBpE1xZlEfvPDSErffrFkU7BNQpSutxQ1PLo6zSerFi9RV/CvMXFXVQ1R1H1VdhGaIbxnAzgQ5u4vtLsUx8yMA7mPmbQAOJKI2VV2XJMlLtVqtViqVaLBUlUqpn0vloTofOhBVMptzv1h4dd4Yn7cR1GSJwwhiQhIbIjUBthBwJoC8ElzvUHqzKL5+/+l4zQuGu9Kbyplw4m04Ix/xjI68+W6r2gZifdI1dFSaEEtdOW2AJYG6hnqXEMaOnL7ptGO/+L5kjVks2/JjM5nIZKJAoihLmUyIIAjIGANjTEBEHSIyWUQ6RWSdqm5V1YqIpC3RDImoQETjiGgKM5eIaKOIDKpq4r2Hcw6NRgO1egzvUq3V6l5Hxhuys9OPP7T0lke7tj41nQNiG0FtBmojeBMR2yzIRNhKQh9U6L6kkMGq/7t6Ii8uXoDfvRE2bzprx0n/hc93FLiQi8x1zYq0CdAHvcdkV4V3Dupi9b6OgosR+wRGvU3PPuXSHcXcPiMGnvAvcJIZlwsjG2UzMESUzWa16SExZGxLGFS9sVbFK5SUAGBYWYoIMzN5BbnUgSCaph5xXCfvvSZJouVaw1NWejrfL3NK1a07frHwmpFsXcgRvA3hTRahNeRsHmKaXpZtIDoa0P0AoBb7SwZqEt+/AP/6ZnD5g/LGnHwbvtlZCAYzAYbzJwwo4U5xOl0aUB8jcDHUxUSuoQ4pJE0gmbCt9vFTLm4UM2NHDCxNlidDweiQOAyCUDkwFLBBEFhSZrVEqkDzHLEVAiA6PFBFE0pFkjhS9YjjVJ1Lkfg0sZ3SO+rI8NBSo7vvznuuz8S+lDMhwBbWhmRtVr3JgmwAmAhqAlolij+h5svfqMW4ZKiaFu49F1e/WUz+4MxFJ92GS3MR246M+bYSGEAizD8mJ4d6p+oa8L4OcQnUJzA+hhWnqU+gUdA2cPKxnylNHj/rmOrW9N7+F5JGOiQjyXIYcgC2zRejiVXFw5Np5Y3xMGxgxBMJPMSlFHtPUI1NG/eNmhNm8uODUzZse+nB+x78WVs9KXXaDMgYspyBNyG8iQATwIRZwIawYPOCQj4LICSFDNX9V6qJ5O5bgH/8Q/D4o3JnnfhzfC6yvM/IdvPXADpaLd0KoaJPNS+xmjSF1QYkTeEkVfYpGR8j9Q5WRKvjRkztPf5DC3j0iCkn+AQvlDdUu6rbXaPWn5KrCEEErTwXTTKALbDmRgSaGxNk26bmppoQc7p7ux546PE7ZHvfutHGUJ4DOGMRmEi9sSQcwgYR2GTgOCRvDFXVaJUU81sA9PcM+X92Trru+yT+8w/F4o/O3nbyrTiaGF8cUwgOIMZRreZegerDgB6YJiQSw0uqgYsh3sFrjMB5eE1gfAovHka9pjaM+ke2TxiaNnWujBkzOcxnO/KFXKHNBpnRAODSRm+lVh6q1odqPT0bkjXrnuW+oS3tLo1HsKGADIQDsAnhjEFAFgmHsDYCmYBSG4BMRgMQvQTQcYBOBwBVPN5TStd6hxvuPx9L/xgc3lL6u5N+hpGwuHl0u33a2N/nDiTSXxBIRHWCNMilMdQ7DSVF6h1YUxXvyKhD6h0CCKCCVLxa9YASKYlyK/AOIJAyCUFBDGImB4KlEEoMbywCCtQbQ8QhxFiEJqDYWLDJakBEm4g1UKFPDI/Rq16xY9AdZQzOXzgf/X8sBm85AeM5t8P0eXwtItYRbfZToOavCyDxKj81RCPgaKJ3iL1TAw9xCVgdvHcw6uBVm/pNvQIKpwJV2pkKBQCEFKoMYoKFITVGQQxPBsZYeLIwNoQQw3BAjiNEzNioQKzAebQzkJRW9lXcbXEqctx5uOryYUv1R9LblkP1+JsxjS1+MDJn7wkDuhKEHACQQqD4OUgExJPFq/EpqTglcXDqEXoPJYETDwbgROBVAQY7ABCIJQKYYQBYZogaWGMAMkhhEJiQPLMaG5BTlvWUsgXjvJahAxS1RqpfH6i5eYjxhfs/i7clj+rbm8VXQSf/HB8T4LOj2uwzgaF/0GZ2oeHuVqjq48zIQzHee4QiSLUZgwN4kDYdt0Kkqq38BM1XhYnAMMwKGDQ979y0rERIRbENQJWIPgDorF0m2Ei9Xt0/5N4njH+//zzc9XamRH5H0iAffiOC9gLOVeD8kXl7bxjyxYC+OqMv0VaoPsCEukAigNqg1EEEFlWBQKHUFC9SBoOYiEUhRDoIaInBiSgyBDpJoeN2m9qG2Mv1/SV3iir+s1zFbc9chLc97vgdzWR+uYIfugUnC/C3keUlHQXTaQiX7LUCox9en1XwIBENCqTcvM1FVe0gSAcMzYVgxN6a8IrrBit+IHFyrCF850Orcf/ll781Pfd69K7l0j/mJxhtLb4+ot2uDy3t1T30Vihxeml/2U1WxpVLPol3PA088O7/MwI6/ib819j2YDOb154vvBVSxfXdA+nEBz6Ns4G3T8e9Eb3mUOkdJsW++NT2UjpHVO/V5vrvrRfVh7f3pTNLdZyLdxE84N0HEEtOgMsRzukdcBUV2vRWwYOnbTuG3HZXw4J3wki8Eb2uQ/WdojW/RLz/n+CluKaZTMhzm4eJwB9aFHADFf1X7+X6h/4MG9+LubzrHDhM934KLyhoaSPB3/yx3Nco42+811UPfBbvWvD67vSu/0eb3enEn/K17RkeNExXvPHTvyfxeuVQQ0be9zn50hs//c7Re8aBw3T/Z+TScl3niuBm9cCbLLeXGjr3mA3yl+/1+N9zAEHQ6oA/rxLLBPF49o1Fl54vxVJ08Ge/kwvkN0vvPYAAHv8K6ur8BbVEnlNF6XUArNQS/ziJv2jJ5/Cm07W/k/SeWOE9UddvUJ5+pimpYhODTtyT1Y29fsOrv2fxhXj+vR3t7+l/BQcO0z2fc0ucEyeil+7OfV7xFYXI4gvx4Hs9zl3pPbfCeyA67cfmFiaziVX/BgCUcL1XGf27z/vz8S7vNN6I3t23oN8caW0//+lcF/0PC+4VIBJgZm2aPw3/y8AD/peJ8DAtOQEuZLfAQ0sK7Q0rbv6SE/Yen/L/017ojH8LZ5/xb+Hs93ocr0f/D6s769KBP+5xAAAAAElFTkSuQmCC\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic3bx5uF1Flff/WVV77zPeIQMJIYQxYRRBpBGcQFEEbVQQUXB6xW5tWx9+Cm07IYIitiJog2P7qu3UCN22aDs0KIIyg0CYyUhCyHiTmzucce+qtd4/zrkhQIIogz6/9Tz1nHP22buG715D1VpVS/gLktnZjg3P2wGz3RC/N9hBCHtjMgOxGjDZv3UAkyZim4EHQO4i2j3UkxXUb9kkcrb+pcYgz3aDNvLjOah7BSZvRnwLX/8D2axILM0Dtx9ODgGGt/P4GNgtqD6A764i3+iJ44dC9CD/jaRXyqzXrHs2x/OsAGhrL9sB8W/B7ARKwz/H7TAE2btAZj89DbAW6X4HHRknnzgW7KdU5fsyeMKmp6X+J6BnDEAzhLWXHYz4c3GlG8l2HgL3frDsmWqzR5KDfpnOykkIL8D4OHPecIcI9oy09kxUaut//CKCfp7qtMuwwb8DnrOd5nOcXIfJCryAOgEpASUwh5HiBMwKEAW6YF1chIghthtqL97uSxG5C8a/TXvsBLx9VGa/8Yane6xPK4C2/kd7EuWblIZ+itU+BMx93E1OFmLchqQpTmaDA0kAlyDSwSwizkAFfN84RAfOQASLCVACDVgAESOGDZjmSDwEtYO20bVVaPMCionjiPoe2eXNy56uMT8tAJp9I2X10GfxyQTJ4GtADn3UDd6NYv5niKtgyQxcYjinkCSYi3gHikeSLhDwXjHzTNlWB4hEYnRAgoUSmIIqxAQsYEFQA/JRNHZw+lqiTn90R/VGYuNKQl5j/cTH5JD3FE917E8ZQFv23b0oJf9GNnQd8PFH1y7rkORKLJ2BTxJcAqQOSQyXGEiC+IB4wZxHXMABeIeZQ3q/MBQRhdiDNGqCaMSiYTFBKIiF64FYKBbAigKLD6PFsYjt+uhOcwHdiUMRe5fMe8uSpzL+pwSgrfje+/CyK1n1dcBeW/7wMoZll4Kfhy95SARXMsSDSxxkIInifIK4gHpH6kAlggjOOwTBJEFV8FIQDcQCGIh6ighOFdMELQKYYF1Bg0KgB2ZuxG4kFqvw+cmoDG7V/cXk7Z9iulx2efvX/1wM/iwA7bLLPIe1v4m4RSTJuWDJI/+675OUB3ClCmSC9yA1cKn1dF3mSFLDRJEsAYk9wJxSTEzQGW3TXlEQC6G7sde/0gzDZ0Zlt5Ty9Arp4GBfnBWCgxghOkIhkBsxGK4QYgs0gHZAQxPtNLH41q2GH4jhNFRfwrzK20ROis84gLbkohLp4C/IuAXso1v+UFmLlK4gLc/Bl4AMfEWQDHzZIAFJhKQMLgWzhMayjTz0kyYbrt2T2Nq3a5WFE3HWqgYzNxeU81ao5ADVpJ2ldLI6G6YN+o3zStI+CF+9n1kvWcYub6hR330mIgEtIHSAYFgOVkDREegYmoO2QfOHCJ3X4thqDirn0rUX0Kr9rex/Uv6MAWhLLiqRDPwPafdBxN79SC3yS1y9S5JVoQpSEnylB5wrgSvTAzCt0Vqzmfs/32By8cs2xZ2vXGJHjo/KnlVz1aEsLZsXKVQkpIlXTHo6T8wFjU5iTELULFpEQmN8Gsub87lq2gy/5pUM7ftb9jtjgNJO07DQQHPBerMeYqsnztaGmBvabhNbVYivemRwfJWitADktbL7OztPO4C25KISvnolWWMN8OZHasj+L5Luih9QfAWSmvVEtwquAi4DyWay6aYHuO/8A1phYOkd9sbVTbdgVlYqJVk5wycpWZaRJY7E+2i44L2Y0Zv8CkgMCMQ0xOCKGAndnCJG8m5ueacba7pkw/Pksp2rvrkH+3/kXqY9fx+cbiA0HbEDdIzYBmvRE+12l7y1GRffsRUj/JBubR6xdbQsOK37tAFol13mOXjzzymNrQL+/pGnS1/FZXvg64IfAKkqSTnBVRRXEaQ8g7HFK7jnU/NHde7tC5N3RpKB4WqpKuVq2cpZSpJlkvrMSqVEvE8ty9JClSJJJIr44BwWY0xjNC9iWZ4HH2OUdrsjzpm1Wi3X6RTW7XZpdNviwsToQfodP92tPpj9z1nG8Pxd0M4mYtugEygaGdpWQgO05aCxFI3/+Mhg5WsUQ3vx0Npj5GVnh6cHwCVfv4x0ZAViH9py0WcXIpUDcPVIOiBIFZIauKrgKg6z2Sw8c0m72XK3uA+MuMq06dVqzSqlTGq1uqVp6iuVsqRpGtIsM++ceO8NEQQi0AGmuKAElAEfYxTAQowUeZBu0U3zTkc7nVzzvGOtVpdGqyHa3rTpUPvSjpVyreDgc/dGZB3aVrRlFE3QlmBtoxhXtPUQlr/nEVTceRQzd5c9/+GUpwygLf7Ke0jHykjrS1suavpV0vqeuDqkdUEGlKTsSQYUqcxmcvky7v38c+6yt/xqvHTwvEp90OqVTKrVitVqNcrVasiyTBLnUhEBGAXW9ssqYD2QA1MT3bQP4g7APGBOv0w3M4kxxm5RxG67nUxOTkq7ndPO29YYb9pQ55a1z3U/Opb9P3wXA7vtgbbXow1HbBk6aYSGoA2hmFgK+ggnxoEPYENR5r/3y382gHb/xQeQFm/Gr/0ImOuD9zXS6p4kg4ofBKkJyYD1gCzty9IfXlVsumuXm7NPrC6Xh6sDA1XJyhWmDQ1ampa0XM689z4FNgP3A0uBdr8vSk/veXpceFj/9y39/6ccAq7/vQLsCewHDMUYQ7fbtWaz6VqtXJutCWt3OtJubBo7rPjMXsmMA5cz/60vJ3TuJzQEm1TiREJsRsKEElqrcEWfE0UJcz4P2Q9kwfvv/ZMBtD98I6XW+QXZkj0Q9uzdLZcgA8OkdYdMF5KakgwK1AWfPof7Lr52vJk27qufVq0PDKTltOoGBqo2OFi3crmszrkB4CHgAXpc5vpgSf/7MFAD9gVe0AcHYAlwK3Af0AQm+gDrVmV2H8i5ZtZpt9s2MdGwRqNJq9WUVqsZ9m1f1BqqxAr7v++laH43sWHEhhAaRhjzMBkJExNgJ/XhWUK+YDVx9FWy/9nbnN4k27oIQK39FUqLFmLhlX1beB+U67jMIRlIGiF1kApen8PCzy0cYZ91Dw2/dadp9ZqrVWtFrVZLa7UyWZaVRGQc+D2wiR73zARKqhqccwqIquKcmzSz14rImUC135uWmZ0nIrf0fw+oqjjnhJ54d4AGPU6dJSL7VqvVoSRJOlmWSKmU+KxSssVjH6zOa/5g8453nn8bzz3tEHzpDrTrECeQKnjBJSmhdQ+izwEWkNx/Oez9ReB924Jpmxxo91+wl0rjjc4vO7d3wQKu8kP84CyyIUHqgh82/ICQVA7k3m9evybstXb98FtnD9Xrrj5Qp5RlWq/Xnfd+AFgILANSVfVAHZjVf4E1EZlhZs8VEWdm3xCRD/QBfqSjIhtU9SIR+XszUxG5T0Q2quokPV25EZjsv4wA7AI838wajUaDZrujk+NjyWSzyZzJ/3pwTnrffPb5u79B2wsJEylxUonjkTAhxPENaOcURBIADfM/6bT+H7L/6Uv/KIBmiN1z/tWS3TqESM81JMlXSQb3QIYgGwQ/CH5QoDaflT+7cXNjYGLl4Lt3qA/W3WC9rtVqxdfr9QxIVPVeYKNzzoCOqjpVLSdJUg0huCRJusC4qp5FT7wPAlYCP1XVkf5zs4DXAbsCtwN7OOfO7nNiRk+EWzHGwntvzrlSn0N3APYzszjZbE50Wu1sfGKcZqNtu05+ffO0aitlj+NeSphcTGyATkCcgDhmhInlWJziutst/E1D9v2nIx/rmH08gPd+7ijSpc/BRnpW1+RWXGUd6WCGH+5xXjJo+MFhRpcu6q6+a2jJzHO65UpdBgeqbmBgwMrlciYiqqqLnXOFqk7vc4WPMRpQTZJkB1U9FEhF5Fwzu9DMbnfO/VBEXqyqrwGmHKW5c+4XZnatqr5dRA7y3p+uqh8DVFVvE5H1QAtwIhLo6dSNzrkKsEeMMQ0h5GNjY9rpFH5iYizutemTrjTvuS0G91iANcYJE544HoljQpyMxOZcsAN7SM3+AHHPhbLvP/9ua7zc47jP7OPEhz+KdcA64MIdSJKBBxFFpPcGigmx1Tc/f9G0syZrtZofHKi5en3AyuVy0gfveufc5qIo6mZWAWaZ2Rzvfdl7/3AI4SozGzCztqqeG0L4ELCPql4QYzzezFRVH1DVB8xMY4zHq+qFwHwzOyOEcF6Msamqg865K2KMK0XE05vaTDOzUgihqqojqnqD9z5kWZYMDAwmpVJGvT4gS6af1baHbzyY0FQwD67nzBVngMNz4xYcbOXHLMRzzEy2CyB3nfdicQ/8FOvMRrsQu78A2xWJYCaY6zGwxf1Y+quwbNrp19QHB6u1Wo16vUalUk5EZGdVXaiqQ3meO+/9BhFZrqqr6OnAE83sH/ttV4HvAB3n3Plmttg5d6Zz7sNm9i0zu69fvqWqH3bOnWlmy83sAhFpiMh/0JtgO+/9e1X1TTHGATNbb2YPOefGVbUEDKvqQmBOqZS5wcE6lXpdqgODlWXTP/Iblv48RePeOOt5wrUvpZrvDt3/7WMxS9zi/+aezx2+NWSPssImeqbY4j23XJDSeszvBAJODFHBWcrIA1c1sr1zN7DHjqVKhUqlQqlUcsA8M3tQVV8MNEVkjqoGEVkLPKCqVwEvA3Iz+yDwG+BMEfmtmV0hIifHGM81e3T8Z+p3jBHgTu/9h4qiOM4591ERuTKEcIb0JCMTkSuAGWa2v3Nujpk5MxsFEhFZ7ZybWyqVHq6pOrQaJuPOcxvNve6sjy2+l+Gdd8EkIAJ9LFHWYFMLokXvN9vzQWCLE2ILgPbA53bS7qrfi3WP7l+6GvxcJPRG4Kwn5LE1l/FVu63e4fM31CsV6pWKZVlm3vshVb0S2BBjTAG890PAAar6ahE5EjjPzOohhLO89xeaWQ6caWZnAS/vA3Wlqv5eRFqPAbEmIkeIyCtCCAeKSEdEPqaqfw/MVdXTReRCEXHAe4B6COEqEbk7y7LNIQRCCEWaprO894eWsmyTxuhDCG7NzH8s77X+jBcyML2AsBIxQ0xwKgTdCcl/h9kRwAJh/fds4blz5aAzVz+aA7vtE527fi7WXz87/wCwO+ZAVIgFuODZuOqmkfrrJirV+k5ZkkiWZWRZtqOZbYgxHglUnXOJiKwMIdyRZdnVIYTXmdl6EXm7qq52zn1BRD6gqv8CnAtc65z7sarua2avF5GjeQyJCH3R/IaZLXLOvSHGeB498f+AmV2oqqu89/8nxjgmIrPSNL0qhDAjhPBiM9sdiCGEpvd+JE3TOTHGDZVyOSpUNtVe/fMZI9cNMn32LliMmBnmIs4ghnshHtHrybU7Iq9/A3DRFgDNEL1l/U6uUhzRN8wbUD+vF8PAepecoHEandburbkvv7mSpZTLFSuXywnQCSFc1xezIefcc83sOOCQoig+18fgy2Z2gZl92cyON7MvAb9wzl2vqqfHGKfW2rmqLnTOPaiqK1XVJUkyD9iD3grlPX0wNwIfU9WXmNmXzGyVmf1cRN7rnDtdVS+MMTpVfTcwA/gVsFBExsyscM4dVy6XnRmxiGqTw6/cYcbqKw9nmo6CbEREURFIDPW7IGETyAwIL9PO+oZZ7516gLOP/budTZfuIXp3FT89Q9yvcaUhpNTzKLuy4TJlcmLZWPqi+2P9wIFqtUq5XDLv/d5mdqeqntLXQaNm9gBwrIisMbN5fZ10sHPuJ8C7gbu9919X1XeZ2dH0ph8Xq+p/A8tFZJr1RObFIvICM9vVzB4Efq6ql5rZXSLyfOBvRWRXVf2EiMxxzh3vnPt2jPEdgJjZcF+kZznnvuGcK6vqwar6ehG5XUSeZ6YbBcOMxIrx20v5is2UXA0NPYesRgfqIf4Bs5l0H/yDWHUZq45eec63/jDpALTg1c7dNouox9Nafh1KgKRnrhWPakKINZrtlzSnv7ZWq5UlTb1kWVYG1hZFsczMFpnZ61X1VBFJrfeKvm1mL+nruLkhhKPN7MNmdngI4Twzu8Y59wHgWjP7sIhcaGbvV9WXqGpZVVv9UlXVl6rqaX0996GpZ/v68jwze4GZfTiE8BpgjpmdZWYvCSF818wwszSE8E4zO9HMVqnqMhFZm2VZ4n0qWVay1ow31Gg0DydaBTMH3qHiEGeoy2kuv4YY3wy3zUL1WOjLq133ritxP3w+2HSgQTrwc8p7DeBqDl/ueVxi1o7Nimza5VNFqVqlkmWSpulBqvrrT3/603bTTTed6pwrPVZ3/TWRqnZf/epX3/ye97znZzHGwVKpNEtEXhVCuL3b7dLpdGzmio9XZKBb4PIKoQXSNEKrS3dJh2LytfRiFqMWTvmDe+m3X5WYne30+kUbnFkvCC38L0U+HdZ2KO9uSMxw0Wh3xyanvXHCe79TImLee8ys1O125998881HOec46aSTfrd06dIlCxYsWHDvvfcu2n///fdutVqNPM8LYNqvf/3rI733d5xwwgnjU4MaGRnZuHHjxk2zZ8/eYfr06dMvvfTS/VV12pve9KbrZWrSvhWJCHfeeWdz8eLFr5kxY8YtL3/5y1t9cOyBBx5YMn/+/N2yLMtGR0c3XXHFFUclSTI8MDBw1THHHON/+ctf2hVXXPGyer1+/ymnnHJkURSrsiwrpWlqeZ6Lc07Gh49bOty4ZJB6UcXFnh+gvcbQfAbGlcDrwaabdcfMznYJt66Yha2+A3hLr4tuBGQIbWe0V7Yp7xlJY5mQPjfUD16YJok453DOzVTVmycnJ1/kvXdFUXROOumkF8cYJ0XkkBNPPPEgM/secBpwu4hc8etf//rI4447bvyEE07YDNwAvA04cqt535L/+Z//WTk5OZmedNJJP6PnsgJIY4xVYHfgqGOPPbZ26qmn8sY3vjE/4ogjqsB/xhhf6r1/N4D3/lxVPfG6667b0O12hy+++OJ9yuXywAknnHD9qaeeuujHP/7x604++eQlIvJKVf2ZiMzy3m/IshJh2iE1Ri/dn8I2o/lGOg8NQLeKCuDWYr04l0tX38rNuoOjU+zpdHHSW2EAKiWE0PO2FTW6K0qE8fWmyXBWqdfTNLM0TUVV91fVDVtzhqp+xjl3HHC+mVXN7FQz+5CZHaSqHwVYt27dGjN7jZmdr6r7hBC+r6qnq+q5ZrYsy7LEOSchhAtCCF/ql/PN7BwzO8HM/tBut7/Sr2uVqh5iZuc7514FnA98N8Z4ppnNz/M8AmRZNg04X0SOPeWUU64WkTkrVqy4sq8bVwP7eO/FewdpdQjxMwgTG2g9tAMxr6BqGBGTdAtO4QFH7ua7qLoXrjHvEQBtGmopph6LghZCc023M/yCr5mZgk2tCBpm1th61aCqJ5vZ9WZ2DvAZoGlm58cY/11V/y/ATjvtNBe4S1VPB74FvBG4EDhTVY9xzomqmpn9j6r+SFV/FGP8jZnd1Tcmx1er1dMAhoaGZqvqN/ov4D5V/ZCqvkNVvxljvDTLMm9mOOcuNLNPm9mNr3zlK1+oqvHKK698oZmpmY2LyIRzzkTEvEinW33ORbTXdqEbUQUjwUiINn0LTtaYi9meiWg8ACkO6GPQQaRCtBwfCyIlEEOHO6jf1bmkSJIkAENmtjaE8KrR0dHvAh/pc+FewMMxxt+IyDkicq2q/peIvK//tlm+fPnyGOMxwIV9Sz1mZpc75xYCRbPZfDO9KcjuZlYDMLMNZrZSRH6rqqsnJiYAzldVAd7br2d1COHMNE33VtW/FxFijIv6n2c6537rnFNVPRxYd9ddd80xs/NDCMc659aratk5twFI1dUXEJM2lnchRlwUMI+TKpCjZEjYT0PsJBJ1fxwHA2ByD6KG8wENAWcZBYKUM4b2ayeJK8eolqZJbma5qh5YrVbX98FLb7jhhi8fcsghpyZJsjbG+Enn3Dn9acyXVRURef+ee+65B70A0b/EGDc5514A/G0I4c0A1Wr1rna7japuvadwd9VHtkEPDAxM9EV4tYicEULY0Xv/9yJyboyRGOMX77vvPpfn+SnOObrd7qdKpdJbzGxP4JMzZsw4ft26dTNijEc65w4ErnbOdfO8GEqSxJLpB25ktFzDaUSDoNERDEQdjvsxDgQ7VFRDglgZo95TZPogJlUsRpCIRkEAqQ672ryuiQQRSVV1DzObEJH/k2XZJ/uK21988cXv74vyHqr6MTMbT9O0VSqVjqHn9OTyyy/fddWqVfckSXJ4lmU2PDxMjPFmVV3TbDZHNm3a9Gag+pOf/OSHSZL4iYmJep7n3YmJCdm8eXPsdDph06ZNOwMvufLKK1/5+9///tA8z2tFUQB0RCTz3n8QwLmesynP83enaTrovf+AmV04f/7862666aY9RWSWiLwjxniqiOyRJH5pURQastkxteowRW6g1tsVZgZiRHsIOBBjEI2VBNXe3kUAtSaQoma4QsEJFoQ0rbq0hjmX9yTKhoHRWq32yRUrVnyn2Wx+qlqt0g9R4pyT/pywBAx1u48E+WOMu91yyy27sR1Kkt7y/PLLL3/L9u6ZIhGZ3e12Z2/93LZo2bJl//2c5zxnLvAl4L4sy+7tdDoHxxg/MzAw8KlOp7PUzIaT3to+NwYMyUpYXiBqRAOHEdWDTiJTLkHFoYVsUYxCCwgQlRiFEAQtDEmHVBLnvS9UNdCLV7SA3efOnfuqkZER1qxZQ6PR2OJ6+muiJEkOFZHXmdmFIvK5I488UkZGRkoDAwOvA3YVkWY/LlOYmSVpDciGevtrQi/Or7FvPGg/YnCDc72NnvRKdEqkidDEaKK2DmMN4hLvk2az2eyISINe7GIDgPd+N4CiKNi4cSMPP/wwo6Oj9EXqr4IWLVp0D/B3RVG8Kc/z7+y9995Lgc7atWt9/5YNwKoYY8PMmnnezXGuRKBFpAk0UBqYTqDoFrxMSTDskTCJ1VCGCQgmZg4vZoZnMoa8UqkMls0siTHuY2YPbauzMUYmJiaYmJggyzJqtRq1Wu0JReyZoKIoaLVaNJtNfvCDH5RGRkZeOmfOnN/ssMMOyxcsWPBGEVl37bXXPnDyySc/L8Y4R0T2EZGFIhLE2ySiE5jMwBCi9QL+Ig7byotvWALxkXi/yRCQRMU5jFhYCXGaxDgeuk2DctL39TXpBcCfkPI8J89zNm/eTJqmU55rSqXS0w5oCIH+epZOp/MoCdiwYcPJ3//+9x/3zNe//vWFJ598MmY2ADRU1RVF4V0+TmahEYxpBBVBAog6ByJWe4ThIglqG3CyCmwepkNmKIZEwUV1DlTF8gZx3GBGGmN0fZ3x+B34j9Bm59xD9BjdqequRVEMbz2oJElI05QkSciyjCRJcM6RJAkissWCAqgqU/NIVSWEQFEU9L3M5Hk+NbnfFq1yzo1OratjjPvTC8azadOmV/bvmWNmDeecxBgTjZMSi9CIQYcR5zykeFUiINQQAZEHzeLaxDTeLUYKzEPcAWLcY6ZmipppjMFI8tEGzeXW9TtnWZYCrFfVA+jtBngUOefuEhEdHh5ePnPmTN2wYYMbHx8HWNV/BmDL4J9BUu/9LWaWzJ49e/nAwIB7+OGHa51OR60XtdsdyNevX39zrVY72Dl3dwghiTGaH1+UabGpFQszEg3OgSgizjnE9u8bkdscdqczC/ejyeL+Mm6WYQ3MopmoBefMJOk21laTxuKKmbrY83aPmlkSQrgeoF6v/wxARO4WkearXvWqHdI0PcE5d2KpVDrhqKOOmiEiLefcfc8kYluT9/7mUqk0/qIXvWh/7/1JaZqeOG3atGOPPfbYrohMAmumTZt23ZIlSxaaWaqqm83MVFVKnQdrne66ajRJXHSiEcQkGjaO0lvOkSxB7QHnE1lCHFyLWc+3H2l5xKtapqYuRpNue6ySFSsXhKCxv05tAHS73dWPEZ3k2GOPnT4yMjL3C1/4AqVSic9+9rOMjIzsfNRRRw3S24X1bJysXGlmpec973kvnT59euUTn/gE8+bN4/TTT+fee+89/MADD1zhnFvfaDRmttvtrK8eGj3VECzLVy0IjfGKRiOqOtRSU0vFaE3hRJi2nsKWJ2xuL9fa7Nc7t7HXtLNGjOwgaoWpYEoSY3emaPeAPG//CkoVEcmdc4QQ5l166aVXNxqNI5MkuQ6we++99yWHHXYYt912G7vvvjs33XQTCxYs4NZbb91XRK53zt1mZjOfcPhPkURk0+zZsx9cuXLlwcPDw6xcuZKhoSHuuusuDj30UFqt1jELFy5cVhTFXO990teteVEUxJi3he4+MXYjgDnUjAg4orWm9nJo2GGmm2bLEnnrzRP6k8MPe8QS41EwkwTFFIsaRIr25qt8vmY8uHlV770lSbIaOOKFL3zh/y5evPhKgOc+97nV733vexx++OEsXbqUiYkJ5s2bx2GHHcbvfvc7jj322MV33nnnI6HUp2nSLfL4PVJHH3307BtuuIHjjz+eK664gvHxcV7xilcwOTnJ9ddfnx1wwAE3NZtNt+uuu74GeKgoCosxGq1VY3lr9CoN7CjO1KI4ExEUxZNN4SSUXiwvu+YT/bCbbRLldoSDMTnSO1seogU1cTEXb2YyvvaOscHB31dG0zdrjFG893cDx+yyyy4bP/OZz5yqqqgqt9xyCz/72c94xzveQQiBgw46iO9973ssWLCA973vfe+cum/Kom5tXacAfeznFEBbfzrnEJFHfe87erdY8F/96lf86Ec/4rTTTuOaa66h0WhwzTXXcPDBB/O2t73tnd1u99uqOlNVfwVYu92VOe3rapMjCyei2lwfxXDOnMTgRQSTl4OB8QczVkJvcyPnvGnuJDI+ihWvAKaJ2kIzqmoiqhBUpNMZr8/ace8j1snzljrnvHMud87tF2Ospmk6bGZOVVmwYAGXXHIJeZ6zdOlS1q9fzx133MEZZ5xBrVZ7FHjbA3Lr348t2+Pex3KhiDBnzhyuuOIKGo0Gq1atYs2aNaxfv54PV+O33AAAECZJREFUfehDJEkSut1uDZihqre3Wm2X5+3unPy3x2548Kpu6sV7QROPpIL3XkYxDu8teev/Kjrjl+dctnpF71W1uFnjzvVHnIV+rSBCRDDBDDWl3GmM/LrUWTbS6XQkxqiqehuwT7fb/Y+pAe68885cfPHFpGmKc45SqcQFF1zAnDlzHgfSE5W+W2pLeTLPPBbsww47jLPOOot2u02e5+y1115cdNFFZFlGv8/7qOqt3W5Xut3cyvmDGzutkSs0UlLFMHEoiIlhbJjCR8PcIWLj1p4o90kvPegaSe/bC2wOcLsZY50CaRdYNzfJI6gbWL3LIe+euyh9+4ZSqSKDg3UnIieq6n3lcnm3vsf4cQP+YwA8lgufSISfSGwfW7z32/s93ul0NojIfFX9z0ajRbPZtP35wZyVt/zbKomTcyspVkqFSoaWUkSEGcCBmKy2uN9id9LCl8NWu7NU9UKsdHEf5YMFNgjgpLcBFpCiPbkTYbKgvXp9nnes3W6rmd0I7Nduty/dlsg9FpjtgbctDvxjL2B79W2v7anS6XQuN7MFwPV5nlur1RLXfXi9FRPtojM5B3D9bXwighNYh3Fgb65culgtfmEKty0A+qHWFZrvNG/LPEddI3WGiLkE8JiKt/TB2340viD9/X7tdtva7bZ18/xhM5swszfGGK/bnu7a3oC2J7pP9P2JxPaJ9KeZEUL4dYzxFBEZy/N89cTEpO902rpP6boDlt98adNjPsE0wSQRIxEDle4ULhp3nu/Xt696HIDy6qVd1Asql/ZMdXy7F5ssiUURvDcRr1i3NT5DWxsmKvnitZONhms1m1oUxdVmVu92u0NmNj4lftsb2JPVgU9WBTwWzG2B2O/ThjzPZ5tZmuf5NZONhmu22jpkS9fE9sho0R4fRhDvRRMxSxLDY2OIvq0nmXIJKm05bWn3cQACOHFna5hzX1+MM8ytS5yId+AEBMErsui671b3Gbjn2G6n02w0GrRarY6ZXQUc0Ol0rrZetOsJOWFbYG5Ld/4xffpk2zGzTp7nd5rZc4HfdDqdvNloWrc12dqrfs+rF1373YokvU2ECeC9+FTEsGQjPbcfxLlLXJJ/+lGYPcr0n3LPerS8D/DbHhfaWxOxsVKCeYdkHhMvRAvDS2/58Y0H1X9fGZ9o0Gw26Xa7m+htAH99nuc/2NoIbP35ZET6sRZ4ayCfTB3barvb7f48xvhK4A+NRmt0YmLSJpsNe/70m+tLbrrsRrUwLe0fB08F6Z2rtzGI7+jpPq6MoTRfTlo6sl0AATq+/U+az76h/1AVlZg6o5xg3uNSj6WefHz9kj1orrCdSsvWTUxMyOjoZtrt9lLgTjN7ewjhB8CfxYl/zpTlCUpeFMUlZnYicHur1Vo+OTnpGo0GOyVLVkvzIR1bt3yPzBNTJ5Y5LHGQelPE5T1JBHTObb7onvFYvB4HYO3kVWuIpRSTb/aXLSd6WJQ6c5nHSg4p9xqS+67+9tCe9QcPk+66NZOTEzI6Okqz1VpsZjer6luLovjZ1jrxyXLlE80Dnwy3TX0C62KMv1PVk4GbGo32srGxMRkd22x0143sOfTQYXf+5t/qpQQyJ5qmWOKQLDHxxiLM3tTXfV/TIknlnSselxXpcQACuEp+Tsx3HERp9Pz/7kWZp1tySEkg8bjUiROscsvln1v7kl2Wvrzb2LhhbGxSxsfGtNlsPqSqV5jZ60IIK1X1uq3r/1PB/BNBm6Lr8zzfqKqvUNX/nZxsPjw2ttmNj08Smps2vnju4iNv+cnn1qVi1VRESg5KhpQTXObIUXdEP/bRiPnsHZzaJ7aJ1bYuykkPt73xbbR+Zu8N2P6ibqycQrmMlZ1ZKYVyIi6VOHzzjz9/99ELlh8dWhvXjI6OsmnTJhsbG5sIIfwXsIOqHhRj/Da9I1mPom0BsD0An+iZrWg8hPDdEMLzgel5nv98fHx8cnRs1MbHN2unuWHkmL0ffMXNl3/+LtEwVErFlRJcmpiVykgpRcXcJrB9eqJbP9OL/5q8c8U2T7E/4WnN8J2df+iTtQKc3If7KyGyf7ONNbqUOoXQbFtoBaJKZc3hJ33igF89sOPVOcNzhoYGQpqWy/V6RUul8qCZHqGqS4CFMcZTVHvO2a0t67asLvC41cTUiuIxn+qc+w/ghc65nUTkd3mej7dardhqtbLJZlMzHXv41c/Z8MqbL/nMIrHG9EqCr1TEVVOjVibUM0g89wFTx14viWHHkLxz9du3h5Hf3h8A57x++i9jrLzV+ZZgzACe6+B3HqYhaIw4J+JMkbwIQyvuvHryZS9//i6tXB9YuSFMC0Xs5W2KoeVElgCY2dFmttjMfk7v8M1g//qWds1sm56XKRAfc20N8J/0gvgvBO4piuKOZrMZx8YmmZycYPPmUXYb3LT+pXtu+ptrf/iJEbHujEpKUstEaplRKxNrKaTCCoR3AB6RJTGfPekle/s5Px3bbuzhjx64bn9tx92yrPigS8b+AcgQNqP8ohuZ28zxrQ7SynHNDtIulE5wxaEn/PP6WJnDT24faJfSSlKtlsvV6oCVKxmlXgCpDOypqrur6m/NbF2Mcb6qvlh7Z+keJbZbr3+dc8E5d4OILPXe7ygiR3rvV5jZgzHGVp7ntNtt2p3cmq2m73ZbjeOfN1nz3TXx1h+fv2PJaVYtOStnSK2C1lJirUxeSlmPcQwwo7e9b+gifPlf5e1rthm+fdIAAoRvzXytSHcv51rn959aCdzcDcxsdPCtHNfuIO0ca+UaWwEGZu+16pDj3vc3Ny1Nfr1oXW1WuVz2pSyxUqki5XK2dSQuU9VhM9vFzKap6gozW2NmDVUt+qcvMxGpi8iOwK5JkkyIyEOqOm5mRVC1kOd0Ojndbod2u0On0427zypWHLlv53X3XfWD69ctv3V+NcPXMmeVBKplQqWEq5eQUsZalBfSOw2PhuqHTct3J+8e+dUfw+bJZ+345vR3kbTrkE8dR1iF8LsisGujQ2xFrNUmtrvUmwXdboHP1RUvPOmfNyYDO02//BbuGu9k8yqlMlk5w7uEUlYy55A0TcQliTkRw0xxHouxd2Cot3PT+kcbxEzEQIIG0SISo1qet6UoAt08aLvb1uGyrj/hMD0gTK7ZeP2PPj+zlGhazoiVBK1lZJUSRb2CVlMk9ayldzJ+DwBi9gG01pV3b3xS2Yz+pLwx8RvDn3RZdwzrgyhsxvhJMPZq52izLVmzwFq5SbdLaEeNndyZlOutw97wwU5anz39ZzeHO9dPMCvxaZp4j08z0iQlSz1Kz/XhxHdxRFR7ESvnPIqPUTPV6LwX63aDKLEXEy6ChVDkc4Z15G8Pyw4qJtZvvOm/v1ixTqNaTpRSySXVFF/NROslpJwY1RKWeBZjnIAw1BtP9gGKclXevfmzTxaTPzlzUfzawBnOhwSXn9dPDpZj9i1DDmp2oR0Iza5qJzhrFfhuR5NOQdENTpPKwNiBx5w6MXOXfY9YtiZcef097e7IhE2XLMlSvDjn8IngncfMgllvQ7JzIoqkGsRUC4kWpCiCodadPuw2vXi/cmXBTslRIyvvvfauX/37YNGdnFbNVDJHUi67WMmIlVSpJs5XMqRWwouzuzB5J5BhqMbkDGflivzD+JMG788CECB8tfZO8cVOzsd/4pF8pz9CGOjm1DoB1+yStgq1dk6RF851g/puTtE10iLSGNpx95EDjjrFTZu928taOXcvebizfMWabvfhTV3f6UI3qqC9o6WC0ywVyiXYeUYp7rZTWpq/c3WPWsYBm9c9ePU9v7lEx9Y/uEOaUMs8oZSQVTJXZF6tZ22dK2eESkYsJf2NU0I/LwKjhPSLEb8i+YfmD/5ULP7s7G321cphEXuv98XeiL2gf3kxwu8N269VENtdF9sFaadQ6/aSDaXdSMgLfKFoEUhiJCcpjw7OnDu+497P1xk77pqV6tNr5drAoE/THUSchLy7odOcnOg2Rpub1q3M1y26zU1sXD1E6ExPElLv0MzjspRQ8iSlhKKSkpRTJ6WUolZSygmZiNyH8VKmMs2Z3BhDtlyRf83e17r1z8HhKaW/m/jywIy65N+XJP4B9JGljsiPMTSiO3cCRTt32im01A3keeF8oap57nxhWsRA0lEwJQQlMcNCz502ldqk109BEwERJHEEcSQlB6knJN6lmdOYlpzLnMZKQpZlrltJ1ZVLpB63CiPF7PgtfTT3KYv+b0TKb5F/HN/852Lw1BMwXobX9dmZmJrL9K3Agv5fOSr/jmdGVJ2bF3Q76nxRqHZy52LUmCsuj06jqu8qgjmiEtTUmEqF0vumgDlx4h2Jd2qJgHcuJk592ROT1PlSopomzpW9xiwl896tQumAvZlH0gcs1sJd4oTAxnCenP3Udko8bTlU7SvMN/NfFW9XAJ9iKmVJL/vkDzEM0V0Ldb5bqObRuagaikBWmLMQCKAuGAFDowKO3gpASbwDBJcICeI08SSJU8kceZKSpGCl1EnqNWJuBYLD7C1bsmBCC+MTMcqrfIjvlQ+y/OkY99ObhNaQ+K8ch+OdPnG3KXwco7xVY/eqyY3iqRk6xyJZUFeEqC4oFOYExaKh9JzaPSMiGOLECw6HpKKWeMyLI/XqxVMIbq1Fmk7scIP9txphxymfiUGfD3zL/3/84ulMifzMpEH+Bmls8yaMU8y535rnNOnP8rdqeA3I1eKsbSolB4NRdFgMb0Y0wcR64mXSSzggggeCw40rTIizrqlUwF5msNOj+gArPVykhR6t8P27q1x2yHt42vcdP6OZzO1sXBjmKODDqvxeEjfN4ANP0JlRRG43GBdljN5OWDCrmWNYYAizgw2mP0EdXzJ0s1NeivDZZDNXP1U990T0rOXSty8wsyuchXMrTLjgmWhDjDNQ3a1kfEr+iY3PRBuPa/PZaGSKDKR9AZc47x6KulUuwqelbrnIqe5c+SdOFJ4+HffHaJse6WeKBKwyyVtDoQca/Eb7qbSfarHItVbovpUB3vxsggfPMoAAcjahW+KNVlhDlZVPGcDIWjFbF5U3yTNgJP7oeJ7tBqdo9F84wMOpivwjsmWS+6dSMLUvpsJ3Bz7CdpMkPpP0FwMQYPyznByUHXFy4Z9VgdrpzjE+7aN8+2nu2pOmvyiAAJvO44tmboNh5/1pT8qnPTpj+se3nRjx2aK/OIBmyKbP8FMzN6bY257MM+LkMjGtzQy89pmc4z0ZetaNyGNJBGtv4k2gczVy+5MwHHcRdVoROOkvDR78FQAIMO+LtIm8zYndr8bodqcrRkuwG73jXTudTeuP1/zM019chLemtWdzpCkvir2EZI8jBx9xCTfNOYvfbev/vwT9VXDgFM05m2sMgiinP477lNMd6F8TePBXxoHQW+6tOYsfFsrDpnyof/GiJGHmzp/mrc/2SuOP0bN7CvpJkICZ4+2rjF8G5RcmDDrHvjt7Xv3XBh78lYnwFMnZhOg5SRxF6hmhyUlyNs/o2dj/X9LKT7D/yo+y31+6H09E/w/wHJVcjfUH5AAAAABJRU5ErkJggg==\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHiczZ15vF1Fle9/a1Xtvc98780cEjJCAgkgiUwiIghCgqLIQyK2qI0D2g6PlmfbbaONCmo/habB4dF+tBX0KdC2PFGZB4GEgECYIQmZp5vc+Yx7qFrr/XHODSEkiDKuz6c+Z9+z9zlV9T2ratWwal3C6yh64YW8Y8Ex4431M4yhOQAtVMUBTOgRQRGEWvtBlJnRgGJABSthdIWHrIyI1l9y3339F154obxedaDXOsPGr2+anFL2TgXOJKZWEIYP5oslL0z7EtE8Zj4M0O49f5qGofKAF32GRTe1GnWTZOkR5DUi0muC0Nxaete7el/L+rwmALdde+34nOcPgei0ILK/j4rlLmbztyBMfkUyUGwT8f+ZNGojWeLeBchvjOR+Xvngqf2vyPe/iLxqABWg/p/+YqFR+WYQREsLXeUuMH8WQPhq5dmRVMRfUR8eqquTt7Din7rOOXsFAfpqZPaqABy88spjlPhfi8XytSbKfZxAB+3l0ZSZ7hXD6wkWCAggClURGWJSggUAUjivokRIoJpq6gkkyl5miOJYqNq9VO6xer3+UxfHp4P9l8Z+8pPLXum6vqIAhy+/fLYXXFksdd1go9wXQZjyggyJH1HDDyEIQ1iawMZAjAWTsWS55QHHbEREGQwPABAYZhLxzjDBIpOcQBx7BwhUvOtDkiXk/WGqcugeirbJxc1LGvXaqY5x7sTPf37NK1XnVwTgg1deGcwcHvkWOKpXuiqLiPjI3XIZJBv91lub59CORWBVAysmDKyQ8QgsQGSNsakaUhAYIAPqlE+hgHpRVeMB730IFcfOK8RbSVPHTghZCsn8IJI0hk/fA8WYXYuhovfVa8O3eudy69eWLzjsP87NXm7dXzbA6oXfnJNBflwaM+4uJrpgt6/fTlFwC+dyY7w1Fvk8KAwYHChHRsQGAVvrEBgSsGE2DtYATAwFE4MBQAUCgkBU4D3EewtRD3WK1FmkzsE7gstI00zQaoEynyFNNkucLCZg+q6lUpVLRoYGj1KWv53wla+sfjn1f1kA+750wblBYGcXunpOJcUBu3zrsIbBryhfnGaiyEghIoSRahiAcgEjyCkCqxwEFsY4BCHBEAnIIzAEsGEGRDVgYgXUiQAMcfAeEDXwqshSFe8t0syxcyRZTIgz0TSDacYkaapoNb2kySaK07MAVEaLqIRnGkNDv3ferR7/rxdd+ZoC1Asv5L6R+k9yudJTuXz+YgA7O3EOwquQjyoo5POI8oRCBC7m1YcRKIgIUUgcRSqhVUSRARtvrGEPkiRuVBsDw83B3m3Ot2KqDQ8TAJR7utXkcjpm4qSgOK4nn88XK1BlOC8iziBOPWeOkCTkk0SROqU0Jm00QEkKtFqqzVZLG406vHxol6q4pNn6XBw3jh23Zf3ZdN11/lUHuPpzn4t6Mr2h3DX2T8T85eeoYjuKuT9QobwPFfPQYp4oX4Tmc0AxD+RyanIBxIZk8hE8sx3cuLnv4ZtubD774MOz01bzQI4KjwTdXRt57NhhDYO00FXOAKA5UgsozUIZGOjOhkemSdI8NMwXnt7vsIVrDl20uDhu2tRxRtT5OCZOUvFJqhTHhFYMbbSIkpai3oSvN0Ct1lY0mqeAMHFn0UUuqo0MHDkU0Kn7X3FF8qoBXL34c1HXxNYNlZ5xawj0qZ03DN3I5UqMYqFgSiX4UhEo5IkKBUUhDxTy4FweEobF4R19g7f+6D8a29etPz6cPPGWniOOHM5Nn1qKyuUKk+UgCMSwigoLMwQARNr9ocucEUC9TzWu1WqNDRvrw8sf6HHbd7xz0uxZd5z0iY+VK+Mm9HCa1aXZJG01iZJYfbVJHDfV1BvwtQbQbDSlVsvB6+JdQHxvZLDvwDq5d8/86U/jVxygfu5zUV//4I1dXeN7QXTWzvdN+GNTKU5DpSJUKYKLRaBShi8UCaWCUr4Ijuy4NY8/9syNP/rPg6mYf3bSu9+zpTJj3/FhaG0YRWRsiCC0yFkLgAWgzFgSMkYBQL0n74iZJRDxnDiHLM3Ue4eklVCaZVlt3fr+3j/8YYrWG7NOOfeTT86cP+8AybIdaLRI63Uy9ZZKowodqZOv10G1WiLVxiD77CO7VPPqkaG+aSMjAyfvf+ONL0kTXxLAa9//fvN2p7+rlMdsYDbn7oQXhj+kSnkmd5cJlS5QpSwoly2KBUGpRFTIj92+YcP663/4g/2CSZMfnnrG6T6sdHUXopByuYKGoaEwDBGEIaIwJGOMhGHovKfEGPVBEGUAkGVJoEqGCGGaJoH3npPUiXcpxXGKJEmRpC3EcapxbWR463X/bZLebQvf95nPrpm4777TpNUYQD1WrtWdr9dCDI8IqjVItcpUra3WNPvMzjqpfH9kZOCACQGd/FL6xD8LUAHaccq7flEuj9/ARP+480YuvIzK3fO4uyzo6iLT1QXpKkMrZZhSyWTQiddd/r1n62lMsz/+sb6ouzKmmC+gWCxImMtTFAQmn89REEQuzIXKABtjhIgUAAPIOgkAAnQMlarCe8+i6tMkYeecbSapxs2WZGmszWaCRquOWt/g0Iaf/WxiJcxl7//8Z+ayoldrdaFaXTEyDD9cI1NtiBseUNtobHDN+FPP1Vkuqg4Pzph40w1nv2yAW4874dxKqZyzJnfZ6Hucz31fyuX9TM9YoKdC6KkIlbuYuyuiheLEbZu3rL3h5z87aNJp77lp7PyDpuZLZZTyEQqFvBaLRQRRzufzeVimoANsAMB2AFsAbAbQByDZDWAEYAKAKQD2ATAZwBhVhXMuSZ3nZrNhm/U6teIUraSl1ZGGDj3xWO/263+76D0f/chjEydPmaWN+nYdqTKGa0B1WGRomDA8QjpYfRZZ/HejdXRZfF6tWfeT77rte381wIHD3zpfQ/s3xfKYL0GVAQA2+iF3lWfpuDFK3V2Erh6Ysd2qlRJToTD3jzffcseza9dMm/s/P7clX6wUKpU8R/mCVkpFLRSKEgTWWmstgEEATwNYC6Cxh3IJgKM6fy9HWyu1c4861wUAswDMA9DtvXdpmvp6vW6arVQazRparRZqQ0Mjq//9+/vvt/9+a4898Z3v0Li1CsNV9cNVz4ODVoeqIsNDHsNDG5G5tiaqukZ96JLM61WT77/nqb8Y4JPz54djw8LN3eVxUxS6PwCA7a+op9JF3V3MEyYQuiqCMV2M7m4gCg/6f7/573uauUJj9t+eXSiXyzaXK3JXuaiFQh6FQsExcxnAJgDPoK1xhHbTpA6g7g6UgwAcDmB2pzhrANwP4KkO7CoA34HoOq9jOp/b13ufJEmi9XpDq9Um4rhOtVrVr/np1Y1CKy68532nH0NZ+gQGhtQPjxCGhlX7Bw2GB70OD4/A65JOvqtGagPbJrK8kx56aI/TPrM3gF8ZO/HfugpdG9W5U+EcyPmnqFioarEYULkCKhWEymVGpUxgc9A11123ws7cb+P+HzprXHdPt+0qV1y5VOCuroqGYRgZYzIADwJ4Fu2mWQHQIyIREUUAciJCRJSo6qeI6NsA7gZwO4ClqvppIrqpU7xIRPJElAfQg/YSWQJgG4AhZh5rrc1HUZgGATMRGRuGKM87yA5v2Tz02N33NOfPPeBQUfSyS1nTDEidauqIUie+Uffk3AQ4NzYy9prBZv307/bt+N1LBrh1zpwDQoTTAzIXtKdO4jSMbuJysULlippKCShXiIolImsPuf6m3y+LDpy7ZeYZp0+qlMvc091FpVJJy+UyBUFQYeanADwCIBWRkIjGiMg+qjqWmaep6nxVPY2IjlLVJ4joMACnAzihkxYR0TCAR1T1M6r6FiIqEVFFRHKqWlFVUlUhohjAWiLKjDEzoyhyxhhlMrCWbGn//UrVoaG1Ty9dGs+fvf+bSWgL0tSqy0BxBmSppSx7FnE8H84zvBzjnLvhf47r2XZpf//AnwWoAFXHTfivclSeR0RTAYA4uJIqpamolJkrJUi5ApTLxLlw9u0P3HdPOnnS0KwzzpjQVSlTsVhAuVxGoVAoMHMOwOMiMkBEkYgAABMRqSqJSAqgn5mfVtVFALYC+Bu0jchVAG4AcAeAJ1R1tqq+T1WfJaJ9mPknABqq6gDEABLvPYjIEFFJRFJVbTDz5CAIDDO1mNmoshRnzOgeWr9+oPfZZ9fOnDr1MFHpJ+cUzqv6lJH5IpL0VqgcAQChCcstFy+6ZKD/Z1/7cwA/PWfeCcWwMMhsP94mqiu4XAJVykzlElG5i6irCCrku9f2bnnk2aGB8rxzz43KlQrKpSKVyxUtFAqWiLyIPKWqCYBJqlpBu5/qEZGJzDybiN6jqkd77+8hoveq6nYi+g4RBar6QQDvBPAOAAuY+W4APyKiBao6A8AtqvpFAAtFBERUZOYKgPGqWlTVHiIa6qRyEAR5a61T9T4IIi4fOCdcd+/SfJGwprvctQ9EEnZi1XmFc6Qu60Ka1gGaDMI+AfFlQ2PG5i7p37F+V168u/YJ9KvWRl/a+UCY+xNyUai5HFMxUuSNIoyQthK6f+XKhQed9/fVUqlIpWKBS6WSz+dzQfurcC+AQe99WVXzqtqtqhNVNSKizap6u4iUVLVFRBc5574IYI6IXOK9f5+qiog8IyLPqKp4798nIpeKyH6qer6IfNN73xCRCjPfCmCt954BdAHoIiIjIkUR6RORZUSURVFki8VykM9HWiyW+KC//3zz/pXPLEySRCm0RiIryAVKuZyafJ45Ch4Y5WBN9EURf7HuZnifp4F/N3f+WwthfoTJvL/9Dt2ihahiKmVoscDIFxWFAlEQzvvtI38anvPpTy/vmjC+p1AqoburolEUBUQ01Tl3D9rW1DNzQ1VrzJyo6j4AFgFYSERLVfXdAC4HsICI3gvgSWPM94joZhFZq6pxpznfraq/Nsb8UUT2AfBhZh4G8FMAxxPRLar6BQBv8t73qeomADuYuQmARKSMdvew0FrTMIYVRAAQFvaf89T9N/x26gGTpkwn7/uQesCnUPHkY99NLlkNYH8AFct6SX+lB5cO9m/eCXZXgF79N4wJp47uvpgo2CxhYYoEgSIMlYKANLB2Ze+W28v7z0m7pk6elM/nUCzkuT20w1Tv/UYAbxWRhqqOrnhscs6tIqJbARyvqomq/j0R3QbgAiK6Q1VvJqKzvPcXqT5//2f0b+89ADxijPmi9/5dAL5MRLc4584H4IkoJKKbiWiMqh4gIlNUNWDmfhExxpgtzDwlDMMtBRHyzvvuqVPGl/af9ejqbVvs3DFjp0kQOAojFROQKQQqLtymSdrmwflPESenAjjxBQDXzZ8/iV1wO4CLAEAJd4kNpyCygAmI2KgGBkjdlCd3bJ+54NOfWFbI5xFFEedzOW+M6QFwFxFtUdXAew9jTMV7fygzv5uZ6977bxNRwXt/gTHmUlV1InIBM38VwDs6oG4RkbuJqLkbxAIRHUdEJzrnDgUQM/OXReQTAKZ0NPBSImIAnwJQAnAnET3mnBs0xkBVM2aeYK09wlo7kM/nDaA44MNnRw9dePFb9ytVMmbaAGtgQqPehIAJ9oFmfwTp20E4gNn+cu3MgybOWvfE9ucBDBJ5X7EY7dynZWOfhjEzjbWkAUOJiRRmxY5t901bfHItly9MyefzUiwW1Vo7SVV3OOcWoz2wtcaYDQBWiMidRHSqqm4jog+LyBZm/i4RnSci3yaii1T1Hmb+tff+QACnEdFJ2E2ICKq6XVWvVNWVzPw/vPff7IA8T1UvFZFNxpgPi8gIgAnOuduDIOgGcKyq7uu9Z+993Vq7PYqifYhou4jXLPOFKSce9/tH7vtTeUHPuGlK7MFWOTAiQQAT8FPe+bcDQCksjvVu5HQAPwQ6RkQB8mtXTwf47e3GQn1gM1WtgbckwqywRN6l4zYn8dsnvfWt46PQahAEFIYhq2rivV8G4Ceq+v9UdbWInOyc+4S1FqoKVf2Bqh6qqr8RkW3e+8tU9XYi+rKqHui9vwzAuWhb6UcA/MY5d7lz7nIAv0F7HNkD4FwiulRV91fVL3vv7/TeXyYiW4jotyJyqKr+H1UFM5Nz7hNEtNg5t0lE7iKiX6vqnUQUW2tNEIQU5iLd9x3vGLclTY7zWdoDqLbrzGBr4GH3VcWAAgDTcbJ29b6jP6xBW99nGJOfEYy0ijSmO1TCLRREPchFZIKANQyFQqPPpunq8C1HPjl+3oHlKMpRsVgQZj5AVR/z3p/V0egBVV0JYDEzbxORfQGMVdXDAPyaiD6JtrH4oYh8TFVPQnscdzkz/5f3fnVnnDiLiBYR0WGqKqq6XkRuNcb8ynv/eGew/W4imi4iXyGiyQBOZ+afiMhH2nqBsdQ2FhONMVeKSAzgMBF5LxGtYOZDiagPquS8mrReWxFv3jw4hrlIaQpJM1LvGN4ZdemDrDRG1qx9VLys+TvJNl8OjFgASGBO6Db58ar+fbxu3dUye78WDBMRVNgAAguHYJ1Lj15w8onLC4UchWGo1tqIiLap6urO2OwMVd3CzFd0xmY/VtVvA/gCgEtFZDERfUlVv+WcewuAW4wxfxCRt6vqlzpGAp0BN9BeUACAQzoJncEyVPW/jTF3O+feTUTfVFUB8CXn3BeIaKL3/nxmvkRV/5GIvq2qARF9QlWnA1gqImuMMbOCIAicc1k+r5h16ruKDz348JGzDe0gIAUzQExEVmBMitVr7obXsyObW5e6+O2Av5oAYC3s78YVxhypinEA1blS/i3PmlnWUtFSPgcuFCjOBc3l3RXz5i9/MQnDkHO5HIVheLCI3P6Nb3xDly9ffg4zR7v3XW8kEZHklFNOuf9jH/vY74IgKFhrJwE40Tn3aJIk2opjevDib+WPGm6lhTTNS6OpiJvQetPrug01P1x9L0ELRNgx0By4byb8aVYBXg+qimIcACj0FsTJOOzojbk40wMSiM90HezwtFNOrhtjJodhKNZaUtVCkiSz7r///hOZmc4888w/rl69etWcOXPmPP300ysPPPDAuc1ms56maQag55ZbbjnOWrvitNNOG2Fuj+H7+vr6+/v7ByZOnDh+zJgxY6655pr5ItKzZMmSezvN73lCRHj00Ucbq1ateteYMWP+dMIJJzQ6cPSZZ55Zvd9++80IwzAcHBwcuPnmm0+w1naXy+XbFy1aZP7whz/ozTfffHylUnnmrLPOeluWZZuCIMgZY5SIyDBj2knvfHbDdb8pz3U+r+otvCayfYeXZnOcQm8BcJoqJgCcKDzbTcCknA0fBnAWAKjyDiWUtZmEfv2mxM6a2VAbdG8L7SGHHzTv0SAIgPZofJyqPtJsNt9sjDFZlsVnnnnmMara6PR3C4noJ6p6HoCHmfmmW2+99bh3v/vdI2ecccYQgGUAzgZw3C7jvjU33HDDxlqtFpx55pk3ABhdUg+89wUAMwGcsHjx4uI555yDM844Izn++OMLAK7z3h9rjPlkB/JFInLm0qVL++I47r7iiisOyOVy5dNPP33pOeecs/K66657z5IlS54mopNV9XcAxllr+7xX2ufNC4sPXH/DvLnS6JPEx1i/UaXZKrR/S9422qtYGz603mUT2AP7Rhxyu89VcOADYnWqHuLSfLpxY5dWa9vZmO6wUCiTseC2+swTkY2jlSciiMjFqvouEbkEQE5VP6mq56vqId77LwPAtm3btqnqqar6HRGZB+BqEfmCiFykqqvCMDRERM65S5xzl3XSd1T1a6p6uqo+2Gw2vwcAvb29G0XkMFX9DjOfDOA7AH6mqhcQ0aw0TT0AhGHYA+A7RLT4Ax/4wB1ENHnDhg23qSpEZDOAedZaMobI5HIltcF4DNd28IZ1OfGuCCiUycP4YJRTyCEE2Jc9zFwY3rf9NqDe9DjhQAUW3oNcipH+HcmEgw/5PhEnDIWIKIC6qtZ362POArCUiP5FRL6lqjVVvURVfy4iPwKAKVOmTAbwsHPuCwB+5Jx7P4BLAVwgIouZmbQtN4jIr0TkV97721T1MREpiMj7SqXS5wGgp6dnkohc2fkBnhKRL3Ys8JUicl0QBKYzhLpMVb8BYOlJJ530NhHxt9xyyzGde0NEVG3rAKVgbk6cP/97IwPb1WWO4T2p91aFAxUaM8rJGp4CmDkWoDcTuON+RqmSlgyJI/Uq3nhKM9SisKX5cJYxrNbamjFmnKpuc869s6+v72cA/rGjhXMAbPbe30ZE/wLgHhG5jog+O6qp69atWy8i7ySiSzuWelhVrxeRx4wxSaPR+EC7K9GZqlrsXO9Q1Q1EdIeIbKnVagDwHVUlAJ9WVRDRJufcBcaYA7335xIRvPerOv3ol9FeFgPaq9a9jz766CTv/XcALGLm7QBCIhokIOJibkY1CJs5X/fshYyqcyQGRBEpUkBDEM0jaGYB7KekC6EEgj7JAOC9aHvnQSAW/cVSuO/cuYn3PmLmnDEm7UzDFlQqlb4OvGDZsmXfO+yww86x1m7z3v8LM39NVd/mvf9+54f77OzZs2eoatF7/x1rba+IHAngFAAf8N6jWCw+2mq1SER29SmcucvQBsVisQ4AW7du3UxE56vqJBE5tzOrEefcZStXruQ0TT/IzGi1Wl/P5/N/Q0SzmfkrY8eOPaO3t3ccgGNEZIGq3sXMKRH1QFXGzp2bbSyWypPTXqgXdd6BvRdVZEr6FBSHMnCYgBMLIA+lCgAo6RZRhFAFeQ8IQSwQR2FPYZ9JW4MgSK21LCJzRaRJRB81xnwNAIwx5oorrvhspynPEpF/BlC11jaiKDoZnd73+uuvn7Fp06bHrLVHhGGo3d3dEJGHsizbGsfxjoGBgTMBFK6//vqrjTFBtVottVqtuNFooL+/X7Msc0NDQ1MBvO3WW289aenSpUfEcVzy3ouIxEQUGmPOA4BRS++c+6SIVIwx53nvL91vv/3uXb58+SwAE4jooyJyjqrOtNauIiJXnLoPJ8Woy2cZwYuyqIoqoEgFWMvAoarUDaBgAfCo96sqWiA15L1CmMApICDJh/l8qcTGGFFVj/aUaqRYLP7L2rVrf9xoNL5eLBZ3aggzEzOHaO9VVJLkuU1+7/2MBx54YAb2Isa0V9h+85vf/Pk9WaKJrVZr4iisUWC7y9q1a389b968aQAuA/CMtfapOI4XeO8vLpfLX4vjeA3aa4gCQE0QqURRDmlKgEK8AAqCihK0iueGV8yA2tGOkYAUIIEoqfckzpPLHBAEFY4iEhFnjPGqOhbt3bGZ++6778l9fX3YsmUL6vX6zqWnN5JYa49g5lNV9d9E5FvHHnss+vr6cpVK5X0AphNRA8BY770AEJsLSYytqMtUUwd4bbMDRBWNUV6AWn6e87WSV9KmGtRhUGdwPYCpE1srzmVE1ErTNFHVLd77AQAwxswEgCzL0N/fj82bN2NwcBBZ9rKdP18xWbVq1ZPe+49nWXam9/4/DzzwwGcBpNu2bTMA0FmE3UpELWNM3RiTsA1CgOpgqpNBnRk1z9QU2J28aFT7dr5BmldFyQIqIPKq5OFh4evqfaiq+c48dC6AjXsqrPce1WoV1WoVYRiiVCqhUCigs+D6mkmWZWg2m2g0Grjqqqui7du3Hzt58uTbJk6cuHb27NnvJ6Kt995779NLlix5E4CJqjqHmR8CYLMkSQP4qgrKBJAwhKBgJVZyFjs9jwEreE4FhajIopRAGSAGkRG1CJwfcnEqlMtZIhIiqhNRcc9Ff07SNMXg4CAGBwcRBAHy7QVYRFH0igN1ziFJEsRxjDiOn9cCduzYcdbVV1/9gs/84Ac/WLFkyRKoahlA3XtPzjmbtVrMiWs66BgQiIWcQpVZQwJ17eQFwBLQC9UNIEyHoqJGYwhIIO21QvbepGktHhpyQXeFOyvNDeCFHvi7yBAzb+zkwSIyLcuynl0rZa1FEASw1iIMQ1hrwcyw1oKInmcQRGR0TREiAuccsiyDcw7OOaRpOrrcvyfZxMyD1PbBgfd+Ptq+NhgYGDgJAIhoHwB1dFQrHhlxuSyteUg3g4xv93VKYFJomaCA0hoCtliBPi4EQ8B0IZ1LgscZUKfwgIoQo7S9v9nYtDGMpkwxYWhtZ2B7SCfT5wkRPU5ErqenZ924ceNkx44dPDIyAgCbReTg0edGK/8qihhj/qSqZuLEiWvL5TJv3ry5GMexV9UxqjoTQNrb27u8VCq9mYgeFxEbxzE11m8Mcn39Dc8QFaiFAkSkKoZID1YlgOQhQB9lBT2tKqsAgBTjhaTmSb0C6kDGiQa8dWuhsWZ9TtVb55wBMKiqxjl3LwCUSqXfjsJj5vqiRYsmBEFwOjOfEUXR6SeeeOJYImow85OvJrFdxRhzfxRFQ0cfffR8a+2ZQRCc0dPTs3jx4sVpZ+q2paen596VK1c+pqoBgMHMeyYiaqxdVwy2bCtnQoEA5Nu+TJ6gI6o0BgC86rMCeoYZfnXm0+0Ytc3CDXiygASqYK9MNFwrNNavnZ2mKURERaQOAEmSbN1t2GIXL148pq+vb8p3v/tdiAi+/e1vo6+vb+qJJ57YjfbK81/syP1XyAZVjRYsWHDc2LFj81//+tcxffp0nHfeeXjyySffsmDBgg3MvKNer49LkiTqdA11FYFzHsn6TbN4qJYnBYmCFQjEU6BKzVFO4tMdgF9rCVib+Oy00OQAAMTUVFEVpUyhUKhFko5F4g5Mk+QOIgqIKGVmOOemX3vttXfW6/XjrLX3EpE89dRTxy5cuBDLly/HvHnz8MADD2D69Ol45JFHDmDmewE8rKrjXk16RDQwadKktRs2bFgYRRFWrVqFSqWCe+65B4cffjhardaihx9+eG2WZVOMMRYARCTN0hQuzRLN0nk+i58ASAOwCOANgcHaVGlb4KbPxjtgjd0fqK4Uf/ROWyxqASYlsVBWBbwCCPv778x6dwzQPvtMMsZoEASbVfVtRx111E3PPPPMzQBwyCGHFK666iocwXXn3QAAEZlJREFUe+yxePbZZ1GtVjF16lQcc8wxWLp0KRYtWrT60Ucf3Wl+X6lB954WXk866aSJy5Ytwwc/+EHcfPPNGBkZwTve8Q40Gg0sXbo0nD9//vJWq8UzZsw4RUQ2dgyVtjZtGgj6+m9X8EQAQlBWKBFYSBBqh1Mq/m3zgQvabrNAnwIPEHAEgBMIspJUIKTkARaFMQ89OlJbvjw/5vTT1DlHxpgnACyaNm1a38UXX/wxEYGI4IEHHsD111+PJUuWgJlxyCGH4Oqrr8acOXPwmc985m9Hnxu1qLta11Ggu7+OAtr1lZlBRM+7Hp3OjVrwG2+8Eddccw3OO+883HXXXRgYGMD999+PBQsW4Oyzz/5okiT/KSLjVPVGL6KNRoOaD64o24cfq3nIFEOkAlIDdYAnAb+jw+sBAtYBnV25v4OphWyHmPidALqgtEIIxbbnIkOg5KrDJT3ggLebgw9ew4aNtTYlonne+2IQBN2qyiKCOXPm4Je//CVEBKtWrUJvby9WrFiB888/H8Vi8Xnw9gZy1793T3vT3t21kIgwefJk3HzzzajX69iwYQN27NiB7du344tf/CKstS5JkhKAsSKyIm61qNVspcnd95yst92RWMAYYjEABYCxsIMKfQsAOPGXpz77w/cg6xkAArj7W65VHs1cGNsZpBYMQKAgiFIevX23tTas64vjmFqtFlT1QQBzkyT5xWgFp0yZgiuuuAJRFIGIEEURLrnkEkyePPkFkF4see+fl17KZ3aHfdRRR+GrX/0qms0m0jTF3LlzcfnllyMMQ2RZ9gtVnauqf0rTVFutBK11m/pNb98tqhIqoKTC3LbAqup3jPJpuVaXh/sTsIun0dMwf+zJd+0PxWRVPA7CjhSglgIJhDxAaSm/OffZT0+17z+9t1AscqlYZCI6Q1WfiaJoWmfF+AUV/nMAdtfCF2vCL9Zsd0/GmL39PRTH8QARzfYi1zXqdbRaseh//fe41uXf326ayZQA0BwMAlLJA8SKsUp4EwhbBlsjz86DPw7Yxb1NgEuc91e0C4qDFboDECK0zY4KyNdbU2iknqT9/X1JHEur1VIRuU9VD0iS5Jo9NbndwewN3p408M/9AHv7vr3lPZriOL5BVfcDsCxNErRaLcRbt/XpSD1zzXiSgtgAyvAwbSPSq4Q3AYB6fzmA/z3KbSfAEP7melqfNrppQqKtAIAhUAiAWD1BbO2XvxyJ7l52QL1eR6vVEieySVWr3vszvff37K3v2luF9tZ0X+z6xZrti/Wfqoosy2713p9FRMNpmm6u1eucpqkU7n/wwPov/m+DAGsgQgAZAhkoWDQZ5TKY1ueuh7/9BQD3BxLfNtHXdHxAPgyiEduZzzJAEFI3PDxO+3aMYP3GvlqtybWRKrz3d6hqMcuyblUdGm1+e6vYi/V7f8n13mDuCWKnTL1pmk4CEKRpeme90aBGKxa/ZsN237d9OKnWxrQdKEm4c9qbCcPKdLa2rcEvAG2c0nZofz7AdjOWC0fSxuiZCMuCHQYg204IIEoA9f7kqlLlqZXvTONGrd5ool6vp9r2OD04juO7te3L8qKa8FIMyksxIC81H1WNW63WU0R0sIjcFsdxVq/VEdfr9a6nn17c95OfFRhCFkohFBZqLKAQ7kfHi62aNtcmkG/syux5AA8GtntxBwB0BwAo0YcD5WFDpAYEA4YBQzPXvf26/75vzP0PlhuNmtZqDWo2m4MAHgLw3iRJfrWrEdj19aU06d0t8K4gX8p37CnvJEl+h7YP4oOtOB6oVqtaq9do3EMrituvuW4ZMt8TgikAwxBgicDKwyD9CAAocIsTN3th22N2zwABIIb8r6G0vqzd4jUnLD4E1HS2OgJAQ6KkuXrlfn79Rp9fs663Xq9ptVbzjUZjjao+AuCDzrlfAHhFNPFlal6aJMkvVfUMAA83m821tWrVjNTqlHt27WbevJmba9bMMgRvALWAsBIFgIA1BRAqgKG08TBBzt+d1wsALgS2irhAIT8CFFCcYYFVAUHzgIYAWRKyUF37o590j9u89ah0247eWrXKw8MjaLZaq1V1uYj8TZZlN6jqyN60cW9a+WLjwJeibaOvALamaXoPgLMUWN5KkjXDw8M0ODSk0rejf0LvjiPX/OA/ihaAhUoHIIekxJCVpLqkzUB+KOLsfOAFUZH2eNDmQ9ClcG5J3uZmKBBCKWcNDQsQCEQ8wSiIRMT0Llvef9CCQxZsjIKnHbikIgKgGQTBJlU9xTm3GsBqANN2b3Z7et2TMdh1/Lf7GHBPY0IigjHmHu89EdERAG6O47hvcGjI1OtNifsHBg5cv/HYJy765gbrfaVAxCGBilDNEWyOTaxCbwJhHIGqQ0ltex/kcz9re9/+eYA/BtynYaqG6HHDZhEIE6D6EClXAHgHYgAsBPLqo833P7Bm4cELjt1A9GhGWnaqEOdbzPS0MWauiExX1Z9re+Qf7KkP2xO43QHuCmhP0Dpz4CERuVZVTyYicc7dPlStJrWRmtZrVY37h/oXbu094bFvfXuFSZOxEZGNFDYCfA6MHDGp6nYiHA8Aqc++lIn//RGQPUb32OtZuR9C1nxc/GcjGz5JoIMBmk+svyPQBAACUqiSVVJIlhW3LLt/5eGHLzx8o8NTKUkh88555wNAN1pr+1T1VBFZx8w3icjB2j6a9bwmt2uzHJU9QdqLBgoz/1xVJxDRUQD+2Izj1fVaLalXq+FwraZZ//DWI/r7j3/4m998gprNSTkgyIE4YmgBLDkiWKUniXAOAAjwi2ra2Ocg+Ev3xmmvAAHgH6A31lz6oZzJEYCxUD6YCHczoUeU2DMMQCBSylzWtfHOu2tHHnXk9DjLHu/1bqzKzj4sZrarARUROUnbHq2/Q9tFrmtXiKPXe1p5GZ2K7QZvC4D/ApADcDSAJ0RkRa1Wy2rVBtXqVQwODMvkoZGtbxoZPuyBr3ytj5JkfJ5gQmWTJ0IR7AsEWKb1qvhIh8uq4WSkVYJ+6N/30HRfEsB/B9zHgWVO3fjIBAsIGhHRDIY+xNAygRVQgjBDlcW7YN0dd2bzDj7YTjXWPJVltVbqOUvTXOYz8c63VGU1M9cBHKKqU1T1NgB3S9uzfho68/Pdm+0uyRHR3UR0B4DtRHQgM5dUdV2apk83m83myMgI1RpNHR4aMLVavX5stV6sbNpaWP71i4qBl2IEaF6ZCyxSAEsemgXEW1RwGkGLANxI1riaVL4yp30YfK/ykmImPApzap7t3EKQ+067ctigwP2xYlwq4AYLN6AcgzQW9S0oeubM2fTmv/v0kY8Q3bUxF3bnCpHJBYFEUZ7CMEQYtnfjmDnsaOE07/047/16Vd3S8RZIvPdgZgugQkSTmHm6MWaYiDZ2oKejO3Rx6rXValAaJ9qMWzohTre9FXTy4z//5dLtDz64fx7gHLdHE3kYXxDhHINyxFsVeCsU+wKQetb6p1T844fA3/jn2LzkqB2PAOeUOSqHQefoP2EThP6YkUxvgnwLoi2QT1RLTZUkUTXehukxXzp/MJw8acyt3j1RZTOhkM/bMBeQNYHmcyEAAxMwhUEgaLurdcJmAUTtU/LS9kfU9jUABrIkJS8eBGiSJJRlmaZpqtV6S3oEAydZO6+1bVvfsv99yVjj0jBH5AsgHzKFecAVwFIAyCi2EeMoKGYBQJrF5zUlSQ4G/s9L4fIXxY15BPhqV5AfsRx2INIQAdd7yP4tQJseQQvQJpRarC4VlRQQU+5qHP2Fz6e5CRPG3dGsr9geBOMtIQzDnLBlCkyAIGBSJTWGoKoZGePEOQ8AbK1R7y0RWSfKDEWaeiiJpnEK5zJkzmfjVftOyOXfnG7v237vv12Wk1qtEAKImGxOyOZBvgClvAHyIDWglar0PwjtfjiV9Lx61iq8CfjWS2XyF0cuegz4+zyHYRTkvwmAFUiJ8WNRPTSGaqzwTYEkDI2hpuVhHSFLoGq7K4NHfvQjtfEHHHj8mlZ828NJq9HnsnHGhNYGhgwD1rYNhaq6dgwZoDM5sCKs4jLy4uEyp+KzrDsIB95aKJSmBdEJ2598+s4HfvaziqtWe3IgChQ2b8hHUJ8TQp5h8gREIEtKjwP4KLU9yKSZxV9IJPmL4P1VAAHgUeAjAduppaD0v9CJd2oIvxKlcgJfjAHTFLIxqcaKLGXlTGBa0CxTsWK40TNjZt9hH1hiumfMeEcs+sS6VmP1hjh221KniXrK4I2gHXiH4cnA+DwZnRQEPCOfs7ML+f0j4gMH1qy/88Hrfikj69aPZ9FijthZUBAxfCQkOYYtClHI6nNgH4AalrThFWd2CAwOp41/F3FrDwV+/pey+Kujtz0EHMXgT48NS3NBGI0XuAqge5TkgFhJEqiPgSCBaiJwKSRogbxTNY7IZ6rGkWYmFw10T506MnXBm3X89Olhvru7GJXLlSCKxgNAliR9rWp1JBkZafZt2JBufPghHtmyuUviZEygFAREYlXZErkIGoTgNMewEYhCIMsBlCMERvkpJX07FPsDACnuG0rrazzk8gXAn/4aDi8r/N2TwJgU+HlPWHqQiL/y3B39NROJE0yJiVwM0VQ0TIE0ZVgH+EzEZKAsgwYegIdmClivnCl5B2KFSHv8xWyhQlBjLElIgCOQDUFqAR9AA8vsQ4ADgc8BgWVKc2DOqQbM2KSqAYHeN1pCUflaNa0fTsCHDgGG/loGLzsA47WA2Q/4Z8sWXUHhQ9o+nAwFUkB/ysAYrzQ1IU0cqYmVxAGcifoU4IwhHmIgTI4FIuQEUAWI2lNGKFQIHZcxVmuElVgQgL0RmBDwAZOxgIQEEylcpBQxY6MRJI4weo4PAJ5pZM1rMnHuTcA36bnjZH+VvGIxVB8HZjvghyVbvCkw9hsKLXRuCSn+rzIEqtMzgnEKTQnkPJwzCJwCHuqkbTUcAeIERNSeAajCWoZqe2XcctuqBJahgUdmGUEAeEswgcIR0XoVWGqD43ZFqZn49CtN11rkgE8d3g7487LlFQ1CqwCtgDmVIH9bDIsPWeJ/1vYUa/T+kwq9j5WKRDQ5UwmFkHmAPRSOQOpJhSHtlcT2fI6gCiYigWGjsAplkBoAAWAMOBPVbSBtEOhotCMZjVYwdioXN9LGmwX844Xwv38lQyK/KmGQHwQCMmYJRD6UM7nbQms+D6V9n/cQ6VYAdwLUIiBSRQVAt5IaArwHgbQ9jFESNu1ItKbtLIFhIlQ73UQOwAlQmrRbzTaIc5c3fHyiMv9Cvb/2sOdicb1i8qpGMleAH7T2BPL6T9aYu3Im7CHQeXsvDQ1CZQVAI6QY0fZ0DqRaVEIXoF0gXgDVMXv7CoVeFvt0KPP+WGPoWwucu/Pl9nMvJq9ZLP0HgXFg89U8h+uZ7SWvRh4i7vyGZNNZ3DcOA171MPDAawhwNL8/sfllzuQ2M/EL9hdejojq5bFrTT1M/RmvVtj3Pclr/t8c7gRswdjfFzjHoOfCh7wcIcU9dYlj6927Xo1+7sVkz0d7XkU5HnDq3ftjadWVdNPOU6J/bSLd1pBWb+bdktcaHvA6AASAo4Bq5s1Xmz79tQKpoN3L/6XJAy7x2VXkzdfe9jJmEy9HXheAAHA00sdJ6QFR/Ye/FmCq+g9edeURSF8z5/Xd5TXvA3eXZRxcam00zMDukeX+nHwj88nYt/jnIvC+HvK6A1SA7jPhb4wJqtSOofBS5NpMsuKtLn3Pha/iGO+lyOsOEACWAXm1we+Ywm4iLPwzjz/mJNuSufT049vHJl5Xed36wF3laKClLjhbJHtEQdW9Gw2qZ97f5505940AD3iDaOCo3GNzx4HxVlZz0Z7ui8o/KrLlxzr3x9e6bHuTN4QGjsrbXHwXRJywnP8C7WP5AkHkjQQPeINpYEfoHpP7hRrapEr/0H5LL2fR8W/z8d/gNZymvRR5IwLEnYBlW/i9gJiACNBYXfOU41/ExeL1kjdUEx6V4wEnLlxCkCogfeqaZ74R4b3h5d6wNP/esDT/9S7Hi8n/B3LrBEUxxEM2AAAAAElFTkSuQmCC\"],\"useMarkerImageFunction\":true,\"colorFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n if (percent < 0.5) {\\n percent *=2*100; \\n return tinycolor.mix('green', 'yellow', amount = percent).toHexString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', amount = percent).toHexString();\\n }\\n}\",\"markerImageFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nvar res = {\\n url: images[0],\\n size: 55\\n};\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n var index = Math.min(2, Math.floor(3 * percent));\\n res.url = images[index];\\n}\\nreturn res;\",\"strokeWeight\":4,\"strokeOpacity\":0.65,\"color\":\"#1976d3\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"showTooltip\":true,\"autocloseTooltip\":true,\"labelFunction\":\"var vehicleType = dsData[dsIndex]['vehicleType'];\\r\\nif (typeof vehicleType !== undefined) {\\r\\n if (vehicleType == \\\"bus\\\") {\\r\\n return 'Bus: ${entityName}';\\r\\n } else if (vehicleType == \\\"car\\\") {\\r\\n return 'Car: ${entityName}';\\r\\n }\\r\\n}\",\"tooltipFunction\":\"var vehicleType = dsData[dsIndex]['vehicleType'];\\r\\nif (typeof vehicleType !== undefined) {\\r\\n if (vehicleType == \\\"bus\\\") {\\r\\n return 'Bus: ${entityName}
Bus route: ${busRoute}
';\\r\\n } else if (vehicleType == \\\"car\\\") {\\r\\n return 'Car: ${entityName}
Current destination: ${destination}
';\\r\\n }\\r\\n}\",\"provider\":\"openstreet-map\",\"defaultCenterPosition\":\"0,0\",\"showTooltipAction\":\"click\"},\"title\":\"Route Map - OpenStreetMap\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" } }, { - "alias": "route_map", - "name": "Route Map", + "alias": "tencent_maps", + "name": "Tencent Maps", "descriptor": { - "type": "timeseries", - "sizeX": 8.5, + "type": "latest", + "sizeX": 9, "sizeY": 6, "resources": [], "templateHtml": "", "templateCss": ".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 200px;\n white-space: nowrap;\n}", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('google-map', true, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('google-map', true);\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('google-map', true);\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('tencent-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('tencent-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('tencent-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First route\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.5851719234007373,\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.9015113051937396,\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.7253460349565717,\"funcBody\":\"var value = prevValue;\\nif (time % 500 < 100) {\\n value = value + Math.random() * 40 - 20;\\n if (value < 45) {\\n \\tvalue = 45;\\n } else if (value > 130) {\\n \\tvalue = 130;\\n }\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH
See advanced settings for details\",\"markerImageSize\":34,\"gmDefaultMapType\":\"roadmap\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"useColorFunction\":true,\"markerImages\":[\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic7b13uB3VdTb+rrX3zJx6i7qQUAEJIQlRBAZc6BgLDDYmIIExLjgJcQk/YkKc4gIGHH+fDSHg2CGOHRuCQ4ltbBODJIroIIoQIJCQdNXLvVe3nT4ze6/1/XHOlYWQAJuWP37refYz58yd3d6zyt5rr1mX8B7S5Xo5/0nPYaNFM1PY0gGqOhfAgQCNBGlWFFUAYEIeihigbhFdZQwt85BV5Gj9r/718R2XX365vFdzoHe7w6d77xnPkn4YpAtU0YiizNJcmPNkMQFkDiSlowHt2HNtGlTSJ6B+pTpsKTfKgTj3Pi8SMtFtEZnFs8d8dPu7OZ93BcCHtt0+OiL+FJjOiqy5K5dtLwD4PBHGvy0dKLYo8B+1+lAldv50FfmFzWX+84i2M3a8Le2/Dr1jAKqCHtl2y1wC/pEMP9ZRLBaYzF8CCN+pPluUkOKfB6qlmk/dBwTyt8eOv2AZCPpOdPaOAPjA1h9/SJX+TyGXuz0TZi4EcPBeOk+U+RErZh2YyMAyQJEoZUjFgtkCAEScgDyx1hmInTglqDj2U1X0WILaPbWvwHO1WummeuLONhaXHTf2wsfe7rm+rQDe133j/i5xPyrmCr+OouhSKPbdQ5fLiezTIYUBQGMJBgYWxMYSISZhbxgQT8wGAgDiwWxUvCiBxKhSKOqdh4OyV5+6XiEfK/kjVOXQ13apG+I0+adKpXaG0/Si0yZdvPbtmvPbAuCNT98YTBhT/8fAmEpHoXgKgPe/6gFGP0nwG8s2YykcaRCAYYQ5tKTkDVuArDEwMRF5AICS4VZ1AQBSr6oEgL36CBAvlKqIsyLOKQl5TZH4uN+TawDuY6o64lWTJX20v1S633uJNvfmvnbRERelb3XubxnAX26+5gDy6Y9HtrU/wERff1XjSt0WwULDmZEMawPOgilgQ4FaGCEygaXQMQyRMaxiUijUkAEAImIGAFURAOrVA1AmI1ZExGuqoqkVFefhyGtKDql4X4eHc6LxJof0VIVM3nVc4uXaHUPlo0Tpc2fv/zer38r83xKAd6y74iImO31EMf9REA7cpdVBY8NbA5+dFNqsCTQipkitBjAUsLUZNd4qm8AyjDMmJAIRhDzDEBEbJkBVAyJWQJ14AEaciIeSGicOgBeBWNHEeXLkXIM8UvFI4bVBCVJNfdk7STd5xOcp0LZzjIqV/eXq/4i61edM/eaN7yqAqpfzf62Nf5LP5lbko/DbCuxU4saEN1mN2kKTzQbIkuEIEWfVagRDEVkOyXCkVq0aDg2p9YYNAySVerU0WN1R27Jjo6ulMQ1V+ggAOgsjNRNEus/IiUFnYUy2kM23AcrivXh2RiTxjhx5iSmVWEWdpmhQ4qvwSBBrXVPfqDmuVsT7C3aZvKslyZcr9dpxdr81F8ynO/w7DuD1q/8y6kDw2872ticN0deG7wvQHXHmdxGK+1ibQag5ikweliIElNUAEayNYBCSRQRiYzf2rNtx11O/rC5d9dj+1aQyM2Pyz3WGozaNisYNWY7SYtgWA0A5KUVO4qAn3t4+lOzYt+Grh+bDwstHzvjA2tPfd1Z+39FTRhGpi7VBKrE4nyBFDKcNJL5OCerqUEXdVeEQb0mk8lECjR0euxe9cqBUOnoQ6RkXT78hfscAvH71X0Z5kf8Z0dH2CgNf2NkI0d0ZbmtElMtFVEAQ5BFIlkKb00AzFJqCGooQcJjv7t868P3/ubayZvua48ZlJt57xLjjB/cpTssXokK7IQNrbeoZ3pIRJm1aYSUW9cwixglZ7xNU40ppY7mr+sy2ezt7G1s+vP+EGfd/+fS/Ko5pH9/pJK04X6MUDSRapcTXkXJN46QKp1UkqNVqvpxVyLzhOajihh1DpVkmrJ7+uak/bbztAF6/+i8j62p3j20vbgXR+cP3LYU/Djg/KcsdEnIWERcRIk+hzWtEOYSch2U76tk1T6+84Tf/NCdni2tOmbRgy6T26WOiKDBhGFEQhrBhiNAyjDGiQp4DFgI8AChg1BGBXOC9p8QJ0kas3jvEcUxxnLgNpTW9izfdOqGWlve7+OOXrThk6qEHKtKehq9xIlWkvoaYytrwFYqlglgrcZxW+oXSz+ycpOLmnsHypDTIfuTNcuKbAvD2288x22dn7hrVnt/ATBftBE/CH2aCtqkZU6CI2hHZomS4YCPK+5AKHFB2ZNe2Nev/739/e9qY3KRnPzHtQp/LtnfkMhnKZDMa2oDCTIjQhghDC2MCCQITAyYxpmkhAIAZDDA7l4bOSeR9YpLEwfkUjXqMOE0QN2LU4waq9aGBX6/+d7O9sXnu3579jbVTx02dlEilL0FDG1pJG64cJX5IGr6MupY5duU1npIv7sTQ4196ytUDx8+sf+TN6MQ3AyBd8+L8W0a15zYw0d8O3ww4vC7ijlkZU5QctVPE7QhNEVlTRNYUjHcy7tu3fuuVSqXBF8z66962fMeIfDaHfD4nmUyWsrk8BdaYIAh9EFoxzExEysYoAQ5A0ioAEIpIBGZmAM459iKaJo6cT209TnyjWkOSNLRWi1GtV9A3sGPg56uvG1vIZ9N/OO9rM8jS9oavSOwqaEhZYh3khq9K3fdpXWsbvdR3MoYCV/UOVadcOvv2C/AG9IYAfue5j1/U0R5mIhNctxM8yvxLyMVpOduJyLRRnto1MkXK23axlB27sXtT1z//8vqDTt3vk/fMGnX4xGyhiEI2Qi6X1Ww2S7lCIQ3DkCxzQEQKYADANgCbW6UHvwcRaO6fAwCjAewLYAKAcao6UkRIBEniEtRqNVOrVKjeSFCP61oaqurKvqe237P2lnkXn/X/PT9l3OT9Eql2V90QN1wZdRqSuhukhi9T3Q2s9ki+NDzHWppeUqnG/qsH/+b7fzSA33ruI7ODIDh/RCH6KkEZAEINfhia4n4ZO0KzphN5005Z06aRaeOAcjP++4Ff3P/86hWTLjr08i3FfEeurS3LUTanhVwe+XxOwjAw1loLoB/ASgBrAdSAV232Gc0NyJGt70+27mlrzNT6nAEwDcBMACO892kcx1KvN6hUqWu9Xka9XsfgUP/Qjcu+Nf3g6bO7zj7urBNT1F+quxLXfUkaMmDrviQ13+8THdqYqvuLZpfq+qrJNXFDbrp87t0v/cEAXr5iduiTMQvHd2QnKDC9+bC9NUfF9kwwgvNmBGW5Q3O2SFkzAkaCg/71Nz9+2MTZ6rlzLs4Vi0WbyWS5o63N5fM5G0VRaoxpA7ChBVw3ANMq1AKoHUAewCwARwHYvzWctQCeaNUrt4pvgeha17Gtevt47+M4jrVSqZlSqepqjQpVyyX/8xU3VBHF2T//+OeOFbgXaq5fa75ENR3SarzDxDToYz846FTORbPRV7oHG9sm+qEPX3TEM3vc9pm9AfiBP53+T6Pbwo0Cd4aog4p/yXK+lDX5IDIFZDinGS7CckEM+JB//u9/e3Z8NGPTgjl/Maq9s8N2FNtcPpc1bW1tFIZhaIxJATwFYA2AtAVWh4hERBQByIgIE1Gsql8gou8AeAjAfQAeVdUvEtE9reFFIpIloiyATgARgCqALQAGmHmUtTYTRWHDhhaGYE0YYmbHEXZj//rBRc/fXTly5qGHEus2FUceCbxP4DShRJ2mvuIFboyqG5kNcNuWVM965MbNd71pAC99+vADA+MnR6F+TeAg6h1TeE/I2bbAFjVLBbJcpIDzZNke8qNf//yxKblZWz42+9Pj2opFbutop7ZCQdva2hAEQZGZXwGwDEBDRCJV7VTVfVV1BDNPUtXZqnomER2tqi8S0REAzgJwUqvMI6JBAM+p6pdU9f1ElGu1E6lqUVVZVYWI6gA2EFFijJmSiUIPsDbXmGT3b59V6Kv0dd334uLGYTPmHK7Q7lRi65DCawqviXWSrEm1PlvgWMh9KPbut+/77Ohtj/97d98bA6igo7aM+O/Ogp0l8BNFPQhyY2RyE0MqcC7Ia2jyGpksBYj2//WDCx9uk/EDZ8783JhiW5HbigXpaG9HNpvNMXMGwAoR6SWiUKS5KhERS0QqIgmAHcz8sqrOA7AdwCcB9AK4CcBvAdwP4EVV3V9VPwGgC8B4Zv4PIqqoqgPQYObEOadExC1A60RUJaLxURQaZqoRW0NEsm/xgI6u7rV9L295vmvGlKmHQ32vk0QdxfA+oYTq+Vgbi70mR4p6BEaKlTid98S/9f4MV7wBgF/66AEnFbPUz+z/VNTBiywLgxxCFDgwGQqR5wznOeR8+6p1657r6uopfu7wv4mKbW0oFvIoFovIZDIBEXkReUlVG6o6Fs2N/EjvfSczj2Hm/YnoY6r6Ae/9w0T0cVXdSkTfE5FsC8iTAZwI4DAAjxDRj0TkUABTACxS1csAzG39MHlmzqvqGCLKt1xZA0Q0QERtQRBkDZMngrcmNAeMmB08uHpxNsrz2pFtbft4TWInDZtSLE5T8i7uSKRS8XDjBX4fYbnusI2jMkt/tGP9rnjxrl+gICP4Riagrzb1ssKa4CkrYRhwwBFHYGSUOZJKo8oPP/vCoV846opSoZCnQj7HxUJRMplMgGblR5h5wHtfbE1oZAvIHBFtVtX7RKTQ4pSrnHOXAThQRK4BcIaqNkTkRRF5UVUTVf1462/TVPVSEfm2974qIm3MvBhAl6pGAEYAaBcR45zLiUiPiDxKRC6bzZpsNhtGUaj5fIG/dNTltYeeWja3ltbVcGgMZX1IWbUUqDUBbBA+OYxDPuDLSORq6KsN76s48MvzZnwwlzNDgaFzAIBAi0LKtGVtEQHlOaQCQpOHoWDWL+9+ZODCuV99cnTbmM5cIY+2JudZIpronHukxUWemavOuZIxpuG9H8fM8wDMJaJHVfV0ANcDOIyIPg5ghTHm+0S0UETWq2oCoA/AI6r6C2PMgyKyD4BPM/MggJ8COIGIFqnqV1T1YADbVXUjEfUaYxrOOcPMBVXdCmCutbZirQGIlIBwavucl2577NaJM6ftO1nJ9aY+YfEpvDryknamSNdAMQ1AGwxdc/DqDjz9k/7Nw5i96ixBSK/MhTRxJ7oUbracmWAoVGNCtRSCYOxLazfcN7VjdjK+beK4KAqpkMtpJpNRABNVdT2AowHUvffjAYgxZpNz7hUiuk9VT1LVWFX/iojuBfA1IrpfVRcS0Xne+6tUX33+M/zdew8AzxljLvPefxTA3xPRIufcpQA8EYUAFhPRSCKaKSL7EFGgqjtU1RDRZmaeGIbh1sh78s7LxM59R09um7585fqNdtqUMZOMMc4igE0DthSppcYWL80VTNbyX1QCPgNN1fJqDvzi0tnjQviObGia3Ee0JEAml+E8DOUo4pxaE4GUJz3yxJr9/vSIv+8uFAu2kM8jl8vBGNNJRE+q6grn3AZV3QRgi6q2AZjHzHNE5FEAp3vvv8HM8wFQSywvADAPwDgAi0TkPwDcBWDhcFHVh9FcXH9ARE4BMI6ZvyEiHwYwSVW/CeB0IlpERJeo6hwiepmIlnrvVzLzemZex8yDzDwZqlUikGGm6R0H66+evuPYafuNynvFkCCF4xjiBd67otN4C4GmEDAqTuVnR3++beWT/z5YfRUHio8/0dEe7DynJTUvswmmEiwxWcCDwGyee37j4ydNO6ucy+YmZMJQM5kMWWvHqmqPc24eADCzENEGAMvTNH2AiM5Q1W1E9GkR2cLM3yOiS0TkO0R0lao+zMy/8N7PBHAmEZ2C3YiIoKrdqnqjqq5i5j/x3n8bTQt8iapeKyKbjDGfFpEhAGOccw8EQdBhjPmQqk723rP3PrTWvhxF0Xgi6vHeayaTyx075fS7nlvxcPGgg8ZNIjHeSKRMdbEUIEHwEuCOA4DOvB25vSRnAfghMGxEFNRb7ZoM0HFNadFeIjvRgMFkhEDKbEl8Oqq7u3bs+/c9cXQUWo2iCGEYsqrG3vvHAPwEwL2qulZETnXO/Zm1FqoKVf2Bqh6qqr8SkW3e++tU9T4i+ntVnem9vw7ARQA6ReQ5AL9yzl3vnLsewK8APIfmovkiIrpWVWeo6t977x/w3l8nIluI6Dcicqiq/quqgpnJOfdnIvJR59wmEVlCRD9S1QeJKLHWmmw2hyAM9bhpp47q7q4d733aSVBlkBoNQGxgYPdVRZ82N5In9lS7dp42GgA483hMyUY0RXgwXzAjQgUtshp1WhOR5YgDzoiB0U2baqsPLB7z0oxxBxWz2Rxls1lh5gNVdbn3/rwWR68moi5VPZWZt4nIvgBGquoRAH5BRH+OprH4oYh8XlVPQXMvfIOI/BJAFxF1qupxRPRBIjpKVSe3dOtdInKbqj5PRIe3RHayiHydiMYDOIuZfyIin0HTfI4kIgAYa4y5UUQaAI4QkY8ZY5YR0aGq0kcE8k5NNS4t665u6G9r47xDCi8pqabsNbFe9WkoRvU0upYl8GunnqebX7kZQ00O9DipLbKjRfQTPWnXYyBTBxMBBiIML2IVkt20sf6B46d9rJjJ5chaQ0EQRAC2pWm6VlVXq+rZIvIXSZKELcX/Y1U9RlW/AWC8iJyqql9V1aOcc99W1SXMfAmAh1X1qy3O+rKIHCMiGRGptUqude9iIrqWiC4brisiDxHRt1X1KFX9qnPuowDGe++vUNUPishNLQkIiOjPVPVs7/02EVkLYHsYhtYYg0wm1FNmnZPftKF2lFPJisCIkhE1DFiFaNLr1i5R+PntGR5lFMcBLWfCxxbhrgkjgqMAjCKgkrWFX48KZ7RHJm8CziJLOXJpUNu4omAuOfbKOMxkKBOGHIbhHBG576qrrtLHH3/8QmaOdtdd/5tIROLTTjvtyc9//vN3BUGQs9aOA3CyiDxXr9dRrzfo2gf/Ljt1TpyYIMnWtQ4nVW2kNd+bri41fOlMADkQerb1p4/f+WGcaS9X8HOLUQIwCgCUdFGi6ehBt7k+3k4DqQ8cOd2+mQdPnP6xijHB+MAYhGEoqppL03T/J5544iRmpvnz5z+4Zs2a1dOnT5/+8ssvr5o5c+aMWq1WSdM0VdXORYsWHW+tXXbmmWcONV2jQG9v744dO3b0jR07dvSIESNG3HbbbbNFpHPBggWPtMTvVUREWL58ee2VV145bcSIEU+ddNJJ1RY4unLlytXTpk2bEoZh2N/f37dw4cKTrLUdxWLxvnnz5pnf/e53unDhwhPa2tpWnnfeecekabopCIIMEYGIyBjGCfufvmbpltuKY6a4LKkzCh8PpZu913g0oIsAOhOKMQTElyvYPrsY43IRP6uK8wCAYHrUo+gpiXoaG+LR0X5VaNgxNEAHz5pz6PIgMGBmBTCKiJZVKpUjjDEmTdPG/PnzPwSgLCJHoLlY/omqXgLgWSJauHjx4uNPP/30obPPPnsAwGNoLl+O32Xdt/a3v/3txnK5HM6fP/+3aJ2JAAi89zkAUwGcdOqpp+YvvPBCnH322fEJJ5yQA3CH9/5YY8yft0C+SkTmP/roo72NRqPjhhtuODCTyRTPOuusRy+88MJVd9xxx8cWLFiwiog+oqp3ARgVBMEO7xVzJ70/v2jdHbNGqu/16uq98WakmuQgANhsU98MRQwMP7N0iYxhUuybD/n3WzqlAMROROElzfY3NrXHrtTNFHTkMvkiGQNiZhGZ7ZzbPDx5IoKIXK2qZzDzd9F0T/0pEV2qqoeKyN8BwLZt27ap6hmq+l0RmQXgZhH5iohcpaqrwzA0RATn3DXOueta5buqeoWqnqWqT9dqte8DwPbt2zeKyBGq+l1m/giA7wL4map+jYj2S5LEA0AYhp0AvsvMp5577rn3Axi/YcOGxaoKEdkCYBYzqzGEMMgUWILRjXSopzfekFUf5wUKYXYQCoZhykcM08C+DMUMw7Rva8sHqHZCJFD1VtTDaYLuoe3xrLGH/Yu1NiZVtcYAQEVVy7vpmPNU9VHv/RUArgZQ9d5f473/qYj8OwBMmDBhPIBnnXNfAfAj59w5AK4F8DURmcfM1JrY/4jIrSJyq/f+XlV9vmVMPlEoFC4GgM7OznEicmPrB3hJRC4Tkc+IyI+897cFQWBay5lrVfVKVX30lFNOOUZV/aJFiz7YMi79RFQiIgbg2NrazHEHf7+70q1eGiwkROoteQkhOmIYp8DQBGUcYIVwOJMepCCAkBCooCAnUPVwXoU1rrXVoyi7nwgoDO1QyymwzTn34d7e3p8B+NsWFx4AYLP3/l4iuoKIHhaR/yaiLw1z6rp169Z57+cR0bUiAiIaVNU7ReR5Y0xcrVbPbf0ek1U1DwCq2qOqG4jofhHZUi6XAeC7IkIAvqCqIKItaG4LZ4jInxERvPevtK5fY+b7W+0eBGD78uXLx6nqd51z85i5G0Bore1rNJJsxuan1EumFo3w3mtKSupAMASNRJEACBk6ixWphWCaKs1tqegVUIWyiBcPIYhRQlLKhQccNDtW9YEIh0TkiciJyGFtbW29LfCCxx577PtHHHHEhdbabd77bzLzFap6jPf+X5o46Jf333//qWh6kP+P934HMx8F4HQA53rvkc/nl9frdYjIQbsw99SWy6opPvl8BQC6u7u3ENFfq+poVb1IRK4iIvHeX7dy5UpKkuR8Zka9Xv9WNps9n4j2B/DNkSNHnrV9+/ZRIvIhIjpMVZeoqlfVEcyQ6WNmpQ8+nyva9m4IO/XeQ1XFE6UKfYkUhyrTEVDEFkAWO4NuZAuAsPnDKlgFzih8ku0cU5y4NQiCxFrLAPYDUCOizxpjrgAAY4y54YYbvtwS5f1E5B9UdSgIgloURR8BIESEO++8c8qmTZtetNYeHYahdnR0wHv/pIhsrVarvX19fQsA5H71q1/dYq01pVKpkCRJXCqVaGBgwDcaDdfX1zcRwDELFy788JIlS96XJEnBOQcADSIKmfkSIsKwpXfO/bmItBljLlHVa6dNm/bIE088sR+AMUT0WRG5kIgmWWtfIWPcuPZJDJ9r90hIRVTEq5KAlBIIdYH0UCg6FMhZUvDvjSDVnZBhUhUSUijICxHCbDFXZGOMqKoH0KmqQ/l8/ptdXV0/rlar38rn8zs5hJmJmUM0jyPb4/j3h/ze+ylLly6dgr2QaepX3Hnnnefv7ZmdoyUamyTJWABoHvTtmbq6un4xa9asSQCuA7DSWvtSo9E4zHt/dbFYvKLRaKwF0E5EwoBENlKVMOPFkcJDCRBVUlEloLQTLgWz1987FAhImCECJVEh8Z6cdzBk20ITkIg4Y4xX1ZFoHuJM3XfffT/S29uLLVu2oFKp7HQ9/W8ia+2RzHyGqv6TiPzjsccei97e3kxbW9uZACYTURVNb7mIiIYmJIOwLUWqTqQVIqFEDFHV6nC7orDMBB22LOzhWbRC0LJRLalqGYqyQWAJVDPGVJIkqQPYrKq9AGCMmQoAaZpix44d2Lx5M/r7+5Gmbzn4822jVatWvei9/9M0Ted77/9j5syZawAk27ZtswCgqt0AtohIzRhTssZWDdvQkA4RtETaxAOqZSWWnXgR1Kr8/kTbG2ThtaAE9QQSZWIQ2EilFteyhoJCa4lxYMvf9xry3qNUKqFUKiEMQxQKBeRyudcVsXeC0jRFrVZDtVrFzTffnOnp6Tl2/Pjx944ePXrt9OnTzyGirY888sjLCxYsOERExhPRDGvtswACrz4m60pOqIMIBIX4ZqCYAWsZLXumAtid6z8A5DSvlgkKFkcMiBERqHUDiUu8994SkQCoEFF+jyPfhZIkQX9/P/r7+xEEAbLZLKIoQhRFbzugzjnEcYxGo4FGo/EqCejp6Tnv5ptvfk2dH/zgB8sWLFgAVS0CqHjvyTlnq2mFYF3VORnJICKwI2IFI0Qi7TCtLaYCVgnbAdoA6GRhaoPXhipIVJkEUCXP7CrleBAd2RHsvYcxpopmfMreaICZN6LpQWYRmZSmaeeuk7LWIggCWGsRhiGstWBmWGuxqwUFABEZ9ilCROCcQ5qmcM7BOYckSYbd/XuiTczcT80YHHjvZ6MZZ4O+vr5hx+14Va1Qa/M9WB0Asa+SUCcIRuAtg5QEBKDYrEJrwdhiIXhBRQyIJkMxQxQvkELh4RUq4kCJ2VHdOLiOx+YmmTC0trWwnQOgsvtoiegFInKdnZ3rRo0aJT09PTw0NAQAm0VkzvBzw5N/B0mMMU+pqhk7dmxXsVjkzZs35xuNhojICDSPRpPt27c/WSgU5hLRC95722g0aOPgWnbcW5VUBYCSJYBBChgQzWnt2J4BsJyheFkVr7Q6Hc2kZYU6ARSejCjZFN259UOrc6reOucMEfWpqnXOPQIAhULhN8PgMXNl3rx5Y4IgOIuZz46i6KyTTz55JBFVmXnFO4nYrmSMeTKKooEPfvCDs40x8621Z3d2dp566qmnxsxcArC1s7PzkVWrVi1X1QBAv/eeiYg2DK0upOgpiCBQIlIBBOrBOgTCCAAQ0jUQrGS1WF1vUPewLlTlKoQCOARewOqVUgzmtlXWTWuKiqiIVAAgjuOtuy1bgtNOO21ET0/PhO9973sQEXznO99BT0/PxJNPPrkDQAO/97C8k7RBVaO5c+ce19nZmb3yyisxZcoU/NVf/RVWrFjx/kMOOWQ9M3dXKpVRjUYjbKmGinOOnPPYWt04PZGhjHoQCZigAQsFpFwbxqlRpx6k6LI6gK5Kpz8zm20d0JHWQFAYTSUlALDexSNdEB+Y+nQxpZRlppSZ4ZybdPvttz9QqVSOt9Y+SkR+xYoVxx522GF4/PHHceCBB2LZsmWYPn06nnrqqQOZ+REiekZERr+T6BFR37hx47rWr18/NwxDvPLKKygWi3jhhRdw5JFHolarzXvuuee60jSdYFordxFJnHNI0rghiGc4jb3xUDEQEngyYEBrwx7KcuJHZzux1t79KZQ++iv5AHTnCadVBZGQhULh1SsIMfoe7KlsGRqTm5Q1xmkQBJtV9dijjz766f06bwAAEgVJREFUnpUrVy4EgIMPPjh300034bjjjsOaNWtQqVQgIjjqqKOwZMkSzJs3b/Xy5cstgFUA3rZF954cr6eccsrYxx57DJ/85CexcOFCDA0N4cQTT0S1WsWjjz4azp49+4l6vc5Tp049TVU3eu/hVXVbZUN/TH33k8c4DVRIiMFEohCjCIdXLC6VY+44DV+zACCEXiiWgnCkEp1EpKsEqqTEIsTq1Axg+eCy/kczp+QmqDZfuXpRVedNmjRpx9VXX32hiEBEsHTpUtx5551YsGABnHM47LDDcNNNN+GAAw7Al770pc8NPzdsUXe1rsOA7n4dBmjXK3NzgbHrZ2beWQDg7rvvxq233oqLL74YS5YswY4dO/Dkk09i7ty5uOCCCz4bx/FPRGSUiNydph71ap2W9T9eGGgsr4iqZSVVsLJ6Z5lIlU5srfmWAlgHtE7lDjgP5SjgAWb6MBTtoroMgpwoERTwniiJhwq5aPrxB+YOWwuQIaKEmWd573NBEHSoKosIpk+fjltvvRWqitWrV6O7uxvLli3DV77yFRQKhVeBtzcgd/2+exmm3bl3dy4kIowfPx4LFy5EpVLBpk2b0Nvbi+7ublx22WWw1ro4jgsARgJYVq/XUG/Uk2fK95+ypXxfrESGGUIEMhYGTP1ovQOYOr2+kcjvVt+K9c130cp4slyX4nDnBqYbRCAGkTZXUELIVtPeezeUu3rjOEaSJFDVpwEcmKbpLcMTnDhxIm644QYEQQDTPDvBNddcg3322ec1IL1e8d6/qryZOruDffTRR+PrX/866vU6kiTBAQccgOuvvx5hGKI15hki8lTz76lura/fUUt6F4siJIKCiREAakhB6BnGp1ST9lwbngJ2CfE99Zd4cPzIcDqg4xl4wQl64EE+BlyicCnYanHz4RMumviR9vO7C4UC5fN5JqKzVfXlKIomtzzGr5nwGwGwOxe+ngi/ntjuXowxe/s+0Gg0+ohofxG5o1KpoFqv6+LBn496dssPt6dcmWAtlCOCNRDKgJgxEopDoLRl60Cy5p5P4Hhgl/A2NbgmTuUGBeCBOUTokVZAtyiIFJSk5QmJlJKeyvaeer2u9XpdVPVxVZ1Zr9dv25PI7Q7M3sDbEwe+0Q+wt/b21vdwqdVqv1XVaar6eJwkqNdj9JY3bW9IKU5cZRwUDNPcuagBE2G7Kg5RAKnI9SD832HcdgJIARYOVdyknXtjoTpBoaRsTPOMHQy7fMutQy/qQzOr1arW63VNvd+kTc/NfO/9I3vTXXub0N5E9/U+v57Yvp7+VFWkabpYVc8DMJSm6aZyqcSNRk1fxOMHPb/5v+pQtWwgUBCxErGCiOJhXHYMuRkU4r7XAHj3aYhTAaC4rakI9dNkMMSWPBhMSsRKmjRKIyuuZ3Bzfe32crnGlVJJReQ+Vc3HcdyuqgPD4re3ib1ZHfhmVcDuYO4JxNaYetI0HYvmMen91WqVqo1YNqVdW2uutz9NSp3KTNpcxMEYgjEYVNULmvVxiwLVu09D/BoAAcAZXL6j7F9SBVRgiUwPkRJYCQaqrEoMWrrqp4WN2ZfmxXGtWq7UqFwuJyJyP4A5cRw/qKryelywNw7ck+58I336ZvtR1Uaj0XgewMEicl+5XPblcpXqtXJtk33x1KUr/6MAbnKdgQKsDFUVMTtUYFWBvpLvohRX7orZqyJU192K6tSz9Qv5HPcQaCpBZyvjRSiyEFIVkDioiBbL1W3LglGduWJ9LKDExnAtCIJEVU/w3t/MzIfsbiD2dn0jHbkrF+1qSPZkXHY3MMNX59ydaB5ePdNoNLZUqlVfrpSxOvO4earr5xvqvm8iGfggBFNIyiGYQwwQ4xwABqqLhmo+c885eJVf7NUx0gDE4iv9Q/JYc1+MDABvDJQs2DDYhlBmxD2Da6YNxOulW9dsr1TLWiqVtF6vrwawXFU/7Zz7TwB/FCf+MUuW1ylJmqY/F5GzVXVZvV5fWy6XaahU5q26asuA22L7hlbvR4a8NVAYKFsgMBACJZDm7mNHSZ41HpfujtdrovS7bkV58p/oRwpZ8zIIhwM0C0SLoBipCmqNnaHAhq3L7MT9D9mfhjIrrYRt3nu0fG9VAKd673+Npq8t82a5cW9ADdOb4bZdljfbRWSpNt9BeSJJknVDQ0MYHBqiwXRHd9+IriPvffpa4YBCE0I5grCFMRlSGFoF4DMt3ffDUtXLPfPxyzcEEADGnoNH01gWFLNmChQhgTJEOqiKQIQEAiPNU09+Zf3jfZNnH3yY9mVWasoFL16sMWVm3gzgNO/9KiJaq6qTdlfyewNv9+f+QNCGPz8qIgLgaFVdVK83egcGBk25UtWBel9f/4Q1x931yFUbYLWNIxgOoDYgDSJYE6IB8CEEjFKg1D2QdscVfHn9r/EaB+YeAdx8B9z0+Sgz8HxgeR6AMVB6hgzaVMk3Q/2JSQHvJOra+GTXlMPmfEi6o+d87NpTLyTeN5j5ZWae6b3fV0RuIaKZqmr3ZJ33BNzuAO4G0B7vMfOQiNyqzcBN8t7fN1QuN0pDJVQqJe2v9u2oTt9w0l0P/uNz3iQjghA2CMmEGXgOCSYDIqJuAk4AgHrDf7We6u/uPx97zO6x13fl1tyOtfucqRcXM+ZFAHNAmA2iu4gwRkBKos0jAVXy4vKvrHvslWlHHHZk2m1eQKJ5VfXOOauqG4Mg6FXVj4nIalVdpKoHqSrtsrzYed1VXAHsDaQ9caAQ0S0iMoqIPkBEDzWSZHWlXI6HBkvBUKWsQ2nf5uSA7SfeueTqFxPUxtpQAxMSmxBqAhKTBZhoBYALAUCBW3ZU/D6Lz8E1e8NprwACwKQv4nf1fvlUMWsJwEgC5oDpIVJ0EhGrJ6sAICCXuvYVqx8uzXj/YZPSWFbWelyHeA/nPRLvqwxa3XRN4COqugrNKPwx2ozifxVww1y3K4CvA95WAHdQ8xWHDwJY4b1/tlwupwNDVVTKQ9rfP6j19h3dsv+Ow29bdEWvUmO0CWBshowJCTZL3kQAW1pPTb1noPTK9oG0no7Cp9b/7LWi+6YAXP8zuMnn4rFG4kfnQ3MYgIgIU5jxDCmKCigBpE1xZlEfvPDSErffrFkU7BNQpSutxQ1PLo6zSerFi9RV/CvMXFXVQ1R1H1VdhGaIbxnAzgQ5u4vtLsUx8yMA7mPmbQAOJKI2VV2XJMlLtVqtViqVaLBUlUqpn0vloTofOhBVMptzv1h4dd4Yn7cR1GSJwwhiQhIbIjUBthBwJoC8ElzvUHqzKL5+/+l4zQuGu9Kbyplw4m04Ix/xjI68+W6r2gZifdI1dFSaEEtdOW2AJYG6hnqXEMaOnL7ptGO/+L5kjVks2/JjM5nIZKJAoihLmUyIIAjIGANjTEBEHSIyWUQ6RWSdqm5V1YqIpC3RDImoQETjiGgKM5eIaKOIDKpq4r2Hcw6NRgO1egzvUq3V6l5Hxhuys9OPP7T0lke7tj41nQNiG0FtBmojeBMR2yzIRNhKQh9U6L6kkMGq/7t6Ii8uXoDfvRE2bzprx0n/hc93FLiQi8x1zYq0CdAHvcdkV4V3Dupi9b6OgosR+wRGvU3PPuXSHcXcPiMGnvAvcJIZlwsjG2UzMESUzWa16SExZGxLGFS9sVbFK5SUAGBYWYoIMzN5BbnUgSCaph5xXCfvvSZJouVaw1NWejrfL3NK1a07frHwmpFsXcgRvA3hTRahNeRsHmKaXpZtIDoa0P0AoBb7SwZqEt+/AP/6ZnD5g/LGnHwbvtlZCAYzAYbzJwwo4U5xOl0aUB8jcDHUxUSuoQ4pJE0gmbCt9vFTLm4UM2NHDCxNlidDweiQOAyCUDkwFLBBEFhSZrVEqkDzHLEVAiA6PFBFE0pFkjhS9YjjVJ1Lkfg0sZ3SO+rI8NBSo7vvznuuz8S+lDMhwBbWhmRtVr3JgmwAmAhqAlolij+h5svfqMW4ZKiaFu49F1e/WUz+4MxFJ92GS3MR246M+bYSGEAizD8mJ4d6p+oa8L4OcQnUJzA+hhWnqU+gUdA2cPKxnylNHj/rmOrW9N7+F5JGOiQjyXIYcgC2zRejiVXFw5Np5Y3xMGxgxBMJPMSlFHtPUI1NG/eNmhNm8uODUzZse+nB+x78WVs9KXXaDMgYspyBNyG8iQATwIRZwIawYPOCQj4LICSFDNX9V6qJ5O5bgH/8Q/D4o3JnnfhzfC6yvM/IdvPXADpaLd0KoaJPNS+xmjSF1QYkTeEkVfYpGR8j9Q5WRKvjRkztPf5DC3j0iCkn+AQvlDdUu6rbXaPWn5KrCEEErTwXTTKALbDmRgSaGxNk26bmppoQc7p7ux546PE7ZHvfutHGUJ4DOGMRmEi9sSQcwgYR2GTgOCRvDFXVaJUU81sA9PcM+X92Trru+yT+8w/F4o/O3nbyrTiaGF8cUwgOIMZRreZegerDgB6YJiQSw0uqgYsh3sFrjMB5eE1gfAovHka9pjaM+ke2TxiaNnWujBkzOcxnO/KFXKHNBpnRAODSRm+lVh6q1odqPT0bkjXrnuW+oS3tLo1HsKGADIQDsAnhjEFAFgmHsDYCmYBSG4BMRgMQvQTQcYBOBwBVPN5TStd6hxvuPx9L/xgc3lL6u5N+hpGwuHl0u33a2N/nDiTSXxBIRHWCNMilMdQ7DSVF6h1YUxXvyKhD6h0CCKCCVLxa9YASKYlyK/AOIJAyCUFBDGImB4KlEEoMbywCCtQbQ8QhxFiEJqDYWLDJakBEm4g1UKFPDI/Rq16xY9AdZQzOXzgf/X8sBm85AeM5t8P0eXwtItYRbfZToOavCyDxKj81RCPgaKJ3iL1TAw9xCVgdvHcw6uBVm/pNvQIKpwJV2pkKBQCEFKoMYoKFITVGQQxPBsZYeLIwNoQQw3BAjiNEzNioQKzAebQzkJRW9lXcbXEqctx5uOryYUv1R9LblkP1+JsxjS1+MDJn7wkDuhKEHACQQqD4OUgExJPFq/EpqTglcXDqEXoPJYETDwbgROBVAQY7ABCIJQKYYQBYZogaWGMAMkhhEJiQPLMaG5BTlvWUsgXjvJahAxS1RqpfH6i5eYjxhfs/i7clj+rbm8VXQSf/HB8T4LOj2uwzgaF/0GZ2oeHuVqjq48zIQzHee4QiSLUZgwN4kDYdt0Kkqq38BM1XhYnAMMwKGDQ979y0rERIRbENQJWIPgDorF0m2Ei9Xt0/5N4njH+//zzc9XamRH5H0iAffiOC9gLOVeD8kXl7bxjyxYC+OqMv0VaoPsCEukAigNqg1EEEFlWBQKHUFC9SBoOYiEUhRDoIaInBiSgyBDpJoeN2m9qG2Mv1/SV3iir+s1zFbc9chLc97vgdzWR+uYIfugUnC/C3keUlHQXTaQiX7LUCox9en1XwIBENCqTcvM1FVe0gSAcMzYVgxN6a8IrrBit+IHFyrCF850Orcf/ll781Pfd69K7l0j/mJxhtLb4+ot2uDy3t1T30Vihxeml/2U1WxpVLPol3PA088O7/MwI6/ib819j2YDOb154vvBVSxfXdA+nEBz6Ns4G3T8e9Eb3mUOkdJsW++NT2UjpHVO/V5vrvrRfVh7f3pTNLdZyLdxE84N0HEEtOgMsRzukdcBUV2vRWwYOnbTuG3HZXw4J3wki8Eb2uQ/WdojW/RLz/n+CluKaZTMhzm4eJwB9aFHADFf1X7+X6h/4MG9+LubzrHDhM934KLyhoaSPB3/yx3Nco42+811UPfBbvWvD67vSu/0eb3enEn/K17RkeNExXvPHTvyfxeuVQQ0be9zn50hs//c7Re8aBw3T/Z+TScl3niuBm9cCbLLeXGjr3mA3yl+/1+N9zAEHQ6oA/rxLLBPF49o1Fl54vxVJ08Ge/kwvkN0vvPYAAHv8K6ur8BbVEnlNF6XUArNQS/ziJv2jJ5/Cm07W/k/SeWOE9UddvUJ5+pimpYhODTtyT1Y29fsOrv2fxhXj+vR3t7+l/BQcO0z2fc0ucEyeil+7OfV7xFYXI4gvx4Hs9zl3pPbfCeyA67cfmFiaziVX/BgCUcL1XGf27z/vz8S7vNN6I3t23oN8caW0//+lcF/0PC+4VIBJgZm2aPw3/y8AD/peJ8DAtOQEuZLfAQ0sK7Q0rbv6SE/Yen/L/017ojH8LZ5/xb+Hs93ocr0f/D6s769KBP+5xAAAAAElFTkSuQmCC\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic3bx5uF1Flff/WVV77zPeIQMJIYQxYRRBpBGcQFEEbVQQUXB6xW5tWx9+Cm07IYIitiJog2P7qu3UCN22aDs0KIIyg0CYyUhCyHiTmzucce+qtd4/zrkhQIIogz6/9Tz1nHP22buG715D1VpVS/gLktnZjg3P2wGz3RC/N9hBCHtjMgOxGjDZv3UAkyZim4EHQO4i2j3UkxXUb9kkcrb+pcYgz3aDNvLjOah7BSZvRnwLX/8D2axILM0Dtx9ODgGGt/P4GNgtqD6A764i3+iJ44dC9CD/jaRXyqzXrHs2x/OsAGhrL9sB8W/B7ARKwz/H7TAE2btAZj89DbAW6X4HHRknnzgW7KdU5fsyeMKmp6X+J6BnDEAzhLWXHYz4c3GlG8l2HgL3frDsmWqzR5KDfpnOykkIL8D4OHPecIcI9oy09kxUaut//CKCfp7qtMuwwb8DnrOd5nOcXIfJCryAOgEpASUwh5HiBMwKEAW6YF1chIghthtqL97uSxG5C8a/TXvsBLx9VGa/8Yane6xPK4C2/kd7EuWblIZ+itU+BMx93E1OFmLchqQpTmaDA0kAlyDSwSwizkAFfN84RAfOQASLCVACDVgAESOGDZjmSDwEtYO20bVVaPMCionjiPoe2eXNy56uMT8tAJp9I2X10GfxyQTJ4GtADn3UDd6NYv5niKtgyQxcYjinkCSYi3gHikeSLhDwXjHzTNlWB4hEYnRAgoUSmIIqxAQsYEFQA/JRNHZw+lqiTn90R/VGYuNKQl5j/cTH5JD3FE917E8ZQFv23b0oJf9GNnQd8PFH1y7rkORKLJ2BTxJcAqQOSQyXGEiC+IB4wZxHXMABeIeZQ3q/MBQRhdiDNGqCaMSiYTFBKIiF64FYKBbAigKLD6PFsYjt+uhOcwHdiUMRe5fMe8uSpzL+pwSgrfje+/CyK1n1dcBeW/7wMoZll4Kfhy95SARXMsSDSxxkIInifIK4gHpH6kAlggjOOwTBJEFV8FIQDcQCGIh6ighOFdMELQKYYF1Bg0KgB2ZuxG4kFqvw+cmoDG7V/cXk7Z9iulx2efvX/1wM/iwA7bLLPIe1v4m4RSTJuWDJI/+675OUB3ClCmSC9yA1cKn1dF3mSFLDRJEsAYk9wJxSTEzQGW3TXlEQC6G7sde/0gzDZ0Zlt5Ty9Arp4GBfnBWCgxghOkIhkBsxGK4QYgs0gHZAQxPtNLH41q2GH4jhNFRfwrzK20ROis84gLbkohLp4C/IuAXso1v+UFmLlK4gLc/Bl4AMfEWQDHzZIAFJhKQMLgWzhMayjTz0kyYbrt2T2Nq3a5WFE3HWqgYzNxeU81ao5ADVpJ2ldLI6G6YN+o3zStI+CF+9n1kvWcYub6hR330mIgEtIHSAYFgOVkDREegYmoO2QfOHCJ3X4thqDirn0rUX0Kr9rex/Uv6MAWhLLiqRDPwPafdBxN79SC3yS1y9S5JVoQpSEnylB5wrgSvTAzCt0Vqzmfs/32By8cs2xZ2vXGJHjo/KnlVz1aEsLZsXKVQkpIlXTHo6T8wFjU5iTELULFpEQmN8Gsub87lq2gy/5pUM7ftb9jtjgNJO07DQQHPBerMeYqsnztaGmBvabhNbVYivemRwfJWitADktbL7OztPO4C25KISvnolWWMN8OZHasj+L5Luih9QfAWSmvVEtwquAi4DyWay6aYHuO/8A1phYOkd9sbVTbdgVlYqJVk5wycpWZaRJY7E+2i44L2Y0Zv8CkgMCMQ0xOCKGAndnCJG8m5ueacba7pkw/Pksp2rvrkH+3/kXqY9fx+cbiA0HbEDdIzYBmvRE+12l7y1GRffsRUj/JBubR6xdbQsOK37tAFol13mOXjzzymNrQL+/pGnS1/FZXvg64IfAKkqSTnBVRRXEaQ8g7HFK7jnU/NHde7tC5N3RpKB4WqpKuVq2cpZSpJlkvrMSqVEvE8ty9JClSJJJIr44BwWY0xjNC9iWZ4HH2OUdrsjzpm1Wi3X6RTW7XZpdNviwsToQfodP92tPpj9z1nG8Pxd0M4mYtugEygaGdpWQgO05aCxFI3/+Mhg5WsUQ3vx0Npj5GVnh6cHwCVfv4x0ZAViH9py0WcXIpUDcPVIOiBIFZIauKrgKg6z2Sw8c0m72XK3uA+MuMq06dVqzSqlTGq1uqVp6iuVsqRpGtIsM++ceO8NEQQi0AGmuKAElAEfYxTAQowUeZBu0U3zTkc7nVzzvGOtVpdGqyHa3rTpUPvSjpVyreDgc/dGZB3aVrRlFE3QlmBtoxhXtPUQlr/nEVTceRQzd5c9/+GUpwygLf7Ke0jHykjrS1suavpV0vqeuDqkdUEGlKTsSQYUqcxmcvky7v38c+6yt/xqvHTwvEp90OqVTKrVitVqNcrVasiyTBLnUhEBGAXW9ssqYD2QA1MT3bQP4g7APGBOv0w3M4kxxm5RxG67nUxOTkq7ndPO29YYb9pQ55a1z3U/Opb9P3wXA7vtgbbXow1HbBk6aYSGoA2hmFgK+ggnxoEPYENR5r/3y382gHb/xQeQFm/Gr/0ImOuD9zXS6p4kg4ofBKkJyYD1gCzty9IfXlVsumuXm7NPrC6Xh6sDA1XJyhWmDQ1ampa0XM689z4FNgP3A0uBdr8vSk/veXpceFj/9y39/6ccAq7/vQLsCewHDMUYQ7fbtWaz6VqtXJutCWt3OtJubBo7rPjMXsmMA5cz/60vJ3TuJzQEm1TiREJsRsKEElqrcEWfE0UJcz4P2Q9kwfvv/ZMBtD98I6XW+QXZkj0Q9uzdLZcgA8OkdYdMF5KakgwK1AWfPof7Lr52vJk27qufVq0PDKTltOoGBqo2OFi3crmszrkB4CHgAXpc5vpgSf/7MFAD9gVe0AcHYAlwK3Af0AQm+gDrVmV2H8i5ZtZpt9s2MdGwRqNJq9WUVqsZ9m1f1BqqxAr7v++laH43sWHEhhAaRhjzMBkJExNgJ/XhWUK+YDVx9FWy/9nbnN4k27oIQK39FUqLFmLhlX1beB+U67jMIRlIGiF1kApen8PCzy0cYZ91Dw2/dadp9ZqrVWtFrVZLa7UyWZaVRGQc+D2wiR73zARKqhqccwqIquKcmzSz14rImUC135uWmZ0nIrf0fw+oqjjnhJ54d4AGPU6dJSL7VqvVoSRJOlmWSKmU+KxSssVjH6zOa/5g8453nn8bzz3tEHzpDrTrECeQKnjBJSmhdQ+izwEWkNx/Oez9ReB924Jpmxxo91+wl0rjjc4vO7d3wQKu8kP84CyyIUHqgh82/ICQVA7k3m9evybstXb98FtnD9Xrrj5Qp5RlWq/Xnfd+AFgILANSVfVAHZjVf4E1EZlhZs8VEWdm3xCRD/QBfqSjIhtU9SIR+XszUxG5T0Q2quokPV25EZjsv4wA7AI838wajUaDZrujk+NjyWSzyZzJ/3pwTnrffPb5u79B2wsJEylxUonjkTAhxPENaOcURBIADfM/6bT+H7L/6Uv/KIBmiN1z/tWS3TqESM81JMlXSQb3QIYgGwQ/CH5QoDaflT+7cXNjYGLl4Lt3qA/W3WC9rtVqxdfr9QxIVPVeYKNzzoCOqjpVLSdJUg0huCRJusC4qp5FT7wPAlYCP1XVkf5zs4DXAbsCtwN7OOfO7nNiRk+EWzHGwntvzrlSn0N3APYzszjZbE50Wu1sfGKcZqNtu05+ffO0aitlj+NeSphcTGyATkCcgDhmhInlWJziutst/E1D9v2nIx/rmH08gPd+7ijSpc/BRnpW1+RWXGUd6WCGH+5xXjJo+MFhRpcu6q6+a2jJzHO65UpdBgeqbmBgwMrlciYiqqqLnXOFqk7vc4WPMRpQTZJkB1U9FEhF5Fwzu9DMbnfO/VBEXqyqrwGmHKW5c+4XZnatqr5dRA7y3p+uqh8DVFVvE5H1QAtwIhLo6dSNzrkKsEeMMQ0h5GNjY9rpFH5iYizutemTrjTvuS0G91iANcYJE544HoljQpyMxOZcsAN7SM3+AHHPhbLvP/9ua7zc47jP7OPEhz+KdcA64MIdSJKBBxFFpPcGigmx1Tc/f9G0syZrtZofHKi5en3AyuVy0gfveufc5qIo6mZWAWaZ2Rzvfdl7/3AI4SozGzCztqqeG0L4ELCPql4QYzzezFRVH1DVB8xMY4zHq+qFwHwzOyOEcF6Msamqg865K2KMK0XE05vaTDOzUgihqqojqnqD9z5kWZYMDAwmpVJGvT4gS6af1baHbzyY0FQwD67nzBVngMNz4xYcbOXHLMRzzEy2CyB3nfdicQ/8FOvMRrsQu78A2xWJYCaY6zGwxf1Y+quwbNrp19QHB6u1Wo16vUalUk5EZGdVXaiqQ3meO+/9BhFZrqqr6OnAE83sH/ttV4HvAB3n3Plmttg5d6Zz7sNm9i0zu69fvqWqH3bOnWlmy83sAhFpiMh/0JtgO+/9e1X1TTHGATNbb2YPOefGVbUEDKvqQmBOqZS5wcE6lXpdqgODlWXTP/Iblv48RePeOOt5wrUvpZrvDt3/7WMxS9zi/+aezx2+NWSPssImeqbY4j23XJDSeszvBAJODFHBWcrIA1c1sr1zN7DHjqVKhUqlQqlUcsA8M3tQVV8MNEVkjqoGEVkLPKCqVwEvA3Iz+yDwG+BMEfmtmV0hIifHGM81e3T8Z+p3jBHgTu/9h4qiOM4591ERuTKEcIb0JCMTkSuAGWa2v3Nujpk5MxsFEhFZ7ZybWyqVHq6pOrQaJuPOcxvNve6sjy2+l+Gdd8EkIAJ9LFHWYFMLokXvN9vzQWCLE2ILgPbA53bS7qrfi3WP7l+6GvxcJPRG4Kwn5LE1l/FVu63e4fM31CsV6pWKZVlm3vshVb0S2BBjTAG890PAAar6ahE5EjjPzOohhLO89xeaWQ6caWZnAS/vA3Wlqv5eRFqPAbEmIkeIyCtCCAeKSEdEPqaqfw/MVdXTReRCEXHAe4B6COEqEbk7y7LNIQRCCEWaprO894eWsmyTxuhDCG7NzH8s77X+jBcyML2AsBIxQ0xwKgTdCcl/h9kRwAJh/fds4blz5aAzVz+aA7vtE527fi7WXz87/wCwO+ZAVIgFuODZuOqmkfrrJirV+k5ZkkiWZWRZtqOZbYgxHglUnXOJiKwMIdyRZdnVIYTXmdl6EXm7qq52zn1BRD6gqv8CnAtc65z7sarua2avF5GjeQyJCH3R/IaZLXLOvSHGeB498f+AmV2oqqu89/8nxjgmIrPSNL0qhDAjhPBiM9sdiCGEpvd+JE3TOTHGDZVyOSpUNtVe/fMZI9cNMn32LliMmBnmIs4ghnshHtHrybU7Iq9/A3DRFgDNEL1l/U6uUhzRN8wbUD+vF8PAepecoHEandburbkvv7mSpZTLFSuXywnQCSFc1xezIefcc83sOOCQoig+18fgy2Z2gZl92cyON7MvAb9wzl2vqqfHGKfW2rmqLnTOPaiqK1XVJUkyD9iD3grlPX0wNwIfU9WXmNmXzGyVmf1cRN7rnDtdVS+MMTpVfTcwA/gVsFBExsyscM4dVy6XnRmxiGqTw6/cYcbqKw9nmo6CbEREURFIDPW7IGETyAwIL9PO+oZZ7516gLOP/budTZfuIXp3FT89Q9yvcaUhpNTzKLuy4TJlcmLZWPqi+2P9wIFqtUq5XDLv/d5mdqeqntLXQaNm9gBwrIisMbN5fZ10sHPuJ8C7gbu9919X1XeZ2dH0ph8Xq+p/A8tFZJr1RObFIvICM9vVzB4Efq6ql5rZXSLyfOBvRWRXVf2EiMxxzh3vnPt2jPEdgJjZcF+kZznnvuGcK6vqwar6ehG5XUSeZ6YbBcOMxIrx20v5is2UXA0NPYesRgfqIf4Bs5l0H/yDWHUZq45eec63/jDpALTg1c7dNouox9Nafh1KgKRnrhWPakKINZrtlzSnv7ZWq5UlTb1kWVYG1hZFsczMFpnZ61X1VBFJrfeKvm1mL+nruLkhhKPN7MNmdngI4Twzu8Y59wHgWjP7sIhcaGbvV9WXqGpZVVv9UlXVl6rqaX0996GpZ/v68jwze4GZfTiE8BpgjpmdZWYvCSF818wwszSE8E4zO9HMVqnqMhFZm2VZ4n0qWVay1ow31Gg0DydaBTMH3qHiEGeoy2kuv4YY3wy3zUL1WOjLq133ritxP3w+2HSgQTrwc8p7DeBqDl/ueVxi1o7Nimza5VNFqVqlkmWSpulBqvrrT3/603bTTTed6pwrPVZ3/TWRqnZf/epX3/ye97znZzHGwVKpNEtEXhVCuL3b7dLpdGzmio9XZKBb4PIKoQXSNEKrS3dJh2LytfRiFqMWTvmDe+m3X5WYne30+kUbnFkvCC38L0U+HdZ2KO9uSMxw0Wh3xyanvXHCe79TImLee8ys1O125998881HOec46aSTfrd06dIlCxYsWHDvvfcu2n///fdutVqNPM8LYNqvf/3rI733d5xwwgnjU4MaGRnZuHHjxk2zZ8/eYfr06dMvvfTS/VV12pve9KbrZWrSvhWJCHfeeWdz8eLFr5kxY8YtL3/5y1t9cOyBBx5YMn/+/N2yLMtGR0c3XXHFFUclSTI8MDBw1THHHON/+ctf2hVXXPGyer1+/ymnnHJkURSrsiwrpWlqeZ6Lc07Gh49bOty4ZJB6UcXFnh+gvcbQfAbGlcDrwaabdcfMznYJt66Yha2+A3hLr4tuBGQIbWe0V7Yp7xlJY5mQPjfUD16YJok453DOzVTVmycnJ1/kvXdFUXROOumkF8cYJ0XkkBNPPPEgM/secBpwu4hc8etf//rI4447bvyEE07YDNwAvA04cqt535L/+Z//WTk5OZmedNJJP6PnsgJIY4xVYHfgqGOPPbZ26qmn8sY3vjE/4ogjqsB/xhhf6r1/N4D3/lxVPfG6667b0O12hy+++OJ9yuXywAknnHD9qaeeuujHP/7x604++eQlIvJKVf2ZiMzy3m/IshJh2iE1Ri/dn8I2o/lGOg8NQLeKCuDWYr04l0tX38rNuoOjU+zpdHHSW2EAKiWE0PO2FTW6K0qE8fWmyXBWqdfTNLM0TUVV91fVDVtzhqp+xjl3HHC+mVXN7FQz+5CZHaSqHwVYt27dGjN7jZmdr6r7hBC+r6qnq+q5ZrYsy7LEOSchhAtCCF/ql/PN7BwzO8HM/tBut7/Sr2uVqh5iZuc7514FnA98N8Z4ppnNz/M8AmRZNg04X0SOPeWUU64WkTkrVqy4sq8bVwP7eO/FewdpdQjxMwgTG2g9tAMxr6BqGBGTdAtO4QFH7ua7qLoXrjHvEQBtGmopph6LghZCc023M/yCr5mZgk2tCBpm1th61aCqJ5vZ9WZ2DvAZoGlm58cY/11V/y/ATjvtNBe4S1VPB74FvBG4EDhTVY9xzomqmpn9j6r+SFV/FGP8jZnd1Tcmx1er1dMAhoaGZqvqN/ov4D5V/ZCqvkNVvxljvDTLMm9mOOcuNLNPm9mNr3zlK1+oqvHKK698oZmpmY2LyIRzzkTEvEinW33ORbTXdqEbUQUjwUiINn0LTtaYi9meiWg8ACkO6GPQQaRCtBwfCyIlEEOHO6jf1bmkSJIkAENmtjaE8KrR0dHvAh/pc+FewMMxxt+IyDkicq2q/peIvK//tlm+fPnyGOMxwIV9Sz1mZpc75xYCRbPZfDO9KcjuZlYDMLMNZrZSRH6rqqsnJiYAzldVAd7br2d1COHMNE33VtW/FxFijIv6n2c6537rnFNVPRxYd9ddd80xs/NDCMc659aratk5twFI1dUXEJM2lnchRlwUMI+TKpCjZEjYT0PsJBJ1fxwHA2ByD6KG8wENAWcZBYKUM4b2ayeJK8eolqZJbma5qh5YrVbX98FLb7jhhi8fcsghpyZJsjbG+Enn3Dn9acyXVRURef+ee+65B70A0b/EGDc5514A/G0I4c0A1Wr1rna7japuvadwd9VHtkEPDAxM9EV4tYicEULY0Xv/9yJyboyRGOMX77vvPpfn+SnOObrd7qdKpdJbzGxP4JMzZsw4ft26dTNijEc65w4ErnbOdfO8GEqSxJLpB25ktFzDaUSDoNERDEQdjvsxDgQ7VFRDglgZo95TZPogJlUsRpCIRkEAqQ672ryuiQQRSVV1DzObEJH/k2XZJ/uK21988cXv74vyHqr6MTMbT9O0VSqVjqHn9OTyyy/fddWqVfckSXJ4lmU2PDxMjPFmVV3TbDZHNm3a9Gag+pOf/OSHSZL4iYmJep7n3YmJCdm8eXPsdDph06ZNOwMvufLKK1/5+9///tA8z2tFUQB0RCTz3n8QwLmesynP83enaTrovf+AmV04f/7862666aY9RWSWiLwjxniqiOyRJH5pURQastkxteowRW6g1tsVZgZiRHsIOBBjEI2VBNXe3kUAtSaQoma4QsEJFoQ0rbq0hjmX9yTKhoHRWq32yRUrVnyn2Wx+qlqt0g9R4pyT/pywBAx1u48E+WOMu91yyy27sR1Kkt7y/PLLL3/L9u6ZIhGZ3e12Z2/93LZo2bJl//2c5zxnLvAl4L4sy+7tdDoHxxg/MzAw8KlOp7PUzIaT3to+NwYMyUpYXiBqRAOHEdWDTiJTLkHFoYVsUYxCCwgQlRiFEAQtDEmHVBLnvS9UNdCLV7SA3efOnfuqkZER1qxZQ6PR2OJ6+muiJEkOFZHXmdmFIvK5I488UkZGRkoDAwOvA3YVkWY/LlOYmSVpDciGevtrQi/Or7FvPGg/YnCDc72NnvRKdEqkidDEaKK2DmMN4hLvk2az2eyISINe7GIDgPd+N4CiKNi4cSMPP/wwo6Oj9EXqr4IWLVp0D/B3RVG8Kc/z7+y9995Lgc7atWt9/5YNwKoYY8PMmnnezXGuRKBFpAk0UBqYTqDoFrxMSTDskTCJ1VCGCQgmZg4vZoZnMoa8UqkMls0siTHuY2YPbauzMUYmJiaYmJggyzJqtRq1Wu0JReyZoKIoaLVaNJtNfvCDH5RGRkZeOmfOnN/ssMMOyxcsWPBGEVl37bXXPnDyySc/L8Y4R0T2EZGFIhLE2ySiE5jMwBCi9QL+Ig7byotvWALxkXi/yRCQRMU5jFhYCXGaxDgeuk2DctL39TXpBcCfkPI8J89zNm/eTJqmU55rSqXS0w5oCIH+epZOp/MoCdiwYcPJ3//+9x/3zNe//vWFJ598MmY2ADRU1RVF4V0+TmahEYxpBBVBAog6ByJWe4ThIglqG3CyCmwepkNmKIZEwUV1DlTF8gZx3GBGGmN0fZ3x+B34j9Bm59xD9BjdqequRVEMbz2oJElI05QkSciyjCRJcM6RJAkissWCAqgqU/NIVSWEQFEU9L3M5Hk+NbnfFq1yzo1OratjjPvTC8azadOmV/bvmWNmDeecxBgTjZMSi9CIQYcR5zykeFUiINQQAZEHzeLaxDTeLUYKzEPcAWLcY6ZmipppjMFI8tEGzeXW9TtnWZYCrFfVA+jtBngUOefuEhEdHh5ePnPmTN2wYYMbHx8HWNV/BmDL4J9BUu/9LWaWzJ49e/nAwIB7+OGHa51OR60XtdsdyNevX39zrVY72Dl3dwghiTGaH1+UabGpFQszEg3OgSgizjnE9u8bkdscdqczC/ejyeL+Mm6WYQ3MopmoBefMJOk21laTxuKKmbrY83aPmlkSQrgeoF6v/wxARO4WkearXvWqHdI0PcE5d2KpVDrhqKOOmiEiLefcfc8kYluT9/7mUqk0/qIXvWh/7/1JaZqeOG3atGOPPfbYrohMAmumTZt23ZIlSxaaWaqqm83MVFVKnQdrne66ajRJXHSiEcQkGjaO0lvOkSxB7QHnE1lCHFyLWc+3H2l5xKtapqYuRpNue6ySFSsXhKCxv05tAHS73dWPEZ3k2GOPnT4yMjL3C1/4AqVSic9+9rOMjIzsfNRRRw3S24X1bJysXGlmpec973kvnT59euUTn/gE8+bN4/TTT+fee+89/MADD1zhnFvfaDRmttvtrK8eGj3VECzLVy0IjfGKRiOqOtRSU0vFaE3hRJi2nsKWJ2xuL9fa7Nc7t7HXtLNGjOwgaoWpYEoSY3emaPeAPG//CkoVEcmdc4QQ5l166aVXNxqNI5MkuQ6we++99yWHHXYYt912G7vvvjs33XQTCxYs4NZbb91XRK53zt1mZjOfcPhPkURk0+zZsx9cuXLlwcPDw6xcuZKhoSHuuusuDj30UFqt1jELFy5cVhTFXO990teteVEUxJi3he4+MXYjgDnUjAg4orWm9nJo2GGmm2bLEnnrzRP6k8MPe8QS41EwkwTFFIsaRIr25qt8vmY8uHlV770lSbIaOOKFL3zh/y5evPhKgOc+97nV733vexx++OEsXbqUiYkJ5s2bx2GHHcbvfvc7jj322MV33nnnI6HUp2nSLfL4PVJHH3307BtuuIHjjz+eK664gvHxcV7xilcwOTnJ9ddfnx1wwAE3NZtNt+uuu74GeKgoCosxGq1VY3lr9CoN7CjO1KI4ExEUxZNN4SSUXiwvu+YT/bCbbRLldoSDMTnSO1seogU1cTEXb2YyvvaOscHB31dG0zdrjFG893cDx+yyyy4bP/OZz5yqqqgqt9xyCz/72c94xzveQQiBgw46iO9973ssWLCA973vfe+cum/Kom5tXacAfeznFEBbfzrnEJFHfe87erdY8F/96lf86Ec/4rTTTuOaa66h0WhwzTXXcPDBB/O2t73tnd1u99uqOlNVfwVYu92VOe3rapMjCyei2lwfxXDOnMTgRQSTl4OB8QczVkJvcyPnvGnuJDI+ihWvAKaJ2kIzqmoiqhBUpNMZr8/ace8j1snzljrnvHMud87tF2Ospmk6bGZOVVmwYAGXXHIJeZ6zdOlS1q9fzx133MEZZ5xBrVZ7FHjbA3Lr348t2+Pex3KhiDBnzhyuuOIKGo0Gq1atYs2aNaxfv54PV+O33AAAECZJREFUfehDJEkSut1uDZihqre3Wm2X5+3unPy3x2548Kpu6sV7QROPpIL3XkYxDu8teev/Kjrjl+dctnpF71W1uFnjzvVHnIV+rSBCRDDBDDWl3GmM/LrUWTbS6XQkxqiqehuwT7fb/Y+pAe68885cfPHFpGmKc45SqcQFF1zAnDlzHgfSE5W+W2pLeTLPPBbsww47jLPOOot2u02e5+y1115cdNFFZFlGv8/7qOqt3W5Xut3cyvmDGzutkSs0UlLFMHEoiIlhbJjCR8PcIWLj1p4o90kvPegaSe/bC2wOcLsZY50CaRdYNzfJI6gbWL3LIe+euyh9+4ZSqSKDg3UnIieq6n3lcnm3vsf4cQP+YwA8lgufSISfSGwfW7z32/s93ul0NojIfFX9z0ajRbPZtP35wZyVt/zbKomTcyspVkqFSoaWUkSEGcCBmKy2uN9id9LCl8NWu7NU9UKsdHEf5YMFNgjgpLcBFpCiPbkTYbKgvXp9nnes3W6rmd0I7Nduty/dlsg9FpjtgbctDvxjL2B79W2v7anS6XQuN7MFwPV5nlur1RLXfXi9FRPtojM5B3D9bXwighNYh3Fgb65culgtfmEKty0A+qHWFZrvNG/LPEddI3WGiLkE8JiKt/TB2340viD9/X7tdtva7bZ18/xhM5swszfGGK/bnu7a3oC2J7pP9P2JxPaJ9KeZEUL4dYzxFBEZy/N89cTEpO902rpP6boDlt98adNjPsE0wSQRIxEDle4ULhp3nu/Xt696HIDy6qVd1Asql/ZMdXy7F5ssiUURvDcRr1i3NT5DWxsmKvnitZONhms1m1oUxdVmVu92u0NmNj4lftsb2JPVgU9WBTwWzG2B2O/ThjzPZ5tZmuf5NZONhmu22jpkS9fE9sho0R4fRhDvRRMxSxLDY2OIvq0nmXIJKm05bWn3cQACOHFna5hzX1+MM8ytS5yId+AEBMErsui671b3Gbjn2G6n02w0GrRarY6ZXQUc0Ol0rrZetOsJOWFbYG5Ld/4xffpk2zGzTp7nd5rZc4HfdDqdvNloWrc12dqrfs+rF1373YokvU2ECeC9+FTEsGQjPbcfxLlLXJJ/+lGYPcr0n3LPerS8D/DbHhfaWxOxsVKCeYdkHhMvRAvDS2/58Y0H1X9fGZ9o0Gw26Xa7m+htAH99nuc/2NoIbP35ZET6sRZ4ayCfTB3barvb7f48xvhK4A+NRmt0YmLSJpsNe/70m+tLbrrsRrUwLe0fB08F6Z2rtzGI7+jpPq6MoTRfTlo6sl0AATq+/U+az76h/1AVlZg6o5xg3uNSj6WefHz9kj1orrCdSsvWTUxMyOjoZtrt9lLgTjN7ewjhB8CfxYl/zpTlCUpeFMUlZnYicHur1Vo+OTnpGo0GOyVLVkvzIR1bt3yPzBNTJ5Y5LHGQelPE5T1JBHTObb7onvFYvB4HYO3kVWuIpRSTb/aXLSd6WJQ6c5nHSg4p9xqS+67+9tCe9QcPk+66NZOTEzI6Okqz1VpsZjer6luLovjZ1jrxyXLlE80Dnwy3TX0C62KMv1PVk4GbGo32srGxMRkd22x0143sOfTQYXf+5t/qpQQyJ5qmWOKQLDHxxiLM3tTXfV/TIknlnSselxXpcQACuEp+Tsx3HERp9Pz/7kWZp1tySEkg8bjUiROscsvln1v7kl2Wvrzb2LhhbGxSxsfGtNlsPqSqV5jZ60IIK1X1uq3r/1PB/BNBm6Lr8zzfqKqvUNX/nZxsPjw2ttmNj08Smps2vnju4iNv+cnn1qVi1VRESg5KhpQTXObIUXdEP/bRiPnsHZzaJ7aJ1bYuykkPt73xbbR+Zu8N2P6ibqycQrmMlZ1ZKYVyIi6VOHzzjz9/99ELlh8dWhvXjI6OsmnTJhsbG5sIIfwXsIOqHhRj/Da9I1mPom0BsD0An+iZrWg8hPDdEMLzgel5nv98fHx8cnRs1MbHN2unuWHkmL0ffMXNl3/+LtEwVErFlRJcmpiVykgpRcXcJrB9eqJbP9OL/5q8c8U2T7E/4WnN8J2df+iTtQKc3If7KyGyf7ONNbqUOoXQbFtoBaJKZc3hJ33igF89sOPVOcNzhoYGQpqWy/V6RUul8qCZHqGqS4CFMcZTVHvO2a0t67asLvC41cTUiuIxn+qc+w/ghc65nUTkd3mej7dardhqtbLJZlMzHXv41c/Z8MqbL/nMIrHG9EqCr1TEVVOjVibUM0g89wFTx14viWHHkLxz9du3h5Hf3h8A57x++i9jrLzV+ZZgzACe6+B3HqYhaIw4J+JMkbwIQyvuvHryZS9//i6tXB9YuSFMC0Xs5W2KoeVElgCY2dFmttjMfk7v8M1g//qWds1sm56XKRAfc20N8J/0gvgvBO4piuKOZrMZx8YmmZycYPPmUXYb3LT+pXtu+ptrf/iJEbHujEpKUstEaplRKxNrKaTCCoR3AB6RJTGfPekle/s5Px3bbuzhjx64bn9tx92yrPigS8b+AcgQNqP8ohuZ28zxrQ7SynHNDtIulE5wxaEn/PP6WJnDT24faJfSSlKtlsvV6oCVKxmlXgCpDOypqrur6m/NbF2Mcb6qvlh7Z+keJbZbr3+dc8E5d4OILPXe7ygiR3rvV5jZgzHGVp7ntNtt2p3cmq2m73ZbjeOfN1nz3TXx1h+fv2PJaVYtOStnSK2C1lJirUxeSlmPcQwwo7e9b+gifPlf5e1rthm+fdIAAoRvzXytSHcv51rn959aCdzcDcxsdPCtHNfuIO0ca+UaWwEGZu+16pDj3vc3Ny1Nfr1oXW1WuVz2pSyxUqki5XK2dSQuU9VhM9vFzKap6gozW2NmDVUt+qcvMxGpi8iOwK5JkkyIyEOqOm5mRVC1kOd0Ojndbod2u0On0427zypWHLlv53X3XfWD69ctv3V+NcPXMmeVBKplQqWEq5eQUsZalBfSOw2PhuqHTct3J+8e+dUfw+bJZ+345vR3kbTrkE8dR1iF8LsisGujQ2xFrNUmtrvUmwXdboHP1RUvPOmfNyYDO02//BbuGu9k8yqlMlk5w7uEUlYy55A0TcQliTkRw0xxHouxd2Cot3PT+kcbxEzEQIIG0SISo1qet6UoAt08aLvb1uGyrj/hMD0gTK7ZeP2PPj+zlGhazoiVBK1lZJUSRb2CVlMk9ayldzJ+DwBi9gG01pV3b3xS2Yz+pLwx8RvDn3RZdwzrgyhsxvhJMPZq52izLVmzwFq5SbdLaEeNndyZlOutw97wwU5anz39ZzeHO9dPMCvxaZp4j08z0iQlSz1Kz/XhxHdxRFR7ESvnPIqPUTPV6LwX63aDKLEXEy6ChVDkc4Z15G8Pyw4qJtZvvOm/v1ixTqNaTpRSySXVFF/NROslpJwY1RKWeBZjnIAw1BtP9gGKclXevfmzTxaTPzlzUfzawBnOhwSXn9dPDpZj9i1DDmp2oR0Iza5qJzhrFfhuR5NOQdENTpPKwNiBx5w6MXOXfY9YtiZcef097e7IhE2XLMlSvDjn8IngncfMgllvQ7JzIoqkGsRUC4kWpCiCodadPuw2vXi/cmXBTslRIyvvvfauX/37YNGdnFbNVDJHUi67WMmIlVSpJs5XMqRWwouzuzB5J5BhqMbkDGflivzD+JMG788CECB8tfZO8cVOzsd/4pF8pz9CGOjm1DoB1+yStgq1dk6RF851g/puTtE10iLSGNpx95EDjjrFTZu928taOXcvebizfMWabvfhTV3f6UI3qqC9o6WC0ywVyiXYeUYp7rZTWpq/c3WPWsYBm9c9ePU9v7lEx9Y/uEOaUMs8oZSQVTJXZF6tZ22dK2eESkYsJf2NU0I/LwKjhPSLEb8i+YfmD/5ULP7s7G321cphEXuv98XeiL2gf3kxwu8N269VENtdF9sFaadQ6/aSDaXdSMgLfKFoEUhiJCcpjw7OnDu+497P1xk77pqV6tNr5drAoE/THUSchLy7odOcnOg2Rpub1q3M1y26zU1sXD1E6ExPElLv0MzjspRQ8iSlhKKSkpRTJ6WUolZSygmZiNyH8VKmMs2Z3BhDtlyRf83e17r1z8HhKaW/m/jywIy65N+XJP4B9JGljsiPMTSiO3cCRTt32im01A3keeF8oap57nxhWsRA0lEwJQQlMcNCz502ldqk109BEwERJHEEcSQlB6knJN6lmdOYlpzLnMZKQpZlrltJ1ZVLpB63CiPF7PgtfTT3KYv+b0TKb5F/HN/852Lw1BMwXobX9dmZmJrL9K3Agv5fOSr/jmdGVJ2bF3Q76nxRqHZy52LUmCsuj06jqu8qgjmiEtTUmEqF0vumgDlx4h2Jd2qJgHcuJk592ROT1PlSopomzpW9xiwl896tQumAvZlH0gcs1sJd4oTAxnCenP3Udko8bTlU7SvMN/NfFW9XAJ9iKmVJL/vkDzEM0V0Ldb5bqObRuagaikBWmLMQCKAuGAFDowKO3gpASbwDBJcICeI08SSJU8kceZKSpGCl1EnqNWJuBYLD7C1bsmBCC+MTMcqrfIjvlQ+y/OkY99ObhNaQ+K8ch+OdPnG3KXwco7xVY/eqyY3iqRk6xyJZUFeEqC4oFOYExaKh9JzaPSMiGOLECw6HpKKWeMyLI/XqxVMIbq1Fmk7scIP9txphxymfiUGfD3zL/3/84ulMifzMpEH+Bmls8yaMU8y535rnNOnP8rdqeA3I1eKsbSolB4NRdFgMb0Y0wcR64mXSSzggggeCw40rTIizrqlUwF5msNOj+gArPVykhR6t8P27q1x2yHt42vcdP6OZzO1sXBjmKODDqvxeEjfN4ANP0JlRRG43GBdljN5OWDCrmWNYYAizgw2mP0EdXzJ0s1NeivDZZDNXP1U990T0rOXSty8wsyuchXMrTLjgmWhDjDNQ3a1kfEr+iY3PRBuPa/PZaGSKDKR9AZc47x6KulUuwqelbrnIqe5c+SdOFJ4+HffHaJse6WeKBKwyyVtDoQca/Eb7qbSfarHItVbovpUB3vxsggfPMoAAcjahW+KNVlhDlZVPGcDIWjFbF5U3yTNgJP7oeJ7tBqdo9F84wMOpivwjsmWS+6dSMLUvpsJ3Bz7CdpMkPpP0FwMQYPyznByUHXFy4Z9VgdrpzjE+7aN8+2nu2pOmvyiAAJvO44tmboNh5/1pT8qnPTpj+se3nRjx2aK/OIBmyKbP8FMzN6bY257MM+LkMjGtzQy89pmc4z0ZetaNyGNJBGtv4k2gczVy+5MwHHcRdVoROOkvDR78FQAIMO+LtIm8zYndr8bodqcrRkuwG73jXTudTeuP1/zM019chLemtWdzpCkvir2EZI8jBx9xCTfNOYvfbev/vwT9VXDgFM05m2sMgiinP477lNMd6F8TePBXxoHQW+6tOYsfFsrDpnyof/GiJGHmzp/mrc/2SuOP0bN7CvpJkICZ4+2rjF8G5RcmDDrHvjt7Xv3XBh78lYnwFMnZhOg5SRxF6hmhyUlyNs/o2dj/X9LKT7D/yo+y31+6H09E/w/wHJVcjfUH5AAAAABJRU5ErkJggg==\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHiczZ15vF1Fle9/a1Xtvc98780cEjJCAgkgiUwiIghCgqLIQyK2qI0D2g6PlmfbbaONCmo/habB4dF+tBX0KdC2PFGZB4GEgECYIQmZp5vc+Yx7qFrr/XHODSEkiDKuz6c+Z9+z9zlV9T2ratWwal3C6yh64YW8Y8Ex4431M4yhOQAtVMUBTOgRQRGEWvtBlJnRgGJABSthdIWHrIyI1l9y3339F154obxedaDXOsPGr2+anFL2TgXOJKZWEIYP5oslL0z7EtE8Zj4M0O49f5qGofKAF32GRTe1GnWTZOkR5DUi0muC0Nxaete7el/L+rwmALdde+34nOcPgei0ILK/j4rlLmbztyBMfkUyUGwT8f+ZNGojWeLeBchvjOR+Xvngqf2vyPe/iLxqABWg/p/+YqFR+WYQREsLXeUuMH8WQPhq5dmRVMRfUR8eqquTt7Din7rOOXsFAfpqZPaqABy88spjlPhfi8XytSbKfZxAB+3l0ZSZ7hXD6wkWCAggClURGWJSggUAUjivokRIoJpq6gkkyl5miOJYqNq9VO6xer3+UxfHp4P9l8Z+8pPLXum6vqIAhy+/fLYXXFksdd1go9wXQZjyggyJH1HDDyEIQ1iawMZAjAWTsWS55QHHbEREGQwPABAYZhLxzjDBIpOcQBx7BwhUvOtDkiXk/WGqcugeirbJxc1LGvXaqY5x7sTPf37NK1XnVwTgg1deGcwcHvkWOKpXuiqLiPjI3XIZJBv91lub59CORWBVAysmDKyQ8QgsQGSNsakaUhAYIAPqlE+hgHpRVeMB730IFcfOK8RbSVPHTghZCsn8IJI0hk/fA8WYXYuhovfVa8O3eudy69eWLzjsP87NXm7dXzbA6oXfnJNBflwaM+4uJrpgt6/fTlFwC+dyY7w1Fvk8KAwYHChHRsQGAVvrEBgSsGE2DtYATAwFE4MBQAUCgkBU4D3EewtRD3WK1FmkzsE7gstI00zQaoEynyFNNkucLCZg+q6lUpVLRoYGj1KWv53wla+sfjn1f1kA+750wblBYGcXunpOJcUBu3zrsIbBryhfnGaiyEghIoSRahiAcgEjyCkCqxwEFsY4BCHBEAnIIzAEsGEGRDVgYgXUiQAMcfAeEDXwqshSFe8t0syxcyRZTIgz0TSDacYkaapoNb2kySaK07MAVEaLqIRnGkNDv3ferR7/rxdd+ZoC1Asv5L6R+k9yudJTuXz+YgA7O3EOwquQjyoo5POI8oRCBC7m1YcRKIgIUUgcRSqhVUSRARtvrGEPkiRuVBsDw83B3m3Ot2KqDQ8TAJR7utXkcjpm4qSgOK4nn88XK1BlOC8iziBOPWeOkCTkk0SROqU0Jm00QEkKtFqqzVZLG406vHxol6q4pNn6XBw3jh23Zf3ZdN11/lUHuPpzn4t6Mr2h3DX2T8T85eeoYjuKuT9QobwPFfPQYp4oX4Tmc0AxD+RyanIBxIZk8hE8sx3cuLnv4ZtubD774MOz01bzQI4KjwTdXRt57NhhDYO00FXOAKA5UgsozUIZGOjOhkemSdI8NMwXnt7vsIVrDl20uDhu2tRxRtT5OCZOUvFJqhTHhFYMbbSIkpai3oSvN0Ct1lY0mqeAMHFn0UUuqo0MHDkU0Kn7X3FF8qoBXL34c1HXxNYNlZ5xawj0qZ03DN3I5UqMYqFgSiX4UhEo5IkKBUUhDxTy4FweEobF4R19g7f+6D8a29etPz6cPPGWniOOHM5Nn1qKyuUKk+UgCMSwigoLMwQARNr9ocucEUC9TzWu1WqNDRvrw8sf6HHbd7xz0uxZd5z0iY+VK+Mm9HCa1aXZJG01iZJYfbVJHDfV1BvwtQbQbDSlVsvB6+JdQHxvZLDvwDq5d8/86U/jVxygfu5zUV//4I1dXeN7QXTWzvdN+GNTKU5DpSJUKYKLRaBShi8UCaWCUr4Ijuy4NY8/9syNP/rPg6mYf3bSu9+zpTJj3/FhaG0YRWRsiCC0yFkLgAWgzFgSMkYBQL0n74iZJRDxnDiHLM3Ue4eklVCaZVlt3fr+3j/8YYrWG7NOOfeTT86cP+8AybIdaLRI63Uy9ZZKowodqZOv10G1WiLVxiD77CO7VPPqkaG+aSMjAyfvf+ONL0kTXxLAa9//fvN2p7+rlMdsYDbn7oQXhj+kSnkmd5cJlS5QpSwoly2KBUGpRFTIj92+YcP663/4g/2CSZMfnnrG6T6sdHUXopByuYKGoaEwDBGEIaIwJGOMhGHovKfEGPVBEGUAkGVJoEqGCGGaJoH3npPUiXcpxXGKJEmRpC3EcapxbWR463X/bZLebQvf95nPrpm4777TpNUYQD1WrtWdr9dCDI8IqjVItcpUra3WNPvMzjqpfH9kZOCACQGd/FL6xD8LUAHaccq7flEuj9/ARP+480YuvIzK3fO4uyzo6iLT1QXpKkMrZZhSyWTQiddd/r1n62lMsz/+sb6ouzKmmC+gWCxImMtTFAQmn89REEQuzIXKABtjhIgUAAPIOgkAAnQMlarCe8+i6tMkYeecbSapxs2WZGmszWaCRquOWt/g0Iaf/WxiJcxl7//8Z+ayoldrdaFaXTEyDD9cI1NtiBseUNtobHDN+FPP1Vkuqg4Pzph40w1nv2yAW4874dxKqZyzJnfZ6Hucz31fyuX9TM9YoKdC6KkIlbuYuyuiheLEbZu3rL3h5z87aNJp77lp7PyDpuZLZZTyEQqFvBaLRQRRzufzeVimoANsAMB2AFsAbAbQByDZDWAEYAKAKQD2ATAZwBhVhXMuSZ3nZrNhm/U6teIUraSl1ZGGDj3xWO/263+76D0f/chjEydPmaWN+nYdqTKGa0B1WGRomDA8QjpYfRZZ/HejdXRZfF6tWfeT77rte381wIHD3zpfQ/s3xfKYL0GVAQA2+iF3lWfpuDFK3V2Erh6Ysd2qlRJToTD3jzffcseza9dMm/s/P7clX6wUKpU8R/mCVkpFLRSKEgTWWmstgEEATwNYC6Cxh3IJgKM6fy9HWyu1c4861wUAswDMA9DtvXdpmvp6vW6arVQazRparRZqQ0Mjq//9+/vvt/9+a4898Z3v0Li1CsNV9cNVz4ODVoeqIsNDHsNDG5G5tiaqukZ96JLM61WT77/nqb8Y4JPz54djw8LN3eVxUxS6PwCA7a+op9JF3V3MEyYQuiqCMV2M7m4gCg/6f7/573uauUJj9t+eXSiXyzaXK3JXuaiFQh6FQsExcxnAJgDPoK1xhHbTpA6g7g6UgwAcDmB2pzhrANwP4KkO7CoA34HoOq9jOp/b13ufJEmi9XpDq9Um4rhOtVrVr/np1Y1CKy68532nH0NZ+gQGhtQPjxCGhlX7Bw2GB70OD4/A65JOvqtGagPbJrK8kx56aI/TPrM3gF8ZO/HfugpdG9W5U+EcyPmnqFioarEYULkCKhWEymVGpUxgc9A11123ws7cb+P+HzprXHdPt+0qV1y5VOCuroqGYRgZYzIADwJ4Fu2mWQHQIyIREUUAciJCRJSo6qeI6NsA7gZwO4ClqvppIrqpU7xIRPJElAfQg/YSWQJgG4AhZh5rrc1HUZgGATMRGRuGKM87yA5v2Tz02N33NOfPPeBQUfSyS1nTDEidauqIUie+Uffk3AQ4NzYy9prBZv307/bt+N1LBrh1zpwDQoTTAzIXtKdO4jSMbuJysULlippKCShXiIolImsPuf6m3y+LDpy7ZeYZp0+qlMvc091FpVJJy+UyBUFQYeanADwCIBWRkIjGiMg+qjqWmaep6nxVPY2IjlLVJ4joMACnAzihkxYR0TCAR1T1M6r6FiIqEVFFRHKqWlFVUlUhohjAWiLKjDEzoyhyxhhlMrCWbGn//UrVoaG1Ty9dGs+fvf+bSWgL0tSqy0BxBmSppSx7FnE8H84zvBzjnLvhf47r2XZpf//AnwWoAFXHTfivclSeR0RTAYA4uJIqpamolJkrJUi5ApTLxLlw9u0P3HdPOnnS0KwzzpjQVSlTsVhAuVxGoVAoMHMOwOMiMkBEkYgAABMRqSqJSAqgn5mfVtVFALYC+Bu0jchVAG4AcAeAJ1R1tqq+T1WfJaJ9mPknABqq6gDEABLvPYjIEFFJRFJVbTDz5CAIDDO1mNmoshRnzOgeWr9+oPfZZ9fOnDr1MFHpJ+cUzqv6lJH5IpL0VqgcAQChCcstFy+6ZKD/Z1/7cwA/PWfeCcWwMMhsP94mqiu4XAJVykzlElG5i6irCCrku9f2bnnk2aGB8rxzz43KlQrKpSKVyxUtFAqWiLyIPKWqCYBJqlpBu5/qEZGJzDybiN6jqkd77+8hoveq6nYi+g4RBar6QQDvBPAOAAuY+W4APyKiBao6A8AtqvpFAAtFBERUZOYKgPGqWlTVHiIa6qRyEAR5a61T9T4IIi4fOCdcd+/SfJGwprvctQ9EEnZi1XmFc6Qu60Ka1gGaDMI+AfFlQ2PG5i7p37F+V168u/YJ9KvWRl/a+UCY+xNyUai5HFMxUuSNIoyQthK6f+XKhQed9/fVUqlIpWKBS6WSz+dzQfurcC+AQe99WVXzqtqtqhNVNSKizap6u4iUVLVFRBc5574IYI6IXOK9f5+qiog8IyLPqKp4798nIpeKyH6qer6IfNN73xCRCjPfCmCt954BdAHoIiIjIkUR6RORZUSURVFki8VykM9HWiyW+KC//3zz/pXPLEySRCm0RiIryAVKuZyafJ45Ch4Y5WBN9EURf7HuZnifp4F/N3f+WwthfoTJvL/9Dt2ihahiKmVoscDIFxWFAlEQzvvtI38anvPpTy/vmjC+p1AqoburolEUBUQ01Tl3D9rW1DNzQ1VrzJyo6j4AFgFYSERLVfXdAC4HsICI3gvgSWPM94joZhFZq6pxpznfraq/Nsb8UUT2AfBhZh4G8FMAxxPRLar6BQBv8t73qeomADuYuQmARKSMdvew0FrTMIYVRAAQFvaf89T9N/x26gGTpkwn7/uQesCnUPHkY99NLlkNYH8AFct6SX+lB5cO9m/eCXZXgF79N4wJp47uvpgo2CxhYYoEgSIMlYKANLB2Ze+W28v7z0m7pk6elM/nUCzkuT20w1Tv/UYAbxWRhqqOrnhscs6tIqJbARyvqomq/j0R3QbgAiK6Q1VvJqKzvPcXqT5//2f0b+89ADxijPmi9/5dAL5MRLc4584H4IkoJKKbiWiMqh4gIlNUNWDmfhExxpgtzDwlDMMtBRHyzvvuqVPGl/af9ejqbVvs3DFjp0kQOAojFROQKQQqLtymSdrmwflPESenAjjxBQDXzZ8/iV1wO4CLAEAJd4kNpyCygAmI2KgGBkjdlCd3bJ+54NOfWFbI5xFFEedzOW+M6QFwFxFtUdXAew9jTMV7fygzv5uZ6977bxNRwXt/gTHmUlV1InIBM38VwDs6oG4RkbuJqLkbxAIRHUdEJzrnDgUQM/OXReQTAKZ0NPBSImIAnwJQAnAnET3mnBs0xkBVM2aeYK09wlo7kM/nDaA44MNnRw9dePFb9ytVMmbaAGtgQqPehIAJ9oFmfwTp20E4gNn+cu3MgybOWvfE9ucBDBJ5X7EY7dynZWOfhjEzjbWkAUOJiRRmxY5t901bfHItly9MyefzUiwW1Vo7SVV3OOcWoz2wtcaYDQBWiMidRHSqqm4jog+LyBZm/i4RnSci3yaii1T1Hmb+tff+QACnEdFJ2E2ICKq6XVWvVNWVzPw/vPff7IA8T1UvFZFNxpgPi8gIgAnOuduDIOgGcKyq7uu9Z+993Vq7PYqifYhou4jXLPOFKSce9/tH7vtTeUHPuGlK7MFWOTAiQQAT8FPe+bcDQCksjvVu5HQAPwQ6RkQB8mtXTwf47e3GQn1gM1WtgbckwqywRN6l4zYn8dsnvfWt46PQahAEFIYhq2rivV8G4Ceq+v9UdbWInOyc+4S1FqoKVf2Bqh6qqr8RkW3e+8tU9XYi+rKqHui9vwzAuWhb6UcA/MY5d7lz7nIAv0F7HNkD4FwiulRV91fVL3vv7/TeXyYiW4jotyJyqKr+H1UFM5Nz7hNEtNg5t0lE7iKiX6vqnUQUW2tNEIQU5iLd9x3vGLclTY7zWdoDqLbrzGBr4GH3VcWAAgDTcbJ29b6jP6xBW99nGJOfEYy0ijSmO1TCLRREPchFZIKANQyFQqPPpunq8C1HPjl+3oHlKMpRsVgQZj5AVR/z3p/V0egBVV0JYDEzbxORfQGMVdXDAPyaiD6JtrH4oYh8TFVPQnscdzkz/5f3fnVnnDiLiBYR0WGqKqq6XkRuNcb8ynv/eGew/W4imi4iXyGiyQBOZ+afiMhH2nqBsdQ2FhONMVeKSAzgMBF5LxGtYOZDiagPquS8mrReWxFv3jw4hrlIaQpJM1LvGN4ZdemDrDRG1qx9VLys+TvJNl8OjFgASGBO6Db58ar+fbxu3dUye78WDBMRVNgAAguHYJ1Lj15w8onLC4UchWGo1tqIiLap6urO2OwMVd3CzFd0xmY/VtVvA/gCgEtFZDERfUlVv+WcewuAW4wxfxCRt6vqlzpGAp0BN9BeUACAQzoJncEyVPW/jTF3O+feTUTfVFUB8CXn3BeIaKL3/nxmvkRV/5GIvq2qARF9QlWnA1gqImuMMbOCIAicc1k+r5h16ruKDz348JGzDe0gIAUzQExEVmBMitVr7obXsyObW5e6+O2Av5oAYC3s78YVxhypinEA1blS/i3PmlnWUtFSPgcuFCjOBc3l3RXz5i9/MQnDkHO5HIVheLCI3P6Nb3xDly9ffg4zR7v3XW8kEZHklFNOuf9jH/vY74IgKFhrJwE40Tn3aJIk2opjevDib+WPGm6lhTTNS6OpiJvQetPrug01P1x9L0ELRNgx0By4byb8aVYBXg+qimIcACj0FsTJOOzojbk40wMSiM90HezwtFNOrhtjJodhKNZaUtVCkiSz7r///hOZmc4888w/rl69etWcOXPmPP300ysPPPDAuc1ms56maQag55ZbbjnOWrvitNNOG2Fuj+H7+vr6+/v7ByZOnDh+zJgxY6655pr5ItKzZMmSezvN73lCRHj00Ucbq1ateteYMWP+dMIJJzQ6cPSZZ55Zvd9++80IwzAcHBwcuPnmm0+w1naXy+XbFy1aZP7whz/ozTfffHylUnnmrLPOeluWZZuCIMgZY5SIyDBj2knvfHbDdb8pz3U+r+otvCayfYeXZnOcQm8BcJoqJgCcKDzbTcCknA0fBnAWAKjyDiWUtZmEfv2mxM6a2VAbdG8L7SGHHzTv0SAIgPZofJyqPtJsNt9sjDFZlsVnnnnmMara6PR3C4noJ6p6HoCHmfmmW2+99bh3v/vdI2ecccYQgGUAzgZw3C7jvjU33HDDxlqtFpx55pk3ABhdUg+89wUAMwGcsHjx4uI555yDM844Izn++OMLAK7z3h9rjPlkB/JFInLm0qVL++I47r7iiisOyOVy5dNPP33pOeecs/K66657z5IlS54mopNV9XcAxllr+7xX2ufNC4sPXH/DvLnS6JPEx1i/UaXZKrR/S9422qtYGz603mUT2AP7Rhxyu89VcOADYnWqHuLSfLpxY5dWa9vZmO6wUCiTseC2+swTkY2jlSciiMjFqvouEbkEQE5VP6mq56vqId77LwPAtm3btqnqqar6HRGZB+BqEfmCiFykqqvCMDRERM65S5xzl3XSd1T1a6p6uqo+2Gw2vwcAvb29G0XkMFX9DjOfDOA7AH6mqhcQ0aw0TT0AhGHYA+A7RLT4Ax/4wB1ENHnDhg23qSpEZDOAedZaMobI5HIltcF4DNd28IZ1OfGuCCiUycP4YJRTyCEE2Jc9zFwY3rf9NqDe9DjhQAUW3oNcipH+HcmEgw/5PhEnDIWIKIC6qtZ362POArCUiP5FRL6lqjVVvURVfy4iPwKAKVOmTAbwsHPuCwB+5Jx7P4BLAVwgIouZmbQtN4jIr0TkV97721T1MREpiMj7SqXS5wGgp6dnkohc2fkBnhKRL3Ys8JUicl0QBKYzhLpMVb8BYOlJJ530NhHxt9xyyzGde0NEVG3rAKVgbk6cP/97IwPb1WWO4T2p91aFAxUaM8rJGp4CmDkWoDcTuON+RqmSlgyJI/Uq3nhKM9SisKX5cJYxrNbamjFmnKpuc869s6+v72cA/rGjhXMAbPbe30ZE/wLgHhG5jog+O6qp69atWy8i7ySiSzuWelhVrxeRx4wxSaPR+EC7K9GZqlrsXO9Q1Q1EdIeIbKnVagDwHVUlAJ9WVRDRJufcBcaYA7335xIRvPerOv3ol9FeFgPaq9a9jz766CTv/XcALGLm7QBCIhokIOJibkY1CJs5X/fshYyqcyQGRBEpUkBDEM0jaGYB7KekC6EEgj7JAOC9aHvnQSAW/cVSuO/cuYn3PmLmnDEm7UzDFlQqlb4OvGDZsmXfO+yww86x1m7z3v8LM39NVd/mvf9+54f77OzZs2eoatF7/x1rba+IHAngFAAf8N6jWCw+2mq1SER29SmcucvQBsVisQ4AW7du3UxE56vqJBE5tzOrEefcZStXruQ0TT/IzGi1Wl/P5/N/Q0SzmfkrY8eOPaO3t3ccgGNEZIGq3sXMKRH1QFXGzp2bbSyWypPTXqgXdd6BvRdVZEr6FBSHMnCYgBMLIA+lCgAo6RZRhFAFeQ8IQSwQR2FPYZ9JW4MgSK21LCJzRaRJRB81xnwNAIwx5oorrvhspynPEpF/BlC11jaiKDoZnd73+uuvn7Fp06bHrLVHhGGo3d3dEJGHsizbGsfxjoGBgTMBFK6//vqrjTFBtVottVqtuNFooL+/X7Msc0NDQ1MBvO3WW289aenSpUfEcVzy3ouIxEQUGmPOA4BRS++c+6SIVIwx53nvL91vv/3uXb58+SwAE4jooyJyjqrOtNauIiJXnLoPJ8Woy2cZwYuyqIoqoEgFWMvAoarUDaBgAfCo96sqWiA15L1CmMApICDJh/l8qcTGGFFVj/aUaqRYLP7L2rVrf9xoNL5eLBZ3aggzEzOHaO9VVJLkuU1+7/2MBx54YAb2Isa0V9h+85vf/Pk9WaKJrVZr4iisUWC7y9q1a389b968aQAuA/CMtfapOI4XeO8vLpfLX4vjeA3aa4gCQE0QqURRDmlKgEK8AAqCihK0iueGV8yA2tGOkYAUIIEoqfckzpPLHBAEFY4iEhFnjPGqOhbt3bGZ++6778l9fX3YsmUL6vX6zqWnN5JYa49g5lNV9d9E5FvHHnss+vr6cpVK5X0AphNRA8BY770AEJsLSYytqMtUUwd4bbMDRBWNUV6AWn6e87WSV9KmGtRhUGdwPYCpE1srzmVE1ErTNFHVLd77AQAwxswEgCzL0N/fj82bN2NwcBBZ9rKdP18xWbVq1ZPe+49nWXam9/4/DzzwwGcBpNu2bTMA0FmE3UpELWNM3RiTsA1CgOpgqpNBnRk1z9QU2J28aFT7dr5BmldFyQIqIPKq5OFh4evqfaiq+c48dC6AjXsqrPce1WoV1WoVYRiiVCqhUCigs+D6mkmWZWg2m2g0Grjqqqui7du3Hzt58uTbJk6cuHb27NnvJ6Kt995779NLlix5E4CJqjqHmR8CYLMkSQP4qgrKBJAwhKBgJVZyFjs9jwEreE4FhajIopRAGSAGkRG1CJwfcnEqlMtZIhIiqhNRcc9Ff07SNMXg4CAGBwcRBAHy7QVYRFH0igN1ziFJEsRxjDiOn9cCduzYcdbVV1/9gs/84Ac/WLFkyRKoahlA3XtPzjmbtVrMiWs66BgQiIWcQpVZQwJ17eQFwBLQC9UNIEyHoqJGYwhIIO21QvbepGktHhpyQXeFOyvNDeCFHvi7yBAzb+zkwSIyLcuynl0rZa1FEASw1iIMQ1hrwcyw1oKInmcQRGR0TREiAuccsiyDcw7OOaRpOrrcvyfZxMyD1PbBgfd+Ptq+NhgYGDgJAIhoHwB1dFQrHhlxuSyteUg3g4xv93VKYFJomaCA0hoCtliBPi4EQ8B0IZ1LgscZUKfwgIoQo7S9v9nYtDGMpkwxYWhtZ2B7SCfT5wkRPU5ErqenZ924ceNkx44dPDIyAgCbReTg0edGK/8qihhj/qSqZuLEiWvL5TJv3ry5GMexV9UxqjoTQNrb27u8VCq9mYgeFxEbxzE11m8Mcn39Dc8QFaiFAkSkKoZID1YlgOQhQB9lBT2tKqsAgBTjhaTmSb0C6kDGiQa8dWuhsWZ9TtVb55wBMKiqxjl3LwCUSqXfjsJj5vqiRYsmBEFwOjOfEUXR6SeeeOJYImow85OvJrFdxRhzfxRFQ0cfffR8a+2ZQRCc0dPTs3jx4sVpZ+q2paen596VK1c+pqoBgMHMeyYiaqxdVwy2bCtnQoEA5Nu+TJ6gI6o0BgC86rMCeoYZfnXm0+0Ytc3CDXiygASqYK9MNFwrNNavnZ2mKURERaQOAEmSbN1t2GIXL148pq+vb8p3v/tdiAi+/e1vo6+vb+qJJ57YjfbK81/syP1XyAZVjRYsWHDc2LFj81//+tcxffp0nHfeeXjyySffsmDBgg3MvKNer49LkiTqdA11FYFzHsn6TbN4qJYnBYmCFQjEU6BKzVFO4tMdgF9rCVib+Oy00OQAAMTUVFEVpUyhUKhFko5F4g5Mk+QOIgqIKGVmOOemX3vttXfW6/XjrLX3EpE89dRTxy5cuBDLly/HvHnz8MADD2D69Ol45JFHDmDmewE8rKrjXk16RDQwadKktRs2bFgYRRFWrVqFSqWCe+65B4cffjhardaihx9+eG2WZVOMMRYARCTN0hQuzRLN0nk+i58ASAOwCOANgcHaVGlb4KbPxjtgjd0fqK4Uf/ROWyxqASYlsVBWBbwCCPv778x6dwzQPvtMMsZoEASbVfVtRx111E3PPPPMzQBwyCGHFK666iocwXXn3QAAEZlJREFUe+yxePbZZ1GtVjF16lQcc8wxWLp0KRYtWrT60Ucf3Wl+X6lB954WXk866aSJy5Ytwwc/+EHcfPPNGBkZwTve8Q40Gg0sXbo0nD9//vJWq8UzZsw4RUQ2dgyVtjZtGgj6+m9X8EQAQlBWKBFYSBBqh1Mq/m3zgQvabrNAnwIPEHAEgBMIspJUIKTkARaFMQ89OlJbvjw/5vTT1DlHxpgnACyaNm1a38UXX/wxEYGI4IEHHsD111+PJUuWgJlxyCGH4Oqrr8acOXPwmc985m9Hnxu1qLta11Ggu7+OAtr1lZlBRM+7Hp3OjVrwG2+8Eddccw3OO+883HXXXRgYGMD999+PBQsW4Oyzz/5okiT/KSLjVPVGL6KNRoOaD64o24cfq3nIFEOkAlIDdYAnAb+jw+sBAtYBnV25v4OphWyHmPidALqgtEIIxbbnIkOg5KrDJT3ggLebgw9ew4aNtTYlonne+2IQBN2qyiKCOXPm4Je//CVEBKtWrUJvby9WrFiB888/H8Vi8Xnw9gZy1793T3vT3t21kIgwefJk3HzzzajX69iwYQN27NiB7du344tf/CKstS5JkhKAsSKyIm61qNVspcnd95yst92RWMAYYjEABYCxsIMKfQsAOPGXpz77w/cg6xkAArj7W65VHs1cGNsZpBYMQKAgiFIevX23tTas64vjmFqtFlT1QQBzkyT5xWgFp0yZgiuuuAJRFIGIEEURLrnkEkyePPkFkF4see+fl17KZ3aHfdRRR+GrX/0qms0m0jTF3LlzcfnllyMMQ2RZ9gtVnauqf0rTVFutBK11m/pNb98tqhIqoKTC3LbAqup3jPJpuVaXh/sTsIun0dMwf+zJd+0PxWRVPA7CjhSglgIJhDxAaSm/OffZT0+17z+9t1AscqlYZCI6Q1WfiaJoWmfF+AUV/nMAdtfCF2vCL9Zsd0/GmL39PRTH8QARzfYi1zXqdbRaseh//fe41uXf326ayZQA0BwMAlLJA8SKsUp4EwhbBlsjz86DPw7Yxb1NgEuc91e0C4qDFboDECK0zY4KyNdbU2iknqT9/X1JHEur1VIRuU9VD0iS5Jo9NbndwewN3p408M/9AHv7vr3lPZriOL5BVfcDsCxNErRaLcRbt/XpSD1zzXiSgtgAyvAwbSPSq4Q3AYB6fzmA/z3KbSfAEP7melqfNrppQqKtAIAhUAiAWD1BbO2XvxyJ7l52QL1eR6vVEieySVWr3vszvff37K3v2luF9tZ0X+z6xZrti/Wfqoosy2713p9FRMNpmm6u1eucpqkU7n/wwPov/m+DAGsgQgAZAhkoWDQZ5TKY1ueuh7/9BQD3BxLfNtHXdHxAPgyiEduZzzJAEFI3PDxO+3aMYP3GvlqtybWRKrz3d6hqMcuyblUdGm1+e6vYi/V7f8n13mDuCWKnTL1pmk4CEKRpeme90aBGKxa/ZsN237d9OKnWxrQdKEm4c9qbCcPKdLa2rcEvAG2c0nZofz7AdjOWC0fSxuiZCMuCHQYg204IIEoA9f7kqlLlqZXvTONGrd5ool6vp9r2OD04juO7te3L8qKa8FIMyksxIC81H1WNW63WU0R0sIjcFsdxVq/VEdfr9a6nn17c95OfFRhCFkohFBZqLKAQ7kfHi62aNtcmkG/syux5AA8GtntxBwB0BwAo0YcD5WFDpAYEA4YBQzPXvf26/75vzP0PlhuNmtZqDWo2m4MAHgLw3iRJfrWrEdj19aU06d0t8K4gX8p37CnvJEl+h7YP4oOtOB6oVqtaq9do3EMrituvuW4ZMt8TgikAwxBgicDKwyD9CAAocIsTN3th22N2zwABIIb8r6G0vqzd4jUnLD4E1HS2OgJAQ6KkuXrlfn79Rp9fs663Xq9ptVbzjUZjjao+AuCDzrlfAHhFNPFlal6aJMkvVfUMAA83m821tWrVjNTqlHt27WbevJmba9bMMgRvALWAsBIFgIA1BRAqgKG08TBBzt+d1wsALgS2irhAIT8CFFCcYYFVAUHzgIYAWRKyUF37o590j9u89ah0247eWrXKw8MjaLZaq1V1uYj8TZZlN6jqyN60cW9a+WLjwJeibaOvALamaXoPgLMUWN5KkjXDw8M0ODSk0rejf0LvjiPX/OA/ihaAhUoHIIekxJCVpLqkzUB+KOLsfOAFUZH2eNDmQ9ClcG5J3uZmKBBCKWcNDQsQCEQ8wSiIRMT0Llvef9CCQxZsjIKnHbikIgKgGQTBJlU9xTm3GsBqANN2b3Z7et2TMdh1/Lf7GHBPY0IigjHmHu89EdERAG6O47hvcGjI1OtNifsHBg5cv/HYJy765gbrfaVAxCGBilDNEWyOTaxCbwJhHIGqQ0ltex/kcz9re9/+eYA/BtynYaqG6HHDZhEIE6D6EClXAHgHYgAsBPLqo833P7Bm4cELjt1A9GhGWnaqEOdbzPS0MWauiExX1Z9re+Qf7KkP2xO43QHuCmhP0Dpz4CERuVZVTyYicc7dPlStJrWRmtZrVY37h/oXbu094bFvfXuFSZOxEZGNFDYCfA6MHDGp6nYiHA8Aqc++lIn//RGQPUb32OtZuR9C1nxc/GcjGz5JoIMBmk+svyPQBAACUqiSVVJIlhW3LLt/5eGHLzx8o8NTKUkh88555wNAN1pr+1T1VBFZx8w3icjB2j6a9bwmt2uzHJU9QdqLBgoz/1xVJxDRUQD+2Izj1fVaLalXq+FwraZZ//DWI/r7j3/4m998gprNSTkgyIE4YmgBLDkiWKUniXAOAAjwi2ra2Ocg+Ev3xmmvAAHgH6A31lz6oZzJEYCxUD6YCHczoUeU2DMMQCBSylzWtfHOu2tHHnXk9DjLHu/1bqzKzj4sZrarARUROUnbHq2/Q9tFrmtXiKPXe1p5GZ2K7QZvC4D/ApADcDSAJ0RkRa1Wy2rVBtXqVQwODMvkoZGtbxoZPuyBr3ytj5JkfJ5gQmWTJ0IR7AsEWKb1qvhIh8uq4WSkVYJ+6N/30HRfEsB/B9zHgWVO3fjIBAsIGhHRDIY+xNAygRVQgjBDlcW7YN0dd2bzDj7YTjXWPJVltVbqOUvTXOYz8c63VGU1M9cBHKKqU1T1NgB3S9uzfho68/Pdm+0uyRHR3UR0B4DtRHQgM5dUdV2apk83m83myMgI1RpNHR4aMLVavX5stV6sbNpaWP71i4qBl2IEaF6ZCyxSAEsemgXEW1RwGkGLANxI1riaVL4yp30YfK/ykmImPApzap7t3EKQ+067ctigwP2xYlwq4AYLN6AcgzQW9S0oeubM2fTmv/v0kY8Q3bUxF3bnCpHJBYFEUZ7CMEQYtnfjmDnsaOE07/047/16Vd3S8RZIvPdgZgugQkSTmHm6MWaYiDZ2oKejO3Rx6rXValAaJ9qMWzohTre9FXTy4z//5dLtDz64fx7gHLdHE3kYXxDhHINyxFsVeCsU+wKQetb6p1T844fA3/jn2LzkqB2PAOeUOSqHQefoP2EThP6YkUxvgnwLoi2QT1RLTZUkUTXehukxXzp/MJw8acyt3j1RZTOhkM/bMBeQNYHmcyEAAxMwhUEgaLurdcJmAUTtU/LS9kfU9jUABrIkJS8eBGiSJJRlmaZpqtV6S3oEAydZO6+1bVvfsv99yVjj0jBH5AsgHzKFecAVwFIAyCi2EeMoKGYBQJrF5zUlSQ4G/s9L4fIXxY15BPhqV5AfsRx2INIQAdd7yP4tQJseQQvQJpRarC4VlRQQU+5qHP2Fz6e5CRPG3dGsr9geBOMtIQzDnLBlCkyAIGBSJTWGoKoZGePEOQ8AbK1R7y0RWSfKDEWaeiiJpnEK5zJkzmfjVftOyOXfnG7v237vv12Wk1qtEAKImGxOyOZBvgClvAHyIDWglar0PwjtfjiV9Lx61iq8CfjWS2XyF0cuegz4+zyHYRTkvwmAFUiJ8WNRPTSGaqzwTYEkDI2hpuVhHSFLoGq7K4NHfvQjtfEHHHj8mlZ828NJq9HnsnHGhNYGhgwD1rYNhaq6dgwZoDM5sCKs4jLy4uEyp+KzrDsIB95aKJSmBdEJ2598+s4HfvaziqtWe3IgChQ2b8hHUJ8TQp5h8gREIEtKjwP4KLU9yKSZxV9IJPmL4P1VAAHgUeAjAduppaD0v9CJd2oIvxKlcgJfjAHTFLIxqcaKLGXlTGBa0CxTsWK40TNjZt9hH1hiumfMeEcs+sS6VmP1hjh221KniXrK4I2gHXiH4cnA+DwZnRQEPCOfs7ML+f0j4gMH1qy/88Hrfikj69aPZ9FijthZUBAxfCQkOYYtClHI6nNgH4AalrThFWd2CAwOp41/F3FrDwV+/pey+Kujtz0EHMXgT48NS3NBGI0XuAqge5TkgFhJEqiPgSCBaiJwKSRogbxTNY7IZ6rGkWYmFw10T506MnXBm3X89Olhvru7GJXLlSCKxgNAliR9rWp1JBkZafZt2JBufPghHtmyuUviZEygFAREYlXZErkIGoTgNMewEYhCIMsBlCMERvkpJX07FPsDACnuG0rrazzk8gXAn/4aDi8r/N2TwJgU+HlPWHqQiL/y3B39NROJE0yJiVwM0VQ0TIE0ZVgH+EzEZKAsgwYegIdmClivnCl5B2KFSHv8xWyhQlBjLElIgCOQDUFqAR9AA8vsQ4ADgc8BgWVKc2DOqQbM2KSqAYHeN1pCUflaNa0fTsCHDgGG/loGLzsA47WA2Q/4Z8sWXUHhQ9o+nAwFUkB/ysAYrzQ1IU0cqYmVxAGcifoU4IwhHmIgTI4FIuQEUAWI2lNGKFQIHZcxVmuElVgQgL0RmBDwAZOxgIQEEylcpBQxY6MRJI4weo4PAJ5pZM1rMnHuTcA36bnjZH+VvGIxVB8HZjvghyVbvCkw9hsKLXRuCSn+rzIEqtMzgnEKTQnkPJwzCJwCHuqkbTUcAeIERNSeAajCWoZqe2XcctuqBJahgUdmGUEAeEswgcIR0XoVWGqD43ZFqZn49CtN11rkgE8d3g7487LlFQ1CqwCtgDmVIH9bDIsPWeJ/1vYUa/T+kwq9j5WKRDQ5UwmFkHmAPRSOQOpJhSHtlcT2fI6gCiYigWGjsAplkBoAAWAMOBPVbSBtEOhotCMZjVYwdioXN9LGmwX844Xwv38lQyK/KmGQHwQCMmYJRD6UM7nbQms+D6V9n/cQ6VYAdwLUIiBSRQVAt5IaArwHgbQ9jFESNu1ItKbtLIFhIlQ73UQOwAlQmrRbzTaIc5c3fHyiMv9Cvb/2sOdicb1i8qpGMleAH7T2BPL6T9aYu3Im7CHQeXsvDQ1CZQVAI6QY0fZ0DqRaVEIXoF0gXgDVMXv7CoVeFvt0KPP+WGPoWwucu/Pl9nMvJq9ZLP0HgXFg89U8h+uZ7SWvRh4i7vyGZNNZ3DcOA171MPDAawhwNL8/sfllzuQ2M/EL9hdejojq5bFrTT1M/RmvVtj3Pclr/t8c7gRswdjfFzjHoOfCh7wcIcU9dYlj6927Xo1+7sVkz0d7XkU5HnDq3ftjadWVdNPOU6J/bSLd1pBWb+bdktcaHvA6AASAo4Bq5s1Xmz79tQKpoN3L/6XJAy7x2VXkzdfe9jJmEy9HXheAAHA00sdJ6QFR/Ye/FmCq+g9edeURSF8z5/Xd5TXvA3eXZRxcam00zMDukeX+nHwj88nYt/jnIvC+HvK6A1SA7jPhb4wJqtSOofBS5NpMsuKtLn3Pha/iGO+lyOsOEACWAXm1we+Ywm4iLPwzjz/mJNuSufT049vHJl5Xed36wF3laKClLjhbJHtEQdW9Gw2qZ97f5505940AD3iDaOCo3GNzx4HxVlZz0Z7ui8o/KrLlxzr3x9e6bHuTN4QGjsrbXHwXRJywnP8C7WP5AkHkjQQPeINpYEfoHpP7hRrapEr/0H5LL2fR8W/z8d/gNZymvRR5IwLEnYBlW/i9gJiACNBYXfOU41/ExeL1kjdUEx6V4wEnLlxCkCogfeqaZ74R4b3h5d6wNP/esDT/9S7Hi8n/B3LrBEUxxEM2AAAAAElFTkSuQmCC\"],\"useMarkerImageFunction\":true,\"colorFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n if (percent < 0.5) {\\n percent *=2*100; \\n return tinycolor.mix('green', 'yellow', amount = percent).toHexString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', amount = percent).toHexString();\\n }\\n}\",\"markerImageFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nvar res = {\\n url: images[0],\\n size: 55\\n};\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n var index = Math.min(2, Math.floor(3 * percent));\\n res.url = images[index];\\n}\\nreturn res;\",\"strokeWeight\":4,\"strokeOpacity\":0.65,\"color\":\"#1976d2\",\"showTooltip\":true,\"autocloseTooltip\":true,\"labelFunction\":\"var vehicleType = dsData[dsIndex]['vehicleType'];\\r\\nif (typeof vehicleType !== undefined) {\\r\\n if (vehicleType == \\\"bus\\\") {\\r\\n return 'Bus: ${entityName}';\\r\\n } else if (vehicleType == \\\"car\\\") {\\r\\n return 'Car: ${entityName}';\\r\\n }\\r\\n}\",\"tooltipFunction\":\"var vehicleType = dsData[dsIndex]['vehicleType'];\\r\\nif (typeof vehicleType !== undefined) {\\r\\n if (vehicleType == \\\"bus\\\") {\\r\\n return 'Bus: ${entityName}
Bus route: ${busRoute}
';\\r\\n } else if (vehicleType == \\\"car\\\") {\\r\\n return 'Car: ${entityName}
Current destination: ${destination}
';\\r\\n }\\r\\n}\"},\"title\":\"Route Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.24727730589425012,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.8437014651129422,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"type\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.7558240907832925,\"funcBody\":\"return \\\"colorpin\\\";\"}]},{\"type\":\"function\",\"name\":\"Second Point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.19266205227372524,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.7995830793603149,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.04902495467943502,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.44120841439482095,\"funcBody\":\"return \\\"thermomether\\\";\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"tmDefaultMapType\":\"roadmap\",\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"showTooltip\":true,\"autocloseTooltip\":true,\"tooltipPattern\":\"
${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details
\",\"markerImageSize\":34,\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"color\":\"#fe7569\",\"useColorFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', amount = percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageFunction\":\"var type = dsData[dsIndex]['type'];\\nif (type == 'thermomether') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"markerImages\":[\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAwgSURBVGiB7Zt5cBT3lce/v18fc89oRoPEIRBCHIUxp2ywCAgIxLExvoidZIFNxXE2VXHirIO3aqtSseM43qpNeZfYKecox3bhpJykYgdjDkU2mBAB5vCamMNYAgQyURBCoxnNPd39O/aP7hGSEUR24L/uqqf+zfR77/Pe69/Rv6kWwcgPLRIJfZUAa7xez2xd90QBwDSNZKlkHJHAK+l09mUA7BP4vPpRUVExMVoRef+L998njxx9X57vPi/PnTsnO850yPaT7XLXrrflqjtWymhF+HA0Gp0wEp/kHymEQqG4ptJDGzf+um5RUxMSiV7Z3Lyt88L5nozgHJWj4pGmpqZav99PWve04onHHuswmViQzWb7ruZX+Udgv8/z3A+f/NGye1evxssvb+wo5PMfTZs6bfqcuXNHL7hlweh58+ZVAOTUpk2b0p9dvjyqqmrs/b8ejpUMc+unzjgUCsXjsYruE+2n1JY/NedM0zCi0VjA7/d7/f4AAgE//H4/vF4fOjvP9h5695C/oaEhcN/q1SyTzVdnMpnklXzTq4EplUsXfmaRCgC7du3cOn78+KfGj59Add3z1Md1vV7vqPa2D1sA4MYbZ6qUiqVX9X21i4TQcfX19QCA6urquN/vn0kAPRQKpYbTnzRpUhgAampqAEFrPjVYSql7fD4AgK5r2tV0AcDj8WkAoOk6JJGeTw2+nocLdsEu2AW7YBfsgl2wC3bBLtgFu2AX7IJdsAt2wS7YBbtgF+yCXbALdsEu2AW7YBfsgl2wC76mh/ppjIQgXVloPxVSBRV0rBe455P6+kTKBYF3tonxY/IWarry7DvI298Tgp0PR9RzACaN1NeIS100+EdvKXW3cMZvF8wCK10Sq2it2NAzakmukP/wmoP/KuId3BRUMg5uCfCSNVSKVn1rNto7Un8jLrUVqJ4Fi2eEQiEYBzOsy3SYL37TNQdzi8Q5FxkqJIQBsNLlYMGF/zqAJWBxSEogDAY+DJibYqTuRg4WFgO3OKhCYTExbKk5G/mbkSPP2DQhLA5IO/NhSz1MMP882BDgnAFQwdiVSs2vPVhYDIJLUMkBgw1favM6lJoZDDAYhKbAYsOX+rqAhcXAuQSIAKzhSy2vS8YmB7NYH4WCfM7kw5VaWtdpOO3bfWZJZVXgPxMX898bVsm6RhkTIseX29yyIErm/J5z5vwr6pvmsLYjBgeDwSpVJS/OmT1n1de+9qANZgLc4q9Dyj2qQhUhSSUAUCL7GBcchCymTEYBYNWqVXj30MGHT586PZEJ+WAul7ts8bjspd9QKDRNU2nz4z94YtI3H3oI+XwB//3j/9m77eRUUJ9/0eh4APGoDz6vCi4ksgUTmYyBC4k8RLGwtzF+EGu+tHqRqqrYtm0rXnzhhQ7G5cpsNnvyiuBIJFKnqvSd55772eilS5fhwIH9ye+/dPaEf1T9otW3T8GtiyYgGNBBymYEgLSbvakidu8/h01vnkYhcab1gcVs5tx5c6PHjh7DU0/9qFsINPb3939UZg28X11dXR0Qwtr9g8efqGtc+Bn89re/O7FhR9BXNaFm+n98uxHTZ1SDKQqKAihweZlITUVtXQwNs8fg+Bmzdk+bnmPdf/7bwsbGeO2ECaED+9/5XCxWuTGbzVpDwJpGNtx+28o77rr7bmzZsu3k7z+cMlHzeiPrvnoTwtVhFAVQHAZY4HBEoiAAeDXUjI/gyJGeQEd6TFj2tHYuXNgYy2azVe0fngiWDLNloHNFo4FZkXDsoTVr1+KD4x8U/3Ci1qP5PV7N74FeFUbClKDEriy57A5JANL5a68hnqoINL8OAPqbXbNp7clTxTVr1/oOHjr0MFXxq2Qy9wEFACnoY//6la9QAHj+9Q/eUL2RWkVXoWgqkhZBypRImkDKBFIWkLIk+h1JWdL+zrmeNCWSDFB0DYquQvWG637TcnozAKxbt45yTr8PAGowGBwVDAbvmT9/Pvbu3dddijV9WdUUUE0BUQm6kwaCYe+ljK/w8ruUdsYCBLlMEUQhoJoCygWM+LIvHTx4sGfevIbqYMD3BSFkJVUUrG5oaFABoPXwhd1UVUBVahtpKtoOnEV/gSHHgBwDso5c6XO6yNF24CNQTbV9qBRUUenuwz1/BoCZM2dplOJeSggWL1myFEII9IeXziIKBVUUW1QKo2Ci41Anei9kkWcY6Ex5R8qfc0wi0ZPF6QNnYeQNB2j7IQpFOtg0WwiBxoWNIBKLVQI6Z8rUqTh69FiWaFNmEIWgLFShoM5TZbIzgVxvFp6ID5rfA6JQgBAIxsGLJkrpAsycAcH4gN1gX0QPTW9vP5Grr58cJJTOpbqmjgWAnp6ei4QSEEJAKAGh1BbHCS2DLAFmMAgmICwObjDnyYMMAtJL9oN89vRc7KWUQtOUsSqhSggA8sWivSEh9qBxTiCEAGRwQARUVaB67Hf5pZAQlA0Ayrq2LTCogVyhlLURNEw55yYABP2+4ED3vHSClBKQ9jiFdHqvEBCMQzAOKYSt6/RqSGnbDPJRbgT93hAAcM4NyhjrBYDKylhswEEZJgYJFxDchnGTwSqasIomuMnsIDiH5GKIzUAQTsCVlZUxB9xLIUVbKpVEff3kiLTMfimEA7HP5bZgHMJ07mnJAiuaYEXT3jcZDMLkTgBD7exgBKRp9NfVTQwnk0kIKduoJGRH8/ZmhMNh4skc3DnEkDlAi4GbtjDDguVAmZM1M6yB68JyKsCGBqD373s7GAySnTt3gBDyFhWCvPHee/8HAJhTU5g0BMg4uMXBTT4AZSUTrGjBKpiwCnablQbDbZuyfTmAuRPMegA4euQopCRbaCaTOd2XSLzX3d2Nu+64bR7PnP3LJSCDMBm4YW9FWcmyQYMytsW+Zpfdsm1MdimAdMc7K29bMedCdzeSyeS76XT6jLNI4PGf/+w5aLqOu25IjOOWKcSg0jJjcLZ2ecsZD5TdybqsOxC0ZYpbJ58frek6nn/+eVBJHgecjXkqk2nu7Ozcdfz4cdx556rJN5C3m8v3jBt2xpdnazjysawNy5lUbKkrbmtZsWL5pGNHj6Or62+7k5lMy5CFNRQKTfN6tAMvvvhSRe3EOqx/4oXXLvia7qO6CsVZrey5154KB5YpKSG5tHs+5/ZsZnEIk6Ei1fLH73373i/09fXi0fWPpgyTLchkMqeGgAEgHA5/vjJWsf2PmzYr1dXV+K8fP7vjLxduWkY8ilpetQZPg+UJxh63lzqlNDi7gTa3fuPraz6bzxXw79/5FutP51am0+kdZdaQ/2kzDKNDUci51179w8pbP3er8sAD6+pnVCWy+/fs21LAqBnlMT50qJXFLq2a2L/5gaVy7N133j69u7sb67/7iFHIFf4tlU6/Ppg1kLGU8hYAywBMeOWV33gfXb9+1Q+ffDL+4Ne/AcYY/tS8PbV5++4Dhy+MopY2ZrLiidQDgDBSp5TS+Y7psS65ZOHsW26++eYosxje2PwGNm586eKzz/x027+sXWsBOAfgbULIQQAgUspaAA8BGAfnsamrq4u0tZ0Q333kkdGmZS3f8JNnlBXLV0AOilRKCS7sWYlxjlKxgHw+j5Y3W/C/Tz/NQ6Hgjp9seKZ31py5ajwe4wAtz9zdAH5OpJTPAqgEgL5USkpu4eLFHloqFXniYh9t3bunauuWrStisSi5//4vYnHTEkyZOhWqokBICcuy0N7ehr2trXjt1VeRzqTl3ffc81bjgsZELF4pQ6EAqa4eI6UEicfj5dhTKoCikynx6Bop5C14dJ2XcjmouipvvGFGoSJaWfr738/7tmzdjl/88pfIZjKwnH2SpmkIhSMYW1ODhvmNGFcztjhudFXR69Wgck58Hg+XEorH5ylDJYA8kVKOckpdB0ADIBOJhOzv70OhUFILuTzPZLNcSE6SfSlvJp0O5A1DN0qGDxLS4/OUAh6PGQqHC5XxeJEQgkgoRH1+L/wBP6LRuIjH4+Uf8gSAUwB+MbhzzQSwCMA0p/QUQADgNJ/PJ/v7+wnnnFiWkJZhKCYzKADoqiZUXeW67iGcSxKPx2QoFAo7AybnuE8COAZgHyHkxGXjeFAQEQCzANQCqAIQBeAH4AXgcex052w45TMcyQHIAOgBcBbAUUJI5uOM/wcaHmf3g9UM7QAAAABJRU5ErkJggg==\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAA3vSURBVGiB7Vt7cFzVef+dc+/d90OrJyO/JSO/4ncxxfULMCYIAyEW08amJJgmM4GmnZjJdNq4gcSGzLQxk3bsaWcaaIHyR8CJrWAbpjgG/AhINsbYxkaSDY6xJFvSrrS7Wu3uvfecr3+cu1pbXhkJs/4nujNndufec77f+d7fd+4uw8gvIxwOfocBaz0e91yXyx0BgKyZiWUz5kcEvBKPJ18EYI+C5rWvkpKSyZGS8LGHGtbQR8ePUUdnB50/f57OfnqWWlpbaN++39O99fdQpCR0NBKJTBwJTfZFE4LBYLmh8+YXXvifKctWrEBPTze9+cbu8/3JVMoWNjwer3/ZsuUTvV4P239gP36yceNZW9CtyWQyei262hcB+7zurU/99Ge3r1nTgJdfevFsqr8/Wlc3rWbGzFkV8+fPr1iwYEEJgLadO3cmbr/jjohh6KXHPjxamsmar39pjoPBYHl5aUnnqZY2/b1Dh9LdPd39kUgk6PP5PD6fH36/Dz6fDx6PF+fOfdZ9+pPTgbq6Ou+aBx+0k/0DVYlEIjYcbX4tYM5pxeK/WKIDwM7Gxt0TJox/dtLESXC53JuHzvV4PBVHDjfvAYDZs+fonMsV16R9rYeM8XG1tbUAgMrKsrDP659DRJ5gMNhbaH5NTU0IAMaPHw9IPv5LAxORy+31AgBcLsO41lwAcLu9BgAYLheIkftLAxfzGgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4D/lME1ke7gDF8ltbOHe3W923oEwYi1jxftWfZWgAziwacZkd2pfyN96XN5IIu7dMtIKA9/TI+zqCnFps2Alg5UlojFnVqIHZUlO2sl4RyC4CU+SEEylux8Z/iyc7mrxw4U7UnYwvGpXMYKIgNGdwXC/76C48oRw3sDWfnCgIkARJXcpwbvpA1e6T0Rq5jDr8EAHKA6OpjUOJwfeXAJAEhAXAGgEPKq+dIMVJqowDO4RAAC0rHV21u5LijAJaABAOIAY5Oh15iFMgj1zEpcUuuXjpIWeCouxjAtnIZcGKA5AVFbRfazPUC50QrKe8+Qy8qiqjBYIODA5DgBd1pBO9WRg9sy7yOhXBca+icYrgTOUGOiKnIVdCdisAxJGBTPsYW0nHRrJqgfNmGVtiqaeR1xchF7Vgz40q/BUNmISlcL7CUgJAMnOUiVwEdF0PURIAAVHaC8ucbAiwcQAb1KQpwXMjFrhtYMcOVO8lhOB457ujcKZd9hBguSYwcelTupKyaQWKYJFEU4xJw/Dhfcw29ilSBcNjEoTucFnSnkeOOvvTJpcVC1cYoGB5NAGEQTukjMAzHoghJghyWCRjenYoTuZjKx8xJiwU4LrSZ6waWpIoBjTuRqxDHRUkSUMWAJAZp6QU5FqOw65HHapG3bGVcBTZXDI5VnFaFgBL1yC34uoBJqEJeIwD2MMY1ilZidAFEMlDOqm9UdpJ0ZawumI+LU9ArwhyqWxyNz14XsBAMUnLVH0ttGB0XococdCGWE3XhOV85MF1WV2OY3omK0S2SkxgYAZYYJoAUpcqEEjG/Ru80isA1ysMXYNCnCum4aKUPgTu90w3sFinXL6nO/MadCAhiKloxBjFMeSuK0S1Kylv1cE1bUVoYyHwhoI6bCswpjjuxK5u2G2lcti2jzNCRTluioHEVw52EBA5/2LKsLBL+h2gs/o+Fjpa+MqtmjCbkqQJSYFF3T3zRsPMvA75i7UiBA4FApa6z5+fNnbd6/frHADghk7QdlhAHdMY0KXkZAHAuozaRMDRtKYMdAYDVq1fjcHPTD860nZlsS3qsv7+/+6pNDr0RDAanGTrf85Onnq75/uNPIJ1O4+dbnj34Ot6B4eFLqksqUeEvgcflAREhZabR09+Li/EorLQ4eFv317D2oW8t0XUdu3a9jud/9auztqD6ZDLZOixwOByeouv8D1u3brtpxYrb0XS4Kfbj3//8VHC8d0nDLXfj67OWIeQJgDGADfoOAxHQl05i14l92PHBXiTPp/c/OrFh9vwF8yMnjp/A5s2bOqXEbX19fX+8CriqqspvmunDTz/10xkr71qFnY07Tr1i7aqsLg2Vb6h/GOPCpdAYgTPlNLmF5AzpvBRp74viX3a/hO6+ge47+hZG61fVTz9y+DCee27Lx15fYFFHR8cAcNkPuw2DPXfP1+vvvf+BB7Br967WX9Mbk70eCn33zlWoCrsgKAFBCdgy/2nLBCyZgCUSMGUSpkzC0G1MrKzE0XMt/la9I0QnM+cWL15cmkwmK1tOnwpksuabg8YVifjnhEOlj69dtw6nT51Kv2q96fYG4fG7gbJwFhn7cxicIJgEZwAfEiokGASpWG1KhvIwg1/91ti1N9DEJ7ZOzKxdt87T1Nz8A67jv2Kx/o85AJDk//zXjzzCAeA/D7zU6PZjkkuXcBuEjN2OrGiHabfDFB2w7HZYoh3mVaMDWWdu1m6Hy5Bw6RIuP6b87+HXdgDAww8/zIXgGwFADwQCFYFA4BuLFi3CoUN/6LRmyL/y6gSXTtC4QDTVgQo/B5iEJFJ6Rt64lI6Vfi3JYBFHd1JA5wIunUNIQvpr/C+bm5u65s9fWBnwe9dISWVc0/DNhQsX6gDwTuuhd3WNYOSGTjjSehGp7EVYsguWuJQfssu51wVTXIIpLsGWlzBgXsSRM5dg6Hk6uk787Zb39gHA7NlzDM7xoM4Yli5fvgJSSiRmmbP9HNA0Qm4D6axEc6uJ6eOzuCloQuOOjlneqiUx2BK4lDBwut2DTFaHoXFYGilaHEjMMOdKKXHb4tvw/nvvL9UZ+Lyb6+pw/PjxpOZhsziX0DigcYLG1QaEBD69ZKA7wRHx2/C7BDSNwEi9AEmZGmJJA/1Z9SJM12hwvcYBzgmaj89obW3pr62dGmCcz+cuQ68GgEtdl7oYU40CZwSeW+As1rmy5KzNkbY1WILDlOp71ubgnKA7czVO4NyhwQhcFS7o6urq5pzDMLRqnXEtCACpdCrFHOHlAsTgYEq0nCnj0jnBY6i8KCTLBxbmzB2yPkczmU4lAYAxHtKFECYAPeDzBQZD4GU+motMueXklECWc7QkSaVDGoTAVetz8AGfLwQAQoisbtt2N4BJZaVlpZQjkntdS8w5UFOFni0YLMGhWfny1rbVPVuoOVKyK9ZeTrMsUl7qAHdzkPyktzeG2tqbw8KihCQlPjVUl2hLBkswmDZD1mJIWxwDWTXSFkfWUs8sZ64QzlqHjiRA2tQ7ZcqUYCwWgyT6hBNjb+3ZvQehUIi52tje3M6FyHHIYNkOqM2RsTjS2cuAs+pe1uYKPLcBkduA+m60sH1+v5/t3fsWGGP/x6VkjR98cAQAMNc7bXJepAyWzWHaimjW4siYDGmTY8DkGMhqapgcaVM9yw5ugMOyeX4DkmGub1otABz/6DiI2O94IpE4E+3p+aCzsxP333PfAvOi2G8JBtMRbU68GZMj44Ao0BzXmgOsRk7spq1oWILB6rQP3nt3/byLnZ2IxWKH4/H4pxoAeFzuC21tretW3rUKnk5mtWiflzAGxhgDQ66IYyrnOnqzBFfDZjAdLk1HMnkpMWRNLldmFomamtrIL/71F+iPJ/8mnc2e4QDQm0jsOXfu3L6TJ0/ivtX3T607M26P6SzMWI5eB7ktPHLPc/MV5xwTjpe9sfLOu2pOHD+JCxc+fyeWSLyZdzCoWsvjNpqef/6F8KTJU/DDLT/a3jM90eDWCS5dqmDvxF7NCRSAOikQhCuMUXHMEDjm3v7jb/+oIRrtxpMbnuzNmvatiUSi7QpgAAiFQneXlZbs3rGjUauorMSmLc+8dShy7HbDELqeA3bC4GCScHxWSMDOgVuaPb2t+t3vPfK9O1P9A/j7v3vC7ov318fj8bdyWFf8YCSbzZ7VNHb+tVdfrV911ypt/bcfq52J2uTBg+//LhWwZ0nJYTtWf6WrcccDGFgLdn5nwkPVD9Q/MLOzsxNPbvhhNpUc+G5vPL7jcqxBjonozwEsBzD5lVde9jy5YcPqTZufKX90/WOwbRv7330nsffDt08dSB41EkZyHPfwmwBAZuTFsBm48GeuWfai2oUzp02fFjKzJhp3NuLFF/+765e//Pfd31q71gLwGYC3GWNNAMCIaBKAJwBUO3uQnZ2d/MyZNv1vn/j+LUuXLq/Z/MyzCIfDTmxW8Y+IVFyWqjKRQkDYNqKxGDb97GkcOXLk7LZt/9F8c12dqKqqYM4LYALQCWAbI6J/A1AGgKK9vSBhoa8vEe+N9TwejcZYU1MTfrN9O6puqkJDw0NYtnwFpk6dCsZUMrFtG22trTiw/11s3/4aotEo1jQ04NZFt6KsrJTCoZKtJaWRiGG4KBKJ5BJWnw4gDedAx+0yMJCywLnQGWOSMabV1NbikUfX40J7B367sxFbt25DMhGHZZkgAC7DhWAojOpx4zF3wS0YP64aVZUVYCoQSN2la4bhIsNlcOS73H5GRBUAHgcwBYABAD09PZROp1gq2V8WTybq4vH4xEQ8oSWSSfSnUkinM7As9RdUw9Dh9XoR8PsQCgYRCodESTj0x1Aw2OrxBXsDgYBdXl6eM2IB4CyAbZcb12wASwBMB1Dq7C4ACJZIJHstM5PWdC2TTmcom80wEtySAFwupum6wbxeDxeCuT0et8/v94UBTTrSJABRAKcAHGCMnbrKjy/bRBjAHAATAFQ5NuAF4IFqAtyOKzKo83MLgAkgA2AAQB+ADgCfAzjBGIsPxfh/6wbDK7xbMFYAAAAASUVORK5CYII=\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAyUSURBVGiB7Zp7kFRVesB/5/S9PdMz/ZoHMwo4MICDuoGVIYICIuzGcn0vC+oWGuNjs8mua9ySP4wpgyaiVVupbHYTsLJmNT7WNXExwqqzrq8g4oNxdXUgyEMQARmZd3fPTE/3vfd8+ePenhlgBsFlrFSqb9Wpvn3vd77f+b7zne87ffsqjv+wE4nYDQqWl5aWfDUcLqkAyOUHunID+Q8EnkilMo8C7gnoPPaRTCYnVyQT71+1bKl80PK+HGw9KPv27ZPde3bLjp075NVXX5FLL7lYKpLx9yoqKuqOR6f6PIFYLFZtW7r54YcfqV+4aBEdHe3ywm+e39eb6etzPZfS0kj5woUX1EUipWrj6xtZedddu11P5mYymc5j6Q19HrgsUrL67r/7+8VLly7j8cce3d3X29vZ0DB9yplnfWXcrFmzxjU2NiaBXevWrUsv/trXKmzbqnz/9+9VDuTyz35hi2OxWHV1ZbJ1245d1ltvvpFtb293Kyoq7LKystKysnLKy8soKyujtDTCxx/vSW3fsT3c0NAQWbpkiZvp7a9Np9Ndo+nWxwJrLYvmzV9gAaxbt/75urrxd592Wp0Oh0tWHSkbiUQSv3unuQlgxoyZltZm0TF1H+umUnrC1KlTAaipqUpESmMzFIRjsVj3SPJTpkyJA0ycOBGMnviFwSISLolEAAiHbftYsgAlJREbwA6HESUlXxg8lkcRXAQXwUVwEVwEF8FFcBH8/xhsnZC0ksw49eQPI5mmNtP54ccAIvqgqbz4aYn8zYoTUXXcFnueyZ8eXtleZt75iQnpU0VUvYiqB5mvu5p+XH9w8RtgnJMOLut/7rd4+fpRBcS52hz65csnHdxQ8clZnyuT3NV40sHRUnfq58mUWFJ70sEn+yiCi+AiuAgugovgIrgILoKL4CK4CC6Ci+D/Q+Djf/higk8Jzs0IMjIGYDGAp0AUeBbiHf3Xs/HGAHyYlYaRX0EYC4txNeIFugvWHyXzua8cnDjYGMBoQIFhRFfLmLjaCxqAw8iuHing/nCwGlLuMrKrveNfnccPFnyLtQ8c0a1jElye8sGFAYwUSCN54Q8GB4ljKKpHkBmLOZbB4FLgjhLVYxNcDFnkMXJUj03m0kOKR0sgYzLHRvlwpcDYI7oaGYvl5HB4ZRrJ1cf9fP5E/5NwQUKM7uoTOI4/ql38kmgUOCMnEHMCL819sag2jJJAxgIs+HNY6PGlpUxXDQWXw5dXjxH8SFZBPf7SyqKrMQLKG7b/OkpmTBJI0BSjbwTGYo6Ni5+ZjMJDj1wkxmQ5iV+VsBh9BzImKbNQFhWjp8wx21c7dKIV9A94IxaJsdplZt9574JQVcUdpr3rzlEHdzLASslpg19EofLMMa3dc0Z9c9YMXT+s7/GCo9FojWWph87+6tmX3XTTzT7XA/F4xutXr4fyOuQZVQUQ0tLphY1nlcn5YqgAuOyyy3inefOtH+36aLJr5Obe3t72o4w68kIsFptuW7pp5d33TPne928hm83yLz+6b9PVb/4niRK9QNfUoquqUaUREEEG+jGd7Zi2Dnpy3qYHGr7OFdcsX2BZFs899ywP/fznu11PLslkMjtHBScSiXrL0m+uXr3mlEWLFrN58+auxD+u2HZWhb0gcvkyShZ/Ax2N+70KPcVvJpMm999NZJ99mi1dzsb3rviLGbNmz6rY0rKFVavubTWG83p6ej4psAbfr66trS03xtlw98p76s+bN5+nnvzFtouevK/s1AnJM+I/vB37j6aDziJeCtxhzUkhTgoYwJpchz3zbJI7fj/pzA829f6iR/bPPW9e9aS6utjbb715YWVl1SOZTMY5DGzb6scXf+OSS6+48kqanntu55+99shkOyLx8uuvIjSuDEzq6Ob5TdzgPJ9GhT2sCbV4W1vK57R+FP9lOrT33PnzKjOZTM2OD7dFB3L5FwaDq6KifGYiXvn95ddey4fbtmWv2fhIiVUqpbpMEao2SH4fiKCMgAbRggSuVkKwEQz22q4iVKtQEYUtJvzdlvX6+bq67PJrr41sbm6+VVv8W1dX7/9oADH6b//0+us1QO/jD6xPhGWSCgsqLJj8PsTdjzj7Ma7fxDkAzn5wjry+H3H2YfL7UGGDCguJEqnPPf3YOoDrrrtOe56+C8CKRqPjotHoN+fMmcObb7zRelsk9W1lC4QFCRlM9yfoKnsoEgOLVWCxDLfYBRwwnXmwDIQVyoMbo6lrfrq5+dCsxsbaaHlkqTFSpUMhvjV79mwLwHvjldewBGxQlqBswXn3Y6T/EDhtiNOGuG2I2444QXPb/WtOGzhtmL7PcN7di7IFFegiJDq3+ZVXAWbMmGlrzRJLKc6/4IJFGGO4MdQ+gxAQEn/2LcH0u+Sa27HO0IRq/V+MSqnBOUZARMAD75DB2w4mq8AKWkggpPiOtJ3dYgznzTuPt996+3xLoc8+vaGBlpaWzFybrygtqCPgeODtcTFtBl1hUBHfGgl+wNGv8FIayWjE6KCfD1UhBVqotPWZO3Zs7506dVpUaT1Lh21rPED7oUNtKH8OUYLSoHTwWRiEAsmBDIA4gCPIAJh8YL3lyw7vi5JAJ7QdamvXWmPbofGW0qEYQL4/0zeYjdTRTQ0Oxp9/Svx9jvKAkBocsCh1dP9AZ76vNwOglI5bnuflAaukPBo9bM8UpMIjvxeiWAUbATHK3/yNJM/h30vKozEAz/Ny2nXddoCKyqrKwc5GDYFMUJmM8peLqyCvkH6FZP1zXP+eGBXIFvQcrquyqroyALdrxGzv7u5i6rTTE3lX0gUL/DIYPPfwFDh+k5xCBhSS1Ui/9s9zQ/cLz0rEGxqEGMWAK92T6yfHu7q6MCLbtSj1UtPzTcTjcfW0E3t5EBSkv0FgPgAMQgtWa/9azpcZHICrhvR48B+52CvRaFS9/PJLKKVe1Mao9e+++zsAtk9rnIwbLBFHIQ5IACWvkJxGBjSSDeDZ4HxAIznty+SV38chGIA/PXumzZoK0PJBCyLq1zqdTn/U2dHxbmtrKxddfmXj1r7QRr9jMH/5Ye4d8OdV+odZ3F+AqyG3F/oFelr62PQnl14667PWVrq6ut5JpVJ7giLBygfWrMYOh3ll/pLx4iojR7p3QMGgpQX4kPUE8OFuF0chrjIvzL78VDsc5sEHH0SLWkmQLuhOp5v27t376tatW7nk8iun/UN8VhM5BblASS5w53BowdXD4L7Lg8EG7Z6SM36z+MILp25p2cqBA/s3dKXTLxRSBeDvtUpL7M0PPfRwYtLken791z9Y++fevmWE/WJBIelbgJbDtz4mePblBksrcPU/ubVrF65Yuayzs50Vt6/ozuXduel0etdhYIB4PH5RVWXy+WeeWR8aV1PDz+6/56W//PDFxbpELGULgwVEcwSYoWXkKExOuatqGl9b8p3vfb2vt5/b/uoWtyfVe0kqlXqpwDpql1lVlbwhUhr52VNPrQ3PPuccNm16PbXrR3f+9pvm0NV+pWEwhQKIqKHnm57iV9nydc6Smxc1zm5MHvj0AHfecUeuv7f/u509PY8N5wyCReRcYCEw6YknHi9bcfvtl9276r7qG2+6Gdd12bhhQ/rghhe3TdmywT4l2zkhEeIUgJTLZ62RygPbT5/rlv/xvLOmnzE9ns/lWb9uPY8++u9tP/3JPzd9e/nyLLAXeE0ptRlAicgk4BZgfDAGc/DgQb1790fWrT+45Zz58xdMue+++0kkk/5N8RO2iPiZ0BiMCMbz8FyXzq4u7l91L5ub3969Zs2/Np/eMM2rrT21YKQBPgPWKBFZAyQA093drTzPobu7uyPV3XNbR2enam5uZu3atdTW1LDsqqtYeMEipk2b5m8GANd12bVzJ69vfI2n1/6Kjo5OvrVsKefOPZeqqkpJJCtXJ5OJinBpRJLxeOF3bI8FZIAYoEN2SHmeJ6GQ2CiMUipUP2UK199wI59+2sp/rVvP6tVryKRTOE4eAcJ2mFg8wfgJE5nZeA4TJ4yntmYcSimUUsaydMi2wxIKKTXM6n4lIuMCV08m2O52dHSQzfbpvkxvZSqTbkinUnWpVDqUzvTS29dHNpvFcfy6aNsWkUgp0fJyYrEYiUTcSybin8RjiZ2lZeXd0WjUra6uDg2L/z3A6uHBNQNYAEwHqvAXTTl4Kp3O9HhOvk+FGMhmHXHdHGLEE8CytNY6rCKRsPY8VRoOh8tisfIkhFxgIAB2AtuA15VS20ZcTsEgEsBM4DTgFKASiAClQAnBig7EC8/8BoAc0AekgE+B/cAWpVTqSMb/AlY1WXIncMcxAAAAAElFTkSuQmCC\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAxNSURBVGiB7Zp7kFTllcB/5/a93dMz3T0PemYIDgoCPhZ5iaD4wNkFjQjRRMlLTNbSlKlyzZpobSVbFRPUbNVWSRCWuKvlxqybtbIrukp4SATZCAgospEBgeElj4EZ5t3d0+++37d/9O2ZnqEHQZzZSlXfqlMz/c253+875zvfOefeHuH8L6u83P+AwH0lJZ4pbrenEiCVSnYmEsndGl4NhSKvAJkLmPPcV0VFxZjKivKPv77wXr274WN9uvm0PnHihD5y9IhuPNioN216Vy+Yf6eurAj8b2Vl5aXnM6d8loLf7w9apvHhyy//29jZ9fW0t7fpdWtWN7Wdao4qpaiqDpbdXF9fV1paKpu3bGbxk08eSWXU9ZFIpOPirC33v7xs+TIdiUT0Pz239NjeaTOTHXXjdb4cuP6W5DOLFx/7aNdH+oknfqQryv0vXZTFfr8/GKyqaN7XeMhc//ba6NSfPFXqS6fESJ29jdGAX69+9KHY9OnTyxbec08mHInWhsPhzsHmNs4FNgxdf+NNN5sAh3/7n40dCxeKedUsOr6x8CzdsnBEQu9sPABwzTWTTMNQ9eec+1x/FDEuGTduHABXtreOKutJYyiFqq4tqD+5O3wJQF1dHSij7nODtdZuj9cLgMfGOpcuQInSFoDldqNFez43eCivIrgILoKL4CK4CC6Ci+AiuAgugovgIrgILoKL4CK4CC6Ci+A/B7B5vor6Mz4PNnbRYAAtoCQLUMMFVobuBWOALWdjVIGxiwbbZC3WkrXWLqAzJBZrR5T0LWTgdSHfdF1YcIlG57t8oM5nfov1OcCKPmDW1Rfi2IsA5yI5F9WFXF0o0i8arARwggsBu4BbhwaM6g0ujXY+9b+GLqrzLR5E5wsH2ziB5QRXoW8lCy3mosH553iwlDlEe9znai2DpMyhAJ+PxUNTJMhZm51+WM9xvsWFXD2kx0nl9rjQ4oYC3C+4BoEMnasl39Vn6wxRdcqbXApXpwupWBcEVgLKGLw6DU1w5bkaCjcChcYuHozuLYtqEFfroXC1TZ67GcbjlEuZWjSIHr6ozjZ7/y/VSWOLdgJIF9zjQl3JFwDOXn1lsYDOULm6X+YaROcLB6s8+LC2tzqvoc+Wx0L2nT/6wlIm5y6LQ9bs5TLXsO5x7jG192lxuJq9bCOg0aIRGcYEkt9lCsPp6lxlMsBlFE4ghcYuGoxznHKFYNjKYq7Zy5XFYW32lMtCBGzbLlwWLwB83m/2NNC44R0iFaP503+8jO1UqHz5wiwW0aNzvysgdPJTQr/7dFD9fHD+vecN9vl8NaYpv546ZeqCBx98CMhGbPXEqZRfcTWmyySTjuO2TMora/B4Sji+832OnWoGYMGCBez88IMfHD50eExG6Yd6enraBjJcAwf8fv+Vbsv1Pz9f/NT1y1esQCnNPz6zeGuy6WBN+MRRrwp1YMR6MOIJMqEuOj49xNFd2zh5aD9SVpr44PCJXVOmXXvpHfPm4fP7rtz98Z/usSz3+lQq1e/fnvuFSHl5+VjTNLb96lfPj6yv/0t2bN/eufJnj+37Uql1c/1Xv8WM279CaZn/rJcBGoj1hNm+7k22rF5JcyK1edp3Hps0bfq0yj0Ne/jFL55pVopZ3d3dx88C19bWlqVS8Z2Lf/7U1XNvu51Vb72x7/irz9fUBEcEv/03PyFYPRJDgZHt9XpvzG8QlAFnWppY+S9LaOnsaPPOWdhxx7z5V320cydLl/7yE2+pb+bp06dj/VxtWbJ03h13zr/r7rtZu2bNwVP/9cKYMiHwtW8+QNAbwOiOIN09SCiChCKQL+EIKhxBhcN4EGpGjuJww66yxNH9gePac+zGm26sikQiNY379/kSydT63uCqrCybXB6oeuS+RYvYv29f/OTKFz1+dIlXXFQrCznRjNhkRfdJzmIMEAExsqbUmh68holWGXf43deMg6NHJ+5btKjkgw8//IFh8lJnZ88nBoBWxpPf+e53DYC1Ly5bVSb6Mo8WSrQgx5uRY6cHSDMcz0q/vx/PSTNeJXi04EOPfe93L70JcP/99xu2bfwUwPT5fNU+n++rM2fO5P3332+uS3V9y9KCG8FSmtjRo3iN0uz+qqylemDnLhpDQDsFJGrHMG2F2xAyGi5Nhr65Y8f21unTZ9T4yrz3KqVHGC4X91x33XUmwN7N775nApbuk90nD5BpbUbaWqG9Dd3eju5o6y/t7dDehrS1kmltYffJ/ViA25nDBcbeLZs2AUyaNNkyDL5minDL7Nm3opSiNtQ0yUQwESydlXg6xc70Sf5CewliYSD9TqHu/anpIMUnJIiLjSVCGjAFTA21odNTlFLMunEWO7bvuMUUjKkTrriCvXv3RDyiJxpacGVXSc56W2uO6DhtKkmFFsocHchmtKhoukURNrJPG5YDdAEuDYaAV/TVjY0HesaNG+8Tw5hmuC1zFEBLS0urkQ3QPtFgILgQTC0IkAZSgEJQCClnTBwdF4KBOPf2iQBnzrS2GYaBZblGmWK4/ADxWCzqoS85iDOZDFiMS2ddV5Kz2EkGhgwECYLOzqOzxy0W7YkAiBgBw7btFIC3tMw/2JsrnS9OI5B2pPdt0AC9gdVZZxkBANu2k0Ymk2kDCI6oqsw1c/nNu8rVW8l+2ZFCkxRNzMhKUjQpNBlnv23nXfbAeTRQHayudMBtBlod6OrqZNz4CeVprcKqd4KsZBxgGk1KNEmBmGiijsScsZRo0s4CMnn3284CMqJCY8aOCXR2dqK0PmBokQ3r1q7D7/dLq7tyY8axMCOatDNZFqhJiCbuWNsLNrJjCUcnt4C0ZOew0WTQnDYr3/X5fLJx4wZE5B1DKVm1a9dHAIyYesPYjEBa+vYwJZAUSAgkHAtjookaWcl9Togm4eim8u5PS9YDNVNmXg7QsLsBreX3RjgcPtzW1rarubmZ+QvumtahXJvzrUzmWRvrZ61yxNnvPKuTA6xvt13bvjxv/tSW5mY6Ozt3hkKhoy4Ar6ek6dChg4vm3nY7oZJAJnG4oUIQESdD5Ud0v30XSBlZC1OGdjyTA/darwK3LcxcPm585ZJnl9ATinwvnkweNgC6wuF1x44d27R3714WfOWucZGrb3g7kee+eJ6LewPLcXU0bzwuuf2G3P3NoyevnzP3tsv3NOylqenkHzvD4fWQ197aikeW/nJJd1dnJ4//9On57V+a8Hoib7K4kQeUAWL0D7RcsJ2oqHv9wUcfu7Orq5MVK5Z3KS0P53j96lsgEPjyiKqKtW/891uu2tpalvzDMxsTW96s9yhMC8HUOCkxm07JO/fZk5A9dkmDTOSqWe/99fcfmRPtifHY3z6a6Q5F7gyFQhsKggFGjKh4wFviffG11153T59xHVu3bg3968/+7g9V3ae+0Zv0kX49l3ISjA2ccpe/NXvR9+uvnX5tRdOpJv7+xz9OxnpiD3d0d/97PqcXrLWeBcwGLnv11d96n3j88QVPPf108KHvPUwmk+HttWu71q96Y0dozzajJBUfXyqMA4gpfShmeY54JkzX19/6VzfMmDmjMpPOsOqtVbzyym9alz23fM23Fy1KACeAP4rIBwCitb4MeAQY5SxEt7a2qIaGBn70wx+OTKXTc5Y+t8w1d85cdN5KtdbYSqGVImPbJOIxotEo6/+wniXPPmsH/L4Ny5etaJk46Rqprq7JPTgooBn4Z9FaPw9UAHR1dSnbTsuZMy1GMpnItLZ2GFu3bq5d/fvVc0ZUjZB7F36d2fW3MmHCFZguF0pr0uk0Bxsb2bL5PV5fuZLuUEjfdffdG2+66ebW6mCVLvP5qa4OAoYEg8Gcg7tNIAIEADHdJnbcxmNZ6UQ05nK7TT1x4sRYRVV1/FTTqdLVa9bywgsvEImESKfSAFiWhT9QzqhL6rh25g3UjbokPnJkTaKkxFRaa8NtGbaIy+Up8eS2VgEx0VpXO66+HKfdbW9vV93d7RKNJl3xeNQOd4d1Mp0i3B3yRCKRsmgiYSVTaa9orS23lfR5vany8vKYLxCIeyxLKqoqtddbKh6PSVVVtQ4Gg5IHPQI8nx9ck4CbgSuBarJnvARsiUai4XBPmGQyqbWGRCxh2VrZAKYYLtNjZUyXSxsuU6oqyg1fwO91nhUSzvQdwB5gm4h8UvA4OYsoByYDY4EaoBLwAN7sYiDvZ4LsqUo60uNIK3AY2CMioYGM/wPREY0iGUY58wAAAABJRU5ErkJggg==\"],\"labelFunction\":\"var deviceType = dsData[dsIndex]['deviceType'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}, ${energy:2} kWt';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}, ${temperature:2} °C';\\r\\n }\\r\\n}\",\"tooltipFunction\":\"var deviceType = dsData[dsIndex]['deviceType'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}
Energy: ${energy:2} kWt
';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}
Temperature: ${temperature:2} °C
';\\r\\n }\\r\\n}\",\"mapProviderHere\":\"HERE.normalDay\",\"provider\":\"tencent-map\",\"defaultCenterPosition\":\"0,0\",\"showTooltipAction\":\"click\",\"showPolygon\":false,\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1},\"title\":\"Tencent Maps\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { - "alias": "route_map_openstreetmap", - "name": "Route Map - OpenStreetMap", + "alias": "here_map", + "name": "HERE Map", "descriptor": { - "type": "timeseries", - "sizeX": 8.5, + "type": "latest", + "sizeX": 9.5, "sizeY": 6, "resources": [], "templateHtml": "", "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('openstreet-map', true, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('openstreet-map', true);\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('openstreet-map', true);\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('here', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('here');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('openstreet-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First route\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.5851719234007373,\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.9015113051937396,\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.7253460349565717,\"funcBody\":\"var value = prevValue;\\nif (time % 500 < 100) {\\n value = value + Math.random() * 40 - 20;\\n if (value < 45) {\\n \\tvalue = 45;\\n } else if (value > 130) {\\n \\tvalue = 130;\\n }\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH
See advanced settings for details\",\"markerImageSize\":34,\"useColorFunction\":true,\"markerImages\":[\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic7b13uB3VdTb+rrX3zJx6i7qQUAEJIQlRBAZc6BgLDDYmIIExLjgJcQk/YkKc4gIGHH+fDSHg2CGOHRuCQ4ltbBODJIroIIoQIJCQdNXLvVe3nT4ze6/1/XHOlYWQAJuWP37refYz58yd3d6zyt5rr1mX8B7S5Xo5/0nPYaNFM1PY0gGqOhfAgQCNBGlWFFUAYEIeihigbhFdZQwt85BV5Gj9r/718R2XX365vFdzoHe7w6d77xnPkn4YpAtU0YiizNJcmPNkMQFkDiSlowHt2HNtGlTSJ6B+pTpsKTfKgTj3Pi8SMtFtEZnFs8d8dPu7OZ93BcCHtt0+OiL+FJjOiqy5K5dtLwD4PBHGvy0dKLYo8B+1+lAldv50FfmFzWX+84i2M3a8Le2/Dr1jAKqCHtl2y1wC/pEMP9ZRLBaYzF8CCN+pPluUkOKfB6qlmk/dBwTyt8eOv2AZCPpOdPaOAPjA1h9/SJX+TyGXuz0TZi4EcPBeOk+U+RErZh2YyMAyQJEoZUjFgtkCAEScgDyx1hmInTglqDj2U1X0WILaPbWvwHO1WummeuLONhaXHTf2wsfe7rm+rQDe133j/i5xPyrmCr+OouhSKPbdQ5fLiezTIYUBQGMJBgYWxMYSISZhbxgQT8wGAgDiwWxUvCiBxKhSKOqdh4OyV5+6XiEfK/kjVOXQ13apG+I0+adKpXaG0/Si0yZdvPbtmvPbAuCNT98YTBhT/8fAmEpHoXgKgPe/6gFGP0nwG8s2YykcaRCAYYQ5tKTkDVuArDEwMRF5AICS4VZ1AQBSr6oEgL36CBAvlKqIsyLOKQl5TZH4uN+TawDuY6o64lWTJX20v1S633uJNvfmvnbRERelb3XubxnAX26+5gDy6Y9HtrU/wERff1XjSt0WwULDmZEMawPOgilgQ4FaGCEygaXQMQyRMaxiUijUkAEAImIGAFURAOrVA1AmI1ZExGuqoqkVFefhyGtKDql4X4eHc6LxJof0VIVM3nVc4uXaHUPlo0Tpc2fv/zer38r83xKAd6y74iImO31EMf9REA7cpdVBY8NbA5+dFNqsCTQipkitBjAUsLUZNd4qm8AyjDMmJAIRhDzDEBEbJkBVAyJWQJ14AEaciIeSGicOgBeBWNHEeXLkXIM8UvFI4bVBCVJNfdk7STd5xOcp0LZzjIqV/eXq/4i61edM/eaN7yqAqpfzf62Nf5LP5lbko/DbCuxU4saEN1mN2kKTzQbIkuEIEWfVagRDEVkOyXCkVq0aDg2p9YYNAySVerU0WN1R27Jjo6ulMQ1V+ggAOgsjNRNEus/IiUFnYUy2kM23AcrivXh2RiTxjhx5iSmVWEWdpmhQ4qvwSBBrXVPfqDmuVsT7C3aZvKslyZcr9dpxdr81F8ynO/w7DuD1q/8y6kDw2872ticN0deG7wvQHXHmdxGK+1ibQag5ikweliIElNUAEayNYBCSRQRiYzf2rNtx11O/rC5d9dj+1aQyM2Pyz3WGozaNisYNWY7SYtgWA0A5KUVO4qAn3t4+lOzYt+Grh+bDwstHzvjA2tPfd1Z+39FTRhGpi7VBKrE4nyBFDKcNJL5OCerqUEXdVeEQb0mk8lECjR0euxe9cqBUOnoQ6RkXT78hfscAvH71X0Z5kf8Z0dH2CgNf2NkI0d0ZbmtElMtFVEAQ5BFIlkKb00AzFJqCGooQcJjv7t868P3/ubayZvua48ZlJt57xLjjB/cpTssXokK7IQNrbeoZ3pIRJm1aYSUW9cwixglZ7xNU40ppY7mr+sy2ezt7G1s+vP+EGfd/+fS/Ko5pH9/pJK04X6MUDSRapcTXkXJN46QKp1UkqNVqvpxVyLzhOajihh1DpVkmrJ7+uak/bbztAF6/+i8j62p3j20vbgXR+cP3LYU/Djg/KcsdEnIWERcRIk+hzWtEOYSch2U76tk1T6+84Tf/NCdni2tOmbRgy6T26WOiKDBhGFEQhrBhiNAyjDGiQp4DFgI8AChg1BGBXOC9p8QJ0kas3jvEcUxxnLgNpTW9izfdOqGWlve7+OOXrThk6qEHKtKehq9xIlWkvoaYytrwFYqlglgrcZxW+oXSz+ycpOLmnsHypDTIfuTNcuKbAvD2288x22dn7hrVnt/ATBftBE/CH2aCtqkZU6CI2hHZomS4YCPK+5AKHFB2ZNe2Nev/739/e9qY3KRnPzHtQp/LtnfkMhnKZDMa2oDCTIjQhghDC2MCCQITAyYxpmkhAIAZDDA7l4bOSeR9YpLEwfkUjXqMOE0QN2LU4waq9aGBX6/+d7O9sXnu3579jbVTx02dlEilL0FDG1pJG64cJX5IGr6MupY5duU1npIv7sTQ4196ytUDx8+sf+TN6MQ3AyBd8+L8W0a15zYw0d8O3ww4vC7ijlkZU5QctVPE7QhNEVlTRNYUjHcy7tu3fuuVSqXBF8z66962fMeIfDaHfD4nmUyWsrk8BdaYIAh9EFoxzExEysYoAQ5A0ioAEIpIBGZmAM459iKaJo6cT209TnyjWkOSNLRWi1GtV9A3sGPg56uvG1vIZ9N/OO9rM8jS9oavSOwqaEhZYh3khq9K3fdpXWsbvdR3MoYCV/UOVadcOvv2C/AG9IYAfue5j1/U0R5mIhNctxM8yvxLyMVpOduJyLRRnto1MkXK23axlB27sXtT1z//8vqDTt3vk/fMGnX4xGyhiEI2Qi6X1Ww2S7lCIQ3DkCxzQEQKYADANgCbW6UHvwcRaO6fAwCjAewLYAKAcao6UkRIBEniEtRqNVOrVKjeSFCP61oaqurKvqe237P2lnkXn/X/PT9l3OT9Eql2V90QN1wZdRqSuhukhi9T3Q2s9ki+NDzHWppeUqnG/qsH/+b7fzSA33ruI7ODIDh/RCH6KkEZAEINfhia4n4ZO0KzphN5005Z06aRaeOAcjP++4Ff3P/86hWTLjr08i3FfEeurS3LUTanhVwe+XxOwjAw1loLoB/ASgBrAdSAV232Gc0NyJGt70+27mlrzNT6nAEwDcBMACO892kcx1KvN6hUqWu9Xka9XsfgUP/Qjcu+Nf3g6bO7zj7urBNT1F+quxLXfUkaMmDrviQ13+8THdqYqvuLZpfq+qrJNXFDbrp87t0v/cEAXr5iduiTMQvHd2QnKDC9+bC9NUfF9kwwgvNmBGW5Q3O2SFkzAkaCg/71Nz9+2MTZ6rlzLs4Vi0WbyWS5o63N5fM5G0VRaoxpA7ChBVw3ANMq1AKoHUAewCwARwHYvzWctQCeaNUrt4pvgeha17Gtevt47+M4jrVSqZlSqepqjQpVyyX/8xU3VBHF2T//+OeOFbgXaq5fa75ENR3SarzDxDToYz846FTORbPRV7oHG9sm+qEPX3TEM3vc9pm9AfiBP53+T6Pbwo0Cd4aog4p/yXK+lDX5IDIFZDinGS7CckEM+JB//u9/e3Z8NGPTgjl/Maq9s8N2FNtcPpc1bW1tFIZhaIxJATwFYA2AtAVWh4hERBQByIgIE1Gsql8gou8AeAjAfQAeVdUvEtE9reFFIpIloiyATgARgCqALQAGmHmUtTYTRWHDhhaGYE0YYmbHEXZj//rBRc/fXTly5qGHEus2FUceCbxP4DShRJ2mvuIFboyqG5kNcNuWVM965MbNd71pAC99+vADA+MnR6F+TeAg6h1TeE/I2bbAFjVLBbJcpIDzZNke8qNf//yxKblZWz42+9Pj2opFbutop7ZCQdva2hAEQZGZXwGwDEBDRCJV7VTVfVV1BDNPUtXZqnomER2tqi8S0REAzgJwUqvMI6JBAM+p6pdU9f1ElGu1E6lqUVVZVYWI6gA2EFFijJmSiUIPsDbXmGT3b59V6Kv0dd334uLGYTPmHK7Q7lRi65DCawqviXWSrEm1PlvgWMh9KPbut+/77Ohtj/97d98bA6igo7aM+O/Ogp0l8BNFPQhyY2RyE0MqcC7Ia2jyGpksBYj2//WDCx9uk/EDZ8783JhiW5HbigXpaG9HNpvNMXMGwAoR6SWiUKS5KhERS0QqIgmAHcz8sqrOA7AdwCcB9AK4CcBvAdwP4EVV3V9VPwGgC8B4Zv4PIqqoqgPQYObEOadExC1A60RUJaLxURQaZqoRW0NEsm/xgI6u7rV9L295vmvGlKmHQ32vk0QdxfA+oYTq+Vgbi70mR4p6BEaKlTid98S/9f4MV7wBgF/66AEnFbPUz+z/VNTBiywLgxxCFDgwGQqR5wznOeR8+6p1657r6uopfu7wv4mKbW0oFvIoFovIZDIBEXkReUlVG6o6Fs2N/EjvfSczj2Hm/YnoY6r6Ae/9w0T0cVXdSkTfE5FsC8iTAZwI4DAAjxDRj0TkUABTACxS1csAzG39MHlmzqvqGCLKt1xZA0Q0QERtQRBkDZMngrcmNAeMmB08uHpxNsrz2pFtbft4TWInDZtSLE5T8i7uSKRS8XDjBX4fYbnusI2jMkt/tGP9rnjxrl+gICP4Riagrzb1ssKa4CkrYRhwwBFHYGSUOZJKo8oPP/vCoV846opSoZCnQj7HxUJRMplMgGblR5h5wHtfbE1oZAvIHBFtVtX7RKTQ4pSrnHOXAThQRK4BcIaqNkTkRRF5UVUTVf1462/TVPVSEfm2974qIm3MvBhAl6pGAEYAaBcR45zLiUiPiDxKRC6bzZpsNhtGUaj5fIG/dNTltYeeWja3ltbVcGgMZX1IWbUUqDUBbBA+OYxDPuDLSORq6KsN76s48MvzZnwwlzNDgaFzAIBAi0LKtGVtEQHlOaQCQpOHoWDWL+9+ZODCuV99cnTbmM5cIY+2JudZIpronHukxUWemavOuZIxpuG9H8fM8wDMJaJHVfV0ANcDOIyIPg5ghTHm+0S0UETWq2oCoA/AI6r6C2PMgyKyD4BPM/MggJ8COIGIFqnqV1T1YADbVXUjEfUaYxrOOcPMBVXdCmCutbZirQGIlIBwavucl2577NaJM6ftO1nJ9aY+YfEpvDryknamSNdAMQ1AGwxdc/DqDjz9k/7Nw5i96ixBSK/MhTRxJ7oUbracmWAoVGNCtRSCYOxLazfcN7VjdjK+beK4KAqpkMtpJpNRABNVdT2AowHUvffjAYgxZpNz7hUiuk9VT1LVWFX/iojuBfA1IrpfVRcS0Xne+6tUX33+M/zdew8AzxljLvPefxTA3xPRIufcpQA8EYUAFhPRSCKaKSL7EFGgqjtU1RDRZmaeGIbh1sh78s7LxM59R09um7585fqNdtqUMZOMMc4igE0DthSppcYWL80VTNbyX1QCPgNN1fJqDvzi0tnjQviObGia3Ee0JEAml+E8DOUo4pxaE4GUJz3yxJr9/vSIv+8uFAu2kM8jl8vBGNNJRE+q6grn3AZV3QRgi6q2AZjHzHNE5FEAp3vvv8HM8wFQSywvADAPwDgAi0TkPwDcBWDhcFHVh9FcXH9ARE4BMI6ZvyEiHwYwSVW/CeB0IlpERJeo6hwiepmIlnrvVzLzemZex8yDzDwZqlUikGGm6R0H66+evuPYafuNynvFkCCF4xjiBd67otN4C4GmEDAqTuVnR3++beWT/z5YfRUHio8/0dEe7DynJTUvswmmEiwxWcCDwGyee37j4ydNO6ucy+YmZMJQM5kMWWvHqmqPc24eADCzENEGAMvTNH2AiM5Q1W1E9GkR2cLM3yOiS0TkO0R0lao+zMy/8N7PBHAmEZ2C3YiIoKrdqnqjqq5i5j/x3n8bTQt8iapeKyKbjDGfFpEhAGOccw8EQdBhjPmQqk723rP3PrTWvhxF0Xgi6vHeayaTyx075fS7nlvxcPGgg8ZNIjHeSKRMdbEUIEHwEuCOA4DOvB25vSRnAfghMGxEFNRb7ZoM0HFNadFeIjvRgMFkhEDKbEl8Oqq7u3bs+/c9cXQUWo2iCGEYsqrG3vvHAPwEwL2qulZETnXO/Zm1FqoKVf2Bqh6qqr8SkW3e++tU9T4i+ntVnem9vw7ARQA6ReQ5AL9yzl3vnLsewK8APIfmovkiIrpWVWeo6t977x/w3l8nIluI6Dcicqiq/quqgpnJOfdnIvJR59wmEVlCRD9S1QeJKLHWmmw2hyAM9bhpp47q7q4d733aSVBlkBoNQGxgYPdVRZ82N5In9lS7dp42GgA483hMyUY0RXgwXzAjQgUtshp1WhOR5YgDzoiB0U2baqsPLB7z0oxxBxWz2Rxls1lh5gNVdbn3/rwWR68moi5VPZWZt4nIvgBGquoRAH5BRH+OprH4oYh8XlVPQXMvfIOI/BJAFxF1qupxRPRBIjpKVSe3dOtdInKbqj5PRIe3RHayiHydiMYDOIuZfyIin0HTfI4kIgAYa4y5UUQaAI4QkY8ZY5YR0aGq0kcE8k5NNS4t665u6G9r47xDCi8pqabsNbFe9WkoRvU0upYl8GunnqebX7kZQ00O9DipLbKjRfQTPWnXYyBTBxMBBiIML2IVkt20sf6B46d9rJjJ5chaQ0EQRAC2pWm6VlVXq+rZIvIXSZKELcX/Y1U9RlW/AWC8iJyqql9V1aOcc99W1SXMfAmAh1X1qy3O+rKIHCMiGRGptUqude9iIrqWiC4brisiDxHRt1X1KFX9qnPuowDGe++vUNUPishNLQkIiOjPVPVs7/02EVkLYHsYhtYYg0wm1FNmnZPftKF2lFPJisCIkhE1DFiFaNLr1i5R+PntGR5lFMcBLWfCxxbhrgkjgqMAjCKgkrWFX48KZ7RHJm8CziJLOXJpUNu4omAuOfbKOMxkKBOGHIbhHBG576qrrtLHH3/8QmaOdtdd/5tIROLTTjvtyc9//vN3BUGQs9aOA3CyiDxXr9dRrzfo2gf/Ljt1TpyYIMnWtQ4nVW2kNd+bri41fOlMADkQerb1p4/f+WGcaS9X8HOLUQIwCgCUdFGi6ehBt7k+3k4DqQ8cOd2+mQdPnP6xijHB+MAYhGEoqppL03T/J5544iRmpvnz5z+4Zs2a1dOnT5/+8ssvr5o5c+aMWq1WSdM0VdXORYsWHW+tXXbmmWcONV2jQG9v744dO3b0jR07dvSIESNG3HbbbbNFpHPBggWPtMTvVUREWL58ee2VV145bcSIEU+ddNJJ1RY4unLlytXTpk2bEoZh2N/f37dw4cKTrLUdxWLxvnnz5pnf/e53unDhwhPa2tpWnnfeecekabopCIIMEYGIyBjGCfufvmbpltuKY6a4LKkzCh8PpZu913g0oIsAOhOKMQTElyvYPrsY43IRP6uK8wCAYHrUo+gpiXoaG+LR0X5VaNgxNEAHz5pz6PIgMGBmBTCKiJZVKpUjjDEmTdPG/PnzPwSgLCJHoLlY/omqXgLgWSJauHjx4uNPP/30obPPPnsAwGNoLl+O32Xdt/a3v/3txnK5HM6fP/+3aJ2JAAi89zkAUwGcdOqpp+YvvPBCnH322fEJJ5yQA3CH9/5YY8yft0C+SkTmP/roo72NRqPjhhtuODCTyRTPOuusRy+88MJVd9xxx8cWLFiwiog+oqp3ARgVBMEO7xVzJ70/v2jdHbNGqu/16uq98WakmuQgANhsU98MRQwMP7N0iYxhUuybD/n3WzqlAMROROElzfY3NrXHrtTNFHTkMvkiGQNiZhGZ7ZzbPDx5IoKIXK2qZzDzd9F0T/0pEV2qqoeKyN8BwLZt27ap6hmq+l0RmQXgZhH5iohcpaqrwzA0RATn3DXOueta5buqeoWqnqWqT9dqte8DwPbt2zeKyBGq+l1m/giA7wL4map+jYj2S5LEA0AYhp0AvsvMp5577rn3Axi/YcOGxaoKEdkCYBYzqzGEMMgUWILRjXSopzfekFUf5wUKYXYQCoZhykcM08C+DMUMw7Rva8sHqHZCJFD1VtTDaYLuoe3xrLGH/Yu1NiZVtcYAQEVVy7vpmPNU9VHv/RUArgZQ9d5f473/qYj8OwBMmDBhPIBnnXNfAfAj59w5AK4F8DURmcfM1JrY/4jIrSJyq/f+XlV9vmVMPlEoFC4GgM7OznEicmPrB3hJRC4Tkc+IyI+897cFQWBay5lrVfVKVX30lFNOOUZV/aJFiz7YMi79RFQiIgbg2NrazHEHf7+70q1eGiwkROoteQkhOmIYp8DQBGUcYIVwOJMepCCAkBCooCAnUPVwXoU1rrXVoyi7nwgoDO1QyymwzTn34d7e3p8B+NsWFx4AYLP3/l4iuoKIHhaR/yaiLw1z6rp169Z57+cR0bUiAiIaVNU7ReR5Y0xcrVbPbf0ek1U1DwCq2qOqG4jofhHZUi6XAeC7IkIAvqCqIKItaG4LZ4jInxERvPevtK5fY+b7W+0eBGD78uXLx6nqd51z85i5G0Bore1rNJJsxuan1EumFo3w3mtKSupAMASNRJEACBk6ixWphWCaKs1tqegVUIWyiBcPIYhRQlLKhQccNDtW9YEIh0TkiciJyGFtbW29LfCCxx577PtHHHHEhdbabd77bzLzFap6jPf+X5o46Jf333//qWh6kP+P934HMx8F4HQA53rvkc/nl9frdYjIQbsw99SWy6opPvl8BQC6u7u3ENFfq+poVb1IRK4iIvHeX7dy5UpKkuR8Zka9Xv9WNps9n4j2B/DNkSNHnrV9+/ZRIvIhIjpMVZeoqlfVEcyQ6WNmpQ8+nyva9m4IO/XeQ1XFE6UKfYkUhyrTEVDEFkAWO4NuZAuAsPnDKlgFzih8ku0cU5y4NQiCxFrLAPYDUCOizxpjrgAAY4y54YYbvtwS5f1E5B9UdSgIgloURR8BIESEO++8c8qmTZtetNYeHYahdnR0wHv/pIhsrVarvX19fQsA5H71q1/dYq01pVKpkCRJXCqVaGBgwDcaDdfX1zcRwDELFy788JIlS96XJEnBOQcADSIKmfkSIsKwpXfO/bmItBljLlHVa6dNm/bIE088sR+AMUT0WRG5kIgmWWtfIWPcuPZJDJ9r90hIRVTEq5KAlBIIdYH0UCg6FMhZUvDvjSDVnZBhUhUSUijICxHCbDFXZGOMqKoH0KmqQ/l8/ptdXV0/rlar38rn8zs5hJmJmUM0jyPb4/j3h/ze+ylLly6dgr2QaepX3Hnnnefv7ZmdoyUamyTJWABoHvTtmbq6un4xa9asSQCuA7DSWvtSo9E4zHt/dbFYvKLRaKwF0E5EwoBENlKVMOPFkcJDCRBVUlEloLQTLgWz1987FAhImCECJVEh8Z6cdzBk20ITkIg4Y4xX1ZFoHuJM3XfffT/S29uLLVu2oFKp7HQ9/W8ia+2RzHyGqv6TiPzjsccei97e3kxbW9uZACYTURVNb7mIiIYmJIOwLUWqTqQVIqFEDFHV6nC7orDMBB22LOzhWbRC0LJRLalqGYqyQWAJVDPGVJIkqQPYrKq9AGCMmQoAaZpix44d2Lx5M/r7+5Gmbzn4822jVatWvei9/9M0Ted77/9j5syZawAk27ZtswCgqt0AtohIzRhTssZWDdvQkA4RtETaxAOqZSWWnXgR1Kr8/kTbG2ThtaAE9QQSZWIQ2EilFteyhoJCa4lxYMvf9xry3qNUKqFUKiEMQxQKBeRyudcVsXeC0jRFrVZDtVrFzTffnOnp6Tl2/Pjx944ePXrt9OnTzyGirY888sjLCxYsOERExhPRDGvtswACrz4m60pOqIMIBIX4ZqCYAWsZLXumAtid6z8A5DSvlgkKFkcMiBERqHUDiUu8994SkQCoEFF+jyPfhZIkQX9/P/r7+xEEAbLZLKIoQhRFbzugzjnEcYxGo4FGo/EqCejp6Tnv5ptvfk2dH/zgB8sWLFgAVS0CqHjvyTlnq2mFYF3VORnJICKwI2IFI0Qi7TCtLaYCVgnbAdoA6GRhaoPXhipIVJkEUCXP7CrleBAd2RHsvYcxpopmfMreaICZN6LpQWYRmZSmaeeuk7LWIggCWGsRhiGstWBmWGuxqwUFABEZ9ilCROCcQ5qmcM7BOYckSYbd/XuiTczcT80YHHjvZ6MZZ4O+vr5hx+14Va1Qa/M9WB0Asa+SUCcIRuAtg5QEBKDYrEJrwdhiIXhBRQyIJkMxQxQvkELh4RUq4kCJ2VHdOLiOx+YmmTC0trWwnQOgsvtoiegFInKdnZ3rRo0aJT09PTw0NAQAm0VkzvBzw5N/B0mMMU+pqhk7dmxXsVjkzZs35xuNhojICDSPRpPt27c/WSgU5hLRC95722g0aOPgWnbcW5VUBYCSJYBBChgQzWnt2J4BsJyheFkVr7Q6Hc2kZYU6ARSejCjZFN259UOrc6reOucMEfWpqnXOPQIAhULhN8PgMXNl3rx5Y4IgOIuZz46i6KyTTz55JBFVmXnFO4nYrmSMeTKKooEPfvCDs40x8621Z3d2dp566qmnxsxcArC1s7PzkVWrVi1X1QBAv/eeiYg2DK0upOgpiCBQIlIBBOrBOgTCCAAQ0jUQrGS1WF1vUPewLlTlKoQCOARewOqVUgzmtlXWTWuKiqiIVAAgjuOtuy1bgtNOO21ET0/PhO9973sQEXznO99BT0/PxJNPPrkDQAO/97C8k7RBVaO5c+ce19nZmb3yyisxZcoU/NVf/RVWrFjx/kMOOWQ9M3dXKpVRjUYjbKmGinOOnPPYWt04PZGhjHoQCZigAQsFpFwbxqlRpx6k6LI6gK5Kpz8zm20d0JHWQFAYTSUlALDexSNdEB+Y+nQxpZRlppSZ4ZybdPvttz9QqVSOt9Y+SkR+xYoVxx522GF4/PHHceCBB2LZsmWYPn06nnrqqQOZ+REiekZERr+T6BFR37hx47rWr18/NwxDvPLKKygWi3jhhRdw5JFHolarzXvuuee60jSdYFordxFJnHNI0rghiGc4jb3xUDEQEngyYEBrwx7KcuJHZzux1t79KZQ++iv5AHTnCadVBZGQhULh1SsIMfoe7KlsGRqTm5Q1xmkQBJtV9dijjz766f06bwAAEgVJREFUnpUrVy4EgIMPPjh300034bjjjsOaNWtQqVQgIjjqqKOwZMkSzJs3b/Xy5cstgFUA3rZF954cr6eccsrYxx57DJ/85CexcOFCDA0N4cQTT0S1WsWjjz4azp49+4l6vc5Tp049TVU3eu/hVXVbZUN/TH33k8c4DVRIiMFEohCjCIdXLC6VY+44DV+zACCEXiiWgnCkEp1EpKsEqqTEIsTq1Axg+eCy/kczp+QmqDZfuXpRVedNmjRpx9VXX32hiEBEsHTpUtx5551YsGABnHM47LDDcNNNN+GAAw7Al770pc8NPzdsUXe1rsOA7n4dBmjXK3NzgbHrZ2beWQDg7rvvxq233oqLL74YS5YswY4dO/Dkk09i7ty5uOCCCz4bx/FPRGSUiNydph71ap2W9T9eGGgsr4iqZSVVsLJ6Z5lIlU5srfmWAlgHtE7lDjgP5SjgAWb6MBTtoroMgpwoERTwniiJhwq5aPrxB+YOWwuQIaKEmWd573NBEHSoKosIpk+fjltvvRWqitWrV6O7uxvLli3DV77yFRQKhVeBtzcgd/2+exmm3bl3dy4kIowfPx4LFy5EpVLBpk2b0Nvbi+7ublx22WWw1ro4jgsARgJYVq/XUG/Uk2fK95+ypXxfrESGGUIEMhYGTP1ovQOYOr2+kcjvVt+K9c130cp4slyX4nDnBqYbRCAGkTZXUELIVtPeezeUu3rjOEaSJFDVpwEcmKbpLcMTnDhxIm644QYEQQDTPDvBNddcg3322ec1IL1e8d6/qryZOruDffTRR+PrX/866vU6kiTBAQccgOuvvx5hGKI15hki8lTz76lura/fUUt6F4siJIKCiREAakhB6BnGp1ST9lwbngJ2CfE99Zd4cPzIcDqg4xl4wQl64EE+BlyicCnYanHz4RMumviR9vO7C4UC5fN5JqKzVfXlKIomtzzGr5nwGwGwOxe+ngi/ntjuXowxe/s+0Gg0+ohofxG5o1KpoFqv6+LBn496dssPt6dcmWAtlCOCNRDKgJgxEopDoLRl60Cy5p5P4Hhgl/A2NbgmTuUGBeCBOUTokVZAtyiIFJSk5QmJlJKeyvaeer2u9XpdVPVxVZ1Zr9dv25PI7Q7M3sDbEwe+0Q+wt/b21vdwqdVqv1XVaar6eJwkqNdj9JY3bW9IKU5cZRwUDNPcuagBE2G7Kg5RAKnI9SD832HcdgJIARYOVdyknXtjoTpBoaRsTPOMHQy7fMutQy/qQzOr1arW63VNvd+kTc/NfO/9I3vTXXub0N5E9/U+v57Yvp7+VFWkabpYVc8DMJSm6aZyqcSNRk1fxOMHPb/5v+pQtWwgUBCxErGCiOJhXHYMuRkU4r7XAHj3aYhTAaC4rakI9dNkMMSWPBhMSsRKmjRKIyuuZ3Bzfe32crnGlVJJReQ+Vc3HcdyuqgPD4re3ib1ZHfhmVcDuYO4JxNaYetI0HYvmMen91WqVqo1YNqVdW2uutz9NSp3KTNpcxMEYgjEYVNULmvVxiwLVu09D/BoAAcAZXL6j7F9SBVRgiUwPkRJYCQaqrEoMWrrqp4WN2ZfmxXGtWq7UqFwuJyJyP4A5cRw/qKryelywNw7ck+58I336ZvtR1Uaj0XgewMEicl+5XPblcpXqtXJtk33x1KUr/6MAbnKdgQKsDFUVMTtUYFWBvpLvohRX7orZqyJU192K6tSz9Qv5HPcQaCpBZyvjRSiyEFIVkDioiBbL1W3LglGduWJ9LKDExnAtCIJEVU/w3t/MzIfsbiD2dn0jHbkrF+1qSPZkXHY3MMNX59ydaB5ePdNoNLZUqlVfrpSxOvO4earr5xvqvm8iGfggBFNIyiGYQwwQ4xwABqqLhmo+c885eJVf7NUx0gDE4iv9Q/JYc1+MDABvDJQs2DDYhlBmxD2Da6YNxOulW9dsr1TLWiqVtF6vrwawXFU/7Zz7TwB/FCf+MUuW1ylJmqY/F5GzVXVZvV5fWy6XaahU5q26asuA22L7hlbvR4a8NVAYKFsgMBACJZDm7mNHSZ41HpfujtdrovS7bkV58p/oRwpZ8zIIhwM0C0SLoBipCmqNnaHAhq3L7MT9D9mfhjIrrYRt3nu0fG9VAKd673+Npq8t82a5cW9ADdOb4bZdljfbRWSpNt9BeSJJknVDQ0MYHBqiwXRHd9+IriPvffpa4YBCE0I5grCFMRlSGFoF4DMt3ffDUtXLPfPxyzcEEADGnoNH01gWFLNmChQhgTJEOqiKQIQEAiPNU09+Zf3jfZNnH3yY9mVWasoFL16sMWVm3gzgNO/9KiJaq6qTdlfyewNv9+f+QNCGPz8qIgLgaFVdVK83egcGBk25UtWBel9f/4Q1x931yFUbYLWNIxgOoDYgDSJYE6IB8CEEjFKg1D2QdscVfHn9r/EaB+YeAdx8B9z0+Sgz8HxgeR6AMVB6hgzaVMk3Q/2JSQHvJOra+GTXlMPmfEi6o+d87NpTLyTeN5j5ZWae6b3fV0RuIaKZqmr3ZJ33BNzuAO4G0B7vMfOQiNyqzcBN8t7fN1QuN0pDJVQqJe2v9u2oTt9w0l0P/uNz3iQjghA2CMmEGXgOCSYDIqJuAk4AgHrDf7We6u/uPx97zO6x13fl1tyOtfucqRcXM+ZFAHNAmA2iu4gwRkBKos0jAVXy4vKvrHvslWlHHHZk2m1eQKJ5VfXOOauqG4Mg6FXVj4nIalVdpKoHqSrtsrzYed1VXAHsDaQ9caAQ0S0iMoqIPkBEDzWSZHWlXI6HBkvBUKWsQ2nf5uSA7SfeueTqFxPUxtpQAxMSmxBqAhKTBZhoBYALAUCBW3ZU/D6Lz8E1e8NprwACwKQv4nf1fvlUMWsJwEgC5oDpIVJ0EhGrJ6sAICCXuvYVqx8uzXj/YZPSWFbWelyHeA/nPRLvqwxa3XRN4COqugrNKPwx2ozifxVww1y3K4CvA95WAHdQ8xWHDwJY4b1/tlwupwNDVVTKQ9rfP6j19h3dsv+Ow29bdEWvUmO0CWBshowJCTZL3kQAW1pPTb1noPTK9oG0no7Cp9b/7LWi+6YAXP8zuMnn4rFG4kfnQ3MYgIgIU5jxDCmKCigBpE1xZlEfvPDSErffrFkU7BNQpSutxQ1PLo6zSerFi9RV/CvMXFXVQ1R1H1VdhGaIbxnAzgQ5u4vtLsUx8yMA7mPmbQAOJKI2VV2XJMlLtVqtViqVaLBUlUqpn0vloTofOhBVMptzv1h4dd4Yn7cR1GSJwwhiQhIbIjUBthBwJoC8ElzvUHqzKL5+/+l4zQuGu9Kbyplw4m04Ix/xjI68+W6r2gZifdI1dFSaEEtdOW2AJYG6hnqXEMaOnL7ptGO/+L5kjVks2/JjM5nIZKJAoihLmUyIIAjIGANjTEBEHSIyWUQ6RWSdqm5V1YqIpC3RDImoQETjiGgKM5eIaKOIDKpq4r2Hcw6NRgO1egzvUq3V6l5Hxhuys9OPP7T0lke7tj41nQNiG0FtBmojeBMR2yzIRNhKQh9U6L6kkMGq/7t6Ii8uXoDfvRE2bzprx0n/hc93FLiQi8x1zYq0CdAHvcdkV4V3Dupi9b6OgosR+wRGvU3PPuXSHcXcPiMGnvAvcJIZlwsjG2UzMESUzWa16SExZGxLGFS9sVbFK5SUAGBYWYoIMzN5BbnUgSCaph5xXCfvvSZJouVaw1NWejrfL3NK1a07frHwmpFsXcgRvA3hTRahNeRsHmKaXpZtIDoa0P0AoBb7SwZqEt+/AP/6ZnD5g/LGnHwbvtlZCAYzAYbzJwwo4U5xOl0aUB8jcDHUxUSuoQ4pJE0gmbCt9vFTLm4UM2NHDCxNlidDweiQOAyCUDkwFLBBEFhSZrVEqkDzHLEVAiA6PFBFE0pFkjhS9YjjVJ1Lkfg0sZ3SO+rI8NBSo7vvznuuz8S+lDMhwBbWhmRtVr3JgmwAmAhqAlolij+h5svfqMW4ZKiaFu49F1e/WUz+4MxFJ92GS3MR246M+bYSGEAizD8mJ4d6p+oa8L4OcQnUJzA+hhWnqU+gUdA2cPKxnylNHj/rmOrW9N7+F5JGOiQjyXIYcgC2zRejiVXFw5Np5Y3xMGxgxBMJPMSlFHtPUI1NG/eNmhNm8uODUzZse+nB+x78WVs9KXXaDMgYspyBNyG8iQATwIRZwIawYPOCQj4LICSFDNX9V6qJ5O5bgH/8Q/D4o3JnnfhzfC6yvM/IdvPXADpaLd0KoaJPNS+xmjSF1QYkTeEkVfYpGR8j9Q5WRKvjRkztPf5DC3j0iCkn+AQvlDdUu6rbXaPWn5KrCEEErTwXTTKALbDmRgSaGxNk26bmppoQc7p7ux546PE7ZHvfutHGUJ4DOGMRmEi9sSQcwgYR2GTgOCRvDFXVaJUU81sA9PcM+X92Trru+yT+8w/F4o/O3nbyrTiaGF8cUwgOIMZRreZegerDgB6YJiQSw0uqgYsh3sFrjMB5eE1gfAovHka9pjaM+ke2TxiaNnWujBkzOcxnO/KFXKHNBpnRAODSRm+lVh6q1odqPT0bkjXrnuW+oS3tLo1HsKGADIQDsAnhjEFAFgmHsDYCmYBSG4BMRgMQvQTQcYBOBwBVPN5TStd6hxvuPx9L/xgc3lL6u5N+hpGwuHl0u33a2N/nDiTSXxBIRHWCNMilMdQ7DSVF6h1YUxXvyKhD6h0CCKCCVLxa9YASKYlyK/AOIJAyCUFBDGImB4KlEEoMbywCCtQbQ8QhxFiEJqDYWLDJakBEm4g1UKFPDI/Rq16xY9AdZQzOXzgf/X8sBm85AeM5t8P0eXwtItYRbfZToOavCyDxKj81RCPgaKJ3iL1TAw9xCVgdvHcw6uBVm/pNvQIKpwJV2pkKBQCEFKoMYoKFITVGQQxPBsZYeLIwNoQQw3BAjiNEzNioQKzAebQzkJRW9lXcbXEqctx5uOryYUv1R9LblkP1+JsxjS1+MDJn7wkDuhKEHACQQqD4OUgExJPFq/EpqTglcXDqEXoPJYETDwbgROBVAQY7ABCIJQKYYQBYZogaWGMAMkhhEJiQPLMaG5BTlvWUsgXjvJahAxS1RqpfH6i5eYjxhfs/i7clj+rbm8VXQSf/HB8T4LOj2uwzgaF/0GZ2oeHuVqjq48zIQzHee4QiSLUZgwN4kDYdt0Kkqq38BM1XhYnAMMwKGDQ979y0rERIRbENQJWIPgDorF0m2Ei9Xt0/5N4njH+//zzc9XamRH5H0iAffiOC9gLOVeD8kXl7bxjyxYC+OqMv0VaoPsCEukAigNqg1EEEFlWBQKHUFC9SBoOYiEUhRDoIaInBiSgyBDpJoeN2m9qG2Mv1/SV3iir+s1zFbc9chLc97vgdzWR+uYIfugUnC/C3keUlHQXTaQiX7LUCox9en1XwIBENCqTcvM1FVe0gSAcMzYVgxN6a8IrrBit+IHFyrCF850Orcf/ll781Pfd69K7l0j/mJxhtLb4+ot2uDy3t1T30Vihxeml/2U1WxpVLPol3PA088O7/MwI6/ib819j2YDOb154vvBVSxfXdA+nEBz6Ns4G3T8e9Eb3mUOkdJsW++NT2UjpHVO/V5vrvrRfVh7f3pTNLdZyLdxE84N0HEEtOgMsRzukdcBUV2vRWwYOnbTuG3HZXw4J3wki8Eb2uQ/WdojW/RLz/n+CluKaZTMhzm4eJwB9aFHADFf1X7+X6h/4MG9+LubzrHDhM934KLyhoaSPB3/yx3Nco42+811UPfBbvWvD67vSu/0eb3enEn/K17RkeNExXvPHTvyfxeuVQQ0be9zn50hs//c7Re8aBw3T/Z+TScl3niuBm9cCbLLeXGjr3mA3yl+/1+N9zAEHQ6oA/rxLLBPF49o1Fl54vxVJ08Ge/kwvkN0vvPYAAHv8K6ur8BbVEnlNF6XUArNQS/ziJv2jJ5/Cm07W/k/SeWOE9UddvUJ5+pimpYhODTtyT1Y29fsOrv2fxhXj+vR3t7+l/BQcO0z2fc0ucEyeil+7OfV7xFYXI4gvx4Hs9zl3pPbfCeyA67cfmFiaziVX/BgCUcL1XGf27z/vz8S7vNN6I3t23oN8caW0//+lcF/0PC+4VIBJgZm2aPw3/y8AD/peJ8DAtOQEuZLfAQ0sK7Q0rbv6SE/Yen/L/017ojH8LZ5/xb+Hs93ocr0f/D6s769KBP+5xAAAAAElFTkSuQmCC\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic3bx5uF1Flff/WVV77zPeIQMJIYQxYRRBpBGcQFEEbVQQUXB6xW5tWx9+Cm07IYIitiJog2P7qu3UCN22aDs0KIIyg0CYyUhCyHiTmzucce+qtd4/zrkhQIIogz6/9Tz1nHP22buG715D1VpVS/gLktnZjg3P2wGz3RC/N9hBCHtjMgOxGjDZv3UAkyZim4EHQO4i2j3UkxXUb9kkcrb+pcYgz3aDNvLjOah7BSZvRnwLX/8D2axILM0Dtx9ODgGGt/P4GNgtqD6A764i3+iJ44dC9CD/jaRXyqzXrHs2x/OsAGhrL9sB8W/B7ARKwz/H7TAE2btAZj89DbAW6X4HHRknnzgW7KdU5fsyeMKmp6X+J6BnDEAzhLWXHYz4c3GlG8l2HgL3frDsmWqzR5KDfpnOykkIL8D4OHPecIcI9oy09kxUaut//CKCfp7qtMuwwb8DnrOd5nOcXIfJCryAOgEpASUwh5HiBMwKEAW6YF1chIghthtqL97uSxG5C8a/TXvsBLx9VGa/8Yane6xPK4C2/kd7EuWblIZ+itU+BMx93E1OFmLchqQpTmaDA0kAlyDSwSwizkAFfN84RAfOQASLCVACDVgAESOGDZjmSDwEtYO20bVVaPMCionjiPoe2eXNy56uMT8tAJp9I2X10GfxyQTJ4GtADn3UDd6NYv5niKtgyQxcYjinkCSYi3gHikeSLhDwXjHzTNlWB4hEYnRAgoUSmIIqxAQsYEFQA/JRNHZw+lqiTn90R/VGYuNKQl5j/cTH5JD3FE917E8ZQFv23b0oJf9GNnQd8PFH1y7rkORKLJ2BTxJcAqQOSQyXGEiC+IB4wZxHXMABeIeZQ3q/MBQRhdiDNGqCaMSiYTFBKIiF64FYKBbAigKLD6PFsYjt+uhOcwHdiUMRe5fMe8uSpzL+pwSgrfje+/CyK1n1dcBeW/7wMoZll4Kfhy95SARXMsSDSxxkIInifIK4gHpH6kAlggjOOwTBJEFV8FIQDcQCGIh6ighOFdMELQKYYF1Bg0KgB2ZuxG4kFqvw+cmoDG7V/cXk7Z9iulx2efvX/1wM/iwA7bLLPIe1v4m4RSTJuWDJI/+675OUB3ClCmSC9yA1cKn1dF3mSFLDRJEsAYk9wJxSTEzQGW3TXlEQC6G7sde/0gzDZ0Zlt5Ty9Arp4GBfnBWCgxghOkIhkBsxGK4QYgs0gHZAQxPtNLH41q2GH4jhNFRfwrzK20ROis84gLbkohLp4C/IuAXso1v+UFmLlK4gLc/Bl4AMfEWQDHzZIAFJhKQMLgWzhMayjTz0kyYbrt2T2Nq3a5WFE3HWqgYzNxeU81ao5ADVpJ2ldLI6G6YN+o3zStI+CF+9n1kvWcYub6hR330mIgEtIHSAYFgOVkDREegYmoO2QfOHCJ3X4thqDirn0rUX0Kr9rex/Uv6MAWhLLiqRDPwPafdBxN79SC3yS1y9S5JVoQpSEnylB5wrgSvTAzCt0Vqzmfs/32By8cs2xZ2vXGJHjo/KnlVz1aEsLZsXKVQkpIlXTHo6T8wFjU5iTELULFpEQmN8Gsub87lq2gy/5pUM7ftb9jtjgNJO07DQQHPBerMeYqsnztaGmBvabhNbVYivemRwfJWitADktbL7OztPO4C25KISvnolWWMN8OZHasj+L5Luih9QfAWSmvVEtwquAi4DyWay6aYHuO/8A1phYOkd9sbVTbdgVlYqJVk5wycpWZaRJY7E+2i44L2Y0Zv8CkgMCMQ0xOCKGAndnCJG8m5ueacba7pkw/Pksp2rvrkH+3/kXqY9fx+cbiA0HbEDdIzYBmvRE+12l7y1GRffsRUj/JBubR6xdbQsOK37tAFol13mOXjzzymNrQL+/pGnS1/FZXvg64IfAKkqSTnBVRRXEaQ8g7HFK7jnU/NHde7tC5N3RpKB4WqpKuVq2cpZSpJlkvrMSqVEvE8ty9JClSJJJIr44BwWY0xjNC9iWZ4HH2OUdrsjzpm1Wi3X6RTW7XZpdNviwsToQfodP92tPpj9z1nG8Pxd0M4mYtugEygaGdpWQgO05aCxFI3/+Mhg5WsUQ3vx0Npj5GVnh6cHwCVfv4x0ZAViH9py0WcXIpUDcPVIOiBIFZIauKrgKg6z2Sw8c0m72XK3uA+MuMq06dVqzSqlTGq1uqVp6iuVsqRpGtIsM++ceO8NEQQi0AGmuKAElAEfYxTAQowUeZBu0U3zTkc7nVzzvGOtVpdGqyHa3rTpUPvSjpVyreDgc/dGZB3aVrRlFE3QlmBtoxhXtPUQlr/nEVTceRQzd5c9/+GUpwygLf7Ke0jHykjrS1suavpV0vqeuDqkdUEGlKTsSQYUqcxmcvky7v38c+6yt/xqvHTwvEp90OqVTKrVitVqNcrVasiyTBLnUhEBGAXW9ssqYD2QA1MT3bQP4g7APGBOv0w3M4kxxm5RxG67nUxOTkq7ndPO29YYb9pQ55a1z3U/Opb9P3wXA7vtgbbXow1HbBk6aYSGoA2hmFgK+ggnxoEPYENR5r/3y382gHb/xQeQFm/Gr/0ImOuD9zXS6p4kg4ofBKkJyYD1gCzty9IfXlVsumuXm7NPrC6Xh6sDA1XJyhWmDQ1ampa0XM689z4FNgP3A0uBdr8vSk/veXpceFj/9y39/6ccAq7/vQLsCewHDMUYQ7fbtWaz6VqtXJutCWt3OtJubBo7rPjMXsmMA5cz/60vJ3TuJzQEm1TiREJsRsKEElqrcEWfE0UJcz4P2Q9kwfvv/ZMBtD98I6XW+QXZkj0Q9uzdLZcgA8OkdYdMF5KakgwK1AWfPof7Lr52vJk27qufVq0PDKTltOoGBqo2OFi3crmszrkB4CHgAXpc5vpgSf/7MFAD9gVe0AcHYAlwK3Af0AQm+gDrVmV2H8i5ZtZpt9s2MdGwRqNJq9WUVqsZ9m1f1BqqxAr7v++laH43sWHEhhAaRhjzMBkJExNgJ/XhWUK+YDVx9FWy/9nbnN4k27oIQK39FUqLFmLhlX1beB+U67jMIRlIGiF1kApen8PCzy0cYZ91Dw2/dadp9ZqrVWtFrVZLa7UyWZaVRGQc+D2wiR73zARKqhqccwqIquKcmzSz14rImUC135uWmZ0nIrf0fw+oqjjnhJ54d4AGPU6dJSL7VqvVoSRJOlmWSKmU+KxSssVjH6zOa/5g8453nn8bzz3tEHzpDrTrECeQKnjBJSmhdQ+izwEWkNx/Oez9ReB924Jpmxxo91+wl0rjjc4vO7d3wQKu8kP84CyyIUHqgh82/ICQVA7k3m9evybstXb98FtnD9Xrrj5Qp5RlWq/Xnfd+AFgILANSVfVAHZjVf4E1EZlhZs8VEWdm3xCRD/QBfqSjIhtU9SIR+XszUxG5T0Q2quokPV25EZjsv4wA7AI838wajUaDZrujk+NjyWSzyZzJ/3pwTnrffPb5u79B2wsJEylxUonjkTAhxPENaOcURBIADfM/6bT+H7L/6Uv/KIBmiN1z/tWS3TqESM81JMlXSQb3QIYgGwQ/CH5QoDaflT+7cXNjYGLl4Lt3qA/W3WC9rtVqxdfr9QxIVPVeYKNzzoCOqjpVLSdJUg0huCRJusC4qp5FT7wPAlYCP1XVkf5zs4DXAbsCtwN7OOfO7nNiRk+EWzHGwntvzrlSn0N3APYzszjZbE50Wu1sfGKcZqNtu05+ffO0aitlj+NeSphcTGyATkCcgDhmhInlWJziutst/E1D9v2nIx/rmH08gPd+7ijSpc/BRnpW1+RWXGUd6WCGH+5xXjJo+MFhRpcu6q6+a2jJzHO65UpdBgeqbmBgwMrlciYiqqqLnXOFqk7vc4WPMRpQTZJkB1U9FEhF5Fwzu9DMbnfO/VBEXqyqrwGmHKW5c+4XZnatqr5dRA7y3p+uqh8DVFVvE5H1QAtwIhLo6dSNzrkKsEeMMQ0h5GNjY9rpFH5iYizutemTrjTvuS0G91iANcYJE544HoljQpyMxOZcsAN7SM3+AHHPhbLvP/9ua7zc47jP7OPEhz+KdcA64MIdSJKBBxFFpPcGigmx1Tc/f9G0syZrtZofHKi5en3AyuVy0gfveufc5qIo6mZWAWaZ2Rzvfdl7/3AI4SozGzCztqqeG0L4ELCPql4QYzzezFRVH1DVB8xMY4zHq+qFwHwzOyOEcF6Msamqg865K2KMK0XE05vaTDOzUgihqqojqnqD9z5kWZYMDAwmpVJGvT4gS6af1baHbzyY0FQwD67nzBVngMNz4xYcbOXHLMRzzEy2CyB3nfdicQ/8FOvMRrsQu78A2xWJYCaY6zGwxf1Y+quwbNrp19QHB6u1Wo16vUalUk5EZGdVXaiqQ3meO+/9BhFZrqqr6OnAE83sH/ttV4HvAB3n3Plmttg5d6Zz7sNm9i0zu69fvqWqH3bOnWlmy83sAhFpiMh/0JtgO+/9e1X1TTHGATNbb2YPOefGVbUEDKvqQmBOqZS5wcE6lXpdqgODlWXTP/Iblv48RePeOOt5wrUvpZrvDt3/7WMxS9zi/+aezx2+NWSPssImeqbY4j23XJDSeszvBAJODFHBWcrIA1c1sr1zN7DHjqVKhUqlQqlUcsA8M3tQVV8MNEVkjqoGEVkLPKCqVwEvA3Iz+yDwG+BMEfmtmV0hIifHGM81e3T8Z+p3jBHgTu/9h4qiOM4591ERuTKEcIb0JCMTkSuAGWa2v3Nujpk5MxsFEhFZ7ZybWyqVHq6pOrQaJuPOcxvNve6sjy2+l+Gdd8EkIAJ9LFHWYFMLokXvN9vzQWCLE2ILgPbA53bS7qrfi3WP7l+6GvxcJPRG4Kwn5LE1l/FVu63e4fM31CsV6pWKZVlm3vshVb0S2BBjTAG890PAAar6ahE5EjjPzOohhLO89xeaWQ6caWZnAS/vA3Wlqv5eRFqPAbEmIkeIyCtCCAeKSEdEPqaqfw/MVdXTReRCEXHAe4B6COEqEbk7y7LNIQRCCEWaprO894eWsmyTxuhDCG7NzH8s77X+jBcyML2AsBIxQ0xwKgTdCcl/h9kRwAJh/fds4blz5aAzVz+aA7vtE527fi7WXz87/wCwO+ZAVIgFuODZuOqmkfrrJirV+k5ZkkiWZWRZtqOZbYgxHglUnXOJiKwMIdyRZdnVIYTXmdl6EXm7qq52zn1BRD6gqv8CnAtc65z7sarua2avF5GjeQyJCH3R/IaZLXLOvSHGeB498f+AmV2oqqu89/8nxjgmIrPSNL0qhDAjhPBiM9sdiCGEpvd+JE3TOTHGDZVyOSpUNtVe/fMZI9cNMn32LliMmBnmIs4ghnshHtHrybU7Iq9/A3DRFgDNEL1l/U6uUhzRN8wbUD+vF8PAepecoHEandburbkvv7mSpZTLFSuXywnQCSFc1xezIefcc83sOOCQoig+18fgy2Z2gZl92cyON7MvAb9wzl2vqqfHGKfW2rmqLnTOPaiqK1XVJUkyD9iD3grlPX0wNwIfU9WXmNmXzGyVmf1cRN7rnDtdVS+MMTpVfTcwA/gVsFBExsyscM4dVy6XnRmxiGqTw6/cYcbqKw9nmo6CbEREURFIDPW7IGETyAwIL9PO+oZZ7516gLOP/budTZfuIXp3FT89Q9yvcaUhpNTzKLuy4TJlcmLZWPqi+2P9wIFqtUq5XDLv/d5mdqeqntLXQaNm9gBwrIisMbN5fZ10sHPuJ8C7gbu9919X1XeZ2dH0ph8Xq+p/A8tFZJr1RObFIvICM9vVzB4Efq6ql5rZXSLyfOBvRWRXVf2EiMxxzh3vnPt2jPEdgJjZcF+kZznnvuGcK6vqwar6ehG5XUSeZ6YbBcOMxIrx20v5is2UXA0NPYesRgfqIf4Bs5l0H/yDWHUZq45eec63/jDpALTg1c7dNouox9Nafh1KgKRnrhWPakKINZrtlzSnv7ZWq5UlTb1kWVYG1hZFsczMFpnZ61X1VBFJrfeKvm1mL+nruLkhhKPN7MNmdngI4Twzu8Y59wHgWjP7sIhcaGbvV9WXqGpZVVv9UlXVl6rqaX0996GpZ/v68jwze4GZfTiE8BpgjpmdZWYvCSF818wwszSE8E4zO9HMVqnqMhFZm2VZ4n0qWVay1ow31Gg0DydaBTMH3qHiEGeoy2kuv4YY3wy3zUL1WOjLq133ritxP3w+2HSgQTrwc8p7DeBqDl/ueVxi1o7Nimza5VNFqVqlkmWSpulBqvrrT3/603bTTTed6pwrPVZ3/TWRqnZf/epX3/ye97znZzHGwVKpNEtEXhVCuL3b7dLpdGzmio9XZKBb4PIKoQXSNEKrS3dJh2LytfRiFqMWTvmDe+m3X5WYne30+kUbnFkvCC38L0U+HdZ2KO9uSMxw0Wh3xyanvXHCe79TImLee8ys1O125998881HOec46aSTfrd06dIlCxYsWHDvvfcu2n///fdutVqNPM8LYNqvf/3rI733d5xwwgnjU4MaGRnZuHHjxk2zZ8/eYfr06dMvvfTS/VV12pve9KbrZWrSvhWJCHfeeWdz8eLFr5kxY8YtL3/5y1t9cOyBBx5YMn/+/N2yLMtGR0c3XXHFFUclSTI8MDBw1THHHON/+ctf2hVXXPGyer1+/ymnnHJkURSrsiwrpWlqeZ6Lc07Gh49bOty4ZJB6UcXFnh+gvcbQfAbGlcDrwaabdcfMznYJt66Yha2+A3hLr4tuBGQIbWe0V7Yp7xlJY5mQPjfUD16YJok453DOzVTVmycnJ1/kvXdFUXROOumkF8cYJ0XkkBNPPPEgM/secBpwu4hc8etf//rI4447bvyEE07YDNwAvA04cqt535L/+Z//WTk5OZmedNJJP6PnsgJIY4xVYHfgqGOPPbZ26qmn8sY3vjE/4ogjqsB/xhhf6r1/N4D3/lxVPfG6667b0O12hy+++OJ9yuXywAknnHD9qaeeuujHP/7x604++eQlIvJKVf2ZiMzy3m/IshJh2iE1Ri/dn8I2o/lGOg8NQLeKCuDWYr04l0tX38rNuoOjU+zpdHHSW2EAKiWE0PO2FTW6K0qE8fWmyXBWqdfTNLM0TUVV91fVDVtzhqp+xjl3HHC+mVXN7FQz+5CZHaSqHwVYt27dGjN7jZmdr6r7hBC+r6qnq+q5ZrYsy7LEOSchhAtCCF/ql/PN7BwzO8HM/tBut7/Sr2uVqh5iZuc7514FnA98N8Z4ppnNz/M8AmRZNg04X0SOPeWUU64WkTkrVqy4sq8bVwP7eO/FewdpdQjxMwgTG2g9tAMxr6BqGBGTdAtO4QFH7ua7qLoXrjHvEQBtGmopph6LghZCc023M/yCr5mZgk2tCBpm1th61aCqJ5vZ9WZ2DvAZoGlm58cY/11V/y/ATjvtNBe4S1VPB74FvBG4EDhTVY9xzomqmpn9j6r+SFV/FGP8jZnd1Tcmx1er1dMAhoaGZqvqN/ov4D5V/ZCqvkNVvxljvDTLMm9mOOcuNLNPm9mNr3zlK1+oqvHKK698oZmpmY2LyIRzzkTEvEinW33ORbTXdqEbUQUjwUiINn0LTtaYi9meiWg8ACkO6GPQQaRCtBwfCyIlEEOHO6jf1bmkSJIkAENmtjaE8KrR0dHvAh/pc+FewMMxxt+IyDkicq2q/peIvK//tlm+fPnyGOMxwIV9Sz1mZpc75xYCRbPZfDO9KcjuZlYDMLMNZrZSRH6rqqsnJiYAzldVAd7br2d1COHMNE33VtW/FxFijIv6n2c6537rnFNVPRxYd9ddd80xs/NDCMc659aratk5twFI1dUXEJM2lnchRlwUMI+TKpCjZEjYT0PsJBJ1fxwHA2ByD6KG8wENAWcZBYKUM4b2ayeJK8eolqZJbma5qh5YrVbX98FLb7jhhi8fcsghpyZJsjbG+Enn3Dn9acyXVRURef+ee+65B70A0b/EGDc5514A/G0I4c0A1Wr1rna7japuvadwd9VHtkEPDAxM9EV4tYicEULY0Xv/9yJyboyRGOMX77vvPpfn+SnOObrd7qdKpdJbzGxP4JMzZsw4ft26dTNijEc65w4ErnbOdfO8GEqSxJLpB25ktFzDaUSDoNERDEQdjvsxDgQ7VFRDglgZo95TZPogJlUsRpCIRkEAqQ672ryuiQQRSVV1DzObEJH/k2XZJ/uK21988cXv74vyHqr6MTMbT9O0VSqVjqHn9OTyyy/fddWqVfckSXJ4lmU2PDxMjPFmVV3TbDZHNm3a9Gag+pOf/OSHSZL4iYmJep7n3YmJCdm8eXPsdDph06ZNOwMvufLKK1/5+9///tA8z2tFUQB0RCTz3n8QwLmesynP83enaTrovf+AmV04f/7862666aY9RWSWiLwjxniqiOyRJH5pURQastkxteowRW6g1tsVZgZiRHsIOBBjEI2VBNXe3kUAtSaQoma4QsEJFoQ0rbq0hjmX9yTKhoHRWq32yRUrVnyn2Wx+qlqt0g9R4pyT/pywBAx1u48E+WOMu91yyy27sR1Kkt7y/PLLL3/L9u6ZIhGZ3e12Z2/93LZo2bJl//2c5zxnLvAl4L4sy+7tdDoHxxg/MzAw8KlOp7PUzIaT3to+NwYMyUpYXiBqRAOHEdWDTiJTLkHFoYVsUYxCCwgQlRiFEAQtDEmHVBLnvS9UNdCLV7SA3efOnfuqkZER1qxZQ6PR2OJ6+muiJEkOFZHXmdmFIvK5I488UkZGRkoDAwOvA3YVkWY/LlOYmSVpDciGevtrQi/Or7FvPGg/YnCDc72NnvRKdEqkidDEaKK2DmMN4hLvk2az2eyISINe7GIDgPd+N4CiKNi4cSMPP/wwo6Oj9EXqr4IWLVp0D/B3RVG8Kc/z7+y9995Lgc7atWt9/5YNwKoYY8PMmnnezXGuRKBFpAk0UBqYTqDoFrxMSTDskTCJ1VCGCQgmZg4vZoZnMoa8UqkMls0siTHuY2YPbauzMUYmJiaYmJggyzJqtRq1Wu0JReyZoKIoaLVaNJtNfvCDH5RGRkZeOmfOnN/ssMMOyxcsWPBGEVl37bXXPnDyySc/L8Y4R0T2EZGFIhLE2ySiE5jMwBCi9QL+Ig7byotvWALxkXi/yRCQRMU5jFhYCXGaxDgeuk2DctL39TXpBcCfkPI8J89zNm/eTJqmU55rSqXS0w5oCIH+epZOp/MoCdiwYcPJ3//+9x/3zNe//vWFJ598MmY2ADRU1RVF4V0+TmahEYxpBBVBAog6ByJWe4ThIglqG3CyCmwepkNmKIZEwUV1DlTF8gZx3GBGGmN0fZ3x+B34j9Bm59xD9BjdqequRVEMbz2oJElI05QkSciyjCRJcM6RJAkissWCAqgqU/NIVSWEQFEU9L3M5Hk+NbnfFq1yzo1OratjjPvTC8azadOmV/bvmWNmDeecxBgTjZMSi9CIQYcR5zykeFUiINQQAZEHzeLaxDTeLUYKzEPcAWLcY6ZmipppjMFI8tEGzeXW9TtnWZYCrFfVA+jtBngUOefuEhEdHh5ePnPmTN2wYYMbHx8HWNV/BmDL4J9BUu/9LWaWzJ49e/nAwIB7+OGHa51OR60XtdsdyNevX39zrVY72Dl3dwghiTGaH1+UabGpFQszEg3OgSgizjnE9u8bkdscdqczC/ejyeL+Mm6WYQ3MopmoBefMJOk21laTxuKKmbrY83aPmlkSQrgeoF6v/wxARO4WkearXvWqHdI0PcE5d2KpVDrhqKOOmiEiLefcfc8kYluT9/7mUqk0/qIXvWh/7/1JaZqeOG3atGOPPfbYrohMAmumTZt23ZIlSxaaWaqqm83MVFVKnQdrne66ajRJXHSiEcQkGjaO0lvOkSxB7QHnE1lCHFyLWc+3H2l5xKtapqYuRpNue6ySFSsXhKCxv05tAHS73dWPEZ3k2GOPnT4yMjL3C1/4AqVSic9+9rOMjIzsfNRRRw3S24X1bJysXGlmpec973kvnT59euUTn/gE8+bN4/TTT+fee+89/MADD1zhnFvfaDRmttvtrK8eGj3VECzLVy0IjfGKRiOqOtRSU0vFaE3hRJi2nsKWJ2xuL9fa7Nc7t7HXtLNGjOwgaoWpYEoSY3emaPeAPG//CkoVEcmdc4QQ5l166aVXNxqNI5MkuQ6we++99yWHHXYYt912G7vvvjs33XQTCxYs4NZbb91XRK53zt1mZjOfcPhPkURk0+zZsx9cuXLlwcPDw6xcuZKhoSHuuusuDj30UFqt1jELFy5cVhTFXO990teteVEUxJi3he4+MXYjgDnUjAg4orWm9nJo2GGmm2bLEnnrzRP6k8MPe8QS41EwkwTFFIsaRIr25qt8vmY8uHlV770lSbIaOOKFL3zh/y5evPhKgOc+97nV733vexx++OEsXbqUiYkJ5s2bx2GHHcbvfvc7jj322MV33nnnI6HUp2nSLfL4PVJHH3307BtuuIHjjz+eK664gvHxcV7xilcwOTnJ9ddfnx1wwAE3NZtNt+uuu74GeKgoCosxGq1VY3lr9CoN7CjO1KI4ExEUxZNN4SSUXiwvu+YT/bCbbRLldoSDMTnSO1seogU1cTEXb2YyvvaOscHB31dG0zdrjFG893cDx+yyyy4bP/OZz5yqqqgqt9xyCz/72c94xzveQQiBgw46iO9973ssWLCA973vfe+cum/Kom5tXacAfeznFEBbfzrnEJFHfe87erdY8F/96lf86Ec/4rTTTuOaa66h0WhwzTXXcPDBB/O2t73tnd1u99uqOlNVfwVYu92VOe3rapMjCyei2lwfxXDOnMTgRQSTl4OB8QczVkJvcyPnvGnuJDI+ihWvAKaJ2kIzqmoiqhBUpNMZr8/ace8j1snzljrnvHMud87tF2Ospmk6bGZOVVmwYAGXXHIJeZ6zdOlS1q9fzx133MEZZ5xBrVZ7FHjbA3Lr348t2+Pex3KhiDBnzhyuuOIKGo0Gq1atYs2aNaxfv54PV+O33AAAECZJREFUfehDJEkSut1uDZihqre3Wm2X5+3unPy3x2548Kpu6sV7QROPpIL3XkYxDu8teev/Kjrjl+dctnpF71W1uFnjzvVHnIV+rSBCRDDBDDWl3GmM/LrUWTbS6XQkxqiqehuwT7fb/Y+pAe68885cfPHFpGmKc45SqcQFF1zAnDlzHgfSE5W+W2pLeTLPPBbsww47jLPOOot2u02e5+y1115cdNFFZFlGv8/7qOqt3W5Xut3cyvmDGzutkSs0UlLFMHEoiIlhbJjCR8PcIWLj1p4o90kvPegaSe/bC2wOcLsZY50CaRdYNzfJI6gbWL3LIe+euyh9+4ZSqSKDg3UnIieq6n3lcnm3vsf4cQP+YwA8lgufSISfSGwfW7z32/s93ul0NojIfFX9z0ajRbPZtP35wZyVt/zbKomTcyspVkqFSoaWUkSEGcCBmKy2uN9id9LCl8NWu7NU9UKsdHEf5YMFNgjgpLcBFpCiPbkTYbKgvXp9nnes3W6rmd0I7Nduty/dlsg9FpjtgbctDvxjL2B79W2v7anS6XQuN7MFwPV5nlur1RLXfXi9FRPtojM5B3D9bXwighNYh3Fgb65culgtfmEKty0A+qHWFZrvNG/LPEddI3WGiLkE8JiKt/TB2340viD9/X7tdtva7bZ18/xhM5swszfGGK/bnu7a3oC2J7pP9P2JxPaJ9KeZEUL4dYzxFBEZy/N89cTEpO902rpP6boDlt98adNjPsE0wSQRIxEDle4ULhp3nu/Xt696HIDy6qVd1Asql/ZMdXy7F5ssiUURvDcRr1i3NT5DWxsmKvnitZONhms1m1oUxdVmVu92u0NmNj4lftsb2JPVgU9WBTwWzG2B2O/ThjzPZ5tZmuf5NZONhmu22jpkS9fE9sho0R4fRhDvRRMxSxLDY2OIvq0nmXIJKm05bWn3cQACOHFna5hzX1+MM8ytS5yId+AEBMErsui671b3Gbjn2G6n02w0GrRarY6ZXQUc0Ol0rrZetOsJOWFbYG5Ld/4xffpk2zGzTp7nd5rZc4HfdDqdvNloWrc12dqrfs+rF1373YokvU2ECeC9+FTEsGQjPbcfxLlLXJJ/+lGYPcr0n3LPerS8D/DbHhfaWxOxsVKCeYdkHhMvRAvDS2/58Y0H1X9fGZ9o0Gw26Xa7m+htAH99nuc/2NoIbP35ZET6sRZ4ayCfTB3barvb7f48xvhK4A+NRmt0YmLSJpsNe/70m+tLbrrsRrUwLe0fB08F6Z2rtzGI7+jpPq6MoTRfTlo6sl0AATq+/U+az76h/1AVlZg6o5xg3uNSj6WefHz9kj1orrCdSsvWTUxMyOjoZtrt9lLgTjN7ewjhB8CfxYl/zpTlCUpeFMUlZnYicHur1Vo+OTnpGo0GOyVLVkvzIR1bt3yPzBNTJ5Y5LHGQelPE5T1JBHTObb7onvFYvB4HYO3kVWuIpRSTb/aXLSd6WJQ6c5nHSg4p9xqS+67+9tCe9QcPk+66NZOTEzI6Okqz1VpsZjer6luLovjZ1jrxyXLlE80Dnwy3TX0C62KMv1PVk4GbGo32srGxMRkd22x0143sOfTQYXf+5t/qpQQyJ5qmWOKQLDHxxiLM3tTXfV/TIknlnSselxXpcQACuEp+Tsx3HERp9Pz/7kWZp1tySEkg8bjUiROscsvln1v7kl2Wvrzb2LhhbGxSxsfGtNlsPqSqV5jZ60IIK1X1uq3r/1PB/BNBm6Lr8zzfqKqvUNX/nZxsPjw2ttmNj08Smps2vnju4iNv+cnn1qVi1VRESg5KhpQTXObIUXdEP/bRiPnsHZzaJ7aJ1bYuykkPt73xbbR+Zu8N2P6ibqycQrmMlZ1ZKYVyIi6VOHzzjz9/99ELlh8dWhvXjI6OsmnTJhsbG5sIIfwXsIOqHhRj/Da9I1mPom0BsD0An+iZrWg8hPDdEMLzgel5nv98fHx8cnRs1MbHN2unuWHkmL0ffMXNl3/+LtEwVErFlRJcmpiVykgpRcXcJrB9eqJbP9OL/5q8c8U2T7E/4WnN8J2df+iTtQKc3If7KyGyf7ONNbqUOoXQbFtoBaJKZc3hJ33igF89sOPVOcNzhoYGQpqWy/V6RUul8qCZHqGqS4CFMcZTVHvO2a0t67asLvC41cTUiuIxn+qc+w/ghc65nUTkd3mej7dardhqtbLJZlMzHXv41c/Z8MqbL/nMIrHG9EqCr1TEVVOjVibUM0g89wFTx14viWHHkLxz9du3h5Hf3h8A57x++i9jrLzV+ZZgzACe6+B3HqYhaIw4J+JMkbwIQyvuvHryZS9//i6tXB9YuSFMC0Xs5W2KoeVElgCY2dFmttjMfk7v8M1g//qWds1sm56XKRAfc20N8J/0gvgvBO4piuKOZrMZx8YmmZycYPPmUXYb3LT+pXtu+ptrf/iJEbHujEpKUstEaplRKxNrKaTCCoR3AB6RJTGfPekle/s5Px3bbuzhjx64bn9tx92yrPigS8b+AcgQNqP8ohuZ28zxrQ7SynHNDtIulE5wxaEn/PP6WJnDT24faJfSSlKtlsvV6oCVKxmlXgCpDOypqrur6m/NbF2Mcb6qvlh7Z+keJbZbr3+dc8E5d4OILPXe7ygiR3rvV5jZgzHGVp7ntNtt2p3cmq2m73ZbjeOfN1nz3TXx1h+fv2PJaVYtOStnSK2C1lJirUxeSlmPcQwwo7e9b+gifPlf5e1rthm+fdIAAoRvzXytSHcv51rn959aCdzcDcxsdPCtHNfuIO0ca+UaWwEGZu+16pDj3vc3Ny1Nfr1oXW1WuVz2pSyxUqki5XK2dSQuU9VhM9vFzKap6gozW2NmDVUt+qcvMxGpi8iOwK5JkkyIyEOqOm5mRVC1kOd0Ojndbod2u0On0427zypWHLlv53X3XfWD69ctv3V+NcPXMmeVBKplQqWEq5eQUsZalBfSOw2PhuqHTct3J+8e+dUfw+bJZ+345vR3kbTrkE8dR1iF8LsisGujQ2xFrNUmtrvUmwXdboHP1RUvPOmfNyYDO02//BbuGu9k8yqlMlk5w7uEUlYy55A0TcQliTkRw0xxHouxd2Cot3PT+kcbxEzEQIIG0SISo1qet6UoAt08aLvb1uGyrj/hMD0gTK7ZeP2PPj+zlGhazoiVBK1lZJUSRb2CVlMk9ayldzJ+DwBi9gG01pV3b3xS2Yz+pLwx8RvDn3RZdwzrgyhsxvhJMPZq52izLVmzwFq5SbdLaEeNndyZlOutw97wwU5anz39ZzeHO9dPMCvxaZp4j08z0iQlSz1Kz/XhxHdxRFR7ESvnPIqPUTPV6LwX63aDKLEXEy6ChVDkc4Z15G8Pyw4qJtZvvOm/v1ixTqNaTpRSySXVFF/NROslpJwY1RKWeBZjnIAw1BtP9gGKclXevfmzTxaTPzlzUfzawBnOhwSXn9dPDpZj9i1DDmp2oR0Iza5qJzhrFfhuR5NOQdENTpPKwNiBx5w6MXOXfY9YtiZcef097e7IhE2XLMlSvDjn8IngncfMgllvQ7JzIoqkGsRUC4kWpCiCodadPuw2vXi/cmXBTslRIyvvvfauX/37YNGdnFbNVDJHUi67WMmIlVSpJs5XMqRWwouzuzB5J5BhqMbkDGflivzD+JMG788CECB8tfZO8cVOzsd/4pF8pz9CGOjm1DoB1+yStgq1dk6RF851g/puTtE10iLSGNpx95EDjjrFTZu928taOXcvebizfMWabvfhTV3f6UI3qqC9o6WC0ywVyiXYeUYp7rZTWpq/c3WPWsYBm9c9ePU9v7lEx9Y/uEOaUMs8oZSQVTJXZF6tZ22dK2eESkYsJf2NU0I/LwKjhPSLEb8i+YfmD/5ULP7s7G321cphEXuv98XeiL2gf3kxwu8N269VENtdF9sFaadQ6/aSDaXdSMgLfKFoEUhiJCcpjw7OnDu+497P1xk77pqV6tNr5drAoE/THUSchLy7odOcnOg2Rpub1q3M1y26zU1sXD1E6ExPElLv0MzjspRQ8iSlhKKSkpRTJ6WUolZSygmZiNyH8VKmMs2Z3BhDtlyRf83e17r1z8HhKaW/m/jywIy65N+XJP4B9JGljsiPMTSiO3cCRTt32im01A3keeF8oap57nxhWsRA0lEwJQQlMcNCz502ldqk109BEwERJHEEcSQlB6knJN6lmdOYlpzLnMZKQpZlrltJ1ZVLpB63CiPF7PgtfTT3KYv+b0TKb5F/HN/852Lw1BMwXobX9dmZmJrL9K3Agv5fOSr/jmdGVJ2bF3Q76nxRqHZy52LUmCsuj06jqu8qgjmiEtTUmEqF0vumgDlx4h2Jd2qJgHcuJk592ROT1PlSopomzpW9xiwl896tQumAvZlH0gcs1sJd4oTAxnCenP3Udko8bTlU7SvMN/NfFW9XAJ9iKmVJL/vkDzEM0V0Ldb5bqObRuagaikBWmLMQCKAuGAFDowKO3gpASbwDBJcICeI08SSJU8kceZKSpGCl1EnqNWJuBYLD7C1bsmBCC+MTMcqrfIjvlQ+y/OkY99ObhNaQ+K8ch+OdPnG3KXwco7xVY/eqyY3iqRk6xyJZUFeEqC4oFOYExaKh9JzaPSMiGOLECw6HpKKWeMyLI/XqxVMIbq1Fmk7scIP9txphxymfiUGfD3zL/3/84ulMifzMpEH+Bmls8yaMU8y535rnNOnP8rdqeA3I1eKsbSolB4NRdFgMb0Y0wcR64mXSSzggggeCw40rTIizrqlUwF5msNOj+gArPVykhR6t8P27q1x2yHt42vcdP6OZzO1sXBjmKODDqvxeEjfN4ANP0JlRRG43GBdljN5OWDCrmWNYYAizgw2mP0EdXzJ0s1NeivDZZDNXP1U990T0rOXSty8wsyuchXMrTLjgmWhDjDNQ3a1kfEr+iY3PRBuPa/PZaGSKDKR9AZc47x6KulUuwqelbrnIqe5c+SdOFJ4+HffHaJse6WeKBKwyyVtDoQca/Eb7qbSfarHItVbovpUB3vxsggfPMoAAcjahW+KNVlhDlZVPGcDIWjFbF5U3yTNgJP7oeJ7tBqdo9F84wMOpivwjsmWS+6dSMLUvpsJ3Bz7CdpMkPpP0FwMQYPyznByUHXFy4Z9VgdrpzjE+7aN8+2nu2pOmvyiAAJvO44tmboNh5/1pT8qnPTpj+se3nRjx2aK/OIBmyKbP8FMzN6bY257MM+LkMjGtzQy89pmc4z0ZetaNyGNJBGtv4k2gczVy+5MwHHcRdVoROOkvDR78FQAIMO+LtIm8zYndr8bodqcrRkuwG73jXTudTeuP1/zM019chLemtWdzpCkvir2EZI8jBx9xCTfNOYvfbev/vwT9VXDgFM05m2sMgiinP477lNMd6F8TePBXxoHQW+6tOYsfFsrDpnyof/GiJGHmzp/mrc/2SuOP0bN7CvpJkICZ4+2rjF8G5RcmDDrHvjt7Xv3XBh78lYnwFMnZhOg5SRxF6hmhyUlyNs/o2dj/X9LKT7D/yo+y31+6H09E/w/wHJVcjfUH5AAAAABJRU5ErkJggg==\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHiczZ15vF1Fle9/a1Xtvc98780cEjJCAgkgiUwiIghCgqLIQyK2qI0D2g6PlmfbbaONCmo/habB4dF+tBX0KdC2PFGZB4GEgECYIQmZp5vc+Yx7qFrr/XHODSEkiDKuz6c+Z9+z9zlV9T2ratWwal3C6yh64YW8Y8Ex4431M4yhOQAtVMUBTOgRQRGEWvtBlJnRgGJABSthdIWHrIyI1l9y3339F154obxedaDXOsPGr2+anFL2TgXOJKZWEIYP5oslL0z7EtE8Zj4M0O49f5qGofKAF32GRTe1GnWTZOkR5DUi0muC0Nxaete7el/L+rwmALdde+34nOcPgei0ILK/j4rlLmbztyBMfkUyUGwT8f+ZNGojWeLeBchvjOR+Xvngqf2vyPe/iLxqABWg/p/+YqFR+WYQREsLXeUuMH8WQPhq5dmRVMRfUR8eqquTt7Din7rOOXsFAfpqZPaqABy88spjlPhfi8XytSbKfZxAB+3l0ZSZ7hXD6wkWCAggClURGWJSggUAUjivokRIoJpq6gkkyl5miOJYqNq9VO6xer3+UxfHp4P9l8Z+8pPLXum6vqIAhy+/fLYXXFksdd1go9wXQZjyggyJH1HDDyEIQ1iawMZAjAWTsWS55QHHbEREGQwPABAYZhLxzjDBIpOcQBx7BwhUvOtDkiXk/WGqcugeirbJxc1LGvXaqY5x7sTPf37NK1XnVwTgg1deGcwcHvkWOKpXuiqLiPjI3XIZJBv91lub59CORWBVAysmDKyQ8QgsQGSNsakaUhAYIAPqlE+hgHpRVeMB730IFcfOK8RbSVPHTghZCsn8IJI0hk/fA8WYXYuhovfVa8O3eudy69eWLzjsP87NXm7dXzbA6oXfnJNBflwaM+4uJrpgt6/fTlFwC+dyY7w1Fvk8KAwYHChHRsQGAVvrEBgSsGE2DtYATAwFE4MBQAUCgkBU4D3EewtRD3WK1FmkzsE7gstI00zQaoEynyFNNkucLCZg+q6lUpVLRoYGj1KWv53wla+sfjn1f1kA+750wblBYGcXunpOJcUBu3zrsIbBryhfnGaiyEghIoSRahiAcgEjyCkCqxwEFsY4BCHBEAnIIzAEsGEGRDVgYgXUiQAMcfAeEDXwqshSFe8t0syxcyRZTIgz0TSDacYkaapoNb2kySaK07MAVEaLqIRnGkNDv3ferR7/rxdd+ZoC1Asv5L6R+k9yudJTuXz+YgA7O3EOwquQjyoo5POI8oRCBC7m1YcRKIgIUUgcRSqhVUSRARtvrGEPkiRuVBsDw83B3m3Ot2KqDQ8TAJR7utXkcjpm4qSgOK4nn88XK1BlOC8iziBOPWeOkCTkk0SROqU0Jm00QEkKtFqqzVZLG406vHxol6q4pNn6XBw3jh23Zf3ZdN11/lUHuPpzn4t6Mr2h3DX2T8T85eeoYjuKuT9QobwPFfPQYp4oX4Tmc0AxD+RyanIBxIZk8hE8sx3cuLnv4ZtubD774MOz01bzQI4KjwTdXRt57NhhDYO00FXOAKA5UgsozUIZGOjOhkemSdI8NMwXnt7vsIVrDl20uDhu2tRxRtT5OCZOUvFJqhTHhFYMbbSIkpai3oSvN0Ct1lY0mqeAMHFn0UUuqo0MHDkU0Kn7X3FF8qoBXL34c1HXxNYNlZ5xawj0qZ03DN3I5UqMYqFgSiX4UhEo5IkKBUUhDxTy4FweEobF4R19g7f+6D8a29etPz6cPPGWniOOHM5Nn1qKyuUKk+UgCMSwigoLMwQARNr9ocucEUC9TzWu1WqNDRvrw8sf6HHbd7xz0uxZd5z0iY+VK+Mm9HCa1aXZJG01iZJYfbVJHDfV1BvwtQbQbDSlVsvB6+JdQHxvZLDvwDq5d8/86U/jVxygfu5zUV//4I1dXeN7QXTWzvdN+GNTKU5DpSJUKYKLRaBShi8UCaWCUr4Ijuy4NY8/9syNP/rPg6mYf3bSu9+zpTJj3/FhaG0YRWRsiCC0yFkLgAWgzFgSMkYBQL0n74iZJRDxnDiHLM3Ue4eklVCaZVlt3fr+3j/8YYrWG7NOOfeTT86cP+8AybIdaLRI63Uy9ZZKowodqZOv10G1WiLVxiD77CO7VPPqkaG+aSMjAyfvf+ONL0kTXxLAa9//fvN2p7+rlMdsYDbn7oQXhj+kSnkmd5cJlS5QpSwoly2KBUGpRFTIj92+YcP663/4g/2CSZMfnnrG6T6sdHUXopByuYKGoaEwDBGEIaIwJGOMhGHovKfEGPVBEGUAkGVJoEqGCGGaJoH3npPUiXcpxXGKJEmRpC3EcapxbWR463X/bZLebQvf95nPrpm4777TpNUYQD1WrtWdr9dCDI8IqjVItcpUra3WNPvMzjqpfH9kZOCACQGd/FL6xD8LUAHaccq7flEuj9/ARP+480YuvIzK3fO4uyzo6iLT1QXpKkMrZZhSyWTQiddd/r1n62lMsz/+sb6ouzKmmC+gWCxImMtTFAQmn89REEQuzIXKABtjhIgUAAPIOgkAAnQMlarCe8+i6tMkYeecbSapxs2WZGmszWaCRquOWt/g0Iaf/WxiJcxl7//8Z+ayoldrdaFaXTEyDD9cI1NtiBseUNtobHDN+FPP1Vkuqg4Pzph40w1nv2yAW4874dxKqZyzJnfZ6Hucz31fyuX9TM9YoKdC6KkIlbuYuyuiheLEbZu3rL3h5z87aNJp77lp7PyDpuZLZZTyEQqFvBaLRQRRzufzeVimoANsAMB2AFsAbAbQByDZDWAEYAKAKQD2ATAZwBhVhXMuSZ3nZrNhm/U6teIUraSl1ZGGDj3xWO/263+76D0f/chjEydPmaWN+nYdqTKGa0B1WGRomDA8QjpYfRZZ/HejdXRZfF6tWfeT77rte381wIHD3zpfQ/s3xfKYL0GVAQA2+iF3lWfpuDFK3V2Erh6Ysd2qlRJToTD3jzffcseza9dMm/s/P7clX6wUKpU8R/mCVkpFLRSKEgTWWmstgEEATwNYC6Cxh3IJgKM6fy9HWyu1c4861wUAswDMA9DtvXdpmvp6vW6arVQazRparRZqQ0Mjq//9+/vvt/9+a4898Z3v0Li1CsNV9cNVz4ODVoeqIsNDHsNDG5G5tiaqukZ96JLM61WT77/nqb8Y4JPz54djw8LN3eVxUxS6PwCA7a+op9JF3V3MEyYQuiqCMV2M7m4gCg/6f7/573uauUJj9t+eXSiXyzaXK3JXuaiFQh6FQsExcxnAJgDPoK1xhHbTpA6g7g6UgwAcDmB2pzhrANwP4KkO7CoA34HoOq9jOp/b13ufJEmi9XpDq9Um4rhOtVrVr/np1Y1CKy68532nH0NZ+gQGhtQPjxCGhlX7Bw2GB70OD4/A65JOvqtGagPbJrK8kx56aI/TPrM3gF8ZO/HfugpdG9W5U+EcyPmnqFioarEYULkCKhWEymVGpUxgc9A11123ws7cb+P+HzprXHdPt+0qV1y5VOCuroqGYRgZYzIADwJ4Fu2mWQHQIyIREUUAciJCRJSo6qeI6NsA7gZwO4ClqvppIrqpU7xIRPJElAfQg/YSWQJgG4AhZh5rrc1HUZgGATMRGRuGKM87yA5v2Tz02N33NOfPPeBQUfSyS1nTDEidauqIUie+Uffk3AQ4NzYy9prBZv307/bt+N1LBrh1zpwDQoTTAzIXtKdO4jSMbuJysULlippKCShXiIolImsPuf6m3y+LDpy7ZeYZp0+qlMvc091FpVJJy+UyBUFQYeanADwCIBWRkIjGiMg+qjqWmaep6nxVPY2IjlLVJ4joMACnAzihkxYR0TCAR1T1M6r6FiIqEVFFRHKqWlFVUlUhohjAWiLKjDEzoyhyxhhlMrCWbGn//UrVoaG1Ty9dGs+fvf+bSWgL0tSqy0BxBmSppSx7FnE8H84zvBzjnLvhf47r2XZpf//AnwWoAFXHTfivclSeR0RTAYA4uJIqpamolJkrJUi5ApTLxLlw9u0P3HdPOnnS0KwzzpjQVSlTsVhAuVxGoVAoMHMOwOMiMkBEkYgAABMRqSqJSAqgn5mfVtVFALYC+Bu0jchVAG4AcAeAJ1R1tqq+T1WfJaJ9mPknABqq6gDEABLvPYjIEFFJRFJVbTDz5CAIDDO1mNmoshRnzOgeWr9+oPfZZ9fOnDr1MFHpJ+cUzqv6lJH5IpL0VqgcAQChCcstFy+6ZKD/Z1/7cwA/PWfeCcWwMMhsP94mqiu4XAJVykzlElG5i6irCCrku9f2bnnk2aGB8rxzz43KlQrKpSKVyxUtFAqWiLyIPKWqCYBJqlpBu5/qEZGJzDybiN6jqkd77+8hoveq6nYi+g4RBar6QQDvBPAOAAuY+W4APyKiBao6A8AtqvpFAAtFBERUZOYKgPGqWlTVHiIa6qRyEAR5a61T9T4IIi4fOCdcd+/SfJGwprvctQ9EEnZi1XmFc6Qu60Ka1gGaDMI+AfFlQ2PG5i7p37F+V168u/YJ9KvWRl/a+UCY+xNyUai5HFMxUuSNIoyQthK6f+XKhQed9/fVUqlIpWKBS6WSz+dzQfurcC+AQe99WVXzqtqtqhNVNSKizap6u4iUVLVFRBc5574IYI6IXOK9f5+qiog8IyLPqKp4798nIpeKyH6qer6IfNN73xCRCjPfCmCt954BdAHoIiIjIkUR6RORZUSURVFki8VykM9HWiyW+KC//3zz/pXPLEySRCm0RiIryAVKuZyafJ45Ch4Y5WBN9EURf7HuZnifp4F/N3f+WwthfoTJvL/9Dt2ihahiKmVoscDIFxWFAlEQzvvtI38anvPpTy/vmjC+p1AqoburolEUBUQ01Tl3D9rW1DNzQ1VrzJyo6j4AFgFYSERLVfXdAC4HsICI3gvgSWPM94joZhFZq6pxpznfraq/Nsb8UUT2AfBhZh4G8FMAxxPRLar6BQBv8t73qeomADuYuQmARKSMdvew0FrTMIYVRAAQFvaf89T9N/x26gGTpkwn7/uQesCnUPHkY99NLlkNYH8AFct6SX+lB5cO9m/eCXZXgF79N4wJp47uvpgo2CxhYYoEgSIMlYKANLB2Ze+W28v7z0m7pk6elM/nUCzkuT20w1Tv/UYAbxWRhqqOrnhscs6tIqJbARyvqomq/j0R3QbgAiK6Q1VvJqKzvPcXqT5//2f0b+89ADxijPmi9/5dAL5MRLc4584H4IkoJKKbiWiMqh4gIlNUNWDmfhExxpgtzDwlDMMtBRHyzvvuqVPGl/af9ejqbVvs3DFjp0kQOAojFROQKQQqLtymSdrmwflPESenAjjxBQDXzZ8/iV1wO4CLAEAJd4kNpyCygAmI2KgGBkjdlCd3bJ+54NOfWFbI5xFFEedzOW+M6QFwFxFtUdXAew9jTMV7fygzv5uZ6977bxNRwXt/gTHmUlV1InIBM38VwDs6oG4RkbuJqLkbxAIRHUdEJzrnDgUQM/OXReQTAKZ0NPBSImIAnwJQAnAnET3mnBs0xkBVM2aeYK09wlo7kM/nDaA44MNnRw9dePFb9ytVMmbaAGtgQqPehIAJ9oFmfwTp20E4gNn+cu3MgybOWvfE9ucBDBJ5X7EY7dynZWOfhjEzjbWkAUOJiRRmxY5t901bfHItly9MyefzUiwW1Vo7SVV3OOcWoz2wtcaYDQBWiMidRHSqqm4jog+LyBZm/i4RnSci3yaii1T1Hmb+tff+QACnEdFJ2E2ICKq6XVWvVNWVzPw/vPff7IA8T1UvFZFNxpgPi8gIgAnOuduDIOgGcKyq7uu9Z+993Vq7PYqifYhou4jXLPOFKSce9/tH7vtTeUHPuGlK7MFWOTAiQQAT8FPe+bcDQCksjvVu5HQAPwQ6RkQB8mtXTwf47e3GQn1gM1WtgbckwqywRN6l4zYn8dsnvfWt46PQahAEFIYhq2rivV8G4Ceq+v9UdbWInOyc+4S1FqoKVf2Bqh6qqr8RkW3e+8tU9XYi+rKqHui9vwzAuWhb6UcA/MY5d7lz7nIAv0F7HNkD4FwiulRV91fVL3vv7/TeXyYiW4jotyJyqKr+H1UFM5Nz7hNEtNg5t0lE7iKiX6vqnUQUW2tNEIQU5iLd9x3vGLclTY7zWdoDqLbrzGBr4GH3VcWAAgDTcbJ29b6jP6xBW99nGJOfEYy0ijSmO1TCLRREPchFZIKANQyFQqPPpunq8C1HPjl+3oHlKMpRsVgQZj5AVR/z3p/V0egBVV0JYDEzbxORfQGMVdXDAPyaiD6JtrH4oYh8TFVPQnscdzkz/5f3fnVnnDiLiBYR0WGqKqq6XkRuNcb8ynv/eGew/W4imi4iXyGiyQBOZ+afiMhH2nqBsdQ2FhONMVeKSAzgMBF5LxGtYOZDiagPquS8mrReWxFv3jw4hrlIaQpJM1LvGN4ZdemDrDRG1qx9VLys+TvJNl8OjFgASGBO6Db58ar+fbxu3dUye78WDBMRVNgAAguHYJ1Lj15w8onLC4UchWGo1tqIiLap6urO2OwMVd3CzFd0xmY/VtVvA/gCgEtFZDERfUlVv+WcewuAW4wxfxCRt6vqlzpGAp0BN9BeUACAQzoJncEyVPW/jTF3O+feTUTfVFUB8CXn3BeIaKL3/nxmvkRV/5GIvq2qARF9QlWnA1gqImuMMbOCIAicc1k+r5h16ruKDz348JGzDe0gIAUzQExEVmBMitVr7obXsyObW5e6+O2Av5oAYC3s78YVxhypinEA1blS/i3PmlnWUtFSPgcuFCjOBc3l3RXz5i9/MQnDkHO5HIVheLCI3P6Nb3xDly9ffg4zR7v3XW8kEZHklFNOuf9jH/vY74IgKFhrJwE40Tn3aJIk2opjevDib+WPGm6lhTTNS6OpiJvQetPrug01P1x9L0ELRNgx0By4byb8aVYBXg+qimIcACj0FsTJOOzojbk40wMSiM90HezwtFNOrhtjJodhKNZaUtVCkiSz7r///hOZmc4888w/rl69etWcOXPmPP300ysPPPDAuc1ms56maQag55ZbbjnOWrvitNNOG2Fuj+H7+vr6+/v7ByZOnDh+zJgxY6655pr5ItKzZMmSezvN73lCRHj00Ucbq1ateteYMWP+dMIJJzQ6cPSZZ55Zvd9++80IwzAcHBwcuPnmm0+w1naXy+XbFy1aZP7whz/ozTfffHylUnnmrLPOeluWZZuCIMgZY5SIyDBj2knvfHbDdb8pz3U+r+otvCayfYeXZnOcQm8BcJoqJgCcKDzbTcCknA0fBnAWAKjyDiWUtZmEfv2mxM6a2VAbdG8L7SGHHzTv0SAIgPZofJyqPtJsNt9sjDFZlsVnnnnmMara6PR3C4noJ6p6HoCHmfmmW2+99bh3v/vdI2ecccYQgGUAzgZw3C7jvjU33HDDxlqtFpx55pk3ABhdUg+89wUAMwGcsHjx4uI555yDM844Izn++OMLAK7z3h9rjPlkB/JFInLm0qVL++I47r7iiisOyOVy5dNPP33pOeecs/K66657z5IlS54mopNV9XcAxllr+7xX2ufNC4sPXH/DvLnS6JPEx1i/UaXZKrR/S9422qtYGz603mUT2AP7Rhxyu89VcOADYnWqHuLSfLpxY5dWa9vZmO6wUCiTseC2+swTkY2jlSciiMjFqvouEbkEQE5VP6mq56vqId77LwPAtm3btqnqqar6HRGZB+BqEfmCiFykqqvCMDRERM65S5xzl3XSd1T1a6p6uqo+2Gw2vwcAvb29G0XkMFX9DjOfDOA7AH6mqhcQ0aw0TT0AhGHYA+A7RLT4Ax/4wB1ENHnDhg23qSpEZDOAedZaMobI5HIltcF4DNd28IZ1OfGuCCiUycP4YJRTyCEE2Jc9zFwY3rf9NqDe9DjhQAUW3oNcipH+HcmEgw/5PhEnDIWIKIC6qtZ362POArCUiP5FRL6lqjVVvURVfy4iPwKAKVOmTAbwsHPuCwB+5Jx7P4BLAVwgIouZmbQtN4jIr0TkV97721T1MREpiMj7SqXS5wGgp6dnkohc2fkBnhKRL3Ys8JUicl0QBKYzhLpMVb8BYOlJJ530NhHxt9xyyzGde0NEVG3rAKVgbk6cP/97IwPb1WWO4T2p91aFAxUaM8rJGp4CmDkWoDcTuON+RqmSlgyJI/Uq3nhKM9SisKX5cJYxrNbamjFmnKpuc869s6+v72cA/rGjhXMAbPbe30ZE/wLgHhG5jog+O6qp69atWy8i7ySiSzuWelhVrxeRx4wxSaPR+EC7K9GZqlrsXO9Q1Q1EdIeIbKnVagDwHVUlAJ9WVRDRJufcBcaYA7335xIRvPerOv3ol9FeFgPaq9a9jz766CTv/XcALGLm7QBCIhokIOJibkY1CJs5X/fshYyqcyQGRBEpUkBDEM0jaGYB7KekC6EEgj7JAOC9aHvnQSAW/cVSuO/cuYn3PmLmnDEm7UzDFlQqlb4OvGDZsmXfO+yww86x1m7z3v8LM39NVd/mvf9+54f77OzZs2eoatF7/x1rba+IHAngFAAf8N6jWCw+2mq1SER29SmcucvQBsVisQ4AW7du3UxE56vqJBE5tzOrEefcZStXruQ0TT/IzGi1Wl/P5/N/Q0SzmfkrY8eOPaO3t3ccgGNEZIGq3sXMKRH1QFXGzp2bbSyWypPTXqgXdd6BvRdVZEr6FBSHMnCYgBMLIA+lCgAo6RZRhFAFeQ8IQSwQR2FPYZ9JW4MgSK21LCJzRaRJRB81xnwNAIwx5oorrvhspynPEpF/BlC11jaiKDoZnd73+uuvn7Fp06bHrLVHhGGo3d3dEJGHsizbGsfxjoGBgTMBFK6//vqrjTFBtVottVqtuNFooL+/X7Msc0NDQ1MBvO3WW289aenSpUfEcVzy3ouIxEQUGmPOA4BRS++c+6SIVIwx53nvL91vv/3uXb58+SwAE4jooyJyjqrOtNauIiJXnLoPJ8Woy2cZwYuyqIoqoEgFWMvAoarUDaBgAfCo96sqWiA15L1CmMApICDJh/l8qcTGGFFVj/aUaqRYLP7L2rVrf9xoNL5eLBZ3aggzEzOHaO9VVJLkuU1+7/2MBx54YAb2Isa0V9h+85vf/Pk9WaKJrVZr4iisUWC7y9q1a389b968aQAuA/CMtfapOI4XeO8vLpfLX4vjeA3aa4gCQE0QqURRDmlKgEK8AAqCihK0iueGV8yA2tGOkYAUIIEoqfckzpPLHBAEFY4iEhFnjPGqOhbt3bGZ++6778l9fX3YsmUL6vX6zqWnN5JYa49g5lNV9d9E5FvHHnss+vr6cpVK5X0AphNRA8BY770AEJsLSYytqMtUUwd4bbMDRBWNUV6AWn6e87WSV9KmGtRhUGdwPYCpE1srzmVE1ErTNFHVLd77AQAwxswEgCzL0N/fj82bN2NwcBBZ9rKdP18xWbVq1ZPe+49nWXam9/4/DzzwwGcBpNu2bTMA0FmE3UpELWNM3RiTsA1CgOpgqpNBnRk1z9QU2J28aFT7dr5BmldFyQIqIPKq5OFh4evqfaiq+c48dC6AjXsqrPce1WoV1WoVYRiiVCqhUCigs+D6mkmWZWg2m2g0Grjqqqui7du3Hzt58uTbJk6cuHb27NnvJ6Kt995779NLlix5E4CJqjqHmR8CYLMkSQP4qgrKBJAwhKBgJVZyFjs9jwEreE4FhajIopRAGSAGkRG1CJwfcnEqlMtZIhIiqhNRcc9Ff07SNMXg4CAGBwcRBAHy7QVYRFH0igN1ziFJEsRxjDiOn9cCduzYcdbVV1/9gs/84Ac/WLFkyRKoahlA3XtPzjmbtVrMiWs66BgQiIWcQpVZQwJ17eQFwBLQC9UNIEyHoqJGYwhIIO21QvbepGktHhpyQXeFOyvNDeCFHvi7yBAzb+zkwSIyLcuynl0rZa1FEASw1iIMQ1hrwcyw1oKInmcQRGR0TREiAuccsiyDcw7OOaRpOrrcvyfZxMyD1PbBgfd+Ptq+NhgYGDgJAIhoHwB1dFQrHhlxuSyteUg3g4xv93VKYFJomaCA0hoCtliBPi4EQ8B0IZ1LgscZUKfwgIoQo7S9v9nYtDGMpkwxYWhtZ2B7SCfT5wkRPU5ErqenZ924ceNkx44dPDIyAgCbReTg0edGK/8qihhj/qSqZuLEiWvL5TJv3ry5GMexV9UxqjoTQNrb27u8VCq9mYgeFxEbxzE11m8Mcn39Dc8QFaiFAkSkKoZID1YlgOQhQB9lBT2tKqsAgBTjhaTmSb0C6kDGiQa8dWuhsWZ9TtVb55wBMKiqxjl3LwCUSqXfjsJj5vqiRYsmBEFwOjOfEUXR6SeeeOJYImow85OvJrFdxRhzfxRFQ0cfffR8a+2ZQRCc0dPTs3jx4sVpZ+q2paen596VK1c+pqoBgMHMeyYiaqxdVwy2bCtnQoEA5Nu+TJ6gI6o0BgC86rMCeoYZfnXm0+0Ytc3CDXiygASqYK9MNFwrNNavnZ2mKURERaQOAEmSbN1t2GIXL148pq+vb8p3v/tdiAi+/e1vo6+vb+qJJ57YjfbK81/syP1XyAZVjRYsWHDc2LFj81//+tcxffp0nHfeeXjyySffsmDBgg3MvKNer49LkiTqdA11FYFzHsn6TbN4qJYnBYmCFQjEU6BKzVFO4tMdgF9rCVib+Oy00OQAAMTUVFEVpUyhUKhFko5F4g5Mk+QOIgqIKGVmOOemX3vttXfW6/XjrLX3EpE89dRTxy5cuBDLly/HvHnz8MADD2D69Ol45JFHDmDmewE8rKrjXk16RDQwadKktRs2bFgYRRFWrVqFSqWCe+65B4cffjhardaihx9+eG2WZVOMMRYARCTN0hQuzRLN0nk+i58ASAOwCOANgcHaVGlb4KbPxjtgjd0fqK4Uf/ROWyxqASYlsVBWBbwCCPv778x6dwzQPvtMMsZoEASbVfVtRx111E3PPPPMzQBwyCGHFK666iocwXXn3QAAEZlJREFUe+yxePbZZ1GtVjF16lQcc8wxWLp0KRYtWrT60Ucf3Wl+X6lB954WXk866aSJy5Ytwwc/+EHcfPPNGBkZwTve8Q40Gg0sXbo0nD9//vJWq8UzZsw4RUQ2dgyVtjZtGgj6+m9X8EQAQlBWKBFYSBBqh1Mq/m3zgQvabrNAnwIPEHAEgBMIspJUIKTkARaFMQ89OlJbvjw/5vTT1DlHxpgnACyaNm1a38UXX/wxEYGI4IEHHsD111+PJUuWgJlxyCGH4Oqrr8acOXPwmc985m9Hnxu1qLta11Ggu7+OAtr1lZlBRM+7Hp3OjVrwG2+8Eddccw3OO+883HXXXRgYGMD999+PBQsW4Oyzz/5okiT/KSLjVPVGL6KNRoOaD64o24cfq3nIFEOkAlIDdYAnAb+jw+sBAtYBnV25v4OphWyHmPidALqgtEIIxbbnIkOg5KrDJT3ggLebgw9ew4aNtTYlonne+2IQBN2qyiKCOXPm4Je//CVEBKtWrUJvby9WrFiB888/H8Vi8Xnw9gZy1793T3vT3t21kIgwefJk3HzzzajX69iwYQN27NiB7du344tf/CKstS5JkhKAsSKyIm61qNVspcnd95yst92RWMAYYjEABYCxsIMKfQsAOPGXpz77w/cg6xkAArj7W65VHs1cGNsZpBYMQKAgiFIevX23tTas64vjmFqtFlT1QQBzkyT5xWgFp0yZgiuuuAJRFIGIEEURLrnkEkyePPkFkF4see+fl17KZ3aHfdRRR+GrX/0qms0m0jTF3LlzcfnllyMMQ2RZ9gtVnauqf0rTVFutBK11m/pNb98tqhIqoKTC3LbAqup3jPJpuVaXh/sTsIun0dMwf+zJd+0PxWRVPA7CjhSglgIJhDxAaSm/OffZT0+17z+9t1AscqlYZCI6Q1WfiaJoWmfF+AUV/nMAdtfCF2vCL9Zsd0/GmL39PRTH8QARzfYi1zXqdbRaseh//fe41uXf326ayZQA0BwMAlLJA8SKsUp4EwhbBlsjz86DPw7Yxb1NgEuc91e0C4qDFboDECK0zY4KyNdbU2iknqT9/X1JHEur1VIRuU9VD0iS5Jo9NbndwewN3p408M/9AHv7vr3lPZriOL5BVfcDsCxNErRaLcRbt/XpSD1zzXiSgtgAyvAwbSPSq4Q3AYB6fzmA/z3KbSfAEP7melqfNrppQqKtAIAhUAiAWD1BbO2XvxyJ7l52QL1eR6vVEieySVWr3vszvff37K3v2luF9tZ0X+z6xZrti/Wfqoosy2713p9FRMNpmm6u1eucpqkU7n/wwPov/m+DAGsgQgAZAhkoWDQZ5TKY1ueuh7/9BQD3BxLfNtHXdHxAPgyiEduZzzJAEFI3PDxO+3aMYP3GvlqtybWRKrz3d6hqMcuyblUdGm1+e6vYi/V7f8n13mDuCWKnTL1pmk4CEKRpeme90aBGKxa/ZsN237d9OKnWxrQdKEm4c9qbCcPKdLa2rcEvAG2c0nZofz7AdjOWC0fSxuiZCMuCHQYg204IIEoA9f7kqlLlqZXvTONGrd5ool6vp9r2OD04juO7te3L8qKa8FIMyksxIC81H1WNW63WU0R0sIjcFsdxVq/VEdfr9a6nn17c95OfFRhCFkohFBZqLKAQ7kfHi62aNtcmkG/syux5AA8GtntxBwB0BwAo0YcD5WFDpAYEA4YBQzPXvf26/75vzP0PlhuNmtZqDWo2m4MAHgLw3iRJfrWrEdj19aU06d0t8K4gX8p37CnvJEl+h7YP4oOtOB6oVqtaq9do3EMrituvuW4ZMt8TgikAwxBgicDKwyD9CAAocIsTN3th22N2zwABIIb8r6G0vqzd4jUnLD4E1HS2OgJAQ6KkuXrlfn79Rp9fs663Xq9ptVbzjUZjjao+AuCDzrlfAHhFNPFlal6aJMkvVfUMAA83m821tWrVjNTqlHt27WbevJmba9bMMgRvALWAsBIFgIA1BRAqgKG08TBBzt+d1wsALgS2irhAIT8CFFCcYYFVAUHzgIYAWRKyUF37o590j9u89ah0247eWrXKw8MjaLZaq1V1uYj8TZZlN6jqyN60cW9a+WLjwJeibaOvALamaXoPgLMUWN5KkjXDw8M0ODSk0rejf0LvjiPX/OA/ihaAhUoHIIekxJCVpLqkzUB+KOLsfOAFUZH2eNDmQ9ClcG5J3uZmKBBCKWcNDQsQCEQ8wSiIRMT0Llvef9CCQxZsjIKnHbikIgKgGQTBJlU9xTm3GsBqANN2b3Z7et2TMdh1/Lf7GHBPY0IigjHmHu89EdERAG6O47hvcGjI1OtNifsHBg5cv/HYJy765gbrfaVAxCGBilDNEWyOTaxCbwJhHIGqQ0ltex/kcz9re9/+eYA/BtynYaqG6HHDZhEIE6D6EClXAHgHYgAsBPLqo833P7Bm4cELjt1A9GhGWnaqEOdbzPS0MWauiExX1Z9re+Qf7KkP2xO43QHuCmhP0Dpz4CERuVZVTyYicc7dPlStJrWRmtZrVY37h/oXbu094bFvfXuFSZOxEZGNFDYCfA6MHDGp6nYiHA8Aqc++lIn//RGQPUb32OtZuR9C1nxc/GcjGz5JoIMBmk+svyPQBAACUqiSVVJIlhW3LLt/5eGHLzx8o8NTKUkh88555wNAN1pr+1T1VBFZx8w3icjB2j6a9bwmt2uzHJU9QdqLBgoz/1xVJxDRUQD+2Izj1fVaLalXq+FwraZZ//DWI/r7j3/4m998gprNSTkgyIE4YmgBLDkiWKUniXAOAAjwi2ra2Ocg+Ev3xmmvAAHgH6A31lz6oZzJEYCxUD6YCHczoUeU2DMMQCBSylzWtfHOu2tHHnXk9DjLHu/1bqzKzj4sZrarARUROUnbHq2/Q9tFrmtXiKPXe1p5GZ2K7QZvC4D/ApADcDSAJ0RkRa1Wy2rVBtXqVQwODMvkoZGtbxoZPuyBr3ytj5JkfJ5gQmWTJ0IR7AsEWKb1qvhIh8uq4WSkVYJ+6N/30HRfEsB/B9zHgWVO3fjIBAsIGhHRDIY+xNAygRVQgjBDlcW7YN0dd2bzDj7YTjXWPJVltVbqOUvTXOYz8c63VGU1M9cBHKKqU1T1NgB3S9uzfho68/Pdm+0uyRHR3UR0B4DtRHQgM5dUdV2apk83m83myMgI1RpNHR4aMLVavX5stV6sbNpaWP71i4qBl2IEaF6ZCyxSAEsemgXEW1RwGkGLANxI1riaVL4yp30YfK/ykmImPApzap7t3EKQ+067ctigwP2xYlwq4AYLN6AcgzQW9S0oeubM2fTmv/v0kY8Q3bUxF3bnCpHJBYFEUZ7CMEQYtnfjmDnsaOE07/047/16Vd3S8RZIvPdgZgugQkSTmHm6MWaYiDZ2oKejO3Rx6rXValAaJ9qMWzohTre9FXTy4z//5dLtDz64fx7gHLdHE3kYXxDhHINyxFsVeCsU+wKQetb6p1T844fA3/jn2LzkqB2PAOeUOSqHQefoP2EThP6YkUxvgnwLoi2QT1RLTZUkUTXehukxXzp/MJw8acyt3j1RZTOhkM/bMBeQNYHmcyEAAxMwhUEgaLurdcJmAUTtU/LS9kfU9jUABrIkJS8eBGiSJJRlmaZpqtV6S3oEAydZO6+1bVvfsv99yVjj0jBH5AsgHzKFecAVwFIAyCi2EeMoKGYBQJrF5zUlSQ4G/s9L4fIXxY15BPhqV5AfsRx2INIQAdd7yP4tQJseQQvQJpRarC4VlRQQU+5qHP2Fz6e5CRPG3dGsr9geBOMtIQzDnLBlCkyAIGBSJTWGoKoZGePEOQ8AbK1R7y0RWSfKDEWaeiiJpnEK5zJkzmfjVftOyOXfnG7v237vv12Wk1qtEAKImGxOyOZBvgClvAHyIDWglar0PwjtfjiV9Lx61iq8CfjWS2XyF0cuegz4+zyHYRTkvwmAFUiJ8WNRPTSGaqzwTYEkDI2hpuVhHSFLoGq7K4NHfvQjtfEHHHj8mlZ828NJq9HnsnHGhNYGhgwD1rYNhaq6dgwZoDM5sCKs4jLy4uEyp+KzrDsIB95aKJSmBdEJ2598+s4HfvaziqtWe3IgChQ2b8hHUJ8TQp5h8gREIEtKjwP4KLU9yKSZxV9IJPmL4P1VAAHgUeAjAduppaD0v9CJd2oIvxKlcgJfjAHTFLIxqcaKLGXlTGBa0CxTsWK40TNjZt9hH1hiumfMeEcs+sS6VmP1hjh221KniXrK4I2gHXiH4cnA+DwZnRQEPCOfs7ML+f0j4gMH1qy/88Hrfikj69aPZ9FijthZUBAxfCQkOYYtClHI6nNgH4AalrThFWd2CAwOp41/F3FrDwV+/pey+Kujtz0EHMXgT48NS3NBGI0XuAqge5TkgFhJEqiPgSCBaiJwKSRogbxTNY7IZ6rGkWYmFw10T506MnXBm3X89Olhvru7GJXLlSCKxgNAliR9rWp1JBkZafZt2JBufPghHtmyuUviZEygFAREYlXZErkIGoTgNMewEYhCIMsBlCMERvkpJX07FPsDACnuG0rrazzk8gXAn/4aDi8r/N2TwJgU+HlPWHqQiL/y3B39NROJE0yJiVwM0VQ0TIE0ZVgH+EzEZKAsgwYegIdmClivnCl5B2KFSHv8xWyhQlBjLElIgCOQDUFqAR9AA8vsQ4ADgc8BgWVKc2DOqQbM2KSqAYHeN1pCUflaNa0fTsCHDgGG/loGLzsA47WA2Q/4Z8sWXUHhQ9o+nAwFUkB/ysAYrzQ1IU0cqYmVxAGcifoU4IwhHmIgTI4FIuQEUAWI2lNGKFQIHZcxVmuElVgQgL0RmBDwAZOxgIQEEylcpBQxY6MRJI4weo4PAJ5pZM1rMnHuTcA36bnjZH+VvGIxVB8HZjvghyVbvCkw9hsKLXRuCSn+rzIEqtMzgnEKTQnkPJwzCJwCHuqkbTUcAeIERNSeAajCWoZqe2XcctuqBJahgUdmGUEAeEswgcIR0XoVWGqD43ZFqZn49CtN11rkgE8d3g7487LlFQ1CqwCtgDmVIH9bDIsPWeJ/1vYUa/T+kwq9j5WKRDQ5UwmFkHmAPRSOQOpJhSHtlcT2fI6gCiYigWGjsAplkBoAAWAMOBPVbSBtEOhotCMZjVYwdioXN9LGmwX844Xwv38lQyK/KmGQHwQCMmYJRD6UM7nbQms+D6V9n/cQ6VYAdwLUIiBSRQVAt5IaArwHgbQ9jFESNu1ItKbtLIFhIlQ73UQOwAlQmrRbzTaIc5c3fHyiMv9Cvb/2sOdicb1i8qpGMleAH7T2BPL6T9aYu3Im7CHQeXsvDQ1CZQVAI6QY0fZ0DqRaVEIXoF0gXgDVMXv7CoVeFvt0KPP+WGPoWwucu/Pl9nMvJq9ZLP0HgXFg89U8h+uZ7SWvRh4i7vyGZNNZ3DcOA171MPDAawhwNL8/sfllzuQ2M/EL9hdejojq5bFrTT1M/RmvVtj3Pclr/t8c7gRswdjfFzjHoOfCh7wcIcU9dYlj6927Xo1+7sVkz0d7XkU5HnDq3ftjadWVdNPOU6J/bSLd1pBWb+bdktcaHvA6AASAo4Bq5s1Xmz79tQKpoN3L/6XJAy7x2VXkzdfe9jJmEy9HXheAAHA00sdJ6QFR/Ye/FmCq+g9edeURSF8z5/Xd5TXvA3eXZRxcam00zMDukeX+nHwj88nYt/jnIvC+HvK6A1SA7jPhb4wJqtSOofBS5NpMsuKtLn3Pha/iGO+lyOsOEACWAXm1we+Ywm4iLPwzjz/mJNuSufT049vHJl5Xed36wF3laKClLjhbJHtEQdW9Gw2qZ97f5505940AD3iDaOCo3GNzx4HxVlZz0Z7ui8o/KrLlxzr3x9e6bHuTN4QGjsrbXHwXRJywnP8C7WP5AkHkjQQPeINpYEfoHpP7hRrapEr/0H5LL2fR8W/z8d/gNZymvRR5IwLEnYBlW/i9gJiACNBYXfOU41/ExeL1kjdUEx6V4wEnLlxCkCogfeqaZ74R4b3h5d6wNP/esDT/9S7Hi8n/B3LrBEUxxEM2AAAAAElFTkSuQmCC\"],\"useMarkerImageFunction\":true,\"colorFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n if (percent < 0.5) {\\n percent *=2*100; \\n return tinycolor.mix('green', 'yellow', amount = percent).toHexString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', amount = percent).toHexString();\\n }\\n}\",\"markerImageFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nvar res = {\\n url: images[0],\\n size: 55\\n};\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n var index = Math.min(2, Math.floor(3 * percent));\\n res.url = images[index];\\n}\\nreturn res;\",\"strokeWeight\":4,\"strokeOpacity\":0.65,\"color\":\"#1976d2\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"showTooltip\":true,\"autocloseTooltip\":true,\"labelFunction\":\"var vehicleType = dsData[dsIndex]['vehicleType'];\\r\\nif (typeof vehicleType !== undefined) {\\r\\n if (vehicleType == \\\"bus\\\") {\\r\\n return 'Bus: ${entityName}';\\r\\n } else if (vehicleType == \\\"car\\\") {\\r\\n return 'Car: ${entityName}';\\r\\n }\\r\\n}\",\"tooltipFunction\":\"var vehicleType = dsData[dsIndex]['vehicleType'];\\r\\nif (typeof vehicleType !== undefined) {\\r\\n if (vehicleType == \\\"bus\\\") {\\r\\n return 'Bus: ${entityName}
Bus route: ${busRoute}
';\\r\\n } else if (vehicleType == \\\"car\\\") {\\r\\n return 'Car: ${entityName}
Current destination: ${destination}
';\\r\\n }\\r\\n}\"},\"title\":\"Route Map - OpenStreetMap\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermomether\\\";\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"markerImageSize\":34,\"useColorFunction\":true,\"markerImages\":[\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAwgSURBVGiB7Zt5cBT3lce/v18fc89oRoPEIRBCHIUxp2ywCAgIxLExvoidZIFNxXE2VXHirIO3aqtSseM43qpNeZfYKecox3bhpJykYgdjDkU2mBAB5vCamMNYAgQyURBCoxnNPd39O/aP7hGSEUR24L/uqqf+zfR77/Pe69/Rv6kWwcgPLRIJfZUAa7xez2xd90QBwDSNZKlkHJHAK+l09mUA7BP4vPpRUVExMVoRef+L998njxx9X57vPi/PnTsnO850yPaT7XLXrrflqjtWymhF+HA0Gp0wEp/kHymEQqG4ptJDGzf+um5RUxMSiV7Z3Lyt88L5nozgHJWj4pGmpqZav99PWve04onHHuswmViQzWb7ruZX+Udgv8/z3A+f/NGye1evxssvb+wo5PMfTZs6bfqcuXNHL7hlweh58+ZVAOTUpk2b0p9dvjyqqmrs/b8ejpUMc+unzjgUCsXjsYruE+2n1JY/NedM0zCi0VjA7/d7/f4AAgE//H4/vF4fOjvP9h5695C/oaEhcN/q1SyTzVdnMpnklXzTq4EplUsXfmaRCgC7du3cOn78+KfGj59Add3z1Md1vV7vqPa2D1sA4MYbZ6qUiqVX9X21i4TQcfX19QCA6urquN/vn0kAPRQKpYbTnzRpUhgAampqAEFrPjVYSql7fD4AgK5r2tV0AcDj8WkAoOk6JJGeTw2+nocLdsEu2AW7YBfsgl2wC3bBLtgFu2AX7IJdsAt2wS7YBbtgF+yCXbALdsEu2AW7YBfsgl2wC76mh/ppjIQgXVloPxVSBRV0rBe455P6+kTKBYF3tonxY/IWarry7DvI298Tgp0PR9RzACaN1NeIS100+EdvKXW3cMZvF8wCK10Sq2it2NAzakmukP/wmoP/KuId3BRUMg5uCfCSNVSKVn1rNto7Un8jLrUVqJ4Fi2eEQiEYBzOsy3SYL37TNQdzi8Q5FxkqJIQBsNLlYMGF/zqAJWBxSEogDAY+DJibYqTuRg4WFgO3OKhCYTExbKk5G/mbkSPP2DQhLA5IO/NhSz1MMP882BDgnAFQwdiVSs2vPVhYDIJLUMkBgw1favM6lJoZDDAYhKbAYsOX+rqAhcXAuQSIAKzhSy2vS8YmB7NYH4WCfM7kw5VaWtdpOO3bfWZJZVXgPxMX898bVsm6RhkTIseX29yyIErm/J5z5vwr6pvmsLYjBgeDwSpVJS/OmT1n1de+9qANZgLc4q9Dyj2qQhUhSSUAUCL7GBcchCymTEYBYNWqVXj30MGHT586PZEJ+WAul7ts8bjspd9QKDRNU2nz4z94YtI3H3oI+XwB//3j/9m77eRUUJ9/0eh4APGoDz6vCi4ksgUTmYyBC4k8RLGwtzF+EGu+tHqRqqrYtm0rXnzhhQ7G5cpsNnvyiuBIJFKnqvSd55772eilS5fhwIH9ye+/dPaEf1T9otW3T8GtiyYgGNBBymYEgLSbvakidu8/h01vnkYhcab1gcVs5tx5c6PHjh7DU0/9qFsINPb3939UZg28X11dXR0Qwtr9g8efqGtc+Bn89re/O7FhR9BXNaFm+n98uxHTZ1SDKQqKAihweZlITUVtXQwNs8fg+Bmzdk+bnmPdf/7bwsbGeO2ECaED+9/5XCxWuTGbzVpDwJpGNtx+28o77rr7bmzZsu3k7z+cMlHzeiPrvnoTwtVhFAVQHAZY4HBEoiAAeDXUjI/gyJGeQEd6TFj2tHYuXNgYy2azVe0fngiWDLNloHNFo4FZkXDsoTVr1+KD4x8U/3Ci1qP5PV7N74FeFUbClKDEriy57A5JANL5a68hnqoINL8OAPqbXbNp7clTxTVr1/oOHjr0MFXxq2Qy9wEFACnoY//6la9QAHj+9Q/eUL2RWkVXoWgqkhZBypRImkDKBFIWkLIk+h1JWdL+zrmeNCWSDFB0DYquQvWG637TcnozAKxbt45yTr8PAGowGBwVDAbvmT9/Pvbu3dddijV9WdUUUE0BUQm6kwaCYe+ljK/w8ruUdsYCBLlMEUQhoJoCygWM+LIvHTx4sGfevIbqYMD3BSFkJVUUrG5oaFABoPXwhd1UVUBVahtpKtoOnEV/gSHHgBwDso5c6XO6yNF24CNQTbV9qBRUUenuwz1/BoCZM2dplOJeSggWL1myFEII9IeXziIKBVUUW1QKo2Ci41Anei9kkWcY6Ex5R8qfc0wi0ZPF6QNnYeQNB2j7IQpFOtg0WwiBxoWNIBKLVQI6Z8rUqTh69FiWaFNmEIWgLFShoM5TZbIzgVxvFp6ID5rfA6JQgBAIxsGLJkrpAsycAcH4gN1gX0QPTW9vP5Grr58cJJTOpbqmjgWAnp6ei4QSEEJAKAGh1BbHCS2DLAFmMAgmICwObjDnyYMMAtJL9oN89vRc7KWUQtOUsSqhSggA8sWivSEh9qBxTiCEAGRwQARUVaB67Hf5pZAQlA0Ayrq2LTCogVyhlLURNEw55yYABP2+4ED3vHSClBKQ9jiFdHqvEBCMQzAOKYSt6/RqSGnbDPJRbgT93hAAcM4NyhjrBYDKylhswEEZJgYJFxDchnGTwSqasIomuMnsIDiH5GKIzUAQTsCVlZUxB9xLIUVbKpVEff3kiLTMfimEA7HP5bZgHMJ07mnJAiuaYEXT3jcZDMLkTgBD7exgBKRp9NfVTQwnk0kIKduoJGRH8/ZmhMNh4skc3DnEkDlAi4GbtjDDguVAmZM1M6yB68JyKsCGBqD373s7GAySnTt3gBDyFhWCvPHee/8HAJhTU5g0BMg4uMXBTT4AZSUTrGjBKpiwCnablQbDbZuyfTmAuRPMegA4euQopCRbaCaTOd2XSLzX3d2Nu+64bR7PnP3LJSCDMBm4YW9FWcmyQYMytsW+Zpfdsm1MdimAdMc7K29bMedCdzeSyeS76XT6jLNI4PGf/+w5aLqOu25IjOOWKcSg0jJjcLZ2ecsZD5TdybqsOxC0ZYpbJ58frek6nn/+eVBJHgecjXkqk2nu7Ozcdfz4cdx556rJN5C3m8v3jBt2xpdnazjysawNy5lUbKkrbmtZsWL5pGNHj6Or62+7k5lMy5CFNRQKTfN6tAMvvvhSRe3EOqx/4oXXLvia7qO6CsVZrey5154KB5YpKSG5tHs+5/ZsZnEIk6Ei1fLH73373i/09fXi0fWPpgyTLchkMqeGgAEgHA5/vjJWsf2PmzYr1dXV+K8fP7vjLxduWkY8ilpetQZPg+UJxh63lzqlNDi7gTa3fuPraz6bzxXw79/5FutP51am0+kdZdaQ/2kzDKNDUci51179w8pbP3er8sAD6+pnVCWy+/fs21LAqBnlMT50qJXFLq2a2L/5gaVy7N133j69u7sb67/7iFHIFf4tlU6/Ppg1kLGU8hYAywBMeOWV33gfXb9+1Q+ffDL+4Ne/AcYY/tS8PbV5++4Dhy+MopY2ZrLiidQDgDBSp5TS+Y7psS65ZOHsW26++eYosxje2PwGNm586eKzz/x027+sXWsBOAfgbULIQQAgUspaAA8BGAfnsamrq4u0tZ0Q333kkdGmZS3f8JNnlBXLV0AOilRKCS7sWYlxjlKxgHw+j5Y3W/C/Tz/NQ6Hgjp9seKZ31py5ajwe4wAtz9zdAH5OpJTPAqgEgL5USkpu4eLFHloqFXniYh9t3bunauuWrStisSi5//4vYnHTEkyZOhWqokBICcuy0N7ehr2trXjt1VeRzqTl3ffc81bjgsZELF4pQ6EAqa4eI6UEicfj5dhTKoCikynx6Bop5C14dJ2XcjmouipvvGFGoSJaWfr738/7tmzdjl/88pfIZjKwnH2SpmkIhSMYW1ODhvmNGFcztjhudFXR69Wgck58Hg+XEorH5ylDJYA8kVKOckpdB0ADIBOJhOzv70OhUFILuTzPZLNcSE6SfSlvJp0O5A1DN0qGDxLS4/OUAh6PGQqHC5XxeJEQgkgoRH1+L/wBP6LRuIjH4+Uf8gSAUwB+MbhzzQSwCMA0p/QUQADgNJ/PJ/v7+wnnnFiWkJZhKCYzKADoqiZUXeW67iGcSxKPx2QoFAo7AybnuE8COAZgHyHkxGXjeFAQEQCzANQCqAIQBeAH4AXgcex052w45TMcyQHIAOgBcBbAUUJI5uOM/wcaHmf3g9UM7QAAAABJRU5ErkJggg==\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAA3vSURBVGiB7Vt7cFzVef+dc+/d90OrJyO/JSO/4ncxxfULMCYIAyEW08amJJgmM4GmnZjJdNq4gcSGzLQxk3bsaWcaaIHyR8CJrWAbpjgG/AhINsbYxkaSDY6xJFvSrrS7Wu3uvfecr3+cu1pbXhkJs/4nujNndufec77f+d7fd+4uw8gvIxwOfocBaz0e91yXyx0BgKyZiWUz5kcEvBKPJ18EYI+C5rWvkpKSyZGS8LGHGtbQR8ePUUdnB50/f57OfnqWWlpbaN++39O99fdQpCR0NBKJTBwJTfZFE4LBYLmh8+YXXvifKctWrEBPTze9+cbu8/3JVMoWNjwer3/ZsuUTvV4P239gP36yceNZW9CtyWQyei262hcB+7zurU/99Ge3r1nTgJdfevFsqr8/Wlc3rWbGzFkV8+fPr1iwYEEJgLadO3cmbr/jjohh6KXHPjxamsmar39pjoPBYHl5aUnnqZY2/b1Dh9LdPd39kUgk6PP5PD6fH36/Dz6fDx6PF+fOfdZ9+pPTgbq6Ou+aBx+0k/0DVYlEIjYcbX4tYM5pxeK/WKIDwM7Gxt0TJox/dtLESXC53JuHzvV4PBVHDjfvAYDZs+fonMsV16R9rYeM8XG1tbUAgMrKsrDP659DRJ5gMNhbaH5NTU0IAMaPHw9IPv5LAxORy+31AgBcLsO41lwAcLu9BgAYLheIkftLAxfzGgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4D/lME1ke7gDF8ltbOHe3W923oEwYi1jxftWfZWgAziwacZkd2pfyN96XN5IIu7dMtIKA9/TI+zqCnFps2Alg5UlojFnVqIHZUlO2sl4RyC4CU+SEEylux8Z/iyc7mrxw4U7UnYwvGpXMYKIgNGdwXC/76C48oRw3sDWfnCgIkARJXcpwbvpA1e6T0Rq5jDr8EAHKA6OpjUOJwfeXAJAEhAXAGgEPKq+dIMVJqowDO4RAAC0rHV21u5LijAJaABAOIAY5Oh15iFMgj1zEpcUuuXjpIWeCouxjAtnIZcGKA5AVFbRfazPUC50QrKe8+Qy8qiqjBYIODA5DgBd1pBO9WRg9sy7yOhXBca+icYrgTOUGOiKnIVdCdisAxJGBTPsYW0nHRrJqgfNmGVtiqaeR1xchF7Vgz40q/BUNmISlcL7CUgJAMnOUiVwEdF0PURIAAVHaC8ucbAiwcQAb1KQpwXMjFrhtYMcOVO8lhOB457ujcKZd9hBguSYwcelTupKyaQWKYJFEU4xJw/Dhfcw29ilSBcNjEoTucFnSnkeOOvvTJpcVC1cYoGB5NAGEQTukjMAzHoghJghyWCRjenYoTuZjKx8xJiwU4LrSZ6waWpIoBjTuRqxDHRUkSUMWAJAZp6QU5FqOw65HHapG3bGVcBTZXDI5VnFaFgBL1yC34uoBJqEJeIwD2MMY1ilZidAFEMlDOqm9UdpJ0ZawumI+LU9ArwhyqWxyNz14XsBAMUnLVH0ttGB0XococdCGWE3XhOV85MF1WV2OY3omK0S2SkxgYAZYYJoAUpcqEEjG/Ru80isA1ysMXYNCnCum4aKUPgTu90w3sFinXL6nO/MadCAhiKloxBjFMeSuK0S1Kylv1cE1bUVoYyHwhoI6bCswpjjuxK5u2G2lcti2jzNCRTluioHEVw52EBA5/2LKsLBL+h2gs/o+Fjpa+MqtmjCbkqQJSYFF3T3zRsPMvA75i7UiBA4FApa6z5+fNnbd6/frHADghk7QdlhAHdMY0KXkZAHAuozaRMDRtKYMdAYDVq1fjcHPTD860nZlsS3qsv7+/+6pNDr0RDAanGTrf85Onnq75/uNPIJ1O4+dbnj34Ot6B4eFLqksqUeEvgcflAREhZabR09+Li/EorLQ4eFv317D2oW8t0XUdu3a9jud/9auztqD6ZDLZOixwOByeouv8D1u3brtpxYrb0XS4Kfbj3//8VHC8d0nDLXfj67OWIeQJgDGADfoOAxHQl05i14l92PHBXiTPp/c/OrFh9vwF8yMnjp/A5s2bOqXEbX19fX+8CriqqspvmunDTz/10xkr71qFnY07Tr1i7aqsLg2Vb6h/GOPCpdAYgTPlNLmF5AzpvBRp74viX3a/hO6+ge47+hZG61fVTz9y+DCee27Lx15fYFFHR8cAcNkPuw2DPXfP1+vvvf+BB7Br967WX9Mbk70eCn33zlWoCrsgKAFBCdgy/2nLBCyZgCUSMGUSpkzC0G1MrKzE0XMt/la9I0QnM+cWL15cmkwmK1tOnwpksuabg8YVifjnhEOlj69dtw6nT51Kv2q96fYG4fG7gbJwFhn7cxicIJgEZwAfEiokGASpWG1KhvIwg1/91ti1N9DEJ7ZOzKxdt87T1Nz8A67jv2Kx/o85AJDk//zXjzzCAeA/D7zU6PZjkkuXcBuEjN2OrGiHabfDFB2w7HZYoh3mVaMDWWdu1m6Hy5Bw6RIuP6b87+HXdgDAww8/zIXgGwFADwQCFYFA4BuLFi3CoUN/6LRmyL/y6gSXTtC4QDTVgQo/B5iEJFJ6Rt64lI6Vfi3JYBFHd1JA5wIunUNIQvpr/C+bm5u65s9fWBnwe9dISWVc0/DNhQsX6gDwTuuhd3WNYOSGTjjSehGp7EVYsguWuJQfssu51wVTXIIpLsGWlzBgXsSRM5dg6Hk6uk787Zb39gHA7NlzDM7xoM4Yli5fvgJSSiRmmbP9HNA0Qm4D6axEc6uJ6eOzuCloQuOOjlneqiUx2BK4lDBwut2DTFaHoXFYGilaHEjMMOdKKXHb4tvw/nvvL9UZ+Lyb6+pw/PjxpOZhsziX0DigcYLG1QaEBD69ZKA7wRHx2/C7BDSNwEi9AEmZGmJJA/1Z9SJM12hwvcYBzgmaj89obW3pr62dGmCcz+cuQ68GgEtdl7oYU40CZwSeW+As1rmy5KzNkbY1WILDlOp71ubgnKA7czVO4NyhwQhcFS7o6urq5pzDMLRqnXEtCACpdCrFHOHlAsTgYEq0nCnj0jnBY6i8KCTLBxbmzB2yPkczmU4lAYAxHtKFECYAPeDzBQZD4GU+motMueXklECWc7QkSaVDGoTAVetz8AGfLwQAQoisbtt2N4BJZaVlpZQjkntdS8w5UFOFni0YLMGhWfny1rbVPVuoOVKyK9ZeTrMsUl7qAHdzkPyktzeG2tqbw8KihCQlPjVUl2hLBkswmDZD1mJIWxwDWTXSFkfWUs8sZ64QzlqHjiRA2tQ7ZcqUYCwWgyT6hBNjb+3ZvQehUIi52tje3M6FyHHIYNkOqM2RsTjS2cuAs+pe1uYKPLcBkduA+m60sH1+v5/t3fsWGGP/x6VkjR98cAQAMNc7bXJepAyWzWHaimjW4siYDGmTY8DkGMhqapgcaVM9yw5ugMOyeX4DkmGub1otABz/6DiI2O94IpE4E+3p+aCzsxP333PfAvOi2G8JBtMRbU68GZMj44Ao0BzXmgOsRk7spq1oWILB6rQP3nt3/byLnZ2IxWKH4/H4pxoAeFzuC21tretW3rUKnk5mtWiflzAGxhgDQ66IYyrnOnqzBFfDZjAdLk1HMnkpMWRNLldmFomamtrIL/71F+iPJ/8mnc2e4QDQm0jsOXfu3L6TJ0/ivtX3T607M26P6SzMWI5eB7ktPHLPc/MV5xwTjpe9sfLOu2pOHD+JCxc+fyeWSLyZdzCoWsvjNpqef/6F8KTJU/DDLT/a3jM90eDWCS5dqmDvxF7NCRSAOikQhCuMUXHMEDjm3v7jb/+oIRrtxpMbnuzNmvatiUSi7QpgAAiFQneXlZbs3rGjUauorMSmLc+8dShy7HbDELqeA3bC4GCScHxWSMDOgVuaPb2t+t3vPfK9O1P9A/j7v3vC7ov318fj8bdyWFf8YCSbzZ7VNHb+tVdfrV911ypt/bcfq52J2uTBg+//LhWwZ0nJYTtWf6WrcccDGFgLdn5nwkPVD9Q/MLOzsxNPbvhhNpUc+G5vPL7jcqxBjonozwEsBzD5lVde9jy5YcPqTZufKX90/WOwbRv7330nsffDt08dSB41EkZyHPfwmwBAZuTFsBm48GeuWfai2oUzp02fFjKzJhp3NuLFF/+765e//Pfd31q71gLwGYC3GWNNAMCIaBKAJwBUO3uQnZ2d/MyZNv1vn/j+LUuXLq/Z/MyzCIfDTmxW8Y+IVFyWqjKRQkDYNqKxGDb97GkcOXLk7LZt/9F8c12dqKqqYM4LYALQCWAbI6J/A1AGgKK9vSBhoa8vEe+N9TwejcZYU1MTfrN9O6puqkJDw0NYtnwFpk6dCsZUMrFtG22trTiw/11s3/4aotEo1jQ04NZFt6KsrJTCoZKtJaWRiGG4KBKJ5BJWnw4gDedAx+0yMJCywLnQGWOSMabV1NbikUfX40J7B367sxFbt25DMhGHZZkgAC7DhWAojOpx4zF3wS0YP64aVZUVYCoQSN2la4bhIsNlcOS73H5GRBUAHgcwBYABAD09PZROp1gq2V8WTybq4vH4xEQ8oSWSSfSnUkinM7As9RdUw9Dh9XoR8PsQCgYRCodESTj0x1Aw2OrxBXsDgYBdXl6eM2IB4CyAbZcb12wASwBMB1Dq7C4ACJZIJHstM5PWdC2TTmcom80wEtySAFwupum6wbxeDxeCuT0et8/v94UBTTrSJABRAKcAHGCMnbrKjy/bRBjAHAATAFQ5NuAF4IFqAtyOKzKo83MLgAkgA2AAQB+ADgCfAzjBGIsPxfh/6wbDK7xbMFYAAAAASUVORK5CYII=\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAyUSURBVGiB7Zp7kFRVesB/5/S9PdMz/ZoHMwo4MICDuoGVIYICIuzGcn0vC+oWGuNjs8mua9ySP4wpgyaiVVupbHYTsLJmNT7WNXExwqqzrq8g4oNxdXUgyEMQARmZd3fPTE/3vfd8+ePenhlgBsFlrFSqb9Wpvn3vd77f+b7zne87ffsqjv+wE4nYDQqWl5aWfDUcLqkAyOUHunID+Q8EnkilMo8C7gnoPPaRTCYnVyQT71+1bKl80PK+HGw9KPv27ZPde3bLjp075NVXX5FLL7lYKpLx9yoqKuqOR6f6PIFYLFZtW7r54YcfqV+4aBEdHe3ywm+e39eb6etzPZfS0kj5woUX1EUipWrj6xtZedddu11P5mYymc5j6Q19HrgsUrL67r/7+8VLly7j8cce3d3X29vZ0DB9yplnfWXcrFmzxjU2NiaBXevWrUsv/trXKmzbqnz/9+9VDuTyz35hi2OxWHV1ZbJ1245d1ltvvpFtb293Kyoq7LKystKysnLKy8soKyujtDTCxx/vSW3fsT3c0NAQWbpkiZvp7a9Np9Ndo+nWxwJrLYvmzV9gAaxbt/75urrxd592Wp0Oh0tWHSkbiUQSv3unuQlgxoyZltZm0TF1H+umUnrC1KlTAaipqUpESmMzFIRjsVj3SPJTpkyJA0ycOBGMnviFwSISLolEAAiHbftYsgAlJREbwA6HESUlXxg8lkcRXAQXwUVwEVwEF8FFcBH8/xhsnZC0ksw49eQPI5mmNtP54ccAIvqgqbz4aYn8zYoTUXXcFnueyZ8eXtleZt75iQnpU0VUvYiqB5mvu5p+XH9w8RtgnJMOLut/7rd4+fpRBcS52hz65csnHdxQ8clZnyuT3NV40sHRUnfq58mUWFJ70sEn+yiCi+AiuAgugovgIrgILoKL4CK4CC6Ci+D/Q+Djf/higk8Jzs0IMjIGYDGAp0AUeBbiHf3Xs/HGAHyYlYaRX0EYC4txNeIFugvWHyXzua8cnDjYGMBoQIFhRFfLmLjaCxqAw8iuHing/nCwGlLuMrKrveNfnccPFnyLtQ8c0a1jElye8sGFAYwUSCN54Q8GB4ljKKpHkBmLOZbB4FLgjhLVYxNcDFnkMXJUj03m0kOKR0sgYzLHRvlwpcDYI7oaGYvl5HB4ZRrJ1cf9fP5E/5NwQUKM7uoTOI4/ql38kmgUOCMnEHMCL819sag2jJJAxgIs+HNY6PGlpUxXDQWXw5dXjxH8SFZBPf7SyqKrMQLKG7b/OkpmTBJI0BSjbwTGYo6Ni5+ZjMJDj1wkxmQ5iV+VsBh9BzImKbNQFhWjp8wx21c7dKIV9A94IxaJsdplZt9574JQVcUdpr3rzlEHdzLASslpg19EofLMMa3dc0Z9c9YMXT+s7/GCo9FojWWph87+6tmX3XTTzT7XA/F4xutXr4fyOuQZVQUQ0tLphY1nlcn5YqgAuOyyy3inefOtH+36aLJr5Obe3t72o4w68kIsFptuW7pp5d33TPne928hm83yLz+6b9PVb/4niRK9QNfUoquqUaUREEEG+jGd7Zi2Dnpy3qYHGr7OFdcsX2BZFs899ywP/fznu11PLslkMjtHBScSiXrL0m+uXr3mlEWLFrN58+auxD+u2HZWhb0gcvkyShZ/Ax2N+70KPcVvJpMm999NZJ99mi1dzsb3rviLGbNmz6rY0rKFVavubTWG83p6ej4psAbfr66trS03xtlw98p76s+bN5+nnvzFtouevK/s1AnJM+I/vB37j6aDziJeCtxhzUkhTgoYwJpchz3zbJI7fj/pzA829f6iR/bPPW9e9aS6utjbb715YWVl1SOZTMY5DGzb6scXf+OSS6+48kqanntu55+99shkOyLx8uuvIjSuDEzq6Ob5TdzgPJ9GhT2sCbV4W1vK57R+FP9lOrT33PnzKjOZTM2OD7dFB3L5FwaDq6KifGYiXvn95ddey4fbtmWv2fhIiVUqpbpMEao2SH4fiKCMgAbRggSuVkKwEQz22q4iVKtQEYUtJvzdlvX6+bq67PJrr41sbm6+VVv8W1dX7/9oADH6b//0+us1QO/jD6xPhGWSCgsqLJj8PsTdjzj7Ma7fxDkAzn5wjry+H3H2YfL7UGGDCguJEqnPPf3YOoDrrrtOe56+C8CKRqPjotHoN+fMmcObb7zRelsk9W1lC4QFCRlM9yfoKnsoEgOLVWCxDLfYBRwwnXmwDIQVyoMbo6lrfrq5+dCsxsbaaHlkqTFSpUMhvjV79mwLwHvjldewBGxQlqBswXn3Y6T/EDhtiNOGuG2I2444QXPb/WtOGzhtmL7PcN7di7IFFegiJDq3+ZVXAWbMmGlrzRJLKc6/4IJFGGO4MdQ+gxAQEn/2LcH0u+Sa27HO0IRq/V+MSqnBOUZARMAD75DB2w4mq8AKWkggpPiOtJ3dYgznzTuPt996+3xLoc8+vaGBlpaWzFybrygtqCPgeODtcTFtBl1hUBHfGgl+wNGv8FIayWjE6KCfD1UhBVqotPWZO3Zs7506dVpUaT1Lh21rPED7oUNtKH8OUYLSoHTwWRiEAsmBDIA4gCPIAJh8YL3lyw7vi5JAJ7QdamvXWmPbofGW0qEYQL4/0zeYjdTRTQ0Oxp9/Svx9jvKAkBocsCh1dP9AZ76vNwOglI5bnuflAaukPBo9bM8UpMIjvxeiWAUbATHK3/yNJM/h30vKozEAz/Ny2nXddoCKyqrKwc5GDYFMUJmM8peLqyCvkH6FZP1zXP+eGBXIFvQcrquyqroyALdrxGzv7u5i6rTTE3lX0gUL/DIYPPfwFDh+k5xCBhSS1Ui/9s9zQ/cLz0rEGxqEGMWAK92T6yfHu7q6MCLbtSj1UtPzTcTjcfW0E3t5EBSkv0FgPgAMQgtWa/9azpcZHICrhvR48B+52CvRaFS9/PJLKKVe1Mao9e+++zsAtk9rnIwbLBFHIQ5IACWvkJxGBjSSDeDZ4HxAIznty+SV38chGIA/PXumzZoK0PJBCyLq1zqdTn/U2dHxbmtrKxddfmXj1r7QRr9jMH/5Ye4d8OdV+odZ3F+AqyG3F/oFelr62PQnl14667PWVrq6ut5JpVJ7giLBygfWrMYOh3ll/pLx4iojR7p3QMGgpQX4kPUE8OFuF0chrjIvzL78VDsc5sEHH0SLWkmQLuhOp5v27t376tatW7nk8iun/UN8VhM5BblASS5w53BowdXD4L7Lg8EG7Z6SM36z+MILp25p2cqBA/s3dKXTLxRSBeDvtUpL7M0PPfRwYtLken791z9Y++fevmWE/WJBIelbgJbDtz4mePblBksrcPU/ubVrF65Yuayzs50Vt6/ozuXduel0etdhYIB4PH5RVWXy+WeeWR8aV1PDz+6/56W//PDFxbpELGULgwVEcwSYoWXkKExOuatqGl9b8p3vfb2vt5/b/uoWtyfVe0kqlXqpwDpql1lVlbwhUhr52VNPrQ3PPuccNm16PbXrR3f+9pvm0NV+pWEwhQKIqKHnm57iV9nydc6Smxc1zm5MHvj0AHfecUeuv7f/u509PY8N5wyCReRcYCEw6YknHi9bcfvtl9276r7qG2+6Gdd12bhhQ/rghhe3TdmywT4l2zkhEeIUgJTLZ62RygPbT5/rlv/xvLOmnzE9ns/lWb9uPY8++u9tP/3JPzd9e/nyLLAXeE0ptRlAicgk4BZgfDAGc/DgQb1790fWrT+45Zz58xdMue+++0kkk/5N8RO2iPiZ0BiMCMbz8FyXzq4u7l91L5ub3969Zs2/Np/eMM2rrT21YKQBPgPWKBFZAyQA093drTzPobu7uyPV3XNbR2enam5uZu3atdTW1LDsqqtYeMEipk2b5m8GANd12bVzJ69vfI2n1/6Kjo5OvrVsKefOPZeqqkpJJCtXJ5OJinBpRJLxeOF3bI8FZIAYoEN2SHmeJ6GQ2CiMUipUP2UK199wI59+2sp/rVvP6tVryKRTOE4eAcJ2mFg8wfgJE5nZeA4TJ4yntmYcSimUUsaydMi2wxIKKTXM6n4lIuMCV08m2O52dHSQzfbpvkxvZSqTbkinUnWpVDqUzvTS29dHNpvFcfy6aNsWkUgp0fJyYrEYiUTcSybin8RjiZ2lZeXd0WjUra6uDg2L/z3A6uHBNQNYAEwHqvAXTTl4Kp3O9HhOvk+FGMhmHXHdHGLEE8CytNY6rCKRsPY8VRoOh8tisfIkhFxgIAB2AtuA15VS20ZcTsEgEsBM4DTgFKASiAClQAnBig7EC8/8BoAc0AekgE+B/cAWpVTqSMb/AlY1WXIncMcxAAAAAElFTkSuQmCC\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAxNSURBVGiB7Zp7kFTllcB/5/a93dMz3T0PemYIDgoCPhZ5iaD4wNkFjQjRRMlLTNbSlKlyzZpobSVbFRPUbNVWSRCWuKvlxqybtbIrukp4SATZCAgospEBgeElj4EZ5t3d0+++37d/9O2ZnqEHQZzZSlXfqlMz/c253+875zvfOefeHuH8L6u83P+AwH0lJZ4pbrenEiCVSnYmEsndGl4NhSKvAJkLmPPcV0VFxZjKivKPv77wXr274WN9uvm0PnHihD5y9IhuPNioN216Vy+Yf6eurAj8b2Vl5aXnM6d8loLf7w9apvHhyy//29jZ9fW0t7fpdWtWN7Wdao4qpaiqDpbdXF9fV1paKpu3bGbxk08eSWXU9ZFIpOPirC33v7xs+TIdiUT0Pz239NjeaTOTHXXjdb4cuP6W5DOLFx/7aNdH+oknfqQryv0vXZTFfr8/GKyqaN7XeMhc//ba6NSfPFXqS6fESJ29jdGAX69+9KHY9OnTyxbec08mHInWhsPhzsHmNs4FNgxdf+NNN5sAh3/7n40dCxeKedUsOr6x8CzdsnBEQu9sPABwzTWTTMNQ9eec+1x/FDEuGTduHABXtreOKutJYyiFqq4tqD+5O3wJQF1dHSij7nODtdZuj9cLgMfGOpcuQInSFoDldqNFez43eCivIrgILoKL4CK4CC6Ci+AiuAgugovgIrgILoKL4CK4CC6Ci+A/B7B5vor6Mz4PNnbRYAAtoCQLUMMFVobuBWOALWdjVIGxiwbbZC3WkrXWLqAzJBZrR5T0LWTgdSHfdF1YcIlG57t8oM5nfov1OcCKPmDW1Rfi2IsA5yI5F9WFXF0o0i8arARwggsBu4BbhwaM6g0ujXY+9b+GLqrzLR5E5wsH2ziB5QRXoW8lCy3mosH553iwlDlEe9znai2DpMyhAJ+PxUNTJMhZm51+WM9xvsWFXD2kx0nl9rjQ4oYC3C+4BoEMnasl39Vn6wxRdcqbXApXpwupWBcEVgLKGLw6DU1w5bkaCjcChcYuHozuLYtqEFfroXC1TZ67GcbjlEuZWjSIHr6ozjZ7/y/VSWOLdgJIF9zjQl3JFwDOXn1lsYDOULm6X+YaROcLB6s8+LC2tzqvoc+Wx0L2nT/6wlIm5y6LQ9bs5TLXsO5x7jG192lxuJq9bCOg0aIRGcYEkt9lCsPp6lxlMsBlFE4ghcYuGoxznHKFYNjKYq7Zy5XFYW32lMtCBGzbLlwWLwB83m/2NNC44R0iFaP503+8jO1UqHz5wiwW0aNzvysgdPJTQr/7dFD9fHD+vecN9vl8NaYpv546ZeqCBx98CMhGbPXEqZRfcTWmyySTjuO2TMora/B4Sji+832OnWoGYMGCBez88IMfHD50eExG6Yd6enraBjJcAwf8fv+Vbsv1Pz9f/NT1y1esQCnNPz6zeGuy6WBN+MRRrwp1YMR6MOIJMqEuOj49xNFd2zh5aD9SVpr44PCJXVOmXXvpHfPm4fP7rtz98Z/usSz3+lQq1e/fnvuFSHl5+VjTNLb96lfPj6yv/0t2bN/eufJnj+37Uql1c/1Xv8WM279CaZn/rJcBGoj1hNm+7k22rF5JcyK1edp3Hps0bfq0yj0Ne/jFL55pVopZ3d3dx88C19bWlqVS8Z2Lf/7U1XNvu51Vb72x7/irz9fUBEcEv/03PyFYPRJDgZHt9XpvzG8QlAFnWppY+S9LaOnsaPPOWdhxx7z5V320cydLl/7yE2+pb+bp06dj/VxtWbJ03h13zr/r7rtZu2bNwVP/9cKYMiHwtW8+QNAbwOiOIN09SCiChCKQL+EIKhxBhcN4EGpGjuJww66yxNH9gePac+zGm26sikQiNY379/kSydT63uCqrCybXB6oeuS+RYvYv29f/OTKFz1+dIlXXFQrCznRjNhkRfdJzmIMEAExsqbUmh68holWGXf43deMg6NHJ+5btKjkgw8//IFh8lJnZ88nBoBWxpPf+e53DYC1Ly5bVSb6Mo8WSrQgx5uRY6cHSDMcz0q/vx/PSTNeJXi04EOPfe93L70JcP/99xu2bfwUwPT5fNU+n++rM2fO5P3332+uS3V9y9KCG8FSmtjRo3iN0uz+qqylemDnLhpDQDsFJGrHMG2F2xAyGi5Nhr65Y8f21unTZ9T4yrz3KqVHGC4X91x33XUmwN7N775nApbuk90nD5BpbUbaWqG9Dd3eju5o6y/t7dDehrS1kmltYffJ/ViA25nDBcbeLZs2AUyaNNkyDL5minDL7Nm3opSiNtQ0yUQwESydlXg6xc70Sf5CewliYSD9TqHu/anpIMUnJIiLjSVCGjAFTA21odNTlFLMunEWO7bvuMUUjKkTrriCvXv3RDyiJxpacGVXSc56W2uO6DhtKkmFFsocHchmtKhoukURNrJPG5YDdAEuDYaAV/TVjY0HesaNG+8Tw5hmuC1zFEBLS0urkQ3QPtFgILgQTC0IkAZSgEJQCClnTBwdF4KBOPf2iQBnzrS2GYaBZblGmWK4/ADxWCzqoS85iDOZDFiMS2ddV5Kz2EkGhgwECYLOzqOzxy0W7YkAiBgBw7btFIC3tMw/2JsrnS9OI5B2pPdt0AC9gdVZZxkBANu2k0Ymk2kDCI6oqsw1c/nNu8rVW8l+2ZFCkxRNzMhKUjQpNBlnv23nXfbAeTRQHayudMBtBlod6OrqZNz4CeVprcKqd4KsZBxgGk1KNEmBmGiijsScsZRo0s4CMnn3284CMqJCY8aOCXR2dqK0PmBokQ3r1q7D7/dLq7tyY8axMCOatDNZFqhJiCbuWNsLNrJjCUcnt4C0ZOew0WTQnDYr3/X5fLJx4wZE5B1DKVm1a9dHAIyYesPYjEBa+vYwJZAUSAgkHAtjookaWcl9Togm4eim8u5PS9YDNVNmXg7QsLsBreX3RjgcPtzW1rarubmZ+QvumtahXJvzrUzmWRvrZ61yxNnvPKuTA6xvt13bvjxv/tSW5mY6Ozt3hkKhoy4Ar6ek6dChg4vm3nY7oZJAJnG4oUIQESdD5Ud0v30XSBlZC1OGdjyTA/darwK3LcxcPm585ZJnl9ATinwvnkweNgC6wuF1x44d27R3714WfOWucZGrb3g7kee+eJ6LewPLcXU0bzwuuf2G3P3NoyevnzP3tsv3NOylqenkHzvD4fWQ197aikeW/nJJd1dnJ4//9On57V+a8Hoib7K4kQeUAWL0D7RcsJ2oqHv9wUcfu7Orq5MVK5Z3KS0P53j96lsgEPjyiKqKtW/891uu2tpalvzDMxsTW96s9yhMC8HUOCkxm07JO/fZk5A9dkmDTOSqWe/99fcfmRPtifHY3z6a6Q5F7gyFQhsKggFGjKh4wFviffG11153T59xHVu3bg3968/+7g9V3ae+0Zv0kX49l3ISjA2ccpe/NXvR9+uvnX5tRdOpJv7+xz9OxnpiD3d0d/97PqcXrLWeBcwGLnv11d96n3j88QVPPf108KHvPUwmk+HttWu71q96Y0dozzajJBUfXyqMA4gpfShmeY54JkzX19/6VzfMmDmjMpPOsOqtVbzyym9alz23fM23Fy1KACeAP4rIBwCitb4MeAQY5SxEt7a2qIaGBn70wx+OTKXTc5Y+t8w1d85cdN5KtdbYSqGVImPbJOIxotEo6/+wniXPPmsH/L4Ny5etaJk46Rqprq7JPTgooBn4Z9FaPw9UAHR1dSnbTsuZMy1GMpnItLZ2GFu3bq5d/fvVc0ZUjZB7F36d2fW3MmHCFZguF0pr0uk0Bxsb2bL5PV5fuZLuUEjfdffdG2+66ebW6mCVLvP5qa4OAoYEg8Gcg7tNIAIEADHdJnbcxmNZ6UQ05nK7TT1x4sRYRVV1/FTTqdLVa9bywgsvEImESKfSAFiWhT9QzqhL6rh25g3UjbokPnJkTaKkxFRaa8NtGbaIy+Up8eS2VgEx0VpXO66+HKfdbW9vV93d7RKNJl3xeNQOd4d1Mp0i3B3yRCKRsmgiYSVTaa9orS23lfR5vany8vKYLxCIeyxLKqoqtddbKh6PSVVVtQ4Gg5IHPQI8nx9ck4CbgSuBarJnvARsiUai4XBPmGQyqbWGRCxh2VrZAKYYLtNjZUyXSxsuU6oqyg1fwO91nhUSzvQdwB5gm4h8UvA4OYsoByYDY4EaoBLwAN7sYiDvZ4LsqUo60uNIK3AY2CMioYGM/wPREY0iGUY58wAAAABJRU5ErkJggg==\"],\"useMarkerImageFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', amount = percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"markerImageFunction\":\"var type = dsData[dsIndex]['type'];\\nif (type == 'thermomether') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"color\":\"#fe7569\",\"mapProvider\":\"HERE.normalDay\",\"showTooltip\":true,\"autocloseTooltip\":true,\"tooltipFunction\":\"var deviceType = dsData[dsIndex]['deviceType'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}
Energy: ${energy:2} kWt
';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}
Temperature: ${temperature:2} °C
';\\r\\n }\\r\\n}\",\"labelFunction\":\"var deviceType = dsData[dsIndex]['deviceType'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}, ${energy:2} kWt';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}, ${temperature:2} °C';\\r\\n }\\r\\n}\",\"provider\":\"here\",\"defaultCenterPosition\":\"0,0\",\"showTooltipAction\":\"click\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"showCoverageOnHover\":true,\"animate\":true,\"maxClusterRadius\":80,\"removeOutsideVisibleBounds\":true,\"zoomOnClick\":true,\"draggableMarker\":false,\"mapProviderHere\":\"HERE.normalDay\",\"credentials\":{\"app_id\":\"AhM6TzD9ThyK78CT3ptx\",\"app_code\":\"p6NPiITB3Vv0GMUFnkLOOg\"}},\"title\":\"HERE Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" } }, { - "alias": "route_map_tencent_maps", - "name": "Route Map - Tencent Maps", + "alias": "image_map", + "name": "Image Map", "descriptor": { - "type": "timeseries", + "type": "latest", "sizeX": 8.5, - "sizeY": 6, + "sizeY": 6.5, "resources": [], "templateHtml": "", - "templateCss": ".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 200px;\n white-space: nowrap;\n}", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('tencent-map', true, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('tencent-map', true);\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('tencent-map', true);\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n", + "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('image-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('image-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('image-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First route\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.5851719234007373,\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.9015113051937396,\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.7253460349565717,\"funcBody\":\"var value = prevValue;\\nif (time % 500 < 100) {\\n value = value + Math.random() * 40 - 20;\\n if (value < 45) {\\n \\tvalue = 45;\\n } else if (value > 130) {\\n \\tvalue = 130;\\n }\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"
${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH
See advanced settings for details
\",\"markerImageSize\":34,\"useColorFunction\":true,\"markerImages\":[\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic7b13uB3VdTb+rrX3zJx6i7qQUAEJIQlRBAZc6BgLDDYmIIExLjgJcQk/YkKc4gIGHH+fDSHg2CGOHRuCQ4ltbBODJIroIIoQIJCQdNXLvVe3nT4ze6/1/XHOlYWQAJuWP37refYz58yd3d6zyt5rr1mX8B7S5Xo5/0nPYaNFM1PY0gGqOhfAgQCNBGlWFFUAYEIeihigbhFdZQwt85BV5Gj9r/718R2XX365vFdzoHe7w6d77xnPkn4YpAtU0YiizNJcmPNkMQFkDiSlowHt2HNtGlTSJ6B+pTpsKTfKgTj3Pi8SMtFtEZnFs8d8dPu7OZ93BcCHtt0+OiL+FJjOiqy5K5dtLwD4PBHGvy0dKLYo8B+1+lAldv50FfmFzWX+84i2M3a8Le2/Dr1jAKqCHtl2y1wC/pEMP9ZRLBaYzF8CCN+pPluUkOKfB6qlmk/dBwTyt8eOv2AZCPpOdPaOAPjA1h9/SJX+TyGXuz0TZi4EcPBeOk+U+RErZh2YyMAyQJEoZUjFgtkCAEScgDyx1hmInTglqDj2U1X0WILaPbWvwHO1WummeuLONhaXHTf2wsfe7rm+rQDe133j/i5xPyrmCr+OouhSKPbdQ5fLiezTIYUBQGMJBgYWxMYSISZhbxgQT8wGAgDiwWxUvCiBxKhSKOqdh4OyV5+6XiEfK/kjVOXQ13apG+I0+adKpXaG0/Si0yZdvPbtmvPbAuCNT98YTBhT/8fAmEpHoXgKgPe/6gFGP0nwG8s2YykcaRCAYYQ5tKTkDVuArDEwMRF5AICS4VZ1AQBSr6oEgL36CBAvlKqIsyLOKQl5TZH4uN+TawDuY6o64lWTJX20v1S633uJNvfmvnbRERelb3XubxnAX26+5gDy6Y9HtrU/wERff1XjSt0WwULDmZEMawPOgilgQ4FaGCEygaXQMQyRMaxiUijUkAEAImIGAFURAOrVA1AmI1ZExGuqoqkVFefhyGtKDql4X4eHc6LxJof0VIVM3nVc4uXaHUPlo0Tpc2fv/zer38r83xKAd6y74iImO31EMf9REA7cpdVBY8NbA5+dFNqsCTQipkitBjAUsLUZNd4qm8AyjDMmJAIRhDzDEBEbJkBVAyJWQJ14AEaciIeSGicOgBeBWNHEeXLkXIM8UvFI4bVBCVJNfdk7STd5xOcp0LZzjIqV/eXq/4i61edM/eaN7yqAqpfzf62Nf5LP5lbko/DbCuxU4saEN1mN2kKTzQbIkuEIEWfVagRDEVkOyXCkVq0aDg2p9YYNAySVerU0WN1R27Jjo6ulMQ1V+ggAOgsjNRNEus/IiUFnYUy2kM23AcrivXh2RiTxjhx5iSmVWEWdpmhQ4qvwSBBrXVPfqDmuVsT7C3aZvKslyZcr9dpxdr81F8ynO/w7DuD1q/8y6kDw2872ticN0deG7wvQHXHmdxGK+1ibQag5ikweliIElNUAEayNYBCSRQRiYzf2rNtx11O/rC5d9dj+1aQyM2Pyz3WGozaNisYNWY7SYtgWA0A5KUVO4qAn3t4+lOzYt+Grh+bDwstHzvjA2tPfd1Z+39FTRhGpi7VBKrE4nyBFDKcNJL5OCerqUEXdVeEQb0mk8lECjR0euxe9cqBUOnoQ6RkXT78hfscAvH71X0Z5kf8Z0dH2CgNf2NkI0d0ZbmtElMtFVEAQ5BFIlkKb00AzFJqCGooQcJjv7t868P3/ubayZvua48ZlJt57xLjjB/cpTssXokK7IQNrbeoZ3pIRJm1aYSUW9cwixglZ7xNU40ppY7mr+sy2ezt7G1s+vP+EGfd/+fS/Ko5pH9/pJK04X6MUDSRapcTXkXJN46QKp1UkqNVqvpxVyLzhOajihh1DpVkmrJ7+uak/bbztAF6/+i8j62p3j20vbgXR+cP3LYU/Djg/KcsdEnIWERcRIk+hzWtEOYSch2U76tk1T6+84Tf/NCdni2tOmbRgy6T26WOiKDBhGFEQhrBhiNAyjDGiQp4DFgI8AChg1BGBXOC9p8QJ0kas3jvEcUxxnLgNpTW9izfdOqGWlve7+OOXrThk6qEHKtKehq9xIlWkvoaYytrwFYqlglgrcZxW+oXSz+ycpOLmnsHypDTIfuTNcuKbAvD2288x22dn7hrVnt/ATBftBE/CH2aCtqkZU6CI2hHZomS4YCPK+5AKHFB2ZNe2Nev/739/e9qY3KRnPzHtQp/LtnfkMhnKZDMa2oDCTIjQhghDC2MCCQITAyYxpmkhAIAZDDA7l4bOSeR9YpLEwfkUjXqMOE0QN2LU4waq9aGBX6/+d7O9sXnu3579jbVTx02dlEilL0FDG1pJG64cJX5IGr6MupY5duU1npIv7sTQ4196ytUDx8+sf+TN6MQ3AyBd8+L8W0a15zYw0d8O3ww4vC7ijlkZU5QctVPE7QhNEVlTRNYUjHcy7tu3fuuVSqXBF8z66962fMeIfDaHfD4nmUyWsrk8BdaYIAh9EFoxzExEysYoAQ5A0ioAEIpIBGZmAM459iKaJo6cT209TnyjWkOSNLRWi1GtV9A3sGPg56uvG1vIZ9N/OO9rM8jS9oavSOwqaEhZYh3khq9K3fdpXWsbvdR3MoYCV/UOVadcOvv2C/AG9IYAfue5j1/U0R5mIhNctxM8yvxLyMVpOduJyLRRnto1MkXK23axlB27sXtT1z//8vqDTt3vk/fMGnX4xGyhiEI2Qi6X1Ww2S7lCIQ3DkCxzQEQKYADANgCbW6UHvwcRaO6fAwCjAewLYAKAcao6UkRIBEniEtRqNVOrVKjeSFCP61oaqurKvqe237P2lnkXn/X/PT9l3OT9Eql2V90QN1wZdRqSuhukhi9T3Q2s9ki+NDzHWppeUqnG/qsH/+b7fzSA33ruI7ODIDh/RCH6KkEZAEINfhia4n4ZO0KzphN5005Z06aRaeOAcjP++4Ff3P/86hWTLjr08i3FfEeurS3LUTanhVwe+XxOwjAw1loLoB/ASgBrAdSAV232Gc0NyJGt70+27mlrzNT6nAEwDcBMACO892kcx1KvN6hUqWu9Xka9XsfgUP/Qjcu+Nf3g6bO7zj7urBNT1F+quxLXfUkaMmDrviQ13+8THdqYqvuLZpfq+qrJNXFDbrp87t0v/cEAXr5iduiTMQvHd2QnKDC9+bC9NUfF9kwwgvNmBGW5Q3O2SFkzAkaCg/71Nz9+2MTZ6rlzLs4Vi0WbyWS5o63N5fM5G0VRaoxpA7ChBVw3ANMq1AKoHUAewCwARwHYvzWctQCeaNUrt4pvgeha17Gtevt47+M4jrVSqZlSqepqjQpVyyX/8xU3VBHF2T//+OeOFbgXaq5fa75ENR3SarzDxDToYz846FTORbPRV7oHG9sm+qEPX3TEM3vc9pm9AfiBP53+T6Pbwo0Cd4aog4p/yXK+lDX5IDIFZDinGS7CckEM+JB//u9/e3Z8NGPTgjl/Maq9s8N2FNtcPpc1bW1tFIZhaIxJATwFYA2AtAVWh4hERBQByIgIE1Gsql8gou8AeAjAfQAeVdUvEtE9reFFIpIloiyATgARgCqALQAGmHmUtTYTRWHDhhaGYE0YYmbHEXZj//rBRc/fXTly5qGHEus2FUceCbxP4DShRJ2mvuIFboyqG5kNcNuWVM965MbNd71pAC99+vADA+MnR6F+TeAg6h1TeE/I2bbAFjVLBbJcpIDzZNke8qNf//yxKblZWz42+9Pj2opFbutop7ZCQdva2hAEQZGZXwGwDEBDRCJV7VTVfVV1BDNPUtXZqnomER2tqi8S0REAzgJwUqvMI6JBAM+p6pdU9f1ElGu1E6lqUVVZVYWI6gA2EFFijJmSiUIPsDbXmGT3b59V6Kv0dd334uLGYTPmHK7Q7lRi65DCawqviXWSrEm1PlvgWMh9KPbut+/77Ohtj/97d98bA6igo7aM+O/Ogp0l8BNFPQhyY2RyE0MqcC7Ia2jyGpksBYj2//WDCx9uk/EDZ8783JhiW5HbigXpaG9HNpvNMXMGwAoR6SWiUKS5KhERS0QqIgmAHcz8sqrOA7AdwCcB9AK4CcBvAdwP4EVV3V9VPwGgC8B4Zv4PIqqoqgPQYObEOadExC1A60RUJaLxURQaZqoRW0NEsm/xgI6u7rV9L295vmvGlKmHQ32vk0QdxfA+oYTq+Vgbi70mR4p6BEaKlTid98S/9f4MV7wBgF/66AEnFbPUz+z/VNTBiywLgxxCFDgwGQqR5wznOeR8+6p1657r6uopfu7wv4mKbW0oFvIoFovIZDIBEXkReUlVG6o6Fs2N/EjvfSczj2Hm/YnoY6r6Ae/9w0T0cVXdSkTfE5FsC8iTAZwI4DAAjxDRj0TkUABTACxS1csAzG39MHlmzqvqGCLKt1xZA0Q0QERtQRBkDZMngrcmNAeMmB08uHpxNsrz2pFtbft4TWInDZtSLE5T8i7uSKRS8XDjBX4fYbnusI2jMkt/tGP9rnjxrl+gICP4Riagrzb1ssKa4CkrYRhwwBFHYGSUOZJKo8oPP/vCoV846opSoZCnQj7HxUJRMplMgGblR5h5wHtfbE1oZAvIHBFtVtX7RKTQ4pSrnHOXAThQRK4BcIaqNkTkRRF5UVUTVf1462/TVPVSEfm2974qIm3MvBhAl6pGAEYAaBcR45zLiUiPiDxKRC6bzZpsNhtGUaj5fIG/dNTltYeeWja3ltbVcGgMZX1IWbUUqDUBbBA+OYxDPuDLSORq6KsN76s48MvzZnwwlzNDgaFzAIBAi0LKtGVtEQHlOaQCQpOHoWDWL+9+ZODCuV99cnTbmM5cIY+2JudZIpronHukxUWemavOuZIxpuG9H8fM8wDMJaJHVfV0ANcDOIyIPg5ghTHm+0S0UETWq2oCoA/AI6r6C2PMgyKyD4BPM/MggJ8COIGIFqnqV1T1YADbVXUjEfUaYxrOOcPMBVXdCmCutbZirQGIlIBwavucl2577NaJM6ftO1nJ9aY+YfEpvDryknamSNdAMQ1AGwxdc/DqDjz9k/7Nw5i96ixBSK/MhTRxJ7oUbracmWAoVGNCtRSCYOxLazfcN7VjdjK+beK4KAqpkMtpJpNRABNVdT2AowHUvffjAYgxZpNz7hUiuk9VT1LVWFX/iojuBfA1IrpfVRcS0Xne+6tUX33+M/zdew8AzxljLvPefxTA3xPRIufcpQA8EYUAFhPRSCKaKSL7EFGgqjtU1RDRZmaeGIbh1sh78s7LxM59R09um7585fqNdtqUMZOMMc4igE0DthSppcYWL80VTNbyX1QCPgNN1fJqDvzi0tnjQviObGia3Ee0JEAml+E8DOUo4pxaE4GUJz3yxJr9/vSIv+8uFAu2kM8jl8vBGNNJRE+q6grn3AZV3QRgi6q2AZjHzHNE5FEAp3vvv8HM8wFQSywvADAPwDgAi0TkPwDcBWDhcFHVh9FcXH9ARE4BMI6ZvyEiHwYwSVW/CeB0IlpERJeo6hwiepmIlnrvVzLzemZex8yDzDwZqlUikGGm6R0H66+evuPYafuNynvFkCCF4xjiBd67otN4C4GmEDAqTuVnR3++beWT/z5YfRUHio8/0dEe7DynJTUvswmmEiwxWcCDwGyee37j4ydNO6ucy+YmZMJQM5kMWWvHqmqPc24eADCzENEGAMvTNH2AiM5Q1W1E9GkR2cLM3yOiS0TkO0R0lao+zMy/8N7PBHAmEZ2C3YiIoKrdqnqjqq5i5j/x3n8bTQt8iapeKyKbjDGfFpEhAGOccw8EQdBhjPmQqk723rP3PrTWvhxF0Xgi6vHeayaTyx075fS7nlvxcPGgg8ZNIjHeSKRMdbEUIEHwEuCOA4DOvB25vSRnAfghMGxEFNRb7ZoM0HFNadFeIjvRgMFkhEDKbEl8Oqq7u3bs+/c9cXQUWo2iCGEYsqrG3vvHAPwEwL2qulZETnXO/Zm1FqoKVf2Bqh6qqr8SkW3e++tU9T4i+ntVnem9vw7ARQA6ReQ5AL9yzl3vnLsewK8APIfmovkiIrpWVWeo6t977x/w3l8nIluI6Dcicqiq/quqgpnJOfdnIvJR59wmEVlCRD9S1QeJKLHWmmw2hyAM9bhpp47q7q4d733aSVBlkBoNQGxgYPdVRZ82N5In9lS7dp42GgA483hMyUY0RXgwXzAjQgUtshp1WhOR5YgDzoiB0U2baqsPLB7z0oxxBxWz2Rxls1lh5gNVdbn3/rwWR68moi5VPZWZt4nIvgBGquoRAH5BRH+OprH4oYh8XlVPQXMvfIOI/BJAFxF1qupxRPRBIjpKVSe3dOtdInKbqj5PRIe3RHayiHydiMYDOIuZfyIin0HTfI4kIgAYa4y5UUQaAI4QkY8ZY5YR0aGq0kcE8k5NNS4t665u6G9r47xDCi8pqabsNbFe9WkoRvU0upYl8GunnqebX7kZQ00O9DipLbKjRfQTPWnXYyBTBxMBBiIML2IVkt20sf6B46d9rJjJ5chaQ0EQRAC2pWm6VlVXq+rZIvIXSZKELcX/Y1U9RlW/AWC8iJyqql9V1aOcc99W1SXMfAmAh1X1qy3O+rKIHCMiGRGptUqude9iIrqWiC4brisiDxHRt1X1KFX9qnPuowDGe++vUNUPishNLQkIiOjPVPVs7/02EVkLYHsYhtYYg0wm1FNmnZPftKF2lFPJisCIkhE1DFiFaNLr1i5R+PntGR5lFMcBLWfCxxbhrgkjgqMAjCKgkrWFX48KZ7RHJm8CziJLOXJpUNu4omAuOfbKOMxkKBOGHIbhHBG576qrrtLHH3/8QmaOdtdd/5tIROLTTjvtyc9//vN3BUGQs9aOA3CyiDxXr9dRrzfo2gf/Ljt1TpyYIMnWtQ4nVW2kNd+bri41fOlMADkQerb1p4/f+WGcaS9X8HOLUQIwCgCUdFGi6ehBt7k+3k4DqQ8cOd2+mQdPnP6xijHB+MAYhGEoqppL03T/J5544iRmpvnz5z+4Zs2a1dOnT5/+8ssvr5o5c+aMWq1WSdM0VdXORYsWHW+tXXbmmWcONV2jQG9v744dO3b0jR07dvSIESNG3HbbbbNFpHPBggWPtMTvVUREWL58ee2VV145bcSIEU+ddNJJ1RY4unLlytXTpk2bEoZh2N/f37dw4cKTrLUdxWLxvnnz5pnf/e53unDhwhPa2tpWnnfeecekabopCIIMEYGIyBjGCfufvmbpltuKY6a4LKkzCh8PpZu913g0oIsAOhOKMQTElyvYPrsY43IRP6uK8wCAYHrUo+gpiXoaG+LR0X5VaNgxNEAHz5pz6PIgMGBmBTCKiJZVKpUjjDEmTdPG/PnzPwSgLCJHoLlY/omqXgLgWSJauHjx4uNPP/30obPPPnsAwGNoLl+O32Xdt/a3v/3txnK5HM6fP/+3aJ2JAAi89zkAUwGcdOqpp+YvvPBCnH322fEJJ5yQA3CH9/5YY8yft0C+SkTmP/roo72NRqPjhhtuODCTyRTPOuusRy+88MJVd9xxx8cWLFiwiog+oqp3ARgVBMEO7xVzJ70/v2jdHbNGqu/16uq98WakmuQgANhsU98MRQwMP7N0iYxhUuybD/n3WzqlAMROROElzfY3NrXHrtTNFHTkMvkiGQNiZhGZ7ZzbPDx5IoKIXK2qZzDzd9F0T/0pEV2qqoeKyN8BwLZt27ap6hmq+l0RmQXgZhH5iohcpaqrwzA0RATn3DXOueta5buqeoWqnqWqT9dqte8DwPbt2zeKyBGq+l1m/giA7wL4map+jYj2S5LEA0AYhp0AvsvMp5577rn3Axi/YcOGxaoKEdkCYBYzqzGEMMgUWILRjXSopzfekFUf5wUKYXYQCoZhykcM08C+DMUMw7Rva8sHqHZCJFD1VtTDaYLuoe3xrLGH/Yu1NiZVtcYAQEVVy7vpmPNU9VHv/RUArgZQ9d5f473/qYj8OwBMmDBhPIBnnXNfAfAj59w5AK4F8DURmcfM1JrY/4jIrSJyq/f+XlV9vmVMPlEoFC4GgM7OznEicmPrB3hJRC4Tkc+IyI+897cFQWBay5lrVfVKVX30lFNOOUZV/aJFiz7YMi79RFQiIgbg2NrazHEHf7+70q1eGiwkROoteQkhOmIYp8DQBGUcYIVwOJMepCCAkBCooCAnUPVwXoU1rrXVoyi7nwgoDO1QyymwzTn34d7e3p8B+NsWFx4AYLP3/l4iuoKIHhaR/yaiLw1z6rp169Z57+cR0bUiAiIaVNU7ReR5Y0xcrVbPbf0ek1U1DwCq2qOqG4jofhHZUi6XAeC7IkIAvqCqIKItaG4LZ4jInxERvPevtK5fY+b7W+0eBGD78uXLx6nqd51z85i5G0Bore1rNJJsxuan1EumFo3w3mtKSupAMASNRJEACBk6ixWphWCaKs1tqegVUIWyiBcPIYhRQlLKhQccNDtW9YEIh0TkiciJyGFtbW29LfCCxx577PtHHHHEhdbabd77bzLzFap6jPf+X5o46Jf333//qWh6kP+P934HMx8F4HQA53rvkc/nl9frdYjIQbsw99SWy6opPvl8BQC6u7u3ENFfq+poVb1IRK4iIvHeX7dy5UpKkuR8Zka9Xv9WNps9n4j2B/DNkSNHnrV9+/ZRIvIhIjpMVZeoqlfVEcyQ6WNmpQ8+nyva9m4IO/XeQ1XFE6UKfYkUhyrTEVDEFkAWO4NuZAuAsPnDKlgFzih8ku0cU5y4NQiCxFrLAPYDUCOizxpjrgAAY4y54YYbvtwS5f1E5B9UdSgIgloURR8BIESEO++8c8qmTZtetNYeHYahdnR0wHv/pIhsrVarvX19fQsA5H71q1/dYq01pVKpkCRJXCqVaGBgwDcaDdfX1zcRwDELFy788JIlS96XJEnBOQcADSIKmfkSIsKwpXfO/bmItBljLlHVa6dNm/bIE088sR+AMUT0WRG5kIgmWWtfIWPcuPZJDJ9r90hIRVTEq5KAlBIIdYH0UCg6FMhZUvDvjSDVnZBhUhUSUijICxHCbDFXZGOMqKoH0KmqQ/l8/ptdXV0/rlar38rn8zs5hJmJmUM0jyPb4/j3h/ze+ylLly6dgr2QaepX3Hnnnefv7ZmdoyUamyTJWABoHvTtmbq6un4xa9asSQCuA7DSWvtSo9E4zHt/dbFYvKLRaKwF0E5EwoBENlKVMOPFkcJDCRBVUlEloLQTLgWz1987FAhImCECJVEh8Z6cdzBk20ITkIg4Y4xX1ZFoHuJM3XfffT/S29uLLVu2oFKp7HQ9/W8ia+2RzHyGqv6TiPzjsccei97e3kxbW9uZACYTURVNb7mIiIYmJIOwLUWqTqQVIqFEDFHV6nC7orDMBB22LOzhWbRC0LJRLalqGYqyQWAJVDPGVJIkqQPYrKq9AGCMmQoAaZpix44d2Lx5M/r7+5Gmbzn4822jVatWvei9/9M0Ted77/9j5syZawAk27ZtswCgqt0AtohIzRhTssZWDdvQkA4RtETaxAOqZSWWnXgR1Kr8/kTbG2ThtaAE9QQSZWIQ2EilFteyhoJCa4lxYMvf9xry3qNUKqFUKiEMQxQKBeRyudcVsXeC0jRFrVZDtVrFzTffnOnp6Tl2/Pjx944ePXrt9OnTzyGirY888sjLCxYsOERExhPRDGvtswACrz4m60pOqIMIBIX4ZqCYAWsZLXumAtid6z8A5DSvlgkKFkcMiBERqHUDiUu8994SkQCoEFF+jyPfhZIkQX9/P/r7+xEEAbLZLKIoQhRFbzugzjnEcYxGo4FGo/EqCejp6Tnv5ptvfk2dH/zgB8sWLFgAVS0CqHjvyTlnq2mFYF3VORnJICKwI2IFI0Qi7TCtLaYCVgnbAdoA6GRhaoPXhipIVJkEUCXP7CrleBAd2RHsvYcxpopmfMreaICZN6LpQWYRmZSmaeeuk7LWIggCWGsRhiGstWBmWGuxqwUFABEZ9ilCROCcQ5qmcM7BOYckSYbd/XuiTczcT80YHHjvZ6MZZ4O+vr5hx+14Va1Qa/M9WB0Asa+SUCcIRuAtg5QEBKDYrEJrwdhiIXhBRQyIJkMxQxQvkELh4RUq4kCJ2VHdOLiOx+YmmTC0trWwnQOgsvtoiegFInKdnZ3rRo0aJT09PTw0NAQAm0VkzvBzw5N/B0mMMU+pqhk7dmxXsVjkzZs35xuNhojICDSPRpPt27c/WSgU5hLRC95722g0aOPgWnbcW5VUBYCSJYBBChgQzWnt2J4BsJyheFkVr7Q6Hc2kZYU6ARSejCjZFN259UOrc6reOucMEfWpqnXOPQIAhULhN8PgMXNl3rx5Y4IgOIuZz46i6KyTTz55JBFVmXnFO4nYrmSMeTKKooEPfvCDs40x8621Z3d2dp566qmnxsxcArC1s7PzkVWrVi1X1QBAv/eeiYg2DK0upOgpiCBQIlIBBOrBOgTCCAAQ0jUQrGS1WF1vUPewLlTlKoQCOARewOqVUgzmtlXWTWuKiqiIVAAgjuOtuy1bgtNOO21ET0/PhO9973sQEXznO99BT0/PxJNPPrkDQAO/97C8k7RBVaO5c+ce19nZmb3yyisxZcoU/NVf/RVWrFjx/kMOOWQ9M3dXKpVRjUYjbKmGinOOnPPYWt04PZGhjHoQCZigAQsFpFwbxqlRpx6k6LI6gK5Kpz8zm20d0JHWQFAYTSUlALDexSNdEB+Y+nQxpZRlppSZ4ZybdPvttz9QqVSOt9Y+SkR+xYoVxx522GF4/PHHceCBB2LZsmWYPn06nnrqqQOZ+REiekZERr+T6BFR37hx47rWr18/NwxDvPLKKygWi3jhhRdw5JFHolarzXvuuee60jSdYFordxFJnHNI0rghiGc4jb3xUDEQEngyYEBrwx7KcuJHZzux1t79KZQ++iv5AHTnCadVBZGQhULh1SsIMfoe7KlsGRqTm5Q1xmkQBJtV9dijjz766f06bwAAEgVJREFUnpUrVy4EgIMPPjh300034bjjjsOaNWtQqVQgIjjqqKOwZMkSzJs3b/Xy5cstgFUA3rZF954cr6eccsrYxx57DJ/85CexcOFCDA0N4cQTT0S1WsWjjz4azp49+4l6vc5Tp049TVU3eu/hVXVbZUN/TH33k8c4DVRIiMFEohCjCIdXLC6VY+44DV+zACCEXiiWgnCkEp1EpKsEqqTEIsTq1Axg+eCy/kczp+QmqDZfuXpRVedNmjRpx9VXX32hiEBEsHTpUtx5551YsGABnHM47LDDcNNNN+GAAw7Al770pc8NPzdsUXe1rsOA7n4dBmjXK3NzgbHrZ2beWQDg7rvvxq233oqLL74YS5YswY4dO/Dkk09i7ty5uOCCCz4bx/FPRGSUiNydph71ap2W9T9eGGgsr4iqZSVVsLJ6Z5lIlU5srfmWAlgHtE7lDjgP5SjgAWb6MBTtoroMgpwoERTwniiJhwq5aPrxB+YOWwuQIaKEmWd573NBEHSoKosIpk+fjltvvRWqitWrV6O7uxvLli3DV77yFRQKhVeBtzcgd/2+exmm3bl3dy4kIowfPx4LFy5EpVLBpk2b0Nvbi+7ublx22WWw1ro4jgsARgJYVq/XUG/Uk2fK95+ypXxfrESGGUIEMhYGTP1ovQOYOr2+kcjvVt+K9c130cp4slyX4nDnBqYbRCAGkTZXUELIVtPeezeUu3rjOEaSJFDVpwEcmKbpLcMTnDhxIm644QYEQQDTPDvBNddcg3322ec1IL1e8d6/qryZOruDffTRR+PrX/866vU6kiTBAQccgOuvvx5hGKI15hki8lTz76lura/fUUt6F4siJIKCiREAakhB6BnGp1ST9lwbngJ2CfE99Zd4cPzIcDqg4xl4wQl64EE+BlyicCnYanHz4RMumviR9vO7C4UC5fN5JqKzVfXlKIomtzzGr5nwGwGwOxe+ngi/ntjuXowxe/s+0Gg0+ohofxG5o1KpoFqv6+LBn496dssPt6dcmWAtlCOCNRDKgJgxEopDoLRl60Cy5p5P4Hhgl/A2NbgmTuUGBeCBOUTokVZAtyiIFJSk5QmJlJKeyvaeer2u9XpdVPVxVZ1Zr9dv25PI7Q7M3sDbEwe+0Q+wt/b21vdwqdVqv1XVaar6eJwkqNdj9JY3bW9IKU5cZRwUDNPcuagBE2G7Kg5RAKnI9SD832HcdgJIARYOVdyknXtjoTpBoaRsTPOMHQy7fMutQy/qQzOr1arW63VNvd+kTc/NfO/9I3vTXXub0N5E9/U+v57Yvp7+VFWkabpYVc8DMJSm6aZyqcSNRk1fxOMHPb/5v+pQtWwgUBCxErGCiOJhXHYMuRkU4r7XAHj3aYhTAaC4rakI9dNkMMSWPBhMSsRKmjRKIyuuZ3Bzfe32crnGlVJJReQ+Vc3HcdyuqgPD4re3ib1ZHfhmVcDuYO4JxNaYetI0HYvmMen91WqVqo1YNqVdW2uutz9NSp3KTNpcxMEYgjEYVNULmvVxiwLVu09D/BoAAcAZXL6j7F9SBVRgiUwPkRJYCQaqrEoMWrrqp4WN2ZfmxXGtWq7UqFwuJyJyP4A5cRw/qKryelywNw7ck+58I336ZvtR1Uaj0XgewMEicl+5XPblcpXqtXJtk33x1KUr/6MAbnKdgQKsDFUVMTtUYFWBvpLvohRX7orZqyJU192K6tSz9Qv5HPcQaCpBZyvjRSiyEFIVkDioiBbL1W3LglGduWJ9LKDExnAtCIJEVU/w3t/MzIfsbiD2dn0jHbkrF+1qSPZkXHY3MMNX59ydaB5ePdNoNLZUqlVfrpSxOvO4earr5xvqvm8iGfggBFNIyiGYQwwQ4xwABqqLhmo+c885eJVf7NUx0gDE4iv9Q/JYc1+MDABvDJQs2DDYhlBmxD2Da6YNxOulW9dsr1TLWiqVtF6vrwawXFU/7Zz7TwB/FCf+MUuW1ylJmqY/F5GzVXVZvV5fWy6XaahU5q26asuA22L7hlbvR4a8NVAYKFsgMBACJZDm7mNHSZ41HpfujtdrovS7bkV58p/oRwpZ8zIIhwM0C0SLoBipCmqNnaHAhq3L7MT9D9mfhjIrrYRt3nu0fG9VAKd673+Npq8t82a5cW9ADdOb4bZdljfbRWSpNt9BeSJJknVDQ0MYHBqiwXRHd9+IriPvffpa4YBCE0I5grCFMRlSGFoF4DMt3ffDUtXLPfPxyzcEEADGnoNH01gWFLNmChQhgTJEOqiKQIQEAiPNU09+Zf3jfZNnH3yY9mVWasoFL16sMWVm3gzgNO/9KiJaq6qTdlfyewNv9+f+QNCGPz8qIgLgaFVdVK83egcGBk25UtWBel9f/4Q1x931yFUbYLWNIxgOoDYgDSJYE6IB8CEEjFKg1D2QdscVfHn9r/EaB+YeAdx8B9z0+Sgz8HxgeR6AMVB6hgzaVMk3Q/2JSQHvJOra+GTXlMPmfEi6o+d87NpTLyTeN5j5ZWae6b3fV0RuIaKZqmr3ZJ33BNzuAO4G0B7vMfOQiNyqzcBN8t7fN1QuN0pDJVQqJe2v9u2oTt9w0l0P/uNz3iQjghA2CMmEGXgOCSYDIqJuAk4AgHrDf7We6u/uPx97zO6x13fl1tyOtfucqRcXM+ZFAHNAmA2iu4gwRkBKos0jAVXy4vKvrHvslWlHHHZk2m1eQKJ5VfXOOauqG4Mg6FXVj4nIalVdpKoHqSrtsrzYed1VXAHsDaQ9caAQ0S0iMoqIPkBEDzWSZHWlXI6HBkvBUKWsQ2nf5uSA7SfeueTqFxPUxtpQAxMSmxBqAhKTBZhoBYALAUCBW3ZU/D6Lz8E1e8NprwACwKQv4nf1fvlUMWsJwEgC5oDpIVJ0EhGrJ6sAICCXuvYVqx8uzXj/YZPSWFbWelyHeA/nPRLvqwxa3XRN4COqugrNKPwx2ozifxVww1y3K4CvA95WAHdQ8xWHDwJY4b1/tlwupwNDVVTKQ9rfP6j19h3dsv+Ow29bdEWvUmO0CWBshowJCTZL3kQAW1pPTb1noPTK9oG0no7Cp9b/7LWi+6YAXP8zuMnn4rFG4kfnQ3MYgIgIU5jxDCmKCigBpE1xZlEfvPDSErffrFkU7BNQpSutxQ1PLo6zSerFi9RV/CvMXFXVQ1R1H1VdhGaIbxnAzgQ5u4vtLsUx8yMA7mPmbQAOJKI2VV2XJMlLtVqtViqVaLBUlUqpn0vloTofOhBVMptzv1h4dd4Yn7cR1GSJwwhiQhIbIjUBthBwJoC8ElzvUHqzKL5+/+l4zQuGu9Kbyplw4m04Ix/xjI68+W6r2gZifdI1dFSaEEtdOW2AJYG6hnqXEMaOnL7ptGO/+L5kjVks2/JjM5nIZKJAoihLmUyIIAjIGANjTEBEHSIyWUQ6RWSdqm5V1YqIpC3RDImoQETjiGgKM5eIaKOIDKpq4r2Hcw6NRgO1egzvUq3V6l5Hxhuys9OPP7T0lke7tj41nQNiG0FtBmojeBMR2yzIRNhKQh9U6L6kkMGq/7t6Ii8uXoDfvRE2bzprx0n/hc93FLiQi8x1zYq0CdAHvcdkV4V3Dupi9b6OgosR+wRGvU3PPuXSHcXcPiMGnvAvcJIZlwsjG2UzMESUzWa16SExZGxLGFS9sVbFK5SUAGBYWYoIMzN5BbnUgSCaph5xXCfvvSZJouVaw1NWejrfL3NK1a07frHwmpFsXcgRvA3hTRahNeRsHmKaXpZtIDoa0P0AoBb7SwZqEt+/AP/6ZnD5g/LGnHwbvtlZCAYzAYbzJwwo4U5xOl0aUB8jcDHUxUSuoQ4pJE0gmbCt9vFTLm4UM2NHDCxNlidDweiQOAyCUDkwFLBBEFhSZrVEqkDzHLEVAiA6PFBFE0pFkjhS9YjjVJ1Lkfg0sZ3SO+rI8NBSo7vvznuuz8S+lDMhwBbWhmRtVr3JgmwAmAhqAlolij+h5svfqMW4ZKiaFu49F1e/WUz+4MxFJ92GS3MR246M+bYSGEAizD8mJ4d6p+oa8L4OcQnUJzA+hhWnqU+gUdA2cPKxnylNHj/rmOrW9N7+F5JGOiQjyXIYcgC2zRejiVXFw5Np5Y3xMGxgxBMJPMSlFHtPUI1NG/eNmhNm8uODUzZse+nB+x78WVs9KXXaDMgYspyBNyG8iQATwIRZwIawYPOCQj4LICSFDNX9V6qJ5O5bgH/8Q/D4o3JnnfhzfC6yvM/IdvPXADpaLd0KoaJPNS+xmjSF1QYkTeEkVfYpGR8j9Q5WRKvjRkztPf5DC3j0iCkn+AQvlDdUu6rbXaPWn5KrCEEErTwXTTKALbDmRgSaGxNk26bmppoQc7p7ux546PE7ZHvfutHGUJ4DOGMRmEi9sSQcwgYR2GTgOCRvDFXVaJUU81sA9PcM+X92Trru+yT+8w/F4o/O3nbyrTiaGF8cUwgOIMZRreZegerDgB6YJiQSw0uqgYsh3sFrjMB5eE1gfAovHka9pjaM+ke2TxiaNnWujBkzOcxnO/KFXKHNBpnRAODSRm+lVh6q1odqPT0bkjXrnuW+oS3tLo1HsKGADIQDsAnhjEFAFgmHsDYCmYBSG4BMRgMQvQTQcYBOBwBVPN5TStd6hxvuPx9L/xgc3lL6u5N+hpGwuHl0u33a2N/nDiTSXxBIRHWCNMilMdQ7DSVF6h1YUxXvyKhD6h0CCKCCVLxa9YASKYlyK/AOIJAyCUFBDGImB4KlEEoMbywCCtQbQ8QhxFiEJqDYWLDJakBEm4g1UKFPDI/Rq16xY9AdZQzOXzgf/X8sBm85AeM5t8P0eXwtItYRbfZToOavCyDxKj81RCPgaKJ3iL1TAw9xCVgdvHcw6uBVm/pNvQIKpwJV2pkKBQCEFKoMYoKFITVGQQxPBsZYeLIwNoQQw3BAjiNEzNioQKzAebQzkJRW9lXcbXEqctx5uOryYUv1R9LblkP1+JsxjS1+MDJn7wkDuhKEHACQQqD4OUgExJPFq/EpqTglcXDqEXoPJYETDwbgROBVAQY7ABCIJQKYYQBYZogaWGMAMkhhEJiQPLMaG5BTlvWUsgXjvJahAxS1RqpfH6i5eYjxhfs/i7clj+rbm8VXQSf/HB8T4LOj2uwzgaF/0GZ2oeHuVqjq48zIQzHee4QiSLUZgwN4kDYdt0Kkqq38BM1XhYnAMMwKGDQ979y0rERIRbENQJWIPgDorF0m2Ei9Xt0/5N4njH+//zzc9XamRH5H0iAffiOC9gLOVeD8kXl7bxjyxYC+OqMv0VaoPsCEukAigNqg1EEEFlWBQKHUFC9SBoOYiEUhRDoIaInBiSgyBDpJoeN2m9qG2Mv1/SV3iir+s1zFbc9chLc97vgdzWR+uYIfugUnC/C3keUlHQXTaQiX7LUCox9en1XwIBENCqTcvM1FVe0gSAcMzYVgxN6a8IrrBit+IHFyrCF850Orcf/ll781Pfd69K7l0j/mJxhtLb4+ot2uDy3t1T30Vihxeml/2U1WxpVLPol3PA088O7/MwI6/ib819j2YDOb154vvBVSxfXdA+nEBz6Ns4G3T8e9Eb3mUOkdJsW++NT2UjpHVO/V5vrvrRfVh7f3pTNLdZyLdxE84N0HEEtOgMsRzukdcBUV2vRWwYOnbTuG3HZXw4J3wki8Eb2uQ/WdojW/RLz/n+CluKaZTMhzm4eJwB9aFHADFf1X7+X6h/4MG9+LubzrHDhM934KLyhoaSPB3/yx3Nco42+811UPfBbvWvD67vSu/0eb3enEn/K17RkeNExXvPHTvyfxeuVQQ0be9zn50hs//c7Re8aBw3T/Z+TScl3niuBm9cCbLLeXGjr3mA3yl+/1+N9zAEHQ6oA/rxLLBPF49o1Fl54vxVJ08Ge/kwvkN0vvPYAAHv8K6ur8BbVEnlNF6XUArNQS/ziJv2jJ5/Cm07W/k/SeWOE9UddvUJ5+pimpYhODTtyT1Y29fsOrv2fxhXj+vR3t7+l/BQcO0z2fc0ucEyeil+7OfV7xFYXI4gvx4Hs9zl3pPbfCeyA67cfmFiaziVX/BgCUcL1XGf27z/vz8S7vNN6I3t23oN8caW0//+lcF/0PC+4VIBJgZm2aPw3/y8AD/peJ8DAtOQEuZLfAQ0sK7Q0rbv6SE/Yen/L/017ojH8LZ5/xb+Hs93ocr0f/D6s769KBP+5xAAAAAElFTkSuQmCC\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic3bx5uF1Flff/WVV77zPeIQMJIYQxYRRBpBGcQFEEbVQQUXB6xW5tWx9+Cm07IYIitiJog2P7qu3UCN22aDs0KIIyg0CYyUhCyHiTmzucce+qtd4/zrkhQIIogz6/9Tz1nHP22buG715D1VpVS/gLktnZjg3P2wGz3RC/N9hBCHtjMgOxGjDZv3UAkyZim4EHQO4i2j3UkxXUb9kkcrb+pcYgz3aDNvLjOah7BSZvRnwLX/8D2axILM0Dtx9ODgGGt/P4GNgtqD6A764i3+iJ44dC9CD/jaRXyqzXrHs2x/OsAGhrL9sB8W/B7ARKwz/H7TAE2btAZj89DbAW6X4HHRknnzgW7KdU5fsyeMKmp6X+J6BnDEAzhLWXHYz4c3GlG8l2HgL3frDsmWqzR5KDfpnOykkIL8D4OHPecIcI9oy09kxUaut//CKCfp7qtMuwwb8DnrOd5nOcXIfJCryAOgEpASUwh5HiBMwKEAW6YF1chIghthtqL97uSxG5C8a/TXvsBLx9VGa/8Yane6xPK4C2/kd7EuWblIZ+itU+BMx93E1OFmLchqQpTmaDA0kAlyDSwSwizkAFfN84RAfOQASLCVACDVgAESOGDZjmSDwEtYO20bVVaPMCionjiPoe2eXNy56uMT8tAJp9I2X10GfxyQTJ4GtADn3UDd6NYv5niKtgyQxcYjinkCSYi3gHikeSLhDwXjHzTNlWB4hEYnRAgoUSmIIqxAQsYEFQA/JRNHZw+lqiTn90R/VGYuNKQl5j/cTH5JD3FE917E8ZQFv23b0oJf9GNnQd8PFH1y7rkORKLJ2BTxJcAqQOSQyXGEiC+IB4wZxHXMABeIeZQ3q/MBQRhdiDNGqCaMSiYTFBKIiF64FYKBbAigKLD6PFsYjt+uhOcwHdiUMRe5fMe8uSpzL+pwSgrfje+/CyK1n1dcBeW/7wMoZll4Kfhy95SARXMsSDSxxkIInifIK4gHpH6kAlggjOOwTBJEFV8FIQDcQCGIh6ighOFdMELQKYYF1Bg0KgB2ZuxG4kFqvw+cmoDG7V/cXk7Z9iulx2efvX/1wM/iwA7bLLPIe1v4m4RSTJuWDJI/+675OUB3ClCmSC9yA1cKn1dF3mSFLDRJEsAYk9wJxSTEzQGW3TXlEQC6G7sde/0gzDZ0Zlt5Ty9Arp4GBfnBWCgxghOkIhkBsxGK4QYgs0gHZAQxPtNLH41q2GH4jhNFRfwrzK20ROis84gLbkohLp4C/IuAXso1v+UFmLlK4gLc/Bl4AMfEWQDHzZIAFJhKQMLgWzhMayjTz0kyYbrt2T2Nq3a5WFE3HWqgYzNxeU81ao5ADVpJ2ldLI6G6YN+o3zStI+CF+9n1kvWcYub6hR330mIgEtIHSAYFgOVkDREegYmoO2QfOHCJ3X4thqDirn0rUX0Kr9rex/Uv6MAWhLLiqRDPwPafdBxN79SC3yS1y9S5JVoQpSEnylB5wrgSvTAzCt0Vqzmfs/32By8cs2xZ2vXGJHjo/KnlVz1aEsLZsXKVQkpIlXTHo6T8wFjU5iTELULFpEQmN8Gsub87lq2gy/5pUM7ftb9jtjgNJO07DQQHPBerMeYqsnztaGmBvabhNbVYivemRwfJWitADktbL7OztPO4C25KISvnolWWMN8OZHasj+L5Luih9QfAWSmvVEtwquAi4DyWay6aYHuO/8A1phYOkd9sbVTbdgVlYqJVk5wycpWZaRJY7E+2i44L2Y0Zv8CkgMCMQ0xOCKGAndnCJG8m5ueacba7pkw/Pksp2rvrkH+3/kXqY9fx+cbiA0HbEDdIzYBmvRE+12l7y1GRffsRUj/JBubR6xdbQsOK37tAFol13mOXjzzymNrQL+/pGnS1/FZXvg64IfAKkqSTnBVRRXEaQ8g7HFK7jnU/NHde7tC5N3RpKB4WqpKuVq2cpZSpJlkvrMSqVEvE8ty9JClSJJJIr44BwWY0xjNC9iWZ4HH2OUdrsjzpm1Wi3X6RTW7XZpdNviwsToQfodP92tPpj9z1nG8Pxd0M4mYtugEygaGdpWQgO05aCxFI3/+Mhg5WsUQ3vx0Npj5GVnh6cHwCVfv4x0ZAViH9py0WcXIpUDcPVIOiBIFZIauKrgKg6z2Sw8c0m72XK3uA+MuMq06dVqzSqlTGq1uqVp6iuVsqRpGtIsM++ceO8NEQQi0AGmuKAElAEfYxTAQowUeZBu0U3zTkc7nVzzvGOtVpdGqyHa3rTpUPvSjpVyreDgc/dGZB3aVrRlFE3QlmBtoxhXtPUQlr/nEVTceRQzd5c9/+GUpwygLf7Ke0jHykjrS1suavpV0vqeuDqkdUEGlKTsSQYUqcxmcvky7v38c+6yt/xqvHTwvEp90OqVTKrVitVqNcrVasiyTBLnUhEBGAXW9ssqYD2QA1MT3bQP4g7APGBOv0w3M4kxxm5RxG67nUxOTkq7ndPO29YYb9pQ55a1z3U/Opb9P3wXA7vtgbbXow1HbBk6aYSGoA2hmFgK+ggnxoEPYENR5r/3y382gHb/xQeQFm/Gr/0ImOuD9zXS6p4kg4ofBKkJyYD1gCzty9IfXlVsumuXm7NPrC6Xh6sDA1XJyhWmDQ1ampa0XM689z4FNgP3A0uBdr8vSk/veXpceFj/9y39/6ccAq7/vQLsCewHDMUYQ7fbtWaz6VqtXJutCWt3OtJubBo7rPjMXsmMA5cz/60vJ3TuJzQEm1TiREJsRsKEElqrcEWfE0UJcz4P2Q9kwfvv/ZMBtD98I6XW+QXZkj0Q9uzdLZcgA8OkdYdMF5KakgwK1AWfPof7Lr52vJk27qufVq0PDKTltOoGBqo2OFi3crmszrkB4CHgAXpc5vpgSf/7MFAD9gVe0AcHYAlwK3Af0AQm+gDrVmV2H8i5ZtZpt9s2MdGwRqNJq9WUVqsZ9m1f1BqqxAr7v++laH43sWHEhhAaRhjzMBkJExNgJ/XhWUK+YDVx9FWy/9nbnN4k27oIQK39FUqLFmLhlX1beB+U67jMIRlIGiF1kApen8PCzy0cYZ91Dw2/dadp9ZqrVWtFrVZLa7UyWZaVRGQc+D2wiR73zARKqhqccwqIquKcmzSz14rImUC135uWmZ0nIrf0fw+oqjjnhJ54d4AGPU6dJSL7VqvVoSRJOlmWSKmU+KxSssVjH6zOa/5g8453nn8bzz3tEHzpDrTrECeQKnjBJSmhdQ+izwEWkNx/Oez9ReB924Jpmxxo91+wl0rjjc4vO7d3wQKu8kP84CyyIUHqgh82/ICQVA7k3m9evybstXb98FtnD9Xrrj5Qp5RlWq/Xnfd+AFgILANSVfVAHZjVf4E1EZlhZs8VEWdm3xCRD/QBfqSjIhtU9SIR+XszUxG5T0Q2quokPV25EZjsv4wA7AI838wajUaDZrujk+NjyWSzyZzJ/3pwTnrffPb5u79B2wsJEylxUonjkTAhxPENaOcURBIADfM/6bT+H7L/6Uv/KIBmiN1z/tWS3TqESM81JMlXSQb3QIYgGwQ/CH5QoDaflT+7cXNjYGLl4Lt3qA/W3WC9rtVqxdfr9QxIVPVeYKNzzoCOqjpVLSdJUg0huCRJusC4qp5FT7wPAlYCP1XVkf5zs4DXAbsCtwN7OOfO7nNiRk+EWzHGwntvzrlSn0N3APYzszjZbE50Wu1sfGKcZqNtu05+ffO0aitlj+NeSphcTGyATkCcgDhmhInlWJziutst/E1D9v2nIx/rmH08gPd+7ijSpc/BRnpW1+RWXGUd6WCGH+5xXjJo+MFhRpcu6q6+a2jJzHO65UpdBgeqbmBgwMrlciYiqqqLnXOFqk7vc4WPMRpQTZJkB1U9FEhF5Fwzu9DMbnfO/VBEXqyqrwGmHKW5c+4XZnatqr5dRA7y3p+uqh8DVFVvE5H1QAtwIhLo6dSNzrkKsEeMMQ0h5GNjY9rpFH5iYizutemTrjTvuS0G91iANcYJE544HoljQpyMxOZcsAN7SM3+AHHPhbLvP/9ua7zc47jP7OPEhz+KdcA64MIdSJKBBxFFpPcGigmx1Tc/f9G0syZrtZofHKi5en3AyuVy0gfveufc5qIo6mZWAWaZ2Rzvfdl7/3AI4SozGzCztqqeG0L4ELCPql4QYzzezFRVH1DVB8xMY4zHq+qFwHwzOyOEcF6Msamqg865K2KMK0XE05vaTDOzUgihqqojqnqD9z5kWZYMDAwmpVJGvT4gS6af1baHbzyY0FQwD67nzBVngMNz4xYcbOXHLMRzzEy2CyB3nfdicQ/8FOvMRrsQu78A2xWJYCaY6zGwxf1Y+quwbNrp19QHB6u1Wo16vUalUk5EZGdVXaiqQ3meO+/9BhFZrqqr6OnAE83sH/ttV4HvAB3n3Plmttg5d6Zz7sNm9i0zu69fvqWqH3bOnWlmy83sAhFpiMh/0JtgO+/9e1X1TTHGATNbb2YPOefGVbUEDKvqQmBOqZS5wcE6lXpdqgODlWXTP/Iblv48RePeOOt5wrUvpZrvDt3/7WMxS9zi/+aezx2+NWSPssImeqbY4j23XJDSeszvBAJODFHBWcrIA1c1sr1zN7DHjqVKhUqlQqlUcsA8M3tQVV8MNEVkjqoGEVkLPKCqVwEvA3Iz+yDwG+BMEfmtmV0hIifHGM81e3T8Z+p3jBHgTu/9h4qiOM4591ERuTKEcIb0JCMTkSuAGWa2v3Nujpk5MxsFEhFZ7ZybWyqVHq6pOrQaJuPOcxvNve6sjy2+l+Gdd8EkIAJ9LFHWYFMLokXvN9vzQWCLE2ILgPbA53bS7qrfi3WP7l+6GvxcJPRG4Kwn5LE1l/FVu63e4fM31CsV6pWKZVlm3vshVb0S2BBjTAG890PAAar6ahE5EjjPzOohhLO89xeaWQ6caWZnAS/vA3Wlqv5eRFqPAbEmIkeIyCtCCAeKSEdEPqaqfw/MVdXTReRCEXHAe4B6COEqEbk7y7LNIQRCCEWaprO894eWsmyTxuhDCG7NzH8s77X+jBcyML2AsBIxQ0xwKgTdCcl/h9kRwAJh/fds4blz5aAzVz+aA7vtE527fi7WXz87/wCwO+ZAVIgFuODZuOqmkfrrJirV+k5ZkkiWZWRZtqOZbYgxHglUnXOJiKwMIdyRZdnVIYTXmdl6EXm7qq52zn1BRD6gqv8CnAtc65z7sarua2avF5GjeQyJCH3R/IaZLXLOvSHGeB498f+AmV2oqqu89/8nxjgmIrPSNL0qhDAjhPBiM9sdiCGEpvd+JE3TOTHGDZVyOSpUNtVe/fMZI9cNMn32LliMmBnmIs4ghnshHtHrybU7Iq9/A3DRFgDNEL1l/U6uUhzRN8wbUD+vF8PAepecoHEandburbkvv7mSpZTLFSuXywnQCSFc1xezIefcc83sOOCQoig+18fgy2Z2gZl92cyON7MvAb9wzl2vqqfHGKfW2rmqLnTOPaiqK1XVJUkyD9iD3grlPX0wNwIfU9WXmNmXzGyVmf1cRN7rnDtdVS+MMTpVfTcwA/gVsFBExsyscM4dVy6XnRmxiGqTw6/cYcbqKw9nmo6CbEREURFIDPW7IGETyAwIL9PO+oZZ7516gLOP/budTZfuIXp3FT89Q9yvcaUhpNTzKLuy4TJlcmLZWPqi+2P9wIFqtUq5XDLv/d5mdqeqntLXQaNm9gBwrIisMbN5fZ10sHPuJ8C7gbu9919X1XeZ2dH0ph8Xq+p/A8tFZJr1RObFIvICM9vVzB4Efq6ql5rZXSLyfOBvRWRXVf2EiMxxzh3vnPt2jPEdgJjZcF+kZznnvuGcK6vqwar6ehG5XUSeZ6YbBcOMxIrx20v5is2UXA0NPYesRgfqIf4Bs5l0H/yDWHUZq45eec63/jDpALTg1c7dNouox9Nafh1KgKRnrhWPakKINZrtlzSnv7ZWq5UlTb1kWVYG1hZFsczMFpnZ61X1VBFJrfeKvm1mL+nruLkhhKPN7MNmdngI4Twzu8Y59wHgWjP7sIhcaGbvV9WXqGpZVVv9UlXVl6rqaX0996GpZ/v68jwze4GZfTiE8BpgjpmdZWYvCSF818wwszSE8E4zO9HMVqnqMhFZm2VZ4n0qWVay1ow31Gg0DydaBTMH3qHiEGeoy2kuv4YY3wy3zUL1WOjLq133ritxP3w+2HSgQTrwc8p7DeBqDl/ueVxi1o7Nimza5VNFqVqlkmWSpulBqvrrT3/603bTTTed6pwrPVZ3/TWRqnZf/epX3/ye97znZzHGwVKpNEtEXhVCuL3b7dLpdGzmio9XZKBb4PIKoQXSNEKrS3dJh2LytfRiFqMWTvmDe+m3X5WYne30+kUbnFkvCC38L0U+HdZ2KO9uSMxw0Wh3xyanvXHCe79TImLee8ys1O125998881HOec46aSTfrd06dIlCxYsWHDvvfcu2n///fdutVqNPM8LYNqvf/3rI733d5xwwgnjU4MaGRnZuHHjxk2zZ8/eYfr06dMvvfTS/VV12pve9KbrZWrSvhWJCHfeeWdz8eLFr5kxY8YtL3/5y1t9cOyBBx5YMn/+/N2yLMtGR0c3XXHFFUclSTI8MDBw1THHHON/+ctf2hVXXPGyer1+/ymnnHJkURSrsiwrpWlqeZ6Lc07Gh49bOty4ZJB6UcXFnh+gvcbQfAbGlcDrwaabdcfMznYJt66Yha2+A3hLr4tuBGQIbWe0V7Yp7xlJY5mQPjfUD16YJok453DOzVTVmycnJ1/kvXdFUXROOumkF8cYJ0XkkBNPPPEgM/secBpwu4hc8etf//rI4447bvyEE07YDNwAvA04cqt535L/+Z//WTk5OZmedNJJP6PnsgJIY4xVYHfgqGOPPbZ26qmn8sY3vjE/4ogjqsB/xhhf6r1/N4D3/lxVPfG6667b0O12hy+++OJ9yuXywAknnHD9qaeeuujHP/7x604++eQlIvJKVf2ZiMzy3m/IshJh2iE1Ri/dn8I2o/lGOg8NQLeKCuDWYr04l0tX38rNuoOjU+zpdHHSW2EAKiWE0PO2FTW6K0qE8fWmyXBWqdfTNLM0TUVV91fVDVtzhqp+xjl3HHC+mVXN7FQz+5CZHaSqHwVYt27dGjN7jZmdr6r7hBC+r6qnq+q5ZrYsy7LEOSchhAtCCF/ql/PN7BwzO8HM/tBut7/Sr2uVqh5iZuc7514FnA98N8Z4ppnNz/M8AmRZNg04X0SOPeWUU64WkTkrVqy4sq8bVwP7eO/FewdpdQjxMwgTG2g9tAMxr6BqGBGTdAtO4QFH7ua7qLoXrjHvEQBtGmopph6LghZCc023M/yCr5mZgk2tCBpm1th61aCqJ5vZ9WZ2DvAZoGlm58cY/11V/y/ATjvtNBe4S1VPB74FvBG4EDhTVY9xzomqmpn9j6r+SFV/FGP8jZnd1Tcmx1er1dMAhoaGZqvqN/ov4D5V/ZCqvkNVvxljvDTLMm9mOOcuNLNPm9mNr3zlK1+oqvHKK698oZmpmY2LyIRzzkTEvEinW33ORbTXdqEbUQUjwUiINn0LTtaYi9meiWg8ACkO6GPQQaRCtBwfCyIlEEOHO6jf1bmkSJIkAENmtjaE8KrR0dHvAh/pc+FewMMxxt+IyDkicq2q/peIvK//tlm+fPnyGOMxwIV9Sz1mZpc75xYCRbPZfDO9KcjuZlYDMLMNZrZSRH6rqqsnJiYAzldVAd7br2d1COHMNE33VtW/FxFijIv6n2c6537rnFNVPRxYd9ddd80xs/NDCMc659aratk5twFI1dUXEJM2lnchRlwUMI+TKpCjZEjYT0PsJBJ1fxwHA2ByD6KG8wENAWcZBYKUM4b2ayeJK8eolqZJbma5qh5YrVbX98FLb7jhhi8fcsghpyZJsjbG+Enn3Dn9acyXVRURef+ee+65B70A0b/EGDc5514A/G0I4c0A1Wr1rna7japuvadwd9VHtkEPDAxM9EV4tYicEULY0Xv/9yJyboyRGOMX77vvPpfn+SnOObrd7qdKpdJbzGxP4JMzZsw4ft26dTNijEc65w4ErnbOdfO8GEqSxJLpB25ktFzDaUSDoNERDEQdjvsxDgQ7VFRDglgZo95TZPogJlUsRpCIRkEAqQ672ryuiQQRSVV1DzObEJH/k2XZJ/uK21988cXv74vyHqr6MTMbT9O0VSqVjqHn9OTyyy/fddWqVfckSXJ4lmU2PDxMjPFmVV3TbDZHNm3a9Gag+pOf/OSHSZL4iYmJep7n3YmJCdm8eXPsdDph06ZNOwMvufLKK1/5+9///tA8z2tFUQB0RCTz3n8QwLmesynP83enaTrovf+AmV04f/7862666aY9RWSWiLwjxniqiOyRJH5pURQastkxteowRW6g1tsVZgZiRHsIOBBjEI2VBNXe3kUAtSaQoma4QsEJFoQ0rbq0hjmX9yTKhoHRWq32yRUrVnyn2Wx+qlqt0g9R4pyT/pywBAx1u48E+WOMu91yyy27sR1Kkt7y/PLLL3/L9u6ZIhGZ3e12Z2/93LZo2bJl//2c5zxnLvAl4L4sy+7tdDoHxxg/MzAw8KlOp7PUzIaT3to+NwYMyUpYXiBqRAOHEdWDTiJTLkHFoYVsUYxCCwgQlRiFEAQtDEmHVBLnvS9UNdCLV7SA3efOnfuqkZER1qxZQ6PR2OJ6+muiJEkOFZHXmdmFIvK5I488UkZGRkoDAwOvA3YVkWY/LlOYmSVpDciGevtrQi/Or7FvPGg/YnCDc72NnvRKdEqkidDEaKK2DmMN4hLvk2az2eyISINe7GIDgPd+N4CiKNi4cSMPP/wwo6Oj9EXqr4IWLVp0D/B3RVG8Kc/z7+y9995Lgc7atWt9/5YNwKoYY8PMmnnezXGuRKBFpAk0UBqYTqDoFrxMSTDskTCJ1VCGCQgmZg4vZoZnMoa8UqkMls0siTHuY2YPbauzMUYmJiaYmJggyzJqtRq1Wu0JReyZoKIoaLVaNJtNfvCDH5RGRkZeOmfOnN/ssMMOyxcsWPBGEVl37bXXPnDyySc/L8Y4R0T2EZGFIhLE2ySiE5jMwBCi9QL+Ig7byotvWALxkXi/yRCQRMU5jFhYCXGaxDgeuk2DctL39TXpBcCfkPI8J89zNm/eTJqmU55rSqXS0w5oCIH+epZOp/MoCdiwYcPJ3//+9x/3zNe//vWFJ598MmY2ADRU1RVF4V0+TmahEYxpBBVBAog6ByJWe4ThIglqG3CyCmwepkNmKIZEwUV1DlTF8gZx3GBGGmN0fZ3x+B34j9Bm59xD9BjdqequRVEMbz2oJElI05QkSciyjCRJcM6RJAkissWCAqgqU/NIVSWEQFEU9L3M5Hk+NbnfFq1yzo1OratjjPvTC8azadOmV/bvmWNmDeecxBgTjZMSi9CIQYcR5zykeFUiINQQAZEHzeLaxDTeLUYKzEPcAWLcY6ZmipppjMFI8tEGzeXW9TtnWZYCrFfVA+jtBngUOefuEhEdHh5ePnPmTN2wYYMbHx8HWNV/BmDL4J9BUu/9LWaWzJ49e/nAwIB7+OGHa51OR60XtdsdyNevX39zrVY72Dl3dwghiTGaH1+UabGpFQszEg3OgSgizjnE9u8bkdscdqczC/ejyeL+Mm6WYQ3MopmoBefMJOk21laTxuKKmbrY83aPmlkSQrgeoF6v/wxARO4WkearXvWqHdI0PcE5d2KpVDrhqKOOmiEiLefcfc8kYluT9/7mUqk0/qIXvWh/7/1JaZqeOG3atGOPPfbYrohMAmumTZt23ZIlSxaaWaqqm83MVFVKnQdrne66ajRJXHSiEcQkGjaO0lvOkSxB7QHnE1lCHFyLWc+3H2l5xKtapqYuRpNue6ySFSsXhKCxv05tAHS73dWPEZ3k2GOPnT4yMjL3C1/4AqVSic9+9rOMjIzsfNRRRw3S24X1bJysXGlmpec973kvnT59euUTn/gE8+bN4/TTT+fee+89/MADD1zhnFvfaDRmttvtrK8eGj3VECzLVy0IjfGKRiOqOtRSU0vFaE3hRJi2nsKWJ2xuL9fa7Nc7t7HXtLNGjOwgaoWpYEoSY3emaPeAPG//CkoVEcmdc4QQ5l166aVXNxqNI5MkuQ6we++99yWHHXYYt912G7vvvjs33XQTCxYs4NZbb91XRK53zt1mZjOfcPhPkURk0+zZsx9cuXLlwcPDw6xcuZKhoSHuuusuDj30UFqt1jELFy5cVhTFXO990teteVEUxJi3he4+MXYjgDnUjAg4orWm9nJo2GGmm2bLEnnrzRP6k8MPe8QS41EwkwTFFIsaRIr25qt8vmY8uHlV770lSbIaOOKFL3zh/y5evPhKgOc+97nV733vexx++OEsXbqUiYkJ5s2bx2GHHcbvfvc7jj322MV33nnnI6HUp2nSLfL4PVJHH3307BtuuIHjjz+eK664gvHxcV7xilcwOTnJ9ddfnx1wwAE3NZtNt+uuu74GeKgoCosxGq1VY3lr9CoN7CjO1KI4ExEUxZNN4SSUXiwvu+YT/bCbbRLldoSDMTnSO1seogU1cTEXb2YyvvaOscHB31dG0zdrjFG893cDx+yyyy4bP/OZz5yqqqgqt9xyCz/72c94xzveQQiBgw46iO9973ssWLCA973vfe+cum/Kom5tXacAfeznFEBbfzrnEJFHfe87erdY8F/96lf86Ec/4rTTTuOaa66h0WhwzTXXcPDBB/O2t73tnd1u99uqOlNVfwVYu92VOe3rapMjCyei2lwfxXDOnMTgRQSTl4OB8QczVkJvcyPnvGnuJDI+ihWvAKaJ2kIzqmoiqhBUpNMZr8/ace8j1snzljrnvHMud87tF2Ospmk6bGZOVVmwYAGXXHIJeZ6zdOlS1q9fzx133MEZZ5xBrVZ7FHjbA3Lr348t2+Pex3KhiDBnzhyuuOIKGo0Gq1atYs2aNaxfv54PV+O33AAAECZJREFUfehDJEkSut1uDZihqre3Wm2X5+3unPy3x2548Kpu6sV7QROPpIL3XkYxDu8teev/Kjrjl+dctnpF71W1uFnjzvVHnIV+rSBCRDDBDDWl3GmM/LrUWTbS6XQkxqiqehuwT7fb/Y+pAe68885cfPHFpGmKc45SqcQFF1zAnDlzHgfSE5W+W2pLeTLPPBbsww47jLPOOot2u02e5+y1115cdNFFZFlGv8/7qOqt3W5Xut3cyvmDGzutkSs0UlLFMHEoiIlhbJjCR8PcIWLj1p4o90kvPegaSe/bC2wOcLsZY50CaRdYNzfJI6gbWL3LIe+euyh9+4ZSqSKDg3UnIieq6n3lcnm3vsf4cQP+YwA8lgufSISfSGwfW7z32/s93ul0NojIfFX9z0ajRbPZtP35wZyVt/zbKomTcyspVkqFSoaWUkSEGcCBmKy2uN9id9LCl8NWu7NU9UKsdHEf5YMFNgjgpLcBFpCiPbkTYbKgvXp9nnes3W6rmd0I7Nduty/dlsg9FpjtgbctDvxjL2B79W2v7anS6XQuN7MFwPV5nlur1RLXfXi9FRPtojM5B3D9bXwighNYh3Fgb65culgtfmEKty0A+qHWFZrvNG/LPEddI3WGiLkE8JiKt/TB2340viD9/X7tdtva7bZ18/xhM5swszfGGK/bnu7a3oC2J7pP9P2JxPaJ9KeZEUL4dYzxFBEZy/N89cTEpO902rpP6boDlt98adNjPsE0wSQRIxEDle4ULhp3nu/Xt696HIDy6qVd1Asql/ZMdXy7F5ssiUURvDcRr1i3NT5DWxsmKvnitZONhms1m1oUxdVmVu92u0NmNj4lftsb2JPVgU9WBTwWzG2B2O/ThjzPZ5tZmuf5NZONhmu22jpkS9fE9sho0R4fRhDvRRMxSxLDY2OIvq0nmXIJKm05bWn3cQACOHFna5hzX1+MM8ytS5yId+AEBMErsui671b3Gbjn2G6n02w0GrRarY6ZXQUc0Ol0rrZetOsJOWFbYG5Ld/4xffpk2zGzTp7nd5rZc4HfdDqdvNloWrc12dqrfs+rF1373YokvU2ECeC9+FTEsGQjPbcfxLlLXJJ/+lGYPcr0n3LPerS8D/DbHhfaWxOxsVKCeYdkHhMvRAvDS2/58Y0H1X9fGZ9o0Gw26Xa7m+htAH99nuc/2NoIbP35ZET6sRZ4ayCfTB3barvb7f48xvhK4A+NRmt0YmLSJpsNe/70m+tLbrrsRrUwLe0fB08F6Z2rtzGI7+jpPq6MoTRfTlo6sl0AATq+/U+az76h/1AVlZg6o5xg3uNSj6WefHz9kj1orrCdSsvWTUxMyOjoZtrt9lLgTjN7ewjhB8CfxYl/zpTlCUpeFMUlZnYicHur1Vo+OTnpGo0GOyVLVkvzIR1bt3yPzBNTJ5Y5LHGQelPE5T1JBHTObb7onvFYvB4HYO3kVWuIpRSTb/aXLSd6WJQ6c5nHSg4p9xqS+67+9tCe9QcPk+66NZOTEzI6Okqz1VpsZjer6luLovjZ1jrxyXLlE80Dnwy3TX0C62KMv1PVk4GbGo32srGxMRkd22x0143sOfTQYXf+5t/qpQQyJ5qmWOKQLDHxxiLM3tTXfV/TIknlnSselxXpcQACuEp+Tsx3HERp9Pz/7kWZp1tySEkg8bjUiROscsvln1v7kl2Wvrzb2LhhbGxSxsfGtNlsPqSqV5jZ60IIK1X1uq3r/1PB/BNBm6Lr8zzfqKqvUNX/nZxsPjw2ttmNj08Smps2vnju4iNv+cnn1qVi1VRESg5KhpQTXObIUXdEP/bRiPnsHZzaJ7aJ1bYuykkPt73xbbR+Zu8N2P6ibqycQrmMlZ1ZKYVyIi6VOHzzjz9/99ELlh8dWhvXjI6OsmnTJhsbG5sIIfwXsIOqHhRj/Da9I1mPom0BsD0An+iZrWg8hPDdEMLzgel5nv98fHx8cnRs1MbHN2unuWHkmL0ffMXNl3/+LtEwVErFlRJcmpiVykgpRcXcJrB9eqJbP9OL/5q8c8U2T7E/4WnN8J2df+iTtQKc3If7KyGyf7ONNbqUOoXQbFtoBaJKZc3hJ33igF89sOPVOcNzhoYGQpqWy/V6RUul8qCZHqGqS4CFMcZTVHvO2a0t67asLvC41cTUiuIxn+qc+w/ghc65nUTkd3mej7dardhqtbLJZlMzHXv41c/Z8MqbL/nMIrHG9EqCr1TEVVOjVibUM0g89wFTx14viWHHkLxz9du3h5Hf3h8A57x++i9jrLzV+ZZgzACe6+B3HqYhaIw4J+JMkbwIQyvuvHryZS9//i6tXB9YuSFMC0Xs5W2KoeVElgCY2dFmttjMfk7v8M1g//qWds1sm56XKRAfc20N8J/0gvgvBO4piuKOZrMZx8YmmZycYPPmUXYb3LT+pXtu+ptrf/iJEbHujEpKUstEaplRKxNrKaTCCoR3AB6RJTGfPekle/s5Px3bbuzhjx64bn9tx92yrPigS8b+AcgQNqP8ohuZ28zxrQ7SynHNDtIulE5wxaEn/PP6WJnDT24faJfSSlKtlsvV6oCVKxmlXgCpDOypqrur6m/NbF2Mcb6qvlh7Z+keJbZbr3+dc8E5d4OILPXe7ygiR3rvV5jZgzHGVp7ntNtt2p3cmq2m73ZbjeOfN1nz3TXx1h+fv2PJaVYtOStnSK2C1lJirUxeSlmPcQwwo7e9b+gifPlf5e1rthm+fdIAAoRvzXytSHcv51rn959aCdzcDcxsdPCtHNfuIO0ca+UaWwEGZu+16pDj3vc3Ny1Nfr1oXW1WuVz2pSyxUqki5XK2dSQuU9VhM9vFzKap6gozW2NmDVUt+qcvMxGpi8iOwK5JkkyIyEOqOm5mRVC1kOd0Ojndbod2u0On0427zypWHLlv53X3XfWD69ctv3V+NcPXMmeVBKplQqWEq5eQUsZalBfSOw2PhuqHTct3J+8e+dUfw+bJZ+345vR3kbTrkE8dR1iF8LsisGujQ2xFrNUmtrvUmwXdboHP1RUvPOmfNyYDO02//BbuGu9k8yqlMlk5w7uEUlYy55A0TcQliTkRw0xxHouxd2Cot3PT+kcbxEzEQIIG0SISo1qet6UoAt08aLvb1uGyrj/hMD0gTK7ZeP2PPj+zlGhazoiVBK1lZJUSRb2CVlMk9ayldzJ+DwBi9gG01pV3b3xS2Yz+pLwx8RvDn3RZdwzrgyhsxvhJMPZq52izLVmzwFq5SbdLaEeNndyZlOutw97wwU5anz39ZzeHO9dPMCvxaZp4j08z0iQlSz1Kz/XhxHdxRFR7ESvnPIqPUTPV6LwX63aDKLEXEy6ChVDkc4Z15G8Pyw4qJtZvvOm/v1ixTqNaTpRSySXVFF/NROslpJwY1RKWeBZjnIAw1BtP9gGKclXevfmzTxaTPzlzUfzawBnOhwSXn9dPDpZj9i1DDmp2oR0Iza5qJzhrFfhuR5NOQdENTpPKwNiBx5w6MXOXfY9YtiZcef097e7IhE2XLMlSvDjn8IngncfMgllvQ7JzIoqkGsRUC4kWpCiCodadPuw2vXi/cmXBTslRIyvvvfauX/37YNGdnFbNVDJHUi67WMmIlVSpJs5XMqRWwouzuzB5J5BhqMbkDGflivzD+JMG788CECB8tfZO8cVOzsd/4pF8pz9CGOjm1DoB1+yStgq1dk6RF851g/puTtE10iLSGNpx95EDjjrFTZu928taOXcvebizfMWabvfhTV3f6UI3qqC9o6WC0ywVyiXYeUYp7rZTWpq/c3WPWsYBm9c9ePU9v7lEx9Y/uEOaUMs8oZSQVTJXZF6tZ22dK2eESkYsJf2NU0I/LwKjhPSLEb8i+YfmD/5ULP7s7G321cphEXuv98XeiL2gf3kxwu8N269VENtdF9sFaadQ6/aSDaXdSMgLfKFoEUhiJCcpjw7OnDu+497P1xk77pqV6tNr5drAoE/THUSchLy7odOcnOg2Rpub1q3M1y26zU1sXD1E6ExPElLv0MzjspRQ8iSlhKKSkpRTJ6WUolZSygmZiNyH8VKmMs2Z3BhDtlyRf83e17r1z8HhKaW/m/jywIy65N+XJP4B9JGljsiPMTSiO3cCRTt32im01A3keeF8oap57nxhWsRA0lEwJQQlMcNCz502ldqk109BEwERJHEEcSQlB6knJN6lmdOYlpzLnMZKQpZlrltJ1ZVLpB63CiPF7PgtfTT3KYv+b0TKb5F/HN/852Lw1BMwXobX9dmZmJrL9K3Agv5fOSr/jmdGVJ2bF3Q76nxRqHZy52LUmCsuj06jqu8qgjmiEtTUmEqF0vumgDlx4h2Jd2qJgHcuJk592ROT1PlSopomzpW9xiwl896tQumAvZlH0gcs1sJd4oTAxnCenP3Udko8bTlU7SvMN/NfFW9XAJ9iKmVJL/vkDzEM0V0Ldb5bqObRuagaikBWmLMQCKAuGAFDowKO3gpASbwDBJcICeI08SSJU8kceZKSpGCl1EnqNWJuBYLD7C1bsmBCC+MTMcqrfIjvlQ+y/OkY99ObhNaQ+K8ch+OdPnG3KXwco7xVY/eqyY3iqRk6xyJZUFeEqC4oFOYExaKh9JzaPSMiGOLECw6HpKKWeMyLI/XqxVMIbq1Fmk7scIP9txphxymfiUGfD3zL/3/84ulMifzMpEH+Bmls8yaMU8y535rnNOnP8rdqeA3I1eKsbSolB4NRdFgMb0Y0wcR64mXSSzggggeCw40rTIizrqlUwF5msNOj+gArPVykhR6t8P27q1x2yHt42vcdP6OZzO1sXBjmKODDqvxeEjfN4ANP0JlRRG43GBdljN5OWDCrmWNYYAizgw2mP0EdXzJ0s1NeivDZZDNXP1U990T0rOXSty8wsyuchXMrTLjgmWhDjDNQ3a1kfEr+iY3PRBuPa/PZaGSKDKR9AZc47x6KulUuwqelbrnIqe5c+SdOFJ4+HffHaJse6WeKBKwyyVtDoQca/Eb7qbSfarHItVbovpUB3vxsggfPMoAAcjahW+KNVlhDlZVPGcDIWjFbF5U3yTNgJP7oeJ7tBqdo9F84wMOpivwjsmWS+6dSMLUvpsJ3Bz7CdpMkPpP0FwMQYPyznByUHXFy4Z9VgdrpzjE+7aN8+2nu2pOmvyiAAJvO44tmboNh5/1pT8qnPTpj+se3nRjx2aK/OIBmyKbP8FMzN6bY257MM+LkMjGtzQy89pmc4z0ZetaNyGNJBGtv4k2gczVy+5MwHHcRdVoROOkvDR78FQAIMO+LtIm8zYndr8bodqcrRkuwG73jXTudTeuP1/zM019chLemtWdzpCkvir2EZI8jBx9xCTfNOYvfbev/vwT9VXDgFM05m2sMgiinP477lNMd6F8TePBXxoHQW+6tOYsfFsrDpnyof/GiJGHmzp/mrc/2SuOP0bN7CvpJkICZ4+2rjF8G5RcmDDrHvjt7Xv3XBh78lYnwFMnZhOg5SRxF6hmhyUlyNs/o2dj/X9LKT7D/yo+y31+6H09E/w/wHJVcjfUH5AAAAABJRU5ErkJggg==\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHiczZ15vF1Fle9/a1Xtvc98780cEjJCAgkgiUwiIghCgqLIQyK2qI0D2g6PlmfbbaONCmo/habB4dF+tBX0KdC2PFGZB4GEgECYIQmZp5vc+Yx7qFrr/XHODSEkiDKuz6c+Z9+z9zlV9T2ratWwal3C6yh64YW8Y8Ex4431M4yhOQAtVMUBTOgRQRGEWvtBlJnRgGJABSthdIWHrIyI1l9y3339F154obxedaDXOsPGr2+anFL2TgXOJKZWEIYP5oslL0z7EtE8Zj4M0O49f5qGofKAF32GRTe1GnWTZOkR5DUi0muC0Nxaete7el/L+rwmALdde+34nOcPgei0ILK/j4rlLmbztyBMfkUyUGwT8f+ZNGojWeLeBchvjOR+Xvngqf2vyPe/iLxqABWg/p/+YqFR+WYQREsLXeUuMH8WQPhq5dmRVMRfUR8eqquTt7Din7rOOXsFAfpqZPaqABy88spjlPhfi8XytSbKfZxAB+3l0ZSZ7hXD6wkWCAggClURGWJSggUAUjivokRIoJpq6gkkyl5miOJYqNq9VO6xer3+UxfHp4P9l8Z+8pPLXum6vqIAhy+/fLYXXFksdd1go9wXQZjyggyJH1HDDyEIQ1iawMZAjAWTsWS55QHHbEREGQwPABAYZhLxzjDBIpOcQBx7BwhUvOtDkiXk/WGqcugeirbJxc1LGvXaqY5x7sTPf37NK1XnVwTgg1deGcwcHvkWOKpXuiqLiPjI3XIZJBv91lub59CORWBVAysmDKyQ8QgsQGSNsakaUhAYIAPqlE+hgHpRVeMB730IFcfOK8RbSVPHTghZCsn8IJI0hk/fA8WYXYuhovfVa8O3eudy69eWLzjsP87NXm7dXzbA6oXfnJNBflwaM+4uJrpgt6/fTlFwC+dyY7w1Fvk8KAwYHChHRsQGAVvrEBgSsGE2DtYATAwFE4MBQAUCgkBU4D3EewtRD3WK1FmkzsE7gstI00zQaoEynyFNNkucLCZg+q6lUpVLRoYGj1KWv53wla+sfjn1f1kA+750wblBYGcXunpOJcUBu3zrsIbBryhfnGaiyEghIoSRahiAcgEjyCkCqxwEFsY4BCHBEAnIIzAEsGEGRDVgYgXUiQAMcfAeEDXwqshSFe8t0syxcyRZTIgz0TSDacYkaapoNb2kySaK07MAVEaLqIRnGkNDv3ferR7/rxdd+ZoC1Asv5L6R+k9yudJTuXz+YgA7O3EOwquQjyoo5POI8oRCBC7m1YcRKIgIUUgcRSqhVUSRARtvrGEPkiRuVBsDw83B3m3Ot2KqDQ8TAJR7utXkcjpm4qSgOK4nn88XK1BlOC8iziBOPWeOkCTkk0SROqU0Jm00QEkKtFqqzVZLG406vHxol6q4pNn6XBw3jh23Zf3ZdN11/lUHuPpzn4t6Mr2h3DX2T8T85eeoYjuKuT9QobwPFfPQYp4oX4Tmc0AxD+RyanIBxIZk8hE8sx3cuLnv4ZtubD774MOz01bzQI4KjwTdXRt57NhhDYO00FXOAKA5UgsozUIZGOjOhkemSdI8NMwXnt7vsIVrDl20uDhu2tRxRtT5OCZOUvFJqhTHhFYMbbSIkpai3oSvN0Ct1lY0mqeAMHFn0UUuqo0MHDkU0Kn7X3FF8qoBXL34c1HXxNYNlZ5xawj0qZ03DN3I5UqMYqFgSiX4UhEo5IkKBUUhDxTy4FweEobF4R19g7f+6D8a29etPz6cPPGWniOOHM5Nn1qKyuUKk+UgCMSwigoLMwQARNr9ocucEUC9TzWu1WqNDRvrw8sf6HHbd7xz0uxZd5z0iY+VK+Mm9HCa1aXZJG01iZJYfbVJHDfV1BvwtQbQbDSlVsvB6+JdQHxvZLDvwDq5d8/86U/jVxygfu5zUV//4I1dXeN7QXTWzvdN+GNTKU5DpSJUKYKLRaBShi8UCaWCUr4Ijuy4NY8/9syNP/rPg6mYf3bSu9+zpTJj3/FhaG0YRWRsiCC0yFkLgAWgzFgSMkYBQL0n74iZJRDxnDiHLM3Ue4eklVCaZVlt3fr+3j/8YYrWG7NOOfeTT86cP+8AybIdaLRI63Uy9ZZKowodqZOv10G1WiLVxiD77CO7VPPqkaG+aSMjAyfvf+ONL0kTXxLAa9//fvN2p7+rlMdsYDbn7oQXhj+kSnkmd5cJlS5QpSwoly2KBUGpRFTIj92+YcP663/4g/2CSZMfnnrG6T6sdHUXopByuYKGoaEwDBGEIaIwJGOMhGHovKfEGPVBEGUAkGVJoEqGCGGaJoH3npPUiXcpxXGKJEmRpC3EcapxbWR463X/bZLebQvf95nPrpm4777TpNUYQD1WrtWdr9dCDI8IqjVItcpUra3WNPvMzjqpfH9kZOCACQGd/FL6xD8LUAHaccq7flEuj9/ARP+480YuvIzK3fO4uyzo6iLT1QXpKkMrZZhSyWTQiddd/r1n62lMsz/+sb6ouzKmmC+gWCxImMtTFAQmn89REEQuzIXKABtjhIgUAAPIOgkAAnQMlarCe8+i6tMkYeecbSapxs2WZGmszWaCRquOWt/g0Iaf/WxiJcxl7//8Z+ayoldrdaFaXTEyDD9cI1NtiBseUNtobHDN+FPP1Vkuqg4Pzph40w1nv2yAW4874dxKqZyzJnfZ6Hucz31fyuX9TM9YoKdC6KkIlbuYuyuiheLEbZu3rL3h5z87aNJp77lp7PyDpuZLZZTyEQqFvBaLRQRRzufzeVimoANsAMB2AFsAbAbQByDZDWAEYAKAKQD2ATAZwBhVhXMuSZ3nZrNhm/U6teIUraSl1ZGGDj3xWO/263+76D0f/chjEydPmaWN+nYdqTKGa0B1WGRomDA8QjpYfRZZ/HejdXRZfF6tWfeT77rte381wIHD3zpfQ/s3xfKYL0GVAQA2+iF3lWfpuDFK3V2Erh6Ysd2qlRJToTD3jzffcseza9dMm/s/P7clX6wUKpU8R/mCVkpFLRSKEgTWWmstgEEATwNYC6Cxh3IJgKM6fy9HWyu1c4861wUAswDMA9DtvXdpmvp6vW6arVQazRparRZqQ0Mjq//9+/vvt/9+a4898Z3v0Li1CsNV9cNVz4ODVoeqIsNDHsNDG5G5tiaqukZ96JLM61WT77/nqb8Y4JPz54djw8LN3eVxUxS6PwCA7a+op9JF3V3MEyYQuiqCMV2M7m4gCg/6f7/573uauUJj9t+eXSiXyzaXK3JXuaiFQh6FQsExcxnAJgDPoK1xhHbTpA6g7g6UgwAcDmB2pzhrANwP4KkO7CoA34HoOq9jOp/b13ufJEmi9XpDq9Um4rhOtVrVr/np1Y1CKy68532nH0NZ+gQGhtQPjxCGhlX7Bw2GB70OD4/A65JOvqtGagPbJrK8kx56aI/TPrM3gF8ZO/HfugpdG9W5U+EcyPmnqFioarEYULkCKhWEymVGpUxgc9A11123ws7cb+P+HzprXHdPt+0qV1y5VOCuroqGYRgZYzIADwJ4Fu2mWQHQIyIREUUAciJCRJSo6qeI6NsA7gZwO4ClqvppIrqpU7xIRPJElAfQg/YSWQJgG4AhZh5rrc1HUZgGATMRGRuGKM87yA5v2Tz02N33NOfPPeBQUfSyS1nTDEidauqIUie+Uffk3AQ4NzYy9prBZv307/bt+N1LBrh1zpwDQoTTAzIXtKdO4jSMbuJysULlippKCShXiIolImsPuf6m3y+LDpy7ZeYZp0+qlMvc091FpVJJy+UyBUFQYeanADwCIBWRkIjGiMg+qjqWmaep6nxVPY2IjlLVJ4joMACnAzihkxYR0TCAR1T1M6r6FiIqEVFFRHKqWlFVUlUhohjAWiLKjDEzoyhyxhhlMrCWbGn//UrVoaG1Ty9dGs+fvf+bSWgL0tSqy0BxBmSppSx7FnE8H84zvBzjnLvhf47r2XZpf//AnwWoAFXHTfivclSeR0RTAYA4uJIqpamolJkrJUi5ApTLxLlw9u0P3HdPOnnS0KwzzpjQVSlTsVhAuVxGoVAoMHMOwOMiMkBEkYgAABMRqSqJSAqgn5mfVtVFALYC+Bu0jchVAG4AcAeAJ1R1tqq+T1WfJaJ9mPknABqq6gDEABLvPYjIEFFJRFJVbTDz5CAIDDO1mNmoshRnzOgeWr9+oPfZZ9fOnDr1MFHpJ+cUzqv6lJH5IpL0VqgcAQChCcstFy+6ZKD/Z1/7cwA/PWfeCcWwMMhsP94mqiu4XAJVykzlElG5i6irCCrku9f2bnnk2aGB8rxzz43KlQrKpSKVyxUtFAqWiLyIPKWqCYBJqlpBu5/qEZGJzDybiN6jqkd77+8hoveq6nYi+g4RBar6QQDvBPAOAAuY+W4APyKiBao6A8AtqvpFAAtFBERUZOYKgPGqWlTVHiIa6qRyEAR5a61T9T4IIi4fOCdcd+/SfJGwprvctQ9EEnZi1XmFc6Qu60Ka1gGaDMI+AfFlQ2PG5i7p37F+V168u/YJ9KvWRl/a+UCY+xNyUai5HFMxUuSNIoyQthK6f+XKhQed9/fVUqlIpWKBS6WSz+dzQfurcC+AQe99WVXzqtqtqhNVNSKizap6u4iUVLVFRBc5574IYI6IXOK9f5+qiog8IyLPqKp4798nIpeKyH6qer6IfNN73xCRCjPfCmCt954BdAHoIiIjIkUR6RORZUSURVFki8VykM9HWiyW+KC//3zz/pXPLEySRCm0RiIryAVKuZyafJ45Ch4Y5WBN9EURf7HuZnifp4F/N3f+WwthfoTJvL/9Dt2ihahiKmVoscDIFxWFAlEQzvvtI38anvPpTy/vmjC+p1AqoburolEUBUQ01Tl3D9rW1DNzQ1VrzJyo6j4AFgFYSERLVfXdAC4HsICI3gvgSWPM94joZhFZq6pxpznfraq/Nsb8UUT2AfBhZh4G8FMAxxPRLar6BQBv8t73qeomADuYuQmARKSMdvew0FrTMIYVRAAQFvaf89T9N/x26gGTpkwn7/uQesCnUPHkY99NLlkNYH8AFct6SX+lB5cO9m/eCXZXgF79N4wJp47uvpgo2CxhYYoEgSIMlYKANLB2Ze+W28v7z0m7pk6elM/nUCzkuT20w1Tv/UYAbxWRhqqOrnhscs6tIqJbARyvqomq/j0R3QbgAiK6Q1VvJqKzvPcXqT5//2f0b+89ADxijPmi9/5dAL5MRLc4584H4IkoJKKbiWiMqh4gIlNUNWDmfhExxpgtzDwlDMMtBRHyzvvuqVPGl/af9ejqbVvs3DFjp0kQOAojFROQKQQqLtymSdrmwflPESenAjjxBQDXzZ8/iV1wO4CLAEAJd4kNpyCygAmI2KgGBkjdlCd3bJ+54NOfWFbI5xFFEedzOW+M6QFwFxFtUdXAew9jTMV7fygzv5uZ6977bxNRwXt/gTHmUlV1InIBM38VwDs6oG4RkbuJqLkbxAIRHUdEJzrnDgUQM/OXReQTAKZ0NPBSImIAnwJQAnAnET3mnBs0xkBVM2aeYK09wlo7kM/nDaA44MNnRw9dePFb9ytVMmbaAGtgQqPehIAJ9oFmfwTp20E4gNn+cu3MgybOWvfE9ucBDBJ5X7EY7dynZWOfhjEzjbWkAUOJiRRmxY5t901bfHItly9MyefzUiwW1Vo7SVV3OOcWoz2wtcaYDQBWiMidRHSqqm4jog+LyBZm/i4RnSci3yaii1T1Hmb+tff+QACnEdFJ2E2ICKq6XVWvVNWVzPw/vPff7IA8T1UvFZFNxpgPi8gIgAnOuduDIOgGcKyq7uu9Z+993Vq7PYqifYhou4jXLPOFKSce9/tH7vtTeUHPuGlK7MFWOTAiQQAT8FPe+bcDQCksjvVu5HQAPwQ6RkQB8mtXTwf47e3GQn1gM1WtgbckwqywRN6l4zYn8dsnvfWt46PQahAEFIYhq2rivV8G4Ceq+v9UdbWInOyc+4S1FqoKVf2Bqh6qqr8RkW3e+8tU9XYi+rKqHui9vwzAuWhb6UcA/MY5d7lz7nIAv0F7HNkD4FwiulRV91fVL3vv7/TeXyYiW4jotyJyqKr+H1UFM5Nz7hNEtNg5t0lE7iKiX6vqnUQUW2tNEIQU5iLd9x3vGLclTY7zWdoDqLbrzGBr4GH3VcWAAgDTcbJ29b6jP6xBW99nGJOfEYy0ijSmO1TCLRREPchFZIKANQyFQqPPpunq8C1HPjl+3oHlKMpRsVgQZj5AVR/z3p/V0egBVV0JYDEzbxORfQGMVdXDAPyaiD6JtrH4oYh8TFVPQnscdzkz/5f3fnVnnDiLiBYR0WGqKqq6XkRuNcb8ynv/eGew/W4imi4iXyGiyQBOZ+afiMhH2nqBsdQ2FhONMVeKSAzgMBF5LxGtYOZDiagPquS8mrReWxFv3jw4hrlIaQpJM1LvGN4ZdemDrDRG1qx9VLys+TvJNl8OjFgASGBO6Db58ar+fbxu3dUye78WDBMRVNgAAguHYJ1Lj15w8onLC4UchWGo1tqIiLap6urO2OwMVd3CzFd0xmY/VtVvA/gCgEtFZDERfUlVv+WcewuAW4wxfxCRt6vqlzpGAp0BN9BeUACAQzoJncEyVPW/jTF3O+feTUTfVFUB8CXn3BeIaKL3/nxmvkRV/5GIvq2qARF9QlWnA1gqImuMMbOCIAicc1k+r5h16ruKDz348JGzDe0gIAUzQExEVmBMitVr7obXsyObW5e6+O2Av5oAYC3s78YVxhypinEA1blS/i3PmlnWUtFSPgcuFCjOBc3l3RXz5i9/MQnDkHO5HIVheLCI3P6Nb3xDly9ffg4zR7v3XW8kEZHklFNOuf9jH/vY74IgKFhrJwE40Tn3aJIk2opjevDib+WPGm6lhTTNS6OpiJvQetPrug01P1x9L0ELRNgx0By4byb8aVYBXg+qimIcACj0FsTJOOzojbk40wMSiM90HezwtFNOrhtjJodhKNZaUtVCkiSz7r///hOZmc4888w/rl69etWcOXPmPP300ysPPPDAuc1ms56maQag55ZbbjnOWrvitNNOG2Fuj+H7+vr6+/v7ByZOnDh+zJgxY6655pr5ItKzZMmSezvN73lCRHj00Ucbq1ateteYMWP+dMIJJzQ6cPSZZ55Zvd9++80IwzAcHBwcuPnmm0+w1naXy+XbFy1aZP7whz/ozTfffHylUnnmrLPOeluWZZuCIMgZY5SIyDBj2knvfHbDdb8pz3U+r+otvCayfYeXZnOcQm8BcJoqJgCcKDzbTcCknA0fBnAWAKjyDiWUtZmEfv2mxM6a2VAbdG8L7SGHHzTv0SAIgPZofJyqPtJsNt9sjDFZlsVnnnnmMara6PR3C4noJ6p6HoCHmfmmW2+99bh3v/vdI2ecccYQgGUAzgZw3C7jvjU33HDDxlqtFpx55pk3ABhdUg+89wUAMwGcsHjx4uI555yDM844Izn++OMLAK7z3h9rjPlkB/JFInLm0qVL++I47r7iiisOyOVy5dNPP33pOeecs/K66657z5IlS54mopNV9XcAxllr+7xX2ufNC4sPXH/DvLnS6JPEx1i/UaXZKrR/S9422qtYGz603mUT2AP7Rhxyu89VcOADYnWqHuLSfLpxY5dWa9vZmO6wUCiTseC2+swTkY2jlSciiMjFqvouEbkEQE5VP6mq56vqId77LwPAtm3btqnqqar6HRGZB+BqEfmCiFykqqvCMDRERM65S5xzl3XSd1T1a6p6uqo+2Gw2vwcAvb29G0XkMFX9DjOfDOA7AH6mqhcQ0aw0TT0AhGHYA+A7RLT4Ax/4wB1ENHnDhg23qSpEZDOAedZaMobI5HIltcF4DNd28IZ1OfGuCCiUycP4YJRTyCEE2Jc9zFwY3rf9NqDe9DjhQAUW3oNcipH+HcmEgw/5PhEnDIWIKIC6qtZ362POArCUiP5FRL6lqjVVvURVfy4iPwKAKVOmTAbwsHPuCwB+5Jx7P4BLAVwgIouZmbQtN4jIr0TkV97721T1MREpiMj7SqXS5wGgp6dnkohc2fkBnhKRL3Ys8JUicl0QBKYzhLpMVb8BYOlJJ530NhHxt9xyyzGde0NEVG3rAKVgbk6cP/97IwPb1WWO4T2p91aFAxUaM8rJGp4CmDkWoDcTuON+RqmSlgyJI/Uq3nhKM9SisKX5cJYxrNbamjFmnKpuc869s6+v72cA/rGjhXMAbPbe30ZE/wLgHhG5jog+O6qp69atWy8i7ySiSzuWelhVrxeRx4wxSaPR+EC7K9GZqlrsXO9Q1Q1EdIeIbKnVagDwHVUlAJ9WVRDRJufcBcaYA7335xIRvPerOv3ol9FeFgPaq9a9jz766CTv/XcALGLm7QBCIhokIOJibkY1CJs5X/fshYyqcyQGRBEpUkBDEM0jaGYB7KekC6EEgj7JAOC9aHvnQSAW/cVSuO/cuYn3PmLmnDEm7UzDFlQqlb4OvGDZsmXfO+yww86x1m7z3v8LM39NVd/mvf9+54f77OzZs2eoatF7/x1rba+IHAngFAAf8N6jWCw+2mq1SER29SmcucvQBsVisQ4AW7du3UxE56vqJBE5tzOrEefcZStXruQ0TT/IzGi1Wl/P5/N/Q0SzmfkrY8eOPaO3t3ccgGNEZIGq3sXMKRH1QFXGzp2bbSyWypPTXqgXdd6BvRdVZEr6FBSHMnCYgBMLIA+lCgAo6RZRhFAFeQ8IQSwQR2FPYZ9JW4MgSK21LCJzRaRJRB81xnwNAIwx5oorrvhspynPEpF/BlC11jaiKDoZnd73+uuvn7Fp06bHrLVHhGGo3d3dEJGHsizbGsfxjoGBgTMBFK6//vqrjTFBtVottVqtuNFooL+/X7Msc0NDQ1MBvO3WW289aenSpUfEcVzy3ouIxEQUGmPOA4BRS++c+6SIVIwx53nvL91vv/3uXb58+SwAE4jooyJyjqrOtNauIiJXnLoPJ8Woy2cZwYuyqIoqoEgFWMvAoarUDaBgAfCo96sqWiA15L1CmMApICDJh/l8qcTGGFFVj/aUaqRYLP7L2rVrf9xoNL5eLBZ3aggzEzOHaO9VVJLkuU1+7/2MBx54YAb2Isa0V9h+85vf/Pk9WaKJrVZr4iisUWC7y9q1a389b968aQAuA/CMtfapOI4XeO8vLpfLX4vjeA3aa4gCQE0QqURRDmlKgEK8AAqCihK0iueGV8yA2tGOkYAUIIEoqfckzpPLHBAEFY4iEhFnjPGqOhbt3bGZ++6778l9fX3YsmUL6vX6zqWnN5JYa49g5lNV9d9E5FvHHnss+vr6cpVK5X0AphNRA8BY770AEJsLSYytqMtUUwd4bbMDRBWNUV6AWn6e87WSV9KmGtRhUGdwPYCpE1srzmVE1ErTNFHVLd77AQAwxswEgCzL0N/fj82bN2NwcBBZ9rKdP18xWbVq1ZPe+49nWXam9/4/DzzwwGcBpNu2bTMA0FmE3UpELWNM3RiTsA1CgOpgqpNBnRk1z9QU2J28aFT7dr5BmldFyQIqIPKq5OFh4evqfaiq+c48dC6AjXsqrPce1WoV1WoVYRiiVCqhUCigs+D6mkmWZWg2m2g0Grjqqqui7du3Hzt58uTbJk6cuHb27NnvJ6Kt995779NLlix5E4CJqjqHmR8CYLMkSQP4qgrKBJAwhKBgJVZyFjs9jwEreE4FhajIopRAGSAGkRG1CJwfcnEqlMtZIhIiqhNRcc9Ff07SNMXg4CAGBwcRBAHy7QVYRFH0igN1ziFJEsRxjDiOn9cCduzYcdbVV1/9gs/84Ac/WLFkyRKoahlA3XtPzjmbtVrMiWs66BgQiIWcQpVZQwJ17eQFwBLQC9UNIEyHoqJGYwhIIO21QvbepGktHhpyQXeFOyvNDeCFHvi7yBAzb+zkwSIyLcuynl0rZa1FEASw1iIMQ1hrwcyw1oKInmcQRGR0TREiAuccsiyDcw7OOaRpOrrcvyfZxMyD1PbBgfd+Ptq+NhgYGDgJAIhoHwB1dFQrHhlxuSyteUg3g4xv93VKYFJomaCA0hoCtliBPi4EQ8B0IZ1LgscZUKfwgIoQo7S9v9nYtDGMpkwxYWhtZ2B7SCfT5wkRPU5ErqenZ924ceNkx44dPDIyAgCbReTg0edGK/8qihhj/qSqZuLEiWvL5TJv3ry5GMexV9UxqjoTQNrb27u8VCq9mYgeFxEbxzE11m8Mcn39Dc8QFaiFAkSkKoZID1YlgOQhQB9lBT2tKqsAgBTjhaTmSb0C6kDGiQa8dWuhsWZ9TtVb55wBMKiqxjl3LwCUSqXfjsJj5vqiRYsmBEFwOjOfEUXR6SeeeOJYImow85OvJrFdxRhzfxRFQ0cfffR8a+2ZQRCc0dPTs3jx4sVpZ+q2paen596VK1c+pqoBgMHMeyYiaqxdVwy2bCtnQoEA5Nu+TJ6gI6o0BgC86rMCeoYZfnXm0+0Ytc3CDXiygASqYK9MNFwrNNavnZ2mKURERaQOAEmSbN1t2GIXL148pq+vb8p3v/tdiAi+/e1vo6+vb+qJJ57YjfbK81/syP1XyAZVjRYsWHDc2LFj81//+tcxffp0nHfeeXjyySffsmDBgg3MvKNer49LkiTqdA11FYFzHsn6TbN4qJYnBYmCFQjEU6BKzVFO4tMdgF9rCVib+Oy00OQAAMTUVFEVpUyhUKhFko5F4g5Mk+QOIgqIKGVmOOemX3vttXfW6/XjrLX3EpE89dRTxy5cuBDLly/HvHnz8MADD2D69Ol45JFHDmDmewE8rKrjXk16RDQwadKktRs2bFgYRRFWrVqFSqWCe+65B4cffjhardaihx9+eG2WZVOMMRYARCTN0hQuzRLN0nk+i58ASAOwCOANgcHaVGlb4KbPxjtgjd0fqK4Uf/ROWyxqASYlsVBWBbwCCPv778x6dwzQPvtMMsZoEASbVfVtRx111E3PPPPMzQBwyCGHFK666iocwXXn3QAAEZlJREFUe+yxePbZZ1GtVjF16lQcc8wxWLp0KRYtWrT60Ucf3Wl+X6lB954WXk866aSJy5Ytwwc/+EHcfPPNGBkZwTve8Q40Gg0sXbo0nD9//vJWq8UzZsw4RUQ2dgyVtjZtGgj6+m9X8EQAQlBWKBFYSBBqh1Mq/m3zgQvabrNAnwIPEHAEgBMIspJUIKTkARaFMQ89OlJbvjw/5vTT1DlHxpgnACyaNm1a38UXX/wxEYGI4IEHHsD111+PJUuWgJlxyCGH4Oqrr8acOXPwmc985m9Hnxu1qLta11Ggu7+OAtr1lZlBRM+7Hp3OjVrwG2+8Eddccw3OO+883HXXXRgYGMD999+PBQsW4Oyzz/5okiT/KSLjVPVGL6KNRoOaD64o24cfq3nIFEOkAlIDdYAnAb+jw+sBAtYBnV25v4OphWyHmPidALqgtEIIxbbnIkOg5KrDJT3ggLebgw9ew4aNtTYlonne+2IQBN2qyiKCOXPm4Je//CVEBKtWrUJvby9WrFiB888/H8Vi8Xnw9gZy1793T3vT3t21kIgwefJk3HzzzajX69iwYQN27NiB7du344tf/CKstS5JkhKAsSKyIm61qNVspcnd95yst92RWMAYYjEABYCxsIMKfQsAOPGXpz77w/cg6xkAArj7W65VHs1cGNsZpBYMQKAgiFIevX23tTas64vjmFqtFlT1QQBzkyT5xWgFp0yZgiuuuAJRFIGIEEURLrnkEkyePPkFkF4see+fl17KZ3aHfdRRR+GrX/0qms0m0jTF3LlzcfnllyMMQ2RZ9gtVnauqf0rTVFutBK11m/pNb98tqhIqoKTC3LbAqup3jPJpuVaXh/sTsIun0dMwf+zJd+0PxWRVPA7CjhSglgIJhDxAaSm/OffZT0+17z+9t1AscqlYZCI6Q1WfiaJoWmfF+AUV/nMAdtfCF2vCL9Zsd0/GmL39PRTH8QARzfYi1zXqdbRaseh//fe41uXf326ayZQA0BwMAlLJA8SKsUp4EwhbBlsjz86DPw7Yxb1NgEuc91e0C4qDFboDECK0zY4KyNdbU2iknqT9/X1JHEur1VIRuU9VD0iS5Jo9NbndwewN3p408M/9AHv7vr3lPZriOL5BVfcDsCxNErRaLcRbt/XpSD1zzXiSgtgAyvAwbSPSq4Q3AYB6fzmA/z3KbSfAEP7melqfNrppQqKtAIAhUAiAWD1BbO2XvxyJ7l52QL1eR6vVEieySVWr3vszvff37K3v2luF9tZ0X+z6xZrti/Wfqoosy2713p9FRMNpmm6u1eucpqkU7n/wwPov/m+DAGsgQgAZAhkoWDQZ5TKY1ueuh7/9BQD3BxLfNtHXdHxAPgyiEduZzzJAEFI3PDxO+3aMYP3GvlqtybWRKrz3d6hqMcuyblUdGm1+e6vYi/V7f8n13mDuCWKnTL1pmk4CEKRpeme90aBGKxa/ZsN237d9OKnWxrQdKEm4c9qbCcPKdLa2rcEvAG2c0nZofz7AdjOWC0fSxuiZCMuCHQYg204IIEoA9f7kqlLlqZXvTONGrd5ool6vp9r2OD04juO7te3L8qKa8FIMyksxIC81H1WNW63WU0R0sIjcFsdxVq/VEdfr9a6nn17c95OfFRhCFkohFBZqLKAQ7kfHi62aNtcmkG/syux5AA8GtntxBwB0BwAo0YcD5WFDpAYEA4YBQzPXvf26/75vzP0PlhuNmtZqDWo2m4MAHgLw3iRJfrWrEdj19aU06d0t8K4gX8p37CnvJEl+h7YP4oOtOB6oVqtaq9do3EMrituvuW4ZMt8TgikAwxBgicDKwyD9CAAocIsTN3th22N2zwABIIb8r6G0vqzd4jUnLD4E1HS2OgJAQ6KkuXrlfn79Rp9fs663Xq9ptVbzjUZjjao+AuCDzrlfAHhFNPFlal6aJMkvVfUMAA83m821tWrVjNTqlHt27WbevJmba9bMMgRvALWAsBIFgIA1BRAqgKG08TBBzt+d1wsALgS2irhAIT8CFFCcYYFVAUHzgIYAWRKyUF37o590j9u89ah0247eWrXKw8MjaLZaq1V1uYj8TZZlN6jqyN60cW9a+WLjwJeibaOvALamaXoPgLMUWN5KkjXDw8M0ODSk0rejf0LvjiPX/OA/ihaAhUoHIIekxJCVpLqkzUB+KOLsfOAFUZH2eNDmQ9ClcG5J3uZmKBBCKWcNDQsQCEQ8wSiIRMT0Llvef9CCQxZsjIKnHbikIgKgGQTBJlU9xTm3GsBqANN2b3Z7et2TMdh1/Lf7GHBPY0IigjHmHu89EdERAG6O47hvcGjI1OtNifsHBg5cv/HYJy765gbrfaVAxCGBilDNEWyOTaxCbwJhHIGqQ0ltex/kcz9re9/+eYA/BtynYaqG6HHDZhEIE6D6EClXAHgHYgAsBPLqo833P7Bm4cELjt1A9GhGWnaqEOdbzPS0MWauiExX1Z9re+Qf7KkP2xO43QHuCmhP0Dpz4CERuVZVTyYicc7dPlStJrWRmtZrVY37h/oXbu094bFvfXuFSZOxEZGNFDYCfA6MHDGp6nYiHA8Aqc++lIn//RGQPUb32OtZuR9C1nxc/GcjGz5JoIMBmk+svyPQBAACUqiSVVJIlhW3LLt/5eGHLzx8o8NTKUkh88555wNAN1pr+1T1VBFZx8w3icjB2j6a9bwmt2uzHJU9QdqLBgoz/1xVJxDRUQD+2Izj1fVaLalXq+FwraZZ//DWI/r7j3/4m998gprNSTkgyIE4YmgBLDkiWKUniXAOAAjwi2ra2Ocg+Ev3xmmvAAHgH6A31lz6oZzJEYCxUD6YCHczoUeU2DMMQCBSylzWtfHOu2tHHnXk9DjLHu/1bqzKzj4sZrarARUROUnbHq2/Q9tFrmtXiKPXe1p5GZ2K7QZvC4D/ApADcDSAJ0RkRa1Wy2rVBtXqVQwODMvkoZGtbxoZPuyBr3ytj5JkfJ5gQmWTJ0IR7AsEWKb1qvhIh8uq4WSkVYJ+6N/30HRfEsB/B9zHgWVO3fjIBAsIGhHRDIY+xNAygRVQgjBDlcW7YN0dd2bzDj7YTjXWPJVltVbqOUvTXOYz8c63VGU1M9cBHKKqU1T1NgB3S9uzfho68/Pdm+0uyRHR3UR0B4DtRHQgM5dUdV2apk83m83myMgI1RpNHR4aMLVavX5stV6sbNpaWP71i4qBl2IEaF6ZCyxSAEsemgXEW1RwGkGLANxI1riaVL4yp30YfK/ykmImPApzap7t3EKQ+067ctigwP2xYlwq4AYLN6AcgzQW9S0oeubM2fTmv/v0kY8Q3bUxF3bnCpHJBYFEUZ7CMEQYtnfjmDnsaOE07/047/16Vd3S8RZIvPdgZgugQkSTmHm6MWaYiDZ2oKejO3Rx6rXValAaJ9qMWzohTre9FXTy4z//5dLtDz64fx7gHLdHE3kYXxDhHINyxFsVeCsU+wKQetb6p1T844fA3/jn2LzkqB2PAOeUOSqHQefoP2EThP6YkUxvgnwLoi2QT1RLTZUkUTXehukxXzp/MJw8acyt3j1RZTOhkM/bMBeQNYHmcyEAAxMwhUEgaLurdcJmAUTtU/LS9kfU9jUABrIkJS8eBGiSJJRlmaZpqtV6S3oEAydZO6+1bVvfsv99yVjj0jBH5AsgHzKFecAVwFIAyCi2EeMoKGYBQJrF5zUlSQ4G/s9L4fIXxY15BPhqV5AfsRx2INIQAdd7yP4tQJseQQvQJpRarC4VlRQQU+5qHP2Fz6e5CRPG3dGsr9geBOMtIQzDnLBlCkyAIGBSJTWGoKoZGePEOQ8AbK1R7y0RWSfKDEWaeiiJpnEK5zJkzmfjVftOyOXfnG7v237vv12Wk1qtEAKImGxOyOZBvgClvAHyIDWglar0PwjtfjiV9Lx61iq8CfjWS2XyF0cuegz4+zyHYRTkvwmAFUiJ8WNRPTSGaqzwTYEkDI2hpuVhHSFLoGq7K4NHfvQjtfEHHHj8mlZ828NJq9HnsnHGhNYGhgwD1rYNhaq6dgwZoDM5sCKs4jLy4uEyp+KzrDsIB95aKJSmBdEJ2598+s4HfvaziqtWe3IgChQ2b8hHUJ8TQp5h8gREIEtKjwP4KLU9yKSZxV9IJPmL4P1VAAHgUeAjAduppaD0v9CJd2oIvxKlcgJfjAHTFLIxqcaKLGXlTGBa0CxTsWK40TNjZt9hH1hiumfMeEcs+sS6VmP1hjh221KniXrK4I2gHXiH4cnA+DwZnRQEPCOfs7ML+f0j4gMH1qy/88Hrfikj69aPZ9FijthZUBAxfCQkOYYtClHI6nNgH4AalrThFWd2CAwOp41/F3FrDwV+/pey+Kujtz0EHMXgT48NS3NBGI0XuAqge5TkgFhJEqiPgSCBaiJwKSRogbxTNY7IZ6rGkWYmFw10T506MnXBm3X89Olhvru7GJXLlSCKxgNAliR9rWp1JBkZafZt2JBufPghHtmyuUviZEygFAREYlXZErkIGoTgNMewEYhCIMsBlCMERvkpJX07FPsDACnuG0rrazzk8gXAn/4aDi8r/N2TwJgU+HlPWHqQiL/y3B39NROJE0yJiVwM0VQ0TIE0ZVgH+EzEZKAsgwYegIdmClivnCl5B2KFSHv8xWyhQlBjLElIgCOQDUFqAR9AA8vsQ4ADgc8BgWVKc2DOqQbM2KSqAYHeN1pCUflaNa0fTsCHDgGG/loGLzsA47WA2Q/4Z8sWXUHhQ9o+nAwFUkB/ysAYrzQ1IU0cqYmVxAGcifoU4IwhHmIgTI4FIuQEUAWI2lNGKFQIHZcxVmuElVgQgL0RmBDwAZOxgIQEEylcpBQxY6MRJI4weo4PAJ5pZM1rMnHuTcA36bnjZH+VvGIxVB8HZjvghyVbvCkw9hsKLXRuCSn+rzIEqtMzgnEKTQnkPJwzCJwCHuqkbTUcAeIERNSeAajCWoZqe2XcctuqBJahgUdmGUEAeEswgcIR0XoVWGqD43ZFqZn49CtN11rkgE8d3g7487LlFQ1CqwCtgDmVIH9bDIsPWeJ/1vYUa/T+kwq9j5WKRDQ5UwmFkHmAPRSOQOpJhSHtlcT2fI6gCiYigWGjsAplkBoAAWAMOBPVbSBtEOhotCMZjVYwdioXN9LGmwX844Xwv38lQyK/KmGQHwQCMmYJRD6UM7nbQms+D6V9n/cQ6VYAdwLUIiBSRQVAt5IaArwHgbQ9jFESNu1ItKbtLIFhIlQ73UQOwAlQmrRbzTaIc5c3fHyiMv9Cvb/2sOdicb1i8qpGMleAH7T2BPL6T9aYu3Im7CHQeXsvDQ1CZQVAI6QY0fZ0DqRaVEIXoF0gXgDVMXv7CoVeFvt0KPP+WGPoWwucu/Pl9nMvJq9ZLP0HgXFg89U8h+uZ7SWvRh4i7vyGZNNZ3DcOA171MPDAawhwNL8/sfllzuQ2M/EL9hdejojq5bFrTT1M/RmvVtj3Pclr/t8c7gRswdjfFzjHoOfCh7wcIcU9dYlj6927Xo1+7sVkz0d7XkU5HnDq3ftjadWVdNPOU6J/bSLd1pBWb+bdktcaHvA6AASAo4Bq5s1Xmz79tQKpoN3L/6XJAy7x2VXkzdfe9jJmEy9HXheAAHA00sdJ6QFR/Ye/FmCq+g9edeURSF8z5/Xd5TXvA3eXZRxcam00zMDukeX+nHwj88nYt/jnIvC+HvK6A1SA7jPhb4wJqtSOofBS5NpMsuKtLn3Pha/iGO+lyOsOEACWAXm1we+Ywm4iLPwzjz/mJNuSufT049vHJl5Xed36wF3laKClLjhbJHtEQdW9Gw2qZ97f5505940AD3iDaOCo3GNzx4HxVlZz0Z7ui8o/KrLlxzr3x9e6bHuTN4QGjsrbXHwXRJywnP8C7WP5AkHkjQQPeINpYEfoHpP7hRrapEr/0H5LL2fR8W/z8d/gNZymvRR5IwLEnYBlW/i9gJiACNBYXfOU41/ExeL1kjdUEx6V4wEnLlxCkCogfeqaZ74R4b3h5d6wNP/esDT/9S7Hi8n/B3LrBEUxxEM2AAAAAElFTkSuQmCC\"],\"useMarkerImageFunction\":true,\"colorFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n if (percent < 0.5) {\\n percent *=2*100; \\n return tinycolor.mix('green', 'yellow', amount = percent).toHexString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', amount = percent).toHexString();\\n }\\n}\",\"markerImageFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nvar res = {\\n url: images[0],\\n size: 55\\n};\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n var index = Math.min(2, Math.floor(3 * percent));\\n res.url = images[index];\\n}\\nreturn res;\",\"strokeWeight\":4,\"strokeOpacity\":0.65,\"color\":\"#1976d2\",\"tmDefaultMapType\":\"roadmap\",\"showTooltip\":true,\"autocloseTooltip\":true,\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"labelFunction\":\"var vehicleType = dsData[dsIndex]['vehicleType'];\\r\\nif (typeof vehicleType !== undefined) {\\r\\n if (vehicleType == \\\"bus\\\") {\\r\\n return 'Bus: ${entityName}';\\r\\n } else if (vehicleType == \\\"car\\\") {\\r\\n return 'Car: ${entityName}';\\r\\n }\\r\\n}\",\"tooltipFunction\":\"var vehicleType = dsData[dsIndex]['vehicleType'];\\r\\nif (typeof vehicleType !== undefined) {\\r\\n if (vehicleType == \\\"bus\\\") {\\r\\n return 'Bus: ${entityName}
Bus route: ${busRoute}
';\\r\\n } else if (vehicleType == \\\"car\\\") {\\r\\n return 'Car: ${entityName}
Current destination: ${destination}
';\\r\\n }\\r\\n}\"},\"title\":\"Route Map - Tencent Maps\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermomether\\\";\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See advanced settings for details\",\"markerImageSize\":34,\"useColorFunction\":true,\"markerImages\":[\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAwgSURBVGiB7Zt5cBT3lce/v18fc89oRoPEIRBCHIUxp2ywCAgIxLExvoidZIFNxXE2VXHirIO3aqtSseM43qpNeZfYKecox3bhpJykYgdjDkU2mBAB5vCamMNYAgQyURBCoxnNPd39O/aP7hGSEUR24L/uqqf+zfR77/Pe69/Rv6kWwcgPLRIJfZUAa7xez2xd90QBwDSNZKlkHJHAK+l09mUA7BP4vPpRUVExMVoRef+L998njxx9X57vPi/PnTsnO850yPaT7XLXrrflqjtWymhF+HA0Gp0wEp/kHymEQqG4ptJDGzf+um5RUxMSiV7Z3Lyt88L5nozgHJWj4pGmpqZav99PWve04onHHuswmViQzWb7ruZX+Udgv8/z3A+f/NGye1evxssvb+wo5PMfTZs6bfqcuXNHL7hlweh58+ZVAOTUpk2b0p9dvjyqqmrs/b8ejpUMc+unzjgUCsXjsYruE+2n1JY/NedM0zCi0VjA7/d7/f4AAgE//H4/vF4fOjvP9h5695C/oaEhcN/q1SyTzVdnMpnklXzTq4EplUsXfmaRCgC7du3cOn78+KfGj59Add3z1Md1vV7vqPa2D1sA4MYbZ6qUiqVX9X21i4TQcfX19QCA6urquN/vn0kAPRQKpYbTnzRpUhgAampqAEFrPjVYSql7fD4AgK5r2tV0AcDj8WkAoOk6JJGeTw2+nocLdsEu2AW7YBfsgl2wC3bBLtgFu2AX7IJdsAt2wS7YBbtgF+yCXbALdsEu2AW7YBfsgl2wC76mh/ppjIQgXVloPxVSBRV0rBe455P6+kTKBYF3tonxY/IWarry7DvI298Tgp0PR9RzACaN1NeIS100+EdvKXW3cMZvF8wCK10Sq2it2NAzakmukP/wmoP/KuId3BRUMg5uCfCSNVSKVn1rNto7Un8jLrUVqJ4Fi2eEQiEYBzOsy3SYL37TNQdzi8Q5FxkqJIQBsNLlYMGF/zqAJWBxSEogDAY+DJibYqTuRg4WFgO3OKhCYTExbKk5G/mbkSPP2DQhLA5IO/NhSz1MMP882BDgnAFQwdiVSs2vPVhYDIJLUMkBgw1favM6lJoZDDAYhKbAYsOX+rqAhcXAuQSIAKzhSy2vS8YmB7NYH4WCfM7kw5VaWtdpOO3bfWZJZVXgPxMX898bVsm6RhkTIseX29yyIErm/J5z5vwr6pvmsLYjBgeDwSpVJS/OmT1n1de+9qANZgLc4q9Dyj2qQhUhSSUAUCL7GBcchCymTEYBYNWqVXj30MGHT586PZEJ+WAul7ts8bjspd9QKDRNU2nz4z94YtI3H3oI+XwB//3j/9m77eRUUJ9/0eh4APGoDz6vCi4ksgUTmYyBC4k8RLGwtzF+EGu+tHqRqqrYtm0rXnzhhQ7G5cpsNnvyiuBIJFKnqvSd55772eilS5fhwIH9ye+/dPaEf1T9otW3T8GtiyYgGNBBymYEgLSbvakidu8/h01vnkYhcab1gcVs5tx5c6PHjh7DU0/9qFsINPb3939UZg28X11dXR0Qwtr9g8efqGtc+Bn89re/O7FhR9BXNaFm+n98uxHTZ1SDKQqKAihweZlITUVtXQwNs8fg+Bmzdk+bnmPdf/7bwsbGeO2ECaED+9/5XCxWuTGbzVpDwJpGNtx+28o77rr7bmzZsu3k7z+cMlHzeiPrvnoTwtVhFAVQHAZY4HBEoiAAeDXUjI/gyJGeQEd6TFj2tHYuXNgYy2azVe0fngiWDLNloHNFo4FZkXDsoTVr1+KD4x8U/3Ci1qP5PV7N74FeFUbClKDEriy57A5JANL5a68hnqoINL8OAPqbXbNp7clTxTVr1/oOHjr0MFXxq2Qy9wEFACnoY//6la9QAHj+9Q/eUL2RWkVXoWgqkhZBypRImkDKBFIWkLIk+h1JWdL+zrmeNCWSDFB0DYquQvWG637TcnozAKxbt45yTr8PAGowGBwVDAbvmT9/Pvbu3dddijV9WdUUUE0BUQm6kwaCYe+ljK/w8ruUdsYCBLlMEUQhoJoCygWM+LIvHTx4sGfevIbqYMD3BSFkJVUUrG5oaFABoPXwhd1UVUBVahtpKtoOnEV/gSHHgBwDso5c6XO6yNF24CNQTbV9qBRUUenuwz1/BoCZM2dplOJeSggWL1myFEII9IeXziIKBVUUW1QKo2Ci41Anei9kkWcY6Ex5R8qfc0wi0ZPF6QNnYeQNB2j7IQpFOtg0WwiBxoWNIBKLVQI6Z8rUqTh69FiWaFNmEIWgLFShoM5TZbIzgVxvFp6ID5rfA6JQgBAIxsGLJkrpAsycAcH4gN1gX0QPTW9vP5Grr58cJJTOpbqmjgWAnp6ei4QSEEJAKAGh1BbHCS2DLAFmMAgmICwObjDnyYMMAtJL9oN89vRc7KWUQtOUsSqhSggA8sWivSEh9qBxTiCEAGRwQARUVaB67Hf5pZAQlA0Ayrq2LTCogVyhlLURNEw55yYABP2+4ED3vHSClBKQ9jiFdHqvEBCMQzAOKYSt6/RqSGnbDPJRbgT93hAAcM4NyhjrBYDKylhswEEZJgYJFxDchnGTwSqasIomuMnsIDiH5GKIzUAQTsCVlZUxB9xLIUVbKpVEff3kiLTMfimEA7HP5bZgHMJ07mnJAiuaYEXT3jcZDMLkTgBD7exgBKRp9NfVTQwnk0kIKduoJGRH8/ZmhMNh4skc3DnEkDlAi4GbtjDDguVAmZM1M6yB68JyKsCGBqD373s7GAySnTt3gBDyFhWCvPHee/8HAJhTU5g0BMg4uMXBTT4AZSUTrGjBKpiwCnablQbDbZuyfTmAuRPMegA4euQopCRbaCaTOd2XSLzX3d2Nu+64bR7PnP3LJSCDMBm4YW9FWcmyQYMytsW+Zpfdsm1MdimAdMc7K29bMedCdzeSyeS76XT6jLNI4PGf/+w5aLqOu25IjOOWKcSg0jJjcLZ2ecsZD5TdybqsOxC0ZYpbJ58frek6nn/+eVBJHgecjXkqk2nu7Ozcdfz4cdx556rJN5C3m8v3jBt2xpdnazjysawNy5lUbKkrbmtZsWL5pGNHj6Or62+7k5lMy5CFNRQKTfN6tAMvvvhSRe3EOqx/4oXXLvia7qO6CsVZrey5154KB5YpKSG5tHs+5/ZsZnEIk6Ei1fLH73373i/09fXi0fWPpgyTLchkMqeGgAEgHA5/vjJWsf2PmzYr1dXV+K8fP7vjLxduWkY8ilpetQZPg+UJxh63lzqlNDi7gTa3fuPraz6bzxXw79/5FutP51am0+kdZdaQ/2kzDKNDUci51179w8pbP3er8sAD6+pnVCWy+/fs21LAqBnlMT50qJXFLq2a2L/5gaVy7N133j69u7sb67/7iFHIFf4tlU6/Ppg1kLGU8hYAywBMeOWV33gfXb9+1Q+ffDL+4Ne/AcYY/tS8PbV5++4Dhy+MopY2ZrLiidQDgDBSp5TS+Y7psS65ZOHsW26++eYosxje2PwGNm586eKzz/x027+sXWsBOAfgbULIQQAgUspaAA8BGAfnsamrq4u0tZ0Q333kkdGmZS3f8JNnlBXLV0AOilRKCS7sWYlxjlKxgHw+j5Y3W/C/Tz/NQ6Hgjp9seKZ31py5ajwe4wAtz9zdAH5OpJTPAqgEgL5USkpu4eLFHloqFXniYh9t3bunauuWrStisSi5//4vYnHTEkyZOhWqokBICcuy0N7ehr2trXjt1VeRzqTl3ffc81bjgsZELF4pQ6EAqa4eI6UEicfj5dhTKoCikynx6Bop5C14dJ2XcjmouipvvGFGoSJaWfr738/7tmzdjl/88pfIZjKwnH2SpmkIhSMYW1ODhvmNGFcztjhudFXR69Wgck58Hg+XEorH5ylDJYA8kVKOckpdB0ADIBOJhOzv70OhUFILuTzPZLNcSE6SfSlvJp0O5A1DN0qGDxLS4/OUAh6PGQqHC5XxeJEQgkgoRH1+L/wBP6LRuIjH4+Uf8gSAUwB+MbhzzQSwCMA0p/QUQADgNJ/PJ/v7+wnnnFiWkJZhKCYzKADoqiZUXeW67iGcSxKPx2QoFAo7AybnuE8COAZgHyHkxGXjeFAQEQCzANQCqAIQBeAH4AXgcex052w45TMcyQHIAOgBcBbAUUJI5uOM/wcaHmf3g9UM7QAAAABJRU5ErkJggg==\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAA3vSURBVGiB7Vt7cFzVef+dc+/d90OrJyO/JSO/4ncxxfULMCYIAyEW08amJJgmM4GmnZjJdNq4gcSGzLQxk3bsaWcaaIHyR8CJrWAbpjgG/AhINsbYxkaSDY6xJFvSrrS7Wu3uvfecr3+cu1pbXhkJs/4nujNndufec77f+d7fd+4uw8gvIxwOfocBaz0e91yXyx0BgKyZiWUz5kcEvBKPJ18EYI+C5rWvkpKSyZGS8LGHGtbQR8ePUUdnB50/f57OfnqWWlpbaN++39O99fdQpCR0NBKJTBwJTfZFE4LBYLmh8+YXXvifKctWrEBPTze9+cbu8/3JVMoWNjwer3/ZsuUTvV4P239gP36yceNZW9CtyWQyei262hcB+7zurU/99Ge3r1nTgJdfevFsqr8/Wlc3rWbGzFkV8+fPr1iwYEEJgLadO3cmbr/jjohh6KXHPjxamsmar39pjoPBYHl5aUnnqZY2/b1Dh9LdPd39kUgk6PP5PD6fH36/Dz6fDx6PF+fOfdZ9+pPTgbq6Ou+aBx+0k/0DVYlEIjYcbX4tYM5pxeK/WKIDwM7Gxt0TJox/dtLESXC53JuHzvV4PBVHDjfvAYDZs+fonMsV16R9rYeM8XG1tbUAgMrKsrDP659DRJ5gMNhbaH5NTU0IAMaPHw9IPv5LAxORy+31AgBcLsO41lwAcLu9BgAYLheIkftLAxfzGgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4D/lME1ke7gDF8ltbOHe3W923oEwYi1jxftWfZWgAziwacZkd2pfyN96XN5IIu7dMtIKA9/TI+zqCnFps2Alg5UlojFnVqIHZUlO2sl4RyC4CU+SEEylux8Z/iyc7mrxw4U7UnYwvGpXMYKIgNGdwXC/76C48oRw3sDWfnCgIkARJXcpwbvpA1e6T0Rq5jDr8EAHKA6OpjUOJwfeXAJAEhAXAGgEPKq+dIMVJqowDO4RAAC0rHV21u5LijAJaABAOIAY5Oh15iFMgj1zEpcUuuXjpIWeCouxjAtnIZcGKA5AVFbRfazPUC50QrKe8+Qy8qiqjBYIODA5DgBd1pBO9WRg9sy7yOhXBca+icYrgTOUGOiKnIVdCdisAxJGBTPsYW0nHRrJqgfNmGVtiqaeR1xchF7Vgz40q/BUNmISlcL7CUgJAMnOUiVwEdF0PURIAAVHaC8ucbAiwcQAb1KQpwXMjFrhtYMcOVO8lhOB457ujcKZd9hBguSYwcelTupKyaQWKYJFEU4xJw/Dhfcw29ilSBcNjEoTucFnSnkeOOvvTJpcVC1cYoGB5NAGEQTukjMAzHoghJghyWCRjenYoTuZjKx8xJiwU4LrSZ6waWpIoBjTuRqxDHRUkSUMWAJAZp6QU5FqOw65HHapG3bGVcBTZXDI5VnFaFgBL1yC34uoBJqEJeIwD2MMY1ilZidAFEMlDOqm9UdpJ0ZawumI+LU9ArwhyqWxyNz14XsBAMUnLVH0ttGB0XococdCGWE3XhOV85MF1WV2OY3omK0S2SkxgYAZYYJoAUpcqEEjG/Ru80isA1ysMXYNCnCum4aKUPgTu90w3sFinXL6nO/MadCAhiKloxBjFMeSuK0S1Kylv1cE1bUVoYyHwhoI6bCswpjjuxK5u2G2lcti2jzNCRTluioHEVw52EBA5/2LKsLBL+h2gs/o+Fjpa+MqtmjCbkqQJSYFF3T3zRsPMvA75i7UiBA4FApa6z5+fNnbd6/frHADghk7QdlhAHdMY0KXkZAHAuozaRMDRtKYMdAYDVq1fjcHPTD860nZlsS3qsv7+/+6pNDr0RDAanGTrf85Onnq75/uNPIJ1O4+dbnj34Ot6B4eFLqksqUeEvgcflAREhZabR09+Li/EorLQ4eFv317D2oW8t0XUdu3a9jud/9auztqD6ZDLZOixwOByeouv8D1u3brtpxYrb0XS4Kfbj3//8VHC8d0nDLXfj67OWIeQJgDGADfoOAxHQl05i14l92PHBXiTPp/c/OrFh9vwF8yMnjp/A5s2bOqXEbX19fX+8CriqqspvmunDTz/10xkr71qFnY07Tr1i7aqsLg2Vb6h/GOPCpdAYgTPlNLmF5AzpvBRp74viX3a/hO6+ge47+hZG61fVTz9y+DCee27Lx15fYFFHR8cAcNkPuw2DPXfP1+vvvf+BB7Br967WX9Mbk70eCn33zlWoCrsgKAFBCdgy/2nLBCyZgCUSMGUSpkzC0G1MrKzE0XMt/la9I0QnM+cWL15cmkwmK1tOnwpksuabg8YVifjnhEOlj69dtw6nT51Kv2q96fYG4fG7gbJwFhn7cxicIJgEZwAfEiokGASpWG1KhvIwg1/91ti1N9DEJ7ZOzKxdt87T1Nz8A67jv2Kx/o85AJDk//zXjzzCAeA/D7zU6PZjkkuXcBuEjN2OrGiHabfDFB2w7HZYoh3mVaMDWWdu1m6Hy5Bw6RIuP6b87+HXdgDAww8/zIXgGwFADwQCFYFA4BuLFi3CoUN/6LRmyL/y6gSXTtC4QDTVgQo/B5iEJFJ6Rt64lI6Vfi3JYBFHd1JA5wIunUNIQvpr/C+bm5u65s9fWBnwe9dISWVc0/DNhQsX6gDwTuuhd3WNYOSGTjjSehGp7EVYsguWuJQfssu51wVTXIIpLsGWlzBgXsSRM5dg6Hk6uk787Zb39gHA7NlzDM7xoM4Yli5fvgJSSiRmmbP9HNA0Qm4D6axEc6uJ6eOzuCloQuOOjlneqiUx2BK4lDBwut2DTFaHoXFYGilaHEjMMOdKKXHb4tvw/nvvL9UZ+Lyb6+pw/PjxpOZhsziX0DigcYLG1QaEBD69ZKA7wRHx2/C7BDSNwEi9AEmZGmJJA/1Z9SJM12hwvcYBzgmaj89obW3pr62dGmCcz+cuQ68GgEtdl7oYU40CZwSeW+As1rmy5KzNkbY1WILDlOp71ubgnKA7czVO4NyhwQhcFS7o6urq5pzDMLRqnXEtCACpdCrFHOHlAsTgYEq0nCnj0jnBY6i8KCTLBxbmzB2yPkczmU4lAYAxHtKFECYAPeDzBQZD4GU+motMueXklECWc7QkSaVDGoTAVetz8AGfLwQAQoisbtt2N4BJZaVlpZQjkntdS8w5UFOFni0YLMGhWfny1rbVPVuoOVKyK9ZeTrMsUl7qAHdzkPyktzeG2tqbw8KihCQlPjVUl2hLBkswmDZD1mJIWxwDWTXSFkfWUs8sZ64QzlqHjiRA2tQ7ZcqUYCwWgyT6hBNjb+3ZvQehUIi52tje3M6FyHHIYNkOqM2RsTjS2cuAs+pe1uYKPLcBkduA+m60sH1+v5/t3fsWGGP/x6VkjR98cAQAMNc7bXJepAyWzWHaimjW4siYDGmTY8DkGMhqapgcaVM9yw5ugMOyeX4DkmGub1otABz/6DiI2O94IpE4E+3p+aCzsxP333PfAvOi2G8JBtMRbU68GZMj44Ao0BzXmgOsRk7spq1oWILB6rQP3nt3/byLnZ2IxWKH4/H4pxoAeFzuC21tretW3rUKnk5mtWiflzAGxhgDQ66IYyrnOnqzBFfDZjAdLk1HMnkpMWRNLldmFomamtrIL/71F+iPJ/8mnc2e4QDQm0jsOXfu3L6TJ0/ivtX3T607M26P6SzMWI5eB7ktPHLPc/MV5xwTjpe9sfLOu2pOHD+JCxc+fyeWSLyZdzCoWsvjNpqef/6F8KTJU/DDLT/a3jM90eDWCS5dqmDvxF7NCRSAOikQhCuMUXHMEDjm3v7jb/+oIRrtxpMbnuzNmvatiUSi7QpgAAiFQneXlZbs3rGjUauorMSmLc+8dShy7HbDELqeA3bC4GCScHxWSMDOgVuaPb2t+t3vPfK9O1P9A/j7v3vC7ov318fj8bdyWFf8YCSbzZ7VNHb+tVdfrV911ypt/bcfq52J2uTBg+//LhWwZ0nJYTtWf6WrcccDGFgLdn5nwkPVD9Q/MLOzsxNPbvhhNpUc+G5vPL7jcqxBjonozwEsBzD5lVde9jy5YcPqTZufKX90/WOwbRv7330nsffDt08dSB41EkZyHPfwmwBAZuTFsBm48GeuWfai2oUzp02fFjKzJhp3NuLFF/+765e//Pfd31q71gLwGYC3GWNNAMCIaBKAJwBUO3uQnZ2d/MyZNv1vn/j+LUuXLq/Z/MyzCIfDTmxW8Y+IVFyWqjKRQkDYNqKxGDb97GkcOXLk7LZt/9F8c12dqKqqYM4LYALQCWAbI6J/A1AGgKK9vSBhoa8vEe+N9TwejcZYU1MTfrN9O6puqkJDw0NYtnwFpk6dCsZUMrFtG22trTiw/11s3/4aotEo1jQ04NZFt6KsrJTCoZKtJaWRiGG4KBKJ5BJWnw4gDedAx+0yMJCywLnQGWOSMabV1NbikUfX40J7B367sxFbt25DMhGHZZkgAC7DhWAojOpx4zF3wS0YP64aVZUVYCoQSN2la4bhIsNlcOS73H5GRBUAHgcwBYABAD09PZROp1gq2V8WTybq4vH4xEQ8oSWSSfSnUkinM7As9RdUw9Dh9XoR8PsQCgYRCodESTj0x1Aw2OrxBXsDgYBdXl6eM2IB4CyAbZcb12wASwBMB1Dq7C4ACJZIJHstM5PWdC2TTmcom80wEtySAFwupum6wbxeDxeCuT0et8/v94UBTTrSJABRAKcAHGCMnbrKjy/bRBjAHAATAFQ5NuAF4IFqAtyOKzKo83MLgAkgA2AAQB+ADgCfAzjBGIsPxfh/6wbDK7xbMFYAAAAASUVORK5CYII=\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAyUSURBVGiB7Zp7kFRVesB/5/S9PdMz/ZoHMwo4MICDuoGVIYICIuzGcn0vC+oWGuNjs8mua9ySP4wpgyaiVVupbHYTsLJmNT7WNXExwqqzrq8g4oNxdXUgyEMQARmZd3fPTE/3vfd8+ePenhlgBsFlrFSqb9Wpvn3vd77f+b7zne87ffsqjv+wE4nYDQqWl5aWfDUcLqkAyOUHunID+Q8EnkilMo8C7gnoPPaRTCYnVyQT71+1bKl80PK+HGw9KPv27ZPde3bLjp075NVXX5FLL7lYKpLx9yoqKuqOR6f6PIFYLFZtW7r54YcfqV+4aBEdHe3ywm+e39eb6etzPZfS0kj5woUX1EUipWrj6xtZedddu11P5mYymc5j6Q19HrgsUrL67r/7+8VLly7j8cce3d3X29vZ0DB9yplnfWXcrFmzxjU2NiaBXevWrUsv/trXKmzbqnz/9+9VDuTyz35hi2OxWHV1ZbJ1245d1ltvvpFtb293Kyoq7LKystKysnLKy8soKyujtDTCxx/vSW3fsT3c0NAQWbpkiZvp7a9Np9Ndo+nWxwJrLYvmzV9gAaxbt/75urrxd592Wp0Oh0tWHSkbiUQSv3unuQlgxoyZltZm0TF1H+umUnrC1KlTAaipqUpESmMzFIRjsVj3SPJTpkyJA0ycOBGMnviFwSISLolEAAiHbftYsgAlJREbwA6HESUlXxg8lkcRXAQXwUVwEVwEF8FFcBH8/xhsnZC0ksw49eQPI5mmNtP54ccAIvqgqbz4aYn8zYoTUXXcFnueyZ8eXtleZt75iQnpU0VUvYiqB5mvu5p+XH9w8RtgnJMOLut/7rd4+fpRBcS52hz65csnHdxQ8clZnyuT3NV40sHRUnfq58mUWFJ70sEn+yiCi+AiuAgugovgIrgILoKL4CK4CC6Ci+D/Q+Djf/higk8Jzs0IMjIGYDGAp0AUeBbiHf3Xs/HGAHyYlYaRX0EYC4txNeIFugvWHyXzua8cnDjYGMBoQIFhRFfLmLjaCxqAw8iuHing/nCwGlLuMrKrveNfnccPFnyLtQ8c0a1jElye8sGFAYwUSCN54Q8GB4ljKKpHkBmLOZbB4FLgjhLVYxNcDFnkMXJUj03m0kOKR0sgYzLHRvlwpcDYI7oaGYvl5HB4ZRrJ1cf9fP5E/5NwQUKM7uoTOI4/ql38kmgUOCMnEHMCL819sag2jJJAxgIs+HNY6PGlpUxXDQWXw5dXjxH8SFZBPf7SyqKrMQLKG7b/OkpmTBJI0BSjbwTGYo6Ni5+ZjMJDj1wkxmQ5iV+VsBh9BzImKbNQFhWjp8wx21c7dKIV9A94IxaJsdplZt9574JQVcUdpr3rzlEHdzLASslpg19EofLMMa3dc0Z9c9YMXT+s7/GCo9FojWWph87+6tmX3XTTzT7XA/F4xutXr4fyOuQZVQUQ0tLphY1nlcn5YqgAuOyyy3inefOtH+36aLJr5Obe3t72o4w68kIsFptuW7pp5d33TPne928hm83yLz+6b9PVb/4niRK9QNfUoquqUaUREEEG+jGd7Zi2Dnpy3qYHGr7OFdcsX2BZFs899ywP/fznu11PLslkMjtHBScSiXrL0m+uXr3mlEWLFrN58+auxD+u2HZWhb0gcvkyShZ/Ax2N+70KPcVvJpMm999NZJ99mi1dzsb3rviLGbNmz6rY0rKFVavubTWG83p6ej4psAbfr66trS03xtlw98p76s+bN5+nnvzFtouevK/s1AnJM+I/vB37j6aDziJeCtxhzUkhTgoYwJpchz3zbJI7fj/pzA829f6iR/bPPW9e9aS6utjbb715YWVl1SOZTMY5DGzb6scXf+OSS6+48kqanntu55+99shkOyLx8uuvIjSuDEzq6Ob5TdzgPJ9GhT2sCbV4W1vK57R+FP9lOrT33PnzKjOZTM2OD7dFB3L5FwaDq6KifGYiXvn95ddey4fbtmWv2fhIiVUqpbpMEao2SH4fiKCMgAbRggSuVkKwEQz22q4iVKtQEYUtJvzdlvX6+bq67PJrr41sbm6+VVv8W1dX7/9oADH6b//0+us1QO/jD6xPhGWSCgsqLJj8PsTdjzj7Ma7fxDkAzn5wjry+H3H2YfL7UGGDCguJEqnPPf3YOoDrrrtOe56+C8CKRqPjotHoN+fMmcObb7zRelsk9W1lC4QFCRlM9yfoKnsoEgOLVWCxDLfYBRwwnXmwDIQVyoMbo6lrfrq5+dCsxsbaaHlkqTFSpUMhvjV79mwLwHvjldewBGxQlqBswXn3Y6T/EDhtiNOGuG2I2444QXPb/WtOGzhtmL7PcN7di7IFFegiJDq3+ZVXAWbMmGlrzRJLKc6/4IJFGGO4MdQ+gxAQEn/2LcH0u+Sa27HO0IRq/V+MSqnBOUZARMAD75DB2w4mq8AKWkggpPiOtJ3dYgznzTuPt996+3xLoc8+vaGBlpaWzFybrygtqCPgeODtcTFtBl1hUBHfGgl+wNGv8FIayWjE6KCfD1UhBVqotPWZO3Zs7506dVpUaT1Lh21rPED7oUNtKH8OUYLSoHTwWRiEAsmBDIA4gCPIAJh8YL3lyw7vi5JAJ7QdamvXWmPbofGW0qEYQL4/0zeYjdTRTQ0Oxp9/Svx9jvKAkBocsCh1dP9AZ76vNwOglI5bnuflAaukPBo9bM8UpMIjvxeiWAUbATHK3/yNJM/h30vKozEAz/Ny2nXddoCKyqrKwc5GDYFMUJmM8peLqyCvkH6FZP1zXP+eGBXIFvQcrquyqroyALdrxGzv7u5i6rTTE3lX0gUL/DIYPPfwFDh+k5xCBhSS1Ui/9s9zQ/cLz0rEGxqEGMWAK92T6yfHu7q6MCLbtSj1UtPzTcTjcfW0E3t5EBSkv0FgPgAMQgtWa/9azpcZHICrhvR48B+52CvRaFS9/PJLKKVe1Mao9e+++zsAtk9rnIwbLBFHIQ5IACWvkJxGBjSSDeDZ4HxAIznty+SV38chGIA/PXumzZoK0PJBCyLq1zqdTn/U2dHxbmtrKxddfmXj1r7QRr9jMH/5Ye4d8OdV+odZ3F+AqyG3F/oFelr62PQnl14667PWVrq6ut5JpVJ7giLBygfWrMYOh3ll/pLx4iojR7p3QMGgpQX4kPUE8OFuF0chrjIvzL78VDsc5sEHH0SLWkmQLuhOp5v27t376tatW7nk8iun/UN8VhM5BblASS5w53BowdXD4L7Lg8EG7Z6SM36z+MILp25p2cqBA/s3dKXTLxRSBeDvtUpL7M0PPfRwYtLken791z9Y++fevmWE/WJBIelbgJbDtz4mePblBksrcPU/ubVrF65Yuayzs50Vt6/ozuXduel0etdhYIB4PH5RVWXy+WeeWR8aV1PDz+6/56W//PDFxbpELGULgwVEcwSYoWXkKExOuatqGl9b8p3vfb2vt5/b/uoWtyfVe0kqlXqpwDpql1lVlbwhUhr52VNPrQ3PPuccNm16PbXrR3f+9pvm0NV+pWEwhQKIqKHnm57iV9nydc6Smxc1zm5MHvj0AHfecUeuv7f/u509PY8N5wyCReRcYCEw6YknHi9bcfvtl9276r7qG2+6Gdd12bhhQ/rghhe3TdmywT4l2zkhEeIUgJTLZ62RygPbT5/rlv/xvLOmnzE9ns/lWb9uPY8++u9tP/3JPzd9e/nyLLAXeE0ptRlAicgk4BZgfDAGc/DgQb1790fWrT+45Zz58xdMue+++0kkk/5N8RO2iPiZ0BiMCMbz8FyXzq4u7l91L5ub3969Zs2/Np/eMM2rrT21YKQBPgPWKBFZAyQA093drTzPobu7uyPV3XNbR2enam5uZu3atdTW1LDsqqtYeMEipk2b5m8GANd12bVzJ69vfI2n1/6Kjo5OvrVsKefOPZeqqkpJJCtXJ5OJinBpRJLxeOF3bI8FZIAYoEN2SHmeJ6GQ2CiMUipUP2UK199wI59+2sp/rVvP6tVryKRTOE4eAcJ2mFg8wfgJE5nZeA4TJ4yntmYcSimUUsaydMi2wxIKKTXM6n4lIuMCV08m2O52dHSQzfbpvkxvZSqTbkinUnWpVDqUzvTS29dHNpvFcfy6aNsWkUgp0fJyYrEYiUTcSybin8RjiZ2lZeXd0WjUra6uDg2L/z3A6uHBNQNYAEwHqvAXTTl4Kp3O9HhOvk+FGMhmHXHdHGLEE8CytNY6rCKRsPY8VRoOh8tisfIkhFxgIAB2AtuA15VS20ZcTsEgEsBM4DTgFKASiAClQAnBig7EC8/8BoAc0AekgE+B/cAWpVTqSMb/AlY1WXIncMcxAAAAAElFTkSuQmCC\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAxNSURBVGiB7Zp7kFTllcB/5/a93dMz3T0PemYIDgoCPhZ5iaD4wNkFjQjRRMlLTNbSlKlyzZpobSVbFRPUbNVWSRCWuKvlxqybtbIrukp4SATZCAgospEBgeElj4EZ5t3d0+++37d/9O2ZnqEHQZzZSlXfqlMz/c253+875zvfOefeHuH8L6u83P+AwH0lJZ4pbrenEiCVSnYmEsndGl4NhSKvAJkLmPPcV0VFxZjKivKPv77wXr274WN9uvm0PnHihD5y9IhuPNioN216Vy+Yf6eurAj8b2Vl5aXnM6d8loLf7w9apvHhyy//29jZ9fW0t7fpdWtWN7Wdao4qpaiqDpbdXF9fV1paKpu3bGbxk08eSWXU9ZFIpOPirC33v7xs+TIdiUT0Pz239NjeaTOTHXXjdb4cuP6W5DOLFx/7aNdH+oknfqQryv0vXZTFfr8/GKyqaN7XeMhc//ba6NSfPFXqS6fESJ29jdGAX69+9KHY9OnTyxbec08mHInWhsPhzsHmNs4FNgxdf+NNN5sAh3/7n40dCxeKedUsOr6x8CzdsnBEQu9sPABwzTWTTMNQ9eec+1x/FDEuGTduHABXtreOKutJYyiFqq4tqD+5O3wJQF1dHSij7nODtdZuj9cLgMfGOpcuQInSFoDldqNFez43eCivIrgILoKL4CK4CC6Ci+AiuAgugovgIrgILoKL4CK4CC6Ci+A/B7B5vor6Mz4PNnbRYAAtoCQLUMMFVobuBWOALWdjVIGxiwbbZC3WkrXWLqAzJBZrR5T0LWTgdSHfdF1YcIlG57t8oM5nfov1OcCKPmDW1Rfi2IsA5yI5F9WFXF0o0i8arARwggsBu4BbhwaM6g0ujXY+9b+GLqrzLR5E5wsH2ziB5QRXoW8lCy3mosH553iwlDlEe9znai2DpMyhAJ+PxUNTJMhZm51+WM9xvsWFXD2kx0nl9rjQ4oYC3C+4BoEMnasl39Vn6wxRdcqbXApXpwupWBcEVgLKGLw6DU1w5bkaCjcChcYuHozuLYtqEFfroXC1TZ67GcbjlEuZWjSIHr6ozjZ7/y/VSWOLdgJIF9zjQl3JFwDOXn1lsYDOULm6X+YaROcLB6s8+LC2tzqvoc+Wx0L2nT/6wlIm5y6LQ9bs5TLXsO5x7jG192lxuJq9bCOg0aIRGcYEkt9lCsPp6lxlMsBlFE4ghcYuGoxznHKFYNjKYq7Zy5XFYW32lMtCBGzbLlwWLwB83m/2NNC44R0iFaP503+8jO1UqHz5wiwW0aNzvysgdPJTQr/7dFD9fHD+vecN9vl8NaYpv546ZeqCBx98CMhGbPXEqZRfcTWmyySTjuO2TMora/B4Sji+832OnWoGYMGCBez88IMfHD50eExG6Yd6enraBjJcAwf8fv+Vbsv1Pz9f/NT1y1esQCnNPz6zeGuy6WBN+MRRrwp1YMR6MOIJMqEuOj49xNFd2zh5aD9SVpr44PCJXVOmXXvpHfPm4fP7rtz98Z/usSz3+lQq1e/fnvuFSHl5+VjTNLb96lfPj6yv/0t2bN/eufJnj+37Uql1c/1Xv8WM279CaZn/rJcBGoj1hNm+7k22rF5JcyK1edp3Hps0bfq0yj0Ne/jFL55pVopZ3d3dx88C19bWlqVS8Z2Lf/7U1XNvu51Vb72x7/irz9fUBEcEv/03PyFYPRJDgZHt9XpvzG8QlAFnWppY+S9LaOnsaPPOWdhxx7z5V320cydLl/7yE2+pb+bp06dj/VxtWbJ03h13zr/r7rtZu2bNwVP/9cKYMiHwtW8+QNAbwOiOIN09SCiChCKQL+EIKhxBhcN4EGpGjuJww66yxNH9gePac+zGm26sikQiNY379/kSydT63uCqrCybXB6oeuS+RYvYv29f/OTKFz1+dIlXXFQrCznRjNhkRfdJzmIMEAExsqbUmh68holWGXf43deMg6NHJ+5btKjkgw8//IFh8lJnZ88nBoBWxpPf+e53DYC1Ly5bVSb6Mo8WSrQgx5uRY6cHSDMcz0q/vx/PSTNeJXi04EOPfe93L70JcP/99xu2bfwUwPT5fNU+n++rM2fO5P3332+uS3V9y9KCG8FSmtjRo3iN0uz+qqylemDnLhpDQDsFJGrHMG2F2xAyGi5Nhr65Y8f21unTZ9T4yrz3KqVHGC4X91x33XUmwN7N775nApbuk90nD5BpbUbaWqG9Dd3eju5o6y/t7dDehrS1kmltYffJ/ViA25nDBcbeLZs2AUyaNNkyDL5minDL7Nm3opSiNtQ0yUQwESydlXg6xc70Sf5CewliYSD9TqHu/anpIMUnJIiLjSVCGjAFTA21odNTlFLMunEWO7bvuMUUjKkTrriCvXv3RDyiJxpacGVXSc56W2uO6DhtKkmFFsocHchmtKhoukURNrJPG5YDdAEuDYaAV/TVjY0HesaNG+8Tw5hmuC1zFEBLS0urkQ3QPtFgILgQTC0IkAZSgEJQCClnTBwdF4KBOPf2iQBnzrS2GYaBZblGmWK4/ADxWCzqoS85iDOZDFiMS2ddV5Kz2EkGhgwECYLOzqOzxy0W7YkAiBgBw7btFIC3tMw/2JsrnS9OI5B2pPdt0AC9gdVZZxkBANu2k0Ymk2kDCI6oqsw1c/nNu8rVW8l+2ZFCkxRNzMhKUjQpNBlnv23nXfbAeTRQHayudMBtBlod6OrqZNz4CeVprcKqd4KsZBxgGk1KNEmBmGiijsScsZRo0s4CMnn3284CMqJCY8aOCXR2dqK0PmBokQ3r1q7D7/dLq7tyY8axMCOatDNZFqhJiCbuWNsLNrJjCUcnt4C0ZOew0WTQnDYr3/X5fLJx4wZE5B1DKVm1a9dHAIyYesPYjEBa+vYwJZAUSAgkHAtjookaWcl9Togm4eim8u5PS9YDNVNmXg7QsLsBreX3RjgcPtzW1rarubmZ+QvumtahXJvzrUzmWRvrZ61yxNnvPKuTA6xvt13bvjxv/tSW5mY6Ozt3hkKhoy4Ar6ek6dChg4vm3nY7oZJAJnG4oUIQESdD5Ud0v30XSBlZC1OGdjyTA/darwK3LcxcPm585ZJnl9ATinwvnkweNgC6wuF1x44d27R3714WfOWucZGrb3g7kee+eJ6LewPLcXU0bzwuuf2G3P3NoyevnzP3tsv3NOylqenkHzvD4fWQ197aikeW/nJJd1dnJ4//9On57V+a8Hoib7K4kQeUAWL0D7RcsJ2oqHv9wUcfu7Orq5MVK5Z3KS0P53j96lsgEPjyiKqKtW/891uu2tpalvzDMxsTW96s9yhMC8HUOCkxm07JO/fZk5A9dkmDTOSqWe/99fcfmRPtifHY3z6a6Q5F7gyFQhsKggFGjKh4wFviffG11153T59xHVu3bg3968/+7g9V3ae+0Zv0kX49l3ISjA2ccpe/NXvR9+uvnX5tRdOpJv7+xz9OxnpiD3d0d/97PqcXrLWeBcwGLnv11d96n3j88QVPPf108KHvPUwmk+HttWu71q96Y0dozzajJBUfXyqMA4gpfShmeY54JkzX19/6VzfMmDmjMpPOsOqtVbzyym9alz23fM23Fy1KACeAP4rIBwCitb4MeAQY5SxEt7a2qIaGBn70wx+OTKXTc5Y+t8w1d85cdN5KtdbYSqGVImPbJOIxotEo6/+wniXPPmsH/L4Ny5etaJk46Rqprq7JPTgooBn4Z9FaPw9UAHR1dSnbTsuZMy1GMpnItLZ2GFu3bq5d/fvVc0ZUjZB7F36d2fW3MmHCFZguF0pr0uk0Bxsb2bL5PV5fuZLuUEjfdffdG2+66ebW6mCVLvP5qa4OAoYEg8Gcg7tNIAIEADHdJnbcxmNZ6UQ05nK7TT1x4sRYRVV1/FTTqdLVa9bywgsvEImESKfSAFiWhT9QzqhL6rh25g3UjbokPnJkTaKkxFRaa8NtGbaIy+Up8eS2VgEx0VpXO66+HKfdbW9vV93d7RKNJl3xeNQOd4d1Mp0i3B3yRCKRsmgiYSVTaa9orS23lfR5vany8vKYLxCIeyxLKqoqtddbKh6PSVVVtQ4Gg5IHPQI8nx9ck4CbgSuBarJnvARsiUai4XBPmGQyqbWGRCxh2VrZAKYYLtNjZUyXSxsuU6oqyg1fwO91nhUSzvQdwB5gm4h8UvA4OYsoByYDY4EaoBLwAN7sYiDvZ4LsqUo60uNIK3AY2CMioYGM/wPREY0iGUY58wAAAABJRU5ErkJggg==\"],\"useMarkerImageFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', amount = percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"markerImageFunction\":\"var type = dsData[dsIndex]['type'];\\nif (type == 'thermomether') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"color\":\"#fe7569\",\"mapImageUrl\":\"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiCiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiCiAgIHhtbG5zOmlua3NjYXBlPSJodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy9uYW1lc3BhY2VzL2lua3NjYXBlIgogICB3aWR0aD0iMTEzNC41MTgzIgogICBoZWlnaHQ9Ijc2Mi43ODI0MSIKICAgaWQ9InN2ZzIiCiAgIHZlcnNpb249IjEuMSIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMC40OC41IHIxMDA0MCIKICAgc29kaXBvZGk6ZG9jbmFtZT0id2ljaGl0YW1hcC1ub2xpYi5zdmciPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM0IiAvPgogIDxzb2RpcG9kaTpuYW1lZHZpZXcKICAgICBpZD0iYmFzZSIKICAgICBwYWdlY29sb3I9IiNmZmZmZmYiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgYm9yZGVyb3BhY2l0eT0iMS4wIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTp6b29tPSIwLjM1IgogICAgIGlua3NjYXBlOmN4PSI4OS45MDc4NTciCiAgICAgaW5rc2NhcGU6Y3k9IjQ1My43ODI0MSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0icHgiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ibGF5ZXIxIgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEzNjYiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iNzIxIgogICAgIGlua3NjYXBlOndpbmRvdy14PSItNCIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iLTQiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgICBpbmtzY2FwZTpvYmplY3QtcGF0aHM9InRydWUiCiAgICAgaW5rc2NhcGU6c25hcC1nbG9iYWw9ImZhbHNlIgogICAgIHNob3dndWlkZXM9InRydWUiCiAgICAgaW5rc2NhcGU6Z3VpZGUtYmJveD0idHJ1ZSIKICAgICBmaXQtbWFyZ2luLXRvcD0iMCIKICAgICBmaXQtbWFyZ2luLWxlZnQ9IjAiCiAgICAgZml0LW1hcmdpbi1yaWdodD0iMCIKICAgICBmaXQtbWFyZ2luLWJvdHRvbT0iMCIgLz4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE3Ij4KICAgIDxyZGY6UkRGPgogICAgICA8Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+CiAgICAgICAgPGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+CiAgICAgICAgPGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPgogICAgICAgIDxkYzp0aXRsZT48L2RjOnRpdGxlPgogICAgICA8L2NjOldvcms+CiAgICA8L3JkZjpSREY+CiAgPC9tZXRhZGF0YT4KICA8ZwogICAgIGlua3NjYXBlOmxhYmVsPSJMYXllciAxIgogICAgIGlua3NjYXBlOmdyb3VwbW9kZT0ibGF5ZXIiCiAgICAgaWQ9ImxheWVyMSIKICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjcuMDcxNDI4LC0zMDcuOTAyOTkpIj4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDM3ODciCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMzY0ZTU5O3N0cm9rZS13aWR0aDoyLjk5OTk5OTc2O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmUiCiAgICAgICBkPSJtIDkwNi4wMzMxNSw3MDYuMTMzNjcgMy40MjkyLDE3Ljc5NTUyIE0gMjguNTcxNDI4LDc2NS4wNTA2NyBjIDE1MC40MzUyMDIsNi44MzM0MiAxNDYuMzkyMzIyLC0yNi4zMzQxNSAxNjYuNDM0NTQyLC0yOS4zMjAwOSAzNi4xNDM3NSwtNS4zODQ3NiAxMTQuMjg2NzYsLTYuNTI1NCAxNDguMzI1MDgsLTguNjIzNTQgNDMuMzc4MDgsLTIuNjczODUgMTQxLjc2MjIxLC0xMS4yMzA5OSAxODguODU1NzgsLTE5LjgzNDE4IDM5LjgxMTM4LC03LjI3Mjg0IDIyMS4zNjk5MSwtMC44NjIzNSAzMTkuMDcxNDEsLTAuODYyMzUgNzAuODI3MzUsMCAxNDYuOTE4NjcsLTEuNzI0NyAyMTguMTc1ODYsLTEuNzI0NyAtMzEuNjE5NywwIDExNy44NTUyLC0yLjU4NzA3IDg2LjIzNTUsLTIuNTg3MDcgbSAtMjUuMDkwNywtNjguMTI2MDYgYyAtNTIuNzk5NiwzNC43ODQ4NCAtNjUuODk1MSw1MS43NDg2NSAtOTUuNjM5LDgxLjQ5MjU4IC0yNC45MzEzLDI0LjkzMTI3IC0xNDAuMzk2NTMsLTE5LjEzOTIgLTE3OC45Mzg3MSwzNi42NTAwNyAtMTIuMjgxNCwxNy43NzcxNSAtNDcuMDAyNTcsNDYuNTQ2NTMgLTY1LjEwNzgzLDU5LjA3MTMzIC0yMC4xMDUsMTMuOTA4MTggLTU2LjAzNjcyLDQ0Ljk1NjY0IC02Ny43Njg4NSw3My4wNzgyNyAtNC44MDE0NywxMS41MDkwMiAtMTMuMzgwNDYsMzUuOTkyOTggLTIzLjQ0OTQ5LDQ2LjA2MjAxIC0xMC40OTY5OSwxMC40OTY5OSAtMzguMzc3MzMsNi4zODU2OSAtNDQuMDIzNDUsMTcuNjQ3NjQgLTE5LjAwNTAyLDM3LjkwODEyIC0yNS40NjUzLDEwMC45MjM1MiAtNjcuNjE3ODksMTAyLjA1MTAyIG0gMTkuMjgxNTEsLTYyNC4wMTQ2NCBjIDM0LjY1OTM0LC0xLjg3MzgyIDg0LjAyNzMzLDcuMzkxMzEgMTA5LjkwMDcxLC00LjI4NTQ1IDEzLjI4MTcyLC01Ljk5NDA4IDQxLjQwNzIxLC0yLjQ2MTM1IDY2LjgyODY2LC0yLjMyMDQ2IDM1LjMyMjM4LDAuMTk1NzggNjQuMzgyNDksMC42MzQ3NyAxMDEuOTE2Nyw1LjAyMzIgMjUuMDMwMzYsMi45MjY1IDQ0LjY2MjczLDM0LjI4NzIyIDU4LjUyNjk4LDUwLjY0MzkgMTcuMDk4NzgsMjAuMTcyNjggNjIuNzYzODYsLTEuNzE0NjcgNjYuMzA1NjYsMzIuMTM0MzMgNS4xMDI3LDQ4Ljc2NTg3IC02LjMyODQsNzguNjM3MjUgNi4xNDExLDk3LjM0MTUgMTkuOTY5MiwyOS45NTM3OSA1MC40ODY0LDE3Ljg1NTc5IDQ0LjYxOTMsODMuOTcxMTkgTSA1ODkuMTAyMjcsMzA5LjcyNzE1IGMgNC42NDM0NiwyMy43MjkyMyAxNS4wNjkwNCw3Mi43NzU3NSAxOS4wNjEyOCwxMzAuNjQyODggMC44NzIwNiwxMi42NDA0OCA1LjQ0NzE4LDI0Ljk5MjUzIDQuMjIyMzEsNDUuMjc3NTcgLTIuNTE3MjEsNDEuNjg3NSAtMTUuNzE3MDYsNDMuNjc3MjcgLTE1LjA5MTIyLDYwLjM2NDg2IDEuNDMxOTUsMzguMTgyMjQgMzAuNjEzNjEsOTMuODM3MTkgMzAuNjEzNjEsMTM5LjcwMTU0IDAsMjQuMTgwOCAtMi42Njk2NCwxMTUuMzkwNDUgNy4zMzAwMSwxMzUuMzg5NzYgMC4xNTkxMSwwLjMxODIxIDEwLjA2NDc2LDM1Ljg4MzMyIDEwLjc3OTQ1LDQ5LjE1NDI0IDAuOTQzNzgsMTcuNTI0NjkgLTI0LjQ3OCwzOS40NzAwOCAtMjguMDI2NTUsNDYuNTY3MTYgLTUuNDc3NywxMC45NTUzOSAtMzYuOTczMjQsMTAuODgxOTcgLTQwLjA5OTUsMjQuMTQ1OTUgLTMuODY4ODQsMTYuNDE0NTEgLTMuODY2Myw0My43OTczNSA0LjA0NjQ3LDU5LjQ0MTI5IG0gOTcuMzM3MzQsLTY5MS4wMDk0MSBjIC01LjAxMzMyLDM1LjUxNTk1IC00My42NTkwMSwxMS4zMTY1MiAtNTguNTM4NjEsMjMuNzgxMzEgLTIxLjMzMDE5LDE3Ljg2ODUyIC02Mi40OTk2NCwzMS40MzIxMiAtNzAuMTI0MzcsMzUuMzY3MDggLTM1LjA4NzYzLDE4LjEwNzkzIC0xMTAuNDcyMTUsLTE1LjE0MTk2IC0xMjUuNjE0MSw0LjI2ODQzIC0xNS45NTA2MywyMC40NDcwMyAtMC4wNzM1LDYxLjQ2NjQ4IC05LjE0NjY2LDg0LjE0OTI0IC02LjAzNTcsMTUuMDg5MjYgLTE4Ljg3NjcsMjMuMDE3MzQgLTI3LjQzOTk3LDMyLjkyNzk4IC0xOS43NDgyOSwyMi44NTU1NSAtNjkuOTc0MjgsNjkuODI0MTkgLTg0Ljc1OTA0LDEwMC4wMDM0NiAtNy40OTc0MSwxNS4zMDQwNCAtMy4yODQyNiw0NC40MjA0MSAtMy40NzA1Myw2My4zNDI4NCAtMC4xMjc5MywxMi45OTQxNCAtMC44MTAxNSwyMy4xMDM4NSAyLjQwMzQzLDI4LjI3NjE4IDQuOTYxNTgsNy45ODU4MSAyMy43MjA1LDI4LjExMjA3IDI0LjIzODY1LDUwLjYxMTQ5IDAuMjk0MTEsMTIuNzcxNDYgMC4wMTMzLDc4LjU5MTAxIDMuMDQ4ODgsODcuNjU1NDkgMi4zMTI1Niw2LjkwNTQ2IDQuMjIwMDQsMjYuNTY0OTcgMTAuMjEzNzcsMzYuNTg2NjIgMTEuMzU0MDEsMTguOTg0MTUgNC4zODczNyw0MC4xNTY2MiAyNy44OTczLDUzLjUwNzk1IDE5LjA1MDEyLDEwLjgxODU5IDQ2Ljg3NzgxLDEyLjIxODYyIDgxLjkyNjE4LDE0LjQ2MDU0IDMzLjcwMzQ1LDIuMTU1ODkgNjEuNTEyMTcsLTEuNDMwMzUgNzYuOTIwNzcsNi4xNDExIDExLjU4NTA4LDUuNjkyNjYgOC41ODE1MSwxNy45MzM0NCAxNC4yOTU0MSwyOS4zNjEyMyA1LjY0MDQyLDExLjI4MDg1IDMxLjUwMjYzLDExLjE1NjI3IDQxLjgwNDA5LDQzLjQ1NDg3IDcuNjA1OSwyMy44NDcxIDMuMDg1OTMsNDQuMTU2OSA2LjcwNzU1LDY1Ljg4NjYiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjY2Nzc3NzY2Njc3Nzc3NzY2Nzc3Nzc3NjY3Nzc3Njc3NzY2Nzc3Nzc3Nzc3Nzc3Nzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Im0gNDMuMjc3ODgxLDUxNy45NDY3OSBjIDAsMCAyMzAuODQ4Mjg5LC0zLjYzODA1IDI1MC4wMDg2MzksLTMuNjU4NjcgNy40ODIyMiwtMC4wMDggOC42MTk1NCw1LjE1MTk0IDE0LjAyMDksMTEuNDU4NjkgMjQuNTk2MDgsMjguNzE4OTMgOTMuOTA5NjYsMTEyLjkzNTg1IDkzLjkwOTY2LDExMi45MzU4NSIKICAgICAgIGlkPSJwYXRoMzc4OSIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJtIDM1Ljk2MDU1NSw1NzcuNzA0OTQgYyAwLDAgMTY1LjUyNDU2NSwtMS42ODQ1NCAyNDguNzc5NTY1LC0xLjY4NDU0IDQuOTQ3NDksMCA3LjcyOTkzLC0yLjg4MzMgMTAuNTM3NzEsLTUuNzI5NzcgOS42NjEwNywtOS43OTQxNiAyNS42MzE5OSwtMjguNTg5OTUgMjUuNjMxOTksLTI4LjU4OTk1IgogICAgICAgaWQ9InBhdGgzNzkxIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOiMzMzMzNjY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gMzguMzk5NjYzLDY0MS43MzE1NSA0MzEuNzA1OTMsNjM3LjQ2MzExIgogICAgICAgaWQ9InBhdGgzNzk1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOiMzMzMzNjY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gMzkuMDA5NDQyLDcwNC41Mzg1OSA1MjMuMTcyNTMsNjk3LjgzMTA0IgogICAgICAgaWQ9InBhdGgzNzk3IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzAzLjk1NzYyLDY4Mi41ODY2MSAxNDYuNzk1NDIsMS44MjkzMyBjIDEwLjUzNDAzLDAuMTMxMjcgMTQuMzQzNzQsLTIuNjM3MzkgMjUuNDg3MTUsLTYuMzcyOCAxMC40MTIxMiwtMy40OTAyNyAzMS40MjQxNSwtMi42OTg5NiA0MS4zODUzOCwtMi43NzM4NSBsIDQwNS41NjA3OSwtMy4wNDg5IgogICAgICAgaWQ9InBhdGgzNzk5IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgaWQ9InBhdGgzODA0IgogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDQyNi4yMTc5NCwzMTQuODkwOTggYyAyLjA2NzU0LDkuMDUyNzMgMS44NDE3Nyw1MS43Mjc3NyA2LjUwNzk0LDc0LjgzNDY2IDEuNjc0NzUsOC4yOTMzNiA4LjY3NTA4LDE0LjA2NTk4IDEwLjA1NTQxLDE0Ljg1ODYyIDQuOTAxNDcsMi44MTQ2MyAxMC44MTQ3OSw4LjE0OTgyIDEzLjA0NTc5LDE2LjA4ODMxIDYuNzU3NzksMjQuMDQ1OTEgMC44Nzk3Miw2OC40NTIxMiAwLjg3OTcyLDExMC42ODkzIDAsNi4wOTc4MiAxLjY2MDEsMzAuMTQ2NiAtMi4xNTU4OCwzMy45NjI1OSAtMi41NDA4NSwyLjU0MDgzIC0wLjI4MTYzLDEyLjk5MDY5IC0zLjQzNjc1LDE2LjE0Mzc3IGwgLTkuODQ5NDQsOS44NDMxMSBjIC0xMC4zNjcxNSwxMC4zNjA0NyAtMTEuNTkwMTcsNi41MjYxNCAtMTcuNzM4NDgsMTguODIyNzYgLTMuNTY3NzIsNy4xMzU0MyA1LjQwMjM1LDIwLjY3MjEgNy4zNTQzMiwyNC41NzYwMiAxLjkzMjE0LDMuODY0MyAtMS44NDIxNiw0Ljc3NzczIC0xLjc5MjM1LDcuNDQ2MjYgMC4yNTI4NiwxMy41NDQ4MyAyLjI5NzUsMzczLjkyNzEyIDIuMjk3NSwzNzMuOTI3MTIiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDM2NS4yNDAyMiw1MTkuNzc2MTIgNC4xMTU5OSw1MDIuMTUxNTgiCiAgICAgICBpZD0icGF0aDM4MDYiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAxMTYuNTMxNjUsNTA0LjE4Njk5IDMuODgwNTksMzEwLjk2NDM2IgogICAgICAgaWQ9InBhdGgzODMxIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDM4ODkiCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzE3LjY3NzYsNTc2LjQ4NTM5IDEzMC4xODc0MiwxLjUyNDQ0IGMgNC41MTA3OSwzLjI0MTY5IDIwLjM0NDcxLDcuOTY4NTMgMjcuNzQ0ODYsNC4yNjg0NCAzLjE1NTQ2LC0xLjU3NzcyIDkuNDE5LC01LjM4ODE3IDE0LjAyNDg5LC0zLjk2MzU1IDQuMjY2OTgsMS4zMTk4MSA2LjAxNjg5LDMuMTE2MzIgMTAuMzY2MjEsMy4wNDg4OSAxMC4zMDQwMywtMC4xNTk3NSAyMC4yMTE3LDAuMzg3NDEgMzAuNDg4ODYsMC4zMDQ4OSAxNzcuODkwOCwtMS40MjgyNyAzNTYuNTkwMzUsLTIuMTMyNDcgNTM0Ljc3NDU2LC0zLjA0ODg4IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2Nzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDc1LjMwNTAxLDU4Mi44ODgwNSBjIC0zLjQ0NDE4LDExLjM1MDY2IC0yLjEwMzQzLDEyLjQzMzczIDMuNjU4NjUsMjEuMDM3MzEgMy43OTQ0NSw1LjY2NTY0IDUwLjg2MjYxLDEzLjAzODQ1IDQxLjQ2NDg1LDI3LjEzNTA5IC0xMC41MzY5NywxNS44MDU0NyAtMjIuODk3NDUsLTUuNDc3NzIgLTMzLjg0MjYzLC0xLjgyOTMzIC01LjQ1MjM2LDEuODE3NDUgLTcuMzQ5MDEsNS40NTYzMSAtMy42NTg2Niw5LjE0NjY1IDIuODA2ODMsMi44MDY4NCA0LjA0OCwxLjgwMzk2IDYuNTIwMzQsNS4xMDA0MSIKICAgICAgIGlkPSJwYXRoMzkxMCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDMyLjAxMDgyLDYzNi44NTMzMyBjIDguMzE4OTksMTMuMTEwMTYgMTguODQ2MjEsMTQuNjM0NjUgMzUuNjcxOTYsMTQuNjM0NjUgMi45Mzg2NSwwIDcuODY5OTgsLTAuOTMzNzEgMTAuNjcxMTEsMCAxMS4zNTkxNywzLjc4NjM5IDI3LjE5Mzk4LDEwLjI3NTc3IDM2LjIwMTkzLDIxLjEyOTQ4IDguMjgwMDIsOS45NzY2MSAxMC4yNTI3OCwyMy44ODMwOCA3LjcwMjAyLDM3LjEwNDI0IC02LjE2OTg5LDMxLjk3OTk4IC0xNi43MTQzMSw1Ni45ODg1MyAtMTkuMDQzNTUsODYuNTY5MDUgLTEuMzQ3OTgsMTcuMTE4OCA0LjUwOTU3LDIyLjUzNTIyIDExLjA3MTQzLDMzLjkyODU3IDEwLjY3MDIzLDE4LjUyNjcyIDguNzI0NTMsMTQuMTk5NTUgOC41NzE0MywzNC4yODU3MiAtMC4xMzk2MywxOC4zMTk0NCAwLDYwLjI2Mzg1IDAsODAuNzE0MjkiCiAgICAgICBpZD0icGF0aDM5MTIiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDUyOC41MDgwNiw2NTguOTU3NzYgYyAtMTAuNjgxMjMsMC45MDQ1NCAtNy4xMDgwNCwtNS42MDI1NSAtMTAuODIzNTQsLTguMDc5NTYgLTQuNzg0NTQsLTMuMTg5NjkgLTEyLjIyNzA0LC0xLjI1MTA0IC0xNi43Njg4OCwtNS43OTI4OCAtMC42NjYxMiwtMC42NjYxMiAtOC44MDk2OSwtNC4xMDg3NyAtMTAuMTc0NDcsLTIuNzQzOTkgLTguMzY0NTksOC4zNjQ1OSAtMy4wNDg4OCwyMC41NTE4OCAtMy4wNDg4OCwzMy41Mzc3NCBsIDMuMDIyLDMzOS42OTc0MyIKICAgICAgIGlkPSJwYXRoMzkxNCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA1MTcuOTg5NDEsNjUxLjAzMDY1IGMgLTAuMjIxNzEsLTIuNzAxODQgMS45MDM0NiwtNS41NjIxMyAzLjM1Mzc3LC03LjAxMjQ1IDEuNzk5NDMsLTEuNzk5NDIgNi45MjI5NCwxLjAwNDE5IDguODQxNzgsLTAuOTE0NjYgMC4yODc2NSwtMC4yODc2NiAwLjg0MzI5LC0xMS4xNjQxIDAuMjI4NjYsLTEzLjU2NzUzIC0yLjA2NDgzLC04LjA3NDE2IC0yLjA1ODAxLC0yOC42NTY1OCAtMi4wNTgwMSwtMzguNzIwODYgbCAwLC03My4xNzMyNiIKICAgICAgIGlkPSJwYXRoMzkxNiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNTI4LjY2MDUsNjc1LjQyMTczIC0wLjQ1NzMzLC0zMS41NTU5NiIKICAgICAgIGlkPSJwYXRoMzk3NCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDc2Ni4zMTYyNSw1NzkuNjQ0MzEgMC40MzExOCwxMy43OTc2OCBjIDMuMTM2NDMsNC42NjkxNSAzLjAxODI0LDkuNjAwNjggMy4wMTgyNCwxNi4zODQ3NSBsIDAsMTU3LjM3OTgxIgogICAgICAgaWQ9InBhdGgzOTgyIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMTEyMi45MDAxLDc2NS45MTMwMyBjIC0yMDIuMzA2NjksNC42OTA1IC00MDMuNzQ0MDUsLTEuMTEzODEgLTYwNS45NTQ1NCwzLjM1MzkgLTEwLjg2MzYyLDAuMjQwMDIgLTMuMzYxNDcsLTguNTg2MyAtMjguNTM2OCwtOC41ODYzIgogICAgICAgaWQ9InBhdGgzOTg0IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA4NjAuMDA4MDUsNzM3LjA2NjUxIGMgMCwwIC05Ny40NDc1LDAuODU4MDYgLTE0Ny41Njg5MiwwLjg1ODA2IC01LjI2ODYxLDAgLTQuNTE1NDYsLTguMzI5ODYgLTcuMzAwODksLTguMzI5ODYgLTMuOTc0MzUsMCAtOC42MjkyNSwwLjAyMDEgLTEwLjUwOTQ4LDAuMDM1OSAtMi4zMzQ3NywwLjAxOTcgLTEuODEwOTQsOC4zNjU5NyAtNC4xNDU4LDguMzY2OTIgLTQ2LjE2ODk5LDAuMDE4OCAtMTY3LjQwNzY3LC0xLjMwNzk5IC0xNzUuMDUyNjMsLTEuMzA3OTkgLTQuNDI5NTUsMCAtOC41NzYyNywtNi40Mzk3MiAtMTMuMTMxOTgsLTYuNDM5NzIgLTEuMzYxMTUsMCAtNi4yMzg3MywwIC0xNC4zOTQ2NywwIgogICAgICAgaWQ9InBhdGgzOTg2IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJNIDY3NS4wMDcwMyw4MzEuMTc0MDIgNjc0LjM5NzI1LDMwOS40MDI5OSIKICAgICAgIGlkPSJwYXRoMzk4OCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDc5OS40MDE1NywzMTMuMDYxNjUgMS4yMTk1NSw0OTUuODY2NTMiCiAgICAgICBpZD0icGF0aDM5OTAiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA3MzYuNTk0NTIsMzEyLjQ1MTg4IC0xLjIxOTU1LDcxNi40ODgyMiIKICAgICAgIGlkPSJwYXRoMzk5MiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDUzMC4wMzA5NCw2NDMuNDU4NTkgMzkyLjM3MTU5LC0zLjAxODI1IgogICAgICAgaWQ9InBhdGg0MDQ4IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gODU5LjQ1MDYsMzE0LjkwMTI4IDEuMjkzNTQsNTA3Ljk4MDU4IgogICAgICAgaWQ9InBhdGg0MDUwIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjAuOTk5OTk5OTRweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gOTIxLjU0MDE3LDMxMC41ODk0OSAxLjcyNDcxLDUzMS43NTIyNyIKICAgICAgIGlkPSJwYXRoNDA1MiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDczNi4yODk2Myw0NTMuMzEwNCAxODUuNjc3MTUsLTAuMzA0ODkiCiAgICAgICBpZD0icGF0aDQxODciCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAxMDYwLjgxMDUsNTE0Ljk2NzY3IGMgMCwwIC0zNjMuMjgxMjYsLTUuNjI2MTggLTU0NC42NTA0MiwyLjUyMTc4IC00LjE3Nzc2LDAuMTg3NjkgLTEyLjUwMDQ0LDEuMDY3MTEgLTEyLjUwMDQ0LDEuMDY3MTEgLTEuNTcwOTUsMC4xMzQxIC0yLjAwMDkzLC0yLjMyNDk1IC0yLjU5MTU1LC0zLjUwNjIzIC0wLjA5NjcsLTAuMTkzNDMgLTcuMDYwODEsLTEuOTMzNCAtNy42MjIyMSwtMS4zNzE5OSAtMi44OTMxNCwyLjg5MzE0IC03LjYzMTY3LDQuMjQ4NjkgLTEyLjE5NTU1LDQuMTE2IEwgMzY5LjIwMTcsNTE0LjUzNjUiCiAgICAgICBpZD0icGF0aDQyNjEiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAzOTkuODE1MzEsNDc5LjYxMTEyIDExLjY0MTgsNS42MDUzIGMgMi45ODQxMiwxLjQzNjc5IDYuNTI4NzgsLTAuNDc3MTIgOS45MTcwOCwtMC40MzExOCBsIDEyNy4xOTczOSwxLjcyNDcxIgogICAgICAgaWQ9InBhdGg0MjYzIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gNTE5LjI1MTUxLDUxNy4xMjM1NyA1MTguODIwMzIsMzA4LjQzMzYyIgogICAgICAgaWQ9InBhdGg0MjY1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDMyLjkyNTQ5LDM4OS43MTQ5OCBjIDExLjA0NDk2LDAgMzUuNTMzMDcsMC42MTkyNyA0Mi41Nzk3OCwtMS4wMDM5NyA4LjQwNTIyLC0xLjkzNjE4IDcuMDY2LC02Ljk1Mzc4IDE0LjE5NzEyLC02Ljk1Mzc4IDcuODA5NSwwIDYuNTQyOTEsOC4wNjIzNyAyMC4xNDE3LDguMDYyMzcgMTMuOTkwNjgsMCA0NC45NzY4OSwwLjM3ODg2IDYzLjkzOTkyLDAuMzc4ODYgMTIuMDgzOTUsMCA4Mi4wMDI2NiwwLjMwNDg5IDkzLjYwMDgxLDAuMzA0ODkgOC43NjA0NywwIDEzLjE1OTcsLTIuMjg4MjcgMjEuMzQyMTksLTcuMDEyNDMgNy4xOTUxNSwtNC4xNTQxMyAyLjA1NDU5LC05LjQ5MTM3IDIwLjQyNzU0LC04Ljg0MTc3IDIzLjE0NTQsMC44MTgzMyAxMi42NDMzNCwxNC4wMjQ4NyAzMi4zMTgxOSwxNC4wMjQ4NyAyNS4zNTk1NCwwIDEzMC45OTkwMiwwIDE1MC45MTk4NSwwIDE0LjMzMjQ0LDAgLTQuMTE5MTEsLTEzLjExMDIxIDI5LjI2OTMsLTEzLjQxNTEiCiAgICAgICBpZD0icGF0aDQyNjkiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc3NzYyIgLz4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjU4OC42Nzk1NyIKICAgICAgIHk9IjczNS44MDQ2MyIKICAgICAgIGlkPSJ0ZXh0NDMxMCIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMiIKICAgICAgICAgeD0iNTg4LjY3OTU3IgogICAgICAgICB5PSI3MzUuODA0NjMiPkxpbmNvbG48L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjY4Ni4zOTg1IgogICAgICAgeT0iNzY1LjYyODQyIgogICAgICAgaWQ9InRleHQ0MzEwLTciCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNiIKICAgICAgICAgeD0iNjg2LjM5ODUiCiAgICAgICAgIHk9Ijc2NS42Mjg0MiI+SGFycnk8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjcwOS44NzE4MyIKICAgICAgIHk9Ii04MDIuMzc3MzgiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMi02LTgiCiAgICAgICAgIHg9IjcwOS44NzE4MyIKICAgICAgICAgeT0iLTgwMi4zNzczOCI+V29vZGxhd248L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjU2Mi4xMTkyNiIKICAgICAgIHk9Ii03NzEuOTY4MTQiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xLTkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtOC0yIgogICAgICAgICB4PSI1NjIuMTE5MjYiCiAgICAgICAgIHk9Ii03NzEuOTY4MTQiPkVkZ2Vtb29yPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTguMzA0ODciCiAgICAgICB5PSItNzM4LjM2NjQ2IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTciCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtOC0yLTkiCiAgICAgICAgIHg9IjU5OC4zMDQ4NyIKICAgICAgICAgeT0iLTczOC4zNjY0NiI+T2xpdmVyPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTIuMTIyODYiCiAgICAgICB5PSItNjc3LjIwMzk4IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTctNSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNi04LTItOS00IgogICAgICAgICB4PSI1OTIuMTIyODYiCiAgICAgICAgIHk9Ii02NzcuMjAzOTgiPkhpbGxzaWRlPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTcuMzI3MDkiCiAgICAgICB5PSItODYyLjYxNDA3IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTctNS0zIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMi02LTgtMi05LTQtMSIKICAgICAgICAgeD0iNTk3LjMyNzA5IgogICAgICAgICB5PSItODYyLjYxNDA3Ij5Sb2NrPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1ODcuMzcwMTgiCiAgICAgICB5PSItOTI2LjEzNjYiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xLTktNy01LTMtMiIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNi04LTItOS00LTEtMyIKICAgICAgICAgeD0iNTg3LjM3MDE4IgogICAgICAgICB5PSItOTI2LjEzNjYiPldlYmI8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9Ijg3MS4xNjEwMSIKICAgICAgIHk9IjYzNy41NzUyIgogICAgICAgaWQ9InRleHQ0NDY1IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NDY3IgogICAgICAgICB4PSI4NzEuMTYxMDEiCiAgICAgICAgIHk9IjYzNy41NzUyIj5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI4NzMuODMyMjgiCiAgICAgICB5PSI1NzcuMDMyNDciCiAgICAgICBpZD0idGV4dDQ0NjUtMyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDQ2Ny00IgogICAgICAgICB4PSI4NzMuODMyMjgiCiAgICAgICAgIHk9IjU3Ny4wMzI0NyI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgaWQ9InRleHQ0NDkwIgogICAgICAgeT0iNTEwLjI2MTgxIgogICAgICAgeD0iODc1Ljk2NjQ5IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbgogICAgICAgICB5PSI1MTAuMjYxODEiCiAgICAgICAgIHg9Ijg3NS45NjY0OSIKICAgICAgICAgaWQ9InRzcGFuNDQ5MiIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+MjFzdDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iODgxLjMxNjU5IgogICAgICAgeT0iNDUwLjE5ODc2IgogICAgICAgaWQ9InRleHQ0NDk0IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NDk2IgogICAgICAgICB4PSI4ODEuMzE2NTkiCiAgICAgICAgIHk9IjQ1MC4xOTg3NiI+Mjl0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNjE1Ljc5MjQ4IgogICAgICAgeT0iMzg3Ljc0NzE2IgogICAgICAgaWQ9InRleHQ0NDY1LTMtMSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDQ2Ny00LTEiCiAgICAgICAgIHg9IjYxNS43OTI0OCIKICAgICAgICAgeT0iMzg3Ljc0NzE2Ij4zN3RoPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ1MTkiCiAgICAgICB5PSI0ODEuNjUyODYiCiAgICAgICB4PSI0ODQuNjkwMzciCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9IjQ4MS42NTI4NiIKICAgICAgICAgeD0iNDg0LjY5MDM3IgogICAgICAgICBpZD0idHNwYW40NTIxIgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIj4yNXRoPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1NjMuMDQ2NzUiCiAgICAgICB5PSI1MTMuMzYxMzMiCiAgICAgICBpZD0idGV4dDQ1MjMiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1MjUiCiAgICAgICAgIHg9IjU2My4wNDY3NSIKICAgICAgICAgeT0iNTEzLjM2MTMzIj4yMXN0PC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ1MjciCiAgICAgICB5PSI1NzcuODk0ODQiCiAgICAgICB4PSI1NjUuOTcxNSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNTc3Ljg5NDg0IgogICAgICAgICB4PSI1NjUuOTcxNSIKICAgICAgICAgaWQ9InRzcGFuNDUyOSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDUzMSIKICAgICAgIHk9Ii00NjAuNzMzMTIiCiAgICAgICB4PSI0MzMuNTgwNzUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii00NjAuNzMzMTIiCiAgICAgICAgIHg9IjQzMy41ODA3NSIKICAgICAgICAgaWQ9InRzcGFuNDUzMyIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+QW1pZG9uPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI0MDUuNTMwOTgiCiAgICAgICB5PSItNTIzLjU0MDE2IgogICAgICAgaWQ9InRleHQ0NTM1IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDUzNyIKICAgICAgICAgeD0iNDA1LjUzMDk4IgogICAgICAgICB5PSItNTIzLjU0MDE2Ij5BcmthbnNhczwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDUzOSIKICAgICAgIHk9Ii0zNzIuNTg1OTQiCiAgICAgICB4PSI3NDUuNDg0NjIiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii0zNzIuNTg1OTQiCiAgICAgICAgIHg9Ijc0NS40ODQ2MiIKICAgICAgICAgaWQ9InRzcGFuNDU0MSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+V2VzdDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNTk2LjcyODMzIgogICAgICAgeT0iLTUzMS4yNTkyOCIKICAgICAgIGlkPSJ0ZXh0NDU0MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NDUiCiAgICAgICAgIHg9IjU5Ni43MjgzMyIKICAgICAgICAgeT0iLTUzMS4yNTkyOCI+V2FjbzwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU1NSIKICAgICAgIHk9Ii0xMjIuNTAyOTUiCiAgICAgICB4PSI1OTUuNDM0ODEiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii0xMjIuNTAyOTUiCiAgICAgICAgIHg9IjU5NS40MzQ4MSIKICAgICAgICAgaWQ9InRzcGFuNDU1NyIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+TWF6aWU8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjY5NS43NzI5NSIKICAgICAgIHk9IjE2Mi4wNjg3NyIKICAgICAgIGlkPSJ0ZXh0NDU1OSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMC43MDcxMDY3OCwwLjcwNzEwNjc4LC0wLjcwNzEwNjc4LDAuNzA3MTA2NzgsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NjEiCiAgICAgICAgIHg9IjY5NS43NzI5NSIKICAgICAgICAgeT0iMTYyLjA2ODc3Ij5ab288L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjI0MC41ODk5NyIKICAgICAgIHk9IjU3NC40NDU0MyIKICAgICAgIGlkPSJ0ZXh0NDU2MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDU2NSIKICAgICAgICAgeD0iMjQwLjU4OTk3IgogICAgICAgICB5PSI1NzQuNDQ1NDMiPjEzdGg8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU2NyIKICAgICAgIHk9IjUxMS42MzY2MyIKICAgICAgIHg9IjIwNi4wMzE3NSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNTExLjYzNjYzIgogICAgICAgICB4PSIyMDYuMDMxNzUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NjkiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPjIxc3Q8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjYyMC40NDMxMiIKICAgICAgIHk9Ii01MDYuNjgyMTkiCiAgICAgICBpZD0idGV4dDQ1NzEiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NTczIgogICAgICAgICB4PSI2MjAuNDQzMTIiCiAgICAgICAgIHk9Ii01MDYuNjgyMTkiPk5pbXM8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU4MyIKICAgICAgIHk9IjY5OC44NDAwOSIKICAgICAgIHg9IjM3MC4yMTY4NiIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNjk4Ljg0MDA5IgogICAgICAgICB4PSIzNzAuMjE2ODYiCiAgICAgICAgIGlkPSJ0c3BhbjQ1ODUiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1hcGxlPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSIzODQuMDg0MiIKICAgICAgIHk9IjY4MC44NTEzOCIKICAgICAgIGlkPSJ0ZXh0NDU5OSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDYwMSIKICAgICAgICAgeD0iMzg0LjA4NDIiCiAgICAgICAgIHk9IjY4MC44NTEzOCI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAzNjcuOTA4MTcsMTAwOS45NTk2IDI2My4wMTgzMywwIgogICAgICAgaWQ9InBhdGg0NjA1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDx0ZXh0CiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ2MDciCiAgICAgICB5PSItNDMzLjEzNzc2IgogICAgICAgeD0iNzM2LjI2NzQ2IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbgogICAgICAgICB5PSItNDMzLjEzNzc2IgogICAgICAgICB4PSI3MzYuMjY3NDYiCiAgICAgICAgIGlkPSJ0c3BhbjQ2MDkiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1lcmlkaWFuPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ5NzkiCiAgICAgICB5PSI2NDAuMjA1MjYiCiAgICAgICB4PSI1NzIuODMyMTUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9IjY0MC4yMDUyNiIKICAgICAgICAgeD0iNTcyLjgzMjE1IgogICAgICAgICBpZD0idHNwYW40OTgxIgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIj5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1NzUuMDg5NjYiCiAgICAgICB5PSI2NzAuOTAzNSIKICAgICAgIGlkPSJ0ZXh0NDk4MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDk4NSIKICAgICAgICAgeD0iNTc1LjA4OTY2IgogICAgICAgICB5PSI2NzAuOTAzNSI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNDk5LjQ4OTYyIgogICAgICAgeT0iMTAwOC42MDY5IgogICAgICAgaWQ9InRleHQ1MDQ3IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDQ5IgogICAgICAgICB4PSI0OTkuNDg5NjIiCiAgICAgICAgIHk9IjEwMDguNjA2OSI+NDd0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iMjE2LjY0NTQzIgogICAgICAgeT0iNzI1Ljk4Mjk3IgogICAgICAgaWQ9InRleHQ1MDUxIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDUzIgogICAgICAgICB4PSIyMTYuNjQ1NDMiCiAgICAgICAgIHk9IjcyNS45ODI5NyI+S2VsbG9nZzwvdHNwYW4+PC90ZXh0PgogICAgPGZsb3dSb290CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgaWQ9ImZsb3dSb290NTA1NSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6MThweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwyODcuMzYyMTgpIj48Zmxvd1JlZ2lvbgogICAgICAgICBpZD0iZmxvd1JlZ2lvbjUwNTciPjxyZWN0CiAgICAgICAgICAgaWQ9InJlY3Q1MDU5IgogICAgICAgICAgIHdpZHRoPSIzNDMuNTcxNDQiCiAgICAgICAgICAgaGVpZ2h0PSIxMDMuNTcxNDMiCiAgICAgICAgICAgeD0iMTkuMjg1NzE1IgogICAgICAgICAgIHk9IjE3LjE0Mjg1NyIKICAgICAgICAgICBzdHlsZT0iZm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIgLz48L2Zsb3dSZWdpb24+PGZsb3dQYXJhCiAgICAgICAgIGlkPSJmbG93UGFyYTUwNjEiPjwvZmxvd1BhcmE+PC9mbG93Um9vdD4gICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDYwNy03IgogICAgICAgeT0iLTUwOC4xODk3MyIKICAgICAgIHg9Ijc3NC44NzU2MSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iLTUwOC4xODk3MyIKICAgICAgICAgeD0iNzc0Ljg3NTYxIgogICAgICAgICBpZD0idHNwYW40NjA5LTciCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1jQ2xlYW48L3RzcGFuPjwvdGV4dD4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzY0LjE1OTk5LDY1OC40Mjg5MSAyOTkuNTEwMjMsLTEuMDEwMTYgYyA2LjQ5ODcyLC0wLjAyMTkgNi45NzcxOSw5LjI1NDEyIDE2LjU5NjMxLDkuMzkyNDcgMTIuMDU0MjcsMC4xNzMzOSAyOS4xMTA4MywtMC41MzU3MiA1NC4xMTQzNywtMC4zMDExIgogICAgICAgaWQ9InBhdGg1NDQwIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjg3LjM2MjE4KSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjM3My45OTMwNCIKICAgICAgIHk9Ijk0NC4zNTc1NCIKICAgICAgIGlkPSJ0ZXh0NTA0Ny05IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDQ5LTMiCiAgICAgICAgIHg9IjM3My45OTMwNCIKICAgICAgICAgeT0iOTQ0LjM1NzU0Ij5NYWNBcnRodXI8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ2MDctNy0xIgogICAgICAgeT0iLTQ5MC4yNDU5NyIKICAgICAgIHg9Ijc4MC44NDYwNyIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iLTQ5MC4yNDU5NyIKICAgICAgICAgeD0iNzgwLjg0NjA3IgogICAgICAgICBpZD0idHNwYW40NjA5LTctOSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+U2VuZWNhPC90c3Bhbj48L3RleHQ+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDM2Ny42OTU1Myw1MzcuMjEwNiAxNDEuMjgzMDMsLTEuMDEwMTUgYyA2LjQ4OTk5LC0wLjA0NjQgMTIuNzgxMTQsNy4yMzU0NSAxOS4xOTI5LDcuMzIzNiA1NS45MjM2MiwwLjc2ODkgMTU4LjY4OTk3LC0wLjE3MzMzIDIzNi41MTQwMiwtMS4wMTAxNSA3LjgzOTU2LC0wLjA4NDMgMjIuNjMxNDcsLTE5Ljg1MzU1IDMwLjMwNDU3LC0yMC40NTU1OSAyMi4yNjU4OSwtMS4zNTE4MSA0NS4xNzk0NSwtMC41MDUwNyA2Ny42ODAyMiwtMC41MDUwNyAxNi4xNDczMSwtMC42MzI0MSAzLjYxMDE2LDIwLjcwODEzIDI2Ljc2OTA0LDIwLjcwODEzIGwgMjQzLjQ0Njc5LC0xLjAxMDE2IgogICAgICAgaWQ9InBhdGg1NDk2IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjg3LjM2MjE4KSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzY2NjY2MiIC8+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI2ODUuMjA4MTMiCiAgICAgICB5PSI4MjcuNTMwODIiCiAgICAgICBpZD0idGV4dDQzMTAtNy04IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtNiIKICAgICAgICAgeD0iNjg1LjIwODEzIgogICAgICAgICB5PSI4MjcuNTMwODIiPlBhd25lZTwvdHNwYW4+PC90ZXh0PgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0iTSA1NTQuMjg1NzIsNzIxLjQyODU3IDU1MCw1NDMuMjE0MjkgNTQ3LjE0Mjg2LDEwMi41IDU0Ni43ODU3MiwyMy4yMTQyODUiCiAgICAgICBpZD0icGF0aDU1MTkiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwyODcuMzYyMTgpIiAvPgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNTI5LjYyNTMxIgogICAgICAgeT0iLTU1MC44NDc3OCIKICAgICAgIGlkPSJ0ZXh0NDU0My01IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDU0NS0wIgogICAgICAgICB4PSI1MjkuNjI1MzEiCiAgICAgICAgIHk9Ii01NTAuODQ3NzgiPkJyb2Fkd2F5PC90c3Bhbj48L3RleHQ+CiAgPC9nPgo8L3N2Zz4K\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"showTooltip\":true,\"autocloseTooltip\":true,\"labelFunction\":\"var deviceType = dsData[dsIndex]['deviceType'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}, ${energy:2} kWt';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}, ${temperature:2} °C';\\r\\n }\\r\\n}\",\"tooltipFunction\":\"var deviceType = dsData[dsIndex]['deviceType'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}
Energy: ${energy:2} kWt
';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}
Temperature: ${temperature:2} °C
';\\r\\n }\\r\\n}\",\"provider\":\"image-map\",\"showTooltipAction\":\"click\"},\"title\":\"Image Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" } }, { - "alias": "tencent_maps", - "name": "Tencent Maps", + "alias": "google_maps", + "name": "Google Maps", "descriptor": { "type": "latest", - "sizeX": 9, + "sizeX": 8.5, "sizeY": 6, "resources": [], "templateHtml": "", "templateCss": ".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 200px;\n white-space: nowrap;\n}", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('tencent-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('tencent-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('tencent-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('google-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('google-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('google-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.24727730589425012,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.8437014651129422,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"type\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.7558240907832925,\"funcBody\":\"return \\\"colorpin\\\";\"}]},{\"type\":\"function\",\"name\":\"Second Point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.19266205227372524,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.7995830793603149,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.04902495467943502,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.44120841439482095,\"funcBody\":\"return \\\"thermomether\\\";\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"tmDefaultMapType\":\"roadmap\",\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"showTooltip\":true,\"autocloseTooltip\":true,\"tooltipPattern\":\"
${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details
\",\"markerImageSize\":34,\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"color\":\"#fe7569\",\"useColorFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', amount = percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageFunction\":\"var type = dsData[dsIndex]['type'];\\nif (type == 'thermomether') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"markerImages\":[\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAwgSURBVGiB7Zt5cBT3lce/v18fc89oRoPEIRBCHIUxp2ywCAgIxLExvoidZIFNxXE2VXHirIO3aqtSseM43qpNeZfYKecox3bhpJykYgdjDkU2mBAB5vCamMNYAgQyURBCoxnNPd39O/aP7hGSEUR24L/uqqf+zfR77/Pe69/Rv6kWwcgPLRIJfZUAa7xez2xd90QBwDSNZKlkHJHAK+l09mUA7BP4vPpRUVExMVoRef+L998njxx9X57vPi/PnTsnO850yPaT7XLXrrflqjtWymhF+HA0Gp0wEp/kHymEQqG4ptJDGzf+um5RUxMSiV7Z3Lyt88L5nozgHJWj4pGmpqZav99PWve04onHHuswmViQzWb7ruZX+Udgv8/z3A+f/NGye1evxssvb+wo5PMfTZs6bfqcuXNHL7hlweh58+ZVAOTUpk2b0p9dvjyqqmrs/b8ejpUMc+unzjgUCsXjsYruE+2n1JY/NedM0zCi0VjA7/d7/f4AAgE//H4/vF4fOjvP9h5695C/oaEhcN/q1SyTzVdnMpnklXzTq4EplUsXfmaRCgC7du3cOn78+KfGj59Add3z1Md1vV7vqPa2D1sA4MYbZ6qUiqVX9X21i4TQcfX19QCA6urquN/vn0kAPRQKpYbTnzRpUhgAampqAEFrPjVYSql7fD4AgK5r2tV0AcDj8WkAoOk6JJGeTw2+nocLdsEu2AW7YBfsgl2wC3bBLtgFu2AX7IJdsAt2wS7YBbtgF+yCXbALdsEu2AW7YBfsgl2wC76mh/ppjIQgXVloPxVSBRV0rBe455P6+kTKBYF3tonxY/IWarry7DvI298Tgp0PR9RzACaN1NeIS100+EdvKXW3cMZvF8wCK10Sq2it2NAzakmukP/wmoP/KuId3BRUMg5uCfCSNVSKVn1rNto7Un8jLrUVqJ4Fi2eEQiEYBzOsy3SYL37TNQdzi8Q5FxkqJIQBsNLlYMGF/zqAJWBxSEogDAY+DJibYqTuRg4WFgO3OKhCYTExbKk5G/mbkSPP2DQhLA5IO/NhSz1MMP882BDgnAFQwdiVSs2vPVhYDIJLUMkBgw1favM6lJoZDDAYhKbAYsOX+rqAhcXAuQSIAKzhSy2vS8YmB7NYH4WCfM7kw5VaWtdpOO3bfWZJZVXgPxMX898bVsm6RhkTIseX29yyIErm/J5z5vwr6pvmsLYjBgeDwSpVJS/OmT1n1de+9qANZgLc4q9Dyj2qQhUhSSUAUCL7GBcchCymTEYBYNWqVXj30MGHT586PZEJ+WAul7ts8bjspd9QKDRNU2nz4z94YtI3H3oI+XwB//3j/9m77eRUUJ9/0eh4APGoDz6vCi4ksgUTmYyBC4k8RLGwtzF+EGu+tHqRqqrYtm0rXnzhhQ7G5cpsNnvyiuBIJFKnqvSd55772eilS5fhwIH9ye+/dPaEf1T9otW3T8GtiyYgGNBBymYEgLSbvakidu8/h01vnkYhcab1gcVs5tx5c6PHjh7DU0/9qFsINPb3939UZg28X11dXR0Qwtr9g8efqGtc+Bn89re/O7FhR9BXNaFm+n98uxHTZ1SDKQqKAihweZlITUVtXQwNs8fg+Bmzdk+bnmPdf/7bwsbGeO2ECaED+9/5XCxWuTGbzVpDwJpGNtx+28o77rr7bmzZsu3k7z+cMlHzeiPrvnoTwtVhFAVQHAZY4HBEoiAAeDXUjI/gyJGeQEd6TFj2tHYuXNgYy2azVe0fngiWDLNloHNFo4FZkXDsoTVr1+KD4x8U/3Ci1qP5PV7N74FeFUbClKDEriy57A5JANL5a68hnqoINL8OAPqbXbNp7clTxTVr1/oOHjr0MFXxq2Qy9wEFACnoY//6la9QAHj+9Q/eUL2RWkVXoWgqkhZBypRImkDKBFIWkLIk+h1JWdL+zrmeNCWSDFB0DYquQvWG637TcnozAKxbt45yTr8PAGowGBwVDAbvmT9/Pvbu3dddijV9WdUUUE0BUQm6kwaCYe+ljK/w8ruUdsYCBLlMEUQhoJoCygWM+LIvHTx4sGfevIbqYMD3BSFkJVUUrG5oaFABoPXwhd1UVUBVahtpKtoOnEV/gSHHgBwDso5c6XO6yNF24CNQTbV9qBRUUenuwz1/BoCZM2dplOJeSggWL1myFEII9IeXziIKBVUUW1QKo2Ci41Anei9kkWcY6Ex5R8qfc0wi0ZPF6QNnYeQNB2j7IQpFOtg0WwiBxoWNIBKLVQI6Z8rUqTh69FiWaFNmEIWgLFShoM5TZbIzgVxvFp6ID5rfA6JQgBAIxsGLJkrpAsycAcH4gN1gX0QPTW9vP5Grr58cJJTOpbqmjgWAnp6ei4QSEEJAKAGh1BbHCS2DLAFmMAgmICwObjDnyYMMAtJL9oN89vRc7KWUQtOUsSqhSggA8sWivSEh9qBxTiCEAGRwQARUVaB67Hf5pZAQlA0Ayrq2LTCogVyhlLURNEw55yYABP2+4ED3vHSClBKQ9jiFdHqvEBCMQzAOKYSt6/RqSGnbDPJRbgT93hAAcM4NyhjrBYDKylhswEEZJgYJFxDchnGTwSqasIomuMnsIDiH5GKIzUAQTsCVlZUxB9xLIUVbKpVEff3kiLTMfimEA7HP5bZgHMJ07mnJAiuaYEXT3jcZDMLkTgBD7exgBKRp9NfVTQwnk0kIKduoJGRH8/ZmhMNh4skc3DnEkDlAi4GbtjDDguVAmZM1M6yB68JyKsCGBqD373s7GAySnTt3gBDyFhWCvPHee/8HAJhTU5g0BMg4uMXBTT4AZSUTrGjBKpiwCnablQbDbZuyfTmAuRPMegA4euQopCRbaCaTOd2XSLzX3d2Nu+64bR7PnP3LJSCDMBm4YW9FWcmyQYMytsW+Zpfdsm1MdimAdMc7K29bMedCdzeSyeS76XT6jLNI4PGf/+w5aLqOu25IjOOWKcSg0jJjcLZ2ecsZD5TdybqsOxC0ZYpbJ58frek6nn/+eVBJHgecjXkqk2nu7Ozcdfz4cdx556rJN5C3m8v3jBt2xpdnazjysawNy5lUbKkrbmtZsWL5pGNHj6Or62+7k5lMy5CFNRQKTfN6tAMvvvhSRe3EOqx/4oXXLvia7qO6CsVZrey5154KB5YpKSG5tHs+5/ZsZnEIk6Ei1fLH73373i/09fXi0fWPpgyTLchkMqeGgAEgHA5/vjJWsf2PmzYr1dXV+K8fP7vjLxduWkY8ilpetQZPg+UJxh63lzqlNDi7gTa3fuPraz6bzxXw79/5FutP51am0+kdZdaQ/2kzDKNDUci51179w8pbP3er8sAD6+pnVCWy+/fs21LAqBnlMT50qJXFLq2a2L/5gaVy7N133j69u7sb67/7iFHIFf4tlU6/Ppg1kLGU8hYAywBMeOWV33gfXb9+1Q+ffDL+4Ne/AcYY/tS8PbV5++4Dhy+MopY2ZrLiidQDgDBSp5TS+Y7psS65ZOHsW26++eYosxje2PwGNm586eKzz/x027+sXWsBOAfgbULIQQAgUspaAA8BGAfnsamrq4u0tZ0Q333kkdGmZS3f8JNnlBXLV0AOilRKCS7sWYlxjlKxgHw+j5Y3W/C/Tz/NQ6Hgjp9seKZ31py5ajwe4wAtz9zdAH5OpJTPAqgEgL5USkpu4eLFHloqFXniYh9t3bunauuWrStisSi5//4vYnHTEkyZOhWqokBICcuy0N7ehr2trXjt1VeRzqTl3ffc81bjgsZELF4pQ6EAqa4eI6UEicfj5dhTKoCikynx6Bop5C14dJ2XcjmouipvvGFGoSJaWfr738/7tmzdjl/88pfIZjKwnH2SpmkIhSMYW1ODhvmNGFcztjhudFXR69Wgck58Hg+XEorH5ylDJYA8kVKOckpdB0ADIBOJhOzv70OhUFILuTzPZLNcSE6SfSlvJp0O5A1DN0qGDxLS4/OUAh6PGQqHC5XxeJEQgkgoRH1+L/wBP6LRuIjH4+Uf8gSAUwB+MbhzzQSwCMA0p/QUQADgNJ/PJ/v7+wnnnFiWkJZhKCYzKADoqiZUXeW67iGcSxKPx2QoFAo7AybnuE8COAZgHyHkxGXjeFAQEQCzANQCqAIQBeAH4AXgcex052w45TMcyQHIAOgBcBbAUUJI5uOM/wcaHmf3g9UM7QAAAABJRU5ErkJggg==\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAA3vSURBVGiB7Vt7cFzVef+dc+/d90OrJyO/JSO/4ncxxfULMCYIAyEW08amJJgmM4GmnZjJdNq4gcSGzLQxk3bsaWcaaIHyR8CJrWAbpjgG/AhINsbYxkaSDY6xJFvSrrS7Wu3uvfecr3+cu1pbXhkJs/4nujNndufec77f+d7fd+4uw8gvIxwOfocBaz0e91yXyx0BgKyZiWUz5kcEvBKPJ18EYI+C5rWvkpKSyZGS8LGHGtbQR8ePUUdnB50/f57OfnqWWlpbaN++39O99fdQpCR0NBKJTBwJTfZFE4LBYLmh8+YXXvifKctWrEBPTze9+cbu8/3JVMoWNjwer3/ZsuUTvV4P239gP36yceNZW9CtyWQyei262hcB+7zurU/99Ge3r1nTgJdfevFsqr8/Wlc3rWbGzFkV8+fPr1iwYEEJgLadO3cmbr/jjohh6KXHPjxamsmar39pjoPBYHl5aUnnqZY2/b1Dh9LdPd39kUgk6PP5PD6fH36/Dz6fDx6PF+fOfdZ9+pPTgbq6Ou+aBx+0k/0DVYlEIjYcbX4tYM5pxeK/WKIDwM7Gxt0TJox/dtLESXC53JuHzvV4PBVHDjfvAYDZs+fonMsV16R9rYeM8XG1tbUAgMrKsrDP659DRJ5gMNhbaH5NTU0IAMaPHw9IPv5LAxORy+31AgBcLsO41lwAcLu9BgAYLheIkftLAxfzGgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4D/lME1ke7gDF8ltbOHe3W923oEwYi1jxftWfZWgAziwacZkd2pfyN96XN5IIu7dMtIKA9/TI+zqCnFps2Alg5UlojFnVqIHZUlO2sl4RyC4CU+SEEylux8Z/iyc7mrxw4U7UnYwvGpXMYKIgNGdwXC/76C48oRw3sDWfnCgIkARJXcpwbvpA1e6T0Rq5jDr8EAHKA6OpjUOJwfeXAJAEhAXAGgEPKq+dIMVJqowDO4RAAC0rHV21u5LijAJaABAOIAY5Oh15iFMgj1zEpcUuuXjpIWeCouxjAtnIZcGKA5AVFbRfazPUC50QrKe8+Qy8qiqjBYIODA5DgBd1pBO9WRg9sy7yOhXBca+icYrgTOUGOiKnIVdCdisAxJGBTPsYW0nHRrJqgfNmGVtiqaeR1xchF7Vgz40q/BUNmISlcL7CUgJAMnOUiVwEdF0PURIAAVHaC8ucbAiwcQAb1KQpwXMjFrhtYMcOVO8lhOB457ujcKZd9hBguSYwcelTupKyaQWKYJFEU4xJw/Dhfcw29ilSBcNjEoTucFnSnkeOOvvTJpcVC1cYoGB5NAGEQTukjMAzHoghJghyWCRjenYoTuZjKx8xJiwU4LrSZ6waWpIoBjTuRqxDHRUkSUMWAJAZp6QU5FqOw65HHapG3bGVcBTZXDI5VnFaFgBL1yC34uoBJqEJeIwD2MMY1ilZidAFEMlDOqm9UdpJ0ZawumI+LU9ArwhyqWxyNz14XsBAMUnLVH0ttGB0XococdCGWE3XhOV85MF1WV2OY3omK0S2SkxgYAZYYJoAUpcqEEjG/Ru80isA1ysMXYNCnCum4aKUPgTu90w3sFinXL6nO/MadCAhiKloxBjFMeSuK0S1Kylv1cE1bUVoYyHwhoI6bCswpjjuxK5u2G2lcti2jzNCRTluioHEVw52EBA5/2LKsLBL+h2gs/o+Fjpa+MqtmjCbkqQJSYFF3T3zRsPMvA75i7UiBA4FApa6z5+fNnbd6/frHADghk7QdlhAHdMY0KXkZAHAuozaRMDRtKYMdAYDVq1fjcHPTD860nZlsS3qsv7+/+6pNDr0RDAanGTrf85Onnq75/uNPIJ1O4+dbnj34Ot6B4eFLqksqUeEvgcflAREhZabR09+Li/EorLQ4eFv317D2oW8t0XUdu3a9jud/9auztqD6ZDLZOixwOByeouv8D1u3brtpxYrb0XS4Kfbj3//8VHC8d0nDLXfj67OWIeQJgDGADfoOAxHQl05i14l92PHBXiTPp/c/OrFh9vwF8yMnjp/A5s2bOqXEbX19fX+8CriqqspvmunDTz/10xkr71qFnY07Tr1i7aqsLg2Vb6h/GOPCpdAYgTPlNLmF5AzpvBRp74viX3a/hO6+ge47+hZG61fVTz9y+DCee27Lx15fYFFHR8cAcNkPuw2DPXfP1+vvvf+BB7Br967WX9Mbk70eCn33zlWoCrsgKAFBCdgy/2nLBCyZgCUSMGUSpkzC0G1MrKzE0XMt/la9I0QnM+cWL15cmkwmK1tOnwpksuabg8YVifjnhEOlj69dtw6nT51Kv2q96fYG4fG7gbJwFhn7cxicIJgEZwAfEiokGASpWG1KhvIwg1/91ti1N9DEJ7ZOzKxdt87T1Nz8A67jv2Kx/o85AJDk//zXjzzCAeA/D7zU6PZjkkuXcBuEjN2OrGiHabfDFB2w7HZYoh3mVaMDWWdu1m6Hy5Bw6RIuP6b87+HXdgDAww8/zIXgGwFADwQCFYFA4BuLFi3CoUN/6LRmyL/y6gSXTtC4QDTVgQo/B5iEJFJ6Rt64lI6Vfi3JYBFHd1JA5wIunUNIQvpr/C+bm5u65s9fWBnwe9dISWVc0/DNhQsX6gDwTuuhd3WNYOSGTjjSehGp7EVYsguWuJQfssu51wVTXIIpLsGWlzBgXsSRM5dg6Hk6uk787Zb39gHA7NlzDM7xoM4Yli5fvgJSSiRmmbP9HNA0Qm4D6axEc6uJ6eOzuCloQuOOjlneqiUx2BK4lDBwut2DTFaHoXFYGilaHEjMMOdKKXHb4tvw/nvvL9UZ+Lyb6+pw/PjxpOZhsziX0DigcYLG1QaEBD69ZKA7wRHx2/C7BDSNwEi9AEmZGmJJA/1Z9SJM12hwvcYBzgmaj89obW3pr62dGmCcz+cuQ68GgEtdl7oYU40CZwSeW+As1rmy5KzNkbY1WILDlOp71ubgnKA7czVO4NyhwQhcFS7o6urq5pzDMLRqnXEtCACpdCrFHOHlAsTgYEq0nCnj0jnBY6i8KCTLBxbmzB2yPkczmU4lAYAxHtKFECYAPeDzBQZD4GU+motMueXklECWc7QkSaVDGoTAVetz8AGfLwQAQoisbtt2N4BJZaVlpZQjkntdS8w5UFOFni0YLMGhWfny1rbVPVuoOVKyK9ZeTrMsUl7qAHdzkPyktzeG2tqbw8KihCQlPjVUl2hLBkswmDZD1mJIWxwDWTXSFkfWUs8sZ64QzlqHjiRA2tQ7ZcqUYCwWgyT6hBNjb+3ZvQehUIi52tje3M6FyHHIYNkOqM2RsTjS2cuAs+pe1uYKPLcBkduA+m60sH1+v5/t3fsWGGP/x6VkjR98cAQAMNc7bXJepAyWzWHaimjW4siYDGmTY8DkGMhqapgcaVM9yw5ugMOyeX4DkmGub1otABz/6DiI2O94IpE4E+3p+aCzsxP333PfAvOi2G8JBtMRbU68GZMj44Ao0BzXmgOsRk7spq1oWILB6rQP3nt3/byLnZ2IxWKH4/H4pxoAeFzuC21tretW3rUKnk5mtWiflzAGxhgDQ66IYyrnOnqzBFfDZjAdLk1HMnkpMWRNLldmFomamtrIL/71F+iPJ/8mnc2e4QDQm0jsOXfu3L6TJ0/ivtX3T607M26P6SzMWI5eB7ktPHLPc/MV5xwTjpe9sfLOu2pOHD+JCxc+fyeWSLyZdzCoWsvjNpqef/6F8KTJU/DDLT/a3jM90eDWCS5dqmDvxF7NCRSAOikQhCuMUXHMEDjm3v7jb/+oIRrtxpMbnuzNmvatiUSi7QpgAAiFQneXlZbs3rGjUauorMSmLc+8dShy7HbDELqeA3bC4GCScHxWSMDOgVuaPb2t+t3vPfK9O1P9A/j7v3vC7ov318fj8bdyWFf8YCSbzZ7VNHb+tVdfrV911ypt/bcfq52J2uTBg+//LhWwZ0nJYTtWf6WrcccDGFgLdn5nwkPVD9Q/MLOzsxNPbvhhNpUc+G5vPL7jcqxBjonozwEsBzD5lVde9jy5YcPqTZufKX90/WOwbRv7330nsffDt08dSB41EkZyHPfwmwBAZuTFsBm48GeuWfai2oUzp02fFjKzJhp3NuLFF/+765e//Pfd31q71gLwGYC3GWNNAMCIaBKAJwBUO3uQnZ2d/MyZNv1vn/j+LUuXLq/Z/MyzCIfDTmxW8Y+IVFyWqjKRQkDYNqKxGDb97GkcOXLk7LZt/9F8c12dqKqqYM4LYALQCWAbI6J/A1AGgKK9vSBhoa8vEe+N9TwejcZYU1MTfrN9O6puqkJDw0NYtnwFpk6dCsZUMrFtG22trTiw/11s3/4aotEo1jQ04NZFt6KsrJTCoZKtJaWRiGG4KBKJ5BJWnw4gDedAx+0yMJCywLnQGWOSMabV1NbikUfX40J7B367sxFbt25DMhGHZZkgAC7DhWAojOpx4zF3wS0YP64aVZUVYCoQSN2la4bhIsNlcOS73H5GRBUAHgcwBYABAD09PZROp1gq2V8WTybq4vH4xEQ8oSWSSfSnUkinM7As9RdUw9Dh9XoR8PsQCgYRCodESTj0x1Aw2OrxBXsDgYBdXl6eM2IB4CyAbZcb12wASwBMB1Dq7C4ACJZIJHstM5PWdC2TTmcom80wEtySAFwupum6wbxeDxeCuT0et8/v94UBTTrSJABRAKcAHGCMnbrKjy/bRBjAHAATAFQ5NuAF4IFqAtyOKzKo83MLgAkgA2AAQB+ADgCfAzjBGIsPxfh/6wbDK7xbMFYAAAAASUVORK5CYII=\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAyUSURBVGiB7Zp7kFRVesB/5/S9PdMz/ZoHMwo4MICDuoGVIYICIuzGcn0vC+oWGuNjs8mua9ySP4wpgyaiVVupbHYTsLJmNT7WNXExwqqzrq8g4oNxdXUgyEMQARmZd3fPTE/3vfd8+ePenhlgBsFlrFSqb9Wpvn3vd77f+b7zne87ffsqjv+wE4nYDQqWl5aWfDUcLqkAyOUHunID+Q8EnkilMo8C7gnoPPaRTCYnVyQT71+1bKl80PK+HGw9KPv27ZPde3bLjp075NVXX5FLL7lYKpLx9yoqKuqOR6f6PIFYLFZtW7r54YcfqV+4aBEdHe3ywm+e39eb6etzPZfS0kj5woUX1EUipWrj6xtZedddu11P5mYymc5j6Q19HrgsUrL67r/7+8VLly7j8cce3d3X29vZ0DB9yplnfWXcrFmzxjU2NiaBXevWrUsv/trXKmzbqnz/9+9VDuTyz35hi2OxWHV1ZbJ1245d1ltvvpFtb293Kyoq7LKystKysnLKy8soKyujtDTCxx/vSW3fsT3c0NAQWbpkiZvp7a9Np9Ndo+nWxwJrLYvmzV9gAaxbt/75urrxd592Wp0Oh0tWHSkbiUQSv3unuQlgxoyZltZm0TF1H+umUnrC1KlTAaipqUpESmMzFIRjsVj3SPJTpkyJA0ycOBGMnviFwSISLolEAAiHbftYsgAlJREbwA6HESUlXxg8lkcRXAQXwUVwEVwEF8FFcBH8/xhsnZC0ksw49eQPI5mmNtP54ccAIvqgqbz4aYn8zYoTUXXcFnueyZ8eXtleZt75iQnpU0VUvYiqB5mvu5p+XH9w8RtgnJMOLut/7rd4+fpRBcS52hz65csnHdxQ8clZnyuT3NV40sHRUnfq58mUWFJ70sEn+yiCi+AiuAgugovgIrgILoKL4CK4CC6Ci+D/Q+Djf/higk8Jzs0IMjIGYDGAp0AUeBbiHf3Xs/HGAHyYlYaRX0EYC4txNeIFugvWHyXzua8cnDjYGMBoQIFhRFfLmLjaCxqAw8iuHing/nCwGlLuMrKrveNfnccPFnyLtQ8c0a1jElye8sGFAYwUSCN54Q8GB4ljKKpHkBmLOZbB4FLgjhLVYxNcDFnkMXJUj03m0kOKR0sgYzLHRvlwpcDYI7oaGYvl5HB4ZRrJ1cf9fP5E/5NwQUKM7uoTOI4/ql38kmgUOCMnEHMCL819sag2jJJAxgIs+HNY6PGlpUxXDQWXw5dXjxH8SFZBPf7SyqKrMQLKG7b/OkpmTBJI0BSjbwTGYo6Ni5+ZjMJDj1wkxmQ5iV+VsBh9BzImKbNQFhWjp8wx21c7dKIV9A94IxaJsdplZt9574JQVcUdpr3rzlEHdzLASslpg19EofLMMa3dc0Z9c9YMXT+s7/GCo9FojWWph87+6tmX3XTTzT7XA/F4xutXr4fyOuQZVQUQ0tLphY1nlcn5YqgAuOyyy3inefOtH+36aLJr5Obe3t72o4w68kIsFptuW7pp5d33TPne928hm83yLz+6b9PVb/4niRK9QNfUoquqUaUREEEG+jGd7Zi2Dnpy3qYHGr7OFdcsX2BZFs899ywP/fznu11PLslkMjtHBScSiXrL0m+uXr3mlEWLFrN58+auxD+u2HZWhb0gcvkyShZ/Ax2N+70KPcVvJpMm999NZJ99mi1dzsb3rviLGbNmz6rY0rKFVavubTWG83p6ej4psAbfr66trS03xtlw98p76s+bN5+nnvzFtouevK/s1AnJM+I/vB37j6aDziJeCtxhzUkhTgoYwJpchz3zbJI7fj/pzA829f6iR/bPPW9e9aS6utjbb715YWVl1SOZTMY5DGzb6scXf+OSS6+48kqanntu55+99shkOyLx8uuvIjSuDEzq6Ob5TdzgPJ9GhT2sCbV4W1vK57R+FP9lOrT33PnzKjOZTM2OD7dFB3L5FwaDq6KifGYiXvn95ddey4fbtmWv2fhIiVUqpbpMEao2SH4fiKCMgAbRggSuVkKwEQz22q4iVKtQEYUtJvzdlvX6+bq67PJrr41sbm6+VVv8W1dX7/9oADH6b//0+us1QO/jD6xPhGWSCgsqLJj8PsTdjzj7Ma7fxDkAzn5wjry+H3H2YfL7UGGDCguJEqnPPf3YOoDrrrtOe56+C8CKRqPjotHoN+fMmcObb7zRelsk9W1lC4QFCRlM9yfoKnsoEgOLVWCxDLfYBRwwnXmwDIQVyoMbo6lrfrq5+dCsxsbaaHlkqTFSpUMhvjV79mwLwHvjldewBGxQlqBswXn3Y6T/EDhtiNOGuG2I2444QXPb/WtOGzhtmL7PcN7di7IFFegiJDq3+ZVXAWbMmGlrzRJLKc6/4IJFGGO4MdQ+gxAQEn/2LcH0u+Sa27HO0IRq/V+MSqnBOUZARMAD75DB2w4mq8AKWkggpPiOtJ3dYgznzTuPt996+3xLoc8+vaGBlpaWzFybrygtqCPgeODtcTFtBl1hUBHfGgl+wNGv8FIayWjE6KCfD1UhBVqotPWZO3Zs7506dVpUaT1Lh21rPED7oUNtKH8OUYLSoHTwWRiEAsmBDIA4gCPIAJh8YL3lyw7vi5JAJ7QdamvXWmPbofGW0qEYQL4/0zeYjdTRTQ0Oxp9/Svx9jvKAkBocsCh1dP9AZ76vNwOglI5bnuflAaukPBo9bM8UpMIjvxeiWAUbATHK3/yNJM/h30vKozEAz/Ny2nXddoCKyqrKwc5GDYFMUJmM8peLqyCvkH6FZP1zXP+eGBXIFvQcrquyqroyALdrxGzv7u5i6rTTE3lX0gUL/DIYPPfwFDh+k5xCBhSS1Ui/9s9zQ/cLz0rEGxqEGMWAK92T6yfHu7q6MCLbtSj1UtPzTcTjcfW0E3t5EBSkv0FgPgAMQgtWa/9azpcZHICrhvR48B+52CvRaFS9/PJLKKVe1Mao9e+++zsAtk9rnIwbLBFHIQ5IACWvkJxGBjSSDeDZ4HxAIznty+SV38chGIA/PXumzZoK0PJBCyLq1zqdTn/U2dHxbmtrKxddfmXj1r7QRr9jMH/5Ye4d8OdV+odZ3F+AqyG3F/oFelr62PQnl14667PWVrq6ut5JpVJ7giLBygfWrMYOh3ll/pLx4iojR7p3QMGgpQX4kPUE8OFuF0chrjIvzL78VDsc5sEHH0SLWkmQLuhOp5v27t376tatW7nk8iun/UN8VhM5BblASS5w53BowdXD4L7Lg8EG7Z6SM36z+MILp25p2cqBA/s3dKXTLxRSBeDvtUpL7M0PPfRwYtLken791z9Y++fevmWE/WJBIelbgJbDtz4mePblBksrcPU/ubVrF65Yuayzs50Vt6/ozuXduel0etdhYIB4PH5RVWXy+WeeWR8aV1PDz+6/56W//PDFxbpELGULgwVEcwSYoWXkKExOuatqGl9b8p3vfb2vt5/b/uoWtyfVe0kqlXqpwDpql1lVlbwhUhr52VNPrQ3PPuccNm16PbXrR3f+9pvm0NV+pWEwhQKIqKHnm57iV9nydc6Smxc1zm5MHvj0AHfecUeuv7f/u509PY8N5wyCReRcYCEw6YknHi9bcfvtl9276r7qG2+6Gdd12bhhQ/rghhe3TdmywT4l2zkhEeIUgJTLZ62RygPbT5/rlv/xvLOmnzE9ns/lWb9uPY8++u9tP/3JPzd9e/nyLLAXeE0ptRlAicgk4BZgfDAGc/DgQb1790fWrT+45Zz58xdMue+++0kkk/5N8RO2iPiZ0BiMCMbz8FyXzq4u7l91L5ub3969Zs2/Np/eMM2rrT21YKQBPgPWKBFZAyQA093drTzPobu7uyPV3XNbR2enam5uZu3atdTW1LDsqqtYeMEipk2b5m8GANd12bVzJ69vfI2n1/6Kjo5OvrVsKefOPZeqqkpJJCtXJ5OJinBpRJLxeOF3bI8FZIAYoEN2SHmeJ6GQ2CiMUipUP2UK199wI59+2sp/rVvP6tVryKRTOE4eAcJ2mFg8wfgJE5nZeA4TJ4yntmYcSimUUsaydMi2wxIKKTXM6n4lIuMCV08m2O52dHSQzfbpvkxvZSqTbkinUnWpVDqUzvTS29dHNpvFcfy6aNsWkUgp0fJyYrEYiUTcSybin8RjiZ2lZeXd0WjUra6uDg2L/z3A6uHBNQNYAEwHqvAXTTl4Kp3O9HhOvk+FGMhmHXHdHGLEE8CytNY6rCKRsPY8VRoOh8tisfIkhFxgIAB2AtuA15VS20ZcTsEgEsBM4DTgFKASiAClQAnBig7EC8/8BoAc0AekgE+B/cAWpVTqSMb/AlY1WXIncMcxAAAAAElFTkSuQmCC\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAxNSURBVGiB7Zp7kFTllcB/5/a93dMz3T0PemYIDgoCPhZ5iaD4wNkFjQjRRMlLTNbSlKlyzZpobSVbFRPUbNVWSRCWuKvlxqybtbIrukp4SATZCAgospEBgeElj4EZ5t3d0+++37d/9O2ZnqEHQZzZSlXfqlMz/c253+875zvfOefeHuH8L6u83P+AwH0lJZ4pbrenEiCVSnYmEsndGl4NhSKvAJkLmPPcV0VFxZjKivKPv77wXr274WN9uvm0PnHihD5y9IhuPNioN216Vy+Yf6eurAj8b2Vl5aXnM6d8loLf7w9apvHhyy//29jZ9fW0t7fpdWtWN7Wdao4qpaiqDpbdXF9fV1paKpu3bGbxk08eSWXU9ZFIpOPirC33v7xs+TIdiUT0Pz239NjeaTOTHXXjdb4cuP6W5DOLFx/7aNdH+oknfqQryv0vXZTFfr8/GKyqaN7XeMhc//ba6NSfPFXqS6fESJ29jdGAX69+9KHY9OnTyxbec08mHInWhsPhzsHmNs4FNgxdf+NNN5sAh3/7n40dCxeKedUsOr6x8CzdsnBEQu9sPABwzTWTTMNQ9eec+1x/FDEuGTduHABXtreOKutJYyiFqq4tqD+5O3wJQF1dHSij7nODtdZuj9cLgMfGOpcuQInSFoDldqNFez43eCivIrgILoKL4CK4CC6Ci+AiuAgugovgIrgILoKL4CK4CC6Ci+A/B7B5vor6Mz4PNnbRYAAtoCQLUMMFVobuBWOALWdjVIGxiwbbZC3WkrXWLqAzJBZrR5T0LWTgdSHfdF1YcIlG57t8oM5nfov1OcCKPmDW1Rfi2IsA5yI5F9WFXF0o0i8arARwggsBu4BbhwaM6g0ujXY+9b+GLqrzLR5E5wsH2ziB5QRXoW8lCy3mosH553iwlDlEe9znai2DpMyhAJ+PxUNTJMhZm51+WM9xvsWFXD2kx0nl9rjQ4oYC3C+4BoEMnasl39Vn6wxRdcqbXApXpwupWBcEVgLKGLw6DU1w5bkaCjcChcYuHozuLYtqEFfroXC1TZ67GcbjlEuZWjSIHr6ozjZ7/y/VSWOLdgJIF9zjQl3JFwDOXn1lsYDOULm6X+YaROcLB6s8+LC2tzqvoc+Wx0L2nT/6wlIm5y6LQ9bs5TLXsO5x7jG192lxuJq9bCOg0aIRGcYEkt9lCsPp6lxlMsBlFE4ghcYuGoxznHKFYNjKYq7Zy5XFYW32lMtCBGzbLlwWLwB83m/2NNC44R0iFaP503+8jO1UqHz5wiwW0aNzvysgdPJTQr/7dFD9fHD+vecN9vl8NaYpv546ZeqCBx98CMhGbPXEqZRfcTWmyySTjuO2TMora/B4Sji+832OnWoGYMGCBez88IMfHD50eExG6Yd6enraBjJcAwf8fv+Vbsv1Pz9f/NT1y1esQCnNPz6zeGuy6WBN+MRRrwp1YMR6MOIJMqEuOj49xNFd2zh5aD9SVpr44PCJXVOmXXvpHfPm4fP7rtz98Z/usSz3+lQq1e/fnvuFSHl5+VjTNLb96lfPj6yv/0t2bN/eufJnj+37Uql1c/1Xv8WM279CaZn/rJcBGoj1hNm+7k22rF5JcyK1edp3Hps0bfq0yj0Ne/jFL55pVopZ3d3dx88C19bWlqVS8Z2Lf/7U1XNvu51Vb72x7/irz9fUBEcEv/03PyFYPRJDgZHt9XpvzG8QlAFnWppY+S9LaOnsaPPOWdhxx7z5V320cydLl/7yE2+pb+bp06dj/VxtWbJ03h13zr/r7rtZu2bNwVP/9cKYMiHwtW8+QNAbwOiOIN09SCiChCKQL+EIKhxBhcN4EGpGjuJww66yxNH9gePac+zGm26sikQiNY379/kSydT63uCqrCybXB6oeuS+RYvYv29f/OTKFz1+dIlXXFQrCznRjNhkRfdJzmIMEAExsqbUmh68holWGXf43deMg6NHJ+5btKjkgw8//IFh8lJnZ88nBoBWxpPf+e53DYC1Ly5bVSb6Mo8WSrQgx5uRY6cHSDMcz0q/vx/PSTNeJXi04EOPfe93L70JcP/99xu2bfwUwPT5fNU+n++rM2fO5P3332+uS3V9y9KCG8FSmtjRo3iN0uz+qqylemDnLhpDQDsFJGrHMG2F2xAyGi5Nhr65Y8f21unTZ9T4yrz3KqVHGC4X91x33XUmwN7N775nApbuk90nD5BpbUbaWqG9Dd3eju5o6y/t7dDehrS1kmltYffJ/ViA25nDBcbeLZs2AUyaNNkyDL5minDL7Nm3opSiNtQ0yUQwESydlXg6xc70Sf5CewliYSD9TqHu/anpIMUnJIiLjSVCGjAFTA21odNTlFLMunEWO7bvuMUUjKkTrriCvXv3RDyiJxpacGVXSc56W2uO6DhtKkmFFsocHchmtKhoukURNrJPG5YDdAEuDYaAV/TVjY0HesaNG+8Tw5hmuC1zFEBLS0urkQ3QPtFgILgQTC0IkAZSgEJQCClnTBwdF4KBOPf2iQBnzrS2GYaBZblGmWK4/ADxWCzqoS85iDOZDFiMS2ddV5Kz2EkGhgwECYLOzqOzxy0W7YkAiBgBw7btFIC3tMw/2JsrnS9OI5B2pPdt0AC9gdVZZxkBANu2k0Ymk2kDCI6oqsw1c/nNu8rVW8l+2ZFCkxRNzMhKUjQpNBlnv23nXfbAeTRQHayudMBtBlod6OrqZNz4CeVprcKqd4KsZBxgGk1KNEmBmGiijsScsZRo0s4CMnn3284CMqJCY8aOCXR2dqK0PmBokQ3r1q7D7/dLq7tyY8axMCOatDNZFqhJiCbuWNsLNrJjCUcnt4C0ZOew0WTQnDYr3/X5fLJx4wZE5B1DKVm1a9dHAIyYesPYjEBa+vYwJZAUSAgkHAtjookaWcl9Togm4eim8u5PS9YDNVNmXg7QsLsBreX3RjgcPtzW1rarubmZ+QvumtahXJvzrUzmWRvrZ61yxNnvPKuTA6xvt13bvjxv/tSW5mY6Ozt3hkKhoy4Ar6ek6dChg4vm3nY7oZJAJnG4oUIQESdD5Ud0v30XSBlZC1OGdjyTA/darwK3LcxcPm585ZJnl9ATinwvnkweNgC6wuF1x44d27R3714WfOWucZGrb3g7kee+eJ6LewPLcXU0bzwuuf2G3P3NoyevnzP3tsv3NOylqenkHzvD4fWQ197aikeW/nJJd1dnJ4//9On57V+a8Hoib7K4kQeUAWL0D7RcsJ2oqHv9wUcfu7Orq5MVK5Z3KS0P53j96lsgEPjyiKqKtW/891uu2tpalvzDMxsTW96s9yhMC8HUOCkxm07JO/fZk5A9dkmDTOSqWe/99fcfmRPtifHY3z6a6Q5F7gyFQhsKggFGjKh4wFviffG11153T59xHVu3bg3968/+7g9V3ae+0Zv0kX49l3ISjA2ccpe/NXvR9+uvnX5tRdOpJv7+xz9OxnpiD3d0d/97PqcXrLWeBcwGLnv11d96n3j88QVPPf108KHvPUwmk+HttWu71q96Y0dozzajJBUfXyqMA4gpfShmeY54JkzX19/6VzfMmDmjMpPOsOqtVbzyym9alz23fM23Fy1KACeAP4rIBwCitb4MeAQY5SxEt7a2qIaGBn70wx+OTKXTc5Y+t8w1d85cdN5KtdbYSqGVImPbJOIxotEo6/+wniXPPmsH/L4Ny5etaJk46Rqprq7JPTgooBn4Z9FaPw9UAHR1dSnbTsuZMy1GMpnItLZ2GFu3bq5d/fvVc0ZUjZB7F36d2fW3MmHCFZguF0pr0uk0Bxsb2bL5PV5fuZLuUEjfdffdG2+66ebW6mCVLvP5qa4OAoYEg8Gcg7tNIAIEADHdJnbcxmNZ6UQ05nK7TT1x4sRYRVV1/FTTqdLVa9bywgsvEImESKfSAFiWhT9QzqhL6rh25g3UjbokPnJkTaKkxFRaa8NtGbaIy+Up8eS2VgEx0VpXO66+HKfdbW9vV93d7RKNJl3xeNQOd4d1Mp0i3B3yRCKRsmgiYSVTaa9orS23lfR5vany8vKYLxCIeyxLKqoqtddbKh6PSVVVtQ4Gg5IHPQI8nx9ck4CbgSuBarJnvARsiUai4XBPmGQyqbWGRCxh2VrZAKYYLtNjZUyXSxsuU6oqyg1fwO91nhUSzvQdwB5gm4h8UvA4OYsoByYDY4EaoBLwAN7sYiDvZ4LsqUo60uNIK3AY2CMioYGM/wPREY0iGUY58wAAAABJRU5ErkJggg==\"],\"labelFunction\":\"var deviceType = dsData[dsIndex]['deviceType'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}, ${energy:2} kWt';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}, ${temperature:2} °C';\\r\\n }\\r\\n}\",\"tooltipFunction\":\"var deviceType = dsData[dsIndex]['deviceType'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}
Energy: ${energy:2} kWt
';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}
Temperature: ${temperature:2} °C
';\\r\\n }\\r\\n}\"},\"title\":\"Tencent Maps\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermomether\\\";\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"markerImageSize\":34,\"gmDefaultMapType\":\"roadmap\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"useColorFunction\":true,\"markerImages\":[\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAwgSURBVGiB7Zt5cBT3lce/v18fc89oRoPEIRBCHIUxp2ywCAgIxLExvoidZIFNxXE2VXHirIO3aqtSseM43qpNeZfYKecox3bhpJykYgdjDkU2mBAB5vCamMNYAgQyURBCoxnNPd39O/aP7hGSEUR24L/uqqf+zfR77/Pe69/Rv6kWwcgPLRIJfZUAa7xez2xd90QBwDSNZKlkHJHAK+l09mUA7BP4vPpRUVExMVoRef+L998njxx9X57vPi/PnTsnO850yPaT7XLXrrflqjtWymhF+HA0Gp0wEp/kHymEQqG4ptJDGzf+um5RUxMSiV7Z3Lyt88L5nozgHJWj4pGmpqZav99PWve04onHHuswmViQzWb7ruZX+Udgv8/z3A+f/NGye1evxssvb+wo5PMfTZs6bfqcuXNHL7hlweh58+ZVAOTUpk2b0p9dvjyqqmrs/b8ejpUMc+unzjgUCsXjsYruE+2n1JY/NedM0zCi0VjA7/d7/f4AAgE//H4/vF4fOjvP9h5695C/oaEhcN/q1SyTzVdnMpnklXzTq4EplUsXfmaRCgC7du3cOn78+KfGj59Add3z1Md1vV7vqPa2D1sA4MYbZ6qUiqVX9X21i4TQcfX19QCA6urquN/vn0kAPRQKpYbTnzRpUhgAampqAEFrPjVYSql7fD4AgK5r2tV0AcDj8WkAoOk6JJGeTw2+nocLdsEu2AW7YBfsgl2wC3bBLtgFu2AX7IJdsAt2wS7YBbtgF+yCXbALdsEu2AW7YBfsgl2wC76mh/ppjIQgXVloPxVSBRV0rBe455P6+kTKBYF3tonxY/IWarry7DvI298Tgp0PR9RzACaN1NeIS100+EdvKXW3cMZvF8wCK10Sq2it2NAzakmukP/wmoP/KuId3BRUMg5uCfCSNVSKVn1rNto7Un8jLrUVqJ4Fi2eEQiEYBzOsy3SYL37TNQdzi8Q5FxkqJIQBsNLlYMGF/zqAJWBxSEogDAY+DJibYqTuRg4WFgO3OKhCYTExbKk5G/mbkSPP2DQhLA5IO/NhSz1MMP882BDgnAFQwdiVSs2vPVhYDIJLUMkBgw1favM6lJoZDDAYhKbAYsOX+rqAhcXAuQSIAKzhSy2vS8YmB7NYH4WCfM7kw5VaWtdpOO3bfWZJZVXgPxMX898bVsm6RhkTIseX29yyIErm/J5z5vwr6pvmsLYjBgeDwSpVJS/OmT1n1de+9qANZgLc4q9Dyj2qQhUhSSUAUCL7GBcchCymTEYBYNWqVXj30MGHT586PZEJ+WAul7ts8bjspd9QKDRNU2nz4z94YtI3H3oI+XwB//3j/9m77eRUUJ9/0eh4APGoDz6vCi4ksgUTmYyBC4k8RLGwtzF+EGu+tHqRqqrYtm0rXnzhhQ7G5cpsNnvyiuBIJFKnqvSd55772eilS5fhwIH9ye+/dPaEf1T9otW3T8GtiyYgGNBBymYEgLSbvakidu8/h01vnkYhcab1gcVs5tx5c6PHjh7DU0/9qFsINPb3939UZg28X11dXR0Qwtr9g8efqGtc+Bn89re/O7FhR9BXNaFm+n98uxHTZ1SDKQqKAihweZlITUVtXQwNs8fg+Bmzdk+bnmPdf/7bwsbGeO2ECaED+9/5XCxWuTGbzVpDwJpGNtx+28o77rr7bmzZsu3k7z+cMlHzeiPrvnoTwtVhFAVQHAZY4HBEoiAAeDXUjI/gyJGeQEd6TFj2tHYuXNgYy2azVe0fngiWDLNloHNFo4FZkXDsoTVr1+KD4x8U/3Ci1qP5PV7N74FeFUbClKDEriy57A5JANL5a68hnqoINL8OAPqbXbNp7clTxTVr1/oOHjr0MFXxq2Qy9wEFACnoY//6la9QAHj+9Q/eUL2RWkVXoWgqkhZBypRImkDKBFIWkLIk+h1JWdL+zrmeNCWSDFB0DYquQvWG637TcnozAKxbt45yTr8PAGowGBwVDAbvmT9/Pvbu3dddijV9WdUUUE0BUQm6kwaCYe+ljK/w8ruUdsYCBLlMEUQhoJoCygWM+LIvHTx4sGfevIbqYMD3BSFkJVUUrG5oaFABoPXwhd1UVUBVahtpKtoOnEV/gSHHgBwDso5c6XO6yNF24CNQTbV9qBRUUenuwz1/BoCZM2dplOJeSggWL1myFEII9IeXziIKBVUUW1QKo2Ci41Anei9kkWcY6Ex5R8qfc0wi0ZPF6QNnYeQNB2j7IQpFOtg0WwiBxoWNIBKLVQI6Z8rUqTh69FiWaFNmEIWgLFShoM5TZbIzgVxvFp6ID5rfA6JQgBAIxsGLJkrpAsycAcH4gN1gX0QPTW9vP5Grr58cJJTOpbqmjgWAnp6ei4QSEEJAKAGh1BbHCS2DLAFmMAgmICwObjDnyYMMAtJL9oN89vRc7KWUQtOUsSqhSggA8sWivSEh9qBxTiCEAGRwQARUVaB67Hf5pZAQlA0Ayrq2LTCogVyhlLURNEw55yYABP2+4ED3vHSClBKQ9jiFdHqvEBCMQzAOKYSt6/RqSGnbDPJRbgT93hAAcM4NyhjrBYDKylhswEEZJgYJFxDchnGTwSqasIomuMnsIDiH5GKIzUAQTsCVlZUxB9xLIUVbKpVEff3kiLTMfimEA7HP5bZgHMJ07mnJAiuaYEXT3jcZDMLkTgBD7exgBKRp9NfVTQwnk0kIKduoJGRH8/ZmhMNh4skc3DnEkDlAi4GbtjDDguVAmZM1M6yB68JyKsCGBqD373s7GAySnTt3gBDyFhWCvPHee/8HAJhTU5g0BMg4uMXBTT4AZSUTrGjBKpiwCnablQbDbZuyfTmAuRPMegA4euQopCRbaCaTOd2XSLzX3d2Nu+64bR7PnP3LJSCDMBm4YW9FWcmyQYMytsW+Zpfdsm1MdimAdMc7K29bMedCdzeSyeS76XT6jLNI4PGf/+w5aLqOu25IjOOWKcSg0jJjcLZ2ecsZD5TdybqsOxC0ZYpbJ58frek6nn/+eVBJHgecjXkqk2nu7Ozcdfz4cdx556rJN5C3m8v3jBt2xpdnazjysawNy5lUbKkrbmtZsWL5pGNHj6Or62+7k5lMy5CFNRQKTfN6tAMvvvhSRe3EOqx/4oXXLvia7qO6CsVZrey5154KB5YpKSG5tHs+5/ZsZnEIk6Ei1fLH73373i/09fXi0fWPpgyTLchkMqeGgAEgHA5/vjJWsf2PmzYr1dXV+K8fP7vjLxduWkY8ilpetQZPg+UJxh63lzqlNDi7gTa3fuPraz6bzxXw79/5FutP51am0+kdZdaQ/2kzDKNDUci51179w8pbP3er8sAD6+pnVCWy+/fs21LAqBnlMT50qJXFLq2a2L/5gaVy7N133j69u7sb67/7iFHIFf4tlU6/Ppg1kLGU8hYAywBMeOWV33gfXb9+1Q+ffDL+4Ne/AcYY/tS8PbV5++4Dhy+MopY2ZrLiidQDgDBSp5TS+Y7psS65ZOHsW26++eYosxje2PwGNm586eKzz/x027+sXWsBOAfgbULIQQAgUspaAA8BGAfnsamrq4u0tZ0Q333kkdGmZS3f8JNnlBXLV0AOilRKCS7sWYlxjlKxgHw+j5Y3W/C/Tz/NQ6Hgjp9seKZ31py5ajwe4wAtz9zdAH5OpJTPAqgEgL5USkpu4eLFHloqFXniYh9t3bunauuWrStisSi5//4vYnHTEkyZOhWqokBICcuy0N7ehr2trXjt1VeRzqTl3ffc81bjgsZELF4pQ6EAqa4eI6UEicfj5dhTKoCikynx6Bop5C14dJ2XcjmouipvvGFGoSJaWfr738/7tmzdjl/88pfIZjKwnH2SpmkIhSMYW1ODhvmNGFcztjhudFXR69Wgck58Hg+XEorH5ylDJYA8kVKOckpdB0ADIBOJhOzv70OhUFILuTzPZLNcSE6SfSlvJp0O5A1DN0qGDxLS4/OUAh6PGQqHC5XxeJEQgkgoRH1+L/wBP6LRuIjH4+Uf8gSAUwB+MbhzzQSwCMA0p/QUQADgNJ/PJ/v7+wnnnFiWkJZhKCYzKADoqiZUXeW67iGcSxKPx2QoFAo7AybnuE8COAZgHyHkxGXjeFAQEQCzANQCqAIQBeAH4AXgcex052w45TMcyQHIAOgBcBbAUUJI5uOM/wcaHmf3g9UM7QAAAABJRU5ErkJggg==\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAA3vSURBVGiB7Vt7cFzVef+dc+/d90OrJyO/JSO/4ncxxfULMCYIAyEW08amJJgmM4GmnZjJdNq4gcSGzLQxk3bsaWcaaIHyR8CJrWAbpjgG/AhINsbYxkaSDY6xJFvSrrS7Wu3uvfecr3+cu1pbXhkJs/4nujNndufec77f+d7fd+4uw8gvIxwOfocBaz0e91yXyx0BgKyZiWUz5kcEvBKPJ18EYI+C5rWvkpKSyZGS8LGHGtbQR8ePUUdnB50/f57OfnqWWlpbaN++39O99fdQpCR0NBKJTBwJTfZFE4LBYLmh8+YXXvifKctWrEBPTze9+cbu8/3JVMoWNjwer3/ZsuUTvV4P239gP36yceNZW9CtyWQyei262hcB+7zurU/99Ge3r1nTgJdfevFsqr8/Wlc3rWbGzFkV8+fPr1iwYEEJgLadO3cmbr/jjohh6KXHPjxamsmar39pjoPBYHl5aUnnqZY2/b1Dh9LdPd39kUgk6PP5PD6fH36/Dz6fDx6PF+fOfdZ9+pPTgbq6Ou+aBx+0k/0DVYlEIjYcbX4tYM5pxeK/WKIDwM7Gxt0TJox/dtLESXC53JuHzvV4PBVHDjfvAYDZs+fonMsV16R9rYeM8XG1tbUAgMrKsrDP659DRJ5gMNhbaH5NTU0IAMaPHw9IPv5LAxORy+31AgBcLsO41lwAcLu9BgAYLheIkftLAxfzGgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4D/lME1ke7gDF8ltbOHe3W923oEwYi1jxftWfZWgAziwacZkd2pfyN96XN5IIu7dMtIKA9/TI+zqCnFps2Alg5UlojFnVqIHZUlO2sl4RyC4CU+SEEylux8Z/iyc7mrxw4U7UnYwvGpXMYKIgNGdwXC/76C48oRw3sDWfnCgIkARJXcpwbvpA1e6T0Rq5jDr8EAHKA6OpjUOJwfeXAJAEhAXAGgEPKq+dIMVJqowDO4RAAC0rHV21u5LijAJaABAOIAY5Oh15iFMgj1zEpcUuuXjpIWeCouxjAtnIZcGKA5AVFbRfazPUC50QrKe8+Qy8qiqjBYIODA5DgBd1pBO9WRg9sy7yOhXBca+icYrgTOUGOiKnIVdCdisAxJGBTPsYW0nHRrJqgfNmGVtiqaeR1xchF7Vgz40q/BUNmISlcL7CUgJAMnOUiVwEdF0PURIAAVHaC8ucbAiwcQAb1KQpwXMjFrhtYMcOVO8lhOB457ujcKZd9hBguSYwcelTupKyaQWKYJFEU4xJw/Dhfcw29ilSBcNjEoTucFnSnkeOOvvTJpcVC1cYoGB5NAGEQTukjMAzHoghJghyWCRjenYoTuZjKx8xJiwU4LrSZ6waWpIoBjTuRqxDHRUkSUMWAJAZp6QU5FqOw65HHapG3bGVcBTZXDI5VnFaFgBL1yC34uoBJqEJeIwD2MMY1ilZidAFEMlDOqm9UdpJ0ZawumI+LU9ArwhyqWxyNz14XsBAMUnLVH0ttGB0XococdCGWE3XhOV85MF1WV2OY3omK0S2SkxgYAZYYJoAUpcqEEjG/Ru80isA1ysMXYNCnCum4aKUPgTu90w3sFinXL6nO/MadCAhiKloxBjFMeSuK0S1Kylv1cE1bUVoYyHwhoI6bCswpjjuxK5u2G2lcti2jzNCRTluioHEVw52EBA5/2LKsLBL+h2gs/o+Fjpa+MqtmjCbkqQJSYFF3T3zRsPMvA75i7UiBA4FApa6z5+fNnbd6/frHADghk7QdlhAHdMY0KXkZAHAuozaRMDRtKYMdAYDVq1fjcHPTD860nZlsS3qsv7+/+6pNDr0RDAanGTrf85Onnq75/uNPIJ1O4+dbnj34Ot6B4eFLqksqUeEvgcflAREhZabR09+Li/EorLQ4eFv317D2oW8t0XUdu3a9jud/9auztqD6ZDLZOixwOByeouv8D1u3brtpxYrb0XS4Kfbj3//8VHC8d0nDLXfj67OWIeQJgDGADfoOAxHQl05i14l92PHBXiTPp/c/OrFh9vwF8yMnjp/A5s2bOqXEbX19fX+8CriqqspvmunDTz/10xkr71qFnY07Tr1i7aqsLg2Vb6h/GOPCpdAYgTPlNLmF5AzpvBRp74viX3a/hO6+ge47+hZG61fVTz9y+DCee27Lx15fYFFHR8cAcNkPuw2DPXfP1+vvvf+BB7Br967WX9Mbk70eCn33zlWoCrsgKAFBCdgy/2nLBCyZgCUSMGUSpkzC0G1MrKzE0XMt/la9I0QnM+cWL15cmkwmK1tOnwpksuabg8YVifjnhEOlj69dtw6nT51Kv2q96fYG4fG7gbJwFhn7cxicIJgEZwAfEiokGASpWG1KhvIwg1/91ti1N9DEJ7ZOzKxdt87T1Nz8A67jv2Kx/o85AJDk//zXjzzCAeA/D7zU6PZjkkuXcBuEjN2OrGiHabfDFB2w7HZYoh3mVaMDWWdu1m6Hy5Bw6RIuP6b87+HXdgDAww8/zIXgGwFADwQCFYFA4BuLFi3CoUN/6LRmyL/y6gSXTtC4QDTVgQo/B5iEJFJ6Rt64lI6Vfi3JYBFHd1JA5wIunUNIQvpr/C+bm5u65s9fWBnwe9dISWVc0/DNhQsX6gDwTuuhd3WNYOSGTjjSehGp7EVYsguWuJQfssu51wVTXIIpLsGWlzBgXsSRM5dg6Hk6uk787Zb39gHA7NlzDM7xoM4Yli5fvgJSSiRmmbP9HNA0Qm4D6axEc6uJ6eOzuCloQuOOjlneqiUx2BK4lDBwut2DTFaHoXFYGilaHEjMMOdKKXHb4tvw/nvvL9UZ+Lyb6+pw/PjxpOZhsziX0DigcYLG1QaEBD69ZKA7wRHx2/C7BDSNwEi9AEmZGmJJA/1Z9SJM12hwvcYBzgmaj89obW3pr62dGmCcz+cuQ68GgEtdl7oYU40CZwSeW+As1rmy5KzNkbY1WILDlOp71ubgnKA7czVO4NyhwQhcFS7o6urq5pzDMLRqnXEtCACpdCrFHOHlAsTgYEq0nCnj0jnBY6i8KCTLBxbmzB2yPkczmU4lAYAxHtKFECYAPeDzBQZD4GU+motMueXklECWc7QkSaVDGoTAVetz8AGfLwQAQoisbtt2N4BJZaVlpZQjkntdS8w5UFOFni0YLMGhWfny1rbVPVuoOVKyK9ZeTrMsUl7qAHdzkPyktzeG2tqbw8KihCQlPjVUl2hLBkswmDZD1mJIWxwDWTXSFkfWUs8sZ64QzlqHjiRA2tQ7ZcqUYCwWgyT6hBNjb+3ZvQehUIi52tje3M6FyHHIYNkOqM2RsTjS2cuAs+pe1uYKPLcBkduA+m60sH1+v5/t3fsWGGP/x6VkjR98cAQAMNc7bXJepAyWzWHaimjW4siYDGmTY8DkGMhqapgcaVM9yw5ugMOyeX4DkmGub1otABz/6DiI2O94IpE4E+3p+aCzsxP333PfAvOi2G8JBtMRbU68GZMj44Ao0BzXmgOsRk7spq1oWILB6rQP3nt3/byLnZ2IxWKH4/H4pxoAeFzuC21tretW3rUKnk5mtWiflzAGxhgDQ66IYyrnOnqzBFfDZjAdLk1HMnkpMWRNLldmFomamtrIL/71F+iPJ/8mnc2e4QDQm0jsOXfu3L6TJ0/ivtX3T607M26P6SzMWI5eB7ktPHLPc/MV5xwTjpe9sfLOu2pOHD+JCxc+fyeWSLyZdzCoWsvjNpqef/6F8KTJU/DDLT/a3jM90eDWCS5dqmDvxF7NCRSAOikQhCuMUXHMEDjm3v7jb/+oIRrtxpMbnuzNmvatiUSi7QpgAAiFQneXlZbs3rGjUauorMSmLc+8dShy7HbDELqeA3bC4GCScHxWSMDOgVuaPb2t+t3vPfK9O1P9A/j7v3vC7ov318fj8bdyWFf8YCSbzZ7VNHb+tVdfrV911ypt/bcfq52J2uTBg+//LhWwZ0nJYTtWf6WrcccDGFgLdn5nwkPVD9Q/MLOzsxNPbvhhNpUc+G5vPL7jcqxBjonozwEsBzD5lVde9jy5YcPqTZufKX90/WOwbRv7330nsffDt08dSB41EkZyHPfwmwBAZuTFsBm48GeuWfai2oUzp02fFjKzJhp3NuLFF/+765e//Pfd31q71gLwGYC3GWNNAMCIaBKAJwBUO3uQnZ2d/MyZNv1vn/j+LUuXLq/Z/MyzCIfDTmxW8Y+IVFyWqjKRQkDYNqKxGDb97GkcOXLk7LZt/9F8c12dqKqqYM4LYALQCWAbI6J/A1AGgKK9vSBhoa8vEe+N9TwejcZYU1MTfrN9O6puqkJDw0NYtnwFpk6dCsZUMrFtG22trTiw/11s3/4aotEo1jQ04NZFt6KsrJTCoZKtJaWRiGG4KBKJ5BJWnw4gDedAx+0yMJCywLnQGWOSMabV1NbikUfX40J7B367sxFbt25DMhGHZZkgAC7DhWAojOpx4zF3wS0YP64aVZUVYCoQSN2la4bhIsNlcOS73H5GRBUAHgcwBYABAD09PZROp1gq2V8WTybq4vH4xEQ8oSWSSfSnUkinM7As9RdUw9Dh9XoR8PsQCgYRCodESTj0x1Aw2OrxBXsDgYBdXl6eM2IB4CyAbZcb12wASwBMB1Dq7C4ACJZIJHstM5PWdC2TTmcom80wEtySAFwupum6wbxeDxeCuT0et8/v94UBTTrSJABRAKcAHGCMnbrKjy/bRBjAHAATAFQ5NuAF4IFqAtyOKzKo83MLgAkgA2AAQB+ADgCfAzjBGIsPxfh/6wbDK7xbMFYAAAAASUVORK5CYII=\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAyUSURBVGiB7Zp7kFRVesB/5/S9PdMz/ZoHMwo4MICDuoGVIYICIuzGcn0vC+oWGuNjs8mua9ySP4wpgyaiVVupbHYTsLJmNT7WNXExwqqzrq8g4oNxdXUgyEMQARmZd3fPTE/3vfd8+ePenhlgBsFlrFSqb9Wpvn3vd77f+b7zne87ffsqjv+wE4nYDQqWl5aWfDUcLqkAyOUHunID+Q8EnkilMo8C7gnoPPaRTCYnVyQT71+1bKl80PK+HGw9KPv27ZPde3bLjp075NVXX5FLL7lYKpLx9yoqKuqOR6f6PIFYLFZtW7r54YcfqV+4aBEdHe3ywm+e39eb6etzPZfS0kj5woUX1EUipWrj6xtZedddu11P5mYymc5j6Q19HrgsUrL67r/7+8VLly7j8cce3d3X29vZ0DB9yplnfWXcrFmzxjU2NiaBXevWrUsv/trXKmzbqnz/9+9VDuTyz35hi2OxWHV1ZbJ1245d1ltvvpFtb293Kyoq7LKystKysnLKy8soKyujtDTCxx/vSW3fsT3c0NAQWbpkiZvp7a9Np9Ndo+nWxwJrLYvmzV9gAaxbt/75urrxd592Wp0Oh0tWHSkbiUQSv3unuQlgxoyZltZm0TF1H+umUnrC1KlTAaipqUpESmMzFIRjsVj3SPJTpkyJA0ycOBGMnviFwSISLolEAAiHbftYsgAlJREbwA6HESUlXxg8lkcRXAQXwUVwEVwEF8FFcBH8/xhsnZC0ksw49eQPI5mmNtP54ccAIvqgqbz4aYn8zYoTUXXcFnueyZ8eXtleZt75iQnpU0VUvYiqB5mvu5p+XH9w8RtgnJMOLut/7rd4+fpRBcS52hz65csnHdxQ8clZnyuT3NV40sHRUnfq58mUWFJ70sEn+yiCi+AiuAgugovgIrgILoKL4CK4CC6Ci+D/Q+Djf/higk8Jzs0IMjIGYDGAp0AUeBbiHf3Xs/HGAHyYlYaRX0EYC4txNeIFugvWHyXzua8cnDjYGMBoQIFhRFfLmLjaCxqAw8iuHing/nCwGlLuMrKrveNfnccPFnyLtQ8c0a1jElye8sGFAYwUSCN54Q8GB4ljKKpHkBmLOZbB4FLgjhLVYxNcDFnkMXJUj03m0kOKR0sgYzLHRvlwpcDYI7oaGYvl5HB4ZRrJ1cf9fP5E/5NwQUKM7uoTOI4/ql38kmgUOCMnEHMCL819sag2jJJAxgIs+HNY6PGlpUxXDQWXw5dXjxH8SFZBPf7SyqKrMQLKG7b/OkpmTBJI0BSjbwTGYo6Ni5+ZjMJDj1wkxmQ5iV+VsBh9BzImKbNQFhWjp8wx21c7dKIV9A94IxaJsdplZt9574JQVcUdpr3rzlEHdzLASslpg19EofLMMa3dc0Z9c9YMXT+s7/GCo9FojWWph87+6tmX3XTTzT7XA/F4xutXr4fyOuQZVQUQ0tLphY1nlcn5YqgAuOyyy3inefOtH+36aLJr5Obe3t72o4w68kIsFptuW7pp5d33TPne928hm83yLz+6b9PVb/4niRK9QNfUoquqUaUREEEG+jGd7Zi2Dnpy3qYHGr7OFdcsX2BZFs899ywP/fznu11PLslkMjtHBScSiXrL0m+uXr3mlEWLFrN58+auxD+u2HZWhb0gcvkyShZ/Ax2N+70KPcVvJpMm999NZJ99mi1dzsb3rviLGbNmz6rY0rKFVavubTWG83p6ej4psAbfr66trS03xtlw98p76s+bN5+nnvzFtouevK/s1AnJM+I/vB37j6aDziJeCtxhzUkhTgoYwJpchz3zbJI7fj/pzA829f6iR/bPPW9e9aS6utjbb715YWVl1SOZTMY5DGzb6scXf+OSS6+48kqanntu55+99shkOyLx8uuvIjSuDEzq6Ob5TdzgPJ9GhT2sCbV4W1vK57R+FP9lOrT33PnzKjOZTM2OD7dFB3L5FwaDq6KifGYiXvn95ddey4fbtmWv2fhIiVUqpbpMEao2SH4fiKCMgAbRggSuVkKwEQz22q4iVKtQEYUtJvzdlvX6+bq67PJrr41sbm6+VVv8W1dX7/9oADH6b//0+us1QO/jD6xPhGWSCgsqLJj8PsTdjzj7Ma7fxDkAzn5wjry+H3H2YfL7UGGDCguJEqnPPf3YOoDrrrtOe56+C8CKRqPjotHoN+fMmcObb7zRelsk9W1lC4QFCRlM9yfoKnsoEgOLVWCxDLfYBRwwnXmwDIQVyoMbo6lrfrq5+dCsxsbaaHlkqTFSpUMhvjV79mwLwHvjldewBGxQlqBswXn3Y6T/EDhtiNOGuG2I2444QXPb/WtOGzhtmL7PcN7di7IFFegiJDq3+ZVXAWbMmGlrzRJLKc6/4IJFGGO4MdQ+gxAQEn/2LcH0u+Sa27HO0IRq/V+MSqnBOUZARMAD75DB2w4mq8AKWkggpPiOtJ3dYgznzTuPt996+3xLoc8+vaGBlpaWzFybrygtqCPgeODtcTFtBl1hUBHfGgl+wNGv8FIayWjE6KCfD1UhBVqotPWZO3Zs7506dVpUaT1Lh21rPED7oUNtKH8OUYLSoHTwWRiEAsmBDIA4gCPIAJh8YL3lyw7vi5JAJ7QdamvXWmPbofGW0qEYQL4/0zeYjdTRTQ0Oxp9/Svx9jvKAkBocsCh1dP9AZ76vNwOglI5bnuflAaukPBo9bM8UpMIjvxeiWAUbATHK3/yNJM/h30vKozEAz/Ny2nXddoCKyqrKwc5GDYFMUJmM8peLqyCvkH6FZP1zXP+eGBXIFvQcrquyqroyALdrxGzv7u5i6rTTE3lX0gUL/DIYPPfwFDh+k5xCBhSS1Ui/9s9zQ/cLz0rEGxqEGMWAK92T6yfHu7q6MCLbtSj1UtPzTcTjcfW0E3t5EBSkv0FgPgAMQgtWa/9azpcZHICrhvR48B+52CvRaFS9/PJLKKVe1Mao9e+++zsAtk9rnIwbLBFHIQ5IACWvkJxGBjSSDeDZ4HxAIznty+SV38chGIA/PXumzZoK0PJBCyLq1zqdTn/U2dHxbmtrKxddfmXj1r7QRr9jMH/5Ye4d8OdV+odZ3F+AqyG3F/oFelr62PQnl14667PWVrq6ut5JpVJ7giLBygfWrMYOh3ll/pLx4iojR7p3QMGgpQX4kPUE8OFuF0chrjIvzL78VDsc5sEHH0SLWkmQLuhOp5v27t376tatW7nk8iun/UN8VhM5BblASS5w53BowdXD4L7Lg8EG7Z6SM36z+MILp25p2cqBA/s3dKXTLxRSBeDvtUpL7M0PPfRwYtLken791z9Y++fevmWE/WJBIelbgJbDtz4mePblBksrcPU/ubVrF65Yuayzs50Vt6/ozuXduel0etdhYIB4PH5RVWXy+WeeWR8aV1PDz+6/56W//PDFxbpELGULgwVEcwSYoWXkKExOuatqGl9b8p3vfb2vt5/b/uoWtyfVe0kqlXqpwDpql1lVlbwhUhr52VNPrQ3PPuccNm16PbXrR3f+9pvm0NV+pWEwhQKIqKHnm57iV9nydc6Smxc1zm5MHvj0AHfecUeuv7f/u509PY8N5wyCReRcYCEw6YknHi9bcfvtl9276r7qG2+6Gdd12bhhQ/rghhe3TdmywT4l2zkhEeIUgJTLZ62RygPbT5/rlv/xvLOmnzE9ns/lWb9uPY8++u9tP/3JPzd9e/nyLLAXeE0ptRlAicgk4BZgfDAGc/DgQb1790fWrT+45Zz58xdMue+++0kkk/5N8RO2iPiZ0BiMCMbz8FyXzq4u7l91L5ub3969Zs2/Np/eMM2rrT21YKQBPgPWKBFZAyQA093drTzPobu7uyPV3XNbR2enam5uZu3atdTW1LDsqqtYeMEipk2b5m8GANd12bVzJ69vfI2n1/6Kjo5OvrVsKefOPZeqqkpJJCtXJ5OJinBpRJLxeOF3bI8FZIAYoEN2SHmeJ6GQ2CiMUipUP2UK199wI59+2sp/rVvP6tVryKRTOE4eAcJ2mFg8wfgJE5nZeA4TJ4yntmYcSimUUsaydMi2wxIKKTXM6n4lIuMCV08m2O52dHSQzfbpvkxvZSqTbkinUnWpVDqUzvTS29dHNpvFcfy6aNsWkUgp0fJyYrEYiUTcSybin8RjiZ2lZeXd0WjUra6uDg2L/z3A6uHBNQNYAEwHqvAXTTl4Kp3O9HhOvk+FGMhmHXHdHGLEE8CytNY6rCKRsPY8VRoOh8tisfIkhFxgIAB2AtuA15VS20ZcTsEgEsBM4DTgFKASiAClQAnBig7EC8/8BoAc0AekgE+B/cAWpVTqSMb/AlY1WXIncMcxAAAAAElFTkSuQmCC\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAxNSURBVGiB7Zp7kFTllcB/5/a93dMz3T0PemYIDgoCPhZ5iaD4wNkFjQjRRMlLTNbSlKlyzZpobSVbFRPUbNVWSRCWuKvlxqybtbIrukp4SATZCAgospEBgeElj4EZ5t3d0+++37d/9O2ZnqEHQZzZSlXfqlMz/c253+875zvfOefeHuH8L6u83P+AwH0lJZ4pbrenEiCVSnYmEsndGl4NhSKvAJkLmPPcV0VFxZjKivKPv77wXr274WN9uvm0PnHihD5y9IhuPNioN216Vy+Yf6eurAj8b2Vl5aXnM6d8loLf7w9apvHhyy//29jZ9fW0t7fpdWtWN7Wdao4qpaiqDpbdXF9fV1paKpu3bGbxk08eSWXU9ZFIpOPirC33v7xs+TIdiUT0Pz239NjeaTOTHXXjdb4cuP6W5DOLFx/7aNdH+oknfqQryv0vXZTFfr8/GKyqaN7XeMhc//ba6NSfPFXqS6fESJ29jdGAX69+9KHY9OnTyxbec08mHInWhsPhzsHmNs4FNgxdf+NNN5sAh3/7n40dCxeKedUsOr6x8CzdsnBEQu9sPABwzTWTTMNQ9eec+1x/FDEuGTduHABXtreOKutJYyiFqq4tqD+5O3wJQF1dHSij7nODtdZuj9cLgMfGOpcuQInSFoDldqNFez43eCivIrgILoKL4CK4CC6Ci+AiuAgugovgIrgILoKL4CK4CC6Ci+A/B7B5vor6Mz4PNnbRYAAtoCQLUMMFVobuBWOALWdjVIGxiwbbZC3WkrXWLqAzJBZrR5T0LWTgdSHfdF1YcIlG57t8oM5nfov1OcCKPmDW1Rfi2IsA5yI5F9WFXF0o0i8arARwggsBu4BbhwaM6g0ujXY+9b+GLqrzLR5E5wsH2ziB5QRXoW8lCy3mosH553iwlDlEe9znai2DpMyhAJ+PxUNTJMhZm51+WM9xvsWFXD2kx0nl9rjQ4oYC3C+4BoEMnasl39Vn6wxRdcqbXApXpwupWBcEVgLKGLw6DU1w5bkaCjcChcYuHozuLYtqEFfroXC1TZ67GcbjlEuZWjSIHr6ozjZ7/y/VSWOLdgJIF9zjQl3JFwDOXn1lsYDOULm6X+YaROcLB6s8+LC2tzqvoc+Wx0L2nT/6wlIm5y6LQ9bs5TLXsO5x7jG192lxuJq9bCOg0aIRGcYEkt9lCsPp6lxlMsBlFE4ghcYuGoxznHKFYNjKYq7Zy5XFYW32lMtCBGzbLlwWLwB83m/2NNC44R0iFaP503+8jO1UqHz5wiwW0aNzvysgdPJTQr/7dFD9fHD+vecN9vl8NaYpv546ZeqCBx98CMhGbPXEqZRfcTWmyySTjuO2TMora/B4Sji+832OnWoGYMGCBez88IMfHD50eExG6Yd6enraBjJcAwf8fv+Vbsv1Pz9f/NT1y1esQCnNPz6zeGuy6WBN+MRRrwp1YMR6MOIJMqEuOj49xNFd2zh5aD9SVpr44PCJXVOmXXvpHfPm4fP7rtz98Z/usSz3+lQq1e/fnvuFSHl5+VjTNLb96lfPj6yv/0t2bN/eufJnj+37Uql1c/1Xv8WM279CaZn/rJcBGoj1hNm+7k22rF5JcyK1edp3Hps0bfq0yj0Ne/jFL55pVopZ3d3dx88C19bWlqVS8Z2Lf/7U1XNvu51Vb72x7/irz9fUBEcEv/03PyFYPRJDgZHt9XpvzG8QlAFnWppY+S9LaOnsaPPOWdhxx7z5V320cydLl/7yE2+pb+bp06dj/VxtWbJ03h13zr/r7rtZu2bNwVP/9cKYMiHwtW8+QNAbwOiOIN09SCiChCKQL+EIKhxBhcN4EGpGjuJww66yxNH9gePac+zGm26sikQiNY379/kSydT63uCqrCybXB6oeuS+RYvYv29f/OTKFz1+dIlXXFQrCznRjNhkRfdJzmIMEAExsqbUmh68holWGXf43deMg6NHJ+5btKjkgw8//IFh8lJnZ88nBoBWxpPf+e53DYC1Ly5bVSb6Mo8WSrQgx5uRY6cHSDMcz0q/vx/PSTNeJXi04EOPfe93L70JcP/99xu2bfwUwPT5fNU+n++rM2fO5P3332+uS3V9y9KCG8FSmtjRo3iN0uz+qqylemDnLhpDQDsFJGrHMG2F2xAyGi5Nhr65Y8f21unTZ9T4yrz3KqVHGC4X91x33XUmwN7N775nApbuk90nD5BpbUbaWqG9Dd3eju5o6y/t7dDehrS1kmltYffJ/ViA25nDBcbeLZs2AUyaNNkyDL5minDL7Nm3opSiNtQ0yUQwESydlXg6xc70Sf5CewliYSD9TqHu/anpIMUnJIiLjSVCGjAFTA21odNTlFLMunEWO7bvuMUUjKkTrriCvXv3RDyiJxpacGVXSc56W2uO6DhtKkmFFsocHchmtKhoukURNrJPG5YDdAEuDYaAV/TVjY0HesaNG+8Tw5hmuC1zFEBLS0urkQ3QPtFgILgQTC0IkAZSgEJQCClnTBwdF4KBOPf2iQBnzrS2GYaBZblGmWK4/ADxWCzqoS85iDOZDFiMS2ddV5Kz2EkGhgwECYLOzqOzxy0W7YkAiBgBw7btFIC3tMw/2JsrnS9OI5B2pPdt0AC9gdVZZxkBANu2k0Ymk2kDCI6oqsw1c/nNu8rVW8l+2ZFCkxRNzMhKUjQpNBlnv23nXfbAeTRQHayudMBtBlod6OrqZNz4CeVprcKqd4KsZBxgGk1KNEmBmGiijsScsZRo0s4CMnn3284CMqJCY8aOCXR2dqK0PmBokQ3r1q7D7/dLq7tyY8axMCOatDNZFqhJiCbuWNsLNrJjCUcnt4C0ZOew0WTQnDYr3/X5fLJx4wZE5B1DKVm1a9dHAIyYesPYjEBa+vYwJZAUSAgkHAtjookaWcl9Togm4eim8u5PS9YDNVNmXg7QsLsBreX3RjgcPtzW1rarubmZ+QvumtahXJvzrUzmWRvrZ61yxNnvPKuTA6xvt13bvjxv/tSW5mY6Ozt3hkKhoy4Ar6ek6dChg4vm3nY7oZJAJnG4oUIQESdD5Ud0v30XSBlZC1OGdjyTA/darwK3LcxcPm585ZJnl9ATinwvnkweNgC6wuF1x44d27R3714WfOWucZGrb3g7kee+eJ6LewPLcXU0bzwuuf2G3P3NoyevnzP3tsv3NOylqenkHzvD4fWQ197aikeW/nJJd1dnJ4//9On57V+a8Hoib7K4kQeUAWL0D7RcsJ2oqHv9wUcfu7Orq5MVK5Z3KS0P53j96lsgEPjyiKqKtW/891uu2tpalvzDMxsTW96s9yhMC8HUOCkxm07JO/fZk5A9dkmDTOSqWe/99fcfmRPtifHY3z6a6Q5F7gyFQhsKggFGjKh4wFviffG11153T59xHVu3bg3968/+7g9V3ae+0Zv0kX49l3ISjA2ccpe/NXvR9+uvnX5tRdOpJv7+xz9OxnpiD3d0d/97PqcXrLWeBcwGLnv11d96n3j88QVPPf108KHvPUwmk+HttWu71q96Y0dozzajJBUfXyqMA4gpfShmeY54JkzX19/6VzfMmDmjMpPOsOqtVbzyym9alz23fM23Fy1KACeAP4rIBwCitb4MeAQY5SxEt7a2qIaGBn70wx+OTKXTc5Y+t8w1d85cdN5KtdbYSqGVImPbJOIxotEo6/+wniXPPmsH/L4Ny5etaJk46Rqprq7JPTgooBn4Z9FaPw9UAHR1dSnbTsuZMy1GMpnItLZ2GFu3bq5d/fvVc0ZUjZB7F36d2fW3MmHCFZguF0pr0uk0Bxsb2bL5PV5fuZLuUEjfdffdG2+66ebW6mCVLvP5qa4OAoYEg8Gcg7tNIAIEADHdJnbcxmNZ6UQ05nK7TT1x4sRYRVV1/FTTqdLVa9bywgsvEImESKfSAFiWhT9QzqhL6rh25g3UjbokPnJkTaKkxFRaa8NtGbaIy+Up8eS2VgEx0VpXO66+HKfdbW9vV93d7RKNJl3xeNQOd4d1Mp0i3B3yRCKRsmgiYSVTaa9orS23lfR5vany8vKYLxCIeyxLKqoqtddbKh6PSVVVtQ4Gg5IHPQI8nx9ck4CbgSuBarJnvARsiUai4XBPmGQyqbWGRCxh2VrZAKYYLtNjZUyXSxsuU6oqyg1fwO91nhUSzvQdwB5gm4h8UvA4OYsoByYDY4EaoBLwAN7sYiDvZ4LsqUo60uNIK3AY2CMioYGM/wPREY0iGUY58wAAAABJRU5ErkJggg==\"],\"useMarkerImageFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', amount = percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"markerImageFunction\":\"var type = dsData[dsIndex]['type'];\\nif (type == 'thermomether') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"color\":\"#fe7568\",\"showTooltip\":true,\"autocloseTooltip\":true,\"labelFunction\":\"var deviceType = dsData[dsIndex]['deviceType'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}, ${energy:2} kWt';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}, ${temperature:2} °C';\\r\\n }\\r\\n}\",\"tooltipFunction\":\"var deviceType = dsData[dsIndex]['deviceType'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}
Energy: ${energy:2} kWt
';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}
Temperature: ${temperature:2} °C
';\\r\\n }\\r\\n}\",\"provider\":\"google-map\",\"defaultCenterPosition\":\"0,0\",\"showTooltipAction\":\"click\"},\"title\":\"Google Maps\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" } }, { - "alias": "test", - "name": "Trip Animation", + "alias": "openstreetmap", + "name": "OpenStreetMap", "descriptor": { - "type": "timeseries", - "sizeX": 10, - "sizeY": 6.5, + "type": "latest", + "sizeX": 8.5, + "sizeY": 6, "resources": [], - "templateHtml": "", - "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", - "controllerScript": " self.onInit = function() {\n var $scope = self.ctx.$scope;\n $scope.self = self;\n }\n \n \n self.actionSources = function () {\n return {\n 'tooltipAction': {\n name: 'widget-action.tooltip-tag-action',\n multiple: false\n }\n }\n };\n", - "settingsSchema": "{\n \"schema\": {\n \"title\": \"Openstreet Map Configuration\",\n \"type\": \"object\",\n \"properties\": {\n \"mapProvider\": {\n \"title\": \"Map provider\",\n \"type\": \"string\",\n \"default\": \"OpenStreetMap.Mapnik\"\n },\n \"normalizationStep\": {\n \"title\": \"Normalization data step (ms)\",\n \"type\": \"number\",\n \"default\": 1000\n },\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"latitude\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"longitude\"\n },\n \"polKeyName\": {\n \"title\": \"Polygon key name\",\n \"type\": \"string\",\n \"default\": \"coordinates\"\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"label\": {\n \"title\": \"Label (pattern examples: '${entityName}', '${entityName}: (Text ${keyName} units.)' )\",\n \"type\": \"string\",\n \"default\": \"${entityName}\"\n },\n \"useLabelFunction\": {\n \"title\": \"Use label function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"labelFunction\": {\n \"title\": \"Label function: f(data, dsData, dsIndex)\",\n \"type\": \"string\"\n },\n \"showTooltip\": {\n \"title\": \"Show tooltip\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"tooltipColor\": {\n \"title\": \"Tooltip background color\",\n \"type\": \"string\",\n \"default\": \"#fff\"\n },\n \"tooltipFontColor\": {\n \"title\": \"Tooltip font color\",\n \"type\": \"string\",\n \"default\": \"#000\"\n },\n \"tooltipOpacity\": {\n \"title\": \"Tooltip opacity (0-1)\",\n \"type\": \"number\",\n \"default\": 1\n },\n \"tooltipPattern\": {\n \"title\": \"Tooltip (for ex. 'Text ${keyName} units.' or Link text')\",\n \"type\": \"string\",\n \"default\": \"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}\"\n },\n \"useTooltipFunction\": {\n \"title\": \"Use tooltip function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"tooltipFunction\": {\n \"title\": \"Tooltip function: f(data, dsData, dsIndex)\",\n \"type\": \"string\"\n },\n \"color\": {\n \"title\": \"Path color\",\n \"type\": \"string\"\n },\n \"strokeWeight\": {\n \"title\": \"Stroke weight\",\n \"type\": \"number\",\n \"default\": 2\n },\n \"strokeOpacity\": {\n \"title\": \"Stroke opacity\",\n \"type\": \"number\",\n \"default\": 1\n },\n \"useColorFunction\": {\n \"title\": \"Use path color function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"colorFunction\": {\n \"title\": \"Path color function: f(data, dsData, dsIndex)\",\n \"type\": \"string\"\n },\n \"usePolylineDecorator\": {\n \"title\": \"Use path decorator\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"decoratorSymbol\": {\n \"title\": \"Decorator symbol\",\n \"type\": \"string\",\n \"default\": \"arrowHead\"\n },\n \"decoratorSymbolSize\": {\n \"title\": \"Decorator symbol size (px)\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"useDecoratorCustomColor\": {\n \"title\": \"Use path decorator custom color\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"decoratorCustomColor\": {\n \"title\": \"Decorator custom color\",\n \"type\": \"string\",\n \"default\": \"#000\"\n },\n \"decoratorOffset\": {\n \"title\": \"Decorator offset\",\n \"type\": \"string\",\n \"default\": \"20px\"\n },\n \"endDecoratorOffset\": {\n \"title\": \"End decorator offset\",\n \"type\": \"string\",\n \"default\": \"20px\"\n },\n \"decoratorRepeat\": {\n \"title\": \"Decorator repeat\",\n \"type\": \"string\",\n \"default\": \"20px\"\n },\n \"showPolygon\": {\n \"title\": \"Show polygon\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"polygonTooltipPattern\": {\n \"title\": \"Tooltip (for ex. 'Text ${keyName} units.' or Link text')\",\n \"type\": \"string\",\n \"default\": \"${entityName}

TimeStamp: ${ts:7}\"\n },\n \"usePolygonTooltipFunction\": {\n \"title\": \"Use polygon tooltip function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"polygonTooltipFunction\": {\n \"title\": \"Polygon tooltip function: f(data, dsData, dsIndex)\",\n \"type\": \"string\"\n },\n \"polygonColor\": {\n \"title\": \"Polygon color\",\n \"type\": \"string\"\n },\n \"polygonOpacity\": {\n \"title\": \"Polygon opacity\",\n \"type\": \"number\",\n \"default\": 0.5\n },\n \"polygonStrokeColor\": {\n \"title\": \"Polygon border color\",\n \"type\": \"string\"\n },\n \"polygonStrokeOpacity\": {\n \"title\": \"Polygon border opacity\",\n \"type\": \"number\",\n \"default\": 1\n },\n \"polygonStrokeWeight\": {\n \"title\": \"Polygon border weight\",\n \"type\": \"number\",\n \"default\": 1\n },\n \"usePolygonColorFunction\": {\n \"title\": \"Use polygon color function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"polygonColorFunction\": {\n \"title\": \"Polygon Color function: f(data, dsData, dsIndex)\",\n \"type\": \"string\"\n },\n \"showPoints\": {\n \"title\": \"Show points\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"pointColor\": {\n \"title\": \"Point color\",\n \"type\": \"string\"\n },\n \"pointSize\": {\n \"title\": \"Point size (px)\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"usePointAsAnchor\": {\n \"title\": \"Use point as anchor\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"pointAsAnchorFunction\": {\n \"title\": \"Point as anchor function: f(data, dsData, dsIndex)\",\n \"type\": \"string\"\n },\n \"pointTooltipOnRightPanel\": {\n \"title\": \"Independant point tooltip\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"autocloseTooltip\": {\n \"title\": \"Auto-close point popup\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"markerImage\": {\n \"title\": \"Custom marker image\",\n \"type\": \"string\"\n },\n \"markerImageSize\": {\n \"title\": \"Custom marker image size (px)\",\n \"type\": \"number\",\n \"default\": 34\n },\n \"rotationAngle\": {\n \"title\": \"Set additional rotation angle for marker (deg)\",\n \"type\": \"number\",\n \"default\": 180\n },\n \"useMarkerImageFunction\":{\n \"title\":\"Use marker image function\",\n \"type\":\"boolean\",\n \"default\":false\n },\n \"markerImageFunction\":{\n \"title\":\"Marker image function: f(data, images, dsData, dsIndex)\",\n \"type\":\"string\"\n },\n \"markerImages\":{\n \"title\":\"Marker images\",\n \"type\":\"array\",\n \"items\":{\n \"title\":\"Marker image\",\n \"type\":\"string\"\n }\n }\n },\n \"required\": []\n },\n \"form\": [{\n \"key\": \"mapProvider\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [{\n \"value\": \"OpenStreetMap.Mapnik\",\n \"label\": \"OpenStreetMap.Mapnik (Default)\"\n }, {\n \"value\": \"OpenStreetMap.BlackAndWhite\",\n \"label\": \"OpenStreetMap.BlackAndWhite\"\n }, {\n \"value\": \"OpenStreetMap.HOT\",\n \"label\": \"OpenStreetMap.HOT\"\n }, {\n \"value\": \"Esri.WorldStreetMap\",\n \"label\": \"Esri.WorldStreetMap\"\n }, {\n \"value\": \"Esri.WorldTopoMap\",\n \"label\": \"Esri.WorldTopoMap\"\n }, {\n \"value\": \"CartoDB.Positron\",\n \"label\": \"CartoDB.Positron\"\n }, {\n \"value\": \"CartoDB.DarkMatter\",\n \"label\": \"CartoDB.DarkMatter\"\n }]\n }, \"normalizationStep\", \"latKeyName\", \"lngKeyName\", \"polKeyName\", \"showLabel\", \"label\", \"useLabelFunction\", {\n \"key\": \"labelFunction\",\n \"type\": \"javascript\"\n }, \"showTooltip\", {\n \"key\": \"tooltipColor\",\n \"type\": \"color\"\n }, {\n \"key\": \"tooltipFontColor\",\n \"type\": \"color\"\n },\"tooltipOpacity\", {\n \"key\": \"tooltipPattern\",\n \"type\": \"textarea\"\n }, \"useTooltipFunction\", {\n \"key\": \"tooltipFunction\",\n \"type\": \"javascript\"\n }, {\n \"key\": \"color\",\n \"type\": \"color\"\n }, \"useColorFunction\", {\n \"key\": \"colorFunction\",\n \"type\": \"javascript\"\n }, \"usePolylineDecorator\", {\n \"key\": \"decoratorSymbol\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [{\n \"value\": \"arrowHead\",\n \"label\": \"Arrow\"\n }, {\n \"value\": \"dash\",\n \"label\": \"Dash\"\n }]\n }, \"decoratorSymbolSize\", \"useDecoratorCustomColor\", {\n \"key\": \"decoratorCustomColor\",\n \"type\": \"color\"\n }, {\n \"key\": \"decoratorOffset\",\n \"type\": \"textarea\"\n },{\n \"key\": \"endDecoratorOffset\",\n \"type\": \"textarea\"\n }, {\n \"key\": \"decoratorRepeat\",\n \"type\": \"textarea\"\n }, \"strokeWeight\", \"strokeOpacity\", \"showPolygon\", {\n \"key\": \"polygonTooltipPattern\",\n \"type\": \"textarea\"\n },\"usePolygonTooltipFunction\", {\n \"key\": \"polygonTooltipFunction\",\n \"type\": \"javascript\"\n },{\n \"key\": \"polygonColor\",\n \"type\": \"color\"\n },\t\"polygonOpacity\", {\n \"key\": \"polygonStrokeColor\",\n \"type\": \"color\"\n },\t\"polygonStrokeOpacity\",\"polygonStrokeWeight\",\"usePolygonColorFunction\",\t{\n \"key\": \"polygonColorFunction\",\n \"type\": \"javascript\"\n },\"showPoints\",{\n \"key\": \"pointColor\",\n \"type\": \"color\"\n }, \"pointSize\",\"usePointAsAnchor\", {\n \"key\": \"pointAsAnchorFunction\",\n \"type\": \"javascript\"\n },\"pointTooltipOnRightPanel\", \"autocloseTooltip\", {\n \"key\": \"markerImage\",\n \"type\": \"image\"\n }, \"markerImageSize\", \"rotationAngle\",\"useMarkerImageFunction\",\n {\n \"key\":\"markerImageFunction\",\n \"type\":\"javascript\"\n }, {\n \"key\":\"markerImages\",\n \"items\":[\n {\n \"key\":\"markerImages[]\",\n \"type\":\"image\"\n }\n ]\n }]\n}", - "dataKeySettingsSchema": "{}", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var gpsData = [\\n\\t37.771210000,-122.510960000,\\n\\t37.771340000,-122.510980000,\\n\\t37.771340000,-122.510980000,\\n\\t37.771360000,-122.510850000,\\n\\t37.771380000,-122.510550000,\\n\\t37.771400000,-122.509900000,\\n\\t37.771410000,-122.509660000,\\n\\t37.771430000,-122.509360000,\\n\\t37.771430000,-122.509270000,\\n\\t37.771450000,-122.508840000,\\n\\t37.771490000,-122.507880000,\\n\\t37.771490000,-122.507780000,\\n\\t37.771530000,-122.507140000,\\n\\t37.771550000,-122.506690000,\\n\\t37.771560000,-122.506310000,\\n\\t37.771600000,-122.505640000,\\n\\t37.771650000,-122.504540000,\\n\\t37.771670000,-122.503990000,\\n\\t37.771700000,-122.503490000,\\n\\t37.771740000,-122.502430000,\\n\\t37.771790000,-122.501360000,\\n\\t37.771840000,-122.500290000,\\n\\t37.771870000,-122.499730000,\\n\\t37.771890000,-122.499210000,\\n\\t37.771940000,-122.498140000,\\n\\t37.771990000,-122.497070000,\\n\\t37.772000000,-122.496690000,\\n\\t37.772020000,-122.496350000,\\n\\t37.772030000,-122.496110000,\\n\\t37.772040000,-122.496000000,\\n\\t37.772040000,-122.495890000,\\n\\t37.772060000,-122.495440000,\\n\\t37.772090000,-122.494930000,\\n\\t37.772120000,-122.494160000,\\n\\t37.772130000,-122.493860000,\\n\\t37.772180000,-122.492790000,\\n\\t37.772200000,-122.492300000,\\n\\t37.772220000,-122.491840000,\\n\\t37.772230000,-122.491710000,\\n\\t37.772280000,-122.490630000,\\n\\t37.772330000,-122.489560000,\\n\\t37.772330000,-122.489470000,\\n\\t37.772360000,-122.489030000,\\n\\t37.772380000,-122.488490000,\\n\\t37.772430000,-122.487420000,\\n\\t37.772450000,-122.486980000,\\n\\t37.772480000,-122.486360000,\\n\\t37.772520000,-122.485280000,\\n\\t37.772560000,-122.484400000,\\n\\t37.772570000,-122.484300000,\\n\\t37.772570000,-122.484150000,\\n\\t37.772620000,-122.483140000,\\n\\t37.772680000,-122.482050000,\\n\\t37.772700000,-122.481370000,\\n\\t37.772710000,-122.481000000,\\n\\t37.772730000,-122.480740000,\\n\\t37.772770000,-122.479930000,\\n\\t37.772820000,-122.478860000,\\n\\t37.772870000,-122.477790000,\\n\\t37.772900000,-122.477110000,\\n\\t37.772920000,-122.476710000,\\n\\t37.772960000,-122.475650000,\\n\\t37.772990000,-122.474950000,\\n\\t37.773010000,-122.474580000,\\n\\t37.773060000,-122.473450000,\\n\\t37.773120000,-122.472330000,\\n\\t37.773140000,-122.471850000,\\n\\t37.773140000,-122.471730000,\\n\\t37.773150000,-122.471640000,\\n\\t37.773170000,-122.471260000,\\n\\t37.773190000,-122.470570000,\\n\\t37.773210000,-122.470190000,\\n\\t37.773230000,-122.469770000,\\n\\t37.773250000,-122.469370000,\\n\\t37.773260000,-122.469120000,\\n\\t37.773290000,-122.468490000,\\n\\t37.773300000,-122.468150000,\\n\\t37.773310000,-122.468050000,\\n\\t37.773310000,-122.467940000,\\n\\t37.773320000,-122.467740000,\\n\\t37.773350000,-122.467270000,\\n\\t37.773360000,-122.466980000,\\n\\t37.773360000,-122.466870000,\\n\\t37.773370000,-122.466610000,\\n\\t37.773390000,-122.466300000,\\n\\t37.773400000,-122.466000000,\\n\\t37.773400000,-122.465910000,\\n\\t37.773410000,-122.465790000,\\n\\t37.773430000,-122.465520000,\\n\\t37.773460000,-122.465210000,\\n\\t37.773490000,-122.464980000,\\n\\t37.773500000,-122.464910000,\\n\\t37.773460000,-122.464830000,\\n\\t37.773560000,-122.464070000,\\n\\t37.773580000,-122.463900000,\\n\\t37.773590000,-122.463810000,\\n\\t37.773600000,-122.463780000,\\n\\t37.773610000,-122.463670000,\\n\\t37.773660000,-122.463320000,\\n\\t37.773740000,-122.462700000,\\n\\t37.773770000,-122.462440000,\\n\\t37.773860000,-122.461730000,\\n\\t37.773870000,-122.461640000,\\n\\t37.773920000,-122.461260000,\\n\\t37.773970000,-122.460890000,\\n\\t37.774010000,-122.460570000,\\n\\t37.774110000,-122.459760000,\\n\\t37.774140000,-122.459490000,\\n\\t37.774270000,-122.458520000,\\n\\t37.774270000,-122.458440000,\\n\\t37.774270000,-122.458380000,\\n\\t37.774320000,-122.458270000,\\n\\t37.774340000,-122.458050000,\\n\\t37.774510000,-122.456680000,\\n\\t37.774560000,-122.456310000,\\n\\t37.774700000,-122.455280000,\\n\\t37.774760000,-122.454780000,\\n\\t37.774770000,-122.454670000,\\n\\t37.774770000,-122.454670000,\\n\\t37.774670000,-122.454650000,\\n\\t37.774670000,-122.454650000,\\n\\t37.774580000,-122.454640000,\\n\\t37.774300000,-122.454580000,\\n\\t37.774190000,-122.454560000,\\n\\t37.773700000,-122.454460000,\\n\\t37.772910000,-122.454310000,\\n\\t37.772620000,-122.454260000,\\n\\t37.772430000,-122.454220000,\\n\\t37.771980000,-122.454110000,\\n\\t37.771910000,-122.454100000,\\n\\t37.771760000,-122.454060000,\\n\\t37.771690000,-122.454050000,\\n\\t37.771620000,-122.454030000,\\n\\t37.771530000,-122.454010000,\\n\\t37.771380000,-122.453970000,\\n\\t37.771250000,-122.453950000,\\n\\t37.771100000,-122.453930000,\\n\\t37.771020000,-122.453920000,\\n\\t37.770920000,-122.453900000,\\n\\t37.770810000,-122.453890000,\\n\\t37.770660000,-122.453860000,\\n\\t37.770110000,-122.453750000,\\n\\t37.769560000,-122.453640000,\\n\\t37.769360000,-122.453600000,\\n\\t37.769250000,-122.453580000,\\n\\t37.769180000,-122.453560000,\\n\\t37.769090000,-122.453540000,\\n\\t37.768780000,-122.453480000,\\n\\t37.768250000,-122.453380000,\\n\\t37.768160000,-122.453360000,\\n\\t37.767820000,-122.453290000,\\n\\t37.767310000,-122.453190000,\\n\\t37.767160000,-122.453160000,\\n\\t37.767010000,-122.453130000,\\n\\t37.766760000,-122.453070000,\\n\\t37.766550000,-122.453030000,\\n\\t37.766550000,-122.453030000,\\n\\t37.766390000,-122.452990000,\\n\\t37.766390000,-122.452990000,\\n\\t37.766290000,-122.453720000,\\n\\t37.766180000,-122.454610000,\\n\\t37.766130000,-122.454980000,\\n\\t37.765960000,-122.456290000,\\n\\t37.765960000,-122.456340000,\\n\\t37.765960000,-122.456360000,\\n\\t37.765960000,-122.456380000,\\n\\t37.765960000,-122.456410000,\\n\\t37.765960000,-122.456460000,\\n\\t37.765940000,-122.456630000,\\n\\t37.765930000,-122.456700000,\\n\\t37.765920000,-122.456810000,\\n\\t37.765910000,-122.456930000,\\n\\t37.765910000,-122.457020000,\\n\\t37.765920000,-122.457160000,\\n\\t37.765930000,-122.457270000,\\n\\t37.765940000,-122.457360000,\\n\\t37.765950000,-122.457410000,\\n\\t37.765960000,-122.457470000,\\n\\t37.765980000,-122.457560000,\\n\\t37.766010000,-122.457660000,\\n\\t37.766070000,-122.457830000,\\n\\t37.766070000,-122.457830000,\\n\\t37.766120000,-122.457980000,\\n\\t37.766180000,-122.458180000,\\n\\t37.766190000,-122.458200000,\\n\\t37.766240000,-122.458400000,\\n\\t37.766270000,-122.458530000,\\n\\t37.766290000,-122.458600000,\\n\\t37.766300000,-122.458690000,\\n\\t37.766300000,-122.458880000,\\n\\t37.766300000,-122.458970000,\\n\\t37.766280000,-122.459470000,\\n\\t37.766270000,-122.459520000,\\n\\t37.766270000,-122.459560000,\\n\\t37.766280000,-122.459600000,\\n\\t37.766280000,-122.459630000,\\n\\t37.766290000,-122.459670000,\\n\\t37.766300000,-122.459700000,\\n\\t37.766310000,-122.459730000,\\n\\t37.766320000,-122.459750000,\\n\\t37.766330000,-122.459770000,\\n\\t37.766350000,-122.459800000,\\n\\t37.766390000,-122.459860000,\\n\\t37.766340000,-122.459970000,\\n\\t37.766290000,-122.460150000,\\n\\t37.766290000,-122.460230000,\\n\\t37.766280000,-122.460280000,\\n\\t37.766260000,-122.460330000,\\n\\t37.766250000,-122.460420000,\\n\\t37.766240000,-122.460520000,\\n\\t37.766230000,-122.460670000,\\n\\t37.766230000,-122.460800000,\\n\\t37.766230000,-122.460900000,\\n\\t37.766210000,-122.461110000,\\n\\t37.766170000,-122.462030000,\\n\\t37.766160000,-122.462170000,\\n\\t37.766150000,-122.462520000,\\n\\t37.766120000,-122.463260000,\\n\\t37.766090000,-122.464130000,\\n\\t37.766070000,-122.464350000,\\n\\t37.766070000,-122.464440000,\\n\\t37.766060000,-122.464620000,\\n\\t37.766030000,-122.465400000,\\n\\t37.765980000,-122.466470000,\\n\\t37.765940000,-122.467530000,\\n\\t37.765930000,-122.467680000,\\n\\t37.765890000,-122.468600000,\\n\\t37.765860000,-122.468980000,\\n\\t37.765830000,-122.469660000,\\n\\t37.765780000,-122.470740000,\\n\\t37.765770000,-122.471030000,\\n\\t37.765770000,-122.471140000,\\n\\t37.765760000,-122.471380000,\\n\\t37.765740000,-122.471820000,\\n\\t37.765690000,-122.472950000,\\n\\t37.765680000,-122.473220000,\\n\\t37.765670000,-122.473320000,\\n\\t37.765640000,-122.474070000,\\n\\t37.765590000,-122.475140000,\\n\\t37.765590000,-122.475400000,\\n\\t37.765580000,-122.475520000,\\n\\t37.765550000,-122.476200000,\\n\\t37.765500000,-122.477180000,\\n\\t37.765500000,-122.477280000,\\n\\t37.765490000,-122.477400000,\\n\\t37.765490000,-122.477440000,\\n\\t37.765490000,-122.477440000,\\n\\t37.765490000,-122.477490000,\\n\\t37.765470000,-122.477770000,\\n\\t37.765450000,-122.478340000,\\n\\t37.765450000,-122.478420000,\\n\\t37.765430000,-122.478870000,\\n\\t37.765400000,-122.479430000,\\n\\t37.765380000,-122.479810000,\\n\\t37.765350000,-122.480480000,\\n\\t37.765300000,-122.481570000,\\n\\t37.765300000,-122.481660000,\\n\\t37.765290000,-122.481950000,\\n\\t37.765260000,-122.482640000,\\n\\t37.765220000,-122.483570000,\\n\\t37.765210000,-122.483710000,\\n\\t37.765210000,-122.483810000,\\n\\t37.765190000,-122.484130000,\\n\\t37.765160000,-122.484780000,\\n\\t37.765120000,-122.485860000,\\n\\t37.765110000,-122.485960000,\\n\\t37.765100000,-122.486170000,\\n\\t37.765070000,-122.486930000,\\n\\t37.765020000,-122.488000000,\\n\\t37.765020000,-122.488100000,\\n\\t37.765000000,-122.488360000,\\n\\t37.764980000,-122.488970000,\\n\\t37.764970000,-122.489070000,\\n\\t37.764930000,-122.490150000,\\n\\t37.764920000,-122.490240000,\\n\\t37.764910000,-122.490490000,\\n\\t37.764880000,-122.491210000,\\n\\t37.764830000,-122.492290000,\\n\\t37.764790000,-122.493260000,\\n\\t37.764780000,-122.493350000,\\n\\t37.764740000,-122.494420000,\\n\\t37.764720000,-122.494730000,\\n\\t37.764710000,-122.495500000,\\n\\t37.764700000,-122.495630000,\\n\\t37.764690000,-122.495940000,\\n\\t37.764680000,-122.496260000,\\n\\t37.764670000,-122.496480000,\\n\\t37.764600000,-122.497490000,\\n\\t37.764590000,-122.497650000,\\n\\t37.764550000,-122.498720000,\\n\\t37.764500000,-122.499780000,\\n\\t37.764450000,-122.500870000,\\n\\t37.764440000,-122.501060000,\\n\\t37.764400000,-122.501930000,\\n\\t37.764360000,-122.502990000,\\n\\t37.764310000,-122.504080000,\\n\\t37.764260000,-122.505140000,\\n\\t37.764260000,-122.505250000,\\n\\t37.764220000,-122.506220000,\\n\\t37.764170000,-122.507280000,\\n\\t37.764120000,-122.508360000,\\n\\t37.764100000,-122.508850000,\\n\\t37.764100000,-122.509150000,\\n\\t37.764100000,-122.509440000,\\n\\t37.764090000,-122.509760000,\\n\\t37.764080000,-122.510200000,\\n\\t37.764070000,-122.510320000,\\n\\t37.764060000,-122.510430000,\\n\\t37.764030000,-122.510430000,\\n\\t37.764030000,-122.510430000,\\n\\t37.763960000,-122.510430000,\\n\\t37.763960000,-122.510290000,\\n\\t37.764070000,-122.510320000,\\n\\t37.764170000,-122.510320000,\\n\\t37.764410000,-122.510340000,\\n\\t37.764610000,-122.510350000,\\n\\t37.764780000,-122.510330000,\\n\\t37.764960000,-122.510310000,\\n\\t37.765340000,-122.510280000,\\n\\t37.765570000,-122.510260000,\\n\\t37.765840000,-122.510250000,\\n\\t37.765900000,-122.510250000,\\n\\t37.766140000,-122.510260000,\\n\\t37.766410000,-122.510260000,\\n\\t37.766620000,-122.510270000,\\n\\t37.767040000,-122.510310000,\\n\\t37.767270000,-122.510330000,\\n\\t37.767620000,-122.510390000,\\n\\t37.767670000,-122.510390000,\\n\\t37.767740000,-122.510410000,\\n\\t37.767840000,-122.510430000,\\n\\t37.768280000,-122.510530000,\\n\\t37.768620000,-122.510600000,\\n\\t37.768690000,-122.510620000,\\n\\t37.769020000,-122.510690000,\\n\\t37.769260000,-122.510730000,\\n\\t37.769420000,-122.510750000,\\n\\t37.770010000,-122.510830000,\\n\\t37.770340000,-122.510860000,\\n\\t37.770460000,-122.510870000,\\n\\t37.770930000,-122.510930000,\\n\\t37.770980000,-122.510930000,\\n];\\n let value = gpsData.indexOf(prevValue); \\nreturn gpsData[(value == -1 ? 0 : value + 2)];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var gpsData = [\\n\\t37.771210000,-122.510960000,\\n\\t37.771340000,-122.510980000,\\n\\t37.771340000,-122.510980000,\\n\\t37.771360000,-122.510850000,\\n\\t37.771380000,-122.510550000,\\n\\t37.771400000,-122.509900000,\\n\\t37.771410000,-122.509660000,\\n\\t37.771430000,-122.509360000,\\n\\t37.771430000,-122.509270000,\\n\\t37.771450000,-122.508840000,\\n\\t37.771490000,-122.507880000,\\n\\t37.771490000,-122.507780000,\\n\\t37.771530000,-122.507140000,\\n\\t37.771550000,-122.506690000,\\n\\t37.771560000,-122.506310000,\\n\\t37.771600000,-122.505640000,\\n\\t37.771650000,-122.504540000,\\n\\t37.771670000,-122.503990000,\\n\\t37.771700000,-122.503490000,\\n\\t37.771740000,-122.502430000,\\n\\t37.771790000,-122.501360000,\\n\\t37.771840000,-122.500290000,\\n\\t37.771870000,-122.499730000,\\n\\t37.771890000,-122.499210000,\\n\\t37.771940000,-122.498140000,\\n\\t37.771990000,-122.497070000,\\n\\t37.772000000,-122.496690000,\\n\\t37.772020000,-122.496350000,\\n\\t37.772030000,-122.496110000,\\n\\t37.772040000,-122.496000000,\\n\\t37.772040000,-122.495890000,\\n\\t37.772060000,-122.495440000,\\n\\t37.772090000,-122.494930000,\\n\\t37.772120000,-122.494160000,\\n\\t37.772130000,-122.493860000,\\n\\t37.772180000,-122.492790000,\\n\\t37.772200000,-122.492300000,\\n\\t37.772220000,-122.491840000,\\n\\t37.772230000,-122.491710000,\\n\\t37.772280000,-122.490630000,\\n\\t37.772330000,-122.489560000,\\n\\t37.772330000,-122.489470000,\\n\\t37.772360000,-122.489030000,\\n\\t37.772380000,-122.488490000,\\n\\t37.772430000,-122.487420000,\\n\\t37.772450000,-122.486980000,\\n\\t37.772480000,-122.486360000,\\n\\t37.772520000,-122.485280000,\\n\\t37.772560000,-122.484400000,\\n\\t37.772570000,-122.484300000,\\n\\t37.772570000,-122.484150000,\\n\\t37.772620000,-122.483140000,\\n\\t37.772680000,-122.482050000,\\n\\t37.772700000,-122.481370000,\\n\\t37.772710000,-122.481000000,\\n\\t37.772730000,-122.480740000,\\n\\t37.772770000,-122.479930000,\\n\\t37.772820000,-122.478860000,\\n\\t37.772870000,-122.477790000,\\n\\t37.772900000,-122.477110000,\\n\\t37.772920000,-122.476710000,\\n\\t37.772960000,-122.475650000,\\n\\t37.772990000,-122.474950000,\\n\\t37.773010000,-122.474580000,\\n\\t37.773060000,-122.473450000,\\n\\t37.773120000,-122.472330000,\\n\\t37.773140000,-122.471850000,\\n\\t37.773140000,-122.471730000,\\n\\t37.773150000,-122.471640000,\\n\\t37.773170000,-122.471260000,\\n\\t37.773190000,-122.470570000,\\n\\t37.773210000,-122.470190000,\\n\\t37.773230000,-122.469770000,\\n\\t37.773250000,-122.469370000,\\n\\t37.773260000,-122.469120000,\\n\\t37.773290000,-122.468490000,\\n\\t37.773300000,-122.468150000,\\n\\t37.773310000,-122.468050000,\\n\\t37.773310000,-122.467940000,\\n\\t37.773320000,-122.467740000,\\n\\t37.773350000,-122.467270000,\\n\\t37.773360000,-122.466980000,\\n\\t37.773360000,-122.466870000,\\n\\t37.773370000,-122.466610000,\\n\\t37.773390000,-122.466300000,\\n\\t37.773400000,-122.466000000,\\n\\t37.773400000,-122.465910000,\\n\\t37.773410000,-122.465790000,\\n\\t37.773430000,-122.465520000,\\n\\t37.773460000,-122.465210000,\\n\\t37.773490000,-122.464980000,\\n\\t37.773500000,-122.464910000,\\n\\t37.773460000,-122.464830000,\\n\\t37.773560000,-122.464070000,\\n\\t37.773580000,-122.463900000,\\n\\t37.773590000,-122.463810000,\\n\\t37.773600000,-122.463780000,\\n\\t37.773610000,-122.463670000,\\n\\t37.773660000,-122.463320000,\\n\\t37.773740000,-122.462700000,\\n\\t37.773770000,-122.462440000,\\n\\t37.773860000,-122.461730000,\\n\\t37.773870000,-122.461640000,\\n\\t37.773920000,-122.461260000,\\n\\t37.773970000,-122.460890000,\\n\\t37.774010000,-122.460570000,\\n\\t37.774110000,-122.459760000,\\n\\t37.774140000,-122.459490000,\\n\\t37.774270000,-122.458520000,\\n\\t37.774270000,-122.458440000,\\n\\t37.774270000,-122.458380000,\\n\\t37.774320000,-122.458270000,\\n\\t37.774340000,-122.458050000,\\n\\t37.774510000,-122.456680000,\\n\\t37.774560000,-122.456310000,\\n\\t37.774700000,-122.455280000,\\n\\t37.774760000,-122.454780000,\\n\\t37.774770000,-122.454670000,\\n\\t37.774770000,-122.454670000,\\n\\t37.774670000,-122.454650000,\\n\\t37.774670000,-122.454650000,\\n\\t37.774580000,-122.454640000,\\n\\t37.774300000,-122.454580000,\\n\\t37.774190000,-122.454560000,\\n\\t37.773700000,-122.454460000,\\n\\t37.772910000,-122.454310000,\\n\\t37.772620000,-122.454260000,\\n\\t37.772430000,-122.454220000,\\n\\t37.771980000,-122.454110000,\\n\\t37.771910000,-122.454100000,\\n\\t37.771760000,-122.454060000,\\n\\t37.771690000,-122.454050000,\\n\\t37.771620000,-122.454030000,\\n\\t37.771530000,-122.454010000,\\n\\t37.771380000,-122.453970000,\\n\\t37.771250000,-122.453950000,\\n\\t37.771100000,-122.453930000,\\n\\t37.771020000,-122.453920000,\\n\\t37.770920000,-122.453900000,\\n\\t37.770810000,-122.453890000,\\n\\t37.770660000,-122.453860000,\\n\\t37.770110000,-122.453750000,\\n\\t37.769560000,-122.453640000,\\n\\t37.769360000,-122.453600000,\\n\\t37.769250000,-122.453580000,\\n\\t37.769180000,-122.453560000,\\n\\t37.769090000,-122.453540000,\\n\\t37.768780000,-122.453480000,\\n\\t37.768250000,-122.453380000,\\n\\t37.768160000,-122.453360000,\\n\\t37.767820000,-122.453290000,\\n\\t37.767310000,-122.453190000,\\n\\t37.767160000,-122.453160000,\\n\\t37.767010000,-122.453130000,\\n\\t37.766760000,-122.453070000,\\n\\t37.766550000,-122.453030000,\\n\\t37.766550000,-122.453030000,\\n\\t37.766390000,-122.452990000,\\n\\t37.766390000,-122.452990000,\\n\\t37.766290000,-122.453720000,\\n\\t37.766180000,-122.454610000,\\n\\t37.766130000,-122.454980000,\\n\\t37.765960000,-122.456290000,\\n\\t37.765960000,-122.456340000,\\n\\t37.765960000,-122.456360000,\\n\\t37.765960000,-122.456380000,\\n\\t37.765960000,-122.456410000,\\n\\t37.765960000,-122.456460000,\\n\\t37.765940000,-122.456630000,\\n\\t37.765930000,-122.456700000,\\n\\t37.765920000,-122.456810000,\\n\\t37.765910000,-122.456930000,\\n\\t37.765910000,-122.457020000,\\n\\t37.765920000,-122.457160000,\\n\\t37.765930000,-122.457270000,\\n\\t37.765940000,-122.457360000,\\n\\t37.765950000,-122.457410000,\\n\\t37.765960000,-122.457470000,\\n\\t37.765980000,-122.457560000,\\n\\t37.766010000,-122.457660000,\\n\\t37.766070000,-122.457830000,\\n\\t37.766070000,-122.457830000,\\n\\t37.766120000,-122.457980000,\\n\\t37.766180000,-122.458180000,\\n\\t37.766190000,-122.458200000,\\n\\t37.766240000,-122.458400000,\\n\\t37.766270000,-122.458530000,\\n\\t37.766290000,-122.458600000,\\n\\t37.766300000,-122.458690000,\\n\\t37.766300000,-122.458880000,\\n\\t37.766300000,-122.458970000,\\n\\t37.766280000,-122.459470000,\\n\\t37.766270000,-122.459520000,\\n\\t37.766270000,-122.459560000,\\n\\t37.766280000,-122.459600000,\\n\\t37.766280000,-122.459630000,\\n\\t37.766290000,-122.459670000,\\n\\t37.766300000,-122.459700000,\\n\\t37.766310000,-122.459730000,\\n\\t37.766320000,-122.459750000,\\n\\t37.766330000,-122.459770000,\\n\\t37.766350000,-122.459800000,\\n\\t37.766390000,-122.459860000,\\n\\t37.766340000,-122.459970000,\\n\\t37.766290000,-122.460150000,\\n\\t37.766290000,-122.460230000,\\n\\t37.766280000,-122.460280000,\\n\\t37.766260000,-122.460330000,\\n\\t37.766250000,-122.460420000,\\n\\t37.766240000,-122.460520000,\\n\\t37.766230000,-122.460670000,\\n\\t37.766230000,-122.460800000,\\n\\t37.766230000,-122.460900000,\\n\\t37.766210000,-122.461110000,\\n\\t37.766170000,-122.462030000,\\n\\t37.766160000,-122.462170000,\\n\\t37.766150000,-122.462520000,\\n\\t37.766120000,-122.463260000,\\n\\t37.766090000,-122.464130000,\\n\\t37.766070000,-122.464350000,\\n\\t37.766070000,-122.464440000,\\n\\t37.766060000,-122.464620000,\\n\\t37.766030000,-122.465400000,\\n\\t37.765980000,-122.466470000,\\n\\t37.765940000,-122.467530000,\\n\\t37.765930000,-122.467680000,\\n\\t37.765890000,-122.468600000,\\n\\t37.765860000,-122.468980000,\\n\\t37.765830000,-122.469660000,\\n\\t37.765780000,-122.470740000,\\n\\t37.765770000,-122.471030000,\\n\\t37.765770000,-122.471140000,\\n\\t37.765760000,-122.471380000,\\n\\t37.765740000,-122.471820000,\\n\\t37.765690000,-122.472950000,\\n\\t37.765680000,-122.473220000,\\n\\t37.765670000,-122.473320000,\\n\\t37.765640000,-122.474070000,\\n\\t37.765590000,-122.475140000,\\n\\t37.765590000,-122.475400000,\\n\\t37.765580000,-122.475520000,\\n\\t37.765550000,-122.476200000,\\n\\t37.765500000,-122.477180000,\\n\\t37.765500000,-122.477280000,\\n\\t37.765490000,-122.477400000,\\n\\t37.765490000,-122.477440000,\\n\\t37.765490000,-122.477440000,\\n\\t37.765490000,-122.477490000,\\n\\t37.765470000,-122.477770000,\\n\\t37.765450000,-122.478340000,\\n\\t37.765450000,-122.478420000,\\n\\t37.765430000,-122.478870000,\\n\\t37.765400000,-122.479430000,\\n\\t37.765380000,-122.479810000,\\n\\t37.765350000,-122.480480000,\\n\\t37.765300000,-122.481570000,\\n\\t37.765300000,-122.481660000,\\n\\t37.765290000,-122.481950000,\\n\\t37.765260000,-122.482640000,\\n\\t37.765220000,-122.483570000,\\n\\t37.765210000,-122.483710000,\\n\\t37.765210000,-122.483810000,\\n\\t37.765190000,-122.484130000,\\n\\t37.765160000,-122.484780000,\\n\\t37.765120000,-122.485860000,\\n\\t37.765110000,-122.485960000,\\n\\t37.765100000,-122.486170000,\\n\\t37.765070000,-122.486930000,\\n\\t37.765020000,-122.488000000,\\n\\t37.765020000,-122.488100000,\\n\\t37.765000000,-122.488360000,\\n\\t37.764980000,-122.488970000,\\n\\t37.764970000,-122.489070000,\\n\\t37.764930000,-122.490150000,\\n\\t37.764920000,-122.490240000,\\n\\t37.764910000,-122.490490000,\\n\\t37.764880000,-122.491210000,\\n\\t37.764830000,-122.492290000,\\n\\t37.764790000,-122.493260000,\\n\\t37.764780000,-122.493350000,\\n\\t37.764740000,-122.494420000,\\n\\t37.764720000,-122.494730000,\\n\\t37.764710000,-122.495500000,\\n\\t37.764700000,-122.495630000,\\n\\t37.764690000,-122.495940000,\\n\\t37.764680000,-122.496260000,\\n\\t37.764670000,-122.496480000,\\n\\t37.764600000,-122.497490000,\\n\\t37.764590000,-122.497650000,\\n\\t37.764550000,-122.498720000,\\n\\t37.764500000,-122.499780000,\\n\\t37.764450000,-122.500870000,\\n\\t37.764440000,-122.501060000,\\n\\t37.764400000,-122.501930000,\\n\\t37.764360000,-122.502990000,\\n\\t37.764310000,-122.504080000,\\n\\t37.764260000,-122.505140000,\\n\\t37.764260000,-122.505250000,\\n\\t37.764220000,-122.506220000,\\n\\t37.764170000,-122.507280000,\\n\\t37.764120000,-122.508360000,\\n\\t37.764100000,-122.508850000,\\n\\t37.764100000,-122.509150000,\\n\\t37.764100000,-122.509440000,\\n\\t37.764090000,-122.509760000,\\n\\t37.764080000,-122.510200000,\\n\\t37.764070000,-122.510320000,\\n\\t37.764060000,-122.510430000,\\n\\t37.764030000,-122.510430000,\\n\\t37.764030000,-122.510430000,\\n\\t37.763960000,-122.510430000,\\n\\t37.763960000,-122.510290000,\\n\\t37.764070000,-122.510320000,\\n\\t37.764170000,-122.510320000,\\n\\t37.764410000,-122.510340000,\\n\\t37.764610000,-122.510350000,\\n\\t37.764780000,-122.510330000,\\n\\t37.764960000,-122.510310000,\\n\\t37.765340000,-122.510280000,\\n\\t37.765570000,-122.510260000,\\n\\t37.765840000,-122.510250000,\\n\\t37.765900000,-122.510250000,\\n\\t37.766140000,-122.510260000,\\n\\t37.766410000,-122.510260000,\\n\\t37.766620000,-122.510270000,\\n\\t37.767040000,-122.510310000,\\n\\t37.767270000,-122.510330000,\\n\\t37.767620000,-122.510390000,\\n\\t37.767670000,-122.510390000,\\n\\t37.767740000,-122.510410000,\\n\\t37.767840000,-122.510430000,\\n\\t37.768280000,-122.510530000,\\n\\t37.768620000,-122.510600000,\\n\\t37.768690000,-122.510620000,\\n\\t37.769020000,-122.510690000,\\n\\t37.769260000,-122.510730000,\\n\\t37.769420000,-122.510750000,\\n\\t37.770010000,-122.510830000,\\n\\t37.770340000,-122.510860000,\\n\\t37.770460000,-122.510870000,\\n\\t37.770930000,-122.510930000,\\n\\t37.770980000,-122.510930000,\\n];\\n let value = gpsData.indexOf(prevValue); \\nreturn gpsData[(value == -1 ? 1 : value + 2)];\"}]}],\"timewindow\":{\"history\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":500}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"mapProvider\":\"OpenStreetMap.Mapnik\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"showTooltip\":true,\"tooltipColor\":\"#fff\",\"tooltipFontColor\":\"#000\",\"tooltipOpacity\":1,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
End Time: ${maxTime}
Start Time: ${minTime}\",\"strokeWeight\":2,\"strokeOpacity\":1,\"pointSize\":10,\"markerImageSize\":34,\"rotationAngle\":180},\"title\":\"Trip Animation\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":false,\"showLegend\":false,\"actions\":{},\"legendConfig\":{\"position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}" + "templateHtml": "", + "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('openstreet-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('openstreet-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('openstreet-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}\n", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermomether\\\";\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"markerImageSize\":34,\"useColorFunction\":true,\"markerImages\":[\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAwgSURBVGiB7Zt5cBT3lce/v18fc89oRoPEIRBCHIUxp2ywCAgIxLExvoidZIFNxXE2VXHirIO3aqtSseM43qpNeZfYKecox3bhpJykYgdjDkU2mBAB5vCamMNYAgQyURBCoxnNPd39O/aP7hGSEUR24L/uqqf+zfR77/Pe69/Rv6kWwcgPLRIJfZUAa7xez2xd90QBwDSNZKlkHJHAK+l09mUA7BP4vPpRUVExMVoRef+L998njxx9X57vPi/PnTsnO850yPaT7XLXrrflqjtWymhF+HA0Gp0wEp/kHymEQqG4ptJDGzf+um5RUxMSiV7Z3Lyt88L5nozgHJWj4pGmpqZav99PWve04onHHuswmViQzWb7ruZX+Udgv8/z3A+f/NGye1evxssvb+wo5PMfTZs6bfqcuXNHL7hlweh58+ZVAOTUpk2b0p9dvjyqqmrs/b8ejpUMc+unzjgUCsXjsYruE+2n1JY/NedM0zCi0VjA7/d7/f4AAgE//H4/vF4fOjvP9h5695C/oaEhcN/q1SyTzVdnMpnklXzTq4EplUsXfmaRCgC7du3cOn78+KfGj59Add3z1Md1vV7vqPa2D1sA4MYbZ6qUiqVX9X21i4TQcfX19QCA6urquN/vn0kAPRQKpYbTnzRpUhgAampqAEFrPjVYSql7fD4AgK5r2tV0AcDj8WkAoOk6JJGeTw2+nocLdsEu2AW7YBfsgl2wC3bBLtgFu2AX7IJdsAt2wS7YBbtgF+yCXbALdsEu2AW7YBfsgl2wC76mh/ppjIQgXVloPxVSBRV0rBe455P6+kTKBYF3tonxY/IWarry7DvI298Tgp0PR9RzACaN1NeIS100+EdvKXW3cMZvF8wCK10Sq2it2NAzakmukP/wmoP/KuId3BRUMg5uCfCSNVSKVn1rNto7Un8jLrUVqJ4Fi2eEQiEYBzOsy3SYL37TNQdzi8Q5FxkqJIQBsNLlYMGF/zqAJWBxSEogDAY+DJibYqTuRg4WFgO3OKhCYTExbKk5G/mbkSPP2DQhLA5IO/NhSz1MMP882BDgnAFQwdiVSs2vPVhYDIJLUMkBgw1favM6lJoZDDAYhKbAYsOX+rqAhcXAuQSIAKzhSy2vS8YmB7NYH4WCfM7kw5VaWtdpOO3bfWZJZVXgPxMX898bVsm6RhkTIseX29yyIErm/J5z5vwr6pvmsLYjBgeDwSpVJS/OmT1n1de+9qANZgLc4q9Dyj2qQhUhSSUAUCL7GBcchCymTEYBYNWqVXj30MGHT586PZEJ+WAul7ts8bjspd9QKDRNU2nz4z94YtI3H3oI+XwB//3j/9m77eRUUJ9/0eh4APGoDz6vCi4ksgUTmYyBC4k8RLGwtzF+EGu+tHqRqqrYtm0rXnzhhQ7G5cpsNnvyiuBIJFKnqvSd55772eilS5fhwIH9ye+/dPaEf1T9otW3T8GtiyYgGNBBymYEgLSbvakidu8/h01vnkYhcab1gcVs5tx5c6PHjh7DU0/9qFsINPb3939UZg28X11dXR0Qwtr9g8efqGtc+Bn89re/O7FhR9BXNaFm+n98uxHTZ1SDKQqKAihweZlITUVtXQwNs8fg+Bmzdk+bnmPdf/7bwsbGeO2ECaED+9/5XCxWuTGbzVpDwJpGNtx+28o77rr7bmzZsu3k7z+cMlHzeiPrvnoTwtVhFAVQHAZY4HBEoiAAeDXUjI/gyJGeQEd6TFj2tHYuXNgYy2azVe0fngiWDLNloHNFo4FZkXDsoTVr1+KD4x8U/3Ci1qP5PV7N74FeFUbClKDEriy57A5JANL5a68hnqoINL8OAPqbXbNp7clTxTVr1/oOHjr0MFXxq2Qy9wEFACnoY//6la9QAHj+9Q/eUL2RWkVXoWgqkhZBypRImkDKBFIWkLIk+h1JWdL+zrmeNCWSDFB0DYquQvWG637TcnozAKxbt45yTr8PAGowGBwVDAbvmT9/Pvbu3dddijV9WdUUUE0BUQm6kwaCYe+ljK/w8ruUdsYCBLlMEUQhoJoCygWM+LIvHTx4sGfevIbqYMD3BSFkJVUUrG5oaFABoPXwhd1UVUBVahtpKtoOnEV/gSHHgBwDso5c6XO6yNF24CNQTbV9qBRUUenuwz1/BoCZM2dplOJeSggWL1myFEII9IeXziIKBVUUW1QKo2Ci41Anei9kkWcY6Ex5R8qfc0wi0ZPF6QNnYeQNB2j7IQpFOtg0WwiBxoWNIBKLVQI6Z8rUqTh69FiWaFNmEIWgLFShoM5TZbIzgVxvFp6ID5rfA6JQgBAIxsGLJkrpAsycAcH4gN1gX0QPTW9vP5Grr58cJJTOpbqmjgWAnp6ei4QSEEJAKAGh1BbHCS2DLAFmMAgmICwObjDnyYMMAtJL9oN89vRc7KWUQtOUsSqhSggA8sWivSEh9qBxTiCEAGRwQARUVaB67Hf5pZAQlA0Ayrq2LTCogVyhlLURNEw55yYABP2+4ED3vHSClBKQ9jiFdHqvEBCMQzAOKYSt6/RqSGnbDPJRbgT93hAAcM4NyhjrBYDKylhswEEZJgYJFxDchnGTwSqasIomuMnsIDiH5GKIzUAQTsCVlZUxB9xLIUVbKpVEff3kiLTMfimEA7HP5bZgHMJ07mnJAiuaYEXT3jcZDMLkTgBD7exgBKRp9NfVTQwnk0kIKduoJGRH8/ZmhMNh4skc3DnEkDlAi4GbtjDDguVAmZM1M6yB68JyKsCGBqD373s7GAySnTt3gBDyFhWCvPHee/8HAJhTU5g0BMg4uMXBTT4AZSUTrGjBKpiwCnablQbDbZuyfTmAuRPMegA4euQopCRbaCaTOd2XSLzX3d2Nu+64bR7PnP3LJSCDMBm4YW9FWcmyQYMytsW+Zpfdsm1MdimAdMc7K29bMedCdzeSyeS76XT6jLNI4PGf/+w5aLqOu25IjOOWKcSg0jJjcLZ2ecsZD5TdybqsOxC0ZYpbJ58frek6nn/+eVBJHgecjXkqk2nu7Ozcdfz4cdx556rJN5C3m8v3jBt2xpdnazjysawNy5lUbKkrbmtZsWL5pGNHj6Or62+7k5lMy5CFNRQKTfN6tAMvvvhSRe3EOqx/4oXXLvia7qO6CsVZrey5154KB5YpKSG5tHs+5/ZsZnEIk6Ei1fLH73373i/09fXi0fWPpgyTLchkMqeGgAEgHA5/vjJWsf2PmzYr1dXV+K8fP7vjLxduWkY8ilpetQZPg+UJxh63lzqlNDi7gTa3fuPraz6bzxXw79/5FutP51am0+kdZdaQ/2kzDKNDUci51179w8pbP3er8sAD6+pnVCWy+/fs21LAqBnlMT50qJXFLq2a2L/5gaVy7N133j69u7sb67/7iFHIFf4tlU6/Ppg1kLGU8hYAywBMeOWV33gfXb9+1Q+ffDL+4Ne/AcYY/tS8PbV5++4Dhy+MopY2ZrLiidQDgDBSp5TS+Y7psS65ZOHsW26++eYosxje2PwGNm586eKzz/x027+sXWsBOAfgbULIQQAgUspaAA8BGAfnsamrq4u0tZ0Q333kkdGmZS3f8JNnlBXLV0AOilRKCS7sWYlxjlKxgHw+j5Y3W/C/Tz/NQ6Hgjp9seKZ31py5ajwe4wAtz9zdAH5OpJTPAqgEgL5USkpu4eLFHloqFXniYh9t3bunauuWrStisSi5//4vYnHTEkyZOhWqokBICcuy0N7ehr2trXjt1VeRzqTl3ffc81bjgsZELF4pQ6EAqa4eI6UEicfj5dhTKoCikynx6Bop5C14dJ2XcjmouipvvGFGoSJaWfr738/7tmzdjl/88pfIZjKwnH2SpmkIhSMYW1ODhvmNGFcztjhudFXR69Wgck58Hg+XEorH5ylDJYA8kVKOckpdB0ADIBOJhOzv70OhUFILuTzPZLNcSE6SfSlvJp0O5A1DN0qGDxLS4/OUAh6PGQqHC5XxeJEQgkgoRH1+L/wBP6LRuIjH4+Uf8gSAUwB+MbhzzQSwCMA0p/QUQADgNJ/PJ/v7+wnnnFiWkJZhKCYzKADoqiZUXeW67iGcSxKPx2QoFAo7AybnuE8COAZgHyHkxGXjeFAQEQCzANQCqAIQBeAH4AXgcex052w45TMcyQHIAOgBcBbAUUJI5uOM/wcaHmf3g9UM7QAAAABJRU5ErkJggg==\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAA3vSURBVGiB7Vt7cFzVef+dc+/d90OrJyO/JSO/4ncxxfULMCYIAyEW08amJJgmM4GmnZjJdNq4gcSGzLQxk3bsaWcaaIHyR8CJrWAbpjgG/AhINsbYxkaSDY6xJFvSrrS7Wu3uvfecr3+cu1pbXhkJs/4nujNndufec77f+d7fd+4uw8gvIxwOfocBaz0e91yXyx0BgKyZiWUz5kcEvBKPJ18EYI+C5rWvkpKSyZGS8LGHGtbQR8ePUUdnB50/f57OfnqWWlpbaN++39O99fdQpCR0NBKJTBwJTfZFE4LBYLmh8+YXXvifKctWrEBPTze9+cbu8/3JVMoWNjwer3/ZsuUTvV4P239gP36yceNZW9CtyWQyei262hcB+7zurU/99Ge3r1nTgJdfevFsqr8/Wlc3rWbGzFkV8+fPr1iwYEEJgLadO3cmbr/jjohh6KXHPjxamsmar39pjoPBYHl5aUnnqZY2/b1Dh9LdPd39kUgk6PP5PD6fH36/Dz6fDx6PF+fOfdZ9+pPTgbq6Ou+aBx+0k/0DVYlEIjYcbX4tYM5pxeK/WKIDwM7Gxt0TJox/dtLESXC53JuHzvV4PBVHDjfvAYDZs+fonMsV16R9rYeM8XG1tbUAgMrKsrDP659DRJ5gMNhbaH5NTU0IAMaPHw9IPv5LAxORy+31AgBcLsO41lwAcLu9BgAYLheIkftLAxfzGgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4D/lME1ke7gDF8ltbOHe3W923oEwYi1jxftWfZWgAziwacZkd2pfyN96XN5IIu7dMtIKA9/TI+zqCnFps2Alg5UlojFnVqIHZUlO2sl4RyC4CU+SEEylux8Z/iyc7mrxw4U7UnYwvGpXMYKIgNGdwXC/76C48oRw3sDWfnCgIkARJXcpwbvpA1e6T0Rq5jDr8EAHKA6OpjUOJwfeXAJAEhAXAGgEPKq+dIMVJqowDO4RAAC0rHV21u5LijAJaABAOIAY5Oh15iFMgj1zEpcUuuXjpIWeCouxjAtnIZcGKA5AVFbRfazPUC50QrKe8+Qy8qiqjBYIODA5DgBd1pBO9WRg9sy7yOhXBca+icYrgTOUGOiKnIVdCdisAxJGBTPsYW0nHRrJqgfNmGVtiqaeR1xchF7Vgz40q/BUNmISlcL7CUgJAMnOUiVwEdF0PURIAAVHaC8ucbAiwcQAb1KQpwXMjFrhtYMcOVO8lhOB457ujcKZd9hBguSYwcelTupKyaQWKYJFEU4xJw/Dhfcw29ilSBcNjEoTucFnSnkeOOvvTJpcVC1cYoGB5NAGEQTukjMAzHoghJghyWCRjenYoTuZjKx8xJiwU4LrSZ6waWpIoBjTuRqxDHRUkSUMWAJAZp6QU5FqOw65HHapG3bGVcBTZXDI5VnFaFgBL1yC34uoBJqEJeIwD2MMY1ilZidAFEMlDOqm9UdpJ0ZawumI+LU9ArwhyqWxyNz14XsBAMUnLVH0ttGB0XococdCGWE3XhOV85MF1WV2OY3omK0S2SkxgYAZYYJoAUpcqEEjG/Ru80isA1ysMXYNCnCum4aKUPgTu90w3sFinXL6nO/MadCAhiKloxBjFMeSuK0S1Kylv1cE1bUVoYyHwhoI6bCswpjjuxK5u2G2lcti2jzNCRTluioHEVw52EBA5/2LKsLBL+h2gs/o+Fjpa+MqtmjCbkqQJSYFF3T3zRsPMvA75i7UiBA4FApa6z5+fNnbd6/frHADghk7QdlhAHdMY0KXkZAHAuozaRMDRtKYMdAYDVq1fjcHPTD860nZlsS3qsv7+/+6pNDr0RDAanGTrf85Onnq75/uNPIJ1O4+dbnj34Ot6B4eFLqksqUeEvgcflAREhZabR09+Li/EorLQ4eFv317D2oW8t0XUdu3a9jud/9auztqD6ZDLZOixwOByeouv8D1u3brtpxYrb0XS4Kfbj3//8VHC8d0nDLXfj67OWIeQJgDGADfoOAxHQl05i14l92PHBXiTPp/c/OrFh9vwF8yMnjp/A5s2bOqXEbX19fX+8CriqqspvmunDTz/10xkr71qFnY07Tr1i7aqsLg2Vb6h/GOPCpdAYgTPlNLmF5AzpvBRp74viX3a/hO6+ge47+hZG61fVTz9y+DCee27Lx15fYFFHR8cAcNkPuw2DPXfP1+vvvf+BB7Br967WX9Mbk70eCn33zlWoCrsgKAFBCdgy/2nLBCyZgCUSMGUSpkzC0G1MrKzE0XMt/la9I0QnM+cWL15cmkwmK1tOnwpksuabg8YVifjnhEOlj69dtw6nT51Kv2q96fYG4fG7gbJwFhn7cxicIJgEZwAfEiokGASpWG1KhvIwg1/91ti1N9DEJ7ZOzKxdt87T1Nz8A67jv2Kx/o85AJDk//zXjzzCAeA/D7zU6PZjkkuXcBuEjN2OrGiHabfDFB2w7HZYoh3mVaMDWWdu1m6Hy5Bw6RIuP6b87+HXdgDAww8/zIXgGwFADwQCFYFA4BuLFi3CoUN/6LRmyL/y6gSXTtC4QDTVgQo/B5iEJFJ6Rt64lI6Vfi3JYBFHd1JA5wIunUNIQvpr/C+bm5u65s9fWBnwe9dISWVc0/DNhQsX6gDwTuuhd3WNYOSGTjjSehGp7EVYsguWuJQfssu51wVTXIIpLsGWlzBgXsSRM5dg6Hk6uk787Zb39gHA7NlzDM7xoM4Yli5fvgJSSiRmmbP9HNA0Qm4D6axEc6uJ6eOzuCloQuOOjlneqiUx2BK4lDBwut2DTFaHoXFYGilaHEjMMOdKKXHb4tvw/nvvL9UZ+Lyb6+pw/PjxpOZhsziX0DigcYLG1QaEBD69ZKA7wRHx2/C7BDSNwEi9AEmZGmJJA/1Z9SJM12hwvcYBzgmaj89obW3pr62dGmCcz+cuQ68GgEtdl7oYU40CZwSeW+As1rmy5KzNkbY1WILDlOp71ubgnKA7czVO4NyhwQhcFS7o6urq5pzDMLRqnXEtCACpdCrFHOHlAsTgYEq0nCnj0jnBY6i8KCTLBxbmzB2yPkczmU4lAYAxHtKFECYAPeDzBQZD4GU+motMueXklECWc7QkSaVDGoTAVetz8AGfLwQAQoisbtt2N4BJZaVlpZQjkntdS8w5UFOFni0YLMGhWfny1rbVPVuoOVKyK9ZeTrMsUl7qAHdzkPyktzeG2tqbw8KihCQlPjVUl2hLBkswmDZD1mJIWxwDWTXSFkfWUs8sZ64QzlqHjiRA2tQ7ZcqUYCwWgyT6hBNjb+3ZvQehUIi52tje3M6FyHHIYNkOqM2RsTjS2cuAs+pe1uYKPLcBkduA+m60sH1+v5/t3fsWGGP/x6VkjR98cAQAMNc7bXJepAyWzWHaimjW4siYDGmTY8DkGMhqapgcaVM9yw5ugMOyeX4DkmGub1otABz/6DiI2O94IpE4E+3p+aCzsxP333PfAvOi2G8JBtMRbU68GZMj44Ao0BzXmgOsRk7spq1oWILB6rQP3nt3/byLnZ2IxWKH4/H4pxoAeFzuC21tretW3rUKnk5mtWiflzAGxhgDQ66IYyrnOnqzBFfDZjAdLk1HMnkpMWRNLldmFomamtrIL/71F+iPJ/8mnc2e4QDQm0jsOXfu3L6TJ0/ivtX3T607M26P6SzMWI5eB7ktPHLPc/MV5xwTjpe9sfLOu2pOHD+JCxc+fyeWSLyZdzCoWsvjNpqef/6F8KTJU/DDLT/a3jM90eDWCS5dqmDvxF7NCRSAOikQhCuMUXHMEDjm3v7jb/+oIRrtxpMbnuzNmvatiUSi7QpgAAiFQneXlZbs3rGjUauorMSmLc+8dShy7HbDELqeA3bC4GCScHxWSMDOgVuaPb2t+t3vPfK9O1P9A/j7v3vC7ov318fj8bdyWFf8YCSbzZ7VNHb+tVdfrV911ypt/bcfq52J2uTBg+//LhWwZ0nJYTtWf6WrcccDGFgLdn5nwkPVD9Q/MLOzsxNPbvhhNpUc+G5vPL7jcqxBjonozwEsBzD5lVde9jy5YcPqTZufKX90/WOwbRv7330nsffDt08dSB41EkZyHPfwmwBAZuTFsBm48GeuWfai2oUzp02fFjKzJhp3NuLFF/+765e//Pfd31q71gLwGYC3GWNNAMCIaBKAJwBUO3uQnZ2d/MyZNv1vn/j+LUuXLq/Z/MyzCIfDTmxW8Y+IVFyWqjKRQkDYNqKxGDb97GkcOXLk7LZt/9F8c12dqKqqYM4LYALQCWAbI6J/A1AGgKK9vSBhoa8vEe+N9TwejcZYU1MTfrN9O6puqkJDw0NYtnwFpk6dCsZUMrFtG22trTiw/11s3/4aotEo1jQ04NZFt6KsrJTCoZKtJaWRiGG4KBKJ5BJWnw4gDedAx+0yMJCywLnQGWOSMabV1NbikUfX40J7B367sxFbt25DMhGHZZkgAC7DhWAojOpx4zF3wS0YP64aVZUVYCoQSN2la4bhIsNlcOS73H5GRBUAHgcwBYABAD09PZROp1gq2V8WTybq4vH4xEQ8oSWSSfSnUkinM7As9RdUw9Dh9XoR8PsQCgYRCodESTj0x1Aw2OrxBXsDgYBdXl6eM2IB4CyAbZcb12wASwBMB1Dq7C4ACJZIJHstM5PWdC2TTmcom80wEtySAFwupum6wbxeDxeCuT0et8/v94UBTTrSJABRAKcAHGCMnbrKjy/bRBjAHAATAFQ5NuAF4IFqAtyOKzKo83MLgAkgA2AAQB+ADgCfAzjBGIsPxfh/6wbDK7xbMFYAAAAASUVORK5CYII=\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAyUSURBVGiB7Zp7kFRVesB/5/S9PdMz/ZoHMwo4MICDuoGVIYICIuzGcn0vC+oWGuNjs8mua9ySP4wpgyaiVVupbHYTsLJmNT7WNXExwqqzrq8g4oNxdXUgyEMQARmZd3fPTE/3vfd8+ePenhlgBsFlrFSqb9Wpvn3vd77f+b7zne87ffsqjv+wE4nYDQqWl5aWfDUcLqkAyOUHunID+Q8EnkilMo8C7gnoPPaRTCYnVyQT71+1bKl80PK+HGw9KPv27ZPde3bLjp075NVXX5FLL7lYKpLx9yoqKuqOR6f6PIFYLFZtW7r54YcfqV+4aBEdHe3ywm+e39eb6etzPZfS0kj5woUX1EUipWrj6xtZedddu11P5mYymc5j6Q19HrgsUrL67r/7+8VLly7j8cce3d3X29vZ0DB9yplnfWXcrFmzxjU2NiaBXevWrUsv/trXKmzbqnz/9+9VDuTyz35hi2OxWHV1ZbJ1245d1ltvvpFtb293Kyoq7LKystKysnLKy8soKyujtDTCxx/vSW3fsT3c0NAQWbpkiZvp7a9Np9Ndo+nWxwJrLYvmzV9gAaxbt/75urrxd592Wp0Oh0tWHSkbiUQSv3unuQlgxoyZltZm0TF1H+umUnrC1KlTAaipqUpESmMzFIRjsVj3SPJTpkyJA0ycOBGMnviFwSISLolEAAiHbftYsgAlJREbwA6HESUlXxg8lkcRXAQXwUVwEVwEF8FFcBH8/xhsnZC0ksw49eQPI5mmNtP54ccAIvqgqbz4aYn8zYoTUXXcFnueyZ8eXtleZt75iQnpU0VUvYiqB5mvu5p+XH9w8RtgnJMOLut/7rd4+fpRBcS52hz65csnHdxQ8clZnyuT3NV40sHRUnfq58mUWFJ70sEn+yiCi+AiuAgugovgIrgILoKL4CK4CC6Ci+D/Q+Djf/higk8Jzs0IMjIGYDGAp0AUeBbiHf3Xs/HGAHyYlYaRX0EYC4txNeIFugvWHyXzua8cnDjYGMBoQIFhRFfLmLjaCxqAw8iuHing/nCwGlLuMrKrveNfnccPFnyLtQ8c0a1jElye8sGFAYwUSCN54Q8GB4ljKKpHkBmLOZbB4FLgjhLVYxNcDFnkMXJUj03m0kOKR0sgYzLHRvlwpcDYI7oaGYvl5HB4ZRrJ1cf9fP5E/5NwQUKM7uoTOI4/ql38kmgUOCMnEHMCL819sag2jJJAxgIs+HNY6PGlpUxXDQWXw5dXjxH8SFZBPf7SyqKrMQLKG7b/OkpmTBJI0BSjbwTGYo6Ni5+ZjMJDj1wkxmQ5iV+VsBh9BzImKbNQFhWjp8wx21c7dKIV9A94IxaJsdplZt9574JQVcUdpr3rzlEHdzLASslpg19EofLMMa3dc0Z9c9YMXT+s7/GCo9FojWWph87+6tmX3XTTzT7XA/F4xutXr4fyOuQZVQUQ0tLphY1nlcn5YqgAuOyyy3inefOtH+36aLJr5Obe3t72o4w68kIsFptuW7pp5d33TPne928hm83yLz+6b9PVb/4niRK9QNfUoquqUaUREEEG+jGd7Zi2Dnpy3qYHGr7OFdcsX2BZFs899ywP/fznu11PLslkMjtHBScSiXrL0m+uXr3mlEWLFrN58+auxD+u2HZWhb0gcvkyShZ/Ax2N+70KPcVvJpMm999NZJ99mi1dzsb3rviLGbNmz6rY0rKFVavubTWG83p6ej4psAbfr66trS03xtlw98p76s+bN5+nnvzFtouevK/s1AnJM+I/vB37j6aDziJeCtxhzUkhTgoYwJpchz3zbJI7fj/pzA829f6iR/bPPW9e9aS6utjbb715YWVl1SOZTMY5DGzb6scXf+OSS6+48kqanntu55+99shkOyLx8uuvIjSuDEzq6Ob5TdzgPJ9GhT2sCbV4W1vK57R+FP9lOrT33PnzKjOZTM2OD7dFB3L5FwaDq6KifGYiXvn95ddey4fbtmWv2fhIiVUqpbpMEao2SH4fiKCMgAbRggSuVkKwEQz22q4iVKtQEYUtJvzdlvX6+bq67PJrr41sbm6+VVv8W1dX7/9oADH6b//0+us1QO/jD6xPhGWSCgsqLJj8PsTdjzj7Ma7fxDkAzn5wjry+H3H2YfL7UGGDCguJEqnPPf3YOoDrrrtOe56+C8CKRqPjotHoN+fMmcObb7zRelsk9W1lC4QFCRlM9yfoKnsoEgOLVWCxDLfYBRwwnXmwDIQVyoMbo6lrfrq5+dCsxsbaaHlkqTFSpUMhvjV79mwLwHvjldewBGxQlqBswXn3Y6T/EDhtiNOGuG2I2444QXPb/WtOGzhtmL7PcN7di7IFFegiJDq3+ZVXAWbMmGlrzRJLKc6/4IJFGGO4MdQ+gxAQEn/2LcH0u+Sa27HO0IRq/V+MSqnBOUZARMAD75DB2w4mq8AKWkggpPiOtJ3dYgznzTuPt996+3xLoc8+vaGBlpaWzFybrygtqCPgeODtcTFtBl1hUBHfGgl+wNGv8FIayWjE6KCfD1UhBVqotPWZO3Zs7506dVpUaT1Lh21rPED7oUNtKH8OUYLSoHTwWRiEAsmBDIA4gCPIAJh8YL3lyw7vi5JAJ7QdamvXWmPbofGW0qEYQL4/0zeYjdTRTQ0Oxp9/Svx9jvKAkBocsCh1dP9AZ76vNwOglI5bnuflAaukPBo9bM8UpMIjvxeiWAUbATHK3/yNJM/h30vKozEAz/Ny2nXddoCKyqrKwc5GDYFMUJmM8peLqyCvkH6FZP1zXP+eGBXIFvQcrquyqroyALdrxGzv7u5i6rTTE3lX0gUL/DIYPPfwFDh+k5xCBhSS1Ui/9s9zQ/cLz0rEGxqEGMWAK92T6yfHu7q6MCLbtSj1UtPzTcTjcfW0E3t5EBSkv0FgPgAMQgtWa/9azpcZHICrhvR48B+52CvRaFS9/PJLKKVe1Mao9e+++zsAtk9rnIwbLBFHIQ5IACWvkJxGBjSSDeDZ4HxAIznty+SV38chGIA/PXumzZoK0PJBCyLq1zqdTn/U2dHxbmtrKxddfmXj1r7QRr9jMH/5Ye4d8OdV+odZ3F+AqyG3F/oFelr62PQnl14667PWVrq6ut5JpVJ7giLBygfWrMYOh3ll/pLx4iojR7p3QMGgpQX4kPUE8OFuF0chrjIvzL78VDsc5sEHH0SLWkmQLuhOp5v27t376tatW7nk8iun/UN8VhM5BblASS5w53BowdXD4L7Lg8EG7Z6SM36z+MILp25p2cqBA/s3dKXTLxRSBeDvtUpL7M0PPfRwYtLken791z9Y++fevmWE/WJBIelbgJbDtz4mePblBksrcPU/ubVrF65Yuayzs50Vt6/ozuXduel0etdhYIB4PH5RVWXy+WeeWR8aV1PDz+6/56W//PDFxbpELGULgwVEcwSYoWXkKExOuatqGl9b8p3vfb2vt5/b/uoWtyfVe0kqlXqpwDpql1lVlbwhUhr52VNPrQ3PPuccNm16PbXrR3f+9pvm0NV+pWEwhQKIqKHnm57iV9nydc6Smxc1zm5MHvj0AHfecUeuv7f/u509PY8N5wyCReRcYCEw6YknHi9bcfvtl9276r7qG2+6Gdd12bhhQ/rghhe3TdmywT4l2zkhEeIUgJTLZ62RygPbT5/rlv/xvLOmnzE9ns/lWb9uPY8++u9tP/3JPzd9e/nyLLAXeE0ptRlAicgk4BZgfDAGc/DgQb1790fWrT+45Zz58xdMue+++0kkk/5N8RO2iPiZ0BiMCMbz8FyXzq4u7l91L5ub3969Zs2/Np/eMM2rrT21YKQBPgPWKBFZAyQA093drTzPobu7uyPV3XNbR2enam5uZu3atdTW1LDsqqtYeMEipk2b5m8GANd12bVzJ69vfI2n1/6Kjo5OvrVsKefOPZeqqkpJJCtXJ5OJinBpRJLxeOF3bI8FZIAYoEN2SHmeJ6GQ2CiMUipUP2UK199wI59+2sp/rVvP6tVryKRTOE4eAcJ2mFg8wfgJE5nZeA4TJ4yntmYcSimUUsaydMi2wxIKKTXM6n4lIuMCV08m2O52dHSQzfbpvkxvZSqTbkinUnWpVDqUzvTS29dHNpvFcfy6aNsWkUgp0fJyYrEYiUTcSybin8RjiZ2lZeXd0WjUra6uDg2L/z3A6uHBNQNYAEwHqvAXTTl4Kp3O9HhOvk+FGMhmHXHdHGLEE8CytNY6rCKRsPY8VRoOh8tisfIkhFxgIAB2AtuA15VS20ZcTsEgEsBM4DTgFKASiAClQAnBig7EC8/8BoAc0AekgE+B/cAWpVTqSMb/AlY1WXIncMcxAAAAAElFTkSuQmCC\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAxNSURBVGiB7Zp7kFTllcB/5/a93dMz3T0PemYIDgoCPhZ5iaD4wNkFjQjRRMlLTNbSlKlyzZpobSVbFRPUbNVWSRCWuKvlxqybtbIrukp4SATZCAgospEBgeElj4EZ5t3d0+++37d/9O2ZnqEHQZzZSlXfqlMz/c253+875zvfOefeHuH8L6u83P+AwH0lJZ4pbrenEiCVSnYmEsndGl4NhSKvAJkLmPPcV0VFxZjKivKPv77wXr274WN9uvm0PnHihD5y9IhuPNioN216Vy+Yf6eurAj8b2Vl5aXnM6d8loLf7w9apvHhyy//29jZ9fW0t7fpdWtWN7Wdao4qpaiqDpbdXF9fV1paKpu3bGbxk08eSWXU9ZFIpOPirC33v7xs+TIdiUT0Pz239NjeaTOTHXXjdb4cuP6W5DOLFx/7aNdH+oknfqQryv0vXZTFfr8/GKyqaN7XeMhc//ba6NSfPFXqS6fESJ29jdGAX69+9KHY9OnTyxbec08mHInWhsPhzsHmNs4FNgxdf+NNN5sAh3/7n40dCxeKedUsOr6x8CzdsnBEQu9sPABwzTWTTMNQ9eec+1x/FDEuGTduHABXtreOKutJYyiFqq4tqD+5O3wJQF1dHSij7nODtdZuj9cLgMfGOpcuQInSFoDldqNFez43eCivIrgILoKL4CK4CC6Ci+AiuAgugovgIrgILoKL4CK4CC6Ci+A/B7B5vor6Mz4PNnbRYAAtoCQLUMMFVobuBWOALWdjVIGxiwbbZC3WkrXWLqAzJBZrR5T0LWTgdSHfdF1YcIlG57t8oM5nfov1OcCKPmDW1Rfi2IsA5yI5F9WFXF0o0i8arARwggsBu4BbhwaM6g0ujXY+9b+GLqrzLR5E5wsH2ziB5QRXoW8lCy3mosH553iwlDlEe9znai2DpMyhAJ+PxUNTJMhZm51+WM9xvsWFXD2kx0nl9rjQ4oYC3C+4BoEMnasl39Vn6wxRdcqbXApXpwupWBcEVgLKGLw6DU1w5bkaCjcChcYuHozuLYtqEFfroXC1TZ67GcbjlEuZWjSIHr6ozjZ7/y/VSWOLdgJIF9zjQl3JFwDOXn1lsYDOULm6X+YaROcLB6s8+LC2tzqvoc+Wx0L2nT/6wlIm5y6LQ9bs5TLXsO5x7jG192lxuJq9bCOg0aIRGcYEkt9lCsPp6lxlMsBlFE4ghcYuGoxznHKFYNjKYq7Zy5XFYW32lMtCBGzbLlwWLwB83m/2NNC44R0iFaP503+8jO1UqHz5wiwW0aNzvysgdPJTQr/7dFD9fHD+vecN9vl8NaYpv546ZeqCBx98CMhGbPXEqZRfcTWmyySTjuO2TMora/B4Sji+832OnWoGYMGCBez88IMfHD50eExG6Yd6enraBjJcAwf8fv+Vbsv1Pz9f/NT1y1esQCnNPz6zeGuy6WBN+MRRrwp1YMR6MOIJMqEuOj49xNFd2zh5aD9SVpr44PCJXVOmXXvpHfPm4fP7rtz98Z/usSz3+lQq1e/fnvuFSHl5+VjTNLb96lfPj6yv/0t2bN/eufJnj+37Uql1c/1Xv8WM279CaZn/rJcBGoj1hNm+7k22rF5JcyK1edp3Hps0bfq0yj0Ne/jFL55pVopZ3d3dx88C19bWlqVS8Z2Lf/7U1XNvu51Vb72x7/irz9fUBEcEv/03PyFYPRJDgZHt9XpvzG8QlAFnWppY+S9LaOnsaPPOWdhxx7z5V320cydLl/7yE2+pb+bp06dj/VxtWbJ03h13zr/r7rtZu2bNwVP/9cKYMiHwtW8+QNAbwOiOIN09SCiChCKQL+EIKhxBhcN4EGpGjuJww66yxNH9gePac+zGm26sikQiNY379/kSydT63uCqrCybXB6oeuS+RYvYv29f/OTKFz1+dIlXXFQrCznRjNhkRfdJzmIMEAExsqbUmh68holWGXf43deMg6NHJ+5btKjkgw8//IFh8lJnZ88nBoBWxpPf+e53DYC1Ly5bVSb6Mo8WSrQgx5uRY6cHSDMcz0q/vx/PSTNeJXi04EOPfe93L70JcP/99xu2bfwUwPT5fNU+n++rM2fO5P3332+uS3V9y9KCG8FSmtjRo3iN0uz+qqylemDnLhpDQDsFJGrHMG2F2xAyGi5Nhr65Y8f21unTZ9T4yrz3KqVHGC4X91x33XUmwN7N775nApbuk90nD5BpbUbaWqG9Dd3eju5o6y/t7dDehrS1kmltYffJ/ViA25nDBcbeLZs2AUyaNNkyDL5minDL7Nm3opSiNtQ0yUQwESydlXg6xc70Sf5CewliYSD9TqHu/anpIMUnJIiLjSVCGjAFTA21odNTlFLMunEWO7bvuMUUjKkTrriCvXv3RDyiJxpacGVXSc56W2uO6DhtKkmFFsocHchmtKhoukURNrJPG5YDdAEuDYaAV/TVjY0HesaNG+8Tw5hmuC1zFEBLS0urkQ3QPtFgILgQTC0IkAZSgEJQCClnTBwdF4KBOPf2iQBnzrS2GYaBZblGmWK4/ADxWCzqoS85iDOZDFiMS2ddV5Kz2EkGhgwECYLOzqOzxy0W7YkAiBgBw7btFIC3tMw/2JsrnS9OI5B2pPdt0AC9gdVZZxkBANu2k0Ymk2kDCI6oqsw1c/nNu8rVW8l+2ZFCkxRNzMhKUjQpNBlnv23nXfbAeTRQHayudMBtBlod6OrqZNz4CeVprcKqd4KsZBxgGk1KNEmBmGiijsScsZRo0s4CMnn3284CMqJCY8aOCXR2dqK0PmBokQ3r1q7D7/dLq7tyY8axMCOatDNZFqhJiCbuWNsLNrJjCUcnt4C0ZOew0WTQnDYr3/X5fLJx4wZE5B1DKVm1a9dHAIyYesPYjEBa+vYwJZAUSAgkHAtjookaWcl9Togm4eim8u5PS9YDNVNmXg7QsLsBreX3RjgcPtzW1rarubmZ+QvumtahXJvzrUzmWRvrZ61yxNnvPKuTA6xvt13bvjxv/tSW5mY6Ozt3hkKhoy4Ar6ek6dChg4vm3nY7oZJAJnG4oUIQESdD5Ud0v30XSBlZC1OGdjyTA/darwK3LcxcPm585ZJnl9ATinwvnkweNgC6wuF1x44d27R3714WfOWucZGrb3g7kee+eJ6LewPLcXU0bzwuuf2G3P3NoyevnzP3tsv3NOylqenkHzvD4fWQ197aikeW/nJJd1dnJ4//9On57V+a8Hoib7K4kQeUAWL0D7RcsJ2oqHv9wUcfu7Orq5MVK5Z3KS0P53j96lsgEPjyiKqKtW/891uu2tpalvzDMxsTW96s9yhMC8HUOCkxm07JO/fZk5A9dkmDTOSqWe/99fcfmRPtifHY3z6a6Q5F7gyFQhsKggFGjKh4wFviffG11153T59xHVu3bg3968/+7g9V3ae+0Zv0kX49l3ISjA2ccpe/NXvR9+uvnX5tRdOpJv7+xz9OxnpiD3d0d/97PqcXrLWeBcwGLnv11d96n3j88QVPPf108KHvPUwmk+HttWu71q96Y0dozzajJBUfXyqMA4gpfShmeY54JkzX19/6VzfMmDmjMpPOsOqtVbzyym9alz23fM23Fy1KACeAP4rIBwCitb4MeAQY5SxEt7a2qIaGBn70wx+OTKXTc5Y+t8w1d85cdN5KtdbYSqGVImPbJOIxotEo6/+wniXPPmsH/L4Ny5etaJk46Rqprq7JPTgooBn4Z9FaPw9UAHR1dSnbTsuZMy1GMpnItLZ2GFu3bq5d/fvVc0ZUjZB7F36d2fW3MmHCFZguF0pr0uk0Bxsb2bL5PV5fuZLuUEjfdffdG2+66ebW6mCVLvP5qa4OAoYEg8Gcg7tNIAIEADHdJnbcxmNZ6UQ05nK7TT1x4sRYRVV1/FTTqdLVa9bywgsvEImESKfSAFiWhT9QzqhL6rh25g3UjbokPnJkTaKkxFRaa8NtGbaIy+Up8eS2VgEx0VpXO66+HKfdbW9vV93d7RKNJl3xeNQOd4d1Mp0i3B3yRCKRsmgiYSVTaa9orS23lfR5vany8vKYLxCIeyxLKqoqtddbKh6PSVVVtQ4Gg5IHPQI8nx9ck4CbgSuBarJnvARsiUai4XBPmGQyqbWGRCxh2VrZAKYYLtNjZUyXSxsuU6oqyg1fwO91nhUSzvQdwB5gm4h8UvA4OYsoByYDY4EaoBLwAN7sYiDvZ4LsqUo60uNIK3AY2CMioYGM/wPREY0iGUY58wAAAABJRU5ErkJggg==\"],\"useMarkerImageFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', amount = percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"markerImageFunction\":\"var type = dsData[dsIndex]['type'];\\nif (type == 'thermomether') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"color\":\"#fe7569\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"showTooltip\":true,\"autocloseTooltip\":true,\"tooltipFunction\":\"var deviceType = dsData[dsIndex]['deviceType'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}
Energy: ${energy:2} kWt
';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}
Temperature: ${temperature:2} °C
';\\r\\n }\\r\\n}\",\"labelFunction\":\"var deviceType = dsData[dsIndex]['deviceType'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}, ${energy:2} kWt';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}, ${temperature:2} °C';\\r\\n }\\r\\n}\",\"provider\":\"openstreet-map\",\"defaultCenterPosition\":\"0,0\",\"showTooltipAction\":\"click\"},\"title\":\"OpenStreetMap\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" } } ] diff --git a/application/src/main/data/json/system/widget_bundles/maps__deprecated_.json b/application/src/main/data/json/system/widget_bundles/maps__deprecated_.json deleted file mode 100644 index 9de69c7f93..0000000000 --- a/application/src/main/data/json/system/widget_bundles/maps__deprecated_.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "widgetsBundle": { - "alias": "maps", - "title": "Maps (Deprecated)", - "image": null - }, - "widgetTypes": [ - { - "alias": "google_maps", - "name": "Google Maps", - "descriptor": { - "type": "latest", - "sizeX": 8.5, - "sizeY": 6, - "resources": [], - "templateHtml": "", - "templateCss": ".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 200px;\n white-space: nowrap;\n}", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidget('google-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"title\": \"Google Map Configuration\",\n \"type\": \"object\",\n \"properties\": {\n \"gmApiKey\": {\n \"title\": \"Google Maps API Key\",\n \"type\": \"string\"\n },\n \"gmDefaultMapType\": {\n \"title\": \"Default map type\",\n \"type\": \"string\",\n \"default\": \"roadmap\"\n },\n \"defaultZoomLevel\": {\n \"title\": \"Default map zoom level (1 - 20)\",\n \"type\": \"number\"\n },\n \"fitMapBounds\": {\n \"title\": \"Fit map bounds to cover all markers\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"markersSettings\": {\n \"title\": \"Markers\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Marker settings\",\n \"type\": \"object\",\n \"properties\": {\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"lat\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"lng\"\n }, \n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"tooltipPattern\": {\n \"title\": \"Pattern ( for ex. 'Text ${keyName} units.' or '${#} units' )\",\n \"type\": \"string\",\n \"default\": \"Latitude: ${lat:7}
Longitude: ${lng:7}\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n },\n \"useColorFunction\": {\n \"title\": \"Use color function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"colorFunction\": {\n \"title\": \"Color function: f(data)\",\n \"type\": \"string\"\n },\n \"markerImage\": {\n \"title\": \"Custom marker image\",\n \"type\": \"string\"\n },\n \"markerImageSize\": {\n \"title\": \"Custom marker image size (px)\",\n \"type\": \"number\",\n \"default\": 34\n },\n \"useMarkerImageFunction\": {\n \"title\": \"Use marker image function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"markerImageFunction\": {\n \"title\": \"Marker image function: f(data, images)\",\n \"type\": \"string\"\n },\n \"markerImages\": {\n \"title\": \"Marker images\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Marker image\",\n \"type\": \"string\"\n }\n }\n }\n }\n }\n },\n \"required\": [\n \"gmApiKey\"\n ]\n },\n \"form\": [\n \"gmApiKey\",\n {\n \"key\": \"gmDefaultMapType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"roadmap\",\n \"label\": \"Roadmap\"\n },\n {\n \"value\": \"satellite\",\n \"label\": \"Satellite\"\n },\n {\n \"value\": \"hybrid\",\n \"label\": \"Hybrid\"\n },\n {\n \"value\": \"terrain\",\n \"label\": \"Terrain\"\n }\n ]\n },\n \"defaultZoomLevel\",\n \"fitMapBounds\",\n {\n \"key\": \"markersSettings\",\n \"items\": [\n \"markersSettings[].latKeyName\",\n \"markersSettings[].lngKeyName\",\n \"markersSettings[].showLabel\",\n \"markersSettings[].label\",\n \"markersSettings[].tooltipPattern\",\n {\n \"key\": \"markersSettings[].color\",\n \"type\": \"color\"\n },\n \"markersSettings[].useColorFunction\",\n {\n \"key\": \"markersSettings[].colorFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"markersSettings[].markerImage\",\n \"type\": \"image\"\n },\n \"markersSettings[].markerImageSize\",\n \"markersSettings[].useMarkerImageFunction\",\n {\n \"key\": \"markersSettings[].markerImageFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"markersSettings[].markerImages\",\n \"items\": [\n {\n \"key\": \"markersSettings[].markerImages[]\",\n \"type\": \"image\"\n }\n ]\n }\n ]\n }\n ]\n}", - "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"lat\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"lng\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.799863043034289,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"markersSettings\":[{\"label\":\"First point\",\"color\":\"#1e88e5\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"tooltipPattern\":\"Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"useColorFunction\":true,\"colorFunction\":\"var temperature = data['temperature'];\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', amount = percent).toHexString();\\n}\\nreturn 'blue';\",\"markerImages\":[],\"useMarkerImageFunction\":false},{\"label\":\"Second point\",\"color\":\"#fdd835\",\"latKeyName\":\"lat\",\"lngKeyName\":\"lng\",\"showLabel\":true,\"tooltipPattern\":\"Latitude: ${lat:7}
Longitude: ${lng:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"markerImageSize\":34,\"markerImages\":[\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAwgSURBVGiB7Zt5cBT3lce/v18fc89oRoPEIRBCHIUxp2ywCAgIxLExvoidZIFNxXE2VXHirIO3aqtSseM43qpNeZfYKecox3bhpJykYgdjDkU2mBAB5vCamMNYAgQyURBCoxnNPd39O/aP7hGSEUR24L/uqqf+zfR77/Pe69/Rv6kWwcgPLRIJfZUAa7xez2xd90QBwDSNZKlkHJHAK+l09mUA7BP4vPpRUVExMVoRef+L998njxx9X57vPi/PnTsnO850yPaT7XLXrrflqjtWymhF+HA0Gp0wEp/kHymEQqG4ptJDGzf+um5RUxMSiV7Z3Lyt88L5nozgHJWj4pGmpqZav99PWve04onHHuswmViQzWb7ruZX+Udgv8/z3A+f/NGye1evxssvb+wo5PMfTZs6bfqcuXNHL7hlweh58+ZVAOTUpk2b0p9dvjyqqmrs/b8ejpUMc+unzjgUCsXjsYruE+2n1JY/NedM0zCi0VjA7/d7/f4AAgE//H4/vF4fOjvP9h5695C/oaEhcN/q1SyTzVdnMpnklXzTq4EplUsXfmaRCgC7du3cOn78+KfGj59Add3z1Md1vV7vqPa2D1sA4MYbZ6qUiqVX9X21i4TQcfX19QCA6urquN/vn0kAPRQKpYbTnzRpUhgAampqAEFrPjVYSql7fD4AgK5r2tV0AcDj8WkAoOk6JJGeTw2+nocLdsEu2AW7YBfsgl2wC3bBLtgFu2AX7IJdsAt2wS7YBbtgF+yCXbALdsEu2AW7YBfsgl2wC76mh/ppjIQgXVloPxVSBRV0rBe455P6+kTKBYF3tonxY/IWarry7DvI298Tgp0PR9RzACaN1NeIS100+EdvKXW3cMZvF8wCK10Sq2it2NAzakmukP/wmoP/KuId3BRUMg5uCfCSNVSKVn1rNto7Un8jLrUVqJ4Fi2eEQiEYBzOsy3SYL37TNQdzi8Q5FxkqJIQBsNLlYMGF/zqAJWBxSEogDAY+DJibYqTuRg4WFgO3OKhCYTExbKk5G/mbkSPP2DQhLA5IO/NhSz1MMP882BDgnAFQwdiVSs2vPVhYDIJLUMkBgw1favM6lJoZDDAYhKbAYsOX+rqAhcXAuQSIAKzhSy2vS8YmB7NYH4WCfM7kw5VaWtdpOO3bfWZJZVXgPxMX898bVsm6RhkTIseX29yyIErm/J5z5vwr6pvmsLYjBgeDwSpVJS/OmT1n1de+9qANZgLc4q9Dyj2qQhUhSSUAUCL7GBcchCymTEYBYNWqVXj30MGHT586PZEJ+WAul7ts8bjspd9QKDRNU2nz4z94YtI3H3oI+XwB//3j/9m77eRUUJ9/0eh4APGoDz6vCi4ksgUTmYyBC4k8RLGwtzF+EGu+tHqRqqrYtm0rXnzhhQ7G5cpsNnvyiuBIJFKnqvSd55772eilS5fhwIH9ye+/dPaEf1T9otW3T8GtiyYgGNBBymYEgLSbvakidu8/h01vnkYhcab1gcVs5tx5c6PHjh7DU0/9qFsINPb3939UZg28X11dXR0Qwtr9g8efqGtc+Bn89re/O7FhR9BXNaFm+n98uxHTZ1SDKQqKAihweZlITUVtXQwNs8fg+Bmzdk+bnmPdf/7bwsbGeO2ECaED+9/5XCxWuTGbzVpDwJpGNtx+28o77rr7bmzZsu3k7z+cMlHzeiPrvnoTwtVhFAVQHAZY4HBEoiAAeDXUjI/gyJGeQEd6TFj2tHYuXNgYy2azVe0fngiWDLNloHNFo4FZkXDsoTVr1+KD4x8U/3Ci1qP5PV7N74FeFUbClKDEriy57A5JANL5a68hnqoINL8OAPqbXbNp7clTxTVr1/oOHjr0MFXxq2Qy9wEFACnoY//6la9QAHj+9Q/eUL2RWkVXoWgqkhZBypRImkDKBFIWkLIk+h1JWdL+zrmeNCWSDFB0DYquQvWG637TcnozAKxbt45yTr8PAGowGBwVDAbvmT9/Pvbu3dddijV9WdUUUE0BUQm6kwaCYe+ljK/w8ruUdsYCBLlMEUQhoJoCygWM+LIvHTx4sGfevIbqYMD3BSFkJVUUrG5oaFABoPXwhd1UVUBVahtpKtoOnEV/gSHHgBwDso5c6XO6yNF24CNQTbV9qBRUUenuwz1/BoCZM2dplOJeSggWL1myFEII9IeXziIKBVUUW1QKo2Ci41Anei9kkWcY6Ex5R8qfc0wi0ZPF6QNnYeQNB2j7IQpFOtg0WwiBxoWNIBKLVQI6Z8rUqTh69FiWaFNmEIWgLFShoM5TZbIzgVxvFp6ID5rfA6JQgBAIxsGLJkrpAsycAcH4gN1gX0QPTW9vP5Grr58cJJTOpbqmjgWAnp6ei4QSEEJAKAGh1BbHCS2DLAFmMAgmICwObjDnyYMMAtJL9oN89vRc7KWUQtOUsSqhSggA8sWivSEh9qBxTiCEAGRwQARUVaB67Hf5pZAQlA0Ayrq2LTCogVyhlLURNEw55yYABP2+4ED3vHSClBKQ9jiFdHqvEBCMQzAOKYSt6/RqSGnbDPJRbgT93hAAcM4NyhjrBYDKylhswEEZJgYJFxDchnGTwSqasIomuMnsIDiH5GKIzUAQTsCVlZUxB9xLIUVbKpVEff3kiLTMfimEA7HP5bZgHMJ07mnJAiuaYEXT3jcZDMLkTgBD7exgBKRp9NfVTQwnk0kIKduoJGRH8/ZmhMNh4skc3DnEkDlAi4GbtjDDguVAmZM1M6yB68JyKsCGBqD373s7GAySnTt3gBDyFhWCvPHee/8HAJhTU5g0BMg4uMXBTT4AZSUTrGjBKpiwCnablQbDbZuyfTmAuRPMegA4euQopCRbaCaTOd2XSLzX3d2Nu+64bR7PnP3LJSCDMBm4YW9FWcmyQYMytsW+Zpfdsm1MdimAdMc7K29bMedCdzeSyeS76XT6jLNI4PGf/+w5aLqOu25IjOOWKcSg0jJjcLZ2ecsZD5TdybqsOxC0ZYpbJ58frek6nn/+eVBJHgecjXkqk2nu7Ozcdfz4cdx556rJN5C3m8v3jBt2xpdnazjysawNy5lUbKkrbmtZsWL5pGNHj6Or62+7k5lMy5CFNRQKTfN6tAMvvvhSRe3EOqx/4oXXLvia7qO6CsVZrey5154KB5YpKSG5tHs+5/ZsZnEIk6Ei1fLH73373i/09fXi0fWPpgyTLchkMqeGgAEgHA5/vjJWsf2PmzYr1dXV+K8fP7vjLxduWkY8ilpetQZPg+UJxh63lzqlNDi7gTa3fuPraz6bzxXw79/5FutP51am0+kdZdaQ/2kzDKNDUci51179w8pbP3er8sAD6+pnVCWy+/fs21LAqBnlMT50qJXFLq2a2L/5gaVy7N133j69u7sb67/7iFHIFf4tlU6/Ppg1kLGU8hYAywBMeOWV33gfXb9+1Q+ffDL+4Ne/AcYY/tS8PbV5++4Dhy+MopY2ZrLiidQDgDBSp5TS+Y7psS65ZOHsW26++eYosxje2PwGNm586eKzz/x027+sXWsBOAfgbULIQQAgUspaAA8BGAfnsamrq4u0tZ0Q333kkdGmZS3f8JNnlBXLV0AOilRKCS7sWYlxjlKxgHw+j5Y3W/C/Tz/NQ6Hgjp9seKZ31py5ajwe4wAtz9zdAH5OpJTPAqgEgL5USkpu4eLFHloqFXniYh9t3bunauuWrStisSi5//4vYnHTEkyZOhWqokBICcuy0N7ehr2trXjt1VeRzqTl3ffc81bjgsZELF4pQ6EAqa4eI6UEicfj5dhTKoCikynx6Bop5C14dJ2XcjmouipvvGFGoSJaWfr738/7tmzdjl/88pfIZjKwnH2SpmkIhSMYW1ODhvmNGFcztjhudFXR69Wgck58Hg+XEorH5ylDJYA8kVKOckpdB0ADIBOJhOzv70OhUFILuTzPZLNcSE6SfSlvJp0O5A1DN0qGDxLS4/OUAh6PGQqHC5XxeJEQgkgoRH1+L/wBP6LRuIjH4+Uf8gSAUwB+MbhzzQSwCMA0p/QUQADgNJ/PJ/v7+wnnnFiWkJZhKCYzKADoqiZUXeW67iGcSxKPx2QoFAo7AybnuE8COAZgHyHkxGXjeFAQEQCzANQCqAIQBeAH4AXgcex052w45TMcyQHIAOgBcBbAUUJI5uOM/wcaHmf3g9UM7QAAAABJRU5ErkJggg==\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAA3vSURBVGiB7Vt7cFzVef+dc+/d90OrJyO/JSO/4ncxxfULMCYIAyEW08amJJgmM4GmnZjJdNq4gcSGzLQxk3bsaWcaaIHyR8CJrWAbpjgG/AhINsbYxkaSDY6xJFvSrrS7Wu3uvfecr3+cu1pbXhkJs/4nujNndufec77f+d7fd+4uw8gvIxwOfocBaz0e91yXyx0BgKyZiWUz5kcEvBKPJ18EYI+C5rWvkpKSyZGS8LGHGtbQR8ePUUdnB50/f57OfnqWWlpbaN++39O99fdQpCR0NBKJTBwJTfZFE4LBYLmh8+YXXvifKctWrEBPTze9+cbu8/3JVMoWNjwer3/ZsuUTvV4P239gP36yceNZW9CtyWQyei262hcB+7zurU/99Ge3r1nTgJdfevFsqr8/Wlc3rWbGzFkV8+fPr1iwYEEJgLadO3cmbr/jjohh6KXHPjxamsmar39pjoPBYHl5aUnnqZY2/b1Dh9LdPd39kUgk6PP5PD6fH36/Dz6fDx6PF+fOfdZ9+pPTgbq6Ou+aBx+0k/0DVYlEIjYcbX4tYM5pxeK/WKIDwM7Gxt0TJox/dtLESXC53JuHzvV4PBVHDjfvAYDZs+fonMsV16R9rYeM8XG1tbUAgMrKsrDP659DRJ5gMNhbaH5NTU0IAMaPHw9IPv5LAxORy+31AgBcLsO41lwAcLu9BgAYLheIkftLAxfzGgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4D/lME1ke7gDF8ltbOHe3W923oEwYi1jxftWfZWgAziwacZkd2pfyN96XN5IIu7dMtIKA9/TI+zqCnFps2Alg5UlojFnVqIHZUlO2sl4RyC4CU+SEEylux8Z/iyc7mrxw4U7UnYwvGpXMYKIgNGdwXC/76C48oRw3sDWfnCgIkARJXcpwbvpA1e6T0Rq5jDr8EAHKA6OpjUOJwfeXAJAEhAXAGgEPKq+dIMVJqowDO4RAAC0rHV21u5LijAJaABAOIAY5Oh15iFMgj1zEpcUuuXjpIWeCouxjAtnIZcGKA5AVFbRfazPUC50QrKe8+Qy8qiqjBYIODA5DgBd1pBO9WRg9sy7yOhXBca+icYrgTOUGOiKnIVdCdisAxJGBTPsYW0nHRrJqgfNmGVtiqaeR1xchF7Vgz40q/BUNmISlcL7CUgJAMnOUiVwEdF0PURIAAVHaC8ucbAiwcQAb1KQpwXMjFrhtYMcOVO8lhOB457ujcKZd9hBguSYwcelTupKyaQWKYJFEU4xJw/Dhfcw29ilSBcNjEoTucFnSnkeOOvvTJpcVC1cYoGB5NAGEQTukjMAzHoghJghyWCRjenYoTuZjKx8xJiwU4LrSZ6waWpIoBjTuRqxDHRUkSUMWAJAZp6QU5FqOw65HHapG3bGVcBTZXDI5VnFaFgBL1yC34uoBJqEJeIwD2MMY1ilZidAFEMlDOqm9UdpJ0ZawumI+LU9ArwhyqWxyNz14XsBAMUnLVH0ttGB0XococdCGWE3XhOV85MF1WV2OY3omK0S2SkxgYAZYYJoAUpcqEEjG/Ru80isA1ysMXYNCnCum4aKUPgTu90w3sFinXL6nO/MadCAhiKloxBjFMeSuK0S1Kylv1cE1bUVoYyHwhoI6bCswpjjuxK5u2G2lcti2jzNCRTluioHEVw52EBA5/2LKsLBL+h2gs/o+Fjpa+MqtmjCbkqQJSYFF3T3zRsPMvA75i7UiBA4FApa6z5+fNnbd6/frHADghk7QdlhAHdMY0KXkZAHAuozaRMDRtKYMdAYDVq1fjcHPTD860nZlsS3qsv7+/+6pNDr0RDAanGTrf85Onnq75/uNPIJ1O4+dbnj34Ot6B4eFLqksqUeEvgcflAREhZabR09+Li/EorLQ4eFv317D2oW8t0XUdu3a9jud/9auztqD6ZDLZOixwOByeouv8D1u3brtpxYrb0XS4Kfbj3//8VHC8d0nDLXfj67OWIeQJgDGADfoOAxHQl05i14l92PHBXiTPp/c/OrFh9vwF8yMnjp/A5s2bOqXEbX19fX+8CriqqspvmunDTz/10xkr71qFnY07Tr1i7aqsLg2Vb6h/GOPCpdAYgTPlNLmF5AzpvBRp74viX3a/hO6+ge47+hZG61fVTz9y+DCee27Lx15fYFFHR8cAcNkPuw2DPXfP1+vvvf+BB7Br967WX9Mbk70eCn33zlWoCrsgKAFBCdgy/2nLBCyZgCUSMGUSpkzC0G1MrKzE0XMt/la9I0QnM+cWL15cmkwmK1tOnwpksuabg8YVifjnhEOlj69dtw6nT51Kv2q96fYG4fG7gbJwFhn7cxicIJgEZwAfEiokGASpWG1KhvIwg1/91ti1N9DEJ7ZOzKxdt87T1Nz8A67jv2Kx/o85AJDk//zXjzzCAeA/D7zU6PZjkkuXcBuEjN2OrGiHabfDFB2w7HZYoh3mVaMDWWdu1m6Hy5Bw6RIuP6b87+HXdgDAww8/zIXgGwFADwQCFYFA4BuLFi3CoUN/6LRmyL/y6gSXTtC4QDTVgQo/B5iEJFJ6Rt64lI6Vfi3JYBFHd1JA5wIunUNIQvpr/C+bm5u65s9fWBnwe9dISWVc0/DNhQsX6gDwTuuhd3WNYOSGTjjSehGp7EVYsguWuJQfssu51wVTXIIpLsGWlzBgXsSRM5dg6Hk6uk787Zb39gHA7NlzDM7xoM4Yli5fvgJSSiRmmbP9HNA0Qm4D6axEc6uJ6eOzuCloQuOOjlneqiUx2BK4lDBwut2DTFaHoXFYGilaHEjMMOdKKXHb4tvw/nvvL9UZ+Lyb6+pw/PjxpOZhsziX0DigcYLG1QaEBD69ZKA7wRHx2/C7BDSNwEi9AEmZGmJJA/1Z9SJM12hwvcYBzgmaj89obW3pr62dGmCcz+cuQ68GgEtdl7oYU40CZwSeW+As1rmy5KzNkbY1WILDlOp71ubgnKA7czVO4NyhwQhcFS7o6urq5pzDMLRqnXEtCACpdCrFHOHlAsTgYEq0nCnj0jnBY6i8KCTLBxbmzB2yPkczmU4lAYAxHtKFECYAPeDzBQZD4GU+motMueXklECWc7QkSaVDGoTAVetz8AGfLwQAQoisbtt2N4BJZaVlpZQjkntdS8w5UFOFni0YLMGhWfny1rbVPVuoOVKyK9ZeTrMsUl7qAHdzkPyktzeG2tqbw8KihCQlPjVUl2hLBkswmDZD1mJIWxwDWTXSFkfWUs8sZ64QzlqHjiRA2tQ7ZcqUYCwWgyT6hBNjb+3ZvQehUIi52tje3M6FyHHIYNkOqM2RsTjS2cuAs+pe1uYKPLcBkduA+m60sH1+v5/t3fsWGGP/x6VkjR98cAQAMNc7bXJepAyWzWHaimjW4siYDGmTY8DkGMhqapgcaVM9yw5ugMOyeX4DkmGub1otABz/6DiI2O94IpE4E+3p+aCzsxP333PfAvOi2G8JBtMRbU68GZMj44Ao0BzXmgOsRk7spq1oWILB6rQP3nt3/byLnZ2IxWKH4/H4pxoAeFzuC21tretW3rUKnk5mtWiflzAGxhgDQ66IYyrnOnqzBFfDZjAdLk1HMnkpMWRNLldmFomamtrIL/71F+iPJ/8mnc2e4QDQm0jsOXfu3L6TJ0/ivtX3T607M26P6SzMWI5eB7ktPHLPc/MV5xwTjpe9sfLOu2pOHD+JCxc+fyeWSLyZdzCoWsvjNpqef/6F8KTJU/DDLT/a3jM90eDWCS5dqmDvxF7NCRSAOikQhCuMUXHMEDjm3v7jb/+oIRrtxpMbnuzNmvatiUSi7QpgAAiFQneXlZbs3rGjUauorMSmLc+8dShy7HbDELqeA3bC4GCScHxWSMDOgVuaPb2t+t3vPfK9O1P9A/j7v3vC7ov318fj8bdyWFf8YCSbzZ7VNHb+tVdfrV911ypt/bcfq52J2uTBg+//LhWwZ0nJYTtWf6WrcccDGFgLdn5nwkPVD9Q/MLOzsxNPbvhhNpUc+G5vPL7jcqxBjonozwEsBzD5lVde9jy5YcPqTZufKX90/WOwbRv7330nsffDt08dSB41EkZyHPfwmwBAZuTFsBm48GeuWfai2oUzp02fFjKzJhp3NuLFF/+765e//Pfd31q71gLwGYC3GWNNAMCIaBKAJwBUO3uQnZ2d/MyZNv1vn/j+LUuXLq/Z/MyzCIfDTmxW8Y+IVFyWqjKRQkDYNqKxGDb97GkcOXLk7LZt/9F8c12dqKqqYM4LYALQCWAbI6J/A1AGgKK9vSBhoa8vEe+N9TwejcZYU1MTfrN9O6puqkJDw0NYtnwFpk6dCsZUMrFtG22trTiw/11s3/4aotEo1jQ04NZFt6KsrJTCoZKtJaWRiGG4KBKJ5BJWnw4gDedAx+0yMJCywLnQGWOSMabV1NbikUfX40J7B367sxFbt25DMhGHZZkgAC7DhWAojOpx4zF3wS0YP64aVZUVYCoQSN2la4bhIsNlcOS73H5GRBUAHgcwBYABAD09PZROp1gq2V8WTybq4vH4xEQ8oSWSSfSnUkinM7As9RdUw9Dh9XoR8PsQCgYRCodESTj0x1Aw2OrxBXsDgYBdXl6eM2IB4CyAbZcb12wASwBMB1Dq7C4ACJZIJHstM5PWdC2TTmcom80wEtySAFwupum6wbxeDxeCuT0et8/v94UBTTrSJABRAKcAHGCMnbrKjy/bRBjAHAATAFQ5NuAF4IFqAtyOKzKo83MLgAkgA2AAQB+ADgCfAzjBGIsPxfh/6wbDK7xbMFYAAAAASUVORK5CYII=\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAyUSURBVGiB7Zp7kFRVesB/5/S9PdMz/ZoHMwo4MICDuoGVIYICIuzGcn0vC+oWGuNjs8mua9ySP4wpgyaiVVupbHYTsLJmNT7WNXExwqqzrq8g4oNxdXUgyEMQARmZd3fPTE/3vfd8+ePenhlgBsFlrFSqb9Wpvn3vd77f+b7zne87ffsqjv+wE4nYDQqWl5aWfDUcLqkAyOUHunID+Q8EnkilMo8C7gnoPPaRTCYnVyQT71+1bKl80PK+HGw9KPv27ZPde3bLjp075NVXX5FLL7lYKpLx9yoqKuqOR6f6PIFYLFZtW7r54YcfqV+4aBEdHe3ywm+e39eb6etzPZfS0kj5woUX1EUipWrj6xtZedddu11P5mYymc5j6Q19HrgsUrL67r/7+8VLly7j8cce3d3X29vZ0DB9yplnfWXcrFmzxjU2NiaBXevWrUsv/trXKmzbqnz/9+9VDuTyz35hi2OxWHV1ZbJ1245d1ltvvpFtb293Kyoq7LKystKysnLKy8soKyujtDTCxx/vSW3fsT3c0NAQWbpkiZvp7a9Np9Ndo+nWxwJrLYvmzV9gAaxbt/75urrxd592Wp0Oh0tWHSkbiUQSv3unuQlgxoyZltZm0TF1H+umUnrC1KlTAaipqUpESmMzFIRjsVj3SPJTpkyJA0ycOBGMnviFwSISLolEAAiHbftYsgAlJREbwA6HESUlXxg8lkcRXAQXwUVwEVwEF8FFcBH8/xhsnZC0ksw49eQPI5mmNtP54ccAIvqgqbz4aYn8zYoTUXXcFnueyZ8eXtleZt75iQnpU0VUvYiqB5mvu5p+XH9w8RtgnJMOLut/7rd4+fpRBcS52hz65csnHdxQ8clZnyuT3NV40sHRUnfq58mUWFJ70sEn+yiCi+AiuAgugovgIrgILoKL4CK4CC6Ci+D/Q+Djf/higk8Jzs0IMjIGYDGAp0AUeBbiHf3Xs/HGAHyYlYaRX0EYC4txNeIFugvWHyXzua8cnDjYGMBoQIFhRFfLmLjaCxqAw8iuHing/nCwGlLuMrKrveNfnccPFnyLtQ8c0a1jElye8sGFAYwUSCN54Q8GB4ljKKpHkBmLOZbB4FLgjhLVYxNcDFnkMXJUj03m0kOKR0sgYzLHRvlwpcDYI7oaGYvl5HB4ZRrJ1cf9fP5E/5NwQUKM7uoTOI4/ql38kmgUOCMnEHMCL819sag2jJJAxgIs+HNY6PGlpUxXDQWXw5dXjxH8SFZBPf7SyqKrMQLKG7b/OkpmTBJI0BSjbwTGYo6Ni5+ZjMJDj1wkxmQ5iV+VsBh9BzImKbNQFhWjp8wx21c7dKIV9A94IxaJsdplZt9574JQVcUdpr3rzlEHdzLASslpg19EofLMMa3dc0Z9c9YMXT+s7/GCo9FojWWph87+6tmX3XTTzT7XA/F4xutXr4fyOuQZVQUQ0tLphY1nlcn5YqgAuOyyy3inefOtH+36aLJr5Obe3t72o4w68kIsFptuW7pp5d33TPne928hm83yLz+6b9PVb/4niRK9QNfUoquqUaUREEEG+jGd7Zi2Dnpy3qYHGr7OFdcsX2BZFs899ywP/fznu11PLslkMjtHBScSiXrL0m+uXr3mlEWLFrN58+auxD+u2HZWhb0gcvkyShZ/Ax2N+70KPcVvJpMm999NZJ99mi1dzsb3rviLGbNmz6rY0rKFVavubTWG83p6ej4psAbfr66trS03xtlw98p76s+bN5+nnvzFtouevK/s1AnJM+I/vB37j6aDziJeCtxhzUkhTgoYwJpchz3zbJI7fj/pzA829f6iR/bPPW9e9aS6utjbb715YWVl1SOZTMY5DGzb6scXf+OSS6+48kqanntu55+99shkOyLx8uuvIjSuDEzq6Ob5TdzgPJ9GhT2sCbV4W1vK57R+FP9lOrT33PnzKjOZTM2OD7dFB3L5FwaDq6KifGYiXvn95ddey4fbtmWv2fhIiVUqpbpMEao2SH4fiKCMgAbRggSuVkKwEQz22q4iVKtQEYUtJvzdlvX6+bq67PJrr41sbm6+VVv8W1dX7/9oADH6b//0+us1QO/jD6xPhGWSCgsqLJj8PsTdjzj7Ma7fxDkAzn5wjry+H3H2YfL7UGGDCguJEqnPPf3YOoDrrrtOe56+C8CKRqPjotHoN+fMmcObb7zRelsk9W1lC4QFCRlM9yfoKnsoEgOLVWCxDLfYBRwwnXmwDIQVyoMbo6lrfrq5+dCsxsbaaHlkqTFSpUMhvjV79mwLwHvjldewBGxQlqBswXn3Y6T/EDhtiNOGuG2I2444QXPb/WtOGzhtmL7PcN7di7IFFegiJDq3+ZVXAWbMmGlrzRJLKc6/4IJFGGO4MdQ+gxAQEn/2LcH0u+Sa27HO0IRq/V+MSqnBOUZARMAD75DB2w4mq8AKWkggpPiOtJ3dYgznzTuPt996+3xLoc8+vaGBlpaWzFybrygtqCPgeODtcTFtBl1hUBHfGgl+wNGv8FIayWjE6KCfD1UhBVqotPWZO3Zs7506dVpUaT1Lh21rPED7oUNtKH8OUYLSoHTwWRiEAsmBDIA4gCPIAJh8YL3lyw7vi5JAJ7QdamvXWmPbofGW0qEYQL4/0zeYjdTRTQ0Oxp9/Svx9jvKAkBocsCh1dP9AZ76vNwOglI5bnuflAaukPBo9bM8UpMIjvxeiWAUbATHK3/yNJM/h30vKozEAz/Ny2nXddoCKyqrKwc5GDYFMUJmM8peLqyCvkH6FZP1zXP+eGBXIFvQcrquyqroyALdrxGzv7u5i6rTTE3lX0gUL/DIYPPfwFDh+k5xCBhSS1Ui/9s9zQ/cLz0rEGxqEGMWAK92T6yfHu7q6MCLbtSj1UtPzTcTjcfW0E3t5EBSkv0FgPgAMQgtWa/9azpcZHICrhvR48B+52CvRaFS9/PJLKKVe1Mao9e+++zsAtk9rnIwbLBFHIQ5IACWvkJxGBjSSDeDZ4HxAIznty+SV38chGIA/PXumzZoK0PJBCyLq1zqdTn/U2dHxbmtrKxddfmXj1r7QRr9jMH/5Ye4d8OdV+odZ3F+AqyG3F/oFelr62PQnl14667PWVrq6ut5JpVJ7giLBygfWrMYOh3ll/pLx4iojR7p3QMGgpQX4kPUE8OFuF0chrjIvzL78VDsc5sEHH0SLWkmQLuhOp5v27t376tatW7nk8iun/UN8VhM5BblASS5w53BowdXD4L7Lg8EG7Z6SM36z+MILp25p2cqBA/s3dKXTLxRSBeDvtUpL7M0PPfRwYtLken791z9Y++fevmWE/WJBIelbgJbDtz4mePblBksrcPU/ubVrF65Yuayzs50Vt6/ozuXduel0etdhYIB4PH5RVWXy+WeeWR8aV1PDz+6/56W//PDFxbpELGULgwVEcwSYoWXkKExOuatqGl9b8p3vfb2vt5/b/uoWtyfVe0kqlXqpwDpql1lVlbwhUhr52VNPrQ3PPuccNm16PbXrR3f+9pvm0NV+pWEwhQKIqKHnm57iV9nydc6Smxc1zm5MHvj0AHfecUeuv7f/u509PY8N5wyCReRcYCEw6YknHi9bcfvtl9276r7qG2+6Gdd12bhhQ/rghhe3TdmywT4l2zkhEeIUgJTLZ62RygPbT5/rlv/xvLOmnzE9ns/lWb9uPY8++u9tP/3JPzd9e/nyLLAXeE0ptRlAicgk4BZgfDAGc/DgQb1790fWrT+45Zz58xdMue+++0kkk/5N8RO2iPiZ0BiMCMbz8FyXzq4u7l91L5ub3969Zs2/Np/eMM2rrT21YKQBPgPWKBFZAyQA093drTzPobu7uyPV3XNbR2enam5uZu3atdTW1LDsqqtYeMEipk2b5m8GANd12bVzJ69vfI2n1/6Kjo5OvrVsKefOPZeqqkpJJCtXJ5OJinBpRJLxeOF3bI8FZIAYoEN2SHmeJ6GQ2CiMUipUP2UK199wI59+2sp/rVvP6tVryKRTOE4eAcJ2mFg8wfgJE5nZeA4TJ4yntmYcSimUUsaydMi2wxIKKTXM6n4lIuMCV08m2O52dHSQzfbpvkxvZSqTbkinUnWpVDqUzvTS29dHNpvFcfy6aNsWkUgp0fJyYrEYiUTcSybin8RjiZ2lZeXd0WjUra6uDg2L/z3A6uHBNQNYAEwHqvAXTTl4Kp3O9HhOvk+FGMhmHXHdHGLEE8CytNY6rCKRsPY8VRoOh8tisfIkhFxgIAB2AtuA15VS20ZcTsEgEsBM4DTgFKASiAClQAnBig7EC8/8BoAc0AekgE+B/cAWpVTqSMb/AlY1WXIncMcxAAAAAElFTkSuQmCC\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAxNSURBVGiB7Zp7kFTllcB/5/a93dMz3T0PemYIDgoCPhZ5iaD4wNkFjQjRRMlLTNbSlKlyzZpobSVbFRPUbNVWSRCWuKvlxqybtbIrukp4SATZCAgospEBgeElj4EZ5t3d0+++37d/9O2ZnqEHQZzZSlXfqlMz/c253+875zvfOefeHuH8L6u83P+AwH0lJZ4pbrenEiCVSnYmEsndGl4NhSKvAJkLmPPcV0VFxZjKivKPv77wXr274WN9uvm0PnHihD5y9IhuPNioN216Vy+Yf6eurAj8b2Vl5aXnM6d8loLf7w9apvHhyy//29jZ9fW0t7fpdWtWN7Wdao4qpaiqDpbdXF9fV1paKpu3bGbxk08eSWXU9ZFIpOPirC33v7xs+TIdiUT0Pz239NjeaTOTHXXjdb4cuP6W5DOLFx/7aNdH+oknfqQryv0vXZTFfr8/GKyqaN7XeMhc//ba6NSfPFXqS6fESJ29jdGAX69+9KHY9OnTyxbec08mHInWhsPhzsHmNs4FNgxdf+NNN5sAh3/7n40dCxeKedUsOr6x8CzdsnBEQu9sPABwzTWTTMNQ9eec+1x/FDEuGTduHABXtreOKutJYyiFqq4tqD+5O3wJQF1dHSij7nODtdZuj9cLgMfGOpcuQInSFoDldqNFez43eCivIrgILoKL4CK4CC6Ci+AiuAgugovgIrgILoKL4CK4CC6Ci+A/B7B5vor6Mz4PNnbRYAAtoCQLUMMFVobuBWOALWdjVIGxiwbbZC3WkrXWLqAzJBZrR5T0LWTgdSHfdF1YcIlG57t8oM5nfov1OcCKPmDW1Rfi2IsA5yI5F9WFXF0o0i8arARwggsBu4BbhwaM6g0ujXY+9b+GLqrzLR5E5wsH2ziB5QRXoW8lCy3mosH553iwlDlEe9znai2DpMyhAJ+PxUNTJMhZm51+WM9xvsWFXD2kx0nl9rjQ4oYC3C+4BoEMnasl39Vn6wxRdcqbXApXpwupWBcEVgLKGLw6DU1w5bkaCjcChcYuHozuLYtqEFfroXC1TZ67GcbjlEuZWjSIHr6ozjZ7/y/VSWOLdgJIF9zjQl3JFwDOXn1lsYDOULm6X+YaROcLB6s8+LC2tzqvoc+Wx0L2nT/6wlIm5y6LQ9bs5TLXsO5x7jG192lxuJq9bCOg0aIRGcYEkt9lCsPp6lxlMsBlFE4ghcYuGoxznHKFYNjKYq7Zy5XFYW32lMtCBGzbLlwWLwB83m/2NNC44R0iFaP503+8jO1UqHz5wiwW0aNzvysgdPJTQr/7dFD9fHD+vecN9vl8NaYpv546ZeqCBx98CMhGbPXEqZRfcTWmyySTjuO2TMora/B4Sji+832OnWoGYMGCBez88IMfHD50eExG6Yd6enraBjJcAwf8fv+Vbsv1Pz9f/NT1y1esQCnNPz6zeGuy6WBN+MRRrwp1YMR6MOIJMqEuOj49xNFd2zh5aD9SVpr44PCJXVOmXXvpHfPm4fP7rtz98Z/usSz3+lQq1e/fnvuFSHl5+VjTNLb96lfPj6yv/0t2bN/eufJnj+37Uql1c/1Xv8WM279CaZn/rJcBGoj1hNm+7k22rF5JcyK1edp3Hps0bfq0yj0Ne/jFL55pVopZ3d3dx88C19bWlqVS8Z2Lf/7U1XNvu51Vb72x7/irz9fUBEcEv/03PyFYPRJDgZHt9XpvzG8QlAFnWppY+S9LaOnsaPPOWdhxx7z5V320cydLl/7yE2+pb+bp06dj/VxtWbJ03h13zr/r7rtZu2bNwVP/9cKYMiHwtW8+QNAbwOiOIN09SCiChCKQL+EIKhxBhcN4EGpGjuJww66yxNH9gePac+zGm26sikQiNY379/kSydT63uCqrCybXB6oeuS+RYvYv29f/OTKFz1+dIlXXFQrCznRjNhkRfdJzmIMEAExsqbUmh68holWGXf43deMg6NHJ+5btKjkgw8//IFh8lJnZ88nBoBWxpPf+e53DYC1Ly5bVSb6Mo8WSrQgx5uRY6cHSDMcz0q/vx/PSTNeJXi04EOPfe93L70JcP/99xu2bfwUwPT5fNU+n++rM2fO5P3332+uS3V9y9KCG8FSmtjRo3iN0uz+qqylemDnLhpDQDsFJGrHMG2F2xAyGi5Nhr65Y8f21unTZ9T4yrz3KqVHGC4X91x33XUmwN7N775nApbuk90nD5BpbUbaWqG9Dd3eju5o6y/t7dDehrS1kmltYffJ/ViA25nDBcbeLZs2AUyaNNkyDL5minDL7Nm3opSiNtQ0yUQwESydlXg6xc70Sf5CewliYSD9TqHu/anpIMUnJIiLjSVCGjAFTA21odNTlFLMunEWO7bvuMUUjKkTrriCvXv3RDyiJxpacGVXSc56W2uO6DhtKkmFFsocHchmtKhoukURNrJPG5YDdAEuDYaAV/TVjY0HesaNG+8Tw5hmuC1zFEBLS0urkQ3QPtFgILgQTC0IkAZSgEJQCClnTBwdF4KBOPf2iQBnzrS2GYaBZblGmWK4/ADxWCzqoS85iDOZDFiMS2ddV5Kz2EkGhgwECYLOzqOzxy0W7YkAiBgBw7btFIC3tMw/2JsrnS9OI5B2pPdt0AC9gdVZZxkBANu2k0Ymk2kDCI6oqsw1c/nNu8rVW8l+2ZFCkxRNzMhKUjQpNBlnv23nXfbAeTRQHayudMBtBlod6OrqZNz4CeVprcKqd4KsZBxgGk1KNEmBmGiijsScsZRo0s4CMnn3284CMqJCY8aOCXR2dqK0PmBokQ3r1q7D7/dLq7tyY8axMCOatDNZFqhJiCbuWNsLNrJjCUcnt4C0ZOew0WTQnDYr3/X5fLJx4wZE5B1DKVm1a9dHAIyYesPYjEBa+vYwJZAUSAgkHAtjookaWcl9Togm4eim8u5PS9YDNVNmXg7QsLsBreX3RjgcPtzW1rarubmZ+QvumtahXJvzrUzmWRvrZ61yxNnvPKuTA6xvt13bvjxv/tSW5mY6Ozt3hkKhoy4Ar6ek6dChg4vm3nY7oZJAJnG4oUIQESdD5Ud0v30XSBlZC1OGdjyTA/darwK3LcxcPm585ZJnl9ATinwvnkweNgC6wuF1x44d27R3714WfOWucZGrb3g7kee+eJ6LewPLcXU0bzwuuf2G3P3NoyevnzP3tsv3NOylqenkHzvD4fWQ197aikeW/nJJd1dnJ4//9On57V+a8Hoib7K4kQeUAWL0D7RcsJ2oqHv9wUcfu7Orq5MVK5Z3KS0P53j96lsgEPjyiKqKtW/891uu2tpalvzDMxsTW96s9yhMC8HUOCkxm07JO/fZk5A9dkmDTOSqWe/99fcfmRPtifHY3z6a6Q5F7gyFQhsKggFGjKh4wFviffG11153T59xHVu3bg3968/+7g9V3ae+0Zv0kX49l3ISjA2ccpe/NXvR9+uvnX5tRdOpJv7+xz9OxnpiD3d0d/97PqcXrLWeBcwGLnv11d96n3j88QVPPf108KHvPUwmk+HttWu71q96Y0dozzajJBUfXyqMA4gpfShmeY54JkzX19/6VzfMmDmjMpPOsOqtVbzyym9alz23fM23Fy1KACeAP4rIBwCitb4MeAQY5SxEt7a2qIaGBn70wx+OTKXTc5Y+t8w1d85cdN5KtdbYSqGVImPbJOIxotEo6/+wniXPPmsH/L4Ny5etaJk46Rqprq7JPTgooBn4Z9FaPw9UAHR1dSnbTsuZMy1GMpnItLZ2GFu3bq5d/fvVc0ZUjZB7F36d2fW3MmHCFZguF0pr0uk0Bxsb2bL5PV5fuZLuUEjfdffdG2+66ebW6mCVLvP5qa4OAoYEg8Gcg7tNIAIEADHdJnbcxmNZ6UQ05nK7TT1x4sRYRVV1/FTTqdLVa9bywgsvEImESKfSAFiWhT9QzqhL6rh25g3UjbokPnJkTaKkxFRaa8NtGbaIy+Up8eS2VgEx0VpXO66+HKfdbW9vV93d7RKNJl3xeNQOd4d1Mp0i3B3yRCKRsmgiYSVTaa9orS23lfR5vany8vKYLxCIeyxLKqoqtddbKh6PSVVVtQ4Gg5IHPQI8nx9ck4CbgSuBarJnvARsiUai4XBPmGQyqbWGRCxh2VrZAKYYLtNjZUyXSxsuU6oqyg1fwO91nhUSzvQdwB5gm4h8UvA4OYsoByYDY4EaoBLwAN7sYiDvZ4LsqUo60uNIK3AY2CMioYGM/wPREY0iGUY58wAAAABJRU5ErkJggg==\"],\"useMarkerImageFunction\":true,\"markerImageFunction\":\"var res = {\\n url: images[0],\\n size: 40\\n}\\nvar temperature = data['temperature'];\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120;\\n var index = Math.floor(4 * percent);\\n res.url = images[index];\\n}\\nreturn res;\",\"useColorFunction\":false}],\"fitMapBounds\":true,\"gmDefaultMapType\":\"roadmap\"},\"title\":\"Google Maps\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false}" - } - }, - { - "alias": "openstreetmap", - "name": "OpenStreetMap", - "descriptor": { - "type": "latest", - "sizeX": 8.5, - "sizeY": 6, - "resources": [], - "templateHtml": "", - "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidget('openstreet-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"title\": \"Google Map Configuration\",\n \"type\": \"object\",\n \"properties\": {\n \"defaultZoomLevel\": {\n \"title\": \"Default map zoom level (1 - 20)\",\n \"type\": \"number\"\n },\n \"fitMapBounds\": {\n \"title\": \"Fit map bounds to cover all markers\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"markersSettings\": {\n \"title\": \"Markers\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Marker settings\",\n \"type\": \"object\",\n \"properties\": {\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"lat\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"lng\"\n }, \n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"tooltipPattern\": {\n \"title\": \"Pattern ( for ex. 'Text ${keyName} units.' or '${#} units' )\",\n \"type\": \"string\",\n \"default\": \"Latitude: ${lat:7}
Longitude: ${lng:7}\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n },\n \"useColorFunction\": {\n \"title\": \"Use color function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"colorFunction\": {\n \"title\": \"Color function: f(data)\",\n \"type\": \"string\"\n },\n \"markerImage\": {\n \"title\": \"Custom marker image\",\n \"type\": \"string\"\n },\n \"markerImageSize\": {\n \"title\": \"Custom marker image size (px)\",\n \"type\": \"number\",\n \"default\": 34\n },\n \"useMarkerImageFunction\": {\n \"title\": \"Use marker image function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"markerImageFunction\": {\n \"title\": \"Marker image function: f(data, images)\",\n \"type\": \"string\"\n },\n \"markerImages\": {\n \"title\": \"Marker images\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Marker image\",\n \"type\": \"string\"\n }\n }\n }\n }\n }\n },\n \"required\": [\n ]\n },\n \"form\": [\n \"defaultZoomLevel\",\n \"fitMapBounds\",\n {\n \"key\": \"markersSettings\",\n \"items\": [\n \"markersSettings[].latKeyName\",\n \"markersSettings[].lngKeyName\",\n \"markersSettings[].showLabel\",\n \"markersSettings[].label\",\n \"markersSettings[].tooltipPattern\",\n {\n \"key\": \"markersSettings[].color\",\n \"type\": \"color\"\n },\n \"markersSettings[].useColorFunction\",\n {\n \"key\": \"markersSettings[].colorFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"markersSettings[].markerImage\",\n \"type\": \"image\"\n },\n \"markersSettings[].markerImageSize\",\n \"markersSettings[].useMarkerImageFunction\",\n {\n \"key\": \"markersSettings[].markerImageFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"markersSettings[].markerImages\",\n \"items\": [\n {\n \"key\": \"markersSettings[].markerImages[]\",\n \"type\": \"image\"\n }\n ]\n }\n ]\n }\n ]\n}", - "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"lat\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"lng\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.21553274887887564,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"markersSettings\":[{\"label\":\"First point\",\"color\":\"#1e88e5\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"tooltipPattern\":\"Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"useColorFunction\":true,\"colorFunction\":\"var temperature = data['temperature'];\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', amount = percent).toHexString();\\n}\\nreturn 'blue';\"},{\"label\":\"Second point\",\"color\":\"#fdd835\",\"latKeyName\":\"lat\",\"lngKeyName\":\"lng\",\"showLabel\":true,\"tooltipPattern\":\"Latitude: ${lat:7}
Longitude: ${lng:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"markerImageSize\":34,\"useMarkerImageFunction\":true,\"markerImages\":[\"data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnNjkzNCIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjEwLjg3bW0iIHdpZHRoPSI0OS45NjZtbSIgdmVyc2lvbj0iMS4xIiB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmlld0JveD0iMCAwIDE3Ny4wNDM3NSA3NDcuMTYyNDkiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyI+CiA8ZGVmcyBpZD0iZGVmczY5MzYiPgogIDxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyR3JhZGllbnQ0NzA3IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgwIC03MTcuNzcgLTcxNy43NyAwIDcwOC4xOCAyMTY4KSI+CiAgIDxzdG9wIGlkPSJzdG9wNDcwOSIgc3RvcC1jb2xvcj0iIzg0ZDdmNSIgb2Zmc2V0PSIwIi8+CiAgIDxzdG9wIGlkPSJzdG9wNDcxMSIgc3RvcC1jb2xvcj0iIzZhOTljZCIgb2Zmc2V0PSIxIi8+CiAgPC9saW5lYXJHcmFkaWVudD4KICA8bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhckdyYWRpZW50NDY4OSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoNDU2LjIyIDAgMCAtNDU2LjIyIDQ4MC4wNyAzNDk0LjMpIj4KICAgPHN0b3AgaWQ9InN0b3A0NjkxIiBzdG9wLWNvbG9yPSIjY2FjOWM4IiBvZmZzZXQ9IjAiLz4KICAgPHN0b3AgaWQ9InN0b3A0NjkzIiBzdG9wLWNvbG9yPSIjZjZmNmY2IiBvZmZzZXQ9Ii43NTI2OSIvPgogICA8c3RvcCBpZD0ic3RvcDQ2OTUiIHN0b3AtY29sb3I9IiNkNGQzZDIiIG9mZnNldD0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXJHcmFkaWVudDQ2NjkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEzNDUuNSAwIDAgLTEzNDUuNSAzNS40MjcgMzA2NS45KSI+CiAgIDxzdG9wIGlkPSJzdG9wNDY3MSIgc3RvcC1jb2xvcj0iI2IzYjNiMiIgb2Zmc2V0PSIwIi8+CiAgIDxzdG9wIGlkPSJzdG9wNDY3MyIgc3RvcC1jb2xvcj0iI2IzYjNiMiIgb2Zmc2V0PSIuMSIvPgogICA8c3RvcCBpZD0ic3RvcDQ2NzUiIHN0b3AtY29sb3I9IiNmZWZmZmYiIG9mZnNldD0iLjI0NzMxIi8+CiAgIDxzdG9wIGlkPSJzdG9wNDY3NyIgc3RvcC1jb2xvcj0iI2EzYTNhMSIgb2Zmc2V0PSIxIi8+CiAgPC9saW5lYXJHcmFkaWVudD4KICA8bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhckdyYWRpZW50NDU5MSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMCAtNzE3Ljc3IC03MTcuNzcgMCA3MDguMTggMjE2OCkiPgogICA8c3RvcCBpZD0ic3RvcDQ1OTMiIHN0b3AtY29sb3I9IiM1ZmM3ZjMiIG9mZnNldD0iMCIvPgogICA8c3RvcCBpZD0ic3RvcDQ1OTUiIHN0b3AtY29sb3I9IiMyNzUyYTYiIG9mZnNldD0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXJHcmFkaWVudDQ1NzMiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDQ1Ni4yMiAwIDAgLTQ1Ni4yMiA0ODAuMDcgMzQ5NC4zKSI+CiAgIDxzdG9wIGlkPSJzdG9wNDU3NSIgc3RvcC1jb2xvcj0iI2EzYTNhMSIgb2Zmc2V0PSIwIi8+CiAgIDxzdG9wIGlkPSJzdG9wNDU3NyIgc3RvcC1jb2xvcj0iI2VjZWNlYyIgb2Zmc2V0PSIuNzUyNjkiLz4KICAgPHN0b3AgaWQ9InN0b3A0NTc5IiBzdG9wLWNvbG9yPSIjYjNiM2IyIiBvZmZzZXQ9IjEiLz4KICA8L2xpbmVhckdyYWRpZW50PgogIDxjbGlwUGF0aCBpZD0iY2xpcFBhdGg2MDk3LTMiIGNsaXBQYXRoVW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgPHBhdGggaWQ9InBhdGg2MDk5LTEiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0ibTM4Mi4zMiAxOTAuMDNjMCA0OC4yMzkgMTQ1Ljg5IDg3LjM5MSAzMjUuODkgODcuMzkxIDE3OS45MSAwIDMyNS44LTM5LjE1MiAzMjUuOC04Ny4zOTEgMC00OC4zMzItMTQ1Ljg5LTg3LjQ4LTMyNS44LTg3LjQ4LTE4MCAwLTMyNS44OSAzOS4xNDgtMzI1Ljg5IDg3LjQ4Ii8+CiAgPC9jbGlwUGF0aD4KICA8Y2xpcFBhdGggaWQ9ImNsaXBQYXRoNDcwMy00IiBjbGlwUGF0aFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CiAgIDxwYXRoIGlkPSJwYXRoNDcwNS05IiBkPSJtNTg1LjI5IDE1MzEuN2MtMzAuNDY1IDAtNTUuMTYgMjQuNjktNTUuMTYgNTUuMTZ2NTI4LjUyYzI4LjQzNyAzMS4xOCA2Ni45OTIgNTMuMDEgMTEwLjMyIDYwLjE0di01ODguNjZjMC0zMC40Ny0yNC42OTUtNTUuMTYtNTUuMTYtNTUuMTYiLz4KICA8L2NsaXBQYXRoPgogIDxjbGlwUGF0aCBpZD0iY2xpcFBhdGg0Njg1LTYiIGNsaXBQYXRoVW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgPHBhdGggaWQ9InBhdGg0Njg3LTgiIGQ9Im01MzAuMTMgMjExNS40djM1NzQuMmMwIDMwLjQ2IDI0LjY5NSA1NS4xNiA1NS4xNiA1NS4xNnM1NS4xNi0yNC43IDU1LjE2LTU1LjE2di0zNTE0Yy00My4zMjgtNy4xMy04MS44ODMtMjguOTYtMTEwLjMyLTYwLjE0Ii8+CiAgPC9jbGlwUGF0aD4KICA8Y2xpcFBhdGggaWQ9ImNsaXBQYXRoNDY2NS02IiBjbGlwUGF0aFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CiAgIDxwYXRoIGlkPSJwYXRoNDY2Ny00IiBkPSJtNzQ0LjkgNTk0MS45aC03My40NDJjLTE1NC4zNyAwLTI3OS45Ni0xMjUuNTktMjc5Ljk2LTI3OS45NnYtNDIwNS41Yy0yMTguMTEtMTE2LjItMzU2LjA3LTM0My4yLTM1Ni4wNy01OTMuNjUtMC4wMDQtMzcwLjk2IDMwMS43OS02NzIuNzUgNjcyLjc1LTY3Mi43NSAzNzAuOTUgMCA2NzIuNzQgMzAxLjc5IDY3Mi43NCA2NzIuNzUgMCAyNTAuNDgtMTM3Ljk2IDQ3Ny40OC0zNTYuMDcgNTkzLjd2NDIwNS41YzAgMTU0LjM3LTEyNS41OSAyNzkuOTYtMjc5Ljk2IDI3OS45Nm0wLTg4LjU4YzEwNS4yNiAwIDE5MS4zOS04Ni4xMiAxOTEuMzktMTkxLjM4di00MjYxLjJjMjA5LjI5LTg4Ljg1IDM1Ni4wNy0yOTYuMjUgMzU2LjA3LTUzNy45NCAwLTMyMi42My0yNjEuNTQtNTg0LjE4LTU4NC4xNy01ODQuMThzLTU4NC4xOCAyNjEuNTUtNTg0LjE4IDU4NC4xOGMwIDI0MS42OSAxNDYuNzkgNDQ5LjA5IDM1Ni4wNyA1MzcuOTR2NDI2MS4yYzAgMTA1LjI2IDg2LjEyNSAxOTEuMzggMTkxLjM5IDE5MS4zOGg3My40NDIiLz4KICA8L2NsaXBQYXRoPgogIDxjbGlwUGF0aCBpZD0iY2xpcFBhdGg0NjUxLTgiIGNsaXBQYXRoVW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgPHBhdGggaWQ9InBhdGg0NjUzLTciIGQ9Im03MDguMTggOTU1Ljg5Yy0xMzUuNDggMC0yNTIuNTkgNDkuODY5LTMwOC4xNCAxMjIuMjQtMjIuMDA4IDI4LjY3LTM0LjM1OSA2MC44OC0zNC4zNTkgOTQuOTEgMCAxMTkuOTIgMTUzLjM0IDIxNy4xNCAzNDIuNSAyMTcuMTQgMTY0LjQgMCAzMDEuNzQtNzMuNDQgMzM0Ljg4LTE3MS4zOSA0Ljk4LTE0Ljc1IDcuNjEtMzAuMDYgNy42MS00NS43NSAwLTUzLjkyLTMwLjk5LTEwMy4yNC04Mi4yOTUtMTQxLjIxLTYyLjgxMi00Ni40NzgtMTU2LjA5LTc1LjkzOS0yNjAuMi03NS45MzkiLz4KICA8L2NsaXBQYXRoPgogIDxjbGlwUGF0aCBpZD0iY2xpcFBhdGg0NjM1LTEiIGNsaXBQYXRoVW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgPHBhdGggaWQ9InBhdGg0NjM3LTciIGQ9Im05NjguMzggMTAzMS44YzUxLjMwNSAzNy45NyA4Mi4yOTUgODcuMjkgODIuMjk1IDE0MS4yMXYtMC43MmMtMC4yOC01My42My0zMS4yMS0xMDIuNjktODIuMjk1LTE0MC40OW0tNTY4LjM0IDQ2LjNjLTIyLjAwOCAyOC42Ny0zNC4zNTkgNjAuODgtMzQuMzU5IDk0LjkxIDAtMzQuMDMgMTIuMzUxLTY2LjI0IDM0LjM1OS05NC45MW02NTAuNjMgOTQuOTFjMCAxNS42OS0yLjYzIDMxLTcuNjEgNDUuNzUgNC45MS0xNC41MyA3LjUzLTI5LjYgNy42MS00NS4wNHYtMC43MSIvPgogIDwvY2xpcFBhdGg+CiAgPHJhZGlhbEdyYWRpZW50IGlkPSJyYWRpYWxHcmFkaWVudDQ2MzktMiIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGN5PSIwIiBjeD0iMCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCg1ODQuMTggMCAwIC01ODQuMTggNzA4LjE4IDg2Mi43NSkiIHI9IjEiPgogICA8c3RvcCBpZD0ic3RvcDQ2NDEtNyIgc3RvcC1jb2xvcj0iIzVmYzdmMyIgb2Zmc2V0PSIwIi8+CiAgIDxzdG9wIGlkPSJzdG9wNDY0My0yIiBzdG9wLWNvbG9yPSIjMjc1MmE2IiBvZmZzZXQ9IjEiLz4KICA8L3JhZGlhbEdyYWRpZW50PgogIDxjbGlwUGF0aCBpZD0iY2xpcFBhdGg0NjE5LTIiIGNsaXBQYXRoVW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgPHBhdGggaWQ9InBhdGg0NjIxLTYiIGQ9Im0xMDUwLjcgMTE3Mi4zdjAuNzIgMC43MS0wLjcxLTAuNzIiLz4KICA8L2NsaXBQYXRoPgogIDxjbGlwUGF0aCBpZD0iY2xpcFBhdGg0NjAzLTEiIGNsaXBQYXRoVW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgPHBhdGggaWQ9InBhdGg0NjA1LTUiIGQ9Im0xMjkyLjQgODYyLjc1YzAtMzIyLjY0LTI2MS41NC01ODQuMTgtNTg0LjE3LTU4NC4xOHMtNTg0LjE4IDI2MS41NC01ODQuMTggNTg0LjE4YzAgMzIyLjYzIDI2MS41NSA1ODQuMTggNTg0LjE4IDU4NC4xOHM1ODQuMTctMjYxLjU1IDU4NC4xNy01ODQuMTgiLz4KICA8L2NsaXBQYXRoPgogIDxjbGlwUGF0aCBpZD0iY2xpcFBhdGg0NTg3LTAiIGNsaXBQYXRoVW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgPHBhdGggaWQ9InBhdGg0NTg5LTkiIGQ9Im00ODAuMDcgMTQwMC43djU4NS45NWMwIDQ5LjQ1IDE5IDk0LjY3IDUwLjA2NyAxMjguNzJ2LTUyOC41MmMwLTMwLjQ3IDI0LjY5NS01NS4xNiA1NS4xNi01NS4xNnM1NS4xNiAyNC42OSA1NS4xNiA1NS4xNnY1ODguNjZjMTAuMDk4IDEuNjcgMjAuNDUzIDIuNTMgMzEuMDA0IDIuNTNoNzMuNDM4YzEwNS4yNiAwIDE5MS4zOS04Ni4xMiAxOTEuMzktMTkxLjM5di01ODUuOTNjLTcwLjA4MiAyOS43Ni0xNDcuMTcgNDYuMjItMjI4LjEgNDYuMjItODAuOTM0IDAtMTU4LjAzLTE2LjQ2LTIyOC4xMS00Ni4yMiIvPgogIDwvY2xpcFBhdGg+CiAgPGNsaXBQYXRoIGlkPSJjbGlwUGF0aDQ1NjktNyIgY2xpcFBhdGhVbml0cz0idXNlclNwYWNlT25Vc2UiPgogICA8cGF0aCBpZD0icGF0aDQ1NzEtMSIgZD0ibTkzNi4yOCAxOTg2LjZjMCAxMDUuMjctODYuMTIxIDE5MS4zOS0xOTEuMzkgMTkxLjM5aC03My40MzhjLTEwLjU1MSAwLTIwLjkwNi0wLjg2LTMxLjAwNC0yLjUzdjM1MTRjMCAzMC40Ni0yNC42OTUgNTUuMTYtNTUuMTYgNTUuMTZzLTU1LjE2LTI0LjctNTUuMTYtNTUuMTZ2LTM1NzQuMmMtMzEuMDY3LTM0LjA1LTUwLjA2Ny03OS4yNy01MC4wNjctMTI4LjcydjM2NzUuM2MwIDEzLjQ2IDEuNDExIDI2LjYyIDQuMDkgMzkuMzEgMTguMjYyIDg2LjU3IDk1LjUwNCAxNTIuMDcgMTg3LjMgMTUyLjA3aDczLjQzOGMxMDUuMjYgMCAxOTEuMzktODYuMTIgMTkxLjM5LTE5MS4zOHYtMzY3NS4zIi8+CiAgPC9jbGlwUGF0aD4KIDwvZGVmcz4KIDxnIGlkPSJsYXllcjEiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0zNjIuNzYgLTE4OC44NCkiPgogIDxnIGZpbGw9IiNmZmYiPgogICA8cGF0aCBpZD0icGF0aDQxODMiIGQ9Im00NTEuMjggOTM2Yy00OC44ODkgMC04OC41MjItMTAuNjMyLTg4LjUyMi0yMy43NSAwLTcuMDg2NCAxMS41NzEtMTMuNDQ5IDI5LjkxOC0xNy44IDAuMDM1NiAwLjAyOTggMC4wNjc0IDAuMDU4NSAwLjEwMjUgMC4wODk3LTE4LjIyIDQuMzM1LTI5LjcgMTAuNjYzLTI5LjcgMTcuNzEgMCAxMy4wNjkgMzkuNDg5IDIzLjY2NCA4OC4yMDEgMjMuNjY0czg4LjItMTAuNTk1IDg4LjItMjMuNjY0YzAtNy4wNDc0LTExLjQ4LTEzLjM3NS0yOS43LTE3LjcxIDAuMDM1LTAuMDMxMiAwLjA2NzUtMC4wNiAwLjEwMjUtMC4wODk3IDE4LjM0OCA0LjM1MSAyOS45MTkgMTAuNzEzIDI5LjkxOSAxNy44IDAgMTMuMTE4LTM5LjYzMiAyMy43NS04OC41MjIgMjMuNzUiLz4KICAgPHBhdGggaWQ9InBhdGg0MTg1IiBkPSJtNDUxLjI4IDkzNS45MmMtNDguNzEyIDAtODguMjAxLTEwLjU5NS04OC4yMDEtMjMuNjY0IDAtNy4wNDc0IDExLjQ4LTEzLjM3NSAyOS43LTE3LjcxIDAuMDMzMyAwLjAyODkgMC4wNjk5IDAuMDYxMSAwLjEwMyAwLjA5MTQtMTguMDg5IDQuMzE3NC0yOS40ODIgMTAuNjExLTI5LjQ4MiAxNy42MTkgMCAxMy4wMjEgMzkuMzQ1IDIzLjU3OCA4Ny44OCAyMy41NzggNDguNTM2IDAgODcuODgtMTAuNTU3IDg3Ljg4LTIzLjU3OCAwLTcuMDA3NC0xMS4zOTQtMTMuMzAxLTI5LjQ4Mi0xNy42MTkgMC4wMzI1LTAuMDMwMiAwLjA3LTAuMDYyNSAwLjEwMjUtMC4wOTE0IDE4LjIyIDQuMzM1IDI5LjcgMTAuNjYzIDI5LjcgMTcuNzEgMCAxMy4wNjktMzkuNDg5IDIzLjY2NC04OC4yIDIzLjY2NCIvPgogICA8cGF0aCBpZD0icGF0aDQxODciIGQ9Im00NTEuMjggOTM1LjgzYy00OC41MzUgMC04Ny44OC0xMC41NTctODcuODgtMjMuNTc4IDAtNy4wMDc0IDExLjM5My0xMy4zMDEgMjkuNDgyLTE3LjYxOSAwLjAzNTEgMC4wMjk3IDAuMDY3OSAwLjA1ODYgMC4xMDMgMC4wODk5LTE3Ljk1OSA0LjMwMjYtMjkuMjY1IDEwLjU2Mi0yOS4yNjUgMTcuNTI5IDAgMTIuOTc0IDM5LjIwMiAyMy40OTEgODcuNTYgMjMuNDkxczg3LjU1OS0xMC41MTggODcuNTU5LTIzLjQ5MWMwLTYuOTY3Mi0xMS4zMDUtMTMuMjI2LTI5LjI2NS0xNy41MjkgMC4wMzYyLTAuMDMxMiAwLjA2NzUtMC4wNjAxIDAuMTAzNzUtMC4wODk5IDE4LjA4OSA0LjMxNzQgMjkuNDgyIDEwLjYxMSAyOS40ODIgMTcuNjE5IDAgMTMuMDIxLTM5LjM0NSAyMy41NzgtODcuODggMjMuNTc4Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNDE4OSIgZD0ibTQ1MS4yOCA5MzUuNzRjLTQ4LjM1OCAwLTg3LjU2LTEwLjUxOC04Ny41Ni0yMy40OTEgMC02Ljk2NzIgMTEuMzA2LTEzLjIyNiAyOS4yNjUtMTcuNTI5IDAuMDMzNyAwLjAyODggMC4wNzA0IDAuMDYxIDAuMTAzNjIgMC4wODk3LTE3LjgzIDQuMjg2Ny0yOS4wNDcgMTAuNTExLTI5LjA0NyAxNy40MzkgMCAxMi45MjYgMzkuMDU4IDIzLjQwNiA4Ny4yMzkgMjMuNDA2IDQ4LjE4IDAgODcuMjM5LTEwLjQ4IDg3LjIzOS0yMy40MDYgMC02LjkyNzgtMTEuMjE5LTEzLjE1Mi0yOS4wNDktMTcuNDM5IDAuMDMzNy0wLjAyODcgMC4wNzEyLTAuMDYxIDAuMTAzNzUtMC4wODk3IDE3Ljk2IDQuMzAyNiAyOS4yNjUgMTAuNTYyIDI5LjI2NSAxNy41MjkgMCAxMi45NzQtMzkuMjAxIDIzLjQ5MS04Ny41NTkgMjMuNDkxIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDE5MSIgZD0ibTQ1MS4yOCA5MzUuNjZjLTQ4LjE4MSAwLTg3LjIzOS0xMC40OC04Ny4yMzktMjMuNDA2IDAtNi45Mjc4IDExLjIxOC0xMy4xNTIgMjkuMDQ3LTE3LjQzOSAwLjAzNTYgMC4wMzEyIDAuMDY4MyAwLjA2MDEgMC4xMDQgMC4wOTE0LTE3LjcwMSA0LjI2OS0yOC44MzEgMTAuNDU5LTI4LjgzMSAxNy4zNDggMCAxMi44NzkgMzguOTE0IDIzLjMyIDg2LjkxOCAyMy4zMiA0OC4wMDMgMCA4Ni45MTgtMTAuNDQxIDg2LjkxOC0yMy4zMiAwLTYuODg4Ni0xMS4xMy0xMy4wNzktMjguODMxLTE3LjM0OCAwLjAzNjMtMC4wMzEyIDAuMDY4OC0wLjA2MDEgMC4xMDM3NS0wLjA5MTQgMTcuODMgNC4yODY2IDI5LjA0OSAxMC41MTEgMjkuMDQ5IDE3LjQzOSAwIDEyLjkyNi0zOS4wNTkgMjMuNDA2LTg3LjIzOSAyMy40MDYiLz4KICAgPHBhdGggaWQ9InBhdGg0MTkzIiBkPSJtNDUxLjI4IDkzNS41N2MtNDguMDA0IDAtODYuOTE4LTEwLjQ0MS04Ni45MTgtMjMuMzIgMC02Ljg4ODYgMTEuMTI5LTEzLjA3OSAyOC44MzEtMTcuMzQ4IDAuMDM1NiAwLjAzMTIgMC4wNjg4IDAuMDYgMC4xMDQgMC4wOTAyLTE3LjU2OSA0LjI1MjYtMjguNjE0IDEwLjQxLTI4LjYxNCAxNy4yNTcgMCAxMi44MzEgMzguNzcxIDIzLjIzNCA4Ni41OTcgMjMuMjM0IDQ3LjgyNyAwIDg2LjU5Ny0xMC40MDMgODYuNTk3LTIzLjIzNCAwLTYuODQ3Ni0xMS4wNDUtMTMuMDA1LTI4LjYxNC0xNy4yNTcgMC4wMzUtMC4wMzAzIDAuMDY4Ny0wLjA1OSAwLjEwMzc1LTAuMDkwMiAxNy43MDEgNC4yNjkgMjguODMxIDEwLjQ1OSAyOC44MzEgMTcuMzQ4IDAgMTIuODc5LTM4LjkxNSAyMy4zMi04Ni45MTggMjMuMzIiLz4KICAgPHBhdGggaWQ9InBhdGg0MTk1IiBkPSJtNDUxLjI4IDkzNS40OWMtNDcuODI3IDAtODYuNTk3LTEwLjQwMy04Ni41OTctMjMuMjM0IDAtNi44NDc2IDExLjA0NS0xMy4wMDUgMjguNjE0LTE3LjI1NyAwLjAzMzYgMC4wMjk5IDAuMDcxMiAwLjA2MjUgMC4xMDQ1IDAuMDkxNC0xNy40NDEgNC4yMzQ5LTI4LjM5OCAxMC4zNTctMjguMzk4IDE3LjE2NiAwIDEyLjc4NCAzOC42MjcgMjMuMTQ3IDg2LjI3NyAyMy4xNDcgNDcuNjQ5IDAgODYuMjc3LTEwLjM2NCA4Ni4yNzctMjMuMTQ3IDAtNi44MDg2LTEwLjk1OC0xMi45MzEtMjguMzk5LTE3LjE2NiAwLjAzMzctMC4wMjg5IDAuMDcxMi0wLjA2MTUgMC4xMDUtMC4wOTE0IDE3LjU2OSA0LjI1MjUgMjguNjE0IDEwLjQxIDI4LjYxNCAxNy4yNTcgMCAxMi44MzEtMzguNzcgMjMuMjM0LTg2LjU5NyAyMy4yMzQiLz4KICAgPHBhdGggaWQ9InBhdGg0MTk3IiBkPSJtNDUxLjI4IDkzNS40Yy00Ny42NSAwLTg2LjI3Ny0xMC4zNjQtODYuMjc3LTIzLjE0NyAwLTYuODA4NiAxMC45NTctMTIuOTMxIDI4LjM5OC0xNy4xNjYgMC4wMzU2IDAuMDMwOCAwLjA2OTMgMC4wNTk1IDAuMTA0ODggMC4wOTA4LTE3LjMxIDQuMjE4Ny0yOC4xODIgMTAuMzA3LTI4LjE4MiAxNy4wNzUgMCAxMi43MzYgMzguNDg0IDIzLjA2MSA4NS45NTYgMjMuMDYxczg1Ljk1Ni0xMC4zMjUgODUuOTU2LTIzLjA2MWMwLTYuNzY4NS0xMC44NzEtMTIuODU2LTI4LjE4MS0xNy4wNzUgMC4wMzUtMC4wMzEyIDAuMDY4OC0wLjA2IDAuMTAzNzUtMC4wOTA4IDE3LjQ0MSA0LjIzNDkgMjguMzk5IDEwLjM1NyAyOC4zOTkgMTcuMTY2IDAgMTIuNzg0LTM4LjYyOCAyMy4xNDctODYuMjc3IDIzLjE0NyIvPgogICA8cGF0aCBpZD0icGF0aDQxOTkiIGQ9Im00NTEuMjggOTM1LjMxYy00Ny40NzIgMC04NS45NTYtMTAuMzI1LTg1Ljk1Ni0yMy4wNjEgMC02Ljc2ODUgMTAuODcyLTEyLjg1NiAyOC4xODItMTcuMDc1IDAuMDM1NyAwLjAzMDQgMC4wNjk5IDAuMDYwMSAwLjEwNTUgMC4wOTA0LTE3LjE4NCA0LjIwMjEtMjcuOTY3IDEwLjI1Ni0yNy45NjcgMTYuOTg1IDAgMTIuNjg4IDM4LjM0IDIyLjk3NiA4NS42MzUgMjIuOTc2IDQ3LjI5NCAwIDg1LjYzNi0xMC4yODggODUuNjM2LTIyLjk3NiAwLTYuNzI5LTEwLjc4NC0xMi43ODMtMjcuOTY4LTE2Ljk4NSAwLjAzNjItMC4wMzAzIDAuMDctMC4wNiAwLjEwNjI1LTAuMDkwNCAxNy4zMSA0LjIxODggMjguMTgxIDEwLjMwNyAyOC4xODEgMTcuMDc1IDAgMTIuNzM2LTM4LjQ4NCAyMy4wNjEtODUuOTU2IDIzLjA2MSIvPgogICA8cGF0aCBpZD0icGF0aDQyMDEiIGQ9Im00NTEuMjggOTM1LjIzYy00Ny4yOTUgMC04NS42MzUtMTAuMjg4LTg1LjYzNS0yMi45NzYgMC02LjcyOSAxMC43ODMtMTIuNzgzIDI3Ljk2Ny0xNi45ODUgMC4wMzM4IDAuMDI4OSAwLjA3MTggMC4wNjI1IDAuMTA1NSAwLjA5MTQtMTcuMDU0IDQuMTg0NS0yNy43NTEgMTAuMjA0LTI3Ljc1MSAxNi44OTQgMCAxMi42NDEgMzguMTk2IDIyLjg5IDg1LjMxNCAyMi44OXM4NS4zMTQtMTAuMjQ5IDg1LjMxNC0yMi44OWMwLTYuNjg5LTEwLjY5OC0xMi43MDktMjcuNzUxLTE2Ljg5NCAwLjAzMzctMC4wMjg5IDAuMDcxMi0wLjA2MjUgMC4xMDUtMC4wOTE0IDE3LjE4NCA0LjIwMjEgMjcuOTY4IDEwLjI1NiAyNy45NjggMTYuOTg1IDAgMTIuNjg4LTM4LjM0MSAyMi45NzYtODUuNjM2IDIyLjk3NiIvPgogICA8cGF0aCBpZD0icGF0aDQyMDMiIGQ9Im00NTEuMjggOTM1LjE0Yy00Ny4xMTggMC04NS4zMTQtMTAuMjQ5LTg1LjMxNC0yMi44OSAwLTYuNjg5IDEwLjY5Ny0xMi43MDkgMjcuNzUxLTE2Ljg5NCAwLjAzNTYgMC4wMjk4IDAuMDY5NyAwLjA1OTUgMC4xMDYgMC4wODk3LTE2LjkyNSA0LjE2OS0yNy41MzcgMTAuMTU0LTI3LjUzNyAxNi44MDQgMCAxMi41OTQgMzguMDUzIDIyLjgwNCA4NC45OTQgMjIuODA0IDQ2Ljk0IDAgODQuOTkzLTEwLjIxIDg0Ljk5My0yMi44MDQgMC02LjY0OTktMTAuNjExLTEyLjYzNS0yNy41MzYtMTYuODA0IDAuMDM2Mi0wLjAzMDMgMC4wNy0wLjA2IDAuMTA2MjUtMC4wODk3IDE3LjA1NCA0LjE4NDUgMjcuNzUxIDEwLjIwNCAyNy43NTEgMTYuODk0IDAgMTIuNjQxLTM4LjE5NiAyMi44OS04NS4zMTQgMjIuODkiLz4KICAgPHBhdGggaWQ9InBhdGg0MjA1IiBkPSJtNDUxLjI4IDkzNS4wNmMtNDYuOTQgMC04NC45OTQtMTAuMjEtODQuOTk0LTIyLjgwNCAwLTYuNjQ5OSAxMC42MTEtMTIuNjM1IDI3LjUzNy0xNi44MDQgMC4wMzU2IDAuMDMxMiAwLjA3MDMgMC4wNjAxIDAuMTA1ODcgMC4wOTE0LTE2Ljc5NiA0LjE1MTQtMjcuMzIyIDEwLjEwMi0yNy4zMjIgMTYuNzEyIDAgMTIuNTQ2IDM3LjkwOSAyMi43MTcgODQuNjczIDIyLjcxNyA0Ni43NjMgMCA4NC42NzMtMTAuMTcxIDg0LjY3My0yMi43MTcgMC02LjYwOTktMTAuNTI2LTEyLjU2MS0yNy4zMjItMTYuNzEyIDAuMDM2Mi0wLjAzMTIgMC4wNzEyLTAuMDYwMSAwLjEwNjI1LTAuMDkxNCAxNi45MjUgNC4xNjkgMjcuNTM2IDEwLjE1NCAyNy41MzYgMTYuODA0IDAgMTIuNTk0LTM4LjA1MiAyMi44MDQtODQuOTkzIDIyLjgwNCIvPgogICA8cGF0aCBpZD0icGF0aDQyMDciIGQ9Im00NTEuMjggOTM0Ljk3Yy00Ni43NjQgMC04NC42NzMtMTAuMTcxLTg0LjY3My0yMi43MTcgMC02LjYwOTkgMTAuNTI1LTEyLjU2MSAyNy4zMjItMTYuNzEyIDAuMDM2MSAwLjAyOTcgMC4wNzA5IDAuMDYgMC4xMDcgMC4wODk5LTE2LjY2OSA0LjEzNTItMjcuMTA4IDEwLjA1My0yNy4xMDggMTYuNjIyIDAgMTIuNDk5IDM3Ljc2NiAyMi42MzEgODQuMzUyIDIyLjYzMSA0Ni41ODcgMCA4NC4zNTItMTAuMTMzIDg0LjM1Mi0yMi42MzEgMC02LjU2OTktMTAuNDM5LTEyLjQ4Ny0yNy4xMDgtMTYuNjIyIDAuMDM2Mi0wLjAyOTkgMC4wNzEyLTAuMDYwMSAwLjEwNjI1LTAuMDg5OSAxNi43OTYgNC4xNTE0IDI3LjMyMiAxMC4xMDIgMjcuMzIyIDE2LjcxMiAwIDEyLjU0Ni0zNy45MSAyMi43MTctODQuNjczIDIyLjcxNyIvPgogICA8cGF0aCBpZD0icGF0aDQyMDkiIGQ9Im00NTEuMjggOTM0Ljg4Yy00Ni41ODYgMC04NC4zNTItMTAuMTMzLTg0LjM1Mi0yMi42MzEgMC02LjU2OTkgMTAuNDM5LTEyLjQ4NyAyNy4xMDgtMTYuNjIyIDAuMDM1NiAwLjAzMTIgMC4wNzA4IDAuMDYxNSAwLjEwNjM4IDAuMDkxMy0xNi41NDEgNC4xMTc2LTI2Ljg5NCAxMC0yNi44OTQgMTYuNTMxIDAgMTIuNDUxIDM3LjYyMiAyMi41NDUgODQuMDMxIDIyLjU0NXM4NC4wMy0xMC4wOTQgODQuMDMtMjIuNTQ1YzAtNi41MzEyLTEwLjM1Mi0xMi40MTQtMjYuODk0LTE2LjUzMSAwLjAzNjItMC4wMjk4IDAuMDcxMi0wLjA2IDAuMTA3NS0wLjA5MTMgMTYuNjY5IDQuMTM1MiAyNy4xMDggMTAuMDUzIDI3LjEwOCAxNi42MjIgMCAxMi40OTktMzcuNzY1IDIyLjYzMS04NC4zNTIgMjIuNjMxIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDIxMSIgZD0ibTQ1MS4yOCA5MzQuOGMtNDYuNDA5IDAtODQuMDMxLTEwLjA5NC04NC4wMzEtMjIuNTQ1IDAtNi41MzEyIDEwLjM1My0xMi40MTQgMjYuODk0LTE2LjUzMSAwLjAzNjEgMC4wMjk3IDAuMDcxOCAwLjA2IDAuMTA3NSAwLjA4OTktMTYuNDE0IDQuMTAxNS0yNi42OCA5Ljk1MDEtMjYuNjggMTYuNDQxIDAgMTIuNDA0IDM3LjQ3OCAyMi40NTkgODMuNzEgMjIuNDU5czgzLjcxLTEwLjA1NSA4My43MS0yMi40NTljMC02LjQ5MTItMTAuMjY2LTEyLjM0LTI2LjY4LTE2LjQ0MSAwLjAzNS0wLjAyOTkgMC4wNzEyLTAuMDYwMSAwLjEwNjI1LTAuMDg5OSAxNi41NDEgNC4xMTc2IDI2Ljg5NCAxMCAyNi44OTQgMTYuNTMxIDAgMTIuNDUxLTM3LjYyMSAyMi41NDUtODQuMDMgMjIuNTQ1Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNDIxMyIgZD0ibTQ1MS4yOCA5MzQuNzFjLTQ2LjIzMiAwLTgzLjcxLTEwLjA1NS04My43MS0yMi40NTkgMC02LjQ5MTIgMTAuMjY2LTEyLjM0IDI2LjY4LTE2LjQ0MSAwLjAzNjEgMC4wMzEyIDAuMDcxOCAwLjA2MTUgMC4xMDc4NyAwLjA5MTItMTYuMjg4IDQuMDg0LTI2LjQ2NyA5Ljg5NzUtMjYuNDY3IDE2LjM1IDAgMTIuMzU2IDM3LjMzNSAyMi4zNzMgODMuMzkgMjIuMzczIDQ2LjA1NCAwIDgzLjM4OS0xMC4wMTYgODMuMzg5LTIyLjM3MyAwLTYuNDUyNi0xMC4xNzktMTIuMjY2LTI2LjQ2OC0xNi4zNSAwLjAzNjItMC4wMjk3IDAuMDcyNS0wLjA2IDAuMTA4NzUtMC4wOTEyIDE2LjQxNCA0LjEwMTUgMjYuNjggOS45NTAxIDI2LjY4IDE2LjQ0MSAwIDEyLjQwNC0zNy40NzkgMjIuNDU5LTgzLjcxIDIyLjQ1OSIvPgogICA8cGF0aCBpZD0icGF0aDQyMTUiIGQ9Im00NTEuMjggOTM0LjYyYy00Ni4wNTUgMC04My4zOS0xMC4wMTYtODMuMzktMjIuMzczIDAtNi40NTI2IDEwLjE3OS0xMi4yNjYgMjYuNDY3LTE2LjM1IDAuMDM2MSAwLjAzMDMgMC4wNzE4IDAuMDYwMSAwLjEwNzg4IDAuMDkxNC0xNi4xNTkgNC4wNjY0LTI2LjI1NCA5Ljg0NjEtMjYuMjU0IDE2LjI1OSAwIDEyLjMwOSAzNy4xOTEgMjIuMjg4IDgzLjA2OSAyMi4yODhzODMuMDY5LTkuOTc5IDgzLjA2OS0yMi4yODhjMC02LjQxMjYtMTAuMDk2LTEyLjE5Mi0yNi4yNTUtMTYuMjU5IDAuMDM2Mi0wLjAzMTIgMC4wNzI1LTAuMDYxMSAwLjEwNzUtMC4wOTE0IDE2LjI4OSA0LjA4NCAyNi40NjggOS44OTc1IDI2LjQ2OCAxNi4zNSAwIDEyLjM1Ni0zNy4zMzUgMjIuMzczLTgzLjM4OSAyMi4zNzMiLz4KICAgPHBhdGggaWQ9InBhdGg0MjE3IiBkPSJtNDUxLjI4IDkzNC41NGMtNDUuODc4IDAtODMuMDY5LTkuOTc5LTgzLjA2OS0yMi4yODggMC02LjQxMjYgMTAuMDk1LTEyLjE5MiAyNi4yNTQtMTYuMjU5IDAuMDM2MSAwLjAzMDMgMC4wNzIyIDAuMDYgMC4xMDgzNyAwLjA4OTgtMTYuMDMxIDQuMDQ4OS0yNi4wNDIgOS43OTY1LTI2LjA0MiAxNi4xNjkgMCAxMi4yNjEgMzcuMDQ3IDIyLjIwMSA4Mi43NDggMjIuMjAxIDQ1LjcgMCA4Mi43NDgtOS45NCA4Mi43NDgtMjIuMjAxIDAtNi4zNzI1LTEwLjAxMS0xMi4xMi0yNi4wNDItMTYuMTY5IDAuMDM2Mi0wLjAyOTggMC4wNzI1LTAuMDU5NSAwLjEwODc1LTAuMDg5OCAxNi4xNTkgNC4wNjY0IDI2LjI1NSA5Ljg0NjEgMjYuMjU1IDE2LjI1OSAwIDEyLjMwOS0zNy4xOTEgMjIuMjg4LTgzLjA2OSAyMi4yODgiLz4KICAgPHBhdGggaWQ9InBhdGg0MjE5IiBkPSJtNDUxLjI4IDkzNC40NWMtNDUuNzAxIDAtODIuNzQ4LTkuOTQtODIuNzQ4LTIyLjIwMSAwLTYuMzcyNSAxMC4wMTEtMTIuMTIgMjYuMDQyLTE2LjE2OSAwLjAzNjEgMC4wMzA0IDAuMDcyNyAwLjA2MTYgMC4xMDg4OCAwLjA5MTQtMTUuOTA2IDQuMDMxMi0yNS44MyA5Ljc0NDEtMjUuODMgMTYuMDc4IDAgMTIuMjE0IDM2LjkwNCAyMi4xMTUgODIuNDI3IDIyLjExNXM4Mi40MjctOS45MDA5IDgyLjQyNy0yMi4xMTVjMC02LjMzMzUtOS45MjM4LTEyLjA0Ni0yNS44My0xNi4wNzggMC4wMzYyLTAuMDI5OCAwLjA3MjUtMC4wNjEgMC4xMDg3NS0wLjA5MTQgMTYuMDMxIDQuMDQ4OSAyNi4wNDIgOS43OTY1IDI2LjA0MiAxNi4xNjkgMCAxMi4yNjEtMzcuMDQ4IDIyLjIwMS04Mi43NDggMjIuMjAxIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDIyMSIgZD0ibTQ1MS4yOCA5MzQuMzdjLTQ1LjUyNCAwLTgyLjQyNy05LjkwMDktODIuNDI3LTIyLjExNSAwLTYuMzMzNSA5LjkyMzgtMTIuMDQ2IDI1LjgzLTE2LjA3OCAwLjAzNjIgMC4wMzAzIDAuMDcyNyAwLjA2MTUgMC4xMDkgMC4wOTEzLTE1Ljc3OSA0LjAxMzgtMjUuNjE4IDkuNjkxNS0yNS42MTggMTUuOTg2IDAgMTIuMTY2IDM2Ljc2IDIyLjAyOSA4Mi4xMDYgMjIuMDI5czgyLjEwNy05Ljg2MjggODIuMTA3LTIyLjAyOWMwLTYuMjk0OS05Ljg0LTExLjk3My0yNS42MTktMTUuOTg2IDAuMDM2Mi0wLjAyOTcgMC4wNzI1LTAuMDYxIDAuMTA4NzUtMC4wOTEzIDE1LjkwNiA0LjAzMTIgMjUuODMgOS43NDQxIDI1LjgzIDE2LjA3OCAwIDEyLjIxNC0zNi45MDQgMjIuMTE1LTgyLjQyNyAyMi4xMTUiLz4KICAgPHBhdGggaWQ9InBhdGg0MjIzIiBkPSJtNDUxLjI4IDkzNC4yOGMtNDUuMzQ2IDAtODIuMTA2LTkuODYyOC04Mi4xMDYtMjIuMDI5IDAtNi4yOTQ5IDkuODM5NC0xMS45NzMgMjUuNjE4LTE1Ljk4NiAwLjAzNjEgMC4wMzA0IDAuMDczMSAwLjA2MDEgMC4xMDkzNyAwLjA4OTktMTUuNjUyIDMuOTk3Ni0yNS40MDcgOS42NDE2LTI1LjQwNyAxNS44OTYgMCAxMi4xMTkgMzYuNjE3IDIxLjk0MiA4MS43ODYgMjEuOTQyczgxLjc4Ni05LjgyMzcgODEuNzg2LTIxLjk0MmMwLTYuMjU0OS05Ljc1NS0xMS44OTktMjUuNDA2LTE1Ljg5NiAwLjAzNjItMC4wMjk3IDAuMDcyNS0wLjA1OTUgMC4xMDg3NS0wLjA4OTkgMTUuNzc5IDQuMDEzOCAyNS42MTkgOS42OTE1IDI1LjYxOSAxNS45ODYgMCAxMi4xNjYtMzYuNzYxIDIyLjAyOS04Mi4xMDcgMjIuMDI5Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNDIyNSIgZD0ibTQ1MS4yOCA5MzQuMTljLTQ1LjE3IDAtODEuNzg2LTkuODIzNy04MS43ODYtMjEuOTQyIDAtNi4yNTQ5IDkuNzU0OS0xMS44OTkgMjUuNDA3LTE1Ljg5NiAwLjAzNjEgMC4wMzAzIDAuMDczNiAwLjA2MTUgMC4xMDk3NSAwLjA5MTQtMTUuNTI2IDMuOTc5OS0yNS4xOTYgOS41ODg5LTI1LjE5NiAxNS44MDUgMCAxMi4wNzEgMzYuNDczIDIxLjg1NiA4MS40NjUgMjEuODU2czgxLjQ2Ni05Ljc4NTIgODEuNDY2LTIxLjg1NmMwLTYuMjE2Mi05LjY3LTExLjgyNS0yNS4xOTYtMTUuODA1IDAuMDM2Mi0wLjAyOTkgMC4wNzM4LTAuMDYxMSAwLjExLTAuMDkxNCAxNS42NTEgMy45OTc2IDI1LjQwNiA5LjY0MTYgMjUuNDA2IDE1Ljg5NiAwIDEyLjExOS0zNi42MTYgMjEuOTQyLTgxLjc4NiAyMS45NDIiLz4KICAgPHBhdGggaWQ9InBhdGg0MjI3IiBkPSJtNDUxLjI4IDkzNC4xMWMtNDQuOTkyIDAtODEuNDY1LTkuNzg1Mi04MS40NjUtMjEuODU2IDAtNi4yMTYyIDkuNjctMTEuODI1IDI1LjE5Ni0xNS44MDUgMC4wMzYyIDAuMDMwMiAwLjA3MzggMC4wNjE1IDAuMTEwMzggMC4wOTEyLTE1LjM5OCAzLjk2MTQtMjQuOTg2IDkuNTM3Ni0yNC45ODYgMTUuNzE0IDAgMTIuMDI0IDM2LjMzIDIxLjc3IDgxLjE0NSAyMS43NyA0NC44MTQgMCA4MS4xNDQtOS43NDYxIDgxLjE0NC0yMS43NyAwLTYuMTc2Mi05LjU4NzUtMTEuNzUyLTI0Ljk4Ni0xNS43MTQgMC4wMzYyLTAuMDI5NyAwLjA3NS0wLjA2MSAwLjExMTI1LTAuMDkxMiAxNS41MjYgMy45Nzk5IDI1LjE5NiA5LjU4ODkgMjUuMTk2IDE1LjgwNSAwIDEyLjA3MS0zNi40NzQgMjEuODU2LTgxLjQ2NiAyMS44NTYiLz4KICAgPHBhdGggaWQ9InBhdGg0MjI5IiBkPSJtNDUxLjI4IDkzNC4wMmMtNDQuODE1IDAtODEuMTQ1LTkuNzQ2MS04MS4xNDUtMjEuNzcgMC02LjE3NjIgOS41ODc5LTExLjc1MiAyNC45ODYtMTUuNzE0IDAuMDM2MSAwLjAyODkgMC4wNzQyIDAuMDYwMSAwLjExMDM3IDAuMDkwNC0xNS4yNzMgMy45NDQ4LTI0Ljc3NSA5LjQ4NTgtMjQuNzc1IDE1LjYyNCAwIDExLjk3NiAzNi4xODYgMjEuNjg1IDgwLjgyNCAyMS42ODVzODAuODIzLTkuNzA5IDgwLjgyMy0yMS42ODVjMC02LjEzNzgtOS41MDI1LTExLjY3OS0yNC43NzUtMTUuNjI0IDAuMDM2Mi0wLjAzMDMgMC4wNzM4LTAuMDYxNSAwLjExLTAuMDkwNCAxNS4zOTkgMy45NjE0IDI0Ljk4NiA5LjUzNzYgMjQuOTg2IDE1LjcxNCAwIDEyLjAyNC0zNi4zMyAyMS43Ny04MS4xNDQgMjEuNzciLz4KICA8L2c+CiAgPGc+CiAgIDxwYXRoIGlkPSJwYXRoNDIzMSIgZD0ibTQ1MS4yOCA5MzMuOTRjLTQ0LjYzOCAwLTgwLjgyNC05LjcwOS04MC44MjQtMjEuNjg1IDAtNi4xMzc4IDkuNTAyNC0xMS42NzkgMjQuNzc1LTE1LjYyNCAwLjAzODYgMC4wMzA4IDAuMDcyMiAwLjA1OTUgMC4xMTEzOCAwLjA5MDctMTUuMTQ2IDMuOTI3OC0yNC41NjYgOS40MzUxLTI0LjU2NiAxNS41MzMgMCAxMS45MjkgMzYuMDQyIDIxLjU5OSA4MC41MDMgMjEuNTk5IDQ0LjQ2IDAgODAuNTAzLTkuNjY5OSA4MC41MDMtMjEuNTk5IDAtNi4wOTc2LTkuNDItMTEuNjA1LTI0LjU2Ni0xNS41MzMgMC4wMzg3LTAuMDMxMiAwLjA3MjUtMC4wNiAwLjExMTI1LTAuMDkwNyAxNS4yNzIgMy45NDQ4IDI0Ljc3NSA5LjQ4NTggMjQuNzc1IDE1LjYyNCAwIDExLjk3Ni0zNi4xODUgMjEuNjg1LTgwLjgyMyAyMS42ODUiIGZpbGw9IiNmZWZmZmYiLz4KICAgPHBhdGggaWQ9InBhdGg0MjMzIiBkPSJtNDUxLjI4IDkzMy44NWMtNDQuNDYgMC04MC41MDMtOS42Njk5LTgwLjUwMy0yMS41OTkgMC02LjA5NzYgOS40Mi0xMS42MDUgMjQuNTY2LTE1LjUzMyAwLjAzNjEgMC4wMzAzIDAuMDc0NiAwLjA2MTUgMC4xMTEyNSAwLjA5MTQtMTUuMDIyIDMuOTA4Ni0yNC4zNTYgOS4zODI4LTI0LjM1NiAxNS40NDEgMCAxMS44ODEgMzUuODk4IDIxLjUxMyA4MC4xODIgMjEuNTEzIDQ0LjI4MyAwIDgwLjE4Mi05LjYzMTQgODAuMTgyLTIxLjUxMyAwLTYuMDU4Ni05LjMzMzgtMTEuNTMzLTI0LjM1Ni0xNS40NDEgMC4wMzYyLTAuMDI5OSAwLjA3NS0wLjA2MTEgMC4xMTEyNS0wLjA5MTQgMTUuMTQ2IDMuOTI3OCAyNC41NjYgOS40MzUxIDI0LjU2NiAxNS41MzMgMCAxMS45MjktMzYuMDQyIDIxLjU5OS04MC41MDMgMjEuNTk5IiBmaWxsPSIjZmVmZmZmIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDIzNSIgZD0ibTQ1MS4yOCA5MzMuNzdjLTQ0LjI4NCAwLTgwLjE4Mi05LjYzMTQtODAuMTgyLTIxLjUxMyAwLTYuMDU4NiA5LjMzNDUtMTEuNTMzIDI0LjM1Ni0xNS40NDEgMC4wMzY2IDAuMDI4NyAwLjA3NTIgMC4wNjE1IDAuMTExODcgMC4wOTAyLTE0Ljg5NiAzLjg5MjEtMjQuMTQ3IDkuMzMyNi0yNC4xNDcgMTUuMzUxIDAgMTEuODM0IDM1Ljc1NSAyMS40MjYgNzkuODYxIDIxLjQyNnM3OS44Ni05LjU5MjMgNzkuODYtMjEuNDI2YzAtNi4wMTg1LTkuMjUxMi0xMS40NTktMjQuMTQ2LTE1LjM1MSAwLjAzNjItMC4wMjg3IDAuMDc1LTAuMDYxNSAwLjExMTI1LTAuMDkwMiAxNS4wMjIgMy45MDg2IDI0LjM1NiA5LjM4MjggMjQuMzU2IDE1LjQ0MSAwIDExLjg4MS0zNS44OTkgMjEuNTEzLTgwLjE4MiAyMS41MTMiIGZpbGw9IiNmZWZlZmYiLz4KICAgPHBhdGggaWQ9InBhdGg0MjM3IiBkPSJtNDUxLjI4IDkzMy42OGMtNDQuMTA2IDAtNzkuODYxLTkuNTkyMy03OS44NjEtMjEuNDI2IDAtNi4wMTg1IDkuMjUxNS0xMS40NTkgMjQuMTQ3LTE1LjM1MSAwLjAzODUgMC4wMzEyIDAuMDczMiAwLjA2MDEgMC4xMTE3NSAwLjA5MDktMTQuNzcgMy44NzU1LTIzLjkzOCA5LjI4MDItMjMuOTM4IDE1LjI2IDAgMTEuNzg2IDM1LjYxMSAyMS4zNCA3OS41NDEgMjEuMzQgNDMuOTI5IDAgNzkuNTQtOS41NTM3IDc5LjU0LTIxLjM0IDAtNS45OC05LjE2ODgtMTEuMzg1LTIzLjkzOS0xNS4yNiAwLjAzODctMC4wMzA4IDAuMDczNy0wLjA1OTYgMC4xMTI1LTAuMDkwOSAxNC44OTUgMy44OTIxIDI0LjE0NiA5LjMzMjYgMjQuMTQ2IDE1LjM1MSAwIDExLjgzNC0zNS43NTUgMjEuNDI2LTc5Ljg2IDIxLjQyNiIgZmlsbD0iI2ZlZmVmZSIvPgogICA8cGF0aCBpZD0icGF0aDQyMzkiIGQ9Im00NTEuMjggOTMzLjU5Yy00My45MjkgMC03OS41NDEtOS41NTM3LTc5LjU0MS0yMS4zNCAwLTUuOTggOS4xNjc5LTExLjM4NSAyMy45MzgtMTUuMjYgMC4wMzY2IDAuMDMwMyAwLjA3NjMgMC4wNjE1IDAuMTEyODggMC4wOTEzLTE0LjY0NiAzLjg1NjUtMjMuNzMgOS4yMjc2LTIzLjczIDE1LjE2OSAwIDExLjczOSAzNS40NjggMjEuMjU1IDc5LjIyIDIxLjI1NXM3OS4yMTktOS41MTYxIDc5LjIxOS0yMS4yNTVjMC01Ljk0MTQtOS4wODUtMTEuMzEyLTIzLjczLTE1LjE2OSAwLjAzNjMtMC4wMjk4IDAuMDc2My0wLjA2MSAwLjExMjUtMC4wOTEzIDE0Ljc3IDMuODc1NSAyMy45MzkgOS4yODAyIDIzLjkzOSAxNS4yNiAwIDExLjc4Ni0zNS42MTEgMjEuMzQtNzkuNTQgMjEuMzQiIGZpbGw9IiNmZWZlZmUiLz4KICAgPHBhdGggaWQ9InBhdGg0MjQxIiBkPSJtNDUxLjI4IDkzMy41MWMtNDMuNzUyIDAtNzkuMjItOS41MTYxLTc5LjIyLTIxLjI1NSAwLTUuOTQxNCA5LjA4NDUtMTEuMzEyIDIzLjczLTE1LjE2OSAwLjAzODUgMC4wMzEyIDAuMDczOCAwLjA1OTEgMC4xMTI3NSAwLjA5MDQtMTQuNTIyIDMuODM5OS0yMy41MjIgOS4xNzcyLTIzLjUyMiAxNS4wNzkgMCAxMS42OTEgMzUuMzI0IDIxLjE2OSA3OC44OTkgMjEuMTY5IDQzLjU3NCAwIDc4Ljg5OS05LjQ3NzYgNzguODk5LTIxLjE2OSAwLTUuOTAxNC05LjAwMTItMTEuMjM5LTIzLjUyMi0xNS4wNzkgMC4wMzg3LTAuMDMxMiAwLjA3MzctMC4wNTkxIDAuMTEyNS0wLjA5MDQgMTQuNjQ1IDMuODU2NSAyMy43MyA5LjIyNzYgMjMuNzMgMTUuMTY5IDAgMTEuNzM5LTM1LjQ2OCAyMS4yNTUtNzkuMjE5IDIxLjI1NSIgZmlsbD0iI2ZkZmVmZSIvPgogICA8cGF0aCBpZD0icGF0aDQyNDMiIGQ9Im00NTEuMjggOTMzLjQyYy00My41NzUgMC03OC44OTktOS40Nzc2LTc4Ljg5OS0yMS4xNjkgMC01LjkwMTQgOS4wMDEtMTEuMjM5IDIzLjUyMi0xNS4wNzkgMC4wMzY2IDAuMDI5NyAwLjA3NjEgMC4wNjI1IDAuMTEzMjUgMC4wOTE0LTE0LjM5NSAzLjgyMDgtMjMuMzE1IDkuMTI1LTIzLjMxNSAxNC45ODcgMCAxMS42NDQgMzUuMTgxIDIxLjA4MyA3OC41NzkgMjEuMDgzczc4LjU3OC05LjQzOSA3OC41NzgtMjEuMDgzYzAtNS44NjIyLTguOTItMTEuMTY2LTIzLjMxNS0xNC45ODcgMC4wMzc1LTAuMDI4OSAwLjA3NjMtMC4wNjE2IDAuMTEzNzUtMC4wOTE0IDE0LjUyMSAzLjgzOTkgMjMuNTIyIDkuMTc3MiAyMy41MjIgMTUuMDc5IDAgMTEuNjkxLTM1LjMyNSAyMS4xNjktNzguODk5IDIxLjE2OSIgZmlsbD0iI2ZkZmVmZSIvPgogICA8cGF0aCBpZD0icGF0aDQyNDUiIGQ9Im00NTEuMjggOTMzLjMzYy00My4zOTggMC03OC41NzktOS40MzktNzguNTc5LTIxLjA4MyAwLTUuODYyMiA4LjkyMDQtMTEuMTY2IDIzLjMxNS0xNC45ODcgMC4wMzg2IDAuMDMxMiAwLjA3NDMgMC4wNiAwLjExMzM3IDAuMDkwNy0xNC4yNzIgMy44MDQyLTIzLjEwNyA5LjA3MjgtMjMuMTA3IDE0Ljg5NiAwIDExLjU5NiAzNS4wMzcgMjAuOTk2IDc4LjI1NyAyMC45OTZzNzguMjU3LTkuMzk5OSA3OC4yNTctMjAuOTk2YzAtNS44MjM4LTguODM1LTExLjA5Mi0yMy4xMDgtMTQuODk2IDAuMDM4OC0wLjAzMDggMC4wNzUtMC4wNTk1IDAuMTEzNzUtMC4wOTA3IDE0LjM5NSAzLjgyMDggMjMuMzE1IDkuMTI1IDIzLjMxNSAxNC45ODcgMCAxMS42NDQtMzUuMTggMjEuMDgzLTc4LjU3OCAyMS4wODMiIGZpbGw9IiNmZGZkZmUiLz4KICAgPHBhdGggaWQ9InBhdGg0MjQ3IiBkPSJtNDUxLjI4IDkzMy4yNWMtNDMuMjIxIDAtNzguMjU3LTkuMzk5OS03OC4yNTctMjAuOTk2IDAtNS44MjM4IDguODM2LTExLjA5MiAyMy4xMDctMTQuODk2IDAuMDM3IDAuMDI4OSAwLjA3NzYgMC4wNjE1IDAuMTE0MjUgMC4wOTE0LTE0LjE0NiAzLjc4NTEtMjIuOTAxIDkuMDIxNC0yMi45MDEgMTQuODA1IDAgMTEuNTQ5IDM0Ljg5MyAyMC45MSA3Ny45MzcgMjAuOTEgNDMuMDQzIDAgNzcuOTM3LTkuMzYxMyA3Ny45MzctMjAuOTEgMC01Ljc4MzgtOC43NTUtMTEuMDItMjIuOTAxLTE0LjgwNSAwLjAzNjItMC4wMjk5IDAuMDc3NS0wLjA2MjUgMC4xMTM3NS0wLjA5MTQgMTQuMjcyIDMuODA0MiAyMy4xMDggOS4wNzI4IDIzLjEwOCAxNC44OTYgMCAxMS41OTYtMzUuMDM2IDIwLjk5Ni03OC4yNTcgMjAuOTk2IiBmaWxsPSIjZmRmZGZkIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDI0OSIgZD0ibTQ1MS4yOCA5MzMuMTZjLTQzLjA0NCAwLTc3LjkzNy05LjM2MTMtNzcuOTM3LTIwLjkxIDAtNS43ODM4IDguNzU0OS0xMS4wMiAyMi45MDEtMTQuODA1IDAuMDM5IDAuMDMxMiAwLjA3NTEgMC4wNiAwLjExNDc1IDAuMDkwMi0xNC4wMjUgMy43Njg2LTIyLjY5NSA4Ljk2OTctMjIuNjk1IDE0LjcxNSAwIDExLjUwMSAzNC43NSAyMC44MjQgNzcuNjE2IDIwLjgyNHM3Ny42MTYtOS4zMjIzIDc3LjYxNi0yMC44MjRjMC01Ljc0NTEtOC42Ny0xMC45NDYtMjIuNjk1LTE0LjcxNSAwLjA0LTAuMDMwMyAwLjA3NjMtMC4wNTkgMC4xMTUtMC4wOTAyIDE0LjE0NiAzLjc4NTEgMjIuOTAxIDkuMDIxNCAyMi45MDEgMTQuODA1IDAgMTEuNTQ5LTM0Ljg5NCAyMC45MS03Ny45MzcgMjAuOTEiIGZpbGw9IiNmZGZkZmQiLz4KICAgPHBhdGggaWQ9InBhdGg0MjUxIiBkPSJtNDUxLjI4IDkzMy4wOGMtNDIuODY2IDAtNzcuNjE2LTkuMzIyMy03Ny42MTYtMjAuODI0IDAtNS43NDUxIDguNjcwNC0xMC45NDYgMjIuNjk1LTE0LjcxNSAwLjAzNjYgMC4wMjk5IDAuMDc3NiAwLjA2MjUgMC4xMTQ3NSAwLjA5MTQtMTMuOSAzLjc1LTIyLjQ4OSA4LjkxNzUtMjIuNDg5IDE0LjYyNCAwIDExLjQ1NCAzNC42MDYgMjAuNzM3IDc3LjI5NSAyMC43MzdzNzcuMjk2LTkuMjgzNyA3Ny4yOTYtMjAuNzM3YzAtNS43MDYtOC41ODg4LTEwLjg3NC0yMi40OS0xNC42MjQgMC4wMzc1LTAuMDI4OSAwLjA3ODgtMC4wNjE1IDAuMTE1LTAuMDkxNCAxNC4wMjUgMy43Njg2IDIyLjY5NSA4Ljk2OTcgMjIuNjk1IDE0LjcxNSAwIDExLjUwMS0zNC43NSAyMC44MjQtNzcuNjE2IDIwLjgyNCIgZmlsbD0iI2ZjZmRmZCIvPgogICA8cGF0aCBpZD0icGF0aDQyNTMiIGQ9Im00NTEuMjggOTMyLjk5Yy00Mi42ODkgMC03Ny4yOTUtOS4yODM3LTc3LjI5NS0yMC43MzcgMC01LjcwNiA4LjU4ODgtMTAuODc0IDIyLjQ4OS0xNC42MjQgMC4wMzkgMC4wMzEyIDAuMDc2MSAwLjA1OTUgMC4xMTUxMyAwLjA5MDctMTMuNzc3IDMuNzMxNS0yMi4yODMgOC44NjUyLTIyLjI4MyAxNC41MzMgMCAxMS40MDYgMzQuNDYyIDIwLjY1MiA3Ni45NzQgMjAuNjUyczc2Ljk3NC05LjI0NjEgNzYuOTc0LTIwLjY1MmMwLTUuNjY3NS04LjUwNjItMTAuODAxLTIyLjI4NC0xNC41MzMgMC4wMzg3LTAuMDMxMiAwLjA3NjItMC4wNTk1IDAuMTE1LTAuMDkwNyAxMy45MDEgMy43NSAyMi40OSA4LjkxNzUgMjIuNDkgMTQuNjI0IDAgMTEuNDU0LTM0LjYwNiAyMC43MzctNzcuMjk2IDIwLjczNyIgZmlsbD0iI2ZjZmRmZCIvPgogICA8cGF0aCBpZD0icGF0aDQyNTUiIGQ9Im00NTEuMjggOTMyLjljLTQyLjUxMiAwLTc2Ljk3NC05LjI0NjEtNzYuOTc0LTIwLjY1MiAwLTUuNjY3NSA4LjUwNjQtMTAuODAxIDIyLjI4My0xNC41MzMgMC4wMzkxIDAuMDMxMiAwLjA3NjggMC4wNjAxIDAuMTE1NzUgMC4wOTE0LTEzLjY1MSAzLjcxMzktMjIuMDc5IDguODEzOS0yMi4wNzkgMTQuNDQxIDAgMTEuMzU5IDM0LjMxOSAyMC41NjYgNzYuNjU0IDIwLjU2NiA0Mi4zMzQgMCA3Ni42NTMtOS4yMDc1IDc2LjY1My0yMC41NjYgMC01LjYyNzUtOC40MjYyLTEwLjcyOC0yMi4wNzgtMTQuNDQxIDAuMDM4Ny0wLjAzMTIgMC4wNzYzLTAuMDYwMSAwLjExNS0wLjA5MTQgMTMuNzc4IDMuNzMxNSAyMi4yODQgOC44NjUyIDIyLjI4NCAxNC41MzMgMCAxMS40MDYtMzQuNDYyIDIwLjY1Mi03Ni45NzQgMjAuNjUyIiBmaWxsPSIjZmNmY2ZkIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDI1NyIgZD0ibTQ1MS4yOCA5MzIuODJjLTQyLjMzNSAwLTc2LjY1NC05LjIwNzUtNzYuNjU0LTIwLjU2NiAwLTUuNjI3NSA4LjQyNzItMTAuNzI4IDIyLjA3OS0xNC40NDEgMC4wMzcxIDAuMDI4NyAwLjA3OTEgMC4wNjI1IDAuMTE2MjUgMC4wOTEyLTEzLjUyOSAzLjY5NTQtMjEuODc0IDguNzYxMi0yMS44NzQgMTQuMzUgMCAxMS4zMTEgMzQuMTc1IDIwLjQ4IDc2LjMzMyAyMC40OCA0Mi4xNTcgMCA3Ni4zMzMtOS4xNjg5IDc2LjMzMy0yMC40OCAwLTUuNTg4OS04LjM0NS0xMC42NTUtMjEuODc0LTE0LjM1IDAuMDM2My0wLjAyODcgMC4wNzg4LTAuMDYyNSAwLjExNjI1LTAuMDkxMiAxMy42NTEgMy43MTM5IDIyLjA3OCA4LjgxMzkgMjIuMDc4IDE0LjQ0MSAwIDExLjM1OS0zNC4zMTkgMjAuNTY2LTc2LjY1MyAyMC41NjYiIGZpbGw9IiNmY2ZjZmMiLz4KICAgPHBhdGggaWQ9InBhdGg0MjU5IiBkPSJtNDUxLjI4IDkzMi43M2MtNDIuMTU4IDAtNzYuMzMzLTkuMTY4OS03Ni4zMzMtMjAuNDggMC01LjU4ODkgOC4zNDQ4LTEwLjY1NSAyMS44NzQtMTQuMzUgMC4wMzkgMC4wMzAzIDAuMDc3MSAwLjA2MDEgMC4xMTYyNSAwLjA5MTQtMTMuNDA1IDMuNjc2Mi0yMS42NyA4LjcwOS0yMS42NyAxNC4yNTkgMCAxMS4yNjQgMzQuMDMyIDIwLjM5NCA3Ni4wMTIgMjAuMzk0czc2LjAxMi05LjEyOTkgNzYuMDEyLTIwLjM5NGMwLTUuNTQ5OC04LjI2NS0xMC41ODItMjEuNjY5LTE0LjI1OSAwLjAzODctMC4wMzEyIDAuMDc2Mi0wLjA2MTEgMC4xMTYyNS0wLjA5MTQgMTMuNTI5IDMuNjk1NCAyMS44NzQgOC43NjEyIDIxLjg3NCAxNC4zNSAwIDExLjMxMS0zNC4xNzYgMjAuNDgtNzYuMzMzIDIwLjQ4IiBmaWxsPSIjZmJmY2ZjIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDI2MSIgZD0ibTQ1MS4yOCA5MzIuNjVjLTQxLjk4IDAtNzYuMDEyLTkuMTI5OS03Ni4wMTItMjAuMzk0IDAtNS41NDk4IDguMjY0Ni0xMC41ODIgMjEuNjctMTQuMjU5IDAuMDM5NSAwLjAyOTggMC4wNzc2IDAuMDYgMC4xMTcxMiAwLjA4OTgtMTMuMjg0IDMuNjYwMi0yMS40NjYgOC42NTc4LTIxLjQ2NiAxNC4xNjkgMCAxMS4yMTYgMzMuODg4IDIwLjMwOCA3NS42OTEgMjAuMzA4czc1LjY5Mi05LjA5MTQgNzUuNjkyLTIwLjMwOGMwLTUuNTExMi04LjE4MjUtMTAuNTA5LTIxLjQ2Ni0xNC4xNjkgMC4wMzg3LTAuMDI5OCAwLjA3NzUtMC4wNiAwLjExNzUtMC4wODk4IDEzLjQwNCAzLjY3NjIgMjEuNjY5IDguNzA5IDIxLjY2OSAxNC4yNTkgMCAxMS4yNjQtMzQuMDMxIDIwLjM5NC03Ni4wMTIgMjAuMzk0IiBmaWxsPSIjZmJmY2ZjIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDI2MyIgZD0ibTQ1MS4yOCA5MzIuNTZjLTQxLjgwMyAwLTc1LjY5MS05LjA5MTQtNzUuNjkxLTIwLjMwOCAwLTUuNTExMiA4LjE4MjEtMTAuNTA5IDIxLjQ2Ni0xNC4xNjkgMC4wMzk2IDAuMDMxMiAwLjA3ODEgMC4wNjE2IDAuMTE3NzUgMC4wOTE0LTEzLjE2MSAzLjY0MTEtMjEuMjYzIDguNjA1LTIxLjI2MyAxNC4wNzggMCAxMS4xNjkgMzMuNzQ1IDIwLjIyMSA3NS4zNzEgMjAuMjIxczc1LjM3LTkuMDUyMiA3NS4zNy0yMC4yMjFjMC01LjQ3MjYtOC4xMDEyLTEwLjQzNi0yMS4yNjItMTQuMDc4IDAuMDM4Ny0wLjAyOTggMC4wNzc1LTAuMDYwMSAwLjExNzUtMC4wOTE0IDEzLjI4NCAzLjY2MDIgMjEuNDY2IDguNjU3OCAyMS40NjYgMTQuMTY5IDAgMTEuMjE2LTMzLjg4OSAyMC4zMDgtNzUuNjkyIDIwLjMwOCIgZmlsbD0iI2ZiZmJmYiIvPgogICA8cGF0aCBpZD0icGF0aDQyNjUiIGQ9Im00NTEuMjggOTMyLjQ3Yy00MS42MjYgMC03NS4zNzEtOS4wNTIyLTc1LjM3MS0yMC4yMjEgMC01LjQ3MjYgOC4xMDE1LTEwLjQzNiAyMS4yNjMtMTQuMDc4IDAuMDM5IDAuMDMxMiAwLjA3ODEgMC4wNjE1IDAuMTE3NjMgMC4wOTEzLTEzLjAzOSAzLjYyMjYtMjEuMDYgOC41NTI4LTIxLjA2IDEzLjk4NiAwIDExLjEyIDMzLjYwMSAyMC4xMzYgNzUuMDUgMjAuMTM2czc1LjA0OS05LjAxNjEgNzUuMDQ5LTIwLjEzNmMwLTUuNDMzNi04LjAyLTEwLjM2NC0yMS4wNTktMTMuOTg2IDAuMDM4OC0wLjAyOTcgMC4wNzc1LTAuMDYgMC4xMTc1LTAuMDkxMyAxMy4xNjEgMy42NDExIDIxLjI2MiA4LjYwNSAyMS4yNjIgMTQuMDc4IDAgMTEuMTY5LTMzLjc0NSAyMC4yMjEtNzUuMzcgMjAuMjIxIiBmaWxsPSIjZmJmYmZiIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDI2NyIgZD0ibTQ1MS4yOCA5MzIuMzljLTQxLjQ0OSAwLTc1LjA1LTkuMDE2MS03NS4wNS0yMC4xMzYgMC01LjQzMzYgOC4wMjEtMTAuMzY0IDIxLjA2LTEzLjk4NiAwLjAzOSAwLjAzMDQgMC4wNzg2IDAuMDYxNiAwLjExODEyIDAuMDkxNC0xMi45MTcgMy42MDQtMjAuODU3IDguNS0yMC44NTcgMTMuODk1IDAgMTEuMDcyIDMzLjQ1NyAyMC4wNSA3NC43MjkgMjAuMDVzNzQuNzI5LTguOTc3NiA3NC43MjktMjAuMDVjMC01LjM5NS03Ljk0LTEwLjI5MS0yMC44NTgtMTMuODk1IDAuMDQtMC4wMjk4IDAuMDc4Ny0wLjA2MSAwLjExODc1LTAuMDkxNCAxMy4wMzkgMy42MjI2IDIxLjA1OSA4LjU1MjggMjEuMDU5IDEzLjk4NiAwIDExLjEyLTMzLjYgMjAuMTM2LTc1LjA0OSAyMC4xMzYiIGZpbGw9IiNmYWZiZmIiLz4KICAgPHBhdGggaWQ9InBhdGg0MjY5IiBkPSJtNDUxLjI4IDkzMi4zYy00MS4yNzIgMC03NC43MjktOC45Nzc2LTc0LjcyOS0yMC4wNSAwLTUuMzk1IDcuOTM5OS0xMC4yOTEgMjAuODU3LTEzLjg5NSAwLjAzOTYgMC4wMjk3IDAuMDc5MSAwLjA2MTUgMC4xMTg2MyAwLjA5MTItMTIuNzkzIDMuNTg1LTIwLjY1NSA4LjQ0ODgtMjAuNjU1IDEzLjgwNCAwIDExLjAyNSAzMy4zMTQgMTkuOTY0IDc0LjQwOCAxOS45NjRzNzQuNDA4LTguOTM5IDc0LjQwOC0xOS45NjRjMC01LjM1NjUtNy44NTg4LTEwLjIxOS0yMC42NTUtMTMuODA0IDAuMDQtMC4wMjk4IDAuMDc4Ny0wLjA2MTUgMC4xMTg3NS0wLjA5MTIgMTIuOTE4IDMuNjA0IDIwLjg1OCA4LjUgMjAuODU4IDEzLjg5NSAwIDExLjA3Mi0zMy40NTggMjAuMDUtNzQuNzI5IDIwLjA1IiBmaWxsPSIjZmFmYmZiIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDI3MSIgZD0ibTQ1MS4yOCA5MzIuMjJjLTQxLjA5NSAwLTc0LjQwOC04LjkzOS03NC40MDgtMTkuOTY0IDAtNS4zNTUgNy44NjE5LTEwLjIxOSAyMC42NTUtMTMuODA0IDAuMDM5NiAwLjAyOTkgMC4wNzkxIDAuMDYxMSAwLjExOTI1IDAuMDkxNC0xMi42NzMgMy41NjU5LTIwLjQ1MyA4LjM5Ni0yMC40NTMgMTMuNzEyIDAgMTAuOTc4IDMzLjE3IDE5Ljg3NyA3NC4wODcgMTkuODc3czc0LjA4OC04Ljg5OTkgNzQuMDg4LTE5Ljg3N2MwLTUuMzE2NC03Ljc4MTItMTAuMTQ2LTIwLjQ1NC0xMy43MTIgMC4wNC0wLjAzMDIgMC4wOC0wLjA2MTUgMC4xMTg3NS0wLjA5MTQgMTIuNzk2IDMuNTg1IDIwLjY1NSA4LjQ0NzIgMjAuNjU1IDEzLjgwNCAwIDExLjAyNS0zMy4zMTQgMTkuOTY0LTc0LjQwOCAxOS45NjQiIGZpbGw9IiNmYWZhZmEiLz4KICAgPHBhdGggaWQ9InBhdGg0MjczIiBkPSJtNDUxLjI4IDkzMi4xM2MtNDAuOTE4IDAtNzQuMDg3LTguODk5OS03NC4wODctMTkuODc3IDAtNS4zMTY0IDcuNzgwMi0xMC4xNDYgMjAuNDUzLTEzLjcxMiAwLjAzOTUgMC4wMjk3IDAuMDc5NSAwLjA2MSAwLjExOTYyIDAuMDkxMy0xMi41NSAzLjU0NzQtMjAuMjUyIDguMzQzOC0yMC4yNTIgMTMuNjIxIDAgMTAuOTMgMzMuMDI2IDE5Ljc5MSA3My43NjcgMTkuNzkxIDQwLjc0IDAgNzMuNzY3LTguODYwOSA3My43NjctMTkuNzkxIDAtNS4yNzc0LTcuNzAyNS0xMC4wNzQtMjAuMjUyLTEzLjYyMSAwLjA0LTAuMDMwMyAwLjA4LTAuMDYxNSAwLjEyLTAuMDkxMyAxMi42NzIgMy41NjU5IDIwLjQ1NCA4LjM5NiAyMC40NTQgMTMuNzEyIDAgMTAuOTc4LTMzLjE3MSAxOS44NzctNzQuMDg4IDE5Ljg3NyIgZmlsbD0iI2ZhZmFmYSIvPgogICA8cGF0aCBpZD0icGF0aDQyNzUiIGQ9Im00NTEuMjggOTMyLjA0Yy00MC43NCAwLTczLjc2Ny04Ljg2MDktNzMuNzY3LTE5Ljc5MSAwLTUuMjc3NCA3LjcwMTYtMTAuMDc0IDIwLjI1Mi0xMy42MjEgMC4wMzk1IDAuMDI5OSAwLjA4IDAuMDYxMSAwLjExOTYzIDAuMDkxNC0xMi40MzEgMy41MjgyLTIwLjA1MSA4LjI5MS0yMC4wNTEgMTMuNTMgMCAxMC44ODIgMzIuODgzIDE5LjcwNSA3My40NDYgMTkuNzA1czczLjQ0Ni04LjgyMjcgNzMuNDQ2LTE5LjcwNWMwLTUuMjM4OC03LjYyLTEwLjAwMi0yMC4wNTEtMTMuNTMgMC4wNC0wLjAzMDMgMC4wOC0wLjA2MTUgMC4xMi0wLjA5MTQgMTIuNTUgMy41NDc0IDIwLjI1MiA4LjM0MzggMjAuMjUyIDEzLjYyMSAwIDEwLjkzLTMzLjAyNiAxOS43OTEtNzMuNzY3IDE5Ljc5MSIgZmlsbD0iI2Y5ZmFmYSIvPgogICA8cGF0aCBpZD0icGF0aDQyNzciIGQ9Im00NTEuMjggOTMxLjk2Yy00MC41NjMgMC03My40NDYtOC44MjI3LTczLjQ0Ni0xOS43MDUgMC01LjIzODggNy42MjAxLTEwLjAwMiAyMC4wNTEtMTMuNTMgMC4wNCAwLjAyOTggMC4wODA1IDAuMDYxIDAuMTIwNSAwLjA5MDctMTIuMzEgMy41MTAyLTE5Ljg1MSA4LjIzODgtMTkuODUxIDEzLjQzOSAwIDEwLjgzNSAzMi43MzkgMTkuNjE5IDczLjEyNiAxOS42MTkgNDAuMzg2IDAgNzMuMTI2LTguNzgzNiA3My4xMjYtMTkuNjE5IDAtNS4yMDAyLTcuNTQxMi05LjkyODgtMTkuODUxLTEzLjQzOSAwLjA0LTAuMDI5NyAwLjA4MTMtMC4wNjEgMC4xMi0wLjA5MDcgMTIuNDMxIDMuNTI4MiAyMC4wNTEgOC4yOTEgMjAuMDUxIDEzLjUzIDAgMTAuODgyLTMyLjg4MiAxOS43MDUtNzMuNDQ2IDE5LjcwNSIgZmlsbD0iI2Y5ZjlmOSIvPgogICA8cGF0aCBpZD0icGF0aDQyNzkiIGQ9Im00NTEuMjggOTMxLjg3Yy00MC4zODYgMC03My4xMjYtOC43ODM2LTczLjEyNi0xOS42MTkgMC01LjIwMDIgNy41NDA1LTkuOTI4OCAxOS44NTEtMTMuNDM5IDAuMDQwMSAwLjAzMDMgMC4wODExIDAuMDYxNSAwLjEyMDYyIDAuMDkxNC0xMi4xODcgMy40OTExLTE5LjY1MSA4LjE4NjUtMTkuNjUxIDEzLjM0OCAwIDEwLjc4OCAzMi41OTYgMTkuNTM0IDcyLjgwNSAxOS41MzQgNDAuMjA4IDAgNzIuODA0LTguNzQ2MSA3Mi44MDQtMTkuNTM0IDAtNS4xNjExLTcuNDYzOC05Ljg1NjUtMTkuNjUxLTEzLjM0OCAwLjA0LTAuMDI5OSAwLjA4MTItMC4wNjExIDAuMTIxMjUtMC4wOTE0IDEyLjMxIDMuNTEwMiAxOS44NTEgOC4yMzg4IDE5Ljg1MSAxMy40MzkgMCAxMC44MzUtMzIuNzQgMTkuNjE5LTczLjEyNiAxOS42MTkiIGZpbGw9IiNmOWY5ZjkiLz4KICAgPHBhdGggaWQ9InBhdGg0MjgxIiBkPSJtNDUxLjI4IDkzMS43OWMtNDAuMjA5IDAtNzIuODA1LTguNzQ2MS03Mi44MDUtMTkuNTM0IDAtNS4xNjExIDcuNDYzOS05Ljg1NjUgMTkuNjUxLTEzLjM0OCAwLjA0MiAwLjAzMTIgMC4wNzk2IDAuMDU5IDAuMTIxNjMgMC4wOTEyLTEyLjA2OCAzLjQ3MjYtMTkuNDUyIDguMTMzOS0xOS40NTIgMTMuMjU2IDAgMTAuNzQgMzIuNDUyIDE5LjQ0OCA3Mi40ODQgMTkuNDQ4czcyLjQ4NC04LjcwNzUgNzIuNDg0LTE5LjQ0OGMwLTUuMTIyNS03LjM4NS05Ljc4MzgtMTkuNDUyLTEzLjI1NiAwLjA0MjUtMC4wMzIyIDAuMDgtMC4wNiAwLjEyMTI1LTAuMDkxMiAxMi4xODggMy40OTExIDE5LjY1MSA4LjE4NjUgMTkuNjUxIDEzLjM0OCAwIDEwLjc4OC0zMi41OTYgMTkuNTM0LTcyLjgwNCAxOS41MzQiIGZpbGw9IiNmOGY5ZjkiLz4KICAgPHBhdGggaWQ9InBhdGg0MjgzIiBkPSJtNDUxLjI4IDkzMS43Yy00MC4wMzIgMC03Mi40ODQtOC43MDc1LTcyLjQ4NC0xOS40NDggMC01LjEyMjUgNy4zODM3LTkuNzgzOCAxOS40NTItMTMuMjU2IDAuMDQgMC4wMzAzIDAuMDgxNSAwLjA2MTUgMC4xMjE2MiAwLjA5MTQtMTEuOTQ5IDMuNDUzNi0xOS4yNTIgOC4wOC0xOS4yNTIgMTMuMTY1IDAgMTAuNjkyIDMyLjMwOSAxOS4zNjEgNzIuMTYzIDE5LjM2MXM3Mi4xNjMtOC42NjkgNzIuMTYzLTE5LjM2MWMwLTUuMDg1LTcuMzAzOC05LjcxMTQtMTkuMjUyLTEzLjE2NSAwLjA0LTAuMDI5OSAwLjA4MTItMC4wNjExIDAuMTIxMjUtMC4wOTE0IDEyLjA2OCAzLjQ3MjYgMTkuNDUyIDguMTMzOSAxOS40NTIgMTMuMjU2IDAgMTAuNzQtMzIuNDUyIDE5LjQ0OC03Mi40ODQgMTkuNDQ4IiBmaWxsPSIjZjhmOGY4Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNDI4NSIgZD0ibTQ1MS4yOCA5MzEuNjFjLTM5Ljg1NCAwLTcyLjE2My04LjY2OS03Mi4xNjMtMTkuMzYxIDAtNS4wODUgNy4zMDM4LTkuNzExNCAxOS4yNTItMTMuMTY1IDAuMDQgMC4wMjg4IDAuMDgyNSAwLjA2MTUgMC4xMjI1IDAuMDkxMy0xMS44MjggMy40MzUxLTE5LjA1NCA4LjAyNzQtMTkuMDU0IDEzLjA3NCAwIDEwLjY0NSAzMi4xNjUgMTkuMjc1IDcxLjg0MiAxOS4yNzVzNzEuODQyLTguNjI5OSA3MS44NDItMTkuMjc1YzAtNS4wNDY0LTcuMjI2Mi05LjYzODYtMTkuMDU0LTEzLjA3NCAwLjA0LTAuMDI5OCAwLjA4MjUtMC4wNjI1IDAuMTIyNS0wLjA5MTMgMTEuOTQ5IDMuNDUzNiAxOS4yNTIgOC4wOCAxOS4yNTIgMTMuMTY1IDAgMTAuNjkyLTMyLjMwOSAxOS4zNjEtNzIuMTYzIDE5LjM2MSIgZmlsbD0iI2Y4ZjhmOCIvPgogICA8cGF0aCBpZD0icGF0aDQyODciIGQ9Im00NTEuMjggOTMxLjUzYy0zOS42NzcgMC03MS44NDItOC42Mjk5LTcxLjg0Mi0xOS4yNzUgMC01LjA0NjQgNy4yMjYtOS42Mzg2IDE5LjA1NC0xMy4wNzQgMC4wNDIgMC4wMzEyIDAuMDgwMSAwLjA1ODYgMC4xMjI2MyAwLjA5MTQtMTEuNzA4IDMuNDE2LTE4Ljg1NiA3Ljk3NS0xOC44NTYgMTIuOTgyIDAgMTAuNTk4IDMyLjAyMiAxOS4xODkgNzEuNTIyIDE5LjE4OXM3MS41MjItOC41OTEzIDcxLjUyMi0xOS4xODljMC01LjAwNzQtNy4xNDg4LTkuNTY2NC0xOC44NTYtMTIuOTgyIDAuMDQyNS0wLjAzMjcgMC4wOC0wLjA2MDEgMC4xMjI1LTAuMDkxNCAxMS44MjggMy40MzUxIDE5LjA1NCA4LjAyNzQgMTkuMDU0IDEzLjA3NCAwIDEwLjY0NS0zMi4xNjUgMTkuMjc1LTcxLjg0MiAxOS4yNzUiIGZpbGw9IiNmN2Y4ZjgiLz4KICAgPHBhdGggaWQ9InBhdGg0Mjg5IiBkPSJtNDUxLjI4IDkzMS40NGMtMzkuNSAwLTcxLjUyMi04LjU5MTMtNzEuNTIyLTE5LjE4OSAwLTUuMDA3NCA3LjE0ODQtOS41NjY0IDE4Ljg1Ni0xMi45ODIgMC4wNCAwLjAyODggMC4wODMgMC4wNjEgMC4xMjMgMC4wOTEzLTExLjU4OCAzLjM5NzUtMTguNjU4IDcuOTIyNC0xOC42NTggMTIuODkxIDAgMTAuNTUgMzEuODc4IDE5LjEwMyA3MS4yMDEgMTkuMTAzczcxLjItOC41NTI4IDcxLjItMTkuMTAzYzAtNC45Njg4LTcuMDctOS40OTM2LTE4LjY1OS0xMi44OTEgMC4wNC0wLjAzMDMgMC4wODM4LTAuMDYyNSAwLjEyMzc1LTAuMDkxMyAxMS43MDggMy40MTYgMTguODU2IDcuOTc1IDE4Ljg1NiAxMi45ODIgMCAxMC41OTgtMzIuMDIxIDE5LjE4OS03MS41MjIgMTkuMTg5IiBmaWxsPSIjZjdmN2Y3Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNDI5MSIgZD0ibTQ1MS4yOCA5MzEuMzVjLTM5LjMyMyAwLTcxLjIwMS04LjU1MjgtNzEuMjAxLTE5LjEwMyAwLTQuOTY4OCA3LjA2OTktOS40OTM2IDE4LjY1OC0xMi44OTEgMC4wNDI1IDAuMDMxMiAwLjA4MSAwLjA2MDEgMC4xMjM1IDAuMDkxNC0xMS40NyAzLjM3ODQtMTguNDYxIDcuODY5Ni0xOC40NjEgMTIuOCAwIDEwLjUwMiAzMS43MzQgMTkuMDE2IDcwLjg4IDE5LjAxNnM3MC44NzktOC41MTM2IDcwLjg3OS0xOS4wMTZjMC00LjkzMDEtNi45OTEyLTkuNDIxNC0xOC40NjEtMTIuOCAwLjA0MjUtMC4wMzEyIDAuMDgxMy0wLjA2MDEgMC4xMjM3NS0wLjA5MTQgMTEuNTg5IDMuMzk3NSAxOC42NTkgNy45MjI0IDE4LjY1OSAxMi44OTEgMCAxMC41NS0zMS44NzggMTkuMTAzLTcxLjIgMTkuMTAzIiBmaWxsPSIjZjZmN2Y3Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNDI5MyIgZD0ibTQ1MS4yOCA5MzEuMjdjLTM5LjE0NiAwLTcwLjg4LTguNTEzNi03MC44OC0xOS4wMTYgMC00LjkzMDEgNi45OTExLTkuNDIxNCAxOC40NjEtMTIuOCAwLjA0MDUgMC4wMjgyIDAuMDg0IDAuMDYxIDAuMTI0IDAuMDkwNy0xMS4zNDkgMy4zNTg5LTE4LjI2NCA3LjgxNzktMTguMjY0IDEyLjcwOSAwIDEwLjQ1NSAzMS41OSAxOC45MzEgNzAuNTU5IDE4LjkzMXM3MC41NTktOC40NzYgNzAuNTU5LTE4LjkzMWMwLTQuODkxMS02LjkxNjItOS4zNTAxLTE4LjI2NS0xMi43MDkgMC4wNC0wLjAyOTcgMC4wODM4LTAuMDYyNSAwLjEyMzc1LTAuMDkwNyAxMS40NyAzLjM3ODQgMTguNDYxIDcuODY5NiAxOC40NjEgMTIuOCAwIDEwLjUwMi0zMS43MzQgMTkuMDE2LTcwLjg3OSAxOS4wMTYiIGZpbGw9IiNmNmY2ZjciLz4KICAgPHBhdGggaWQ9InBhdGg0Mjk1IiBkPSJtNDUxLjI4IDkzMS4xOGMtMzguOTY5IDAtNzAuNTU5LTguNDc2LTcwLjU1OS0xOC45MzEgMC00Ljg5MTEgNi45MTUtOS4zNTAxIDE4LjI2NC0xMi43MDkgMC4wNDI1IDAuMDMxMiAwLjA4MjYgMC4wNjAxIDAuMTI1IDAuMDkxNC0xMS4yMzIgMy4zNDAyLTE4LjA2OCA3Ljc2NTEtMTguMDY4IDEyLjYxOCAwIDEwLjQwOCAzMS40NDcgMTguODQ1IDcwLjIzOCAxOC44NDUgMzguNzkyIDAgNzAuMjM4LTguNDM3NSA3MC4yMzgtMTguODQ1IDAtNC44NTI1LTYuODM2Mi05LjI3NzQtMTguMDY5LTEyLjYxOCAwLjA0MzgtMC4wMzEyIDAuMDgyNS0wLjA2MDEgMC4xMjUtMC4wOTE0IDExLjM0OSAzLjM1ODkgMTguMjY1IDcuODE3OSAxOC4yNjUgMTIuNzA5IDAgMTAuNDU1LTMxLjU5IDE4LjkzMS03MC41NTkgMTguOTMxIiBmaWxsPSIjZjZmNmY2Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNDI5NyIgZD0ibTQ1MS4yOCA5MzEuMWMtMzguNzkyIDAtNzAuMjM4LTguNDM3NS03MC4yMzgtMTguODQ1IDAtNC44NTI1IDYuODM2NC05LjI3NzQgMTguMDY4LTEyLjYxOCAwLjA0MDEgMC4wMjg3IDAuMDg0NSAwLjA2MTUgMC4xMjUgMC4wOTEyLTExLjExMyAzLjMyMTMtMTcuODcyIDcuNzExNC0xNy44NzIgMTIuNTI2IDAgMTAuMzYgMzEuMzAzIDE4Ljc1OSA2OS45MTggMTguNzU5IDM4LjYxNCAwIDY5LjkxOC04LjM5ODkgNjkuOTE4LTE4Ljc1OSAwLTQuODE1LTYuNzYtOS4yMDUxLTE3Ljg3NC0xMi41MjYgMC4wNDEyLTAuMDI5NyAwLjA4NS0wLjA2MjUgMC4xMjUtMC4wOTEyIDExLjIzMiAzLjM0MDIgMTguMDY5IDcuNzY1MSAxOC4wNjkgMTIuNjE4IDAgMTAuNDA4LTMxLjQ0NiAxOC44NDUtNzAuMjM4IDE4Ljg0NSIgZmlsbD0iI2Y1ZjZmNiIvPgogICA8cGF0aCBpZD0icGF0aDQyOTkiIGQ9Im00NTEuMjggOTMxLjAxYy0zOC42MTQgMC02OS45MTgtOC4zOTg5LTY5LjkxOC0xOC43NTkgMC00LjgxNSA2Ljc1OTItOS4yMDUxIDE3Ljg3Mi0xMi41MjYgMC4wNDI1IDAuMDMxMiAwLjA4MyAwLjA2MDEgMC4xMjU1IDAuMDkxNC0xMC45OTUgMy4zMDI2LTE3LjY3NyA3LjY1ODYtMTcuNjc3IDEyLjQzNSAwIDEwLjMxMiAzMS4xNTkgMTguNjcyIDY5LjU5NyAxOC42NzIgMzguNDM3IDAgNjkuNTk3LTguMzU5OSA2OS41OTctMTguNjcyIDAtNC43NzY0LTYuNjgyNS05LjEzMjQtMTcuNjc4LTEyLjQzNSAwLjA0MjUtMC4wMzEyIDAuMDgyNS0wLjA2MDEgMC4xMjUtMC4wOTE0IDExLjExNCAzLjMyMTMgMTcuODc0IDcuNzExNCAxNy44NzQgMTIuNTI2IDAgMTAuMzYtMzEuMzA0IDE4Ljc1OS02OS45MTggMTguNzU5IiBmaWxsPSIjZjVmNWY1Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNDMwMSIgZD0ibTQ1MS4yOCA5MzAuOTJjLTM4LjQzOCAwLTY5LjU5Ny04LjM1OTktNjkuNTk3LTE4LjY3MiAwLTQuNzc2NCA2LjY4MjYtOS4xMzI0IDE3LjY3Ny0xMi40MzUgMC4wNDI1IDAuMDMxMiAwLjA4MzUgMC4wNiAwLjEyNiAwLjA5MTMtMTAuODc3IDMuMjgyOC0xNy40ODIgNy42MDY1LTE3LjQ4MiAxMi4zNDQgMCAxMC4yNjUgMzEuMDE2IDE4LjU4NiA2OS4yNzYgMTguNTg2czY5LjI3Ni04LjMyMTMgNjkuMjc2LTE4LjU4NmMwLTQuNzM3Mi02LjYwNS05LjA2MS0xNy40ODItMTIuMzQ0IDAuMDQyNS0wLjAzMTIgMC4wODM4LTAuMDYgMC4xMjYyNS0wLjA5MTMgMTAuOTk1IDMuMzAyNiAxNy42NzggNy42NTg2IDE3LjY3OCAxMi40MzUgMCAxMC4zMTItMzEuMTYgMTguNjcyLTY5LjU5NyAxOC42NzIiIGZpbGw9IiNmNWY1ZjUiLz4KICAgPHBhdGggaWQ9InBhdGg0MzAzIiBkPSJtNDUxLjI4IDkzMC44NGMtMzguMjYgMC02OS4yNzYtOC4zMjEzLTY5LjI3Ni0xOC41ODYgMC00LjczNzIgNi42MDU1LTkuMDYxIDE3LjQ4Mi0xMi4zNDQgMC4wNDMgMC4wMzEyIDAuMDg0IDAuMDYxIDAuMTI2NSAwLjA5MTItMTAuNzU4IDMuMjYzOC0xNy4yODkgNy41NTM4LTE3LjI4OSAxMi4yNTIgMCAxMC4yMTcgMzAuODczIDE4LjUgNjguOTU2IDE4LjVzNjguOTU2LTguMjgyOCA2OC45NTYtMTguNWMwLTQuNjk4OC02LjUzMTItOC45ODg4LTE3LjI4OS0xMi4yNTIgMC4wNDI1LTAuMDMwMyAwLjA4MzgtMC4wNiAwLjEyNjI1LTAuMDkxMiAxMC44NzggMy4yODI4IDE3LjQ4MiA3LjYwNjUgMTcuNDgyIDEyLjM0NCAwIDEwLjI2NS0zMS4wMTUgMTguNTg2LTY5LjI3NiAxOC41ODYiIGZpbGw9IiNmNGY0ZjQiLz4KICAgPHBhdGggaWQ9InBhdGg0MzA1IiBkPSJtNDUxLjI4IDkzMC43NWMtMzguMDgzIDAtNjguOTU2LTguMjgyOC02OC45NTYtMTguNSAwLTQuNjk4OCA2LjUzMDgtOC45ODg4IDE3LjI4OS0xMi4yNTIgMC4wNDA1IDAuMDI5OSAwLjA4NjQgMC4wNjI1IDAuMTI2ODcgMC4wOTE0LTEwLjY0MiAzLjI0MzYtMTcuMDk1IDcuNS0xNy4wOTUgMTIuMTYxIDAgMTAuMTcgMzAuNzI5IDE4LjQxNSA2OC42MzUgMTguNDE1czY4LjYzNC04LjI0NTIgNjguNjM0LTE4LjQxNWMwLTQuNjYxMS02LjQ1MjUtOC45MTc1LTE3LjA5NS0xMi4xNjEgMC4wNDEyLTAuMDI4OSAwLjA4NzUtMC4wNjE1IDAuMTI3NS0wLjA5MTQgMTAuNzU4IDMuMjYzOCAxNy4yODkgNy41NTM4IDE3LjI4OSAxMi4yNTIgMCAxMC4yMTctMzAuODcyIDE4LjUtNjguOTU2IDE4LjUiIGZpbGw9IiNmNGY0ZjQiLz4KICAgPHBhdGggaWQ9InBhdGg0MzA3IiBkPSJtNDUxLjI4IDkzMC42N2MtMzcuOTA2IDAtNjguNjM1LTguMjQ1Mi02OC42MzUtMTguNDE1IDAtNC42NjExIDYuNDUzMS04LjkxNzUgMTcuMDk1LTEyLjE2MSAwLjA0MyAwLjAzMTIgMC4wODUgMC4wNjEgMC4xMjggMC4wOTIyLTEwLjUyNCAzLjIyMzctMTYuOTAyIDcuNDQ2NC0xNi45MDIgMTIuMDY5IDAgMTAuMTIyIDMwLjU4NiAxOC4zMjkgNjguMzE0IDE4LjMyOXM2OC4zMTQtOC4yMDYxIDY4LjMxNC0xOC4zMjljMC00LjYyMjUtNi4zNzc1LTguODQ1Mi0xNi45MDItMTIuMDY5IDAuMDQzNy0wLjAzMTIgMC4wODUtMC4wNjEgMC4xMjc1LTAuMDkyMiAxMC42NDIgMy4yNDM2IDE3LjA5NSA3LjUgMTcuMDk1IDEyLjE2MSAwIDEwLjE3LTMwLjcyOSAxOC40MTUtNjguNjM0IDE4LjQxNSIgZmlsbD0iI2YzZjNmMyIvPgogICA8cGF0aCBpZD0icGF0aDQzMDkiIGQ9Im00NTEuMjggOTMwLjU4Yy0zNy43MjggMC02OC4zMTQtOC4yMDYxLTY4LjMxNC0xOC4zMjkgMC00LjYyMjUgNi4zNzc1LTguODQ1MiAxNi45MDItMTIuMDY5IDAuMDQzIDAuMDMwMyAwLjA4NSAwLjA2MDEgMC4xMjc4OCAwLjA5MTQtMTAuNDA3IDMuMjA1LTE2LjcwOSA3LjM5MzUtMTYuNzA5IDExLjk3OCAwIDEwLjA3NSAzMC40NDIgMTguMjQzIDY3Ljk5MyAxOC4yNDMgMzcuNTUyIDAgNjcuOTkzLTguMTY3NCA2Ny45OTMtMTguMjQzIDAtNC41ODQtNi4zMDEyLTguNzcyNS0xNi43MDktMTEuOTc4IDAuMDQyNS0wLjAzMTIgMC4wODUtMC4wNjExIDAuMTI3NS0wLjA5MTQgMTAuNTI1IDMuMjIzNiAxNi45MDIgNy40NDY0IDE2LjkwMiAxMi4wNjkgMCAxMC4xMjItMzAuNTg2IDE4LjMyOS02OC4zMTQgMTguMzI5IiBmaWxsPSIjZjNmM2YyIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDMxMSIgZD0ibTQ1MS4yOCA5MzAuNWMtMzcuNTUyIDAtNjcuOTkzLTguMTY3NC02Ny45OTMtMTguMjQzIDAtNC41ODQgNi4zMDE5LTguNzcyNSAxNi43MDktMTEuOTc4IDAuMDQzIDAuMDI5OCAwLjA4NTUgMC4wNjE1IDAuMTI4NSAwLjA5MTMtMTAuMjg5IDMuMTg1MS0xNi41MTcgNy4zNDE0LTE2LjUxNyAxMS44ODYgMCAxMC4wMjcgMzAuMjk4IDE4LjE1NiA2Ny42NzIgMTguMTU2czY3LjY3Mi04LjEyODkgNjcuNjcyLTE4LjE1NmMwLTQuNTQ2NC02LjIyNS04LjcwMTEtMTYuNTE2LTExLjg4NiAwLjA0MjUtMC4wMjk4IDAuMDg1LTAuMDYxNSAwLjEyODc1LTAuMDkxMyAxMC40MDggMy4yMDUgMTYuNzA5IDcuMzkzNSAxNi43MDkgMTEuOTc4IDAgMTAuMDc1LTMwLjQ0MSAxOC4yNDMtNjcuOTkzIDE4LjI0MyIgZmlsbD0iI2YyZjJmMiIvPgogICA8cGF0aCBpZD0icGF0aDQzMTMiIGQ9Im00NTEuMjggOTMwLjQxYy0zNy4zNzQgMC02Ny42NzItOC4xMjg5LTY3LjY3Mi0xOC4xNTYgMC00LjU0NDkgNi4yMjgtOC43MDExIDE2LjUxNy0xMS44ODYgMC4wNDI5IDAuMDI5OCAwLjA4NTkgMC4wNjEgMC4xMjkzNyAwLjA5MTQtMTAuMTc0IDMuMTY1LTE2LjMyNSA3LjI4NzUtMTYuMzI1IDExLjc5NSAwIDkuOTggMzAuMTU0IDE4LjA3IDY3LjM1MiAxOC4wNyAzNy4xOTcgMCA2Ny4zNTItOC4wODk4IDY3LjM1Mi0xOC4wNyAwLTQuNTA3NC02LjE1MjUtOC42Mjk5LTE2LjMyNS0xMS43OTUgMC4wNDI1LTAuMDMwNCAwLjA4NjItMC4wNjE2IDAuMTI4NzUtMC4wOTE0IDEwLjI5MSAzLjE4NTEgMTYuNTE2IDcuMzM5OSAxNi41MTYgMTEuODg2IDAgMTAuMDI3LTMwLjI5OCAxOC4xNTYtNjcuNjcyIDE4LjE1NiIgZmlsbD0iI2YyZjFmMSIvPgogICA8cGF0aCBpZD0icGF0aDQzMTUiIGQ9Im00NTEuMjggOTMwLjMyYy0zNy4xOTcgMC02Ny4zNTItOC4wODk4LTY3LjM1Mi0xOC4wNyAwLTQuNTA3NCA2LjE1MTQtOC42Mjk5IDE2LjMyNS0xMS43OTUgMC4wNDMgMC4wMzEyIDAuMDg2NCAwLjA2MSAwLjEyOTM4IDAuMDkyMy0xMC4wNTYgMy4xNDUtMTYuMTM0IDcuMjMzOS0xNi4xMzQgMTEuNzAzIDAgOS45MzI2IDMwLjAxMSAxNy45ODQgNjcuMDMxIDE3Ljk4NHM2Ny4wMy04LjA1MTMgNjcuMDMtMTcuOTg0YzAtNC40Njg4LTYuMDc3NS04LjU1NzYtMTYuMTM0LTExLjcwMyAwLjA0MjUtMC4wMzEyIDAuMDg2My0wLjA2MSAwLjEzLTAuMDkyMyAxMC4xNzIgMy4xNjUgMTYuMzI1IDcuMjg3NSAxNi4zMjUgMTEuNzk1IDAgOS45OC0zMC4xNTUgMTguMDctNjcuMzUyIDE4LjA3IiBmaWxsPSIjZjFmMWYxIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDMxNyIgZD0ibTQ1MS4yOCA5MzAuMjRjLTM3LjAyIDAtNjcuMDMxLTguMDUxMy02Ny4wMzEtMTcuOTg0IDAtNC40Njg4IDYuMDc3Ni04LjU1NzYgMTYuMTM0LTExLjcwMyAwLjA0MyAwLjAzMDMgMC4wODY5IDAuMDYxNSAwLjEyOTg3IDAuMDkxMy05LjkzOTkgMy4xMjUtMTUuOTQzIDcuMTgwMi0xNS45NDMgMTEuNjExIDAgOS44ODQ4IDI5Ljg2NyAxNy44OTcgNjYuNzEgMTcuODk3czY2LjcxLTguMDEyNyA2Ni43MS0xNy44OTdjMC00LjQzMTEtNi4wMDM4LTguNDg2NC0xNS45NDQtMTEuNjExIDAuMDQzOC0wLjAyOTcgMC4wODc1LTAuMDYxIDAuMTMtMC4wOTEzIDEwLjA1NiAzLjE0NSAxNi4xMzQgNy4yMzM5IDE2LjEzNCAxMS43MDMgMCA5LjkzMjYtMzAuMDEgMTcuOTg0LTY3LjAzIDE3Ljk4NCIgZmlsbD0iI2YwZjBmMCIvPgogICA8cGF0aCBpZD0icGF0aDQzMTkiIGQ9Im00NTEuMjggOTMwLjE1Yy0zNi44NDMgMC02Ni43MS04LjAxMjctNjYuNzEtMTcuODk3IDAtNC40MzExIDYuMDAzLTguNDg2NCAxNS45NDMtMTEuNjExIDAuMDQzNSAwLjAzMDQgMC4wODc0IDAuMDYxNiAwLjEzMDg4IDAuMDkxNC05LjgyNTIgMy4xMDY0LTE1Ljc1MyA3LjEyNzQtMTUuNzUzIDExLjUyIDAgOS44Mzc0IDI5LjcyMyAxNy44MTIgNjYuMzg5IDE3LjgxMnM2Ni4zODktNy45NzUxIDY2LjM4OS0xNy44MTJjMC00LjM5MjYtNS45Mjg4LTguNDEzNi0xNS43NTQtMTEuNTIgMC4wNDM3LTAuMDI5OCAwLjA4ODgtMC4wNjEgMC4xMzEyNS0wLjA5MTQgOS45NCAzLjEyNSAxNS45NDQgNy4xODAyIDE1Ljk0NCAxMS42MTEgMCA5Ljg4NDgtMjkuODY4IDE3Ljg5Ny02Ni43MSAxNy44OTciIGZpbGw9IiNmMGYwZWYiLz4KICAgPHBhdGggaWQ9InBhdGg0MzIxIiBkPSJtNDUxLjI4IDkzMC4wNmMtMzYuNjY2IDAtNjYuMzg5LTcuOTc1MS02Ni4zODktMTcuODEyIDAtNC4zOTI2IDUuOTI3Ni04LjQxMzYgMTUuNzUzLTExLjUyIDAuMDQ1NCAwLjAzMTIgMC4wODU1IDAuMDYgMC4xMzEzNyAwLjA5MTItOS43MTA1IDMuMDg2NS0xNS41NjQgNy4wNzM4LTE1LjU2NCAxMS40MjkgMCA5Ljc5IDI5LjU4IDE3LjcyNiA2Ni4wNjggMTcuNzI2czY2LjA2OC03LjkzNjEgNjYuMDY4LTE3LjcyNmMwLTQuMzU1LTUuODUyNS04LjM0MjItMTUuNTY0LTExLjQyOSAwLjA0NjItMC4wMzEyIDAuMDg2My0wLjA2IDAuMTMxMjUtMC4wOTEyIDkuODI1IDMuMTA2NCAxNS43NTQgNy4xMjc0IDE1Ljc1NCAxMS41MiAwIDkuODM3NC0yOS43MjQgMTcuODEyLTY2LjM4OSAxNy44MTIiIGZpbGw9IiNlZmVmZWYiLz4KICAgPHBhdGggaWQ9InBhdGg0MzIzIiBkPSJtNDUxLjI4IDkyOS45OGMtMzYuNDg5IDAtNjYuMDY4LTcuOTM2MS02Ni4wNjgtMTcuNzI2IDAtNC4zNTUgNS44NTMtOC4zNDIyIDE1LjU2NC0xMS40MjkgMC4wNDI5IDAuMDI5OSAwLjA4ODQgMC4wNjI1IDAuMTMxMzggMC4wOTI0LTkuNTk0MiAzLjA2NDktMTUuMzc1IDcuMDItMTUuMzc1IDExLjMzNiAwIDkuNzQyNiAyOS40MzcgMTcuNjQgNjUuNzQ4IDE3LjY0IDM2LjMxMiAwIDY1Ljc0OC03Ljg5NzUgNjUuNzQ4LTE3LjY0IDAtNC4zMTY0LTUuNzgtOC4yNzE1LTE1LjM3NS0xMS4zMzYgMC4wNDM3LTAuMDI5OSAwLjA4ODctMC4wNjI1IDAuMTMxMjUtMC4wOTI0IDkuNzExMiAzLjA4NjUgMTUuNTY0IDcuMDczOCAxNS41NjQgMTEuNDI5IDAgOS43OS0yOS41OCAxNy43MjYtNjYuMDY4IDE3LjcyNiIgZmlsbD0iI2VlZSIvPgogICA8cGF0aCBpZD0icGF0aDQzMjUiIGQ9Im00NTEuMjggOTI5Ljg5Yy0zNi4zMTIgMC02NS43NDgtNy44OTc1LTY1Ljc0OC0xNy42NCAwLTQuMzE2NCA1Ljc4MDQtOC4yNzE1IDE1LjM3NS0xMS4zMzYgMC4wNDM0IDAuMDMwMiAwLjA4ODggMC4wNjE1IDAuMTMyMjUgMC4wOTEyLTkuNDgxNCAzLjA0NjQtMTUuMTg2IDYuOTY2NC0xNS4xODYgMTEuMjQ1IDAgOS42OTQ5IDI5LjI5MiAxNy41NTQgNjUuNDI3IDE3LjU1NCAzNi4xMzQgMCA2NS40MjctNy44NTg4IDY1LjQyNy0xNy41NTQgMC00LjI3ODgtNS43MDM4LTguMTk4OC0xNS4xODYtMTEuMjQ1IDAuMDQzOC0wLjAyOTcgMC4wODg4LTAuMDYxIDAuMTMyNS0wLjA5MTIgOS41OTUgMy4wNjQ5IDE1LjM3NSA3LjAyIDE1LjM3NSAxMS4zMzYgMCA5Ljc0MjYtMjkuNDM2IDE3LjY0LTY1Ljc0OCAxNy42NCIgZmlsbD0iI2VlZWVlZCIvPgogICA8cGF0aCBpZD0icGF0aDQzMjciIGQ9Im00NTEuMjggOTI5LjgxYy0zNi4xMzQgMC02NS40MjctNy44NTg4LTY1LjQyNy0xNy41NTQgMC00LjI3ODggNS43MDQxLTguMTk4OCAxNS4xODYtMTEuMjQ1IDAuMDQ1NCAwLjAzMTIgMC4wODY5IDAuMDYwMSAwLjEzMjg3IDAuMDkxNC05LjM2NzIgMy4wMjYyLTE0Ljk5OCA2LjkxMzUtMTQuOTk4IDExLjE1NCAwIDkuNjQ3NSAyOS4xNDggMTcuNDY3IDY1LjEwNiAxNy40NjcgMzUuOTU3IDAgNjUuMTA2LTcuODE5OCA2NS4xMDYtMTcuNDY3IDAtNC4yNDAyLTUuNjMtOC4xMjc1LTE0Ljk5OC0xMS4xNTQgMC4wNDYzLTAuMDMxMiAwLjA4NzUtMC4wNjAxIDAuMTMyNS0wLjA5MTQgOS40ODI1IDMuMDQ2NCAxNS4xODYgNi45NjY0IDE1LjE4NiAxMS4yNDUgMCA5LjY5NDktMjkuMjkyIDE3LjU1NC02NS40MjcgMTcuNTU0IiBmaWxsPSIjZWRlZGVjIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDMyOSIgZD0ibTQ1MS4yOCA5MjkuNzJjLTM1Ljk1OCAwLTY1LjEwNi03LjgxOTgtNjUuMTA2LTE3LjQ2NyAwLTQuMjQwMiA1LjYzMDQtOC4xMjc1IDE0Ljk5OC0xMS4xNTQgMC4wNDM0IDAuMDMwMyAwLjA4OTcgMC4wNjI1IDAuMTMzMjUgMC4wOTIzLTkuMjUxIDMuMDA1NC0xNC44MSA2Ljg1ODktMTQuODEgMTEuMDYyIDAgOS42MDAxIDI5LjAwNiAxNy4zODEgNjQuNzg2IDE3LjM4MXM2NC43ODYtNy43ODEyIDY0Ljc4Ni0xNy4zODFjMC00LjIwMjYtNS41Ni04LjA1NjEtMTQuODExLTExLjA2MiAwLjA0MzctMC4wMjk4IDAuMDktMC4wNjIgMC4xMzM3NS0wLjA5MjMgOS4zNjc1IDMuMDI2MiAxNC45OTggNi45MTM1IDE0Ljk5OCAxMS4xNTQgMCA5LjY0NzUtMjkuMTQ4IDE3LjQ2Ny02NS4xMDYgMTcuNDY3IiBmaWxsPSIjZWRlZGVjIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDMzMSIgZD0ibTQ1MS4yOCA5MjkuNjNjLTM1Ljc4IDAtNjQuNzg2LTcuNzgxMi02NC43ODYtMTcuMzgxIDAtNC4yMDI2IDUuNTU5NS04LjA1NjEgMTQuODEtMTEuMDYyIDAuMDQ1OSAwLjAzMTcgMC4wODg0IDAuMDYgMC4xMzM3NSAwLjA5MTItOS4xMzgxIDIuOTg1NC0xNC42MjQgNi44MDY2LTE0LjYyNCAxMC45NyAwIDkuNTUyOCAyOC44NjIgMTcuMjk1IDY0LjQ2NSAxNy4yOTUgMzUuNjAyIDAgNjQuNDY0LTcuNzQyMiA2NC40NjQtMTcuMjk1IDAtNC4xNjM2LTUuNDg1LTcuOTg0OS0xNC42MjQtMTAuOTcgMC4wNDYyLTAuMDMxMiAwLjA4ODctMC4wNTk1IDAuMTMzNzUtMC4wOTEyIDkuMjUxMiAzLjAwNTQgMTQuODExIDYuODU4OSAxNC44MTEgMTEuMDYyIDAgOS42MDAxLTI5LjAwNiAxNy4zODEtNjQuNzg2IDE3LjM4MSIgZmlsbD0iI2VjZWNlYiIvPgogICA8cGF0aCBpZD0icGF0aDQzMzMiIGQ9Im00NTEuMjggOTI5LjU1Yy0zNS42MDMgMC02NC40NjUtNy43NDIyLTY0LjQ2NS0xNy4yOTUgMC00LjE2MzYgNS40ODU0LTcuOTg0OSAxNC42MjQtMTAuOTcgMC4wNDQgMC4wMzAzIDAuMDkxNCAwLjA2MjUgMC4xMzQ3NSAwLjA5MjgtOS4wMjQ0IDIuOTYzOS0xNC40MzggNi43NTEtMTQuNDM4IDEwLjg3OCAwIDkuNTA0OSAyOC43MTggMTcuMjEgNjQuMTQ0IDE3LjIxczY0LjE0My03LjcwNTEgNjQuMTQzLTE3LjIxYzAtNC4xMjY1LTUuNDEyNS03LjkxMzYtMTQuNDM2LTEwLjg3OCAwLjA0MzgtMC4wMzAzIDAuMDktMC4wNjI1IDAuMTMzNzUtMC4wOTI4IDkuMTM4OCAyLjk4NTQgMTQuNjI0IDYuODA2NiAxNC42MjQgMTAuOTcgMCA5LjU1MjgtMjguODYyIDE3LjI5NS02NC40NjQgMTcuMjk1IiBmaWxsPSIjZWNlYmVhIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDMzNSIgZD0ibTQ1MS4yOCA5MjkuNDZjLTM1LjQyNiAwLTY0LjE0NC03LjcwNTEtNjQuMTQ0LTE3LjIxIDAtNC4xMjY1IDUuNDEzMS03LjkxMzYgMTQuNDM4LTEwLjg3OCAwLjA0NiAwLjAzMTIgMC4wODg5IDAuMDYwMSAwLjEzNDg4IDAuMDkxNC04LjkxMTEgMi45NDM5LTE0LjI1MiA2LjY5NzItMTQuMjUyIDEwLjc4NiAwIDkuNDU3NSAyOC41NzUgMTcuMTI0IDYzLjgyMyAxNy4xMjRzNjMuODIzLTcuNjY2IDYzLjgyMy0xNy4xMjRjMC00LjA4ODktNS4zNC03Ljg0MjItMTQuMjUxLTEwLjc4NiAwLjA0NS0wLjAzMTIgMC4wODg4LTAuMDYwMSAwLjEzNS0wLjA5MTQgOS4wMjM4IDIuOTYzOSAxNC40MzYgNi43NTEgMTQuNDM2IDEwLjg3OCAwIDkuNTA0OS0yOC43MTcgMTcuMjEtNjQuMTQzIDE3LjIxIiBmaWxsPSIjZWJlYmVhIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDMzNyIgZD0ibTQ1MS4yOCA5MjkuMzhjLTM1LjI0OCAwLTYzLjgyMy03LjY2Ni02My44MjMtMTcuMTI0IDAtNC4wODg5IDUuMzQwNC03Ljg0MjIgMTQuMjUyLTEwLjc4NiAwLjA0MzkgMC4wMjk3IDAuMDkyMiAwLjA2MjUgMC4xMzU3NSAwLjA5MTItOC43OTg5IDIuOTI0OS0xNC4wNjYgNi42NDUxLTE0LjA2NiAxMC42OTUgMCA5LjQxMDEgMjguNDMxIDE3LjAzOCA2My41MDIgMTcuMDM4czYzLjUwMi03LjYyNzUgNjMuNTAyLTE3LjAzOGMwLTQuMDQ5OC01LjI2NzUtNy43Ny0xNC4wNjYtMTAuNjk1IDAuMDQzNy0wLjAyODcgMC4wOTI1LTAuMDYxNSAwLjEzNjI1LTAuMDkxMiA4LjkxMTIgMi45NDM5IDE0LjI1MSA2LjY5NzIgMTQuMjUxIDEwLjc4NiAwIDkuNDU3NS0yOC41NzQgMTcuMTI0LTYzLjgyMyAxNy4xMjQiIGZpbGw9IiNlYWVhZTkiLz4KICAgPHBhdGggaWQ9InBhdGg0MzM5IiBkPSJtNDUxLjI4IDkyOS4yOWMtMzUuMDcxIDAtNjMuNTAyLTcuNjI3NS02My41MDItMTcuMDM4IDAtNC4wNDk4IDUuMjY3Ni03Ljc3IDE0LjA2Ni0xMC42OTUgMC4wNDY0IDAuMDMxMiAwLjA5MDMgMC4wNjExIDAuMTM2MTIgMC4wOTI0LTguNjg1IDIuOTAzOC0xMy44ODIgNi41ODk4LTEzLjg4MiAxMC42MDIgMCA5LjM2MjIgMjguMjg4IDE2Ljk1MSA2My4xODIgMTYuOTUxczYzLjE4Mi03LjU4ODkgNjMuMTgyLTE2Ljk1MWMwLTQuMDEyOC01LjE5NzUtNy42OTg4LTEzLjg4Mi0xMC42MDIgMC4wNDYzLTAuMDMxMiAwLjA5LTAuMDYxMSAwLjEzNjI1LTAuMDkyNCA4Ljc5ODggMi45MjQ5IDE0LjA2NiA2LjY0NTEgMTQuMDY2IDEwLjY5NSAwIDkuNDEwMS0yOC40MyAxNy4wMzgtNjMuNTAyIDE3LjAzOCIgZmlsbD0iI2VhZWFlOCIvPgogICA8cGF0aCBpZD0icGF0aDQzNDEiIGQ9Im00NTEuMjggOTI5LjJjLTM0Ljg5NCAwLTYzLjE4Mi03LjU4ODktNjMuMTgyLTE2Ljk1MSAwLTQuMDEyOCA1LjE5NjgtNy42OTg4IDEzLjg4Mi0xMC42MDIgMC4wNDY0IDAuMDMxMiAwLjA5MDkgMC4wNjEgMC4xMzY3NSAwLjA5MTMtOC41NzIyIDIuODgzOC0xMy42OTggNi41Mzc2LTEzLjY5OCAxMC41MTEgMCA5LjMxNSAyOC4xNDQgMTYuODY1IDYyLjg2MSAxNi44NjVzNjIuODYtNy41NDk4IDYyLjg2LTE2Ljg2NWMwLTMuOTczNi01LjEyNS03LjYyNzUtMTMuNjk4LTEwLjUxMSAwLjA0NjItMC4wMzAzIDAuMDktMC4wNiAwLjEzNjI1LTAuMDkxMyA4LjY4NSAyLjkwMzggMTMuODgyIDYuNTg5OCAxMy44ODIgMTAuNjAyIDAgOS4zNjIyLTI4LjI4OCAxNi45NTEtNjMuMTgyIDE2Ljk1MSIgZmlsbD0iI2U5ZTllOCIvPgogICA8cGF0aCBpZD0icGF0aDQzNDMiIGQ9Im00NTEuMjggOTI5LjEyYy0zNC43MTcgMC02Mi44NjEtNy41NDk4LTYyLjg2MS0xNi44NjUgMC0zLjk3MzYgNS4xMjU1LTcuNjI3NSAxMy42OTgtMTAuNTExIDAuMDQ2NCAwLjAzMTIgMC4wOTE0IDAuMDYxIDAuMTM3NzUgMC4wOTIzLTguNDYxIDIuODYyOS0xMy41MTUgNi40ODMtMTMuNTE1IDEwLjQxOSAwIDkuMjY3NiAyOCAxNi43NzkgNjIuNTQgMTYuNzc5czYyLjUzOS03LjUxMTIgNjIuNTM5LTE2Ljc3OWMwLTMuOTM2LTUuMDUzOC03LjU1NjEtMTMuNTE0LTEwLjQxOSAwLjA0NjItMC4wMzEyIDAuMDkxMi0wLjA2MSAwLjEzNzUtMC4wOTIzIDguNTcyNSAyLjg4MzggMTMuNjk4IDYuNTM3NiAxMy42OTggMTAuNTExIDAgOS4zMTUtMjguMTQ0IDE2Ljg2NS02Mi44NiAxNi44NjUiIGZpbGw9IiNlOWU4ZTciLz4KICAgPHBhdGggaWQ9InBhdGg0MzQ1IiBkPSJtNDUxLjI4IDkyOS4wM2MtMzQuNTQgMC02Mi41NC03LjUxMTItNjIuNTQtMTYuNzc5IDAtMy45MzYgNS4wNTM4LTcuNTU2MSAxMy41MTUtMTAuNDE5IDAuMDQ1OSAwLjAzMTIgMC4wOTE3IDAuMDYxNiAwLjEzNzYzIDAuMDkyOS04LjM0OTYgMi44NDEzLTEzLjMzMiA2LjQyNzMtMTMuMzMyIDEwLjMyNiAwIDkuMjIwMiAyNy44NTYgMTYuNjk0IDYyLjIxOSAxNi42OTQgMzQuMzYyIDAgNjIuMjE5LTcuNDczNiA2Mi4yMTktMTYuNjk0IDAtMy44OTg5LTQuOTgyNS03LjQ4NDktMTMuMzMyLTEwLjMyNiAwLjA0NjMtMC4wMzEyIDAuMDkyNS0wLjA2MTYgMC4xMzg3NS0wLjA5MjkgOC40NiAyLjg2MjkgMTMuNTE0IDYuNDgzIDEzLjUxNCAxMC40MTkgMCA5LjI2NzYtMjcuOTk5IDE2Ljc3OS02Mi41MzkgMTYuNzc5IiBmaWxsPSIjZThlOGU2Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNDM0NyIgZD0ibTQ1MS4yOCA5MjguOTVjLTM0LjM2MyAwLTYyLjIxOS03LjQ3MzYtNjIuMjE5LTE2LjY5NCAwLTMuODk4OSA0Ljk4MTktNy40ODQ5IDEzLjMzMi0xMC4zMjYgMC4wNDY0IDAuMDI5OCAwLjA5MjQgMC4wNiAwLjEzODc1IDAuMDkxMy04LjIzNzQgMi44MTk5LTEzLjE1IDYuMzczNS0xMy4xNSAxMC4yMzUgMCA5LjE3MjQgMjcuNzEzIDE2LjYwNyA2MS44OTggMTYuNjA3IDM0LjE4NiAwIDYxLjg5OC03LjQzNTEgNjEuODk4LTE2LjYwNyAwLTMuODYxNC00LjkxMTItNy40MTUtMTMuMTQ5LTEwLjIzNSAwLjA0NjItMC4wMzEyIDAuMDkxMy0wLjA2MTUgMC4xMzc1LTAuMDkxMyA4LjM1IDIuODQxMyAxMy4zMzIgNi40MjczIDEzLjMzMiAxMC4zMjYgMCA5LjIyMDItMjcuODU3IDE2LjY5NC02Mi4yMTkgMTYuNjk0IiBmaWxsPSIjZTdlN2U2Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNDM0OSIgZD0ibTQ1MS4yOCA5MjguODZjLTM0LjE4NiAwLTYxLjg5OC03LjQzNTEtNjEuODk4LTE2LjYwNyAwLTMuODYxNCA0LjkxMjEtNy40MTUgMTMuMTUtMTAuMjM1IDAuMDQ2NCAwLjAzMTIgMC4wOTI4IDAuMDYxIDAuMTM5MTIgMC4wOTIyLTguMTI4IDIuNzk4OS0xMi45NjggNi4zMTg5LTEyLjk2OCAxMC4xNDMgMCA5LjEyMzUgMjcuNTY5IDE2LjUyMSA2MS41NzggMTYuNTIxIDM0LjAwOCAwIDYxLjU3OC03LjM5OCA2MS41NzgtMTYuNTIxIDAtMy44MjM4LTQuODQtNy4zNDM4LTEyLjk2OS0xMC4xNDMgMC4wNDYzLTAuMDMxMiAwLjA5MzctMC4wNjEgMC4xNC0wLjA5MjIgOC4yMzc1IDIuODE5OSAxMy4xNDkgNi4zNzM1IDEzLjE0OSAxMC4yMzUgMCA5LjE3MjQtMjcuNzEyIDE2LjYwNy02MS44OTggMTYuNjA3IiBmaWxsPSIjZTdlNmU1Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNDM1MSIgZD0ibTQ1MS4yOCA5MjguNzdjLTM0LjAwOSAwLTYxLjU3OC03LjM5OC02MS41NzgtMTYuNTIxIDAtMy44MjM4IDQuODM5Ny03LjM0MzggMTIuOTY4LTEwLjE0MyAwLjA0NjQgMC4wMzA0IDAuMDkzMyAwLjA2MTYgMC4xMzk2MyAwLjA5MTQtOC4wMTU2IDIuNzc4OC0xMi43ODcgNi4yNjYxLTEyLjc4NyAxMC4wNTEgMCA5LjA3NzYgMjcuNDI2IDE2LjQzNSA2MS4yNTcgMTYuNDM1czYxLjI1Ny03LjM1NzQgNjEuMjU3LTE2LjQzNWMwLTMuNzg1MS00Ljc3MTItNy4yNzI1LTEyLjc4OC0xMC4wNTEgMC4wNDc1LTAuMDI5OCAwLjA5MzctMC4wNjEgMC4xNC0wLjA5MTQgOC4xMjg4IDIuNzk4OSAxMi45NjkgNi4zMTg5IDEyLjk2OSAxMC4xNDMgMCA5LjEyMzUtMjcuNTcgMTYuNTIxLTYxLjU3OCAxNi41MjEiIGZpbGw9IiNlNmU2ZTQiLz4KICAgPHBhdGggaWQ9InBhdGg0MzUzIiBkPSJtNDUxLjI4IDkyOC42OWMtMzMuODMyIDAtNjEuMjU3LTcuMzU3NC02MS4yNTctMTYuNDM1IDAtMy43ODUxIDQuNzcxNS03LjI3MjUgMTIuNzg3LTEwLjA1MSAwLjA0NjQgMC4wMzEyIDAuMDk0MiAwLjA2MjUgMC4xNDA2MiAwLjA5MjItNy45MDYyIDIuNzU3OS0xMi42MDYgNi4yMTE1LTEyLjYwNiA5Ljk1OSAwIDkuMDI4OCAyNy4yODIgMTYuMzQ5IDYwLjkzNiAxNi4zNDlzNjAuOTM2LTcuMzE5OSA2MC45MzYtMTYuMzQ5YzAtMy43NDc1LTQuNy03LjIwMTEtMTIuNjA2LTkuOTU5IDAuMDQ2My0wLjAyOTcgMC4wOTM3LTAuMDYxIDAuMTQtMC4wOTIyIDguMDE2MiAyLjc3ODggMTIuNzg4IDYuMjY2MSAxMi43ODggMTAuMDUxIDAgOS4wNzc2LTI3LjQyNiAxNi40MzUtNjEuMjU3IDE2LjQzNSIgZmlsbD0iI2U1ZTVlMyIvPgogICA8cGF0aCBpZD0icGF0aDQzNTUiIGQ9Im00NTEuMjggOTI4LjZjLTMzLjY1NCAwLTYwLjkzNi03LjMxOTktNjAuOTM2LTE2LjM0OSAwLTMuNzQ3NSA0LjcwMDEtNy4yMDExIDEyLjYwNi05Ljk1OSAwLjA0NjQgMC4wMzAzIDAuMDk0NyAwLjA2MTUgMC4xNDExMyAwLjA5MjgtNy43OTU0IDIuNzM2NC0xMi40MjcgNi4xNTYyLTEyLjQyNyA5Ljg2NjIgMCA4Ljk4MTUgMjcuMTM4IDE2LjI2MyA2MC42MTUgMTYuMjYzczYwLjYxNi03LjI4MTIgNjAuNjE2LTE2LjI2M2MwLTMuNzEtNC42MzEyLTcuMTI5OS0xMi40MjgtOS44NjYyIDAuMDQ2Mi0wLjAzMTIgMC4wOTUtMC4wNjI1IDAuMTQxMjUtMC4wOTI4IDcuOTA2MiAyLjc1NzkgMTIuNjA2IDYuMjExNSAxMi42MDYgOS45NTkgMCA5LjAyODgtMjcuMjgyIDE2LjM0OS02MC45MzYgMTYuMzQ5IiBmaWxsPSIjZTRlNGUzIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDM1NyIgZD0ibTQ1MS4yOCA5MjguNTJjLTMzLjQ3NyAwLTYwLjYxNS03LjI4MTItNjAuNjE1LTE2LjI2MyAwLTMuNzEgNC42MzE0LTcuMTI5OSAxMi40MjctOS44NjYyIDAuMDQ2OSAwLjAyOTkgMC4wOTUzIDAuMDYxMSAwLjE0MTYyIDAuMDkxNC03LjY4NiAyLjcxNjItMTIuMjQ4IDYuMTAyNS0xMi4yNDggOS43NzQ5IDAgOC45MzM2IDI2Ljk5NSAxNi4xNzYgNjAuMjk1IDE2LjE3NnM2MC4yOTQtNy4yNDI2IDYwLjI5NC0xNi4xNzZjMC0zLjY3MjQtNC41NjEyLTcuMDU4Ni0xMi4yNDgtOS43NzQ5IDAuMDQ2My0wLjAzMDIgMC4wOTUtMC4wNjE1IDAuMTQxMjUtMC4wOTE0IDcuNzk2MiAyLjczNjQgMTIuNDI4IDYuMTU2MiAxMi40MjggOS44NjYyIDAgOC45ODE1LTI3LjEzOCAxNi4yNjMtNjAuNjE2IDE2LjI2MyIgZmlsbD0iI2U0ZTNlMiIvPgogICA8cGF0aCBpZD0icGF0aDQzNTkiIGQ9Im00NTEuMjggOTI4LjQzYy0zMy4zIDAtNjAuMjk1LTcuMjQyNi02MC4yOTUtMTYuMTc2IDAtMy42NzI0IDQuNTYyMS03LjA1ODYgMTIuMjQ4LTkuNzc0OSAwLjA0ODggMC4wMzIzIDAuMDkzMyAwLjA2MSAwLjE0MjUgMC4wOTIyLTcuNTc3NiAyLjY5MzktMTIuMDcgNi4wNDc5LTEyLjA3IDkuNjgyNiAwIDguODg2MiAyNi44NTEgMTYuMDkxIDU5Ljk3NCAxNi4wOTEgMzMuMTIyIDAgNTkuOTc0LTcuMjA1MSA1OS45NzQtMTYuMDkxIDAtMy42MzQ4LTQuNDkyNS02Ljk4ODgtMTIuMDctOS42ODI2IDAuMDQ4Ny0wLjAzMTIgMC4wOTM3LTAuMDYgMC4xNDI1LTAuMDkyMiA3LjY4NjIgMi43MTYyIDEyLjI0OCA2LjEwMjUgMTIuMjQ4IDkuNzc0OSAwIDguOTMzNi0yNi45OTQgMTYuMTc2LTYwLjI5NCAxNi4xNzYiIGZpbGw9IiNlM2UzZTEiLz4KICAgPHBhdGggaWQ9InBhdGg0MzYxIiBkPSJtNDUxLjI4IDkyOC4zNGMtMzMuMTIzIDAtNTkuOTc0LTcuMjA1MS01OS45NzQtMTYuMDkxIDAtMy42MzQ4IDQuNDkyMS02Ljk4ODggMTIuMDctOS42ODI2IDAuMDQ2NSAwLjAzMDMgMC4wOTYyIDAuMDYyNSAwLjE0MzEzIDAuMDkyOC03LjQ2NzggMi42NzI0LTExLjg5MiA1Ljk5MjItMTEuODkyIDkuNTg5OSAwIDguODM4OSAyNi43MDggMTYuMDA1IDU5LjY1MyAxNi4wMDVzNTkuNjUzLTcuMTY2IDU5LjY1My0xNi4wMDVjMC0zLjU5NzYtNC40MjM4LTYuOTE3NS0xMS44OTEtOS41ODk5IDAuMDQ2My0wLjAzMDMgMC4wOTYyLTAuMDYyNSAwLjE0MjUtMC4wOTI4IDcuNTc3NSAyLjY5MzkgMTIuMDcgNi4wNDc5IDEyLjA3IDkuNjgyNiAwIDguODg2Mi0yNi44NTIgMTYuMDkxLTU5Ljk3NCAxNi4wOTEiIGZpbGw9IiNlMmUyZTAiLz4KICAgPHBhdGggaWQ9InBhdGg0MzYzIiBkPSJtNDUxLjI4IDkyOC4yNmMtMzIuOTQ1IDAtNTkuNjUzLTcuMTY2LTU5LjY1My0xNi4wMDUgMC0zLjU5NzYgNC40MjQ0LTYuOTE3NSAxMS44OTItOS41ODk5IDAuMDQ4OSAwLjAzMTIgMC4wOTQyIDAuMDYwMSAwLjE0MzUgMC4wOTA5LTcuMzU4OSAyLjY1MjgtMTEuNzE1IDUuOTM5LTExLjcxNSA5LjQ5OSAwIDguNzkxIDI2LjU2NCAxNS45MTkgNTkuMzMyIDE1LjkxOXM1OS4zMzItNy4xMjggNTkuMzMyLTE1LjkxOWMwLTMuNTYtNC4zNTUtNi44NDYyLTExLjcxNC05LjQ5OSAwLjA0ODgtMC4wMzA4IDAuMDkzNy0wLjA1OTYgMC4xNDM3NS0wLjA5MDkgNy40Njc1IDIuNjcyNCAxMS44OTEgNS45OTIyIDExLjg5MSA5LjU4OTkgMCA4LjgzODktMjYuNzA4IDE2LjAwNS01OS42NTMgMTYuMDA1IiBmaWxsPSIjZTJlMWRmIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDM2NSIgZD0ibTQ1MS4yOCA5MjguMTdjLTMyLjc2OSAwLTU5LjMzMi03LjEyOC01OS4zMzItMTUuOTE5IDAtMy41NiA0LjM1NTktNi44NDYyIDExLjcxNS05LjQ5OSAwLjA0NjkgMC4wMzAzIDAuMDk3MiAwLjA2MyAwLjE0NDEyIDAuMDkyOC03LjI1MDUgMi42Mjk5LTExLjUzOCA1Ljg4MzgtMTEuNTM4IDkuNDA2MiAwIDguNzQzNiAyNi40MiAxNS44MzMgNTkuMDEyIDE1LjgzMyAzMi41OTEgMCA1OS4wMTItNy4wODg5IDU5LjAxMi0xNS44MzMgMC0zLjUyMjUtNC4yODc1LTYuNzc2NC0xMS41MzktOS40MDYyIDAuMDQ3NS0wLjAyOTggMC4wOTc1LTAuMDYyNSAwLjE0NS0wLjA5MjggNy4zNTg4IDIuNjUyOCAxMS43MTQgNS45MzkgMTEuNzE0IDkuNDk5IDAgOC43OTEtMjYuNTY0IDE1LjkxOS01OS4zMzIgMTUuOTE5IiBmaWxsPSIjZTFlMGRmIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDM2NyIgZD0ibTQ1MS4yOCA5MjguMDhjLTMyLjU5MSAwLTU5LjAxMi03LjA4ODktNTkuMDEyLTE1LjgzMyAwLTMuNTIyNSA0LjI4NzYtNi43NzY0IDExLjUzOC05LjQwNjIgMC4wNDkyIDAuMDMxMiAwLjA5NTYgMC4wNjEgMC4xNDUgMC4wOTIyLTcuMTQyMSAyLjYwODktMTEuMzYyIDUuODI5MS0xMS4zNjIgOS4zMTQgMCA4LjY5NjIgMjYuMjc3IDE1Ljc0NiA1OC42OTEgMTUuNzQ2czU4LjY5LTcuMDQ5OCA1OC42OS0xNS43NDZjMC0zLjQ4NDktNC4yMi02LjcwNTEtMTEuMzYyLTkuMzE0IDAuMDUtMC4wMzEyIDAuMDk2Mi0wLjA2MSAwLjE0NS0wLjA5MjIgNy4yNTEyIDIuNjI5OSAxMS41MzkgNS44ODM4IDExLjUzOSA5LjQwNjIgMCA4Ljc0MzYtMjYuNDIgMTUuODMzLTU5LjAxMiAxNS44MzMiIGZpbGw9IiNlMGUwZGUiLz4KICAgPHBhdGggaWQ9InBhdGg0MzY5IiBkPSJtNDUxLjI4IDkyOGMtMzIuNDE0IDAtNTguNjkxLTcuMDQ5OC01OC42OTEtMTUuNzQ2IDAtMy40ODQ5IDQuMjIwMi02LjcwNTEgMTEuMzYyLTkuMzE0IDAuMDQ2OSAwLjAzMDMgMC4wOTkxIDAuMDYyNSAwLjE0NiAwLjA5MjgtNy4wMzY2IDIuNTg2LTExLjE4OCA1Ljc3MzUtMTEuMTg4IDkuMjIxMiAwIDguNjQ4OSAyNi4xMzMgMTUuNjYgNTguMzcgMTUuNjZzNTguMzctNy4wMTEzIDU4LjM3LTE1LjY2YzAtMy40NDc4LTQuMTUxMi02LjYzNTItMTEuMTg4LTkuMjIxMiAwLjA0NjItMC4wMzAzIDAuMDk4Ny0wLjA2MjUgMC4xNDUtMC4wOTI4IDcuMTQyNSAyLjYwODkgMTEuMzYyIDUuODI5MSAxMS4zNjIgOS4zMTQgMCA4LjY5NjItMjYuMjc2IDE1Ljc0Ni01OC42OSAxNS43NDYiIGZpbGw9IiNlMGRmZGQiLz4KICAgPHBhdGggaWQ9InBhdGg0MzcxIiBkPSJtNDUxLjI4IDkyNy45MWMtMzIuMjM3IDAtNTguMzctNy4wMTEzLTU4LjM3LTE1LjY2IDAtMy40NDc4IDQuMTUwOS02LjYzNTIgMTEuMTg4LTkuMjIxMiAwLjA0OTIgMC4wMzEyIDAuMDk2NiAwLjA2MTEgMC4xNDYgMC4wOTI0LTYuOTI3MiAyLjU2NDktMTEuMDEzIDUuNzE4OC0xMS4wMTMgOS4xMjg5IDAgOC42MDExIDI1Ljk4OSAxNS41NzUgNTguMDQ5IDE1LjU3NXM1OC4wNDktNi45NzQxIDU4LjA0OS0xNS41NzVjMC0zLjQxMDEtNC4wODUtNi41NjQtMTEuMDEyLTkuMTI4OSAwLjA0ODctMC4wMzEyIDAuMDk2Mi0wLjA2MTEgMC4xNDYyNS0wLjA5MjQgNy4wMzYyIDIuNTg2IDExLjE4OCA1Ljc3MzUgMTEuMTg4IDkuMjIxMiAwIDguNjQ4OS0yNi4xMzQgMTUuNjYtNTguMzcgMTUuNjYiIGZpbGw9IiNkZmRlZGMiLz4KICAgPHBhdGggaWQ9InBhdGg0MzczIiBkPSJtNDUxLjI4IDkyNy44M2MtMzIuMDYgMC01OC4wNDktNi45NzQxLTU4LjA0OS0xNS41NzUgMC0zLjQxMDEgNC4wODU1LTYuNTY0IDExLjAxMy05LjEyODkgMC4wNDk3IDAuMDMxMiAwLjA5NzYgMC4wNjE1IDAuMTQ3IDAuMDkyOC02LjgyMTkgMi41NDI0LTEwLjgzOSA1LjY2MzYtMTAuODM5IDkuMDM2MSAwIDguNTUzOCAyNS44NDYgMTUuNDg5IDU3LjcyOCAxNS40ODlzNTcuNzI4LTYuOTM1IDU3LjcyOC0xNS40ODljMC0zLjM3MjUtNC4wMTYyLTYuNDkzNi0xMC44MzktOS4wMzYxIDAuMDUtMC4wMzEyIDAuMDk3NS0wLjA2MTUgMC4xNDc1LTAuMDkyOCA2LjkyNzUgMi41NjQ5IDExLjAxMiA1LjcxODggMTEuMDEyIDkuMTI4OSAwIDguNjAxMS0yNS45OSAxNS41NzUtNTguMDQ5IDE1LjU3NSIgZmlsbD0iI2RlZGRkYiIvPgogICA8cGF0aCBpZD0icGF0aDQzNzUiIGQ9Im00NTEuMjggOTI3Ljc0Yy0zMS44ODMgMC01Ny43MjgtNi45MzUtNTcuNzI4LTE1LjQ4OSAwLTMuMzcyNSA0LjAxNy02LjQ5MzYgMTAuODM5LTkuMDM2MSAwLjA0OTIgMC4wMzEyIDAuMDk4NiAwLjA2MSAwLjE0Nzg4IDAuMDkyMi02LjcxNTIgMi41MjE1LTEwLjY2NiA1LjYwODktMTAuNjY2IDguOTQzOSAwIDguNTA2NCAyNS43MDIgMTUuNDAyIDU3LjQwOCAxNS40MDJzNTcuNDA4LTYuODk2IDU3LjQwOC0xNS40MDJjMC0zLjMzNS0zLjk1MTItNi40MjI0LTEwLjY2Ni04Ljk0MzkgMC4wNDg4LTAuMDMxMiAwLjA5NzUtMC4wNjEgMC4xNDc1LTAuMDkyMiA2LjgyMjUgMi41NDI1IDEwLjgzOSA1LjY2MzYgMTAuODM5IDkuMDM2MSAwIDguNTUzOC0yNS44NDYgMTUuNDg5LTU3LjcyOCAxNS40ODkiIGZpbGw9IiNkZGRjZGEiLz4KICAgPHBhdGggaWQ9InBhdGg0Mzc3IiBkPSJtNDUxLjI4IDkyNy42NWMtMzEuNzA2IDAtNTcuNDA4LTYuODk2LTU3LjQwOC0xNS40MDIgMC0zLjMzNSAzLjk1MDgtNi40MjI0IDEwLjY2Ni04Ljk0MzkgMC4wNDk5IDAuMDMwMyAwLjA5ODYgMC4wNjE1IDAuMTQ4NSAwLjA5MjgtNi42MDk0IDIuNDk4Ni0xMC40OTQgNS41NTM4LTEwLjQ5NCA4Ljg1MTEgMCA4LjQ1OSAyNS41NTkgMTUuMzE2IDU3LjA4NyAxNS4zMTZzNTcuMDg3LTYuODU3NCA1Ny4wODctMTUuMzE2YzAtMy4yOTc0LTMuODgzOC02LjM1MjUtMTAuNDk0LTguODUxMSAwLjA1LTAuMDMxMiAwLjA5ODctMC4wNjI1IDAuMTQ4NzUtMC4wOTI4IDYuNzE1IDIuNTIxNSAxMC42NjYgNS42MDg5IDEwLjY2NiA4Ljk0MzkgMCA4LjUwNjQtMjUuNzAyIDE1LjQwMi01Ny40MDggMTUuNDAyIiBmaWxsPSIjZGRkY2RhIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDM3OSIgZD0ibTQ1MS4yOCA5MjcuNTdjLTMxLjUyOCAwLTU3LjA4Ny02Ljg1NzQtNTcuMDg3LTE1LjMxNiAwLTMuMjk3NCAzLjg4NDgtNi4zNTI1IDEwLjQ5NC04Ljg1MTEgMC4wNDk3IDAuMDI5OSAwLjA5OTYgMC4wNjExIDAuMTQ4ODcgMC4wOTI0LTYuNTAyOSAyLjQ3NzUtMTAuMzIyIDUuNDk5LTEwLjMyMiA4Ljc1ODggMCA4LjQxMTEgMjUuNDE1IDE1LjIzIDU2Ljc2NiAxNS4yM3M1Ni43NjctNi44MTg4IDU2Ljc2Ny0xNS4yM2MwLTMuMjU5OC0zLjgyLTYuMjgxMi0xMC4zMjItOC43NTg4IDAuMDQ4Ny0wLjAzMTIgMC4wOTg3LTAuMDYyNSAwLjE0ODc1LTAuMDkyNCA2LjYxIDIuNDk4NiAxMC40OTQgNS41NTM4IDEwLjQ5NCA4Ljg1MTEgMCA4LjQ1OS0yNS41NTggMTUuMzE2LTU3LjA4NyAxNS4zMTYiIGZpbGw9IiNkY2RiZDkiLz4KICAgPHBhdGggaWQ9InBhdGg0MzgxIiBkPSJtNDUxLjI4IDkyNy40OGMtMzEuMzUxIDAtNTYuNzY2LTYuODE4OC01Ni43NjYtMTUuMjMgMC0zLjI1OTggMy44MTg5LTYuMjgxMiAxMC4zMjItOC43NTg4IDAuMDQ5OSAwLjAzMDMgMC4xMDA2MyAwLjA2MSAwLjE0OTg4IDAuMDkyMy02LjM5NzQgMi40NTUxLTEwLjE1MSA1LjQ0MzktMTAuMTUxIDguNjY2NSAwIDguMzYzOCAyNS4yNzEgMTUuMTQ0IDU2LjQ0NSAxNS4xNDRzNTYuNDQ2LTYuNzc5OCA1Ni40NDYtMTUuMTQ0YzAtMy4yMjI2LTMuNzUzOC02LjIxMTQtMTAuMTUxLTguNjY2NSAwLjA1LTAuMDMxMiAwLjEtMC4wNjIgMC4xNS0wLjA5MjMgNi41MDI1IDIuNDc3NSAxMC4zMjIgNS40OTkgMTAuMzIyIDguNzU4OCAwIDguNDExMS0yNS40MTYgMTUuMjMtNTYuNzY3IDE1LjIzIiBmaWxsPSIjZGJkYWQ4Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNDM4MyIgZD0ibTQ1MS4yOCA5MjcuNGMtMzEuMTc0IDAtNTYuNDQ1LTYuNzc5OC01Ni40NDUtMTUuMTQ0IDAtMy4yMjI2IDMuNzUzNS02LjIxMTQgMTAuMTUxLTguNjY2NSAwLjA0OTkgMC4wMzAzIDAuMTAxMTIgMC4wNjE1IDAuMTUwODcgMC4wOTI4LTYuMjkzNCAyLjQzMjYtOS45ODE0IDUuMzg3Ny05Ljk4MTQgOC41NzM3IDAgOC4zMTY0IDI1LjEyOCAxNS4wNTggNTYuMTI1IDE1LjA1OCAzMC45OTYgMCA1Ni4xMjQtNi43NDEyIDU2LjEyNC0xNS4wNTggMC0zLjE4Ni0zLjY4NzUtNi4xNDExLTkuOTgxMi04LjU3MzggMC4wNS0wLjAzMTIgMC4xMDEyNS0wLjA2MjUgMC4xNTEyNS0wLjA5MjggNi4zOTc1IDIuNDU1MSAxMC4xNTEgNS40NDM5IDEwLjE1MSA4LjY2NjUgMCA4LjM2MzgtMjUuMjcyIDE1LjE0NC01Ni40NDYgMTUuMTQ0IiBmaWxsPSIjZGFkOWQ3Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNDM4NSIgZD0ibTQ1MS4yOCA5MjcuMzFjLTMwLjk5NyAwLTU2LjEyNS02Ljc0MTItNTYuMTI1LTE1LjA1OCAwLTMuMTg2IDMuNjg4LTYuMTQxMSA5Ljk4MTQtOC41NzM4IDAuMDQ5OSAwLjAyOTggMC4xMDE2MyAwLjA2MSAwLjE1MTM4IDAuMDkyMy02LjE4ODUgMi40MTE2LTkuODEyIDUuMzMyNi05LjgxMiA4LjQ4MTUgMCA4LjI2ODUgMjQuOTg0IDE0Ljk3MyA1NS44MDQgMTQuOTczIDMwLjgxOSAwIDU1LjgwNC02LjcwNDIgNTUuODA0LTE0Ljk3MyAwLTMuMTQ4OS0zLjYyMzgtNi4wNjk5LTkuODEyNS04LjQ4MTUgMC4wNS0wLjAzMTIgMC4xMDI1LTAuMDYyNSAwLjE1MTI1LTAuMDkyMyA2LjI5MzggMi40MzI2IDkuOTgxMiA1LjM4NzggOS45ODEyIDguNTczOCAwIDguMzE2NC0yNS4xMjggMTUuMDU4LTU2LjEyNCAxNS4wNTgiIGZpbGw9IiNkOWQ4ZDYiLz4KICAgPHBhdGggaWQ9InBhdGg0Mzg3IiBkPSJtNDUxLjI4IDkyNy4yM2MtMzAuODIgMC01NS44MDQtNi43MDQyLTU1LjgwNC0xNC45NzMgMC0zLjE0ODkgMy42MjM1LTYuMDY5OSA5LjgxMi04LjQ4MTUgMC4wNTIzIDAuMDMxMiAwLjA5OTYgMC4wNjE2IDAuMTUxODcgMC4wOTI5LTYuMDg0NSAyLjM4ODYtOS42NDMgNS4yNzczLTkuNjQzIDguMzg4NiAwIDguMjIxMiAyNC44NDEgMTQuODg2IDU1LjQ4MyAxNC44ODZzNTUuNDgzLTYuNjY1IDU1LjQ4My0xNC44ODZjMC0zLjExMTQtMy41NTg4LTYtOS42NDI1LTguMzg4NiAwLjA1MTItMC4wMzEyIDAuMDk4Ny0wLjA2MTYgMC4xNTEyNS0wLjA5MjkgNi4xODg4IDIuNDExNiA5LjgxMjUgNS4zMzI2IDkuODEyNSA4LjQ4MTUgMCA4LjI2ODUtMjQuOTg1IDE0Ljk3My01NS44MDQgMTQuOTczIiBmaWxsPSIjZDhkN2Q2Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNDM4OSIgZD0ibTQ1MS4yOCA5MjcuMTRjLTMwLjY0MyAwLTU1LjQ4My02LjY2NS01NS40ODMtMTQuODg2IDAtMy4xMTE0IDMuNTU4NS02IDkuNjQzLTguMzg4NiAwLjA1MDMgMC4wMjk4IDAuMTAzIDAuMDYyNSAwLjE1Mjg4IDAuMDkyMy01Ljk4MTUgMi4zNjYzLTkuNDc1MSA1LjIyMTEtOS40NzUxIDguMjk2NCAwIDguMTczOSAyNC42OTcgMTQuOCA1NS4xNjMgMTQuOCAzMC40NjUgMCA1NS4xNjItNi42MjU5IDU1LjE2Mi0xNC44IDAtMy4wNzUyLTMuNDkyNS01LjkzMDEtOS40NzUtOC4yOTY0IDAuMDUtMC4wMjk4IDAuMTAzNzUtMC4wNjI1IDAuMTUzNzUtMC4wOTIzIDYuMDgzOCAyLjM4ODYgOS42NDI1IDUuMjc3MyA5LjY0MjUgOC4zODg2IDAgOC4yMjEyLTI0Ljg0IDE0Ljg4Ni01NS40ODMgMTQuODg2IiBmaWxsPSIjZDdkNmQ1Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNDM5MSIgZD0ibTQ1MS4yOCA5MjcuMDVjLTMwLjQ2NSAwLTU1LjE2My02LjYyNTktNTUuMTYzLTE0LjggMC0zLjA3NTIgMy40OTM2LTUuOTMwMSA5LjQ3NTEtOC4yOTY0IDAuMDUyOCAwLjAzMTIgMC4xMDE1IDAuMDYxNSAwLjE1Mzc1IDAuMDkyOC01Ljg3NzkgMi4zNDM4LTkuMzA4MSA1LjE2Ni05LjMwODEgOC4yMDM2IDAgOC4xMjY1IDI0LjU1MyAxNC43MTQgNTQuODQyIDE0LjcxNCAzMC4yODggMCA1NC44NDItNi41ODc0IDU0Ljg0Mi0xNC43MTQgMC0zLjAzNzYtMy40My01Ljg1OTktOS4zMDg4LTguMjAzNiAwLjA1MjUtMC4wMzEyIDAuMTAxMjUtMC4wNjE1IDAuMTUzNzUtMC4wOTI4IDUuOTgyNSAyLjM2NjMgOS40NzUgNS4yMjExIDkuNDc1IDguMjk2NCAwIDguMTczOS0yNC42OTYgMTQuOC01NS4xNjIgMTQuOCIgZmlsbD0iI2Q2ZDZkNCIvPgogICA8cGF0aCBpZD0icGF0aDQzOTMiIGQ9Im00NTEuMjggOTI2Ljk3Yy0zMC4yODkgMC01NC44NDItNi41ODc0LTU0Ljg0Mi0xNC43MTQgMC0zLjAzNzYgMy40MzAyLTUuODU5OSA5LjMwODEtOC4yMDM2IDAuMDUwNCAwLjAyOTcgMC4xMDQgMC4wNjI1IDAuMTU0MzcgMC4wOTIyLTUuNzc1NCAyLjMyMjctOS4xNDE2IDUuMTExNC05LjE0MTYgOC4xMTE0IDAgOC4wNzg2IDI0LjQxIDE0LjYyNyA1NC41MjEgMTQuNjI3czU0LjUyLTYuNTQ4OCA1NC41Mi0xNC42MjdjMC0zLTMuMzY2Mi01Ljc4ODYtOS4xNDEyLTguMTExNCAwLjA1LTAuMDI5NyAwLjEwMzc1LTAuMDYyNSAwLjE1Mzc1LTAuMDkyMiA1Ljg3ODggMi4zNDM4IDkuMzA4OCA1LjE2NiA5LjMwODggOC4yMDM2IDAgOC4xMjY1LTI0LjU1NCAxNC43MTQtNTQuODQyIDE0LjcxNCIgZmlsbD0iI2Q1ZDVkMyIvPgogICA8cGF0aCBpZD0icGF0aDQzOTUiIGQ9Im00NTEuMjggOTI2Ljg4Yy0zMC4xMTEgMC01NC41MjEtNi41NDg4LTU0LjUyMS0xNC42MjcgMC0zIDMuMzY2Mi01Ljc4ODYgOS4xNDE2LTguMTExNCAwLjA1MjggMC4wMzI4IDAuMTAzIDAuMDYyNSAwLjE1NTI1IDAuMDkzNy01LjY3MjQgMi4yOTg5LTguOTc2MSA1LjA1NTItOC45NzYxIDguMDE3NiAwIDguMDMxMiAyNC4yNjYgMTQuNTQxIDU0LjIgMTQuNTQxczU0LjItNi41MDk4IDU0LjItMTQuNTQxYzAtMi45NjI0LTMuMzAzOC01LjcxODgtOC45NzYyLTguMDE3NiAwLjA1MjUtMC4wMzEyIDAuMTAyNS0wLjA2MSAwLjE1NS0wLjA5MzcgNS43NzUgMi4zMjI3IDkuMTQxMiA1LjExMTQgOS4xNDEyIDguMTExNCAwIDguMDc4Ni0yNC40MSAxNC42MjctNTQuNTIgMTQuNjI3IiBmaWxsPSIjZDRkNGQyIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDM5NyIgZD0ibTQ1MS4yOCA5MjYuNzljLTI5LjkzNCAwLTU0LjItNi41MDk4LTU0LjItMTQuNTQxIDAtMi45NjI0IDMuMzAzOC01LjcxODggOC45NzYxLTguMDE3NiAwLjA1MjggMC4wMzEyIDAuMTAzNSAwLjA2MTYgMC4xNTYyNSAwLjA5MjktNS41NzA5IDIuMjc1OS04LjgxMTUgNC45OTg1LTguODExNSA3LjkyNDggMCA3Ljk4MzkgMjQuMTIyIDE0LjQ1NSA1My44NzkgMTQuNDU1czUzLjg3OS02LjQ3MTIgNTMuODc5LTE0LjQ1NWMwLTIuOTI2Mi0zLjI0MTItNS42NDg5LTguODExMi03LjkyNDggMC4wNTI1LTAuMDMxMiAwLjEwMzc1LTAuMDYxNiAwLjE1NjI1LTAuMDkyOSA1LjY3MjUgMi4yOTg5IDguOTc2MiA1LjA1NTIgOC45NzYyIDguMDE3NiAwIDguMDMxMi0yNC4yNjcgMTQuNTQxLTU0LjIgMTQuNTQxIiBmaWxsPSIjZDRkM2QxIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDM5OSIgZD0ibTQ1MS4yOCA5MjYuNzFjLTI5Ljc1NyAwLTUzLjg3OS02LjQ3MTItNTMuODc5LTE0LjQ1NSAwLTIuOTI2MiAzLjI0MDYtNS42NDg5IDguODExNS03LjkyNDggMC4wNTI4IDAuMDMxMiAwLjEwNCAwLjA2MSAwLjE1Njc1IDAuMDkyMy01LjQ3MDIgMi4yNTM5LTguNjQ3NSA0Ljk0MzktOC42NDc1IDcuODMyNSAwIDcuOTM2IDIzLjk3OSAxNC4zNyA1My41NTkgMTQuMzdzNTMuNTU4LTYuNDM0MSA1My41NTgtMTQuMzdjMC0yLjg4ODYtMy4xNzc1LTUuNTc4Ni04LjY0NzUtNy44MzI1IDAuMDUzOC0wLjAzMTIgMC4xMDUtMC4wNjEgMC4xNTc1LTAuMDkyMyA1LjU3IDIuMjc1OSA4LjgxMTIgNC45OTg1IDguODExMiA3LjkyNDggMCA3Ljk4MzktMjQuMTIyIDE0LjQ1NS01My44NzkgMTQuNDU1IiBmaWxsPSIjZDNkMmQwIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDQwMSIgZD0ibTQ1MS4yOCA5MjYuNjJjLTI5LjU4IDAtNTMuNTU5LTYuNDM0MS01My41NTktMTQuMzcgMC0yLjg4ODYgMy4xNzcyLTUuNTc4NiA4LjY0NzUtNy44MzI1IDAuMDUwMyAwLjAyOTcgMC4xMDY4OCAwLjA2NCAwLjE1NzYzIDAuMDkzNy01LjM2ODYgMi4yMy04LjQ4NDQgNC44ODYyLTguNDg0NCA3LjczODggMCA3Ljg4ODYgMjMuODM2IDE0LjI4NCA1My4yMzggMTQuMjg0czUzLjIzOC02LjM5NTEgNTMuMjM4LTE0LjI4NGMwLTIuODUyNS0zLjExNjItNS41MDg4LTguNDg1LTcuNzM4OCAwLjA1MTItMC4wMjk4IDAuMTA3NS0wLjA2NCAwLjE1NzUtMC4wOTM3IDUuNDcgMi4yNTM5IDguNjQ3NSA0Ljk0MzkgOC42NDc1IDcuODMyNSAwIDcuOTM2LTIzLjk3OCAxNC4zNy01My41NTggMTQuMzciIGZpbGw9IiNkMmQxY2YiLz4KICAgPHBhdGggaWQ9InBhdGg0NDAzIiBkPSJtNDUxLjI4IDkyNi41NGMtMjkuNDAyIDAtNTMuMjM4LTYuMzk1MS01My4yMzgtMTQuMjg0IDAtMi44NTI1IDMuMTE1OC01LjUwODggOC40ODQ0LTcuNzM4OCAwLjA1MjggMC4wMzEyIDAuMTA1NSAwLjA2MjUgMC4xNTgyNSAwLjA5MjItNS4yNjg1IDIuMjA3NS04LjMyMTggNC44MzE1LTguMzIxOCA3LjY0NjUgMCA3Ljg0MTIgMjMuNjkxIDE0LjE5NyA1Mi45MTcgMTQuMTk3IDI5LjIyNSAwIDUyLjkxNy02LjM1NiA1Mi45MTctMTQuMTk3IDAtMi44MTUtMy4wNTM4LTUuNDM5LTguMzIxMi03LjY0NjUgMC4wNTI1LTAuMDMxMiAwLjEwNS0wLjA2MSAwLjE1NzUtMC4wOTIyIDUuMzY4OCAyLjIzIDguNDg1IDQuODg2MiA4LjQ4NSA3LjczODggMCA3Ljg4ODYtMjMuODM2IDE0LjI4NC01My4yMzggMTQuMjg0IiBmaWxsPSIjZDFkMGNmIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDQwNSIgZD0ibTQ1MS4yOCA5MjYuNDVjLTI5LjIyNiAwLTUyLjkxNy02LjM1Ni01Mi45MTctMTQuMTk3IDAtMi44MTUgMy4wNTMyLTUuNDM5IDguMzIxOC03LjY0NjUgMC4wNTUxIDAuMDMyOCAwLjEwNCAwLjA2MTUgMC4xNTkyNSAwLjA5MzctNS4xNjggMi4xODQxLTguMTYwOCA0Ljc3NC04LjE2MDggNy41NTI4IDAgNy43OTQgMjMuNTQ5IDE0LjExMSA1Mi41OTcgMTQuMTExczUyLjU5Ny02LjMxNzMgNTIuNTk3LTE0LjExMWMwLTIuNzc4OC0yLjk5MjUtNS4zNjg2LTguMTYxMi03LjU1MjggMC4wNTUtMC4wMzIyIDAuMTAzNzUtMC4wNjEgMC4xNi0wLjA5MzcgNS4yNjc1IDIuMjA3NSA4LjMyMTIgNC44MzE1IDguMzIxMiA3LjY0NjUgMCA3Ljg0MTItMjMuNjkyIDE0LjE5Ny01Mi45MTcgMTQuMTk3IiBmaWxsPSIjZDBjZmNkIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDQwNyIgZD0ibTQ1MS4yOCA5MjYuMzZjLTI5LjA0OCAwLTUyLjU5Ny02LjMxNzMtNTIuNTk3LTE0LjExMSAwLTIuNzc4OCAyLjk5MjgtNS4zNjg2IDguMTYwOC03LjU1MjggMC4wNTMxIDAuMDMwMyAwLjEwNzM3IDAuMDYyNSAwLjE2MDEyIDAuMDkyOC01LjA2ODQgMi4xNjExLTcuOTk5NSA0LjcxODgtNy45OTk1IDcuNDYgMCA3Ljc0NjEgMjMuNDA0IDE0LjAyNSA1Mi4yNzUgMTQuMDI1czUyLjI3Ni02LjI3ODggNTIuMjc2LTE0LjAyNWMwLTIuNzQxMi0yLjkzMTItNS4yOTg5LTgtNy40NiAwLjA1MjUtMC4wMzAzIDAuMTA3NS0wLjA2MjUgMC4xNi0wLjA5MjggNS4xNjg4IDIuMTg0MSA4LjE2MTIgNC43NzQgOC4xNjEyIDcuNTUyOCAwIDcuNzk0LTIzLjU0OSAxNC4xMTEtNTIuNTk3IDE0LjExMSIgZmlsbD0iI2NmY2VjYyIvPgogICA8cGF0aCBpZD0icGF0aDQ0MDkiIGQ9Im00NTEuMjggOTI2LjI4Yy0yOC44NzEgMC01Mi4yNzUtNi4yNzg4LTUyLjI3NS0xNC4wMjUgMC0yLjc0MTIgMi45MzExLTUuMjk4OSA3Ljk5OTUtNy40NiAwLjA1MzIgMC4wMzEyIDAuMTA4MzggMC4wNjI1IDAuMTYxMTMgMC4wOTM3LTQuOTY4OCAyLjEzNzItNy44NDA0IDQuNjYxMS03Ljg0MDQgNy4zNjYyIDAgNy42OTg4IDIzLjI2MSAxMy45NCA1MS45NTUgMTMuOTRzNTEuOTU0LTYuMjQxMiA1MS45NTQtMTMuOTRjMC0yLjcwNTEtMi44NzEyLTUuMjI5LTcuODQtNy4zNjYyIDAuMDUzOC0wLjAzMTIgMC4xMDg3NS0wLjA2MjUgMC4xNjEyNS0wLjA5MzcgNS4wNjg4IDIuMTYxMSA4IDQuNzE4OCA4IDcuNDYgMCA3Ljc0NjEtMjMuNDA0IDE0LjAyNS01Mi4yNzYgMTQuMDI1IiBmaWxsPSIjY2VjZGNiIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDQxMSIgZD0ibTQ1MS4yOCA5MjYuMTljLTI4LjY5NCAwLTUxLjk1NS02LjI0MTItNTEuOTU1LTEzLjk0IDAtMi43MDUxIDIuODcxNi01LjIyOSA3Ljg0MDQtNy4zNjYyIDAuMDUzMiAwLjAyOTkgMC4xMDg4NyAwLjA2MjUgMC4xNjIxMyAwLjA5MjQtNC44NzA2IDIuMTEzOC03LjY4MTYgNC42MDY0LTcuNjgxNiA3LjI3MzkgMCA3LjY1MTQgMjMuMTE4IDEzLjg1NCA1MS42MzQgMTMuODU0IDI4LjUxNyAwIDUxLjYzNC02LjIwMjEgNTEuNjM0LTEzLjg1NCAwLTIuNjY3NS0yLjgxMTItNS4xNjAxLTcuNjgxMi03LjI3MzkgMC4wNTI1LTAuMDI5OSAwLjEwODc1LTAuMDYyNSAwLjE2MTI1LTAuMDkyNCA0Ljk2ODggMi4xMzcyIDcuODQgNC42NjExIDcuODQgNy4zNjYyIDAgNy42OTg4LTIzLjI2IDEzLjk0LTUxLjk1NCAxMy45NCIgZmlsbD0iI2NkY2NjYSIvPgogICA8cGF0aCBpZD0icGF0aDQ0MTMiIGQ9Im00NTEuMjggOTI2LjExYy0yOC41MTcgMC01MS42MzQtNi4yMDIxLTUxLjYzNC0xMy44NTQgMC0yLjY2NzUgMi44MTEtNS4xNjAxIDcuNjgxNi03LjI3MzkgMC4wNTUxIDAuMDMyNiAwLjEwNjg3IDAuMDYxNSAwLjE2MjQ5IDAuMDkzNy00Ljc3MjkgMi4wOTAyLTcuNTIyOSA0LjU0ODctNy41MjI5IDcuMTgwMSAwIDcuNjAzNSAyMi45NzQgMTMuNzY4IDUxLjMxMyAxMy43NjhzNTEuMzEzLTYuMTY0MSA1MS4zMTMtMTMuNzY4YzAtMi42MzE0LTIuNzUtNS4wODk5LTcuNTIzOC03LjE4MDEgMC4wNTYzLTAuMDMyMyAwLjEwNzUtMC4wNjExIDAuMTYzNzUtMC4wOTM3IDQuODcgMi4xMTM4IDcuNjgxMiA0LjYwNjQgNy42ODEyIDcuMjczOSAwIDcuNjUxNC0yMy4xMTggMTMuODU0LTUxLjYzNCAxMy44NTQiIGZpbGw9IiNjY2NiYzkiLz4KICAgPHBhdGggaWQ9InBhdGg0NDE1IiBkPSJtNDUxLjI4IDkyNi4wMmMtMjguMzM5IDAtNTEuMzEzLTYuMTY0MS01MS4zMTMtMTMuNzY4IDAtMi42MzE0IDIuNzUtNS4wODk5IDcuNTIyOS03LjE4MDEgMC4wNTMyIDAuMDMwMiAwLjExMDM4IDAuMDYyNSAwLjE2MzYzIDAuMDkyNy00LjY3NTIgMi4wNjc0LTcuMzY2MiA0LjQ5MjEtNy4zNjYyIDcuMDg3NCAwIDcuNTU2MSAyMi44MyAxMy42ODEgNTAuOTkzIDEzLjY4MXM1MC45OTMtNi4xMjUgNTAuOTkzLTEzLjY4MWMwLTIuNTk1Mi0yLjY5MTItNS4wMi03LjM2NjItNy4wODc0IDAuMDUyNS0wLjAzMDIgMC4xMS0wLjA2MjUgMC4xNjI1LTAuMDkyNyA0Ljc3MzggMi4wOTAyIDcuNTIzOCA0LjU0ODcgNy41MjM4IDcuMTgwMSAwIDcuNjAzNS0yMi45NzQgMTMuNzY4LTUxLjMxMyAxMy43NjgiIGZpbGw9IiNjYmM5YzgiLz4KICAgPHBhdGggaWQ9InBhdGg0NDE3IiBkPSJtNDUxLjI4IDkyNS45M2MtMjguMTYzIDAtNTAuOTkzLTYuMTI1LTUwLjk5My0xMy42ODEgMC0yLjU5NTIgMi42OTEtNS4wMiA3LjM2NjItNy4wODc0IDAuMDU1NiAwLjAzMjMgMC4xMDg4NyAwLjA2MjUgMC4xNjQ1IDAuMDkzNy00LjU3NzEgMi4wNDM0LTcuMjA5OSA0LjQzNi03LjIwOTkgNi45OTM2IDAgNy41MDg4IDIyLjY4NiAxMy41OTUgNTAuNjcyIDEzLjU5NSAyNy45ODUgMCA1MC42NzItNi4wODY1IDUwLjY3Mi0xMy41OTUgMC0yLjU1NzYtMi42MzI1LTQuOTUwMi03LjIxLTYuOTkzNiAwLjA1NS0wLjAzMTIgMC4xMDg3NS0wLjA2MTUgMC4xNjUtMC4wOTM3IDQuNjc1IDIuMDY3NCA3LjM2NjIgNC40OTIxIDcuMzY2MiA3LjA4NzQgMCA3LjU1NjEtMjIuODMgMTMuNjgxLTUwLjk5MyAxMy42ODEiIGZpbGw9IiNjYWM4YzciLz4KICAgPHBhdGggaWQ9InBhdGg0NDE5IiBkPSJtNDUxLjI4IDkyNS44NWMtMjcuOTg1IDAtNTAuNjcyLTYuMDg2NS01MC42NzItMTMuNTk1IDAtMi41NTc2IDIuNjMyOC00Ljk1MDIgNy4yMDk5LTYuOTkzNiAwLjA1NjMgMC4wMzEyIDAuMTA5ODcgMC4wNjI1IDAuMTY1NjIgMC4wOTM3LTQuNDgwNSAyLjAyLTcuMDU0OCA0LjM3ODQtNy4wNTQ4IDYuODk5OSAwIDcuNDYxNCAyMi41NDMgMTMuNTA5IDUwLjM1MSAxMy41MDlzNTAuMzUtNi4wNDc0IDUwLjM1LTEzLjUwOWMwLTIuNTIxNS0yLjU3MzgtNC44Nzk5LTcuMDU1LTYuODk5OSAwLjA1NjMtMC4wMzEyIDAuMTEtMC4wNjI1IDAuMTY2MjUtMC4wOTM3IDQuNTc3NSAyLjA0MzQgNy4yMSA0LjQzNiA3LjIxIDYuOTkzNiAwIDcuNTA4OC0yMi42ODYgMTMuNTk1LTUwLjY3MiAxMy41OTUiIGZpbGw9IiNjOWM3YzYiLz4KICAgPHBhdGggaWQ9InBhdGg0NDIxIiBkPSJtNDUxLjI4IDkyNS43NmMtMjcuODA4IDAtNTAuMzUxLTYuMDQ3NC01MC4zNTEtMTMuNTA5IDAtMi41MjE1IDIuNTc0Mi00Ljg3OTkgNy4wNTQ4LTYuODk5OSAwLjA1NjEgMC4wMzEyIDAuMTEwNzUgMC4wNjI1IDAuMTY2NSAwLjA5MzctNC4zODUyIDEuOTk0Ni02LjkwMDQgNC4zMjIyLTYuOTAwNCA2LjgwNjEgMCA3LjQxMzYgMjIuMzk5IDEzLjQyMiA1MC4wMyAxMy40MjJzNTAuMDMtNi4wMDg3IDUwLjAzLTEzLjQyMmMwLTIuNDgzOS0yLjUxNS00LjgxMTUtNi45MDEyLTYuODA2MSAwLjA1NjMtMC4wMzEyIDAuMTExMjUtMC4wNjI1IDAuMTY2MjUtMC4wOTM3IDQuNDgxMiAyLjAyIDcuMDU1IDQuMzc4NCA3LjA1NSA2Ljg5OTkgMCA3LjQ2MTQtMjIuNTQyIDEzLjUwOS01MC4zNSAxMy41MDkiIGZpbGw9IiNjOGM2YzUiLz4KICAgPHBhdGggaWQ9InBhdGg0NDIzIiBkPSJtNDUxLjI4IDkyNS42N2MtMjcuNjMxIDAtNTAuMDMtNi4wMDg3LTUwLjAzLTEzLjQyMiAwLTIuNDgzOSAyLjUxNTEtNC44MTE1IDYuOTAwNC02LjgwNjEgMC4wNTYxIDAuMDMxMiAwLjExMTI1IDAuMDYyNSAwLjE2NzUgMC4wOTM3LTQuMjkwMSAxLjk3MTEtNi43NDcxIDQuMjY0Ni02Ljc0NzEgNi43MTI0IDAgNy4zNjYyIDIyLjI1NiAxMy4zMzcgNDkuNzEgMTMuMzM3czQ5LjcwOS01Ljk3MTIgNDkuNzA5LTEzLjMzN2MwLTIuNDQ3OC0yLjQ1NzUtNC43NDEyLTYuNzQ3NS02LjcxMjQgMC4wNTYzLTAuMDMxMiAwLjExMjUtMC4wNjI1IDAuMTY3NS0wLjA5MzcgNC4zODYyIDEuOTk0NiA2LjkwMTIgNC4zMjIyIDYuOTAxMiA2LjgwNjEgMCA3LjQxMzYtMjIuNCAxMy40MjItNTAuMDMgMTMuNDIyIiBmaWxsPSIjYzdjNWMzIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDQyNSIgZD0ibTQ1MS4yOCA5MjUuNTljLTI3LjQ1NCAwLTQ5LjcxLTUuOTcxMi00OS43MS0xMy4zMzcgMC0yLjQ0NzggMi40NTctNC43NDEyIDYuNzQ3MS02LjcxMjQgMC4wNTYxIDAuMDMxMiAwLjExMjI1IDAuMDYyNSAwLjE2ODM4IDAuMDkyMi00LjE5NDIgMS45NDg4LTYuNTk0OCA0LjIwOS02LjU5NDggNi42MjAxIDAgNy4zMTg5IDIyLjExMiAxMy4yNTEgNDkuMzg5IDEzLjI1MSAyNy4yNzYgMCA0OS4zODktNS45MzI2IDQ5LjM4OS0xMy4yNTEgMC0yLjQxMTEtMi40MDEyLTQuNjcxNC02LjU5NS02LjYyMDEgMC4wNTYzLTAuMDI5NyAwLjExMjUtMC4wNjEgMC4xNjc1LTAuMDkyMiA0LjI5IDEuOTcxMSA2Ljc0NzUgNC4yNjQ2IDYuNzQ3NSA2LjcxMjQgMCA3LjM2NjItMjIuMjU2IDEzLjMzNy00OS43MDkgMTMuMzM3IiBmaWxsPSIjYzZjNGMyIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDQyNyIgZD0ibTQ1MS4yOCA5MjUuNWMtMjcuMjc3IDAtNDkuMzg5LTUuOTMyNi00OS4zODktMTMuMjUxIDAtMi40MTExIDIuNDAwNS00LjY3MTQgNi41OTQ4LTYuNjIwMSAwLjA1NjEgMC4wMzEyIDAuMTEzMjUgMC4wNjQgMC4xNjk1IDAuMDkzNy00LjEwMDEgMS45MjM5LTYuNDQzNCA0LjE1MTQtNi40NDM0IDYuNTI2NCAwIDcuMjcxIDIxLjk2OCAxMy4xNjUgNDkuMDY4IDEzLjE2NSAyNy4wOTkgMCA0OS4wNjgtNS44OTQgNDkuMDY4LTEzLjE2NSAwLTIuMzc1LTIuMzQzOC00LjYwMjUtNi40NDM4LTYuNTI2NCAwLjA1NjItMC4wMjk3IDAuMTEzNzUtMC4wNjI1IDAuMTctMC4wOTM3IDQuMTkzOCAxLjk0ODggNi41OTUgNC4yMDkgNi41OTUgNi42MjAxIDAgNy4zMTg5LTIyLjExMyAxMy4yNTEtNDkuMzg5IDEzLjI1MSIgZmlsbD0iI2M0YzNjMSIvPgogICA8cGF0aCBpZD0icGF0aDQ0MjkiIGQ9Im00NTEuMjggOTI1LjQyYy0yNy4xIDAtNDkuMDY4LTUuODk0LTQ5LjA2OC0xMy4xNjUgMC0yLjM3NSAyLjM0MzItNC42MDI1IDYuNDQzNC02LjUyNjQgMC4wNTYxIDAuMDMxMiAwLjExMzc1IDAuMDY0IDAuMTY5ODcgMC4wOTM3LTQuMDA1NCAxLjg5OTktNi4yOTI1IDQuMDk1My02LjI5MjUgNi40MzI2IDAgNy4yMjM2IDIxLjgyNSAxMy4wNzkgNDguNzQ3IDEzLjA3OXM0OC43NDctNS44NTUgNDguNzQ3LTEzLjA3OWMwLTIuMzM3NC0yLjI4NzUtNC41MzI4LTYuMjkyNS02LjQzMjYgMC4wNTYzLTAuMDI5NyAwLjExMzc1LTAuMDYyNSAwLjE3LTAuMDkzNyA0LjEgMS45MjM5IDYuNDQzOCA0LjE1MTQgNi40NDM4IDYuNTI2NCAwIDcuMjcxLTIxLjk2OSAxMy4xNjUtNDkuMDY4IDEzLjE2NSIgZmlsbD0iI2MzYzJjMCIvPgogICA8cGF0aCBpZD0icGF0aDQ0MzEiIGQ9Im00NTEuMjggOTI1LjMzYy0yNi45MjIgMC00OC43NDctNS44NTUtNDguNzQ3LTEzLjA3OSAwLTIuMzM3NCAyLjI4NzEtNC41MzI4IDYuMjkyNS02LjQzMjYgMC4wNTY2IDAuMDMxMiAwLjExNTI1IDAuMDY0IDAuMTcxMzggMC4wOTM3LTMuOTEyNiAxLjg3NS02LjE0MzUgNC4wMzc2LTYuMTQzNSA2LjMzODkgMCA3LjE3NjIgMjEuNjgyIDEyLjk5MyA0OC40MjcgMTIuOTkzczQ4LjQyNy01LjgxNjQgNDguNDI3LTEyLjk5M2MwLTIuMzAxMi0yLjIzMTItNC40NjM5LTYuMTQzOC02LjMzODkgMC4wNTYzLTAuMDI5NyAwLjExNS0wLjA2MjUgMC4xNzEyNS0wLjA5MzcgNC4wMDUgMS44OTk5IDYuMjkyNSA0LjA5NTMgNi4yOTI1IDYuNDMyNiAwIDcuMjIzNi0yMS44MjQgMTMuMDc5LTQ4Ljc0NyAxMy4wNzkiIGZpbGw9IiNjMmMxYmYiLz4KICAgPHBhdGggaWQ9InBhdGg0NDMzIiBkPSJtNDUxLjI4IDkyNS4yNWMtMjYuNzQ1IDAtNDguNDI3LTUuODE2NC00OC40MjctMTIuOTkzIDAtMi4zMDEyIDIuMjMwOS00LjQ2MzkgNi4xNDM1LTYuMzM4OSAwLjA1OTEgMC4wMzI4IDAuMTEzNzUgMC4wNjI1IDAuMTcyODcgMC4wOTUzLTMuODIwOCAxLjg1MDEtNS45OTU2IDMuOTc4NS01Ljk5NTYgNi4yNDM2IDAgNy4xMjg5IDIxLjUzOCAxMi45MDYgNDguMTA2IDEyLjkwNnM0OC4xMDYtNS43Nzc0IDQ4LjEwNi0xMi45MDZjMC0yLjI2NTEtMi4xNzM4LTQuMzkzNS01Ljk5NS02LjI0MzYgMC4wNTg4LTAuMDMyOCAwLjExMzc1LTAuMDYyNSAwLjE3MjUtMC4wOTUzIDMuOTEyNSAxLjg3NSA2LjE0MzggNC4wMzc2IDYuMTQzOCA2LjMzODkgMCA3LjE3NjItMjEuNjgyIDEyLjk5My00OC40MjcgMTIuOTkzIiBmaWxsPSIjYzFjMGJlIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDQzNSIgZD0ibTQ1MS4yOCA5MjUuMTZjLTI2LjU2OCAwLTQ4LjEwNi01Ljc3NzQtNDguMTA2LTEyLjkwNiAwLTIuMjY1MSAyLjE3NDktNC4zOTM1IDUuOTk1Ni02LjI0MzYgMC4wNTYxIDAuMDI5NyAwLjExNjc1IDAuMDYyNSAwLjE3MzM4IDAuMDkzNy0zLjcyODEgMS44MjQ2LTUuODQ3OCAzLjkyMTQtNS44NDc4IDYuMTQ5OSAwIDcuMDgxIDIxLjM5NCAxMi44MiA0Ny43ODUgMTIuODJzNDcuNzg0LTUuNzM4OCA0Ny43ODQtMTIuODJjMC0yLjIyODUtMi4xMi00LjMyNTItNS44NDc1LTYuMTQ5OSAwLjA1NjItMC4wMzEyIDAuMTE3NS0wLjA2NCAwLjE3Mzc1LTAuMDkzNyAzLjgyMTIgMS44NTAxIDUuOTk1IDMuOTc4NSA1Ljk5NSA2LjI0MzYgMCA3LjEyODktMjEuNTM4IDEyLjkwNi00OC4xMDYgMTIuOTA2IiBmaWxsPSIjYzBiZmJkIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDQzNyIgZD0ibTQ1MS4yOCA5MjUuMDdjLTI2LjM5MSAwLTQ3Ljc4NS01LjczODgtNDcuNzg1LTEyLjgyIDAtMi4yMjg1IDIuMTE5Ni00LjMyNTIgNS44NDc4LTYuMTQ5OSAwLjA1ODUgMC4wMzEyIDAuMTE1MjUgMC4wNjI1IDAuMTc0MjUgMC4wOTM3LTMuNjM2MiAxLjgwMTItNS43MDE2IDMuODYzOC01LjcwMTYgNi4wNTYxIDAgNy4wMzI4IDIxLjI1MSAxMi43MzUgNDcuNDY0IDEyLjczNSAyNi4yMTQgMCA0Ny40NjQtNS43MDIxIDQ3LjQ2NC0xMi43MzUgMC0yLjE5MjQtMi4wNjYyLTQuMjU0OS01LjcwMTItNi4wNTYxIDAuMDU4OC0wLjAzMTIgMC4xMTUtMC4wNjI1IDAuMTczNzUtMC4wOTM3IDMuNzI3NSAxLjgyNDYgNS44NDc1IDMuOTIxNCA1Ljg0NzUgNi4xNDk5IDAgNy4wODEtMjEuMzk0IDEyLjgyLTQ3Ljc4NCAxMi44MiIgZmlsbD0iI2JmYmViYyIvPgogICA8cGF0aCBpZD0icGF0aDQ0MzkiIGQ9Im00NTEuMjggOTI0Ljk5Yy0yNi4yMTMgMC00Ny40NjQtNS43MDIxLTQ3LjQ2NC0xMi43MzUgMC0yLjE5MjQgMi4wNjU0LTQuMjU0OSA1LjcwMTYtNi4wNTYxIDAuMDU5MSAwLjAzMTIgMC4xMTY3NSAwLjA2MjUgMC4xNzUzNyAwLjA5MzctMy41NDYgMS43NzU5LTUuNTU2MiAzLjgwNjEtNS41NTYyIDUuOTYyNCAwIDYuOTg0OSAyMS4xMDcgMTIuNjQ5IDQ3LjE0NCAxMi42NDlzNDcuMTQzLTUuNjY0IDQ3LjE0My0xMi42NDljMC0yLjE1NjItMi4wMS00LjE4NjUtNS41NTYyLTUuOTYyNCAwLjA1ODgtMC4wMzEyIDAuMTE2MjUtMC4wNjI1IDAuMTc2MjUtMC4wOTM3IDMuNjM1IDEuODAxMiA1LjcwMTIgMy44NjM4IDUuNzAxMiA2LjA1NjEgMCA3LjAzMjgtMjEuMjUgMTIuNzM1LTQ3LjQ2NCAxMi43MzUiIGZpbGw9IiNiZWJkYmIiLz4KICAgPHBhdGggaWQ9InBhdGg0NDQxIiBkPSJtNDUxLjI4IDkyNC45Yy0yNi4wMzcgMC00Ny4xNDQtNS42NjQtNDcuMTQ0LTEyLjY0OSAwLTIuMTU2MiAyLjAxMDItNC4xODY1IDUuNTU2Mi01Ljk2MjQgMC4wNTkgMC4wMzEyIDAuMTE3NjMgMC4wNjI1IDAuMTc2NzUgMC4wOTM3LTMuNDU1NiAxLjc1MjQtNS40MTIxIDMuNzQ4NS01LjQxMjEgNS44Njg2IDAgNi45Mzc1IDIwLjk2MyAxMi41NjIgNDYuODIzIDEyLjU2MiAyNS44NTkgMCA0Ni44MjMtNS42MjUgNDYuODIzLTEyLjU2MiAwLTIuMTIwMS0xLjk1NzUtNC4xMTYyLTUuNDEyNS01Ljg2ODYgMC4wNTg4LTAuMDMxMiAwLjExNzUtMC4wNjI1IDAuMTc2MjUtMC4wOTM3IDMuNTQ2MiAxLjc3NTkgNS41NTYyIDMuODA2MSA1LjU1NjIgNS45NjI0IDAgNi45ODQ5LTIxLjEwNiAxMi42NDktNDcuMTQzIDEyLjY0OSIgZmlsbD0iI2JkYmNiYSIvPgogICA8cGF0aCBpZD0icGF0aDQ0NDMiIGQ9Im00NTEuMjggOTI0LjgxYy0yNS44NTkgMC00Ni44MjMtNS42MjUtNDYuODIzLTEyLjU2MiAwLTIuMTIwMSAxLjk1NjUtNC4xMTYyIDUuNDEyMS01Ljg2ODYgMC4wNTkgMC4wMzIzIDAuMTE4NjMgMC4wNjM1IDAuMTc3NzUgMC4wOTQ4LTMuMzY1OCAxLjcyNjUtNS4yNjkxIDMuNjg5OS01LjI2OTEgNS43NzM5IDAgNi44OTAxIDIwLjgyIDEyLjQ3NiA0Ni41MDIgMTIuNDc2czQ2LjUwMi01LjU4NiA0Ni41MDItMTIuNDc2YzAtMi4wODQtMS45MDM4LTQuMDQ3NC01LjI2ODgtNS43NzM5IDAuMDU4OC0wLjAzMTIgMC4xMTg3NS0wLjA2MjUgMC4xNzc1LTAuMDk0OCAzLjQ1NSAxLjc1MjQgNS40MTI1IDMuNzQ4NSA1LjQxMjUgNS44Njg2IDAgNi45Mzc1LTIwLjk2NCAxMi41NjItNDYuODIzIDEyLjU2MiIgZmlsbD0iI2JjYmJiOSIvPgogICA8cGF0aCBpZD0icGF0aDQ0NDUiIGQ9Im00NTEuMjggOTI0LjczYy0yNS42ODIgMC00Ni41MDItNS41ODYtNDYuNTAyLTEyLjQ3NiAwLTIuMDg0IDEuOTAzNC00LjA0NzQgNS4yNjkxLTUuNzczOSAwLjA1OTUgMC4wMzEyIDAuMTE5NjMgMC4wNjI1IDAuMTc5MTMgMC4wOTM3LTMuMjc3NCAxLjcwMTEtNS4xMjc0IDMuNjMyOC01LjEyNzQgNS42ODAxIDAgNi44NDIyIDIwLjY3NiAxMi4zOSA0Ni4xODEgMTIuMzlzNDYuMTgtNS41NDc5IDQ2LjE4LTEyLjM5YzAtMi4wNDc0LTEuODUtMy45NzktNS4xMjc1LTUuNjgwMSAwLjA2LTAuMDMxMiAwLjEyLTAuMDYyNSAwLjE4LTAuMDkzNyAzLjM2NSAxLjcyNjUgNS4yNjg4IDMuNjg5OSA1LjI2ODggNS43NzM5IDAgNi44OTAxLTIwLjgyIDEyLjQ3Ni00Ni41MDIgMTIuNDc2IiBmaWxsPSIjYmJiYWI4Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNDQ0NyIgZD0ibTQ1MS4yOCA5MjQuNjRjLTI1LjUwNSAwLTQ2LjE4MS01LjU0NzktNDYuMTgxLTEyLjM5IDAtMi4wNDc0IDEuODUtMy45NzkgNS4xMjc0LTUuNjgwMSAwLjA1OTEgMC4wMzEyIDAuMTIwNjIgMC4wNjM5IDAuMTc5NzUgMC4wOTUxLTMuMTg5IDEuNjc2Mi00Ljk4NjQgMy41NzM4LTQuOTg2NCA1LjU4NSAwIDYuNzk0OSAyMC41MzIgMTIuMzA0IDQ1Ljg2IDEyLjMwNHM0NS44Ni01LjUwODggNDUuODYtMTIuMzA0YzAtMi4wMTEyLTEuNzk3NS0zLjkwODgtNC45ODYyLTUuNTg1IDAuMDU4OC0wLjAzMTIgMC4xMi0wLjA2MzkgMC4xNzg3NS0wLjA5NTEgMy4yNzc1IDEuNzAxMSA1LjEyNzUgMy42MzI4IDUuMTI3NSA1LjY4MDEgMCA2Ljg0MjItMjAuNjc2IDEyLjM5LTQ2LjE4IDEyLjM5IiBmaWxsPSIjYmFiOWI3Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNDQ0OSIgZD0ibTQ1MS4yOCA5MjQuNTZjLTI1LjMyOCAwLTQ1Ljg2LTUuNTA4OC00NS44Ni0xMi4zMDQgMC0yLjAxMTIgMS43OTc0LTMuOTA4OCA0Ljk4NjQtNS41ODUgMC4wNjIgMC4wMzI3IDAuMTE5NjIgMC4wNjI1IDAuMTgxMTIgMC4wOTM3LTMuMTAwNiAxLjY1MTQtNC44NDY4IDMuNTE2MS00Ljg0NjggNS40OTEyIDAgNi43NDc1IDIwLjM4OSAxMi4yMTkgNDUuNTQgMTIuMjE5czQ1LjUzOS01LjQ3MTIgNDUuNTM5LTEyLjIxOWMwLTEuOTc1MS0xLjc0NjItMy44Mzk5LTQuODQ2Mi01LjQ5MTIgMC4wNjEzLTAuMDMxMiAwLjExODc1LTAuMDYxIDAuMTgxMjUtMC4wOTM3IDMuMTg4OCAxLjY3NjIgNC45ODYyIDMuNTczOCA0Ljk4NjIgNS41ODUgMCA2Ljc5NDktMjAuNTMyIDEyLjMwNC00NS44NiAxMi4zMDQiIGZpbGw9IiNiOWI4YjYiLz4KICAgPHBhdGggaWQ9InBhdGg0NDUxIiBkPSJtNDUxLjI4IDkyNC40N2MtMjUuMTUxIDAtNDUuNTQtNS40NzEyLTQ1LjU0LTEyLjIxOSAwLTEuOTc1MSAxLjc0NjEtMy44Mzk5IDQuODQ2OC01LjQ5MTIgMC4wNTk1IDAuMDMxMiAwLjEyMyAwLjA2MzUgMC4xODI2MyAwLjA5NTItMy4wMTMyIDEuNjI0NS00LjcwODUgMy40NTctNC43MDg1IDUuMzk2IDAgNi43MDAyIDIwLjI0NSAxMi4xMzIgNDUuMjE5IDEyLjEzMnM0NS4yMTgtNS40MzIxIDQ1LjIxOC0xMi4xMzJjMC0xLjkzOS0xLjY5NS0zLjc3MTUtNC43MDc1LTUuMzk2IDAuMDU4OC0wLjAzMTcgMC4xMjI1LTAuMDY0IDAuMTgyNS0wLjA5NTIgMy4xIDEuNjUxNCA0Ljg0NjIgMy41MTYxIDQuODQ2MiA1LjQ5MTIgMCA2Ljc0NzUtMjAuMzg4IDEyLjIxOS00NS41MzkgMTIuMjE5IiBmaWxsPSIjYjdiN2I1Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNDQ1MyIgZD0ibTQ1MS4yOCA5MjQuMzhjLTI0Ljk3NCAwLTQ1LjIxOS01LjQzMjEtNDUuMjE5LTEyLjEzMiAwLTEuOTM5IDEuNjk1Mi0zLjc3MTUgNC43MDg1LTUuMzk2IDAuMDYyIDAuMDMyMyAwLjEyMTUgMC4wNjIgMC4xODM2MiAwLjA5NDgtMi45MjcyIDEuNTk4Ni00LjU3MTQgMy4zOTg5LTQuNTcxNCA1LjMwMTIgMCA2LjY1MjQgMjAuMTAyIDEyLjA0NiA0NC44OTggMTIuMDQ2czQ0Ljg5OC01LjM5NCA0NC44OTgtMTIuMDQ2YzAtMS45MDI0LTEuNjQ1LTMuNzAyNi00LjU3MTItNS4zMDEyIDAuMDYxMy0wLjAzMjggMC4xMjEyNS0wLjA2MjUgMC4xODM3NS0wLjA5NDggMy4wMTI1IDEuNjI0NSA0LjcwNzUgMy40NTcgNC43MDc1IDUuMzk2IDAgNi43MDAyLTIwLjI0NCAxMi4xMzItNDUuMjE4IDEyLjEzMiIgZmlsbD0iI2I2YjZiNCIvPgogICA8cGF0aCBpZD0icGF0aDQ0NTUiIGQ9Im00NTEuMjggOTI0LjNjLTI0Ljc5NiAwLTQ0Ljg5OC01LjM5NC00NC44OTgtMTIuMDQ2IDAtMS45MDI0IDEuNjQ0MS0zLjcwMjYgNC41NzE0LTUuMzAxMiAwLjA2MiAwLjAzMTIgMC4xMjI1IDAuMDYyNSAwLjE4NSAwLjA5MzctMi44NDI4IDEuNTc1MS00LjQzNTUgMy4zNDEyLTQuNDM1NSA1LjIwNzUgMCA2LjYwNSAxOS45NTggMTEuOTYgNDQuNTc3IDExLjk2czQ0LjU3Ny01LjM1NSA0NC41NzctMTEuOTZjMC0xLjg2NjItMS41OTI1LTMuNjMyNC00LjQzNS01LjIwNzUgMC4wNjI1LTAuMDMxMiAwLjEyMjUtMC4wNjI1IDAuMTg1LTAuMDkzNyAyLjkyNjIgMS41OTg2IDQuNTcxMiAzLjM5ODkgNC41NzEyIDUuMzAxMiAwIDYuNjUyNC0yMC4xMDIgMTIuMDQ2LTQ0Ljg5OCAxMi4wNDYiIGZpbGw9IiNiNWI0YjMiLz4KICAgPHBhdGggaWQ9InBhdGg0NDU3IiBkPSJtNDUxLjI4IDkyNC4yMWMtMjQuNjE5IDAtNDQuNTc3LTUuMzU1LTQ0LjU3Ny0xMS45NiAwLTEuODY2MiAxLjU5MjgtMy42MzI0IDQuNDM1NS01LjIwNzUgMC4wNTk2IDAuMDMxMiAwLjEyNiAwLjA2NDkgMC4xODYgMC4wOTUyLTIuNzU3MiAxLjU0ODItNC4zMDEyIDMuMjgyMS00LjMwMTIgNS4xMTIyIDAgNi41NTc2IDE5LjgxNCAxMS44NzQgNDQuMjU3IDExLjg3NCAyNC40NDIgMCA0NC4yNTctNS4zMTU5IDQ0LjI1Ny0xMS44NzQgMC0xLjgzMDEtMS41NDM4LTMuNTY0LTQuMzAxMi01LjExMjIgMC4wNi0wLjAzMDQgMC4xMjYyNS0wLjA2NCAwLjE4NjI1LTAuMDk1MiAyLjg0MjUgMS41NzUxIDQuNDM1IDMuMzQxMiA0LjQzNSA1LjIwNzUgMCA2LjYwNS0xOS45NTggMTEuOTYtNDQuNTc3IDExLjk2IiBmaWxsPSIjYjRiM2IyIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDQ1OSIgZD0ibTQ1MS4yOCA5MjQuMTNjLTI0LjQ0MiAwLTQ0LjI1Ny01LjMxNTktNDQuMjU3LTExLjg3NCAwLTEuODMwMSAxLjU0NC0zLjU2NCA0LjMwMTItNS4xMTIyIDAuMDY0NSAwLjAzMzYgMC4xMjI2MyAwLjA2MjUgMC4xODcgMC4wOTQ2LTIuNjcyOCAxLjUyMjUtNC4xNjc0IDMuMjIzNi00LjE2NzQgNS4wMTc2IDAgNi41MDk4IDE5LjY3MSAxMS43ODggNDMuOTM2IDExLjc4OHM0My45MzYtNS4yNzc4IDQzLjkzNi0xMS43ODhjMC0xLjc5NC0xLjQ5MzgtMy40OTUxLTQuMTY3NS01LjAxNzYgMC4wNjUtMC4wMzIxIDAuMTIyNS0wLjA2MSAwLjE4NzUtMC4wOTQ2IDIuNzU3NSAxLjU0ODIgNC4zMDEyIDMuMjgyMSA0LjMwMTIgNS4xMTIyIDAgNi41NTc2LTE5LjgxNCAxMS44NzQtNDQuMjU3IDExLjg3NCIgZmlsbD0iI2IzYjJiMSIvPgogICA8cGF0aCBpZD0icGF0aDQ0NjEiIGQ9Im00NTEuMjggOTI0LjA0Yy0yNC4yNjUgMC00My45MzYtNS4yNzc4LTQzLjkzNi0xMS43ODggMC0xLjc5NCAxLjQ5NDYtMy40OTUxIDQuMTY3NC01LjAxNzYgMC4wNjI1IDAuMDMxMiAwLjEyNjUgMC4wNjQgMC4xODkgMC4wOTUzLTIuNTkwNCAxLjQ5NjEtNC4wMzUxIDMuMTYzNi00LjAzNTEgNC45MjI0IDAgNi40NjI0IDE5LjUyNyAxMS43MDEgNDMuNjE1IDExLjcwMXM0My42MTQtNS4yMzg4IDQzLjYxNC0xMS43MDFjMC0xLjc1ODgtMS40NDUtMy40MjYyLTQuMDM1LTQuOTIyNCAwLjA2MjUtMC4wMzEyIDAuMTI2MjUtMC4wNjQgMC4xODg3NS0wLjA5NTMgMi42NzM4IDEuNTIyNSA0LjE2NzUgMy4yMjM2IDQuMTY3NSA1LjAxNzYgMCA2LjUwOTgtMTkuNjcgMTEuNzg4LTQzLjkzNiAxMS43ODgiIGZpbGw9IiNiMWIxYjAiLz4KICAgPHBhdGggaWQ9InBhdGg0NDYzIiBkPSJtNDUxLjI4IDkyMy45NWMtMjQuMDg4IDAtNDMuNjE1LTUuMjM4OC00My42MTUtMTEuNzAxIDAtMS43NTg4IDEuNDQ0OC0zLjQyNjIgNC4wMzUxLTQuOTIyNCAwLjA2MiAwLjAzMTIgMC4xMjc1IDAuMDYzNSAwLjE5IDAuMDk0Ny0yLjUwNzQgMS40NzAyLTMuOTA0OSAzLjEwNS0zLjkwNDkgNC44Mjc2IDAgNi40MTUgMTkuMzg0IDExLjYxNiA0My4yOTQgMTEuNjE2IDIzLjkxMSAwIDQzLjI5NC01LjIwMTIgNDMuMjk0LTExLjYxNiAwLTEuNzIyNi0xLjM5NzUtMy4zNTc0LTMuOTA1LTQuODI3NiAwLjA2MjUtMC4wMzEyIDAuMTI3NS0wLjA2MzUgMC4xOS0wLjA5NDcgMi41OSAxLjQ5NjEgNC4wMzUgMy4xNjM2IDQuMDM1IDQuOTIyNCAwIDYuNDYyNC0xOS41MjYgMTEuNzAxLTQzLjYxNCAxMS43MDEiIGZpbGw9IiNiMGIwYWYiLz4KICAgPHBhdGggaWQ9InBhdGg0NDY1IiBkPSJtNDUxLjI4IDkyMy44N2MtMjMuOTExIDAtNDMuMjk0LTUuMjAxMi00My4yOTQtMTEuNjE2IDAtMS43MjI2IDEuMzk3NS0zLjM1NzQgMy45MDQ5LTQuODI3NiAwLjA2NDkgMC4wMzI3IDAuMTI1ODggMC4wNjI1IDAuMTkwODggMC4wOTUyLTIuNDI0OCAxLjQ0MzgtMy43NzQ5IDMuMDQ1OS0zLjc3NDkgNC43MzI0IDAgNi4zNjc2IDE5LjI0IDExLjUzIDQyLjk3NCAxMS41MyAyMy43MzMgMCA0Mi45NzMtNS4xNjIyIDQyLjk3My0xMS41MyAwLTEuNjg2NS0xLjM1LTMuMjg4Ni0zLjc3NS00LjczMjQgMC4wNjUtMC4wMzI3IDAuMTI2MjUtMC4wNjI1IDAuMTkxMjUtMC4wOTUyIDIuNTA3NSAxLjQ3MDIgMy45MDUgMy4xMDUgMy45MDUgNC44Mjc2IDAgNi40MTUtMTkuMzg0IDExLjYxNi00My4yOTQgMTEuNjE2IiBmaWxsPSIjYWZhZmFkIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDQ2NyIgZD0ibTQ1MS4yOCA5MjMuNzhjLTIzLjczMyAwLTQyLjk3NC01LjE2MjItNDIuOTc0LTExLjUzIDAtMS42ODY1IDEuMzUwMS0zLjI4ODYgMy43NzQ5LTQuNzMyNCAwLjA2MyAwLjAzMTIgMC4xMzAzNyAwLjA2NDkgMC4xOTI4NyAwLjA5NjEtMi4zNDQyIDEuNDE2LTMuNjQ3IDIuOTg2NC0zLjY0NyA0LjYzNjIgMCA2LjMxOTkgMTkuMDk2IDExLjQ0NCA0Mi42NTMgMTEuNDQ0IDIzLjU1NiAwIDQyLjY1My01LjEyNCA0Mi42NTMtMTEuNDQ0IDAtMS42NDk5LTEuMzAyNS0zLjIyMDItMy42NDc1LTQuNjM2MiAwLjA2MjUtMC4wMzEyIDAuMTMtMC4wNjQ5IDAuMTkyNS0wLjA5NjEgMi40MjUgMS40NDM4IDMuNzc1IDMuMDQ1OSAzLjc3NSA0LjczMjQgMCA2LjM2NzYtMTkuMjQgMTEuNTMtNDIuOTczIDExLjUzIiBmaWxsPSIjYWVhZWFjIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDQ2OSIgZD0ibTQ1MS4yOCA5MjMuN2MtMjMuNTU3IDAtNDIuNjUzLTUuMTI0LTQyLjY1My0xMS40NDQgMC0xLjY0OTkgMS4zMDI4LTMuMjIwMiAzLjY0Ny00LjYzNjIgMC4wNjU0IDAuMDMxMiAwLjEyODg4IDAuMDYyNSAwLjE5NDM4IDAuMDk0Ny0yLjI2NDggMS4zOTE2LTMuNTIwNSAyLjkyNjItMy41MjA1IDQuNTQxNSAwIDYuMjcyNSAxOC45NTMgMTEuMzU3IDQyLjMzMiAxMS4zNTdzNDIuMzMyLTUuMDg0OSA0Mi4zMzItMTEuMzU3YzAtMS42MTUyLTEuMjU2Mi0zLjE0OTktMy41Mi00LjU0MTUgMC4wNjUtMC4wMzIyIDAuMTI4NzUtMC4wNjM1IDAuMTkzNzUtMC4wOTQ3IDIuMzQ1IDEuNDE2IDMuNjQ3NSAyLjk4NjQgMy42NDc1IDQuNjM2MiAwIDYuMzE5OS0xOS4wOTcgMTEuNDQ0LTQyLjY1MyAxMS40NDQiIGZpbGw9IiNhZGFjYWIiLz4KICAgPHBhdGggaWQ9InBhdGg0NDcxIiBkPSJtNDUxLjI4IDkyMy42MWMtMjMuMzc5IDAtNDIuMzMyLTUuMDg0OS00Mi4zMzItMTEuMzU3IDAtMS42MTUyIDEuMjU1OC0zLjE0OTkgMy41MjA1LTQuNTQxNSAwLjA2NDkgMC4wMzE3IDAuMTI5ODcgMC4wNjQgMC4xOTUyNSAwLjA5NTItMi4xODUgMS4zNjUyLTMuMzk1IDIuODY3Ni0zLjM5NSA0LjQ0NjIgMCA2LjIyNTEgMTguODA5IDExLjI3MSA0Mi4wMTEgMTEuMjcxczQyLjAxLTUuMDQ2NCA0Mi4wMS0xMS4yNzFjMC0xLjU3ODYtMS4yMDg4LTMuMDgxLTMuMzk1LTQuNDQ2MiAwLjA2NjItMC4wMzEyIDAuMTMxMjUtMC4wNjM1IDAuMTk2MjUtMC4wOTUyIDIuMjYzOCAxLjM5MTYgMy41MiAyLjkyNjIgMy41MiA0LjU0MTUgMCA2LjI3MjUtMTguOTUyIDExLjM1Ny00Mi4zMzIgMTEuMzU3IiBmaWxsPSIjYWNhYmFhIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDQ3MyIgZD0ibTQ1MS4yOCA5MjMuNTJjLTIzLjIwMiAwLTQyLjAxMS01LjA0NjQtNDIuMDExLTExLjI3MSAwLTEuNTc4NiAxLjIxLTMuMDgxIDMuMzk1LTQuNDQ2MiAwLjA2NTUgMC4wMzI2IDAuMTMxMzcgMC4wNjM5IDAuMTk2NzUgMC4wOTYxLTIuMTA2NCAxLjMzNzQtMy4yNzEgMi44MDc2LTMuMjcxIDQuMzUwMSAwIDYuMTc3MiAxOC42NjYgMTEuMTg1IDQxLjY5IDExLjE4NSAyMy4wMjUgMCA0MS42OS01LjAwNzggNDEuNjktMTEuMTg1IDAtMS41NDI1LTEuMTY1LTMuMDEyOC0zLjI3MTItNC4zNTAxIDAuMDY1LTAuMDMyMiAwLjEzMTI1LTAuMDYzNSAwLjE5NjI1LTAuMDk2MSAyLjE4NjIgMS4zNjUyIDMuMzk1IDIuODY3NiAzLjM5NSA0LjQ0NjIgMCA2LjIyNTEtMTguODA5IDExLjI3MS00Mi4wMSAxMS4yNzEiIGZpbGw9IiNhYWFhYTkiLz4KICAgPHBhdGggaWQ9InBhdGg0NDc1IiBkPSJtNDUxLjI4IDkyMy40NGMtMjMuMDI1IDAtNDEuNjktNS4wMDc4LTQxLjY5LTExLjE4NSAwLTEuNTQyNSAxLjE2NDYtMy4wMTI4IDMuMjcxLTQuMzUwMSAwLjA2NTUgMC4wMzEyIDAuMTMzMzcgMC4wNjQgMC4xOTg3NSAwLjA5NTMtMi4wMjkyIDEuMzExLTMuMTQ4OSAyLjc0NzUtMy4xNDg5IDQuMjU0OSAwIDYuMTI5OSAxOC41MjIgMTEuMDk5IDQxLjM3IDExLjA5OXM0MS4zNjktNC45Njg4IDQxLjM2OS0xMS4wOTljMC0xLjUwNzQtMS4xMi0yLjk0MzktMy4xNDg4LTQuMjU0OSAwLjA2NjItMC4wMzEyIDAuMTMzNzUtMC4wNjQgMC4xOTg3NS0wLjA5NTMgMi4xMDYyIDEuMzM3NCAzLjI3MTIgMi44MDc2IDMuMjcxMiA0LjM1MDEgMCA2LjE3NzItMTguNjY2IDExLjE4NS00MS42OSAxMS4xODUiIGZpbGw9IiNhOWE5YTgiLz4KICAgPHBhdGggaWQ9InBhdGg0NDc3IiBkPSJtNDUxLjI4IDkyMy4zNWMtMjIuODQ4IDAtNDEuMzctNC45Njg4LTQxLjM3LTExLjA5OSAwLTEuNTA3NCAxLjExOTYtMi45NDM5IDMuMTQ4OS00LjI1NDkgMC4wNjc5IDAuMDMyMiAwLjEzMTg3IDAuMDYzNSAwLjE5OTc1IDAuMDk2MS0xLjk1MjEgMS4yODIyLTMuMDI3OSAyLjY4NzUtMy4wMjc5IDQuMTU4OCAwIDYuMDgyNSAxOC4zNzggMTEuMDE0IDQxLjA0OSAxMS4wMTQgMjIuNjcgMCA0MS4wNDktNC45MzExIDQxLjA0OS0xMS4wMTQgMC0xLjQ3MTItMS4wNzYyLTIuODc2NS0zLjAyODgtNC4xNTg4IDAuMDY4Ny0wLjAzMjYgMC4xMzI1LTAuMDYzOSAwLjItMC4wOTYxIDIuMDI4OCAxLjMxMSAzLjE0ODggMi43NDc1IDMuMTQ4OCA0LjI1NDkgMCA2LjEyOTktMTguNTIyIDExLjA5OS00MS4zNjkgMTEuMDk5IiBmaWxsPSIjYThhOGE3Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNDQ3OSIgZD0ibTQ1MS4yOCA5MjMuMjdjLTIyLjY3MSAwLTQxLjA0OS00LjkzMTEtNDEuMDQ5LTExLjAxNCAwLTEuNDcxMiAxLjA3NTgtMi44NzY1IDMuMDI3OS00LjE1ODggMC4wNjU5IDAuMDMxMiAwLjEzMjc1IDAuMDYyNSAwLjE5ODI1IDAuMDkzNy0xLjg3NiAxLjI1NjQtMi45MDkyIDIuNjI2LTIuOTA5MiA0LjA2MTEgMCA2LjA0MTUgMTguMjM2IDEwLjkzNSA0MC43MzYgMTAuOTM1IDIyLjQ4OCAwIDQwLjcyNS00Ljg5MzUgNDAuNzI1LTEwLjkzNSAwLTEuNDM1MS0xLjAzMjUtMi44MDQ4LTIuOTA4OC00LjA2MDEgMC4wNjYyLTAuMDMxMiAwLjEzNS0wLjA2MzUgMC4yLTAuMDk0NyAxLjk1MjUgMS4yODIyIDMuMDI4OCAyLjY4NzUgMy4wMjg4IDQuMTU4OCAwIDYuMDgyNS0xOC4zNzkgMTEuMDE0LTQxLjA0OSAxMS4wMTQiIGZpbGw9IiNhN2E2YTUiLz4KICAgPHBhdGggaWQ9InBhdGg0NDgxIiBkPSJtNDU4IDkxMi4yNWMwIDAuOTk3NS0zLjAxMTggMS44MDUxLTYuNzI3IDEuODA1MS0zLjcxNTQgMC02LjcyNzEtMC44MDc2Mi02LjcyNzEtMS44MDUxIDAtMC45OTYxMiAzLjAxMTgtMS44MDUxIDYuNzI3MS0xLjgwNTEgMy43MTUyIDAgNi43MjcgMC44MDkgNi43MjcgMS44MDUxIiBmaWxsPSIjMTAwZjBkIi8+CiAgPC9nPgogIDxnIGlkPSJnNDU2NSIgdHJhbnNmb3JtPSJtYXRyaXgoLjEyNSAwIDAgLS4xMjUgMzYyLjc2IDkzNikiPgogICA8ZyBpZD0iZzQ1NjciIGNsaXAtcGF0aD0idXJsKCNjbGlwUGF0aDQ1NjktNykiPgogICAgPHBhdGggaWQ9InBhdGg0NTgxIiBkPSJtOTM2LjI4IDE5ODYuNmMwIDEwNS4yNy04Ni4xMjEgMTkxLjM5LTE5MS4zOSAxOTEuMzloLTczLjQzOGMtMTAuNTUxIDAtMjAuOTA2LTAuODYtMzEuMDA0LTIuNTN2MzUxNGMwIDMwLjQ2LTI0LjY5NSA1NS4xNi01NS4xNiA1NS4xNnMtNTUuMTYtMjQuNy01NS4xNi01NS4xNnYtMzU3NC4yYy0zMS4wNjctMzQuMDUtNTAuMDY3LTc5LjI3LTUwLjA2Ny0xMjguNzJ2MzY3NS4zYzAgMTMuNDYgMS40MTEgMjYuNjIgNC4wOSAzOS4zMSAxOC4yNjIgODYuNTcgOTUuNTA0IDE1Mi4wNyAxODcuMyAxNTIuMDdoNzMuNDM4YzEwNS4yNiAwIDE5MS4zOS04Ni4xMiAxOTEuMzktMTkxLjM4di0zNjc1LjMiIGZpbGw9InVybCgjbGluZWFyR3JhZGllbnQ0NTczKSIvPgogICA8L2c+CiAgPC9nPgogIDxnIGlkPSJnNDU4MyIgdHJhbnNmb3JtPSJtYXRyaXgoLjEyNSAwIDAgLS4xMjUgMzYyLjc2IDkzNikiPgogICA8ZyBpZD0iZzQ1ODUiIGNsaXAtcGF0aD0idXJsKCNjbGlwUGF0aDQ1ODctMCkiPgogICAgPHBhdGggaWQ9InBhdGg0NTk3IiBkPSJtNDgwLjA3IDE0MDAuN3Y1ODUuOTVjMCA0OS40NSAxOSA5NC42NyA1MC4wNjcgMTI4Ljcydi01MjguNTJjMC0zMC40NyAyNC42OTUtNTUuMTYgNTUuMTYtNTUuMTZzNTUuMTYgMjQuNjkgNTUuMTYgNTUuMTZ2NTg4LjY2YzEwLjA5OCAxLjY3IDIwLjQ1MyAyLjUzIDMxLjAwNCAyLjUzaDczLjQzOGMxMDUuMjYgMCAxOTEuMzktODYuMTIgMTkxLjM5LTE5MS4zOXYtNTg1LjkzYy03MC4wODIgMjkuNzYtMTQ3LjE3IDQ2LjIyLTIyOC4xIDQ2LjIyLTgwLjkzNCAwLTE1OC4wMy0xNi40Ni0yMjguMTEtNDYuMjIiIGZpbGw9InVybCgjbGluZWFyR3JhZGllbnQ0NTkxKSIvPgogICA8L2c+CiAgPC9nPgogIDxnIGlkPSJnNDU5OSIgdHJhbnNmb3JtPSJtYXRyaXgoLjEyNSAwIDAgLS4xMjUgMzYyLjc2IDkzNikiPgogICA8ZyBpZD0iZzQ2MDEiIGNsaXAtcGF0aD0idXJsKCNjbGlwUGF0aDQ2MDMtMSkiPgogICAgPHBhdGggaWQ9InBhdGg0NjEzIiBkPSJtMTI5Mi40IDg2Mi43NWMwLTMyMi42NC0yNjEuNTQtNTg0LjE4LTU4NC4xNy01ODQuMThzLTU4NC4xOCAyNjEuNTQtNTg0LjE4IDU4NC4xOGMwIDMyMi42MyAyNjEuNTUgNTg0LjE4IDU4NC4xOCA1ODQuMThzNTg0LjE3LTI2MS41NSA1ODQuMTctNTg0LjE4IiBmaWxsPSJ1cmwoI3JhZGlhbEdyYWRpZW50NDYzOS0yKSIvPgogICA8L2c+CiAgPC9nPgogIDxnIGlkPSJnNDYxNSIgdHJhbnNmb3JtPSJtYXRyaXgoLjEyNSAwIDAgLS4xMjUgMzYyLjc2IDkzNikiPgogICA8ZyBpZD0iZzQ2MTciIGNsaXAtcGF0aD0idXJsKCNjbGlwUGF0aDQ2MTktMikiPgogICAgPHBhdGggaWQ9InBhdGg0NjI5IiBkPSJtMTA1MC43IDExNzIuM3YxLjQzLTAuNzEtMC43MiIgZmlsbD0idXJsKCNyYWRpYWxHcmFkaWVudDQ2MzktMikiLz4KICAgPC9nPgogIDwvZz4KICA8ZyBpZD0iZzQ2MzEiIHRyYW5zZm9ybT0ibWF0cml4KC4xMjUgMCAwIC0uMTI1IDM2Mi43NiA5MzYpIj4KICAgPGcgaWQ9Imc0NjMzIiBjbGlwLXBhdGg9InVybCgjY2xpcFBhdGg0NjM1LTEpIj4KICAgIDxwYXRoIGlkPSJwYXRoNDY0NSIgZD0ibTk2OC4zOCAxMDMxLjhjNTEuMzA1IDM3Ljk3IDgyLjI5NSA4Ny4yOSA4Mi4yOTUgMTQxLjIxdi0wLjcyYy0wLjI4LTUzLjYzLTMxLjIxLTEwMi42OS04Mi4yOTUtMTQwLjQ5bS01NjguMzQgNDYuM2MtMjIuMDA4IDI4LjY3LTM0LjM1OSA2MC44OC0zNC4zNTkgOTQuOTEgMC0zNC4wMyAxMi4zNTEtNjYuMjQgMzQuMzU5LTk0LjkxbTY1MC42MyA5NC45MWMwIDE1LjY5LTIuNjMgMzEtNy42MSA0NS43NSA0LjkxLTE0LjUzIDcuNTMtMjkuNiA3LjYxLTQ1LjA0di0wLjcxIiBmaWxsPSJ1cmwoI3JhZGlhbEdyYWRpZW50NDYzOS0yKSIvPgogICA8L2c+CiAgPC9nPgogIDxnIGlkPSJnNDY0NyIgdHJhbnNmb3JtPSJtYXRyaXgoLjEyNSAwIDAgLS4xMjUgMzYyLjc2IDkzNikiPgogICA8ZyBpZD0iZzQ2NDkiIGNsaXAtcGF0aD0idXJsKCNjbGlwUGF0aDQ2NTEtOCkiPgogICAgPGcgaWQ9Imc0NjU1IiB0cmFuc2Zvcm09Im1hdHJpeCg2OTYuMiwwLDAsNDUzLjIsMzU5LjksOTQ1LjkpIj4KICAgICA8aW1hZ2UgaWQ9ImltYWdlNDY1NyIgeGxpbms6aHJlZj0iZGF0YTppbWFnZS9wbmc7YmFzZTY0LGlWQk9SdzBLR2dvQUFBQU5TVWhFVWdBQUFKRUFBQUJlQ0FZQUFBQWpadlpDQUFBQUJITkNTVlFJQ0FnSWZBaGtpQUFBRnNoSlJFRlVlSnp0WGRlTzdMaXVKUTMvLzkvZWx3dVVlUjRrU290SnRpdDE5OHdRMkx0dFdWbExUQXJGLy9mL0Q2R2ZJUDZSVXYvNTlBT2p1Ui9IOFoyUzJLR21iQ3pFNDAvM3lMdVIvT0g2Q3RiM1lsbnllVlR0MzhJUWJkcVlzNEdUQ1I3VC90Y0huRDJRMzA0eGYzbDVFQ1UreTlWMkNORVh4bmQvZkJLcERBK2pNWko4ZDRsV25YVEtuUlpwYjdTMUF0enJvQmc1blh6VzhxdjJKT25MTExOSitUN2FqK01OT1lkMitnQ0ozMWlLUnJuQWJUc3I3Qkt4QTUrY3plWW53WEs5bkpQeS9lZXJJbU9VdHdMWkhRQ2UwLzU0QlVTaHZ0ekRIUEpOaHlTellndVJKdm5PNnh5Qzc0TEpEK2FuOUMxSnlycVZYUHZuYXYyOHlMb0lFRWsrUE5rbHozT2lqZktCU2NOTWdQdk9yaE5PQm50MGJsSHZxM3JQUi9YTkM1bmY1WFFCbUc2R2h2eDh2eVhwUXhXZTA2SHVjeUxsR2tjdmxLRXhpbTRHZVM0Z3ZyU2lTRUp1NEY4YzNkNlp2TEVQK2pIQzVzbXprM2JWQ0JFcSt5Mno2RXhlbWJKK0QwejdjWG5RR0xpS3RQZU55WnJreFd4aG9WUmNsVnhEMHNkMUdrZXJ3ZnE0bFVaMm9KN0J6Ukw1bmx0RG1FL0hSeUlkTUs1T2N1cDlKdk5kbUs1VS9pSW5RZ0QxWnhLaWgxZ2dpYmoyYVRoRE9FNUxtbm41Tk81eHBpa0FlWWRDUjc4NXY1ZXA0aXI0VUlrcnA2aFdZbEJjR09KZ2dBZVpSazIxVG9RZGk3b09nb25Jc3IzTnlWbWhEakt5SGJDU1htZVNEUlgzaTJBNng4aUxJSGlib1hlRGUyWDZ6Q3J0Z2FxSGhtSEZRUjB4K3BJZjA1aDV6b20yellvdG5QeENYUytTV1JGMUpCNGFEeXAzU0J6RndZRmVGU3MxbUl5cC9jTTZFVkp1OHA5eG5xdmZDdTRqUHR4eG1JTXN3SkF4R083WHBZNnptUGNqczZhT2pnWUZDNE5zWko0WmIxMU00Vmhta2trZzdhZ1Qyemkrd1Q1K2FKUVQ3U0hlT1VDOVQrZFZPdlU5bGZYUTlLY0ZMTDRSaGM3MDhYM3lNWTQ5clRJVWhyVDRqRG9UdEhWL0hJOGV1VHYxekVCRFpRYUhBY1NvVGhRWWduS3dsYXk2UWhJYmpybHRNTE13VlRHWUFUUnY1MUN2MVdOcHVaM3B1T2xFeXZxLzROQ2piS2N6VldNNHVOdlJPUkU2aFkrakFVazVUdVVBMWZBRGRCemxjbWppRzdGeXpJUWpYeWc4N2R5Q1k2M3E5UEdGMjR0MFZvOVEvd1huemJpUUFlbVI1NW5GOVhsbDRzc3IxRmdQWTlVeDdZZlFaRlBiUmdacFhuUXBOMk5LQU1iMlBWdEVaUURNNEY3RnpHQVhhTVJZd2RIdTZCdGZwK3QxTzEyZmsvQlF0SjM3eElXNE9ERkxvR2h5ZGxhYkV6a2ROMjBCbGlHUUFTSGpHNGltWWE1TFY4aUFVNFMyZXd2dmNHR1l4bk9vUEM5T3l4bXQ5S1grSXBMa3FhTFdBdkdEbkdaWFRNSlV5VTdVZzZDeTBHUUNBdWtSakNaUDdzNUc5T01vcThKM0pyQzh5RnBoUWxNQlI2Nmw5ZGhzclp1M3dKbjloVmx2R1k3TTZrRUR6RnRQVU16dk5QVHpWRU02Y3B4MUhYTUc1VjBvUkN3Q2JxTStzUTV1WVV4V2dkYXhWVWFCM0VoQmJCeW5UdGNWb3YweEhFdVpHUTRWUTlGR0NrWW44bHc4NFlNWVVDdkVWaFJ4aGlMTnA3ZERZcml0SktTL09TamZvVlJHTCtKQXFKOVFtYTJpNlprbWgyRHVjeCs0SDQ0RmN3TXdkcDh1RjRIVVkzTHhsT0ZnSFVSbzk5dDhlR01TMEZPNDZ6SGlXSVFXZ2x5ejFRWFFKMXNYd2RBWXp6WkhKMFFRcDNwbTFZbC9ncDZ2NTlCbnN3OCtmNjlHZGhZMHgxREcvM05vMm5jR1FJMzR5dUZGaUZnYXVGU2RJUUp4WnN4eU4zcm9zZFpsamxFRFpLY2NKNXRYblAzMm9GNUgzMEViQzRUNVRLdkIrSjNhMEpxeXR0Ukt1UGZyWlgwWDFJK3d1MEludndNZyt1N1E1QmVIQzFFQXRUaHRlK3hHVnJSNEhLa3daV21aS3hBd25pSVlnU0pabklUcENBZmZ5VEVTWk9SWjJYWHdjQW5BOTVKY3JwUFJaay9pQ1htM3daRXNrZ1krb0lEUnRNTVZBMGtSVkdFZHplZEZOTlk4aFdsL2FFYlNRYUxtaitjQ0RBQUo2MmhJR3pHYWdxWU9YZUhPOUlSa3kwTGVwWGRBOEpOaTdpcUhXYVhoNG10Zzl6WXRrM1YwampGMjMrVnd6QWpHMVhrR1V1VzArNm5iQXV6dy9ISTM4N2Z4U2lKMjUrRkJ4Q3hUSndvY3E0dWhMVlljaFRWNkJxb096eUdnSmtXVnpzY2x4K1crSmZKUUQ3eFM1cFYyZVRUTThEaldUbzNRNUtnRXU3RWIxcUxwTGxCNlI3Z0YzLzRnZG9tNi9GTmdkVE1KbmRDR01uYkliRmVJWWVWL0xPb0xOYkNtV3p0cWNEQ3Z2NmZmZnBJaGxSV285UjZpeW9CWXBPV05waTlISlJJQVlQeVZtZmtaVmdrbmdJQ3VwRHBTRjJmcUloaXBsUkVOajNWTExBZDNyN1VUWlFOY1h1RUQ1cXYyS2VsRXdJcGtyV0g0MzVLY2JwVDZMY3IxSy9YSTJ5amxOMVI4KzErZWNjV0QwNnlQT2tBTlo1TE03U1BJNWxpeE9rWEpmb2dRUFhxaEcyYWtPWGZnS0xDVUxZSDZaTnRqMFQvOVJqYmlXc0Y5OXRzL2hlNjJVZHhUSXU3OGhCM2NDWExSY2RLdElVeGRva3l1WXhJZDdYRS8yQUhHUEt0dUpFUHg1b0UwN3FoR3J0SUJCMERmc2c3aEJFSzQvREpibENUOUxaem1zMVN1bjJWOUZNM2RrR3dhWEtpK0FMQ1FBM0hqWDlQNUI5KzhXMkFzd0RKazN0WXFJQktCMTVsaDI2MVl6bXAwbFFuR0kyUHJtZkxYeTA5QmgzbThqUkY5aXFPOUMrVFJXbTJQN2ZrZ0dGaS9NSnFTWnhMK1VSdytKdGhFdjRtV2NRQ3dpUFpqZ0FlVUw1YmhxVFpjek5RRlFFZmRVZ3ZFcnI3UzQ5VUQrRGo1dnRZR1Y5Ky9SVmZMdjJLSm5hV3R5NTU5bmFWaEU2ODlRSDFDbGJxcHpVUWl1UEcvU2FDNWlpL2RQUlpPbk5JQUZnbWJSZjc1RFFIbDJhVXF5U3F0WlZacTBRbTJzWFZIVHdYK0xuZytKUmF2MXNQR2l3YUQ0OUltZk1WNU9xY2dWYWt6ZGFXL0t6ZEppaDMxQ2ZYcUl1NDRCZ05zbTlLRTJwb1o5ZkszK1pmUnRsZGR4dnRkdEg2eXdhRGFCa3JvQ0Q5YkVHU05FamdIaWo2WUs0UDRUSnFyK1oxeDBxb09LMUlSTnZQMUhOMXkrUXBjTTQxSW44aERSTW1NTm5RaHphb3I1bVluVDN1UW8zTWloaE1ackVEZU9BQndtdjJUazh4OVpsN2I5L0NlRGNNbmNTQkM2RHhDUHE3QkZ3YkxnL1JxdW51MHlxOTNkb2h6bGlacmR5S0dURmlXczllRjhKMEhRNWd1SmdXWHpDUkVFMWlIZ25CbXNZdHlseTVsV1BVZFJiWkRua0VzYmVYR0tUc3IvS3p4alp4L3J4bGZoZEladnVIWFQ0bXZxM1RHL2M3cTU4WGYxWWxnRnpyelZEeTVoemlnd2Q0UUdWeEdwdFFpdGM0VVFNSm15UUszQmJRTU45Q1BiTVBDYlJnRDlUbDdqU0d0ME9YR0s2TVBWUG45dEdKOWo2cXAxV2lsRThiSm1jZXMrZ3c1Q25JMW9XR2R5MkcramJIcDRHbmJRWVQyQjFsTFRQV3lBYUNOcHN5Q3ZVQThFdmpheDRhZDg0RFcwUHFtajg4QjQxMXVwOCtjOXovTFZMM1I2MFpFb05aNm0ycW1VMWZpQmlZdnhrWldUTHZRUnNNUE1KUXExVlNZeGw0U2JpSktXSWdQdG9jbm9RcDVjM0xXdmJGdFZOUWJLdGZCKytoOWwxWVJmVkprWm4wenkreUs5K3B1b3BCYklTR0UyaGdMVDF5TU1nUUUwRVRUL2xESFViYVUwWjk1ckpOeGQ0UFBUSG53Sk4zeDVpcGw2bWtiK1NqMEtkL2dMSWYza0ZWV1g2ZjNBZko2VHFneWVHWGI2MUg0NW5YSG5nZXUxdXNPUjF3aUFZbWpXMmYzWS9JY3loVXZtZXRmeXBFR0I5bFNLeUZXM05wanRpRyt3UkEzT2JkVks4cXJiditXbUh5bW5DeE5JV3JTaXorelBsVC8wSm96MmREdTEySHBlNDA2cUxqZzFsUGYxajNXaWo1TnFLanNBTUtkY0VSakdXUzlpSXB4T291RHRSNjJzSzVyR0hKMGNkTDNPd0I3SjEwdDUxcThlZ3JWMXFtUTluTVN2aXg3T2lZRisxOGZVUmt5aTdmY1YvSFZsQit5Ykp2akRzVXdFVkdYbFY1MFJRWFZWZFlkTTRtTlFtN2xDSThDbFF1elYxeVRTYjArUmlzeENiYmtuWVZXZlRWNXJDWEJURk54dHl3ZW1QR3pRazZxVEtDMUkwTzQzc3BiRUxHcy8zVVh1RjFKNzNEQ3VLNXdBemZmT1l2T0d1VmMwSjFZQXR5cm1NdDh2a09TUFBrb21iaFMwRTJPUHI2SkN4TXNJN2ZHSlAydTIwYlUzUGZkTDJUTWZlNm5QUmcyNGtzZjJHRVY4VWFveVRQNU0wMjRQak14d1lCRU0zT2Njc2Z1SFNNSjFQUnM2T2VCeUpXWS9HV1VIdFd3SlBBL0pDUWZMQ0VNK3pLT2xTMTI3b2hVWFdxTUdhdGtPaUJkejdNemxmMEEwV1ZQd3ZZODFEK0FPeFdMcG01cTRRSG9rbWFXRkVYaXRBS0xGUFAvMEhuWDZWWGU5RFJrUVR6a1lNRXc1Qlk2eWU3V1N5ZjFiUEdSSFBnMFBGMGd6QlEvd2JpMzQvUU00WGJXU3hCZmRhZGxGVkxhT05wbG5tS25ySUE3djY5MU02UlZMcCtnRXk1em9lQUxnbTlaNHBHSytScXlJYjlnbmMzOUdHcUFOZXRNRUNoVDBkYVRqczFOMUxsTk1SQTZUK1lnSWdDWURzZ2pGMDRaOThMYitsYm9rUFR4VlI1VE9EeHU1ZUJUWDRzWDArUlhGK1Y5WnZzWkxTNkFaTlZsblE3SVEvU3ZYKzdxSjBUMndhSEVDSWNXZ2RFSzQ1NTVvd21XeVk3WlZBNGRrZjI5dkhDaDFua2VjdVlPdUpyVGZib0htQ3ZrT2JvVlQ2dDY1TzRVM1FXUjYwekQ4aDV4TGVoc0tnOHdIQ3NtYzFJYWo0WVI2ZmJZcWRFYlMzbHN0aytHZlRpKzVpWVRyd3ZGRzRuZGQ5T0pCc0ttY2RmRmxZODlRMXZlMzdYTXpoeXh5N1NtR2F1TkpGNHBqakZzbmxISnh2N1ArazlnS3l4UmU4UVRIL3RrazQxVGpDdUVHRGlKUC85TnF1NUtVNlo3cURndytZTjBZUkE1UEVDMzV4clVzR1F2Y1NZMmYvTVU3OUtJTXFCY0ZXY1lvd0tjRkhFbUovTExHbGFFOFlnYnZkbG9zZFU5Ni9kYXE2NjBqM1A1dlM1alA1Rm9jVk1xRWhFNElOdjdReFFjZHRBOTdMempra2lJdzY1RXJVeWVpLytlVXozcmN5NzJXZTVVcjFmVlF4WGp0bmNyL2p5UWxJc2s0VWIvaVdObE9hWUhaRU9CMzRXS0R1ejlFRzRpYk9ncktIK3RQb1NaOHhCakhXSkJSN0xjSjI1ZXE4Ulh5MU9QYWtmeThNeVZ5NHcrczEzaldRbzI4S1UwcThPYkdVQ3doQm5td2FMdlIxS1BpUUhOUTRpSFpVWWt0QXZOQlRkZE9tamJxNGU5Tll1SEpYNlVwMU9oUm1CRUVUSjNUV0xEVFFtenlhZGlhNExlaEg1WDdYbWE3bktvZk8zTGp3WG01VGY0ZVpVQmxaS3NIbmpnVVZQeHROcUUyOGtQSXRvUGN4bW5qTHdONit3am8wc0xWdE93MG5wVXdReG15elNLcjVVSWpBM2pLeGVvbjNDYjMzZTFqQ2NFd1RvUEFTRFl0TzBaMThGR21rWHp4ZndGY0lIczBtK3FGeEhwTVdvaUl1YmlEazl1NE5FcjJqanlEVXJlWVJNa0lSRHJGSlZTUGVOR0JmOCtJT29Venc3Nm1XNXpKNmVjbzZ6ajF2V2VLL3A1VHN0M09Fcm14YVRpcDYwd0NaajRZbmtCMDh4a3ZCTVJ5ZFNadktoYWRsdlB4M2dRWEFkc1NiNDM3dHhiSm9Hdnk3VDM2ZHlhQ2wvS1QvREJYOE9TckFiNDA4VTRxZTc4YkZsY3lWZHhKQk9JQmxDYXJuRzZ2U2xKUkxyRm8xV21tK3VEbXpBc3VNdVFsWGdtelM3S2VsNDJPWkZ0bkszOXd6VnV5MXE0SUFrUEJDNklWY0lYUmR3RkxodVhoTEpZUU5sNStoZ3BlWFBLdFhpbDNYTVZFSi9FUkhMMFNyR3BnblZBd2ppelh2eXBKMk9GOWN4Rmk4Sk0wbmxIVTU1bjRxbE0yOGI0KzRNYU5EZlgzbHlWSXhlcW9Mci9HeHd6MTBkSStnR2RLQVhJVXRqZnFzMlZBNW8yUncrYWc3eGlubzBkamJIeWlrVURYcXRIczhySHlzVkJ0SnU3cU9DajVyK0IvWVJtZkc1VlVlaFFvYTBFUVRSV2hiSnpyNW1DV2VsTjUvUWJ6TGRhMzdscTZydmpxQzQzMTZ0dzJOU1dnNENMYmdZRW1ybFhxdGRCcU4yWHZRdmNpcDk1S3g0TVRRTzlhUzdJemtvMzN3NkNvQy9ocHFjUW5QNUZURmNHV0FHNTRtWGZzc0NlcFZ3cGRyYlI2UlY5Y3lkaEhMVTR1U1hjZjNBWW41T0NSQmR0MWR1ZE1penpoZHNKMkJYWm4raGdxQmZxT1AycnFPaFRVSFFqOC9UT1JLdHVwL1VJYlBqRUt2bHo1Q2VhVHBhcUxaWWp4RzlDRnBBejM1Wm5mc2pDS0NJWExidTV4MXE1aG1iQk1US1JtTXV6dFFrRVQyMWZ2L1A1akkzOXZxbmFVVmtud0p2eFhsL2RTLzFYU2RzK3IyL05kYUl6VVpqejQzSHR3MUNVbGVNNG5hbW9YYmFFc2d0VXVzR3BpeXVKMncvR0RUS2hzQWtvYlhDb1NySXBMZlBDWmk1SE1TQjhyNmg2MXcvblBmdURlVFd0aExMblBoa24wWUFZYjA1ZFZTRXNLQ1ZMU3FoSDZjUnZ6N3YreUtLYTlVeTQ3d2YxRnMzVWF5UDkvOEV0RUZaVll6S3BiU0ZpOXUvN3ZGNGd6OGZlUC9pTm52aDUrWktNNWhURXVkTmRsdVRqNmFUbjVGczFaWnVZRTlvR2xFRW5nc3BrSnF0eGZzV2xQdVJjMXZIb0ZXamZMQnZIbTZEdjNnUDB6c0g5RnVVd3NlT1JweXZjQ3BKOXgxamVTc3NZZ1BJaWJwZWg0L2FPNWh0Q1BtRTA2NkUwSThjaXlGUlBnOWlHS2dPT2pZb1hlVnJMeFdwY2QraTNXbWozSjhYNWFWWlVncjNabjROdGlpTlEwR0dKSXpvb01iMlcxLzd0UnFjWmR4VUo2UmJIZUpPWkR1NEdDN0xSa3RoNHBzbnNoMW1ack9rQXZtRFNYNlcvYUtGVmhNcTFCd081OEdoSDJkTWRNNzV1TzhZeHRjQWlzcE9acVhtMHVlbTRQYTl4eVZYVGVmQTJrRGw0MlhBd0h3Q1I2T041REhEWUhLYkk4elBHVmxyalhsdVIrSzFjNXlwZEFUeUNvUkpoa3lNRngyRXF1cXpwWDFtQ1FWY1N1ejF1SDJ0ckVPaHhtQTdSTUJHSjV0bjZJbHFvK0N5dzNoTlFoVm5hcm13UCtTTVVmbDUrU1ZHOFdXdkwwMEp2Z2hPdjFYU1U0cG1vbjREZG9QS25xL0dqT003ZmdYVllacFBvV2VOcHJYeXZhUFVyNEgrUHJqY201MHJaVUlQNUgzVHp5UVR3eHc2dlFCRHoyb1Y0TG5TT3J5MnpJU0VYRTBURllFWlczMkZudDFkaTdXdzJmZ00xZjZFT05xNFpBM2NldjZ4dDRaR3VBSE9ZR0JOd093SjJXR2prRW9uWVE0dUpEeWRDZ1VNY1B4VVlIWkEvc1FINjZiMjBSYnEzdHVGaVhvR2JyT3V4UG5zV2t4OHVma3RqQWJBTHZCQUJjM1Q5OU1BZ1NaeDIvYS91bzhiS1ZVTTE5aVVWM3o5T2J5ci9SeVdxbkRzYWcySk1OYzdRajFicnd2YmJydElMMVRRMnNXU2dIUk5XbThzMnY0WDFSTWVxWFdXZm8yaC92SmQrQWxTblFISVJqcENpMG9kUThZNWxDZW1GRG1KMUh5dWlHZ3JLam1jVE0zcUVlOFJxMlhRSnNBK3hxTCtxaXo4akxXc1B2VE5oaXJ5UlYxZTYwcTZiN3pYUWpGc3hpS25SUlJSK2N4YmozMTF1R0QrdjlpTDlsaU5FNzFLWG5sbTJxUlhyODJEekxIbjRMa1RnN202ZVpvWkVZd3hPbFdsS09SYWRpTE9LL0g3cks1VGk1UmV6bmE5V1RaYXZaZmhjWjR0eERtbjhyT3RFMWkwK2Z2NFZFdmpsaVN2K3BLeFdwNHpoQmM0UjdZamZTeThENkNTRGU4bzJoQ2VnOFduTUw1NFRnM1ZHUkN4Q3dod2NlSnZqUWt3UlZPYjdZaVN6eWxYaThRcDlXa24rRnIxYTd6dnBNOUc2OGhONVBBalJXRGNWVmhCSlhFVlJZc2dFZm95b0pBUlkrRmE1VitoNUR2S1h1RTlGcndCb2xmYkthZGRWbU9VNGVkeDJ5WlhHN0N1enE1bjljRHBQK210V2RiMURvekN2T3gzcHdmaFh1VTlGenlyaGQ1UGRBVTFNMnhkaXBXK1BIZHRlM1VHMzlQQnBVU2lSRlh1ZXNrK2xnbjVDUCtIYy91MjBuTHhGZUNhbWxtVTRVMDJJaUpocHg1Y2g2MWpGVXZmL0xNUVFrajhvV0lIRGcrMHFKdjY2MkxwTHo4NlZhbEg2SmJIWjlCN3FmOHh5MVc1eTc4N0pjUVJmNzJ3RWlYZVZWbEdmV1huM292UGZRdS9jcFhCM2RjRDZoY1NHYTJhaVI0YTZMalEwYm1Id1hxdlA4aDZZcXNvK3kwM1E5ZkJ2b1ZjQTlEclgwWHhzVHVNNlJ0RWIxS2h6SXFJcDQyaWErbmdSZzNFMjNyUytUQ1hPbzlnODRmbWZ0WGZvSG4yaTZaZDBTM0FVVG1WNmZtVFNuWTJrb09ta3l5QjZ1UlVSM0FTQ0piZ3RBbkpQNUYyaGY0SUoveXA5RDBDU2ZsZWRlVGdCY1ovUzBJbUlpSTdKZmViQUtaaktxaVJCZWVSWHdQVXZaa0F2VThWdFRqZVFNS1ExM0lnTVZ5THU0bXh5RUZXN2kvT1ZzTU1mdDQvTWVQOTJudkYzeUI2YWJ5VGpQdzBRL0VQMlJRYlFkdFZZNVpEaGNHeUZ5RURYWE9JQXpWb2d2ODBWNExCa3o2SmRhT0IvV0Z6U1hUOVo0RHI5MWR4blQyUlgzbjA1N2hSSisrbk9obzlkaU5vdkNUSFppejBEbTBsMkszNXF2ODkvOHVzcjFINDBtQWh2blFFRXBHdFkzRVdhL3FWRGRJOTEwNElVYTVnUlFvZk5rK1ZLUXhLNjNXZmp4MlV1QXU0L2tYaU5ycDYrRDF5SEtOL2c1VS9RRG02VUtOdTQxSytLOVlpbW9vek5LMlk5TWhFbnVzYjM4RnZucm5LbjlBRVQ3NTlHZDFpMVpDZ2lDaUtyMnBhUlpXazJVMU5mOW1BT2QxU2JIUEVrUVY5cnc0SDJyb0ZYU0xYLy8raTlGTzVDNktiNmtDa3dkcnJFb1NrbkVEdEdaSTZURURkT3hDSk54cWxzM0VCNUlpTGFhRjV1dGNsa2hadGxmZGtXQU5Id0M4QmcxNWovYUVGblo4cE0zUDdYMzhYa2x5QU9HTWMreHNJU3ZieXFRL2UvL3dQclJ0bllLOXcxL1FBQUFBQkpSVTVFcmtKZ2dnPT0iIHRyYW5zZm9ybT0ibWF0cml4KDEsMCwwLC0xLDAsMSkiIGhlaWdodD0iMSIgd2lkdGg9IjEiIHByZXNlcnZlQXNwZWN0UmF0aW89Im5vbmUiLz4KICAgIDwvZz4KICAgPC9nPgogIDwvZz4KICA8cGF0aCBpZD0icGF0aDQ2NTkiIGQ9Im00NTUuODcgMTg4Ljg0aC05LjE4MDJjLTIxLjczNyAwLTM5LjQyMyAxNy42ODUtMzkuNDIzIDM5LjQyMnY1MjMuMDdjLTI3LjM0IDE1LjY0NC00NC41MDkgNDQuNzc1LTQ0LjUwOSA3Ni44MjYgMCA0OC44MTEgMzkuNzExIDg4LjUyMiA4OC41MjIgODguNTIyIDQ4LjgxIDAgODguNTIyLTM5LjcxMiA4OC41MjItODguNTIyIDAtMzIuMDUyLTE3LjE2OS02MS4xODItNDQuNTA5LTc2LjgyNnYtNTIzLjA3YzAtMjEuNzM4LTE3LjY4NS0zOS40MjItMzkuNDIzLTM5LjQyMm0wIDE1LjVjMTMuMTU4IDAgMjMuOTIzIDEwLjc2NSAyMy45MjMgMjMuOTIydjUzMi42NWMyNi4xNjEgMTEuMTA2IDQ0LjUwOSAzNy4wMzEgNDQuNTA5IDY3LjI0MiAwIDQwLjMzLTMyLjY5MiA3My4wMjItNzMuMDIyIDczLjAyMi00MC4zMjkgMC03My4wMjItMzIuNjkyLTczLjAyMi03My4wMjIgMC0zMC4yMTEgMTguMzQ5LTU2LjEzNiA0NC41MDktNjcuMjQydi01MzIuNjVjMC0xMy4xNTggMTAuNzY2LTIzLjkyMiAyMy45MjMtMjMuOTIyaDkuMTgwMiIgZmlsbD0iIzEwMGYwZCIvPgogIDxnIGlkPSJnNDY2MSIgdHJhbnNmb3JtPSJtYXRyaXgoLjEyNSAwIDAgLS4xMjUgMzYyLjc2IDkzNikiPgogICA8ZyBpZD0iZzQ2NjMiIGNsaXAtcGF0aD0idXJsKCNjbGlwUGF0aDQ2NjUtNikiPgogICAgPHBhdGggaWQ9InBhdGg0Njc5IiBkPSJtNzQ0LjkgNTk0MS45aC03My40NDJjLTE1NC4zNyAwLTI3OS45Ni0xMjUuNTktMjc5Ljk2LTI3OS45NnYtNDIwNS41Yy0yMTguMTEtMTE2LjItMzU2LjA3LTM0My4yLTM1Ni4wNy01OTMuNjUtMC4wMDQtMzcwLjk2IDMwMS43OS02NzIuNzUgNjcyLjc1LTY3Mi43NSAzNzAuOTUgMCA2NzIuNzQgMzAxLjc5IDY3Mi43NCA2NzIuNzUgMCAyNTAuNDgtMTM3Ljk2IDQ3Ny40OC0zNTYuMDcgNTkzLjd2NDIwNS41YzAgMTU0LjM3LTEyNS41OSAyNzkuOTYtMjc5Ljk2IDI3OS45Nm0wLTg4LjU4YzEwNS4yNiAwIDE5MS4zOS04Ni4xMiAxOTEuMzktMTkxLjM4di00MjYxLjJjMjA5LjI5LTg4Ljg1IDM1Ni4wNy0yOTYuMjUgMzU2LjA3LTUzNy45NCAwLTMyMi42My0yNjEuNTQtNTg0LjE4LTU4NC4xNy01ODQuMThzLTU4NC4xOCAyNjEuNTUtNTg0LjE4IDU4NC4xOGMwIDI0MS42OSAxNDYuNzkgNDQ5LjA5IDM1Ni4wNyA1MzcuOTR2NDI2MS4yYzAgMTA1LjI2IDg2LjEyNSAxOTEuMzggMTkxLjM5IDE5MS4zOGg3My40NDIiIGZpbGw9InVybCgjbGluZWFyR3JhZGllbnQ0NjY5KSIvPgogICA8L2c+CiAgPC9nPgogIDxnIGlkPSJnNDY4MSIgdHJhbnNmb3JtPSJtYXRyaXgoLjEyNSAwIDAgLS4xMjUgMzYyLjc2IDkzNikiPgogICA8ZyBpZD0iZzQ2ODMiIGNsaXAtcGF0aD0idXJsKCNjbGlwUGF0aDQ2ODUtNikiPgogICAgPHBhdGggaWQ9InBhdGg0Njk3IiBkPSJtNTMwLjEzIDIxMTUuNHYzNTc0LjJjMCAzMC40NiAyNC42OTUgNTUuMTYgNTUuMTYgNTUuMTZzNTUuMTYtMjQuNyA1NS4xNi01NS4xNnYtMzUxNGMtNDMuMzI4LTcuMTMtODEuODgzLTI4Ljk2LTExMC4zMi02MC4xNCIgZmlsbD0idXJsKCNsaW5lYXJHcmFkaWVudDQ2ODkpIi8+CiAgIDwvZz4KICA8L2c+CiAgPGcgaWQ9Imc0Njk5IiB0cmFuc2Zvcm09Im1hdHJpeCguMTI1IDAgMCAtLjEyNSAzNjIuNzYgOTM2KSI+CiAgIDxnIGlkPSJnNDcwMSIgY2xpcC1wYXRoPSJ1cmwoI2NsaXBQYXRoNDcwMy00KSI+CiAgICA8cGF0aCBpZD0icGF0aDQ3MTMiIGQ9Im01ODUuMjkgMTUzMS43Yy0zMC40NjUgMC01NS4xNiAyNC42OS01NS4xNiA1NS4xNnY1MjguNTJjMjguNDM3IDMxLjE4IDY2Ljk5MiA1My4wMSAxMTAuMzIgNjAuMTR2LTU4OC42NmMwLTMwLjQ3LTI0LjY5NS01NS4xNi01NS4xNi01NS4xNiIgZmlsbD0idXJsKCNsaW5lYXJHcmFkaWVudDQ3MDcpIi8+CiAgIDwvZz4KICA8L2c+CiAgPGcgaWQ9Imc2MDkzIiB0cmFuc2Zvcm09Im1hdHJpeCguMTI1IDAgMCAtLjEyNSAzNjIuNzYgOTM2KSI+CiAgIDxnIGlkPSJnNjA5NSIgY2xpcC1wYXRoPSJ1cmwoI2NsaXBQYXRoNjA5Ny0zKSI+CiAgICA8ZyBpZD0iZzYxMDEiIHRyYW5zZm9ybT0ibWF0cml4KDY1OC4yLDAsMCwxODQuMiwzNzguOSw5OC45KSI+CiAgICAgPGltYWdlIGlkPSJpbWFnZTYxMDMiIHhsaW5rOmhyZWY9ImRhdGE6aW1hZ2UvcG5nO2Jhc2U2NCxpVkJPUncwS0dnb0FBQUFOU1VoRVVnQUFBUklBQUFCTUNBWUFBQUNoeERnUUFBQUFCSE5DU1ZRSUNBZ0lmQWhraUFBQUdRSkpSRUZVZUp6dG5mdXZka2RWeDc4ejgveXNnc1ZTU2x0N295VTBoYWJTVk5BYWJWS1RrclFKalZoTTBYSXBwTmltQ0RYSXhhWVJCSWtWSlkwMGxsZ0VtdDVMYWNHYVlOUUVqVVlpVVFsR0V2MFB2S0QrL3N6NHc4eWF2V2J0TmJOblArZnluUE4ydnNtYjl6eDdybnVmTTUvelhXdlAzc2Q0N3dPR3V1WER5Ymxjd2Z2dXVsZmMra3gzWFdOZHg5amJycjYrKy9ndDNlTzJ4K3M3MTZYdlRsajQvcTI1cHIzOTFzcTBzV3AxdFo4N3JhNSt6SGZVQ2UzUFlxNUJYR2t6UUxKT3B4VWtYZjBkcE8wUlg1Y0JrcE1ORWp2cmNXaG9hR2lsQmtpR2hvWU9yTTNMZitTSEFBRC8vVC8vdCtlcERBME5uVGE5OXJMTEFBQWJPakNBTWpRMDFDc0NDR2tqS3d5Z0RBME4xWFQ1WmE5Umo4OUFRaHBBR1JvYUl0VUFRcXFDaERTQU1qVDAwdFZsbDE3U1ZXOFJKS1FCbEtHaGw0NTZBVUxxQmdscEFHVm82TXpWYXk2NWVLZDJxMEZDR2tBWkdqcHp0Q3RBU0R1RGhEU0FNalIwZW5YcHhSY2RTajhIQmdscEFHVm82UFRvc0FCQ09qU1FrQVpRaG9aT3JpNjU2TUlqNmZmUVFVSWlvQUFES2tORCs5WlJBWVIwWkNEaEdpNWxhT2o0ZGZHRkZ4emJXTWNDRXRJQXl0RFEwZXM0QVVJNlZwQ1FSdGd6TkhTNHV1akh6OS9yK0hzQkNkZHdLVU5EdTJ2ZkFDR1pQM3Z4RytIdHQ5NjY3M2tVT3NsUUdhOWFyTFFkcjFwY1hiYnJxeFl2dk9DOGxiTTdXajMreEJNdzMvajZDeUdFZ0JBQ2Z2a2R0KzE3VG9WT0lsQUdTQ3B0QjBoV2w2MEZ5VWtEeUplLzhpaU1NZkhmMTE5NFBvT0UvcjN6OWwvWjl4eG5PaWxRR1NDcHRCMGdXVjNXQTVJTHpuLzFUdk01U24zeFQ3NDBBY1FZQUlCNS9tdlBaWkFBY2FIUTV6dmUvYTU5enJlcWZVSmxnS1RTZG9Ca2RWa05KQ2NSSGdEd2hUOStKTVBERWtBSUtGOTc3cXNoaEZBQUJJZ25GTHlIRHdGM3Z1KzkrNXgvVThjTmxRR1NTdHNCa3RWbGZLenp6enQzcDNHUFF3LzkwY093eHNCWU96a1FCaFJqRE14elgzMDJlQkhhMEFuRzR4N0JSOURjL2F2djMrZjVMT280b0RKQVVtazdRTEs2N0x4eno5bHByT1BTZzMvNCtRUVFBMlBpSDV6Z1FDbGc4dXl6endRSkVRSUlnQXlSRUFLODkvREI0NFAzM0xQUDgrdlNVVUZsZ0tUU2RvQ2txK3lrd3dNQVB2dTV6OEVhQzV1QWtjT1lCQlFOSnVhWlo1NE9Ha1NDTDNNbUJKRWMvdmlBZXovMHdYMmViN2NPRXlvREpKVzJBeVRWc2xlLzZwVTc5WG5jZXVEM1BndFk3alFpVEFCTXpxTUdrNmVmZmlwb0VLRUZVNFBJTmdEZWV5QUVmT1REdjc3UDgxK3RnNEJsZ0tUU2RvQWtmMzFhd0VINjlHZCtGekFHMWxvNGc5MWc4dFJUVDRhMVRvUkRCSWgvQjlSdlk1M2YvTmhIOW5VOWR0WWFzQXlRVk5xK3hFRnk3amxuNzlSdVgvckViMzg2Z3NFbFNNQWNEQ1pQUHZGNG1OeUhWM01pS2tSaWd3SWlJY1EvTFJ5OHgvMzNmWHdQbCtmZ1dvTEtBRW1sN1VzTUpLY05IS1Q3Zit1VGNlR0QzWGxaQVpOcXpvUkEwZ01SQUlVYnFVR0UrZ0NBVDl4LzN4NHUxK0dLdzJXQXBOTDJEQWZKYVFVSDZXUDMzVitBUUlNSkJ3bUFSWmdVSUhuODhjZUNoQWlBeFpCR1FnVEFEQ1RlVCtYZWUzem1VNS9jejFVOFpQM25ELzUzMzFNQU1FQ2kxbHNxN3dESmFZY0c2Y01mL1hoZStDWUJvblFVYlpoSWtBQ293a1FGeVVGQ0dnMGlRQUtUaiswZStKMVBIZjlWUFdMdEF5NERKRXE5cFhJeHoxZWQvWW9kWjNSeWRlOXZmRFFDQVlDMXBncVR3d2h4TWtnZWUrelJzUFl1elM1dWhDQkM3WHlxKytEdlA3Q0hTMzA4T21xNERKQW85UnBsNS96WVdZY3ptUk9vdTMvdFhsZ09pdVF1V2lBQjFyc1NTeHZUQkV5Szk1RklpUFJLZzBpdjd2ckFoekowSG5yd0Q3cmJuUWFkOWJJZlZvK2ZsTkRvVE5XWkRBeXVPKy8rUUliRGtuaGtZSzJGQnpKTUZ1VURRaHJDdzJlWWNEVmZiTVRkeUZxRkhhQnk1MTMzWU9zOUh2NzhnK3NIUEVXcUFRWVlrT25WSzE4aXNORDAzdmZmRGRjRER3VEU1ZWU3WUZNMlRtdVhYTW1DdXQrUXRnWUk4N1l4ck9uVkhYZmVGVjJLRDNqa0N3L3RQTzVwVkFzeXBETWRObWVmOWZKOVQrSEU2VjEzM0Fsais5MkgzL3FjODFpcmdKQnpMTDNhKzZzV2wzVDd1OStYYmRtWEhubDR6N001R2VxQmpkUi8vTmNQam1BbXl4cFEyRjIzM2Y0ZU9QYkU3WEZvVndDZGVKQnczWGI3ZTNLNDlQaFh2cmp2Nlp3cXZlSkhYN2J2S1F4MTZPM3ZlT2Y2TUVUb0lPRFoxY1YwZzhSYXUzTjRZNjJCOTFnVjNzaHhaZkwzMXR0dWg5OXVFVUxBMDA4OHV0Tzhob1pPZ243aDF0dlNIUk9YanhsanV2SWdOZTBLQkdDNi9idEdWWkFRMVlJRjNNcUVLNGZPV2dBNWE3SE5tZVhwMlorVzN2cTJYNHBmQkkrdEQzaisyU2Y3SnpzMGRNeTYrWlpmQkFBNGF3RGxEZ2hYdm1YTGJ1UDJTdDcrWFN0KyszZEpHU1RXbUhoTHlGckFBejcwTFg1alRIeDR6MXBZNytIUnQ2Y2dqdVB6bU43NzZGY1cydEtGOWQ3RE9nZS8zV2I0a0c1NjY5dXlpL25UNTUvdE9vK2hvYVBVVzI2K3BiaFZTeEJ4ZWQrSDY3NlY2NndGeEw2UUhta2IwbnFsUGNESHRUSEdBbmI1TjM4a1UxekFKa1RyUkp2TGVpWVBlSGpmMTA2NkVvczRyZ1FHSHlPV0FjNTZiUDBFcDdmY2ZFdmU0K0s5eHpkZmZLRTUzNkdodzlBTk45NEVhOHRIN1drUkx6a1IrbmxlNDBaYW05RmFxbTFHVzVMYzJUb0xiZkxDOTNIekNSRERHMVJBazA5T3VKSmFiZ09ZWU5MclNvdzFNNWhJVjhJcXoyQkM4NEsxdU9IR202WlhTWHFQdi9qbWk0c1hiV2lvUjlmZmNHTmErTFlOa1NUTmpVaUlTR2x1aEVORVUydDdmRTFMcnhPUTJzU1FKcm9TNi9Xblc0MHhPVmVpdVpMWkpGaVl4SE1sNUVwNDByVUdFd2tNc1BJYVRMSmpXWUFKemRnaWZ2UHB4VTViNy9HdHYvcnp4dVVkR3ByMHM5Zi9mQUVLQUgwUXFZUTBIQ0lrN2tacUVPRmEyaG92dGJRMVhwUDI5Ty9HWkJmQllBS1dLMEhwS21vaERvRHNTdWczUHM5bkFDVk1aSWdqNFRPRFNWcitTODVrNHh4Q0NHcVlRekN4enNWUUJ4RW1lUWRmK3VIWWVvK1EzTlRmZk9zdk8zK3NoczVrL2RSMVB4Y1hYYzV4MkJJVWR2cHQzUXBuNkU1TWp4TlpnZ2lwNS9tYVhMZnlzSjU4aFFCcDZXRTkwcVpzVkljSlVJWTR0RGhyTUxFcEw5RURreno1QldjQ1FNMlpaS2V5MmFSZGZWczFad0lBM2hpRUVBcDNRcGVEeWh5QWJaclBULy9NOWJIUDRDUDRFUEQzZi92WGZUOTlRNmRXMTc3cE9saG44M01sQkJFT0VBRFZVQ2FYQ1lqdzI3dzFpQmdyMjllZFNDOUVXay84eXJ4SXp4Ty9KT3B6WTR3cEZqNlFTTmlBQ2QwTzdvRUpnR2FZUS9lN1d6a1Q1eHdRUWpVQkN3RGJCQUVaNmdBV01EN2ZHdWJ1eEtVTDRjblZJTG9UNHh4TWNsWmI3d0h2NFl5RE5SWStlRno3cHV0eWVPYVRlL21IYi8vZFFYNXVoL2FvTjE3N1pnQnBVY0xzREJDcXJ5VlZhNkZNVVdidDNJWEVUcHM1a1o1d3BnY2lyYnlJREtQa08xczMyU0x4RUFkWWRDWU9pTzZrQVJOakxRSXdTOERTcENNMzRpc0d0QVNzUzJHSzZrNHNZTHlKNzFFUjdvUytvVE4zNGhDQndxQWpnV0lTR0lEb1VEaFFET0tZQkJTLzlYRFdJVmlMTjE3NzV0d3V2cGJCNHgrLzgrMmRmckNIamtaWFhYME5ySmwrNHhNSThyczdFa0JvVGZRQWhPcjE1a0p5SHl0ZENJQVpSRGhBZU52YWJkN0Yxd1VBWFU1RWU0djhCSkk0RStZaTZtRU9MZEphQWhiQWxQOVE3dVlBeUhkME5IZGl0Z2FXWG1rQUJpQmo0SXhSM1FsZDZNRExXTGhqMGk3WUdsQ01jekFoRkE3RkpZaDQ5czNJVVBFbVFpU0ZQUVlHc0M0OWNSbkxmK0thbjh4UTRlY01BTi85cCsrc1h3bERYYnJ5RFZjWEM3QUhIa0RwUHFndXo0RUFXSFlnc1dFM1FHTDFlUzVFOXQzclF2ZzRtZ3ZoYldWaTFmQnhHemtSN2UvYWJDakpTUjBFTStVUU5KaFFTQ0ZESFRCQUlBUTExSW5yTjRaUXZlNGtoeHRzMFJmaGpqRUlOb1UwRWlqcGYzNk1nRktFUEt5ZU55WURCVURUcFFEb2dvcUZRL0RUZXh4ODhManE2bXNLc05DMUNON2pYNzczejgyRjhsTFg2NjU0UFlBSkNnQ0tSWlRCWUUwQmdMWHc0R05vN29QYXQwS1lQSFlQUUdLSHEzSWgrUnlVTnJ1R01zVjREQ0lTb0ZPYjVFaHNPb0hpMWk4UGRheUhnOG12WTVTaERweUJveGRFTjl4SkVPNmtDRjJZTzRtYmUwMStveHFGTzNTUmw0RGlyRVB3b1hBb05CWWRBNUNnTWdFRllMa1crbVlRVk5LZG5nd1Y1K0o1Q3FkaTRSQXNBMFFLZ1RTd09PZXlZM0dwUHppSEs5OXdkV3pMNEZKOFRuMS8vMSsvaHpOTmw3LzJpbUlCQWloZ0FHQldMaDBIbFhGdzVIcEsyRUo5U25qRU5vMEVhcXhjM0xKZHlvSEVKZ2NEQ0QvZWt3dWhhMUM3dmR2S2g3VCt1bDRlenhoc3JMVjVrZE15SnBIZER6NUJocmtUNXh4c3NQbU5hc0RrVHB5enhhc1p5V0ZNSVZFODBhQUFaVnJvSlZETTFreHpGQTZsQ0htb1BjdWgwRW0zWEFvUTU1Q2g0aXkyUGl4Q0JYUWU2YmNRN1VrQlVBVUxnSHdIQ0pnRFkvcmJRanBBNlBNVlYxNVZ2SUtRUDlQRWI5bHJ6enJSc1gvL3QrL1B5bmJWSlpkZW5oY0xsM1pMVVpieDdkb3RZTVM2YzJnQXFEb09xaXRkQjdXVllZdGFSNEVIOVY5ekg3bmNUa2xVT3RZS1lYZy9Td0RKN1FWQXBybk5iKzN1OG5kcnFJeTdFQTZVTWtjQ3dORUNaZTlqemJrVDQrQk5nS0dYUmRNM01hNzUrSXBHRTl2SmNNZTV5V0dzQVFxRlBOeWhSSWhKOEtSeEVsQUFaSmRpZkFLSkx4MEpRWVdEWmdhVjJEQkRKVHVSRUpLajhQQXBiT05Bb0x0SCtaMjIzTEdRYTFMZ0FtQVJNRVdiQ2p4a0dlK0g2M1ZYdlA1QUw2MGlhUUNSejNMMEFJVy94cThIR0x3Tmh3YUFMbkRFOXZPY0IzMjk1RHh5bnhWNGFPWGNmZVQ2MGxuczZFQnlmNTBBNFgzMy9zRndHcVBJa2ZEQitjdWNjNUJCeDJsaHAzREhPR1NnNU1VcGdNTERIYUFQS0xQd0pTMVltOElXK2lFaWwwSkE0ZWRBbjdsTEFWQ0hpbk54RTFvYUs0OExwRmNWV0RnWEhZWjBLK0QxRTFqSXNSUWdjQzRuc2psY0FPVG5oNElWemtNQWhzNmJybVUrVm53dFlLSThmTmtMRGdrbStVdW5KdFdWaUdkTCtGNEV6WTBBeThEZ2JXV1l3dnVxT1E2cVd3Tkg3bGZBWStZc1pEL2kyUmgrQjRibVVYTWZkTjd5cWQzREJBaGR5OTQ3TXJsdkFROWV0bkdibEdOZ1A2elo4aWV3eUdTc1NmWjlMVkI0eUVOQUFWZ094Y2JieFpTVWxXNGpYa3oyT2JrVUJ4ZHpLV0c2RmN4ZlJMMUo0UnM0TkJTb0FGUDRRMS9YM0lvR0ZxcEhlMUlrTUR3N0Y1NW5vUTFLUzREaGRXSi9MS3hSWU1OVmc4ZGh2WW0rOWlTcDZsVFlVeDc4dlJtRkc2bkFRbzYxRmhwRjNVNXdVQnNPaHRMaDZIVTBlTVR6N0hjZi9IUHJWbTYrcmpzNGtIZ3QrZ0VDTnJaejhUeWpJekVzeDhDMnJjK0F3bzkzQUNYZjRlRWhEd0JuYlBGM2NyaEx5WXVKdXhRMkYrNCt5S1hRUlNTWEFrQ0ZDaURDSHdZVmgrUXMvT1JLQU9Sa0xRQUVjWHh5TEF3c3dKUzRGYTZGNEpLZEM4cG5tMlFvWTcwT0VMNzQ4NWliT1JTMEJ5WjdYdytoUFVNRmxBdS9KZTFONDlMUlNQandGL25JeFEvVWs2OXh2TjJnVWRSdGdJUHFTZGN4OVYyR0xid1A2VHppV1Azd29QT3F1bzgwYml1Skt2dGNDeERlSHdjSUhjL0pWcHBNTDFCNHlFTkFtZVZRS0N5UlNkbmtVZ2dvUU51bEFNaFFrVzZENW04dEEwZDYyVFIzS2pRR2R4aFZ0eEk2d1JMS01rZmpMTUNGMnBKa01yVklvSW9FcXl6ZnlESUZIalhYb2IyU29laDRwV3B2OU5MY2lvU0tWU0RDanhlN0tsdVF5WXQvSFRUS3RuWmUxZ0FISFpNNUQycXZoUzI4ck9aYStCTzZhOXpITkFlcjlEa2xVZWxhU3JmUjQwRDRzUmphR0F1UGtCMERYUndKbEF3T0JoUVlreGQxb001RHVzdVRnQUpNb1lNTlU2NkVYTW9HYVVFZU1sU29INWxUY1pnZ29ya1ZEU3hnYllDMGJ3WllEUmYrdVZqRUFqTFVEeCtESHdQbWNKQlBiV3Voakd4VFk4VlNEa1VMVnpScEFKRnQ1ZE9yV3RnaTI5VmdFWS9OZ1ZGOFZxQkJuemswWk4yMTRJaGpzMXZReXI2UFdmMEtQSXErV0x1RHVBL2VwbkErRllnUVFHYnVCTFNQeEpwcGp3Z0RDb0dEQTRYREpMc1Y1bEp5dSt4UzRxRGNwZVFMSDZZTmFVVXVCZWlDQ21oZkNPWlFvWGJ4QWdod2tDT2dPMENZZ3dWUUhFczhDQUF6MXdMb2NLRzZRQXlMZVBtRzFaR1E0ZTFpWXg3S3pHRkRVdUdoT0JUZzhISWpVclZjaVhRZ1FQc3VEbjl2aHdZS1hsKys2NVQzclFHaktGZWdrZXV6UlVOMStLS2ZIV3U0RHVwSGcwK1A4NkMrZXVGQjUxWnpIM3dPV3VpU3J3TURTRkVQSmFSeWFHTWNMZjRFRkpRdXhORTNXWVE5TTVjQ05GMUsvSnhDaHJSdzEwSUZtRUFBWUxxakJPUy8rcWVCaGNLTmZNejZLbGlvRFQ4Zk9sYURDNVMyRWpDOGpvUU1yOGRCQTB5d2tjZDVQOU1BYzBoc3ZRNFVxUnA0cERRd2FDcUFrQnZQWWFNQkF0QWhBZFFjU2lzSjJ3YU1CZzM2YkpWamNXNmw0NkE2TlhBQTRsWXpPMTdMZVFEb2dzZlV0Mzc3dG13N0J3Zi83TngwVFdaMUdFQjR1eUpIRWhkL3lFQUJNQXQ3dUVzQmtLSEN5MnRRb1dUakVsU0FldmdEWUxyenM3RkFDRTIzQXRUQndqZmpBUkVzc2F6TXNkVGdBbmJjQ0pEVUFNUDdjSDUrak5jdEFKR2VHZExxYTNDSjEyTmIxT0hxQmNaYXRRRERvVkNyMzNZcGRWQUFjMWp3ZWozQUFOQ0VCakIzRzd4T3kzSEV0bk53VUo4ejF4RW5JOXhEMjNuUS9EVjQ1TEtLQXdHd0dMNU1ZODNyYkp4MStXLzlGcUZMWHVpVFN3R1FvVUkvaUR6MEFTYUhJa01mb0E0VjJGaTJsRk9CVFVCSmVRaDZOMG92V0RUSFFuT21jVFRYRXNzbTUwSjl5N1owanZ5NFVRQUJ0b2hib05sQTJSZXlrRmlWOEduVjFkUzd4NlEzVHdMb2NORVNzelhIQVV5UWtHV2FHNUd3NEczV0FDT1cxNkZCeDJXZk1sU2gvMlhmUzY0ajlqbmZkVXJYUXd0YjV1MTNoMGR1Yit2dENYQWI2MnpjU3A0WCs5eGhrRXNCVU9SU2Nua0lSWkpTUW9XKzFxQUNDcTFDQU5LdVQyL1NvaGR1aFMvYVhjRUNJSWRjTmt3N0xscHc0Wi9YQW9iM0lmc0dLcUNKSC9LWE1uUnA1VVlJSGEyWGVSL0dUdGFXV3BBeFNxalRjaUdBRGdsWlQ0T0xWY3FYZ0tIVnJZVW84WHdtdDBIbDZoZ0NYTDNnb0xHV0VxWjVuZ3BNYXZEUXltVHVveGIrbEE4OTBqNFNPMTNRdzRZS1VBOS82R3RmOU4vblZoeGNGU3pSNnBzQ0xQR2tmWjVUQ3k2eEhHazhIUUs5Z05IRzR2VjVYM3djWHNkaHZ2aFZoMUZ4SGMzYnZFdDlOdFNmSjZtQVJXbmZDblVLY0N4QUJKakRJdFp0T3d5dGJDbEVVY2VTYmlOMmxOdXNCUWVOMzNJZDhuaVA4NkE1YS9EZ1gvUDVHbGFYNnN4eUpCcFU4djZQRHFqQXhJV3psRlBoL1ZuUmQxNjBkRHVYeXIzUFlBa01MSnBqQWFaRlRVNUl1cFo0RVdpZmgrSmNsTENJdEFRWWZpd3YwSTJBaGdJYlBuN1JOcmRwMy9iVjJ2SE14Rkc3RVZMVGxTZ0FtYjNFZU1HbDhJMXhMVkR3dGoyd0FIU1hVZFJkQ1EzNm4rYzRwbU5UcUJLUDFjRXhhOU1BaUV5WThqb3liQ25LVkZEcDhPQjFOL1RiSXVjdkZLaFlXQVRQRjdydVZLWUZINEhpUTc5YjRYVjZ3UUpnZXI4SndPQVN3U0pkQzdBZUxrREZVVlFjRE0ySnp6dGZ6eVRwWnVUWExlRHcrWERWZHFJZWRtNUVxbnRQU2NXOWFEdGxPUmppR0xyN2tQMjJRQUdzZ3dXZkd3Y0c3M3N0TlBnY1pJS1U1clFyT0FBOVpNbGZLd25UVnA4MWVQQjYvSncyZWNNTy9jWkhHeXFBSHY1UVBhcGJoQzlMYmlWK0FOQUdDeDByWElDMU1kY1JwaHdMdVpaWVp4MWNZcHMwSndFWTU5aUM2NEFNdjl1amdZYXVkVDYrNEQ1a3VZUlByVjNaNTdydzVTQ3lTajVrS21zblc3WFBMWWhva0FCUUlHa05MUGg0TFdERXczVm9jS2ZCNTFuTGNlUjJDbERXZ21NK2h6cEFaTTZEWDdOcU9NWEF0N0dXSGhqell0L0hmQThGTFd6ajJEczk2TUczaFJDb2RCRUpCZ3BZbGh3THdGeEF4YlZRblI2NEZHMFhBQlByOUVNR3FJUXNMSnprc0luMVN5M3RZT1h6bHRyMUtkKzE2czJWMU54TFQ2Z2pON3BwZ0pCZjE4SWY2U3lBT2l6NDF6MHVJeDVmQnczK1dhc2pReFd0SGdjSHphWFhkZkM1OHpxcUs1cmxhZUs4V0k3RVpoaUg0TVhkbENtRWlPVnM4ZE5PV0JZQ1VaMWR3Y0xieU0xZlczRlhoTU5sbHZoVTRGSzBuZVU1ZE1BVWZRcklsRzZKL2JDeTMvd3FiTkw1NUM4RlFqaDArUGlVNzZndC9oWVNqbW8zYTAyMVhhNjVYRHVtUUtVM1B3SlpUem9LSUlPQzkxczZteElXdkh3Tk1MVC9lNkVCb0pyanlPMHFqa1BydHhjYzgvcTF1ME4yMW5aamJKeFVkQUVwSkRIMFdzSlltZHhLa2ZSRTI2MnNBUXVzQUFIUHNhQmMvSVl2TE9aYWVCM051ZkQvZXdGVDFCV1FLY3ZtK1E4NDVQTlZIYzNVU2Y1eXl5Z3dnMDVzRENrdFh3SlVjaWFWaFgxUVJ3S3NjQ1dWcDRkbFhpUjFXcllWODY4QlFzNm5GeFN4WElkRldiWWVHSzMvbDZBQllKYmpvTEpXQ05RS1Y3VDJTNjVqTmk4ekhkdlFlMFBqd3ZVWktpWk1JVXZOclJ3bVdJci9yWkliOFJNRW1uQ0pYd0E0T0dEVXV0WDhSaDAweGR3Z0FNR0FZeUVYdFoyMVRSMURrd1JSMGVTWTNRaXBsWXd0UUZBMFduWWxHaHkwTWV0NUZMNndyRkpldnoxOEdNQm8xbDl3Ry9UL0hHQjF4MUZ2MCs4NjB1Um1vQUpTamlRWStzMXBNbFNBRWl6SWE1UmVUMGlmOVRBb3RwK0hHeklVaXZYTE8wSkZmZW9qSlhCcmNBRndhSURSeWpJVTJPc1R0ZnI4L0l0Mm1HQVQ4ellpbEJHd2FDWmVIYlVwNGNDWDA5eGxISjBiQWZvZFNhdWU5ZzZUeGR2QUVqQUtJR1M5MmhQR2VxaFRCMGt2TUdwMXRiekdVdithMjVpZjk3TGptSmZYd1RHMVozVUtWMlN4TWF5VFlKQVc3UFIwNjBIQW9qa1dnTUVoZmMyVHQ5SzEwTGo4TTNjdWdPWWFEZ1lZYlN5bkhKTmZhN0NaSlZNcndDbmE1N3E4M0JYbkpsWGJUNktOZTV4YUFvd0VRVzZudWhMaFBHWVFhUU5DZm0wWDZpd2Q2M1VZQUxxZ1VRK1YrcUFoKzJuZlZqNFlPRmhWQU1CbXMzSEZ3dlkreElvbXNXSUhzTVQvMllLaHhHU1JCMm5BeFpRTGxqc1hPbDZGaTNBdlJka0NZSXFYTmZPRjF3Qk4wYjRDRFBtWkE0ZVgxUktwR2dSYWlkTWxxRXo5SG02NEl4ZDZUVFY0QUZBVHRLMndSdnRjQTRUODNBMktXRkN0cDRVazhWejZYY3hTWG1PcGJTdC9JNzh2aHdFT0svSTAvdzk1Y0d6L2FGZkY2Z0FBQUFCSlJVNUVya0pnZ2c9PSIgdHJhbnNmb3JtPSJtYXRyaXgoMSwwLDAsLTEsMCwxKSIgaGVpZ2h0PSIxIiB3aWR0aD0iMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSIvPgogICAgPC9nPgogICA8L2c+CiAgPC9nPgogPC9nPgo8L3N2Zz4K\",\"data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnNjkzNCIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjEwLjg3bW0iIHdpZHRoPSI0OS45NjZtbSIgdmVyc2lvbj0iMS4xIiB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmlld0JveD0iMCAwIDE3Ny4wNDM3NSA3NDcuMTYyNDkiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyI+CiA8ZGVmcyBpZD0iZGVmczY5MzYiPgogIDxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyR3JhZGllbnQ1MTM5IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxZS03IC0yNjEzLjcgLTI2MTMuNyAtMWUtNyAyNjUyLjQgMzk0Mi44KSI+CiAgIDxzdG9wIGlkPSJzdG9wNTE0MSIgc3RvcC1jb2xvcj0iI2M3ZTQ0YiIgb2Zmc2V0PSIwIi8+CiAgIDxzdG9wIGlkPSJzdG9wNTE0MyIgc3RvcC1jb2xvcj0iIzYwYmE2NSIgb2Zmc2V0PSIxIi8+CiAgPC9saW5lYXJHcmFkaWVudD4KICA8bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhckdyYWRpZW50NTEyMSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoNDU2LjIyIDAgMCAtNDU2LjIyIDI0MjQuMyAzNDk0LjMpIj4KICAgPHN0b3AgaWQ9InN0b3A1MTIzIiBzdG9wLWNvbG9yPSIjY2FjOWM4IiBvZmZzZXQ9IjAiLz4KICAgPHN0b3AgaWQ9InN0b3A1MTI1IiBzdG9wLWNvbG9yPSIjZjZmNmY2IiBvZmZzZXQ9Ii43NTI2OSIvPgogICA8c3RvcCBpZD0ic3RvcDUxMjciIHN0b3AtY29sb3I9IiNkNGQzZDIiIG9mZnNldD0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXJHcmFkaWVudDUxMDEiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEzNDUuNSAwIDAgLTEzNDUuNSAxOTc5LjYgMzA2NS45KSI+CiAgIDxzdG9wIGlkPSJzdG9wNTEwMyIgc3RvcC1jb2xvcj0iI2IzYjNiMiIgb2Zmc2V0PSIwIi8+CiAgIDxzdG9wIGlkPSJzdG9wNTEwNSIgc3RvcC1jb2xvcj0iI2IzYjNiMiIgb2Zmc2V0PSIuMSIvPgogICA8c3RvcCBpZD0ic3RvcDUxMDciIHN0b3AtY29sb3I9IiNmZWZmZmYiIG9mZnNldD0iLjI0NzMxIi8+CiAgIDxzdG9wIGlkPSJzdG9wNTEwOSIgc3RvcC1jb2xvcj0iI2EzYTNhMSIgb2Zmc2V0PSIxIi8+CiAgPC9saW5lYXJHcmFkaWVudD4KICA8bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhckdyYWRpZW50NTAyMyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMWUtNyAtMjYxMy43IC0yNjEzLjcgLTFlLTcgMjY1Mi40IDM5NDIuOCkiPgogICA8c3RvcCBpZD0ic3RvcDUwMjUiIHN0b3AtY29sb3I9IiNhNmQ3MWMiIG9mZnNldD0iMCIvPgogICA8c3RvcCBpZD0ic3RvcDUwMjciIHN0b3AtY29sb3I9IiMyNjgwM2EiIG9mZnNldD0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXJHcmFkaWVudDUwMDUiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDQ1Ni4yMiAwIDAgLTQ1Ni4yMiAyNDI0LjMgMzQ5NC4zKSI+CiAgIDxzdG9wIGlkPSJzdG9wNTAwNyIgc3RvcC1jb2xvcj0iI2EzYTNhMSIgb2Zmc2V0PSIwIi8+CiAgIDxzdG9wIGlkPSJzdG9wNTAwOSIgc3RvcC1jb2xvcj0iI2VjZWNlYyIgb2Zmc2V0PSIuNzUyNjkiLz4KICAgPHN0b3AgaWQ9InN0b3A1MDExIiBzdG9wLWNvbG9yPSIjYjNiM2IyIiBvZmZzZXQ9IjEiLz4KICA8L2xpbmVhckdyYWRpZW50PgogIDxjbGlwUGF0aCBpZD0iY2xpcFBhdGg2MTA5LTkiIGNsaXBQYXRoVW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgPHBhdGggaWQ9InBhdGg2MTExLTQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0ibTIxOTguMiAxOTAuMDNjMCA2Ny4yMzEgMjAzLjMxIDEyMS43NyA0NTQuMTQgMTIxLjc3czQ1NC4xNC01NC41MzkgNDU0LjE0LTEyMS43N2MwLTY3LjMyLTIwMy4zMS0xMjEuODYtNDU0LjE0LTEyMS44NnMtNDU0LjE0IDU0LjU0My00NTQuMTQgMTIxLjg2Ii8+CiAgPC9jbGlwUGF0aD4KICA8Y2xpcFBhdGggaWQ9ImNsaXBQYXRoNTEzNS03IiBjbGlwUGF0aFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CiAgIDxwYXRoIGlkPSJwYXRoNTEzNy04IiBkPSJtMjUyOS41IDE1MzEuN2MtMzAuNDYgMC01NS4xNiAyNC42OS01NS4xNiA1NS4xNnYyMjc4LjhjMjguNDQgMzEuMTggNjcgNTMgMTEwLjMzIDYwLjE0di0yMzM4LjljMC0zMC40Ny0yNC43LTU1LjE2LTU1LjE3LTU1LjE2Ii8+CiAgPC9jbGlwUGF0aD4KICA8Y2xpcFBhdGggaWQ9ImNsaXBQYXRoNTExNy0wIiBjbGlwUGF0aFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CiAgIDxwYXRoIGlkPSJwYXRoNTExOS0zIiBkPSJtMjQ3NC4zIDM4NjUuNnYxODIzLjljMCAzMC40NiAyNC43IDU1LjE2IDU1LjE2IDU1LjE2IDMwLjQ3IDAgNTUuMTctMjQuNyA1NS4xNy01NS4xNnYtMTc2My44Yy00My4zMy03LjE0LTgxLjg5LTI4Ljk2LTExMC4zMy02MC4xNCIvPgogIDwvY2xpcFBhdGg+CiAgPGNsaXBQYXRoIGlkPSJjbGlwUGF0aDUwOTctNiIgY2xpcFBhdGhVbml0cz0idXNlclNwYWNlT25Vc2UiPgogICA8cGF0aCBpZD0icGF0aDUwOTktMyIgZD0ibTI2ODkuMSA1OTQxLjloLTczLjQ0Yy0xNTQuMzcgMC0yNzkuOTYtMTI1LjU5LTI3OS45Ni0yNzkuOTZ2LTQyMDUuNWMtMjE4LjExLTExNi4yMi0zNTYuMDctMzQzLjIyLTM1Ni4wNy01OTMuNyAwLTM3MC45NiAzMDEuNzktNjcyLjc1IDY3Mi43NS02NzIuNzUgMzcwLjk1IDAgNjcyLjc0IDMwMS43OSA2NzIuNzQgNjcyLjc1IDAgMjUwLjQ4LTEzNy45NSA0NzcuNDgtMzU2LjA3IDU5My43djQyMDUuNWMwIDE1NC4zNy0xMjUuNTkgMjc5Ljk2LTI3OS45NSAyNzkuOTZtMC04OC41OGMxMDUuMjYgMCAxOTEuMzgtODYuMTIgMTkxLjM4LTE5MS4zOHYtNDI2MS4yYzIwOS4yOS04OC44NSAzNTYuMDctMjk2LjI1IDM1Ni4wNy01MzcuOTQgMC0zMjIuNjMtMjYxLjU0LTU4NC4xOC01ODQuMTctNTg0LjE4LTMyMi42NCAwLTU4NC4xOCAyNjEuNTUtNTg0LjE4IDU4NC4xOCAwIDI0MS42OSAxNDYuNzkgNDQ5LjA5IDM1Ni4wNyA1MzcuOTR2NDI2MS4yYzAgMTA1LjI2IDg2LjEyIDE5MS4zOCAxOTEuMzkgMTkxLjM4aDczLjQ0Ii8+CiAgPC9jbGlwUGF0aD4KICA8Y2xpcFBhdGggaWQ9ImNsaXBQYXRoNTA4My01IiBjbGlwUGF0aFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CiAgIDxwYXRoIGlkPSJwYXRoNTA4NS01IiBkPSJtMjY1Mi40IDk1NS44OWMtMTg5LjE2IDAtMzQyLjUgOTcuMjE5LTM0Mi41IDIxNy4xNSAwIDExOS45MiAxNTMuMzQgMjE3LjE0IDM0Mi41IDIxNy4xNCAxMzkuNjUgMCAyNTkuNzctNTIuOTkgMzEzLjA5LTEyOC45OSAxOC45LTI2Ljk0IDI5LjQtNTYuNzYgMjkuNC04OC4xNSAwLTY2LjY3LTQ3LjQtMTI2LjMzLTEyMS45OS0xNjYuMTYtNTkuNTgtMzEuODEtMTM2LjUxLTUwLjk4OS0yMjAuNS01MC45ODkiLz4KICA8L2NsaXBQYXRoPgogIDxjbGlwUGF0aCBpZD0iY2xpcFBhdGg1MDY3LTQiIGNsaXBQYXRoVW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgPHBhdGggaWQ9InBhdGg1MDY5LTciIGQ9Im0yODcyLjkgMTAwNi45Yzc0LjU5IDM5LjgzIDEyMS45OSA5OS40OSAxMjEuOTkgMTY2LjE2di0xLjAyYy0wLjQ4LTY2LjI2LTQ3Ljc2LTEyNS41MS0xMjEuOTktMTY1LjE0bTEyMS45OSAxNjYuMTZjMCAzMS4zOS0xMC41IDYxLjIxLTI5LjQgODguMTUgMTguNy0yNi42NSAyOS4xOC01Ni4xMyAyOS40LTg3LjE0di0xLjAxIi8+CiAgPC9jbGlwUGF0aD4KICA8cmFkaWFsR3JhZGllbnQgaWQ9InJhZGlhbEdyYWRpZW50NTA3MS02IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY3k9IjAiIGN4PSIwIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDU4NC4xOCAwIDAgLTU4NC4xOCAyNjUyLjQgODYyLjc1KSIgcj0iMSI+CiAgIDxzdG9wIGlkPSJzdG9wNTA3My01IiBzdG9wLWNvbG9yPSIjYTZkNzFjIiBvZmZzZXQ9IjAiLz4KICAgPHN0b3AgaWQ9InN0b3A1MDc1LTYiIHN0b3AtY29sb3I9IiMyNjgwM2EiIG9mZnNldD0iMSIvPgogIDwvcmFkaWFsR3JhZGllbnQ+CiAgPGNsaXBQYXRoIGlkPSJjbGlwUGF0aDUwNTEtOSIgY2xpcFBhdGhVbml0cz0idXNlclNwYWNlT25Vc2UiPgogICA8cGF0aCBpZD0icGF0aDUwNTMtMyIgZD0ibTI5OTQuOSAxMTcydjEuMDIgMS4wMS0xLjAxLTEuMDIiLz4KICA8L2NsaXBQYXRoPgogIDxjbGlwUGF0aCBpZD0iY2xpcFBhdGg1MDM1LTIiIGNsaXBQYXRoVW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgPHBhdGggaWQ9InBhdGg1MDM3LTUiIGQ9Im0zMjM2LjUgODYyLjc1YzAtMzIyLjY0LTI2MS41NC01ODQuMTgtNTg0LjE3LTU4NC4xOC0zMjIuNjQgMC01ODQuMTggMjYxLjU0LTU4NC4xOCA1ODQuMTggMCAzMjIuNjMgMjYxLjU0IDU4NC4xOCA1ODQuMTggNTg0LjE4IDMyMi42MyAwIDU4NC4xNy0yNjEuNTUgNTg0LjE3LTU4NC4xOCIvPgogIDwvY2xpcFBhdGg+CiAgPGNsaXBQYXRoIGlkPSJjbGlwUGF0aDUwMTktNCIgY2xpcFBhdGhVbml0cz0idXNlclNwYWNlT25Vc2UiPgogICA8cGF0aCBpZD0icGF0aDUwMjEtMyIgZD0ibTI4ODAuNSAxNDAwLjdjLTcwLjA4IDI5Ljc2LTE0Ny4xNyA0Ni4yMi0yMjguMSA0Ni4yMi04MC45NCAwLTE1OC4wMy0xNi40Ni0yMjguMTEtNDYuMjJ2MjMzNi4yYzAgNDkuNDQgMTkgOTQuNjYgNTAuMDYgMTI4Ljcydi0yMjc4LjhjMC0zMC40NyAyNC43LTU1LjE2IDU1LjE2LTU1LjE2IDMwLjQ3IDAgNTUuMTcgMjQuNjkgNTUuMTcgNTUuMTZ2MjMzOC45YzEwLjA5IDEuNjYgMjAuNDUgMi41MiAzMSAyLjUyaDczLjQ0YzEwNS4yNiAwIDE5MS4zOC04Ni4xMiAxOTEuMzgtMTkxLjM4di0yMzM2LjIiLz4KICA8L2NsaXBQYXRoPgogIDxjbGlwUGF0aCBpZD0iY2xpcFBhdGg1MDAxLTgiIGNsaXBQYXRoVW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgPHBhdGggaWQ9InBhdGg1MDAzLTYiIGQ9Im0yODgwLjUgMzczNi45YzAgMTA1LjI2LTg2LjEyIDE5MS4zOC0xOTEuMzggMTkxLjM4aC03My40NGMtMTAuNTUgMC0yMC45MS0wLjg2LTMxLTIuNTJ2MTc2My44YzAgMzAuNDYtMjQuNyA1NS4xNi01NS4xNyA1NS4xNi0zMC40NiAwLTU1LjE2LTI0LjctNTUuMTYtNTUuMTZ2LTE4MjMuOWMtMzEuMDYtMzQuMDYtNTAuMDYtNzkuMjgtNTAuMDYtMTI4LjcydjE5MjVjMCAxMDUuMjYgODYuMTIgMTkxLjM4IDE5MS4zOSAxOTEuMzhoNzMuNDRjMTA1LjI2IDAgMTkxLjM4LTg2LjEyIDE5MS4zOC0xOTEuMzh2LTE5MjUiLz4KICA8L2NsaXBQYXRoPgogPC9kZWZzPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTI1Ny4wNiAtMjAwLjk3KSI+CiAgPGcgZmlsbD0iI2ZmZiI+CiAgIDxwYXRoIGlkPSJwYXRoNDcxNSIgZD0ibTM0NS41OCA5NDguMTRjLTQ4Ljg5IDAtODguNTIyLTEwLjYzMi04OC41MjItMjMuNzUgMC03LjA4NjQgMTEuNTcxLTEzLjQ0OSAyOS45MTktMTcuOCAwLjAzNSAwLjAyOTggMC4wNjc1IDAuMDU4NSAwLjEwMjUgMC4wODk3LTE4LjIyIDQuMzM1LTI5LjcgMTAuNjYzLTI5LjcgMTcuNzEgMCAxMy4wNjkgMzkuNDg5IDIzLjY2NCA4OC4yMDEgMjMuNjY0IDQ4LjcxMSAwIDg4LjIwMS0xMC41OTUgODguMjAxLTIzLjY2NCAwLTcuMDQ3NC0xMS40ODEtMTMuMzc1LTI5LjcwMS0xNy43MSAwLjAzNS0wLjAzMTIgMC4wNjctMC4wNiAwLjEwMjUtMC4wODk3IDE4LjM0OCA0LjM1MSAyOS45MTkgMTAuNzEzIDI5LjkxOSAxNy44IDAgMTMuMTE4LTM5LjYzMiAyMy43NS04OC41MjEgMjMuNzUiLz4KICAgPHBhdGggaWQ9InBhdGg0NzE3IiBkPSJtMzQ1LjU4IDk0OC4wNWMtNDguNzEyIDAtODguMjAxLTEwLjU5NS04OC4yMDEtMjMuNjY0IDAtNy4wNDc0IDExLjQ4LTEzLjM3NSAyOS43LTE3LjcxIDAuMDMzNyAwLjAyODkgMC4wNyAwLjA2MTEgMC4xMDI1IDAuMDkxNC0xOC4wODkgNC4zMTc0LTI5LjQ4MiAxMC42MTEtMjkuNDgyIDE3LjYxOSAwIDEzLjAyMSAzOS4zNDYgMjMuNTc4IDg3Ljg4MSAyMy41NzhzODcuODgtMTAuNTU3IDg3Ljg4LTIzLjU3OGMwLTcuMDA3NC0xMS4zOTQtMTMuMzAxLTI5LjQ4Mi0xNy42MTkgMC4wMzMtMC4wMzAyIDAuMDctMC4wNjI1IDAuMTAyNS0wLjA5MTQgMTguMjIgNC4zMzUgMjkuNzAxIDEwLjY2MyAyOS43MDEgMTcuNzEgMCAxMy4wNjktMzkuNDkgMjMuNjY0LTg4LjIwMSAyMy42NjQiLz4KICAgPHBhdGggaWQ9InBhdGg0NzE5IiBkPSJtMzQ1LjU4IDk0Ny45NmMtNDguNTM1IDAtODcuODgxLTEwLjU1Ny04Ny44ODEtMjMuNTc4IDAtNy4wMDc0IDExLjM5NC0xMy4zMDEgMjkuNDgyLTE3LjYxOSAwLjAzNjIgMC4wMjk3IDAuMDY4NyAwLjA1ODYgMC4xMDM3NSAwLjA4OTktMTcuOTYgNC4zMDI2LTI5LjI2NSAxMC41NjItMjkuMjY1IDE3LjUyOSAwIDEyLjk3NCAzOS4yMDEgMjMuNDkxIDg3LjU2IDIzLjQ5MSA0OC4zNTggMCA4Ny41NTktMTAuNTE4IDg3LjU1OS0yMy40OTEgMC02Ljk2NzItMTEuMzA1LTEzLjIyNi0yOS4yNjUtMTcuNTI5IDAuMDM2LTAuMDMxMiAwLjA2OS0wLjA2MDEgMC4xMDM4LTAuMDg5OSAxOC4wODkgNC4zMTc0IDI5LjQ4MiAxMC42MTEgMjkuNDgyIDE3LjYxOSAwIDEzLjAyMS0zOS4zNDUgMjMuNTc4LTg3Ljg4IDIzLjU3OCIvPgogICA8cGF0aCBpZD0icGF0aDQ3MjEiIGQ9Im0zNDUuNTggOTQ3Ljg4Yy00OC4zNTkgMC04Ny41Ni0xMC41MTgtODcuNTYtMjMuNDkxIDAtNi45NjcyIDExLjMwNS0xMy4yMjYgMjkuMjY1LTE3LjUyOSAwLjAzMjUgMC4wMjg4IDAuMDcgMC4wNjEgMC4xMDM3NSAwLjA4OTctMTcuODMgNC4yODY2LTI5LjA0OCAxMC41MTEtMjkuMDQ4IDE3LjQzOSAwIDEyLjkyNiAzOS4wNTggMjMuNDA2IDg3LjIzOSAyMy40MDYgNDguMTggMCA4Ny4yMzktMTAuNDggODcuMjM5LTIzLjQwNiAwLTYuOTI3OC0xMS4yMTktMTMuMTUyLTI5LjA0OC0xNy40MzkgMC4wMzMtMC4wMjg3IDAuMDctMC4wNjEgMC4xMDI1LTAuMDg5NyAxNy45NiA0LjMwMjYgMjkuMjY1IDEwLjU2MiAyOS4yNjUgMTcuNTI5IDAgMTIuOTc0LTM5LjIwMSAyMy40OTEtODcuNTU5IDIzLjQ5MSIvPgogICA8cGF0aCBpZD0icGF0aDQ3MjMiIGQ9Im0zNDUuNTggOTQ3Ljc5Yy00OC4xODEgMC04Ny4yMzktMTAuNDgtODcuMjM5LTIzLjQwNiAwLTYuOTI3OCAxMS4yMTgtMTMuMTUyIDI5LjA0OC0xNy40MzkgMC4wMzUgMC4wMzEyIDAuMDY3NSAwLjA2MDEgMC4xMDM3NSAwLjA5MTQtMTcuNzAxIDQuMjY5LTI4LjgzMSAxMC40NTktMjguODMxIDE3LjM0OCAwIDEyLjg3OSAzOC45MTUgMjMuMzIgODYuOTE5IDIzLjMyIDQ4LjAwMyAwIDg2LjkxOC0xMC40NDEgODYuOTE4LTIzLjMyIDAtNi44ODg2LTExLjEzLTEzLjA3OS0yOC44MzEtMTcuMzQ4IDAuMDM2LTAuMDMxMiAwLjA2OS0wLjA2MDEgMC4xMDUtMC4wOTE0IDE3LjgyOSA0LjI4NjYgMjkuMDQ4IDEwLjUxMSAyOS4wNDggMTcuNDM5IDAgMTIuOTI2LTM5LjA1OSAyMy40MDYtODcuMjM5IDIzLjQwNiIvPgogICA8cGF0aCBpZD0icGF0aDQ3MjUiIGQ9Im0zNDUuNTggOTQ3LjcxYy00OC4wMDQgMC04Ni45MTktMTAuNDQxLTg2LjkxOS0yMy4zMiAwLTYuODg4NiAxMS4xMy0xMy4wNzkgMjguODMxLTE3LjM0OCAwLjAzNSAwLjAzMTIgMC4wNjg3IDAuMDYgMC4xMDM3NSAwLjA5MDItMTcuNTY5IDQuMjUyNi0yOC42MTQgMTAuNDEtMjguNjE0IDE3LjI1NyAwIDEyLjgzMSAzOC43NyAyMy4yMzQgODYuNTk4IDIzLjIzNCA0Ny44MjYgMCA4Ni41OTgtMTAuNDAzIDg2LjU5OC0yMy4yMzQgMC02Ljg0NzYtMTEuMDQ2LTEzLjAwNS0yOC42MTUtMTcuMjU3IDAuMDM1LTAuMDMwMyAwLjA2OS0wLjA1OSAwLjEwMzctMC4wOTAyIDE3LjcwMSA0LjI2OSAyOC44MzEgMTAuNDU5IDI4LjgzMSAxNy4zNDggMCAxMi44NzktMzguOTE1IDIzLjMyLTg2LjkxOCAyMy4zMiIvPgogICA8cGF0aCBpZD0icGF0aDQ3MjciIGQ9Im0zNDUuNTggOTQ3LjYyYy00Ny44MjggMC04Ni41OTgtMTAuNDAzLTg2LjU5OC0yMy4yMzQgMC02Ljg0NzYgMTEuMDQ1LTEzLjAwNSAyOC42MTQtMTcuMjU3IDAuMDMzNyAwLjAyOTkgMC4wNzEyIDAuMDYyNSAwLjEwNSAwLjA5MTQtMTcuNDQxIDQuMjM0OS0yOC4zOTkgMTAuMzU3LTI4LjM5OSAxNy4xNjYgMCAxMi43ODQgMzguNjI4IDIzLjE0NyA4Ni4yNzggMjMuMTQ3IDQ3LjY0OSAwIDg2LjI3Ni0xMC4zNjQgODYuMjc2LTIzLjE0NyAwLTYuODA4Ni0xMC45NTgtMTIuOTMxLTI4LjM5OS0xNy4xNjYgMC4wMzQtMC4wMjg5IDAuMDcxLTAuMDYxNSAwLjEwNS0wLjA5MTQgMTcuNTY5IDQuMjUyNSAyOC42MTUgMTAuNDEgMjguNjE1IDE3LjI1NyAwIDEyLjgzMS0zOC43NzEgMjMuMjM0LTg2LjU5OCAyMy4yMzQiLz4KICAgPHBhdGggaWQ9InBhdGg0NzI5IiBkPSJtMzQ1LjU4IDk0Ny41M2MtNDcuNjUgMC04Ni4yNzgtMTAuMzY0LTg2LjI3OC0yMy4xNDcgMC02LjgwODYgMTAuOTU4LTEyLjkzMSAyOC4zOTktMTcuMTY2IDAuMDM1IDAuMDMwOCAwLjA2ODcgMC4wNTk1IDAuMTA1IDAuMDkwOC0xNy4zMTEgNC4yMTg3LTI4LjE4MiAxMC4zMDctMjguMTgyIDE3LjA3NSAwIDEyLjczNiAzOC40ODQgMjMuMDYxIDg1Ljk1NiAyMy4wNjEgNDcuNDcxIDAgODUuOTU1LTEwLjMyNSA4NS45NTUtMjMuMDYxIDAtNi43Njg1LTEwLjg2OC0xMi44NTYtMjguMTgxLTE3LjA3NSAwLjAzNS0wLjAzMTIgMC4wNjktMC4wNiAwLjEwMzgtMC4wOTA4IDE3LjQ0MSA0LjIzNDkgMjguMzk5IDEwLjM1NyAyOC4zOTkgMTcuMTY2IDAgMTIuNzg0LTM4LjYyOCAyMy4xNDctODYuMjc2IDIzLjE0NyIvPgogICA8cGF0aCBpZD0icGF0aDQ3MzEiIGQ9Im0zNDUuNTggOTQ3LjQ1Yy00Ny40NzIgMC04NS45NTYtMTAuMzI1LTg1Ljk1Ni0yMy4wNjEgMC02Ljc2ODUgMTAuODcxLTEyLjg1NiAyOC4xODItMTcuMDc1IDAuMDM1IDAuMDMwNCAwLjA2ODcgMC4wNjAxIDAuMTA1IDAuMDkwNC0xNy4xODQgNC4yMDIxLTI3Ljk2NiAxMC4yNTYtMjcuOTY2IDE2Ljk4NSAwIDEyLjY4OCAzOC4zNCAyMi45NzYgODUuNjM1IDIyLjk3NnM4NS42MzUtMTAuMjg4IDg1LjYzNS0yMi45NzZjMC02LjcyOS0xMC43ODQtMTIuNzgzLTI3Ljk2OC0xNi45ODUgMC4wMzYtMC4wMzAzIDAuMDctMC4wNiAwLjEwNjItMC4wOTA0IDE3LjMxNCA0LjIxODggMjguMTgxIDEwLjMwNyAyOC4xODEgMTcuMDc1IDAgMTIuNzM2LTM4LjQ4NCAyMy4wNjEtODUuOTU1IDIzLjA2MSIvPgogICA8cGF0aCBpZD0icGF0aDQ3MzMiIGQ9Im0zNDUuNTggOTQ3LjM2Yy00Ny4yOTUgMC04NS42MzUtMTAuMjg4LTg1LjYzNS0yMi45NzYgMC02LjcyOSAxMC43ODItMTIuNzgzIDI3Ljk2Ni0xNi45ODUgMC4wMzM3IDAuMDI4OSAwLjA3MTIgMC4wNjI1IDAuMTA1IDAuMDkxNC0xNy4wNTQgNC4xODQ1LTI3Ljc1MSAxMC4yMDQtMjcuNzUxIDE2Ljg5MyAwIDEyLjY0MSAzOC4xOTYgMjIuODkgODUuMzE1IDIyLjg5IDQ3LjExOCAwIDg1LjMxNC0xMC4yNDkgODUuMzE0LTIyLjg5IDAtNi42ODktMTAuNjk4LTEyLjcwOS0yNy43NTEtMTYuODk0IDAuMDM0LTAuMDI4OSAwLjA3Mi0wLjA2MjUgMC4xMDUtMC4wOTE0IDE3LjE4NCA0LjIwMjEgMjcuOTY4IDEwLjI1NiAyNy45NjggMTYuOTg1IDAgMTIuNjg4LTM4LjM0IDIyLjk3Ni04NS42MzUgMjIuOTc2Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNDczNSIgZD0ibTM0NS41OCA5NDcuMjhjLTQ3LjExOSAwLTg1LjMxNS0xMC4yNDktODUuMzE1LTIyLjg5IDAtNi42ODkgMTAuNjk4LTEyLjcwOSAyNy43NTEtMTYuODk0IDAuMDM2MiAwLjAyOTggMC4wNyAwLjA1OTUgMC4xMDYyNSAwLjA4OTctMTYuOTI1IDQuMTY5LTI3LjUzNiAxMC4xNTQtMjcuNTM2IDE2LjgwNCAwIDEyLjU5NCAzOC4wNTIgMjIuODA0IDg0Ljk5NCAyMi44MDQgNDYuOTQgMCA4NC45OTQtMTAuMjEgODQuOTk0LTIyLjgwNCAwLTYuNjQ5OS0xMC42MTItMTIuNjM1LTI3LjUzOC0xNi44MDQgMC4wMzYtMC4wMzAzIDAuMDctMC4wNiAwLjEwNjMtMC4wODk3IDE3LjA1NCA0LjE4NDUgMjcuNzUxIDEwLjIwNCAyNy43NTEgMTYuODk0IDAgMTIuNjQxLTM4LjE5NiAyMi44OS04NS4zMTQgMjIuODkiLz4KICAgPHBhdGggaWQ9InBhdGg0NzM3IiBkPSJtMzQ1LjU4IDk0Ny4xOWMtNDYuOTQxIDAtODQuOTk0LTEwLjIxLTg0Ljk5NC0yMi44MDQgMC02LjY0OTkgMTAuNjExLTEyLjYzNSAyNy41MzYtMTYuODA0IDAuMDM1IDAuMDMxMiAwLjA3IDAuMDYwMSAwLjEwNjI1IDAuMDkxNC0xNi43OTYgNC4xNTE0LTI3LjMyMiAxMC4xMDItMjcuMzIyIDE2LjcxMiAwIDEyLjU0NiAzNy45MSAyMi43MTcgODQuNjc0IDIyLjcxNyA0Ni43NjMgMCA4NC42NzMtMTAuMTcxIDg0LjY3My0yMi43MTcgMC02LjYwOTktMTAuNTI2LTEyLjU2MS0yNy4zMjItMTYuNzEyIDAuMDM2LTAuMDMxMiAwLjA3MS0wLjA2MDEgMC4xMDYyLTAuMDkxNCAxNi45MjUgNC4xNjkgMjcuNTM4IDEwLjE1NCAyNy41MzggMTYuODA0IDAgMTIuNTk0LTM4LjA1NCAyMi44MDQtODQuOTk0IDIyLjgwNCIvPgogICA8cGF0aCBpZD0icGF0aDQ3MzkiIGQ9Im0zNDUuNTggOTQ3LjFjLTQ2Ljc2NCAwLTg0LjY3NC0xMC4xNzEtODQuNjc0LTIyLjcxNyAwLTYuNjA5OSAxMC41MjYtMTIuNTYxIDI3LjMyMi0xNi43MTIgMC4wMzYyIDAuMDI5NyAwLjA3IDAuMDYgMC4xMDYyNSAwLjA4OTktMTYuNjY5IDQuMTM1Mi0yNy4xMDggMTAuMDUzLTI3LjEwOCAxNi42MjIgMCAxMi40OTkgMzcuNzY2IDIyLjYzMSA4NC4zNTIgMjIuNjMxczg0LjM1MS0xMC4xMzMgODQuMzUxLTIyLjYzMWMwLTYuNTY5OS0xMC40MzktMTIuNDg3LTI3LjEwOC0xNi42MjIgMC4wMzYtMC4wMjk5IDAuMDcxLTAuMDYwMSAwLjEwNjMtMC4wODk5IDE2Ljc5NiA0LjE1MTQgMjcuMzIyIDEwLjEwMiAyNy4zMjIgMTYuNzEyIDAgMTIuNTQ2LTM3LjkxIDIyLjcxNy04NC42NzMgMjIuNzE3Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNDc0MSIgZD0ibTM0NS41OCA5NDcuMDJjLTQ2LjU4NiAwLTg0LjM1Mi0xMC4xMzMtODQuMzUyLTIyLjYzMSAwLTYuNTY5OSAxMC40MzktMTIuNDg3IDI3LjEwOC0xNi42MjIgMC4wMzYyIDAuMDMxMiAwLjA3MTIgMC4wNjE1IDAuMTA3NSAwLjA5MTMtMTYuNTQxIDQuMTE3Ni0yNi44OTQgMTAtMjYuODk0IDE2LjUzMSAwIDEyLjQ1MSAzNy42MjEgMjIuNTQ1IDg0LjAzMSAyMi41NDUgNDYuNDA5IDAgODQuMDMxLTEwLjA5NCA4NC4wMzEtMjIuNTQ1IDAtNi41MzEyLTEwLjM1NC0xMi40MTQtMjYuODk1LTE2LjUzMSAwLjAzNi0wLjAyOTggMC4wNzEtMC4wNiAwLjEwNzUtMC4wOTEzIDE2LjY2OSA0LjEzNTIgMjcuMTA4IDEwLjA1MyAyNy4xMDggMTYuNjIyIDAgMTIuNDk5LTM3Ljc2NSAyMi42MzEtODQuMzUxIDIyLjYzMSIvPgogICA8cGF0aCBpZD0icGF0aDQ3NDMiIGQ9Im0zNDUuNTggOTQ2LjkzYy00Ni40MSAwLTg0LjAzMS0xMC4wOTQtODQuMDMxLTIyLjU0NSAwLTYuNTMxMiAxMC4zNTItMTIuNDE0IDI2Ljg5NC0xNi41MzEgMC4wMzUgMC4wMjk3IDAuMDcxMiAwLjA2IDAuMTA3NSAwLjA4OTktMTYuNDE1IDQuMTAxNS0yNi42ODEgOS45NTAxLTI2LjY4MSAxNi40NDEgMCAxMi40MDQgMzcuNDc5IDIyLjQ1OSA4My43MTEgMjIuNDU5IDQ2LjIzMSAwIDgzLjcxLTEwLjA1NSA4My43MS0yMi40NTkgMC02LjQ5MTItMTAuMjY2LTEyLjM0LTI2LjY4LTE2LjQ0MSAwLjAzNS0wLjAyOTkgMC4wNzEtMC4wNjAxIDAuMTA2Mi0wLjA4OTkgMTYuNTQxIDQuMTE3NiAyNi44OTUgMTAgMjYuODk1IDE2LjUzMSAwIDEyLjQ1MS0zNy42MjIgMjIuNTQ1LTg0LjAzMSAyMi41NDUiLz4KICAgPHBhdGggaWQ9InBhdGg0NzQ1IiBkPSJtMzQ1LjU4IDk0Ni44NGMtNDYuMjMyIDAtODMuNzExLTEwLjA1NS04My43MTEtMjIuNDU5IDAtNi40OTEyIDEwLjI2Ni0xMi4zNCAyNi42ODEtMTYuNDQxIDAuMDM1IDAuMDMxMiAwLjA3MTIgMC4wNjE1IDAuMTA3NSAwLjA5MTItMTYuMjg5IDQuMDg0LTI2LjQ2OCA5Ljg5NzUtMjYuNDY4IDE2LjM1IDAgMTIuMzU2IDM3LjMzNSAyMi4zNzMgODMuMzkgMjIuMzczczgzLjM4OS0xMC4wMTYgODMuMzg5LTIyLjM3M2MwLTYuNDUyNi0xMC4xNzktMTIuMjY2LTI2LjQ2Ni0xNi4zNSAwLjAzNS0wLjAyOTcgMC4wNzEtMC4wNiAwLjEwNzUtMC4wOTEyIDE2LjQxNCA0LjEwMTUgMjYuNjggOS45NTAxIDI2LjY4IDE2LjQ0MSAwIDEyLjQwNC0zNy40NzkgMjIuNDU5LTgzLjcxIDIyLjQ1OSIvPgogICA8cGF0aCBpZD0icGF0aDQ3NDciIGQ9Im0zNDUuNTggOTQ2Ljc2Yy00Ni4wNTUgMC04My4zOS0xMC4wMTYtODMuMzktMjIuMzczIDAtNi40NTI2IDEwLjE3OS0xMi4yNjYgMjYuNDY4LTE2LjM1IDAuMDM1IDAuMDMwMyAwLjA3MTIgMC4wNjAxIDAuMTA3NSAwLjA5MTQtMTYuMTU5IDQuMDY2NC0yNi4yNTQgOS44NDYxLTI2LjI1NCAxNi4yNTkgMCAxMi4zMDkgMzcuMTkgMjIuMjg4IDgzLjA2OSAyMi4yODggNDUuODc4IDAgODMuMDY5LTkuOTc5IDgzLjA2OS0yMi4yODggMC02LjQxMjYtMTAuMDk2LTEyLjE5Mi0yNi4yNTUtMTYuMjU5IDAuMDM2LTAuMDMxMiAwLjA3Mi0wLjA2MTEgMC4xMDg4LTAuMDkxNCAxNi4yODggNC4wODQgMjYuNDY2IDkuODk3NSAyNi40NjYgMTYuMzUgMCAxMi4zNTYtMzcuMzM0IDIyLjM3My04My4zODkgMjIuMzczIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDc0OSIgZD0ibTM0NS41OCA5NDYuNjdjLTQ1Ljg3OSAwLTgzLjA2OS05Ljk3OS04My4wNjktMjIuMjg4IDAtNi40MTI2IDEwLjA5NS0xMi4xOTIgMjYuMjU0LTE2LjI1OSAwLjAzNjIgMC4wMzAzIDAuMDcyNSAwLjA2IDAuMTA4NzUgMC4wODk4LTE2LjAzMSA0LjA0ODktMjYuMDQyIDkuNzk2NS0yNi4wNDIgMTYuMTY5IDAgMTIuMjYxIDM3LjA0OCAyMi4yMDEgODIuNzQ5IDIyLjIwMSA0NS43IDAgODIuNzQ4LTkuOTQgODIuNzQ4LTIyLjIwMSAwLTYuMzcyNS0xMC4wMTEtMTIuMTItMjYuMDQyLTE2LjE2OSAwLjAzNi0wLjAyOTggMC4wNzItMC4wNTk1IDAuMTA4Ny0wLjA4OTggMTYuMTU5IDQuMDY2NCAyNi4yNTUgOS44NDYxIDI2LjI1NSAxNi4yNTkgMCAxMi4zMDktMzcuMTkxIDIyLjI4OC04My4wNjkgMjIuMjg4Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNDc1MSIgZD0ibTM0NS41OCA5NDYuNTljLTQ1LjcwMSAwLTgyLjc0OS05Ljk0LTgyLjc0OS0yMi4yMDEgMC02LjM3MjUgMTAuMDExLTEyLjEyIDI2LjA0Mi0xNi4xNjkgMC4wMzYzIDAuMDMwNCAwLjA3MjUgMC4wNjE2IDAuMTA4NzUgMC4wOTE0LTE1LjkwNiA0LjAzMTItMjUuODMgOS43NDQxLTI1LjgzIDE2LjA3OCAwIDEyLjIxNCAzNi45MDQgMjIuMTE1IDgyLjQyOCAyMi4xMTUgNDUuNTIzIDAgODIuNDI2LTkuOTAwOSA4Mi40MjYtMjIuMTE1IDAtNi4zMzM1LTkuOTIzNy0xMi4wNDYtMjUuODMtMTYuMDc4IDAuMDM2LTAuMDI5OCAwLjA3NC0wLjA2MSAwLjEwODgtMC4wOTE0IDE2LjAzMSA0LjA0ODkgMjYuMDQyIDkuNzk2NSAyNi4wNDIgMTYuMTY5IDAgMTIuMjYxLTM3LjA0OCAyMi4yMDEtODIuNzQ4IDIyLjIwMSIvPgogICA8cGF0aCBpZD0icGF0aDQ3NTMiIGQ9Im0zNDUuNTggOTQ2LjVjLTQ1LjUyNCAwLTgyLjQyOC05LjkwMDktODIuNDI4LTIyLjExNSAwLTYuMzMzNSA5LjkyMzgtMTIuMDQ2IDI1LjgzLTE2LjA3OCAwLjAzNjIgMC4wMzAzIDAuMDcyNSAwLjA2MTUgMC4xMDg3NSAwLjA5MTMtMTUuNzc5IDQuMDEzNy0yNS42MTkgOS42OTE0LTI1LjYxOSAxNS45ODYgMCAxMi4xNjYgMzYuNzYxIDIyLjAyOSA4Mi4xMDggMjIuMDI5IDQ1LjM0NiAwIDgyLjEwNi05Ljg2MjggODIuMTA2LTIyLjAyOSAwLTYuMjk0OS05Ljg0LTExLjk3My0yNS42MTktMTUuOTg2IDAuMDM2LTAuMDI5NyAwLjA3Mi0wLjA2MSAwLjEwODctMC4wOTEzIDE1LjkwNiA0LjAzMTIgMjUuODMgOS43NDQxIDI1LjgzIDE2LjA3OCAwIDEyLjIxNC0zNi45MDQgMjIuMTE1LTgyLjQyNiAyMi4xMTUiLz4KICAgPHBhdGggaWQ9InBhdGg0NzU1IiBkPSJtMzQ1LjU4IDk0Ni40MWMtNDUuMzQ2IDAtODIuMTA4LTkuODYyOC04Mi4xMDgtMjIuMDI5IDAtNi4yOTQ5IDkuODQtMTEuOTczIDI1LjYxOS0xNS45ODYgMC4wMzYzIDAuMDMwNCAwLjA3MzggMC4wNjAxIDAuMTEgMC4wODk5LTE1LjY1MiAzLjk5NzYtMjUuNDA4IDkuNjQxNi0yNS40MDggMTUuODk2IDAgMTIuMTE5IDM2LjYxNiAyMS45NDIgODEuNzg2IDIxLjk0MiA0NS4xNjkgMCA4MS43ODUtOS44MjM3IDgxLjc4NS0yMS45NDIgMC02LjI1NDktOS43NTM4LTExLjg5OS0yNS40MDYtMTUuODk2IDAuMDM2LTAuMDI5NyAwLjA3Mi0wLjA1OTUgMC4xMDg4LTAuMDg5OSAxNS43NzkgNC4wMTM4IDI1LjYxOSA5LjY5MTUgMjUuNjE5IDE1Ljk4NiAwIDEyLjE2Ni0zNi43NiAyMi4wMjktODIuMTA2IDIyLjAyOSIvPgogICA8cGF0aCBpZD0icGF0aDQ3NTciIGQ9Im0zNDUuNTggOTQ2LjMzYy00NS4xNyAwLTgxLjc4Ni05LjgyMzctODEuNzg2LTIxLjk0MiAwLTYuMjU0OSA5Ljc1NS0xMS44OTkgMjUuNDA4LTE1Ljg5NiAwLjAzNjIgMC4wMzAzIDAuMDcyNSAwLjA2MTUgMC4xMDg3NSAwLjA5MTQtMTUuNTI2IDMuOTc5OS0yNS4xOTUgOS41ODg5LTI1LjE5NSAxNS44MDUgMCAxMi4wNzEgMzYuNDcyIDIxLjg1NiA4MS40NjUgMjEuODU2IDQ0Ljk5MSAwIDgxLjQ2NS05Ljc4NTIgODEuNDY1LTIxLjg1NiAwLTYuMjE2Mi05LjY3LTExLjgyNS0yNS4xOTYtMTUuODA1IDAuMDM2LTAuMDI5OSAwLjA3NC0wLjA2MTEgMC4xMS0wLjA5MTQgMTUuNjUyIDMuOTk3NiAyNS40MDYgOS42NDE2IDI1LjQwNiAxNS44OTYgMCAxMi4xMTktMzYuNjE2IDIxLjk0Mi04MS43ODUgMjEuOTQyIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDc1OSIgZD0ibTM0NS41OCA5NDYuMjRjLTQ0Ljk5MiAwLTgxLjQ2NS05Ljc4NTItODEuNDY1LTIxLjg1NiAwLTYuMjE2MiA5LjY2ODgtMTEuODI1IDI1LjE5NS0xNS44MDUgMC4wMzYzIDAuMDMwMiAwLjA3NSAwLjA2MTUgMC4xMTEyNSAwLjA5MTItMTUuMzk5IDMuOTYxNC0yNC45ODYgOS41Mzc3LTI0Ljk4NiAxNS43MTQgMCAxMi4wMjQgMzYuMzMgMjEuNzcgODEuMTQ1IDIxLjc3IDQ0LjgxNCAwIDgxLjE0NC05Ljc0NjEgODEuMTQ0LTIxLjc3IDAtNi4xNzYyLTkuNTg3NS0xMS43NTItMjQuOTg2LTE1LjcxNCAwLjAzOC0wLjAyOTcgMC4wNzUtMC4wNjEgMC4xMTEyLTAuMDkxMiAxNS41MjYgMy45Nzk5IDI1LjE5NiA5LjU4ODkgMjUuMTk2IDE1LjgwNSAwIDEyLjA3MS0zNi40NzQgMjEuODU2LTgxLjQ2NSAyMS44NTYiLz4KICAgPHBhdGggaWQ9InBhdGg0NzYxIiBkPSJtMzQ1LjU4IDk0Ni4xNmMtNDQuODE1IDAtODEuMTQ1LTkuNzQ2MS04MS4xNDUtMjEuNzcgMC02LjE3NjIgOS41ODc1LTExLjc1MiAyNC45ODYtMTUuNzE0IDAuMDM2MiAwLjAyODkgMC4wNzM3IDAuMDYwMSAwLjExIDAuMDkwNC0xNS4yNzIgMy45NDQ4LTI0Ljc3NSA5LjQ4NTgtMjQuNzc1IDE1LjYyNCAwIDExLjk3NiAzNi4xODYgMjEuNjg1IDgwLjgyNCAyMS42ODVzODAuODIzLTkuNzA5IDgwLjgyMy0yMS42ODVjMC02LjEzNzgtOS41MDI1LTExLjY3OS0yNC43NzUtMTUuNjI0IDAuMDM2LTAuMDMwMyAwLjA3NC0wLjA2MTUgMC4xMS0wLjA5MDQgMTUuMzk5IDMuOTYxNCAyNC45ODYgOS41Mzc2IDI0Ljk4NiAxNS43MTQgMCAxMi4wMjQtMzYuMzMgMjEuNzctODEuMTQ0IDIxLjc3Ii8+CiAgPC9nPgogIDxnPgogICA8cGF0aCBpZD0icGF0aDQ3NjMiIGQ9Im0zNDUuNTggOTQ2LjA3Yy00NC42MzggMC04MC44MjQtOS43MDktODAuODI0LTIxLjY4NSAwLTYuMTM3OCA5LjUwMjUtMTEuNjc5IDI0Ljc3NS0xNS42MjQgMC4wMzg4IDAuMDMwOCAwLjA3MjUgMC4wNTk1IDAuMTExMjUgMC4wOTA3LTE1LjE0NiAzLjkyNzgtMjQuNTY2IDkuNDM1MS0yNC41NjYgMTUuNTMzIDAgMTEuOTI5IDM2LjA0MiAyMS41OTkgODAuNTA0IDIxLjU5OSA0NC40NiAwIDgwLjUwMy05LjY2OTkgODAuNTAzLTIxLjU5OSAwLTYuMDk3Ni05LjQyLTExLjYwNS0yNC41NjYtMTUuNTMzIDAuMDM5LTAuMDMxMiAwLjA3Mi0wLjA2IDAuMTExMy0wLjA5MDcgMTUuMjcyIDMuOTQ0OCAyNC43NzUgOS40ODU4IDI0Ljc3NSAxNS42MjQgMCAxMS45NzYtMzYuMTg1IDIxLjY4NS04MC44MjMgMjEuNjg1IiBmaWxsPSIjZmVmZmZmIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDc2NSIgZD0ibTM0NS41OCA5NDUuOThjLTQ0LjQ2MSAwLTgwLjUwNC05LjY2OTktODAuNTA0LTIxLjU5OSAwLTYuMDk3NiA5LjQyLTExLjYwNSAyNC41NjYtMTUuNTMzIDAuMDM2MiAwLjAzMDMgMC4wNzUgMC4wNjE1IDAuMTExMjUgMC4wOTE0LTE1LjAyMiAzLjkwODYtMjQuMzU2IDkuMzgyNy0yNC4zNTYgMTUuNDQxIDAgMTEuODgxIDM1Ljg5OSAyMS41MTMgODAuMTgyIDIxLjUxM3M4MC4xODEtOS42MzE0IDgwLjE4MS0yMS41MTNjMC02LjA1ODYtOS4zMzM3LTExLjUzMy0yNC4zNTYtMTUuNDQxIDAuMDM2LTAuMDI5OSAwLjA3NS0wLjA2MTEgMC4xMTEyLTAuMDkxNCAxNS4xNDYgMy45Mjc4IDI0LjU2NiA5LjQzNTEgMjQuNTY2IDE1LjUzMyAwIDExLjkyOS0zNi4wNDIgMjEuNTk5LTgwLjUwMyAyMS41OTkiIGZpbGw9IiNmZWZmZmYiLz4KICAgPHBhdGggaWQ9InBhdGg0NzY3IiBkPSJtMzQ1LjU4IDk0NS45Yy00NC4yODQgMC04MC4xODItOS42MzE0LTgwLjE4Mi0yMS41MTMgMC02LjA1ODYgOS4zMzM4LTExLjUzMyAyNC4zNTYtMTUuNDQxIDAuMDM2MiAwLjAyODcgMC4wNzUgMC4wNjE1IDAuMTExMjUgMC4wOTAyLTE0Ljg5NSAzLjg5MjEtMjQuMTQ2IDkuMzMyNi0yNC4xNDYgMTUuMzUxIDAgMTEuODM0IDM1Ljc1NSAyMS40MjYgNzkuODYxIDIxLjQyNnM3OS44NjEtOS41OTIzIDc5Ljg2MS0yMS40MjZjMC02LjAxODUtOS4yNTEyLTExLjQ1OS0yNC4xNDgtMTUuMzUxIDAuMDM2LTAuMDI4NyAwLjA3NS0wLjA2MTUgMC4xMTEzLTAuMDkwMiAxNS4wMjIgMy45MDg2IDI0LjM1NiA5LjM4MjggMjQuMzU2IDE1LjQ0MSAwIDExLjg4MS0zNS44OTkgMjEuNTEzLTgwLjE4MSAyMS41MTMiIGZpbGw9IiNmZWZlZmYiLz4KICAgPHBhdGggaWQ9InBhdGg0NzY5IiBkPSJtMzQ1LjU4IDk0NS44MWMtNDQuMTA2IDAtNzkuODYxLTkuNTkyMy03OS44NjEtMjEuNDI2IDAtNi4wMTg1IDkuMjUxMi0xMS40NTkgMjQuMTQ2LTE1LjM1MSAwLjAzODcgMC4wMzEyIDAuMDczOCAwLjA2MDEgMC4xMTI1IDAuMDkwOS0xNC43NyAzLjg3NTUtMjMuOTM5IDkuMjgwMi0yMy45MzkgMTUuMjYgMCAxMS43ODYgMzUuNjExIDIxLjM0IDc5LjU0MSAyMS4zNCA0My45MjkgMCA3OS41NC05LjU1MzcgNzkuNTQtMjEuMzQgMC01Ljk4LTkuMTY3NS0xMS4zODUtMjMuOTM5LTE1LjI2IDAuMDM5LTAuMDMwOCAwLjA3NC0wLjA1OTYgMC4xMTI1LTAuMDkwOSAxNC44OTYgMy44OTIxIDI0LjE0OCA5LjMzMjYgMjQuMTQ4IDE1LjM1MSAwIDExLjgzNC0zNS43NTUgMjEuNDI2LTc5Ljg2MSAyMS40MjYiIGZpbGw9IiNmZWZlZmUiLz4KICAgPHBhdGggaWQ9InBhdGg0NzcxIiBkPSJtMzQ1LjU4IDk0NS43M2MtNDMuOTMgMC03OS41NDEtOS41NTM3LTc5LjU0MS0yMS4zNCAwLTUuOTggOS4xNjg4LTExLjM4NSAyMy45MzktMTUuMjYgMC4wMzYyIDAuMDMwMyAwLjA3NjIgMC4wNjE1IDAuMTEyNSAwLjA5MTMtMTQuNjQ1IDMuODU2NS0yMy43MyA5LjIyNzYtMjMuNzMgMTUuMTY5IDAgMTEuNzM5IDM1LjQ2OCAyMS4yNTUgNzkuMjIgMjEuMjU1IDQzLjc1MSAwIDc5LjIxOS05LjUxNjEgNzkuMjE5LTIxLjI1NSAwLTUuOTQxNC05LjA4MzctMTEuMzEyLTIzLjczLTE1LjE2OSAwLjAzNi0wLjAyOTggMC4wNzYtMC4wNjEgMC4xMTI1LTAuMDkxMyAxNC43NzEgMy44NzU1IDIzLjkzOSA5LjI4MDIgMjMuOTM5IDE1LjI2IDAgMTEuNzg2LTM1LjYxMSAyMS4zNC03OS41NCAyMS4zNCIgZmlsbD0iI2ZlZmVmZSIvPgogICA8cGF0aCBpZD0icGF0aDQ3NzMiIGQ9Im0zNDUuNTggOTQ1LjY0Yy00My43NTIgMC03OS4yMi05LjUxNjEtNzkuMjItMjEuMjU1IDAtNS45NDE0IDkuMDg1LTExLjMxMiAyMy43My0xNS4xNjkgMC4wMzg4IDAuMDMxMiAwLjA3MzggMC4wNTkxIDAuMTEyNSAwLjA5MDQtMTQuNTIxIDMuODM5OS0yMy41MjIgOS4xNzcyLTIzLjUyMiAxNS4wNzkgMCAxMS42OTEgMzUuMzI1IDIxLjE2OSA3OC45IDIxLjE2OSA0My41NzQgMCA3OC44OTktOS40Nzc2IDc4Ljg5OS0yMS4xNjkgMC01LjkwMTQtOS4wMDEyLTExLjIzOS0yMy41MjItMTUuMDc5IDAuMDM5LTAuMDMxMiAwLjA3NC0wLjA1OTEgMC4xMTI1LTAuMDkwNCAxNC42NDYgMy44NTY1IDIzLjczIDkuMjI3NiAyMy43MyAxNS4xNjkgMCAxMS43MzktMzUuNDY4IDIxLjI1NS03OS4yMTkgMjEuMjU1IiBmaWxsPSIjZmRmZWZlIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDc3NSIgZD0ibTM0NS41OCA5NDUuNTVjLTQzLjU3NSAwLTc4LjktOS40Nzc2LTc4LjktMjEuMTY5IDAtNS45MDE0IDkuMDAxMi0xMS4yMzkgMjMuNTIyLTE1LjA3OSAwLjAzNzUgMC4wMjk3IDAuMDc3NSAwLjA2MjUgMC4xMTM3NSAwLjA5MTQtMTQuMzk1IDMuODIwOC0yMy4zMTUgOS4xMjUtMjMuMzE1IDE0Ljk4NyAwIDExLjY0NCAzNS4xODEgMjEuMDgzIDc4LjU3OSAyMS4wODNzNzguNTc4LTkuNDM5IDc4LjU3OC0yMS4wODNjMC01Ljg2MjItOC45Mi0xMS4xNjYtMjMuMzE1LTE0Ljk4NyAwLjAzOC0wLjAyODkgMC4wNzYtMC4wNjE2IDAuMTEzNy0wLjA5MTQgMTQuNTIxIDMuODM5OSAyMy41MjIgOS4xNzcyIDIzLjUyMiAxNS4wNzkgMCAxMS42OTEtMzUuMzI1IDIxLjE2OS03OC44OTkgMjEuMTY5IiBmaWxsPSIjZmRmZWZlIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDc3NyIgZD0ibTM0NS41OCA5NDUuNDdjLTQzLjM5OCAwLTc4LjU3OS05LjQzOS03OC41NzktMjEuMDgzIDAtNS44NjIyIDguOTItMTEuMTY2IDIzLjMxNS0xNC45ODcgMC4wMzg3IDAuMDMxMiAwLjA3NSAwLjA2IDAuMTEzNzUgMC4wOTA3LTE0LjI3MSAzLjgwNDItMjMuMTA4IDkuMDcyOC0yMy4xMDggMTQuODk2IDAgMTEuNTk2IDM1LjAzNiAyMC45OTYgNzguMjU4IDIwLjk5NiA0My4yMiAwIDc4LjI1OC05LjM5OTkgNzguMjU4LTIwLjk5NiAwLTUuODIzOC04LjgzNjMtMTEuMDkyLTIzLjEwOS0xNC44OTYgMC4wMzktMC4wMzA4IDAuMDc1LTAuMDU5NSAwLjExMzgtMC4wOTA3IDE0LjM5NSAzLjgyMDggMjMuMzE1IDkuMTI1IDIzLjMxNSAxNC45ODcgMCAxMS42NDQtMzUuMTggMjEuMDgzLTc4LjU3OCAyMS4wODMiIGZpbGw9IiNmZGZkZmUiLz4KICAgPHBhdGggaWQ9InBhdGg0Nzc5IiBkPSJtMzQ1LjU4IDk0NS4zOGMtNDMuMjIxIDAtNzguMjU4LTkuMzk5OS03OC4yNTgtMjAuOTk2IDAtNS44MjM4IDguODM2Mi0xMS4wOTIgMjMuMTA4LTE0Ljg5NiAwLjAzNjIgMC4wMjg5IDAuMDc3NSAwLjA2MTUgMC4xMTM3NSAwLjA5MTQtMTQuMTQ2IDMuNzg1MS0yMi45MDEgOS4wMjE0LTIyLjkwMSAxNC44MDUgMCAxMS41NDkgMzQuODk0IDIwLjkxIDc3LjkzOCAyMC45MSA0My4wNDMgMCA3Ny45MzYtOS4zNjEzIDc3LjkzNi0yMC45MSAwLTUuNzgzOC04Ljc1NS0xMS4wMi0yMi45MDEtMTQuODA1IDAuMDM2LTAuMDI5OSAwLjA3OC0wLjA2MjUgMC4xMTM3LTAuMDkxNCAxNC4yNzIgMy44MDQyIDIzLjEwOSA5LjA3MjggMjMuMTA5IDE0Ljg5NiAwIDExLjU5Ni0zNS4wMzggMjAuOTk2LTc4LjI1OCAyMC45OTYiIGZpbGw9IiNmZGZkZmQiLz4KICAgPHBhdGggaWQ9InBhdGg0NzgxIiBkPSJtMzQ1LjU4IDk0NS4zYy00My4wNDQgMC03Ny45MzgtOS4zNjEzLTc3LjkzOC0yMC45MSAwLTUuNzgzOCA4Ljc1NS0xMS4wMiAyMi45MDEtMTQuODA1IDAuMDM4NyAwLjAzMTIgMC4wNzYyIDAuMDYgMC4xMTUgMC4wOTAyLTE0LjAyNSAzLjc2ODYtMjIuNjk1IDguOTY5Ny0yMi42OTUgMTQuNzE1IDAgMTEuNTAxIDM0Ljc1IDIwLjgyNCA3Ny42MTYgMjAuODI0IDQyLjg2NSAwIDc3LjYxNS05LjMyMjMgNzcuNjE1LTIwLjgyNCAwLTUuNzQ1MS04LjY3LTEwLjk0Ni0yMi42OTUtMTQuNzE1IDAuMDQtMC4wMzAzIDAuMDc2LTAuMDU5IDAuMTE1LTAuMDkwMiAxNC4xNDYgMy43ODUxIDIyLjkwMSA5LjAyMTQgMjIuOTAxIDE0LjgwNSAwIDExLjU0OS0zNC44OTQgMjAuOTEtNzcuOTM2IDIwLjkxIiBmaWxsPSIjZmRmZGZkIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDc4MyIgZD0ibTM0NS41OCA5NDUuMjFjLTQyLjg2NiAwLTc3LjYxNi05LjMyMjMtNzcuNjE2LTIwLjgyNCAwLTUuNzQ1MSA4LjY3LTEwLjk0NiAyMi42OTUtMTQuNzE1IDAuMDM2MiAwLjAyOTkgMC4wNzc1IDAuMDYyNSAwLjExNSAwLjA5MTQtMTMuOTAxIDMuNzUtMjIuNDkgOC45MTc1LTIyLjQ5IDE0LjYyNCAwIDExLjQ1NCAzNC42MDggMjAuNzM3IDc3LjI5NiAyMC43MzcgNDIuNjg5IDAgNzcuMjk1LTkuMjgzNyA3Ny4yOTUtMjAuNzM3IDAtNS43MDYtOC41ODg4LTEwLjg3NC0yMi40OS0xNC42MjQgMC4wMzgtMC4wMjg5IDAuMDc5LTAuMDYxNSAwLjExNS0wLjA5MTQgMTQuMDI1IDMuNzY4NiAyMi42OTUgOC45Njk3IDIyLjY5NSAxNC43MTUgMCAxMS41MDEtMzQuNzUgMjAuODI0LTc3LjYxNSAyMC44MjQiIGZpbGw9IiNmY2ZkZmQiLz4KICAgPHBhdGggaWQ9InBhdGg0Nzg1IiBkPSJtMzQ1LjU4IDk0NS4xMmMtNDIuNjg5IDAtNzcuMjk2LTkuMjgzNy03Ny4yOTYtMjAuNzM3IDAtNS43MDYgOC41ODg4LTEwLjg3NCAyMi40OS0xNC42MjQgMC4wMzg3IDAuMDMxMiAwLjA3NjMgMC4wNTk1IDAuMTE1IDAuMDkwNy0xMy43NzggMy43MzE1LTIyLjI4NCA4Ljg2NTItMjIuMjg0IDE0LjUzMyAwIDExLjQwNiAzNC40NjIgMjAuNjUyIDc2Ljk3NSAyMC42NTIgNDIuNTExIDAgNzYuOTc0LTkuMjQ2MSA3Ni45NzQtMjAuNjUyIDAtNS42Njc1LTguNTA2Mi0xMC44MDEtMjIuMjg0LTE0LjUzMyAwLjA0LTAuMDMxMiAwLjA3Ni0wLjA1OTUgMC4xMTUtMC4wOTA3IDEzLjkwMSAzLjc1IDIyLjQ5IDguOTE3NSAyMi40OSAxNC42MjQgMCAxMS40NTQtMzQuNjA2IDIwLjczNy03Ny4yOTUgMjAuNzM3IiBmaWxsPSIjZmNmZGZkIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDc4NyIgZD0ibTM0NS41OCA5NDUuMDRjLTQyLjUxMiAwLTc2Ljk3NS05LjI0NjEtNzYuOTc1LTIwLjY1MiAwLTUuNjY3NSA4LjUwNjItMTAuODAxIDIyLjI4NC0xNC41MzMgMC4wMzg3IDAuMDMxMiAwLjA3NjIgMC4wNjAxIDAuMTE2MjUgMC4wOTE0LTEzLjY1MiAzLjcxMzktMjIuMDc5IDguODEzOS0yMi4wNzkgMTQuNDQxIDAgMTEuMzU5IDM0LjMxOSAyMC41NjYgNzYuNjU0IDIwLjU2NiA0Mi4zMzQgMCA3Ni42NTQtOS4yMDc1IDc2LjY1NC0yMC41NjYgMC01LjYyNzUtOC40Mjc1LTEwLjcyOC0yMi4wNzktMTQuNDQxIDAuMDM5LTAuMDMxMiAwLjA3Ni0wLjA2MDEgMC4xMTUtMC4wOTE0IDEzLjc3OCAzLjczMTUgMjIuMjg0IDguODY1MiAyMi4yODQgMTQuNTMzIDAgMTEuNDA2LTM0LjQ2MiAyMC42NTItNzYuOTc0IDIwLjY1MiIgZmlsbD0iI2ZjZmNmZCIvPgogICA8cGF0aCBpZD0icGF0aDQ3ODkiIGQ9Im0zNDUuNTggOTQ0Ljk1Yy00Mi4zMzUgMC03Ni42NTQtOS4yMDc1LTc2LjY1NC0yMC41NjYgMC01LjYyNzUgOC40MjYyLTEwLjcyOCAyMi4wNzktMTQuNDQxIDAuMDM2MiAwLjAyODcgMC4wNzg3IDAuMDYyNSAwLjExNSAwLjA5MTItMTMuNTI5IDMuNjk1NC0yMS44NzQgOC43NjEyLTIxLjg3NCAxNC4zNSAwIDExLjMxMSAzNC4xNzYgMjAuNDggNzYuMzM0IDIwLjQ4czc2LjMzMy05LjE2ODkgNzYuMzMzLTIwLjQ4YzAtNS41ODg5LTguMzQ1LTEwLjY1NS0yMS44NzQtMTQuMzUgMC4wMzYtMC4wMjg3IDAuMDc5LTAuMDYyNSAwLjExNjMtMC4wOTEyIDEzLjY1MSAzLjcxMzkgMjIuMDc5IDguODEzOSAyMi4wNzkgMTQuNDQxIDAgMTEuMzU5LTM0LjMyIDIwLjU2Ni03Ni42NTQgMjAuNTY2IiBmaWxsPSIjZmNmY2ZjIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDc5MSIgZD0ibTM0NS41OCA5NDQuODdjLTQyLjE1OCAwLTc2LjMzNC05LjE2ODktNzYuMzM0LTIwLjQ4IDAtNS41ODg5IDguMzQ1LTEwLjY1NSAyMS44NzQtMTQuMzUgMC4wNCAwLjAzMDMgMC4wNzc1IDAuMDYwMSAwLjExNzUgMC4wOTE0LTEzLjQwNSAzLjY3NjItMjEuNjcgOC43MDktMjEuNjcgMTQuMjU5IDAgMTEuMjY0IDM0LjAzMSAyMC4zOTQgNzYuMDEyIDIwLjM5NCA0MS45OCAwIDc2LjAxMS05LjEyOTkgNzYuMDExLTIwLjM5NCAwLTUuNTQ5OC04LjI2NS0xMC41ODItMjEuNjY5LTE0LjI1OSAwLjAzOS0wLjAzMTIgMC4wNzYtMC4wNjExIDAuMTE2Mi0wLjA5MTQgMTMuNTI5IDMuNjk1NCAyMS44NzQgOC43NjEyIDIxLjg3NCAxNC4zNSAwIDExLjMxMS0zNC4xNzUgMjAuNDgtNzYuMzMzIDIwLjQ4IiBmaWxsPSIjZmJmY2ZjIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDc5MyIgZD0ibTM0NS41OCA5NDQuNzhjLTQxLjk4MSAwLTc2LjAxMi05LjEyOTktNzYuMDEyLTIwLjM5NCAwLTUuNTQ5OCA4LjI2NS0xMC41ODIgMjEuNjctMTQuMjU5IDAuMDM4NyAwLjAyOTggMC4wNzc1IDAuMDYgMC4xMTYyNSAwLjA4OTgtMTMuMjg0IDMuNjYwMi0yMS40NjUgOC42NTc4LTIxLjQ2NSAxNC4xNjkgMCAxMS4yMTYgMzMuODg4IDIwLjMwOCA3NS42OTEgMjAuMzA4czc1LjY5MS05LjA5MTQgNzUuNjkxLTIwLjMwOGMwLTUuNTExMi04LjE4MjUtMTAuNTA5LTIxLjQ2Ni0xNC4xNjkgMC4wMzktMC4wMjk4IDAuMDc4LTAuMDYgMC4xMTc1LTAuMDg5OCAxMy40MDQgMy42NzYyIDIxLjY2OSA4LjcwOSAyMS42NjkgMTQuMjU5IDAgMTEuMjY0LTM0LjAzMSAyMC4zOTQtNzYuMDExIDIwLjM5NCIgZmlsbD0iI2ZiZmNmYyIvPgogICA8cGF0aCBpZD0icGF0aDQ3OTUiIGQ9Im0zNDUuNTggOTQ0LjY5Yy00MS44MDQgMC03NS42OTEtOS4wOTE0LTc1LjY5MS0yMC4zMDggMC01LjUxMTIgOC4xODEyLTEwLjUwOSAyMS40NjUtMTQuMTY5IDAuMDQgMC4wMzEyIDAuMDc4NyAwLjA2MTYgMC4xMTc1IDAuMDkxNC0xMy4xNjEgMy42NDExLTIxLjI2MiA4LjYwNS0yMS4yNjIgMTQuMDc4IDAgMTEuMTY5IDMzLjc0NSAyMC4yMjEgNzUuMzcxIDIwLjIyMXM3NS4zNy05LjA1MjIgNzUuMzctMjAuMjIxYzAtNS40NzI2LTguMTAxMy0xMC40MzYtMjEuMjYyLTE0LjA3OCAwLjA0LTAuMDI5OCAwLjA3OC0wLjA2MDEgMC4xMTc1LTAuMDkxNCAxMy4yODQgMy42NjAyIDIxLjQ2NiA4LjY1NzggMjEuNDY2IDE0LjE2OSAwIDExLjIxNi0zMy44ODkgMjAuMzA4LTc1LjY5MSAyMC4zMDgiIGZpbGw9IiNmYmZiZmIiLz4KICAgPHBhdGggaWQ9InBhdGg0Nzk3IiBkPSJtMzQ1LjU4IDk0NC42MWMtNDEuNjI2IDAtNzUuMzcxLTkuMDUyMi03NS4zNzEtMjAuMjIxIDAtNS40NzI2IDguMTAxMi0xMC40MzYgMjEuMjYyLTE0LjA3OCAwLjA0IDAuMDMxMiAwLjA3ODcgMC4wNjE1IDAuMTE3NSAwLjA5MTMtMTMuMDM5IDMuNjIyNi0yMS4wNTkgOC41NTI4LTIxLjA1OSAxMy45ODYgMCAxMS4xMiAzMy42IDIwLjEzNiA3NS4wNSAyMC4xMzYgNDEuNDQ5IDAgNzUuMDQ5LTkuMDE2MSA3NS4wNDktMjAuMTM2IDAtNS40MzM2LTguMDItMTAuMzY0LTIxLjA1OS0xMy45ODYgMC4wMzktMC4wMjk3IDAuMDc5LTAuMDYgMC4xMTc1LTAuMDkxMyAxMy4xNjEgMy42NDExIDIxLjI2MiA4LjYwNSAyMS4yNjIgMTQuMDc4IDAgMTEuMTY5LTMzLjc0NCAyMC4yMjEtNzUuMzcgMjAuMjIxIiBmaWxsPSIjZmJmYmZiIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDc5OSIgZD0ibTM0NS41OCA5NDQuNTJjLTQxLjQ1IDAtNzUuMDUtOS4wMTYxLTc1LjA1LTIwLjEzNiAwLTUuNDMzNiA4LjAyLTEwLjM2NCAyMS4wNTktMTMuOTg2IDAuMDQgMC4wMzA0IDAuMDc4OCAwLjA2MTYgMC4xMTg3NSAwLjA5MTQtMTIuOTE4IDMuNjA0LTIwLjg1OCA4LjUtMjAuODU4IDEzLjg5NSAwIDExLjA3MiAzMy40NTggMjAuMDUgNzQuNzMgMjAuMDUgNDEuMjcxIDAgNzQuNzI5LTguOTc3NiA3NC43MjktMjAuMDUgMC01LjM5NS03Ljk0LTEwLjI5MS0yMC44NTgtMTMuODk1IDAuMDQtMC4wMjk4IDAuMDc5LTAuMDYxIDAuMTE4OC0wLjA5MTQgMTMuMDM5IDMuNjIyNiAyMS4wNTkgOC41NTI4IDIxLjA1OSAxMy45ODYgMCAxMS4xMi0zMy42IDIwLjEzNi03NS4wNDkgMjAuMTM2IiBmaWxsPSIjZmFmYmZiIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDgwMSIgZD0ibTM0NS41OCA5NDQuNDRjLTQxLjI3MiAwLTc0LjczLTguOTc3Ni03NC43My0yMC4wNSAwLTUuMzk1IDcuOTQtMTAuMjkxIDIwLjg1OC0xMy44OTUgMC4wNCAwLjAyOTcgMC4wNzg4IDAuMDYxNSAwLjExODc1IDAuMDkxMi0xMi43OTQgMy41ODUtMjAuNjU1IDguNDQ4OC0yMC42NTUgMTMuODA0IDAgMTEuMDI1IDMzLjMxNCAxOS45NjQgNzQuNDA5IDE5Ljk2NCA0MS4wOTQgMCA3NC40MDgtOC45MzkgNzQuNDA4LTE5Ljk2NCAwLTUuMzU2NS03Ljg1ODgtMTAuMjE5LTIwLjY1NS0xMy44MDQgMC4wNC0wLjAyOTggMC4wOC0wLjA2MTUgMC4xMTg3LTAuMDkxMiAxMi45MTggMy42MDQgMjAuODU4IDguNSAyMC44NTggMTMuODk1IDAgMTEuMDcyLTMzLjQ1OCAyMC4wNS03NC43MjkgMjAuMDUiIGZpbGw9IiNmYWZiZmIiLz4KICAgPHBhdGggaWQ9InBhdGg0ODAzIiBkPSJtMzQ1LjU4IDk0NC4zNWMtNDEuMDk1IDAtNzQuNDA5LTguOTM5LTc0LjQwOS0xOS45NjQgMC01LjM1NSA3Ljg2MTItMTAuMjE5IDIwLjY1NS0xMy44MDQgMC4wNCAwLjAyOTkgMC4wNzg3IDAuMDYxMSAwLjExODc1IDAuMDkxNC0xMi42NzIgMy41NjU5LTIwLjQ1MiA4LjM5Ni0yMC40NTIgMTMuNzEyIDAgMTAuOTc4IDMzLjE3IDE5Ljg3NyA3NC4wODggMTkuODc3czc0LjA4OC04Ljg5OTkgNzQuMDg4LTE5Ljg3N2MwLTUuMzE2NC03Ljc4MTMtMTAuMTQ2LTIwLjQ1NC0xMy43MTIgMC4wNC0wLjAzMDIgMC4wOC0wLjA2MTUgMC4xMTg4LTAuMDkxNCAxMi43OTYgMy41ODUgMjAuNjU1IDguNDQ3MiAyMC42NTUgMTMuODA0IDAgMTEuMDI1LTMzLjMxNCAxOS45NjQtNzQuNDA4IDE5Ljk2NCIgZmlsbD0iI2ZhZmFmYSIvPgogICA8cGF0aCBpZD0icGF0aDQ4MDUiIGQ9Im0zNDUuNTggOTQ0LjI2Yy00MC45MTggMC03NC4wODgtOC44OTk5LTc0LjA4OC0xOS44NzcgMC01LjMxNjQgNy43OC0xMC4xNDYgMjAuNDUyLTEzLjcxMiAwLjA0IDAuMDI5NyAwLjA4IDAuMDYxIDAuMTIgMC4wOTEzLTEyLjU1IDMuNTQ3NC0yMC4yNTIgOC4zNDM4LTIwLjI1MiAxMy42MjEgMCAxMC45MyAzMy4wMjYgMTkuNzkxIDczLjc2OCAxOS43OTEgNDAuNzQgMCA3My43NjYtOC44NjA5IDczLjc2Ni0xOS43OTEgMC01LjI3NzQtNy43MDEyLTEwLjA3NC0yMC4yNTItMTMuNjIxIDAuMDQtMC4wMzAzIDAuMDgtMC4wNjE1IDAuMTItMC4wOTEzIDEyLjY3MiAzLjU2NTkgMjAuNDU0IDguMzk2IDIwLjQ1NCAxMy43MTIgMCAxMC45NzgtMzMuMTcgMTkuODc3LTc0LjA4OCAxOS44NzciIGZpbGw9IiNmYWZhZmEiLz4KICAgPHBhdGggaWQ9InBhdGg0ODA3IiBkPSJtMzQ1LjU4IDk0NC4xOGMtNDAuNzQxIDAtNzMuNzY4LTguODYwOS03My43NjgtMTkuNzkxIDAtNS4yNzc0IDcuNzAyNS0xMC4wNzQgMjAuMjUyLTEzLjYyMSAwLjA0IDAuMDI5OSAwLjA4IDAuMDYxMSAwLjEyIDAuMDkxNC0xMi40MzEgMy41MjgyLTIwLjA1MSA4LjI5MS0yMC4wNTEgMTMuNTMgMCAxMC44ODIgMzIuODgyIDE5LjcwNSA3My40NDYgMTkuNzA1IDQwLjU2MyAwIDczLjQ0NS04LjgyMjcgNzMuNDQ1LTE5LjcwNSAwLTUuMjM4OC03LjYxODgtMTAuMDAyLTIwLjA1MS0xMy41MyAwLjA0LTAuMDMwMyAwLjA4MS0wLjA2MTUgMC4xMi0wLjA5MTQgMTIuNTUxIDMuNTQ3NCAyMC4yNTIgOC4zNDM4IDIwLjI1MiAxMy42MjEgMCAxMC45My0zMy4wMjYgMTkuNzkxLTczLjc2NiAxOS43OTEiIGZpbGw9IiNmOWZhZmEiLz4KICAgPHBhdGggaWQ9InBhdGg0ODA5IiBkPSJtMzQ1LjU4IDk0NC4wOWMtNDAuNTY0IDAtNzMuNDQ2LTguODIyNy03My40NDYtMTkuNzA1IDAtNS4yMzg4IDcuNjItMTAuMDAyIDIwLjA1MS0xMy41MyAwLjA0IDAuMDI5OCAwLjA4IDAuMDYxIDAuMTIgMC4wOTA3LTEyLjMxIDMuNTEwMi0xOS44NTEgOC4yMzg4LTE5Ljg1MSAxMy40MzkgMCAxMC44MzUgMzIuNzQgMTkuNjE5IDczLjEyNiAxOS42MTkgNDAuMzg1IDAgNzMuMTI1LTguNzgzNiA3My4xMjUtMTkuNjE5IDAtNS4yMDAyLTcuNTQxMy05LjkyODgtMTkuODUxLTEzLjQzOSAwLjA0LTAuMDI5NyAwLjA4MS0wLjA2MSAwLjEyLTAuMDkwNyAxMi40MzIgMy41MjgyIDIwLjA1MSA4LjI5MSAyMC4wNTEgMTMuNTMgMCAxMC44ODItMzIuODgyIDE5LjcwNS03My40NDUgMTkuNzA1IiBmaWxsPSIjZjlmOWY5Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNDgxMSIgZD0ibTM0NS41OCA5NDRjLTQwLjM4NiAwLTczLjEyNi04Ljc4MzYtNzMuMTI2LTE5LjYxOSAwLTUuMjAwMiA3LjU0MTItOS45Mjg4IDE5Ljg1MS0xMy40MzkgMC4wNCAwLjAzMDMgMC4wODEzIDAuMDYxNSAwLjEyMTI1IDAuMDkxNC0xMi4xODggMy40OTExLTE5LjY1MSA4LjE4NjUtMTkuNjUxIDEzLjM0OCAwIDEwLjc4OCAzMi41OTYgMTkuNTM0IDcyLjgwNSAxOS41MzRzNzIuODA0LTguNzQ2MSA3Mi44MDQtMTkuNTM0YzAtNS4xNjExLTcuNDYzNy05Ljg1NjUtMTkuNjUxLTEzLjM0OCAwLjA0LTAuMDMxMiAwLjA4MS0wLjA2MTEgMC4xMjEyLTAuMDkxNCAxMi4zMSAzLjUxMDIgMTkuODUxIDguMjM4OCAxOS44NTEgMTMuNDM5IDAgMTAuODM1LTMyLjc0IDE5LjYxOS03My4xMjUgMTkuNjE5IiBmaWxsPSIjZjlmOWY5Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNDgxMyIgZD0ibTM0NS41OCA5NDMuOTJjLTQwLjIwOSAwLTcyLjgwNS04Ljc0NjEtNzIuODA1LTE5LjUzNCAwLTUuMTYxMSA3LjQ2MzgtOS44NTY1IDE5LjY1MS0xMy4zNDggMC4wNDEyIDAuMDMxMiAwLjA3ODggMC4wNTkgMC4xMjEyNSAwLjA5MTItMTIuMDY4IDMuNDcyNi0xOS40NTEgOC4xMzM5LTE5LjQ1MSAxMy4yNTYgMCAxMC43NCAzMi40NTEgMTkuNDQ4IDcyLjQ4NCAxOS40NDggNDAuMDMxIDAgNzIuNDg0LTguNzA3NSA3Mi40ODQtMTkuNDQ4IDAtNS4xMjI1LTcuMzg1LTkuNzgzOC0xOS40NTItMTMuMjU2IDAuMDQzLTAuMDMyMiAwLjA4LTAuMDYgMC4xMjEzLTAuMDkxMiAxMi4xODggMy40OTExIDE5LjY1MSA4LjE4NjUgMTkuNjUxIDEzLjM0OCAwIDEwLjc4OC0zMi41OTUgMTkuNTM0LTcyLjgwNCAxOS41MzQiIGZpbGw9IiNmOGY5ZjkiLz4KICAgPHBhdGggaWQ9InBhdGg0ODE1IiBkPSJtMzQ1LjU4IDk0My44M2MtNDAuMDMyIDAtNzIuNDg0LTguNzA3NS03Mi40ODQtMTkuNDQ4IDAtNS4xMjI1IDcuMzgzOC05Ljc4MzggMTkuNDUxLTEzLjI1NiAwLjA0IDAuMDMwMyAwLjA4MTMgMC4wNjE1IDAuMTIxMjUgMC4wOTE0LTExLjk0OSAzLjQ1MzYtMTkuMjUyIDguMDgtMTkuMjUyIDEzLjE2NSAwIDEwLjY5MiAzMi4zMDkgMTkuMzYxIDcyLjE2NCAxOS4zNjEgMzkuODU0IDAgNzIuMTYzLTguNjY5IDcyLjE2My0xOS4zNjEgMC01LjA4NS03LjMwMzgtOS43MTE0LTE5LjI1Mi0xMy4xNjUgMC4wNC0wLjAyOTkgMC4wODEtMC4wNjExIDAuMTIxMi0wLjA5MTQgMTIuMDY4IDMuNDcyNiAxOS40NTIgOC4xMzM5IDE5LjQ1MiAxMy4yNTYgMCAxMC43NC0zMi40NTIgMTkuNDQ4LTcyLjQ4NCAxOS40NDgiIGZpbGw9IiNmOGY4ZjgiLz4KICAgPHBhdGggaWQ9InBhdGg0ODE3IiBkPSJtMzQ1LjU4IDk0My43NWMtMzkuODU1IDAtNzIuMTY0LTguNjY5LTcyLjE2NC0xOS4zNjEgMC01LjA4NSA3LjMwMzgtOS43MTE0IDE5LjI1Mi0xMy4xNjUgMC4wNCAwLjAyODggMC4wODI1IDAuMDYxNSAwLjEyMjUgMC4wOTEzLTExLjgyOCAzLjQzNTEtMTkuMDU0IDguMDI3NC0xOS4wNTQgMTMuMDc0IDAgMTAuNjQ1IDMyLjE2NSAxOS4yNzUgNzEuODQyIDE5LjI3NSAzOS42NzYgMCA3MS44NDEtOC42Mjk5IDcxLjg0MS0xOS4yNzUgMC01LjA0NjQtNy4yMjUtOS42Mzg2LTE5LjA1NC0xMy4wNzQgMC4wNC0wLjAyOTggMC4wODItMC4wNjI1IDAuMTIyNS0wLjA5MTMgMTEuOTQ5IDMuNDUzNiAxOS4yNTIgOC4wOCAxOS4yNTIgMTMuMTY1IDAgMTAuNjkyLTMyLjMwOSAxOS4zNjEtNzIuMTYzIDE5LjM2MSIgZmlsbD0iI2Y4ZjhmOCIvPgogICA8cGF0aCBpZD0icGF0aDQ4MTkiIGQ9Im0zNDUuNTggOTQzLjY2Yy0zOS42NzggMC03MS44NDItOC42Mjk5LTcxLjg0Mi0xOS4yNzUgMC01LjA0NjQgNy4yMjYyLTkuNjM4NiAxOS4wNTQtMTMuMDc0IDAuMDQyNSAwLjAzMTIgMC4wOCAwLjA1ODYgMC4xMjI1IDAuMDkxNC0xMS43MDggMy40MTYtMTguODU2IDcuOTc1LTE4Ljg1NiAxMi45ODIgMCAxMC41OTggMzIuMDIxIDE5LjE4OSA3MS41MjIgMTkuMTg5IDM5LjUgMCA3MS41MjEtOC41OTEzIDcxLjUyMS0xOS4xODkgMC01LjAwNzQtNy4xNDg3LTkuNTY2NC0xOC44NTYtMTIuOTgyIDAuMDQzLTAuMDMyNyAwLjA4LTAuMDYwMSAwLjEyMjUtMC4wOTE0IDExLjgyOSAzLjQzNTEgMTkuMDU0IDguMDI3NCAxOS4wNTQgMTMuMDc0IDAgMTAuNjQ1LTMyLjE2NSAxOS4yNzUtNzEuODQxIDE5LjI3NSIgZmlsbD0iI2Y3ZjhmOCIvPgogICA8cGF0aCBpZD0icGF0aDQ4MjEiIGQ9Im0zNDUuNTggOTQzLjU3Yy0zOS41MDEgMC03MS41MjItOC41OTEzLTcxLjUyMi0xOS4xODkgMC01LjAwNzQgNy4xNDg4LTkuNTY2NCAxOC44NTYtMTIuOTgyIDAuMDQgMC4wMjg4IDAuMDgzNyAwLjA2MSAwLjEyMzc1IDAuMDkxMy0xMS41ODkgMy4zOTc1LTE4LjY1OSA3LjkyMjQtMTguNjU5IDEyLjg5MSAwIDEwLjU1IDMxLjg3OCAxOS4xMDMgNzEuMjAxIDE5LjEwM3M3MS4yLTguNTUyOCA3MS4yLTE5LjEwM2MwLTQuOTY4OC03LjA3LTkuNDkzNi0xOC42NTktMTIuODkxIDAuMDQtMC4wMzAzIDAuMDg0LTAuMDYyNSAwLjEyMzgtMC4wOTEzIDExLjcwOCAzLjQxNiAxOC44NTYgNy45NzUgMTguODU2IDEyLjk4MiAwIDEwLjU5OC0zMi4wMjEgMTkuMTg5LTcxLjUyMSAxOS4xODkiIGZpbGw9IiNmN2Y3ZjciLz4KICAgPHBhdGggaWQ9InBhdGg0ODIzIiBkPSJtMzQ1LjU4IDk0My40OWMtMzkuMzI0IDAtNzEuMjAxLTguNTUyOC03MS4yMDEtMTkuMTAzIDAtNC45Njg4IDcuMDctOS40OTM2IDE4LjY1OS0xMi44OTEgMC4wNDI1IDAuMDMxMiAwLjA4MTIgMC4wNjAxIDAuMTIzNzUgMC4wOTE0LTExLjQ3IDMuMzc4NC0xOC40NjEgNy44Njk2LTE4LjQ2MSAxMi44IDAgMTAuNTAyIDMxLjczNCAxOS4wMTYgNzAuODggMTkuMDE2IDM5LjE0NSAwIDcwLjg4LTguNTEzNiA3MC44OC0xOS4wMTYgMC00LjkzMDEtNi45OTI1LTkuNDIxNC0xOC40NjEtMTIuOCAwLjA0MS0wLjAzMTIgMC4wOC0wLjA2MDEgMC4xMjI1LTAuMDkxNCAxMS41ODkgMy4zOTc1IDE4LjY1OSA3LjkyMjQgMTguNjU5IDEyLjg5MSAwIDEwLjU1LTMxLjg3OCAxOS4xMDMtNzEuMiAxOS4xMDMiIGZpbGw9IiNmNmY3ZjciLz4KICAgPHBhdGggaWQ9InBhdGg0ODI1IiBkPSJtMzQ1LjU4IDk0My40Yy0zOS4xNDYgMC03MC44OC04LjUxMzYtNzAuODgtMTkuMDE2IDAtNC45MzAxIDYuOTkxMi05LjQyMTQgMTguNDYxLTEyLjggMC4wNCAwLjAyODIgMC4wODM3IDAuMDYxIDAuMTIzNzUgMC4wOTA3LTExLjM0OSAzLjM1ODktMTguMjY1IDcuODE3OS0xOC4yNjUgMTIuNzA5IDAgMTAuNDU1IDMxLjU5MSAxOC45MzEgNzAuNTYgMTguOTMxczcwLjU1OS04LjQ3NiA3MC41NTktMTguOTMxYzAtNC44OTExLTYuOTE1LTkuMzUwMS0xOC4yNjUtMTIuNzA5IDAuMDQtMC4wMjk3IDAuMDg0LTAuMDYyNSAwLjEyNS0wLjA5MDcgMTEuNDY5IDMuMzc4NCAxOC40NjEgNy44Njk2IDE4LjQ2MSAxMi44IDAgMTAuNTAyLTMxLjczNSAxOS4wMTYtNzAuODggMTkuMDE2IiBmaWxsPSIjZjZmNmY3Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNDgyNyIgZD0ibTM0NS41OCA5NDMuMzJjLTM4Ljk2OSAwLTcwLjU2LTguNDc2LTcwLjU2LTE4LjkzMSAwLTQuODkxMSA2LjkxNjItOS4zNTAxIDE4LjI2NS0xMi43MDkgMC4wNDI1IDAuMDMxMiAwLjA4MjUgMC4wNjAxIDAuMTI1IDAuMDkxNC0xMS4yMzIgMy4zNDAyLTE4LjA2OSA3Ljc2NTEtMTguMDY5IDEyLjYxOCAwIDEwLjQwOCAzMS40NDYgMTguODQ1IDcwLjIzOSAxOC44NDUgMzguNzkxIDAgNzAuMjM4LTguNDM3NSA3MC4yMzgtMTguODQ1IDAtNC44NTI1LTYuODM2My05LjI3NzQtMTguMDY4LTEyLjYxOCAwLjA0My0wLjAzMTIgMC4wODEtMC4wNjAxIDAuMTIzNy0wLjA5MTQgMTEuMzUgMy4zNTg5IDE4LjI2NSA3LjgxNzkgMTguMjY1IDEyLjcwOSAwIDEwLjQ1NS0zMS41OSAxOC45MzEtNzAuNTU5IDE4LjkzMSIgZmlsbD0iI2Y2ZjZmNiIvPgogICA8cGF0aCBpZD0icGF0aDQ4MjkiIGQ9Im0zNDUuNTggOTQzLjIzYy0zOC43OTIgMC03MC4yMzktOC40Mzc1LTcwLjIzOS0xOC44NDUgMC00Ljg1MjUgNi44MzYyLTkuMjc3NCAxOC4wNjktMTIuNjE4IDAuMDQgMC4wMjg3IDAuMDgzNyAwLjA2MTUgMC4xMjUgMC4wOTEyLTExLjExNCAzLjMyMTMtMTcuODcyIDcuNzExNC0xNy44NzIgMTIuNTI2IDAgMTAuMzYgMzEuMzAyIDE4Ljc1OSA2OS45MTggMTguNzU5IDM4LjYxNCAwIDY5LjkxOC04LjM5ODkgNjkuOTE4LTE4Ljc1OSAwLTQuODE1LTYuNzYtOS4yMDUxLTE3Ljg3NC0xMi41MjYgMC4wNDEtMC4wMjk3IDAuMDg1LTAuMDYyNSAwLjEyNjMtMC4wOTEyIDExLjIzMSAzLjM0MDIgMTguMDY4IDcuNzY1MSAxOC4wNjggMTIuNjE4IDAgMTAuNDA4LTMxLjQ0NiAxOC44NDUtNzAuMjM4IDE4Ljg0NSIgZmlsbD0iI2Y1ZjZmNiIvPgogICA8cGF0aCBpZD0icGF0aDQ4MzEiIGQ9Im0zNDUuNTggOTQzLjE0Yy0zOC42MTUgMC02OS45MTgtOC4zOTg5LTY5LjkxOC0xOC43NTkgMC00LjgxNSA2Ljc1ODgtOS4yMDUxIDE3Ljg3Mi0xMi41MjYgMC4wNDI1IDAuMDMxMiAwLjA4MjUgMC4wNjAxIDAuMTI1IDAuMDkxNC0xMC45OTUgMy4zMDI2LTE3LjY3OCA3LjY1ODYtMTcuNjc4IDEyLjQzNSAwIDEwLjMxMiAzMS4xNiAxOC42NzIgNjkuNTk4IDE4LjY3MiAzOC40MzYgMCA2OS41OTYtOC4zNTk5IDY5LjU5Ni0xOC42NzIgMC00Ljc3NjQtNi42ODI1LTkuMTMyNC0xNy42NzgtMTIuNDM1IDAuMDQzLTAuMDMxMiAwLjA4Mi0wLjA2MDEgMC4xMjUtMC4wOTE0IDExLjExNCAzLjMyMTMgMTcuODc0IDcuNzExNCAxNy44NzQgMTIuNTI2IDAgMTAuMzYtMzEuMzA0IDE4Ljc1OS02OS45MTggMTguNzU5IiBmaWxsPSIjZjVmNWY1Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNDgzMyIgZD0ibTM0NS41OCA5NDMuMDZjLTM4LjQzOCAwLTY5LjU5OC04LjM1OTktNjkuNTk4LTE4LjY3MiAwLTQuNzc2NCA2LjY4MjUtOS4xMzI0IDE3LjY3OC0xMi40MzUgMC4wNDI1IDAuMDMxMiAwLjA4MzcgMC4wNiAwLjEyNjI1IDAuMDkxMy0xMC44NzggMy4yODI4LTE3LjQ4MiA3LjYwNjUtMTcuNDgyIDEyLjM0NCAwIDEwLjI2NSAzMS4wMTYgMTguNTg2IDY5LjI3NiAxOC41ODZzNjkuMjc2LTguMzIxMyA2OS4yNzYtMTguNTg2YzAtNC43MzcyLTYuNjA2Mi05LjA2MS0xNy40ODQtMTIuMzQ0IDAuMDQzLTAuMDMxMiAwLjA4NC0wLjA2IDAuMTI2Mi0wLjA5MTMgMTAuOTk1IDMuMzAyNiAxNy42NzggNy42NTg2IDE3LjY3OCAxMi40MzUgMCAxMC4zMTItMzEuMTYgMTguNjcyLTY5LjU5NiAxOC42NzIiIGZpbGw9IiNmNWY1ZjUiLz4KICAgPHBhdGggaWQ9InBhdGg0ODM1IiBkPSJtMzQ1LjU4IDk0Mi45N2MtMzguMjYgMC02OS4yNzYtOC4zMjEzLTY5LjI3Ni0xOC41ODYgMC00LjczNzIgNi42MDUtOS4wNjEgMTcuNDgyLTEyLjM0NCAwLjA0MjUgMC4wMzEyIDAuMDgzNyAwLjA2MSAwLjEyNjI1IDAuMDkxMi0xMC43NTggMy4yNjM4LTE3LjI4OSA3LjU1MzgtMTcuMjg5IDEyLjI1MiAwIDEwLjIxNyAzMC44NzIgMTguNSA2OC45NTYgMTguNSAzOC4wODMgMCA2OC45NTUtOC4yODI4IDY4Ljk1NS0xOC41IDAtNC42OTg4LTYuNTMxMy04Ljk4ODgtMTcuMjg5LTEyLjI1MiAwLjA0My0wLjAzMDMgMC4wODQtMC4wNiAwLjEyNjMtMC4wOTEyIDEwLjg3OCAzLjI4MjggMTcuNDg0IDcuNjA2NSAxNy40ODQgMTIuMzQ0IDAgMTAuMjY1LTMxLjAxNiAxOC41ODYtNjkuMjc2IDE4LjU4NiIgZmlsbD0iI2Y0ZjRmNCIvPgogICA8cGF0aCBpZD0icGF0aDQ4MzciIGQ9Im0zNDUuNTggOTQyLjg5Yy0zOC4wODQgMC02OC45NTYtOC4yODI4LTY4Ljk1Ni0xOC41IDAtNC42OTg4IDYuNTMxMi04Ljk4ODggMTcuMjg5LTEyLjI1MiAwLjA0MTIgMC4wMjk5IDAuMDg2MiAwLjA2MjUgMC4xMjc1IDAuMDkxNC0xMC42NDIgMy4yNDM2LTE3LjA5NSA3LjUtMTcuMDk1IDEyLjE2MSAwIDEwLjE3IDMwLjcyOSAxOC40MTUgNjguNjM1IDE4LjQxNSAzNy45MDUgMCA2OC42MzQtOC4yNDUyIDY4LjYzNC0xOC40MTUgMC00LjY2MTEtNi40NTI1LTguOTE3NS0xNy4wOTUtMTIuMTYxIDAuMDQxLTAuMDI4OSAwLjA4OC0wLjA2MTUgMC4xMjc1LTAuMDkxNCAxMC43NTggMy4yNjM4IDE3LjI4OSA3LjU1MzggMTcuMjg5IDEyLjI1MiAwIDEwLjIxNy0zMC44NzIgMTguNS02OC45NTUgMTguNSIgZmlsbD0iI2Y0ZjRmNCIvPgogICA8cGF0aCBpZD0icGF0aDQ4MzkiIGQ9Im0zNDUuNTggOTQyLjhjLTM3LjkwNiAwLTY4LjYzNS04LjI0NTItNjguNjM1LTE4LjQxNSAwLTQuNjYxMSA2LjQ1MjUtOC45MTc1IDE3LjA5NS0xMi4xNjEgMC4wNDI1IDAuMDMxMiAwLjA4NSAwLjA2MSAwLjEyNzUgMC4wOTIyLTEwLjUyNCAzLjIyMzYtMTYuOTAxIDcuNDQ2NC0xNi45MDEgMTIuMDY5IDAgMTAuMTIyIDMwLjU4NSAxOC4zMjkgNjguMzE0IDE4LjMyOXM2OC4zMTQtOC4yMDYxIDY4LjMxNC0xOC4zMjljMC00LjYyMjUtNi4zNzc1LTguODQ1Mi0xNi45MDItMTIuMDY5IDAuMDQ0LTAuMDMxMiAwLjA4NS0wLjA2MSAwLjEyNzUtMC4wOTIyIDEwLjY0MiAzLjI0MzYgMTcuMDk1IDcuNSAxNy4wOTUgMTIuMTYxIDAgMTAuMTctMzAuNzI5IDE4LjQxNS02OC42MzQgMTguNDE1IiBmaWxsPSIjZjNmM2YzIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDg0MSIgZD0ibTM0NS41OCA5NDIuNzFjLTM3LjcyOSAwLTY4LjMxNC04LjIwNjEtNjguMzE0LTE4LjMyOSAwLTQuNjIyNSA2LjM3NzUtOC44NDUyIDE2LjkwMS0xMi4wNjkgMC4wNDI1IDAuMDMwMyAwLjA4NSAwLjA2MDEgMC4xMjc1IDAuMDkxNC0xMC40MDggMy4yMDUtMTYuNzA5IDcuMzkzNS0xNi43MDkgMTEuOTc4IDAgMTAuMDc1IDMwLjQ0MSAxOC4yNDMgNjcuOTk0IDE4LjI0MyAzNy41NTEgMCA2Ny45OTMtOC4xNjc0IDY3Ljk5My0xOC4yNDMgMC00LjU4NC02LjMwMTMtOC43NzI1LTE2LjcwOS0xMS45NzggMC4wNDMtMC4wMzEyIDAuMDg1LTAuMDYxMSAwLjEyNzUtMC4wOTE0IDEwLjUyNSAzLjIyMzYgMTYuOTAyIDcuNDQ2NCAxNi45MDIgMTIuMDY5IDAgMTAuMTIyLTMwLjU4NSAxOC4zMjktNjguMzE0IDE4LjMyOSIgZmlsbD0iI2YzZjNmMiIvPgogICA8cGF0aCBpZD0icGF0aDQ4NDMiIGQ9Im0zNDUuNTggOTQyLjYzYy0zNy41NTIgMC02Ny45OTQtOC4xNjc0LTY3Ljk5NC0xOC4yNDMgMC00LjU4NCA2LjMwMTItOC43NzI1IDE2LjcwOS0xMS45NzggMC4wNDM3IDAuMDI5OCAwLjA4NjMgMC4wNjE1IDAuMTI4NzUgMC4wOTEzLTEwLjI4OSAzLjE4NTEtMTYuNTE2IDcuMzQxMy0xNi41MTYgMTEuODg2IDAgMTAuMDI3IDMwLjI5OCAxOC4xNTYgNjcuNjcyIDE4LjE1NnM2Ny42NzEtOC4xMjg5IDY3LjY3MS0xOC4xNTZjMC00LjU0NjQtNi4yMjUtOC43MDExLTE2LjUxNi0xMS44ODYgMC4wNDMtMC4wMjk4IDAuMDg2LTAuMDYxNSAwLjEyODctMC4wOTEzIDEwLjQwOCAzLjIwNSAxNi43MDkgNy4zOTM1IDE2LjcwOSAxMS45NzggMCAxMC4wNzUtMzAuNDQxIDE4LjI0My02Ny45OTMgMTguMjQzIiBmaWxsPSIjZjJmMmYyIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDg0NSIgZD0ibTM0NS41OCA5NDIuNTRjLTM3LjM3NSAwLTY3LjY3Mi04LjEyODktNjcuNjcyLTE4LjE1NiAwLTQuNTQ0OSA2LjIyNzUtOC43MDExIDE2LjUxNi0xMS44ODYgMC4wNDI1IDAuMDI5OCAwLjA4NjMgMC4wNjEgMC4xMjg3NSAwLjA5MTQtMTAuMTcyIDMuMTY1LTE2LjMyNCA3LjI4NzUtMTYuMzI0IDExLjc5NSAwIDkuOTggMzAuMTU0IDE4LjA3IDY3LjM1MSAxOC4wNyAzNy4xOTYgMCA2Ny4zNTEtOC4wODk4IDY3LjM1MS0xOC4wNyAwLTQuNTA3NC02LjE1MTItOC42Mjk5LTE2LjMyNS0xMS43OTUgMC4wNDMtMC4wMzA0IDAuMDg2LTAuMDYxNiAwLjEyODgtMC4wOTE0IDEwLjI5MSAzLjE4NTEgMTYuNTE2IDcuMzM5OSAxNi41MTYgMTEuODg2IDAgMTAuMDI3LTMwLjI5OCAxOC4xNTYtNjcuNjcxIDE4LjE1NiIgZmlsbD0iI2YyZjFmMSIvPgogICA8cGF0aCBpZD0icGF0aDQ4NDciIGQ9Im0zNDUuNTggOTQyLjQ2Yy0zNy4xOTggMC02Ny4zNTEtOC4wODk4LTY3LjM1MS0xOC4wNyAwLTQuNTA3NCA2LjE1MTItOC42Mjk5IDE2LjMyNC0xMS43OTUgMC4wNDM4IDAuMDMxMiAwLjA4NzUgMC4wNjEgMC4xMyAwLjA5MjMtMTAuMDU2IDMuMTQ1LTE2LjEzNCA3LjIzMzktMTYuMTM0IDExLjcwMyAwIDkuOTMyNiAzMC4wMTEgMTcuOTg0IDY3LjAzMSAxNy45ODRzNjcuMDMtOC4wNTEzIDY3LjAzLTE3Ljk4NGMwLTQuNDY4OC02LjA3NzUtOC41NTc2LTE2LjEzNC0xMS43MDMgMC4wNDQtMC4wMzEyIDAuMDg2LTAuMDYxIDAuMTMtMC4wOTIzIDEwLjE3NCAzLjE2NSAxNi4zMjUgNy4yODc1IDE2LjMyNSAxMS43OTUgMCA5Ljk4LTMwLjE1NSAxOC4wNy02Ny4zNTEgMTguMDciIGZpbGw9IiNmMWYxZjEiLz4KICAgPHBhdGggaWQ9InBhdGg0ODQ5IiBkPSJtMzQ1LjU4IDk0Mi4zN2MtMzcuMDIgMC02Ny4wMzEtOC4wNTEzLTY3LjAzMS0xNy45ODQgMC00LjQ2ODggNi4wNzc1LTguNTU3NiAxNi4xMzQtMTEuNzAzIDAuMDQzOCAwLjAzMDMgMC4wODc1IDAuMDYxNSAwLjEzIDAuMDkxMy05Ljk0IDMuMTI1LTE1Ljk0MiA3LjE4MDItMTUuOTQyIDExLjYxMSAwIDkuODg0OCAyOS44NjYgMTcuODk3IDY2LjcxIDE3Ljg5NyAzNi44NDMgMCA2Ni43MS04LjAxMjcgNjYuNzEtMTcuODk3IDAtNC40MzExLTYuMDAzOC04LjQ4NjQtMTUuOTQ0LTExLjYxMSAwLjA0NC0wLjAyOTcgMC4wODgtMC4wNjEgMC4xMy0wLjA5MTMgMTAuMDU2IDMuMTQ1IDE2LjEzNCA3LjIzMzkgMTYuMTM0IDExLjcwMyAwIDkuOTMyNi0zMC4wMSAxNy45ODQtNjcuMDMgMTcuOTg0IiBmaWxsPSIjZjBmMGYwIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDg1MSIgZD0ibTM0NS41OCA5NDIuMjhjLTM2Ljg0NCAwLTY2LjcxLTguMDEyNy02Ni43MS0xNy44OTcgMC00LjQzMTEgNi4wMDI1LTguNDg2NCAxNS45NDItMTEuNjExIDAuMDQzOCAwLjAzMDQgMC4wODc1IDAuMDYxNiAwLjEzMTI1IDAuMDkxNC05LjgyNSAzLjEwNjQtMTUuNzU0IDcuMTI3NC0xNS43NTQgMTEuNTIgMCA5LjgzNzQgMjkuNzI0IDE3LjgxMiA2Ni4zOSAxNy44MTIgMzYuNjY1IDAgNjYuMzg5LTcuOTc1MSA2Ni4zODktMTcuODEyIDAtNC4zOTI2LTUuOTI4Ny04LjQxMzYtMTUuNzUyLTExLjUyIDAuMDQzLTAuMDI5OCAwLjA4OC0wLjA2MSAwLjEzLTAuMDkxNCA5Ljk0IDMuMTI1IDE1Ljk0NCA3LjE4MDIgMTUuOTQ0IDExLjYxMSAwIDkuODg0OC0yOS44NjggMTcuODk3LTY2LjcxIDE3Ljg5NyIgZmlsbD0iI2YwZjBlZiIvPgogICA8cGF0aCBpZD0icGF0aDQ4NTMiIGQ9Im0zNDUuNTggOTQyLjJjLTM2LjY2NiAwLTY2LjM5LTcuOTc1MS02Ni4zOS0xNy44MTIgMC00LjM5MjYgNS45Mjg4LTguNDEzNiAxNS43NTQtMTEuNTIgMC4wNDUgMC4wMzEyIDAuMDg1IDAuMDYgMC4xMzEyNSAwLjA5MTItOS43MTEyIDMuMDg2NS0xNS41NjQgNy4wNzM4LTE1LjU2NCAxMS40MjkgMCA5Ljc5IDI5LjU4IDE3LjcyNiA2Ni4wNjkgMTcuNzI2IDM2LjQ4OCAwIDY2LjA2OC03LjkzNjEgNjYuMDY4LTE3LjcyNiAwLTQuMzU1LTUuODUyNS04LjM0MjItMTUuNTYyLTExLjQyOSAwLjA0NS0wLjAzMTIgMC4wODUtMC4wNiAwLjEzMTItMC4wOTEyIDkuODIzOCAzLjEwNjQgMTUuNzUyIDcuMTI3NCAxNS43NTIgMTEuNTIgMCA5LjgzNzQtMjkuNzI0IDE3LjgxMi02Ni4zODkgMTcuODEyIiBmaWxsPSIjZWZlZmVmIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDg1NSIgZD0ibTM0NS41OCA5NDIuMTFjLTM2LjQ4OSAwLTY2LjA2OS03LjkzNjEtNjYuMDY5LTE3LjcyNiAwLTQuMzU1IDUuODUyNS04LjM0MjIgMTUuNTY0LTExLjQyOSAwLjA0MjUgMC4wMjk5IDAuMDg3NSAwLjA2MjUgMC4xMzEyNSAwLjA5MjQtOS41OTM4IDMuMDY0OS0xNS4zNzQgNy4wMi0xNS4zNzQgMTEuMzM2IDAgOS43NDI2IDI5LjQzNiAxNy42NCA2NS43NDggMTcuNjQgMzYuMzExIDAgNjUuNzQ4LTcuODk3NSA2NS43NDgtMTcuNjQgMC00LjMxNjQtNS43OC04LjI3MTUtMTUuMzc1LTExLjMzNiAwLjA0NC0wLjAyOTkgMC4wODktMC4wNjI1IDAuMTMyNS0wLjA5MjQgOS43MSAzLjA4NjUgMTUuNTYyIDcuMDczOCAxNS41NjIgMTEuNDI5IDAgOS43OS0yOS41OCAxNy43MjYtNjYuMDY4IDE3LjcyNiIgZmlsbD0iI2VlZSIvPgogICA8cGF0aCBpZD0icGF0aDQ4NTciIGQ9Im0zNDUuNTggOTQyLjAzYy0zNi4zMTEgMC02NS43NDgtNy44OTc1LTY1Ljc0OC0xNy42NCAwLTQuMzE2NCA1Ljc4LTguMjcxNSAxNS4zNzQtMTEuMzM2IDAuMDQzNyAwLjAzMDIgMC4wODg4IDAuMDYxNSAwLjEzMjUgMC4wOTEyLTkuNDgyNSAzLjA0NjQtMTUuMTg2IDYuOTY2NC0xNS4xODYgMTEuMjQ1IDAgOS42OTQ5IDI5LjI5MiAxNy41NTQgNjUuNDI4IDE3LjU1NCAzNi4xMzQgMCA2NS40MjYtNy44NTg4IDY1LjQyNi0xNy41NTQgMC00LjI3ODgtNS43MDM3LTguMTk4OC0xNS4xODYtMTEuMjQ1IDAuMDQ0LTAuMDI5NyAwLjA5LTAuMDYxIDAuMTMyNS0wLjA5MTIgOS41OTUgMy4wNjQ5IDE1LjM3NSA3LjAyIDE1LjM3NSAxMS4zMzYgMCA5Ljc0MjYtMjkuNDM2IDE3LjY0LTY1Ljc0OCAxNy42NCIgZmlsbD0iI2VlZWVlZCIvPgogICA8cGF0aCBpZD0icGF0aDQ4NTkiIGQ9Im0zNDUuNTggOTQxLjk0Yy0zNi4xMzUgMC02NS40MjgtNy44NTg4LTY1LjQyOC0xNy41NTQgMC00LjI3ODggNS43MDM4LTguMTk4OCAxNS4xODYtMTEuMjQ1IDAuMDQ1IDAuMDMxMiAwLjA4NjMgMC4wNjAxIDAuMTMyNSAwLjA5MTQtOS4zNjc1IDMuMDI2Mi0xNC45OTggNi45MTM1LTE0Ljk5OCAxMS4xNTQgMCA5LjY0NzUgMjkuMTQ5IDE3LjQ2NyA2NS4xMDYgMTcuNDY3IDM1Ljk1NiAwIDY1LjEwNi03LjgxOTggNjUuMTA2LTE3LjQ2NyAwLTQuMjQwMi01LjYzMTItOC4xMjc1LTE0Ljk5OS0xMS4xNTQgMC4wNDYtMC4wMzEyIDAuMDg4LTAuMDYwMSAwLjEzMjUtMC4wOTE0IDkuNDgyNSAzLjA0NjQgMTUuMTg2IDYuOTY2NCAxNS4xODYgMTEuMjQ1IDAgOS42OTQ5LTI5LjI5MiAxNy41NTQtNjUuNDI2IDE3LjU1NCIgZmlsbD0iI2VkZWRlYyIvPgogICA8cGF0aCBpZD0icGF0aDQ4NjEiIGQ9Im0zNDUuNTggOTQxLjg1Yy0zNS45NTggMC02NS4xMDYtNy44MTk4LTY1LjEwNi0xNy40NjcgMC00LjI0MDIgNS42My04LjEyNzUgMTQuOTk4LTExLjE1NCAwLjA0MzcgMC4wMzAzIDAuMDkgMC4wNjI1IDAuMTMzNzUgMC4wOTIzLTkuMjUxMiAzLjAwNTQtMTQuODExIDYuODU4OS0xNC44MTEgMTEuMDYyIDAgOS42MDAxIDI5LjAwNiAxNy4zODEgNjQuNzg2IDE3LjM4MXM2NC43ODUtNy43ODEyIDY0Ljc4NS0xNy4zODFjMC00LjIwMjYtNS41Ni04LjA1NjEtMTQuODExLTExLjA2MiAwLjA0NC0wLjAyOTggMC4wOTEtMC4wNjIgMC4xMzM4LTAuMDkyMyA5LjM2NzUgMy4wMjYyIDE0Ljk5OSA2LjkxMzUgMTQuOTk5IDExLjE1NCAwIDkuNjQ3NS0yOS4xNSAxNy40NjctNjUuMTA2IDE3LjQ2NyIgZmlsbD0iI2VkZWRlYyIvPgogICA8cGF0aCBpZD0icGF0aDQ4NjMiIGQ9Im0zNDUuNTggOTQxLjc3Yy0zNS43OCAwLTY0Ljc4Ni03Ljc4MTItNjQuNzg2LTE3LjM4MSAwLTQuMjAyNiA1LjU2LTguMDU2MSAxNC44MTEtMTEuMDYyIDAuMDQ1IDAuMDMxNyAwLjA4NzUgMC4wNiAwLjEzMzc1IDAuMDkxMi05LjEzODggMi45ODU0LTE0LjYyNCA2LjgwNjYtMTQuNjI0IDEwLjk3IDAgOS41NTI4IDI4Ljg2MSAxNy4yOTUgNjQuNDY1IDE3LjI5NSAzNS42MDMgMCA2NC40NjQtNy43NDIyIDY0LjQ2NC0xNy4yOTUgMC00LjE2MzYtNS40ODUtNy45ODQ5LTE0LjYyNC0xMC45NyAwLjA0Ni0wLjAzMTIgMC4wODktMC4wNTk1IDAuMTMzNy0wLjA5MTIgOS4yNTEzIDMuMDA1NCAxNC44MTEgNi44NTg5IDE0LjgxMSAxMS4wNjIgMCA5LjYwMDEtMjkuMDA1IDE3LjM4MS02NC43ODUgMTcuMzgxIiBmaWxsPSIjZWNlY2ViIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDg2NSIgZD0ibTM0NS41OCA5NDEuNjhjLTM1LjYwNCAwLTY0LjQ2NS03Ljc0MjItNjQuNDY1LTE3LjI5NSAwLTQuMTYzNiA1LjQ4NS03Ljk4NDkgMTQuNjI0LTEwLjk3IDAuMDQzOCAwLjAzMDMgMC4wOTEzIDAuMDYyNSAwLjEzMzc1IDAuMDkyOC05LjAyMzggMi45NjM5LTE0LjQzNiA2Ljc1MS0xNC40MzYgMTAuODc4IDAgOS41MDQ5IDI4LjcxOCAxNy4yMSA2NC4xNDQgMTcuMjEgMzUuNDI1IDAgNjQuMTQ0LTcuNzA1MSA2NC4xNDQtMTcuMjEgMC00LjEyNjUtNS40MTM3LTcuOTEzNi0xNC40MzgtMTAuODc5IDAuMDQ0LTAuMDI4OSAwLjA5MS0wLjA2MTEgMC4xMzM4LTAuMDkxNCA5LjEzODcgMi45ODU0IDE0LjYyNCA2LjgwNjYgMTQuNjI0IDEwLjk3IDAgOS41NTI4LTI4Ljg2MSAxNy4yOTUtNjQuNDY0IDE3LjI5NSIgZmlsbD0iI2VjZWJlYSIvPgogICA8cGF0aCBpZD0icGF0aDQ4NjciIGQ9Im0zNDUuNTggOTQxLjZjLTM1LjQyNiAwLTY0LjE0NC03LjcwNTEtNjQuMTQ0LTE3LjIxIDAtNC4xMjY1IDUuNDEyNS03LjkxMzYgMTQuNDM2LTEwLjg3OCAwLjA0NjMgMC4wMzEyIDAuMDkgMC4wNjAxIDAuMTM1IDAuMDkxNC04LjkxMTIgMi45NDM5LTE0LjI1MSA2LjY5NzItMTQuMjUxIDEwLjc4NiAwIDkuNDU3NSAyOC41NzUgMTcuMTI0IDYzLjgyNCAxNy4xMjQgMzUuMjQ4IDAgNjMuODIzLTcuNjY2IDYzLjgyMy0xNy4xMjQgMC00LjA4ODktNS4zNC03Ljg0MjItMTQuMjUxLTEwLjc4NiAwLjA0Ni0wLjAzMTIgMC4wODktMC4wNjE1IDAuMTM1LTAuMDkyOCA5LjAyMzggMi45NjUyIDE0LjQzOCA2Ljc1MjQgMTQuNDM4IDEwLjg3OSAwIDkuNTA0OS0yOC43MTkgMTcuMjEtNjQuMTQ0IDE3LjIxIiBmaWxsPSIjZWJlYmVhIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDg2OSIgZD0ibTM0NS41OCA5NDEuNTFjLTM1LjI0OSAwLTYzLjgyNC03LjY2Ni02My44MjQtMTcuMTI0IDAtNC4wODg5IDUuMzQtNy44NDIyIDE0LjI1MS0xMC43ODYgMC4wNDM3IDAuMDI5NyAwLjA5MjUgMC4wNjI1IDAuMTM2MjUgMC4wOTEyLTguNzk4OCAyLjkyNDktMTQuMDY2IDYuNjQ1Mi0xNC4wNjYgMTAuNjk1IDAgOS40MTAxIDI4LjQzIDE3LjAzOCA2My41MDIgMTcuMDM4IDM1LjA3MSAwIDYzLjUwMS03LjYyNzUgNjMuNTAxLTE3LjAzOCAwLTQuMDQ5OC01LjI2NzUtNy43Ny0xNC4wNjYtMTAuNjk1IDAuMDQ0LTAuMDI4NyAwLjA5Mi0wLjA2MTUgMC4xMzYyLTAuMDkxMiA4LjkxMTMgMi45NDM5IDE0LjI1MSA2LjY5NzIgMTQuMjUxIDEwLjc4NiAwIDkuNDU3NS0yOC41NzUgMTcuMTI0LTYzLjgyMyAxNy4xMjQiIGZpbGw9IiNlYWVhZTkiLz4KICAgPHBhdGggaWQ9InBhdGg0ODcxIiBkPSJtMzQ1LjU4IDk0MS40MmMtMzUuMDcyIDAtNjMuNTAyLTcuNjI3NS02My41MDItMTcuMDM4IDAtNC4wNDk4IDUuMjY3NS03Ljc3IDE0LjA2Ni0xMC42OTUgMC4wNDYyIDAuMDMxMiAwLjA5IDAuMDYxMSAwLjEzNjI1IDAuMDkyNC04LjY4NSAyLjkwMzgtMTMuODgyIDYuNTg5OC0xMy44ODIgMTAuNjAyIDAgOS4zNjIyIDI4LjI4OCAxNi45NTEgNjMuMTgyIDE2Ljk1MXM2My4xODEtNy41ODg5IDYzLjE4MS0xNi45NTFjMC00LjAxMjgtNS4xOTc1LTcuNjk4OC0xMy44ODItMTAuNjAyIDAuMDQ2LTAuMDMxMiAwLjA5LTAuMDYxMSAwLjEzNjMtMC4wOTI0IDguNzk4NyAyLjkyNDkgMTQuMDY2IDYuNjQ1MSAxNC4wNjYgMTAuNjk1IDAgOS40MTAxLTI4LjQzIDE3LjAzOC02My41MDEgMTcuMDM4IiBmaWxsPSIjZWFlYWU4Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNDg3MyIgZD0ibTM0NS41OCA5NDEuMzRjLTM0Ljg5NSAwLTYzLjE4Mi03LjU4ODktNjMuMTgyLTE2Ljk1MSAwLTQuMDEyOCA1LjE5NzUtNy42OTg4IDEzLjg4Mi0xMC42MDIgMC4wNDYyIDAuMDMxMiAwLjA5IDAuMDYxIDAuMTM2MjUgMC4wOTEzLTguNTcyNSAyLjg4MzgtMTMuNjk4IDYuNTM3Ni0xMy42OTggMTAuNTExIDAgOS4zMTUgMjguMTQ0IDE2Ljg2NSA2Mi44NjEgMTYuODY1IDM0LjcxNiAwIDYyLjg2LTcuNTQ5OCA2Mi44Ni0xNi44NjUgMC0zLjk3MzYtNS4xMjUtNy42Mjc1LTEzLjY5OC0xMC41MTEgMC4wNDYtMC4wMzAzIDAuMDktMC4wNiAwLjEzNjItMC4wOTEzIDguNjg1IDIuOTAzOCAxMy44ODIgNi41ODk4IDEzLjg4MiAxMC42MDIgMCA5LjM2MjItMjguMjg4IDE2Ljk1MS02My4xODEgMTYuOTUxIiBmaWxsPSIjZTllOWU4Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNDg3NSIgZD0ibTM0NS41OCA5NDEuMjVjLTM0LjcxOCAwLTYyLjg2MS03LjU0OTgtNjIuODYxLTE2Ljg2NSAwLTMuOTczNiA1LjEyNS03LjYyNzUgMTMuNjk4LTEwLjUxMSAwLjA0NjIgMC4wMzEyIDAuMDkxMyAwLjA2MSAwLjEzNzUgMC4wOTIzLTguNDYgMi44NjI5LTEzLjUxNCA2LjQ4My0xMy41MTQgMTAuNDE5IDAgOS4yNjc2IDI4IDE2Ljc3OSA2Mi41NCAxNi43NzlzNjIuNTQtNy41MTEyIDYyLjU0LTE2Ljc3OWMwLTMuOTM2LTUuMDU1LTcuNTU2MS0xMy41MTUtMTAuNDE5IDAuMDQ2LTAuMDMxMiAwLjA5MS0wLjA2MSAwLjEzNzUtMC4wOTIzIDguNTcyNSAyLjg4MzggMTMuNjk4IDYuNTM3NiAxMy42OTggMTAuNTExIDAgOS4zMTUtMjguMTQ0IDE2Ljg2NS02Mi44NiAxNi44NjUiIGZpbGw9IiNlOWU4ZTciLz4KICAgPHBhdGggaWQ9InBhdGg0ODc3IiBkPSJtMzQ1LjU4IDk0MS4xNmMtMzQuNTQgMC02Mi41NC03LjUxMTItNjIuNTQtMTYuNzc5IDAtMy45MzYgNS4wNTM4LTcuNTU2MSAxMy41MTQtMTAuNDE5IDAuMDQ2MiAwLjAzMTIgMC4wOTI1IDAuMDYxNiAwLjEzODc1IDAuMDkyOS04LjM1IDIuODQxMy0xMy4zMzIgNi40MjczLTEzLjMzMiAxMC4zMjYgMCA5LjIyMDIgMjcuODU2IDE2LjY5NCA2Mi4yMiAxNi42OTQgMzQuMzYzIDAgNjIuMjE5LTcuNDczNiA2Mi4yMTktMTYuNjk0IDAtMy44OTg5LTQuOTgyNS03LjQ4NDktMTMuMzMyLTEwLjMyNiAwLjA0Ny0wLjAzMTIgMC4wOTItMC4wNjE2IDAuMTM4OC0wLjA5MjkgOC40NiAyLjg2MjkgMTMuNTE1IDYuNDgzIDEzLjUxNSAxMC40MTkgMCA5LjI2NzYtMjggMTYuNzc5LTYyLjU0IDE2Ljc3OSIgZmlsbD0iI2U4ZThlNiIvPgogICA8cGF0aCBpZD0icGF0aDQ4NzkiIGQ9Im0zNDUuNTggOTQxLjA4Yy0zNC4zNjQgMC02Mi4yMi03LjQ3MzYtNjIuMjItMTYuNjk0IDAtMy44OTg5IDQuOTgyNS03LjQ4NDkgMTMuMzMyLTEwLjMyNiAwLjA0NjIgMC4wMjk4IDAuMDkxMyAwLjA2IDAuMTM3NSAwLjA5MTMtOC4yMzYyIDIuODE5OS0xMy4xNDkgNi4zNzM1LTEzLjE0OSAxMC4yMzUgMCA5LjE3MjQgMjcuNzEyIDE2LjYwNyA2MS44OTkgMTYuNjA3IDM0LjE4NSAwIDYxLjg5OC03LjQzNTEgNjEuODk4LTE2LjYwNyAwLTMuODYxNC00LjkxMTMtNy40MTUtMTMuMTQ5LTEwLjIzNSAwLjA0Ni0wLjAzMTIgMC4wOTItMC4wNjE1IDAuMTM3NS0wLjA5MTMgOC4zNSAyLjg0MTMgMTMuMzMyIDYuNDI3MyAxMy4zMzIgMTAuMzI2IDAgOS4yMjAyLTI3Ljg1NiAxNi42OTQtNjIuMjE5IDE2LjY5NCIgZmlsbD0iI2U3ZTdlNiIvPgogICA8cGF0aCBpZD0icGF0aDQ4ODEiIGQ9Im0zNDUuNTggOTQwLjk5Yy0zNC4xODYgMC02MS44OTktNy40MzUxLTYxLjg5OS0xNi42MDcgMC0zLjg2MTQgNC45MTI1LTcuNDE1IDEzLjE0OS0xMC4yMzUgMC4wNDc1IDAuMDMxMiAwLjA5MzcgMC4wNjEgMC4xNCAwLjA5MjItOC4xMjYyIDIuNzk4OS0xMi45NjkgNi4zMTk5LTEyLjk2OSAxMC4xNDMgMCA5LjEyMzUgMjcuNTcgMTYuNTIxIDYxLjU3OSAxNi41MjEgMzQuMDA4IDAgNjEuNTc4LTcuMzk4IDYxLjU3OC0xNi41MjEgMC0zLjgyMzgtNC44NC03LjM0MzgtMTIuOTY5LTEwLjE0MyAwLjA0Ni0wLjAzMTIgMC4wOTQtMC4wNjEgMC4xNC0wLjA5MjIgOC4yMzc1IDIuODE5OSAxMy4xNDkgNi4zNzM1IDEzLjE0OSAxMC4yMzUgMCA5LjE3MjQtMjcuNzEyIDE2LjYwNy02MS44OTggMTYuNjA3IiBmaWxsPSIjZTdlNmU1Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNDg4MyIgZD0ibTM0NS41OCA5NDAuOTFjLTM0LjAwOSAwLTYxLjU3OS03LjM5OC02MS41NzktMTYuNTIxIDAtMy44MjI4IDQuODQyNS03LjM0MzggMTIuOTY5LTEwLjE0MyAwLjA0NjMgMC4wMzA0IDAuMDkzNyAwLjA2MTYgMC4xNCAwLjA5MTQtOC4wMTYyIDIuNzc4OC0xMi43ODggNi4yNjYxLTEyLjc4OCAxMC4wNTEgMCA5LjA3NzYgMjcuNDI2IDE2LjQzNSA2MS4yNTggMTYuNDM1IDMzLjgzMSAwIDYxLjI1Ni03LjM1NzQgNjEuMjU2LTE2LjQzNSAwLTMuNzg1MS00Ljc3MTItNy4yNzI1LTEyLjc4Ni0xMC4wNTEgMC4wNDYtMC4wMjk4IDAuMDkzLTAuMDYxIDAuMTM4Ny0wLjA5MTQgOC4xMjg4IDIuNzk4OSAxMi45NjkgNi4zMTg5IDEyLjk2OSAxMC4xNDMgMCA5LjEyMzUtMjcuNTcgMTYuNTIxLTYxLjU3OCAxNi41MjEiIGZpbGw9IiNlNmU2ZTQiLz4KICAgPHBhdGggaWQ9InBhdGg0ODg1IiBkPSJtMzQ1LjU4IDk0MC44MmMtMzMuODMxIDAtNjEuMjU4LTcuMzU3NC02MS4yNTgtMTYuNDM1IDAtMy43ODUxIDQuNzcxMi03LjI3MjUgMTIuNzg4LTEwLjA1MSAwLjA0NjIgMC4wMzEyIDAuMDkzNyAwLjA2MjUgMC4xNCAwLjA5MjItNy45MDYyIDIuNzU3OS0xMi42MDYgNi4yMTE1LTEyLjYwNiA5Ljk1OSAwIDkuMDI4OCAyNy4yODEgMTYuMzQ5IDYwLjkzNiAxNi4zNDkgMzMuNjU0IDAgNjAuOTM2LTcuMzE5OSA2MC45MzYtMTYuMzQ5IDAtMy43NDc1LTQuNzAxMi03LjIwMTEtMTIuNjA4LTkuOTU5IDAuMDQ2LTAuMDI5NyAwLjA5NC0wLjA2MSAwLjE0MTMtMC4wOTIyIDguMDE1IDIuNzc4OCAxMi43ODYgNi4yNjYxIDEyLjc4NiAxMC4wNTEgMCA5LjA3NzYtMjcuNDI1IDE2LjQzNS02MS4yNTYgMTYuNDM1IiBmaWxsPSIjZTVlNWUzIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDg4NyIgZD0ibTM0NS41OCA5NDAuNzNjLTMzLjY1NSAwLTYwLjkzNi03LjMxOTktNjAuOTM2LTE2LjM0OSAwLTMuNzQ3NSA0LjctNy4yMDExIDEyLjYwNi05Ljk1OSAwLjA0NjIgMC4wMzAzIDAuMDk1IDAuMDYxNSAwLjE0MTI1IDAuMDkyOC03Ljc5NjIgMi43MzY0LTEyLjQyOCA2LjE1NjItMTIuNDI4IDkuODY2MiAwIDguOTgxNSAyNy4xMzkgMTYuMjYzIDYwLjYxNiAxNi4yNjMgMzMuNDc2IDAgNjAuNjE1LTcuMjgxMiA2MC42MTUtMTYuMjYzIDAtMy43MS00LjYzMTMtNy4xMjk5LTEyLjQyOC05Ljg2NjIgMC4wNDYtMC4wMzEyIDAuMDk1LTAuMDYyNSAwLjE0MTItMC4wOTI4IDcuOTA2MyAyLjc1NzkgMTIuNjA4IDYuMjExNSAxMi42MDggOS45NTkgMCA5LjAyODgtMjcuMjgyIDE2LjM0OS02MC45MzYgMTYuMzQ5IiBmaWxsPSIjZTRlNGUzIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDg4OSIgZD0ibTM0NS41OCA5NDAuNjVjLTMzLjQ3OCAwLTYwLjYxNi03LjI4MTItNjAuNjE2LTE2LjI2MyAwLTMuNzEgNC42MzEyLTcuMTI5OSAxMi40MjgtOS44NjYyIDAuMDQ2MiAwLjAyOTkgMC4wOTUgMC4wNjExIDAuMTQxMjUgMC4wOTE0LTcuNjg2MiAyLjcxNjItMTIuMjQ4IDYuMTAyNS0xMi4yNDggOS43NzQ5IDAgOC45MzM2IDI2Ljk5NSAxNi4xNzYgNjAuMjk1IDE2LjE3NiAzMy4yOTkgMCA2MC4yOTQtNy4yNDI2IDYwLjI5NC0xNi4xNzYgMC0zLjY3MjQtNC41NjEyLTcuMDU4Ni0xMi4yNDgtOS43NzQ5IDAuMDQ2LTAuMDMwMiAwLjA5NS0wLjA2MTUgMC4xNDEzLTAuMDkxNCA3Ljc5NjIgMi43MzY0IDEyLjQyOCA2LjE1NjIgMTIuNDI4IDkuODY2MiAwIDguOTgxNS0yNy4xMzkgMTYuMjYzLTYwLjYxNSAxNi4yNjMiIGZpbGw9IiNlNGUzZTIiLz4KICAgPHBhdGggaWQ9InBhdGg0ODkxIiBkPSJtMzQ1LjU4IDk0MC41NmMtMzMuMyAwLTYwLjI5NS03LjI0MjYtNjAuMjk1LTE2LjE3NiAwLTMuNjcyNCA0LjU2MTItNy4wNTg2IDEyLjI0OC05Ljc3NDkgMC4wNDg4IDAuMDMyMyAwLjA5MzcgMC4wNjEgMC4xNDI1IDAuMDkyMi03LjU3NzUgMi42OTM5LTEyLjA2OSA2LjA0NzktMTIuMDY5IDkuNjgyNiAwIDguODg2MiAyNi44NSAxNi4wOTEgNTkuOTc0IDE2LjA5MSAzMy4xMjMgMCA1OS45NzQtNy4yMDUxIDU5Ljk3NC0xNi4wOTEgMC0zLjYzNDgtNC40OTI1LTYuOTg4OC0xMi4wNy05LjY4MjYgMC4wNDktMC4wMzEyIDAuMDk0LTAuMDYgMC4xNDI1LTAuMDkyMiA3LjY4NjMgMi43MTYyIDEyLjI0OCA2LjEwMjUgMTIuMjQ4IDkuNzc0OSAwIDguOTMzNi0yNi45OTUgMTYuMTc2LTYwLjI5NCAxNi4xNzYiIGZpbGw9IiNlM2UzZTEiLz4KICAgPHBhdGggaWQ9InBhdGg0ODkzIiBkPSJtMzQ1LjU4IDk0MC40OGMtMzMuMTI0IDAtNTkuOTc0LTcuMjA1MS01OS45NzQtMTYuMDkxIDAtMy42MzQ4IDQuNDkxMi02Ljk4ODggMTIuMDY5LTkuNjgyNiAwLjA0NjIgMC4wMzAzIDAuMDk2MiAwLjA2MjUgMC4xNDM3NSAwLjA5MjgtNy40Njc1IDIuNjcyNC0xMS44OTIgNS45OTIyLTExLjg5MiA5LjU4OTkgMCA4LjgzODkgMjYuNzA4IDE2LjAwNSA1OS42NTQgMTYuMDA1IDMyLjk0NSAwIDU5LjY1My03LjE2NiA1OS42NTMtMTYuMDA1IDAtMy41OTc2LTQuNDIzOC02LjkxNzUtMTEuODkxLTkuNTg5OSAwLjA0Ni0wLjAzMDMgMC4wOTYtMC4wNjI1IDAuMTQyNS0wLjA5MjggNy41Nzc1IDIuNjkzOSAxMi4wNyA2LjA0NzkgMTIuMDcgOS42ODI2IDAgOC44ODYyLTI2Ljg1MSAxNi4wOTEtNTkuOTc0IDE2LjA5MSIgZmlsbD0iI2UyZTJlMCIvPgogICA8cGF0aCBpZD0icGF0aDQ4OTUiIGQ9Im0zNDUuNTggOTQwLjM5Yy0zMi45NDYgMC01OS42NTQtNy4xNjYtNTkuNjU0LTE2LjAwNSAwLTMuNTk3NiA0LjQyNS02LjkxNzUgMTEuODkyLTkuNTg5OSAwLjA0ODggMC4wMzEyIDAuMDkzNyAwLjA2MDEgMC4xNDI1IDAuMDkwOS03LjM1ODggMi42NTI4LTExLjcxNCA1LjkzOS0xMS43MTQgOS40OTkgMCA4Ljc5MSAyNi41NjQgMTUuOTE5IDU5LjMzMiAxNS45MTlzNTkuMzMxLTcuMTI4IDU5LjMzMS0xNS45MTljMC0zLjU2LTQuMzU1LTYuODQ2Mi0xMS43MTQtOS40OTkgMC4wNDktMC4wMzA4IDAuMDk0LTAuMDU5NiAwLjE0MzctMC4wOTA5IDcuNDY3NSAyLjY3MjQgMTEuODkxIDUuOTkyMiAxMS44OTEgOS41ODk5IDAgOC44Mzg5LTI2LjcwOCAxNi4wMDUtNTkuNjUzIDE2LjAwNSIgZmlsbD0iI2UyZTFkZiIvPgogICA8cGF0aCBpZD0icGF0aDQ4OTciIGQ9Im0zNDUuNTggOTQwLjNjLTMyLjc2OSAwLTU5LjMzMi03LjEyOC01OS4zMzItMTUuOTE5IDAtMy41NiA0LjM1NS02Ljg0NjIgMTEuNzE0LTkuNDk5IDAuMDQ3NSAwLjAzMDMgMC4wOTc1IDAuMDYzIDAuMTQ1IDAuMDkyOC03LjI1MTIgMi42Mjk4LTExLjUzOSA1Ljg4MzctMTEuNTM5IDkuNDA2MiAwIDguNzQzNiAyNi40MjEgMTUuODMzIDU5LjAxMiAxNS44MzNzNTkuMDExLTcuMDg4OSA1OS4wMTEtMTUuODMzYzAtMy41MjI1LTQuMjg3NS02Ljc3NjQtMTEuNTM5LTkuNDA2MiAwLjA0OC0wLjAyOTggMC4wOTgtMC4wNjI1IDAuMTQ1LTAuMDkyOCA3LjM1ODcgMi42NTI4IDExLjcxNCA1LjkzOSAxMS43MTQgOS40OTkgMCA4Ljc5MS0yNi41NjQgMTUuOTE5LTU5LjMzMSAxNS45MTkiIGZpbGw9IiNlMWUwZGYiLz4KICAgPHBhdGggaWQ9InBhdGg0ODk5IiBkPSJtMzQ1LjU4IDk0MC4yMmMtMzIuNTkxIDAtNTkuMDEyLTcuMDg4OS01OS4wMTItMTUuODMzIDAtMy41MjI1IDQuMjg3NS02Ljc3NjQgMTEuNTM5LTkuNDA2MiAwLjA0ODcgMC4wMzEyIDAuMDk1IDAuMDYxIDAuMTQ1IDAuMDkyMi03LjE0MjUgMi42MDg5LTExLjM2MiA1LjgyOTEtMTEuMzYyIDkuMzE0IDAgOC42OTYyIDI2LjI3NiAxNS43NDYgNTguNjkxIDE1Ljc0NiAzMi40MTQgMCA1OC42OS03LjA0OTggNTguNjktMTUuNzQ2IDAtMy40ODQ5LTQuMjItNi43MDUxLTExLjM2Mi05LjMxNCAwLjA1LTAuMDMxMiAwLjA5Ni0wLjA2MSAwLjE0NS0wLjA5MjIgNy4yNTEyIDIuNjI5OSAxMS41MzkgNS44ODM4IDExLjUzOSA5LjQwNjIgMCA4Ljc0MzYtMjYuNDIgMTUuODMzLTU5LjAxMSAxNS44MzMiIGZpbGw9IiNlMGUwZGUiLz4KICAgPHBhdGggaWQ9InBhdGg0OTAxIiBkPSJtMzQ1LjU4IDk0MC4xM2MtMzIuNDE1IDAtNTguNjkxLTcuMDQ5OC01OC42OTEtMTUuNzQ2IDAtMy40ODQ5IDQuMjItNi43MDUxIDExLjM2Mi05LjMxNCAwLjA0NjMgMC4wMzAzIDAuMDk4NyAwLjA2MjUgMC4xNDUgMC4wOTI4LTcuMDM2MiAyLjU4Ni0xMS4xODYgNS43NzM1LTExLjE4NiA5LjIyMTIgMCA4LjY0ODkgMjYuMTMyIDE1LjY2IDU4LjM3IDE1LjY2IDMyLjIzNiAwIDU4LjM3LTcuMDExMyA1OC4zNy0xNS42NiAwLTMuNDQ3OC00LjE1MTMtNi42MzUyLTExLjE4OC05LjIyMTIgMC4wNDYtMC4wMzAzIDAuMDk5LTAuMDYyNSAwLjE0NS0wLjA5MjggNy4xNDI1IDIuNjA4OSAxMS4zNjIgNS44MjkxIDExLjM2MiA5LjMxNCAwIDguNjk2Mi0yNi4yNzYgMTUuNzQ2LTU4LjY5IDE1Ljc0NiIgZmlsbD0iI2UwZGZkZCIvPgogICA8cGF0aCBpZD0icGF0aDQ5MDMiIGQ9Im0zNDUuNTggOTQwLjA1Yy0zMi4yMzggMC01OC4zNy03LjAxMTMtNTguMzctMTUuNjYgMC0zLjQ0NzggNC4xNS02LjYzNTIgMTEuMTg2LTkuMjIxMiAwLjA1IDAuMDMxMiAwLjA5NzUgMC4wNjExIDAuMTQ3NSAwLjA5MjQtNi45Mjg4IDIuNTY0OS0xMS4wMTQgNS43MTg4LTExLjAxNCA5LjEyODkgMCA4LjYwMTEgMjUuOTkgMTUuNTc1IDU4LjA1IDE1LjU3NSAzMi4wNTkgMCA1OC4wNDktNi45NzQxIDU4LjA0OS0xNS41NzUgMC0zLjQxMDEtNC4wODUtNi41NjQtMTEuMDEyLTkuMTI4OSAwLjA0OS0wLjAzMTIgMC4wOTYtMC4wNjExIDAuMTQ2My0wLjA5MjQgNy4wMzYyIDIuNTg2IDExLjE4OCA1Ljc3MzUgMTEuMTg4IDkuMjIxMiAwIDguNjQ4OS0yNi4xMzQgMTUuNjYtNTguMzcgMTUuNjYiIGZpbGw9IiNkZmRlZGMiLz4KICAgPHBhdGggaWQ9InBhdGg0OTA1IiBkPSJtMzQ1LjU4IDkzOS45NmMtMzIuMDYgMC01OC4wNS02Ljk3NDEtNTguMDUtMTUuNTc1IDAtMy40MTAxIDQuMDg1LTYuNTY0IDExLjAxNC05LjEyODkgMC4wNDg3IDAuMDMxMiAwLjA5NjIgMC4wNjE1IDAuMTQ2MjUgMC4wOTI4LTYuODIxMiAyLjU0MjUtMTAuODM5IDUuNjYzNi0xMC44MzkgOS4wMzYxIDAgOC41NTM4IDI1Ljg0NiAxNS40ODkgNTcuNzI5IDE1LjQ4OXM1Ny43MjgtNi45MzUgNTcuNzI4LTE1LjQ4OWMwLTMuMzcyNS00LjAxNjMtNi40OTM2LTEwLjgzOS05LjAzNjEgMC4wNS0wLjAzMTIgMC4wOTctMC4wNjE1IDAuMTQ3NS0wLjA5MjggNi45Mjc1IDIuNTY0OSAxMS4wMTIgNS43MTg4IDExLjAxMiA5LjEyODkgMCA4LjYwMTEtMjUuOTkgMTUuNTc1LTU4LjA0OSAxNS41NzUiIGZpbGw9IiNkZWRkZGIiLz4KICAgPHBhdGggaWQ9InBhdGg0OTA3IiBkPSJtMzQ1LjU4IDkzOS44N2MtMzEuODgyIDAtNTcuNzI5LTYuOTM1LTU3LjcyOS0xNS40ODkgMC0zLjM3MjUgNC4wMTc1LTYuNDkzNiAxMC44MzktOS4wMzYxIDAuMDUgMC4wMzEyIDAuMDk4NyAwLjA2MSAwLjE0NzUgMC4wOTIyLTYuNzE1IDIuNTIxNS0xMC42NjYgNS42MDg5LTEwLjY2NiA4Ljk0MzkgMCA4LjUwNjQgMjUuNzAyIDE1LjQwMiA1Ny40MDkgMTUuNDAyIDMxLjcwNSAwIDU3LjQwOC02Ljg5NiA1Ny40MDgtMTUuNDAyIDAtMy4zMzUtMy45NTEzLTYuNDIyNC0xMC42NjYtOC45NDM5IDAuMDQ5LTAuMDMxMiAwLjA5OS0wLjA2MSAwLjE0NzUtMC4wOTIyIDYuODIyNSAyLjU0MjUgMTAuODM5IDUuNjYzNiAxMC44MzkgOS4wMzYxIDAgOC41NTM4LTI1Ljg0NSAxNS40ODktNTcuNzI4IDE1LjQ4OSIgZmlsbD0iI2RkZGNkYSIvPgogICA8cGF0aCBpZD0icGF0aDQ5MDkiIGQ9Im0zNDUuNTggOTM5Ljc5Yy0zMS43MDYgMC01Ny40MDktNi44OTYtNTcuNDA5LTE1LjQwMiAwLTMuMzM1IDMuOTUxMi02LjQyMjQgMTAuNjY2LTguOTQzOSAwLjA1IDAuMDMwMyAwLjA5ODcgMC4wNjE1IDAuMTQ4NzUgMC4wOTI4LTYuNjEgMi40OTg2LTEwLjQ5NCA1LjU1MzgtMTAuNDk0IDguODUxMSAwIDguNDU5IDI1LjU1OSAxNS4zMTYgNTcuMDg4IDE1LjMxNiAzMS41MjggMCA1Ny4wODYtNi44NTc0IDU3LjA4Ni0xNS4zMTYgMC0zLjI5NzQtMy44ODM3LTYuMzUyNS0xMC40OTQtOC44NTExIDAuMDUtMC4wMzEyIDAuMDk5LTAuMDYyNSAwLjE0ODctMC4wOTI4IDYuNzE1IDIuNTIxNSAxMC42NjYgNS42MDg5IDEwLjY2NiA4Ljk0MzkgMCA4LjUwNjQtMjUuNzAyIDE1LjQwMi01Ny40MDggMTUuNDAyIiBmaWxsPSIjZGRkY2RhIi8+CiAgIDxwYXRoIGlkPSJwYXRoNDkxMSIgZD0ibTM0NS41OCA5MzkuN2MtMzEuNTI5IDAtNTcuMDg4LTYuODU3NC01Ny4wODgtMTUuMzE2IDAtMy4yOTc0IDMuODgzOC02LjM1MjUgMTAuNDk0LTguODUxMSAwLjA1IDAuMDI5OSAwLjEwMjUgMC4wNjM1IDAuMTUyNSAwLjA5MzctNi41MDUgMi40NzYxLTEwLjMyNCA1LjQ5NDYtMTAuMzI0IDguNzUzNSAwIDguNDE1IDI1LjQxNCAxNS4yMzMgNTYuNzY4IDE1LjIzM3M1Ni43NjgtNi44MTc5IDU2Ljc2OC0xNS4yMzNjMC0zLjI1ODktMy44Mi02LjI3ODktMTAuMzI4LTguNzU0OSAwLjA1LTAuMDMxMiAwLjEtMC4wNjI1IDAuMTUtMC4wOTI0IDYuNjEgMi40OTg2IDEwLjQ5NCA1LjU1MzggMTAuNDk0IDguODUxMSAwIDguNDU5LTI1LjU1OSAxNS4zMTYtNTcuMDg2IDE1LjMxNiIgZmlsbD0iI2RjZGJkOSIvPgogICA8cGF0aCBpZD0icGF0aDQ5MTMiIGQ9Im0zNTIuMyA5MjQuMzljMCAwLjk5NzUtMy4wMTEyIDEuODA1MS02LjcyNjIgMS44MDUxLTMuNzE2MiAwLTYuNzI3NS0wLjgwNzYyLTYuNzI3NS0xLjgwNTEgMC0wLjk5NjEyIDMuMDExMi0xLjgwNTEgNi43Mjc1LTEuODA1MSAzLjcxNSAwIDYuNzI2MiAwLjgwOSA2LjcyNjIgMS44MDUxIiBmaWxsPSIjMTAwZjBkIi8+CiAgPC9nPgogIDxnIGlkPSJnNDk5NyIgdHJhbnNmb3JtPSJtYXRyaXgoLjEyNSAwIDAgLS4xMjUgMTQuMDMyIDk0OC4xNCkiPgogICA8ZyBpZD0iZzQ5OTkiIGNsaXAtcGF0aD0idXJsKCNjbGlwUGF0aDUwMDEtOCkiPgogICAgPHBhdGggaWQ9InBhdGg1MDEzIiBkPSJtMjg4MC41IDM3MzYuOWMwIDEwNS4yNi04Ni4xMiAxOTEuMzgtMTkxLjM4IDE5MS4zOGgtNzMuNDRjLTEwLjU1IDAtMjAuOTEtMC44Ni0zMS0yLjUydjE3NjMuOGMwIDMwLjQ2LTI0LjcgNTUuMTYtNTUuMTcgNTUuMTYtMzAuNDYgMC01NS4xNi0yNC43LTU1LjE2LTU1LjE2di0xODIzLjljLTMxLjA2LTM0LjA2LTUwLjA2LTc5LjI4LTUwLjA2LTEyOC43MnYxOTI1YzAgMTA1LjI2IDg2LjEyIDE5MS4zOCAxOTEuMzkgMTkxLjM4aDczLjQ0YzEwNS4yNiAwIDE5MS4zOC04Ni4xMiAxOTEuMzgtMTkxLjM4di0xOTI1IiBmaWxsPSJ1cmwoI2xpbmVhckdyYWRpZW50NTAwNSkiLz4KICAgPC9nPgogIDwvZz4KICA8ZyBpZD0iZzUwMTUiIHRyYW5zZm9ybT0ibWF0cml4KC4xMjUgMCAwIC0uMTI1IDE0LjAzMiA5NDguMTQpIj4KICAgPGcgaWQ9Imc1MDE3IiBjbGlwLXBhdGg9InVybCgjY2xpcFBhdGg1MDE5LTQpIj4KICAgIDxwYXRoIGlkPSJwYXRoNTAyOSIgZD0ibTI4ODAuNSAxNDAwLjdjLTcwLjA4IDI5Ljc2LTE0Ny4xNyA0Ni4yMi0yMjguMSA0Ni4yMi04MC45NCAwLTE1OC4wMy0xNi40Ni0yMjguMTEtNDYuMjJ2MjMzNi4yYzAgNDkuNDQgMTkgOTQuNjYgNTAuMDYgMTI4Ljcydi0yMjc4LjhjMC0zMC40NyAyNC43LTU1LjE2IDU1LjE2LTU1LjE2IDMwLjQ3IDAgNTUuMTcgMjQuNjkgNTUuMTcgNTUuMTZ2MjMzOC45YzEwLjA5IDEuNjYgMjAuNDUgMi41MiAzMSAyLjUyaDczLjQ0YzEwNS4yNiAwIDE5MS4zOC04Ni4xMiAxOTEuMzgtMTkxLjM4di0yMzM2LjIiIGZpbGw9InVybCgjbGluZWFyR3JhZGllbnQ1MDIzKSIvPgogICA8L2c+CiAgPC9nPgogIDxnIGlkPSJnNTAzMSIgdHJhbnNmb3JtPSJtYXRyaXgoLjEyNSAwIDAgLS4xMjUgMTQuMDMyIDk0OC4xNCkiPgogICA8ZyBpZD0iZzUwMzMiIGNsaXAtcGF0aD0idXJsKCNjbGlwUGF0aDUwMzUtMikiPgogICAgPHBhdGggaWQ9InBhdGg1MDQ1IiBkPSJtMzIzNi41IDg2Mi43NWMwLTMyMi42NC0yNjEuNTQtNTg0LjE4LTU4NC4xNy01ODQuMTgtMzIyLjY0IDAtNTg0LjE4IDI2MS41NC01ODQuMTggNTg0LjE4IDAgMzIyLjYzIDI2MS41NCA1ODQuMTggNTg0LjE4IDU4NC4xOCAzMjIuNjMgMCA1ODQuMTctMjYxLjU1IDU4NC4xNy01ODQuMTgiIGZpbGw9InVybCgjcmFkaWFsR3JhZGllbnQ1MDcxLTYpIi8+CiAgIDwvZz4KICA8L2c+CiAgPGcgaWQ9Imc1MDQ3IiB0cmFuc2Zvcm09Im1hdHJpeCguMTI1IDAgMCAtLjEyNSAxNC4wMzIgOTQ4LjE0KSI+CiAgIDxnIGlkPSJnNTA0OSIgY2xpcC1wYXRoPSJ1cmwoI2NsaXBQYXRoNTA1MS05KSI+CiAgICA8cGF0aCBpZD0icGF0aDUwNjEiIGQ9Im0yOTk0LjkgMTE3MnYyLjAzLTEuMDEtMS4wMiIgZmlsbD0idXJsKCNyYWRpYWxHcmFkaWVudDUwNzEtNikiLz4KICAgPC9nPgogIDwvZz4KICA8ZyBpZD0iZzUwNjMiIHRyYW5zZm9ybT0ibWF0cml4KC4xMjUgMCAwIC0uMTI1IDE0LjAzMiA5NDguMTQpIj4KICAgPGcgaWQ9Imc1MDY1IiBjbGlwLXBhdGg9InVybCgjY2xpcFBhdGg1MDY3LTQpIj4KICAgIDxwYXRoIGlkPSJwYXRoNTA3NyIgZD0ibTI4NzIuOSAxMDA2LjljNzQuNTkgMzkuODMgMTIxLjk5IDk5LjQ5IDEyMS45OSAxNjYuMTZ2LTEuMDJjLTAuNDgtNjYuMjYtNDcuNzYtMTI1LjUxLTEyMS45OS0xNjUuMTRtMTIxLjk5IDE2Ni4xNmMwIDMxLjM5LTEwLjUgNjEuMjEtMjkuNCA4OC4xNSAxOC43LTI2LjY1IDI5LjE4LTU2LjEzIDI5LjQtODcuMTR2LTEuMDEiIGZpbGw9InVybCgjcmFkaWFsR3JhZGllbnQ1MDcxLTYpIi8+CiAgIDwvZz4KICA8L2c+CiAgPGcgaWQ9Imc1MDc5IiB0cmFuc2Zvcm09Im1hdHJpeCguMTI1IDAgMCAtLjEyNSAxNC4wMzIgOTQ4LjE0KSI+CiAgIDxnIGlkPSJnNTA4MSIgY2xpcC1wYXRoPSJ1cmwoI2NsaXBQYXRoNTA4My01KSI+CiAgICA8ZyBpZD0iZzUwODciIHRyYW5zZm9ybT0ibWF0cml4KDY5Ni4yLDAsMCw0NTMuMiwyMzAzLjksOTQ1LjkpIj4KICAgICA8aW1hZ2UgaWQ9ImltYWdlNTA4OSIgeGxpbms6aHJlZj0iZGF0YTppbWFnZS9wbmc7YmFzZTY0LGlWQk9SdzBLR2dvQUFBQU5TVWhFVWdBQUFKRUFBQUJlQ0FZQUFBQWpadlpDQUFBQUJITkNTVlFJQ0FnSWZBaGtpQUFBSGMxSlJFRlVlSnpOWGQyYTZ5aU1sTngrLzdmZHZabGp0QmNncVNRRWR0THA3dVdiT1ozWUlEQVVwUi9BNGYvOTczK0UvaUR4cCtSOFN0QWZKL25RS1B6RllKNlgvUHZsS3ArUCtzRVB1dVJQcHNEZnBDYXZ6cGpmNlp5elNmdlJDcDR3eFNwTDIvVEJld1QwRjRoN3JhWHZ0UEJKbVU4eFhaWE82MGRBSktQcnBIekNPMkM5QTVEN01uOUZXZnVXL1FSbzFvRGhVZmF6TnNCSG1jakJJZUZCWTVObFBLUXM3dGVkZEFlOHU0NzlLOVBwdTlCOXdpQXhDNmUvZUYvQ3ZVK3gwM25KZjk4V1lnTU1qVm9OMnZKNmRRTUJicktMeWg2a3Z6TEEzeHNvcHByQ2o4ZnlWOVZXMTc4THB2T1M2KzNDUUR6OWV6RlFqOEdFQU53T2VOdktYU1Y1QVBCUHBXK3pEeEdWclJ4anRSdjA3NERuM1hhZi85N3d6bmp4ZVE4bU5vMGNyaGVaK2VIVTRGRDd1a3l1b3N6NTduUjhRSEgzb3RXQ2ZOWUdLUUFtUlNWQ1hNcXMyaU9MejAvU2VlMWNJRWhNUkR4Y2JpSHZPNzJDQXlwUzJ6a2x1Qlo1dGM3NUtoWit4aW01MHlvd3Y1Mlc2dVIxdnF1QVVOczJjeE9lcXFsOHFZTXNqcDEvZnRaVFo2TUhUQ1QrOStEeFFhSWhUWlNaZ2NZMWJOVnJLbStWamhmTm9teEovTGFmOXFycjhuQmVXM29Pb01uRkdaK3lvYzJ4SFRjRDlOZzcwOEZYQytxd1N0dGs2bklJRWlhVnM1OXNUeTdid3owRkh6N2hieG5ZNzlnYnQ2Nzd3N284TDdKWXpDUlM4WHlmYm8yOGcyVkZjNURPZjYxbUlwM3RwcmFTMnNuUXc4R0pnZWFMME51b3huQVZtR1orM3doZWd1V1BJOXp2bWw2eUtWc3lrWDNTdmovU1BTbmEwNEtzOEhsOHFWaHlDallxZUM1UVlRRWdtTmV5U0ZBWkZtZ3N5blRiS0xja2NwbDlLNGhyeHlTaG5vZUQ5VWxpZWhVZjd3WU5jOC9XeG5PMnBYU2NvNTNUeE0wUVJNSUVwbFFGZ3Vsc1dGU0Vtc1ExSytiTVFuNnZRWUFJaFFidmpkMWdOTU04NVYxQktsd0RScHp1VFMyclV4bUt1aW56bmZSVTlpN2ZFK040ZVUyQnhCd041bFRPMWhjZ2t5UXF3UElONUJJUm5WZTd6TTFtRW1MdWlPUmg2MWhoOE1iNndFdFFRNEdoRnA2M2d4R0F5ekh6SXN4R0pETlVvb2Y0L3krOXpFeFRnZHBGZDlub1ZXRytZK1FSeTV6WnAycGpIOXMxZUxwS1pRT2xndlFVYW1Od2g3TW5FZ1pueFI1Ni8wZ0R5U1RSYU9QY3pKUnVhR2pMTWkrajUzZmg5cXFidjJXay9IMXJ1S050b3l3VEoyc2RhMUptS3VvWEI1YzBaQ3FtczhrL1UxbEVIUlFpRHA3SU5yTmJkS1ZMQjZlNUExOE84a1lld2J3Yk1qaURkWjZIRmNubFVFT1Y2bERuejZkOUFERzcxcmxzL3A0OHFxSmNnMERMenU5dWs4MDB5OFg2bkpsbTkzOHdrWXhnSWx2Rmh6TWhNYWZtVEZnU2tqR1NsOGdVbHlFYTRKeEZwTVo3UFRuL05rMFpLOFBwbDN6N2xQWUJ4SGtpaGJMRnRlZ2RWVDBkb3orcm1KTUJSTlJBQWFDa05vaDRQZEg5NzR2cGZWT2FzdEFSSDArajFHTGxtSGlJWVJoa2pUOXEwd3dLN0krYVl6dVpmdkVlVVdlNDFiaXY1dEFXSnNod1A0eW5wNTdobkMzN3RYdTU4ZXV4dk5lS2kyaFRCZEFBZVRnVFhjNUFTVlJyUkdlVHkrMGFRY041eEduUUdHYS9IL1dRLzZtQWhUeDJiSUF4QVdzeDhDc1Y5NVM1Zm5LRDFqdHBhd3ROQTc4Q1MxeElueG1vQU15aWpuajlzR3NJTVB2TEEwUkJFak1kSThpSHdvOHh5Z2NSdGVHNWRhYXFqZTZzd1MzaUxUbFNnUW5uUnd3MVBCbjRpdHlMaHYxKzJyVDlsU1dSYnNkY0lMSitxR0RMUEdBZnphZGdZVHFzWFMzdDhuQUdjcnljUWhjMVlXSVdBMFlBQmJDVXFpcFZaWjFvZW5YcVZEb3JzZW5QQTJRR3Rsb01yS1F5UFM4QXFweVRhNFlxYjRhTTM2U21oM3VmZDlZUkVZWURFUVFNZVNXQmJwYVkyU0xMMWI0VHBSRzdEM2FQSUZBWGNsbnpNcDBpRnhHd3lVRVM3UjN4MkUyUEo4bFFjVjVGQjVjUWovZ0JlbFVJUEcyRXFSK2hrcTM4d2FacEZJQlR4MkFYQXEzT0l1OFBxTGRxaTB0dFpHc1Raa3R2WnFtMWR6WXhTOG9UMUZRSUxzYmVFL2dlZ1JPdk4yakxLU1JFSWpiVEc3bG5sbzFuR2F0eENJU2VaMVFqc2JvSVJtK283UVNnTEI4ZkplZW5rRi9MYUp2ek5leUVYZnFVbHF2VXd5djVaNXNRbm0raGtpbzVSRHpaUTVsUllqUjZBTWtBRTZ4YUFKNDdXR0Y1Uk1qalJKMWQrbTFkTWtXMEhKU0JNejdMdklwUEJQRW11RGNQT29JbFJZQlNMS3JxdG9xQk10M205RnZiUXBhMnpxTENQajF6bytjblJEWFV5K0ZrakhHbnlEQ2pYUnNpMXVCTUx0L2dTNVRaamU2elNSdEJ3S0VaNFRtWXhFWmZPK1hMS21rZXdHTjhCR2VWaWNuZ0hwSGZ6N013MjJVNFA3Q05GU3VWVHd2cGlTRzc4aUJOeGtlUVZ4bTRzcERQUmE3NWU2WGVNZ3R4ZVc4c1k5QkZZTjFPb1FHaHZIamJpT2lnVStnL3VvVG9HUFpPRTRnRndTTjR4QUJORFRIR09raElFcGdJYkNkZjhmY01xdUtxTVF1THdPbGVhZmdYTWxibDkwa2VndVJWcVRmM054bml3QlcyVnM0SEJUeXVNM041QjA2bkVDOXkxV0NVV0krNllTS3RSNng5NXZ1b1RzekJhQkNMN3pkQ3pWb01MbEdVcFprNUE4N3l6bHlSR2FmQjUzbnI2NXhrYys4bjB3NDQxYjNNMmxWeTgrQUkxM3lzSWxqODNoaW54RVljZWxOWk1ObERHVUQyblZXZFhiYW1GZDM1d1NEb1NrRkN1K1p3djU0c01BNTJETnBTOXRDTHdjL0hqN0tNN09ST2FZdVVuN0tBVnJXdEc3TnpFcEc3SzdYVXk4OXUrQ1JuWXhzUkhXQW8reFNmQTQ4T3FHeVVhOTZUNkJwYlA4aWdheXY2cEp1VytoZTloamFQTjBLR1d1dUp3MExzMms2WlRtSkl3c0ZvV01VbVFqTm0wTjdLcWJhaFBwc3FHeWRjV2VCNHRuT1N6MW1vbUozc0RENTM2d2ZqV0lUYnAyY1pEcWl1Z1RmWGFEQlJtUHhqS3dqYVFjd3kyUy9NSGdyQXRhOEd3dlNqMlVOSnhSR0JYUVM2TUd6a1REMldnYkRxMEczMGVsUHVwOVBPM25xbS9qaTUyTmtWbnd2bDlTNzhncXhuK1NiMUJleERDRlpRWjBTdXRuRExhOCsybU4weWxqL0FOdXAyRVlOeHhDQ0pBckMwSU5wZE9rZXFDR3UxUlRjQ0txbmJEU1BsOUYxV2VnTElOUU90RGVWWUxtK004YzAwRVJ4Nk5TZVljdUx5ZE1rYzE4bXFkanNUNlNqMTFJVG9iSFFOdGNYRTNMMnZ3MkkvRW13bDFCL1Q0RnRmWEFZV0FTbTZIdWV0Uzg5WFBqZzhQckxlSzZOZXFkQWJsL203cWR6enZNeGJYQnNEbXZPc3JOTDZHZnRsTkpTSkhHQWVFOEpyWU9uS3JOWjBQQytJSndrSm5TVGRvdEVGVmlZaDRtaUR3SllURzgwSUhMK3VvQ3Z0bGZTQTZER0VEc2hBTlhVWmpiOXNIMkhaWGZvclZZYnBmdlZGRjM2RWRnd1J5MGZWdHBLdmNiNmVnTU1ENjNCUVhRZ29sa1o2WEZXSTZGUmhseEI5S1hoRUJ5T2hQWUhEQTRvS3JOaDh6SXUyZ00yeFFrV1pYSlVBTjRSaXVLc0NhazVWaC82MHEvOE9TQ2VRaEZaZW9JTFc1YXBsaVYyYnZEZTVCTXRjNkREMjBYcEVtRTRaSjJDWnhKWVhqa0UvT2tnV2VSNU1oY1kxSXpOcGZ2aWNCMW9QQWhBSkdPYldSOXJVTkd1aVBKUTFQYWVvakpncTF2cU5WTlcxQ2xMc041MWhlUzR6dWR6STJGa2Rac1BacjdsY3JNUHpYeVRrYTNQS1dDZHVTVlczL0pLT1VlWjVHMGRRVmFMcUM5bEJqTWxjSGVFZ1lrUDFTaTkvQkpVWXU3QmlqM29mVXcydTFZQjhjdWYxNDQzNWVmSXNVdVZBaU9BZWFoam9ORTNRMXBsa0JPa0VBVXBKb1BMVUNJSG00UUlob2JQSmYxT1FrWW02T3k1a3F5bk0ycmowaEtOVmF1TWdhMWxuWU14STBPU21vU0lWNXoxbDRLcTR5ZDRSc1MwWDNpd3A3UVdvTGpROFo2MjJjRlRwM1pjdnhOcHhJR1Y2d05WV2ozS0xTR0NkNk1uaGl4a3l3NGV0SDZuT1VzMEZGbU8zaWFJTFAyTTJmek1XWW5maVp6dHB0cDJ3Q3hSMHNRclh0YmpJbW14NmIwT1JudGhKdTdUYjgvT2c5UGR5eWMxOXluR21QQ1Y0dW9mTFdUT0FFaXRWZGhITmF0THJFdDBLY3JucUdDTWcxRldMQ21xRGxiNVVlT0JabWRXSEpQYkJKOUJtY3phUlk1YzBHTXhwWjRFM3RmVHMzb1hCcStEYjFmT0lpVkttT0gxamF4dzhGZHNsUm9QeU13UFZyRlF0WjF4d0grdlVpTFdDN1JTNmlGVWRzYXEwTmxqRzBSelpSWTF1OGZzVVZVNitwdmx4OEd2Tk1aTjFrOXFqSzFubmJ2UTJTUGtPQjFYVnZFSnFVOVp4WWIxU2VQL3FQUWxzVk9ldGdWdGJpcmd4emVvWG9sUGtIelZtT3FodmdjaXM1RlFucm1hd2hzUVNBZ3lteXlmOVhCc0hRQkZSZU1FVjdQNjFzdGlRZkI1Tzg3d3k4SzhPN0hmVFhWWDVQclp0M2t2TktaK2FCU3ZHMGU4KzNXZVFIU0UveWxEbXd5MkhHRUxvQzhDc1ROU3o5VVZZUHljcTBCSzBUL1NGRDdNTkJDRUJlUFlPbk1SQWVqOHhGTkdLS1B6cCt6WW9aOENwVENIZ096YlNkMUxsWFlVazVjZFVPdWFvRjFudlhQcHNPK205UHVwWlpvTzFNcWFvU21PZGZkUlBFVmpGSjlqQnlDMEk2ZTU5TjZGeHUrb1hLOU5BSElmVGl4a0FJZWp5dXhIcjdGUFpPams4NEtjd2s0dElSRWV4YVd0RnpuYi9tNmM5NGlETjlhOWMvN2oxanhKVmVBOU9qSld5VjJxb1loaTluaGRVRlVndDFDVUwrd21QRTZGTnBLckdtdExNRGdrMmpReHh3ejNycm5oNkFZUStISHRYNEJQSWFMcktEUjJHTXlBWGhNVDVNdVR0VEJXVEw1dlU2VTdGNFVMd0s4bEx4QmFWZHM2R2xkYm55SlRIVzdqSGZKZ1d5RldzbjlWSFh3dkc0MFhLTzBNWGdKeStuMGowVUNMUlFiNlc1dnVNT0hoYlBNYmJ6cFdGQ1NTQlNVekZzUUkxTHFVRVZXZVB3Y1p1SmdOUTFma3dXZGd3Wi9JZzZWYmJWZi9kcWJzNytOemRYLzhtUjFEK1ExWmtwL2pPb0Y2bTJLa1YyVVV1aWd1c2N4bk1IN2JRWnBVb3V0cXYzNVhCbUVUNmFKNGlEUVpTLzdJeFRGWXZ2cUtmUWdBMDVwczRrK1FUVGZwdlJUS1JmZHFVTjljL3lVbmJQR05Yck5NbjdLWHNhenl2VGFoYXJTZWFHYVBpNHBnSEJwcUlxZ1ZXM0V3VzE3OXlQUnpVWUFUUUFKVTRRL2ExTTBHbXdPaXlSNi90dXJpWEZZMXJJN29KZEVxakRQTHhGQzFEZmRCSDArQUV1VW5CN0FMTm56bWQ4VE5KSlB0aHN4R2NQOGRCVDFOVjF0Y1JGTGJaSiszSnppQlRkakwyQ2UzczN2alpxRHQ2NW1XSldnQnBDY09NWWhlbHJCVmV2cEFHTThlRFZCWHRvdGdZWXlMYXNRL1dNelBRSDcxTlpwbDg4Ty9aY3JKbmltd091S1BJQStxcnFpSmNDMmR4Q01NR0dMTHBjblJoekZjNmVweUl4bDdabnNmRU5ISEdPTVFIbWJnTFBJWlJoL0dhSmpLZjJ3STVtZW0wc2NIR0pnU29zaGprNWRnSFM2d3NHT2pwK3RpN2FiVjBzaUxFSFdEaVVaOFpHSkdwZVBwY2VXZStDcS81THhMUnQzOUVWWVZiWnJXOHFzVTJaSjFxSG4rcElTM1MzWFp5VmpoTTFjWEhybC9hUmhFYzdKY2l1MHdtcE9lelJkVmtYSVA0YjdubE1uMzRVUG8rT09lM3Z1Sy9tbWVlUWpZaXhZcThsL015UWJZUWpiY3BnRXc4ckJFWktoOHJPbFU3MnRJQ1N6amhnYW9PRzJPTUJLMVRockJqMkVUMFZVQ05RYVkyTklKRWJsVVJCaTJqYlFTemxXYVFyajcxZHIwR2d2M0xBT08xZVVDWGlubGZaMkNwMklxZHJYUk41UXJHQXdiQytycnlZbE50SFM5czNydWQ5c2lkcng4YVM5aWFRV0JrSTVqWXZvOU80MnhnWjQvUGVSWlZWR0FiVUg3VGdpNmtZR0VFd00rcWN3Zk9UL0tTOXhkV1dIbFZzN3FKY3B3ZGF0Yyt5NEk2UnA5WDhuQ1JLUWN0Y1lHVmFENUZhM1VQZWJZOWxrU29jYmQ5RkJqOU9oc2dlamIzcnJLMmRrWlJSS2pIMW8vYlpuTlNLQzVoeExQN2tVZWdGL3d6L21BTnRtV0hoazlyc0x0cWx2WE5EQlhmdWx2eHJFMi9rZC92VksvVlVFTzRXUitybXNvcTB0VmIzaTNRTlEyVE9rUk4raWQ4eWNNcDhzKzNaVWhYUlYrRFFnd3N1cXdCWU5LVi80NGNmL2pBYU1CZWh2M3hqeDk4Qk44Z1VXNkFrTGpzeUZZZzI5cEFMNmQzclprVlJyWTRubTZ1M3Y0eHE3MUtiZ1JpbHNmbHBJcExHaGp4OXVDaUgzVGt3RllYMkx4RTQvMUUyUVpSdDU5SEk2eXBBMWpSMFBZQUpUNlViUUUzTnNKNzF1S0NmYlRPQlJNRitlL1F5bHJtL2RWWG9QWmUyK1lsRGxSaHlQZkpmbEd6Z2VLMldYdXpuUUhFeDdQblJaWUNRUmJwMXZwd29rS0FWR2dzd0xMMGhVdDJXMlhhOWpFczdYVGtyYXMvWUJxTk05bURqdnY1MEs0dVJiQW9BTkdZaWRvNGVtVmtZQTQzVXo2L2xtd0NlcFpXYkhhM3pyWktxMFhZMHJiaGVXWGRaU0RUajFWNEdPRFYrNmxqRURMNnZPcXUyOGlKNW10bUc3bDhJb3ZyU1ZlVnRqMjJzZERYMEEvYTJMN3RnNjBWcVBZOEdDbmpPNEdCcnV2VHZnY2FIK3dyNlNsbHNwVlhGcGhuTTRnQjc1dHJUOUorbSt4emFiTDlkRmNvTXROczc3Z3E2aVBHZEF6dmVxNER2ZVFEeXZ1QnJ5YmUwMFM2Tjh5bjRTWE9oQjE0WFo2L2dsaklER3NpVlROTVRab0ZEOXVRY1F6UjBXTlRjMGNvYkFVWk13TnRwVXVJdnJReFZsZnNIVk5vN1BtaUtSZzdFK3Q0bFN6ZXRZYzA3ZXJiM3J0OTF4QjRadkNnZlRwbHRkZnpOWXIyWWUyTnBRMXRnZlhHcjdvQUF6V0pMT2pNRllLTnZoa2tXaUtoT2IyWjBqcmRlaE5JS2ZXQXJBb08xN0g2bktvS1ViUDZneWFpbmRocFB5anJGemxZOVVXUUVnZmtLYUFxS2JFdGUwblRpMDJYY3ZFZUx0WXF6SHh3d3lwYzhZTTZEaGFmL2NZNmJzVVNDNDVtbEJQVzRBWlRuWjJhMmpqSncrWTJrNERqcFRiSWlOVWNNTERLTmt3eXZmVkQyYy9OWkZkOXhQN1F4NWdaREFhN2h4SThDcnNiRm54eGFiaWVTNVdqOUlLcWVaaHlWR3NWNWJyenZseTFESk81NktPRzd6S1NYRFp1alVYN3htMmZDRFMzaDNSeU05enpkdXNTeVNuMmN1MyttcjBBcE5GY1haM1hONlFKVWRqY2p5amxGTHZwUDIzVWdrbW8zcGwrOTlmUkNIWkhLVE4zOXQwclpMS0MrQlJJbnFjN2JxdkJIM0lBT25TUTQrdGw4SVhsOHdhYy90c2N0WnBEdVZoS3Yrc2JQWTBSckt3dWt3aWRYVjBNaGxCQVFOaTNiMEJ6TjEwSDFVRkNJMjhMMzUxOVdtaGdkT2xqZ3dPNzBRM3pKRkRnSnJaK244UDlPVW1vODdzSjdaTGJ2S0h0a1dWWFhwem54NFMyMG1wTENGRS8vdXpUTTc0SkpMNU1xTi92YmVqYlpkMSswckxtNFEwaUdkdGpuUXI5dEVlRFVSVjdTc0JqVjIzSjA0cVorZ2Q4dkF3WTdRQjlKRk54aEY1YjZEWXZNcVdvbHZhS0lyWm5JM1NUbnZCYWJXSDZ2WFRWdXJCcWkxQThJOCtFZmVmWGN3MU1SUDlvdU42cC92emF2clJUUzIyUHdFWUt3dUdkaVc2UFpjQUtqOHdpcEQ5VDFjTUE4QXBpZHV2Y1hzQVF2RFJuTEFVRE5wMUlZS0RUSG1IUFJHNUFocWRLN0lZZTNQL2ZWTGR2WHQvUzY3Rk1wT2xZQW8vK3hITFIzb2tiNzExbGVaa20zYkM1ckU1a09yZVZORm80dkxNK3pDTCthaGxWUm5HN2hxczh0R2xXMFd1Tkg4VnRzazdhRWJEMldKQXZkaFBLblFLSWdpQ0xaZTdTSjdjV3ZST0lySXFzWWtNOUlWTW9FMTFFQVNCUk9mcUNhalFCM0JzWDIrNmhmWHlRcnZ6SExZamVKbmQ0VHBGT2M4eTZvYXd2bGhJcktrZGpCbGo4aDRZVmFyanhua2VJdmJreExMRjYzTTlkMlVQYVJQeEZ4L0FyaldyWWt6OTAxZFV4WWJjbEpTZHpIc3Vid2dHMTI3NnpZZVJSUHN4ckFJQ2liZk9NV1IyN0oxV1hRWTlMUXFCQlI2UlJRMEJDd0JGWCtSdW90ck5udllDQmZGVS9QdmZVSEZOYmJoY0o2YThPQ1NLSEFNL2lIcDJ1REN2TGpZeXBQaDVyZWZHeVA3aXJYSytyc0RXS3A3amJQelF6UzgwYi91a1I5NVg1L05jTjQ3Tm9QRnB5QjAyMmpWK012MHlFKzZteFRGei9pZ0poMGpHNXVoVmtPVjhXT1lYK2tSRFJsekFKREliYk5WNlJyYndQMVllTjZGNlhQM0pPQVZNeUFBVEdqN0dMRU9GYjFmVlRLNENSVi8vZHU1dFZYcFZlQWRSYXhtczZySTQwRSttcm9HT3FBVWNwZnFNVEV1OUZhN05xZzU4ZjQ1Q3ZadXJNMk01RFBIN2JnMGRuS0NHb2UwL2UwV1lIOGZqWkJnUE9HSDV4MHRQeTJrRGNCY2tFZGhPd1hkeDduUjVIVmF4OWxkQkJDQ1pVYjZzQnJuNjc0dzF6WnBsV3AwdVFJYkZ0ZWFDbmZVWGdrdWNVbHk3Y1UwTmxqK3FwNTlWcGx0U1pxRnQvaklrZTkwcm9aLzN0MTk1MjF1MnhqcXZ1LzRzYnpFWWhRNFNReFkxSWRBQXZBNVF2cE1aNTd2R2xaRFlLQW9qcjdhMnA5ekx6aFBQNVZ1UGFDc25lMEhjMzdrL3l0blpTVFBtMWRsTXVtYTZNdkptWloyL081YzF4ZklGeitHSkhWbFVEcFY0MGRjY1dKZmZSYnQzRnY2VHZQdVJ4dFBWTDNmMVEzazlxMjRhMDhRVTlNWUduUnRkYkYyenQwWTJCS0F3L21tSjV5MnhPSGF3aFNHQS9FYkZqbHNxNysyVGFCUXcxNVhQNEVnWXV5eU83aDBkNENLNXJ2aGhrVERaTUtJdGJZSDFyQjliWnZicjQ1aEFpUEMzUzZCS2kwMGZOYmU4MmdFU0l6ekQ3UnhXTVJ1ODBWMGkzVmRKUVo0SE9FYVNValdITTQxc3pnMng3TUNOajY0N2pobG4yQUh1VzlyamIzOFVYZU0yQVMrQ0M3NjdTMGJoRmhZUEpEWWVvNnFJUkhxL3IvaVFQQ1JoakVSSFJNUUNrZThlNkJIL2RIb3NGSFUyY3VPUG5kU3NneU95Z1JrUmY3TWp1KzRpd21jWkZzZWxEYkRqblB5VEc2cEt6UEkxMGpFUzFoOVJ5OEp6dk82UzBQbk5mSjJmZ1BUUEc4MlJFOGYyNk5BVVJnM1JwVkJuemFKVDdWbys4S1kxc1RQdGVWN2VoakkzNks0aHhtNlQrK2hXWmVsSThUbmFNYlQ0ei9pSWl4MXArTzRjeHhrQ1BrTnNpQ2hSdFlBU1ZBak4zQkc0UndhNVRacW9IdEFMT3B4SWE3Q3RBVGE0NjNwUDVPaktOcTVYTVpCS3ZRNzhTbEd4RHZLc3hOTHAxaDVEK0g2WXRLZGxnNi9WWFljNitkdEtDcTZjN0Q4VllCemVnaWMxMk5rTTR2UU43UEVFREpSWGV3RWJtTnhpUjZvUDRFV3ZYODM1ZUlUNFc1b3RySm5NWGFOcWZ5MzhWWU0rWXA1UnEvYWtETHV1OGhFRkV6U2VVZjg3S09GdWlIR1VxRFBQRzNZK3FSVngxZVJtMWpmeDVtOFR5cDI0andDMGRydFpNUHhFYXpMaFVFUlpYN2I1K1YzYUtkR3pxVFNqWVdObzVBU0RHV3JGYmNWWUdReGsrTHFQWkwybWROWE1zUWJlOFhOeVkxRXVSWlhUVUREUTlKcXIzd01PMnNuMGNsSjhkWVRCUWxuU1BFTnBhaDAxUVhTdUxiZVBPUkNoVEFVUzZVVzNjaTF0WWg4MHk4cHMyNWVHeWlzQzdyLzFoeUs3NUdsaU9sb1pUc2FqbWRpU1JFT2JObG5ROXNpRzBiaUgzV2JZNnN4YlplVnZqK3dNYmJscFlEVlNyKzRYY21LaEVhaHpJWjNrTE5CRHRINzEyK1JTWGZpLy9PdmJwSVNYUWcrUEFvb05uM3JicTM2YW1oczg2QTF6TElWWEVkNUQ1M2lOZHpwaVhia3MrQUpuMXl4N3lETTdnV3JEQUM5cHQrcEc3VkVQWlV4djUyRmExZTVCdCtoMTlnK3NWOGtUMWhoS3I2VlgxVmhGWE1nSnp4YWo1N2Rlb2lhWHZyVFZqdWpkQmY4SktGMk9aeEg0d1dOOEtPLzR6MVVORWNKcVdpYmdGTU1USGFYWVY4MWhFbXhJN1VaMHlZeUhvUGJtOCtlcm5rb1BqQVF2dDFKakp3eUgzWTBJRWZkZnpBWU9FOXZUODhZMW9pUTZNaGFpVENOeHJkbkpXN2RnK0lIYmFJOW9qQXpnMFhvWStnb1dzNE9CcVRhcS9tMGdZejV1TkVEckRDeUUwdTUwRVlhSnhicW92b1BZY2VhNTRrTkw5dHd5QzJscUpqTmp6WmEvbFoxUE5UbG4xenBZU2dqQnUzNmltaHU2RjFnQ0t4bkFpQjhjM29rSHNSN1FVdG12b3AvQ0drYjcwcEd1WXpueEVwd3BBZTBITHhnQzNYbU5LcUJnRExjNUl3QW9HSkcrZjBwYi9MMUFHdXFwNnA1SGFTbm9mdS9SdTBmWHBZdW1kM2YwZUJ1ZFNtWVVRM0ZMa1czbHdQazYrZ3lLdXpmRTBjUVRreFd2WUxwY1Mvb1oyc200RklmQ1VlcWJHYktjNmdEdjhvU1NlK01CN1FST3JMSnhSTnVwdUUxWHoxS1BheWxJMW1JYW9hVkNlck9TLzVLaHR5andCVm1sa0Y0TmI1US9QU2xRTXJyTnM1S2tLUUNBWC9rWUE2UlJmOXlKRXJEMHpTVjk0ZFkzTDhYalFHRzJHL1BIbzBEem9XdTZpYmxzZEFKb3VMd0lKRzV4WnB3SlR6cWQ1YzJmbGhNSE5UNmU3dGJNZGNITDVGVHZoOXdwQUZYaFdlU2RHelBXS0w5TnJmZ1RZS2VPSVVQUys1c1l5Q0hTMXBRMEJMeXNOdWxidEsrNjhCUklWOWMrR3NGKzRBOVJLenBORjB1K21aOTZYZnEvYkF6dEJiSkxWcWk0Q0tKZU5XMFpvQWdSKzNnR1hKTmRQcnM2bUJMYVJWaEE4b0FKSStaZEtzMXJEUWN4cXkvY3dSVmFxYktCS1BqNVVUbUVDL0VGNlVpMjREVjZ1R015S2ZWeEd2OTlYdVRqZEc4dllZTklHZHRtd2x0V2IyRW0vbnYybXZ1eTgxNEEyaktvMFlsdVVXTnBHZUQ4RFF5dG05cGx5R1B5aS9WT3B0enNiYUdmL3ZJS2RwL3owS1R6dWpPdGN6d3BBbVlHeW5EbUNYVjNMQzY1Y3lsVndlaHQ0Z0lqbXdSSm1pQnZsQnRYa0d4WlJVNWtNSkM4RCtTU0RveDZxVHhuVnVmNVhVcVh5bjZUWGpldTY3TXJRenZjcUFQVXllVm5kNjF1ellkMWJweWdMRGRzb013RlJaS05zR3hFNVA4WmRQNzNGYU9SV2pEVDlXSGV5cWZ5QjllRmpoMVUyVUFXcVZmb0xEVmVyanZYM2VpdEhUTlZtL0JXQTVxV08yaFlLQnJYNE5TRWVjU2VPTmxHMlE3UXkzeWh3RDZReUV2d0NrTFQ0eXBBdWplaVlkV3RZVjdKL0t0M1pYOVh0R1Voelk5R215ZXBtVldmbGlWVjloKzFRQUZWdHlkcnI3SHVsWnpiU2x1SzdHUlZJRnNsZU5GWVhXU3NWTmhubzFPTklSSENPTTZtcTZrR3FTTyt1WS83S3VONVZWd05wamZLS2hXN2pRSW1aWnNhWmJhR3BqUnVWRnBnbzM5aE5XT2NOZ2N6RHFPYklaSVJBVWdFOEJ5U1JsYko5Tm9wWXF1eWRYWHQvaVh5V2RlK0FQVStFZVN1SGZZWjhLL2FSQktxS2daUWpWc1owVm1QNngvTzVLaHRiUVdaVkpoUlo1Nm1ScWd4bUlFaU1sUE5tSUJGRlZxckFCSUw5YThGS2Z1OXY3SjZjOW95ME43WXI4T1E4bFp3TUlHU2hWK0prQ3N3WWNQUy9IckZXVlNaK2JFZkF4RlpQRFcyam1ZMzZ0Y3hJUk0vWklJT0ppSUx4L2NRejg3eFBWOHQrSnoxUlZaNDNwanU3cDBxckNaUkJWYkhRVGg2eWtCRFIvd0V3WWxkZjl6LzJkUUFBQUFCSlJVNUVya0pnZ2c9PSIgdHJhbnNmb3JtPSJtYXRyaXgoMSwwLDAsLTEsMCwxKSIgaGVpZ2h0PSIxIiB3aWR0aD0iMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSIvPgogICAgPC9nPgogICA8L2c+CiAgPC9nPgogIDxwYXRoIGlkPSJwYXRoNTA5MSIgZD0ibTM1MC4xNyAyMDAuOTdoLTkuMThjLTIxLjczOSAwLTM5LjQyNCAxNy42ODUtMzkuNDI0IDM5LjQyMnY1MjMuMDdjLTI3LjM0IDE1LjY0NC00NC41MDkgNDQuNzc1LTQ0LjUwOSA3Ni44MjYgMCA0OC44MTEgMzkuNzExIDg4LjUyMiA4OC41MjIgODguNTIyIDQ4LjgxIDAgODguNTIxLTM5LjcxMiA4OC41MjEtODguNTIyIDAtMzIuMDUyLTE3LjE2OS02MS4xODItNDQuNTA5LTc2LjgyNnYtNTIzLjA3YzAtMjEuNzM4LTE3LjY4NS0zOS40MjItMzkuNDIzLTM5LjQyMm0wIDE1LjVjMTMuMTU4IDAgMjMuOTIzIDEwLjc2NSAyMy45MjMgMjMuOTIydjUzMi42NWMyNi4xNjEgMTEuMTA2IDQ0LjUwOSAzNy4wMzEgNDQuNTA5IDY3LjI0MiAwIDQwLjMzLTMyLjY5MiA3My4wMjItNzMuMDIxIDczLjAyMi00MC4zMyAwLTczLjAyMi0zMi42OTItNzMuMDIyLTczLjAyMiAwLTMwLjIxMSAxOC4zNDktNTYuMTM2IDQ0LjUwOS02Ny4yNDJ2LTUzMi42NWMwLTEzLjE1OCAxMC43NjUtMjMuOTIyIDIzLjkyNC0yMy45MjJoOS4xOCIgZmlsbD0iIzEwMGYwZCIvPgogIDxnIGlkPSJnNTA5MyIgdHJhbnNmb3JtPSJtYXRyaXgoLjEyNSAwIDAgLS4xMjUgMTQuMDMyIDk0OC4xNCkiPgogICA8ZyBpZD0iZzUwOTUiIGNsaXAtcGF0aD0idXJsKCNjbGlwUGF0aDUwOTctNikiPgogICAgPHBhdGggaWQ9InBhdGg1MTExIiBkPSJtMjY4OS4xIDU5NDEuOWgtNzMuNDRjLTE1NC4zNyAwLTI3OS45Ni0xMjUuNTktMjc5Ljk2LTI3OS45NnYtNDIwNS41Yy0yMTguMTEtMTE2LjIyLTM1Ni4wNy0zNDMuMjItMzU2LjA3LTU5My43IDAtMzcwLjk2IDMwMS43OS02NzIuNzUgNjcyLjc1LTY3Mi43NSAzNzAuOTUgMCA2NzIuNzQgMzAxLjc5IDY3Mi43NCA2NzIuNzUgMCAyNTAuNDgtMTM3Ljk1IDQ3Ny40OC0zNTYuMDcgNTkzLjd2NDIwNS41YzAgMTU0LjM3LTEyNS41OSAyNzkuOTYtMjc5Ljk1IDI3OS45Nm0wLTg4LjU4YzEwNS4yNiAwIDE5MS4zOC04Ni4xMiAxOTEuMzgtMTkxLjM4di00MjYxLjJjMjA5LjI5LTg4Ljg1IDM1Ni4wNy0yOTYuMjUgMzU2LjA3LTUzNy45NCAwLTMyMi42My0yNjEuNTQtNTg0LjE4LTU4NC4xNy01ODQuMTgtMzIyLjY0IDAtNTg0LjE4IDI2MS41NS01ODQuMTggNTg0LjE4IDAgMjQxLjY5IDE0Ni43OSA0NDkuMDkgMzU2LjA3IDUzNy45NHY0MjYxLjJjMCAxMDUuMjYgODYuMTIgMTkxLjM4IDE5MS4zOSAxOTEuMzhoNzMuNDQiIGZpbGw9InVybCgjbGluZWFyR3JhZGllbnQ1MTAxKSIvPgogICA8L2c+CiAgPC9nPgogIDxnIGlkPSJnNTExMyIgdHJhbnNmb3JtPSJtYXRyaXgoLjEyNSAwIDAgLS4xMjUgMTQuMDMyIDk0OC4xNCkiPgogICA8ZyBpZD0iZzUxMTUiIGNsaXAtcGF0aD0idXJsKCNjbGlwUGF0aDUxMTctMCkiPgogICAgPHBhdGggaWQ9InBhdGg1MTI5IiBkPSJtMjQ3NC4zIDM4NjUuNnYxODIzLjljMCAzMC40NiAyNC43IDU1LjE2IDU1LjE2IDU1LjE2IDMwLjQ3IDAgNTUuMTctMjQuNyA1NS4xNy01NS4xNnYtMTc2My44Yy00My4zMy03LjE0LTgxLjg5LTI4Ljk2LTExMC4zMy02MC4xNCIgZmlsbD0idXJsKCNsaW5lYXJHcmFkaWVudDUxMjEpIi8+CiAgIDwvZz4KICA8L2c+CiAgPGcgaWQ9Imc1MTMxIiB0cmFuc2Zvcm09Im1hdHJpeCguMTI1IDAgMCAtLjEyNSAxNC4wMzIgOTQ4LjE0KSI+CiAgIDxnIGlkPSJnNTEzMyIgY2xpcC1wYXRoPSJ1cmwoI2NsaXBQYXRoNTEzNS03KSI+CiAgICA8cGF0aCBpZD0icGF0aDUxNDUiIGQ9Im0yNTI5LjUgMTUzMS43Yy0zMC40NiAwLTU1LjE2IDI0LjY5LTU1LjE2IDU1LjE2djIyNzguOGMyOC40NCAzMS4xOCA2NyA1MyAxMTAuMzMgNjAuMTR2LTIzMzguOWMwLTMwLjQ3LTI0LjctNTUuMTYtNTUuMTctNTUuMTYiIGZpbGw9InVybCgjbGluZWFyR3JhZGllbnQ1MTM5KSIvPgogICA8L2c+CiAgPC9nPgogIDxnIGlkPSJnNjEwNSIgdHJhbnNmb3JtPSJtYXRyaXgoLjEyNSAwIDAgLS4xMjUgMTQuMDMyIDk0OC4xNCkiPgogICA8ZyBpZD0iZzYxMDciIGNsaXAtcGF0aD0idXJsKCNjbGlwUGF0aDYxMDktOSkiPgogICAgPGcgaWQ9Imc2MTEzIiB0cmFuc2Zvcm09Im1hdHJpeCg5MTguMiwwLDAsMjUzLjIsMjE5Mi45LDYyLjkpIj4KICAgICA8aW1hZ2UgaWQ9ImltYWdlNjExNSIgeGxpbms6aHJlZj0iZGF0YTppbWFnZS9wbmc7YmFzZTY0LGlWQk9SdzBLR2dvQUFBQU5TVWhFVWdBQUFYNEFBQUJwQ0FZQUFBQTlma2hyQUFBQUJITkNTVlFJQ0FnSWZBaGtpQUFBSUFCSlJFRlVlSnp0blh2d0xWbFYzNys3ei95YmtNR0JZUmdId2tzR1poeFVOQ0dPb0lRU2Rhd2FTbFRFcWxGSFVTbUloQUJURXRSWWxCS2lwYUZDYVVKRlV4Z2ZWV0pSeG9JcXFBaUpLRVJOQ0NOUERUQno3M0R2OWM2ZCs1N0xuWEdHdVo3dS9MRjc3MTU3N2JYMm83dlA0M2R2cjZwZm5UNjkxMXA3ZDUveis2eTFWKy91WTdxdTY5REwxWS83QjlpMDNIUG9NSXd4TUtZQmdIN2JvT2xmVGRQNGZVNzh2bjdiMjVIM0RkRy8zT1hHWDd3VlprV09sMjdTN1lhZGs2Q05OVFd5UCs1VHM1ZVV0SThrc3FXU2Fpdnd2UXNaL29NeTBpWjhLRzJpYjJFbnQ0OVUyUHV1N2NpMnJodm9kWXJPdXNQZGIvd1FzWWtQcGxOT1Vpc2RTL0crdGtDblM3OW5ZKzNBOWZVKzZibmh2cmtmZXBLcHo1YjQrUHF2ZXg0MktlY3ZYUFRicG0zYjZHdzkvaC85dzQwTzRON0Q5eTNnbjBsS2d3Q1FnWHVqNkFtNnNtOWxnQlVCSWV1TFM2bmVwaVVCZENvYTNJRkU4Q2lBdktpV0FIM2tJeFVVQ21FZjJpL2dyd0gvcG9FUEFPY2UvSEx3UGdBLy9SQ3V1ZnB4R3gzSW9mdU8yQUVzNEo5TlVrRUFtR2syVU9BN3RKWDNwK2hmK2xFV0I0Z05TZ3JtZ1Y1dVZxQW9GRUVlcUFNOTA0OEJwdWl0Ty96Vm16NU05TFF4TCtBdkJmL3picmtsR3ZPY2N1YjhoZUM5NDZUNSsvVTZPbHZ1WkR6aHE2N2U2S0FBNFBDWGppN2czNEJFUVFDWVpUWWc2aXQyVWo4cG4xbWphYXF6UzNHSko2TmNWZVlCc3BBWGZaWm05VXlYd3o0YzN3Sit6WGNPL0pzR1BnQ2NQbnNld01CSUtoNzg0b2NGNElsYmdQOTlSNDR0NE4rZzNQaUx0d0pBOFd3QXlKZDdwZ1FEcWIvWVY3cTl5dG1tcElMODQ4bzhtcTlNTmkvWWxtYjFBUEJYYjhobjlhbTJCZnhwOE4veXRadUgvcW16NTdWL1BjdlRTNWN1QlNPTWduOS9NTmRlOC9nTkRDK1VvOGVPTCtEZnNJaEJBSmdjQ0t5T3RFOGpmMktRUXY5SjNSMlVmRXBMUEVCSm1VZnJvNnkrTDlrblFjOXN1bldIVDk3MVArUXhMT0JYKzZ3Ri96YUFEd0FuejV3TCtBbkUvMjdtMHFWTDhYZEMrWkMyQWY5angwOHM0TitpekJZSUJCdXJwL2V0QmdYRlYwcTIrZkZYbFhnQUZleStXUUM4M1YvbVN3d1F1WXU5Q2RpSGZoYndhMzNXZ1ArV20yK094amEzbkR4elR0elBnd0FBbU1mNmpELzFBUVBEQ1hyU0U2K1pPcjRpT1g3aXBOOWV3TDk1Y2YrQXozM2J0d0NvRHdSQWZYYWZ5OVNUZ2FHaW45bWxFdndhMkcxYlhUOWpJQStVZ3o3MnZZQmY2N01FL05zQVBnQThjT29NQUxtZXo4VVlBL09WeHg1VFA5bjRwQXh5M1JZQ2dJUC9BdjdOaS9RUENKUUhBaUIxSVhka3VXZEVDYWM2V0ZSS0N1S3lmazZocnAraUZUMzllbzFQL2ZTZkVIOFZ0YW1ndndYOFdwODU4RzhMK2ljYzlJVTJMUkJFNEUvQm5wKzBKMS83aFBwUmpwRDdUNTVld0w5aDBjRFB4UVVDb0N3WUFDTUNRb0ZQVlgyRDlmNmF1djVnbFBOWkFYakZYN2Z1QXNqTC9TemdGOTl2QVB4ZmUvTk4wVGcySWZlZlBCM3RDMHJrdkkwRUFmUG9vNDlHWjh2dEVFOGtPMUhYWDNkdHhWQ255UU9ueml6ZzM1Q1VnbCtTWkRBQWt2RE9yKzRaOGJsdTRxc3c0dlNVekE2cVYvU3NPM3o2elI4Ui9KU1ZhbXRsQWIvZXB3VCtiVUdmbHNLZDhPdytHUVFjK0d0Z3ovVzZyc01OMTE5WE91Wko0aTVnTE9DZlY2YUFYNUpzTVBDTmFUOWpQOVk1U3o2MTVSMXZsek5MdEx0eXpXZDZ5R2RkTGVEZktmaHZ2dW01VWQrYmtyKzkvd0cvTFYyNEJmSkJ3RHp5NktOZDdtVDQvUXoyWFA4cE4xeGZOdklaeEsxVFhjQS9qOHdOL3BRVUJ3V3ZOTDZ2cVYrTDBhZWx3STdEZllxN0JmeTdBZjgyZ1gvMDJIRy9IWlJ0M04yNEZVSEEvTjBqai9oREs4bnN1UjV0YjdzT1QzdnFEV1ZITVpPNFc1SVg4RStUYllJL0p6UXdBSVhCSVNWanpDZWVqbzdkRVAvWnQveVpyRmNJNUFYOCt3ZittNTd6bktpL1RZbTd5ZFVKWCs3TzkrZUNnSG40NFlmakduOG1zK2V3NTdwZDErRVpUM3RxeWZITUltZk9YMWpBUDFIMkNmdzFrTHJwN1M4cTFpMEpJQnpZbW1nZ3I1VUYvQWNQL05zRS9xSDdqb1NRNzdkelFVQ3lvZUxCUHdmc2gzM1c1cGxQZjFyQm9jMG4vQWwwaTVUTFFRVi9rYitwOWhzOE53djREdzc0bjN2anN5UC9tNVI3RDk4SEFQNUp4blo3bmlCZ0hucm9vUmo4UWlsSGdqM2REbXBmWFJmb2ZNMHpuNUU5eURsbENRRDFzb0EvWWIrQXY2cnRjZ1QvdHFIL3hYc1BBVUQ4RkFNaENOQjJLUWlJcGFDTEZ5OWE4Q2ZxOW54ZkNleDVJTG54MlYrVFBkZzVaWUYvblN6Z1Q5Z3Y0SzlxdTV6QS81eG5ieGY0bi8vQ0YwVlF1MjBwQ0dobG5VWUlESDRXOE9VTEY2S012N1NVdzlzNDdPbEo3THJ0MThlQUpRQ1V5Z0wraFAwQy9xcTJ5d0g4MndZK0FQek41NzhRL21ZR3pDeEJRSndGWEhqd1FYOG1wbWIzL25vQW5BMENXN2Z1ZFp0TG9Kd3NBU0F0Qy9nVDlndjRxOW9PTXZpM1haa0FnTTkrN3EvN1I5S0hJQzhKQW1OTFFlYjgrZlArVE15ZDNRL3RRNkR3ZWx0OFRDbVZKUURJc29BL1liK0F2NnJ0SUlKL0Y4RC96R2MvNDU4eFloQkMyZ1dCSVpOSHFLZGN1QjFnbjc0Z2JNNmRPeWVBZjF4MnI4SGU2N0Zmb2RuVzcwMUtzZ1NBVUJid0ord1g4RmUxSFNUd1AvdFp6eFRIdFduNTVLYytqY2JEdmQ5cEdnOTJ1My9lV1VDdzc5eTVjMTBPK0tYWnZTdmxTTm05MDIzYnNDK24rL3h2K0FieEJHMWFsZ0JnWlFGL3duNEJmMVhiUVFEL3JvRC9pYnZ2RnNIY05DYTh5NXdFZ1pKWlFPb21MbkVXY1BiczJhNFcrRkx0dmlTN2wvcHA2VzkvQXZpbjMvU053dW5hdkZ6cEFXQUJmOEorQVg5VjI3NkRmMWZRLy9qLy9ZVGZ0cUNYQTREZGRqdkxad0UxWlNCejVzenBqb00rQmZ6U2NvN2JUWUZQL1hQZ2QxMXJnMEhiNFp2LzJRdVVVN2Q1dVZJRHdBTCtoUDBDL3FxMmZRVC90dThsb3ZJWGYvbS9ZVHpvWFUzZnlnRDZ1RDRmekFLRWF3RlRBb0E1ZmZwVUJINWdHdkNsY283VDkyMjh2ZC9ma24wdnZQVlcvV3h1V0s2MEFMQ0FQMkcvZ0wrcWJaL0EvNnhuUEYzc2R4dnlzVC8vY3dBV3RvMERkd0Q2Sm5pTVZHb1drQ29EbFY0SENQeWVPbld5R3dQODB2cDlLcnQzUGdFTGZEK09ickQ5MWhlOU1EcWgyNVFySlFBczRFL1lMK0N2YXRzSDhPOFMrQUR3WngvOVdBaHhFd1lBQU1Fc1lKTUJRSm9CbUpNbkgraUE4VFg4MllEdis2VFhHQVovTC82MmJ4TlA4TGJrY2c4QUMvZ1Q5Z3Y0cTlwMkNmNWRBLzhqZi9xbkFJYU0zbTM3MXowSkFPYUJFL2QzcVZVNmN3S2YvbkJCRHZpMC9OUzFMZHErL2FYZi91M1MrZDZhWEs0QllBRi93bjRCZjFYYkxzQy83UWRDY3ZuUWh6L2NBejBzcmRBQVFQZk5IZ0FxcndHWUV5ZU8yNHlmQWQvdUkyV2R6RVZicllaZm0rRnJ3S2U2WGRmaHU3N3pPMU9mdzFia2Nnb0NDL2dUOWd2NHE5cTJDZjVkQXg4QS92c2YvekVBQ3ZydEJBRHRJckMyQ2lpQS8vMzNIKytTZGZ6RXNreHBsVTdxb3UwWTRIUG91ekVDd0cyM2ZaZjJXV3hWTG9jQXNJQS9ZYitBdjZwdDArRGY1bTk5cE9RREgvZ2dBMnU0UWljWEFPWXFBZVdXZ1VyM0Faamp4NC8xTmY1OFdTZTFEci96KzFzWitMMlNWc1BQWmZodWpHNi84OXQxSFY1MisrM3BUMmhMY3BBRHdBTCtoUDBDL3FxMlRZRi9YNEQvdnZlL1B3QnZZNXJKQVVDN0NLeXRBbkx3RC9zckwvK1k0OGVQZGJWWmZxcXNvOVh4cVIyb1BnRSs5VjhDZkI0Z1h2NDkzNVAvMUxZa0J5MElMT0JQMkMvZ3IycWJHL3hQLzhkUFNRMXhhL0xmL3VpUFJLZ0hvR1lCZ091N0FDQ1ZiSGo1QjBBUUFPWW8vM2k5WTBlUCtMTThlNWFmS09zTVVBL0xPbEdBNklOU3krMm9mdHNOZ1FNZFh2RjkzNWYrQkxjb0J5VUFMT0JQMkMvZ3IycWJBL3o3QW5zQStJUDN2dGZDRlE3Y01kQnBBSWhMSzZFT2dDZ0FsTlQvWjgzK2p4MDkwbWxyOGt0cStWT3ovRlFkM3dHZnRuSGcyejQ3ZElnRHh3Lyt3QThVZmJEYmtuME9BZ3Y0RS9ZTCtLdmFwb0QvYVUrOW9XSjBtNVhmZjg5N0FBSGFQQURJbVgxWS9xRnRVdlkvdE1mWnYzcy9KdnZYbG42YW8wZS8xSldVZG5KTE5ITzEvS2xaZmhRZytpeWZBdCsxb2V1d1hnKzJQL3hEZHhSKzFOdVJmUXdBQy9nVDlndjRxOXBxd2I5UHNBZUEzL25kMy9NQVhhMzZUSnhCMjhFZlFCUUFTc28vSmRsLzZ1SnZPT3VJcy85czZlZm9rY1AyNG03L3VhU2dYMUxhQ1FDTUVQb2xGMjlMc254YTFnbDhFT0JMOWo5NjU0OVVmUUcySWZzU0JCYndKK3dYOEZlMWxZRC9xVS81NmxIajJiVDgxOS8rSFRHRFg2MmFBUDYrYlVUMm43cjRXMUw3ejYzOEtWbjNiNDRlT2R5NUxCOUFFdnE1ZGZrMXBaMG9RQ2haUGgxTFRaYlAyMm5mUC9HcUg2djlQbXhGZGhrRUZ2QW43QmZ3VjdWcDROOVgyQVBBZjNuM2I0VVp1UENYeS82MXJMNjI5Qk8yVFlPL3M3V3ZBL3pOa1M4ZDd1YUdmbXFaWm0xcGgvWXAxdko3NEFmOWtUOXhodEdQNXpXdi9za1JYNUh0eUxhRHdBTCtoUDBDL3FvMjJ0ZFRicmgrVkwvYmtIZjk1OTlRWUdzaStOTjJudjA3K0FNUXMvK2EwczlRUHBKTFB4VCt0ais5N3ArRS8zMkhEL1dsbnZtZ0g5YmlkZWlubG1uV2xuYWs4azdVYi8rRnRQdGI3L3QxLytLMVk3ODdXNUZ0QklFRi9BbjdCZnhWYlRkY2Y5Mm92cllsdi9ZZi94UEwwQnVoemo3QTMrb0kyWDlGNllmcVNLV2ZtcnIvSFBBMzl4MCsxTzA3OUR2RVVKZEtPOEV4Q05CdjZUamJRYWR0VzdSZGl6ZSsvdlhqdjAxYmtrMEZnUVg4Q2ZzRi9ObTJmWWM5QUx6am5lOUVZeG8wWkNYTkFHa1ovbDVISy8xSXdXRlA0Ui9VL0E4ZnVxZlArTWRkeUExS05wbWEvaWFoVDhHdlFkOEIzeDVIQ0gzdm8rMXcxNXZlT1BhN3RYV1pLeEFzNEUvWUwrQVg1YXVmL0tSUlByY3R2L3J2M3dFUStEbjRBdzdDT3Z3RG5TM0RmeE0xZjkvLzRVUDNkQlNjcVhYNlkxYnZjREFEK1pwK1RYbG5idWl2TzZEdC8wbmU4dE4zVGZpNmJWK21CSUVGL0FuN0Jmd0FEZzdvbmZ5N1gvNFZ3QmcwVFlPVlFRUi9BRDc3M3piOGEycit0YXQ5VXIvMjVYMVQ4SSs5T1d2c2trME4zcmtMdVZKcFoyN29vKysvN2Z2N056LzdscW5mdzYxTFRTQll3Sit3djRMQmYvMTExNDZ5MjZXODdlMi9CQUJvVnYyakNtYUF2MnNINUZVNzBnVmZRTC9aaTQ2QmdsL3lQMldkdjFydlAzVHZGN3NwSlo3YXVqNkZ2cmRSU2p3YytnREViRDhGZlQ5R29hYlBvUS8wNEdmUTd6cGJjT3JhRm0vOStaK2I4SlhjcmFRQ3dRTCtoUDBWQlA2RENIb0FlT3N2dk0zQ0VXVDkvSW84cGlBQi81cWFQODM2Z1JEUzJtcWYwc2M4ekZIdkx5NzVTT0FmZTFmdVhDV2Vtcm8raFg1Z3c3SjlGZnIyQUlkc1B3RjkraUM1dG0zeGIzL2hyUk8rcXJzWEdnZ1c4Q2ZzTDJQd0gxVFFPL25abjM4cnk5aGorRHZ3QThqQ240SWZrSUNiS2ZrQVdmalBVZklwZmJhUEJINEFNUGZlODRXdStIRU1NMmI3WXNhL29SSVBCN1phNG1IUTkvMEF3YkcxN1dEZnRpMSsrZTF2bS9UbDNSYzVjLzdDcm9jQVlBRy9xSmRyTHdEL1FZZThremYvek05NVVKcytrNDh5WThqdzUrQUhCbGlXbG54OFA0VWxId2QrSVAxNEJ3Mzh3eGhuelBvcCtPZXE3WE00QTV2UDl1VUFORitKaDBQZjlkVzJyWDNNQlRyODZpKzlmZElYZWg5bEY4RmdBYitnbDJ0blkzenl0VThZT2FMOWxidis5YzhBNk92bWpSSGhMNEVmS0t2M2MvQUQrVFgrYzJiOUh2S1ZXVDhORHFWWi8xVzVreTJ0NUNrUkR2MGE0ZEN2c2hNdTZPWkV5L1p6ZlZIb3Qrc1diN2pyemQ3Zk85L3hLOFhqM21lNTV1ckhSZnYyWldhd2lKWHJubmpOcm9ld01YbmRHKzVDMDhPNlhiZStiaTlKMTNWb0FUUnRDelNOWjRjRFkwcmFyZ1ZhV3dxeVBoQmNjRTMxU1dXOWJnZjRWOWkyYU5HZ0tScnJZTnZDbUFaZDF4WGJBWGJsWmhiODJtRDVMOXlYMkVXMTlVS1JidFFxRlo3dDE0eVhaL3RGL2ZYNlAvV3YzdVJuQisvNnRmOVFOZVo5RnlrWUFFdEEyTFJjem9Ebjh0cC8rUWEvMWw2VHRtMjlEdDB1a1hVSHJOb09uVS9RYTRBTEd4d3E3THg5MjFuREViWnQxL21nVnQxdjF3SWs2NytxcmN6R1EyZjFJSjdUZHN3RlNWcm1xUlZhMjYrUjEvelU2N0h1YlgvelhiOWUzZTlCRVMwZ0FFdFFLSlVuWFVGdzUvS1RyMzBkVmszakw4Uks0ckoreTc5SytLR0Q2ZXFEQk5DWDVFWUFGK2lEaGJISWFjYmFvejVRQUVPZzRiYlZHYi9zZkh4TmRtcjlkSXI5ZWtMWDdjaGpmdFdyWDR1dTdmRHUzM3pYK000UG9LU0NncFBMUFRoY2U4M2pkejJFdlpNZis0blh3UFQxK3BRNFdJK0JINEJzbVNocDIzV2pnQTJndWd3enQ3MG1zNEQvU3BVcHM2VTdYL1ZxQVBhRC9lMTMvOFpjUXpyUVVoSWN1SncrZTM0REk4bkxBdkh4Y3NlZFA0N1ZCSWpYeXFiZ2VaQmxBZjhleUIxMy9yaS9Edkg3di90YnV4N09nWkluZk5YVnV4N0NJZ1h5Z3ovMG93QlFYV0taUTNZSi9YME5PSlBCYjR3Qm1tWjB1Y2NZTTZsY004VitaY2FYZSt3WHVCMlY5WnZHb0VFamxvdGVlY2VkYU5kcmRGMkg5NzduOThZTmJwRkY5a0MrLzVWMzlPdm9Wd0F3cVZRenhSYkE2RElQTU53Yk1GYW1qSHRxNE5CV0psM1ZOR1ljdkl3RjN4am8wb09aWXQrZzdvN1R4alJvMFQrQzJocFg5ZGswQTZ5cnhtME1WazJEZFYrbkxMMCs4UDJ2dkFQcnRnVzZGdXUydy92KzhBL0srMXhra1MzTDdTOS9oVjNMM2hqQTZLQjBaWjZtYVlxV1RGSng2L0diZ3VzQ1hLSjEvSlhDYjZxcTdadmVIekNtMzdIWEdmaWR2a0JseHU4T3VCYldwakYyalN3c2VHdnRWNnZHLzhwV1RmK21NV2phZnVsVkJYRDlCOXQxUU5PZ0lZOXJ5TmsxVFFPMGJkQm5icnc4S0RTcmxjLzZxZHorOGxkNGZ4OTQzeDhXSGNzaWkyeFN2dnRsM3h2Y1FPV2c3K0crV21WQk9kZ09kNzJXUWwyNmdhdFloQWUybFp1YTBZRmdVZ0JoVCtxc3N1MWZremR3bWNhZ2F6dWZKWGZvaW0rQm5KTFIyNmc4dk8rc2svSStIYWpSNU84MWFBeFdyWVY3MjdZd25jMG8yblhhenM4NGZMbW53STVuL2YwTkcxcGdjUEMzTnNDcWFiRnVoMkR5M1MvN1huK1BRZHUyK05BSDM1OCsxa1VXbVVGZWV0dnRhSnJ3YnRZQStvcFF1Tk5zUHdWNEIzSVBkWkx0cDBSOVpFTkd0RWMybElqMHlJWWFPK2twblhuYitKRU5wWElWWUNOQTIzYldycUtFNDhvOXNFNTY0T1poTDU2VXh0NHRsOHFzSlRzM0FxMVBudldqeFZEdVVXd0NpTk9zSC9yTXdjQjR1NUtzbjhPZlovMVVYNE8vR3hlYUJpKzk3ZmJocHlYYkZ2L3pReDlVeitNaWk5VElTMTU2V3cvcVJvVytGeUhiNTlEbkltWDdGUHFTcEI3WG9Bb3Q4MVJrKzZtZlpFemFPUnZoQ1ozRnRwVmxIdWx4RFpKY1ZYSnhsR2I5TG5LV0JZWkJOeWozSkRMeHhoaGJlbWRaUHkvM1JEYkJqbnpXNzhmV0lNcjZPMkZxWXhUNFN4ZDUxWklQaHJLT3Z5bURYT2hObFh4SzRPK09vWUg5WjNXUHNGaTNMVDc2a1ErcjUzeVJSYWk4K0NYZjRXRUhvQWo2V29rbktPUDBRck45RGZwVWVMYlBvYzhsKzRBMlFWSVBhTk9FdzdrMDI0K2hYbmJoT3ZXY25uUi84V09abzFJUHpmclI1Sit6bzJYOVFDS2pKaGRad3dhNVAyTk1FdjVTUHdPbzdWUWl5dnBwUU1pVmZMb2hGTGdnSThNL3RNdkIzKzByZ1QrUXp2eFhHTzR1ZExlVDA3WVh2K1E3Yk1EcGc4Ny8rdWlmaUovTklsZVdmTXVML3JtSGgybDZjQWZsbU9GbkN0WHlUZytoa2t5L0ZQcGFpWWRDM3duTjlqWG9CMUx3Y0RZcVVyYWZxOU5yMlg3U0p2Rnd0cFM0TWs5cHRnLzBHVDlnRTBacGRZOVU2ODlsL1ZFR2pqRHI1M2JCQUgzSkJ6N3JUK29QWmtHZnpzYU9SWUUvSy9uWUthQU9mN2RzZFU3NHIxYXIvdUZTY3RuSEhXL1hkV0xOMzl1ejdML3QrK2phRm0xdnZ3S3dCb0MyeFF1LzlTWCsrVW50Mm42dS8rY3ZQaVorbm90Y1B2S0NiMzRSbWxYRG5nRWZBaC9RczN6Zlp1UUx1YzZuVnRNUDdlWHlEdFZKUWIvb09meTlsRHlSMDBucGo3QU0rdm5ITVhQaDJUNkhmcVN2WlBzTytwcG92N3ViL0xIMXVYNlFoZm9IMG85bzlyb0Z6K2FuZnFYSE5OT3grREZtSHRNTUlQdWpMUFlZOU9mekQrZHdzRzBGL1g1UXZ2VFR0djM0eUpMUm9LMkh2N09sU3owOS9QMzVHeTc4MHRvL0xmOTBSSjhHQUsvYnR2akV4LzlTLzFZdHN0ZnlUUys0RlVBUHlQNGFsQVo4dDQ4REg4Q29MRDlvWTFtK2E1T2dIMlQ5bWZKT2FhWS81eTl2QVhGR3JrRS85QnYvVGZuWlJhcGI4aWhtZXg0S3dBOWs0RThlMDF3Qy8wZzM4M3grcHpNWC9PM1lDNTdSYnhVbi9UZ0xnT2lSelJUTVVRQWdnSGUyTGdCMFVoc0pBR3Qzb1prRmdPRWM1Z09BMDNPZkJ3QWZCRnJhM3RyMlQ5NzljU3l5UC9MMXovOG5GaUxOQUdvSGV3QWUrQjVjQlJtKzA0dUFieHZFNVpwU2xtL1Y5ZElPb0svZXNUNWo2Tk1NUGZlakt3Q0tNdjA1b1E5Z0k4L2Z0enBsUDdUdWRLVnNId0RNZlljUGRlNEM2Rmo0MDFMTW5EL0Y2SFRtZ0wvMWxmNkJGdXBIZ2orQTVJKzBBQkN6ZjI3THMzL1hMOC8rL2ZGUENBRCttRWdBOE9lQ0JRaHRGbUMvQjNJUW9HTUJnTTk4Nm00c3NobTU1ZXVlSHdCVGdqMkFaSGJ2ZEVNd2h5VWRaNU1EZnF5WHFlVmJBekhMOTc2RUxKLzN3N044YnV1Zzc0RHZ6NEVBL1pxZldhUitwa0ovK0Z6eTBCL2E4OUNudWlyMFRRTno1RXVIdTdhMU1BVGtrby9kUHgvOFhUL0E5TElQa1A4QmRucGNZMzZXRWRCTFArNGNsZjVLRjUwNWxKWi8zSGtxQ1FCQVdBS2lZK0N6SDIwVzRIendJQUJBTEFmNThRbUJnQWFMdi83Y3A3R0lMamZkL0R3QURPSUVwRUZXNStBdlpQYk9Ub0s5dDJuMGk3Yk9QbFhTOFg0bUFKLzY0R3YwUjJYNWZUK3BKWnU1MVRzYTlEbjRwNVozM09mSSt4d0xmZis5S0lBK0FKaWpSdzVieG0wUi9yUVBRSVkvMWRIZzczUm85aThGanJGMWYrK2ZaZi8ybk1nLzBXalBTM241eCs4ZkdRRDhNVkxZSm1ZQnJyK1NJQUFNTXdHMzdYMGtaZ051bkY2UHptcUFTSmUzZi83L2ZRNlhtOXo0bkpzRE9BSUk0QTBnYWk4QnZkTWJZSkxQN0syTmlZT0JrdDNidmxhaEwxTlEwckdLQWJ4ZDM2VmxIZGRYN1U4cWd1alZsSGFDL29TL2twOVdwSDFMMEsrcDZWdWRlYUR2ZkRXTnNlQUhMTTlTOEFmbXEvbFQveEw4ZVR1SHZ4M0wrTktQYjYvSi91M0Jpclgvb0QrQUJaYnhBY0NmU3dueXlrVmc1OGZOQW1nN09wZk41NE1BM0hHd2NwRzFsd01CRU04SW5KNzdqQU5kSlNCUW0yZy9TUzZrRytuY3ZudnYrWHpVTmxhZSthd2JQWVNvOEgzMEg0ekRQZGlYQUR5MW9hVWJJTTdvblc0cXEzZmJ2Rzd2ZFl5NzJEcXMwSEgrdGV6ZXR6ZjZTcDFBSjVIaDIzM2x3Qi9HVmxiTEQvcGtXVDQ5SDFLVzcvcVNzdnlnTGJGa1U0Tys1SC9zaGR5aFBaL3AyLzBZd0EvTUFQOWVJUVFzeStJVGRmK09saWJHbEg3Nmc2ak4vdTB4aEFFZ2hISFp5aDk2enNZRUFLb24yYm5aUm1vV1FOdGRIMklRc0laUk9XZzR4L2xBNFBhdE9lQ0ZZR0MvR3VtQUVOZ29zT2R0MUE4WEtURFVpbmlEVUNZSVNBRUF5QU1leUVNZWdBcDZ0NDluOWM1SFZNYXhCbG5ZdXo2MTdONlBTUUczVnNPbmZZd0J2clBud0hmbnVxYVc3L2NMZndCR2wzYmNXRGoweFJtR0FIMnFrMXE5WTc4Ny9mZE1nSDdqKzdZNlZ4bXo2cC9EMDhJWWE5UzI5bzdYNGFDTkR3Q20vN0xRRzd3YTB3Q21Ed0JzbmIrN3djdVkvZzdoSnJ6Skt4RDMvVy9oSCtqbURqcUFPMW5yYjR4Qlo3b0F1TzVHTDJybmJOMTZmOS9XdjdxYnZjd0tNQ1FBdURYLy92ajZ2bFp0Wi92cEE4QnFaVHpBdmUvK2k5dTFyZi9TVU1EYUw3dDk3ODhaT3BpMVFVUEE3YzlkLzM1bGpBMENxNVdGZk5QQnRMMTkyd1g2NjM1N3RWcjVJTENDS3djMVdLMkdhd0pZTmNOc29OZHZRQUxCYWhVRkFxeFc5dDRCaE1FQUlET0RKaDBRM0hHNzQvUDdnbTBHZitHTzdGTFE4MEJDUVowU01ldG50L3NIMDNnaDJ3ZEN1SE1mdFpDbjcxT2d0NzdpbXIwZFJ6NnpkOGVtd1o3YVN0bTlPMjc2VkUwSy9BQytNd1BmK2lndjY5Q3hjZUQ3TmlITHAzWlNscy9iS2ZERHRyaTBNNHcxaGo3OTltclFENzdpcHJGMzdob1lkS1pKd3Q4NlJRUi8wd2NGN1NhdjNySi9iYjJqcG9laFZJSnhOM3F0ek1wbi8xUWM3STNQL2hHY25LN3JQQnpkbmI0MENCZzNrNkg3amVuOXhRSEE2YmdickVvQ0FFQldBUGtwdTRVWkI3a0xBUDQ5Qmg4dUNOQmdRWU5IU1JCdzUzVk50dWwrczE0RFdQVUJJZzRFVG84R0FpQzhwdUhHVHZmbkFnS3VpbWNKMWg4cDg3QTd0ZDE1OGUwSzdLZjhIQ2dWN1paOWNTWkFieGdpejMrWHdFNTlyeGpJcVY0cDVBTmRFMmIwcm84VTZDTWRvWXpqeHlyQTNvMUpXNGRQZGFWQVljai9TS0JUVU5KeDU3Z0UrQ0QrUzhvNmZEOEZmdFJHZ08vYlJtVDVWTCsybmsrL0R4SDB6UkNBemQ4ZU85cjVmOHcrODNjeXBmVGo5S1hTRC9WVmV1SFgrZVVsSEtwWGV2RTM2TDhMYS90OFhHN01xUktRNzRkZUEraFBZS29NWkk4cHJ1blQ5OXB5VUdycnhoT1V1OWcxQVgrTzJIV0JTTWY1NDZ1RXJJUCt2VndlQWprdU56NDNOdHFtbFhHbzdacnE4TklPeTlodFgyV2cxNTZnV3ZwREhkSUR2ZmlNZ1FjTCtwd2FEbXNnZlQyQTFxS3BYUzZiRDNRWjZGMWJLcXZuT2xOaFQvdVJzbnZYSG1UM2ZiOCt1N2RHRVdDM0NYeDNqampzL2ZGVjFQTEQvWFZaUHBBdTdRejczY2tkenFreEJ1YjQ4V01kaFNzUUJnQU9mN3N2REFBTy9yNU51UEJMN1lDeTJyK3pzZTBPMHZyRlg2ZFhFZ0FrSDFvQXNNY3lYQU1ZM3V2M0FBQklyZ1R5K3NnSEFTQzhIZ0JBdkNqc2JQaUZZU0N1Ky90enhjQXQ2bFVHQXljbFFTRWNUd2psVkoxZjBuZXlWdmFQbFZVakJ3VnBOc0NEUUNOQW4rNFBIaFdRQ2dvVThGWTVHQnVGdkhzZjIwcmxJbG1Qd28vWDNwMjlnelNBV1dEdi9HalovVEFHL2E3Ym9BOHB3eDRCL0xBUDJVZHBXY2Q5em5ObCtZRWQ5TkpPQVAybUI3OXJwd0dnTlBzSEVBVUFDbitnYnRtbjFaZWhUc2ZJcyswNUF3QWZnOWRwaDFWQVFSOEZzd0RYN21ZQkFOVDdBV2ovems0S0F1NXo0aGVHblQzZnh3TkIxTjRPL2wzYm1zTmZDQWIwUGZYTmc0TGtoOXRHOEdmQWwwbzdwV1dkM0RVQXFYd2pTZTdpTGhBQ25kdElZS2Y3T2R6dFBoM3cxRThBYjhUWlBOZWxHWDJrTDREZTloMnZ5SEg2MnF4QWduM2dpOWhwU3pLcFgyMVpKclVKK2xlQXZRM2dCK2VnQVBoV3Y3NldQK3gzanNuTUNRakdhVTZjT043ellQam56MlgvZHJ1dS9BTnNMZ0RRY2VRQ0FOWDFFR1NyZ0lMakdqa0xvTWZyeDEwUUJIeGYvamoxYkg2V1FPQStXT2l6Z3VDOUVEaG9lN0N2RHdxU1hlL2NiMHJCd1lrSWUyRUdNTGlkTitNSFpOZ0RjWVlQeEVHQTZnUS9WaUtBbmVvN3VBZjdFb0FQMmdYSWUzMENMNjZqZ1I3SVovWGNSMGxtNzN4SnNBY3dPYnVuL3FoT2JtbW1PNGRqZ1IrTUl4cGZlVm5INld0bEhkZXUxZko5UHczcis4U0o0eDFnLy9lRGNnN1AvdTFPT04yUzhvL2RYMTcvRC9RVEFjRGJrM0ZLQVNCNG43b0hvRDhvYlJiZzNtdDNBZzlqekFjQlAzWWhDTGl4OFdzQzdueW1aZ1AwblBCQUFNU2xJZWZISDFzaUdIRGY3cnpUOTFTSDIyaDZkbHh4Y0hBU2xXeVVPajROR3Bxa0FnVVZDZVNTaUw4MkpkVC9KYUE3U1lHZDdoT0RRZ25nN1E3WmpvQTAyc2N5ZW1lbmdkNzU0UmRvdlc2bWpPUDB4c0RMNUx6R0FBQVBlVWxFUVZTZTJqYnN2WmpkOStjazBzbGN0UFhITWpMRHQzclRnVS9QZVdsWlp4aG40M1hOQXlmdXQrQ0hnMEFtQUNUS1AzYmY3Z0tBZTUrN0E5ajdLcHdGY0I5OExMNDlFd1FBdlJ3RVFMd3dETVN6QVhkZXRVQVFIQS9acDgwS0FoMGhpTkQzV2tBSWROdDRIOWRkYTRHZzVFSXVDUlpjU2dGZks2bUFRQUd1NmFkbkFUcllnUmp1Vks4RzhPTCtWWmdkOHRLTmU1MENldGN2djBEcjlWa1paL0Fmdzk3WlJKQlZNbndnbmQySGZjaytwRlU2WEdjTzRBODZaWFY4dSswR2t5anJFT0M3ejhXY1BQbUFCWDg3M0hFSmhBR0FRaFhJQjRBZ1M4NEVBS0NzQk9UODVlNEFkdVBWWmdHaEwza1c0SStoRzVhRFRnMENibHhTbHIySlFFRGJ0Rm1CZlkxbkJxS2VZay8zdWFBQUZHVDlmRjErQnZqYUJkdFMwSmV1OFMrdDh3TnlNSkF1QktlQ0FBVkpMdHVuLzcybGdBL2FsRXllNmxDLzNHY1I2UHR4bG1iMTNvZnZJNS9aK3pZQjFHNWJxOTI3Y2RaazkyNWNVai9oMlBNMWZPcDdPRjQ1dzQvN213SDQ3cHlmT25YU2d0OW56dk1GQUtkTEE0QzF5d2NBYWd1UU1aQlpnRlIvRDQ4bFB3dncyek1IZ1VoUFdCN0s5Y1VWUWxZSlFCd0kzRmc1ckxzdXZFYmcrcUU2MFlxYnlvQVE2QWFCT2hFWXlMaUJHT1ljNHZLRlhCMzBjOXlwbTVKVVVKQit1U21WNVFONXFITWZITVMwWGN2Z3JjNDB5RHRkc1E4V2FGS2dsL3JpeXk5ZDN5cGNDeko3cVMwRis3Q2ZmSGJ2M3VjZXB1WmZEYUx6TUtha1k3ZmRnTkxBcDdvVStGNzM5T2xUWFNmODQyNDdBRkMvcVVkQWdOaHdxSmJNQXFoL1RiOGtDQUFvdWlaQTIveTJNaHNZOXNtQndMK1NRT0QwM0luWFFFMW5CdTZjdWY1b2V5NGcyRzA1S0ZEOXlFYjRqa2w2WWdhdlpQV2x5elpMWndWT3BFeGVFbTJaSndUN1pOWlBRWjZCUGxBSGQ3cXR6UTYwVEY3c2kvaWlwUnRuTXhmb3ZYNEMrTUE0MkV1NnFkbzkzUzdPN3ZzVFNvT0p0UXR0dGcxOGIzUG16R21mOGRjR0FOdk9yZ0hZblY1WFd3VUVBTG43QUlDNERCU01UWmdGV0J1aDlGSVpCT2hZZUJDSWZHWm1BMjY3SkJDNDQ5SUNnUnR6TklaRU1LQ2Z6ZFNBSU5ueTlqQzRFeDBsUUhCNzJqOFhEZUtienZhZEpMTitBZmg4V1dkdUZrQnZKRXVCbmRwS013UnBkaUJsOFlGdXJtUmpIVWEyRlBMRFBsNGlhZUpBSTJUUTB2Nm9oTk9QTFlJNHE5bHJ2bkt3bDNYMTdEN29SOG51cVEzOXhJdFc2VmdubzBvNjBqRUJnRGw3OW13UGZnRW1DQU1BTVB3anB3S0ExeU1CQU1oZkI3QjIraXpBMnltekFEcitLVUVnOE1PQ2dQZUoyRjl1TnNDM3RVRGd6amNOQkc2c0dwQnJad2IrTStvbEZ4RGNtT2k0cVI3MXorRmNFaUQ0ZVBpNHVKUm04V01EUXZHYWZtVjJJTjBKYkJEcU5nMS9MMmY0S2JBRFpYQ24yNmtzSHFpSFBCMERyOUc3TWNYWnJBNTZ2ay9LNnYyMmNJRlcxTnN3N1BuNUdjNkZuTjNIWTJ4OEc4M3VyUjA3TnlVMWZQYTU4OEJpenAwNzEya0FsVjZCY0JZd0pMMERuTFFBNFBTMU1oQVF6Z0tjWDNVVzBDdEtRVUF2cmFTRGdOc3ZCWUZvdTNBMkFLUUR3ZENuZk02MVdRRWRjMjVtUUY5NVFLQTJKVUVoOERtNFVPL016V1gzcVVDUnNndmJ5Z0xCSE1LQkhiYmxMKzd5ZmFsWmdBUjFBRUVJRVROL0JlNjB2eFRnN2U1eGtIZTZzYjk4UmcvVWdUN25MMWV6NTl2RnNMY25WNFc5MVowL3UvZCsvUkRpOHk2T0ZVTndNY2JBbkQ5L3ZoUC9vUlV3YXdIQXRrMmJCZEMrcEZtQWI2OE1BdFN2dEFSU1A3NzBiQ0RhVGdRQy8wb0NnZGEzTml1SWRBcUNBYlhoQVVIeVd4SVUzUEg1ZHFXRUUzNzJvZVR1ME9YMlhFb3krZEpaZ1NaYU5zOUZteDJVbEg3NGpXRVMwUG0yVmc3aW1Uc1F3NTM2cWdHODNhOUQzcjFLa09kdFd1bEcxUk51cXBKMGVWWlB4NjdwNjljWjZtQlA3U1RZaDhjOUxyc25weWxaenBINjRtM213b01QZGh3UWRGc0xBSUdPVUFhaXR1TEZZTnZnOWZtMUFOcmZsQ0FnSFVzSTYzaDFrSGlzYkRZZzZjZSt3MENnMmFBTHJ4Rm9yeVhCd0IxVHFDY0hCR3BURkJTc2d0OFVnME4vUEg2VElUOTYybVpodHA5QytDYnUxRTJKZGhldmI1ZjJaYkorSUFGMHF6em9WWUxkTmplUjdSVEFpL1lGc05kcTlGNGY4WmhLUU8vM0pVbzRibHU2SjRBZU45Ky9NZGhiUjhYWnZSMUtlVGxIMHJIbnk4QjgrY0tGamdNRmtQL3BhOHRBdHExOEZ1RDA1d29DZm14Q0VIRCtVeVVoMm05OG5PTUNnZlB2amwwOWw0WEJnTDZXQm9SUU53d0traCtBcjlEUlN6OUV5Vyt1ZVZOMFI2NlE3U2RRcjlYOUl4OFRNMzZnSXV0WG51NFpBZHc2RFcxWklGbHhFd0ZtZkRzRmR0c3V3ejFzR3c5NDZYVk1Ocy85ekFWNmQzeXByTjYvR2hEYnVqSU85ZVgwNW9SOTdGOFBYSHc3S0J0ZXZIaXhrNStKUG0wV0FHdy9DRGc5RGxwN1BPblpRR1E3SVJEdy9sSzIyZ1ZqNmRVQmNtcEFFSDBqbmltNGN4S2ZTL256amxmbVZHYjNTcDJlQjQ3QVpNdlp2cFBjeGQ4STNrQUFjQ2M4c0Vnd2wvcVVvQTd3MGtZanRPdVpZQ25nK1Q0SjhPS3JVcC9uci9GNDlOS05iak11cTNmOURlZXJEdlpUTW50eUNrZlg3dm01Y2VjaWFyOTQ4YUwvdHdwKzhTZ3hDNkRiMnd3Q1FObUZZV3RmUGh1UXh4N0RWUXNFa2owL0p1bENxNTZaeHpNRFVhOHlJRWo3OHA5dkhCaGl1L0J6cE1Kcjkvd2NTZUJPUFZ1L05JdWZJOXNIeWpQK2xKNzBEUC9zc2s0ZUVBUkljTDFVVnNqM2lUQW96ZUR0enJSTkpwUG5yMUkySHgvM2RORDcxMFJXUC9qSXI4YWhmWlRBUHV4ak91eWo0M0pqbFQ1WCt2MTQ2S0dIQnZBTC8rVFNMSUR1bDJEcDlzOGFCR3hEWUNNRkFmZGFPeHZ3OWtvZ2lPeFpJT0Q5UytlSEg1dTI2bWJ1Z0FEb1FTRzNqL2FYMG9tUFV3NFNWamNHdTNaSHJyYWVYK3QzbTVLR3Zkd20zZUZyZUdZZlFUKy96Sk8rbC83cDZYWnVYeTZERC9ZSk5YbkpSc3ZrQTUzRWRZb3dPQXdnMHpMdXlNY0kwQVA1RWc0NVRjN2hySmw5N3Rqb3R2cTVTNzRmZnZoaEdmd2VOdktLalZUNXdHN1BGd1FpbTRyWmdIdWRJeEJveHhpQVhnZ0dxUXhjT2thdFRDUWRGN1hYQW9MYU41c3BhRGFsMjduc1BsdnFjZnVWOGswdUNPeGFOTmdESWJ6OVBnWGMycjRvSUdRQVQ3ZEZzTnVHckEySHV6MmVmQlpQWDJzaDcxN1RLNGppV1JNdDNRdytaTkRiMTdoOEUvbENPcXVuNThqYlptcjIwakhKNTZEZzNnN3BNOHpjNkJlQW44cStCSUhJdm1JMmtPcDdkQ0RvRFZTSVM5QWVPVE9nKzFPcmJaSkJRbHJKVXhrWUFEMDRhSDVLMy9PeDVYUnIyamN0dVRLUTFpNEZpRndRU0wzUFFWM1REL2FOZ0x1MEx3WDRsSzhjNU8xckFtWW1IZ00vQmkyakg5cjVXTXV6ZW42c1l5L1Fhc2ZKdDhmQVBqaTJ2M3Zra1NIanp6d0JVYXYzVGlrSDVmeVBtZzNZeHNCT0N3VHVOYjZMTk16b2FYLzJPTXRtQmJUUDNNd2dkMDYwNDA2dHNzbVZjVktCSVduSHg4WGVyOGN1Mnh3WkJNYnFsa29POGlWNnVZeGUyNmZCbk91cTI0aUJBY1JncDlzbGNBL2FLZ0JQeHpFVzh1NDl2MzdDTDhZU2RkdnZGa0ZQank4WXY5RC9uTERuMjZMdUk0OCsyZ0ZTeHBrT0FuUzdKZ2pFMitXekFXQjdnUURRWndWK3JOR0Z6THFaZ2RSbkxpQm8rK2p4QjhlUVdaZGZ0SzJ0NkFFL3B2cHNud2NNSnp4d3hHYTd6ZmFkNUFMQ1Nsbm15UUV1K2NxK3p3Q2QyOVNVZzNqbXp2M21mSXdCdkRpV0RPUmRtd1I1SUYyamQrMzJmZUE0V29ZcmxXKzRYUzZyMThhZ3RlZHE5bG9md2JpVlJRVG1VUWQrMGpnbENGRDlxVUVnMTBjTW52R0JBSWl2RWZCeFNMTUNPdTdTWUtBZEgvY2xIYlAxV1I4VXJMMThMcVhnVUxvdHZzK3Q3RkhXNktkZ1hncjZUUVdFMG93L3B5ditjd3ByL1ZPcmVyaWYwakpRTGRnMSt4U2s1Z0E4SFI4ZkcvVW5RZDdwU05tODA0aytnc0tNM3I0U013YjY0SGlFY2RSazlaRk9UUmtuTlNiMytwWEhIdXM0MkRjUkJQaitYUVFDcXllczFtRVhpNTJ0TkN2Z1l5a0pCcTVmS3JVQlFmU1pDQXFxWHFwY3c4YkoyN1VBTWVaOWRuL20yVHY3ZEtFM2RWRVhrSUVLS0VHZ051dG43NlV5VE1rWWNzRWlkUytBQmpkUlp3VGdpWm1YVXNqYjkwRUh2YitEQjNxK0hZeWpBUFpVMTN6bHNjZUMveUFKN0FQTUpGRHNieUN3WTZmdGdwL0NXWUd6THdrR1FQelFzTkVCZ1JqWEJJVm9PL05relpyZ0VQU2JDQlMydjNTdHY2U2ZYV2Y3VGpaVjU5ZjJSMURNekFJMDNiSVpnQTUydXMzQkhyWUZsQXoyelFINHFBOWlOd1h5emcvL0NLYUFYdEwxeDdRajJBZnZPZmk1N0hvMmtMU3ZDQVN1djlKWlFlUXJFUXlBelFjRW9Dd29STzNNZjBxMzlPbWFZN1A3YUV4S1ZwK3pUOTNndFF1UmJ0Q2lrZ3dFQ1hpbjdFdExQVFUzZU5udDlMSlRucm5UZmRKNUtBVThVQVo1RWZDMm85NXZmQzRreUhNZkpSZGo0MzM3QzNwSlAyaDc3TktsRGlqN1o2MEpBcEsrcEx1cFFPRGFVbENTWmdWV3B5d1lCTFl6QndUck96d3U3WUtyazFSZ2lPelZ6THR3MmVhSXAydHVNdHZmTjVrcjY0OUxPVHpyTDRPUEJQU2tqUUIyMjM4NTNKa2JhNStCTzdXdmdYd3FrK2MrSk1qenNlUkFYeHhzWndSOVpGZWdyOW1iUzVjdWlmOWEreHdJZ0xLU2hYMmZuaFZvZlVyQmdQdXordk1HQkdtY3FhQkFmWlFHQm1tOHRRR0M5NTNTci9rdTdQT2R1aW5KbFlFMmMzTlhHY3lqOXlPZ3puVnJNbmQ1UENIYzdiNm84NzRQK1ZoS3l6VldkenprYzhjeUYraXo0eXpRRjMzeWM0OGUvRnlSN3hnVEJMaWZ1UU1CYnl1ZEZkajNkY0hBOVYwYkRFUS9tWUFBMUFVRnFoLzJRMjJVTWsvaUFtcHBrQkRibFgyU1NNZFk2Nk5XZDZyTVUrdlAvOE5xZmlLLy9HMEc1bDR2QTNYQnRmV1plUVMwNUtlMlJCUDRMc2ppcVk5YXlQT3hqc25tZWIvN0J2cEk1Ky9YOXRtSEtvU2xmVHNNQlB6OW1GbUIvTDZ1VEVUNzFnS0M2RmVaSVFSK1JPRGIxK0RlZ2tSZzROdmFMMVJwQVVJYVkzNmxUWG84WXFjNTNSRTZ1NUpjUUZEYnRkMVJabG53VDYvQTNMNVBBeDJRb1M3NTR2NVVzTnRCa1g3THNuZnFmd3pnQTk4elFKNjNsWmJZdUdUSG5ORVhmVXFmVGFaL0QzNHUreHdJdUg3cUluTXFHR1Q5Wk9yeVJmMFh6aEtzWFVGUXNJcHh1ekJqMFByaGZVVG5LQUg0ZURaWUJ2dGMwSkNrOUNMdXRvTkJhY1lQbE1FNjhsLzArR1lHa053NEtvRE8rMGlXWTN4RGZlWk8rNUQ4emduNGxCMlFoandmdzFqSVI3YUZOcUx2Z3F3KzVjKzA3TDljZlc3S2xnT0I1bWVPWU1EYjZ4OHVWaFlRVXVPUWJtSkt6UlNzcmQ1UDVGTUYvckJkRWlDay9xVCtTMy96VnROSzlaL3FkNStrSkNCb0paN1NVQ0w5M20vSjlRQ3BmKzRyQjNVZ0RYYXJPZy9jdVYvNmZncmdnZkdadlBRK2FOc0QwS2Q4Qm1XcXpvcW9tTHJRTm5jZzBIeE9tUlZ3bTVwZ0FFd1BDRlNuSmlnQTB3S0QxcC9vTnduODhIMjBGSFZpUnA3Nlhtenp4OU0zS2FrZlpnZDBrSXdKSXNVZ0J3S1lBekxRK1RoeWo1Uk9nZDNhajRjNzd5dGw2NlFtaXkveGw3SVY3UXZ0UlA4emc1NzdOaEwxOXprUWNKOHFWQXBuQnVMN2lRRkI3bU5hVU9CajB4OTdrTzdUK3NpUDM3ZFZCQXB0SEU2U0phUXRyczJueDF0VHRwbEx0S3hmQ3hKRlF5d0V1ZlhIQWFqMW00WTY3NmNFaG5SZjZVcWxYUUplc2hkOUZOcUovaXRxOUNtL0tkQnorZi90WEZVUUxDUk1YQUFBQUFCSlJVNUVya0pnZ2c9PSIgdHJhbnNmb3JtPSJtYXRyaXgoMSwwLDAsLTEsMCwxKSIgaGVpZ2h0PSIxIiB3aWR0aD0iMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSIvPgogICAgPC9nPgogICA8L2c+CiAgPC9nPgogPC9nPgo8L3N2Zz4K\",\"data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnNjkzNCIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjEwLjg3bW0iIHdpZHRoPSI0OS45NjZtbSIgdmVyc2lvbj0iMS4xIiB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmlld0JveD0iMCAwIDE3Ny4wNDM3NSA3NDcuMTYyNDkiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyI+CiA8ZGVmcyBpZD0iZGVmczY5MzYiPgogIDxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyR3JhZGllbnQ1NTcxIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxZS03IC0xNzA0LjMgLTE3MDQuMyAtMWUtNyA0NTk2LjYgMjgyOC42KSI+CiAgIDxzdG9wIGlkPSJzdG9wNTU3MyIgc3RvcC1jb2xvcj0iI2Y3ZDY0OSIgb2Zmc2V0PSIwIi8+CiAgIDxzdG9wIGlkPSJzdG9wNTU3NSIgc3RvcC1jb2xvcj0iI2YxODM1NSIgb2Zmc2V0PSIxIi8+CiAgPC9saW5lYXJHcmFkaWVudD4KICA8bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhckdyYWRpZW50NTU1MyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoNDU2LjIyIDAgMCAtNDU2LjIyIDQzNjguNCAzNDk0LjMpIj4KICAgPHN0b3AgaWQ9InN0b3A1NTU1IiBzdG9wLWNvbG9yPSIjY2FjOWM4IiBvZmZzZXQ9IjAiLz4KICAgPHN0b3AgaWQ9InN0b3A1NTU3IiBzdG9wLWNvbG9yPSIjZjZmNmY2IiBvZmZzZXQ9Ii43NTI2OSIvPgogICA8c3RvcCBpZD0ic3RvcDU1NTkiIHN0b3AtY29sb3I9IiNkNGQzZDIiIG9mZnNldD0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXJHcmFkaWVudDU1MzMiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEzNDUuNSAwIDAgLTEzNDUuNSAzOTIzLjggMzA2NS45KSI+CiAgIDxzdG9wIGlkPSJzdG9wNTUzNSIgc3RvcC1jb2xvcj0iI2IzYjNiMiIgb2Zmc2V0PSIwIi8+CiAgIDxzdG9wIGlkPSJzdG9wNTUzNyIgc3RvcC1jb2xvcj0iI2IzYjNiMiIgb2Zmc2V0PSIuMSIvPgogICA8c3RvcCBpZD0ic3RvcDU1MzkiIHN0b3AtY29sb3I9IiNmZWZmZmYiIG9mZnNldD0iLjI0NzMxIi8+CiAgIDxzdG9wIGlkPSJzdG9wNTU0MSIgc3RvcC1jb2xvcj0iI2EzYTNhMSIgb2Zmc2V0PSIxIi8+CiAgPC9saW5lYXJHcmFkaWVudD4KICA8bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhckdyYWRpZW50NTQ1NSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMWUtNyAtMTcwNC4zIC0xNzA0LjMgLTFlLTcgNDU5Ni42IDI4MjguNikiPgogICA8c3RvcCBpZD0ic3RvcDU0NTciIHN0b3AtY29sb3I9IiNmM2I3MGMiIG9mZnNldD0iMCIvPgogICA8c3RvcCBpZD0ic3RvcDU0NTkiIHN0b3AtY29sb3I9IiNlNzNlMjAiIG9mZnNldD0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXJHcmFkaWVudDU0MzciIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDQ1Ni4yMiAwIDAgLTQ1Ni4yMiA0MzY4LjQgMzQ5NC4zKSI+CiAgIDxzdG9wIGlkPSJzdG9wNTQzOSIgc3RvcC1jb2xvcj0iI2EzYTNhMSIgb2Zmc2V0PSIwIi8+CiAgIDxzdG9wIGlkPSJzdG9wNTQ0MSIgc3RvcC1jb2xvcj0iI2VjZWNlYyIgb2Zmc2V0PSIuNzUyNjkiLz4KICAgPHN0b3AgaWQ9InN0b3A1NDQzIiBzdG9wLWNvbG9yPSIjYjNiM2IyIiBvZmZzZXQ9IjEiLz4KICA8L2xpbmVhckdyYWRpZW50PgogIDxjbGlwUGF0aCBpZD0iY2xpcFBhdGg2MTIxLTMiIGNsaXBQYXRoVW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgPHBhdGggaWQ9InBhdGg2MTIzLTYiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0ibTQxNDIuNCAxOTAuMDNjMCA2Ny4yMzEgMjAzLjMxIDEyMS43NyA0NTQuMTQgMTIxLjc3czQ1NC4xNC01NC41MzkgNDU0LjE0LTEyMS43N2MwLTY3LjMyLTIwMy4zMS0xMjEuODYtNDU0LjE0LTEyMS44NnMtNDU0LjE0IDU0LjU0My00NTQuMTQgMTIxLjg2Ii8+CiAgPC9jbGlwUGF0aD4KICA8Y2xpcFBhdGggaWQ9ImNsaXBQYXRoNTU2Ny03IiBjbGlwUGF0aFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CiAgIDxwYXRoIGlkPSJwYXRoNTU2OS01IiBkPSJtNDQ3My43IDE1MzEuN2MtMzAuNDcgMC01NS4xNiAyNC42OS01NS4xNiA1NS4xNnYyOTE0LjFjMjguNDMgMzEuMTggNjYuOTkgNTMgMTEwLjMyIDYwLjE0di0yOTc0LjJjMC0zMC40Ny0yNC43LTU1LjE2LTU1LjE2LTU1LjE2Ii8+CiAgPC9jbGlwUGF0aD4KICA8Y2xpcFBhdGggaWQ9ImNsaXBQYXRoNTU0OS02IiBjbGlwUGF0aFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CiAgIDxwYXRoIGlkPSJwYXRoNTU1MS0yIiBkPSJtNDQxOC41IDQ1MDF2MTE4OC42YzAgMzAuNDYgMjQuNjkgNTUuMTYgNTUuMTYgNTUuMTYgMzAuNDYgMCA1NS4xNi0yNC43IDU1LjE2LTU1LjE2di0xMTI4LjRjLTQzLjMzLTcuMTQtODEuODktMjguOTYtMTEwLjMyLTYwLjE0Ii8+CiAgPC9jbGlwUGF0aD4KICA8Y2xpcFBhdGggaWQ9ImNsaXBQYXRoNTUyOS03IiBjbGlwUGF0aFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CiAgIDxwYXRoIGlkPSJwYXRoNTUzMS0wIiBkPSJtNDYzMy4zIDU5NDEuOWgtNzMuNDRjLTE1NC4zNyAwLTI3OS45Ni0xMjUuNTktMjc5Ljk2LTI3OS45NnYtNDIwNS41Yy0yMTguMTEtMTE2LjIyLTM1Ni4wNy0zNDMuMjItMzU2LjA3LTU5My43IDAtMzcwLjk2IDMwMS43OS02NzIuNzUgNjcyLjc1LTY3Mi43NSAzNzAuOTUgMCA2NzIuNzQgMzAxLjc5IDY3Mi43NCA2NzIuNzUgMCAyNTAuNDgtMTM3Ljk1IDQ3Ny40OC0zNTYuMDcgNTkzLjd2NDIwNS41YzAgMTU0LjM3LTEyNS41OSAyNzkuOTYtMjc5Ljk1IDI3OS45Nm0wLTg4LjU4YzEwNS4yNiAwIDE5MS4zOS04Ni4xMiAxOTEuMzktMTkxLjM4di00MjYxLjJjMjA5LjI4LTg4Ljg1IDM1Ni4wNy0yOTYuMjUgMzU2LjA3LTUzNy45NCAwLTMyMi42My0yNjEuNTUtNTg0LjE4LTU4NC4xOC01ODQuMTgtMzIyLjY0IDAtNTg0LjE4IDI2MS41NS01ODQuMTggNTg0LjE4IDAgMjQxLjY5IDE0Ni43OCA0NDkuMDkgMzU2LjA3IDUzNy45NHY0MjYxLjJjMCAxMDUuMjYgODYuMTIgMTkxLjM4IDE5MS4zOSAxOTEuMzhoNzMuNDQiLz4KICA8L2NsaXBQYXRoPgogIDxjbGlwUGF0aCBpZD0iY2xpcFBhdGg1NTE1LTYiIGNsaXBQYXRoVW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgPHBhdGggaWQ9InBhdGg1NTE3LTIiIGQ9Im00NTk2LjYgOTU1Ljg5Yy0xODkuMTYgMC0zNDIuNSA5Ny4yMTktMzQyLjUgMjE3LjE1IDAgMTE5LjkyIDE1My4zNCAyMTcuMTQgMzQyLjUgMjE3LjE0IDExOC41OSAwIDIyMy4xLTM4LjIxIDI4NC41Ny05Ni4yOCAzNi41OC0zNC41NSA1Ny45Mi03Ni4xMiA1Ny45Mi0xMjAuODYgMC04My4zOS03NC4xMy0xNTUuOC0xODIuOS0xOTIuMTgtNDcuNjUtMTUuOTM3LTEwMS45Ni0yNC45NjgtMTU5LjU5LTI0Ljk2OCIvPgogIDwvY2xpcFBhdGg+CiAgPGNsaXBQYXRoIGlkPSJjbGlwUGF0aDU0OTktNiIgY2xpcFBhdGhVbml0cz0idXNlclNwYWNlT25Vc2UiPgogICA8cGF0aCBpZD0icGF0aDU1MDEtMSIgZD0ibTQ3NTYuMiA5ODAuODZjMTA4Ljc3IDM2LjM4MSAxODIuOSAxMDguNzkgMTgyLjkgMTkyLjE4di0xLjI0Yy0wLjczLTgyLjg3LTc0LjY3LTE1NC43My0xODIuOS0xOTAuOTRtMTgyLjkgMTkyLjE4YzAgNDQuNzQtMjEuMzQgODYuMzEtNTcuOTIgMTIwLjg2IDM2LjI0LTM0LjIzIDU3LjUzLTc1LjM2IDU3LjkyLTExOS42M3YtMS4yMyIvPgogIDwvY2xpcFBhdGg+CiAgPHJhZGlhbEdyYWRpZW50IGlkPSJyYWRpYWxHcmFkaWVudDU1MDMtOCIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGN5PSIwIiBjeD0iMCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCg1ODQuMTggMCAwIC01ODQuMTggNDU5Ni42IDg2Mi43NSkiIHI9IjEiPgogICA8c3RvcCBpZD0ic3RvcDU1MDUtNyIgc3RvcC1jb2xvcj0iI2YzYjcwYyIgb2Zmc2V0PSIwIi8+CiAgIDxzdG9wIGlkPSJzdG9wNTUwNy05IiBzdG9wLWNvbG9yPSIjZTczZTIwIiBvZmZzZXQ9IjEiLz4KICA8L3JhZGlhbEdyYWRpZW50PgogIDxjbGlwUGF0aCBpZD0iY2xpcFBhdGg1NDgzLTIiIGNsaXBQYXRoVW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgPHBhdGggaWQ9InBhdGg1NDg1LTAiIGQ9Im00OTM5IDExNzEuOHYxLjI0IDEuMjMtMS4yMy0xLjI0Ii8+CiAgPC9jbGlwUGF0aD4KICA8Y2xpcFBhdGggaWQ9ImNsaXBQYXRoNTQ2Ny01IiBjbGlwUGF0aFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CiAgIDxwYXRoIGlkPSJwYXRoNTQ2OS05IiBkPSJtNTE4MC43IDg2Mi43NWMwLTMyMi42NC0yNjEuNTUtNTg0LjE4LTU4NC4xOC01ODQuMTgtMzIyLjY0IDAtNTg0LjE4IDI2MS41NC01ODQuMTggNTg0LjE4IDAgMzIyLjYzIDI2MS41NCA1ODQuMTggNTg0LjE4IDU4NC4xOCAzMjIuNjMgMCA1ODQuMTgtMjYxLjU1IDU4NC4xOC01ODQuMTgiLz4KICA8L2NsaXBQYXRoPgogIDxjbGlwUGF0aCBpZD0iY2xpcFBhdGg1NDUxLTkiIGNsaXBQYXRoVW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgPHBhdGggaWQ9InBhdGg1NDUzLTciIGQ9Im00ODI0LjcgMTQwMC43Yy03MC4wOSAyOS43Ni0xNDcuMTggNDYuMjItMjI4LjExIDQ2LjIyLTgwLjk0IDAtMTU4LjAzLTE2LjQ2LTIyOC4xMS00Ni4yMnYyOTcxLjVjMCA0OS40NCAxOSA5NC42NyA1MC4wNyAxMjguNzJ2LTI5MTQuMWMwLTMwLjQ3IDI0LjY5LTU1LjE2IDU1LjE2LTU1LjE2IDMwLjQ2IDAgNTUuMTYgMjQuNjkgNTUuMTYgNTUuMTZ2Mjk3NC4yYzEwLjA5IDEuNjYgMjAuNDUgMi41MyAzMSAyLjUzaDczLjQ0YzEwNS4yNiAwIDE5MS4zOS04Ni4xMyAxOTEuMzktMTkxLjM5di0yOTcxLjUiLz4KICA8L2NsaXBQYXRoPgogIDxjbGlwUGF0aCBpZD0iY2xpcFBhdGg1NDMzLTEiIGNsaXBQYXRoVW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgPHBhdGggaWQ9InBhdGg1NDM1LTIiIGQ9Im00ODI0LjcgNDM3Mi4yYzAgMTA1LjI2LTg2LjEzIDE5MS4zOS0xOTEuMzkgMTkxLjM5aC03My40NGMtMTAuNTUgMC0yMC45MS0wLjg3LTMxLTIuNTN2MTEyOC40YzAgMzAuNDYtMjQuNyA1NS4xNi01NS4xNiA1NS4xNi0zMC40NyAwLTU1LjE2LTI0LjctNTUuMTYtNTUuMTZ2LTExODguNmMtMzEuMDctMzQuMDUtNTAuMDctNzkuMjgtNTAuMDctMTI4LjcydjEyODkuN2MwIDEwNS4yNiA4Ni4xMiAxOTEuMzggMTkxLjM5IDE5MS4zOGg3My40NGMxMDUuMjYgMCAxOTEuMzktODYuMTIgMTkxLjM5LTE5MS4zOHYtMTI4OS43Ii8+CiAgPC9jbGlwUGF0aD4KIDwvZGVmcz4KIDxnIGlkPSJsYXllcjEiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMTcuMDcgLTIwNC41MykiPgogIDxnIGZpbGw9IiNmZmYiPgogICA8cGF0aCBpZD0icGF0aDUxNDciIGQ9Im0zMDUuNTkgOTUxLjdjLTQ4Ljg5IDAtODguNTIzLTEwLjYzMi04OC41MjMtMjMuNzUgMC03LjA4NjQgMTEuNTcxLTEzLjQ0OSAyOS45MTktMTcuOCAwLjAzNSAwLjAzIDAuMDY3NSAwLjA1OCAwLjEwMjUgMC4wOS0xOC4yMiA0LjMzNS0yOS43IDEwLjY2My0yOS43IDE3LjcxIDAgMTMuMDY5IDM5LjQ4OSAyMy42NjQgODguMjAxIDIzLjY2NCA0OC43MTEgMCA4OC4yMDEtMTAuNTk1IDg4LjIwMS0yMy42NjQgMC03LjA0NzQtMTEuNDgxLTEzLjM3NS0yOS43MDEtMTcuNzEgMC4wMzYyLTAuMDMxIDAuMDY3NS0wLjA2IDAuMTAyNS0wLjA5IDE4LjM0OCA0LjM1MSAyOS45MTkgMTAuNzEzIDI5LjkxOSAxNy44IDAgMTMuMTE4LTM5LjYzMiAyMy43NS04OC41MjEgMjMuNzVtMzcuNTAyLTQ1LjE3NGMwLjA0MzctMC4wMjYgMC4wODc1LTAuMDU0IDAuMTMxMjUtMC4wNzktMC4wNDM4IDAuMDI1LTAuMDg3NSAwLjA1Mi0wLjEzMTI1IDAuMDc5Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTE0OSIgZD0ibTMwNS41OSA5NTEuNjFjLTQ4LjcxMyAwLTg4LjIwMS0xMC41OTUtODguMjAxLTIzLjY2NCAwLTcuMDQ3NCAxMS40OC0xMy4zNzUgMjkuNy0xNy43MSAwLjAzMzcgMC4wMjkgMC4wNyAwLjA2MSAwLjEwMjUgMC4wOTEtMTguMDg5IDQuMzE3NC0yOS40ODEgMTAuNjExLTI5LjQ4MSAxNy42MTkgMCAxMy4wMjEgMzkuMzQ1IDIzLjU3OCA4Ny44OCAyMy41NzhzODcuODgtMTAuNTU3IDg3Ljg4LTIzLjU3OGMwLTcuMDA3NC0xMS4zOTQtMTMuMzAxLTI5LjQ4Mi0xNy42MTkgMC4wMzM4LTAuMDMgMC4wNy0wLjA2MiAwLjEwMjUtMC4wOTEgMTguMjIgNC4zMzUgMjkuNzAxIDEwLjY2MyAyOS43MDEgMTcuNzEgMCAxMy4wNjktMzkuNDkgMjMuNjY0LTg4LjIwMSAyMy42NjRtMzcuMzcxLTQ1LjAxYzAuMDQ1LTAuMDI2IDAuMDg2My0wLjA1MSAwLjEzMTI1LTAuMDc4LTAuMDQ1IDAuMDI2LTAuMDg2MyAwLjA1MS0wLjEzMTI1IDAuMDc4Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTE1MSIgZD0ibTMwNS41OSA5NTEuNTJjLTQ4LjUzNSAwLTg3Ljg4LTEwLjU1Ny04Ny44OC0yMy41NzggMC03LjAwNzQgMTEuMzkyLTEzLjMwMSAyOS40ODEtMTcuNjE5IDAuMDM2MiAwLjAzIDAuMDY4OCAwLjA1OSAwLjEwMzc1IDAuMDktMTcuOTYgNC4zMDI2LTI5LjI2NSAxMC41NjItMjkuMjY1IDE3LjUyOSAwIDEyLjk3NCAzOS4yMDEgMjMuNDkxIDg3LjU2IDIzLjQ5MSA0OC4zNTggMCA4Ny41NTktMTAuNTE4IDg3LjU1OS0yMy40OTEgMC02Ljk2NzItMTEuMzA1LTEzLjIyNi0yOS4yNjQtMTcuNTI5IDAuMDM1LTAuMDMxIDAuMDY3NS0wLjA2IDAuMTAyNS0wLjA5IDE4LjA4OSA0LjMxNzQgMjkuNDgyIDEwLjYxMSAyOS40ODIgMTcuNjE5IDAgMTMuMDIxLTM5LjM0NSAyMy41NzgtODcuODggMjMuNTc4bTM3LjI0LTQ0Ljg0NWMwLjA0MjUtMC4wMjUgMC4wODc1LTAuMDUyIDAuMTMxMjUtMC4wNzktMC4wNDM4IDAuMDI2LTAuMDg4NyAwLjA1NC0wLjEzMTI1IDAuMDc5Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTE1MyIgZD0ibTMwNS41OSA5NTEuNDRjLTQ4LjM1OSAwLTg3LjU2LTEwLjUxOC04Ny41Ni0yMy40OTEgMC02Ljk2NzIgMTEuMzA1LTEzLjIyNiAyOS4yNjUtMTcuNTI5IDAuMDMzNyAwLjAyOSAwLjA3IDAuMDYxIDAuMTAzNzUgMC4wOS0xNy44MyA0LjI4NjYtMjkuMDQ4IDEwLjUxMS0yOS4wNDggMTcuNDM5IDAgMTIuOTI2IDM5LjA1OCAyMy40MDYgODcuMjM5IDIzLjQwNiA0OC4xOCAwIDg3LjIzOS0xMC40OCA4Ny4yMzktMjMuNDA2IDAtNi45Mjc4LTExLjIxOC0xMy4xNTItMjkuMDQ4LTE3LjQzOSAwLjAzMjUtMC4wMjkgMC4wNy0wLjA2MSAwLjEwMzc1LTAuMDkgMTcuOTU5IDQuMzAyNiAyOS4yNjQgMTAuNTYyIDI5LjI2NCAxNy41MjkgMCAxMi45NzQtMzkuMjAxIDIzLjQ5MS04Ny41NTkgMjMuNDkxbTM3LjEwOC00NC42OGMwLjA0MjUtMC4wMjYgMC4wODg4LTAuMDUyIDAuMTMyNS0wLjA3OS0wLjA0MzggMC4wMjYtMC4wOSAwLjA1My0wLjEzMjUgMC4wNzkiLz4KICAgPHBhdGggaWQ9InBhdGg1MTU1IiBkPSJtMzA1LjU5IDk1MS4zNWMtNDguMTgxIDAtODcuMjM5LTEwLjQ4LTg3LjIzOS0yMy40MDYgMC02LjkyNzcgMTEuMjE4LTEzLjE1MiAyOS4wNDgtMTcuNDM5IDAuMDM1IDAuMDMxIDAuMDY3NSAwLjA2IDAuMTAzNzUgMC4wOTEtMTcuNzAxIDQuMjY5LTI4LjgzMSAxMC40NTktMjguODMxIDE3LjM0OCAwIDEyLjg3OSAzOC45MTUgMjMuMzIgODYuOTE5IDIzLjMyczg2LjkxOC0xMC40NDEgODYuOTE4LTIzLjMyYzAtNi44ODg2LTExLjEzLTEzLjA3OS0yOC44My0xNy4zNDggMC4wMzUtMC4wMzEgMC4wNjc1LTAuMDYgMC4xMDM3NS0wLjA5MSAxNy44MyA0LjI4NjYgMjkuMDQ4IDEwLjUxMSAyOS4wNDggMTcuNDM5IDAgMTIuOTI2LTM5LjA1OSAyMy40MDYtODcuMjM5IDIzLjQwNm0zNi45NzQtNDQuNTE2YzAuMDQ2My0wLjAyOCAwLjA4NzUtMC4wNTMgMC4xMzM3NS0wLjA3OS0wLjA0NjMgMC4wMjYtMC4wODc1IDAuMDUxLTAuMTMzNzUgMC4wNzkiLz4KICAgPHBhdGggaWQ9InBhdGg1MTU3IiBkPSJtMzA1LjU5IDk1MS4yN2MtNDguMDA0IDAtODYuOTE5LTEwLjQ0MS04Ni45MTktMjMuMzIgMC02Ljg4ODYgMTEuMTMtMTMuMDc5IDI4LjgzMS0xNy4zNDggMC4wMzUgMC4wMzEgMC4wNjg4IDAuMDYgMC4xMDM3NSAwLjA5LTE3LjU2OSA0LjI1MjUtMjguNjE0IDEwLjQxLTI4LjYxNCAxNy4yNTcgMCAxMi44MzEgMzguNzcxIDIzLjIzNCA4Ni41OTggMjMuMjM0IDQ3LjgyNiAwIDg2LjU5OC0xMC40MDMgODYuNTk4LTIzLjIzNCAwLTYuODQ3Ni0xMS4wNDYtMTMuMDA1LTI4LjYxNS0xNy4yNTYgMC4wMzYyLTAuMDMxIDAuMDY4Ny0wLjA2IDAuMTA1LTAuMDkxIDE3LjcgNC4yNjkgMjguODMgMTAuNDU5IDI4LjgzIDE3LjM0OCAwIDEyLjg3OS0zOC45MTQgMjMuMzItODYuOTE4IDIzLjMybTM2Ljg0MS00NC4zNTNjMC4wNDI1LTAuMDI1IDAuMDktMC4wNTIgMC4xMzI1LTAuMDc3LTAuMDQyNSAwLjAyNS0wLjA5IDAuMDUyLTAuMTMyNSAwLjA3NyIvPgogICA8cGF0aCBpZD0icGF0aDUxNTkiIGQ9Im0zMDUuNTkgOTUxLjE4Yy00Ny44MjYgMC04Ni41OTgtMTAuNDAzLTg2LjU5OC0yMy4yMzQgMC02Ljg0NzYgMTEuMDQ1LTEzLjAwNSAyOC42MTQtMTcuMjU3IDAuMDMzNyAwLjAzIDAuMDcxMiAwLjA2MiAwLjEwNSAwLjA5MS0xNy40NDEgNC4yMzQ5LTI4LjM5OCAxMC4zNTctMjguMzk4IDE3LjE2NiAwIDEyLjc4NCAzOC42MjYgMjMuMTQ4IDg2LjI3NiAyMy4xNDggNDcuNjQ5IDAgODYuMjc2LTEwLjM2NCA4Ni4yNzYtMjMuMTQ4IDAtNi44MDg2LTEwLjk1Ni0xMi45MzEtMjguMzk4LTE3LjE2NiAwLjAzMjUtMC4wMjkgMC4wNzEyLTAuMDYyIDAuMTAzNzUtMC4wOSAxNy41NjkgNC4yNTE1IDI4LjYxNSAxMC40MDkgMjguNjE1IDE3LjI1NiAwIDEyLjgzMS0zOC43NzEgMjMuMjM0LTg2LjU5OCAyMy4yMzRtMzYuNzA4LTQ0LjE4OGMwLjA0NS0wLjAyNiAwLjA4NzUtMC4wNTEgMC4xMzM3NS0wLjA3OS0wLjA0NjMgMC4wMjgtMC4wODg3IDAuMDUzLTAuMTMzNzUgMC4wNzkiLz4KICAgPHBhdGggaWQ9InBhdGg1MTYxIiBkPSJtMzA1LjU5IDk1MS4wOWMtNDcuNjUgMC04Ni4yNzYtMTAuMzY0LTg2LjI3Ni0yMy4xNDggMC02LjgwODYgMTAuOTU2LTEyLjkzMSAyOC4zOTgtMTcuMTY2IDAuMDM1IDAuMDMxIDAuMDY4OCAwLjA2IDAuMTA1IDAuMDkxLTE3LjMxMSA0LjIxODctMjguMTgyIDEwLjMwNy0yOC4xODIgMTcuMDc1IDAgMTIuNzM2IDM4LjQ4NCAyMy4wNjEgODUuOTU2IDIzLjA2MSA0Ny40NzEgMCA4NS45NTUtMTAuMzI1IDg1Ljk1NS0yMy4wNjEgMC02Ljc2ODUtMTAuODcxLTEyLjg1Ni0yOC4xODEtMTcuMDc1IDAuMDM1LTAuMDMxIDAuMDY4OC0wLjA2IDAuMTA1LTAuMDkxIDE3LjQ0MSA0LjIzNDggMjguMzk4IDEwLjM1NyAyOC4zOTggMTcuMTY2IDAgMTIuNzg0LTM4LjYyOCAyMy4xNDctODYuMjc2IDIzLjE0N20zNi41NzItNDQuMDI0YzAuMDQzOC0wLjAyNSAwLjA5MTMtMC4wNTIgMC4xMzUtMC4wNzctMC4wNDM4IDAuMDI1LTAuMDkxMyAwLjA1Mi0wLjEzNSAwLjA3NyIvPgogICA8cGF0aCBpZD0icGF0aDUxNjMiIGQ9Im0zMDUuNTkgOTUxLjAxYy00Ny40NzMgMC04NS45NTYtMTAuMzI1LTg1Ljk1Ni0yMy4wNjEgMC02Ljc2ODUgMTAuODcxLTEyLjg1NiAyOC4xODItMTcuMDc1IDAuMDM1IDAuMDMgMC4wNjg4IDAuMDYgMC4xMDUgMC4wOS0xNy4xODQgNC4yMDIxLTI3Ljk2NiAxMC4yNTYtMjcuOTY2IDE2Ljk4NSAwIDEyLjY4OCAzOC4zNCAyMi45NzYgODUuNjM1IDIyLjk3NnM4NS42MzUtMTAuMjg4IDg1LjYzNS0yMi45NzZjMC02LjcyOS0xMC43ODQtMTIuNzgzLTI3Ljk2Ni0xNi45ODUgMC4wMzUtMC4wMyAwLjA2ODctMC4wNiAwLjEwNS0wLjA5IDE3LjMxIDQuMjE4OCAyOC4xODEgMTAuMzA3IDI4LjE4MSAxNy4wNzUgMCAxMi43MzYtMzguNDg0IDIzLjA2MS04NS45NTUgMjMuMDYxbTM2LjQzOC00My44NThjMC4wNDUtMC4wMjYgMC4wODg4LTAuMDUzIDAuMTM1LTAuMDc5LTAuMDQ2MyAwLjAyNi0wLjA5IDAuMDUzLTAuMTM1IDAuMDc5Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTE2NSIgZD0ibTMwNS41OSA5NTAuOTJjLTQ3LjI5NSAwLTg1LjYzNS0xMC4yODgtODUuNjM1LTIyLjk3NiAwLTYuNzI5IDEwLjc4Mi0xMi43ODMgMjcuOTY2LTE2Ljk4NSAwLjAzMzcgMC4wMjkgMC4wNzEyIDAuMDYyIDAuMTA1IDAuMDkxLTE3LjA1NCA0LjE4NDUtMjcuNzUxIDEwLjIwNC0yNy43NTEgMTYuODk0IDAgMTIuNjQxIDM4LjE5OCAyMi44OSA4NS4zMTUgMjIuODkgNDcuMTE4IDAgODUuMzE0LTEwLjI0OSA4NS4zMTQtMjIuODkgMC02LjY4OS0xMC42OTgtMTIuNzA5LTI3Ljc1MS0xNi44OTQgMC4wMzM3LTAuMDI5IDAuMDcyNS0wLjA2MiAwLjEwNjI1LTAuMDkxIDE3LjE4MiA0LjIwMjEgMjcuOTY2IDEwLjI1NiAyNy45NjYgMTYuOTg1IDAgMTIuNjg4LTM4LjM0IDIyLjk3Ni04NS42MzUgMjIuOTc2bTM2LjMwMS00My42OTZjMC4wNDYzLTAuMDI2IDAuMDktMC4wNTEgMC4xMzYyNS0wLjA3Ny0wLjA0NjMgMC4wMjYtMC4wOSAwLjA1MS0wLjEzNjI1IDAuMDc3Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTE2NyIgZD0ibTMwNS41OSA5NTAuODRjLTQ3LjExOCAwLTg1LjMxNS0xMC4yNDktODUuMzE1LTIyLjg5IDAtNi42ODkgMTAuNjk4LTEyLjcwOSAyNy43NTEtMTYuODk0IDAuMDM2MiAwLjAzIDAuMDcgMC4wNiAwLjEwNjI1IDAuMDktMTYuOTI1IDQuMTY5LTI3LjUzNiAxMC4xNTQtMjcuNTM2IDE2LjgwNCAwIDEyLjU5NCAzOC4wNTIgMjIuODA0IDg0Ljk5NCAyMi44MDQgNDYuOTQgMCA4NC45OTItMTAuMjEgODQuOTkyLTIyLjgwNCAwLTYuNjQ5OS0xMC42MTEtMTIuNjM1LTI3LjUzNi0xNi44MDQgMC4wMzYyLTAuMDMgMC4wNzEzLTAuMDYgMC4xMDYyNS0wLjA5IDE3LjA1NCA0LjE4NDUgMjcuNzUxIDEwLjIwNCAyNy43NTEgMTYuODk0IDAgMTIuNjQxLTM4LjE5NiAyMi44OS04NS4zMTQgMjIuODltMzYuMTY2LTQzLjUzMWMwLjA0MzgtMC4wMjYgMC4wOTEzLTAuMDU0IDAuMTM1LTAuMDc5LTAuMDQzOCAwLjAyNS0wLjA5MTMgMC4wNTMtMC4xMzUgMC4wNzkiLz4KICAgPHBhdGggaWQ9InBhdGg1MTY5IiBkPSJtMzA1LjU5IDk1MC43NWMtNDYuOTQxIDAtODQuOTk0LTEwLjIxLTg0Ljk5NC0yMi44MDQgMC02LjY0OTkgMTAuNjExLTEyLjYzNSAyNy41MzYtMTYuODA0IDAuMDM2MyAwLjAzMSAwLjA3IDAuMDYgMC4xMDYyNSAwLjA5MS0xNi43OTYgNC4xNTEzLTI3LjMyMSAxMC4xMDItMjcuMzIxIDE2LjcxMiAwIDEyLjU0NiAzNy45MDkgMjIuNzE3IDg0LjY3MyAyMi43MTcgNDYuNzYyIDAgODQuNjcyLTEwLjE3MSA4NC42NzItMjIuNzE3IDAtNi42MDk5LTEwLjUyNS0xMi41NjEtMjcuMzIyLTE2LjcxMiAwLjAzNjItMC4wMzEgMC4wNzEyLTAuMDYgMC4xMDYyNS0wLjA5MSAxNi45MjUgNC4xNjkgMjcuNTM2IDEwLjE1NCAyNy41MzYgMTYuODA0IDAgMTIuNTk0LTM4LjA1MiAyMi44MDQtODQuOTkyIDIyLjgwNG0zNi4wMjktNDMuMzY4YzAuMDQ2My0wLjAyNiAwLjA5MTMtMC4wNTIgMC4xMzc1LTAuMDc3LTAuMDQ2MyAwLjAyNS0wLjA5MTMgMC4wNTEtMC4xMzc1IDAuMDc3Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTE3MSIgZD0ibTMwNS41OSA5NTAuNjZjLTQ2Ljc2NCAwLTg0LjY3My0xMC4xNzEtODQuNjczLTIyLjcxNyAwLTYuNjA5OSAxMC41MjUtMTIuNTYxIDI3LjMyMS0xNi43MTIgMC4wMzYyIDAuMDMgMC4wNzEyIDAuMDYgMC4xMDYyNSAwLjA5LTE2LjY2OCA0LjEzNTMtMjcuMTA4IDEwLjA1My0yNy4xMDggMTYuNjIyIDAgMTIuNDk5IDM3Ljc2NiAyMi42MzEgODQuMzUzIDIyLjYzMSA0Ni41ODYgMCA4NC4zNTEtMTAuMTMzIDg0LjM1MS0yMi42MzEgMC02LjU2OTktMTAuNDM5LTEyLjQ4Ny0yNy4xMDgtMTYuNjIyIDAuMDM2Mi0wLjAzIDAuMDcxMi0wLjA2IDAuMTA2MjUtMC4wOSAxNi43OTggNC4xNTE0IDI3LjMyMiAxMC4xMDIgMjcuMzIyIDE2LjcxMiAwIDEyLjU0Ni0zNy45MSAyMi43MTctODQuNjcyIDIyLjcxN20zNS44OTEtNDMuMjA0YzAuMDQ2My0wLjAyNiAwLjA5MTMtMC4wNTEgMC4xMzc1LTAuMDc4LTAuMDQ2MyAwLjAyNi0wLjA5MTMgMC4wNTEtMC4xMzc1IDAuMDc4Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTE3MyIgZD0ibTMwNS41OSA5NTAuNThjLTQ2LjU4NiAwLTg0LjM1My0xMC4xMzMtODQuMzUzLTIyLjYzMSAwLTYuNTY5OSAxMC40NC0xMi40ODcgMjcuMTA4LTE2LjYyMiAwLjAzNjIgMC4wMzEgMC4wNzEzIDAuMDYyIDAuMTA3NSAwLjA5MS0xNi41NDEgNC4xMTc2LTI2Ljg5NCAxMC0yNi44OTQgMTYuNTMxIDAgMTIuNDUxIDM3LjYyMSAyMi41NDUgODQuMDMxIDIyLjU0NSA0Ni40MDkgMCA4NC4wMzEtMTAuMDk0IDg0LjAzMS0yMi41NDUgMC02LjUzMTMtMTAuMzUyLTEyLjQxNC0yNi44OTQtMTYuNTMxIDAuMDM1LTAuMDMgMC4wNzEyLTAuMDYgMC4xMDYyNS0wLjA5MSAxNi42NjkgNC4xMzUzIDI3LjEwOCAxMC4wNTMgMjcuMTA4IDE2LjYyMiAwIDEyLjQ5OS0zNy43NjUgMjIuNjMxLTg0LjM1MSAyMi42MzFtMzUuNzU0LTQzLjA0YzAuMDQ2My0wLjAyNSAwLjA5MjUtMC4wNTEgMC4xMzc1LTAuMDc4LTAuMDQ1IDAuMDI2LTAuMDkxMiAwLjA1My0wLjEzNzUgMC4wNzgiLz4KICAgPHBhdGggaWQ9InBhdGg1MTc1IiBkPSJtMzA1LjU5IDk1MC40OWMtNDYuNDEgMC04NC4wMzEtMTAuMDk0LTg0LjAzMS0yMi41NDUgMC02LjUzMTIgMTAuMzUyLTEyLjQxNCAyNi44OTQtMTYuNTMxIDAuMDM1IDAuMDMgMC4wNzEyIDAuMDYgMC4xMDc1IDAuMDkxLTE2LjQxNSA0LjEwMDEtMjYuNjgxIDkuOTQ4Ny0yNi42ODEgMTYuNDQgMCAxMi40MDQgMzcuNDc5IDIyLjQ1OSA4My43MTEgMjIuNDU5IDQ2LjIzMSAwIDgzLjcxLTEwLjA1NSA4My43MS0yMi40NTkgMC02LjQ5MTMtMTAuMjY2LTEyLjM0LTI2LjY4LTE2LjQ0IDAuMDM2Mi0wLjAzMSAwLjA3MTItMC4wNjIgMC4xMDc1LTAuMDkxIDE2LjU0MSA0LjExNzYgMjYuODk0IDEwIDI2Ljg5NCAxNi41MzEgMCAxMi40NTEtMzcuNjIyIDIyLjU0NS04NC4wMzEgMjIuNTQ1bTM1LjYxNi00Mi44NzZjMC4wNDUtMC4wMjUgMC4wOTEzLTAuMDUxIDAuMTM3NS0wLjA3OC0wLjA0NjMgMC4wMjYtMC4wOTI1IDAuMDUyLTAuMTM3NSAwLjA3OCIvPgogICA8cGF0aCBpZD0icGF0aDUxNzciIGQ9Im0zMDUuNTkgOTUwLjQxYy00Ni4yMzMgMC04My43MTEtMTAuMDU1LTgzLjcxMS0yMi40NTkgMC02LjQ5MTIgMTAuMjY2LTEyLjM0IDI2LjY4MS0xNi40NCAwLjAzNSAwLjAzIDAuMDcxMyAwLjA2IDAuMTA3NSAwLjA5LTE2LjI4OSA0LjA4NC0yNi40NjggOS44OTc1LTI2LjQ2OCAxNi4zNSAwIDEyLjM1NiAzNy4zMzUgMjIuMzczIDgzLjM5IDIyLjM3M3M4My4zOS0xMC4wMTYgODMuMzktMjIuMzczYzAtNi40NTI2LTEwLjE4LTEyLjI2Ni0yNi40NjgtMTYuMzUgMC4wMzYyLTAuMDMgMC4wNzEzLTAuMDYgMC4xMDc1LTAuMDkgMTYuNDE0IDQuMTAwMSAyNi42OCA5Ljk0ODggMjYuNjggMTYuNDQgMCAxMi40MDQtMzcuNDc5IDIyLjQ1OS04My43MSAyMi40NTltMzUuNDc2LTQyLjcxMmMwLjA0NjMtMC4wMjYgMC4wOTM3LTAuMDUzIDAuMTQtMC4wNzktMC4wNDYzIDAuMDI2LTAuMDkzNyAwLjA1Mi0wLjE0IDAuMDc5Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTE3OSIgZD0ibTMwNS41OSA5NTAuMzJjLTQ2LjA1NSAwLTgzLjM5LTEwLjAxNi04My4zOS0yMi4zNzMgMC02LjQ1MjYgMTAuMTc5LTEyLjI2NiAyNi40NjgtMTYuMzUgMC4wMzYyIDAuMDMgMC4wNzEzIDAuMDYgMC4xMDc1IDAuMDkxLTE2LjE1OSA0LjA2NjQtMjYuMjU0IDkuODQ2MS0yNi4yNTQgMTYuMjU5IDAgMTIuMzA5IDM3LjE5IDIyLjI4OCA4My4wNjkgMjIuMjg4IDQ1Ljg3OCAwIDgzLjA2OS05Ljk3OSA4My4wNjktMjIuMjg4IDAtNi40MTI2LTEwLjA5NS0xMi4xOTItMjYuMjU1LTE2LjI1OSAwLjAzNjItMC4wMyAwLjA3MjUtMC4wNjEgMC4xMDg3NS0wLjA5MSAxNi4yODggNC4wODQgMjYuNDY4IDkuODk3NSAyNi40NjggMTYuMzUgMCAxMi4zNTYtMzcuMzM1IDIyLjM3My04My4zOSAyMi4zNzNtMzUuMzM4LTQyLjU0N2MwLjA0NjMtMC4wMjYgMC4wOTI1LTAuMDUzIDAuMTM4NzUtMC4wNzgtMC4wNDYzIDAuMDI1LTAuMDkyNSAwLjA1MS0wLjEzODc1IDAuMDc4Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTE4MSIgZD0ibTMwNS41OSA5NTAuMjNjLTQ1Ljg3OSAwLTgzLjA2OS05Ljk3OS04My4wNjktMjIuMjg4IDAtNi40MTI2IDEwLjA5NS0xMi4xOTIgMjYuMjU0LTE2LjI1OSAwLjAzNjIgMC4wMyAwLjA3MjUgMC4wNiAwLjEwODc1IDAuMDktMTYuMDMxIDQuMDQ4OC0yNi4wNDIgOS43OTY1LTI2LjA0MiAxNi4xNjkgMCAxMi4yNjEgMzcuMDQ4IDIyLjIwMSA4Mi43NDkgMjIuMjAxIDQ1LjcgMCA4Mi43NDgtOS45Mzk5IDgyLjc0OC0yMi4yMDEgMC02LjM3MjUtMTAuMDExLTEyLjEyLTI2LjA0MS0xNi4xNjkgMC4wMzUtMC4wMyAwLjA3MTMtMC4wNiAwLjEwNzUtMC4wOSAxNi4xNiA0LjA2NjMgMjYuMjU1IDkuODQ2MSAyNi4yNTUgMTYuMjU5IDAgMTIuMzA5LTM3LjE5MSAyMi4yODgtODMuMDY5IDIyLjI4OG0zNS4xOTgtNDIuMzg2YzAuMDQ2My0wLjAyNSAwLjA5MzctMC4wNTEgMC4xNC0wLjA3Ni0wLjA0NjMgMC4wMjUtMC4wOTM3IDAuMDUxLTAuMTQgMC4wNzYiLz4KICAgPHBhdGggaWQ9InBhdGg1MTgzIiBkPSJtMzA1LjU5IDk1MC4xNWMtNDUuNzAxIDAtODIuNzQ5LTkuOTQtODIuNzQ5LTIyLjIwMSAwLTYuMzcyNSAxMC4wMTEtMTIuMTIgMjYuMDQyLTE2LjE2OSAwLjAzNjIgMC4wMzEgMC4wNzI1IDAuMDYyIDAuMTA4NzUgMC4wOTEtMTUuOTA2IDQuMDMxMy0yNS44MyA5Ljc0NDEtMjUuODMgMTYuMDc4IDAgMTIuMjE0IDM2LjkwNCAyMi4xMTUgODIuNDI4IDIyLjExNSA0NS41MjIgMCA4Mi40MjgtOS45MDA5IDgyLjQyOC0yMi4xMTUgMC02LjMzMzUtOS45MjM4LTEyLjA0Ni0yNS44My0xNi4wNzggMC4wMzYyLTAuMDMgMC4wNzI1LTAuMDYgMC4xMDg3NS0wLjA5MSAxNi4wMyA0LjA0ODkgMjYuMDQxIDkuNzk2NSAyNi4wNDEgMTYuMTY5IDAgMTIuMjYxLTM3LjA0OCAyMi4yMDEtODIuNzQ4IDIyLjIwMW0zNS4wNTYtNDIuMjIzYzAuMDQ4Ny0wLjAyNiAwLjA5MjUtMC4wNSAwLjE0MTI1LTAuMDc3LTAuMDQ4NyAwLjAyNy0wLjA5MjUgMC4wNTEtMC4xNDEyNSAwLjA3NyIvPgogICA8cGF0aCBpZD0icGF0aDUxODUiIGQ9Im0zMDUuNTkgOTUwLjA2Yy00NS41MjQgMC04Mi40MjgtOS45MDA5LTgyLjQyOC0yMi4xMTUgMC02LjMzMzUgOS45MjM4LTEyLjA0NiAyNS44My0xNi4wNzggMC4wMzYyIDAuMDMgMC4wNzI1IDAuMDYyIDAuMTA4NzUgMC4wOTEtMTUuNzc5IDQuMDEzNy0yNS42MTkgOS42OTE0LTI1LjYxOSAxNS45ODYgMCAxMi4xNjYgMzYuNzYxIDIyLjAyOSA4Mi4xMDggMjIuMDI5IDQ1LjM0NiAwIDgyLjEwNi05Ljg2MjggODIuMTA2LTIyLjAyOSAwLTYuMjk0OS05LjgzODgtMTEuOTczLTI1LjYxOS0xNS45ODYgMC4wMzYyLTAuMDMgMC4wNzM4LTAuMDYxIDAuMTEtMC4wOTEgMTUuOTA2IDQuMDMxMyAyNS44MyA5Ljc0NDIgMjUuODMgMTYuMDc4IDAgMTIuMjE0LTM2LjkwNSAyMi4xMTUtODIuNDI4IDIyLjExNW0zNC45MTUtNDIuMDU5YzAuMDQ2My0wLjAyNSAwLjA5NS0wLjA1MSAwLjE0MTI1LTAuMDc4LTAuMDQ2MyAwLjAyNi0wLjA5NSAwLjA1My0wLjE0MTI1IDAuMDc4Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTE4NyIgZD0ibTMwNS41OSA5NDkuOThjLTQ1LjM0NiAwLTgyLjEwOC05Ljg2MjgtODIuMTA4LTIyLjAyOSAwLTYuMjk0OSA5Ljg0LTExLjk3MyAyNS42MTktMTUuOTg2IDAuMDM2MyAwLjAzIDAuMDczOCAwLjA2IDAuMTEgMC4wOS0xNS42NTIgMy45OTc3LTI1LjQwOCA5LjY0MTctMjUuNDA4IDE1Ljg5NiAwIDEyLjExOSAzNi42MTYgMjEuOTQyIDgxLjc4NiAyMS45NDIgNDUuMTY5IDAgODEuNzg1LTkuODIzNyA4MS43ODUtMjEuOTQyIDAtNi4yNTQ4LTkuNzUzOC0xMS44OTktMjUuNDA2LTE1Ljg5NiAwLjAzNjItMC4wMyAwLjA3MjUtMC4wNiAwLjEwODc1LTAuMDkgMTUuNzggNC4wMTM4IDI1LjYxOSA5LjY5MTUgMjUuNjE5IDE1Ljk4NiAwIDEyLjE2Ni0zNi43NiAyMi4wMjktODIuMTA2IDIyLjAyOW0zNC43NzItNDEuODk1YzAuMDQ4Ny0wLjAyNiAwLjA5MzctMC4wNTEgMC4xNDI1LTAuMDc4LTAuMDQ4NyAwLjAyNi0wLjA5MzcgMC4wNTEtMC4xNDI1IDAuMDc4Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTE4OSIgZD0ibTMwNS41OSA5NDkuODljLTQ1LjE3IDAtODEuNzg2LTkuODIzNy04MS43ODYtMjEuOTQyIDAtNi4yNTQ5IDkuNzU1LTExLjg5OSAyNS40MDgtMTUuODk2IDAuMDM2MiAwLjAzIDAuMDcyNSAwLjA2MiAwLjExIDAuMDkxLTE1LjUyOCAzLjk3OTktMjUuMTk2IDkuNTg4OS0yNS4xOTYgMTUuODA1IDAgMTIuMDcxIDM2LjQ3MiAyMS44NTYgODEuNDY1IDIxLjg1NiA0NC45OTEgMCA4MS40NjUtOS43ODUyIDgxLjQ2NS0yMS44NTYgMC02LjIxNjItOS42Ny0xMS44MjUtMjUuMTk2LTE1LjgwNSAwLjAzNjItMC4wMyAwLjA3MzgtMC4wNjEgMC4xMS0wLjA5MSAxNS42NTIgMy45OTc2IDI1LjQwNiA5LjY0MTYgMjUuNDA2IDE1Ljg5NiAwIDEyLjExOS0zNi42MTYgMjEuOTQyLTgxLjc4NSAyMS45NDJtMzQuNjMtNDEuNzMxYzAuMDQ3NS0wLjAyNSAwLjA5NjItMC4wNTMgMC4xNDI1LTAuMDc4LTAuMDQ2MyAwLjAyNS0wLjA5NSAwLjA1Mi0wLjE0MjUgMC4wNzgiLz4KICAgPHBhdGggaWQ9InBhdGg1MTkxIiBkPSJtMzA1LjU5IDk0OS44Yy00NC45OTMgMC04MS40NjUtOS43ODUyLTgxLjQ2NS0yMS44NTYgMC02LjIxNjIgOS42Njg4LTExLjgyNSAyNS4xOTYtMTUuODA1IDAuMDM2MiAwLjAzIDAuMDczOCAwLjA2MiAwLjExIDAuMDkxLTE1LjM5OCAzLjk2MTQtMjQuOTg2IDkuNTM3Ny0yNC45ODYgMTUuNzE0IDAgMTIuMDI0IDM2LjMzIDIxLjc3IDgxLjE0NSAyMS43NyA0NC44MTQgMCA4MS4xNDQtOS43NDYxIDgxLjE0NC0yMS43NyAwLTYuMTc2Mi05LjU4NzUtMTEuNzUyLTI0Ljk4NS0xNS43MTQgMC4wMzYyLTAuMDMgMC4wNzM3LTAuMDYxIDAuMTEtMC4wOTEgMTUuNTI2IDMuOTc5OSAyNS4xOTYgOS41ODg5IDI1LjE5NiAxNS44MDUgMCAxMi4wNzEtMzYuNDc0IDIxLjg1Ni04MS40NjUgMjEuODU2bTM0LjQ4OC00MS41NjljMC4wNDg3LTAuMDI1IDAuMDk1LTAuMDUgMC4xNDI1LTAuMDc2LTAuMDQ3NSAwLjAyNi0wLjA5MzcgMC4wNTEtMC4xNDI1IDAuMDc2Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTE5MyIgZD0ibTMwNS41OSA5NDkuNzJjLTQ0LjgxNSAwLTgxLjE0NS05Ljc0NjEtODEuMTQ1LTIxLjc3IDAtNi4xNzYyIDkuNTg4OC0xMS43NTIgMjQuOTg2LTE1LjcxNCAwLjAzNjIgMC4wMjkgMC4wNzM3IDAuMDYgMC4xMSAwLjA5LTE1LjI3MiAzLjk0NDgtMjQuNzc1IDkuNDg1OC0yNC43NzUgMTUuNjI0IDAgMTEuOTc2IDM2LjE4NiAyMS42ODUgODAuODI0IDIxLjY4NXM4MC44MjItOS43MDkgODAuODIyLTIxLjY4NWMwLTYuMTM3Ny05LjUwMTItMTEuNjc5LTI0Ljc3NS0xNS42MjQgMC4wMzYyLTAuMDMgMC4wNzUtMC4wNjIgMC4xMTEyNS0wLjA5IDE1LjM5OCAzLjk2MTQgMjQuOTg1IDkuNTM3NyAyNC45ODUgMTUuNzE0IDAgMTIuMDI0LTM2LjMzIDIxLjc3LTgxLjE0NCAyMS43N20zNC4zNDQtNDEuNDA1YzAuMDQ2My0wLjAyNSAwLjA5NjMtMC4wNTIgMC4xNDM3NS0wLjA3Ny0wLjA0NzUgMC4wMjUtMC4wOTc1IDAuMDUyLTAuMTQzNzUgMC4wNzciLz4KICA8L2c+CiAgPGc+CiAgIDxwYXRoIGlkPSJwYXRoNTE5NSIgZD0ibTMwNS41OSA5NDkuNjNjLTQ0LjYzOCAwLTgwLjgyNC05LjcwOS04MC44MjQtMjEuNjg1IDAtNi4xMzc3IDkuNTAyNS0xMS42NzkgMjQuNzc1LTE1LjYyNCAwLjAzODcgMC4wMzEgMC4wNzI1IDAuMDYgMC4xMTEyNSAwLjA5MS0xNS4xNDYgMy45Mjc4LTI0LjU2NiA5LjQzNTEtMjQuNTY2IDE1LjUzMyAwIDExLjkyOSAzNi4wNDIgMjEuNTk5IDgwLjUwNCAyMS41OTkgNDQuNDYgMCA4MC41MDItOS42Njk5IDgwLjUwMi0yMS41OTkgMC02LjA5NzctOS40Mi0xMS42MDUtMjQuNTY2LTE1LjUzMyAwLjAzODctMC4wMzEgMC4wNzI1LTAuMDYgMC4xMTEyNS0wLjA5MSAxNS4yNzQgMy45NDQ4IDI0Ljc3NSA5LjQ4NTggMjQuNzc1IDE1LjYyNCAwIDExLjk3Ni0zNi4xODUgMjEuNjg1LTgwLjgyMiAyMS42ODVtMzQuMTk5LTQxLjI0NGMwLjA0ODctMC4wMjUgMC4wOTYyLTAuMDUgMC4xNDUtMC4wNzctMC4wNDg3IDAuMDI2LTAuMDk2MiAwLjA1MS0wLjE0NSAwLjA3NyIgZmlsbD0iI2ZlZmZmZiIvPgogICA8cGF0aCBpZD0icGF0aDUxOTciIGQ9Im0zMDUuNTkgOTQ5LjU1Yy00NC40NjEgMC04MC41MDQtOS42Ny04MC41MDQtMjEuNTk5IDAtNi4wOTc2IDkuNDItMTEuNjA1IDI0LjU2Ni0xNS41MzMgMC4wMzYyIDAuMDMgMC4wNzUgMC4wNjIgMC4xMTEyNSAwLjA5MS0xNS4wMjIgMy45MTAxLTI0LjM1NiA5LjM4MjctMjQuMzU2IDE1LjQ0MSAwIDExLjg4MSAzNS44OTkgMjEuNTEzIDgwLjE4MyAyMS41MTMgNDQuMjgyIDAgODAuMTgxLTkuNjMxMyA4MC4xODEtMjEuNTEzIDAtNi4wNTg2LTkuMzMzOC0xMS41MzEtMjQuMzU2LTE1LjQ0MSAwLjAzNzUtMC4wMyAwLjA3NS0wLjA2MSAwLjExMTI1LTAuMDkxIDE1LjE0NiAzLjkyNzggMjQuNTY2IDkuNDM1MiAyNC41NjYgMTUuNTMzIDAgMTEuOTI5LTM2LjA0MiAyMS41OTktODAuNTAyIDIxLjU5OW0zNC4wNTQtNDEuMDhjMC4wNDg3LTAuMDI2IDAuMDk2Mi0wLjA1MSAwLjE0NS0wLjA3Ny0wLjA0ODcgMC4wMjYtMC4wOTYyIDAuMDUxLTAuMTQ1IDAuMDc3IiBmaWxsPSIjZmVmZmZmIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTE5OSIgZD0ibTMwNS41OSA5NDkuNDZjLTQ0LjI4NCAwLTgwLjE4My05LjYzMTMtODAuMTgzLTIxLjUxMyAwLTYuMDU4NiA5LjMzMzgtMTEuNTMxIDI0LjM1Ni0xNS40NDEgMC4wMzYyIDAuMDI5IDAuMDc1IDAuMDYyIDAuMTExMjUgMC4wOS0xNC44OTUgMy44OTIyLTI0LjE0NiA5LjMzMjctMjQuMTQ2IDE1LjM1MSAwIDExLjgzNCAzNS43NTUgMjEuNDI2IDc5Ljg2MSAyMS40MjZzNzkuODYxLTkuNTkyMyA3OS44NjEtMjEuNDI2YzAtNi4wMTg1LTkuMjUxMi0xMS40NTktMjQuMTQ4LTE1LjM1MSAwLjAzNjItMC4wMjkgMC4wNzUtMC4wNjIgMC4xMTEyNS0wLjA5IDE1LjAyMiAzLjkxMDIgMjQuMzU2IDkuMzgyOCAyNC4zNTYgMTUuNDQxIDAgMTEuODgxLTM1Ljg5OSAyMS41MTMtODAuMTgxIDIxLjUxM20zMy45MDktNDAuOTE3YzAuMDQ4Ny0wLjAyNSAwLjA5NjItMC4wNSAwLjE0NS0wLjA3Ny0wLjA0ODcgMC4wMjYtMC4wOTYyIDAuMDUxLTAuMTQ1IDAuMDc3IiBmaWxsPSIjZmVmZWZmIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTIwMSIgZD0ibTMwNS41OSA5NDkuMzdjLTQ0LjEwNiAwLTc5Ljg2MS05LjU5MjMtNzkuODYxLTIxLjQyNiAwLTYuMDE4NSA5LjI1MTItMTEuNDU5IDI0LjE0Ni0xNS4zNTEgMC4wMzg3IDAuMDMxIDAuMDczOCAwLjA2IDAuMTEyNSAwLjA5MS0xNC43NyAzLjg3NTUtMjMuOTM5IDkuMjgwMi0yMy45MzkgMTUuMjYgMCAxMS43ODYgMzUuNjExIDIxLjM0IDc5LjU0MSAyMS4zNCA0My45MjkgMCA3OS41NC05LjU1MzcgNzkuNTQtMjEuMzQgMC01Ljk4LTkuMTY3NS0xMS4zODUtMjMuOTM5LTE1LjI2IDAuMDM4Ny0wLjAzMSAwLjA3MzctMC4wNiAwLjExMjUtMC4wOTEgMTQuODk2IDMuODkyMSAyNC4xNDggOS4zMzI2IDI0LjE0OCAxNS4zNTEgMCAxMS44MzQtMzUuNzU1IDIxLjQyNi03OS44NjEgMjEuNDI2bTMzLjc2Mi00MC43NTRjMC4wNDYzLTAuMDI1IDAuMDk4OC0wLjA1MiAwLjE0NjI1LTAuMDc3LTAuMDQ3NSAwLjAyNS0wLjEgMC4wNTItMC4xNDYyNSAwLjA3NyIgZmlsbD0iI2ZlZmVmZSIvPgogICA8cGF0aCBpZD0icGF0aDUyMDMiIGQ9Im0zMDUuNTkgOTQ5LjI5Yy00My45MyAwLTc5LjU0MS05LjU1MzctNzkuNTQxLTIxLjM0IDAtNS45OCA5LjE2ODgtMTEuMzg1IDIzLjkzOS0xNS4yNiAwLjAzNjIgMC4wMyAwLjA3NjMgMC4wNjIgMC4xMTI1IDAuMDkxLTE0LjY0NSAzLjg1NjUtMjMuNzMgOS4yMjc2LTIzLjczIDE1LjE2OSAwIDExLjczOSAzNS40NjggMjEuMjU1IDc5LjIyIDIxLjI1NSA0My43NTEgMCA3OS4yMTktOS41MTYyIDc5LjIxOS0yMS4yNTUgMC01Ljk0MTQtOS4wODM4LTExLjMxMi0yMy43My0xNS4xNjkgMC4wMzc1LTAuMDMgMC4wNzYzLTAuMDYxIDAuMTEyNS0wLjA5MSAxNC43NzEgMy44NzU1IDIzLjkzOSA5LjI4MDIgMjMuOTM5IDE1LjI2IDAgMTEuNzg2LTM1LjYxMSAyMS4zNC03OS41NCAyMS4zNG0zMy42MTUtNDAuNTkxYzAuMDQ4Ny0wLjAyNSAwLjA5NzUtMC4wNTEgMC4xNDc1LTAuMDc2LTAuMDUgMC4wMjUtMC4wOTg3IDAuMDUxLTAuMTQ3NSAwLjA3NiIgZmlsbD0iI2ZlZmVmZSIvPgogICA8cGF0aCBpZD0icGF0aDUyMDUiIGQ9Im0zMDUuNTkgOTQ5LjJjLTQzLjc1MyAwLTc5LjIyLTkuNTE2MS03OS4yMi0yMS4yNTUgMC01Ljk0MTQgOS4wODUtMTEuMzEyIDIzLjczLTE1LjE2OSAwLjAzODcgMC4wMzEgMC4wNzM4IDAuMDU5IDAuMTEyNSAwLjA5LTE0LjUyMSAzLjgzOTktMjMuNTIyIDkuMTc3My0yMy41MjIgMTUuMDc5IDAgMTEuNjkxIDM1LjMyNSAyMS4xNjkgNzguOSAyMS4xNjlzNzguODk5LTkuNDc3NiA3OC44OTktMjEuMTY5YzAtNS45MDEzLTkuMDAxMi0xMS4yMzktMjMuNTIyLTE1LjA3OSAwLjAzODctMC4wMzEgMC4wNzM3LTAuMDU5IDAuMTEyNS0wLjA5IDE0LjY0NiAzLjg1NjUgMjMuNzMgOS4yMjc2IDIzLjczIDE1LjE2OSAwIDExLjczOS0zNS40NjggMjEuMjU1LTc5LjIxOSAyMS4yNTVtMzMuNDY4LTQwLjQzYzAuMDUxMy0wLjAyNiAwLjA5NjMtMC4wNSAwLjE0NzUtMC4wNzctMC4wNTEyIDAuMDI2LTAuMDk2MiAwLjA1LTAuMTQ3NSAwLjA3NyIgZmlsbD0iI2ZkZmVmZSIvPgogICA8cGF0aCBpZD0icGF0aDUyMDciIGQ9Im0zMDUuNTkgOTQ5LjEyYy00My41NzUgMC03OC45LTkuNDc3Ni03OC45LTIxLjE2OSAwLTUuOTAxNCA5LjAwMTItMTEuMjM5IDIzLjUyMi0xNS4wNzlsMC4xMTM3NSAwLjA5MWMtMTQuMzk1IDMuODIwNy0yMy4zMTUgOS4xMjUtMjMuMzE1IDE0Ljk4NyAwIDExLjY0NCAzNS4xODEgMjEuMDgzIDc4LjU3OSAyMS4wODNzNzguNTc4LTkuNDM5IDc4LjU3OC0yMS4wODNjMC01Ljg2MjItOC45Mi0xMS4xNjYtMjMuMzE1LTE0Ljk4NyAwLjAzNzUtMC4wMjkgMC4wNzc1LTAuMDYyIDAuMTEzNzUtMC4wOTEgMTQuNTIxIDMuODM5OSAyMy41MjIgOS4xNzcyIDIzLjUyMiAxNS4wNzkgMCAxMS42OTEtMzUuMzI0IDIxLjE2OS03OC44OTkgMjEuMTY5bTMzLjMxOS00MC4yNjhjMC4wNDg3LTAuMDI1IDAuMDk4Ny0wLjA1MSAwLjE0ODc1LTAuMDc2LTAuMDUgMC4wMjUtMC4xIDAuMDUxLTAuMTQ4NzUgMC4wNzYiIGZpbGw9IiNmZGZlZmUiLz4KICAgPHBhdGggaWQ9InBhdGg1MjA5IiBkPSJtMzA1LjU5IDk0OS4wM2MtNDMuMzk4IDAtNzguNTc5LTkuNDM4OS03OC41NzktMjEuMDgyIDAtNS44NjIyIDguOTItMTEuMTY2IDIzLjMxNS0xNC45ODcgMC4wMzg3IDAuMDMxIDAuMDc1IDAuMDYgMC4xMTM3NSAwLjA5MS0xNC4yNzEgMy44MDQyLTIzLjEwOCA5LjA3MjctMjMuMTA4IDE0Ljg5NiAwIDExLjU5NiAzNS4wMzYgMjAuOTk2IDc4LjI1OCAyMC45OTYgNDMuMjIgMCA3OC4yNTgtOS4zOTk5IDc4LjI1OC0yMC45OTYgMC01LjgyMzgtOC44MzYyLTExLjA5Mi0yMy4xMDktMTQuODk2IDAuMDQtMC4wMzEgMC4wNzUtMC4wNiAwLjExMzc1LTAuMDkxIDE0LjM5NSAzLjgyMDcgMjMuMzE1IDkuMTI1IDIzLjMxNSAxNC45ODcgMCAxMS42NDQtMzUuMTggMjEuMDgyLTc4LjU3OCAyMS4wODJtMzMuMTctNDAuMTA1YzAuMDQ4Ny0wLjAyNSAwLjEtMC4wNTEgMC4xNDg3NS0wLjA3Ni0wLjA0ODcgMC4wMjUtMC4xIDAuMDUxLTAuMTQ4NzUgMC4wNzYiIGZpbGw9IiNmZGZkZmUiLz4KICAgPHBhdGggaWQ9InBhdGg1MjExIiBkPSJtMzA1LjU5IDk0OC45NGMtNDMuMjIxIDAtNzguMjU4LTkuMzk5OS03OC4yNTgtMjAuOTk2IDAtNS44MjM3IDguODM2Mi0xMS4wOTIgMjMuMTA4LTE0Ljg5NiAwLjAzNjIgMC4wMjkgMC4wNzc1IDAuMDYyIDAuMTEzNzUgMC4wOTEtMTQuMTQ2IDMuNzg1MS0yMi45MDEgOS4wMjE0LTIyLjkwMSAxNC44MDUgMCAxMS41NDkgMzQuODk0IDIwLjkxIDc3LjkzOCAyMC45MSA0My4wNDIgMCA3Ny45MzYtOS4zNjE0IDc3LjkzNi0yMC45MSAwLTUuNzgzNy04Ljc1NS0xMS4wMi0yMi45MDEtMTQuODA1bDAuMTEzNzUtMC4wOTFjMTQuMjcyIDMuODA0MiAyMy4xMDkgOS4wNzI3IDIzLjEwOSAxNC44OTYgMCAxMS41OTYtMzUuMDM4IDIwLjk5Ni03OC4yNTggMjAuOTk2bTMzLjAyLTM5Ljk0MmMwLjA0ODctMC4wMjUgMC4xMDEyNS0wLjA1MSAwLjE1LTAuMDc2LTAuMDQ4NyAwLjAyNS0wLjEwMTI1IDAuMDUxLTAuMTUgMC4wNzYiIGZpbGw9IiNmZGZkZmQiLz4KICAgPHBhdGggaWQ9InBhdGg1MjEzIiBkPSJtMzA1LjU5IDk0OC44NmMtNDMuMDQ0IDAtNzcuOTM4LTkuMzYxNC03Ny45MzgtMjAuOTEgMC01Ljc4MzcgOC43NTUtMTEuMDIgMjIuOTAxLTE0LjgwNSAwLjA0IDAuMDMxIDAuMDc2MiAwLjA2IDAuMTE1IDAuMDktMTQuMDI1IDMuNzY4Ni0yMi42OTUgOC45Njk3LTIyLjY5NSAxNC43MTUgMCAxMS41MDEgMzQuNzUgMjAuODI0IDc3LjYxNiAyMC44MjRzNzcuNjE1LTkuMzIyMyA3Ny42MTUtMjAuODI0YzAtNS43NDUyLTguNjctMTAuOTQ2LTIyLjY5NC0xNC43MTUgMC4wMzg3LTAuMDMgMC4wNzUtMC4wNTkgMC4xMTM3NS0wLjA5IDE0LjE0NiAzLjc4NTEgMjIuOTAxIDkuMDIxNCAyMi45MDEgMTQuODA1IDAgMTEuNTQ5LTM0Ljg5NCAyMC45MS03Ny45MzYgMjAuOTFtMzIuODctMzkuNzhjMC4wNTEyLTAuMDI2IDAuMDk4Ny0wLjA1IDAuMTUtMC4wNzYtMC4wNTEzIDAuMDI2LTAuMDk4OCAwLjA1LTAuMTUgMC4wNzYiIGZpbGw9IiNmZGZkZmQiLz4KICAgPHBhdGggaWQ9InBhdGg1MjE1IiBkPSJtMzA1LjU5IDk0OC43N2MtNDIuODY2IDAtNzcuNjE2LTkuMzIyMi03Ny42MTYtMjAuODI0IDAtNS43NDUxIDguNjctMTAuOTQ2IDIyLjY5NS0xNC43MTUgMC4wMzYyIDAuMDMgMC4wNzc1IDAuMDYyIDAuMTE1IDAuMDkxLTEzLjkwMSAzLjc1LTIyLjQ4OSA4LjkxNzUtMjIuNDg5IDE0LjYyNCAwIDExLjQ1NCAzNC42MDYgMjAuNzM3IDc3LjI5NSAyMC43MzdzNzcuMjk1LTkuMjgzNiA3Ny4yOTUtMjAuNzM3YzAtNS43MDYtOC41ODg4LTEwLjg3NC0yMi40ODktMTQuNjI0IDAuMDM2Mi0wLjAyOSAwLjA3NzUtMC4wNjIgMC4xMTUtMC4wOTEgMTQuMDI0IDMuNzY4NyAyMi42OTQgOC45Njk4IDIyLjY5NCAxNC43MTUgMCAxMS41MDItMzQuNzQ5IDIwLjgyNC03Ny42MTUgMjAuODI0bTMyLjcxOS0zOS42MThjMC4wNS0wLjAyNSAwLjEwMTI1LTAuMDUxIDAuMTUxMjUtMC4wNzYtMC4wNSAwLjAyNS0wLjEwMTI1IDAuMDUxLTAuMTUxMjUgMC4wNzYiIGZpbGw9IiNmY2ZkZmQiLz4KICAgPHBhdGggaWQ9InBhdGg1MjE3IiBkPSJtMzA1LjU5IDk0OC42OGMtNDIuNjg5IDAtNzcuMjk1LTkuMjgzNy03Ny4yOTUtMjAuNzM3IDAtNS43MDYgOC41ODc1LTEwLjg3NCAyMi40ODktMTQuNjI0IDAuMDM4OCAwLjAzMSAwLjA3NjMgMC4wNiAwLjExNSAwLjA5MS0xMy43NzggMy43MzE1LTIyLjI4NCA4Ljg2NTMtMjIuMjg0IDE0LjUzMyAwIDExLjQwNiAzNC40NjIgMjAuNjUyIDc2Ljk3NSAyMC42NTIgNDIuNTExIDAgNzYuOTc0LTkuMjQ2MSA3Ni45NzQtMjAuNjUyIDAtNS42Njc1LTguNTA2Mi0xMC44MDEtMjIuMjg0LTE0LjUzMyAwLjA0LTAuMDMxIDAuMDc2Mi0wLjA1OSAwLjExNjI1LTAuMDkxIDEzLjkgMy43NSAyMi40ODkgOC45MTc1IDIyLjQ4OSAxNC42MjQgMCAxMS40NTQtMzQuNjA2IDIwLjczNy03Ny4yOTUgMjAuNzM3bTMyLjU2Ni0zOS40NTVjMC4wNTI1LTAuMDI2IDAuMTAxMjUtMC4wNSAwLjE1MjUtMC4wNzYtMC4wNTEyIDAuMDI2LTAuMSAwLjA1LTAuMTUyNSAwLjA3NiIgZmlsbD0iI2ZjZmRmZCIvPgogICA8cGF0aCBpZD0icGF0aDUyMTkiIGQ9Im0zMDUuNTkgOTQ4LjZjLTQyLjUxMyAwLTc2Ljk3NS05LjI0NjEtNzYuOTc1LTIwLjY1MiAwLTUuNjY3NSA4LjUwNjItMTAuODAxIDIyLjI4NC0xNC41MzMgMC4wMzg3IDAuMDMxIDAuMDc2MyAwLjA2IDAuMTE2MjUgMC4wOTEtMTMuNjUxIDMuNzEzOC0yMi4wNzkgOC44MTM4LTIyLjA3OSAxNC40NDEgMCAxMS4zNTkgMzQuMzE5IDIwLjU2NiA3Ni42NTQgMjAuNTY2IDQyLjMzNCAwIDc2LjY1NC05LjIwNzUgNzYuNjU0LTIwLjU2NiAwLTUuNjI3NS04LjQyNzUtMTAuNzI4LTIyLjA3OS0xNC40NDEgMC4wMzg3LTAuMDMxIDAuMDc2My0wLjA2IDAuMTE1LTAuMDkxIDEzLjc3OCAzLjczMTUgMjIuMjg0IDguODY1MiAyMi4yODQgMTQuNTMzIDAgMTEuNDA2LTM0LjQ2MiAyMC42NTItNzYuOTc0IDIwLjY1Mm0zMi40MTQtMzkuMjk0YzAuMDUtMC4wMjUgMC4xMDM3NS0wLjA1MSAwLjE1MjUtMC4wNzctMC4wNDg3IDAuMDI1LTAuMTAyNSAwLjA1MS0wLjE1MjUgMC4wNzciIGZpbGw9IiNmY2ZjZmQiLz4KICAgPHBhdGggaWQ9InBhdGg1MjIxIiBkPSJtMzA1LjU5IDk0OC41MWMtNDIuMzM1IDAtNzYuNjU0LTkuMjA3NS03Ni42NTQtMjAuNTY2IDAtNS42Mjc1IDguNDI3NS0xMC43MjggMjIuMDc5LTE0LjQ0MSAwLjAzNjIgMC4wMjkgMC4wNzg3IDAuMDYyIDAuMTE2MjUgMC4wOTEtMTMuNTMgMy42OTU0LTIxLjg3NSA4Ljc2MTMtMjEuODc1IDE0LjM1IDAgMTEuMzExIDM0LjE3NiAyMC40OCA3Ni4zMzQgMjAuNDhzNzYuMzMyLTkuMTY4OSA3Ni4zMzItMjAuNDhjMC01LjU4ODktOC4zNDUtMTAuNjU1LTIxLjg3NC0xNC4zNSAwLjAzNzUtMC4wMjkgMC4wNzg4LTAuMDYyIDAuMTE2MjUtMC4wOTEgMTMuNjUxIDMuNzEzOSAyMi4wNzkgOC44MTM5IDIyLjA3OSAxNC40NDEgMCAxMS4zNTktMzQuMzIgMjAuNTY2LTc2LjY1NCAyMC41NjZtMzIuMjYxLTM5LjEzM2MwLjA1MTItMC4wMjUgMC4xMDEyNS0wLjA1IDAuMTUyNS0wLjA3NS0wLjA1MTIgMC4wMjUtMC4xMDEyNSAwLjA1LTAuMTUyNSAwLjA3NSIgZmlsbD0iI2ZjZmNmYyIvPgogICA8cGF0aCBpZD0icGF0aDUyMjMiIGQ9Im0zMDUuNTkgOTQ4LjQzYy00Mi4xNTggMC03Ni4zMzQtOS4xNjg5LTc2LjMzNC0yMC40OCAwLTUuNTg4OSA4LjM0NS0xMC42NTUgMjEuODc1LTE0LjM1IDAuMDM4NyAwLjAzIDAuMDc2MyAwLjA2IDAuMTE2MjUgMC4wOTEtMTMuNDA1IDMuNjc2Mi0yMS42NyA4LjcwOS0yMS42NyAxNC4yNTkgMCAxMS4yNjQgMzQuMDMxIDIwLjM5NCA3Ni4wMTMgMjAuMzk0IDQxLjk4IDAgNzYuMDExLTkuMTI5OSA3Ni4wMTEtMjAuMzk0IDAtNS41NDk3LTguMjYzOC0xMC41ODItMjEuNjY5LTE0LjI1OSAwLjAzODctMC4wMzEgMC4wNzc1LTAuMDYxIDAuMTE2MjUtMC4wOTEgMTMuNTI5IDMuNjk1NCAyMS44NzQgOC43NjEyIDIxLjg3NCAxNC4zNSAwIDExLjMxMS0zNC4xNzUgMjAuNDgtNzYuMzMyIDIwLjQ4bTMyLjEwOC0zOC45N2MwLjA1MTMtMC4wMjYgMC4xMDI1LTAuMDUgMC4xNTM3NS0wLjA3Ni0wLjA1MTIgMC4wMjYtMC4xMDI1IDAuMDUtMC4xNTM3NSAwLjA3NiIgZmlsbD0iI2ZiZmNmYyIvPgogICA8cGF0aCBpZD0icGF0aDUyMjUiIGQ9Im0zMDUuNTkgOTQ4LjM0Yy00MS45ODEgMC03Ni4wMTMtOS4xMjk5LTc2LjAxMy0yMC4zOTQgMC01LjU0OTcgOC4yNjUtMTAuNTgyIDIxLjY3LTE0LjI1OSAwLjAzODcgMC4wMyAwLjA3NzUgMC4wNiAwLjExNjI1IDAuMDktMTMuMjg0IDMuNjYwMi0yMS40NjUgOC42NTc3LTIxLjQ2NSAxNC4xNjkgMCAxMS4yMTYgMzMuODg4IDIwLjMwOCA3NS42OTEgMjAuMzA4IDQxLjgwMiAwIDc1LjY5MS05LjA5MTQgNzUuNjkxLTIwLjMwOCAwLTUuNTExMy04LjE4MjUtMTAuNTA5LTIxLjQ2Ni0xNC4xNjkgMC4wNC0wLjAzIDAuMDc3NS0wLjA2IDAuMTE3NS0wLjA5IDEzLjQwNSAzLjY3NjIgMjEuNjY5IDguNzA5IDIxLjY2OSAxNC4yNTkgMCAxMS4yNjQtMzQuMDMxIDIwLjM5NC03Ni4wMTEgMjAuMzk0bTMxLjk1Mi0zOC44MDljMC4wNTEzLTAuMDI1IDAuMTAyNS0wLjA1IDAuMTU1LTAuMDc1LTAuMDUyNSAwLjAyNS0wLjEwMzc1IDAuMDUtMC4xNTUgMC4wNzUiIGZpbGw9IiNmYmZjZmMiLz4KICAgPHBhdGggaWQ9InBhdGg1MjI3IiBkPSJtMzA1LjU5IDk0OC4yNWMtNDEuODA0IDAtNzUuNjkxLTkuMDkxMy03NS42OTEtMjAuMzA4IDAtNS41MTEyIDguMTgxMi0xMC41MDkgMjEuNDY1LTE0LjE2OSAwLjA0IDAuMDMxIDAuMDc4NyAwLjA2MiAwLjExNzUgMC4wOTEtMTMuMTYxIDMuNjQxMS0yMS4yNjIgOC42MDUtMjEuMjYyIDE0LjA3OCAwIDExLjE2OSAzMy43NDUgMjAuMjIxIDc1LjM3MSAyMC4yMjFzNzUuMzctOS4wNTIyIDc1LjM3LTIwLjIyMWMwLTUuNDcyNi04LjEwMTItMTAuNDM2LTIxLjI2Mi0xNC4wNzggMC4wNC0wLjAzIDAuMDc4OC0wLjA2IDAuMTE3NS0wLjA5MSAxMy4yODQgMy42NjAzIDIxLjQ2NiA4LjY1NzggMjEuNDY2IDE0LjE2OSAwIDExLjIxNi0zMy44ODkgMjAuMzA4LTc1LjY5MSAyMC4zMDhtMzEuNzk4LTM4LjY0NmMwLjA1MTMtMC4wMjUgMC4xMDM3NS0wLjA1MSAwLjE1NS0wLjA3Ni0wLjA1MTIgMC4wMjUtMC4xMDM3NSAwLjA1MS0wLjE1NSAwLjA3NiIgZmlsbD0iI2ZiZmJmYiIvPgogICA8cGF0aCBpZD0icGF0aDUyMjkiIGQ9Im0zMDUuNTkgOTQ4LjE3Yy00MS42MjYgMC03NS4zNzEtOS4wNTIyLTc1LjM3MS0yMC4yMjEgMC01LjQ3MjYgOC4xMDEyLTEwLjQzNiAyMS4yNjItMTQuMDc4IDAuMDQgMC4wMzEgMC4wNzg3IDAuMDYyIDAuMTE3NSAwLjA5MS0xMy4wMzggMy42MjI2LTIxLjA1OSA4LjU1MjctMjEuMDU5IDEzLjk4NiAwIDExLjEyIDMzLjYwMSAyMC4xMzYgNzUuMDUgMjAuMTM2czc1LjA0OS05LjAxNjEgNzUuMDQ5LTIwLjEzNmMwLTUuNDMzNy04LjAyLTEwLjM2NC0yMS4wNTktMTMuOTg2IDAuMDQtMC4wMyAwLjA3ODgtMC4wNiAwLjExNzUtMC4wOTEgMTMuMTYxIDMuNjQxMSAyMS4yNjIgOC42MDUgMjEuMjYyIDE0LjA3OCAwIDExLjE2OS0zMy43NDQgMjAuMjIxLTc1LjM3IDIwLjIyMW0zMS42NDEtMzguNDg1YzAuMDUxMy0wLjAyNSAwLjEwMzc1LTAuMDUgMC4xNTYyNS0wLjA3NS0wLjA1MjUgMC4wMjUtMC4xMDUgMC4wNS0wLjE1NjI1IDAuMDc1IiBmaWxsPSIjZmJmYmZiIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTIzMSIgZD0ibTMwNS41OSA5NDguMDhjLTQxLjQ0OSAwLTc1LjA1LTkuMDE2MS03NS4wNS0yMC4xMzYgMC01LjQzMzYgOC4wMjEyLTEwLjM2NCAyMS4wNTktMTMuOTg2IDAuMDQgMC4wMyAwLjA3ODggMC4wNjIgMC4xMTg3NSAwLjA5MS0xMi45MTYgMy42MDQtMjAuODU4IDguNS0yMC44NTggMTMuODk1IDAgMTEuMDcyIDMzLjQ1OCAyMC4wNSA3NC43MyAyMC4wNSA0MS4yNzEgMCA3NC43MjktOC45Nzc1IDc0LjcyOS0yMC4wNSAwLTUuMzk1LTcuOTQtMTAuMjkxLTIwLjg1OC0xMy44OTUgMC4wNC0wLjAzIDAuMDc4Ny0wLjA2MSAwLjExODc1LTAuMDkxIDEzLjAzOSAzLjYyMjcgMjEuMDU5IDguNTUyOCAyMS4wNTkgMTMuOTg2IDAgMTEuMTItMzMuNiAyMC4xMzYtNzUuMDQ5IDIwLjEzNm0zMS40ODQtMzguMzI1bDAuMTU3NS0wLjA3NS0wLjE1NzUgMC4wNzUiIGZpbGw9IiNmYWZiZmIiLz4KICAgPHBhdGggaWQ9InBhdGg1MjMzIiBkPSJtMzA1LjU5IDk0OGMtNDEuMjczIDAtNzQuNzMtOC45Nzc1LTc0LjczLTIwLjA1IDAtNS4zOTUgNy45NDEyLTEwLjI5MSAyMC44NTgtMTMuODk1IDAuMDQgMC4wMyAwLjA3ODggMC4wNjIgMC4xMTg3NSAwLjA5MS0xMi43OTIgMy41ODUtMjAuNjU1IDguNDQ4OC0yMC42NTUgMTMuODA0IDAgMTEuMDI1IDMzLjMxNCAxOS45NjQgNzQuNDA5IDE5Ljk2NCA0MS4wOTQgMCA3NC40MDgtOC45MzkgNzQuNDA4LTE5Ljk2NCAwLTUuMzU1LTcuODYxMi0xMC4yMTktMjAuNjU0LTEzLjgwNCAwLjAzODctMC4wMyAwLjA3ODctMC4wNjIgMC4xMTc1LTAuMDkxIDEyLjkxOCAzLjYwNCAyMC44NTggOC41IDIwLjg1OCAxMy44OTUgMCAxMS4wNzItMzMuNDU4IDIwLjA1LTc0LjcyOSAyMC4wNW0zMS4zMjYtMzguMTYyYzAuMDUyNS0wLjAyNiAwLjEwNS0wLjA1MSAwLjE1NzUtMC4wNzctMC4wNTI1IDAuMDI1LTAuMTA1IDAuMDUxLTAuMTU3NSAwLjA3NyIgZmlsbD0iI2ZhZmJmYiIvPgogICA8cGF0aCBpZD0icGF0aDUyMzUiIGQ9Im0zMDUuNTkgOTQ3LjkxYy00MS4wOTUgMC03NC40MDktOC45MzktNzQuNDA5LTE5Ljk2NCAwLTUuMzU1IDcuODYyNS0xMC4yMTkgMjAuNjU1LTEzLjgwNCAwLjA0IDAuMDMgMC4wNzg4IDAuMDYxIDAuMTE4NzUgMC4wOTEtMTIuNjcyIDMuNTY1OC0yMC40NTIgOC4zOTYtMjAuNDUyIDEzLjcxMiAwIDEwLjk3OCAzMy4xNyAxOS44NzggNzQuMDg4IDE5Ljg3OHM3NC4wODgtOC45IDc0LjA4OC0xOS44NzhjMC01LjMxNjMtNy43ODEyLTEwLjE0Ni0yMC40NTQtMTMuNzEyIDAuMDQtMC4wMyAwLjA4LTAuMDYyIDAuMTItMC4wOTEgMTIuNzkyIDMuNTg1IDIwLjY1NCA4LjQ0ODcgMjAuNjU0IDEzLjgwNCAwIDExLjAyNS0zMy4zMTQgMTkuOTY0LTc0LjQwOCAxOS45NjRtMzEuMTY4LTM4LjAwMmMwLjA1MjUtMC4wMjUgMC4xMDYyNS0wLjA1MSAwLjE1ODc1LTAuMDc1LTAuMDUyNSAwLjAyMy0wLjEwNjI1IDAuMDUtMC4xNTg3NSAwLjA3NSIgZmlsbD0iI2ZhZmFmYSIvPgogICA8cGF0aCBpZD0icGF0aDUyMzciIGQ9Im0zMDUuNTkgOTQ3LjgyYy00MC45MTggMC03NC4wODgtOC45LTc0LjA4OC0xOS44NzggMC01LjMxNjQgNy43OC0xMC4xNDYgMjAuNDUyLTEzLjcxMiAwLjA0IDAuMDMgMC4wOCAwLjA2MSAwLjEyIDAuMDkxLTEyLjU1IDMuNTQ3NC0yMC4yNTIgOC4zNDM4LTIwLjI1MiAxMy42MjEgMCAxMC45MyAzMy4wMjggMTkuNzkxIDczLjc2OCAxOS43OTFzNzMuNzY2LTguODYwOSA3My43NjYtMTkuNzkxYzAtNS4yNzc0LTcuNzAxMi0xMC4wNzQtMjAuMjUxLTEzLjYyMSAwLjAzODctMC4wMyAwLjA3ODgtMC4wNjIgMC4xMTg3NS0wLjA5MSAxMi42NzIgMy41NjU5IDIwLjQ1NCA4LjM5NiAyMC40NTQgMTMuNzEyIDAgMTAuOTc4LTMzLjE3IDE5Ljg3OC03NC4wODggMTkuODc4bTMxLjAwOS0zNy44NGMwLjA1MzctMC4wMjYgMC4xMDUtMC4wNSAwLjE1ODc1LTAuMDc1LTAuMDUzNyAwLjAyNS0wLjEwNSAwLjA0OS0wLjE1ODc1IDAuMDc1IiBmaWxsPSIjZmFmYWZhIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTIzOSIgZD0ibTMwNS41OSA5NDcuNzRjLTQwLjc0IDAtNzMuNzY4LTguODYwOS03My43NjgtMTkuNzkxIDAtNS4yNzc0IDcuNzAyNS0xMC4wNzQgMjAuMjUyLTEzLjYyMSAwLjA0IDAuMDMgMC4wOCAwLjA2MSAwLjEyIDAuMDkxLTEyLjQzMSAzLjUyODItMjAuMDUxIDguMjkxLTIwLjA1MSAxMy41MyAwIDEwLjg4MiAzMi44ODIgMTkuNzA1IDczLjQ0NiAxOS43MDUgNDAuNTYyIDAgNzMuNDQ1LTguODIyNyA3My40NDUtMTkuNzA1IDAtNS4yMzg3LTcuNjE4OC0xMC4wMDItMjAuMDUtMTMuNTMgMC4wMzg4LTAuMDMgMC4wOC0wLjA2MiAwLjEyLTAuMDkxIDEyLjU1IDMuNTQ3NCAyMC4yNTEgOC4zNDM3IDIwLjI1MSAxMy42MjEgMCAxMC45My0zMy4wMjYgMTkuNzkxLTczLjc2NiAxOS43OTFtMzAuODQ5LTM3LjY3OWMwLjA1MTItMC4wMjUgMC4xMDc1LTAuMDUxIDAuMTYtMC4wNzUtMC4wNTI1IDAuMDIzLTAuMTA4NzUgMC4wNS0wLjE2IDAuMDc1IiBmaWxsPSIjZjlmYWZhIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTI0MSIgZD0ibTMwNS41OSA5NDcuNjVjLTQwLjU2NCAwLTczLjQ0Ni04LjgyMjctNzMuNDQ2LTE5LjcwNSAwLTUuMjM4NyA3LjYyLTEwLjAwMiAyMC4wNTEtMTMuNTMgMC4wNCAwLjAzIDAuMDggMC4wNjEgMC4xMiAwLjA5MS0xMi4zMSAzLjUxMDItMTkuODUxIDguMjM4Ny0xOS44NTEgMTMuNDM5IDAgMTAuODM1IDMyLjc0IDE5LjYxOSA3My4xMjYgMTkuNjE5IDQwLjM4NSAwIDczLjEyNS04Ljc4MzYgNzMuMTI1LTE5LjYxOSAwLTUuMjAwMy03LjU0LTkuOTI4OC0xOS44NTEtMTMuNDM5IDAuMDQtMC4wMyAwLjA4MTMtMC4wNjEgMC4xMjEyNS0wLjA5MSAxMi40MzEgMy41MjgyIDIwLjA1IDguMjkxIDIwLjA1IDEzLjUzIDAgMTAuODgyLTMyLjg4MiAxOS43MDUtNzMuNDQ1IDE5LjcwNW0zMC42ODgtMzcuNTE4YzAuMDU1LTAuMDI2IDAuMTA2MjUtMC4wNSAwLjE2MTI1LTAuMDc1LTAuMDU1IDAuMDI2LTAuMTA2MjUgMC4wNDktMC4xNjEyNSAwLjA3NSIgZmlsbD0iI2Y5ZjlmOSIvPgogICA8cGF0aCBpZD0icGF0aDUyNDMiIGQ9Im0zMDUuNTkgOTQ3LjU3Yy00MC4zODYgMC03My4xMjYtOC43ODM3LTczLjEyNi0xOS42MTkgMC01LjIwMDIgNy41NDEyLTkuOTI4NyAxOS44NTEtMTMuNDM5IDAuMDQgMC4wMyAwLjA4MTMgMC4wNjIgMC4xMjEyNSAwLjA5MS0xMi4xODggMy40OTExLTE5LjY1MSA4LjE4NjUtMTkuNjUxIDEzLjM0OCAwIDEwLjc4OCAzMi41OTYgMTkuNTM0IDcyLjgwNSAxOS41MzRzNzIuODA0LTguNzQ2IDcyLjgwNC0xOS41MzRjMC01LjE2MTEtNy40NjM4LTkuODU2NS0xOS42NS0xMy4zNDggMC4wNC0wLjAzIDAuMDgtMC4wNjEgMC4xMi0wLjA5MSAxMi4zMTEgMy41MTAzIDE5Ljg1MSA4LjIzODggMTkuODUxIDEzLjQzOSAwIDEwLjgzNS0zMi43NCAxOS42MTktNzMuMTI1IDE5LjYxOW0zMC41MjYtMzcuMzU3YzAuMDUzOC0wLjAyNSAwLjEwNjI1LTAuMDQ5IDAuMTYxMjUtMC4wNzQtMC4wNTUgMC4wMjUtMC4xMDc1IDAuMDQ5LTAuMTYxMjUgMC4wNzQiIGZpbGw9IiNmOWY5ZjkiLz4KICAgPHBhdGggaWQ9InBhdGg1MjQ1IiBkPSJtMzA1LjU5IDk0Ny40OGMtNDAuMjA5IDAtNzIuODA1LTguNzQ2MS03Mi44MDUtMTkuNTM0IDAtNS4xNjExIDcuNDYzOC05Ljg1NjUgMTkuNjUxLTEzLjM0OCAwLjA0MTIgMC4wMzEgMC4wNzg4IDAuMDU5IDAuMTIxMjUgMC4wOTEtMTIuMDY4IDMuNDcyNi0xOS40NTEgOC4xMzM5LTE5LjQ1MSAxMy4yNTYgMCAxMC43NCAzMi40NTEgMTkuNDQ4IDcyLjQ4NCAxOS40NDggNDAuMDMxIDAgNzIuNDg0LTguNzA3NSA3Mi40ODQtMTkuNDQ4IDAtNS4xMjI1LTcuMzgzOC05Ljc4MzgtMTkuNDUyLTEzLjI1NiAwLjA0MjUtMC4wMzIgMC4wOC0wLjA2IDAuMTIyNS0wLjA5MSAxMi4xODYgMy40OTExIDE5LjY1IDguMTg2NSAxOS42NSAxMy4zNDggMCAxMC43ODgtMzIuNTk1IDE5LjUzNC03Mi44MDQgMTkuNTM0bTMwLjM2Mi0zNy4xOTdjMC4wNTUtMC4wMjUgMC4xMDg3NS0wLjA1IDAuMTYzNzUtMC4wNzUtMC4wNTUgMC4wMjUtMC4xMDg3NSAwLjA1LTAuMTYzNzUgMC4wNzUiIGZpbGw9IiNmOGY5ZjkiLz4KICAgPHBhdGggaWQ9InBhdGg1MjQ3IiBkPSJtMzA1LjU5IDk0Ny4zOWMtNDAuMDMzIDAtNzIuNDg0LTguNzA3NS03Mi40ODQtMTkuNDQ4IDAtNS4xMjI1IDcuMzgzOC05Ljc4MzcgMTkuNDUxLTEzLjI1NiAwLjA0IDAuMDMgMC4wODEzIDAuMDYyIDAuMTIxMjUgMC4wOTEtMTEuOTQ5IDMuNDUzNy0xOS4yNTIgOC4wOC0xOS4yNTIgMTMuMTY1IDAgMTAuNjkyIDMyLjMwOSAxOS4zNjEgNzIuMTY0IDE5LjM2MSAzOS44NTQgMCA3Mi4xNjItOC42NjkgNzIuMTYyLTE5LjM2MSAwLTUuMDg1LTcuMzAzOC05LjcxMTMtMTkuMjUyLTEzLjE2NSAwLjA0LTAuMDMgMC4wODI1LTAuMDYxIDAuMTIxMjUtMC4wOTEgMTIuMDY5IDMuNDcyNyAxOS40NTIgOC4xMzM5IDE5LjQ1MiAxMy4yNTYgMCAxMC43NC0zMi40NTIgMTkuNDQ4LTcyLjQ4NCAxOS40NDhtMzAuMi0zNy4wMzdjMC4wNTI1LTAuMDIzIDAuMTEtMC4wNTEgMC4xNjI1LTAuMDc1LTAuMDUyNSAwLjAyNC0wLjExIDAuMDUxLTAuMTYyNSAwLjA3NSIgZmlsbD0iI2Y4ZjhmOCIvPgogICA8cGF0aCBpZD0icGF0aDUyNDkiIGQ9Im0zMDUuNTkgOTQ3LjMxYy0zOS44NTUgMC03Mi4xNjQtOC42Njg5LTcyLjE2NC0xOS4zNjEgMC01LjA4NSA3LjMwMzgtOS43MTE0IDE5LjI1Mi0xMy4xNjUgMC4wNCAwLjAyOSAwLjA4MjUgMC4wNjIgMC4xMjI1IDAuMDkxLTExLjgyOCAzLjQzNTEtMTkuMDU0IDguMDI3NC0xOS4wNTQgMTMuMDc0IDAgMTAuNjQ1IDMyLjE2NSAxOS4yNzUgNzEuODQzIDE5LjI3NXM3MS44NDEtOC42Mjk5IDcxLjg0MS0xOS4yNzVjMC01LjA0NjQtNy4yMjUtOS42Mzg3LTE5LjA1NC0xMy4wNzQgMC4wNC0wLjAzIDAuMDgyNS0wLjA2MiAwLjEyMjUtMC4wOTEgMTEuOTQ5IDMuNDUzNiAxOS4yNTIgOC4wOCAxOS4yNTIgMTMuMTY1IDAgMTAuNjkyLTMyLjMwOSAxOS4zNjEtNzIuMTYyIDE5LjM2MW0zMC4wMzUtMzYuODc2YzAuMDU1LTAuMDI0IDAuMTEtMC4wNDkgMC4xNjUtMC4wNzQtMC4wNTUgMC4wMjUtMC4xMSAwLjA1LTAuMTY1IDAuMDc0IiBmaWxsPSIjZjhmOGY4Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTI1MSIgZD0ibTMwNS41OSA5NDcuMjJjLTM5LjY3OCAwLTcxLjg0My04LjYyOTktNzEuODQzLTE5LjI3NSAwLTUuMDQ2NCA3LjIyNjItOS42Mzg2IDE5LjA1NC0xMy4wNzQgMC4wNDI1IDAuMDMxIDAuMDgxMiAwLjA1OSAwLjEyMjUgMC4wOTEtMTEuNzA4IDMuNDE2LTE4Ljg1NSA3Ljk3NS0xOC44NTUgMTIuOTgyIDAgMTAuNTk4IDMyLjAyMSAxOS4xODkgNzEuNTIxIDE5LjE4OXM3MS41MjEtOC41OTEzIDcxLjUyMS0xOS4xODljMC01LjAwNzMtNy4xNDg4LTkuNTY2My0xOC44NTYtMTIuOTgyIDAuMDQyNS0wLjAzMyAwLjA4MTMtMC4wNiAwLjEyMjUtMC4wOTEgMTEuODI5IDMuNDM1MSAxOS4wNTQgOC4wMjczIDE5LjA1NCAxMy4wNzQgMCAxMC42NDUtMzIuMTY0IDE5LjI3NS03MS44NDEgMTkuMjc1bTI5Ljg3LTM2LjcxNWMwLjA1NzUtMC4wMjYgMC4xMDg3NS0wLjA0OSAwLjE2NS0wLjA3NS0wLjA1NjMgMC4wMjYtMC4xMDc1IDAuMDQ5LTAuMTY1IDAuMDc1IiBmaWxsPSIjZjdmOGY4Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTI1MyIgZD0ibTMwNS41OSA5NDcuMTRjLTM5LjUgMC03MS41MjEtOC41OTE0LTcxLjUyMS0xOS4xODkgMC01LjAwNzQgNy4xNDc1LTkuNTY2NCAxOC44NTUtMTIuOTgyIDAuMDQgMC4wMjkgMC4wODM3IDAuMDYxIDAuMTIzNzUgMC4wOTEtMTEuNTg5IDMuMzk3NS0xOC42NTkgNy45MjI0LTE4LjY1OSAxMi44OTEgMCAxMC41NSAzMS44NzggMTkuMTAyIDcxLjIwMSAxOS4xMDIgMzkuMzIyIDAgNzEuMi04LjU1MjggNzEuMi0xOS4xMDIgMC00Ljk2ODgtNy4wNy05LjQ5MzctMTguNjU4LTEyLjg5MSAwLjA0LTAuMDMgMC4wODI1LTAuMDYyIDAuMTIyNS0wLjA5MSAxMS43MDggMy40MTYgMTguODU2IDcuOTc1IDE4Ljg1NiAxMi45ODIgMCAxMC41OTgtMzIuMDIxIDE5LjE4OS03MS41MjEgMTkuMTg5bTI5LjcwNC0zNi41NTVjMC4wNTUtMC4wMjQgMC4xMTEyNS0wLjA0OSAwLjE2NjI1LTAuMDc0LTAuMDU1IDAuMDI1LTAuMTExMjUgMC4wNS0wLjE2NjI1IDAuMDc0IiBmaWxsPSIjZjdmN2Y3Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTI1NSIgZD0ibTMwNS41OSA5NDcuMDVjLTM5LjMyNCAwLTcxLjIwMS04LjU1MjgtNzEuMjAxLTE5LjEwMyAwLTQuOTY4NyA3LjA3LTkuNDkzNiAxOC42NTktMTIuODkxIDAuMDQyNSAwLjAzMSAwLjA4MTMgMC4wNiAwLjEyMzc1IDAuMDkxLTExLjQ3IDMuMzc4NC0xOC40NjEgNy44Njk2LTE4LjQ2MSAxMi44IDAgMTAuNTAyIDMxLjczNCAxOS4wMTYgNzAuODggMTkuMDE2czcwLjg4LTguNTEzNyA3MC44OC0xOS4wMTZjMC00LjkzMDEtNi45OTI1LTkuNDIxMy0xOC40NjEtMTIuOCAwLjA0MjUtMC4wMzEgMC4wODEzLTAuMDYgMC4xMjM3NS0wLjA5MSAxMS41ODggMy4zOTc1IDE4LjY1OCA3LjkyMjQgMTguNjU4IDEyLjg5MSAwIDEwLjU1LTMxLjg3OCAxOS4xMDMtNzEuMiAxOS4xMDNtMjkuNTM4LTM2LjM5NGMwLjA1NS0wLjAyNSAwLjExMTI1LTAuMDUgMC4xNjYyNS0wLjA3NS0wLjA1NSAwLjAyNS0wLjExMTI1IDAuMDUtMC4xNjYyNSAwLjA3NSIgZmlsbD0iI2Y2ZjdmNyIvPgogICA8cGF0aCBpZD0icGF0aDUyNTciIGQ9Im0zMDUuNTkgOTQ2Ljk2Yy0zOS4xNDYgMC03MC44OC04LjUxMzYtNzAuODgtMTkuMDE2IDAtNC45MzAxIDYuOTkxMi05LjQyMTQgMTguNDYxLTEyLjggMC4wNCAwLjAyOCAwLjA4MzcgMC4wNjEgMC4xMjM3NSAwLjA5MS0xMS4zNDkgMy4zNTg4LTE4LjI2NSA3LjgxNzgtMTguMjY1IDEyLjcwOSAwIDEwLjQ1NSAzMS41OTEgMTguOTMxIDcwLjU2IDE4LjkzMXM3MC41NTktOC40NzYgNzAuNTU5LTE4LjkzMWMwLTQuODkxMi02LjkxNS05LjM1MDItMTguMjY1LTEyLjcwOSAwLjA0MTItMC4wMyAwLjA4NS0wLjA2MiAwLjEyNS0wLjA5MSAxMS40NjkgMy4zNzgzIDE4LjQ2MSA3Ljg2OTYgMTguNDYxIDEyLjggMCAxMC41MDItMzEuNzM0IDE5LjAxNi03MC44OCAxOS4wMTZtMjkuMzctMzYuMjMzYzAuMDU2My0wLjAyNSAwLjExLTAuMDQ5IDAuMTY3NS0wLjA3NC0wLjA1NzUgMC4wMjUtMC4xMTEyNSAwLjA0OS0wLjE2NzUgMC4wNzQiIGZpbGw9IiNmNmY2ZjciLz4KICAgPHBhdGggaWQ9InBhdGg1MjU5IiBkPSJtMzA1LjU5IDk0Ni44OGMtMzguOTY5IDAtNzAuNTYtOC40NzYxLTcwLjU2LTE4LjkzMSAwLTQuODkxMSA2LjkxNjItOS4zNTAxIDE4LjI2NS0xMi43MDkgMC4wNDI1IDAuMDMxIDAuMDgyNSAwLjA2IDAuMTI1IDAuMDkxLTExLjIzMiAzLjM0MDMtMTguMDY5IDcuNzY1MS0xOC4wNjkgMTIuNjE4IDAgMTAuNDA4IDMxLjQ0NiAxOC44NDUgNzAuMjM5IDE4Ljg0NSAzOC43OTEgMCA3MC4yMzgtOC40Mzc0IDcwLjIzOC0xOC44NDUgMC00Ljg1MjUtNi44MzYyLTkuMjc3My0xOC4wNjgtMTIuNjE4IDAuMDQyNS0wLjAzMSAwLjA4MTMtMC4wNiAwLjEyMzc1LTAuMDkxIDExLjM1IDMuMzU4OSAxOC4yNjUgNy44MTc5IDE4LjI2NSAxMi43MDkgMCAxMC40NTUtMzEuNTkgMTguOTMxLTcwLjU1OSAxOC45MzFtMjkuMjAxLTM2LjA3NWMwLjA1NS0wLjAyNCAwLjExMzc1LTAuMDUgMC4xNjg3NS0wLjA3NC0wLjA1NSAwLjAyMy0wLjExMzc1IDAuMDUtMC4xNjg3NSAwLjA3NCIgZmlsbD0iI2Y2ZjZmNiIvPgogICA8cGF0aCBpZD0icGF0aDUyNjEiIGQ9Im0zMDUuNTkgOTQ2Ljc5Yy0zOC43OTMgMC03MC4yMzktOC40Mzc0LTcwLjIzOS0xOC44NDUgMC00Ljg1MjUgNi44MzYyLTkuMjc3NCAxOC4wNjktMTIuNjE4IDAuMDQgMC4wMjkgMC4wODM4IDAuMDYyIDAuMTI1IDAuMDkxLTExLjExNCAzLjMyMTMtMTcuODcyIDcuNzExNC0xNy44NzIgMTIuNTI2IDAgMTAuMzYgMzEuMzAyIDE4Ljc1OSA2OS45MTggMTguNzU5IDM4LjYxNCAwIDY5LjkxOC04LjM5OSA2OS45MTgtMTguNzU5IDAtNC44MTUtNi43Ni05LjIwNTEtMTcuODcyLTEyLjUyNiAwLjA0LTAuMDMgMC4wODUtMC4wNjIgMC4xMjUtMC4wOTEgMTEuMjMxIDMuMzQwMiAxOC4wNjggNy43NjUxIDE4LjA2OCAxMi42MTggMCAxMC40MDgtMzEuNDQ2IDE4Ljg0NS03MC4yMzggMTguODQ1bTI5LjAzMS0zNS45MTVjMC4wNTc1LTAuMDI1IDAuMTEyNS0wLjA0OSAwLjE3LTAuMDc0LTAuMDU3NSAwLjAyNS0wLjExMjUgMC4wNDgtMC4xNyAwLjA3NCIgZmlsbD0iI2Y1ZjZmNiIvPgogICA8cGF0aCBpZD0icGF0aDUyNjMiIGQ9Im0zMDUuNTkgOTQ2LjcxYy0zOC42MTUgMC02OS45MTgtOC4zOTg5LTY5LjkxOC0xOC43NTkgMC00LjgxNSA2Ljc1ODgtOS4yMDUxIDE3Ljg3Mi0xMi41MjYgMC4wNDI1IDAuMDMxIDAuMDgyNSAwLjA2IDAuMTI1IDAuMDkxLTEwLjk5NSAzLjMwMjctMTcuNjc4IDcuNjU4Ny0xNy42NzggMTIuNDM1IDAgMTAuMzEyIDMxLjE2IDE4LjY3MiA2OS41OTggMTguNjcyczY5LjU5Ni04LjM1OTkgNjkuNTk2LTE4LjY3MmMwLTQuNzc2My02LjY4MjUtOS4xMzIzLTE3LjY3OC0xMi40MzUgMC4wNDI1LTAuMDMxIDAuMDgzOC0wLjA2IDAuMTI2MjUtMC4wOTEgMTEuMTEyIDMuMzIxMyAxNy44NzIgNy43MTE0IDE3Ljg3MiAxMi41MjYgMCAxMC4zNi0zMS4zMDQgMTguNzU5LTY5LjkxOCAxOC43NTltMjguODYxLTM1Ljc1NWMwLjA1NjItMC4wMjUgMC4xMTI1LTAuMDQ5IDAuMTctMC4wNzQtMC4wNTc1IDAuMDI1LTAuMTEzNzUgMC4wNDgtMC4xNyAwLjA3NCIgZmlsbD0iI2Y1ZjVmNSIvPgogICA8cGF0aCBpZD0icGF0aDUyNjUiIGQ9Im0zMDUuNTkgOTQ2LjYyYy0zOC40MzggMC02OS41OTgtOC4zNTk5LTY5LjU5OC0xOC42NzIgMC00Ljc3NjQgNi42ODI1LTkuMTMyNCAxNy42NzgtMTIuNDM1IDAuMDQyNSAwLjAzMSAwLjA4MzggMC4wNiAwLjEyNjI1IDAuMDkxLTEwLjg3OCAzLjI4MjgtMTcuNDgyIDcuNjA2NS0xNy40ODIgMTIuMzQ0IDAgMTAuMjY1IDMxLjAxNiAxOC41ODYgNjkuMjc2IDE4LjU4NnM2OS4yNzYtOC4zMjEzIDY5LjI3Ni0xOC41ODZjMC00LjczNzMtNi42MDYyLTkuMDYxLTE3LjQ4NC0xMi4zNDQgMC4wNDM3LTAuMDMxIDAuMDgzOC0wLjA2IDAuMTI2MjUtMC4wOTEgMTAuOTk1IDMuMzAyNiAxNy42NzggNy42NTg2IDE3LjY3OCAxMi40MzUgMCAxMC4zMTItMzEuMTU5IDE4LjY3Mi02OS41OTYgMTguNjcybTI4LjY4OS0zNS41OTVjMC4wNTUtMC4wMjQgMC4xMTYyNS0wLjA1IDAuMTcyNS0wLjA3NC0wLjA1NjMgMC4wMjQtMC4xMTc1IDAuMDUtMC4xNzI1IDAuMDc0IiBmaWxsPSIjZjVmNWY1Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTI2NyIgZD0ibTMwNS41OSA5NDYuNTNjLTM4LjI2IDAtNjkuMjc2LTguMzIxMy02OS4yNzYtMTguNTg2IDAtNC43MzcyIDYuNjA1LTkuMDYxIDE3LjQ4Mi0xMi4zNDQgMC4wNDI1IDAuMDMxIDAuMDgzNyAwLjA2MSAwLjEyNjI1IDAuMDkxLTEwLjc1OCAzLjI2MzctMTcuMjg4IDcuNTUzNy0xNy4yODggMTIuMjUyIDAgMTAuMjE3IDMwLjg3MSAxOC41IDY4Ljk1NSAxOC41IDM4LjA4MiAwIDY4Ljk1NS04LjI4MjggNjguOTU1LTE4LjUgMC00LjY5ODgtNi41MzEyLTguOTg4OC0xNy4yODktMTIuMjUyIDAuMDQyNS0wLjAzIDAuMDgzOC0wLjA2IDAuMTI2MjUtMC4wOTEgMTAuODc4IDMuMjgyNyAxNy40ODQgNy42MDY1IDE3LjQ4NCAxMi4zNDQgMCAxMC4yNjUtMzEuMDE2IDE4LjU4Ni02OS4yNzYgMTguNTg2bTI4LjUxNi0zNS40MzVjMC4wNTc1LTAuMDI1IDAuMTE1LTAuMDQ5IDAuMTcyNS0wLjA3NC0wLjA1NzUgMC4wMjUtMC4xMTUgMC4wNDktMC4xNzI1IDAuMDc0IiBmaWxsPSIjZjRmNGY0Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTI2OSIgZD0ibTMwNS41OSA5NDYuNDVjLTM4LjA4NCAwLTY4Ljk1NS04LjI4MjctNjguOTU1LTE4LjUgMC00LjY5ODcgNi41My04Ljk4ODcgMTcuMjg4LTEyLjI1MiAwLjA0MTIgMC4wMyAwLjA4NjMgMC4wNjIgMC4xMjc1IDAuMDkxLTEwLjY0MiAzLjI0MzYtMTcuMDk1IDcuNS0xNy4wOTUgMTIuMTYxIDAgMTAuMTcgMzAuNzI5IDE4LjQxNSA2OC42MzUgMTguNDE1IDM3LjkwNSAwIDY4LjYzNC04LjI0NTIgNjguNjM0LTE4LjQxNSAwLTQuNjYxMS02LjQ1MjUtOC45MTc1LTE3LjA5NC0xMi4xNjEgMC4wNC0wLjAyOSAwLjA4NjMtMC4wNjIgMC4xMjYyNS0wLjA5MSAxMC43NTggMy4yNjM4IDE3LjI4OSA3LjU1MzggMTcuMjg5IDEyLjI1MiAwIDEwLjIxNy0zMC44NzIgMTguNS02OC45NTUgMTguNW0yOC4zNDQtMzUuMjc2YzAuMDU4OC0wLjAyNSAwLjExMjUtMC4wNDcgMC4xNzI1LTAuMDcyLTAuMDYgMC4wMjUtMC4xMTM3NSAwLjA0Ny0wLjE3MjUgMC4wNzIiIGZpbGw9IiNmNGY0ZjQiLz4KICAgPHBhdGggaWQ9InBhdGg1MjcxIiBkPSJtMzA1LjU5IDk0Ni4zNmMtMzcuOTA2IDAtNjguNjM1LTguMjQ1Mi02OC42MzUtMTguNDE1IDAtNC42NjExIDYuNDUyNS04LjkxNzUgMTcuMDk1LTEyLjE2MSAwLjA0MjUgMC4wMzEgMC4wODUgMC4wNjEgMC4xMjc1IDAuMDkyLTEwLjUyNCAzLjIyMzYtMTYuOTAxIDcuNDQ2NC0xNi45MDEgMTIuMDY5IDAgMTAuMTIyIDMwLjU4NSAxOC4zMjkgNjguMzE0IDE4LjMyOXM2OC4zMTQtOC4yMDYxIDY4LjMxNC0xOC4zMjljMC00LjYyMjUtNi4zNzc1LTguODQ1My0xNi45MDItMTIuMDY5IDAuMDQzNy0wLjAzMSAwLjA4NS0wLjA2MSAwLjEyODc1LTAuMDkyIDEwLjY0MSAzLjI0MzYgMTcuMDk0IDcuNSAxNy4wOTQgMTIuMTYxIDAgMTAuMTctMzAuNzI5IDE4LjQxNS02OC42MzQgMTguNDE1bTI4LjE2OS0zNS4xMThjMC4wNTc1LTAuMDI0IDAuMTE2MjUtMC4wNDkgMC4xNzUtMC4wNzQtMC4wNTg3IDAuMDI1LTAuMTE3NSAwLjA1LTAuMTc1IDAuMDc0IiBmaWxsPSIjZjNmM2YzIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTI3MyIgZD0ibTMwNS41OSA5NDYuMjhjLTM3LjcyOSAwLTY4LjMxNC04LjIwNjEtNjguMzE0LTE4LjMyOSAwLTQuNjIyNSA2LjM3NzUtOC44NDUyIDE2LjkwMS0xMi4wNjkgMC4wNDI1IDAuMDMgMC4wODUgMC4wNiAwLjEyNzUgMC4wOTEtMTAuNDA2IDMuMjA1LTE2LjcwOSA3LjM5MzUtMTYuNzA5IDExLjk3OCAwIDEwLjA3NSAzMC40NDEgMTguMjQzIDY3Ljk5NCAxOC4yNDMgMzcuNTUxIDAgNjcuOTkyLTguMTY3NCA2Ny45OTItMTguMjQzIDAtNC41ODQtNi4zMDEyLTguNzcyNS0xNi43MDktMTEuOTc4IDAuMDQyNS0wLjAzMSAwLjA4NS0wLjA2MSAwLjEyNzUtMC4wOTEgMTAuNTI1IDMuMjIzNyAxNi45MDIgNy40NDY0IDE2LjkwMiAxMi4wNjkgMCAxMC4xMjItMzAuNTg1IDE4LjMyOS02OC4zMTQgMTguMzI5bTI3Ljk5Mi0zNC45NThjMC4wNTg4LTAuMDI1IDAuMTE4NzUtMC4wNSAwLjE3NjI1LTAuMDc0LTAuMDU3NSAwLjAyNC0wLjExNzUgMC4wNDktMC4xNzYyNSAwLjA3NCIgZmlsbD0iI2YzZjNmMiIvPgogICA8cGF0aCBpZD0icGF0aDUyNzUiIGQ9Im0zMDUuNTkgOTQ2LjE5Yy0zNy41NTMgMC02Ny45OTQtOC4xNjc0LTY3Ljk5NC0xOC4yNDMgMC00LjU4NCA2LjMwMjUtOC43NzI1IDE2LjcwOS0xMS45NzggMC4wNDM3IDAuMDMgMC4wODYzIDAuMDYyIDAuMTI4NzUgMC4wOTEtMTAuMjg5IDMuMTg1MS0xNi41MTYgNy4zNDEzLTE2LjUxNiAxMS44ODYgMCAxMC4wMjcgMzAuMjk4IDE4LjE1NiA2Ny42NzMgMTguMTU2IDM3LjM3NCAwIDY3LjY3Mi04LjEyODkgNjcuNjcyLTE4LjE1NiAwLTQuNTQ0OS02LjIyODgtOC43MDExLTE2LjUxOC0xMS44ODYgMC4wNDM3LTAuMDMgMC4wODYyLTAuMDYyIDAuMTI4NzUtMC4wOTEgMTAuNDA4IDMuMjA1IDE2LjcwOSA3LjM5MzUgMTYuNzA5IDExLjk3OCAwIDEwLjA3NS0zMC40NDEgMTguMjQzLTY3Ljk5MiAxOC4yNDNtMjcuODE2LTM0Ljc5OWMwLjA2LTAuMDI1IDAuMTE2MjUtMC4wNDcgMC4xNzYyNS0wLjA3My0wLjA2IDAuMDI1LTAuMTE2MjUgMC4wNDgtMC4xNzYyNSAwLjA3MyIgZmlsbD0iI2YyZjJmMiIvPgogICA8cGF0aCBpZD0icGF0aDUyNzciIGQ9Im0zMDUuNTkgOTQ2LjFjLTM3LjM3NSAwLTY3LjY3My04LjEyODktNjcuNjczLTE4LjE1NiAwLTQuNTQ0OSA2LjIyNzUtOC43MDExIDE2LjUxNi0xMS44ODYgMC4wNDM3IDAuMDMgMC4wODYzIDAuMDYxIDAuMTI4NzUgMC4wOTEtMTAuMTcyIDMuMTY1LTE2LjMyNCA3LjI4NzUtMTYuMzI0IDExLjc5NSAwIDkuOTggMzAuMTU0IDE4LjA3IDY3LjM1MSAxOC4wNyAzNy4xOTggMCA2Ny4zNTEtOC4wODk5IDY3LjM1MS0xOC4wNyAwLTQuNTA3My02LjE1MTItOC42Mjk4LTE2LjMyNS0xMS43OTUgMC4wNDI1LTAuMDMgMC4wODYyLTAuMDYyIDAuMTI4NzUtMC4wOTEgMTAuMjg5IDMuMTg1MSAxNi41MTggNy4zNDEzIDE2LjUxOCAxMS44ODYgMCAxMC4wMjctMzAuMjk5IDE4LjE1Ni02Ny42NzIgMTguMTU2bTI3LjYzOS0zNC42NGMwLjA1ODgtMC4wMjQgMC4xMi0wLjA0OSAwLjE3NzUtMC4wNzItMC4wNTc1IDAuMDIzLTAuMTE4NzUgMC4wNDktMC4xNzc1IDAuMDcyIiBmaWxsPSIjZjJmMWYxIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTI3OSIgZD0ibTMwNS41OSA5NDYuMDJjLTM3LjE5OCAwLTY3LjM1MS04LjA4OTgtNjcuMzUxLTE4LjA3IDAtNC41MDc0IDYuMTUxMi04LjYyOTkgMTYuMzI0LTExLjc5NSAwLjA0MzggMC4wMzEgMC4wODc1IDAuMDYxIDAuMTMgMC4wOTItMTAuMDU2IDMuMTQ1LTE2LjEzNCA3LjIzMzktMTYuMTM0IDExLjcwMyAwIDkuOTMyNiAzMC4wMTEgMTcuOTg0IDY3LjAzMSAxNy45ODRzNjcuMDMtOC4wNTEzIDY3LjAzLTE3Ljk4NGMwLTQuNDY4OC02LjA3NzUtOC41NTc3LTE2LjEzNC0xMS43MDMgMC4wNDM4LTAuMDMxIDAuMDg3NS0wLjA2MSAwLjEzLTAuMDkyIDEwLjE3NCAzLjE2NSAxNi4zMjUgNy4yODc1IDE2LjMyNSAxMS43OTUgMCA5Ljk4LTMwLjE1NCAxOC4wNy02Ny4zNTEgMTguMDdtMjcuNDYtMzQuNDhjMC4wNi0wLjAyNSAwLjExODc1LTAuMDQ5IDAuMTc4NzUtMC4wNzQtMC4wNiAwLjAyNS0wLjExODc1IDAuMDQ5LTAuMTc4NzUgMC4wNzQiIGZpbGw9IiNmMWYxZjEiLz4KICAgPHBhdGggaWQ9InBhdGg1MjgxIiBkPSJtMzA1LjU5IDk0NS45M2MtMzcuMDIgMC02Ny4wMzEtOC4wNTEzLTY3LjAzMS0xNy45ODQgMC00LjQ2ODcgNi4wNzc1LTguNTU3NiAxNi4xMzQtMTEuNzAzIDAuMDQzOCAwLjAzIDAuMDg3NSAwLjA2MiAwLjEzIDAuMDkxLTkuOTQgMy4xMjUtMTUuOTQyIDcuMTgwMi0xNS45NDIgMTEuNjExIDAgOS44ODQ3IDI5Ljg2NiAxNy44OTcgNjYuNzEgMTcuODk3IDM2Ljg0MiAwIDY2LjcxLTguMDEyNyA2Ni43MS0xNy44OTcgMC00LjQzMTItNi4wMDM4LTguNDg2NC0xNS45NDQtMTEuNjExIDAuMDQzOC0wLjAzIDAuMDg3NS0wLjA2MSAwLjEzLTAuMDkxIDEwLjA1NiAzLjE0NSAxNi4xMzQgNy4yMzM5IDE2LjEzNCAxMS43MDMgMCA5LjkzMjYtMzAuMDEgMTcuOTg0LTY3LjAzIDE3Ljk4NG0yNy4yODEtMzQuMzIxYzAuMDYtMC4wMjUgMC4xMTg3NS0wLjA0OSAwLjE3ODc1LTAuMDczLTAuMDYgMC4wMjQtMC4xMTg3NSAwLjA0OC0wLjE3ODc1IDAuMDczIiBmaWxsPSIjZjBmMGYwIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTI4MyIgZD0ibTMwNS41OSA5NDUuODRjLTM2Ljg0NCAwLTY2LjcxLTguMDEyNy02Ni43MS0xNy44OTggMC00LjQzMTEgNi4wMDI1LTguNDg2NCAxNS45NDItMTEuNjExIDAuMDQzNyAwLjAzIDAuMDg3NSAwLjA2MiAwLjEzMTI1IDAuMDkxLTkuODI1IDMuMTA2NC0xNS43NTQgNy4xMjc0LTE1Ljc1NCAxMS41MiAwIDkuODM3NCAyOS43MjQgMTcuODEyIDY2LjM5IDE3LjgxMiAzNi42NjUgMCA2Ni4zODktNy45NzUxIDY2LjM4OS0xNy44MTIgMC00LjM5MjYtNS45Mjc1LTguNDEzNi0xNS43NTItMTEuNTIgMC4wNDI1LTAuMDMgMC4wODc1LTAuMDYxIDAuMTMtMC4wOTEgOS45NCAzLjEyNSAxNS45NDQgNy4xODAzIDE1Ljk0NCAxMS42MTEgMCA5Ljg4NDgtMjkuODY4IDE3Ljg5OC02Ni43MSAxNy44OThtMjcuMS0zNC4xNjNjMC4wNi0wLjAyNCAwLjEyLTAuMDQ5IDAuMTgxMjUtMC4wNzItMC4wNjEyIDAuMDIzLTAuMTIxMjUgMC4wNDktMC4xODEyNSAwLjA3MiIgZmlsbD0iI2YwZjBlZiIvPgogICA8cGF0aCBpZD0icGF0aDUyODUiIGQ9Im0zMDUuNTkgOTQ1Ljc2Yy0zNi42NjYgMC02Ni4zOS03Ljk3NTEtNjYuMzktMTcuODEyIDAtNC4zOTI2IDUuOTI4OC04LjQxMzYgMTUuNzU0LTExLjUyIDAuMDQ1IDAuMDMxIDAuMDg1IDAuMDYgMC4xMzEyNSAwLjA5MS05LjcxMTIgMy4wODY1LTE1LjU2NCA3LjA3MzgtMTUuNTY0IDExLjQyOSAwIDkuNzkgMjkuNTggMTcuNzI2IDY2LjA2OSAxNy43MjZzNjYuMDY4LTcuOTM2IDY2LjA2OC0xNy43MjZjMC00LjM1NS01Ljg1MjUtOC4zNDIzLTE1LjU2Mi0xMS40MjkgMC4wNDUtMC4wMzEgMC4wODUtMC4wNiAwLjEzMTI1LTAuMDkxIDkuODI1IDMuMTA2NCAxNS43NTIgNy4xMjc0IDE1Ljc1MiAxMS41MiAwIDkuODM3NC0yOS43MjQgMTcuODEyLTY2LjM4OSAxNy44MTJtMjYuOTE4LTM0LjAwNWwwLjE4MjUtMC4wNzNjLTAuMDYgMC4wMjQtMC4xMjEyNSAwLjA0OS0wLjE4MjUgMC4wNzMiIGZpbGw9IiNlZmVmZWYiLz4KICAgPHBhdGggaWQ9InBhdGg1Mjg3IiBkPSJtMzA1LjU5IDk0NS42N2MtMzYuNDg5IDAtNjYuMDY5LTcuOTM2MS02Ni4wNjktMTcuNzI2IDAtNC4zNTUgNS44NTI1LTguMzQyMiAxNS41NjQtMTEuNDI5IDAuMDQyNSAwLjAzIDAuMDg3NSAwLjA2MiAwLjEzMTI1IDAuMDkyLTkuNTkzOCAzLjA2NDgtMTUuMzc0IDcuMDItMTUuMzc0IDExLjMzNiAwIDkuNzQyNyAyOS40MzYgMTcuNjQgNjUuNzQ4IDE3LjY0IDM2LjMxMSAwIDY1Ljc0OC03Ljg5NzUgNjUuNzQ4LTE3LjY0IDAtNC4zMTYzLTUuNzgtOC4yNzE1LTE1LjM3NS0xMS4zMzYgMC4wNDM3LTAuMDMgMC4wODg3LTAuMDYyIDAuMTMyNS0wLjA5MiA5LjcxIDMuMDg2NSAxNS41NjIgNy4wNzM3IDE1LjU2MiAxMS40MjkgMCA5Ljc5LTI5LjU3OSAxNy43MjYtNjYuMDY4IDE3LjcyNm0yNi43MzUtMzMuODQ2YzAuMDYtMC4wMjMgMC4xMjI1LTAuMDQ5IDAuMTgyNS0wLjA3Mi0wLjA2IDAuMDIzLTAuMTIyNSAwLjA0OS0wLjE4MjUgMC4wNzIiIGZpbGw9IiNlZWUiLz4KICAgPHBhdGggaWQ9InBhdGg1Mjg5IiBkPSJtMzA1LjU5IDk0NS41OWMtMzYuMzExIDAtNjUuNzQ4LTcuODk3Ni02NS43NDgtMTcuNjQgMC00LjMxNjQgNS43OC04LjI3MTUgMTUuMzc0LTExLjMzNiAwLjA0MzggMC4wMyAwLjA4ODggMC4wNjIgMC4xMzI1IDAuMDkxLTkuNDgxMiAzLjA0NjUtMTUuMTg2IDYuOTY2NS0xNS4xODYgMTEuMjQ1IDAgOS42OTQ5IDI5LjI5MiAxNy41NTQgNjUuNDI4IDE3LjU1NCAzNi4xMzQgMCA2NS40MjYtNy44NTg4IDY1LjQyNi0xNy41NTQgMC00LjI3ODctNS43MDM4LTguMTk4Ny0xNS4xODUtMTEuMjQ1IDAuMDQyNS0wLjAzIDAuMDg4OC0wLjA2MSAwLjEzMTI1LTAuMDkxIDkuNTk1IDMuMDY0OSAxNS4zNzUgNy4wMiAxNS4zNzUgMTEuMzM2IDAgOS43NDI2LTI5LjQzNiAxNy42NC02NS43NDggMTcuNjRtMjYuNTUxLTMzLjY4OGMwLjA2MjUtMC4wMjUgMC4xMjEyNS0wLjA0OCAwLjE4Mzc1LTAuMDczLTAuMDYyNSAwLjAyNS0wLjEyMTI1IDAuMDQ4LTAuMTgzNzUgMC4wNzMiIGZpbGw9IiNlZWVlZWQiLz4KICAgPHBhdGggaWQ9InBhdGg1MjkxIiBkPSJtMzA1LjU5IDk0NS41Yy0zNi4xMzUgMC02NS40MjgtNy44NTg4LTY1LjQyOC0xNy41NTQgMC00LjI3ODcgNS43MDUtOC4xOTg3IDE1LjE4Ni0xMS4yNDUgMC4wNDUgMC4wMzEgMC4wODYzIDAuMDYgMC4xMzI1IDAuMDkxLTkuMzY3NSAzLjAyNjItMTQuOTk4IDYuOTEzNS0xNC45OTggMTEuMTU0IDAgOS42NDc1IDI5LjE0OSAxNy40NjcgNjUuMTA2IDE3LjQ2NyAzNS45NTYgMCA2NS4xMDYtNy44MTk4IDY1LjEwNi0xNy40NjcgMC00LjI0MDItNS42MzEyLTguMTI3NS0xNC45OTktMTEuMTU0IDAuMDQ2My0wLjAzMSAwLjA4NzUtMC4wNiAwLjEzMzc1LTAuMDkxIDkuNDgxMiAzLjA0NjQgMTUuMTg1IDYuOTY2NCAxNS4xODUgMTEuMjQ1IDAgOS42OTQ5LTI5LjI5MiAxNy41NTQtNjUuNDI2IDE3LjU1NG0yNi4zNjUtMzMuNTNjMC4wNjEzLTAuMDI0IDAuMTI1LTAuMDQ5IDAuMTg2MjUtMC4wNzEtMC4wNjEzIDAuMDIzLTAuMTI1IDAuMDQ3LTAuMTg2MjUgMC4wNzEiIGZpbGw9IiNlZGVkZWMiLz4KICAgPHBhdGggaWQ9InBhdGg1MjkzIiBkPSJtMzA1LjU5IDk0NS40MWMtMzUuOTU4IDAtNjUuMTA2LTcuODE5OC02NS4xMDYtMTcuNDY3IDAtNC4yNDAyIDUuNjMtOC4xMjc1IDE0Ljk5OC0xMS4xNTQgMC4wNDM3IDAuMDMgMC4wOSAwLjA2MiAwLjEzMzc1IDAuMDkyLTkuMjUxMiAzLjAwNTMtMTQuODExIDYuODU4OC0xNC44MTEgMTEuMDYyIDAgOS42MDAxIDI5LjAwNiAxNy4zODEgNjQuNzg2IDE3LjM4MXM2NC43ODUtNy43ODEyIDY0Ljc4NS0xNy4zODFjMC00LjIwMjctNS41NTg4LTguMDU2Mi0xNC44MS0xMS4wNjIgMC4wNDI1LTAuMDMgMC4wOS0wLjA2MiAwLjEzMjUtMC4wOTIgOS4zNjc1IDMuMDI2MiAxNC45OTkgNi45MTM1IDE0Ljk5OSAxMS4xNTQgMCA5LjY0NzUtMjkuMTUgMTcuNDY3LTY1LjEwNiAxNy40NjdtMjYuMTc5LTMzLjM3MWMwLjA2MjUtMC4wMjQgMC4xMjM3NS0wLjA0NyAwLjE4NjI1LTAuMDcyLTAuMDYyNSAwLjAyNS0wLjEyMzc1IDAuMDQ4LTAuMTg2MjUgMC4wNzIiIGZpbGw9IiNlZGVkZWMiLz4KICAgPHBhdGggaWQ9InBhdGg1Mjk1IiBkPSJtMzA1LjU5IDk0NS4zM2MtMzUuNzggMC02NC43ODYtNy43ODEzLTY0Ljc4Ni0xNy4zODEgMC00LjIwMjYgNS41Ni04LjA1NjEgMTQuODExLTExLjA2MiAwLjA0NSAwLjAzMiAwLjA4NzUgMC4wNiAwLjEzMzc1IDAuMDkxLTkuMTM4OCAyLjk4NTQtMTQuNjI0IDYuODA2Ni0xNC42MjQgMTAuOTcgMCA5LjU1MjcgMjguODYyIDE3LjI5NSA2NC40NjUgMTcuMjk1IDM1LjYwMiAwIDY0LjQ2NS03Ljc0MjIgNjQuNDY1LTE3LjI5NSAwLTQuMTYzNy01LjQ4NS03Ljk4NDktMTQuNjI0LTEwLjk3IDAuMDQ1LTAuMDMxIDAuMDg3NS0wLjA1OSAwLjEzMzc1LTAuMDkxIDkuMjUxMiAzLjAwNTQgMTQuODEgNi44NTg5IDE0LjgxIDExLjA2MiAwIDkuNjAwMS0yOS4wMDUgMTcuMzgxLTY0Ljc4NSAxNy4zODFtMjUuOTkxLTMzLjIxNGMwLjA2MjUtMC4wMjQgMC4xMjM3NS0wLjA0NyAwLjE4NzUtMC4wNzEtMC4wNjI1IDAuMDI0LTAuMTI1IDAuMDQ3LTAuMTg3NSAwLjA3MSIgZmlsbD0iI2VjZWNlYiIvPgogICA8cGF0aCBpZD0icGF0aDUyOTciIGQ9Im0zMDUuNTkgOTQ1LjI0Yy0zNS42MDMgMC02NC40NjUtNy43NDIxLTY0LjQ2NS0xNy4yOTUgMC00LjE2MzYgNS40ODUtNy45ODQ5IDE0LjYyNC0xMC45NyAwLjA0MzcgMC4wMyAwLjA5MTMgMC4wNjIgMC4xMzM3NSAwLjA5My05LjAyMzggMi45NjM4LTE0LjQzNiA2Ljc1MS0xNC40MzYgMTAuODc4IDAgOS41MDQ4IDI4LjcxOCAxNy4yMSA2NC4xNDQgMTcuMjEgMzUuNDI1IDAgNjQuMTQ0LTcuNzA1MSA2NC4xNDQtMTcuMjEgMC00LjEyNjUtNS40MTI1LTcuOTEzNy0xNC40MzgtMTAuODc4IDAuMDQzOC0wLjAzIDAuMDkxMy0wLjA2MiAwLjEzNS0wLjA5MyA5LjEzODggMi45ODUzIDE0LjYyNCA2LjgwNjYgMTQuNjI0IDEwLjk3IDAgOS41NTI4LTI4Ljg2MiAxNy4yOTUtNjQuNDY1IDE3LjI5NW0yNS44MDItMzMuMDU1YzAuMDYyNS0wLjAyNCAwLjEyNS0wLjA0OSAwLjE4ODc1LTAuMDczLTAuMDYzNyAwLjAyNC0wLjEyNjI1IDAuMDQ5LTAuMTg4NzUgMC4wNzMiIGZpbGw9IiNlY2ViZWEiLz4KICAgPHBhdGggaWQ9InBhdGg1Mjk5IiBkPSJtMzA1LjU5IDk0NS4xNmMtMzUuNDI2IDAtNjQuMTQ0LTcuNzA1MS02NC4xNDQtMTcuMjEgMC00LjEyNjUgNS40MTI1LTcuOTEzNiAxNC40MzYtMTAuODc4IDAuMDQ2MyAwLjAzMSAwLjA5IDAuMDYgMC4xMzYyNSAwLjA5MS04LjkxMjUgMi45NDM5LTE0LjI1MiA2LjY5NzMtMTQuMjUyIDEwLjc4NiAwIDkuNDU3NSAyOC41NzUgMTcuMTI0IDYzLjgyNCAxNy4xMjRzNjMuODIyLTcuNjY2MSA2My44MjItMTcuMTI0YzAtNC4wODg4LTUuMzQtNy44NDIyLTE0LjI1MS0xMC43ODYgMC4wNDYyLTAuMDMxIDAuMDg4OC0wLjA2IDAuMTM1LTAuMDkxIDkuMDI1IDIuOTYzOSAxNC40MzggNi43NTEgMTQuNDM4IDEwLjg3OCAwIDkuNTA0OS0yOC43MTkgMTcuMjEtNjQuMTQ0IDE3LjIxbTI1LjYxMS0zMi44OTljMC4wNjM3LTAuMDIzIDAuMTI3NS0wLjA0NyAwLjE5MTI1LTAuMDcxLTAuMDYzNyAwLjAyMy0wLjEyNzUgMC4wNDctMC4xOTEyNSAwLjA3MSIgZmlsbD0iI2ViZWJlYSIvPgogICA8cGF0aCBpZD0icGF0aDUzMDEiIGQ9Im0zMDUuNTkgOTQ1LjA3Yy0zNS4yNDkgMC02My44MjQtNy42NjYxLTYzLjgyNC0xNy4xMjQgMC00LjA4ODkgNS4zNC03Ljg0MjIgMTQuMjUyLTEwLjc4NiAwLjA0MjUgMC4wMyAwLjA5MTMgMC4wNjIgMC4xMzUgMC4wOTEtOC43OTg4IDIuOTI0OS0xNC4wNjYgNi42NDUyLTE0LjA2NiAxMC42OTUgMCA5LjQxMDEgMjguNDMxIDE3LjAzOCA2My41MDMgMTcuMDM4IDM1LjA3MSAwIDYzLjUwMi03LjYyNzUgNjMuNTAyLTE3LjAzOCAwLTQuMDQ5Ny01LjI2NzUtNy43Ny0xNC4wNjgtMTAuNjk1IDAuMDQzNy0wLjAyOSAwLjA5MjUtMC4wNjIgMC4xMzYyNS0wLjA5MSA4LjkxMTIgMi45NDM5IDE0LjI1MSA2LjY5NzMgMTQuMjUxIDEwLjc4NiAwIDkuNDU3NS0yOC41NzQgMTcuMTI0LTYzLjgyMiAxNy4xMjRtMjUuNDItMzIuNzQxYzAuMDYyNS0wLjAyNCAwLjEyODc1LTAuMDQ3IDAuMTkxMjUtMC4wNzEtMC4wNjI1IDAuMDI0LTAuMTI4NzUgMC4wNDgtMC4xOTEyNSAwLjA3MSIgZmlsbD0iI2VhZWFlOSIvPgogICA8cGF0aCBpZD0icGF0aDUzMDMiIGQ9Im0zMDUuNTkgOTQ0Ljk4Yy0zNS4wNzEgMC02My41MDMtNy42Mjc1LTYzLjUwMy0xNy4wMzggMC00LjA0OTcgNS4yNjc1LTcuNzcgMTQuMDY2LTEwLjY5NSAwLjA0NjMgMC4wMzEgMC4wOSAwLjA2MSAwLjEzNjI1IDAuMDkyLTguNjg1IDIuOTAzOC0xMy44ODEgNi41ODk4LTEzLjg4MSAxMC42MDIgMCA5LjM2MjMgMjguMjg2IDE2Ljk1MSA2My4xODEgMTYuOTUxIDM0Ljg5NCAwIDYzLjE4MS03LjU4ODkgNjMuMTgxLTE2Ljk1MSAwLTQuMDEyNy01LjE5NjItNy42OTg3LTEzLjg4Mi0xMC42MDIgMC4wNDYzLTAuMDMxIDAuMDkxMy0wLjA2MSAwLjEzNjI1LTAuMDkyIDguOCAyLjkyNDkgMTQuMDY4IDYuNjQ1MiAxNC4wNjggMTAuNjk1IDAgOS40MTAxLTI4LjQzMSAxNy4wMzgtNjMuNTAyIDE3LjAzOG0yNS4yMjgtMzIuNTg0YzAuMDY1LTAuMDI0IDAuMTI3NS0wLjA0NiAwLjE5MjUtMC4wNzEtMC4wNjUgMC4wMjUtMC4xMjc1IDAuMDQ4LTAuMTkyNSAwLjA3MSIgZmlsbD0iI2VhZWFlOCIvPgogICA8cGF0aCBpZD0icGF0aDUzMDUiIGQ9Im0zMDUuNTkgOTQ0LjljLTM0Ljg5NSAwLTYzLjE4MS03LjU4ODktNjMuMTgxLTE2Ljk1MSAwLTQuMDEyNyA1LjE5NjItNy42OTg3IDEzLjg4MS0xMC42MDIgMC4wNDYyIDAuMDMxIDAuMDkxMyAwLjA2MSAwLjEzNjI1IDAuMDkxLTguNTcyNSAyLjg4MzgtMTMuNjk4IDYuNTM3Ni0xMy42OTggMTAuNTExIDAgOS4zMTUgMjguMTQ0IDE2Ljg2NSA2Mi44NjEgMTYuODY1IDM0LjcxNiAwIDYyLjg2LTcuNTQ5NyA2Mi44Ni0xNi44NjUgMC0zLjk3MzctNS4xMjUtNy42Mjc1LTEzLjY5OC0xMC41MTEgMC4wNDYyLTAuMDMgMC4wOTEzLTAuMDYgMC4xMzYyNS0wLjA5MSA4LjY4NjIgMi45MDM4IDEzLjg4MiA2LjU4OTggMTMuODgyIDEwLjYwMiAwIDkuMzYyMy0yOC4yODggMTYuOTUxLTYzLjE4MSAxNi45NTFtMjUuMDMyLTMyLjQyNmMwLjA2MzctMC4wMjIgMC4xMzEyNS0wLjA0NyAwLjE5NS0wLjA3MS0wLjA2MzcgMC4wMjQtMC4xMzEyNSAwLjA0OS0wLjE5NSAwLjA3MSIgZmlsbD0iI2U5ZTllOCIvPgogICA8cGF0aCBpZD0icGF0aDUzMDciIGQ9Im0zMDUuNTkgOTQ0LjgxYy0zNC43MTggMC02Mi44NjEtNy41NDk4LTYyLjg2MS0xNi44NjUgMC0zLjk3MzYgNS4xMjUtNy42Mjc1IDEzLjY5OC0xMC41MTEgMC4wNDYyIDAuMDMxIDAuMDkxMyAwLjA2MSAwLjEzNzUgMC4wOTItOC40NiAyLjg2MjgtMTMuNTE0IDYuNDgzLTEzLjUxNCAxMC40MTkgMCA5LjI2NzYgMjggMTYuNzc5IDYyLjU0IDE2Ljc3OXM2Mi41NC03LjUxMTIgNjIuNTQtMTYuNzc5YzAtMy45MzYtNS4wNTM4LTcuNTU2Mi0xMy41MTUtMTAuNDE5IDAuMDQ2Mi0wLjAzMSAwLjA5MTItMC4wNjEgMC4xMzc1LTAuMDkyIDguNTcyNSAyLjg4MzcgMTMuNjk4IDYuNTM3NiAxMy42OTggMTAuNTExIDAgOS4zMTUtMjguMTQ0IDE2Ljg2NS02Mi44NiAxNi44NjVtMjQuODM4LTMyLjI2OGMwLjA2NS0wLjAyNCAwLjEzLTAuMDQ3IDAuMTk1LTAuMDcxLTAuMDY1IDAuMDI0LTAuMTMgMC4wNDctMC4xOTUgMC4wNzEiIGZpbGw9IiNlOWU4ZTciLz4KICAgPHBhdGggaWQ9InBhdGg1MzA5IiBkPSJtMzA1LjU5IDk0NC43M2MtMzQuNTQgMC02Mi41NC03LjUxMTItNjIuNTQtMTYuNzc5IDAtMy45MzYgNS4wNTM4LTcuNTU2MSAxMy41MTQtMTAuNDE5IDAuMDQ2MiAwLjAzMSAwLjA5MjUgMC4wNjIgMC4xMzg3NSAwLjA5My04LjM1IDIuODQxMy0xMy4zMzIgNi40MjczLTEzLjMzMiAxMC4zMjYgMCA5LjIyMDMgMjcuODU4IDE2LjY5NCA2Mi4yMiAxNi42OTRzNjIuMjE5LTcuNDczNiA2Mi4yMTktMTYuNjk0YzAtMy44OTg4LTQuOTgyNS03LjQ4NDgtMTMuMzMxLTEwLjMyNiAwLjA0NjMtMC4wMzEgMC4wOTEzLTAuMDYyIDAuMTM3NS0wLjA5MyA4LjQ2MTIgMi44NjI5IDEzLjUxNSA2LjQ4MyAxMy41MTUgMTAuNDE5IDAgOS4yNjc2LTI4IDE2Ljc3OS02Mi41NCAxNi43NzltMjQuNjQtMzIuMTExYzAuMDY2My0wLjAyNCAwLjEzMTI1LTAuMDQ3IDAuMTk3NS0wLjA3MS0wLjA2NjIgMC4wMjQtMC4xMzEyNSAwLjA0Ny0wLjE5NzUgMC4wNzEiIGZpbGw9IiNlOGU4ZTYiLz4KICAgPHBhdGggaWQ9InBhdGg1MzExIiBkPSJtMzA1LjU5IDk0NC42NGMtMzQuMzYzIDAtNjIuMjItNy40NzM2LTYyLjIyLTE2LjY5NCAwLTMuODk4OSA0Ljk4MjUtNy40ODQ5IDEzLjMzMi0xMC4zMjYgMC4wNDYyIDAuMDMgMC4wOTEzIDAuMDYgMC4xMzg3NSAwLjA5MS04LjIzNzUgMi44MTk5LTEzLjE1IDYuMzczNS0xMy4xNSAxMC4yMzUgMCA5LjE3MjMgMjcuNzEyIDE2LjYwNyA2MS44OTkgMTYuNjA3IDM0LjE4NSAwIDYxLjg5OS03LjQzNTEgNjEuODk5LTE2LjYwNyAwLTMuODYxNC00LjkxMjUtNy40MTUtMTMuMTUtMTAuMjM1IDAuMDQ2Mi0wLjAzIDAuMDkyNS0wLjA2MiAwLjEzODc1LTAuMDkxIDguMzQ4OCAyLjg0MTIgMTMuMzMxIDYuNDI3MiAxMy4zMzEgMTAuMzI2IDAgOS4yMjAzLTI3Ljg1NiAxNi42OTQtNjIuMjE5IDE2LjY5NG0yNC40NDEtMzEuOTU3YzAuMDY2Mi0wLjAyMiAwLjEzMjUtMC4wNDYgMC4xOTg3NS0wLjA3LTAuMDY2MiAwLjAyNC0wLjEzMjUgMC4wNDctMC4xOTg3NSAwLjA3IiBmaWxsPSIjZTdlN2U2Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTMxMyIgZD0ibTMwNS41OSA5NDQuNTVjLTM0LjE4NiAwLTYxLjg5OS03LjQzNS02MS44OTktMTYuNjA3IDAtMy44NjE0IDQuOTEyNS03LjQxNSAxMy4xNS0xMC4yMzUgMC4wNDYzIDAuMDMxIDAuMDkyNSAwLjA2MSAwLjEzODc1IDAuMDkyLTguMTI2MiAyLjc5ODktMTIuOTY4IDYuMzE5OS0xMi45NjggMTAuMTQzIDAgOS4xMjM1IDI3LjU2OSAxNi41MjIgNjEuNTc4IDE2LjUyMiAzNC4wMDggMCA2MS41NzgtNy4zOTggNjEuNTc4LTE2LjUyMiAwLTMuODIyOC00Ljg0MjUtNy4zNDM4LTEyLjk2OS0xMC4xNDMgMC4wNDc1LTAuMDMxIDAuMDkzNy0wLjA2MSAwLjE0LTAuMDkyIDguMjM3NSAyLjgxOTkgMTMuMTUgNi4zNzM1IDEzLjE1IDEwLjIzNSAwIDkuMTcyNC0yNy43MTQgMTYuNjA3LTYxLjg5OSAxNi42MDdtMjQuMjQxLTMxLjc5OWMwLjA2ODctMC4wMjQgMC4xMzI1LTAuMDQ2IDAuMi0wLjA3MS0wLjA2NzUgMC4wMjYtMC4xMzEyNSAwLjA0OC0wLjIgMC4wNzEiIGZpbGw9IiNlN2U2ZTUiLz4KICAgPHBhdGggaWQ9InBhdGg1MzE1IiBkPSJtMzA1LjU5IDk0NC40N2MtMzQuMDA5IDAtNjEuNTc4LTcuMzk4LTYxLjU3OC0xNi41MjIgMC0zLjgyMjcgNC44NDEyLTcuMzQzNyAxMi45NjgtMTAuMTQzIDAuMDQ2MiAwLjAzIDAuMDkzNyAwLjA2MiAwLjE0IDAuMDkxLTguMDE2MiAyLjc3ODctMTIuNzg4IDYuMjY2MS0xMi43ODggMTAuMDUxIDAgOS4wNzc3IDI3LjQyNiAxNi40MzUgNjEuMjU4IDE2LjQzNSAzMy44MzEgMCA2MS4yNTYtNy4zNTc0IDYxLjI1Ni0xNi40MzUgMC0zLjc4NTEtNC43NzEyLTcuMjcyNS0xMi43ODYtMTAuMDUxIDAuMDQ2Mi0wLjAzIDAuMDkyNS0wLjA2MSAwLjEzODc1LTAuMDkxIDguMTI2MiAyLjc5ODkgMTIuOTY5IDYuMzE5OSAxMi45NjkgMTAuMTQzIDAgOS4xMjM1LTI3LjU3IDE2LjUyMi02MS41NzggMTYuNTIybTI0LjA0LTMxLjY0M2MwLjA2NjItMC4wMjIgMC4xMzYyNS0wLjA0NiAwLjIwMTI1LTAuMDctMC4wNjUgMC4wMjQtMC4xMzUgMC4wNDgtMC4yMDEyNSAwLjA3IiBmaWxsPSIjZTZlNmU0Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTMxNyIgZD0ibTMwNS41OSA5NDQuMzhjLTMzLjgzMSAwLTYxLjI1OC03LjM1NzUtNjEuMjU4LTE2LjQzNSAwLTMuNzg1MSA0Ljc3MTItNy4yNzI1IDEyLjc4OC0xMC4wNTEgMC4wNDYzIDAuMDMxIDAuMDkzNyAwLjA2MiAwLjE0IDAuMDkyLTcuOTA2MiAyLjc1NzgtMTIuNjA2IDYuMjExNS0xMi42MDYgOS45NTkgMCA5LjAyODcgMjcuMjgxIDE2LjM0OSA2MC45MzYgMTYuMzQ5IDMzLjY1NCAwIDYwLjkzNi03LjMxOTkgNjAuOTM2LTE2LjM0OSAwLTMuNzQ3NS00LjcwMTItNy4yMDEyLTEyLjYwOC05Ljk1OSAwLjA0NzUtMC4wMyAwLjA5NS0wLjA2MSAwLjE0MTI1LTAuMDkyIDguMDE1IDIuNzc4NyAxMi43ODYgNi4yNjYxIDEyLjc4NiAxMC4wNTEgMCA5LjA3NzYtMjcuNDI1IDE2LjQzNS02MS4yNTYgMTYuNDM1bTIzLjgzOC0zMS40ODVjMC4wNjg3LTAuMDI0IDAuMTM1LTAuMDQ4IDAuMjAyNS0wLjA3MS0wLjA2NzUgMC4wMjQtMC4xMzM3NSAwLjA0Ny0wLjIwMjUgMC4wNzEiIGZpbGw9IiNlNWU1ZTMiLz4KICAgPHBhdGggaWQ9InBhdGg1MzE5IiBkPSJtMzA1LjU5IDk0NC4zYy0zMy42NTUgMC02MC45MzYtNy4zMTk5LTYwLjkzNi0xNi4zNDkgMC0zLjc0NzUgNC43LTcuMjAxMSAxMi42MDYtOS45NTkgMC4wNDYyIDAuMDMgMC4wOTUgMC4wNjIgMC4xNDEyNSAwLjA5My03Ljc5NSAyLjczNjQtMTIuNDI4IDYuMTU2My0xMi40MjggOS44NjYzIDAgOC45ODE1IDI3LjEzOSAxNi4yNjMgNjAuNjE2IDE2LjI2MyAzMy40NzYgMCA2MC42MTUtNy4yODEyIDYwLjYxNS0xNi4yNjMgMC0zLjcxLTQuNjMxMi03LjEyOTktMTIuNDI4LTkuODY2MyAwLjA0NzUtMC4wMzEgMC4wOTUtMC4wNjIgMC4xNDEyNS0wLjA5MyA3LjkwNjIgMi43NTc5IDEyLjYwOCA2LjIxMTUgMTIuNjA4IDkuOTU5IDAgOS4wMjg4LTI3LjI4MiAxNi4zNDktNjAuOTM2IDE2LjM0OW0yMy42MzItMzEuMzI5YzAuMDY4Ny0wLjAyNCAwLjEzNzUtMC4wNDcgMC4yMDUtMC4wNy0wLjA2NzUgMC4wMjItMC4xMzYyNSAwLjA0Ni0wLjIwNSAwLjA3IiBmaWxsPSIjZTRlNGUzIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTMyMSIgZD0ibTMwNS41OSA5NDQuMjFjLTMzLjQ3OCAwLTYwLjYxNi03LjI4MTItNjAuNjE2LTE2LjI2MyAwLTMuNzEgNC42MzI1LTcuMTI5OSAxMi40MjgtOS44NjYyIDAuMDQ2MyAwLjAzIDAuMDk1IDAuMDYxIDAuMTQxMjUgMC4wOTEtNy42ODUgMi43MTYyLTEyLjI0OCA2LjEwMjUtMTIuMjQ4IDkuNzc0OCAwIDguOTMzNyAyNi45OTUgMTYuMTc2IDYwLjI5NSAxNi4xNzZzNjAuMjk0LTcuMjQyNiA2MC4yOTQtMTYuMTc2YzAtMy42NzIzLTQuNTYxMi03LjA1ODYtMTIuMjQ4LTkuNzc0OCAwLjA0NjMtMC4wMyAwLjA5NS0wLjA2MiAwLjE0MTI1LTAuMDkxIDcuNzk2MiAyLjczNjMgMTIuNDI4IDYuMTU2MiAxMi40MjggOS44NjYyIDAgOC45ODE1LTI3LjEzOSAxNi4yNjMtNjAuNjE1IDE2LjI2M20yMy40MjgtMzEuMTczYzAuMDY3NS0wLjAyNCAwLjEzNzUtMC4wNDYgMC4yMDUtMC4wNy0wLjA2NzUgMC4wMjQtMC4xMzc1IDAuMDQ2LTAuMjA1IDAuMDciIGZpbGw9IiNlNGUzZTIiLz4KICAgPHBhdGggaWQ9InBhdGg1MzIzIiBkPSJtMzA1LjU5IDk0NC4xMmMtMzMuMyAwLTYwLjI5NS03LjI0MjctNjAuMjk1LTE2LjE3NiAwLTMuNjcyNCA0LjU2MjUtNy4wNTg2IDEyLjI0OC05Ljc3NDkgMC4wNSAwLjAzMiAwLjA5MzcgMC4wNjEgMC4xNDI1IDAuMDkyLTcuNTc3NSAyLjY5MzktMTIuMDY5IDYuMDQ3OS0xMi4wNjkgOS42ODI3IDAgOC44ODYyIDI2Ljg1MSAxNi4wOTEgNTkuOTc0IDE2LjA5MSAzMy4xMjIgMCA1OS45NzQtNy4yMDUxIDU5Ljk3NC0xNi4wOTEgMC0zLjYzNDgtNC40OTI1LTYuOTg4OC0xMi4wNy05LjY4MjcgMC4wNDg3LTAuMDMxIDAuMDkzNy0wLjA2IDAuMTQyNS0wLjA5MiA3LjY4NjIgMi43MTYzIDEyLjI0OCA2LjEwMjUgMTIuMjQ4IDkuNzc0OSAwIDguOTMzNi0yNi45OTQgMTYuMTc2LTYwLjI5NCAxNi4xNzZtMjMuMjItMzEuMDE2YzAuMDY3NS0wLjAyNCAwLjEzODc1LTAuMDQ4IDAuMjA3NS0wLjA3LTAuMDY4OCAwLjAyMi0wLjE0IDAuMDQ2LTAuMjA3NSAwLjA3IiBmaWxsPSIjZTNlM2UxIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTMyNSIgZD0ibTMwNS41OSA5NDQuMDRjLTMzLjEyMyAwLTU5Ljk3NC03LjIwNS01OS45NzQtMTYuMDkxIDAtMy42MzQ3IDQuNDkxMi02Ljk4ODcgMTIuMDY5LTkuNjgyNiAwLjA0NzUgMC4wMyAwLjA5NjIgMC4wNjIgMC4xNDM3NSAwLjA5My03LjQ2NzUgMi42NzI0LTExLjg5MiA1Ljk5MjItMTEuODkyIDkuNTg5OSAwIDguODM4OCAyNi43MDggMTYuMDA1IDU5LjY1NCAxNi4wMDUgMzIuOTQ1IDAgNTkuNjUyLTcuMTY2IDU5LjY1Mi0xNi4wMDUgMC0zLjU5NzctNC40MjM4LTYuOTE3NS0xMS44OTEtOS41ODk5IDAuMDQ2My0wLjAzIDAuMDk2Mi0wLjA2MiAwLjE0MjUtMC4wOTMgNy41Nzc1IDIuNjkzOSAxMi4wNyA2LjA0NzkgMTIuMDcgOS42ODI2IDAgOC44ODYzLTI2Ljg1MSAxNi4wOTEtNTkuOTc0IDE2LjA5MW0yMy4wMS0zMC44NjFjMC4wNzEzLTAuMDI0IDAuMTM4NzUtMC4wNDYgMC4yMS0wLjA3LTAuMDcxMyAwLjAyNC0wLjEzODc1IDAuMDQ2LTAuMjEgMC4wNyIgZmlsbD0iI2UyZTJlMCIvPgogICA8cGF0aCBpZD0icGF0aDUzMjciIGQ9Im0zMDUuNTkgOTQzLjk1Yy0zMi45NDYgMC01OS42NTQtNy4xNjYtNTkuNjU0LTE2LjAwNSAwLTMuNTk3NiA0LjQyNS02LjkxNzUgMTEuODkyLTkuNTg5OSAwLjA0ODcgMC4wMzEgMC4wOTM3IDAuMDYgMC4xNDM3NSAwLjA5MS03LjM2IDIuNjUyOC0xMS43MTUgNS45MzktMTEuNzE1IDkuNDk5IDAgOC43OTEgMjYuNTY0IDE1LjkxOSA1OS4zMzMgMTUuOTE5IDMyLjc2OCAwIDU5LjMzMi03LjEyOCA1OS4zMzItMTUuOTE5IDAtMy41Ni00LjM1NjItNi44NDYyLTExLjcxNS05LjQ5OSAwLjA0ODgtMC4wMzEgMC4wOTUtMC4wNiAwLjE0Mzc1LTAuMDkxIDcuNDY3NSAyLjY3MjQgMTEuODkxIDUuOTkyMyAxMS44OTEgOS41ODk5IDAgOC44Mzg5LTI2LjcwOCAxNi4wMDUtNTkuNjUyIDE2LjAwNW0yMi43OTktMzAuNzA2YzAuMDcxMi0wLjAyMiAwLjE0MTI1LTAuMDQ2IDAuMjExMjUtMC4wNjktMC4wNyAwLjAyMy0wLjE0IDAuMDQ2LTAuMjExMjUgMC4wNjkiIGZpbGw9IiNlMmUxZGYiLz4KICAgPHBhdGggaWQ9InBhdGg1MzI5IiBkPSJtMzA1LjU5IDk0My44N2MtMzIuNzY5IDAtNTkuMzMzLTcuMTI4LTU5LjMzMy0xNS45MTkgMC0zLjU2IDQuMzU1LTYuODQ2MiAxMS43MTUtOS40OTkgMC4wNDYyIDAuMDMgMC4wOTYyIDAuMDYzIDAuMTQzNzUgMC4wOTMtNy4yNTEyIDIuNjI5OC0xMS41MzkgNS44ODM3LTExLjUzOSA5LjQwNjIgMCA4Ljc0MzYgMjYuNDIxIDE1LjgzMiA1OS4wMTMgMTUuODMyIDMyLjU5MSAwIDU5LjAxMS03LjA4ODkgNTkuMDExLTE1LjgzMiAwLTMuNTIyNS00LjI4NzUtNi43NzY0LTExLjUzOS05LjQwNjIgMC4wNDc1LTAuMDMgMC4wOTc1LTAuMDYyIDAuMTQ1LTAuMDkzIDcuMzU4OCAyLjY1MjcgMTEuNzE1IDUuOTM5IDExLjcxNSA5LjQ5OSAwIDguNzkxLTI2LjU2NSAxNS45MTktNTkuMzMyIDE1LjkxOW0yMi41ODYtMzAuNTVjMC4wNjg3LTAuMDIyIDAuMTQzNzUtMC4wNDcgMC4yMTI1LTAuMDctMC4wNjg4IDAuMDIyLTAuMTQzNzUgMC4wNDctMC4yMTI1IDAuMDciIGZpbGw9IiNlMWUwZGYiLz4KICAgPHBhdGggaWQ9InBhdGg1MzMxIiBkPSJtMzA1LjU5IDk0My43OGMtMzIuNTkxIDAtNTkuMDEzLTcuMDg4OS01OS4wMTMtMTUuODMyIDAtMy41MjI1IDQuMjg3NS02Ljc3NjQgMTEuNTM5LTkuNDA2MiAwLjA0ODcgMC4wMzEgMC4wOTYyIDAuMDYxIDAuMTQ1IDAuMDkyLTcuMTQyNSAyLjYwODgtMTEuMzYyIDUuODI5MS0xMS4zNjIgOS4zMTQgMCA4LjY5NjIgMjYuMjc2IDE1Ljc0NiA1OC42OTEgMTUuNzQ2IDMyLjQxNCAwIDU4LjY5LTcuMDQ5OSA1OC42OS0xNS43NDYgMC0zLjQ4NDktNC4yMi02LjcwNTItMTEuMzYxLTkuMzE0IDAuMDQ4OC0wLjAzMSAwLjA5NS0wLjA2MSAwLjE0Mzc1LTAuMDkyIDcuMjUxMiAyLjYyOTggMTEuNTM5IDUuODgzNyAxMS41MzkgOS40MDYyIDAgOC43NDM2LTI2LjQyIDE1LjgzMi01OS4wMTEgMTUuODMybTIyLjM3Mi0zMC4zOTVjMC4wNzI1LTAuMDIyIDAuMTQtMC4wNDUgMC4yMTM3NS0wLjA2OS0wLjA3MzggMC4wMjQtMC4xNDEyNSAwLjA0Ni0wLjIxMzc1IDAuMDY5IiBmaWxsPSIjZTBlMGRlIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTMzMyIgZD0ibTMwNS41OSA5NDMuNjljLTMyLjQxNSAwLTU4LjY5MS03LjA0OTgtNTguNjkxLTE1Ljc0NiAwLTMuNDg0OSA0LjIyLTYuNzA1MSAxMS4zNjItOS4zMTQgMC4wNDYyIDAuMDMgMC4wOTg3IDAuMDYyIDAuMTQ1IDAuMDkzLTcuMDM2MiAyLjU4Ni0xMS4xODYgNS43NzM1LTExLjE4NiA5LjIyMTMgMCA4LjY0ODggMjYuMTMyIDE1LjY2IDU4LjM3IDE1LjY2IDMyLjIzNiAwIDU4LjM3LTcuMDExMyA1OC4zNy0xNS42NiAwLTMuNDQ3OC00LjE1MTItNi42MzUzLTExLjE4OC05LjIyMTMgMC4wNDc1LTAuMDMgMC4wOTg3LTAuMDYyIDAuMTQ2MjUtMC4wOTMgNy4xNDEyIDIuNjA4OSAxMS4zNjEgNS44MjkxIDExLjM2MSA5LjMxNCAwIDguNjk2My0yNi4yNzYgMTUuNzQ2LTU4LjY5IDE1Ljc0Nm0yMi4xNTUtMzAuMjM5YzAuMDcxMy0wLjAyMyAwLjE0NjI1LTAuMDQ2IDAuMjE3NS0wLjA3LTAuMDcxMiAwLjAyNC0wLjE0NjI1IDAuMDQ3LTAuMjE3NSAwLjA3IiBmaWxsPSIjZTBkZmRkIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTMzNSIgZD0ibTMwNS41OSA5NDMuNjFjLTMyLjIzOCAwLTU4LjM3LTcuMDExMy01OC4zNy0xNS42NiAwLTMuNDQ3NyA0LjE1LTYuNjM1MiAxMS4xODYtOS4yMjEyIDAuMDUgMC4wMzEgMC4wOTc1IDAuMDYxIDAuMTQ3NSAwLjA5Mi02LjkyODggMi41NjQ4LTExLjAxNCA1LjcxODctMTEuMDE0IDkuMTI4OCAwIDguNjAxMiAyNS45OSAxNS41NzUgNTguMDUgMTUuNTc1czU4LjA0OS02Ljk3NCA1OC4wNDktMTUuNTc1YzAtMy40MTAxLTQuMDg1LTYuNTY0LTExLjAxMi05LjEyODggMC4wNDg3LTAuMDMxIDAuMDk2Mi0wLjA2MSAwLjE0NjI1LTAuMDkyIDcuMDM2MiAyLjU4NiAxMS4xODggNS43NzM1IDExLjE4OCA5LjIyMTIgMCA4LjY0ODktMjYuMTM0IDE1LjY2LTU4LjM3IDE1LjY2bTIxLjkzOC0zMC4wODRjMC4wNzM4LTAuMDIyIDAuMTQzNzUtMC4wNDUgMC4yMTc1LTAuMDY5LTAuMDczNyAwLjAyNC0wLjE0Mzc1IDAuMDQ2LTAuMjE3NSAwLjA2OSIgZmlsbD0iI2RmZGVkYyIvPgogICA8cGF0aCBpZD0icGF0aDUzMzciIGQ9Im0zMDUuNTkgOTQzLjUyYy0zMi4wNiAwLTU4LjA1LTYuOTc0MS01OC4wNS0xNS41NzUgMC0zLjQxMDEgNC4wODUtNi41NjQgMTEuMDE0LTkuMTI4OSAwLjA0ODcgMC4wMzEgMC4wOTc1IDAuMDYyIDAuMTQ2MjUgMC4wOTMtNi44MjEyIDIuNTQyNS0xMC44MzkgNS42NjM3LTEwLjgzOSA5LjAzNjIgMCA4LjU1MzcgMjUuODQ2IDE1LjQ4OSA1Ny43MjkgMTUuNDg5IDMxLjg4MiAwIDU3LjcyOS02LjkzNSA1Ny43MjktMTUuNDg5IDAtMy4zNzI1LTQuMDE3NS02LjQ5MzctMTAuODQtOS4wMzYyIDAuMDUtMC4wMzEgMC4wOTc1LTAuMDYyIDAuMTQ3NS0wLjA5MyA2LjkyNzUgMi41NjQ5IDExLjAxMiA1LjcxODggMTEuMDEyIDkuMTI4OSAwIDguNjAxMS0yNS45ODkgMTUuNTc1LTU4LjA0OSAxNS41NzVtMjEuNzE4LTI5LjkzYzAuMDczOC0wLjAyMyAwLjE0NjI1LTAuMDQ1IDAuMjItMC4wNjktMC4wNzM3IDAuMDI0LTAuMTQ2MjUgMC4wNDYtMC4yMiAwLjA2OSIgZmlsbD0iI2RlZGRkYiIvPgogICA8cGF0aCBpZD0icGF0aDUzMzkiIGQ9Im0zMDUuNTkgOTQzLjQ0Yy0zMS44ODMgMC01Ny43MjktNi45MzUtNTcuNzI5LTE1LjQ4OSAwLTMuMzcyNSA0LjAxNzUtNi40OTM2IDEwLjgzOS05LjAzNjEgMC4wNSAwLjAzMSAwLjA5ODcgMC4wNjEgMC4xNDc1IDAuMDkyLTYuNzE1IDIuNTIxNS0xMC42NjYgNS42MDg5LTEwLjY2NiA4Ljk0MzkgMCA4LjUwNjQgMjUuNzAyIDE1LjQwMiA1Ny40MDkgMTUuNDAyIDMxLjcwNSAwIDU3LjQwOC02Ljg5NTkgNTcuNDA4LTE1LjQwMiAwLTMuMzM1LTMuOTUxMi02LjQyMjQtMTAuNjY2LTguOTQzOSAwLjA1LTAuMDMxIDAuMDk4OC0wLjA2MSAwLjE0NzUtMC4wOTIgNi44MjI1IDIuNTQyNSAxMC44NCA1LjY2MzYgMTAuODQgOS4wMzYxIDAgOC41NTM4LTI1Ljg0NiAxNS40ODktNTcuNzI5IDE1LjQ4OW0yMS40OTUtMjkuNzc1YzAuMDczOC0wLjAyMyAwLjE0ODc1LTAuMDQ2IDAuMjIyNS0wLjA2OS0wLjA3MzcgMC4wMjItMC4xNDg3NSAwLjA0Ni0wLjIyMjUgMC4wNjkiIGZpbGw9IiNkZGRjZGEiLz4KICAgPHBhdGggaWQ9InBhdGg1MzQxIiBkPSJtMzA1LjU5IDk0My4zNWMtMzEuNzA2IDAtNTcuNDA5LTYuODk2LTU3LjQwOS0xNS40MDIgMC0zLjMzNSAzLjk1MTItNi40MjI0IDEwLjY2Ni04Ljk0MzkgMC4wNSAwLjAzIDAuMSAwLjA2MiAwLjE0ODc1IDAuMDkzLTYuNjEgMi40OTg3LTEwLjQ5NCA1LjU1MzgtMTAuNDk0IDguODUxMiAwIDguNDU5IDI1LjU1OSAxNS4zMTYgNTcuMDg4IDE1LjMxNiAzMS41MjggMCA1Ny4wODYtNi44NTc0IDU3LjA4Ni0xNS4zMTYgMC0zLjI5NzQtMy44ODM4LTYuMzUyNS0xMC40OTQtOC44NTEyIDAuMDUtMC4wMzEgMC4wOTg3LTAuMDYyIDAuMTQ4NzUtMC4wOTMgNi43MTUgMi41MjE1IDEwLjY2NiA1LjYwODkgMTAuNjY2IDguOTQzOSAwIDguNTA2NC0yNS43MDIgMTUuNDAyLTU3LjQwOCAxNS40MDJtMjEuMjctMjkuNjJjMC4wNzM3LTAuMDIzIDAuMTUxMjUtMC4wNDYgMC4yMjUtMC4wNjktMC4wNzM4IDAuMDIyLTAuMTUxMjUgMC4wNDYtMC4yMjUgMC4wNjkiIGZpbGw9IiNkZGRjZGEiLz4KICAgPHBhdGggaWQ9InBhdGg1MzQzIiBkPSJtMzA1LjU5IDk0My4yNmMtMzEuNTI5IDAtNTcuMDg4LTYuODU3NC01Ny4wODgtMTUuMzE2IDAtMy4yOTc0IDMuODgzOC02LjM1MjUgMTAuNDk0LTguODUxMSAwLjA1IDAuMDMgMC4xMDI1IDAuMDYyIDAuMTUxMjUgMC4wOTQtNi41MDUgMi40NzYxLTEwLjMyNCA1LjQ5NDYtMTAuMzI0IDguNzUzNSAwIDguNDE1IDI1LjQxNCAxNS4yMzMgNTYuNzY4IDE1LjIzM3M1Ni43NjgtNi44MTc5IDU2Ljc2OC0xNS4yMzNjMC0zLjI1ODktMy44MjEyLTYuMjc4OS0xMC4zMjYtOC43NTQ5IDAuMDUtMC4wMyAwLjEwMTI1LTAuMDYyIDAuMTUtMC4wOTIgNi42MSAyLjQ5ODYgMTAuNDk0IDUuNTUzOCAxMC40OTQgOC44NTExIDAgOC40NTktMjUuNTU5IDE1LjMxNi01Ny4wODYgMTUuMzE2bTIxLjAzNC0yOS40NjJjMC4wNzc1LTAuMDI0IDAuMTU4NzUtMC4wNDggMC4yMzYyNS0wLjA3MS0wLjA3NzUgMC4wMjMtMC4xNTg3NSAwLjA0Ny0wLjIzNjI1IDAuMDcxIiBmaWxsPSIjZGNkYmQ5Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTM0NSIgZD0ibTMxMi4zMiA5MjcuOTVjMCAwLjk5NzUtMy4wMTEyIDEuODA1MS02LjcyNjIgMS44MDUxcy02LjcyNzUtMC44MDc2LTYuNzI3NS0xLjgwNTFjMC0wLjk5NjEgMy4wMTI1LTEuODA1MSA2LjcyNzUtMS44MDUxczYuNzI2MiAwLjgwOSA2LjcyNjIgMS44MDUxIiBmaWxsPSIjMTAwZjBkIi8+CiAgPC9nPgogIDxnIGlkPSJnNTQyOSIgdHJhbnNmb3JtPSJtYXRyaXgoMC4xMjUgMCAwIC0wLjEyNSAtMjY4Ljk4IDk1MS43KSI+CiAgIDxnIGlkPSJnNTQzMSIgY2xpcC1wYXRoPSJ1cmwoI2NsaXBQYXRoNTQzMy0xKSI+CiAgICA8cGF0aCBpZD0icGF0aDU0NDUiIGQ9Im00ODI0LjcgNDM3Mi4yYzAgMTA1LjI2LTg2LjEzIDE5MS4zOS0xOTEuMzkgMTkxLjM5aC03My40NGMtMTAuNTUgMC0yMC45MS0wLjg3LTMxLTIuNTN2MTEyOC40YzAgMzAuNDYtMjQuNyA1NS4xNi01NS4xNiA1NS4xNi0zMC40NyAwLTU1LjE2LTI0LjctNTUuMTYtNTUuMTZ2LTExODguNmMtMzEuMDctMzQuMDUtNTAuMDctNzkuMjgtNTAuMDctMTI4LjcydjEyODkuN2MwIDEwNS4yNiA4Ni4xMiAxOTEuMzggMTkxLjM5IDE5MS4zOGg3My40NGMxMDUuMjYgMCAxOTEuMzktODYuMTIgMTkxLjM5LTE5MS4zOHYtMTI4OS43IiBmaWxsPSJ1cmwoI2xpbmVhckdyYWRpZW50NTQzNykiLz4KICAgPC9nPgogIDwvZz4KICA8ZyBpZD0iZzU0NDciIHRyYW5zZm9ybT0ibWF0cml4KDAuMTI1IDAgMCAtMC4xMjUgLTI2OC45OCA5NTEuNykiPgogICA8ZyBpZD0iZzU0NDkiIGNsaXAtcGF0aD0idXJsKCNjbGlwUGF0aDU0NTEtOSkiPgogICAgPHBhdGggaWQ9InBhdGg1NDYxIiBkPSJtNDgyNC43IDE0MDAuN2MtNzAuMDkgMjkuNzYtMTQ3LjE4IDQ2LjIyLTIyOC4xMSA0Ni4yMi04MC45NCAwLTE1OC4wMy0xNi40Ni0yMjguMTEtNDYuMjJ2Mjk3MS41YzAgNDkuNDQgMTkgOTQuNjcgNTAuMDcgMTI4Ljcydi0yOTE0LjFjMC0zMC40NyAyNC42OS01NS4xNiA1NS4xNi01NS4xNiAzMC40NiAwIDU1LjE2IDI0LjY5IDU1LjE2IDU1LjE2djI5NzQuMmMxMC4wOSAxLjY2IDIwLjQ1IDIuNTMgMzEgMi41M2g3My40NGMxMDUuMjYgMCAxOTEuMzktODYuMTMgMTkxLjM5LTE5MS4zOXYtMjk3MS41IiBmaWxsPSJ1cmwoI2xpbmVhckdyYWRpZW50NTQ1NSkiLz4KICAgPC9nPgogIDwvZz4KICA8ZyBpZD0iZzU0NjMiIHRyYW5zZm9ybT0ibWF0cml4KDAuMTI1IDAgMCAtMC4xMjUgLTI2OC45OCA5NTEuNykiPgogICA8ZyBpZD0iZzU0NjUiIGNsaXAtcGF0aD0idXJsKCNjbGlwUGF0aDU0NjctNSkiPgogICAgPHBhdGggaWQ9InBhdGg1NDc3IiBkPSJtNTE4MC43IDg2Mi43NWMwLTMyMi42NC0yNjEuNTUtNTg0LjE4LTU4NC4xOC01ODQuMTgtMzIyLjY0IDAtNTg0LjE4IDI2MS41NC01ODQuMTggNTg0LjE4IDAgMzIyLjYzIDI2MS41NCA1ODQuMTggNTg0LjE4IDU4NC4xOCAzMjIuNjMgMCA1ODQuMTgtMjYxLjU1IDU4NC4xOC01ODQuMTgiIGZpbGw9InVybCgjcmFkaWFsR3JhZGllbnQ1NTAzLTgpIi8+CiAgIDwvZz4KICA8L2c+CiAgPGcgaWQ9Imc1NDc5IiB0cmFuc2Zvcm09Im1hdHJpeCgwLjEyNSAwIDAgLTAuMTI1IC0yNjguOTggOTUxLjcpIj4KICAgPGcgaWQ9Imc1NDgxIiBjbGlwLXBhdGg9InVybCgjY2xpcFBhdGg1NDgzLTIpIj4KICAgIDxwYXRoIGlkPSJwYXRoNTQ5MyIgZD0ibTQ5MzkgMTE3MS44djIuNDctMS4yMy0xLjI0IiBmaWxsPSJ1cmwoI3JhZGlhbEdyYWRpZW50NTUwMy04KSIvPgogICA8L2c+CiAgPC9nPgogIDxnIGlkPSJnNTQ5NSIgdHJhbnNmb3JtPSJtYXRyaXgoMC4xMjUgMCAwIC0wLjEyNSAtMjY4Ljk4IDk1MS43KSI+CiAgIDxnIGlkPSJnNTQ5NyIgY2xpcC1wYXRoPSJ1cmwoI2NsaXBQYXRoNTQ5OS02KSI+CiAgICA8cGF0aCBpZD0icGF0aDU1MDkiIGQ9Im00NzU2LjIgOTgwLjg2YzEwOC43NyAzNi4zODEgMTgyLjkgMTA4Ljc5IDE4Mi45IDE5Mi4xOHYtMS4yNGMtMC43My04Mi44Ny03NC42Ny0xNTQuNzMtMTgyLjktMTkwLjk0bTE4Mi45IDE5Mi4xOGMwIDQ0Ljc0LTIxLjM0IDg2LjMxLTU3LjkyIDEyMC44NiAzNi4yNC0zNC4yMyA1Ny41My03NS4zNiA1Ny45Mi0xMTkuNjN2LTEuMjMiIGZpbGw9InVybCgjcmFkaWFsR3JhZGllbnQ1NTAzLTgpIi8+CiAgIDwvZz4KICA8L2c+CiAgPGcgaWQ9Imc1NTExIiB0cmFuc2Zvcm09Im1hdHJpeCgwLjEyNSAwIDAgLTAuMTI1IC0yNjguOTggOTUxLjcpIj4KICAgPGcgaWQ9Imc1NTEzIiBjbGlwLXBhdGg9InVybCgjY2xpcFBhdGg1NTE1LTYpIj4KICAgIDxnIGlkPSJnNTUxOSIgdHJhbnNmb3JtPSJtYXRyaXgoNjk2LjIsMCwwLDQ1My4yLDQyNDcuOSw5NDUuOSkiPgogICAgIDxpbWFnZSBpZD0iaW1hZ2U1NTIxIiB4bGluazpocmVmPSJkYXRhOmltYWdlL3BuZztiYXNlNjQsaVZCT1J3MEtHZ29BQUFBTlNVaEVVZ0FBQUpFQUFBQmVDQVlBQUFBalp2WkNBQUFBQkhOQ1NWUUlDQWdJZkFoa2lBQUFHWHBKUkVGVWVKemRYZG1TNURDcWhlejgvNys5RWZlaHk4eURCVG9jSVZuT3JLenFHU0lxeW91RU5nd0h0S1FlLy85L0p2L05wUC9kMVE4eS9lMGF2RXhQK2Z2MU9lNlBiK0J4MWJmL0l6SjBTZC9SenVNYmVCVDAvQmhua2M1NkVJUWJYNTEzM3YrS3hybExMMmtvVzk1K056M2wrS0FtOHZiZmJVUWxNTi9kRWZvaDgyRS9JT3l2bXI0UFZlMHA5a0Voc3UvNWtENUMzMkZxSy9xZ1luK0hQcW5Jbi9ZT0psb0l5QjNoMFhkYStIanhxL3dYQi90NHJSOXNvN08zVU1HTHcvQzBOOHlacWt6TmdwcUlQUFpxOVlvRmlHTC9SV0Y0ays3MngvWm5kS3krZW52WkVqL2w2KzlyT1IrTHh0SUFKemtyaEc3V3ROVUhkdG5nZnhXSXYrSEszMm9TZFZDNlhmRjU0YU44aXIzNEtjOVU3ME5HNXdEdmQvclFCZTBWT1hDczg0L0tVTkFMM1I1TjJsRVpWMGxtNWI4ZzVFLzVlNkdKS3A3RkoyRStlSlYxWE5XckFyaFZCNEFHV3pienlqcHZtdGkzYVdVNkxpalZjTmZHcklSeXdVSjNoV25CNHlWTlpCWEhMNmtGUWhjVlVMbitJbjNRZ2NkV3R3TGdUakwvU3hncXh1UkY4THdsbEMrQTVrcEcxWXBCV3duUmNTdzBFZkVLWlRBelQ2NEZac2FiaFl5VFZmbFlzK3g2WTVBdmNmM0oyWVYzbGQ2T3dLM016eDJUVlh5a3BSSXNudDNTUktNRzBzbVlGQ1VWV0ttekdiWE5tS2I5MzYzdUZLMXY1djhrM1FMSU8ya21qWjFwK2lMOUtUQ3NmZlpNMmxOYm5NaFFDekE0WmFBTGFVTmFzVHhLcHlKWm8xeHBwQVhaYnBEd1g1c3VlUUd3VHZIS0RrSGVEc2pYR3FnL1krR2hSTUJiVFZFVFFjSUIzMXdNaENxWnVLeFpzam14N3Zvemp3VlpnWTNHZWxRWmwyeC9seTdxWmlxaVcxaklhbll6d2IwU0VtYTIwRWdtSWsrenIvTUtoUVlHZW1tR25EL2pwdEs3NmtLUWhhZW90QlBVcVp4TzR6NjZJekNmMGxEZkhRZENkaGNlMkxRcnArWnJVblpWRHl3YitSMHVSSkJJMWJJcGNET0VRbVlTbWlOa28xMU1ZMExXK1djWHR2TWZPakdBZXRFb2VMeHQ0aEw5TzBLMFpiWm1IOUVrNzNRcVpPcU5SY2I4Y2lZOFVNWlQwRHRUN1R3ZUlvb00yVnp4SzN4ZkJmd2NWaVd0QlRjSHRZOEVJd2tZeFhwbVNzVldKdklYekp6T1lqNWNUVEJoTVc2VlJocUN1cHVZQjNsVWhIeEFVRTJVaE9wazNwZUN1Qm55L00zS3hRZ2RJZ29EYXp4eU82NytneVY1a3E1NFZHcXZDOEl2L0RWdDlUNmxPbnpIMHFBN2JqdUhDQ1pDbHZVRENQQkNDL1U0UUxqNFpNS2NFQS9oWEprTG5CWHBrQWUrRjA1VFZBaTlQdW9zWlhOYWtUZTBhU3FyeXZoaEtnVUh3ZklGTmx0R1lLNDB6eXdzd3ZsWTBIR2Nqem9kOG5sYU0yZUtBdy9hU0hBNkE4dEdrM0kwVHdJYndwcXA2cXlacTcrRm9ndmFpVGVWK1c2bVozcW52S3QyelV6Z0xOK1Z3QkFNR1hHb2xtbXpHY1BxYVYrVUZ1T0hlQWMxREVvb2FSNlgzbVQrQWtoVHdUT05valkreC9ZVVg2UytFVy82bG56ZlRKZHgzNnFlTENRVDdUUElZc3MzZkxlc2ZTYlBBMytaQTJzWEFqMExDNjMwSmFJSVRwTlc2clZTQWN6UnhGdG5EdWRzanExMFA2QUFKSy8vWmpEdVV5dGhkK24yT3AxU1dHWnA5N3d3RGFFSmQvcDh2dEpjU1dnQWFuamU5dWpVUkNiZHhUYnRwa3A3QitoRUs5a1EzVGJRU3RRWXRWTW9WL2hwRmpDc05GVkZGVXhZcGY4dXdIMDN1cndyV010RlZZdDAyZ1orQ29nMVpkT0o5ckhpdVZMWnozQ2xEa21lbUttZXQ1b2xONWhGMnBVSE5ycFlxVTBVS2hCeHJWRUV4NjZXY0t5Q2xwanN1MHpnckh4bnV5dFVWL1V0STliTmNaaVlxQ25mcEdVbW50c3NtbTBLZWlKam1YRHh6VFVORElaakhRWFBMR0VjdFM1amxRZUcxMlR1OEQxT3pRMGRzMXBrdHRCT1V4UDJRMHRCWnNVdnRXSVZXYjZJV0d2RmMyS1Nob3pDR2dqZGV6czdFYlVSRHB2MS9LY1FxWWgrdGErZFBTc1JzYStPazB3a3RJM0N5T21YOU9CZUZVUEN5cEpXTVpNaGhqUUkydUpkVFM4Q29kMUZheTh1T3BzR0hKRndNRk5tR1FUdEhGRGl5WFZiYXF3UjY4Uno0MmVacDJ2SnA0ZUtrd0FaNVhwbzZYbWx4dmx6ampQNGN4eWNDdi9nWEIya0tSZjhyL0FUcDBIYXdUKzc1bTFIaHI0Qko2bElDTVdWVnFyN2RXSGlIRTNnMENUY3ErY0FvT2tEcjh3bmlMdDNkblNCR1VvZGtMNmNBMnNpd3d3K202c1VjNEtFbkUrQUo3SXJjRlBuVVR3S3BGaGsraGQyaGdTZ0xlcDMxYzVoT3VMQ2k1aHBwREpJZWI3VVEvdHI0LzhGcGpLVnA4a2hLaWUyc2NvRGMxTUVBeHhZQ2IwMDRZQWxBQjBSV2E0TUFFQWY3N25oa1JhdUQ3amxmcm5TS0wrOGVWRnBnSWJxcnVwL0ZWRGtOQmZtUy9FVkM5QkI0K2pYMnZIVVUrMXZwRGh4MGFUaWhTMGZ0S05MODZOdzQxTWxMa2E4WFBqYkc5TUZadzZzZzZidG1UeC9sMTRFOUtmYm5BT0E1MDJsc1ZqRDBQM0ZuSm1sOTRCc1dmTmczdkxkK2E4dkJSRTVZMFVIYUNRZmZEUmRRdThFbm92UVVoTDZQNlBaYktzV1g5Q2ozQ1lBdk9oK1ExamVEVVp1QlJOMzZqWERNbHpZd0d1TUVaVmxPYzcwWVl5cGhnTHpZRm1oalpybWF0TmNucTFOZTFpOFRJVkdnK0RCUThZZ0lsUStMZlh3Uk53aERKU3JCZW1zeldacE40Vm1pQTl0NU5tbEpiNS9OVjYwWElDLzFqUWpGaHBOMkZBbVgxZm1FSVJSRFlWSWVzUTZxVFhYS0M0d0QzakptaUtseHdvMzArWTBpMDRuL2xnSEdZbWZNeStxMnBDdVpQbStiWnR0V2FpMDNPVnUxRmwxcXJiTzVybEUrdUQ3TUZWZUZsN254VjZaUDREcm1PbHZlUGxjQ3VLZzk5RkhWVTJHeElQM2hJTlBKazZINlJDOHdUeFVXU2w0TXBYbWpwSU1nUHkrb0JqRnJuU3hoV2Y2NXM0aW9wVmRYQ3pmbU9mTkdrUzEwSXhXOUgrVVFmbGRCMFNlODhGVDdHL1dPSUw1SUxjcFRKNFdnQmJ6Vi92MFdiZ3F1cnNZZjVsdWdyTmtGSTRwWFZpTkZhMEVib3ZScFA5NkFlMy9JZzVVbWxKWENKekhLRkdSeGtRYUdNclBuMlpITzkwRHpCZG1UcTQ1Z1I2Y0trRmhpdlE5cmFMcDQ4QmpkSWhjYnlmQysxVjBHUVNhNlVvcFhZM3hsbEtyZUZ6aG96dFI4RUdqeUNCWXFwSm0zMU1abFZBaVQ4M2Fab2hMV1gvMkZQbDdZaGNFUko2QXRNc0FodzdKTVorMEZrbVRPZXFyQVZvYTB4eFh3c3JGNWRWb0Y4OTRITzRHR0hWVFNONkhVZGZhaGlqaHJzcktINjN5MnE3VHl3TDM0UE5qZkowUWlNRWY1RFZ6NzB6MWhFWThkell4VHhGa3hGQ0EwUFVnWU43UUhwUThnVGYzUkUrYmxPSXdIemR6Z2VINmhjTWJ0azNXcnJBaFhXbWF3SUtnd1dkTFlGbEdBQUFQQy9hZDNUUUFxYUdKWXVoZEZGaERNVTlST202dndFYkJVZHdrZ2JhSnFaTGdCNEpuV1lEZ09vVWxBRHQwellRZUhWZmErUmNORThrald6VCtFZy9kRUl3bEpIdG5IMzIxZXpYZCtFZFllRmhWK2dRMUp0cnN5RHhVdEp2Q0ZDdnFlVjF3dTR1UGd4VmVtUUhMWmwvVEpzZjJ6OU82ZHZKQ0VzK3VyWklTU1dWN3BhbWhkelFLQ3VqdXNUVWZvR3BNdDdkRkR5WjVGdmVaNEJ0TWdvTEIrTE85dDdKTU1KOHdUemFVWnlKUFM4ZG5XTm1HcERwb3diNnFaSzNqdHJWWW4xUVNtb1ZaSEtucS9KbGdZZDBXZy9hcExVUzM5czlYVGJneWVRZ3JlSnJFdTYvaTYrYXBXcWw0YUJtN2l1RWo0WWwxYW5LYXdYTTkwUU5BbEJJWDNNcnNiL3lnaHFScHN2ZDFSZ1dzN2lqU1JKRjlKa3czQm53YjAzeElJOTBLQTF6MWpkUFZ5UjZiQUYxeFMwNllweE9JTy9ZNVk0T2Fxekxnb3ROcDBwYjlLZkwzbEU3Y21BaTE0QU9QVkxvdGpLK1pURmxJZTdXc1pBRHBIV1FQd2tKYWJEQ0RCZWxzZXhMUlN4c0pOMmpQczlzcmZEaHhCWG5Ud0JyanlBTnZHald6WkVwcFVOTlkwWVltZkZhWXRYTmxvMzJCNmVuQVdVRlFrOGZVek0rd2lLM3hVQlVwdnlidkFWcnVZWWlST0w3RXdzT2RPS0V0SmJQSmE0dXN2SnpTc2pqOHdQd1JNeVdUbDk0UFdtdDBVclNsc3lSSVdDQ1lOL28vWUMyMTVwMGxkOXkzKzdUUlJkQ3NQdWdXMnNhOW5XUnIzZHk1b0hEWFRtTkl1Y0tWOEJpYnVpZ1RyaGY0cTZSUGdlMEpYOXRJSTlJeHp5Qk0vb0ZYVXhncXd3UnRCOGc1TFU2aWp0NmtnZ0IxamVJbUxKNGZ2TnVqYlJzeTFrZ3c0TjRZSmF3VWhHYlI2dWR3UWdCbGJzSVhXblBpdHBZMlkwTVM3Z2pMU2wxOHE5QlZiWFRBVE1MZzV4QWxzRkt4SkUxMTJQalloY2N2RVdJVjhhUkJBOEhnbnN0andiVEVxa1hweUQvNFY5ckRZeFpVT1c2UG9hbWFmYUZmcDNCMlhuWEMzWDFrYjYwVCtvQjJHczFTblc2dU1NbkZ0dlNTZUk5YXJOS0FHZ0NadEJXdXBZWTgxbDJ6dnJMeFhHR0VwUW1Zclg0L3htc212dnZSMDVXSG9FODFDYlVPTGt0ek9DRXQ4aS9wdXdEMlJsbGMxT3pvR3lzQ2xmVlNrMFhsTldzTVZSRGlJb3h3dnRmekl6eWFaRkdBY2NCSjdib2ZMZk93ZW8xekNKVG1EWTdUQVpac0J2MGZDaUhhZHIvMmt6d1NPMnJzWkphL3NJZzFWYkdsVCtFaHAwWGNaL0laam01NHBDa2F2dG9lZEdqU0Q0Rm5BRkFQWm9vMEVXcWV6THZwZzNNQzFvT05CbmlJVFJocnBrWFArOXhZQ0FmSVN1VjlsYTdYbkhkb09YYVRmK3FRODd0MHBlbU9yR1BDY2RpZTBSOHhDdVpOWEhCdWpDdUdYdllYNDZDY0pFN1Zhek1ZcDNjVzNwS0ltemRja0thTmd3bDVVQ2hVQnZLZ1VHcFVjdElwWmdOT211RnBaRjJhVVpHOTVTSGZaY0pXZENYVFIvM3hkRVhmUHBhS2RZa3EwSlczT2pGb3VNSEx3bm8vR29ZRkdUQUtCZWpSdmJmdTRvdUkvWkZ4OXQwazRrZ1BsWDRpQ0MvbTF5cTIwSlF3OE11UmFXOFVkTXF3b0swbGNuTW5CVjI1OXdNWXFaaDhtTGpNelRyYlJOaEVwRjVUNUplbzdEbndtRENOSjRRS2ZXbU9GSEE1QWFQTjF4T2RTMEY4WDMwRW9HWXVQRFlnTFVxekdyTW9ORUpsN0R5S1V2TXlocGpadnpOL3hvVEpmdXJZdlZjMlN0NDR2RUhGY2w5U2VieXZMVjFuKzlsQks0NlR3UnBFMTB4UVRwL3ZWRGlDMkh6QXRGZ25CTzQrMms0MGFWNUF0ZVVaUFRNOG53Z0FYa3lacEt6MWIzRDFHRlVOdEhOaXV2K3RYYkE3OGc1MmZMWkxaUERTcXFXdXcvV2tjRDRWRGNjRDZtQ29yVHlOV3huVDd1S3J1S3djZzBuRFRRTjQrSUppNFpHZzM4Y2w3NVVLVFdmRnMwNG51NVd1bDR0d0FSUnhvWUZlV01mZmkxaGhyTzB0US8xcjNIYnhaMU1jay9YUlNzK1NSKzZQMFk2NUpzTHFSSnk0QS9xbkhWOW5qSUJYSWVLZ3R1dncwQTdwYTZaUmE0SHdKVzJhOXVERGkwSkxxT1owS1VtMXgzODErTVYyN0hlRVpVYmxoaFJxaDRqc0NaUkJINndXbWtuUmRCeS9BWGRtZDc5YTVhaEgxem85cGxUVUh3WExSSjdxSTg1Ymd0S3Mvdm5lbzhtdWtSUk5HZzN3Mlk1aXBMRWxJS2pENzhCV1FsWUp6NFVXK0syajlvYm1YQWt2djYvU015NVN3SkFyQVQxOE5JcVBHUU9LVGZoQ1BvNmNsTTJkNXoweFVlRzI2MkZ0dHl1Y3Z1Z1RjTjZBQXdTSnBqV2l1a3J2c0RPS3dHUnFKQUgwbEhmMTY0ejhBVERmbjZDcVBVNnJBUjhXMHE5NUo2VTFlRkVaOHd6ZkpianBJdWQ0QnZ4eFpZQnpaWUdSTk1uS1V4eVZ3d0JyY1paUUhIZDNhREpQQmhjcHRPNHRKSUhCVmlRUDRzcHJZczFUN1VsM29wQkJ5UWZwT3dUclN0TnNtckpMZnZ3Y3RNWDR3Wmd3UGpKNGxkaVJoamt4c2sxTUtuekFoN2JOaXltTzB3citJMm5nZklxdFl5Zkw4YVJ1dzhaV0ljYUtjaVFMenlBa1k5Mkh2WEU3ZUdqUUNCV0FXZkI1bFZhSExGd0pWQkttaVlRUG1BY2NueFcvZG0vaTVzb0g1T1JodVBQREtML0tHUkdTckkyU0psTE1BYTUzdUhqOFZTZE1vNk1BVmU0MUw4V2RtVHJNVzJHajZqbVhoWHc1NzR6dWFLVTd3cmM3alhGbDFxczBnR3RTOGxrd0VqN2tZWE1qbEl0aGhqaUExT0VPSUt5bnlOOWVvRWVpUVV1a2hWQXVOR0MybERFTkNoRCtGOG1lMjQ0bW1peEE0K20yVjA3OG1McmwzNkNWdHRkT2U1RTAyRk1RUGx0WGpSL2oxWE1XcEZaZWRkUnd1bVplb2ZtMG5kbklsUTl3WlhuN2N4SWtUNlI1a1JvT1B1WmpjZ0dhZldXRnA2aG9pNzFoMGp0QWIwekNmc0xWdjB2akx0WHpYd2ZMVU1tSUdFK2tzUVRXbVc4dnB5clg4cnNqNXp0YWZDZk5QalJ6MkhiQXdndm45UWNxZ2dJQzF3b1lwUThrVlJ3MURuOGxLNkk0VlVyT21BZ1cxZDJsN3dvQnZGSjJDblVrWnUzMUN2eE9LMEw4SjRMRW1pZHdqcWZGOUs1MVN2UXU4cFJ3NWFFd3QzdnF6MmMyNFh5L1BBdklCYWlJU0pmUG5LSU9DeUE4RzRRN1c0eCtTeU5WQWpHSEp4M3d6dEpWL0k3NXUrRVFPaE5SbVp4K3dPRVVHdDl6QXBhbk9mNklQQTdyV2crRkRBYmUxTEpiWDBpcTRqSVRuQ2ZqWjVHaFYyNzRLUWdtYnZIVUl5dnkvalRONnJUcHFha3NjQXMvNDNRcTVWUUs5dTBwU0pVNzMzbWVzeVFOVW9OWmZmWjRTMHZkRnV1ZmdTWVIrME02bGR6MUNEaHl3N1NkK3VIOEVSdnhjaE9lTStNdkxnSDNoZXFZeFlkK1dxaFcydTNXWEZybW1WYTh6a0lHbGFDWjVPZzJxZDh1bkRZMWY0NUhINkd0ZW9LbnlGYytVaG9xR3dVbWQ1dUFOZ3RTSkN2QU5XSXZKQWgyWHBxdlhWeTFNbkUvYWNMdXJCcFlhUmVnUE4xQm5VQnJ3VEMvcW8yZUlNK0h4UWRNNHllYTB6bFBheWZxdXhBa1FhMjBrR2pmbjBUSDBDQ3dUc3RqZTlaUkE5Rzc2UzlaN3dRa2R5UGVNM3BITzYxNDd6Z1FWL3dxVENNeWVOTmxIdFlvVFFBSDB4aG1xMWY0NU4vMGpzcnBtZkVCRVJKbk5uWnRFa0xYTWhuZ0lmN2gzNzR1aU5RamFqT2VlVWRjaEkxZ2JDVkZPa3pQNzNjMTFJeCtVanRWZFYxcHJJZlVab3F2bVUvU0xPZEZDRWF5TnVEV1Z4MWhkSU5LNmhCNVduUDh3NlJoQXA0T3FXd3hyVWtKemZTUU9nanAxNHhmSnZqb1Z1ZXV6bno4TFhDOXhFZEZwWGJNSC9mZkxESk4xN2k4STJteXhEamZ4ZlNHOHdLdDVYdlpubkljZmEyUW8ybG5ja2pmY3k4eWdsb0tDeWhjeXdHQ2hJM0J1TkZZNzlydDV6UXpnU2dHb1B3SnJRWHR4bzFlaWtrTjlTdVlyQUt3VGhYZU0zcEhlZEw2b0VySXFqSlJnRVNrSGdocjVzenhDR09adHVkZUZVeGhNam1nblR4cnpLVlI1WHVaSFFOVkI1N3p6RFBqSk9ya3l3TTJGbCsyVnBzZ2Q0V3RlRmE2MFp1c0IzOWlwVjFFeW9iM1V6dG1lUUN1bE9aeEhJK0hJZllpcmRTV2tqeEZqcndZbnBldFJnWVpnNUt1VVZ3d3FtVVgxZlloazdZWjBrRzY1UDhySWt3MUxQNjY0SkZtUm5hT3hMdEJnMUpkQ1BqME1Lelo4MEVKd01lR3kwRjRpeEFLQUJKREFjWkRiY3hCUjhBN0FOZnFFN0FKZ0FsZm5KMHhjNjhkUkx1Z3RRcW16WTVFQTFaeUFaM0ZlVVlPUTJlSDhGeEZaMWRzdjVuNjRlSGp1MmgrS1RTTFdsN01qNVZZaCt0QldxZzZKUzA1UWdrVDZaRHVxWTZPU2svTDRwSGhZalFZbEk2RFR0V1cxbXA3Y3Y2Qm1hS0JnNG5DaVQ0a3BjUk94V0E4YnVLaGltSkg4QnZ6SStHQVZaaHRXdkQ4UGcwUm15WnhOOTd6V1pyb0RYTW03ckpiMWc5NE0reDVBOXlDRVd2ejFVZFFxRFNBamVPSFo5a001THhwd0VNVkhoS2JtQjVGRENrSlRacnFJQnM5aXlNUjNSV2VKYTRpbURHam5WVUJEc0VxWWJvcS83eTIvb2dGaDhwWEVUbXNRNGFoZXFSaGhyaVRpd0dVWXlJU1B5K2tFb0lVWnpibUw5NzZpYkN6aHFFM0ZqY3llRmRoS1EvclAwaU1HaW5GaEZpNkp2YWQ4eUdMRFZ6MWlhUDJacnVZSy9JNmxzSlU1YTJlUFVUR0F4Lzh4akh1UW9BNEkyNlpiY1NtVmcybWFBRkRQUWVSUm5ETkdBWEFOWnhPUE5iU0crY1R0WkpWYk9TdHRpbFZWQzBqNFRKM3ZuSlhCUjhHUnNGKzZ5enJ6ZWV6ZGxtZHhyL3Q2VkY4RG1OWUczbG1BTlVKZDltSVI1L1ZWc3M0NzlqL280WUlRZUxQR1U0VmFhZEZKTUF1K2F2SlA2Nm5XWmlRcm1KR3lIUkcvRHNWdjBGM01WVVYycGhHclR0V0hiZWh5K214SVZBV3hIamEvMW5qZGFRMytjcG91WWpCcjFGSEljMUdKZStxblYwZCs4N2NMdExobjJuUjBxRkprTkpVQ0xSNzJDS1NnQmgxVnFHQnRvS0R2eWc3ZCtoeWtYM2wxUzR5RDNOclIzOHhvSlVHbHM4aWJOUkEwcUZReEpQYTgyZmNFUkFQUlJTQ0pTMG9DZXFsaWk1akN3SlE1N2JrZ2FmR0I4QmVnK3Jsc1VhdllKNGJDOWtTM1FISlRoT2hMdDF6VHIraFRVdVhuUU9Sek44eGpqVXRSUm90ZitmV3ZUeVRIckVXa1hNdEVaN0phSEpPWFlSa3RTczNWMUNwWWVsa3UxZkVSeURSSWtLTHptamswMCtHR2dtUVRnZGkxNE9MdEZIZVpwNFYzZEY0UzBFcVhxNStFOVl2cXpZZ1RyTGNqMGtiaFRueld3dmU0M21Pa3JEUnMzT3ludkRCM3JWelptelVnV29jQUFyQ29CaFZMR2J2MDdyczJKWkVuY1czc3pnUlZYVkt5TytUWnU0TjNzTW1VSkhSNVRzdWhFZWsrRENzZjl4OExwRmxuc1BIZUtBMzNyQlJ3MmxQUm1zSHgxaHd6NWtWd3VVMnIxazVOUU90QkxaUlpCUVE1NExDeEQ5YXpDYXNORjhiOXVzRGJ2MVdlVHZDVkdpZTByUnhsUDVTZTJZVDVqenpwa1RMWTN4QTFSdnVzbWJla3FIQk9OR1o0SHdkKysrOXhGWnhlMGliaUcwMjg1RVpjbXZWUzNINFpKSk5VdEpXWTV2akJYZFNKUWo4YTVBVnpYNWo1S2RvSnpCVkhpZFROQTBlbEk0SzNzQkFvd2FKKzBKSWcrWFJCQ2ZTRUtZNnVwbnJtQWpubm9hZkhKYzJOMlZOQWx2ZzBEMHdyekQ4ajRPN2d3ZTArT0dxcDFmeVNwbVU3eCtiQU9nM0JXaTNBcXlGRzlYbkc0QXRxSVJIQklTbnJSY2pMTlI1ZWZvR1NRNWsxYlJBT1YxenJxRTNFM2s2c3pSR1BrOEdEN1dCS1h0WURLZ0tyTmtGcklNcklGUHN5Q3RlNFNNb1hxbWpTaURkaEhsS3IzcGJuNllyRTFRMWlSZldEOXJKZVZ1WmFKajNTemlvcFdIKzFXL0Z3dlBBUitiQSttaFBkNVprSEUyUUVNZElFL2pBTUtHT3FKVkFyRVhBUzBqdXBNSXZBQUl0RlJmSFVQNDF1bEJNNWF6L0lBaEZSdFJtbFpLRzN3cXhTb0FPRXFDS3NHL2IvTnVUNTBOT015VjVrTWxVU2N2anA2YUZ5ZmZGYmRZYVpDcCtvR2kwZ2IrYXl3WDJSVzh0WU5EUy9mK0hhSHFRZ2toWi8wRW1DdStKOCtkZ1l1V201L1RER25kM3BEeHRjK3RSeEZTYU9jdW1qQVRKWi9PeGdHcUdrYjBSREJwR0V1dnQ0WFFpdFRjMmxEUHZmSjI2Ynpmb2poZjNSbEZYSjNKY0hiVW5JdE5wa0V2aFNlVk1Lc2hDaGdKRWd2amtiV1dDSHpKcXBGVGdLVncrbFpHY242a3c5UWFWd29ScFcvb3hWaUoxNThva3RySkxyNTdHSHhyNDFmaUJydXRjdldNelhRa1A5dEZNMEVTU3V6OHNnUzBFeUc4UkQwa0FhNnBVYkJYeVNpQmVTdFVRd1pQZmZVcXR4NFVHN3ExOVJhZlRRTjRkbHBjV3pyOHRCTy9YNFhZWlhGVjRRTjk0WEE4SnlOeVpDMHBpSWdHOEczZzU4K0E2cEtZRS9nTk8zb202SEd4VDdRQUFBQUJKUlU1RXJrSmdnZz09IiB0cmFuc2Zvcm09Im1hdHJpeCgxLDAsMCwtMSwwLDEpIiBoZWlnaHQ9IjEiIHdpZHRoPSIxIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJub25lIi8+CiAgICA8L2c+CiAgIDwvZz4KICA8L2c+CiAgPHBhdGggaWQ9InBhdGg1NTIzIiBkPSJtMzEwLjE4IDIwNC41M2gtOS4xOGMtMjEuNzM5IDAtMzkuNDI0IDE3LjY4NS0zOS40MjQgMzkuNDIydjUyMy4wN2MtMjcuMzQgMTUuNjQ0LTQ0LjUwOSA0NC43NzUtNDQuNTA5IDc2LjgyNiAwIDQ4LjgxMSAzOS43MTEgODguNTIyIDg4LjUyMyA4OC41MjIgNDguODExIDAgODguNTIxLTM5LjcxMiA4OC41MjEtODguNTIyIDAtMzIuMDUyLTE3LjE2OC02MS4xODItNDQuNTA5LTc2LjgyNnYtNTIzLjA3YzAtMjEuNzM4LTE3LjY4NS0zOS40MjItMzkuNDIyLTM5LjQyMm0wIDE1LjVjMTMuMTU4IDAgMjMuOTI0IDEwLjc2NSAyMy45MjQgMjMuOTIydjUzMi42NWMyNi4xNiAxMS4xMDYgNDQuNTA5IDM3LjAzMSA0NC41MDkgNjcuMjQyIDAgNDAuMzMtMzIuNjk0IDczLjAyMi03My4wMjIgNzMuMDIyLTQwLjMzIDAtNzMuMDIzLTMyLjY5Mi03My4wMjMtNzMuMDIyIDAtMzAuMjExIDE4LjM0OS01Ni4xMzYgNDQuNTA5LTY3LjI0MnYtNTMyLjY1YzAtMTMuMTU4IDEwLjc2NS0yMy45MjIgMjMuOTI0LTIzLjkyMmg5LjE4IiBmaWxsPSIjMTAwZjBkIi8+CiAgPGcgaWQ9Imc1NTI1IiB0cmFuc2Zvcm09Im1hdHJpeCgwLjEyNSAwIDAgLTAuMTI1IC0yNjguOTggOTUxLjcpIj4KICAgPGcgaWQ9Imc1NTI3IiBjbGlwLXBhdGg9InVybCgjY2xpcFBhdGg1NTI5LTcpIj4KICAgIDxwYXRoIGlkPSJwYXRoNTU0MyIgZD0ibTQ2MzMuMyA1OTQxLjloLTczLjQ0Yy0xNTQuMzcgMC0yNzkuOTYtMTI1LjU5LTI3OS45Ni0yNzkuOTZ2LTQyMDUuNWMtMjE4LjExLTExNi4yMi0zNTYuMDctMzQzLjIyLTM1Ni4wNy01OTMuNyAwLTM3MC45NiAzMDEuNzktNjcyLjc1IDY3Mi43NS02NzIuNzUgMzcwLjk1IDAgNjcyLjc0IDMwMS43OSA2NzIuNzQgNjcyLjc1IDAgMjUwLjQ4LTEzNy45NSA0NzcuNDgtMzU2LjA3IDU5My43djQyMDUuNWMwIDE1NC4zNy0xMjUuNTkgMjc5Ljk2LTI3OS45NSAyNzkuOTZtMC04OC41OGMxMDUuMjYgMCAxOTEuMzktODYuMTIgMTkxLjM5LTE5MS4zOHYtNDI2MS4yYzIwOS4yOC04OC44NSAzNTYuMDctMjk2LjI1IDM1Ni4wNy01MzcuOTQgMC0zMjIuNjMtMjYxLjU1LTU4NC4xOC01ODQuMTgtNTg0LjE4LTMyMi42NCAwLTU4NC4xOCAyNjEuNTUtNTg0LjE4IDU4NC4xOCAwIDI0MS42OSAxNDYuNzggNDQ5LjA5IDM1Ni4wNyA1MzcuOTR2NDI2MS4yYzAgMTA1LjI2IDg2LjEyIDE5MS4zOCAxOTEuMzkgMTkxLjM4aDczLjQ0IiBmaWxsPSJ1cmwoI2xpbmVhckdyYWRpZW50NTUzMykiLz4KICAgPC9nPgogIDwvZz4KICA8ZyBpZD0iZzU1NDUiIHRyYW5zZm9ybT0ibWF0cml4KDAuMTI1IDAgMCAtMC4xMjUgLTI2OC45OCA5NTEuNykiPgogICA8ZyBpZD0iZzU1NDciIGNsaXAtcGF0aD0idXJsKCNjbGlwUGF0aDU1NDktNikiPgogICAgPHBhdGggaWQ9InBhdGg1NTYxIiBkPSJtNDQxOC41IDQ1MDF2MTE4OC42YzAgMzAuNDYgMjQuNjkgNTUuMTYgNTUuMTYgNTUuMTYgMzAuNDYgMCA1NS4xNi0yNC43IDU1LjE2LTU1LjE2di0xMTI4LjRjLTQzLjMzLTcuMTQtODEuODktMjguOTYtMTEwLjMyLTYwLjE0IiBmaWxsPSJ1cmwoI2xpbmVhckdyYWRpZW50NTU1MykiLz4KICAgPC9nPgogIDwvZz4KICA8ZyBpZD0iZzU1NjMiIHRyYW5zZm9ybT0ibWF0cml4KDAuMTI1IDAgMCAtMC4xMjUgLTI2OC45OCA5NTEuNykiPgogICA8ZyBpZD0iZzU1NjUiIGNsaXAtcGF0aD0idXJsKCNjbGlwUGF0aDU1NjctNykiPgogICAgPHBhdGggaWQ9InBhdGg1NTc3IiBkPSJtNDQ3My43IDE1MzEuN2MtMzAuNDcgMC01NS4xNiAyNC42OS01NS4xNiA1NS4xNnYyOTE0LjFjMjguNDMgMzEuMTggNjYuOTkgNTMgMTEwLjMyIDYwLjE0di0yOTc0LjJjMC0zMC40Ny0yNC43LTU1LjE2LTU1LjE2LTU1LjE2IiBmaWxsPSJ1cmwoI2xpbmVhckdyYWRpZW50NTU3MSkiLz4KICAgPC9nPgogIDwvZz4KICA8ZyBpZD0iZzYxMTciIHRyYW5zZm9ybT0ibWF0cml4KDAuMTI1IDAgMCAtMC4xMjUgLTI2OC45OCA5NTEuNykiPgogICA8ZyBpZD0iZzYxMTkiIGNsaXAtcGF0aD0idXJsKCNjbGlwUGF0aDYxMjEtMykiPgogICAgPGcgaWQ9Imc2MTI1IiB0cmFuc2Zvcm09Im1hdHJpeCg5MTUuMiwwLDAsMjUzLjIsNDEzOS45LDYyLjkpIj4KICAgICA8aW1hZ2UgaWQ9ImltYWdlNjEyNyIgeGxpbms6aHJlZj0iZGF0YTppbWFnZS9wbmc7YmFzZTY0LGlWQk9SdzBLR2dvQUFBQU5TVWhFVWdBQUFYMEFBQUJwQ0FZQUFBRFdTZk5vQUFBQUJITkNTVlFJQ0FnSWZBaGtpQUFBSUFCSlJFRlVlSnp0blh2d1pWbFYzNy83M1BsWGNZYUJZUmdITWp3RVpCeFVZa0lrRXNnWUpKQ0NDQVpJYXBKTVVMUWdFZ0ppRWFPeEtLTXhGZ2tsWlJKS3NUREdKQll4OFZWQ0JKSm9JR3BDb0ZRRU04K2VtVzU2K2pIZDA5UE1qTk5POHpzN2YreTc5MWw3N2JYMjR6enU3L2ZyUHF2cVY3OTc5bDVydis2OW43WE9PdnVjYTZ5MUZsdTU4Z2xmZ1NYbHpydVB3QmdEQURDbWd6RUd4aGgwMi8rbUc4cThoTEx0YTJjYkgzZEUvMUtYa3krOXdiM1lUam1hdXJIa05UT2t4eDJyeXRreDNXTGJ1VExlVjZYTkpTR1phVnVyVEZ5eXFTbnI4L1ZSZnpuZDZQVmdZNW5PVjN6azg4Tmh6eHYwTnVuQWU2Rk0wcFBMK2dvZG16OW1ZN1hnK25xZnR0ZmI1dTJBMnBHcW5yVHhEVi8vQWl3cDU4NC9IRjZidm85SGY5VlhmZVdpbmQ5MTVCN1g4UXI5eVhMeXBUZEVvRlFkQUJBRE5RUDJCTXJTMG5KSG9DMS9hN25VdjZwWXA3YTRWQTVYQlh1dWpWcm90MENlNi9QMlJvQStNbCtoM3dUOXBXRVBBQTgrOUtYb09FQ2Z2Z0ZYWC9tRXhRZHk5ejMzcmRDZlViSU9BSmpuTEVDekYreFV2Vng1cVk2cTFUcUhCU1VMOGtoeFpIME40QVc5TE9TNWZnSjlIZlJmK2RFdmtEcDUwQ3YwNjZIL2dwdHVTc1k4cDV3NWR6NDY5cHcwWDk3YmkwYm9GK0pKVDd4eTBRRUJ3SkY3ajY3UVgwQzRBd0FhemdLazR6a2RnYVpiVXpkRmR3bXA5VHM1dlZ4ZEJlQ0JSc2p6WTJhYkEzMnN0MEpmYTdzRS9hVmhEd0FQbkQwSFlHQWtGZlBsdlQycnZsRUFucnd3L08rNTc5Z0svUVdGWHdNQUNtY0JURmM4Rm1CZTdRZ1UreXE3cWJwelNzdUpSaXZZTXpaRndFdTJ0ZEg4VnZjSi8vV1B0M1g2d0Zmb2o0UCtUViszUFBCUG56Mm5uMlIzSGN6Rml4ZkRDSlAzZnp1UmE2NithcUhoRFhMMDJQRVYrZ3VMNUFDQUdad0FVTzhJY20xazJoclZ6bExTQW53TjZvVzIxTlJSS1lxWHlnclJ2SWQ4T29ZVitscWZyZERmQmV3QjROU1pCeU4rQXNMWDkzRUNmVUIvTTNjQi9tUEhUNnpRMzZITTVnUzBzZ3k4WjkzRjArSWtwa29KNGx3S0RxSWE3bHBiWWxrRzhsc2JEZlN4M1FwOXJjOFc2TjkwNDQzSjJPYVdVMmNlVk9zU0ovRDR4WXMyOStZQ3crSTg1Y2xYVHg5ZGhSdy9jU3E4WHFHL3ZQZ3YzK21YUGNNVnREb0J3YVpZWGdCMTg0WGFYWDBFR29lVnZkaWJjeUFqQWUvNlRPMXFJSisyczBKZjY3TUcrcnVBUFFDY1BIMEdnSnkvNTJLTWdmblR4eDhYMzcxMFFRYTVkb2Z3WDZHL3ZFaGZQcURCQ1FDeUl4QnNpK1ZlUmtUdlMrL29xZDZ0NDJWa2FrZXRxd1E4QUh6VngyNGJpaFFRbDJTRnZ0NW5DZnE3QXY0SkQzeWhUbk1DRWZSem9PY0w5dFJybmpSdWxJMXkvNmtIVnVndkxCcjB1UVFuQUlpZnNpWm5vTFRSVk05bHlUVFBHRzZXbGxXclY1eUwrRGJaR1BDaTNRcDkrWGdCNkgvZGpjOVB4ckdFM0gvcWdhUXNTb3Z6T3VJQXpJVUxGK0tjUG4xZFdLVHJycjJtY2FqajVlVHBNeXYwRjVKYTZFc3l5aEdFeWtLL1k5N1dKVDRLWTVhbnhxWUY3dHMyci96NDdZSitYWHEyVlZibzYzMUswTjhWOEduNjJ3dVA2ck1PNE1LRkMyRnU0c0lKYjJKMEttTXRyci91MnZvUlR4Qi9zV0tGL3J3eUJmcWFhS2toS3NXM2JVcTZacTZQeE5naEZGSkIyU1hmMWwyMUJYenhoR0dGL3I1Qy84Ym5mMjNTOTFMeXhmdFBodGY4QW0wb0x6Z0E4eWVQUFphc1ZnM291ZTdUcnIrdWN0alR4ZTlEWGFFL2p5d0JmVTFLWndaY0pyMjFVM1A4clRsOGF0cVEycmxLaU40VlZhV3ZGZnI3QmYxZEFmL29zZVBoZFpTcUNjOHlxM2NBQWZwalFFL0wvQnQ0dzlPdnI1ekdkUEczR2EvUW55YTdoSDVKSXFjQVRJN1l4M3cwSmk4SHMzL2lmN3RUVnF1RThRcjlnd2Y5NXovdmVVbC9TNG0vZ2RVTDM5TE95MHNPd0R6NjZLTnhUbjhFNkhuNU0vN00wMnJtTW91Y09YZCtoZjVFT1VqUWJ3SFVBemMvcTc3aG1vOUk1VEpvRUcrVkZmcUhEL3E3aEwxL1Bwa1gvM3FxQXpDUFB2cW9uUVAwL3JWL001NzFqQnVxSmphWDhDZkpyVkl2aHhYNlZlMU50Vjl3YlZib0h4N29mKzF6bjVPMHY2VFFweEY3R2VNQUpCdnp5Q09QV0FuMHdMQkl0YUQzWlZUbmE1NzF6SW9wemljci9OdGxoWDdHZm9WK1U5MmxDUDFkQS8rT3UrNEdnUFRwQklJRG9QV1NBeENqLzRjZmZ0aEtvQWVHTjBVOUU3RHhtUUQvRDdpRmZlNXp2cVptcnJQS0N2OTZXYUdmc1YraDMxUjNLVUgvZWMvWkxleHZ1LzBPQUhxVVBzWUJpTkgvbDg2ZmR4ZHlNMUg5R05EVEJiUjI5L2t3WUFWL3JhelF6OWl2MEcrcXV4U2d2MnZZQThBZjMzWjd0T25Bd0loUnVuOHRPWURhOUk4NS85QkRGc2hIOWJuMFRkRHgrWDk0RzBTMnU5N2lSR1dGZjE1VzZHZnNWK2czMVIxbTZPOUhSdUtQUHYrRjdXUGxZNGpYT0lEMGYyWDBmKzdjdVNqU255dXFIK3FaazRERkxoODFTbVdGdnl3cjlEUDJLL1NiNmc0ajlQY0Q5Z0R3dVQvNkhHQTZHTEFvdm9zaDdybnQ5VnJTUCtLRjNBY2ZmSkJCZjF4VVh3UDZvWjNkL1Q2a0pDdjhZMW1objdGZm9kOVVkOWlndngvQS8vMC8rRU4wQWV6YlFnOXBEQkg2Mk9pL0NQK3paODl1b1Y4SCs5cW9QdWlGc3dWMzJQYzJPWU40NFRkK283ZzRTOHNLZnljcjlEUDJLL1NiNmc0TDlKL3o3SVo3UEdhU3ozejJzeUtVdTg2d256TnRpLzZiVXo5bnpqd1FWb0xDdmphRjQzUDFFZXlGcUY1ektyMjNCL0RudituUGlvdTF0Rnp1OEYraG43RmZvZDlVZDlDaHZ4K3dCNEJQLzkvUGhOY084aW1VcGVoZmc3KzNhVTM5R05NNTZOZkFYcm93cTZWd3BLamV0MjJ0RGFCM2RxNU5heTFzYi9ITmYrRkYrc290TEpjci9GZm9aK3hYNkRmVkhWVG83L3ArSVMrLyszdi9HNlpqME4zV0RaQTNTVVFlUmY4VFVqOFMvTTNwMDZlaW5ENlF3cjQyWDU5TDRWRFllMnNLZXkvOXR1eGJYdnhpWlJtWGw4c04vaXYwTS9ZcjlKdnFEaHIwOXd2MkFQQ3AzL2tkR0dQUWVXaEhrTytpSjRQNDZMOGw5VFBjZ0VVY1NnMzhUNTA2T2FSM0prVDJBZWdFOWxJS0o2cmZsbnZRZXlWcSs1ZGU4aTNTZXU1RUxoZjRyOURQMksvUWI2bzdDTkIvOWpPZndkVjNLdi96azUrS0FXNFF3UjlBaVA3M0EvN20xS21UZGduWWF5a2MzeWFRaHoySXpjdGUrbEo1ZFhja2x6cjhWK2huN0Zmb045WHRKL1QzRy9hLzlkdS9EV0NJNVAzcjhMOEFmd0JSNm1jTS9HdHkvdWJFaWVQa1F1NzRuSDB1c2xkaEh4d012YVlRdDJmN1BudzQvc3EzZnF1MDFqdVRTeFgrSy9RejlpdjBtK3IyQS9xN2ZyZ2psNDkvNGhOYm1MT0ltc0NjbHUwMy9NMkpFOGZ0bE4wNGM4S2U1djg5N0tPeTdkOWZmY1VyOHUvQ0R1UlNjZ0FyOURQMksvU2I2bllKL2YyR1BRRDg1c2MrQm9CQzNneDN2bmJkSlBnM3AzMHF0M3FhKys4L2JxWG92bVhyNVpTY3ZSYlpTN0QzWS9UbHIzclZLek52eDI3a1VvRC9DdjJNL1FyOXBycWxvZi9NRzU2dWptbVg4cEdQZkpSQk5kNkpzeXY0bDdaNml2bis0OGVQMmJuejloVDJYbjlPMkhzbllxM0ZhMTc5NnVJYnRBczV6UEJmb1oreFg2SGZWTGNVOUE4SzdIL3QxMzg5Z201bnVpcjRhMm1mWE01ZjIrMlQyK2Rmay9JeFh6eDIxTTZWeXBHMlh1WXUwUEtjUFFWOXBMOTFTaFQyWFArMTMvN3R4VGRzVjNMWUhNQUsvWXo5Q3YybXVybWh2OHRmNGN2SkwvL0tyd0NJWVY2Q1AzY0FIdjV4ZWV3TVBOazUvR3Z5L2RvZS95VHFQM2IwUGd1MFIvZityWkdpKzF3cUo0N2VoK2krQnZaMEhJUFRzSVBUZ01YcnYrTTdLdDdDM2NoaGdmOEsvWXo5Q3YybXVqbWdmMUJBRHdBZi9xVmZjbUNGaDNZSzh3SHFCT2hkcXVPUEtmeW5wbnlxb241K2M5ZXhvL2RaQ2ZpbG02dm1pTzV6cVJ3UGUxN1A5WHByWVpIVy9jMDN2S0gyZmQySkhHUUhzRUkvWTc5Q3Y2bHVDdlJ2ZVByMURhTmJYbjd4d3g5T29jM2dMMGYwY2RSUDYycFRQalZSLzloY3Z6bDYzNUd3OGxvNloycnVma3AwTDZaekdPeHBIYXpGM3Q1ZyszZis5aTMxNy9LTzVLQTVnQlg2R2ZzVitrMTFyZEEvYUtBSGdGLzQ5LzhCeGhoc050c29tNmQwUUMvU3B1bWVVc3BuYXRSZnl2VVhkL2g0NkplQVgwem4yUGlzSU5LdHlOM1hSUGM4bFJPMXNZVzladnYzYnYyN2pXLzk4bkpRNEw5Q1AyTy9RcitwcmdiNlQzL2FWNDhhejlMeWIzLyszNG1wbTgybWk4QWY2b1dvbjl1MlJQMVNmbjVzdWljSGZuUGZ2UTc2SmVCbnQySTJwSE9TeUJ4MUYycWxWSTdYNGRFOXI2ZDl2L2s3MzlUNldkaUo3S2NEV0tHZnNWK2gzMVNuOWZXMDY2OGJOWVpkeU05KzZPZElkSzVIN2h6K3RWRi9LZGZma3U2aFp4RnhuL1Y1Zm5QZnZVZHNEZkFwb0FFZCtMbXRtQzNwbkFqOFd1NStxeU1CbjhKZUdzOWJ2dWU3UjM5SWxwWmRPNEFWK2huN0ZmcE5kYlN2Z3d4NkFQanBuL21nQUZxVGdKL1dTK0FIOUZ4L2Jib25janBDdXFjMXo2ODl3YlByRE13OVIrNjJCeEg0OFJtRkRId1ArNmhQQnZ6NGpNS1BwdytPNUcxLy82MWpQek03a1YwNGdCWDZHZnNWK2sxMTExOTM3YWkrZGlVLzlhLy9UWUF6c0FWaEFsb1N2UXNBcCtDUGRETGc5M3E3QnI4VThac2pkOSs1emVsUFMra3NBZnhTL2w0OEt4Q0FIMTlQR0lEdjV0dWp0ejNlK2ZhM2ovNGc3VXFXY2dBcjlEUDJLL1NMZFFjZDlBRHd2dmUvSDUzcDBKRjk4Z09nWmZBSG5RejRBUWJ1RWJ0N3hvSy9KZFVUNWZpUDNIMm41Y0FINGloL2FnNy9JQUNmUG1xQ0F6KzAwMXU4Ni92ZU9mcUR0V3VaeXdtczBNL1lyOUFYNWF1ZitwUlJiZTVhL3NXL2ZCOUF3T2NCNmVHZkF6OUFkdGpNQVA2V2lELzNDQWNPL3RhTHUrYnV1KzRJa1g1dUgzN053OUkwNEh0OVlMNlV6aExBMzdOQTc3OGcxdUlIM3YzOUV6NXV1NVVwRG1DRmZzWitoVDZBd3dONUx6LytFKzhGakVIWGRkZ1lST0FIRUVYOWM0Qy9KZFZUbStPdnZZTlhTdk00SFJuODV1Njc3ckFsNEFQNXRJNmNScEZ5NmpMd0kvM01SZHVXSFA1VTRGdFk5TnYrL3NrUC9zRFV6K0RPcGNVSnJORFAyRi9HMEwvdTJtdEcyZTJuL05NZiszRUh3YzMyOFFNSytGc2kvdHBVVDVMS0tZQi96SGJPMm4zOFVyUWZiTzY2ODNZN05xM1Rrc2R2MlljdkFSOG83OUtoNDNEamR0QXZBUi9BQUgwR2ZHdXRXNU8reDN0KytJZW1mU0wzVVhKT1lJVit4djR5Z3Y1aGhEd0F2T2RIZnRTQkVmRnVsUnJ3ZDJUM1RnbjhuV0ZRejRDZlExK3lLYVY1NG5ITm1PYWgwTmVBRDZBNnlwK1MxbW5KNDFQZyszNmtLRjhEZmhnYmpmSXp3SGRyTWJUell6L3lubW1mMUgwVzZnUlc2R2ZzTDJIb0gxYkllL25CSDM0UGk5Umo4QWZvQTBYd1UrZzc5VHo0cFhSTkNmeHpwSGxvNm9icTFrVDd3QmI4ZDl4K1cvakU3Q0xLMTlJelM2UjFvdXNEcGJRT0FYN1VEeEROcmU4SCs3N3Y4UlAvN0VlbmZYSVBpSnc1ZDM2L2h3QmdoYjZvVjZxdmdQNWhCN3lYZC8vakh3cVFObHVRQXl3aWhnQitCbjFRblFYeSt4VDZBSnJTUEVtYWFPNW8zME9mQWg5QUZmUWw0THU2dWx3K0JiN3JLdzk5RGZoUm54VlJmbXRhaDdaQisrcjczdDNKdkxWNzMzdi8rZFRQOUlHUy9YQUVLL1FGdlZJOUcrTlRyM25TeUJFZFhIbm45LytqQVBDdU13bjRKZWhqKzc4MnplUGJBRkNWNXBrcjJ1OG93Q3VqL1NtNS9TdWtCUllmazF3cEhQaTFvajB0czZsUGR2RzJKRnFVWDlNWEJ6NEF2T05kNzBiZjkzai8rOTViUGZhRExGZGYrWVNrN0tDY0Vhemk1Tm9uWDczZlExaE0zdmFPZDZFajBid21mZCtqNnpyMEFMcStCN29Pc0RhQU9DZldXdlRvZ1I1REc1VjJYR3I2azJ4NzlGR0VYbWZiUitDdmxiNjNNdlRUbmpBSzVJQ2MycWszanFQODFuNTVoRjdYWlJybDE5cjFXLzN2L1lmZkY5SkFIL2lwbjJ3YTkwRVh5UkVBcXpOWVdpNWx1SE41Nno5NFJ4YjA3aHBrSDZWMmFxWGZPb1ZOYjJHN2NhQ3VkUXpCRmhhOUJib2VRRU9mdEcvdjBFekJBVXEyUUErUWFMOE8ra2xESXdCT0JyRWZ0dnhoYnEzOWVvaTN5bHUrOSszWTI5cCs4QVAvcXRuK3NJam1ESURWSWRUS1V5NGpzSFA1N3JlK0RadHV1Tm1KU29qazkvcjQ0bXlsQkRzTHNmMnNyYldqWUFzTWpxSVY4clBaOXpiYUR1cGxGUFQ1b1BiRGR0S09rMzUveHYyZDMvTlcyTjdpUXgvOHdPZzJEcVBrSElLWHk4RXhYSFAxVmZzOWhBTWxiM3J6VzJDNmZQcW10N1lwcXFZeUhicmpVaWkrNzdIOVRyWHZiVDVkTkFuNmw2dTBwSXNrZWRPYjN4TE9ISDcrUXo4ejA2Z090OVE0Qmk0UG5EMjN3RWpLc3NKN3ZOeHk2M2RoUSs0eVhXWDNNZ242eHBoRmR6Y2NWT202YmpMNHZkeHk2M2VGOU5Fdi9zTFB6ZExtNVNKUGV1S1YrejJFVlNya2piZmNHbTJ2M0pXc2prV1cwZENmQ3Z3cDlzWVlkQmgzVTVFeEJ0WVpqKzU3N0xqZDdvRGgvZ0V1Yjd6bFZ2UjdlN0RXWXEvdjhjdi82VCtPNm1lVlZmWmIvc1liYjlsdWw5eE1BbjdON3AyY1RBRy8zN0k1dHQ5cGZZKzNMZTBFR2dWOVk3cnFiWkd4M1RDUlZuQk9zZTI2RG01WFZ2dkYyTTRZZCtWOEc5M1gyZ2Q5QUp1dUN4ZHphOFFZZzAzWDRiV3YvMXVBN2JIWFcvemFmL2x3MDdoWFdXWFg4cHJYdlFHYnpnQUtkUHpuZW93VG9QdnRPK0hpWkU3NFB2MVc0VGRvTmRtU0c3UkcyVTV3SHZUaGExVHFvRzhRQnQ0TTY4NjRQYkRvd2lNZHF1eU1jY0RjZE5FZHVYVzJIZEQxNk56MjIzcFFiN3Bocjc2MVFOZFYzVURqN1p4ejZhdjc5RitDUGI4N29lL1JiVGJvOS9ZUzNWZS85dlVocGZRYnYvcWZxK2F6eWlwTHlxdGU4N3JvNWlnUC9BRDJ6U1pyYnpwcUd6K3RzaVRTelZtMUlqMTFzMWI0elZtMUl2Mldib3Z3SjI2MkNIOFVRd0o5WXd6UXVlMCtuZW1hb21NK29HcTd6amhBVzhDUGJsU2ZYWWV1WW0rOVQvRnNlZ2YydnU5aHJOdFBteE9heTNjUlE0Kyt6OXNGNTBYaGp0NDVRY0doK0M5THY3ZTN0UUUyWFkrOWZ1ai9WYTk1WFhRUHdXLyt4cTlteDczS0tuUEpLLzdhWDQ4QUZBRmZrRmlIMkdTaWRYcDNiSGhkc1ZWVGV3eERTYlRITU5RSWpjYnA4L1ZiN09odjZOYmJ0dCtqQUd6dnlPMDZnNzYzYmprYjBqYXUwOTYzVkFWYjcwUzRsQUR2by8ya1BlVHorc0dab0Q3RkkwWDdYU1o2ZCtzblIvdlNCVi8vb2FmZzU5RSsxZFhBVDlmODVhOTh0UnR1MzZQdmUvejNqMzlVbmQ4cXE3VEl6UzkvNVJiU0Rvd2MrSkd3S0o4RG40c1U1ZlBISWFRMitpTVlOTWs5Z2lFbnVaOVJ6SW4yQ0lhaVhlYVh0RXI5U1k5ZzBLU1kzZ25SUG14MWlvZm4zMzJLSjJjblJmdkE5cmtqaWwyNG9Cc04yRU5hZG1EVThkUkUreVpxTTQzMkthUno0S2ZSZnJETmdOOUgrelhneC9ZTHRiM2hEemUvL0pYaHNSUjdmWTlQL3RZbjFIVmZaUlVxTDd2NTI2STBRZzc0WGtkSzYwaHBGeHJsYThDbndxTjhEbnd1cFlldFNaSjcySm9tSE1xMVViNWtWL1A0aGR4emQ4cjlwYy9VdjhJZjBHaC9tNFJYZDVrTWpRclJQblM0TzBpM1Jmczh0MSt5Y2VtcEFmdzB0eTlGMGhMNGFiUWZidkRJZ0orbmVjSUhPQU4rYTIwVitBRmtVejNvT215d2ZYaVhmM1lJaHR2TnIrZzZ2T3ptYjNOOWJ2djlYNS84SCtKYXIzSjV5Vjk4eVY4TzhEVGRGdHBSQ21iNG9aRnNTcWNpd3E4RnZwYldvY0QzVXYxWTVhM1VQR2lOU3VsQmE1Sm9VWDdSWmtTVUgzUWFvbnhqVFA2WHM0QzJwMjA2L2VVZXNRd3M5RU1xVG5IVWMvV2xCN0FCQ0xZOVdZK3crOGZQcFI5MGJEL1UwNTArdm94dTVmUTdldno2aFhGcyt3cFBBOTJtZTJqVTc4djkrOUR2dWJPNC8vTzduOHArd0ZZNS9QS2liMzVKeUhFUGovT05ZUS9JMFQzQTgvTnhkQTlBQkg0RTlBendReHNWd09kcEhTMlBuMHZydFA1SSt0Z2ZVUEh6MXFKOERuemVmdkdSeW02aUNmU2xLRCswSWYwd09nWC9sQjlUOGZvNThGdEp0L0JzZmQ3dUxzSHY1cUEvWDM5WVE2dUNmMWhISzRMZjEwZDFXL0RUT2c5LzNpNkZmMWlEU3ZnSDNiN0haejc5ZTFqbGNNbzN2ZWpGQUxZQWh3TmgvQWpmQWZhK2pNTWVRSE4wNzl2aUYyM2pkSkNjdzQraS9jeGpsSU5PS2NLZkVmaEFDbnNOK0FDcWZ5clJMYVhjL2x5UFUzYWZBd1g2QUlyZzk5QUg2c0RQb1E3a242OGYycDN4UjFYYzJQUGdEK1BMZ0QvMEJ4bjh2cDQvZHBsQ1dZcjY5d2l3cGFnL3FpTlJQNEJKOFBmMkZQNEFnZ09nWnd4dTNYcjgvbWMvalZVT2puekRDLzljL0lYZlF0dUR6OE4rQUV3NXN2ZDZQQXJYdG1TT2llNUR2d1Q0SE5xbGk3YTVITDV2ZjQ1bjV3TjF3QTk2TXo4L2Y2Z3YvMW9XMVpXaWZBQU8rZ0I5VHZ5UUp5K0JQNEg1anNGUHg2MkJmMmhMLzNFVk9pOEovRzR0OUI5WThYMjBwbnNBSlBDUHdGMlo4Z0hxNEI5c1Nkb0gyM25zRWNkQW8vOHdkOEVCUk9NSDhMay8rQ3hXV1VadSt2b1hSckRzekxEdDBRTWJRRGFxOTdveGxDdlNPRTZ4Q3ZaK2ZBbnNuVUZ6ZE0vN2FmMmhsTEFHQXZCcmZ4cVJ0akZIaEQrOEwzbmd4M1VUZnhmWERITTE5eHk1Mi9LZlBoejdRK25BUE9Dblk1bVM0NC9hVWNEdmRjYjhzbFpZbjRhb1ArNnZuUEx4NjFTSy9IMVpMdWNQTWxZcCt2ZjFrZ01BSUthQXd2Z0VKMEFkeFJjKy80ZFlSWmZuMy9nQ0FBemdEUEt1ZmdDQ0ZORjdPd24wd1liMHdTTktuck1Ia0tSeFlqMGQ5dDZXdzk3M0srWHVhZHVTelpoMGpyUE43OUtwdVdpYkEzNTRIdzR3OEYwYkhjeDk5eDZ4ZmU5QUNDd2Y4ZE0rQUJuOFZFY0NmOVNHbFg5TzBldG92NkVieGlta2U2TDJHNkordHk0eC9Da0FlY29ubEJmZzcvVW8vQUVrT1g4QThRVmZ0OEFoK2cvdDJEanRWWElBL25Wb2c1MEZET01ibkVEUW8zTkM3QWlpNDIzOWJmL3Y4N2pVNUxuUHV6RUNJNEFJM0FDU2VnM3l3QkROZTcwQXBZbWdCOUlVRG9CaXp0NlBzeFRaKzc1clV6bStyNmJvM25XaVJ2ZGhUVWY4Qmk1US9qbEUzNFlFZkFwN1NXOU1TbWVZVHgzd3U4N0FITDN2aUdQYlJQQlRPMEFHUDlYUndDL3BVUEM3c2NqcEh0RmhFUEJIZFN6cWQvT0o0UitkeFFpNWZnQ1Q0ZS9IVkVyN1VEM3RnaSsxazZKL0FLSURBT0l6Z0hETVVrVE9YbllDWVMwZ3c1MXZDT0N3bDJ4b21hWkx4WmZkZGVkdFNkMVllZGF6bnd0Z0FLNFhma3kvWEJ6c1VSbURPNEFrVlJQcEs1RzgxOTJ3dm1JUXQ0TSt0S0ZFOWJRUDB3bjFGSjRNdGpXd2ovUUk3SDJiSFBaZVg4cmRwMjNWUmZlMEx5bTZqK3FWYlpsVGdEKzBzUUR3RFJ6MEFSZkVTdUFIeWhkM3ZmNlU3Wnl1WGs3M1JNQVcwajFELzIxUmZ6eW1PT3AzODY1UCtkQTF5OEUvaktXUTg2ZDZZWTJzbnZyeCtsVU93QmttS2FCaGplT3pnTEJPRE5UOFRJRFdVVWZnUGp0NVowQnRJajN5K2FEbFhyUm5JMGxPb1ZXMEczcjRyeWhSUFFuK1FCeTU4ellrd0hzYkNuZ0FhaVR2eXpqay9aamlLRDNPMHdjZEFmUytUeTJxRDJOU29LM2w3S2xlTHJMM1piV3c5K3ZXa3JzUDY5VVkzUS9yck50SzJ6TEZNNHRvVFBXN2RQeThjam44b1h6YjV0R2o5MW9FY01mZ2QyVngxQytCSHhpZjU2ZDlBQ240YVgxTDFDL2FCU0NsS1oraGZEejhnVFR5ZDNPU1V6Z1UvbjR1V3VySEgvczVBaGpsQU53WTB3dkE3blhjVnhnbldaL1FscEtla2M0SWFEMTNCbjdldEMxcTUrZEZSWHB3WHkza3VST2hrTTZKNUFENDNaUlJKQ2RBSFpEQnpzY2lBZDdYYStraERubGZ4Nk41MzI3UXFZam8vZHdrMEZNOUNsQWExZnQ1KzZqZTIwaDJwVFNPSHllRmZkQ3JUT1ZFL1F1Z2JvM3VvN29SMFgxY2wwYjNkSDdEbk9MbzNxMUxDbndLKzYweHJqQXdzS1lEYkE5ai9OMjVybkgzYzJGK29aMURNRnREWXdCckJ2anp4elVZWXdMWXBEdDNZVXdFTk5lZmUyU0RnWUhwVFlBL0ZXTU1yTEV3QWY2SUZzWmFpNDB4RWZ6cFdLeTE0ZEVOb1h6NzM5L0ZhemFBSWZEdnVpN2NOZHZiSHRhNGRqWUFOcWFMNEwvWmJKS2N2LzhnMjc0UGZRTE94bjN3eVRITWNHZnZua0ZIb001dC9UeXgyVGpBZHhZYmJFSUt5TDkzMWxyc2tkZTBmSVBCQ1d3MnhBbHN1dVJNb1BNMm00M29DSUFCMWwzQkdlQ0sxQ0c0OWtpMHorL0Faby9JMEVCZjgyVFVHc245TG1xUzNpRmZQL3FRTHducXRHMzZUQm9KMnJTOEJIaC9UQ041MzRjRytjRW1uN29KWTYwQXZWOERMYXFueDlyMlM5b0dpUE9waWV4ZGUvV3dwK1VVOXJ6Y3czNW92eTI2VC9wdmpPN2pmdVBvUHUySEFkOE16dmNLLzRLQ2Y3TXgyNmkvUzhBUERPa2VzRWMyZEtZRHpCRDEreStDQjc5alZNOGdPMHlVZ3BrK2twbTJTUUZoTm1hQVAyd01CV095OERjKzBxYmx4cmZYQTJhRDN0Z0kvcHZOQnAzdG9zZ2ZHNE9OdFJIOERZeGJRNTc2SWZDbjQ2RlFseHpBQmh2M1RKK1NBM0FkT2NCdjRyVE5SamdMR042YmdoTUFvck1Ccit2N3BzZTF6b0RhMEhyWFIrb1lKQnN2dFkvczVrN0VTODJUR0lFMHNnZlNNd1h1S0hKUUIvTDVmd3BFYXNldkQvQW9QdEt0aEx6cko3MVl1ejFJWU8zSEo0SGU2ZW5iTHIxdE5vV3o3VGQzZ1phT0o1ZkdpZG92d1o3TWw4SitHTE1NK2xtamUwQk41d1RkOEZtSnh4SG16NEJQZ3hMenhXTkhiZlRsZ3cxZmNOdVE3Z2wxRFJkNUI1djZyWjIwclVTL010OHZ0ZUhHWE03NVV6MXR0dytBN0k2Zm9BODU5OC9YUzh2L2h6clNKdDhCQkNCSjg0UzFZc0FXOWZ5eGNsM0EyY1ZuYTVKOWRIR1dSdlpLU2tlN2dDdnBVdGxUeXNlSzlIUklRRDRMNEE2Z0U0RFB5eld3VTcwSTdrNDVHaHNGdkQ5T2JjdVFEM29NMU9HMUJINGxvcWYySFBRQW1xUDZZUXhkTWliNktPU2FIVGtBQmU0NDJOTTJhbUF2alczeDZONFpoM240TVpyang0OVpZQUFyRUlNZlNPRWZBNmt0MSsvdFFmVVR3TTRMLzJqTVZzLzUrN0xjelYxaDNKbThmeGp6ZHNlUHI0ZVZ3YTA1QUc4bk9RRC9QdkVkUU42ZWwzRW5rTlQzUS90MHpXc2RnWmNXaDhCdE9jajVZN09sZEU1dEtxZVU4OWN1Mm5LUllKL3M3TWxFL3hMVWFYbjBYUG9LdU5OMkpNRDdlcHF1OFhVMGt2ZGxHdVJEV1NGSEgvcHJCTDIzMDI2cW91MUtLUnkvemlKb0ZWam4wampEMk11dzkyUGg3ZEIxck4yWjQvcGRJTG9uVHNuY2YvOXhHMkJPd0EvSVViOTd6UzlHVXBzNitFZjlzQXU5VGw4R09oMm5CdjlRUnVEUDlRSUEyVzZmYUY3Q1dZSHJQeC85Ui9PRjdnRDhHa3NPZ1BiankzTm5BYlN0MEtldlo4NlQ3d2dDMmgwQlBaWjA2SzkvY2J0dDQrRWxkUXhhdTFwOVVqZHpwQS9Jb0FmU3lCNFFjdjFFUndLNkswL2hUMzkxcWdYdXJ1bU1Qb0VSMTZtQnZLL24wVHh2UTByZHVIa3BvQWV5dVhwM3JFZjF0Qjl0NjZVL0xsMmc5V3U0SzlnUCtqSHN2VDZIUFgzL2FxUDdVR1U2bUJNbmpwUHZmd3IvZ0tER2xJOXZqOEkvdDcwejBzL0FQOWl6Y2RiYzFldm5remdTcTZkKy9MRzIxMzhZNDN3T2dQYmgxek4zRmtEWGhEc0JJRTBIK1hiQzNEakVGYmhyem9EcTVCeENVaTQ0Qmk5Sm1rYkoyM09ISVVuT1NWQ1JJQzZKK0N0UlFyNWZncm1YSE5ScG1lZ1FhdUR1Q21RN0F0R2tyQUx5dkV5SzVyMHVmK0tsdDVVaWV0ZFdIZWlwWFhWVXYxMlRwTDV3YzFXWVN5R05NK2hWd0g2N1dCejJnODcwVkU3Y1poZDB6Y2tUOTRmWTBnWEE4VmE1Q1A2WmxJOHJteC8rYmh6bGJaNitMSGRucjlmTnBYNkFPUHFYMnREU1AwTmQ2Z0NBSVFVVTljbXZBV3dYbHdJN21TOTBKOEIxQStRTFp3TytMYWxjY3dhaUxqMjdVMkF2MlhOOVhoZDBoTjhQenVuUEpacERrSDZYY2JhV0FBQU8vVWxFUVZRSE5nRjhOdnJYb1E0Z0Fzd1V1SXZsRk1nWUxyNVNIUjdKKy9ZMHlQdDJ6UkRDRm5QMGZteFRRUy9WNTZMNnVDKzlqYkU1ZTZlM0RPeTNTN3R0TDVQS0liQjNYUnVZVTZkTzJyQjFUb0EvQldyUW1RbitRRjNPUDdUWjkzTEVUUjFHSnZVVEhTdlJmK2kvMFFIRWJjY093STlMaXE2bHN3Q2czUW00UHZLcG5Kd2o0RzFIZW9vOUxmUHJCZVNkZ2w4TEtpWFlheGRuYXlGZnU0ZS9OcThQeUU1QXV1aWJjd0FTMExrTkJ6dXQxK0F1MWlrUlBOWFJvbmczMXZHUTk3bzhtdmZsUEVmdis1VlNONkZPZ0xSL3JlWHF3L3lWcUo0ZWF5a2NiaU5mVDVBdjBBN3pIUWQ3WDFjTCsrM2JFZVlkM3NQVHAwOVpHbEVQR2VYNTRlL3N5dkNudGdBWmd3aFZQZlhqajdVYnZjTHhSQWN3OUtPZGlhUVhnZW40Z2ZRc0lQeFhuSUFmS3dlMWRqWkE2NUtkTlkzT0lOS2xEamZuRk1pNGdSVGtIT0R5UlZzZDhuUGNnWnVUbkVPUWZxa29GOTBEekVFSVFPZHR0SUFkaUtOM1NiY1c4RjVYUER0Z3pvZEQzdi9QUmZOT0o3MFlTL3ZsanplV1h0ZUEzclUvTGFvZjVpUTVKWDAzam0rYmpzSFBmZTdJbnVwUzJBZmQwNmRQaFcvU2ZzR2Z0a3NCM1pyNm9YMUowVDl0WHdOM3F3T1ErdlFPUUc2ZmczaThFL0I2ZnVFMVNFdG5CUEVZNnB5QmV4MDdCS20veEVad0RKS2VWQTkrdkpYYXJabTFad05lcEFoZUUzRTdwMkNmamZaNVhRYnFRSnFTY1hveTJPbHI3YXlnR3ZCa2J2emlhMkxYQ0hsdUg2QmFnUDB1UUU5ZlMxRzludyszS1cyOXBHdWV1MEJMbG4weTdJUE5tVE1QMkJTc1pmaTdlcGJ6ZDRWQlY5dnRBNkE1OWNQdGE2Si9ic1BIUE1ZQkpHM2FkQXVvOUhxc0V3ajJKQjJVakNIakNPaDdNOVVaU0xhOFBuYnNxV1BndHR5ZTlzOGxCL0NsSTMyZ0VPMExzT2RiTjB2UnZ4U3BPN3NZM054V0F6c2Rnd1QzeUxZUjhQNC9CWHpVM2dqSVMrVkpOTDhkbTZpanBHN29hdzU2TjZaMFhyRnVmVlRQMTBWTDRiZzFpL3NKYTVXNVFCdmFHZ043Ly9yczJiTldnckkvQmdZWVNEbC9WOC9zMkZaUFlIcjBIOFpqQldqTjZBQ2lkcGdEQ0cwaWJhOTBGaENQVjNjQ2ZyMTdQamZsYkNCWkgvSmZUQTJSTjZYV0dmZ3grZjZIWm1Ud1M1OGhydE96VkkxRmZFekh4YVUyZWgvckNLcjM3Q3RuQmRJZHZnYXhidGZ4NHp6UU5SM3FXRFN3Ui9vWnVBUElBdDRWQzgvN2p5THFqcFdsTzM5eWtPZGxValFmWHJPTHNWUkhha3VMNkNXNzNKMnp2TjNXcUQ2ZGk1Q3ZkNDFVN2NaeDdaZGhIK3pPbmowYnZrRldnVWNNeWo2Q2YyU1hnYi9YbnkzNjN5cW1qaXJ2QUtJNjVnQjhYZWtpc05mWHpnTENmOFVKSkcweEo1RG9DV2NEZE14akhBRlFQak1BNU5STnppblFjWEs3cUMrbFhnTjZEdURjZ1N3cEhOWnhYZmxDcmxTbUFWOENPb0RJZlhCZ0FuSUthQXpjcVg0dDRIbGJpZDBFeVBzNWxhSjUvMXI2MFJLcDM3bEI3M1RybzNyM09qU2lSdldoWFFYMjNNbnp2a0w1Z3c4K1NLQWZROFdYbGVEdjZ1cWpmNi9mRXYySCtvVWNRRHEvL0ZsQThyclJDV2g5UjA1d29pT2dOdHdaU08xbUhZSlREUFB6b3FWdEVwalQxeFhwbkttcG5OcXpBVTIwS0o2TGRsWlFrKzdoTjMxSk1PZXZ0UlFRaHpxUWdwMjIxUUozVno0TzhMeU8vNitGdkIrTDFtWXViWlBYcjB6ZHVNRWs2MWFUdm9ublBTNnFkLy85TU9Tb1BwMm5Fdm1mTzNmT0Frb2twOEEvMGhGU1A5Uld2UERyS2lMOUpSeEFPaS81SWpDZlg4dFpRTHhldWhQUWJFcG5BOXd1NXdqOG5HSzkrSDJRMnFoeUNFNGh2TnlqeFlKam9QTU9ldnlwbVpWUmZnN2ZTOXlCbTVQY2t6ZUJHTnlockNiYTEyRHVsQWU5U3FqVDF4enNjVjA3M0VYN0N0QnJPZm1najNSTUpjZFJHODBQK3NPOGNvQXNSZlNEWGhuMHZ2MldYTDIzMGFKNmJZN1NYSkt4bkgvb0lRdklwK1JTMU96TGw0eit0ZjY0QS9CNmtnTUlZN054RzJCMm9VNDVDeEJ0RlNlZ3JRMGRYL0ZzWUxzUWUxcWVYZmxmNnd4aTNiSkRBUGhPbklySVhuRU92QzAvVnk1U2ZqL1lLM24rcEkySmtUN1FFTzByVCtsTTRPMGFqVzJaRTlsd2t3TFFuWW9PZFZjdlJiN3p3VjM2VHdIdnk2VW8zcmV0WFV6bFphMlFIOXJQUi9QaHZ4bktXbE0zdEMydnR4VG8rVnk5cmFUajF5Q1VmZW44K1NUU2w3NzR1YlNKcURPREF3RDBNd0JmNTJ5SGNkRkllNWhQL1ZsQTZHT2tFK0Q5WloxQm95TUFNTm9aNUhUY1dLUUx0YnBUNFBicERwekdxRjdKeTNPbkVabnNPTXIzVXJyUW00QWJpT0R0aFR1VkpQb24wS25MKyt0UWQvVTZGRXB3bDhxMEZJMzR2eERGKy8vcGVQUjBqV1lyNWVicG12QnlLVzJUam5GZTBOUDVTT2tiVjErZnE1Zld4SzlGMHNiRER6L3NvQy9kUmFsQ1g0L0dSZjBkT2dCbnp4eFQ0MW1Bbjl0WUorRG41T3UwaTZxMWprQ3pwWXRUY2daYVdjN0JTMDZCMnZoMTRuT1c5SGg3Z0E1dDdmbjR0ZEg3SEZFK1VCL3A1M1NsWi9BWHQyNXlaeURBbk92bG9rRmVKb0tnQUcxQWp0N0YvNFVJbnYvbndDMUJYb3ZrMC9yMmFINW9JOTUxQStnWFk0YzZvandSOVBrMUsyemRsZDVYMnU0amp6eVNSUHJ5THhvVm9MQ2tBM0FWa1kyMEM4ai96NldCZkIvRHZNWTVBVDhYS2EwaXJRK2ZXODRSaVA5SE9nTkFkd2lsTXRwZlRpZWRwK3dnQm4xMkZxQkUrZHArZmEzZlhVck9JWEJvQnhzeDJvOGRRTzFqbWZseExkVHA2eGF3aTNaQzlKNXJmd3pnK2YvOGZ2L3hrUGYySFBLOEQ2K1g3THB4RFJaVE45Um1hZERUdHFPeVJ4OTlOSVUrZ1JvdkEzYnJBQktiaHJNQS8zOE9KNkROTVlLOGNITlRMdktXNWppWE04alpiQThBREE0aG5WL2I2MUpVWDB6ditISTErdDgvd05lSUJub2cvbEtITWtFL0IvWEVHUlNBVGwrTFVIY1ZSUnNPZGplZk10enAvN0dBOS8zS080WFNzeVdhcnZGbGMwSGUxM1BJMHpVS3RvVWN2VE9kRG5wQWNmQ1p0Z0VDZlNvSHhRRWs5aVBQQXZqWU5DY1F4cWc1QVdLZ25RM3dPdTB1MTJ3YVJuQjJyYzZBdHRNTGExWUV1WENtd0hWejdVakhVdG1ZdTI5cjZwZVdVdXBIVC9lMHdiNTBYQUs2cGgrVk5ZQmRLcXVCZTg2dUJIajNQd015azQ1Qm5rTWQ1TDF1YlRUUDU5cDZNYlkwVC82NkZ2VDhkU2o3azhjZWM1Rit4Wk1NNTNZQXBmWkhuUVc0eXNpdXhRbTRkbUtBMC83Y1BGTkhvRWJ5MHU2ZUVZNUFtM2N5RnNXKzFTbG85dUs0MlBIZTJLMlpJeDNBV04xYUtRRytScThtc3BmS05ueG5VRU9FRDhSUmFBN3E5SFd1VFBxaDl6RndkM2JqQU8rUCtmV1NtcHo4VUQ4djVLbGRMcHFYK3Q4SjZLbnVZeGN1MkZ4T05pcWY2QURTMS9WbkFjRHVuQUNRWGhpbSt2eHN3TTEzM0JsQjlGOXhCcUp1ZzBQUTdLcGVLenQzM0JTblJmbmNXWGpoVHFQWXpqNUp5Umtrd0I0TWkyMFZqeFdZQXh4SU9zaTExelZnbDlxZUFuZHhySldBOTIzeEVlWjIyUGg2ZHh3MW5HeTFsVkkyM0s0bG1xZHRhTHFsSEwxbUY0MWIyVEJnTGx5NEVIMkZwamdBcmovQXA5MEJsUHJnZTdtbk9BRmczTmtBMWE4OUk5RG1KN1lsckt0MmRwQnJsNDlMU2h0cGJlVmVpOGVsSFR6S0h2d2N5R3NodjVRenFJMzBTN3JpRjFQWXk1K0RPRytuTnZVVFI0M0N4YjNDbGxJTjdIUzhjOENkam8rUHJRUjRyeU5GOFY0bmVRdEdSUEp1eU10Rjg0bE9JVWNmVDBmZkdlWmZtUXNYTGlUZmxBRm9FanhTSjlEaUFJRDljUUpPVDRpNjJZVmhid3VrRjRlbHNaVE9DSHkvVkZxZGdkaG14aUdvZXJrVWpURE9HdWRRYzl4U0pvMkR5MEc2cUp1N2dBdklNQVVVQjlBYTdiTmpLVUt2SFVQdWpDQzMxMThEbTZnekF1N0VMRWlYdmQ2UWorSmRlN3VGdktRYjVqSXhiY1AxRXh0QjEvenA0NCtIYjVBSWRQcDZnYk1BWUJrbjRNWk82NFYyS3M4R3FIMnJJM0J0eG4xWE93TmlYT01RTkwzU0V6SmJIRVBVcjNMMjRLVjBnMVpOUC9zZDVYdVpJOXB2S1UrQVdJaitOZDI2eUYrSE9uM05RYWEyV1FsM3FXMDZxeHpjcVUwdDRIMGI2WmxUZk9IVjIyVmhtam1qa0hURG5HWkkyM0Q5eEtha1M2SFBoVU45MXc0Z2E5L2dCSHgvdFdjRFNWc1Zqb0NQU1JwWHJUT2c0L0FpUnRyTUlmRFhRMWxGQ2llenQ3NHB2Vk1MYVNXYUw5bHJOMjd0bDBnM1gzSFJ2N2hsUjFJNkk4aEYvOXJOVzFRdkY1MW0reXFBM2ZWWkY3a0RiWUNYVWpTdTNUckF1LzkwS3Zrb1ByUTFJWkpQZENaQ251cEtGcmxuUkpuSEwxNjBOVi9VRmdjZzZVdTZOVTRBYUQ4VDhIVTVJRWxuQTA2bnpoRkV0ak03QTlkMlBLK2NRd0R5WndtSnZScHhWMjdOSFBtVXpKWVVUNm51SUV0clhsOHJUOU0zUE5xdkE0ODdsaUdRaTlSNWZRdllXVFBPUHJOamlJOVRqTjVkaDl1Mjh5a2EybFlKOE5xWWFuTHkvSGd1eUJmSFdOQVYyL1B6ZXZ6aXhlU2JkZENjQUsrcmNRTHVPSDgyb1BVcE9RTGVudE9mMXhsSTQ4dzVCTnBHeVNrQWltTmdEVGFsZHlyU055MmZoWU44QjI1T1NxbWZaVzdjeWtSeXVkeC9BOURkK0hRQTVpSjJhbE1DdXl0TE90NzJJYzlsU2NEVDlqUjlMNjJRbDQ0bG02UzlDbjJ4VGQ0M0FIT1JRWjkvcmNZNEFON09IRTZBMjR4SkNibmpOa2RBK3kxZEl4ajAyNXdCYmEvR0lkRFgyZytJOERPRnhENXpzVFIzMFpaL1FGcWo5MWhQVDlXMEFINlh6cUEydDUrUDlzdGZWcTJkcEYxK21FQXBEM051azR2VWdUcW84M1pLVWJ2cmF6emNxZjJjZ09mSDJidWlaNDdrZ1duUmZMYWRMKys1NXhpV3dCM0s5dEVKOE9QYXM0SEVMdU1JYUgyTkE1SzJJTGFlR1VUdFZEZ0VxZTNjZUV1T1FSeHJNdC84K3k3bDJzWFBpdEpNMWVmcWdFYjZRTmtacVBWYXNhQmZ1bjZnZ2R3ZDUyRU95RURYeGlKQjNaVW5neUw5MW9HZHRqOEc3bEhiQ3dNK3Fkc3g1R3ZhNGUwRjZGTlJBU3lWN2RnSmNQM2Fzd0ZnZmtkUUdudHRtc2paVkRnRXA1aldrK3FTWStCOUpHdVVnWHQ2RmxnSCtwTERrS1QyZ3UydUhVRnRwQS9VWGVSTjJxOTZCRE9EUjJrY1dXZWdwQnBxSXZXZ0xLZGllUHUxVVR0dGF3NjQ1K3lBUE9ENUdNYW1hcmp1MkhTTk9BWk5UL3RWdDU1OHk5WG5vT3pZQ1dqdHpPRUllSDM3ZzhMcW5FRnBISnBEa1Bwd3Ruby9TWHNxN0lmWE5jNWhxTSt2U2UxdjFPYTBXc2R3MEtUR0dhZ1hVeHY2NGIvUld3c2hxZjlzaEI0cTlFaWQ5OWNTc2RQMmFxNXJ6QUYzWUJuQWN6dEpmeGVRejdVWlhYdm9NOS9ZWFRrQnJjM1MyVUJ1TE55bXhSRUEwNTBCMVdtOXdKbExHYWw5RmZiTWg3b0s1OEQ3QTRUdHBoTWo4ZHdZZC9sRDUwdEs3a2ZVZ1Z5VVBjNkJWRUU4S09SaHpzZWgzK0JWaHJxenJ3ZTdYTGNjM0d2YXk5bUs5cFYyWXZzelE1NjNiZmgrVGUzTG1OdFpzU3Nud051YzZnakU0NG5PUU81anVrT2dZOU1mWlpEdmMyaG5oSU53aHFxdU5nNHYyYlRSRHZmZTAvbTJwR3JtRWkzYTF4eEUxUkNUZEU4dTJ1ZndLNmQzOU1zUmJWQ25aYVhyQk9wNEMzQVhiVVpHNzl4V2JhUFNUbXkvNHNKclRiczV5SFA1LytGZnBDUWdMOUdSQUFBQUFFbEZUa1N1UW1DQyIgdHJhbnNmb3JtPSJtYXRyaXgoMSwwLDAsLTEsMCwxKSIgaGVpZ2h0PSIxIiB3aWR0aD0iMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSIvPgogICAgPC9nPgogICA8L2c+CiAgPC9nPgogPC9nPgo8L3N2Zz4K\",\"data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnNjkzNCIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjEwLjg3bW0iIHdpZHRoPSI0OS45NjZtbSIgdmVyc2lvbj0iMS4xIiB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmlld0JveD0iMCAwIDE3Ny4wNDUwMSA3NDcuMTYyNDkiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyI+CiA8ZGVmcyBpZD0iZGVmczY5MzYiPgogIDxjbGlwUGF0aCBpZD0iY2xpcFBhdGg2MTMzIiBjbGlwUGF0aFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CiAgIDxwYXRoIGlkPSJwYXRoNjEzNSIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJtNjIxNSAxOTAuMDNjMCA0OC4yMzkgMTQ1Ljg5IDg3LjM5MSAzMjUuOCA4Ny4zOTFzMzI1LjgtMzkuMTUyIDMyNS44LTg3LjM5MWMwLTQ4LjMzMi0xNDUuODktODcuNDgtMzI1LjgtODcuNDhzLTMyNS44IDM5LjE0OC0zMjUuOCA4Ny40OCIvPgogIDwvY2xpcFBhdGg+CiAgPGNsaXBQYXRoIGlkPSJjbGlwUGF0aDYwODEiIGNsaXBQYXRoVW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgPHBhdGggaWQ9InBhdGg2MDgzIiBkPSJtNjQxNy45IDE1MzEuN2MtMzAuNDcgMC01NS4xNiAyNC42OS01NS4xNiA1NS4xNnY0MTAyLjdjMCAzMC40NiAyNC42OSA1NS4xNiA1NS4xNiA1NS4xNiAzMC40NiAwIDU1LjE2LTI0LjcgNTUuMTYtNTUuMTZ2LTQxMDIuN2MwLTMwLjQ3LTI0LjctNTUuMTYtNTUuMTYtNTUuMTYiLz4KICA8L2NsaXBQYXRoPgogIDxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyR3JhZGllbnQ2MDg1IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxZS03IC0yMzQ5LjEgLTIzNDkuMSAtMWUtNyA2NTQwLjggMzQ2OS4yKSI+CiAgIDxzdG9wIGlkPSJzdG9wNjA4NyIgc3RvcC1jb2xvcj0iI2YxNDg1ZiIgb2Zmc2V0PSIwIi8+CiAgIDxzdG9wIGlkPSJzdG9wNjA4OSIgc3RvcC1jb2xvcj0iI2ExN2Q1YSIgb2Zmc2V0PSIxIi8+CiAgPC9saW5lYXJHcmFkaWVudD4KICA8Y2xpcFBhdGggaWQ9ImNsaXBQYXRoNjA2MSIgY2xpcFBhdGhVbml0cz0idXNlclNwYWNlT25Vc2UiPgogICA8cGF0aCBpZD0icGF0aDYwNjMiIGQ9Im02NTc3LjUgNTk0MS45aC03My40NGMtMTU0LjM3IDAtMjc5Ljk2LTEyNS41OS0yNzkuOTYtMjc5Ljk2di00MjA1LjVjLTIxOC4xLTExNi4yLTM1Ni4xLTM0My4yLTM1Ni4xLTU5My42NSAwLTM3MC45NiAzMDEuOC02NzIuNzUgNjcyLjgtNjcyLjc1IDM3MC45NiAwIDY3Mi43NSAzMDEuNzkgNjcyLjc1IDY3Mi43NSAwIDI1MC40OC0xMzcuOTYgNDc3LjQ4LTM1Ni4wNyA1OTMuN3Y0MjA1LjVjMCAxNTQuMzctMTI1LjU5IDI3OS45Ni0yNzkuOTYgMjc5Ljk2bTAtODguNThjMTA1LjI2IDAgMTkxLjM5LTg2LjEyIDE5MS4zOS0xOTEuMzh2LTQyNjEuMmMyMDkuMjgtODguODUgMzU2LjA3LTI5Ni4yNSAzNTYuMDctNTM3Ljk0IDAtMzIyLjYzLTI2MS41NS01ODQuMTgtNTg0LjE4LTU4NC4xOHMtNTg0LjE4IDI2MS41NS01ODQuMTggNTg0LjE4YzAgMjQxLjY5IDE0Ni43OSA0NDkuMDkgMzU2LjA3IDUzNy45NHY0MjYxLjJjMCAxMDUuMjYgODYuMTMgMTkxLjM4IDE5MS4zOSAxOTEuMzhoNzMuNDQiLz4KICA8L2NsaXBQYXRoPgogIDxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyR3JhZGllbnQ2MDY1IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxMzQ1LjUgMCAwIC0xMzQ1LjUgNTg2OCAzMDY1LjkpIj4KICAgPHN0b3AgaWQ9InN0b3A2MDY3IiBzdG9wLWNvbG9yPSIjYjNiM2IyIiBvZmZzZXQ9IjAiLz4KICAgPHN0b3AgaWQ9InN0b3A2MDY5IiBzdG9wLWNvbG9yPSIjYjNiM2IyIiBvZmZzZXQ9Ii4xIi8+CiAgIDxzdG9wIGlkPSJzdG9wNjA3MSIgc3RvcC1jb2xvcj0iI2ZlZmZmZiIgb2Zmc2V0PSIuMjQ3MzEiLz4KICAgPHN0b3AgaWQ9InN0b3A2MDczIiBzdG9wLWNvbG9yPSIjYTNhM2ExIiBvZmZzZXQ9IjEiLz4KICA8L2xpbmVhckdyYWRpZW50PgogIDxjbGlwUGF0aCBpZD0iY2xpcFBhdGg2MDQ3IiBjbGlwUGF0aFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CiAgIDxwYXRoIGlkPSJwYXRoNjA0OSIgZD0ibTY1NDAuOCA5NTUuODljLTE4OS4xNiAwLTM0Mi41IDk3LjIxOS0zNDIuNSAyMTcuMTUgMCAxMTkuOTIgMTUzLjM0IDIxNy4xNCAzNDIuNSAyMTcuMTQgMTI3LjgzIDAgMjM5LjMtNDQuNCAyOTguMTQtMTEwLjIgMjguMjMtMzEuNTYgNDQuMzUtNjguMDYgNDQuMzUtMTA2Ljk0IDAtNjUtNDUuMDQtMTIzLjMzLTExNi40Mi0xNjMuMTItNjAuMzItMzMuNjMxLTEzOS40NC01NC4wMjktMjI2LjA3LTU0LjAyOSIvPgogIDwvY2xpcFBhdGg+CiAgPGNsaXBQYXRoIGlkPSJjbGlwUGF0aDYwMzEiIGNsaXBQYXRoVW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgPHBhdGggaWQ9InBhdGg2MDMzIiBkPSJtNjc2Ni44IDEwMDkuOWM3MS4zOCAzOS43OSAxMTYuNDIgOTguMTIgMTE2LjQyIDE2My4xMnYtMS4yNGMtMC41Ni02NC41LTQ1LjQ5LTEyMi4zNC0xMTYuNDItMTYxLjg4bTExNi40MiAxNjMuMTJjMCAzOC44OC0xNi4xMiA3NS4zOC00NC4zNSAxMDYuOTQgMjcuOTQtMzEuMjMgNDQuMDItNjcuMjkgNDQuMzUtMTA1Ljcxdi0xLjIzIi8+CiAgPC9jbGlwUGF0aD4KICA8cmFkaWFsR3JhZGllbnQgaWQ9InJhZGlhbEdyYWRpZW50NjAzNSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGN5PSIwIiBjeD0iMCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCg1ODQuMTggMCAwIC01ODQuMTggNjU0MC44IDg2Mi43NSkiIHI9IjEiPgogICA8c3RvcCBpZD0ic3RvcDYwMzciIHN0b3AtY29sb3I9IiNlZDFjMjQiIG9mZnNldD0iMCIvPgogICA8c3RvcCBpZD0ic3RvcDYwMzkiIHN0b3AtY29sb3I9IiM2MzJkMTYiIG9mZnNldD0iMSIvPgogIDwvcmFkaWFsR3JhZGllbnQ+CiAgPGNsaXBQYXRoIGlkPSJjbGlwUGF0aDYwMTUiIGNsaXBQYXRoVW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgPHBhdGggaWQ9InBhdGg2MDE3IiBkPSJtNjg4My4yIDExNzEuOHYxLjI0IDEuMjNjMC4wMS0wLjQxIDAuMDEtMC44MiAwLjAxLTEuMjNzMC0wLjgzLTAuMDEtMS4yNCIvPgogIDwvY2xpcFBhdGg+CiAgPGNsaXBQYXRoIGlkPSJjbGlwUGF0aDU5OTkiIGNsaXBQYXRoVW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgPHBhdGggaWQ9InBhdGg2MDAxIiBkPSJtNzEyNC45IDg2Mi43NWMwLTMyMi42NC0yNjEuNTUtNTg0LjE4LTU4NC4xOC01ODQuMThzLTU4NC4xOCAyNjEuNTQtNTg0LjE4IDU4NC4xOGMwIDMyMi42MyAyNjEuNTUgNTg0LjE4IDU4NC4xOCA1ODQuMThzNTg0LjE4LTI2MS41NSA1ODQuMTgtNTg0LjE4Ii8+CiAgPC9jbGlwUGF0aD4KICA8Y2xpcFBhdGggaWQ9ImNsaXBQYXRoNTk4MyIgY2xpcFBhdGhVbml0cz0idXNlclNwYWNlT25Vc2UiPgogICA8cGF0aCBpZD0icGF0aDU5ODUiIGQ9Im02MzEyLjYgMTQwMC43djQyNjEuMmMwIDEwNS4yNiA4Ni4xMyAxOTEuMzggMTkxLjM5IDE5MS4zOGg3My40NGMxMDUuMjYgMCAxOTEuMzktODYuMTIgMTkxLjM5LTE5MS4zOHYtNDI2MS4yYy03MC4wOCAyOS43Ni0xNDcuMTggNDYuMjItMjI4LjExIDQ2LjIycy0xNTguMDMtMTYuNDYtMjI4LjExLTQ2LjIybTEwNS4yMyA0MzQ0Yy0zMC40NyAwLTU1LjE2LTI0LjctNTUuMTYtNTUuMTZ2LTQxMDIuN2MwLTMwLjQ3IDI0LjY5LTU1LjE2IDU1LjE2LTU1LjE2IDMwLjQ2IDAgNTUuMTYgMjQuNjkgNTUuMTYgNTUuMTZ2NDEwMi43YzAgMzAuNDYtMjQuNyA1NS4xNi01NS4xNiA1NS4xNiIvPgogIDwvY2xpcFBhdGg+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXJHcmFkaWVudDU5ODciIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDFlLTcgLTIzNDkuMSAtMjM0OS4xIC0xZS03IDY1NDAuOCAzNDY5LjIpIj4KICAgPHN0b3AgaWQ9InN0b3A1OTg5IiBzdG9wLWNvbG9yPSIjZWQxYzI0IiBvZmZzZXQ9IjAiLz4KICAgPHN0b3AgaWQ9InN0b3A1OTkxIiBzdG9wLWNvbG9yPSIjNjMyZDE2IiBvZmZzZXQ9IjEiLz4KICA8L2xpbmVhckdyYWRpZW50PgogIDxjbGlwUGF0aCBpZD0iY2xpcFBhdGg1OTY1IiBjbGlwUGF0aFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CiAgIDxwYXRoIGlkPSJwYXRoNTk2NyIgZD0ibTY3NjguOSAxMzI2LjhjMC0xMDUuMjYtODYuMTMtMTkxLjM5LTE5MS4zOS0xOTEuMzloLTczLjQ0Yy0xMDUuMjYgMC0xOTEuMzkgODYuMTMtMTkxLjM5IDE5MS4zOXY0MzM1LjJjMCAxMDUuMjYgODYuMTMgMTkxLjM4IDE5MS4zOSAxOTEuMzhoNzMuNDRjMTA1LjI2IDAgMTkxLjM5LTg2LjEyIDE5MS4zOS0xOTEuMzh2LTQzMzUuMiIvPgogIDwvY2xpcFBhdGg+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXJHcmFkaWVudDU5NjkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDQ1Ni4yMiAwIDAgLTQ1Ni4yMiA2MzEyLjYgMzQ5NC4zKSI+CiAgIDxzdG9wIGlkPSJzdG9wNTk3MSIgc3RvcC1jb2xvcj0iI2EzYTNhMSIgb2Zmc2V0PSIwIi8+CiAgIDxzdG9wIGlkPSJzdG9wNTk3MyIgc3RvcC1jb2xvcj0iI2VjZWNlYyIgb2Zmc2V0PSIuNzUyNjkiLz4KICAgPHN0b3AgaWQ9InN0b3A1OTc1IiBzdG9wLWNvbG9yPSIjYjNiM2IyIiBvZmZzZXQ9IjEiLz4KICA8L2xpbmVhckdyYWRpZW50PgogPC9kZWZzPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTIzMS4zNyAtMTc2LjY3KSI+CiAgPGcgZmlsbD0iI2ZmZiI+CiAgIDxwYXRoIGlkPSJwYXRoNTU3OSIgZD0ibTMxOS44OSA5MjMuODNjLTQ4Ljg4OSAwLTg4LjUyMi0xMC42MzItODguNTIyLTIzLjc1IDAtNy4wODY0IDExLjU3Mi0xMy40NDkgMjkuOTE5LTE3LjggMC4wMzYyIDAuMDI5OCAwLjA2NzUgMC4wNTg1IDAuMTAyNSAwLjA4OTctMTguMjE5IDQuMzM1LTI5LjcgMTAuNjYzLTI5LjcgMTcuNzEgMCAxMy4wNjkgMzkuNDg5IDIzLjY2NCA4OC4yMDEgMjMuNjY0czg4LjIwMS0xMC41OTUgODguMjAxLTIzLjY2NGMwLTcuMDQ3NC0xMS40OC0xMy4zNzUtMjkuNy0xNy43MSAwLjAzNS0wLjAzMTIgMC4wNjc1LTAuMDU4NSAwLjEwMjUtMC4wODk3IDE4LjM0OCA0LjM1MSAyOS45MTggMTAuNzEzIDI5LjkxOCAxNy44IDAgMTMuMTE4LTM5LjYzMiAyMy43NS04OC41MjEgMjMuNzUiLz4KICAgPHBhdGggaWQ9InBhdGg1NTgxIiBkPSJtMzE5Ljg5IDkyMy43NGMtNDguNzEyIDAtODguMjAxLTEwLjU5NS04OC4yMDEtMjMuNjY0IDAtNy4wNDc0IDExLjQ4MS0xMy4zNzUgMjkuNy0xNy43MSAwLjAzMzcgMC4wMjg5IDAuMDcgMC4wNjExIDAuMTAzNzUgMC4wOTE0LTE4LjA5IDQuMzE3NC0yOS40ODIgMTAuNjExLTI5LjQ4MiAxNy42MTkgMCAxMy4wMjEgMzkuMzQ1IDIzLjU3OCA4Ny44OCAyMy41NzhzODcuODgtMTAuNTU3IDg3Ljg4LTIzLjU3OGMwLTcuMDA3NC0xMS4zOTItMTMuMzAxLTI5LjQ4Mi0xNy42MTkgMC4wMzM4LTAuMDMwMiAwLjA3LTAuMDYyNSAwLjEwMzc1LTAuMDkxNCAxOC4yMiA0LjMzNSAyOS43IDEwLjY2MyAyOS43IDE3LjcxIDAgMTMuMDY5LTM5LjQ4OSAyMy42NjQtODguMjAxIDIzLjY2NCIvPgogICA8cGF0aCBpZD0icGF0aDU1ODMiIGQ9Im0zMTkuODkgOTIzLjY2Yy00OC41MzUgMC04Ny44OC0xMC41NTctODcuODgtMjMuNTc4IDAtNy4wMDc0IDExLjM5Mi0xMy4zMDEgMjkuNDgyLTE3LjYxOSAwLjAzNSAwLjAyOTcgMC4wNjc1IDAuMDU4NiAwLjEwMjUgMC4wODk5LTE3Ljk1OSA0LjMwMjYtMjkuMjY1IDEwLjU2Mi0yOS4yNjUgMTcuNTI5IDAgMTIuOTc0IDM5LjIwMiAyMy40OTEgODcuNTYgMjMuNDkxczg3LjU2LTEwLjUxOCA4Ny41Ni0yMy40OTFjMC02Ljk2NzItMTEuMzA2LTEzLjIyNi0yOS4yNjUtMTcuNTI5IDAuMDM1LTAuMDMxMiAwLjA2NzUtMC4wNjAxIDAuMTAyNS0wLjA4OTkgMTguMDkgNC4zMTc0IDI5LjQ4MiAxMC42MTEgMjkuNDgyIDE3LjYxOSAwIDEzLjAyMS0zOS4zNDUgMjMuNTc4LTg3Ljg4IDIzLjU3OCIvPgogICA8cGF0aCBpZD0icGF0aDU1ODUiIGQ9Im0zMTkuODkgOTIzLjU3Yy00OC4zNTggMC04Ny41Ni0xMC41MTgtODcuNTYtMjMuNDkxIDAtNi45NjcyIDExLjMwNi0xMy4yMjYgMjkuMjY1LTE3LjUyOSAwLjAzMzcgMC4wMjg4IDAuMDcgMC4wNjEgMC4xMDM3NSAwLjA4OTctMTcuODMgNC4yODY2LTI5LjA0OCAxMC41MTEtMjkuMDQ4IDE3LjQzOSAwIDEyLjkyNiAzOS4wNTggMjMuNDA2IDg3LjIzOSAyMy40MDYgNDguMTggMCA4Ny4yMzktMTAuNDggODcuMjM5LTIzLjQwNiAwLTYuOTI3OC0xMS4yMTgtMTMuMTUyLTI5LjA0OC0xNy40MzggMC4wMzM3LTAuMDMwMyAwLjA3LTAuMDYyNSAwLjEwMzc1LTAuMDkxMyAxNy45NTkgNC4zMDI2IDI5LjI2NSAxMC41NjIgMjkuMjY1IDE3LjUyOSAwIDEyLjk3NC0zOS4yMDIgMjMuNDkxLTg3LjU2IDIzLjQ5MSIvPgogICA8cGF0aCBpZD0icGF0aDU1ODciIGQ9Im0zMTkuODkgOTIzLjQ5Yy00OC4xODEgMC04Ny4yMzktMTAuNDgtODcuMjM5LTIzLjQwNiAwLTYuOTI3OCAxMS4yMTgtMTMuMTUyIDI5LjA0OC0xNy40MzkgMC4wMzUgMC4wMzEyIDAuMDY4NyAwLjA2MDEgMC4xMDM3NSAwLjA5MTQtMTcuNzAxIDQuMjY5LTI4LjgzMSAxMC40NTktMjguODMxIDE3LjM0OCAwIDEyLjg3OSAzOC45MTUgMjMuMzIgODYuOTE5IDIzLjMyczg2LjkxOC0xMC40NDEgODYuOTE4LTIzLjMyYzAtNi44ODg2LTExLjEyOS0xMy4wNzktMjguODMtMTcuMzQ4IDAuMDM1LTAuMDMxMiAwLjA2ODctMC4wNjAxIDAuMTAzNzUtMC4wODk5IDE3LjgzIDQuMjg1MSAyOS4wNDggMTAuNTEgMjkuMDQ4IDE3LjQzOCAwIDEyLjkyNi0zOS4wNTkgMjMuNDA2LTg3LjIzOSAyMy40MDYiLz4KICAgPHBhdGggaWQ9InBhdGg1NTg5IiBkPSJtMzE5Ljg5IDkyMy40Yy00OC4wMDQgMC04Ni45MTktMTAuNDQxLTg2LjkxOS0yMy4zMiAwLTYuODg4NiAxMS4xMy0xMy4wNzkgMjguODMxLTE3LjM0OCAwLjAzNjIgMC4wMzEyIDAuMDY4NyAwLjA2IDAuMTA1IDAuMDkwMi0xNy41NyA0LjI1MjUtMjguNjE1IDEwLjQxLTI4LjYxNSAxNy4yNTcgMCAxMi44MzEgMzguNzcxIDIzLjIzNCA4Ni41OTggMjMuMjM0IDQ3LjgyNiAwIDg2LjU5OC0xMC40MDMgODYuNTk4LTIzLjIzNCAwLTYuODQ3Ni0xMS4wNDUtMTMuMDA1LTI4LjYxNC0xNy4yNTYgMC4wMzUtMC4wMzEyIDAuMDY4Ny0wLjA2IDAuMTAzNzUtMC4wOTEyIDE3LjcwMSA0LjI2OSAyOC44MyAxMC40NTkgMjguODMgMTcuMzQ4IDAgMTIuODc5LTM4LjkxNCAyMy4zMi04Ni45MTggMjMuMzIiLz4KICAgPHBhdGggaWQ9InBhdGg1NTkxIiBkPSJtMzE5Ljg5IDkyMy4zMWMtNDcuODI2IDAtODYuNTk4LTEwLjQwMy04Ni41OTgtMjMuMjM0IDAtNi44NDc2IDExLjA0NS0xMy4wMDUgMjguNjE1LTE3LjI1NyAwLjAzMjUgMC4wMjk5IDAuMDcgMC4wNjI1IDAuMTAzNzUgMC4wOTE0LTE3LjQ0MSA0LjIzNDktMjguMzk4IDEwLjM1Ny0yOC4zOTggMTcuMTY2IDAgMTIuNzg0IDM4LjYyNiAyMy4xNDcgODYuMjc2IDIzLjE0NyA0Ny42NDkgMCA4Ni4yNzYtMTAuMzY0IDg2LjI3Ni0yMy4xNDcgMC02LjgwODYtMTAuOTU2LTEyLjkzMS0yOC4zOTgtMTcuMTY2IDAuMDMzNy0wLjAyODkgMC4wNzEyLTAuMDYxNSAwLjEwNS0wLjA5MDQgMTcuNTY5IDQuMjUxNSAyOC42MTQgMTAuNDA5IDI4LjYxNCAxNy4yNTYgMCAxMi44MzEtMzguNzcxIDIzLjIzNC04Ni41OTggMjMuMjM0Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTU5MyIgZD0ibTMxOS44OSA5MjMuMjNjLTQ3LjY1IDAtODYuMjc2LTEwLjM2NC04Ni4yNzYtMjMuMTQ4IDAtNi44MDg2IDEwLjk1Ni0xMi45MzEgMjguMzk4LTE3LjE2NiAwLjAzNjIgMC4wMzA4IDAuMDY4NyAwLjA1OTUgMC4xMDUgMC4wODk5LTE3LjMxNCA0LjIxOTYtMjguMTgyIDEwLjMwOC0yOC4xODIgMTcuMDc2IDAgMTIuNzM2IDM4LjQ4NCAyMy4wNjEgODUuOTU2IDIzLjA2MXM4NS45NTYtMTAuMzI1IDg1Ljk1Ni0yMy4wNjFjMC02Ljc2ODUtMTAuODcyLTEyLjg1Ni0yOC4xODItMTcuMDc1IDAuMDM2My0wLjAzMTIgMC4wNy0wLjA2IDAuMTA1LTAuMDkwOCAxNy40NDEgNC4yMzQ5IDI4LjM5OCAxMC4zNTcgMjguMzk4IDE3LjE2NiAwIDEyLjc4NC0zOC42MjggMjMuMTQ3LTg2LjI3NiAyMy4xNDciLz4KICAgPHBhdGggaWQ9InBhdGg1NTk1IiBkPSJtMzE5Ljg5IDkyMy4xNGMtNDcuNDcyIDAtODUuOTU2LTEwLjMyNS04NS45NTYtMjMuMDYxIDAtNi43Njg1IDEwLjg2OS0xMi44NTYgMjguMTgyLTE3LjA3NiAwLjAzNSAwLjAzMTIgMC4wNyAwLjA2MSAwLjEwNSAwLjA5MTMtMTcuMTg0IDQuMjAyMS0yNy45NjYgMTAuMjU2LTI3Ljk2NiAxNi45ODUgMCAxMi42ODggMzguMzQgMjIuOTc2IDg1LjYzNSAyMi45NzZzODUuNjM1LTEwLjI4OCA4NS42MzUtMjIuOTc2YzAtNi43MjktMTAuNzgyLTEyLjc4My0yNy45NjYtMTYuOTg1IDAuMDM1LTAuMDMwMyAwLjA3LTAuMDYgMC4xMDUtMC4wOTA0IDE3LjMxIDQuMjE4OCAyOC4xODIgMTAuMzA3IDI4LjE4MiAxNy4wNzUgMCAxMi43MzYtMzguNDg0IDIzLjA2MS04NS45NTYgMjMuMDYxIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTU5NyIgZD0ibTMxOS44OSA5MjMuMDZjLTQ3LjI5NSAwLTg1LjYzNS0xMC4yODgtODUuNjM1LTIyLjk3NiAwLTYuNzI5IDEwLjc4Mi0xMi43ODMgMjcuOTY2LTE2Ljk4NSAwLjAzMzcgMC4wMjg5IDAuMDcyNSAwLjA2MjUgMC4xMDYyNSAwLjA5MTQtMTcuMDU1IDQuMTg0NS0yNy43NTIgMTAuMjA0LTI3Ljc1MiAxNi44OTQgMCAxMi42NDEgMzguMTk4IDIyLjg5IDg1LjMxNSAyMi44OSA0Ny4xMTggMCA4NS4zMTQtMTAuMjQ5IDg1LjMxNC0yMi44OSAwLTYuNjg5LTEwLjY5Ni0xMi43MDktMjcuNzUxLTE2Ljg5NCAwLjAzMzctMC4wMjg5IDAuMDcyNS0wLjA2MjUgMC4xMDYyNS0wLjA5MTQgMTcuMTg0IDQuMjAyMSAyNy45NjYgMTAuMjU2IDI3Ljk2NiAxNi45ODUgMCAxMi42ODgtMzguMzQgMjIuOTc2LTg1LjYzNSAyMi45NzYiLz4KICAgPHBhdGggaWQ9InBhdGg1NTk5IiBkPSJtMzE5Ljg5IDkyMi45N2MtNDcuMTE4IDAtODUuMzE1LTEwLjI0OS04NS4zMTUtMjIuODkgMC02LjY4OSAxMC42OTgtMTIuNzA5IDI3Ljc1Mi0xNi44OTQgMC4wMzUgMC4wMjk4IDAuMDcgMC4wNTk1IDAuMTA1IDAuMDg5Ny0xNi45MjUgNC4xNjktMjcuNTM2IDEwLjE1NC0yNy41MzYgMTYuODA0IDAgMTIuNTk0IDM4LjA1MiAyMi44MDQgODQuOTk0IDIyLjgwNCA0Ni45NCAwIDg0Ljk5NC0xMC4yMSA4NC45OTQtMjIuODA0IDAtNi42NDk5LTEwLjYxMS0xMi42MzUtMjcuNTM2LTE2LjgwNCAwLjAzNS0wLjAzMDMgMC4wNy0wLjA2IDAuMTA1LTAuMDg5NyAxNy4wNTUgNC4xODQ1IDI3Ljc1MSAxMC4yMDQgMjcuNzUxIDE2Ljg5NCAwIDEyLjY0MS0zOC4xOTYgMjIuODktODUuMzE0IDIyLjg5Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTYwMSIgZD0ibTMxOS44OSA5MjIuODhjLTQ2Ljk0MSAwLTg0Ljk5NC0xMC4yMS04NC45OTQtMjIuODA0IDAtNi42NDk5IDEwLjYxMS0xMi42MzUgMjcuNTM2LTE2LjgwNCAwLjAzNjIgMC4wMzEyIDAuMDcxMyAwLjA2MDEgMC4xMDYyNSAwLjA5MTQtMTYuNzk5IDQuMTUxNC0yNy4zMjEgMTAuMTAxLTI3LjMyMSAxNi43MTIgMCAxMi41NDYgMzcuOTA5IDIyLjcxNyA4NC42NzIgMjIuNzE3IDQ2Ljc2NCAwIDg0LjY3Mi0xMC4xNzEgODQuNjcyLTIyLjcxNyAwLTYuNjA5OS0xMC41MjUtMTIuNTYxLTI3LjMyMS0xNi43MTIgMC4wMzUtMC4wMzEyIDAuMDctMC4wNjAxIDAuMTA2MjUtMC4wOTE0IDE2LjkyNSA0LjE2OSAyNy41MzYgMTAuMTU0IDI3LjUzNiAxNi44MDQgMCAxMi41OTQtMzguMDU0IDIyLjgwNC04NC45OTQgMjIuODA0Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTYwMyIgZD0ibTMxOS44OSA5MjIuOGMtNDYuNzY0IDAtODQuNjcyLTEwLjE3MS04NC42NzItMjIuNzE3IDAtNi42MTE0IDEwLjUyMi0xMi41NjEgMjcuMzIxLTE2LjcxMiAwLjAzNjIgMC4wMjk3IDAuMDcxMyAwLjA2IDAuMTA3NSAwLjA4OTktMTYuNjY5IDQuMTM1Mi0yNy4xMDkgMTAuMDUzLTI3LjEwOSAxNi42MjIgMCAxMi40OTkgMzcuNzY2IDIyLjYzMSA4NC4zNTIgMjIuNjMxczg0LjM1MS0xMC4xMzMgODQuMzUxLTIyLjYzMWMwLTYuNTY5OS0xMC40MzktMTIuNDg3LTI3LjEwNi0xNi42MjIgMC4wMzUtMC4wMjk5IDAuMDctMC4wNjAxIDAuMTA2MjUtMC4wODk5IDE2Ljc5NiA0LjE1MTQgMjcuMzIxIDEwLjEwMiAyNy4zMjEgMTYuNzEyIDAgMTIuNTQ2LTM3LjkwOSAyMi43MTctODQuNjcyIDIyLjcxNyIvPgogICA8cGF0aCBpZD0icGF0aDU2MDUiIGQ9Im0zMTkuODkgOTIyLjcxYy00Ni41ODYgMC04NC4zNTItMTAuMTMzLTg0LjM1Mi0yMi42MzEgMC02LjU2OTkgMTAuNDQtMTIuNDg3IDI3LjEwOS0xNi42MjIgMC4wMzUgMC4wMzEyIDAuMDcgMC4wNjE1IDAuMTA2MjUgMC4wOTEzLTE2LjU0MSA0LjExNzYtMjYuODk0IDEwLTI2Ljg5NCAxNi41MzEgMCAxMi40NTEgMzcuNjIyIDIyLjU0NSA4NC4wMzEgMjIuNTQ1czg0LjAzMS0xMC4wOTQgODQuMDMxLTIyLjU0NWMwLTYuNTMxMi0xMC4zNTItMTIuNDE0LTI2Ljg5NC0xNi41MzEgMC4wMzYyLTAuMDI5OCAwLjA3MTItMC4wNiAwLjEwNzUtMC4wOTEzIDE2LjY2OCA0LjEzNTIgMjcuMTA2IDEwLjA1MyAyNy4xMDYgMTYuNjIyIDAgMTIuNDk5LTM3Ljc2NSAyMi42MzEtODQuMzUxIDIyLjYzMSIvPgogICA8cGF0aCBpZD0icGF0aDU2MDciIGQ9Im0zMTkuODkgOTIyLjYyYy00Ni40MDkgMC04NC4wMzEtMTAuMDk0LTg0LjAzMS0yMi41NDUgMC02LjUzMTIgMTAuMzUyLTEyLjQxNCAyNi44OTQtMTYuNTMxIDAuMDM2MiAwLjAyOTcgMC4wNzEzIDAuMDYgMC4xMDc1IDAuMDg5OS0xNi40MTQgNC4xMDE1LTI2LjY4MSA5Ljk1MDEtMjYuNjgxIDE2LjQ0MSAwIDEyLjQwNCAzNy40NzkgMjIuNDU5IDgzLjcxMSAyMi40NTlzODMuNzEtMTAuMDU1IDgzLjcxLTIyLjQ1OWMwLTYuNDkxMi0xMC4yNjUtMTIuMzQtMjYuNjgtMTYuNDQgMC4wMzYyLTAuMDMxMiAwLjA3MTItMC4wNjE1IDAuMTA3NS0wLjA5MTIgMTYuNTQxIDQuMTE3NiAyNi44OTQgMTAgMjYuODk0IDE2LjUzMSAwIDEyLjQ1MS0zNy42MjIgMjIuNTQ1LTg0LjAzMSAyMi41NDUiLz4KICAgPHBhdGggaWQ9InBhdGg1NjA5IiBkPSJtMzE5Ljg5IDkyMi41NGMtNDYuMjMyIDAtODMuNzExLTEwLjA1NS04My43MTEtMjIuNDU5IDAtNi40OTEyIDEwLjI2OC0xMi4zNCAyNi42ODEtMTYuNDQxIDAuMDM2MiAwLjAzMTIgMC4wNzEyIDAuMDYxNSAwLjEwNzUgMC4wOTEyLTE2LjI4OCA0LjA4NC0yNi40NjggOS44OTc1LTI2LjQ2OCAxNi4zNSAwIDEyLjM1NiAzNy4zMzUgMjIuMzczIDgzLjM5IDIyLjM3M3M4My4zOS0xMC4wMTYgODMuMzktMjIuMzczYzAtNi40NTI2LTEwLjE4LTEyLjI2Ni0yNi40NjgtMTYuMzUgMC4wMzYyLTAuMDI5NyAwLjA3MTMtMC4wNiAwLjEwNzUtMC4wODk5IDE2LjQxNSA0LjEwMDEgMjYuNjggOS45NDg4IDI2LjY4IDE2LjQ0IDAgMTIuNDA0LTM3LjQ3OCAyMi40NTktODMuNzEgMjIuNDU5Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTYxMSIgZD0ibTMxOS44OSA5MjIuNDVjLTQ2LjA1NSAwLTgzLjM5LTEwLjAxNi04My4zOS0yMi4zNzMgMC02LjQ1MjYgMTAuMTgtMTIuMjY2IDI2LjQ2OC0xNi4zNSAwLjAzNjMgMC4wMzAzIDAuMDcyNSAwLjA2MDEgMC4xMDg3NSAwLjA5MTQtMTYuMTYgNC4wNjY0LTI2LjI1NSA5Ljg0NjEtMjYuMjU1IDE2LjI1OSAwIDEyLjMwOSAzNy4xOTEgMjIuMjg4IDgzLjA2OSAyMi4yODhzODMuMDY5LTkuOTc5IDgzLjA2OS0yMi4yODhjMC02LjQxMjYtMTAuMDk1LTEyLjE5Mi0yNi4yNTQtMTYuMjU5IDAuMDM1LTAuMDI5OSAwLjA3MTItMC4wNjExIDAuMTA3NS0wLjA5MTQgMTYuMjg4IDQuMDg0IDI2LjQ2OCA5Ljg5NzUgMjYuNDY4IDE2LjM1IDAgMTIuMzU2LTM3LjMzNSAyMi4zNzMtODMuMzkgMjIuMzczIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTYxMyIgZD0ibTMxOS44OSA5MjIuMzdjLTQ1Ljg3OCAwLTgzLjA2OS05Ljk3OS04My4wNjktMjIuMjg4IDAtNi40MTI2IDEwLjA5NS0xMi4xOTIgMjYuMjU1LTE2LjI1OSAwLjAzNSAwLjAzMDMgMC4wNzEyIDAuMDYgMC4xMDc1IDAuMDg5OC0xNi4wMyA0LjA0ODktMjYuMDQyIDkuNzk2NS0yNi4wNDIgMTYuMTY5IDAgMTIuMjYxIDM3LjA0OSAyMi4yMDEgODIuNzQ5IDIyLjIwMXM4Mi43NDgtOS45Mzk5IDgyLjc0OC0yMi4yMDFjMC02LjM3MjUtMTAuMDExLTEyLjEyLTI2LjA0MS0xNi4xNjkgMC4wMzYyLTAuMDI5OCAwLjA3MjUtMC4wNTk1IDAuMTA4NzUtMC4wODk4IDE2LjE1OSA0LjA2NjQgMjYuMjU0IDkuODQ2MSAyNi4yNTQgMTYuMjU5IDAgMTIuMzA5LTM3LjE5MSAyMi4yODgtODMuMDY5IDIyLjI4OCIvPgogICA8cGF0aCBpZD0icGF0aDU2MTUiIGQ9Im0zMTkuODkgOTIyLjI4Yy00NS43IDAtODIuNzQ5LTkuOTQtODIuNzQ5LTIyLjIwMSAwLTYuMzcyNSAxMC4wMTItMTIuMTIgMjYuMDQyLTE2LjE2OSAwLjAzNjMgMC4wMzA0IDAuMDcyNSAwLjA2MTYgMC4xMDg3NSAwLjA5MTQtMTUuOTA2IDQuMDMxMi0yNS44MyA5Ljc0NDEtMjUuODMgMTYuMDc4IDAgMTIuMjE0IDM2LjkwNCAyMi4xMTUgODIuNDI4IDIyLjExNXM4Mi40MjgtOS45MDA5IDgyLjQyOC0yMi4xMTVjMC02LjMzMzUtOS45MjM4LTEyLjA0Ni0yNS44My0xNi4wNzggMC4wMzYyLTAuMDI5OCAwLjA3MjUtMC4wNjAxIDAuMTA4NzUtMC4wOTE0IDE2LjAzIDQuMDQ4OSAyNi4wNDEgOS43OTY1IDI2LjA0MSAxNi4xNjkgMCAxMi4yNjEtMzcuMDQ4IDIyLjIwMS04Mi43NDggMjIuMjAxIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTYxNyIgZD0ibTMxOS44OSA5MjIuMTljLTQ1LjUyNCAwLTgyLjQyOC05LjkwMDgtODIuNDI4LTIyLjExNSAwLTYuMzMzNSA5LjkyMzgtMTIuMDQ2IDI1LjgzLTE2LjA3OCAwLjAzNjIgMC4wMzAzIDAuMDczNyAwLjA2MTUgMC4xMSAwLjA5MTMtMTUuNzggNC4wMTM4LTI1LjYxOSA5LjY5MTUtMjUuNjE5IDE1Ljk4NiAwIDEyLjE2NiAzNi43NiAyMi4wMjkgODIuMTA2IDIyLjAyOXM4Mi4xMDYtOS44NjI4IDgyLjEwNi0yMi4wMjljMC02LjI5NDktOS44Mzg4LTExLjk3My0yNS42MTgtMTUuOTg2IDAuMDM2My0wLjAyOTcgMC4wNzI1LTAuMDYxIDAuMTA4NzUtMC4wOTEzIDE1LjkwNiA0LjAzMTIgMjUuODMgOS43NDQxIDI1LjgzIDE2LjA3OCAwIDEyLjIxNC0zNi45MDQgMjIuMTE1LTgyLjQyOCAyMi4xMTUiLz4KICAgPHBhdGggaWQ9InBhdGg1NjE5IiBkPSJtMzE5Ljg5IDkyMi4xMWMtNDUuMzQ2IDAtODIuMTA2LTkuODYyOC04Mi4xMDYtMjIuMDI5IDAtNi4yOTQ5IDkuODM4OC0xMS45NzMgMjUuNjE5LTE1Ljk4NiAwLjAzNSAwLjAzMDQgMC4wNzI1IDAuMDYwMSAwLjEwODc1IDAuMDg5OS0xNS42NTIgMy45OTc2LTI1LjQwOCA5LjY0MTYtMjUuNDA4IDE1Ljg5NiAwIDEyLjExOSAzNi42MTggMjEuOTQyIDgxLjc4NiAyMS45NDIgNDUuMTY5IDAgODEuNzg2LTkuODIzNyA4MS43ODYtMjEuOTQyIDAtNi4yNTQ5LTkuNzU1LTExLjg5OS0yNS40MDgtMTUuODk2IDAuMDM2Mi0wLjAyOTcgMC4wNzM3LTAuMDU5NSAwLjExLTAuMDg5OSAxNS43NzkgNC4wMTM4IDI1LjYxOCA5LjY5MTUgMjUuNjE4IDE1Ljk4NiAwIDEyLjE2Ni0zNi43NiAyMi4wMjktODIuMTA2IDIyLjAyOSIvPgogICA8cGF0aCBpZD0icGF0aDU2MjEiIGQ9Im0zMTkuODkgOTIyLjAyYy00NS4xNjkgMC04MS43ODYtOS44MjM4LTgxLjc4Ni0yMS45NDIgMC02LjI1NDkgOS43NTUtMTEuODk5IDI1LjQwOC0xNS44OTYgMC4wMzYyIDAuMDMwMyAwLjA3MzcgMC4wNjE1IDAuMTEgMC4wOTE0LTE1LjUyNiAzLjk3OTktMjUuMTk2IDkuNTg4OS0yNS4xOTYgMTUuODA1IDAgMTIuMDcxIDM2LjQ3MiAyMS44NTYgODEuNDY1IDIxLjg1NiA0NC45OTEgMCA4MS40NjUtOS43ODUyIDgxLjQ2NS0yMS44NTYgMC02LjIxNjItOS42Ny0xMS44MjUtMjUuMTk2LTE1LjgwNSAwLjAzNjItMC4wMjk5IDAuMDczOC0wLjA2MTEgMC4xMS0wLjA5MTQgMTUuNjUyIDMuOTk3NiAyNS40MDggOS42NDE2IDI1LjQwOCAxNS44OTYgMCAxMi4xMTktMzYuNjE4IDIxLjk0Mi04MS43ODYgMjEuOTQyIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTYyMyIgZD0ibTMxOS44OSA5MjEuOTRjLTQ0Ljk5MiAwLTgxLjQ2NS05Ljc4NTEtODEuNDY1LTIxLjg1NiAwLTYuMjE2MiA5LjY3LTExLjgyNSAyNS4xOTYtMTUuODA1IDAuMDM2MyAwLjAzMDIgMC4wNzM4IDAuMDYxNSAwLjExIDAuMDkxMi0xNS4zOTggMy45NjE0LTI0Ljk4NSA5LjUzNzYtMjQuOTg1IDE1LjcxNCAwIDEyLjAyNCAzNi4zMjkgMjEuNzcgODEuMTQ0IDIxLjc3czgxLjE0NC05Ljc0NjEgODEuMTQ0LTIxLjc3YzAtNi4xNzYyLTkuNTg3NS0xMS43NTItMjQuOTg1LTE1LjcxNCAwLjAzNjItMC4wMjk3IDAuMDczNy0wLjA2MSAwLjExLTAuMDkxMiAxNS41MjYgMy45Nzk5IDI1LjE5NiA5LjU4ODkgMjUuMTk2IDE1LjgwNSAwIDEyLjA3MS0zNi40NzQgMjEuODU2LTgxLjQ2NSAyMS44NTYiLz4KICAgPHBhdGggaWQ9InBhdGg1NjI1IiBkPSJtMzE5Ljg5IDkyMS44NWMtNDQuODE1IDAtODEuMTQ0LTkuNzQ2MS04MS4xNDQtMjEuNzcgMC02LjE3NjIgOS41ODc1LTExLjc1MiAyNC45ODUtMTUuNzE0IDAuMDM2MiAwLjAyODkgMC4wNzM4IDAuMDYwMSAwLjExMTI1IDAuMDkwNC0xNS4yNzQgMy45NDQ4LTI0Ljc3NiA5LjQ4NTgtMjQuNzc2IDE1LjYyNCAwIDExLjk3NiAzNi4xODYgMjEuNjg1IDgwLjgyNCAyMS42ODVzODAuODI0LTkuNzA5IDgwLjgyNC0yMS42ODVjMC02LjEzNzgtOS41MDI1LTExLjY3OS0yNC43NzUtMTUuNjI0IDAuMDM2Mi0wLjAzMDMgMC4wNzM4LTAuMDYwMSAwLjExLTAuMDkwNCAxNS4zOTggMy45NjE0IDI0Ljk4NSA5LjUzNzYgMjQuOTg1IDE1LjcxNCAwIDEyLjAyNC0zNi4zMjkgMjEuNzctODEuMTQ0IDIxLjc3Ii8+CiAgPC9nPgogIDxnPgogICA8cGF0aCBpZD0icGF0aDU2MjciIGQ9Im0zMTkuODkgOTIxLjc3Yy00NC42MzggMC04MC44MjQtOS43MDktODAuODI0LTIxLjY4NSAwLTYuMTM3OCA5LjUwMjUtMTEuNjc5IDI0Ljc3Ni0xNS42MjQgMC4wMzc1IDAuMDMwOCAwLjA3MTMgMC4wNTk1IDAuMTEgMC4wOTA3LTE1LjE0NSAzLjkyNzgtMjQuNTY1IDkuNDM1Mi0yNC41NjUgMTUuNTMzIDAgMTEuOTI5IDM2LjA0MiAyMS41OTkgODAuNTAyIDIxLjU5OXM4MC41MDItOS42Njk5IDgwLjUwMi0yMS41OTljMC02LjA5NzYtOS40Mi0xMS42MDUtMjQuNTY1LTE1LjUzMyAwLjAzODctMC4wMzEyIDAuMDcyNS0wLjA1ODUgMC4xMTEyNS0wLjA5MDcgMTUuMjcyIDMuOTQ0OCAyNC43NzUgOS40ODU4IDI0Ljc3NSAxNS42MjQgMCAxMS45NzYtMzYuMTg2IDIxLjY4NS04MC44MjQgMjEuNjg1IiBmaWxsPSIjZmVmZmZmIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTYyOSIgZD0ibTMxOS44OSA5MjEuNjhjLTQ0LjQ2IDAtODAuNTAyLTkuNjY5OS04MC41MDItMjEuNTk5IDAtNi4wOTc2IDkuNDItMTEuNjA1IDI0LjU2NS0xNS41MzMgMC4wMzYyIDAuMDMwMyAwLjA3NSAwLjA2MTUgMC4xMTEyNSAwLjA5MTQtMTUuMDIxIDMuOTA4Ni0yNC4zNTYgOS4zODI3LTI0LjM1NiAxNS40NDEgMCAxMS44ODEgMzUuODk5IDIxLjUxMyA4MC4xODIgMjEuNTEzIDQ0LjI4NCAwIDgwLjE4Mi05LjYzMTQgODAuMTgyLTIxLjUxMyAwLTYuMDU4Ni05LjMzNS0xMS41MzEtMjQuMzU2LTE1LjQ0MSAwLjAzNjItMC4wMjk5IDAuMDc1LTAuMDYxMSAwLjExMTI1LTAuMDkxNCAxNS4xNDUgMy45Mjc4IDI0LjU2NSA5LjQzNTEgMjQuNTY1IDE1LjUzMyAwIDExLjkyOS0zNi4wNDIgMjEuNTk5LTgwLjUwMiAyMS41OTkiIGZpbGw9IiNmZWZmZmYiLz4KICAgPHBhdGggaWQ9InBhdGg1NjMxIiBkPSJtMzE5Ljg5IDkyMS41OWMtNDQuMjg0IDAtODAuMTgyLTkuNjMxNC04MC4xODItMjEuNTEzIDAtNi4wNTg2IDkuMzM1LTExLjUzMyAyNC4zNTYtMTUuNDQxIDAuMDM3NSAwLjAyODcgMC4wNzYyIDAuMDYgMC4xMTI1IDAuMDkwMi0xNC44OTYgMy44OTIxLTI0LjE0OCA5LjMzMjYtMjQuMTQ4IDE1LjM1MSAwIDExLjgzNCAzNS43NTUgMjEuNDI2IDc5Ljg2MSAyMS40MjZzNzkuODYxLTkuNTkyMyA3OS44NjEtMjEuNDI2YzAtNi4wMTg1LTkuMjUxMi0xMS40NTktMjQuMTQ4LTE1LjM1MSAwLjAzNzUtMC4wMjg3IDAuMDc1LTAuMDYxNSAwLjExMjUtMC4wOTAyIDE1LjAyMSAzLjkxMDEgMjQuMzU2IDkuMzgyOCAyNC4zNTYgMTUuNDQxIDAgMTEuODgxLTM1Ljg5OSAyMS41MTMtODAuMTgyIDIxLjUxMyIgZmlsbD0iI2ZlZmVmZiIvPgogICA8cGF0aCBpZD0icGF0aDU2MzMiIGQ9Im0zMTkuODkgOTIxLjUxYy00NC4xMDYgMC03OS44NjEtOS41OTIzLTc5Ljg2MS0yMS40MjYgMC02LjAxODUgOS4yNTEyLTExLjQ1OSAyNC4xNDgtMTUuMzUxIDAuMDM4NyAwLjAzMTIgMC4wNzI1IDAuMDYwMSAwLjExMTI1IDAuMDkwOS0xNC43NyAzLjg3NTUtMjMuOTM5IDkuMjgwMi0yMy45MzkgMTUuMjYgMCAxMS43ODYgMzUuNjEyIDIxLjM0IDc5LjU0MSAyMS4zNHM3OS41NC05LjU1MzcgNzkuNTQtMjEuMzRjMC01Ljk4LTkuMTY3NS0xMS4zODUtMjMuOTM4LTE1LjI2IDAuMDM4Ny0wLjAzMDggMC4wNzI1LTAuMDU5NiAwLjExMTI1LTAuMDkwOSAxNC44OTYgMy44OTIxIDI0LjE0OCA5LjMzMjYgMjQuMTQ4IDE1LjM1MSAwIDExLjgzNC0zNS43NTUgMjEuNDI2LTc5Ljg2MSAyMS40MjYiIGZpbGw9IiNmZWZlZmUiLz4KICAgPHBhdGggaWQ9InBhdGg1NjM1IiBkPSJtMzE5Ljg5IDkyMS40MmMtNDMuOTI5IDAtNzkuNTQxLTkuNTUzNy03OS41NDEtMjEuMzQgMC01Ljk4IDkuMTY4OC0xMS4zODUgMjMuOTM5LTE1LjI2IDAuMDM3NSAwLjAzMDMgMC4wNzYyIDAuMDYxNSAwLjExMzc1IDAuMDkxMy0xNC42NDYgMy44NTY0LTIzLjczMSA5LjIyNzYtMjMuNzMxIDE1LjE2OSAwIDExLjczOSAzNS40NjggMjEuMjU1IDc5LjIyIDIxLjI1NSA0My43NTEgMCA3OS4yMi05LjUxNjEgNzkuMjItMjEuMjU1IDAtNS45NDE0LTkuMDg1LTExLjMxMi0yMy43My0xNS4xNjkgMC4wMzYzLTAuMDI5OCAwLjA3NS0wLjA2MSAwLjExMjUtMC4wOTEzIDE0Ljc3IDMuODc1NSAyMy45MzggOS4yODAyIDIzLjkzOCAxNS4yNiAwIDExLjc4Ni0zNS42MTEgMjEuMzQtNzkuNTQgMjEuMzQiIGZpbGw9IiNmZWZlZmUiLz4KICAgPHBhdGggaWQ9InBhdGg1NjM3IiBkPSJtMzE5Ljg5IDkyMS4zM2MtNDMuNzUyIDAtNzkuMjItOS41MTYxLTc5LjIyLTIxLjI1NSAwLTUuOTQxNCA5LjA4NS0xMS4zMTIgMjMuNzMxLTE1LjE2OSAwLjAzODcgMC4wMzEyIDAuMDczOCAwLjA1OTEgMC4xMTI1IDAuMDkwNC0xNC41MjEgMy44Mzk5LTIzLjUyMiA5LjE3NzItMjMuNTIyIDE1LjA3OSAwIDExLjY5MSAzNS4zMjQgMjEuMTY5IDc4Ljg5OSAyMS4xNjlzNzguODk5LTkuNDc3NiA3OC44OTktMjEuMTY5YzAtNS45MDE0LTkuMDAxMi0xMS4yMzktMjMuNTIyLTE1LjA3OCAwLjAzODctMC4wMzEyIDAuMDc1LTAuMDYwMSAwLjExMzc1LTAuMDkxNCAxNC42NDUgMy44NTY1IDIzLjczIDkuMjI3NiAyMy43MyAxNS4xNjkgMCAxMS43MzktMzUuNDY5IDIxLjI1NS03OS4yMiAyMS4yNTUiIGZpbGw9IiNmZGZlZmUiLz4KICAgPHBhdGggaWQ9InBhdGg1NjM5IiBkPSJtMzE5Ljg5IDkyMS4yNWMtNDMuNTc1IDAtNzguODk5LTkuNDc3Ni03OC44OTktMjEuMTY5IDAtNS45MDE0IDkuMDAxMi0xMS4yMzkgMjMuNTIyLTE1LjA3OSAwLjAzNjIgMC4wMjk3IDAuMDc2MiAwLjA2MjUgMC4xMTI1IDAuMDkxNC0xNC4zOTQgMy44MjA4LTIzLjMxNSA5LjEyNS0yMy4zMTUgMTQuOTg3IDAgMTEuNjQ0IDM1LjE4MSAyMS4wODMgNzguNTc5IDIxLjA4M3M3OC41NzktOS40MzkgNzguNTc5LTIxLjA4M2MwLTUuODYyMi04LjkyMTItMTEuMTY2LTIzLjMxNS0xNC45ODcgMC4wMzYyLTAuMDI4OSAwLjA3NjMtMC4wNjE2IDAuMTEyNS0wLjA5MDQgMTQuNTIxIDMuODM4OSAyMy41MjIgOS4xNzYyIDIzLjUyMiAxNS4wNzggMCAxMS42OTEtMzUuMzI0IDIxLjE2OS03OC44OTkgMjEuMTY5IiBmaWxsPSIjZmRmZWZlIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTY0MSIgZD0ibTMxOS44OSA5MjEuMTZjLTQzLjM5OCAwLTc4LjU3OS05LjQzOS03OC41NzktMjEuMDgzIDAtNS44NjIyIDguOTIxMi0xMS4xNjYgMjMuMzE1LTE0Ljk4NyAwLjA0IDAuMDMxMiAwLjA3NSAwLjA2IDAuMTEzNzUgMC4wOTA3LTE0LjI3MSAzLjgwNDItMjMuMTA4IDkuMDcyOC0yMy4xMDggMTQuODk2IDAgMTEuNTk2IDM1LjAzNiAyMC45OTYgNzguMjU4IDIwLjk5NiA0My4yMiAwIDc4LjI1OC05LjM5OTkgNzguMjU4LTIwLjk5NiAwLTUuODIzOC04LjgzNjItMTEuMDkyLTIzLjEwOC0xNC44OTYgMC4wMzg3LTAuMDMwOCAwLjA3NS0wLjA1OTUgMC4xMTM3NS0wLjA5MDcgMTQuMzk0IDMuODIwOCAyMy4zMTUgOS4xMjUgMjMuMzE1IDE0Ljk4NyAwIDExLjY0NC0zNS4xODEgMjEuMDgzLTc4LjU3OSAyMS4wODMiIGZpbGw9IiNmZGZkZmUiLz4KICAgPHBhdGggaWQ9InBhdGg1NjQzIiBkPSJtMzE5Ljg5IDkyMS4wOGMtNDMuMjIxIDAtNzguMjU4LTkuMzk5OS03OC4yNTgtMjAuOTk2IDAtNS44MjM4IDguODM2Mi0xMS4wOTIgMjMuMTA4LTE0Ljg5NiAwLjAzNzUgMC4wMjg5IDAuMDc3NSAwLjA2MTUgMC4xMTUgMC4wOTE0LTE0LjE0NiAzLjc4NTEtMjIuOTAxIDkuMDIxNC0yMi45MDEgMTQuODA1IDAgMTEuNTQ5IDM0Ljg5MiAyMC45MSA3Ny45MzYgMjAuOTEgNDMuMDQyIDAgNzcuOTM2LTkuMzYxMyA3Ny45MzYtMjAuOTEgMC01Ljc4MzgtOC43NTUtMTEuMDItMjIuOTAxLTE0LjgwNSAwLjAzNzUtMC4wMjk5IDAuMDc3NS0wLjA2MjUgMC4xMTUtMC4wOTE0IDE0LjI3MSAzLjgwNDIgMjMuMTA4IDkuMDcyOCAyMy4xMDggMTQuODk2IDAgMTEuNTk2LTM1LjAzOCAyMC45OTYtNzguMjU4IDIwLjk5NiIgZmlsbD0iI2ZkZmRmZCIvPgogICA8cGF0aCBpZD0icGF0aDU2NDUiIGQ9Im0zMTkuODkgOTIwLjk5Yy00My4wNDQgMC03Ny45MzYtOS4zNjEzLTc3LjkzNi0yMC45MSAwLTUuNzgzOCA4Ljc1NS0xMS4wMiAyMi45MDEtMTQuODA1IDAuMDM4NyAwLjAzMTIgMC4wNzUgMC4wNiAwLjExMzc1IDAuMDkwMi0xNC4wMjQgMy43Njg2LTIyLjY5NSA4Ljk2OTctMjIuNjk1IDE0LjcxNSAwIDExLjUwMSAzNC43NSAyMC44MjQgNzcuNjE2IDIwLjgyNHM3Ny42MTYtOS4zMjIzIDc3LjYxNi0yMC44MjRjMC01Ljc0NTEtOC42Ny0xMC45NDYtMjIuNjk1LTE0LjcxNSAwLjAzODctMC4wMzAzIDAuMDc1LTAuMDU5IDAuMTEzNzUtMC4wOTAyIDE0LjE0NiAzLjc4NTEgMjIuOTAxIDkuMDIxNCAyMi45MDEgMTQuODA1IDAgMTEuNTQ5LTM0Ljg5NCAyMC45MS03Ny45MzYgMjAuOTEiIGZpbGw9IiNmZGZkZmQiLz4KICAgPHBhdGggaWQ9InBhdGg1NjQ3IiBkPSJtMzE5Ljg5IDkyMC45Yy00Mi44NjYgMC03Ny42MTYtOS4zMjIzLTc3LjYxNi0yMC44MjQgMC01Ljc0NTEgOC42NzEyLTEwLjk0NiAyMi42OTUtMTQuNzE1IDAuMDM3NSAwLjAyOTkgMC4wNzg4IDAuMDYyNSAwLjExNSAwLjA5MTQtMTMuOSAzLjc1LTIyLjQ4OSA4LjkxNzUtMjIuNDg5IDE0LjYyNCAwIDExLjQ1NCAzNC42MDYgMjAuNzM3IDc3LjI5NSAyMC43MzdzNzcuMjk1LTkuMjgzNyA3Ny4yOTUtMjAuNzM3YzAtNS43MDYtOC41ODg4LTEwLjg3NC0yMi40ODktMTQuNjI0IDAuMDM3NS0wLjAyODkgMC4wNzc1LTAuMDYxNSAwLjExNS0wLjA5MTQgMTQuMDI1IDMuNzY4NiAyMi42OTUgOC45Njk3IDIyLjY5NSAxNC43MTUgMCAxMS41MDEtMzQuNzUgMjAuODI0LTc3LjYxNiAyMC44MjQiIGZpbGw9IiNmY2ZkZmQiLz4KICAgPHBhdGggaWQ9InBhdGg1NjQ5IiBkPSJtMzE5Ljg5IDkyMC44MmMtNDIuNjg5IDAtNzcuMjk1LTkuMjgzNy03Ny4yOTUtMjAuNzM3IDAtNS43MDYgOC41ODg4LTEwLjg3NCAyMi40ODktMTQuNjI0IDAuMDM4NyAwLjAzMTIgMC4wNzYzIDAuMDU5NSAwLjExNSAwLjA5MDctMTMuNzc2IDMuNzMxNS0yMi4yODQgOC44NjUyLTIyLjI4NCAxNC41MzMgMCAxMS40MDYgMzQuNDYyIDIwLjY1MiA3Ni45NzUgMjAuNjUyIDQyLjUxMSAwIDc2Ljk3NC05LjI0NjEgNzYuOTc0LTIwLjY1MiAwLTUuNjY3NS04LjUwNjItMTAuODAxLTIyLjI4Mi0xNC41MzMgMC4wMzg3LTAuMDMxMiAwLjA3NjItMC4wNTk1IDAuMTE1LTAuMDkwNyAxMy45IDMuNzUgMjIuNDg5IDguOTE3NSAyMi40ODkgMTQuNjI0IDAgMTEuNDU0LTM0LjYwNiAyMC43MzctNzcuMjk1IDIwLjczNyIgZmlsbD0iI2ZjZmRmZCIvPgogICA8cGF0aCBpZD0icGF0aDU2NTEiIGQ9Im0zMTkuODkgOTIwLjczYy00Mi41MTIgMC03Ni45NzUtOS4yNDYxLTc2Ljk3NS0yMC42NTIgMC01LjY2NzUgOC41MDc1LTEwLjgwMSAyMi4yODQtMTQuNTMzIDAuMDQgMC4wMzEyIDAuMDc3NSAwLjA2MDEgMC4xMTYyNSAwLjA5MTQtMTMuNjUxIDMuNzEzOS0yMi4wNzkgOC44MTM5LTIyLjA3OSAxNC40NDEgMCAxMS4zNTkgMzQuMzE5IDIwLjU2NiA3Ni42NTQgMjAuNTY2czc2LjY1NC05LjIwNzUgNzYuNjU0LTIwLjU2NmMwLTUuNjI3NS04LjQyNzUtMTAuNzI4LTIyLjA3OS0xNC40NDEgMC4wNC0wLjAzMTIgMC4wNzYzLTAuMDYwMSAwLjExNjI1LTAuMDkxNCAxMy43NzYgMy43MzE1IDIyLjI4MiA4Ljg2NTIgMjIuMjgyIDE0LjUzMyAwIDExLjQwNi0zNC40NjIgMjAuNjUyLTc2Ljk3NCAyMC42NTIiIGZpbGw9IiNmY2ZjZmQiLz4KICAgPHBhdGggaWQ9InBhdGg1NjUzIiBkPSJtMzE5Ljg5IDkyMC42NWMtNDIuMzM1IDAtNzYuNjU0LTkuMjA3NS03Ni42NTQtMjAuNTY2IDAtNS42Mjc1IDguNDI3NS0xMC43MjggMjIuMDc5LTE0LjQ0MSAwLjAzNzUgMC4wMjg3IDAuMDc4OCAwLjA2MTUgMC4xMTYyNSAwLjA5MTItMTMuNTI5IDMuNjk1NC0yMS44NzQgOC43NjEzLTIxLjg3NCAxNC4zNSAwIDExLjMxMSAzNC4xNzUgMjAuNDggNzYuMzMyIDIwLjQ4IDQyLjE1OCAwIDc2LjMzMi05LjE2ODkgNzYuMzMyLTIwLjQ4IDAtNS41ODg5LTguMzQzOC0xMC42NTUtMjEuODc0LTE0LjM1IDAuMDQtMC4wMzEyIDAuMDc3NS0wLjA2IDAuMTE2MjUtMC4wOTEyIDEzLjY1MSAzLjcxMzkgMjIuMDc5IDguODEzOSAyMi4wNzkgMTQuNDQxIDAgMTEuMzU5LTM0LjMxOSAyMC41NjYtNzYuNjU0IDIwLjU2NiIgZmlsbD0iI2ZjZmNmYyIvPgogICA8cGF0aCBpZD0icGF0aDU2NTUiIGQ9Im0zMTkuODkgOTIwLjU2Yy00Mi4xNTggMC03Ni4zMzItOS4xNjg5LTc2LjMzMi0yMC40OCAwLTUuNTg4OSA4LjM0NS0xMC42NTUgMjEuODc0LTE0LjM1IDAuMDM4OCAwLjAzMDMgMC4wNzc1IDAuMDYwMSAwLjExNjI1IDAuMDkwNC0xMy40MDUgMy42NzcyLTIxLjY3IDguNzEtMjEuNjcgMTQuMjYgMCAxMS4yNjQgMzQuMDMyIDIwLjM5NCA3Ni4wMTIgMjAuMzk0czc2LjAxMi05LjEyOTkgNzYuMDEyLTIwLjM5NGMwLTUuNTQ5OC04LjI2NS0xMC41ODItMjEuNjctMTQuMjU5IDAuMDM3NS0wLjAyOTkgMC4wOC0wLjA2MjUgMC4xMTYyNS0wLjA5MTQgMTMuNTMgMy42OTU0IDIxLjg3NCA4Ljc2MTIgMjEuODc0IDE0LjM1IDAgMTEuMzExLTM0LjE3NSAyMC40OC03Ni4zMzIgMjAuNDgiIGZpbGw9IiNmYmZjZmMiLz4KICAgPHBhdGggaWQ9InBhdGg1NjU3IiBkPSJtMzE5Ljg5IDkyMC40N2MtNDEuOTggMC03Ni4wMTItOS4xMjk5LTc2LjAxMi0yMC4zOTQgMC01LjU0OTggOC4yNjUtMTAuNTgyIDIxLjY3LTE0LjI2IDAuMDM4NyAwLjAzMDggMC4wNzc1IDAuMDYxIDAuMTE3NSAwLjA5MDgtMTMuMjg0IDMuNjU5Mi0yMS40NjYgOC42NTc4LTIxLjQ2NiAxNC4xNjkgMCAxMS4yMTYgMzMuODg4IDIwLjMwOCA3NS42OTEgMjAuMzA4IDQxLjgwNCAwIDc1LjY5MS05LjA5MTQgNzUuNjkxLTIwLjMwOCAwLTUuNTExMi04LjE4MTItMTAuNTA5LTIxLjQ2Ni0xNC4xNjkgMC4wNC0wLjAyOTggMC4wNzg3LTAuMDYgMC4xMTc1LTAuMDg5OCAxMy40MDUgMy42NzYyIDIxLjY3IDguNzA5IDIxLjY3IDE0LjI1OSAwIDExLjI2NC0zNC4wMzIgMjAuMzk0LTc2LjAxMiAyMC4zOTQiIGZpbGw9IiNmYmZjZmMiLz4KICAgPHBhdGggaWQ9InBhdGg1NjU5IiBkPSJtMzE5Ljg5IDkyMC4zOWMtNDEuODA0IDAtNzUuNjkxLTkuMDkxNC03NS42OTEtMjAuMzA4IDAtNS41MTEyIDguMTgyNS0xMC41MSAyMS40NjYtMTQuMTY5IDAuMDM4NyAwLjAzMTIgMC4wNzc1IDAuMDYxNiAwLjExNzUgMC4wOTE0LTEzLjE2MSAzLjY0MTEtMjEuMjYyIDguNjA1LTIxLjI2MiAxNC4wNzggMCAxMS4xNjkgMzMuNzQ0IDIwLjIyMSA3NS4zNyAyMC4yMjFzNzUuMzctOS4wNTIyIDc1LjM3LTIwLjIyMWMwLTUuNDcyNi04LjEwMTItMTAuNDM2LTIxLjI2MS0xNC4wNzggMC4wMzg3LTAuMDI5OCAwLjA3NzUtMC4wNjAxIDAuMTE2MjUtMC4wOTE0IDEzLjI4NSAzLjY2MDIgMjEuNDY2IDguNjU3OCAyMS40NjYgMTQuMTY5IDAgMTEuMjE2LTMzLjg4OCAyMC4zMDgtNzUuNjkxIDIwLjMwOCIgZmlsbD0iI2ZiZmJmYiIvPgogICA8cGF0aCBpZD0icGF0aDU2NjEiIGQ9Im0zMTkuODkgOTIwLjNjLTQxLjYyNiAwLTc1LjM3LTkuMDUyMi03NS4zNy0yMC4yMjEgMC01LjQ3MjYgOC4xMDEyLTEwLjQzNiAyMS4yNjItMTQuMDc4IDAuMDM4NyAwLjAzMTIgMC4wNzc1IDAuMDYxNSAwLjExNzUgMC4wOTEzLTEzLjAzOSAzLjYyMjYtMjEuMDYgOC41NTI4LTIxLjA2IDEzLjk4NiAwIDExLjEyIDMzLjYwMSAyMC4xMzYgNzUuMDUgMjAuMTM2czc1LjA1LTkuMDE2MSA3NS4wNS0yMC4xMzZjMC01LjQzMzYtOC4wMjEyLTEwLjM2NC0yMS4wNi0xMy45ODYgMC4wNC0wLjAyOTcgMC4wNzg4LTAuMDYgMC4xMTg3NS0wLjA5MTMgMTMuMTYgMy42NDExIDIxLjI2MSA4LjYwNSAyMS4yNjEgMTQuMDc4IDAgMTEuMTY5LTMzLjc0NCAyMC4yMjEtNzUuMzcgMjAuMjIxIiBmaWxsPSIjZmJmYmZiIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTY2MyIgZD0ibTMxOS44OSA5MjAuMjJjLTQxLjQ0OSAwLTc1LjA1LTkuMDE2MS03NS4wNS0yMC4xMzYgMC01LjQzMzYgOC4wMjEyLTEwLjM2NCAyMS4wNi0xMy45ODYgMC4wMzg4IDAuMDMwNCAwLjA3ODggMC4wNjE2IDAuMTE3NSAwLjA5MTQtMTIuOTE2IDMuNjA0LTIwLjg1NiA4LjUtMjAuODU2IDEzLjg5NSAwIDExLjA3MiAzMy40NTYgMjAuMDUgNzQuNzI5IDIwLjA1IDQxLjI3MSAwIDc0LjcyOS04Ljk3NzYgNzQuNzI5LTIwLjA1IDAtNS4zOTUtNy45NC0xMC4yOTEtMjAuODU2LTEzLjg5NSAwLjAzODctMC4wMjk4IDAuMDc4Ny0wLjA2MDEgMC4xMTc1LTAuMDkxNCAxMy4wMzkgMy42MjI2IDIxLjA2IDguNTUyOCAyMS4wNiAxMy45ODYgMCAxMS4xMi0zMy42MDEgMjAuMTM2LTc1LjA1IDIwLjEzNiIgZmlsbD0iI2ZhZmJmYiIvPgogICA8cGF0aCBpZD0icGF0aDU2NjUiIGQ9Im0zMTkuODkgOTIwLjEzYy00MS4yNzIgMC03NC43MjktOC45Nzc2LTc0LjcyOS0yMC4wNSAwLTUuMzk1IDcuOTQtMTAuMjkxIDIwLjg1Ni0xMy44OTUgMC4wNCAwLjAyOTcgMC4wOCAwLjA2MTUgMC4xMTg3NSAwLjA5MTItMTIuNzk1IDMuNTg1LTIwLjY1NSA4LjQ0NzItMjAuNjU1IDEzLjgwNCAwIDExLjAyNSAzMy4zMTQgMTkuOTY0IDc0LjQwOSAxOS45NjRzNzQuNDA5LTguOTM5IDc0LjQwOS0xOS45NjRjMC01LjM1NS03Ljg2MjUtMTAuMjE5LTIwLjY1NS0xMy44MDQgMC4wMzg3LTAuMDI5OCAwLjA3ODctMC4wNjE1IDAuMTE4NzUtMC4wOTEyIDEyLjkxNiAzLjYwNCAyMC44NTYgOC41IDIwLjg1NiAxMy44OTUgMCAxMS4wNzItMzMuNDU4IDIwLjA1LTc0LjcyOSAyMC4wNSIgZmlsbD0iI2ZhZmJmYiIvPgogICA8cGF0aCBpZD0icGF0aDU2NjciIGQ9Im0zMTkuODkgOTIwLjA0Yy00MS4wOTUgMC03NC40MDktOC45MzktNzQuNDA5LTE5Ljk2NCAwLTUuMzU2NSA3Ljg2LTEwLjIxOSAyMC42NTUtMTMuODA0IDAuMDQgMC4wMjk5IDAuMDggMC4wNjExIDAuMTIgMC4wOTE0LTEyLjY3NCAzLjU2NTgtMjAuNDU0IDguMzk2LTIwLjQ1NCAxMy43MTIgMCAxMC45NzggMzMuMTcgMTkuODc3IDc0LjA4OCAxOS44NzdzNzQuMDg4LTguODk5OSA3NC4wODgtMTkuODc3YzAtNS4zMTY0LTcuNzgtMTAuMTQ2LTIwLjQ1Mi0xMy43MTIgMC4wMzg3LTAuMDMwMiAwLjA3ODgtMC4wNjE1IDAuMTE4NzUtMC4wOTE0IDEyLjc5MiAzLjU4NSAyMC42NTUgOC40NDg4IDIwLjY1NSAxMy44MDQgMCAxMS4wMjUtMzMuMzE0IDE5Ljk2NC03NC40MDkgMTkuOTY0IiBmaWxsPSIjZmFmYWZhIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTY2OSIgZD0ibTMxOS44OSA5MTkuOTZjLTQwLjkxOCAwLTc0LjA4OC04Ljg5OTktNzQuMDg4LTE5Ljg3NyAwLTUuMzE2NCA3Ljc4LTEwLjE0NiAyMC40NTQtMTMuNzEyIDAuMDM4NyAwLjAyOTcgMC4wOCAwLjA2MSAwLjExODc1IDAuMDkxMy0xMi41NSAzLjU0NzQtMjAuMjUxIDguMzQzOC0yMC4yNTEgMTMuNjIxIDAgMTAuOTMgMzMuMDI2IDE5Ljc5MSA3My43NjYgMTkuNzkxczczLjc2Ni04Ljg2MDkgNzMuNzY2LTE5Ljc5MWMwLTUuMjc3NC03LjcwMTItMTAuMDc0LTIwLjI1MS0xMy42MjEgMC4wNC0wLjAzMDMgMC4wOC0wLjA2MTUgMC4xMi0wLjA5MTMgMTIuNjcyIDMuNTY1OSAyMC40NTIgOC4zOTYgMjAuNDUyIDEzLjcxMiAwIDEwLjk3OC0zMy4xNyAxOS44NzctNzQuMDg4IDE5Ljg3NyIgZmlsbD0iI2ZhZmFmYSIvPgogICA8cGF0aCBpZD0icGF0aDU2NzEiIGQ9Im0zMTkuODkgOTE5Ljg3Yy00MC43NCAwLTczLjc2Ni04Ljg2MDktNzMuNzY2LTE5Ljc5MSAwLTUuMjc3NCA3LjcwMTItMTAuMDc0IDIwLjI1MS0xMy42MjEgMC4wNCAwLjAyOTkgMC4wOCAwLjA1OTYgMC4xMiAwLjA5MTQtMTIuNDMxIDMuNTI4Mi0yMC4wNTEgOC4yOTEtMjAuMDUxIDEzLjUzIDAgMTAuODgyIDMyLjg4MiAxOS43MDUgNzMuNDQ2IDE5LjcwNSA0MC41NjIgMCA3My40NDYtOC44MjI3IDczLjQ0Ni0xOS43MDUgMC01LjIzODgtNy42Mi0xMC4wMDItMjAuMDUxLTEzLjUzIDAuMDQtMC4wMzAzIDAuMDgtMC4wNjE1IDAuMTItMC4wOTE0IDEyLjU1IDMuNTQ3NCAyMC4yNTEgOC4zNDM4IDIwLjI1MSAxMy42MjEgMCAxMC45My0zMy4wMjYgMTkuNzkxLTczLjc2NiAxOS43OTEiIGZpbGw9IiNmOWZhZmEiLz4KICAgPHBhdGggaWQ9InBhdGg1NjczIiBkPSJtMzE5Ljg5IDkxOS43OWMtNDAuNTY0IDAtNzMuNDQ2LTguODIyNy03My40NDYtMTkuNzA1IDAtNS4yMzg4IDcuNjItMTAuMDAyIDIwLjA1MS0xMy41MyAwLjA0IDAuMDI5OCAwLjA4MTMgMC4wNjEgMC4xMjEyNSAwLjA5MDctMTIuMzExIDMuNTEwMi0xOS44NTEgOC4yMzg4LTE5Ljg1MSAxMy40MzkgMCAxMC44MzUgMzIuNzM5IDE5LjYxOSA3My4xMjUgMTkuNjE5czczLjEyNS04Ljc4MzYgNzMuMTI1LTE5LjYxOWMwLTUuMjAwMi03LjU0LTkuOTI3Mi0xOS44NS0xMy40MzkgMC4wMzg3LTAuMDI5NyAwLjA4LTAuMDYxIDAuMTItMC4wOTA3IDEyLjQzMSAzLjUyODIgMjAuMDUxIDguMjkxIDIwLjA1MSAxMy41MyAwIDEwLjg4Mi0zMi44ODQgMTkuNzA1LTczLjQ0NiAxOS43MDUiIGZpbGw9IiNmOWY5ZjkiLz4KICAgPHBhdGggaWQ9InBhdGg1Njc1IiBkPSJtMzE5Ljg5IDkxOS43Yy00MC4zODYgMC03My4xMjUtOC43ODM2LTczLjEyNS0xOS42MTkgMC01LjIwMDIgNy41NC05LjkyODggMTkuODUxLTEzLjQzOSAwLjAzODcgMC4wMzAzIDAuMDggMC4wNjAxIDAuMTIgMC4wOTE0LTEyLjE4OCAzLjQ5MTEtMTkuNjUxIDguMTg2NS0xOS42NTEgMTMuMzQ4IDAgMTAuNzg4IDMyLjU5NiAxOS41MzQgNzIuODA1IDE5LjUzNHM3Mi44MDQtOC43NDYxIDcyLjgwNC0xOS41MzRjMC01LjE2MTEtNy40NjI1LTkuODU2NS0xOS42NS0xMy4zNDggMC4wNC0wLjAyOTkgMC4wODEyLTAuMDYxMSAwLjEyMTI1LTAuMDkxNCAxMi4zMSAzLjUxMTggMTkuODUgOC4yMzg4IDE5Ljg1IDEzLjQzOSAwIDEwLjgzNS0zMi43MzkgMTkuNjE5LTczLjEyNSAxOS42MTkiIGZpbGw9IiNmOWY5ZjkiLz4KICAgPHBhdGggaWQ9InBhdGg1Njc3IiBkPSJtMzE5Ljg5IDkxOS42MWMtNDAuMjA5IDAtNzIuODA1LTguNzQ2MS03Mi44MDUtMTkuNTM0IDAtNS4xNjExIDcuNDYzOC05Ljg1NjUgMTkuNjUxLTEzLjM0OCAwLjA0MjUgMC4wMzEyIDAuMDggMC4wNTkgMC4xMjEyNSAwLjA5MTItMTIuMDY4IDMuNDcyNi0xOS40NTEgOC4xMzM5LTE5LjQ1MSAxMy4yNTYgMCAxMC43NCAzMi40NTIgMTkuNDQ4IDcyLjQ4NCAxOS40NDggNDAuMDMxIDAgNzIuNDg0LTguNzA3NSA3Mi40ODQtMTkuNDQ4IDAtNS4xMjI1LTcuMzgzOC05Ljc4MzgtMTkuNDUxLTEzLjI1NiAwLjA0MjUtMC4wMzIyIDAuMDc4Ny0wLjA2IDAuMTIxMjUtMC4wOTEyIDEyLjE4OCAzLjQ5MTEgMTkuNjUgOC4xODY1IDE5LjY1IDEzLjM0OCAwIDEwLjc4OC0zMi41OTUgMTkuNTM0LTcyLjgwNCAxOS41MzQiIGZpbGw9IiNmOGY5ZjkiLz4KICAgPHBhdGggaWQ9InBhdGg1Njc5IiBkPSJtMzE5Ljg5IDkxOS41M2MtNDAuMDMxIDAtNzIuNDg0LTguNzA3NS03Mi40ODQtMTkuNDQ4IDAtNS4xMjI1IDcuMzgzOC05Ljc4MzggMTkuNDUxLTEzLjI1NiAwLjA0IDAuMDI4OSAwLjA4MjUgMC4wNjE1IDAuMTIyNSAwLjA5MTQtMTEuOTQ5IDMuNDUzNi0xOS4yNTIgOC4wOC0xOS4yNTIgMTMuMTY1IDAgMTAuNjkyIDMyLjMwOCAxOS4zNjEgNzIuMTYyIDE5LjM2MSAzOS44NTUgMCA3Mi4xNjItOC42NjkgNzIuMTYyLTE5LjM2MSAwLTUuMDg1LTcuMzAzOC05LjcxMTQtMTkuMjUyLTEzLjE2NSAwLjA0LTAuMDI5OSAwLjA4MjUtMC4wNjExIDAuMTIyNS0wLjA5MTQgMTIuMDY4IDMuNDcyNiAxOS40NTEgOC4xMzM5IDE5LjQ1MSAxMy4yNTYgMCAxMC43NC0zMi40NTIgMTkuNDQ4LTcyLjQ4NCAxOS40NDgiIGZpbGw9IiNmOGY4ZjgiLz4KICAgPHBhdGggaWQ9InBhdGg1NjgxIiBkPSJtMzE5Ljg5IDkxOS40NGMtMzkuODU1IDAtNzIuMTYyLTguNjY5LTcyLjE2Mi0xOS4zNjEgMC01LjA4NSA3LjMwMzgtOS43MTE0IDE5LjI1Mi0xMy4xNjUgMC4wNCAwLjAyODggMC4wODEyIDAuMDYxNSAwLjEyMTI1IDAuMDkxMy0xMS44MjggMy40MzUxLTE5LjA1NCA4LjAyNzQtMTkuMDU0IDEzLjA3NCAwIDEwLjY0NSAzMi4xNjUgMTkuMjc1IDcxLjg0MiAxOS4yNzUgMzkuNjc4IDAgNzEuODQyLTguNjI5OSA3MS44NDItMTkuMjc1IDAtNS4wNDY0LTcuMjI2Mi05LjYzODYtMTkuMDU0LTEzLjA3NCAwLjA0LTAuMDI5OCAwLjA4MTMtMC4wNjI1IDAuMTIxMjUtMC4wOTEzIDExLjk0OSAzLjQ1MzYgMTkuMjUyIDguMDggMTkuMjUyIDEzLjE2NSAwIDEwLjY5Mi0zMi4zMDggMTkuMzYxLTcyLjE2MiAxOS4zNjEiIGZpbGw9IiNmOGY4ZjgiLz4KICAgPHBhdGggaWQ9InBhdGg1NjgzIiBkPSJtMzE5Ljg5IDkxOS4zNWMtMzkuNjc4IDAtNzEuODQyLTguNjI5OS03MS44NDItMTkuMjc1IDAtNS4wNDY0IDcuMjI2Mi05LjYzODYgMTkuMDU0LTEzLjA3NCAwLjA0MjUgMC4wMzEyIDAuMDgxMyAwLjA1ODYgMC4xMjM3NSAwLjA5MTQtMTEuNzA5IDMuNDE2LTE4Ljg1NiA3Ljk3NS0xOC44NTYgMTIuOTgyIDAgMTAuNTk4IDMyLjAyMSAxOS4xODkgNzEuNTIxIDE5LjE4OXM3MS41MjEtOC41OTEzIDcxLjUyMS0xOS4xODljMC01LjAwNzQtNy4xNDc1LTkuNTY2NC0xOC44NTYtMTIuOTgyIDAuMDQyNS0wLjAzMTIgMC4wODEzLTAuMDYwMSAwLjEyMzc1LTAuMDkxNCAxMS44MjggMy40MzUxIDE5LjA1NCA4LjAyNzQgMTkuMDU0IDEzLjA3NCAwIDEwLjY0NS0zMi4xNjUgMTkuMjc1LTcxLjg0MiAxOS4yNzUiIGZpbGw9IiNmN2Y4ZjgiLz4KICAgPHBhdGggaWQ9InBhdGg1Njg1IiBkPSJtMzE5Ljg5IDkxOS4yN2MtMzkuNSAwLTcxLjUyMS04LjU5MTMtNzEuNTIxLTE5LjE4OSAwLTUuMDA3NCA3LjE0NzUtOS41NjY0IDE4Ljg1Ni0xMi45ODIgMC4wNCAwLjAyODggMC4wODI1IDAuMDYxIDAuMTIyNSAwLjA5MTMtMTEuNTg4IDMuMzk3NS0xOC42NTkgNy45MjI0LTE4LjY1OSAxMi44OTEgMCAxMC41NSAzMS44NzkgMTkuMTAzIDcxLjIwMSAxOS4xMDNzNzEuMi04LjU1MjggNzEuMi0xOS4xMDNjMC00Ljk2ODgtNy4wNjg4LTkuNDkzNi0xOC42NTgtMTIuODkxIDAuMDQtMC4wMzAzIDAuMDgyNS0wLjA2MjUgMC4xMjI1LTAuMDkxMyAxMS43MDkgMy40MTYgMTguODU2IDcuOTc1IDE4Ljg1NiAxMi45ODIgMCAxMC41OTgtMzIuMDIxIDE5LjE4OS03MS41MjEgMTkuMTg5IiBmaWxsPSIjZjdmN2Y3Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTY4NyIgZD0ibTMxOS44OSA5MTkuMThjLTM5LjMyMiAwLTcxLjIwMS04LjU1MjgtNzEuMjAxLTE5LjEwMyAwLTQuOTY4OCA3LjA3MTItOS40OTM2IDE4LjY1OS0xMi44OTEgMC4wNDI1IDAuMDMxMiAwLjA4MTIgMC4wNjAxIDAuMTIzNzUgMC4wOTE0LTExLjQ3IDMuMzc4NC0xOC40NjEgNy44Njk2LTE4LjQ2MSAxMi44IDAgMTAuNTAyIDMxLjczNCAxOS4wMTYgNzAuODggMTkuMDE2czcwLjg4LTguNTEzNiA3MC44OC0xOS4wMTZjMC00LjkzMDEtNi45OTEyLTkuNDIxNC0xOC40NjEtMTIuOCAwLjA0MjUtMC4wMzEyIDAuMDgxMy0wLjA2MDEgMC4xMjM3NS0wLjA5MTQgMTEuNTg5IDMuMzk3NSAxOC42NTggNy45MjI0IDE4LjY1OCAxMi44OTEgMCAxMC41NS0zMS44NzggMTkuMTAzLTcxLjIgMTkuMTAzIiBmaWxsPSIjZjZmN2Y3Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTY4OSIgZD0ibTMxOS44OSA5MTkuMWMtMzkuMTQ2IDAtNzAuODgtOC41MTM2LTcwLjg4LTE5LjAxNiAwLTQuOTMwMSA2Ljk5MTItOS40MjE0IDE4LjQ2MS0xMi44IDAuMDQgMC4wMjgyIDAuMDgzOCAwLjA2MSAwLjEyMzc1IDAuMDkwNy0xMS4zNDkgMy4zNTg5LTE4LjI2NCA3LjgxNzktMTguMjY0IDEyLjcwOSAwIDEwLjQ1NSAzMS41OSAxOC45MzEgNzAuNTU5IDE4LjkzMXM3MC41NTktOC40NzYgNzAuNTU5LTE4LjkzMWMwLTQuODkxMS02LjkxNS05LjM1MDEtMTguMjY0LTEyLjcwOSAwLjA0LTAuMDI5NyAwLjA4MzgtMC4wNjI1IDAuMTIzNzUtMC4wOTA3IDExLjQ3IDMuMzc4NCAxOC40NjEgNy44Njk2IDE4LjQ2MSAxMi44IDAgMTAuNTAyLTMxLjczNCAxOS4wMTYtNzAuODggMTkuMDE2IiBmaWxsPSIjZjZmNmY3Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTY5MSIgZD0ibTMxOS44OSA5MTkuMDFjLTM4Ljk2OSAwLTcwLjU1OS04LjQ3Ni03MC41NTktMTguOTMxIDAtNC44OTExIDYuOTE1LTkuMzUwMSAxOC4yNjQtMTIuNzA5IDAuMDQyNSAwLjAzMTIgMC4wODI1IDAuMDYwMSAwLjEyNSAwLjA5MTQtMTEuMjMxIDMuMzQwMi0xOC4wNjkgNy43NjUxLTE4LjA2OSAxMi42MTggMCAxMC40MDggMzEuNDQ4IDE4Ljg0NSA3MC4yMzkgMTguODQ1czcwLjIzOS04LjQzNzUgNzAuMjM5LTE4Ljg0NWMwLTQuODUyNS02LjgzNjItOS4yNzc0LTE4LjA2OS0xMi42MTggMC4wNDI1LTAuMDMxMiAwLjA4MjUtMC4wNjAxIDAuMTI1LTAuMDkxNCAxMS4zNDkgMy4zNTg5IDE4LjI2NCA3LjgxNzkgMTguMjY0IDEyLjcwOSAwIDEwLjQ1NS0zMS41OSAxOC45MzEtNzAuNTU5IDE4LjkzMSIgZmlsbD0iI2Y2ZjZmNiIvPgogICA8cGF0aCBpZD0icGF0aDU2OTMiIGQ9Im0zMTkuODkgOTE4LjkzYy0zOC43OTEgMC03MC4yMzktOC40Mzc1LTcwLjIzOS0xOC44NDUgMC00Ljg1MjUgNi44Mzc1LTkuMjc3NCAxOC4wNjktMTIuNjE4IDAuMDQgMC4wMjg3IDAuMDg1IDAuMDYxNSAwLjEyNSAwLjA5MTItMTEuMTEyIDMuMzIxMy0xNy44NzIgNy43MTE0LTE3Ljg3MiAxMi41MjYgMCAxMC4zNiAzMS4zMDIgMTguNzU5IDY5LjkxOCAxOC43NTkgMzguNjE0IDAgNjkuOTE4LTguMzk4OSA2OS45MTgtMTguNzU5IDAtNC44MTUtNi43Ni05LjIwNTEtMTcuODcyLTEyLjUyNiAwLjA0LTAuMDI5NyAwLjA4NS0wLjA2MjUgMC4xMjUtMC4wOTEyIDExLjIzMiAzLjM0MDIgMTguMDY5IDcuNzY1MSAxOC4wNjkgMTIuNjE4IDAgMTAuNDA4LTMxLjQ0OCAxOC44NDUtNzAuMjM5IDE4Ljg0NSIgZmlsbD0iI2Y1ZjZmNiIvPgogICA8cGF0aCBpZD0icGF0aDU2OTUiIGQ9Im0zMTkuODkgOTE4Ljg0Yy0zOC42MTUgMC02OS45MTgtOC4zOTg5LTY5LjkxOC0xOC43NTkgMC00LjgxNSA2Ljc2LTkuMjA1MSAxNy44NzItMTIuNTI2IDAuMDQyNSAwLjAzMTIgMC4wODI1IDAuMDYwMSAwLjEyNjI1IDAuMDkxNC0xMC45OTUgMy4zMDI2LTE3LjY3OSA3LjY1ODYtMTcuNjc5IDEyLjQzNSAwIDEwLjMxMiAzMS4xNiAxOC42NzIgNjkuNTk4IDE4LjY3MnM2OS41OTYtOC4zNTk5IDY5LjU5Ni0xOC42NzJjMC00Ljc3NjQtNi42ODI1LTkuMTMyNC0xNy42NzYtMTIuNDM1IDAuMDQyNS0wLjAzMTIgMC4wODI1LTAuMDYwMSAwLjEyNS0wLjA5MTQgMTEuMTEyIDMuMzIxMyAxNy44NzIgNy43MTE0IDE3Ljg3MiAxMi41MjYgMCAxMC4zNi0zMS4zMDQgMTguNzU5LTY5LjkxOCAxOC43NTkiIGZpbGw9IiNmNWY1ZjUiLz4KICAgPHBhdGggaWQ9InBhdGg1Njk3IiBkPSJtMzE5Ljg5IDkxOC43NWMtMzguNDM4IDAtNjkuNTk4LTguMzU5OS02OS41OTgtMTguNjcyIDAtNC43NzY0IDYuNjgzOC05LjEzMjQgMTcuNjc5LTEyLjQzNSAwLjA0MjUgMC4wMzEyIDAuMDgyNSAwLjA2IDAuMTI1IDAuMDkxMy0xMC44NzYgMy4yODI4LTE3LjQ4MiA3LjYwNjUtMTcuNDgyIDEyLjM0NCAwIDEwLjI2NSAzMS4wMTYgMTguNTg2IDY5LjI3NiAxOC41ODZzNjkuMjc2LTguMzIxMyA2OS4yNzYtMTguNTg2YzAtNC43MzcyLTYuNjA2Mi05LjA2MS0xNy40ODItMTIuMzQ0IDAuMDQyNS0wLjAzMTIgMC4wODI1LTAuMDYgMC4xMjYyNS0wLjA5MTMgMTAuOTk0IDMuMzAyNiAxNy42NzYgNy42NTg2IDE3LjY3NiAxMi40MzUgMCAxMC4zMTItMzEuMTU5IDE4LjY3Mi02OS41OTYgMTguNjcyIiBmaWxsPSIjZjVmNWY1Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTY5OSIgZD0ibTMxOS44OSA5MTguNjdjLTM4LjI2IDAtNjkuMjc2LTguMzIxMy02OS4yNzYtMTguNTg2IDAtNC43MzcyIDYuNjA2Mi05LjA2MSAxNy40ODItMTIuMzQ0IDAuMDQxMiAwLjAyOTcgMC4wODYzIDAuMDYyNSAwLjEyNzUgMC4wOTEyLTEwLjc1OSAzLjI2MzgtMTcuMjg5IDcuNTUzOC0xNy4yODkgMTIuMjUyIDAgMTAuMjE3IDMwLjg3MiAxOC41IDY4Ljk1NSAxOC41IDM4LjA4MiAwIDY4Ljk1NS04LjI4MjggNjguOTU1LTE4LjUgMC00LjY5ODgtNi41My04Ljk4ODgtMTcuMjg5LTEyLjI1MiAwLjA0MzctMC4wMzAzIDAuMDg1LTAuMDYgMC4xMjc1LTAuMDkxMiAxMC44NzYgMy4yODI4IDE3LjQ4MiA3LjYwNjUgMTcuNDgyIDEyLjM0NCAwIDEwLjI2NS0zMS4wMTYgMTguNTg2LTY5LjI3NiAxOC41ODYiIGZpbGw9IiNmNGY0ZjQiLz4KICAgPHBhdGggaWQ9InBhdGg1NzAxIiBkPSJtMzE5Ljg5IDkxOC41OGMtMzguMDgyIDAtNjguOTU1LTguMjgyOC02OC45NTUtMTguNSAwLTQuNjk4OCA2LjUzLTguOTg4OCAxNy4yODktMTIuMjUyIDAuMDQyNSAwLjAzMTIgMC4wODM4IDAuMDYxMSAwLjEyNjI1IDAuMDkxNC0xMC42NDEgMy4yNDM2LTE3LjA5NSA3LjUtMTcuMDk1IDEyLjE2MSAwIDEwLjE3IDMwLjcyOSAxOC40MTUgNjguNjM1IDE4LjQxNXM2OC42MzQtOC4yNDUyIDY4LjYzNC0xOC40MTVjMC00LjY2MTEtNi40NTI1LTguOTE2NS0xNy4wOTQtMTIuMTYgMC4wNC0wLjAyOTkgMC4wODYzLTAuMDYyNSAwLjEyNjI1LTAuMDkyNCAxMC43NTkgMy4yNjM4IDE3LjI4OSA3LjU1MzggMTcuMjg5IDEyLjI1MiAwIDEwLjIxNy0zMC44NzIgMTguNS02OC45NTUgMTguNSIgZmlsbD0iI2Y0ZjRmNCIvPgogICA8cGF0aCBpZD0icGF0aDU3MDMiIGQ9Im0zMTkuODkgOTE4LjVjLTM3LjkwNiAwLTY4LjYzNS04LjI0NTItNjguNjM1LTE4LjQxNSAwLTQuNjYxMSA2LjQ1MzgtOC45MTc1IDE3LjA5NS0xMi4xNjEgMC4wNDM3IDAuMDMxMiAwLjA4NSAwLjA2MSAwLjEyNzUgMC4wOTIyLTEwLjUyNCAzLjIyMzYtMTYuOTAxIDcuNDQ2NC0xNi45MDEgMTIuMDY5IDAgMTAuMTIyIDMwLjU4NSAxOC4zMjkgNjguMzE0IDE4LjMyOXM2OC4zMTQtOC4yMDYxIDY4LjMxNC0xOC4zMjljMC00LjYyMjUtNi4zNzc1LTguODQ1Mi0xNi45MDEtMTIuMDY5IDAuMDQyNS0wLjAzMTIgMC4wODUtMC4wNjEgMC4xMjc1LTAuMDkxMiAxMC42NDEgMy4yNDM2IDE3LjA5NCA3LjQ5OSAxNy4wOTQgMTIuMTYgMCAxMC4xNy0zMC43MjggMTguNDE1LTY4LjYzNCAxOC40MTUiIGZpbGw9IiNmM2YzZjMiLz4KICAgPHBhdGggaWQ9InBhdGg1NzA1IiBkPSJtMzE5Ljg5IDkxOC40MWMtMzcuNzI5IDAtNjguMzE0LTguMjA2MS02OC4zMTQtMTguMzI5IDAtNC42MjI1IDYuMzc3NS04Ljg0NTIgMTYuOTAxLTEyLjA2OSAwLjA0MzcgMC4wMzAzIDAuMDg1IDAuMDYwMSAwLjEyODc1IDAuMDkxNC0xMC40MDggMy4yMDM2LTE2LjcxIDcuMzkzNS0xNi43MSAxMS45NzggMCAxMC4wNzUgMzAuNDQyIDE4LjI0MyA2Ny45OTQgMTguMjQzIDM3LjU1MSAwIDY3Ljk5Mi04LjE2NzQgNjcuOTkyLTE4LjI0MyAwLTQuNTg0LTYuMzAxMi04Ljc3MjUtMTYuNzA5LTExLjk3OCAwLjA0MzctMC4wMzEyIDAuMDg2My0wLjA2MTEgMC4xMjg3NS0wLjA5MTQgMTAuNTI0IDMuMjIzNiAxNi45MDEgNy40NDY0IDE2LjkwMSAxMi4wNjkgMCAxMC4xMjItMzAuNTg1IDE4LjMyOS02OC4zMTQgMTguMzI5IiBmaWxsPSIjZjNmM2YyIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTcwNyIgZD0ibTMxOS44OSA5MTguMzJjLTM3LjU1MSAwLTY3Ljk5NC04LjE2NzQtNjcuOTk0LTE4LjI0MyAwLTQuNTg0IDYuMzAyNS04Ljc3MzkgMTYuNzEtMTEuOTc4IDAuMDQyNSAwLjAyOTggMC4wODUgMC4wNjE1IDAuMTI4NzUgMC4wOTEzLTEwLjI5MSAzLjE4NTEtMTYuNTE4IDcuMzM5OS0xNi41MTggMTEuODg2IDAgMTAuMDI3IDMwLjI5OSAxOC4xNTYgNjcuNjcyIDE4LjE1NiAzNy4zNzQgMCA2Ny42NzItOC4xMjg5IDY3LjY3Mi0xOC4xNTYgMC00LjU0NDktNi4yMjg4LTguNzAxMS0xNi41MTYtMTEuODg2IDAuMDQyNS0wLjAyOTggMC4wODUtMC4wNjE1IDAuMTI3NS0wLjA5MTMgMTAuNDA4IDMuMjA1IDE2LjcwOSA3LjM5MzUgMTYuNzA5IDExLjk3OCAwIDEwLjA3NS0zMC40NDEgMTguMjQzLTY3Ljk5MiAxOC4yNDMiIGZpbGw9IiNmMmYyZjIiLz4KICAgPHBhdGggaWQ9InBhdGg1NzA5IiBkPSJtMzE5Ljg5IDkxOC4yNGMtMzcuMzc0IDAtNjcuNjcyLTguMTI4OS02Ny42NzItMTguMTU2IDAtNC41NDY0IDYuMjI2Mi04LjcwMTEgMTYuNTE4LTExLjg4NiAwLjA0MjUgMC4wMjk4IDAuMDg1IDAuMDYxIDAuMTI4NzUgMC4wOTE0LTEwLjE3NCAzLjE2NS0xNi4zMjUgNy4yODc1LTE2LjMyNSAxMS43OTUgMCA5Ljk4IDMwLjE1NCAxOC4wNyA2Ny4zNTEgMTguMDcgMzcuMTk4IDAgNjcuMzUxLTguMDg5OCA2Ny4zNTEtMTguMDcgMC00LjUwNzQtNi4xNTEyLTguNjI5OS0xNi4zMjUtMTEuNzk1IDAuMDQzNy0wLjAzMDQgMC4wODYyLTAuMDYxNiAwLjEzLTAuMDkxNCAxMC4yODggMy4xODUxIDE2LjUxNiA3LjM0MTQgMTYuNTE2IDExLjg4NiAwIDEwLjAyNy0zMC4yOTkgMTguMTU2LTY3LjY3MiAxOC4xNTYiIGZpbGw9IiNmMmYxZjEiLz4KICAgPHBhdGggaWQ9InBhdGg1NzExIiBkPSJtMzE5Ljg5IDkxOC4xNWMtMzcuMTk4IDAtNjcuMzUxLTguMDg5OC02Ny4zNTEtMTguMDcgMC00LjUwNzQgNi4xNTEyLTguNjI5OSAxNi4zMjUtMTEuNzk1IDAuMDQyNSAwLjAzMTIgMC4wODYzIDAuMDYxIDAuMTMgMC4wOTEzLTEwLjA1OCAzLjE0Ni0xNi4xMzUgNy4yMzQ5LTE2LjEzNSAxMS43MDQgMCA5LjkzMjYgMzAuMDExIDE3Ljk4NCA2Ny4wMzEgMTcuOTg0czY3LjAzLTguMDUxMyA2Ny4wMy0xNy45ODRjMC00LjQ2ODgtNi4wNzYyLTguNTU3Ni0xNi4xMzItMTEuNzAzIDAuMDQyNS0wLjAzMTIgMC4wODYzLTAuMDYxIDAuMTI4NzUtMC4wOTIzIDEwLjE3NCAzLjE2NSAxNi4zMjUgNy4yODc1IDE2LjMyNSAxMS43OTUgMCA5Ljk4LTMwLjE1NCAxOC4wNy02Ny4zNTEgMTguMDciIGZpbGw9IiNmMWYxZjEiLz4KICAgPHBhdGggaWQ9InBhdGg1NzEzIiBkPSJtMzE5Ljg5IDkxOC4wNmMtMzcuMDIgMC02Ny4wMzEtOC4wNTEzLTY3LjAzMS0xNy45ODQgMC00LjQ2ODggNi4wNzc1LTguNTU3NiAxNi4xMzUtMTEuNzA0IDAuMDQyNSAwLjAzMTIgMC4wODYzIDAuMDYxIDAuMTMgMC4wOTIyLTkuOTQzOCAzLjEyNS0xNS45NDQgNy4xODAyLTE1Ljk0NCAxMS42MTEgMCA5Ljg4NDggMjkuODY4IDE3Ljg5NyA2Ni43MSAxNy44OTdzNjYuNzEtOC4wMTI3IDY2LjcxLTE3Ljg5N2MwLTQuNDMxMS02LjAwMjUtOC40ODY0LTE1Ljk0Mi0xMS42MTEgMC4wNDI1LTAuMDI5NyAwLjA4NjMtMC4wNjEgMC4xMy0wLjA5MTMgMTAuMDU2IDMuMTQ1IDE2LjEzMiA3LjIzMzkgMTYuMTMyIDExLjcwMyAwIDkuOTMyNi0zMC4wMSAxNy45ODQtNjcuMDMgMTcuOTg0IiBmaWxsPSIjZjBmMGYwIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTcxNSIgZD0ibTMxOS44OSA5MTcuOThjLTM2Ljg0MiAwLTY2LjcxLTguMDEyNy02Ni43MS0xNy44OTcgMC00LjQzMTEgNi04LjQ4NjQgMTUuOTQ0LTExLjYxMSAwLjA0MjUgMC4wMzA0IDAuMDg2MyAwLjA2MTYgMC4xMyAwLjA5MTQtOS44MjUgMy4xMDY0LTE1Ljc1MiA3LjEyNzQtMTUuNzUyIDExLjUyIDAgOS44Mzc0IDI5LjcyMiAxNy44MTIgNjYuMzg5IDE3LjgxMiAzNi42NjUgMCA2Ni4zODktNy45NzUxIDY2LjM4OS0xNy44MTIgMC00LjM5MjYtNS45Mjc1LTguNDEzNi0xNS43NTItMTEuNTIgMC4wNDM3LTAuMDI5OCAwLjA4NzUtMC4wNjEgMC4xMzEyNS0wLjA5MTQgOS45NCAzLjEyNSAxNS45NDIgNy4xODAyIDE1Ljk0MiAxMS42MTEgMCA5Ljg4NDgtMjkuODY4IDE3Ljg5Ny02Ni43MSAxNy44OTciIGZpbGw9IiNmMGYwZWYiLz4KICAgPHBhdGggaWQ9InBhdGg1NzE3IiBkPSJtMzE5Ljg5IDkxNy44OWMtMzYuNjY2IDAtNjYuMzg5LTcuOTc1MS02Ni4zODktMTcuODEyIDAtNC4zOTI2IDUuOTI3NS04LjQxMzYgMTUuNzUyLTExLjUyIDAuMDQ1IDAuMDMxMiAwLjA4NjIgMC4wNiAwLjEzMTI1IDAuMDkxMi05LjcxIDMuMDg2NS0xNS41NjQgNy4wNzM4LTE1LjU2NCAxMS40MjkgMCA5Ljc5IDI5LjU4IDE3LjcyNiA2Ni4wNjkgMTcuNzI2czY2LjA2OS03LjkzNjEgNjYuMDY5LTE3LjcyNmMwLTQuMzU1LTUuODUzOC04LjM0MjItMTUuNTY0LTExLjQyOSAwLjA0NjItMC4wMzEyIDAuMDg2My0wLjA1ODUgMC4xMzEyNS0wLjA5MTIgOS44MjUgMy4xMDY0IDE1Ljc1MiA3LjEyNzQgMTUuNzUyIDExLjUyIDAgOS44Mzc0LTI5LjcyNCAxNy44MTItNjYuMzg5IDE3LjgxMiIgZmlsbD0iI2VmZWZlZiIvPgogICA8cGF0aCBpZD0icGF0aDU3MTkiIGQ9Im0zMTkuODkgOTE3LjgxYy0zNi40ODkgMC02Ni4wNjktNy45MzYxLTY2LjA2OS0xNy43MjYgMC00LjM1NSA1Ljg1MzgtOC4zNDIyIDE1LjU2NC0xMS40MjkgMC4wNDM3IDAuMDI5OSAwLjA4ODggMC4wNjI1IDAuMTMxMjUgMC4wOTI0LTkuNTkzOCAzLjA2NDktMTUuMzc0IDcuMDItMTUuMzc0IDExLjMzNiAwIDkuNzQyNiAyOS40MzYgMTcuNjQgNjUuNzQ4IDE3LjY0IDM2LjMxMSAwIDY1Ljc0OC03Ljg5NzUgNjUuNzQ4LTE3LjY0IDAtNC4zMTY0LTUuNzgtOC4yNzE1LTE1LjM3NC0xMS4zMzYgMC4wNDI1LTAuMDI5OSAwLjA4NzUtMC4wNjExIDAuMTMxMjUtMC4wOTI0IDkuNzEgMy4wODY1IDE1LjU2NCA3LjA3MzggMTUuNTY0IDExLjQyOSAwIDkuNzktMjkuNTggMTcuNzI2LTY2LjA2OSAxNy43MjYiIGZpbGw9IiNlZWUiLz4KICAgPHBhdGggaWQ9InBhdGg1NzIxIiBkPSJtMzE5Ljg5IDkxNy43MmMtMzYuMzExIDAtNjUuNzQ4LTcuODk3NS02NS43NDgtMTcuNjQgMC00LjMxNjQgNS43OC04LjI3MTUgMTUuMzc0LTExLjMzNiAwLjA0MzcgMC4wMzAyIDAuMDg4OCAwLjA2MTUgMC4xMzI1IDAuMDkxMi05LjQ4MTIgMy4wNDY0LTE1LjE4NiA2Ljk2NjQtMTUuMTg2IDExLjI0NSAwIDkuNjk0OSAyOS4yOTIgMTcuNTU0IDY1LjQyOCAxNy41NTQgMzYuMTM0IDAgNjUuNDI2LTcuODU4OCA2NS40MjYtMTcuNTU0IDAtNC4yNzg4LTUuNzAzOC04LjE5ODgtMTUuMTg1LTExLjI0NSAwLjA0MzgtMC4wMjk3IDAuMDg4OC0wLjA2MSAwLjEzMjUtMC4wOTEyIDkuNTkzOCAzLjA2NDkgMTUuMzc0IDcuMDIgMTUuMzc0IDExLjMzNiAwIDkuNzQyNi0yOS40MzYgMTcuNjQtNjUuNzQ4IDE3LjY0IiBmaWxsPSIjZWVlZWVkIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTcyMyIgZD0ibTMxOS44OSA5MTcuNjNjLTM2LjEzNSAwLTY1LjQyOC03Ljg1ODgtNjUuNDI4LTE3LjU1NCAwLTQuMjc4OCA1LjcwNS04LjE5ODggMTUuMTg2LTExLjI0NSAwLjA0NjIgMC4wMzEyIDAuMDg3NSAwLjA2MDEgMC4xMzI1IDAuMDkxNC05LjM2NjIgMy4wMjYyLTE0Ljk5OCA2LjkxMzUtMTQuOTk4IDExLjE1NCAwIDkuNjQ3NSAyOS4xNDkgMTcuNDY3IDY1LjEwNiAxNy40NjcgMzUuOTU4IDAgNjUuMTA2LTcuODE5OCA2NS4xMDYtMTcuNDY3IDAtNC4yNDAyLTUuNjMxMi04LjEyNzUtMTQuOTk4LTExLjE1NCAwLjA0NS0wLjAzMTIgMC4wODYzLTAuMDYwMSAwLjEzMjUtMC4wOTE0IDkuNDgxMiAzLjA0NjQgMTUuMTg1IDYuOTY2NCAxNS4xODUgMTEuMjQ1IDAgOS42OTQ5LTI5LjI5MiAxNy41NTQtNjUuNDI2IDE3LjU1NCIgZmlsbD0iI2VkZWRlYyIvPgogICA8cGF0aCBpZD0icGF0aDU3MjUiIGQ9Im0zMTkuODkgOTE3LjU1Yy0zNS45NTggMC02NS4xMDYtNy44MTk4LTY1LjEwNi0xNy40NjcgMC00LjI0MDIgNS42MzEyLTguMTI3NSAxNC45OTgtMTEuMTU0IDAuMDQzNyAwLjAzMDMgMC4wOSAwLjA2MjUgMC4xMzM3NSAwLjA5MjMtOS4yNTEyIDMuMDA1NC0xNC44MSA2Ljg1ODktMTQuODEgMTEuMDYyIDAgOS42MDAxIDI5LjAwNSAxNy4zODEgNjQuNzg1IDE3LjM4MXM2NC43ODUtNy43ODEyIDY0Ljc4NS0xNy4zODFjMC00LjIwMjYtNS41NTg4LTguMDU2MS0xNC44MS0xMS4wNjIgMC4wNDM3LTAuMDI5OCAwLjA5LTAuMDYyIDAuMTMzNzUtMC4wOTIzIDkuMzY2MiAzLjAyNjIgMTQuOTk4IDYuOTEzNSAxNC45OTggMTEuMTU0IDAgOS42NDc1LTI5LjE0OSAxNy40NjctNjUuMTA2IDE3LjQ2NyIgZmlsbD0iI2VkZWRlYyIvPgogICA8cGF0aCBpZD0icGF0aDU3MjciIGQ9Im0zMTkuODkgOTE3LjQ2Yy0zNS43OCAwLTY0Ljc4NS03Ljc4MTItNjQuNzg1LTE3LjM4MSAwLTQuMjAyNiA1LjU1ODgtOC4wNTYxIDE0LjgxLTExLjA2MiAwLjA0NjIgMC4wMzE3IDAuMDg4OCAwLjA2IDAuMTMzNzUgMC4wOTEyLTkuMTM4OCAyLjk4NTQtMTQuNjI0IDYuODA2Ni0xNC42MjQgMTAuOTcgMCA5LjU1MjggMjguODYyIDE3LjI5NSA2NC40NjUgMTcuMjk1IDM1LjYwMiAwIDY0LjQ2NS03Ljc0MjIgNjQuNDY1LTE3LjI5NSAwLTQuMTYzNi01LjQ4NS03Ljk4NDktMTQuNjI0LTEwLjk3IDAuMDQ1LTAuMDMxMiAwLjA4NzUtMC4wNTk1IDAuMTMzNzUtMC4wOTEyIDkuMjUxMiAzLjAwNTQgMTQuODEgNi44NTg5IDE0LjgxIDExLjA2MiAwIDkuNjAwMS0yOS4wMDUgMTcuMzgxLTY0Ljc4NSAxNy4zODEiIGZpbGw9IiNlY2VjZWIiLz4KICAgPHBhdGggaWQ9InBhdGg1NzI5IiBkPSJtMzE5Ljg5IDkxNy4zN2MtMzUuNjAyIDAtNjQuNDY1LTcuNzQyMi02NC40NjUtMTcuMjk1IDAtNC4xNjM2IDUuNDg1LTcuOTg0OSAxNC42MjQtMTAuOTcgMC4wNDM3IDAuMDMwMyAwLjA5MTMgMC4wNjI1IDAuMTM1IDAuMDkxNC05LjAyNSAyLjk2NTItMTQuNDM4IDYuNzUyNC0xNC40MzggMTAuODc5IDAgOS41MDQ5IDI4LjcxOCAxNy4yMSA2NC4xNDQgMTcuMjFzNjQuMTQ0LTcuNzA1MSA2NC4xNDQtMTcuMjFjMC00LjEyNjUtNS40MTI1LTcuOTEzNi0xNC40MzgtMTAuODc4IDAuMDQzOC0wLjAzMDMgMC4wOTEzLTAuMDYyNSAwLjEzNS0wLjA5MjggOS4xMzg4IDIuOTg1NCAxNC42MjQgNi44MDY2IDE0LjYyNCAxMC45NyAwIDkuNTUyOC0yOC44NjIgMTcuMjk1LTY0LjQ2NSAxNy4yOTUiIGZpbGw9IiNlY2ViZWEiLz4KICAgPHBhdGggaWQ9InBhdGg1NzMxIiBkPSJtMzE5Ljg5IDkxNy4yOWMtMzUuNDI2IDAtNjQuMTQ0LTcuNzA1MS02NC4xNDQtMTcuMjEgMC00LjEyNjUgNS40MTI1LTcuOTEzNiAxNC40MzgtMTAuODc5IDAuMDQ2MyAwLjAzMTIgMC4wODg4IDAuMDYxNSAwLjEzNSAwLjA5MjgtOC45MTEyIDIuOTQzOS0xNC4yNTIgNi42OTcyLTE0LjI1MiAxMC43ODYgMCA5LjQ1NzUgMjguNTc1IDE3LjEyNCA2My44MjQgMTcuMTI0czYzLjgyMi03LjY2NiA2My44MjItMTcuMTI0YzAtNC4wODg5LTUuMzQtNy44NDIyLTE0LjI1MS0xMC43ODYgMC4wNDYyLTAuMDMxMiAwLjA5LTAuMDYwMSAwLjEzNS0wLjA5MTQgOS4wMjUgMi45NjM5IDE0LjQzOCA2Ljc1MSAxNC40MzggMTAuODc4IDAgOS41MDQ5LTI4LjcxOCAxNy4yMS02NC4xNDQgMTcuMjEiIGZpbGw9IiNlYmViZWEiLz4KICAgPHBhdGggaWQ9InBhdGg1NzMzIiBkPSJtMzE5Ljg5IDkxNy4yYy0zNS4yNDkgMC02My44MjQtNy42NjYtNjMuODI0LTE3LjEyNCAwLTQuMDg4OSA1LjM0MTItNy44NDIyIDE0LjI1Mi0xMC43ODYgMC4wNDM3IDAuMDI5NyAwLjA5MTMgMC4wNjI1IDAuMTM1IDAuMDkxMi04Ljc5ODggMi45MjM0LTE0LjA2NiA2LjY0NTItMTQuMDY2IDEwLjY5NSAwIDkuNDEwMSAyOC40MzEgMTcuMDM4IDYzLjUwMiAxNy4wMzhzNjMuNTAyLTcuNjI3NSA2My41MDItMTcuMDM4YzAtNC4wNDk4LTUuMjY3NS03Ljc3LTE0LjA2Ni0xMC42OTUgMC4wNDM3LTAuMDI4NyAwLjA5MTMtMC4wNjE1IDAuMTM1LTAuMDkxMiA4LjkxMTIgMi45NDM5IDE0LjI1MSA2LjY5NzIgMTQuMjUxIDEwLjc4NiAwIDkuNDU3NS0yOC41NzQgMTcuMTI0LTYzLjgyMiAxNy4xMjQiIGZpbGw9IiNlYWVhZTkiLz4KICAgPHBhdGggaWQ9InBhdGg1NzM1IiBkPSJtMzE5Ljg5IDkxNy4xMmMtMzUuMDcxIDAtNjMuNTAyLTcuNjI3NS02My41MDItMTcuMDM4IDAtNC4wNDk4IDUuMjY3NS03Ljc3MTUgMTQuMDY2LTEwLjY5NSAwLjA0NjIgMC4wMzEyIDAuMDkxMyAwLjA2MTEgMC4xMzYyNSAwLjA5MjQtOC42ODUgMi45MDIyLTEzLjg4MSA2LjU4OTgtMTMuODgxIDEwLjYwMiAwIDkuMzYyMiAyOC4yODYgMTYuOTUxIDYzLjE4MSAxNi45NTEgMzQuODk0IDAgNjMuMTgxLTcuNTg4OSA2My4xODEtMTYuOTUxIDAtNC4wMTI4LTUuMTk2Mi03LjY5ODgtMTMuODgxLTEwLjYwMiAwLjA0NjMtMC4wMzEyIDAuMDktMC4wNjExIDAuMTM2MjUtMC4wOTI0IDguNzk4OCAyLjkyNDkgMTQuMDY2IDYuNjQ1MSAxNC4wNjYgMTAuNjk1IDAgOS40MTAxLTI4LjQzMSAxNy4wMzgtNjMuNTAyIDE3LjAzOCIgZmlsbD0iI2VhZWFlOCIvPgogICA8cGF0aCBpZD0icGF0aDU3MzciIGQ9Im0zMTkuODkgOTE3LjAzYy0zNC44OTUgMC02My4xODEtNy41ODg5LTYzLjE4MS0xNi45NTEgMC00LjAxMjggNS4xOTYyLTcuNzAwMiAxMy44ODEtMTAuNjAyIDAuMDQ2MyAwLjAzMTIgMC4wOTEzIDAuMDYxIDAuMTM3NSAwLjA5MTMtOC41NzI1IDIuODgyNC0xMy42OTkgNi41Mzc2LTEzLjY5OSAxMC41MTEgMCA5LjMxNSAyOC4xNDQgMTYuODY1IDYyLjg2MSAxNi44NjUgMzQuNzE4IDAgNjIuODYxLTcuNTQ5OCA2Mi44NjEtMTYuODY1IDAtMy45NzM2LTUuMTI2Mi03LjYyNzUtMTMuNjk5LTEwLjUxMSAwLjA0NjItMC4wMzAzIDAuMDkxMy0wLjA2IDAuMTM3NS0wLjA5MTMgOC42ODUgMi45MDM4IDEzLjg4MSA2LjU4OTggMTMuODgxIDEwLjYwMiAwIDkuMzYyMi0yOC4yODggMTYuOTUxLTYzLjE4MSAxNi45NTEiIGZpbGw9IiNlOWU5ZTgiLz4KICAgPHBhdGggaWQ9InBhdGg1NzM5IiBkPSJtMzE5Ljg5IDkxNi45NGMtMzQuNzE4IDAtNjIuODYxLTcuNTQ5OC02Mi44NjEtMTYuODY1IDAtMy45NzM2IDUuMTI2Mi03LjYyODkgMTMuNjk5LTEwLjUxMSAwLjA0NjIgMC4wMzEyIDAuMDkxMyAwLjA2MSAwLjEzNzUgMC4wOTIzLTguNDYxMiAyLjg2MjktMTMuNTE1IDYuNDgzLTEzLjUxNSAxMC40MTkgMCA5LjI2NzYgMjggMTYuNzc5IDYyLjU0IDE2Ljc3OXM2Mi41NC03LjUxMTIgNjIuNTQtMTYuNzc5YzAtMy45MzYtNS4wNTM4LTcuNTU2MS0xMy41MTUtMTAuNDE5IDAuMDQ2Mi0wLjAzMTIgMC4wOTEyLTAuMDYxIDAuMTM3NS0wLjA5MjMgOC41NzI1IDIuODgzOCAxMy42OTkgNi41Mzc2IDEzLjY5OSAxMC41MTEgMCA5LjMxNS0yOC4xNDQgMTYuODY1LTYyLjg2MSAxNi44NjUiIGZpbGw9IiNlOWU4ZTciLz4KICAgPHBhdGggaWQ9InBhdGg1NzQxIiBkPSJtMzE5Ljg5IDkxNi44NmMtMzQuNTQgMC02Mi41NC03LjUxMTItNjIuNTQtMTYuNzc5IDAtMy45MzYgNS4wNTM4LTcuNTU2MSAxMy41MTUtMTAuNDE5IDAuMDQ2MiAwLjAzMTIgMC4wOTEzIDAuMDYxNiAwLjEzNzUgMC4wOTE0LTguMzUgMi44NDI4LTEzLjMzMSA2LjQyODgtMTMuMzMxIDEwLjMyOCAwIDkuMjIwMiAyNy44NTYgMTYuNjk0IDYyLjIxOSAxNi42OTQgMzQuMzYyIDAgNjIuMjE5LTcuNDczNiA2Mi4yMTktMTYuNjk0IDAtMy44OTg5LTQuOTgxMi03LjQ4NDktMTMuMzMxLTEwLjMyNiAwLjA0NjMtMC4wMzEyIDAuMDkxMy0wLjA2MTYgMC4xMzc1LTAuMDkyOSA4LjQ2MTIgMi44NjI5IDEzLjUxNSA2LjQ4MyAxMy41MTUgMTAuNDE5IDAgOS4yNjc2LTI4IDE2Ljc3OS02Mi41NCAxNi43NzkiIGZpbGw9IiNlOGU4ZTYiLz4KICAgPHBhdGggaWQ9InBhdGg1NzQzIiBkPSJtMzE5Ljg5IDkxNi43N2MtMzQuMzYyIDAtNjIuMjE5LTcuNDczNi02Mi4yMTktMTYuNjk0IDAtMy44OTg5IDQuOTgxMi03LjQ4NDkgMTMuMzMxLTEwLjMyOCAwLjA0NjIgMC4wMzEyIDAuMDkyNSAwLjA2MTUgMC4xMzg3NSAwLjA5MjgtOC4yMzc1IDIuODE5OS0xMy4xNSA2LjM3MzUtMTMuMTUgMTAuMjM1IDAgOS4xNzI0IDI3LjcxMiAxNi42MDcgNjEuODk5IDE2LjYwNyAzNC4xODUgMCA2MS44OTktNy40MzUxIDYxLjg5OS0xNi42MDcgMC0zLjg2MTQtNC45MTI1LTcuNDE1LTEzLjE1LTEwLjIzNSAwLjA0NjItMC4wMzAzIDAuMDkyNS0wLjA2MTUgMC4xMzg3NS0wLjA5MTMgOC4zNSAyLjg0MTMgMTMuMzMxIDYuNDI3MyAxMy4zMzEgMTAuMzI2IDAgOS4yMjAyLTI3Ljg1NiAxNi42OTQtNjIuMjE5IDE2LjY5NCIgZmlsbD0iI2U3ZTdlNiIvPgogICA8cGF0aCBpZD0icGF0aDU3NDUiIGQ9Im0zMTkuODkgOTE2LjY5Yy0zNC4xODYgMC02MS44OTktNy40MzUxLTYxLjg5OS0xNi42MDcgMC0zLjg2MTQgNC45MTI1LTcuNDE1IDEzLjE1LTEwLjIzNSAwLjA0NjIgMC4wMzEyIDAuMDkyNSAwLjA2MSAwLjEzODc1IDAuMDkyMi04LjEyNzUgMi43OTg5LTEyLjk2OCA2LjMxODktMTIuOTY4IDEwLjE0MyAwIDkuMTIzNSAyNy41NjkgMTYuNTIxIDYxLjU3OCAxNi41MjFzNjEuNTc4LTcuMzk4IDYxLjU3OC0xNi41MjFjMC0zLjgyMjgtNC44NDEyLTcuMzQzOC0xMi45NjgtMTAuMTQzIDAuMDQ2My0wLjAzMTIgMC4wOTI1LTAuMDYxIDAuMTM4NzUtMC4wOTIyIDguMjM3NSAyLjgxOTkgMTMuMTUgNi4zNzM1IDEzLjE1IDEwLjIzNSAwIDkuMTcyNC0yNy43MTQgMTYuNjA3LTYxLjg5OSAxNi42MDciIGZpbGw9IiNlN2U2ZTUiLz4KICAgPHBhdGggaWQ9InBhdGg1NzQ3IiBkPSJtMzE5Ljg5IDkxNi42Yy0zNC4wMDkgMC02MS41NzgtNy4zOTgtNjEuNTc4LTE2LjUyMSAwLTMuODIzOCA0Ljg0LTcuMzQzOCAxMi45NjgtMTAuMTQzIDAuMDQ2MyAwLjAzMDQgMC4wOTM3IDAuMDYxNiAwLjE0IDAuMDkxNC04LjAxNSAyLjc3ODgtMTIuNzg4IDYuMjY2MS0xMi43ODggMTAuMDUxIDAgOS4wNzc2IDI3LjQyNiAxNi40MzUgNjEuMjU4IDE2LjQzNSAzMy44MzEgMCA2MS4yNTgtNy4zNTc0IDYxLjI1OC0xNi40MzUgMC0zLjc4NTEtNC43NzEyLTcuMjcyNS0xMi43ODgtMTAuMDUxIDAuMDQ2Mi0wLjAyOTggMC4wOTM3LTAuMDYxIDAuMTQtMC4wOTE0IDguMTI2MiAyLjc5ODkgMTIuOTY4IDYuMzE5OSAxMi45NjggMTAuMTQzIDAgOS4xMjM1LTI3LjU2OSAxNi41MjEtNjEuNTc4IDE2LjUyMSIgZmlsbD0iI2U2ZTZlNCIvPgogICA8cGF0aCBpZD0icGF0aDU3NDkiIGQ9Im0zMTkuODkgOTE2LjUyYy0zMy44MzEgMC02MS4yNTgtNy4zNTc0LTYxLjI1OC0xNi40MzUgMC0zLjc4NTEgNC43NzI1LTcuMjcyNSAxMi43ODgtMTAuMDUxIDAuMDQ2MiAwLjAzMTIgMC4wOTM3IDAuMDYyNSAwLjE0IDAuMDkyMi03LjkwNjIgMi43NTc5LTEyLjYwNiA2LjIxMTUtMTIuNjA2IDkuOTU5IDAgOS4wMjg4IDI3LjI4MiAxNi4zNDkgNjAuOTM2IDE2LjM0OXM2MC45MzYtNy4zMTk5IDYwLjkzNi0xNi4zNDljMC0zLjc0NzUtNC43LTcuMjAxMS0xMi42MDYtOS45NTkgMC4wNDYzLTAuMDI5NyAwLjA5MzctMC4wNjEgMC4xNC0wLjA5MjIgOC4wMTYyIDIuNzc4OCAxMi43ODggNi4yNjYxIDEyLjc4OCAxMC4wNTEgMCA5LjA3NzYtMjcuNDI2IDE2LjQzNS02MS4yNTggMTYuNDM1IiBmaWxsPSIjZTVlNWUzIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTc1MSIgZD0ibTMxOS44OSA5MTYuNDNjLTMzLjY1NCAwLTYwLjkzNi03LjMxOTktNjAuOTM2LTE2LjM0OSAwLTMuNzQ3NSA0LjctNy4yMDExIDEyLjYwNi05Ljk1OSAwLjA0NzUgMC4wMzAzIDAuMDk1IDAuMDYxNSAwLjE0MTI1IDAuMDkyOC03Ljc5NSAyLjczNjQtMTIuNDI4IDYuMTU2Mi0xMi40MjggOS44NjYyIDAgOC45ODE1IDI3LjEzOSAxNi4yNjMgNjAuNjE2IDE2LjI2MyAzMy40NzggMCA2MC42MTUtNy4yODEyIDYwLjYxNS0xNi4yNjMgMC0zLjcxLTQuNjMxMi03LjEyOTktMTIuNDI2LTkuODY2MiAwLjA0NjItMC4wMzEyIDAuMDkzNy0wLjA2MjUgMC4xNDEyNS0wLjA5MjggNy45MDYyIDIuNzU3OSAxMi42MDYgNi4yMTE1IDEyLjYwNiA5Ljk1OSAwIDkuMDI4OC0yNy4yODIgMTYuMzQ5LTYwLjkzNiAxNi4zNDkiIGZpbGw9IiNlNGU0ZTMiLz4KICAgPHBhdGggaWQ9InBhdGg1NzUzIiBkPSJtMzE5Ljg5IDkxNi4zNGMtMzMuNDc4IDAtNjAuNjE2LTcuMjgxMi02MC42MTYtMTYuMjYzIDAtMy43MSA0LjYzMjUtNy4xMjk5IDEyLjQyOC05Ljg2NjIgMC4wNDc1IDAuMDI5OSAwLjA5NSAwLjA2MTEgMC4xNDI1IDAuMDkxNC03LjY4NjIgMi43MTQ5LTEyLjI0OSA2LjEwMjUtMTIuMjQ5IDkuNzc0OSAwIDguOTMzNiAyNi45OTUgMTYuMTc2IDYwLjI5NSAxNi4xNzZzNjAuMjk1LTcuMjQyNiA2MC4yOTUtMTYuMTc2YzAtMy42NzI0LTQuNTYyNS03LjA1ODYtMTIuMjQ5LTkuNzc0OSAwLjA0NzUtMC4wMzAyIDAuMDk1LTAuMDYxNSAwLjE0MjUtMC4wOTE0IDcuNzk1IDIuNzM2NCAxMi40MjYgNi4xNTYyIDEyLjQyNiA5Ljg2NjIgMCA4Ljk4MTUtMjcuMTM4IDE2LjI2My02MC42MTUgMTYuMjYzIiBmaWxsPSIjZTRlM2UyIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTc1NSIgZD0ibTMxOS44OSA5MTYuMjZjLTMzLjMgMC02MC4yOTUtNy4yNDI2LTYwLjI5NS0xNi4xNzYgMC0zLjY3MjQgNC41NjI1LTcuMDYgMTIuMjQ5LTkuNzc0OSAwLjA0ODcgMC4wMzIzIDAuMDkyNSAwLjA2MSAwLjE0MjUgMC4wOTIyLTcuNTc3NSAyLjY5MzktMTIuMDcgNi4wNDc5LTEyLjA3IDkuNjgyNiAwIDguODg2MiAyNi44NTEgMTYuMDkxIDU5Ljk3NCAxNi4wOTEgMzMuMTIyIDAgNTkuOTc0LTcuMjA1MSA1OS45NzQtMTYuMDkxIDAtMy42MzQ4LTQuNDkyNS02Ljk4ODgtMTIuMDY5LTkuNjgyNiAwLjA0ODctMC4wMzEyIDAuMDkyNS0wLjA2IDAuMTQxMjUtMC4wOTIyIDcuNjg2MiAyLjcxNjIgMTIuMjQ5IDYuMTAyNSAxMi4yNDkgOS43NzQ5IDAgOC45MzM2LTI2Ljk5NSAxNi4xNzYtNjAuMjk1IDE2LjE3NiIgZmlsbD0iI2UzZTNlMSIvPgogICA8cGF0aCBpZD0icGF0aDU3NTciIGQ9Im0zMTkuODkgOTE2LjE3Yy0zMy4xMjIgMC01OS45NzQtNy4yMDUxLTU5Ljk3NC0xNi4wOTEgMC0zLjYzNDggNC40OTI1LTYuOTg4OCAxMi4wNy05LjY4MjYgMC4wNDYzIDAuMDMwMyAwLjA5NjIgMC4wNjI1IDAuMTQyNSAwLjA5MjgtNy40Njc1IDIuNjcyNC0xMS44OTIgNS45OTIyLTExLjg5MiA5LjU4OTkgMCA4LjgzODkgMjYuNzA5IDE2LjAwNSA1OS42NTQgMTYuMDA1czU5LjY1Mi03LjE2NiA1OS42NTItMTYuMDA1YzAtMy41OTc2LTQuNDIzOC02LjkxNzUtMTEuODkxLTkuNTg5OSAwLjA0NjMtMC4wMzAzIDAuMDk2Mi0wLjA2MjUgMC4xNDM3NS0wLjA5MjggNy41NzYyIDIuNjkzOSAxMi4wNjkgNi4wNDc5IDEyLjA2OSA5LjY4MjYgMCA4Ljg4NjItMjYuODUxIDE2LjA5MS01OS45NzQgMTYuMDkxIiBmaWxsPSIjZTJlMmUwIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTc1OSIgZD0ibTMxOS44OSA5MTYuMDhjLTMyLjk0NSAwLTU5LjY1NC03LjE2Ni01OS42NTQtMTYuMDA1IDAtMy41OTc2IDQuNDI1LTYuOTE3NSAxMS44OTItOS41ODk5IDAuMDQ4NyAwLjAzMTIgMC4wOTUgMC4wNjAxIDAuMTQzNzUgMC4wOTA5LTcuMzU4OCAyLjY1MjgtMTEuNzE1IDUuOTM5LTExLjcxNSA5LjQ5OSAwIDguNzkxIDI2LjU2NCAxNS45MTkgNTkuMzMyIDE1LjkxOSAzMi43NjkgMCA1OS4zMzItNy4xMjggNTkuMzMyLTE1LjkxOSAwLTMuNTYtNC4zNTYyLTYuODQ2Mi0xMS43MTUtOS40OTc1IDAuMDQ4OC0wLjAzMjIgMC4wOTUtMC4wNjExIDAuMTQzNzUtMC4wOTI0IDcuNDY3NSAyLjY3MjQgMTEuODkxIDUuOTkyMiAxMS44OTEgOS41ODk5IDAgOC44Mzg5LTI2LjcwOCAxNi4wMDUtNTkuNjUyIDE2LjAwNSIgZmlsbD0iI2UyZTFkZiIvPgogICA8cGF0aCBpZD0icGF0aDU3NjEiIGQ9Im0zMTkuODkgOTE2Yy0zMi43NjkgMC01OS4zMzItNy4xMjgtNTkuMzMyLTE1LjkxOSAwLTMuNTYgNC4zNTYyLTYuODQ2MiAxMS43MTUtOS40OTkgMC4wNDYzIDAuMDMwMyAwLjA5NzUgMC4wNjMgMC4xNDM3NSAwLjA5MjgtNy4yNSAyLjYyOTktMTEuNTM4IDUuODgzOC0xMS41MzggOS40MDYyIDAgOC43NDM2IDI2LjQyIDE1LjgzMyA1OS4wMTEgMTUuODMzczU5LjAxMS03LjA4ODkgNTkuMDExLTE1LjgzM2MwLTMuNTIyNS00LjI4NjItNi43NzY0LTExLjUzOC05LjQwNjIgMC4wNDYyLTAuMDI5OCAwLjA5NzUtMC4wNjEgMC4xNDM3NS0wLjA5MTMgNy4zNTg4IDIuNjUxMiAxMS43MTUgNS45Mzc1IDExLjcxNSA5LjQ5NzUgMCA4Ljc5MS0yNi41NjQgMTUuOTE5LTU5LjMzMiAxNS45MTkiIGZpbGw9IiNlMWUwZGYiLz4KICAgPHBhdGggaWQ9InBhdGg1NzYzIiBkPSJtMzE5Ljg5IDkxNS45MWMtMzIuNTkxIDAtNTkuMDExLTcuMDg4OS01OS4wMTEtMTUuODMzIDAtMy41MjI1IDQuMjg3NS02Ljc3NjQgMTEuNTM4LTkuNDA2MiAwLjA1IDAuMDMxMiAwLjA5NjIgMC4wNjEgMC4xNDUgMC4wOTIyLTcuMTQyNSAyLjYwODktMTEuMzYyIDUuODI5MS0xMS4zNjIgOS4zMTQgMCA4LjY5NjIgMjYuMjc4IDE1Ljc0NiA1OC42OTEgMTUuNzQ2IDMyLjQxNCAwIDU4LjY5MS03LjA0OTggNTguNjkxLTE1Ljc0NiAwLTMuNDg0OS00LjIyLTYuNzA1MS0xMS4zNjItOS4zMTQgMC4wNDg4LTAuMDMxMiAwLjA5NS0wLjA2MSAwLjE0NS0wLjA5MjIgNy4yNTEyIDIuNjI5OSAxMS41MzggNS44ODM4IDExLjUzOCA5LjQwNjIgMCA4Ljc0MzYtMjYuNDIgMTUuODMzLTU5LjAxMSAxNS44MzMiIGZpbGw9IiNlMGUwZGUiLz4KICAgPHBhdGggaWQ9InBhdGg1NzY1IiBkPSJtMzE5Ljg5IDkxNS44M2MtMzIuNDE0IDAtNTguNjkxLTcuMDQ5OC01OC42OTEtMTUuNzQ2IDAtMy40ODQ5IDQuMjItNi43MDUxIDExLjM2Mi05LjMxNCAwLjA0NzUgMC4wMzAzIDAuMDk4OCAwLjA2MjUgMC4xNDYyNSAwLjA5MjgtNy4wMzYyIDIuNTg2LTExLjE4OCA1Ljc3MzUtMTEuMTg4IDkuMjIxMiAwIDguNjQ4OSAyNi4xMzIgMTUuNjYgNTguMzcgMTUuNjYgMzIuMjM2IDAgNTguMzctNy4wMTEzIDU4LjM3LTE1LjY2IDAtMy40NDc4LTQuMTUyNS02LjYzNTItMTEuMTg4LTkuMjIxMiAwLjA0NzUtMC4wMzAzIDAuMDk4Ny0wLjA2MjUgMC4xNDYyNS0wLjA5MjggNy4xNDI1IDIuNjA4OSAxMS4zNjIgNS44MjkxIDExLjM2MiA5LjMxNCAwIDguNjk2Mi0yNi4yNzggMTUuNzQ2LTU4LjY5MSAxNS43NDYiIGZpbGw9IiNlMGRmZGQiLz4KICAgPHBhdGggaWQ9InBhdGg1NzY3IiBkPSJtMzE5Ljg5IDkxNS43NGMtMzIuMjM4IDAtNTguMzctNy4wMTEzLTU4LjM3LTE1LjY2IDAtMy40NDc4IDQuMTUxMi02LjYzNTIgMTEuMTg4LTkuMjIxMiAwLjA0ODggMC4wMzEyIDAuMDk2MiAwLjA2MTEgMC4xNDYyNSAwLjA5MjQtNi45Mjc1IDIuNTY0OS0xMS4wMTIgNS43MTg4LTExLjAxMiA5LjEyODkgMCA4LjYwMTEgMjUuOTg5IDE1LjU3NSA1OC4wNDkgMTUuNTc1czU4LjA0OS02Ljk3NDEgNTguMDQ5LTE1LjU3NWMwLTMuNDEwMS00LjA4NS02LjU2NC0xMS4wMTItOS4xMjg5IDAuMDUtMC4wMzEyIDAuMDk3NS0wLjA2MTEgMC4xNDYyNS0wLjA5MjQgNy4wMzUgMi41ODYgMTEuMTg4IDUuNzczNSAxMS4xODggOS4yMjEyIDAgOC42NDg5LTI2LjEzNCAxNS42Ni01OC4zNyAxNS42NiIgZmlsbD0iI2RmZGVkYyIvPgogICA8cGF0aCBpZD0icGF0aDU3NjkiIGQ9Im0zMTkuODkgOTE1LjY2Yy0zMi4wNiAwLTU4LjA0OS02Ljk3NDEtNTguMDQ5LTE1LjU3NSAwLTMuNDEwMSA0LjA4NS02LjU2NCAxMS4wMTItOS4xMjg5IDAuMDQ4NyAwLjAzMTIgMC4wOTc1IDAuMDYxNSAwLjE0NjI1IDAuMDkyOC02LjgyMTIgMi41NDI1LTEwLjgzOSA1LjY2MzYtMTAuODM5IDkuMDM2MSAwIDguNTUzOCAyNS44NDYgMTUuNDg5IDU3LjcyOSAxNS40ODkgMzEuODgyIDAgNTcuNzI5LTYuOTM1IDU3LjcyOS0xNS40ODkgMC0zLjM3MjUtNC4wMTc1LTYuNDkyNi0xMC44MzktOS4wMzYxIDAuMDQ4OC0wLjAzMTIgMC4wOTc1LTAuMDYxNSAwLjE0NjI1LTAuMDkyOCA2LjkyNzUgMi41NjQ5IDExLjAxMiA1LjcxODggMTEuMDEyIDkuMTI4OSAwIDguNjAxMS0yNS45ODkgMTUuNTc1LTU4LjA0OSAxNS41NzUiIGZpbGw9IiNkZWRkZGIiLz4KICAgPHBhdGggaWQ9InBhdGg1NzcxIiBkPSJtMzE5Ljg5IDkxNS41N2MtMzEuODgyIDAtNTcuNzI5LTYuOTM1LTU3LjcyOS0xNS40ODkgMC0zLjM3MjUgNC4wMTc1LTYuNDkzNiAxMC44MzktOS4wMzYxIDAuMDUgMC4wMzEyIDAuMDk4NyAwLjA2MSAwLjE0ODc1IDAuMDkyMi02LjcxNSAyLjUyMTUtMTAuNjY2IDUuNjA4OS0xMC42NjYgOC45NDM5IDAgOC41MDY0IDI1LjcwMiAxNS40MDIgNTcuNDA4IDE1LjQwMiAzMS43MDUgMCA1Ny40MDgtNi44OTYgNTcuNDA4LTE1LjQwMiAwLTMuMzM1LTMuOTUtNi40MjI0LTEwLjY2Ni04Ljk0MzkgMC4wNS0wLjAzMTIgMC4wOTg4LTAuMDYxIDAuMTQ4NzUtMC4wOTIyIDYuODIxMiAyLjU0MzUgMTAuODM5IDUuNjYzNiAxMC44MzkgOS4wMzYxIDAgOC41NTM4LTI1Ljg0NiAxNS40ODktNTcuNzI5IDE1LjQ4OSIgZmlsbD0iI2RkZGNkYSIvPgogICA8cGF0aCBpZD0icGF0aDU3NzMiIGQ9Im0zMTkuODkgOTE1LjQ4Yy0zMS43MDUgMC01Ny40MDgtNi44OTYtNTcuNDA4LTE1LjQwMiAwLTMuMzM1IDMuOTUxMi02LjQyMjQgMTAuNjY2LTguOTQzOSAwLjA0ODcgMC4wMzAzIDAuMDk4NyAwLjA2MTUgMC4xNDg3NSAwLjA5MjgtNi42MSAyLjQ5ODYtMTAuNDk1IDUuNTUzOC0xMC40OTUgOC44NTExIDAgOC40NTkgMjUuNTU5IDE1LjMxNiA1Ny4wODggMTUuMzE2IDMxLjUyOCAwIDU3LjA4OC02Ljg1NzQgNTcuMDg4LTE1LjMxNiAwLTMuMjk3NC0zLjg4NS02LjM1MjUtMTAuNDk0LTguODUxMSAwLjA0ODctMC4wMzEyIDAuMDk4Ny0wLjA2MjUgMC4xNDc1LTAuMDkyOCA2LjcxNjIgMi41MjE1IDEwLjY2NiA1LjYwODkgMTAuNjY2IDguOTQzOSAwIDguNTA2NC0yNS43MDIgMTUuNDAyLTU3LjQwOCAxNS40MDIiIGZpbGw9IiNkZGRjZGEiLz4KICAgPHBhdGggaWQ9InBhdGg1Nzc1IiBkPSJtMzE5Ljg5IDkxNS40Yy0zMS41MjkgMC01Ny4wODgtNi44NTc0LTU3LjA4OC0xNS4zMTYgMC0zLjI5NzQgMy44ODUtNi4zNTI1IDEwLjQ5NS04Ljg1MTEgMC4wNDg3IDAuMDI5OSAwLjA5ODcgMC4wNjExIDAuMTQ4NzUgMC4wOTI0LTYuNTAzOCAyLjQ3NzUtMTAuMzIyIDUuNDk5LTEwLjMyMiA4Ljc1ODggMCA4LjQxMTEgMjUuNDE1IDE1LjIzIDU2Ljc2NiAxNS4yM3M1Ni43NjYtNi44MTg4IDU2Ljc2Ni0xNS4yM2MwLTMuMjU5OC0zLjgxODgtNi4yODEyLTEwLjMyMi04Ljc1ODggMC4wNS0wLjAzMTIgMC4xLTAuMDYyNSAwLjE1LTAuMDkyNCA2LjYwODggMi40OTg2IDEwLjQ5NCA1LjU1MzggMTAuNDk0IDguODUxMSAwIDguNDU5LTI1LjU2IDE1LjMxNi01Ny4wODggMTUuMzE2IiBmaWxsPSIjZGNkYmQ5Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTc3NyIgZD0ibTMxOS44OSA5MTUuMzFjLTMxLjM1MSAwLTU2Ljc2Ni02LjgxODgtNTYuNzY2LTE1LjIzIDAtMy4yNTk4IDMuODE4OC02LjI4MTIgMTAuMzIyLTguNzU4OCAwLjA0ODcgMC4wMzAzIDAuMSAwLjA2MSAwLjE1IDAuMDkyMy02LjM5ODggMi40NTUxLTEwLjE1MSA1LjQ0MzktMTAuMTUxIDguNjY2NSAwIDguMzYzOCAyNS4yNzEgMTUuMTQ0IDU2LjQ0NSAxNS4xNDRzNTYuNDQ1LTYuNzc5OCA1Ni40NDUtMTUuMTQ0YzAtMy4yMjI2LTMuNzUyNS02LjIxMTQtMTAuMTUxLTguNjY2NSAwLjA1LTAuMDMxMiAwLjEwMTI1LTAuMDYyIDAuMTUtMC4wOTIzIDYuNTAzOCAyLjQ3NzUgMTAuMzIyIDUuNDk5IDEwLjMyMiA4Ljc1ODggMCA4LjQxMTEtMjUuNDE1IDE1LjIzLTU2Ljc2NiAxNS4yMyIgZmlsbD0iI2RiZGFkOCIvPgogICA8cGF0aCBpZD0icGF0aDU3NzkiIGQ9Im0zMTkuODkgOTE1LjIyYy0zMS4xNzQgMC01Ni40NDUtNi43Nzk4LTU2LjQ0NS0xNS4xNDQgMC0zLjIyMjYgMy43NTI1LTYuMjExNCAxMC4xNTEtOC42NjY1IDAuMDQ4NyAwLjAzMDMgMC4xIDAuMDYxNSAwLjE1IDAuMDkyOC02LjI5MzggMi40MzI2LTkuOTgxMiA1LjM4NzgtOS45ODEyIDguNTczOCAwIDguMzE2NCAyNS4xMjkgMTUuMDU4IDU2LjEyNSAxNS4wNThzNTYuMTI1LTYuNzQxMiA1Ni4xMjUtMTUuMDU4YzAtMy4xODYtMy42ODc1LTYuMTQxMS05Ljk4MTItOC41NzM4IDAuMDUtMC4wMzEyIDAuMTAxMjUtMC4wNjI1IDAuMTUtMC4wOTI4IDYuMzk4OCAyLjQ1NTEgMTAuMTUxIDUuNDQzOSAxMC4xNTEgOC42NjY1IDAgOC4zNjM4LTI1LjI3MSAxNS4xNDQtNTYuNDQ1IDE1LjE0NCIgZmlsbD0iI2RhZDlkNyIvPgogICA8cGF0aCBpZD0icGF0aDU3ODEiIGQ9Im0zMTkuODkgOTE1LjE0Yy0zMC45OTYgMC01Ni4xMjUtNi43NDEyLTU2LjEyNS0xNS4wNTggMC0zLjE4NiAzLjY4NzUtNi4xNDExIDkuOTgxMi04LjU3MzggMC4wNSAwLjAyOTggMC4xMDEyNSAwLjA2MSAwLjE1MTI1IDAuMDkyMy02LjE4NzUgMi40MTE2LTkuODExMiA1LjMzMjYtOS44MTEyIDguNDgxNSAwIDguMjY4NSAyNC45ODQgMTQuOTczIDU1LjgwNCAxNC45NzNzNTUuODA0LTYuNzA0MiA1NS44MDQtMTQuOTczYzAtMy4xNDg5LTMuNjIyNS02LjA2OTktOS44MTEyLTguNDgxNSAwLjA1LTAuMDI5OCAwLjEwMTI1LTAuMDYyNSAwLjE1MTI1LTAuMDkyMyA2LjI5MzggMi40MzI2IDkuOTgxMiA1LjM4NzggOS45ODEyIDguNTczOCAwIDguMzE2NC0yNS4xMjkgMTUuMDU4LTU2LjEyNSAxNS4wNTgiIGZpbGw9IiNkOWQ4ZDYiLz4KICAgPHBhdGggaWQ9InBhdGg1NzgzIiBkPSJtMzE5Ljg5IDkxNS4wNWMtMzAuODIgMC01NS44MDQtNi43MDQyLTU1LjgwNC0xNC45NzMgMC0zLjE0ODkgMy42MjM4LTYuMDY5OSA5LjgxMTItOC40ODE1IDAuMDUyNSAwLjAzMTIgMC4xIDAuMDYwMSAwLjE1MjUgMC4wOTI5LTYuMDg1IDIuMzg4Ni05LjY0MzggNS4yNzczLTkuNjQzOCA4LjM4ODYgMCA4LjIyMTIgMjQuODQxIDE0Ljg4NiA1NS40ODQgMTQuODg2IDMwLjY0MiAwIDU1LjQ4NC02LjY2NSA1NS40ODQtMTQuODg2IDAtMy4xMTE0LTMuNTU4OC02LTkuNjQzOC04LjM4ODYgMC4wNTI1LTAuMDMxMiAwLjEtMC4wNjE2IDAuMTUyNS0wLjA5MjkgNi4xODg4IDIuNDExNiA5LjgxMTIgNS4zMzI2IDkuODExMiA4LjQ4MTUgMCA4LjI2ODUtMjQuOTg0IDE0Ljk3My01NS44MDQgMTQuOTczIiBmaWxsPSIjZDhkN2Q2Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTc4NSIgZD0ibTMxOS44OSA5MTQuOTdjLTMwLjY0MiAwLTU1LjQ4NC02LjY2NS01NS40ODQtMTQuODg2IDAtMy4xMTE0IDMuNTU4OC02IDkuNjQzOC04LjM4ODYgMC4wNSAwLjAyOTggMC4xMDI1IDAuMDYyNSAwLjE1MjUgMC4wOTIzLTUuOTgxMiAyLjM2NjMtOS40NzUgNS4yMjExLTkuNDc1IDguMjk2NCAwIDguMTczOSAyNC42OTggMTQuOCA1NS4xNjIgMTQuOCAzMC40NjUgMCA1NS4xNjItNi42MjU5IDU1LjE2Mi0xNC44IDAtMy4wNzUyLTMuNDkzOC01LjkzMDEtOS40NzUtOC4yOTY0IDAuMDUtMC4wMjk4IDAuMTAyNS0wLjA2MjUgMC4xNTI1LTAuMDkyMyA2LjA4NSAyLjM4ODYgOS42NDM4IDUuMjc3MyA5LjY0MzggOC4zODg2IDAgOC4yMjEyLTI0Ljg0MSAxNC44ODYtNTUuNDg0IDE0Ljg4NiIgZmlsbD0iI2Q3ZDZkNSIvPgogICA8cGF0aCBpZD0icGF0aDU3ODciIGQ9Im0zMTkuODkgOTE0Ljg4Yy0zMC40NjUgMC01NS4xNjItNi42MjU5LTU1LjE2Mi0xNC44IDAtMy4wNzUyIDMuNDkzOC01LjkzMDEgOS40NzUtOC4yOTY0IDAuMDUyNSAwLjAzMTIgMC4xMDEyNSAwLjA2MTUgMC4xNTM3NSAwLjA5MjgtNS44Nzc1IDIuMzQzOC05LjMwNzUgNS4xNjYtOS4zMDc1IDguMjAzNiAwIDguMTI2NSAyNC41NTIgMTQuNzE0IDU0Ljg0MSAxNC43MTQgMzAuMjg4IDAgNTQuODQxLTYuNTg3NCA1NC44NDEtMTQuNzE0IDAtMy4wMzc2LTMuNDMtNS44NTk5LTkuMzA3NS04LjIwMzYgMC4wNTI1LTAuMDMxMiAwLjEwMTI1LTAuMDYgMC4xNTM3NS0wLjA5MjggNS45ODEyIDIuMzY2MyA5LjQ3NSA1LjIyMTEgOS40NzUgOC4yOTY0IDAgOC4xNzM5LTI0LjY5OCAxNC44LTU1LjE2MiAxNC44IiBmaWxsPSIjZDZkNmQ0Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTc4OSIgZD0ibTMxOS44OSA5MTQuNzljLTMwLjI4OSAwLTU0Ljg0MS02LjU4NzQtNTQuODQxLTE0LjcxNCAwLTMuMDM3NiAzLjQzLTUuODU5OSA5LjMwNzUtOC4yMDM2IDAuMDUgMC4wMjk3IDAuMTA1IDAuMDYyNSAwLjE1NSAwLjA5MjItNS43NzYyIDIuMzIyNy05LjE0MjUgNS4xMTE0LTkuMTQyNSA4LjExMTQgMCA4LjA3ODYgMjQuNDEgMTQuNjI3IDU0LjUyMSAxNC42MjdzNTQuNTIxLTYuNTQ4OCA1NC41MjEtMTQuNjI3YzAtMy0zLjM2NjItNS43ODg2LTkuMTQyNS04LjEwOTkgMC4wNTEyLTAuMDMwMyAwLjEwNS0wLjA2NCAwLjE1NS0wLjA5MzcgNS44Nzc1IDIuMzQzOCA5LjMwNzUgNS4xNjYgOS4zMDc1IDguMjAzNiAwIDguMTI2NS0yNC41NTQgMTQuNzE0LTU0Ljg0MSAxNC43MTQiIGZpbGw9IiNkNWQ1ZDMiLz4KICAgPHBhdGggaWQ9InBhdGg1NzkxIiBkPSJtMzE5Ljg5IDkxNC43MWMtMzAuMTExIDAtNTQuNTIxLTYuNTQ4OC01NC41MjEtMTQuNjI3IDAtMyAzLjM2NjItNS43ODg2IDkuMTQyNS04LjExMTQgMC4wNTI1IDAuMDMyOCAwLjEwMjUgMC4wNjI1IDAuMTU1IDAuMDkzNy01LjY3MjUgMi4yOTg5LTguOTc2MiA1LjA1NTItOC45NzYyIDguMDE3NiAwIDguMDMxMiAyNC4yNjYgMTQuNTQxIDU0LjIgMTQuNTQxczU0LjItNi41MDk4IDU0LjItMTQuNTQxYzAtMi45NjI0LTMuMzAzOC01LjcxODgtOC45NzYyLTguMDE3NiAwLjA1MjUtMC4wMzEyIDAuMTAyNS0wLjA2MSAwLjE1NS0wLjA5MjIgNS43NzYyIDIuMzIxMiA5LjE0MjUgNS4xMDk5IDkuMTQyNSA4LjEwOTkgMCA4LjA3ODYtMjQuNDEgMTQuNjI3LTU0LjUyMSAxNC42MjciIGZpbGw9IiNkNGQ0ZDIiLz4KICAgPHBhdGggaWQ9InBhdGg1NzkzIiBkPSJtMzE5Ljg5IDkxNC42MmMtMjkuOTM0IDAtNTQuMi02LjUwOTgtNTQuMi0xNC41NDEgMC0yLjk2MjQgMy4zMDM4LTUuNzE4OCA4Ljk3NjItOC4wMTc2IDAuMDUyNSAwLjAzMTIgMC4xMDM3NSAwLjA2MTYgMC4xNTYyNSAwLjA5MjktNS41NzEyIDIuMjc1OS04LjgxMjUgNC45OTg1LTguODEyNSA3LjkyNDggMCA3Ljk4MzkgMjQuMTIyIDE0LjQ1NSA1My44OCAxNC40NTUgMjkuNzU2IDAgNTMuODc5LTYuNDcxMiA1My44NzktMTQuNDU1IDAtMi45MjYyLTMuMjQtNS42NDg5LTguODExMi03LjkyNDggMC4wNTM4LTAuMDMxMiAwLjEwMzc1LTAuMDYxNiAwLjE1NjI1LTAuMDkyOSA1LjY3MjUgMi4yOTg5IDguOTc2MiA1LjA1NTIgOC45NzYyIDguMDE3NiAwIDguMDMxMi0yNC4yNjYgMTQuNTQxLTU0LjIgMTQuNTQxIiBmaWxsPSIjZDRkM2QxIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTc5NSIgZD0ibTMxOS44OSA5MTQuNTRjLTI5Ljc1OCAwLTUzLjg4LTYuNDcxMi01My44OC0xNC40NTUgMC0yLjkyNjIgMy4yNDEyLTUuNjQ4OSA4LjgxMjUtNy45MjQ4IDAuMDUyNSAwLjAzMTIgMC4xMDM3NSAwLjA2MSAwLjE1NjI1IDAuMDkyMy01LjQ3IDIuMjUzOS04LjY0NzUgNC45NDM5LTguNjQ3NSA3LjgzMjUgMCA3LjkzNiAyMy45NzkgMTQuMzcgNTMuNTU5IDE0LjM3IDI5LjU3OSAwIDUzLjU1OS02LjQzNDEgNTMuNTU5LTE0LjM3IDAtMi44ODg2LTMuMTc3NS01LjU3ODYtOC42NDc1LTcuODMyNSAwLjA1MjUtMC4wMjk4IDAuMTAzNzUtMC4wNjEgMC4xNTYyNS0wLjA5MjMgNS41NzEyIDIuMjc1OSA4LjgxMTIgNC45OTg1IDguODExMiA3LjkyNDggMCA3Ljk4MzktMjQuMTIyIDE0LjQ1NS01My44NzkgMTQuNDU1IiBmaWxsPSIjZDNkMmQwIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTc5NyIgZD0ibTMxOS44OSA5MTQuNDVjLTI5LjU4IDAtNTMuNTU5LTYuNDM0MS01My41NTktMTQuMzcgMC0yLjg4ODYgMy4xNzc1LTUuNTc4NiA4LjY0NzUtNy44MzI1IDAuMDUgMC4wMjk3IDAuMTA3NSAwLjA2NCAwLjE1NzUgMC4wOTM3LTUuMzY4OCAyLjIzLTguNDgzOCA0Ljg4NjItOC40ODM4IDcuNzM4OCAwIDcuODg4NiAyMy44MzUgMTQuMjg0IDUzLjIzOCAxNC4yODQgMjkuNDAyIDAgNTMuMjM4LTYuMzk1MSA1My4yMzgtMTQuMjg0IDAtMi44NTI1LTMuMTE1LTUuNTA4OC04LjQ4MzgtNy43Mzg4IDAuMDUtMC4wMjk4IDAuMTA3NS0wLjA2NCAwLjE1NzUtMC4wOTM3IDUuNDcgMi4yNTM5IDguNjQ3NSA0Ljk0MzkgOC42NDc1IDcuODMyNSAwIDcuOTM2LTIzLjk4IDE0LjM3LTUzLjU1OSAxNC4zNyIgZmlsbD0iI2QyZDFjZiIvPgogICA8cGF0aCBpZD0icGF0aDU3OTkiIGQ9Im0zMTkuODkgOTE0LjM2Yy0yOS40MDIgMC01My4yMzgtNi4zOTUxLTUzLjIzOC0xNC4yODQgMC0yLjg1MjUgMy4xMTUtNS41MDg4IDguNDgzOC03LjczODggMC4wNTI1IDAuMDMxMiAwLjEwNjI1IDAuMDYxIDAuMTU4NzUgMC4wOTIyLTUuMjY4OCAyLjIwNzUtOC4zMjI1IDQuODMxNS04LjMyMjUgNy42NDY1IDAgNy44NDEyIDIzLjY5MiAxNC4xOTcgNTIuOTE4IDE0LjE5NyAyOS4yMjUgMCA1Mi45MTgtNi4zNTYgNTIuOTE4LTE0LjE5NyAwLTIuODE1LTMuMDUzOC01LjQzOS04LjMyMjUtNy42NDY1IDAuMDUyNS0wLjAyOTcgMC4xMDYyNS0wLjA2MSAwLjE1ODc1LTAuMDkyMiA1LjM2ODggMi4yMyA4LjQ4MzggNC44ODYyIDguNDgzOCA3LjczODggMCA3Ljg4ODYtMjMuODM1IDE0LjI4NC01My4yMzggMTQuMjg0IiBmaWxsPSIjZDFkMGNmIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTgwMSIgZD0ibTMxOS44OSA5MTQuMjhjLTI5LjIyNSAwLTUyLjkxOC02LjM1Ni01Mi45MTgtMTQuMTk3IDAtMi44MTUgMy4wNTM4LTUuNDM5IDguMzIyNS03LjY0NjUgMC4wNTUgMC4wMzI4IDAuMTAzNzUgMC4wNjE1IDAuMTU4NzUgMC4wOTM3LTUuMTY3NSAyLjE4NDEtOC4xNiA0Ljc3NC04LjE2IDcuNTUyOCAwIDcuNzk0IDIzLjU0OCAxNC4xMTEgNTIuNTk2IDE0LjExMSAyOS4wNDkgMCA1Mi41OTYtNi4zMTczIDUyLjU5Ni0xNC4xMTEgMC0yLjc3ODgtMi45OTI1LTUuMzY4Ni04LjE2LTcuNTUyOCAwLjA1NS0wLjAzMjIgMC4xMDM3NS0wLjA2MSAwLjE1ODc1LTAuMDkzNyA1LjI2ODggMi4yMDc1IDguMzIyNSA0LjgzMTUgOC4zMjI1IDcuNjQ2NSAwIDcuODQxMi0yMy42OTIgMTQuMTk3LTUyLjkxOCAxNC4xOTciIGZpbGw9IiNkMGNmY2QiLz4KICAgPHBhdGggaWQ9InBhdGg1ODAzIiBkPSJtMzE5Ljg5IDkxNC4xOWMtMjkuMDQ5IDAtNTIuNTk2LTYuMzE3My01Mi41OTYtMTQuMTExIDAtMi43Nzg4IDIuOTkyNS01LjM2ODYgOC4xNi03LjU1MjggMC4wNTM3IDAuMDMwMyAwLjEwNzUgMC4wNjI1IDAuMTYxMjUgMC4wOTI4LTUuMDY4OCAyLjE2MTEtOCA0LjcxODgtOCA3LjQ2IDAgNy43NDYxIDIzLjQwNCAxNC4wMjUgNTIuMjc1IDE0LjAyNXM1Mi4yNzUtNi4yNzg4IDUyLjI3NS0xNC4wMjVjMC0yLjc0MTItMi45MzEyLTUuMjk4OS04LTcuNDYgMC4wNTM3LTAuMDMwMyAwLjEwNzUtMC4wNjI1IDAuMTYxMjUtMC4wOTI4IDUuMTY3NSAyLjE4NDEgOC4xNiA0Ljc3NCA4LjE2IDcuNTUyOCAwIDcuNzk0LTIzLjU0OCAxNC4xMTEtNTIuNTk2IDE0LjExMSIgZmlsbD0iI2NmY2VjYyIvPgogICA8cGF0aCBpZD0icGF0aDU4MDUiIGQ9Im0zMTkuODkgOTE0LjFjLTI4Ljg3MSAwLTUyLjI3NS02LjI3ODgtNTIuMjc1LTE0LjAyNSAwLTIuNzQxMiAyLjkzMTItNS4yOTg5IDgtNy40NiAwLjA1MjUgMC4wMzEyIDAuMTA3NSAwLjA2MjUgMC4xNiAwLjA5MzctNC45Njg4IDIuMTM3Mi03Ljg0IDQuNjYxMS03Ljg0IDcuMzY2MiAwIDcuNjk4OCAyMy4yNjEgMTMuOTQgNTEuOTU1IDEzLjk0czUxLjk1NS02LjI0MTIgNTEuOTU1LTEzLjk0YzAtMi43MDUxLTIuODcxMi01LjIyOS03Ljg0LTcuMzY2MiAwLjA1MjUtMC4wMzEyIDAuMTA3NS0wLjA2MjUgMC4xNi0wLjA5MzcgNS4wNjg4IDIuMTYxMSA4IDQuNzE4OCA4IDcuNDYgMCA3Ljc0NjEtMjMuNDA0IDE0LjAyNS01Mi4yNzUgMTQuMDI1IiBmaWxsPSIjY2VjZGNiIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTgwNyIgZD0ibTMxOS44OSA5MTQuMDJjLTI4LjY5NCAwLTUxLjk1NS02LjI0MTItNTEuOTU1LTEzLjk0IDAtMi43MDUxIDIuODcxMi01LjIyOSA3Ljg0LTcuMzY2MiAwLjA1MzggMC4wMjk5IDAuMTA4NzUgMC4wNjI1IDAuMTYyNSAwLjA5MjQtNC44NyAyLjExMzgtNy42ODEyIDQuNjA2NC03LjY4MTIgNy4yNzM5IDAgNy42NTE0IDIzLjExOCAxMy44NTQgNTEuNjM0IDEzLjg1NHM1MS42MzQtNi4yMDIxIDUxLjYzNC0xMy44NTRjMC0yLjY2NzUtMi44MTEyLTUuMTU4OC03LjY4MTItNy4yNzM5IDAuMDUzOC0wLjAyOTkgMC4xMDg3NS0wLjA2MjUgMC4xNjI1LTAuMDkyNCA0Ljk2ODggMi4xMzcyIDcuODQgNC42NjExIDcuODQgNy4zNjYyIDAgNy42OTg4LTIzLjI2MSAxMy45NC01MS45NTUgMTMuOTQiIGZpbGw9IiNjZGNjY2EiLz4KICAgPHBhdGggaWQ9InBhdGg1ODA5IiBkPSJtMzE5Ljg5IDkxMy45M2MtMjguNTE2IDAtNTEuNjM0LTYuMjAyMS01MS42MzQtMTMuODU0IDAtMi42Njc1IDIuODExMi01LjE2MDEgNy42ODEyLTcuMjczOSAwLjA1NSAwLjAzMjYgMC4xMDc1IDAuMDYxNSAwLjE2MjUgMC4wOTM3LTQuNzcyNSAyLjA5MDItNy41MjM4IDQuNTQ4Ny03LjUyMzggNy4xODAxIDAgNy42MDM1IDIyLjk3NCAxMy43NjggNTEuMzE0IDEzLjc2OHM1MS4zMTItNi4xNjQxIDUxLjMxMi0xMy43NjhjMC0yLjYzMTQtMi43NTEyLTUuMDg5OS03LjUyMjUtNy4xODAxIDAuMDU1LTAuMDMyMyAwLjEwNzUtMC4wNjExIDAuMTYyNS0wLjA5MzcgNC44NyAyLjExNTEgNy42ODEyIDQuNjA2NCA3LjY4MTIgNy4yNzM5IDAgNy42NTE0LTIzLjExOCAxMy44NTQtNTEuNjM0IDEzLjg1NCIgZmlsbD0iI2NjY2JjOSIvPgogICA8cGF0aCBpZD0icGF0aDU4MTEiIGQ9Im0zMTkuODkgOTEzLjg1Yy0yOC4zNCAwLTUxLjMxNC02LjE2NDEtNTEuMzE0LTEzLjc2OCAwLTIuNjMxNCAyLjc1MTItNS4wODk5IDcuNTIzOC03LjE4MDEgMC4wNTM3IDAuMDMwMiAwLjExIDAuMDYyNSAwLjE2Mzc1IDAuMDkyNy00LjY3NSAyLjA2NzQtNy4zNjYyIDQuNDkyMS03LjM2NjIgNy4wODc0IDAgNy41NTYxIDIyLjgzIDEzLjY4MSA1MC45OTIgMTMuNjgxczUwLjk5Mi02LjEyNSA1MC45OTItMTMuNjgxYzAtMi41OTM4LTIuNjkyNS01LjAyLTcuMzY2Mi03LjA4NjQgMC4wNTM3LTAuMDMxMiAwLjExLTAuMDYzNSAwLjE2Mzc1LTAuMDkzNyA0Ljc3MTIgMi4wOTAyIDcuNTIyNSA0LjU0ODcgNy41MjI1IDcuMTgwMSAwIDcuNjAzNS0yMi45NzIgMTMuNzY4LTUxLjMxMiAxMy43NjgiIGZpbGw9IiNjYmM5YzgiLz4KICAgPHBhdGggaWQ9InBhdGg1ODEzIiBkPSJtMzE5Ljg5IDkxMy43NmMtMjguMTYyIDAtNTAuOTkyLTYuMTI1LTUwLjk5Mi0xMy42ODEgMC0yLjU5NTIgMi42OTEyLTUuMDIgNy4zNjYyLTcuMDg3NCAwLjA1NjIgMC4wMzIzIDAuMTA4NzUgMC4wNjI1IDAuMTY1IDAuMDkzNy00LjU3NzUgMi4wNDM0LTcuMjEgNC40MzYtNy4yMSA2Ljk5MzYgMCA3LjUwODggMjIuNjg2IDEzLjU5NSA1MC42NzEgMTMuNTk1czUwLjY3MS02LjA4NjUgNTAuNjcxLTEzLjU5NWMwLTIuNTU3Ni0yLjYzMjUtNC45NTAyLTcuMjEtNi45OTM2IDAuMDU2My0wLjAzMTIgMC4xMS0wLjA2MTUgMC4xNjUtMC4wOTI3IDQuNjczOCAyLjA2NjQgNy4zNjYyIDQuNDkyNiA3LjM2NjIgNy4wODY0IDAgNy41NTYxLTIyLjgzIDEzLjY4MS01MC45OTIgMTMuNjgxIiBmaWxsPSIjY2FjOGM3Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTgxNSIgZD0ibTMxOS44OSA5MTMuNjhjLTI3Ljk4NSAwLTUwLjY3MS02LjA4NjUtNTAuNjcxLTEzLjU5NSAwLTIuNTU3NiAyLjYzMjUtNC45NTAyIDcuMjEtNi45OTM2IDAuMDU1IDAuMDMxMiAwLjEwODc1IDAuMDYyNSAwLjE2NSAwLjA5MzctNC40ODEyIDIuMDItNy4wNTUgNC4zNzg0LTcuMDU1IDYuODk5OSAwIDcuNDYxNCAyMi41NDIgMTMuNTA5IDUwLjM1MSAxMy41MDkgMjcuODA4IDAgNTAuMzUxLTYuMDQ3NCA1MC4zNTEtMTMuNTA5IDAtMi41MjE1LTIuNTczOC00Ljg3OTktNy4wNTUtNi44OTk5IDAuMDU2My0wLjAzMTIgMC4xMS0wLjA2MTUgMC4xNjUtMC4wOTM3IDQuNTc3NSAyLjA0MzQgNy4yMSA0LjQzNiA3LjIxIDYuOTkzNiAwIDcuNTA4OC0yMi42ODYgMTMuNTk1LTUwLjY3MSAxMy41OTUiIGZpbGw9IiNjOWM3YzYiLz4KICAgPHBhdGggaWQ9InBhdGg1ODE3IiBkPSJtMzE5Ljg5IDkxMy41OWMtMjcuODA5IDAtNTAuMzUxLTYuMDQ3NC01MC4zNTEtMTMuNTA5IDAtMi41MjE1IDIuNTczOC00Ljg3OTkgNy4wNTUtNi44OTk5IDAuMDU2MiAwLjAzMTIgMC4xMSAwLjA2MjUgMC4xNjYyNSAwLjA5MzctNC4zODUgMS45OTQ2LTYuOSA0LjMyMjItNi45IDYuODA2MSAwIDcuNDEzNiAyMi4zOTkgMTMuNDIyIDUwLjAzIDEzLjQyMnM1MC4wMy02LjAwODcgNTAuMDMtMTMuNDIyYzAtMi40ODM5LTIuNTE1LTQuODExNS02LjktNi44MDYxIDAuMDU2My0wLjAzMTIgMC4xMS0wLjA2MjUgMC4xNjYyNS0wLjA5MzcgNC40ODEyIDIuMDIgNy4wNTUgNC4zNzg0IDcuMDU1IDYuODk5OSAwIDcuNDYxNC0yMi41NDQgMTMuNTA5LTUwLjM1MSAxMy41MDkiIGZpbGw9IiNjOGM2YzUiLz4KICAgPHBhdGggaWQ9InBhdGg1ODE5IiBkPSJtMzE5Ljg5IDkxMy41Yy0yNy42MzEgMC01MC4wMy02LjAwODctNTAuMDMtMTMuNDIyIDAtMi40ODM5IDIuNTE1LTQuODExNSA2LjktNi44MDYxIDAuMDUzOCAwLjAyOTcgMC4xMTM3NSAwLjA2MzUgMC4xNjc1IDAuMDkzNy00LjI5IDEuOTcxMS02Ljc0NzUgNC4yNjQ2LTYuNzQ3NSA2LjcxMjQgMCA3LjM2NjIgMjIuMjU2IDEzLjMzNyA0OS43MSAxMy4zMzdzNDkuNzA5LTUuOTcxMiA0OS43MDktMTMuMzM3YzAtMi40NDc4LTIuNDU3NS00Ljc0MTItNi43NDYyLTYuNzEyNCAwLjA1NjItMC4wMzEyIDAuMTExMjUtMC4wNjI1IDAuMTY3NS0wLjA5MzcgNC4zODUgMS45OTQ2IDYuOSA0LjMyMjIgNi45IDYuODA2MSAwIDcuNDEzNi0yMi4zOTkgMTMuNDIyLTUwLjAzIDEzLjQyMiIgZmlsbD0iI2M3YzVjMyIvPgogICA8cGF0aCBpZD0icGF0aDU4MjEiIGQ9Im0zMTkuODkgOTEzLjQyYy0yNy40NTQgMC00OS43MS01Ljk3MTItNDkuNzEtMTMuMzM3IDAtMi40NDc4IDIuNDU3NS00Ljc0MTIgNi43NDc1LTYuNzEyNCAwLjA1ODcgMC4wMzIzIDAuMTEgMC4wNiAwLjE2ODc1IDAuMDkyMi00LjE5NSAxLjk0ODgtNi41OTUgNC4yMDktNi41OTUgNi42MjAxIDAgNy4zMTg5IDIyLjExMiAxMy4yNTEgNDkuMzg5IDEzLjI1MSAyNy4yNzYgMCA0OS4zODktNS45MzI2IDQ5LjM4OS0xMy4yNTEgMC0yLjQxMTEtMi40LTQuNjcxNC02LjU5NS02LjYxODYgMC4wNTYzLTAuMDMxMiAwLjExMjUtMC4wNjI1IDAuMTY4NzUtMC4wOTM3IDQuMjg4OCAxLjk3MTEgNi43NDYyIDQuMjY0NiA2Ljc0NjIgNi43MTI0IDAgNy4zNjYyLTIyLjI1NSAxMy4zMzctNDkuNzA5IDEzLjMzNyIgZmlsbD0iI2M2YzRjMiIvPgogICA8cGF0aCBpZD0icGF0aDU4MjMiIGQ9Im0zMTkuODkgOTEzLjMzYy0yNy4yNzYgMC00OS4zODktNS45MzI2LTQ5LjM4OS0xMy4yNTEgMC0yLjQxMTEgMi40LTQuNjcxNCA2LjU5NS02LjYyMDEgMC4wNTYyIDAuMDMxMiAwLjExMjUgMC4wNjQgMC4xNjg3NSAwLjA5MzctNC4xIDEuOTIzOS02LjQ0MjUgNC4xNTE0LTYuNDQyNSA2LjUyNjQgMCA3LjI3MSAyMS45NjggMTMuMTY1IDQ5LjA2OCAxMy4xNjVzNDkuMDY4LTUuODk0IDQ5LjA2OC0xMy4xNjVjMC0yLjM3NS0yLjM0MjUtNC42MDI1LTYuNDQyNS02LjUyNjQgMC4wNTYzLTAuMDI5NyAwLjExMjUtMC4wNjI1IDAuMTY4NzUtMC4wOTIyIDQuMTk1IDEuOTQ3MiA2LjU5NSA0LjIwNzUgNi41OTUgNi42MTg2IDAgNy4zMTg5LTIyLjExMiAxMy4yNTEtNDkuMzg5IDEzLjI1MSIgZmlsbD0iI2M0YzNjMSIvPgogICA8cGF0aCBpZD0icGF0aDU4MjUiIGQ9Im0zMTkuODkgOTEzLjI1Yy0yNy4xIDAtNDkuMDY4LTUuODk0LTQ5LjA2OC0xMy4xNjUgMC0yLjM3NSAyLjM0MjUtNC42MDI1IDYuNDQyNS02LjUyNjQgMC4wNTYzIDAuMDMxMiAwLjExNSAwLjA2NCAwLjE3MTI1IDAuMDkzNy00LjAwNjIgMS44OTk5LTYuMjkzOCA0LjA5NTMtNi4yOTM4IDYuNDMyNiAwIDcuMjIzNiAyMS44MjUgMTMuMDc5IDQ4Ljc0OCAxMy4wNzkgMjYuOTIyIDAgNDguNzQ4LTUuODU1IDQ4Ljc0OC0xMy4wNzkgMC0yLjMzNzQtMi4yODc1LTQuNTMyOC02LjI5MzgtNi40MzI2IDAuMDU2My0wLjAyOTcgMC4xMTUtMC4wNjI1IDAuMTcxMjUtMC4wOTM3IDQuMSAxLjkyMzkgNi40NDI1IDQuMTUxNCA2LjQ0MjUgNi41MjY0IDAgNy4yNzEtMjEuOTY4IDEzLjE2NS00OS4wNjggMTMuMTY1IiBmaWxsPSIjYzNjMmMwIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTgyNyIgZD0ibTMxOS44OSA5MTMuMTZjLTI2LjkyMiAwLTQ4Ljc0OC01Ljg1NS00OC43NDgtMTMuMDc5IDAtMi4zMzc0IDIuMjg3NS00LjUzMjggNi4yOTM4LTYuNDMyNiAwLjA1NjMgMC4wMzEyIDAuMTE1IDAuMDY0IDAuMTcxMjUgMC4wOTM3LTMuOTEyNSAxLjg3NS02LjE0MzggNC4wMzc2LTYuMTQzOCA2LjMzODkgMCA3LjE3NjIgMjEuNjgxIDEyLjk5MyA0OC40MjYgMTIuOTkzczQ4LjQyNi01LjgxNjQgNDguNDI2LTEyLjk5M2MwLTIuMzAxMi0yLjIzMTItNC40NjM5LTYuMTQzOC02LjMzODkgMC4wNTYzLTAuMDI5NyAwLjExNS0wLjA2MjUgMC4xNzEyNS0wLjA5MzcgNC4wMDYyIDEuODk5OSA2LjI5MzggNC4wOTUzIDYuMjkzOCA2LjQzMjYgMCA3LjIyMzYtMjEuODI1IDEzLjA3OS00OC43NDggMTMuMDc5IiBmaWxsPSIjYzJjMWJmIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTgyOSIgZD0ibTMxOS44OSA5MTMuMDdjLTI2Ljc0NSAwLTQ4LjQyNi01LjgxNjQtNDguNDI2LTEyLjk5MyAwLTIuMzAxMiAyLjIzMTItNC40NjM5IDYuMTQzOC02LjMzODkgMC4wNTg3IDAuMDMyOCAwLjExMzc1IDAuMDYyNSAwLjE3MjUgMC4wOTM3LTMuODIxMiAxLjg1MTYtNS45OTYyIDMuOTgtNS45OTYyIDYuMjQ1MSAwIDcuMTI4OSAyMS41MzkgMTIuOTA2IDQ4LjEwNiAxMi45MDYgMjYuNTY4IDAgNDguMTA1LTUuNzc3NCA0OC4xMDUtMTIuOTA2IDAtMi4yNjUxLTIuMTc1LTQuMzkzNS01Ljk5NS02LjI0MzYgMC4wNTg4LTAuMDMyOCAwLjExMzc1LTAuMDYyNSAwLjE3MjUtMC4wOTUzIDMuOTEyNSAxLjg3NSA2LjE0MzggNC4wMzc2IDYuMTQzOCA2LjMzODkgMCA3LjE3NjItMjEuNjgxIDEyLjk5My00OC40MjYgMTIuOTkzIiBmaWxsPSIjYzFjMGJlIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTgzMSIgZD0ibTMxOS44OSA5MTIuOTljLTI2LjU2OCAwLTQ4LjEwNi01Ljc3NzQtNDguMTA2LTEyLjkwNiAwLTIuMjY1MSAyLjE3NS00LjM5MzUgNS45OTYyLTYuMjQ1MSAwLjA1NjMgMC4wMzEyIDAuMTE2MjUgMC4wNjQgMC4xNzI1IDAuMDk1My0zLjcyNzUgMS44MjQ2LTUuODQ3NSAzLjkyMTQtNS44NDc1IDYuMTQ5OSAwIDcuMDgxIDIxLjM5NCAxMi44MiA0Ny43ODUgMTIuODJzNDcuNzg1LTUuNzM4OCA0Ny43ODUtMTIuODJjMC0yLjIyODUtMi4xMi00LjMyNTItNS44NDc1LTYuMTQ5OSAwLjA1NjMtMC4wMzEyIDAuMTE2MjUtMC4wNjQgMC4xNzI1LTAuMDkzNyAzLjgyIDEuODUwMSA1Ljk5NSAzLjk3ODUgNS45OTUgNi4yNDM2IDAgNy4xMjg5LTIxLjUzOCAxMi45MDYtNDguMTA1IDEyLjkwNiIgZmlsbD0iI2MwYmZiZCIvPgogICA8cGF0aCBpZD0icGF0aDU4MzMiIGQ9Im0zMTkuODkgOTEyLjljLTI2LjM5MSAwLTQ3Ljc4NS01LjczODgtNDcuNzg1LTEyLjgyIDAtMi4yMjg1IDIuMTItNC4zMjUyIDUuODQ3NS02LjE0OTkgMC4wNTg4IDAuMDMxMiAwLjExNjI1IDAuMDYxIDAuMTc1IDAuMDkzNy0zLjYzNjIgMS44MDEyLTUuNzAxMiAzLjg2MzgtNS43MDEyIDYuMDU2MSAwIDcuMDMyOCAyMS4yNSAxMi43MzUgNDcuNDY0IDEyLjczNXM0Ny40NjQtNS43MDIxIDQ3LjQ2NC0xMi43MzVjMC0yLjE5MjQtMi4wNjUtNC4yNTQ5LTUuNzAxMi02LjA1NjEgMC4wNTg4LTAuMDMxMiAwLjExNjI1LTAuMDYyNSAwLjE3NS0wLjA5MzcgMy43Mjc1IDEuODI0NiA1Ljg0NzUgMy45MjE0IDUuODQ3NSA2LjE0OTkgMCA3LjA4MS0yMS4zOTQgMTIuODItNDcuNzg1IDEyLjgyIiBmaWxsPSIjYmZiZWJjIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTgzNSIgZD0ibTMxOS44OSA5MTIuODFjLTI2LjIxNCAwLTQ3LjQ2NC01LjcwMjEtNDcuNDY0LTEyLjczNSAwLTIuMTkyNCAyLjA2NS00LjI1NDkgNS43MDEyLTYuMDU2MSAwLjA1ODcgMC4wMzEyIDAuMTE2MjUgMC4wNjI1IDAuMTc1IDAuMDkzNy0zLjU0NSAxLjc3NTktNS41NTYyIDMuODA2MS01LjU1NjIgNS45NjI0IDAgNi45ODQ5IDIxLjEwOCAxMi42NDkgNDcuMTQ0IDEyLjY0OXM0Ny4xNDItNS42NjQgNDcuMTQyLTEyLjY0OWMwLTIuMTU2Mi0yLjAxLTQuMTg2NS01LjU1NS01Ljk2MjQgMC4wNTg3LTAuMDMxMiAwLjExNjI1LTAuMDYyNSAwLjE3NS0wLjA5MzcgMy42MzYyIDEuODAxMiA1LjcwMTIgMy44NjM4IDUuNzAxMiA2LjA1NjEgMCA3LjAzMjgtMjEuMjUgMTIuNzM1LTQ3LjQ2NCAxMi43MzUiIGZpbGw9IiNiZWJkYmIiLz4KICAgPHBhdGggaWQ9InBhdGg1ODM3IiBkPSJtMzE5Ljg5IDkxMi43M2MtMjYuMDM2IDAtNDcuMTQ0LTUuNjY0LTQ3LjE0NC0xMi42NDkgMC0yLjE1NjIgMi4wMTEyLTQuMTg2NSA1LjU1NjItNS45NjI0IDAuMDYgMC4wMzEyIDAuMTE3NSAwLjA2MjUgMC4xNzc1IDAuMDkzNy0zLjQ1NjIgMS43NTI0LTUuNDEyNSAzLjc0ODUtNS40MTI1IDUuODY4NiAwIDYuOTM3NSAyMC45NjIgMTIuNTYyIDQ2LjgyMiAxMi41NjIgMjUuODU5IDAgNDYuODIyLTUuNjI1IDQ2LjgyMi0xMi41NjIgMC0yLjEyMDEtMS45NTYyLTQuMTE2Mi01LjQxMjUtNS44Njc2IDAuMDYtMC4wMzIzIDAuMTE3NS0wLjA2MzUgMC4xNzc1LTAuMDk0OCAzLjU0NSAxLjc3NTkgNS41NTUgMy44MDYxIDUuNTU1IDUuOTYyNCAwIDYuOTg0OS0yMS4xMDYgMTIuNjQ5LTQ3LjE0MiAxMi42NDkiIGZpbGw9IiNiZGJjYmEiLz4KICAgPHBhdGggaWQ9InBhdGg1ODM5IiBkPSJtMzE5Ljg5IDkxMi42NGMtMjUuODYgMC00Ni44MjItNS42MjUtNDYuODIyLTEyLjU2MiAwLTIuMTIwMSAxLjk1NjItNC4xMTYyIDUuNDEyNS01Ljg2ODYgMC4wNTg3IDAuMDMyMyAwLjExODc1IDAuMDYzNSAwLjE3NzUgMC4wOTQ4LTMuMzY2MiAxLjcyNjUtNS4yNyAzLjY4OTktNS4yNyA1Ljc3MzkgMCA2Ljg5MDEgMjAuODIgMTIuNDc2IDQ2LjUwMiAxMi40NzZzNDYuNTAxLTUuNTg2IDQ2LjUwMS0xMi40NzZjMC0yLjA4NC0xLjkwMjUtNC4wNDc0LTUuMjY4OC01Ljc3MzkgMC4wNTg4LTAuMDMxMiAwLjExODc1LTAuMDYyNSAwLjE3NzUtMC4wOTM3IDMuNDU2MiAxLjc1MTQgNS40MTI1IDMuNzQ3NSA1LjQxMjUgNS44Njc2IDAgNi45Mzc1LTIwLjk2NCAxMi41NjItNDYuODIyIDEyLjU2MiIgZmlsbD0iI2JjYmJiOSIvPgogICA8cGF0aCBpZD0icGF0aDU4NDEiIGQ9Im0zMTkuODkgOTEyLjU2Yy0yNS42ODIgMC00Ni41MDItNS41ODYtNDYuNTAyLTEyLjQ3NiAwLTIuMDg0IDEuOTAzOC00LjA0NzQgNS4yNy01Ljc3MzkgMC4wNTg3IDAuMDMxMiAwLjEyIDAuMDYyNSAwLjE3ODc1IDAuMDkzNy0zLjI3NzUgMS43MDExLTUuMTI3NSAzLjYzMjgtNS4xMjc1IDUuNjgwMSAwIDYuODQyMiAyMC42NzYgMTIuMzkgNDYuMTgxIDEyLjM5czQ2LjE4MS01LjU0NzkgNDYuMTgxLTEyLjM5YzAtMi4wNDY0LTEuODUxMi0zLjk3OS01LjEyNzUtNS42ODAxIDAuMDU4OC0wLjAzMTIgMC4xMi0wLjA2MjUgMC4xNzg3NS0wLjA5MzcgMy4zNjYyIDEuNzI2NSA1LjI2ODggMy42ODk5IDUuMjY4OCA1Ljc3MzkgMCA2Ljg5MDEtMjAuODE5IDEyLjQ3Ni00Ni41MDEgMTIuNDc2IiBmaWxsPSIjYmJiYWI4Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTg0MyIgZD0ibTMxOS44OSA5MTIuNDdjLTI1LjUwNSAwLTQ2LjE4MS01LjU0NzktNDYuMTgxLTEyLjM5IDAtMi4wNDc0IDEuODUtMy45NzkgNS4xMjc1LTUuNjgwMSAwLjA1ODcgMC4wMzEyIDAuMTIgMC4wNjM5IDAuMTggMC4wOTUxLTMuMTg4OCAxLjY3NjItNC45ODYyIDMuNTczOC00Ljk4NjIgNS41ODUgMCA2Ljc5NDkgMjAuNTMyIDEyLjMwNCA0NS44NiAxMi4zMDRzNDUuODYtNS41MDg4IDQ1Ljg2LTEyLjMwNGMwLTIuMDA5OC0xLjc5ODgtMy45MDg4LTQuOTg2Mi01LjU4NSAwLjA2LTAuMDMxMiAwLjEyMTI1LTAuMDYzOSAwLjE4LTAuMDk1MSAzLjI3NjIgMS43MDExIDUuMTI3NSAzLjYzMzcgNS4xMjc1IDUuNjgwMSAwIDYuODQyMi0yMC42NzYgMTIuMzktNDYuMTgxIDEyLjM5IiBmaWxsPSIjYmFiOWI3Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTg0NSIgZD0ibTMxOS44OSA5MTIuMzhjLTI1LjMyOCAwLTQ1Ljg2LTUuNTA4OC00NS44Ni0xMi4zMDQgMC0yLjAxMTIgMS43OTc1LTMuOTA4OCA0Ljk4NjItNS41ODUgMC4wNjEyIDAuMDMyNyAwLjExODc1IDAuMDYyNSAwLjE4MTI1IDAuMDkzNy0zLjEwMTIgMS42NTE0LTQuODQ3NSAzLjUxNjEtNC44NDc1IDUuNDkxMiAwIDYuNzQ3NSAyMC4zODkgMTIuMjE5IDQ1LjU0IDEyLjIxOSAyNS4xNSAwIDQ1LjUzOS01LjQ3MTIgNDUuNTM5LTEyLjIxOSAwLTEuOTc1MS0xLjc0NS0zLjgzOTktNC44NDYyLTUuNDkxMiAwLjA2MjUtMC4wMzEyIDAuMTItMC4wNjEgMC4xODEyNS0wLjA5MzcgMy4xODc1IDEuNjc2MiA0Ljk4NjIgMy41NzUyIDQuOTg2MiA1LjU4NSAwIDYuNzk0OS0yMC41MzIgMTIuMzA0LTQ1Ljg2IDEyLjMwNCIgZmlsbD0iI2I5YjhiNiIvPgogICA8cGF0aCBpZD0icGF0aDU4NDciIGQ9Im0zMTkuODkgOTEyLjNjLTI1LjE1MSAwLTQ1LjU0LTUuNDcxMi00NS41NC0xMi4yMTkgMC0xLjk3NTEgMS43NDYyLTMuODM5OSA0Ljg0NzUtNS40OTEyIDAuMDU4OCAwLjAzMTIgMC4xMjI1IDAuMDYzNSAwLjE4MjUgMC4wOTUyLTMuMDEzOCAxLjYyNDUtNC43MDg4IDMuNDU3LTQuNzA4OCA1LjM5NiAwIDYuNzAwMiAyMC4yNDUgMTIuMTMyIDQ1LjIxOSAxMi4xMzJzNDUuMjE5LTUuNDMyMSA0NS4yMTktMTIuMTMyYzAtMS45MzktMS42OTUtMy43NzE1LTQuNzA4OC01LjM5NiAwLjA2LTAuMDMxNyAwLjEyMzc1LTAuMDY0IDAuMTgyNS0wLjA5NTIgMy4xMDEyIDEuNjUxNCA0Ljg0NjIgMy41MTYxIDQuODQ2MiA1LjQ5MTIgMCA2Ljc0NzUtMjAuMzg5IDEyLjIxOS00NS41MzkgMTIuMjE5IiBmaWxsPSIjYjdiN2I1Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTg0OSIgZD0ibTMxOS44OSA5MTIuMjFjLTI0Ljk3NCAwLTQ1LjIxOS01LjQzMjEtNDUuMjE5LTEyLjEzMiAwLTEuOTM5IDEuNjk1LTMuNzcxNSA0LjcwODgtNS4zOTYgMC4wNjEzIDAuMDMyMyAwLjEyMTI1IDAuMDYyIDAuMTgzNzUgMC4wOTQ4LTIuOTI3NSAxLjU5ODYtNC41NzI1IDMuMzk4OS00LjU3MjUgNS4zMDEyIDAgNi42NTI0IDIwLjEwMiAxMi4wNDYgNDQuODk5IDEyLjA0NiAyNC43OTYgMCA0NC44OTgtNS4zOTQgNDQuODk4LTEyLjA0NiAwLTEuOTAyNC0xLjY0MzgtMy43MDI2LTQuNTcxMi01LjMwMTIgMC4wNjI1LTAuMDMyOCAwLjEyMjUtMC4wNjI1IDAuMTgzNzUtMC4wOTQ4IDMuMDEzOCAxLjYyNDUgNC43MDg4IDMuNDU3IDQuNzA4OCA1LjM5NiAwIDYuNzAwMi0yMC4yNDUgMTIuMTMyLTQ1LjIxOSAxMi4xMzIiIGZpbGw9IiNiNmI2YjQiLz4KICAgPHBhdGggaWQ9InBhdGg1ODUxIiBkPSJtMzE5Ljg5IDkxMi4xM2MtMjQuNzk2IDAtNDQuODk5LTUuMzk0LTQ0Ljg5OS0xMi4wNDYgMC0xLjkwMjQgMS42NDUtMy43MDI2IDQuNTcyNS01LjMwMTIgMC4wNjEyIDAuMDMxMiAwLjEyMjUgMC4wNjI1IDAuMTg1IDAuMDkzNy0yLjg0MjUgMS41NzUxLTQuNDM2MiAzLjM0MTItNC40MzYyIDUuMjA3NSAwIDYuNjA1IDE5Ljk1OCAxMS45NiA0NC41NzggMTEuOTYgMjQuNjE5IDAgNDQuNTc4LTUuMzU1IDQ0LjU3OC0xMS45NiAwLTEuODY2Mi0xLjU5NS0zLjYzMjQtNC40MzYyLTUuMjA3NSAwLjA2MjUtMC4wMzEyIDAuMTIzNzUtMC4wNjI1IDAuMTg1LTAuMDkzNyAyLjkyNzUgMS41OTg2IDQuNTcxMiAzLjM5ODkgNC41NzEyIDUuMzAxMiAwIDYuNjUyNC0yMC4xMDEgMTIuMDQ2LTQ0Ljg5OCAxMi4wNDYiIGZpbGw9IiNiNWI0YjMiLz4KICAgPHBhdGggaWQ9InBhdGg1ODUzIiBkPSJtMzE5Ljg5IDkxMi4wNGMtMjQuNjIgMC00NC41NzgtNS4zNTUtNDQuNTc4LTExLjk2IDAtMS44NjYyIDEuNTkzOC0zLjYzMjQgNC40MzYyLTUuMjA3NSAwLjA1ODggMC4wMzEyIDAuMTI2MjUgMC4wNjQ5IDAuMTg1IDAuMDk1Mi0yLjc1NjIgMS41NDgyLTQuMyAzLjI4MjEtNC4zIDUuMTEyMiAwIDYuNTU3NiAxOS44MTQgMTEuODc0IDQ0LjI1NiAxMS44NzRzNDQuMjU2LTUuMzE1OSA0NC4yNTYtMTEuODc0YzAtMS44MzAxLTEuNTQzOC0zLjU2NC00LjMtNS4xMTIyIDAuMDYxMy0wLjAzMTIgMC4xMjM3NS0wLjA2MjUgMC4xODUtMC4wOTUyIDIuODQxMiAxLjU3NTEgNC40MzYyIDMuMzQxMiA0LjQzNjIgNS4yMDc1IDAgNi42MDUtMTkuOTU5IDExLjk2LTQ0LjU3OCAxMS45NiIgZmlsbD0iI2I0YjNiMiIvPgogICA8cGF0aCBpZD0icGF0aDU4NTUiIGQ9Im0zMTkuODkgOTExLjk1Yy0yNC40NDIgMC00NC4yNTYtNS4zMTU5LTQ0LjI1Ni0xMS44NzQgMC0xLjgzMDEgMS41NDM4LTMuNTY0IDQuMy01LjExMjIgMC4wNjUgMC4wMzM2IDAuMTIyNSAwLjA2MjUgMC4xODc1IDAuMDk0Ni0yLjY3MjUgMS41MjI1LTQuMTY3NSAzLjIyMzYtNC4xNjc1IDUuMDE3NiAwIDYuNTA5OCAxOS42NzEgMTEuNzg4IDQzLjkzNiAxMS43ODhzNDMuOTM1LTUuMjc3OCA0My45MzUtMTEuNzg4YzAtMS43OTQtMS40OTM4LTMuNDk1MS00LjE2NjItNS4wMTc2IDAuMDYyNS0wLjAzMTIgMC4xMjUtMC4wNjI1IDAuMTg3NS0wLjA5NDYgMi43NTYyIDEuNTQ4MiA0LjMgMy4yODIxIDQuMyA1LjExMjIgMCA2LjU1NzYtMTkuODE0IDExLjg3NC00NC4yNTYgMTEuODc0IiBmaWxsPSIjYjNiMmIxIi8+CiAgIDxwYXRoIGlkPSJwYXRoNTg1NyIgZD0ibTMxOS44OSA5MTEuODdjLTI0LjI2NSAwLTQzLjkzNi01LjI3NzgtNDMuOTM2LTExLjc4OCAwLTEuNzk0IDEuNDk1LTMuNDk1MSA0LjE2NzUtNS4wMTc2IDAuMDYyNSAwLjAzMTIgMC4xMjYyNSAwLjA2NCAwLjE4ODc1IDAuMDk1My0yLjU5IDEuNDk2MS00LjAzNSAzLjE2MzYtNC4wMzUgNC45MjI0IDAgNi40NjI0IDE5LjUyOCAxMS43MDEgNDMuNjE1IDExLjcwMSAyNC4wODggMCA0My42MTUtNS4yMzg4IDQzLjYxNS0xMS43MDEgMC0xLjc1ODgtMS40NDUtMy40MjYyLTQuMDM1LTQuOTIyNCAwLjA2MjUtMC4wMzEyIDAuMTI2MjUtMC4wNjQgMC4xODg3NS0wLjA5NTMgMi42NzI1IDEuNTIyNSA0LjE2NjIgMy4yMjM2IDQuMTY2MiA1LjAxNzYgMCA2LjUwOTgtMTkuNjcgMTEuNzg4LTQzLjkzNSAxMS43ODgiIGZpbGw9IiNiMWIxYjAiLz4KICAgPHBhdGggaWQ9InBhdGg1ODU5IiBkPSJtMzE5Ljg5IDkxMS43OGMtMjQuMDg4IDAtNDMuNjE1LTUuMjM4OC00My42MTUtMTEuNzAxIDAtMS43NTg4IDEuNDQ1LTMuNDI2MiA0LjAzNS00LjkyMjQgMC4wNjI1IDAuMDMxMiAwLjEyNzUgMC4wNjM1IDAuMTkgMC4wOTQ3LTIuNTA3NSAxLjQ3MDItMy45MDM4IDMuMTA1LTMuOTAzOCA0LjgyNzYgMCA2LjQxNSAxOS4zODIgMTEuNjE2IDQzLjI5NCAxMS42MTYgMjMuOTEgMCA0My4yOTQtNS4yMDEyIDQzLjI5NC0xMS42MTYgMC0xLjcyMjYtMS4zOTYyLTMuMzU3NC0zLjkwMzgtNC44Mjc2IDAuMDYyNS0wLjAzMTIgMC4xMjc1LTAuMDYzNSAwLjE5LTAuMDk0NyAyLjU5IDEuNDk2MSA0LjAzNSAzLjE2MzYgNC4wMzUgNC45MjI0IDAgNi40NjI0LTE5LjUyOCAxMS43MDEtNDMuNjE1IDExLjcwMSIgZmlsbD0iI2IwYjBhZiIvPgogICA8cGF0aCBpZD0icGF0aDU4NjEiIGQ9Im0zMTkuODkgOTExLjdjLTIzLjkxMSAwLTQzLjI5NC01LjIwMTItNDMuMjk0LTExLjYxNiAwLTEuNzIyNiAxLjM5NjItMy4zNTc0IDMuOTAzOC00LjgyNzYgMC4wNjUgMC4wMzI3IDAuMTI2MjUgMC4wNjI1IDAuMTkxMjUgMC4wOTUyLTIuNDI1IDEuNDQzOC0zLjc3NSAzLjA0NTktMy43NzUgNC43MzI0IDAgNi4zNjc2IDE5LjI0IDExLjUzIDQyLjk3NCAxMS41M3M0Mi45NzQtNS4xNjIyIDQyLjk3NC0xMS41M2MwLTEuNjg2NS0xLjM1LTMuMjg4Ni0zLjc3NS00LjczMjQgMC4wNjUtMC4wMzEyIDAuMTI2MjUtMC4wNjI1IDAuMTkxMjUtMC4wOTUyIDIuNTA3NSAxLjQ3MDIgMy45MDM4IDMuMTA1IDMuOTAzOCA0LjgyNzYgMCA2LjQxNS0xOS4zODQgMTEuNjE2LTQzLjI5NCAxMS42MTYiIGZpbGw9IiNhZmFmYWQiLz4KICAgPHBhdGggaWQ9InBhdGg1ODYzIiBkPSJtMzE5Ljg5IDkxMS42MWMtMjMuNzM0IDAtNDIuOTc0LTUuMTYyMi00Mi45NzQtMTEuNTMgMC0xLjY4NjUgMS4zNS0zLjI4ODYgMy43NzUtNC43MzI0IDAuMDYyNSAwLjAzMTIgMC4xMyAwLjA2NDkgMC4xOTI1IDAuMDk2MS0yLjM0MzggMS40MTYtMy42NDYyIDIuOTg2NC0zLjY0NjIgNC42MzYyIDAgNi4zMTk5IDE5LjA5NiAxMS40NDQgNDIuNjUyIDExLjQ0NHM0Mi42NTItNS4xMjQgNDIuNjUyLTExLjQ0NGMwLTEuNjQ5OS0xLjMwMjUtMy4yMTg4LTMuNjQ2Mi00LjYzNjIgMC4wNjI1LTAuMDMxMiAwLjEzLTAuMDY0OSAwLjE5MjUtMC4wOTYxIDIuNDI1IDEuNDQzOCAzLjc3NSAzLjA0NTkgMy43NzUgNC43MzI0IDAgNi4zNjc2LTE5LjI0IDExLjUzLTQyLjk3NCAxMS41MyIgZmlsbD0iI2FlYWVhYyIvPgogICA8cGF0aCBpZD0icGF0aDU4NjUiIGQ9Im0zMTkuODkgOTExLjUyYy0yMy41NTYgMC00Mi42NTItNS4xMjQtNDIuNjUyLTExLjQ0NCAwLTEuNjQ5OSAxLjMwMjUtMy4yMjAyIDMuNjQ2Mi00LjYzNjIgMC4wNjYyIDAuMDMxMiAwLjEyODc1IDAuMDYyNSAwLjE5NSAwLjA5NDctMi4yNjUgMS4zOTE2LTMuNTIxMiAyLjkyNjItMy41MjEyIDQuNTQxNSAwIDYuMjcyNSAxOC45NTIgMTEuMzU3IDQyLjMzMiAxMS4zNTcgMjMuMzc5IDAgNDIuMzMxLTUuMDg0OSA0Mi4zMzEtMTEuMzU3IDAtMS42MTUyLTEuMjU1LTMuMTQ5OS0zLjUyLTQuNTQxNSAwLjA2NjMtMC4wMzIyIDAuMTI4NzUtMC4wNjM1IDAuMTk1LTAuMDk0NyAyLjM0MzggMS40MTc1IDMuNjQ2MiAyLjk4NjQgMy42NDYyIDQuNjM2MiAwIDYuMzE5OS0xOS4wOTYgMTEuNDQ0LTQyLjY1MiAxMS40NDQiIGZpbGw9IiNhZGFjYWIiLz4KICAgPHBhdGggaWQ9InBhdGg1ODY3IiBkPSJtMzE5Ljg5IDkxMS40NGMtMjMuMzggMC00Mi4zMzItNS4wODQ5LTQyLjMzMi0xMS4zNTcgMC0xLjYxNTIgMS4yNTYyLTMuMTQ5OSAzLjUyMTItNC41NDE1IDAuMDY1IDAuMDMxNyAwLjEzIDAuMDY0IDAuMTk1IDAuMDk1Mi0yLjE4NSAxLjM2NTItMy4zOTUgMi44Njc2LTMuMzk1IDQuNDQ2MiAwIDYuMjI1MSAxOC44MDkgMTEuMjcxIDQyLjAxMSAxMS4yNzEgMjMuMjAxIDAgNDIuMDExLTUuMDQ2NCA0Mi4wMTEtMTEuMjcxIDAtMS41Nzg2LTEuMjEtMy4wODEtMy4zOTUtNC40NDYyIDAuMDY1LTAuMDMxMiAwLjEzLTAuMDYzNSAwLjE5NS0wLjA5NTIgMi4yNjUgMS4zOTE2IDMuNTIgMi45MjYyIDMuNTIgNC41NDE1IDAgNi4yNzI1LTE4Ljk1MiAxMS4zNTctNDIuMzMxIDExLjM1NyIgZmlsbD0iI2FjYWJhYSIvPgogICA8cGF0aCBpZD0icGF0aDU4NjkiIGQ9Im0zMTkuODkgOTExLjM1Yy0yMy4yMDIgMC00Mi4wMTEtNS4wNDY0LTQyLjAxMS0xMS4yNzEgMC0xLjU3ODYgMS4yMS0zLjA4MSAzLjM5NS00LjQ0NjIgMC4wNjUgMC4wMzI2IDAuMTMxMjUgMC4wNjM5IDAuMTk3NSAwLjA5NjEtMi4xMDc1IDEuMzM3NC0zLjI3MTIgMi44MDc2LTMuMjcxMiA0LjM1MDEgMCA2LjE3NzIgMTguNjY1IDExLjE4NSA0MS42OSAxMS4xODVzNDEuNjktNS4wMDc4IDQxLjY5LTExLjE4NWMwLTEuNTQyNS0xLjE2MzgtMy4wMTI4LTMuMjcxMi00LjM1MDEgMC4wNjYyLTAuMDMyMiAwLjEzMjUtMC4wNjM1IDAuMTk3NS0wLjA5NjEgMi4xODUgMS4zNjUyIDMuMzk1IDIuODY3NiAzLjM5NSA0LjQ0NjIgMCA2LjIyNTEtMTguODEgMTEuMjcxLTQyLjAxMSAxMS4yNzEiIGZpbGw9IiNhYWFhYTkiLz4KICAgPHBhdGggaWQ9InBhdGg1ODcxIiBkPSJtMzE5Ljg5IDkxMS4yN2MtMjMuMDI1IDAtNDEuNjktNS4wMDc4LTQxLjY5LTExLjE4NSAwLTEuNTQyNSAxLjE2MzgtMy4wMTI4IDMuMjcxMi00LjM1MDEgMC4wNjUgMC4wMzEyIDAuMTMyNSAwLjA2NCAwLjE5NzUgMC4wOTUzLTIuMDI4OCAxLjMxMS0zLjE0ODggMi43NDc1LTMuMTQ4OCA0LjI1NDkgMCA2LjEyOTkgMTguNTIyIDExLjA5OSA0MS4zNyAxMS4wOTlzNDEuMzctNC45Njg4IDQxLjM3LTExLjA5OWMwLTEuNTA3NC0xLjEyLTIuOTQzOS0zLjE0ODgtNC4yNTQ5IDAuMDY1LTAuMDMxMiAwLjEzMjUtMC4wNjQgMC4xOTc1LTAuMDk1MyAyLjEwNzUgMS4zMzc0IDMuMjcxMiAyLjgwNzYgMy4yNzEyIDQuMzUwMSAwIDYuMTc3Mi0xOC42NjUgMTEuMTg1LTQxLjY5IDExLjE4NSIgZmlsbD0iI2E5YTlhOCIvPgogICA8cGF0aCBpZD0icGF0aDU4NzMiIGQ9Im0zMTkuODkgOTExLjE4Yy0yMi44NDggMC00MS4zNy00Ljk2ODgtNDEuMzctMTEuMDk5IDAtMS41MDc0IDEuMTItMi45NDM5IDMuMTQ4OC00LjI1NDkgMC4wNjg4IDAuMDMyMiAwLjEzMjUgMC4wNjM1IDAuMiAwLjA5NjEtMS45NTEyIDEuMjgyMi0zLjAyNzUgMi42ODc1LTMuMDI3NSA0LjE1ODggMCA2LjA4MjUgMTguMzc4IDExLjAxNCA0MS4wNDkgMTEuMDE0IDIyLjY3IDAgNDEuMDQ5LTQuOTMxMSA0MS4wNDktMTEuMDE0IDAtMS40NzEyLTEuMDc2Mi0yLjg3NS0zLjAyNzUtNC4xNTg4IDAuMDY3NS0wLjAzMjYgMC4xMzEyNS0wLjA2MzkgMC4yLTAuMDk2MSAyLjAyODggMS4zMTEgMy4xNDg4IDIuNzQ3NSAzLjE0ODggNC4yNTQ5IDAgNi4xMjk5LTE4LjUyMiAxMS4wOTktNDEuMzcgMTEuMDk5IiBmaWxsPSIjYThhOGE3Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTg3NSIgZD0ibTMxOS44OSA5MTEuMDljLTIyLjY3MSAwLTQxLjA0OS00LjkzMTEtNDEuMDQ5LTExLjAxNCAwLTEuNDcxMiAxLjA3NjItMi44NzY1IDMuMDI3NS00LjE1ODggMC4wNjYzIDAuMDMxMiAwLjEzNzUgMC4wNjUgMC4yMDI1IDAuMDk2My0xLjg3NSAxLjI1NDktMi45MDYyIDIuNjI1LTIuOTA2MiA0LjA1ODYgMCA2LjA0MTUgMTguMjM2IDEwLjkzNSA0MC43MjUgMTAuOTM1czQwLjcyNS00Ljg5MzUgNDAuNzI1LTEwLjkzNWMwLTEuNDMzNi0xLjAzMTItMi44MDM4LTIuOTA2Mi00LjA1ODYgMC4wNjYyLTAuMDMxMiAwLjEzNjI1LTAuMDY1IDAuMjAyNS0wLjA5NjMgMS45NTEyIDEuMjgzOCAzLjAyNzUgMi42ODc1IDMuMDI3NSA0LjE1ODggMCA2LjA4MjUtMTguMzc5IDExLjAxNC00MS4wNDkgMTEuMDE0IiBmaWxsPSIjYTdhNmE1Ii8+CiAgIDxwYXRoIGlkPSJwYXRoNTg3NyIgZD0ibTMyNi42MiA5MDAuMDhjMCAwLjk5NzUtMy4wMTI1IDEuODA1MS02LjcyNzUgMS44MDUxcy02LjcyNzUtMC44MDc2Mi02LjcyNzUtMS44MDUxYzAtMC45OTYxMiAzLjAxMjUtMS44MDUxIDYuNzI3NS0xLjgwNTFzNi43Mjc1IDAuODA5IDYuNzI3NSAxLjgwNTEiIGZpbGw9IiMxMDBmMGQiLz4KICA8L2c+CiAgPGcgaWQ9Imc1OTYxIiB0cmFuc2Zvcm09Im1hdHJpeCguMTI1IDAgMCAtLjEyNSAtNDk3LjcgOTIzLjgzKSI+CiAgIDxnIGlkPSJnNTk2MyIgY2xpcC1wYXRoPSJ1cmwoI2NsaXBQYXRoNTk2NSkiPgogICAgPHBhdGggaWQ9InBhdGg1OTc3IiBkPSJtNjc2OC45IDEzMjYuOGMwLTEwNS4yNi04Ni4xMy0xOTEuMzktMTkxLjM5LTE5MS4zOWgtNzMuNDRjLTEwNS4yNiAwLTE5MS4zOSA4Ni4xMy0xOTEuMzkgMTkxLjM5djQzMzUuMmMwIDEwNS4yNiA4Ni4xMyAxOTEuMzggMTkxLjM5IDE5MS4zOGg3My40NGMxMDUuMjYgMCAxOTEuMzktODYuMTIgMTkxLjM5LTE5MS4zOHYtNDMzNS4yIiBmaWxsPSJ1cmwoI2xpbmVhckdyYWRpZW50NTk2OSkiLz4KICAgPC9nPgogIDwvZz4KICA8ZyBpZD0iZzU5NzkiIHRyYW5zZm9ybT0ibWF0cml4KC4xMjUgMCAwIC0uMTI1IC00OTcuNyA5MjMuODMpIj4KICAgPGcgaWQ9Imc1OTgxIiBjbGlwLXBhdGg9InVybCgjY2xpcFBhdGg1OTgzKSI+CiAgICA8cGF0aCBpZD0icGF0aDU5OTMiIGQ9Im02MzEyLjYgMTQwMC43djQyNjEuMmMwIDEwNS4yNiA4Ni4xMyAxOTEuMzggMTkxLjM5IDE5MS4zOGg3My40NGMxMDUuMjYgMCAxOTEuMzktODYuMTIgMTkxLjM5LTE5MS4zOHYtNDI2MS4yYy03MC4wOCAyOS43Ni0xNDcuMTggNDYuMjItMjI4LjExIDQ2LjIycy0xNTguMDMtMTYuNDYtMjI4LjExLTQ2LjIybTEwNS4yMyA0MzQ0Yy0zMC40NyAwLTU1LjE2LTI0LjctNTUuMTYtNTUuMTZ2LTQxMDIuN2MwLTMwLjQ3IDI0LjY5LTU1LjE2IDU1LjE2LTU1LjE2IDMwLjQ2IDAgNTUuMTYgMjQuNjkgNTUuMTYgNTUuMTZ2NDEwMi43YzAgMzAuNDYtMjQuNyA1NS4xNi01NS4xNiA1NS4xNiIgZmlsbD0idXJsKCNsaW5lYXJHcmFkaWVudDU5ODcpIi8+CiAgIDwvZz4KICA8L2c+CiAgPGcgaWQ9Imc1OTk1IiB0cmFuc2Zvcm09Im1hdHJpeCguMTI1IDAgMCAtLjEyNSAtNDk3LjcgOTIzLjgzKSI+CiAgIDxnIGlkPSJnNTk5NyIgY2xpcC1wYXRoPSJ1cmwoI2NsaXBQYXRoNTk5OSkiPgogICAgPHBhdGggaWQ9InBhdGg2MDA5IiBkPSJtNzEyNC45IDg2Mi43NWMwLTMyMi42NC0yNjEuNTUtNTg0LjE4LTU4NC4xOC01ODQuMThzLTU4NC4xOCAyNjEuNTQtNTg0LjE4IDU4NC4xOGMwIDMyMi42MyAyNjEuNTUgNTg0LjE4IDU4NC4xOCA1ODQuMThzNTg0LjE4LTI2MS41NSA1ODQuMTgtNTg0LjE4IiBmaWxsPSJ1cmwoI3JhZGlhbEdyYWRpZW50NjAzNSkiLz4KICAgPC9nPgogIDwvZz4KICA8ZyBpZD0iZzYwMTEiIHRyYW5zZm9ybT0ibWF0cml4KC4xMjUgMCAwIC0uMTI1IC00OTcuNyA5MjMuODMpIj4KICAgPGcgaWQ9Imc2MDEzIiBjbGlwLXBhdGg9InVybCgjY2xpcFBhdGg2MDE1KSI+CiAgICA8cGF0aCBpZD0icGF0aDYwMjUiIGQ9Im02ODgzLjIgMTE3MS44djIuNDdjMC4wMS0wLjQxIDAuMDEtMC44MiAwLjAxLTEuMjNzMC0wLjgzLTAuMDEtMS4yNCIgZmlsbD0idXJsKCNyYWRpYWxHcmFkaWVudDYwMzUpIi8+CiAgIDwvZz4KICA8L2c+CiAgPGcgaWQ9Imc2MDI3IiB0cmFuc2Zvcm09Im1hdHJpeCguMTI1IDAgMCAtLjEyNSAtNDk3LjcgOTIzLjgzKSI+CiAgIDxnIGlkPSJnNjAyOSIgY2xpcC1wYXRoPSJ1cmwoI2NsaXBQYXRoNjAzMSkiPgogICAgPHBhdGggaWQ9InBhdGg2MDQxIiBkPSJtNjc2Ni44IDEwMDkuOWM3MS4zOCAzOS43OSAxMTYuNDIgOTguMTIgMTE2LjQyIDE2My4xMnYtMS4yNGMtMC41Ni02NC41LTQ1LjQ5LTEyMi4zNC0xMTYuNDItMTYxLjg4bTExNi40MiAxNjMuMTJjMCAzOC44OC0xNi4xMiA3NS4zOC00NC4zNSAxMDYuOTQgMjcuOTQtMzEuMjMgNDQuMDItNjcuMjkgNDQuMzUtMTA1Ljcxdi0xLjIzIiBmaWxsPSJ1cmwoI3JhZGlhbEdyYWRpZW50NjAzNSkiLz4KICAgPC9nPgogIDwvZz4KICA8ZyBpZD0iZzYwNDMiIHRyYW5zZm9ybT0ibWF0cml4KC4xMjUgMCAwIC0uMTI1IC00OTcuNyA5MjMuODMpIj4KICAgPGcgaWQ9Imc2MDQ1IiBjbGlwLXBhdGg9InVybCgjY2xpcFBhdGg2MDQ3KSI+CiAgICA8ZyBpZD0iZzYwNTEiIHRyYW5zZm9ybT0ibWF0cml4KDcwMS4yLDAsMCw0NTMuMiw2MTkxLjksOTQ1LjkpIj4KICAgICA8aW1hZ2UgaWQ9ImltYWdlNjA1MyIgeGxpbms6aHJlZj0iZGF0YTppbWFnZS9wbmc7YmFzZTY0LGlWQk9SdzBLR2dvQUFBQU5TVWhFVWdBQUFKSUFBQUJlQ0FZQUFBRElVVTFCQUFBQUJITkNTVlFJQ0FnSWZBaGtpQUFBRys5SlJFRlVlSnpsWGR1VzlDQ0xCYis4LytQT21qVi9SK1pDd1EyaVNhcXFEN09HaSs2cXhMTUlHMFNMLytlLy8wdm90K2tYVzhEeW1jcUYrU1BsdkVTL1dMWFNVYyt2YjYvazVUR3UzejlHdjcrSzdwRVFFWlhkeTAzZUgramtJYlYrYXdWTXp5Y0xtZWV0TVJEUGhwK1NQcythQUczZ04rcy9lNW12dE9POW1pL3BxUFg4YUlGUittUWQ0QXZleFR6dmp2MnkzRzhhV3ZrbUdTb1BpcFdWNUlycFBqZ0VoM3lRa1ZqMm5LOU10bTEvWUxLN2ZYM0tjSDlGcFQxaEVFYzdab0VwdldLV2wrc1A5QlpHV2trZkZpTFpTSjJydHIvU054MndUMHF3N3lTYndCYzZLMFNPV2RMM0MrSVNtQ2NrZmxWS3ZZNlJSSEsxeFl1T0FIQk84OEhETzMzaEJZSjNlUjkyN2JGVWU0WGpWWkpJK0kvbFBwak5xelpFa0s0TGZGbkZpNWJSY1Q2UVNGa2RFZThJYlNaazFjYUswbXpma1IwejNxVVZQdnFVTU52aXBBZElZdGNld1VGZXFEbGpsbEJuTnNRU0daeWVTYWZiR0NsYVg5d0hTMElhWkFwN0xsTWJ4N3NOT09kWHpQOGJuZjh6MnUraWMxRURPUkNOblRoZndFSWxqSU5aaEhOQmQ4YnJOaU1KZGVhcEF0L3pDdkU1MXl0VzJEZHpxKzgvNXJsNEYzRit5S2w1WlcxdCs3dnVnMm5STXRxWllkaXBGNFZUeHNyb3FBdU1GSnNWTFRKT0doT3RzcEpJb3FpNkpvbTJlRGZ5ajZjMWtkRy80U3Q2bGFJM1BMYjlqcmRja2s4dDd6d09mQkxGZFQwTkYwcXFjNVprcTlGZFNpU1RMdm82S2RCMVhIakNIaWxXY0RxNHZkKzVCYWJCemRKY3ZIZTBZUDUzS2VYZkt3a1RGdG9lRTIwa2pscXN3RHdwRGtyVkZodjJFT1laVDVsVTJBUDd5U0dwYlpsQWJjVTBIaC9wdDBvZDF4aGo2RWpONjhaTi9qbStSV3RzeGx2M2RIamE1enZpL0pPMEtQeHVuWjU1d29MQ2NlaWlYeVJMU2ROcUVVRTFLakE5eWxCUnVwR05uY1Rublk1NmVrWkNkWlJhWHpVeHZjV3ZCT3pqRWlNbERPRmtXbjlmYnVDWFYzMUhuOTVubmJxMHdEU1RDbCswQXg5WG5FTFdwU3RUUFN2SmhmaUlpSWpRV0o4WVRiekxZTkZPbkZxbjJncmdJQloyMG1NOGIzZzcrbjNpWkpwVWd0cm5QR3UxcGZrcmVWeVZNVTBtNWU3UVV5L0ZWYjZuZEZXT216aFVXd3NHYlV5VVNCTWk0L0xJN0JNRzB1K0FqMkk3eFZUaGVIWkl0OEpLZHpBNnZGS1pXSitEdVcvTkJZYUtrNWxhZFRMS3NUU0wwWng4UGVMcnp5akhXSXZFRy9vdWRYZlhnZW5xN3gzd1NtNHVTSmdtTGhFU2w5R3BwVlg3T0taRGJjTW1iYVFYWFR2REhGS2JqSk9LTERENllHSk5nWEd3M2dwdWJpYytKQ1NudW1vZUdhQXA0cUxMWENpVDFaalU2YVRjTHdUdTdNem5xZThKT0Y5Wi9Da3pFVTBBUDlZL1NSZElYNTJLeElhQkJES3MxQmQyVjVrSDFXck13WUtxU0NYUktGbWxFOUhBU1ZVQVYwbGNPWWs2czFiVEk5TGtidmp1MnFiMkdoYkpOL0xVYlEvRUJEWDNrMzZ2OG5YZWRMakVZNTJwRGR5c092K1VpYmlTOExEcUQvazZ5WHBrZXJUSmprbGlTR3NOUGkvdzJhUlZuM1hjZDV1ODIwRjZxV1M3VWsrcGFndFc1T2p1Zldud0U3VGNPb2xXVXBaM2tiWEd4RkVpT1NieGFmUWRDQnBYdnlCb3huYnBYQUhvUDZoVzQwd3ZRVGhsQXBhVzNsbDNoSitCSTFCVVVzSUFXTFpNNDduTXV3VFhVY3I5dkNiYlU5WS9XSWtwQSsySzR6QXVnU09tdkIwRHRVVXNMbDBzQjMxS2pobk5SUUJsRW9TUnpHYTZkSFhXS2lyaHRYRTMyUHZPdXV1ZmtVSENnaGlkVU41RFgxVm9UYllrWndtMGY2OFVMZUZQMCtXdUVOQXUzSWFJOXRKS0pVdm9UeXhTT0hmNnVzK3F3dnJFYVJkVTRqVWdMZ01iWWQ0aWROQlo3WVdDWlZWSmhwMXFhMXhCWUNGUUExWVdTY3NNSFkzcXFrUTlHZ1puS1lVeXVzSkszOHhJVEd0VjVCSmRVTnJNSUpvbjFSYlNERFdWbE9td1JmOENjMXdWNmpqODRzZFBDaEdkUk1mNTlSOWlZZys0YVVpVVFtUFY2RXBEeTQySnFZbzRYMDhKSmlTTERLc3Y5R0cyRmNtWUw5dGgySXA2cTIrZitEZndrWkhoaStkWm5hU1pyTE9aQmtvUnNNcDdXYmI0aFppWkJEalMvRWVnUXJSdWlXbjZpNE9xT0JaenpDUnQyOE84eStBbWQzNHVUYS9KSkZFZnFkazFXallpSE1IUHRNU215YkJodnY3L1V5b3NjMU84U3BQYVM0RGhjbDhOazU2TkNXdzhkNzZQd0hRMU1JcjZqeWJTZEdjcnpCOWt3UFlTSGZTZkwrcTgwQ1FUQjZ6U21ZbG9BR21VV3ZwZDIxMUFoK3FFRzVOaEFGdnNySDRWNy93TWJmYlBrOW5OSWkxZmNVcE84eDNLZkVLU2pIKzZxWHFEZjlwM1hYWHdNTE42UzJjV0ZRQTh0QUp1dVhnd1BiNHpDMGd1bUpNQ1ROakxPWVNxaSt0cERLQVZpbmZpaWFTbXVCQ3MvbzA2c1hJbG1XMmp3TWtiR295U1NLTmJKVnlYL1FuQ01WczZHSjl3T3lkc0RhYThUZm9HRERxNEc2RUlQQnRWS1pabUt4ZW4vS0N2MDZTQVNwSEJJQzNUYkdhemhTdzQwLzlrYjRWUklnME1mRWVwQXl0RXZBODY0cTJVd21PY3NML2dCVmhPNmFaeG1Zb1Rvc1NTdzRYVTM5M3hKeEdSL1BQK1FzV3lMZ2lPaUloYmtOdkFTRHAvVGIwZUVqemJMck1NaVlQK0phTGF6WHJ1R01xM1VIR1Q2L09UQlFmNXAzYzNuanl0YjZyN0pyMFUvTC9RMDVuek5Hc0s4ODE2QXliMVd4N3c3QXhTa210TGNIcHBKMW5iOVIwVEhYU2VReEtaUEZOcHd5VEpuaGdENkM3UU1zRU8wUEMxWlhnbzRoK3RkZzcrajZ2UDA1VksvYzdqU1kvNGFKVllEWlR3UG0vMndEaWlQcDlkbmdTYjJmUVVmQ2Z1WFhRVktDNHlYQldsRm5OakpFM2NKRk5qcGlKUVZKUXVSQ0dBRGRNTUhHVE1ncXJPT1IxSFQ3TURBamNPU21SSWRDcjdEcjE2OHZidXlkcXhZQktnVERUdC9VMFRyL2tEYzB6ZktSODBDWlBoOEpQaUt5SHpDMDF0RkdpVHFveVREZHNkOG5YYUJxeUlVQUdScUFCWnVwOEl3YldaL0xIek10N2hDUmNuZVNwWWRpVHduQnhOSVNlVTBEM0lkRW5mS0xqdTBjclZFYjY3clEwWGZPWlRTcEhKU3JiaEJYTS9DdlMyY1IvR293eGp3SVdQbUdjZDk5cWdzdElMWTJGd25jTC9hQzlpaDN0RlRucUZkMHZxRVFiNDNiMG12M0t6c3RUZ2UySzF2UXZHbnpDaHJkUFF3QmllczdJcDFNZ1J4cmxaVk1RZWxoUUtVNWZncHRxM0hjelk2bVV4RWRWL0FXWkFjTzBoNTlsVUdaVG45c2VxMzJjekZkZ1pUU2ZYVFViWUZuSFNaQlZEVkRrOTQrWlVqbmdGSkVSRGdvYnlJZ2JiMFU5SW82djIxQlhuSytPRW5DYjlOVjQ3NWt1MlJLS1VjZGhYeURrNE1US0FxR000eGNZbHFGNG1PdVNzZnA4ck9aUW9aM3pHdlg5Q0JaZE9EYlZyYWhubEkxT2g1eG1qUklkVW1RVTdTaXpQdktHRkQ4VE1Kd0Q1bFNYbDRBYlcxd2VobkdIUzQxRWxFdWRFWkdXRUxoVUtnY2VheUlmeXdMU0kxbisydW0zaGNkOG00NkEraWZ4ZVd5OWIxU3B6azJLSGhzV3BwRUhzMC80elNKaFdNZGY1NkpIbWNLWjdNa0Z1MjBMQ1IyQ0drbVFlanRLTVF2cWJnWFB2cXJXWENDczk1OGNPT3dLWEZ5S3FhaUZyUGhoUUJvYXBDbElGR0Nnd1ZCc2pDVTFpb3VKQ0FMdjZjZkpyRXJFSGY1MURTb0RlTmFrSmpEV3dEaHZvY2t5bnpzeGt3dEZsZ0cyWkxEUGN6NE5WaWY2b3Jab1E5eStsZDczZUdjVzIzT0xqUkpYSGQyaVpUYzdoc2c2UFphSzJIMWZHZDErK1RGSlBla0x1T01uN2wwSWRaWGk0bVlnT0VvRDJIWU1VY0lFekVaV0svQW9jRE1jTW1NUWtDbWUyUEpINXBFSi8zUC9RN3IwWnY0dGZXdEN0azdpWGV1cTZqTlZ0S1ZOVitHVnh0cjh4UVJLeFdqMkFqallRb3lsUDVEYlZpWWk0MUlHOXVNM2JHSjR4OGhncE9hUloyOTNRS1RqcWVUckpFNDhrTVluYnNqREpnOXNZbWhjK3UxQVRORU54MVFSSmljd1l0YU9OTWVoK3BORm0ydFAzM25RNDZJYllpL3lxOGM5aWc5a29iWElaUVdhV0h4a0p4dCs1Q2RRdzZ1QVpWVjRyeEVzcUlTS3VNbFNxT3F0NUFPM0tSQWQvaldhYWl0TEo2czRrdzBNT1dMY1VhTzFOb2JVZ21HSkl4eVFaTmpGSU5nSmFENWF6U2ovbGQvK1c5TEV0RWp4M3Vram5BSGg0N2lJd2trYlpjVVRub1I0Z1BRUDMyWTBrQ2xrcWRaQ04xaHlQK1B3bWZRQ0VGMWRNOTJ6RElKdEtBZEFzVVFyVU9XYmJxa2gyK0l0ais2RTJHNU42M3hHMlFjbXB0K1ErcGl0eVBxM3RqdmhENkwwcHk2M3FWeEI5WkJCYWpOSFo2b3BuMk55WHNJbzFyRFpDQTZZbVhXeUxKS2dEVTQzS2VWWWUwVUUxWGpraExuN2JPZmhRSUlGL0NWVVl0cHViYzhKM0FqNFg3ZnNDSjJrSExhUGtqTE9hcC9KNEJoK0lJMG9BckhzbkxoMlJiaTNjckZrUDkwUkxDOG9iY1VNald4WXZydUM1bGNFZVMwVVZ3T1JDcm9rNm9BOStKNWVQQmZiYWFsLzMwdlNoWXFVbVRieWNpQmJZVUlVSWtvRVNVT3hPajJqajVqRUkwbVRxOHpiOUUxSHdpdEI0U2lXN25BSHFqLzFmSHFkZUVRL3d2UVR4TkRiVDR5YXR0Z09mQ2JWUTNHRWRpb2t1cTZOSWMwaTZVQS9ZN1VkcFFZYVhGTFVEVS9CZ0p0Z2RiUDh5Sm9KeUhjN0QzaVlXM2tSM0JRaWNFdjQwN2RvbzA2cDVvekNDazB2SkJROEQxNFR2MUVBODNqN1NIalo4SzRYTUorWEs3aEt4bFRNbXc2Q01TaWh1K1EvNittcjdheFZTb2Y1VGt4NmFnWnVyNnZ2cFdONnNyd2EySS9DTHdwbG1nRDYrYm1PWjFtNEJtZVQ3VDBpYmpKYVN0aVRqUURNVzR2REJ1bFhuTXIySzB0bG9xcXdTd1JtMWFPMEpsWkpZaHNXcXNqYWpXdE9UUlpyMm9MTzZtQjVCcjNVLzVJUWhJZEZhY3l0Qy9KNGRFUkYzRVlpZWF1bmd1N2hCQTlXcDZaNzRpWDdLclA4RW9VVjM0U1p3MWh6c253MG03VXh6ZHE5M3VPQktnNlVGckNPVkZYWVMraHhaekhvN1VlcHcweEQ5NkxhY2JOc3IwclZSODJ3TEFGT0dGUzNpUW1NVmdCa1lJOUJrbWZTUTlPUFlhRjI4ejlSaHFoVUVtVS9XNlY2a2pIbGZLWCtYeDBtSDZZeDlYc2JxcmtuRlIrc05ZUDladE53RTFQdFR1OUpEU1hxaTBoak54bDh4VXBGeDZTVEQvZ3Qxb08yeGl3cE1oUDY5TGRqYkRPUFU0THdkNnRxVlkzMWRXWEVKdm5KcExtYmI1WHZJR1V2SjZJZGpteWM3VmFMNUdCNDZhUVQ1K0FRR0lQSmVDRzRid0pYSVdWcnVmMGxBdVRWRXhRYTV1RzBTSnVidUNUZk0xUmV2Q0IxOG5qMVVwT09aanBXc1hKQSsyZEZxMi8zSGRIYXBBekFjNGsyaHhLc05uMitxS2N2M2YwbXRaWlJJa0MxL2I5U2hubEZqYXZNazA2cGhGOGVrZ0Z2dk9VSm13MXZkR0R6cFRTczFEM2dWSVc0WXFYTlZyV0ZTaHFQUTFLNjcvd1FxTVFrQkQ5V2RRRDcwSTdOaFhXZ0lISVZ5YVNLR0NpUDl5Q045UCttM2tKTkNUQTZqcU1yeGtrcGNQblhWV0dSQWR0b0VUdm5FdXlpNWhCS0ZxRkNscXBLSTFkSUQwWDlxUFlCMVZlSUowMEhubCtFakpnMERZZEtmaUZDd2JKZEowR0N1VkhKa3M5VExpNkd6SldIR3JKd3JkYmVzTjFDcWxyNFpWRjBXbitSQk5iYkxZNU91ZFFXVjJiemRQQTJhd0UxR3J0Z3l5cXRkaFhGcCs2b1k2RTlFUkVWNkRGV0wyejdvcTE5VG81V0kxNmR1MzQwSXZGaWcveFFQVFlmR1ZmVjRDODA2Qk8vM1F6NlVlVnl0Mi9IZU1PZWQvRThwNjBFOGlEaTlUd3dPVjA0UXYzTWRBSUJwSHB0S1RCeWlLRlZLcVJXR3ZpTjlWN2hTWlNZNXhacFFNY0N0aC9McTF4SFlCcmhGRzhPMXhad1VaV3A5bjFobVRzSWtaVGtLNWZqNGJsUmg0cGpzOGFUZndFNFIwRDZoeDhMc1ppQVU0a1g3WGh6aW5OWGRZb1V4a1oxZHM3UmxhQmppSml4aS9MWlFYd1NtL2JoSm9HNzJxOXRSKzJTYnRrMTlDV2xNa2s2Zm11cFNrM0VRUEJ5cFplQmVHeVJOTmdpeG5MaXBtMjBhM1RreWRQV2pPRVRQOE5TMm5NMjdWUHJnNzZnbGpZaFowcU5PSUhuMHN2VXB2dHFsSC85c25jTkpJWGVrTEtoR3RmeVVIMm9oRjc3V1F6YUp1RHNralhYcWFENExPVXVzV01uRFVqSVZLSzZQRGxQaGMyeXJXMGtUY0diaTVQeC9IUHBvRVk0Q1k3cWZwNWtwTmdtY0RTTnBFdnRlbStvUkw4WWRRemhuY1ozWFpHUWV4VVZNVFZyWnZKVVdybUpUWHp1T2NtMXFrdEpDYlowVTZaUHI5RGVlSmxHR0VRRGhRZDBaTUE4N2lHNUFRY3B0Qjc3bTQzNVhzbndTVDMrS011WjJ3Qm1mRTAzcXdDWWV5cW9sNzJzejhkdWI1djMyYlRCbzJ5V003ZnAvTmNZeHRjaERNMVVtKzIwVEpyM1ZWaHRVQjllN1JzZmQvczVva1FsRVpvWnlJQW82bHZtU3NDeDltR01zamtXNlVuNURBajBoMzI2ZVhrUzF4elJiV2ltdzcrTTFIZjltc2d2WTdYZzI0QXM4ZXFiU3pWZ2dTQ0dWV3ZhL3YyZ25iVW5NQlJCQmN5dk1pMXVwN003MWk4Z1UrelAvNE0xb1dCeU1HUy81cnp0cE5lVmRsYnVqZDBYV3pZclNhaEx0ZkprdmdNeFo1Wk03MTlZc01kMGJZNnkyeFVqRlp6R2M5V1FuTld4SDRoeE1mN0JaYmVMYnAzZ0lnTGhlOGpBT0FQVC9Na3BISFUxRTg1WUw1Y3hoSFVud1ZFYXJ5SURMTS84eHpPVVRKTW1YeEVub2pJOHdKb1piTXBBZXExRklrYVMxTkVHQ0RRbWlsOU93MVNzTTZKdklBdUhFR0lJNzAxRGJkaEcvM2lGQzBsdEs2aUlhbm0yVlZqTHdUeitTTk1kaWV3Q09BNUgxbVBHN3BFbHlwcnNyUlo1Y01mdWRGQVpxMWFvdGMwVHFFODB4VFdRZ2ZRL014VkNYcVVOalpqRzhoai9EUlhwbE1yYXhOSW5XYnJWVkowUzNsSmpJN2ZCUEc3QXFsYlQ4d0JUeHU1TkFHZTRKR0F6elRpNkhKTzJjQ3hQOEVhaWRIZ2FZMjJhdmtxaEY4OTNFN0RHK0d3WThMa2JuYjVMQlVOeWYxNDV0RU1pcmxhaFNiZVJqb3U2d1BQajhHcXZXMkhjd2laTmYxRlVjc1pNNitqbXF0UmhHNjlKcmYxQnlSU21WNUJ0cC80aWsrUVlTQ015MzhXTVBqWVJBZXRYQkVJN2l1Z3JUbktYVE1DSDlDVDIvWjBjV3lOUkQxVHB6eWZCc0Y2Rnh3QkhEYUZYTmtZQTBFUHJuZWo3U0x2RlBHQmpNdDNvZUYrNHoxcm5DU3ZlUzNhWkg1ZTJsNUtTMmVZeHRmRGFWRmxTZHUrRXR3aENlbTgwQzJndWtUOFg2V096aWtOcXZ6eGtYdHRzSmtIR1hwS28xSW5KN2NQL0lOeUNPbmRETWpPNWRrc2MyaTJOQm55SXI2eWNsV2FiSDM2Q0FmWWpGUVVCVGEwaytVelpSclBXdjZQRXhKWVNZQ2wwQXpPTkVUUC9YckxZUXNXZ3FSK0orUGRseEtDWXZnZUwrMjFCZFhwQVdvdG15RXFMSU5SOVJYV2taUDR5WnZENStxNmhtcWZsVmFXUHAxSlk0NW1BUVh3YXNhYVFmYWd4eEViYS9nV3hXUzA3RzdvajZtZHBlVzRVR2RUU21ZRG9DWldzem5uY2p3RWN5bUVCWHgrUzRoSUhCb3UwMmpjQjhqdTdNeFIvQjExdTZXaWVMOXdNenlTUjk0cnFKeDQ2TTJUcGVxWkRPbUE2OTR3R3dONTdpb2Vxb01TMmQvZWIveGhpcXcxcklCd0psazBaZ01mbFlJcmJuVFVxSlNhWDFFV3pQcEFpOHg5cDVRSzh5ejUzVHRTdmdONlY3b1JFdkNkNCtRcHgwT3dKc0dvd2k0ZjN3WFFHZ1Z1OTJmeURzTXhXV0h1TUcxMmdYYmplMm9lOUhxSUd0ZjcxUXhEbW94akI5WXl4L1p4SVlGT2JFSEpLSVk1K0h0M1FhdEF0NnFnSmo4aytHNldaZzhYYmVaMHlZUWI2Qmd3VFM2VTE0a21Db3hqRVNHVFAwUTdkTXVJejhEdEpLeDBqb0pUWUdFRkJKeWkzMjNhc2V4c01Eb2NQdVp5UUV3Mjh4RGIxR0MyQzVwWjkwVUY1MTdKWDJKOG1td3dUc0djb01wWERMQ0pubWFRbjA0djZHbXdCVHdlMXd5R2hZMTFGcVdKTDlYSnRKbURyaWs0YWFFc2QwMDBrVHJVVDg5K3l5aU9XNDdlWmdOd0hadTNDM3dZL1JYYUFkREpXbitBbGRBMnEzNENSYk92WWczRlVQUUZpNFFSYzdNMXVwWTVrZXZxSUpldVpDUk1kNXR0MS81aGJFTml5MkFhQWoyRWI4aE9NVDc0bHl2cU1NRTFFWXR4MHpST1IzUlR2d2NFVlhTUi94WWtqOFJDREcxUmpmT1V3NVBXN2ZPVEJadU1yRzh1RHdtc1FaNGJoMjFTRFRPTnZJL1hnYkVSMUUxVFpqZFplZjBleEhCb0NyM21JUG9uckxMblBJVHBzNEpuSkxKWXo0SlpCZE1GclcyQ3Y2THFFVloveE9aUlBqQlRFdkNmTlF4MGJVaHg3Qk5Gdys0NlFXU2pIQXF4VWpBNWljQzBLSXFaU21CZy9XU3lRQ1RzcWtEZ0pzZDgwTmlKWEdsSDRNOENnM3ZuQUtKd1c5TjZTUVZiSlk2aitveVI3UnNEejg5NHcyVEljWGtOcDBRREtCZERaTlBJWkw5OTZVNmVMNngrY0M5Wm41TCszWndWWEdSR01vU0dBZUxWbHZ0dlhTaUJ4Z3o3cGRDZFFrcUU0b3hZK1EySjlBU2JwbDJwdjA3bSszdjJMMlQyMUlDM2Ivc25UVFQybnhZQ3JuclE3ejVSYXpqRE1XK3V1ZkZWbXo3K282ZGRqbnI1MUdJVHE0bnNGcWEyQ3FReXZTUzlrUkswbjNQWlV1RjdYS0REdzdxU2JKbUt3MmdseHVwQWVUdHVTUDhHSjNnLzVMOVd6S1c3MjYxWVJOSXBicGJVSEpreFNGK0xRd1VhVnhQS2tRKzUrU1lMakZSRG14WSs0ZWp6UnVCaHlYYTZIVUdaOVZPbWxRbldFaUlaZUd5RE1WTW93YmN5ZnVhS1FXVEJBeVBmRWIvWlJheTJicFhjcmlrTzFkOGhCaXJlMnRrSGNjaGx5cXRrd2E5ZmNWSWg4TlovV3lpRlFkdHNuV2Q0ZlVrOWdtVDhDVTVDbmtGa055dFZWNHNOTDZHS2gxSkhrVGU2WWxUTDFOQ2szcFRSWDE0L1JBY3MyaVBNOFRNUk9vZlpUOVE5S0E1N3NuYU1IOVRRSkpUOU9BOXhBZUEzMjNad2RKUDJtcjZzeE9ka0JBcG1PbzBRRlRhYTVQOFNwbGZRKytwdFMzdjZBNzZiS2Y1dndwYWZTVTBwWDJzTEU3VEdqaUIrN2wxclVQZGJkM1E2cEVqZEx3a3RpUEEyZ1lpZUVrcGk2VjJzTVdqeVRRUCtuQVdGUTllWGtaR1FmSHhUemU1aWFZZjFLVWlTdzRLaCtncE9EUUJvcXZzakZkenMxUFNhMUZBMWJWcCtvclNYQ1pqZ2lOSmlJQ0xhUFNCTEVPQUdmTnJoSklCdlBwWldzVnM2bzg0SDd6ZnhaM3JTVWF4TUkwN3Yzb2o1REFUditzbFJudFMvTXZMTkFnRnJ6a2lnZVNiU3I0bCtpeStnWEQ3SjdoR09HNHdZcFhoQ28wYm9lSnY4dlc4TkJndVVuT3M5aWhBeFdLaGRxdmxyYmdmNG8vZ2p3bUZ5VVZUcHd4Q2tvbmNWbk40ak53UHNVOUpBT0RIWHVpQXYrZlVQckwyUXNHYTJQZXZ6TEJhUkV5LzVHanJoSjEza1huTEtZRHRTbjlxcHZEZVFJdHNtbHNxa3AvM2xUZFlIZWhrYVpkdWo0YU9DdzIrQm1uRlVNSTVXcU9Gb1AyVWZxMG12dE5ycC9yMXBBUWZSMVYyRkJQQzNXZ21LaURJenNDcFZLcEY5aHUvbmVNMVA4cGh1b0swaTRnc0swVGp2VTU2YVhIQXh3K2tpN0ZWbWJka2lMS0N1L3VZSWEvU010MnI0RGlReVlWLzhFTUk5TGxMUU96NjNTQ21sUUZaRnBIaHRxTHpha1VmaDFKQ0xZeTNKMUZuWDFrU0JvRDNqU0Q2aExBM2loQi9QMHFsN1N4L1ZVTXg0Y2JPUFVydEozL01ERVRLRmtCeDV0MWNqSkVSQU5rYTFLUlpLN0lmbnRrWEhmZFh4Umd3cDdoRUJuUmtQNFVTQy9FWkJuRWF0dnp0TzA5blRKZmpyMnVDWmdvYzFyR3Zia3JSdm5MV0V2Z2Z5YVpYZzJEUVJjQU5UeURoMXlWa0grbFN4NlVWaXEvREdURDhTZlZWTWZxWGtpdExkMnBsMVp3bEVReC8yUndwVEU1Ri9Zd0tuVWlqeUF4Kzl1TThxckkraENIWnN5RWQwOWZObS9SRGx6aGdiR2lWMlc2dkNzcFZsV2p3Wm1lK0lpZEdCSUgrZ1dOOFphbUJDblZrN0V2YjkvWEI4d1ZtZWhoOXJucU56QklSbmREUGErU0Rjd3cranZsdWQ5ZUxVYWxpTTVMWEtOVC9WcDFINllHdG1ITVdDVVkwWUdYUlJBTjlWWFFFcU9tb3BvazZyZmRNbUttWENwN2hIWFYwOWlUSk9PdHpkVW5Mb01QNjd0SHhWMGtkZ082eWJLQmtUR2JTUkVhYW14WkZ1TlhOc2lDeDhiUmlqdHMwcFVwZ0J0OVB3YmdIcXpvWGprR1lyckJRTmo0YkRBa3BQRk5XZEJmQmtNM0thcTVYWmZ1cEZrVXJ4ZG0rYndEeDZMalVkL1l2aHd3RWF0RTB1QWtBbWJTdWVVd21ZTkpCaVRuTGlrcVZCYWFkZDJySjJuZVVXZC9rYTRZNVozOEZFMy9XVUxablVuaTMyUFpxdDVRS1poMUp4MGpZYlNqVU50bncvZ2prMVJMbGNYZU91UDdjT0VqOUhCRi9qbjY0YmFYQmRTTVRPUmdDd2dSL0trYVRYaVk4N0ZuaUw4VWlYbzFvM0VmdU9KNWRnbzRVL1VmcDlWRS9DVUcrNWgxK1p5ODVocjZpdTB2dVRrMmlhVVNLa3lnT3FpTjhiejVMMDRsOVV1N1VpWncrS2VYWmtGUHBGR1ZFRTd5VFlPWC9XaXdvNy9DUkVTMzJpTGZlVlVQNEoyVmgzcEpnYUdtUTVha0VvbUkvaEYzak5Oa1NIcW45YTcrTEFMeWJrTmZwTlhBdDkrdi81dWdhVVFaL256N1RFRERFWHNFMDVrUjV5WVVNQlM2bVUyVHhmUDV1azhXTGE4WS94dmZyejcvTlAxVkppSnFiZnVWOWlVZ09YNmVzaXc4S1F5Q1J2UC9MME11U2RHeGtqSzVBQUFBQUVsRlRrU3VRbUNDIiB0cmFuc2Zvcm09Im1hdHJpeCgxLDAsMCwtMSwwLDEpIiBoZWlnaHQ9IjEiIHdpZHRoPSIxIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJub25lIi8+CiAgICA8L2c+CiAgIDwvZz4KICA8L2c+CiAgPHBhdGggaWQ9InBhdGg2MDU1IiBkPSJtMzI0LjQ4IDE3Ni42N2gtOS4xOGMtMjEuNzM4IDAtMzkuNDIyIDE3LjY4NS0zOS40MjIgMzkuNDIydjUyMy4wN2MtMjcuMzQxIDE1LjY0NC00NC41MDkgNDQuNzc1LTQ0LjUwOSA3Ni44MjYgMCA0OC44MTEgMzkuNzEgODguNTIyIDg4LjUyMSA4OC41MjJzODguNTIyLTM5LjcxMiA4OC41MjItODguNTIyYzAtMzIuMDUyLTE3LjE2OS02MS4xODItNDQuNTA5LTc2LjgyNnYtNTIzLjA3YzAtMjEuNzM4LTE3LjY4Ni0zOS40MjItMzkuNDI0LTM5LjQyMm0wIDE1LjVjMTMuMTU4IDAgMjMuOTI0IDEwLjc2NSAyMy45MjQgMjMuOTIydjUzMi42NWMyNi4xNiAxMS4xMDYgNDQuNTA5IDM3LjAzMSA0NC41MDkgNjcuMjQyIDAgNDAuMzMtMzIuNjk0IDczLjAyMi03My4wMjIgNzMuMDIyLTQwLjMyOSAwLTczLjAyMi0zMi42OTItNzMuMDIyLTczLjAyMiAwLTMwLjIxMSAxOC4zNDktNTYuMTM2IDQ0LjUwOS02Ny4yNDJ2LTUzMi42NWMwLTEzLjE1OCAxMC43NjYtMjMuOTIyIDIzLjkyNC0yMy45MjJoOS4xOCIgZmlsbD0iIzEwMGYwZCIvPgogIDxnIGlkPSJnNjA1NyIgdHJhbnNmb3JtPSJtYXRyaXgoLjEyNSAwIDAgLS4xMjUgLTQ5Ny43IDkyMy44MykiPgogICA8ZyBpZD0iZzYwNTkiIGNsaXAtcGF0aD0idXJsKCNjbGlwUGF0aDYwNjEpIj4KICAgIDxwYXRoIGlkPSJwYXRoNjA3NSIgZD0ibTY1NzcuNSA1OTQxLjloLTczLjQ0Yy0xNTQuMzcgMC0yNzkuOTYtMTI1LjU5LTI3OS45Ni0yNzkuOTZ2LTQyMDUuNWMtMjE4LjEtMTE2LjItMzU2LjEtMzQzLjItMzU2LjEtNTkzLjY1IDAtMzcwLjk2IDMwMS44LTY3Mi43NSA2NzIuOC02NzIuNzUgMzcwLjk2IDAgNjcyLjc1IDMwMS43OSA2NzIuNzUgNjcyLjc1IDAgMjUwLjQ4LTEzNy45NiA0NzcuNDgtMzU2LjA3IDU5My43djQyMDUuNWMwIDE1NC4zNy0xMjUuNTkgMjc5Ljk2LTI3OS45NiAyNzkuOTZtMC04OC41OGMxMDUuMjYgMCAxOTEuMzktODYuMTIgMTkxLjM5LTE5MS4zOHYtNDI2MS4yYzIwOS4yOC04OC44NSAzNTYuMDctMjk2LjI1IDM1Ni4wNy01MzcuOTQgMC0zMjIuNjMtMjYxLjU1LTU4NC4xOC01ODQuMTgtNTg0LjE4cy01ODQuMTggMjYxLjU1LTU4NC4xOCA1ODQuMThjMCAyNDEuNjkgMTQ2Ljc5IDQ0OS4wOSAzNTYuMDcgNTM3Ljk0djQyNjEuMmMwIDEwNS4yNiA4Ni4xMyAxOTEuMzggMTkxLjM5IDE5MS4zOGg3My40NCIgZmlsbD0idXJsKCNsaW5lYXJHcmFkaWVudDYwNjUpIi8+CiAgIDwvZz4KICA8L2c+CiAgPGcgaWQ9Imc2MDc3IiB0cmFuc2Zvcm09Im1hdHJpeCguMTI1IDAgMCAtLjEyNSAtNDk3LjcgOTIzLjgzKSI+CiAgIDxnIGlkPSJnNjA3OSIgY2xpcC1wYXRoPSJ1cmwoI2NsaXBQYXRoNjA4MSkiPgogICAgPHBhdGggaWQ9InBhdGg2MDkxIiBkPSJtNjQxNy45IDE1MzEuN2MtMzAuNDcgMC01NS4xNiAyNC42OS01NS4xNiA1NS4xNnY0MTAyLjdjMCAzMC40NiAyNC42OSA1NS4xNiA1NS4xNiA1NS4xNiAzMC40NiAwIDU1LjE2LTI0LjcgNTUuMTYtNTUuMTZ2LTQxMDIuN2MwLTMwLjQ3LTI0LjctNTUuMTYtNTUuMTYtNTUuMTYiIGZpbGw9InVybCgjbGluZWFyR3JhZGllbnQ2MDg1KSIvPgogICA8L2c+CiAgPC9nPgogIDxnIGlkPSJnNjEyOSIgdHJhbnNmb3JtPSJtYXRyaXgoLjEyNSAwIDAgLS4xMjUgLTQ5Ny43IDkyMy44MykiPgogICA8ZyBpZD0iZzYxMzEiIGNsaXAtcGF0aD0idXJsKCNjbGlwUGF0aDYxMzMpIj4KICAgIDxnIGlkPSJnNjEzNyIgdHJhbnNmb3JtPSJtYXRyaXgoNjYxLjIsMCwwLDE4NC4yLDYyMTAuOSw5OC45KSI+CiAgICAgPGltYWdlIGlkPSJpbWFnZTYxMzkiIHhsaW5rOmhyZWY9ImRhdGE6aW1hZ2UvcG5nO2Jhc2U2NCxpVkJPUncwS0dnb0FBQUFOU1VoRVVnQUFBUk1BQUFCTUNBWUFBQUJPQmxNdUFBQUFCSE5DU1ZRSUNBZ0lmQWhraUFBQUdVNUpSRUZVZUp6dG5XMnNiVWRaeC84enN6L1hVb3VsTFczdEd5M1NsRGJGcHFEV1lCTWhRaXloRVJ0ejFTc0ZtbFpxYlNtcENKSUdRU1RWbXZwU0lwQWlvbHh2M3d1Tk1SbzFRYU9SU0ZDRGtVUy8rc2tYMU85N3hnOHp6OHd6czU2Wk5XdWRmZTQrcDNmK3ljM1plODNyV2ozejYvOTUxcXgxbExYV1lXaVJyRHNhbDh4WnU2ait2VGRjMUZWUDlZemRPZVp2ZitQZk8ydk9qTmQ1cmozemNvMy9ma3V2YVZlZmxiTGFXRko5NlhkT3FpY2ZzeDExWFB0N01WZUhzajZnQmt5VzY3akNaTGEvZzdZL3hPc3lZSEwwWWFJbnZRNE5EUTJ0a0Q3djNIUDJQWWVob2FGanJ1KzU5aHBzQU9DOGM4L0JmLy9QLysxN1BrTkRROGRNcjczbW12aDVReC9Jb1F5b0RBME56WWxEaERUSm1aeDM3amtZb2MvUTBGQk4xMTd6R3ZINFJqeUs0VlNHaG9aeTFTQkNxc0tFTktBeU5IUjI2NXFycitxcU53c1Qwb0RLME5EWnBWNklrTHBoUWhwUUdScDZlZXMxVjEyNXF0MWltSkFHVklhR1hsNWFDeEhTYXBpUUJsU0dobzZ2cnI3eWlwMzFkV0NZa0FaVWhvYU9qM1lKRWRMT1lFTGllMVFHV0lhR2pwYXV1dUx5USt0NzV6RGhHbTVsYU9obzZEQWhRanBVbUpBR1ZJYUc5cU1yTDcvc2pJMTFSbUJDR2xBWkdqcDhuVW1BY0oxUm1KQkdYbVZvYVBlNjRyc3YzZXY0ZTRFSjEzQXJRMFBydFcrQWNPbFRwMC92ZXc0QTB0UEs0NG5sb2FGNVhYN1pKVWNLSktkT240WjY2U3RmZHM0NU9PZndVejk1WXQ5em11Z29PcGJ4RHRoSysvRU8yRVZsUzk4QmUvbGxsNnlhMjJIcWkzL3doMURLdjRKY2ZlWExMMGFZMEwrZk9mblRlNTdpVkVjSktnTW1sZllESm92S2VtQnkyYVd2WGpXZnc5YnZmZUgzRTBTVTh2OWVmT0Y1QnlDQ3hES292UGV1ZCs5MXdqWHRHeXdESnBYMkF5YUx5bXBqWFhySnhhdm1jQ2IwdVNjL0Q4QURSQk5FNk44THp6L25DQ0lBTW9maXJJVjFEdmZjL2I1OXpyK3BmWUJsd0tUU2ZzQmtVUmtmNnlnREJBQis5ek9mQlFBb3JUT0FaRUI1L3JsbkhYY2pBREtRT0dmaHJJZk5mVDk3N3o3UFoxWm5DaXdESnBYMkF5YUx5aTY1K01KVlk1MUovYzRUbndZQUtLMmdsUGJ3RUlBQ0FPclpaNTl4cFJzQk1BR0pjdzdXV2xobjhlRDk5Ky90NUhwMW1HQVpNS20wSHpDWkxUc09BSG5zOGNlaGxZYlcvaFhSMFlGVWdFSjExRFBQUE8zS3NBWkFGU1RSd1ZpSGh6N3c0TjVPZUtsMkNaY0JrMHI3QVJOUnI3N29WYXY2UE5QNnRWOS9ETkFxZ3dSQnBRVVVJTURrNmFlZmNtVllBMkFXSk50dzdheTFnSFA0ME1NZjNNc0ZXS09EZ21YQXBOSit3QVRBOFlFSDZaT2ZlaFJRQ2xwckdJWDFRSG5xcWROdXFTTXBRZUxnWUxlK3ppOTkrRVA3dUI0SDBsSzRESmhVMnAvRk1MbjR3Z3RXdGR1bmZ2a1RuL1NBTUJvSzZzQkF5V0RTQXhJQTJEb1pKTTc1UDJmc3JNVWpILzNJL3E3U0FUVUhsd0dUU3Z1ekNDYkhFUjRBOE1qSFB1NFhQNUNGS0hOQTBjcm5UNXBBT2YxSHAxd0xKQUFtcnFRRkVnQXNaUEw5Zk9Kamo1enhpN1pMbFhBWk1LbTBmeG5ENUxqQ2cvVGhqejVTZ0NBQmhXQUNZQllvSlV3QUpLQ2NPdlVsdHlSUHdrRUNRSFFsdkEvSFBuL3FWejYraDh1NGUvM250LzkzMzFNQU1HQWkxdXVwTXdPVDR3NE8wc08vK0pHNCtHTzRNZ09VQ1V5QVdhQk1ZTklFQ2REbFNqaElBRXhnWW0xcTk5aWp2M3JHTCs1aGFoK0FHVEFSNnZYVUtlWjUwUVd2WERHam82c0hQL2dMRVF4YXF3d29Fa3lBR2FCVVlBSWdBMHJmS3dncXJxVFpwQUlTMGdNUFBReHJMUjUvN05HbDErcEk2dnhYZk1mazJGRnhNRU5lRjM3WCtmdWV3cUhwdmdjZWdnNEFxSW5Xb3RZYUZvQzJGbUJBYVNrYUJGakFJZ01LYVZPNmtsNUpybVJKT3h0Y3pQdC8vZ1ArdUxWNDRqZC9vM3Y4NHlBSk1NQ0F6R0hyNVF5TlV2ZiszQU5WZ0RnNCtHVnB4Y1ZmazRPREl2T2dOWXgxZ0o1dk8zRW1QUEc2VkR4TVd0UXV1Smg3M244L3RxSHRaNTc0clZWek9BNnFRUVlZb09uVnE4NGlZSlI2MzczM3dXZ05GSEN3emdIa1BMWTJKbFY3RkozSHduYXB2YTJIT1c2aFU2bE5ibTNidSs3Mnp3RTU2L0RrWnorOXVxL2pwaFpvU0djRGNDNDQvN3g5VCtGSTZkM3Z2UWRLMThNWTUxeTM4NWkwQmJwQ25XcDc2NkMwMnY5ckczdDA4cTY3NFp6REY1Nzh6TDZuY2lUVUE1eFMvL0ZmM3o2RW1jeHJRR0c5VHB4OEQ4eUM4R1Fmc3M3RkIvMk9MRXkwMXBOdzZjVEo5OFF3NnRRWFA3K25tUjFQdmZJN1g3SHZLUXgxNk00VEorTmRsK01nelVCWGhRblJjRTI0UWlCWTA5Wm9qVzFIMnp0UG5JVGRidUdjdzlaYVBQZlVseGFQTlRSMEZQUmpkNTRJdDJYTmFoZkNYd1d3dUMxd0lQZERtOWN5bUVRaVdpeE93aXF0b2EyRnhUSUEwWmpXV29SWHZuVzMxY2JBYnJjd1d1T2Q3L29Kd0Zsc3JjT0x6eDZObDJRUERkVjAreDAvRHFNVm9LWU9oTzY4ckFseDRqNFMyaSt5UU9VK2syVnROVFpLYVVCYmFOdS9UVnpCRDJxM0FUaXVaK2RKM3M1RHkzYU5TYkFoMXpMbmZINzBuZThDNE84U3ZmVENNMTNuTkRSMG1IcmI3WGRrbThZSUpEM0EwRnJINUN0QnBuZXhTNXZXZWxWdVdwdFRNMmNTUXgyTmNLOVp4NDFyYysxNHpzT2ZnTis0MXBvNHVaTU1HdkQ3WUdyMXVUdlpXc0JvaTYxTjd1aHR0OStSdmZEcFQxNTZvVG4zb2FGZDZhMXZmMGYyVkcxcnJ3ZUZPS1VyYVMzK2NrY3JkeVV0MWJiVHo2bjFmQTRRWUpLNUU2QXIxSkhjQ1NqVUVXQ1RnSUtwTzhFME5DcUJvcEc3azdJKy9RZmdRSWxBMHpvQzVhMXZmNGVIbHJXdzF1TFAvL1NQWnkvaTBGQ1BidnZoSHdrdXdpKzQ4bjBmcE5LVmxDQXBKYm1TT1RpMXR0TFgxTnBLM3hJOW03UFJTdFVYdnczYlp6SHZUcFJTT1ZDUTM1RXBGNzhFRkNsM0VsMk9BSlRTbmN3QlJkUGRJZHBPRFA4TFFDK0gybHFMci83bG56VXYzTkFRNmMyM3ZRV0t2OXF3QWhMKytzTWVrSEJYVWdNSlYrbEtTcENVbW52SVQxTHJJVC9TSmlWT2MzZkNGMzh0M0ZFdVFTR3JYd0VLZHljT2JoWW9CSTBlb0pqTnh0ODJGa0llcXFPTWdYTXVQcE5BUUNIWWJMVEdtMjk3aTNjK3dmMzg5VmYvb3Z1WGEramxxKysvOVljQWhNV25Bd3l5UlovZTl5R0ZOb2JDZ1FaSXpBUk1kWkRVd2hzT0VsTHRBYi95OVFPa250Y1BaUFZEbno3TXFRQkZESGUwcWdJRlFFekc3aElvQUpvaGp3bVFxT1ZRQUlXdFVtbXJQN2tVcmYyRFRpenNnZFl3QUxid3Q4eCs0QWR2OCsyYzlkQ0V3OS85elYrdCtvVWNPajY2NVUyM3hrV21sWjVBQktpN2theXNjQ094YkNaSDBncHRZaDhkanFRWEpEMVBDbk5KYjFyYnFMRElab0dDQkpTV1EvRVZna3VaQVFxZ1lHMGRLRUI2YmtmS29kREZvcjBtWmRpek1RYk9CZUF3bHdJQVZpbW9FTjZRU3pIYTUxWnN1RURPMmdnVlpmMUZ0YzdpbGpmZDZrOFRMdVpmL3Y1cmY3dnkxM1pvMzNyRHpXK01RS0JjSU5BUEVWOTN1UnVodnFTd2hwZVZPUkp5QmkyUVNBN21vQ0FwdzV2eUxmVWJ2amx0VjBDaEJkN0tvWkNia0pLeUZIcFk1OFN3eHhnRHA0TVRxYmdVbWdPNUZFQUR5c2E5S0R6MFVXRkxzSFd1Q2hWUXlNV2dRdjhobk5iNDNsdSt6MThYUzA5aFczemo2MS9iMGEvNzBDNTA0MDAzeDljUEtxMHlHQkJFQ0NDK3pqcUlBSEtTTmV0RDVmdEkxb1kxY2U2TlpHdFBqbVF1dEVuOXlDREpZQkxWQUFvdDNGcFNWcmtBaWZBQ0pGOHBCd29QU2Vna0NDaThiYzJsR09VVHZlUkVQQ09VZjlpSXVaUXVxQkQwa0p4S1RFano4SWY5Y2xHaVZ0bjBvbDNyL0hoMmErSElIY0hnRFRlL01kN1d0czVtZTJQKzZSKyt2bVFkREMzUTlUZmNsQzNDSG9BQWVUN0UxeTlEanhUT2xPVkxJWklkS3lIaUN4ZTVrVGovU2xpVGxhOThvNXJ2by82SHVQemRIS1J3SWN4NEFoU0VSVmE3eTJPTWppOVFLc01lQmNDRmszS0ZTMGtYZ0Y2aWxOb2FZNkRadTAraXMxRnFBaFVMQzZOTkYxUW8vT0ZPSlVJdVFNVmZXd1Vib0FLYXUwcHhjUTlZZ0hCTnJQSmpCTmR5NDAwM1ozQ3hiSXgvL3VZL2RpK2NzMUd2dSs3MUFKQ0RvUUVQT2s0N1FpV0FBTWlTcXJGTnhZWEVjcFZ5SXJHUEZVNkUycFlRb1hGTGlQZytscnNSb0IzVytMYjFWelBTZFM5Qm9wVENSbXZ0Y3dmQjRrY0ZvRUFaV09WZk9DMkZQVW9wT09YYjk3b1VBNzlvNGdKbkxzVWJFdXZEaDlDV1F5VnJ3NkZDTUJTY0NvQll6bytwN1RaQkJXaTZGUUJkWU5Fd2NOYkdYMWJyL0xsQW0vVGUzT0I0Q0M1VXozcDdodXR2dUNuV283Nno3MkYrMy9xWGIrTGxwbXRmZTEyMjRBQmtRQUF3S2EvQkEwanVnK3FWSVF4OW5nTUlqVnR6SWJHOGtSTkovYStEU0R5ZkJibVJOSzlsYmlTZHV4eld4SDRaVkxJd2g5S2lWTkdGaFNXNUZLVVV0QlBDSGdCR1RWMEtrTCtkTGNGS1pZQnd3WUgwUWdXWWhqK1VVeUdveFBQaDBHSEgxSGJyNTE4RGl6SGhYSUtEc1JZMlFERnVnQXUvU05SM0JoZk5IQWgzTGd3dUJDSGZOdFdsY2orL2RQdmRXb3ZycnI4eGU4TWRmOHE2ckZ1S2p2M2J2MzVyVXJaV1YxMTlMWUMwa0VtVE93SDhkNjRBUm5aTXFEY0hEZ0JkOEtEK3l4REd0NU1kQ05BR0NLOHJoVEtUY3I0b1Z6Z1IzbDlQU0VQMWwvd1ppL0o2bEc2RWo1WEJSSVgvRS90Ym9XeVRXWFFUYXVKUy9BTFgrVXVvaGRBSHprMmhvalVjVUlXS01TYUdQd1FWQUZCYmxZVS9QQTlqNkh3SUhEcTBZVzdGRnlld21BQ0xPYkJRTzZzVWRQaXNsZkt3WVE3TEJZZWhXUmhEaTU3MnNCaHRNc0RRZFFHUVFZYTM5Wi96TnJ3ZXpVODZYdlpEZXQxMXIxLzhaanhKSlRCSVN0ZkJJc0VDeUlGUjl0RUxEZ0JWNTBISFd1NkRQcGQzWkdJZElZeWhNWE5JMVYwSXpZY1daM2JYWkZjUThRY25FSW5qTE1pTnBEWUpLcnhzczlrWWJMbGpvUDlvWVJMMEtzYmNUZmpRUjlIN1kxRWthSjNOUXA5ZXFFamhUNG9IMDJJbHNCQlVBRFRkQ29BSUZzcHY5SURGQU9FMUJ4ckdlQkRBV2NCb2JLM0xYQXZCaFVCaXFYMkFSNnhYdUJjQThWV1ZUdWNPcElRTU1BVU43eWQ5WjBBUkhvbm9oWWYweUVLUEpMQVFJR0pmN0prT0NSYkFGQmk4bnhZNHFMd1dKcFh3b0xMU2ZWQy9zWTdnUUhpZEVpRFVmNjhMb1hPZFM2enljMWpxUk9JY2hFMW96YjhqWEVDRUgxZEt3UmlORGNLSExTVkx3NktpZnhUNnhPTXFoVDRUcUZDSUlrRUZlZmdEY2hURm44eWdDMFJRQVpDQnhZZEJhTG9WZmg1QUFSWmp2RHZRRGdZQkhEYUhDSGNoTWx5Y0NCYytWODNhOHdjTmFUNUFnZ0J0ODY5QkJwdDBMSE1lM0oxc0M2QVV6M0hYQUxMTFA1ZFJ1aERTSk14aGo4WlBkbDhXc09EOUdnNmFTdjVFQWdjZG45VHRnQWZWbThDaFBGNXhJTHplSEVEODNOb3VKSjRMa05mcENHZm8rczVCQk1oQklZVTBIQ0tod0NkZ25YTXA1eUJBaFNkb2U2RWloVDh3Q3NhNVBLY0NCV05VbHFnbHQwSy9UdHl0QU1tRmFNMitCN0FZK0h6RXJHUHhBM2x3bUxEUEpyZ1dZMW5JMGdFWEFEQWhYeE1tREFBWllHaHMvbjBPTWh1aERTLzNZMHlCRStzVnpzS1AxUWVQRWs2azNwY05sMDRFbURxYkVqNHRXQUR0L0FwZmFMeWRDQTVmTVlPQ1B6Y2p0bXZCbzZ3amhUQTB2eDZBOEhGNlhBaUFyc1FxbjA4cm5NbjY3NFFJSGR2d3lyMVF5Y0lmYW9lVVV5R28wRUFFRlFEUnJSQlVnUFMzaTAyNEV0eXRBUGx0WlQ0bjdrSktzRkEvVWlnRUpBZXhDVERscm9YZ0VqZllGWENKN29iQkJmQkFBWExBQUZnTkdYKzk1QnlJQkp4WVZrQ2s1ajYybGVPeDQ0VXl1ZzZhRWh3bFdMUUFrdko0Q1F6ZVR3c2FmRzV6cnFPc3krRkI0N1RjUi93c0FXVUZRQUFzZGlGcER2WGRxM3lNTW5TWkMyZkN3ZXc0QUd5TTB2RDNGZkk3SEJ3cVplaEQ1WlNvamJBSnUwa0pLZ0FpV09LRmNIa0lCQ0J6SzBBRExMUm5CQWhKVEQzNUM0TCtoQk1Nak5hZ1B3Q210c3BEajlXbnhib0VMandzQXV0UEFrenVZRXc4WDZvZk9nemZtZnNRSU5LQ1JQbmtkMDlZVStQRlhFNmxsbXd0SllVOWNqNUZ6cDlJcm9RZk55enYwZ01OM284RURpcm40SmpVcmNDRHpxTk1vZ0tJT1pEWWY4TzlyQUVJV0YzSmhmakxrMEladXM0bERNckVLdS9YbVB6YzR6K2t0aHR0d2o0VDZ5SlVPRHl5SkdhUnFNMUNJSGJNS1JVM2ZuRzM0cis3NkZZQVpHRFpoUFpMd2VMN25ib09DdUdpYTlIcEJVMDljQUhnZDhIU1FpMGcwQXNZQUhEaGw1ckdBYVpPWmlPMEM1M0hqeHc0ZkJ6ZWI2MThVcmJEZkFtcGxqY3BuUWpRdm1Vc2djSWZGMXdKdTdaTG9PRzdidGN2d3hhcW82VmpEWGpFT2tJZlVnamp6NnNDRUtDWkMvSGY2eTZFajZPVkFBaDJqQ0F5Y1Nrb1FSWENIS1g4eHJNSUZlZmlNUTRQN2xhY3k2R2oyQ0xrYm9IQTRrSStJZ3VENktMT2dBV29oMEkwcHRQK21SNkh1bXNodU1SakJWd28zNUxsVjBJLzhkeTRlL0VIL2Z3S2lQQVFpWC9uZFZLK1l6TnB4OEVDeU1BaFRjS1ZTbDZrQkZGTkxRQ1JKRGhJeXFBUUcwK0JJMEdDMUlKRldmOGcwQkRiVlZ5SG45YzhQTXBqa3Z1Z3VtVU9oTnBLRHNUM0pRUEUxNnNuVkdOZENSSktEbVZpSFpRNUY1WXpvUXNVUXhqRndFQnVSYm1zVHE5YkFaQVdkRmlBRVN6YWw4MkJCVHBBSllRUC9POGVtNDJPODhqZ0VpQW53WVhteU9IQ2oxRllSSDBDbUFDR3pvdmFBVWc3WXl1UTRXVWxhUGl4ckEvSXdDbnJTNER4ODk1bTMzbFkwd09NdGFxQmhrT2hWcmZ0VnRvaEVML2RQQWNOWHJZVUhMN050RTR0OUtuQmcvcU5kN2RZKzU0UUp2Vi9NSURRNTVZTDRXTkpBRkpLWVdPMDhWdStWWjRYQVpDNUZRQVJMSHdCN2hvc1FBcUZEUEtIQW5QWG9vQXdyeEl1SEFMK1BISzQrREdtSVEwUFVTYnVSQUNNMUxiczB4VDloZzhndFdBREJGQlVvSlAxS2ZTSHphWmFyNmJlUFNpOWVSTkFob3VVckcyQlJRSkYyV1lwTU1yK2xvS2piRHU1b3lUVlh3QVBQODlwRXBXdVJ4bkM4TFlsUUtReURoQ3h2T0ZDK0hmYU1PaHpKclNSeTdsMEd4ZTVXd0dRNVZZQVZNTWdvQTZXTkU2NlU1UEt3bFoxRlJaK2NDM2tLbXF1WlE0dXdEUXNRdGlhNzh0azkwSmo4ZTg4UlBMbGJjandQbmlTdEhRMFpSczBJRkdDb1lRUGtBT28xQzUydkxiVUFvMFN3cDZXR3dFSzhBaWdLUHRZQWd3Z2g0WlV0eGNjVkZhRFVnc2VxZDgrOTBGalN5NmoxNEZJWlZJdXBBYWo5TDRYLzkzZkdpYWdRL2x0NTlGSlRJSGc0a0puWUZGK01jMDVsdGlIWTMwSXJrWEg0eVk2Q0FrdUUxY1Fub0dKY1BFZHgyU3VpdHZqWmZjQ0lBTU1rSktVU3lIajYrU2d5Y2FyaEN3U2NJRGM0VWpsZEY2U21yZUFXLzNOcURkdkFsUnVHd3Z0bSs2a0xHdkFBaEFXTTZiQTRHM21YTXhTY0ZCZlBHazZhZGNCajNSTVQ5cG5ZR3NzL0I0SDRzK3hEeUJVbDErblNjNEVPbDJRcFdDQkNsQ1FRaUgvSmU1ZjZYSXR6dDhaU21Pa2tFaUNpeFFXUVlQMW5ic1hmeUVTWUFEQllTeUVERkFIRFNERGhvL0x4eTdiOHI3ajl3b0FTaEMxK2pnTU5kMkpBSkh5dmFKbEh4T1FDTTdDdDZuRGduOFc0ZE9BUnRaMklUaXl0a1hZUXVQdUNoNzB1VXlpVHVwMEFDU05Wd2RJa1RNSm9VT1pMK2tBQzlXanVpa1Brb09GMTh2MmNhRHRXdWhZR1JLbDhxbHpBUUJuODNNQ2N2Y0NGSUR4bGYxOERnZ1pQMzQ5ck9GektZL0h6eHNCR3VVdDRYSzdmR1hINnE1ekphVzY5NXhVWEl5MG81YkR3WS9SNTFhV2hEMDFZR1QxRzlBQXNCb2Nmdnc4NTVITnFRRVBxYnpsUHVMbklvbks2OGloVWg5QVVwOGFHM3FyZTN3Zkt2SkZXSE1zVkViYnN6TVE2U0tVNFhEQk1yZ0FZR0VQQ3llS3NDajJhZTNFdlFBNVlPTGNDd2VUUXFSdzU4Z1grbm5OUUFhWWdvYnZZZkYxdzgvR1pyTWFkTXF5eVhkMnE2WUZodFlmUXR1MVNnamtaZk1KV09sWURTVFZqVzlDWDNPaDBCcG84UG9TT1B4eExZS0RmazdhRlF1OVBGYkNZOUpHY0IrOFR1aytZaGwvQ0hNR0lMeHNvNVNHVW1IUlpjbFF4UC9ybDJCUkpyd0hCQW9hR3M0eUJ6RGpXbnkvZWE2RjE1WGdrdlVWUGtzNUZ3RHg3V1ljTVB4YzVnQURGRG1NR1NjRENLRHhGZjBQS1h4aHQ2Ukw0UGcyN0hOSFdOTnlIMnVmRUY2aTN0eEp6Y1gwaEQyVDdmaU45cjJ3NEhNcWdjSDdXUU9OL0xpYzBLMjVEdnBaSG1zNUR6ckhGanpLejVMN2FOZFhRdEkzL1hlSkQvb3BSYTgzOUFYTzJXSW5hMzRydFJVTzBYSHVXckoydW5BYlBjN0ZId1JRZHkvbFdCa2tDZ2RENXdSd21PU1F5Y3NLMEJpa2wwcVhvUEVONDBlQ2pUSEY0czVjU3lWc1lUbXRNR3oydlZRTENZZXg0N1dsMm03WVdDNGQ2M0VubFh4SnFKenFzZkZic09DZlMyRGtaZXVoa2RYckJVYzRIN0UrcG5PYTY3ZlhmYVQ2NmJ4cUFPRnROa3I3aVhubmtFQUE2QXdzK1YyV0JseEM4cFc3RnFyVGdrc3RMQUl3dVZOMEVNQUE4eTZHemkrcjJ3RWFRSWFOenVyb0hBUUNkQUFCUE94Y3MwTU5mRXp5S0pYRmZSQlhRdXAySjVXbmppZFE4SjNtYll2NW03TEpEQ2g4bFRvc2ZMbjBmK28rYVBEUGMza08vcE9EbzFxM2t2Tm85ZDBEajdKOVBmRXJBRVNsNzFwcGJPSjdTS043dC9BUmlBSWNtcTRsTFg0NTF3SzA0VUxqVGRwb0lWZGlDeEIwQWlicnR4TXlmZzc5b0tGcjRtVW1jeTNiQTNYbzVIVzEyQmFWdk1lMk9NdzNuSjZKT3ppUzVoSzBFeUFBR1JSSXJWREdOMG5qOU9WVjZyRHc1ZE83UFhOT1E2cXoxSEVBNkhZZDlMTjBWWWNHRHorNXlYZzB4NDNXQms2bFJhZXR6c0F5NTFvQXhIZWNTbkNoTmh3dXFSOTB1UmMvaG5DblJ3QU1iNk9LRUdFT01rWTRscm1Pd3RGSTlmMjhhdnRHK0lKTzBDbnJsZjBCQlF3NEpGaWZmTmxPM2NiaHVSSlNyenVwMVpQZWdUS1hxSjNtVnFhUUtPdlZua3p1Y1JqOGMrMG5JTHNOOGVlTTQ2ajluTDV4Ymo1c1NlUE13eVArTE54SHJKUGRFZkxITjBZcldMTHVDbkE2L2RJcnB5YXVoZUNDdUQ3bjRRSk13eUw2S1NWMGZmMDhxVnUyOCtOVW5NWkt5QUIxMFBCeGF1VVJDT3pWakpNN09pVTBaaEtzM1BId2N5dFYyMjlTRy9kTWFnNHkwaDRUYWFlc0ttRFRTdEsybkl3WTFzeEFSVHBXQTRiVVRuSWJZcjBaeDhIYnpyVnYzM0plQmc4YWw3Zm5WMWhyK2xNWHpDMVk2K0pBVGlIa00rak5ZdE9RU0lJTGhVVWtubk9oT3JWTlh5VmdBR1NBNFhWM0JSbGVad0lhL3dWQUFrMVozZ01PRVRwQUJoNnBqOGxjK1BGSzZESUhsdFR2YmtPZmNySFhKTUVqOXJIeWxuSFRyY3lBb3ZiWjhQek9BbUR3T1N4eEc3N2RPbkRRbVBVazhPN2hRZTJvenY4RG5nRS95eTZSUUpZQUFBQUFTVVZPUks1Q1lJST0iIHRyYW5zZm9ybT0ibWF0cml4KDEsMCwwLC0xLDAsMSkiIGhlaWdodD0iMSIgd2lkdGg9IjEiIHByZXNlcnZlQXNwZWN0UmF0aW89Im5vbmUiLz4KICAgIDwvZz4KICAgPC9nPgogIDwvZz4KIDwvZz4KPC9zdmc+Cg==\"],\"markerImageFunction\":\"var res = {\\n url: images[0],\\n size: 40\\n}\\nvar temperature = data['temperature'];\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120;\\n var index = Math.floor(4 * percent);\\n res.url = images[index];\\n}\\nreturn res;\"}],\"fitMapBounds\":true},\"title\":\"OpenStreetMap\"}" - } - }, - { - "alias": "route_map", - "name": "Route Map", - "descriptor": { - "type": "timeseries", - "sizeX": 8.5, - "sizeY": 6, - "resources": [], - "templateHtml": "", - "templateCss": ".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 200px;\n white-space: nowrap;\n}", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidget('google-map', true, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"title\": \"Route Map Configuration\",\n \"type\": \"object\",\n \"properties\": {\n \"gmApiKey\": {\n \"title\": \"Google Maps API Key\",\n \"type\": \"string\"\n },\n \"gmDefaultMapType\": {\n \"title\": \"Default map type\",\n \"type\": \"string\",\n \"default\": \"roadmap\"\n },\n \"defaultZoomLevel\": {\n \"title\": \"Default map zoom level (1 - 20)\",\n \"type\": \"number\"\n },\n \"fitMapBounds\": {\n \"title\": \"Fit map bounds to cover all routes\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"routesSettings\": {\n \"title\": \"Routes\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Route settings\",\n \"type\": \"object\",\n \"properties\": {\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"lat\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"lng\"\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"tooltipPattern\": {\n \"title\": \"Pattern ( for ex. 'Text ${keyName} units.' or '${#} units' )\",\n \"type\": \"string\",\n \"default\": \"Latitude: ${lat:7}
Longitude: ${lng:7}\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n },\n \"useColorFunction\": {\n \"title\": \"Use color function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"colorFunction\": {\n \"title\": \"Color function: f(data)\",\n \"type\": \"string\"\n },\n \"markerImage\": {\n \"title\": \"Custom marker image\",\n \"type\": \"string\"\n },\n \"markerImageSize\": {\n \"title\": \"Custom marker image size (px)\",\n \"type\": \"number\",\n \"default\": 34\n },\n \"useMarkerImageFunction\": {\n \"title\": \"Use marker image function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"markerImageFunction\": {\n \"title\": \"Marker image function: f(data, images)\",\n \"type\": \"string\"\n },\n \"markerImages\": {\n \"title\": \"Marker images\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Marker image\",\n \"type\": \"string\"\n }\n },\n \"strokeWeight\": {\n \"title\": \"Stroke weight\",\n \"type\": \"number\",\n \"default\": 2\n },\n \"strokeOpacity\": {\n \"title\": \"Stroke opacity\",\n \"type\": \"number\",\n \"default\": 1.0\n }\n }\n }\n }\n },\n \"required\": [\n \"gmApiKey\"\n ]\n },\n \"form\": [\n \"gmApiKey\",\n {\n \"key\": \"gmDefaultMapType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"roadmap\",\n \"label\": \"Roadmap\"\n },\n {\n \"value\": \"satellite\",\n \"label\": \"Satellite\"\n },\n {\n \"value\": \"hybrid\",\n \"label\": \"Hybrid\"\n },\n {\n \"value\": \"terrain\",\n \"label\": \"Terrain\"\n }\n ]\n }, \n \"defaultZoomLevel\",\n \"fitMapBounds\",\n {\n \"key\": \"routesSettings\",\n \"items\": [\n \"routesSettings[].latKeyName\",\n \"routesSettings[].lngKeyName\",\n \"routesSettings[].showLabel\",\n \"routesSettings[].label\",\n \"routesSettings[].tooltipPattern\",\n {\n \"key\": \"routesSettings[].color\",\n \"type\": \"color\"\n },\n \"routesSettings[].useColorFunction\",\n {\n \"key\": \"routesSettings[].colorFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"routesSettings[].markerImage\",\n \"type\": \"image\"\n },\n \"routesSettings[].markerImageSize\",\n \"routesSettings[].useMarkerImageFunction\",\n {\n \"key\": \"routesSettings[].markerImageFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"routesSettings[].markerImages\",\n \"items\": [\n {\n \"key\": \"routesSettings[].markerImages[]\",\n \"type\": \"image\"\n }\n ]\n },\n \"routesSettings[].strokeWeight\",\n \"routesSettings[].strokeOpacity\"\n ]\n }\n ]\n}", - "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.3467277073670627,\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.058309787276281666,\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\"}],\"intervalSec\":60},{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.14288960550237473,\"funcBody\":\"var value = prevValue;\\nif (time % 500 < 100) {\\n value = value + Math.random() * 40 - 20;\\n if (value < 45) {\\n \\tvalue = 45;\\n } else if (value > 130) {\\n \\tvalue = 130;\\n }\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":30000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"fitMapBounds\":true,\"routesSettings\":[{\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"color\":\"#1976d2\",\"strokeWeight\":4,\"strokeOpacity\":0.65,\"label\":\"First route\",\"tooltipPattern\":\"Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH
See advanced settings for details\",\"useColorFunction\":true,\"markerImageSize\":34,\"useMarkerImageFunction\":true,\"markerImageFunction\":\"var speed = data['Speed'];\\nvar res = {\\n url: images[0],\\n size: 55\\n};\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n var index = Math.floor(3 * percent);\\n res.url = images[index];\\n}\\nreturn res;\",\"markerImages\":[\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic7b13uB3VdTb+rrX3zJx6i7qQUAEJIQlRBAZc6BgLDDYmIIExLjgJcQk/YkKc4gIGHH+fDSHg2CGOHRuCQ4ltbBODJIroIIoQIJCQdNXLvVe3nT4ze6/1/XHOlYWQAJuWP37refYz58yd3d6zyt5rr1mX8B7S5Xo5/0nPYaNFM1PY0gGqOhfAgQCNBGlWFFUAYEIeihigbhFdZQwt85BV5Gj9r/718R2XX365vFdzoHe7w6d77xnPkn4YpAtU0YiizNJcmPNkMQFkDiSlowHt2HNtGlTSJ6B+pTpsKTfKgTj3Pi8SMtFtEZnFs8d8dPu7OZ93BcCHtt0+OiL+FJjOiqy5K5dtLwD4PBHGvy0dKLYo8B+1+lAldv50FfmFzWX+84i2M3a8Le2/Dr1jAKqCHtl2y1wC/pEMP9ZRLBaYzF8CCN+pPluUkOKfB6qlmk/dBwTyt8eOv2AZCPpOdPaOAPjA1h9/SJX+TyGXuz0TZi4EcPBeOk+U+RErZh2YyMAyQJEoZUjFgtkCAEScgDyx1hmInTglqDj2U1X0WILaPbWvwHO1WummeuLONhaXHTf2wsfe7rm+rQDe133j/i5xPyrmCr+OouhSKPbdQ5fLiezTIYUBQGMJBgYWxMYSISZhbxgQT8wGAgDiwWxUvCiBxKhSKOqdh4OyV5+6XiEfK/kjVOXQ13apG+I0+adKpXaG0/Si0yZdvPbtmvPbAuCNT98YTBhT/8fAmEpHoXgKgPe/6gFGP0nwG8s2YykcaRCAYYQ5tKTkDVuArDEwMRF5AICS4VZ1AQBSr6oEgL36CBAvlKqIsyLOKQl5TZH4uN+TawDuY6o64lWTJX20v1S633uJNvfmvnbRERelb3XubxnAX26+5gDy6Y9HtrU/wERff1XjSt0WwULDmZEMawPOgilgQ4FaGCEygaXQMQyRMaxiUijUkAEAImIGAFURAOrVA1AmI1ZExGuqoqkVFefhyGtKDql4X4eHc6LxJof0VIVM3nVc4uXaHUPlo0Tpc2fv/zer38r83xKAd6y74iImO31EMf9REA7cpdVBY8NbA5+dFNqsCTQipkitBjAUsLUZNd4qm8AyjDMmJAIRhDzDEBEbJkBVAyJWQJ14AEaciIeSGicOgBeBWNHEeXLkXIM8UvFI4bVBCVJNfdk7STd5xOcp0LZzjIqV/eXq/4i61edM/eaN7yqAqpfzf62Nf5LP5lbko/DbCuxU4saEN1mN2kKTzQbIkuEIEWfVagRDEVkOyXCkVq0aDg2p9YYNAySVerU0WN1R27Jjo6ulMQ1V+ggAOgsjNRNEus/IiUFnYUy2kM23AcrivXh2RiTxjhx5iSmVWEWdpmhQ4qvwSBBrXVPfqDmuVsT7C3aZvKslyZcr9dpxdr81F8ynO/w7DuD1q/8y6kDw2872ticN0deG7wvQHXHmdxGK+1ibQag5ikweliIElNUAEayNYBCSRQRiYzf2rNtx11O/rC5d9dj+1aQyM2Pyz3WGozaNisYNWY7SYtgWA0A5KUVO4qAn3t4+lOzYt+Grh+bDwstHzvjA2tPfd1Z+39FTRhGpi7VBKrE4nyBFDKcNJL5OCerqUEXdVeEQb0mk8lECjR0euxe9cqBUOnoQ6RkXT78hfscAvH71X0Z5kf8Z0dH2CgNf2NkI0d0ZbmtElMtFVEAQ5BFIlkKb00AzFJqCGooQcJjv7t868P3/ubayZvua48ZlJt57xLjjB/cpTssXokK7IQNrbeoZ3pIRJm1aYSUW9cwixglZ7xNU40ppY7mr+sy2ezt7G1s+vP+EGfd/+fS/Ko5pH9/pJK04X6MUDSRapcTXkXJN46QKp1UkqNVqvpxVyLzhOajihh1DpVkmrJ7+uak/bbztAF6/+i8j62p3j20vbgXR+cP3LYU/Djg/KcsdEnIWERcRIk+hzWtEOYSch2U76tk1T6+84Tf/NCdni2tOmbRgy6T26WOiKDBhGFEQhrBhiNAyjDGiQp4DFgI8AChg1BGBXOC9p8QJ0kas3jvEcUxxnLgNpTW9izfdOqGWlve7+OOXrThk6qEHKtKehq9xIlWkvoaYytrwFYqlglgrcZxW+oXSz+ycpOLmnsHypDTIfuTNcuKbAvD2288x22dn7hrVnt/ATBftBE/CH2aCtqkZU6CI2hHZomS4YCPK+5AKHFB2ZNe2Nev/739/e9qY3KRnPzHtQp/LtnfkMhnKZDMa2oDCTIjQhghDC2MCCQITAyYxpmkhAIAZDDA7l4bOSeR9YpLEwfkUjXqMOE0QN2LU4waq9aGBX6/+d7O9sXnu3579jbVTx02dlEilL0FDG1pJG64cJX5IGr6MupY5duU1npIv7sTQ4196ytUDx8+sf+TN6MQ3AyBd8+L8W0a15zYw0d8O3ww4vC7ijlkZU5QctVPE7QhNEVlTRNYUjHcy7tu3fuuVSqXBF8z66962fMeIfDaHfD4nmUyWsrk8BdaYIAh9EFoxzExEysYoAQ5A0ioAEIpIBGZmAM459iKaJo6cT209TnyjWkOSNLRWi1GtV9A3sGPg56uvG1vIZ9N/OO9rM8jS9oavSOwqaEhZYh3khq9K3fdpXWsbvdR3MoYCV/UOVadcOvv2C/AG9IYAfue5j1/U0R5mIhNctxM8yvxLyMVpOduJyLRRnto1MkXK23axlB27sXtT1z//8vqDTt3vk/fMGnX4xGyhiEI2Qi6X1Ww2S7lCIQ3DkCxzQEQKYADANgCbW6UHvwcRaO6fAwCjAewLYAKAcao6UkRIBEniEtRqNVOrVKjeSFCP61oaqurKvqe237P2lnkXn/X/PT9l3OT9Eql2V90QN1wZdRqSuhukhi9T3Q2s9ki+NDzHWppeUqnG/qsH/+b7fzSA33ruI7ODIDh/RCH6KkEZAEINfhia4n4ZO0KzphN5005Z06aRaeOAcjP++4Ff3P/86hWTLjr08i3FfEeurS3LUTanhVwe+XxOwjAw1loLoB/ASgBrAdSAV232Gc0NyJGt70+27mlrzNT6nAEwDcBMACO892kcx1KvN6hUqWu9Xka9XsfgUP/Qjcu+Nf3g6bO7zj7urBNT1F+quxLXfUkaMmDrviQ13+8THdqYqvuLZpfq+qrJNXFDbrp87t0v/cEAXr5iduiTMQvHd2QnKDC9+bC9NUfF9kwwgvNmBGW5Q3O2SFkzAkaCg/71Nz9+2MTZ6rlzLs4Vi0WbyWS5o63N5fM5G0VRaoxpA7ChBVw3ANMq1AKoHUAewCwARwHYvzWctQCeaNUrt4pvgeha17Gtevt47+M4jrVSqZlSqepqjQpVyyX/8xU3VBHF2T//+OeOFbgXaq5fa75ENR3SarzDxDToYz846FTORbPRV7oHG9sm+qEPX3TEM3vc9pm9AfiBP53+T6Pbwo0Cd4aog4p/yXK+lDX5IDIFZDinGS7CckEM+JB//u9/e3Z8NGPTgjl/Maq9s8N2FNtcPpc1bW1tFIZhaIxJATwFYA2AtAVWh4hERBQByIgIE1Gsql8gou8AeAjAfQAeVdUvEtE9reFFIpIloiyATgARgCqALQAGmHmUtTYTRWHDhhaGYE0YYmbHEXZj//rBRc/fXTly5qGHEus2FUceCbxP4DShRJ2mvuIFboyqG5kNcNuWVM965MbNd71pAC99+vADA+MnR6F+TeAg6h1TeE/I2bbAFjVLBbJcpIDzZNke8qNf//yxKblZWz42+9Pj2opFbutop7ZCQdva2hAEQZGZXwGwDEBDRCJV7VTVfVV1BDNPUtXZqnomER2tqi8S0REAzgJwUqvMI6JBAM+p6pdU9f1ElGu1E6lqUVVZVYWI6gA2EFFijJmSiUIPsDbXmGT3b59V6Kv0dd334uLGYTPmHK7Q7lRi65DCawqviXWSrEm1PlvgWMh9KPbut+/77Ohtj/97d98bA6igo7aM+O/Ogp0l8BNFPQhyY2RyE0MqcC7Ia2jyGpksBYj2//WDCx9uk/EDZ8783JhiW5HbigXpaG9HNpvNMXMGwAoR6SWiUKS5KhERS0QqIgmAHcz8sqrOA7AdwCcB9AK4CcBvAdwP4EVV3V9VPwGgC8B4Zv4PIqqoqgPQYObEOadExC1A60RUJaLxURQaZqoRW0NEsm/xgI6u7rV9L295vmvGlKmHQ32vk0QdxfA+oYTq+Vgbi70mR4p6BEaKlTid98S/9f4MV7wBgF/66AEnFbPUz+z/VNTBiywLgxxCFDgwGQqR5wznOeR8+6p1657r6uopfu7wv4mKbW0oFvIoFovIZDIBEXkReUlVG6o6Fs2N/EjvfSczj2Hm/YnoY6r6Ae/9w0T0cVXdSkTfE5FsC8iTAZwI4DAAjxDRj0TkUABTACxS1csAzG39MHlmzqvqGCLKt1xZA0Q0QERtQRBkDZMngrcmNAeMmB08uHpxNsrz2pFtbft4TWInDZtSLE5T8i7uSKRS8XDjBX4fYbnusI2jMkt/tGP9rnjxrl+gICP4Riagrzb1ssKa4CkrYRhwwBFHYGSUOZJKo8oPP/vCoV846opSoZCnQj7HxUJRMplMgGblR5h5wHtfbE1oZAvIHBFtVtX7RKTQ4pSrnHOXAThQRK4BcIaqNkTkRRF5UVUTVf1462/TVPVSEfm2974qIm3MvBhAl6pGAEYAaBcR45zLiUiPiDxKRC6bzZpsNhtGUaj5fIG/dNTltYeeWja3ltbVcGgMZX1IWbUUqDUBbBA+OYxDPuDLSORq6KsN76s48MvzZnwwlzNDgaFzAIBAi0LKtGVtEQHlOaQCQpOHoWDWL+9+ZODCuV99cnTbmM5cIY+2JudZIpronHukxUWemavOuZIxpuG9H8fM8wDMJaJHVfV0ANcDOIyIPg5ghTHm+0S0UETWq2oCoA/AI6r6C2PMgyKyD4BPM/MggJ8COIGIFqnqV1T1YADbVXUjEfUaYxrOOcPMBVXdCmCutbZirQGIlIBwavucl2577NaJM6ftO1nJ9aY+YfEpvDryknamSNdAMQ1AGwxdc/DqDjz9k/7Nw5i96ixBSK/MhTRxJ7oUbracmWAoVGNCtRSCYOxLazfcN7VjdjK+beK4KAqpkMtpJpNRABNVdT2AowHUvffjAYgxZpNz7hUiuk9VT1LVWFX/iojuBfA1IrpfVRcS0Xne+6tUX33+M/zdew8AzxljLvPefxTA3xPRIufcpQA8EYUAFhPRSCKaKSL7EFGgqjtU1RDRZmaeGIbh1sh78s7LxM59R09um7585fqNdtqUMZOMMc4igE0DthSppcYWL80VTNbyX1QCPgNN1fJqDvzi0tnjQviObGia3Ee0JEAml+E8DOUo4pxaE4GUJz3yxJr9/vSIv+8uFAu2kM8jl8vBGNNJRE+q6grn3AZV3QRgi6q2AZjHzHNE5FEAp3vvv8HM8wFQSywvADAPwDgAi0TkPwDcBWDhcFHVh9FcXH9ARE4BMI6ZvyEiHwYwSVW/CeB0IlpERJeo6hwiepmIlnrvVzLzemZex8yDzDwZqlUikGGm6R0H66+evuPYafuNynvFkCCF4xjiBd67otN4C4GmEDAqTuVnR3++beWT/z5YfRUHio8/0dEe7DynJTUvswmmEiwxWcCDwGyee37j4ydNO6ucy+YmZMJQM5kMWWvHqmqPc24eADCzENEGAMvTNH2AiM5Q1W1E9GkR2cLM3yOiS0TkO0R0lao+zMy/8N7PBHAmEZ2C3YiIoKrdqnqjqq5i5j/x3n8bTQt8iapeKyKbjDGfFpEhAGOccw8EQdBhjPmQqk723rP3PrTWvhxF0Xgi6vHeayaTyx075fS7nlvxcPGgg8ZNIjHeSKRMdbEUIEHwEuCOA4DOvB25vSRnAfghMGxEFNRb7ZoM0HFNadFeIjvRgMFkhEDKbEl8Oqq7u3bs+/c9cXQUWo2iCGEYsqrG3vvHAPwEwL2qulZETnXO/Zm1FqoKVf2Bqh6qqr8SkW3e++tU9T4i+ntVnem9vw7ARQA6ReQ5AL9yzl3vnLsewK8APIfmovkiIrpWVWeo6t977x/w3l8nIluI6Dcicqiq/quqgpnJOfdnIvJR59wmEVlCRD9S1QeJKLHWmmw2hyAM9bhpp47q7q4d733aSVBlkBoNQGxgYPdVRZ82N5In9lS7dp42GgA483hMyUY0RXgwXzAjQgUtshp1WhOR5YgDzoiB0U2baqsPLB7z0oxxBxWz2Rxls1lh5gNVdbn3/rwWR68moi5VPZWZt4nIvgBGquoRAH5BRH+OprH4oYh8XlVPQXMvfIOI/BJAFxF1qupxRPRBIjpKVSe3dOtdInKbqj5PRIe3RHayiHydiMYDOIuZfyIin0HTfI4kIgAYa4y5UUQaAI4QkY8ZY5YR0aGq0kcE8k5NNS4t665u6G9r47xDCi8pqabsNbFe9WkoRvU0upYl8GunnqebX7kZQ00O9DipLbKjRfQTPWnXYyBTBxMBBiIML2IVkt20sf6B46d9rJjJ5chaQ0EQRAC2pWm6VlVXq+rZIvIXSZKELcX/Y1U9RlW/AWC8iJyqql9V1aOcc99W1SXMfAmAh1X1qy3O+rKIHCMiGRGptUqude9iIrqWiC4brisiDxHRt1X1KFX9qnPuowDGe++vUNUPishNLQkIiOjPVPVs7/02EVkLYHsYhtYYg0wm1FNmnZPftKF2lFPJisCIkhE1DFiFaNLr1i5R+PntGR5lFMcBLWfCxxbhrgkjgqMAjCKgkrWFX48KZ7RHJm8CziJLOXJpUNu4omAuOfbKOMxkKBOGHIbhHBG576qrrtLHH3/8QmaOdtdd/5tIROLTTjvtyc9//vN3BUGQs9aOA3CyiDxXr9dRrzfo2gf/Ljt1TpyYIMnWtQ4nVW2kNd+bri41fOlMADkQerb1p4/f+WGcaS9X8HOLUQIwCgCUdFGi6ehBt7k+3k4DqQ8cOd2+mQdPnP6xijHB+MAYhGEoqppL03T/J5544iRmpvnz5z+4Zs2a1dOnT5/+8ssvr5o5c+aMWq1WSdM0VdXORYsWHW+tXXbmmWcONV2jQG9v744dO3b0jR07dvSIESNG3HbbbbNFpHPBggWPtMTvVUREWL58ee2VV145bcSIEU+ddNJJ1RY4unLlytXTpk2bEoZh2N/f37dw4cKTrLUdxWLxvnnz5pnf/e53unDhwhPa2tpWnnfeecekabopCIIMEYGIyBjGCfufvmbpltuKY6a4LKkzCh8PpZu913g0oIsAOhOKMQTElyvYPrsY43IRP6uK8wCAYHrUo+gpiXoaG+LR0X5VaNgxNEAHz5pz6PIgMGBmBTCKiJZVKpUjjDEmTdPG/PnzPwSgLCJHoLlY/omqXgLgWSJauHjx4uNPP/30obPPPnsAwGNoLl+O32Xdt/a3v/3txnK5HM6fP/+3aJ2JAAi89zkAUwGcdOqpp+YvvPBCnH322fEJJ5yQA3CH9/5YY8yft0C+SkTmP/roo72NRqPjhhtuODCTyRTPOuusRy+88MJVd9xxx8cWLFiwiog+oqp3ARgVBMEO7xVzJ70/v2jdHbNGqu/16uq98WakmuQgANhsU98MRQwMP7N0iYxhUuybD/n3WzqlAMROROElzfY3NrXHrtTNFHTkMvkiGQNiZhGZ7ZzbPDx5IoKIXK2qZzDzd9F0T/0pEV2qqoeKyN8BwLZt27ap6hmq+l0RmQXgZhH5iohcpaqrwzA0RATn3DXOueta5buqeoWqnqWqT9dqte8DwPbt2zeKyBGq+l1m/giA7wL4map+jYj2S5LEA0AYhp0AvsvMp5577rn3Axi/YcOGxaoKEdkCYBYzqzGEMMgUWILRjXSopzfekFUf5wUKYXYQCoZhykcM08C+DMUMw7Rva8sHqHZCJFD1VtTDaYLuoe3xrLGH/Yu1NiZVtcYAQEVVy7vpmPNU9VHv/RUArgZQ9d5f473/qYj8OwBMmDBhPIBnnXNfAfAj59w5AK4F8DURmcfM1JrY/4jIrSJyq/f+XlV9vmVMPlEoFC4GgM7OznEicmPrB3hJRC4Tkc+IyI+897cFQWBay5lrVfVKVX30lFNOOUZV/aJFiz7YMi79RFQiIgbg2NrazHEHf7+70q1eGiwkROoteQkhOmIYp8DQBGUcYIVwOJMepCCAkBCooCAnUPVwXoU1rrXVoyi7nwgoDO1QyymwzTn34d7e3p8B+NsWFx4AYLP3/l4iuoKIHhaR/yaiLw1z6rp169Z57+cR0bUiAiIaVNU7ReR5Y0xcrVbPbf0ek1U1DwCq2qOqG4jofhHZUi6XAeC7IkIAvqCqIKItaG4LZ4jInxERvPevtK5fY+b7W+0eBGD78uXLx6nqd51z85i5G0Bore1rNJJsxuan1EumFo3w3mtKSupAMASNRJEACBk6ixWphWCaKs1tqegVUIWyiBcPIYhRQlLKhQccNDtW9YEIh0TkiciJyGFtbW29LfCCxx577PtHHHHEhdbabd77bzLzFap6jPf+X5o46Jf333//qWh6kP+P934HMx8F4HQA53rvkc/nl9frdYjIQbsw99SWy6opPvl8BQC6u7u3ENFfq+poVb1IRK4iIvHeX7dy5UpKkuR8Zka9Xv9WNps9n4j2B/DNkSNHnrV9+/ZRIvIhIjpMVZeoqlfVEcyQ6WNmpQ8+nyva9m4IO/XeQ1XFE6UKfYkUhyrTEVDEFkAWO4NuZAuAsPnDKlgFzih8ku0cU5y4NQiCxFrLAPYDUCOizxpjrgAAY4y54YYbvtwS5f1E5B9UdSgIgloURR8BIESEO++8c8qmTZtetNYeHYahdnR0wHv/pIhsrVarvX19fQsA5H71q1/dYq01pVKpkCRJXCqVaGBgwDcaDdfX1zcRwDELFy788JIlS96XJEnBOQcADSIKmfkSIsKwpXfO/bmItBljLlHVa6dNm/bIE088sR+AMUT0WRG5kIgmWWtfIWPcuPZJDJ9r90hIRVTEq5KAlBIIdYH0UCg6FMhZUvDvjSDVnZBhUhUSUijICxHCbDFXZGOMqKoH0KmqQ/l8/ptdXV0/rlar38rn8zs5hJmJmUM0jyPb4/j3h/ze+ylLly6dgr2QaepX3Hnnnefv7ZmdoyUamyTJWABoHvTtmbq6un4xa9asSQCuA7DSWvtSo9E4zHt/dbFYvKLRaKwF0E5EwoBENlKVMOPFkcJDCRBVUlEloLQTLgWz1987FAhImCECJVEh8Z6cdzBk20ITkIg4Y4xX1ZFoHuJM3XfffT/S29uLLVu2oFKp7HQ9/W8ia+2RzHyGqv6TiPzjsccei97e3kxbW9uZACYTURVNb7mIiIYmJIOwLUWqTqQVIqFEDFHV6nC7orDMBB22LOzhWbRC0LJRLalqGYqyQWAJVDPGVJIkqQPYrKq9AGCMmQoAaZpix44d2Lx5M/r7+5Gmbzn4822jVatWvei9/9M0Ted77/9j5syZawAk27ZtswCgqt0AtohIzRhTssZWDdvQkA4RtETaxAOqZSWWnXgR1Kr8/kTbG2ThtaAE9QQSZWIQ2EilFteyhoJCa4lxYMvf9xry3qNUKqFUKiEMQxQKBeRyudcVsXeC0jRFrVZDtVrFzTffnOnp6Tl2/Pjx944ePXrt9OnTzyGirY888sjLCxYsOERExhPRDGvtswACrz4m60pOqIMIBIX4ZqCYAWsZLXumAtid6z8A5DSvlgkKFkcMiBERqHUDiUu8994SkQCoEFF+jyPfhZIkQX9/P/r7+xEEAbLZLKIoQhRFbzugzjnEcYxGo4FGo/EqCejp6Tnv5ptvfk2dH/zgB8sWLFgAVS0CqHjvyTlnq2mFYF3VORnJICKwI2IFI0Qi7TCtLaYCVgnbAdoA6GRhaoPXhipIVJkEUCXP7CrleBAd2RHsvYcxpopmfMreaICZN6LpQWYRmZSmaeeuk7LWIggCWGsRhiGstWBmWGuxqwUFABEZ9ilCROCcQ5qmcM7BOYckSYbd/XuiTczcT80YHHjvZ6MZZ4O+vr5hx+14Va1Qa/M9WB0Asa+SUCcIRuAtg5QEBKDYrEJrwdhiIXhBRQyIJkMxQxQvkELh4RUq4kCJ2VHdOLiOx+YmmTC0trWwnQOgsvtoiegFInKdnZ3rRo0aJT09PTw0NAQAm0VkzvBzw5N/B0mMMU+pqhk7dmxXsVjkzZs35xuNhojICDSPRpPt27c/WSgU5hLRC95722g0aOPgWnbcW5VUBYCSJYBBChgQzWnt2J4BsJyheFkVr7Q6Hc2kZYU6ARSejCjZFN259UOrc6reOucMEfWpqnXOPQIAhULhN8PgMXNl3rx5Y4IgOIuZz46i6KyTTz55JBFVmXnFO4nYrmSMeTKKooEPfvCDs40x8621Z3d2dp566qmnxsxcArC1s7PzkVWrVi1X1QBAv/eeiYg2DK0upOgpiCBQIlIBBOrBOgTCCAAQ0jUQrGS1WF1vUPewLlTlKoQCOARewOqVUgzmtlXWTWuKiqiIVAAgjuOtuy1bgtNOO21ET0/PhO9973sQEXznO99BT0/PxJNPPrkDQAO/97C8k7RBVaO5c+ce19nZmb3yyisxZcoU/NVf/RVWrFjx/kMOOWQ9M3dXKpVRjUYjbKmGinOOnPPYWt04PZGhjHoQCZigAQsFpFwbxqlRpx6k6LI6gK5Kpz8zm20d0JHWQFAYTSUlALDexSNdEB+Y+nQxpZRlppSZ4ZybdPvttz9QqVSOt9Y+SkR+xYoVxx522GF4/PHHceCBB2LZsmWYPn06nnrqqQOZ+REiekZERr+T6BFR37hx47rWr18/NwxDvPLKKygWi3jhhRdw5JFHolarzXvuuee60jSdYFordxFJnHNI0rghiGc4jb3xUDEQEngyYEBrwx7KcuJHZzux1t79KZQ++iv5AHTnCadVBZGQhULh1SsIMfoe7KlsGRqTm5Q1xmkQBJtV9dijjz766f06bwAAEgVJREFUnpUrVy4EgIMPPjh300034bjjjsOaNWtQqVQgIjjqqKOwZMkSzJs3b/Xy5cstgFUA3rZF954cr6eccsrYxx57DJ/85CexcOFCDA0N4cQTT0S1WsWjjz4azp49+4l6vc5Tp049TVU3eu/hVXVbZUN/TH33k8c4DVRIiMFEohCjCIdXLC6VY+44DV+zACCEXiiWgnCkEp1EpKsEqqTEIsTq1Axg+eCy/kczp+QmqDZfuXpRVedNmjRpx9VXX32hiEBEsHTpUtx5551YsGABnHM47LDDcNNNN+GAAw7Al770pc8NPzdsUXe1rsOA7n4dBmjXK3NzgbHrZ2beWQDg7rvvxq233oqLL74YS5YswY4dO/Dkk09i7ty5uOCCCz4bx/FPRGSUiNydph71ap2W9T9eGGgsr4iqZSVVsLJ6Z5lIlU5srfmWAlgHtE7lDjgP5SjgAWb6MBTtoroMgpwoERTwniiJhwq5aPrxB+YOWwuQIaKEmWd573NBEHSoKosIpk+fjltvvRWqitWrV6O7uxvLli3DV77yFRQKhVeBtzcgd/2+exmm3bl3dy4kIowfPx4LFy5EpVLBpk2b0Nvbi+7ublx22WWw1ro4jgsARgJYVq/XUG/Uk2fK95+ypXxfrESGGUIEMhYGTP1ovQOYOr2+kcjvVt+K9c130cp4slyX4nDnBqYbRCAGkTZXUELIVtPeezeUu3rjOEaSJFDVpwEcmKbpLcMTnDhxIm644QYEQQDTPDvBNddcg3322ec1IL1e8d6/qryZOruDffTRR+PrX/866vU6kiTBAQccgOuvvx5hGKI15hki8lTz76lura/fUUt6F4siJIKCiREAakhB6BnGp1ST9lwbngJ2CfE99Zd4cPzIcDqg4xl4wQl64EE+BlyicCnYanHz4RMumviR9vO7C4UC5fN5JqKzVfXlKIomtzzGr5nwGwGwOxe+ngi/ntjuXowxe/s+0Gg0+ohofxG5o1KpoFqv6+LBn496dssPt6dcmWAtlCOCNRDKgJgxEopDoLRl60Cy5p5P4Hhgl/A2NbgmTuUGBeCBOUTokVZAtyiIFJSk5QmJlJKeyvaeer2u9XpdVPVxVZ1Zr9dv25PI7Q7M3sDbEwe+0Q+wt/b21vdwqdVqv1XVaar6eJwkqNdj9JY3bW9IKU5cZRwUDNPcuagBE2G7Kg5RAKnI9SD832HcdgJIARYOVdyknXtjoTpBoaRsTPOMHQy7fMutQy/qQzOr1arW63VNvd+kTc/NfO/9I3vTXXub0N5E9/U+v57Yvp7+VFWkabpYVc8DMJSm6aZyqcSNRk1fxOMHPb/5v+pQtWwgUBCxErGCiOJhXHYMuRkU4r7XAHj3aYhTAaC4rakI9dNkMMSWPBhMSsRKmjRKIyuuZ3Bzfe32crnGlVJJReQ+Vc3HcdyuqgPD4re3ib1ZHfhmVcDuYO4JxNaYetI0HYvmMen91WqVqo1YNqVdW2uutz9NSp3KTNpcxMEYgjEYVNULmvVxiwLVu09D/BoAAcAZXL6j7F9SBVRgiUwPkRJYCQaqrEoMWrrqp4WN2ZfmxXGtWq7UqFwuJyJyP4A5cRw/qKryelywNw7ck+58I336ZvtR1Uaj0XgewMEicl+5XPblcpXqtXJtk33x1KUr/6MAbnKdgQKsDFUVMTtUYFWBvpLvohRX7orZqyJU192K6tSz9Qv5HPcQaCpBZyvjRSiyEFIVkDioiBbL1W3LglGduWJ9LKDExnAtCIJEVU/w3t/MzIfsbiD2dn0jHbkrF+1qSPZkXHY3MMNX59ydaB5ePdNoNLZUqlVfrpSxOvO4earr5xvqvm8iGfggBFNIyiGYQwwQ4xwABqqLhmo+c885eJVf7NUx0gDE4iv9Q/JYc1+MDABvDJQs2DDYhlBmxD2Da6YNxOulW9dsr1TLWiqVtF6vrwawXFU/7Zz7TwB/FCf+MUuW1ylJmqY/F5GzVXVZvV5fWy6XaahU5q26asuA22L7hlbvR4a8NVAYKFsgMBACJZDm7mNHSZ41HpfujtdrovS7bkV58p/oRwpZ8zIIhwM0C0SLoBipCmqNnaHAhq3L7MT9D9mfhjIrrYRt3nu0fG9VAKd673+Npq8t82a5cW9ADdOb4bZdljfbRWSpNt9BeSJJknVDQ0MYHBqiwXRHd9+IriPvffpa4YBCE0I5grCFMRlSGFoF4DMt3ffDUtXLPfPxyzcEEADGnoNH01gWFLNmChQhgTJEOqiKQIQEAiPNU09+Zf3jfZNnH3yY9mVWasoFL16sMWVm3gzgNO/9KiJaq6qTdlfyewNv9+f+QNCGPz8qIgLgaFVdVK83egcGBk25UtWBel9f/4Q1x931yFUbYLWNIxgOoDYgDSJYE6IB8CEEjFKg1D2QdscVfHn9r/EaB+YeAdx8B9z0+Sgz8HxgeR6AMVB6hgzaVMk3Q/2JSQHvJOra+GTXlMPmfEi6o+d87NpTLyTeN5j5ZWae6b3fV0RuIaKZqmr3ZJ33BNzuAO4G0B7vMfOQiNyqzcBN8t7fN1QuN0pDJVQqJe2v9u2oTt9w0l0P/uNz3iQjghA2CMmEGXgOCSYDIqJuAk4AgHrDf7We6u/uPx97zO6x13fl1tyOtfucqRcXM+ZFAHNAmA2iu4gwRkBKos0jAVXy4vKvrHvslWlHHHZk2m1eQKJ5VfXOOauqG4Mg6FXVj4nIalVdpKoHqSrtsrzYed1VXAHsDaQ9caAQ0S0iMoqIPkBEDzWSZHWlXI6HBkvBUKWsQ2nf5uSA7SfeueTqFxPUxtpQAxMSmxBqAhKTBZhoBYALAUCBW3ZU/D6Lz8E1e8NprwACwKQv4nf1fvlUMWsJwEgC5oDpIVJ0EhGrJ6sAICCXuvYVqx8uzXj/YZPSWFbWelyHeA/nPRLvqwxa3XRN4COqugrNKPwx2ozifxVww1y3K4CvA95WAHdQ8xWHDwJY4b1/tlwupwNDVVTKQ9rfP6j19h3dsv+Ow29bdEWvUmO0CWBshowJCTZL3kQAW1pPTb1noPTK9oG0no7Cp9b/7LWi+6YAXP8zuMnn4rFG4kfnQ3MYgIgIU5jxDCmKCigBpE1xZlEfvPDSErffrFkU7BNQpSutxQ1PLo6zSerFi9RV/CvMXFXVQ1R1H1VdhGaIbxnAzgQ5u4vtLsUx8yMA7mPmbQAOJKI2VV2XJMlLtVqtViqVaLBUlUqpn0vloTofOhBVMptzv1h4dd4Yn7cR1GSJwwhiQhIbIjUBthBwJoC8ElzvUHqzKL5+/+l4zQuGu9Kbyplw4m04Ix/xjI68+W6r2gZifdI1dFSaEEtdOW2AJYG6hnqXEMaOnL7ptGO/+L5kjVks2/JjM5nIZKJAoihLmUyIIAjIGANjTEBEHSIyWUQ6RWSdqm5V1YqIpC3RDImoQETjiGgKM5eIaKOIDKpq4r2Hcw6NRgO1egzvUq3V6l5Hxhuys9OPP7T0lke7tj41nQNiG0FtBmojeBMR2yzIRNhKQh9U6L6kkMGq/7t6Ii8uXoDfvRE2bzprx0n/hc93FLiQi8x1zYq0CdAHvcdkV4V3Dupi9b6OgosR+wRGvU3PPuXSHcXcPiMGnvAvcJIZlwsjG2UzMESUzWa16SExZGxLGFS9sVbFK5SUAGBYWYoIMzN5BbnUgSCaph5xXCfvvSZJouVaw1NWejrfL3NK1a07frHwmpFsXcgRvA3hTRahNeRsHmKaXpZtIDoa0P0AoBb7SwZqEt+/AP/6ZnD5g/LGnHwbvtlZCAYzAYbzJwwo4U5xOl0aUB8jcDHUxUSuoQ4pJE0gmbCt9vFTLm4UM2NHDCxNlidDweiQOAyCUDkwFLBBEFhSZrVEqkDzHLEVAiA6PFBFE0pFkjhS9YjjVJ1Lkfg0sZ3SO+rI8NBSo7vvznuuz8S+lDMhwBbWhmRtVr3JgmwAmAhqAlolij+h5svfqMW4ZKiaFu49F1e/WUz+4MxFJ92GS3MR246M+bYSGEAizD8mJ4d6p+oa8L4OcQnUJzA+hhWnqU+gUdA2cPKxnylNHj/rmOrW9N7+F5JGOiQjyXIYcgC2zRejiVXFw5Np5Y3xMGxgxBMJPMSlFHtPUI1NG/eNmhNm8uODUzZse+nB+x78WVs9KXXaDMgYspyBNyG8iQATwIRZwIawYPOCQj4LICSFDNX9V6qJ5O5bgH/8Q/D4o3JnnfhzfC6yvM/IdvPXADpaLd0KoaJPNS+xmjSF1QYkTeEkVfYpGR8j9Q5WRKvjRkztPf5DC3j0iCkn+AQvlDdUu6rbXaPWn5KrCEEErTwXTTKALbDmRgSaGxNk26bmppoQc7p7ux546PE7ZHvfutHGUJ4DOGMRmEi9sSQcwgYR2GTgOCRvDFXVaJUU81sA9PcM+X92Trru+yT+8w/F4o/O3nbyrTiaGF8cUwgOIMZRreZegerDgB6YJiQSw0uqgYsh3sFrjMB5eE1gfAovHka9pjaM+ke2TxiaNnWujBkzOcxnO/KFXKHNBpnRAODSRm+lVh6q1odqPT0bkjXrnuW+oS3tLo1HsKGADIQDsAnhjEFAFgmHsDYCmYBSG4BMRgMQvQTQcYBOBwBVPN5TStd6hxvuPx9L/xgc3lL6u5N+hpGwuHl0u33a2N/nDiTSXxBIRHWCNMilMdQ7DSVF6h1YUxXvyKhD6h0CCKCCVLxa9YASKYlyK/AOIJAyCUFBDGImB4KlEEoMbywCCtQbQ8QhxFiEJqDYWLDJakBEm4g1UKFPDI/Rq16xY9AdZQzOXzgf/X8sBm85AeM5t8P0eXwtItYRbfZToOavCyDxKj81RCPgaKJ3iL1TAw9xCVgdvHcw6uBVm/pNvQIKpwJV2pkKBQCEFKoMYoKFITVGQQxPBsZYeLIwNoQQw3BAjiNEzNioQKzAebQzkJRW9lXcbXEqctx5uOryYUv1R9LblkP1+JsxjS1+MDJn7wkDuhKEHACQQqD4OUgExJPFq/EpqTglcXDqEXoPJYETDwbgROBVAQY7ABCIJQKYYQBYZogaWGMAMkhhEJiQPLMaG5BTlvWUsgXjvJahAxS1RqpfH6i5eYjxhfs/i7clj+rbm8VXQSf/HB8T4LOj2uwzgaF/0GZ2oeHuVqjq48zIQzHee4QiSLUZgwN4kDYdt0Kkqq38BM1XhYnAMMwKGDQ979y0rERIRbENQJWIPgDorF0m2Ei9Xt0/5N4njH+//zzc9XamRH5H0iAffiOC9gLOVeD8kXl7bxjyxYC+OqMv0VaoPsCEukAigNqg1EEEFlWBQKHUFC9SBoOYiEUhRDoIaInBiSgyBDpJoeN2m9qG2Mv1/SV3iir+s1zFbc9chLc97vgdzWR+uYIfugUnC/C3keUlHQXTaQiX7LUCox9en1XwIBENCqTcvM1FVe0gSAcMzYVgxN6a8IrrBit+IHFyrCF850Orcf/ll781Pfd69K7l0j/mJxhtLb4+ot2uDy3t1T30Vihxeml/2U1WxpVLPol3PA088O7/MwI6/ib819j2YDOb154vvBVSxfXdA+nEBz6Ns4G3T8e9Eb3mUOkdJsW++NT2UjpHVO/V5vrvrRfVh7f3pTNLdZyLdxE84N0HEEtOgMsRzukdcBUV2vRWwYOnbTuG3HZXw4J3wki8Eb2uQ/WdojW/RLz/n+CluKaZTMhzm4eJwB9aFHADFf1X7+X6h/4MG9+LubzrHDhM934KLyhoaSPB3/yx3Nco42+811UPfBbvWvD67vSu/0eb3enEn/K17RkeNExXvPHTvyfxeuVQQ0be9zn50hs//c7Re8aBw3T/Z+TScl3niuBm9cCbLLeXGjr3mA3yl+/1+N9zAEHQ6oA/rxLLBPF49o1Fl54vxVJ08Ge/kwvkN0vvPYAAHv8K6ur8BbVEnlNF6XUArNQS/ziJv2jJ5/Cm07W/k/SeWOE9UddvUJ5+pimpYhODTtyT1Y29fsOrv2fxhXj+vR3t7+l/BQcO0z2fc0ucEyeil+7OfV7xFYXI4gvx4Hs9zl3pPbfCeyA67cfmFiaziVX/BgCUcL1XGf27z/vz8S7vNN6I3t23oN8caW0//+lcF/0PC+4VIBJgZm2aPw3/y8AD/peJ8DAtOQEuZLfAQ0sK7Q0rbv6SE/Yen/L/017ojH8LZ5/xb+Hs93ocr0f/D6s769KBP+5xAAAAAElFTkSuQmCC\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic3bx5uF1Flff/WVV77zPeIQMJIYQxYRRBpBGcQFEEbVQQUXB6xW5tWx9+Cm07IYIitiJog2P7qu3UCN22aDs0KIIyg0CYyUhCyHiTmzucce+qtd4/zrkhQIIogz6/9Tz1nHP22buG715D1VpVS/gLktnZjg3P2wGz3RC/N9hBCHtjMgOxGjDZv3UAkyZim4EHQO4i2j3UkxXUb9kkcrb+pcYgz3aDNvLjOah7BSZvRnwLX/8D2axILM0Dtx9ODgGGt/P4GNgtqD6A764i3+iJ44dC9CD/jaRXyqzXrHs2x/OsAGhrL9sB8W/B7ARKwz/H7TAE2btAZj89DbAW6X4HHRknnzgW7KdU5fsyeMKmp6X+J6BnDEAzhLWXHYz4c3GlG8l2HgL3frDsmWqzR5KDfpnOykkIL8D4OHPecIcI9oy09kxUaut//CKCfp7qtMuwwb8DnrOd5nOcXIfJCryAOgEpASUwh5HiBMwKEAW6YF1chIghthtqL97uSxG5C8a/TXvsBLx9VGa/8Yane6xPK4C2/kd7EuWblIZ+itU+BMx93E1OFmLchqQpTmaDA0kAlyDSwSwizkAFfN84RAfOQASLCVACDVgAESOGDZjmSDwEtYO20bVVaPMCionjiPoe2eXNy56uMT8tAJp9I2X10GfxyQTJ4GtADn3UDd6NYv5niKtgyQxcYjinkCSYi3gHikeSLhDwXjHzTNlWB4hEYnRAgoUSmIIqxAQsYEFQA/JRNHZw+lqiTn90R/VGYuNKQl5j/cTH5JD3FE917E8ZQFv23b0oJf9GNnQd8PFH1y7rkORKLJ2BTxJcAqQOSQyXGEiC+IB4wZxHXMABeIeZQ3q/MBQRhdiDNGqCaMSiYTFBKIiF64FYKBbAigKLD6PFsYjt+uhOcwHdiUMRe5fMe8uSpzL+pwSgrfje+/CyK1n1dcBeW/7wMoZll4Kfhy95SARXMsSDSxxkIInifIK4gHpH6kAlggjOOwTBJEFV8FIQDcQCGIh6ighOFdMELQKYYF1Bg0KgB2ZuxG4kFqvw+cmoDG7V/cXk7Z9iulx2efvX/1wM/iwA7bLLPIe1v4m4RSTJuWDJI/+675OUB3ClCmSC9yA1cKn1dF3mSFLDRJEsAYk9wJxSTEzQGW3TXlEQC6G7sde/0gzDZ0Zlt5Ty9Arp4GBfnBWCgxghOkIhkBsxGK4QYgs0gHZAQxPtNLH41q2GH4jhNFRfwrzK20ROis84gLbkohLp4C/IuAXso1v+UFmLlK4gLc/Bl4AMfEWQDHzZIAFJhKQMLgWzhMayjTz0kyYbrt2T2Nq3a5WFE3HWqgYzNxeU81ao5ADVpJ2ldLI6G6YN+o3zStI+CF+9n1kvWcYub6hR330mIgEtIHSAYFgOVkDREegYmoO2QfOHCJ3X4thqDirn0rUX0Kr9rex/Uv6MAWhLLiqRDPwPafdBxN79SC3yS1y9S5JVoQpSEnylB5wrgSvTAzCt0Vqzmfs/32By8cs2xZ2vXGJHjo/KnlVz1aEsLZsXKVQkpIlXTHo6T8wFjU5iTELULFpEQmN8Gsub87lq2gy/5pUM7ftb9jtjgNJO07DQQHPBerMeYqsnztaGmBvabhNbVYivemRwfJWitADktbL7OztPO4C25KISvnolWWMN8OZHasj+L5Luih9QfAWSmvVEtwquAi4DyWay6aYHuO/8A1phYOkd9sbVTbdgVlYqJVk5wycpWZaRJY7E+2i44L2Y0Zv8CkgMCMQ0xOCKGAndnCJG8m5ueacba7pkw/Pksp2rvrkH+3/kXqY9fx+cbiA0HbEDdIzYBmvRE+12l7y1GRffsRUj/JBubR6xdbQsOK37tAFol13mOXjzzymNrQL+/pGnS1/FZXvg64IfAKkqSTnBVRRXEaQ8g7HFK7jnU/NHde7tC5N3RpKB4WqpKuVq2cpZSpJlkvrMSqVEvE8ty9JClSJJJIr44BwWY0xjNC9iWZ4HH2OUdrsjzpm1Wi3X6RTW7XZpdNviwsToQfodP92tPpj9z1nG8Pxd0M4mYtugEygaGdpWQgO05aCxFI3/+Mhg5WsUQ3vx0Npj5GVnh6cHwCVfv4x0ZAViH9py0WcXIpUDcPVIOiBIFZIauKrgKg6z2Sw8c0m72XK3uA+MuMq06dVqzSqlTGq1uqVp6iuVsqRpGtIsM++ceO8NEQQi0AGmuKAElAEfYxTAQowUeZBu0U3zTkc7nVzzvGOtVpdGqyHa3rTpUPvSjpVyreDgc/dGZB3aVrRlFE3QlmBtoxhXtPUQlr/nEVTceRQzd5c9/+GUpwygLf7Ke0jHykjrS1suavpV0vqeuDqkdUEGlKTsSQYUqcxmcvky7v38c+6yt/xqvHTwvEp90OqVTKrVitVqNcrVasiyTBLnUhEBGAXW9ssqYD2QA1MT3bQP4g7APGBOv0w3M4kxxm5RxG67nUxOTkq7ndPO29YYb9pQ55a1z3U/Opb9P3wXA7vtgbbXow1HbBk6aYSGoA2hmFgK+ggnxoEPYENR5r/3y382gHb/xQeQFm/Gr/0ImOuD9zXS6p4kg4ofBKkJyYD1gCzty9IfXlVsumuXm7NPrC6Xh6sDA1XJyhWmDQ1ampa0XM689z4FNgP3A0uBdr8vSk/veXpceFj/9y39/6ccAq7/vQLsCewHDMUYQ7fbtWaz6VqtXJutCWt3OtJubBo7rPjMXsmMA5cz/60vJ3TuJzQEm1TiREJsRsKEElqrcEWfE0UJcz4P2Q9kwfvv/ZMBtD98I6XW+QXZkj0Q9uzdLZcgA8OkdYdMF5KakgwK1AWfPof7Lr52vJk27qufVq0PDKTltOoGBqo2OFi3crmszrkB4CHgAXpc5vpgSf/7MFAD9gVe0AcHYAlwK3Af0AQm+gDrVmV2H8i5ZtZpt9s2MdGwRqNJq9WUVqsZ9m1f1BqqxAr7v++laH43sWHEhhAaRhjzMBkJExNgJ/XhWUK+YDVx9FWy/9nbnN4k27oIQK39FUqLFmLhlX1beB+U67jMIRlIGiF1kApen8PCzy0cYZ91Dw2/dadp9ZqrVWtFrVZLa7UyWZaVRGQc+D2wiR73zARKqhqccwqIquKcmzSz14rImUC135uWmZ0nIrf0fw+oqjjnhJ54d4AGPU6dJSL7VqvVoSRJOlmWSKmU+KxSssVjH6zOa/5g8453nn8bzz3tEHzpDrTrECeQKnjBJSmhdQ+izwEWkNx/Oez9ReB924Jpmxxo91+wl0rjjc4vO7d3wQKu8kP84CyyIUHqgh82/ICQVA7k3m9evybstXb98FtnD9Xrrj5Qp5RlWq/Xnfd+AFgILANSVfVAHZjVf4E1EZlhZs8VEWdm3xCRD/QBfqSjIhtU9SIR+XszUxG5T0Q2quokPV25EZjsv4wA7AI838wajUaDZrujk+NjyWSzyZzJ/3pwTnrffPb5u79B2wsJEylxUonjkTAhxPENaOcURBIADfM/6bT+H7L/6Uv/KIBmiN1z/tWS3TqESM81JMlXSQb3QIYgGwQ/CH5QoDaflT+7cXNjYGLl4Lt3qA/W3WC9rtVqxdfr9QxIVPVeYKNzzoCOqjpVLSdJUg0huCRJusC4qp5FT7wPAlYCP1XVkf5zs4DXAbsCtwN7OOfO7nNiRk+EWzHGwntvzrlSn0N3APYzszjZbE50Wu1sfGKcZqNtu05+ffO0aitlj+NeSphcTGyATkCcgDhmhInlWJziutst/E1D9v2nIx/rmH08gPd+7ijSpc/BRnpW1+RWXGUd6WCGH+5xXjJo+MFhRpcu6q6+a2jJzHO65UpdBgeqbmBgwMrlciYiqqqLnXOFqk7vc4WPMRpQTZJkB1U9FEhF5Fwzu9DMbnfO/VBEXqyqrwGmHKW5c+4XZnatqr5dRA7y3p+uqh8DVFVvE5H1QAtwIhLo6dSNzrkKsEeMMQ0h5GNjY9rpFH5iYizutemTrjTvuS0G91iANcYJE544HoljQpyMxOZcsAN7SM3+AHHPhbLvP/9ua7zc47jP7OPEhz+KdcA64MIdSJKBBxFFpPcGigmx1Tc/f9G0syZrtZofHKi5en3AyuVy0gfveufc5qIo6mZWAWaZ2Rzvfdl7/3AI4SozGzCztqqeG0L4ELCPql4QYzzezFRVH1DVB8xMY4zHq+qFwHwzOyOEcF6Msamqg865K2KMK0XE05vaTDOzUgihqqojqnqD9z5kWZYMDAwmpVJGvT4gS6af1baHbzyY0FQwD67nzBVngMNz4xYcbOXHLMRzzEy2CyB3nfdicQ/8FOvMRrsQu78A2xWJYCaY6zGwxf1Y+quwbNrp19QHB6u1Wo16vUalUk5EZGdVXaiqQ3meO+/9BhFZrqqr6OnAE83sH/ttV4HvAB3n3Plmttg5d6Zz7sNm9i0zu69fvqWqH3bOnWlmy83sAhFpiMh/0JtgO+/9e1X1TTHGATNbb2YPOefGVbUEDKvqQmBOqZS5wcE6lXpdqgODlWXTP/Iblv48RePeOOt5wrUvpZrvDt3/7WMxS9zi/+aezx2+NWSPssImeqbY4j23XJDSeszvBAJODFHBWcrIA1c1sr1zN7DHjqVKhUqlQqlUcsA8M3tQVV8MNEVkjqoGEVkLPKCqVwEvA3Iz+yDwG+BMEfmtmV0hIifHGM81e3T8Z+p3jBHgTu/9h4qiOM4591ERuTKEcIb0JCMTkSuAGWa2v3Nujpk5MxsFEhFZ7ZybWyqVHq6pOrQaJuPOcxvNve6sjy2+l+Gdd8EkIAJ9LFHWYFMLokXvN9vzQWCLE2ILgPbA53bS7qrfi3WP7l+6GvxcJPRG4Kwn5LE1l/FVu63e4fM31CsV6pWKZVlm3vshVb0S2BBjTAG890PAAar6ahE5EjjPzOohhLO89xeaWQ6caWZnAS/vA3Wlqv5eRFqPAbEmIkeIyCtCCAeKSEdEPqaqfw/MVdXTReRCEXHAe4B6COEqEbk7y7LNIQRCCEWaprO894eWsmyTxuhDCG7NzH8s77X+jBcyML2AsBIxQ0xwKgTdCcl/h9kRwAJh/fds4blz5aAzVz+aA7vtE527fi7WXz87/wCwO+ZAVIgFuODZuOqmkfrrJirV+k5ZkkiWZWRZtqOZbYgxHglUnXOJiKwMIdyRZdnVIYTXmdl6EXm7qq52zn1BRD6gqv8CnAtc65z7sarua2avF5GjeQyJCH3R/IaZLXLOvSHGeB498f+AmV2oqqu89/8nxjgmIrPSNL0qhDAjhPBiM9sdiCGEpvd+JE3TOTHGDZVyOSpUNtVe/fMZI9cNMn32LliMmBnmIs4ghnshHtHrybU7Iq9/A3DRFgDNEL1l/U6uUhzRN8wbUD+vF8PAepecoHEandburbkvv7mSpZTLFSuXywnQCSFc1xezIefcc83sOOCQoig+18fgy2Z2gZl92cyON7MvAb9wzl2vqqfHGKfW2rmqLnTOPaiqK1XVJUkyD9iD3grlPX0wNwIfU9WXmNmXzGyVmf1cRN7rnDtdVS+MMTpVfTcwA/gVsFBExsyscM4dVy6XnRmxiGqTw6/cYcbqKw9nmo6CbEREURFIDPW7IGETyAwIL9PO+oZZ7516gLOP/budTZfuIXp3FT89Q9yvcaUhpNTzKLuy4TJlcmLZWPqi+2P9wIFqtUq5XDLv/d5mdqeqntLXQaNm9gBwrIisMbN5fZ10sHPuJ8C7gbu9919X1XeZ2dH0ph8Xq+p/A8tFZJr1RObFIvICM9vVzB4Efq6ql5rZXSLyfOBvRWRXVf2EiMxxzh3vnPt2jPEdgJjZcF+kZznnvuGcK6vqwar6ehG5XUSeZ6YbBcOMxIrx20v5is2UXA0NPYesRgfqIf4Bs5l0H/yDWHUZq45eec63/jDpALTg1c7dNouox9Nafh1KgKRnrhWPakKINZrtlzSnv7ZWq5UlTb1kWVYG1hZFsczMFpnZ61X1VBFJrfeKvm1mL+nruLkhhKPN7MNmdngI4Twzu8Y59wHgWjP7sIhcaGbvV9WXqGpZVVv9UlXVl6rqaX0996GpZ/v68jwze4GZfTiE8BpgjpmdZWYvCSF818wwszSE8E4zO9HMVqnqMhFZm2VZ4n0qWVay1ow31Gg0DydaBTMH3qHiEGeoy2kuv4YY3wy3zUL1WOjLq133ritxP3w+2HSgQTrwc8p7DeBqDl/ueVxi1o7Nimza5VNFqVqlkmWSpulBqvrrT3/603bTTTed6pwrPVZ3/TWRqnZf/epX3/ye97znZzHGwVKpNEtEXhVCuL3b7dLpdGzmio9XZKBb4PIKoQXSNEKrS3dJh2LytfRiFqMWTvmDe+m3X5WYne30+kUbnFkvCC38L0U+HdZ2KO9uSMxw0Wh3xyanvXHCe79TImLee8ys1O125998881HOec46aSTfrd06dIlCxYsWHDvvfcu2n///fdutVqNPM8LYNqvf/3rI733d5xwwgnjU4MaGRnZuHHjxk2zZ8/eYfr06dMvvfTS/VV12pve9KbrZWrSvhWJCHfeeWdz8eLFr5kxY8YtL3/5y1t9cOyBBx5YMn/+/N2yLMtGR0c3XXHFFUclSTI8MDBw1THHHON/+ctf2hVXXPGyer1+/ymnnHJkURSrsiwrpWlqeZ6Lc07Gh49bOty4ZJB6UcXFnh+gvcbQfAbGlcDrwaabdcfMznYJt66Yha2+A3hLr4tuBGQIbWe0V7Yp7xlJY5mQPjfUD16YJok453DOzVTVmycnJ1/kvXdFUXROOumkF8cYJ0XkkBNPPPEgM/secBpwu4hc8etf//rI4447bvyEE07YDNwAvA04cqt535L/+Z//WTk5OZmedNJJP6PnsgJIY4xVYHfgqGOPPbZ26qmn8sY3vjE/4ogjqsB/xhhf6r1/N4D3/lxVPfG6667b0O12hy+++OJ9yuXywAknnHD9qaeeuujHP/7x604++eQlIvJKVf2ZiMzy3m/IshJh2iE1Ri/dn8I2o/lGOg8NQLeKCuDWYr04l0tX38rNuoOjU+zpdHHSW2EAKiWE0PO2FTW6K0qE8fWmyXBWqdfTNLM0TUVV91fVDVtzhqp+xjl3HHC+mVXN7FQz+5CZHaSqHwVYt27dGjN7jZmdr6r7hBC+r6qnq+q5ZrYsy7LEOSchhAtCCF/ql/PN7BwzO8HM/tBut7/Sr2uVqh5iZuc7514FnA98N8Z4ppnNz/M8AmRZNg04X0SOPeWUU64WkTkrVqy4sq8bVwP7eO/FewdpdQjxMwgTG2g9tAMxr6BqGBGTdAtO4QFH7ua7qLoXrjHvEQBtGmopph6LghZCc023M/yCr5mZgk2tCBpm1th61aCqJ5vZ9WZ2DvAZoGlm58cY/11V/y/ATjvtNBe4S1VPB74FvBG4EDhTVY9xzomqmpn9j6r+SFV/FGP8jZnd1Tcmx1er1dMAhoaGZqvqN/ov4D5V/ZCqvkNVvxljvDTLMm9mOOcuNLNPm9mNr3zlK1+oqvHKK698oZmpmY2LyIRzzkTEvEinW33ORbTXdqEbUQUjwUiINn0LTtaYi9meiWg8ACkO6GPQQaRCtBwfCyIlEEOHO6jf1bmkSJIkAENmtjaE8KrR0dHvAh/pc+FewMMxxt+IyDkicq2q/peIvK//tlm+fPnyGOMxwIV9Sz1mZpc75xYCRbPZfDO9KcjuZlYDMLMNZrZSRH6rqqsnJiYAzldVAd7br2d1COHMNE33VtW/FxFijIv6n2c6537rnFNVPRxYd9ddd80xs/NDCMc659aratk5twFI1dUXEJM2lnchRlwUMI+TKpCjZEjYT0PsJBJ1fxwHA2ByD6KG8wENAWcZBYKUM4b2ayeJK8eolqZJbma5qh5YrVbX98FLb7jhhi8fcsghpyZJsjbG+Enn3Dn9acyXVRURef+ee+65B70A0b/EGDc5514A/G0I4c0A1Wr1rna7japuvadwd9VHtkEPDAxM9EV4tYicEULY0Xv/9yJyboyRGOMX77vvPpfn+SnOObrd7qdKpdJbzGxP4JMzZsw4ft26dTNijEc65w4ErnbOdfO8GEqSxJLpB25ktFzDaUSDoNERDEQdjvsxDgQ7VFRDglgZo95TZPogJlUsRpCIRkEAqQ672ryuiQQRSVV1DzObEJH/k2XZJ/uK21988cXv74vyHqr6MTMbT9O0VSqVjqHn9OTyyy/fddWqVfckSXJ4lmU2PDxMjPFmVV3TbDZHNm3a9Gag+pOf/OSHSZL4iYmJep7n3YmJCdm8eXPsdDph06ZNOwMvufLKK1/5+9///tA8z2tFUQB0RCTz3n8QwLmesynP83enaTrovf+AmV04f/7862666aY9RWSWiLwjxniqiOyRJH5pURQastkxteowRW6g1tsVZgZiRHsIOBBjEI2VBNXe3kUAtSaQoma4QsEJFoQ0rbq0hjmX9yTKhoHRWq32yRUrVnyn2Wx+qlqt0g9R4pyT/pywBAx1u48E+WOMu91yyy27sR1Kkt7y/PLLL3/L9u6ZIhGZ3e12Z2/93LZo2bJl//2c5zxnLvAl4L4sy+7tdDoHxxg/MzAw8KlOp7PUzIaT3to+NwYMyUpYXiBqRAOHEdWDTiJTLkHFoYVsUYxCCwgQlRiFEAQtDEmHVBLnvS9UNdCLV7SA3efOnfuqkZER1qxZQ6PR2OJ6+muiJEkOFZHXmdmFIvK5I488UkZGRkoDAwOvA3YVkWY/LlOYmSVpDciGevtrQi/Or7FvPGg/YnCDc72NnvRKdEqkidDEaKK2DmMN4hLvk2az2eyISINe7GIDgPd+N4CiKNi4cSMPP/wwo6Oj9EXqr4IWLVp0D/B3RVG8Kc/z7+y9995Lgc7atWt9/5YNwKoYY8PMmnnezXGuRKBFpAk0UBqYTqDoFrxMSTDskTCJ1VCGCQgmZg4vZoZnMoa8UqkMls0siTHuY2YPbauzMUYmJiaYmJggyzJqtRq1Wu0JReyZoKIoaLVaNJtNfvCDH5RGRkZeOmfOnN/ssMMOyxcsWPBGEVl37bXXPnDyySc/L8Y4R0T2EZGFIhLE2ySiE5jMwBCi9QL+Ig7byotvWALxkXi/yRCQRMU5jFhYCXGaxDgeuk2DctL39TXpBcCfkPI8J89zNm/eTJqmU55rSqXS0w5oCIH+epZOp/MoCdiwYcPJ3//+9x/3zNe//vWFJ598MmY2ADRU1RVF4V0+TmahEYxpBBVBAog6ByJWe4ThIglqG3CyCmwepkNmKIZEwUV1DlTF8gZx3GBGGmN0fZ3x+B34j9Bm59xD9BjdqequRVEMbz2oJElI05QkSciyjCRJcM6RJAkissWCAqgqU/NIVSWEQFEU9L3M5Hk+NbnfFq1yzo1OratjjPvTC8azadOmV/bvmWNmDeecxBgTjZMSi9CIQYcR5zykeFUiINQQAZEHzeLaxDTeLUYKzEPcAWLcY6ZmipppjMFI8tEGzeXW9TtnWZYCrFfVA+jtBngUOefuEhEdHh5ePnPmTN2wYYMbHx8HWNV/BmDL4J9BUu/9LWaWzJ49e/nAwIB7+OGHa51OR60XtdsdyNevX39zrVY72Dl3dwghiTGaH1+UabGpFQszEg3OgSgizjnE9u8bkdscdqczC/ejyeL+Mm6WYQ3MopmoBefMJOk21laTxuKKmbrY83aPmlkSQrgeoF6v/wxARO4WkearXvWqHdI0PcE5d2KpVDrhqKOOmiEiLefcfc8kYluT9/7mUqk0/qIXvWh/7/1JaZqeOG3atGOPPfbYrohMAmumTZt23ZIlSxaaWaqqm83MVFVKnQdrne66ajRJXHSiEcQkGjaO0lvOkSxB7QHnE1lCHFyLWc+3H2l5xKtapqYuRpNue6ySFSsXhKCxv05tAHS73dWPEZ3k2GOPnT4yMjL3C1/4AqVSic9+9rOMjIzsfNRRRw3S24X1bJysXGlmpec973kvnT59euUTn/gE8+bN4/TTT+fee+89/MADD1zhnFvfaDRmttvtrK8eGj3VECzLVy0IjfGKRiOqOtRSU0vFaE3hRJi2nsKWJ2xuL9fa7Nc7t7HXtLNGjOwgaoWpYEoSY3emaPeAPG//CkoVEcmdc4QQ5l166aVXNxqNI5MkuQ6we++99yWHHXYYt912G7vvvjs33XQTCxYs4NZbb91XRK53zt1mZjOfcPhPkURk0+zZsx9cuXLlwcPDw6xcuZKhoSHuuusuDj30UFqt1jELFy5cVhTFXO990teteVEUxJi3he4+MXYjgDnUjAg4orWm9nJo2GGmm2bLEnnrzRP6k8MPe8QS41EwkwTFFIsaRIr25qt8vmY8uHlV770lSbIaOOKFL3zh/y5evPhKgOc+97nV733vexx++OEsXbqUiYkJ5s2bx2GHHcbvfvc7jj322MV33nnnI6HUp2nSLfL4PVJHH3307BtuuIHjjz+eK664gvHxcV7xilcwOTnJ9ddfnx1wwAE3NZtNt+uuu74GeKgoCosxGq1VY3lr9CoN7CjO1KI4ExEUxZNN4SSUXiwvu+YT/bCbbRLldoSDMTnSO1seogU1cTEXb2YyvvaOscHB31dG0zdrjFG893cDx+yyyy4bP/OZz5yqqqgqt9xyCz/72c94xzveQQiBgw46iO9973ssWLCA973vfe+cum/Kom5tXacAfeznFEBbfzrnEJFHfe87erdY8F/96lf86Ec/4rTTTuOaa66h0WhwzTXXcPDBB/O2t73tnd1u99uqOlNVfwVYu92VOe3rapMjCyei2lwfxXDOnMTgRQSTl4OB8QczVkJvcyPnvGnuJDI+ihWvAKaJ2kIzqmoiqhBUpNMZr8/ace8j1snzljrnvHMud87tF2Ospmk6bGZOVVmwYAGXXHIJeZ6zdOlS1q9fzx133MEZZ5xBrVZ7FHjbA3Lr348t2+Pex3KhiDBnzhyuuOIKGo0Gq1atYs2aNaxfv54PV+O33AAAECZJREFUfehDJEkSut1uDZihqre3Wm2X5+3unPy3x2548Kpu6sV7QROPpIL3XkYxDu8teev/Kjrjl+dctnpF71W1uFnjzvVHnIV+rSBCRDDBDDWl3GmM/LrUWTbS6XQkxqiqehuwT7fb/Y+pAe68885cfPHFpGmKc45SqcQFF1zAnDlzHgfSE5W+W2pLeTLPPBbsww47jLPOOot2u02e5+y1115cdNFFZFlGv8/7qOqt3W5Xut3cyvmDGzutkSs0UlLFMHEoiIlhbJjCR8PcIWLj1p4o90kvPegaSe/bC2wOcLsZY50CaRdYNzfJI6gbWL3LIe+euyh9+4ZSqSKDg3UnIieq6n3lcnm3vsf4cQP+YwA8lgufSISfSGwfW7z32/s93ul0NojIfFX9z0ajRbPZtP35wZyVt/zbKomTcyspVkqFSoaWUkSEGcCBmKy2uN9id9LCl8NWu7NU9UKsdHEf5YMFNgjgpLcBFpCiPbkTYbKgvXp9nnes3W6rmd0I7Nduty/dlsg9FpjtgbctDvxjL2B79W2v7anS6XQuN7MFwPV5nlur1RLXfXi9FRPtojM5B3D9bXwighNYh3Fgb65culgtfmEKty0A+qHWFZrvNG/LPEddI3WGiLkE8JiKt/TB2340viD9/X7tdtva7bZ18/xhM5swszfGGK/bnu7a3oC2J7pP9P2JxPaJ9KeZEUL4dYzxFBEZy/N89cTEpO902rpP6boDlt98adNjPsE0wSQRIxEDle4ULhp3nu/Xt696HIDy6qVd1Asql/ZMdXy7F5ssiUURvDcRr1i3NT5DWxsmKvnitZONhms1m1oUxdVmVu92u0NmNj4lftsb2JPVgU9WBTwWzG2B2O/ThjzPZ5tZmuf5NZONhmu22jpkS9fE9sho0R4fRhDvRRMxSxLDY2OIvq0nmXIJKm05bWn3cQACOHFna5hzX1+MM8ytS5yId+AEBMErsui671b3Gbjn2G6n02w0GrRarY6ZXQUc0Ol0rrZetOsJOWFbYG5Ld/4xffpk2zGzTp7nd5rZc4HfdDqdvNloWrc12dqrfs+rF1373YokvU2ECeC9+FTEsGQjPbcfxLlLXJJ/+lGYPcr0n3LPerS8D/DbHhfaWxOxsVKCeYdkHhMvRAvDS2/58Y0H1X9fGZ9o0Gw26Xa7m+htAH99nuc/2NoIbP35ZET6sRZ4ayCfTB3barvb7f48xvhK4A+NRmt0YmLSJpsNe/70m+tLbrrsRrUwLe0fB08F6Z2rtzGI7+jpPq6MoTRfTlo6sl0AATq+/U+az76h/1AVlZg6o5xg3uNSj6WefHz9kj1orrCdSsvWTUxMyOjoZtrt9lLgTjN7ewjhB8CfxYl/zpTlCUpeFMUlZnYicHur1Vo+OTnpGo0GOyVLVkvzIR1bt3yPzBNTJ5Y5LHGQelPE5T1JBHTObb7onvFYvB4HYO3kVWuIpRSTb/aXLSd6WJQ6c5nHSg4p9xqS+67+9tCe9QcPk+66NZOTEzI6Okqz1VpsZjer6luLovjZ1jrxyXLlE80Dnwy3TX0C62KMv1PVk4GbGo32srGxMRkd22x0143sOfTQYXf+5t/qpQQyJ5qmWOKQLDHxxiLM3tTXfV/TIknlnSselxXpcQACuEp+Tsx3HERp9Pz/7kWZp1tySEkg8bjUiROscsvln1v7kl2Wvrzb2LhhbGxSxsfGtNlsPqSqV5jZ60IIK1X1uq3r/1PB/BNBm6Lr8zzfqKqvUNX/nZxsPjw2ttmNj08Smps2vnju4iNv+cnn1qVi1VRESg5KhpQTXObIUXdEP/bRiPnsHZzaJ7aJ1bYuykkPt73xbbR+Zu8N2P6ibqycQrmMlZ1ZKYVyIi6VOHzzjz9/99ELlh8dWhvXjI6OsmnTJhsbG5sIIfwXsIOqHhRj/Da9I1mPom0BsD0An+iZrWg8hPDdEMLzgel5nv98fHx8cnRs1MbHN2unuWHkmL0ffMXNl3/+LtEwVErFlRJcmpiVykgpRcXcJrB9eqJbP9OL/5q8c8U2T7E/4WnN8J2df+iTtQKc3If7KyGyf7ONNbqUOoXQbFtoBaJKZc3hJ33igF89sOPVOcNzhoYGQpqWy/V6RUul8qCZHqGqS4CFMcZTVHvO2a0t67asLvC41cTUiuIxn+qc+w/ghc65nUTkd3mej7dardhqtbLJZlMzHXv41c/Z8MqbL/nMIrHG9EqCr1TEVVOjVibUM0g89wFTx14viWHHkLxz9du3h5Hf3h8A57x++i9jrLzV+ZZgzACe6+B3HqYhaIw4J+JMkbwIQyvuvHryZS9//i6tXB9YuSFMC0Xs5W2KoeVElgCY2dFmttjMfk7v8M1g//qWds1sm56XKRAfc20N8J/0gvgvBO4piuKOZrMZx8YmmZycYPPmUXYb3LT+pXtu+ptrf/iJEbHujEpKUstEaplRKxNrKaTCCoR3AB6RJTGfPekle/s5Px3bbuzhjx64bn9tx92yrPigS8b+AcgQNqP8ohuZ28zxrQ7SynHNDtIulE5wxaEn/PP6WJnDT24faJfSSlKtlsvV6oCVKxmlXgCpDOypqrur6m/NbF2Mcb6qvlh7Z+keJbZbr3+dc8E5d4OILPXe7ygiR3rvV5jZgzHGVp7ntNtt2p3cmq2m73ZbjeOfN1nz3TXx1h+fv2PJaVYtOStnSK2C1lJirUxeSlmPcQwwo7e9b+gifPlf5e1rthm+fdIAAoRvzXytSHcv51rn959aCdzcDcxsdPCtHNfuIO0ca+UaWwEGZu+16pDj3vc3Ny1Nfr1oXW1WuVz2pSyxUqki5XK2dSQuU9VhM9vFzKap6gozW2NmDVUt+qcvMxGpi8iOwK5JkkyIyEOqOm5mRVC1kOd0Ojndbod2u0On0427zypWHLlv53X3XfWD69ctv3V+NcPXMmeVBKplQqWEq5eQUsZalBfSOw2PhuqHTct3J+8e+dUfw+bJZ+345vR3kbTrkE8dR1iF8LsisGujQ2xFrNUmtrvUmwXdboHP1RUvPOmfNyYDO02//BbuGu9k8yqlMlk5w7uEUlYy55A0TcQliTkRw0xxHouxd2Cot3PT+kcbxEzEQIIG0SISo1qet6UoAt08aLvb1uGyrj/hMD0gTK7ZeP2PPj+zlGhazoiVBK1lZJUSRb2CVlMk9ayldzJ+DwBi9gG01pV3b3xS2Yz+pLwx8RvDn3RZdwzrgyhsxvhJMPZq52izLVmzwFq5SbdLaEeNndyZlOutw97wwU5anz39ZzeHO9dPMCvxaZp4j08z0iQlSz1Kz/XhxHdxRFR7ESvnPIqPUTPV6LwX63aDKLEXEy6ChVDkc4Z15G8Pyw4qJtZvvOm/v1ixTqNaTpRSySXVFF/NROslpJwY1RKWeBZjnIAw1BtP9gGKclXevfmzTxaTPzlzUfzawBnOhwSXn9dPDpZj9i1DDmp2oR0Iza5qJzhrFfhuR5NOQdENTpPKwNiBx5w6MXOXfY9YtiZcef097e7IhE2XLMlSvDjn8IngncfMgllvQ7JzIoqkGsRUC4kWpCiCodadPuw2vXi/cmXBTslRIyvvvfauX/37YNGdnFbNVDJHUi67WMmIlVSpJs5XMqRWwouzuzB5J5BhqMbkDGflivzD+JMG788CECB8tfZO8cVOzsd/4pF8pz9CGOjm1DoB1+yStgq1dk6RF851g/puTtE10iLSGNpx95EDjjrFTZu928taOXcvebizfMWabvfhTV3f6UI3qqC9o6WC0ywVyiXYeUYp7rZTWpq/c3WPWsYBm9c9ePU9v7lEx9Y/uEOaUMs8oZSQVTJXZF6tZ22dK2eESkYsJf2NU0I/LwKjhPSLEb8i+YfmD/5ULP7s7G321cphEXuv98XeiL2gf3kxwu8N269VENtdF9sFaadQ6/aSDaXdSMgLfKFoEUhiJCcpjw7OnDu+497P1xk77pqV6tNr5drAoE/THUSchLy7odOcnOg2Rpub1q3M1y26zU1sXD1E6ExPElLv0MzjspRQ8iSlhKKSkpRTJ6WUolZSygmZiNyH8VKmMs2Z3BhDtlyRf83e17r1z8HhKaW/m/jywIy65N+XJP4B9JGljsiPMTSiO3cCRTt32im01A3keeF8oap57nxhWsRA0lEwJQQlMcNCz502ldqk109BEwERJHEEcSQlB6knJN6lmdOYlpzLnMZKQpZlrltJ1ZVLpB63CiPF7PgtfTT3KYv+b0TKb5F/HN/852Lw1BMwXobX9dmZmJrL9K3Agv5fOSr/jmdGVJ2bF3Q76nxRqHZy52LUmCsuj06jqu8qgjmiEtTUmEqF0vumgDlx4h2Jd2qJgHcuJk592ROT1PlSopomzpW9xiwl896tQumAvZlH0gcs1sJd4oTAxnCenP3Udko8bTlU7SvMN/NfFW9XAJ9iKmVJL/vkDzEM0V0Ldb5bqObRuagaikBWmLMQCKAuGAFDowKO3gpASbwDBJcICeI08SSJU8kceZKSpGCl1EnqNWJuBYLD7C1bsmBCC+MTMcqrfIjvlQ+y/OkY99ObhNaQ+K8ch+OdPnG3KXwco7xVY/eqyY3iqRk6xyJZUFeEqC4oFOYExaKh9JzaPSMiGOLECw6HpKKWeMyLI/XqxVMIbq1Fmk7scIP9txphxymfiUGfD3zL/3/84ulMifzMpEH+Bmls8yaMU8y535rnNOnP8rdqeA3I1eKsbSolB4NRdFgMb0Y0wcR64mXSSzggggeCw40rTIizrqlUwF5msNOj+gArPVykhR6t8P27q1x2yHt42vcdP6OZzO1sXBjmKODDqvxeEjfN4ANP0JlRRG43GBdljN5OWDCrmWNYYAizgw2mP0EdXzJ0s1NeivDZZDNXP1U990T0rOXSty8wsyuchXMrTLjgmWhDjDNQ3a1kfEr+iY3PRBuPa/PZaGSKDKR9AZc47x6KulUuwqelbrnIqe5c+SdOFJ4+HffHaJse6WeKBKwyyVtDoQca/Eb7qbSfarHItVbovpUB3vxsggfPMoAAcjahW+KNVlhDlZVPGcDIWjFbF5U3yTNgJP7oeJ7tBqdo9F84wMOpivwjsmWS+6dSMLUvpsJ3Bz7CdpMkPpP0FwMQYPyznByUHXFy4Z9VgdrpzjE+7aN8+2nu2pOmvyiAAJvO44tmboNh5/1pT8qnPTpj+se3nRjx2aK/OIBmyKbP8FMzN6bY257MM+LkMjGtzQy89pmc4z0ZetaNyGNJBGtv4k2gczVy+5MwHHcRdVoROOkvDR78FQAIMO+LtIm8zYndr8bodqcrRkuwG73jXTudTeuP1/zM019chLemtWdzpCkvir2EZI8jBx9xCTfNOYvfbev/vwT9VXDgFM05m2sMgiinP477lNMd6F8TePBXxoHQW+6tOYsfFsrDpnyof/GiJGHmzp/mrc/2SuOP0bN7CvpJkICZ4+2rjF8G5RcmDDrHvjt7Xv3XBh78lYnwFMnZhOg5SRxF6hmhyUlyNs/o2dj/X9LKT7D/yo+y31+6H09E/w/wHJVcjfUH5AAAAABJRU5ErkJggg==\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHiczZ15vF1Fle9/a1Xtvc98780cEjJCAgkgiUwiIghCgqLIQyK2qI0D2g6PlmfbbaONCmo/habB4dF+tBX0KdC2PFGZB4GEgECYIQmZp5vc+Yx7qFrr/XHODSEkiDKuz6c+Z9+z9zlV9T2ratWwal3C6yh64YW8Y8Ex4431M4yhOQAtVMUBTOgRQRGEWvtBlJnRgGJABSthdIWHrIyI1l9y3339F154obxedaDXOsPGr2+anFL2TgXOJKZWEIYP5oslL0z7EtE8Zj4M0O49f5qGofKAF32GRTe1GnWTZOkR5DUi0muC0Nxaete7el/L+rwmALdde+34nOcPgei0ILK/j4rlLmbztyBMfkUyUGwT8f+ZNGojWeLeBchvjOR+Xvngqf2vyPe/iLxqABWg/p/+YqFR+WYQREsLXeUuMH8WQPhq5dmRVMRfUR8eqquTt7Din7rOOXsFAfpqZPaqABy88spjlPhfi8XytSbKfZxAB+3l0ZSZ7hXD6wkWCAggClURGWJSggUAUjivokRIoJpq6gkkyl5miOJYqNq9VO6xer3+UxfHp4P9l8Z+8pPLXum6vqIAhy+/fLYXXFksdd1go9wXQZjyggyJH1HDDyEIQ1iawMZAjAWTsWS55QHHbEREGQwPABAYZhLxzjDBIpOcQBx7BwhUvOtDkiXk/WGqcugeirbJxc1LGvXaqY5x7sTPf37NK1XnVwTgg1deGcwcHvkWOKpXuiqLiPjI3XIZJBv91lub59CORWBVAysmDKyQ8QgsQGSNsakaUhAYIAPqlE+hgHpRVeMB730IFcfOK8RbSVPHTghZCsn8IJI0hk/fA8WYXYuhovfVa8O3eudy69eWLzjsP87NXm7dXzbA6oXfnJNBflwaM+4uJrpgt6/fTlFwC+dyY7w1Fvk8KAwYHChHRsQGAVvrEBgSsGE2DtYATAwFE4MBQAUCgkBU4D3EewtRD3WK1FmkzsE7gstI00zQaoEynyFNNkucLCZg+q6lUpVLRoYGj1KWv53wla+sfjn1f1kA+750wblBYGcXunpOJcUBu3zrsIbBryhfnGaiyEghIoSRahiAcgEjyCkCqxwEFsY4BCHBEAnIIzAEsGEGRDVgYgXUiQAMcfAeEDXwqshSFe8t0syxcyRZTIgz0TSDacYkaapoNb2kySaK07MAVEaLqIRnGkNDv3ferR7/rxdd+ZoC1Asv5L6R+k9yudJTuXz+YgA7O3EOwquQjyoo5POI8oRCBC7m1YcRKIgIUUgcRSqhVUSRARtvrGEPkiRuVBsDw83B3m3Ot2KqDQ8TAJR7utXkcjpm4qSgOK4nn88XK1BlOC8iziBOPWeOkCTkk0SROqU0Jm00QEkKtFqqzVZLG406vHxol6q4pNn6XBw3jh23Zf3ZdN11/lUHuPpzn4t6Mr2h3DX2T8T85eeoYjuKuT9QobwPFfPQYp4oX4Tmc0AxD+RyanIBxIZk8hE8sx3cuLnv4ZtubD774MOz01bzQI4KjwTdXRt57NhhDYO00FXOAKA5UgsozUIZGOjOhkemSdI8NMwXnt7vsIVrDl20uDhu2tRxRtT5OCZOUvFJqhTHhFYMbbSIkpai3oSvN0Ct1lY0mqeAMHFn0UUuqo0MHDkU0Kn7X3FF8qoBXL34c1HXxNYNlZ5xawj0qZ03DN3I5UqMYqFgSiX4UhEo5IkKBUUhDxTy4FweEobF4R19g7f+6D8a29etPz6cPPGWniOOHM5Nn1qKyuUKk+UgCMSwigoLMwQARNr9ocucEUC9TzWu1WqNDRvrw8sf6HHbd7xz0uxZd5z0iY+VK+Mm9HCa1aXZJG01iZJYfbVJHDfV1BvwtQbQbDSlVsvB6+JdQHxvZLDvwDq5d8/86U/jVxygfu5zUV//4I1dXeN7QXTWzvdN+GNTKU5DpSJUKYKLRaBShi8UCaWCUr4Ijuy4NY8/9syNP/rPg6mYf3bSu9+zpTJj3/FhaG0YRWRsiCC0yFkLgAWgzFgSMkYBQL0n74iZJRDxnDiHLM3Ue4eklVCaZVlt3fr+3j/8YYrWG7NOOfeTT86cP+8AybIdaLRI63Uy9ZZKowodqZOv10G1WiLVxiD77CO7VPPqkaG+aSMjAyfvf+ONL0kTXxLAa9//fvN2p7+rlMdsYDbn7oQXhj+kSnkmd5cJlS5QpSwoly2KBUGpRFTIj92+YcP663/4g/2CSZMfnnrG6T6sdHUXopByuYKGoaEwDBGEIaIwJGOMhGHovKfEGPVBEGUAkGVJoEqGCGGaJoH3npPUiXcpxXGKJEmRpC3EcapxbWR463X/bZLebQvf95nPrpm4777TpNUYQD1WrtWdr9dCDI8IqjVItcpUra3WNPvMzjqpfH9kZOCACQGd/FL6xD8LUAHaccq7flEuj9/ARP+480YuvIzK3fO4uyzo6iLT1QXpKkMrZZhSyWTQiddd/r1n62lMsz/+sb6ouzKmmC+gWCxImMtTFAQmn89REEQuzIXKABtjhIgUAAPIOgkAAnQMlarCe8+i6tMkYeecbSapxs2WZGmszWaCRquOWt/g0Iaf/WxiJcxl7//8Z+ayoldrdaFaXTEyDD9cI1NtiBseUNtobHDN+FPP1Vkuqg4Pzph40w1nv2yAW4874dxKqZyzJnfZ6Hucz31fyuX9TM9YoKdC6KkIlbuYuyuiheLEbZu3rL3h5z87aNJp77lp7PyDpuZLZZTyEQqFvBaLRQRRzufzeVimoANsAMB2AFsAbAbQByDZDWAEYAKAKQD2ATAZwBhVhXMuSZ3nZrNhm/U6teIUraSl1ZGGDj3xWO/263+76D0f/chjEydPmaWN+nYdqTKGa0B1WGRomDA8QjpYfRZZ/HejdXRZfF6tWfeT77rte381wIHD3zpfQ/s3xfKYL0GVAQA2+iF3lWfpuDFK3V2Erh6Ysd2qlRJToTD3jzffcseza9dMm/s/P7clX6wUKpU8R/mCVkpFLRSKEgTWWmstgEEATwNYC6Cxh3IJgKM6fy9HWyu1c4861wUAswDMA9DtvXdpmvp6vW6arVQazRparRZqQ0Mjq//9+/vvt/9+a4898Z3v0Li1CsNV9cNVz4ODVoeqIsNDHsNDG5G5tiaqukZ96JLM61WT77/nqb8Y4JPz54djw8LN3eVxUxS6PwCA7a+op9JF3V3MEyYQuiqCMV2M7m4gCg/6f7/573uauUJj9t+eXSiXyzaXK3JXuaiFQh6FQsExcxnAJgDPoK1xhHbTpA6g7g6UgwAcDmB2pzhrANwP4KkO7CoA34HoOq9jOp/b13ufJEmi9XpDq9Um4rhOtVrVr/np1Y1CKy68532nH0NZ+gQGhtQPjxCGhlX7Bw2GB70OD4/A65JOvqtGagPbJrK8kx56aI/TPrM3gF8ZO/HfugpdG9W5U+EcyPmnqFioarEYULkCKhWEymVGpUxgc9A11123ws7cb+P+HzprXHdPt+0qV1y5VOCuroqGYRgZYzIADwJ4Fu2mWQHQIyIREUUAciJCRJSo6qeI6NsA7gZwO4ClqvppIrqpU7xIRPJElAfQg/YSWQJgG4AhZh5rrc1HUZgGATMRGRuGKM87yA5v2Tz02N33NOfPPeBQUfSyS1nTDEidauqIUie+Uffk3AQ4NzYy9prBZv307/bt+N1LBrh1zpwDQoTTAzIXtKdO4jSMbuJysULlippKCShXiIolImsPuf6m3y+LDpy7ZeYZp0+qlMvc091FpVJJy+UyBUFQYeanADwCIBWRkIjGiMg+qjqWmaep6nxVPY2IjlLVJ4joMACnAzihkxYR0TCAR1T1M6r6FiIqEVFFRHKqWlFVUlUhohjAWiLKjDEzoyhyxhhlMrCWbGn//UrVoaG1Ty9dGs+fvf+bSWgL0tSqy0BxBmSppSx7FnE8H84zvBzjnLvhf47r2XZpf//AnwWoAFXHTfivclSeR0RTAYA4uJIqpamolJkrJUi5ApTLxLlw9u0P3HdPOnnS0KwzzpjQVSlTsVhAuVxGoVAoMHMOwOMiMkBEkYgAABMRqSqJSAqgn5mfVtVFALYC+Bu0jchVAG4AcAeAJ1R1tqq+T1WfJaJ9mPknABqq6gDEABLvPYjIEFFJRFJVbTDz5CAIDDO1mNmoshRnzOgeWr9+oPfZZ9fOnDr1MFHpJ+cUzqv6lJH5IpL0VqgcAQChCcstFy+6ZKD/Z1/7cwA/PWfeCcWwMMhsP94mqiu4XAJVykzlElG5i6irCCrku9f2bnnk2aGB8rxzz43KlQrKpSKVyxUtFAqWiLyIPKWqCYBJqlpBu5/qEZGJzDybiN6jqkd77+8hoveq6nYi+g4RBar6QQDvBPAOAAuY+W4APyKiBao6A8AtqvpFAAtFBERUZOYKgPGqWlTVHiIa6qRyEAR5a61T9T4IIi4fOCdcd+/SfJGwprvctQ9EEnZi1XmFc6Qu60Ka1gGaDMI+AfFlQ2PG5i7p37F+V168u/YJ9KvWRl/a+UCY+xNyUai5HFMxUuSNIoyQthK6f+XKhQed9/fVUqlIpWKBS6WSz+dzQfurcC+AQe99WVXzqtqtqhNVNSKizap6u4iUVLVFRBc5574IYI6IXOK9f5+qiog8IyLPqKp4798nIpeKyH6qer6IfNN73xCRCjPfCmCt954BdAHoIiIjIkUR6RORZUSURVFki8VykM9HWiyW+KC//3zz/pXPLEySRCm0RiIryAVKuZyafJ45Ch4Y5WBN9EURf7HuZnifp4F/N3f+WwthfoTJvL/9Dt2ihahiKmVoscDIFxWFAlEQzvvtI38anvPpTy/vmjC+p1AqoburolEUBUQ01Tl3D9rW1DNzQ1VrzJyo6j4AFgFYSERLVfXdAC4HsICI3gvgSWPM94joZhFZq6pxpznfraq/Nsb8UUT2AfBhZh4G8FMAxxPRLar6BQBv8t73qeomADuYuQmARKSMdvew0FrTMIYVRAAQFvaf89T9N/x26gGTpkwn7/uQesCnUPHkY99NLlkNYH8AFct6SX+lB5cO9m/eCXZXgF79N4wJp47uvpgo2CxhYYoEgSIMlYKANLB2Ze+W28v7z0m7pk6elM/nUCzkuT20w1Tv/UYAbxWRhqqOrnhscs6tIqJbARyvqomq/j0R3QbgAiK6Q1VvJqKzvPcXqT5//2f0b+89ADxijPmi9/5dAL5MRLc4584H4IkoJKKbiWiMqh4gIlNUNWDmfhExxpgtzDwlDMMtBRHyzvvuqVPGl/af9ejqbVvs3DFjp0kQOAojFROQKQQqLtymSdrmwflPESenAjjxBQDXzZ8/iV1wO4CLAEAJd4kNpyCygAmI2KgGBkjdlCd3bJ+54NOfWFbI5xFFEedzOW+M6QFwFxFtUdXAew9jTMV7fygzv5uZ6977bxNRwXt/gTHmUlV1InIBM38VwDs6oG4RkbuJqLkbxAIRHUdEJzrnDgUQM/OXReQTAKZ0NPBSImIAnwJQAnAnET3mnBs0xkBVM2aeYK09wlo7kM/nDaA44MNnRw9dePFb9ytVMmbaAGtgQqPehIAJ9oFmfwTp20E4gNn+cu3MgybOWvfE9ucBDBJ5X7EY7dynZWOfhjEzjbWkAUOJiRRmxY5t901bfHItly9MyefzUiwW1Vo7SVV3OOcWoz2wtcaYDQBWiMidRHSqqm4jog+LyBZm/i4RnSci3yaii1T1Hmb+tff+QACnEdFJ2E2ICKq6XVWvVNWVzPw/vPff7IA8T1UvFZFNxpgPi8gIgAnOuduDIOgGcKyq7uu9Z+993Vq7PYqifYhou4jXLPOFKSce9/tH7vtTeUHPuGlK7MFWOTAiQQAT8FPe+bcDQCksjvVu5HQAPwQ6RkQB8mtXTwf47e3GQn1gM1WtgbckwqywRN6l4zYn8dsnvfWt46PQahAEFIYhq2rivV8G4Ceq+v9UdbWInOyc+4S1FqoKVf2Bqh6qqr8RkW3e+8tU9XYi+rKqHui9vwzAuWhb6UcA/MY5d7lz7nIAv0F7HNkD4FwiulRV91fVL3vv7/TeXyYiW4jotyJyqKr+H1UFM5Nz7hNEtNg5t0lE7iKiX6vqnUQUW2tNEIQU5iLd9x3vGLclTY7zWdoDqLbrzGBr4GH3VcWAAgDTcbJ29b6jP6xBW99nGJOfEYy0ijSmO1TCLRREPchFZIKANQyFQqPPpunq8C1HPjl+3oHlKMpRsVgQZj5AVR/z3p/V0egBVV0JYDEzbxORfQGMVdXDAPyaiD6JtrH4oYh8TFVPQnscdzkz/5f3fnVnnDiLiBYR0WGqKqq6XkRuNcb8ynv/eGew/W4imi4iXyGiyQBOZ+afiMhH2nqBsdQ2FhONMVeKSAzgMBF5LxGtYOZDiagPquS8mrReWxFv3jw4hrlIaQpJM1LvGN4ZdemDrDRG1qx9VLys+TvJNl8OjFgASGBO6Db58ar+fbxu3dUye78WDBMRVNgAAguHYJ1Lj15w8onLC4UchWGo1tqIiLap6urO2OwMVd3CzFd0xmY/VtVvA/gCgEtFZDERfUlVv+WcewuAW4wxfxCRt6vqlzpGAp0BN9BeUACAQzoJncEyVPW/jTF3O+feTUTfVFUB8CXn3BeIaKL3/nxmvkRV/5GIvq2qARF9QlWnA1gqImuMMbOCIAicc1k+r5h16ruKDz348JGzDe0gIAUzQExEVmBMitVr7obXsyObW5e6+O2Av5oAYC3s78YVxhypinEA1blS/i3PmlnWUtFSPgcuFCjOBc3l3RXz5i9/MQnDkHO5HIVheLCI3P6Nb3xDly9ffg4zR7v3XW8kEZHklFNOuf9jH/vY74IgKFhrJwE40Tn3aJIk2opjevDib+WPGm6lhTTNS6OpiJvQetPrug01P1x9L0ELRNgx0By4byb8aVYBXg+qimIcACj0FsTJOOzojbk40wMSiM90HezwtFNOrhtjJodhKNZaUtVCkiSz7r///hOZmc4888w/rl69etWcOXPmPP300ysPPPDAuc1ms56maQag55ZbbjnOWrvitNNOG2Fuj+H7+vr6+/v7ByZOnDh+zJgxY6655pr5ItKzZMmSezvN73lCRHj00Ucbq1ateteYMWP+dMIJJzQ6cPSZZ55Zvd9++80IwzAcHBwcuPnmm0+w1naXy+XbFy1aZP7whz/ozTfffHylUnnmrLPOeluWZZuCIMgZY5SIyDBj2knvfHbDdb8pz3U+r+otvCayfYeXZnOcQm8BcJoqJgCcKDzbTcCknA0fBnAWAKjyDiWUtZmEfv2mxM6a2VAbdG8L7SGHHzTv0SAIgPZofJyqPtJsNt9sjDFZlsVnnnnmMara6PR3C4noJ6p6HoCHmfmmW2+99bh3v/vdI2ecccYQgGUAzgZw3C7jvjU33HDDxlqtFpx55pk3ABhdUg+89wUAMwGcsHjx4uI555yDM844Izn++OMLAK7z3h9rjPlkB/JFInLm0qVL++I47r7iiisOyOVy5dNPP33pOeecs/K66657z5IlS54mopNV9XcAxllr+7xX2ufNC4sPXH/DvLnS6JPEx1i/UaXZKrR/S9422qtYGz603mUT2AP7Rhxyu89VcOADYnWqHuLSfLpxY5dWa9vZmO6wUCiTseC2+swTkY2jlSciiMjFqvouEbkEQE5VP6mq56vqId77LwPAtm3btqnqqar6HRGZB+BqEfmCiFykqqvCMDRERM65S5xzl3XSd1T1a6p6uqo+2Gw2vwcAvb29G0XkMFX9DjOfDOA7AH6mqhcQ0aw0TT0AhGHYA+A7RLT4Ax/4wB1ENHnDhg23qSpEZDOAedZaMobI5HIltcF4DNd28IZ1OfGuCCiUycP4YJRTyCEE2Jc9zFwY3rf9NqDe9DjhQAUW3oNcipH+HcmEgw/5PhEnDIWIKIC6qtZ362POArCUiP5FRL6lqjVVvURVfy4iPwKAKVOmTAbwsHPuCwB+5Jx7P4BLAVwgIouZmbQtN4jIr0TkV97721T1MREpiMj7SqXS5wGgp6dnkohc2fkBnhKRL3Ys8JUicl0QBKYzhLpMVb8BYOlJJ530NhHxt9xyyzGde0NEVG3rAKVgbk6cP/97IwPb1WWO4T2p91aFAxUaM8rJGp4CmDkWoDcTuON+RqmSlgyJI/Uq3nhKM9SisKX5cJYxrNbamjFmnKpuc869s6+v72cA/rGjhXMAbPbe30ZE/wLgHhG5jog+O6qp69atWy8i7ySiSzuWelhVrxeRx4wxSaPR+EC7K9GZqlrsXO9Q1Q1EdIeIbKnVagDwHVUlAJ9WVRDRJufcBcaYA7335xIRvPerOv3ol9FeFgPaq9a9jz766CTv/XcALGLm7QBCIhokIOJibkY1CJs5X/fshYyqcyQGRBEpUkBDEM0jaGYB7KekC6EEgj7JAOC9aHvnQSAW/cVSuO/cuYn3PmLmnDEm7UzDFlQqlb4OvGDZsmXfO+yww86x1m7z3v8LM39NVd/mvf9+54f77OzZs2eoatF7/x1rba+IHAngFAAf8N6jWCw+2mq1SER29SmcucvQBsVisQ4AW7du3UxE56vqJBE5tzOrEefcZStXruQ0TT/IzGi1Wl/P5/N/Q0SzmfkrY8eOPaO3t3ccgGNEZIGq3sXMKRH1QFXGzp2bbSyWypPTXqgXdd6BvRdVZEr6FBSHMnCYgBMLIA+lCgAo6RZRhFAFeQ8IQSwQR2FPYZ9JW4MgSK21LCJzRaRJRB81xnwNAIwx5oorrvhspynPEpF/BlC11jaiKDoZnd73+uuvn7Fp06bHrLVHhGGo3d3dEJGHsizbGsfxjoGBgTMBFK6//vqrjTFBtVottVqtuNFooL+/X7Msc0NDQ1MBvO3WW289aenSpUfEcVzy3ouIxEQUGmPOA4BRS++c+6SIVIwx53nvL91vv/3uXb58+SwAE4jooyJyjqrOtNauIiJXnLoPJ8Woy2cZwYuyqIoqoEgFWMvAoarUDaBgAfCo96sqWiA15L1CmMApICDJh/l8qcTGGFFVj/aUaqRYLP7L2rVrf9xoNL5eLBZ3aggzEzOHaO9VVJLkuU1+7/2MBx54YAb2Isa0V9h+85vf/Pk9WaKJrVZr4iisUWC7y9q1a389b968aQAuA/CMtfapOI4XeO8vLpfLX4vjeA3aa4gCQE0QqURRDmlKgEK8AAqCihK0iueGV8yA2tGOkYAUIIEoqfckzpPLHBAEFY4iEhFnjPGqOhbt3bGZ++6778l9fX3YsmUL6vX6zqWnN5JYa49g5lNV9d9E5FvHHnss+vr6cpVK5X0AphNRA8BY770AEJsLSYytqMtUUwd4bbMDRBWNUV6AWn6e87WSV9KmGtRhUGdwPYCpE1srzmVE1ErTNFHVLd77AQAwxswEgCzL0N/fj82bN2NwcBBZ9rKdP18xWbVq1ZPe+49nWXam9/4/DzzwwGcBpNu2bTMA0FmE3UpELWNM3RiTsA1CgOpgqpNBnRk1z9QU2J28aFT7dr5BmldFyQIqIPKq5OFh4evqfaiq+c48dC6AjXsqrPce1WoV1WoVYRiiVCqhUCigs+D6mkmWZWg2m2g0Grjqqqui7du3Hzt58uTbJk6cuHb27NnvJ6Kt995779NLlix5E4CJqjqHmR8CYLMkSQP4qgrKBJAwhKBgJVZyFjs9jwEreE4FhajIopRAGSAGkRG1CJwfcnEqlMtZIhIiqhNRcc9Ff07SNMXg4CAGBwcRBAHy7QVYRFH0igN1ziFJEsRxjDiOn9cCduzYcdbVV1/9gs/84Ac/WLFkyRKoahlA3XtPzjmbtVrMiWs66BgQiIWcQpVZQwJ17eQFwBLQC9UNIEyHoqJGYwhIIO21QvbepGktHhpyQXeFOyvNDeCFHvi7yBAzb+zkwSIyLcuynl0rZa1FEASw1iIMQ1hrwcyw1oKInmcQRGR0TREiAuccsiyDcw7OOaRpOrrcvyfZxMyD1PbBgfd+Ptq+NhgYGDgJAIhoHwB1dFQrHhlxuSyteUg3g4xv93VKYFJomaCA0hoCtliBPi4EQ8B0IZ1LgscZUKfwgIoQo7S9v9nYtDGMpkwxYWhtZ2B7SCfT5wkRPU5ErqenZ924ceNkx44dPDIyAgCbReTg0edGK/8qihhj/qSqZuLEiWvL5TJv3ry5GMexV9UxqjoTQNrb27u8VCq9mYgeFxEbxzE11m8Mcn39Dc8QFaiFAkSkKoZID1YlgOQhQB9lBT2tKqsAgBTjhaTmSb0C6kDGiQa8dWuhsWZ9TtVb55wBMKiqxjl3LwCUSqXfjsJj5vqiRYsmBEFwOjOfEUXR6SeeeOJYImow85OvJrFdxRhzfxRFQ0cfffR8a+2ZQRCc0dPTs3jx4sVpZ+q2paen596VK1c+pqoBgMHMeyYiaqxdVwy2bCtnQoEA5Nu+TJ6gI6o0BgC86rMCeoYZfnXm0+0Ytc3CDXiygASqYK9MNFwrNNavnZ2mKURERaQOAEmSbN1t2GIXL148pq+vb8p3v/tdiAi+/e1vo6+vb+qJJ57YjfbK81/syP1XyAZVjRYsWHDc2LFj81//+tcxffp0nHfeeXjyySffsmDBgg3MvKNer49LkiTqdA11FYFzHsn6TbN4qJYnBYmCFQjEU6BKzVFO4tMdgF9rCVib+Oy00OQAAMTUVFEVpUyhUKhFko5F4g5Mk+QOIgqIKGVmOOemX3vttXfW6/XjrLX3EpE89dRTxy5cuBDLly/HvHnz8MADD2D69Ol45JFHDmDmewE8rKrjXk16RDQwadKktRs2bFgYRRFWrVqFSqWCe+65B4cffjhardaihx9+eG2WZVOMMRYARCTN0hQuzRLN0nk+i58ASAOwCOANgcHaVGlb4KbPxjtgjd0fqK4Uf/ROWyxqASYlsVBWBbwCCPv778x6dwzQPvtMMsZoEASbVfVtRx111E3PPPPMzQBwyCGHFK666iocwXXn3QAAEZlJREFUe+yxePbZZ1GtVjF16lQcc8wxWLp0KRYtWrT60Ucf3Wl+X6lB954WXk866aSJy5Ytwwc/+EHcfPPNGBkZwTve8Q40Gg0sXbo0nD9//vJWq8UzZsw4RUQ2dgyVtjZtGgj6+m9X8EQAQlBWKBFYSBBqh1Mq/m3zgQvabrNAnwIPEHAEgBMIspJUIKTkARaFMQ89OlJbvjw/5vTT1DlHxpgnACyaNm1a38UXX/wxEYGI4IEHHsD111+PJUuWgJlxyCGH4Oqrr8acOXPwmc985m9Hnxu1qLta11Ggu7+OAtr1lZlBRM+7Hp3OjVrwG2+8Eddccw3OO+883HXXXRgYGMD999+PBQsW4Oyzz/5okiT/KSLjVPVGL6KNRoOaD64o24cfq3nIFEOkAlIDdYAnAb+jw+sBAtYBnV25v4OphWyHmPidALqgtEIIxbbnIkOg5KrDJT3ggLebgw9ew4aNtTYlonne+2IQBN2qyiKCOXPm4Je//CVEBKtWrUJvby9WrFiB888/H8Vi8Xnw9gZy1793T3vT3t21kIgwefJk3HzzzajX69iwYQN27NiB7du344tf/CKstS5JkhKAsSKyIm61qNVspcnd95yst92RWMAYYjEABYCxsIMKfQsAOPGXpz77w/cg6xkAArj7W65VHs1cGNsZpBYMQKAgiFIevX23tTas64vjmFqtFlT1QQBzkyT5xWgFp0yZgiuuuAJRFIGIEEURLrnkEkyePPkFkF4see+fl17KZ3aHfdRRR+GrX/0qms0m0jTF3LlzcfnllyMMQ2RZ9gtVnauqf0rTVFutBK11m/pNb98tqhIqoKTC3LbAqup3jPJpuVaXh/sTsIun0dMwf+zJd+0PxWRVPA7CjhSglgIJhDxAaSm/OffZT0+17z+9t1AscqlYZCI6Q1WfiaJoWmfF+AUV/nMAdtfCF2vCL9Zsd0/GmL39PRTH8QARzfYi1zXqdbRaseh//fe41uXf326ayZQA0BwMAlLJA8SKsUp4EwhbBlsjz86DPw7Yxb1NgEuc91e0C4qDFboDECK0zY4KyNdbU2iknqT9/X1JHEur1VIRuU9VD0iS5Jo9NbndwewN3p408M/9AHv7vr3lPZriOL5BVfcDsCxNErRaLcRbt/XpSD1zzXiSgtgAyvAwbSPSq4Q3AYB6fzmA/z3KbSfAEP7melqfNrppQqKtAIAhUAiAWD1BbO2XvxyJ7l52QL1eR6vVEieySVWr3vszvff37K3v2luF9tZ0X+z6xZrti/Wfqoosy2713p9FRMNpmm6u1eucpqkU7n/wwPov/m+DAGsgQgAZAhkoWDQZ5TKY1ueuh7/9BQD3BxLfNtHXdHxAPgyiEduZzzJAEFI3PDxO+3aMYP3GvlqtybWRKrz3d6hqMcuyblUdGm1+e6vYi/V7f8n13mDuCWKnTL1pmk4CEKRpeme90aBGKxa/ZsN237d9OKnWxrQdKEm4c9qbCcPKdLa2rcEvAG2c0nZofz7AdjOWC0fSxuiZCMuCHQYg204IIEoA9f7kqlLlqZXvTONGrd5ool6vp9r2OD04juO7te3L8qKa8FIMyksxIC81H1WNW63WU0R0sIjcFsdxVq/VEdfr9a6nn17c95OfFRhCFkohFBZqLKAQ7kfHi62aNtcmkG/syux5AA8GtntxBwB0BwAo0YcD5WFDpAYEA4YBQzPXvf26/75vzP0PlhuNmtZqDWo2m4MAHgLw3iRJfrWrEdj19aU06d0t8K4gX8p37CnvJEl+h7YP4oOtOB6oVqtaq9do3EMrituvuW4ZMt8TgikAwxBgicDKwyD9CAAocIsTN3th22N2zwABIIb8r6G0vqzd4jUnLD4E1HS2OgJAQ6KkuXrlfn79Rp9fs663Xq9ptVbzjUZjjao+AuCDzrlfAHhFNPFlal6aJMkvVfUMAA83m821tWrVjNTqlHt27WbevJmba9bMMgRvALWAsBIFgIA1BRAqgKG08TBBzt+d1wsALgS2irhAIT8CFFCcYYFVAUHzgIYAWRKyUF37o590j9u89ah0247eWrXKw8MjaLZaq1V1uYj8TZZlN6jqyN60cW9a+WLjwJeibaOvALamaXoPgLMUWN5KkjXDw8M0ODSk0rejf0LvjiPX/OA/ihaAhUoHIIekxJCVpLqkzUB+KOLsfOAFUZH2eNDmQ9ClcG5J3uZmKBBCKWcNDQsQCEQ8wSiIRMT0Llvef9CCQxZsjIKnHbikIgKgGQTBJlU9xTm3GsBqANN2b3Z7et2TMdh1/Lf7GHBPY0IigjHmHu89EdERAG6O47hvcGjI1OtNifsHBg5cv/HYJy765gbrfaVAxCGBilDNEWyOTaxCbwJhHIGqQ0ltex/kcz9re9/+eYA/BtynYaqG6HHDZhEIE6D6EClXAHgHYgAsBPLqo833P7Bm4cELjt1A9GhGWnaqEOdbzPS0MWauiExX1Z9re+Qf7KkP2xO43QHuCmhP0Dpz4CERuVZVTyYicc7dPlStJrWRmtZrVY37h/oXbu094bFvfXuFSZOxEZGNFDYCfA6MHDGp6nYiHA8Aqc++lIn//RGQPUb32OtZuR9C1nxc/GcjGz5JoIMBmk+svyPQBAACUqiSVVJIlhW3LLt/5eGHLzx8o8NTKUkh88555wNAN1pr+1T1VBFZx8w3icjB2j6a9bwmt2uzHJU9QdqLBgoz/1xVJxDRUQD+2Izj1fVaLalXq+FwraZZ//DWI/r7j3/4m998gprNSTkgyIE4YmgBLDkiWKUniXAOAAjwi2ra2Ocg+Ev3xmmvAAHgH6A31lz6oZzJEYCxUD6YCHczoUeU2DMMQCBSylzWtfHOu2tHHnXk9DjLHu/1bqzKzj4sZrarARUROUnbHq2/Q9tFrmtXiKPXe1p5GZ2K7QZvC4D/ApADcDSAJ0RkRa1Wy2rVBtXqVQwODMvkoZGtbxoZPuyBr3ytj5JkfJ5gQmWTJ0IR7AsEWKb1qvhIh8uq4WSkVYJ+6N/30HRfEsB/B9zHgWVO3fjIBAsIGhHRDIY+xNAygRVQgjBDlcW7YN0dd2bzDj7YTjXWPJVltVbqOUvTXOYz8c63VGU1M9cBHKKqU1T1NgB3S9uzfho68/Pdm+0uyRHR3UR0B4DtRHQgM5dUdV2apk83m83myMgI1RpNHR4aMLVavX5stV6sbNpaWP71i4qBl2IEaF6ZCyxSAEsemgXEW1RwGkGLANxI1riaVL4yp30YfK/ykmImPApzap7t3EKQ+067ctigwP2xYlwq4AYLN6AcgzQW9S0oeubM2fTmv/v0kY8Q3bUxF3bnCpHJBYFEUZ7CMEQYtnfjmDnsaOE07/047/16Vd3S8RZIvPdgZgugQkSTmHm6MWaYiDZ2oKejO3Rx6rXValAaJ9qMWzohTre9FXTy4z//5dLtDz64fx7gHLdHE3kYXxDhHINyxFsVeCsU+wKQetb6p1T844fA3/jn2LzkqB2PAOeUOSqHQefoP2EThP6YkUxvgnwLoi2QT1RLTZUkUTXehukxXzp/MJw8acyt3j1RZTOhkM/bMBeQNYHmcyEAAxMwhUEgaLurdcJmAUTtU/LS9kfU9jUABrIkJS8eBGiSJJRlmaZpqtV6S3oEAydZO6+1bVvfsv99yVjj0jBH5AsgHzKFecAVwFIAyCi2EeMoKGYBQJrF5zUlSQ4G/s9L4fIXxY15BPhqV5AfsRx2INIQAdd7yP4tQJseQQvQJpRarC4VlRQQU+5qHP2Fz6e5CRPG3dGsr9geBOMtIQzDnLBlCkyAIGBSJTWGoKoZGePEOQ8AbK1R7y0RWSfKDEWaeiiJpnEK5zJkzmfjVftOyOXfnG7v237vv12Wk1qtEAKImGxOyOZBvgClvAHyIDWglar0PwjtfjiV9Lx61iq8CfjWS2XyF0cuegz4+zyHYRTkvwmAFUiJ8WNRPTSGaqzwTYEkDI2hpuVhHSFLoGq7K4NHfvQjtfEHHHj8mlZ828NJq9HnsnHGhNYGhgwD1rYNhaq6dgwZoDM5sCKs4jLy4uEyp+KzrDsIB95aKJSmBdEJ2598+s4HfvaziqtWe3IgChQ2b8hHUJ8TQp5h8gREIEtKjwP4KLU9yKSZxV9IJPmL4P1VAAHgUeAjAduppaD0v9CJd2oIvxKlcgJfjAHTFLIxqcaKLGXlTGBa0CxTsWK40TNjZt9hH1hiumfMeEcs+sS6VmP1hjh221KniXrK4I2gHXiH4cnA+DwZnRQEPCOfs7ML+f0j4gMH1qy/88Hrfikj69aPZ9FijthZUBAxfCQkOYYtClHI6nNgH4AalrThFWd2CAwOp41/F3FrDwV+/pey+Kujtz0EHMXgT48NS3NBGI0XuAqge5TkgFhJEqiPgSCBaiJwKSRogbxTNY7IZ6rGkWYmFw10T506MnXBm3X89Olhvru7GJXLlSCKxgNAliR9rWp1JBkZafZt2JBufPghHtmyuUviZEygFAREYlXZErkIGoTgNMewEYhCIMsBlCMERvkpJX07FPsDACnuG0rrazzk8gXAn/4aDi8r/N2TwJgU+HlPWHqQiL/y3B39NROJE0yJiVwM0VQ0TIE0ZVgH+EzEZKAsgwYegIdmClivnCl5B2KFSHv8xWyhQlBjLElIgCOQDUFqAR9AA8vsQ4ADgc8BgWVKc2DOqQbM2KSqAYHeN1pCUflaNa0fTsCHDgGG/loGLzsA47WA2Q/4Z8sWXUHhQ9o+nAwFUkB/ysAYrzQ1IU0cqYmVxAGcifoU4IwhHmIgTI4FIuQEUAWI2lNGKFQIHZcxVmuElVgQgL0RmBDwAZOxgIQEEylcpBQxY6MRJI4weo4PAJ5pZM1rMnHuTcA36bnjZH+VvGIxVB8HZjvghyVbvCkw9hsKLXRuCSn+rzIEqtMzgnEKTQnkPJwzCJwCHuqkbTUcAeIERNSeAajCWoZqe2XcctuqBJahgUdmGUEAeEswgcIR0XoVWGqD43ZFqZn49CtN11rkgE8d3g7487LlFQ1CqwCtgDmVIH9bDIsPWeJ/1vYUa/T+kwq9j5WKRDQ5UwmFkHmAPRSOQOpJhSHtlcT2fI6gCiYigWGjsAplkBoAAWAMOBPVbSBtEOhotCMZjVYwdioXN9LGmwX844Xwv38lQyK/KmGQHwQCMmYJRD6UM7nbQms+D6V9n/cQ6VYAdwLUIiBSRQVAt5IaArwHgbQ9jFESNu1ItKbtLIFhIlQ73UQOwAlQmrRbzTaIc5c3fHyiMv9Cvb/2sOdicb1i8qpGMleAH7T2BPL6T9aYu3Im7CHQeXsvDQ1CZQVAI6QY0fZ0DqRaVEIXoF0gXgDVMXv7CoVeFvt0KPP+WGPoWwucu/Pl9nMvJq9ZLP0HgXFg89U8h+uZ7SWvRh4i7vyGZNNZ3DcOA171MPDAawhwNL8/sfllzuQ2M/EL9hdejojq5bFrTT1M/RmvVtj3Pclr/t8c7gRswdjfFzjHoOfCh7wcIcU9dYlj6927Xo1+7sVkz0d7XkU5HnDq3ftjadWVdNPOU6J/bSLd1pBWb+bdktcaHvA6AASAo4Bq5s1Xmz79tQKpoN3L/6XJAy7x2VXkzdfe9jJmEy9HXheAAHA00sdJ6QFR/Ye/FmCq+g9edeURSF8z5/Xd5TXvA3eXZRxcam00zMDukeX+nHwj88nYt/jnIvC+HvK6A1SA7jPhb4wJqtSOofBS5NpMsuKtLn3Pha/iGO+lyOsOEACWAXm1we+Ywm4iLPwzjz/mJNuSufT049vHJl5Xed36wF3laKClLjhbJHtEQdW9Gw2qZ97f5505940AD3iDaOCo3GNzx4HxVlZz0Z7ui8o/KrLlxzr3x9e6bHuTN4QGjsrbXHwXRJywnP8C7WP5AkHkjQQPeINpYEfoHpP7hRrapEr/0H5LL2fR8W/z8d/gNZymvRR5IwLEnYBlW/i9gJiACNBYXfOU41/ExeL1kjdUEx6V4wEnLlxCkCogfeqaZ74R4b3h5d6wNP/esDT/9S7Hi8n/B3LrBEUxxEM2AAAAAElFTkSuQmCC\"],\"colorFunction\":\"var speed = data['Speed'];\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n if (percent < 0.5) {\\n percent *=2*100; \\n return tinycolor.mix('green', 'yellow', amount = percent).toHexString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', amount = percent).toHexString();\\n }\\n}\\nreturn 'green';\"}]},\"title\":\"Route Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false}" - } - }, - { - "alias": "route_map_openstreetmap", - "name": "Route Map - OpenStreetMap", - "descriptor": { - "type": "timeseries", - "sizeX": 8.5, - "sizeY": 6, - "resources": [], - "templateHtml": "", - "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidget('openstreet-map', true, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"title\": \"Route Map Configuration\",\n \"type\": \"object\",\n \"properties\": {\n \"defaultZoomLevel\": {\n \"title\": \"Default map zoom level (1 - 20)\",\n \"type\": \"number\"\n },\n \"fitMapBounds\": {\n \"title\": \"Fit map bounds to cover all markers\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"routesSettings\": {\n \"title\": \"Routes\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Route settings\",\n \"type\": \"object\",\n \"properties\": {\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"lat\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"lng\"\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"tooltipPattern\": {\n \"title\": \"Pattern ( for ex. 'Text ${keyName} units.' or '${#} units' )\",\n \"type\": \"string\",\n \"default\": \"Latitude: ${lat:7}
Longitude: ${lng:7}\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n },\n \"useColorFunction\": {\n \"title\": \"Use color function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"colorFunction\": {\n \"title\": \"Color function: f(data)\",\n \"type\": \"string\"\n },\n \"markerImage\": {\n \"title\": \"Custom marker image\",\n \"type\": \"string\"\n },\n \"markerImageSize\": {\n \"title\": \"Custom marker image size (px)\",\n \"type\": \"number\",\n \"default\": 34\n },\n \"useMarkerImageFunction\": {\n \"title\": \"Use marker image function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"markerImageFunction\": {\n \"title\": \"Marker image function: f(data, images)\",\n \"type\": \"string\"\n },\n \"markerImages\": {\n \"title\": \"Marker images\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Marker image\",\n \"type\": \"string\"\n }\n },\n \"strokeWeight\": {\n \"title\": \"Stroke weight\",\n \"type\": \"number\",\n \"default\": 2\n },\n \"strokeOpacity\": {\n \"title\": \"Stroke opacity\",\n \"type\": \"number\",\n \"default\": 1.0\n }\n }\n }\n }\n },\n \"required\": [\n ]\n },\n \"form\": [\n \"defaultZoomLevel\",\n \"fitMapBounds\",\n {\n \"key\": \"routesSettings\",\n \"items\": [\n \"routesSettings[].latKeyName\",\n \"routesSettings[].lngKeyName\",\n \"routesSettings[].showLabel\",\n \"routesSettings[].label\",\n \"routesSettings[].tooltipPattern\",\n {\n \"key\": \"routesSettings[].color\",\n \"type\": \"color\"\n },\n \"routesSettings[].useColorFunction\",\n {\n \"key\": \"routesSettings[].colorFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"routesSettings[].markerImage\",\n \"type\": \"image\"\n },\n \"routesSettings[].markerImageSize\",\n \"routesSettings[].useMarkerImageFunction\",\n {\n \"key\": \"routesSettings[].markerImageFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"routesSettings[].markerImages\",\n \"items\": [\n {\n \"key\": \"routesSettings[].markerImages[]\",\n \"type\": \"image\"\n }\n ]\n },\n \"routesSettings[].strokeWeight\",\n \"routesSettings[].strokeOpacity\"\n ]\n }\n ]\n}", - "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8950926999078694,\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.2757675428823283,\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\"}],\"intervalSec\":60},{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.14481354591724638,\"funcBody\":\"var value = prevValue;\\nif (time % 500 < 100) {\\n value = value + Math.random() * 40 - 20;\\n if (value < 45) {\\n \\tvalue = 45;\\n } else if (value > 130) {\\n \\tvalue = 130;\\n }\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":30000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"routesSettings\":[{\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"tooltipPattern\":\"Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH
See advanced settings for details\",\"strokeWeight\":4,\"label\":\"First route\",\"color\":\"#3d5afe\",\"strokeOpacity\":1,\"useColorFunction\":true,\"useMarkerImageFunction\":true,\"markerImages\":[\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic7b13uB3VdTb+rrX3zJx6i7qQUAEJIQlRBAZc6BgLDDYmIIExLjgJcQk/YkKc4gIGHH+fDSHg2CGOHRuCQ4ltbBODJIroIIoQIJCQdNXLvVe3nT4ze6/1/XHOlYWQAJuWP37refYz58yd3d6zyt5rr1mX8B7S5Xo5/0nPYaNFM1PY0gGqOhfAgQCNBGlWFFUAYEIeihigbhFdZQwt85BV5Gj9r/718R2XX365vFdzoHe7w6d77xnPkn4YpAtU0YiizNJcmPNkMQFkDiSlowHt2HNtGlTSJ6B+pTpsKTfKgTj3Pi8SMtFtEZnFs8d8dPu7OZ93BcCHtt0+OiL+FJjOiqy5K5dtLwD4PBHGvy0dKLYo8B+1+lAldv50FfmFzWX+84i2M3a8Le2/Dr1jAKqCHtl2y1wC/pEMP9ZRLBaYzF8CCN+pPluUkOKfB6qlmk/dBwTyt8eOv2AZCPpOdPaOAPjA1h9/SJX+TyGXuz0TZi4EcPBeOk+U+RErZh2YyMAyQJEoZUjFgtkCAEScgDyx1hmInTglqDj2U1X0WILaPbWvwHO1WummeuLONhaXHTf2wsfe7rm+rQDe133j/i5xPyrmCr+OouhSKPbdQ5fLiezTIYUBQGMJBgYWxMYSISZhbxgQT8wGAgDiwWxUvCiBxKhSKOqdh4OyV5+6XiEfK/kjVOXQ13apG+I0+adKpXaG0/Si0yZdvPbtmvPbAuCNT98YTBhT/8fAmEpHoXgKgPe/6gFGP0nwG8s2YykcaRCAYYQ5tKTkDVuArDEwMRF5AICS4VZ1AQBSr6oEgL36CBAvlKqIsyLOKQl5TZH4uN+TawDuY6o64lWTJX20v1S633uJNvfmvnbRERelb3XubxnAX26+5gDy6Y9HtrU/wERff1XjSt0WwULDmZEMawPOgilgQ4FaGCEygaXQMQyRMaxiUijUkAEAImIGAFURAOrVA1AmI1ZExGuqoqkVFefhyGtKDql4X4eHc6LxJof0VIVM3nVc4uXaHUPlo0Tpc2fv/zer38r83xKAd6y74iImO31EMf9REA7cpdVBY8NbA5+dFNqsCTQipkitBjAUsLUZNd4qm8AyjDMmJAIRhDzDEBEbJkBVAyJWQJ14AEaciIeSGicOgBeBWNHEeXLkXIM8UvFI4bVBCVJNfdk7STd5xOcp0LZzjIqV/eXq/4i61edM/eaN7yqAqpfzf62Nf5LP5lbko/DbCuxU4saEN1mN2kKTzQbIkuEIEWfVagRDEVkOyXCkVq0aDg2p9YYNAySVerU0WN1R27Jjo6ulMQ1V+ggAOgsjNRNEus/IiUFnYUy2kM23AcrivXh2RiTxjhx5iSmVWEWdpmhQ4qvwSBBrXVPfqDmuVsT7C3aZvKslyZcr9dpxdr81F8ynO/w7DuD1q/8y6kDw2872ticN0deG7wvQHXHmdxGK+1ibQag5ikweliIElNUAEayNYBCSRQRiYzf2rNtx11O/rC5d9dj+1aQyM2Pyz3WGozaNisYNWY7SYtgWA0A5KUVO4qAn3t4+lOzYt+Grh+bDwstHzvjA2tPfd1Z+39FTRhGpi7VBKrE4nyBFDKcNJL5OCerqUEXdVeEQb0mk8lECjR0euxe9cqBUOnoQ6RkXT78hfscAvH71X0Z5kf8Z0dH2CgNf2NkI0d0ZbmtElMtFVEAQ5BFIlkKb00AzFJqCGooQcJjv7t868P3/ubayZvua48ZlJt57xLjjB/cpTssXokK7IQNrbeoZ3pIRJm1aYSUW9cwixglZ7xNU40ppY7mr+sy2ezt7G1s+vP+EGfd/+fS/Ko5pH9/pJK04X6MUDSRapcTXkXJN46QKp1UkqNVqvpxVyLzhOajihh1DpVkmrJ7+uak/bbztAF6/+i8j62p3j20vbgXR+cP3LYU/Djg/KcsdEnIWERcRIk+hzWtEOYSch2U76tk1T6+84Tf/NCdni2tOmbRgy6T26WOiKDBhGFEQhrBhiNAyjDGiQp4DFgI8AChg1BGBXOC9p8QJ0kas3jvEcUxxnLgNpTW9izfdOqGWlve7+OOXrThk6qEHKtKehq9xIlWkvoaYytrwFYqlglgrcZxW+oXSz+ycpOLmnsHypDTIfuTNcuKbAvD2288x22dn7hrVnt/ATBftBE/CH2aCtqkZU6CI2hHZomS4YCPK+5AKHFB2ZNe2Nev/739/e9qY3KRnPzHtQp/LtnfkMhnKZDMa2oDCTIjQhghDC2MCCQITAyYxpmkhAIAZDDA7l4bOSeR9YpLEwfkUjXqMOE0QN2LU4waq9aGBX6/+d7O9sXnu3579jbVTx02dlEilL0FDG1pJG64cJX5IGr6MupY5duU1npIv7sTQ4196ytUDx8+sf+TN6MQ3AyBd8+L8W0a15zYw0d8O3ww4vC7ijlkZU5QctVPE7QhNEVlTRNYUjHcy7tu3fuuVSqXBF8z66962fMeIfDaHfD4nmUyWsrk8BdaYIAh9EFoxzExEysYoAQ5A0ioAEIpIBGZmAM459iKaJo6cT209TnyjWkOSNLRWi1GtV9A3sGPg56uvG1vIZ9N/OO9rM8jS9oavSOwqaEhZYh3khq9K3fdpXWsbvdR3MoYCV/UOVadcOvv2C/AG9IYAfue5j1/U0R5mIhNctxM8yvxLyMVpOduJyLRRnto1MkXK23axlB27sXtT1z//8vqDTt3vk/fMGnX4xGyhiEI2Qi6X1Ww2S7lCIQ3DkCxzQEQKYADANgCbW6UHvwcRaO6fAwCjAewLYAKAcao6UkRIBEniEtRqNVOrVKjeSFCP61oaqurKvqe237P2lnkXn/X/PT9l3OT9Eql2V90QN1wZdRqSuhukhi9T3Q2s9ki+NDzHWppeUqnG/qsH/+b7fzSA33ruI7ODIDh/RCH6KkEZAEINfhia4n4ZO0KzphN5005Z06aRaeOAcjP++4Ff3P/86hWTLjr08i3FfEeurS3LUTanhVwe+XxOwjAw1loLoB/ASgBrAdSAV232Gc0NyJGt70+27mlrzNT6nAEwDcBMACO892kcx1KvN6hUqWu9Xka9XsfgUP/Qjcu+Nf3g6bO7zj7urBNT1F+quxLXfUkaMmDrviQ13+8THdqYqvuLZpfq+qrJNXFDbrp87t0v/cEAXr5iduiTMQvHd2QnKDC9+bC9NUfF9kwwgvNmBGW5Q3O2SFkzAkaCg/71Nz9+2MTZ6rlzLs4Vi0WbyWS5o63N5fM5G0VRaoxpA7ChBVw3ANMq1AKoHUAewCwARwHYvzWctQCeaNUrt4pvgeha17Gtevt47+M4jrVSqZlSqepqjQpVyyX/8xU3VBHF2T//+OeOFbgXaq5fa75ENR3SarzDxDToYz846FTORbPRV7oHG9sm+qEPX3TEM3vc9pm9AfiBP53+T6Pbwo0Cd4aog4p/yXK+lDX5IDIFZDinGS7CckEM+JB//u9/e3Z8NGPTgjl/Maq9s8N2FNtcPpc1bW1tFIZhaIxJATwFYA2AtAVWh4hERBQByIgIE1Gsql8gou8AeAjAfQAeVdUvEtE9reFFIpIloiyATgARgCqALQAGmHmUtTYTRWHDhhaGYE0YYmbHEXZj//rBRc/fXTly5qGHEus2FUceCbxP4DShRJ2mvuIFboyqG5kNcNuWVM965MbNd71pAC99+vADA+MnR6F+TeAg6h1TeE/I2bbAFjVLBbJcpIDzZNke8qNf//yxKblZWz42+9Pj2opFbutop7ZCQdva2hAEQZGZXwGwDEBDRCJV7VTVfVV1BDNPUtXZqnomER2tqi8S0REAzgJwUqvMI6JBAM+p6pdU9f1ElGu1E6lqUVVZVYWI6gA2EFFijJmSiUIPsDbXmGT3b59V6Kv0dd334uLGYTPmHK7Q7lRi65DCawqviXWSrEm1PlvgWMh9KPbut+/77Ohtj/97d98bA6igo7aM+O/Ogp0l8BNFPQhyY2RyE0MqcC7Ia2jyGpksBYj2//WDCx9uk/EDZ8783JhiW5HbigXpaG9HNpvNMXMGwAoR6SWiUKS5KhERS0QqIgmAHcz8sqrOA7AdwCcB9AK4CcBvAdwP4EVV3V9VPwGgC8B4Zv4PIqqoqgPQYObEOadExC1A60RUJaLxURQaZqoRW0NEsm/xgI6u7rV9L295vmvGlKmHQ32vk0QdxfA+oYTq+Vgbi70mR4p6BEaKlTid98S/9f4MV7wBgF/66AEnFbPUz+z/VNTBiywLgxxCFDgwGQqR5wznOeR8+6p1657r6uopfu7wv4mKbW0oFvIoFovIZDIBEXkReUlVG6o6Fs2N/EjvfSczj2Hm/YnoY6r6Ae/9w0T0cVXdSkTfE5FsC8iTAZwI4DAAjxDRj0TkUABTACxS1csAzG39MHlmzqvqGCLKt1xZA0Q0QERtQRBkDZMngrcmNAeMmB08uHpxNsrz2pFtbft4TWInDZtSLE5T8i7uSKRS8XDjBX4fYbnusI2jMkt/tGP9rnjxrl+gICP4Riagrzb1ssKa4CkrYRhwwBFHYGSUOZJKo8oPP/vCoV846opSoZCnQj7HxUJRMplMgGblR5h5wHtfbE1oZAvIHBFtVtX7RKTQ4pSrnHOXAThQRK4BcIaqNkTkRRF5UVUTVf1462/TVPVSEfm2974qIm3MvBhAl6pGAEYAaBcR45zLiUiPiDxKRC6bzZpsNhtGUaj5fIG/dNTltYeeWja3ltbVcGgMZX1IWbUUqDUBbBA+OYxDPuDLSORq6KsN76s48MvzZnwwlzNDgaFzAIBAi0LKtGVtEQHlOaQCQpOHoWDWL+9+ZODCuV99cnTbmM5cIY+2JudZIpronHukxUWemavOuZIxpuG9H8fM8wDMJaJHVfV0ANcDOIyIPg5ghTHm+0S0UETWq2oCoA/AI6r6C2PMgyKyD4BPM/MggJ8COIGIFqnqV1T1YADbVXUjEfUaYxrOOcPMBVXdCmCutbZirQGIlIBwavucl2577NaJM6ftO1nJ9aY+YfEpvDryknamSNdAMQ1AGwxdc/DqDjz9k/7Nw5i96ixBSK/MhTRxJ7oUbracmWAoVGNCtRSCYOxLazfcN7VjdjK+beK4KAqpkMtpJpNRABNVdT2AowHUvffjAYgxZpNz7hUiuk9VT1LVWFX/iojuBfA1IrpfVRcS0Xne+6tUX33+M/zdew8AzxljLvPefxTA3xPRIufcpQA8EYUAFhPRSCKaKSL7EFGgqjtU1RDRZmaeGIbh1sh78s7LxM59R09um7585fqNdtqUMZOMMc4igE0DthSppcYWL80VTNbyX1QCPgNN1fJqDvzi0tnjQviObGia3Ee0JEAml+E8DOUo4pxaE4GUJz3yxJr9/vSIv+8uFAu2kM8jl8vBGNNJRE+q6grn3AZV3QRgi6q2AZjHzHNE5FEAp3vvv8HM8wFQSywvADAPwDgAi0TkPwDcBWDhcFHVh9FcXH9ARE4BMI6ZvyEiHwYwSVW/CeB0IlpERJeo6hwiepmIlnrvVzLzemZex8yDzDwZqlUikGGm6R0H66+evuPYafuNynvFkCCF4xjiBd67otN4C4GmEDAqTuVnR3++beWT/z5YfRUHio8/0dEe7DynJTUvswmmEiwxWcCDwGyee37j4ydNO6ucy+YmZMJQM5kMWWvHqmqPc24eADCzENEGAMvTNH2AiM5Q1W1E9GkR2cLM3yOiS0TkO0R0lao+zMy/8N7PBHAmEZ2C3YiIoKrdqnqjqq5i5j/x3n8bTQt8iapeKyKbjDGfFpEhAGOccw8EQdBhjPmQqk723rP3PrTWvhxF0Xgi6vHeayaTyx075fS7nlvxcPGgg8ZNIjHeSKRMdbEUIEHwEuCOA4DOvB25vSRnAfghMGxEFNRb7ZoM0HFNadFeIjvRgMFkhEDKbEl8Oqq7u3bs+/c9cXQUWo2iCGEYsqrG3vvHAPwEwL2qulZETnXO/Zm1FqoKVf2Bqh6qqr8SkW3e++tU9T4i+ntVnem9vw7ARQA6ReQ5AL9yzl3vnLsewK8APIfmovkiIrpWVWeo6t977x/w3l8nIluI6Dcicqiq/quqgpnJOfdnIvJR59wmEVlCRD9S1QeJKLHWmmw2hyAM9bhpp47q7q4d733aSVBlkBoNQGxgYPdVRZ82N5In9lS7dp42GgA483hMyUY0RXgwXzAjQgUtshp1WhOR5YgDzoiB0U2baqsPLB7z0oxxBxWz2Rxls1lh5gNVdbn3/rwWR68moi5VPZWZt4nIvgBGquoRAH5BRH+OprH4oYh8XlVPQXMvfIOI/BJAFxF1qupxRPRBIjpKVSe3dOtdInKbqj5PRIe3RHayiHydiMYDOIuZfyIin0HTfI4kIgAYa4y5UUQaAI4QkY8ZY5YR0aGq0kcE8k5NNS4t665u6G9r47xDCi8pqabsNbFe9WkoRvU0upYl8GunnqebX7kZQ00O9DipLbKjRfQTPWnXYyBTBxMBBiIML2IVkt20sf6B46d9rJjJ5chaQ0EQRAC2pWm6VlVXq+rZIvIXSZKELcX/Y1U9RlW/AWC8iJyqql9V1aOcc99W1SXMfAmAh1X1qy3O+rKIHCMiGRGptUqude9iIrqWiC4brisiDxHRt1X1KFX9qnPuowDGe++vUNUPishNLQkIiOjPVPVs7/02EVkLYHsYhtYYg0wm1FNmnZPftKF2lFPJisCIkhE1DFiFaNLr1i5R+PntGR5lFMcBLWfCxxbhrgkjgqMAjCKgkrWFX48KZ7RHJm8CziJLOXJpUNu4omAuOfbKOMxkKBOGHIbhHBG576qrrtLHH3/8QmaOdtdd/5tIROLTTjvtyc9//vN3BUGQs9aOA3CyiDxXr9dRrzfo2gf/Ljt1TpyYIMnWtQ4nVW2kNd+bri41fOlMADkQerb1p4/f+WGcaS9X8HOLUQIwCgCUdFGi6ehBt7k+3k4DqQ8cOd2+mQdPnP6xijHB+MAYhGEoqppL03T/J5544iRmpvnz5z+4Zs2a1dOnT5/+8ssvr5o5c+aMWq1WSdM0VdXORYsWHW+tXXbmmWcONV2jQG9v744dO3b0jR07dvSIESNG3HbbbbNFpHPBggWPtMTvVUREWL58ee2VV145bcSIEU+ddNJJ1RY4unLlytXTpk2bEoZh2N/f37dw4cKTrLUdxWLxvnnz5pnf/e53unDhwhPa2tpWnnfeecekabopCIIMEYGIyBjGCfufvmbpltuKY6a4LKkzCh8PpZu913g0oIsAOhOKMQTElyvYPrsY43IRP6uK8wCAYHrUo+gpiXoaG+LR0X5VaNgxNEAHz5pz6PIgMGBmBTCKiJZVKpUjjDEmTdPG/PnzPwSgLCJHoLlY/omqXgLgWSJauHjx4uNPP/30obPPPnsAwGNoLl+O32Xdt/a3v/3txnK5HM6fP/+3aJ2JAAi89zkAUwGcdOqpp+YvvPBCnH322fEJJ5yQA3CH9/5YY8yft0C+SkTmP/roo72NRqPjhhtuODCTyRTPOuusRy+88MJVd9xxx8cWLFiwiog+oqp3ARgVBMEO7xVzJ70/v2jdHbNGqu/16uq98WakmuQgANhsU98MRQwMP7N0iYxhUuybD/n3WzqlAMROROElzfY3NrXHrtTNFHTkMvkiGQNiZhGZ7ZzbPDx5IoKIXK2qZzDzd9F0T/0pEV2qqoeKyN8BwLZt27ap6hmq+l0RmQXgZhH5iohcpaqrwzA0RATn3DXOueta5buqeoWqnqWqT9dqte8DwPbt2zeKyBGq+l1m/giA7wL4map+jYj2S5LEA0AYhp0AvsvMp5577rn3Axi/YcOGxaoKEdkCYBYzqzGEMMgUWILRjXSopzfekFUf5wUKYXYQCoZhykcM08C+DMUMw7Rva8sHqHZCJFD1VtTDaYLuoe3xrLGH/Yu1NiZVtcYAQEVVy7vpmPNU9VHv/RUArgZQ9d5f473/qYj8OwBMmDBhPIBnnXNfAfAj59w5AK4F8DURmcfM1JrY/4jIrSJyq/f+XlV9vmVMPlEoFC4GgM7OznEicmPrB3hJRC4Tkc+IyI+897cFQWBay5lrVfVKVX30lFNOOUZV/aJFiz7YMi79RFQiIgbg2NrazHEHf7+70q1eGiwkROoteQkhOmIYp8DQBGUcYIVwOJMepCCAkBCooCAnUPVwXoU1rrXVoyi7nwgoDO1QyymwzTn34d7e3p8B+NsWFx4AYLP3/l4iuoKIHhaR/yaiLw1z6rp169Z57+cR0bUiAiIaVNU7ReR5Y0xcrVbPbf0ek1U1DwCq2qOqG4jofhHZUi6XAeC7IkIAvqCqIKItaG4LZ4jInxERvPevtK5fY+b7W+0eBGD78uXLx6nqd51z85i5G0Bore1rNJJsxuan1EumFo3w3mtKSupAMASNRJEACBk6ixWphWCaKs1tqegVUIWyiBcPIYhRQlLKhQccNDtW9YEIh0TkiciJyGFtbW29LfCCxx577PtHHHHEhdbabd77bzLzFap6jPf+X5o46Jf333//qWh6kP+P934HMx8F4HQA53rvkc/nl9frdYjIQbsw99SWy6opPvl8BQC6u7u3ENFfq+poVb1IRK4iIvHeX7dy5UpKkuR8Zka9Xv9WNps9n4j2B/DNkSNHnrV9+/ZRIvIhIjpMVZeoqlfVEcyQ6WNmpQ8+nyva9m4IO/XeQ1XFE6UKfYkUhyrTEVDEFkAWO4NuZAuAsPnDKlgFzih8ku0cU5y4NQiCxFrLAPYDUCOizxpjrgAAY4y54YYbvtwS5f1E5B9UdSgIgloURR8BIESEO++8c8qmTZtetNYeHYahdnR0wHv/pIhsrVarvX19fQsA5H71q1/dYq01pVKpkCRJXCqVaGBgwDcaDdfX1zcRwDELFy788JIlS96XJEnBOQcADSIKmfkSIsKwpXfO/bmItBljLlHVa6dNm/bIE088sR+AMUT0WRG5kIgmWWtfIWPcuPZJDJ9r90hIRVTEq5KAlBIIdYH0UCg6FMhZUvDvjSDVnZBhUhUSUijICxHCbDFXZGOMqKoH0KmqQ/l8/ptdXV0/rlar38rn8zs5hJmJmUM0jyPb4/j3h/ze+ylLly6dgr2QaepX3Hnnnefv7ZmdoyUamyTJWABoHvTtmbq6un4xa9asSQCuA7DSWvtSo9E4zHt/dbFYvKLRaKwF0E5EwoBENlKVMOPFkcJDCRBVUlEloLQTLgWz1987FAhImCECJVEh8Z6cdzBk20ITkIg4Y4xX1ZFoHuJM3XfffT/S29uLLVu2oFKp7HQ9/W8ia+2RzHyGqv6TiPzjsccei97e3kxbW9uZACYTURVNb7mIiIYmJIOwLUWqTqQVIqFEDFHV6nC7orDMBB22LOzhWbRC0LJRLalqGYqyQWAJVDPGVJIkqQPYrKq9AGCMmQoAaZpix44d2Lx5M/r7+5Gmbzn4822jVatWvei9/9M0Ted77/9j5syZawAk27ZtswCgqt0AtohIzRhTssZWDdvQkA4RtETaxAOqZSWWnXgR1Kr8/kTbG2ThtaAE9QQSZWIQ2EilFteyhoJCa4lxYMvf9xry3qNUKqFUKiEMQxQKBeRyudcVsXeC0jRFrVZDtVrFzTffnOnp6Tl2/Pjx944ePXrt9OnTzyGirY888sjLCxYsOERExhPRDGvtswACrz4m60pOqIMIBIX4ZqCYAWsZLXumAtid6z8A5DSvlgkKFkcMiBERqHUDiUu8994SkQCoEFF+jyPfhZIkQX9/P/r7+xEEAbLZLKIoQhRFbzugzjnEcYxGo4FGo/EqCejp6Tnv5ptvfk2dH/zgB8sWLFgAVS0CqHjvyTlnq2mFYF3VORnJICKwI2IFI0Qi7TCtLaYCVgnbAdoA6GRhaoPXhipIVJkEUCXP7CrleBAd2RHsvYcxpopmfMreaICZN6LpQWYRmZSmaeeuk7LWIggCWGsRhiGstWBmWGuxqwUFABEZ9ilCROCcQ5qmcM7BOYckSYbd/XuiTczcT80YHHjvZ6MZZ4O+vr5hx+14Va1Qa/M9WB0Asa+SUCcIRuAtg5QEBKDYrEJrwdhiIXhBRQyIJkMxQxQvkELh4RUq4kCJ2VHdOLiOx+YmmTC0trWwnQOgsvtoiegFInKdnZ3rRo0aJT09PTw0NAQAm0VkzvBzw5N/B0mMMU+pqhk7dmxXsVjkzZs35xuNhojICDSPRpPt27c/WSgU5hLRC95722g0aOPgWnbcW5VUBYCSJYBBChgQzWnt2J4BsJyheFkVr7Q6Hc2kZYU6ARSejCjZFN259UOrc6reOucMEfWpqnXOPQIAhULhN8PgMXNl3rx5Y4IgOIuZz46i6KyTTz55JBFVmXnFO4nYrmSMeTKKooEPfvCDs40x8621Z3d2dp566qmnxsxcArC1s7PzkVWrVi1X1QBAv/eeiYg2DK0upOgpiCBQIlIBBOrBOgTCCAAQ0jUQrGS1WF1vUPewLlTlKoQCOARewOqVUgzmtlXWTWuKiqiIVAAgjuOtuy1bgtNOO21ET0/PhO9973sQEXznO99BT0/PxJNPPrkDQAO/97C8k7RBVaO5c+ce19nZmb3yyisxZcoU/NVf/RVWrFjx/kMOOWQ9M3dXKpVRjUYjbKmGinOOnPPYWt04PZGhjHoQCZigAQsFpFwbxqlRpx6k6LI6gK5Kpz8zm20d0JHWQFAYTSUlALDexSNdEB+Y+nQxpZRlppSZ4ZybdPvttz9QqVSOt9Y+SkR+xYoVxx522GF4/PHHceCBB2LZsmWYPn06nnrqqQOZ+REiekZERr+T6BFR37hx47rWr18/NwxDvPLKKygWi3jhhRdw5JFHolarzXvuuee60jSdYFordxFJnHNI0rghiGc4jb3xUDEQEngyYEBrwx7KcuJHZzux1t79KZQ++iv5AHTnCadVBZGQhULh1SsIMfoe7KlsGRqTm5Q1xmkQBJtV9dijjz766f06bwAAEgVJREFUnpUrVy4EgIMPPjh300034bjjjsOaNWtQqVQgIjjqqKOwZMkSzJs3b/Xy5cstgFUA3rZF954cr6eccsrYxx57DJ/85CexcOFCDA0N4cQTT0S1WsWjjz4azp49+4l6vc5Tp049TVU3eu/hVXVbZUN/TH33k8c4DVRIiMFEohCjCIdXLC6VY+44DV+zACCEXiiWgnCkEp1EpKsEqqTEIsTq1Axg+eCy/kczp+QmqDZfuXpRVedNmjRpx9VXX32hiEBEsHTpUtx5551YsGABnHM47LDDcNNNN+GAAw7Al770pc8NPzdsUXe1rsOA7n4dBmjXK3NzgbHrZ2beWQDg7rvvxq233oqLL74YS5YswY4dO/Dkk09i7ty5uOCCCz4bx/FPRGSUiNydph71ap2W9T9eGGgsr4iqZSVVsLJ6Z5lIlU5srfmWAlgHtE7lDjgP5SjgAWb6MBTtoroMgpwoERTwniiJhwq5aPrxB+YOWwuQIaKEmWd573NBEHSoKosIpk+fjltvvRWqitWrV6O7uxvLli3DV77yFRQKhVeBtzcgd/2+exmm3bl3dy4kIowfPx4LFy5EpVLBpk2b0Nvbi+7ublx22WWw1ro4jgsARgJYVq/XUG/Uk2fK95+ypXxfrESGGUIEMhYGTP1ovQOYOr2+kcjvVt+K9c130cp4slyX4nDnBqYbRCAGkTZXUELIVtPeezeUu3rjOEaSJFDVpwEcmKbpLcMTnDhxIm644QYEQQDTPDvBNddcg3322ec1IL1e8d6/qryZOruDffTRR+PrX/866vU6kiTBAQccgOuvvx5hGKI15hki8lTz76lura/fUUt6F4siJIKCiREAakhB6BnGp1ST9lwbngJ2CfE99Zd4cPzIcDqg4xl4wQl64EE+BlyicCnYanHz4RMumviR9vO7C4UC5fN5JqKzVfXlKIomtzzGr5nwGwGwOxe+ngi/ntjuXowxe/s+0Gg0+ohofxG5o1KpoFqv6+LBn496dssPt6dcmWAtlCOCNRDKgJgxEopDoLRl60Cy5p5P4Hhgl/A2NbgmTuUGBeCBOUTokVZAtyiIFJSk5QmJlJKeyvaeer2u9XpdVPVxVZ1Zr9dv25PI7Q7M3sDbEwe+0Q+wt/b21vdwqdVqv1XVaar6eJwkqNdj9JY3bW9IKU5cZRwUDNPcuagBE2G7Kg5RAKnI9SD832HcdgJIARYOVdyknXtjoTpBoaRsTPOMHQy7fMutQy/qQzOr1arW63VNvd+kTc/NfO/9I3vTXXub0N5E9/U+v57Yvp7+VFWkabpYVc8DMJSm6aZyqcSNRk1fxOMHPb/5v+pQtWwgUBCxErGCiOJhXHYMuRkU4r7XAHj3aYhTAaC4rakI9dNkMMSWPBhMSsRKmjRKIyuuZ3Bzfe32crnGlVJJReQ+Vc3HcdyuqgPD4re3ib1ZHfhmVcDuYO4JxNaYetI0HYvmMen91WqVqo1YNqVdW2uutz9NSp3KTNpcxMEYgjEYVNULmvVxiwLVu09D/BoAAcAZXL6j7F9SBVRgiUwPkRJYCQaqrEoMWrrqp4WN2ZfmxXGtWq7UqFwuJyJyP4A5cRw/qKryelywNw7ck+58I336ZvtR1Uaj0XgewMEicl+5XPblcpXqtXJtk33x1KUr/6MAbnKdgQKsDFUVMTtUYFWBvpLvohRX7orZqyJU192K6tSz9Qv5HPcQaCpBZyvjRSiyEFIVkDioiBbL1W3LglGduWJ9LKDExnAtCIJEVU/w3t/MzIfsbiD2dn0jHbkrF+1qSPZkXHY3MMNX59ydaB5ePdNoNLZUqlVfrpSxOvO4earr5xvqvm8iGfggBFNIyiGYQwwQ4xwABqqLhmo+c885eJVf7NUx0gDE4iv9Q/JYc1+MDABvDJQs2DDYhlBmxD2Da6YNxOulW9dsr1TLWiqVtF6vrwawXFU/7Zz7TwB/FCf+MUuW1ylJmqY/F5GzVXVZvV5fWy6XaahU5q26asuA22L7hlbvR4a8NVAYKFsgMBACJZDm7mNHSZ41HpfujtdrovS7bkV58p/oRwpZ8zIIhwM0C0SLoBipCmqNnaHAhq3L7MT9D9mfhjIrrYRt3nu0fG9VAKd673+Npq8t82a5cW9ADdOb4bZdljfbRWSpNt9BeSJJknVDQ0MYHBqiwXRHd9+IriPvffpa4YBCE0I5grCFMRlSGFoF4DMt3ffDUtXLPfPxyzcEEADGnoNH01gWFLNmChQhgTJEOqiKQIQEAiPNU09+Zf3jfZNnH3yY9mVWasoFL16sMWVm3gzgNO/9KiJaq6qTdlfyewNv9+f+QNCGPz8qIgLgaFVdVK83egcGBk25UtWBel9f/4Q1x931yFUbYLWNIxgOoDYgDSJYE6IB8CEEjFKg1D2QdscVfHn9r/EaB+YeAdx8B9z0+Sgz8HxgeR6AMVB6hgzaVMk3Q/2JSQHvJOra+GTXlMPmfEi6o+d87NpTLyTeN5j5ZWae6b3fV0RuIaKZqmr3ZJ33BNzuAO4G0B7vMfOQiNyqzcBN8t7fN1QuN0pDJVQqJe2v9u2oTt9w0l0P/uNz3iQjghA2CMmEGXgOCSYDIqJuAk4AgHrDf7We6u/uPx97zO6x13fl1tyOtfucqRcXM+ZFAHNAmA2iu4gwRkBKos0jAVXy4vKvrHvslWlHHHZk2m1eQKJ5VfXOOauqG4Mg6FXVj4nIalVdpKoHqSrtsrzYed1VXAHsDaQ9caAQ0S0iMoqIPkBEDzWSZHWlXI6HBkvBUKWsQ2nf5uSA7SfeueTqFxPUxtpQAxMSmxBqAhKTBZhoBYALAUCBW3ZU/D6Lz8E1e8NprwACwKQv4nf1fvlUMWsJwEgC5oDpIVJ0EhGrJ6sAICCXuvYVqx8uzXj/YZPSWFbWelyHeA/nPRLvqwxa3XRN4COqugrNKPwx2ozifxVww1y3K4CvA95WAHdQ8xWHDwJY4b1/tlwupwNDVVTKQ9rfP6j19h3dsv+Ow29bdEWvUmO0CWBshowJCTZL3kQAW1pPTb1noPTK9oG0no7Cp9b/7LWi+6YAXP8zuMnn4rFG4kfnQ3MYgIgIU5jxDCmKCigBpE1xZlEfvPDSErffrFkU7BNQpSutxQ1PLo6zSerFi9RV/CvMXFXVQ1R1H1VdhGaIbxnAzgQ5u4vtLsUx8yMA7mPmbQAOJKI2VV2XJMlLtVqtViqVaLBUlUqpn0vloTofOhBVMptzv1h4dd4Yn7cR1GSJwwhiQhIbIjUBthBwJoC8ElzvUHqzKL5+/+l4zQuGu9Kbyplw4m04Ix/xjI68+W6r2gZifdI1dFSaEEtdOW2AJYG6hnqXEMaOnL7ptGO/+L5kjVks2/JjM5nIZKJAoihLmUyIIAjIGANjTEBEHSIyWUQ6RWSdqm5V1YqIpC3RDImoQETjiGgKM5eIaKOIDKpq4r2Hcw6NRgO1egzvUq3V6l5Hxhuys9OPP7T0lke7tj41nQNiG0FtBmojeBMR2yzIRNhKQh9U6L6kkMGq/7t6Ii8uXoDfvRE2bzprx0n/hc93FLiQi8x1zYq0CdAHvcdkV4V3Dupi9b6OgosR+wRGvU3PPuXSHcXcPiMGnvAvcJIZlwsjG2UzMESUzWa16SExZGxLGFS9sVbFK5SUAGBYWYoIMzN5BbnUgSCaph5xXCfvvSZJouVaw1NWejrfL3NK1a07frHwmpFsXcgRvA3hTRahNeRsHmKaXpZtIDoa0P0AoBb7SwZqEt+/AP/6ZnD5g/LGnHwbvtlZCAYzAYbzJwwo4U5xOl0aUB8jcDHUxUSuoQ4pJE0gmbCt9vFTLm4UM2NHDCxNlidDweiQOAyCUDkwFLBBEFhSZrVEqkDzHLEVAiA6PFBFE0pFkjhS9YjjVJ1Lkfg0sZ3SO+rI8NBSo7vvznuuz8S+lDMhwBbWhmRtVr3JgmwAmAhqAlolij+h5svfqMW4ZKiaFu49F1e/WUz+4MxFJ92GS3MR246M+bYSGEAizD8mJ4d6p+oa8L4OcQnUJzA+hhWnqU+gUdA2cPKxnylNHj/rmOrW9N7+F5JGOiQjyXIYcgC2zRejiVXFw5Np5Y3xMGxgxBMJPMSlFHtPUI1NG/eNmhNm8uODUzZse+nB+x78WVs9KXXaDMgYspyBNyG8iQATwIRZwIawYPOCQj4LICSFDNX9V6qJ5O5bgH/8Q/D4o3JnnfhzfC6yvM/IdvPXADpaLd0KoaJPNS+xmjSF1QYkTeEkVfYpGR8j9Q5WRKvjRkztPf5DC3j0iCkn+AQvlDdUu6rbXaPWn5KrCEEErTwXTTKALbDmRgSaGxNk26bmppoQc7p7ux546PE7ZHvfutHGUJ4DOGMRmEi9sSQcwgYR2GTgOCRvDFXVaJUU81sA9PcM+X92Trru+yT+8w/F4o/O3nbyrTiaGF8cUwgOIMZRreZegerDgB6YJiQSw0uqgYsh3sFrjMB5eE1gfAovHka9pjaM+ke2TxiaNnWujBkzOcxnO/KFXKHNBpnRAODSRm+lVh6q1odqPT0bkjXrnuW+oS3tLo1HsKGADIQDsAnhjEFAFgmHsDYCmYBSG4BMRgMQvQTQcYBOBwBVPN5TStd6hxvuPx9L/xgc3lL6u5N+hpGwuHl0u33a2N/nDiTSXxBIRHWCNMilMdQ7DSVF6h1YUxXvyKhD6h0CCKCCVLxa9YASKYlyK/AOIJAyCUFBDGImB4KlEEoMbywCCtQbQ8QhxFiEJqDYWLDJakBEm4g1UKFPDI/Rq16xY9AdZQzOXzgf/X8sBm85AeM5t8P0eXwtItYRbfZToOavCyDxKj81RCPgaKJ3iL1TAw9xCVgdvHcw6uBVm/pNvQIKpwJV2pkKBQCEFKoMYoKFITVGQQxPBsZYeLIwNoQQw3BAjiNEzNioQKzAebQzkJRW9lXcbXEqctx5uOryYUv1R9LblkP1+JsxjS1+MDJn7wkDuhKEHACQQqD4OUgExJPFq/EpqTglcXDqEXoPJYETDwbgROBVAQY7ABCIJQKYYQBYZogaWGMAMkhhEJiQPLMaG5BTlvWUsgXjvJahAxS1RqpfH6i5eYjxhfs/i7clj+rbm8VXQSf/HB8T4LOj2uwzgaF/0GZ2oeHuVqjq48zIQzHee4QiSLUZgwN4kDYdt0Kkqq38BM1XhYnAMMwKGDQ979y0rERIRbENQJWIPgDorF0m2Ei9Xt0/5N4njH+//zzc9XamRH5H0iAffiOC9gLOVeD8kXl7bxjyxYC+OqMv0VaoPsCEukAigNqg1EEEFlWBQKHUFC9SBoOYiEUhRDoIaInBiSgyBDpJoeN2m9qG2Mv1/SV3iir+s1zFbc9chLc97vgdzWR+uYIfugUnC/C3keUlHQXTaQiX7LUCox9en1XwIBENCqTcvM1FVe0gSAcMzYVgxN6a8IrrBit+IHFyrCF850Orcf/ll781Pfd69K7l0j/mJxhtLb4+ot2uDy3t1T30Vihxeml/2U1WxpVLPol3PA088O7/MwI6/ib819j2YDOb154vvBVSxfXdA+nEBz6Ns4G3T8e9Eb3mUOkdJsW++NT2UjpHVO/V5vrvrRfVh7f3pTNLdZyLdxE84N0HEEtOgMsRzukdcBUV2vRWwYOnbTuG3HZXw4J3wki8Eb2uQ/WdojW/RLz/n+CluKaZTMhzm4eJwB9aFHADFf1X7+X6h/4MG9+LubzrHDhM934KLyhoaSPB3/yx3Nco42+811UPfBbvWvD67vSu/0eb3enEn/K17RkeNExXvPHTvyfxeuVQQ0be9zn50hs//c7Re8aBw3T/Z+TScl3niuBm9cCbLLeXGjr3mA3yl+/1+N9zAEHQ6oA/rxLLBPF49o1Fl54vxVJ08Ge/kwvkN0vvPYAAHv8K6ur8BbVEnlNF6XUArNQS/ziJv2jJ5/Cm07W/k/SeWOE9UddvUJ5+pimpYhODTtyT1Y29fsOrv2fxhXj+vR3t7+l/BQcO0z2fc0ucEyeil+7OfV7xFYXI4gvx4Hs9zl3pPbfCeyA67cfmFiaziVX/BgCUcL1XGf27z/vz8S7vNN6I3t23oN8caW0//+lcF/0PC+4VIBJgZm2aPw3/y8AD/peJ8DAtOQEuZLfAQ0sK7Q0rbv6SE/Yen/L/017ojH8LZ5/xb+Hs93ocr0f/D6s769KBP+5xAAAAAElFTkSuQmCC\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic3bx5uF1Flff/WVV77zPeIQMJIYQxYRRBpBGcQFEEbVQQUXB6xW5tWx9+Cm07IYIitiJog2P7qu3UCN22aDs0KIIyg0CYyUhCyHiTmzucce+qtd4/zrkhQIIogz6/9Tz1nHP22buG715D1VpVS/gLktnZjg3P2wGz3RC/N9hBCHtjMgOxGjDZv3UAkyZim4EHQO4i2j3UkxXUb9kkcrb+pcYgz3aDNvLjOah7BSZvRnwLX/8D2axILM0Dtx9ODgGGt/P4GNgtqD6A764i3+iJ44dC9CD/jaRXyqzXrHs2x/OsAGhrL9sB8W/B7ARKwz/H7TAE2btAZj89DbAW6X4HHRknnzgW7KdU5fsyeMKmp6X+J6BnDEAzhLWXHYz4c3GlG8l2HgL3frDsmWqzR5KDfpnOykkIL8D4OHPecIcI9oy09kxUaut//CKCfp7qtMuwwb8DnrOd5nOcXIfJCryAOgEpASUwh5HiBMwKEAW6YF1chIghthtqL97uSxG5C8a/TXvsBLx9VGa/8Yane6xPK4C2/kd7EuWblIZ+itU+BMx93E1OFmLchqQpTmaDA0kAlyDSwSwizkAFfN84RAfOQASLCVACDVgAESOGDZjmSDwEtYO20bVVaPMCionjiPoe2eXNy56uMT8tAJp9I2X10GfxyQTJ4GtADn3UDd6NYv5niKtgyQxcYjinkCSYi3gHikeSLhDwXjHzTNlWB4hEYnRAgoUSmIIqxAQsYEFQA/JRNHZw+lqiTn90R/VGYuNKQl5j/cTH5JD3FE917E8ZQFv23b0oJf9GNnQd8PFH1y7rkORKLJ2BTxJcAqQOSQyXGEiC+IB4wZxHXMABeIeZQ3q/MBQRhdiDNGqCaMSiYTFBKIiF64FYKBbAigKLD6PFsYjt+uhOcwHdiUMRe5fMe8uSpzL+pwSgrfje+/CyK1n1dcBeW/7wMoZll4Kfhy95SARXMsSDSxxkIInifIK4gHpH6kAlggjOOwTBJEFV8FIQDcQCGIh6ighOFdMELQKYYF1Bg0KgB2ZuxG4kFqvw+cmoDG7V/cXk7Z9iulx2efvX/1wM/iwA7bLLPIe1v4m4RSTJuWDJI/+675OUB3ClCmSC9yA1cKn1dF3mSFLDRJEsAYk9wJxSTEzQGW3TXlEQC6G7sde/0gzDZ0Zlt5Ty9Arp4GBfnBWCgxghOkIhkBsxGK4QYgs0gHZAQxPtNLH41q2GH4jhNFRfwrzK20ROis84gLbkohLp4C/IuAXso1v+UFmLlK4gLc/Bl4AMfEWQDHzZIAFJhKQMLgWzhMayjTz0kyYbrt2T2Nq3a5WFE3HWqgYzNxeU81ao5ADVpJ2ldLI6G6YN+o3zStI+CF+9n1kvWcYub6hR330mIgEtIHSAYFgOVkDREegYmoO2QfOHCJ3X4thqDirn0rUX0Kr9rex/Uv6MAWhLLiqRDPwPafdBxN79SC3yS1y9S5JVoQpSEnylB5wrgSvTAzCt0Vqzmfs/32By8cs2xZ2vXGJHjo/KnlVz1aEsLZsXKVQkpIlXTHo6T8wFjU5iTELULFpEQmN8Gsub87lq2gy/5pUM7ftb9jtjgNJO07DQQHPBerMeYqsnztaGmBvabhNbVYivemRwfJWitADktbL7OztPO4C25KISvnolWWMN8OZHasj+L5Luih9QfAWSmvVEtwquAi4DyWay6aYHuO/8A1phYOkd9sbVTbdgVlYqJVk5wycpWZaRJY7E+2i44L2Y0Zv8CkgMCMQ0xOCKGAndnCJG8m5ueacba7pkw/Pksp2rvrkH+3/kXqY9fx+cbiA0HbEDdIzYBmvRE+12l7y1GRffsRUj/JBubR6xdbQsOK37tAFol13mOXjzzymNrQL+/pGnS1/FZXvg64IfAKkqSTnBVRRXEaQ8g7HFK7jnU/NHde7tC5N3RpKB4WqpKuVq2cpZSpJlkvrMSqVEvE8ty9JClSJJJIr44BwWY0xjNC9iWZ4HH2OUdrsjzpm1Wi3X6RTW7XZpdNviwsToQfodP92tPpj9z1nG8Pxd0M4mYtugEygaGdpWQgO05aCxFI3/+Mhg5WsUQ3vx0Npj5GVnh6cHwCVfv4x0ZAViH9py0WcXIpUDcPVIOiBIFZIauKrgKg6z2Sw8c0m72XK3uA+MuMq06dVqzSqlTGq1uqVp6iuVsqRpGtIsM++ceO8NEQQi0AGmuKAElAEfYxTAQowUeZBu0U3zTkc7nVzzvGOtVpdGqyHa3rTpUPvSjpVyreDgc/dGZB3aVrRlFE3QlmBtoxhXtPUQlr/nEVTceRQzd5c9/+GUpwygLf7Ke0jHykjrS1suavpV0vqeuDqkdUEGlKTsSQYUqcxmcvky7v38c+6yt/xqvHTwvEp90OqVTKrVitVqNcrVasiyTBLnUhEBGAXW9ssqYD2QA1MT3bQP4g7APGBOv0w3M4kxxm5RxG67nUxOTkq7ndPO29YYb9pQ55a1z3U/Opb9P3wXA7vtgbbXow1HbBk6aYSGoA2hmFgK+ggnxoEPYENR5r/3y382gHb/xQeQFm/Gr/0ImOuD9zXS6p4kg4ofBKkJyYD1gCzty9IfXlVsumuXm7NPrC6Xh6sDA1XJyhWmDQ1ampa0XM689z4FNgP3A0uBdr8vSk/veXpceFj/9y39/6ccAq7/vQLsCewHDMUYQ7fbtWaz6VqtXJutCWt3OtJubBo7rPjMXsmMA5cz/60vJ3TuJzQEm1TiREJsRsKEElqrcEWfE0UJcz4P2Q9kwfvv/ZMBtD98I6XW+QXZkj0Q9uzdLZcgA8OkdYdMF5KakgwK1AWfPof7Lr52vJk27qufVq0PDKTltOoGBqo2OFi3crmszrkB4CHgAXpc5vpgSf/7MFAD9gVe0AcHYAlwK3Af0AQm+gDrVmV2H8i5ZtZpt9s2MdGwRqNJq9WUVqsZ9m1f1BqqxAr7v++laH43sWHEhhAaRhjzMBkJExNgJ/XhWUK+YDVx9FWy/9nbnN4k27oIQK39FUqLFmLhlX1beB+U67jMIRlIGiF1kApen8PCzy0cYZ91Dw2/dadp9ZqrVWtFrVZLa7UyWZaVRGQc+D2wiR73zARKqhqccwqIquKcmzSz14rImUC135uWmZ0nIrf0fw+oqjjnhJ54d4AGPU6dJSL7VqvVoSRJOlmWSKmU+KxSssVjH6zOa/5g8453nn8bzz3tEHzpDrTrECeQKnjBJSmhdQ+izwEWkNx/Oez9ReB924Jpmxxo91+wl0rjjc4vO7d3wQKu8kP84CyyIUHqgh82/ICQVA7k3m9evybstXb98FtnD9Xrrj5Qp5RlWq/Xnfd+AFgILANSVfVAHZjVf4E1EZlhZs8VEWdm3xCRD/QBfqSjIhtU9SIR+XszUxG5T0Q2quokPV25EZjsv4wA7AI838wajUaDZrujk+NjyWSzyZzJ/3pwTnrffPb5u79B2wsJEylxUonjkTAhxPENaOcURBIADfM/6bT+H7L/6Uv/KIBmiN1z/tWS3TqESM81JMlXSQb3QIYgGwQ/CH5QoDaflT+7cXNjYGLl4Lt3qA/W3WC9rtVqxdfr9QxIVPVeYKNzzoCOqjpVLSdJUg0huCRJusC4qp5FT7wPAlYCP1XVkf5zs4DXAbsCtwN7OOfO7nNiRk+EWzHGwntvzrlSn0N3APYzszjZbE50Wu1sfGKcZqNtu05+ffO0aitlj+NeSphcTGyATkCcgDhmhInlWJziutst/E1D9v2nIx/rmH08gPd+7ijSpc/BRnpW1+RWXGUd6WCGH+5xXjJo+MFhRpcu6q6+a2jJzHO65UpdBgeqbmBgwMrlciYiqqqLnXOFqk7vc4WPMRpQTZJkB1U9FEhF5Fwzu9DMbnfO/VBEXqyqrwGmHKW5c+4XZnatqr5dRA7y3p+uqh8DVFVvE5H1QAtwIhLo6dSNzrkKsEeMMQ0h5GNjY9rpFH5iYizutemTrjTvuS0G91iANcYJE544HoljQpyMxOZcsAN7SM3+AHHPhbLvP/9ua7zc47jP7OPEhz+KdcA64MIdSJKBBxFFpPcGigmx1Tc/f9G0syZrtZofHKi5en3AyuVy0gfveufc5qIo6mZWAWaZ2Rzvfdl7/3AI4SozGzCztqqeG0L4ELCPql4QYzzezFRVH1DVB8xMY4zHq+qFwHwzOyOEcF6Msamqg865K2KMK0XE05vaTDOzUgihqqojqnqD9z5kWZYMDAwmpVJGvT4gS6af1baHbzyY0FQwD67nzBVngMNz4xYcbOXHLMRzzEy2CyB3nfdicQ/8FOvMRrsQu78A2xWJYCaY6zGwxf1Y+quwbNrp19QHB6u1Wo16vUalUk5EZGdVXaiqQ3meO+/9BhFZrqqr6OnAE83sH/ttV4HvAB3n3Plmttg5d6Zz7sNm9i0zu69fvqWqH3bOnWlmy83sAhFpiMh/0JtgO+/9e1X1TTHGATNbb2YPOefGVbUEDKvqQmBOqZS5wcE6lXpdqgODlWXTP/Iblv48RePeOOt5wrUvpZrvDt3/7WMxS9zi/+aezx2+NWSPssImeqbY4j23XJDSeszvBAJODFHBWcrIA1c1sr1zN7DHjqVKhUqlQqlUcsA8M3tQVV8MNEVkjqoGEVkLPKCqVwEvA3Iz+yDwG+BMEfmtmV0hIifHGM81e3T8Z+p3jBHgTu/9h4qiOM4591ERuTKEcIb0JCMTkSuAGWa2v3Nujpk5MxsFEhFZ7ZybWyqVHq6pOrQaJuPOcxvNve6sjy2+l+Gdd8EkIAJ9LFHWYFMLokXvN9vzQWCLE2ILgPbA53bS7qrfi3WP7l+6GvxcJPRG4Kwn5LE1l/FVu63e4fM31CsV6pWKZVlm3vshVb0S2BBjTAG890PAAar6ahE5EjjPzOohhLO89xeaWQ6caWZnAS/vA3Wlqv5eRFqPAbEmIkeIyCtCCAeKSEdEPqaqfw/MVdXTReRCEXHAe4B6COEqEbk7y7LNIQRCCEWaprO894eWsmyTxuhDCG7NzH8s77X+jBcyML2AsBIxQ0xwKgTdCcl/h9kRwAJh/fds4blz5aAzVz+aA7vtE527fi7WXz87/wCwO+ZAVIgFuODZuOqmkfrrJirV+k5ZkkiWZWRZtqOZbYgxHglUnXOJiKwMIdyRZdnVIYTXmdl6EXm7qq52zn1BRD6gqv8CnAtc65z7sarua2avF5GjeQyJCH3R/IaZLXLOvSHGeB498f+AmV2oqqu89/8nxjgmIrPSNL0qhDAjhPBiM9sdiCGEpvd+JE3TOTHGDZVyOSpUNtVe/fMZI9cNMn32LliMmBnmIs4ghnshHtHrybU7Iq9/A3DRFgDNEL1l/U6uUhzRN8wbUD+vF8PAepecoHEandburbkvv7mSpZTLFSuXywnQCSFc1xezIefcc83sOOCQoig+18fgy2Z2gZl92cyON7MvAb9wzl2vqqfHGKfW2rmqLnTOPaiqK1XVJUkyD9iD3grlPX0wNwIfU9WXmNmXzGyVmf1cRN7rnDtdVS+MMTpVfTcwA/gVsFBExsyscM4dVy6XnRmxiGqTw6/cYcbqKw9nmo6CbEREURFIDPW7IGETyAwIL9PO+oZZ7516gLOP/budTZfuIXp3FT89Q9yvcaUhpNTzKLuy4TJlcmLZWPqi+2P9wIFqtUq5XDLv/d5mdqeqntLXQaNm9gBwrIisMbN5fZ10sHPuJ8C7gbu9919X1XeZ2dH0ph8Xq+p/A8tFZJr1RObFIvICM9vVzB4Efq6ql5rZXSLyfOBvRWRXVf2EiMxxzh3vnPt2jPEdgJjZcF+kZznnvuGcK6vqwar6ehG5XUSeZ6YbBcOMxIrx20v5is2UXA0NPYesRgfqIf4Bs5l0H/yDWHUZq45eec63/jDpALTg1c7dNouox9Nafh1KgKRnrhWPakKINZrtlzSnv7ZWq5UlTb1kWVYG1hZFsczMFpnZ61X1VBFJrfeKvm1mL+nruLkhhKPN7MNmdngI4Twzu8Y59wHgWjP7sIhcaGbvV9WXqGpZVVv9UlXVl6rqaX0996GpZ/v68jwze4GZfTiE8BpgjpmdZWYvCSF818wwszSE8E4zO9HMVqnqMhFZm2VZ4n0qWVay1ow31Gg0DydaBTMH3qHiEGeoy2kuv4YY3wy3zUL1WOjLq133ritxP3w+2HSgQTrwc8p7DeBqDl/ueVxi1o7Nimza5VNFqVqlkmWSpulBqvrrT3/603bTTTed6pwrPVZ3/TWRqnZf/epX3/ye97znZzHGwVKpNEtEXhVCuL3b7dLpdGzmio9XZKBb4PIKoQXSNEKrS3dJh2LytfRiFqMWTvmDe+m3X5WYne30+kUbnFkvCC38L0U+HdZ2KO9uSMxw0Wh3xyanvXHCe79TImLee8ys1O125998881HOec46aSTfrd06dIlCxYsWHDvvfcu2n///fdutVqNPM8LYNqvf/3rI733d5xwwgnjU4MaGRnZuHHjxk2zZ8/eYfr06dMvvfTS/VV12pve9KbrZWrSvhWJCHfeeWdz8eLFr5kxY8YtL3/5y1t9cOyBBx5YMn/+/N2yLMtGR0c3XXHFFUclSTI8MDBw1THHHON/+ctf2hVXXPGyer1+/ymnnHJkURSrsiwrpWlqeZ6Lc07Gh49bOty4ZJB6UcXFnh+gvcbQfAbGlcDrwaabdcfMznYJt66Yha2+A3hLr4tuBGQIbWe0V7Yp7xlJY5mQPjfUD16YJok453DOzVTVmycnJ1/kvXdFUXROOumkF8cYJ0XkkBNPPPEgM/secBpwu4hc8etf//rI4447bvyEE07YDNwAvA04cqt535L/+Z//WTk5OZmedNJJP6PnsgJIY4xVYHfgqGOPPbZ26qmn8sY3vjE/4ogjqsB/xhhf6r1/N4D3/lxVPfG6667b0O12hy+++OJ9yuXywAknnHD9qaeeuujHP/7x604++eQlIvJKVf2ZiMzy3m/IshJh2iE1Ri/dn8I2o/lGOg8NQLeKCuDWYr04l0tX38rNuoOjU+zpdHHSW2EAKiWE0PO2FTW6K0qE8fWmyXBWqdfTNLM0TUVV91fVDVtzhqp+xjl3HHC+mVXN7FQz+5CZHaSqHwVYt27dGjN7jZmdr6r7hBC+r6qnq+q5ZrYsy7LEOSchhAtCCF/ql/PN7BwzO8HM/tBut7/Sr2uVqh5iZuc7514FnA98N8Z4ppnNz/M8AmRZNg04X0SOPeWUU64WkTkrVqy4sq8bVwP7eO/FewdpdQjxMwgTG2g9tAMxr6BqGBGTdAtO4QFH7ua7qLoXrjHvEQBtGmopph6LghZCc023M/yCr5mZgk2tCBpm1th61aCqJ5vZ9WZ2DvAZoGlm58cY/11V/y/ATjvtNBe4S1VPB74FvBG4EDhTVY9xzomqmpn9j6r+SFV/FGP8jZnd1Tcmx1er1dMAhoaGZqvqN/ov4D5V/ZCqvkNVvxljvDTLMm9mOOcuNLNPm9mNr3zlK1+oqvHKK698oZmpmY2LyIRzzkTEvEinW33ORbTXdqEbUQUjwUiINn0LTtaYi9meiWg8ACkO6GPQQaRCtBwfCyIlEEOHO6jf1bmkSJIkAENmtjaE8KrR0dHvAh/pc+FewMMxxt+IyDkicq2q/peIvK//tlm+fPnyGOMxwIV9Sz1mZpc75xYCRbPZfDO9KcjuZlYDMLMNZrZSRH6rqqsnJiYAzldVAd7br2d1COHMNE33VtW/FxFijIv6n2c6537rnFNVPRxYd9ddd80xs/NDCMc659aratk5twFI1dUXEJM2lnchRlwUMI+TKpCjZEjYT0PsJBJ1fxwHA2ByD6KG8wENAWcZBYKUM4b2ayeJK8eolqZJbma5qh5YrVbX98FLb7jhhi8fcsghpyZJsjbG+Enn3Dn9acyXVRURef+ee+65B70A0b/EGDc5514A/G0I4c0A1Wr1rna7japuvadwd9VHtkEPDAxM9EV4tYicEULY0Xv/9yJyboyRGOMX77vvPpfn+SnOObrd7qdKpdJbzGxP4JMzZsw4ft26dTNijEc65w4ErnbOdfO8GEqSxJLpB25ktFzDaUSDoNERDEQdjvsxDgQ7VFRDglgZo95TZPogJlUsRpCIRkEAqQ672ryuiQQRSVV1DzObEJH/k2XZJ/uK21988cXv74vyHqr6MTMbT9O0VSqVjqHn9OTyyy/fddWqVfckSXJ4lmU2PDxMjPFmVV3TbDZHNm3a9Gag+pOf/OSHSZL4iYmJep7n3YmJCdm8eXPsdDph06ZNOwMvufLKK1/5+9///tA8z2tFUQB0RCTz3n8QwLmesynP83enaTrovf+AmV04f/7862666aY9RWSWiLwjxniqiOyRJH5pURQastkxteowRW6g1tsVZgZiRHsIOBBjEI2VBNXe3kUAtSaQoma4QsEJFoQ0rbq0hjmX9yTKhoHRWq32yRUrVnyn2Wx+qlqt0g9R4pyT/pywBAx1u48E+WOMu91yyy27sR1Kkt7y/PLLL3/L9u6ZIhGZ3e12Z2/93LZo2bJl//2c5zxnLvAl4L4sy+7tdDoHxxg/MzAw8KlOp7PUzIaT3to+NwYMyUpYXiBqRAOHEdWDTiJTLkHFoYVsUYxCCwgQlRiFEAQtDEmHVBLnvS9UNdCLV7SA3efOnfuqkZER1qxZQ6PR2OJ6+muiJEkOFZHXmdmFIvK5I488UkZGRkoDAwOvA3YVkWY/LlOYmSVpDciGevtrQi/Or7FvPGg/YnCDc72NnvRKdEqkidDEaKK2DmMN4hLvk2az2eyISINe7GIDgPd+N4CiKNi4cSMPP/wwo6Oj9EXqr4IWLVp0D/B3RVG8Kc/z7+y9995Lgc7atWt9/5YNwKoYY8PMmnnezXGuRKBFpAk0UBqYTqDoFrxMSTDskTCJ1VCGCQgmZg4vZoZnMoa8UqkMls0siTHuY2YPbauzMUYmJiaYmJggyzJqtRq1Wu0JReyZoKIoaLVaNJtNfvCDH5RGRkZeOmfOnN/ssMMOyxcsWPBGEVl37bXXPnDyySc/L8Y4R0T2EZGFIhLE2ySiE5jMwBCi9QL+Ig7byotvWALxkXi/yRCQRMU5jFhYCXGaxDgeuk2DctL39TXpBcCfkPI8J89zNm/eTJqmU55rSqXS0w5oCIH+epZOp/MoCdiwYcPJ3//+9x/3zNe//vWFJ598MmY2ADRU1RVF4V0+TmahEYxpBBVBAog6ByJWe4ThIglqG3CyCmwepkNmKIZEwUV1DlTF8gZx3GBGGmN0fZ3x+B34j9Bm59xD9BjdqequRVEMbz2oJElI05QkSciyjCRJcM6RJAkissWCAqgqU/NIVSWEQFEU9L3M5Hk+NbnfFq1yzo1OratjjPvTC8azadOmV/bvmWNmDeecxBgTjZMSi9CIQYcR5zykeFUiINQQAZEHzeLaxDTeLUYKzEPcAWLcY6ZmipppjMFI8tEGzeXW9TtnWZYCrFfVA+jtBngUOefuEhEdHh5ePnPmTN2wYYMbHx8HWNV/BmDL4J9BUu/9LWaWzJ49e/nAwIB7+OGHa51OR60XtdsdyNevX39zrVY72Dl3dwghiTGaH1+UabGpFQszEg3OgSgizjnE9u8bkdscdqczC/ejyeL+Mm6WYQ3MopmoBefMJOk21laTxuKKmbrY83aPmlkSQrgeoF6v/wxARO4WkearXvWqHdI0PcE5d2KpVDrhqKOOmiEiLefcfc8kYluT9/7mUqk0/qIXvWh/7/1JaZqeOG3atGOPPfbYrohMAmumTZt23ZIlSxaaWaqqm83MVFVKnQdrne66ajRJXHSiEcQkGjaO0lvOkSxB7QHnE1lCHFyLWc+3H2l5xKtapqYuRpNue6ySFSsXhKCxv05tAHS73dWPEZ3k2GOPnT4yMjL3C1/4AqVSic9+9rOMjIzsfNRRRw3S24X1bJysXGlmpec973kvnT59euUTn/gE8+bN4/TTT+fee+89/MADD1zhnFvfaDRmttvtrK8eGj3VECzLVy0IjfGKRiOqOtRSU0vFaE3hRJi2nsKWJ2xuL9fa7Nc7t7HXtLNGjOwgaoWpYEoSY3emaPeAPG//CkoVEcmdc4QQ5l166aVXNxqNI5MkuQ6we++99yWHHXYYt912G7vvvjs33XQTCxYs4NZbb91XRK53zt1mZjOfcPhPkURk0+zZsx9cuXLlwcPDw6xcuZKhoSHuuusuDj30UFqt1jELFy5cVhTFXO990teteVEUxJi3he4+MXYjgDnUjAg4orWm9nJo2GGmm2bLEnnrzRP6k8MPe8QS41EwkwTFFIsaRIr25qt8vmY8uHlV770lSbIaOOKFL3zh/y5evPhKgOc+97nV733vexx++OEsXbqUiYkJ5s2bx2GHHcbvfvc7jj322MV33nnnI6HUp2nSLfL4PVJHH3307BtuuIHjjz+eK664gvHxcV7xilcwOTnJ9ddfnx1wwAE3NZtNt+uuu74GeKgoCosxGq1VY3lr9CoN7CjO1KI4ExEUxZNN4SSUXiwvu+YT/bCbbRLldoSDMTnSO1seogU1cTEXb2YyvvaOscHB31dG0zdrjFG893cDx+yyyy4bP/OZz5yqqqgqt9xyCz/72c94xzveQQiBgw46iO9973ssWLCA973vfe+cum/Kom5tXacAfeznFEBbfzrnEJFHfe87erdY8F/96lf86Ec/4rTTTuOaa66h0WhwzTXXcPDBB/O2t73tnd1u99uqOlNVfwVYu92VOe3rapMjCyei2lwfxXDOnMTgRQSTl4OB8QczVkJvcyPnvGnuJDI+ihWvAKaJ2kIzqmoiqhBUpNMZr8/ace8j1snzljrnvHMud87tF2Ospmk6bGZOVVmwYAGXXHIJeZ6zdOlS1q9fzx133MEZZ5xBrVZ7FHjbA3Lr348t2+Pex3KhiDBnzhyuuOIKGo0Gq1atYs2aNaxfv54PV+O33AAAECZJREFUfehDJEkSut1uDZihqre3Wm2X5+3unPy3x2548Kpu6sV7QROPpIL3XkYxDu8teev/Kjrjl+dctnpF71W1uFnjzvVHnIV+rSBCRDDBDDWl3GmM/LrUWTbS6XQkxqiqehuwT7fb/Y+pAe68885cfPHFpGmKc45SqcQFF1zAnDlzHgfSE5W+W2pLeTLPPBbsww47jLPOOot2u02e5+y1115cdNFFZFlGv8/7qOqt3W5Xut3cyvmDGzutkSs0UlLFMHEoiIlhbJjCR8PcIWLj1p4o90kvPegaSe/bC2wOcLsZY50CaRdYNzfJI6gbWL3LIe+euyh9+4ZSqSKDg3UnIieq6n3lcnm3vsf4cQP+YwA8lgufSISfSGwfW7z32/s93ul0NojIfFX9z0ajRbPZtP35wZyVt/zbKomTcyspVkqFSoaWUkSEGcCBmKy2uN9id9LCl8NWu7NU9UKsdHEf5YMFNgjgpLcBFpCiPbkTYbKgvXp9nnes3W6rmd0I7Nduty/dlsg9FpjtgbctDvxjL2B79W2v7anS6XQuN7MFwPV5nlur1RLXfXi9FRPtojM5B3D9bXwighNYh3Fgb65culgtfmEKty0A+qHWFZrvNG/LPEddI3WGiLkE8JiKt/TB2340viD9/X7tdtva7bZ18/xhM5swszfGGK/bnu7a3oC2J7pP9P2JxPaJ9KeZEUL4dYzxFBEZy/N89cTEpO902rpP6boDlt98adNjPsE0wSQRIxEDle4ULhp3nu/Xt696HIDy6qVd1Asql/ZMdXy7F5ssiUURvDcRr1i3NT5DWxsmKvnitZONhms1m1oUxdVmVu92u0NmNj4lftsb2JPVgU9WBTwWzG2B2O/ThjzPZ5tZmuf5NZONhmu22jpkS9fE9sho0R4fRhDvRRMxSxLDY2OIvq0nmXIJKm05bWn3cQACOHFna5hzX1+MM8ytS5yId+AEBMErsui671b3Gbjn2G6n02w0GrRarY6ZXQUc0Ol0rrZetOsJOWFbYG5Ld/4xffpk2zGzTp7nd5rZc4HfdDqdvNloWrc12dqrfs+rF1373YokvU2ECeC9+FTEsGQjPbcfxLlLXJJ/+lGYPcr0n3LPerS8D/DbHhfaWxOxsVKCeYdkHhMvRAvDS2/58Y0H1X9fGZ9o0Gw26Xa7m+htAH99nuc/2NoIbP35ZET6sRZ4ayCfTB3barvb7f48xvhK4A+NRmt0YmLSJpsNe/70m+tLbrrsRrUwLe0fB08F6Z2rtzGI7+jpPq6MoTRfTlo6sl0AATq+/U+az76h/1AVlZg6o5xg3uNSj6WefHz9kj1orrCdSsvWTUxMyOjoZtrt9lLgTjN7ewjhB8CfxYl/zpTlCUpeFMUlZnYicHur1Vo+OTnpGo0GOyVLVkvzIR1bt3yPzBNTJ5Y5LHGQelPE5T1JBHTObb7onvFYvB4HYO3kVWuIpRSTb/aXLSd6WJQ6c5nHSg4p9xqS+67+9tCe9QcPk+66NZOTEzI6Okqz1VpsZjer6luLovjZ1jrxyXLlE80Dnwy3TX0C62KMv1PVk4GbGo32srGxMRkd22x0143sOfTQYXf+5t/qpQQyJ5qmWOKQLDHxxiLM3tTXfV/TIknlnSselxXpcQACuEp+Tsx3HERp9Pz/7kWZp1tySEkg8bjUiROscsvln1v7kl2Wvrzb2LhhbGxSxsfGtNlsPqSqV5jZ60IIK1X1uq3r/1PB/BNBm6Lr8zzfqKqvUNX/nZxsPjw2ttmNj08Smps2vnju4iNv+cnn1qVi1VRESg5KhpQTXObIUXdEP/bRiPnsHZzaJ7aJ1bYuykkPt73xbbR+Zu8N2P6ibqycQrmMlZ1ZKYVyIi6VOHzzjz9/99ELlh8dWhvXjI6OsmnTJhsbG5sIIfwXsIOqHhRj/Da9I1mPom0BsD0An+iZrWg8hPDdEMLzgel5nv98fHx8cnRs1MbHN2unuWHkmL0ffMXNl3/+LtEwVErFlRJcmpiVykgpRcXcJrB9eqJbP9OL/5q8c8U2T7E/4WnN8J2df+iTtQKc3If7KyGyf7ONNbqUOoXQbFtoBaJKZc3hJ33igF89sOPVOcNzhoYGQpqWy/V6RUul8qCZHqGqS4CFMcZTVHvO2a0t67asLvC41cTUiuIxn+qc+w/ghc65nUTkd3mej7dardhqtbLJZlMzHXv41c/Z8MqbL/nMIrHG9EqCr1TEVVOjVibUM0g89wFTx14viWHHkLxz9du3h5Hf3h8A57x++i9jrLzV+ZZgzACe6+B3HqYhaIw4J+JMkbwIQyvuvHryZS9//i6tXB9YuSFMC0Xs5W2KoeVElgCY2dFmttjMfk7v8M1g//qWds1sm56XKRAfc20N8J/0gvgvBO4piuKOZrMZx8YmmZycYPPmUXYb3LT+pXtu+ptrf/iJEbHujEpKUstEaplRKxNrKaTCCoR3AB6RJTGfPekle/s5Px3bbuzhjx64bn9tx92yrPigS8b+AcgQNqP8ohuZ28zxrQ7SynHNDtIulE5wxaEn/PP6WJnDT24faJfSSlKtlsvV6oCVKxmlXgCpDOypqrur6m/NbF2Mcb6qvlh7Z+keJbZbr3+dc8E5d4OILPXe7ygiR3rvV5jZgzHGVp7ntNtt2p3cmq2m73ZbjeOfN1nz3TXx1h+fv2PJaVYtOStnSK2C1lJirUxeSlmPcQwwo7e9b+gifPlf5e1rthm+fdIAAoRvzXytSHcv51rn959aCdzcDcxsdPCtHNfuIO0ca+UaWwEGZu+16pDj3vc3Ny1Nfr1oXW1WuVz2pSyxUqki5XK2dSQuU9VhM9vFzKap6gozW2NmDVUt+qcvMxGpi8iOwK5JkkyIyEOqOm5mRVC1kOd0Ojndbod2u0On0427zypWHLlv53X3XfWD69ctv3V+NcPXMmeVBKplQqWEq5eQUsZalBfSOw2PhuqHTct3J+8e+dUfw+bJZ+345vR3kbTrkE8dR1iF8LsisGujQ2xFrNUmtrvUmwXdboHP1RUvPOmfNyYDO02//BbuGu9k8yqlMlk5w7uEUlYy55A0TcQliTkRw0xxHouxd2Cot3PT+kcbxEzEQIIG0SISo1qet6UoAt08aLvb1uGyrj/hMD0gTK7ZeP2PPj+zlGhazoiVBK1lZJUSRb2CVlMk9ayldzJ+DwBi9gG01pV3b3xS2Yz+pLwx8RvDn3RZdwzrgyhsxvhJMPZq52izLVmzwFq5SbdLaEeNndyZlOutw97wwU5anz39ZzeHO9dPMCvxaZp4j08z0iQlSz1Kz/XhxHdxRFR7ESvnPIqPUTPV6LwX63aDKLEXEy6ChVDkc4Z15G8Pyw4qJtZvvOm/v1ixTqNaTpRSySXVFF/NROslpJwY1RKWeBZjnIAw1BtP9gGKclXevfmzTxaTPzlzUfzawBnOhwSXn9dPDpZj9i1DDmp2oR0Iza5qJzhrFfhuR5NOQdENTpPKwNiBx5w6MXOXfY9YtiZcef097e7IhE2XLMlSvDjn8IngncfMgllvQ7JzIoqkGsRUC4kWpCiCodadPuw2vXi/cmXBTslRIyvvvfauX/37YNGdnFbNVDJHUi67WMmIlVSpJs5XMqRWwouzuzB5J5BhqMbkDGflivzD+JMG788CECB8tfZO8cVOzsd/4pF8pz9CGOjm1DoB1+yStgq1dk6RF851g/puTtE10iLSGNpx95EDjjrFTZu928taOXcvebizfMWabvfhTV3f6UI3qqC9o6WC0ywVyiXYeUYp7rZTWpq/c3WPWsYBm9c9ePU9v7lEx9Y/uEOaUMs8oZSQVTJXZF6tZ22dK2eESkYsJf2NU0I/LwKjhPSLEb8i+YfmD/5ULP7s7G321cphEXuv98XeiL2gf3kxwu8N269VENtdF9sFaadQ6/aSDaXdSMgLfKFoEUhiJCcpjw7OnDu+497P1xk77pqV6tNr5drAoE/THUSchLy7odOcnOg2Rpub1q3M1y26zU1sXD1E6ExPElLv0MzjspRQ8iSlhKKSkpRTJ6WUolZSygmZiNyH8VKmMs2Z3BhDtlyRf83e17r1z8HhKaW/m/jywIy65N+XJP4B9JGljsiPMTSiO3cCRTt32im01A3keeF8oap57nxhWsRA0lEwJQQlMcNCz502ldqk109BEwERJHEEcSQlB6knJN6lmdOYlpzLnMZKQpZlrltJ1ZVLpB63CiPF7PgtfTT3KYv+b0TKb5F/HN/852Lw1BMwXobX9dmZmJrL9K3Agv5fOSr/jmdGVJ2bF3Q76nxRqHZy52LUmCsuj06jqu8qgjmiEtTUmEqF0vumgDlx4h2Jd2qJgHcuJk592ROT1PlSopomzpW9xiwl896tQumAvZlH0gcs1sJd4oTAxnCenP3Udko8bTlU7SvMN/NfFW9XAJ9iKmVJL/vkDzEM0V0Ldb5bqObRuagaikBWmLMQCKAuGAFDowKO3gpASbwDBJcICeI08SSJU8kceZKSpGCl1EnqNWJuBYLD7C1bsmBCC+MTMcqrfIjvlQ+y/OkY99ObhNaQ+K8ch+OdPnG3KXwco7xVY/eqyY3iqRk6xyJZUFeEqC4oFOYExaKh9JzaPSMiGOLECw6HpKKWeMyLI/XqxVMIbq1Fmk7scIP9txphxymfiUGfD3zL/3/84ulMifzMpEH+Bmls8yaMU8y535rnNOnP8rdqeA3I1eKsbSolB4NRdFgMb0Y0wcR64mXSSzggggeCw40rTIizrqlUwF5msNOj+gArPVykhR6t8P27q1x2yHt42vcdP6OZzO1sXBjmKODDqvxeEjfN4ANP0JlRRG43GBdljN5OWDCrmWNYYAizgw2mP0EdXzJ0s1NeivDZZDNXP1U990T0rOXSty8wsyuchXMrTLjgmWhDjDNQ3a1kfEr+iY3PRBuPa/PZaGSKDKR9AZc47x6KulUuwqelbrnIqe5c+SdOFJ4+HffHaJse6WeKBKwyyVtDoQca/Eb7qbSfarHItVbovpUB3vxsggfPMoAAcjahW+KNVlhDlZVPGcDIWjFbF5U3yTNgJP7oeJ7tBqdo9F84wMOpivwjsmWS+6dSMLUvpsJ3Bz7CdpMkPpP0FwMQYPyznByUHXFy4Z9VgdrpzjE+7aN8+2nu2pOmvyiAAJvO44tmboNh5/1pT8qnPTpj+se3nRjx2aK/OIBmyKbP8FMzN6bY257MM+LkMjGtzQy89pmc4z0ZetaNyGNJBGtv4k2gczVy+5MwHHcRdVoROOkvDR78FQAIMO+LtIm8zYndr8bodqcrRkuwG73jXTudTeuP1/zM019chLemtWdzpCkvir2EZI8jBx9xCTfNOYvfbev/vwT9VXDgFM05m2sMgiinP477lNMd6F8TePBXxoHQW+6tOYsfFsrDpnyof/GiJGHmzp/mrc/2SuOP0bN7CvpJkICZ4+2rjF8G5RcmDDrHvjt7Xv3XBh78lYnwFMnZhOg5SRxF6hmhyUlyNs/o2dj/X9LKT7D/yo+y31+6H09E/w/wHJVcjfUH5AAAAABJRU5ErkJggg==\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHiczZ15vF1Fle9/a1Xtvc98780cEjJCAgkgiUwiIghCgqLIQyK2qI0D2g6PlmfbbaONCmo/habB4dF+tBX0KdC2PFGZB4GEgECYIQmZp5vc+Yx7qFrr/XHODSEkiDKuz6c+Z9+z9zlV9T2ratWwal3C6yh64YW8Y8Ex4431M4yhOQAtVMUBTOgRQRGEWvtBlJnRgGJABSthdIWHrIyI1l9y3339F154obxedaDXOsPGr2+anFL2TgXOJKZWEIYP5oslL0z7EtE8Zj4M0O49f5qGofKAF32GRTe1GnWTZOkR5DUi0muC0Nxaete7el/L+rwmALdde+34nOcPgei0ILK/j4rlLmbztyBMfkUyUGwT8f+ZNGojWeLeBchvjOR+Xvngqf2vyPe/iLxqABWg/p/+YqFR+WYQREsLXeUuMH8WQPhq5dmRVMRfUR8eqquTt7Din7rOOXsFAfpqZPaqABy88spjlPhfi8XytSbKfZxAB+3l0ZSZ7hXD6wkWCAggClURGWJSggUAUjivokRIoJpq6gkkyl5miOJYqNq9VO6xer3+UxfHp4P9l8Z+8pPLXum6vqIAhy+/fLYXXFksdd1go9wXQZjyggyJH1HDDyEIQ1iawMZAjAWTsWS55QHHbEREGQwPABAYZhLxzjDBIpOcQBx7BwhUvOtDkiXk/WGqcugeirbJxc1LGvXaqY5x7sTPf37NK1XnVwTgg1deGcwcHvkWOKpXuiqLiPjI3XIZJBv91lub59CORWBVAysmDKyQ8QgsQGSNsakaUhAYIAPqlE+hgHpRVeMB730IFcfOK8RbSVPHTghZCsn8IJI0hk/fA8WYXYuhovfVa8O3eudy69eWLzjsP87NXm7dXzbA6oXfnJNBflwaM+4uJrpgt6/fTlFwC+dyY7w1Fvk8KAwYHChHRsQGAVvrEBgSsGE2DtYATAwFE4MBQAUCgkBU4D3EewtRD3WK1FmkzsE7gstI00zQaoEynyFNNkucLCZg+q6lUpVLRoYGj1KWv53wla+sfjn1f1kA+750wblBYGcXunpOJcUBu3zrsIbBryhfnGaiyEghIoSRahiAcgEjyCkCqxwEFsY4BCHBEAnIIzAEsGEGRDVgYgXUiQAMcfAeEDXwqshSFe8t0syxcyRZTIgz0TSDacYkaapoNb2kySaK07MAVEaLqIRnGkNDv3ferR7/rxdd+ZoC1Asv5L6R+k9yudJTuXz+YgA7O3EOwquQjyoo5POI8oRCBC7m1YcRKIgIUUgcRSqhVUSRARtvrGEPkiRuVBsDw83B3m3Ot2KqDQ8TAJR7utXkcjpm4qSgOK4nn88XK1BlOC8iziBOPWeOkCTkk0SROqU0Jm00QEkKtFqqzVZLG406vHxol6q4pNn6XBw3jh23Zf3ZdN11/lUHuPpzn4t6Mr2h3DX2T8T85eeoYjuKuT9QobwPFfPQYp4oX4Tmc0AxD+RyanIBxIZk8hE8sx3cuLnv4ZtubD774MOz01bzQI4KjwTdXRt57NhhDYO00FXOAKA5UgsozUIZGOjOhkemSdI8NMwXnt7vsIVrDl20uDhu2tRxRtT5OCZOUvFJqhTHhFYMbbSIkpai3oSvN0Ct1lY0mqeAMHFn0UUuqo0MHDkU0Kn7X3FF8qoBXL34c1HXxNYNlZ5xawj0qZ03DN3I5UqMYqFgSiX4UhEo5IkKBUUhDxTy4FweEobF4R19g7f+6D8a29etPz6cPPGWniOOHM5Nn1qKyuUKk+UgCMSwigoLMwQARNr9ocucEUC9TzWu1WqNDRvrw8sf6HHbd7xz0uxZd5z0iY+VK+Mm9HCa1aXZJG01iZJYfbVJHDfV1BvwtQbQbDSlVsvB6+JdQHxvZLDvwDq5d8/86U/jVxygfu5zUV//4I1dXeN7QXTWzvdN+GNTKU5DpSJUKYKLRaBShi8UCaWCUr4Ijuy4NY8/9syNP/rPg6mYf3bSu9+zpTJj3/FhaG0YRWRsiCC0yFkLgAWgzFgSMkYBQL0n74iZJRDxnDiHLM3Ue4eklVCaZVlt3fr+3j/8YYrWG7NOOfeTT86cP+8AybIdaLRI63Uy9ZZKowodqZOv10G1WiLVxiD77CO7VPPqkaG+aSMjAyfvf+ONL0kTXxLAa9//fvN2p7+rlMdsYDbn7oQXhj+kSnkmd5cJlS5QpSwoly2KBUGpRFTIj92+YcP663/4g/2CSZMfnnrG6T6sdHUXopByuYKGoaEwDBGEIaIwJGOMhGHovKfEGPVBEGUAkGVJoEqGCGGaJoH3npPUiXcpxXGKJEmRpC3EcapxbWR463X/bZLebQvf95nPrpm4777TpNUYQD1WrtWdr9dCDI8IqjVItcpUra3WNPvMzjqpfH9kZOCACQGd/FL6xD8LUAHaccq7flEuj9/ARP+480YuvIzK3fO4uyzo6iLT1QXpKkMrZZhSyWTQiddd/r1n62lMsz/+sb6ouzKmmC+gWCxImMtTFAQmn89REEQuzIXKABtjhIgUAAPIOgkAAnQMlarCe8+i6tMkYeecbSapxs2WZGmszWaCRquOWt/g0Iaf/WxiJcxl7//8Z+ayoldrdaFaXTEyDD9cI1NtiBseUNtobHDN+FPP1Vkuqg4Pzph40w1nv2yAW4874dxKqZyzJnfZ6Hucz31fyuX9TM9YoKdC6KkIlbuYuyuiheLEbZu3rL3h5z87aNJp77lp7PyDpuZLZZTyEQqFvBaLRQRRzufzeVimoANsAMB2AFsAbAbQByDZDWAEYAKAKQD2ATAZwBhVhXMuSZ3nZrNhm/U6teIUraSl1ZGGDj3xWO/263+76D0f/chjEydPmaWN+nYdqTKGa0B1WGRomDA8QjpYfRZZ/HejdXRZfF6tWfeT77rte381wIHD3zpfQ/s3xfKYL0GVAQA2+iF3lWfpuDFK3V2Erh6Ysd2qlRJToTD3jzffcseza9dMm/s/P7clX6wUKpU8R/mCVkpFLRSKEgTWWmstgEEATwNYC6Cxh3IJgKM6fy9HWyu1c4861wUAswDMA9DtvXdpmvp6vW6arVQazRparRZqQ0Mjq//9+/vvt/9+a4898Z3v0Li1CsNV9cNVz4ODVoeqIsNDHsNDG5G5tiaqukZ96JLM61WT77/nqb8Y4JPz54djw8LN3eVxUxS6PwCA7a+op9JF3V3MEyYQuiqCMV2M7m4gCg/6f7/573uauUJj9t+eXSiXyzaXK3JXuaiFQh6FQsExcxnAJgDPoK1xhHbTpA6g7g6UgwAcDmB2pzhrANwP4KkO7CoA34HoOq9jOp/b13ufJEmi9XpDq9Um4rhOtVrVr/np1Y1CKy68532nH0NZ+gQGhtQPjxCGhlX7Bw2GB70OD4/A65JOvqtGagPbJrK8kx56aI/TPrM3gF8ZO/HfugpdG9W5U+EcyPmnqFioarEYULkCKhWEymVGpUxgc9A11123ws7cb+P+HzprXHdPt+0qV1y5VOCuroqGYRgZYzIADwJ4Fu2mWQHQIyIREUUAciJCRJSo6qeI6NsA7gZwO4ClqvppIrqpU7xIRPJElAfQg/YSWQJgG4AhZh5rrc1HUZgGATMRGRuGKM87yA5v2Tz02N33NOfPPeBQUfSyS1nTDEidauqIUie+Uffk3AQ4NzYy9prBZv307/bt+N1LBrh1zpwDQoTTAzIXtKdO4jSMbuJysULlippKCShXiIolImsPuf6m3y+LDpy7ZeYZp0+qlMvc091FpVJJy+UyBUFQYeanADwCIBWRkIjGiMg+qjqWmaep6nxVPY2IjlLVJ4joMACnAzihkxYR0TCAR1T1M6r6FiIqEVFFRHKqWlFVUlUhohjAWiLKjDEzoyhyxhhlMrCWbGn//UrVoaG1Ty9dGs+fvf+bSWgL0tSqy0BxBmSppSx7FnE8H84zvBzjnLvhf47r2XZpf//AnwWoAFXHTfivclSeR0RTAYA4uJIqpamolJkrJUi5ApTLxLlw9u0P3HdPOnnS0KwzzpjQVSlTsVhAuVxGoVAoMHMOwOMiMkBEkYgAABMRqSqJSAqgn5mfVtVFALYC+Bu0jchVAG4AcAeAJ1R1tqq+T1WfJaJ9mPknABqq6gDEABLvPYjIEFFJRFJVbTDz5CAIDDO1mNmoshRnzOgeWr9+oPfZZ9fOnDr1MFHpJ+cUzqv6lJH5IpL0VqgcAQChCcstFy+6ZKD/Z1/7cwA/PWfeCcWwMMhsP94mqiu4XAJVykzlElG5i6irCCrku9f2bnnk2aGB8rxzz43KlQrKpSKVyxUtFAqWiLyIPKWqCYBJqlpBu5/qEZGJzDybiN6jqkd77+8hoveq6nYi+g4RBar6QQDvBPAOAAuY+W4APyKiBao6A8AtqvpFAAtFBERUZOYKgPGqWlTVHiIa6qRyEAR5a61T9T4IIi4fOCdcd+/SfJGwprvctQ9EEnZi1XmFc6Qu60Ka1gGaDMI+AfFlQ2PG5i7p37F+V168u/YJ9KvWRl/a+UCY+xNyUai5HFMxUuSNIoyQthK6f+XKhQed9/fVUqlIpWKBS6WSz+dzQfurcC+AQe99WVXzqtqtqhNVNSKizap6u4iUVLVFRBc5574IYI6IXOK9f5+qiog8IyLPqKp4798nIpeKyH6qer6IfNN73xCRCjPfCmCt954BdAHoIiIjIkUR6RORZUSURVFki8VykM9HWiyW+KC//3zz/pXPLEySRCm0RiIryAVKuZyafJ45Ch4Y5WBN9EURf7HuZnifp4F/N3f+WwthfoTJvL/9Dt2ihahiKmVoscDIFxWFAlEQzvvtI38anvPpTy/vmjC+p1AqoburolEUBUQ01Tl3D9rW1DNzQ1VrzJyo6j4AFgFYSERLVfXdAC4HsICI3gvgSWPM94joZhFZq6pxpznfraq/Nsb8UUT2AfBhZh4G8FMAxxPRLar6BQBv8t73qeomADuYuQmARKSMdvew0FrTMIYVRAAQFvaf89T9N/x26gGTpkwn7/uQesCnUPHkY99NLlkNYH8AFct6SX+lB5cO9m/eCXZXgF79N4wJp47uvpgo2CxhYYoEgSIMlYKANLB2Ze+W28v7z0m7pk6elM/nUCzkuT20w1Tv/UYAbxWRhqqOrnhscs6tIqJbARyvqomq/j0R3QbgAiK6Q1VvJqKzvPcXqT5//2f0b+89ADxijPmi9/5dAL5MRLc4584H4IkoJKKbiWiMqh4gIlNUNWDmfhExxpgtzDwlDMMtBRHyzvvuqVPGl/af9ejqbVvs3DFjp0kQOAojFROQKQQqLtymSdrmwflPESenAjjxBQDXzZ8/iV1wO4CLAEAJd4kNpyCygAmI2KgGBkjdlCd3bJ+54NOfWFbI5xFFEedzOW+M6QFwFxFtUdXAew9jTMV7fygzv5uZ6977bxNRwXt/gTHmUlV1InIBM38VwDs6oG4RkbuJqLkbxAIRHUdEJzrnDgUQM/OXReQTAKZ0NPBSImIAnwJQAnAnET3mnBs0xkBVM2aeYK09wlo7kM/nDaA44MNnRw9dePFb9ytVMmbaAGtgQqPehIAJ9oFmfwTp20E4gNn+cu3MgybOWvfE9ucBDBJ5X7EY7dynZWOfhjEzjbWkAUOJiRRmxY5t901bfHItly9MyefzUiwW1Vo7SVV3OOcWoz2wtcaYDQBWiMidRHSqqm4jog+LyBZm/i4RnSci3yaii1T1Hmb+tff+QACnEdFJ2E2ICKq6XVWvVNWVzPw/vPff7IA8T1UvFZFNxpgPi8gIgAnOuduDIOgGcKyq7uu9Z+993Vq7PYqifYhou4jXLPOFKSce9/tH7vtTeUHPuGlK7MFWOTAiQQAT8FPe+bcDQCksjvVu5HQAPwQ6RkQB8mtXTwf47e3GQn1gM1WtgbckwqywRN6l4zYn8dsnvfWt46PQahAEFIYhq2rivV8G4Ceq+v9UdbWInOyc+4S1FqoKVf2Bqh6qqr8RkW3e+8tU9XYi+rKqHui9vwzAuWhb6UcA/MY5d7lz7nIAv0F7HNkD4FwiulRV91fVL3vv7/TeXyYiW4jotyJyqKr+H1UFM5Nz7hNEtNg5t0lE7iKiX6vqnUQUW2tNEIQU5iLd9x3vGLclTY7zWdoDqLbrzGBr4GH3VcWAAgDTcbJ29b6jP6xBW99nGJOfEYy0ijSmO1TCLRREPchFZIKANQyFQqPPpunq8C1HPjl+3oHlKMpRsVgQZj5AVR/z3p/V0egBVV0JYDEzbxORfQGMVdXDAPyaiD6JtrH4oYh8TFVPQnscdzkz/5f3fnVnnDiLiBYR0WGqKqq6XkRuNcb8ynv/eGew/W4imi4iXyGiyQBOZ+afiMhH2nqBsdQ2FhONMVeKSAzgMBF5LxGtYOZDiagPquS8mrReWxFv3jw4hrlIaQpJM1LvGN4ZdemDrDRG1qx9VLys+TvJNl8OjFgASGBO6Db58ar+fbxu3dUye78WDBMRVNgAAguHYJ1Lj15w8onLC4UchWGo1tqIiLap6urO2OwMVd3CzFd0xmY/VtVvA/gCgEtFZDERfUlVv+WcewuAW4wxfxCRt6vqlzpGAp0BN9BeUACAQzoJncEyVPW/jTF3O+feTUTfVFUB8CXn3BeIaKL3/nxmvkRV/5GIvq2qARF9QlWnA1gqImuMMbOCIAicc1k+r5h16ruKDz348JGzDe0gIAUzQExEVmBMitVr7obXsyObW5e6+O2Av5oAYC3s78YVxhypinEA1blS/i3PmlnWUtFSPgcuFCjOBc3l3RXz5i9/MQnDkHO5HIVheLCI3P6Nb3xDly9ffg4zR7v3XW8kEZHklFNOuf9jH/vY74IgKFhrJwE40Tn3aJIk2opjevDib+WPGm6lhTTNS6OpiJvQetPrug01P1x9L0ELRNgx0By4byb8aVYBXg+qimIcACj0FsTJOOzojbk40wMSiM90HezwtFNOrhtjJodhKNZaUtVCkiSz7r///hOZmc4888w/rl69etWcOXPmPP300ysPPPDAuc1ms56maQag55ZbbjnOWrvitNNOG2Fuj+H7+vr6+/v7ByZOnDh+zJgxY6655pr5ItKzZMmSezvN73lCRHj00Ucbq1ateteYMWP+dMIJJzQ6cPSZZ55Zvd9++80IwzAcHBwcuPnmm0+w1naXy+XbFy1aZP7whz/ozTfffHylUnnmrLPOeluWZZuCIMgZY5SIyDBj2knvfHbDdb8pz3U+r+otvCayfYeXZnOcQm8BcJoqJgCcKDzbTcCknA0fBnAWAKjyDiWUtZmEfv2mxM6a2VAbdG8L7SGHHzTv0SAIgPZofJyqPtJsNt9sjDFZlsVnnnnmMara6PR3C4noJ6p6HoCHmfmmW2+99bh3v/vdI2ecccYQgGUAzgZw3C7jvjU33HDDxlqtFpx55pk3ABhdUg+89wUAMwGcsHjx4uI555yDM844Izn++OMLAK7z3h9rjPlkB/JFInLm0qVL++I47r7iiisOyOVy5dNPP33pOeecs/K66657z5IlS54mopNV9XcAxllr+7xX2ufNC4sPXH/DvLnS6JPEx1i/UaXZKrR/S9422qtYGz603mUT2AP7Rhxyu89VcOADYnWqHuLSfLpxY5dWa9vZmO6wUCiTseC2+swTkY2jlSciiMjFqvouEbkEQE5VP6mq56vqId77LwPAtm3btqnqqar6HRGZB+BqEfmCiFykqqvCMDRERM65S5xzl3XSd1T1a6p6uqo+2Gw2vwcAvb29G0XkMFX9DjOfDOA7AH6mqhcQ0aw0TT0AhGHYA+A7RLT4Ax/4wB1ENHnDhg23qSpEZDOAedZaMobI5HIltcF4DNd28IZ1OfGuCCiUycP4YJRTyCEE2Jc9zFwY3rf9NqDe9DjhQAUW3oNcipH+HcmEgw/5PhEnDIWIKIC6qtZ362POArCUiP5FRL6lqjVVvURVfy4iPwKAKVOmTAbwsHPuCwB+5Jx7P4BLAVwgIouZmbQtN4jIr0TkV97721T1MREpiMj7SqXS5wGgp6dnkohc2fkBnhKRL3Ys8JUicl0QBKYzhLpMVb8BYOlJJ530NhHxt9xyyzGde0NEVG3rAKVgbk6cP/97IwPb1WWO4T2p91aFAxUaM8rJGp4CmDkWoDcTuON+RqmSlgyJI/Uq3nhKM9SisKX5cJYxrNbamjFmnKpuc869s6+v72cA/rGjhXMAbPbe30ZE/wLgHhG5jog+O6qp69atWy8i7ySiSzuWelhVrxeRx4wxSaPR+EC7K9GZqlrsXO9Q1Q1EdIeIbKnVagDwHVUlAJ9WVRDRJufcBcaYA7335xIRvPerOv3ol9FeFgPaq9a9jz766CTv/XcALGLm7QBCIhokIOJibkY1CJs5X/fshYyqcyQGRBEpUkBDEM0jaGYB7KekC6EEgj7JAOC9aHvnQSAW/cVSuO/cuYn3PmLmnDEm7UzDFlQqlb4OvGDZsmXfO+yww86x1m7z3v8LM39NVd/mvf9+54f77OzZs2eoatF7/x1rba+IHAngFAAf8N6jWCw+2mq1SER29SmcucvQBsVisQ4AW7du3UxE56vqJBE5tzOrEefcZStXruQ0TT/IzGi1Wl/P5/N/Q0SzmfkrY8eOPaO3t3ccgGNEZIGq3sXMKRH1QFXGzp2bbSyWypPTXqgXdd6BvRdVZEr6FBSHMnCYgBMLIA+lCgAo6RZRhFAFeQ8IQSwQR2FPYZ9JW4MgSK21LCJzRaRJRB81xnwNAIwx5oorrvhspynPEpF/BlC11jaiKDoZnd73+uuvn7Fp06bHrLVHhGGo3d3dEJGHsizbGsfxjoGBgTMBFK6//vqrjTFBtVottVqtuNFooL+/X7Msc0NDQ1MBvO3WW289aenSpUfEcVzy3ouIxEQUGmPOA4BRS++c+6SIVIwx53nvL91vv/3uXb58+SwAE4jooyJyjqrOtNauIiJXnLoPJ8Woy2cZwYuyqIoqoEgFWMvAoarUDaBgAfCo96sqWiA15L1CmMApICDJh/l8qcTGGFFVj/aUaqRYLP7L2rVrf9xoNL5eLBZ3aggzEzOHaO9VVJLkuU1+7/2MBx54YAb2Isa0V9h+85vf/Pk9WaKJrVZr4iisUWC7y9q1a389b968aQAuA/CMtfapOI4XeO8vLpfLX4vjeA3aa4gCQE0QqURRDmlKgEK8AAqCihK0iueGV8yA2tGOkYAUIIEoqfckzpPLHBAEFY4iEhFnjPGqOhbt3bGZ++6778l9fX3YsmUL6vX6zqWnN5JYa49g5lNV9d9E5FvHHnss+vr6cpVK5X0AphNRA8BY770AEJsLSYytqMtUUwd4bbMDRBWNUV6AWn6e87WSV9KmGtRhUGdwPYCpE1srzmVE1ErTNFHVLd77AQAwxswEgCzL0N/fj82bN2NwcBBZ9rKdP18xWbVq1ZPe+49nWXam9/4/DzzwwGcBpNu2bTMA0FmE3UpELWNM3RiTsA1CgOpgqpNBnRk1z9QU2J28aFT7dr5BmldFyQIqIPKq5OFh4evqfaiq+c48dC6AjXsqrPce1WoV1WoVYRiiVCqhUCigs+D6mkmWZWg2m2g0Grjqqqui7du3Hzt58uTbJk6cuHb27NnvJ6Kt995779NLlix5E4CJqjqHmR8CYLMkSQP4qgrKBJAwhKBgJVZyFjs9jwEreE4FhajIopRAGSAGkRG1CJwfcnEqlMtZIhIiqhNRcc9Ff07SNMXg4CAGBwcRBAHy7QVYRFH0igN1ziFJEsRxjDiOn9cCduzYcdbVV1/9gs/84Ac/WLFkyRKoahlA3XtPzjmbtVrMiWs66BgQiIWcQpVZQwJ17eQFwBLQC9UNIEyHoqJGYwhIIO21QvbepGktHhpyQXeFOyvNDeCFHvi7yBAzb+zkwSIyLcuynl0rZa1FEASw1iIMQ1hrwcyw1oKInmcQRGR0TREiAuccsiyDcw7OOaRpOrrcvyfZxMyD1PbBgfd+Ptq+NhgYGDgJAIhoHwB1dFQrHhlxuSyteUg3g4xv93VKYFJomaCA0hoCtliBPi4EQ8B0IZ1LgscZUKfwgIoQo7S9v9nYtDGMpkwxYWhtZ2B7SCfT5wkRPU5ErqenZ924ceNkx44dPDIyAgCbReTg0edGK/8qihhj/qSqZuLEiWvL5TJv3ry5GMexV9UxqjoTQNrb27u8VCq9mYgeFxEbxzE11m8Mcn39Dc8QFaiFAkSkKoZID1YlgOQhQB9lBT2tKqsAgBTjhaTmSb0C6kDGiQa8dWuhsWZ9TtVb55wBMKiqxjl3LwCUSqXfjsJj5vqiRYsmBEFwOjOfEUXR6SeeeOJYImow85OvJrFdxRhzfxRFQ0cfffR8a+2ZQRCc0dPTs3jx4sVpZ+q2paen596VK1c+pqoBgMHMeyYiaqxdVwy2bCtnQoEA5Nu+TJ6gI6o0BgC86rMCeoYZfnXm0+0Ytc3CDXiygASqYK9MNFwrNNavnZ2mKURERaQOAEmSbN1t2GIXL148pq+vb8p3v/tdiAi+/e1vo6+vb+qJJ57YjfbK81/syP1XyAZVjRYsWHDc2LFj81//+tcxffp0nHfeeXjyySffsmDBgg3MvKNer49LkiTqdA11FYFzHsn6TbN4qJYnBYmCFQjEU6BKzVFO4tMdgF9rCVib+Oy00OQAAMTUVFEVpUyhUKhFko5F4g5Mk+QOIgqIKGVmOOemX3vttXfW6/XjrLX3EpE89dRTxy5cuBDLly/HvHnz8MADD2D69Ol45JFHDmDmewE8rKrjXk16RDQwadKktRs2bFgYRRFWrVqFSqWCe+65B4cffjhardaihx9+eG2WZVOMMRYARCTN0hQuzRLN0nk+i58ASAOwCOANgcHaVGlb4KbPxjtgjd0fqK4Uf/ROWyxqASYlsVBWBbwCCPv778x6dwzQPvtMMsZoEASbVfVtRx111E3PPPPMzQBwyCGHFK666iocwXXn3QAAEZlJREFUe+yxePbZZ1GtVjF16lQcc8wxWLp0KRYtWrT60Ucf3Wl+X6lB954WXk866aSJy5Ytwwc/+EHcfPPNGBkZwTve8Q40Gg0sXbo0nD9//vJWq8UzZsw4RUQ2dgyVtjZtGgj6+m9X8EQAQlBWKBFYSBBqh1Mq/m3zgQvabrNAnwIPEHAEgBMIspJUIKTkARaFMQ89OlJbvjw/5vTT1DlHxpgnACyaNm1a38UXX/wxEYGI4IEHHsD111+PJUuWgJlxyCGH4Oqrr8acOXPwmc985m9Hnxu1qLta11Ggu7+OAtr1lZlBRM+7Hp3OjVrwG2+8Eddccw3OO+883HXXXRgYGMD999+PBQsW4Oyzz/5okiT/KSLjVPVGL6KNRoOaD64o24cfq3nIFEOkAlIDdYAnAb+jw+sBAtYBnV25v4OphWyHmPidALqgtEIIxbbnIkOg5KrDJT3ggLebgw9ew4aNtTYlonne+2IQBN2qyiKCOXPm4Je//CVEBKtWrUJvby9WrFiB888/H8Vi8Xnw9gZy1793T3vT3t21kIgwefJk3HzzzajX69iwYQN27NiB7du344tf/CKstS5JkhKAsSKyIm61qNVspcnd95yst92RWMAYYjEABYCxsIMKfQsAOPGXpz77w/cg6xkAArj7W65VHs1cGNsZpBYMQKAgiFIevX23tTas64vjmFqtFlT1QQBzkyT5xWgFp0yZgiuuuAJRFIGIEEURLrnkEkyePPkFkF4see+fl17KZ3aHfdRRR+GrX/0qms0m0jTF3LlzcfnllyMMQ2RZ9gtVnauqf0rTVFutBK11m/pNb98tqhIqoKTC3LbAqup3jPJpuVaXh/sTsIun0dMwf+zJd+0PxWRVPA7CjhSglgIJhDxAaSm/OffZT0+17z+9t1AscqlYZCI6Q1WfiaJoWmfF+AUV/nMAdtfCF2vCL9Zsd0/GmL39PRTH8QARzfYi1zXqdbRaseh//fe41uXf326ayZQA0BwMAlLJA8SKsUp4EwhbBlsjz86DPw7Yxb1NgEuc91e0C4qDFboDECK0zY4KyNdbU2iknqT9/X1JHEur1VIRuU9VD0iS5Jo9NbndwewN3p408M/9AHv7vr3lPZriOL5BVfcDsCxNErRaLcRbt/XpSD1zzXiSgtgAyvAwbSPSq4Q3AYB6fzmA/z3KbSfAEP7melqfNrppQqKtAIAhUAiAWD1BbO2XvxyJ7l52QL1eR6vVEieySVWr3vszvff37K3v2luF9tZ0X+z6xZrti/Wfqoosy2713p9FRMNpmm6u1eucpqkU7n/wwPov/m+DAGsgQgAZAhkoWDQZ5TKY1ueuh7/9BQD3BxLfNtHXdHxAPgyiEduZzzJAEFI3PDxO+3aMYP3GvlqtybWRKrz3d6hqMcuyblUdGm1+e6vYi/V7f8n13mDuCWKnTL1pmk4CEKRpeme90aBGKxa/ZsN237d9OKnWxrQdKEm4c9qbCcPKdLa2rcEvAG2c0nZofz7AdjOWC0fSxuiZCMuCHQYg204IIEoA9f7kqlLlqZXvTONGrd5ool6vp9r2OD04juO7te3L8qKa8FIMyksxIC81H1WNW63WU0R0sIjcFsdxVq/VEdfr9a6nn17c95OfFRhCFkohFBZqLKAQ7kfHi62aNtcmkG/syux5AA8GtntxBwB0BwAo0YcD5WFDpAYEA4YBQzPXvf26/75vzP0PlhuNmtZqDWo2m4MAHgLw3iRJfrWrEdj19aU06d0t8K4gX8p37CnvJEl+h7YP4oOtOB6oVqtaq9do3EMrituvuW4ZMt8TgikAwxBgicDKwyD9CAAocIsTN3th22N2zwABIIb8r6G0vqzd4jUnLD4E1HS2OgJAQ6KkuXrlfn79Rp9fs663Xq9ptVbzjUZjjao+AuCDzrlfAHhFNPFlal6aJMkvVfUMAA83m821tWrVjNTqlHt27WbevJmba9bMMgRvALWAsBIFgIA1BRAqgKG08TBBzt+d1wsALgS2irhAIT8CFFCcYYFVAUHzgIYAWRKyUF37o590j9u89ah0247eWrXKw8MjaLZaq1V1uYj8TZZlN6jqyN60cW9a+WLjwJeibaOvALamaXoPgLMUWN5KkjXDw8M0ODSk0rejf0LvjiPX/OA/ihaAhUoHIIekxJCVpLqkzUB+KOLsfOAFUZH2eNDmQ9ClcG5J3uZmKBBCKWcNDQsQCEQ8wSiIRMT0Llvef9CCQxZsjIKnHbikIgKgGQTBJlU9xTm3GsBqANN2b3Z7et2TMdh1/Lf7GHBPY0IigjHmHu89EdERAG6O47hvcGjI1OtNifsHBg5cv/HYJy765gbrfaVAxCGBilDNEWyOTaxCbwJhHIGqQ0ltex/kcz9re9/+eYA/BtynYaqG6HHDZhEIE6D6EClXAHgHYgAsBPLqo833P7Bm4cELjt1A9GhGWnaqEOdbzPS0MWauiExX1Z9re+Qf7KkP2xO43QHuCmhP0Dpz4CERuVZVTyYicc7dPlStJrWRmtZrVY37h/oXbu094bFvfXuFSZOxEZGNFDYCfA6MHDGp6nYiHA8Aqc++lIn//RGQPUb32OtZuR9C1nxc/GcjGz5JoIMBmk+svyPQBAACUqiSVVJIlhW3LLt/5eGHLzx8o8NTKUkh88555wNAN1pr+1T1VBFZx8w3icjB2j6a9bwmt2uzHJU9QdqLBgoz/1xVJxDRUQD+2Izj1fVaLalXq+FwraZZ//DWI/r7j3/4m998gprNSTkgyIE4YmgBLDkiWKUniXAOAAjwi2ra2Ocg+Ev3xmmvAAHgH6A31lz6oZzJEYCxUD6YCHczoUeU2DMMQCBSylzWtfHOu2tHHnXk9DjLHu/1bqzKzj4sZrarARUROUnbHq2/Q9tFrmtXiKPXe1p5GZ2K7QZvC4D/ApADcDSAJ0RkRa1Wy2rVBtXqVQwODMvkoZGtbxoZPuyBr3ytj5JkfJ5gQmWTJ0IR7AsEWKb1qvhIh8uq4WSkVYJ+6N/30HRfEsB/B9zHgWVO3fjIBAsIGhHRDIY+xNAygRVQgjBDlcW7YN0dd2bzDj7YTjXWPJVltVbqOUvTXOYz8c63VGU1M9cBHKKqU1T1NgB3S9uzfho68/Pdm+0uyRHR3UR0B4DtRHQgM5dUdV2apk83m83myMgI1RpNHR4aMLVavX5stV6sbNpaWP71i4qBl2IEaF6ZCyxSAEsemgXEW1RwGkGLANxI1riaVL4yp30YfK/ykmImPApzap7t3EKQ+067ctigwP2xYlwq4AYLN6AcgzQW9S0oeubM2fTmv/v0kY8Q3bUxF3bnCpHJBYFEUZ7CMEQYtnfjmDnsaOE07/047/16Vd3S8RZIvPdgZgugQkSTmHm6MWaYiDZ2oKejO3Rx6rXValAaJ9qMWzohTre9FXTy4z//5dLtDz64fx7gHLdHE3kYXxDhHINyxFsVeCsU+wKQetb6p1T844fA3/jn2LzkqB2PAOeUOSqHQefoP2EThP6YkUxvgnwLoi2QT1RLTZUkUTXehukxXzp/MJw8acyt3j1RZTOhkM/bMBeQNYHmcyEAAxMwhUEgaLurdcJmAUTtU/LS9kfU9jUABrIkJS8eBGiSJJRlmaZpqtV6S3oEAydZO6+1bVvfsv99yVjj0jBH5AsgHzKFecAVwFIAyCi2EeMoKGYBQJrF5zUlSQ4G/s9L4fIXxY15BPhqV5AfsRx2INIQAdd7yP4tQJseQQvQJpRarC4VlRQQU+5qHP2Fz6e5CRPG3dGsr9geBOMtIQzDnLBlCkyAIGBSJTWGoKoZGePEOQ8AbK1R7y0RWSfKDEWaeiiJpnEK5zJkzmfjVftOyOXfnG7v237vv12Wk1qtEAKImGxOyOZBvgClvAHyIDWglar0PwjtfjiV9Lx61iq8CfjWS2XyF0cuegz4+zyHYRTkvwmAFUiJ8WNRPTSGaqzwTYEkDI2hpuVhHSFLoGq7K4NHfvQjtfEHHHj8mlZ828NJq9HnsnHGhNYGhgwD1rYNhaq6dgwZoDM5sCKs4jLy4uEyp+KzrDsIB95aKJSmBdEJ2598+s4HfvaziqtWe3IgChQ2b8hHUJ8TQp5h8gREIEtKjwP4KLU9yKSZxV9IJPmL4P1VAAHgUeAjAduppaD0v9CJd2oIvxKlcgJfjAHTFLIxqcaKLGXlTGBa0CxTsWK40TNjZt9hH1hiumfMeEcs+sS6VmP1hjh221KniXrK4I2gHXiH4cnA+DwZnRQEPCOfs7ML+f0j4gMH1qy/88Hrfikj69aPZ9FijthZUBAxfCQkOYYtClHI6nNgH4AalrThFWd2CAwOp41/F3FrDwV+/pey+Kujtz0EHMXgT48NS3NBGI0XuAqge5TkgFhJEqiPgSCBaiJwKSRogbxTNY7IZ6rGkWYmFw10T506MnXBm3X89Olhvru7GJXLlSCKxgNAliR9rWp1JBkZafZt2JBufPghHtmyuUviZEygFAREYlXZErkIGoTgNMewEYhCIMsBlCMERvkpJX07FPsDACnuG0rrazzk8gXAn/4aDi8r/N2TwJgU+HlPWHqQiL/y3B39NROJE0yJiVwM0VQ0TIE0ZVgH+EzEZKAsgwYegIdmClivnCl5B2KFSHv8xWyhQlBjLElIgCOQDUFqAR9AA8vsQ4ADgc8BgWVKc2DOqQbM2KSqAYHeN1pCUflaNa0fTsCHDgGG/loGLzsA47WA2Q/4Z8sWXUHhQ9o+nAwFUkB/ysAYrzQ1IU0cqYmVxAGcifoU4IwhHmIgTI4FIuQEUAWI2lNGKFQIHZcxVmuElVgQgL0RmBDwAZOxgIQEEylcpBQxY6MRJI4weo4PAJ5pZM1rMnHuTcA36bnjZH+VvGIxVB8HZjvghyVbvCkw9hsKLXRuCSn+rzIEqtMzgnEKTQnkPJwzCJwCHuqkbTUcAeIERNSeAajCWoZqe2XcctuqBJahgUdmGUEAeEswgcIR0XoVWGqD43ZFqZn49CtN11rkgE8d3g7487LlFQ1CqwCtgDmVIH9bDIsPWeJ/1vYUa/T+kwq9j5WKRDQ5UwmFkHmAPRSOQOpJhSHtlcT2fI6gCiYigWGjsAplkBoAAWAMOBPVbSBtEOhotCMZjVYwdioXN9LGmwX844Xwv38lQyK/KmGQHwQCMmYJRD6UM7nbQms+D6V9n/cQ6VYAdwLUIiBSRQVAt5IaArwHgbQ9jFESNu1ItKbtLIFhIlQ73UQOwAlQmrRbzTaIc5c3fHyiMv9Cvb/2sOdicb1i8qpGMleAH7T2BPL6T9aYu3Im7CHQeXsvDQ1CZQVAI6QY0fZ0DqRaVEIXoF0gXgDVMXv7CoVeFvt0KPP+WGPoWwucu/Pl9nMvJq9ZLP0HgXFg89U8h+uZ7SWvRh4i7vyGZNNZ3DcOA171MPDAawhwNL8/sfllzuQ2M/EL9hdejojq5bFrTT1M/RmvVtj3Pclr/t8c7gRswdjfFzjHoOfCh7wcIcU9dYlj6927Xo1+7sVkz0d7XkU5HnDq3ftjadWVdNPOU6J/bSLd1pBWb+bdktcaHvA6AASAo4Bq5s1Xmz79tQKpoN3L/6XJAy7x2VXkzdfe9jJmEy9HXheAAHA00sdJ6QFR/Ye/FmCq+g9edeURSF8z5/Xd5TXvA3eXZRxcam00zMDukeX+nHwj88nYt/jnIvC+HvK6A1SA7jPhb4wJqtSOofBS5NpMsuKtLn3Pha/iGO+lyOsOEACWAXm1we+Ywm4iLPwzjz/mJNuSufT049vHJl5Xed36wF3laKClLjhbJHtEQdW9Gw2qZ97f5505940AD3iDaOCo3GNzx4HxVlZz0Z7ui8o/KrLlxzr3x9e6bHuTN4QGjsrbXHwXRJywnP8C7WP5AkHkjQQPeINpYEfoHpP7hRrapEr/0H5LL2fR8W/z8d/gNZymvRR5IwLEnYBlW/i9gJiACNBYXfOU41/ExeL1kjdUEx6V4wEnLlxCkCogfeqaZ74R4b3h5d6wNP/esDT/9S7Hi8n/B3LrBEUxxEM2AAAAAElFTkSuQmCC\"],\"colorFunction\":\"var speed = data['Speed'];\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n if (percent < 0.5) {\\n percent *=2*100; \\n return tinycolor.mix('green', 'yellow', amount = percent).toHexString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', amount = percent).toHexString();\\n }\\n}\\nreturn 'green';\",\"markerImageFunction\":\"var speed = data['Speed'];\\nvar res = {\\n url: images[0],\\n size: 55\\n};\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n var index = Math.floor(3 * percent);\\n res.url = images[index];\\n}\\nreturn res;\"}]},\"title\":\"Route Map - OpenStreetMap\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false}" - } - } - ] -} \ No newline at end of file diff --git a/application/src/main/data/json/tenant/device_profile/rule_chain_template.json b/application/src/main/data/json/tenant/device_profile/rule_chain_template.json new file mode 100644 index 0000000000..da9a95b423 --- /dev/null +++ b/application/src/main/data/json/tenant/device_profile/rule_chain_template.json @@ -0,0 +1,136 @@ +{ + "ruleChain": { + "additionalInfo": { + "description": "" + }, + "name": "Device Profile Rule Chain Template", + "firstRuleNodeId": null, + "root": false, + "debugMode": false, + "configuration": null + }, + "metadata": { + "firstNodeIndex": 6, + "nodes": [ + { + "additionalInfo": { + "layoutX": 822, + "layoutY": 294 + }, + "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", + "name": "Save Timeseries", + "debugMode": false, + "configuration": { + "defaultTTL": 0 + } + }, + { + "additionalInfo": { + "layoutX": 824, + "layoutY": 221 + }, + "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", + "name": "Save Client Attributes", + "debugMode": false, + "configuration": { + "scope": "CLIENT_SCOPE" + } + }, + { + "additionalInfo": { + "layoutX": 494, + "layoutY": 309 + }, + "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", + "name": "Message Type Switch", + "debugMode": false, + "configuration": { + "version": 0 + } + }, + { + "additionalInfo": { + "layoutX": 824, + "layoutY": 383 + }, + "type": "org.thingsboard.rule.engine.action.TbLogNode", + "name": "Log RPC from Device", + "debugMode": false, + "configuration": { + "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" + } + }, + { + "additionalInfo": { + "layoutX": 823, + "layoutY": 444 + }, + "type": "org.thingsboard.rule.engine.action.TbLogNode", + "name": "Log Other", + "debugMode": false, + "configuration": { + "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" + } + }, + { + "additionalInfo": { + "layoutX": 822, + "layoutY": 507 + }, + "type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", + "name": "RPC Call Request", + "debugMode": false, + "configuration": { + "timeoutInSeconds": 60 + } + }, + { + "additionalInfo": { + "description": "", + "layoutX": 209, + "layoutY": 307 + }, + "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", + "name": "Device Profile Node", + "debugMode": false, + "configuration": { + "persistAlarmRulesState": false, + "fetchAlarmRulesStateOnStart": false + } + } + ], + "connections": [ + { + "fromIndex": 2, + "toIndex": 4, + "type": "Other" + }, + { + "fromIndex": 2, + "toIndex": 1, + "type": "Post attributes" + }, + { + "fromIndex": 2, + "toIndex": 0, + "type": "Post telemetry" + }, + { + "fromIndex": 2, + "toIndex": 3, + "type": "RPC Request from Device" + }, + { + "fromIndex": 2, + "toIndex": 5, + "type": "RPC Request to Device" + }, + { + "fromIndex": 6, + "toIndex": 2, + "type": "Success" + } + ], + "ruleChainConnections": null + } +} \ No newline at end of file diff --git a/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json b/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json index a299fa4c38..a37b1fc865 100644 --- a/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json +++ b/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json @@ -8,7 +8,7 @@ "configuration": null }, "metadata": { - "firstNodeIndex": 2, + "firstNodeIndex": 6, "nodes": [ { "additionalInfo": { @@ -82,9 +82,28 @@ "configuration": { "timeoutInSeconds": 60 } + }, + { + "additionalInfo": { + "description": "Process incoming messages from devices with the alarm rules defined in the device profile. Dispatch all incoming messages with \"Success\" relation type.", + "layoutX": 204, + "layoutY": 240 + }, + "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", + "name": "Device Profile Node", + "debugMode": false, + "configuration": { + "persistAlarmRulesState": false, + "fetchAlarmRulesStateOnStart": false + } } ], "connections": [ + { + "fromIndex": 6, + "toIndex": 2, + "type": "Success" + }, { "fromIndex": 2, "toIndex": 4, diff --git a/application/src/main/data/upgrade/2.4.3/schema_update_ttl.sql b/application/src/main/data/upgrade/2.4.3/schema_update_ttl.sql index dda3bd7b1c..7de74032ce 100644 --- a/application/src/main/data/upgrade/2.4.3/schema_update_ttl.sql +++ b/application/src/main/data/upgrade/2.4.3/schema_update_ttl.sql @@ -22,45 +22,45 @@ BEGIN END; $$ LANGUAGE plpgsql; -CREATE OR REPLACE FUNCTION delete_device_records_from_ts_kv(tenant_id varchar, customer_id varchar, ttl bigint, +CREATE OR REPLACE FUNCTION delete_device_records_from_ts_kv(tenant_id uuid, customer_id uuid, ttl bigint, OUT deleted bigint) AS $$ BEGIN EXECUTE format( - 'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT to_uuid(device.id) as entity_id FROM device WHERE tenant_id = %L and customer_id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted', + 'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT device.id as entity_id FROM device WHERE tenant_id = %L and customer_id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted', tenant_id, customer_id, ttl) into deleted; END; $$ LANGUAGE plpgsql; -CREATE OR REPLACE FUNCTION delete_asset_records_from_ts_kv(tenant_id varchar, customer_id varchar, ttl bigint, +CREATE OR REPLACE FUNCTION delete_asset_records_from_ts_kv(tenant_id uuid, customer_id uuid, ttl bigint, OUT deleted bigint) AS $$ BEGIN EXECUTE format( - 'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT to_uuid(asset.id) as entity_id FROM asset WHERE tenant_id = %L and customer_id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted', + 'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT asset.id as entity_id FROM asset WHERE tenant_id = %L and customer_id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted', tenant_id, customer_id, ttl) into deleted; END; $$ LANGUAGE plpgsql; -CREATE OR REPLACE FUNCTION delete_customer_records_from_ts_kv(tenant_id varchar, customer_id varchar, ttl bigint, +CREATE OR REPLACE FUNCTION delete_customer_records_from_ts_kv(tenant_id uuid, customer_id uuid, ttl bigint, OUT deleted bigint) AS $$ BEGIN EXECUTE format( - 'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT to_uuid(customer.id) as entity_id FROM customer WHERE tenant_id = %L and id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted', + 'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT customer.id as entity_id FROM customer WHERE tenant_id = %L and id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted', tenant_id, customer_id, ttl) into deleted; END; $$ LANGUAGE plpgsql; -CREATE OR REPLACE PROCEDURE cleanup_timeseries_by_ttl(IN null_uuid varchar(31), +CREATE OR REPLACE PROCEDURE cleanup_timeseries_by_ttl(IN null_uuid uuid, IN system_ttl bigint, INOUT deleted bigint) LANGUAGE plpgsql AS $$ DECLARE tenant_cursor CURSOR FOR select tenant.id as tenant_id from tenant; - tenant_id_record varchar; - customer_id_record varchar; + tenant_id_record uuid; + customer_id_record uuid; tenant_ttl bigint; customer_ttl bigint; deleted_for_entities bigint; diff --git a/application/src/main/data/upgrade/3.0.1/schema_ts_latest.sql b/application/src/main/data/upgrade/3.0.1/schema_ts_latest.sql new file mode 100644 index 0000000000..bcdd1a72d4 --- /dev/null +++ b/application/src/main/data/upgrade/3.0.1/schema_ts_latest.sql @@ -0,0 +1,35 @@ +-- +-- Copyright © 2016-2020 The Thingsboard Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +CREATE TABLE IF NOT EXISTS ts_kv_latest +( + entity_id uuid NOT NULL, + key int NOT NULL, + ts bigint NOT NULL, + bool_v boolean, + str_v varchar(10000000), + long_v bigint, + dbl_v double precision, + json_v json, + CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key) +); + +CREATE TABLE IF NOT EXISTS ts_kv_dictionary +( + key varchar(255) NOT NULL, + key_id serial UNIQUE, + CONSTRAINT ts_key_id_pkey PRIMARY KEY (key) +); \ No newline at end of file diff --git a/application/src/main/data/upgrade/3.0.1/schema_update_to_uuid.sql b/application/src/main/data/upgrade/3.0.1/schema_update_to_uuid.sql new file mode 100644 index 0000000000..0e4e84d7b1 --- /dev/null +++ b/application/src/main/data/upgrade/3.0.1/schema_update_to_uuid.sql @@ -0,0 +1,878 @@ +-- +-- Copyright © 2016-2020 The Thingsboard Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +CREATE OR REPLACE FUNCTION to_uuid(IN entity_id varchar, OUT uuid_id uuid) AS +$$ +BEGIN + uuid_id := substring(entity_id, 8, 8) || '-' || substring(entity_id, 4, 4) || '-1' || substring(entity_id, 1, 3) || + '-' || substring(entity_id, 16, 4) || '-' || substring(entity_id, 20, 12); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION extract_ts(uuid UUID) RETURNS BIGINT AS +$$ +DECLARE + bytes bytea; +BEGIN + bytes := uuid_send(uuid); + RETURN + ( + ( + (get_byte(bytes, 0)::bigint << 24) | + (get_byte(bytes, 1)::bigint << 16) | + (get_byte(bytes, 2)::bigint << 8) | + (get_byte(bytes, 3)::bigint << 0) + ) + ( + ((get_byte(bytes, 4)::bigint << 8 | + get_byte(bytes, 5)::bigint)) << 32 + ) + ( + (((get_byte(bytes, 6)::bigint & 15) << 8 | get_byte(bytes, 7)::bigint) & 4095) << 48 + ) - 122192928000000000 + ) / 10000::double precision; +END +$$ LANGUAGE plpgsql + IMMUTABLE + PARALLEL SAFE + RETURNS NULL ON NULL INPUT; + + +CREATE OR REPLACE FUNCTION column_type_to_uuid(table_name varchar, column_name varchar) RETURNS VOID + LANGUAGE plpgsql AS +$$ +BEGIN + execute format('ALTER TABLE %s RENAME COLUMN %s TO old_%s;', table_name, column_name, column_name); + execute format('ALTER TABLE %s ADD COLUMN %s UUID;', table_name, column_name); + execute format('UPDATE %s SET %s = to_uuid(old_%s) WHERE old_%s is not null;', table_name, column_name, column_name, column_name); + execute format('ALTER TABLE %s DROP COLUMN old_%s;', table_name, column_name); +END; +$$; + +CREATE OR REPLACE FUNCTION get_column_type(table_name varchar, column_name varchar, OUT data_type varchar) RETURNS varchar + LANGUAGE plpgsql AS +$$ +BEGIN + execute (format('SELECT data_type from information_schema.columns where table_name = %L and column_name = %L', + table_name, column_name)) INTO data_type; +END; +$$; + + +CREATE OR REPLACE PROCEDURE drop_all_idx() + LANGUAGE plpgsql AS +$$ +BEGIN + DROP INDEX IF EXISTS idx_alarm_originator_alarm_type; + DROP INDEX IF EXISTS idx_alarm_originator_created_time; + DROP INDEX IF EXISTS idx_alarm_tenant_created_time; + DROP INDEX IF EXISTS idx_event_type_entity_id; + DROP INDEX IF EXISTS idx_relation_to_id; + DROP INDEX IF EXISTS idx_relation_from_id; + DROP INDEX IF EXISTS idx_device_customer_id; + DROP INDEX IF EXISTS idx_device_customer_id_and_type; + DROP INDEX IF EXISTS idx_device_type; + DROP INDEX IF EXISTS idx_asset_customer_id; + DROP INDEX IF EXISTS idx_asset_customer_id_and_type; + DROP INDEX IF EXISTS idx_asset_type; + DROP INDEX IF EXISTS idx_attribute_kv_by_key_and_last_update_ts; +END; +$$; + +CREATE OR REPLACE PROCEDURE create_all_idx() + LANGUAGE plpgsql AS +$$ +BEGIN + CREATE INDEX IF NOT EXISTS idx_alarm_originator_alarm_type ON alarm(originator_id, type, start_ts DESC); + CREATE INDEX IF NOT EXISTS idx_alarm_originator_created_time ON alarm(originator_id, created_time DESC); + CREATE INDEX IF NOT EXISTS idx_alarm_tenant_created_time ON alarm(tenant_id, created_time DESC); + CREATE INDEX IF NOT EXISTS idx_event_type_entity_id ON event(tenant_id, event_type, entity_type, entity_id); + CREATE INDEX IF NOT EXISTS idx_relation_to_id ON relation(relation_type_group, to_type, to_id); + CREATE INDEX IF NOT EXISTS idx_relation_from_id ON relation(relation_type_group, from_type, from_id); + CREATE INDEX IF NOT EXISTS idx_device_customer_id ON device(tenant_id, customer_id); + CREATE INDEX IF NOT EXISTS idx_device_customer_id_and_type ON device(tenant_id, customer_id, type); + CREATE INDEX IF NOT EXISTS idx_device_type ON device(tenant_id, type); + CREATE INDEX IF NOT EXISTS idx_asset_customer_id ON asset(tenant_id, customer_id); + CREATE INDEX IF NOT EXISTS idx_asset_customer_id_and_type ON asset(tenant_id, customer_id, type); + CREATE INDEX IF NOT EXISTS idx_asset_type ON asset(tenant_id, type); + CREATE INDEX IF NOT EXISTS idx_attribute_kv_by_key_and_last_update_ts ON attribute_kv(entity_id, attribute_key, last_update_ts desc); +END; +$$; + + +-- admin_settings +CREATE OR REPLACE PROCEDURE update_admin_settings() + LANGUAGE plpgsql AS +$$ +DECLARE + data_type varchar; + table_name varchar := 'admin_settings'; + column_id varchar := 'id'; +BEGIN + data_type := get_column_type(table_name, column_id); + IF data_type = 'character varying' THEN + ALTER TABLE admin_settings DROP CONSTRAINT admin_settings_pkey; + PERFORM column_type_to_uuid(table_name, column_id); + ALTER TABLE admin_settings ADD CONSTRAINT admin_settings_pkey PRIMARY KEY (id); + ALTER TABLE admin_settings ADD COLUMN created_time BIGINT; + UPDATE admin_settings SET created_time = extract_ts(id) WHERE id is not null; + RAISE NOTICE 'Table % column % updated!', table_name, column_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_id; + END IF; +END; +$$; + + +-- alarm +CREATE OR REPLACE PROCEDURE update_alarm() + LANGUAGE plpgsql AS +$$ +DECLARE + data_type varchar; + table_name varchar := 'alarm'; + column_id varchar := 'id'; + column_originator_id varchar := 'originator_id'; + column_tenant_id varchar := 'tenant_id'; +BEGIN + data_type := get_column_type(table_name, column_id); + IF data_type = 'character varying' THEN + ALTER TABLE alarm DROP CONSTRAINT alarm_pkey; + PERFORM column_type_to_uuid(table_name, column_id); + ALTER TABLE alarm ADD COLUMN created_time BIGINT; + UPDATE alarm SET created_time = extract_ts(id) WHERE id is not null; + ALTER TABLE alarm ADD CONSTRAINT alarm_pkey PRIMARY KEY (id); + RAISE NOTICE 'Table % column % updated!', table_name, column_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_id; + END IF; + + data_type := get_column_type(table_name, column_originator_id); + IF data_type = 'character varying' THEN + PERFORM column_type_to_uuid(table_name, column_originator_id); + RAISE NOTICE 'Table % column % updated!', table_name, column_originator_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_originator_id; + END IF; + + data_type := get_column_type(table_name, column_tenant_id); + IF data_type = 'character varying' THEN + PERFORM column_type_to_uuid(table_name, column_tenant_id); + RAISE NOTICE 'Table % column % updated!', table_name, column_tenant_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_tenant_id; + END IF; +END; +$$; + + +-- asset +CREATE OR REPLACE PROCEDURE update_asset() + LANGUAGE plpgsql AS +$$ +DECLARE + data_type varchar; + table_name varchar := 'asset'; + column_id varchar := 'id'; + column_customer_id varchar := 'customer_id'; + column_tenant_id varchar := 'tenant_id'; +BEGIN + data_type := get_column_type(table_name, column_id); + IF data_type = 'character varying' THEN + ALTER TABLE asset DROP CONSTRAINT asset_pkey; + PERFORM column_type_to_uuid(table_name, column_id); + ALTER TABLE asset ADD COLUMN created_time BIGINT; + UPDATE asset SET created_time = extract_ts(id) WHERE id is not null; + ALTER TABLE asset ADD CONSTRAINT asset_pkey PRIMARY KEY (id); + RAISE NOTICE 'Table % column % updated!', table_name, column_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_id; + END IF; + + data_type := get_column_type(table_name, column_customer_id); + IF data_type = 'character varying' THEN + PERFORM column_type_to_uuid(table_name, column_customer_id); + RAISE NOTICE 'Table % column % updated!', table_name, column_customer_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_customer_id; + END IF; + + data_type := get_column_type(table_name, column_tenant_id); + IF data_type = 'character varying' THEN + ALTER TABLE asset DROP CONSTRAINT asset_name_unq_key; + PERFORM column_type_to_uuid(table_name, column_tenant_id); + ALTER TABLE asset ADD CONSTRAINT asset_name_unq_key UNIQUE (tenant_id, name); + RAISE NOTICE 'Table % column % updated!', table_name, column_tenant_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_tenant_id; + END IF; + END; +$$; + +-- attribute_kv +CREATE OR REPLACE PROCEDURE update_attribute_kv() + LANGUAGE plpgsql AS +$$ +DECLARE + data_type varchar; + table_name varchar := 'attribute_kv'; + column_entity_id varchar := 'entity_id'; +BEGIN + data_type := get_column_type(table_name, column_entity_id); + IF data_type = 'character varying' THEN + ALTER TABLE attribute_kv DROP CONSTRAINT attribute_kv_pkey; + PERFORM column_type_to_uuid(table_name, column_entity_id); + ALTER TABLE attribute_kv ADD CONSTRAINT attribute_kv_pkey PRIMARY KEY (entity_type, entity_id, attribute_type, attribute_key); + RAISE NOTICE 'Table % column % updated!', table_name, column_entity_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_entity_id; + END IF; +END; +$$; + +-- audit_log +CREATE OR REPLACE PROCEDURE update_audit_log() + LANGUAGE plpgsql AS +$$ +DECLARE + data_type varchar; + table_name varchar := 'audit_log'; + column_id varchar := 'id'; + column_customer_id varchar := 'customer_id'; + column_tenant_id varchar := 'tenant_id'; + column_entity_id varchar := 'entity_id'; + column_user_id varchar := 'user_id'; +BEGIN + data_type := get_column_type(table_name, column_id); + IF data_type = 'character varying' THEN + ALTER TABLE audit_log DROP CONSTRAINT audit_log_pkey; + PERFORM column_type_to_uuid(table_name, column_id); + ALTER TABLE audit_log ADD COLUMN created_time BIGINT; + UPDATE audit_log SET created_time = extract_ts(id) WHERE id is not null; + ALTER TABLE audit_log ADD CONSTRAINT audit_log_pkey PRIMARY KEY (id); + RAISE NOTICE 'Table % column % updated!', table_name, column_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_id; + END IF; + + data_type := get_column_type(table_name, column_customer_id); + IF data_type = 'character varying' THEN + PERFORM column_type_to_uuid(table_name, column_customer_id); + RAISE NOTICE 'Table % column % updated!', table_name, column_customer_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_customer_id; + END IF; + + data_type := get_column_type(table_name, column_tenant_id); + IF data_type = 'character varying' THEN + PERFORM column_type_to_uuid(table_name, column_tenant_id); + RAISE NOTICE 'Table % column % updated!', table_name, column_tenant_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_tenant_id; + END IF; + + data_type := get_column_type(table_name, column_entity_id); + IF data_type = 'character varying' THEN + PERFORM column_type_to_uuid(table_name, column_entity_id); + RAISE NOTICE 'Table % column % updated!', table_name, column_entity_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_entity_id; + END IF; + + data_type := get_column_type(table_name, column_user_id); + IF data_type = 'character varying' THEN + PERFORM column_type_to_uuid(table_name, column_user_id); + RAISE NOTICE 'Table % column % updated!', table_name, column_user_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_user_id; + END IF; +END; +$$; + + +-- component_descriptor +CREATE OR REPLACE PROCEDURE update_component_descriptor() + LANGUAGE plpgsql AS +$$ +DECLARE + data_type varchar; + table_name varchar := 'component_descriptor'; + column_id varchar := 'id'; +BEGIN + data_type := get_column_type(table_name, column_id); + IF data_type = 'character varying' THEN + ALTER TABLE component_descriptor DROP CONSTRAINT component_descriptor_pkey; + PERFORM column_type_to_uuid(table_name, column_id); + ALTER TABLE component_descriptor ADD CONSTRAINT component_descriptor_pkey PRIMARY KEY (id); + ALTER TABLE component_descriptor ADD COLUMN created_time BIGINT; + UPDATE component_descriptor SET created_time = extract_ts(id) WHERE id is not null; + RAISE NOTICE 'Table % column % updated!', table_name, column_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_id; + END IF; +END; +$$; + +-- customer +CREATE OR REPLACE PROCEDURE update_customer() + LANGUAGE plpgsql AS +$$ +DECLARE + data_type varchar; + table_name varchar := 'customer'; + column_id varchar := 'id'; + column_tenant_id varchar := 'tenant_id'; +BEGIN + data_type := get_column_type(table_name, column_id); + IF data_type = 'character varying' THEN + ALTER TABLE customer DROP CONSTRAINT customer_pkey; + PERFORM column_type_to_uuid(table_name, column_id); + ALTER TABLE customer ADD CONSTRAINT customer_pkey PRIMARY KEY (id); + ALTER TABLE customer ADD COLUMN created_time BIGINT; + UPDATE customer SET created_time = extract_ts(id) WHERE id is not null; + RAISE NOTICE 'Table % column % updated!', table_name, column_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_id; + END IF; + + data_type := get_column_type(table_name, column_tenant_id); + IF data_type = 'character varying' THEN + PERFORM column_type_to_uuid(table_name, column_tenant_id); + RAISE NOTICE 'Table % column % updated!', table_name, column_tenant_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_tenant_id; + END IF; +END; +$$; + + +-- dashboard +CREATE OR REPLACE PROCEDURE update_dashboard() + LANGUAGE plpgsql AS +$$ +DECLARE + data_type varchar; + table_name varchar := 'dashboard'; + column_id varchar := 'id'; + column_tenant_id varchar := 'tenant_id'; +BEGIN + data_type := get_column_type(table_name, column_id); + IF data_type = 'character varying' THEN + ALTER TABLE dashboard DROP CONSTRAINT dashboard_pkey; + PERFORM column_type_to_uuid(table_name, column_id); + ALTER TABLE dashboard ADD CONSTRAINT dashboard_pkey PRIMARY KEY (id); + ALTER TABLE dashboard ADD COLUMN created_time BIGINT; + UPDATE dashboard SET created_time = extract_ts(id) WHERE id is not null; + RAISE NOTICE 'Table % column % updated!', table_name, column_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_id; + END IF; + + data_type := get_column_type(table_name, column_tenant_id); + IF data_type = 'character varying' THEN + PERFORM column_type_to_uuid(table_name, column_tenant_id); + RAISE NOTICE 'Table % column % updated!', table_name, column_tenant_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_tenant_id; + END IF; +END; +$$; + +-- device +CREATE OR REPLACE PROCEDURE update_device() + LANGUAGE plpgsql AS +$$ +DECLARE + data_type varchar; + table_name varchar := 'device'; + column_id varchar := 'id'; + column_customer_id varchar := 'customer_id'; + column_tenant_id varchar := 'tenant_id'; +BEGIN + data_type := get_column_type(table_name, column_id); + IF data_type = 'character varying' THEN + ALTER TABLE device DROP CONSTRAINT device_pkey; + PERFORM column_type_to_uuid(table_name, column_id); + ALTER TABLE device ADD COLUMN created_time BIGINT; + UPDATE device SET created_time = extract_ts(id) WHERE id is not null; + ALTER TABLE device ADD CONSTRAINT device_pkey PRIMARY KEY (id); + RAISE NOTICE 'Table % column % updated!', table_name, column_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_id; + END IF; + + data_type := get_column_type(table_name, column_customer_id); + IF data_type = 'character varying' THEN + PERFORM column_type_to_uuid(table_name, column_customer_id); + RAISE NOTICE 'Table % column % updated!', table_name, column_customer_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_customer_id; + END IF; + + data_type := get_column_type(table_name, column_tenant_id); + IF data_type = 'character varying' THEN + ALTER TABLE device DROP CONSTRAINT device_name_unq_key; + PERFORM column_type_to_uuid(table_name, column_tenant_id); + ALTER TABLE device ADD CONSTRAINT device_name_unq_key UNIQUE (tenant_id, name); + RAISE NOTICE 'Table % column % updated!', table_name, column_tenant_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_tenant_id; + END IF; +END; +$$; + + +-- device_credentials +CREATE OR REPLACE PROCEDURE update_device_credentials() + LANGUAGE plpgsql AS +$$ +DECLARE + data_type varchar; + table_name varchar := 'device_credentials'; + column_id varchar := 'id'; + column_device_id varchar := 'device_id'; +BEGIN + data_type := get_column_type(table_name, column_id); + IF data_type = 'character varying' THEN + ALTER TABLE device_credentials DROP CONSTRAINT device_credentials_pkey; + PERFORM column_type_to_uuid(table_name, column_id); + ALTER TABLE device_credentials ADD COLUMN created_time BIGINT; + UPDATE device_credentials SET created_time = extract_ts(id) WHERE id is not null; + ALTER TABLE device_credentials ADD CONSTRAINT device_credentials_pkey PRIMARY KEY (id); + RAISE NOTICE 'Table % column % updated!', table_name, column_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_id; + END IF; + + data_type := get_column_type(table_name, column_device_id); + IF data_type = 'character varying' THEN + ALTER TABLE device_credentials DROP CONSTRAINT IF EXISTS device_credentials_device_id_unq_key; + PERFORM column_type_to_uuid(table_name, column_device_id); + -- remove duplicate credentials with same device_id + DELETE from device_credentials where id in ( + select dc.id + from ( + SELECT id, device_id, + ROW_NUMBER() OVER ( + PARTITION BY + device_id + ORDER BY + created_time DESC + ) row_num + FROM + device_credentials + ) as dc + WHERE dc.row_num > 1 + ); + ALTER TABLE device_credentials ADD CONSTRAINT device_credentials_device_id_unq_key UNIQUE (device_id); + RAISE NOTICE 'Table % column % updated!', table_name, column_device_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_device_id; + END IF; +END; +$$; + + +-- event +CREATE OR REPLACE PROCEDURE update_event() + LANGUAGE plpgsql AS +$$ +DECLARE + data_type varchar; + table_name varchar := 'event'; + column_id varchar := 'id'; + column_entity_id varchar := 'entity_id'; + column_tenant_id varchar := 'tenant_id'; +BEGIN + data_type := get_column_type(table_name, column_id); + IF data_type = 'character varying' THEN + ALTER TABLE event DROP CONSTRAINT event_pkey; + PERFORM column_type_to_uuid(table_name, column_id); + ALTER TABLE event ADD COLUMN created_time BIGINT; + UPDATE event SET created_time = extract_ts(id) WHERE id is not null; + ALTER TABLE event ADD CONSTRAINT event_pkey PRIMARY KEY (id); + RAISE NOTICE 'Table % column % updated!', table_name, column_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_id; + END IF; + + ALTER TABLE event DROP CONSTRAINT event_unq_key; + + data_type := get_column_type(table_name, column_entity_id); + IF data_type = 'character varying' THEN + PERFORM column_type_to_uuid(table_name, column_entity_id); + RAISE NOTICE 'Table % column % updated!', table_name, column_entity_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_entity_id; + END IF; + + data_type := get_column_type(table_name, column_tenant_id); + IF data_type = 'character varying' THEN + PERFORM column_type_to_uuid(table_name, column_tenant_id); + RAISE NOTICE 'Table % column % updated!', table_name, column_tenant_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_tenant_id; + END IF; + + ALTER TABLE event ADD CONSTRAINT event_unq_key UNIQUE (tenant_id, entity_type, entity_id, event_type, event_uid); +END; +$$; + + +-- relation +CREATE OR REPLACE PROCEDURE update_relation() + LANGUAGE plpgsql AS +$$ +DECLARE + data_type varchar; + table_name varchar := 'relation'; + column_from_id varchar := 'from_id'; + column_to_id varchar := 'to_id'; +BEGIN + ALTER TABLE relation DROP CONSTRAINT relation_pkey; + + data_type := get_column_type(table_name, column_from_id); + IF data_type = 'character varying' THEN + PERFORM column_type_to_uuid(table_name, column_from_id); + RAISE NOTICE 'Table % column % updated!', table_name, column_from_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_from_id; + END IF; + + data_type := get_column_type(table_name, column_to_id); + IF data_type = 'character varying' THEN + PERFORM column_type_to_uuid(table_name, column_to_id); + RAISE NOTICE 'Table % column % updated!', table_name, column_to_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_to_id; + END IF; + + ALTER TABLE relation ADD CONSTRAINT relation_pkey PRIMARY KEY (from_id, from_type, relation_type_group, relation_type, to_id, to_type); +END; +$$; + + +-- tb_user +CREATE OR REPLACE PROCEDURE update_tb_user() + LANGUAGE plpgsql AS +$$ +DECLARE + data_type varchar; + table_name varchar := 'tb_user'; + column_id varchar := 'id'; + column_customer_id varchar := 'customer_id'; + column_tenant_id varchar := 'tenant_id'; +BEGIN + data_type := get_column_type(table_name, column_id); + IF data_type = 'character varying' THEN + ALTER TABLE tb_user DROP CONSTRAINT tb_user_pkey; + PERFORM column_type_to_uuid(table_name, column_id); + ALTER TABLE tb_user ADD COLUMN created_time BIGINT; + UPDATE tb_user SET created_time = extract_ts(id) WHERE id is not null; + ALTER TABLE tb_user ADD CONSTRAINT tb_user_pkey PRIMARY KEY (id); + RAISE NOTICE 'Table % column % updated!', table_name, column_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_id; + END IF; + + data_type := get_column_type(table_name, column_customer_id); + IF data_type = 'character varying' THEN + PERFORM column_type_to_uuid(table_name, column_customer_id); + RAISE NOTICE 'Table % column % updated!', table_name, column_customer_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_customer_id; + END IF; + + data_type := get_column_type(table_name, column_tenant_id); + IF data_type = 'character varying' THEN + PERFORM column_type_to_uuid(table_name, column_tenant_id); + RAISE NOTICE 'Table % column % updated!', table_name, column_tenant_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_tenant_id; + END IF; +END; +$$; + + +-- tenant +CREATE OR REPLACE PROCEDURE update_tenant() + LANGUAGE plpgsql AS +$$ +DECLARE + data_type varchar; + table_name varchar := 'tenant'; + column_id varchar := 'id'; +BEGIN + data_type := get_column_type(table_name, column_id); + IF data_type = 'character varying' THEN + ALTER TABLE tenant DROP CONSTRAINT tenant_pkey; + PERFORM column_type_to_uuid(table_name, column_id); + ALTER TABLE tenant ADD COLUMN created_time BIGINT; + UPDATE tenant SET created_time = extract_ts(id) WHERE id is not null; + ALTER TABLE tenant ADD CONSTRAINT tenant_pkey PRIMARY KEY (id); + RAISE NOTICE 'Table % column % updated!', table_name, column_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_id; + END IF; +END; +$$; + + +-- user_credentials +CREATE OR REPLACE PROCEDURE update_user_credentials() + LANGUAGE plpgsql AS +$$ +DECLARE + data_type varchar; + table_name varchar := 'user_credentials'; + column_id varchar := 'id'; + column_user_id varchar := 'user_id'; +BEGIN + data_type := get_column_type(table_name, column_id); + IF data_type = 'character varying' THEN + ALTER TABLE user_credentials DROP CONSTRAINT user_credentials_pkey; + PERFORM column_type_to_uuid(table_name, column_id); + ALTER TABLE user_credentials ADD COLUMN created_time BIGINT; + UPDATE user_credentials SET created_time = extract_ts(id) WHERE id is not null; + ALTER TABLE user_credentials ADD CONSTRAINT user_credentials_pkey PRIMARY KEY (id); + RAISE NOTICE 'Table % column % updated!', table_name, column_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_id; + END IF; + + data_type := get_column_type(table_name, column_user_id); + IF data_type = 'character varying' THEN + ALTER TABLE user_credentials DROP CONSTRAINT user_credentials_user_id_key; + ALTER TABLE user_credentials RENAME COLUMN user_id TO old_user_id; + ALTER TABLE user_credentials ADD COLUMN user_id UUID UNIQUE; + UPDATE user_credentials SET user_id = to_uuid(old_user_id) WHERE old_user_id is not null; + ALTER TABLE user_credentials DROP COLUMN old_user_id; + RAISE NOTICE 'Table % column % updated!', table_name, column_user_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_user_id; + END IF; +END; +$$; + + +-- widget_type +CREATE OR REPLACE PROCEDURE update_widget_type() + LANGUAGE plpgsql AS +$$ +DECLARE + data_type varchar; + table_name varchar := 'widget_type'; + column_id varchar := 'id'; + column_tenant_id varchar := 'tenant_id'; +BEGIN + data_type := get_column_type(table_name, column_id); + IF data_type = 'character varying' THEN + ALTER TABLE widget_type DROP CONSTRAINT widget_type_pkey; + PERFORM column_type_to_uuid(table_name, column_id); + ALTER TABLE widget_type ADD COLUMN created_time BIGINT; + UPDATE widget_type SET created_time = extract_ts(id) WHERE id is not null; + ALTER TABLE widget_type ADD CONSTRAINT widget_type_pkey PRIMARY KEY (id); + RAISE NOTICE 'Table % column % updated!', table_name, column_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_id; + END IF; + + data_type := get_column_type(table_name, column_tenant_id); + IF data_type = 'character varying' THEN + PERFORM column_type_to_uuid(table_name, column_tenant_id); + RAISE NOTICE 'Table % column % updated!', table_name, column_tenant_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_tenant_id; + END IF; +END; +$$; + + +-- widgets_bundle +CREATE OR REPLACE PROCEDURE update_widgets_bundle() + LANGUAGE plpgsql AS +$$ +DECLARE + data_type varchar; + table_name varchar := 'widgets_bundle'; + column_id varchar := 'id'; + column_tenant_id varchar := 'tenant_id'; +BEGIN + data_type := get_column_type(table_name, column_id); + IF data_type = 'character varying' THEN + ALTER TABLE widgets_bundle DROP CONSTRAINT widgets_bundle_pkey; + PERFORM column_type_to_uuid(table_name, column_id); + ALTER TABLE widgets_bundle ADD COLUMN created_time BIGINT; + UPDATE widgets_bundle SET created_time = extract_ts(id) WHERE id is not null; + ALTER TABLE widgets_bundle ADD CONSTRAINT widgets_bundle_pkey PRIMARY KEY (id); + RAISE NOTICE 'Table % column % updated!', table_name, column_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_id; + END IF; + + data_type := get_column_type(table_name, column_tenant_id); + IF data_type = 'character varying' THEN + PERFORM column_type_to_uuid(table_name, column_tenant_id); + RAISE NOTICE 'Table % column % updated!', table_name, column_tenant_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_tenant_id; + END IF; +END; +$$; + + +-- rule_chain +CREATE OR REPLACE PROCEDURE update_rule_chain() + LANGUAGE plpgsql AS +$$ +DECLARE + data_type varchar; + table_name varchar := 'rule_chain'; + column_id varchar := 'id'; + column_first_rule_node_id varchar := 'first_rule_node_id'; + column_tenant_id varchar := 'tenant_id'; +BEGIN + data_type := get_column_type(table_name, column_id); + IF data_type = 'character varying' THEN + ALTER TABLE rule_chain DROP CONSTRAINT rule_chain_pkey; + PERFORM column_type_to_uuid(table_name, column_id); + ALTER TABLE rule_chain ADD COLUMN created_time BIGINT; + UPDATE rule_chain SET created_time = extract_ts(id) WHERE id is not null; + ALTER TABLE rule_chain ADD CONSTRAINT rule_chain_pkey PRIMARY KEY (id); + RAISE NOTICE 'Table % column % updated!', table_name, column_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_id; + END IF; + + data_type := get_column_type(table_name, column_first_rule_node_id); + IF data_type = 'character varying' THEN + PERFORM column_type_to_uuid(table_name, column_first_rule_node_id); + RAISE NOTICE 'Table % column % updated!', table_name, column_first_rule_node_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_first_rule_node_id; + END IF; + + data_type := get_column_type(table_name, column_tenant_id); + IF data_type = 'character varying' THEN + PERFORM column_type_to_uuid(table_name, column_tenant_id); + RAISE NOTICE 'Table % column % updated!', table_name, column_tenant_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_tenant_id; + END IF; +END; +$$; + + +-- rule_node +CREATE OR REPLACE PROCEDURE update_rule_node() + LANGUAGE plpgsql AS +$$ +DECLARE + data_type varchar; + table_name varchar := 'rule_node'; + column_id varchar := 'id'; + column_rule_chain_id varchar := 'rule_chain_id'; +BEGIN + data_type := get_column_type(table_name, column_id); + IF data_type = 'character varying' THEN + ALTER TABLE rule_node DROP CONSTRAINT rule_node_pkey; + PERFORM column_type_to_uuid(table_name, column_id); + ALTER TABLE rule_node ADD COLUMN created_time BIGINT; + UPDATE rule_node SET created_time = extract_ts(id) WHERE id is not null; + ALTER TABLE rule_node ADD CONSTRAINT rule_node_pkey PRIMARY KEY (id); + RAISE NOTICE 'Table % column % updated!', table_name, column_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_id; + END IF; + + data_type := get_column_type(table_name, column_rule_chain_id); + IF data_type = 'character varying' THEN + PERFORM column_type_to_uuid(table_name, column_rule_chain_id); + RAISE NOTICE 'Table % column % updated!', table_name, column_rule_chain_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_rule_chain_id; + END IF; +END; +$$; + + +-- entity_view +CREATE OR REPLACE PROCEDURE update_entity_view() + LANGUAGE plpgsql AS +$$ +DECLARE + data_type varchar; + table_name varchar := 'entity_view'; + column_id varchar := 'id'; + column_entity_id varchar := 'entity_id'; + column_tenant_id varchar := 'tenant_id'; + column_customer_id varchar := 'customer_id'; +BEGIN + data_type := get_column_type(table_name, column_id); + IF data_type = 'character varying' THEN + ALTER TABLE entity_view DROP CONSTRAINT entity_view_pkey; + PERFORM column_type_to_uuid(table_name, column_id); + ALTER TABLE entity_view ADD COLUMN created_time BIGINT; + UPDATE entity_view SET created_time = extract_ts(id) WHERE id is not null; + ALTER TABLE entity_view ADD CONSTRAINT entity_view_pkey PRIMARY KEY (id); + RAISE NOTICE 'Table % column % updated!', table_name, column_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_id; + END IF; + + data_type := get_column_type(table_name, column_entity_id); + IF data_type = 'character varying' THEN + PERFORM column_type_to_uuid(table_name, column_entity_id); + RAISE NOTICE 'Table % column % updated!', table_name, column_entity_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_entity_id; + END IF; + + data_type := get_column_type(table_name, column_tenant_id); + IF data_type = 'character varying' THEN + PERFORM column_type_to_uuid(table_name, column_tenant_id); + RAISE NOTICE 'Table % column % updated!', table_name, column_tenant_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_tenant_id; + END IF; + + data_type := get_column_type(table_name, column_customer_id); + IF data_type = 'character varying' THEN + PERFORM column_type_to_uuid(table_name, column_customer_id); + RAISE NOTICE 'Table % column % updated!', table_name, column_customer_id; + ELSE + RAISE NOTICE 'Table % column % already updated!', table_name, column_customer_id; + END IF; +END; +$$; + +CREATE TABLE IF NOT EXISTS ts_kv_latest +( + entity_id uuid NOT NULL, + key int NOT NULL, + ts bigint NOT NULL, + bool_v boolean, + str_v varchar(10000000), + long_v bigint, + dbl_v double precision, + json_v json, + CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key) +); + +CREATE TABLE IF NOT EXISTS ts_kv_dictionary +( + key varchar(255) NOT NULL, + key_id serial UNIQUE, + CONSTRAINT ts_key_id_pkey PRIMARY KEY (key) +); diff --git a/application/src/main/data/upgrade/3.1.0/schema_update.sql b/application/src/main/data/upgrade/3.1.0/schema_update.sql new file mode 100644 index 0000000000..e3b2cfb9a4 --- /dev/null +++ b/application/src/main/data/upgrade/3.1.0/schema_update.sql @@ -0,0 +1,17 @@ +-- +-- Copyright © 2016-2020 The Thingsboard Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +CREATE INDEX IF NOT EXISTS idx_alarm_tenant_alarm_type_created_time ON alarm(tenant_id, type, created_time DESC); diff --git a/application/src/main/data/upgrade/3.1.1/schema_update_after.sql b/application/src/main/data/upgrade/3.1.1/schema_update_after.sql new file mode 100644 index 0000000000..c8f9d2970e --- /dev/null +++ b/application/src/main/data/upgrade/3.1.1/schema_update_after.sql @@ -0,0 +1,28 @@ +-- +-- Copyright © 2016-2020 The Thingsboard Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +DROP PROCEDURE IF EXISTS update_tenant_profiles; +DROP PROCEDURE IF EXISTS update_device_profiles; + +ALTER TABLE tenant ALTER COLUMN tenant_profile_id SET NOT NULL; +ALTER TABLE tenant DROP CONSTRAINT IF EXISTS fk_tenant_profile; +ALTER TABLE tenant ADD CONSTRAINT fk_tenant_profile FOREIGN KEY (tenant_profile_id) REFERENCES tenant_profile(id); +ALTER TABLE tenant DROP COLUMN IF EXISTS isolated_tb_core; +ALTER TABLE tenant DROP COLUMN IF EXISTS isolated_tb_rule_engine; + +ALTER TABLE device ALTER COLUMN device_profile_id SET NOT NULL; +ALTER TABLE device DROP CONSTRAINT IF EXISTS fk_device_profile; +ALTER TABLE device ADD CONSTRAINT fk_device_profile FOREIGN KEY (device_profile_id) REFERENCES device_profile(id); diff --git a/application/src/main/data/upgrade/3.1.1/schema_update_before.sql b/application/src/main/data/upgrade/3.1.1/schema_update_before.sql new file mode 100644 index 0000000000..5f30f135e1 --- /dev/null +++ b/application/src/main/data/upgrade/3.1.1/schema_update_before.sql @@ -0,0 +1,154 @@ +-- +-- Copyright © 2016-2020 The Thingsboard Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +CREATE TABLE IF NOT EXISTS oauth2_client_registration_info ( + id uuid NOT NULL CONSTRAINT oauth2_client_registration_info_pkey PRIMARY KEY, + enabled boolean, + created_time bigint NOT NULL, + additional_info varchar, + client_id varchar(255), + client_secret varchar(255), + authorization_uri varchar(255), + token_uri varchar(255), + scope varchar(255), + user_info_uri varchar(255), + user_name_attribute_name varchar(255), + jwk_set_uri varchar(255), + client_authentication_method varchar(255), + login_button_label varchar(255), + login_button_icon varchar(255), + allow_user_creation boolean, + activate_user boolean, + type varchar(31), + basic_email_attribute_key varchar(31), + basic_first_name_attribute_key varchar(31), + basic_last_name_attribute_key varchar(31), + basic_tenant_name_strategy varchar(31), + basic_tenant_name_pattern varchar(255), + basic_customer_name_pattern varchar(255), + basic_default_dashboard_name varchar(255), + basic_always_full_screen boolean, + custom_url varchar(255), + custom_username varchar(255), + custom_password varchar(255), + custom_send_token boolean +); + +CREATE TABLE IF NOT EXISTS oauth2_client_registration ( + id uuid NOT NULL CONSTRAINT oauth2_client_registration_pkey PRIMARY KEY, + created_time bigint NOT NULL, + domain_name varchar(255), + domain_scheme varchar(31), + client_registration_info_id uuid +); + +CREATE TABLE IF NOT EXISTS oauth2_client_registration_template ( + id uuid NOT NULL CONSTRAINT oauth2_client_registration_template_pkey PRIMARY KEY, + created_time bigint NOT NULL, + additional_info varchar, + provider_id varchar(255), + authorization_uri varchar(255), + token_uri varchar(255), + scope varchar(255), + user_info_uri varchar(255), + user_name_attribute_name varchar(255), + jwk_set_uri varchar(255), + client_authentication_method varchar(255), + type varchar(31), + basic_email_attribute_key varchar(31), + basic_first_name_attribute_key varchar(31), + basic_last_name_attribute_key varchar(31), + basic_tenant_name_strategy varchar(31), + basic_tenant_name_pattern varchar(255), + basic_customer_name_pattern varchar(255), + basic_default_dashboard_name varchar(255), + basic_always_full_screen boolean, + comment varchar, + login_button_icon varchar(255), + login_button_label varchar(255), + help_link varchar(255), + CONSTRAINT oauth2_template_provider_id_unq_key UNIQUE (provider_id) +); + +CREATE TABLE IF NOT EXISTS device_profile ( + id uuid NOT NULL CONSTRAINT device_profile_pkey PRIMARY KEY, + created_time bigint NOT NULL, + name varchar(255), + type varchar(255), + transport_type varchar(255), + provision_type varchar(255), + profile_data jsonb, + description varchar, + search_text varchar(255), + is_default boolean, + tenant_id uuid, + default_rule_chain_id uuid, + default_queue_name varchar(255), + provision_device_key varchar, + CONSTRAINT device_profile_name_unq_key UNIQUE (tenant_id, name), + CONSTRAINT device_provision_key_unq_key UNIQUE (provision_device_key), + CONSTRAINT fk_default_rule_chain_device_profile FOREIGN KEY (default_rule_chain_id) REFERENCES rule_chain(id) +); + +CREATE TABLE IF NOT EXISTS tenant_profile ( + id uuid NOT NULL CONSTRAINT tenant_profile_pkey PRIMARY KEY, + created_time bigint NOT NULL, + name varchar(255), + profile_data jsonb, + description varchar, + search_text varchar(255), + is_default boolean, + isolated_tb_core boolean, + isolated_tb_rule_engine boolean, + CONSTRAINT tenant_profile_name_unq_key UNIQUE (name) +); + +CREATE OR REPLACE PROCEDURE update_tenant_profiles() + LANGUAGE plpgsql AS +$$ +BEGIN + UPDATE tenant as t SET tenant_profile_id = p.id + FROM + (SELECT id from tenant_profile WHERE isolated_tb_core = false AND isolated_tb_rule_engine = false) as p + WHERE t.tenant_profile_id IS NULL AND t.isolated_tb_core = false AND t.isolated_tb_rule_engine = false; + + UPDATE tenant as t SET tenant_profile_id = p.id + FROM + (SELECT id from tenant_profile WHERE isolated_tb_core = true AND isolated_tb_rule_engine = false) as p + WHERE t.tenant_profile_id IS NULL AND t.isolated_tb_core = true AND t.isolated_tb_rule_engine = false; + + UPDATE tenant as t SET tenant_profile_id = p.id + FROM + (SELECT id from tenant_profile WHERE isolated_tb_core = false AND isolated_tb_rule_engine = true) as p + WHERE t.tenant_profile_id IS NULL AND t.isolated_tb_core = false AND t.isolated_tb_rule_engine = true; + + UPDATE tenant as t SET tenant_profile_id = p.id + FROM + (SELECT id from tenant_profile WHERE isolated_tb_core = true AND isolated_tb_rule_engine = true) as p + WHERE t.tenant_profile_id IS NULL AND t.isolated_tb_core = true AND t.isolated_tb_rule_engine = true; +END; +$$; + +CREATE OR REPLACE PROCEDURE update_device_profiles() + LANGUAGE plpgsql AS +$$ +BEGIN + UPDATE device as d SET device_profile_id = p.id, device_data = '{"configuration":{"type":"DEFAULT"}, "transportConfiguration":{"type":"DEFAULT"}}' + FROM + (SELECT id, tenant_id, name from device_profile) as p + WHERE d.device_profile_id IS NULL AND p.tenant_id = d.tenant_id AND d.type = p.name; +END; +$$; diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index 34a689de58..52dbb38c9c 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -44,7 +44,6 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.tools.TbRateLimits; -import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.audit.AuditLogService; @@ -58,17 +57,23 @@ import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.nosql.CassandraBufferedRateExecutor; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; +import org.thingsboard.server.dao.rule.RuleNodeStateService; +import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.usagestats.TbApiUsageClient; +import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.component.ComponentDiscoveryService; -import org.thingsboard.server.service.encoding.DataDecodingEncodingService; +import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.executors.ExternalCallExecutorService; import org.thingsboard.server.service.executors.SharedEventLoopGroupService; import org.thingsboard.server.service.mail.MailExecutorService; +import org.thingsboard.server.service.profile.TbDeviceProfileCache; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.service.queue.TbClusterService; import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService; import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService; @@ -76,6 +81,7 @@ import org.thingsboard.server.service.script.JsExecutorService; import org.thingsboard.server.service.script.JsInvokeService; import org.thingsboard.server.service.session.DeviceSessionCacheService; import org.thingsboard.server.service.state.DeviceStateService; +import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import org.thingsboard.server.service.transport.TbCoreToTransportService; @@ -88,7 +94,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; @Slf4j @Component @@ -102,6 +107,14 @@ public class ActorSystemContext { return debugPerTenantLimits; } + @Autowired + @Getter + private TbApiUsageStateService apiUsageStateService; + + @Autowired + @Getter + private TbApiUsageClient apiUsageClient; + @Autowired @Getter @Setter @@ -124,6 +137,14 @@ public class ActorSystemContext { @Getter private DeviceService deviceService; + @Autowired + @Getter + private TbTenantProfileCache tenantProfileCache; + + @Autowired + @Getter + private TbDeviceProfileCache deviceProfileCache; + @Autowired @Getter private AssetService assetService; @@ -136,6 +157,10 @@ public class ActorSystemContext { @Getter private TenantService tenantService; + @Autowired + @Getter + private TenantProfileService tenantProfileService; + @Autowired @Getter private CustomerService customerService; @@ -148,6 +173,10 @@ public class ActorSystemContext { @Getter private RuleChainService ruleChainService; + @Autowired + @Getter + private RuleNodeStateService ruleNodeStateService; + @Autowired private PartitionService partitionService; @@ -167,10 +196,6 @@ public class ActorSystemContext { @Getter private EventService eventService; - @Autowired - @Getter - private AlarmService alarmService; - @Autowired @Getter private RelationService relationService; @@ -187,6 +212,10 @@ public class ActorSystemContext { @Getter private TelemetrySubscriptionService tsSubService; + @Autowired + @Getter + private AlarmSubscriptionService alarmService; + @Autowired @Getter private JsInvokeService jsSandbox; @@ -524,6 +553,11 @@ public class ActorSystemContext { public void scheduleMsgWithDelay(TbActorRef ctx, TbActorMsg msg, long delayInMs) { log.debug("Scheduling msg {} with delay {} ms", msg, delayInMs); - getScheduler().schedule(() -> ctx.tell(msg), delayInMs, TimeUnit.MILLISECONDS); + if (delayInMs > 0) { + getScheduler().schedule(() -> ctx.tell(msg), delayInMs, TimeUnit.MILLISECONDS); + } else { + ctx.tell(msg); + } } + } diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java index 953188f3f7..ba8654854c 100644 --- a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java @@ -27,6 +27,7 @@ import org.thingsboard.server.actors.service.DefaultActorService; import org.thingsboard.server.actors.tenant.TenantActor; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageDataIterable; @@ -38,8 +39,8 @@ import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.ServiceType; -import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper; import java.util.HashSet; @@ -49,13 +50,14 @@ import java.util.Set; @Slf4j public class AppActor extends ContextAwareActor { - private static final TenantId SYSTEM_TENANT = new TenantId(ModelConstants.NULL_UUID); + private final TbTenantProfileCache tenantProfileCache; private final TenantService tenantService; private final Set deletedTenants; private boolean ruleChainsInitialized; private AppActor(ActorSystemContext systemContext) { super(systemContext); + this.tenantProfileCache = systemContext.getTenantProfileCache(); this.tenantService = systemContext.getTenantService(); this.deletedTenants = new HashSet<>(); } @@ -116,7 +118,8 @@ public class AppActor extends ContextAwareActor { boolean isRuleEngine = systemContext.getServiceInfoProvider().isService(ServiceType.TB_RULE_ENGINE); boolean isCore = systemContext.getServiceInfoProvider().isService(ServiceType.TB_CORE); for (Tenant tenant : tenantIterator) { - if (isCore || (isRuleEngine && !tenant.isIsolatedTbRuleEngine())) { + TenantProfile tenantProfile = tenantProfileCache.get(tenant.getTenantProfileId()); + if (isCore || (isRuleEngine && !tenantProfile.isIsolatedTbRuleEngine())) { log.debug("[{}] Creating tenant actor", tenant.getId()); getOrCreateTenantActor(tenant.getId()); log.debug("[{}] Tenant actor created.", tenant.getId()); @@ -130,7 +133,7 @@ public class AppActor extends ContextAwareActor { } private void onQueueToRuleEngineMsg(QueueToRuleEngineMsg msg) { - if (SYSTEM_TENANT.equals(msg.getTenantId())) { + if (TenantId.SYS_TENANT_ID.equals(msg.getTenantId())) { msg.getTbMsg().getCallback().onFailure(new RuleEngineException("Message has system tenant id!")); } else { if (!deletedTenants.contains(msg.getTenantId())) { @@ -143,15 +146,20 @@ public class AppActor extends ContextAwareActor { private void onComponentLifecycleMsg(ComponentLifecycleMsg msg) { TbActorRef target = null; - if (SYSTEM_TENANT.equals(msg.getTenantId())) { - log.warn("Message has system tenant id: {}", msg); + if (TenantId.SYS_TENANT_ID.equals(msg.getTenantId())) { + if (!EntityType.TENANT_PROFILE.equals(msg.getEntityId().getEntityType())) { + log.warn("Message has system tenant id: {}", msg); + } } else { - if (msg.getEntityId().getEntityType() == EntityType.TENANT - && msg.getEvent() == ComponentLifecycleEvent.DELETED) { - log.info("[{}] Handling tenant deleted notification: {}", msg.getTenantId(), msg); + if (EntityType.TENANT.equals(msg.getEntityId().getEntityType())) { TenantId tenantId = new TenantId(msg.getEntityId().getId()); - deletedTenants.add(tenantId); - ctx.stop(new TbEntityActorId(tenantId)); + if (msg.getEvent() == ComponentLifecycleEvent.DELETED) { + log.info("[{}] Handling tenant deleted notification: {}", msg.getTenantId(), msg); + deletedTenants.add(tenantId); + ctx.stop(new TbEntityActorId(tenantId)); + } else { + target = getOrCreateTenantActor(msg.getTenantId()); + } } else { target = getOrCreateTenantActor(msg.getTenantId()); } diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 8c53a6c02c..6749ed69ed 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.actors.ruleChain; -import com.datastax.driver.core.ResultSetFuture; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.netty.channel.EventLoopGroup; @@ -24,6 +23,8 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.util.StringUtils; import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.api.MailService; +import org.thingsboard.rule.engine.api.RuleEngineAlarmService; +import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; import org.thingsboard.rule.engine.api.RuleEngineRpcService; import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; import org.thingsboard.rule.engine.api.ScriptEngine; @@ -34,20 +35,24 @@ import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.ServiceQueue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.cassandra.CassandraCluster; @@ -56,6 +61,7 @@ import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.nosql.CassandraStatementTask; +import org.thingsboard.server.dao.nosql.TbResultSetFuture; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantService; @@ -68,6 +74,7 @@ import org.thingsboard.server.service.script.RuleNodeJsScriptEngine; import java.util.Collections; import java.util.Set; +import java.util.function.BiConsumer; import java.util.function.Consumer; /** @@ -307,7 +314,7 @@ class DefaultTbContext implements TbContext { @Override public ScriptEngine createJsScriptEngine(String script, String... argNames) { - return new RuleNodeJsScriptEngine(mainCtx.getJsSandbox(), nodeCtx.getSelf().getId(), script, argNames); + return new RuleNodeJsScriptEngine(getTenantId(), mainCtx.getJsSandbox(), nodeCtx.getSelf().getId(), script, argNames); } @Override @@ -372,7 +379,7 @@ class DefaultTbContext implements TbContext { } @Override - public AlarmService getAlarmService() { + public RuleEngineAlarmService getAlarmService() { return mainCtx.getAlarmService(); } @@ -401,6 +408,11 @@ class DefaultTbContext implements TbContext { return mainCtx.getEntityViewService(); } + @Override + public RuleEngineDeviceProfileCache getDeviceProfileCache() { + return mainCtx.getDeviceProfileCache(); + } + @Override public EventLoopGroup getSharedEventLoop() { return mainCtx.getSharedEventLoopGroupService().getSharedEventLoopGroup(); @@ -426,7 +438,7 @@ class DefaultTbContext implements TbContext { } @Override - public ResultSetFuture submitCassandraTask(CassandraStatementTask task) { + public TbResultSetFuture submitCassandraTask(CassandraStatementTask task) { return mainCtx.getCassandraBufferedRateExecutor().submit(task); } @@ -435,6 +447,56 @@ class DefaultTbContext implements TbContext { return mainCtx.getRedisTemplate(); } + @Override + public PageData findRuleNodeStates(PageLink pageLink) { + if (log.isDebugEnabled()) { + log.debug("[{}][{}] Fetch Rule Node States.", getTenantId(), getSelfId()); + } + return mainCtx.getRuleNodeStateService().findByRuleNodeId(getTenantId(), getSelfId(), pageLink); + } + + @Override + public RuleNodeState findRuleNodeStateForEntity(EntityId entityId) { + if (log.isDebugEnabled()) { + log.debug("[{}][{}][{}] Fetch Rule Node State for entity.", getTenantId(), getSelfId(), entityId); + } + return mainCtx.getRuleNodeStateService().findByRuleNodeIdAndEntityId(getTenantId(), getSelfId(), entityId); + } + + @Override + public RuleNodeState saveRuleNodeState(RuleNodeState state) { + if (log.isDebugEnabled()) { + log.debug("[{}][{}][{}] Persist Rule Node State for entity: {}", getTenantId(), getSelfId(), state.getEntityId(), state.getStateData()); + } + state.setRuleNodeId(getSelfId()); + return mainCtx.getRuleNodeStateService().save(getTenantId(), state); + } + + @Override + public void clearRuleNodeStates() { + if (log.isDebugEnabled()) { + log.debug("[{}][{}] Going to clear rule node states", getTenantId(), getSelfId()); + } + mainCtx.getRuleNodeStateService().removeByRuleNodeId(getTenantId(), getSelfId()); + } + + @Override + public void removeRuleNodeStateForEntity(EntityId entityId) { + if (log.isDebugEnabled()) { + log.debug("[{}][{}][{}] Remove Rule Node State for entity.", getTenantId(), getSelfId(), entityId); + } + mainCtx.getRuleNodeStateService().removeByRuleNodeIdAndEntityId(getTenantId(), getSelfId(), entityId); + } + + @Override + public void addDeviceProfileListeners(Consumer profileListener, BiConsumer deviceListener) { + mainCtx.getDeviceProfileCache().addListener(getTenantId(), getSelfId(), profileListener, deviceListener); + } + + @Override + public void removeProfileListener() { + mainCtx.getDeviceProfileCache().removeListener(getTenantId(), getSelfId()); + } private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) { TbMsgMetaData metaData = new TbMsgMetaData(); diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java index 5ca394be35..ad5f97ab48 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java @@ -23,6 +23,7 @@ import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.actors.TbEntityActorId; import org.thingsboard.server.actors.service.DefaultActorService; import org.thingsboard.server.actors.shared.ComponentMsgProcessor; +import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; @@ -46,6 +47,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.common.MultipleTbQueueTbMsgCallbackWrapper; import org.thingsboard.server.queue.common.TbQueueTbMsgCallbackWrapper; +import org.thingsboard.server.queue.usagestats.TbApiUsageClient; import org.thingsboard.server.service.queue.TbClusterService; import java.util.ArrayList; @@ -68,15 +70,16 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor> nodeRoutes; private final RuleChainService service; private final TbClusterService clusterService; + private final TbApiUsageClient apiUsageClient; private String ruleChainName; private RuleNodeId firstId; private RuleNodeCtx firstNode; private boolean started; - RuleChainActorMessageProcessor(TenantId tenantId, RuleChain ruleChain, ActorSystemContext systemContext - , TbActorRef parent, TbActorRef self) { + RuleChainActorMessageProcessor(TenantId tenantId, RuleChain ruleChain, ActorSystemContext systemContext, TbActorRef parent, TbActorRef self) { super(systemContext, tenantId, ruleChain.getId()); + this.apiUsageClient = systemContext.getApiUsageClient(); this.ruleChainName = ruleChain.getName(); this.parent = parent; this.self = self; diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java index eb46d7c098..0f828dfeb4 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java @@ -64,6 +64,12 @@ public abstract class RuleChainManagerActor extends ContextAwareActor { } } + protected void destroyRuleChains() { + for (RuleChain ruleChain : new PageDataIterable<>(link -> ruleChainService.findTenantRuleChains(tenantId, link), ContextAwareActor.ENTITY_PACK_LIMIT)) { + ctx.stop(new TbEntityActorId(ruleChain.getId())); + } + } + protected void visit(RuleChain entity, TbActorRef actorRef) { if (entity != null && entity.isRoot()) { rootChain = entity; diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java index db55ff8edf..95d21090bd 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java @@ -21,13 +21,18 @@ import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.actors.shared.ComponentMsgProcessor; +import org.thingsboard.server.common.data.ApiUsageRecordKey; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentLifecycleState; import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileConfiguration; +import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.RuleNodeException; import org.thingsboard.server.common.msg.queue.RuleNodeInfo; +import org.thingsboard.server.queue.usagestats.TbApiUsageClient; /** * @author Andrew Shvayka @@ -36,6 +41,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor extends Abstract this.entityId = id; } + protected TenantProfileConfiguration getTenantProfileConfiguration() { + return systemContext.getTenantProfileCache().get(tenantId).getProfileData().getConfiguration(); + } + public abstract String getComponentName(); public abstract void start(TbActorCtx context) throws Exception; diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java index b069a5b198..72351a480a 100644 --- a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java @@ -29,8 +29,10 @@ import org.thingsboard.server.actors.device.DeviceActorCreator; import org.thingsboard.server.actors.ruleChain.RuleChainManagerActor; import org.thingsboard.server.actors.service.ContextBasedCreator; import org.thingsboard.server.actors.service.DefaultActorService; +import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; @@ -46,6 +48,7 @@ import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper; import java.util.List; import java.util.Optional; @@ -55,6 +58,7 @@ public class TenantActor extends RuleChainManagerActor { private boolean isRuleEngineForCurrentTenant; private boolean isCore; + private ApiUsageState apiUsageState; private TenantActor(ActorSystemContext systemContext, TenantId tenantId) { super(systemContext, tenantId); @@ -72,17 +76,24 @@ public class TenantActor extends RuleChainManagerActor { cantFindTenant = true; log.info("[{}] Started tenant actor for missing tenant.", tenantId); } else { + apiUsageState = new ApiUsageState(systemContext.getApiUsageStateService().getApiUsageState(tenant.getId())); + // This Service may be started for specific tenant only. Optional isolatedTenantId = systemContext.getServiceInfoProvider().getIsolatedTenant(); - isRuleEngineForCurrentTenant = systemContext.getServiceInfoProvider().isService(ServiceType.TB_RULE_ENGINE); - isCore = systemContext.getServiceInfoProvider().isService(ServiceType.TB_CORE); + TenantProfile tenantProfile = systemContext.getTenantProfileCache().get(tenant.getTenantProfileId()); + isCore = systemContext.getServiceInfoProvider().isService(ServiceType.TB_CORE); + isRuleEngineForCurrentTenant = systemContext.getServiceInfoProvider().isService(ServiceType.TB_RULE_ENGINE); if (isRuleEngineForCurrentTenant) { try { - if (isolatedTenantId.map(id -> id.equals(tenantId)).orElseGet(() -> !tenant.isIsolatedTbRuleEngine())) { - log.info("[{}] Going to init rule chains", tenantId); - initRuleChains(); + if (isolatedTenantId.map(id -> id.equals(tenantId)).orElseGet(() -> !tenantProfile.isIsolatedTbRuleEngine())) { + if (apiUsageState.isReExecEnabled()) { + log.info("[{}] Going to init rule chains", tenantId); + initRuleChains(); + } else { + log.info("[{}] Skip init of the rule chains due to API limits", tenantId); + } } else { isRuleEngineForCurrentTenant = false; } @@ -94,8 +105,6 @@ public class TenantActor extends RuleChainManagerActor { } } catch (Exception e) { log.warn("[{}] Unknown failure", tenantId, e); -// TODO: throw this in 3.1? -// throw new TbActorException("Failed to init actor", e); } } @@ -111,6 +120,9 @@ public class TenantActor extends RuleChainManagerActor { if (msg.getMsgType().equals(MsgType.QUEUE_TO_RULE_ENGINE_MSG)) { QueueToRuleEngineMsg queueMsg = (QueueToRuleEngineMsg) msg; queueMsg.getTbMsg().getCallback().onSuccess(); + } else if (msg.getMsgType().equals(MsgType.TRANSPORT_TO_DEVICE_ACTOR_MSG)) { + TransportToDeviceActorMsgWrapper transportMsg = (TransportToDeviceActorMsgWrapper) msg; + transportMsg.getCallback().onSuccess(); } return true; } @@ -166,26 +178,33 @@ public class TenantActor extends RuleChainManagerActor { return; } TbMsg tbMsg = msg.getTbMsg(); - if (tbMsg.getRuleChainId() == null) { - if (getRootChainActor() != null) { - getRootChainActor().tell(msg); + if (apiUsageState.isReExecEnabled()) { + if (tbMsg.getRuleChainId() == null) { + if (getRootChainActor() != null) { + getRootChainActor().tell(msg); + } else { + tbMsg.getCallback().onFailure(new RuleEngineException("No Root Rule Chain available!")); + log.info("[{}] No Root Chain: {}", tenantId, msg); + } } else { - tbMsg.getCallback().onFailure(new RuleEngineException("No Root Rule Chain available!")); - log.info("[{}] No Root Chain: {}", tenantId, msg); + try { + ctx.tell(new TbEntityActorId(tbMsg.getRuleChainId()), msg); + } catch (TbActorNotRegisteredException ex) { + log.trace("Received message for non-existing rule chain: [{}]", tbMsg.getRuleChainId()); + //TODO: 3.1 Log it to dead letters queue; + tbMsg.getCallback().onSuccess(); + } } } else { - try { - ctx.tell(new TbEntityActorId(tbMsg.getRuleChainId()), msg); - } catch (TbActorNotRegisteredException ex) { - log.trace("Received message for non-existing rule chain: [{}]", tbMsg.getRuleChainId()); - //TODO: 3.1 Log it to dead letters queue; - tbMsg.getCallback().onSuccess(); - } + log.trace("[{}] Ack message because Rule Engine is disabled", tenantId); + tbMsg.getCallback().onSuccess(); } } private void onRuleChainMsg(RuleChainAwareMsg msg) { - getOrCreateActor(msg.getRuleChainId()).tell(msg); + if (apiUsageState.isReExecEnabled()) { + getOrCreateActor(msg.getRuleChainId()).tell(msg); + } } private void onToDeviceActorMsg(DeviceAwareMsg msg, boolean priority) { @@ -201,6 +220,17 @@ public class TenantActor extends RuleChainManagerActor { } private void onComponentLifecycleMsg(ComponentLifecycleMsg msg) { + if (msg.getEntityId().getEntityType().equals(EntityType.API_USAGE_STATE)) { + ApiUsageState old = apiUsageState; + apiUsageState = new ApiUsageState(systemContext.getApiUsageStateService().getApiUsageState(tenantId)); + if (old.isReExecEnabled() && !apiUsageState.isReExecEnabled()) { + log.info("[{}] Received API state update. Going to DISABLE Rule Engine execution.", tenantId); + destroyRuleChains(); + } else if (!old.isReExecEnabled() && apiUsageState.isReExecEnabled()) { + log.info("[{}] Received API state update. Going to ENABLE Rule Engine execution.", tenantId); + initRuleChains(); + } + } if (isRuleEngineForCurrentTenant) { TbActorRef target = getEntityActorRef(msg.getEntityId()); if (target != null) { diff --git a/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java b/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java new file mode 100644 index 0000000000..de34db0eaf --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java @@ -0,0 +1,267 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.keygen.Base64StringKeyGenerator; +import org.springframework.security.crypto.keygen.StringKeyGenerator; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; +import org.springframework.security.oauth2.core.oidc.OidcScopes; +import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; +import org.springframework.security.web.util.UrlUtils; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; +import org.thingsboard.server.dao.oauth2.OAuth2Configuration; +import org.thingsboard.server.utils.MiscUtils; + +import javax.servlet.http.HttpServletRequest; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +@Service +@Slf4j +public class CustomOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver { + public static final String DEFAULT_AUTHORIZATION_REQUEST_BASE_URI = "/oauth2/authorization"; + public static final String DEFAULT_LOGIN_PROCESSING_URI = "/login/oauth2/code/"; + private static final String REGISTRATION_ID_URI_VARIABLE_NAME = "registrationId"; + private static final char PATH_DELIMITER = '/'; + + private final AntPathRequestMatcher authorizationRequestMatcher = new AntPathRequestMatcher( + DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/{" + REGISTRATION_ID_URI_VARIABLE_NAME + "}"); + private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder()); + private final StringKeyGenerator secureKeyGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96); + + @Autowired + private ClientRegistrationRepository clientRegistrationRepository; + + @Autowired(required = false) + private OAuth2Configuration oauth2Configuration; + + + @Override + public OAuth2AuthorizationRequest resolve(HttpServletRequest request) { + String registrationId = this.resolveRegistrationId(request); + String redirectUriAction = getAction(request, "login"); + return resolve(request, registrationId, redirectUriAction); + } + + @Override + public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId) { + if (registrationId == null) { + return null; + } + String redirectUriAction = getAction(request, "authorize"); + return resolve(request, registrationId, redirectUriAction); + } + + private String getAction(HttpServletRequest request, String defaultAction) { + String action = request.getParameter("action"); + if (action == null) { + return defaultAction; + } + return action; + } + + private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction) { + if (registrationId == null) { + return null; + } + + ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId); + if (clientRegistration == null) { + throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId); + } + + Map attributes = new HashMap<>(); + attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()); + + OAuth2AuthorizationRequest.Builder builder; + if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) { + builder = OAuth2AuthorizationRequest.authorizationCode(); + Map additionalParameters = new HashMap<>(); + if (!CollectionUtils.isEmpty(clientRegistration.getScopes()) && + clientRegistration.getScopes().contains(OidcScopes.OPENID)) { + // Section 3.1.2.1 Authentication Request - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest + // scope + // REQUIRED. OpenID Connect requests MUST contain the "openid" scope value. + addNonceParameters(attributes, additionalParameters); + } + if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) { + addPkceParameters(attributes, additionalParameters); + } + builder.additionalParameters(additionalParameters); + } else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) { + builder = OAuth2AuthorizationRequest.implicit(); + } else { + throw new IllegalArgumentException("Invalid Authorization Grant Type (" + + clientRegistration.getAuthorizationGrantType().getValue() + + ") for Client Registration with Id: " + clientRegistration.getRegistrationId()); + } + + String redirectUriStr = expandRedirectUri(request, clientRegistration, redirectUriAction); + + return builder + .clientId(clientRegistration.getClientId()) + .authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri()) + .redirectUri(redirectUriStr) + .scopes(clientRegistration.getScopes()) + .state(this.stateGenerator.generateKey()) + .attributes(attributes) + .build(); + } + + private String resolveRegistrationId(HttpServletRequest request) { + if (this.authorizationRequestMatcher.matches(request)) { + return this.authorizationRequestMatcher + .matcher(request).getVariables().get(REGISTRATION_ID_URI_VARIABLE_NAME); + } + return null; + } + + /** + * Expands the {@link ClientRegistration#getRedirectUriTemplate()} with following provided variables:
+ * - baseUrl (e.g. https://localhost/app)
+ * - baseScheme (e.g. https)
+ * - baseHost (e.g. localhost)
+ * - basePort (e.g. :8080)
+ * - basePath (e.g. /app)
+ * - registrationId (e.g. google)
+ * - action (e.g. login)
+ *

+ * Null variables are provided as empty strings. + *

+ * Default redirectUriTemplate is: {@link org.springframework.security.config.oauth2.client}.CommonOAuth2Provider#DEFAULT_REDIRECT_URL + * + * @return expanded URI + */ + private String expandRedirectUri(HttpServletRequest request, ClientRegistration clientRegistration, String action) { + Map uriVariables = new HashMap<>(); + uriVariables.put("registrationId", clientRegistration.getRegistrationId()); + + UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request)) + .replacePath(request.getContextPath()) + .replaceQuery(null) + .fragment(null) + .build(); + String scheme = uriComponents.getScheme(); + uriVariables.put("baseScheme", scheme == null ? "" : scheme); + String host = uriComponents.getHost(); + uriVariables.put("baseHost", host == null ? "" : host); + // following logic is based on HierarchicalUriComponents#toUriString() + int port = uriComponents.getPort(); + uriVariables.put("basePort", port == -1 ? "" : ":" + port); + String path = uriComponents.getPath(); + if (StringUtils.hasLength(path)) { + if (path.charAt(0) != PATH_DELIMITER) { + path = PATH_DELIMITER + path; + } + } + uriVariables.put("basePath", path == null ? "" : path); + uriVariables.put("baseUrl", uriComponents.toUriString()); + + uriVariables.put("action", action == null ? "" : action); + + String redirectUri = getRedirectUri(request); + log.trace("Redirect URI - {}.", redirectUri); + + return UriComponentsBuilder.fromUriString(redirectUri) + .buildAndExpand(uriVariables) + .toUriString(); + } + + private String getRedirectUri(HttpServletRequest request) { + String loginProcessingUri = oauth2Configuration != null ? oauth2Configuration.getLoginProcessingUrl() : DEFAULT_LOGIN_PROCESSING_URI; + + String scheme = MiscUtils.getScheme(request); + String domainName = MiscUtils.getDomainName(request); + int port = MiscUtils.getPort(request); + String baseUrl = scheme + "://" + domainName; + if (needsPort(scheme, port)){ + baseUrl += ":" + port; + } + return baseUrl + loginProcessingUri; + } + + private boolean needsPort(String scheme, int port) { + boolean isHttpDefault = "http".equals(scheme.toLowerCase()) && port == 80; + boolean isHttpsDefault = "https".equals(scheme.toLowerCase()) && port == 443; + return !isHttpDefault && !isHttpsDefault; + } + + /** + * Creates nonce and its hash for use in OpenID Connect 1.0 Authentication Requests. + * + * @param attributes where the {@link OidcParameterNames#NONCE} is stored for the authentication request + * @param additionalParameters where the {@link OidcParameterNames#NONCE} hash is added for the authentication request + * + * @since 5.2 + * @see 3.1.2.1. Authentication Request + */ + private void addNonceParameters(Map attributes, Map additionalParameters) { + try { + String nonce = this.secureKeyGenerator.generateKey(); + String nonceHash = createHash(nonce); + attributes.put(OidcParameterNames.NONCE, nonce); + additionalParameters.put(OidcParameterNames.NONCE, nonceHash); + } catch (NoSuchAlgorithmException e) { } + } + + /** + * Creates and adds additional PKCE parameters for use in the OAuth 2.0 Authorization and Access Token Requests + * + * @param attributes where {@link PkceParameterNames#CODE_VERIFIER} is stored for the token request + * @param additionalParameters where {@link PkceParameterNames#CODE_CHALLENGE} and, usually, + * {@link PkceParameterNames#CODE_CHALLENGE_METHOD} are added to be used in the authorization request. + * + * @since 5.2 + * @see 1.1. Protocol Flow + * @see 4.1. Client Creates a Code Verifier + * @see 4.2. Client Creates the Code Challenge + */ + private void addPkceParameters(Map attributes, Map additionalParameters) { + String codeVerifier = this.secureKeyGenerator.generateKey(); + attributes.put(PkceParameterNames.CODE_VERIFIER, codeVerifier); + try { + String codeChallenge = createHash(codeVerifier); + additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge); + additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256"); + } catch (NoSuchAlgorithmException e) { + additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeVerifier); + } + } + + private static String createHash(String value) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] digest = md.digest(value.getBytes(StandardCharsets.US_ASCII)); + return Base64.getUrlEncoder().withoutPadding().encodeToString(digest); + } +} diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java index 9b00c15a7e..aec5fc2d8f 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java @@ -15,28 +15,11 @@ */ package org.thingsboard.server.config; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.collections.ExtendedProperties; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.velocity.app.VelocityEngine; -import org.apache.velocity.exception.ResourceNotFoundException; -import org.apache.velocity.runtime.RuntimeConstants; -import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.context.support.ResourceBundleMessageSource; -import org.springframework.core.io.DefaultResourceLoader; -import org.springframework.core.io.Resource; -import org.springframework.core.io.ResourceLoader; -import org.springframework.util.StringUtils; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.util.Arrays; @Configuration public class ThingsboardMessageConfiguration { @@ -49,114 +32,4 @@ public class ThingsboardMessageConfiguration { messageSource.setDefaultEncoding("UTF-8"); return messageSource; } - - private static final String DEFAULT_RESOURCE_LOADER_PATH = "classpath:/templates/"; - - private ResourceLoader resourceLoader = new DefaultResourceLoader(); - - @Bean - public VelocityEngine velocityEngine() { - VelocityEngine velocityEngine = new VelocityEngine(); - try { - Resource resource = resourceLoader.getResource(DEFAULT_RESOURCE_LOADER_PATH); - File file = resource.getFile(); - velocityEngine.setProperty(RuntimeConstants.RESOURCE_LOADER, "file"); - velocityEngine.setProperty(RuntimeConstants.FILE_RESOURCE_LOADER_CACHE, "true"); - velocityEngine.setProperty(RuntimeConstants.FILE_RESOURCE_LOADER_PATH, file.getAbsolutePath()); - } catch (IOException e) { - initSpringResourceLoader(velocityEngine, DEFAULT_RESOURCE_LOADER_PATH); - } - velocityEngine.init(); - return velocityEngine; - } - - private void initSpringResourceLoader(VelocityEngine velocityEngine, String resourceLoaderPath) { - velocityEngine.setProperty( - RuntimeConstants.RESOURCE_LOADER, SpringResourceLoader.NAME); - velocityEngine.setProperty( - SpringResourceLoader.SPRING_RESOURCE_LOADER_CLASS, SpringResourceLoader.class.getName()); - velocityEngine.setProperty( - SpringResourceLoader.SPRING_RESOURCE_LOADER_CACHE, "true"); - velocityEngine.setApplicationAttribute( - SpringResourceLoader.SPRING_RESOURCE_LOADER, resourceLoader); - velocityEngine.setApplicationAttribute( - SpringResourceLoader.SPRING_RESOURCE_LOADER_PATH, resourceLoaderPath); - } - - @Slf4j - public static class SpringResourceLoader extends org.apache.velocity.runtime.resource.loader.ResourceLoader { - - public static final String NAME = "spring"; - - public static final String SPRING_RESOURCE_LOADER_CLASS = "spring.resource.loader.class"; - - public static final String SPRING_RESOURCE_LOADER_CACHE = "spring.resource.loader.cache"; - - public static final String SPRING_RESOURCE_LOADER = "spring.resource.loader"; - - public static final String SPRING_RESOURCE_LOADER_PATH = "spring.resource.loader.path"; - - private org.springframework.core.io.ResourceLoader resourceLoader; - - private String[] resourceLoaderPaths; - - - @Override - public void init(ExtendedProperties configuration) { - this.resourceLoader = (org.springframework.core.io.ResourceLoader) - this.rsvc.getApplicationAttribute(SPRING_RESOURCE_LOADER); - String resourceLoaderPath = (String) this.rsvc.getApplicationAttribute(SPRING_RESOURCE_LOADER_PATH); - if (this.resourceLoader == null) { - throw new IllegalArgumentException( - "'resourceLoader' application attribute must be present for SpringResourceLoader"); - } - if (resourceLoaderPath == null) { - throw new IllegalArgumentException( - "'resourceLoaderPath' application attribute must be present for SpringResourceLoader"); - } - this.resourceLoaderPaths = StringUtils.commaDelimitedListToStringArray(resourceLoaderPath); - for (int i = 0; i < this.resourceLoaderPaths.length; i++) { - String path = this.resourceLoaderPaths[i]; - if (!path.endsWith("/")) { - this.resourceLoaderPaths[i] = path + "/"; - } - } - if (log.isInfoEnabled()) { - log.info("SpringResourceLoader for Velocity: using resource loader [" + this.resourceLoader + - "] and resource loader paths " + Arrays.asList(this.resourceLoaderPaths)); - } - } - - @Override - public InputStream getResourceStream(String source) throws ResourceNotFoundException { - if (log.isDebugEnabled()) { - log.debug("Looking for Velocity resource with name [" + source + "]"); - } - for (String resourceLoaderPath : this.resourceLoaderPaths) { - org.springframework.core.io.Resource resource = - this.resourceLoader.getResource(resourceLoaderPath + source); - try { - return resource.getInputStream(); - } - catch (IOException ex) { - if (log.isDebugEnabled()) { - log.debug("Could not find Velocity resource: " + resource); - } - } - } - throw new ResourceNotFoundException( - "Could not find resource [" + source + "] in Spring resource loader path"); - } - - @Override - public boolean isSourceModified(org.apache.velocity.runtime.resource.Resource resource) { - return false; - } - - @Override - public long getLastModified(org.apache.velocity.runtime.resource.Resource resource) { - return 0; - } - - } } diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java index 683345fe7f..4894ecd6ae 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -32,6 +32,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @@ -69,7 +70,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login"; public static final String PUBLIC_LOGIN_ENTRY_POINT = "/api/auth/login/public"; public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token"; - protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/static/**", "/api/noauth/**", "/webjars/**"}; + protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/assets/**", "/static/**", "/api/noauth/**", "/webjars/**"}; public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**"; public static final String WS_TOKEN_BASED_AUTH_ENTRY_POINT = "/api/ws/**"; @@ -172,9 +173,12 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt @Override public void configure(WebSecurity web) throws Exception { - web.ignoring().antMatchers("/static/**"); + web.ignoring().antMatchers("/*.js","/*.css","/*.ico","/assets/**","/static/**"); } + @Autowired + private OAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver; + @Override protected void configure(HttpSecurity http) throws Exception { http.headers().cacheControl().and().frameOptions().disable() @@ -207,8 +211,10 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class); - if (oauth2Configuration != null && oauth2Configuration.isEnabled()) { + if (oauth2Configuration != null) { http.oauth2Login() + .authorizationEndpoint().authorizationRequestResolver(oAuth2AuthorizationRequestResolver) + .and() .loginPage("/oauth2Login") .loginProcessingUrl(oauth2Configuration.getLoginProcessingUrl()) .successHandler(oauth2AuthenticationSuccessHandler) diff --git a/application/src/main/java/org/thingsboard/server/config/WebConfig.java b/application/src/main/java/org/thingsboard/server/config/WebConfig.java index a1b8af692d..78e55374af 100644 --- a/application/src/main/java/org/thingsboard/server/config/WebConfig.java +++ b/application/src/main/java/org/thingsboard/server/config/WebConfig.java @@ -21,7 +21,7 @@ import org.springframework.web.bind.annotation.RequestMapping; @Controller public class WebConfig { - @RequestMapping(value = "/{path:^(?!api$)(?!static$)(?!webjars$)[^\\.]*}/**") + @RequestMapping(value = "/{path:^(?!api$)(?!assets$)(?!static$)(?!webjars$)[^\\.]*}/**") public String redirect() { return "forward:/index.html"; } diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java index 6f3350143b..47aa4aa550 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java @@ -39,12 +39,14 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; -import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.util.UUID; + @RestController @TbCoreComponent @RequestMapping("/api") @@ -85,10 +87,10 @@ public class AlarmController extends BaseController { try { alarm.setTenantId(getCurrentUser().getTenantId()); - checkEntity(alarm.getId(), alarm, Resource.ALARM); + checkEntity(alarm.getId(), alarm, Resource.ALARM); Alarm savedAlarm = checkNotNull(alarmService.createOrUpdateAlarm(alarm)); - logEntityAction(savedAlarm.getId(), savedAlarm, + logEntityAction(savedAlarm.getOriginator(), savedAlarm, getCurrentUser().getCustomerId(), alarm.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); return savedAlarm; @@ -125,7 +127,7 @@ public class AlarmController extends BaseController { alarmService.ackAlarm(getCurrentUser().getTenantId(), alarmId, ackTs).get(); alarm.setAckTs(ackTs); alarm.setStatus(alarm.getStatus().isCleared() ? AlarmStatus.CLEARED_ACK : AlarmStatus.ACTIVE_ACK); - logEntityAction(alarmId, alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_ACK, null); + logEntityAction(alarm.getOriginator(), alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_ACK, null); } catch (Exception e) { throw handleException(e); } @@ -143,7 +145,7 @@ public class AlarmController extends BaseController { alarmService.clearAlarm(getCurrentUser().getTenantId(), alarmId, null, clearTs).get(); alarm.setClearTs(clearTs); alarm.setStatus(alarm.getStatus().isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK); - logEntityAction(alarmId, alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_CLEAR, null); + logEntityAction(alarm.getOriginator(), alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_CLEAR, null); } catch (Exception e) { throw handleException(e); } @@ -152,15 +154,18 @@ public class AlarmController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/alarm/{entityType}/{entityId}", method = RequestMethod.GET) @ResponseBody - public TimePageData getAlarms( + public PageData getAlarms( @PathVariable("entityType") String strEntityType, @PathVariable("entityId") String strEntityId, @RequestParam(required = false) String searchStatus, @RequestParam(required = false) String status, - @RequestParam int limit, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder, @RequestParam(required = false) Long startTime, @RequestParam(required = false) Long endTime, - @RequestParam(required = false, defaultValue = "false") boolean ascOrder, @RequestParam(required = false) String offset, @RequestParam(required = false) Boolean fetchOriginator ) throws ThingsboardException { @@ -174,9 +179,13 @@ public class AlarmController extends BaseController { "and 'status' can't be specified at the same time!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } checkEntityId(entityId, Operation.READ); + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); + UUID idOffsetUuid = null; + if (StringUtils.isNotEmpty(offset)) { + idOffsetUuid = toUUID(offset); + } try { - TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset); - return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(entityId, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator)).get()); + return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(entityId, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator, idOffsetUuid)).get()); } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java index 731eefc838..2c28feebf2 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java @@ -30,14 +30,16 @@ import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetSearchQuery; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -69,6 +71,19 @@ public class AssetController extends BaseController { } } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/asset/info/{assetId}", method = RequestMethod.GET) + @ResponseBody + public AssetInfo getAssetInfoById(@PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException { + checkParameter(ASSET_ID, strAssetId); + try { + AssetId assetId = new AssetId(toUUID(strAssetId)); + return checkAssetInfoId(assetId, Operation.READ); + } catch (Exception e) { + throw handleException(e); + } + } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/asset", method = RequestMethod.POST) @ResponseBody @@ -204,18 +219,19 @@ public class AssetController extends BaseController { } @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/assets", params = {"limit"}, method = RequestMethod.GET) + @RequestMapping(value = "/tenant/assets", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody - public TextPageData getTenantAssets( - @RequestParam int limit, + public PageData getTenantAssets( + @RequestParam int pageSize, + @RequestParam int page, @RequestParam(required = false) String type, @RequestParam(required = false) String textSearch, - @RequestParam(required = false) String idOffset, - @RequestParam(required = false) String textOffset) throws ThingsboardException { + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); - TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset); - if (type != null && type.trim().length() > 0) { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (type != null && type.trim().length()>0) { return checkNotNull(assetService.findAssetsByTenantIdAndType(tenantId, type, pageLink)); } else { return checkNotNull(assetService.findAssetsByTenantId(tenantId, pageLink)); @@ -225,6 +241,29 @@ public class AssetController extends BaseController { } } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/tenant/assetInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getTenantAssetInfos( + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String type, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + TenantId tenantId = getCurrentUser().getTenantId(); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (type != null && type.trim().length() > 0) { + return checkNotNull(assetService.findAssetInfosByTenantIdAndType(tenantId, type, pageLink)); + } else { + return checkNotNull(assetService.findAssetInfosByTenantId(tenantId, pageLink)); + } + } catch (Exception e) { + throw handleException(e); + } + } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/tenant/assets", params = {"assetName"}, method = RequestMethod.GET) @ResponseBody @@ -239,22 +278,23 @@ public class AssetController extends BaseController { } @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/customer/{customerId}/assets", params = {"limit"}, method = RequestMethod.GET) + @RequestMapping(value = "/customer/{customerId}/assets", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody - public TextPageData getCustomerAssets( + public PageData getCustomerAssets( @PathVariable("customerId") String strCustomerId, - @RequestParam int limit, + @RequestParam int pageSize, + @RequestParam int page, @RequestParam(required = false) String type, @RequestParam(required = false) String textSearch, - @RequestParam(required = false) String idOffset, - @RequestParam(required = false) String textOffset) throws ThingsboardException { + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("customerId", strCustomerId); try { TenantId tenantId = getCurrentUser().getTenantId(); CustomerId customerId = new CustomerId(toUUID(strCustomerId)); checkCustomerId(customerId, Operation.READ); - TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset); - if (type != null && type.trim().length() > 0) { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (type != null && type.trim().length()>0) { return checkNotNull(assetService.findAssetsByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink)); } else { return checkNotNull(assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink)); @@ -264,6 +304,33 @@ public class AssetController extends BaseController { } } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/customer/{customerId}/assetInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getCustomerAssetInfos( + @PathVariable("customerId") String strCustomerId, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String type, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + checkParameter("customerId", strCustomerId); + try { + TenantId tenantId = getCurrentUser().getTenantId(); + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + checkCustomerId(customerId, Operation.READ); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (type != null && type.trim().length() > 0) { + return checkNotNull(assetService.findAssetInfosByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink)); + } else { + return checkNotNull(assetService.findAssetInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink)); + } + } catch (Exception e) { + throw handleException(e); + } + } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/assets", params = {"assetIds"}, method = RequestMethod.GET) @ResponseBody diff --git a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java index c99ba8a018..7eb74fef90 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java @@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -45,20 +45,22 @@ import java.util.stream.Collectors; public class AuditLogController extends BaseController { @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/audit/logs/customer/{customerId}", params = {"limit"}, method = RequestMethod.GET) + @RequestMapping(value = "/audit/logs/customer/{customerId}", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody - public TimePageData getAuditLogsByCustomerId( + public PageData getAuditLogsByCustomerId( @PathVariable("customerId") String strCustomerId, - @RequestParam int limit, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder, @RequestParam(required = false) Long startTime, @RequestParam(required = false) Long endTime, - @RequestParam(required = false, defaultValue = "false") boolean ascOrder, - @RequestParam(required = false) String offset, @RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException { try { checkParameter("CustomerId", strCustomerId); TenantId tenantId = getCurrentUser().getTenantId(); - TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset); + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); List actionTypes = parseActionTypesStr(actionTypesStr); return checkNotNull(auditLogService.findAuditLogsByTenantIdAndCustomerId(tenantId, new CustomerId(UUID.fromString(strCustomerId)), actionTypes, pageLink)); } catch (Exception e) { @@ -67,20 +69,22 @@ public class AuditLogController extends BaseController { } @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/audit/logs/user/{userId}", params = {"limit"}, method = RequestMethod.GET) + @RequestMapping(value = "/audit/logs/user/{userId}", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody - public TimePageData getAuditLogsByUserId( + public PageData getAuditLogsByUserId( @PathVariable("userId") String strUserId, - @RequestParam int limit, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder, @RequestParam(required = false) Long startTime, @RequestParam(required = false) Long endTime, - @RequestParam(required = false, defaultValue = "false") boolean ascOrder, - @RequestParam(required = false) String offset, @RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException { try { checkParameter("UserId", strUserId); TenantId tenantId = getCurrentUser().getTenantId(); - TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset); + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); List actionTypes = parseActionTypesStr(actionTypesStr); return checkNotNull(auditLogService.findAuditLogsByTenantIdAndUserId(tenantId, new UserId(UUID.fromString(strUserId)), actionTypes, pageLink)); } catch (Exception e) { @@ -89,22 +93,24 @@ public class AuditLogController extends BaseController { } @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/audit/logs/entity/{entityType}/{entityId}", params = {"limit"}, method = RequestMethod.GET) + @RequestMapping(value = "/audit/logs/entity/{entityType}/{entityId}", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody - public TimePageData getAuditLogsByEntityId( + public PageData getAuditLogsByEntityId( @PathVariable("entityType") String strEntityType, @PathVariable("entityId") String strEntityId, - @RequestParam int limit, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder, @RequestParam(required = false) Long startTime, @RequestParam(required = false) Long endTime, - @RequestParam(required = false, defaultValue = "false") boolean ascOrder, - @RequestParam(required = false) String offset, @RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException { try { checkParameter("EntityId", strEntityId); checkParameter("EntityType", strEntityType); TenantId tenantId = getCurrentUser().getTenantId(); - TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset); + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); List actionTypes = parseActionTypesStr(actionTypesStr); return checkNotNull(auditLogService.findAuditLogsByTenantIdAndEntityId(tenantId, EntityIdFactory.getByTypeAndId(strEntityType, strEntityId), actionTypes, pageLink)); } catch (Exception e) { @@ -113,19 +119,21 @@ public class AuditLogController extends BaseController { } @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/audit/logs", params = {"limit"}, method = RequestMethod.GET) + @RequestMapping(value = "/audit/logs", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody - public TimePageData getAuditLogs( - @RequestParam int limit, + public PageData getAuditLogs( + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder, @RequestParam(required = false) Long startTime, @RequestParam(required = false) Long endTime, - @RequestParam(required = false, defaultValue = "false") boolean ascOrder, - @RequestParam(required = false) String offset, @RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); - TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset); List actionTypes = parseActionTypesStr(actionTypesStr); + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); return checkNotNull(auditLogService.findAuditLogsByTenantId(tenantId, actionTypes, pageLink)); } catch (Exception e) { throw handleException(e); diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index d57346cf48..47119c4bb9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -38,10 +38,8 @@ import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.dao.audit.AuditLogService; -import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; @@ -84,9 +82,6 @@ public class AuthController extends BaseController { @Autowired private AuditLogService auditLogService; - @Autowired - private OAuth2Service oauth2Service; - @PreAuthorize("isAuthenticated()") @RequestMapping(value = "/auth/user", method = RequestMethod.GET) public @ResponseBody User getUser() throws ThingsboardException { @@ -337,14 +332,4 @@ public class AuthController extends BaseController { throw handleException(e); } } - - @RequestMapping(value = "/noauth/oauth2Clients", method = RequestMethod.POST) - @ResponseBody - public List getOAuth2Clients() throws ThingsboardException { - try { - return oauth2Service.getOAuth2Clients(); - } catch (Exception e) { - throw handleException(e); - } - } } diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 48e528b0d8..869cbea127 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -27,20 +27,11 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.ExceptionHandler; -import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.Dashboard; -import org.thingsboard.server.common.data.DashboardInfo; -import org.thingsboard.server.common.data.DataConstants; -import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.EntityView; -import org.thingsboard.server.common.data.HasName; -import org.thingsboard.server.common.data.HasTenantId; -import org.thingsboard.server.common.data.Tenant; -import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.*; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -49,18 +40,21 @@ import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.DataType; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -71,7 +65,6 @@ import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.audit.AuditLogService; @@ -79,13 +72,17 @@ import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.ClaimDevicesService; import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; +import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; +import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.widget.WidgetTypeService; @@ -95,12 +92,15 @@ import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.component.ComponentDiscoveryService; +import org.thingsboard.server.service.profile.TbDeviceProfileCache; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.service.queue.TbClusterService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.AccessControlService; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.state.DeviceStateService; +import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import javax.mail.MessagingException; @@ -130,6 +130,9 @@ public abstract class BaseController { @Autowired protected TenantService tenantService; + @Autowired + protected TenantProfileService tenantProfileService; + @Autowired protected CustomerService customerService; @@ -139,11 +142,14 @@ public abstract class BaseController { @Autowired protected DeviceService deviceService; + @Autowired + protected DeviceProfileService deviceProfileService; + @Autowired protected AssetService assetService; @Autowired - protected AlarmService alarmService; + protected AlarmSubscriptionService alarmService; @Autowired protected DeviceCredentialsService deviceCredentialsService; @@ -157,6 +163,12 @@ public abstract class BaseController { @Autowired protected DashboardService dashboardService; + @Autowired + protected OAuth2Service oAuth2Service; + + @Autowired + protected OAuth2ConfigTemplateService oAuth2ConfigTemplateService; + @Autowired protected ComponentDiscoveryService componentDescriptorService; @@ -193,6 +205,12 @@ public abstract class BaseController { @Autowired protected TbQueueProducerProvider producerProvider; + @Autowired + protected TbTenantProfileCache tenantProfileCache; + + @Autowired + protected TbDeviceProfileCache deviceProfileCache; + @Value("${server.log_controller_error_stack_trace}") @Getter private boolean logControllerErrorStackTrace; @@ -264,21 +282,27 @@ public abstract class BaseController { return UUID.fromString(id); } - TimePageLink createPageLink(int limit, Long startTime, Long endTime, boolean ascOrder, String idOffset) { - UUID idOffsetUuid = null; - if (StringUtils.isNotEmpty(idOffset)) { - idOffsetUuid = toUUID(idOffset); + PageLink createPageLink(int pageSize, int page, String textSearch, String sortProperty, String sortOrder) throws ThingsboardException { + if (!StringUtils.isEmpty(sortProperty)) { + SortOrder.Direction direction = SortOrder.Direction.ASC; + if (!StringUtils.isEmpty(sortOrder)) { + try { + direction = SortOrder.Direction.valueOf(sortOrder.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new ThingsboardException("Unsupported sort order '" + sortOrder + "'! Only 'ASC' or 'DESC' types are allowed.", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + } + SortOrder sort = new SortOrder(sortProperty, direction); + return new PageLink(pageSize, page, textSearch, sort); + } else { + return new PageLink(pageSize, page, textSearch); } - return new TimePageLink(limit, startTime, endTime, ascOrder, idOffsetUuid); } - - TextPageLink createPageLink(int limit, String textSearch, String idOffset, String textOffset) { - UUID idOffsetUuid = null; - if (StringUtils.isNotEmpty(idOffset)) { - idOffsetUuid = toUUID(idOffset); - } - return new TextPageLink(limit, textSearch, idOffsetUuid, textOffset); + TimePageLink createTimePageLink(int pageSize, int page, String textSearch, + String sortProperty, String sortOrder, Long startTime, Long endTime) throws ThingsboardException { + PageLink pageLink = this.createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return new TimePageLink(pageLink, startTime, endTime); } protected SecurityUser getCurrentUser() throws ThingsboardException { @@ -302,6 +326,30 @@ public abstract class BaseController { } } + TenantInfo checkTenantInfoId(TenantId tenantId, Operation operation) throws ThingsboardException { + try { + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + TenantInfo tenant = tenantService.findTenantInfoById(tenantId); + checkNotNull(tenant); + accessControlService.checkPermission(getCurrentUser(), Resource.TENANT, operation, tenantId, tenant); + return tenant; + } catch (Exception e) { + throw handleException(e, false); + } + } + + TenantProfile checkTenantProfileId(TenantProfileId tenantProfileId, Operation operation) throws ThingsboardException { + try { + validateId(tenantProfileId, "Incorrect tenantProfileId " + tenantProfileId); + TenantProfile tenantProfile = tenantProfileService.findTenantProfileById(getTenantId(), tenantProfileId); + checkNotNull(tenantProfile); + accessControlService.checkPermission(getCurrentUser(), Resource.TENANT_PROFILE, operation); + return tenantProfile; + } catch (Exception e) { + throw handleException(e, false); + } + } + protected TenantId getTenantId() throws ThingsboardException { return getCurrentUser().getTenantId(); } @@ -350,12 +398,18 @@ public abstract class BaseController { case DEVICE: checkDeviceId(new DeviceId(entityId.getId()), operation); return; + case DEVICE_PROFILE: + checkDeviceProfileId(new DeviceProfileId(entityId.getId()), operation); + return; case CUSTOMER: checkCustomerId(new CustomerId(entityId.getId()), operation); return; case TENANT: checkTenantId(new TenantId(entityId.getId()), operation); return; + case TENANT_PROFILE: + checkTenantProfileId(new TenantProfileId(entityId.getId()), operation); + return; case RULE_CHAIN: checkRuleChain(new RuleChainId(entityId.getId()), operation); return; @@ -400,6 +454,30 @@ public abstract class BaseController { } } + DeviceInfo checkDeviceInfoId(DeviceId deviceId, Operation operation) throws ThingsboardException { + try { + validateId(deviceId, "Incorrect deviceId " + deviceId); + DeviceInfo device = deviceService.findDeviceInfoById(getCurrentUser().getTenantId(), deviceId); + checkNotNull(device); + accessControlService.checkPermission(getCurrentUser(), Resource.DEVICE, operation, deviceId, device); + return device; + } catch (Exception e) { + throw handleException(e, false); + } + } + + DeviceProfile checkDeviceProfileId(DeviceProfileId deviceProfileId, Operation operation) throws ThingsboardException { + try { + validateId(deviceProfileId, "Incorrect deviceProfileId " + deviceProfileId); + DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(getCurrentUser().getTenantId(), deviceProfileId); + checkNotNull(deviceProfile); + accessControlService.checkPermission(getCurrentUser(), Resource.DEVICE_PROFILE, operation, deviceProfileId, deviceProfile); + return deviceProfile; + } catch (Exception e) { + throw handleException(e, false); + } + } + protected EntityView checkEntityViewId(EntityViewId entityViewId, Operation operation) throws ThingsboardException { try { validateId(entityViewId, "Incorrect entityViewId " + entityViewId); @@ -412,6 +490,18 @@ public abstract class BaseController { } } + EntityViewInfo checkEntityViewInfoId(EntityViewId entityViewId, Operation operation) throws ThingsboardException { + try { + validateId(entityViewId, "Incorrect entityViewId " + entityViewId); + EntityViewInfo entityView = entityViewService.findEntityViewInfoById(getCurrentUser().getTenantId(), entityViewId); + checkNotNull(entityView); + accessControlService.checkPermission(getCurrentUser(), Resource.ENTITY_VIEW, operation, entityViewId, entityView); + return entityView; + } catch (Exception e) { + throw handleException(e, false); + } + } + Asset checkAssetId(AssetId assetId, Operation operation) throws ThingsboardException { try { validateId(assetId, "Incorrect assetId " + assetId); @@ -424,6 +514,18 @@ public abstract class BaseController { } } + AssetInfo checkAssetInfoId(AssetId assetId, Operation operation) throws ThingsboardException { + try { + validateId(assetId, "Incorrect assetId " + assetId); + AssetInfo asset = assetService.findAssetInfoById(getCurrentUser().getTenantId(), assetId); + checkNotNull(asset); + accessControlService.checkPermission(getCurrentUser(), Resource.ASSET, operation, assetId, asset); + return asset; + } catch (Exception e) { + throw handleException(e, false); + } + } + Alarm checkAlarmId(AlarmId alarmId, Operation operation) throws ThingsboardException { try { validateId(alarmId, "Incorrect alarmId " + alarmId); @@ -601,6 +703,12 @@ public abstract class BaseController { case ASSIGNED_TO_TENANT: msgType = DataConstants.ENTITY_ASSIGNED_TO_TENANT; break; + case PROVISION_SUCCESS: + msgType = DataConstants.PROVISION_SUCCESS; + break; + case PROVISION_FAILURE: + msgType = DataConstants.PROVISION_FAILURE; + break; } if (!StringUtils.isEmpty(msgType)) { try { diff --git a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java index 75dee42ff8..ff299e5c1f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java @@ -34,8 +34,8 @@ import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; @@ -144,14 +144,15 @@ public class CustomerController extends BaseController { } @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/customers", params = {"limit"}, method = RequestMethod.GET) + @RequestMapping(value = "/customers", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody - public TextPageData getCustomers(@RequestParam int limit, - @RequestParam(required = false) String textSearch, - @RequestParam(required = false) String idOffset, - @RequestParam(required = false) String textOffset) throws ThingsboardException { + public PageData getCustomers(@RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { - TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); TenantId tenantId = getCurrentUser().getTenantId(); return checkNotNull(customerService.findCustomersByTenantId(tenantId, pageLink)); } catch (Exception e) { diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java index e56b475b78..02c5f2ff20 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -36,9 +36,8 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; -import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.permission.Operation; @@ -415,18 +414,19 @@ public class DashboardController extends BaseController { } @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/tenant/{tenantId}/dashboards", params = {"limit"}, method = RequestMethod.GET) + @RequestMapping(value = "/tenant/{tenantId}/dashboards", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody - public TextPageData getTenantDashboards( + public PageData getTenantDashboards( @PathVariable("tenantId") String strTenantId, - @RequestParam int limit, + @RequestParam int pageSize, + @RequestParam int page, @RequestParam(required = false) String textSearch, - @RequestParam(required = false) String idOffset, - @RequestParam(required = false) String textOffset) throws ThingsboardException { + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { TenantId tenantId = new TenantId(toUUID(strTenantId)); checkTenantId(tenantId, Operation.READ); - TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return checkNotNull(dashboardService.findDashboardsByTenantId(tenantId, pageLink)); } catch (Exception e) { throw handleException(e); @@ -434,16 +434,17 @@ public class DashboardController extends BaseController { } @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/dashboards", params = {"limit"}, method = RequestMethod.GET) + @RequestMapping(value = "/tenant/dashboards", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody - public TextPageData getTenantDashboards( - @RequestParam int limit, + public PageData getTenantDashboards( + @RequestParam int pageSize, + @RequestParam int page, @RequestParam(required = false) String textSearch, - @RequestParam(required = false) String idOffset, - @RequestParam(required = false) String textOffset) throws ThingsboardException { + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); - TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return checkNotNull(dashboardService.findDashboardsByTenantId(tenantId, pageLink)); } catch (Exception e) { throw handleException(e); @@ -451,22 +452,22 @@ public class DashboardController extends BaseController { } @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/customer/{customerId}/dashboards", params = {"limit"}, method = RequestMethod.GET) + @RequestMapping(value = "/customer/{customerId}/dashboards", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody - public TimePageData getCustomerDashboards( + public PageData getCustomerDashboards( @PathVariable("customerId") String strCustomerId, - @RequestParam int limit, - @RequestParam(required = false) Long startTime, - @RequestParam(required = false) Long endTime, - @RequestParam(required = false, defaultValue = "false") boolean ascOrder, - @RequestParam(required = false) String offset) throws ThingsboardException { + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("customerId", strCustomerId); try { TenantId tenantId = getCurrentUser().getTenantId(); CustomerId customerId = new CustomerId(toUUID(strCustomerId)); checkCustomerId(customerId, Operation.READ); - TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset); - return checkNotNull(dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink).get()); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return checkNotNull(dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink)); } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index 4f226d2d4c..03ccc9fb93 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.ClaimRequest; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Tenant; @@ -46,9 +47,11 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; @@ -90,6 +93,19 @@ public class DeviceController extends BaseController { } } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/device/info/{deviceId}", method = RequestMethod.GET) + @ResponseBody + public DeviceInfo getDeviceInfoById(@PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { + checkParameter(DEVICE_ID, strDeviceId); + try { + DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); + return checkDeviceInfoId(deviceId, Operation.READ); + } catch (Exception e) { + throw handleException(e); + } + } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/device", method = RequestMethod.POST) @ResponseBody @@ -104,6 +120,8 @@ public class DeviceController extends BaseController { tbClusterService.pushMsgToCore(new DeviceNameOrTypeUpdateMsg(savedDevice.getTenantId(), savedDevice.getId(), savedDevice.getName(), savedDevice.getType()), null); + tbClusterService.onEntityStateChange(savedDevice.getTenantId(), savedDevice.getId(), + device.getId() == null ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); logEntityAction(savedDevice.getId(), savedDevice, savedDevice.getCustomerId(), @@ -272,17 +290,18 @@ public class DeviceController extends BaseController { } @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/devices", params = {"limit"}, method = RequestMethod.GET) + @RequestMapping(value = "/tenant/devices", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody - public TextPageData getTenantDevices( - @RequestParam int limit, + public PageData getTenantDevices( + @RequestParam int pageSize, + @RequestParam int page, @RequestParam(required = false) String type, @RequestParam(required = false) String textSearch, - @RequestParam(required = false) String idOffset, - @RequestParam(required = false) String textOffset) throws ThingsboardException { + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); - TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); if (type != null && type.trim().length() > 0) { return checkNotNull(deviceService.findDevicesByTenantIdAndType(tenantId, type, pageLink)); } else { @@ -293,6 +312,33 @@ public class DeviceController extends BaseController { } } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/tenant/deviceInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getTenantDeviceInfos( + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String type, + @RequestParam(required = false) String deviceProfileId, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + TenantId tenantId = getCurrentUser().getTenantId(); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (type != null && type.trim().length() > 0) { + return checkNotNull(deviceService.findDeviceInfosByTenantIdAndType(tenantId, type, pageLink)); + } else if (deviceProfileId != null && deviceProfileId.length() > 0) { + DeviceProfileId profileId = new DeviceProfileId(toUUID(deviceProfileId)); + return checkNotNull(deviceService.findDeviceInfosByTenantIdAndDeviceProfileId(tenantId, profileId, pageLink)); + } else { + return checkNotNull(deviceService.findDeviceInfosByTenantId(tenantId, pageLink)); + } + } catch (Exception e) { + throw handleException(e); + } + } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/tenant/devices", params = {"deviceName"}, method = RequestMethod.GET) @ResponseBody @@ -307,21 +353,22 @@ public class DeviceController extends BaseController { } @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/customer/{customerId}/devices", params = {"limit"}, method = RequestMethod.GET) + @RequestMapping(value = "/customer/{customerId}/devices", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody - public TextPageData getCustomerDevices( + public PageData getCustomerDevices( @PathVariable("customerId") String strCustomerId, - @RequestParam int limit, + @RequestParam int pageSize, + @RequestParam int page, @RequestParam(required = false) String type, @RequestParam(required = false) String textSearch, - @RequestParam(required = false) String idOffset, - @RequestParam(required = false) String textOffset) throws ThingsboardException { + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("customerId", strCustomerId); try { TenantId tenantId = getCurrentUser().getTenantId(); CustomerId customerId = new CustomerId(toUUID(strCustomerId)); checkCustomerId(customerId, Operation.READ); - TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); if (type != null && type.trim().length() > 0) { return checkNotNull(deviceService.findDevicesByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink)); } else { @@ -332,6 +379,37 @@ public class DeviceController extends BaseController { } } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/customer/{customerId}/deviceInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getCustomerDeviceInfos( + @PathVariable("customerId") String strCustomerId, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String type, + @RequestParam(required = false) String deviceProfileId, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + checkParameter("customerId", strCustomerId); + try { + TenantId tenantId = getCurrentUser().getTenantId(); + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + checkCustomerId(customerId, Operation.READ); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (type != null && type.trim().length() > 0) { + return checkNotNull(deviceService.findDeviceInfosByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink)); + } else if (deviceProfileId != null && deviceProfileId.length() > 0) { + DeviceProfileId profileId = new DeviceProfileId(toUUID(deviceProfileId)); + return checkNotNull(deviceService.findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(tenantId, customerId, profileId, pageLink)); + } else { + return checkNotNull(deviceService.findDeviceInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink)); + } + } catch (Exception e) { + throw handleException(e); + } + } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/devices", params = {"deviceIds"}, method = RequestMethod.GET) @ResponseBody diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java new file mode 100644 index 0000000000..fcb34530b5 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java @@ -0,0 +1,202 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileInfo; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.permission.Resource; + +@RestController +@TbCoreComponent +@RequestMapping("/api") +@Slf4j +public class DeviceProfileController extends BaseController { + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/deviceProfile/{deviceProfileId}", method = RequestMethod.GET) + @ResponseBody + public DeviceProfile getDeviceProfileById(@PathVariable("deviceProfileId") String strDeviceProfileId) throws ThingsboardException { + checkParameter("deviceProfileId", strDeviceProfileId); + try { + DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId)); + return checkDeviceProfileId(deviceProfileId, Operation.READ); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/deviceProfileInfo/{deviceProfileId}", method = RequestMethod.GET) + @ResponseBody + public DeviceProfileInfo getDeviceProfileInfoById(@PathVariable("deviceProfileId") String strDeviceProfileId) throws ThingsboardException { + checkParameter("deviceProfileId", strDeviceProfileId); + try { + DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId)); + return checkNotNull(deviceProfileService.findDeviceProfileInfoById(getTenantId(), deviceProfileId)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/deviceProfileInfo/default", method = RequestMethod.GET) + @ResponseBody + public DeviceProfileInfo getDefaultDeviceProfileInfo() throws ThingsboardException { + try { + return checkNotNull(deviceProfileService.findDefaultDeviceProfileInfo(getTenantId())); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/deviceProfile", method = RequestMethod.POST) + @ResponseBody + public DeviceProfile saveDeviceProfile(@RequestBody DeviceProfile deviceProfile) throws ThingsboardException { + try { + boolean created = deviceProfile.getId() == null; + deviceProfile.setTenantId(getTenantId()); + + checkEntity(deviceProfile.getId(), deviceProfile, Resource.DEVICE_PROFILE); + + DeviceProfile savedDeviceProfile = checkNotNull(deviceProfileService.saveDeviceProfile(deviceProfile)); + + tbClusterService.onDeviceProfileChange(savedDeviceProfile, null); + tbClusterService.onEntityStateChange(deviceProfile.getTenantId(), savedDeviceProfile.getId(), + created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); + + logEntityAction(savedDeviceProfile.getId(), savedDeviceProfile, + null, + savedDeviceProfile.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); + + return savedDeviceProfile; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.DEVICE_PROFILE), deviceProfile, + null, deviceProfile.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e); + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/deviceProfile/{deviceProfileId}", method = RequestMethod.DELETE) + @ResponseStatus(value = HttpStatus.OK) + public void deleteDeviceProfile(@PathVariable("deviceProfileId") String strDeviceProfileId) throws ThingsboardException { + checkParameter("deviceProfileId", strDeviceProfileId); + try { + DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId)); + DeviceProfile deviceProfile = checkDeviceProfileId(deviceProfileId, Operation.DELETE); + deviceProfileService.deleteDeviceProfile(getTenantId(), deviceProfileId); + + tbClusterService.onDeviceProfileDelete(deviceProfile, null); + tbClusterService.onEntityStateChange(deviceProfile.getTenantId(), deviceProfile.getId(), ComponentLifecycleEvent.DELETED); + + logEntityAction(deviceProfileId, deviceProfile, + null, + ActionType.DELETED, null, strDeviceProfileId); + + } catch (Exception e) { + logEntityAction(emptyId(EntityType.DEVICE_PROFILE), + null, + null, + ActionType.DELETED, e, strDeviceProfileId); + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/deviceProfile/{deviceProfileId}/default", method = RequestMethod.POST) + @ResponseBody + public DeviceProfile setDefaultDeviceProfile(@PathVariable("deviceProfileId") String strDeviceProfileId) throws ThingsboardException { + checkParameter("deviceProfileId", strDeviceProfileId); + try { + DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId)); + DeviceProfile deviceProfile = checkDeviceProfileId(deviceProfileId, Operation.WRITE); + DeviceProfile previousDefaultDeviceProfile = deviceProfileService.findDefaultDeviceProfile(getTenantId()); + if (deviceProfileService.setDefaultDeviceProfile(getTenantId(), deviceProfileId)) { + if (previousDefaultDeviceProfile != null) { + previousDefaultDeviceProfile = deviceProfileService.findDeviceProfileById(getTenantId(), previousDefaultDeviceProfile.getId()); + + logEntityAction(previousDefaultDeviceProfile.getId(), previousDefaultDeviceProfile, + null, ActionType.UPDATED, null); + } + deviceProfile = deviceProfileService.findDeviceProfileById(getTenantId(), deviceProfileId); + + logEntityAction(deviceProfile.getId(), deviceProfile, + null, ActionType.UPDATED, null); + } + return deviceProfile; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.DEVICE_PROFILE), + null, + null, + ActionType.UPDATED, e, strDeviceProfileId); + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/deviceProfiles", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getDeviceProfiles(@RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return checkNotNull(deviceProfileService.findDeviceProfiles(getTenantId(), pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/deviceProfileInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getDeviceProfileInfos(@RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder, + @RequestParam(required = false) String transportType) throws ThingsboardException { + try { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return checkNotNull(deviceProfileService.findDeviceProfileInfos(getTenantId(), pageLink, transportType)); + } catch (Exception e) { + throw handleException(e); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java new file mode 100644 index 0000000000..94417886d6 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java @@ -0,0 +1,79 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AlarmData; +import org.thingsboard.server.common.data.query.AlarmDataQuery; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.query.EntityQueryService; + +@RestController +@TbCoreComponent +@RequestMapping("/api") +public class EntityQueryController extends BaseController { + + @Autowired + private EntityQueryService entityQueryService; + + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/entitiesQuery/count", method = RequestMethod.POST) + @ResponseBody + public long countEntitiesByQuery(@RequestBody EntityCountQuery query) throws ThingsboardException { + checkNotNull(query); + try { + return this.entityQueryService.countEntitiesByQuery(getCurrentUser(), query); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/entitiesQuery/find", method = RequestMethod.POST) + @ResponseBody + public PageData findEntityDataByQuery(@RequestBody EntityDataQuery query) throws ThingsboardException { + checkNotNull(query); + try { + return this.entityQueryService.findEntityDataByQuery(getCurrentUser(), query); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/alarmsQuery/find", method = RequestMethod.POST) + @ResponseBody + public PageData findAlarmDataByQuery(@RequestBody AlarmDataQuery query) throws ThingsboardException { + checkNotNull(query); + try { + return this.entityQueryService.findAlarmDataByQuery(getCurrentUser(), query); + } catch (Exception e) { + throw handleException(e); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java index 47023c24a6..e2ed0dde69 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java @@ -19,7 +19,9 @@ import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; @@ -30,11 +32,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; -import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.DataConstants; -import org.thingsboard.server.common.data.EntitySubtype; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.*; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -44,10 +42,14 @@ import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; +import org.thingsboard.server.common.data.kv.ReadTsKvQuery; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; @@ -56,10 +58,12 @@ import org.thingsboard.server.service.security.permission.Resource; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; +import static org.apache.commons.lang.StringUtils.isBlank; import static org.thingsboard.server.controller.CustomerController.CUSTOMER_ID; /** @@ -73,6 +77,9 @@ public class EntityViewController extends BaseController { public static final String ENTITY_VIEW_ID = "entityViewId"; + @Autowired + private TimeseriesService tsService; + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/entityView/{entityViewId}", method = RequestMethod.GET) @ResponseBody @@ -85,6 +92,19 @@ public class EntityViewController extends BaseController { } } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/entityView/info/{entityViewId}", method = RequestMethod.GET) + @ResponseBody + public EntityViewInfo getEntityViewInfoById(@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { + checkParameter(ENTITY_VIEW_ID, strEntityViewId); + try { + EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId)); + return checkEntityViewInfoId(entityViewId, Operation.READ); + } catch (Exception e) { + throw handleException(e); + } + } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/entityView", method = RequestMethod.POST) @ResponseBody @@ -92,16 +112,35 @@ public class EntityViewController extends BaseController { try { entityView.setTenantId(getCurrentUser().getTenantId()); - checkEntity(entityView.getId(), entityView, Resource.ENTITY_VIEW); + List> futures = new ArrayList<>(); + + if (entityView.getId() == null) { + accessControlService + .checkPermission(getCurrentUser(), Resource.ENTITY_VIEW, Operation.CREATE, null, entityView); + } else { + EntityView existingEntityView = checkEntityViewId(entityView.getId(), Operation.WRITE); + if (existingEntityView.getKeys() != null) { + if (existingEntityView.getKeys().getAttributes() != null) { + futures.add(deleteAttributesFromEntityView(existingEntityView, DataConstants.CLIENT_SCOPE, existingEntityView.getKeys().getAttributes().getCs(), getCurrentUser())); + futures.add(deleteAttributesFromEntityView(existingEntityView, DataConstants.SERVER_SCOPE, existingEntityView.getKeys().getAttributes().getCs(), getCurrentUser())); + futures.add(deleteAttributesFromEntityView(existingEntityView, DataConstants.SHARED_SCOPE, existingEntityView.getKeys().getAttributes().getCs(), getCurrentUser())); + } + } + List tsKeys = existingEntityView.getKeys() != null && existingEntityView.getKeys().getTimeseries() != null ? + existingEntityView.getKeys().getTimeseries() : Collections.emptyList(); + futures.add(deleteLatestFromEntityView(existingEntityView, tsKeys, getCurrentUser())); + } EntityView savedEntityView = checkNotNull(entityViewService.saveEntityView(entityView)); - List>> futures = new ArrayList<>(); - if (savedEntityView.getKeys() != null && savedEntityView.getKeys().getAttributes() != null) { - futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.CLIENT_SCOPE, savedEntityView.getKeys().getAttributes().getCs(), getCurrentUser())); - futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.SERVER_SCOPE, savedEntityView.getKeys().getAttributes().getSs(), getCurrentUser())); - futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.SHARED_SCOPE, savedEntityView.getKeys().getAttributes().getSh(), getCurrentUser())); + if (savedEntityView.getKeys() != null) { + if (savedEntityView.getKeys().getAttributes() != null) { + futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.CLIENT_SCOPE, savedEntityView.getKeys().getAttributes().getCs(), getCurrentUser())); + futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.SERVER_SCOPE, savedEntityView.getKeys().getAttributes().getSs(), getCurrentUser())); + futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.SHARED_SCOPE, savedEntityView.getKeys().getAttributes().getSh(), getCurrentUser())); + } + futures.add(copyLatestFromEntityToEntityView(savedEntityView, getCurrentUser())); } - for (ListenableFuture> future : futures) { + for (ListenableFuture future : futures) { try { future.get(); } catch (InterruptedException | ExecutionException e) { @@ -119,6 +158,125 @@ public class EntityViewController extends BaseController { } } + private ListenableFuture deleteLatestFromEntityView(EntityView entityView, List keys, SecurityUser user) { + EntityViewId entityId = entityView.getId(); + SettableFuture resultFuture = SettableFuture.create(); + if (keys != null && !keys.isEmpty()) { + tsSubService.deleteLatest(entityView.getTenantId(), entityId, keys, new FutureCallback() { + @Override + public void onSuccess(@Nullable Void tmp) { + try { + logTimeseriesDeleted(user, entityId, keys, null); + } catch (ThingsboardException e) { + log.error("Failed to log timeseries delete", e); + } + resultFuture.set(tmp); + } + + @Override + public void onFailure(Throwable t) { + try { + logTimeseriesDeleted(user, entityId, keys, t); + } catch (ThingsboardException e) { + log.error("Failed to log timeseries delete", e); + } + resultFuture.setException(t); + } + }); + } else { + tsSubService.deleteAllLatest(entityView.getTenantId(), entityId, new FutureCallback>() { + @Override + public void onSuccess(@Nullable Collection keys) { + try { + logTimeseriesDeleted(user, entityId, new ArrayList<>(keys), null); + } catch (ThingsboardException e) { + log.error("Failed to log timeseries delete", e); + } + resultFuture.set(null); + } + + @Override + public void onFailure(Throwable t) { + try { + logTimeseriesDeleted(user, entityId, Collections.emptyList(), t); + } catch (ThingsboardException e) { + log.error("Failed to log timeseries delete", e); + } + resultFuture.setException(t); + } + }); + } + return resultFuture; + } + + private ListenableFuture deleteAttributesFromEntityView(EntityView entityView, String scope, List keys, SecurityUser user) { + EntityViewId entityId = entityView.getId(); + SettableFuture resultFuture = SettableFuture.create(); + if (keys != null && !keys.isEmpty()) { + tsSubService.deleteAndNotify(entityView.getTenantId(), entityId, scope, keys, new FutureCallback() { + @Override + public void onSuccess(@Nullable Void tmp) { + try { + logAttributesDeleted(user, entityId, scope, keys, null); + } catch (ThingsboardException e) { + log.error("Failed to log attribute delete", e); + } + resultFuture.set(tmp); + } + + @Override + public void onFailure(Throwable t) { + try { + logAttributesDeleted(user, entityId, scope, keys, t); + } catch (ThingsboardException e) { + log.error("Failed to log attribute delete", e); + } + resultFuture.setException(t); + } + }); + } else { + resultFuture.set(null); + } + return resultFuture; + } + + private ListenableFuture> copyLatestFromEntityToEntityView(EntityView entityView, SecurityUser user) { + EntityViewId entityId = entityView.getId(); + List keys = entityView.getKeys() != null && entityView.getKeys().getTimeseries() != null ? + entityView.getKeys().getTimeseries() : Collections.emptyList(); + long startTs = entityView.getStartTimeMs(); + long endTs = entityView.getEndTimeMs() == 0 ? Long.MAX_VALUE : entityView.getEndTimeMs(); + ListenableFuture> keysFuture; + if (keys.isEmpty()) { + keysFuture = Futures.transform(tsService.findAllLatest(user.getTenantId(), + entityView.getEntityId()), latest -> latest.stream().map(TsKvEntry::getKey).collect(Collectors.toList()), MoreExecutors.directExecutor()); + } else { + keysFuture = Futures.immediateFuture(keys); + } + ListenableFuture> latestFuture = Futures.transformAsync(keysFuture, fetchKeys -> { + List queries = fetchKeys.stream().filter(key -> !isBlank(key)).map(key -> new BaseReadTsKvQuery(key, startTs, endTs, 1, "DESC")).collect(Collectors.toList()); + if (!queries.isEmpty()) { + return tsService.findAll(user.getTenantId(), entityView.getEntityId(), queries); + } else { + return Futures.immediateFuture(null); + } + }, MoreExecutors.directExecutor()); + return Futures.transform(latestFuture, latestValues -> { + if (latestValues != null && !latestValues.isEmpty()) { + tsSubService.saveLatestAndNotify(entityView.getTenantId(), entityId, latestValues, new FutureCallback() { + @Override + public void onSuccess(@Nullable Void tmp) { + } + + @Override + public void onFailure(Throwable t) { + } + }); + } + return null; + }, MoreExecutors.directExecutor()); + } + private ListenableFuture> copyAttributesFromEntityToEntityView(EntityView entityView, String scope, Collection keys, SecurityUser user) throws ThingsboardException { EntityViewId entityId = entityView.getId(); if (keys != null && !keys.isEmpty()) { @@ -165,10 +323,20 @@ public class EntityViewController extends BaseController { } private void logAttributesUpdated(SecurityUser user, EntityId entityId, String scope, List attributes, Throwable e) throws ThingsboardException { - logEntityAction(user, (UUIDBased & EntityId) entityId, null, null, ActionType.ATTRIBUTES_UPDATED, toException(e), + logEntityAction(user, entityId, null, null, ActionType.ATTRIBUTES_UPDATED, toException(e), scope, attributes); } + private void logAttributesDeleted(SecurityUser user, EntityId entityId, String scope, List keys, Throwable e) throws ThingsboardException { + logEntityAction(user, entityId, null, null, ActionType.ATTRIBUTES_DELETED, toException(e), + scope, keys); + } + + private void logTimeseriesDeleted(SecurityUser user, EntityId entityId, List keys, Throwable e) throws ThingsboardException { + logEntityAction(user, entityId, null, null, ActionType.TIMESERIES_DELETED, toException(e), + keys); + } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/entityView/{entityViewId}", method = RequestMethod.DELETE) @ResponseStatus(value = HttpStatus.OK) @@ -256,21 +424,22 @@ public class EntityViewController extends BaseController { } @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/customer/{customerId}/entityViews", params = {"limit"}, method = RequestMethod.GET) + @RequestMapping(value = "/customer/{customerId}/entityViews", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody - public TextPageData getCustomerEntityViews( + public PageData getCustomerEntityViews( @PathVariable("customerId") String strCustomerId, - @RequestParam int limit, + @RequestParam int pageSize, + @RequestParam int page, @RequestParam(required = false) String type, @RequestParam(required = false) String textSearch, - @RequestParam(required = false) String idOffset, - @RequestParam(required = false) String textOffset) throws ThingsboardException { + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("customerId", strCustomerId); try { TenantId tenantId = getCurrentUser().getTenantId(); CustomerId customerId = new CustomerId(toUUID(strCustomerId)); checkCustomerId(customerId, Operation.READ); - TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); if (type != null && type.trim().length() > 0) { return checkNotNull(entityViewService.findEntityViewsByTenantIdAndCustomerIdAndType(tenantId, customerId, pageLink, type)); } else { @@ -281,18 +450,46 @@ public class EntityViewController extends BaseController { } } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/customer/{customerId}/entityViewInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getCustomerEntityViewInfos( + @PathVariable("customerId") String strCustomerId, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String type, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + checkParameter("customerId", strCustomerId); + try { + TenantId tenantId = getCurrentUser().getTenantId(); + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + checkCustomerId(customerId, Operation.READ); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (type != null && type.trim().length() > 0) { + return checkNotNull(entityViewService.findEntityViewInfosByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink)); + } else { + return checkNotNull(entityViewService.findEntityViewInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink)); + } + } catch (Exception e) { + throw handleException(e); + } + } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/entityViews", params = {"limit"}, method = RequestMethod.GET) + @RequestMapping(value = "/tenant/entityViews", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody - public TextPageData getTenantEntityViews( - @RequestParam int limit, + public PageData getTenantEntityViews( + @RequestParam int pageSize, + @RequestParam int page, @RequestParam(required = false) String type, @RequestParam(required = false) String textSearch, - @RequestParam(required = false) String idOffset, - @RequestParam(required = false) String textOffset) throws ThingsboardException { + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); - TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); if (type != null && type.trim().length() > 0) { return checkNotNull(entityViewService.findEntityViewByTenantIdAndType(tenantId, pageLink, type)); @@ -304,6 +501,29 @@ public class EntityViewController extends BaseController { } } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/tenant/entityViewInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getTenantEntityViewInfos( + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String type, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + TenantId tenantId = getCurrentUser().getTenantId(); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (type != null && type.trim().length() > 0) { + return checkNotNull(entityViewService.findEntityViewInfosByTenantIdAndType(tenantId, type, pageLink)); + } else { + return checkNotNull(entityViewService.findEntityViewInfosByTenantId(tenantId, pageLink)); + } + } catch (Exception e) { + throw handleException(e); + } + } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/entityViews", method = RequestMethod.POST) @ResponseBody diff --git a/application/src/main/java/org/thingsboard/server/controller/EventController.java b/application/src/main/java/org/thingsboard/server/controller/EventController.java index 500549c09a..cf868b29a2 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EventController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EventController.java @@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -45,17 +45,18 @@ public class EventController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/events/{entityType}/{entityId}/{eventType}", method = RequestMethod.GET) @ResponseBody - public TimePageData getEvents( + public PageData getEvents( @PathVariable("entityType") String strEntityType, @PathVariable("entityId") String strEntityId, @PathVariable("eventType") String eventType, @RequestParam("tenantId") String strTenantId, - @RequestParam int limit, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder, @RequestParam(required = false) Long startTime, - @RequestParam(required = false) Long endTime, - @RequestParam(required = false, defaultValue = "false") boolean ascOrder, - @RequestParam(required = false) String offset - ) throws ThingsboardException { + @RequestParam(required = false) Long endTime) throws ThingsboardException { checkParameter("EntityId", strEntityId); checkParameter("EntityType", strEntityType); try { @@ -63,8 +64,7 @@ public class EventController extends BaseController { EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId); checkEntityId(entityId, Operation.READ); - - TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset); + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); return checkNotNull(eventService.findEvents(tenantId, entityId, eventType, pageLink)); } catch (Exception e) { throw handleException(e); @@ -74,16 +74,17 @@ public class EventController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/events/{entityType}/{entityId}", method = RequestMethod.GET) @ResponseBody - public TimePageData getEvents( + public PageData getEvents( @PathVariable("entityType") String strEntityType, @PathVariable("entityId") String strEntityId, @RequestParam("tenantId") String strTenantId, - @RequestParam int limit, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder, @RequestParam(required = false) Long startTime, - @RequestParam(required = false) Long endTime, - @RequestParam(required = false, defaultValue = "false") boolean ascOrder, - @RequestParam(required = false) String offset - ) throws ThingsboardException { + @RequestParam(required = false) Long endTime) throws ThingsboardException { checkParameter("EntityId", strEntityId); checkParameter("EntityType", strEntityType); try { @@ -92,7 +93,8 @@ public class EventController extends BaseController { EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId); checkEntityId(entityId, Operation.READ); - TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset); + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); + return checkNotNull(eventService.findEvents(tenantId, entityId, pageLink)); } catch (Exception e) { throw handleException(e); diff --git a/application/src/main/java/org/thingsboard/server/controller/OAuth2ConfigTemplateController.java b/application/src/main/java/org/thingsboard/server/controller/OAuth2ConfigTemplateController.java new file mode 100644 index 0000000000..35ed01f03e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/OAuth2ConfigTemplateController.java @@ -0,0 +1,77 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.permission.Resource; + +import java.util.List; + +@RestController +@TbCoreComponent +@RequestMapping("/api/oauth2/config/template") +@Slf4j +public class OAuth2ConfigTemplateController extends BaseController { + private static final String CLIENT_REGISTRATION_TEMPLATE_ID = "clientRegistrationTemplateId"; + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @RequestMapping(method = RequestMethod.POST) + @ResponseStatus(value = HttpStatus.OK) + public OAuth2ClientRegistrationTemplate saveClientRegistrationTemplate(@RequestBody OAuth2ClientRegistrationTemplate clientRegistrationTemplate) throws ThingsboardException { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_TEMPLATE, Operation.WRITE); + return oAuth2ConfigTemplateService.saveClientRegistrationTemplate(clientRegistrationTemplate); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @RequestMapping(value = "/{clientRegistrationTemplateId}", method = RequestMethod.DELETE) + @ResponseStatus(value = HttpStatus.OK) + public void deleteClientRegistrationTemplate(@PathVariable(CLIENT_REGISTRATION_TEMPLATE_ID) String strClientRegistrationTemplateId) throws ThingsboardException { + checkParameter(CLIENT_REGISTRATION_TEMPLATE_ID, strClientRegistrationTemplateId); + try { + accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_TEMPLATE, Operation.DELETE); + OAuth2ClientRegistrationTemplateId clientRegistrationTemplateId = new OAuth2ClientRegistrationTemplateId(toUUID(strClientRegistrationTemplateId)); + oAuth2ConfigTemplateService.deleteClientRegistrationTemplateById(clientRegistrationTemplateId); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(method = RequestMethod.GET, produces = "application/json") + @ResponseBody + public List getClientRegistrationTemplates() throws ThingsboardException { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_TEMPLATE, Operation.READ); + return oAuth2ConfigTemplateService.findAllClientRegistrationTemplates(); + } catch (Exception e) { + throw handleException(e); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java new file mode 100644 index 0000000000..fc5e0a1426 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java @@ -0,0 +1,91 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientsParams; +import org.thingsboard.server.common.data.oauth2.SchemeType; +import org.thingsboard.server.dao.oauth2.OAuth2Configuration; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.permission.Resource; +import org.thingsboard.server.utils.MiscUtils; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +@RestController +@TbCoreComponent +@RequestMapping("/api") +@Slf4j +public class OAuth2Controller extends BaseController { + + @Autowired + private OAuth2Configuration oAuth2Configuration; + + @RequestMapping(value = "/noauth/oauth2Clients", method = RequestMethod.POST) + @ResponseBody + public List getOAuth2Clients(HttpServletRequest request) throws ThingsboardException { + try { + return oAuth2Service.getOAuth2Clients(MiscUtils.getScheme(request), MiscUtils.getDomainNameAndPort(request)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @RequestMapping(value = "/oauth2/config", method = RequestMethod.GET, produces = "application/json") + @ResponseBody + public OAuth2ClientsParams getCurrentOAuth2Params() throws ThingsboardException { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_INFO, Operation.READ); + return oAuth2Service.findOAuth2Params(); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @RequestMapping(value = "/oauth2/config", method = RequestMethod.POST) + @ResponseStatus(value = HttpStatus.OK) + public OAuth2ClientsParams saveOAuth2Params(@RequestBody OAuth2ClientsParams oauth2Params) throws ThingsboardException { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_INFO, Operation.WRITE); + oAuth2Service.saveOAuth2Params(oauth2Params); + return oAuth2Service.findOAuth2Params(); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @RequestMapping(value = "/oauth2/loginProcessingUrl", method = RequestMethod.GET) + @ResponseBody + public String getLoginProcessingUrl() throws ThingsboardException { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_INFO, Operation.READ); + return "\"" + oAuth2Configuration.getLoginProcessingUrl() + "\""; + } catch (Exception e) { + throw handleException(e); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java index 73b89b034f..7392f74231 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.controller; -import com.datastax.driver.core.utils.UUIDs; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -25,6 +24,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; @@ -45,10 +45,13 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.data.rule.DefaultRuleChainCreateRequest; import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainData; +import org.thingsboard.server.common.data.rule.RuleChainImportResult; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.msg.TbMsg; @@ -56,6 +59,7 @@ import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.install.InstallScripts; import org.thingsboard.server.service.script.JsInvokeService; import org.thingsboard.server.service.script.RuleNodeJsScriptEngine; import org.thingsboard.server.service.security.permission.Operation; @@ -78,6 +82,9 @@ public class RuleChainController extends BaseController { private static final ObjectMapper objectMapper = new ObjectMapper(); + @Autowired + private InstallScripts installScripts; + @Autowired private EventService eventService; @@ -147,6 +154,29 @@ public class RuleChainController extends BaseController { } } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/ruleChain/device/default", method = RequestMethod.POST) + @ResponseBody + public RuleChain saveRuleChain(@RequestBody DefaultRuleChainCreateRequest request) throws ThingsboardException { + try { + checkNotNull(request); + checkParameter(request.getName(), "name"); + + RuleChain savedRuleChain = installScripts.createDefaultRuleChain(getCurrentUser().getTenantId(), request.getName()); + + tbClusterService.onEntityStateChange(savedRuleChain.getTenantId(), savedRuleChain.getId(), ComponentLifecycleEvent.CREATED); + + logEntityAction(savedRuleChain.getId(), savedRuleChain, null, ActionType.ADDED, null); + + return savedRuleChain; + } catch (Exception e) { + RuleChain ruleChain = new RuleChain(); + ruleChain.setName(request.getName()); + logEntityAction(emptyId(EntityType.RULE_CHAIN), ruleChain, null, ActionType.ADDED, e); + throw handleException(e); + } + } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/ruleChain/{ruleChainId}/root", method = RequestMethod.POST) @ResponseBody @@ -220,16 +250,17 @@ public class RuleChainController extends BaseController { } @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChains", params = {"limit"}, method = RequestMethod.GET) + @RequestMapping(value = "/ruleChains", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody - public TextPageData getRuleChains( - @RequestParam int limit, + public PageData getRuleChains( + @RequestParam int pageSize, + @RequestParam int page, @RequestParam(required = false) String textSearch, - @RequestParam(required = false) String idOffset, - @RequestParam(required = false) String textOffset) throws ThingsboardException { + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); - TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return checkNotNull(ruleChainService.findTenantRuleChains(tenantId, pageLink)); } catch (Exception e) { throw handleException(e); @@ -316,7 +347,7 @@ public class RuleChainController extends BaseController { String errorText = ""; ScriptEngine engine = null; try { - engine = new RuleNodeJsScriptEngine(jsInvokeService, getCurrentUser().getId(), script, argNames); + engine = new RuleNodeJsScriptEngine(getTenantId(), jsInvokeService, getCurrentUser().getId(), script, argNames); TbMsg inMsg = TbMsg.newMsg(msgType, null, new TbMsgMetaData(metadata), TbMsgDataType.JSON, data); switch (scriptType) { case "update": @@ -360,6 +391,36 @@ public class RuleChainController extends BaseController { } } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/ruleChains/export", params = {"limit"}, method = RequestMethod.GET) + @ResponseBody + public RuleChainData exportRuleChains(@RequestParam("limit") int limit) throws ThingsboardException { + try { + TenantId tenantId = getCurrentUser().getTenantId(); + PageLink pageLink = new PageLink(limit); + return checkNotNull(ruleChainService.exportTenantRuleChains(tenantId, pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/ruleChains/import", method = RequestMethod.POST) + @ResponseBody + public void importRuleChains(@RequestBody RuleChainData ruleChainData, @RequestParam(required = false, defaultValue = "false") boolean overwrite) throws ThingsboardException { + try { + TenantId tenantId = getCurrentUser().getTenantId(); + List importResults = ruleChainService.importTenantRuleChains(tenantId, ruleChainData, overwrite); + if (!CollectionUtils.isEmpty(importResults)) { + for (RuleChainImportResult importResult : importResults) { + tbClusterService.onEntityStateChange(importResult.getTenantId(), importResult.getRuleChainId(), importResult.getLifecycleEvent()); + } + } + } catch (Exception e) { + throw handleException(e); + } + } + private String msgToOutput(TbMsg msg) throws Exception { ObjectNode msgData = objectMapper.createObjectNode(); if (!StringUtils.isEmpty(msg.getData())) { diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java index e8c4756a6f..fb5fade521 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java @@ -357,10 +357,9 @@ public class TelemetryController extends BaseController { DataConstants.SHARED_SCOPE.equals(scope) || DataConstants.CLIENT_SCOPE.equals(scope)) { return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.WRITE_ATTRIBUTES, entityIdSrc, (result, tenantId, entityId) -> { - ListenableFuture> future = attributesService.removeAll(user.getTenantId(), entityId, scope, keys); - Futures.addCallback(future, new FutureCallback>() { + tsSubService.deleteAndNotify(tenantId, entityId, scope, keys, new FutureCallback() { @Override - public void onSuccess(@Nullable List tmp) { + public void onSuccess(@Nullable Void tmp) { logAttributesDeleted(user, entityId, scope, keys, null); if (entityIdSrc.getEntityType().equals(EntityType.DEVICE)) { DeviceId deviceId = new DeviceId(entityId.getId()); @@ -377,7 +376,7 @@ public class TelemetryController extends BaseController { logAttributesDeleted(user, entityId, scope, keys, t); result.setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); } - }, executor); + }); }); } else { return getImmediateDeferredResult("Invalid attribute scope: " + scope, HttpStatus.BAD_REQUEST); diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantController.java b/application/src/main/java/org/thingsboard/server/controller/TenantController.java index ca87e08c96..a42cbb0f91 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantController.java @@ -28,10 +28,11 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -58,8 +59,20 @@ public class TenantController extends BaseController { checkParameter("tenantId", strTenantId); try { TenantId tenantId = new TenantId(toUUID(strTenantId)); - checkTenantId(tenantId, Operation.READ); - return checkNotNull(tenantService.findTenantById(tenantId)); + return checkTenantId(tenantId, Operation.READ); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/tenant/info/{tenantId}", method = RequestMethod.GET) + @ResponseBody + public TenantInfo getTenantInfoById(@PathVariable("tenantId") String strTenantId) throws ThingsboardException { + checkParameter("tenantId", strTenantId); + try { + TenantId tenantId = new TenantId(toUUID(strTenantId)); + return checkTenantInfoId(tenantId, Operation.READ); } catch (Exception e) { throw handleException(e); } @@ -78,6 +91,10 @@ public class TenantController extends BaseController { if (newTenant) { installScripts.createDefaultRuleChains(tenant.getId()); } + tenantProfileCache.evict(tenant.getId()); + tbClusterService.onTenantChange(tenant, null); + tbClusterService.onEntityStateChange(tenant.getId(), tenant.getId(), + newTenant ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); return tenant; } catch (Exception e) { throw handleException(e); @@ -91,8 +108,10 @@ public class TenantController extends BaseController { checkParameter("tenantId", strTenantId); try { TenantId tenantId = new TenantId(toUUID(strTenantId)); - checkTenantId(tenantId, Operation.DELETE); + Tenant tenant = checkTenantId(tenantId, Operation.DELETE); tenantService.deleteTenant(tenantId); + tenantProfileCache.evict(tenantId); + tbClusterService.onTenantDelete(tenant, null); tbClusterService.onEntityStateChange(tenantId, tenantId, ComponentLifecycleEvent.DELETED); } catch (Exception e) { throw handleException(e); @@ -100,18 +119,35 @@ public class TenantController extends BaseController { } @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/tenants", params = {"limit"}, method = RequestMethod.GET) + @RequestMapping(value = "/tenants", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody - public TextPageData getTenants(@RequestParam int limit, - @RequestParam(required = false) String textSearch, - @RequestParam(required = false) String idOffset, - @RequestParam(required = false) String textOffset) throws ThingsboardException { + public PageData getTenants(@RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { - TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return checkNotNull(tenantService.findTenants(pageLink)); } catch (Exception e) { throw handleException(e); } } + @PreAuthorize("hasAuthority('SYS_ADMIN')") + @RequestMapping(value = "/tenantInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getTenantInfos(@RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return checkNotNull(tenantService.findTenantInfos(pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java new file mode 100644 index 0000000000..0941fac725 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java @@ -0,0 +1,170 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.permission.Resource; + +@RestController +@TbCoreComponent +@RequestMapping("/api") +@Slf4j +public class TenantProfileController extends BaseController { + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @RequestMapping(value = "/tenantProfile/{tenantProfileId}", method = RequestMethod.GET) + @ResponseBody + public TenantProfile getTenantProfileById(@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException { + checkParameter("tenantProfileId", strTenantProfileId); + try { + TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId)); + return checkTenantProfileId(tenantProfileId, Operation.READ); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @RequestMapping(value = "/tenantProfileInfo/{tenantProfileId}", method = RequestMethod.GET) + @ResponseBody + public EntityInfo getTenantProfileInfoById(@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException { + checkParameter("tenantProfileId", strTenantProfileId); + try { + TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId)); + return checkNotNull(tenantProfileService.findTenantProfileInfoById(getTenantId(), tenantProfileId)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @RequestMapping(value = "/tenantProfileInfo/default", method = RequestMethod.GET) + @ResponseBody + public EntityInfo getDefaultTenantProfileInfo() throws ThingsboardException { + try { + return checkNotNull(tenantProfileService.findDefaultTenantProfileInfo(getTenantId())); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('SYS_ADMIN')") + @RequestMapping(value = "/tenantProfile", method = RequestMethod.POST) + @ResponseBody + public TenantProfile saveTenantProfile(@RequestBody TenantProfile tenantProfile) throws ThingsboardException { + try { + boolean newTenantProfile = tenantProfile.getId() == null; + if (newTenantProfile) { + accessControlService + .checkPermission(getCurrentUser(), Resource.TENANT_PROFILE, Operation.CREATE); + } else { + checkEntityId(tenantProfile.getId(), Operation.WRITE); + } + + tenantProfile = checkNotNull(tenantProfileService.saveTenantProfile(getTenantId(), tenantProfile)); + tenantProfileCache.put(tenantProfile); + tbClusterService.onTenantProfileChange(tenantProfile, null); + tbClusterService.onEntityStateChange(TenantId.SYS_TENANT_ID, tenantProfile.getId(), + newTenantProfile ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); + return tenantProfile; + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('SYS_ADMIN')") + @RequestMapping(value = "/tenantProfile/{tenantProfileId}", method = RequestMethod.DELETE) + @ResponseStatus(value = HttpStatus.OK) + public void deleteTenantProfile(@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException { + checkParameter("tenantProfileId", strTenantProfileId); + try { + TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId)); + TenantProfile profile = checkTenantProfileId(tenantProfileId, Operation.DELETE); + tenantProfileService.deleteTenantProfile(getTenantId(), tenantProfileId); + tbClusterService.onTenantProfileDelete(profile, null); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @RequestMapping(value = "/tenantProfile/{tenantProfileId}/default", method = RequestMethod.POST) + @ResponseBody + public TenantProfile setDefaultTenantProfile(@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException { + checkParameter("tenantProfileId", strTenantProfileId); + try { + TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId)); + TenantProfile tenantProfile = checkTenantProfileId(tenantProfileId, Operation.WRITE); + tenantProfileService.setDefaultTenantProfile(getTenantId(), tenantProfileId); + return tenantProfile; + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('SYS_ADMIN')") + @RequestMapping(value = "/tenantProfiles", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getTenantProfiles(@RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return checkNotNull(tenantProfileService.findTenantProfiles(getTenantId(), pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('SYS_ADMIN')") + @RequestMapping(value = "/tenantProfileInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getTenantProfileInfos(@RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return checkNotNull(tenantProfileService.findTenantProfileInfos(getTenantId(), pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java index d1868b2916..cc00850b4a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -40,8 +40,8 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -138,7 +138,8 @@ public class UserController extends BaseController { @RequestParam(required = false, defaultValue = "true") boolean sendActivationMail, HttpServletRequest request) throws ThingsboardException { try { - if (getCurrentUser().getAuthority() == Authority.TENANT_ADMIN) { + + if (Authority.TENANT_ADMIN.equals(getCurrentUser().getAuthority())) { user.setTenantId(getCurrentUser().getTenantId()); } @@ -249,19 +250,42 @@ public class UserController extends BaseController { } } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/users", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getUsers( + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + SecurityUser currentUser = getCurrentUser(); + if (Authority.TENANT_ADMIN.equals(currentUser.getAuthority())) { + return checkNotNull(userService.findUsersByTenantId(currentUser.getTenantId(), pageLink)); + } else { + return checkNotNull(userService.findCustomerUsers(currentUser.getTenantId(), currentUser.getCustomerId(), pageLink)); + } + } catch (Exception e) { + throw handleException(e); + } + } + @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/tenant/{tenantId}/users", params = {"limit"}, method = RequestMethod.GET) + @RequestMapping(value = "/tenant/{tenantId}/users", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody - public TextPageData getTenantAdmins( + public PageData getTenantAdmins( @PathVariable("tenantId") String strTenantId, - @RequestParam int limit, + @RequestParam int pageSize, + @RequestParam int page, @RequestParam(required = false) String textSearch, - @RequestParam(required = false) String idOffset, - @RequestParam(required = false) String textOffset) throws ThingsboardException { + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("tenantId", strTenantId); try { TenantId tenantId = new TenantId(toUUID(strTenantId)); - TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return checkNotNull(userService.findTenantAdmins(tenantId, pageLink)); } catch (Exception e) { throw handleException(e); @@ -269,19 +293,20 @@ public class UserController extends BaseController { } @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/customer/{customerId}/users", params = {"limit"}, method = RequestMethod.GET) + @RequestMapping(value = "/customer/{customerId}/users", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody - public TextPageData getCustomerUsers( + public PageData getCustomerUsers( @PathVariable("customerId") String strCustomerId, - @RequestParam int limit, + @RequestParam int pageSize, + @RequestParam int page, @RequestParam(required = false) String textSearch, - @RequestParam(required = false) String idOffset, - @RequestParam(required = false) String textOffset) throws ThingsboardException { + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("customerId", strCustomerId); try { CustomerId customerId = new CustomerId(toUUID(strCustomerId)); checkCustomerId(customerId, Operation.READ); - TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); TenantId tenantId = getCurrentUser().getTenantId(); return checkNotNull(userService.findCustomerUsers(tenantId, customerId, pageLink)); } catch (Exception e) { diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java index ffff90536f..2b7f88a512 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java @@ -60,7 +60,7 @@ public class WidgetTypeController extends BaseController { @ResponseBody public WidgetType saveWidgetType(@RequestBody WidgetType widgetType) throws ThingsboardException { try { - if (getCurrentUser().getAuthority() == Authority.SYS_ADMIN) { + if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) { widgetType.setTenantId(TenantId.SYS_TENANT_ID); } else { widgetType.setTenantId(getCurrentUser().getTenantId()); diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java index 1b6b371297..0eed69d50a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java @@ -28,8 +28,8 @@ import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetsBundleId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -61,7 +61,7 @@ public class WidgetsBundleController extends BaseController { @ResponseBody public WidgetsBundle saveWidgetsBundle(@RequestBody WidgetsBundle widgetsBundle) throws ThingsboardException { try { - if (getCurrentUser().getAuthority() == Authority.SYS_ADMIN) { + if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) { widgetsBundle.setTenantId(TenantId.SYS_TENANT_ID); } else { widgetsBundle.setTenantId(getCurrentUser().getTenantId()); @@ -89,16 +89,17 @@ public class WidgetsBundleController extends BaseController { } @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/widgetsBundles", params = {"limit"}, method = RequestMethod.GET) + @RequestMapping(value = "/widgetsBundles", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody - public TextPageData getWidgetsBundles( - @RequestParam int limit, + public PageData getWidgetsBundles( + @RequestParam int pageSize, + @RequestParam int page, @RequestParam(required = false) String textSearch, - @RequestParam(required = false) String idOffset, - @RequestParam(required = false) String textOffset) throws ThingsboardException { + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { - TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset); - if (getCurrentUser().getAuthority() == Authority.SYS_ADMIN) { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) { return checkNotNull(widgetsBundleService.findSystemWidgetsBundlesByPageLink(getTenantId(), pageLink)); } else { TenantId tenantId = getCurrentUser().getTenantId(); @@ -114,7 +115,7 @@ public class WidgetsBundleController extends BaseController { @ResponseBody public List getWidgetsBundles() throws ThingsboardException { try { - if (getCurrentUser().getAuthority() == Authority.SYS_ADMIN) { + if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) { return checkNotNull(widgetsBundleService.findSystemWidgetsBundles(getTenantId())); } else { TenantId tenantId = getCurrentUser().getTenantId(); diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 01ad8a29e9..c5afb06483 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -28,6 +28,9 @@ import org.thingsboard.server.service.install.DatabaseTsUpgradeService; import org.thingsboard.server.service.install.EntityDatabaseSchemaService; import org.thingsboard.server.service.install.SystemDataLoaderService; import org.thingsboard.server.service.install.TsDatabaseSchemaService; +import org.thingsboard.server.service.install.TsLatestDatabaseSchemaService; +import org.thingsboard.server.service.install.migrate.EntitiesMigrateService; +import org.thingsboard.server.service.install.migrate.TsLatestMigrateService; import org.thingsboard.server.service.install.update.DataUpdateService; @Service @@ -50,6 +53,9 @@ public class ThingsboardInstallService { @Autowired private TsDatabaseSchemaService tsDatabaseSchemaService; + @Autowired(required = false) + private TsLatestDatabaseSchemaService tsLatestDatabaseSchemaService; + @Autowired private DatabaseEntitiesUpgradeService databaseEntitiesUpgradeService; @@ -68,111 +74,122 @@ public class ThingsboardInstallService { @Autowired private DataUpdateService dataUpdateService; + @Autowired(required = false) + private EntitiesMigrateService entitiesMigrateService; + + @Autowired(required = false) + private TsLatestMigrateService latestMigrateService; + public void performInstall() { try { if (isUpgrade) { log.info("Starting ThingsBoard Upgrade from version {} ...", upgradeFromVersion); - switch (upgradeFromVersion) { - case "1.2.3": //NOSONAR, Need to execute gradual upgrade starting from upgradeFromVersion - log.info("Upgrading ThingsBoard from version 1.2.3 to 1.3.0 ..."); - - databaseEntitiesUpgradeService.upgradeDatabase("1.2.3"); - - case "1.3.0": //NOSONAR, Need to execute gradual upgrade starting from upgradeFromVersion - log.info("Upgrading ThingsBoard from version 1.3.0 to 1.3.1 ..."); - - databaseEntitiesUpgradeService.upgradeDatabase("1.3.0"); - - case "1.3.1": //NOSONAR, Need to execute gradual upgrade starting from upgradeFromVersion - log.info("Upgrading ThingsBoard from version 1.3.1 to 1.4.0 ..."); - - databaseEntitiesUpgradeService.upgradeDatabase("1.3.1"); - - case "1.4.0": - log.info("Upgrading ThingsBoard from version 1.4.0 to 2.0.0 ..."); - - databaseEntitiesUpgradeService.upgradeDatabase("1.4.0"); - - dataUpdateService.updateData("1.4.0"); - - case "2.0.0": - log.info("Upgrading ThingsBoard from version 2.0.0 to 2.1.1 ..."); - - databaseEntitiesUpgradeService.upgradeDatabase("2.0.0"); - - case "2.1.1": - log.info("Upgrading ThingsBoard from version 2.1.1 to 2.1.2 ..."); - - databaseEntitiesUpgradeService.upgradeDatabase("2.1.1"); - case "2.1.3": - log.info("Upgrading ThingsBoard from version 2.1.3 to 2.2.0 ..."); - - databaseEntitiesUpgradeService.upgradeDatabase("2.1.3"); - - case "2.3.0": - log.info("Upgrading ThingsBoard from version 2.3.0 to 2.3.1 ..."); - - databaseEntitiesUpgradeService.upgradeDatabase("2.3.0"); - - case "2.3.1": - log.info("Upgrading ThingsBoard from version 2.3.1 to 2.4.0 ..."); - - databaseEntitiesUpgradeService.upgradeDatabase("2.3.1"); - - case "2.4.0": - log.info("Upgrading ThingsBoard from version 2.4.0 to 2.4.1 ..."); - - case "2.4.1": - log.info("Upgrading ThingsBoard from version 2.4.1 to 2.4.2 ..."); - - databaseEntitiesUpgradeService.upgradeDatabase("2.4.1"); - case "2.4.2": - log.info("Upgrading ThingsBoard from version 2.4.2 to 2.4.3 ..."); - - databaseEntitiesUpgradeService.upgradeDatabase("2.4.2"); - - case "2.4.3": - log.info("Upgrading ThingsBoard from version 2.4.3 to 2.5.0 ..."); - - if (databaseTsUpgradeService != null) { - databaseTsUpgradeService.upgradeDatabase("2.4.3"); - } - databaseEntitiesUpgradeService.upgradeDatabase("2.4.3"); - - case "2.5.0": - log.info("Upgrading ThingsBoard from version 2.5.0 to 2.5.1 ..."); - if (databaseTsUpgradeService != null) { - databaseTsUpgradeService.upgradeDatabase("2.5.0"); - } - - case "2.5.4": - log.info("Upgrading ThingsBoard from version 2.5.4 to 2.5.5 ..."); - if (databaseTsUpgradeService != null) { - databaseTsUpgradeService.upgradeDatabase("2.5.4"); - } - - log.info("Updating system data..."); - - systemDataLoaderService.deleteSystemWidgetBundle("charts"); - systemDataLoaderService.deleteSystemWidgetBundle("cards"); - systemDataLoaderService.deleteSystemWidgetBundle("maps"); - systemDataLoaderService.deleteSystemWidgetBundle("analogue_gauges"); - systemDataLoaderService.deleteSystemWidgetBundle("digital_gauges"); - systemDataLoaderService.deleteSystemWidgetBundle("gpio_widgets"); - systemDataLoaderService.deleteSystemWidgetBundle("alarm_widgets"); - systemDataLoaderService.deleteSystemWidgetBundle("control_widgets"); - systemDataLoaderService.deleteSystemWidgetBundle("maps_v2"); - systemDataLoaderService.deleteSystemWidgetBundle("gateway_widgets"); - systemDataLoaderService.deleteSystemWidgetBundle("input_widgets"); - systemDataLoaderService.deleteSystemWidgetBundle("date"); - systemDataLoaderService.deleteSystemWidgetBundle("entity_admin_widgets"); - - systemDataLoaderService.loadSystemWidgets(); - break; - default: - throw new RuntimeException("Unable to upgrade ThingsBoard, unsupported fromVersion: " + upgradeFromVersion); - + if ("2.5.0-cassandra".equals(upgradeFromVersion)) { + log.info("Migrating ThingsBoard entities data from cassandra to SQL database ..."); + entitiesMigrateService.migrate(); + log.info("Updating system data..."); + systemDataLoaderService.updateSystemWidgets(); + } else if ("3.0.1-cassandra".equals(upgradeFromVersion)) { + log.info("Migrating ThingsBoard latest timeseries data from cassandra to SQL database ..."); + latestMigrateService.migrate(); + log.info("Updating system data..."); + } else { + switch (upgradeFromVersion) { + case "1.2.3": //NOSONAR, Need to execute gradual upgrade starting from upgradeFromVersion + log.info("Upgrading ThingsBoard from version 1.2.3 to 1.3.0 ..."); + + databaseEntitiesUpgradeService.upgradeDatabase("1.2.3"); + + case "1.3.0": //NOSONAR, Need to execute gradual upgrade starting from upgradeFromVersion + log.info("Upgrading ThingsBoard from version 1.3.0 to 1.3.1 ..."); + + databaseEntitiesUpgradeService.upgradeDatabase("1.3.0"); + + case "1.3.1": //NOSONAR, Need to execute gradual upgrade starting from upgradeFromVersion + log.info("Upgrading ThingsBoard from version 1.3.1 to 1.4.0 ..."); + + databaseEntitiesUpgradeService.upgradeDatabase("1.3.1"); + + case "1.4.0": + log.info("Upgrading ThingsBoard from version 1.4.0 to 2.0.0 ..."); + + databaseEntitiesUpgradeService.upgradeDatabase("1.4.0"); + + dataUpdateService.updateData("1.4.0"); + + case "2.0.0": + log.info("Upgrading ThingsBoard from version 2.0.0 to 2.1.1 ..."); + + databaseEntitiesUpgradeService.upgradeDatabase("2.0.0"); + + case "2.1.1": + log.info("Upgrading ThingsBoard from version 2.1.1 to 2.1.2 ..."); + + databaseEntitiesUpgradeService.upgradeDatabase("2.1.1"); + case "2.1.3": + log.info("Upgrading ThingsBoard from version 2.1.3 to 2.2.0 ..."); + + databaseEntitiesUpgradeService.upgradeDatabase("2.1.3"); + + case "2.3.0": + log.info("Upgrading ThingsBoard from version 2.3.0 to 2.3.1 ..."); + + databaseEntitiesUpgradeService.upgradeDatabase("2.3.0"); + + case "2.3.1": + log.info("Upgrading ThingsBoard from version 2.3.1 to 2.4.0 ..."); + + databaseEntitiesUpgradeService.upgradeDatabase("2.3.1"); + + case "2.4.0": + log.info("Upgrading ThingsBoard from version 2.4.0 to 2.4.1 ..."); + + case "2.4.1": + log.info("Upgrading ThingsBoard from version 2.4.1 to 2.4.2 ..."); + + databaseEntitiesUpgradeService.upgradeDatabase("2.4.1"); + case "2.4.2": + log.info("Upgrading ThingsBoard from version 2.4.2 to 2.4.3 ..."); + + databaseEntitiesUpgradeService.upgradeDatabase("2.4.2"); + + case "2.4.3": + log.info("Upgrading ThingsBoard from version 2.4.3 to 2.5 ..."); + + if (databaseTsUpgradeService != null) { + databaseTsUpgradeService.upgradeDatabase("2.4.3"); + } + databaseEntitiesUpgradeService.upgradeDatabase("2.4.3"); + case "2.5.0": + log.info("Upgrading ThingsBoard from version 2.5.0 to 2.5.1 ..."); + if (databaseTsUpgradeService != null) { + databaseTsUpgradeService.upgradeDatabase("2.5.0"); + } + case "2.5.1": + log.info("Upgrading ThingsBoard from version 2.5.1 to 3.0.0 ..."); + case "3.0.1": + log.info("Upgrading ThingsBoard from version 3.0.1 to 3.1.0 ..."); + databaseEntitiesUpgradeService.upgradeDatabase("3.0.1"); + dataUpdateService.updateData("3.0.1"); + case "3.1.0": + log.info("Upgrading ThingsBoard from version 3.1.0 to 3.1.1 ..."); + databaseEntitiesUpgradeService.upgradeDatabase("3.1.0"); + case "3.1.1": + log.info("Upgrading ThingsBoard from version 3.1.1 to 3.2.0 ..."); + if (databaseTsUpgradeService != null) { + databaseTsUpgradeService.upgradeDatabase("3.1.1"); + } + databaseEntitiesUpgradeService.upgradeDatabase("3.1.1"); + dataUpdateService.updateData("3.1.1"); + log.info("Updating system data..."); + systemDataLoaderService.updateSystemWidgets(); + systemDataLoaderService.createOAuth2Templates(); + break; + default: + throw new RuntimeException("Unable to upgrade ThingsBoard, unsupported fromVersion: " + upgradeFromVersion); + + } } log.info("Upgrade finished successfully!"); @@ -188,13 +205,19 @@ public class ThingsboardInstallService { tsDatabaseSchemaService.createDatabaseSchema(); + if (tsLatestDatabaseSchemaService != null) { + tsLatestDatabaseSchemaService.createDatabaseSchema(); + } + log.info("Loading system data..."); componentDiscoveryService.discoverComponents(); systemDataLoaderService.createSysAdmin(); + systemDataLoaderService.createDefaultTenantProfiles(); systemDataLoaderService.createAdminSettings(); systemDataLoaderService.loadSystemWidgets(); + systemDataLoaderService.createOAuth2Templates(); // systemDataLoaderService.loadSystemPlugins(); // systemDataLoaderService.loadSystemRules(); diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java new file mode 100644 index 0000000000..79f05fa897 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java @@ -0,0 +1,368 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.apiusage; + +import com.google.common.util.concurrent.FutureCallback; +import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.annotation.Lazy; +import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; +import org.springframework.data.util.Pair; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.ApiFeature; +import org.thingsboard.server.common.data.ApiUsageRecordKey; +import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.ApiUsageStateValue; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.ApiUsageStateId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.page.PageDataIterable; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileConfiguration; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.common.msg.tools.SchedulerUtils; +import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.dao.timeseries.TimeseriesService; +import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.UsageStatsKVProto; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.discovery.PartitionChangeEvent; +import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.scheduler.SchedulerComponent; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import org.thingsboard.server.service.queue.TbClusterService; +import org.thingsboard.server.service.telemetry.InternalTelemetryService; + +import javax.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@Slf4j +@Service +public class DefaultTbApiUsageStateService implements TbApiUsageStateService { + + public static final String HOURLY = "Hourly"; + public static final FutureCallback VOID_CALLBACK = new FutureCallback() { + @Override + public void onSuccess(@Nullable Integer result) { + } + + @Override + public void onFailure(Throwable t) { + } + }; + private final TbClusterService clusterService; + private final PartitionService partitionService; + private final TenantService tenantService; + private final TimeseriesService tsService; + private final ApiUsageStateService apiUsageStateService; + private final SchedulerComponent scheduler; + private final TbTenantProfileCache tenantProfileCache; + + @Lazy + @Autowired + private InternalTelemetryService tsWsService; + + // Tenants that should be processed on this server + private final Map myTenantStates = new ConcurrentHashMap<>(); + // Tenants that should be processed on other servers + private final Map otherTenantStates = new ConcurrentHashMap<>(); + + @Value("${usage.stats.report.enabled:true}") + private boolean enabled; + + @Value("${usage.stats.check.cycle:60000}") + private long nextCycleCheckInterval; + + private final Lock updateLock = new ReentrantLock(); + + public DefaultTbApiUsageStateService(TbClusterService clusterService, + PartitionService partitionService, + TenantService tenantService, + TimeseriesService tsService, + ApiUsageStateService apiUsageStateService, + SchedulerComponent scheduler, + TbTenantProfileCache tenantProfileCache) { + this.clusterService = clusterService; + this.partitionService = partitionService; + this.tenantService = tenantService; + this.tsService = tsService; + this.apiUsageStateService = apiUsageStateService; + this.scheduler = scheduler; + this.tenantProfileCache = tenantProfileCache; + } + + @PostConstruct + public void init() { + if (enabled) { + log.info("Starting api usage service."); + scheduler.scheduleAtFixedRate(this::checkStartOfNextCycle, nextCycleCheckInterval, nextCycleCheckInterval, TimeUnit.MILLISECONDS); + log.info("Started api usage service."); + } + } + + @Override + public void process(TbProtoQueueMsg msg, TbCallback callback) { + ToUsageStatsServiceMsg statsMsg = msg.getValue(); + TenantId tenantId = new TenantId(new UUID(statsMsg.getTenantIdMSB(), statsMsg.getTenantIdLSB())); + TenantApiUsageState tenantState; + List updatedEntries; + Map result; + updateLock.lock(); + try { + tenantState = getOrFetchState(tenantId); + long ts = tenantState.getCurrentCycleTs(); + long hourTs = tenantState.getCurrentHourTs(); + long newHourTs = SchedulerUtils.getStartOfCurrentHour(); + if (newHourTs != hourTs) { + tenantState.setHour(newHourTs); + } + updatedEntries = new ArrayList<>(ApiUsageRecordKey.values().length); + Set apiFeatures = new HashSet<>(); + for (UsageStatsKVProto kvProto : statsMsg.getValuesList()) { + ApiUsageRecordKey recordKey = ApiUsageRecordKey.valueOf(kvProto.getKey()); + long newValue = tenantState.add(recordKey, kvProto.getValue()); + updatedEntries.add(new BasicTsKvEntry(ts, new LongDataEntry(recordKey.getApiCountKey(), newValue))); + long newHourlyValue = tenantState.addToHourly(recordKey, kvProto.getValue()); + updatedEntries.add(new BasicTsKvEntry(hourTs, new LongDataEntry(recordKey.getApiCountKey() + HOURLY, newHourlyValue))); + apiFeatures.add(recordKey.getApiFeature()); + } + result = tenantState.checkStateUpdatedDueToThreshold(apiFeatures); + } finally { + updateLock.unlock(); + } + tsWsService.saveAndNotifyInternal(tenantId, tenantState.getApiUsageState().getId(), updatedEntries, VOID_CALLBACK); + if (!result.isEmpty()) { + persistAndNotify(tenantState, result); + } + callback.onSuccess(); + } + + @Override + public void onApplicationEvent(PartitionChangeEvent partitionChangeEvent) { + if (partitionChangeEvent.getServiceType().equals(ServiceType.TB_CORE)) { + myTenantStates.entrySet().removeIf(entry -> !partitionService.resolve(ServiceType.TB_CORE, entry.getKey(), entry.getKey()).isMyPartition()); + otherTenantStates.entrySet().removeIf(entry -> partitionService.resolve(ServiceType.TB_CORE, entry.getKey(), entry.getKey()).isMyPartition()); + initStatesFromDataBase(); + } + } + + @Override + public ApiUsageState getApiUsageState(TenantId tenantId) { + TenantApiUsageState tenantState = myTenantStates.get(tenantId); + if (tenantState != null) { + return tenantState.getApiUsageState(); + } else { + ApiUsageState state = otherTenantStates.get(tenantId); + if (state != null) { + return state; + } else { + if (partitionService.resolve(ServiceType.TB_CORE, tenantId, tenantId).isMyPartition()) { + return getOrFetchState(tenantId).getApiUsageState(); + } else { + updateLock.lock(); + try { + state = otherTenantStates.get(tenantId); + if (state == null) { + state = apiUsageStateService.findTenantApiUsageState(tenantId); + if (state != null) { + otherTenantStates.put(tenantId, state); + } + } + } finally { + updateLock.unlock(); + } + return state; + } + } + } + } + + @Override + public void onApiUsageStateUpdate(TenantId tenantId) { + otherTenantStates.remove(tenantId); + } + + @Override + public void onTenantProfileUpdate(TenantProfileId tenantProfileId) { + TenantProfile tenantProfile = tenantProfileCache.get(tenantProfileId); + updateLock.lock(); + try { + myTenantStates.values().forEach(state -> { + if (tenantProfile.getId().equals(state.getTenantProfileId())) { + updateTenantState(state, tenantProfile); + } + }); + } finally { + updateLock.unlock(); + } + } + + @Override + public void onTenantUpdate(TenantId tenantId) { + TenantProfile tenantProfile = tenantProfileCache.get(tenantId); + updateLock.lock(); + try { + TenantApiUsageState state = myTenantStates.get(tenantId); + if (state != null && !state.getTenantProfileId().equals(tenantProfile.getId())) { + updateTenantState(state, tenantProfile); + } + } finally { + updateLock.unlock(); + } + } + + private void updateTenantState(TenantApiUsageState state, TenantProfile tenantProfile) { + TenantProfileData oldProfileData = state.getTenantProfileData(); + state.setTenantProfileId(tenantProfile.getId()); + state.setTenantProfileData(tenantProfile.getProfileData()); + Map result = state.checkStateUpdatedDueToThresholds(); + if (!result.isEmpty()) { + persistAndNotify(state, result); + } + updateProfileThresholds(state.getTenantId(), state.getApiUsageState().getId(), + oldProfileData.getConfiguration(), tenantProfile.getProfileData().getConfiguration()); + } + + private void updateProfileThresholds(TenantId tenantId, ApiUsageStateId id, + TenantProfileConfiguration oldData, TenantProfileConfiguration newData) { + long ts = System.currentTimeMillis(); + List profileThresholds = new ArrayList<>(); + for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { + long newProfileThreshold = newData.getProfileThreshold(key); + if (oldData == null || oldData.getProfileThreshold(key) != newProfileThreshold) { + log.info("[{}] Updating profile threshold [{}]:[{}]", tenantId, key, newProfileThreshold); + profileThresholds.add(new BasicTsKvEntry(ts, new LongDataEntry(key.getApiLimitKey(), newProfileThreshold))); + } + } + if (!profileThresholds.isEmpty()) { + tsWsService.saveAndNotifyInternal(tenantId, id, profileThresholds, VOID_CALLBACK); + } + } + + private void persistAndNotify(TenantApiUsageState state, Map result) { + log.info("[{}] Detected update of the API state: {}", state.getTenantId(), result); + apiUsageStateService.update(state.getApiUsageState()); + clusterService.onApiStateChange(state.getApiUsageState(), null); + long ts = System.currentTimeMillis(); + List stateTelemetry = new ArrayList<>(); + result.forEach(((apiFeature, aState) -> stateTelemetry.add(new BasicTsKvEntry(ts, new StringDataEntry(apiFeature.getApiStateKey(), aState.name()))))); + tsWsService.saveAndNotifyInternal(state.getTenantId(), state.getApiUsageState().getId(), stateTelemetry, VOID_CALLBACK); + //TODO: notify tenant admin via email! + } + + private void checkStartOfNextCycle() { + updateLock.lock(); + try { + long now = System.currentTimeMillis(); + myTenantStates.values().forEach(state -> { + if ((state.getNextCycleTs() > now) && (state.getNextCycleTs() - now < TimeUnit.HOURS.toMillis(1))) { + state.setCycles(state.getNextCycleTs(), SchedulerUtils.getStartOfNextNextMonth()); + } + }); + } finally { + updateLock.unlock(); + } + } + + private TenantApiUsageState getOrFetchState(TenantId tenantId) { + TenantApiUsageState tenantState = myTenantStates.get(tenantId); + if (tenantState == null) { + ApiUsageState dbStateEntity = apiUsageStateService.findTenantApiUsageState(tenantId); + if (dbStateEntity == null) { + try { + dbStateEntity = apiUsageStateService.createDefaultApiUsageState(tenantId); + } catch (Exception e) { + dbStateEntity = apiUsageStateService.findTenantApiUsageState(tenantId); + } + } + TenantProfile tenantProfile = tenantProfileCache.get(tenantId); + tenantState = new TenantApiUsageState(tenantProfile, dbStateEntity); + try { + List dbValues = tsService.findAllLatest(tenantId, dbStateEntity.getId()).get(); + for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { + boolean cycleEntryFound = false; + boolean hourlyEntryFound = false; + for (TsKvEntry tsKvEntry : dbValues) { + if (tsKvEntry.getKey().equals(key.getApiCountKey())) { + cycleEntryFound = true; + tenantState.put(key, tsKvEntry.getTs() == tenantState.getCurrentCycleTs() ? tsKvEntry.getLongValue().get() : 0L); + } else if (tsKvEntry.getKey().equals(key.getApiCountKey() + HOURLY)) { + hourlyEntryFound = true; + tenantState.putHourly(key, tsKvEntry.getTs() == tenantState.getCurrentHourTs() ? tsKvEntry.getLongValue().get() : 0L); + } + if (cycleEntryFound && hourlyEntryFound) { + break; + } + } + } + log.debug("[{}] Initialized state: {}", tenantId, dbStateEntity); + myTenantStates.put(tenantId, tenantState); + } catch (InterruptedException | ExecutionException e) { + log.warn("[{}] Failed to fetch api usage state from db.", tenantId, e); + } + } + return tenantState; + } + + private void initStatesFromDataBase() { + try { + log.info("Initializing tenant states."); + PageDataIterable tenantIterator = new PageDataIterable<>(tenantService::findTenants, 1024); + for (Tenant tenant : tenantIterator) { + if (!myTenantStates.containsKey(tenant.getId()) && partitionService.resolve(ServiceType.TB_CORE, tenant.getId(), tenant.getId()).isMyPartition()) { + log.debug("[{}] Initializing tenant state.", tenant.getId()); + updateLock.lock(); + try { + updateTenantState(getOrFetchState(tenant.getId()), tenantProfileCache.get(tenant.getTenantProfileId())); + log.debug("[{}] Initialized tenant state.", tenant.getId()); + } catch (Exception e) { + log.warn("[{}] Failed to initialize tenant API state", tenant.getId(), e); + } finally { + updateLock.unlock(); + } + } + } + log.info("Initialized tenant states."); + } catch (Exception e) { + log.warn("Unknown failure", e); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/TbApiUsageStateService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/TbApiUsageStateService.java new file mode 100644 index 0000000000..e19c0a4154 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/TbApiUsageStateService.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.apiusage; + +import org.springframework.context.ApplicationListener; +import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.discovery.PartitionChangeEvent; + +public interface TbApiUsageStateService extends ApplicationListener { + + void process(TbProtoQueueMsg msg, TbCallback callback); + + ApiUsageState getApiUsageState(TenantId tenantId); + + void onTenantProfileUpdate(TenantProfileId tenantProfileId); + + void onTenantUpdate(TenantId tenantId); + + void onApiUsageStateUpdate(TenantId tenantId); +} diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/TenantApiUsageState.java b/application/src/main/java/org/thingsboard/server/service/apiusage/TenantApiUsageState.java new file mode 100644 index 0000000000..a43afcd2c5 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/TenantApiUsageState.java @@ -0,0 +1,186 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.apiusage; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.data.util.Pair; +import org.thingsboard.server.common.data.ApiFeature; +import org.thingsboard.server.common.data.ApiUsageRecordKey; +import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.ApiUsageStateValue; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; +import org.thingsboard.server.common.msg.tools.SchedulerUtils; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class TenantApiUsageState { + + private final Map currentCycleValues = new ConcurrentHashMap<>(); + private final Map currentHourValues = new ConcurrentHashMap<>(); + + @Getter + @Setter + private TenantProfileId tenantProfileId; + @Getter + @Setter + private TenantProfileData tenantProfileData; + @Getter + private final ApiUsageState apiUsageState; + @Getter + private volatile long currentCycleTs; + @Getter + private volatile long nextCycleTs; + @Getter + private volatile long currentHourTs; + + public TenantApiUsageState(TenantProfile tenantProfile, ApiUsageState apiUsageState) { + this.tenantProfileId = tenantProfile.getId(); + this.tenantProfileData = tenantProfile.getProfileData(); + this.apiUsageState = apiUsageState; + this.currentCycleTs = SchedulerUtils.getStartOfCurrentMonth(); + this.nextCycleTs = SchedulerUtils.getStartOfNextMonth(); + this.currentHourTs = SchedulerUtils.getStartOfCurrentHour(); + } + + public void put(ApiUsageRecordKey key, Long value) { + currentCycleValues.put(key, value); + } + + public void putHourly(ApiUsageRecordKey key, Long value) { + currentHourValues.put(key, value); + } + + public long add(ApiUsageRecordKey key, long value) { + long result = currentCycleValues.getOrDefault(key, 0L) + value; + currentCycleValues.put(key, result); + return result; + } + + public long get(ApiUsageRecordKey key) { + return currentCycleValues.getOrDefault(key, 0L); + } + + public long addToHourly(ApiUsageRecordKey key, long value) { + long result = currentHourValues.getOrDefault(key, 0L) + value; + currentHourValues.put(key, result); + return result; + } + + public void setHour(long currentHourTs) { + this.currentHourTs = currentHourTs; + for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { + currentHourValues.put(key, 0L); + } + } + + public void setCycles(long currentCycleTs, long nextCycleTs) { + this.currentCycleTs = currentCycleTs; + this.nextCycleTs = nextCycleTs; + for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { + currentCycleValues.put(key, 0L); + } + } + + public long getProfileThreshold(ApiUsageRecordKey key) { + return tenantProfileData.getConfiguration().getProfileThreshold(key); + } + + public long getProfileWarnThreshold(ApiUsageRecordKey key) { + return tenantProfileData.getConfiguration().getWarnThreshold(key); + } + + public TenantId getTenantId() { + return apiUsageState.getTenantId(); + } + + public ApiUsageStateValue getFeatureValue(ApiFeature feature) { + switch (feature) { + case TRANSPORT: + return apiUsageState.getTransportState(); + case RE: + return apiUsageState.getReExecState(); + case DB: + return apiUsageState.getDbStorageState(); + case JS: + return apiUsageState.getJsExecState(); + default: + return ApiUsageStateValue.ENABLED; + } + } + + public boolean setFeatureValue(ApiFeature feature, ApiUsageStateValue value) { + ApiUsageStateValue currentValue = getFeatureValue(feature); + switch (feature) { + case TRANSPORT: + apiUsageState.setTransportState(value); + break; + case RE: + apiUsageState.setReExecState(value); + break; + case DB: + apiUsageState.setDbStorageState(value); + break; + case JS: + apiUsageState.setJsExecState(value); + break; + } + return !currentValue.equals(value); + } + + public Map checkStateUpdatedDueToThresholds() { + return checkStateUpdatedDueToThreshold(new HashSet<>(Arrays.asList(ApiFeature.values()))); + } + + public Map checkStateUpdatedDueToThreshold(Set features) { + Map result = new HashMap<>(); + for (ApiFeature feature : features) { + Pair tmp = checkStateUpdatedDueToThreshold(feature); + if (tmp != null) { + result.put(tmp.getFirst(), tmp.getSecond()); + } + } + return result; + } + + public Pair checkStateUpdatedDueToThreshold(ApiFeature feature) { + ApiUsageStateValue featureValue = ApiUsageStateValue.ENABLED; + for (ApiUsageRecordKey recordKey : ApiUsageRecordKey.getKeys(feature)) { + long value = get(recordKey); + long threshold = getProfileThreshold(recordKey); + long warnThreshold = getProfileWarnThreshold(recordKey); + ApiUsageStateValue tmpValue; + if (threshold == 0 || value < warnThreshold) { + tmpValue = ApiUsageStateValue.ENABLED; + } else if (value < threshold) { + tmpValue = ApiUsageStateValue.WARNING; + } else { + tmpValue = ApiUsageStateValue.DISABLED; + } + featureValue = ApiUsageStateValue.toMoreRestricted(featureValue, tmpValue); + } + return setFeatureValue(feature, featureValue) ? Pair.of(feature, featureValue) : null; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java new file mode 100644 index 0000000000..d372b878f7 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java @@ -0,0 +1,257 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.device; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.audit.AuditLogService; +import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceDao; +import org.thingsboard.server.dao.device.DeviceProfileDao; +import org.thingsboard.server.dao.device.DeviceProvisionService; +import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.device.provision.ProvisionFailedException; +import org.thingsboard.server.dao.device.provision.ProvisionRequest; +import org.thingsboard.server.dao.device.provision.ProvisionResponse; +import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; +import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueProducer; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.provider.TbQueueProducerProvider; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.state.DeviceStateService; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.locks.ReentrantLock; + + +@Service +@Slf4j +@TbCoreComponent +public class DeviceProvisionServiceImpl implements DeviceProvisionService { + + protected TbQueueProducer> ruleEngineMsgProducer; + + private static final String DEVICE_PROVISION_STATE = "provisionState"; + private static final String PROVISIONED_STATE = "provisioned"; + + private final ReentrantLock deviceCreationLock = new ReentrantLock(); + + @Autowired + DeviceDao deviceDao; + + @Autowired + DeviceProfileDao deviceProfileDao; + + @Autowired + DeviceService deviceService; + + @Autowired + DeviceCredentialsService deviceCredentialsService; + + @Autowired + AttributesService attributesService; + + @Autowired + DeviceStateService deviceStateService; + + @Autowired + AuditLogService auditLogService; + + @Autowired + PartitionService partitionService; + + public DeviceProvisionServiceImpl(TbQueueProducerProvider producerProvider) { + ruleEngineMsgProducer = producerProvider.getRuleEngineMsgProducer(); + } + + @Override + public ProvisionResponse provisionDevice(ProvisionRequest provisionRequest) { + String provisionRequestKey = provisionRequest.getCredentials().getProvisionDeviceKey(); + String provisionRequestSecret = provisionRequest.getCredentials().getProvisionDeviceSecret(); + + if (StringUtils.isEmpty(provisionRequestKey) || StringUtils.isEmpty(provisionRequestSecret)) { + throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name()); + } + + DeviceProfile targetProfile = deviceProfileDao.findByProvisionDeviceKey(provisionRequestKey); + + if (targetProfile == null || targetProfile.getProfileData().getProvisionConfiguration() == null || + targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret() == null) { + throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name()); + } + + Device targetDevice = deviceDao.findDeviceByTenantIdAndName(targetProfile.getTenantId().getId(), provisionRequest.getDeviceName()).orElse(null); + + switch (targetProfile.getProvisionType()) { + case ALLOW_CREATE_NEW_DEVICES: + if (targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret().equals(provisionRequestSecret)) { + if (targetDevice != null) { + log.warn("[{}] The device is present and could not be provisioned once more!", targetDevice.getName()); + notify(targetDevice, provisionRequest, DataConstants.PROVISION_FAILURE, false); + throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); + } else { + return createDevice(provisionRequest, targetProfile); + } + } + break; + case CHECK_PRE_PROVISIONED_DEVICES: + if (targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret().equals(provisionRequestSecret)) { + if (targetDevice != null && targetDevice.getDeviceProfileId().equals(targetProfile.getId())) { + return processProvision(targetDevice, provisionRequest); + } else { + log.warn("[{}] Failed to find pre provisioned device!", provisionRequest.getDeviceName()); + throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); + } + } + break; + } + throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name()); + } + + private ProvisionResponse processProvision(Device device, ProvisionRequest provisionRequest) { + try { + Optional provisionState = attributesService.find(device.getTenantId(), device.getId(), + DataConstants.SERVER_SCOPE, DEVICE_PROVISION_STATE).get(); + if (provisionState != null && provisionState.isPresent() && !provisionState.get().getValueAsString().equals(PROVISIONED_STATE)) { + notify(device, provisionRequest, DataConstants.PROVISION_FAILURE, false); + throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); + } else { + saveProvisionStateAttribute(device).get(); + notify(device, provisionRequest, DataConstants.PROVISION_SUCCESS, true); + } + } catch (InterruptedException | ExecutionException e) { + throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); + } + return new ProvisionResponse(deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), device.getId()), ProvisionResponseStatus.SUCCESS); + } + + private ProvisionResponse createDevice(ProvisionRequest provisionRequest, DeviceProfile profile) { + deviceCreationLock.lock(); + try { + return processCreateDevice(provisionRequest, profile); + } finally { + deviceCreationLock.unlock(); + } + } + + private void notify(Device device, ProvisionRequest provisionRequest, String type, boolean success) { + pushProvisionEventToRuleEngine(provisionRequest, device, type); + logAction(device.getTenantId(), device.getCustomerId(), device, success, provisionRequest); + } + + private ProvisionResponse processCreateDevice(ProvisionRequest provisionRequest, DeviceProfile profile) { + Device device = deviceService.findDeviceByTenantIdAndName(profile.getTenantId(), provisionRequest.getDeviceName()); + try { + if (device == null) { + Device savedDevice = deviceService.saveDevice(provisionRequest, profile); + + deviceStateService.onDeviceAdded(savedDevice); + saveProvisionStateAttribute(savedDevice).get(); + pushDeviceCreatedEventToRuleEngine(savedDevice); + notify(savedDevice, provisionRequest, DataConstants.PROVISION_SUCCESS, true); + + return new ProvisionResponse(getDeviceCredentials(savedDevice), ProvisionResponseStatus.SUCCESS); + } else { + log.warn("[{}] The device is already provisioned!", device.getName()); + notify(device, provisionRequest, DataConstants.PROVISION_FAILURE, false); + throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); + } + } catch (InterruptedException | ExecutionException e) { + throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); + } + } + + private ListenableFuture> saveProvisionStateAttribute(Device device) { + return attributesService.save(device.getTenantId(), device.getId(), DataConstants.SERVER_SCOPE, + Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry(DEVICE_PROVISION_STATE, PROVISIONED_STATE), + System.currentTimeMillis()))); + } + + private DeviceCredentials getDeviceCredentials(Device device) { + return deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), device.getId()); + } + + private void pushProvisionEventToRuleEngine(ProvisionRequest request, Device device, String type) { + try { + JsonNode entityNode = JacksonUtil.valueToTree(request); + TbMsg msg = TbMsg.newMsg(type, device.getId(), createTbMsgMetaData(device), JacksonUtil.toString(entityNode)); + sendToRuleEngine(device.getTenantId(), msg, null); + } catch (IllegalArgumentException e) { + log.warn("[{}] Failed to push device action to rule engine: {}", device.getId(), type, e); + } + } + + private void pushDeviceCreatedEventToRuleEngine(Device device) { + try { + ObjectNode entityNode = JacksonUtil.OBJECT_MAPPER.valueToTree(device); + TbMsg msg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, device.getId(), createTbMsgMetaData(device), JacksonUtil.OBJECT_MAPPER.writeValueAsString(entityNode)); + sendToRuleEngine(device.getTenantId(), msg, null); + } catch (JsonProcessingException | IllegalArgumentException e) { + log.warn("[{}] Failed to push device action to rule engine: {}", device.getId(), DataConstants.ENTITY_CREATED, e); + } + } + + protected void sendToRuleEngine(TenantId tenantId, TbMsg tbMsg, TbQueueCallback callback) { + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tenantId, tbMsg.getOriginator()); + TransportProtos.ToRuleEngineMsg msg = TransportProtos.ToRuleEngineMsg.newBuilder().setTbMsg(TbMsg.toByteString(tbMsg)) + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()).build(); + ruleEngineMsgProducer.send(tpi, new TbProtoQueueMsg<>(tbMsg.getId(), msg), callback); + } + + private TbMsgMetaData createTbMsgMetaData(Device device) { + TbMsgMetaData metaData = new TbMsgMetaData(); + metaData.putValue("tenantId", device.getTenantId().toString()); + return metaData; + } + + private void logAction(TenantId tenantId, CustomerId customerId, Device device, boolean success, ProvisionRequest provisionRequest) { + ActionType actionType = success ? ActionType.PROVISION_SUCCESS : ActionType.PROVISION_FAILURE; + auditLogService.logEntityAction(tenantId, customerId, new UserId(UserId.NULL_UUID), device.getName(), device.getId(), device, actionType, null, provisionRequest); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/encoding/DataDecodingEncodingService.java b/application/src/main/java/org/thingsboard/server/service/encoding/DataDecodingEncodingService.java deleted file mode 100644 index 4a781b8673..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/encoding/DataDecodingEncodingService.java +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.encoding; - -import org.thingsboard.server.common.msg.TbActorMsg; - -import java.util.Optional; - -public interface DataDecodingEncodingService { - - Optional decode(byte[] byteArray); - - byte[] encode(TbActorMsg msq); - -} - diff --git a/application/src/main/java/org/thingsboard/server/service/encoding/ProtoWithFSTService.java b/application/src/main/java/org/thingsboard/server/service/encoding/ProtoWithFSTService.java deleted file mode 100644 index 8d89059488..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/encoding/ProtoWithFSTService.java +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.encoding; - -import lombok.extern.slf4j.Slf4j; -import org.nustaq.serialization.FSTConfiguration; -import org.springframework.stereotype.Service; -import org.thingsboard.server.common.msg.TbActorMsg; - -import java.util.Optional; - -@Slf4j -@Service -public class ProtoWithFSTService implements DataDecodingEncodingService { - - private final FSTConfiguration config = FSTConfiguration.createDefaultConfiguration(); - - @Override - public Optional decode(byte[] byteArray) { - try { - TbActorMsg msg = (TbActorMsg) config.asObject(byteArray); - return Optional.of(msg); - - } catch (IllegalArgumentException e) { - log.error("Error during deserialization message, [{}]", e.getMessage()); - return Optional.empty(); - } - } - - @Override - public byte[] encode(TbActorMsg msq) { - return config.asByteArray(msq); - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraAbstractDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraAbstractDatabaseSchemaService.java index 3456a94859..d5b30e0873 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/CassandraAbstractDatabaseSchemaService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraAbstractDatabaseSchemaService.java @@ -45,10 +45,18 @@ public abstract class CassandraAbstractDatabaseSchemaService implements Database @Override public void createDatabaseSchema() throws Exception { + this.createDatabaseSchema(true); + } + + @Override + public void createDatabaseSchema(boolean createIndexes) throws Exception { log.info("Installing Cassandra DataBase schema part: " + schemaCql); Path schemaFile = Paths.get(installScripts.getDataDir(), CASSANDRA_DIR, schemaCql); loadCql(schemaFile); + } + @Override + public void createDatabaseIndexes() throws Exception { } private void loadCql(Path cql) throws Exception { diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java deleted file mode 100644 index 573d152635..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java +++ /dev/null @@ -1,314 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.install; - -import com.datastax.driver.core.KeyspaceMetadata; -import com.datastax.driver.core.exceptions.InvalidQueryException; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Service; -import org.thingsboard.server.dao.dashboard.DashboardService; -import org.thingsboard.server.dao.util.NoSqlDao; -import org.thingsboard.server.service.install.cql.CassandraDbHelper; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - -import static org.thingsboard.server.service.install.DatabaseHelper.ADDITIONAL_INFO; -import static org.thingsboard.server.service.install.DatabaseHelper.ASSET; -import static org.thingsboard.server.service.install.DatabaseHelper.ASSIGNED_CUSTOMERS; -import static org.thingsboard.server.service.install.DatabaseHelper.CONFIGURATION; -import static org.thingsboard.server.service.install.DatabaseHelper.CUSTOMER_ID; -import static org.thingsboard.server.service.install.DatabaseHelper.DASHBOARD; -import static org.thingsboard.server.service.install.DatabaseHelper.DEVICE; -import static org.thingsboard.server.service.install.DatabaseHelper.END_TS; -import static org.thingsboard.server.service.install.DatabaseHelper.ENTITY_ID; -import static org.thingsboard.server.service.install.DatabaseHelper.ENTITY_TYPE; -import static org.thingsboard.server.service.install.DatabaseHelper.ENTITY_VIEW; -import static org.thingsboard.server.service.install.DatabaseHelper.ENTITY_VIEWS; -import static org.thingsboard.server.service.install.DatabaseHelper.ID; -import static org.thingsboard.server.service.install.DatabaseHelper.KEYS; -import static org.thingsboard.server.service.install.DatabaseHelper.NAME; -import static org.thingsboard.server.service.install.DatabaseHelper.SEARCH_TEXT; -import static org.thingsboard.server.service.install.DatabaseHelper.START_TS; -import static org.thingsboard.server.service.install.DatabaseHelper.TENANT_ID; -import static org.thingsboard.server.service.install.DatabaseHelper.TITLE; -import static org.thingsboard.server.service.install.DatabaseHelper.TYPE; - -@Service -@NoSqlDao -@Profile("install") -@Slf4j -public class CassandraDatabaseUpgradeService extends AbstractCassandraDatabaseUpgradeService implements DatabaseEntitiesUpgradeService { - - private static final String SCHEMA_UPDATE_CQL = "schema_update.cql"; - - @Autowired - private DashboardService dashboardService; - - @Autowired - private InstallScripts installScripts; - - @Override - public void upgradeDatabase(String fromVersion) throws Exception { - - switch (fromVersion) { - case "1.2.3": - - log.info("Upgrading Cassandara DataBase from version {} to 1.3.0 ...", fromVersion); - - //Dump devices, assets and relations - - cluster.getSession(); - - KeyspaceMetadata ks = cluster.getCluster().getMetadata().getKeyspace(cluster.getKeyspaceName()); - - log.info("Dumping devices ..."); - Path devicesDump = CassandraDbHelper.dumpCfIfExists(ks, cluster.getSession(), DEVICE, - new String[]{"id", TENANT_ID, CUSTOMER_ID, "name", SEARCH_TEXT, ADDITIONAL_INFO, "type"}, - new String[]{"", "", "", "", "", "", "default"}, - "tb-devices"); - log.info("Devices dumped."); - - log.info("Dumping assets ..."); - Path assetsDump = CassandraDbHelper.dumpCfIfExists(ks, cluster.getSession(), ASSET, - new String[]{"id", TENANT_ID, CUSTOMER_ID, "name", SEARCH_TEXT, ADDITIONAL_INFO, "type"}, - new String[]{"", "", "", "", "", "", "default"}, - "tb-assets"); - log.info("Assets dumped."); - - log.info("Dumping relations ..."); - Path relationsDump = CassandraDbHelper.dumpCfIfExists(ks, cluster.getSession(), "relation", - new String[]{"from_id", "from_type", "to_id", "to_type", "relation_type", ADDITIONAL_INFO, "relation_type_group"}, - new String[]{"", "", "", "", "", "", "COMMON"}, - "tb-relations"); - log.info("Relations dumped."); - - log.info("Updating schema ..."); - Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "1.3.0", SCHEMA_UPDATE_CQL); - loadCql(schemaUpdateFile); - log.info("Schema updated."); - - //Restore devices, assets and relations - - log.info("Restoring devices ..."); - if (devicesDump != null) { - CassandraDbHelper.loadCf(ks, cluster.getSession(), DEVICE, - new String[]{"id", TENANT_ID, CUSTOMER_ID, "name", SEARCH_TEXT, ADDITIONAL_INFO, "type"}, devicesDump); - Files.deleteIfExists(devicesDump); - } - log.info("Devices restored."); - - log.info("Dumping device types ..."); - Path deviceTypesDump = CassandraDbHelper.dumpCfIfExists(ks, cluster.getSession(), DEVICE, - new String[]{TENANT_ID, "type"}, - new String[]{"", ""}, - "tb-device-types"); - if (deviceTypesDump != null) { - CassandraDbHelper.appendToEndOfLine(deviceTypesDump, "DEVICE"); - } - log.info("Device types dumped."); - log.info("Loading device types ..."); - if (deviceTypesDump != null) { - CassandraDbHelper.loadCf(ks, cluster.getSession(), "entity_subtype", - new String[]{TENANT_ID, "type", "entity_type"}, deviceTypesDump); - Files.deleteIfExists(deviceTypesDump); - } - log.info("Device types loaded."); - - log.info("Restoring assets ..."); - if (assetsDump != null) { - CassandraDbHelper.loadCf(ks, cluster.getSession(), ASSET, - new String[]{"id", TENANT_ID, CUSTOMER_ID, "name", SEARCH_TEXT, ADDITIONAL_INFO, "type"}, assetsDump); - Files.deleteIfExists(assetsDump); - } - log.info("Assets restored."); - - log.info("Dumping asset types ..."); - Path assetTypesDump = CassandraDbHelper.dumpCfIfExists(ks, cluster.getSession(), ASSET, - new String[]{TENANT_ID, "type"}, - new String[]{"", ""}, - "tb-asset-types"); - if (assetTypesDump != null) { - CassandraDbHelper.appendToEndOfLine(assetTypesDump, "ASSET"); - } - log.info("Asset types dumped."); - log.info("Loading asset types ..."); - if (assetTypesDump != null) { - CassandraDbHelper.loadCf(ks, cluster.getSession(), "entity_subtype", - new String[]{TENANT_ID, "type", "entity_type"}, assetTypesDump); - Files.deleteIfExists(assetTypesDump); - } - log.info("Asset types loaded."); - - log.info("Restoring relations ..."); - if (relationsDump != null) { - CassandraDbHelper.loadCf(ks, cluster.getSession(), "relation", - new String[]{"from_id", "from_type", "to_id", "to_type", "relation_type", ADDITIONAL_INFO, "relation_type_group"}, relationsDump); - Files.deleteIfExists(relationsDump); - } - log.info("Relations restored."); - - break; - case "1.3.0": - break; - case "1.3.1": - - cluster.getSession(); - - ks = cluster.getCluster().getMetadata().getKeyspace(cluster.getKeyspaceName()); - - log.info("Dumping dashboards ..."); - Path dashboardsDump = CassandraDbHelper.dumpCfIfExists(ks, cluster.getSession(), DASHBOARD, - new String[]{ID, TENANT_ID, CUSTOMER_ID, TITLE, SEARCH_TEXT, ASSIGNED_CUSTOMERS, CONFIGURATION}, - new String[]{"", "", "", "", "", "", ""}, - "tb-dashboards", true); - log.info("Dashboards dumped."); - - - log.info("Updating schema ..."); - schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "1.4.0", SCHEMA_UPDATE_CQL); - loadCql(schemaUpdateFile); - log.info("Schema updated."); - - log.info("Restoring dashboards ..."); - if (dashboardsDump != null) { - CassandraDbHelper.loadCf(ks, cluster.getSession(), DASHBOARD, - new String[]{ID, TENANT_ID, TITLE, SEARCH_TEXT, CONFIGURATION}, dashboardsDump, true); - DatabaseHelper.upgradeTo40_assignDashboards(dashboardsDump, dashboardService, false); - Files.deleteIfExists(dashboardsDump); - } - log.info("Dashboards restored."); - break; - case "1.4.0": - - log.info("Updating schema ..."); - schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.0.0", SCHEMA_UPDATE_CQL); - loadCql(schemaUpdateFile); - log.info("Schema updated."); - - break; - - case "2.0.0": - - log.info("Updating schema ..."); - schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.1.1", SCHEMA_UPDATE_CQL); - loadCql(schemaUpdateFile); - log.info("Schema updated."); - - break; - - case "2.1.1": - - log.info("Upgrading Cassandra DataBase from version {} to 2.1.2 ...", fromVersion); - - cluster.getSession(); - - ks = cluster.getCluster().getMetadata().getKeyspace(cluster.getKeyspaceName()); - - log.info("Dumping entity views ..."); - Path entityViewsDump = CassandraDbHelper.dumpCfIfExists(ks, cluster.getSession(), ENTITY_VIEWS, - new String[]{ID, ENTITY_ID, ENTITY_TYPE, TENANT_ID, CUSTOMER_ID, NAME, TYPE, KEYS, START_TS, END_TS, SEARCH_TEXT, ADDITIONAL_INFO}, - new String[]{"", "", "", "", "", "", "default", "", "0", "0", "", ""}, - "tb-entity-views"); - log.info("Entity views dumped."); - - log.info("Updating schema ..."); - schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.1.2", SCHEMA_UPDATE_CQL); - loadCql(schemaUpdateFile); - log.info("Schema updated."); - - log.info("Restoring entity views ..."); - if (entityViewsDump != null) { - CassandraDbHelper.loadCf(ks, cluster.getSession(), ENTITY_VIEW, - new String[]{ID, ENTITY_ID, ENTITY_TYPE, TENANT_ID, CUSTOMER_ID, NAME, TYPE, KEYS, START_TS, END_TS, SEARCH_TEXT, ADDITIONAL_INFO}, entityViewsDump); - Files.deleteIfExists(entityViewsDump); - } - log.info("Entity views restored."); - - break; - case "2.1.3": - break; - case "2.3.0": - break; - case "2.3.1": - log.info("Updating schema ..."); - String updateDeviceTableStmt = "alter table device add label text"; - try { - cluster.getSession().execute(updateDeviceTableStmt); - Thread.sleep(2500); - } catch (InvalidQueryException e) { - } - log.info("Schema updated."); - break; - case "2.4.1": - log.info("Updating schema ..."); - String updateAssetTableStmt = "alter table asset add label text"; - try { - log.info("Updating assets ..."); - cluster.getSession().execute(updateAssetTableStmt); - Thread.sleep(2500); - log.info("Assets updated."); - } catch (InvalidQueryException e) { - } - log.info("Schema updated."); - break; - case "2.4.2": - log.info("Updating schema ..."); - String updateAlarmTableStmt = "alter table alarm add propagate_relation_types text"; - try { - log.info("Updating alarms ..."); - cluster.getSession().execute(updateAlarmTableStmt); - Thread.sleep(2500); - log.info("Alarms updated."); - } catch (InvalidQueryException e) { - } - log.info("Schema updated."); - break; - case "2.4.3": - log.info("Updating schema ..."); - String updateAttributeKvTableStmt = "alter table attributes_kv_cf add json_v text"; - try { - log.info("Updating attributes ..."); - cluster.getSession().execute(updateAttributeKvTableStmt); - Thread.sleep(2500); - log.info("Attributes updated."); - } catch (InvalidQueryException e) { - } - - String updateTenantCoreTableStmt = "alter table tenant add isolated_tb_core boolean"; - String updateTenantRuleEngineTableStmt = "alter table tenant add isolated_tb_rule_engine boolean"; - - try { - log.info("Updating tenant..."); - cluster.getSession().execute(updateTenantCoreTableStmt); - Thread.sleep(2500); - - cluster.getSession().execute(updateTenantRuleEngineTableStmt); - Thread.sleep(2500); - log.info("Tenant updated."); - } catch (InvalidQueryException e) { - } - log.info("Schema updated."); - break; - default: - throw new RuntimeException("Unable to upgrade Cassandra database, unsupported fromVersion: " + fromVersion); - } - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraEntityDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraEntityDatabaseSchemaService.java deleted file mode 100644 index 4f075112af..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/install/CassandraEntityDatabaseSchemaService.java +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.install; - -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Service; -import org.thingsboard.server.dao.util.NoSqlDao; - -@Service -@NoSqlDao -@Profile("install") -public class CassandraEntityDatabaseSchemaService extends CassandraAbstractDatabaseSchemaService - implements EntityDatabaseSchemaService { - public CassandraEntityDatabaseSchemaService() { - super("schema-entities.cql"); - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraTsDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraTsDatabaseUpgradeService.java index 07b8522323..17857e2807 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/CassandraTsDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraTsDatabaseUpgradeService.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.service.install; -import com.datastax.driver.core.exceptions.InvalidQueryException; +import com.datastax.oss.driver.api.core.servererrors.InvalidQueryException; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -49,7 +49,7 @@ public class CassandraTsDatabaseUpgradeService extends AbstractCassandraDatabase log.info("Schema updated."); break; case "2.5.0": - case "2.5.4": + case "3.1.1": break; default: throw new RuntimeException("Unable to upgrade Cassandra database, unsupported fromVersion: " + fromVersion); diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraTsLatestDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraTsLatestDatabaseSchemaService.java new file mode 100644 index 0000000000..f2df8a50da --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraTsLatestDatabaseSchemaService.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.install; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.thingsboard.server.dao.util.NoSqlTsLatestDao; + +@Service +@NoSqlTsLatestDao +@Profile("install") +public class CassandraTsLatestDatabaseSchemaService extends CassandraAbstractDatabaseSchemaService + implements TsLatestDatabaseSchemaService { + public CassandraTsLatestDatabaseSchemaService() { + super("schema-ts-latest.cql"); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/DatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/DatabaseSchemaService.java index c120f833fa..6b67a4213d 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DatabaseSchemaService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DatabaseSchemaService.java @@ -19,4 +19,8 @@ public interface DatabaseSchemaService { void createDatabaseSchema() throws Exception; + void createDatabaseSchema(boolean createIndexes) throws Exception; + + void createDatabaseIndexes() throws Exception; + } diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index d0ec2367df..fd757ed6dd 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -27,11 +27,16 @@ import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry; @@ -46,9 +51,12 @@ import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.settings.AdminSettingsService; +import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.widget.WidgetsBundleService; @@ -82,6 +90,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { @Autowired private TenantService tenantService; + @Autowired + private TenantProfileService tenantProfileService; + @Autowired private CustomerService customerService; @@ -94,6 +105,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { @Autowired private DeviceService deviceService; + @Autowired + private DeviceProfileService deviceProfileService; + @Autowired private AttributesService attributesService; @@ -110,6 +124,58 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { createUser(Authority.SYS_ADMIN, null, null, "sysadmin@thingsboard.org", "sysadmin"); } + @Override + public void createDefaultTenantProfiles() throws Exception { + tenantProfileService.findOrCreateDefaultTenantProfile(TenantId.SYS_TENANT_ID); + + TenantProfileData tenantProfileData = new TenantProfileData(); + tenantProfileData.setConfiguration(new DefaultTenantProfileConfiguration()); + + TenantProfile isolatedTbCoreProfile = new TenantProfile(); + isolatedTbCoreProfile.setDefault(false); + isolatedTbCoreProfile.setName("Isolated TB Core"); + isolatedTbCoreProfile.setProfileData(new TenantProfileData()); + isolatedTbCoreProfile.setDescription("Isolated TB Core tenant profile"); + isolatedTbCoreProfile.setIsolatedTbCore(true); + isolatedTbCoreProfile.setIsolatedTbRuleEngine(false); + isolatedTbCoreProfile.setProfileData(tenantProfileData); + try { + tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, isolatedTbCoreProfile); + } catch (DataValidationException e) { + log.warn(e.getMessage()); + } + + TenantProfile isolatedTbRuleEngineProfile = new TenantProfile(); + isolatedTbRuleEngineProfile.setDefault(false); + isolatedTbRuleEngineProfile.setName("Isolated TB Rule Engine"); + isolatedTbRuleEngineProfile.setProfileData(new TenantProfileData()); + isolatedTbRuleEngineProfile.setDescription("Isolated TB Rule Engine tenant profile"); + isolatedTbRuleEngineProfile.setIsolatedTbCore(false); + isolatedTbRuleEngineProfile.setIsolatedTbRuleEngine(true); + isolatedTbRuleEngineProfile.setProfileData(tenantProfileData); + + try { + tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, isolatedTbRuleEngineProfile); + } catch (DataValidationException e) { + log.warn(e.getMessage()); + } + + TenantProfile isolatedTbCoreAndTbRuleEngineProfile = new TenantProfile(); + isolatedTbCoreAndTbRuleEngineProfile.setDefault(false); + isolatedTbCoreAndTbRuleEngineProfile.setName("Isolated TB Core and TB Rule Engine"); + isolatedTbCoreAndTbRuleEngineProfile.setProfileData(new TenantProfileData()); + isolatedTbCoreAndTbRuleEngineProfile.setDescription("Isolated TB Core and TB Rule Engine tenant profile"); + isolatedTbCoreAndTbRuleEngineProfile.setIsolatedTbCore(true); + isolatedTbCoreAndTbRuleEngineProfile.setIsolatedTbRuleEngine(true); + isolatedTbCoreAndTbRuleEngineProfile.setProfileData(tenantProfileData); + + try { + tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, isolatedTbCoreAndTbRuleEngineProfile); + } catch (DataValidationException e) { + log.warn(e.getMessage()); + } + } + @Override public void createAdminSettings() throws Exception { AdminSettings generalSettings = new AdminSettings(); @@ -137,6 +203,11 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, mailSettings); } + @Override + public void createOAuth2Templates() throws Exception { + installScripts.createOAuth2Templates(); + } + @Override public void loadDemoData() throws Exception { Tenant demoTenant = new Tenant(); @@ -163,16 +234,18 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { createUser(Authority.CUSTOMER_USER, demoTenant.getId(), customerB.getId(), "customerB@thingsboard.org", CUSTOMER_CRED); createUser(Authority.CUSTOMER_USER, demoTenant.getId(), customerC.getId(), "customerC@thingsboard.org", CUSTOMER_CRED); - createDevice(demoTenant.getId(), customerA.getId(), DEFAULT_DEVICE_TYPE, "Test Device A1", "A1_TEST_TOKEN", null); - createDevice(demoTenant.getId(), customerA.getId(), DEFAULT_DEVICE_TYPE, "Test Device A2", "A2_TEST_TOKEN", null); - createDevice(demoTenant.getId(), customerA.getId(), DEFAULT_DEVICE_TYPE, "Test Device A3", "A3_TEST_TOKEN", null); - createDevice(demoTenant.getId(), customerB.getId(), DEFAULT_DEVICE_TYPE, "Test Device B1", "B1_TEST_TOKEN", null); - createDevice(demoTenant.getId(), customerC.getId(), DEFAULT_DEVICE_TYPE, "Test Device C1", "C1_TEST_TOKEN", null); + DeviceProfile defaultDeviceProfile = this.deviceProfileService.findOrCreateDeviceProfile(demoTenant.getId(), DEFAULT_DEVICE_TYPE); + + createDevice(demoTenant.getId(), customerA.getId(), defaultDeviceProfile.getId(), "Test Device A1", "A1_TEST_TOKEN", null); + createDevice(demoTenant.getId(), customerA.getId(), defaultDeviceProfile.getId(), "Test Device A2", "A2_TEST_TOKEN", null); + createDevice(demoTenant.getId(), customerA.getId(), defaultDeviceProfile.getId(), "Test Device A3", "A3_TEST_TOKEN", null); + createDevice(demoTenant.getId(), customerB.getId(), defaultDeviceProfile.getId(), "Test Device B1", "B1_TEST_TOKEN", null); + createDevice(demoTenant.getId(), customerC.getId(), defaultDeviceProfile.getId(), "Test Device C1", "C1_TEST_TOKEN", null); - createDevice(demoTenant.getId(), null, DEFAULT_DEVICE_TYPE, "DHT11 Demo Device", "DHT11_DEMO_TOKEN", "Demo device that is used in sample " + + createDevice(demoTenant.getId(), null, defaultDeviceProfile.getId(), "DHT11 Demo Device", "DHT11_DEMO_TOKEN", "Demo device that is used in sample " + "applications that upload data from DHT11 temperature and humidity sensor"); - createDevice(demoTenant.getId(), null, DEFAULT_DEVICE_TYPE, "Raspberry Pi Demo Device", "RASPBERRY_PI_DEMO_TOKEN", "Demo device that is used in " + + createDevice(demoTenant.getId(), null, defaultDeviceProfile.getId(), "Raspberry Pi Demo Device", "RASPBERRY_PI_DEMO_TOKEN", "Demo device that is used in " + "Raspberry Pi GPIO control sample application"); Asset thermostatAlarms = new Asset(); @@ -181,8 +254,10 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { thermostatAlarms.setType("AlarmPropagationAsset"); thermostatAlarms = assetService.saveAsset(thermostatAlarms); - DeviceId t1Id = createDevice(demoTenant.getId(), null, "thermostat", "Thermostat T1", "T1_TEST_TOKEN", "Demo device for Thermostats dashboard").getId(); - DeviceId t2Id = createDevice(demoTenant.getId(), null, "thermostat", "Thermostat T2", "T2_TEST_TOKEN", "Demo device for Thermostats dashboard").getId(); + DeviceProfile thermostatDeviceProfile = this.deviceProfileService.findOrCreateDeviceProfile(demoTenant.getId(), "thermostat"); + + DeviceId t1Id = createDevice(demoTenant.getId(), null, thermostatDeviceProfile.getId(), "Thermostat T1", "T1_TEST_TOKEN", "Demo device for Thermostats dashboard").getId(); + DeviceId t2Id = createDevice(demoTenant.getId(), null, thermostatDeviceProfile.getId(), "Thermostat T2", "T2_TEST_TOKEN", "Demo device for Thermostats dashboard").getId(); relationService.saveRelation(thermostatAlarms.getTenantId(), new EntityRelation(thermostatAlarms.getId(), t1Id, "ToAlarmPropagationAsset")); relationService.saveRelation(thermostatAlarms.getTenantId(), new EntityRelation(thermostatAlarms.getId(), t2Id, "ToAlarmPropagationAsset")); @@ -219,6 +294,24 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { installScripts.loadSystemWidgets(); } + @Override + public void updateSystemWidgets() throws Exception { + this.deleteSystemWidgetBundle("charts"); + this.deleteSystemWidgetBundle("cards"); + this.deleteSystemWidgetBundle("maps"); + this.deleteSystemWidgetBundle("analogue_gauges"); + this.deleteSystemWidgetBundle("digital_gauges"); + this.deleteSystemWidgetBundle("gpio_widgets"); + this.deleteSystemWidgetBundle("alarm_widgets"); + this.deleteSystemWidgetBundle("control_widgets"); + this.deleteSystemWidgetBundle("maps_v2"); + this.deleteSystemWidgetBundle("gateway_widgets"); + this.deleteSystemWidgetBundle("input_widgets"); + this.deleteSystemWidgetBundle("date"); + this.deleteSystemWidgetBundle("entity_admin_widgets"); + installScripts.loadSystemWidgets(); + } + private User createUser(Authority authority, TenantId tenantId, CustomerId customerId, @@ -240,14 +333,14 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { private Device createDevice(TenantId tenantId, CustomerId customerId, - String type, + DeviceProfileId deviceProfileId, String name, String accessToken, String description) { Device device = new Device(); device.setTenantId(tenantId); device.setCustomerId(customerId); - device.setType(type); + device.setDeviceProfileId(deviceProfileId); device.setName(name); if (description != null) { ObjectNode additionalInfo = objectMapper.createObjectNode(); diff --git a/application/src/main/java/org/thingsboard/server/service/install/HsqlEntityDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/HsqlEntityDatabaseSchemaService.java index 0b3232903b..8124356bba 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/HsqlEntityDatabaseSchemaService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/HsqlEntityDatabaseSchemaService.java @@ -18,11 +18,9 @@ package org.thingsboard.server.service.install; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; import org.thingsboard.server.dao.util.HsqlDao; -import org.thingsboard.server.dao.util.SqlDao; @Service @HsqlDao -@SqlDao @Profile("install") public class HsqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaService implements EntityDatabaseSchemaService { diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java index 5181834b80..ac047060ae 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java +++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java @@ -26,11 +26,13 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.dashboard.DashboardService; +import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; @@ -40,6 +42,7 @@ import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Optional; import static org.thingsboard.server.service.install.DatabaseHelper.objectMapper; @@ -57,9 +60,11 @@ public class InstallScripts { public static final String JSON_DIR = "json"; public static final String SYSTEM_DIR = "system"; public static final String TENANT_DIR = "tenant"; + public static final String DEVICE_PROFILE_DIR = "device_profile"; public static final String DEMO_DIR = "demo"; public static final String RULE_CHAINS_DIR = "rule_chains"; public static final String WIDGET_BUNDLES_DIR = "widget_bundles"; + public static final String OAUTH2_CONFIG_TEMPLATES_DIR = "oauth2_config_templates"; public static final String DASHBOARDS_DIR = "dashboards"; public static final String JSON_EXT = ".json"; @@ -79,10 +84,17 @@ public class InstallScripts { @Autowired private WidgetsBundleService widgetsBundleService; + @Autowired + private OAuth2ConfigTemplateService oAuth2TemplateService; + public Path getTenantRuleChainsDir() { return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, RULE_CHAINS_DIR); } + public Path getDeviceProfileDefaultRuleChainTemplateFilePath() { + return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, DEVICE_PROFILE_DIR, "rule_chain_template.json"); + } + public String getDataDir() { if (!StringUtils.isEmpty(dataDir)) { if (!Paths.get(this.dataDir).toFile().isDirectory()) { @@ -110,15 +122,7 @@ public class InstallScripts { dirStream.forEach( path -> { try { - JsonNode ruleChainJson = objectMapper.readTree(path.toFile()); - RuleChain ruleChain = objectMapper.treeToValue(ruleChainJson.get("ruleChain"), RuleChain.class); - RuleChainMetaData ruleChainMetaData = objectMapper.treeToValue(ruleChainJson.get("metadata"), RuleChainMetaData.class); - - ruleChain.setTenantId(tenantId); - ruleChain = ruleChainService.saveRuleChain(ruleChain); - - ruleChainMetaData.setRuleChainId(ruleChain.getId()); - ruleChainService.saveRuleChainMetaData(new TenantId(EntityId.NULL_UUID), ruleChainMetaData); + createRuleChainFromFile(tenantId, path, null); } catch (Exception e) { log.error("Unable to load rule chain from json: [{}]", path.toString()); throw new RuntimeException("Unable to load rule chain from json", e); @@ -128,6 +132,28 @@ public class InstallScripts { } } + public RuleChain createDefaultRuleChain(TenantId tenantId, String ruleChainName) throws IOException { + return createRuleChainFromFile(tenantId, getDeviceProfileDefaultRuleChainTemplateFilePath(), ruleChainName); + } + + public RuleChain createRuleChainFromFile(TenantId tenantId, Path templateFilePath, String newRuleChainName) throws IOException { + JsonNode ruleChainJson = objectMapper.readTree(templateFilePath.toFile()); + RuleChain ruleChain = objectMapper.treeToValue(ruleChainJson.get("ruleChain"), RuleChain.class); + RuleChainMetaData ruleChainMetaData = objectMapper.treeToValue(ruleChainJson.get("metadata"), RuleChainMetaData.class); + + ruleChain.setTenantId(tenantId); + if (!StringUtils.isEmpty(newRuleChainName)) { + ruleChain.setName(newRuleChainName); + } + ruleChain = ruleChainService.saveRuleChain(ruleChain); + + ruleChainMetaData.setRuleChainId(ruleChain.getId()); + ruleChainService.saveRuleChainMetaData(new TenantId(EntityId.NULL_UUID), ruleChainMetaData); + + return ruleChain; + } + + public void loadSystemWidgets() throws Exception { Path widgetBundlesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_BUNDLES_DIR); try (DirectoryStream dirStream = Files.newDirectoryStream(widgetBundlesDir, path -> path.toString().endsWith(JSON_EXT))) { @@ -209,4 +235,27 @@ public class InstallScripts { throw new RuntimeException("Unable to load dashboard from json", e); } } + + public void createOAuth2Templates() throws Exception { + Path oauth2ConfigTemplatesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, OAUTH2_CONFIG_TEMPLATES_DIR); + try (DirectoryStream dirStream = Files.newDirectoryStream(oauth2ConfigTemplatesDir, path -> path.toString().endsWith(JSON_EXT))) { + dirStream.forEach( + path -> { + try { + JsonNode oauth2ConfigTemplateJson = objectMapper.readTree(path.toFile()); + OAuth2ClientRegistrationTemplate clientRegistrationTemplate = objectMapper.treeToValue(oauth2ConfigTemplateJson, OAuth2ClientRegistrationTemplate.class); + Optional existingClientRegistrationTemplate = + oAuth2TemplateService.findClientRegistrationTemplateByProviderId(clientRegistrationTemplate.getProviderId()); + if (existingClientRegistrationTemplate.isPresent()) { + clientRegistrationTemplate.setId(existingClientRegistrationTemplate.get().getId()); + } + oAuth2TemplateService.saveClientRegistrationTemplate(clientRegistrationTemplate); + } catch (Exception e) { + log.error("Unable to load oauth2 config templates from json: [{}]", path.toString()); + throw new RuntimeException("Unable to load oauth2 config templates from json", e); + } + } + ); + } + } } diff --git a/application/src/main/java/org/thingsboard/server/service/install/PsqlEntityDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/PsqlEntityDatabaseSchemaService.java index 11da8b306a..364457f11e 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/PsqlEntityDatabaseSchemaService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/PsqlEntityDatabaseSchemaService.java @@ -18,10 +18,8 @@ package org.thingsboard.server.service.install; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; import org.thingsboard.server.dao.util.PsqlDao; -import org.thingsboard.server.dao.util.SqlDao; @Service -@SqlDao @PsqlDao @Profile("install") public class PsqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaService diff --git a/application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java index 396f84664a..18433c6f53 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java @@ -195,8 +195,10 @@ public class PsqlTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeSe executeQuery(conn, "UPDATE tb_schema_settings SET schema_version = 2005001"); } break; - case "2.5.4": + case "3.1.1": try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + log.info("Load TTL functions ..."); + loadSql(conn, LOAD_TTL_FUNCTIONS_SQL); log.info("Load Drop Partitions functions ..."); loadSql(conn, LOAD_DROP_PARTITIONS_FUNCTIONS_SQL); } @@ -245,4 +247,4 @@ public class PsqlTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeSe log.info("Failed to load PostgreSQL upgrade functions due to: {}", e.getMessage()); } } -} \ No newline at end of file +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java index cfc32d31b3..5febae30ab 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java @@ -54,6 +54,11 @@ public abstract class SqlAbstractDatabaseSchemaService implements DatabaseSchema @Override public void createDatabaseSchema() throws Exception { + this.createDatabaseSchema(true); + } + + @Override + public void createDatabaseSchema(boolean createIndexes) throws Exception { log.info("Installing SQL DataBase schema part: " + schemaSql); @@ -63,9 +68,15 @@ public abstract class SqlAbstractDatabaseSchemaService implements DatabaseSchema conn.createStatement().execute(sql); //NOSONAR, ignoring because method used to load initial thingsboard database schema } + if (createIndexes) { + this.createDatabaseIndexes(); + } + } + + @Override + public void createDatabaseIndexes() throws Exception { if (schemaIdxSql != null) { log.info("Installing SQL DataBase schema indexes part: " + schemaIdxSql); - Path schemaIdxFile = Paths.get(installScripts.getDataDir(), SQL_DIR, schemaIdxSql); try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { String sql = new String(Files.readAllBytes(schemaIdxFile), Charset.forName("UTF-8")); diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index 87164737c7..19727684a3 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -20,8 +20,15 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.dashboard.DashboardService; -import org.thingsboard.server.dao.util.SqlDao; +import org.thingsboard.server.dao.device.DeviceProfileService; +import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.service.install.sql.SqlDbHelper; import java.nio.charset.Charset; @@ -30,7 +37,12 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.sql.Connection; import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; import java.sql.SQLSyntaxErrorException; +import java.sql.SQLWarning; +import java.sql.Statement; +import java.util.List; import static org.thingsboard.server.service.install.DatabaseHelper.ADDITIONAL_INFO; import static org.thingsboard.server.service.install.DatabaseHelper.ASSIGNED_CUSTOMERS; @@ -54,7 +66,6 @@ import static org.thingsboard.server.service.install.DatabaseHelper.TYPE; @Service @Profile("install") @Slf4j -@SqlDao public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService { private static final String SCHEMA_UPDATE_SQL = "schema_update.sql"; @@ -74,6 +85,22 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService @Autowired private InstallScripts installScripts; + @Autowired + private SystemDataLoaderService systemDataLoaderService; + + @Autowired + private TenantService tenantService; + + @Autowired + private DeviceService deviceService; + + @Autowired + private DeviceProfileService deviceProfileService; + + @Autowired + private ApiUsageStateService apiUsageStateService; + + @Override public void upgradeDatabase(String fromVersion) throws Exception { switch (fromVersion) { @@ -183,18 +210,22 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService log.info("Updating schema ..."); try { conn.createStatement().execute("ALTER TABLE asset ADD COLUMN label varchar(255)"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script - } catch (Exception e) {} + } catch (Exception e) { + } schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.4.2", SCHEMA_UPDATE_SQL); loadSql(schemaUpdateFile, conn); try { conn.createStatement().execute("ALTER TABLE device ADD CONSTRAINT device_name_unq_key UNIQUE (tenant_id, name)"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script - } catch (Exception e) {} + } catch (Exception e) { + } try { conn.createStatement().execute("ALTER TABLE device_credentials ADD CONSTRAINT device_credentials_id_unq_key UNIQUE (credentials_id)"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script - } catch (Exception e) {} + } catch (Exception e) { + } try { conn.createStatement().execute("ALTER TABLE asset ADD CONSTRAINT asset_name_unq_key UNIQUE (tenant_id, name)"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script - } catch (Exception e) {} + } catch (Exception e) { + } log.info("Schema updated."); } break; @@ -233,6 +264,161 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService log.info("Schema updated."); } break; + case "3.0.1": + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + log.info("Updating schema ..."); + if (isOldSchema(conn, 3000001)) { + String[] tables = new String[]{"admin_settings", "alarm", "asset", "audit_log", "attribute_kv", + "component_descriptor", "customer", "dashboard", "device", "device_credentials", "event", + "relation", "tb_user", "tenant", "user_credentials", "widget_type", "widgets_bundle", + "rule_chain", "rule_node", "entity_view"}; + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.0.1", "schema_update_to_uuid.sql"); + loadSql(schemaUpdateFile, conn); + + conn.createStatement().execute("call drop_all_idx()"); + + log.info("Optimizing alarm relations..."); + conn.createStatement().execute("DELETE from relation WHERE relation_type_group = 'ALARM' AND relation_type <> 'ALARM_ANY';"); + conn.createStatement().execute("DELETE from relation WHERE relation_type_group = 'ALARM' AND relation_type = 'ALARM_ANY' " + + "AND exists(SELECT * FROM alarm WHERE alarm.id = relation.to_id AND alarm.originator_id = relation.from_id)"); + log.info("Alarm relations optimized."); + + for (String table : tables) { + log.info("Updating table {}.", table); + Statement statement = conn.createStatement(); + statement.execute("call update_" + table + "();"); + + SQLWarning warnings = statement.getWarnings(); + if (warnings != null) { + log.info("{}", warnings.getMessage()); + SQLWarning nextWarning = warnings.getNextWarning(); + while (nextWarning != null) { + log.info("{}", nextWarning.getMessage()); + nextWarning = nextWarning.getNextWarning(); + } + } + + conn.createStatement().execute("DROP PROCEDURE update_" + table); + log.info("Table {} updated.", table); + } + conn.createStatement().execute("call create_all_idx()"); + + conn.createStatement().execute("DROP PROCEDURE drop_all_idx"); + conn.createStatement().execute("DROP PROCEDURE create_all_idx"); + conn.createStatement().execute("DROP FUNCTION column_type_to_uuid"); + + log.info("Updating alarm relations..."); + conn.createStatement().execute("UPDATE relation SET relation_type = 'ANY' WHERE relation_type_group = 'ALARM' AND relation_type = 'ALARM_ANY';"); + log.info("Alarm relations updated."); + + conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3001000;"); + + conn.createStatement().execute("VACUUM FULL"); + } + log.info("Schema updated."); + } catch (Exception e) { + log.error("Failed updating schema!!!", e); + } + break; + case "3.1.0": + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + log.info("Updating schema ..."); + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.1.0", SCHEMA_UPDATE_SQL); + loadSql(schemaUpdateFile, conn); + log.info("Schema updated."); + } + break; + case "3.1.1": + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + log.info("Updating schema ..."); + if (isOldSchema(conn, 3001000)) { + + try { + conn.createStatement().execute("ALTER TABLE device ADD COLUMN device_profile_id uuid, ADD COLUMN device_data jsonb"); + } catch (Exception e) { + } + + try { + conn.createStatement().execute("ALTER TABLE tenant ADD COLUMN tenant_profile_id uuid"); + } catch (Exception e) { + } + + try { + conn.createStatement().execute("CREATE TABLE IF NOT EXISTS rule_node_state (" + + " id uuid NOT NULL CONSTRAINT rule_node_state_pkey PRIMARY KEY," + + " created_time bigint NOT NULL," + + " rule_node_id uuid NOT NULL," + + " entity_type varchar(32) NOT NULL," + + " entity_id uuid NOT NULL," + + " state_data varchar(16384) NOT NULL," + + " CONSTRAINT rule_node_state_unq_key UNIQUE (rule_node_id, entity_id)," + + " CONSTRAINT fk_rule_node_state_node_id FOREIGN KEY (rule_node_id) REFERENCES rule_node(id) ON DELETE CASCADE)"); + } catch (Exception e) { + } + + try { + conn.createStatement().execute("CREATE TABLE IF NOT EXISTS api_usage_state (" + + " id uuid NOT NULL CONSTRAINT usage_record_pkey PRIMARY KEY," + + " created_time bigint NOT NULL," + + " tenant_id uuid," + + " entity_type varchar(32)," + + " entity_id uuid," + + " transport varchar(32)," + + " db_storage varchar(32)," + + " re_exec varchar(32)," + + " js_exec varchar(32)," + + " CONSTRAINT api_usage_state_unq_key UNIQUE (tenant_id, entity_id)\n" + + ");"); + } catch (Exception e) { + } + + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.1.1", "schema_update_before.sql"); + loadSql(schemaUpdateFile, conn); + + log.info("Creating default tenant profiles..."); + systemDataLoaderService.createDefaultTenantProfiles(); + + log.info("Updating tenant profiles..."); + conn.createStatement().execute("call update_tenant_profiles()"); + + log.info("Creating default device profiles..."); + PageLink pageLink = new PageLink(100); + PageData pageData; + do { + pageData = tenantService.findTenants(pageLink); + for (Tenant tenant : pageData.getData()) { + try { + apiUsageStateService.createDefaultApiUsageState(tenant.getId()); + } catch (Exception e) { + } + List deviceTypes = deviceService.findDeviceTypesByTenantId(tenant.getId()).get(); + try { + deviceProfileService.createDefaultDeviceProfile(tenant.getId()); + } catch (Exception e) { + } + for (EntitySubtype deviceType : deviceTypes) { + try { + deviceProfileService.findOrCreateDeviceProfile(tenant.getId(), deviceType.getType()); + } catch (Exception e) { + } + } + } + pageLink = pageLink.nextPageLink(); + } while (pageData.hasNext()); + + log.info("Updating device profiles..."); + conn.createStatement().execute("call update_device_profiles()"); + + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.1.1", "schema_update_after.sql"); + loadSql(schemaUpdateFile, conn); + + conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3002000;"); + } + log.info("Schema updated."); + } catch (Exception e) { + log.error("Failed updating schema!!!", e); + } + break; default: throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); } @@ -243,4 +429,24 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService conn.createStatement().execute(sql); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script Thread.sleep(5000); } + + protected boolean isOldSchema(Connection conn, long fromVersion) { + boolean isOldSchema = true; + try { + Statement statement = conn.createStatement(); + statement.execute("CREATE TABLE IF NOT EXISTS tb_schema_settings ( schema_version bigint NOT NULL, CONSTRAINT tb_schema_settings_pkey PRIMARY KEY (schema_version));"); + Thread.sleep(1000); + ResultSet resultSet = statement.executeQuery("SELECT schema_version FROM tb_schema_settings;"); + if (resultSet.next()) { + isOldSchema = resultSet.getLong(1) <= fromVersion; + } else { + resultSet.close(); + statement.execute("INSERT INTO tb_schema_settings (schema_version) VALUES (" + fromVersion + ")"); + } + statement.close(); + } catch (InterruptedException | SQLException e) { + log.info("Failed to check current PostgreSQL schema due to: {}", e.getMessage()); + } + return isOldSchema; + } } diff --git a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java index e386e84132..b1c670a14c 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java @@ -19,10 +19,16 @@ public interface SystemDataLoaderService { void createSysAdmin() throws Exception; + void createDefaultTenantProfiles() throws Exception; + void createAdminSettings() throws Exception; + void createOAuth2Templates() throws Exception; + void loadSystemWidgets() throws Exception; + void updateSystemWidgets() throws Exception; + void loadDemoData() throws Exception; void deleteSystemWidgetBundle(String bundleAlias) throws Exception; diff --git a/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java index a929a51fb5..756356581a 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java @@ -177,7 +177,7 @@ public class TimescaleTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgr executeQuery(conn, "UPDATE tb_schema_settings SET schema_version = 2005001"); } break; - case "2.5.4": + case "3.1.1": break; default: throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); @@ -209,4 +209,4 @@ public class TimescaleTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgr log.info("Failed to load PostgreSQL upgrade functions due to: {}", e.getMessage()); } } -} \ No newline at end of file +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/TsLatestDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/TsLatestDatabaseSchemaService.java new file mode 100644 index 0000000000..070cc2060e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/TsLatestDatabaseSchemaService.java @@ -0,0 +1,19 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.install; + +public interface TsLatestDatabaseSchemaService extends DatabaseSchemaService { +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java b/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java index fb75a12dac..7e0ee6c539 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java +++ b/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java @@ -15,25 +15,27 @@ */ package org.thingsboard.server.service.install.cql; -import com.datastax.driver.core.BoundStatement; -import com.datastax.driver.core.DataType; -import com.datastax.driver.core.KeyspaceMetadata; -import com.datastax.driver.core.PreparedStatement; -import com.datastax.driver.core.ResultSet; -import com.datastax.driver.core.Row; -import com.datastax.driver.core.Session; -import com.datastax.driver.core.SimpleStatement; -import com.datastax.driver.core.Statement; -import com.datastax.driver.core.TableMetadata; +import com.datastax.oss.driver.api.core.cql.BoundStatementBuilder; +import com.datastax.oss.driver.api.core.cql.PreparedStatement; +import com.datastax.oss.driver.api.core.cql.ResultSet; +import com.datastax.oss.driver.api.core.cql.Row; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata; +import com.datastax.oss.driver.api.core.metadata.schema.TableMetadata; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.protocol.internal.ProtocolConstants; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVParser; import org.apache.commons.csv.CSVPrinter; import org.apache.commons.csv.CSVRecord; +import org.thingsboard.server.dao.cassandra.guava.GuavaSession; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import java.time.Instant; import java.util.ArrayList; import java.util.Date; import java.util.Iterator; @@ -44,12 +46,12 @@ import static org.thingsboard.server.service.install.DatabaseHelper.CSV_DUMP_FOR public class CassandraDbHelper { - public static Path dumpCfIfExists(KeyspaceMetadata ks, Session session, String cfName, + public static Path dumpCfIfExists(KeyspaceMetadata ks, GuavaSession session, String cfName, String[] columns, String[] defaultValues, String dumpPrefix) throws Exception { return dumpCfIfExists(ks, session, cfName, columns, defaultValues, dumpPrefix, false); } - public static Path dumpCfIfExists(KeyspaceMetadata ks, Session session, String cfName, + public static Path dumpCfIfExists(KeyspaceMetadata ks, GuavaSession session, String cfName, String[] columns, String[] defaultValues, String dumpPrefix, boolean printHeader) throws Exception { if (ks.getTable(cfName) != null) { Path dumpFile = Files.createTempFile(dumpPrefix, null); @@ -59,8 +61,8 @@ public class CassandraDbHelper { csvFormat = csvFormat.withHeader(columns); } try (CSVPrinter csvPrinter = new CSVPrinter(Files.newBufferedWriter(dumpFile), csvFormat)) { - Statement stmt = new SimpleStatement("SELECT * FROM " + cfName); - stmt.setFetchSize(1000); + Statement stmt = SimpleStatement.newInstance("SELECT * FROM " + cfName); + stmt.setPageSize(1000); ResultSet rs = session.execute(stmt); Iterator iter = rs.iterator(); while (iter.hasNext()) { @@ -95,12 +97,12 @@ public class CassandraDbHelper { Files.move(tmp, targetDumpFile, StandardCopyOption.REPLACE_EXISTING); } - public static void loadCf(KeyspaceMetadata ks, Session session, String cfName, String[] columns, Path sourceFile) throws Exception { + public static void loadCf(KeyspaceMetadata ks, GuavaSession session, String cfName, String[] columns, Path sourceFile) throws Exception { loadCf(ks, session, cfName, columns, sourceFile, false); } - public static void loadCf(KeyspaceMetadata ks, Session session, String cfName, String[] columns, Path sourceFile, boolean parseHeader) throws Exception { - TableMetadata tableMetadata = ks.getTable(cfName); + public static void loadCf(KeyspaceMetadata ks, GuavaSession session, String cfName, String[] columns, Path sourceFile, boolean parseHeader) throws Exception { + TableMetadata tableMetadata = ks.getTable(cfName).get(); PreparedStatement prepared = session.prepare(createInsertStatement(cfName, columns)); CSVFormat csvFormat = CSV_DUMP_FORMAT; if (parseHeader) { @@ -110,11 +112,11 @@ public class CassandraDbHelper { } try (CSVParser csvParser = new CSVParser(Files.newBufferedReader(sourceFile), csvFormat)) { csvParser.forEach(record -> { - BoundStatement boundStatement = prepared.bind(); + BoundStatementBuilder boundStatementBuilder = new BoundStatementBuilder(prepared.bind()); for (String column : columns) { - setColumnValue(tableMetadata, column, record, boundStatement); + setColumnValue(tableMetadata, column, record, boundStatementBuilder); } - session.execute(boundStatement); + session.execute(boundStatementBuilder.build()); }); } } @@ -136,27 +138,27 @@ public class CassandraDbHelper { } private static String getColumnValue(String column, String defaultValue, Row row) { - int index = row.getColumnDefinitions().getIndexOf(column); + int index = row.getColumnDefinitions().firstIndexOf(column); if (index > -1) { String str; - DataType type = row.getColumnDefinitions().getType(index); + DataType type = row.getColumnDefinitions().get(index).getType(); try { if (row.isNull(index)) { return null; - } else if (type == DataType.cdouble()) { + } else if (type.getProtocolCode() == ProtocolConstants.DataType.DOUBLE) { str = new Double(row.getDouble(index)).toString(); - } else if (type == DataType.cint()) { + } else if (type.getProtocolCode() == ProtocolConstants.DataType.INT) { str = new Integer(row.getInt(index)).toString(); - } else if (type == DataType.bigint()) { + } else if (type.getProtocolCode() == ProtocolConstants.DataType.BIGINT) { str = new Long(row.getLong(index)).toString(); - } else if (type == DataType.uuid()) { - str = row.getUUID(index).toString(); - } else if (type == DataType.timeuuid()) { - str = row.getUUID(index).toString(); - } else if (type == DataType.cfloat()) { + } else if (type.getProtocolCode() == ProtocolConstants.DataType.UUID) { + str = row.getUuid(index).toString(); + } else if (type.getProtocolCode() == ProtocolConstants.DataType.TIMEUUID) { + str = row.getUuid(index).toString(); + } else if (type.getProtocolCode() == ProtocolConstants.DataType.FLOAT) { str = new Float(row.getFloat(index)).toString(); - } else if (type == DataType.timestamp()) { - str = ""+row.getTimestamp(index).getTime(); + } else if (type.getProtocolCode() == ProtocolConstants.DataType.TIMESTAMP) { + str = ""+row.getInstant(index).toEpochMilli(); } else { str = row.getString(index); } @@ -186,27 +188,27 @@ public class CassandraDbHelper { } private static void setColumnValue(TableMetadata tableMetadata, String column, - CSVRecord record, BoundStatement boundStatement) { + CSVRecord record, BoundStatementBuilder boundStatementBuilder) { String value = record.get(column); - DataType type = tableMetadata.getColumn(column).getType(); + DataType type = tableMetadata.getColumn(column).get().getType(); if (value == null) { - boundStatement.setToNull(column); - } else if (type == DataType.cdouble()) { - boundStatement.setDouble(column, Double.valueOf(value)); - } else if (type == DataType.cint()) { - boundStatement.setInt(column, Integer.valueOf(value)); - } else if (type == DataType.bigint()) { - boundStatement.setLong(column, Long.valueOf(value)); - } else if (type == DataType.uuid()) { - boundStatement.setUUID(column, UUID.fromString(value)); - } else if (type == DataType.timeuuid()) { - boundStatement.setUUID(column, UUID.fromString(value)); - } else if (type == DataType.cfloat()) { - boundStatement.setFloat(column, Float.valueOf(value)); - } else if (type == DataType.timestamp()) { - boundStatement.setTimestamp(column, new Date(Long.valueOf(value))); + boundStatementBuilder.setToNull(column); + } else if (type.getProtocolCode() == ProtocolConstants.DataType.DOUBLE) { + boundStatementBuilder.setDouble(column, Double.valueOf(value)); + } else if (type.getProtocolCode() == ProtocolConstants.DataType.INT) { + boundStatementBuilder.setInt(column, Integer.valueOf(value)); + } else if (type.getProtocolCode() == ProtocolConstants.DataType.BIGINT) { + boundStatementBuilder.setLong(column, Long.valueOf(value)); + } else if (type.getProtocolCode() == ProtocolConstants.DataType.UUID) { + boundStatementBuilder.setUuid(column, UUID.fromString(value)); + } else if (type.getProtocolCode() == ProtocolConstants.DataType.TIMEUUID) { + boundStatementBuilder.setUuid(column, UUID.fromString(value)); + } else if (type.getProtocolCode() == ProtocolConstants.DataType.FLOAT) { + boundStatementBuilder.setFloat(column, Float.valueOf(value)); + } else if (type.getProtocolCode() == ProtocolConstants.DataType.TIMESTAMP) { + boundStatementBuilder.setInstant(column, Instant.ofEpochMilli(Long.valueOf(value))); } else { - boundStatement.setString(column, value); + boundStatementBuilder.setString(column, value); } } diff --git a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraEntitiesToSqlMigrateService.java b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraEntitiesToSqlMigrateService.java new file mode 100644 index 0000000000..77459604ef --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraEntitiesToSqlMigrateService.java @@ -0,0 +1,327 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.install.migrate; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.UUIDConverter; +import org.thingsboard.server.dao.cassandra.CassandraCluster; +import org.thingsboard.server.dao.util.NoSqlAnyDao; +import org.thingsboard.server.service.install.EntityDatabaseSchemaService; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.util.Arrays; +import java.util.List; + +import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.bigintColumn; +import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.booleanColumn; +import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.doubleColumn; +import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.enumToIntColumn; +import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.idColumn; +import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.jsonColumn; +import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.stringColumn; + +@Service +@Profile("install") +@NoSqlAnyDao +@Slf4j +public class CassandraEntitiesToSqlMigrateService implements EntitiesMigrateService { + + @Autowired + private EntityDatabaseSchemaService entityDatabaseSchemaService; + + @Autowired + protected CassandraCluster cluster; + + @Value("${spring.datasource.url}") + protected String dbUrl; + + @Value("${spring.datasource.username}") + protected String dbUserName; + + @Value("${spring.datasource.password}") + protected String dbPassword; + + @Override + public void migrate() throws Exception { + log.info("Performing migration of entities data from cassandra to SQL database ..."); + entityDatabaseSchemaService.createDatabaseSchema(false); + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + conn.setAutoCommit(false); + for (CassandraToSqlTable table: tables) { + table.migrateToSql(cluster.getSession(), conn); + } + } catch (Exception e) { + log.error("Unexpected error during ThingsBoard entities data migration!", e); + throw e; + } + entityDatabaseSchemaService.createDatabaseIndexes(); + } + + private static List tables = Arrays.asList( + new CassandraToSqlTable("admin_settings", + idColumn("id"), + stringColumn("key"), + stringColumn("json_value")), + new CassandraToSqlTable("alarm", + idColumn("id"), + idColumn("tenant_id"), + stringColumn("type"), + idColumn("originator_id"), + enumToIntColumn("originator_type", EntityType.class), + stringColumn("severity"), + stringColumn("status"), + bigintColumn("start_ts"), + bigintColumn("end_ts"), + bigintColumn("ack_ts"), + bigintColumn("clear_ts"), + stringColumn("details", "additional_info"), + booleanColumn("propagate"), + stringColumn("propagate_relation_types")), + new CassandraToSqlTable("asset", + idColumn("id"), + idColumn("tenant_id"), + idColumn("customer_id"), + stringColumn("name"), + stringColumn("type"), + stringColumn("label"), + stringColumn("search_text"), + stringColumn("additional_info")) { + @Override + protected boolean onConstraintViolation(List batchData, + CassandraToSqlColumnData[] data, String constraint) { + if (constraint.equalsIgnoreCase("asset_name_unq_key")) { + this.handleUniqueNameViolation(data, "asset"); + return true; + } + return super.onConstraintViolation(batchData, data, constraint); + } + }, + new CassandraToSqlTable("audit_log_by_tenant_id", "audit_log", + idColumn("id"), + idColumn("tenant_id"), + idColumn("customer_id"), + idColumn("entity_id"), + stringColumn("entity_type"), + stringColumn("entity_name"), + idColumn("user_id"), + stringColumn("user_name"), + stringColumn("action_type"), + stringColumn("action_data"), + stringColumn("action_status"), + stringColumn("action_failure_details")), + new CassandraToSqlTable("attributes_kv_cf", "attribute_kv", + idColumn("entity_id"), + stringColumn("entity_type"), + stringColumn("attribute_type"), + stringColumn("attribute_key"), + booleanColumn("bool_v", true), + stringColumn("str_v"), + bigintColumn("long_v"), + doubleColumn("dbl_v"), + jsonColumn("json_v"), + bigintColumn("last_update_ts")), + new CassandraToSqlTable("component_descriptor", + idColumn("id"), + stringColumn("type"), + stringColumn("scope"), + stringColumn("name"), + stringColumn("search_text"), + stringColumn("clazz"), + stringColumn("configuration_descriptor"), + stringColumn("actions")) { + @Override + protected boolean onConstraintViolation(List batchData, + CassandraToSqlColumnData[] data, String constraint) { + if (constraint.equalsIgnoreCase("component_descriptor_clazz_key")) { + String clazz = this.getColumnData(data, "clazz").getValue(); + log.warn("Found component_descriptor record with duplicate clazz [{}]. Record will be ignored!", clazz); + this.ignoreRecord(batchData, data); + return true; + } + return super.onConstraintViolation(batchData, data, constraint); + } + }, + new CassandraToSqlTable("customer", + idColumn("id"), + idColumn("tenant_id"), + stringColumn("title"), + stringColumn("search_text"), + stringColumn("country"), + stringColumn("state"), + stringColumn("city"), + stringColumn("address"), + stringColumn("address2"), + stringColumn("zip"), + stringColumn("phone"), + stringColumn("email"), + stringColumn("additional_info")), + new CassandraToSqlTable("dashboard", + idColumn("id"), + idColumn("tenant_id"), + stringColumn("title"), + stringColumn("search_text"), + stringColumn("assigned_customers"), + stringColumn("configuration")), + new CassandraToSqlTable("device", + idColumn("id"), + idColumn("tenant_id"), + idColumn("customer_id"), + stringColumn("name"), + stringColumn("type"), + stringColumn("label"), + stringColumn("search_text"), + stringColumn("additional_info")) { + @Override + protected boolean onConstraintViolation(List batchData, + CassandraToSqlColumnData[] data, String constraint) { + if (constraint.equalsIgnoreCase("device_name_unq_key")) { + this.handleUniqueNameViolation(data, "device"); + return true; + } + return super.onConstraintViolation(batchData, data, constraint); + } + }, + new CassandraToSqlTable("device_credentials", + idColumn("id"), + idColumn("device_id"), + stringColumn("credentials_type"), + stringColumn("credentials_id"), + stringColumn("credentials_value")), + new CassandraToSqlTable("event", + idColumn("id"), + idColumn("tenant_id"), + idColumn("entity_id"), + stringColumn("entity_type"), + stringColumn("event_type"), + stringColumn("event_uid"), + stringColumn("body"), + new CassandraToSqlEventTsColumn()), + new CassandraToSqlTable("relation", + idColumn("from_id"), + stringColumn("from_type"), + idColumn("to_id"), + stringColumn("to_type"), + stringColumn("relation_type_group"), + stringColumn("relation_type"), + stringColumn("additional_info")), + new CassandraToSqlTable("user", "tb_user", + idColumn("id"), + idColumn("tenant_id"), + idColumn("customer_id"), + stringColumn("email"), + stringColumn("search_text"), + stringColumn("authority"), + stringColumn("first_name"), + stringColumn("last_name"), + stringColumn("additional_info")) { + @Override + protected boolean onConstraintViolation(List batchData, + CassandraToSqlColumnData[] data, String constraint) { + if (constraint.equalsIgnoreCase("tb_user_email_key")) { + this.handleUniqueEmailViolation(data); + return true; + } + return super.onConstraintViolation(batchData, data, constraint); + } + }, + new CassandraToSqlTable("tenant", + idColumn("id"), + stringColumn("title"), + stringColumn("search_text"), + stringColumn("region"), + stringColumn("country"), + stringColumn("state"), + stringColumn("city"), + stringColumn("address"), + stringColumn("address2"), + stringColumn("zip"), + stringColumn("phone"), + stringColumn("email"), + stringColumn("additional_info"), + booleanColumn("isolated_tb_core"), + booleanColumn("isolated_tb_rule_engine")), + new CassandraToSqlTable("user_credentials", + idColumn("id"), + idColumn("user_id"), + booleanColumn("enabled"), + stringColumn("password"), + stringColumn("activate_token"), + stringColumn("reset_token")) { + @Override + protected boolean onConstraintViolation(List batchData, + CassandraToSqlColumnData[] data, String constraint) { + if (constraint.equalsIgnoreCase("user_credentials_user_id_key")) { + String id = UUIDConverter.fromString(this.getColumnData(data, "id").getValue()).toString(); + log.warn("Found user credentials record with duplicate user_id [id:[{}]]. Record will be ignored!", id); + this.ignoreRecord(batchData, data); + return true; + } + return super.onConstraintViolation(batchData, data, constraint); + } + }, + new CassandraToSqlTable("widget_type", + idColumn("id"), + idColumn("tenant_id"), + stringColumn("bundle_alias"), + stringColumn("alias"), + stringColumn("name"), + stringColumn("descriptor")), + new CassandraToSqlTable("widgets_bundle", + idColumn("id"), + idColumn("tenant_id"), + stringColumn("alias"), + stringColumn("title"), + stringColumn("search_text")), + new CassandraToSqlTable("rule_chain", + idColumn("id"), + idColumn("tenant_id"), + stringColumn("name"), + stringColumn("search_text"), + idColumn("first_rule_node_id"), + booleanColumn("root"), + booleanColumn("debug_mode"), + stringColumn("configuration"), + stringColumn("additional_info")), + new CassandraToSqlTable("rule_node", + idColumn("id"), + idColumn("rule_chain_id"), + stringColumn("type"), + stringColumn("name"), + booleanColumn("debug_mode"), + stringColumn("search_text"), + stringColumn("configuration"), + stringColumn("additional_info")), + new CassandraToSqlTable("entity_view", + idColumn("id"), + idColumn("tenant_id"), + idColumn("customer_id"), + idColumn("entity_id"), + stringColumn("entity_type"), + stringColumn("name"), + stringColumn("type"), + stringColumn("keys"), + bigintColumn("start_ts"), + bigintColumn("end_ts"), + stringColumn("search_text"), + stringColumn("additional_info")) + ); +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumn.java b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumn.java new file mode 100644 index 0000000000..6ad7c54743 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumn.java @@ -0,0 +1,178 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.install.migrate; + +import com.datastax.oss.driver.api.core.cql.Row; +import lombok.Data; +import org.thingsboard.server.common.data.UUIDConverter; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; +import java.util.regex.Pattern; + +@Data +public class CassandraToSqlColumn { + + private static final ThreadLocal PATTERN_THREAD_LOCAL = ThreadLocal.withInitial(() -> Pattern.compile(String.valueOf(Character.MIN_VALUE))); + private static final String EMPTY_STR = ""; + + private int index; + private int sqlIndex; + private String cassandraColumnName; + private String sqlColumnName; + private CassandraToSqlColumnType type; + private int sqlType; + private int size; + private Class enumClass; + private boolean allowNullBoolean = false; + + public static CassandraToSqlColumn idColumn(String name) { + return new CassandraToSqlColumn(name, CassandraToSqlColumnType.ID); + } + + public static CassandraToSqlColumn stringColumn(String name) { + return new CassandraToSqlColumn(name, CassandraToSqlColumnType.STRING); + } + + public static CassandraToSqlColumn stringColumn(String cassandraColumnName, String sqlColumnName) { + return new CassandraToSqlColumn(cassandraColumnName, sqlColumnName); + } + + public static CassandraToSqlColumn bigintColumn(String name) { + return new CassandraToSqlColumn(name, CassandraToSqlColumnType.BIGINT); + } + + public static CassandraToSqlColumn doubleColumn(String name) { + return new CassandraToSqlColumn(name, CassandraToSqlColumnType.DOUBLE); + } + + public static CassandraToSqlColumn booleanColumn(String name) { + return booleanColumn(name, false); + } + + public static CassandraToSqlColumn booleanColumn(String name, boolean allowNullBoolean) { + return new CassandraToSqlColumn(name, name, CassandraToSqlColumnType.BOOLEAN, null, allowNullBoolean); + } + + public static CassandraToSqlColumn jsonColumn(String name) { + return new CassandraToSqlColumn(name, CassandraToSqlColumnType.JSON); + } + + public static CassandraToSqlColumn enumToIntColumn(String name, Class enumClass) { + return new CassandraToSqlColumn(name, CassandraToSqlColumnType.ENUM_TO_INT, enumClass); + } + + public CassandraToSqlColumn(String columnName) { + this(columnName, columnName, CassandraToSqlColumnType.STRING, null, false); + } + + public CassandraToSqlColumn(String columnName, CassandraToSqlColumnType type) { + this(columnName, columnName, type, null, false); + } + + public CassandraToSqlColumn(String columnName, CassandraToSqlColumnType type, Class enumClass) { + this(columnName, columnName, type, enumClass, false); + } + + public CassandraToSqlColumn(String cassandraColumnName, String sqlColumnName) { + this(cassandraColumnName, sqlColumnName, CassandraToSqlColumnType.STRING, null, false); + } + + public CassandraToSqlColumn(String cassandraColumnName, String sqlColumnName, CassandraToSqlColumnType type, + Class enumClass, boolean allowNullBoolean) { + this.cassandraColumnName = cassandraColumnName; + this.sqlColumnName = sqlColumnName; + this.type = type; + this.enumClass = enumClass; + this.allowNullBoolean = allowNullBoolean; + } + + public String getColumnValue(Row row) { + if (row.isNull(index)) { + if (this.type == CassandraToSqlColumnType.BOOLEAN && !this.allowNullBoolean) { + return Boolean.toString(false); + } else { + return null; + } + } else { + switch (this.type) { + case ID: + return UUIDConverter.fromTimeUUID(row.getUuid(index)); + case DOUBLE: + return Double.toString(row.getDouble(index)); + case INTEGER: + return Integer.toString(row.getInt(index)); + case FLOAT: + return Float.toString(row.getFloat(index)); + case BIGINT: + return Long.toString(row.getLong(index)); + case BOOLEAN: + return Boolean.toString(row.getBoolean(index)); + case STRING: + case JSON: + case ENUM_TO_INT: + default: + String value = row.getString(index); + return this.replaceNullChars(value); + } + } + } + + public void setColumnValue(PreparedStatement sqlInsertStatement, String value) throws SQLException { + if (value == null) { + sqlInsertStatement.setNull(this.sqlIndex, this.sqlType); + } else { + switch (this.type) { + case DOUBLE: + sqlInsertStatement.setDouble(this.sqlIndex, Double.parseDouble(value)); + break; + case INTEGER: + sqlInsertStatement.setInt(this.sqlIndex, Integer.parseInt(value)); + break; + case FLOAT: + sqlInsertStatement.setFloat(this.sqlIndex, Float.parseFloat(value)); + break; + case BIGINT: + sqlInsertStatement.setLong(this.sqlIndex, Long.parseLong(value)); + break; + case BOOLEAN: + sqlInsertStatement.setBoolean(this.sqlIndex, Boolean.parseBoolean(value)); + break; + case ENUM_TO_INT: + Enum enumVal = Enum.valueOf(this.enumClass, value); + int intValue = enumVal.ordinal(); + sqlInsertStatement.setInt(this.sqlIndex, intValue); + break; + case JSON: + case STRING: + case ID: + default: + sqlInsertStatement.setString(this.sqlIndex, value); + break; + } + } + } + + private String replaceNullChars(String strValue) { + if (strValue != null) { + return PATTERN_THREAD_LOCAL.get().matcher(strValue).replaceAll(EMPTY_STR); + } + return strValue; + } + +} + diff --git a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumnData.java b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumnData.java new file mode 100644 index 0000000000..6f7052e56f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumnData.java @@ -0,0 +1,64 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.install.migrate; + +import lombok.Data; + +@Data +public class CassandraToSqlColumnData { + + private String value; + private String originalValue; + private int constraintCounter = 0; + + public CassandraToSqlColumnData(String value) { + this.value = value; + this.originalValue = value; + } + + public int nextContraintCounter() { + return ++constraintCounter; + } + + public String getNextConstraintStringValue(CassandraToSqlColumn column) { + int counter = this.nextContraintCounter(); + String newValue = this.originalValue + counter; + int overflow = newValue.length() - column.getSize(); + if (overflow > 0) { + newValue = this.originalValue.substring(0, this.originalValue.length()-overflow) + counter; + } + return newValue; + } + + public String getNextConstraintEmailValue(CassandraToSqlColumn column) { + int counter = this.nextContraintCounter(); + String[] emailValues = this.originalValue.split("@"); + String newValue = emailValues[0] + "+" + counter + "@" + emailValues[1]; + int overflow = newValue.length() - column.getSize(); + if (overflow > 0) { + newValue = emailValues[0].substring(0, emailValues[0].length()-overflow) + "+" + counter + "@" + emailValues[1]; + } + return newValue; + } + + public String getLogValue() { + if (this.value != null && this.value.length() > 255) { + return this.value.substring(0, 255) + "...[truncated " + (this.value.length() - 255) + " symbols]"; + } + return this.value; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumnType.java b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumnType.java new file mode 100644 index 0000000000..f97b01ae77 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumnType.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.install.migrate; + +public enum CassandraToSqlColumnType { + ID, + DOUBLE, + INTEGER, + FLOAT, + BIGINT, + BOOLEAN, + STRING, + JSON, + ENUM_TO_INT +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlEventTsColumn.java b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlEventTsColumn.java new file mode 100644 index 0000000000..9966e7cbce --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlEventTsColumn.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.install.migrate; + +import com.datastax.oss.driver.api.core.cql.Row; + +import java.util.UUID; + +import static org.thingsboard.server.dao.model.ModelConstants.EPOCH_DIFF; + +public class CassandraToSqlEventTsColumn extends CassandraToSqlColumn { + + CassandraToSqlEventTsColumn() { + super("id", "ts", CassandraToSqlColumnType.BIGINT, null, false); + } + + @Override + public String getColumnValue(Row row) { + UUID id = row.getUuid(getIndex()); + long ts = getTs(id); + return ts + ""; + } + + private long getTs(UUID uuid) { + return (uuid.timestamp() - EPOCH_DIFF) / 10000; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlTable.java b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlTable.java new file mode 100644 index 0000000000..b18f48ec85 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlTable.java @@ -0,0 +1,304 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.install.migrate; + +import com.datastax.oss.driver.api.core.cql.ResultSet; +import com.datastax.oss.driver.api.core.cql.Row; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.Statement; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.internal.util.JdbcExceptionHelper; +import org.postgresql.util.PSQLException; +import org.thingsboard.server.common.data.UUIDConverter; +import org.thingsboard.server.dao.cassandra.guava.GuavaSession; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; + +@Data +@Slf4j +public class CassandraToSqlTable { + + private static final int DEFAULT_BATCH_SIZE = 10000; + + private String cassandraCf; + private String sqlTableName; + + private List columns; + + private int batchSize = DEFAULT_BATCH_SIZE; + + private PreparedStatement sqlInsertStatement; + + public CassandraToSqlTable(String tableName, CassandraToSqlColumn... columns) { + this(tableName, tableName, DEFAULT_BATCH_SIZE, columns); + } + + public CassandraToSqlTable(String tableName, String sqlTableName, CassandraToSqlColumn... columns) { + this(tableName, sqlTableName, DEFAULT_BATCH_SIZE, columns); + } + + public CassandraToSqlTable(String tableName, int batchSize, CassandraToSqlColumn... columns) { + this(tableName, tableName, batchSize, columns); + } + + public CassandraToSqlTable(String cassandraCf, String sqlTableName, int batchSize, CassandraToSqlColumn... columns) { + this.cassandraCf = cassandraCf; + this.sqlTableName = sqlTableName; + this.batchSize = batchSize; + this.columns = Arrays.asList(columns); + for (int i=0;i iter = rs.iterator(); + int rowCounter = 0; + List batchData; + boolean hasNext; + do { + batchData = this.extractBatchData(iter); + hasNext = batchData.size() == this.batchSize; + this.batchInsert(batchData, conn); + rowCounter += batchData.size(); + log.info("[{}] {} records migrated so far...", this.sqlTableName, rowCounter); + } while (hasNext); + this.sqlInsertStatement.close(); + log.info("[{}] {} total records migrated.", this.sqlTableName, rowCounter); + log.info("[{}] Finished migration data from cassandra '{}' Column Family to '{}' SQL table.", + this.sqlTableName, this.cassandraCf, this.sqlTableName); + } + + private List extractBatchData(Iterator iter) { + List batchData = new ArrayList<>(); + while (iter.hasNext() && batchData.size() < this.batchSize) { + Row row = iter.next(); + if (row != null) { + CassandraToSqlColumnData[] data = this.extractRowData(row); + batchData.add(data); + } + } + return batchData; + } + + private CassandraToSqlColumnData[] extractRowData(Row row) { + CassandraToSqlColumnData[] data = new CassandraToSqlColumnData[this.columns.size()]; + for (CassandraToSqlColumn column: this.columns) { + String value = column.getColumnValue(row); + data[column.getIndex()] = new CassandraToSqlColumnData(value); + } + return this.validateColumnData(data); + } + + protected CassandraToSqlColumnData[] validateColumnData(CassandraToSqlColumnData[] data) { + for (int i=0;i column.getSize()) { + log.warn("[{}] Value size [{}] exceeds maximum size [{}] of column [{}] and will be truncated!", + this.sqlTableName, + value.length(), column.getSize(), column.getSqlColumnName()); + log.warn("[{}] Affected data:\n{}", this.sqlTableName, this.dataToString(data)); + value = value.substring(0, column.getSize()); + columnData.setOriginalValue(value); + columnData.setValue(value); + } + } + } + return data; + } + + protected void batchInsert(List batchData, Connection conn) throws SQLException { + boolean retry = false; + for (CassandraToSqlColumnData[] data : batchData) { + for (CassandraToSqlColumn column: this.columns) { + column.setColumnValue(this.sqlInsertStatement, data[column.getIndex()].getValue()); + } + try { + this.sqlInsertStatement.executeUpdate(); + } catch (SQLException e) { + if (this.handleInsertException(batchData, data, conn, e)) { + retry = true; + break; + } else { + throw e; + } + } + } + if (retry) { + this.batchInsert(batchData, conn); + } else { + conn.commit(); + } + } + + private boolean handleInsertException(List batchData, + CassandraToSqlColumnData[] data, + Connection conn, SQLException ex) throws SQLException { + conn.commit(); + String constraint = extractConstraintName(ex).orElse(null); + if (constraint != null) { + if (this.onConstraintViolation(batchData, data, constraint)) { + return true; + } else { + log.error("[{}] Unhandled constraint violation [{}] during insert!", this.sqlTableName, constraint); + log.error("[{}] Affected data:\n{}", this.sqlTableName, this.dataToString(data)); + } + } else { + log.error("[{}] Unhandled exception during insert!", this.sqlTableName); + log.error("[{}] Affected data:\n{}", this.sqlTableName, this.dataToString(data)); + } + return false; + } + + private String dataToString(CassandraToSqlColumnData[] data) { + StringBuffer stringData = new StringBuffer("{\n"); + for (int i=0;i batchData, + CassandraToSqlColumnData[] data, String constraint) { + return false; + } + + protected void handleUniqueNameViolation(CassandraToSqlColumnData[] data, String entityType) { + CassandraToSqlColumn nameColumn = this.getColumn("name"); + CassandraToSqlColumn searchTextColumn = this.getColumn("search_text"); + CassandraToSqlColumnData nameColumnData = data[nameColumn.getIndex()]; + CassandraToSqlColumnData searchTextColumnData = data[searchTextColumn.getIndex()]; + String prevName = nameColumnData.getValue(); + String newName = nameColumnData.getNextConstraintStringValue(nameColumn); + nameColumnData.setValue(newName); + searchTextColumnData.setValue(searchTextColumnData.getNextConstraintStringValue(searchTextColumn)); + String id = UUIDConverter.fromString(this.getColumnData(data, "id").getValue()).toString(); + log.warn("Found {} with duplicate name [id:[{}]]. Attempting to rename {} from '{}' to '{}'...", entityType, id, entityType, prevName, newName); + } + + protected void handleUniqueEmailViolation(CassandraToSqlColumnData[] data) { + CassandraToSqlColumn emailColumn = this.getColumn("email"); + CassandraToSqlColumn searchTextColumn = this.getColumn("search_text"); + CassandraToSqlColumnData emailColumnData = data[emailColumn.getIndex()]; + CassandraToSqlColumnData searchTextColumnData = data[searchTextColumn.getIndex()]; + String prevEmail = emailColumnData.getValue(); + String newEmail = emailColumnData.getNextConstraintEmailValue(emailColumn); + emailColumnData.setValue(newEmail); + searchTextColumnData.setValue(searchTextColumnData.getNextConstraintEmailValue(searchTextColumn)); + String id = UUIDConverter.fromString(this.getColumnData(data, "id").getValue()).toString(); + log.warn("Found user with duplicate email [id:[{}]]. Attempting to rename email from '{}' to '{}'...", id, prevEmail, newEmail); + } + + protected void ignoreRecord(List batchData, CassandraToSqlColumnData[] data) { + log.warn("[{}] Affected data:\n{}", this.sqlTableName, this.dataToString(data)); + int index = batchData.indexOf(data); + if (index > 0) { + batchData.remove(index); + } + } + + protected CassandraToSqlColumn getColumn(String sqlColumnName) { + return this.columns.stream().filter(col -> col.getSqlColumnName().equals(sqlColumnName)).findFirst().get(); + } + + protected CassandraToSqlColumnData getColumnData(CassandraToSqlColumnData[] data, String sqlColumnName) { + CassandraToSqlColumn column = this.getColumn(sqlColumnName); + return data[column.getIndex()]; + } + + private Optional extractConstraintName(SQLException ex) { + final String sqlState = JdbcExceptionHelper.extractSqlState( ex ); + if (sqlState != null) { + String sqlStateClassCode = JdbcExceptionHelper.determineSqlStateClassCode( sqlState ); + if ( sqlStateClassCode != null ) { + if (Arrays.asList( + "23", // "integrity constraint violation" + "27", // "triggered data change violation" + "44" // "with check option violation" + ).contains(sqlStateClassCode)) { + if (ex instanceof PSQLException) { + return Optional.of(((PSQLException)ex).getServerErrorMessage().getConstraint()); + } + } + } + } + return Optional.empty(); + } + + protected Statement createCassandraSelectStatement() { + StringBuilder selectStatementBuilder = new StringBuilder(); + selectStatementBuilder.append("SELECT "); + for (CassandraToSqlColumn column : columns) { + selectStatementBuilder.append(column.getCassandraColumnName()).append(","); + } + selectStatementBuilder.deleteCharAt(selectStatementBuilder.length() - 1); + selectStatementBuilder.append(" FROM ").append(cassandraCf); + return SimpleStatement.newInstance(selectStatementBuilder.toString()); + } + + private PreparedStatement createSqlInsertStatement(Connection conn) throws SQLException { + StringBuilder insertStatementBuilder = new StringBuilder(); + insertStatementBuilder.append("INSERT INTO ").append(this.sqlTableName).append(" ("); + for (CassandraToSqlColumn column : columns) { + insertStatementBuilder.append(column.getSqlColumnName()).append(","); + } + insertStatementBuilder.deleteCharAt(insertStatementBuilder.length() - 1); + insertStatementBuilder.append(") VALUES ("); + for (CassandraToSqlColumn column : columns) { + if (column.getType() == CassandraToSqlColumnType.JSON) { + insertStatementBuilder.append("cast(? AS json)"); + } else { + insertStatementBuilder.append("?"); + } + insertStatementBuilder.append(","); + } + insertStatementBuilder.deleteCharAt(insertStatementBuilder.length() - 1); + insertStatementBuilder.append(")"); + return conn.prepareStatement(insertStatementBuilder.toString()); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraTsLatestToSqlMigrateService.java b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraTsLatestToSqlMigrateService.java new file mode 100644 index 0000000000..f1fa9a43f0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraTsLatestToSqlMigrateService.java @@ -0,0 +1,233 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.install.migrate; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.hibernate.exception.ConstraintViolationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.UUIDConverter; +import org.thingsboard.server.dao.cassandra.CassandraCluster; +import org.thingsboard.server.dao.model.sqlts.dictionary.TsKvDictionary; +import org.thingsboard.server.dao.model.sqlts.dictionary.TsKvDictionaryCompositeKey; +import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; +import org.thingsboard.server.dao.sqlts.dictionary.TsKvDictionaryRepository; +import org.thingsboard.server.dao.sqlts.insert.latest.InsertLatestTsRepository; +import org.thingsboard.server.dao.util.NoSqlTsDao; +import org.thingsboard.server.dao.util.SqlTsLatestDao; +import org.thingsboard.server.service.install.InstallScripts; + +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.sql.Connection; +import java.sql.DriverManager; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; + +import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.bigintColumn; +import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.booleanColumn; +import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.doubleColumn; +import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.idColumn; +import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.jsonColumn; +import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.stringColumn; + +@Service +@Profile("install") +@NoSqlTsDao +@SqlTsLatestDao +@Slf4j +public class CassandraTsLatestToSqlMigrateService implements TsLatestMigrateService { + + private static final int MAX_KEY_LENGTH = 255; + private static final int MAX_STR_V_LENGTH = 10000000; + + @Autowired + private InsertLatestTsRepository insertLatestTsRepository; + + @Autowired + protected CassandraCluster cluster; + + @Autowired + protected TsKvDictionaryRepository dictionaryRepository; + + @Autowired + private InstallScripts installScripts; + + @Value("${spring.datasource.url}") + protected String dbUrl; + + @Value("${spring.datasource.username}") + protected String dbUserName; + + @Value("${spring.datasource.password}") + protected String dbPassword; + + private final ConcurrentMap tsKvDictionaryMap = new ConcurrentHashMap<>(); + + protected static final ReentrantLock tsCreationLock = new ReentrantLock(); + + @Override + public void migrate() throws Exception { + log.info("Performing migration of latest timeseries data from cassandra to SQL database ..."); + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.0.1", "schema_ts_latest.sql"); + loadSql(schemaUpdateFile, conn); + conn.setAutoCommit(false); + for (CassandraToSqlTable table : tables) { + table.migrateToSql(cluster.getSession(), conn); + } + } catch (Exception e) { + log.error("Unexpected error during ThingsBoard entities data migration!", e); + throw e; + } + } + + private List tables = Arrays.asList( + new CassandraToSqlTable("ts_kv_latest_cf", "ts_kv_latest", + idColumn("entity_id"), + stringColumn("key"), + bigintColumn("ts"), + booleanColumn("bool_v"), + stringColumn("str_v"), + bigintColumn("long_v"), + doubleColumn("dbl_v"), + jsonColumn("json_v")) { + + @Override + protected void batchInsert(List batchData, Connection conn) { + insertLatestTsRepository + .saveOrUpdate(batchData.stream().map(data -> getTsKvLatestEntity(data)).collect(Collectors.toList())); + } + + @Override + protected CassandraToSqlColumnData[] validateColumnData(CassandraToSqlColumnData[] data) { + return data; + } + }); + + private TsKvLatestEntity getTsKvLatestEntity(CassandraToSqlColumnData[] data) { + TsKvLatestEntity latestEntity = new TsKvLatestEntity(); + latestEntity.setEntityId(UUIDConverter.fromString(data[0].getValue())); + latestEntity.setKey(getOrSaveKeyId(data[1].getValue())); + latestEntity.setTs(Long.parseLong(data[2].getValue())); + + String strV = data[4].getValue(); + if (strV != null) { + if (strV.length() > MAX_STR_V_LENGTH) { + log.warn("[ts_kv_latest] Value size [{}] exceeds maximum size [{}] of column [str_v] and will be truncated!", + strV.length(), MAX_STR_V_LENGTH); + log.warn("Affected data:\n{}", strV); + strV = strV.substring(0, MAX_STR_V_LENGTH); + } + latestEntity.setStrValue(strV); + } else { + Long longV = null; + try { + longV = Long.parseLong(data[5].getValue()); + } catch (Exception e) { + } + if (longV != null) { + latestEntity.setLongValue(longV); + } else { + Double doubleV = null; + try { + doubleV = Double.parseDouble(data[6].getValue()); + } catch (Exception e) { + } + if (doubleV != null) { + latestEntity.setDoubleValue(doubleV); + } else { + + String jsonV = data[7].getValue(); + if (StringUtils.isNoneEmpty(jsonV)) { + latestEntity.setJsonValue(jsonV); + } else { + Boolean boolV = null; + try { + boolV = Boolean.parseBoolean(data[3].getValue()); + } catch (Exception e) { + } + if (boolV != null) { + latestEntity.setBooleanValue(boolV); + } else { + log.warn("All values in key-value row are nullable "); + } + } + } + } + } + return latestEntity; + } + + protected Integer getOrSaveKeyId(String strKey) { + if (strKey.length() > MAX_KEY_LENGTH) { + log.warn("[ts_kv_latest] Value size [{}] exceeds maximum size [{}] of column [key] and will be truncated!", + strKey.length(), MAX_KEY_LENGTH); + log.warn("Affected data:\n{}", strKey); + strKey = strKey.substring(0, MAX_KEY_LENGTH); + } + + Integer keyId = tsKvDictionaryMap.get(strKey); + if (keyId == null) { + Optional tsKvDictionaryOptional; + tsKvDictionaryOptional = dictionaryRepository.findById(new TsKvDictionaryCompositeKey(strKey)); + if (!tsKvDictionaryOptional.isPresent()) { + tsCreationLock.lock(); + try { + tsKvDictionaryOptional = dictionaryRepository.findById(new TsKvDictionaryCompositeKey(strKey)); + if (!tsKvDictionaryOptional.isPresent()) { + TsKvDictionary tsKvDictionary = new TsKvDictionary(); + tsKvDictionary.setKey(strKey); + try { + TsKvDictionary saved = dictionaryRepository.save(tsKvDictionary); + tsKvDictionaryMap.put(saved.getKey(), saved.getKeyId()); + keyId = saved.getKeyId(); + } catch (ConstraintViolationException e) { + tsKvDictionaryOptional = dictionaryRepository.findById(new TsKvDictionaryCompositeKey(strKey)); + TsKvDictionary dictionary = tsKvDictionaryOptional.orElseThrow(() -> new RuntimeException("Failed to get TsKvDictionary entity from DB!")); + tsKvDictionaryMap.put(dictionary.getKey(), dictionary.getKeyId()); + keyId = dictionary.getKeyId(); + } + } else { + keyId = tsKvDictionaryOptional.get().getKeyId(); + } + } finally { + tsCreationLock.unlock(); + } + } else { + keyId = tsKvDictionaryOptional.get().getKeyId(); + tsKvDictionaryMap.put(strKey, keyId); + } + } + return keyId; + } + + private void loadSql(Path sqlFile, Connection conn) throws Exception { + String sql = new String(Files.readAllBytes(sqlFile), Charset.forName("UTF-8")); + conn.createStatement().execute(sql); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script + Thread.sleep(5000); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/migrate/EntitiesMigrateService.java b/application/src/main/java/org/thingsboard/server/service/install/migrate/EntitiesMigrateService.java new file mode 100644 index 0000000000..944401062f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/migrate/EntitiesMigrateService.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.install.migrate; + +public interface EntitiesMigrateService { + + void migrate() throws Exception; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/migrate/TsLatestMigrateService.java b/application/src/main/java/org/thingsboard/server/service/install/migrate/TsLatestMigrateService.java new file mode 100644 index 0000000000..491cac2c99 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/migrate/TsLatestMigrateService.java @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.install.migrate; + +public interface TsLatestMigrateService { + + void migrate() throws Exception; +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index 0d2401b0fd..aded7868bd 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -15,20 +15,51 @@ */ package org.thingsboard.server.service.install.update; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.thingsboard.rule.engine.profile.TbDeviceProfileNode; +import org.thingsboard.rule.engine.profile.TbDeviceProfileNodeConfiguration; +import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.SearchTextBased; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UUIDBased; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; +import org.thingsboard.server.common.data.kv.ReadTsKvQuery; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.dao.timeseries.TimeseriesService; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; import org.thingsboard.server.service.install.InstallScripts; +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +import static org.apache.commons.lang.StringUtils.isBlank; +import static org.thingsboard.server.service.install.DatabaseHelper.objectMapper; + @Service @Profile("install") @Slf4j @@ -43,6 +74,12 @@ public class DefaultDataUpdateService implements DataUpdateService { @Autowired private InstallScripts installScripts; + @Autowired + private EntityViewService entityViewService; + + @Autowired + private TimeseriesService tsService; + @Override public void updateData(String fromVersion) throws Exception { switch (fromVersion) { @@ -50,6 +87,14 @@ public class DefaultDataUpdateService implements DataUpdateService { log.info("Updating data from version 1.4.0 to 2.0.0 ..."); tenantsDefaultRuleChainUpdater.updateEntities(null); break; + case "3.0.1": + log.info("Updating data from version 3.0.1 to 3.1.0 ..."); + tenantsEntityViewsUpdater.updateEntities(null); + break; + case "3.1.1": + log.info("Updating data from version 3.1.1 to 3.2.0 ..."); + tenantsRootRuleChainUpdater.updateEntities(null); + break; default: throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion); } @@ -59,7 +104,7 @@ public class DefaultDataUpdateService implements DataUpdateService { new PaginatedUpdater() { @Override - protected TextPageData findEntities(String region, TextPageLink pageLink) { + protected PageData findEntities(String region, PageLink pageLink) { return tenantService.findTenants(pageLink); } @@ -76,4 +121,127 @@ public class DefaultDataUpdateService implements DataUpdateService { } }; -} \ No newline at end of file + private PaginatedUpdater tenantsRootRuleChainUpdater = + new PaginatedUpdater() { + + @Override + protected PageData findEntities(String region, PageLink pageLink) { + return tenantService.findTenants(pageLink); + } + + @Override + protected void updateEntity(Tenant tenant) { + try { + RuleChain ruleChain = ruleChainService.getRootTenantRuleChain(tenant.getId()); + if (ruleChain == null) { + installScripts.createDefaultRuleChains(tenant.getId()); + } else { + RuleChainMetaData md = ruleChainService.loadRuleChainMetaData(tenant.getId(), ruleChain.getId()); + int oldIdx = md.getFirstNodeIndex(); + int newIdx = md.getNodes().size(); + + if (md.getNodes().size() < oldIdx) { + // Skip invalid rule chains + return; + } + + RuleNode oldFirstNode = md.getNodes().get(oldIdx); + if (oldFirstNode.getType().equals(TbDeviceProfileNode.class.getName())) { + // No need to update the rule node twice. + return; + } + + RuleNode ruleNode = new RuleNode(); + ruleNode.setRuleChainId(ruleChain.getId()); + ruleNode.setName("Device Profile Node"); + ruleNode.setType(TbDeviceProfileNode.class.getName()); + ruleNode.setDebugMode(false); + TbDeviceProfileNodeConfiguration ruleNodeConfiguration = new TbDeviceProfileNodeConfiguration().defaultConfiguration(); + ruleNode.setConfiguration(JacksonUtil.valueToTree(ruleNodeConfiguration)); + ObjectNode additionalInfo = JacksonUtil.newObjectNode(); + additionalInfo.put("description", "Process incoming messages from devices with the alarm rules defined in the device profile. Dispatch all incoming messages with \"Success\" relation type."); + additionalInfo.put("layoutX", 204); + additionalInfo.put("layoutY", 240); + ruleNode.setAdditionalInfo(additionalInfo); + + md.getNodes().add(ruleNode); + md.setFirstNodeIndex(newIdx); + md.addConnectionInfo(newIdx, oldIdx, "Success"); + ruleChainService.saveRuleChainMetaData(tenant.getId(), md); + } + } catch (Exception e) { + log.error("[{}] Unable to update Tenant: {}", tenant.getId(), tenant.getName(), e); + } + } + }; + + private PaginatedUpdater tenantsEntityViewsUpdater = + new PaginatedUpdater() { + + @Override + protected PageData findEntities(String region, PageLink pageLink) { + return tenantService.findTenants(pageLink); + } + + @Override + protected void updateEntity(Tenant tenant) { + updateTenantEntityViews(tenant.getId()); + } + }; + + private void updateTenantEntityViews(TenantId tenantId) { + PageLink pageLink = new PageLink(100); + PageData pageData = entityViewService.findEntityViewByTenantId(tenantId, pageLink); + boolean hasNext = true; + while (hasNext) { + List>> updateFutures = new ArrayList<>(); + for (EntityView entityView : pageData.getData()) { + updateFutures.add(updateEntityViewLatestTelemetry(entityView)); + } + + try { + Futures.allAsList(updateFutures).get(); + } catch (InterruptedException | ExecutionException e) { + log.error("Failed to copy latest telemetry to entity view", e); + } + + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + pageData = entityViewService.findEntityViewByTenantId(tenantId, pageLink); + } else { + hasNext = false; + } + } + } + + private ListenableFuture> updateEntityViewLatestTelemetry(EntityView entityView) { + EntityViewId entityId = entityView.getId(); + List keys = entityView.getKeys() != null && entityView.getKeys().getTimeseries() != null ? + entityView.getKeys().getTimeseries() : Collections.emptyList(); + long startTs = entityView.getStartTimeMs(); + long endTs = entityView.getEndTimeMs() == 0 ? Long.MAX_VALUE : entityView.getEndTimeMs(); + ListenableFuture> keysFuture; + if (keys.isEmpty()) { + keysFuture = Futures.transform(tsService.findAllLatest(TenantId.SYS_TENANT_ID, + entityView.getEntityId()), latest -> latest.stream().map(TsKvEntry::getKey).collect(Collectors.toList()), MoreExecutors.directExecutor()); + } else { + keysFuture = Futures.immediateFuture(keys); + } + ListenableFuture> latestFuture = Futures.transformAsync(keysFuture, fetchKeys -> { + List queries = fetchKeys.stream().filter(key -> !isBlank(key)).map(key -> new BaseReadTsKvQuery(key, startTs, endTs, 1, "DESC")).collect(Collectors.toList()); + if (!queries.isEmpty()) { + return tsService.findAll(TenantId.SYS_TENANT_ID, entityView.getEntityId(), queries); + } else { + return Futures.immediateFuture(null); + } + }, MoreExecutors.directExecutor()); + return Futures.transformAsync(latestFuture, latestValues -> { + if (latestValues != null && !latestValues.isEmpty()) { + ListenableFuture> saveFuture = tsService.saveLatest(TenantId.SYS_TENANT_ID, entityId, latestValues); + return saveFuture; + } + return Futures.immediateFuture(null); + }, MoreExecutors.directExecutor()); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/PaginatedUpdater.java b/application/src/main/java/org/thingsboard/server/service/install/update/PaginatedUpdater.java index d2d551421e..970bc2746e 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/PaginatedUpdater.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/PaginatedUpdater.java @@ -17,29 +17,29 @@ package org.thingsboard.server.service.install.update; import org.thingsboard.server.common.data.SearchTextBased; import org.thingsboard.server.common.data.id.UUIDBased; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; public abstract class PaginatedUpdater> { private static final int DEFAULT_LIMIT = 100; public void updateEntities(I id) { - TextPageLink pageLink = new TextPageLink(DEFAULT_LIMIT); + PageLink pageLink = new PageLink(DEFAULT_LIMIT); boolean hasNext = true; while (hasNext) { - TextPageData entities = findEntities(id, pageLink); + PageData entities = findEntities(id, pageLink); for (D entity : entities.getData()) { updateEntity(entity); } hasNext = entities.hasNext(); if (hasNext) { - pageLink = entities.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } } - protected abstract TextPageData findEntities(I id, TextPageLink pageLink); + protected abstract PageData findEntities(I id, PageLink pageLink); protected abstract void updateEntity(D entity); diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java index c951add112..bcf541880c 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java @@ -16,18 +16,17 @@ package org.thingsboard.server.service.mail; import com.fasterxml.jackson.databind.JsonNode; +import freemarker.template.Configuration; +import freemarker.template.Template; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import org.apache.velocity.VelocityContext; -import org.apache.velocity.app.VelocityEngine; -import org.apache.velocity.exception.VelocityException; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.MessageSource; import org.springframework.core.NestedRuntimeException; import org.springframework.mail.javamail.JavaMailSenderImpl; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Service; +import org.springframework.ui.freemarker.FreeMarkerTemplateUtils; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; @@ -40,8 +39,6 @@ import org.thingsboard.server.dao.settings.AdminSettingsService; import javax.annotation.PostConstruct; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; -import java.io.StringWriter; -import java.io.Writer; import java.util.HashMap; import java.util.Locale; import java.util.Map; @@ -58,8 +55,7 @@ public class DefaultMailService implements MailService { private MessageSource messages; @Autowired - @Qualifier("velocityEngine") - private VelocityEngine engine; + private Configuration freemarkerConfig; private JavaMailSenderImpl mailSender; @@ -112,8 +108,11 @@ public class DefaultMailService implements MailService { } } javaMailProperties.put(MAIL_PROP + protocol + ".starttls.enable", enableTls); - if (enableTls && jsonConfig.has("tlsVersion") && StringUtils.isNoneEmpty(jsonConfig.get("tlsVersion").asText())) { - javaMailProperties.put(MAIL_PROP + protocol + ".ssl.protocols", jsonConfig.get("tlsVersion").asText()); + if (enableTls && jsonConfig.has("tlsVersion") && !jsonConfig.get("tlsVersion").isNull()) { + String tlsVersion = jsonConfig.get("tlsVersion").asText(); + if (StringUtils.isNoneEmpty(tlsVersion)) { + javaMailProperties.put(MAIL_PROP + protocol + ".ssl.protocols", tlsVersion); + } } boolean enableProxy = jsonConfig.has("enableProxy") && jsonConfig.get("enableProxy").asBoolean(); @@ -152,11 +151,10 @@ public class DefaultMailService implements MailService { String mailFrom = jsonConfig.get("mailFrom").asText(); String subject = messages.getMessage("test.message.subject", null, Locale.US); - Map model = new HashMap(); + Map model = new HashMap<>(); model.put(TARGET_EMAIL, email); - String message = mergeTemplateIntoString(this.engine, - "test.vm", UTF_8, model); + String message = mergeTemplateIntoString("test.ftl", model); sendMail(testMailSender, mailFrom, email, subject, message); } @@ -166,12 +164,11 @@ public class DefaultMailService implements MailService { String subject = messages.getMessage("activation.subject", null, Locale.US); - Map model = new HashMap(); + Map model = new HashMap<>(); model.put("activationLink", activationLink); model.put(TARGET_EMAIL, email); - String message = mergeTemplateIntoString(this.engine, - "activation.vm", UTF_8, model); + String message = mergeTemplateIntoString("activation.ftl", model); sendMail(mailSender, mailFrom, email, subject, message); } @@ -181,12 +178,11 @@ public class DefaultMailService implements MailService { String subject = messages.getMessage("account.activated.subject", null, Locale.US); - Map model = new HashMap(); + Map model = new HashMap<>(); model.put("loginLink", loginLink); model.put(TARGET_EMAIL, email); - String message = mergeTemplateIntoString(this.engine, - "account.activated.vm", UTF_8, model); + String message = mergeTemplateIntoString("account.activated.ftl", model); sendMail(mailSender, mailFrom, email, subject, message); } @@ -196,12 +192,11 @@ public class DefaultMailService implements MailService { String subject = messages.getMessage("reset.password.subject", null, Locale.US); - Map model = new HashMap(); + Map model = new HashMap<>(); model.put("passwordResetLink", passwordResetLink); model.put(TARGET_EMAIL, email); - String message = mergeTemplateIntoString(this.engine, - "reset.password.vm", UTF_8, model); + String message = mergeTemplateIntoString("reset.password.ftl", model); sendMail(mailSender, mailFrom, email, subject, message); } @@ -211,12 +206,11 @@ public class DefaultMailService implements MailService { String subject = messages.getMessage("password.was.reset.subject", null, Locale.US); - Map model = new HashMap(); + Map model = new HashMap<>(); model.put("loginLink", loginLink); model.put(TARGET_EMAIL, email); - String message = mergeTemplateIntoString(this.engine, - "password.was.reset.vm", UTF_8, model); + String message = mergeTemplateIntoString("password.was.reset.ftl", model); sendMail(mailSender, mailFrom, email, subject, message); } @@ -242,13 +236,12 @@ public class DefaultMailService implements MailService { public void sendAccountLockoutEmail(String lockoutEmail, String email, Integer maxFailedLoginAttempts) throws ThingsboardException { String subject = messages.getMessage("account.lockout.subject", null, Locale.US); - Map model = new HashMap(); + Map model = new HashMap<>(); model.put("lockoutAccount", lockoutEmail); model.put("maxFailedLoginAttempts", maxFailedLoginAttempts); model.put(TARGET_EMAIL, email); - String message = mergeTemplateIntoString(this.engine, - "account.lockout.vm", UTF_8, model); + String message = mergeTemplateIntoString("account.lockout.ftl", model); sendMail(mailSender, mailFrom, email, subject, message); } @@ -269,20 +262,14 @@ public class DefaultMailService implements MailService { } } - private static String mergeTemplateIntoString(VelocityEngine velocityEngine, String templateLocation, - String encoding, Map model) throws VelocityException { - - StringWriter result = new StringWriter(); - mergeTemplate(velocityEngine, templateLocation, encoding, model, result); - return result.toString(); - } - - private static void mergeTemplate( - VelocityEngine velocityEngine, String templateLocation, String encoding, - Map model, Writer writer) throws VelocityException { - - VelocityContext velocityContext = new VelocityContext(model); - velocityEngine.mergeTemplate(templateLocation, encoding, velocityContext, writer); + private String mergeTemplateIntoString(String templateLocation, + Map model) throws ThingsboardException { + try { + Template template = freemarkerConfig.getTemplate(templateLocation); + return FreeMarkerTemplateUtils.processTemplateIntoString(template, model); + } catch (Exception e) { + throw handleException(e); + } } protected ThingsboardException handleException(Exception exception) { diff --git a/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java b/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java new file mode 100644 index 0000000000..3db23c65e2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java @@ -0,0 +1,162 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.profile; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.device.DeviceProfileService; +import org.thingsboard.server.dao.device.DeviceService; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +@Service +@Slf4j +public class DefaultTbDeviceProfileCache implements TbDeviceProfileCache { + + private final Lock deviceProfileFetchLock = new ReentrantLock(); + private final DeviceProfileService deviceProfileService; + private final DeviceService deviceService; + + private final ConcurrentMap deviceProfilesMap = new ConcurrentHashMap<>(); + private final ConcurrentMap devicesMap = new ConcurrentHashMap<>(); + private final ConcurrentMap>> profileListeners = new ConcurrentHashMap<>(); + private final ConcurrentMap>> deviceProfileListeners = new ConcurrentHashMap<>(); + + public DefaultTbDeviceProfileCache(DeviceProfileService deviceProfileService, DeviceService deviceService) { + this.deviceProfileService = deviceProfileService; + this.deviceService = deviceService; + } + + @Override + public DeviceProfile get(TenantId tenantId, DeviceProfileId deviceProfileId) { + DeviceProfile profile = deviceProfilesMap.get(deviceProfileId); + if (profile == null) { + deviceProfileFetchLock.lock(); + try { + profile = deviceProfilesMap.get(deviceProfileId); + if (profile == null) { + profile = deviceProfileService.findDeviceProfileById(tenantId, deviceProfileId); + if (profile != null) { + deviceProfilesMap.put(deviceProfileId, profile); + log.debug("[{}] Fetch device profile into cache: {}", profile.getId(), profile); + } + } + } finally { + deviceProfileFetchLock.unlock(); + } + } + log.trace("[{}] Found device profile in cache: {}", deviceProfileId, profile); + return profile; + } + + @Override + public DeviceProfile get(TenantId tenantId, DeviceId deviceId) { + DeviceProfileId profileId = devicesMap.get(deviceId); + if (profileId == null) { + Device device = deviceService.findDeviceById(tenantId, deviceId); + if (device != null) { + profileId = device.getDeviceProfileId(); + devicesMap.put(deviceId, profileId); + } else { + return null; + } + } + return get(tenantId, profileId); + } + + @Override + public void evict(TenantId tenantId, DeviceProfileId profileId) { + DeviceProfile oldProfile = deviceProfilesMap.remove(profileId); + log.debug("[{}] evict device profile from cache: {}", profileId, oldProfile); + DeviceProfile newProfile = get(tenantId, profileId); + if (newProfile != null) { + notifyProfileListeners(newProfile); + } + } + + @Override + public void evict(TenantId tenantId, DeviceId deviceId) { + DeviceProfileId old = devicesMap.remove(deviceId); + if (old != null) { + DeviceProfile newProfile = get(tenantId, deviceId); + if (newProfile == null || !old.equals(newProfile.getId())) { + notifyDeviceListeners(tenantId, deviceId, newProfile); + } + } + } + + @Override + public void addListener(TenantId tenantId, EntityId listenerId, + Consumer profileListener, + BiConsumer deviceListener) { + if (profileListener != null) { + profileListeners.computeIfAbsent(tenantId, id -> new ConcurrentHashMap<>()).put(listenerId, profileListener); + } + if (deviceListener != null) { + deviceProfileListeners.computeIfAbsent(tenantId, id -> new ConcurrentHashMap<>()).put(listenerId, deviceListener); + } + } + + @Override + public DeviceProfile find(DeviceProfileId deviceProfileId) { + return deviceProfileService.findDeviceProfileById(TenantId.SYS_TENANT_ID, deviceProfileId); + } + + @Override + public DeviceProfile findOrCreateDeviceProfile(TenantId tenantId, String profileName) { + return deviceProfileService.findOrCreateDeviceProfile(tenantId, profileName); + } + + @Override + public void removeListener(TenantId tenantId, EntityId listenerId) { + ConcurrentMap> tenantListeners = profileListeners.get(tenantId); + if (tenantListeners != null) { + tenantListeners.remove(listenerId); + } + ConcurrentMap> deviceListeners = deviceProfileListeners.get(tenantId); + if (deviceListeners != null) { + deviceListeners.remove(listenerId); + } + } + + private void notifyProfileListeners(DeviceProfile profile) { + ConcurrentMap> tenantListeners = profileListeners.get(profile.getTenantId()); + if (tenantListeners != null) { + tenantListeners.forEach((id, listener) -> listener.accept(profile)); + } + } + + private void notifyDeviceListeners(TenantId tenantId, DeviceId deviceId, DeviceProfile profile) { + if (profile != null) { + ConcurrentMap> tenantListeners = deviceProfileListeners.get(tenantId); + if (tenantListeners != null) { + tenantListeners.forEach((id, listener) -> listener.accept(deviceId, profile)); + } + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/profile/TbDeviceProfileCache.java b/application/src/main/java/org/thingsboard/server/service/profile/TbDeviceProfileCache.java new file mode 100644 index 0000000000..10e28bd299 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/profile/TbDeviceProfileCache.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.profile; + +import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.TenantId; + +public interface TbDeviceProfileCache extends RuleEngineDeviceProfileCache { + + void evict(TenantId tenantId, DeviceProfileId id); + + void evict(TenantId tenantId, DeviceId id); + + DeviceProfile find(DeviceProfileId deviceProfileId); + + DeviceProfile findOrCreateDeviceProfile(TenantId tenantId, String deviceType); +} diff --git a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java new file mode 100644 index 0000000000..b8ccad466d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java @@ -0,0 +1,103 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.query; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AlarmData; +import org.thingsboard.server.common.data.query.AlarmDataQuery; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.dao.alarm.AlarmService; +import org.thingsboard.server.dao.entity.EntityService; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.model.SecurityUser; + +import java.util.LinkedHashMap; + +@Service +@Slf4j +@TbCoreComponent +public class DefaultEntityQueryService implements EntityQueryService { + + @Autowired + private EntityService entityService; + + @Autowired + private AlarmService alarmService; + + @Value("${server.ws.max_entities_per_alarm_subscription:1000}") + private int maxEntitiesPerAlarmSubscription; + + @Override + public long countEntitiesByQuery(SecurityUser securityUser, EntityCountQuery query) { + return entityService.countEntitiesByQuery(securityUser.getTenantId(), securityUser.getCustomerId(), query); + } + + @Override + public PageData findEntityDataByQuery(SecurityUser securityUser, EntityDataQuery query) { + return entityService.findEntityDataByQuery(securityUser.getTenantId(), securityUser.getCustomerId(), query); + } + + @Override + public PageData findAlarmDataByQuery(SecurityUser securityUser, AlarmDataQuery query) { + EntityDataQuery entityDataQuery = this.buildEntityDataQuery(query); + PageData entities = entityService.findEntityDataByQuery(securityUser.getTenantId(), + securityUser.getCustomerId(), entityDataQuery); + if (entities.getTotalElements() > 0) { + LinkedHashMap entitiesMap = new LinkedHashMap<>(); + for (EntityData entityData : entities.getData()) { + entitiesMap.put(entityData.getEntityId(), entityData); + } + PageData alarms = alarmService.findAlarmDataByQueryForEntities(securityUser.getTenantId(), + securityUser.getCustomerId(), query, entitiesMap.keySet()); + for (AlarmData alarmData : alarms.getData()) { + EntityId entityId = alarmData.getEntityId(); + if (entityId != null) { + EntityData entityData = entitiesMap.get(entityId); + if (entityData != null) { + alarmData.getLatest().putAll(entityData.getLatest()); + } + } + } + return alarms; + } else { + return new PageData<>(); + } + } + + private EntityDataQuery buildEntityDataQuery(AlarmDataQuery query) { + EntityDataSortOrder sortOrder = query.getPageLink().getSortOrder(); + EntityDataSortOrder entitiesSortOrder; + if (sortOrder == null || sortOrder.getKey().getType().equals(EntityKeyType.ALARM_FIELD)) { + entitiesSortOrder = new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, ModelConstants.CREATED_TIME_PROPERTY)); + } else { + entitiesSortOrder = sortOrder; + } + EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder); + return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters()); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java b/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java new file mode 100644 index 0000000000..15f7d86252 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.query; + +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AlarmData; +import org.thingsboard.server.common.data.query.AlarmDataQuery; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.service.security.model.SecurityUser; + +public interface EntityQueryService { + + long countEntitiesByQuery(SecurityUser securityUser, EntityCountQuery query); + + PageData findEntityDataByQuery(SecurityUser securityUser, EntityDataQuery query); + + PageData findAlarmDataByQuery(SecurityUser securityUser, AlarmDataQuery query); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 926b08f38b..18f064b745 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -21,14 +21,25 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.thingsboard.rule.engine.api.msg.ToDeviceActorNotificationMsg; +import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.FromDeviceRPCResponseProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; @@ -37,10 +48,11 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotifica import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueProducer; +import org.thingsboard.server.queue.common.MultipleTbQueueCallbackWrapper; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; -import org.thingsboard.server.service.encoding.DataDecodingEncodingService; +import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.rpc.FromDeviceRpcResponse; import java.util.HashSet; @@ -64,11 +76,13 @@ public class DefaultTbClusterService implements TbClusterService { private final TbQueueProducerProvider producerProvider; private final PartitionService partitionService; private final DataDecodingEncodingService encodingService; + private final TbDeviceProfileCache deviceProfileCache; - public DefaultTbClusterService(TbQueueProducerProvider producerProvider, PartitionService partitionService, DataDecodingEncodingService encodingService) { + public DefaultTbClusterService(TbQueueProducerProvider producerProvider, PartitionService partitionService, DataDecodingEncodingService encodingService, TbDeviceProfileCache deviceProfileCache) { this.producerProvider = producerProvider; this.partitionService = partitionService; this.encodingService = encodingService; + this.deviceProfileCache = deviceProfileCache; } @Override @@ -124,6 +138,12 @@ public class DefaultTbClusterService implements TbClusterService { log.warn("[{}][{}] Received invalid message: {}", tenantId, entityId, tbMsg); return; } + } else { + if (entityId.getEntityType().equals(EntityType.DEVICE)) { + tbMsg = transformMsg(tbMsg, deviceProfileCache.get(tenantId, new DeviceId(entityId.getId()))); + } else if (entityId.getEntityType().equals(EntityType.DEVICE_PROFILE)) { + tbMsg = transformMsg(tbMsg, deviceProfileCache.get(tenantId, new DeviceProfileId(entityId.getId()))); + } } TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tenantId, entityId); log.trace("PUSHING msg: {} to:{}", tbMsg, tpi); @@ -135,6 +155,24 @@ public class DefaultTbClusterService implements TbClusterService { toRuleEngineMsgs.incrementAndGet(); } + private TbMsg transformMsg(TbMsg tbMsg, DeviceProfile deviceProfile) { + if (deviceProfile != null) { + RuleChainId targetRuleChainId = deviceProfile.getDefaultRuleChainId(); + String targetQueueName = deviceProfile.getDefaultQueueName(); + boolean isRuleChainTransform = targetRuleChainId != null && !targetRuleChainId.equals(tbMsg.getRuleChainId()); + boolean isQueueTransform = targetQueueName != null && !targetQueueName.equals(tbMsg.getQueueName()); + + if (isRuleChainTransform && isQueueTransform) { + tbMsg = TbMsg.transformMsg(tbMsg, targetRuleChainId, targetQueueName); + } else if (isRuleChainTransform) { + tbMsg = TbMsg.transformMsg(tbMsg, targetRuleChainId); + } else if (isQueueTransform) { + tbMsg = TbMsg.transformMsg(tbMsg, targetQueueName); + } + } + return tbMsg; + } + @Override public void pushNotificationToRuleEngine(String serviceId, FromDeviceRpcResponse response, TbQueueCallback callback) { TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceId); @@ -163,11 +201,81 @@ public class DefaultTbClusterService implements TbClusterService { broadcast(new ComponentLifecycleMsg(tenantId, entityId, state)); } + @Override + public void onDeviceProfileChange(DeviceProfile deviceProfile, TbQueueCallback callback) { + onEntityChange(deviceProfile.getTenantId(), deviceProfile.getId(), deviceProfile, callback); + } + + @Override + public void onTenantProfileChange(TenantProfile tenantProfile, TbQueueCallback callback) { + onEntityChange(TenantId.SYS_TENANT_ID, tenantProfile.getId(), tenantProfile, callback); + } + + @Override + public void onTenantChange(Tenant tenant, TbQueueCallback callback) { + onEntityChange(TenantId.SYS_TENANT_ID, tenant.getId(), tenant, callback); + } + + @Override + public void onApiStateChange(ApiUsageState apiUsageState, TbQueueCallback callback) { + onEntityChange(apiUsageState.getTenantId(), apiUsageState.getId(), apiUsageState, callback); + broadcast(new ComponentLifecycleMsg(apiUsageState.getTenantId(), apiUsageState.getId(), ComponentLifecycleEvent.UPDATED)); + } + + @Override + public void onDeviceProfileDelete(DeviceProfile entity, TbQueueCallback callback) { + onEntityDelete(entity.getTenantId(), entity.getId(), entity.getName(), callback); + } + + @Override + public void onTenantProfileDelete(TenantProfile entity, TbQueueCallback callback) { + onEntityDelete(TenantId.SYS_TENANT_ID, entity.getId(), entity.getName(), callback); + } + + @Override + public void onTenantDelete(Tenant entity, TbQueueCallback callback) { + onEntityDelete(TenantId.SYS_TENANT_ID, entity.getId(), entity.getName(), callback); + } + + public void onEntityChange(TenantId tenantId, EntityId entityid, T entity, TbQueueCallback callback) { + String entityName = (entity instanceof HasName) ? ((HasName) entity).getName() : entity.getClass().getName(); + log.trace("[{}][{}][{}] Processing [{}] change event", tenantId, entityid.getEntityType(), entityid.getId(), entityName); + TransportProtos.EntityUpdateMsg entityUpdateMsg = TransportProtos.EntityUpdateMsg.newBuilder() + .setEntityType(entityid.getEntityType().name()) + .setData(ByteString.copyFrom(encodingService.encode(entity))).build(); + ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setEntityUpdateMsg(entityUpdateMsg).build(); + broadcast(transportMsg, callback); + } + + private void onEntityDelete(TenantId tenantId, EntityId entityId, String name, TbQueueCallback callback) { + log.trace("[{}][{}][{}] Processing [{}] delete event", tenantId, entityId.getEntityType(), entityId.getId(), name); + TransportProtos.EntityDeleteMsg entityDeleteMsg = TransportProtos.EntityDeleteMsg.newBuilder() + .setEntityType(entityId.getEntityType().name()) + .setEntityIdMSB(entityId.getId().getMostSignificantBits()) + .setEntityIdLSB(entityId.getId().getLeastSignificantBits()) + .build(); + ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setEntityDeleteMsg(entityDeleteMsg).build(); + broadcast(transportMsg, callback); + } + + private void broadcast(ToTransportMsg transportMsg, TbQueueCallback callback) { + TbQueueProducer> toTransportNfProducer = producerProvider.getTransportNotificationsMsgProducer(); + Set tbTransportServices = partitionService.getAllServiceIds(ServiceType.TB_TRANSPORT); + TbQueueCallback proxyCallback = callback != null ? new MultipleTbQueueCallbackWrapper(tbTransportServices.size(), callback) : null; + for (String transportServiceId : tbTransportServices) { + TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_TRANSPORT, transportServiceId); + toTransportNfProducer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), transportMsg), proxyCallback); + toTransportNfs.incrementAndGet(); + } + } + private void broadcast(ComponentLifecycleMsg msg) { byte[] msgBytes = encodingService.encode(msg); TbQueueProducer> toRuleEngineProducer = producerProvider.getRuleEngineNotificationsMsgProducer(); Set tbRuleEngineServices = new HashSet<>(partitionService.getAllServiceIds(ServiceType.TB_RULE_ENGINE)); - if (msg.getEntityId().getEntityType().equals(EntityType.TENANT)) { + if (msg.getEntityId().getEntityType().equals(EntityType.TENANT) + || msg.getEntityId().getEntityType().equals(EntityType.DEVICE_PROFILE) + || msg.getEntityId().getEntityType().equals(EntityType.API_USAGE_STATE)) { TbQueueProducer> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer(); Set tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE); for (String serviceId : tbCoreServices) { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 698b2fdaa6..f56e08b10f 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -17,23 +17,45 @@ package org.thingsboard.server.service.queue; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; +import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.rule.engine.api.RpcError; import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; -import org.thingsboard.server.gen.transport.TransportProtos.*; +import org.thingsboard.server.common.stats.StatsFactory; +import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.server.gen.transport.TransportProtos.DeviceStateServiceMsgProto; +import org.thingsboard.server.gen.transport.TransportProtos.FromDeviceRPCResponseProto; +import org.thingsboard.server.gen.transport.TransportProtos.LocalSubscriptionServiceMsgProto; +import org.thingsboard.server.gen.transport.TransportProtos.SubscriptionMgrMsgProto; +import org.thingsboard.server.gen.transport.TransportProtos.TbAlarmDeleteProto; +import org.thingsboard.server.gen.transport.TransportProtos.TbAlarmUpdateProto; +import org.thingsboard.server.gen.transport.TransportProtos.TbAttributeDeleteProto; +import org.thingsboard.server.gen.transport.TransportProtos.TbAttributeUpdateProto; +import org.thingsboard.server.gen.transport.TransportProtos.TbSubscriptionCloseProto; +import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesUpdateProto; +import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionChangeEvent; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; -import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.encoding.DataDecodingEncodingService; +import org.thingsboard.server.service.apiusage.TbApiUsageStateService; +import org.thingsboard.server.service.profile.TbDeviceProfileCache; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.service.queue.processing.AbstractConsumerService; import org.thingsboard.server.service.rpc.FromDeviceRpcResponse; import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService; @@ -52,6 +74,8 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.stream.Collectors; @@ -70,32 +94,57 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> mainConsumer; private final DeviceStateService stateService; + private final TbApiUsageStateService statsService; private final TbLocalSubscriptionService localSubscriptionService; private final SubscriptionManagerService subscriptionManagerService; private final TbCoreDeviceRpcService tbCoreDeviceRpcService; private final TbCoreConsumerStats stats; + protected final TbQueueConsumer> usageStatsConsumer; + + protected volatile ExecutorService usageStatsExecutor; - public DefaultTbCoreConsumerService(TbCoreQueueFactory tbCoreQueueFactory, ActorSystemContext actorContext, - DeviceStateService stateService, TbLocalSubscriptionService localSubscriptionService, - SubscriptionManagerService subscriptionManagerService, DataDecodingEncodingService encodingService, - TbCoreDeviceRpcService tbCoreDeviceRpcService, StatsFactory statsFactory) { - super(actorContext, encodingService, tbCoreQueueFactory.createToCoreNotificationsMsgConsumer()); + public DefaultTbCoreConsumerService(TbCoreQueueFactory tbCoreQueueFactory, + ActorSystemContext actorContext, + DeviceStateService stateService, + TbLocalSubscriptionService localSubscriptionService, + SubscriptionManagerService subscriptionManagerService, + DataDecodingEncodingService encodingService, + TbCoreDeviceRpcService tbCoreDeviceRpcService, + StatsFactory statsFactory, + TbDeviceProfileCache deviceProfileCache, + TbApiUsageStateService statsService, + TbTenantProfileCache tenantProfileCache, + TbApiUsageStateService apiUsageStateService) { + super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, apiUsageStateService, tbCoreQueueFactory.createToCoreNotificationsMsgConsumer()); this.mainConsumer = tbCoreQueueFactory.createToCoreMsgConsumer(); + this.usageStatsConsumer = tbCoreQueueFactory.createToUsageStatsServiceMsgConsumer(); this.stateService = stateService; this.localSubscriptionService = localSubscriptionService; this.subscriptionManagerService = subscriptionManagerService; this.tbCoreDeviceRpcService = tbCoreDeviceRpcService; this.stats = new TbCoreConsumerStats(statsFactory); + this.statsService = statsService; } @PostConstruct public void init() { super.init("tb-core-consumer", "tb-core-notifications-consumer"); + this.usageStatsExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("tb-core-usage-stats-consumer")); } @PreDestroy public void destroy() { super.destroy(); + if (usageStatsExecutor != null) { + usageStatsExecutor.shutdownNow(); + } + } + + @EventListener(ApplicationReadyEvent.class) + @Order(value = 2) + public void onApplicationEvent(ApplicationReadyEvent event) { + super.onApplicationEvent(event); + launchUsageStatsConsumer(); } @Override @@ -103,6 +152,12 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService tpi.newByTopic(usageStatsConsumer.getTopic())) + .collect(Collectors.toSet())); } } @@ -197,11 +252,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService actorMsg = encodingService.decode(toCoreNotification.getComponentLifecycleMsg().toByteArray()); - if (actorMsg.isPresent()) { - log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg.get()); - actorContext.tellWithHighPriority(actorMsg.get()); - } + handleComponentLifecycleMsg(id, toCoreNotification.getComponentLifecycleMsg()); callback.onSuccess(); } if (statsEnabled) { @@ -209,6 +260,53 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService { + while (!stopped) { + try { + List> msgs = usageStatsConsumer.poll(getNotificationPollDuration()); + if (msgs.isEmpty()) { + continue; + } + ConcurrentMap> pendingMap = msgs.stream().collect( + Collectors.toConcurrentMap(s -> UUID.randomUUID(), Function.identity())); + CountDownLatch processingTimeoutLatch = new CountDownLatch(1); + TbPackProcessingContext> ctx = new TbPackProcessingContext<>( + processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); + pendingMap.forEach((id, msg) -> { + log.trace("[{}] Creating usage stats callback for message: {}", id, msg.getValue()); + TbCallback callback = new TbPackCallback<>(id, ctx); + try { + handleUsageStats(msg, callback); + } catch (Throwable e) { + log.warn("[{}] Failed to process usge stats: {}", id, msg, e); + callback.onFailure(e); + } + }); + if (!processingTimeoutLatch.await(getNotificationPackProcessingTimeout(), TimeUnit.MILLISECONDS)) { + ctx.getAckMap().forEach((id, msg) -> log.warn("[{}] Timeout to process usage stats: {}", id, msg.getValue())); + ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process usage stats: {}", id, msg.getValue())); + } + usageStatsConsumer.commit(); + } catch (Exception e) { + if (!stopped) { + log.warn("Failed to obtain usage stats from queue.", e); + try { + Thread.sleep(getNotificationPollDuration()); + } catch (InterruptedException e2) { + log.trace("Failed to wait until the server has capacity to handle new usage stats", e2); + } + } + } + } + log.info("TB Usage Stats Consumer stopped."); + }); + } + + private void handleUsageStats(TbProtoQueueMsg msg, TbCallback callback) { + statsService.process(msg, callback); + } + private void forwardToCoreRpcService(FromDeviceRPCResponseProto proto, TbCallback callback) { RpcError error = proto.getError() > 0 ? RpcError.values()[proto.getError()] : null; FromDeviceRpcResponse response = new FromDeviceRpcResponse(new UUID(proto.getRequestIdMSB(), proto.getRequestIdLSB()) @@ -228,6 +326,8 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService implements TbRuleEngineConsumerService { + public static final String SUCCESSFUL_STATUS = "successful"; + public static final String FAILED_STATUS = "failed"; @Value("${queue.rule-engine.poll-interval}") private long pollDuration; @Value("${queue.rule-engine.pack-processing-timeout}") @@ -98,11 +100,16 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< public DefaultTbRuleEngineConsumerService(TbRuleEngineProcessingStrategyFactory processingStrategyFactory, TbRuleEngineSubmitStrategyFactory submitStrategyFactory, TbQueueRuleEngineSettings ruleEngineSettings, - TbRuleEngineQueueFactory tbRuleEngineQueueFactory, RuleEngineStatisticsService statisticsService, - ActorSystemContext actorContext, DataDecodingEncodingService encodingService, + TbRuleEngineQueueFactory tbRuleEngineQueueFactory, + RuleEngineStatisticsService statisticsService, + ActorSystemContext actorContext, + DataDecodingEncodingService encodingService, TbRuleEngineDeviceRpcService tbDeviceRpcService, - StatsFactory statsFactory) { - super(actorContext, encodingService, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer()); + StatsFactory statsFactory, + TbDeviceProfileCache deviceProfileCache, + TbTenantProfileCache tenantProfileCache, + TbApiUsageStateService apiUsageStateService) { + super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, apiUsageStateService, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer()); this.statisticsService = statisticsService; this.ruleEngineSettings = ruleEngineSettings; this.tbRuleEngineQueueFactory = tbRuleEngineQueueFactory; @@ -170,7 +177,9 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< log.trace("[{}] Creating callback for message: {}", id, msg.getValue()); ToRuleEngineMsg toRuleEngineMsg = msg.getValue(); TenantId tenantId = new TenantId(new UUID(toRuleEngineMsg.getTenantIdMSB(), toRuleEngineMsg.getTenantIdLSB())); - TbMsgCallback callback = new TbMsgPackCallback(id, tenantId, ctx); + TbMsgCallback callback = statsEnabled ? + new TbMsgPackCallback(id, tenantId, ctx, stats.getTimer(tenantId, SUCCESSFUL_STATUS), stats.getTimer(tenantId, FAILED_STATUS)) : + new TbMsgPackCallback(id, tenantId, ctx); try { if (toRuleEngineMsg.getTbMsg() != null && !toRuleEngineMsg.getTbMsg().isEmpty()) { forwardToRuleEngineActor(configuration.getName(), tenantId, toRuleEngineMsg, callback); @@ -258,11 +267,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< protected void handleNotification(UUID id, TbProtoQueueMsg msg, TbCallback callback) throws Exception { ToRuleEngineNotificationMsg nfMsg = msg.getValue(); if (nfMsg.getComponentLifecycleMsg() != null && !nfMsg.getComponentLifecycleMsg().isEmpty()) { - Optional actorMsg = encodingService.decode(nfMsg.getComponentLifecycleMsg().toByteArray()); - if (actorMsg.isPresent()) { - log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg.get()); - actorContext.tellWithHighPriority(actorMsg.get()); - } + handleComponentLifecycleMsg(id, nfMsg.getComponentLifecycleMsg()); callback.onSuccess(); } else if (nfMsg.hasFromDeviceRpcResponse()) { TransportProtos.FromDeviceRPCResponseProto proto = nfMsg.getFromDeviceRpcResponse(); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTenantRoutingInfoService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTenantRoutingInfoService.java index aae3cef0ac..2bcf7e7e88 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTenantRoutingInfoService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTenantRoutingInfoService.java @@ -19,10 +19,12 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.queue.discovery.TenantRoutingInfo; import org.thingsboard.server.queue.discovery.TenantRoutingInfoService; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; @Slf4j @Service @@ -31,15 +33,19 @@ public class DefaultTenantRoutingInfoService implements TenantRoutingInfoService private final TenantService tenantService; - public DefaultTenantRoutingInfoService(TenantService tenantService) { + private final TbTenantProfileCache tenantProfileCache; + + public DefaultTenantRoutingInfoService(TenantService tenantService, TbTenantProfileCache tenantProfileCache) { this.tenantService = tenantService; + this.tenantProfileCache = tenantProfileCache; } @Override public TenantRoutingInfo getRoutingInfo(TenantId tenantId) { Tenant tenant = tenantService.findTenantById(tenantId); if (tenant != null) { - return new TenantRoutingInfo(tenantId, tenant.isIsolatedTbCore(), tenant.isIsolatedTbRuleEngine()); + TenantProfile tenantProfile = tenantProfileCache.get(tenant.getTenantProfileId()); + return new TenantRoutingInfo(tenantId, tenantProfile.isIsolatedTbCore(), tenantProfile.isIsolatedTbRuleEngine()); } else { throw new RuntimeException("Tenant not found!"); } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/TbClusterService.java index cc722720d5..b36af16716 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/TbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/TbClusterService.java @@ -16,6 +16,10 @@ package org.thingsboard.server.service.queue; import org.thingsboard.rule.engine.api.msg.ToDeviceActorNotificationMsg; +import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; @@ -49,4 +53,17 @@ public interface TbClusterService { void onEntityStateChange(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent state); + void onDeviceProfileChange(DeviceProfile deviceProfile, TbQueueCallback callback); + + void onDeviceProfileDelete(DeviceProfile deviceProfile, TbQueueCallback callback); + + void onTenantProfileChange(TenantProfile tenantProfile, TbQueueCallback callback); + + void onTenantProfileDelete(TenantProfile tenantProfile, TbQueueCallback callback); + + void onTenantChange(Tenant tenant, TbQueueCallback callback); + + void onTenantDelete(Tenant tenant, TbQueueCallback callback); + + void onApiStateChange(ApiUsageState apiUsageState, TbQueueCallback callback); } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackCallback.java b/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackCallback.java index 5712f29eac..376c64a17d 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackCallback.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackCallback.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.queue; +import io.micrometer.core.instrument.Timer; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; @@ -23,28 +24,45 @@ import org.thingsboard.server.common.msg.queue.RuleNodeInfo; import org.thingsboard.server.common.msg.queue.TbMsgCallback; import java.util.UUID; +import java.util.concurrent.TimeUnit; @Slf4j public class TbMsgPackCallback implements TbMsgCallback { private final UUID id; private final TenantId tenantId; private final TbMsgPackProcessingContext ctx; + private final long startMsgProcessing; + private final Timer successfulMsgTimer; + private final Timer failedMsgTimer; public TbMsgPackCallback(UUID id, TenantId tenantId, TbMsgPackProcessingContext ctx) { + this(id, tenantId, ctx, null, null); + } + + public TbMsgPackCallback(UUID id, TenantId tenantId, TbMsgPackProcessingContext ctx, Timer successfulMsgTimer, Timer failedMsgTimer) { this.id = id; this.tenantId = tenantId; this.ctx = ctx; + this.successfulMsgTimer = successfulMsgTimer; + this.failedMsgTimer = failedMsgTimer; + startMsgProcessing = System.currentTimeMillis(); } @Override public void onSuccess() { log.trace("[{}] ON SUCCESS", id); + if (successfulMsgTimer != null) { + successfulMsgTimer.record(System.currentTimeMillis() - startMsgProcessing, TimeUnit.MILLISECONDS); + } ctx.onSuccess(id); } @Override public void onFailure(RuleEngineException e) { log.trace("[{}] ON FAILURE", id, e); + if (failedMsgTimer != null) { + failedMsgTimer.record(System.currentTimeMillis() - startMsgProcessing, TimeUnit.MILLISECONDS); + } ctx.onFailure(tenantId, id, e); } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbRuleEngineConsumerStats.java b/application/src/main/java/org/thingsboard/server/service/queue/TbRuleEngineConsumerStats.java index 41004b41f5..011e3c0d59 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/TbRuleEngineConsumerStats.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/TbRuleEngineConsumerStats.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.service.queue; +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.Timer; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.queue.RuleEngineException; @@ -41,6 +43,8 @@ public class TbRuleEngineConsumerStats { public static final String SUCCESSFUL_ITERATIONS = "successfulIterations"; public static final String FAILED_ITERATIONS = "failedIterations"; + private final StatsFactory statsFactory; + private final StatsCounter totalMsgCounter; private final StatsCounter successMsgCounter; private final StatsCounter tmpTimeoutMsgCounter; @@ -54,12 +58,14 @@ public class TbRuleEngineConsumerStats { private final List counters = new ArrayList<>(); private final ConcurrentMap tenantStats = new ConcurrentHashMap<>(); + private final ConcurrentMap tenantMsgProcessTimers = new ConcurrentHashMap<>(); private final ConcurrentMap tenantExceptions = new ConcurrentHashMap<>(); private final String queueName; public TbRuleEngineConsumerStats(String queueName, StatsFactory statsFactory) { this.queueName = queueName; + this.statsFactory = statsFactory; String statsKey = StatsType.RULE_ENGINE.getName() + "." + queueName; this.totalMsgCounter = statsFactory.createStatsCounter(statsKey, TOTAL_MSGS); @@ -82,6 +88,14 @@ public class TbRuleEngineConsumerStats { counters.add(failedIterationsCounter); } + public Timer getTimer(TenantId tenantId, String status){ + return tenantMsgProcessTimers.computeIfAbsent(tenantId, + id -> statsFactory.createTimer(StatsType.RULE_ENGINE.getName() + "." + queueName, + "tenantId", tenantId.getId().toString(), + "status", status + )); + } + public void log(TbRuleEngineProcessingResult msg, boolean finalIterationForPack) { int success = msg.getSuccessMap().size(); int pending = msg.getPendingMap().size(); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java index 4007c9e17d..0dfc9c5742 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java @@ -15,23 +15,36 @@ */ package org.thingsboard.server.service.queue.processing; +import com.google.protobuf.ByteString; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.ApplicationListener; import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.msg.TbActorMsg; +import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionChangeEvent; -import org.thingsboard.server.service.encoding.DataDecodingEncodingService; +import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; +import org.thingsboard.server.service.apiusage.TbApiUsageStateService; +import org.thingsboard.server.service.profile.TbDeviceProfileCache; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.service.queue.TbPackCallback; import org.thingsboard.server.service.queue.TbPackProcessingContext; import javax.annotation.PreDestroy; import java.util.List; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -51,12 +64,19 @@ public abstract class AbstractConsumerService> nfConsumer; - public AbstractConsumerService(ActorSystemContext actorContext, DataDecodingEncodingService encodingService, TbQueueConsumer> nfConsumer) { + public AbstractConsumerService(ActorSystemContext actorContext, DataDecodingEncodingService encodingService, + TbTenantProfileCache tenantProfileCache, TbDeviceProfileCache deviceProfileCache, TbApiUsageStateService apiUsageStateService, TbQueueConsumer> nfConsumer) { this.actorContext = actorContext; this.encodingService = encodingService; + this.tenantProfileCache = tenantProfileCache; + this.deviceProfileCache = deviceProfileCache; + this.apiUsageStateService = apiUsageStateService; this.nfConsumer = nfConsumer; } @@ -66,6 +86,7 @@ public abstract class AbstractConsumerService actorMsgOpt = encodingService.decode(nfMsg.toByteArray()); + if (actorMsgOpt.isPresent()) { + TbActorMsg actorMsg = actorMsgOpt.get(); + if (actorMsg instanceof ComponentLifecycleMsg) { + ComponentLifecycleMsg componentLifecycleMsg = (ComponentLifecycleMsg) actorMsg; + if (EntityType.TENANT_PROFILE.equals(componentLifecycleMsg.getEntityId().getEntityType())) { + TenantProfileId tenantProfileId = new TenantProfileId(componentLifecycleMsg.getEntityId().getId()); + tenantProfileCache.evict(tenantProfileId); + if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.UPDATED)) { + apiUsageStateService.onTenantProfileUpdate(tenantProfileId); + } + } else if (EntityType.TENANT.equals(componentLifecycleMsg.getEntityId().getEntityType())) { + tenantProfileCache.evict(componentLifecycleMsg.getTenantId()); + if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.UPDATED)) { + apiUsageStateService.onTenantUpdate(componentLifecycleMsg.getTenantId()); + } + } else if (EntityType.DEVICE_PROFILE.equals(componentLifecycleMsg.getEntityId().getEntityType())) { + deviceProfileCache.evict(componentLifecycleMsg.getTenantId(), new DeviceProfileId(componentLifecycleMsg.getEntityId().getId())); + } else if (EntityType.DEVICE.equals(componentLifecycleMsg.getEntityId().getEntityType())) { + deviceProfileCache.evict(componentLifecycleMsg.getTenantId(), new DeviceId(componentLifecycleMsg.getEntityId().getId())); + } else if (EntityType.API_USAGE_STATE.equals(componentLifecycleMsg.getEntityId().getEntityType())) { + apiUsageStateService.onApiUsageStateUpdate(componentLifecycleMsg.getTenantId()); + } + } + log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg); + actorContext.tellWithHighPriority(actorMsg); + } + } + protected abstract void handleNotification(UUID id, TbProtoQueueMsg msg, TbCallback callback) throws Exception; @PreDestroy public void destroy() { stopped = true; - stopMainConsumers(); - if (nfConsumer != null) { nfConsumer.unsubscribe(); } - if (consumersExecutor != null) { consumersExecutor.shutdownNow(); } diff --git a/application/src/main/java/org/thingsboard/server/service/script/AbstractJsInvokeService.java b/application/src/main/java/org/thingsboard/server/service/script/AbstractJsInvokeService.java index a903698e3c..8d9fa898dd 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/AbstractJsInvokeService.java +++ b/application/src/main/java/org/thingsboard/server/service/script/AbstractJsInvokeService.java @@ -19,6 +19,10 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.ApiUsageRecordKey; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.queue.usagestats.TbApiUsageClient; +import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import java.util.Map; import java.util.UUID; @@ -33,9 +37,16 @@ import java.util.concurrent.atomic.AtomicInteger; @Slf4j public abstract class AbstractJsInvokeService implements JsInvokeService { + private final TbApiUsageStateService apiUsageStateService; + private final TbApiUsageClient apiUsageClient; protected ScheduledExecutorService timeoutExecutorService; protected Map scriptIdToNameMap = new ConcurrentHashMap<>(); - protected Map blackListedFunctions = new ConcurrentHashMap<>(); + protected Map disabledFunctions = new ConcurrentHashMap<>(); + + protected AbstractJsInvokeService(TbApiUsageStateService apiUsageStateService, TbApiUsageClient apiUsageClient) { + this.apiUsageStateService = apiUsageStateService; + this.apiUsageClient = apiUsageClient; + } public void init(long maxRequestsTimeout) { if (maxRequestsTimeout > 0) { @@ -50,24 +61,33 @@ public abstract class AbstractJsInvokeService implements JsInvokeService { } @Override - public ListenableFuture eval(JsScriptType scriptType, String scriptBody, String... argNames) { - UUID scriptId = UUID.randomUUID(); - String functionName = "invokeInternal_" + scriptId.toString().replace('-', '_'); - String jsScript = generateJsScript(scriptType, functionName, scriptBody, argNames); - return doEval(scriptId, functionName, jsScript); + public ListenableFuture eval(TenantId tenantId, JsScriptType scriptType, String scriptBody, String... argNames) { + if (apiUsageStateService.getApiUsageState(tenantId).isJsExecEnabled()) { + UUID scriptId = UUID.randomUUID(); + String functionName = "invokeInternal_" + scriptId.toString().replace('-', '_'); + String jsScript = generateJsScript(scriptType, functionName, scriptBody, argNames); + return doEval(scriptId, functionName, jsScript); + } else { + return Futures.immediateFailedFuture(new RuntimeException("JS Execution is disabled due to API limits!")); + } } @Override - public ListenableFuture invokeFunction(UUID scriptId, Object... args) { - String functionName = scriptIdToNameMap.get(scriptId); - if (functionName == null) { - return Futures.immediateFailedFuture(new RuntimeException("No compiled script found for scriptId: [" + scriptId + "]!")); - } - if (!isBlackListed(scriptId)) { - return doInvokeFunction(scriptId, functionName, args); + public ListenableFuture invokeFunction(TenantId tenantId, UUID scriptId, Object... args) { + if (apiUsageStateService.getApiUsageState(tenantId).isJsExecEnabled()) { + String functionName = scriptIdToNameMap.get(scriptId); + if (functionName == null) { + return Futures.immediateFailedFuture(new RuntimeException("No compiled script found for scriptId: [" + scriptId + "]!")); + } + if (!isDisabled(scriptId)) { + apiUsageClient.report(tenantId, ApiUsageRecordKey.JS_EXEC_COUNT, 1); + return doInvokeFunction(scriptId, functionName, args); + } else { + return Futures.immediateFailedFuture( + new RuntimeException("Script invocation is blocked due to maximum error count " + getMaxErrors() + "!")); + } } else { - return Futures.immediateFailedFuture( - new RuntimeException("Script is blacklisted due to maximum error count " + getMaxErrors() + "!")); + return Futures.immediateFailedFuture(new RuntimeException("JS Execution is disabled due to API limits!")); } } @@ -77,7 +97,7 @@ public abstract class AbstractJsInvokeService implements JsInvokeService { if (functionName != null) { try { scriptIdToNameMap.remove(scriptId); - blackListedFunctions.remove(scriptId); + disabledFunctions.remove(scriptId); doRelease(scriptId, functionName); } catch (Exception e) { return Futures.immediateFailedFuture(e); @@ -97,7 +117,7 @@ public abstract class AbstractJsInvokeService implements JsInvokeService { protected abstract long getMaxBlacklistDuration(); protected void onScriptExecutionError(UUID scriptId) { - blackListedFunctions.computeIfAbsent(scriptId, key -> new BlackListInfo()).incrementAndGet(); + disabledFunctions.computeIfAbsent(scriptId, key -> new DisableListInfo()).incrementAndGet(); } private String generateJsScript(JsScriptType scriptType, String functionName, String scriptBody, String... argNames) { @@ -107,11 +127,11 @@ public abstract class AbstractJsInvokeService implements JsInvokeService { throw new RuntimeException("No script factory implemented for scriptType: " + scriptType); } - private boolean isBlackListed(UUID scriptId) { - BlackListInfo errorCount = blackListedFunctions.get(scriptId); + private boolean isDisabled(UUID scriptId) { + DisableListInfo errorCount = disabledFunctions.get(scriptId); if (errorCount != null) { if (errorCount.getExpirationTime() <= System.currentTimeMillis()) { - blackListedFunctions.remove(scriptId); + disabledFunctions.remove(scriptId); return false; } else { return errorCount.get() >= getMaxErrors(); @@ -121,11 +141,11 @@ public abstract class AbstractJsInvokeService implements JsInvokeService { } } - private class BlackListInfo { + private class DisableListInfo { private final AtomicInteger counter; private long expirationTime; - private BlackListInfo() { + private DisableListInfo() { this.counter = new AtomicInteger(0); } diff --git a/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsInvokeService.java b/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsInvokeService.java index 8e6666bbba..db8bb68b4e 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsInvokeService.java +++ b/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsInvokeService.java @@ -24,10 +24,10 @@ import delight.nashornsandbox.NashornSandboxes; import jdk.nashorn.api.scripting.NashornScriptEngineFactory; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; -import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.queue.usagestats.TbApiUsageClient; +import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; @@ -38,7 +38,6 @@ import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -57,9 +56,8 @@ public abstract class AbstractNashornJsInvokeService extends AbstractJsInvokeSer private final FutureCallback evalCallback = new JsStatCallback<>(jsEvalMsgs, jsTimeoutMsgs, jsFailedMsgs); private final FutureCallback invokeCallback = new JsStatCallback<>(jsInvokeMsgs, jsTimeoutMsgs, jsFailedMsgs); - @Autowired @Getter - private JsExecutorService jsExecutor; + private final JsExecutorService jsExecutor; @Value("${js.local.max_requests_timeout:0}") private long maxRequestsTimeout; @@ -67,6 +65,11 @@ public abstract class AbstractNashornJsInvokeService extends AbstractJsInvokeSer @Value("${js.local.stats.enabled:false}") private boolean statsEnabled; + public AbstractNashornJsInvokeService(TbApiUsageStateService apiUsageStateService, TbApiUsageClient apiUsageClient, JsExecutorService jsExecutor) { + super(apiUsageStateService, apiUsageClient); + this.jsExecutor = jsExecutor; + } + @Scheduled(fixedDelayString = "${js.local.stats.print_interval_ms:10000}") public void printStats() { if (statsEnabled) { diff --git a/application/src/main/java/org/thingsboard/server/service/script/JsExecutorService.java b/application/src/main/java/org/thingsboard/server/service/script/JsExecutorService.java index 97e40e6f62..15fd8c7534 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/JsExecutorService.java +++ b/application/src/main/java/org/thingsboard/server/service/script/JsExecutorService.java @@ -27,7 +27,7 @@ public class JsExecutorService extends AbstractListeningExecutor { @Override protected int getThreadPollSize() { - return jsExecutorThreadPoolSize; + return Math.max(jsExecutorThreadPoolSize, 1); } } diff --git a/application/src/main/java/org/thingsboard/server/service/script/JsInvokeService.java b/application/src/main/java/org/thingsboard/server/service/script/JsInvokeService.java index a9f727e4a7..ae990af66c 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/JsInvokeService.java +++ b/application/src/main/java/org/thingsboard/server/service/script/JsInvokeService.java @@ -17,14 +17,15 @@ package org.thingsboard.server.service.script; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; import java.util.UUID; public interface JsInvokeService { - ListenableFuture eval(JsScriptType scriptType, String scriptBody, String... argNames); + ListenableFuture eval(TenantId tenantId, JsScriptType scriptType, String scriptBody, String... argNames); - ListenableFuture invokeFunction(UUID scriptId, Object... args); + ListenableFuture invokeFunction(TenantId tenantId, UUID scriptId, Object... args); ListenableFuture release(UUID scriptId); diff --git a/application/src/main/java/org/thingsboard/server/service/script/NashornJsInvokeService.java b/application/src/main/java/org/thingsboard/server/service/script/NashornJsInvokeService.java index 66a14cc827..0dd45d340c 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/NashornJsInvokeService.java +++ b/application/src/main/java/org/thingsboard/server/service/script/NashornJsInvokeService.java @@ -19,6 +19,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; +import org.thingsboard.server.queue.usagestats.TbApiUsageClient; +import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import java.util.concurrent.TimeUnit; @@ -42,6 +44,10 @@ public class NashornJsInvokeService extends AbstractNashornJsInvokeService { @Value("${js.local.max_black_list_duration_sec:60}") private int maxBlackListDurationSec; + public NashornJsInvokeService(TbApiUsageStateService apiUsageStateService, TbApiUsageClient apiUsageClient, JsExecutorService jsExecutor) { + super(apiUsageStateService, apiUsageClient, jsExecutor); + } + @Override protected boolean useJsSandbox() { return useJsSandbox; diff --git a/application/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java b/application/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java index cfb0c8a8ee..7b4114dce6 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java +++ b/application/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java @@ -30,6 +30,8 @@ import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.usagestats.TbApiUsageClient; +import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import javax.annotation.Nullable; import javax.annotation.PostConstruct; @@ -68,6 +70,10 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService { private final AtomicInteger queueFailedMsgs = new AtomicInteger(0); private final AtomicInteger queueTimeoutMsgs = new AtomicInteger(0); + public RemoteJsInvokeService(TbApiUsageStateService apiUsageStateService, TbApiUsageClient apiUsageClient) { + super(apiUsageStateService, apiUsageClient); + } + @Scheduled(fixedDelayString = "${js.remote.stats.print_interval_ms}") public void printStats() { if (statsEnabled) { diff --git a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java index 110f410fad..275e645ce2 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java +++ b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java @@ -25,6 +25,7 @@ import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -43,13 +44,15 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S private final JsInvokeService sandboxService; private final UUID scriptId; + private final TenantId tenantId; private final EntityId entityId; - public RuleNodeJsScriptEngine(JsInvokeService sandboxService, EntityId entityId, String script, String... argNames) { + public RuleNodeJsScriptEngine(TenantId tenantId, JsInvokeService sandboxService, EntityId entityId, String script, String... argNames) { + this.tenantId = tenantId; this.sandboxService = sandboxService; this.entityId = entityId; try { - this.scriptId = this.sandboxService.eval(JsScriptType.RULE_NODE_SCRIPT, script, argNames).get(); + this.scriptId = this.sandboxService.eval(tenantId, JsScriptType.RULE_NODE_SCRIPT, script, argNames).get(); } catch (Exception e) { Throwable t = e; if (e instanceof ExecutionException) { @@ -203,7 +206,7 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S private JsonNode executeScript(TbMsg msg) throws ScriptException { try { String[] inArgs = prepareArgs(msg); - String eval = sandboxService.invokeFunction(this.scriptId, inArgs[0], inArgs[1], inArgs[2]).get().toString(); + String eval = sandboxService.invokeFunction(tenantId, this.scriptId, inArgs[0], inArgs[1], inArgs[2]).get().toString(); return mapper.readTree(eval); } catch (ExecutionException e) { if (e.getCause() instanceof ScriptException) { @@ -220,7 +223,7 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S private ListenableFuture executeScriptAsync(TbMsg msg) { String[] inArgs = prepareArgs(msg); - return Futures.transformAsync(sandboxService.invokeFunction(this.scriptId, inArgs[0], inArgs[1], inArgs[2]), + return Futures.transformAsync(sandboxService.invokeFunction(tenantId, this.scriptId, inArgs[0], inArgs[1], inArgs[2]), o -> { try { return Futures.immediateFuture(mapper.readTree(o.toString())); diff --git a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java index 9579561df6..8a40b456c6 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java +++ b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java @@ -25,31 +25,40 @@ import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.web.context.request.async.DeferredResult; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.ApiUsageStateId; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.controller.HttpValidationCallback; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.customer.CustomerService; +import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.AccessControlService; @@ -70,6 +79,7 @@ import java.util.function.BiConsumer; @Component public class AccessValidator { + public static final String ONLY_SYSTEM_ADMINISTRATOR_IS_ALLOWED_TO_PERFORM_THIS_OPERATION = "Only system administrator is allowed to perform this operation!"; public static final String CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION = "Customer user is not allowed to perform this operation!"; public static final String SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION = "System administrator is not allowed to perform this operation!"; public static final String DEVICE_WITH_REQUESTED_ID_NOT_FOUND = "Device with requested id wasn't found!"; @@ -87,6 +97,9 @@ public class AccessValidator { @Autowired protected DeviceService deviceService; + @Autowired + protected DeviceProfileService deviceProfileService; + @Autowired protected AssetService assetService; @@ -102,6 +115,9 @@ public class AccessValidator { @Autowired protected AccessControlService accessControlService; + @Autowired + protected ApiUsageStateService apiUsageStateService; + private ExecutorService executor; @PostConstruct @@ -143,7 +159,11 @@ public class AccessValidator { new FutureCallback>() { @Override public void onSuccess(@Nullable DeferredResult result) { - onSuccess.accept(response, currentUser.getTenantId(), entityId); + try { + onSuccess.accept(response, currentUser.getTenantId(), entityId); + } catch (Exception e) { + onFailure(e); + } } @Override @@ -160,6 +180,9 @@ public class AccessValidator { case DEVICE: validateDevice(currentUser, operation, entityId, callback); return; + case DEVICE_PROFILE: + validateDeviceProfile(currentUser, operation, entityId, callback); + return; case ASSET: validateAsset(currentUser, operation, entityId, callback); return; @@ -172,9 +195,18 @@ public class AccessValidator { case TENANT: validateTenant(currentUser, operation, entityId, callback); return; + case TENANT_PROFILE: + validateTenantProfile(currentUser, operation, entityId, callback); + return; + case USER: + validateUser(currentUser, operation, entityId, callback); + return; case ENTITY_VIEW: validateEntityView(currentUser, operation, entityId, callback); return; + case API_USAGE_STATE: + validateApiUsageState(currentUser, operation, entityId, callback); + return; default: //TODO: add support of other entities throw new IllegalStateException("Not Implemented!"); @@ -201,6 +233,45 @@ public class AccessValidator { } } + private void validateDeviceProfile(final SecurityUser currentUser, Operation operation, EntityId entityId, FutureCallback callback) { + if (currentUser.isSystemAdmin()) { + callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); + } else { + DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(currentUser.getTenantId(), new DeviceProfileId(entityId.getId())); + if (deviceProfile == null) { + callback.onSuccess(ValidationResult.entityNotFound("Device profile with requested id wasn't found!")); + } else { + try { + accessControlService.checkPermission(currentUser, Resource.DEVICE_PROFILE, operation, entityId, deviceProfile); + } catch (ThingsboardException e) { + callback.onSuccess(ValidationResult.accessDenied(e.getMessage())); + } + callback.onSuccess(ValidationResult.ok(deviceProfile)); + } + } + } + + private void validateApiUsageState(final SecurityUser currentUser, Operation operation, EntityId entityId, FutureCallback callback) { + if (currentUser.isSystemAdmin()) { + callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); + } else { + if (!operation.equals(Operation.READ_TELEMETRY)) { + callback.onSuccess(ValidationResult.accessDenied("Allowed only READ_TELEMETRY operation!")); + } + ApiUsageState apiUsageState = apiUsageStateService.findApiUsageStateById(currentUser.getTenantId(), new ApiUsageStateId(entityId.getId())); + if (apiUsageState == null) { + callback.onSuccess(ValidationResult.entityNotFound("Api Usage State with requested id wasn't found!")); + } else { + try { + accessControlService.checkPermission(currentUser, Resource.API_USAGE_STATE, operation, entityId, apiUsageState); + } catch (ThingsboardException e) { + callback.onSuccess(ValidationResult.accessDenied(e.getMessage())); + } + callback.onSuccess(ValidationResult.ok(apiUsageState)); + } + } + } + private void validateAsset(final SecurityUser currentUser, Operation operation, EntityId entityId, FutureCallback callback) { if (currentUser.isSystemAdmin()) { callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); @@ -308,6 +379,30 @@ public class AccessValidator { } } + private void validateTenantProfile(final SecurityUser currentUser, Operation operation, EntityId entityId, FutureCallback callback) { + if (currentUser.isSystemAdmin()) { + callback.onSuccess(ValidationResult.ok(null)); + } else { + callback.onSuccess(ValidationResult.accessDenied(ONLY_SYSTEM_ADMINISTRATOR_IS_ALLOWED_TO_PERFORM_THIS_OPERATION)); + } + } + + private void validateUser(final SecurityUser currentUser, Operation operation, EntityId entityId, FutureCallback callback) { + ListenableFuture userFuture = userService.findUserByIdAsync(currentUser.getTenantId(), new UserId(entityId.getId())); + Futures.addCallback(userFuture, getCallback(callback, user -> { + if (user == null) { + return ValidationResult.entityNotFound("User with requested id wasn't found!"); + } + try { + accessControlService.checkPermission(currentUser, Resource.USER, operation, entityId, user); + } catch (ThingsboardException e) { + return ValidationResult.accessDenied(e.getMessage()); + } + return ValidationResult.ok(user); + + }), executor); + } + private void validateEntityView(final SecurityUser currentUser, Operation operation, EntityId entityId, FutureCallback callback) { if (currentUser.isSystemAdmin()) { callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); @@ -344,9 +439,9 @@ public class AccessValidator { public static void handleError(Throwable e, final DeferredResult response, HttpStatus defaultErrorStatus) { ResponseEntity responseEntity; - if (e != null && e instanceof ToErrorResponseEntity) { + if (e instanceof ToErrorResponseEntity) { responseEntity = ((ToErrorResponseEntity) e).toErrorResponseEntity(); - } else if (e != null && e instanceof IllegalArgumentException) { + } else if (e instanceof IllegalArgumentException || e instanceof IncorrectParameterException) { responseEntity = new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); } else { responseEntity = new ResponseEntity<>(defaultErrorStatus); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java index c51af005cc..f76521ac48 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java @@ -17,7 +17,6 @@ package org.thingsboard.server.service.security.auth.oauth2; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.base.Strings; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -32,10 +31,10 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.IdBased; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; -import org.thingsboard.server.common.data.page.TimePageData; -import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.dao.customer.CustomerService; @@ -50,7 +49,6 @@ import org.thingsboard.server.service.security.model.UserPrincipal; import java.io.IOException; import java.util.List; import java.util.Optional; -import java.util.concurrent.ExecutionException; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -80,12 +78,15 @@ public abstract class AbstractOAuth2ClientMapper { private final Lock userCreationLock = new ReentrantLock(); - protected SecurityUser getOrCreateSecurityUserFromOAuth2User(OAuth2User oauth2User, boolean allowUserCreation, boolean activateUser) { + protected SecurityUser getOrCreateSecurityUserFromOAuth2User(OAuth2User oauth2User, OAuth2ClientRegistrationInfo clientRegistration) { + + OAuth2MapperConfig config = clientRegistration.getMapperConfig(); + UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, oauth2User.getEmail()); User user = userService.findUserByEmail(TenantId.SYS_TENANT_ID, oauth2User.getEmail()); - if (user == null && !allowUserCreation) { + if (user == null && !config.isAllowUserCreation()) { throw new UsernameNotFoundException("User not found: " + oauth2User.getEmail()); } @@ -110,21 +111,28 @@ public abstract class AbstractOAuth2ClientMapper { user.setFirstName(oauth2User.getFirstName()); user.setLastName(oauth2User.getLastName()); + ObjectNode additionalInfo = objectMapper.createObjectNode(); + if (!StringUtils.isEmpty(oauth2User.getDefaultDashboardName())) { Optional dashboardIdOpt = user.getAuthority() == Authority.TENANT_ADMIN ? getDashboardId(tenantId, oauth2User.getDefaultDashboardName()) : getDashboardId(tenantId, customerId, oauth2User.getDefaultDashboardName()); if (dashboardIdOpt.isPresent()) { - ObjectNode additionalInfo = objectMapper.createObjectNode(); additionalInfo.put("defaultDashboardFullscreen", oauth2User.isAlwaysFullScreen()); additionalInfo.put("defaultDashboardId", dashboardIdOpt.get().getId().toString()); - user.setAdditionalInfo(additionalInfo); } } + if (clientRegistration.getAdditionalInfo() != null && + clientRegistration.getAdditionalInfo().has("providerName")) { + additionalInfo.put("authProviderName", clientRegistration.getAdditionalInfo().get("providerName").asText()); + } + + user.setAdditionalInfo(additionalInfo); + user = userService.saveUser(user); - if (activateUser) { + if (config.isActivateUser()) { UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getTenantId(), user.getId()); userService.activateUserCredentials(user.getTenantId(), userCredentials.getActivateToken(), passwordEncoder.encode("")); } @@ -147,7 +155,7 @@ public abstract class AbstractOAuth2ClientMapper { } private TenantId getTenantId(String tenantName) throws IOException { - List tenants = tenantService.findTenants(new TextPageLink(1, tenantName)).getData(); + List tenants = tenantService.findTenants(new PageLink(1, 0, tenantName)).getData(); Tenant tenant; if (tenants == null || tenants.isEmpty()) { tenant = new Tenant(); @@ -176,23 +184,19 @@ public abstract class AbstractOAuth2ClientMapper { } private Optional getDashboardId(TenantId tenantId, String dashboardName) { - TextPageLink searchTextLink = new TextPageLink(1, dashboardName); - TextPageData dashboardsPage = dashboardService.findDashboardsByTenantId(tenantId, searchTextLink); + PageLink searchTextLink = new PageLink(1, 0, dashboardName); + PageData dashboardsPage = dashboardService.findDashboardsByTenantId(tenantId, searchTextLink); return dashboardsPage.getData().stream() .findAny() .map(IdBased::getId); } private Optional getDashboardId(TenantId tenantId, CustomerId customerId, String dashboardName) { - TimePageData dashboardsPage = null; + PageData dashboardsPage; + PageLink pageLink = null; do { - TimePageLink timePageLink = dashboardsPage != null ? - dashboardsPage.getNextPageLink() : new TimePageLink(DASHBOARDS_REQUEST_LIMIT); - try { - dashboardsPage = dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, timePageLink).get(); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException("Failed to get customer's dashboards.", e); - } + pageLink = pageLink == null ? new PageLink(DASHBOARDS_REQUEST_LIMIT) : pageLink.nextPageLink(); + dashboardsPage = dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink); Optional dashboardInfoOpt = dashboardsPage.getData().stream() .filter(dashboardInfo -> dashboardName.equals(dashboardInfo.getName())) .findAny(); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicMapperUtils.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicMapperUtils.java new file mode 100644 index 0000000000..246e74245d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicMapperUtils.java @@ -0,0 +1,78 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.security.auth.oauth2; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.text.StrSubstitutor; +import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; +import org.thingsboard.server.dao.oauth2.OAuth2User; + +import java.util.Map; + +@Slf4j +public class BasicMapperUtils { + private static final String START_PLACEHOLDER_PREFIX = "%{"; + private static final String END_PLACEHOLDER_PREFIX = "}"; + + public static OAuth2User getOAuth2User(String email, Map attributes, OAuth2MapperConfig config) { + OAuth2User oauth2User = new OAuth2User(); + oauth2User.setEmail(email); + oauth2User.setTenantName(getTenantName(email, attributes, config)); + if (!StringUtils.isEmpty(config.getBasic().getLastNameAttributeKey())) { + String lastName = getStringAttributeByKey(attributes, config.getBasic().getLastNameAttributeKey()); + oauth2User.setLastName(lastName); + } + if (!StringUtils.isEmpty(config.getBasic().getFirstNameAttributeKey())) { + String firstName = getStringAttributeByKey(attributes, config.getBasic().getFirstNameAttributeKey()); + oauth2User.setFirstName(firstName); + } + if (!StringUtils.isEmpty(config.getBasic().getCustomerNamePattern())) { + StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX); + String customerName = sub.replace(config.getBasic().getCustomerNamePattern()); + oauth2User.setCustomerName(customerName); + } + oauth2User.setAlwaysFullScreen(config.getBasic().isAlwaysFullScreen()); + if (!StringUtils.isEmpty(config.getBasic().getDefaultDashboardName())) { + oauth2User.setDefaultDashboardName(config.getBasic().getDefaultDashboardName()); + } + return oauth2User; + } + + public static String getTenantName(String email, Map attributes, OAuth2MapperConfig config) { + switch (config.getBasic().getTenantNameStrategy()) { + case EMAIL: + return email; + case DOMAIN: + return email.substring(email .indexOf("@") + 1); + case CUSTOM: + StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX); + return sub.replace(config.getBasic().getTenantNamePattern()); + default: + throw new RuntimeException("Tenant Name Strategy with type " + config.getBasic().getTenantNameStrategy() + " is not supported!"); + } + } + + public static String getStringAttributeByKey(Map attributes, String key) { + String result = null; + try { + result = (String) attributes.get(key); + } catch (Exception e) { + log.warn("Can't convert attribute to String by key " + key); + } + return result; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java index 2170479f92..bd6434f50a 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java @@ -16,11 +16,10 @@ package org.thingsboard.server.service.security.auth.oauth2; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.text.StrSubstitutor; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; -import org.thingsboard.server.dao.oauth2.OAuth2ClientMapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; import org.thingsboard.server.dao.oauth2.OAuth2User; import org.thingsboard.server.service.security.model.SecurityUser; @@ -30,62 +29,13 @@ import java.util.Map; @Slf4j public class BasicOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper { - private static final String START_PLACEHOLDER_PREFIX = "%{"; - private static final String END_PLACEHOLDER_PREFIX = "}"; - private static final String EMAIL_TENANT_STRATEGY = "email"; - private static final String DOMAIN_TENANT_STRATEGY = "domain"; - private static final String CUSTOM_TENANT_STRATEGY = "custom"; - @Override - public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig config) { - OAuth2User oauth2User = new OAuth2User(); + public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2ClientRegistrationInfo clientRegistration) { + OAuth2MapperConfig config = clientRegistration.getMapperConfig(); Map attributes = token.getPrincipal().getAttributes(); - String email = getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey()); - oauth2User.setEmail(email); - oauth2User.setTenantName(getTenantName(attributes, config)); - if (!StringUtils.isEmpty(config.getBasic().getLastNameAttributeKey())) { - String lastName = getStringAttributeByKey(attributes, config.getBasic().getLastNameAttributeKey()); - oauth2User.setLastName(lastName); - } - if (!StringUtils.isEmpty(config.getBasic().getFirstNameAttributeKey())) { - String firstName = getStringAttributeByKey(attributes, config.getBasic().getFirstNameAttributeKey()); - oauth2User.setFirstName(firstName); - } - if (!StringUtils.isEmpty(config.getBasic().getCustomerNamePattern())) { - StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX); - String customerName = sub.replace(config.getBasic().getCustomerNamePattern()); - oauth2User.setCustomerName(customerName); - } - oauth2User.setAlwaysFullScreen(config.getBasic().isAlwaysFullScreen()); - if (!StringUtils.isEmpty(config.getBasic().getDefaultDashboardName())) { - oauth2User.setDefaultDashboardName(config.getBasic().getDefaultDashboardName()); - } - - return getOrCreateSecurityUserFromOAuth2User(oauth2User, config.isAllowUserCreation(), config.isActivateUser()); - } - - private String getTenantName(Map attributes, OAuth2ClientMapperConfig config) { - switch (config.getBasic().getTenantNameStrategy()) { - case EMAIL_TENANT_STRATEGY: - return getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey()); - case DOMAIN_TENANT_STRATEGY: - String email = getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey()); - return email.substring(email .indexOf("@") + 1); - case CUSTOM_TENANT_STRATEGY: - StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX); - return sub.replace(config.getBasic().getTenantNamePattern()); - default: - throw new RuntimeException("Tenant Name Strategy with type " + config.getBasic().getTenantNameStrategy() + " is not supported!"); - } - } + String email = BasicMapperUtils.getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey()); + OAuth2User oauth2User = BasicMapperUtils.getOAuth2User(email, attributes, config); - private String getStringAttributeByKey(Map attributes, String key) { - String result = null; - try { - result = (String) attributes.get(key); - } catch (Exception e) { - log.warn("Can't convert attribute to String by key " + key); - } - return result; + return getOrCreateSecurityUserFromOAuth2User(oauth2User, clientRegistration); } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java index 42ba95b4ee..0cd292f76f 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java @@ -23,28 +23,36 @@ import org.springframework.security.oauth2.client.authentication.OAuth2Authentic import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.springframework.web.client.RestTemplate; -import org.thingsboard.server.dao.oauth2.OAuth2ClientMapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; import org.thingsboard.server.dao.oauth2.OAuth2User; import org.thingsboard.server.service.security.model.SecurityUser; @Service(value = "customOAuth2ClientMapper") @Slf4j public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper { + private static final String PROVIDER_ACCESS_TOKEN = "provider-access-token"; private static final ObjectMapper json = new ObjectMapper(); private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); @Override - public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig config) { - OAuth2User oauth2User = getOAuth2User(token, config.getCustom()); - return getOrCreateSecurityUserFromOAuth2User(oauth2User, config.isAllowUserCreation(), config.isActivateUser()); + public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2ClientRegistrationInfo clientRegistration) { + OAuth2MapperConfig config = clientRegistration.getMapperConfig(); + OAuth2User oauth2User = getOAuth2User(token, providerAccessToken, config.getCustom()); + return getOrCreateSecurityUserFromOAuth2User(oauth2User, clientRegistration); } - private synchronized OAuth2User getOAuth2User(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig.CustomOAuth2ClientMapperConfig custom) { + private synchronized OAuth2User getOAuth2User(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2CustomMapperConfig custom) { if (!StringUtils.isEmpty(custom.getUsername()) && !StringUtils.isEmpty(custom.getPassword())) { restTemplateBuilder = restTemplateBuilder.basicAuthentication(custom.getUsername(), custom.getPassword()); } + if (custom.isSendToken() && !StringUtils.isEmpty(providerAccessToken)) { + restTemplateBuilder = restTemplateBuilder.defaultHeader(PROVIDER_ACCESS_TOKEN, providerAccessToken); + } + RestTemplate restTemplate = restTemplateBuilder.build(); String request; try { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java new file mode 100644 index 0000000000..f259720626 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java @@ -0,0 +1,93 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.security.auth.oauth2; + +import lombok.Data; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; +import org.thingsboard.server.dao.oauth2.OAuth2Configuration; +import org.thingsboard.server.dao.oauth2.OAuth2User; +import org.thingsboard.server.service.security.model.SecurityUser; + +import java.util.ArrayList; +import java.util.Map; +import java.util.Optional; + +@Service(value = "githubOAuth2ClientMapper") +@Slf4j +public class GithubOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper { + private static final String EMAIL_URL_KEY = "emailUrl"; + + private static final String AUTHORIZATION = "Authorization"; + + private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); + + @Autowired + private OAuth2Configuration oAuth2Configuration; + + @Override + public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2ClientRegistrationInfo clientRegistration) { + OAuth2MapperConfig config = clientRegistration.getMapperConfig(); + Map githubMapperConfig = oAuth2Configuration.getGithubMapper(); + String email = getEmail(githubMapperConfig.get(EMAIL_URL_KEY), providerAccessToken); + Map attributes = token.getPrincipal().getAttributes(); + OAuth2User oAuth2User = BasicMapperUtils.getOAuth2User(email, attributes, config); + return getOrCreateSecurityUserFromOAuth2User(oAuth2User, clientRegistration); + } + + private synchronized String getEmail(String emailUrl, String oauth2Token) { + restTemplateBuilder = restTemplateBuilder.defaultHeader(AUTHORIZATION, "token " + oauth2Token); + + RestTemplate restTemplate = restTemplateBuilder.build(); + GithubEmailsResponse githubEmailsResponse; + try { + githubEmailsResponse = restTemplate.getForEntity(emailUrl, GithubEmailsResponse.class).getBody(); + if (githubEmailsResponse == null){ + throw new RuntimeException("Empty Github response!"); + } + } catch (Exception e) { + log.error("There was an error during connection to Github API", e); + throw new RuntimeException("Unable to login. Please contact your Administrator!"); + } + Optional emailOpt = githubEmailsResponse.stream() + .filter(GithubEmailResponse::isPrimary) + .map(GithubEmailResponse::getEmail) + .findAny(); + if (emailOpt.isPresent()){ + return emailOpt.get(); + } else { + log.error("Could not find primary email from {}.", githubEmailsResponse); + throw new RuntimeException("Unable to login. Please contact your Administrator!"); + } + } + private static class GithubEmailsResponse extends ArrayList {} + + @Data + @ToString + private static class GithubEmailResponse { + private String email; + private boolean verified; + private boolean primary; + private String visibility; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java index 196bfe7b50..4c9804930e 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java @@ -16,9 +16,9 @@ package org.thingsboard.server.service.security.auth.oauth2; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; -import org.thingsboard.server.dao.oauth2.OAuth2ClientMapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; import org.thingsboard.server.service.security.model.SecurityUser; public interface OAuth2ClientMapper { - SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig config); + SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2ClientRegistrationInfo clientRegistration); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java index e1c5b694bb..2e764fe223 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java @@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.oauth2.MapperType; @Component @Slf4j @@ -32,14 +33,20 @@ public class OAuth2ClientMapperProvider { @Qualifier("customOAuth2ClientMapper") private OAuth2ClientMapper customOAuth2ClientMapper; - public OAuth2ClientMapper getOAuth2ClientMapperByType(String oauth2ClientType) { - switch (oauth2ClientType) { - case "custom": + @Autowired + @Qualifier("githubOAuth2ClientMapper") + private OAuth2ClientMapper githubOAuth2ClientMapper; + + public OAuth2ClientMapper getOAuth2ClientMapperByType(MapperType oauth2MapperType) { + switch (oauth2MapperType) { + case CUSTOM: return customOAuth2ClientMapper; - case "basic": + case BASIC: return basicOAuth2ClientMapper; + case GITHUB: + return githubOAuth2ClientMapper; default: - throw new RuntimeException("OAuth2ClientMapper with type " + oauth2ClientType + " is not supported!"); + throw new RuntimeException("OAuth2ClientRegistrationMapper with type " + oauth2MapperType + " is not supported!"); } } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java index 6641ab0ac4..8a65eadedc 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java @@ -16,13 +16,14 @@ package org.thingsboard.server.service.security.auth.oauth2; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.stereotype.Component; -import org.thingsboard.server.dao.oauth2.OAuth2Client; -import org.thingsboard.server.dao.oauth2.OAuth2Configuration; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; +import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.token.JwtToken; @@ -34,25 +35,28 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.util.UUID; @Component(value = "oauth2AuthenticationSuccessHandler") -@ConditionalOnProperty(prefix = "security.oauth2", value = "enabled", havingValue = "true") public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { private final JwtTokenFactory tokenFactory; private final RefreshTokenRepository refreshTokenRepository; private final OAuth2ClientMapperProvider oauth2ClientMapperProvider; - private final OAuth2Configuration oauth2Configuration; + private final OAuth2Service oAuth2Service; + private final OAuth2AuthorizedClientService oAuth2AuthorizedClientService; @Autowired public Oauth2AuthenticationSuccessHandler(final JwtTokenFactory tokenFactory, final RefreshTokenRepository refreshTokenRepository, final OAuth2ClientMapperProvider oauth2ClientMapperProvider, - final OAuth2Configuration oauth2Configuration) { + final OAuth2Service oAuth2Service, + final OAuth2AuthorizedClientService oAuth2AuthorizedClientService) { this.tokenFactory = tokenFactory; this.refreshTokenRepository = refreshTokenRepository; this.oauth2ClientMapperProvider = oauth2ClientMapperProvider; - this.oauth2Configuration = oauth2Configuration; + this.oAuth2Service = oAuth2Service; + this.oAuth2AuthorizedClientService = oAuth2AuthorizedClientService; } @Override @@ -63,9 +67,13 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS try { OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) authentication; - OAuth2Client oauth2Client = oauth2Configuration.getClientByRegistrationId(token.getAuthorizedClientRegistrationId()); - OAuth2ClientMapper mapper = oauth2ClientMapperProvider.getOAuth2ClientMapperByType(oauth2Client.getMapperConfig().getType()); - SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(token, oauth2Client.getMapperConfig()); + OAuth2ClientRegistrationInfo clientRegistration = oAuth2Service.findClientRegistrationInfo(UUID.fromString(token.getAuthorizedClientRegistrationId())); + OAuth2AuthorizedClient oAuth2AuthorizedClient = oAuth2AuthorizedClientService.loadAuthorizedClient( + token.getAuthorizedClientRegistrationId(), + token.getPrincipal().getName()); + OAuth2ClientMapper mapper = oauth2ClientMapperProvider.getOAuth2ClientMapperByType(clientRegistration.getMapperConfig().getType()); + SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(), + clientRegistration); JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser); @@ -76,4 +84,4 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS URLEncoder.encode(e.getMessage(), StandardCharsets.UTF_8.toString())); } } -} \ No newline at end of file +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java index c9aed5bd7a..377929cf7d 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java @@ -105,7 +105,7 @@ public class CustomerUserPermissions extends AbstractPermissions { @Override public boolean hasPermission(SecurityUser user, Operation operation, UserId userId, User userEntity) { - if (userEntity.getAuthority() != Authority.CUSTOMER_USER) { + if (!Authority.CUSTOMER_USER.equals(userEntity.getAuthority())) { return false; } if (!user.getId().equals(userId)) { diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java index a66b822fca..9d16760464 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java @@ -31,7 +31,12 @@ public enum Resource { RULE_CHAIN(EntityType.RULE_CHAIN), USER(EntityType.USER), WIDGETS_BUNDLE(EntityType.WIDGETS_BUNDLE), - WIDGET_TYPE(EntityType.WIDGET_TYPE); + WIDGET_TYPE(EntityType.WIDGET_TYPE), + OAUTH2_CONFIGURATION_INFO(), + OAUTH2_CONFIGURATION_TEMPLATE(), + TENANT_PROFILE(EntityType.TENANT_PROFILE), + DEVICE_PROFILE(EntityType.DEVICE_PROFILE), + API_USAGE_STATE(EntityType.API_USAGE_STATE); private final EntityType entityType; diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java index d7b90f9311..abed9d5bc7 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java @@ -19,14 +19,10 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.service.security.model.SecurityUser; -import java.util.HashMap; -import java.util.Optional; - @Component(value="sysAdminPermissions") public class SysAdminPermissions extends AbstractPermissions { @@ -39,6 +35,9 @@ public class SysAdminPermissions extends AbstractPermissions { put(Resource.USER, userPermissionChecker); put(Resource.WIDGETS_BUNDLE, systemEntityPermissionChecker); put(Resource.WIDGET_TYPE, systemEntityPermissionChecker); + put(Resource.OAUTH2_CONFIGURATION_INFO, PermissionChecker.allowAllPermissionChecker); + put(Resource.OAUTH2_CONFIGURATION_TEMPLATE, PermissionChecker.allowAllPermissionChecker); + put(Resource.TENANT_PROFILE, PermissionChecker.allowAllPermissionChecker); } private static final PermissionChecker systemEntityPermissionChecker = new PermissionChecker() { @@ -57,7 +56,7 @@ public class SysAdminPermissions extends AbstractPermissions { @Override public boolean hasPermission(SecurityUser user, Operation operation, UserId userId, User userEntity) { - if (userEntity.getAuthority() == Authority.CUSTOMER_USER) { + if (Authority.CUSTOMER_USER.equals(userEntity.getAuthority())) { return false; } return true; diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java index 705c955ea1..e0c34200f4 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java @@ -19,13 +19,10 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.service.security.model.SecurityUser; -import java.util.HashMap; - @Component(value="tenantAdminPermissions") public class TenantAdminPermissions extends AbstractPermissions { @@ -42,6 +39,8 @@ public class TenantAdminPermissions extends AbstractPermissions { put(Resource.USER, userPermissionChecker); put(Resource.WIDGETS_BUNDLE, widgetsPermissionChecker); put(Resource.WIDGET_TYPE, widgetsPermissionChecker); + put(Resource.DEVICE_PROFILE, tenantEntityPermissionChecker); + put(Resource.API_USAGE_STATE, tenantEntityPermissionChecker); } public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() { @@ -76,7 +75,7 @@ public class TenantAdminPermissions extends AbstractPermissions { @Override public boolean hasPermission(SecurityUser user, Operation operation, UserId userId, User userEntity) { - if (userEntity.getAuthority() == Authority.SYS_ADMIN) { + if (Authority.SYS_ADMIN.equals(userEntity.getAuthority())) { return false; } if (!user.getTenantId().equals(userEntity.getTenantId())) { diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java index dcc8d590b7..5ef5adc386 100644 --- a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java @@ -40,8 +40,8 @@ import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -339,13 +339,13 @@ public class DefaultDeviceStateService implements DeviceStateService { //TODO 3.0: replace this dummy search with new functionality to search by partitions using SQL capabilities. // Adding only devices that are in new partitions - List tenants = tenantService.findTenants(new TextPageLink(Integer.MAX_VALUE)).getData(); + List tenants = tenantService.findTenants(new PageLink(Integer.MAX_VALUE)).getData(); for (Tenant tenant : tenants) { - TextPageLink pageLink = new TextPageLink(initFetchPackSize); + PageLink pageLink = new PageLink(initFetchPackSize); while (pageLink != null) { List> fetchFutures = new ArrayList<>(); - TextPageData page = deviceService.findDevicesByTenantId(tenant.getId(), pageLink); - pageLink = page.getNextPageLink(); + PageData page = deviceService.findDevicesByTenantId(tenant.getId(), pageLink); + pageLink = page.hasNext() ? pageLink.nextPageLink() : null; for (Device device : page.getData()) { TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenant.getId(), device.getId()); if (addedPartitions.contains(tpi)) { diff --git a/application/src/main/java/org/thingsboard/server/service/stats/DefaultRuleEngineStatisticsService.java b/application/src/main/java/org/thingsboard/server/service/stats/DefaultRuleEngineStatisticsService.java index b46033389b..a832eb0870 100644 --- a/application/src/main/java/org/thingsboard/server/service/stats/DefaultRuleEngineStatisticsService.java +++ b/application/src/main/java/org/thingsboard/server/service/stats/DefaultRuleEngineStatisticsService.java @@ -48,9 +48,9 @@ import java.util.stream.Collectors; public class DefaultRuleEngineStatisticsService implements RuleEngineStatisticsService { public static final String TB_SERVICE_QUEUE = "TbServiceQueue"; - public static final FutureCallback CALLBACK = new FutureCallback() { + public static final FutureCallback CALLBACK = new FutureCallback() { @Override - public void onSuccess(@Nullable Void result) { + public void onSuccess(@Nullable Integer result) { } @@ -85,7 +85,7 @@ public class DefaultRuleEngineStatisticsService implements RuleEngineStatisticsS .map(kv -> new BasicTsKvEntry(ts, new LongDataEntry(kv.getKey(), (long) kv.getValue().get()))) .collect(Collectors.toList()); if (!tsList.isEmpty()) { - tsService.saveAndNotify(tenantId, serviceAssetId, tsList, CALLBACK); + tsService.saveAndNotifyInternal(tenantId, serviceAssetId, tsList, CALLBACK); } } } catch (DataValidationException e) { @@ -97,7 +97,7 @@ public class DefaultRuleEngineStatisticsService implements RuleEngineStatisticsS ruleEngineStats.getTenantExceptions().forEach((tenantId, e) -> { TsKvEntry tsKv = new BasicTsKvEntry(ts, new JsonDataEntry("ruleEngineException", e.toJsonString())); try { - tsService.saveAndNotify(tenantId, getServiceAssetId(tenantId, queueName), Collections.singletonList(tsKv), CALLBACK); + tsService.saveAndNotifyInternal(tenantId, getServiceAssetId(tenantId, queueName), Collections.singletonList(tsKv), CALLBACK); } catch (DataValidationException e2) { if (!e2.getMessage().equalsIgnoreCase("Asset is referencing to non-existent tenant!")) { throw e2; diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java index f40133808b..5120698856 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java @@ -18,12 +18,12 @@ package org.thingsboard.server.service.subscription; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -32,12 +32,14 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; +import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.timeseries.TimeseriesService; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; import org.thingsboard.server.gen.transport.TransportProtos.*; import org.thingsboard.server.gen.transport.TransportProtos.LocalSubscriptionServiceMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.TbSubscriptionUpdateProto; @@ -52,7 +54,8 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.queue.TbClusterService; import org.thingsboard.server.service.state.DefaultDeviceStateService; import org.thingsboard.server.service.state.DeviceStateService; -import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate; +import org.thingsboard.server.service.telemetry.sub.AlarmSubscriptionUpdate; +import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; @@ -124,7 +127,7 @@ public class DefaultSubscriptionManagerService implements SubscriptionManagerSer @Override public void addSubscription(TbSubscription subscription, TbCallback callback) { - log.trace("[{}][{}][{}] Registering remote subscription for entity [{}]", + log.trace("[{}][{}][{}] Registering subscription for entity [{}]", subscription.getServiceId(), subscription.getSessionId(), subscription.getSubscriptionId(), subscription.getEntityId()); TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, subscription.getTenantId(), subscription.getEntityId()); if (currentPartitions.contains(tpi)) { @@ -146,6 +149,9 @@ public class DefaultSubscriptionManagerService implements SubscriptionManagerSer case ATTRIBUTES: handleNewAttributeSubscription((TbAttributeSubscription) subscription); break; + case ALARMS: + handleNewAlarmsSubscription((TbAlarmsSubscription) subscription); + break; } } } @@ -184,7 +190,11 @@ public class DefaultSubscriptionManagerService implements SubscriptionManagerSer removedPartitions.forEach(partition -> { Set subs = partitionedSubscriptions.remove(partition); if (subs != null) { - subs.forEach(this::removeSubscriptionFromEntityMap); + subs.forEach(sub -> { + if (!serviceId.equals(sub.getServiceId())) { + removeSubscriptionFromEntityMap(sub); + } + }); } }); } @@ -192,7 +202,7 @@ public class DefaultSubscriptionManagerService implements SubscriptionManagerSer @Override public void onTimeSeriesUpdate(TenantId tenantId, EntityId entityId, List ts, TbCallback callback) { - onLocalSubUpdate(entityId, + onLocalTelemetrySubUpdate(entityId, s -> { if (TbSubscriptionType.TIMESERIES.equals(s.getType())) { return (TbTimeseriesSubscription) s; @@ -216,12 +226,12 @@ public class DefaultSubscriptionManagerService implements SubscriptionManagerSer @Override public void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List attributes, TbCallback callback) { - onAttributesUpdate(tenantId, entityId, scope, attributes, callback, true); + onAttributesUpdate(tenantId, entityId, scope, attributes, true, callback); } @Override - public void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List attributes, TbCallback callback, boolean notifyDevice) { - onLocalSubUpdate(entityId, + public void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List attributes, boolean notifyDevice, TbCallback callback) { + onLocalTelemetrySubUpdate(entityId, s -> { if (TbSubscriptionType.ATTRIBUTES.equals(s.getType())) { return (TbAttributeSubscription) s; @@ -258,17 +268,77 @@ public class DefaultSubscriptionManagerService implements SubscriptionManagerSer callback.onSuccess(); } - private void onLocalSubUpdate(EntityId entityId, - Function castFunction, - Predicate filterFunction, - Function> processFunction) { + @Override + public void onAlarmUpdate(TenantId tenantId, EntityId entityId, Alarm alarm, TbCallback callback) { + onLocalAlarmSubUpdate(entityId, + s -> { + if (TbSubscriptionType.ALARMS.equals(s.getType())) { + return (TbAlarmsSubscription) s; + } else { + return null; + } + }, + s -> alarm.getCreatedTime() >= s.getTs(), + s -> alarm, + false + ); + callback.onSuccess(); + } + + @Override + public void onAlarmDeleted(TenantId tenantId, EntityId entityId, Alarm alarm, TbCallback callback) { + onLocalAlarmSubUpdate(entityId, + s -> { + if (TbSubscriptionType.ALARMS.equals(s.getType())) { + return (TbAlarmsSubscription) s; + } else { + return null; + } + }, + s -> alarm.getCreatedTime() >= s.getTs(), + s -> alarm, + true + ); + callback.onSuccess(); + } + + @Override + public void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List keys, TbCallback callback) { + onLocalTelemetrySubUpdate(entityId, + s -> { + if (TbSubscriptionType.ATTRIBUTES.equals(s.getType())) { + return (TbAttributeSubscription) s; + } else { + return null; + } + }, + s -> (TbAttributeSubscriptionScope.ANY_SCOPE.equals(s.getScope()) || scope.equals(s.getScope().name())), + s -> { + List subscriptionUpdate = null; + for (String key : keys) { + if (s.isAllKeys() || s.getKeyStates().containsKey(key)) { + if (subscriptionUpdate == null) { + subscriptionUpdate = new ArrayList<>(); + } + subscriptionUpdate.add(new BasicTsKvEntry(0, new StringDataEntry(key, null))); + } + } + return subscriptionUpdate; + }); + callback.onSuccess(); + } + + private void onLocalTelemetrySubUpdate(EntityId entityId, + Function castFunction, + Predicate filterFunction, + Function> processFunction) { Set entitySubscriptions = subscriptionsByEntityId.get(entityId); if (entitySubscriptions != null) { entitySubscriptions.stream().map(castFunction).filter(Objects::nonNull).filter(filterFunction).forEach(s -> { List subscriptionUpdate = processFunction.apply(s); if (subscriptionUpdate != null && !subscriptionUpdate.isEmpty()) { if (serviceId.equals(s.getServiceId())) { - SubscriptionUpdate update = new SubscriptionUpdate(s.getSubscriptionId(), subscriptionUpdate); + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(s.getSubscriptionId(), subscriptionUpdate); localSubscriptionService.onSubscriptionUpdate(s.getSessionId(), update, TbCallback.EMPTY); } else { TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_CORE, s.getServiceId()); @@ -281,6 +351,29 @@ public class DefaultSubscriptionManagerService implements SubscriptionManagerSer } } + private void onLocalAlarmSubUpdate(EntityId entityId, + Function castFunction, + Predicate filterFunction, + Function processFunction, boolean deleted) { + Set entitySubscriptions = subscriptionsByEntityId.get(entityId); + if (entitySubscriptions != null) { + entitySubscriptions.stream().map(castFunction).filter(Objects::nonNull).filter(filterFunction).forEach(s -> { + Alarm alarm = processFunction.apply(s); + if (alarm != null) { + if (serviceId.equals(s.getServiceId())) { + AlarmSubscriptionUpdate update = new AlarmSubscriptionUpdate(s.getSubscriptionId(), alarm, deleted); + localSubscriptionService.onSubscriptionUpdate(s.getSessionId(), update, TbCallback.EMPTY); + } else { + TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_CORE, s.getServiceId()); + toCoreNotificationsProducer.send(tpi, toProto(s, alarm, deleted), null); + } + } + }); + } else { + log.debug("[{}] No device subscriptions to process!", entityId); + } + } + private boolean isInTimeRange(TbTimeseriesSubscription subscription, long kvTime) { return (subscription.getStartTime() == 0 || subscription.getStartTime() <= kvTime) && (subscription.getEndTime() == 0 || subscription.getEndTime() >= kvTime); @@ -324,6 +417,12 @@ public class DefaultSubscriptionManagerService implements SubscriptionManagerSer e -> log.error("Failed to fetch missed updates.", e), tsCallBackExecutor); } + private void handleNewAlarmsSubscription(TbAlarmsSubscription subscription) { + log.trace("[{}][{}][{}] Processing remote alarm subscription for entity [{}]", + serviceId, subscription.getSessionId(), subscription.getSubscriptionId(), subscription.getEntityId()); + //TODO: @dlandiak search all new alarms for this entity. + } + private void handleNewTelemetrySubscription(TbTimeseriesSubscription subscription) { log.trace("[{}][{}][{}] Processing remote telemetry subscription for entity [{}]", serviceId, subscription.getSessionId(), subscription.getSubscriptionId(), subscription.getEntityId()); @@ -382,4 +481,19 @@ public class DefaultSubscriptionManagerService implements SubscriptionManagerSer return new TbProtoQueueMsg<>(subscription.getEntityId().getId(), toCoreMsg); } + private TbProtoQueueMsg toProto(TbSubscription subscription, Alarm alarm, boolean deleted) { + TbAlarmSubscriptionUpdateProto.Builder builder = TbAlarmSubscriptionUpdateProto.newBuilder(); + + builder.setSessionId(subscription.getSessionId()); + builder.setSubscriptionId(subscription.getSubscriptionId()); + builder.setAlarm(JacksonUtil.toString(alarm)); + builder.setDeleted(deleted); + + ToCoreNotificationMsg toCoreMsg = ToCoreNotificationMsg.newBuilder().setToLocalSubscriptionServiceMsg( + LocalSubscriptionServiceMsgProto.newBuilder() + .setAlarmSubUpdate(builder.build()).build()) + .build(); + return new TbProtoQueueMsg<>(subscription.getEntityId().getId(), toCoreMsg); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java new file mode 100644 index 0000000000..a968eb47d8 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java @@ -0,0 +1,484 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.subscription; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; +import org.thingsboard.server.common.data.kv.ReadTsKvQuery; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AlarmDataQuery; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.TsValue; +import org.thingsboard.server.dao.alarm.AlarmService; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.entity.EntityService; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.timeseries.TimeseriesService; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.executors.DbCallbackExecutorService; +import org.thingsboard.server.service.telemetry.TelemetryWebSocketService; +import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; +import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataUpdate; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityHistoryCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.GetTsCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.LatestValueCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.TimeSeriesCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.UnsubscribeCmd; +import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +@Slf4j +@TbCoreComponent +@Service +public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubscriptionService { + + private static final int DEFAULT_LIMIT = 100; + private final Map> subscriptionsBySessionId = new ConcurrentHashMap<>(); + + @Autowired + private TelemetryWebSocketService wsService; + + @Autowired + private EntityService entityService; + + @Autowired + private AlarmService alarmService; + + @Autowired + private AttributesService attributesService; + + @Autowired + @Lazy + private TbLocalSubscriptionService localSubscriptionService; + + @Autowired + private TimeseriesService tsService; + + @Autowired + private TbServiceInfoProvider serviceInfoProvider; + + @Autowired + @Getter + private DbCallbackExecutorService dbCallbackExecutor; + + private ScheduledExecutorService scheduler; + + @Value("${database.ts.type}") + private String databaseTsType; + @Value("${server.ws.dynamic_page_link.refresh_interval:6}") + private long dynamicPageLinkRefreshInterval; + @Value("${server.ws.dynamic_page_link.refresh_pool_size:1}") + private int dynamicPageLinkRefreshPoolSize; + @Value("${server.ws.max_entities_per_data_subscription:1000}") + private int maxEntitiesPerDataSubscription; + @Value("${server.ws.max_entities_per_alarm_subscription:1000}") + private int maxEntitiesPerAlarmSubscription; + + private ExecutorService wsCallBackExecutor; + private boolean tsInSqlDB; + private String serviceId; + private SubscriptionServiceStatistics stats = new SubscriptionServiceStatistics(); + + @PostConstruct + public void initExecutor() { + serviceId = serviceInfoProvider.getServiceId(); + wsCallBackExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("ws-entity-sub-callback")); + tsInSqlDB = databaseTsType.equalsIgnoreCase("sql") || databaseTsType.equalsIgnoreCase("timescale"); + ThreadFactory tbThreadFactory = ThingsBoardThreadFactory.forName("ws-entity-sub-scheduler"); + if (dynamicPageLinkRefreshPoolSize == 1) { + scheduler = Executors.newSingleThreadScheduledExecutor(tbThreadFactory); + } else { + scheduler = Executors.newScheduledThreadPool(dynamicPageLinkRefreshPoolSize, tbThreadFactory); + } + } + + @PreDestroy + public void shutdownExecutor() { + if (wsCallBackExecutor != null) { + wsCallBackExecutor.shutdownNow(); + } + if (scheduler != null) { + scheduler.shutdownNow(); + } + } + + @Override + public void handleCmd(TelemetryWebSocketSessionRef session, EntityDataCmd cmd) { + TbEntityDataSubCtx ctx = getSubCtx(session.getSessionId(), cmd.getCmdId()); + if (ctx != null) { + log.debug("[{}][{}] Updating existing subscriptions using: {}", session.getSessionId(), cmd.getCmdId(), cmd); + if (cmd.getLatestCmd() != null || cmd.getTsCmd() != null || cmd.getHistoryCmd() != null) { + ctx.clearEntitySubscriptions(); + } + } else { + log.debug("[{}][{}] Creating new subscription using: {}", session.getSessionId(), cmd.getCmdId(), cmd); + ctx = createSubCtx(session, cmd); + } + ctx.setCurrentCmd(cmd); + if (cmd.getQuery() != null) { + if (ctx.getQuery() == null) { + log.debug("[{}][{}] Initializing data using query: {}", session.getSessionId(), cmd.getCmdId(), cmd.getQuery()); + } else { + log.debug("[{}][{}] Updating data using query: {}", session.getSessionId(), cmd.getCmdId(), cmd.getQuery()); + } + ctx.setAndResolveQuery(cmd.getQuery()); + TenantId tenantId = ctx.getTenantId(); + CustomerId customerId = ctx.getCustomerId(); + EntityDataQuery query = ctx.getQuery(); + //Step 1. Update existing query with the contents of LatestValueCmd + if (cmd.getLatestCmd() != null) { + cmd.getLatestCmd().getKeys().forEach(key -> { + if (!query.getLatestValues().contains(key)) { + query.getLatestValues().add(key); + } + }); + } + long start = System.currentTimeMillis(); + ctx.fetchData(); + long end = System.currentTimeMillis(); + stats.getRegularQueryInvocationCnt().incrementAndGet(); + stats.getRegularQueryTimeSpent().addAndGet(end - start); + ctx.cancelTasks(); + if (ctx.getQuery().getPageLink().isDynamic()) { + //TODO: validate number of dynamic page links against rate limits. Ignore dynamic flag if limit is reached. + TbEntityDataSubCtx finalCtx = ctx; + ScheduledFuture task = scheduler.scheduleWithFixedDelay( + () -> refreshDynamicQuery(tenantId, customerId, finalCtx), + dynamicPageLinkRefreshInterval, dynamicPageLinkRefreshInterval, TimeUnit.SECONDS); + finalCtx.setRefreshTask(task); + } + } + ListenableFuture historyFuture; + if (cmd.getHistoryCmd() != null) { + log.trace("[{}][{}] Going to process history command: {}", session.getSessionId(), cmd.getCmdId(), cmd.getHistoryCmd()); + historyFuture = handleHistoryCmd(ctx, cmd.getHistoryCmd()); + } else { + historyFuture = Futures.immediateFuture(ctx); + } + Futures.addCallback(historyFuture, new FutureCallback() { + @Override + public void onSuccess(@Nullable TbEntityDataSubCtx theCtx) { + if (cmd.getLatestCmd() != null) { + handleLatestCmd(theCtx, cmd.getLatestCmd()); + } else if (cmd.getTsCmd() != null) { + handleTimeSeriesCmd(theCtx, cmd.getTsCmd()); + } else if (!theCtx.isInitialDataSent()) { + EntityDataUpdate update = new EntityDataUpdate(theCtx.getCmdId(), theCtx.getData(), null, theCtx.getMaxEntitiesPerDataSubscription()); + wsService.sendWsMsg(theCtx.getSessionId(), update); + theCtx.setInitialDataSent(true); + } + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}][{}] Failed to process command", session.getSessionId(), cmd.getCmdId()); + } + }, wsCallBackExecutor); + } + + @Override + public void handleCmd(TelemetryWebSocketSessionRef session, AlarmDataCmd cmd) { + TbAlarmDataSubCtx ctx = getSubCtx(session.getSessionId(), cmd.getCmdId()); + if (ctx == null) { + log.debug("[{}][{}] Creating new alarm subscription using: {}", session.getSessionId(), cmd.getCmdId(), cmd); + ctx = createSubCtx(session, cmd); + } + ctx.setAndResolveQuery(cmd.getQuery()); + AlarmDataQuery adq = ctx.getQuery(); + long start = System.currentTimeMillis(); + ctx.fetchData(); + long end = System.currentTimeMillis(); + stats.getRegularQueryInvocationCnt().incrementAndGet(); + stats.getRegularQueryTimeSpent().addAndGet(end - start); + List entities = ctx.getEntitiesData(); + ctx.cancelTasks(); + ctx.clearEntitySubscriptions(); + if (entities.isEmpty()) { + AlarmDataUpdate update = new AlarmDataUpdate(cmd.getCmdId(), new PageData<>(), null, 0, 0); + wsService.sendWsMsg(ctx.getSessionId(), update); + } else { + ctx.fetchAlarms(); + ctx.createSubscriptions(cmd.getQuery().getLatestValues(), true); + if (adq.getPageLink().getTimeWindow() > 0) { + TbAlarmDataSubCtx finalCtx = ctx; + ScheduledFuture task = scheduler.scheduleWithFixedDelay( + finalCtx::cleanupOldAlarms, dynamicPageLinkRefreshInterval, dynamicPageLinkRefreshInterval, TimeUnit.SECONDS); + finalCtx.setRefreshTask(task); + } + } + } + + private void refreshDynamicQuery(TenantId tenantId, CustomerId customerId, TbEntityDataSubCtx finalCtx) { + try { + long start = System.currentTimeMillis(); + finalCtx.update(); + long end = System.currentTimeMillis(); + stats.getDynamicQueryInvocationCnt().incrementAndGet(); + stats.getDynamicQueryTimeSpent().addAndGet(end - start); + } catch (Exception e) { + log.warn("[{}][{}] Failed to refresh query", finalCtx.getSessionId(), finalCtx.getCmdId(), e); + } + } + + @Scheduled(fixedDelayString = "${server.ws.dynamic_page_link.stats:10000}") + public void printStats() { + int alarmQueryInvocationCntValue = stats.getAlarmQueryInvocationCnt().getAndSet(0); + long alarmQueryInvocationTimeValue = stats.getAlarmQueryTimeSpent().getAndSet(0); + int regularQueryInvocationCntValue = stats.getRegularQueryInvocationCnt().getAndSet(0); + long regularQueryInvocationTimeValue = stats.getRegularQueryTimeSpent().getAndSet(0); + int dynamicQueryInvocationCntValue = stats.getDynamicQueryInvocationCnt().getAndSet(0); + long dynamicQueryInvocationTimeValue = stats.getDynamicQueryTimeSpent().getAndSet(0); + long dynamicQueryCnt = subscriptionsBySessionId.values().stream().map(Map::values).count(); + if (regularQueryInvocationCntValue > 0 || dynamicQueryInvocationCntValue > 0 || dynamicQueryCnt > 0 || alarmQueryInvocationCntValue > 0) { + log.info("Stats: regularQueryInvocationCnt = [{}], regularQueryInvocationTime = [{}], " + + "dynamicQueryCnt = [{}] dynamicQueryInvocationCnt = [{}], dynamicQueryInvocationTime = [{}], " + + "alarmQueryInvocationCnt = [{}], alarmQueryInvocationTime = [{}]", + regularQueryInvocationCntValue, regularQueryInvocationTimeValue, + dynamicQueryCnt, dynamicQueryInvocationCntValue, dynamicQueryInvocationTimeValue, + alarmQueryInvocationCntValue, alarmQueryInvocationTimeValue); + } + } + + private TbEntityDataSubCtx createSubCtx(TelemetryWebSocketSessionRef sessionRef, EntityDataCmd cmd) { + Map sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>()); + TbEntityDataSubCtx ctx = new TbEntityDataSubCtx(serviceId, wsService, entityService, localSubscriptionService, + attributesService, stats, sessionRef, cmd.getCmdId(), maxEntitiesPerDataSubscription); + ctx.setAndResolveQuery(cmd.getQuery()); + sessionSubs.put(cmd.getCmdId(), ctx); + return ctx; + } + + private TbAlarmDataSubCtx createSubCtx(TelemetryWebSocketSessionRef sessionRef, AlarmDataCmd cmd) { + Map sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>()); + TbAlarmDataSubCtx ctx = new TbAlarmDataSubCtx(serviceId, wsService, entityService, localSubscriptionService, + attributesService, stats, alarmService, sessionRef, cmd.getCmdId(), maxEntitiesPerAlarmSubscription); + ctx.setAndResolveQuery(cmd.getQuery()); + sessionSubs.put(cmd.getCmdId(), ctx); + return ctx; + } + + private T getSubCtx(String sessionId, int cmdId) { + Map sessionSubs = subscriptionsBySessionId.get(sessionId); + if (sessionSubs != null) { + return (T) sessionSubs.get(cmdId); + } else { + return null; + } + } + + private ListenableFuture handleTimeSeriesCmd(TbEntityDataSubCtx ctx, TimeSeriesCmd cmd) { + log.debug("[{}][{}] Fetching time-series data for last {} ms for keys: ({})", ctx.getSessionId(), ctx.getCmdId(), cmd.getTimeWindow(), cmd.getKeys()); + return handleGetTsCmd(ctx, cmd, true); + } + + + private ListenableFuture handleHistoryCmd(TbEntityDataSubCtx ctx, EntityHistoryCmd cmd) { + log.debug("[{}][{}] Fetching history data for start {} and end {} ms for keys: ({})", ctx.getSessionId(), ctx.getCmdId(), cmd.getStartTs(), cmd.getEndTs(), cmd.getKeys()); + return handleGetTsCmd(ctx, cmd, false); + } + + private ListenableFuture handleGetTsCmd(TbEntityDataSubCtx ctx, GetTsCmd cmd, boolean subscribe) { + List keys = cmd.getKeys(); + List finalTsKvQueryList; + List tsKvQueryList = cmd.getKeys().stream().map(key -> new BaseReadTsKvQuery( + key, cmd.getStartTs(), cmd.getEndTs(), cmd.getInterval(), getLimit(cmd.getLimit()), cmd.getAgg() + )).collect(Collectors.toList()); + if (cmd.isFetchLatestPreviousPoint()) { + finalTsKvQueryList = new ArrayList<>(tsKvQueryList); + finalTsKvQueryList.addAll(cmd.getKeys().stream().map(key -> new BaseReadTsKvQuery( + key, cmd.getStartTs() - TimeUnit.DAYS.toMillis(365), cmd.getStartTs(), cmd.getInterval(), 1, cmd.getAgg() + )).collect(Collectors.toList())); + } else { + finalTsKvQueryList = tsKvQueryList; + } + Map>> fetchResultMap = new HashMap<>(); + ctx.getData().getData().forEach(entityData -> fetchResultMap.put(entityData, + tsService.findAll(ctx.getTenantId(), entityData.getEntityId(), finalTsKvQueryList))); + return Futures.transform(Futures.allAsList(fetchResultMap.values()), f -> { + fetchResultMap.forEach((entityData, future) -> { + Map> keyData = new LinkedHashMap<>(); + cmd.getKeys().forEach(key -> keyData.put(key, new ArrayList<>())); + try { + List entityTsData = future.get(); + if (entityTsData != null) { + entityTsData.forEach(entry -> keyData.get(entry.getKey()).add(new TsValue(entry.getTs(), entry.getValueAsString()))); + } + keyData.forEach((k, v) -> entityData.getTimeseries().put(k, v.toArray(new TsValue[v.size()]))); + if (cmd.isFetchLatestPreviousPoint()) { + entityData.getTimeseries().values().forEach(dataArray -> { + Arrays.sort(dataArray, (o1, o2) -> Long.compare(o2.getTs(), o1.getTs())); + }); + } + } catch (InterruptedException | ExecutionException e) { + log.warn("[{}][{}][{}] Failed to fetch historical data", ctx.getSessionId(), ctx.getCmdId(), entityData.getEntityId(), e); + wsService.sendWsMsg(ctx.getSessionId(), + new EntityDataUpdate(ctx.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR.getCode(), "Failed to fetch historical data!")); + } + }); + EntityDataUpdate update; + if (!ctx.isInitialDataSent()) { + update = new EntityDataUpdate(ctx.getCmdId(), ctx.getData(), null, ctx.getMaxEntitiesPerDataSubscription()); + ctx.setInitialDataSent(true); + } else { + update = new EntityDataUpdate(ctx.getCmdId(), null, ctx.getData().getData(), ctx.getMaxEntitiesPerDataSubscription()); + } + wsService.sendWsMsg(ctx.getSessionId(), update); + if (subscribe) { + ctx.createSubscriptions(keys.stream().map(key -> new EntityKey(EntityKeyType.TIME_SERIES, key)).collect(Collectors.toList()), false); + } + ctx.getData().getData().forEach(ed -> ed.getTimeseries().clear()); + return ctx; + }, wsCallBackExecutor); + } + + private void handleLatestCmd(TbEntityDataSubCtx ctx, LatestValueCmd latestCmd) { + log.trace("[{}][{}] Going to process latest command: {}", ctx.getSessionId(), ctx.getCmdId(), latestCmd); + //Fetch the latest values for telemetry keys (in case they are not copied from NoSQL to SQL DB in hybrid mode. + if (!tsInSqlDB) { + log.trace("[{}][{}] Going to fetch missing latest values: {}", ctx.getSessionId(), ctx.getCmdId(), latestCmd); + List allTsKeys = latestCmd.getKeys().stream() + .filter(key -> key.getType().equals(EntityKeyType.TIME_SERIES)) + .map(EntityKey::getKey).collect(Collectors.toList()); + + Map>> missingTelemetryFutures = new HashMap<>(); + for (EntityData entityData : ctx.getData().getData()) { + Map> latestEntityData = entityData.getLatest(); + Map tsEntityData = latestEntityData.get(EntityKeyType.TIME_SERIES); + Set missingTsKeys = new LinkedHashSet<>(allTsKeys); + if (tsEntityData != null) { + missingTsKeys.removeAll(tsEntityData.keySet()); + } else { + tsEntityData = new HashMap<>(); + latestEntityData.put(EntityKeyType.TIME_SERIES, tsEntityData); + } + + ListenableFuture> missingTsData = tsService.findLatest(ctx.getTenantId(), entityData.getEntityId(), missingTsKeys); + missingTelemetryFutures.put(entityData, Futures.transform(missingTsData, this::toTsValue, MoreExecutors.directExecutor())); + } + Futures.addCallback(Futures.allAsList(missingTelemetryFutures.values()), new FutureCallback>>() { + @Override + public void onSuccess(@Nullable List> result) { + missingTelemetryFutures.forEach((key, value) -> { + try { + key.getLatest().get(EntityKeyType.TIME_SERIES).putAll(value.get()); + } catch (InterruptedException | ExecutionException e) { + log.warn("[{}][{}] Failed to lookup latest telemetry: {}:{}", ctx.getSessionId(), ctx.getCmdId(), key.getEntityId(), allTsKeys, e); + } + }); + EntityDataUpdate update; + if (!ctx.isInitialDataSent()) { + update = new EntityDataUpdate(ctx.getCmdId(), ctx.getData(), null, ctx.getMaxEntitiesPerDataSubscription()); + ctx.setInitialDataSent(true); + } else { + update = new EntityDataUpdate(ctx.getCmdId(), null, ctx.getData().getData(), ctx.getMaxEntitiesPerDataSubscription()); + } + wsService.sendWsMsg(ctx.getSessionId(), update); + ctx.createSubscriptions(latestCmd.getKeys(), true); + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}][{}] Failed to process websocket command: {}:{}", ctx.getSessionId(), ctx.getCmdId(), ctx.getQuery(), latestCmd, t); + wsService.sendWsMsg(ctx.getSessionId(), + new EntityDataUpdate(ctx.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR.getCode(), "Failed to process websocket command!")); + } + }, wsCallBackExecutor); + } else { + if (!ctx.isInitialDataSent()) { + EntityDataUpdate update = new EntityDataUpdate(ctx.getCmdId(), ctx.getData(), null, ctx.getMaxEntitiesPerDataSubscription()); + wsService.sendWsMsg(ctx.getSessionId(), update); + ctx.setInitialDataSent(true); + } + ctx.createSubscriptions(latestCmd.getKeys(), true); + } + } + + private Map toTsValue(List data) { + return data.stream().collect(Collectors.toMap(TsKvEntry::getKey, value -> new TsValue(value.getTs(), value.getValueAsString()))); + } + + @Override + public void cancelSubscription(String sessionId, UnsubscribeCmd cmd) { + cleanupAndCancel(getSubCtx(sessionId, cmd.getCmdId())); + } + + private void cleanupAndCancel(TbAbstractDataSubCtx ctx) { + if (ctx != null) { + ctx.cancelTasks(); + ctx.clearEntitySubscriptions(); + ctx.clearDynamicValueSubscriptions(); + } + } + + @Override + public void cancelAllSessionSubscriptions(String sessionId) { + Map sessionSubs = subscriptionsBySessionId.remove(sessionId); + if (sessionSubs != null) { + sessionSubs.values().stream().filter(sub -> sub instanceof TbEntityDataSubCtx).map(sub -> (TbEntityDataSubCtx) sub).forEach(this::cleanupAndCancel); + } + } + + private int getLimit(int limit) { + return limit == 0 ? DEFAULT_LIMIT : limit; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java index d57067fa28..89233d2f4f 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java @@ -36,8 +36,9 @@ import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.queue.TbClusterService; -import org.thingsboard.server.service.telemetry.TelemetryWebSocketService; -import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate; +import org.thingsboard.server.service.telemetry.DefaultTelemetryWebSocketService; +import org.thingsboard.server.service.telemetry.sub.AlarmSubscriptionUpdate; +import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; @@ -58,9 +59,6 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer private final Set currentPartitions = ConcurrentHashMap.newKeySet(); private final Map> subscriptionsBySessionId = new ConcurrentHashMap<>(); - @Autowired - private TelemetryWebSocketService wsService; - @Autowired private EntityViewService entityViewService; @@ -117,11 +115,6 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer //TODO 3.1: replace null callbacks with callbacks from websocket service. @Override public void addSubscription(TbSubscription subscription) { - EntityId entityId = subscription.getEntityId(); - // Telemetry subscription on Entity Views are handled differently, because we need to allow only certain keys and time ranges; - if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW) && TbSubscriptionType.TIMESERIES.equals(subscription.getType())) { - subscription = resolveEntityViewSubscription((TbTimeseriesSubscription) subscription); - } pushSubscriptionToManagerService(subscription, true); registerSubscription(subscription); } @@ -141,7 +134,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer } @Override - public void onSubscriptionUpdate(String sessionId, SubscriptionUpdate update, TbCallback callback) { + public void onSubscriptionUpdate(String sessionId, TelemetrySubscriptionUpdate update, TbCallback callback) { TbSubscription subscription = subscriptionsBySessionId .getOrDefault(sessionId, Collections.emptyMap()).get(update.getSubscriptionId()); if (subscription != null) { @@ -155,7 +148,17 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer update.getLatestValues().forEach((key, value) -> attrSub.getKeyStates().put(key, value)); break; } - wsService.sendWsMsg(sessionId, update); + subscription.getUpdateConsumer().accept(sessionId, update); + } + callback.onSuccess(); + } + + @Override + public void onSubscriptionUpdate(String sessionId, AlarmSubscriptionUpdate update, TbCallback callback) { + TbSubscription subscription = subscriptionsBySessionId + .getOrDefault(sessionId, Collections.emptyMap()).get(update.getSubscriptionId()); + if (subscription != null && subscription.getType() == TbSubscriptionType.ALARMS) { + subscription.getUpdateConsumer().accept(sessionId, update); } callback.onSuccess(); } @@ -196,30 +199,6 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer } } - private TbSubscription resolveEntityViewSubscription(TbTimeseriesSubscription subscription) { - EntityView entityView = entityViewService.findEntityViewById(TenantId.SYS_TENANT_ID, new EntityViewId(subscription.getEntityId().getId())); - - Map keyStates; - if (subscription.isAllKeys()) { - keyStates = entityView.getKeys().getTimeseries().stream().collect(Collectors.toMap(k -> k, k -> 0L)); - } else { - keyStates = subscription.getKeyStates().entrySet() - .stream().filter(entry -> entityView.getKeys().getTimeseries().contains(entry.getKey())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - } - - return TbTimeseriesSubscription.builder() - .serviceId(subscription.getServiceId()) - .sessionId(subscription.getSessionId()) - .subscriptionId(subscription.getSubscriptionId()) - .tenantId(subscription.getTenantId()) - .entityId(entityView.getEntityId()) - .startTime(entityView.getStartTimeMs()) - .endTime(entityView.getEndTimeMs()) - .allKeys(false) - .keyStates(keyStates).build(); - } - private void registerSubscription(TbSubscription subscription) { Map sessionSubscriptions = subscriptionsBySessionId.computeIfAbsent(subscription.getSessionId(), k -> new ConcurrentHashMap<>()); sessionSubscriptions.put(subscription.getSubscriptionId(), subscription); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java index 4d9454ccce..c0ca01685d 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java @@ -16,12 +16,13 @@ package org.thingsboard.server.service.subscription; import org.springframework.context.ApplicationListener; +import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; -import org.thingsboard.server.queue.discovery.PartitionChangeEvent; import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.queue.discovery.PartitionChangeEvent; import java.util.List; @@ -35,6 +36,13 @@ public interface SubscriptionManagerService extends ApplicationListener attributes, TbCallback callback); - void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List attributes, TbCallback callback, boolean notifyDevice); + void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List attributes, boolean notifyDevice, TbCallback callback); + + void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List keys, TbCallback empty); + + void onAlarmUpdate(TenantId tenantId, EntityId entityId, Alarm alarm, TbCallback callback); + + void onAlarmDeleted(TenantId tenantId, EntityId entityId, Alarm alarm, TbCallback callback); + } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionServiceStatistics.java b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionServiceStatistics.java new file mode 100644 index 0000000000..80f3759c80 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionServiceStatistics.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.subscription; + +import lombok.Data; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +@Data +public class SubscriptionServiceStatistics { + private AtomicInteger alarmQueryInvocationCnt = new AtomicInteger(); + private AtomicInteger regularQueryInvocationCnt = new AtomicInteger(); + private AtomicInteger dynamicQueryInvocationCnt = new AtomicInteger(); + private AtomicLong alarmQueryTimeSpent = new AtomicLong(); + private AtomicLong regularQueryTimeSpent = new AtomicLong(); + private AtomicLong dynamicQueryTimeSpent = new AtomicLong(); +} diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java new file mode 100644 index 0000000000..41fbfd7ab2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java @@ -0,0 +1,470 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.subscription; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AbstractDataQuery; +import org.thingsboard.server.common.data.query.ComplexFilterPredicate; +import org.thingsboard.server.common.data.query.DynamicValue; +import org.thingsboard.server.common.data.query.DynamicValueSourceType; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.FilterPredicateType; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.KeyFilterPredicate; +import org.thingsboard.server.common.data.query.SimpleKeyFilterPredicate; +import org.thingsboard.server.common.data.query.TsValue; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.entity.EntityService; +import org.thingsboard.server.service.telemetry.TelemetryWebSocketService; +import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; +import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledFuture; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Slf4j +@Data +public abstract class TbAbstractDataSubCtx> { + + protected final String serviceId; + protected final SubscriptionServiceStatistics stats; + protected final TelemetryWebSocketService wsService; + protected final EntityService entityService; + protected final TbLocalSubscriptionService localSubscriptionService; + protected final AttributesService attributesService; + protected final TelemetryWebSocketSessionRef sessionRef; + protected final int cmdId; + protected final Map subToEntityIdMap; + protected final Set subToDynamicValueKeySet; + @Getter + protected final Map> dynamicValues; + @Getter + protected PageData data; + @Getter + @Setter + protected T query; + @Setter + protected volatile ScheduledFuture refreshTask; + + public TbAbstractDataSubCtx(String serviceId, TelemetryWebSocketService wsService, + EntityService entityService, TbLocalSubscriptionService localSubscriptionService, + AttributesService attributesService, SubscriptionServiceStatistics stats, + TelemetryWebSocketSessionRef sessionRef, int cmdId) { + this.serviceId = serviceId; + this.wsService = wsService; + this.entityService = entityService; + this.localSubscriptionService = localSubscriptionService; + this.attributesService = attributesService; + this.stats = stats; + this.sessionRef = sessionRef; + this.cmdId = cmdId; + this.subToEntityIdMap = new ConcurrentHashMap<>(); + this.subToDynamicValueKeySet = ConcurrentHashMap.newKeySet(); + this.dynamicValues = new ConcurrentHashMap<>(); + } + + public void setAndResolveQuery(T query) { + dynamicValues.clear(); + this.query = query; + if (query.getKeyFilters() != null) { + for (KeyFilter filter : query.getKeyFilters()) { + registerDynamicValues(filter.getPredicate()); + } + } + resolve(getTenantId(), getCustomerId(), getUserId()); + } + + public void resolve(TenantId tenantId, CustomerId customerId, UserId userId) { + List> futures = new ArrayList<>(); + for (DynamicValueKey key : dynamicValues.keySet()) { + switch (key.getSourceType()) { + case CURRENT_TENANT: + futures.add(resolveEntityValue(tenantId, tenantId, key)); + break; + case CURRENT_CUSTOMER: + if (customerId != null && !customerId.isNullUid()) { + futures.add(resolveEntityValue(tenantId, customerId, key)); + } + break; + case CURRENT_USER: + if (userId != null && !userId.isNullUid()) { + futures.add(resolveEntityValue(tenantId, userId, key)); + } + break; + } + } + try { + Map> tmpSubMap = new HashMap<>(); + for (DynamicValueKeySub sub : Futures.successfulAsList(futures).get()) { + tmpSubMap.computeIfAbsent(sub.getEntityId(), tmp -> new HashMap<>()).put(sub.getKey().getSourceAttribute(), sub); + } + for (EntityId entityId : tmpSubMap.keySet()) { + Map keyStates = new HashMap<>(); + Map dynamicValueKeySubMap = tmpSubMap.get(entityId); + dynamicValueKeySubMap.forEach((k, v) -> keyStates.put(k, v.getLastUpdateTs())); + int subIdx = sessionRef.getSessionSubIdSeq().incrementAndGet(); + TbAttributeSubscription sub = TbAttributeSubscription.builder() + .serviceId(serviceId) + .sessionId(sessionRef.getSessionId()) + .subscriptionId(subIdx) + .tenantId(sessionRef.getSecurityCtx().getTenantId()) + .entityId(entityId) + .updateConsumer((s, subscriptionUpdate) -> dynamicValueSubUpdate(s, subscriptionUpdate, dynamicValueKeySubMap)) + .allKeys(false) + .keyStates(keyStates) + .scope(TbAttributeSubscriptionScope.SERVER_SCOPE) + .build(); + subToDynamicValueKeySet.add(subIdx); + localSubscriptionService.addSubscription(sub); + } + } catch (InterruptedException | ExecutionException e) { + log.info("[{}][{}][{}] Failed to resolve dynamic values: {}", tenantId, customerId, userId, dynamicValues.keySet()); + } + + } + + private void dynamicValueSubUpdate(String sessionId, TelemetrySubscriptionUpdate subscriptionUpdate, + Map dynamicValueKeySubMap) { + Map latestUpdate = new HashMap<>(); + subscriptionUpdate.getData().forEach((k, v) -> { + Object[] data = (Object[]) v.get(0); + latestUpdate.put(k, new TsValue((Long) data[0], (String) data[1])); + }); + + boolean invalidateFilter = false; + for (Map.Entry entry : latestUpdate.entrySet()) { + String k = entry.getKey(); + TsValue tsValue = entry.getValue(); + DynamicValueKeySub sub = dynamicValueKeySubMap.get(k); + if (sub.updateValue(tsValue)) { + invalidateFilter = true; + updateDynamicValuesByKey(sub, tsValue); + } + } + + if (invalidateFilter) { + update(); + } + } + + public void fetchData() { + this.data = findEntityData(); + } + + protected PageData findEntityData() { + PageData result = entityService.findEntityDataByQuery(getTenantId(), getCustomerId(), buildEntityDataQuery()); + if (log.isTraceEnabled()) { + result.getData().forEach(ed -> { + log.trace("[{}][{}] EntityData: {}", getSessionId(), getCmdId(), ed); + }); + } + return result; + } + + protected synchronized void update() { + long start = System.currentTimeMillis(); + PageData newData = findEntityData(); + long end = System.currentTimeMillis(); + stats.getRegularQueryInvocationCnt().incrementAndGet(); + stats.getRegularQueryTimeSpent().addAndGet(end - start); + Map oldDataMap; + if (data != null && !data.getData().isEmpty()) { + oldDataMap = data.getData().stream().collect(Collectors.toMap(EntityData::getEntityId, Function.identity(), (a, b) -> a)); + } else { + oldDataMap = Collections.emptyMap(); + } + Map newDataMap = newData.getData().stream().collect(Collectors.toMap(EntityData::getEntityId, Function.identity(), (a,b)-> a)); + if (oldDataMap.size() == newDataMap.size() && oldDataMap.keySet().equals(newDataMap.keySet())) { + log.trace("[{}][{}] No updates to entity data found", sessionRef.getSessionId(), cmdId); + } else { + this.data = newData; + doUpdate(newDataMap); + } + } + + protected abstract void doUpdate(Map newDataMap); + + protected abstract EntityDataQuery buildEntityDataQuery(); + + public List getEntitiesData() { + return data.getData(); + } + + @Data + private static class DynamicValueKeySub { + private final DynamicValueKey key; + private final EntityId entityId; + private long lastUpdateTs; + private String lastUpdateValue; + + boolean updateValue(TsValue value) { + if (value.getTs() > lastUpdateTs && (lastUpdateValue == null || !lastUpdateValue.equals(value.getValue()))) { + this.lastUpdateTs = value.getTs(); + this.lastUpdateValue = value.getValue(); + return true; + } else { + return false; + } + } + } + + private ListenableFuture resolveEntityValue(TenantId tenantId, EntityId entityId, DynamicValueKey key) { + ListenableFuture> entry = attributesService.find(tenantId, entityId, + TbAttributeSubscriptionScope.SERVER_SCOPE.name(), key.getSourceAttribute()); + return Futures.transform(entry, attributeOpt -> { + DynamicValueKeySub sub = new DynamicValueKeySub(key, entityId); + if (attributeOpt.isPresent()) { + AttributeKvEntry attribute = attributeOpt.get(); + sub.setLastUpdateTs(attribute.getLastUpdateTs()); + sub.setLastUpdateValue(attribute.getValueAsString()); + updateDynamicValuesByKey(sub, new TsValue(attribute.getLastUpdateTs(), attribute.getValueAsString())); + } + return sub; + }, MoreExecutors.directExecutor()); + } + + private void updateDynamicValuesByKey(DynamicValueKeySub sub, TsValue tsValue) { + DynamicValueKey dvk = sub.getKey(); + switch (dvk.getPredicateType()) { + case STRING: + dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(tsValue.getValue())); + break; + case NUMERIC: + try { + Double dValue = Double.parseDouble(tsValue.getValue()); + dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(dValue)); + } catch (NumberFormatException e) { + dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(null)); + } + break; + case BOOLEAN: + Boolean bValue = Boolean.parseBoolean(tsValue.getValue()); + dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(bValue)); + break; + } + } + + private void registerDynamicValues(KeyFilterPredicate predicate) { + switch (predicate.getType()) { + case STRING: + case NUMERIC: + case BOOLEAN: + Optional value = getDynamicValueFromSimplePredicate((SimpleKeyFilterPredicate) predicate); + if (value.isPresent()) { + DynamicValue dynamicValue = value.get(); + DynamicValueKey key = new DynamicValueKey( + predicate.getType(), + dynamicValue.getSourceType(), + dynamicValue.getSourceAttribute()); + dynamicValues.computeIfAbsent(key, tmp -> new ArrayList<>()).add(dynamicValue); + } + break; + case COMPLEX: + ((ComplexFilterPredicate) predicate).getPredicates().forEach(this::registerDynamicValues); + } + } + + private Optional> getDynamicValueFromSimplePredicate(SimpleKeyFilterPredicate predicate) { + if (predicate.getValue().getUserValue() == null) { + return Optional.ofNullable(predicate.getValue().getDynamicValue()); + } else { + return Optional.empty(); + } + } + + public String getSessionId() { + return sessionRef.getSessionId(); + } + + public TenantId getTenantId() { + return sessionRef.getSecurityCtx().getTenantId(); + } + + public CustomerId getCustomerId() { + return sessionRef.getSecurityCtx().getCustomerId(); + } + + public UserId getUserId() { + return sessionRef.getSecurityCtx().getId(); + } + + public void clearEntitySubscriptions() { + if (subToEntityIdMap != null) { + for (Integer subId : subToEntityIdMap.keySet()) { + localSubscriptionService.cancelSubscription(sessionRef.getSessionId(), subId); + } + subToEntityIdMap.clear(); + } + } + + public void clearDynamicValueSubscriptions() { + if (subToDynamicValueKeySet != null) { + for (Integer subId : subToDynamicValueKeySet) { + localSubscriptionService.cancelSubscription(sessionRef.getSessionId(), subId); + } + subToDynamicValueKeySet.clear(); + } + } + + public void setRefreshTask(ScheduledFuture task) { + this.refreshTask = task; + } + + public void cancelTasks() { + if (this.refreshTask != null) { + log.trace("[{}][{}] Canceling old refresh task", sessionRef.getSessionId(), cmdId); + this.refreshTask.cancel(true); + } + } + + public void createSubscriptions(List keys, boolean resultToLatestValues) { + Map> keysByType = getEntityKeyByTypeMap(keys); + for (EntityData entityData : data.getData()) { + List entitySubscriptions = addSubscriptions(entityData, keysByType, resultToLatestValues); + entitySubscriptions.forEach(localSubscriptionService::addSubscription); + } + } + + protected Map> getEntityKeyByTypeMap(List keys) { + Map> keysByType = new HashMap<>(); + keys.forEach(key -> keysByType.computeIfAbsent(key.getType(), k -> new ArrayList<>()).add(key)); + return keysByType; + } + + protected List addSubscriptions(EntityData entityData, Map> keysByType, boolean resultToLatestValues) { + List subscriptionList = new ArrayList<>(); + keysByType.forEach((keysType, keysList) -> { + int subIdx = sessionRef.getSessionSubIdSeq().incrementAndGet(); + subToEntityIdMap.put(subIdx, entityData.getEntityId()); + switch (keysType) { + case TIME_SERIES: + subscriptionList.add(createTsSub(entityData, subIdx, keysList, resultToLatestValues)); + break; + case CLIENT_ATTRIBUTE: + subscriptionList.add(createAttrSub(entityData, subIdx, keysType, TbAttributeSubscriptionScope.CLIENT_SCOPE, keysList)); + break; + case SHARED_ATTRIBUTE: + subscriptionList.add(createAttrSub(entityData, subIdx, keysType, TbAttributeSubscriptionScope.SHARED_SCOPE, keysList)); + break; + case SERVER_ATTRIBUTE: + subscriptionList.add(createAttrSub(entityData, subIdx, keysType, TbAttributeSubscriptionScope.SERVER_SCOPE, keysList)); + break; + case ATTRIBUTE: + subscriptionList.add(createAttrSub(entityData, subIdx, keysType, TbAttributeSubscriptionScope.ANY_SCOPE, keysList)); + break; + } + }); + return subscriptionList; + } + + private TbSubscription createAttrSub(EntityData entityData, int subIdx, EntityKeyType keysType, TbAttributeSubscriptionScope scope, List subKeys) { + Map keyStates = buildKeyStats(entityData, keysType, subKeys); + log.trace("[{}][{}][{}] Creating attributes subscription for [{}] with keys: {}", serviceId, cmdId, subIdx, entityData.getEntityId(), keyStates); + return TbAttributeSubscription.builder() + .serviceId(serviceId) + .sessionId(sessionRef.getSessionId()) + .subscriptionId(subIdx) + .tenantId(sessionRef.getSecurityCtx().getTenantId()) + .entityId(entityData.getEntityId()) + .updateConsumer((s, subscriptionUpdate) -> sendWsMsg(s, subscriptionUpdate, keysType)) + .allKeys(false) + .keyStates(keyStates) + .scope(scope) + .build(); + } + + private TbSubscription createTsSub(EntityData entityData, int subIdx, List subKeys, boolean resultToLatestValues) { + Map keyStates = buildKeyStats(entityData, EntityKeyType.TIME_SERIES, subKeys); + if (entityData.getTimeseries() != null) { + entityData.getTimeseries().forEach((k, v) -> { + long ts = Arrays.stream(v).map(TsValue::getTs).max(Long::compareTo).orElse(0L); + log.trace("[{}][{}] Updating key: {} with ts: {}", serviceId, cmdId, k, ts); + keyStates.put(k, ts); + }); + } + log.trace("[{}][{}][{}] Creating time-series subscription for [{}] with keys: {}", serviceId, cmdId, subIdx, entityData.getEntityId(), keyStates); + return TbTimeseriesSubscription.builder() + .serviceId(serviceId) + .sessionId(sessionRef.getSessionId()) + .subscriptionId(subIdx) + .tenantId(sessionRef.getSecurityCtx().getTenantId()) + .entityId(entityData.getEntityId()) + .updateConsumer((sessionId, subscriptionUpdate) -> sendWsMsg(sessionId, subscriptionUpdate, EntityKeyType.TIME_SERIES, resultToLatestValues)) + .allKeys(false) + .keyStates(keyStates) + .build(); + } + + private void sendWsMsg(String sessionId, TelemetrySubscriptionUpdate subscriptionUpdate, EntityKeyType keyType) { + sendWsMsg(sessionId, subscriptionUpdate, keyType, true); + } + + private Map buildKeyStats(EntityData entityData, EntityKeyType keysType, List subKeys) { + Map keyStates = new HashMap<>(); + subKeys.forEach(key -> keyStates.put(key.getKey(), 0L)); + if (entityData.getLatest() != null) { + Map currentValues = entityData.getLatest().get(keysType); + if (currentValues != null) { + currentValues.forEach((k, v) -> { + log.trace("[{}][{}] Updating key: {} with ts: {}", serviceId, cmdId, k, v.getTs()); + keyStates.put(k, v.getTs()); + }); + } + } + return keyStates; + } + + abstract void sendWsMsg(String sessionId, TelemetrySubscriptionUpdate subscriptionUpdate, EntityKeyType keyType, boolean resultToLatestValues); + + @Data + private static class DynamicValueKey { + @Getter + private final FilterPredicateType predicateType; + @Getter + private final DynamicValueSourceType sourceType; + @Getter + private final String sourceAttribute; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java new file mode 100644 index 0000000000..1f7ab2a04b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java @@ -0,0 +1,309 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.subscription; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AlarmData; +import org.thingsboard.server.common.data.query.AlarmDataPageLink; +import org.thingsboard.server.common.data.query.AlarmDataQuery; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.TsValue; +import org.thingsboard.server.dao.alarm.AlarmService; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.entity.EntityService; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.service.telemetry.TelemetryWebSocketService; +import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; +import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataUpdate; +import org.thingsboard.server.service.telemetry.sub.AlarmSubscriptionUpdate; +import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Slf4j +public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx { + + private final AlarmService alarmService; + @Getter + @Setter + private final LinkedHashMap entitiesMap; + @Getter + @Setter + private final HashMap alarmsMap; + + private final int maxEntitiesPerAlarmSubscription; + + @Getter + @Setter + private PageData alarms; + @Getter + @Setter + private boolean tooManyEntities; + + public TbAlarmDataSubCtx(String serviceId, TelemetryWebSocketService wsService, + EntityService entityService, TbLocalSubscriptionService localSubscriptionService, + AttributesService attributesService, SubscriptionServiceStatistics stats, AlarmService alarmService, + TelemetryWebSocketSessionRef sessionRef, int cmdId, int maxEntitiesPerAlarmSubscription) { + super(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, sessionRef, cmdId); + this.maxEntitiesPerAlarmSubscription = maxEntitiesPerAlarmSubscription; + this.alarmService = alarmService; + this.entitiesMap = new LinkedHashMap<>(); + this.alarmsMap = new HashMap<>(); + } + + public void fetchAlarms() { + AlarmDataUpdate update; + if (!entitiesMap.isEmpty()) { + long start = System.currentTimeMillis(); + PageData alarms = alarmService.findAlarmDataByQueryForEntities(getTenantId(), getCustomerId(), + query, getOrderedEntityIds()); + long end = System.currentTimeMillis(); + stats.getAlarmQueryInvocationCnt().incrementAndGet(); + stats.getAlarmQueryTimeSpent().addAndGet(end - start); + alarms = setAndMergeAlarmsData(alarms); + update = new AlarmDataUpdate(cmdId, alarms, null, maxEntitiesPerAlarmSubscription, data.getTotalElements()); + } else { + update = new AlarmDataUpdate(cmdId, new PageData<>(), null, maxEntitiesPerAlarmSubscription, data.getTotalElements()); + } + wsService.sendWsMsg(getSessionId(), update); + } + + public void fetchData() { + super.fetchData(); + entitiesMap.clear(); + tooManyEntities = data.hasNext(); + for (EntityData entityData : data.getData()) { + entitiesMap.put(entityData.getEntityId(), entityData); + } + } + + public Collection getOrderedEntityIds() { + return entitiesMap.keySet(); + } + + public PageData setAndMergeAlarmsData(PageData alarms) { + this.alarms = alarms; + for (AlarmData alarmData : alarms.getData()) { + EntityId entityId = alarmData.getEntityId(); + if (entityId != null) { + EntityData entityData = entitiesMap.get(entityId); + if (entityData != null) { + alarmData.getLatest().putAll(entityData.getLatest()); + } + } + } + alarmsMap.clear(); + alarmsMap.putAll(alarms.getData().stream().collect(Collectors.toMap(AlarmData::getId, Function.identity(), (a, b) -> a))); + return this.alarms; + } + + @Override + public void createSubscriptions(List keys, boolean resultToLatestValues) { + super.createSubscriptions(keys, resultToLatestValues); + createAlarmSubscriptions(); + } + + public void createAlarmSubscriptions() { + AlarmDataPageLink pageLink = query.getPageLink(); + long startTs = System.currentTimeMillis() - pageLink.getTimeWindow(); + for (EntityData entityData : entitiesMap.values()) { + createAlarmSubscriptionForEntity(pageLink, startTs, entityData); + } + } + + private void createAlarmSubscriptionForEntity(AlarmDataPageLink pageLink, long startTs, EntityData entityData) { + int subIdx = sessionRef.getSessionSubIdSeq().incrementAndGet(); + subToEntityIdMap.put(subIdx, entityData.getEntityId()); + log.trace("[{}][{}][{}] Creating alarms subscription for [{}] with query: {}", serviceId, cmdId, subIdx, entityData.getEntityId(), pageLink); + TbAlarmsSubscription subscription = TbAlarmsSubscription.builder() + .serviceId(serviceId) + .sessionId(sessionRef.getSessionId()) + .subscriptionId(subIdx) + .tenantId(sessionRef.getSecurityCtx().getTenantId()) + .entityId(entityData.getEntityId()) + .updateConsumer(this::sendWsMsg) + .ts(startTs) + .build(); + localSubscriptionService.addSubscription(subscription); + } + + @Override + void sendWsMsg(String sessionId, TelemetrySubscriptionUpdate subscriptionUpdate, EntityKeyType keyType, boolean resultToLatestValues) { + EntityId entityId = subToEntityIdMap.get(subscriptionUpdate.getSubscriptionId()); + if (entityId != null) { + Map latestUpdate = new HashMap<>(); + subscriptionUpdate.getData().forEach((k, v) -> { + Object[] data = (Object[]) v.get(0); + latestUpdate.put(k, new TsValue((Long) data[0], (String) data[1])); + }); + EntityData entityData = entitiesMap.get(entityId); + entityData.getLatest().computeIfAbsent(keyType, tmp -> new HashMap<>()).putAll(latestUpdate); + log.trace("[{}][{}][{}][{}] Received subscription update: {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), keyType, subscriptionUpdate); + List update = alarmsMap.values().stream().filter(alarm -> entityId.equals(alarm.getEntityId())).map(alarm -> { + alarm.getLatest().computeIfAbsent(keyType, tmp -> new HashMap<>()).putAll(latestUpdate); + return alarm; + }).collect(Collectors.toList()); + wsService.sendWsMsg(sessionId, new AlarmDataUpdate(cmdId, null, update, maxEntitiesPerAlarmSubscription, data.getTotalElements())); + } else { + log.trace("[{}][{}][{}][{}] Received stale subscription update: {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), keyType, subscriptionUpdate); + } + } + + private void sendWsMsg(String sessionId, AlarmSubscriptionUpdate subscriptionUpdate) { + Alarm alarm = subscriptionUpdate.getAlarm(); + AlarmId alarmId = alarm.getId(); + if (subscriptionUpdate.isAlarmDeleted()) { + Alarm deleted = alarmsMap.remove(alarmId); + if (deleted != null) { + fetchAlarms(); + } + } else { + AlarmData current = alarmsMap.get(alarmId); + boolean onCurrentPage = current != null; + boolean matchesFilter = filter(alarm); + if (onCurrentPage) { + if (matchesFilter) { + AlarmData updated = new AlarmData(alarm, current.getOriginatorName(), current.getEntityId()); + updated.getLatest().putAll(current.getLatest()); + alarmsMap.put(alarmId, updated); + wsService.sendWsMsg(sessionId, new AlarmDataUpdate(cmdId, null, Collections.singletonList(updated), maxEntitiesPerAlarmSubscription, data.getTotalElements())); + } else { + fetchAlarms(); + } + } else if (matchesFilter && query.getPageLink().getPage() == 0) { + fetchAlarms(); + } + } + } + + public void cleanupOldAlarms() { + long expTime = System.currentTimeMillis() - query.getPageLink().getTimeWindow(); + boolean shouldRefresh = false; + for (AlarmData alarmData : alarms.getData()) { + if (alarmData.getCreatedTime() < expTime) { + shouldRefresh = true; + break; + } + } + if (shouldRefresh) { + fetchAlarms(); + } + } + + private boolean filter(Alarm alarm) { + AlarmDataPageLink filter = query.getPageLink(); + long startTs = System.currentTimeMillis() - filter.getTimeWindow(); + if (alarm.getCreatedTime() < startTs) { + //Skip update that does not match time window. + return false; + } + if (filter.getTypeList() != null && !filter.getTypeList().isEmpty() && !filter.getTypeList().contains(alarm.getType())) { + return false; + } + if (filter.getSeverityList() != null && !filter.getSeverityList().isEmpty()) { + if (!filter.getSeverityList().contains(alarm.getSeverity())) { + return false; + } + } + if (filter.getStatusList() != null && !filter.getStatusList().isEmpty()) { + boolean matches = false; + for (AlarmSearchStatus status : filter.getStatusList()) { + if (status.getStatuses().contains(alarm.getStatus())) { + matches = true; + break; + } + } + if (!matches) { + return false; + } + } + return true; + } + + @Override + protected synchronized void doUpdate(Map newDataMap) { + entitiesMap.clear(); + tooManyEntities = data.hasNext(); + for (EntityData entityData : data.getData()) { + entitiesMap.put(entityData.getEntityId(), entityData); + } + fetchAlarms(); + List subIdsToCancel = new ArrayList<>(); + List subsToAdd = new ArrayList<>(); + Set currentSubs = new HashSet<>(); + subToEntityIdMap.forEach((subId, entityId) -> { + if (!newDataMap.containsKey(entityId)) { + subIdsToCancel.add(subId); + } else { + currentSubs.add(entityId); + } + }); + log.trace("[{}][{}] Subscriptions that are invalid: {}", sessionRef.getSessionId(), cmdId, subIdsToCancel); + subIdsToCancel.forEach(subToEntityIdMap::remove); + List newSubsList = newDataMap.entrySet().stream().filter(entry -> !currentSubs.contains(entry.getKey())).map(Map.Entry::getValue).collect(Collectors.toList()); + if (!newSubsList.isEmpty()) { + List keys = query.getLatestValues(); + if (keys != null && !keys.isEmpty()) { + Map> keysByType = getEntityKeyByTypeMap(keys); + newSubsList.forEach( + entity -> { + log.trace("[{}][{}] Found new subscription for entity: {}", sessionRef.getSessionId(), cmdId, entity.getEntityId()); + subsToAdd.addAll(addSubscriptions(entity, keysByType, true)); + } + ); + } + long startTs = System.currentTimeMillis() - query.getPageLink().getTimeWindow(); + newSubsList.forEach(entity -> createAlarmSubscriptionForEntity(query.getPageLink(), startTs, entity)); + } + subIdsToCancel.forEach(subId -> localSubscriptionService.cancelSubscription(getSessionId(), subId)); + subsToAdd.forEach(localSubscriptionService::addSubscription); + } + + @Override + protected EntityDataQuery buildEntityDataQuery() { + EntityDataSortOrder sortOrder = query.getPageLink().getSortOrder(); + EntityDataSortOrder entitiesSortOrder; + if (sortOrder == null || sortOrder.getKey().getType().equals(EntityKeyType.ALARM_FIELD)) { + entitiesSortOrder = new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, ModelConstants.CREATED_TIME_PROPERTY)); + } else { + entitiesSortOrder = sortOrder; + } + EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder); + return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters()); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java new file mode 100644 index 0000000000..3f570cffaa --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.subscription; + +import lombok.Builder; +import lombok.Getter; +import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.service.telemetry.sub.AlarmSubscriptionUpdate; + +import java.util.List; +import java.util.function.BiConsumer; + +public class TbAlarmsSubscription extends TbSubscription { + + @Getter + private final long ts; + + @Builder + public TbAlarmsSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId, + BiConsumer updateConsumer, long ts) { + super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.ALARMS, updateConsumer); + this.ts = ts; + } + + @Override + public boolean equals(Object o) { + return super.equals(o); + } + + @Override + public int hashCode() { + return super.hashCode(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAttributeSubscription.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAttributeSubscription.java index 83a86efefc..52a90c9bbf 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAttributeSubscription.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAttributeSubscription.java @@ -16,14 +16,15 @@ package org.thingsboard.server.service.subscription; import lombok.Builder; -import lombok.Data; import lombok.Getter; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; import java.util.Map; +import java.util.function.BiConsumer; -public class TbAttributeSubscription extends TbSubscription { +public class TbAttributeSubscription extends TbSubscription { @Getter private final boolean allKeys; @Getter private final Map keyStates; @@ -31,8 +32,9 @@ public class TbAttributeSubscription extends TbSubscription { @Builder public TbAttributeSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId, + BiConsumer updateConsumer, boolean allKeys, Map keyStates, TbAttributeSubscriptionScope scope) { - super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.ATTRIBUTES); + super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.ATTRIBUTES, updateConsumer); this.allKeys = allKeys; this.keyStates = keyStates; this.scope = scope; diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java new file mode 100644 index 0000000000..c43ee664fc --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java @@ -0,0 +1,220 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.subscription; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.TsValue; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.entity.EntityService; +import org.thingsboard.server.service.telemetry.TelemetryWebSocketService; +import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; +import org.thingsboard.server.service.telemetry.cmd.v2.LatestValueCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.TimeSeriesCmd; +import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +@Slf4j +public class TbEntityDataSubCtx extends TbAbstractDataSubCtx { + + @Getter + @Setter + private TimeSeriesCmd tsCmd; + @Getter + @Setter + private boolean initialDataSent; + private TimeSeriesCmd curTsCmd; + private LatestValueCmd latestValueCmd; + @Getter + private final int maxEntitiesPerDataSubscription; + + public TbEntityDataSubCtx(String serviceId, TelemetryWebSocketService wsService, EntityService entityService, + TbLocalSubscriptionService localSubscriptionService, AttributesService attributesService, + SubscriptionServiceStatistics stats, TelemetryWebSocketSessionRef sessionRef, int cmdId, int maxEntitiesPerDataSubscription) { + super(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, sessionRef, cmdId); + this.maxEntitiesPerDataSubscription = maxEntitiesPerDataSubscription; + } + + @Override + protected void sendWsMsg(String sessionId, TelemetrySubscriptionUpdate subscriptionUpdate, EntityKeyType keyType, boolean resultToLatestValues) { + EntityId entityId = subToEntityIdMap.get(subscriptionUpdate.getSubscriptionId()); + if (entityId != null) { + log.trace("[{}][{}][{}][{}] Received subscription update: {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), keyType, subscriptionUpdate); + if (resultToLatestValues) { + sendLatestWsMsg(entityId, sessionId, subscriptionUpdate, keyType); + } else { + sendTsWsMsg(entityId, sessionId, subscriptionUpdate, keyType); + } + } else { + log.trace("[{}][{}][{}][{}] Received stale subscription update: {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), keyType, subscriptionUpdate); + } + } + + private void sendLatestWsMsg(EntityId entityId, String sessionId, TelemetrySubscriptionUpdate subscriptionUpdate, EntityKeyType keyType) { + Map latestUpdate = new HashMap<>(); + subscriptionUpdate.getData().forEach((k, v) -> { + Object[] data = (Object[]) v.get(0); + latestUpdate.put(k, new TsValue((Long) data[0], (String) data[1])); + }); + EntityData entityData = getDataForEntity(entityId); + if (entityData != null && entityData.getLatest() != null) { + Map latestCtxValues = entityData.getLatest().get(keyType); + log.trace("[{}][{}][{}] Going to compare update with {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), latestCtxValues); + if (latestCtxValues != null) { + latestCtxValues.forEach((k, v) -> { + TsValue update = latestUpdate.get(k); + if (update != null) { + //Ignore notifications about deleted keys + if (!(update.getTs() == 0 && (update.getValue() == null || update.getValue().isEmpty()))) { + if (update.getTs() < v.getTs()) { + log.trace("[{}][{}][{}] Removed stale update for key: {} and ts: {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), k, update.getTs()); + latestUpdate.remove(k); + } else if ((update.getTs() == v.getTs() && update.getValue().equals(v.getValue()))) { + log.trace("[{}][{}][{}] Removed duplicate update for key: {} and ts: {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), k, update.getTs()); + latestUpdate.remove(k); + } + } else { + log.trace("[{}][{}][{}] Received deleted notification for: {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), k); + } + } + }); + //Setting new values + latestUpdate.forEach(latestCtxValues::put); + } + } + if (!latestUpdate.isEmpty()) { + Map> latestMap = Collections.singletonMap(keyType, latestUpdate); + entityData = new EntityData(entityId, latestMap, null); + wsService.sendWsMsg(sessionId, new EntityDataUpdate(cmdId, null, Collections.singletonList(entityData), maxEntitiesPerDataSubscription)); + } + } + + private void sendTsWsMsg(EntityId entityId, String sessionId, TelemetrySubscriptionUpdate subscriptionUpdate, EntityKeyType keyType) { + Map> tsUpdate = new HashMap<>(); + subscriptionUpdate.getData().forEach((k, v) -> { + Object[] data = (Object[]) v.get(0); + tsUpdate.computeIfAbsent(k, key -> new ArrayList<>()).add(new TsValue((Long) data[0], (String) data[1])); + }); + EntityData entityData = getDataForEntity(entityId); + if (entityData != null && entityData.getLatest() != null) { + Map latestCtxValues = entityData.getLatest().get(keyType); + log.trace("[{}][{}][{}] Going to compare update with {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), latestCtxValues); + if (latestCtxValues != null) { + latestCtxValues.forEach((k, v) -> { + List updateList = tsUpdate.get(k); + if (updateList != null) { + for (TsValue update : new ArrayList<>(updateList)) { + if (update.getTs() < v.getTs()) { + log.trace("[{}][{}][{}] Removed stale update for key: {} and ts: {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), k, update.getTs()); + updateList.remove(update); + } else if ((update.getTs() == v.getTs() && update.getValue().equals(v.getValue()))) { + log.trace("[{}][{}][{}] Removed duplicate update for key: {} and ts: {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), k, update.getTs()); + updateList.remove(update); + } + if (updateList.isEmpty()) { + tsUpdate.remove(k); + } + } + } + }); + //Setting new values + tsUpdate.forEach((k, v) -> { + Optional maxValue = v.stream().max(Comparator.comparingLong(TsValue::getTs)); + maxValue.ifPresent(max -> latestCtxValues.put(k, max)); + }); + } + } + if (!tsUpdate.isEmpty()) { + Map tsMap = new HashMap<>(); + tsUpdate.forEach((key, tsValue) -> tsMap.put(key, tsValue.toArray(new TsValue[tsValue.size()]))); + entityData = new EntityData(entityId, null, tsMap); + wsService.sendWsMsg(sessionId, new EntityDataUpdate(cmdId, null, Collections.singletonList(entityData), maxEntitiesPerDataSubscription)); + } + } + + private EntityData getDataForEntity(EntityId entityId) { + return data.getData().stream().filter(item -> item.getEntityId().equals(entityId)).findFirst().orElse(null); + } + + public synchronized void doUpdate(Map newDataMap) { + List subIdsToCancel = new ArrayList<>(); + List subsToAdd = new ArrayList<>(); + Set currentSubs = new HashSet<>(); + subToEntityIdMap.forEach((subId, entityId) -> { + if (!newDataMap.containsKey(entityId)) { + subIdsToCancel.add(subId); + } else { + currentSubs.add(entityId); + } + }); + log.trace("[{}][{}] Subscriptions that are invalid: {}", sessionRef.getSessionId(), cmdId, subIdsToCancel); + subIdsToCancel.forEach(subToEntityIdMap::remove); + List newSubsList = newDataMap.entrySet().stream().filter(entry -> !currentSubs.contains(entry.getKey())).map(Map.Entry::getValue).collect(Collectors.toList()); + if (!newSubsList.isEmpty()) { + boolean resultToLatestValues; + List keys = null; + if (curTsCmd != null) { + resultToLatestValues = false; + keys = curTsCmd.getKeys().stream().map(key -> new EntityKey(EntityKeyType.TIME_SERIES, key)).collect(Collectors.toList()); + } else if (latestValueCmd != null) { + resultToLatestValues = true; + keys = latestValueCmd.getKeys(); + } else { + resultToLatestValues = true; + } + if (keys != null && !keys.isEmpty()) { + Map> keysByType = getEntityKeyByTypeMap(keys); + newSubsList.forEach( + entity -> { + log.trace("[{}][{}] Found new subscription for entity: {}", sessionRef.getSessionId(), cmdId, entity.getEntityId()); + subsToAdd.addAll(addSubscriptions(entity, keysByType, resultToLatestValues)); + } + ); + } + } + wsService.sendWsMsg(sessionRef.getSessionId(), new EntityDataUpdate(cmdId, data, null, maxEntitiesPerDataSubscription)); + subIdsToCancel.forEach(subId -> localSubscriptionService.cancelSubscription(getSessionId(), subId)); + subsToAdd.forEach(localSubscriptionService::addSubscription); + } + + public void setCurrentCmd(EntityDataCmd cmd) { + curTsCmd = cmd.getTsCmd(); + latestValueCmd = cmd.getLatestCmd(); + } + + @Override + protected EntityDataQuery buildEntityDataQuery() { + return query; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubscriptionService.java new file mode 100644 index 0000000000..4f5d9543b9 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubscriptionService.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.subscription; + +import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; +import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUnsubscribeCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.UnsubscribeCmd; + +public interface TbEntityDataSubscriptionService { + + void handleCmd(TelemetryWebSocketSessionRef sessionId, EntityDataCmd cmd); + + void handleCmd(TelemetryWebSocketSessionRef sessionId, AlarmDataCmd cmd); + + void cancelSubscription(String sessionId, UnsubscribeCmd subscriptionId); + + void cancelAllSessionSubscriptions(String sessionId); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbLocalSubscriptionService.java index 58fba24250..e8c0c46505 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbLocalSubscriptionService.java @@ -18,7 +18,8 @@ package org.thingsboard.server.service.subscription; import org.thingsboard.server.queue.discovery.ClusterTopologyChangeEvent; import org.thingsboard.server.queue.discovery.PartitionChangeEvent; import org.thingsboard.server.common.msg.queue.TbCallback; -import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate; +import org.thingsboard.server.service.telemetry.sub.AlarmSubscriptionUpdate; +import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; public interface TbLocalSubscriptionService { @@ -28,7 +29,9 @@ public interface TbLocalSubscriptionService { void cancelAllSessionSubscriptions(String sessionId); - void onSubscriptionUpdate(String sessionId, SubscriptionUpdate update, TbCallback callback); + void onSubscriptionUpdate(String sessionId, TelemetrySubscriptionUpdate update, TbCallback callback); + + void onSubscriptionUpdate(String sessionId, AlarmSubscriptionUpdate update, TbCallback callback); void onApplicationEvent(PartitionChangeEvent event); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java index 22b37ff690..d49f31be8b 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java @@ -21,10 +21,11 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import java.util.Objects; +import java.util.function.BiConsumer; @Data @AllArgsConstructor -public abstract class TbSubscription { +public abstract class TbSubscription { private final String serviceId; private final String sessionId; @@ -32,6 +33,7 @@ public abstract class TbSubscription { private final TenantId tenantId; private final EntityId entityId; private final TbSubscriptionType type; + private final BiConsumer updateConsumer; @Override public boolean equals(Object o) { diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionType.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionType.java index d7a4715460..e7dfb168b1 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionType.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionType.java @@ -16,5 +16,5 @@ package org.thingsboard.server.service.subscription; public enum TbSubscriptionType { - TIMESERIES, ATTRIBUTES + TIMESERIES, ATTRIBUTES, ALARMS } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java index d0b4d713f9..9b9581754c 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.subscription; +import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; @@ -29,11 +30,14 @@ import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.KeyValueProto; import org.thingsboard.server.gen.transport.TransportProtos.KeyValueType; import org.thingsboard.server.gen.transport.TransportProtos.SubscriptionMgrMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.TbAttributeSubscriptionProto; import org.thingsboard.server.gen.transport.TransportProtos.TbAttributeUpdateProto; +import org.thingsboard.server.gen.transport.TransportProtos.TbAttributeDeleteProto; import org.thingsboard.server.gen.transport.TransportProtos.TbSubscriptionCloseProto; import org.thingsboard.server.gen.transport.TransportProtos.TbSubscriptionKetStateProto; import org.thingsboard.server.gen.transport.TransportProtos.TbSubscriptionProto; @@ -42,8 +46,11 @@ import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesSubscrip import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesUpdateProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto; +import org.thingsboard.server.gen.transport.TransportProtos.TbAlarmUpdateProto; +import org.thingsboard.server.gen.transport.TransportProtos.TbAlarmDeleteProto; +import org.thingsboard.server.service.telemetry.sub.AlarmSubscriptionUpdate; import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; -import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate; +import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; import java.util.ArrayList; import java.util.HashMap; @@ -88,6 +95,13 @@ public class TbSubscriptionUtils { TbSubscriptionKetStateProto.newBuilder().setKey(key).setTs(value).build())); msgBuilder.setAttributeSub(aSubProto.build()); break; + case ALARMS: + TbAlarmsSubscription alarmSub = (TbAlarmsSubscription) subscription; + TransportProtos.TbAlarmSubscriptionProto.Builder alarmSubProto = TransportProtos.TbAlarmSubscriptionProto.newBuilder() + .setSub(subscriptionProto) + .setTs(alarmSub.getTs()); + msgBuilder.setAlarmSub(alarmSubProto.build()); + break; } return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build(); } @@ -136,9 +150,21 @@ public class TbSubscriptionUtils { return builder.build(); } - public static SubscriptionUpdate fromProto(TbSubscriptionUpdateProto proto) { + public static TbSubscription fromProto(TransportProtos.TbAlarmSubscriptionProto alarmSub) { + TbSubscriptionProto subProto = alarmSub.getSub(); + TbAlarmsSubscription.TbAlarmsSubscriptionBuilder builder = TbAlarmsSubscription.builder() + .serviceId(subProto.getServiceId()) + .sessionId(subProto.getSessionId()) + .subscriptionId(subProto.getSubscriptionId()) + .entityId(EntityIdFactory.getByTypeAndUuid(subProto.getEntityType(), new UUID(subProto.getEntityIdMSB(), subProto.getEntityIdLSB()))) + .tenantId(new TenantId(new UUID(subProto.getTenantIdMSB(), subProto.getTenantIdLSB()))); + builder.ts(alarmSub.getTs()); + return builder.build(); + } + + public static TelemetrySubscriptionUpdate fromProto(TbSubscriptionUpdateProto proto) { if (proto.getErrorCode() > 0) { - return new SubscriptionUpdate(proto.getSubscriptionId(), SubscriptionErrorCode.forCode(proto.getErrorCode()), proto.getErrorMsg()); + return new TelemetrySubscriptionUpdate(proto.getSubscriptionId(), SubscriptionErrorCode.forCode(proto.getErrorCode()), proto.getErrorMsg()); } else { Map> data = new TreeMap<>(); proto.getDataList().forEach(v -> { @@ -150,10 +176,20 @@ public class TbSubscriptionUtils { values.add(value); } }); - return new SubscriptionUpdate(proto.getSubscriptionId(), data); + return new TelemetrySubscriptionUpdate(proto.getSubscriptionId(), data); } } + public static AlarmSubscriptionUpdate fromProto(TransportProtos.TbAlarmSubscriptionUpdateProto proto) { + if (proto.getErrorCode() > 0) { + return new AlarmSubscriptionUpdate(proto.getSubscriptionId(), SubscriptionErrorCode.forCode(proto.getErrorCode()), proto.getErrorMsg()); + } else { + Alarm alarm = JacksonUtil.fromString(proto.getAlarm(), Alarm.class); + return new AlarmSubscriptionUpdate(proto.getSubscriptionId(), alarm); + } + } + + public static ToCoreMsg toTimeseriesUpdateProto(TenantId tenantId, EntityId entityId, List ts) { TbTimeSeriesUpdateProto.Builder builder = TbTimeSeriesUpdateProto.newBuilder(); builder.setEntityType(entityId.getEntityType().name()); @@ -182,6 +218,22 @@ public class TbSubscriptionUtils { return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build(); } + public static ToCoreMsg toAttributesDeleteProto(TenantId tenantId, EntityId entityId, String scope, List keys) { + TbAttributeDeleteProto.Builder builder = TbAttributeDeleteProto.newBuilder(); + builder.setEntityType(entityId.getEntityType().name()); + builder.setEntityIdMSB(entityId.getId().getMostSignificantBits()); + builder.setEntityIdLSB(entityId.getId().getLeastSignificantBits()); + builder.setTenantIdMSB(tenantId.getId().getMostSignificantBits()); + builder.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()); + builder.setScope(scope); + builder.addAllKeys(keys); + + SubscriptionMgrMsgProto.Builder msgBuilder = SubscriptionMgrMsgProto.newBuilder(); + msgBuilder.setAttrDelete(builder); + return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build(); + } + + private static TsKvProto.Builder toKeyValueProto(long ts, KvEntry attr) { KeyValueProto.Builder dataBuilder = KeyValueProto.newBuilder(); dataBuilder.setKey(attr.getKey()); @@ -244,4 +296,30 @@ public class TbSubscriptionUtils { } return entry; } + + public static ToCoreMsg toAlarmUpdateProto(TenantId tenantId, EntityId entityId, Alarm alarm) { + TbAlarmUpdateProto.Builder builder = TbAlarmUpdateProto.newBuilder(); + builder.setEntityType(entityId.getEntityType().name()); + builder.setEntityIdMSB(entityId.getId().getMostSignificantBits()); + builder.setEntityIdLSB(entityId.getId().getLeastSignificantBits()); + builder.setTenantIdMSB(tenantId.getId().getMostSignificantBits()); + builder.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()); + builder.setAlarm(JacksonUtil.toString(alarm)); + SubscriptionMgrMsgProto.Builder msgBuilder = SubscriptionMgrMsgProto.newBuilder(); + msgBuilder.setAlarmUpdate(builder); + return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build(); + } + + public static ToCoreMsg toAlarmDeletedProto(TenantId tenantId, EntityId entityId, Alarm alarm) { + TbAlarmDeleteProto.Builder builder = TbAlarmDeleteProto.newBuilder(); + builder.setEntityType(entityId.getEntityType().name()); + builder.setEntityIdMSB(entityId.getId().getMostSignificantBits()); + builder.setEntityIdLSB(entityId.getId().getLeastSignificantBits()); + builder.setTenantIdMSB(tenantId.getId().getMostSignificantBits()); + builder.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()); + builder.setAlarm(JacksonUtil.toString(alarm)); + SubscriptionMgrMsgProto.Builder msgBuilder = SubscriptionMgrMsgProto.newBuilder(); + msgBuilder.setAlarmDelete(builder); + return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build(); + } } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeseriesSubscription.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeseriesSubscription.java index 0be63f7b65..3ee55da7a5 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeseriesSubscription.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeseriesSubscription.java @@ -19,20 +19,27 @@ import lombok.Builder; import lombok.Getter; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; import java.util.Map; +import java.util.function.BiConsumer; -public class TbTimeseriesSubscription extends TbSubscription { +public class TbTimeseriesSubscription extends TbSubscription { - @Getter private final boolean allKeys; - @Getter private final Map keyStates; - @Getter private final long startTime; - @Getter private final long endTime; + @Getter + private final boolean allKeys; + @Getter + private final Map keyStates; + @Getter + private final long startTime; + @Getter + private final long endTime; @Builder public TbTimeseriesSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId, + BiConsumer updateConsumer, boolean allKeys, Map keyStates, long startTime, long endTime) { - super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.TIMESERIES); + super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.TIMESERIES, updateConsumer); this.allKeys = allKeys; this.keyStates = keyStates; this.startTime = startTime; diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/AbstractSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/AbstractSubscriptionService.java new file mode 100644 index 0000000000..468c3c26b0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/AbstractSubscriptionService.java @@ -0,0 +1,120 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.BooleanDataEntry; +import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.timeseries.TimeseriesService; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.discovery.PartitionChangeEvent; +import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.service.queue.TbClusterService; +import org.thingsboard.server.service.subscription.SubscriptionManagerService; +import org.thingsboard.server.service.subscription.TbSubscriptionUtils; + +import javax.annotation.Nullable; +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; + +/** + * Created by ashvayka on 27.03.18. + */ +@Slf4j +public abstract class AbstractSubscriptionService implements ApplicationListener { + + protected final Set currentPartitions = ConcurrentHashMap.newKeySet(); + + protected final TbClusterService clusterService; + protected final PartitionService partitionService; + protected Optional subscriptionManagerService; + + protected ExecutorService wsCallBackExecutor; + + public AbstractSubscriptionService(TbClusterService clusterService, + PartitionService partitionService) { + this.clusterService = clusterService; + this.partitionService = partitionService; + } + + @Autowired(required = false) + public void setSubscriptionManagerService(Optional subscriptionManagerService) { + this.subscriptionManagerService = subscriptionManagerService; + } + + abstract String getExecutorPrefix(); + + @PostConstruct + public void initExecutor() { + wsCallBackExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName(getExecutorPrefix() + "-service-ws-callback")); + } + + @PreDestroy + public void shutdownExecutor() { + if (wsCallBackExecutor != null) { + wsCallBackExecutor.shutdownNow(); + } + } + + @Override + @EventListener(PartitionChangeEvent.class) + public void onApplicationEvent(PartitionChangeEvent partitionChangeEvent) { + if (ServiceType.TB_CORE.equals(partitionChangeEvent.getServiceType())) { + currentPartitions.clear(); + currentPartitions.addAll(partitionChangeEvent.getPartitions()); + } + } + + protected void addWsCallback(ListenableFuture saveFuture, Consumer callback) { + Futures.addCallback(saveFuture, new FutureCallback() { + @Override + public void onSuccess(@Nullable T result) { + callback.accept(null); + } + + @Override + public void onFailure(Throwable t) { + } + }, wsCallBackExecutor); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/AlarmSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/AlarmSubscriptionService.java new file mode 100644 index 0000000000..b2825fb79a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/AlarmSubscriptionService.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry; + +import org.springframework.context.ApplicationListener; +import org.thingsboard.rule.engine.api.RuleEngineAlarmService; +import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; +import org.thingsboard.server.queue.discovery.PartitionChangeEvent; + +/** + * Created by ashvayka on 27.03.18. + */ +public interface AlarmSubscriptionService extends RuleEngineAlarmService, ApplicationListener { + +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java new file mode 100644 index 0000000000..4dea992067 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java @@ -0,0 +1,214 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmQuery; +import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.BooleanDataEntry; +import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AlarmData; +import org.thingsboard.server.common.data.query.AlarmDataPageLink; +import org.thingsboard.server.common.data.query.AlarmDataQuery; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.dao.alarm.AlarmOperationResult; +import org.thingsboard.server.dao.alarm.AlarmService; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.timeseries.TimeseriesService; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.discovery.PartitionChangeEvent; +import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.service.queue.TbClusterService; +import org.thingsboard.server.service.subscription.SubscriptionManagerService; +import org.thingsboard.server.service.subscription.TbSubscriptionUtils; +import org.thingsboard.server.service.telemetry.sub.AlarmSubscriptionUpdate; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; + +/** + * Created by ashvayka on 27.03.18. + */ +@Service +@Slf4j +public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService implements AlarmSubscriptionService { + + private final AlarmService alarmService; + + public DefaultAlarmSubscriptionService(TbClusterService clusterService, + PartitionService partitionService, + AlarmService alarmService) { + super(clusterService, partitionService); + this.alarmService = alarmService; + } + + @Autowired(required = false) + public void setSubscriptionManagerService(Optional subscriptionManagerService) { + this.subscriptionManagerService = subscriptionManagerService; + } + + @Override + String getExecutorPrefix() { + return "alarm"; + } + + @Override + public Alarm createOrUpdateAlarm(Alarm alarm) { + AlarmOperationResult result = alarmService.createOrUpdateAlarm(alarm); + if (result.isSuccessful()) { + onAlarmUpdated(result); + } + return result.getAlarm(); + } + + @Override + public Boolean deleteAlarm(TenantId tenantId, AlarmId alarmId) { + AlarmOperationResult result = alarmService.deleteAlarm(tenantId, alarmId); + onAlarmDeleted(result); + return result.isSuccessful(); + } + + @Override + public ListenableFuture ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTs) { + ListenableFuture result = alarmService.ackAlarm(tenantId, alarmId, ackTs); + Futures.addCallback(result, new AlarmUpdateCallback(), wsCallBackExecutor); + return Futures.transform(result, AlarmOperationResult::isSuccessful, wsCallBackExecutor); + } + + @Override + public ListenableFuture clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs) { + ListenableFuture result = alarmService.clearAlarm(tenantId, alarmId, details, clearTs); + Futures.addCallback(result, new AlarmUpdateCallback(), wsCallBackExecutor); + return Futures.transform(result, AlarmOperationResult::isSuccessful, wsCallBackExecutor); + } + + @Override + public ListenableFuture findAlarmByIdAsync(TenantId tenantId, AlarmId alarmId) { + return alarmService.findAlarmByIdAsync(tenantId, alarmId); + } + + @Override + public ListenableFuture findAlarmInfoByIdAsync(TenantId tenantId, AlarmId alarmId) { + return alarmService.findAlarmInfoByIdAsync(tenantId, alarmId); + } + + @Override + public ListenableFuture> findAlarms(TenantId tenantId, AlarmQuery query) { + return alarmService.findAlarms(tenantId, query); + } + + @Override + public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus) { + return alarmService.findHighestAlarmSeverity(tenantId, entityId, alarmSearchStatus, alarmStatus); + } + + @Override + public PageData findAlarmDataByQueryForEntities(TenantId tenantId, CustomerId customerId, AlarmDataQuery query, Collection orderedEntityIds) { + return alarmService.findAlarmDataByQueryForEntities(tenantId, customerId, query, orderedEntityIds); + } + + @Override + public ListenableFuture findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type) { + return alarmService.findLatestByOriginatorAndType(tenantId, originator, type); + } + + private void onAlarmUpdated(AlarmOperationResult result) { + wsCallBackExecutor.submit(() -> { + Alarm alarm = result.getAlarm(); + TenantId tenantId = result.getAlarm().getTenantId(); + for (EntityId entityId : result.getPropagatedEntitiesList()) { + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId); + if (currentPartitions.contains(tpi)) { + if (subscriptionManagerService.isPresent()) { + subscriptionManagerService.get().onAlarmUpdate(tenantId, entityId, alarm, TbCallback.EMPTY); + } else { + log.warn("Possible misconfiguration because subscriptionManagerService is null!"); + } + } else { + TransportProtos.ToCoreMsg toCoreMsg = TbSubscriptionUtils.toAlarmUpdateProto(tenantId, entityId, alarm); + clusterService.pushMsgToCore(tpi, entityId.getId(), toCoreMsg, null); + } + } + }); + } + + private void onAlarmDeleted(AlarmOperationResult result) { + wsCallBackExecutor.submit(() -> { + Alarm alarm = result.getAlarm(); + TenantId tenantId = result.getAlarm().getTenantId(); + for (EntityId entityId : result.getPropagatedEntitiesList()) { + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId); + if (currentPartitions.contains(tpi)) { + if (subscriptionManagerService.isPresent()) { + subscriptionManagerService.get().onAlarmDeleted(tenantId, entityId, alarm, TbCallback.EMPTY); + } else { + log.warn("Possible misconfiguration because subscriptionManagerService is null!"); + } + } else { + TransportProtos.ToCoreMsg toCoreMsg = TbSubscriptionUtils.toAlarmDeletedProto(tenantId, entityId, alarm); + clusterService.pushMsgToCore(tpi, entityId.getId(), toCoreMsg, null); + } + } + }); + } + + private class AlarmUpdateCallback implements FutureCallback { + @Override + public void onSuccess(@Nullable AlarmOperationResult result) { + onAlarmUpdated(result); + } + + @Override + public void onFailure(Throwable t) { + log.warn("Failed to update alarm", t); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java index 390c6f19b4..7a35e6b317 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java @@ -18,11 +18,13 @@ package org.thingsboard.server.service.telemetry; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.ApiUsageRecordKey; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; @@ -36,63 +38,69 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.timeseries.TimeseriesService; +import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.queue.discovery.PartitionChangeEvent; import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.usagestats.TbApiUsageClient; +import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.queue.TbClusterService; -import org.thingsboard.server.service.subscription.SubscriptionManagerService; import org.thingsboard.server.service.subscription.TbSubscriptionUtils; import javax.annotation.Nullable; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; +import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.function.Consumer; /** * Created by ashvayka on 27.03.18. */ @Service @Slf4j -public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptionService { - - private final Set currentPartitions = ConcurrentHashMap.newKeySet(); +public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionService implements TelemetrySubscriptionService { private final AttributesService attrService; private final TimeseriesService tsService; - private final TbClusterService clusterService; - private final PartitionService partitionService; - private Optional subscriptionManagerService; + private final EntityViewService entityViewService; + private final TbApiUsageClient apiUsageClient; + private final TbApiUsageStateService apiUsageStateService; private ExecutorService tsCallBackExecutor; - private ExecutorService wsCallBackExecutor; public DefaultTelemetrySubscriptionService(AttributesService attrService, TimeseriesService tsService, + EntityViewService entityViewService, TbClusterService clusterService, - PartitionService partitionService) { + PartitionService partitionService, + TbApiUsageClient apiUsageClient, + TbApiUsageStateService apiUsageStateService) { + super(clusterService, partitionService); this.attrService = attrService; this.tsService = tsService; - this.clusterService = clusterService; - this.partitionService = partitionService; - } - - @Autowired(required = false) - public void setSubscriptionManagerService(Optional subscriptionManagerService) { - this.subscriptionManagerService = subscriptionManagerService; + this.entityViewService = entityViewService; + this.apiUsageClient = apiUsageClient; + this.apiUsageStateService = apiUsageStateService; } @PostConstruct public void initExecutor() { + super.initExecutor(); tsCallBackExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("ts-service-ts-callback")); - wsCallBackExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("ts-service-ws-callback")); + } + + @Override + protected String getExecutorPrefix() { + return "ts"; } @PreDestroy @@ -100,44 +108,167 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio if (tsCallBackExecutor != null) { tsCallBackExecutor.shutdownNow(); } - if (wsCallBackExecutor != null) { - wsCallBackExecutor.shutdownNow(); - } + super.shutdownExecutor(); + } + + @Override + public void saveAndNotify(TenantId tenantId, EntityId entityId, List ts, FutureCallback callback) { + saveAndNotify(tenantId, entityId, ts, 0L, callback); } @Override - @EventListener(PartitionChangeEvent.class) - public void onApplicationEvent(PartitionChangeEvent partitionChangeEvent) { - if (ServiceType.TB_CORE.equals(partitionChangeEvent.getServiceType())) { - currentPartitions.clear(); - currentPartitions.addAll(partitionChangeEvent.getPartitions()); + public void saveAndNotify(TenantId tenantId, EntityId entityId, List ts, long ttl, FutureCallback callback) { + checkInternalEntity(entityId); + if (apiUsageStateService.getApiUsageState(tenantId).isDbStorageEnabled()) { + saveAndNotifyInternal(tenantId, entityId, ts, ttl, new FutureCallback() { + @Override + public void onSuccess(Integer result) { + if (result != null && result > 0) { + apiUsageClient.report(tenantId, ApiUsageRecordKey.STORAGE_DP_COUNT, result); + } + callback.onSuccess(null); + } + + @Override + public void onFailure(Throwable t) { + callback.onFailure(t); + } + }); + } else{ + callback.onFailure(new RuntimeException("DB storage writes are disabled due to API limits!")); } } @Override - public void saveAndNotify(TenantId tenantId, EntityId entityId, List ts, FutureCallback callback) { - saveAndNotify(tenantId, entityId, ts, 0L, callback); + public void saveAndNotifyInternal(TenantId tenantId, EntityId entityId, List ts, FutureCallback callback) { + saveAndNotifyInternal(tenantId, entityId, ts, 0L, callback); } @Override - public void saveAndNotify(TenantId tenantId, EntityId entityId, List ts, long ttl, FutureCallback callback) { - ListenableFuture> saveFuture = tsService.save(tenantId, entityId, ts, ttl); + public void saveAndNotifyInternal(TenantId tenantId, EntityId entityId, List ts, long ttl, FutureCallback callback) { + ListenableFuture saveFuture = tsService.save(tenantId, entityId, ts, ttl); addMainCallback(saveFuture, callback); addWsCallback(saveFuture, success -> onTimeSeriesUpdate(tenantId, entityId, ts)); + if (EntityType.DEVICE.equals(entityId.getEntityType()) || EntityType.ASSET.equals(entityId.getEntityType())) { + Futures.addCallback(this.entityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, entityId), + new FutureCallback>() { + @Override + public void onSuccess(@Nullable List result) { + if (result != null) { + Map> tsMap = new HashMap<>(); + for (TsKvEntry entry : ts) { + tsMap.computeIfAbsent(entry.getKey(), s -> new ArrayList<>()).add(entry); + } + for (EntityView entityView : result) { + List keys = entityView.getKeys() != null && entityView.getKeys().getTimeseries() != null ? + entityView.getKeys().getTimeseries() : new ArrayList<>(tsMap.keySet()); + List entityViewLatest = new ArrayList<>(); + long startTs = entityView.getStartTimeMs(); + long endTs = entityView.getEndTimeMs() == 0 ? Long.MAX_VALUE : entityView.getEndTimeMs(); + for (String key : keys) { + List entries = tsMap.get(key); + if (entries != null) { + Optional tsKvEntry = entries.stream() + .filter(entry -> entry.getTs() > startTs && entry.getTs() <= endTs) + .max(Comparator.comparingLong(TsKvEntry::getTs)); + tsKvEntry.ifPresent(entityViewLatest::add); + } + } + if (!entityViewLatest.isEmpty()) { + saveLatestAndNotify(tenantId, entityView.getId(), entityViewLatest, new FutureCallback() { + @Override + public void onSuccess(@Nullable Void tmp) { + } + + @Override + public void onFailure(Throwable t) { + } + }); + } + } + } + } + + @Override + public void onFailure(Throwable t) { + log.error("Error while finding entity views by tenantId and entityId", t); + } + }, MoreExecutors.directExecutor()); + } } @Override public void saveAndNotify(TenantId tenantId, EntityId entityId, String scope, List attributes, FutureCallback callback) { - saveAndNotify(tenantId, entityId, scope, attributes, callback, true); + saveAndNotify(tenantId, entityId, scope, attributes, true, callback); + } + + @Override + public void saveAndNotify(TenantId tenantId, EntityId entityId, String scope, List attributes, boolean notifyDevice, FutureCallback callback) { + checkInternalEntity(entityId); + saveAndNotifyInternal(tenantId, entityId, scope, attributes, notifyDevice, callback); } @Override - public void saveAndNotify(TenantId tenantId, EntityId entityId, String scope, List attributes, FutureCallback callback, boolean notifyDevice) { + public void saveAndNotifyInternal(TenantId tenantId, EntityId entityId, String scope, List attributes, boolean notifyDevice, FutureCallback callback) { ListenableFuture> saveFuture = attrService.save(tenantId, entityId, scope, attributes); - addMainCallback(saveFuture, callback); + addVoidCallback(saveFuture, callback); addWsCallback(saveFuture, success -> onAttributesUpdate(tenantId, entityId, scope, attributes, notifyDevice)); } + @Override + public void saveLatestAndNotify(TenantId tenantId, EntityId entityId, List ts, FutureCallback callback) { + checkInternalEntity(entityId); + saveLatestAndNotifyInternal(tenantId, entityId, ts, callback); + } + + @Override + public void saveLatestAndNotifyInternal(TenantId tenantId, EntityId entityId, List ts, FutureCallback callback) { + ListenableFuture> saveFuture = tsService.saveLatest(tenantId, entityId, ts); + addVoidCallback(saveFuture, callback); + addWsCallback(saveFuture, success -> onTimeSeriesUpdate(tenantId, entityId, ts)); + } + + @Override + public void deleteAndNotify(TenantId tenantId, EntityId entityId, String scope, List keys, FutureCallback callback) { + checkInternalEntity(entityId); + deleteAndNotifyInternal(tenantId, entityId, scope, keys, callback); + } + + @Override + public void deleteAndNotifyInternal(TenantId tenantId, EntityId entityId, String scope, List keys, FutureCallback callback) { + ListenableFuture> deleteFuture = attrService.removeAll(tenantId, entityId, scope, keys); + addVoidCallback(deleteFuture, callback); + addWsCallback(deleteFuture, success -> onAttributesDelete(tenantId, entityId, scope, keys)); + } + + @Override + public void deleteLatest(TenantId tenantId, EntityId entityId, List keys, FutureCallback callback) { + checkInternalEntity(entityId); + deleteLatestInternal(tenantId, entityId, keys, callback); + } + + @Override + public void deleteLatestInternal(TenantId tenantId, EntityId entityId, List keys, FutureCallback callback) { + ListenableFuture> deleteFuture = tsService.removeLatest(tenantId, entityId, keys); + addVoidCallback(deleteFuture, callback); + } + + @Override + public void deleteAllLatest(TenantId tenantId, EntityId entityId, FutureCallback> callback) { + ListenableFuture> deleteFuture = tsService.removeAllLatest(tenantId, entityId); + Futures.addCallback(deleteFuture, new FutureCallback>() { + @Override + public void onSuccess(@Nullable Collection result) { + callback.onSuccess(result); + } + + @Override + public void onFailure(Throwable t) { + callback.onFailure(t); + } + }, tsCallBackExecutor); + } + @Override public void saveAttrAndNotify(TenantId tenantId, EntityId entityId, String scope, String key, long value, FutureCallback callback) { saveAndNotify(tenantId, entityId, scope, Collections.singletonList(new BaseAttributeKvEntry(new LongDataEntry(key, value) @@ -166,7 +297,7 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId); if (currentPartitions.contains(tpi)) { if (subscriptionManagerService.isPresent()) { - subscriptionManagerService.get().onAttributesUpdate(tenantId, entityId, scope, attributes, TbCallback.EMPTY, notifyDevice); + subscriptionManagerService.get().onAttributesUpdate(tenantId, entityId, scope, attributes, notifyDevice, TbCallback.EMPTY); } else { log.warn("Possible misconfiguration because subscriptionManagerService is null!"); } @@ -176,6 +307,20 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio } } + private void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List keys) { + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId); + if (currentPartitions.contains(tpi)) { + if (subscriptionManagerService.isPresent()) { + subscriptionManagerService.get().onAttributesDelete(tenantId, entityId, scope, keys, TbCallback.EMPTY); + } else { + log.warn("Possible misconfiguration because subscriptionManagerService is null!"); + } + } else { + TransportProtos.ToCoreMsg toCoreMsg = TbSubscriptionUtils.toAttributesDeleteProto(tenantId, entityId, scope, keys); + clusterService.pushMsgToCore(tpi, entityId.getId(), toCoreMsg, null); + } + } + private void onTimeSeriesUpdate(TenantId tenantId, EntityId entityId, List ts) { TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId); if (currentPartitions.contains(tpi)) { @@ -190,10 +335,10 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio } } - private void addMainCallback(ListenableFuture> saveFuture, final FutureCallback callback) { - Futures.addCallback(saveFuture, new FutureCallback>() { + private void addVoidCallback(ListenableFuture saveFuture, final FutureCallback callback) { + Futures.addCallback(saveFuture, new FutureCallback() { @Override - public void onSuccess(@Nullable List result) { + public void onSuccess(@Nullable S result) { callback.onSuccess(null); } @@ -204,16 +349,24 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio }, tsCallBackExecutor); } - private void addWsCallback(ListenableFuture> saveFuture, Consumer callback) { - Futures.addCallback(saveFuture, new FutureCallback>() { + private void addMainCallback(ListenableFuture saveFuture, final FutureCallback callback) { + Futures.addCallback(saveFuture, new FutureCallback() { @Override - public void onSuccess(@Nullable List result) { - callback.accept(null); + public void onSuccess(@Nullable S result) { + callback.onSuccess(result); } @Override public void onFailure(Throwable t) { + callback.onFailure(t); } - }, wsCallBackExecutor); + }, tsCallBackExecutor); } + + private void checkInternalEntity(EntityId entityId) { + if (EntityType.API_USAGE_STATE.equals(entityId.getEntityType())) { + throw new RuntimeException("Can't update API Usage State!"); + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java index d6d9a6d070..16e6bca911 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java @@ -51,19 +51,27 @@ import org.thingsboard.server.service.security.ValidationResult; import org.thingsboard.server.service.security.ValidationResultCode; import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.subscription.TbEntityDataSubscriptionService; import org.thingsboard.server.service.subscription.TbLocalSubscriptionService; import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope; import org.thingsboard.server.service.subscription.TbAttributeSubscription; import org.thingsboard.server.service.subscription.TbTimeseriesSubscription; -import org.thingsboard.server.service.telemetry.cmd.AttributesSubscriptionCmd; -import org.thingsboard.server.service.telemetry.cmd.GetHistoryCmd; -import org.thingsboard.server.service.telemetry.cmd.SubscriptionCmd; -import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmd; +import org.thingsboard.server.service.telemetry.cmd.v1.AttributesSubscriptionCmd; +import org.thingsboard.server.service.telemetry.cmd.v1.GetHistoryCmd; +import org.thingsboard.server.service.telemetry.cmd.v1.SubscriptionCmd; +import org.thingsboard.server.service.telemetry.cmd.v1.TelemetryPluginCmd; import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper; -import org.thingsboard.server.service.telemetry.cmd.TimeseriesSubscriptionCmd; +import org.thingsboard.server.service.telemetry.cmd.v1.TimeseriesSubscriptionCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataUnsubscribeCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.DataUpdate; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUnsubscribeCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; +import org.thingsboard.server.service.telemetry.cmd.v2.UnsubscribeCmd; import org.thingsboard.server.service.telemetry.exception.UnauthorizedException; import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; -import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate; +import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; import javax.annotation.Nullable; import javax.annotation.PostConstruct; @@ -100,11 +108,15 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi private static final String FAILED_TO_FETCH_DATA = "Failed to fetch data!"; private static final String FAILED_TO_FETCH_ATTRIBUTES = "Failed to fetch attributes!"; private static final String SESSION_META_DATA_NOT_FOUND = "Session meta-data not found!"; + private static final String FAILED_TO_PARSE_WS_COMMAND = "Failed to parse websocket command!"; private final ConcurrentMap wsSessionsMap = new ConcurrentHashMap<>(); @Autowired - private TbLocalSubscriptionService subService; + private TbLocalSubscriptionService oldSubService; + + @Autowired + private TbEntityDataSubscriptionService entityDataSubService; @Autowired private TelemetryWebSocketMsgEndpoint msgEndpoint; @@ -121,6 +133,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi @Autowired private TbServiceInfoProvider serviceInfoProvider; + @Value("${server.ws.limits.max_subscriptions_per_tenant:0}") private int maxSubscriptionsPerTenant; @Value("${server.ws.limits.max_subscriptions_per_customer:0}") @@ -164,7 +177,8 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi break; case CLOSED: wsSessionsMap.remove(sessionId); - subService.cancelAllSessionSubscriptions(sessionId); + oldSubService.cancelAllSessionSubscriptions(sessionId); + entityDataSubService.cancelAllSessionSubscriptions(sessionId); processSessionClose(sessionRef); break; } @@ -196,19 +210,68 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi if (cmdsWrapper.getHistoryCmds() != null) { cmdsWrapper.getHistoryCmds().forEach(cmd -> handleWsHistoryCmd(sessionRef, cmd)); } + if (cmdsWrapper.getEntityDataCmds() != null) { + cmdsWrapper.getEntityDataCmds().forEach(cmd -> handleWsEntityDataCmd(sessionRef, cmd)); + } + if (cmdsWrapper.getAlarmDataCmds() != null) { + cmdsWrapper.getAlarmDataCmds().forEach(cmd -> handleWsAlarmDataCmd(sessionRef, cmd)); + } + if (cmdsWrapper.getEntityDataUnsubscribeCmds() != null) { + cmdsWrapper.getEntityDataUnsubscribeCmds().forEach(cmd -> handleWsDataUnsubscribeCmd(sessionRef, cmd)); + } + if (cmdsWrapper.getAlarmDataUnsubscribeCmds() != null) { + cmdsWrapper.getAlarmDataUnsubscribeCmds().forEach(cmd -> handleWsDataUnsubscribeCmd(sessionRef, cmd)); + } } } catch (IOException e) { log.warn("Failed to decode subscription cmd: {}", e.getMessage(), e); - SubscriptionUpdate update = new SubscriptionUpdate(UNKNOWN_SUBSCRIPTION_ID, SubscriptionErrorCode.INTERNAL_ERROR, SESSION_META_DATA_NOT_FOUND); - sendWsMsg(sessionRef, update); + sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(UNKNOWN_SUBSCRIPTION_ID, SubscriptionErrorCode.BAD_REQUEST, FAILED_TO_PARSE_WS_COMMAND)); + } + } + + private void handleWsEntityDataCmd(TelemetryWebSocketSessionRef sessionRef, EntityDataCmd cmd) { + String sessionId = sessionRef.getSessionId(); + log.debug("[{}] Processing: {}", sessionId, cmd); + + if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId) + && validateSubscriptionCmd(sessionRef, cmd)) { + entityDataSubService.handleCmd(sessionRef, cmd); + } + } + + private void handleWsAlarmDataCmd(TelemetryWebSocketSessionRef sessionRef, AlarmDataCmd cmd) { + String sessionId = sessionRef.getSessionId(); + log.debug("[{}] Processing: {}", sessionId, cmd); + + if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId) + && validateSubscriptionCmd(sessionRef, cmd)) { + entityDataSubService.handleCmd(sessionRef, cmd); + } + } + + private void handleWsDataUnsubscribeCmd(TelemetryWebSocketSessionRef sessionRef, UnsubscribeCmd cmd) { + String sessionId = sessionRef.getSessionId(); + log.debug("[{}] Processing: {}", sessionId, cmd); + + if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId)) { + entityDataSubService.cancelSubscription(sessionRef.getSessionId(), cmd); } } @Override - public void sendWsMsg(String sessionId, SubscriptionUpdate update) { + public void sendWsMsg(String sessionId, TelemetrySubscriptionUpdate update) { + sendWsMsg(sessionId, update.getSubscriptionId(), update); + } + + @Override + public void sendWsMsg(String sessionId, DataUpdate update) { + sendWsMsg(sessionId, update.getCmdId(), update); + } + + private void sendWsMsg(String sessionId, int cmdId, T update) { WsSessionMetaData md = wsSessionsMap.get(sessionId); if (md != null) { - sendWsMsg(md.getSessionRef(), update); + sendWsMsg(md.getSessionRef(), cmdId, update); } } @@ -339,7 +402,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi @Override public void onSuccess(List data) { List attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList()); - sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData)); + sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData)); Map subState = new HashMap<>(keys.size()); keys.forEach(key -> subState.put(key, 0L)); @@ -355,19 +418,21 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi .entityId(entityId) .allKeys(false) .keyStates(subState) - .scope(scope).build(); - subService.addSubscription(sub); + .scope(scope) + .updateConsumer(DefaultTelemetryWebSocketService.this::sendWsMsg) + .build(); + oldSubService.addSubscription(sub); } @Override public void onFailure(Throwable e) { log.error(FAILED_TO_FETCH_ATTRIBUTES, e); - SubscriptionUpdate update; + TelemetrySubscriptionUpdate update; if (e instanceof UnauthorizedException) { - update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED, + update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED, SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg()); } else { - update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, + update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, FAILED_TO_FETCH_ATTRIBUTES); } sendWsMsg(sessionRef, update); @@ -386,19 +451,19 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId); if (sessionMD == null) { log.warn("[{}] Session meta data not found. ", sessionId); - SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, SESSION_META_DATA_NOT_FOUND); sendWsMsg(sessionRef, update); return; } if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty() || cmd.getEntityType() == null || cmd.getEntityType().isEmpty()) { - SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, "Device id is empty!"); sendWsMsg(sessionRef, update); return; } if (cmd.getKeys() == null || cmd.getKeys().isEmpty()) { - SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, "Keys are empty!"); sendWsMsg(sessionRef, update); return; @@ -411,17 +476,17 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi FutureCallback> callback = new FutureCallback>() { @Override public void onSuccess(List data) { - sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data)); + sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data)); } @Override public void onFailure(Throwable e) { - SubscriptionUpdate update; + TelemetrySubscriptionUpdate update; if (UnauthorizedException.class.isInstance(e)) { - update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED, + update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED, SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg()); } else { - update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, + update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, FAILED_TO_FETCH_DATA); } sendWsMsg(sessionRef, update); @@ -437,7 +502,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi @Override public void onSuccess(List data) { List attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList()); - sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData)); + sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData)); Map subState = new HashMap<>(attributesData.size()); attributesData.forEach(v -> subState.put(v.getKey(), v.getTs())); @@ -452,14 +517,15 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi .entityId(entityId) .allKeys(true) .keyStates(subState) + .updateConsumer(DefaultTelemetryWebSocketService.this::sendWsMsg) .scope(scope).build(); - subService.addSubscription(sub); + oldSubService.addSubscription(sub); } @Override public void onFailure(Throwable e) { log.error(FAILED_TO_FETCH_ATTRIBUTES, e); - SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, FAILED_TO_FETCH_ATTRIBUTES); sendWsMsg(sessionRef, update); } @@ -522,7 +588,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi FutureCallback> callback = new FutureCallback>() { @Override public void onSuccess(List data) { - sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data)); + sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data)); Map subState = new HashMap<>(data.size()); data.forEach(v -> subState.put(v.getKey(), v.getTs())); @@ -532,19 +598,20 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi .subscriptionId(cmd.getCmdId()) .tenantId(sessionRef.getSecurityCtx().getTenantId()) .entityId(entityId) + .updateConsumer(DefaultTelemetryWebSocketService.this::sendWsMsg) .allKeys(true) .keyStates(subState).build(); - subService.addSubscription(sub); + oldSubService.addSubscription(sub); } @Override public void onFailure(Throwable e) { - SubscriptionUpdate update; + TelemetrySubscriptionUpdate update; if (UnauthorizedException.class.isInstance(e)) { - update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED, + update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED, SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg()); } else { - update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, + update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, FAILED_TO_FETCH_DATA); } sendWsMsg(sessionRef, update); @@ -558,7 +625,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi return new FutureCallback>() { @Override public void onSuccess(List data) { - sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data)); + sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data)); Map subState = new HashMap<>(keys.size()); keys.forEach(key -> subState.put(key, startTs)); data.forEach(v -> subState.put(v.getKey(), v.getTs())); @@ -569,9 +636,10 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi .subscriptionId(cmd.getCmdId()) .tenantId(sessionRef.getSecurityCtx().getTenantId()) .entityId(entityId) + .updateConsumer(DefaultTelemetryWebSocketService.this::sendWsMsg) .allKeys(false) .keyStates(subState).build(); - subService.addSubscription(sub); + oldSubService.addSubscription(sub); } @Override @@ -581,7 +649,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi } else { log.info(FAILED_TO_FETCH_DATA, e); } - SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, FAILED_TO_FETCH_DATA); sendWsMsg(sessionRef, update); } @@ -590,15 +658,45 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi private void unsubscribe(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) { if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) { - subService.cancelAllSessionSubscriptions(sessionId); + oldSubService.cancelAllSessionSubscriptions(sessionId); } else { - subService.cancelSubscription(sessionId, cmd.getCmdId()); + oldSubService.cancelSubscription(sessionId, cmd.getCmdId()); } } + private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, EntityDataCmd cmd) { + if (cmd.getCmdId() < 0) { + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, + "Cmd id is negative value!"); + sendWsMsg(sessionRef, update); + return false; + } else if (cmd.getQuery() == null && cmd.getLatestCmd() == null && cmd.getHistoryCmd() == null && cmd.getTsCmd() == null) { + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, + "Query is empty!"); + sendWsMsg(sessionRef, update); + return false; + } + return true; + } + + private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, AlarmDataCmd cmd) { + if (cmd.getCmdId() < 0) { + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, + "Cmd id is negative value!"); + sendWsMsg(sessionRef, update); + return false; + } else if (cmd.getQuery() == null) { + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, + "Query is empty!"); + sendWsMsg(sessionRef, update); + return false; + } + return true; + } + private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd) { if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) { - SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, "Device id is empty!"); sendWsMsg(sessionRef, update); return false; @@ -607,10 +705,14 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi } private boolean validateSessionMetadata(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) { + return validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId); + } + + private boolean validateSessionMetadata(TelemetryWebSocketSessionRef sessionRef, int cmdId, String sessionId) { WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId); if (sessionMD == null) { log.warn("[{}] Session meta data not found. ", sessionId); - SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmdId, SubscriptionErrorCode.INTERNAL_ERROR, SESSION_META_DATA_NOT_FOUND); sendWsMsg(sessionRef, update); return false; @@ -619,18 +721,30 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi } } - private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, SubscriptionUpdate update) { - executor.submit(() -> { - try { - msgEndpoint.send(sessionRef, update.getSubscriptionId(), jsonMapper.writeValueAsString(update)); - } catch (JsonProcessingException e) { - log.warn("[{}] Failed to encode reply: {}", sessionRef.getSessionId(), update, e); - } catch (IOException e) { - log.warn("[{}] Failed to send reply: {}", sessionRef.getSessionId(), update, e); - } - }); + private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, EntityDataUpdate update) { + sendWsMsg(sessionRef, update.getCmdId(), update); + } + + private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, TelemetrySubscriptionUpdate update) { + sendWsMsg(sessionRef, update.getSubscriptionId(), update); } + private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, int cmdId, Object update) { + try { + String msg = jsonMapper.writeValueAsString(update); + executor.submit(() -> { + try { + msgEndpoint.send(sessionRef, cmdId, msg); + } catch (IOException e) { + log.warn("[{}] Failed to send reply: {}", sessionRef.getSessionId(), update, e); + } + }); + } catch (JsonProcessingException e) { + log.warn("[{}] Failed to encode reply: {}", sessionRef.getSessionId(), update, e); + } + } + + private static Optional> getKeys(TelemetryPluginCmd cmd) { if (!StringUtils.isEmpty(cmd.getKeys())) { Set keys = new HashSet<>(); @@ -740,7 +854,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi } - private static Aggregation getAggregation(String agg) { + public static Aggregation getAggregation(String agg) { return StringUtils.isEmpty(agg) ? DEFAULT_AGGREGATION : Aggregation.valueOf(agg); } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/InternalTelemetryService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/InternalTelemetryService.java new file mode 100644 index 0000000000..1ca2cdeacb --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/InternalTelemetryService.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry; + +import com.google.common.util.concurrent.FutureCallback; +import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; + +import java.util.List; + +/** + * Created by ashvayka on 27.03.18. + */ +public interface InternalTelemetryService extends RuleEngineTelemetryService { + + void saveAndNotifyInternal(TenantId tenantId, EntityId entityId, List ts, FutureCallback callback); + + void saveAndNotifyInternal(TenantId tenantId, EntityId entityId, List ts, long ttl, FutureCallback callback); + + void saveAndNotifyInternal(TenantId tenantId, EntityId entityId, String scope, List attributes, boolean notifyDevice, FutureCallback callback); + + void saveLatestAndNotifyInternal(TenantId tenantId, EntityId entityId, List ts, FutureCallback callback); + + void deleteAndNotifyInternal(TenantId tenantId, EntityId entityId, String scope, List keys, FutureCallback callback); + + void deleteLatestInternal(TenantId tenantId, EntityId entityId, List keys, FutureCallback callback); + + + +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetrySubscriptionService.java index d652a3f1c5..a88a378cd7 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetrySubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetrySubscriptionService.java @@ -22,6 +22,6 @@ import org.thingsboard.server.queue.discovery.PartitionChangeEvent; /** * Created by ashvayka on 27.03.18. */ -public interface TelemetrySubscriptionService extends RuleEngineTelemetryService, ApplicationListener { +public interface TelemetrySubscriptionService extends InternalTelemetryService, ApplicationListener { } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java index f4f7274ae9..45c061c40a 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java @@ -15,7 +15,8 @@ */ package org.thingsboard.server.service.telemetry; -import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate; +import org.thingsboard.server.service.telemetry.cmd.v2.DataUpdate; +import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; /** * Created by ashvayka on 27.03.18. @@ -26,5 +27,8 @@ public interface TelemetryWebSocketService { void handleWebSocketMsg(TelemetryWebSocketSessionRef sessionRef, String msg); - void sendWsMsg(String sessionId, SubscriptionUpdate update); + void sendWsMsg(String sessionId, TelemetrySubscriptionUpdate update); + + void sendWsMsg(String sessionId, DataUpdate update); + } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketSessionRef.java b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketSessionRef.java index cd70f5af4f..0c2a7cedbb 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketSessionRef.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketSessionRef.java @@ -20,6 +20,7 @@ import org.thingsboard.server.service.security.model.SecurityUser; import java.net.InetSocketAddress; import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; /** * Created by ashvayka on 27.03.18. @@ -36,12 +37,15 @@ public class TelemetryWebSocketSessionRef { private final InetSocketAddress localAddress; @Getter private final InetSocketAddress remoteAddress; + @Getter + private final AtomicInteger sessionSubIdSeq; public TelemetryWebSocketSessionRef(String sessionId, SecurityUser securityCtx, InetSocketAddress localAddress, InetSocketAddress remoteAddress) { this.sessionId = sessionId; this.securityCtx = securityCtx; this.localAddress = localAddress; this.remoteAddress = remoteAddress; + this.sessionSubIdSeq = new AtomicInteger(); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/AttributesSubscriptionCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/AttributesSubscriptionCmd.java deleted file mode 100644 index 198ba31765..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/AttributesSubscriptionCmd.java +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd; - -import lombok.NoArgsConstructor; -import org.thingsboard.server.service.telemetry.TelemetryFeature; - -/** - * @author Andrew Shvayka - */ -@NoArgsConstructor -public class AttributesSubscriptionCmd extends SubscriptionCmd { - - @Override - public TelemetryFeature getType() { - return TelemetryFeature.ATTRIBUTES; - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/GetHistoryCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/GetHistoryCmd.java deleted file mode 100644 index 6a410b53ce..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/GetHistoryCmd.java +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * @author Andrew Shvayka - */ -@NoArgsConstructor -@AllArgsConstructor -@Data -public class GetHistoryCmd implements TelemetryPluginCmd { - - private int cmdId; - private String entityType; - private String entityId; - private String keys; - private long startTs; - private long endTs; - private long interval; - private int limit; - private String agg; - -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/SubscriptionCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/SubscriptionCmd.java deleted file mode 100644 index 4ff4750178..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/SubscriptionCmd.java +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.thingsboard.server.service.telemetry.TelemetryFeature; - -@NoArgsConstructor -@AllArgsConstructor -@Data -public abstract class SubscriptionCmd implements TelemetryPluginCmd { - - private int cmdId; - private String entityType; - private String entityId; - private String keys; - private String scope; - private boolean unsubscribe; - - public abstract TelemetryFeature getType(); - - @Override - public String toString() { - return "SubscriptionCmd [entityType=" + entityType + ", entityId=" + entityId + ", tags=" + keys + ", unsubscribe=" + unsubscribe + "]"; - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmd.java deleted file mode 100644 index af2d1af3bb..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmd.java +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd; - -/** - * @author Andrew Shvayka - */ -public interface TelemetryPluginCmd { - - int getCmdId(); - - void setCmdId(int cmdId); - - String getKeys(); - -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmdsWrapper.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmdsWrapper.java index ae7f27fc9f..06842ea7f4 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmdsWrapper.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmdsWrapper.java @@ -15,11 +15,21 @@ */ package org.thingsboard.server.service.telemetry.cmd; +import lombok.Data; +import org.thingsboard.server.service.telemetry.cmd.v1.AttributesSubscriptionCmd; +import org.thingsboard.server.service.telemetry.cmd.v1.GetHistoryCmd; +import org.thingsboard.server.service.telemetry.cmd.v1.TimeseriesSubscriptionCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataUnsubscribeCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUnsubscribeCmd; + import java.util.List; /** * @author Andrew Shvayka */ +@Data public class TelemetryPluginCmdsWrapper { private List attrSubCmds; @@ -28,31 +38,12 @@ public class TelemetryPluginCmdsWrapper { private List historyCmds; - public TelemetryPluginCmdsWrapper() { - super(); - } - - public List getAttrSubCmds() { - return attrSubCmds; - } - - public void setAttrSubCmds(List attrSubCmds) { - this.attrSubCmds = attrSubCmds; - } + private List entityDataCmds; - public List getTsSubCmds() { - return tsSubCmds; - } + private List entityDataUnsubscribeCmds; - public void setTsSubCmds(List tsSubCmds) { - this.tsSubCmds = tsSubCmds; - } + private List alarmDataCmds; - public List getHistoryCmds() { - return historyCmds; - } + private List alarmDataUnsubscribeCmds; - public void setHistoryCmds(List historyCmds) { - this.historyCmds = historyCmds; - } } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TimeseriesSubscriptionCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TimeseriesSubscriptionCmd.java deleted file mode 100644 index f85e08880b..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TimeseriesSubscriptionCmd.java +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.cmd; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.thingsboard.server.service.telemetry.TelemetryFeature; - -/** - * @author Andrew Shvayka - */ -@NoArgsConstructor -@AllArgsConstructor -@Data -public class TimeseriesSubscriptionCmd extends SubscriptionCmd { - - private long startTs; - private long timeWindow; - private long interval; - private int limit; - private String agg; - - @Override - public TelemetryFeature getType() { - return TelemetryFeature.TIMESERIES; - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/AttributesSubscriptionCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/AttributesSubscriptionCmd.java new file mode 100644 index 0000000000..ef911e337e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/AttributesSubscriptionCmd.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.cmd.v1; + +import lombok.NoArgsConstructor; +import org.thingsboard.server.service.telemetry.TelemetryFeature; + +/** + * @author Andrew Shvayka + */ +@NoArgsConstructor +public class AttributesSubscriptionCmd extends SubscriptionCmd { + + @Override + public TelemetryFeature getType() { + return TelemetryFeature.ATTRIBUTES; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/GetHistoryCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/GetHistoryCmd.java new file mode 100644 index 0000000000..60799ae93c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/GetHistoryCmd.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.cmd.v1; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author Andrew Shvayka + */ +@NoArgsConstructor +@AllArgsConstructor +@Data +public class GetHistoryCmd implements TelemetryPluginCmd { + + private int cmdId; + private String entityType; + private String entityId; + private String keys; + private long startTs; + private long endTs; + private long interval; + private int limit; + private String agg; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/SubscriptionCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/SubscriptionCmd.java new file mode 100644 index 0000000000..c8da00f088 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/SubscriptionCmd.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.cmd.v1; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.service.telemetry.TelemetryFeature; + +@NoArgsConstructor +@AllArgsConstructor +@Data +public abstract class SubscriptionCmd implements TelemetryPluginCmd { + + private int cmdId; + private String entityType; + private String entityId; + private String keys; + private String scope; + private boolean unsubscribe; + + public abstract TelemetryFeature getType(); + + @Override + public String toString() { + return "SubscriptionCmd [entityType=" + entityType + ", entityId=" + entityId + ", tags=" + keys + ", unsubscribe=" + unsubscribe + "]"; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/TelemetryPluginCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/TelemetryPluginCmd.java new file mode 100644 index 0000000000..94968fa1a1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/TelemetryPluginCmd.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.cmd.v1; + +/** + * @author Andrew Shvayka + */ +public interface TelemetryPluginCmd { + + int getCmdId(); + + void setCmdId(int cmdId); + + String getKeys(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/TimeseriesSubscriptionCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/TimeseriesSubscriptionCmd.java new file mode 100644 index 0000000000..7237a7645e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/TimeseriesSubscriptionCmd.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.cmd.v1; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.service.telemetry.TelemetryFeature; + +/** + * @author Andrew Shvayka + */ +@NoArgsConstructor +@AllArgsConstructor +@Data +public class TimeseriesSubscriptionCmd extends SubscriptionCmd { + + private long startTs; + private long timeWindow; + private long interval; + private int limit; + private String agg; + + @Override + public TelemetryFeature getType() { + return TelemetryFeature.TIMESERIES; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataCmd.java new file mode 100644 index 0000000000..3e9c4bd6f6 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataCmd.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.cmd.v2; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import org.thingsboard.server.common.data.query.AlarmDataQuery; + +public class AlarmDataCmd extends DataCmd { + + @Getter + private final AlarmDataQuery query; + + @JsonCreator + public AlarmDataCmd(@JsonProperty("cmdId") int cmdId, @JsonProperty("query") AlarmDataQuery query) { + super(cmdId); + this.query = query; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUnsubscribeCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUnsubscribeCmd.java new file mode 100644 index 0000000000..b886cff349 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUnsubscribeCmd.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.cmd.v2; + +import lombok.Data; + +@Data +public class AlarmDataUnsubscribeCmd implements UnsubscribeCmd { + + private final int cmdId; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUpdate.java new file mode 100644 index 0000000000..7a55d4bf75 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUpdate.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.cmd.v2; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AlarmData; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; + +import java.util.List; + +public class AlarmDataUpdate extends DataUpdate { + + @Getter + private long allowedEntities; + @Getter + private long totalEntities; + + public AlarmDataUpdate(int cmdId, PageData data, List update, long allowedEntities, long totalEntities) { + super(cmdId, data, update, SubscriptionErrorCode.NO_ERROR.getCode(), null); + this.allowedEntities = allowedEntities; + this.totalEntities = totalEntities; + } + + public AlarmDataUpdate(int cmdId, int errorCode, String errorMsg) { + super(cmdId, null, null, errorCode, errorMsg); + } + + @Override + public DataUpdateType getDataUpdateType() { + return DataUpdateType.ALARM_DATA; + } + + @JsonCreator + public AlarmDataUpdate(@JsonProperty("cmdId") int cmdId, + @JsonProperty("data") PageData data, + @JsonProperty("update") List update, + @JsonProperty("errorCode") int errorCode, + @JsonProperty("errorMsg") String errorMsg, + @JsonProperty("allowedEntities") long allowedEntities, + @JsonProperty("totalEntities") long totalEntities) { + super(cmdId, data, update, errorCode, errorMsg); + this.allowedEntities = allowedEntities; + this.totalEntities = totalEntities; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataCmd.java new file mode 100644 index 0000000000..d06e9f107d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataCmd.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.cmd.v2; + +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Data +public class DataCmd { + + @Getter + private final int cmdId; + + public DataCmd(int cmdId) { + this.cmdId = cmdId; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdate.java new file mode 100644 index 0000000000..c841e220fb --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdate.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.cmd.v2; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; + +import java.util.List; + +@Data +@AllArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class DataUpdate { + + private final int cmdId; + private final PageData data; + private final List update; + private final int errorCode; + private final String errorMsg; + + public DataUpdate(int cmdId, PageData data, List update) { + this(cmdId, data, update, SubscriptionErrorCode.NO_ERROR.getCode(), null); + } + + public DataUpdate(int cmdId, int errorCode, String errorMsg) { + this(cmdId, null, null, errorCode, errorMsg); + } + + public abstract DataUpdateType getDataUpdateType(); +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdateType.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdateType.java new file mode 100644 index 0000000000..d43ab5ab99 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdateType.java @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.cmd.v2; + +public enum DataUpdateType { + ENTITY_DATA, + ALARM_DATA +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataCmd.java new file mode 100644 index 0000000000..7b1f6fb60c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataCmd.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.cmd.v2; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import org.thingsboard.server.common.data.query.EntityDataQuery; + +public class EntityDataCmd extends DataCmd { + + @Getter + private final EntityDataQuery query; + @Getter + private final EntityHistoryCmd historyCmd; + @Getter + private final LatestValueCmd latestCmd; + @Getter + private final TimeSeriesCmd tsCmd; + + @JsonCreator + public EntityDataCmd(@JsonProperty("cmdId") int cmdId, + @JsonProperty("query") EntityDataQuery query, + @JsonProperty("historyCmd") EntityHistoryCmd historyCmd, + @JsonProperty("latestCmd") LatestValueCmd latestCmd, + @JsonProperty("tsCmd") TimeSeriesCmd tsCmd) { + super(cmdId); + this.query = query; + this.historyCmd = historyCmd; + this.latestCmd = latestCmd; + this.tsCmd = tsCmd; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUnsubscribeCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUnsubscribeCmd.java new file mode 100644 index 0000000000..f75f622db1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUnsubscribeCmd.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.cmd.v2; + +import lombok.Data; + +@Data +public class EntityDataUnsubscribeCmd implements UnsubscribeCmd { + + private final int cmdId; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUpdate.java new file mode 100644 index 0000000000..1467090874 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUpdate.java @@ -0,0 +1,57 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.cmd.v2; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; + +import java.util.List; + + +public class EntityDataUpdate extends DataUpdate { + + @Getter + private long allowedEntities; + + public EntityDataUpdate(int cmdId, PageData data, List update, long allowedEntities) { + super(cmdId, data, update, SubscriptionErrorCode.NO_ERROR.getCode(), null); + this.allowedEntities = allowedEntities; + } + + public EntityDataUpdate(int cmdId, int errorCode, String errorMsg) { + super(cmdId, null, null, errorCode, errorMsg); + } + + @Override + public DataUpdateType getDataUpdateType() { + return DataUpdateType.ENTITY_DATA; + } + + @JsonCreator + public EntityDataUpdate(@JsonProperty("cmdId") int cmdId, + @JsonProperty("data") PageData data, + @JsonProperty("update") List update, + @JsonProperty("errorCode") int errorCode, + @JsonProperty("errorMsg") String errorMsg) { + super(cmdId, data, update, errorCode, errorMsg); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityHistoryCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityHistoryCmd.java new file mode 100644 index 0000000000..b78cef98d7 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityHistoryCmd.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.cmd.v2; + +import lombok.Data; +import org.thingsboard.server.common.data.kv.Aggregation; + +import java.util.List; + +@Data +public class EntityHistoryCmd implements GetTsCmd { + + private List keys; + private long startTs; + private long endTs; + private long interval; + private int limit; + private Aggregation agg; + private boolean fetchLatestPreviousPoint; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/GetTsCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/GetTsCmd.java new file mode 100644 index 0000000000..25cc1a8e0f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/GetTsCmd.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.cmd.v2; + +import org.thingsboard.server.common.data.kv.Aggregation; + +import java.util.List; + +public interface GetTsCmd { + + long getStartTs(); + + long getEndTs(); + + List getKeys(); + + long getInterval(); + + int getLimit(); + + Aggregation getAgg(); + + boolean isFetchLatestPreviousPoint(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/LatestValueCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/LatestValueCmd.java new file mode 100644 index 0000000000..5e7a65b5c6 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/LatestValueCmd.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.cmd.v2; + +import lombok.Data; +import org.thingsboard.server.common.data.query.EntityKey; + +import java.util.List; + +@Data +public class LatestValueCmd { + + private List keys; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/TimeSeriesCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/TimeSeriesCmd.java new file mode 100644 index 0000000000..8762ee3438 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/TimeSeriesCmd.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.cmd.v2; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import org.thingsboard.server.common.data.kv.Aggregation; + +import java.util.List; + +@Data +public class TimeSeriesCmd implements GetTsCmd { + + private List keys; + private long startTs; + private long timeWindow; + private long interval; + private int limit; + private Aggregation agg; + private boolean fetchLatestPreviousPoint; + + @JsonIgnore + @Override + public long getEndTs() { + return startTs + timeWindow; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/UnsubscribeCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/UnsubscribeCmd.java new file mode 100644 index 0000000000..a2f5bfe770 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/UnsubscribeCmd.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.cmd.v2; + +import lombok.Data; + +public interface UnsubscribeCmd { + + int getCmdId(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/AlarmSubscriptionUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/sub/AlarmSubscriptionUpdate.java new file mode 100644 index 0000000000..87a94dae96 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/sub/AlarmSubscriptionUpdate.java @@ -0,0 +1,70 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.sub; + +import lombok.Getter; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.query.AlarmData; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Collectors; + +public class AlarmSubscriptionUpdate { + + @Getter + private int subscriptionId; + @Getter + private int errorCode; + @Getter + private String errorMsg; + @Getter + private Alarm alarm; + @Getter + private boolean alarmDeleted; + + public AlarmSubscriptionUpdate(int subscriptionId, Alarm alarm) { + this(subscriptionId, alarm, false); + } + + public AlarmSubscriptionUpdate(int subscriptionId, Alarm alarm, boolean alarmDeleted) { + super(); + this.subscriptionId = subscriptionId; + this.alarm = alarm; + this.alarmDeleted = alarmDeleted; + } + + public AlarmSubscriptionUpdate(int subscriptionId, SubscriptionErrorCode errorCode) { + this(subscriptionId, errorCode, null); + } + + public AlarmSubscriptionUpdate(int subscriptionId, SubscriptionErrorCode errorCode, String errorMsg) { + super(); + this.subscriptionId = subscriptionId; + this.errorCode = errorCode.getCode(); + this.errorMsg = errorMsg != null ? errorMsg : errorCode.getDefaultMsg(); + } + + @Override + public String toString() { + return "AlarmUpdate [subscriptionId=" + subscriptionId + ", errorCode=" + errorCode + ", errorMsg=" + errorMsg + ", alarm=" + + alarm + "]"; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionUpdate.java deleted file mode 100644 index 992dc26af9..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionUpdate.java +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.telemetry.sub; - -import org.thingsboard.server.common.data.kv.TsKvEntry; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; -import java.util.stream.Collectors; - -public class SubscriptionUpdate { - - private int subscriptionId; - private int errorCode; - private String errorMsg; - private Map> data; - - public SubscriptionUpdate(int subscriptionId, List data) { - super(); - this.subscriptionId = subscriptionId; - this.data = new TreeMap<>(); - if (data != null) { - for (TsKvEntry tsEntry : data) { - List values = this.data.computeIfAbsent(tsEntry.getKey(), k -> new ArrayList<>()); - Object[] value = new Object[2]; - value[0] = tsEntry.getTs(); - value[1] = tsEntry.getValueAsString(); - values.add(value); - } - } - } - - public SubscriptionUpdate(int subscriptionId, Map> data) { - super(); - this.subscriptionId = subscriptionId; - this.data = data; - } - - public SubscriptionUpdate(int subscriptionId, SubscriptionErrorCode errorCode) { - this(subscriptionId, errorCode, null); - } - - public SubscriptionUpdate(int subscriptionId, SubscriptionErrorCode errorCode, String errorMsg) { - super(); - this.subscriptionId = subscriptionId; - this.errorCode = errorCode.getCode(); - this.errorMsg = errorMsg != null ? errorMsg : errorCode.getDefaultMsg(); - } - - public int getSubscriptionId() { - return subscriptionId; - } - - public Map> getData() { - return data; - } - - public Map getLatestValues() { - if (data == null) { - return Collections.emptyMap(); - } else { - return data.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> { - List data = e.getValue(); - Object[] latest = (Object[]) data.get(data.size() - 1); - return (long) latest[0]; - })); - } - } - - public int getErrorCode() { - return errorCode; - } - - public String getErrorMsg() { - return errorMsg; - } - - @Override - public String toString() { - return "SubscriptionUpdate [subscriptionId=" + subscriptionId + ", errorCode=" + errorCode + ", errorMsg=" + errorMsg + ", data=" - + data + "]"; - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/TelemetrySubscriptionUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/sub/TelemetrySubscriptionUpdate.java new file mode 100644 index 0000000000..bd5359f642 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/sub/TelemetrySubscriptionUpdate.java @@ -0,0 +1,99 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.sub; + +import org.thingsboard.server.common.data.kv.TsKvEntry; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Collectors; + +public class TelemetrySubscriptionUpdate { + + private int subscriptionId; + private int errorCode; + private String errorMsg; + private Map> data; + + public TelemetrySubscriptionUpdate(int subscriptionId, List data) { + super(); + this.subscriptionId = subscriptionId; + this.data = new TreeMap<>(); + if (data != null) { + for (TsKvEntry tsEntry : data) { + List values = this.data.computeIfAbsent(tsEntry.getKey(), k -> new ArrayList<>()); + Object[] value = new Object[2]; + value[0] = tsEntry.getTs(); + value[1] = tsEntry.getValueAsString(); + values.add(value); + } + } + } + + public TelemetrySubscriptionUpdate(int subscriptionId, Map> data) { + super(); + this.subscriptionId = subscriptionId; + this.data = data; + } + + public TelemetrySubscriptionUpdate(int subscriptionId, SubscriptionErrorCode errorCode) { + this(subscriptionId, errorCode, null); + } + + public TelemetrySubscriptionUpdate(int subscriptionId, SubscriptionErrorCode errorCode, String errorMsg) { + super(); + this.subscriptionId = subscriptionId; + this.errorCode = errorCode.getCode(); + this.errorMsg = errorMsg != null ? errorMsg : errorCode.getDefaultMsg(); + } + + public int getSubscriptionId() { + return subscriptionId; + } + + public Map> getData() { + return data; + } + + public Map getLatestValues() { + if (data == null) { + return Collections.emptyMap(); + } else { + return data.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> { + List data = e.getValue(); + Object[] latest = (Object[]) data.get(data.size() - 1); + return (long) latest[0]; + })); + } + } + + public int getErrorCode() { + return errorCode; + } + + public String getErrorMsg() { + return errorMsg; + } + + @Override + public String toString() { + return "TsSubscriptionUpdate [subscriptionId=" + subscriptionId + ", errorCode=" + errorCode + ", errorMsg=" + errorMsg + ", data=" + + data + "]"; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index 3aa14ed639..3e9e413ceb 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -21,31 +21,45 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import com.google.protobuf.ByteString; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; +import org.thingsboard.server.common.data.device.credentials.ProvisionDeviceCredentialsData; +import org.thingsboard.server.common.data.device.profile.ProvisionDeviceProfileCredentials; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceProvisionService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.device.provision.ProvisionRequest; +import org.thingsboard.server.dao.device.provision.ProvisionResponse; import org.thingsboard.server.dao.relation.RelationService; -import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto; import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayResponseMsg; -import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoRequestMsg; -import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoResponseMsg; +import org.thingsboard.server.gen.transport.TransportProtos.GetEntityProfileRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.GetEntityProfileResponseMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg; @@ -53,11 +67,18 @@ import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenR import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.dao.device.provision.ProvisionFailedException; +import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.executors.DbCallbackExecutorService; +import org.thingsboard.server.service.profile.TbDeviceProfileCache; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.service.queue.TbClusterService; import org.thingsboard.server.service.state.DeviceStateService; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** @@ -70,45 +91,66 @@ public class DefaultTransportApiService implements TransportApiService { private static final ObjectMapper mapper = new ObjectMapper(); - //TODO: Constructor dependencies; - @Autowired - private TenantService tenantService; + private final TbDeviceProfileCache deviceProfileCache; + private final TbTenantProfileCache tenantProfileCache; + private final TbApiUsageStateService apiUsageStateService; + private final DeviceService deviceService; + private final RelationService relationService; + private final DeviceCredentialsService deviceCredentialsService; + private final DeviceStateService deviceStateService; + private final DbCallbackExecutorService dbCallbackExecutorService; + private final TbClusterService tbClusterService; + private final DataDecodingEncodingService dataDecodingEncodingService; + private final DeviceProvisionService deviceProvisionService; - @Autowired - private DeviceService deviceService; + private final ConcurrentMap deviceCreationLocks = new ConcurrentHashMap<>(); - @Autowired - private RelationService relationService; - - @Autowired - private DeviceCredentialsService deviceCredentialsService; - - @Autowired - private DeviceStateService deviceStateService; - - @Autowired - private DbCallbackExecutorService dbCallbackExecutorService; - - @Autowired - protected TbClusterService tbClusterService; - - private ReentrantLock deviceCreationLock = new ReentrantLock(); + public DefaultTransportApiService(TbDeviceProfileCache deviceProfileCache, + TbTenantProfileCache tenantProfileCache, TbApiUsageStateService apiUsageStateService, DeviceService deviceService, + RelationService relationService, DeviceCredentialsService deviceCredentialsService, + DeviceStateService deviceStateService, DbCallbackExecutorService dbCallbackExecutorService, + TbClusterService tbClusterService, DataDecodingEncodingService dataDecodingEncodingService, + DeviceProvisionService deviceProvisionService) { + this.deviceProfileCache = deviceProfileCache; + this.tenantProfileCache = tenantProfileCache; + this.apiUsageStateService = apiUsageStateService; + this.deviceService = deviceService; + this.relationService = relationService; + this.deviceCredentialsService = deviceCredentialsService; + this.deviceStateService = deviceStateService; + this.dbCallbackExecutorService = dbCallbackExecutorService; + this.tbClusterService = tbClusterService; + this.dataDecodingEncodingService = dataDecodingEncodingService; + this.deviceProvisionService = deviceProvisionService; + } @Override public ListenableFuture> handle(TbProtoQueueMsg tbProtoQueueMsg) { TransportApiRequestMsg transportApiRequestMsg = tbProtoQueueMsg.getValue(); if (transportApiRequestMsg.hasValidateTokenRequestMsg()) { ValidateDeviceTokenRequestMsg msg = transportApiRequestMsg.getValidateTokenRequestMsg(); - return Futures.transform(validateCredentials(msg.getToken(), DeviceCredentialsType.ACCESS_TOKEN), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); + return Futures.transform(validateCredentials(msg.getToken(), DeviceCredentialsType.ACCESS_TOKEN), + value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); + } else if (transportApiRequestMsg.hasValidateBasicMqttCredRequestMsg()) { + TransportProtos.ValidateBasicMqttCredRequestMsg msg = transportApiRequestMsg.getValidateBasicMqttCredRequestMsg(); + return Futures.transform(validateCredentials(msg), + value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); } else if (transportApiRequestMsg.hasValidateX509CertRequestMsg()) { ValidateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateX509CertRequestMsg(); - return Futures.transform(validateCredentials(msg.getHash(), DeviceCredentialsType.X509_CERTIFICATE), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); + return Futures.transform(validateCredentials(msg.getHash(), DeviceCredentialsType.X509_CERTIFICATE), + value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); } else if (transportApiRequestMsg.hasGetOrCreateDeviceRequestMsg()) { - return Futures.transform(handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg()), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); - } else if (transportApiRequestMsg.hasGetTenantRoutingInfoRequestMsg()) { - return Futures.transform(handle(transportApiRequestMsg.getGetTenantRoutingInfoRequestMsg()), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); + return Futures.transform(handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg()), + value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); + } else if (transportApiRequestMsg.hasEntityProfileRequestMsg()) { + return Futures.transform(handle(transportApiRequestMsg.getEntityProfileRequestMsg()), + value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); + } else if (transportApiRequestMsg.hasProvisionDeviceRequestMsg()) { + return Futures.transform(handle(transportApiRequestMsg.getProvisionDeviceRequestMsg()), + value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); } - return Futures.transform(getEmptyTransportApiResponseFuture(), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); + return Futures.transform(getEmptyTransportApiResponseFuture(), + value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); } private ListenableFuture validateCredentials(String credentialsId, DeviceCredentialsType credentialsType) { @@ -121,10 +163,67 @@ public class DefaultTransportApiService implements TransportApiService { } } + private ListenableFuture validateCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg mqtt) { + DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(mqtt.getUserName()); + if (credentials != null) { + if (credentials.getCredentialsType() == DeviceCredentialsType.ACCESS_TOKEN) { + return getDeviceInfo(credentials.getDeviceId(), credentials); + } else if (credentials.getCredentialsType() == DeviceCredentialsType.MQTT_BASIC) { + if (!checkMqttCredentials(mqtt, credentials)) { + credentials = null; + } + } + } + if (credentials == null) { + credentials = checkMqttCredentials(mqtt, EncryptionUtil.getSha3Hash("|", mqtt.getClientId(), mqtt.getUserName())); + if (credentials == null) { + credentials = checkMqttCredentials(mqtt, EncryptionUtil.getSha3Hash(mqtt.getClientId())); + } + } + if (credentials != null) { + return getDeviceInfo(credentials.getDeviceId(), credentials); + } else { + return getEmptyTransportApiResponseFuture(); + } + } + + private DeviceCredentials checkMqttCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg clientCred, String credId) { + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(credId); + if (deviceCredentials != null && deviceCredentials.getCredentialsType() == DeviceCredentialsType.MQTT_BASIC) { + if (!checkMqttCredentials(clientCred, deviceCredentials)) { + return null; + } else { + return deviceCredentials; + } + } + return null; + } + + private boolean checkMqttCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg clientCred, DeviceCredentials deviceCredentials) { + BasicMqttCredentials dbCred = JacksonUtil.fromString(deviceCredentials.getCredentialsValue(), BasicMqttCredentials.class); + if (!StringUtils.isEmpty(dbCred.getClientId()) && !dbCred.getClientId().equals(clientCred.getClientId())) { + return false; + } + if (!StringUtils.isEmpty(dbCred.getUserName()) && !dbCred.getUserName().equals(clientCred.getUserName())) { + return false; + } + if (!StringUtils.isEmpty(dbCred.getPassword())) { + if (StringUtils.isEmpty(clientCred.getPassword())) { + return false; + } else { + if (!dbCred.getPassword().equals(clientCred.getPassword())) { + return false; + } + } + } + return true; + } + private ListenableFuture handle(GetOrCreateDeviceFromGatewayRequestMsg requestMsg) { DeviceId gatewayId = new DeviceId(new UUID(requestMsg.getGatewayIdMSB(), requestMsg.getGatewayIdLSB())); ListenableFuture gatewayFuture = deviceService.findDeviceByIdAsync(TenantId.SYS_TENANT_ID, gatewayId); return Futures.transform(gatewayFuture, gateway -> { + Lock deviceCreationLock = deviceCreationLocks.computeIfAbsent(requestMsg.getDeviceName(), id -> new ReentrantLock()); deviceCreationLock.lock(); try { Device device = deviceService.findDeviceByTenantIdAndName(gateway.getTenantId(), requestMsg.getDeviceName()); @@ -135,6 +234,8 @@ public class DefaultTransportApiService implements TransportApiService { device.setName(requestMsg.getDeviceName()); device.setType(requestMsg.getDeviceType()); device.setCustomerId(gateway.getCustomerId()); + DeviceProfile deviceProfile = deviceProfileCache.findOrCreateDeviceProfile(gateway.getTenantId(), requestMsg.getDeviceType()); + device.setDeviceProfileId(deviceProfile.getId()); device = deviceService.saveDevice(device); relationService.saveRelationAsync(TenantId.SYS_TENANT_ID, new EntityRelation(gateway.getId(), device.getId(), "Created")); deviceStateService.onDeviceAdded(device); @@ -151,10 +252,19 @@ public class DefaultTransportApiService implements TransportApiService { TbMsg tbMsg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, deviceId, metaData, TbMsgDataType.JSON, mapper.writeValueAsString(entityNode)); tbClusterService.pushMsgToRuleEngine(tenantId, deviceId, tbMsg, null); } + GetOrCreateDeviceFromGatewayResponseMsg.Builder builder = GetOrCreateDeviceFromGatewayResponseMsg.newBuilder() + .setDeviceInfo(getDeviceInfoProto(device)); + DeviceProfile deviceProfile = deviceProfileCache.get(device.getTenantId(), device.getDeviceProfileId()); + if (deviceProfile != null) { + builder.setProfileBody(ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfile))); + } else { + log.warn("[{}] Failed to find device profile [{}] for device. ", device.getId(), device.getDeviceProfileId()); + } return TransportApiResponseMsg.newBuilder() - .setGetOrCreateDeviceResponseMsg(GetOrCreateDeviceFromGatewayResponseMsg.newBuilder().setDeviceInfo(getDeviceInfoProto(device)).build()).build(); + .setGetOrCreateDeviceResponseMsg(builder.build()) + .build(); } catch (JsonProcessingException e) { - log.warn("[{}][{}] Failed to lookup device by gateway id and name", gatewayId, requestMsg.getDeviceName(), e); + log.warn("[{}] Failed to lookup device by gateway id and name: [{}]", gatewayId, requestMsg.getDeviceName(), e); throw new RuntimeException(e); } finally { deviceCreationLock.unlock(); @@ -162,12 +272,68 @@ public class DefaultTransportApiService implements TransportApiService { }, dbCallbackExecutorService); } - private ListenableFuture handle(GetTenantRoutingInfoRequestMsg requestMsg) { - TenantId tenantId = new TenantId(new UUID(requestMsg.getTenantIdMSB(), requestMsg.getTenantIdLSB())); - ListenableFuture tenantFuture = tenantService.findTenantByIdAsync(TenantId.SYS_TENANT_ID, tenantId); - return Futures.transform(tenantFuture, tenant -> TransportApiResponseMsg.newBuilder() - .setGetTenantRoutingInfoResponseMsg(GetTenantRoutingInfoResponseMsg.newBuilder().setIsolatedTbCore(tenant.isIsolatedTbCore()) - .setIsolatedTbRuleEngine(tenant.isIsolatedTbRuleEngine()).build()).build(), dbCallbackExecutorService); + private ListenableFuture handle(ProvisionDeviceRequestMsg requestMsg) { + ListenableFuture provisionResponseFuture = null; + try { + provisionResponseFuture = Futures.immediateFuture(deviceProvisionService.provisionDevice( + new ProvisionRequest( + requestMsg.getDeviceName(), + requestMsg.getCredentialsType() != null ? DeviceCredentialsType.valueOf(requestMsg.getCredentialsType().name()) : null, + new ProvisionDeviceCredentialsData(requestMsg.getCredentialsDataProto().getValidateDeviceTokenRequestMsg().getToken(), + requestMsg.getCredentialsDataProto().getValidateBasicMqttCredRequestMsg().getClientId(), + requestMsg.getCredentialsDataProto().getValidateBasicMqttCredRequestMsg().getUserName(), + requestMsg.getCredentialsDataProto().getValidateBasicMqttCredRequestMsg().getPassword(), + requestMsg.getCredentialsDataProto().getValidateDeviceX509CertRequestMsg().getHash()), + new ProvisionDeviceProfileCredentials( + requestMsg.getProvisionDeviceCredentialsMsg().getProvisionDeviceKey(), + requestMsg.getProvisionDeviceCredentialsMsg().getProvisionDeviceSecret())))); + } catch (ProvisionFailedException e) { + return Futures.immediateFuture(getTransportApiResponseMsg( + TransportProtos.DeviceCredentialsProto.getDefaultInstance(), + TransportProtos.ProvisionResponseStatus.valueOf(e.getMessage()))); + } + return Futures.transform(provisionResponseFuture, provisionResponse -> getTransportApiResponseMsg( + getDeviceCredentials(provisionResponse.getDeviceCredentials()), TransportProtos.ProvisionResponseStatus.SUCCESS), + dbCallbackExecutorService); + } + + private TransportApiResponseMsg getTransportApiResponseMsg(TransportProtos.DeviceCredentialsProto deviceCredentials, TransportProtos.ProvisionResponseStatus status) { + return TransportApiResponseMsg.newBuilder() + .setProvisionDeviceResponseMsg(TransportProtos.ProvisionDeviceResponseMsg.newBuilder() + .setDeviceCredentials(deviceCredentials) + .setProvisionResponseStatus(status) + .build()) + .build(); + } + + private TransportProtos.DeviceCredentialsProto getDeviceCredentials(DeviceCredentials deviceCredentials) { + return TransportProtos.DeviceCredentialsProto.newBuilder() + .setDeviceIdMSB(deviceCredentials.getDeviceId().getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceCredentials.getDeviceId().getId().getLeastSignificantBits()) + .setCredentialsType(TransportProtos.CredentialsType.valueOf(deviceCredentials.getCredentialsType().name())) + .setCredentialsId(deviceCredentials.getCredentialsId()) + .setCredentialsValue(deviceCredentials.getCredentialsValue() != null ? deviceCredentials.getCredentialsValue() : "") + .build(); + } + + private ListenableFuture handle(GetEntityProfileRequestMsg requestMsg) { + EntityType entityType = EntityType.valueOf(requestMsg.getEntityType()); + UUID entityUuid = new UUID(requestMsg.getEntityIdMSB(), requestMsg.getEntityIdLSB()); + GetEntityProfileResponseMsg.Builder builder = GetEntityProfileResponseMsg.newBuilder(); + if (entityType.equals(EntityType.DEVICE_PROFILE)) { + DeviceProfileId deviceProfileId = new DeviceProfileId(entityUuid); + DeviceProfile deviceProfile = deviceProfileCache.find(deviceProfileId); + builder.setData(ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfile))); + } else if (entityType.equals(EntityType.TENANT)) { + TenantId tenantId = new TenantId(entityUuid); + TenantProfile tenantProfile = tenantProfileCache.get(tenantId); + ApiUsageState state = apiUsageStateService.getApiUsageState(tenantId); + builder.setData(ByteString.copyFrom(dataDecodingEncodingService.encode(tenantProfile))); + builder.setApiState(ByteString.copyFrom(dataDecodingEncodingService.encode(state))); + } else { + throw new RuntimeException("Invalid entity profile request: " + entityType); + } + return Futures.immediateFuture(TransportApiResponseMsg.newBuilder().setEntityProfileResponseMsg(builder).build()); } private ListenableFuture getDeviceInfo(DeviceId deviceId, DeviceCredentials credentials) { @@ -179,11 +345,17 @@ public class DefaultTransportApiService implements TransportApiService { try { ValidateDeviceCredentialsResponseMsg.Builder builder = ValidateDeviceCredentialsResponseMsg.newBuilder(); builder.setDeviceInfo(getDeviceInfoProto(device)); + DeviceProfile deviceProfile = deviceProfileCache.get(device.getTenantId(), device.getDeviceProfileId()); + if (deviceProfile != null) { + builder.setProfileBody(ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfile))); + } else { + log.warn("[{}] Failed to find device profile [{}] for device. ", device.getId(), device.getDeviceProfileId()); + } if (!StringUtils.isEmpty(credentials.getCredentialsValue())) { builder.setCredentialsBody(credentials.getCredentialsValue()); } return TransportApiResponseMsg.newBuilder() - .setValidateTokenResponseMsg(builder.build()).build(); + .setValidateCredResponseMsg(builder.build()).build(); } catch (JsonProcessingException e) { log.warn("[{}] Failed to lookup device by id", deviceId, e); return getEmptyTransportApiResponse(); @@ -199,6 +371,8 @@ public class DefaultTransportApiService implements TransportApiService { .setDeviceIdLSB(device.getId().getId().getLeastSignificantBits()) .setDeviceName(device.getName()) .setDeviceType(device.getType()) + .setDeviceProfileIdMSB(device.getDeviceProfileId().getId().getMostSignificantBits()) + .setDeviceProfileIdLSB(device.getDeviceProfileId().getId().getLeastSignificantBits()) .setAdditionalInfo(mapper.writeValueAsString(device.getAdditionalInfo())) .build(); } @@ -209,6 +383,6 @@ public class DefaultTransportApiService implements TransportApiService { private TransportApiResponseMsg getEmptyTransportApiResponse() { return TransportApiResponseMsg.newBuilder() - .setValidateTokenResponseMsg(ValidateDeviceCredentialsResponseMsg.getDefaultInstance()).build(); + .setValidateCredResponseMsg(ValidateDeviceCredentialsResponseMsg.getDefaultInstance()).build(); } } diff --git a/application/src/main/java/org/thingsboard/server/service/transport/TbCoreTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/TbCoreTransportApiService.java index a191a3167e..44fbf17a42 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/TbCoreTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/TbCoreTransportApiService.java @@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; import org.springframework.stereotype.Service; import org.thingsboard.server.common.stats.MessagesStats; import org.thingsboard.server.common.stats.StatsFactory; @@ -90,6 +91,7 @@ public class TbCoreTransportApiService { } @EventListener(ApplicationReadyEvent.class) + @Order(value = 2) public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) { log.info("Received application ready event. Starting polling for events."); transportApiTemplate.init(transportApiService); diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/events/EventsCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/events/EventsCleanUpService.java index ca52bca7e0..0f3cd71f00 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/events/EventsCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/events/EventsCleanUpService.java @@ -20,7 +20,6 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.thingsboard.server.dao.util.PsqlDao; -import org.thingsboard.server.dao.util.SqlDao; import org.thingsboard.server.service.ttl.AbstractCleanUpService; import java.sql.Connection; @@ -28,7 +27,6 @@ import java.sql.DriverManager; import java.sql.SQLException; @PsqlDao -@SqlDao @Slf4j @Service public class EventsCleanUpService extends AbstractCleanUpService { diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/timeseries/PsqlTimeseriesCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/timeseries/PsqlTimeseriesCleanUpService.java index 2464ab4677..73a5c73732 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/timeseries/PsqlTimeseriesCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/timeseries/PsqlTimeseriesCleanUpService.java @@ -19,12 +19,14 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.util.PsqlTsDao; +import org.thingsboard.server.dao.util.PsqlDao; +import org.thingsboard.server.dao.util.SqlTsDao; import java.sql.Connection; import java.sql.SQLException; -@PsqlTsDao +@SqlTsDao +@PsqlDao @Service @Slf4j public class PsqlTimeseriesCleanUpService extends AbstractTimeseriesCleanUpService { @@ -36,7 +38,7 @@ public class PsqlTimeseriesCleanUpService extends AbstractTimeseriesCleanUpServi protected void doCleanUp(Connection connection) throws SQLException { long totalPartitionsRemoved = executeQuery(connection, "call drop_partitions_by_max_ttl('" + partitionType + "'," + systemTtl + ", 0);"); log.info("Total partitions removed by TTL: [{}]", totalPartitionsRemoved); - long totalEntitiesTelemetryRemoved = executeQuery(connection, "call cleanup_timeseries_by_ttl('" + ModelConstants.NULL_UUID_STR + "'," + systemTtl + ", 0);"); + long totalEntitiesTelemetryRemoved = executeQuery(connection, "call cleanup_timeseries_by_ttl('" + ModelConstants.NULL_UUID + "'," + systemTtl + ", 0);"); log.info("Total telemetry removed stats by TTL for entities: [{}]", totalEntitiesTelemetryRemoved); } } \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/timeseries/TimescaleTimeseriesCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/timeseries/TimescaleTimeseriesCleanUpService.java index 8bdeea46ae..40febd1988 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/timeseries/TimescaleTimeseriesCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/timeseries/TimescaleTimeseriesCleanUpService.java @@ -30,7 +30,7 @@ public class TimescaleTimeseriesCleanUpService extends AbstractTimeseriesCleanUp @Override protected void doCleanUp(Connection connection) throws SQLException { - long totalEntitiesTelemetryRemoved = executeQuery(connection, "call cleanup_timeseries_by_ttl('" + ModelConstants.NULL_UUID_STR + "'," + systemTtl + ", 0);"); + long totalEntitiesTelemetryRemoved = executeQuery(connection, "call cleanup_timeseries_by_ttl('" + ModelConstants.NULL_UUID + "'," + systemTtl + ", 0);"); log.info("Total telemetry removed stats by TTL for entities: [{}]", totalEntitiesTelemetryRemoved); } -} \ No newline at end of file +} diff --git a/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java b/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java index ed13ca603d..e2d9302d79 100644 --- a/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java @@ -49,12 +49,43 @@ public class MiscUtils { } public static String constructBaseUrl(HttpServletRequest request) { - String scheme = request.getScheme(); + return String.format("%s://%s:%d", + getScheme(request), + getDomainName(request), + getPort(request)); + } + public static String getScheme(HttpServletRequest request){ + String scheme = request.getScheme(); String forwardedProto = request.getHeader("x-forwarded-proto"); if (forwardedProto != null) { scheme = forwardedProto; } + return scheme; + } + + public static String getDomainName(HttpServletRequest request){ + return request.getServerName(); + } + + public static String getDomainNameAndPort(HttpServletRequest request){ + String domainName = getDomainName(request); + String scheme = getScheme(request); + int port = MiscUtils.getPort(request); + if (needsPort(scheme, port)) { + domainName += ":" + port; + } + return domainName; + } + + private static boolean needsPort(String scheme, int port) { + boolean isHttpDefault = "http".equals(scheme.toLowerCase()) && port == 80; + boolean isHttpsDefault = "https".equals(scheme.toLowerCase()) && port == 443; + return !isHttpDefault && !isHttpsDefault; + } + + public static int getPort(HttpServletRequest request){ + String forwardedProto = request.getHeader("x-forwarded-proto"); int serverPort = request.getServerPort(); if (request.getHeader("x-forwarded-port") != null) { @@ -72,11 +103,6 @@ public class MiscUtils { break; } } - - String baseUrl = String.format("%s://%s:%d", - scheme, - request.getServerName(), - serverPort); - return baseUrl; + return serverPort; } } diff --git a/application/src/main/resources/logback.xml b/application/src/main/resources/logback.xml index 81eb788e35..e154de2d9b 100644 --- a/application/src/main/resources/logback.xml +++ b/application/src/main/resources/logback.xml @@ -32,6 +32,8 @@ + + diff --git a/application/src/main/resources/templates/account.activated.ftl b/application/src/main/resources/templates/account.activated.ftl new file mode 100644 index 0000000000..55683743de --- /dev/null +++ b/application/src/main/resources/templates/account.activated.ftl @@ -0,0 +1,124 @@ +<#-- + + Copyright © 2016-2020 The Thingsboard Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--> + + + + + +Thingsboard - Account Activated + + + + + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + +
+

Your Thingsboard account has been activated

+
+ Congratulations! Your Thingsboard account has been activated. +
+ Now you can login to your Thingsboard space. +
+ +
+ — The Thingsboard +
+ +
+
+ + diff --git a/application/src/main/resources/templates/account.activated.vm b/application/src/main/resources/templates/account.activated.vm deleted file mode 100644 index b5dcf927dd..0000000000 --- a/application/src/main/resources/templates/account.activated.vm +++ /dev/null @@ -1,122 +0,0 @@ -#* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - *# - - - - - -Thingsboard - Account Activated - - - - - - - - - - - -
-
- - -
- - - - - - - - - - - - - - - -
-

Your Thingsboard account has been activated

-
- Congratulations! Your Thingsboard account has been activated. -
- Now you can login to your Thingsboard space. -
- -
- — The Thingsboard -
- -
-
- - diff --git a/application/src/main/resources/templates/account.lockout.ftl b/application/src/main/resources/templates/account.lockout.ftl new file mode 100644 index 0000000000..15214aad50 --- /dev/null +++ b/application/src/main/resources/templates/account.lockout.ftl @@ -0,0 +1,114 @@ +<#-- + + Copyright © 2016-2020 The Thingsboard Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--> + + + + + +Thingsboard - Account Lockout + + + + + + + + + + + +
+
+ + +
+ + + + + + + + + +
+

Thingsboard user account has been locked out

+
+ Thingsboard user account ${lockoutAccount} has been lockout due to failed credentials were provided more than ${maxFailedLoginAttempts} times. +
+ — The Thingsboard +
+ +
+
+ + diff --git a/application/src/main/resources/templates/account.lockout.vm b/application/src/main/resources/templates/account.lockout.vm deleted file mode 100644 index 08ce236cf9..0000000000 --- a/application/src/main/resources/templates/account.lockout.vm +++ /dev/null @@ -1,112 +0,0 @@ -#* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - *# - - - - - -Thingsboard - Account Lockout - - - - - - - - - - - -
-
- - -
- - - - - - - - - -
-

Thingsboard user account has been locked out

-
- Thingsboard user account $lockoutAccount has been lockout due to failed credentials were provided more than $maxFailedLoginAttempts times. -
- — The Thingsboard -
- -
-
- - diff --git a/application/src/main/resources/templates/activation.ftl b/application/src/main/resources/templates/activation.ftl new file mode 100644 index 0000000000..ae89bd6342 --- /dev/null +++ b/application/src/main/resources/templates/activation.ftl @@ -0,0 +1,124 @@ +<#-- + + Copyright © 2016-2020 The Thingsboard Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--> + + + + + +Thingsboard - Account Activation + + + + + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + +
+

Activate your Thingsboard account

+
+ To confirm your email address and choose a password, just click the button below. +
+ We may need to send you critical information about our service and it is important that we have an accurate email address. +
+ +
+ — The Thingsboard +
+ +
+
+ + diff --git a/application/src/main/resources/templates/activation.vm b/application/src/main/resources/templates/activation.vm deleted file mode 100644 index 5002903545..0000000000 --- a/application/src/main/resources/templates/activation.vm +++ /dev/null @@ -1,122 +0,0 @@ -#* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - *# - - - - - -Thingsboard - Account Activation - - - - - - - - - - - -
-
- - -
- - - - - - - - - - - - - - - -
-

Activate your Thingsboard account

-
- To confirm your email address and choose a password, just click the button below. -
- We may need to send you critical information about our service and it is important that we have an accurate email address. -
- -
- — The Thingsboard -
- -
-
- - diff --git a/application/src/main/resources/templates/password.was.reset.ftl b/application/src/main/resources/templates/password.was.reset.ftl new file mode 100644 index 0000000000..15e5c9bd0b --- /dev/null +++ b/application/src/main/resources/templates/password.was.reset.ftl @@ -0,0 +1,124 @@ +<#-- + + Copyright © 2016-2020 The Thingsboard Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--> + + + + + +Thingsboard - Account Password Has Been Reset + + + + + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + +
+

Your Thingsboard account password has been reset

+
+ You have successfully created new password for your Thingsboard account. +
+ Now you can login to your Thingsboard space using your newly created password. +
+ +
+ — The Thingsboard +
+ +
+
+ + diff --git a/application/src/main/resources/templates/password.was.reset.vm b/application/src/main/resources/templates/password.was.reset.vm deleted file mode 100644 index f7303b375c..0000000000 --- a/application/src/main/resources/templates/password.was.reset.vm +++ /dev/null @@ -1,122 +0,0 @@ -#* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - *# - - - - - -Thingsboard - Account Password Has Been Reset - - - - - - - - - - - -
-
- - -
- - - - - - - - - - - - - - - -
-

Your Thingsboard account password has been reset

-
- You have successfully created new password for your Thingsboard account. -
- Now you can login to your Thingsboard space using your newly created password. -
- -
- — The Thingsboard -
- -
-
- - diff --git a/application/src/main/resources/templates/reset.password.ftl b/application/src/main/resources/templates/reset.password.ftl new file mode 100644 index 0000000000..fdd1e07e0f --- /dev/null +++ b/application/src/main/resources/templates/reset.password.ftl @@ -0,0 +1,124 @@ +<#-- + + Copyright © 2016-2020 The Thingsboard Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--> + + + + + +Thingsboard - Reset Password Request + + + + + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + +
+

Password reset has been requested

+
+ You have requested password reset for your Thingsboard account. +
+ Click below in order to proceed password reset procedure. +
+ +
+ — The Thingsboard +
+ +
+
+ + diff --git a/application/src/main/resources/templates/reset.password.vm b/application/src/main/resources/templates/reset.password.vm deleted file mode 100644 index 117a2dd17e..0000000000 --- a/application/src/main/resources/templates/reset.password.vm +++ /dev/null @@ -1,122 +0,0 @@ -#* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - *# - - - - - -Thingsboard - Reset Password Request - - - - - - - - - - - -
-
- - -
- - - - - - - - - - - - - - - -
-

Password reset has been requested

-
- You have requested password reset for your Thingsboard account. -
- Click below in order to proceed password reset procedure. -
- -
- — The Thingsboard -
- -
-
- - diff --git a/application/src/main/resources/templates/test.ftl b/application/src/main/resources/templates/test.ftl new file mode 100644 index 0000000000..7a5d50b1b6 --- /dev/null +++ b/application/src/main/resources/templates/test.ftl @@ -0,0 +1,114 @@ +<#-- + + Copyright © 2016-2020 The Thingsboard Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--> + + + + + +Thingsboard - Test Message + + + + + + + + + + + +
+
+ + +
+ + + + + + + + + +
+

Test message from Thingsboard

+
+ This email is indicating that your outgoing mail settings were set up correctly. +
+ — The Thingsboard +
+ +
+
+ + diff --git a/application/src/main/resources/templates/test.vm b/application/src/main/resources/templates/test.vm deleted file mode 100644 index e0631dfb5d..0000000000 --- a/application/src/main/resources/templates/test.vm +++ /dev/null @@ -1,112 +0,0 @@ -#* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - *# - - - - - -Thingsboard - Test Message - - - - - - - - - - - -
-
- - -
- - - - - - - - - -
-

Test message from Thingsboard

-
- This email is indicating that your outgoing mail settings were set up correctly. -
- — The Thingsboard -
- -
-
- - diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 4cefe63e54..f0f4b9dd54 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -46,6 +46,12 @@ server: max_subscriptions_per_regular_user: "${TB_SERVER_WS_TENANT_RATE_LIMITS_MAX_SUBSCRIPTIONS_PER_REGULAR_USER:0}" max_subscriptions_per_public_user: "${TB_SERVER_WS_TENANT_RATE_LIMITS_MAX_SUBSCRIPTIONS_PER_PUBLIC_USER:0}" max_updates_per_session: "${TB_SERVER_WS_TENANT_RATE_LIMITS_MAX_UPDATES_PER_SESSION:300:1,3000:60}" + dynamic_page_link: + refresh_interval: "${TB_SERVER_WS_DYNAMIC_PAGE_LINK_REFRESH_INTERVAL_SEC:60}" + refresh_pool_size: "${TB_SERVER_WS_DYNAMIC_PAGE_LINK_REFRESH_POOL_SIZE:1}" + max_per_user: "${TB_SERVER_WS_DYNAMIC_PAGE_LINK_MAX_PER_USER:10}" + max_entities_per_data_subscription: "${TB_SERVER_WS_MAX_ENTITIES_PER_DATA_SUBSCRIPTION:10000}" + max_entities_per_alarm_subscription: "${TB_SERVER_WS_MAX_ENTITIES_PER_ALARM_SUBSCRIPTION:10000}" rest: limits: tenant: @@ -107,62 +113,10 @@ security: basic: enabled: "${SECURITY_BASIC_ENABLED:false}" oauth2: - # Enable/disable OAuth 2 login functionality - # For details please refer to https://thingsboard.io/docs/user-guide/oauth-2-support/ - enabled: "${SECURITY_OAUTH2_ENABLED:false}" # Redirect URL where access code from external user management system will be processed loginProcessingUrl: "${SECURITY_OAUTH2_LOGIN_PROCESSING_URL:/login/oauth2/code/}" - # List of SSO clients - clients: - default: - # Label that going to be show on login button - 'Login with {loginButtonLabel}' - loginButtonLabel: "${SECURITY_OAUTH2_DEFAULT_LOGIN_BUTTON_LABEL:Default}" - # Icon that going to be show on login button. Material design icon ID (https://material.angularjs.org/latest/api/directive/mdIcon) - loginButtonIcon: "${SECURITY_OAUTH2_DEFAULT_LOGIN_BUTTON_ICON:}" - clientName: "${SECURITY_OAUTH2_DEFAULT_CLIENT_NAME:ClientName}" - clientId: "${SECURITY_OAUTH2_DEFAULT_CLIENT_ID:}" - clientSecret: "${SECURITY_OAUTH2_DEFAULT_CLIENT_SECRET:}" - accessTokenUri: "${SECURITY_OAUTH2_DEFAULT_ACCESS_TOKEN_URI:}" - authorizationUri: "${SECURITY_OAUTH2_DEFAULT_AUTHORIZATION_URI:}" - scope: "${SECURITY_OAUTH2_DEFAULT_SCOPE:}" - # Redirect URL that must be in sync with 'security.oauth2.loginProcessingUrl', but domain name added - redirectUriTemplate: "${SECURITY_OAUTH2_DEFAULT_REDIRECT_URI_TEMPLATE:http://localhost:8080/login/oauth2/code/}" - jwkSetUri: "${SECURITY_OAUTH2_DEFAULT_JWK_SET_URI:}" - # 'authorization_code', 'implicit', 'refresh_token' or 'client_credentials' - authorizationGrantType: "${SECURITY_OAUTH2_DEFAULT_AUTHORIZATION_GRANT_TYPE:authorization_code}" - clientAuthenticationMethod: "${SECURITY_OAUTH2_DEFAULT_CLIENT_AUTHENTICATION_METHOD:post}" # basic or post - userInfoUri: "${SECURITY_OAUTH2_DEFAULT_USER_INFO_URI:}" - userNameAttributeName: "${SECURITY_OAUTH2_DEFAULT_USER_NAME_ATTRIBUTE_NAME:email}" - mapperConfig: - # Allows to create user if it not exists - allowUserCreation: "${SECURITY_OAUTH2_DEFAULT_MAPPER_ALLOW_USER_CREATION:true}" - # Allows user to setup ThingsBoard internal password and login over default Login window - activateUser: "${SECURITY_OAUTH2_DEFAULT_MAPPER_ACTIVATE_USER:false}" - # Mapper type of converter from external user into internal - 'basic' or 'custom' - type: "${SECURITY_OAUTH2_DEFAULT_MAPPER_TYPE:basic}" - basic: - # Key from attributes of external user object to use as email - emailAttributeKey: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_EMAIL_ATTRIBUTE_KEY:email}" - firstNameAttributeKey: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_FIRST_NAME_ATTRIBUTE_KEY:}" - lastNameAttributeKey: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_LAST_NAME_ATTRIBUTE_KEY:}" - # Strategy for generating Tenant from external user object - 'domain', 'email' or 'custom' - # 'domain' - name of the Tenant will be extracted as domain from the email of the user - # 'email' - name of the Tenant will email of the user - # 'custom' - please configure 'tenantNamePattern' for custom mapping - tenantNameStrategy: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_TENANT_NAME_STRATEGY:domain}" - # %{attribute_key} as placeholder for attribute value of attributes of external user object - tenantNamePattern: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_TENANT_NAME_PATTERN:}" - # If this field is not empty, user will be created as a user under defined Customer - # %{attribute_key} as placeholder for attribute value of attributes of external user object - customerNamePattern: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_CUSTOMER_NAME_PATTERN:}" - # If this field is not empty, user will be created with default defined Dashboard - defaultDashboardName: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_DEFAULT_DASHBOARD_NAME:}" - # If this field is set 'true' along with non-empty 'defaultDashboardName', user will start from the defined Dashboard in fullscreen mode - alwaysFullScreen: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_ALWAYS_FULL_SCREEN:false}" - custom: - url: "${SECURITY_OAUTH2_DEFAULT_MAPPER_CUSTOM_URL:}" - username: "${SECURITY_OAUTH2_DEFAULT_MAPPER_CUSTOM_USERNAME:}" - password: "${SECURITY_OAUTH2_DEFAULT_MAPPER_CUSTOM_PASSWORD:}" + githubMapper: + emailUrl: "${SECURITY_OAUTH2_GITHUB_MAPPER_EMAIL_URL_KEY:https://api.github.com/user/emails}" # Dashboard parameters dashboard: @@ -171,10 +125,10 @@ dashboard: database: ts_max_intervals: "${DATABASE_TS_MAX_INTERVALS:700}" # Max number of DB queries generated by single API call to fetch telemetry records - entities: - type: "${DATABASE_ENTITIES_TYPE:sql}" # cassandra OR sql ts: type: "${DATABASE_TS_TYPE:sql}" # cassandra, sql, or timescale (for hybrid mode, DATABASE_TS_TYPE value should be cassandra, or timescale) + ts_latest: + type: "${DATABASE_TS_LATEST_TYPE:sql}" # cassandra, sql, or timescale (for hybrid mode, DATABASE_TS_TYPE value should be cassandra, or timescale) # note: timescale works only with postgreSQL database for DATABASE_ENTITIES_TYPE. @@ -186,12 +140,27 @@ cassandra: keyspace_name: "${CASSANDRA_KEYSPACE_NAME:thingsboard}" # Specify node list url: "${CASSANDRA_URL:127.0.0.1:9042}" - # Enable/disable secure connection - ssl: "${CASSANDRA_USE_SSL:false}" + # Specify local datacenter name + local_datacenter: "${CASSANDRA_LOCAL_DATACENTER:datacenter1}" + ssl: + # Enable/disable secure connection + enabled: "${CASSANDRA_USE_SSL:false}" + # Enable/disable validation of Cassandra server hostname + # If enabled, hostname of Cassandra server must match CN of server certificate + hostname_validation: "${CASSANDRA_SSL_HOSTNAME_VALIDATION:true}" + # Set trust store for client authentication of server (optional, uses trust store from default SSLContext if not set) + trust_store: "${CASSANDRA_SSL_TRUST_STORE:}" + trust_store_password: "${CASSANDRA_SSL_TRUST_STORE_PASSWORD:}" + # Set key store for server authentication of client (optional, uses key store from default SSLContext if not set) + # A key store is only needed if the Cassandra server requires client authentication + key_store: "${CASSANDRA_SSL_KEY_STORE:}" + key_store_password: "${CASSANDRA_SSL_KEY_STORE_PASSWORD:}" + # Comma separated list of cipher suites (optional, uses Java default cipher suites if not set) + cipher_suites: "${CASSANDRA_SSL_CIPHER_SUITES:}" # Enable/disable JMX - jmx: "${CASSANDRA_USE_JMX:true}" + jmx: "${CASSANDRA_USE_JMX:false}" # Enable/disable metrics collection. - metrics: "${CASSANDRA_DISABLE_METRICS:true}" + metrics: "${CASSANDRA_USE_METRICS:false}" # NONE SNAPPY LZ4 compression: "${CASSANDRA_COMPRESSION:none}" # Specify cassandra cluster initialization timeout in milliseconds (if no hosts available during startup) @@ -263,8 +232,13 @@ sql: batch_max_delay: "${SQL_TS_LATEST_BATCH_MAX_DELAY_MS:100}" stats_print_interval_ms: "${SQL_TS_LATEST_BATCH_STATS_PRINT_MS:10000}" batch_threads: "${SQL_TS_LATEST_BATCH_THREADS:4}" + # Specify whether to sort entities before batch update. Should be enabled for cluster mode to avoid deadlocks + batch_sort: "${SQL_BATCH_SORT:false}" # Specify whether to remove null characters from strValue of attributes and timeseries before insert remove_null_chars: "${SQL_REMOVE_NULL_CHARS:true}" + # Specify whether to log database queries and their parameters generated by entity query repository + log_queries: "${SQL_LOG_QUERIES:false}" + log_queries_threshold: "${SQL_LOG_QUERIES_THRESHOLD:5000}" postgres: # Specify partitioning size for timestamp key-value storage. Example: DAYS, MONTHS, YEARS, INDEFINITE. ts_key_value_partitioning: "${SQL_POSTGRES_TS_KV_PARTITIONING:MONTHS}" @@ -339,28 +313,34 @@ caffeine: specs: relations: timeToLiveInMinutes: 1440 - maxSize: 100000 + maxSize: 0 deviceCredentials: timeToLiveInMinutes: 1440 - maxSize: 100000 + maxSize: 0 devices: timeToLiveInMinutes: 1440 - maxSize: 100000 + maxSize: 0 sessions: timeToLiveInMinutes: 1440 - maxSize: 100000 + maxSize: 0 assets: timeToLiveInMinutes: 1440 - maxSize: 100000 + maxSize: 0 entityViews: timeToLiveInMinutes: 1440 - maxSize: 100000 + maxSize: 0 claimDevices: timeToLiveInMinutes: 1 - maxSize: 100000 + maxSize: 0 securitySettings: timeToLiveInMinutes: 1440 - maxSize: 1 + maxSize: 0 + tenantProfiles: + timeToLiveInMinutes: 1440 + maxSize: 0 + deviceProfiles: + timeToLiveInMinutes: 1440 + maxSize: 0 redis: # standalone or cluster @@ -407,6 +387,9 @@ updates: # Enable/disable updates checking. enabled: "${UPDATES_ENABLED:true}" +# spring freemarker configuration +spring.freemarker.checkTemplateLocation: "false" + # spring CORS configuration spring.mvc.cors: mappings: @@ -431,6 +414,7 @@ spring.resources.chain: enabled: "true" spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation: "true" +spring.jpa.properties.hibernate.order_by.default_null_ordering: "last" # SQL DAO Configuration spring: @@ -449,7 +433,7 @@ spring: username: "${SPRING_DATASOURCE_USERNAME:postgres}" password: "${SPRING_DATASOURCE_PASSWORD:postgres}" hikari: - maximumPoolSize: "${SPRING_DATASOURCE_MAXIMUM_POOL_SIZE:5}" + maximumPoolSize: "${SPRING_DATASOURCE_MAXIMUM_POOL_SIZE:16}" # Audit log parameters audit-log: @@ -471,6 +455,7 @@ audit-log: "rule_chain": "${AUDIT_LOG_MASK_RULE_CHAIN:W}" "alarm": "${AUDIT_LOG_MASK_ALARM:W}" "entity_view": "${AUDIT_LOG_MASK_ENTITY_VIEW:W}" + "device_profile": "${AUDIT_LOG_MASK_DEVICE_PROFILE:W}" sink: # Type of external sink. possible options: none, elasticsearch type: "${AUDIT_LOG_SINK_TYPE:none}" @@ -503,7 +488,7 @@ js: # Specify thread pool size for JavaScript sandbox resource monitor monitor_thread_pool_size: "${LOCAL_JS_SANDBOX_MONITOR_THREAD_POOL_SIZE:4}" # Maximum CPU time in milliseconds allowed for script execution - max_cpu_time: "${LOCAL_JS_SANDBOX_MAX_CPU_TIME:10000}" + max_cpu_time: "${LOCAL_JS_SANDBOX_MAX_CPU_TIME:8000}" # Maximum allowed JavaScript execution errors before JavaScript will be blacklisted max_errors: "${LOCAL_JS_SANDBOX_MAX_ERRORS:3}" # JS Eval max request timeout. 0 - no timeout @@ -517,7 +502,7 @@ js: remote: # Maximum allowed JavaScript execution errors before JavaScript will be blacklisted max_errors: "${REMOTE_JS_SANDBOX_MAX_ERRORS:3}" - # Maximum time in seconds for black listed function to stay in the list. + # Maximum time in seconds for black listed function to stay in 1:the list. max_black_list_duration_sec: "${REMOTE_JS_SANDBOX_MAX_BLACKLIST_DURATION_SEC:60}" stats: enabled: "${TB_JS_REMOTE_STATS_ENABLED:false}" @@ -527,10 +512,6 @@ transport: sessions: inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}" report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:30000}" - rate_limits: - enabled: "${TB_TRANSPORT_RATE_LIMITS_ENABLED:false}" - tenant: "${TB_TRANSPORT_RATE_LIMITS_TENANT:1000:1,20000:60}" - device: "${TB_TRANSPORT_RATE_LIMITS_DEVICE:10:1,300:60}" json: # Cast String data types to Numeric if possible when processing Telemetry/Attributes JSON type_cast_enabled: "${JSON_TYPE_CAST_ENABLED:true}" @@ -538,6 +519,8 @@ transport: max_string_value_length: "${JSON_MAX_STRING_VALUE_LENGTH:0}" client_side_rpc: timeout: "${CLIENT_SIDE_RPC_TIMEOUT:60000}" + # Enable/disable http/mqtt/coap transport protocols (has higher priority than certain protocol's 'enabled' property) + api_enabled: "${TB_TRANSPORT_API_ENABLED:true}" # Local HTTP transport parameters http: enabled: "${HTTP_ENABLED:true}" @@ -690,6 +673,7 @@ queue: poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}" partitions: "${TB_QUEUE_CORE_PARTITIONS:10}" pack-processing-timeout: "${TB_QUEUE_CORE_PACK_PROCESSING_TIMEOUT_MS:2000}" + usage-stats-topic: "${TB_QUEUE_US_TOPIC:tb_usage_stats}" stats: enabled: "${TB_QUEUE_CORE_STATS_ENABLED:true}" print-interval-ms: "${TB_QUEUE_CORE_STATS_PRINT_INTERVAL_MS:60000}" @@ -778,10 +762,14 @@ service: metrics: # Enable/disable actuator metrics. enabled: "${METRICS_ENABLED:false}" + timer: + # Metrics percentiles returned by actuator for timer metrics. List of double values (divided by ,). + percentiles: "${METRICS_TIMER_PERCENTILES:0.5}" + management: endpoints: web: exposure: # Expose metrics endpoint (use value 'prometheus' to enable prometheus metrics). - include: '${METRICS_ENDPOINTS_EXPOSE:info}' \ No newline at end of file + include: '${METRICS_ENDPOINTS_EXPOSE:info}' diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java index b819d70a7a..f57b5a1dfd 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java @@ -15,79 +15,17 @@ */ package org.thingsboard.server.controller; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Header; -import io.jsonwebtoken.Jwt; -import io.jsonwebtoken.Jwts; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.hamcrest.Matcher; -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Rule; -import org.junit.rules.TestRule; -import org.junit.rules.TestWatcher; -import org.junit.runner.Description; import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootContextLoader; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.converter.StringHttpMessageConverter; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; -import org.springframework.mock.http.MockHttpInputMessage; -import org.springframework.mock.http.MockHttpOutputMessage; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.MvcResult; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.test.web.servlet.ResultMatcher; -import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import org.springframework.web.context.WebApplicationContext; -import org.thingsboard.server.common.data.BaseData; -import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.Tenant; -import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UUIDBased; -import org.thingsboard.server.common.data.page.TextPageLink; -import org.thingsboard.server.common.data.page.TimePageLink; -import org.thingsboard.server.common.data.security.Authority; -import org.thingsboard.server.config.ThingsboardSecurityConfiguration; -import org.thingsboard.server.service.mail.TestMailService; -import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest; -import org.thingsboard.server.service.security.auth.rest.LoginRequest; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.List; - -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; @ActiveProfiles("test") @RunWith(SpringRunner.class) @@ -96,405 +34,8 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppC @Configuration @ComponentScan({"org.thingsboard.server"}) @WebAppConfiguration -@SpringBootTest +@SpringBootTest() @Slf4j -public abstract class AbstractControllerTest { - - protected ObjectMapper mapper = new ObjectMapper(); - - protected static final String TEST_TENANT_NAME = "TEST TENANT"; - - protected static final String SYS_ADMIN_EMAIL = "sysadmin@thingsboard.org"; - private static final String SYS_ADMIN_PASSWORD = "sysadmin"; - - protected static final String TENANT_ADMIN_EMAIL = "testtenant@thingsboard.org"; - private static final String TENANT_ADMIN_PASSWORD = "tenant"; - - protected static final String CUSTOMER_USER_EMAIL = "testcustomer@thingsboard.org"; - private static final String CUSTOMER_USER_PASSWORD = "customer"; - - /** See {@link org.springframework.test.web.servlet.DefaultMvcResult#getAsyncResult(long)} - * and {@link org.springframework.mock.web.MockAsyncContext#getTimeout()} - */ - private static final long DEFAULT_TIMEOUT = -1L; - - protected MediaType contentType = MediaType.APPLICATION_JSON; - - protected MockMvc mockMvc; - - protected String token; - protected String refreshToken; - protected String username; - - private TenantId tenantId; - - @SuppressWarnings("rawtypes") - private HttpMessageConverter mappingJackson2HttpMessageConverter; - - @SuppressWarnings("rawtypes") - private HttpMessageConverter stringHttpMessageConverter; - - @Autowired - private WebApplicationContext webApplicationContext; - - @Rule - public TestRule watcher = new TestWatcher() { - protected void starting(Description description) { - log.info("Starting test: {}", description.getMethodName()); - } - - protected void finished(Description description) { - log.info("Finished test: {}", description.getMethodName()); - } - }; - - @Autowired - void setConverters(HttpMessageConverter[] converters) { - - this.mappingJackson2HttpMessageConverter = Arrays.stream(converters) - .filter(hmc -> hmc instanceof MappingJackson2HttpMessageConverter) - .findAny() - .get(); - - this.stringHttpMessageConverter = Arrays.stream(converters) - .filter(hmc -> hmc instanceof StringHttpMessageConverter) - .findAny() - .get(); - - Assert.assertNotNull("the JSON message converter must not be null", - this.mappingJackson2HttpMessageConverter); - } - - @Before - public void setup() throws Exception { - log.info("Executing setup"); - if (this.mockMvc == null) { - this.mockMvc = webAppContextSetup(webApplicationContext) - .apply(springSecurity()).build(); - } - loginSysAdmin(); - Tenant tenant = new Tenant(); - tenant.setTitle(TEST_TENANT_NAME); - Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class); - Assert.assertNotNull(savedTenant); - tenantId = savedTenant.getId(); - - User tenantAdmin = new User(); - tenantAdmin.setAuthority(Authority.TENANT_ADMIN); - tenantAdmin.setTenantId(tenantId); - tenantAdmin.setEmail(TENANT_ADMIN_EMAIL); - - createUserAndLogin(tenantAdmin, TENANT_ADMIN_PASSWORD); - - Customer customer = new Customer(); - customer.setTitle("Customer"); - customer.setTenantId(tenantId); - Customer savedCustomer = doPost("/api/customer", customer, Customer.class); - - User customerUser = new User(); - customerUser.setAuthority(Authority.CUSTOMER_USER); - customerUser.setTenantId(tenantId); - customerUser.setCustomerId(savedCustomer.getId()); - customerUser.setEmail(CUSTOMER_USER_EMAIL); - - createUserAndLogin(customerUser, CUSTOMER_USER_PASSWORD); - - logout(); - - log.info("Executed setup"); - } - - @After - public void teardown() throws Exception { - log.info("Executing teardown"); - loginSysAdmin(); - doDelete("/api/tenant/" + tenantId.getId().toString()) - .andExpect(status().isOk()); - log.info("Executed teardown"); - } - - protected void loginSysAdmin() throws Exception { - login(SYS_ADMIN_EMAIL, SYS_ADMIN_PASSWORD); - } - - protected void loginTenantAdmin() throws Exception { - login(TENANT_ADMIN_EMAIL, TENANT_ADMIN_PASSWORD); - } - - protected void loginCustomerUser() throws Exception { - login(CUSTOMER_USER_EMAIL, CUSTOMER_USER_PASSWORD); - } - - private Tenant savedDifferentTenant; - - protected void loginDifferentTenant() throws Exception { - loginSysAdmin(); - Tenant tenant = new Tenant(); - tenant.setTitle("Different tenant"); - savedDifferentTenant = doPost("/api/tenant", tenant, Tenant.class); - Assert.assertNotNull(savedDifferentTenant); - User differentTenantAdmin = new User(); - differentTenantAdmin.setAuthority(Authority.TENANT_ADMIN); - differentTenantAdmin.setTenantId(savedDifferentTenant.getId()); - differentTenantAdmin.setEmail("different_tenant@thingsboard.org"); - - createUserAndLogin(differentTenantAdmin, "testPassword"); - } - - protected void deleteDifferentTenant() throws Exception { - loginSysAdmin(); - doDelete("/api/tenant/" + savedDifferentTenant.getId().getId().toString()) - .andExpect(status().isOk()); - } - - protected User createUserAndLogin(User user, String password) throws Exception { - User savedUser = doPost("/api/user", user, User.class); - logout(); - doGet("/api/noauth/activate?activateToken={activateToken}", TestMailService.currentActivateToken) - .andExpect(status().isSeeOther()) - .andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken)); - JsonNode activateRequest = new ObjectMapper().createObjectNode() - .put("activateToken", TestMailService.currentActivateToken) - .put("password", password); - JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", activateRequest).andExpect(status().isOk()), JsonNode.class); - validateAndSetJwtToken(tokenInfo, user.getEmail()); - return savedUser; - } - - protected void login(String username, String password) throws Exception { - this.token = null; - this.refreshToken = null; - this.username = null; - JsonNode tokenInfo = readResponse(doPost("/api/auth/login", new LoginRequest(username, password)).andExpect(status().isOk()), JsonNode.class); - validateAndSetJwtToken(tokenInfo, username); - } - - protected void refreshToken() throws Exception { - this.token = null; - JsonNode tokenInfo = readResponse(doPost("/api/auth/token", new RefreshTokenRequest(this.refreshToken)).andExpect(status().isOk()), JsonNode.class); - validateAndSetJwtToken(tokenInfo, this.username); - } - - protected void validateAndSetJwtToken(JsonNode tokenInfo, String username) { - Assert.assertNotNull(tokenInfo); - Assert.assertTrue(tokenInfo.has("token")); - Assert.assertTrue(tokenInfo.has("refreshToken")); - String token = tokenInfo.get("token").asText(); - String refreshToken = tokenInfo.get("refreshToken").asText(); - validateJwtToken(token, username); - validateJwtToken(refreshToken, username); - this.token = token; - this.refreshToken = refreshToken; - this.username = username; - } - - protected void validateJwtToken(String token, String username) { - Assert.assertNotNull(token); - Assert.assertFalse(token.isEmpty()); - int i = token.lastIndexOf('.'); - Assert.assertTrue(i > 0); - String withoutSignature = token.substring(0, i + 1); - Jwt jwsClaims = Jwts.parser().parseClaimsJwt(withoutSignature); - Claims claims = jwsClaims.getBody(); - String subject = claims.getSubject(); - Assert.assertEquals(username, subject); - } - - protected void logout() throws Exception { - this.token = null; - this.refreshToken = null; - this.username = null; - } - - protected void setJwtToken(MockHttpServletRequestBuilder request) { - if (this.token != null) { - request.header(ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM, "Bearer " + this.token); - } - } - - protected ResultActions doGet(String urlTemplate, Object... urlVariables) throws Exception { - MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables); - setJwtToken(getRequest); - return mockMvc.perform(getRequest); - } - - protected T doGet(String urlTemplate, Class responseClass, Object... urlVariables) throws Exception { - return readResponse(doGet(urlTemplate, urlVariables).andExpect(status().isOk()), responseClass); - } - - protected T doGet(String urlTemplate, Class responseClass, ResultMatcher resultMatcher, Object... urlVariables) throws Exception { - return readResponse(doGet(urlTemplate, urlVariables).andExpect(resultMatcher), responseClass); - } - - protected T doGetAsync(String urlTemplate, Class responseClass, Object... urlVariables) throws Exception { - return readResponse(doGetAsync(urlTemplate, urlVariables).andExpect(status().isOk()), responseClass); - } - - protected ResultActions doGetAsync(String urlTemplate, Object... urlVariables) throws Exception { - MockHttpServletRequestBuilder getRequest; - getRequest = get(urlTemplate, urlVariables); - setJwtToken(getRequest); - return mockMvc.perform(asyncDispatch(mockMvc.perform(getRequest).andExpect(request().asyncStarted()).andReturn())); - } - - protected T doGetTyped(String urlTemplate, TypeReference responseType, Object... urlVariables) throws Exception { - return readResponse(doGet(urlTemplate, urlVariables).andExpect(status().isOk()), responseType); - } - - protected T doGetTypedWithPageLink(String urlTemplate, TypeReference responseType, - TextPageLink pageLink, - Object... urlVariables) throws Exception { - List pageLinkVariables = new ArrayList<>(); - urlTemplate += "limit={limit}"; - pageLinkVariables.add(pageLink.getLimit()); - if (StringUtils.isNotEmpty(pageLink.getTextSearch())) { - urlTemplate += "&textSearch={textSearch}"; - pageLinkVariables.add(pageLink.getTextSearch()); - } - if (pageLink.getIdOffset() != null) { - urlTemplate += "&idOffset={idOffset}"; - pageLinkVariables.add(pageLink.getIdOffset().toString()); - } - if (StringUtils.isNotEmpty(pageLink.getTextOffset())) { - urlTemplate += "&textOffset={textOffset}"; - pageLinkVariables.add(pageLink.getTextOffset()); - } - - Object[] vars = new Object[urlVariables.length + pageLinkVariables.size()]; - System.arraycopy(urlVariables, 0, vars, 0, urlVariables.length); - System.arraycopy(pageLinkVariables.toArray(), 0, vars, urlVariables.length, pageLinkVariables.size()); - - return readResponse(doGet(urlTemplate, vars).andExpect(status().isOk()), responseType); - } - - protected T doGetTypedWithTimePageLink(String urlTemplate, TypeReference responseType, - TimePageLink pageLink, - Object... urlVariables) throws Exception { - List pageLinkVariables = new ArrayList<>(); - urlTemplate += "limit={limit}"; - pageLinkVariables.add(pageLink.getLimit()); - if (pageLink.getStartTime() != null) { - urlTemplate += "&startTime={startTime}"; - pageLinkVariables.add(pageLink.getStartTime()); - } - if (pageLink.getEndTime() != null) { - urlTemplate += "&endTime={endTime}"; - pageLinkVariables.add(pageLink.getEndTime()); - } - if (pageLink.getIdOffset() != null) { - urlTemplate += "&offset={offset}"; - pageLinkVariables.add(pageLink.getIdOffset().toString()); - } - if (pageLink.isAscOrder()) { - urlTemplate += "&ascOrder={ascOrder}"; - pageLinkVariables.add(pageLink.isAscOrder()); - } - Object[] vars = new Object[urlVariables.length + pageLinkVariables.size()]; - System.arraycopy(urlVariables, 0, vars, 0, urlVariables.length); - System.arraycopy(pageLinkVariables.toArray(), 0, vars, urlVariables.length, pageLinkVariables.size()); - - return readResponse(doGet(urlTemplate, vars).andExpect(status().isOk()), responseType); - } - - protected T doPost(String urlTemplate, Class responseClass, String... params) throws Exception { - return readResponse(doPost(urlTemplate, params).andExpect(status().isOk()), responseClass); - } - - protected T doPost(String urlTemplate, T content, Class responseClass, ResultMatcher resultMatcher, String... params) throws Exception { - return readResponse(doPost(urlTemplate, content, params).andExpect(resultMatcher), responseClass); - } - - protected T doPost(String urlTemplate, T content, Class responseClass, String... params) throws Exception { - return readResponse(doPost(urlTemplate, content, params).andExpect(status().isOk()), responseClass); - } - - protected T doPostAsync(String urlTemplate, T content, Class responseClass, ResultMatcher resultMatcher, String... params) throws Exception { - return readResponse(doPostAsync(urlTemplate, content, DEFAULT_TIMEOUT, params).andExpect(resultMatcher), responseClass); - } - - protected T doPostAsync(String urlTemplate, T content, Class responseClass, ResultMatcher resultMatcher, Long timeout, String... params) throws Exception { - return readResponse(doPostAsync(urlTemplate, content, timeout, params).andExpect(resultMatcher), responseClass); - } - - protected T doDelete(String urlTemplate, Class responseClass, String... params) throws Exception { - return readResponse(doDelete(urlTemplate, params).andExpect(status().isOk()), responseClass); - } - - protected ResultActions doPost(String urlTemplate, String... params) throws Exception { - MockHttpServletRequestBuilder postRequest = post(urlTemplate); - setJwtToken(postRequest); - populateParams(postRequest, params); - return mockMvc.perform(postRequest); - } - - protected ResultActions doPost(String urlTemplate, T content, String... params) throws Exception { - MockHttpServletRequestBuilder postRequest = post(urlTemplate); - setJwtToken(postRequest); - String json = json(content); - postRequest.contentType(contentType).content(json); - return mockMvc.perform(postRequest); - } - - protected ResultActions doPostAsync(String urlTemplate, T content, Long timeout, String... params) throws Exception { - MockHttpServletRequestBuilder postRequest = post(urlTemplate); - setJwtToken(postRequest); - String json = json(content); - postRequest.contentType(contentType).content(json); - MvcResult result = mockMvc.perform(postRequest).andReturn(); - result.getAsyncResult(timeout); - return mockMvc.perform(asyncDispatch(result)); - } - - protected ResultActions doDelete(String urlTemplate, String... params) throws Exception { - MockHttpServletRequestBuilder deleteRequest = delete(urlTemplate); - setJwtToken(deleteRequest); - populateParams(deleteRequest, params); - return mockMvc.perform(deleteRequest); - } - - protected void populateParams(MockHttpServletRequestBuilder request, String... params) { - if (params != null && params.length > 0) { - Assert.assertEquals(0, params.length % 2); - MultiValueMap paramsMap = new LinkedMultiValueMap<>(); - for (int i = 0; i < params.length; i += 2) { - paramsMap.add(params[i], params[i + 1]); - } - request.params(paramsMap); - } - } - - @SuppressWarnings("unchecked") - protected String json(Object o) throws IOException { - MockHttpOutputMessage mockHttpOutputMessage = new MockHttpOutputMessage(); - - HttpMessageConverter converter = o instanceof String ? stringHttpMessageConverter : mappingJackson2HttpMessageConverter; - converter.write(o, MediaType.APPLICATION_JSON, mockHttpOutputMessage); - return mockHttpOutputMessage.getBodyAsString(); - } - - @SuppressWarnings("unchecked") - protected T readResponse(ResultActions result, Class responseClass) throws Exception { - byte[] content = result.andReturn().getResponse().getContentAsByteArray(); - MockHttpInputMessage mockHttpInputMessage = new MockHttpInputMessage(content); - HttpMessageConverter converter = responseClass.equals(String.class) ? stringHttpMessageConverter : mappingJackson2HttpMessageConverter; - return (T) converter.read(responseClass, mockHttpInputMessage); - } - - protected T readResponse(ResultActions result, TypeReference type) throws Exception { - byte[] content = result.andReturn().getResponse().getContentAsByteArray(); - ObjectMapper mapper = new ObjectMapper(); - return mapper.readerFor(type).readValue(content); - } - - public class IdComparator> implements Comparator { - @Override - public int compare(D o1, D o2) { - return o1.getId().getId().compareTo(o2.getId().getId()); - } - } - - protected static ResultMatcher statusReason(Matcher matcher) { - return jsonPath("$.message", matcher); - } +public abstract class AbstractControllerTest extends AbstractWebTest { } diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java index 4bc1ced5fe..b5c8c7e27c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java @@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.Event; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; @@ -56,10 +56,10 @@ public class AbstractRuleEngineControllerTest extends AbstractControllerTest { return doGet("/api/ruleChain/metadata/" + ruleChainId.getId().toString(), RuleChainMetaData.class); } - protected TimePageData getDebugEvents(TenantId tenantId, EntityId entityId, int limit) throws Exception { + protected PageData getDebugEvents(TenantId tenantId, EntityId entityId, int limit) throws Exception { TimePageLink pageLink = new TimePageLink(limit); return doGetTypedWithTimePageLink("/api/events/{entityType}/{entityId}/{eventType}?tenantId={tenantId}&", - new TypeReference>() { + new TypeReference>() { }, pageLink, entityId.getEntityType(), entityId.getId(), DataConstants.DEBUG_RULE_NODE, tenantId.getId()); } diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java new file mode 100644 index 0000000000..cd4a87d4de --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -0,0 +1,547 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.Jwt; +import io.jsonwebtoken.Jwts; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.hamcrest.Matcher; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.rules.TestRule; +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootContextLoader; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.mock.http.MockHttpInputMessage; +import org.springframework.mock.http.MockHttpOutputMessage; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.context.WebApplicationContext; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.device.profile.ProvisionDeviceProfileCredentials; +import org.thingsboard.server.common.data.id.HasId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UUIDBased; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.config.ThingsboardSecurityConfiguration; +import org.thingsboard.server.service.mail.TestMailService; +import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest; +import org.thingsboard.server.service.security.auth.rest.LoginRequest; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; + +@Slf4j +public abstract class AbstractWebTest { + + protected ObjectMapper mapper = new ObjectMapper(); + + protected static final String TEST_TENANT_NAME = "TEST TENANT"; + + protected static final String SYS_ADMIN_EMAIL = "sysadmin@thingsboard.org"; + private static final String SYS_ADMIN_PASSWORD = "sysadmin"; + + protected static final String TENANT_ADMIN_EMAIL = "testtenant@thingsboard.org"; + private static final String TENANT_ADMIN_PASSWORD = "tenant"; + + protected static final String CUSTOMER_USER_EMAIL = "testcustomer@thingsboard.org"; + private static final String CUSTOMER_USER_PASSWORD = "customer"; + + /** See {@link org.springframework.test.web.servlet.DefaultMvcResult#getAsyncResult(long)} + * and {@link org.springframework.mock.web.MockAsyncContext#getTimeout()} + */ + private static final long DEFAULT_TIMEOUT = -1L; + + protected MediaType contentType = MediaType.APPLICATION_JSON; + + protected MockMvc mockMvc; + + protected String token; + protected String refreshToken; + protected String username; + + private TenantId tenantId; + + @SuppressWarnings("rawtypes") + private HttpMessageConverter mappingJackson2HttpMessageConverter; + + @SuppressWarnings("rawtypes") + private HttpMessageConverter stringHttpMessageConverter; + + @Autowired + private WebApplicationContext webApplicationContext; + + @Rule + public TestRule watcher = new TestWatcher() { + protected void starting(Description description) { + log.info("Starting test: {}", description.getMethodName()); + } + + protected void finished(Description description) { + log.info("Finished test: {}", description.getMethodName()); + } + }; + + @Autowired + void setConverters(HttpMessageConverter[] converters) { + + this.mappingJackson2HttpMessageConverter = Arrays.stream(converters) + .filter(hmc -> hmc instanceof MappingJackson2HttpMessageConverter) + .findAny() + .get(); + + this.stringHttpMessageConverter = Arrays.stream(converters) + .filter(hmc -> hmc instanceof StringHttpMessageConverter) + .findAny() + .get(); + + Assert.assertNotNull("the JSON message converter must not be null", + this.mappingJackson2HttpMessageConverter); + } + + @Before + public void setup() throws Exception { + log.info("Executing setup"); + if (this.mockMvc == null) { + this.mockMvc = webAppContextSetup(webApplicationContext) + .apply(springSecurity()).build(); + } + loginSysAdmin(); + + Tenant tenant = new Tenant(); + tenant.setTitle(TEST_TENANT_NAME); + Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class); + Assert.assertNotNull(savedTenant); + tenantId = savedTenant.getId(); + + User tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(tenantId); + tenantAdmin.setEmail(TENANT_ADMIN_EMAIL); + + createUserAndLogin(tenantAdmin, TENANT_ADMIN_PASSWORD); + + Customer customer = new Customer(); + customer.setTitle("Customer"); + customer.setTenantId(tenantId); + Customer savedCustomer = doPost("/api/customer", customer, Customer.class); + + User customerUser = new User(); + customerUser.setAuthority(Authority.CUSTOMER_USER); + customerUser.setTenantId(tenantId); + customerUser.setCustomerId(savedCustomer.getId()); + customerUser.setEmail(CUSTOMER_USER_EMAIL); + + createUserAndLogin(customerUser, CUSTOMER_USER_PASSWORD); + + logout(); + + log.info("Executed setup"); + } + + @After + public void teardown() throws Exception { + log.info("Executing teardown"); + loginSysAdmin(); + doDelete("/api/tenant/" + tenantId.getId().toString()) + .andExpect(status().isOk()); + log.info("Executed teardown"); + } + + protected void loginSysAdmin() throws Exception { + login(SYS_ADMIN_EMAIL, SYS_ADMIN_PASSWORD); + } + + protected void loginTenantAdmin() throws Exception { + login(TENANT_ADMIN_EMAIL, TENANT_ADMIN_PASSWORD); + } + + protected void loginCustomerUser() throws Exception { + login(CUSTOMER_USER_EMAIL, CUSTOMER_USER_PASSWORD); + } + + protected void loginUser(String userName, String password) throws Exception { + login(userName, password); + } + + private Tenant savedDifferentTenant; + + protected void loginDifferentTenant() throws Exception { + loginSysAdmin(); + Tenant tenant = new Tenant(); + tenant.setTitle("Different tenant"); + savedDifferentTenant = doPost("/api/tenant", tenant, Tenant.class); + Assert.assertNotNull(savedDifferentTenant); + User differentTenantAdmin = new User(); + differentTenantAdmin.setAuthority(Authority.TENANT_ADMIN); + differentTenantAdmin.setTenantId(savedDifferentTenant.getId()); + differentTenantAdmin.setEmail("different_tenant@thingsboard.org"); + + createUserAndLogin(differentTenantAdmin, "testPassword"); + } + + protected void deleteDifferentTenant() throws Exception { + loginSysAdmin(); + doDelete("/api/tenant/" + savedDifferentTenant.getId().getId().toString()) + .andExpect(status().isOk()); + } + + protected User createUserAndLogin(User user, String password) throws Exception { + User savedUser = doPost("/api/user", user, User.class); + logout(); + JsonNode activateRequest = getActivateRequest(password); + JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", activateRequest).andExpect(status().isOk()), JsonNode.class); + validateAndSetJwtToken(tokenInfo, user.getEmail()); + return savedUser; + } + + protected User createUser(User user, String password) throws Exception { + User savedUser = doPost("/api/user", user, User.class); + JsonNode activateRequest = getActivateRequest(password); + ResultActions resultActions = doPost("/api/noauth/activate", activateRequest); + resultActions.andExpect(status().isOk()); + return savedUser; + } + + private JsonNode getActivateRequest(String password) throws Exception { + doGet("/api/noauth/activate?activateToken={activateToken}", TestMailService.currentActivateToken) + .andExpect(status().isSeeOther()) + .andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken)); + return new ObjectMapper().createObjectNode() + .put("activateToken", TestMailService.currentActivateToken) + .put("password", password); + } + + protected void login(String username, String password) throws Exception { + this.token = null; + this.refreshToken = null; + this.username = null; + JsonNode tokenInfo = readResponse(doPost("/api/auth/login", new LoginRequest(username, password)).andExpect(status().isOk()), JsonNode.class); + validateAndSetJwtToken(tokenInfo, username); + } + + protected void refreshToken() throws Exception { + this.token = null; + JsonNode tokenInfo = readResponse(doPost("/api/auth/token", new RefreshTokenRequest(this.refreshToken)).andExpect(status().isOk()), JsonNode.class); + validateAndSetJwtToken(tokenInfo, this.username); + } + + protected void validateAndSetJwtToken(JsonNode tokenInfo, String username) { + Assert.assertNotNull(tokenInfo); + Assert.assertTrue(tokenInfo.has("token")); + Assert.assertTrue(tokenInfo.has("refreshToken")); + String token = tokenInfo.get("token").asText(); + String refreshToken = tokenInfo.get("refreshToken").asText(); + validateJwtToken(token, username); + validateJwtToken(refreshToken, username); + this.token = token; + this.refreshToken = refreshToken; + this.username = username; + } + + protected void validateJwtToken(String token, String username) { + Assert.assertNotNull(token); + Assert.assertFalse(token.isEmpty()); + int i = token.lastIndexOf('.'); + Assert.assertTrue(i > 0); + String withoutSignature = token.substring(0, i + 1); + Jwt jwsClaims = Jwts.parser().parseClaimsJwt(withoutSignature); + Claims claims = jwsClaims.getBody(); + String subject = claims.getSubject(); + Assert.assertEquals(username, subject); + } + + protected void logout() throws Exception { + this.token = null; + this.refreshToken = null; + this.username = null; + } + + protected void setJwtToken(MockHttpServletRequestBuilder request) { + if (this.token != null) { + request.header(ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM, "Bearer " + this.token); + } + } + + protected DeviceProfile createDeviceProfile(String name) { + DeviceProfile deviceProfile = new DeviceProfile(); + deviceProfile.setName(name); + deviceProfile.setType(DeviceProfileType.DEFAULT); + deviceProfile.setTransportType(DeviceTransportType.DEFAULT); + deviceProfile.setDescription(name + " Test"); + DeviceProfileData deviceProfileData = new DeviceProfileData(); + DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration(); + DefaultDeviceProfileTransportConfiguration transportConfiguration = new DefaultDeviceProfileTransportConfiguration(); + deviceProfileData.setConfiguration(configuration); + deviceProfileData.setTransportConfiguration(transportConfiguration); + deviceProfile.setProfileData(deviceProfileData); + deviceProfile.setDefault(false); + deviceProfile.setDefaultRuleChainId(null); + return deviceProfile; + } + + protected ResultActions doGet(String urlTemplate, Object... urlVariables) throws Exception { + MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables); + setJwtToken(getRequest); + return mockMvc.perform(getRequest); + } + + protected T doGet(String urlTemplate, Class responseClass, Object... urlVariables) throws Exception { + return readResponse(doGet(urlTemplate, urlVariables).andExpect(status().isOk()), responseClass); + } + + protected T doGet(String urlTemplate, Class responseClass, ResultMatcher resultMatcher, Object... urlVariables) throws Exception { + return readResponse(doGet(urlTemplate, urlVariables).andExpect(resultMatcher), responseClass); + } + + protected T doGetAsync(String urlTemplate, Class responseClass, Object... urlVariables) throws Exception { + return readResponse(doGetAsync(urlTemplate, urlVariables).andExpect(status().isOk()), responseClass); + } + + protected ResultActions doGetAsync(String urlTemplate, Object... urlVariables) throws Exception { + MockHttpServletRequestBuilder getRequest; + getRequest = get(urlTemplate, urlVariables); + setJwtToken(getRequest); + return mockMvc.perform(asyncDispatch(mockMvc.perform(getRequest).andExpect(request().asyncStarted()).andReturn())); + } + + protected T doGetTyped(String urlTemplate, TypeReference responseType, Object... urlVariables) throws Exception { + return readResponse(doGet(urlTemplate, urlVariables).andExpect(status().isOk()), responseType); + } + + protected T doGetTypedWithPageLink(String urlTemplate, TypeReference responseType, + PageLink pageLink, + Object... urlVariables) throws Exception { + List pageLinkVariables = new ArrayList<>(); + urlTemplate += "pageSize={pageSize}&page={page}"; + pageLinkVariables.add(pageLink.getPageSize()); + pageLinkVariables.add(pageLink.getPage()); + if (StringUtils.isNotEmpty(pageLink.getTextSearch())) { + urlTemplate += "&textSearch={textSearch}"; + pageLinkVariables.add(pageLink.getTextSearch()); + } + if (pageLink.getSortOrder() != null) { + urlTemplate += "&sortProperty={sortProperty}&sortOrder={sortOrder}"; + pageLinkVariables.add(pageLink.getSortOrder().getProperty()); + pageLinkVariables.add(pageLink.getSortOrder().getDirection().name()); + } + + Object[] vars = new Object[urlVariables.length + pageLinkVariables.size()]; + System.arraycopy(urlVariables, 0, vars, 0, urlVariables.length); + System.arraycopy(pageLinkVariables.toArray(), 0, vars, urlVariables.length, pageLinkVariables.size()); + + return readResponse(doGet(urlTemplate, vars).andExpect(status().isOk()), responseType); + } + + protected T doGetTypedWithTimePageLink(String urlTemplate, TypeReference responseType, + TimePageLink pageLink, + Object... urlVariables) throws Exception { + List pageLinkVariables = new ArrayList<>(); + urlTemplate += "pageSize={pageSize}&page={page}"; + pageLinkVariables.add(pageLink.getPageSize()); + pageLinkVariables.add(pageLink.getPage()); + if (pageLink.getStartTime() != null) { + urlTemplate += "&startTime={startTime}"; + pageLinkVariables.add(pageLink.getStartTime()); + } + if (pageLink.getEndTime() != null) { + urlTemplate += "&endTime={endTime}"; + pageLinkVariables.add(pageLink.getEndTime()); + } + if (StringUtils.isNotEmpty(pageLink.getTextSearch())) { + urlTemplate += "&textSearch={textSearch}"; + pageLinkVariables.add(pageLink.getTextSearch()); + } + if (pageLink.getSortOrder() != null) { + urlTemplate += "&sortProperty={sortProperty}&sortOrder={sortOrder}"; + pageLinkVariables.add(pageLink.getSortOrder().getProperty()); + pageLinkVariables.add(pageLink.getSortOrder().getDirection().name()); + } + Object[] vars = new Object[urlVariables.length + pageLinkVariables.size()]; + System.arraycopy(urlVariables, 0, vars, 0, urlVariables.length); + System.arraycopy(pageLinkVariables.toArray(), 0, vars, urlVariables.length, pageLinkVariables.size()); + + return readResponse(doGet(urlTemplate, vars).andExpect(status().isOk()), responseType); + } + + protected T doPost(String urlTemplate, Class responseClass, String... params) throws Exception { + return readResponse(doPost(urlTemplate, params).andExpect(status().isOk()), responseClass); + } + + protected T doPost(String urlTemplate, T content, Class responseClass, ResultMatcher resultMatcher, String... params) throws Exception { + return readResponse(doPost(urlTemplate, content, params).andExpect(resultMatcher), responseClass); + } + + protected T doPost(String urlTemplate, T content, Class responseClass, String... params) throws Exception { + return readResponse(doPost(urlTemplate, content, params).andExpect(status().isOk()), responseClass); + } + + protected R doPostWithResponse(String urlTemplate, T content, Class responseClass, String... params) throws Exception { + return readResponse(doPost(urlTemplate, content, params).andExpect(status().isOk()), responseClass); + } + + protected R doPostWithTypedResponse(String urlTemplate, T content, TypeReference responseType, String... params) throws Exception { + return readResponse(doPost(urlTemplate, content, params).andExpect(status().isOk()), responseType); + } + + protected T doPostAsync(String urlTemplate, T content, Class responseClass, ResultMatcher resultMatcher, String... params) throws Exception { + return readResponse(doPostAsync(urlTemplate, content, DEFAULT_TIMEOUT, params).andExpect(resultMatcher), responseClass); + } + + protected T doPostAsync(String urlTemplate, T content, Class responseClass, ResultMatcher resultMatcher, Long timeout, String... params) throws Exception { + return readResponse(doPostAsync(urlTemplate, content, timeout, params).andExpect(resultMatcher), responseClass); + } + + protected T doPostClaimAsync(String urlTemplate, Object content, Class responseClass, ResultMatcher resultMatcher, String... params) throws Exception { + return readResponse(doPostAsync(urlTemplate, content, DEFAULT_TIMEOUT, params).andExpect(resultMatcher), responseClass); + } + + protected T doDelete(String urlTemplate, Class responseClass, String... params) throws Exception { + return readResponse(doDelete(urlTemplate, params).andExpect(status().isOk()), responseClass); + } + + protected ResultActions doPost(String urlTemplate, String... params) throws Exception { + MockHttpServletRequestBuilder postRequest = post(urlTemplate); + setJwtToken(postRequest); + populateParams(postRequest, params); + return mockMvc.perform(postRequest); + } + + protected ResultActions doPost(String urlTemplate, T content, String... params) throws Exception { + MockHttpServletRequestBuilder postRequest = post(urlTemplate, params); + setJwtToken(postRequest); + String json = json(content); + postRequest.contentType(contentType).content(json); + return mockMvc.perform(postRequest); + } + + protected ResultActions doPostAsync(String urlTemplate, T content, Long timeout, String... params) throws Exception { + MockHttpServletRequestBuilder postRequest = post(urlTemplate, params); + setJwtToken(postRequest); + String json = json(content); + postRequest.contentType(contentType).content(json); + MvcResult result = mockMvc.perform(postRequest).andReturn(); + result.getAsyncResult(timeout); + return mockMvc.perform(asyncDispatch(result)); + } + + protected ResultActions doDelete(String urlTemplate, String... params) throws Exception { + MockHttpServletRequestBuilder deleteRequest = delete(urlTemplate); + setJwtToken(deleteRequest); + populateParams(deleteRequest, params); + return mockMvc.perform(deleteRequest); + } + + protected void populateParams(MockHttpServletRequestBuilder request, String... params) { + if (params != null && params.length > 0) { + Assert.assertEquals(0, params.length % 2); + MultiValueMap paramsMap = new LinkedMultiValueMap<>(); + for (int i = 0; i < params.length; i += 2) { + paramsMap.add(params[i], params[i + 1]); + } + request.params(paramsMap); + } + } + + @SuppressWarnings("unchecked") + protected String json(Object o) throws IOException { + MockHttpOutputMessage mockHttpOutputMessage = new MockHttpOutputMessage(); + + HttpMessageConverter converter = o instanceof String ? stringHttpMessageConverter : mappingJackson2HttpMessageConverter; + converter.write(o, MediaType.APPLICATION_JSON, mockHttpOutputMessage); + return mockHttpOutputMessage.getBodyAsString(); + } + + @SuppressWarnings("unchecked") + protected T readResponse(ResultActions result, Class responseClass) throws Exception { + byte[] content = result.andReturn().getResponse().getContentAsByteArray(); + MockHttpInputMessage mockHttpInputMessage = new MockHttpInputMessage(content); + HttpMessageConverter converter = responseClass.equals(String.class) ? stringHttpMessageConverter : mappingJackson2HttpMessageConverter; + return (T) converter.read(responseClass, mockHttpInputMessage); + } + + protected T readResponse(ResultActions result, TypeReference type) throws Exception { + byte[] content = result.andReturn().getResponse().getContentAsByteArray(); + ObjectMapper mapper = new ObjectMapper(); + return mapper.readerFor(type).readValue(content); + } + + public class IdComparator implements Comparator { + @Override + public int compare(D o1, D o2) { + return o1.getId().getId().compareTo(o2.getId().getId()); + } + } + + protected static ResultMatcher statusReason(Matcher matcher) { + return jsonPath("$.message", matcher); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebsocketTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebsocketTest.java new file mode 100644 index 0000000000..5a75e4916a --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebsocketTest.java @@ -0,0 +1,115 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.Jwt; +import io.jsonwebtoken.Jwts; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.hamcrest.Matcher; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.rules.TestRule; +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootContextLoader; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.mock.http.MockHttpInputMessage; +import org.springframework.mock.http.MockHttpOutputMessage; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.context.WebApplicationContext; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UUIDBased; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.config.ThingsboardSecurityConfiguration; +import org.thingsboard.server.service.mail.TestMailService; +import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest; +import org.thingsboard.server.service.security.auth.rest.LoginRequest; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; + +@ActiveProfiles("test") +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = AbstractControllerTest.class, loader = SpringBootContextLoader.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +@Configuration +@ComponentScan({"org.thingsboard.server"}) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Slf4j +public abstract class AbstractWebsocketTest extends AbstractWebTest { + + protected static final String WS_URL = "ws://localhost:"; + + @LocalServerPort + protected int wsPort; + + protected TbTestWebSocketClient buildAndConnectWebSocketClient() throws URISyntaxException, InterruptedException { + TbTestWebSocketClient wsClient = new TbTestWebSocketClient(new URI(WS_URL + wsPort + "/api/ws/plugins/telemetry?token=" + token)); + Assert.assertTrue(wsClient.connectBlocking()); + return wsClient; + } + +} diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java index 24dbcca2db..b0f193aec3 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.controller; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.core.type.TypeReference; import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; @@ -28,8 +28,8 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.service.stats.DefaultRuleEngineStatisticsService; @@ -218,7 +218,7 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { asset.setType("default"); Asset savedAsset = doPost("/api/asset", asset, Asset.class); - doPost("/api/customer/" + UUIDs.timeBased().toString() + doPost("/api/customer/" + Uuids.timeBased().toString() + "/asset/" + savedAsset.getId().getId().toString()) .andExpect(status().isNotFound()); } @@ -272,15 +272,14 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { assets.add(doPost("/api/asset", asset, Asset.class)); } List loadedAssets = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(23); - TextPageData pageData = null; + PageLink pageLink = new PageLink(23); + PageData pageData = null; do { pageData = doGetTypedWithPageLink("/api/tenant/assets?", - new TypeReference>() { - }, pageLink); + new TypeReference>(){}, pageLink); loadedAssets.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -318,15 +317,14 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { } List loadedAssetsTitle1 = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(15, title1); - TextPageData pageData = null; + PageLink pageLink = new PageLink(15, 0, title1); + PageData pageData = null; do { pageData = doGetTypedWithPageLink("/api/tenant/assets?", - new TypeReference>() { - }, pageLink); + new TypeReference>(){}, pageLink); loadedAssetsTitle1.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -336,14 +334,13 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { Assert.assertEquals(assetsTitle1, loadedAssetsTitle1); List loadedAssetsTitle2 = new ArrayList<>(); - pageLink = new TextPageLink(4, title2); + pageLink = new PageLink(4, 0, title2); do { pageData = doGetTypedWithPageLink("/api/tenant/assets?", - new TypeReference>() { - }, pageLink); + new TypeReference>(){}, pageLink); loadedAssetsTitle2.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -357,10 +354,9 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } - pageLink = new TextPageLink(4, title1); + pageLink = new PageLink(4, 0, title1); pageData = doGetTypedWithPageLink("/api/tenant/assets?", - new TypeReference>() { - }, pageLink); + new TypeReference>(){}, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -369,10 +365,9 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } - pageLink = new TextPageLink(4, title2); + pageLink = new PageLink(4, 0, title2); pageData = doGetTypedWithPageLink("/api/tenant/assets?", - new TypeReference>() { - }, pageLink); + new TypeReference>(){}, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); } @@ -405,15 +400,14 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { } List loadedAssetsType1 = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(15); - TextPageData pageData = null; + PageLink pageLink = new PageLink(15); + PageData pageData = null; do { pageData = doGetTypedWithPageLink("/api/tenant/assets?type={type}&", - new TypeReference>() { - }, pageLink, type1); + new TypeReference>(){}, pageLink, type1); loadedAssetsType1.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -423,14 +417,13 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { Assert.assertEquals(assetsType1, loadedAssetsType1); List loadedAssetsType2 = new ArrayList<>(); - pageLink = new TextPageLink(4); + pageLink = new PageLink(4); do { pageData = doGetTypedWithPageLink("/api/tenant/assets?type={type}&", - new TypeReference>() { - }, pageLink, type2); + new TypeReference>(){}, pageLink, type2); loadedAssetsType2.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -444,10 +437,9 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } - pageLink = new TextPageLink(4); + pageLink = new PageLink(4); pageData = doGetTypedWithPageLink("/api/tenant/assets?type={type}&", - new TypeReference>() { - }, pageLink, type1); + new TypeReference>(){}, pageLink, type1); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -456,10 +448,9 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } - pageLink = new TextPageLink(4); + pageLink = new PageLink(4); pageData = doGetTypedWithPageLink("/api/tenant/assets?type={type}&", - new TypeReference>() { - }, pageLink, type2); + new TypeReference>(){}, pageLink, type2); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); } @@ -482,15 +473,14 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { } List loadedAssets = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(23); - TextPageData pageData = null; + PageLink pageLink = new PageLink(23); + PageData pageData = null; do { pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/assets?", - new TypeReference>() { - }, pageLink); + new TypeReference>(){}, pageLink); loadedAssets.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -535,15 +525,14 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { } List loadedAssetsTitle1 = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(15, title1); - TextPageData pageData = null; + PageLink pageLink = new PageLink(15, 0, title1); + PageData pageData = null; do { pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/assets?", - new TypeReference>() { - }, pageLink); + new TypeReference>(){}, pageLink); loadedAssetsTitle1.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -553,14 +542,13 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { Assert.assertEquals(assetsTitle1, loadedAssetsTitle1); List loadedAssetsTitle2 = new ArrayList<>(); - pageLink = new TextPageLink(4, title2); + pageLink = new PageLink(4, 0, title2); do { pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/assets?", - new TypeReference>() { - }, pageLink); + new TypeReference>(){}, pageLink); loadedAssetsTitle2.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -574,10 +562,9 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } - pageLink = new TextPageLink(4, title1); + pageLink = new PageLink(4, 0, title1); pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/assets?", - new TypeReference>() { - }, pageLink); + new TypeReference>(){}, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -586,10 +573,9 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } - pageLink = new TextPageLink(4, title2); + pageLink = new PageLink(4, 0, title2); pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/assets?", - new TypeReference>() { - }, pageLink); + new TypeReference>(){}, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); } @@ -631,15 +617,14 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { } List loadedAssetsType1 = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(15); - TextPageData pageData = null; + PageLink pageLink = new PageLink(15); + PageData pageData = null; do { pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/assets?type={type}&", - new TypeReference>() { - }, pageLink, type1); + new TypeReference>(){}, pageLink, type1); loadedAssetsType1.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -649,14 +634,13 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { Assert.assertEquals(assetsType1, loadedAssetsType1); List loadedAssetsType2 = new ArrayList<>(); - pageLink = new TextPageLink(4); + pageLink = new PageLink(4); do { pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/assets?type={type}&", - new TypeReference>() { - }, pageLink, type2); + new TypeReference>(){}, pageLink, type2); loadedAssetsType2.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -670,10 +654,9 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } - pageLink = new TextPageLink(4); + pageLink = new PageLink(4); pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/assets?type={type}&", - new TypeReference>() { - }, pageLink, type1); + new TypeReference>(){}, pageLink, type1); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -682,10 +665,9 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } - pageLink = new TextPageLink(4); + pageLink = new PageLink(4); pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/assets?type={type}&", - new TypeReference>() { - }, pageLink, type2); + new TypeReference>(){}, pageLink, type2); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java index 5f6b944505..26f3086de4 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java @@ -24,7 +24,7 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.AuditLog; -import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.model.ModelConstants; @@ -77,14 +77,14 @@ public abstract class BaseAuditLogControllerTest extends AbstractControllerTest List loadedAuditLogs = new ArrayList<>(); TimePageLink pageLink = new TimePageLink(23); - TimePageData pageData; + PageData pageData; do { pageData = doGetTypedWithTimePageLink("/api/audit/logs?", - new TypeReference>() { + new TypeReference>() { }, pageLink); loadedAuditLogs.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -94,11 +94,11 @@ public abstract class BaseAuditLogControllerTest extends AbstractControllerTest pageLink = new TimePageLink(23); do { pageData = doGetTypedWithTimePageLink("/api/audit/logs/customer/" + ModelConstants.NULL_UUID + "?", - new TypeReference>() { + new TypeReference>() { }, pageLink); loadedAuditLogs.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -108,11 +108,11 @@ public abstract class BaseAuditLogControllerTest extends AbstractControllerTest pageLink = new TimePageLink(23); do { pageData = doGetTypedWithTimePageLink("/api/audit/logs/user/" + tenantAdmin.getId().getId().toString() + "?", - new TypeReference>() { + new TypeReference>() { }, pageLink); loadedAuditLogs.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -132,14 +132,14 @@ public abstract class BaseAuditLogControllerTest extends AbstractControllerTest List loadedAuditLogs = new ArrayList<>(); TimePageLink pageLink = new TimePageLink(23); - TimePageData pageData; + PageData pageData; do { pageData = doGetTypedWithTimePageLink("/api/audit/logs/entity/DEVICE/" + savedDevice.getId().getId() + "?", - new TypeReference>() { + new TypeReference>() { }, pageLink); loadedAuditLogs.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseCustomerControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseCustomerControllerTest.java index 558600c73e..796477689b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseCustomerControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseCustomerControllerTest.java @@ -29,8 +29,8 @@ import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.junit.Assert; import org.junit.Test; @@ -169,14 +169,13 @@ public abstract class BaseCustomerControllerTest extends AbstractControllerTest } List loadedCustomers = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(23); - TextPageData pageData = null; + PageLink pageLink = new PageLink(23); + PageData pageData = null; do { - pageData = doGetTypedWithPageLink("/api/customers?", new TypeReference>() { - }, pageLink); + pageData = doGetTypedWithPageLink("/api/customers?", new TypeReference>(){}, pageLink); loadedCustomers.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -214,14 +213,13 @@ public abstract class BaseCustomerControllerTest extends AbstractControllerTest } List loadedCustomersTitle1 = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(15, title1); - TextPageData pageData = null; + PageLink pageLink = new PageLink(15, 0, title1); + PageData pageData = null; do { - pageData = doGetTypedWithPageLink("/api/customers?", new TypeReference>() { - }, pageLink); + pageData = doGetTypedWithPageLink("/api/customers?", new TypeReference>(){}, pageLink); loadedCustomersTitle1.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -231,13 +229,12 @@ public abstract class BaseCustomerControllerTest extends AbstractControllerTest Assert.assertEquals(customersTitle1, loadedCustomersTitle1); List loadedCustomersTitle2 = new ArrayList<>(); - pageLink = new TextPageLink(4, title2); + pageLink = new PageLink(4, 0, title2); do { - pageData = doGetTypedWithPageLink("/api/customers?", new TypeReference>() { - }, pageLink); + pageData = doGetTypedWithPageLink("/api/customers?", new TypeReference>(){}, pageLink); loadedCustomersTitle2.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -251,9 +248,8 @@ public abstract class BaseCustomerControllerTest extends AbstractControllerTest .andExpect(status().isOk()); } - pageLink = new TextPageLink(4, title1); - pageData = doGetTypedWithPageLink("/api/customers?", new TypeReference>() { - }, pageLink); + pageLink = new PageLink(4, 0, title1); + pageData = doGetTypedWithPageLink("/api/customers?", new TypeReference>(){}, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -262,9 +258,8 @@ public abstract class BaseCustomerControllerTest extends AbstractControllerTest .andExpect(status().isOk()); } - pageLink = new TextPageLink(4, title2); - pageData = doGetTypedWithPageLink("/api/customers?", new TypeReference>() { - }, pageLink); + pageLink = new PageLink(4, 0, title2); + pageData = doGetTypedWithPageLink("/api/customers?", new TypeReference>(){}, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java index e8fbfe4aa1..57b23d88c8 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java @@ -22,13 +22,12 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import org.apache.commons.lang3.RandomStringUtils; import org.thingsboard.server.common.data.*; import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; -import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.security.Authority; import org.junit.After; @@ -167,7 +166,7 @@ public abstract class BaseDashboardControllerTest extends AbstractControllerTest dashboard.setTitle("My dashboard"); Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class); - doPost("/api/customer/" + UUIDs.timeBased().toString() + doPost("/api/customer/" + Uuids.timeBased().toString() + "/dashboard/" + savedDashboard.getId().getId().toString()) .andExpect(status().isNotFound()); } @@ -219,14 +218,14 @@ public abstract class BaseDashboardControllerTest extends AbstractControllerTest dashboards.add(new DashboardInfo(doPost("/api/dashboard", dashboard, Dashboard.class))); } List loadedDashboards = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(24); - TextPageData pageData = null; + PageLink pageLink = new PageLink(24); + PageData pageData = null; do { pageData = doGetTypedWithPageLink("/api/tenant/dashboards?", - new TypeReference>(){}, pageLink); + new TypeReference>(){}, pageLink); loadedDashboards.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -260,14 +259,14 @@ public abstract class BaseDashboardControllerTest extends AbstractControllerTest } List loadedDashboardsTitle1 = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(15, title1); - TextPageData pageData = null; + PageLink pageLink = new PageLink(15, 0, title1); + PageData pageData = null; do { pageData = doGetTypedWithPageLink("/api/tenant/dashboards?", - new TypeReference>(){}, pageLink); + new TypeReference>(){}, pageLink); loadedDashboardsTitle1.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -277,13 +276,13 @@ public abstract class BaseDashboardControllerTest extends AbstractControllerTest Assert.assertEquals(dashboardsTitle1, loadedDashboardsTitle1); List loadedDashboardsTitle2 = new ArrayList<>(); - pageLink = new TextPageLink(4, title2); + pageLink = new PageLink(4, 0, title2); do { pageData = doGetTypedWithPageLink("/api/tenant/dashboards?", - new TypeReference>(){}, pageLink); + new TypeReference>(){}, pageLink); loadedDashboardsTitle2.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -297,9 +296,9 @@ public abstract class BaseDashboardControllerTest extends AbstractControllerTest .andExpect(status().isOk()); } - pageLink = new TextPageLink(4, title1); + pageLink = new PageLink(4, 0, title1); pageData = doGetTypedWithPageLink("/api/tenant/dashboards?", - new TypeReference>(){}, pageLink); + new TypeReference>(){}, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -308,9 +307,9 @@ public abstract class BaseDashboardControllerTest extends AbstractControllerTest .andExpect(status().isOk()); } - pageLink = new TextPageLink(4, title2); + pageLink = new PageLink(4, 0, title2); pageData = doGetTypedWithPageLink("/api/tenant/dashboards?", - new TypeReference>(){}, pageLink); + new TypeReference>(){}, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); } @@ -332,14 +331,14 @@ public abstract class BaseDashboardControllerTest extends AbstractControllerTest } List loadedDashboards = new ArrayList<>(); - TimePageLink pageLink = new TimePageLink(21); - TimePageData pageData = null; + PageLink pageLink = new PageLink(21); + PageData pageData = null; do { - pageData = doGetTypedWithTimePageLink("/api/customer/" + customerId.getId().toString() + "/dashboards?", - new TypeReference>(){}, pageLink); + pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/dashboards?", + new TypeReference>(){}, pageLink); loadedDashboards.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java index 99de423086..72a885b062 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.controller; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.core.type.TypeReference; import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; @@ -24,14 +24,15 @@ import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceCredentialsId; import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.security.Authority; @@ -185,9 +186,8 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { public void testSaveDeviceWithEmptyType() throws Exception { Device device = new Device(); device.setName("My device"); - doPost("/api/device", device) - .andExpect(status().isBadRequest()) - .andExpect(statusReason(containsString("Device type should be specified"))); + Device savedDevice = doPost("/api/device", device, Device.class); + Assert.assertEquals("default", savedDevice.getType()); } @Test @@ -231,8 +231,7 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { device.setName("My device"); device.setType("default"); Device savedDevice = doPost("/api/device", device, Device.class); - - doPost("/api/customer/" + UUIDs.timeBased().toString() + doPost("/api/customer/" + Uuids.timeBased().toString() + "/device/" + savedDevice.getId().getId().toString()) .andExpect(status().isNotFound()); } @@ -350,7 +349,7 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { Device savedDevice = doPost("/api/device", device, Device.class); DeviceCredentials deviceCredentials = doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class); - DeviceCredentials newDeviceCredentials = new DeviceCredentials(new DeviceCredentialsId(UUIDs.timeBased())); + DeviceCredentials newDeviceCredentials = new DeviceCredentials(new DeviceCredentialsId(Uuids.timeBased())); newDeviceCredentials.setCreatedTime(deviceCredentials.getCreatedTime()); newDeviceCredentials.setDeviceId(deviceCredentials.getDeviceId()); newDeviceCredentials.setCredentialsType(deviceCredentials.getCredentialsType()); @@ -368,7 +367,7 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { Device savedDevice = doPost("/api/device", device, Device.class); DeviceCredentials deviceCredentials = doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class); - deviceCredentials.setDeviceId(new DeviceId(UUIDs.timeBased())); + deviceCredentials.setDeviceId(new DeviceId(Uuids.timeBased())); doPost("/api/device/credentials", deviceCredentials) .andExpect(status().isNotFound()); } @@ -383,15 +382,15 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { devices.add(doPost("/api/device", device, Device.class)); } List loadedDevices = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(23); - TextPageData pageData = null; + PageLink pageLink = new PageLink(23); + PageData pageData = null; do { pageData = doGetTypedWithPageLink("/api/tenant/devices?", - new TypeReference>() { - }, pageLink); + new TypeReference>(){}, pageLink); + loadedDevices.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -427,15 +426,14 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { } List loadedDevicesTitle1 = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(15, title1); - TextPageData pageData = null; + PageLink pageLink = new PageLink(15, 0, title1); + PageData pageData = null; do { pageData = doGetTypedWithPageLink("/api/tenant/devices?", - new TypeReference>() { - }, pageLink); + new TypeReference>(){}, pageLink); loadedDevicesTitle1.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -445,14 +443,13 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { Assert.assertEquals(devicesTitle1, loadedDevicesTitle1); List loadedDevicesTitle2 = new ArrayList<>(); - pageLink = new TextPageLink(4, title2); + pageLink = new PageLink(4, 0, title2); do { pageData = doGetTypedWithPageLink("/api/tenant/devices?", - new TypeReference>() { - }, pageLink); + new TypeReference>(){}, pageLink); loadedDevicesTitle2.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -465,11 +462,9 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { doDelete("/api/device/" + device.getId().getId().toString()) .andExpect(status().isOk()); } - - pageLink = new TextPageLink(4, title1); - pageData = doGetTypedWithPageLink("/api/tenant/devices?", - new TypeReference>() { - }, pageLink); + pageLink = new PageLink(4, 0, title1); + pageData = doGetTypedWithPageLink("/api/tenant/devices?", + new TypeReference>(){}, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -477,11 +472,9 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { doDelete("/api/device/" + device.getId().getId().toString()) .andExpect(status().isOk()); } - - pageLink = new TextPageLink(4, title2); - pageData = doGetTypedWithPageLink("/api/tenant/devices?", - new TypeReference>() { - }, pageLink); + pageLink = new PageLink(4, 0, title2); + pageData = doGetTypedWithPageLink("/api/tenant/devices?", + new TypeReference>(){}, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); } @@ -514,15 +507,14 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { } List loadedDevicesType1 = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(15); - TextPageData pageData = null; + PageLink pageLink = new PageLink(15); + PageData pageData = null; do { pageData = doGetTypedWithPageLink("/api/tenant/devices?type={type}&", - new TypeReference>() { - }, pageLink, type1); + new TypeReference>(){}, pageLink, type1); loadedDevicesType1.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -532,14 +524,13 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { Assert.assertEquals(devicesType1, loadedDevicesType1); List loadedDevicesType2 = new ArrayList<>(); - pageLink = new TextPageLink(4); + pageLink = new PageLink(4); do { pageData = doGetTypedWithPageLink("/api/tenant/devices?type={type}&", - new TypeReference>() { - }, pageLink, type2); + new TypeReference>(){}, pageLink, type2); loadedDevicesType2.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -553,10 +544,9 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } - pageLink = new TextPageLink(4); + pageLink = new PageLink(4); pageData = doGetTypedWithPageLink("/api/tenant/devices?type={type}&", - new TypeReference>() { - }, pageLink, type1); + new TypeReference>(){}, pageLink, type1); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -565,10 +555,9 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } - pageLink = new TextPageLink(4); + pageLink = new PageLink(4); pageData = doGetTypedWithPageLink("/api/tenant/devices?type={type}&", - new TypeReference>() { - }, pageLink, type2); + new TypeReference>(){}, pageLink, type2); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); } @@ -591,15 +580,14 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { } List loadedDevices = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(23); - TextPageData pageData = null; + PageLink pageLink = new PageLink(23); + PageData pageData = null; do { pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?", - new TypeReference>() { - }, pageLink); + new TypeReference>(){}, pageLink); loadedDevices.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -644,15 +632,14 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { } List loadedDevicesTitle1 = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(15, title1); - TextPageData pageData = null; + PageLink pageLink = new PageLink(15, 0, title1); + PageData pageData = null; do { pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?", - new TypeReference>() { - }, pageLink); + new TypeReference>(){}, pageLink); loadedDevicesTitle1.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -662,14 +649,13 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { Assert.assertEquals(devicesTitle1, loadedDevicesTitle1); List loadedDevicesTitle2 = new ArrayList<>(); - pageLink = new TextPageLink(4, title2); + pageLink = new PageLink(4, 0, title2); do { pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?", - new TypeReference>() { - }, pageLink); + new TypeReference>(){}, pageLink); loadedDevicesTitle2.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -682,11 +668,9 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { doDelete("/api/customer/device/" + device.getId().getId().toString()) .andExpect(status().isOk()); } - - pageLink = new TextPageLink(4, title1); - pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?", - new TypeReference>() { - }, pageLink); + pageLink = new PageLink(4, 0, title1); + pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?", + new TypeReference>(){}, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -694,11 +678,9 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { doDelete("/api/customer/device/" + device.getId().getId().toString()) .andExpect(status().isOk()); } - - pageLink = new TextPageLink(4, title2); - pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?", - new TypeReference>() { - }, pageLink); + pageLink = new PageLink(4, 0, title2); + pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?", + new TypeReference>(){}, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); } @@ -740,15 +722,14 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { } List loadedDevicesType1 = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(15); - TextPageData pageData = null; + PageLink pageLink = new PageLink(15); + PageData pageData = null; do { pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?type={type}&", - new TypeReference>() { - }, pageLink, type1); + new TypeReference>(){}, pageLink, type1); loadedDevicesType1.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -758,14 +739,13 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { Assert.assertEquals(devicesType1, loadedDevicesType1); List loadedDevicesType2 = new ArrayList<>(); - pageLink = new TextPageLink(4); + pageLink = new PageLink(4); do { pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?type={type}&", - new TypeReference>() { - }, pageLink, type2); + new TypeReference>(){}, pageLink, type2); loadedDevicesType2.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -779,10 +759,9 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } - pageLink = new TextPageLink(4); + pageLink = new PageLink(4); pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?type={type}&", - new TypeReference>() { - }, pageLink, type1); + new TypeReference>(){}, pageLink, type1); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -791,10 +770,9 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } - pageLink = new TextPageLink(4); + pageLink = new PageLink(4); pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?type={type}&", - new TypeReference>() { - }, pageLink, type2); + new TypeReference>(){}, pageLink, type2); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java new file mode 100644 index 0000000000..3376c16573 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java @@ -0,0 +1,321 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileInfo; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.device.profile.ProvisionDeviceProfileCredentials; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.security.Authority; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public abstract class BaseDeviceProfileControllerTest extends AbstractControllerTest { + + private IdComparator idComparator = new IdComparator<>(); + private IdComparator deviceProfileInfoIdComparator = new IdComparator<>(); + + private Tenant savedTenant; + private User tenantAdmin; + + @Before + public void beforeTest() throws Exception { + loginSysAdmin(); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + savedTenant = doPost("/api/tenant", tenant, Tenant.class); + Assert.assertNotNull(savedTenant); + + tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant2@thingsboard.org"); + tenantAdmin.setFirstName("Joe"); + tenantAdmin.setLastName("Downs"); + + tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); + } + + @After + public void afterTest() throws Exception { + loginSysAdmin(); + + doDelete("/api/tenant/" + savedTenant.getId().getId().toString()) + .andExpect(status().isOk()); + } + + @Test + public void testSaveDeviceProfile() throws Exception { + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); + DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + Assert.assertNotNull(savedDeviceProfile); + Assert.assertNotNull(savedDeviceProfile.getId()); + Assert.assertTrue(savedDeviceProfile.getCreatedTime() > 0); + Assert.assertEquals(deviceProfile.getName(), savedDeviceProfile.getName()); + Assert.assertEquals(deviceProfile.getDescription(), savedDeviceProfile.getDescription()); + Assert.assertEquals(deviceProfile.getProfileData(), savedDeviceProfile.getProfileData()); + Assert.assertEquals(deviceProfile.isDefault(), savedDeviceProfile.isDefault()); + Assert.assertEquals(deviceProfile.getDefaultRuleChainId(), savedDeviceProfile.getDefaultRuleChainId()); + savedDeviceProfile.setName("New device profile"); + doPost("/api/deviceProfile", savedDeviceProfile, DeviceProfile.class); + DeviceProfile foundDeviceProfile = doGet("/api/deviceProfile/"+savedDeviceProfile.getId().getId().toString(), DeviceProfile.class); + Assert.assertEquals(savedDeviceProfile.getName(), foundDeviceProfile.getName()); + } + + @Test + public void testFindDeviceProfileById() throws Exception { + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); + DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile foundDeviceProfile = doGet("/api/deviceProfile/"+savedDeviceProfile.getId().getId().toString(), DeviceProfile.class); + Assert.assertNotNull(foundDeviceProfile); + Assert.assertEquals(savedDeviceProfile, foundDeviceProfile); + } + + @Test + public void testFindDeviceProfileInfoById() throws Exception { + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); + DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfileInfo foundDeviceProfileInfo = doGet("/api/deviceProfileInfo/"+savedDeviceProfile.getId().getId().toString(), DeviceProfileInfo.class); + Assert.assertNotNull(foundDeviceProfileInfo); + Assert.assertEquals(savedDeviceProfile.getId(), foundDeviceProfileInfo.getId()); + Assert.assertEquals(savedDeviceProfile.getName(), foundDeviceProfileInfo.getName()); + Assert.assertEquals(savedDeviceProfile.getType(), foundDeviceProfileInfo.getType()); + } + + @Test + public void testFindDefaultDeviceProfileInfo() throws Exception { + DeviceProfileInfo foundDefaultDeviceProfileInfo = doGet("/api/deviceProfileInfo/default", DeviceProfileInfo.class); + Assert.assertNotNull(foundDefaultDeviceProfileInfo); + Assert.assertNotNull(foundDefaultDeviceProfileInfo.getId()); + Assert.assertNotNull(foundDefaultDeviceProfileInfo.getName()); + Assert.assertNotNull(foundDefaultDeviceProfileInfo.getType()); + Assert.assertEquals(DeviceProfileType.DEFAULT, foundDefaultDeviceProfileInfo.getType()); + Assert.assertEquals("default", foundDefaultDeviceProfileInfo.getName()); + } + + @Test + public void testSetDefaultDeviceProfile() throws Exception { + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile 1"); + DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile defaultDeviceProfile = doPost("/api/deviceProfile/"+savedDeviceProfile.getId().getId().toString()+"/default", null, DeviceProfile.class); + Assert.assertNotNull(defaultDeviceProfile); + DeviceProfileInfo foundDefaultDeviceProfile = doGet("/api/deviceProfileInfo/default", DeviceProfileInfo.class); + Assert.assertNotNull(foundDefaultDeviceProfile); + Assert.assertEquals(savedDeviceProfile.getName(), foundDefaultDeviceProfile.getName()); + Assert.assertEquals(savedDeviceProfile.getId(), foundDefaultDeviceProfile.getId()); + Assert.assertEquals(savedDeviceProfile.getType(), foundDefaultDeviceProfile.getType()); + } + + @Test + public void testSaveDeviceProfileWithEmptyName() throws Exception { + DeviceProfile deviceProfile = new DeviceProfile(); + doPost("/api/deviceProfile", deviceProfile).andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Device profile name should be specified"))); + } + + @Test + public void testSaveDeviceProfileWithSameName() throws Exception { + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); + doPost("/api/deviceProfile", deviceProfile).andExpect(status().isOk()); + DeviceProfile deviceProfile2 = this.createDeviceProfile("Device Profile"); + doPost("/api/deviceProfile", deviceProfile2).andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Device profile with such name already exists"))); + } + + @Test + public void testSaveDeviceProfileWithSameProvisionDeviceKey() throws Exception { + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); + deviceProfile.setProvisionDeviceKey("testProvisionDeviceKey"); + doPost("/api/deviceProfile", deviceProfile).andExpect(status().isOk()); + DeviceProfile deviceProfile2 = this.createDeviceProfile("Device Profile 2"); + deviceProfile2.setProvisionDeviceKey("testProvisionDeviceKey"); + doPost("/api/deviceProfile", deviceProfile2).andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Device profile with such provision device key already exists"))); + } + + @Ignore + @Test + public void testChangeDeviceProfileTypeWithExistingDevices() throws Exception { + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); + DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + Device device = new Device(); + device.setName("Test device"); + device.setType("default"); + device.setDeviceProfileId(savedDeviceProfile.getId()); + doPost("/api/device", device, Device.class); + //TODO uncomment once we have other device types; + //savedDeviceProfile.setType(DeviceProfileType.LWM2M); + doPost("/api/deviceProfile", savedDeviceProfile).andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Can't change device profile type because devices referenced it"))); + } + + @Test + public void testChangeDeviceProfileTransportTypeWithExistingDevices() throws Exception { + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); + DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + Device device = new Device(); + device.setName("Test device"); + device.setType("default"); + device.setDeviceProfileId(savedDeviceProfile.getId()); + doPost("/api/device", device, Device.class); + savedDeviceProfile.setTransportType(DeviceTransportType.MQTT); + doPost("/api/deviceProfile", savedDeviceProfile).andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Can't change device profile transport type because devices referenced it"))); + } + + @Test + public void testDeleteDeviceProfileWithExistingDevice() throws Exception { + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); + DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + + Device device = new Device(); + device.setName("Test device"); + device.setType("default"); + device.setDeviceProfileId(savedDeviceProfile.getId()); + + Device savedDevice = doPost("/api/device", device, Device.class); + + doDelete("/api/deviceProfile/" + savedDeviceProfile.getId().getId().toString()) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("The device profile referenced by the devices cannot be deleted"))); + } + + @Test + public void testDeleteDeviceProfile() throws Exception { + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); + DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + + doDelete("/api/deviceProfile/" + savedDeviceProfile.getId().getId().toString()) + .andExpect(status().isOk()); + + doGet("/api/deviceProfile/" + savedDeviceProfile.getId().getId().toString()) + .andExpect(status().isNotFound()); + } + + @Test + public void testFindDeviceProfiles() throws Exception { + List deviceProfiles = new ArrayList<>(); + PageLink pageLink = new PageLink(17); + PageData pageData = doGetTypedWithPageLink("/api/deviceProfiles?", + new TypeReference>(){}, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(1, pageData.getTotalElements()); + deviceProfiles.addAll(pageData.getData()); + + for (int i=0;i<28;i++) { + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"+i); + deviceProfiles.add(doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class)); + } + + List loadedDeviceProfiles = new ArrayList<>(); + pageLink = new PageLink(17); + do { + pageData = doGetTypedWithPageLink("/api/deviceProfiles?", + new TypeReference>(){}, pageLink); + loadedDeviceProfiles.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(deviceProfiles, idComparator); + Collections.sort(loadedDeviceProfiles, idComparator); + + Assert.assertEquals(deviceProfiles, loadedDeviceProfiles); + + for (DeviceProfile deviceProfile : loadedDeviceProfiles) { + if (!deviceProfile.isDefault()) { + doDelete("/api/deviceProfile/" + deviceProfile.getId().getId().toString()) + .andExpect(status().isOk()); + } + } + + pageLink = new PageLink(17); + pageData = doGetTypedWithPageLink("/api/deviceProfiles?", + new TypeReference>(){}, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(1, pageData.getTotalElements()); + } + + @Test + public void testFindDeviceProfileInfos() throws Exception { + List deviceProfiles = new ArrayList<>(); + PageLink pageLink = new PageLink(17); + PageData deviceProfilePageData = doGetTypedWithPageLink("/api/deviceProfiles?", + new TypeReference>(){}, pageLink); + Assert.assertFalse(deviceProfilePageData.hasNext()); + Assert.assertEquals(1, deviceProfilePageData.getTotalElements()); + deviceProfiles.addAll(deviceProfilePageData.getData()); + + for (int i=0;i<28;i++) { + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"+i); + deviceProfiles.add(doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class)); + } + + List loadedDeviceProfileInfos = new ArrayList<>(); + pageLink = new PageLink(17); + PageData pageData; + do { + pageData = doGetTypedWithPageLink("/api/deviceProfileInfos?", + new TypeReference>(){}, pageLink); + loadedDeviceProfileInfos.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(deviceProfiles, idComparator); + Collections.sort(loadedDeviceProfileInfos, deviceProfileInfoIdComparator); + + List deviceProfileInfos = deviceProfiles.stream().map(deviceProfile -> new DeviceProfileInfo(deviceProfile.getId(), + deviceProfile.getName(), deviceProfile.getType(), deviceProfile.getTransportType())).collect(Collectors.toList()); + + Assert.assertEquals(deviceProfileInfos, loadedDeviceProfileInfos); + + for (DeviceProfile deviceProfile : deviceProfiles) { + if (!deviceProfile.isDefault()) { + doDelete("/api/deviceProfile/" + deviceProfile.getId().getId().toString()) + .andExpect(status().isOk()); + } + } + + pageLink = new PageLink(17); + pageData = doGetTypedWithPageLink("/api/deviceProfileInfos?", + new TypeReference>(){}, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(1, pageData.getTotalElements()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java new file mode 100644 index 0000000000..4c589da4db --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java @@ -0,0 +1,288 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import org.apache.http.conn.ssl.TrustStrategy; +import org.apache.http.ssl.SSLContextBuilder; +import org.apache.http.ssl.SSLContexts; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.boot.web.server.LocalServerPort; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.Aggregation; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.DeviceTypeFilter; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityListFilter; +import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.NumericFilterPredicate; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityHistoryCmd; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.stream.Collectors; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public abstract class BaseEntityQueryControllerTest extends AbstractControllerTest { + + private Tenant savedTenant; + private User tenantAdmin; + + @Before + public void beforeTest() throws Exception { + loginSysAdmin(); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + savedTenant = doPost("/api/tenant", tenant, Tenant.class); + Assert.assertNotNull(savedTenant); + + tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant2@thingsboard.org"); + tenantAdmin.setFirstName("Joe"); + tenantAdmin.setLastName("Downs"); + + tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); + } + + @After + public void afterTest() throws Exception { + loginSysAdmin(); + + doDelete("/api/tenant/" + savedTenant.getId().getId().toString()) + .andExpect(status().isOk()); + } + + @Test + public void testCountEntitiesByQuery() throws Exception { + List devices = new ArrayList<>(); + for (int i = 0; i < 97; i++) { + Device device = new Device(); + device.setName("Device" + i); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + devices.add(doPost("/api/device", device, Device.class)); + Thread.sleep(1); + } + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceType("default"); + filter.setDeviceNameFilter(""); + + EntityCountQuery countQuery = new EntityCountQuery(filter); + + Long count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class); + Assert.assertEquals(97, count.longValue()); + + filter.setDeviceType("unknown"); + count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class); + Assert.assertEquals(0, count.longValue()); + + filter.setDeviceType("default"); + filter.setDeviceNameFilter("Device1"); + + count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class); + Assert.assertEquals(11, count.longValue()); + + EntityListFilter entityListFilter = new EntityListFilter(); + entityListFilter.setEntityType(EntityType.DEVICE); + entityListFilter.setEntityList(devices.stream().map(Device::getId).map(DeviceId::toString).collect(Collectors.toList())); + + countQuery = new EntityCountQuery(entityListFilter); + + count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class); + Assert.assertEquals(97, count.longValue()); + } + + @Test + public void testSimpleFindEntityDataByQuery() throws Exception { + List devices = new ArrayList<>(); + for (int i = 0; i < 97; i++) { + Device device = new Device(); + device.setName("Device" + i); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + devices.add(doPost("/api/device", device, Device.class)); + Thread.sleep(1); + } + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceType("default"); + filter.setDeviceNameFilter(""); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC + ); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, null); + + PageData data = + doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() { + }); + + Assert.assertEquals(97, data.getTotalElements()); + Assert.assertEquals(10, data.getTotalPages()); + Assert.assertTrue(data.hasNext()); + Assert.assertEquals(10, data.getData().size()); + + List loadedEntities = new ArrayList<>(data.getData()); + while (data.hasNext()) { + query = query.next(); + data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() { + }); + loadedEntities.addAll(data.getData()); + } + Assert.assertEquals(97, loadedEntities.size()); + + List loadedIds = loadedEntities.stream().map(EntityData::getEntityId).collect(Collectors.toList()); + List deviceIds = devices.stream().map(Device::getId).collect(Collectors.toList()); + + Assert.assertEquals(deviceIds, loadedIds); + + List loadedNames = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()).collect(Collectors.toList()); + List deviceNames = devices.stream().map(Device::getName).collect(Collectors.toList()); + + Assert.assertEquals(deviceNames, loadedNames); + + sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), EntityDataSortOrder.Direction.DESC + ); + + pageLink = new EntityDataPageLink(10, 0, "device1", sortOrder); + query = new EntityDataQuery(filter, pageLink, entityFields, null, null); + data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() { + }); + Assert.assertEquals(11, data.getTotalElements()); + Assert.assertEquals("Device19", data.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); + + } + + @Test + public void testFindEntityDataByQueryWithAttributes() throws Exception { + + List devices = new ArrayList<>(); + List temperatures = new ArrayList<>(); + List highTemperatures = new ArrayList<>(); + for (int i = 0; i < 67; i++) { + Device device = new Device(); + String name = "Device" + i; + device.setName(name); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + devices.add(doPost("/api/device?accessToken=" + name, device, Device.class)); + Thread.sleep(1); + long temperature = (long) (Math.random() * 100); + temperatures.add(temperature); + if (temperature > 45) { + highTemperatures.add(temperature); + } + } + for (int i = 0; i < devices.size(); i++) { + Device device = devices.get(i); + String payload = "{\"temperature\":" + temperatures.get(i) + "}"; + doPost("/api/plugins/telemetry/" + device.getId() + "/" + DataConstants.SHARED_SCOPE, payload, String.class, status().isOk()); + } + Thread.sleep(1000); + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceType("default"); + filter.setDeviceNameFilter(""); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC + ); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + List latestValues = Collections.singletonList(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null); + PageData data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() { + }); + + List loadedEntities = new ArrayList<>(data.getData()); + while (data.hasNext()) { + query = query.next(); + data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() { + }); + loadedEntities.addAll(data.getData()); + } + Assert.assertEquals(67, loadedEntities.size()); + + List loadedTemperatures = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList()); + List deviceTemperatures = temperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); + Assert.assertEquals(deviceTemperatures, loadedTemperatures); + + pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + KeyFilter highTemperatureFilter = new KeyFilter(); + highTemperatureFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + NumericFilterPredicate predicate = new NumericFilterPredicate(); + predicate.setValue(FilterPredicateValue.fromDouble(45)); + predicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + highTemperatureFilter.setPredicate(predicate); + List keyFilters = Collections.singletonList(highTemperatureFilter); + + query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters); + + data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() { + }); + loadedEntities = new ArrayList<>(data.getData()); + while (data.hasNext()) { + query = query.next(); + data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() { + }); + loadedEntities.addAll(data.getData()); + } + Assert.assertEquals(highTemperatures.size(), loadedEntities.size()); + + List loadedHighTemperatures = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList()); + List deviceHighTemperatures = highTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); + + Assert.assertEquals(deviceHighTemperatures, loadedHighTemperatures); + + } +} diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java index bec1b55a79..5aeb12699b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java @@ -15,27 +15,24 @@ */ package org.thingsboard.server.controller; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.core.type.TypeReference; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.RandomStringUtils; import org.eclipse.paho.client.mqttv3.MqttAsyncClient; import org.eclipse.paho.client.mqttv3.MqttConnectOptions; import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.EntityView; -import org.thingsboard.server.common.data.Tenant; -import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.*; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.objects.AttributesEntityView; import org.thingsboard.server.common.data.objects.TelemetryEntityView; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.dao.model.ModelConstants; @@ -188,7 +185,7 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes @Test public void testAssignEntityViewToNonExistentCustomer() throws Exception { EntityView savedView = getNewSavedEntityView("Test entity view"); - doPost("/api/customer/" + UUIDs.timeBased().toString() + "/device/" + savedView.getId().getId().toString()) + doPost("/api/customer/" + Uuids.timeBased().toString() + "/device/" + savedView.getId().getId().toString()) .andExpect(status().isNotFound()); } @@ -226,16 +223,20 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes @Test public void testGetCustomerEntityViews() throws Exception { - CustomerId customerId = doPost("/api/customer", getNewCustomer("Test customer"), Customer.class).getId(); - String urlTemplate = "/api/customer/" + customerId.getId().toString() + "/entityViews?"; + Customer customer = doPost("/api/customer", getNewCustomer("Test customer"), Customer.class); + CustomerId customerId = customer.getId(); + String urlTemplate = "/api/customer/" + customerId.getId().toString() + "/entityViewInfos?"; - List views = new ArrayList<>(); + List views = new ArrayList<>(); for (int i = 0; i < 128; i++) { - views.add(doPost("/api/customer/" + customerId.getId().toString() + "/entityView/" - + getNewSavedEntityView("Test entity view " + i).getId().getId().toString(), EntityView.class)); + views.add( + new EntityViewInfo(doPost("/api/customer/" + customerId.getId().toString() + "/entityView/" + + getNewSavedEntityView("Test entity view " + i).getId().getId().toString(), EntityView.class), + customer.getTitle(), customer.isPublic()) + ); } - List loadedViews = loadListOf(new TextPageLink(23), urlTemplate); + List loadedViews = loadListOfInfo(new PageLink(23), urlTemplate); Collections.sort(views, idComparator); Collections.sort(loadedViews, idComparator); @@ -251,7 +252,7 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes String name1 = "Entity view name1"; List namesOfView1 = fillListOf(125, name1, "/api/customer/" + customerId.getId().toString() + "/entityView/"); - List loadedNamesOfView1 = loadListOf(new TextPageLink(15, name1), urlTemplate); + List loadedNamesOfView1 = loadListOf(new PageLink(15, 0, name1), urlTemplate); Collections.sort(namesOfView1, idComparator); Collections.sort(loadedNamesOfView1, idComparator); assertEquals(namesOfView1, loadedNamesOfView1); @@ -259,7 +260,7 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes String name2 = "Entity view name2"; List NamesOfView2 = fillListOf(143, name2, "/api/customer/" + customerId.getId().toString() + "/entityView/"); - List loadedNamesOfView2 = loadListOf(new TextPageLink(4, name2), urlTemplate); + List loadedNamesOfView2 = loadListOf(new PageLink(4, 0, name2), urlTemplate); Collections.sort(NamesOfView2, idComparator); Collections.sort(loadedNamesOfView2, idComparator); assertEquals(NamesOfView2, loadedNamesOfView2); @@ -267,18 +268,18 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes for (EntityView view : loadedNamesOfView1) { doDelete("/api/customer/entityView/" + view.getId().getId().toString()).andExpect(status().isOk()); } - TextPageData pageData = doGetTypedWithPageLink(urlTemplate, - new TypeReference>() { - }, new TextPageLink(4, name1)); + PageData pageData = doGetTypedWithPageLink(urlTemplate, + new TypeReference>() { + }, new PageLink(4, 0, name1)); Assert.assertFalse(pageData.hasNext()); assertEquals(0, pageData.getData().size()); for (EntityView view : loadedNamesOfView2) { doDelete("/api/customer/entityView/" + view.getId().getId().toString()).andExpect(status().isOk()); } - pageData = doGetTypedWithPageLink(urlTemplate, new TypeReference>() { + pageData = doGetTypedWithPageLink(urlTemplate, new TypeReference>() { }, - new TextPageLink(4, name2)); + new PageLink(4, 0, name2)); Assert.assertFalse(pageData.hasNext()); assertEquals(0, pageData.getData().size()); } @@ -286,11 +287,11 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes @Test public void testGetTenantEntityViews() throws Exception { - List views = new ArrayList<>(); + List views = new ArrayList<>(); for (int i = 0; i < 178; i++) { - views.add(getNewSavedEntityView("Test entity view" + i)); + views.add(new EntityViewInfo(getNewSavedEntityView("Test entity view" + i), null, false)); } - List loadedViews = loadListOf(new TextPageLink(23), "/api/tenant/entityViews?"); + List loadedViews = loadListOfInfo(new PageLink(23), "/api/tenant/entityViewInfos?"); Collections.sort(views, idComparator); Collections.sort(loadedViews, idComparator); @@ -302,14 +303,14 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes public void testGetTenantEntityViewsByName() throws Exception { String name1 = "Entity view name1"; List namesOfView1 = fillListOf(143, name1); - List loadedNamesOfView1 = loadListOf(new TextPageLink(15, name1), "/api/tenant/entityViews?"); + List loadedNamesOfView1 = loadListOf(new PageLink(15, 0, name1), "/api/tenant/entityViews?"); Collections.sort(namesOfView1, idComparator); Collections.sort(loadedNamesOfView1, idComparator); assertEquals(namesOfView1, loadedNamesOfView1); String name2 = "Entity view name2"; List NamesOfView2 = fillListOf(75, name2); - List loadedNamesOfView2 = loadListOf(new TextPageLink(4, name2), "/api/tenant/entityViews?"); + List loadedNamesOfView2 = loadListOf(new PageLink(4, 0, name2), "/api/tenant/entityViews?"); Collections.sort(NamesOfView2, idComparator); Collections.sort(loadedNamesOfView2, idComparator); assertEquals(NamesOfView2, loadedNamesOfView2); @@ -317,18 +318,18 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes for (EntityView view : loadedNamesOfView1) { doDelete("/api/entityView/" + view.getId().getId().toString()).andExpect(status().isOk()); } - TextPageData pageData = doGetTypedWithPageLink("/api/tenant/entityViews?", - new TypeReference>() { - }, new TextPageLink(4, name1)); + PageData pageData = doGetTypedWithPageLink("/api/tenant/entityViews?", + new TypeReference>() { + }, new PageLink(4, 0, name1)); Assert.assertFalse(pageData.hasNext()); assertEquals(0, pageData.getData().size()); for (EntityView view : loadedNamesOfView2) { doDelete("/api/entityView/" + view.getId().getId().toString()).andExpect(status().isOk()); } - pageData = doGetTypedWithPageLink("/api/tenant/entityViews?", new TypeReference>() { + pageData = doGetTypedWithPageLink("/api/tenant/entityViews?", new TypeReference>() { }, - new TextPageLink(4, name2)); + new PageLink(4, 0, name2)); Assert.assertFalse(pageData.hasNext()); assertEquals(0, pageData.getData().size()); } @@ -424,7 +425,7 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes assertNotNull(accessToken); String clientId = MqttAsyncClient.generateClientId(); - MqttAsyncClient client = new MqttAsyncClient("tcp://localhost:1883", clientId); + MqttAsyncClient client = new MqttAsyncClient("tcp://localhost:1883", clientId, new MemoryPersistence()); MqttConnectOptions options = new MqttConnectOptions(); options.setUserName(accessToken); @@ -466,7 +467,7 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes assertNotNull(accessToken); String clientId = MqttAsyncClient.generateClientId(); - MqttAsyncClient client = new MqttAsyncClient("tcp://localhost:1883", clientId); + MqttAsyncClient client = new MqttAsyncClient("tcp://localhost:1883", clientId, new MemoryPersistence()); MqttConnectOptions options = new MqttConnectOptions(); options.setUserName(accessToken); @@ -538,15 +539,30 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes return viewNames; } - private List loadListOf(TextPageLink pageLink, String urlTemplate) throws Exception { + private List loadListOf(PageLink pageLink, String urlTemplate) throws Exception { List loadedItems = new ArrayList<>(); - TextPageData pageData; + PageData pageData; + do { + pageData = doGetTypedWithPageLink(urlTemplate, new TypeReference>() { + }, pageLink); + loadedItems.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + return loadedItems; + } + + private List loadListOfInfo(PageLink pageLink, String urlTemplate) throws Exception { + List loadedItems = new ArrayList<>(); + PageData pageData; do { - pageData = doGetTypedWithPageLink(urlTemplate, new TypeReference>() { + pageData = doGetTypedWithPageLink(urlTemplate, new TypeReference>() { }, pageLink); loadedItems.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseTenantControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseTenantControllerTest.java index 0da36557c7..2294987668 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseTenantControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseTenantControllerTest.java @@ -15,21 +15,21 @@ */ package org.thingsboard.server.controller; -import static org.hamcrest.Matchers.containsString; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.fasterxml.jackson.core.type.TypeReference; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.Assert; +import org.junit.Test; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantInfo; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import org.apache.commons.lang3.RandomStringUtils; -import org.thingsboard.server.common.data.Tenant; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; -import org.junit.Assert; -import org.junit.Test; - -import com.fasterxml.jackson.core.type.TypeReference; +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; public abstract class BaseTenantControllerTest extends AbstractControllerTest { @@ -65,6 +65,19 @@ public abstract class BaseTenantControllerTest extends AbstractControllerTest { doDelete("/api/tenant/"+savedTenant.getId().getId().toString()) .andExpect(status().isOk()); } + + @Test + public void testFindTenantInfoById() throws Exception { + loginSysAdmin(); + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class); + TenantInfo foundTenant = doGet("/api/tenant/info/"+savedTenant.getId().getId().toString(), TenantInfo.class); + Assert.assertNotNull(foundTenant); + Assert.assertEquals(new TenantInfo(savedTenant, "Default"), foundTenant); + doDelete("/api/tenant/"+savedTenant.getId().getId().toString()) + .andExpect(status().isOk()); + } @Test public void testSaveTenantWithEmptyTitle() throws Exception { @@ -102,8 +115,8 @@ public abstract class BaseTenantControllerTest extends AbstractControllerTest { public void testFindTenants() throws Exception { loginSysAdmin(); List tenants = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(17); - TextPageData pageData = doGetTypedWithPageLink("/api/tenants?", new TypeReference>(){}, pageLink); + PageLink pageLink = new PageLink(17); + PageData pageData = doGetTypedWithPageLink("/api/tenants?", new TypeReference>(){}, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(1, pageData.getData().size()); tenants.addAll(pageData.getData()); @@ -115,12 +128,12 @@ public abstract class BaseTenantControllerTest extends AbstractControllerTest { } List loadedTenants = new ArrayList<>(); - pageLink = new TextPageLink(17); + pageLink = new PageLink(17); do { - pageData = doGetTypedWithPageLink("/api/tenants?", new TypeReference>(){}, pageLink); + pageData = doGetTypedWithPageLink("/api/tenants?", new TypeReference>(){}, pageLink); loadedTenants.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -136,8 +149,8 @@ public abstract class BaseTenantControllerTest extends AbstractControllerTest { } } - pageLink = new TextPageLink(17); - pageData = doGetTypedWithPageLink("/api/tenants?", new TypeReference>(){}, pageLink); + pageLink = new PageLink(17); + pageData = doGetTypedWithPageLink("/api/tenants?", new TypeReference>(){}, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(1, pageData.getData().size()); } @@ -167,13 +180,13 @@ public abstract class BaseTenantControllerTest extends AbstractControllerTest { } List loadedTenantsTitle1 = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(15, title1); - TextPageData pageData = null; + PageLink pageLink = new PageLink(15, 0, title1); + PageData pageData = null; do { - pageData = doGetTypedWithPageLink("/api/tenants?", new TypeReference>(){}, pageLink); + pageData = doGetTypedWithPageLink("/api/tenants?", new TypeReference>(){}, pageLink); loadedTenantsTitle1.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -183,12 +196,12 @@ public abstract class BaseTenantControllerTest extends AbstractControllerTest { Assert.assertEquals(tenantsTitle1, loadedTenantsTitle1); List loadedTenantsTitle2 = new ArrayList<>(); - pageLink = new TextPageLink(4, title2); + pageLink = new PageLink(4, 0, title2); do { - pageData = doGetTypedWithPageLink("/api/tenants?", new TypeReference>(){}, pageLink); + pageData = doGetTypedWithPageLink("/api/tenants?", new TypeReference>(){}, pageLink); loadedTenantsTitle2.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -202,8 +215,8 @@ public abstract class BaseTenantControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } - pageLink = new TextPageLink(4, title1); - pageData = doGetTypedWithPageLink("/api/tenants?", new TypeReference>(){}, pageLink); + pageLink = new PageLink(4, 0, title1); + pageData = doGetTypedWithPageLink("/api/tenants?", new TypeReference>(){}, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -212,9 +225,53 @@ public abstract class BaseTenantControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } - pageLink = new TextPageLink(4, title2); - pageData = doGetTypedWithPageLink("/api/tenants?", new TypeReference>(){}, pageLink); + pageLink = new PageLink(4, 0, title2); + pageData = doGetTypedWithPageLink("/api/tenants?", new TypeReference>(){}, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); } + + @Test + public void testFindTenantInfos() throws Exception { + loginSysAdmin(); + List tenants = new ArrayList<>(); + PageLink pageLink = new PageLink(17); + PageData pageData = doGetTypedWithPageLink("/api/tenantInfos?", new TypeReference>(){}, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(1, pageData.getData().size()); + tenants.addAll(pageData.getData()); + + for (int i=0;i<56;i++) { + Tenant tenant = new Tenant(); + tenant.setTitle("Tenant"+i); + tenants.add(new TenantInfo(doPost("/api/tenant", tenant, Tenant.class), "Default")); + } + + List loadedTenants = new ArrayList<>(); + pageLink = new PageLink(17); + do { + pageData = doGetTypedWithPageLink("/api/tenantInfos?", new TypeReference>(){}, pageLink); + loadedTenants.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(tenants, idComparator); + Collections.sort(loadedTenants, idComparator); + + Assert.assertEquals(tenants, loadedTenants); + + for (TenantInfo tenant : loadedTenants) { + if (!tenant.getTitle().equals(TEST_TENANT_NAME)) { + doDelete("/api/tenant/"+tenant.getId().getId().toString()) + .andExpect(status().isOk()); + } + } + + pageLink = new PageLink(17); + pageData = doGetTypedWithPageLink("/api/tenantInfos?", new TypeReference>(){}, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(1, pageData.getData().size()); + } } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseTenantProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseTenantProfileControllerTest.java new file mode 100644 index 0000000000..4f0d1e201b --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/BaseTenantProfileControllerTest.java @@ -0,0 +1,297 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.tenant.TenantProfileService; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public abstract class BaseTenantProfileControllerTest extends AbstractControllerTest { + + private IdComparator idComparator = new IdComparator<>(); + private IdComparator tenantProfileInfoIdComparator = new IdComparator<>(); + + @Autowired + private TenantProfileService tenantProfileService; + + @After + @Override + public void teardown() throws Exception { + super.teardown(); + tenantProfileService.deleteTenantProfiles(TenantId.SYS_TENANT_ID); + } + + @Test + public void testSaveTenantProfile() throws Exception { + loginSysAdmin(); + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + Assert.assertNotNull(savedTenantProfile); + Assert.assertNotNull(savedTenantProfile.getId()); + Assert.assertTrue(savedTenantProfile.getCreatedTime() > 0); + Assert.assertEquals(tenantProfile.getName(), savedTenantProfile.getName()); + Assert.assertEquals(tenantProfile.getDescription(), savedTenantProfile.getDescription()); + Assert.assertEquals(tenantProfile.getProfileData(), savedTenantProfile.getProfileData()); + Assert.assertEquals(tenantProfile.isDefault(), savedTenantProfile.isDefault()); + Assert.assertEquals(tenantProfile.isIsolatedTbCore(), savedTenantProfile.isIsolatedTbCore()); + Assert.assertEquals(tenantProfile.isIsolatedTbRuleEngine(), savedTenantProfile.isIsolatedTbRuleEngine()); + + savedTenantProfile.setName("New tenant profile"); + doPost("/api/tenantProfile", savedTenantProfile, TenantProfile.class); + TenantProfile foundTenantProfile = doGet("/api/tenantProfile/"+savedTenantProfile.getId().getId().toString(), TenantProfile.class); + Assert.assertEquals(foundTenantProfile.getName(), savedTenantProfile.getName()); + } + + @Test + public void testFindTenantProfileById() throws Exception { + loginSysAdmin(); + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + TenantProfile foundTenantProfile = doGet("/api/tenantProfile/"+savedTenantProfile.getId().getId().toString(), TenantProfile.class); + Assert.assertNotNull(foundTenantProfile); + Assert.assertEquals(savedTenantProfile, foundTenantProfile); + } + + @Test + public void testFindTenantProfileInfoById() throws Exception { + loginSysAdmin(); + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + EntityInfo foundTenantProfileInfo = doGet("/api/tenantProfileInfo/"+savedTenantProfile.getId().getId().toString(), EntityInfo.class); + Assert.assertNotNull(foundTenantProfileInfo); + Assert.assertEquals(savedTenantProfile.getId(), foundTenantProfileInfo.getId()); + Assert.assertEquals(savedTenantProfile.getName(), foundTenantProfileInfo.getName()); + } + + @Test + public void testFindDefaultTenantProfileInfo() throws Exception { + loginSysAdmin(); + EntityInfo foundDefaultTenantProfile = doGet("/api/tenantProfileInfo/default", EntityInfo.class); + Assert.assertNotNull(foundDefaultTenantProfile); + Assert.assertEquals("Default", foundDefaultTenantProfile.getName()); + } + + @Test + public void testSetDefaultTenantProfile() throws Exception { + loginSysAdmin(); + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile 1"); + TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + TenantProfile defaultTenantProfile = doPost("/api/tenantProfile/"+savedTenantProfile.getId().getId().toString()+"/default", null, TenantProfile.class); + Assert.assertNotNull(defaultTenantProfile); + EntityInfo foundDefaultTenantProfile = doGet("/api/tenantProfileInfo/default", EntityInfo.class); + Assert.assertNotNull(foundDefaultTenantProfile); + Assert.assertEquals(savedTenantProfile.getName(), foundDefaultTenantProfile.getName()); + Assert.assertEquals(savedTenantProfile.getId(), foundDefaultTenantProfile.getId()); + } + + @Test + public void testSaveTenantProfileWithEmptyName() throws Exception { + loginSysAdmin(); + TenantProfile tenantProfile = new TenantProfile(); + doPost("/api/tenantProfile", tenantProfile).andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Tenant profile name should be specified"))); + } + + @Test + public void testSaveTenantProfileWithSameName() throws Exception { + loginSysAdmin(); + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + doPost("/api/tenantProfile", tenantProfile).andExpect(status().isOk()); + TenantProfile tenantProfile2 = this.createTenantProfile("Tenant Profile"); + doPost("/api/tenantProfile", tenantProfile2).andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Tenant profile with such name already exists"))); + } + + @Test + public void testSaveSameTenantProfileWithDifferentIsolatedTbRuleEngine() throws Exception { + loginSysAdmin(); + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + savedTenantProfile.setIsolatedTbRuleEngine(true); + doPost("/api/tenantProfile", savedTenantProfile).andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Can't update isolatedTbRuleEngine property"))); + } + + @Test + public void testSaveSameTenantProfileWithDifferentIsolatedTbCore() throws Exception { + loginSysAdmin(); + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + savedTenantProfile.setIsolatedTbCore(true); + doPost("/api/tenantProfile", savedTenantProfile).andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Can't update isolatedTbCore property"))); + } + + @Test + public void testDeleteTenantProfileWithExistingTenant() throws Exception { + loginSysAdmin(); + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant with tenant profile"); + tenant.setTenantProfileId(savedTenantProfile.getId()); + Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class); + + doDelete("/api/tenantProfile/" + savedTenantProfile.getId().getId().toString()) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("The tenant profile referenced by the tenants cannot be deleted"))); + + doDelete("/api/tenant/"+savedTenant.getId().getId().toString()) + .andExpect(status().isOk()); + } + + @Test + public void testDeleteTenantProfile() throws Exception { + loginSysAdmin(); + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + + doDelete("/api/tenantProfile/" + savedTenantProfile.getId().getId().toString()) + .andExpect(status().isOk()); + + doGet("/api/tenantProfile/" + savedTenantProfile.getId().getId().toString()) + .andExpect(status().isNotFound()); + } + + @Test + public void testFindTenantProfiles() throws Exception { + loginSysAdmin(); + List tenantProfiles = new ArrayList<>(); + PageLink pageLink = new PageLink(17); + PageData pageData = doGetTypedWithPageLink("/api/tenantProfiles?", + new TypeReference>(){}, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(1, pageData.getTotalElements()); + tenantProfiles.addAll(pageData.getData()); + + for (int i=0;i<28;i++) { + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"+i); + tenantProfiles.add(doPost("/api/tenantProfile", tenantProfile, TenantProfile.class)); + } + + List loadedTenantProfiles = new ArrayList<>(); + pageLink = new PageLink(17); + do { + pageData = doGetTypedWithPageLink("/api/tenantProfiles?", + new TypeReference>(){}, pageLink); + loadedTenantProfiles.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(tenantProfiles, idComparator); + Collections.sort(loadedTenantProfiles, idComparator); + + Assert.assertEquals(tenantProfiles, loadedTenantProfiles); + + for (TenantProfile tenantProfile : loadedTenantProfiles) { + if (!tenantProfile.isDefault()) { + doDelete("/api/tenantProfile/" + tenantProfile.getId().getId().toString()) + .andExpect(status().isOk()); + } + } + + pageLink = new PageLink(17); + pageData = doGetTypedWithPageLink("/api/tenantProfiles?", + new TypeReference>(){}, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(1, pageData.getTotalElements()); + } + + @Test + public void testFindTenantProfileInfos() throws Exception { + loginSysAdmin(); + List tenantProfiles = new ArrayList<>(); + PageLink pageLink = new PageLink(17); + PageData tenantProfilePageData = doGetTypedWithPageLink("/api/tenantProfiles?", + new TypeReference>(){}, pageLink); + Assert.assertFalse(tenantProfilePageData.hasNext()); + Assert.assertEquals(1, tenantProfilePageData.getTotalElements()); + tenantProfiles.addAll(tenantProfilePageData.getData()); + + for (int i=0;i<28;i++) { + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"+i); + tenantProfiles.add(doPost("/api/tenantProfile", tenantProfile, TenantProfile.class)); + } + + List loadedTenantProfileInfos = new ArrayList<>(); + pageLink = new PageLink(17); + PageData pageData; + do { + pageData = doGetTypedWithPageLink("/api/tenantProfileInfos?", + new TypeReference>(){}, pageLink); + loadedTenantProfileInfos.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(tenantProfiles, idComparator); + Collections.sort(loadedTenantProfileInfos, tenantProfileInfoIdComparator); + + List tenantProfileInfos = tenantProfiles.stream().map(tenantProfile -> new EntityInfo(tenantProfile.getId(), + tenantProfile.getName())).collect(Collectors.toList()); + + Assert.assertEquals(tenantProfileInfos, loadedTenantProfileInfos); + + for (TenantProfile tenantProfile : tenantProfiles) { + if (!tenantProfile.isDefault()) { + doDelete("/api/tenantProfile/" + tenantProfile.getId().getId().toString()) + .andExpect(status().isOk()); + } + } + + pageLink = new PageLink(17); + pageData = doGetTypedWithPageLink("/api/tenantProfileInfos?", + new TypeReference>(){}, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(1, pageData.getTotalElements()); + } + + private TenantProfile createTenantProfile(String name) { + TenantProfile tenantProfile = new TenantProfile(); + tenantProfile.setName(name); + tenantProfile.setDescription(name + " Test"); + TenantProfileData tenantProfileData = new TenantProfileData(); + tenantProfileData.setConfiguration(new DefaultTenantProfileConfiguration()); + tenantProfile.setProfileData(tenantProfileData); + tenantProfile.setDefault(false); + tenantProfile.setIsolatedTbCore(false); + tenantProfile.setIsolatedTbRuleEngine(false); + return tenantProfile; + } +} diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java index e290ac9f24..75cc71eecf 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java @@ -27,8 +27,8 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.service.mail.TestMailService; @@ -353,15 +353,14 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { } List loadedTenantAdmins = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(33); - TextPageData pageData = null; + PageLink pageLink = new PageLink(33); + PageData pageData = null; do { pageData = doGetTypedWithPageLink("/api/tenant/" + tenantId.getId().toString() + "/users?", - new TypeReference>() { - }, pageLink); + new TypeReference>(){}, pageLink); loadedTenantAdmins.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -370,13 +369,12 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { Assert.assertEquals(tenantAdmins, loadedTenantAdmins); - doDelete("/api/tenant/" + savedTenant.getId().getId().toString()) - .andExpect(status().isOk()); - - pageLink = new TextPageLink(33); - pageData = doGetTypedWithPageLink("/api/tenant/" + tenantId.getId().toString() + "/users?", - new TypeReference>() { - }, pageLink); + doDelete("/api/tenant/"+savedTenant.getId().getId().toString()) + .andExpect(status().isOk()); + + pageLink = new PageLink(33); + pageData = doGetTypedWithPageLink("/api/tenant/" + tenantId.getId().toString() + "/users?", + new TypeReference>(){}, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertTrue(pageData.getData().isEmpty()); } @@ -422,15 +420,14 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { } List loadedTenantAdminsEmail1 = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(33, email1); - TextPageData pageData = null; + PageLink pageLink = new PageLink(33, 0, email1); + PageData pageData = null; do { pageData = doGetTypedWithPageLink("/api/tenant/" + tenantId.getId().toString() + "/users?", - new TypeReference>() { - }, pageLink); + new TypeReference>(){}, pageLink); loadedTenantAdminsEmail1.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -440,14 +437,13 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { Assert.assertEquals(tenantAdminsEmail1, loadedTenantAdminsEmail1); List loadedTenantAdminsEmail2 = new ArrayList<>(); - pageLink = new TextPageLink(16, email2); + pageLink = new PageLink(16, 0, email2); do { pageData = doGetTypedWithPageLink("/api/tenant/" + tenantId.getId().toString() + "/users?", - new TypeReference>() { - }, pageLink); + new TypeReference>(){}, pageLink); loadedTenantAdminsEmail2.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -461,10 +457,9 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } - pageLink = new TextPageLink(4, email1); - pageData = doGetTypedWithPageLink("/api/tenant/" + tenantId.getId().toString() + "/users?", - new TypeReference>() { - }, pageLink); + pageLink = new PageLink(4, 0, email1); + pageData = doGetTypedWithPageLink("/api/tenant/" + tenantId.getId().toString() + "/users?", + new TypeReference>(){}, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -473,10 +468,9 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } - pageLink = new TextPageLink(4, email2); - pageData = doGetTypedWithPageLink("/api/tenant/" + tenantId.getId().toString() + "/users?", - new TypeReference>() { - }, pageLink); + pageLink = new PageLink(4, 0, email2); + pageData = doGetTypedWithPageLink("/api/tenant/" + tenantId.getId().toString() + "/users?", + new TypeReference>(){}, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -519,15 +513,14 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { } List loadedCustomerUsers = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(33); - TextPageData pageData = null; + PageLink pageLink = new PageLink(33); + PageData pageData = null; do { pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/users?", - new TypeReference>() { - }, pageLink); + new TypeReference>(){}, pageLink); loadedCustomerUsers.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -599,15 +592,14 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { } List loadedCustomerUsersEmail1 = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(33, email1); - TextPageData pageData = null; + PageLink pageLink = new PageLink(33, 0, email1); + PageData pageData = null; do { pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/users?", - new TypeReference>() { - }, pageLink); + new TypeReference>(){}, pageLink); loadedCustomerUsersEmail1.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -617,14 +609,13 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { Assert.assertEquals(customerUsersEmail1, loadedCustomerUsersEmail1); List loadedCustomerUsersEmail2 = new ArrayList<>(); - pageLink = new TextPageLink(16, email2); + pageLink = new PageLink(16, 0, email2); do { pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/users?", - new TypeReference>() { - }, pageLink); + new TypeReference>(){}, pageLink); loadedCustomerUsersEmail2.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -638,10 +629,9 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } - pageLink = new TextPageLink(4, email1); - pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/users?", - new TypeReference>() { - }, pageLink); + pageLink = new PageLink(4, 0, email1); + pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/users?", + new TypeReference>(){}, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -650,10 +640,9 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } - pageLink = new TextPageLink(4, email2); - pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/users?", - new TypeReference>() { - }, pageLink); + pageLink = new PageLink(4, 0, email2); + pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/users?", + new TypeReference>(){}, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java new file mode 100644 index 0000000000..fb4d0320d7 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java @@ -0,0 +1,683 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.google.common.util.concurrent.FutureCallback; +import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.kv.Aggregation; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.DeviceTypeFilter; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.TsValue; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope; +import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; +import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityHistoryCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.LatestValueCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.TimeSeriesCmd; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +public class BaseWebsocketApiTest extends AbstractWebsocketTest { + + private Tenant savedTenant; + private User tenantAdmin; + private TbTestWebSocketClient wsClient; + + @Autowired + private TelemetrySubscriptionService tsService; + + @Before + public void beforeTest() throws Exception { + loginSysAdmin(); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + savedTenant = doPost("/api/tenant", tenant, Tenant.class); + Assert.assertNotNull(savedTenant); + + tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant2@thingsboard.org"); + tenantAdmin.setFirstName("Joe"); + tenantAdmin.setLastName("Downs"); + + tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); + + wsClient = buildAndConnectWebSocketClient(); + } + + @After + public void afterTest() throws Exception { + wsClient.close(); + + loginSysAdmin(); + + doDelete("/api/tenant/" + savedTenant.getId().getId().toString()) + .andExpect(status().isOk()); + } + + @Test + public void testEntityDataHistoryWsCmd() throws Exception { + Device device = new Device(); + device.setName("Device"); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + device = doPost("/api/device", device, Device.class); + + long now = System.currentTimeMillis(); + + DeviceTypeFilter dtf = new DeviceTypeFilter(); + dtf.setDeviceNameFilter("D"); + dtf.setDeviceType("default"); + EntityDataQuery edq = new EntityDataQuery(dtf, + new EntityDataPageLink(1, 0, null, null), + Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); + + EntityHistoryCmd historyCmd = new EntityHistoryCmd(); + historyCmd.setKeys(Arrays.asList("temperature")); + historyCmd.setAgg(Aggregation.NONE); + historyCmd.setLimit(1000); + historyCmd.setStartTs(now - TimeUnit.HOURS.toMillis(1)); + historyCmd.setEndTs(now); + EntityDataCmd cmd = new EntityDataCmd(1, edq, historyCmd, null, null); + + TelemetryPluginCmdsWrapper wrapper = new TelemetryPluginCmdsWrapper(); + wrapper.setEntityDataCmds(Collections.singletonList(cmd)); + + wsClient.send(mapper.writeValueAsString(wrapper)); + String msg = wsClient.waitForReply(); + EntityDataUpdate update = mapper.readValue(msg, EntityDataUpdate.class); + Assert.assertEquals(1, update.getCmdId()); + PageData pageData = update.getData(); + Assert.assertNotNull(pageData); + Assert.assertEquals(1, pageData.getData().size()); + Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId()); + Assert.assertEquals(0, pageData.getData().get(0).getTimeseries().get("temperature").length); + + TsKvEntry dataPoint1 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("temperature", 42L)); + TsKvEntry dataPoint2 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(2), new LongDataEntry("temperature", 42L)); + TsKvEntry dataPoint3 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(3), new LongDataEntry("temperature", 42L)); + List tsData = Arrays.asList(dataPoint1, dataPoint2, dataPoint3); + + sendTelemetry(device, tsData); + Thread.sleep(100); + + wsClient.send(mapper.writeValueAsString(wrapper)); + msg = wsClient.waitForReply(); + update = mapper.readValue(msg, EntityDataUpdate.class); + Assert.assertEquals(1, update.getCmdId()); + List dataList = update.getUpdate(); + Assert.assertNotNull(dataList); + Assert.assertEquals(1, dataList.size()); + Assert.assertEquals(device.getId(), dataList.get(0).getEntityId()); + TsValue[] tsArray = dataList.get(0).getTimeseries().get("temperature"); + Assert.assertEquals(3, tsArray.length); + Assert.assertEquals(new TsValue(dataPoint1.getTs(), dataPoint1.getValueAsString()), tsArray[0]); + Assert.assertEquals(new TsValue(dataPoint2.getTs(), dataPoint2.getValueAsString()), tsArray[1]); + Assert.assertEquals(new TsValue(dataPoint3.getTs(), dataPoint3.getValueAsString()), tsArray[2]); + } + + @Test + public void testEntityDataTimeSeriesWsCmd() throws Exception { + Device device = new Device(); + device.setName("Device"); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + device = doPost("/api/device", device, Device.class); + + long now = System.currentTimeMillis(); + + DeviceTypeFilter dtf = new DeviceTypeFilter(); + dtf.setDeviceNameFilter("D"); + dtf.setDeviceType("default"); + EntityDataQuery edq = new EntityDataQuery(dtf, new EntityDataPageLink(1, 0, null, null), + Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); + + EntityDataCmd cmd = new EntityDataCmd(1, edq, null, null, null); + + TelemetryPluginCmdsWrapper wrapper = new TelemetryPluginCmdsWrapper(); + wrapper.setEntityDataCmds(Collections.singletonList(cmd)); + + wsClient.send(mapper.writeValueAsString(wrapper)); + String msg = wsClient.waitForReply(); + EntityDataUpdate update = mapper.readValue(msg, EntityDataUpdate.class); + Assert.assertEquals(1, update.getCmdId()); + PageData pageData = update.getData(); + Assert.assertNotNull(pageData); + Assert.assertEquals(1, pageData.getData().size()); + Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId()); + + TimeSeriesCmd tsCmd = new TimeSeriesCmd(); + tsCmd.setKeys(Arrays.asList("temperature")); + tsCmd.setAgg(Aggregation.NONE); + tsCmd.setLimit(1000); + tsCmd.setStartTs(now - TimeUnit.HOURS.toMillis(1)); + tsCmd.setTimeWindow(TimeUnit.HOURS.toMillis(1)); + + TsKvEntry dataPoint1 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("temperature", 42L)); + TsKvEntry dataPoint2 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(2), new LongDataEntry("temperature", 43L)); + TsKvEntry dataPoint3 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(3), new LongDataEntry("temperature", 44L)); + List tsData = Arrays.asList(dataPoint1, dataPoint2, dataPoint3); + + sendTelemetry(device, tsData); + Thread.sleep(100); + + cmd = new EntityDataCmd(1, null, null, null, tsCmd); + wrapper = new TelemetryPluginCmdsWrapper(); + wrapper.setEntityDataCmds(Collections.singletonList(cmd)); + wsClient.send(mapper.writeValueAsString(wrapper)); + msg = wsClient.waitForReply(); + update = mapper.readValue(msg, EntityDataUpdate.class); + Assert.assertEquals(1, update.getCmdId()); + List listData = update.getUpdate(); + Assert.assertNotNull(listData); + Assert.assertEquals(1, listData.size()); + Assert.assertEquals(device.getId(), listData.get(0).getEntityId()); + TsValue[] tsArray = listData.get(0).getTimeseries().get("temperature"); + Assert.assertEquals(3, tsArray.length); + Assert.assertEquals(new TsValue(dataPoint1.getTs(), dataPoint1.getValueAsString()), tsArray[0]); + Assert.assertEquals(new TsValue(dataPoint2.getTs(), dataPoint2.getValueAsString()), tsArray[1]); + Assert.assertEquals(new TsValue(dataPoint3.getTs(), dataPoint3.getValueAsString()), tsArray[2]); + + now = System.currentTimeMillis(); + TsKvEntry dataPoint4 = new BasicTsKvEntry(now, new LongDataEntry("temperature", 45L)); + wsClient.registerWaitForUpdate(); + Thread.sleep(100); + sendTelemetry(device, Arrays.asList(dataPoint4)); + msg = wsClient.waitForUpdate(); + + update = mapper.readValue(msg, EntityDataUpdate.class); + Assert.assertEquals(1, update.getCmdId()); + List eData = update.getUpdate(); + Assert.assertNotNull(eData); + Assert.assertEquals(1, eData.size()); + Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); + Assert.assertNotNull(eData.get(0).getTimeseries()); + TsValue[] tsValues = eData.get(0).getTimeseries().get("temperature"); + Assert.assertNotNull(tsValues); + Assert.assertEquals(new TsValue(dataPoint4.getTs(), dataPoint4.getValueAsString()), tsValues[0]); + } + + @Test + public void testEntityDataLatestWidgetFlow() throws Exception { + Device device = new Device(); + device.setName("Device"); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + device = doPost("/api/device", device, Device.class); + + long now = System.currentTimeMillis(); + + DeviceTypeFilter dtf = new DeviceTypeFilter(); + dtf.setDeviceNameFilter("D"); + dtf.setDeviceType("default"); + EntityDataQuery edq = new EntityDataQuery(dtf, new EntityDataPageLink(1, 0, null, null), Collections.emptyList(), + Collections.singletonList(new EntityKey(EntityKeyType.TIME_SERIES, "temperature")), Collections.emptyList()); + + EntityDataCmd cmd = new EntityDataCmd(1, edq, null, null, null); + + TelemetryPluginCmdsWrapper wrapper = new TelemetryPluginCmdsWrapper(); + wrapper.setEntityDataCmds(Collections.singletonList(cmd)); + + wsClient.send(mapper.writeValueAsString(wrapper)); + String msg = wsClient.waitForReply(); + EntityDataUpdate update = mapper.readValue(msg, EntityDataUpdate.class); + Assert.assertEquals(1, update.getCmdId()); + PageData pageData = update.getData(); + Assert.assertNotNull(pageData); + Assert.assertEquals(1, pageData.getData().size()); + Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId()); + Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature")); + Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getTs()); + Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()); + + TsKvEntry dataPoint1 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("temperature", 42L)); + List tsData = Arrays.asList(dataPoint1); + sendTelemetry(device, tsData); + + Thread.sleep(100); + + LatestValueCmd latestCmd = new LatestValueCmd(); + latestCmd.setKeys(Collections.singletonList(new EntityKey(EntityKeyType.TIME_SERIES, "temperature"))); + cmd = new EntityDataCmd(1, null, null, latestCmd, null); + wrapper = new TelemetryPluginCmdsWrapper(); + wrapper.setEntityDataCmds(Collections.singletonList(cmd)); + + wsClient.send(mapper.writeValueAsString(wrapper)); + msg = wsClient.waitForReply(); + update = mapper.readValue(msg, EntityDataUpdate.class); + + Assert.assertEquals(1, update.getCmdId()); + + List listData = update.getUpdate(); + Assert.assertNotNull(listData); + Assert.assertEquals(1, listData.size()); + Assert.assertEquals(device.getId(), listData.get(0).getEntityId()); + Assert.assertNotNull(listData.get(0).getLatest().get(EntityKeyType.TIME_SERIES)); + TsValue tsValue = listData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature"); + Assert.assertEquals(new TsValue(dataPoint1.getTs(), dataPoint1.getValueAsString()), tsValue); + + now = System.currentTimeMillis(); + TsKvEntry dataPoint2 = new BasicTsKvEntry(now, new LongDataEntry("temperature", 52L)); + + wsClient.registerWaitForUpdate(); + sendTelemetry(device, Arrays.asList(dataPoint2)); + msg = wsClient.waitForUpdate(); + + update = mapper.readValue(msg, EntityDataUpdate.class); + Assert.assertEquals(1, update.getCmdId()); + List eData = update.getUpdate(); + Assert.assertNotNull(eData); + Assert.assertEquals(1, eData.size()); + Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); + Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES)); + tsValue = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature"); + Assert.assertEquals(new TsValue(dataPoint2.getTs(), dataPoint2.getValueAsString()), tsValue); + + //Sending update from the past, while latest value has new timestamp; + wsClient.registerWaitForUpdate(); + sendTelemetry(device, Arrays.asList(dataPoint1)); + msg = wsClient.waitForUpdate(TimeUnit.SECONDS.toMillis(1)); + Assert.assertNull(msg); + + //Sending duplicate update again + wsClient.registerWaitForUpdate(); + sendTelemetry(device, Arrays.asList(dataPoint2)); + msg = wsClient.waitForUpdate(TimeUnit.SECONDS.toMillis(1)); + Assert.assertNull(msg); + } + + @Test + public void testEntityDataLatestTsWsCmd() throws Exception { + Device device = new Device(); + device.setName("Device"); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + device = doPost("/api/device", device, Device.class); + + long now = System.currentTimeMillis(); + + DeviceTypeFilter dtf = new DeviceTypeFilter(); + dtf.setDeviceNameFilter("D"); + dtf.setDeviceType("default"); + EntityDataQuery edq = new EntityDataQuery(dtf, new EntityDataPageLink(1, 0, null, null), Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); + + LatestValueCmd latestCmd = new LatestValueCmd(); + latestCmd.setKeys(Collections.singletonList(new EntityKey(EntityKeyType.TIME_SERIES, "temperature"))); + EntityDataCmd cmd = new EntityDataCmd(1, edq, null, latestCmd, null); + + TelemetryPluginCmdsWrapper wrapper = new TelemetryPluginCmdsWrapper(); + wrapper.setEntityDataCmds(Collections.singletonList(cmd)); + + wsClient.send(mapper.writeValueAsString(wrapper)); + String msg = wsClient.waitForReply(); + EntityDataUpdate update = mapper.readValue(msg, EntityDataUpdate.class); + Assert.assertEquals(1, update.getCmdId()); + PageData pageData = update.getData(); + Assert.assertNotNull(pageData); + Assert.assertEquals(1, pageData.getData().size()); + Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId()); + Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature")); + Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getTs()); + Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()); + + TsKvEntry dataPoint1 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("temperature", 42L)); + List tsData = Arrays.asList(dataPoint1); + sendTelemetry(device, tsData); + + Thread.sleep(100); + + cmd = new EntityDataCmd(1, edq, null, latestCmd, null); + wrapper = new TelemetryPluginCmdsWrapper(); + wrapper.setEntityDataCmds(Collections.singletonList(cmd)); + + wsClient.send(mapper.writeValueAsString(wrapper)); + msg = wsClient.waitForReply(); + update = mapper.readValue(msg, EntityDataUpdate.class); + + Assert.assertEquals(1, update.getCmdId()); + + List listData = update.getUpdate(); + Assert.assertNotNull(listData); + Assert.assertEquals(1, listData.size()); + Assert.assertEquals(device.getId(), listData.get(0).getEntityId()); + Assert.assertNotNull(listData.get(0).getLatest().get(EntityKeyType.TIME_SERIES)); + TsValue tsValue = listData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature"); + Assert.assertEquals(new TsValue(dataPoint1.getTs(), dataPoint1.getValueAsString()), tsValue); + + now = System.currentTimeMillis(); + TsKvEntry dataPoint2 = new BasicTsKvEntry(now, new LongDataEntry("temperature", 52L)); + + wsClient.registerWaitForUpdate(); + sendTelemetry(device, Arrays.asList(dataPoint2)); + msg = wsClient.waitForUpdate(); + + update = mapper.readValue(msg, EntityDataUpdate.class); + Assert.assertEquals(1, update.getCmdId()); + List eData = update.getUpdate(); + Assert.assertNotNull(eData); + Assert.assertEquals(1, eData.size()); + Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); + Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES)); + tsValue = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature"); + Assert.assertEquals(new TsValue(dataPoint2.getTs(), dataPoint2.getValueAsString()), tsValue); + + //Sending update from the past, while latest value has new timestamp; + wsClient.registerWaitForUpdate(); + sendTelemetry(device, Arrays.asList(dataPoint1)); + msg = wsClient.waitForUpdate(TimeUnit.SECONDS.toMillis(1)); + Assert.assertNull(msg); + + //Sending duplicate update again + wsClient.registerWaitForUpdate(); + sendTelemetry(device, Arrays.asList(dataPoint2)); + msg = wsClient.waitForUpdate(TimeUnit.SECONDS.toMillis(1)); + Assert.assertNull(msg); + } + + @Test + public void testEntityDataLatestAttrWsCmd() throws Exception { + Device device = new Device(); + device.setName("Device"); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + device = doPost("/api/device", device, Device.class); + + long now = System.currentTimeMillis(); + + DeviceTypeFilter dtf = new DeviceTypeFilter(); + dtf.setDeviceNameFilter("D"); + dtf.setDeviceType("default"); + EntityDataQuery edq = new EntityDataQuery(dtf, new EntityDataPageLink(1, 0, null, null), + Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); + + LatestValueCmd latestCmd = new LatestValueCmd(); + latestCmd.setKeys(Collections.singletonList(new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, "serverAttributeKey"))); + EntityDataCmd cmd = new EntityDataCmd(1, edq, null, latestCmd, null); + + TelemetryPluginCmdsWrapper wrapper = new TelemetryPluginCmdsWrapper(); + wrapper.setEntityDataCmds(Collections.singletonList(cmd)); + + wsClient.send(mapper.writeValueAsString(wrapper)); + String msg = wsClient.waitForReply(); + Assert.assertNotNull(msg); + EntityDataUpdate update = mapper.readValue(msg, EntityDataUpdate.class); + Assert.assertEquals(1, update.getCmdId()); + PageData pageData = update.getData(); + Assert.assertNotNull(pageData); + Assert.assertEquals(1, pageData.getData().size()); + Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId()); + Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey")); + Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getTs()); + Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getValue()); + + + wsClient.registerWaitForUpdate(); + Thread.sleep(500); + + AttributeKvEntry dataPoint1 = new BaseAttributeKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("serverAttributeKey", 42L)); + List tsData = Arrays.asList(dataPoint1); + sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, tsData); + + msg = wsClient.waitForUpdate(); + Assert.assertNotNull(msg); + update = mapper.readValue(msg, EntityDataUpdate.class); + + Assert.assertEquals(1, update.getCmdId()); + List listData = update.getUpdate(); + Assert.assertNotNull(listData); + Assert.assertEquals(1, listData.size()); + Assert.assertEquals(device.getId(), listData.get(0).getEntityId()); + Assert.assertNotNull(listData.get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE)); + TsValue tsValue = listData.get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey"); + Assert.assertEquals(new TsValue(dataPoint1.getLastUpdateTs(), dataPoint1.getValueAsString()), tsValue); + + now = System.currentTimeMillis(); + AttributeKvEntry dataPoint2 = new BaseAttributeKvEntry(now, new LongDataEntry("serverAttributeKey", 52L)); + + wsClient.registerWaitForUpdate(); + Thread.sleep(500); + sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, Arrays.asList(dataPoint2)); + msg = wsClient.waitForUpdate(); + Assert.assertNotNull(msg); + + update = mapper.readValue(msg, EntityDataUpdate.class); + Assert.assertEquals(1, update.getCmdId()); + List eData = update.getUpdate(); + Assert.assertNotNull(eData); + Assert.assertEquals(1, eData.size()); + Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); + Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE)); + tsValue = eData.get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey"); + Assert.assertEquals(new TsValue(dataPoint2.getLastUpdateTs(), dataPoint2.getValueAsString()), tsValue); + + //Sending update from the past, while latest value has new timestamp; + wsClient.registerWaitForUpdate(); + Thread.sleep(500); + sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, Arrays.asList(dataPoint1)); + msg = wsClient.waitForUpdate(TimeUnit.SECONDS.toMillis(1)); + Assert.assertNull(msg); + + //Sending duplicate update again + wsClient.registerWaitForUpdate(); + Thread.sleep(500); + sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, Arrays.asList(dataPoint2)); + msg = wsClient.waitForUpdate(TimeUnit.SECONDS.toMillis(1)); + Assert.assertNull(msg); + } + + @Test + public void testEntityDataLatestAttrTypesWsCmd() throws Exception { + Device device = new Device(); + device.setName("Device"); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + device = doPost("/api/device", device, Device.class); + + long now = System.currentTimeMillis(); + + DeviceTypeFilter dtf = new DeviceTypeFilter(); + dtf.setDeviceNameFilter("D"); + dtf.setDeviceType("default"); + EntityDataQuery edq = new EntityDataQuery(dtf, new EntityDataPageLink(1, 0, null, null), + Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); + + LatestValueCmd latestCmd = new LatestValueCmd(); + List keys = new ArrayList<>(); + keys.add(new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, "serverAttributeKey")); + keys.add(new EntityKey(EntityKeyType.CLIENT_ATTRIBUTE, "clientAttributeKey")); + keys.add(new EntityKey(EntityKeyType.SHARED_ATTRIBUTE, "sharedAttributeKey")); + keys.add(new EntityKey(EntityKeyType.ATTRIBUTE, "anyAttributeKey")); + latestCmd.setKeys(keys); + EntityDataCmd cmd = new EntityDataCmd(1, edq, null, latestCmd, null); + + TelemetryPluginCmdsWrapper wrapper = new TelemetryPluginCmdsWrapper(); + wrapper.setEntityDataCmds(Collections.singletonList(cmd)); + + wsClient.send(mapper.writeValueAsString(wrapper)); + String msg = wsClient.waitForReply(); + EntityDataUpdate update = mapper.readValue(msg, EntityDataUpdate.class); + Assert.assertEquals(1, update.getCmdId()); + PageData pageData = update.getData(); + Assert.assertNotNull(pageData); + Assert.assertEquals(1, pageData.getData().size()); + Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId()); + Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey")); + Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getTs()); + Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getValue()); + Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey")); + Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey").getTs()); + Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey").getValue()); + Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey")); + Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey").getTs()); + Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey").getValue()); + Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey")); + Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey").getTs()); + Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey").getValue()); + + wsClient.registerWaitForUpdate(); + AttributeKvEntry dataPoint1 = new BaseAttributeKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("serverAttributeKey", 42L)); + List tsData = Arrays.asList(dataPoint1); + Thread.sleep(100); + + sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, tsData); + + msg = wsClient.waitForUpdate(); + Assert.assertNotNull(msg); + update = mapper.readValue(msg, EntityDataUpdate.class); + Assert.assertEquals(1, update.getCmdId()); + List eData = update.getUpdate(); + Assert.assertNotNull(eData); + Assert.assertEquals(1, eData.size()); + Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); + Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE)); + TsValue attrValue = eData.get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey"); + Assert.assertEquals(new TsValue(dataPoint1.getLastUpdateTs(), dataPoint1.getValueAsString()), attrValue); + + //Sending update from the past, while latest value has new timestamp; + wsClient.registerWaitForUpdate(); + sendAttributes(device, TbAttributeSubscriptionScope.SHARED_SCOPE, Arrays.asList(dataPoint1)); + msg = wsClient.waitForUpdate(TimeUnit.SECONDS.toMillis(1)); + Assert.assertNull(msg); + + //Sending duplicate update again + wsClient.registerWaitForUpdate(); + sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, Arrays.asList(dataPoint1)); + msg = wsClient.waitForUpdate(TimeUnit.SECONDS.toMillis(1)); + Assert.assertNull(msg); + + //Sending update from the past, while latest value has new timestamp; + wsClient.registerWaitForUpdate(); + AttributeKvEntry dataPoint2 = new BaseAttributeKvEntry(now, new LongDataEntry("sharedAttributeKey", 42L)); + sendAttributes(device, TbAttributeSubscriptionScope.SHARED_SCOPE, Arrays.asList(dataPoint2)); + msg = wsClient.waitForUpdate(TimeUnit.SECONDS.toMillis(1)); + update = mapper.readValue(msg, EntityDataUpdate.class); + Assert.assertEquals(1, update.getCmdId()); + eData = update.getUpdate(); + Assert.assertNotNull(eData); + Assert.assertEquals(1, eData.size()); + Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); + Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.SHARED_ATTRIBUTE)); + attrValue = eData.get(0).getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey"); + Assert.assertEquals(new TsValue(dataPoint2.getLastUpdateTs(), dataPoint2.getValueAsString()), attrValue); + + wsClient.registerWaitForUpdate(); + AttributeKvEntry dataPoint3 = new BaseAttributeKvEntry(now, new LongDataEntry("clientAttributeKey", 42L)); + sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, Arrays.asList(dataPoint3)); + msg = wsClient.waitForUpdate(TimeUnit.SECONDS.toMillis(1)); + update = mapper.readValue(msg, EntityDataUpdate.class); + Assert.assertEquals(1, update.getCmdId()); + eData = update.getUpdate(); + Assert.assertNotNull(eData); + Assert.assertEquals(1, eData.size()); + Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); + Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE)); + attrValue = eData.get(0).getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey"); + Assert.assertEquals(new TsValue(dataPoint3.getLastUpdateTs(), dataPoint3.getValueAsString()), attrValue); + + wsClient.registerWaitForUpdate(); + AttributeKvEntry dataPoint4 = new BaseAttributeKvEntry(now, new LongDataEntry("anyAttributeKey", 42L)); + sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, Arrays.asList(dataPoint4)); + msg = wsClient.waitForUpdate(TimeUnit.SECONDS.toMillis(1)); + update = mapper.readValue(msg, EntityDataUpdate.class); + Assert.assertEquals(1, update.getCmdId()); + eData = update.getUpdate(); + Assert.assertNotNull(eData); + Assert.assertEquals(1, eData.size()); + Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); + Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.ATTRIBUTE)); + attrValue = eData.get(0).getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey"); + Assert.assertEquals(new TsValue(dataPoint4.getLastUpdateTs(), dataPoint4.getValueAsString()), attrValue); + + wsClient.registerWaitForUpdate(); + AttributeKvEntry dataPoint5 = new BaseAttributeKvEntry(now, new LongDataEntry("anyAttributeKey", 43L)); + sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, Arrays.asList(dataPoint5)); + msg = wsClient.waitForUpdate(TimeUnit.SECONDS.toMillis(1)); + update = mapper.readValue(msg, EntityDataUpdate.class); + Assert.assertEquals(1, update.getCmdId()); + eData = update.getUpdate(); + Assert.assertNotNull(eData); + Assert.assertEquals(1, eData.size()); + Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); + Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.ATTRIBUTE)); + attrValue = eData.get(0).getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey"); + Assert.assertEquals(new TsValue(dataPoint5.getLastUpdateTs(), dataPoint5.getValueAsString()), attrValue); + } + + private void sendTelemetry(Device device, List tsData) throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + tsService.saveAndNotify(device.getTenantId(), device.getId(), tsData, 0, new FutureCallback() { + @Override + public void onSuccess(@Nullable Void result) { + latch.countDown(); + } + + @Override + public void onFailure(Throwable t) { + latch.countDown(); + } + }); + latch.await(3, TimeUnit.SECONDS); + } + + private void sendAttributes(Device device, TbAttributeSubscriptionScope scope, List attrData) throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + tsService.saveAndNotify(device.getTenantId(), device.getId(), scope.name(), attrData, new FutureCallback() { + @Override + public void onSuccess(@Nullable Void result) { + latch.countDown(); + } + + @Override + public void onFailure(Throwable t) { + latch.countDown(); + } + }); + latch.await(3, TimeUnit.SECONDS); + } +} diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseWidgetsBundleControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseWidgetsBundleControllerTest.java index ae23f6633b..dbb25995cf 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseWidgetsBundleControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseWidgetsBundleControllerTest.java @@ -22,8 +22,8 @@ import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.widget.WidgetsBundle; @@ -161,14 +161,14 @@ public abstract class BaseWidgetsBundleControllerTest extends AbstractController widgetsBundles.addAll(sysWidgetsBundles); List loadedWidgetsBundles = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(14); - TextPageData pageData; + PageLink pageLink = new PageLink(14); + PageData pageData; do { pageData = doGetTypedWithPageLink("/api/widgetsBundles?", - new TypeReference>(){}, pageLink); + new TypeReference>(){}, pageLink); loadedWidgetsBundles.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -197,14 +197,14 @@ public abstract class BaseWidgetsBundleControllerTest extends AbstractController widgetsBundles.addAll(sysWidgetsBundles); List loadedWidgetsBundles = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(14); - TextPageData pageData; + PageLink pageLink = new PageLink(14); + PageData pageData; do { pageData = doGetTypedWithPageLink("/api/widgetsBundles?", - new TypeReference>(){}, pageLink); + new TypeReference>(){}, pageLink); loadedWidgetsBundles.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -218,14 +218,14 @@ public abstract class BaseWidgetsBundleControllerTest extends AbstractController .andExpect(status().isOk()); } - pageLink = new TextPageLink(17); + pageLink = new PageLink(17); loadedWidgetsBundles.clear(); do { pageData = doGetTypedWithPageLink("/api/widgetsBundles?", - new TypeReference>(){}, pageLink); + new TypeReference>(){}, pageLink); loadedWidgetsBundles.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); diff --git a/application/src/test/java/org/thingsboard/server/controller/ControllerNoSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/controller/ControllerNoSqlTestSuite.java deleted file mode 100644 index 781c483fc5..0000000000 --- a/application/src/test/java/org/thingsboard/server/controller/ControllerNoSqlTestSuite.java +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.controller; - -import org.cassandraunit.dataset.cql.ClassPathCQLDataSet; -import org.junit.BeforeClass; -import org.junit.ClassRule; -import org.junit.extensions.cpsuite.ClasspathSuite; -import org.junit.runner.RunWith; -import org.thingsboard.server.dao.CustomCassandraCQLUnit; -import org.thingsboard.server.queue.memory.InMemoryStorage; - -import java.util.Arrays; - -@RunWith(ClasspathSuite.class) -@ClasspathSuite.ClassnameFilters({ - "org.thingsboard.server.controller.nosql.*Test"}) -public class ControllerNoSqlTestSuite { - - @ClassRule - public static CustomCassandraCQLUnit cassandraUnit = - new CustomCassandraCQLUnit( - Arrays.asList( - new ClassPathCQLDataSet("cassandra/schema-ts.cql", false, false), - new ClassPathCQLDataSet("cassandra/schema-entities.cql", false, false), - new ClassPathCQLDataSet("cassandra/system-data.cql", false, false), - new ClassPathCQLDataSet("cassandra/system-test.cql", false, false)), - "cassandra-test.yaml", 30000l); - - @BeforeClass - public static void cleanupInMemStorage(){ - InMemoryStorage.getInstance().cleanup(); - } -} diff --git a/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java index d8653e945d..bc2c36b501 100644 --- a/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java @@ -26,13 +26,15 @@ import java.util.Arrays; @RunWith(ClasspathSuite.class) @ClasspathSuite.ClassnameFilters({ +// "org.thingsboard.server.controller.sql.WebsocketApiSqlTest", +// "org.thingsboard.server.controller.sql.TenantProfileControllerSqlTest", "org.thingsboard.server.controller.sql.*Test", }) public class ControllerSqlTestSuite { @ClassRule public static CustomSqlUnit sqlUnit = new CustomSqlUnit( - Arrays.asList("sql/schema-ts-hsql.sql", "sql/schema-entities-hsql.sql", "sql/schema-entities-idx.sql", "sql/system-data.sql"), + Arrays.asList("sql/schema-types-hsql.sql", "sql/schema-ts-hsql.sql", "sql/schema-entities-hsql.sql", "sql/schema-entities-idx.sql", "sql/system-data.sql"), "sql/hsql/drop-all-tables.sql", "sql-test.properties"); diff --git a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java new file mode 100644 index 0000000000..85cb54783c --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java @@ -0,0 +1,97 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import lombok.extern.slf4j.Slf4j; +import org.java_websocket.client.WebSocketClient; +import org.java_websocket.handshake.ServerHandshake; + +import java.net.URI; +import java.nio.channels.NotYetConnectedException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@Slf4j +public class TbTestWebSocketClient extends WebSocketClient { + + private volatile String lastMsg; + private CountDownLatch reply; + private CountDownLatch update; + + public TbTestWebSocketClient(URI serverUri) { + super(serverUri); + } + + @Override + public void onOpen(ServerHandshake serverHandshake) { + + } + + @Override + public void onMessage(String s) { + log.info("RECEIVED: {}", s); + lastMsg = s; + if (reply != null) { + reply.countDown(); + } + if (update != null) { + update.countDown(); + } + } + + @Override + public void onClose(int i, String s, boolean b) { + log.error("CLOSED:"); + } + + @Override + public void onError(Exception e) { + log.error("ERROR:", e); + } + + public void registerWaitForUpdate() { + lastMsg = null; + update = new CountDownLatch(1); + } + + @Override + public void send(String text) throws NotYetConnectedException { + reply = new CountDownLatch(1); + super.send(text); + } + + public String waitForUpdate() { + return waitForUpdate(TimeUnit.SECONDS.toMillis(3)); + } + + public String waitForUpdate(long ms) { + try { + update.await(ms, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + log.warn("Failed to await reply", e); + } + return lastMsg; + } + + public String waitForReply() { + try { + reply.await(3, TimeUnit.SECONDS); + } catch (InterruptedException e) { + log.warn("Failed to await reply", e); + } + return lastMsg; + } +} diff --git a/application/src/test/java/org/thingsboard/server/controller/nosql/AdminControllerNoSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/nosql/AdminControllerNoSqlTest.java deleted file mode 100644 index 3daf4ed452..0000000000 --- a/application/src/test/java/org/thingsboard/server/controller/nosql/AdminControllerNoSqlTest.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.controller.nosql; - -import org.thingsboard.server.controller.BaseAdminControllerTest; -import org.thingsboard.server.dao.service.DaoNoSqlTest; - -/** - * Created by Valerii Sosliuk on 6/28/2017. - */ -@DaoNoSqlTest -public class AdminControllerNoSqlTest extends BaseAdminControllerTest { -} diff --git a/application/src/test/java/org/thingsboard/server/controller/nosql/AssetControllerNoSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/nosql/AssetControllerNoSqlTest.java deleted file mode 100644 index 65a3490078..0000000000 --- a/application/src/test/java/org/thingsboard/server/controller/nosql/AssetControllerNoSqlTest.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.controller.nosql; - -import org.thingsboard.server.controller.BaseAssetControllerTest; -import org.thingsboard.server.dao.service.DaoNoSqlTest; -import org.thingsboard.server.dao.util.NoSqlDao; - -/** - * Created by Valerii Sosliuk on 6/28/2017. - */ -@DaoNoSqlTest -public class AssetControllerNoSqlTest extends BaseAssetControllerTest { -} diff --git a/application/src/test/java/org/thingsboard/server/controller/nosql/AuditLogControllerNoSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/nosql/AuditLogControllerNoSqlTest.java deleted file mode 100644 index e0e29b2b51..0000000000 --- a/application/src/test/java/org/thingsboard/server/controller/nosql/AuditLogControllerNoSqlTest.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.controller.nosql; - -import org.thingsboard.server.controller.BaseAuditLogControllerTest; -import org.thingsboard.server.dao.service.DaoNoSqlTest; - -@DaoNoSqlTest -public class AuditLogControllerNoSqlTest extends BaseAuditLogControllerTest { -} diff --git a/application/src/test/java/org/thingsboard/server/controller/nosql/AuthControllerNoSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/nosql/AuthControllerNoSqlTest.java deleted file mode 100644 index 89158a8091..0000000000 --- a/application/src/test/java/org/thingsboard/server/controller/nosql/AuthControllerNoSqlTest.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.controller.nosql; - -import org.thingsboard.server.controller.BaseAuthControllerTest; -import org.thingsboard.server.dao.service.DaoNoSqlTest; - -/** - * Created by Valerii Sosliuk on 6/28/2017. - */ -@DaoNoSqlTest -public class AuthControllerNoSqlTest extends BaseAuthControllerTest { -} diff --git a/application/src/test/java/org/thingsboard/server/controller/nosql/ComponentDescriptorControllerNoSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/nosql/ComponentDescriptorControllerNoSqlTest.java deleted file mode 100644 index 487db8259a..0000000000 --- a/application/src/test/java/org/thingsboard/server/controller/nosql/ComponentDescriptorControllerNoSqlTest.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.controller.nosql; - -import org.thingsboard.server.controller.BaseComponentDescriptorControllerTest; -import org.thingsboard.server.dao.service.DaoNoSqlTest; - -/** - * Created by Valerii Sosliuk on 6/28/2017. - */ -@DaoNoSqlTest -public class ComponentDescriptorControllerNoSqlTest extends BaseComponentDescriptorControllerTest { -} diff --git a/application/src/test/java/org/thingsboard/server/controller/nosql/CustomerControllerNoSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/nosql/CustomerControllerNoSqlTest.java deleted file mode 100644 index aab833458d..0000000000 --- a/application/src/test/java/org/thingsboard/server/controller/nosql/CustomerControllerNoSqlTest.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.controller.nosql; - -import org.thingsboard.server.controller.BaseCustomerControllerTest; -import org.thingsboard.server.dao.service.DaoNoSqlTest; - -/** - * Created by Valerii Sosliuk on 6/28/2017. - */ -@DaoNoSqlTest -public class CustomerControllerNoSqlTest extends BaseCustomerControllerTest { -} diff --git a/application/src/test/java/org/thingsboard/server/controller/nosql/DashboardControllerNoSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/nosql/DashboardControllerNoSqlTest.java deleted file mode 100644 index ae54eceeef..0000000000 --- a/application/src/test/java/org/thingsboard/server/controller/nosql/DashboardControllerNoSqlTest.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.controller.nosql; - -import org.thingsboard.server.controller.BaseDashboardControllerTest; -import org.thingsboard.server.dao.service.DaoNoSqlTest; - -/** - * Created by Valerii Sosliuk on 6/28/2017. - */ -@DaoNoSqlTest -public class DashboardControllerNoSqlTest extends BaseDashboardControllerTest { -} diff --git a/application/src/test/java/org/thingsboard/server/controller/nosql/DeviceControllerNoSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/nosql/DeviceControllerNoSqlTest.java deleted file mode 100644 index 47a6613c73..0000000000 --- a/application/src/test/java/org/thingsboard/server/controller/nosql/DeviceControllerNoSqlTest.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.controller.nosql; - -import org.thingsboard.server.controller.BaseDeviceControllerTest; -import org.thingsboard.server.dao.service.DaoNoSqlTest; - -/** - * Created by Valerii Sosliuk on 6/28/2017. - */ -@DaoNoSqlTest -public class DeviceControllerNoSqlTest extends BaseDeviceControllerTest { -} diff --git a/application/src/test/java/org/thingsboard/server/controller/nosql/EntityViewControllerNoSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/nosql/EntityViewControllerNoSqlTest.java deleted file mode 100644 index cb56bcef50..0000000000 --- a/application/src/test/java/org/thingsboard/server/controller/nosql/EntityViewControllerNoSqlTest.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.controller.nosql; - -import org.thingsboard.server.controller.BaseEntityViewControllerTest; -import org.thingsboard.server.dao.service.DaoNoSqlTest; - -/** - * Created by Victor Basanets on 8/27/2017. - */ -@DaoNoSqlTest -public class EntityViewControllerNoSqlTest extends BaseEntityViewControllerTest { -} diff --git a/application/src/test/java/org/thingsboard/server/controller/nosql/TenantControllerNoSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/nosql/TenantControllerNoSqlTest.java deleted file mode 100644 index 68ef0822ca..0000000000 --- a/application/src/test/java/org/thingsboard/server/controller/nosql/TenantControllerNoSqlTest.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.controller.nosql; - -import org.thingsboard.server.controller.BaseTenantControllerTest; -import org.thingsboard.server.dao.service.DaoNoSqlTest; - -/** - * Created by Valerii Sosliuk on 6/28/2017. - */ -@DaoNoSqlTest -public class TenantControllerNoSqlTest extends BaseTenantControllerTest { -} diff --git a/application/src/test/java/org/thingsboard/server/controller/nosql/UserControllerNoSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/nosql/UserControllerNoSqlTest.java deleted file mode 100644 index 5a96e6e83b..0000000000 --- a/application/src/test/java/org/thingsboard/server/controller/nosql/UserControllerNoSqlTest.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.controller.nosql; - -import org.thingsboard.server.controller.BaseUserControllerTest; -import org.thingsboard.server.dao.service.DaoNoSqlTest; - -/** - * Created by Valerii Sosliuk on 6/28/2017. - */ -@DaoNoSqlTest -public class UserControllerNoSqlTest extends BaseUserControllerTest { -} diff --git a/application/src/test/java/org/thingsboard/server/controller/nosql/WidgetTypeControllerNoSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/nosql/WidgetTypeControllerNoSqlTest.java deleted file mode 100644 index dc2be410e2..0000000000 --- a/application/src/test/java/org/thingsboard/server/controller/nosql/WidgetTypeControllerNoSqlTest.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.controller.nosql; - -import org.thingsboard.server.controller.BaseWidgetTypeControllerTest; -import org.thingsboard.server.dao.service.DaoNoSqlTest; - -/** - * Created by Valerii Sosliuk on 6/28/2017. - */ -@DaoNoSqlTest -public class WidgetTypeControllerNoSqlTest extends BaseWidgetTypeControllerTest { -} diff --git a/application/src/test/java/org/thingsboard/server/controller/nosql/WidgetsBundleControllerNoSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/nosql/WidgetsBundleControllerNoSqlTest.java deleted file mode 100644 index f7a2f79454..0000000000 --- a/application/src/test/java/org/thingsboard/server/controller/nosql/WidgetsBundleControllerNoSqlTest.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.controller.nosql; - -import org.thingsboard.server.controller.BaseWidgetsBundleControllerTest; -import org.thingsboard.server.dao.service.DaoNoSqlTest; - -/** - * Created by Valerii Sosliuk on 6/28/2017. - */ -@DaoNoSqlTest -public class WidgetsBundleControllerNoSqlTest extends BaseWidgetsBundleControllerTest { -} diff --git a/application/src/test/java/org/thingsboard/server/controller/sql/DeviceProfileControllerSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/sql/DeviceProfileControllerSqlTest.java new file mode 100644 index 0000000000..493c15b578 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/sql/DeviceProfileControllerSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller.sql; + +import org.thingsboard.server.controller.BaseDeviceProfileControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class DeviceProfileControllerSqlTest extends BaseDeviceProfileControllerTest { +} diff --git a/application/src/test/java/org/thingsboard/server/controller/sql/EntityQueryControllerSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/sql/EntityQueryControllerSqlTest.java new file mode 100644 index 0000000000..ac4c85f156 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/sql/EntityQueryControllerSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller.sql; + +import org.thingsboard.server.controller.BaseEntityQueryControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class EntityQueryControllerSqlTest extends BaseEntityQueryControllerTest { +} diff --git a/application/src/test/java/org/thingsboard/server/controller/sql/TenantProfileControllerSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/sql/TenantProfileControllerSqlTest.java new file mode 100644 index 0000000000..869fd41470 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/sql/TenantProfileControllerSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller.sql; + +import org.thingsboard.server.controller.BaseTenantProfileControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class TenantProfileControllerSqlTest extends BaseTenantProfileControllerTest { +} diff --git a/application/src/test/java/org/thingsboard/server/controller/sql/WebsocketApiSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/sql/WebsocketApiSqlTest.java new file mode 100644 index 0000000000..ba2a393c69 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/sql/WebsocketApiSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller.sql; + +import org.thingsboard.server.controller.BaseWebsocketApiTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class WebsocketApiSqlTest extends BaseWebsocketApiTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/AbstractMqttIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/AbstractMqttIntegrationTest.java new file mode 100644 index 0000000000..687269c30c --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/AbstractMqttIntegrationTest.java @@ -0,0 +1,281 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; +import org.junit.Assert; +import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.device.profile.AllowCreateNewDevicesDeviceProfileProvisionConfiguration; +import org.thingsboard.server.common.data.device.profile.CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.device.profile.DeviceProfileProvisionConfiguration; +import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration; +import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +public abstract class AbstractMqttIntegrationTest extends AbstractControllerTest { + + protected static final String MQTT_URL = "tcp://localhost:1883"; + + private static final AtomicInteger atomicInteger = new AtomicInteger(2); + + protected Tenant savedTenant; + protected User tenantAdmin; + + protected Device savedDevice; + protected String accessToken; + + protected Device savedGateway; + protected String gatewayAccessToken; + + protected void processBeforeTest (String deviceName, String gatewayName, TransportPayloadType payloadType, String telemetryTopic, String attributesTopic) throws Exception { + this.processBeforeTest(deviceName, gatewayName, payloadType, telemetryTopic, attributesTopic, DeviceProfileProvisionType.DISABLED, null, null); + } + + protected void processBeforeTest(String deviceName, + String gatewayName, + TransportPayloadType payloadType, + String telemetryTopic, + String attributesTopic, + DeviceProfileProvisionType provisionType, + String provisionKey, String provisionSecret + ) throws Exception { + loginSysAdmin(); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + savedTenant = doPost("/api/tenant", tenant, Tenant.class); + Assert.assertNotNull(savedTenant); + + tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant" + atomicInteger.getAndIncrement() + "@thingsboard.org"); + tenantAdmin.setFirstName("Joe"); + tenantAdmin.setLastName("Downs"); + + tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); + + Device device = new Device(); + device.setName(deviceName); + device.setType("default"); + + Device gateway = new Device(); + gateway.setName(gatewayName); + gateway.setType("default"); + ObjectNode additionalInfo = mapper.createObjectNode(); + additionalInfo.put("gateway", true); + gateway.setAdditionalInfo(additionalInfo); + + if (payloadType != null) { + DeviceProfile mqttDeviceProfile = createMqttDeviceProfile(payloadType, telemetryTopic, attributesTopic, provisionType, provisionKey, provisionSecret); + DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", mqttDeviceProfile, DeviceProfile.class); + device.setType(savedDeviceProfile.getName()); + device.setDeviceProfileId(savedDeviceProfile.getId()); + gateway.setType(savedDeviceProfile.getName()); + gateway.setDeviceProfileId(savedDeviceProfile.getId()); + } + + savedDevice = doPost("/api/device", device, Device.class); + + DeviceCredentials deviceCredentials = + doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class); + + savedGateway = doPost("/api/device", gateway, Device.class); + + DeviceCredentials gatewayCredentials = + doGet("/api/device/" + savedGateway.getId().getId().toString() + "/credentials", DeviceCredentials.class); + + assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId()); + accessToken = deviceCredentials.getCredentialsId(); + assertNotNull(accessToken); + + assertEquals(savedGateway.getId(), gatewayCredentials.getDeviceId()); + gatewayAccessToken = gatewayCredentials.getCredentialsId(); + assertNotNull(gatewayAccessToken); + + } + + protected void processAfterTest() throws Exception { + loginSysAdmin(); + if (savedTenant != null) { + doDelete("/api/tenant/" + savedTenant.getId().getId().toString()).andExpect(status().isOk()); + } + } + + protected MqttAsyncClient getMqttAsyncClient(String accessToken) throws MqttException { + String clientId = MqttAsyncClient.generateClientId(); + MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId, new MemoryPersistence()); + + MqttConnectOptions options = new MqttConnectOptions(); + options.setUserName(accessToken); + client.connect(options).waitForCompletion(); + return client; + } + + protected void publishMqttMsg(MqttAsyncClient client, byte[] payload, String topic) throws MqttException { + MqttMessage message = new MqttMessage(); + message.setPayload(payload); + client.publish(topic, message); + } + + protected List getKvProtos(List expectedKeys) { + List keyValueProtos = new ArrayList<>(); + TransportProtos.KeyValueProto strKeyValueProto = getKeyValueProto(expectedKeys.get(0), "value1", TransportProtos.KeyValueType.STRING_V); + TransportProtos.KeyValueProto boolKeyValueProto = getKeyValueProto(expectedKeys.get(1), "true", TransportProtos.KeyValueType.BOOLEAN_V); + TransportProtos.KeyValueProto dblKeyValueProto = getKeyValueProto(expectedKeys.get(2), "3.0", TransportProtos.KeyValueType.DOUBLE_V); + TransportProtos.KeyValueProto longKeyValueProto = getKeyValueProto(expectedKeys.get(3), "4", TransportProtos.KeyValueType.LONG_V); + TransportProtos.KeyValueProto jsonKeyValueProto = getKeyValueProto(expectedKeys.get(4), "{\"someNumber\": 42, \"someArray\": [1,2,3], \"someNestedObject\": {\"key\": \"value\"}}", TransportProtos.KeyValueType.JSON_V); + keyValueProtos.add(strKeyValueProto); + keyValueProtos.add(boolKeyValueProto); + keyValueProtos.add(dblKeyValueProto); + keyValueProtos.add(longKeyValueProto); + keyValueProtos.add(jsonKeyValueProto); + return keyValueProtos; + } + + protected TransportProtos.KeyValueProto getKeyValueProto(String key, String strValue, TransportProtos.KeyValueType type) { + TransportProtos.KeyValueProto.Builder keyValueProtoBuilder = TransportProtos.KeyValueProto.newBuilder(); + keyValueProtoBuilder.setKey(key); + keyValueProtoBuilder.setType(type); + switch (type) { + case BOOLEAN_V: + keyValueProtoBuilder.setBoolV(Boolean.parseBoolean(strValue)); + break; + case LONG_V: + keyValueProtoBuilder.setLongV(Long.parseLong(strValue)); + break; + case DOUBLE_V: + keyValueProtoBuilder.setDoubleV(Double.parseDouble(strValue)); + break; + case STRING_V: + keyValueProtoBuilder.setStringV(strValue); + break; + case JSON_V: + keyValueProtoBuilder.setJsonV(strValue); + break; + } + return keyValueProtoBuilder.build(); + } + + protected DeviceProfile createMqttDeviceProfile(TransportPayloadType transportPayloadType, + String telemetryTopic, String attributesTopic, + DeviceProfileProvisionType provisionType, + String provisionKey, String provisionSecret + ) { + DeviceProfile deviceProfile = new DeviceProfile(); + deviceProfile.setName(transportPayloadType.name()); + deviceProfile.setType(DeviceProfileType.DEFAULT); + deviceProfile.setTransportType(DeviceTransportType.MQTT); + deviceProfile.setProvisionType(provisionType); + deviceProfile.setProvisionDeviceKey(provisionKey); + deviceProfile.setDescription(transportPayloadType.name() + " Test"); + DeviceProfileData deviceProfileData = new DeviceProfileData(); + DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration(); + MqttDeviceProfileTransportConfiguration transportConfiguration = new MqttDeviceProfileTransportConfiguration(); + transportConfiguration.setTransportPayloadType(transportPayloadType); + if (!StringUtils.isEmpty(telemetryTopic)) { + transportConfiguration.setDeviceTelemetryTopic(telemetryTopic); + } + if (!StringUtils.isEmpty(attributesTopic)) { + transportConfiguration.setDeviceAttributesTopic(attributesTopic); + } + deviceProfileData.setTransportConfiguration(transportConfiguration); + DeviceProfileProvisionConfiguration provisionConfiguration; + switch (provisionType) { + case ALLOW_CREATE_NEW_DEVICES: + provisionConfiguration = new AllowCreateNewDevicesDeviceProfileProvisionConfiguration(provisionSecret); + break; + case CHECK_PRE_PROVISIONED_DEVICES: + provisionConfiguration = new CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration(provisionSecret); + break; + case DISABLED: + default: + provisionConfiguration = new DisabledDeviceProfileProvisionConfiguration(provisionSecret); + } + deviceProfileData.setProvisionConfiguration(provisionConfiguration); + deviceProfileData.setConfiguration(configuration); + deviceProfile.setProfileData(deviceProfileData); + deviceProfile.setDefault(false); + deviceProfile.setDefaultRuleChainId(null); + return deviceProfile; + } + + protected TransportProtos.PostAttributeMsg getPostAttributeMsg(List expectedKeys) { + List kvProtos = getKvProtos(expectedKeys); + TransportProtos.PostAttributeMsg.Builder builder = TransportProtos.PostAttributeMsg.newBuilder(); + builder.addAllKv(kvProtos); + return builder.build(); + } + + protected T doExecuteWithRetriesAndInterval(SupplierWithThrowable supplier, int retries, int intervalMs) throws Exception { + int count = 0; + T result = null; + Throwable lastException = null; + while (count < retries) { + try { + result = supplier.get(); + if (result != null) { + return result; + } + } catch (Throwable e) { + lastException = e; + } + count++; + if (count < retries) { + Thread.sleep(intervalMs); + } + } + if (lastException != null) { + throw new RuntimeException(lastException); + } else { + return result; + } + } + + @FunctionalInterface + public interface SupplierWithThrowable { + T get() throws Throwable; + } +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/MqttNoSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/mqtt/MqttNoSqlTestSuite.java index 7360c5c506..bebf1ae017 100644 --- a/application/src/test/java/org/thingsboard/server/mqtt/MqttNoSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/mqtt/MqttNoSqlTestSuite.java @@ -21,6 +21,7 @@ import org.junit.ClassRule; import org.junit.extensions.cpsuite.ClasspathSuite; import org.junit.runner.RunWith; import org.thingsboard.server.dao.CustomCassandraCQLUnit; +import org.thingsboard.server.dao.CustomSqlUnit; import org.thingsboard.server.queue.memory.InMemoryStorage; import java.util.Arrays; @@ -30,13 +31,19 @@ import java.util.Arrays; "org.thingsboard.server.mqtt.*.nosql.*Test"}) public class MqttNoSqlTestSuite { + @ClassRule + public static CustomSqlUnit sqlUnit = new CustomSqlUnit( + Arrays.asList("sql/schema-types-hsql.sql", "sql/schema-entities-hsql.sql", "sql/system-data.sql"), + "sql/hsql/drop-all-tables.sql", + "nosql-test.properties"); + @ClassRule public static CustomCassandraCQLUnit cassandraUnit = new CustomCassandraCQLUnit( Arrays.asList( new ClassPathCQLDataSet("cassandra/schema-ts.cql", false, false), - new ClassPathCQLDataSet("cassandra/schema-entities.cql", false, false), - new ClassPathCQLDataSet("cassandra/system-data.cql", false, false)), + new ClassPathCQLDataSet("cassandra/schema-ts-latest.cql", false, false) + ), "cassandra-test.yaml", 30000l); @BeforeClass diff --git a/application/src/test/java/org/thingsboard/server/mqtt/MqttSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/mqtt/MqttSqlTestSuite.java index 0e7234c716..f71e8f37a2 100644 --- a/application/src/test/java/org/thingsboard/server/mqtt/MqttSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/mqtt/MqttSqlTestSuite.java @@ -27,13 +27,18 @@ import java.util.Arrays; @RunWith(ClasspathSuite.class) @ClasspathSuite.ClassnameFilters({ "org.thingsboard.server.mqtt.rpc.sql.*Test", - "org.thingsboard.server.mqtt.telemetry.sql.*Test" + "org.thingsboard.server.mqtt.telemetry.timeseries.sql.*Test", + "org.thingsboard.server.mqtt.telemetry.attributes.sql.*Test", + "org.thingsboard.server.mqtt.attributes.updates.sql.*Test", + "org.thingsboard.server.mqtt.attributes.request.sql.*Test", + "org.thingsboard.server.mqtt.claim.sql.*Test", + "org.thingsboard.server.mqtt.provision.sql.*Test" }) public class MqttSqlTestSuite { @ClassRule public static CustomSqlUnit sqlUnit = new CustomSqlUnit( - Arrays.asList("sql/schema-ts-hsql.sql", "sql/schema-entities-hsql.sql", "sql/system-data.sql"), + Arrays.asList("sql/schema-types-hsql.sql", "sql/schema-ts-hsql.sql", "sql/schema-entities-hsql.sql", "sql/system-data.sql"), "sql/hsql/drop-all-tables.sql", "sql-test.properties"); diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/AbstractMqttAttributesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/AbstractMqttAttributesIntegrationTest.java new file mode 100644 index 0000000000..32488c8eb0 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/AbstractMqttAttributesIntegrationTest.java @@ -0,0 +1,111 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.attributes; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.mqtt.AbstractMqttIntegrationTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +@Slf4j +public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqttIntegrationTest { + + protected static final String POST_ATTRIBUTES_PAYLOAD = "{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73," + + "\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}"; + + protected void processBeforeTest(String deviceName, String gatewayName, TransportPayloadType payloadType, String telemetryTopic, String attributesTopic) throws Exception { + super.processBeforeTest(deviceName, gatewayName, payloadType, telemetryTopic, attributesTopic); + } + + protected void processAfterTest() throws Exception { + super.processAfterTest(); + } + + protected List getTsKvProtoList() { + TransportProtos.TsKvProto tsKvProtoAttribute1 = getTsKvProto("attribute1", "value1", TransportProtos.KeyValueType.STRING_V); + TransportProtos.TsKvProto tsKvProtoAttribute2 = getTsKvProto("attribute2", "true", TransportProtos.KeyValueType.BOOLEAN_V); + TransportProtos.TsKvProto tsKvProtoAttribute3 = getTsKvProto("attribute3", "42.0", TransportProtos.KeyValueType.DOUBLE_V); + TransportProtos.TsKvProto tsKvProtoAttribute4 = getTsKvProto("attribute4", "73", TransportProtos.KeyValueType.LONG_V); + TransportProtos.TsKvProto tsKvProtoAttribute5 = getTsKvProto("attribute5", "{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}", TransportProtos.KeyValueType.JSON_V); + List tsKvProtoList = new ArrayList<>(); + tsKvProtoList.add(tsKvProtoAttribute1); + tsKvProtoList.add(tsKvProtoAttribute2); + tsKvProtoList.add(tsKvProtoAttribute3); + tsKvProtoList.add(tsKvProtoAttribute4); + tsKvProtoList.add(tsKvProtoAttribute5); + return tsKvProtoList; + } + + + protected TransportProtos.TsKvProto getTsKvProto(String key, String value, TransportProtos.KeyValueType keyValueType) { + TransportProtos.TsKvProto.Builder tsKvProtoBuilder = TransportProtos.TsKvProto.newBuilder(); + TransportProtos.KeyValueProto keyValueProto = getKeyValueProto(key, value, keyValueType); + tsKvProtoBuilder.setKv(keyValueProto); + return tsKvProtoBuilder.build(); + } + + protected TestMqttCallback getTestMqttCallback() { + CountDownLatch latch = new CountDownLatch(1); + return new TestMqttCallback(latch); + } + + protected static class TestMqttCallback implements MqttCallback { + + private final CountDownLatch latch; + private Integer qoS; + private byte[] payloadBytes; + + TestMqttCallback(CountDownLatch latch) { + this.latch = latch; + } + + public int getQoS() { + return qoS; + } + + public byte[] getPayloadBytes() { + return payloadBytes; + } + + public CountDownLatch getLatch() { + return latch; + } + + @Override + public void connectionLost(Throwable throwable) { + } + + @Override + public void messageArrived(String requestTopic, MqttMessage mqttMessage) throws Exception { + qoS = mqttMessage.getQos(); + payloadBytes = mqttMessage.getPayload(); + latch.countDown(); + } + + @Override + public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { + + } + } + +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestIntegrationTest.java new file mode 100644 index 0000000000..9e4529caf2 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestIntegrationTest.java @@ -0,0 +1,154 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.attributes.request; + +import com.google.protobuf.InvalidProtocolBufferException; +import io.netty.handler.codec.mqtt.MqttQoS; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.server.mqtt.attributes.AbstractMqttAttributesIntegrationTest; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +public abstract class AbstractMqttAttributesRequestIntegrationTest extends AbstractMqttAttributesIntegrationTest { + + @Before + public void beforeTest() throws Exception { + processBeforeTest("Test Request attribute values from the server", "Gateway Test Request attribute values from the server", null, null, null); + } + + @After + public void afterTest() throws Exception { + processAfterTest(); + } + + @Test + public void testRequestAttributesValuesFromTheServer() throws Exception { + processTestRequestAttributesValuesFromTheServer(); + } + + @Test + public void testRequestAttributesValuesFromTheServerGateway() throws Exception { + processTestGatewayRequestAttributesValuesFromTheServer(); + } + + protected void processTestRequestAttributesValuesFromTheServer() throws Exception { + + MqttAsyncClient client = getMqttAsyncClient(accessToken); + + postAttributesAndSubscribeToTopic(savedDevice, client); + + Thread.sleep(1000); + + TestMqttCallback callback = getTestMqttCallback(); + client.setCallback(callback); + + validateResponse(client, callback.getLatch(), callback); + } + + protected void processTestGatewayRequestAttributesValuesFromTheServer() throws Exception { + + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + + postGatewayDeviceClientAttributes(client); + + Device savedDevice = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + "Gateway Device Request Attributes", Device.class), + 20, + 100); + + assertNotNull(savedDevice); + + Thread.sleep(1000); + + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + + Thread.sleep(1000); + + client.subscribe(MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC, MqttQoS.AT_LEAST_ONCE.value()); + + TestMqttCallback clientAttributesCallback = getTestMqttCallback(); + client.setCallback(clientAttributesCallback); + validateClientResponseGateway(client, clientAttributesCallback); + + TestMqttCallback sharedAttributesCallback = getTestMqttCallback(); + client.setCallback(sharedAttributesCallback); + validateSharedResponseGateway(client, sharedAttributesCallback); + } + + protected void postAttributesAndSubscribeToTopic(Device savedDevice, MqttAsyncClient client) throws Exception { + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + client.publish(MqttTopics.DEVICE_ATTRIBUTES_TOPIC, new MqttMessage(POST_ATTRIBUTES_PAYLOAD.getBytes())); + client.subscribe(MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC, MqttQoS.AT_MOST_ONCE.value()); + } + + protected void postGatewayDeviceClientAttributes(MqttAsyncClient client) throws Exception { + String postClientAttributes = "{\"" + "Gateway Device Request Attributes" + "\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}}"; + client.publish(MqttTopics.GATEWAY_ATTRIBUTES_TOPIC, new MqttMessage(postClientAttributes.getBytes())); + } + + protected void validateResponse(MqttAsyncClient client, CountDownLatch latch, TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException { + String keys = "attribute1,attribute2,attribute3,attribute4,attribute5"; + String payloadStr = "{\"clientKeys\":\"" + keys + "\", \"sharedKeys\":\"" + keys + "\"}"; + MqttMessage mqttMessage = new MqttMessage(); + mqttMessage.setPayload(payloadStr.getBytes()); + client.publish(MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX + "1", mqttMessage); + latch.await(3, TimeUnit.SECONDS); + assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); + String expectedRequestPayload = "{\"client\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}},\"shared\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}}"; + assertEquals(JacksonUtil.toJsonNode(expectedRequestPayload), JacksonUtil.toJsonNode(new String(callback.getPayloadBytes(), StandardCharsets.UTF_8))); + } + + protected void validateClientResponseGateway(MqttAsyncClient client, TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException { + String payloadStr = "{\"id\": 1, \"device\": \"" + "Gateway Device Request Attributes" + "\", \"client\": true, \"keys\": [\"attribute1\", \"attribute2\", \"attribute3\", \"attribute4\", \"attribute5\"]}"; + MqttMessage mqttMessage = new MqttMessage(); + mqttMessage.setPayload(payloadStr.getBytes()); + client.publish(MqttTopics.GATEWAY_ATTRIBUTES_REQUEST_TOPIC, mqttMessage); + callback.getLatch().await(3, TimeUnit.SECONDS); + assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS()); + String expectedRequestPayload = "{\"id\":1,\"device\":\"" + "Gateway Device Request Attributes" + "\",\"values\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}}"; + assertEquals(JacksonUtil.toJsonNode(expectedRequestPayload), JacksonUtil.toJsonNode(new String(callback.getPayloadBytes(), StandardCharsets.UTF_8))); + } + + protected void validateSharedResponseGateway(MqttAsyncClient client, TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException { + String payloadStr = "{\"id\": 1, \"device\": \"" + "Gateway Device Request Attributes" + "\", \"client\": false, \"keys\": [\"attribute1\", \"attribute2\", \"attribute3\", \"attribute4\", \"attribute5\"]}"; + MqttMessage mqttMessage = new MqttMessage(); + mqttMessage.setPayload(payloadStr.getBytes()); + client.publish(MqttTopics.GATEWAY_ATTRIBUTES_REQUEST_TOPIC, mqttMessage); + callback.getLatch().await(3, TimeUnit.SECONDS); + assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS()); + String expectedRequestPayload = "{\"id\":1,\"device\":\"" + "Gateway Device Request Attributes" + "\",\"values\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}}"; + assertEquals(JacksonUtil.toJsonNode(expectedRequestPayload), JacksonUtil.toJsonNode(new String(callback.getPayloadBytes(), StandardCharsets.UTF_8))); + } +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestJsonIntegrationTest.java new file mode 100644 index 0000000000..4b824ea0b5 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestJsonIntegrationTest.java @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.attributes.request; + +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.thingsboard.server.common.data.TransportPayloadType; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@Slf4j +public abstract class AbstractMqttAttributesRequestJsonIntegrationTest extends AbstractMqttAttributesRequestIntegrationTest { + + @Before + public void beforeTest() throws Exception { + processBeforeTest("Test Request attribute values from the server json", "Gateway Test Request attribute values from the server json", TransportPayloadType.JSON, null, null); + } + + @After + public void afterTest() throws Exception { + processAfterTest(); + } + + @Test + public void testRequestAttributesValuesFromTheServer() throws Exception { + processTestRequestAttributesValuesFromTheServer(); + } + + @Test + public void testRequestAttributesValuesFromTheServerGateway() throws Exception { + processTestGatewayRequestAttributesValuesFromTheServer(); + } +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestProtoIntegrationTest.java new file mode 100644 index 0000000000..96cc88fa2f --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestProtoIntegrationTest.java @@ -0,0 +1,201 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.attributes.request; + +import com.google.protobuf.InvalidProtocolBufferException; +import io.netty.handler.codec.mqtt.MqttQoS; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.gen.transport.TransportApiProtos; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +public abstract class AbstractMqttAttributesRequestProtoIntegrationTest extends AbstractMqttAttributesRequestIntegrationTest { + + @Before + public void beforeTest() throws Exception { + processBeforeTest("Test Request attribute values from the server proto", "Gateway Test Request attribute values from the server proto", TransportPayloadType.PROTOBUF, null, null); + } + + @After + public void afterTest() throws Exception { + processAfterTest(); + } + + @Test + public void testRequestAttributesValuesFromTheServer() throws Exception { + processTestRequestAttributesValuesFromTheServer(); + } + + + @Test + public void testRequestAttributesValuesFromTheServerGateway() throws Exception { + processTestGatewayRequestAttributesValuesFromTheServer(); + } + + protected void postAttributesAndSubscribeToTopic(Device savedDevice, MqttAsyncClient client) throws Exception { + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + String keys = "attribute1,attribute2,attribute3,attribute4,attribute5"; + List expectedKeys = Arrays.asList(keys.split(",")); + TransportProtos.PostAttributeMsg postAttributeMsg = getPostAttributeMsg(expectedKeys); + byte[] payload = postAttributeMsg.toByteArray(); + client.publish(MqttTopics.DEVICE_ATTRIBUTES_TOPIC, new MqttMessage(payload)); + client.subscribe(MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC, MqttQoS.AT_MOST_ONCE.value()); + } + + protected void postGatewayDeviceClientAttributes(MqttAsyncClient client) throws Exception { + String keys = "attribute1,attribute2,attribute3,attribute4,attribute5"; + List expectedKeys = Arrays.asList(keys.split(",")); + TransportProtos.PostAttributeMsg postAttributeMsg = getPostAttributeMsg(expectedKeys); + TransportApiProtos.AttributesMsg.Builder attributesMsgBuilder = TransportApiProtos.AttributesMsg.newBuilder(); + attributesMsgBuilder.setDeviceName("Gateway Device Request Attributes"); + attributesMsgBuilder.setMsg(postAttributeMsg); + TransportApiProtos.AttributesMsg attributesMsg = attributesMsgBuilder.build(); + TransportApiProtos.GatewayAttributesMsg.Builder gatewayAttributeMsgBuilder = TransportApiProtos.GatewayAttributesMsg.newBuilder(); + gatewayAttributeMsgBuilder.addMsg(attributesMsg); + byte[] bytes = gatewayAttributeMsgBuilder.build().toByteArray(); + client.publish(MqttTopics.GATEWAY_ATTRIBUTES_TOPIC, new MqttMessage(bytes)); + } + + protected void validateResponse(MqttAsyncClient client, CountDownLatch latch, TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException { + String keys = "attribute1,attribute2,attribute3,attribute4,attribute5"; + TransportApiProtos.AttributesRequest.Builder attributesRequestBuilder = TransportApiProtos.AttributesRequest.newBuilder(); + attributesRequestBuilder.setClientKeys(keys); + attributesRequestBuilder.setSharedKeys(keys); + TransportApiProtos.AttributesRequest attributesRequest = attributesRequestBuilder.build(); + MqttMessage mqttMessage = new MqttMessage(); + mqttMessage.setPayload(attributesRequest.toByteArray()); + client.publish(MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX + "1", mqttMessage); + latch.await(3, TimeUnit.SECONDS); + assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); + TransportProtos.GetAttributeResponseMsg expectedAttributesResponse = getExpectedAttributeResponseMsg(); + TransportProtos.GetAttributeResponseMsg actualAttributesResponse = TransportProtos.GetAttributeResponseMsg.parseFrom(callback.getPayloadBytes()); + assertEquals(expectedAttributesResponse.getRequestId(), actualAttributesResponse.getRequestId()); + List expectedClientKeyValueProtos = expectedAttributesResponse.getClientAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + List expectedSharedKeyValueProtos = expectedAttributesResponse.getSharedAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + List actualClientKeyValueProtos = actualAttributesResponse.getClientAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + List actualSharedKeyValueProtos = actualAttributesResponse.getSharedAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + assertTrue(actualClientKeyValueProtos.containsAll(expectedClientKeyValueProtos)); + assertTrue(actualSharedKeyValueProtos.containsAll(expectedSharedKeyValueProtos)); + } + + protected void validateClientResponseGateway(MqttAsyncClient client, TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException { + String keys = "attribute1,attribute2,attribute3,attribute4,attribute5"; + TransportApiProtos.GatewayAttributesRequestMsg gatewayAttributesRequestMsg = getGatewayAttributesRequestMsg(keys, true); + client.publish(MqttTopics.GATEWAY_ATTRIBUTES_REQUEST_TOPIC, new MqttMessage(gatewayAttributesRequestMsg.toByteArray())); + callback.getLatch().await(3, TimeUnit.SECONDS); + assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS()); + TransportApiProtos.GatewayAttributeResponseMsg expectedGatewayAttributeResponseMsg = getExpectedGatewayAttributeResponseMsg(true); + TransportApiProtos.GatewayAttributeResponseMsg actualGatewayAttributeResponseMsg = TransportApiProtos.GatewayAttributeResponseMsg.parseFrom(callback.getPayloadBytes()); + assertEquals(expectedGatewayAttributeResponseMsg.getDeviceName(), actualGatewayAttributeResponseMsg.getDeviceName()); + + TransportProtos.GetAttributeResponseMsg expectedResponseMsg = expectedGatewayAttributeResponseMsg.getResponseMsg(); + TransportProtos.GetAttributeResponseMsg actualResponseMsg = actualGatewayAttributeResponseMsg.getResponseMsg(); + assertEquals(expectedResponseMsg.getRequestId(), actualResponseMsg.getRequestId()); + + List expectedClientKeyValueProtos = expectedResponseMsg.getClientAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + List actualClientKeyValueProtos = actualResponseMsg.getClientAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + assertTrue(actualClientKeyValueProtos.containsAll(expectedClientKeyValueProtos)); + } + + protected void validateSharedResponseGateway(MqttAsyncClient client, TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException { + String keys = "attribute1,attribute2,attribute3,attribute4,attribute5"; + TransportApiProtos.GatewayAttributesRequestMsg gatewayAttributesRequestMsg = getGatewayAttributesRequestMsg(keys, false); + client.publish(MqttTopics.GATEWAY_ATTRIBUTES_REQUEST_TOPIC, new MqttMessage(gatewayAttributesRequestMsg.toByteArray())); + callback.getLatch().await(3, TimeUnit.SECONDS); + assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS()); + TransportApiProtos.GatewayAttributeResponseMsg expectedGatewayAttributeResponseMsg = getExpectedGatewayAttributeResponseMsg(false); + TransportApiProtos.GatewayAttributeResponseMsg actualGatewayAttributeResponseMsg = TransportApiProtos.GatewayAttributeResponseMsg.parseFrom(callback.getPayloadBytes()); + assertEquals(expectedGatewayAttributeResponseMsg.getDeviceName(), actualGatewayAttributeResponseMsg.getDeviceName()); + + TransportProtos.GetAttributeResponseMsg expectedResponseMsg = expectedGatewayAttributeResponseMsg.getResponseMsg(); + TransportProtos.GetAttributeResponseMsg actualResponseMsg = actualGatewayAttributeResponseMsg.getResponseMsg(); + assertEquals(expectedResponseMsg.getRequestId(), actualResponseMsg.getRequestId()); + + List expectedSharedKeyValueProtos = expectedResponseMsg.getSharedAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + List actualSharedKeyValueProtos = actualResponseMsg.getSharedAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + + assertTrue(actualSharedKeyValueProtos.containsAll(expectedSharedKeyValueProtos)); + } + + private TransportApiProtos.GatewayAttributesRequestMsg getGatewayAttributesRequestMsg(String keys, boolean client) { + return TransportApiProtos.GatewayAttributesRequestMsg.newBuilder() + .setClient(client) + .addAllKeys(Arrays.asList(keys.split(","))) + .setDeviceName("Gateway Device Request Attributes") + .setId(1).build(); + } + + private TransportProtos.GetAttributeResponseMsg getExpectedAttributeResponseMsg() { + TransportProtos.GetAttributeResponseMsg.Builder result = TransportProtos.GetAttributeResponseMsg.newBuilder(); + List tsKvProtoList = getTsKvProtoList(); + result.addAllClientAttributeList(tsKvProtoList); + result.addAllSharedAttributeList(tsKvProtoList); + result.setRequestId(1); + return result.build(); + } + + private TransportApiProtos.GatewayAttributeResponseMsg getExpectedGatewayAttributeResponseMsg(boolean client) { + TransportApiProtos.GatewayAttributeResponseMsg.Builder gatewayAttributeResponseMsg = TransportApiProtos.GatewayAttributeResponseMsg.newBuilder(); + TransportProtos.GetAttributeResponseMsg.Builder getAttributeResponseMsgBuilder = TransportProtos.GetAttributeResponseMsg.newBuilder(); + List tsKvProtoList = getTsKvProtoList(); + if (client) { + getAttributeResponseMsgBuilder.addAllClientAttributeList(tsKvProtoList); + } else { + getAttributeResponseMsgBuilder.addAllSharedAttributeList(tsKvProtoList); + } + getAttributeResponseMsgBuilder.setRequestId(1); + TransportProtos.GetAttributeResponseMsg getAttributeResponseMsg = getAttributeResponseMsgBuilder.build(); + gatewayAttributeResponseMsg.setDeviceName("Gateway Device Request Attributes"); + gatewayAttributeResponseMsg.setResponseMsg(getAttributeResponseMsg); + return gatewayAttributeResponseMsg.build(); + } + + protected List getKvProtos(List expectedKeys) { + List keyValueProtos = new ArrayList<>(); + TransportProtos.KeyValueProto strKeyValueProto = getKeyValueProto(expectedKeys.get(0), "value1", TransportProtos.KeyValueType.STRING_V); + TransportProtos.KeyValueProto boolKeyValueProto = getKeyValueProto(expectedKeys.get(1), "true", TransportProtos.KeyValueType.BOOLEAN_V); + TransportProtos.KeyValueProto dblKeyValueProto = getKeyValueProto(expectedKeys.get(2), "42.0", TransportProtos.KeyValueType.DOUBLE_V); + TransportProtos.KeyValueProto longKeyValueProto = getKeyValueProto(expectedKeys.get(3), "73", TransportProtos.KeyValueType.LONG_V); + TransportProtos.KeyValueProto jsonKeyValueProto = getKeyValueProto(expectedKeys.get(4), "{\"someNumber\": 42, \"someArray\": [1,2,3], \"someNestedObject\": {\"key\": \"value\"}}", TransportProtos.KeyValueType.JSON_V); + keyValueProtos.add(strKeyValueProto); + keyValueProtos.add(boolKeyValueProto); + keyValueProtos.add(dblKeyValueProto); + keyValueProtos.add(longKeyValueProto); + keyValueProtos.add(jsonKeyValueProto); + return keyValueProtos; + } + +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/nosql/MqttAttributesRequestNoSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/nosql/MqttAttributesRequestNoSqlIntegrationTest.java new file mode 100644 index 0000000000..e94ad9519c --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/nosql/MqttAttributesRequestNoSqlIntegrationTest.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.attributes.request.nosql; + +import org.thingsboard.server.dao.service.DaoNoSqlTest; +import org.thingsboard.server.mqtt.attributes.request.AbstractMqttAttributesRequestIntegrationTest; + + +@DaoNoSqlTest +public class MqttAttributesRequestNoSqlIntegrationTest extends AbstractMqttAttributesRequestIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/sql/MqttAttributesRequestJsonSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/sql/MqttAttributesRequestJsonSqlIntegrationTest.java new file mode 100644 index 0000000000..d3750d49a3 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/sql/MqttAttributesRequestJsonSqlIntegrationTest.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.attributes.request.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.attributes.request.AbstractMqttAttributesRequestIntegrationTest; +import org.thingsboard.server.mqtt.attributes.request.AbstractMqttAttributesRequestJsonIntegrationTest; + +@DaoSqlTest +public class MqttAttributesRequestJsonSqlIntegrationTest extends AbstractMqttAttributesRequestJsonIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/sql/MqttAttributesRequestProtoSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/sql/MqttAttributesRequestProtoSqlIntegrationTest.java new file mode 100644 index 0000000000..f52e79b2ab --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/sql/MqttAttributesRequestProtoSqlIntegrationTest.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.attributes.request.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.attributes.request.AbstractMqttAttributesRequestJsonIntegrationTest; +import org.thingsboard.server.mqtt.attributes.request.AbstractMqttAttributesRequestProtoIntegrationTest; + +@DaoSqlTest +public class MqttAttributesRequestProtoSqlIntegrationTest extends AbstractMqttAttributesRequestProtoIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/sql/MqttAttributesRequestSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/sql/MqttAttributesRequestSqlIntegrationTest.java new file mode 100644 index 0000000000..af18294b47 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/sql/MqttAttributesRequestSqlIntegrationTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.attributes.request.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.attributes.request.AbstractMqttAttributesRequestIntegrationTest; + +@DaoSqlTest +public class MqttAttributesRequestSqlIntegrationTest extends AbstractMqttAttributesRequestIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesIntegrationTest.java new file mode 100644 index 0000000000..d2febdf357 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesIntegrationTest.java @@ -0,0 +1,171 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.attributes.updates; + +import com.google.protobuf.InvalidProtocolBufferException; +import io.netty.handler.codec.mqtt.MqttQoS; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.server.mqtt.attributes.AbstractMqttAttributesIntegrationTest; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +public abstract class AbstractMqttAttributesUpdatesIntegrationTest extends AbstractMqttAttributesIntegrationTest { + + private static final String RESPONSE_ATTRIBUTES_PAYLOAD_DELETED = "{\"deleted\":[\"attribute5\"]}"; + + private static String getResponseGatewayAttributesUpdatedPayload() { + return "{\"device\":\"" + "Gateway Device Subscribe to attribute updates" + "\"," + + "\"data\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}}"; + } + + private static String getResponseGatewayAttributesDeletedPayload() { + return "{\"device\":\"" + "Gateway Device Subscribe to attribute updates" + "\",\"data\":{\"deleted\":[\"attribute5\"]}}"; + } + + @Before + public void beforeTest() throws Exception { + processBeforeTest("Test Subscribe to attribute updates", "Gateway Test Subscribe to attribute updates", TransportPayloadType.JSON, null, null); + } + + @After + public void afterTest() throws Exception { + processAfterTest(); + } + + @Test + public void testSubscribeToAttributesUpdatesFromTheServer() throws Exception { + processTestSubscribeToAttributesUpdates(); + } + + @Test + public void testSubscribeToAttributesUpdatesFromTheServerGateway() throws Exception { + processGatewayTestSubscribeToAttributesUpdates(); + } + + protected void processTestSubscribeToAttributesUpdates() throws Exception { + + MqttAsyncClient client = getMqttAsyncClient(accessToken); + + TestMqttCallback onUpdateCallback = getTestMqttCallback(); + client.setCallback(onUpdateCallback); + + client.subscribe(MqttTopics.DEVICE_ATTRIBUTES_TOPIC, MqttQoS.AT_MOST_ONCE.value()); + + Thread.sleep(1000); + + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + onUpdateCallback.getLatch().await(3, TimeUnit.SECONDS); + + validateUpdateAttributesResponse(onUpdateCallback); + + TestMqttCallback onDeleteCallback = getTestMqttCallback(); + client.setCallback(onDeleteCallback); + + doDelete("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/SHARED_SCOPE?keys=attribute5", String.class); + onDeleteCallback.getLatch().await(3, TimeUnit.SECONDS); + + validateDeleteAttributesResponse(onDeleteCallback); + } + + protected void validateUpdateAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { + assertNotNull(callback.getPayloadBytes()); + String response = new String(callback.getPayloadBytes(), StandardCharsets.UTF_8); + assertEquals(JacksonUtil.toJsonNode(POST_ATTRIBUTES_PAYLOAD), JacksonUtil.toJsonNode(response)); + } + + protected void validateDeleteAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { + assertNotNull(callback.getPayloadBytes()); + String response = new String(callback.getPayloadBytes(), StandardCharsets.UTF_8); + assertEquals(JacksonUtil.toJsonNode(RESPONSE_ATTRIBUTES_PAYLOAD_DELETED), JacksonUtil.toJsonNode(response)); + } + + protected void processGatewayTestSubscribeToAttributesUpdates() throws Exception { + + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + + TestMqttCallback onUpdateCallback = getTestMqttCallback(); + client.setCallback(onUpdateCallback); + + Device device = new Device(); + device.setName("Gateway Device Subscribe to attribute updates"); + device.setType("default"); + + byte[] connectPayloadBytes = getConnectPayloadBytes(); + + publishMqttMsg(client, connectPayloadBytes, MqttTopics.GATEWAY_CONNECT_TOPIC); + + Device savedDevice = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + "Gateway Device Subscribe to attribute updates", Device.class), + 20, + 100); + + assertNotNull(savedDevice); + + client.subscribe(MqttTopics.GATEWAY_ATTRIBUTES_TOPIC, MqttQoS.AT_MOST_ONCE.value()); + + Thread.sleep(1000); + + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + onUpdateCallback.getLatch().await(3, TimeUnit.SECONDS); + + validateGatewayUpdateAttributesResponse(onUpdateCallback); + + TestMqttCallback onDeleteCallback = getTestMqttCallback(); + client.setCallback(onDeleteCallback); + + doDelete("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/SHARED_SCOPE?keys=attribute5", String.class); + onDeleteCallback.getLatch().await(3, TimeUnit.SECONDS); + + validateGatewayDeleteAttributesResponse(onDeleteCallback); + + } + + protected void validateGatewayUpdateAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { + assertNotNull(callback.getPayloadBytes()); + String s = new String(callback.getPayloadBytes(), StandardCharsets.UTF_8); + assertEquals(getResponseGatewayAttributesUpdatedPayload(), s); + } + + protected void validateGatewayDeleteAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { + assertNotNull(callback.getPayloadBytes()); + String s = new String(callback.getPayloadBytes(), StandardCharsets.UTF_8); + assertEquals(s, getResponseGatewayAttributesDeletedPayload()); + } + + protected byte[] getConnectPayloadBytes() { + String connectPayload = "{\"device\": \"Gateway Device Subscribe to attribute updates\", \"type\": \"" + TransportPayloadType.JSON.name() + "\"}"; + return connectPayload.getBytes(); + } +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesJsonIntegrationTest.java new file mode 100644 index 0000000000..58379e4016 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesJsonIntegrationTest.java @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.attributes.updates; + +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.TransportPayloadType; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@Slf4j +public abstract class AbstractMqttAttributesUpdatesJsonIntegrationTest extends AbstractMqttAttributesUpdatesIntegrationTest { + + @Before + public void beforeTest() throws Exception { + processBeforeTest("Test Subscribe to attribute updates", "Gateway Test Subscribe to attribute updates", TransportPayloadType.JSON, null, null); + } + + @After + public void afterTest() throws Exception { + processAfterTest(); + } + + @Test + public void testSubscribeToAttributesUpdatesFromTheServer() throws Exception { + processTestSubscribeToAttributesUpdates(); + } + + @Test + public void testSubscribeToAttributesUpdatesFromTheServerGateway() throws Exception { + processGatewayTestSubscribeToAttributesUpdates(); + } +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesProtoIntegrationTest.java new file mode 100644 index 0000000000..faf8e1ce4d --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesProtoIntegrationTest.java @@ -0,0 +1,149 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.attributes.updates; + +import com.google.protobuf.InvalidProtocolBufferException; +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.gen.transport.TransportApiProtos; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@Slf4j +public abstract class AbstractMqttAttributesUpdatesProtoIntegrationTest extends AbstractMqttAttributesUpdatesIntegrationTest { + + @Before + public void beforeTest() throws Exception { + processBeforeTest("Test Subscribe to attribute updates", "Gateway Test Subscribe to attribute updates", TransportPayloadType.PROTOBUF, null, null); + } + + @After + public void afterTest() throws Exception { + processAfterTest(); + } + + @Test + public void testSubscribeToAttributesUpdatesFromTheServer() throws Exception { + processTestSubscribeToAttributesUpdates(); + } + + @Test + public void testSubscribeToAttributesUpdatesFromTheServerGateway() throws Exception { + processGatewayTestSubscribeToAttributesUpdates(); + } + + protected void validateUpdateAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { + assertNotNull(callback.getPayloadBytes()); + TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder(); + List tsKvProtoList = getTsKvProtoList(); + attributeUpdateNotificationMsgBuilder.addAllSharedUpdated(tsKvProtoList); + + TransportProtos.AttributeUpdateNotificationMsg expectedAttributeUpdateNotificationMsg = attributeUpdateNotificationMsgBuilder.build(); + TransportProtos.AttributeUpdateNotificationMsg actualAttributeUpdateNotificationMsg = TransportProtos.AttributeUpdateNotificationMsg.parseFrom(callback.getPayloadBytes()); + + List actualSharedUpdatedList = actualAttributeUpdateNotificationMsg.getSharedUpdatedList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + List expectedSharedUpdatedList = expectedAttributeUpdateNotificationMsg.getSharedUpdatedList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + + assertEquals(expectedSharedUpdatedList.size(), actualSharedUpdatedList.size()); + assertTrue(actualSharedUpdatedList.containsAll(expectedSharedUpdatedList)); + + } + + protected void validateDeleteAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { + assertNotNull(callback.getPayloadBytes()); + TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder(); + attributeUpdateNotificationMsgBuilder.addSharedDeleted("attribute5"); + + TransportProtos.AttributeUpdateNotificationMsg expectedAttributeUpdateNotificationMsg = attributeUpdateNotificationMsgBuilder.build(); + TransportProtos.AttributeUpdateNotificationMsg actualAttributeUpdateNotificationMsg = TransportProtos.AttributeUpdateNotificationMsg.parseFrom(callback.getPayloadBytes()); + + assertEquals(expectedAttributeUpdateNotificationMsg.getSharedDeletedList().size(), actualAttributeUpdateNotificationMsg.getSharedDeletedList().size()); + assertEquals("attribute5", actualAttributeUpdateNotificationMsg.getSharedDeletedList().get(0)); + + } + + protected void validateGatewayUpdateAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { + assertNotNull(callback.getPayloadBytes()); + + TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder(); + List tsKvProtoList = getTsKvProtoList(); + attributeUpdateNotificationMsgBuilder.addAllSharedUpdated(tsKvProtoList); + TransportProtos.AttributeUpdateNotificationMsg expectedAttributeUpdateNotificationMsg = attributeUpdateNotificationMsgBuilder.build(); + + TransportApiProtos.GatewayAttributeUpdateNotificationMsg.Builder gatewayAttributeUpdateNotificationMsgBuilder = TransportApiProtos.GatewayAttributeUpdateNotificationMsg.newBuilder(); + gatewayAttributeUpdateNotificationMsgBuilder.setDeviceName("Gateway Device Subscribe to attribute updates"); + gatewayAttributeUpdateNotificationMsgBuilder.setNotificationMsg(expectedAttributeUpdateNotificationMsg); + + TransportApiProtos.GatewayAttributeUpdateNotificationMsg expectedGatewayAttributeUpdateNotificationMsg = gatewayAttributeUpdateNotificationMsgBuilder.build(); + TransportApiProtos.GatewayAttributeUpdateNotificationMsg actualGatewayAttributeUpdateNotificationMsg = TransportApiProtos.GatewayAttributeUpdateNotificationMsg.parseFrom(callback.getPayloadBytes()); + + assertEquals(expectedGatewayAttributeUpdateNotificationMsg.getDeviceName(), actualGatewayAttributeUpdateNotificationMsg.getDeviceName()); + + List actualSharedUpdatedList = actualGatewayAttributeUpdateNotificationMsg.getNotificationMsg().getSharedUpdatedList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + List expectedSharedUpdatedList = expectedGatewayAttributeUpdateNotificationMsg.getNotificationMsg().getSharedUpdatedList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + + assertEquals(expectedSharedUpdatedList.size(), actualSharedUpdatedList.size()); + assertTrue(actualSharedUpdatedList.containsAll(expectedSharedUpdatedList)); + + } + + protected void validateGatewayDeleteAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { + assertNotNull(callback.getPayloadBytes()); + TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder(); + attributeUpdateNotificationMsgBuilder.addSharedDeleted("attribute5"); + TransportProtos.AttributeUpdateNotificationMsg attributeUpdateNotificationMsg = attributeUpdateNotificationMsgBuilder.build(); + + TransportApiProtos.GatewayAttributeUpdateNotificationMsg.Builder gatewayAttributeUpdateNotificationMsgBuilder = TransportApiProtos.GatewayAttributeUpdateNotificationMsg.newBuilder(); + gatewayAttributeUpdateNotificationMsgBuilder.setDeviceName("Gateway Device Subscribe to attribute updates"); + gatewayAttributeUpdateNotificationMsgBuilder.setNotificationMsg(attributeUpdateNotificationMsg); + + TransportApiProtos.GatewayAttributeUpdateNotificationMsg expectedGatewayAttributeUpdateNotificationMsg = gatewayAttributeUpdateNotificationMsgBuilder.build(); + TransportApiProtos.GatewayAttributeUpdateNotificationMsg actualGatewayAttributeUpdateNotificationMsg = TransportApiProtos.GatewayAttributeUpdateNotificationMsg.parseFrom(callback.getPayloadBytes()); + + assertEquals(expectedGatewayAttributeUpdateNotificationMsg.getDeviceName(), actualGatewayAttributeUpdateNotificationMsg.getDeviceName()); + + TransportProtos.AttributeUpdateNotificationMsg expectedAttributeUpdateNotificationMsg = expectedGatewayAttributeUpdateNotificationMsg.getNotificationMsg(); + TransportProtos.AttributeUpdateNotificationMsg actualAttributeUpdateNotificationMsg = actualGatewayAttributeUpdateNotificationMsg.getNotificationMsg(); + + assertEquals(expectedAttributeUpdateNotificationMsg.getSharedDeletedList().size(), actualAttributeUpdateNotificationMsg.getSharedDeletedList().size()); + assertEquals("attribute5", actualAttributeUpdateNotificationMsg.getSharedDeletedList().get(0)); + + } + + protected byte[] getConnectPayloadBytes() { + TransportApiProtos.ConnectMsg connectProto = getConnectProto(); + return connectProto.toByteArray(); + } + + private TransportApiProtos.ConnectMsg getConnectProto() { + TransportApiProtos.ConnectMsg.Builder builder = TransportApiProtos.ConnectMsg.newBuilder(); + builder.setDeviceName("Gateway Device Subscribe to attribute updates"); + builder.setDeviceType(TransportPayloadType.PROTOBUF.name()); + return builder.build(); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/nosql/MqttAttributesUpdatesNoSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/nosql/MqttAttributesUpdatesNoSqlIntegrationTest.java new file mode 100644 index 0000000000..993e0869e4 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/nosql/MqttAttributesUpdatesNoSqlIntegrationTest.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.attributes.updates.nosql; + +import org.thingsboard.server.dao.service.DaoNoSqlTest; +import org.thingsboard.server.mqtt.attributes.updates.AbstractMqttAttributesUpdatesJsonIntegrationTest; + + +@DaoNoSqlTest +public class MqttAttributesUpdatesNoSqlIntegrationTest extends AbstractMqttAttributesUpdatesJsonIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlIntegrationTest.java new file mode 100644 index 0000000000..cdafc3a9ac --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlIntegrationTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.attributes.updates.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.attributes.updates.AbstractMqttAttributesUpdatesIntegrationTest; + +@DaoSqlTest +public class MqttAttributesUpdatesSqlIntegrationTest extends AbstractMqttAttributesUpdatesIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlJsonIntegrationTest.java new file mode 100644 index 0000000000..a8fd4687f7 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlJsonIntegrationTest.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.attributes.updates.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.attributes.updates.AbstractMqttAttributesUpdatesIntegrationTest; +import org.thingsboard.server.mqtt.attributes.updates.AbstractMqttAttributesUpdatesJsonIntegrationTest; + +@DaoSqlTest +public class MqttAttributesUpdatesSqlJsonIntegrationTest extends AbstractMqttAttributesUpdatesJsonIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlProtoIntegrationTest.java new file mode 100644 index 0000000000..723e5e3dcd --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlProtoIntegrationTest.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.attributes.updates.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.attributes.updates.AbstractMqttAttributesUpdatesJsonIntegrationTest; +import org.thingsboard.server.mqtt.attributes.updates.AbstractMqttAttributesUpdatesProtoIntegrationTest; + +@DaoSqlTest +public class MqttAttributesUpdatesSqlProtoIntegrationTest extends AbstractMqttAttributesUpdatesProtoIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimDeviceTest.java b/application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimDeviceTest.java new file mode 100644 index 0000000000..dd16c17be2 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimDeviceTest.java @@ -0,0 +1,206 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.claim; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.thingsboard.server.common.data.ClaimRequest; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.dao.device.claim.ClaimResponse; +import org.thingsboard.server.dao.device.claim.ClaimResult; +import org.thingsboard.server.mqtt.AbstractMqttIntegrationTest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +public abstract class AbstractMqttClaimDeviceTest extends AbstractMqttIntegrationTest { + + protected static final String CUSTOMER_USER_PASSWORD = "customerUser123!"; + + protected User customerAdmin; + protected Customer savedCustomer; + + @Before + public void beforeTest() throws Exception { + super.processBeforeTest("Test Claim device", "Test Claim gateway", null, null, null); + createCustomerAndUser(); + } + + protected void createCustomerAndUser() throws Exception { + Customer customer = new Customer(); + customer.setTenantId(savedTenant.getId()); + customer.setTitle("Test Claiming Customer"); + savedCustomer = doPost("/api/customer", customer, Customer.class); + assertNotNull(savedCustomer); + assertEquals(savedTenant.getId(), savedCustomer.getTenantId()); + + User user = new User(); + user.setAuthority(Authority.CUSTOMER_USER); + user.setTenantId(savedTenant.getId()); + user.setCustomerId(savedCustomer.getId()); + user.setEmail("customer@thingsboard.org"); + + customerAdmin = createUser(user, CUSTOMER_USER_PASSWORD); + assertNotNull(customerAdmin); + assertEquals(customerAdmin.getCustomerId(), savedCustomer.getId()); + } + + @After + public void afterTest() throws Exception { + super.processAfterTest(); + } + + @Test + public void testClaimingDevice() throws Exception { + processTestClaimingDevice(false); + } + + @Test + public void testClaimingDeviceWithoutSecretAndDuration() throws Exception { + processTestClaimingDevice(true); + } + + @Test + public void testGatewayClaimingDevice() throws Exception { + processTestGatewayClaimingDevice("Test claiming gateway device", false); + } + + @Test + public void testGatewayClaimingDeviceWithoutSecretAndDuration() throws Exception { + processTestGatewayClaimingDevice("Test claiming gateway device empty payload", true); + } + + + protected void processTestClaimingDevice(boolean emptyPayload) throws Exception { + MqttAsyncClient client = getMqttAsyncClient(accessToken); + byte[] payloadBytes; + byte[] failurePayloadBytes; + if (emptyPayload) { + payloadBytes = "{}".getBytes(); + failurePayloadBytes = "{\"durationMs\":1}".getBytes(); + } else { + payloadBytes = "{\"secretKey\":\"value\", \"durationMs\":60000}".getBytes(); + failurePayloadBytes = "{\"secretKey\":\"value\", \"durationMs\":1}".getBytes(); + } + validateClaimResponse(emptyPayload, client, payloadBytes, failurePayloadBytes); + } + + protected void validateClaimResponse(boolean emptyPayload, MqttAsyncClient client, byte[] payloadBytes, byte[] failurePayloadBytes) throws Exception { + client.publish(MqttTopics.DEVICE_CLAIM_TOPIC, new MqttMessage(failurePayloadBytes)); + + loginUser(customerAdmin.getName(), CUSTOMER_USER_PASSWORD); + ClaimRequest claimRequest; + if (!emptyPayload) { + claimRequest = new ClaimRequest("value"); + } else { + claimRequest = new ClaimRequest(null); + } + + ClaimResponse claimResponse = doExecuteWithRetriesAndInterval( + () -> doPostClaimAsync("/api/customer/device/" + savedDevice.getName() + "/claim", claimRequest, ClaimResponse.class, status().isBadRequest()), + 20, + 100 + ); + + assertEquals(claimResponse, ClaimResponse.FAILURE); + + client.publish(MqttTopics.DEVICE_CLAIM_TOPIC, new MqttMessage(payloadBytes)); + + ClaimResult claimResult = doExecuteWithRetriesAndInterval( + () -> doPostClaimAsync("/api/customer/device/" + savedDevice.getName() + "/claim", claimRequest, ClaimResult.class, status().isOk()), + 20, + 100 + ); + assertEquals(claimResult.getResponse(), ClaimResponse.SUCCESS); + Device claimedDevice = claimResult.getDevice(); + assertNotNull(claimedDevice); + assertNotNull(claimedDevice.getCustomerId()); + assertEquals(customerAdmin.getCustomerId(), claimedDevice.getCustomerId()); + + claimResponse = doPostClaimAsync("/api/customer/device/" + savedDevice.getName() + "/claim", claimRequest, ClaimResponse.class, status().isBadRequest()); + assertEquals(claimResponse, ClaimResponse.CLAIMED); + } + + protected void validateGatewayClaimResponse(String deviceName, boolean emptyPayload, MqttAsyncClient client, byte[] failurePayloadBytes, byte[] payloadBytes) throws Exception { + client.publish(MqttTopics.GATEWAY_CLAIM_TOPIC, new MqttMessage(failurePayloadBytes)); + + Device savedDevice = doExecuteWithRetriesAndInterval( + () -> doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class), + 20, + 100 + ); + + assertNotNull(savedDevice); + + loginUser(customerAdmin.getName(), CUSTOMER_USER_PASSWORD); + ClaimRequest claimRequest; + if (!emptyPayload) { + claimRequest = new ClaimRequest("value"); + } else { + claimRequest = new ClaimRequest(null); + } + + ClaimResponse claimResponse = doPostClaimAsync("/api/customer/device/" + deviceName + "/claim", claimRequest, ClaimResponse.class, status().isBadRequest()); + assertEquals(claimResponse, ClaimResponse.FAILURE); + + client.publish(MqttTopics.GATEWAY_CLAIM_TOPIC, new MqttMessage(payloadBytes)); + + ClaimResult claimResult = doExecuteWithRetriesAndInterval( + () -> doPostClaimAsync("/api/customer/device/" + deviceName + "/claim", claimRequest, ClaimResult.class, status().isOk()), + 20, + 100 + ); + + assertEquals(claimResult.getResponse(), ClaimResponse.SUCCESS); + Device claimedDevice = claimResult.getDevice(); + assertNotNull(claimedDevice); + assertNotNull(claimedDevice.getCustomerId()); + assertEquals(customerAdmin.getCustomerId(), claimedDevice.getCustomerId()); + + claimResponse = doPostClaimAsync("/api/customer/device/" + deviceName + "/claim", claimRequest, ClaimResponse.class, status().isBadRequest()); + assertEquals(claimResponse, ClaimResponse.CLAIMED); + } + + protected void processTestGatewayClaimingDevice(String deviceName, boolean emptyPayload) throws Exception { + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + byte[] failurePayloadBytes; + byte[] payloadBytes; + String failurePayload; + String payload; + if (emptyPayload) { + failurePayload = "{\"" + deviceName + "\": " + "{\"durationMs\":1}" + "}"; + payload = "{\"" + deviceName + "\": " + "{}" + "}"; + } else { + failurePayload = "{\"" + deviceName + "\": " + "{\"secretKey\":\"value\", \"durationMs\":1}" + "}"; + payload = "{\"" + deviceName + "\": " + "{\"secretKey\":\"value\", \"durationMs\":60000}" + "}"; + } + payloadBytes = payload.getBytes(); + failurePayloadBytes = failurePayload.getBytes(); + validateGatewayClaimResponse(deviceName, emptyPayload, client, failurePayloadBytes, payloadBytes); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimJsonDeviceTest.java b/application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimJsonDeviceTest.java new file mode 100644 index 0000000000..f55cfa57c8 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimJsonDeviceTest.java @@ -0,0 +1,58 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.claim; + +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.thingsboard.server.common.data.TransportPayloadType; + +@Slf4j +public abstract class AbstractMqttClaimJsonDeviceTest extends AbstractMqttClaimDeviceTest { + + @Before + public void beforeTest() throws Exception { + super.processBeforeTest("Test Claim device", "Test Claim gateway", TransportPayloadType.JSON, null, null); + createCustomerAndUser(); + } + + @After + public void afterTest() throws Exception { + super.afterTest(); + } + + @Test + public void testClaimingDevice() throws Exception { + processTestClaimingDevice(false); + } + + @Test + public void testClaimingDeviceWithoutSecretAndDuration() throws Exception { + processTestClaimingDevice(true); + } + + @Test + public void testGatewayClaimingDevice() throws Exception { + processTestGatewayClaimingDevice("Test claiming gateway device Json", false); + } + + @Test + public void testGatewayClaimingDeviceWithoutSecretAndDuration() throws Exception { + processTestGatewayClaimingDevice("Test claiming gateway device empty payload Json", true); + } +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimProtoDeviceTest.java b/application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimProtoDeviceTest.java new file mode 100644 index 0000000000..d371c09f37 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimProtoDeviceTest.java @@ -0,0 +1,116 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.claim; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.gen.transport.TransportApiProtos; + +@Slf4j +public abstract class AbstractMqttClaimProtoDeviceTest extends AbstractMqttClaimDeviceTest { + + @Before + public void beforeTest() throws Exception { + processBeforeTest("Test Claim device", "Test Claim gateway", TransportPayloadType.PROTOBUF, null, null); + createCustomerAndUser(); + } + + @After + public void afterTest() throws Exception { super.afterTest(); } + + @Test + public void testClaimingDevice() throws Exception { + processTestClaimingDevice(false); + } + + @Test + public void testClaimingDeviceWithoutSecretAndDuration() throws Exception { + processTestClaimingDevice(true); + } + + @Test + public void testGatewayClaimingDevice() throws Exception { + processTestGatewayClaimingDevice("Test claiming gateway device Proto", false); + } + + @Test + public void testGatewayClaimingDeviceWithoutSecretAndDuration() throws Exception { + processTestGatewayClaimingDevice("Test claiming gateway device empty payload Proto", true); + } + + protected void processTestClaimingDevice(boolean emptyPayload) throws Exception { + MqttAsyncClient client = getMqttAsyncClient(accessToken); + byte[] payloadBytes; + if (emptyPayload) { + payloadBytes = getClaimDevice(0, emptyPayload).toByteArray(); + } else { + payloadBytes = getClaimDevice(60000, emptyPayload).toByteArray(); + } + byte[] failurePayloadBytes = getClaimDevice(1, emptyPayload).toByteArray(); + validateClaimResponse(emptyPayload, client, payloadBytes, failurePayloadBytes); + } + + protected void processTestGatewayClaimingDevice(String deviceName, boolean emptyPayload) throws Exception { + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + byte[] failurePayloadBytes; + byte[] payloadBytes; + if (emptyPayload) { + payloadBytes = getGatewayClaimMsg(deviceName, 0, emptyPayload).toByteArray(); + } else { + payloadBytes = getGatewayClaimMsg(deviceName, 60000, emptyPayload).toByteArray(); + } + failurePayloadBytes = getGatewayClaimMsg(deviceName, 1, emptyPayload).toByteArray(); + + validateGatewayClaimResponse(deviceName, emptyPayload, client, failurePayloadBytes, payloadBytes); + } + + private TransportApiProtos.GatewayClaimMsg getGatewayClaimMsg(String deviceName, long duration, boolean emptyPayload) { + TransportApiProtos.GatewayClaimMsg.Builder gatewayClaimMsgBuilder = TransportApiProtos.GatewayClaimMsg.newBuilder(); + TransportApiProtos.ClaimDeviceMsg.Builder claimDeviceMsgBuilder = TransportApiProtos.ClaimDeviceMsg.newBuilder(); + TransportApiProtos.ClaimDevice.Builder claimDeviceBuilder = TransportApiProtos.ClaimDevice.newBuilder(); + if (!emptyPayload) { + claimDeviceBuilder.setSecretKey("value"); + } + if (duration > 0) { + claimDeviceBuilder.setDurationMs(duration); + } + TransportApiProtos.ClaimDevice claimDevice = claimDeviceBuilder.build(); + claimDeviceMsgBuilder.setClaimRequest(claimDevice); + claimDeviceMsgBuilder.setDeviceName(deviceName); + TransportApiProtos.ClaimDeviceMsg claimDeviceMsg = claimDeviceMsgBuilder.build(); + gatewayClaimMsgBuilder.addMsg(claimDeviceMsg); + return gatewayClaimMsgBuilder.build(); + } + + private TransportApiProtos.ClaimDevice getClaimDevice(long duration, boolean emptyPayload) { + TransportApiProtos.ClaimDevice.Builder claimDeviceBuilder = TransportApiProtos.ClaimDevice.newBuilder(); + if (!emptyPayload) { + claimDeviceBuilder.setSecretKey("value"); + } + if (duration > 0) { + claimDeviceBuilder.setSecretKey("value"); + claimDeviceBuilder.setDurationMs(duration); + } + return claimDeviceBuilder.build(); + } + + +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/claim/nosql/MqttClaimDeviceNoSqlTest.java b/application/src/test/java/org/thingsboard/server/mqtt/claim/nosql/MqttClaimDeviceNoSqlTest.java new file mode 100644 index 0000000000..72b9f95328 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/claim/nosql/MqttClaimDeviceNoSqlTest.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.claim.nosql; + +import org.thingsboard.server.dao.service.DaoNoSqlTest; +import org.thingsboard.server.mqtt.claim.AbstractMqttClaimDeviceTest; + + +@DaoNoSqlTest +public class MqttClaimDeviceNoSqlTest extends AbstractMqttClaimDeviceTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/claim/sql/MqttClaimDeviceJsonSqlTest.java b/application/src/test/java/org/thingsboard/server/mqtt/claim/sql/MqttClaimDeviceJsonSqlTest.java new file mode 100644 index 0000000000..da794288f4 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/claim/sql/MqttClaimDeviceJsonSqlTest.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.claim.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.claim.AbstractMqttClaimDeviceTest; +import org.thingsboard.server.mqtt.claim.AbstractMqttClaimJsonDeviceTest; + +@DaoSqlTest +public class MqttClaimDeviceJsonSqlTest extends AbstractMqttClaimJsonDeviceTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/claim/sql/MqttClaimDeviceProtoSqlTest.java b/application/src/test/java/org/thingsboard/server/mqtt/claim/sql/MqttClaimDeviceProtoSqlTest.java new file mode 100644 index 0000000000..a63978e4de --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/claim/sql/MqttClaimDeviceProtoSqlTest.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.claim.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.claim.AbstractMqttClaimJsonDeviceTest; +import org.thingsboard.server.mqtt.claim.AbstractMqttClaimProtoDeviceTest; + +@DaoSqlTest +public class MqttClaimDeviceProtoSqlTest extends AbstractMqttClaimProtoDeviceTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/claim/sql/MqttClaimDeviceSqlTest.java b/application/src/test/java/org/thingsboard/server/mqtt/claim/sql/MqttClaimDeviceSqlTest.java new file mode 100644 index 0000000000..ff0c2becb7 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/claim/sql/MqttClaimDeviceSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.claim.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.claim.AbstractMqttClaimDeviceTest; + +@DaoSqlTest +public class MqttClaimDeviceSqlTest extends AbstractMqttClaimDeviceTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/provision/AbstractMqttProvisionJsonDeviceTest.java b/application/src/test/java/org/thingsboard/server/mqtt/provision/AbstractMqttProvisionJsonDeviceTest.java new file mode 100644 index 0000000000..7c341029b6 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/provision/AbstractMqttProvisionJsonDeviceTest.java @@ -0,0 +1,285 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.provision; + +import com.google.gson.JsonObject; +import io.netty.handler.codec.mqtt.MqttQoS; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; +import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.msg.EncryptionUtil; +import org.thingsboard.server.common.transport.util.JsonUtils; +import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.server.mqtt.AbstractMqttIntegrationTest; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@Slf4j +public abstract class AbstractMqttProvisionJsonDeviceTest extends AbstractMqttIntegrationTest { + + @Autowired + DeviceCredentialsService deviceCredentialsService; + + @Autowired + DeviceService deviceService; + + @After + public void afterTest() throws Exception { + super.processAfterTest(); + } + + @Test + public void testProvisioningDisabledDevice() throws Exception { + processTestProvisioningDisabledDevice(); + } + + @Test + public void testProvisioningCheckPreProvisionedDevice() throws Exception { + processTestProvisioningCheckPreProvisionedDevice(); + } + + @Test + public void testProvisioningCreateNewDeviceWithoutCredentials() throws Exception { + processTestProvisioningCreateNewDeviceWithoutCredentials(); + } + + @Test + public void testProvisioningCreateNewDeviceWithAccessToken() throws Exception { + processTestProvisioningCreateNewDeviceWithAccessToken(); + } + + @Test + public void testProvisioningCreateNewDeviceWithCert() throws Exception { + processTestProvisioningCreateNewDeviceWithCert(); + } + + @Test + public void testProvisioningCreateNewDeviceWithMqttBasic() throws Exception { + processTestProvisioningCreateNewDeviceWithMqttBasic(); + } + + @Test + public void testProvisioningWithBadKeyDevice() throws Exception { + processTestProvisioningWithBadKeyDevice(); + } + + + protected void processTestProvisioningDisabledDevice() throws Exception { + super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.DISABLED, null, null); + byte[] result = createMqttClientAndPublish().getPayloadBytes(); + JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); + Assert.assertEquals("Provision data was not found!", response.get("errorMsg").getAsString()); + Assert.assertEquals(ProvisionResponseStatus.NOT_FOUND.name(), response.get("provisionDeviceStatus").getAsString()); + } + + + protected void processTestProvisioningCreateNewDeviceWithoutCredentials() throws Exception { + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + byte[] result = createMqttClientAndPublish().getPayloadBytes(); + JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); + + Device createdDevice = deviceService.findDeviceByTenantIdAndName(savedTenant.getTenantId(), "Test Provision device"); + + Assert.assertNotNull(createdDevice); + Assert.assertEquals(createdDevice.getId().toString(), response.get("deviceId").getAsString()); + + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedTenant.getTenantId(), createdDevice.getId()); + + Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.get("credentialsType").getAsString()); + Assert.assertEquals(deviceCredentials.getCredentialsId(), response.get("credentialsId").getAsString()); + Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.get("provisionDeviceStatus").getAsString()); + } + + + protected void processTestProvisioningCreateNewDeviceWithAccessToken() throws Exception { + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + String requestCredentials = ",\"credentialsType\": \"ACCESS_TOKEN\",\"token\": \"test_token\""; + byte[] result = createMqttClientAndPublish(requestCredentials).getPayloadBytes(); + JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); + + Device createdDevice = deviceService.findDeviceByTenantIdAndName(savedTenant.getTenantId(), "Test Provision device"); + + Assert.assertNotNull(createdDevice); + Assert.assertEquals(createdDevice.getId().toString(), response.get("deviceId").getAsString()); + + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedTenant.getTenantId(), createdDevice.getId()); + + Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.get("credentialsType").getAsString()); + Assert.assertEquals(deviceCredentials.getCredentialsId(), response.get("credentialsId").getAsString()); + Assert.assertEquals(deviceCredentials.getCredentialsType().name(), "ACCESS_TOKEN"); + Assert.assertEquals(deviceCredentials.getCredentialsId(), "test_token"); + Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.get("provisionDeviceStatus").getAsString()); + } + + + protected void processTestProvisioningCreateNewDeviceWithCert() throws Exception { + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + String requestCredentials = ",\"credentialsType\": \"X509_CERTIFICATE\",\"hash\": \"testHash\""; + byte[] result = createMqttClientAndPublish(requestCredentials).getPayloadBytes(); + JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); + + Device createdDevice = deviceService.findDeviceByTenantIdAndName(savedTenant.getTenantId(), "Test Provision device"); + + Assert.assertNotNull(createdDevice); + Assert.assertEquals(createdDevice.getId().toString(), response.get("deviceId").getAsString()); + + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedTenant.getTenantId(), createdDevice.getId()); + + Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.get("credentialsType").getAsString()); + Assert.assertEquals(deviceCredentials.getCredentialsId(), response.get("credentialsId").getAsString()); + Assert.assertEquals(deviceCredentials.getCredentialsType().name(), "X509_CERTIFICATE"); + + String cert = EncryptionUtil.trimNewLines(deviceCredentials.getCredentialsValue()); + String sha3Hash = EncryptionUtil.getSha3Hash(cert); + + Assert.assertEquals(deviceCredentials.getCredentialsId(), sha3Hash); + + Assert.assertEquals(deviceCredentials.getCredentialsValue(), "testHash"); + Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.get("provisionDeviceStatus").getAsString()); + } + + + protected void processTestProvisioningCreateNewDeviceWithMqttBasic() throws Exception { + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + String requestCredentials = ",\"credentialsType\": \"MQTT_BASIC\",\"clientId\": \"test_clientId\",\"username\": \"test_username\",\"password\": \"test_password\""; + byte[] result = createMqttClientAndPublish(requestCredentials).getPayloadBytes(); + JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); + + Device createdDevice = deviceService.findDeviceByTenantIdAndName(savedTenant.getTenantId(), "Test Provision device"); + + Assert.assertNotNull(createdDevice); + Assert.assertEquals(createdDevice.getId().toString(), response.get("deviceId").getAsString()); + + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedTenant.getTenantId(), createdDevice.getId()); + + Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.get("credentialsType").getAsString()); + Assert.assertEquals(deviceCredentials.getCredentialsId(), response.get("credentialsId").getAsString()); + Assert.assertEquals(deviceCredentials.getCredentialsType().name(), "MQTT_BASIC"); + Assert.assertEquals(deviceCredentials.getCredentialsId(), EncryptionUtil.getSha3Hash("|", "test_clientId", "test_username")); + + BasicMqttCredentials mqttCredentials = new BasicMqttCredentials(); + mqttCredentials.setClientId("test_clientId"); + mqttCredentials.setUserName("test_username"); + mqttCredentials.setPassword("test_password"); + + Assert.assertEquals(deviceCredentials.getCredentialsValue(), JacksonUtil.toString(mqttCredentials)); + Assert.assertEquals(deviceCredentials.getCredentialsId(), response.get("credentialsId").getAsString()); + Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.get("provisionDeviceStatus").getAsString()); + } + + protected void processTestProvisioningCheckPreProvisionedDevice() throws Exception { + super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, "testProvisionKey", "testProvisionSecret"); + byte[] result = createMqttClientAndPublish().getPayloadBytes(); + JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); + Assert.assertEquals(savedDevice.getId().toString(), response.get("deviceId").getAsString()); + + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedTenant.getTenantId(), savedDevice.getId()); + + Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.get("credentialsType").getAsString()); + Assert.assertEquals(deviceCredentials.getCredentialsId(), response.get("credentialsId").getAsString()); + Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.get("provisionDeviceStatus").getAsString()); + } + + protected void processTestProvisioningWithBadKeyDevice() throws Exception { + super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, "testProvisionKeyOrig", "testProvisionSecret"); + byte[] result = createMqttClientAndPublish().getPayloadBytes(); + JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); + Assert.assertEquals("Provision data was not found!", response.get("errorMsg").getAsString()); + Assert.assertEquals(ProvisionResponseStatus.NOT_FOUND.name(), response.get("provisionDeviceStatus").getAsString()); + } + + protected TestMqttCallback createMqttClientAndPublish() throws Exception { + return createMqttClientAndPublish(""); + } + + protected TestMqttCallback createMqttClientAndPublish(String deviceCredentials) throws Exception { + String provisionRequestMsg = createTestProvisionMessage(deviceCredentials); + MqttAsyncClient client = getMqttAsyncClient("provision"); + TestMqttCallback onProvisionCallback = getTestMqttCallback(); + client.setCallback(onProvisionCallback); + client.subscribe(MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC, MqttQoS.AT_MOST_ONCE.value()); + Thread.sleep(2000); + client.publish(MqttTopics.DEVICE_PROVISION_REQUEST_TOPIC, new MqttMessage(provisionRequestMsg.getBytes())); + onProvisionCallback.getLatch().await(3, TimeUnit.SECONDS); + return onProvisionCallback; + } + + + protected TestMqttCallback getTestMqttCallback() { + CountDownLatch latch = new CountDownLatch(1); + return new TestMqttCallback(latch); + } + + + protected static class TestMqttCallback implements MqttCallback { + + private final CountDownLatch latch; + private Integer qoS; + private byte[] payloadBytes; + + TestMqttCallback(CountDownLatch latch) { + this.latch = latch; + } + + public int getQoS() { + return qoS; + } + + public byte[] getPayloadBytes() { + return payloadBytes; + } + + public CountDownLatch getLatch() { + return latch; + } + + @Override + public void connectionLost(Throwable throwable) { + } + + @Override + public void messageArrived(String requestTopic, MqttMessage mqttMessage) throws Exception { + qoS = mqttMessage.getQos(); + payloadBytes = mqttMessage.getPayload(); + latch.countDown(); + } + + @Override + public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { + + } + } + + protected String createTestProvisionMessage(String deviceCredentials) { + return "{\"deviceName\":\"Test Provision device\",\"provisionDeviceKey\":\"testProvisionKey\", \"provisionDeviceSecret\":\"testProvisionSecret\"" + deviceCredentials + "}"; + } +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/provision/AbstractMqttProvisionProtoDeviceTest.java b/application/src/test/java/org/thingsboard/server/mqtt/provision/AbstractMqttProvisionProtoDeviceTest.java new file mode 100644 index 0000000000..12d8f91eb6 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/provision/AbstractMqttProvisionProtoDeviceTest.java @@ -0,0 +1,305 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.provision; + +import io.netty.handler.codec.mqtt.MqttQoS; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; +import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.common.msg.EncryptionUtil; +import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.server.gen.transport.TransportProtos.CredentialsDataProto; +import org.thingsboard.server.gen.transport.TransportProtos.CredentialsType; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceCredentialsMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ValidateBasicMqttCredRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; +import org.thingsboard.server.mqtt.AbstractMqttIntegrationTest; + +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@Slf4j +public abstract class AbstractMqttProvisionProtoDeviceTest extends AbstractMqttIntegrationTest { + + @Autowired + DeviceCredentialsService deviceCredentialsService; + + @Autowired + DeviceService deviceService; + + @After + public void afterTest() throws Exception { + super.processAfterTest(); + } + + @Test + public void testProvisioningDisabledDevice() throws Exception { + processTestProvisioningDisabledDevice(); + } + + @Test + public void testProvisioningCheckPreProvisionedDevice() throws Exception { + processTestProvisioningCheckPreProvisionedDevice(); + } + + @Test + public void testProvisioningCreateNewDeviceWithoutCredentials() throws Exception { + processTestProvisioningCreateNewDeviceWithoutCredentials(); + } + + @Test + public void testProvisioningCreateNewDeviceWithAccessToken() throws Exception { + processTestProvisioningCreateNewDeviceWithAccessToken(); + } + + @Test + public void testProvisioningCreateNewDeviceWithCert() throws Exception { + processTestProvisioningCreateNewDeviceWithCert(); + } + + @Test + public void testProvisioningCreateNewDeviceWithMqttBasic() throws Exception { + processTestProvisioningCreateNewDeviceWithMqttBasic(); + } + + @Test + public void testProvisioningWithBadKeyDevice() throws Exception { + processTestProvisioningWithBadKeyDevice(); + } + + + protected void processTestProvisioningDisabledDevice() throws Exception { + super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, DeviceProfileProvisionType.DISABLED, null, null); + ProvisionDeviceResponseMsg result = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish().getPayloadBytes()); + Assert.assertNotNull(result); + Assert.assertEquals(ProvisionResponseStatus.NOT_FOUND.name(), result.getProvisionResponseStatus().toString()); + } + + protected void processTestProvisioningCreateNewDeviceWithoutCredentials() throws Exception { + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish().getPayloadBytes()); + + Device createdDevice = deviceService.findDeviceByTenantIdAndName(savedTenant.getTenantId(), "Test Provision device"); + + Assert.assertNotNull(createdDevice); + Assert.assertEquals(createdDevice.getId().getId(), new UUID(response.getDeviceCredentials().getDeviceIdMSB(), response.getDeviceCredentials().getDeviceIdLSB())); + + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedTenant.getTenantId(), createdDevice.getId()); + + Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.getDeviceCredentials().getCredentialsType().toString()); + Assert.assertEquals(deviceCredentials.getCredentialsId(), response.getDeviceCredentials().getCredentialsId()); + Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.getProvisionResponseStatus().toString()); + } + + protected void processTestProvisioningCreateNewDeviceWithAccessToken() throws Exception { + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + CredentialsDataProto requestCredentials = CredentialsDataProto.newBuilder().setValidateDeviceTokenRequestMsg(ValidateDeviceTokenRequestMsg.newBuilder().setToken("test_token").build()).build(); + + ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish(createTestsProvisionMessage(CredentialsType.ACCESS_TOKEN, requestCredentials)).getPayloadBytes()); + + Device createdDevice = deviceService.findDeviceByTenantIdAndName(savedTenant.getTenantId(), "Test Provision device"); + + Assert.assertNotNull(createdDevice); + Assert.assertEquals(createdDevice.getId().getId(), new UUID(response.getDeviceCredentials().getDeviceIdMSB(), response.getDeviceCredentials().getDeviceIdLSB())); + + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedTenant.getTenantId(), createdDevice.getId()); + + Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.getDeviceCredentials().getCredentialsType().toString()); + Assert.assertEquals(deviceCredentials.getCredentialsId(), response.getDeviceCredentials().getCredentialsId()); + Assert.assertEquals(deviceCredentials.getCredentialsType(), DeviceCredentialsType.ACCESS_TOKEN); + Assert.assertEquals(deviceCredentials.getCredentialsId(), "test_token"); + Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.getProvisionResponseStatus().toString()); + } + + protected void processTestProvisioningCreateNewDeviceWithCert() throws Exception { + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + CredentialsDataProto requestCredentials = CredentialsDataProto.newBuilder().setValidateDeviceX509CertRequestMsg(ValidateDeviceX509CertRequestMsg.newBuilder().setHash("testHash").build()).build(); + + ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish(createTestsProvisionMessage(CredentialsType.X509_CERTIFICATE, requestCredentials)).getPayloadBytes()); + + Device createdDevice = deviceService.findDeviceByTenantIdAndName(savedTenant.getTenantId(), "Test Provision device"); + + Assert.assertNotNull(createdDevice); + Assert.assertEquals(createdDevice.getId().getId(), new UUID(response.getDeviceCredentials().getDeviceIdMSB(), response.getDeviceCredentials().getDeviceIdLSB())); + + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedTenant.getTenantId(), createdDevice.getId()); + + Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.getDeviceCredentials().getCredentialsType().toString()); + Assert.assertEquals(deviceCredentials.getCredentialsId(), response.getDeviceCredentials().getCredentialsId()); + Assert.assertEquals(deviceCredentials.getCredentialsType(), DeviceCredentialsType.X509_CERTIFICATE); + + String cert = EncryptionUtil.trimNewLines(deviceCredentials.getCredentialsValue()); + String sha3Hash = EncryptionUtil.getSha3Hash(cert); + + Assert.assertEquals(deviceCredentials.getCredentialsId(), sha3Hash); + + Assert.assertEquals(deviceCredentials.getCredentialsValue(), "testHash"); + Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.getProvisionResponseStatus().toString()); + } + + protected void processTestProvisioningCreateNewDeviceWithMqttBasic() throws Exception { + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + CredentialsDataProto requestCredentials = CredentialsDataProto.newBuilder().setValidateBasicMqttCredRequestMsg( + ValidateBasicMqttCredRequestMsg.newBuilder() + .setClientId("test_clientId") + .setUserName("test_username") + .setPassword("test_password") + .build() + ).build(); + + ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish(createTestsProvisionMessage(CredentialsType.MQTT_BASIC, requestCredentials)).getPayloadBytes()); + + Device createdDevice = deviceService.findDeviceByTenantIdAndName(savedTenant.getTenantId(), "Test Provision device"); + + Assert.assertNotNull(createdDevice); + Assert.assertEquals(createdDevice.getId().getId(), new UUID(response.getDeviceCredentials().getDeviceIdMSB(), response.getDeviceCredentials().getDeviceIdLSB())); + + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedTenant.getTenantId(), createdDevice.getId()); + + Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.getDeviceCredentials().getCredentialsType().toString()); + Assert.assertEquals(deviceCredentials.getCredentialsId(), response.getDeviceCredentials().getCredentialsId()); + Assert.assertEquals(deviceCredentials.getCredentialsType(), DeviceCredentialsType.MQTT_BASIC); + Assert.assertEquals(deviceCredentials.getCredentialsId(), EncryptionUtil.getSha3Hash("|", "test_clientId", "test_username")); + + BasicMqttCredentials mqttCredentials = new BasicMqttCredentials(); + mqttCredentials.setClientId("test_clientId"); + mqttCredentials.setUserName("test_username"); + mqttCredentials.setPassword("test_password"); + + Assert.assertEquals(deviceCredentials.getCredentialsValue(), JacksonUtil.toString(mqttCredentials)); + Assert.assertEquals(deviceCredentials.getCredentialsId(), response.getDeviceCredentials().getCredentialsId()); + Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.getProvisionResponseStatus().toString()); + } + + protected void processTestProvisioningCheckPreProvisionedDevice() throws Exception { + super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, "testProvisionKey", "testProvisionSecret"); + ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish().getPayloadBytes()); + Assert.assertEquals(savedDevice.getId().getId(), new UUID(response.getDeviceCredentials().getDeviceIdMSB(), response.getDeviceCredentials().getDeviceIdLSB())); + + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedTenant.getTenantId(), savedDevice.getId()); + + Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.getDeviceCredentials().getCredentialsType().toString()); + Assert.assertEquals(deviceCredentials.getCredentialsId(), response.getDeviceCredentials().getCredentialsId()); + Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.getProvisionResponseStatus().toString()); + } + + protected void processTestProvisioningWithBadKeyDevice() throws Exception { + super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, "testProvisionKeyOrig", "testProvisionSecret"); + ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish().getPayloadBytes()); + Assert.assertEquals(ProvisionResponseStatus.NOT_FOUND.name(), response.getProvisionResponseStatus().toString()); + } + + protected TestMqttCallback createMqttClientAndPublish() throws Exception { + byte[] provisionRequestMsg = createTestProvisionMessage(); + return createMqttClientAndPublish(provisionRequestMsg); + } + + protected TestMqttCallback createMqttClientAndPublish(byte[] provisionRequestMsg) throws Exception { + MqttAsyncClient client = getMqttAsyncClient("provision"); + TestMqttCallback onProvisionCallback = getTestMqttCallback(); + client.setCallback(onProvisionCallback); + client.subscribe(MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC, MqttQoS.AT_MOST_ONCE.value()); + Thread.sleep(2000); + client.publish(MqttTopics.DEVICE_PROVISION_REQUEST_TOPIC, new MqttMessage(provisionRequestMsg)); + onProvisionCallback.getLatch().await(3, TimeUnit.SECONDS); + return onProvisionCallback; + } + + + protected TestMqttCallback getTestMqttCallback() { + CountDownLatch latch = new CountDownLatch(1); + return new TestMqttCallback(latch); + } + + + protected static class TestMqttCallback implements MqttCallback { + + private final CountDownLatch latch; + private Integer qoS; + private byte[] payloadBytes; + + TestMqttCallback(CountDownLatch latch) { + this.latch = latch; + } + + public int getQoS() { + return qoS; + } + + public byte[] getPayloadBytes() { + return payloadBytes; + } + + public CountDownLatch getLatch() { + return latch; + } + + @Override + public void connectionLost(Throwable throwable) { + } + + @Override + public void messageArrived(String requestTopic, MqttMessage mqttMessage) throws Exception { + qoS = mqttMessage.getQos(); + payloadBytes = mqttMessage.getPayload(); + latch.countDown(); + } + + @Override + public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { + + } + } + + protected byte[] createTestsProvisionMessage(CredentialsType credentialsType, CredentialsDataProto credentialsData) throws Exception { + return ProvisionDeviceRequestMsg.newBuilder() + .setDeviceName("Test Provision device") + .setCredentialsType(credentialsType != null ? credentialsType : CredentialsType.ACCESS_TOKEN) + .setCredentialsDataProto(credentialsData != null ? credentialsData: CredentialsDataProto.newBuilder().build()) + .setProvisionDeviceCredentialsMsg( + ProvisionDeviceCredentialsMsg.newBuilder() + .setProvisionDeviceKey("testProvisionKey") + .setProvisionDeviceSecret("testProvisionSecret") + ).build() + .toByteArray(); + } + + + protected byte[] createTestProvisionMessage() throws Exception { + return createTestsProvisionMessage(null, null); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/provision/sql/MqttProvisionDeviceJsonSqlTest.java b/application/src/test/java/org/thingsboard/server/mqtt/provision/sql/MqttProvisionDeviceJsonSqlTest.java new file mode 100644 index 0000000000..c9e6495fd3 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/provision/sql/MqttProvisionDeviceJsonSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.provision.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.provision.AbstractMqttProvisionJsonDeviceTest; + +@DaoSqlTest +public class MqttProvisionDeviceJsonSqlTest extends AbstractMqttProvisionJsonDeviceTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/provision/sql/MqttProvisionDeviceProtoSqlTest.java b/application/src/test/java/org/thingsboard/server/mqtt/provision/sql/MqttProvisionDeviceProtoSqlTest.java new file mode 100644 index 0000000000..7a8170d01e --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/provision/sql/MqttProvisionDeviceProtoSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.provision.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.provision.AbstractMqttProvisionProtoDeviceTest; + +@DaoSqlTest +public class MqttProvisionDeviceProtoSqlTest extends AbstractMqttProvisionProtoDeviceTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcDefaultIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcDefaultIntegrationTest.java new file mode 100644 index 0000000000..23b93f427e --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcDefaultIntegrationTest.java @@ -0,0 +1,137 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.rpc; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.protobuf.InvalidProtocolBufferException; +import com.nimbusds.jose.util.StandardCharset; +import com.datastax.oss.driver.api.core.uuid.Uuids; +import io.netty.handler.codec.mqtt.MqttQoS; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.server.service.security.AccessValidator; + +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Valerii Sosliuk + */ +@Slf4j +public abstract class AbstractMqttServerSideRpcDefaultIntegrationTest extends AbstractMqttServerSideRpcIntegrationTest { + + @Before + public void beforeTest() throws Exception { + processBeforeTest("RPC test device", "RPC test gateway", null, null, null); + } + + @After + public void afterTest() throws Exception { + super.processAfterTest(); + } + + @Test + public void testServerMqttOneWayRpcDeviceOffline() throws Exception { + String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"24\",\"value\": 1},\"timeout\": 6000}"; + String deviceId = savedDevice.getId().getId().toString(); + + doPostAsync("/api/plugins/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().is(409), + asyncContextTimeoutToUseRpcPlugin); + } + + @Test + public void testServerMqttOneWayRpcDeviceDoesNotExist() throws Exception { + String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"25\",\"value\": 1}}"; + String nonExistentDeviceId = Uuids.timeBased().toString(); + + String result = doPostAsync("/api/plugins/rpc/oneway/" + nonExistentDeviceId, setGpioRequest, String.class, + status().isNotFound()); + Assert.assertEquals(AccessValidator.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result); + } + + @Test + public void testServerMqttTwoWayRpcDeviceOffline() throws Exception { + String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"27\",\"value\": 1},\"timeout\": 6000}"; + String deviceId = savedDevice.getId().getId().toString(); + + doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().is(409), + asyncContextTimeoutToUseRpcPlugin); + } + + @Test + public void testServerMqttTwoWayRpcDeviceDoesNotExist() throws Exception { + String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"28\",\"value\": 1}}"; + String nonExistentDeviceId = Uuids.timeBased().toString(); + + String result = doPostAsync("/api/plugins/rpc/twoway/" + nonExistentDeviceId, setGpioRequest, String.class, + status().isNotFound()); + Assert.assertEquals(AccessValidator.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result); + } + + @Test + public void testServerMqttOneWayRpc() throws Exception { + processOneWayRpcTest(); + } + + @Test + public void testServerMqttTwoWayRpc() throws Exception { + processTwoWayRpcTest(); + } + + @Test + public void testGatewayServerMqttOneWayRpc() throws Exception { + processOneWayRpcTestGateway("Gateway Device OneWay RPC"); + } + + @Test + public void testGatewayServerMqttTwoWayRpc() throws Exception { + processTwoWayRpcTestGateway("Gateway Device TwoWay RPC"); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java index 54bd9a4836..e08f1665a4 100644 --- a/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java @@ -15,7 +15,11 @@ */ package org.thingsboard.server.mqtt.rpc; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.protobuf.InvalidProtocolBufferException; +import com.nimbusds.jose.util.StandardCharset; import io.netty.handler.codec.mqtt.MqttQoS; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -23,15 +27,28 @@ import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; import org.eclipse.paho.client.mqttv3.MqttAsyncClient; import org.eclipse.paho.client.mqttv3.MqttCallback; import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; -import org.junit.*; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.controller.AbstractControllerTest; -import org.thingsboard.server.mqtt.telemetry.AbstractMqttTelemetryIntegrationTest; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.server.mqtt.AbstractMqttIntegrationTest; import org.thingsboard.server.service.security.AccessValidator; import java.util.Arrays; @@ -47,72 +64,27 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. * @author Valerii Sosliuk */ @Slf4j -public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractControllerTest { +public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractMqttIntegrationTest { - private static final String MQTT_URL = "tcp://localhost:1883"; - private static final Long TIME_TO_HANDLE_REQUEST = 500L; + protected static final String DEVICE_RESPONSE = "{\"value1\":\"A\",\"value2\":\"B\"}"; - private Tenant savedTenant; - private User tenantAdmin; - private Long asyncContextTimeoutToUseRpcPlugin; - - private static final AtomicInteger atomicInteger = new AtomicInteger(2); - - - @Before - public void beforeTest() throws Exception { - loginSysAdmin(); + protected Long asyncContextTimeoutToUseRpcPlugin; + protected void processBeforeTest(String deviceName, String gatewayName, TransportPayloadType payloadType, String telemetryTopic, String attributesTopic) throws Exception { + super.processBeforeTest(deviceName, gatewayName, payloadType, telemetryTopic, attributesTopic); asyncContextTimeoutToUseRpcPlugin = 10000L; - - Tenant tenant = new Tenant(); - tenant.setTitle("My tenant"); - savedTenant = doPost("/api/tenant", tenant, Tenant.class); - Assert.assertNotNull(savedTenant); - - tenantAdmin = new User(); - tenantAdmin.setAuthority(Authority.TENANT_ADMIN); - tenantAdmin.setTenantId(savedTenant.getId()); - tenantAdmin.setEmail("tenant" + atomicInteger.getAndIncrement() + "@thingsboard.org"); - tenantAdmin.setFirstName("Joe"); - tenantAdmin.setLastName("Downs"); - - createUserAndLogin(tenantAdmin, "testPassword1"); } - @After - public void afterTest() throws Exception { - loginSysAdmin(); - if (savedTenant != null) { - doDelete("/api/tenant/" + savedTenant.getId().getId().toString()).andExpect(status().isOk()); - } - } - - @Test - public void testServerMqttOneWayRpc() throws Exception { - Device device = new Device(); - device.setName("Test One-Way Server-Side RPC"); - device.setType("default"); - Device savedDevice = getSavedDevice(device); - DeviceCredentials deviceCredentials = getDeviceCredentials(savedDevice); - assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId()); - String accessToken = deviceCredentials.getCredentialsId(); - assertNotNull(accessToken); - - String clientId = MqttAsyncClient.generateClientId(); - MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId); - - MqttConnectOptions options = new MqttConnectOptions(); - options.setUserName(accessToken); - client.connect(options).waitForCompletion(); + protected void processOneWayRpcTest() throws Exception { + MqttAsyncClient client = getMqttAsyncClient(accessToken); CountDownLatch latch = new CountDownLatch(1); TestMqttCallback callback = new TestMqttCallback(client, latch); client.setCallback(callback); - client.subscribe("v1/devices/me/rpc/request/+", MqttQoS.AT_MOST_ONCE.value()); + client.subscribe(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC, MqttQoS.AT_MOST_ONCE.value()); - Thread.sleep(2000); + Thread.sleep(1000); String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}"; String deviceId = savedDevice.getId().getId().toString(); @@ -122,100 +94,112 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractC assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); } - @Test - public void testServerMqttOneWayRpcDeviceOffline() throws Exception { - Device device = new Device(); - device.setName("Test One-Way Server-Side RPC Device Offline"); - device.setType("default"); - Device savedDevice = getSavedDevice(device); - DeviceCredentials deviceCredentials = getDeviceCredentials(savedDevice); - assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId()); - String accessToken = deviceCredentials.getCredentialsId(); - assertNotNull(accessToken); - - String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"24\",\"value\": 1},\"timeout\": 6000}"; + protected void processOneWayRpcTestGateway(String deviceName) throws Exception { + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + String payload = "{\"device\":\"" + deviceName + "\"}"; + byte[] payloadBytes = payload.getBytes(); + validateOneWayRpcGatewayResponse(deviceName, client, payloadBytes); + } + + protected void processTwoWayRpcTest() throws Exception { + MqttAsyncClient client = getMqttAsyncClient(accessToken); + client.subscribe(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC, 1); + + CountDownLatch latch = new CountDownLatch(1); + TestMqttCallback callback = new TestMqttCallback(client, latch); + client.setCallback(callback); + + Thread.sleep(1000); + + String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"26\",\"value\": 1}}"; String deviceId = savedDevice.getId().getId().toString(); - doPostAsync("/api/plugins/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().is(409), - asyncContextTimeoutToUseRpcPlugin); + String result = doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk()); + String expected = "{\"value1\":\"A\",\"value2\":\"B\"}"; + latch.await(3, TimeUnit.SECONDS); + Assert.assertEquals(expected, result); } - @Test - public void testServerMqttOneWayRpcDeviceDoesNotExist() throws Exception { - String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"25\",\"value\": 1}}"; - String nonExistentDeviceId = UUIDs.timeBased().toString(); + protected void processTwoWayRpcTestGateway(String deviceName) throws Exception { + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + + String payload = "{\"device\":\"" + deviceName + "\"}"; + byte[] payloadBytes = payload.getBytes(); - String result = doPostAsync("/api/plugins/rpc/oneway/" + nonExistentDeviceId, setGpioRequest, String.class, - status().isNotFound()); - Assert.assertEquals(AccessValidator.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result); + validateTwoWayRpcGateway(deviceName, client, payloadBytes); } - @Test - public void testServerMqttTwoWayRpc() throws Exception { - Device device = new Device(); - device.setName("Test Two-Way Server-Side RPC"); - device.setType("default"); - Device savedDevice = getSavedDevice(device); - DeviceCredentials deviceCredentials = getDeviceCredentials(savedDevice); - assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId()); - String accessToken = deviceCredentials.getCredentialsId(); - assertNotNull(accessToken); + protected void validateOneWayRpcGatewayResponse(String deviceName, MqttAsyncClient client, byte[] payloadBytes) throws Exception { + publishMqttMsg(client, payloadBytes, MqttTopics.GATEWAY_CONNECT_TOPIC); - String clientId = MqttAsyncClient.generateClientId(); - MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId); + Device savedDevice = doExecuteWithRetriesAndInterval( + () -> getDeviceByName(deviceName), + 20, + 100 + ); + assertNotNull(savedDevice); - MqttConnectOptions options = new MqttConnectOptions(); - options.setUserName(accessToken); - client.connect(options).waitForCompletion(); - client.subscribe("v1/devices/me/rpc/request/+", 1); - client.setCallback(new TestMqttCallback(client, new CountDownLatch(1))); + CountDownLatch latch = new CountDownLatch(1); + TestMqttCallback callback = new TestMqttCallback(client, latch); + client.setCallback(callback); - Thread.sleep(2000); + client.subscribe(MqttTopics.GATEWAY_RPC_TOPIC, MqttQoS.AT_MOST_ONCE.value()); - String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"26\",\"value\": 1}}"; - String deviceId = savedDevice.getId().getId().toString(); + Thread.sleep(1000); - String result = doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk()); - Assert.assertEquals("{\"value1\":\"A\",\"value2\":\"B\"}", result); + String setGpioRequest = "{\"method\": \"toggle_gpio\", \"params\": {\"pin\":1}}"; + String deviceId = savedDevice.getId().getId().toString(); + String result = doPostAsync("/api/plugins/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().isOk()); + Assert.assertTrue(StringUtils.isEmpty(result)); + latch.await(3, TimeUnit.SECONDS); + assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); } - @Test - public void testServerMqttTwoWayRpcDeviceOffline() throws Exception { - Device device = new Device(); - device.setName("Test Two-Way Server-Side RPC Device Offline"); - device.setType("default"); - Device savedDevice = getSavedDevice(device); - DeviceCredentials deviceCredentials = getDeviceCredentials(savedDevice); - assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId()); - String accessToken = deviceCredentials.getCredentialsId(); - assertNotNull(accessToken); - - String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"27\",\"value\": 1},\"timeout\": 6000}"; - String deviceId = savedDevice.getId().getId().toString(); + protected void validateTwoWayRpcGateway(String deviceName, MqttAsyncClient client, byte[] payloadBytes) throws Exception { + publishMqttMsg(client, payloadBytes, MqttTopics.GATEWAY_CONNECT_TOPIC); - doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().is(409), - asyncContextTimeoutToUseRpcPlugin); - } + Device savedDevice = doExecuteWithRetriesAndInterval( + () -> getDeviceByName(deviceName), + 20, + 100 + ); + assertNotNull(savedDevice); + + CountDownLatch latch = new CountDownLatch(1); + TestMqttCallback callback = new TestMqttCallback(client, latch); + client.setCallback(callback); + + client.subscribe(MqttTopics.GATEWAY_RPC_TOPIC, MqttQoS.AT_MOST_ONCE.value()); - @Test - public void testServerMqttTwoWayRpcDeviceDoesNotExist() throws Exception { - String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"28\",\"value\": 1}}"; - String nonExistentDeviceId = UUIDs.timeBased().toString(); + Thread.sleep(1000); - String result = doPostAsync("/api/plugins/rpc/twoway/" + nonExistentDeviceId, setGpioRequest, String.class, - status().isNotFound()); - Assert.assertEquals(AccessValidator.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result); + String setGpioRequest = "{\"method\": \"toggle_gpio\", \"params\": {\"pin\":1}}"; + String deviceId = savedDevice.getId().getId().toString(); + String result = doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk()); + latch.await(3, TimeUnit.SECONDS); + String expected = "{\"success\":true}"; + assertEquals(expected, result); + assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); } - private Device getSavedDevice(Device device) throws Exception { - return doPost("/api/device", device, Device.class); + private Device getDeviceByName(String deviceName) throws Exception { + return doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class); } - private DeviceCredentials getDeviceCredentials(Device savedDevice) throws Exception { - return doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class); + protected MqttMessage processMessageArrived(String requestTopic, MqttMessage mqttMessage) throws MqttException, InvalidProtocolBufferException { + MqttMessage message = new MqttMessage(); + if (requestTopic.startsWith(MqttTopics.BASE_DEVICE_API_TOPIC)) { + message.setPayload(DEVICE_RESPONSE.getBytes(StandardCharset.UTF_8)); + } else { + JsonNode requestMsgNode = JacksonUtil.toJsonNode(new String(mqttMessage.getPayload(), StandardCharset.UTF_8)); + String deviceName = requestMsgNode.get("device").asText(); + int requestId = requestMsgNode.get("data").get("id").asInt(); + message.setPayload(("{\"device\": \"" + deviceName + "\", \"id\": " + requestId + ", \"data\": {\"success\": true}}").getBytes(StandardCharset.UTF_8)); + } + return message; } - private static class TestMqttCallback implements MqttCallback { + private class TestMqttCallback implements MqttCallback { private final MqttAsyncClient client; private final CountDownLatch latch; @@ -237,11 +221,9 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractC @Override public void messageArrived(String requestTopic, MqttMessage mqttMessage) throws Exception { log.info("Message Arrived: " + Arrays.toString(mqttMessage.getPayload())); - MqttMessage message = new MqttMessage(); String responseTopic = requestTopic.replace("request", "response"); - message.setPayload("{\"value1\":\"A\", \"value2\":\"B\"}".getBytes("UTF-8")); qoS = mqttMessage.getQos(); - client.publish(responseTopic, message); + client.publish(responseTopic, processMessageArrived(requestTopic, mqttMessage)); latch.countDown(); } diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcJsonIntegrationTest.java new file mode 100644 index 0000000000..d9ff14e1d2 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcJsonIntegrationTest.java @@ -0,0 +1,66 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.rpc; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.thingsboard.server.common.data.TransportPayloadType; + +@Slf4j +public abstract class AbstractMqttServerSideRpcJsonIntegrationTest extends AbstractMqttServerSideRpcIntegrationTest { + + @Before + public void beforeTest() throws Exception { + processBeforeTest("RPC test device", "RPC test gateway", TransportPayloadType.JSON, null, null); + } + + @After + public void afterTest() throws Exception { + super.processAfterTest(); + } + + @Test + public void testServerMqttOneWayRpc() throws Exception { + processOneWayRpcTest(); + } + + @Test + public void testServerMqttTwoWayRpc() throws Exception { + processTwoWayRpcTest(); + } + + @Test + public void testGatewayServerMqttOneWayRpc() throws Exception { + processOneWayRpcTestGateway("Gateway Device OneWay RPC Json"); + } + + @Test + public void testGatewayServerMqttTwoWayRpc() throws Exception { + processTwoWayRpcTestGateway("Gateway Device TwoWay RPC Json"); + } + + protected void processOneWayRpcTestGateway(String deviceName) throws Exception { + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + String payload = "{\"device\": \"" + deviceName + "\", \"type\": \"" + TransportPayloadType.JSON.name() + "\"}"; + byte[] payloadBytes = payload.getBytes(); + validateOneWayRpcGatewayResponse(deviceName, client, payloadBytes); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcProtoIntegrationTest.java new file mode 100644 index 0000000000..759a5da912 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcProtoIntegrationTest.java @@ -0,0 +1,114 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.rpc; + +import com.google.protobuf.InvalidProtocolBufferException; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.gen.transport.TransportApiProtos; +import org.thingsboard.server.gen.transport.TransportProtos; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@Slf4j +public abstract class AbstractMqttServerSideRpcProtoIntegrationTest extends AbstractMqttServerSideRpcIntegrationTest { + + @Before + public void beforeTest() throws Exception { + processBeforeTest("RPC test device", "RPC test gateway", TransportPayloadType.PROTOBUF, null, null); + } + + @After + public void afterTest() throws Exception { + super.processAfterTest(); + } + + @Test + public void testServerMqttOneWayRpc() throws Exception { + processOneWayRpcTest(); + } + + @Test + public void testServerMqttTwoWayRpc() throws Exception { + processTwoWayRpcTest(); + } + + @Test + public void testGatewayServerMqttOneWayRpc() throws Exception { + processOneWayRpcTestGateway("Gateway Device OneWay RPC Proto"); + } + + @Test + public void testGatewayServerMqttTwoWayRpc() throws Exception { + processTwoWayRpcTestGateway("Gateway Device TwoWay RPC Proto"); + } + + protected void processTwoWayRpcTestGateway(String deviceName) throws Exception { + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + TransportApiProtos.ConnectMsg connectMsgProto = getConnectProto(deviceName); + byte[] payloadBytes = connectMsgProto.toByteArray(); + validateTwoWayRpcGateway(deviceName, client, payloadBytes); + } + + protected void processOneWayRpcTestGateway(String deviceName) throws Exception { + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + TransportApiProtos.ConnectMsg connectMsgProto = getConnectProto(deviceName); + byte[] payloadBytes = connectMsgProto.toByteArray(); + validateOneWayRpcGatewayResponse(deviceName, client, payloadBytes); + } + + + private TransportApiProtos.ConnectMsg getConnectProto(String deviceName) { + TransportApiProtos.ConnectMsg.Builder builder = TransportApiProtos.ConnectMsg.newBuilder(); + builder.setDeviceName(deviceName); + builder.setDeviceType(TransportPayloadType.PROTOBUF.name()); + return builder.build(); + } + + protected MqttMessage processMessageArrived(String requestTopic, MqttMessage mqttMessage) throws MqttException, InvalidProtocolBufferException { + MqttMessage message = new MqttMessage(); + if (requestTopic.startsWith(MqttTopics.BASE_DEVICE_API_TOPIC)) { + TransportProtos.ToDeviceRpcResponseMsg toDeviceRpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() + .setPayload(DEVICE_RESPONSE) + .setRequestId(0) + .build(); + message.setPayload(toDeviceRpcResponseMsg.toByteArray()); + } else { + TransportApiProtos.GatewayDeviceRpcRequestMsg msg = TransportApiProtos.GatewayDeviceRpcRequestMsg.parseFrom(mqttMessage.getPayload()); + String deviceName = msg.getDeviceName(); + int requestId = msg.getRpcRequestMsg().getRequestId(); + TransportApiProtos.GatewayRpcResponseMsg gatewayRpcResponseMsg = TransportApiProtos.GatewayRpcResponseMsg.newBuilder() + .setDeviceName(deviceName) + .setId(requestId) + .setData("{\"success\": true}") + .build(); + message.setPayload(gatewayRpcResponseMsg.toByteArray()); + } + return message; + } + + + +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/nosql/MqttServerSideRpcNoSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/nosql/MqttServerSideRpcNoSqlIntegrationTest.java index e90c48a974..6a5cb69c52 100644 --- a/application/src/test/java/org/thingsboard/server/mqtt/rpc/nosql/MqttServerSideRpcNoSqlIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/nosql/MqttServerSideRpcNoSqlIntegrationTest.java @@ -16,11 +16,11 @@ package org.thingsboard.server.mqtt.rpc.nosql; import org.thingsboard.server.dao.service.DaoNoSqlTest; -import org.thingsboard.server.mqtt.rpc.AbstractMqttServerSideRpcIntegrationTest; +import org.thingsboard.server.mqtt.rpc.AbstractMqttServerSideRpcDefaultIntegrationTest; /** * Created by Valerii Sosliuk on 8/22/2017. */ @DaoNoSqlTest -public class MqttServerSideRpcNoSqlIntegrationTest extends AbstractMqttServerSideRpcIntegrationTest { +public class MqttServerSideRpcNoSqlIntegrationTest extends AbstractMqttServerSideRpcDefaultIntegrationTest { } diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcJsonSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcJsonSqlIntegrationTest.java new file mode 100644 index 0000000000..4d4e900767 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcJsonSqlIntegrationTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.rpc.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.rpc.AbstractMqttServerSideRpcJsonIntegrationTest; + +@DaoSqlTest +public class MqttServerSideRpcJsonSqlIntegrationTest extends AbstractMqttServerSideRpcJsonIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcProtoSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcProtoSqlIntegrationTest.java new file mode 100644 index 0000000000..7fb91a636c --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcProtoSqlIntegrationTest.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.rpc.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.rpc.AbstractMqttServerSideRpcProtoIntegrationTest; + + +@DaoSqlTest +public class MqttServerSideRpcProtoSqlIntegrationTest extends AbstractMqttServerSideRpcProtoIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcSqlIntegrationTest.java index dc2511f4c3..7bddfbbe52 100644 --- a/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcSqlIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcSqlIntegrationTest.java @@ -16,11 +16,11 @@ package org.thingsboard.server.mqtt.rpc.sql; import org.thingsboard.server.dao.service.DaoSqlTest; -import org.thingsboard.server.mqtt.rpc.AbstractMqttServerSideRpcIntegrationTest; +import org.thingsboard.server.mqtt.rpc.AbstractMqttServerSideRpcDefaultIntegrationTest; /** * Created by Valerii Sosliuk on 8/22/2017. */ @DaoSqlTest -public class MqttServerSideRpcSqlIntegrationTest extends AbstractMqttServerSideRpcIntegrationTest { +public class MqttServerSideRpcSqlIntegrationTest extends AbstractMqttServerSideRpcDefaultIntegrationTest { } diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/AbstractMqttTelemetryIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/AbstractMqttTelemetryIntegrationTest.java deleted file mode 100644 index 47e9537ef9..0000000000 --- a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/AbstractMqttTelemetryIntegrationTest.java +++ /dev/null @@ -1,163 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.mqtt.telemetry; - -import io.netty.handler.codec.mqtt.MqttQoS; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.paho.client.mqttv3.*; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -import org.springframework.web.util.UriComponentsBuilder; -import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.security.DeviceCredentials; -import org.thingsboard.server.controller.AbstractControllerTest; -import org.thingsboard.server.dao.service.DaoNoSqlTest; - -import java.net.URI; -import java.util.*; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -/** - * @author Valerii Sosliuk - */ -@Slf4j -public abstract class AbstractMqttTelemetryIntegrationTest extends AbstractControllerTest { - - private static final String MQTT_URL = "tcp://localhost:1883"; - - private Device savedDevice; - private String accessToken; - - @Before - public void beforeTest() throws Exception { - loginTenantAdmin(); - - Device device = new Device(); - device.setName("Test device"); - device.setType("default"); - savedDevice = doPost("/api/device", device, Device.class); - - DeviceCredentials deviceCredentials = - doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class); - - assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId()); - accessToken = deviceCredentials.getCredentialsId(); - assertNotNull(accessToken); - } - - @Test - public void testPushMqttRpcData() throws Exception { - String clientId = MqttAsyncClient.generateClientId(); - MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId); - - MqttConnectOptions options = new MqttConnectOptions(); - options.setUserName(accessToken); - client.connect(options); - Thread.sleep(3000); - MqttMessage message = new MqttMessage(); - message.setPayload("{\"key1\":\"value1\", \"key2\":true, \"key3\": 3.0, \"key4\": 4}".getBytes()); - client.publish("v1/devices/me/telemetry", message); - - String deviceId = savedDevice.getId().getId().toString(); - - Thread.sleep(2000); - List actualKeys = doGetAsync("/api/plugins/telemetry/DEVICE/" + deviceId + "/keys/timeseries", List.class); - Set actualKeySet = new HashSet<>(actualKeys); - - List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4"); - Set expectedKeySet = new HashSet<>(expectedKeys); - - assertEquals(expectedKeySet, actualKeySet); - - String getTelemetryValuesUrl = "/api/plugins/telemetry/DEVICE/" + deviceId + "/values/timeseries?keys=" + String.join(",", actualKeySet); - Map>> values = doGetAsync(getTelemetryValuesUrl, Map.class); - - assertEquals("value1", values.get("key1").get(0).get("value")); - assertEquals("true", values.get("key2").get(0).get("value")); - assertEquals("3.0", values.get("key3").get(0).get("value")); - assertEquals("4", values.get("key4").get(0).get("value")); - } - - -// @Test - Unstable - public void testMqttQoSLevel() throws Exception { - String clientId = MqttAsyncClient.generateClientId(); - MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId); - - MqttConnectOptions options = new MqttConnectOptions(); - options.setUserName(accessToken); - CountDownLatch latch = new CountDownLatch(1); - TestMqttCallback callback = new TestMqttCallback(client, latch); - client.setCallback(callback); - client.connect(options).waitForCompletion(5000); - client.subscribe("v1/devices/me/attributes", MqttQoS.AT_MOST_ONCE.value()); - String payload = "{\"key\":\"uniqueValue\"}"; -// TODO 3.1: we need to acknowledge subscription only after it is processed by device actor and not when the message is pushed to queue. -// MqttClient -> SUB REQUEST -> Transport -> Kafka -> Device Actor (subscribed) -// MqttClient <- SUB_ACK <- Transport - Thread.sleep(5000); - doPostAsync("/api/plugins/telemetry/" + savedDevice.getId() + "/SHARED_SCOPE", payload, String.class, status().isOk()); - latch.await(10, TimeUnit.SECONDS); - assertEquals(payload, callback.getPayload()); - assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); - } - - private static class TestMqttCallback implements MqttCallback { - - private final MqttAsyncClient client; - private final CountDownLatch latch; - private volatile Integer qoS; - private volatile String payload; - - String getPayload() { - return payload; - } - - TestMqttCallback(MqttAsyncClient client, CountDownLatch latch) { - this.client = client; - this.latch = latch; - } - - int getQoS() { - return qoS; - } - - @Override - public void connectionLost(Throwable throwable) { - log.error("Client connection lost", throwable); - } - - @Override - public void messageArrived(String requestTopic, MqttMessage mqttMessage) { - payload = new String(mqttMessage.getPayload()); - qoS = mqttMessage.getQos(); - latch.countDown(); - } - - @Override - public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { - - } - } - - -} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/AbstractMqttAttributesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/AbstractMqttAttributesIntegrationTest.java new file mode 100644 index 0000000000..5ac0746a43 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/AbstractMqttAttributesIntegrationTest.java @@ -0,0 +1,182 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.attributes; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.mqtt.AbstractMqttIntegrationTest; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@Slf4j +public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqttIntegrationTest { + + protected static final String PAYLOAD_VALUES_STR = "{\"key1\":\"value1\", \"key2\":true, \"key3\": 3.0, \"key4\": 4," + + " \"key5\": {\"someNumber\": 42, \"someArray\": [1,2,3], \"someNestedObject\": {\"key\": \"value\"}}}"; + + @Before + public void beforeTest() throws Exception { + processBeforeTest("Test Post Attributes device", "Test Post Attributes gateway", null, null, null); + } + + @After + public void afterTest() throws Exception { + processAfterTest(); + } + + @Test + public void testPushMqttAttributes() throws Exception { + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + processAttributesTest(MqttTopics.DEVICE_ATTRIBUTES_TOPIC, expectedKeys, PAYLOAD_VALUES_STR.getBytes()); + } + + @Test + public void testPushMqttAttributesGateway() throws Exception { + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + String deviceName1 = "Device A"; + String deviceName2 = "Device B"; + String payload = getGatewayAttributesJsonPayload(deviceName1, deviceName2); + processGatewayAttributesTest(expectedKeys, payload.getBytes(), deviceName1, deviceName2); + } + + protected void processAttributesTest(String topic, List expectedKeys, byte[] payload) throws Exception { + MqttAsyncClient client = getMqttAsyncClient(accessToken); + + publishMqttMsg(client, payload, topic); + + DeviceId deviceId = savedDevice.getId(); + + long start = System.currentTimeMillis(); + long end = System.currentTimeMillis() + 5000; + + List actualKeys = null; + while (start <= end) { + actualKeys = doGetAsync("/api/plugins/telemetry/DEVICE/" + deviceId + "/keys/attributes/CLIENT_SCOPE", List.class); + if (actualKeys.size() == expectedKeys.size()) { + break; + } + Thread.sleep(100); + start += 100; + } + assertNotNull(actualKeys); + + Set actualKeySet = new HashSet<>(actualKeys); + + Set expectedKeySet = new HashSet<>(expectedKeys); + + assertEquals(expectedKeySet, actualKeySet); + + String getAttributesValuesUrl = getAttributesValuesUrl(deviceId, actualKeySet); + List> values = doGetAsync(getAttributesValuesUrl, List.class); + assertAttributesValues(values, expectedKeySet); + String deleteAttributesUrl = "/api/plugins/telemetry/DEVICE/" + deviceId + "/CLIENT_SCOPE?keys=" + String.join(",", actualKeySet); + doDelete(deleteAttributesUrl); + } + + protected void processGatewayAttributesTest(List expectedKeys, byte[] payload, String firstDeviceName, String secondDeviceName) throws Exception { + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + + publishMqttMsg(client, payload, MqttTopics.GATEWAY_ATTRIBUTES_TOPIC); + + Device firstDevice = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + firstDeviceName, Device.class), + 20, + 100); + + assertNotNull(firstDevice); + + Device secondDevice = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + secondDeviceName, Device.class), + 20, + 100); + + assertNotNull(secondDevice); + + Thread.sleep(2000); + + List firstDeviceActualKeys = doGetAsync("/api/plugins/telemetry/DEVICE/" + firstDevice.getId() + "/keys/attributes/CLIENT_SCOPE", List.class); + Set firstDeviceActualKeySet = new HashSet<>(firstDeviceActualKeys); + + List secondDeviceActualKeys = doGetAsync("/api/plugins/telemetry/DEVICE/" + secondDevice.getId() + "/keys/attributes/CLIENT_SCOPE", List.class); + Set secondDeviceActualKeySet = new HashSet<>(secondDeviceActualKeys); + + Set expectedKeySet = new HashSet<>(expectedKeys); + + assertEquals(expectedKeySet, firstDeviceActualKeySet); + assertEquals(expectedKeySet, secondDeviceActualKeySet); + + String getAttributesValuesUrlFirstDevice = getAttributesValuesUrl(firstDevice.getId(), firstDeviceActualKeySet); + String getAttributesValuesUrlSecondDevice = getAttributesValuesUrl(firstDevice.getId(), secondDeviceActualKeySet); + + List> firstDeviceValues = doGetAsync(getAttributesValuesUrlFirstDevice, List.class); + List> secondDeviceValues = doGetAsync(getAttributesValuesUrlSecondDevice, List.class); + + assertAttributesValues(firstDeviceValues, expectedKeySet); + assertAttributesValues(secondDeviceValues, expectedKeySet); + + } + + protected void assertAttributesValues(List> deviceValues, Set expectedKeySet) { + for (Map map : deviceValues) { + String key = (String) map.get("key"); + Object value = map.get("value"); + assertTrue(expectedKeySet.contains(key)); + switch (key) { + case "key1": + assertEquals("value1", value); + break; + case "key2": + assertEquals(true, value); + break; + case "key3": + assertEquals(3.0, value); + break; + case "key4": + assertEquals(4, value); + break; + case "key5": + assertNotNull(value); + assertEquals(3, ((LinkedHashMap) value).size()); + assertEquals(42, ((LinkedHashMap) value).get("someNumber")); + assertEquals(Arrays.asList(1, 2, 3), ((LinkedHashMap) value).get("someArray")); + LinkedHashMap someNestedObject = (LinkedHashMap) ((LinkedHashMap) value).get("someNestedObject"); + assertEquals("value", someNestedObject.get("key")); + break; + } + } + } + + protected String getGatewayAttributesJsonPayload(String deviceA, String deviceB) { + return "{\"" + deviceA + "\": " + PAYLOAD_VALUES_STR + ", \"" + deviceB + "\": " + PAYLOAD_VALUES_STR + "}"; + } + + private String getAttributesValuesUrl(DeviceId deviceId, Set actualKeySet) { + return "/api/plugins/telemetry/DEVICE/" + deviceId + "/values/attributes/CLIENT_SCOPE?keys=" + String.join(",", actualKeySet); + } +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/AbstractMqttAttributesJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/AbstractMqttAttributesJsonIntegrationTest.java new file mode 100644 index 0000000000..fc79131ef1 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/AbstractMqttAttributesJsonIntegrationTest.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.attributes; + +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.TransportPayloadType; + +import java.util.Arrays; +import java.util.List; + +@Slf4j +public abstract class AbstractMqttAttributesJsonIntegrationTest extends AbstractMqttAttributesIntegrationTest { + + private static final String POST_DATA_ATTRIBUTES_TOPIC = "data/attributes"; + + @Before + public void beforeTest() throws Exception { + processBeforeTest("Test Post Attributes device", "Test Post Attributes gateway", TransportPayloadType.JSON, null, POST_DATA_ATTRIBUTES_TOPIC); + } + + @After + public void afterTest() throws Exception { + processAfterTest(); + } + + @Test + public void testPushMqttAttributes() throws Exception { + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + processAttributesTest(POST_DATA_ATTRIBUTES_TOPIC, expectedKeys, PAYLOAD_VALUES_STR.getBytes()); + } + + @Test + public void testPushMqttAttributesGateway() throws Exception { + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + String deviceName1 = "Device A"; + String deviceName2 = "Device B"; + String payload = getGatewayAttributesJsonPayload(deviceName1, deviceName2); + processGatewayAttributesTest(expectedKeys, payload.getBytes(), deviceName1, deviceName2); + } +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/AbstractMqttAttributesProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/AbstractMqttAttributesProtoIntegrationTest.java new file mode 100644 index 0000000000..e9adf19359 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/AbstractMqttAttributesProtoIntegrationTest.java @@ -0,0 +1,75 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.attributes; + +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.gen.transport.TransportApiProtos; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@Slf4j +public abstract class AbstractMqttAttributesProtoIntegrationTest extends AbstractMqttAttributesIntegrationTest { + + private static final String POST_DATA_ATTRIBUTES_TOPIC = "proto/attributes"; + + @Before + public void beforeTest() throws Exception { + processBeforeTest("Test Post Attributes device", "Test Post Attributes gateway", TransportPayloadType.PROTOBUF, null, POST_DATA_ATTRIBUTES_TOPIC); + } + + @After + public void afterTest() throws Exception { + processAfterTest(); + } + + @Test + public void testPushMqttAttributes() throws Exception { + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + TransportProtos.PostAttributeMsg msg = getPostAttributeMsg(expectedKeys); + processAttributesTest(POST_DATA_ATTRIBUTES_TOPIC, expectedKeys, msg.toByteArray()); + } + + @Test + public void testPushMqttAttributesGateway() throws Exception { + TransportApiProtos.GatewayAttributesMsg.Builder gatewayAttributesMsgProtoBuilder = TransportApiProtos.GatewayAttributesMsg.newBuilder(); + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + String deviceName1 = "Device A"; + String deviceName2 = "Device B"; + TransportApiProtos.AttributesMsg firstDeviceAttributesMsgProto = getDeviceAttributesMsgProto(deviceName1, expectedKeys); + TransportApiProtos.AttributesMsg secondDeviceAttributesMsgProto = getDeviceAttributesMsgProto(deviceName2, expectedKeys); + gatewayAttributesMsgProtoBuilder.addAllMsg(Arrays.asList(firstDeviceAttributesMsgProto, secondDeviceAttributesMsgProto)); + TransportApiProtos.GatewayAttributesMsg gatewayAttributesMsg = gatewayAttributesMsgProtoBuilder.build(); + processGatewayAttributesTest(expectedKeys, gatewayAttributesMsg.toByteArray(), deviceName1, deviceName2); + } + + private TransportApiProtos.AttributesMsg getDeviceAttributesMsgProto(String deviceName, List expectedKeys) { + TransportApiProtos.AttributesMsg.Builder deviceAttributesMsgBuilder = TransportApiProtos.AttributesMsg.newBuilder(); + TransportProtos.PostAttributeMsg msg = getPostAttributeMsg(expectedKeys); + deviceAttributesMsgBuilder.setDeviceName(deviceName); + deviceAttributesMsgBuilder.setMsg(msg); + return deviceAttributesMsgBuilder.build(); + } +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/nosql/MqttAttributesNoSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/nosql/MqttAttributesNoSqlIntegrationTest.java new file mode 100644 index 0000000000..e6dce8d9d5 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/nosql/MqttAttributesNoSqlIntegrationTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.attributes.nosql; + +import org.thingsboard.server.dao.service.DaoNoSqlTest; +import org.thingsboard.server.mqtt.telemetry.attributes.AbstractMqttAttributesIntegrationTest; + +@DaoNoSqlTest +public class MqttAttributesNoSqlIntegrationTest extends AbstractMqttAttributesIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/nosql/MqttAttributesNoSqlJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/nosql/MqttAttributesNoSqlJsonIntegrationTest.java new file mode 100644 index 0000000000..68ab8cff62 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/nosql/MqttAttributesNoSqlJsonIntegrationTest.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.attributes.nosql; + +import org.thingsboard.server.dao.service.DaoNoSqlTest; +import org.thingsboard.server.mqtt.telemetry.attributes.AbstractMqttAttributesIntegrationTest; +import org.thingsboard.server.mqtt.telemetry.attributes.AbstractMqttAttributesJsonIntegrationTest; + +@DaoNoSqlTest +public class MqttAttributesNoSqlJsonIntegrationTest extends AbstractMqttAttributesJsonIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/nosql/MqttAttributesNoSqlProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/nosql/MqttAttributesNoSqlProtoIntegrationTest.java new file mode 100644 index 0000000000..d508fd76c5 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/nosql/MqttAttributesNoSqlProtoIntegrationTest.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.attributes.nosql; + +import org.thingsboard.server.dao.service.DaoNoSqlTest; +import org.thingsboard.server.mqtt.telemetry.attributes.AbstractMqttAttributesIntegrationTest; +import org.thingsboard.server.mqtt.telemetry.attributes.AbstractMqttAttributesProtoIntegrationTest; + +@DaoNoSqlTest +public class MqttAttributesNoSqlProtoIntegrationTest extends AbstractMqttAttributesProtoIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/sql/MqttAttributesSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/sql/MqttAttributesSqlIntegrationTest.java new file mode 100644 index 0000000000..24dc362757 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/sql/MqttAttributesSqlIntegrationTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.attributes.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.telemetry.attributes.AbstractMqttAttributesIntegrationTest; + +@DaoSqlTest +public class MqttAttributesSqlIntegrationTest extends AbstractMqttAttributesIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/sql/MqttAttributesSqlJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/sql/MqttAttributesSqlJsonIntegrationTest.java new file mode 100644 index 0000000000..dcf1fb3026 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/sql/MqttAttributesSqlJsonIntegrationTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.attributes.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.telemetry.attributes.AbstractMqttAttributesJsonIntegrationTest; + +@DaoSqlTest +public class MqttAttributesSqlJsonIntegrationTest extends AbstractMqttAttributesJsonIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/sql/MqttAttributesSqlProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/sql/MqttAttributesSqlProtoIntegrationTest.java new file mode 100644 index 0000000000..5f486916ae --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/sql/MqttAttributesSqlProtoIntegrationTest.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.attributes.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.telemetry.attributes.AbstractMqttAttributesJsonIntegrationTest; +import org.thingsboard.server.mqtt.telemetry.attributes.AbstractMqttAttributesProtoIntegrationTest; + +@DaoSqlTest +public class MqttAttributesSqlProtoIntegrationTest extends AbstractMqttAttributesProtoIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/nosql/MqttTelemetryNoSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/nosql/MqttTelemetryNoSqlIntegrationTest.java deleted file mode 100644 index 7f978f4fc7..0000000000 --- a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/nosql/MqttTelemetryNoSqlIntegrationTest.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.mqtt.telemetry.nosql; - -import org.thingsboard.server.dao.service.DaoNoSqlTest; -import org.thingsboard.server.mqtt.telemetry.AbstractMqttTelemetryIntegrationTest; - -/** - * Created by Valerii Sosliuk on 8/22/2017. - */ -@DaoNoSqlTest -public class MqttTelemetryNoSqlIntegrationTest extends AbstractMqttTelemetryIntegrationTest { -} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/sql/MqttTelemetrySqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/sql/MqttTelemetrySqlIntegrationTest.java deleted file mode 100644 index b4b48365ac..0000000000 --- a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/sql/MqttTelemetrySqlIntegrationTest.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.mqtt.telemetry.sql; - -import org.thingsboard.server.dao.service.DaoNoSqlTest; -import org.thingsboard.server.dao.service.DaoSqlTest; -import org.thingsboard.server.mqtt.telemetry.AbstractMqttTelemetryIntegrationTest; - -/** - * Created by Valerii Sosliuk on 8/22/2017. - */ -@DaoSqlTest -public class MqttTelemetrySqlIntegrationTest extends AbstractMqttTelemetryIntegrationTest { -} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java new file mode 100644 index 0000000000..f6294cd990 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java @@ -0,0 +1,300 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.timeseries; + +import io.netty.handler.codec.mqtt.MqttQoS; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.mqtt.AbstractMqttIntegrationTest; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +public abstract class AbstractMqttTimeseriesIntegrationTest extends AbstractMqttIntegrationTest { + + protected static final String PAYLOAD_VALUES_STR = "{\"key1\":\"value1\", \"key2\":true, \"key3\": 3.0, \"key4\": 4," + + " \"key5\": {\"someNumber\": 42, \"someArray\": [1,2,3], \"someNestedObject\": {\"key\": \"value\"}}}"; + + @Before + public void beforeTest() throws Exception { + processBeforeTest("Test Post Telemetry device", "Test Post Telemetry gateway", null, null, null); + } + + @After + public void afterTest() throws Exception { + processAfterTest(); + } + + @Test + public void testPushMqttTelemetry() throws Exception { + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + processTelemetryTest(MqttTopics.DEVICE_TELEMETRY_TOPIC, expectedKeys, PAYLOAD_VALUES_STR.getBytes(), false); + } + + @Test + public void testPushMqttTelemetryWithTs() throws Exception { + String payloadStr = "{\"ts\": 10000, \"values\": " + PAYLOAD_VALUES_STR + "}"; + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + processTelemetryTest(MqttTopics.DEVICE_TELEMETRY_TOPIC, expectedKeys, payloadStr.getBytes(), true); + } + + @Test + public void testPushMqttTelemetryGateway() throws Exception { + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + String deviceName1 = "Device A"; + String deviceName2 = "Device B"; + String payload = getGatewayTelemetryJsonPayload(deviceName1, deviceName2, "10000", "20000"); + processGatewayTelemetryTest(MqttTopics.GATEWAY_TELEMETRY_TOPIC, expectedKeys, payload.getBytes(), deviceName1, deviceName2); + } + + @Test + public void testGatewayConnect() throws Exception { + String payload = "{\"device\":\"Device A\"}"; + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + publishMqttMsg(client, payload.getBytes(), MqttTopics.GATEWAY_CONNECT_TOPIC); + + String deviceName = "Device A"; + + Device device = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class), + 20, + 100); + + assertNotNull(device); + } + + protected void processTelemetryTest(String topic, List expectedKeys, byte[] payload, boolean withTs) throws Exception { + MqttAsyncClient client = getMqttAsyncClient(accessToken); + publishMqttMsg(client, payload, topic); + + String deviceId = savedDevice.getId().getId().toString(); + + long start = System.currentTimeMillis(); + long end = System.currentTimeMillis() + 5000; + + List actualKeys = null; + while (start <= end) { + actualKeys = doGetAsync("/api/plugins/telemetry/DEVICE/" + deviceId + "/keys/timeseries", List.class); + if (actualKeys.size() == expectedKeys.size()) { + break; + } + Thread.sleep(100); + start += 100; + } + assertNotNull(actualKeys); + + Set actualKeySet = new HashSet<>(actualKeys); + Set expectedKeySet = new HashSet<>(expectedKeys); + + assertEquals(expectedKeySet, actualKeySet); + + String getTelemetryValuesUrl; + if (withTs) { + getTelemetryValuesUrl = "/api/plugins/telemetry/DEVICE/" + deviceId + "/values/timeseries?startTs=0&endTs=15000&keys=" + String.join(",", actualKeySet); + } else { + getTelemetryValuesUrl = "/api/plugins/telemetry/DEVICE/" + deviceId + "/values/timeseries?keys=" + String.join(",", actualKeySet); + } + Map>> values = doGetAsync(getTelemetryValuesUrl, Map.class); + + if (withTs) { + assertTs(values, expectedKeys, 10000, 0); + } + assertValues(values, 0); + } + + protected void processGatewayTelemetryTest(String topic, List expectedKeys, byte[] payload, String firstDeviceName, String secondDeviceName) throws Exception { + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + + publishMqttMsg(client, payload, topic); + + Device firstDevice = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + firstDeviceName, Device.class), + 20, + 100); + + assertNotNull(firstDevice); + + Device secondDevice = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + secondDeviceName, Device.class), + 20, + 100); + + assertNotNull(secondDevice); + + Thread.sleep(2000); + + List firstDeviceActualKeys = doGetAsync("/api/plugins/telemetry/DEVICE/" + firstDevice.getId() + "/keys/timeseries", List.class); + Set firstDeviceActualKeySet = new HashSet<>(firstDeviceActualKeys); + + List secondDeviceActualKeys = doGetAsync("/api/plugins/telemetry/DEVICE/" + secondDevice.getId() + "/keys/timeseries", List.class); + Set secondDeviceActualKeySet = new HashSet<>(secondDeviceActualKeys); + + Set expectedKeySet = new HashSet<>(expectedKeys); + + assertEquals(expectedKeySet, firstDeviceActualKeySet); + assertEquals(expectedKeySet, secondDeviceActualKeySet); + + String getTelemetryValuesUrlFirstDevice = getTelemetryValuesUrl(firstDevice.getId(), firstDeviceActualKeySet); + String getTelemetryValuesUrlSecondDevice = getTelemetryValuesUrl(firstDevice.getId(), secondDeviceActualKeySet); + + Map>> firstDeviceValues = doGetAsync(getTelemetryValuesUrlFirstDevice, Map.class); + Map>> secondDeviceValues = doGetAsync(getTelemetryValuesUrlSecondDevice, Map.class); + + assertGatewayDeviceData(firstDeviceValues, expectedKeys); + assertGatewayDeviceData(secondDeviceValues, expectedKeys); + } + + protected String getGatewayTelemetryJsonPayload(String deviceA, String deviceB, String firstTsValue, String secondTsValue) { + String payload = "[{\"ts\": " + firstTsValue + ", \"values\": " + PAYLOAD_VALUES_STR + "}, " + + "{\"ts\": " + secondTsValue + ", \"values\": " + PAYLOAD_VALUES_STR + "}]"; + return "{\"" + deviceA + "\": " + payload + ", \"" + deviceB + "\": " + payload + "}"; + } + + private String getTelemetryValuesUrl(DeviceId deviceId, Set actualKeySet) { + return "/api/plugins/telemetry/DEVICE/" + deviceId + "/values/timeseries?startTs=0&endTs=25000&keys=" + String.join(",", actualKeySet); + } + + private void assertGatewayDeviceData(Map>> deviceValues, List expectedKeys) { + + assertEquals(2, deviceValues.get(expectedKeys.get(0)).size()); + assertEquals(2, deviceValues.get(expectedKeys.get(1)).size()); + assertEquals(2, deviceValues.get(expectedKeys.get(2)).size()); + assertEquals(2, deviceValues.get(expectedKeys.get(3)).size()); + assertEquals(2, deviceValues.get(expectedKeys.get(4)).size()); + + assertTs(deviceValues, expectedKeys, 20000, 0); + assertTs(deviceValues, expectedKeys, 10000, 1); + + assertValues(deviceValues, 0); + assertValues(deviceValues, 1); + + } + + private void assertValues(Map>> deviceValues, int arrayIndex) { + for (Map.Entry>> entry : deviceValues.entrySet()) { + String key = entry.getKey(); + List> tsKv = entry.getValue(); + String value = tsKv.get(arrayIndex).get("value"); + switch (key) { + case "key1": + assertEquals("value1", value); + break; + case "key2": + assertEquals("true", value); + break; + case "key3": + assertEquals("3.0", value); + break; + case "key4": + assertEquals("4", value); + break; + case "key5": + assertEquals("{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}", value); + break; + } + } + } + + private void assertTs(Map>> deviceValues, List expectedKeys, int ts, int arrayIndex) { + assertEquals(ts, deviceValues.get(expectedKeys.get(0)).get(arrayIndex).get("ts")); + assertEquals(ts, deviceValues.get(expectedKeys.get(1)).get(arrayIndex).get("ts")); + assertEquals(ts, deviceValues.get(expectedKeys.get(2)).get(arrayIndex).get("ts")); + assertEquals(ts, deviceValues.get(expectedKeys.get(3)).get(arrayIndex).get("ts")); + assertEquals(ts, deviceValues.get(expectedKeys.get(4)).get(arrayIndex).get("ts")); + } + + // @Test - Unstable + public void testMqttQoSLevel() throws Exception { + String clientId = MqttAsyncClient.generateClientId(); + MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId, new MemoryPersistence()); + + MqttConnectOptions options = new MqttConnectOptions(); + options.setUserName(accessToken); + CountDownLatch latch = new CountDownLatch(1); + TestMqttCallback callback = new TestMqttCallback(client, latch); + client.setCallback(callback); + client.connect(options).waitForCompletion(5000); + client.subscribe("v1/devices/me/attributes", MqttQoS.AT_MOST_ONCE.value()); + String payload = "{\"key\":\"uniqueValue\"}"; +// TODO 3.1: we need to acknowledge subscription only after it is processed by device actor and not when the message is pushed to queue. +// MqttClient -> SUB REQUEST -> Transport -> Kafka -> Device Actor (subscribed) +// MqttClient <- SUB_ACK <- Transport + Thread.sleep(5000); + doPostAsync("/api/plugins/telemetry/" + savedDevice.getId() + "/SHARED_SCOPE", payload, String.class, status().isOk()); + latch.await(10, TimeUnit.SECONDS); + assertEquals(payload, callback.getPayload()); + assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); + } + + private static class TestMqttCallback implements MqttCallback { + + private final MqttAsyncClient client; + private final CountDownLatch latch; + private volatile Integer qoS; + private volatile String payload; + + String getPayload() { + return payload; + } + + TestMqttCallback(MqttAsyncClient client, CountDownLatch latch) { + this.client = client; + this.latch = latch; + } + + int getQoS() { + return qoS; + } + + @Override + public void connectionLost(Throwable throwable) { + log.error("Client connection lost", throwable); + } + + @Override + public void messageArrived(String requestTopic, MqttMessage mqttMessage) { + payload = new String(mqttMessage.getPayload()); + qoS = mqttMessage.getQos(); + latch.countDown(); + } + + @Override + public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { + + } + } + + +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/AbstractMqttTimeseriesJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/AbstractMqttTimeseriesJsonIntegrationTest.java new file mode 100644 index 0000000000..edab8405cd --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/AbstractMqttTimeseriesJsonIntegrationTest.java @@ -0,0 +1,82 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.timeseries; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.profile.MqttTopics; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@Slf4j +public abstract class AbstractMqttTimeseriesJsonIntegrationTest extends AbstractMqttTimeseriesIntegrationTest { + + private static final String POST_DATA_TELEMETRY_TOPIC = "data/telemetry"; + + @Before + public void beforeTest() throws Exception { + processBeforeTest("Test Post Telemetry device json payload", "Test Post Telemetry gateway json payload", TransportPayloadType.JSON, POST_DATA_TELEMETRY_TOPIC, null); + } + + @After + public void afterTest() throws Exception { + processAfterTest(); + } + + @Test + public void testPushMqttTelemetry() throws Exception { + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + processTelemetryTest(POST_DATA_TELEMETRY_TOPIC, expectedKeys, PAYLOAD_VALUES_STR.getBytes(), false); + } + + @Test + public void testPushMqttTelemetryWithTs() throws Exception { + String payloadStr = "{\"ts\": 10000, \"values\": " + PAYLOAD_VALUES_STR + "}"; + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + processTelemetryTest(POST_DATA_TELEMETRY_TOPIC, expectedKeys, payloadStr.getBytes(), true); + } + + @Test + public void testPushMqttTelemetryGateway() throws Exception { + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + String deviceName1 = "Device A"; + String deviceName2 = "Device B"; + String payload = getGatewayTelemetryJsonPayload(deviceName1, deviceName2, "10000", "20000"); + processGatewayTelemetryTest(MqttTopics.GATEWAY_TELEMETRY_TOPIC, expectedKeys, payload.getBytes(), deviceName1, deviceName2); + } + + @Test + public void testGatewayConnect() throws Exception { + String payload = "{\"device\":\"Device A\", \"type\": \"" + TransportPayloadType.JSON.name() + "\"}"; + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + publishMqttMsg(client, payload.getBytes(), MqttTopics.GATEWAY_CONNECT_TOPIC); + + String deviceName = "Device A"; + Device device = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class), + 20, + 100); + assertNotNull(device); + } +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java new file mode 100644 index 0000000000..2257350d31 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java @@ -0,0 +1,116 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.timeseries; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.gen.transport.TransportApiProtos; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@Slf4j +public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends AbstractMqttTimeseriesIntegrationTest { + + private static final String POST_DATA_TELEMETRY_TOPIC = "proto/telemetry"; + + @Before + public void beforeTest() throws Exception { + processBeforeTest("Test Post Telemetry device proto payload", "Test Post Telemetry gateway proto payload", TransportPayloadType.PROTOBUF, POST_DATA_TELEMETRY_TOPIC, null); + } + + @After + public void afterTest() throws Exception { + processAfterTest(); + } + + @Test + public void testPushMqttTelemetry() throws Exception { + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + TransportProtos.TsKvListProto tsKvListProto = getTsKvListProto(expectedKeys, 0); + processTelemetryTest(POST_DATA_TELEMETRY_TOPIC, expectedKeys, tsKvListProto.toByteArray(), false); + } + + @Test + public void testPushMqttTelemetryWithTs() throws Exception { + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + TransportProtos.TsKvListProto tsKvListProto = getTsKvListProto(expectedKeys, 10000); + processTelemetryTest(POST_DATA_TELEMETRY_TOPIC, expectedKeys, tsKvListProto.toByteArray(), true); + } + + @Test + public void testPushMqttTelemetryGateway() throws Exception { + TransportApiProtos.GatewayTelemetryMsg.Builder gatewayTelemetryMsgProtoBuilder = TransportApiProtos.GatewayTelemetryMsg.newBuilder(); + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + String deviceName1 = "Device A"; + String deviceName2 = "Device B"; + TransportApiProtos.TelemetryMsg deviceATelemetryMsgProto = getDeviceTelemetryMsgProto(deviceName1, expectedKeys, 10000, 20000); + TransportApiProtos.TelemetryMsg deviceBTelemetryMsgProto = getDeviceTelemetryMsgProto(deviceName2, expectedKeys, 10000, 20000); + gatewayTelemetryMsgProtoBuilder.addAllMsg(Arrays.asList(deviceATelemetryMsgProto, deviceBTelemetryMsgProto)); + TransportApiProtos.GatewayTelemetryMsg gatewayTelemetryMsg = gatewayTelemetryMsgProtoBuilder.build(); + processGatewayTelemetryTest(MqttTopics.GATEWAY_TELEMETRY_TOPIC, expectedKeys, gatewayTelemetryMsg.toByteArray(), deviceName1, deviceName2); + } + + @Test + public void testGatewayConnect() throws Exception { + String deviceName = "Device A"; + TransportApiProtos.ConnectMsg connectMsgProto = getConnectProto(deviceName); + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + publishMqttMsg(client, connectMsgProto.toByteArray(), MqttTopics.GATEWAY_CONNECT_TOPIC); + + Device device = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class), + 20, + 100); + + assertNotNull(device); + } + + private TransportApiProtos.ConnectMsg getConnectProto(String deviceName) { + TransportApiProtos.ConnectMsg.Builder builder = TransportApiProtos.ConnectMsg.newBuilder(); + builder.setDeviceName(deviceName); + builder.setDeviceType(TransportPayloadType.PROTOBUF.name()); + return builder.build(); + } + + private TransportApiProtos.TelemetryMsg getDeviceTelemetryMsgProto(String deviceName, List expectedKeys, long firstTs, long secondTs) { + TransportApiProtos.TelemetryMsg.Builder deviceTelemetryMsgBuilder = TransportApiProtos.TelemetryMsg.newBuilder(); + TransportProtos.TsKvListProto tsKvListProto1 = getTsKvListProto(expectedKeys, firstTs); + TransportProtos.TsKvListProto tsKvListProto2 = getTsKvListProto(expectedKeys, secondTs); + TransportProtos.PostTelemetryMsg.Builder msg = TransportProtos.PostTelemetryMsg.newBuilder(); + msg.addAllTsKvList(Arrays.asList(tsKvListProto1, tsKvListProto2)); + deviceTelemetryMsgBuilder.setDeviceName(deviceName); + deviceTelemetryMsgBuilder.setMsg(msg); + return deviceTelemetryMsgBuilder.build(); + } + + private TransportProtos.TsKvListProto getTsKvListProto(List expectedKeys, long ts) { + List kvProtos = getKvProtos(expectedKeys); + TransportProtos.TsKvListProto.Builder builder = TransportProtos.TsKvListProto.newBuilder(); + builder.addAllKv(kvProtos); + builder.setTs(ts); + return builder.build(); + } +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/nosql/MqttTimeseriesNoSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/nosql/MqttTimeseriesNoSqlIntegrationTest.java new file mode 100644 index 0000000000..77acef04d9 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/nosql/MqttTimeseriesNoSqlIntegrationTest.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.timeseries.nosql; + +import org.thingsboard.server.dao.service.DaoNoSqlTest; +import org.thingsboard.server.mqtt.telemetry.timeseries.AbstractMqttTimeseriesIntegrationTest; + +/** + * Created by Valerii Sosliuk on 8/22/2017. + */ +@DaoNoSqlTest +public class MqttTimeseriesNoSqlIntegrationTest extends AbstractMqttTimeseriesIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/nosql/MqttTimeseriesNoSqlJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/nosql/MqttTimeseriesNoSqlJsonIntegrationTest.java new file mode 100644 index 0000000000..4ac82c2518 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/nosql/MqttTimeseriesNoSqlJsonIntegrationTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.timeseries.nosql; + +import org.thingsboard.server.dao.service.DaoNoSqlTest; +import org.thingsboard.server.mqtt.telemetry.timeseries.AbstractMqttTimeseriesJsonIntegrationTest; + +@DaoNoSqlTest +public class MqttTimeseriesNoSqlJsonIntegrationTest extends AbstractMqttTimeseriesJsonIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/nosql/MqttTimeseriesNoSqlProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/nosql/MqttTimeseriesNoSqlProtoIntegrationTest.java new file mode 100644 index 0000000000..c208574e5c --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/nosql/MqttTimeseriesNoSqlProtoIntegrationTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.timeseries.nosql; + +import org.thingsboard.server.dao.service.DaoNoSqlTest; +import org.thingsboard.server.mqtt.telemetry.timeseries.AbstractMqttTimeseriesProtoIntegrationTest; + +@DaoNoSqlTest +public class MqttTimeseriesNoSqlProtoIntegrationTest extends AbstractMqttTimeseriesProtoIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/sql/MqttTimeseriesSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/sql/MqttTimeseriesSqlIntegrationTest.java new file mode 100644 index 0000000000..cfb39a29db --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/sql/MqttTimeseriesSqlIntegrationTest.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.timeseries.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.telemetry.timeseries.AbstractMqttTimeseriesIntegrationTest; + +/** + * Created by Valerii Sosliuk on 8/22/2017. + */ +@DaoSqlTest +public class MqttTimeseriesSqlIntegrationTest extends AbstractMqttTimeseriesIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/sql/MqttTimeseriesSqlJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/sql/MqttTimeseriesSqlJsonIntegrationTest.java new file mode 100644 index 0000000000..f313cc5d29 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/sql/MqttTimeseriesSqlJsonIntegrationTest.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.timeseries.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.telemetry.timeseries.AbstractMqttTimeseriesIntegrationTest; +import org.thingsboard.server.mqtt.telemetry.timeseries.AbstractMqttTimeseriesJsonIntegrationTest; + +/** + * Created by Valerii Sosliuk on 8/22/2017. + */ +@DaoSqlTest +public class MqttTimeseriesSqlJsonIntegrationTest extends AbstractMqttTimeseriesJsonIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/sql/MqttTimeseriesSqlProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/sql/MqttTimeseriesSqlProtoIntegrationTest.java new file mode 100644 index 0000000000..3c7d5e2e2a --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/sql/MqttTimeseriesSqlProtoIntegrationTest.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.mqtt.telemetry.timeseries.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.telemetry.timeseries.AbstractMqttTimeseriesJsonIntegrationTest; +import org.thingsboard.server.mqtt.telemetry.timeseries.AbstractMqttTimeseriesProtoIntegrationTest; + +/** + * Created by Valerii Sosliuk on 8/22/2017. + */ +@DaoSqlTest +public class MqttTimeseriesSqlProtoIntegrationTest extends AbstractMqttTimeseriesProtoIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/rules/RuleEngineSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/rules/RuleEngineSqlTestSuite.java index 72484e3f21..384573b190 100644 --- a/application/src/test/java/org/thingsboard/server/rules/RuleEngineSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/rules/RuleEngineSqlTestSuite.java @@ -32,7 +32,7 @@ public class RuleEngineSqlTestSuite { @ClassRule public static CustomSqlUnit sqlUnit = new CustomSqlUnit( - Arrays.asList("sql/schema-ts-hsql.sql", "sql/schema-entities-hsql.sql", "sql/system-data.sql"), + Arrays.asList("sql/schema-types-hsql.sql", "sql/schema-ts-hsql.sql", "sql/schema-entities-hsql.sql", "sql/system-data.sql"), "sql/hsql/drop-all-tables.sql", "sql-test.properties"); diff --git a/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java index 3b2c66a9e6..86e3f0e635 100644 --- a/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java @@ -27,7 +27,7 @@ import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.common.data.*; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; -import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; @@ -150,7 +150,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule actorSystem.tell(qMsg); Mockito.verify(tbMsgCallback, Mockito.timeout(10000)).onSuccess(); - TimePageData eventsPage = getDebugEvents(savedTenant.getId(), ruleChain.getFirstRuleNodeId(), 1000); + PageData eventsPage = getDebugEvents(savedTenant.getId(), ruleChain.getFirstRuleNodeId(), 1000); List events = eventsPage.getData().stream().filter(filterByCustomEvent()).collect(Collectors.toList()); Assert.assertEquals(2, events.size()); @@ -263,7 +263,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule Mockito.verify(tbMsgCallback, Mockito.timeout(10000)).onSuccess(); - TimePageData eventsPage = getDebugEvents(savedTenant.getId(), rootRuleChain.getFirstRuleNodeId(), 1000); + PageData eventsPage = getDebugEvents(savedTenant.getId(), rootRuleChain.getFirstRuleNodeId(), 1000); List events = eventsPage.getData().stream().filter(filterByCustomEvent()).collect(Collectors.toList()); Assert.assertEquals(2, events.size()); diff --git a/application/src/test/java/org/thingsboard/server/rules/flow/nosql/RuleEngineFlowNoSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/flow/nosql/RuleEngineFlowNoSqlIntegrationTest.java deleted file mode 100644 index b3387a8389..0000000000 --- a/application/src/test/java/org/thingsboard/server/rules/flow/nosql/RuleEngineFlowNoSqlIntegrationTest.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.rules.flow.nosql; - -import org.thingsboard.server.dao.service.DaoNoSqlTest; -import org.thingsboard.server.rules.flow.AbstractRuleEngineFlowIntegrationTest; - -/** - * Created by Valerii Sosliuk on 8/22/2017. - */ -@DaoNoSqlTest -public class RuleEngineFlowNoSqlIntegrationTest extends AbstractRuleEngineFlowIntegrationTest { -} diff --git a/application/src/test/java/org/thingsboard/server/rules/flow/sql/RuleEngineFlowSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/flow/sql/RuleEngineFlowSqlIntegrationTest.java index ee97cd3c2f..11377c4ba0 100644 --- a/application/src/test/java/org/thingsboard/server/rules/flow/sql/RuleEngineFlowSqlIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/rules/flow/sql/RuleEngineFlowSqlIntegrationTest.java @@ -16,7 +16,6 @@ package org.thingsboard.server.rules.flow.sql; import org.thingsboard.server.dao.service.DaoSqlTest; -import org.thingsboard.server.mqtt.rpc.AbstractMqttServerSideRpcIntegrationTest; import org.thingsboard.server.rules.flow.AbstractRuleEngineFlowIntegrationTest; /** diff --git a/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java index 50d7caa7bd..4ff15fd1d6 100644 --- a/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java @@ -31,7 +31,7 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; -import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; @@ -142,7 +142,7 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac Mockito.verify(tbMsgCallback, Mockito.timeout(3000)).onSuccess(); - TimePageData eventsPage = getDebugEvents(savedTenant.getId(), ruleChain.getFirstRuleNodeId(), 1000); + PageData eventsPage = getDebugEvents(savedTenant.getId(), ruleChain.getFirstRuleNodeId(), 1000); List events = eventsPage.getData().stream().filter(filterByCustomEvent()).collect(Collectors.toList()); Assert.assertEquals(2, events.size()); diff --git a/application/src/test/java/org/thingsboard/server/rules/lifecycle/nosql/RuleEngineLifecycleNoSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/lifecycle/nosql/RuleEngineLifecycleNoSqlIntegrationTest.java deleted file mode 100644 index 0d8ef84f12..0000000000 --- a/application/src/test/java/org/thingsboard/server/rules/lifecycle/nosql/RuleEngineLifecycleNoSqlIntegrationTest.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.rules.lifecycle.nosql; - -import org.thingsboard.server.dao.service.DaoNoSqlTest; -import org.thingsboard.server.rules.lifecycle.AbstractRuleEngineLifecycleIntegrationTest; - -/** - * Created by Valerii Sosliuk on 8/22/2017. - */ -@DaoNoSqlTest -public class RuleEngineLifecycleNoSqlIntegrationTest extends AbstractRuleEngineLifecycleIntegrationTest { -} diff --git a/application/src/test/java/org/thingsboard/server/service/cluster/routing/HashPartitionServiceTest.java b/application/src/test/java/org/thingsboard/server/service/cluster/routing/HashPartitionServiceTest.java index dcf98c1443..977c41b23c 100644 --- a/application/src/test/java/org/thingsboard/server/service/cluster/routing/HashPartitionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cluster/routing/HashPartitionServiceTest.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.service.cluster.routing; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import lombok.extern.slf4j.Slf4j; import org.junit.Assert; import org.junit.Before; @@ -101,7 +101,7 @@ public class HashPartitionServiceTest { public void testDispersionOnMillionDevices() { List devices = new ArrayList<>(); for (int i = 0; i < ITERATIONS; i++) { - devices.add(new DeviceId(UUIDs.timeBased())); + devices.add(new DeviceId(Uuids.timeBased())); } testDevicesDispersion(devices); } diff --git a/application/src/test/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngineTest.java b/application/src/test/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngineTest.java deleted file mode 100644 index 62881bd5af..0000000000 --- a/application/src/test/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngineTest.java +++ /dev/null @@ -1,250 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.script; - -import com.datastax.driver.core.utils.UUIDs; -import com.google.common.collect.Sets; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.thingsboard.rule.engine.api.ScriptEngine; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.RuleNodeId; -import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.msg.TbMsgDataType; -import org.thingsboard.server.common.msg.TbMsgMetaData; - -import javax.script.ScriptException; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicInteger; - -import static org.junit.Assert.*; - -public class RuleNodeJsScriptEngineTest { - - private ScriptEngine scriptEngine; - private TestNashornJsInvokeService jsSandboxService; - - private EntityId ruleNodeId = new RuleNodeId(UUIDs.timeBased()); - - @Before - public void beforeTest() throws Exception { - jsSandboxService = new TestNashornJsInvokeService(false, 1, 100, 3); - } - - @After - public void afterTest() throws Exception { - jsSandboxService.stop(); - } - - @Test - public void msgCanBeUpdated() throws ScriptException { - String function = "metadata.temp = metadata.temp * 10; return {metadata: metadata};"; - scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, function); - - TbMsgMetaData metaData = new TbMsgMetaData(); - metaData.putValue("temp", "7"); - metaData.putValue("humidity", "99"); - String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}"; - - TbMsg msg = TbMsg.newMsg( "USER", null, metaData, TbMsgDataType.JSON, rawJson); - - TbMsg actual = scriptEngine.executeUpdate(msg); - assertEquals("70", actual.getMetaData().getValue("temp")); - scriptEngine.destroy(); - } - - @Test - public void newAttributesCanBeAddedInMsg() throws ScriptException { - String function = "metadata.newAttr = metadata.humidity - msg.passed; return {metadata: metadata};"; - scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, function); - TbMsgMetaData metaData = new TbMsgMetaData(); - metaData.putValue("temp", "7"); - metaData.putValue("humidity", "99"); - String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}"; - - TbMsg msg = TbMsg.newMsg( "USER", null, metaData, TbMsgDataType.JSON, rawJson); - - TbMsg actual = scriptEngine.executeUpdate(msg); - assertEquals("94", actual.getMetaData().getValue("newAttr")); - scriptEngine.destroy(); - } - - @Test - public void payloadCanBeUpdated() throws ScriptException { - String function = "msg.passed = msg.passed * metadata.temp; msg.bigObj.newProp = 'Ukraine'; return {msg: msg};"; - scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, function); - TbMsgMetaData metaData = new TbMsgMetaData(); - metaData.putValue("temp", "7"); - metaData.putValue("humidity", "99"); - String rawJson = "{\"name\":\"Vit\",\"passed\": 5,\"bigObj\":{\"prop\":42}}"; - - TbMsg msg =TbMsg.newMsg("USER", null, metaData, TbMsgDataType.JSON, rawJson); - - TbMsg actual = scriptEngine.executeUpdate(msg); - - String expectedJson = "{\"name\":\"Vit\",\"passed\":35,\"bigObj\":{\"prop\":42,\"newProp\":\"Ukraine\"}}"; - assertEquals(expectedJson, actual.getData()); - scriptEngine.destroy(); - } - - @Test - public void metadataAccessibleForFilter() throws ScriptException { - String function = "return metadata.humidity < 15;"; - scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, function); - TbMsgMetaData metaData = new TbMsgMetaData(); - metaData.putValue("temp", "7"); - metaData.putValue("humidity", "99"); - String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}"; - - TbMsg msg = TbMsg.newMsg("USER", null, metaData, TbMsgDataType.JSON, rawJson); - assertFalse(scriptEngine.executeFilter(msg)); - scriptEngine.destroy(); - } - - @Test - public void dataAccessibleForFilter() throws ScriptException { - String function = "return msg.passed < 15 && msg.name === 'Vit' && metadata.temp == 7 && msg.bigObj.prop == 42;"; - scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, function); - TbMsgMetaData metaData = new TbMsgMetaData(); - metaData.putValue("temp", "7"); - metaData.putValue("humidity", "99"); - String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}"; - - TbMsg msg = TbMsg.newMsg( "USER", null, metaData,TbMsgDataType.JSON, rawJson); - assertTrue(scriptEngine.executeFilter(msg)); - scriptEngine.destroy(); - } - - @Test - public void dataAccessibleForSwitch() throws ScriptException { - String jsCode = "function nextRelation(metadata, msg) {\n" + - " if(msg.passed == 5 && metadata.temp == 10)\n" + - " return 'one'\n" + - " else\n" + - " return 'two';\n" + - "};\n" + - "\n" + - "return nextRelation(metadata, msg);"; - scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, jsCode); - TbMsgMetaData metaData = new TbMsgMetaData(); - metaData.putValue("temp", "10"); - metaData.putValue("humidity", "99"); - String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}"; - - TbMsg msg = TbMsg.newMsg( "USER", null, metaData, TbMsgDataType.JSON, rawJson); - Set actual = scriptEngine.executeSwitch(msg); - assertEquals(Sets.newHashSet("one"), actual); - scriptEngine.destroy(); - } - - @Test - public void multipleRelationsReturnedFromSwitch() throws ScriptException { - String jsCode = "function nextRelation(metadata, msg) {\n" + - " if(msg.passed == 5 && metadata.temp == 10)\n" + - " return ['three', 'one']\n" + - " else\n" + - " return 'two';\n" + - "};\n" + - "\n" + - "return nextRelation(metadata, msg);"; - scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, ruleNodeId, jsCode); - TbMsgMetaData metaData = new TbMsgMetaData(); - metaData.putValue("temp", "10"); - metaData.putValue("humidity", "99"); - String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}"; - - TbMsg msg = TbMsg.newMsg( "USER", null, metaData, TbMsgDataType.JSON, rawJson); - Set actual = scriptEngine.executeSwitch(msg); - assertEquals(Sets.newHashSet("one", "three"), actual); - scriptEngine.destroy(); - } - - @Test - public void concurrentReleasedCorrectly() throws InterruptedException, ExecutionException { - String code = "metadata.temp = metadata.temp * 10; return {metadata: metadata};"; - - int repeat = 1000; - ExecutorService service = Executors.newFixedThreadPool(repeat); - Map scriptIds = new ConcurrentHashMap<>(); - CountDownLatch startLatch = new CountDownLatch(repeat); - CountDownLatch finishLatch = new CountDownLatch(repeat); - AtomicInteger failedCount = new AtomicInteger(0); - - for (int i = 0; i < repeat; i++) { - service.submit(() -> runScript(startLatch, finishLatch, failedCount, scriptIds, code)); - } - - finishLatch.await(); - assertTrue(scriptIds.size() == 1); - assertTrue(failedCount.get() == 0); - - CountDownLatch nextStart = new CountDownLatch(repeat); - CountDownLatch nextFinish = new CountDownLatch(repeat); - for (int i = 0; i < repeat; i++) { - service.submit(() -> runScript(nextStart, nextFinish, failedCount, scriptIds, code)); - } - - nextFinish.await(); - assertTrue(scriptIds.size() == 1); - assertTrue(failedCount.get() == 0); - service.shutdownNow(); - } - - @Test - public void concurrentFailedEvaluationShouldThrowException() throws InterruptedException { - String code = "metadata.temp = metadata.temp * 10; urn {metadata: metadata};"; - - int repeat = 10000; - ExecutorService service = Executors.newFixedThreadPool(repeat); - Map scriptIds = new ConcurrentHashMap<>(); - CountDownLatch startLatch = new CountDownLatch(repeat); - CountDownLatch finishLatch = new CountDownLatch(repeat); - AtomicInteger failedCount = new AtomicInteger(0); - for (int i = 0; i < repeat; i++) { - service.submit(() -> { - service.submit(() -> runScript(startLatch, finishLatch, failedCount, scriptIds, code)); - }); - } - - finishLatch.await(); - assertTrue(scriptIds.isEmpty()); - assertEquals(repeat, failedCount.get()); - service.shutdownNow(); - } - - private void runScript(CountDownLatch startLatch, CountDownLatch finishLatch, AtomicInteger failedCount, - Map scriptIds, String code) { - try { - for (int k = 0; k < 10; k++) { - startLatch.countDown(); - startLatch.await(); - UUID scriptId = jsSandboxService.eval(JsScriptType.RULE_NODE_SCRIPT, code).get(); - scriptIds.put(scriptId, new Object()); - jsSandboxService.invokeFunction(scriptId, "{}", "{}", "TEXT").get(); - jsSandboxService.release(scriptId).get(); - } - } catch (Throwable th) { - failedCount.incrementAndGet(); - } finally { - finishLatch.countDown(); - } - } - -} \ No newline at end of file diff --git a/application/src/test/java/org/thingsboard/server/service/script/TestNashornJsInvokeService.java b/application/src/test/java/org/thingsboard/server/service/script/TestNashornJsInvokeService.java deleted file mode 100644 index 8a89de5dff..0000000000 --- a/application/src/test/java/org/thingsboard/server/service/script/TestNashornJsInvokeService.java +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.script; - -public class TestNashornJsInvokeService extends AbstractNashornJsInvokeService { - - private boolean useJsSandbox; - private final int monitorThreadPoolSize; - private final long maxCpuTime; - private final int maxErrors; - - public TestNashornJsInvokeService(boolean useJsSandbox, int monitorThreadPoolSize, long maxCpuTime, int maxErrors) { - this.useJsSandbox = useJsSandbox; - this.monitorThreadPoolSize = monitorThreadPoolSize; - this.maxCpuTime = maxCpuTime; - this.maxErrors = maxErrors; - init(); - } - - @Override - protected boolean useJsSandbox() { - return useJsSandbox; - } - - @Override - protected int getMonitorThreadPoolSize() { - return monitorThreadPoolSize; - } - - @Override - protected long getMaxCpuTime() { - return maxCpuTime; - } - - @Override - protected int getMaxErrors() { - return maxErrors; - } - - @Override - protected long getMaxBlacklistDuration() { - return 100000; - } -} diff --git a/application/src/test/java/org/thingsboard/server/system/SystemNoSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/system/SystemNoSqlTestSuite.java deleted file mode 100644 index c4182db3ee..0000000000 --- a/application/src/test/java/org/thingsboard/server/system/SystemNoSqlTestSuite.java +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.system; - -import org.cassandraunit.dataset.cql.ClassPathCQLDataSet; -import org.junit.BeforeClass; -import org.junit.ClassRule; -import org.junit.extensions.cpsuite.ClasspathSuite; -import org.junit.runner.RunWith; -import org.thingsboard.server.dao.CustomCassandraCQLUnit; -import org.thingsboard.server.queue.memory.InMemoryStorage; - -import java.util.Arrays; - -/** - * @author Andrew Shvayka - */ -@RunWith(ClasspathSuite.class) -@ClasspathSuite.ClassnameFilters({"org.thingsboard.server.system.*NoSqlTest"}) -public class SystemNoSqlTestSuite { - - @ClassRule - public static CustomCassandraCQLUnit cassandraUnit = - new CustomCassandraCQLUnit( - Arrays.asList( - new ClassPathCQLDataSet("cassandra/schema-ts.cql", false, false), - new ClassPathCQLDataSet("cassandra/schema-entities.cql", false, false), - new ClassPathCQLDataSet("cassandra/system-data.cql", false, false)), - "cassandra-test.yaml", 30000l); - - @BeforeClass - public static void cleanupInMemStorage(){ - InMemoryStorage.getInstance().cleanup(); - } -} diff --git a/application/src/test/java/org/thingsboard/server/system/SystemSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/system/SystemSqlTestSuite.java index 36d24e9b00..6aa451747f 100644 --- a/application/src/test/java/org/thingsboard/server/system/SystemSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/system/SystemSqlTestSuite.java @@ -33,7 +33,7 @@ public class SystemSqlTestSuite { @ClassRule public static CustomSqlUnit sqlUnit = new CustomSqlUnit( - Arrays.asList("sql/schema-ts-hsql.sql", "sql/schema-entities-hsql.sql", "sql/system-data.sql"), + Arrays.asList("sql/schema-types-hsql.sql", "sql/schema-ts-hsql.sql", "sql/schema-entities-hsql.sql", "sql/system-data.sql"), "sql/hsql/drop-all-tables.sql", "sql-test.properties"); diff --git a/application/src/test/java/org/thingsboard/server/system/nosql/DeviceApiNoSqlTest.java b/application/src/test/java/org/thingsboard/server/system/nosql/DeviceApiNoSqlTest.java deleted file mode 100644 index 4b1f0949f9..0000000000 --- a/application/src/test/java/org/thingsboard/server/system/nosql/DeviceApiNoSqlTest.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.system.nosql; - -import org.thingsboard.server.dao.service.DaoNoSqlTest; -import org.thingsboard.server.dao.util.NoSqlDao; -import org.thingsboard.server.system.BaseHttpDeviceApiTest; - -/** - * Created by Valerii Sosliuk on 6/27/2017. - */ -@DaoNoSqlTest -public class DeviceApiNoSqlTest extends BaseHttpDeviceApiTest { -} diff --git a/application/src/test/resources/logback.xml b/application/src/test/resources/logback.xml index 4f083906e5..f991a40078 100644 --- a/application/src/test/resources/logback.xml +++ b/application/src/test/resources/logback.xml @@ -7,6 +7,8 @@ + + diff --git a/common/actor/pom.xml b/common/actor/pom.xml index ee626dac6f..9258ee75a4 100644 --- a/common/actor/pom.xml +++ b/common/actor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT common org.thingsboard.common diff --git a/common/dao-api/pom.xml b/common/dao-api/pom.xml index 879e479c3a..650b05c04e 100644 --- a/common/dao-api/pom.xml +++ b/common/dao-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT common org.thingsboard.common @@ -78,18 +78,13 @@ provided - com.datastax.cassandra - cassandra-driver-core + com.datastax.oss + java-driver-core provided - com.datastax.cassandra - cassandra-driver-mapping - provided - - - com.datastax.cassandra - cassandra-driver-extras + io.dropwizard.metrics + metrics-jmx provided @@ -111,6 +106,25 @@ + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + false + + diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmOperationResult.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmOperationResult.java new file mode 100644 index 0000000000..291d358829 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmOperationResult.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.alarm; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.id.EntityId; + +import java.util.Collections; +import java.util.List; + +@Data +public class AlarmOperationResult { + private final Alarm alarm; + private final boolean successful; + private final List propagatedEntitiesList; + + public AlarmOperationResult(Alarm alarm, boolean successful) { + this.alarm = alarm; + this.successful = successful; + this.propagatedEntitiesList = Collections.emptyList(); + } + + public AlarmOperationResult(Alarm alarm, boolean successful, List propagatedEntitiesList) { + this.alarm = alarm; + this.successful = successful; + this.propagatedEntitiesList = propagatedEntitiesList; + } +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java index 3132aa99c7..0d5f4e6d37 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java @@ -24,32 +24,40 @@ import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AlarmData; +import org.thingsboard.server.common.data.query.AlarmDataPageLink; +import org.thingsboard.server.common.data.query.AlarmDataQuery; + +import java.util.Collection; /** * Created by ashvayka on 11.05.17. */ public interface AlarmService { - Alarm createOrUpdateAlarm(Alarm alarm); + AlarmOperationResult createOrUpdateAlarm(Alarm alarm); - Boolean deleteAlarm(TenantId tenantId, AlarmId alarmId); + AlarmOperationResult deleteAlarm(TenantId tenantId, AlarmId alarmId); - ListenableFuture ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTs); + ListenableFuture ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTs); - ListenableFuture clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs); + ListenableFuture clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs); ListenableFuture findAlarmByIdAsync(TenantId tenantId, AlarmId alarmId); ListenableFuture findAlarmInfoByIdAsync(TenantId tenantId, AlarmId alarmId); - ListenableFuture> findAlarms(TenantId tenantId, AlarmQuery query); + ListenableFuture> findAlarms(TenantId tenantId, AlarmQuery query); AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus); ListenableFuture findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type); + PageData findAlarmDataByQueryForEntities(TenantId tenantId, CustomerId customerId, + AlarmDataQuery query, Collection orderedEntityIds); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java index 953ef5d9d4..6455e6c368 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java @@ -18,18 +18,21 @@ package org.thingsboard.server.dao.asset; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetSearchQuery; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import java.util.List; import java.util.Optional; public interface AssetService { + AssetInfo findAssetInfoById(TenantId tenantId, AssetId assetId); + Asset findAssetById(TenantId tenantId, AssetId assetId); ListenableFuture findAssetByIdAsync(TenantId tenantId, AssetId assetId); @@ -44,17 +47,25 @@ public interface AssetService { void deleteAsset(TenantId tenantId, AssetId assetId); - TextPageData findAssetsByTenantId(TenantId tenantId, TextPageLink pageLink); + PageData findAssetsByTenantId(TenantId tenantId, PageLink pageLink); + + PageData findAssetInfosByTenantId(TenantId tenantId, PageLink pageLink); + + PageData findAssetsByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink); - TextPageData findAssetsByTenantIdAndType(TenantId tenantId, String type, TextPageLink pageLink); + PageData findAssetInfosByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink); ListenableFuture> findAssetsByTenantIdAndIdsAsync(TenantId tenantId, List assetIds); void deleteAssetsByTenantId(TenantId tenantId); - TextPageData findAssetsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TextPageLink pageLink); + PageData findAssetsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink); + + PageData findAssetInfosByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink); + + PageData findAssetsByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink); - TextPageData findAssetsByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, TextPageLink pageLink); + PageData findAssetInfosByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink); ListenableFuture> findAssetsByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List assetIds); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java index ef6e62e931..c832d8ea06 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java @@ -23,20 +23,20 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import java.util.List; public interface AuditLogService { - TimePageData findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, List actionTypes, TimePageLink pageLink); + PageData findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, List actionTypes, TimePageLink pageLink); - TimePageData findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, List actionTypes, TimePageLink pageLink); + PageData findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, List actionTypes, TimePageLink pageLink); - TimePageData findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, List actionTypes, TimePageLink pageLink); + PageData findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, List actionTypes, TimePageLink pageLink); - TimePageData findAuditLogsByTenantId(TenantId tenantId, List actionTypes, TimePageLink pageLink); + PageData findAuditLogsByTenantId(TenantId tenantId, List actionTypes, TimePageLink pageLink); ListenableFuture> logEntityAction( TenantId tenantId, diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java index 90bd5a292f..e2082947c0 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java @@ -16,117 +16,52 @@ package org.thingsboard.server.dao.cassandra; -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.ConsistencyLevel; -import com.datastax.driver.core.HostDistance; -import com.datastax.driver.core.PoolingOptions; -import com.datastax.driver.core.ProtocolOptions.Compression; -import com.datastax.driver.core.Session; -import com.datastax.driver.mapping.DefaultPropertyMapper; -import com.datastax.driver.mapping.Mapper; -import com.datastax.driver.mapping.MappingConfiguration; -import com.datastax.driver.mapping.MappingManager; -import com.datastax.driver.mapping.PropertyAccessStrategy; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.jmx.JmxReporter; +import com.datastax.oss.driver.api.core.ConsistencyLevel; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.env.Environment; +import org.thingsboard.server.dao.cassandra.guava.GuavaSession; +import org.thingsboard.server.dao.cassandra.guava.GuavaSessionBuilder; +import org.thingsboard.server.dao.cassandra.guava.GuavaSessionUtils; import javax.annotation.PreDestroy; -import java.net.InetSocketAddress; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; @Slf4j public abstract class AbstractCassandraCluster { - private static final String COMMA = ","; - private static final String COLON = ":"; - - @Value("${cassandra.cluster_name}") - private String clusterName; - @Value("${cassandra.url}") - private String url; - @Value("${cassandra.compression}") - private String compression; - @Value("${cassandra.ssl}") - private Boolean ssl; @Value("${cassandra.jmx}") private Boolean jmx; @Value("${cassandra.metrics}") private Boolean metrics; - @Value("${cassandra.credentials}") - private Boolean credentials; - @Value("${cassandra.username}") - private String username; - @Value("${cassandra.password}") - private String password; - @Value("${cassandra.init_timeout_ms}") - private long initTimeout; - @Value("${cassandra.init_retry_interval_ms}") - private long initRetryInterval; - @Value("${cassandra.max_requests_per_connection_local:32768}") - private int max_requests_local; - @Value("${cassandra.max_requests_per_connection_remote:32768}") - private int max_requests_remote; + @Value("${cassandra.local_datacenter:datacenter1}") + private String localDatacenter; @Autowired - private CassandraSocketOptions socketOpts; - - @Autowired - private CassandraQueryOptions queryOpts; + private CassandraDriverOptions driverOptions; @Autowired private Environment environment; - private Cluster cluster; - private Cluster.Builder clusterBuilder; + private GuavaSessionBuilder sessionBuilder; - private Session session; + private GuavaSession session; - private MappingManager mappingManager; - - public Mapper getMapper(Class clazz) { - return mappingManager.mapper(clazz); - } + private JmxReporter reporter; private String keyspaceName; protected void init(String keyspaceName) { this.keyspaceName = keyspaceName; - this.clusterBuilder = Cluster.builder() - .addContactPointsWithPorts(getContactPoints(url)) - .withClusterName(clusterName) - .withSocketOptions(socketOpts.getOpts()) - .withPoolingOptions(new PoolingOptions() - .setMaxRequestsPerConnection(HostDistance.LOCAL, max_requests_local) - .setMaxRequestsPerConnection(HostDistance.REMOTE, max_requests_remote)); - this.clusterBuilder.withQueryOptions(queryOpts.getOpts()); - this.clusterBuilder.withCompression(StringUtils.isEmpty(compression) ? Compression.NONE : Compression.valueOf(compression.toUpperCase())); - if (ssl) { - this.clusterBuilder.withSSL(); - } - if (!jmx) { - this.clusterBuilder.withoutJMXReporting(); - } - if (!metrics) { - this.clusterBuilder.withoutMetrics(); - } - if (credentials) { - this.clusterBuilder.withCredentials(username, password); - } + this.sessionBuilder = GuavaSessionUtils.builder().withConfigLoader(this.driverOptions.getLoader()); if (!isInstall()) { initSession(); } } - public Cluster getCluster() { - return cluster; - } - - public Session getSession() { + public GuavaSession getSession() { if (!isInstall()) { return session; } else { @@ -146,64 +81,40 @@ public abstract class AbstractCassandraCluster { } private void initSession() { - long endTime = System.currentTimeMillis() + initTimeout; - while (System.currentTimeMillis() < endTime) { - try { - cluster = clusterBuilder.build(); - cluster.init(); - if (this.keyspaceName != null) { - session = cluster.connect(keyspaceName); - } else { - session = cluster.connect(); - } -// For Cassandra Driver version 3.5.0 - DefaultPropertyMapper propertyMapper = new DefaultPropertyMapper(); - propertyMapper.setPropertyAccessStrategy(PropertyAccessStrategy.FIELDS); - MappingConfiguration configuration = MappingConfiguration.builder().withPropertyMapper(propertyMapper).build(); - mappingManager = new MappingManager(session, configuration); -// For Cassandra Driver version 3.0.0 -// mappingManager = new MappingManager(session); - break; - } catch (Exception e) { - log.warn("Failed to initialize cassandra cluster due to {}. Will retry in {} ms", e.getMessage(), initRetryInterval); - try { - Thread.sleep(initRetryInterval); - } catch (InterruptedException ie) { - log.warn("Failed to wait until retry", ie); - Thread.currentThread().interrupt(); - } - } + if (this.keyspaceName != null) { + this.sessionBuilder.withKeyspace(this.keyspaceName); + } + this.sessionBuilder.withLocalDatacenter(localDatacenter); + session = sessionBuilder.build(); + if (this.metrics && this.jmx) { + MetricRegistry registry = + session.getMetrics().orElseThrow( + () -> new IllegalStateException("Metrics are disabled")) + .getRegistry(); + this.reporter = + JmxReporter.forRegistry(registry) + .inDomain("com.datastax.oss.driver") + .build(); + this.reporter.start(); } } @PreDestroy public void close() { - if (cluster != null) { - cluster.close(); + if (reporter != null) { + reporter.stop(); } - } - - private List getContactPoints(String url) { - List result; - if (StringUtils.isBlank(url)) { - result = Collections.emptyList(); - } else { - result = new ArrayList<>(); - for (String hostPort : url.split(COMMA)) { - String host = hostPort.split(COLON)[0]; - Integer port = Integer.valueOf(hostPort.split(COLON)[1]); - result.add(new InetSocketAddress(host, port)); - } + if (session != null) { + session.close(); } - return result; } public ConsistencyLevel getDefaultReadConsistencyLevel() { - return queryOpts.getDefaultReadConsistencyLevel(); + return driverOptions.getDefaultReadConsistencyLevel(); } public ConsistencyLevel getDefaultWriteConsistencyLevel() { - return queryOpts.getDefaultWriteConsistencyLevel(); + return driverOptions.getDefaultWriteConsistencyLevel(); } } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/CassandraDriverOptions.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/CassandraDriverOptions.java new file mode 100644 index 0000000000..6db7f84dc3 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/CassandraDriverOptions.java @@ -0,0 +1,258 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.cassandra; + +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.DefaultConsistencyLevel; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfigLoader; +import com.datastax.oss.driver.api.core.config.ProgrammaticDriverConfigLoaderBuilder; +import com.datastax.oss.driver.api.core.metrics.DefaultSessionMetric; +import com.datastax.oss.driver.api.core.metrics.DefaultNodeMetric; +import lombok.Data; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; +import org.thingsboard.server.dao.util.NoSqlAnyDao; + +import javax.annotation.PostConstruct; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +@Component +@Configuration +@Data +@NoSqlAnyDao +public class CassandraDriverOptions { + + private static final String COMMA = ","; + + @Value("${cassandra.cluster_name}") + private String clusterName; + @Value("${cassandra.url}") + private String url; + + @Value("${cassandra.socket.connect_timeout}") + private int connectTimeoutMillis; + @Value("${cassandra.socket.read_timeout}") + private int readTimeoutMillis; + @Value("${cassandra.socket.keep_alive}") + private Boolean keepAlive; + @Value("${cassandra.socket.reuse_address}") + private Boolean reuseAddress; + @Value("${cassandra.socket.so_linger}") + private Integer soLinger; + @Value("${cassandra.socket.tcp_no_delay}") + private Boolean tcpNoDelay; + @Value("${cassandra.socket.receive_buffer_size}") + private Integer receiveBufferSize; + @Value("${cassandra.socket.send_buffer_size}") + private Integer sendBufferSize; + + @Value("${cassandra.max_requests_per_connection_local:32768}") + private int max_requests_local; + @Value("${cassandra.max_requests_per_connection_remote:32768}") + private int max_requests_remote; + + @Value("${cassandra.query.default_fetch_size}") + private Integer defaultFetchSize; + @Value("${cassandra.query.read_consistency_level}") + private String readConsistencyLevel; + @Value("${cassandra.query.write_consistency_level}") + private String writeConsistencyLevel; + + @Value("${cassandra.compression}") + private String compression; + + @Value("${cassandra.ssl.enabled}") + private Boolean ssl; + @Value("${cassandra.ssl.key_store}") + private String sslKeyStore; + @Value("${cassandra.ssl.key_store_password}") + private String sslKeyStorePassword; + @Value("${cassandra.ssl.trust_store}") + private String sslTrustStore; + @Value("${cassandra.ssl.trust_store_password}") + private String sslTrustStorePassword; + @Value("${cassandra.ssl.hostname_validation}") + private Boolean sslHostnameValidation; + @Value("${cassandra.ssl.cipher_suites}") + private List sslCipherSuites; + + @Value("${cassandra.metrics}") + private Boolean metrics; + + @Value("${cassandra.credentials}") + private Boolean credentials; + @Value("${cassandra.username}") + private String username; + @Value("${cassandra.password}") + private String password; + + @Value("${cassandra.init_timeout_ms}") + private long initTimeout; + @Value("${cassandra.init_retry_interval_ms}") + private long initRetryInterval; + + private DriverConfigLoader loader; + + private ConsistencyLevel defaultReadConsistencyLevel; + private ConsistencyLevel defaultWriteConsistencyLevel; + + @PostConstruct + public void initLoader() { + ProgrammaticDriverConfigLoaderBuilder driverConfigBuilder = + DriverConfigLoader.programmaticBuilder(); + + driverConfigBuilder + .withStringList(DefaultDriverOption.CONTACT_POINTS, getContactPoints(url)) + .withString(DefaultDriverOption.SESSION_NAME, clusterName); + + this.initSocketOptions(driverConfigBuilder); + this.initPoolingOptions(driverConfigBuilder); + this.initQueryOptions(driverConfigBuilder); + + driverConfigBuilder.withString(DefaultDriverOption.PROTOCOL_COMPRESSION, + StringUtils.isEmpty(this.compression) ? "none" : this.compression.toLowerCase()); + + if (this.ssl) { + driverConfigBuilder.withString(DefaultDriverOption.SSL_ENGINE_FACTORY_CLASS, + "DefaultSslEngineFactory") + .withBoolean(DefaultDriverOption.SSL_HOSTNAME_VALIDATION, this.sslHostnameValidation); + if(!this.sslTrustStore.isEmpty()) { + driverConfigBuilder.withString(DefaultDriverOption.SSL_TRUSTSTORE_PATH, this.sslTrustStore) + .withString(DefaultDriverOption.SSL_TRUSTSTORE_PASSWORD, this.sslTrustStorePassword); + } + if(!this.sslKeyStore.isEmpty()) { + driverConfigBuilder.withString(DefaultDriverOption.SSL_KEYSTORE_PATH, this.sslKeyStore) + .withString(DefaultDriverOption.SSL_KEYSTORE_PASSWORD, this.sslKeyStorePassword); + } + if(!this.sslCipherSuites.isEmpty()) { + driverConfigBuilder.withStringList(DefaultDriverOption.SSL_CIPHER_SUITES, this.sslCipherSuites); + } + } + + if (this.metrics) { + driverConfigBuilder.withStringList(DefaultDriverOption.METRICS_SESSION_ENABLED, + Arrays.asList(DefaultSessionMetric.CONNECTED_NODES.getPath(), + DefaultSessionMetric.CQL_REQUESTS.getPath())); + driverConfigBuilder.withStringList(DefaultDriverOption.METRICS_NODE_ENABLED, + Arrays.asList(DefaultNodeMetric.OPEN_CONNECTIONS.getPath(), + DefaultNodeMetric.IN_FLIGHT.getPath())); + } + + if (this.credentials) { + driverConfigBuilder.withString(DefaultDriverOption.AUTH_PROVIDER_CLASS, + "PlainTextAuthProvider"); + driverConfigBuilder.withString(DefaultDriverOption.AUTH_PROVIDER_USER_NAME, + this.username); + driverConfigBuilder.withString(DefaultDriverOption.AUTH_PROVIDER_PASSWORD, + this.password); + } + + driverConfigBuilder.withBoolean(DefaultDriverOption.RECONNECT_ON_INIT, + true); + driverConfigBuilder.withString(DefaultDriverOption.RECONNECTION_POLICY_CLASS, + "ExponentialReconnectionPolicy"); + driverConfigBuilder.withDuration(DefaultDriverOption.RECONNECTION_BASE_DELAY, + Duration.ofMillis(this.initRetryInterval)); + driverConfigBuilder.withDuration(DefaultDriverOption.RECONNECTION_MAX_DELAY, + Duration.ofMillis(this.initTimeout)); + + this.loader = driverConfigBuilder.build(); + } + + protected ConsistencyLevel getDefaultReadConsistencyLevel() { + if (defaultReadConsistencyLevel == null) { + if (readConsistencyLevel != null) { + defaultReadConsistencyLevel = DefaultConsistencyLevel.valueOf(readConsistencyLevel.toUpperCase()); + } else { + defaultReadConsistencyLevel = DefaultConsistencyLevel.ONE; + } + } + return defaultReadConsistencyLevel; + } + + protected ConsistencyLevel getDefaultWriteConsistencyLevel() { + if (defaultWriteConsistencyLevel == null) { + if (writeConsistencyLevel != null) { + defaultWriteConsistencyLevel = DefaultConsistencyLevel.valueOf(writeConsistencyLevel.toUpperCase()); + } else { + defaultWriteConsistencyLevel = DefaultConsistencyLevel.ONE; + } + } + return defaultWriteConsistencyLevel; + } + + private void initSocketOptions(ProgrammaticDriverConfigLoaderBuilder driverConfigBuilder) { + driverConfigBuilder.withDuration(DefaultDriverOption.CONNECTION_CONNECT_TIMEOUT, + Duration.ofMillis(this.connectTimeoutMillis)); + driverConfigBuilder.withDuration(DefaultDriverOption.REQUEST_TIMEOUT, + Duration.ofMillis(this.readTimeoutMillis)); + if (this.keepAlive != null) { + driverConfigBuilder.withBoolean(DefaultDriverOption.SOCKET_KEEP_ALIVE, + this.keepAlive); + } + if (this.reuseAddress != null) { + driverConfigBuilder.withBoolean(DefaultDriverOption.SOCKET_REUSE_ADDRESS, + this.reuseAddress); + } + if (this.soLinger != null) { + driverConfigBuilder.withInt(DefaultDriverOption.SOCKET_LINGER_INTERVAL, + this.soLinger); + } + if (this.tcpNoDelay != null) { + driverConfigBuilder.withBoolean(DefaultDriverOption.SOCKET_TCP_NODELAY, + this.tcpNoDelay); + } + if (this.receiveBufferSize != null) { + driverConfigBuilder.withInt(DefaultDriverOption.SOCKET_RECEIVE_BUFFER_SIZE, + this.receiveBufferSize); + } + if (this.sendBufferSize != null) { + driverConfigBuilder.withInt(DefaultDriverOption.SOCKET_SEND_BUFFER_SIZE, + this.sendBufferSize); + } + } + + private void initPoolingOptions(ProgrammaticDriverConfigLoaderBuilder driverConfigBuilder) { + driverConfigBuilder.withInt(DefaultDriverOption.CONNECTION_MAX_REQUESTS, + this.max_requests_local); + } + + private void initQueryOptions(ProgrammaticDriverConfigLoaderBuilder driverConfigBuilder) { + driverConfigBuilder.withInt(DefaultDriverOption.REQUEST_PAGE_SIZE, + this.defaultFetchSize); + } + + private List getContactPoints(String url) { + List result; + if (StringUtils.isBlank(url)) { + result = Collections.emptyList(); + } else { + result = new ArrayList<>(); + for (String hostPort : url.split(COMMA)) { + result.add(hostPort); + } + } + return result; + } + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/CassandraQueryOptions.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/CassandraQueryOptions.java deleted file mode 100644 index 41191629ca..0000000000 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/CassandraQueryOptions.java +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.cassandra; - -import com.datastax.driver.core.ConsistencyLevel; -import com.datastax.driver.core.QueryOptions; -import lombok.Data; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Configuration; -import org.springframework.stereotype.Component; -import org.thingsboard.server.dao.util.NoSqlAnyDao; - -import javax.annotation.PostConstruct; - -@Component -@Configuration -@Data -@NoSqlAnyDao -public class CassandraQueryOptions { - - @Value("${cassandra.query.default_fetch_size}") - private Integer defaultFetchSize; - @Value("${cassandra.query.read_consistency_level}") - private String readConsistencyLevel; - @Value("${cassandra.query.write_consistency_level}") - private String writeConsistencyLevel; - - private QueryOptions opts; - - private ConsistencyLevel defaultReadConsistencyLevel; - private ConsistencyLevel defaultWriteConsistencyLevel; - - @PostConstruct - public void initOpts(){ - opts = new QueryOptions(); - opts.setFetchSize(defaultFetchSize); - } - - protected ConsistencyLevel getDefaultReadConsistencyLevel() { - if (defaultReadConsistencyLevel == null) { - if (readConsistencyLevel != null) { - defaultReadConsistencyLevel = ConsistencyLevel.valueOf(readConsistencyLevel.toUpperCase()); - } else { - defaultReadConsistencyLevel = ConsistencyLevel.ONE; - } - } - return defaultReadConsistencyLevel; - } - - protected ConsistencyLevel getDefaultWriteConsistencyLevel() { - if (defaultWriteConsistencyLevel == null) { - if (writeConsistencyLevel != null) { - defaultWriteConsistencyLevel = ConsistencyLevel.valueOf(writeConsistencyLevel.toUpperCase()); - } else { - defaultWriteConsistencyLevel = ConsistencyLevel.ONE; - } - } - return defaultWriteConsistencyLevel; - } -} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/CassandraSocketOptions.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/CassandraSocketOptions.java deleted file mode 100644 index cae5f5da30..0000000000 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/CassandraSocketOptions.java +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.cassandra; - -import com.datastax.driver.core.SocketOptions; -import lombok.Data; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Configuration; -import org.springframework.stereotype.Component; -import org.thingsboard.server.dao.util.NoSqlAnyDao; - -import javax.annotation.PostConstruct; - -@Component -@Configuration -@Data -@NoSqlAnyDao -public class CassandraSocketOptions { - - @Value("${cassandra.socket.connect_timeout}") - private int connectTimeoutMillis; - @Value("${cassandra.socket.read_timeout}") - private int readTimeoutMillis; - @Value("${cassandra.socket.keep_alive}") - private Boolean keepAlive; - @Value("${cassandra.socket.reuse_address}") - private Boolean reuseAddress; - @Value("${cassandra.socket.so_linger}") - private Integer soLinger; - @Value("${cassandra.socket.tcp_no_delay}") - private Boolean tcpNoDelay; - @Value("${cassandra.socket.receive_buffer_size}") - private Integer receiveBufferSize; - @Value("${cassandra.socket.send_buffer_size}") - private Integer sendBufferSize; - - private SocketOptions opts; - - @PostConstruct - public void initOpts() { - opts = new SocketOptions(); - opts.setConnectTimeoutMillis(connectTimeoutMillis); - opts.setReadTimeoutMillis(readTimeoutMillis); - if (keepAlive != null) { - opts.setKeepAlive(keepAlive); - } - if (reuseAddress != null) { - opts.setReuseAddress(reuseAddress); - } - if (soLinger != null) { - opts.setSoLinger(soLinger); - } - if (tcpNoDelay != null) { - opts.setTcpNoDelay(tcpNoDelay); - } - if (receiveBufferSize != null) { - opts.setReceiveBufferSize(receiveBufferSize); - } - if (sendBufferSize != null) { - opts.setSendBufferSize(sendBufferSize); - } - } -} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/DefaultGuavaSession.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/DefaultGuavaSession.java new file mode 100644 index 0000000000..b8ec3fb8e1 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/DefaultGuavaSession.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.cassandra.guava; + +import com.datastax.oss.driver.api.core.session.Session; +import com.datastax.oss.driver.internal.core.session.SessionWrapper; + +public class DefaultGuavaSession extends SessionWrapper implements GuavaSession { + + public DefaultGuavaSession(Session delegate) { + super(delegate); + } +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/GuavaDriverContext.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/GuavaDriverContext.java new file mode 100644 index 0000000000..31962b0d09 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/GuavaDriverContext.java @@ -0,0 +1,84 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.cassandra.guava; + +import com.datastax.oss.driver.api.core.config.DriverConfigLoader; +import com.datastax.oss.driver.api.core.cql.PrepareRequest; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeStateListener; +import com.datastax.oss.driver.api.core.metadata.schema.SchemaChangeListener; +import com.datastax.oss.driver.api.core.session.ProgrammaticArguments; +import com.datastax.oss.driver.api.core.tracker.RequestTracker; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.internal.core.context.DefaultDriverContext; +import com.datastax.oss.driver.internal.core.cql.CqlPrepareAsyncProcessor; +import com.datastax.oss.driver.internal.core.cql.CqlPrepareSyncProcessor; +import com.datastax.oss.driver.internal.core.cql.CqlRequestAsyncProcessor; +import com.datastax.oss.driver.internal.core.cql.CqlRequestSyncProcessor; +import com.datastax.oss.driver.internal.core.session.RequestProcessorRegistry; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; + +/** + * A Custom {@link DefaultDriverContext} that overrides {@link #getRequestProcessorRegistry()} to + * return a {@link RequestProcessorRegistry} that includes processors for returning guava futures. + */ +public class GuavaDriverContext extends DefaultDriverContext { + + public GuavaDriverContext( + DriverConfigLoader configLoader, + List> typeCodecs, + NodeStateListener nodeStateListener, + SchemaChangeListener schemaChangeListener, + RequestTracker requestTracker, + Map localDatacenters, + Map> nodeFilters, + ClassLoader classLoader) { + super( + configLoader, + ProgrammaticArguments.builder() + .addTypeCodecs(typeCodecs.toArray(new TypeCodec[0])) + .withNodeStateListener(nodeStateListener) + .withSchemaChangeListener(schemaChangeListener) + .withRequestTracker(requestTracker) + .withLocalDatacenters(localDatacenters) + .withNodeFilters(nodeFilters) + .withClassLoader(classLoader) + .build()); + } + + @Override + public RequestProcessorRegistry buildRequestProcessorRegistry() { + // Register the typical request processors, except instead of the normal async processors, + // use GuavaRequestAsyncProcessor to return ListenableFutures in async methods. + + CqlRequestAsyncProcessor cqlRequestAsyncProcessor = new CqlRequestAsyncProcessor(); + CqlPrepareAsyncProcessor cqlPrepareAsyncProcessor = new CqlPrepareAsyncProcessor(); + CqlRequestSyncProcessor cqlRequestSyncProcessor = + new CqlRequestSyncProcessor(cqlRequestAsyncProcessor); + + return new RequestProcessorRegistry( + getSessionName(), + cqlRequestSyncProcessor, + new CqlPrepareSyncProcessor(cqlPrepareAsyncProcessor), + new GuavaRequestAsyncProcessor<>( + cqlRequestAsyncProcessor, Statement.class, GuavaSession.ASYNC), + new GuavaRequestAsyncProcessor<>( + cqlPrepareAsyncProcessor, PrepareRequest.class, GuavaSession.ASYNC_PREPARED)); + } +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/GuavaMultiPageResultSet.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/GuavaMultiPageResultSet.java new file mode 100644 index 0000000000..dc4aebe542 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/GuavaMultiPageResultSet.java @@ -0,0 +1,123 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.cassandra.guava; + +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.cql.ResultSet; +import com.datastax.oss.driver.api.core.cql.Row; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.datastax.oss.driver.internal.core.util.CountingIterator; +import com.datastax.oss.driver.internal.core.util.concurrent.BlockingOperation; +import edu.umd.cs.findbugs.annotations.NonNull; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class GuavaMultiPageResultSet implements ResultSet { + + private final RowIterator iterator; + private final List executionInfos = new ArrayList<>(); + private ColumnDefinitions columnDefinitions; + + public GuavaMultiPageResultSet(@NonNull GuavaSession session, @NonNull Statement statement, @NonNull AsyncResultSet firstPage) { + assert firstPage.hasMorePages(); + this.iterator = new RowIterator(session, statement, firstPage); + this.executionInfos.add(firstPage.getExecutionInfo()); + this.columnDefinitions = firstPage.getColumnDefinitions(); + } + + @NonNull + @Override + public ColumnDefinitions getColumnDefinitions() { + return columnDefinitions; + } + + @NonNull + @Override + public List getExecutionInfos() { + return executionInfos; + } + + @Override + public boolean isFullyFetched() { + return iterator.isFullyFetched(); + } + + @Override + public int getAvailableWithoutFetching() { + return iterator.remaining(); + } + + @NonNull + @Override + public Iterator iterator() { + return iterator; + } + + @Override + public boolean wasApplied() { + return iterator.wasApplied(); + } + + private class RowIterator extends CountingIterator { + private GuavaSession session; + private Statement statement; + private AsyncResultSet currentPage; + private Iterator currentRows; + + private RowIterator(GuavaSession session, Statement statement, AsyncResultSet firstPage) { + super(firstPage.remaining()); + this.session = session; + this.statement = statement; + this.currentPage = firstPage; + this.currentRows = firstPage.currentPage().iterator(); + } + + @Override + protected Row computeNext() { + maybeMoveToNextPage(); + return currentRows.hasNext() ? currentRows.next() : endOfData(); + } + + private void maybeMoveToNextPage() { + if (!currentRows.hasNext() && currentPage.hasMorePages()) { + BlockingOperation.checkNotDriverThread(); + ByteBuffer nextPagingState = currentPage.getExecutionInfo().getPagingState(); + this.statement = this.statement.setPagingState(nextPagingState); + AsyncResultSet nextPage = GuavaSession.getSafe(this.session.executeAsync(this.statement)); + currentPage = nextPage; + remaining += nextPage.remaining(); + currentRows = nextPage.currentPage().iterator(); + executionInfos.add(nextPage.getExecutionInfo()); + // The definitions can change from page to page if this result set was built from a bound + // 'SELECT *', and the schema was altered. + columnDefinitions = nextPage.getColumnDefinitions(); + } + } + + private boolean isFullyFetched() { + return !currentPage.hasMorePages(); + } + + private boolean wasApplied() { + return currentPage.wasApplied(); + } + } +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/GuavaRequestAsyncProcessor.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/GuavaRequestAsyncProcessor.java new file mode 100644 index 0000000000..7947296cdb --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/GuavaRequestAsyncProcessor.java @@ -0,0 +1,79 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.cassandra.guava; + +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.session.DefaultSession; +import com.datastax.oss.driver.internal.core.session.RequestProcessor; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import java.util.concurrent.CompletionStage; + +/** + * Wraps a {@link RequestProcessor} that returns {@link CompletionStage}s and converts them to a + * {@link ListenableFuture}s. + * + * @param The type of request + * @param The type of responses enclosed in the future response. + */ +public class GuavaRequestAsyncProcessor + implements RequestProcessor> { + + private final RequestProcessor> subProcessor; + + private final GenericType resultType; + + private final Class requestClass; + + GuavaRequestAsyncProcessor( + RequestProcessor> subProcessor, + Class requestClass, + GenericType resultType) { + this.subProcessor = subProcessor; + this.requestClass = requestClass; + this.resultType = resultType; + } + + @Override + public boolean canProcess(Request request, GenericType resultType) { + return requestClass.isInstance(request) && resultType.equals(this.resultType); + } + + @Override + public ListenableFuture process( + T request, DefaultSession session, InternalDriverContext context, String sessionLogPrefix) { + SettableFuture future = SettableFuture.create(); + subProcessor + .process(request, session, context, sessionLogPrefix) + .whenComplete( + (r, ex) -> { + if (ex != null) { + future.setException(ex); + } else { + future.set(r); + } + }); + return future; + } + + @Override + public ListenableFuture newFailure(RuntimeException error) { + return Futures.immediateFailedFuture(error); + } +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/GuavaSession.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/GuavaSession.java new file mode 100644 index 0000000000..73c8f8c80c --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/GuavaSession.java @@ -0,0 +1,74 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.cassandra.guava; + +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.PreparedStatement; +import com.datastax.oss.driver.api.core.cql.ResultSet; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.datastax.oss.driver.api.core.cql.SyncCqlSession; +import com.datastax.oss.driver.api.core.session.Session; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.internal.core.cql.DefaultPrepareRequest; +import com.datastax.oss.driver.internal.core.cql.SinglePageResultSet; +import com.google.common.util.concurrent.ListenableFuture; +import edu.umd.cs.findbugs.annotations.NonNull; + +import java.util.concurrent.ExecutionException; + +public interface GuavaSession extends Session, SyncCqlSession { + + GenericType> ASYNC = + new GenericType>() {}; + + GenericType> ASYNC_PREPARED = + new GenericType>() {}; + + @NonNull + default ResultSet execute(@NonNull Statement statement) { + AsyncResultSet firstPage = getSafe(this.executeAsync(statement)); + if (firstPage.hasMorePages()) { + return new GuavaMultiPageResultSet(this, statement, firstPage); + } else { + return new SinglePageResultSet(firstPage); + } + } + + default ListenableFuture executeAsync(Statement statement) { + return this.execute(statement, ASYNC); + } + + default ListenableFuture executeAsync(String statement) { + return this.executeAsync(SimpleStatement.newInstance(statement)); + } + + default ListenableFuture prepareAsync(SimpleStatement statement) { + return this.execute(new DefaultPrepareRequest(statement), ASYNC_PREPARED); + } + + default ListenableFuture prepareAsync(String statement) { + return this.prepareAsync(SimpleStatement.newInstance(statement)); + } + + static AsyncResultSet getSafe(ListenableFuture future) { + try { + return future.get(); + } catch (InterruptedException | ExecutionException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/GuavaSessionBuilder.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/GuavaSessionBuilder.java new file mode 100644 index 0000000000..884a493f1f --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/GuavaSessionBuilder.java @@ -0,0 +1,59 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.cassandra.guava; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.config.DriverConfigLoader; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeStateListener; +import com.datastax.oss.driver.api.core.metadata.schema.SchemaChangeListener; +import com.datastax.oss.driver.api.core.session.SessionBuilder; +import com.datastax.oss.driver.api.core.tracker.RequestTracker; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; + +public class GuavaSessionBuilder extends SessionBuilder { + + @Override + protected DriverContext buildContext( + DriverConfigLoader configLoader, + List> typeCodecs, + NodeStateListener nodeStateListener, + SchemaChangeListener schemaChangeListener, + RequestTracker requestTracker, + Map localDatacenters, + Map> nodeFilters, + ClassLoader classLoader) { + return new GuavaDriverContext( + configLoader, + typeCodecs, + nodeStateListener, + schemaChangeListener, + requestTracker, + localDatacenters, + nodeFilters, + classLoader); + } + + @Override + protected GuavaSession wrap(@NonNull CqlSession defaultSession) { + return new DefaultGuavaSession(defaultSession); + } +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/GuavaSessionUtils.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/GuavaSessionUtils.java new file mode 100644 index 0000000000..acc8fb6ddf --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/GuavaSessionUtils.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.cassandra.guava; + +public class GuavaSessionUtils { + public static GuavaSessionBuilder builder() { + return new GuavaSessionBuilder(); + } +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/component/ComponentDescriptorService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/component/ComponentDescriptorService.java index cf5fa278a9..fb3cccc97d 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/component/ComponentDescriptorService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/component/ComponentDescriptorService.java @@ -18,8 +18,8 @@ package org.thingsboard.server.dao.component; import com.fasterxml.jackson.databind.JsonNode; import org.thingsboard.server.common.data.id.ComponentDescriptorId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentScope; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -35,9 +35,9 @@ public interface ComponentDescriptorService { ComponentDescriptor findByClazz(TenantId tenantId, String clazz); - TextPageData findByTypeAndPageLink(TenantId tenantId, ComponentType type, TextPageLink pageLink); + PageData findByTypeAndPageLink(TenantId tenantId, ComponentType type, PageLink pageLink); - TextPageData findByScopeAndTypeAndPageLink(TenantId tenantId, ComponentScope scope, ComponentType type, TextPageLink pageLink); + PageData findByScopeAndTypeAndPageLink(TenantId tenantId, ComponentScope scope, ComponentType type, PageLink pageLink); boolean validate(TenantId tenantId, ComponentDescriptor component, JsonNode configuration); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java index 60cadeaf8d..a129386d23 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java @@ -19,8 +19,8 @@ import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import java.util.Optional; @@ -38,7 +38,7 @@ public interface CustomerService { Customer findOrCreatePublicCustomer(TenantId tenantId); - TextPageData findCustomersByTenantId(TenantId tenantId, TextPageLink pageLink); + PageData findCustomersByTenantId(TenantId tenantId, PageLink pageLink); void deleteCustomersByTenantId(TenantId tenantId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java index 42693de88e..91590d8221 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java @@ -21,9 +21,8 @@ import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; -import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; public interface DashboardService { @@ -44,11 +43,11 @@ public interface DashboardService { void deleteDashboard(TenantId tenantId, DashboardId dashboardId); - TextPageData findDashboardsByTenantId(TenantId tenantId, TextPageLink pageLink); + PageData findDashboardsByTenantId(TenantId tenantId, PageLink pageLink); void deleteDashboardsByTenantId(TenantId tenantId); - ListenableFuture> findDashboardsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink); + PageData findDashboardsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink); void unassignCustomerDashboards(TenantId tenantId, CustomerId customerId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java new file mode 100644 index 0000000000..b4ec680aaf --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java @@ -0,0 +1,54 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.device; + +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileInfo; +import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; + +public interface DeviceProfileService { + + DeviceProfile findDeviceProfileById(TenantId tenantId, DeviceProfileId deviceProfileId); + + DeviceProfile findDeviceProfileByName(TenantId tenantId, String profileName); + + DeviceProfileInfo findDeviceProfileInfoById(TenantId tenantId, DeviceProfileId deviceProfileId); + + DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile); + + void deleteDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId); + + PageData findDeviceProfiles(TenantId tenantId, PageLink pageLink); + + PageData findDeviceProfileInfos(TenantId tenantId, PageLink pageLink, String transportType); + + DeviceProfile findOrCreateDeviceProfile(TenantId tenantId, String profileName); + + DeviceProfile createDefaultDeviceProfile(TenantId tenantId); + + DeviceProfile findDefaultDeviceProfile(TenantId tenantId); + + DeviceProfileInfo findDefaultDeviceProfileInfo(TenantId tenantId); + + boolean setDefaultDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId); + + void deleteDeviceProfilesByTenantId(TenantId tenantId); + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProvisionService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProvisionService.java new file mode 100644 index 0000000000..5d038b2d31 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProvisionService.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.device; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.dao.device.provision.ProvisionFailedException; +import org.thingsboard.server.dao.device.provision.ProvisionRequest; +import org.thingsboard.server.dao.device.provision.ProvisionResponse; + +public interface DeviceProvisionService { + + ProvisionResponse provisionDevice(ProvisionRequest provisionRequest) throws ProvisionFailedException; +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java index 1ed9ab9819..715313538d 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java @@ -17,18 +17,24 @@ package org.thingsboard.server.dao.device; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceInfo; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.device.DeviceSearchQuery; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.device.provision.ProvisionRequest; import java.util.List; public interface DeviceService { + DeviceInfo findDeviceInfoById(TenantId tenantId, DeviceId deviceId); + Device findDeviceById(TenantId tenantId, DeviceId deviceId); ListenableFuture findDeviceByIdAsync(TenantId tenantId, DeviceId deviceId); @@ -45,17 +51,29 @@ public interface DeviceService { void deleteDevice(TenantId tenantId, DeviceId deviceId); - TextPageData findDevicesByTenantId(TenantId tenantId, TextPageLink pageLink); + PageData findDevicesByTenantId(TenantId tenantId, PageLink pageLink); + + PageData findDeviceInfosByTenantId(TenantId tenantId, PageLink pageLink); + + PageData findDevicesByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink); + + PageData findDeviceInfosByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink); - TextPageData findDevicesByTenantIdAndType(TenantId tenantId, String type, TextPageLink pageLink); + PageData findDeviceInfosByTenantIdAndDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId, PageLink pageLink); ListenableFuture> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List deviceIds); void deleteDevicesByTenantId(TenantId tenantId); - TextPageData findDevicesByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TextPageLink pageLink); + PageData findDevicesByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink); - TextPageData findDevicesByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, TextPageLink pageLink); + PageData findDeviceInfosByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink); + + PageData findDevicesByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink); + + PageData findDeviceInfosByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink); + + PageData findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, PageLink pageLink); ListenableFuture> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List deviceIds); @@ -67,4 +85,6 @@ public interface DeviceService { Device assignDeviceToTenant(TenantId tenantId, Device device); + Device saveDevice(ProvisionRequest provisionRequest, DeviceProfile profile); + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionFailedException.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionFailedException.java new file mode 100644 index 0000000000..04e8e00ce5 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionFailedException.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.device.provision; + +public class ProvisionFailedException extends RuntimeException { + public ProvisionFailedException(String errorMsg) { + super(errorMsg); + } +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionRequest.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionRequest.java new file mode 100644 index 0000000000..f45b541074 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionRequest.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.device.provision; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.thingsboard.server.common.data.device.credentials.ProvisionDeviceCredentialsData; +import org.thingsboard.server.common.data.device.profile.ProvisionDeviceProfileCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; + +@Data +@AllArgsConstructor +public class ProvisionRequest { + private String deviceName; + private DeviceCredentialsType credentialsType; + private ProvisionDeviceCredentialsData credentialsData; + private ProvisionDeviceProfileCredentials credentials; +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionResponse.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionResponse.java new file mode 100644 index 0000000000..d38b53b134 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionResponse.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.device.provision; + +import lombok.Data; +import org.thingsboard.server.common.data.security.DeviceCredentials; + +@Data +public class ProvisionResponse { + private final DeviceCredentials deviceCredentials; + private final ProvisionResponseStatus responseStatus; +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionResponseStatus.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionResponseStatus.java new file mode 100644 index 0000000000..d4fbe65a97 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionResponseStatus.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.device.provision; + +public enum ProvisionResponseStatus { + UNKNOWN, + SUCCESS, + NOT_FOUND, + FAILURE +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java index 9e9c9359c5..04f567e934 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java @@ -16,8 +16,13 @@ package org.thingsboard.server.dao.entity; import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityDataQuery; public interface EntityService { @@ -25,4 +30,8 @@ public interface EntityService { void deleteEntityRelations(TenantId tenantId, EntityId entityId); + long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query); + + PageData findEntityDataByQuery(TenantId tenantId, CustomerId customerId, EntityDataQuery query); + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java index efe72ba60c..251d999abc 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java @@ -18,14 +18,15 @@ package org.thingsboard.server.dao.entityview; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.EntityViewInfo; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import java.util.List; @@ -42,17 +43,27 @@ public interface EntityViewService { void unassignCustomerEntityViews(TenantId tenantId, CustomerId customerId); + EntityViewInfo findEntityViewInfoById(TenantId tenantId, EntityViewId entityViewId); + EntityView findEntityViewById(TenantId tenantId, EntityViewId entityViewId); EntityView findEntityViewByTenantIdAndName(TenantId tenantId, String name); - TextPageData findEntityViewByTenantId(TenantId tenantId, TextPageLink pageLink); + PageData findEntityViewByTenantId(TenantId tenantId, PageLink pageLink); + + PageData findEntityViewInfosByTenantId(TenantId tenantId, PageLink pageLink); + + PageData findEntityViewByTenantIdAndType(TenantId tenantId, PageLink pageLink, String type); + + PageData findEntityViewInfosByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink); + + PageData findEntityViewsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink); - TextPageData findEntityViewByTenantIdAndType(TenantId tenantId, TextPageLink pageLink, String type); + PageData findEntityViewInfosByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink); - TextPageData findEntityViewsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TextPageLink pageLink); + PageData findEntityViewsByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, PageLink pageLink, String type); - TextPageData findEntityViewsByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, TextPageLink pageLink, String type); + PageData findEntityViewInfosByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink); ListenableFuture> findEntityViewsByQuery(TenantId tenantId, EntityViewSearchQuery query); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/event/EventService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/event/EventService.java index 99f5e8d537..f1157e84d8 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/event/EventService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/event/EventService.java @@ -19,7 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.Event; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import java.util.List; @@ -35,9 +35,9 @@ public interface EventService { Optional findEvent(TenantId tenantId, EntityId entityId, String eventType, String eventUid); - TimePageData findEvents(TenantId tenantId, EntityId entityId, TimePageLink pageLink); + PageData findEvents(TenantId tenantId, EntityId entityId, TimePageLink pageLink); - TimePageData findEvents(TenantId tenantId, EntityId entityId, String eventType, TimePageLink pageLink); + PageData findEvents(TenantId tenantId, EntityId entityId, String eventType, TimePageLink pageLink); List findLatestEvents(TenantId tenantId, EntityId entityId, String eventType, int limit); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/nosql/CassandraStatementTask.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/nosql/CassandraStatementTask.java index a433692d5c..b75c506ac9 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/nosql/CassandraStatementTask.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/nosql/CassandraStatementTask.java @@ -15,12 +15,17 @@ */ package org.thingsboard.server.dao.nosql; -import com.datastax.driver.core.Session; -import com.datastax.driver.core.Statement; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import lombok.Data; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.cassandra.guava.GuavaSession; import org.thingsboard.server.dao.util.AsyncTask; +import java.util.function.Function; + /** * Created by ashvayka on 24.10.18. */ @@ -28,7 +33,14 @@ import org.thingsboard.server.dao.util.AsyncTask; public class CassandraStatementTask implements AsyncTask { private final TenantId tenantId; - private final Session session; + private final GuavaSession session; private final Statement statement; + public ListenableFuture executeAsync(Function executeAsyncFunction) { + return Futures.transform(session.executeAsync(statement), + result -> new TbResultSet(statement, result, executeAsyncFunction), + MoreExecutors.directExecutor() + ); + } + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/nosql/TbResultSet.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/nosql/TbResultSet.java new file mode 100644 index 0000000000..735e6daab6 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/nosql/TbResultSet.java @@ -0,0 +1,131 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.nosql; + +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.cql.Row; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.google.common.collect.Lists; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; +import edu.umd.cs.findbugs.annotations.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; +import java.util.function.Function; + +public class TbResultSet implements AsyncResultSet { + + private final Statement originalStatement; + private final AsyncResultSet delegate; + private final Function executeAsyncFunction; + + public TbResultSet(Statement originalStatement, AsyncResultSet delegate, + Function executeAsyncFunction) { + this.originalStatement = originalStatement; + this.delegate = delegate; + this.executeAsyncFunction = executeAsyncFunction; + } + + @NonNull + @Override + public ColumnDefinitions getColumnDefinitions() { + return delegate.getColumnDefinitions(); + } + + @NonNull + @Override + public ExecutionInfo getExecutionInfo() { + return delegate.getExecutionInfo(); + } + + @Override + public int remaining() { + return delegate.remaining(); + } + + @NonNull + @Override + public Iterable currentPage() { + return delegate.currentPage(); + } + + @Override + public boolean hasMorePages() { + return delegate.hasMorePages(); + } + + @NonNull + @Override + public CompletionStage fetchNextPage() throws IllegalStateException { + return delegate.fetchNextPage(); + } + + @Override + public boolean wasApplied() { + return delegate.wasApplied(); + } + + public ListenableFuture> allRows(Executor executor) { + List allRows = new ArrayList<>(); + SettableFuture> resultFuture = SettableFuture.create(); + this.processRows(originalStatement, delegate, allRows, resultFuture, executor); + return resultFuture; + } + + private void processRows(Statement statement, + AsyncResultSet resultSet, + List allRows, + SettableFuture> resultFuture, + Executor executor) { + allRows.addAll(loadRows(resultSet)); + if (resultSet.hasMorePages()) { + ByteBuffer nextPagingState = resultSet.getExecutionInfo().getPagingState(); + Statement nextStatement = statement.setPagingState(nextPagingState); + TbResultSetFuture resultSetFuture = executeAsyncFunction.apply(nextStatement); + Futures.addCallback(resultSetFuture, + new FutureCallback() { + @Override + public void onSuccess(@Nullable TbResultSet result) { + processRows(nextStatement, result, + allRows, resultFuture, executor); + } + + @Override + public void onFailure(Throwable t) { + resultFuture.setException(t); + } + }, executor != null ? executor : MoreExecutors.directExecutor() + ); + } else { + resultFuture.set(allRows); + } + } + + List loadRows(AsyncResultSet resultSet) { + return Lists.newArrayList(resultSet.currentPage()); + } + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/nosql/TbResultSetFuture.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/nosql/TbResultSetFuture.java new file mode 100644 index 0000000000..2799f4d0b9 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/nosql/TbResultSetFuture.java @@ -0,0 +1,92 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.nosql; + +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * Created by ashvayka on 24.10.18. + */ +public class TbResultSetFuture implements ListenableFuture { + + private final SettableFuture mainFuture; + + public TbResultSetFuture(SettableFuture mainFuture) { + this.mainFuture = mainFuture; + } + + public TbResultSet getUninterruptibly() { + return getSafe(); + } + + public TbResultSet getUninterruptibly(long timeout, TimeUnit unit) throws TimeoutException { + return getSafe(timeout, unit); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return mainFuture.cancel(mayInterruptIfRunning); + } + + @Override + public boolean isCancelled() { + return mainFuture.isCancelled(); + } + + @Override + public boolean isDone() { + return mainFuture.isDone(); + } + + @Override + public TbResultSet get() throws InterruptedException, ExecutionException { + return mainFuture.get(); + } + + @Override + public TbResultSet get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return mainFuture.get(timeout, unit); + } + + @Override + public void addListener(Runnable listener, Executor executor) { + mainFuture.addListener(listener, executor); + } + + private TbResultSet getSafe() { + try { + return mainFuture.get(); + } catch (InterruptedException | ExecutionException e) { + throw new IllegalStateException(e); + } + } + + private TbResultSet getSafe(long timeout, TimeUnit unit) throws TimeoutException { + try { + return mainFuture.get(timeout, unit); + } catch (InterruptedException | ExecutionException e) { + throw new IllegalStateException(e); + } + } + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateService.java new file mode 100644 index 0000000000..54a47d9fdb --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateService.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.oauth2; + +import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; + +import java.util.List; +import java.util.Optional; + +public interface OAuth2ConfigTemplateService { + OAuth2ClientRegistrationTemplate saveClientRegistrationTemplate(OAuth2ClientRegistrationTemplate clientRegistrationTemplate); + + Optional findClientRegistrationTemplateByProviderId(String providerId); + + OAuth2ClientRegistrationTemplate findClientRegistrationTemplateById(OAuth2ClientRegistrationTemplateId templateId); + + List findAllClientRegistrationTemplates(); + + void deleteClientRegistrationTemplateById(OAuth2ClientRegistrationTemplateId templateId); +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Service.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Service.java index d72b6ef98c..073bab823b 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Service.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Service.java @@ -16,10 +16,20 @@ package org.thingsboard.server.dao.oauth2; import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientsParams; import java.util.List; +import java.util.UUID; public interface OAuth2Service { + List getOAuth2Clients(String domainScheme, String domainName); - List getOAuth2Clients(); + void saveOAuth2Params(OAuth2ClientsParams oauth2Params); + + OAuth2ClientsParams findOAuth2Params(); + + OAuth2ClientRegistrationInfo findClientRegistrationInfo(UUID id); + + List findAllClientRegistrationInfos(); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java index 806f49fe82..05fda67939 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java @@ -16,13 +16,16 @@ package org.thingsboard.server.dao.rule; import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainData; +import org.thingsboard.server.common.data.rule.RuleChainImportResult; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; @@ -57,10 +60,14 @@ public interface RuleChainService { List getRuleNodeRelations(TenantId tenantId, RuleNodeId ruleNodeId); - TextPageData findTenantRuleChains(TenantId tenantId, TextPageLink pageLink); + PageData findTenantRuleChains(TenantId tenantId, PageLink pageLink); void deleteRuleChainById(TenantId tenantId, RuleChainId ruleChainId); void deleteRuleChainsByTenantId(TenantId tenantId); + RuleChainData exportTenantRuleChains(TenantId tenantId, PageLink pageLink) throws ThingsboardException; + + List importTenantRuleChains(TenantId tenantId, RuleChainData ruleChainData, boolean overwrite); + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleNodeStateService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleNodeStateService.java new file mode 100644 index 0000000000..924cad0884 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleNodeStateService.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.rule; + +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.rule.RuleNodeState; + +public interface RuleNodeStateService { + + PageData findByRuleNodeId(TenantId tenantId, RuleNodeId ruleNodeId, PageLink pageLink); + + RuleNodeState findByRuleNodeIdAndEntityId(TenantId tenantId, RuleNodeId ruleNodeId, EntityId entityId); + + RuleNodeState save(TenantId tenantId, RuleNodeState ruleNodeState); + + void removeByRuleNodeId(TenantId tenantId, RuleNodeId selfId); + + void removeByRuleNodeIdAndEntityId(TenantId tenantId, RuleNodeId selfId, EntityId entityId); +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java index ee263c2564..a0c8d46d72 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java @@ -24,7 +24,7 @@ public interface AdminSettingsService { AdminSettings findAdminSettingsById(TenantId tenantId, AdminSettingsId adminSettingsId); AdminSettings findAdminSettingsByKey(TenantId tenantId, String key); - + AdminSettings saveAdminSettings(TenantId tenantId, AdminSettings adminSettings); - + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TbTenantProfileCache.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TbTenantProfileCache.java new file mode 100644 index 0000000000..735e23e73a --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TbTenantProfileCache.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.tenant; + +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; + +public interface TbTenantProfileCache { + + TenantProfile get(TenantId tenantId); + + TenantProfile get(TenantProfileId tenantProfileId); + + void put(TenantProfile profile); + + void evict(TenantProfileId id); + + void evict(TenantId id); + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileService.java new file mode 100644 index 0000000000..5ac3acfcb9 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileService.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.tenant; + +import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; + +public interface TenantProfileService { + + TenantProfile findTenantProfileById(TenantId tenantId, TenantProfileId tenantProfileId); + + EntityInfo findTenantProfileInfoById(TenantId tenantId, TenantProfileId tenantProfileId); + + TenantProfile saveTenantProfile(TenantId tenantId, TenantProfile tenantProfile); + + void deleteTenantProfile(TenantId tenantId, TenantProfileId tenantProfileId); + + PageData findTenantProfiles(TenantId tenantId, PageLink pageLink); + + PageData findTenantProfileInfos(TenantId tenantId, PageLink pageLink); + + TenantProfile findOrCreateDefaultTenantProfile(TenantId tenantId); + + TenantProfile findDefaultTenantProfile(TenantId tenantId); + + EntityInfo findDefaultTenantProfileInfo(TenantId tenantId); + + boolean setDefaultTenantProfile(TenantId tenantId, TenantProfileId tenantProfileId); + + void deleteTenantProfiles(TenantId tenantId); + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java index 6cd2df1f60..eaf835812b 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java @@ -17,21 +17,26 @@ package org.thingsboard.server.dao.tenant; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; public interface TenantService { Tenant findTenantById(TenantId tenantId); + TenantInfo findTenantInfoById(TenantId tenantId); + ListenableFuture findTenantByIdAsync(TenantId callerId, TenantId tenantId); Tenant saveTenant(Tenant tenant); void deleteTenant(TenantId tenantId); - TextPageData findTenants(TextPageLink pageLink); + PageData findTenants(PageLink pageLink); + + PageData findTenantInfos(PageLink pageLink); void deleteTenants(); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java index 9b3a578f58..0e11aedaf5 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java @@ -36,9 +36,15 @@ public interface TimeseriesService { ListenableFuture> findAllLatest(TenantId tenantId, EntityId entityId); - ListenableFuture> save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry); + ListenableFuture save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry); - ListenableFuture> save(TenantId tenantId, EntityId entityId, List tsKvEntry, long ttl); + ListenableFuture save(TenantId tenantId, EntityId entityId, List tsKvEntry, long ttl); + + ListenableFuture> saveLatest(TenantId tenantId, EntityId entityId, List tsKvEntry); ListenableFuture> remove(TenantId tenantId, EntityId entityId, List queries); + + ListenableFuture> removeLatest(TenantId tenantId, EntityId entityId, Collection keys); + + ListenableFuture> removeAllLatest(TenantId tenantId, EntityId entityId); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateService.java new file mode 100644 index 0000000000..5388cab283 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateService.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.usagerecord; + +import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.id.ApiUsageStateId; +import org.thingsboard.server.common.data.id.TenantId; + +public interface ApiUsageStateService { + + ApiUsageState createDefaultApiUsageState(TenantId id); + + ApiUsageState update(ApiUsageState apiUsageState); + + ApiUsageState findTenantApiUsageState(TenantId tenantId); + + void deleteApiUsageStateByTenantId(TenantId tenantId); + + ApiUsageState findApiUsageStateById(TenantId tenantId, ApiUsageStateId id); +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java index f0e2a0e7a1..494e233299 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java @@ -21,8 +21,8 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserCredentialsId; import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.UserCredentials; public interface UserService { @@ -52,12 +52,14 @@ public interface UserService { UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials); void deleteUser(TenantId tenantId, UserId userId); - - TextPageData findTenantAdmins(TenantId tenantId, TextPageLink pageLink); + + PageData findUsersByTenantId(TenantId tenantId, PageLink pageLink); + + PageData findTenantAdmins(TenantId tenantId, PageLink pageLink); void deleteTenantAdmins(TenantId tenantId); - - TextPageData findCustomerUsers(TenantId tenantId, CustomerId customerId, TextPageLink pageLink); + + PageData findCustomerUsers(TenantId tenantId, CustomerId customerId, PageLink pageLink); void deleteCustomerUsers(TenantId tenantId, CustomerId customerId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/util/NoSqlAnyDao.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/NoSqlAnyDao.java index bcf1222988..e267e5288d 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/util/NoSqlAnyDao.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/NoSqlAnyDao.java @@ -21,6 +21,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) -@ConditionalOnExpression("'${database.ts.type}'=='cassandra' || '${database.entities.type}'=='cassandra'") +@ConditionalOnExpression("'${database.ts.type}'=='cassandra' || '${database.ts_latest.type}'=='cassandra'") public @interface NoSqlAnyDao { } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/util/NoSqlDao.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/NoSqlDao.java deleted file mode 100644 index dde0a9a176..0000000000 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/util/NoSqlDao.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.util; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -@Retention(RetentionPolicy.RUNTIME) -@ConditionalOnProperty(prefix = "database.entities", value = "type", havingValue = "cassandra") -public @interface NoSqlDao { -} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/util/NoSqlTsLatestDao.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/NoSqlTsLatestDao.java new file mode 100644 index 0000000000..b9e83ddb56 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/NoSqlTsLatestDao.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.util; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@ConditionalOnProperty(prefix = "database.ts_latest", value = "type", havingValue = "cassandra") +public @interface NoSqlTsLatestDao { +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/util/PsqlTsAnyDao.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/PsqlTsAnyDao.java deleted file mode 100644 index a215a16b29..0000000000 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/util/PsqlTsAnyDao.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.util; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -@Retention(RetentionPolicy.RUNTIME) -@ConditionalOnExpression("('${database.ts.type}'=='sql' || '${database.ts.type}'=='timescale') " + - "&& '${spring.jpa.database-platform}'=='org.hibernate.dialect.PostgreSQLDialect'") -public @interface PsqlTsAnyDao { -} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/util/PsqlTsDao.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/PsqlTsDao.java deleted file mode 100644 index 95ca8c2b05..0000000000 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/util/PsqlTsDao.java +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.util; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -@Retention(RetentionPolicy.RUNTIME) -@ConditionalOnExpression("'${database.ts.type}'=='sql' && '${spring.jpa.database-platform}'=='org.hibernate.dialect.PostgreSQLDialect'") -public @interface PsqlTsDao { } \ No newline at end of file diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/util/PsqlTsLatestAnyDao.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/PsqlTsLatestAnyDao.java new file mode 100644 index 0000000000..9cf597e291 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/PsqlTsLatestAnyDao.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.util; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@ConditionalOnExpression("('${database.ts_latest.type}'=='sql' || '${database.ts_latest.type}'=='timescale') " + + "&& '${spring.jpa.database-platform}'=='org.hibernate.dialect.PostgreSQLDialect'") +public @interface PsqlTsLatestAnyDao { +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/util/SqlDao.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/SqlDao.java deleted file mode 100644 index 408f81a845..0000000000 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/util/SqlDao.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.util; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -@Retention(RetentionPolicy.RUNTIME) -@ConditionalOnProperty(prefix = "database.entities", value = "type", havingValue = "sql") -public @interface SqlDao { -} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/util/SqlTsAnyDao.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/SqlTsAnyDao.java deleted file mode 100644 index 9be3321988..0000000000 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/util/SqlTsAnyDao.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.util; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -@Retention(RetentionPolicy.RUNTIME) -@ConditionalOnExpression("'${database.ts.type}'=='sql' || '${database.ts.type}'=='timescale'") -public @interface SqlTsAnyDao { -} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/util/SqlTsLatestAnyDao.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/SqlTsLatestAnyDao.java new file mode 100644 index 0000000000..424e75ccdd --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/SqlTsLatestAnyDao.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.util; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@ConditionalOnExpression("'${database.ts_latest.type}'=='sql' || '${database.ts_latest.type}'=='timescale'") +public @interface SqlTsLatestAnyDao { +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/util/SqlTsLatestDao.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/SqlTsLatestDao.java new file mode 100644 index 0000000000..a2c07a43eb --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/SqlTsLatestDao.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.util; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@ConditionalOnProperty(prefix = "database.ts_latest", value = "type", havingValue = "sql") +public @interface SqlTsLatestDao { +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/util/SqlTsOrTsLatestAnyDao.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/SqlTsOrTsLatestAnyDao.java new file mode 100644 index 0000000000..f2f5ecae4a --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/SqlTsOrTsLatestAnyDao.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.util; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@ConditionalOnExpression("'${database.ts.type}'=='sql' || '${database.ts.type}'=='timescale' || '${database.ts_latest.type}'=='sql' || '${database.ts_latest.type}'=='timescale'") +public @interface SqlTsOrTsLatestAnyDao { +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/util/TimescaleDBTsLatestDao.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/TimescaleDBTsLatestDao.java new file mode 100644 index 0000000000..579cd1a600 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/TimescaleDBTsLatestDao.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.util; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@ConditionalOnProperty(prefix = "database.ts_latest", value = "type", havingValue = "timescale") +public @interface TimescaleDBTsLatestDao { +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/util/TimescaleDBTsOrTsLatestDao.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/TimescaleDBTsOrTsLatestDao.java new file mode 100644 index 0000000000..dcc44a0014 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/TimescaleDBTsOrTsLatestDao.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.util; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@ConditionalOnExpression("'${database.ts.type}'=='timescale' || '${database.ts_latest.type}'=='timescale'") +public @interface TimescaleDBTsOrTsLatestDao { +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java index f2ab6709df..1dce4f4155 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java @@ -17,8 +17,8 @@ package org.thingsboard.server.dao.widget; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetsBundleId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.widget.WidgetsBundle; import java.util.List; @@ -33,13 +33,13 @@ public interface WidgetsBundleService { WidgetsBundle findWidgetsBundleByTenantIdAndAlias(TenantId tenantId, String alias); - TextPageData findSystemWidgetsBundlesByPageLink(TenantId tenantId, TextPageLink pageLink); + PageData findSystemWidgetsBundlesByPageLink(TenantId tenantId, PageLink pageLink); List findSystemWidgetsBundles(TenantId tenantId); - TextPageData findTenantWidgetsBundlesByTenantId(TenantId tenantId, TextPageLink pageLink); + PageData findTenantWidgetsBundlesByTenantId(TenantId tenantId, PageLink pageLink); - TextPageData findAllTenantWidgetsBundlesByTenantIdAndPageLink(TenantId tenantId, TextPageLink pageLink); + PageData findAllTenantWidgetsBundlesByTenantIdAndPageLink(TenantId tenantId, PageLink pageLink); List findAllTenantWidgetsBundlesByTenantId(TenantId tenantId); diff --git a/common/data/pom.xml b/common/data/pom.xml index 8b43e5c8b1..eb7a3b226c 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT common org.thingsboard.common @@ -67,14 +67,33 @@ test - com.datastax.cassandra - cassandra-driver-core + com.datastax.oss + java-driver-core test + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + false + + diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ApiFeature.java b/common/data/src/main/java/org/thingsboard/server/common/data/ApiFeature.java new file mode 100644 index 0000000000..e12cb5fb14 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ApiFeature.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +import lombok.Getter; + +public enum ApiFeature { + TRANSPORT("transportApiState"), + DB("dbApiState"), + RE("ruleEngineApiState"), + JS("jsExecutionApiState"); + + @Getter + private final String apiStateKey; + + ApiFeature(String apiStateKey) { + this.apiStateKey = apiStateKey; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageRecordKey.java b/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageRecordKey.java new file mode 100644 index 0000000000..dc8556a755 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageRecordKey.java @@ -0,0 +1,60 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +import lombok.Getter; + +public enum ApiUsageRecordKey { + + TRANSPORT_MSG_COUNT(ApiFeature.TRANSPORT, "transportMsgCount", "transportMsgLimit"), + TRANSPORT_DP_COUNT(ApiFeature.TRANSPORT, "transportDataPointsCount", "transportDataPointsLimit"), + STORAGE_DP_COUNT(ApiFeature.DB, "storageDataPointsCount", "storageDataPointsLimit"), + RE_EXEC_COUNT(ApiFeature.RE, "ruleEngineExecutionCount", "ruleEngineExecutionLimit"), + JS_EXEC_COUNT(ApiFeature.JS, "jsExecutionCount", "jsExecutionLimit"); + private static final ApiUsageRecordKey[] JS_RECORD_KEYS = {JS_EXEC_COUNT}; + private static final ApiUsageRecordKey[] RE_RECORD_KEYS = {RE_EXEC_COUNT}; + private static final ApiUsageRecordKey[] DB_RECORD_KEYS = {STORAGE_DP_COUNT}; + private static final ApiUsageRecordKey[] TRANSPORT_RECORD_KEYS = {TRANSPORT_MSG_COUNT, TRANSPORT_DP_COUNT}; + + @Getter + private final ApiFeature apiFeature; + @Getter + private final String apiCountKey; + @Getter + private final String apiLimitKey; + + ApiUsageRecordKey(ApiFeature apiFeature, String apiCountKey, String apiLimitKey) { + this.apiFeature = apiFeature; + this.apiCountKey = apiCountKey; + this.apiLimitKey = apiLimitKey; + } + + public static ApiUsageRecordKey[] getKeys(ApiFeature feature) { + switch (feature) { + case TRANSPORT: + return TRANSPORT_RECORD_KEYS; + case DB: + return DB_RECORD_KEYS; + case RE: + return RE_RECORD_KEYS; + case JS: + return JS_RECORD_KEYS; + default: + return new ApiUsageRecordKey[]{}; + } + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageState.java b/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageState.java new file mode 100644 index 0000000000..52797baa3b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageState.java @@ -0,0 +1,84 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.ApiUsageStateId; + +@ToString +@EqualsAndHashCode(callSuper = true) +public class ApiUsageState extends BaseData implements HasTenantId { + + private static final long serialVersionUID = 8250339805336035966L; + + @Getter + @Setter + private TenantId tenantId; + @Getter + @Setter + private EntityId entityId; + @Getter + @Setter + private ApiUsageStateValue transportState; + @Getter + @Setter + private ApiUsageStateValue dbStorageState; + @Getter + @Setter + private ApiUsageStateValue reExecState; + @Getter + @Setter + private ApiUsageStateValue jsExecState; + + public ApiUsageState() { + super(); + } + + public ApiUsageState(ApiUsageStateId id) { + super(id); + } + + public ApiUsageState(ApiUsageState ur) { + super(ur); + this.tenantId = ur.getTenantId(); + this.entityId = ur.getEntityId(); + this.transportState = ur.getTransportState(); + this.dbStorageState = ur.getDbStorageState(); + this.reExecState = ur.getReExecState(); + this.jsExecState = ur.getJsExecState(); + } + + public boolean isTransportEnabled() { + return !ApiUsageStateValue.DISABLED.equals(transportState); + } + + public boolean isReExecEnabled() { + return !ApiUsageStateValue.DISABLED.equals(reExecState); + } + + public boolean isDbStorageEnabled() { + return !ApiUsageStateValue.DISABLED.equals(dbStorageState); + } + + public boolean isJsExecEnabled() { + return !ApiUsageStateValue.DISABLED.equals(jsExecState); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageStateValue.java b/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageStateValue.java new file mode 100644 index 0000000000..ae14ff80bf --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageStateValue.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +public enum ApiUsageStateValue { + + ENABLED, WARNING, DISABLED; + + + public static ApiUsageStateValue toMoreRestricted(ApiUsageStateValue a, ApiUsageStateValue b) { + return a.ordinal() > b.ordinal() ? a : b; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index 1890e7ba37..fe9f3df3ae 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -24,4 +24,6 @@ public class CacheConstants { public static final String ENTITY_VIEW_CACHE = "entityViews"; public static final String CLAIM_DEVICES_CACHE = "claimDevices"; public static final String SECURITY_SETTINGS_CACHE = "securitySettings"; + public static final String TENANT_PROFILE_CACHE = "tenantProfiles"; + public static final String DEVICE_PROFILE_CACHE = "deviceProfiles"; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java index e99db672a3..5aadca44ec 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java @@ -28,6 +28,10 @@ public class DataConstants { public static final String SERVER_SCOPE = "SERVER_SCOPE"; public static final String SHARED_SCOPE = "SHARED_SCOPE"; public static final String LATEST_TS = "LATEST_TS"; + public static final String IS_NEW_ALARM = "isNewAlarm"; + public static final String IS_EXISTING_ALARM = "isExistingAlarm"; + public static final String IS_SEVERITY_UPDATED_ALARM = "isSeverityUpdated"; + public static final String IS_CLEARED_ALARM = "isClearedAlarm"; public static final String[] allScopes() { return new String[]{CLIENT_SCOPE, SHARED_SCOPE, SERVER_SCOPE}; @@ -59,6 +63,8 @@ public class DataConstants { public static final String ALARM_CLEAR = "ALARM_CLEAR"; public static final String ENTITY_ASSIGNED_FROM_TENANT = "ENTITY_ASSIGNED_FROM_TENANT"; public static final String ENTITY_ASSIGNED_TO_TENANT = "ENTITY_ASSIGNED_TO_TENANT"; + public static final String PROVISION_SUCCESS = "PROVISION_SUCCESS"; + public static final String PROVISION_FAILURE = "PROVISION_FAILURE"; public static final String RPC_CALL_FROM_SERVER_TO_DEVICE = "RPC_CALL_FROM_SERVER_TO_DEVICE"; @@ -66,4 +72,18 @@ public class DataConstants { public static final String SECRET_KEY_FIELD_NAME = "secretKey"; public static final String DURATION_MS_FIELD_NAME = "durationMs"; + public static final String PROVISION = "provision"; + public static final String PROVISION_KEY = "provisionDeviceKey"; + public static final String PROVISION_SECRET = "provisionDeviceSecret"; + + public static final String DEVICE_NAME = "deviceName"; + public static final String DEVICE_TYPE = "deviceType"; + public static final String CERT_PUB_KEY = "x509CertPubKey"; + public static final String CREDENTIALS_TYPE = "credentialsType"; + public static final String TOKEN = "token"; + public static final String HASH = "hash"; + public static final String CLIENT_ID = "clientId"; + public static final String USERNAME = "username"; + public static final String PASSWORD = "password"; + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java index cd617ec345..ca8e5f5575 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java @@ -15,12 +15,22 @@ */ package org.thingsboard.server.common.data; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.core.JsonProcessingException; import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.device.data.DeviceData; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; +import java.io.ByteArrayInputStream; +import java.io.IOException; + @EqualsAndHashCode(callSuper = true) +@Slf4j public class Device extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId, HasCustomerId { private static final long serialVersionUID = 2807343040519543363L; @@ -30,6 +40,10 @@ public class Device extends SearchTextBasedWithAdditionalInfo implemen private String name; private String type; private String label; + private DeviceProfileId deviceProfileId; + private transient DeviceData deviceData; + @JsonIgnore + private byte[] deviceDataBytes; public Device() { super(); @@ -46,6 +60,8 @@ public class Device extends SearchTextBasedWithAdditionalInfo implemen this.name = device.getName(); this.type = device.getType(); this.label = device.getLabel(); + this.deviceProfileId = device.getDeviceProfileId(); + this.setDeviceData(device.getDeviceData()); } public TenantId getTenantId() { @@ -89,6 +105,41 @@ public class Device extends SearchTextBasedWithAdditionalInfo implemen this.label = label; } + public DeviceProfileId getDeviceProfileId() { + return deviceProfileId; + } + + public void setDeviceProfileId(DeviceProfileId deviceProfileId) { + this.deviceProfileId = deviceProfileId; + } + + public DeviceData getDeviceData() { + if (deviceData != null) { + return deviceData; + } else { + if (deviceDataBytes != null) { + try { + deviceData = mapper.readValue(new ByteArrayInputStream(deviceDataBytes), DeviceData.class); + } catch (IOException e) { + log.warn("Can't deserialize device data: ", e); + return null; + } + return deviceData; + } else { + return null; + } + } + } + + public void setDeviceData(DeviceData data) { + this.deviceData = data; + try { + this.deviceDataBytes = data != null ? mapper.writeValueAsBytes(data) : null; + } catch (JsonProcessingException e) { + log.warn("Can't serialize device data: ", e); + } + } + @Override public String getSearchText() { return getName(); @@ -107,6 +158,10 @@ public class Device extends SearchTextBasedWithAdditionalInfo implemen builder.append(type); builder.append(", label="); builder.append(label); + builder.append(", deviceProfileId="); + builder.append(deviceProfileId); + builder.append(", deviceData="); + builder.append(deviceData); builder.append(", additionalInfo="); builder.append(getAdditionalInfo()); builder.append(", createdTime="); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceInfo.java new file mode 100644 index 0000000000..56fb4bc11c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceInfo.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +import lombok.Data; +import org.thingsboard.server.common.data.id.DeviceId; + +@Data +public class DeviceInfo extends Device { + + private String customerTitle; + private boolean customerIsPublic; + private String deviceProfileName; + + public DeviceInfo() { + super(); + } + + public DeviceInfo(DeviceId deviceId) { + super(deviceId); + } + + public DeviceInfo(Device device, String customerTitle, boolean customerIsPublic, String deviceProfileName) { + super(device); + this.customerTitle = customerTitle; + this.customerIsPublic = customerIsPublic; + this.deviceProfileName = deviceProfileName; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java new file mode 100644 index 0000000000..b9ecb862eb --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java @@ -0,0 +1,109 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.core.JsonProcessingException; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo.mapper; + +@Data +@EqualsAndHashCode(callSuper = true) +@Slf4j +public class DeviceProfile extends SearchTextBased implements HasName, HasTenantId { + + private TenantId tenantId; + private String name; + private String description; + private boolean isDefault; + private DeviceProfileType type; + private DeviceTransportType transportType; + private DeviceProfileProvisionType provisionType; + private RuleChainId defaultRuleChainId; + private String defaultQueueName; + private transient DeviceProfileData profileData; + @JsonIgnore + private byte[] profileDataBytes; + private String provisionDeviceKey; + + public DeviceProfile() { + super(); + } + + public DeviceProfile(DeviceProfileId deviceProfileId) { + super(deviceProfileId); + } + + public DeviceProfile(DeviceProfile deviceProfile) { + super(deviceProfile); + this.tenantId = deviceProfile.getTenantId(); + this.name = deviceProfile.getName(); + this.description = deviceProfile.getDescription(); + this.isDefault = deviceProfile.isDefault(); + this.defaultRuleChainId = deviceProfile.getDefaultRuleChainId(); + this.defaultQueueName = deviceProfile.getDefaultQueueName(); + this.setProfileData(deviceProfile.getProfileData()); + this.provisionDeviceKey = deviceProfile.getProvisionDeviceKey(); + } + + @Override + public String getSearchText() { + return getName(); + } + + @Override + public String getName() { + return name; + } + + public DeviceProfileData getProfileData() { + if (profileData != null) { + return profileData; + } else { + if (profileDataBytes != null) { + try { + profileData = mapper.readValue(new ByteArrayInputStream(profileDataBytes), DeviceProfileData.class); + } catch (IOException e) { + log.warn("Can't deserialize device profile data: ", e); + return null; + } + return profileData; + } else { + return null; + } + } + } + + public void setProfileData(DeviceProfileData data) { + this.profileData = data; + try { + this.profileDataBytes = data != null ? mapper.writeValueAsBytes(data) : null; + } catch (JsonProcessingException e) { + log.warn("Can't serialize device profile data: ", e); + } + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java new file mode 100644 index 0000000000..310c9ece60 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import lombok.Value; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; + +import java.util.UUID; + +@Value +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DeviceProfileInfo extends EntityInfo { + + private final DeviceProfileType type; + private final DeviceTransportType transportType; + + @JsonCreator + public DeviceProfileInfo(@JsonProperty("id") EntityId id, + @JsonProperty("name") String name, + @JsonProperty("type") DeviceProfileType type, + @JsonProperty("transportType") DeviceTransportType transportType) { + super(id, name); + this.type = type; + this.transportType = transportType; + } + + public DeviceProfileInfo(UUID uuid, String name, DeviceProfileType type, DeviceTransportType transportType) { + super(EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE_PROFILE, uuid), name); + this.type = type; + this.transportType = transportType; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileProvisionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileProvisionType.java new file mode 100644 index 0000000000..f33c705d52 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileProvisionType.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +public enum DeviceProfileProvisionType { + DISABLED, + ALLOW_CREATE_NEW_DEVICES, + CHECK_PRE_PROVISIONED_DEVICES +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileType.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileType.java new file mode 100644 index 0000000000..93ca102082 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileType.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +public enum DeviceProfileType { + DEFAULT +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceTransportType.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceTransportType.java new file mode 100644 index 0000000000..f4a0f99f69 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceTransportType.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +public enum DeviceTransportType { + DEFAULT, + MQTT, + LWM2M +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityInfo.java new file mode 100644 index 0000000000..3bb3b134d4 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityInfo.java @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.HasId; + +import java.util.UUID; + +@Data +public class EntityInfo implements HasId, HasName { + + private final EntityId id; + private final String name; + + @JsonCreator + public EntityInfo(@JsonProperty("id") EntityId id, @JsonProperty("name") String name) { + this.id = id; + this.name = name; + } + + public EntityInfo(UUID uuid, String entityType, String name) { + this.id = EntityIdFactory.getByTypeAndUuid(entityType, uuid); + this.name = name; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index 7e43c59797..9f7fb711e8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -19,5 +19,5 @@ package org.thingsboard.server.common.data; * @author Andrew Shvayka */ public enum EntityType { - TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, RULE_CHAIN, RULE_NODE, ENTITY_VIEW, WIDGETS_BUNDLE, WIDGET_TYPE + TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, RULE_CHAIN, RULE_NODE, ENTITY_VIEW, WIDGETS_BUNDLE, WIDGET_TYPE, TENANT_PROFILE, DEVICE_PROFILE, API_USAGE_STATE; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java index 4c7e69d79e..0bf080899e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java @@ -55,6 +55,14 @@ public class EntityView extends SearchTextBasedWithAdditionalInfo public EntityView(EntityView entityView) { super(entityView); + this.entityId = entityView.getEntityId(); + this.tenantId = entityView.getTenantId(); + this.customerId = entityView.getCustomerId(); + this.name = entityView.getName(); + this.type = entityView.getType(); + this.keys = entityView.getKeys(); + this.startTimeMs = entityView.getStartTimeMs(); + this.endTimeMs = entityView.getEndTimeMs(); } @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityViewInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityViewInfo.java new file mode 100644 index 0000000000..fdf803e904 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityViewInfo.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +import lombok.Data; +import org.thingsboard.server.common.data.id.EntityViewId; + +@Data +public class EntityViewInfo extends EntityView { + + private String customerTitle; + private boolean customerIsPublic; + + public EntityViewInfo() { + super(); + } + + public EntityViewInfo(EntityViewId entityViewId) { + super(entityViewId); + } + + public EntityViewInfo(EntityView entityView, String customerTitle, boolean customerIsPublic) { + super(entityView); + this.customerTitle = customerTitle; + this.customerIsPublic = customerIsPublic; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SearchTextBasedWithAdditionalInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/SearchTextBasedWithAdditionalInfo.java index 8dc9bf6abc..4efd673d7b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/SearchTextBasedWithAdditionalInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/SearchTextBasedWithAdditionalInfo.java @@ -35,7 +35,7 @@ import java.util.function.Consumer; @Slf4j public abstract class SearchTextBasedWithAdditionalInfo extends SearchTextBased implements HasAdditionalInfo { - private static final ObjectMapper mapper = new ObjectMapper(); + public static final ObjectMapper mapper = new ObjectMapper(); private transient JsonNode additionalInfo; @JsonIgnore private byte[] additionalInfoBytes; @@ -84,7 +84,7 @@ public abstract class SearchTextBasedWithAdditionalInfo ext byte[] data = binaryData.get(); if (data != null) { try { - return new ObjectMapper().readTree(new ByteArrayInputStream(data)); + return mapper.readTree(new ByteArrayInputStream(data)); } catch (IOException e) { log.warn("Can't deserialize json data: ", e); return null; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java b/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java index 766d405e25..e7d71ab457 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java @@ -19,8 +19,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.id.TenantId; - -import com.fasterxml.jackson.databind.JsonNode; +import org.thingsboard.server.common.data.id.TenantProfileId; @EqualsAndHashCode(callSuper = true) public class Tenant extends ContactBased implements HasTenantId { @@ -29,8 +28,7 @@ public class Tenant extends ContactBased implements HasTenantId { private String title; private String region; - private boolean isolatedTbCore; - private boolean isolatedTbRuleEngine; + private TenantProfileId tenantProfileId; public Tenant() { super(); @@ -44,6 +42,7 @@ public class Tenant extends ContactBased implements HasTenantId { super(tenant); this.title = tenant.getTitle(); this.region = tenant.getRegion(); + this.tenantProfileId = tenant.getTenantProfileId(); } public String getTitle() { @@ -74,20 +73,12 @@ public class Tenant extends ContactBased implements HasTenantId { this.region = region; } - public boolean isIsolatedTbCore() { - return isolatedTbCore; - } - - public void setIsolatedTbCore(boolean isolatedTbCore) { - this.isolatedTbCore = isolatedTbCore; - } - - public boolean isIsolatedTbRuleEngine() { - return isolatedTbRuleEngine; + public TenantProfileId getTenantProfileId() { + return tenantProfileId; } - public void setIsolatedTbRuleEngine(boolean isolatedTbRuleEngine) { - this.isolatedTbRuleEngine = isolatedTbRuleEngine; + public void setTenantProfileId(TenantProfileId tenantProfileId) { + this.tenantProfileId = tenantProfileId; } @Override @@ -102,10 +93,8 @@ public class Tenant extends ContactBased implements HasTenantId { builder.append(title); builder.append(", region="); builder.append(region); - builder.append(", isolatedTbCore="); - builder.append(isolatedTbCore); - builder.append(", isolatedTbRuleEngine="); - builder.append(isolatedTbRuleEngine); + builder.append(", tenantProfileId="); + builder.append(tenantProfileId); builder.append(", additionalInfo="); builder.append(getAdditionalInfo()); builder.append(", country="); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TenantInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/TenantInfo.java new file mode 100644 index 0000000000..ee94b26f18 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TenantInfo.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +import lombok.Data; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +public class TenantInfo extends Tenant { + + private String tenantProfileName; + + public TenantInfo() { + super(); + } + + public TenantInfo(TenantId tenantId) { + super(tenantId); + } + + public TenantInfo(Tenant tenant, String tenantProfileName) { + super(tenant); + this.tenantProfileName = tenantProfileName; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java new file mode 100644 index 0000000000..acb9c239b8 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java @@ -0,0 +1,107 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.core.JsonProcessingException; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo.mapper; + +@Data +@EqualsAndHashCode(callSuper = true) +@Slf4j +public class TenantProfile extends SearchTextBased implements HasName { + + private String name; + private String description; + private boolean isDefault; + private boolean isolatedTbCore; + private boolean isolatedTbRuleEngine; + private transient TenantProfileData profileData; + @JsonIgnore + private byte[] profileDataBytes; + + public TenantProfile() { + super(); + } + + public TenantProfile(TenantProfileId tenantProfileId) { + super(tenantProfileId); + } + + public TenantProfile(TenantProfile tenantProfile) { + super(tenantProfile); + this.name = tenantProfile.getName(); + this.description = tenantProfile.getDescription(); + this.isDefault = tenantProfile.isDefault(); + this.isolatedTbCore = tenantProfile.isIsolatedTbCore(); + this.isolatedTbRuleEngine = tenantProfile.isIsolatedTbRuleEngine(); + this.setProfileData(tenantProfile.getProfileData()); + } + + @Override + public String getSearchText() { + return getName(); + } + + @Override + public String getName() { + return name; + } + + public TenantProfileData getProfileData() { + if (profileData != null) { + return profileData; + } else { + if (profileDataBytes != null) { + try { + profileData = mapper.readValue(new ByteArrayInputStream(profileDataBytes), TenantProfileData.class); + } catch (IOException e) { + log.warn("Can't deserialize tenant profile data: ", e); + return createDefaultTenantProfileData(); + } + return profileData; + } else { + return createDefaultTenantProfileData(); + } + } + } + + public TenantProfileData createDefaultTenantProfileData() { + TenantProfileData tpd = new TenantProfileData(); + tpd.setConfiguration(new DefaultTenantProfileConfiguration()); + return tpd; + } + + public void setProfileData(TenantProfileData data) { + this.profileData = data; + try { + this.profileDataBytes = data != null ? mapper.writeValueAsBytes(data) : null; + } catch (JsonProcessingException e) { + log.warn("Can't serialize tenant profile data: ", e); + } + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfileType.java b/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfileType.java new file mode 100644 index 0000000000..2b0ed502ac --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfileType.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +public enum TenantProfileType { + DEFAULT +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TransportPayloadType.java b/common/data/src/main/java/org/thingsboard/server/common/data/TransportPayloadType.java new file mode 100644 index 0000000000..99939914f3 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TransportPayloadType.java @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +public enum TransportPayloadType { + JSON, + PROTOBUF +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java index 3a44a3f5ff..9ae1a74d91 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java @@ -29,6 +29,11 @@ public class AlarmInfo extends Alarm { super(alarm); } + public AlarmInfo(Alarm alarm, String originatorName) { + super(alarm); + this.originatorName = originatorName; + } + public String getOriginatorName() { return originatorName; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java index 224ac6050a..995dc102e7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java @@ -22,6 +22,8 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.TimePageLink; +import java.util.UUID; + /** * Created by ashvayka on 11.05.17. */ @@ -35,5 +37,6 @@ public class AlarmQuery { private AlarmSearchStatus searchStatus; private AlarmStatus status; private Boolean fetchOriginator; + private UUID idOffset; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSearchStatus.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSearchStatus.java index fafb0d0347..4231170d69 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSearchStatus.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSearchStatus.java @@ -15,8 +15,26 @@ */ package org.thingsboard.server.common.data.alarm; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + public enum AlarmSearchStatus { - ANY, ACTIVE, CLEARED, ACK, UNACK + ANY(AlarmStatus.values()), + ACTIVE(AlarmStatus.ACTIVE_ACK, AlarmStatus.ACTIVE_UNACK), + CLEARED(AlarmStatus.CLEARED_ACK, AlarmStatus.CLEARED_UNACK), + ACK(AlarmStatus.ACTIVE_ACK, AlarmStatus.CLEARED_ACK), + UNACK(AlarmStatus.ACTIVE_UNACK, AlarmStatus.CLEARED_UNACK); + + @JsonIgnore + @Getter + private Set statuses; + AlarmSearchStatus(AlarmStatus... statuses) { + this.statuses = new LinkedHashSet<>(Arrays.asList(statuses)); + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetInfo.java new file mode 100644 index 0000000000..4dc065a973 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetInfo.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.asset; + +import lombok.Data; +import org.thingsboard.server.common.data.id.AssetId; + +@Data +public class AssetInfo extends Asset { + + private String customerTitle; + private boolean customerIsPublic; + + public AssetInfo() { + super(); + } + + public AssetInfo(AssetId assetId) { + super(assetId); + } + + public AssetInfo(Asset asset, String customerTitle, boolean customerIsPublic) { + super(asset); + this.customerTitle = customerTitle; + this.customerIsPublic = customerIsPublic; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java index 14a38c810c..e30e20090d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java @@ -42,7 +42,9 @@ public enum ActionType { LOGOUT(false), LOCKOUT(false), ASSIGNED_FROM_TENANT(false), - ASSIGNED_TO_TENANT(false); + ASSIGNED_TO_TENANT(false), + PROVISION_SUCCESS(false), + PROVISION_FAILURE(false); private final boolean isRead; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/BasicMqttCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/BasicMqttCredentials.java new file mode 100644 index 0000000000..eeef8617b5 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/BasicMqttCredentials.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.credentials; + +import lombok.Data; + +@Data +public class BasicMqttCredentials { + + private String clientId; + private String userName; + private String password; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/ProvisionDeviceCredentialsData.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/ProvisionDeviceCredentialsData.java new file mode 100644 index 0000000000..fe6cc2621a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/ProvisionDeviceCredentialsData.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.credentials; + +import lombok.Data; + +@Data +public class ProvisionDeviceCredentialsData { + private final String token; + private final String clientId; + private final String username; + private final String password; + private final String x509CertHash; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DefaultDeviceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DefaultDeviceConfiguration.java new file mode 100644 index 0000000000..61a2481922 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DefaultDeviceConfiguration.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.data; + +import lombok.Data; +import org.thingsboard.server.common.data.DeviceProfileType; + +@Data +public class DefaultDeviceConfiguration implements DeviceConfiguration { + + @Override + public DeviceProfileType getType() { + return DeviceProfileType.DEFAULT; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DefaultDeviceTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DefaultDeviceTransportConfiguration.java new file mode 100644 index 0000000000..1825193e01 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DefaultDeviceTransportConfiguration.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.data; + +import lombok.Data; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; + +@Data +public class DefaultDeviceTransportConfiguration implements DeviceTransportConfiguration { + + @Override + public DeviceTransportType getType() { + return DeviceTransportType.DEFAULT; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceConfiguration.java new file mode 100644 index 0000000000..1ea2ee4f97 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceConfiguration.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.data; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.thingsboard.server.common.data.DeviceProfileType; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = DefaultDeviceConfiguration.class, name = "DEFAULT")}) +public interface DeviceConfiguration { + + @JsonIgnore + DeviceProfileType getType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceData.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceData.java new file mode 100644 index 0000000000..6c24ba5e28 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceData.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.data; + +import lombok.Data; + +@Data +public class DeviceData { + + private DeviceConfiguration configuration; + private DeviceTransportConfiguration transportConfiguration; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceTransportConfiguration.java new file mode 100644 index 0000000000..e9bd1a3245 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceTransportConfiguration.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.data; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.thingsboard.server.common.data.DeviceTransportType; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = DefaultDeviceTransportConfiguration.class, name = "DEFAULT"), + @JsonSubTypes.Type(value = MqttDeviceTransportConfiguration.class, name = "MQTT"), + @JsonSubTypes.Type(value = Lwm2mDeviceTransportConfiguration.class, name = "LWM2M")}) +public interface DeviceTransportConfiguration { + + @JsonIgnore + DeviceTransportType getType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/Lwm2mDeviceTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/Lwm2mDeviceTransportConfiguration.java new file mode 100644 index 0000000000..e37ef14933 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/Lwm2mDeviceTransportConfiguration.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.data; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; + +import java.util.HashMap; +import java.util.Map; + +@Data +public class Lwm2mDeviceTransportConfiguration implements DeviceTransportConfiguration { + + @JsonIgnore + private Map properties = new HashMap<>(); + + @JsonAnyGetter + public Map properties() { + return this.properties; + } + + @JsonAnySetter + public void put(String name, Object value) { + this.properties.put(name, value); + } + + @Override + public DeviceTransportType getType() { + return DeviceTransportType.LWM2M; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/MqttDeviceTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/MqttDeviceTransportConfiguration.java new file mode 100644 index 0000000000..3d27193ae8 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/MqttDeviceTransportConfiguration.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.data; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import org.thingsboard.server.common.data.DeviceTransportType; + +import java.util.HashMap; +import java.util.Map; + +@Data +public class MqttDeviceTransportConfiguration implements DeviceTransportConfiguration { + + @JsonIgnore + private Map properties = new HashMap<>(); + + @JsonAnyGetter + public Map properties() { + return this.properties; + } + + @JsonAnySetter + public void put(String name, Object value) { + this.properties.put(name, value); + } + + @Override + public DeviceTransportType getType() { + return DeviceTransportType.MQTT; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java new file mode 100644 index 0000000000..32db0f730f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import org.thingsboard.server.common.data.query.KeyFilter; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class AlarmCondition { + + private List condition; + private AlarmConditionSpec spec; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java new file mode 100644 index 0000000000..915f62681b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = SimpleAlarmConditionSpec.class, name = "SIMPLE"), + @JsonSubTypes.Type(value = DurationAlarmConditionSpec.class, name = "DURATION"), + @JsonSubTypes.Type(value = RepeatingAlarmConditionSpec.class, name = "REPEATING")}) +public interface AlarmConditionSpec { + + @JsonIgnore + AlarmConditionSpecType getType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpecType.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpecType.java new file mode 100644 index 0000000000..11ea8e6347 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpecType.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +public enum AlarmConditionSpecType { + + SIMPLE, + DURATION, + REPEATING + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java new file mode 100644 index 0000000000..cf830ab8de --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import lombok.Data; + +@Data +public class AlarmRule { + + private AlarmCondition condition; + private AlarmSchedule schedule; + // Advanced + private String alarmDetails; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java new file mode 100644 index 0000000000..2c7d460df0 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = AnyTimeSchedule.class, name = "ANY_TIME"), + @JsonSubTypes.Type(value = SpecificTimeSchedule.class, name = "SPECIFIC_TIME"), + @JsonSubTypes.Type(value = CustomTimeSchedule.class, name = "CUSTOM")}) +public interface AlarmSchedule { + + AlarmScheduleType getType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmScheduleType.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmScheduleType.java new file mode 100644 index 0000000000..e72502a954 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmScheduleType.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +public enum AlarmScheduleType { + + ANY_TIME, + SPECIFIC_TIME, + CUSTOM + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AllowCreateNewDevicesDeviceProfileProvisionConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AllowCreateNewDevicesDeviceProfileProvisionConfiguration.java new file mode 100644 index 0000000000..8414caa400 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AllowCreateNewDevicesDeviceProfileProvisionConfiguration.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import lombok.Data; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; + +@Data +public class AllowCreateNewDevicesDeviceProfileProvisionConfiguration implements DeviceProfileProvisionConfiguration { + + private final String provisionDeviceSecret; + + @Override + public DeviceProfileProvisionType getType() { + return DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AnyTimeSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AnyTimeSchedule.java new file mode 100644 index 0000000000..fb7e10bc22 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AnyTimeSchedule.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +public class AnyTimeSchedule implements AlarmSchedule { + + @Override + public AlarmScheduleType getType() { + return AlarmScheduleType.ANY_TIME; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.java new file mode 100644 index 0000000000..40af29ea51 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import lombok.Data; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; + +@Data +public class CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration implements DeviceProfileProvisionConfiguration { + + private final String provisionDeviceSecret; + + @Override + public DeviceProfileProvisionType getType() { + return DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeSchedule.java new file mode 100644 index 0000000000..7d41a72f46 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeSchedule.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import lombok.Data; + +import java.util.List; + +@Data +public class CustomTimeSchedule implements AlarmSchedule { + + private String timezone; + private List items; + + @Override + public AlarmScheduleType getType() { + return AlarmScheduleType.CUSTOM; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java new file mode 100644 index 0000000000..ba5735988d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import lombok.Data; + +import java.util.List; + +@Data +public class CustomTimeScheduleItem { + + private boolean enabled; + private int dayOfWeek; + private long startsOn; + private long endsOn; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultDeviceProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultDeviceProfileConfiguration.java new file mode 100644 index 0000000000..d8e6cef10b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultDeviceProfileConfiguration.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import lombok.Data; +import org.thingsboard.server.common.data.DeviceProfileType; + +@Data +public class DefaultDeviceProfileConfiguration implements DeviceProfileConfiguration { + + @Override + public DeviceProfileType getType() { + return DeviceProfileType.DEFAULT; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultDeviceProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultDeviceProfileTransportConfiguration.java new file mode 100644 index 0000000000..5610e2555f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultDeviceProfileTransportConfiguration.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import lombok.Data; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; + +@Data +public class DefaultDeviceProfileTransportConfiguration implements DeviceProfileTransportConfiguration { + + @Override + public DeviceTransportType getType() { + return DeviceTransportType.DEFAULT; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java new file mode 100644 index 0000000000..17035f337b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import lombok.Data; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@Data +public class DeviceProfileAlarm { + + private String id; + private String alarmType; + + private LinkedHashMap createRules; + private AlarmRule clearRule; + + // Hidden in advanced settings + private boolean propagate; + private List propagateRelationTypes; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileConfiguration.java new file mode 100644 index 0000000000..3bb3d29c34 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileConfiguration.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.thingsboard.server.common.data.DeviceProfileType; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = DefaultDeviceProfileConfiguration.class, name = "DEFAULT")}) +public interface DeviceProfileConfiguration { + + @JsonIgnore + DeviceProfileType getType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java new file mode 100644 index 0000000000..7cb3304934 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import lombok.Data; + +import java.util.List; + +@Data +public class DeviceProfileData { + + private DeviceProfileConfiguration configuration; + private DeviceProfileTransportConfiguration transportConfiguration; + private DeviceProfileProvisionConfiguration provisionConfiguration; + private List alarms; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java new file mode 100644 index 0000000000..f892aabf76 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; + + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = DisabledDeviceProfileProvisionConfiguration.class, name = "DISABLED"), + @JsonSubTypes.Type(value = AllowCreateNewDevicesDeviceProfileProvisionConfiguration.class, name = "ALLOW_CREATE_NEW_DEVICES"), + @JsonSubTypes.Type(value = CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.class, name = "CHECK_PRE_PROVISIONED_DEVICES")}) +public interface DeviceProfileProvisionConfiguration { + + String getProvisionDeviceSecret(); + + @JsonIgnore + DeviceProfileProvisionType getType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileTransportConfiguration.java new file mode 100644 index 0000000000..34854958d1 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileTransportConfiguration.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = DefaultDeviceProfileTransportConfiguration.class, name = "DEFAULT"), + @JsonSubTypes.Type(value = MqttDeviceProfileTransportConfiguration.class, name = "MQTT"), + @JsonSubTypes.Type(value = Lwm2mDeviceProfileTransportConfiguration.class, name = "LWM2M")}) +public interface DeviceProfileTransportConfiguration { + + @JsonIgnore + DeviceTransportType getType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DisabledDeviceProfileProvisionConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DisabledDeviceProfileProvisionConfiguration.java new file mode 100644 index 0000000000..01876c810d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DisabledDeviceProfileProvisionConfiguration.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import lombok.Data; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; + +@Data +public class DisabledDeviceProfileProvisionConfiguration implements DeviceProfileProvisionConfiguration { + + private final String provisionDeviceSecret; + + @Override + public DeviceProfileProvisionType getType() { + return DeviceProfileProvisionType.DISABLED; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DurationAlarmConditionSpec.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DurationAlarmConditionSpec.java new file mode 100644 index 0000000000..cb27e45538 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DurationAlarmConditionSpec.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +import java.util.concurrent.TimeUnit; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class DurationAlarmConditionSpec implements AlarmConditionSpec { + + private TimeUnit unit; + private long value; + + @Override + public AlarmConditionSpecType getType() { + return AlarmConditionSpecType.DURATION; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/Lwm2mDeviceProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/Lwm2mDeviceProfileTransportConfiguration.java new file mode 100644 index 0000000000..b2bdd63009 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/Lwm2mDeviceProfileTransportConfiguration.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; + +import java.util.HashMap; +import java.util.Map; + +@Data +public class Lwm2mDeviceProfileTransportConfiguration implements DeviceProfileTransportConfiguration { + + @JsonIgnore + private Map properties = new HashMap<>(); + + @JsonAnyGetter + public Map properties() { + return this.properties; + } + + @JsonAnySetter + public void put(String name, Object value) { + this.properties.put(name, value); + } + + @Override + public DeviceTransportType getType() { + return DeviceTransportType.LWM2M; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java new file mode 100644 index 0000000000..d88ac24cbb --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import lombok.Data; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.DeviceTransportType; + +@Data +public class MqttDeviceProfileTransportConfiguration implements DeviceProfileTransportConfiguration { + + private TransportPayloadType transportPayloadType = TransportPayloadType.JSON; + + private String deviceTelemetryTopic = MqttTopics.DEVICE_TELEMETRY_TOPIC; + private String deviceAttributesTopic = MqttTopics.DEVICE_ATTRIBUTES_TOPIC; + + @Override + public DeviceTransportType getType() { + return DeviceTransportType.MQTT; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttTopics.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttTopics.java new file mode 100644 index 0000000000..b226b87999 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttTopics.java @@ -0,0 +1,74 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +/** + * Created by ashvayka on 19.01.17. + */ +public class MqttTopics { + + private static final String REQUEST = "/request"; + private static final String RESPONSE = "/response"; + private static final String RPC = "/rpc"; + private static final String CONNECT = "/connect"; + private static final String DISCONNECT = "/disconnect"; + private static final String TELEMETRY = "/telemetry"; + private static final String ATTRIBUTES = "/attributes"; + private static final String CLAIM = "/claim"; + private static final String SUB_TOPIC = "+"; + private static final String PROVISION = "/provision"; + + private static final String ATTRIBUTES_RESPONSE = ATTRIBUTES + RESPONSE; + private static final String ATTRIBUTES_REQUEST = ATTRIBUTES + REQUEST; + + private static final String DEVICE_RPC_RESPONSE = RPC + RESPONSE + "/"; + private static final String DEVICE_RPC_REQUEST = RPC + REQUEST + "/"; + + private static final String DEVICE_ATTRIBUTES_RESPONSE = ATTRIBUTES_RESPONSE + "/"; + private static final String DEVICE_ATTRIBUTES_REQUEST = ATTRIBUTES_REQUEST + "/"; + + // V1_JSON topics + + public static final String BASE_DEVICE_API_TOPIC = "v1/devices/me"; + + public static final String DEVICE_RPC_RESPONSE_TOPIC = BASE_DEVICE_API_TOPIC + DEVICE_RPC_RESPONSE; + public static final String DEVICE_RPC_RESPONSE_SUB_TOPIC = DEVICE_RPC_RESPONSE_TOPIC + SUB_TOPIC; + public static final String DEVICE_RPC_REQUESTS_TOPIC = BASE_DEVICE_API_TOPIC + DEVICE_RPC_REQUEST; + public static final String DEVICE_RPC_REQUESTS_SUB_TOPIC = DEVICE_RPC_REQUESTS_TOPIC + SUB_TOPIC; + public static final String DEVICE_ATTRIBUTES_RESPONSE_TOPIC_PREFIX = BASE_DEVICE_API_TOPIC + DEVICE_ATTRIBUTES_RESPONSE; + public static final String DEVICE_ATTRIBUTES_RESPONSES_TOPIC = DEVICE_ATTRIBUTES_RESPONSE_TOPIC_PREFIX + SUB_TOPIC; + public static final String DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX = BASE_DEVICE_API_TOPIC + DEVICE_ATTRIBUTES_REQUEST; + public static final String DEVICE_TELEMETRY_TOPIC = BASE_DEVICE_API_TOPIC + TELEMETRY; + public static final String DEVICE_CLAIM_TOPIC = BASE_DEVICE_API_TOPIC + CLAIM; + public static final String DEVICE_ATTRIBUTES_TOPIC = BASE_DEVICE_API_TOPIC + ATTRIBUTES; + public static final String DEVICE_PROVISION_REQUEST_TOPIC = PROVISION + REQUEST; + public static final String DEVICE_PROVISION_RESPONSE_TOPIC = PROVISION + RESPONSE; + + // V1_JSON gateway topics + + public static final String BASE_GATEWAY_API_TOPIC = "v1/gateway"; + public static final String GATEWAY_CONNECT_TOPIC = BASE_GATEWAY_API_TOPIC + CONNECT; + public static final String GATEWAY_DISCONNECT_TOPIC = BASE_GATEWAY_API_TOPIC + DISCONNECT; + public static final String GATEWAY_ATTRIBUTES_TOPIC = BASE_GATEWAY_API_TOPIC + ATTRIBUTES; + public static final String GATEWAY_TELEMETRY_TOPIC = BASE_GATEWAY_API_TOPIC + TELEMETRY; + public static final String GATEWAY_CLAIM_TOPIC = BASE_GATEWAY_API_TOPIC + CLAIM; + public static final String GATEWAY_RPC_TOPIC = BASE_GATEWAY_API_TOPIC + RPC; + public static final String GATEWAY_ATTRIBUTES_REQUEST_TOPIC = BASE_GATEWAY_API_TOPIC + ATTRIBUTES_REQUEST; + public static final String GATEWAY_ATTRIBUTES_RESPONSE_TOPIC = BASE_GATEWAY_API_TOPIC + ATTRIBUTES_RESPONSE; + + private MqttTopics() { + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/ProvisionDeviceProfileCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/ProvisionDeviceProfileCredentials.java new file mode 100644 index 0000000000..659da71488 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/ProvisionDeviceProfileCredentials.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import lombok.Data; + +@Data +public class ProvisionDeviceProfileCredentials { + private final String provisionDeviceKey; + private final String provisionDeviceSecret; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/RepeatingAlarmConditionSpec.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/RepeatingAlarmConditionSpec.java new file mode 100644 index 0000000000..3883676ee5 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/RepeatingAlarmConditionSpec.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +import java.util.concurrent.TimeUnit; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class RepeatingAlarmConditionSpec implements AlarmConditionSpec { + + private int count; + + @Override + public AlarmConditionSpecType getType() { + return AlarmConditionSpecType.REPEATING; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SimpleAlarmConditionSpec.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SimpleAlarmConditionSpec.java new file mode 100644 index 0000000000..547044581a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SimpleAlarmConditionSpec.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SimpleAlarmConditionSpec implements AlarmConditionSpec { + @Override + public AlarmConditionSpecType getType() { + return AlarmConditionSpecType.SIMPLE; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SpecificTimeSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SpecificTimeSchedule.java new file mode 100644 index 0000000000..c099b57680 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SpecificTimeSchedule.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.profile; + +import lombok.Data; + +import java.util.List; +import java.util.Set; + +@Data +public class SpecificTimeSchedule implements AlarmSchedule { + + private String timezone; + private Set daysOfWeek; + private long startsOn; + private long endsOn; + + @Override + public AlarmScheduleType getType() { + return AlarmScheduleType.SPECIFIC_TIME; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiUsageStateId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiUsageStateId.java new file mode 100644 index 0000000000..87983f519a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiUsageStateId.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.thingsboard.server.common.data.EntityType; + +import java.util.UUID; + +public class ApiUsageStateId extends UUIDBased implements EntityId { + + @JsonCreator + public ApiUsageStateId(@JsonProperty("id") UUID id) { + super(id); + } + + public static ApiUsageStateId fromString(String userId) { + return new ApiUsageStateId(UUID.fromString(userId)); + } + + @JsonIgnore + @Override + public EntityType getEntityType() { + return EntityType.API_USAGE_STATE; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceProfileId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceProfileId.java new file mode 100644 index 0000000000..0350e37205 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceProfileId.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.id; + +import java.util.UUID; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.thingsboard.server.common.data.EntityType; + +public class DeviceProfileId extends UUIDBased implements EntityId { + + private static final long serialVersionUID = 1L; + + @JsonCreator + public DeviceProfileId(@JsonProperty("id") UUID id) { + super(id); + } + + public static DeviceProfileId fromString(String deviceProfileId) { + return new DeviceProfileId(UUID.fromString(deviceProfileId)); + } + + @JsonIgnore + @Override + public EntityType getEntityType() { + return EntityType.DEVICE_PROFILE; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java index a799a40b18..8c754db18c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java @@ -29,7 +29,7 @@ import java.util.UUID; @JsonDeserialize(using = EntityIdDeserializer.class) @JsonSerialize(using = EntityIdSerializer.class) -public interface EntityId extends Serializable { //NOSONAR, the constant is closely related to EntityId +public interface EntityId extends HasUUID, Serializable { //NOSONAR, the constant is closely related to EntityId UUID NULL_UUID = UUID.fromString("13814000-1dd2-11b2-8080-808080808080"); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index 11db1da1a0..8a1a769c5e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -62,6 +62,12 @@ public class EntityIdFactory { return new WidgetsBundleId(uuid); case WIDGET_TYPE: return new WidgetTypeId(uuid); + case DEVICE_PROFILE: + return new DeviceProfileId(uuid); + case TENANT_PROFILE: + return new TenantProfileId(uuid); + case API_USAGE_STATE: + return new ApiUsageStateId(uuid); } throw new IllegalArgumentException("EntityType " + type + " is not supported!"); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/HasId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/HasId.java new file mode 100644 index 0000000000..86d2269079 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/HasId.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.id; + +import java.io.Serializable; + +public interface HasId extends Serializable { + + I getId(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/HasUUID.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/HasUUID.java new file mode 100644 index 0000000000..e43294603d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/HasUUID.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.thingsboard.server.common.data.id; + +import java.util.UUID; + +public interface HasUUID { + + UUID getId(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/IdBased.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/IdBased.java index 79b8a3cce1..8957253bbf 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/IdBased.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/IdBased.java @@ -20,7 +20,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import java.io.Serializable; import java.util.UUID; -public abstract class IdBased implements Serializable { +public abstract class IdBased implements HasId { protected I id; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientRegistrationId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientRegistrationId.java new file mode 100644 index 0000000000..e87d9c7b2e --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientRegistrationId.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.UUID; + +public class OAuth2ClientRegistrationId extends UUIDBased { + + @JsonCreator + public OAuth2ClientRegistrationId(@JsonProperty("id") UUID id) { + super(id); + } + + public static OAuth2ClientRegistrationId fromString(String clientRegistrationId) { + return new OAuth2ClientRegistrationId(UUID.fromString(clientRegistrationId)); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientRegistrationInfoId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientRegistrationInfoId.java new file mode 100644 index 0000000000..87a2109909 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientRegistrationInfoId.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.UUID; + +public class OAuth2ClientRegistrationInfoId extends UUIDBased { + + @JsonCreator + public OAuth2ClientRegistrationInfoId(@JsonProperty("id") UUID id) { + super(id); + } + + public static OAuth2ClientRegistrationInfoId fromString(String clientRegistrationInfoId) { + return new OAuth2ClientRegistrationInfoId(UUID.fromString(clientRegistrationInfoId)); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientRegistrationTemplateId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientRegistrationTemplateId.java new file mode 100644 index 0000000000..cc6ee3a795 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientRegistrationTemplateId.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.UUID; + +public class OAuth2ClientRegistrationTemplateId extends UUIDBased { + + @JsonCreator + public OAuth2ClientRegistrationTemplateId(@JsonProperty("id") UUID id) { + super(id); + } + + public static OAuth2ClientRegistrationTemplateId fromString(String clientRegistrationTemplateId) { + return new OAuth2ClientRegistrationTemplateId(UUID.fromString(clientRegistrationTemplateId)); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2IntegrationId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2IntegrationId.java deleted file mode 100644 index 30fd55d204..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2IntegrationId.java +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.common.data.id; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.UUID; - -public class OAuth2IntegrationId extends UUIDBased { - - private static final long serialVersionUID = 1L; - - @JsonCreator - public OAuth2IntegrationId(@JsonProperty("id") UUID id) { - super(id); - } - - public static OAuth2IntegrationId fromString(String oauth2IntegrationId) { - return new OAuth2IntegrationId(UUID.fromString(oauth2IntegrationId)); - } -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeStateId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeStateId.java new file mode 100644 index 0000000000..7bdf411353 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeStateId.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.UUID; + +public class RuleNodeStateId extends UUIDBased { + + private static final long serialVersionUID = 1L; + + @JsonCreator + public RuleNodeStateId(@JsonProperty("id") UUID id) { + super(id); + } + + public static RuleNodeStateId fromString(String eventId) { + return new RuleNodeStateId(UUID.fromString(eventId)); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantProfileId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantProfileId.java new file mode 100644 index 0000000000..8f3d27891c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantProfileId.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.id; + +import java.util.UUID; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.thingsboard.server.common.data.EntityType; + +public class TenantProfileId extends UUIDBased implements EntityId { + + private static final long serialVersionUID = 1L; + + @JsonCreator + public TenantProfileId(@JsonProperty("id") UUID id) { + super(id); + } + + public static TenantProfileId fromString(String tenantProfileId) { + return new TenantProfileId(UUID.fromString(tenantProfileId)); + } + + @JsonIgnore + @Override + public EntityType getEntityType() { + return EntityType.TENANT_PROFILE; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java index 7e07d216f4..d70b9d0d30 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java @@ -18,7 +18,7 @@ package org.thingsboard.server.common.data.id; import java.io.Serializable; import java.util.UUID; -public abstract class UUIDBased implements Serializable { +public abstract class UUIDBased implements HasUUID, Serializable { private static final long serialVersionUID = 1L; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseReadTsKvQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseReadTsKvQuery.java index a2433440a7..9e40bc11e2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseReadTsKvQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseReadTsKvQuery.java @@ -23,27 +23,27 @@ public class BaseReadTsKvQuery extends BaseTsKvQuery implements ReadTsKvQuery { private final long interval; private final int limit; private final Aggregation aggregation; - private final String orderBy; + private final String order; public BaseReadTsKvQuery(String key, long startTs, long endTs, long interval, int limit, Aggregation aggregation) { this(key, startTs, endTs, interval, limit, aggregation, "DESC"); } public BaseReadTsKvQuery(String key, long startTs, long endTs, long interval, int limit, Aggregation aggregation, - String orderBy) { + String order) { super(key, startTs, endTs); this.interval = interval; this.limit = limit; this.aggregation = aggregation; - this.orderBy = orderBy; + this.order = order; } public BaseReadTsKvQuery(String key, long startTs, long endTs) { this(key, startTs, endTs, endTs - startTs, 1, Aggregation.AVG, "DESC"); } - public BaseReadTsKvQuery(String key, long startTs, long endTs, int limit, String orderBy) { - this(key, startTs, endTs, endTs - startTs, limit, Aggregation.NONE, orderBy); + public BaseReadTsKvQuery(String key, long startTs, long endTs, int limit, String order) { + this(key, startTs, endTs, endTs - startTs, limit, Aggregation.NONE, order); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java index c2d6688004..bcf49899dc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java @@ -19,7 +19,7 @@ import java.util.Objects; import java.util.Optional; public class BasicTsKvEntry implements TsKvEntry { - + private static final int MAX_CHARS_PER_DATA_POINT = 512; private final long ts; private final KvEntry kv; @@ -99,4 +99,21 @@ public class BasicTsKvEntry implements TsKvEntry { public String getValueAsString() { return kv.getValueAsString(); } + + @Override + public int getDataPoints() { + int length; + switch (getDataType()) { + case STRING: + length = getStrValue().get().length(); + break; + case JSON: + length = getJsonValue().get().length(); + break; + default: + return 1; + } + return Math.max(1, (length + MAX_CHARS_PER_DATA_POINT - 1) / MAX_CHARS_PER_DATA_POINT); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/ReadTsKvQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/ReadTsKvQuery.java index 5b73693af3..9c5e6541bc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/ReadTsKvQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/ReadTsKvQuery.java @@ -23,6 +23,6 @@ public interface ReadTsKvQuery extends TsKvQuery { Aggregation getAggregation(); - String getOrderBy(); + String getOrder(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntry.java index 315d3940df..e2f8eaf1e5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntry.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntry.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.kv; +import com.fasterxml.jackson.annotation.JsonIgnore; + /** * Represents time series KV data entry * @@ -25,4 +27,7 @@ public interface TsKvEntry extends KvEntry { long getTs(); + @JsonIgnore + int getDataPoints(); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/ClientRegistrationDto.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/ClientRegistrationDto.java new file mode 100644 index 0000000000..b3b1911523 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/ClientRegistrationDto.java @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.oauth2; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.*; +import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationInfoId; + +import java.util.List; + +@EqualsAndHashCode +@Data +@ToString(exclude = {"clientSecret"}) +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ClientRegistrationDto { + private OAuth2MapperConfig mapperConfig; + private String clientId; + private String clientSecret; + private String authorizationUri; + private String accessTokenUri; + private List scope; + private String userInfoUri; + private String userNameAttributeName; + private String jwkSetUri; + private String clientAuthenticationMethod; + private String loginButtonLabel; + private String loginButtonIcon; + private JsonNode additionalInfo; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/DomainInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/DomainInfo.java new file mode 100644 index 0000000000..7eb5b34661 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/DomainInfo.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.oauth2; + +import lombok.*; + +@EqualsAndHashCode +@Data +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class DomainInfo { + private SchemeType scheme; + private String name; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/ExtendedOAuth2ClientRegistrationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/ExtendedOAuth2ClientRegistrationInfo.java new file mode 100644 index 0000000000..9998b2bd35 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/ExtendedOAuth2ClientRegistrationInfo.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.oauth2; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +public class ExtendedOAuth2ClientRegistrationInfo extends OAuth2ClientRegistrationInfo { + + private String domainName; + private SchemeType domainScheme; + + public ExtendedOAuth2ClientRegistrationInfo() { + super(); + } + + public ExtendedOAuth2ClientRegistrationInfo(OAuth2ClientRegistrationInfo oAuth2ClientRegistrationInfo, + SchemeType domainScheme, + String domainName) { + super(oAuth2ClientRegistrationInfo); + this.domainScheme = domainScheme; + this.domainName = domainName; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/MapperType.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/MapperType.java new file mode 100644 index 0000000000..25faad187e --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/MapperType.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.oauth2; + +public enum MapperType { + BASIC, CUSTOM, GITHUB; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2BasicMapperConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2BasicMapperConfig.java new file mode 100644 index 0000000000..3f9f1c6dc2 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2BasicMapperConfig.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.oauth2; + +import lombok.*; + +@Builder(toBuilder = true) +@EqualsAndHashCode +@Data +@ToString +public class OAuth2BasicMapperConfig { + private final String emailAttributeKey; + private final String firstNameAttributeKey; + private final String lastNameAttributeKey; + private final TenantNameStrategyType tenantNameStrategy; + private final String tenantNamePattern; + private final String customerNamePattern; + private final String defaultDashboardName; + private final boolean alwaysFullScreen; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientInfo.java index 0ee5832e63..f15706a55e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientInfo.java @@ -17,27 +17,20 @@ package org.thingsboard.server.common.data.oauth2; import lombok.Data; import lombok.EqualsAndHashCode; -import org.thingsboard.server.common.data.BaseData; -import org.thingsboard.server.common.data.id.OAuth2IntegrationId; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; -@EqualsAndHashCode(callSuper = true) +@EqualsAndHashCode @Data -public class OAuth2ClientInfo extends BaseData { +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2ClientInfo { private String name; private String icon; private String url; - public OAuth2ClientInfo() { - super(); - } - - public OAuth2ClientInfo(OAuth2IntegrationId id) { - super(id); - } - public OAuth2ClientInfo(OAuth2ClientInfo oauth2ClientInfo) { - super(oauth2ClientInfo); this.name = oauth2ClientInfo.getName(); this.icon = oauth2ClientInfo.getIcon(); this.url = oauth2ClientInfo.getUrl(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistration.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistration.java new file mode 100644 index 0000000000..0ced05cfdb --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistration.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.oauth2; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationId; +import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationInfoId; + +@EqualsAndHashCode(callSuper = true) +@Data +@ToString +@NoArgsConstructor +public class OAuth2ClientRegistration extends BaseData { + + private OAuth2ClientRegistrationInfoId clientRegistrationId; + private String domainName; + private SchemeType domainScheme; + + public OAuth2ClientRegistration(OAuth2ClientRegistration clientRegistration) { + super(clientRegistration); + this.clientRegistrationId = clientRegistration.clientRegistrationId; + this.domainName = clientRegistration.domainName; + this.domainScheme = clientRegistration.domainScheme; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationInfo.java new file mode 100644 index 0000000000..177d677abf --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationInfo.java @@ -0,0 +1,76 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.oauth2; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; +import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationInfoId; + +import java.util.List; + +@EqualsAndHashCode(callSuper = true) +@Data +@ToString(exclude = {"clientSecret"}) +@NoArgsConstructor +public class OAuth2ClientRegistrationInfo extends SearchTextBasedWithAdditionalInfo implements HasName { + + private boolean enabled; + private OAuth2MapperConfig mapperConfig; + private String clientId; + private String clientSecret; + private String authorizationUri; + private String accessTokenUri; + private List scope; + private String userInfoUri; + private String userNameAttributeName; + private String jwkSetUri; + private String clientAuthenticationMethod; + private String loginButtonLabel; + private String loginButtonIcon; + + public OAuth2ClientRegistrationInfo(OAuth2ClientRegistrationInfo clientRegistration) { + super(clientRegistration); + this.enabled = clientRegistration.enabled; + this.mapperConfig = clientRegistration.mapperConfig; + this.clientId = clientRegistration.clientId; + this.clientSecret = clientRegistration.clientSecret; + this.authorizationUri = clientRegistration.authorizationUri; + this.accessTokenUri = clientRegistration.accessTokenUri; + this.scope = clientRegistration.scope; + this.userInfoUri = clientRegistration.userInfoUri; + this.userNameAttributeName = clientRegistration.userNameAttributeName; + this.jwkSetUri = clientRegistration.jwkSetUri; + this.clientAuthenticationMethod = clientRegistration.clientAuthenticationMethod; + this.loginButtonLabel = clientRegistration.loginButtonLabel; + this.loginButtonIcon = clientRegistration.loginButtonIcon; + } + + @Override + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + public String getName() { + return loginButtonLabel; + } + + @Override + public String getSearchText() { + return getName(); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java new file mode 100644 index 0000000000..30c6e1c4a9 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java @@ -0,0 +1,76 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.oauth2; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; +import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.List; + +@EqualsAndHashCode(callSuper = true) +@Data +@ToString +@NoArgsConstructor +public class OAuth2ClientRegistrationTemplate extends SearchTextBasedWithAdditionalInfo implements HasName { + + private String providerId; + private OAuth2MapperConfig mapperConfig; + private String authorizationUri; + private String accessTokenUri; + private List scope; + private String userInfoUri; + private String userNameAttributeName; + private String jwkSetUri; + private String clientAuthenticationMethod; + private String comment; + private String loginButtonIcon; + private String loginButtonLabel; + private String helpLink; + + public OAuth2ClientRegistrationTemplate(OAuth2ClientRegistrationTemplate clientRegistrationTemplate) { + super(clientRegistrationTemplate); + this.providerId = clientRegistrationTemplate.providerId; + this.mapperConfig = clientRegistrationTemplate.mapperConfig; + this.authorizationUri = clientRegistrationTemplate.authorizationUri; + this.accessTokenUri = clientRegistrationTemplate.accessTokenUri; + this.scope = clientRegistrationTemplate.scope; + this.userInfoUri = clientRegistrationTemplate.userInfoUri; + this.userNameAttributeName = clientRegistrationTemplate.userNameAttributeName; + this.jwkSetUri = clientRegistrationTemplate.jwkSetUri; + this.clientAuthenticationMethod = clientRegistrationTemplate.clientAuthenticationMethod; + this.comment = clientRegistrationTemplate.comment; + this.loginButtonIcon = clientRegistrationTemplate.loginButtonIcon; + this.loginButtonLabel = clientRegistrationTemplate.loginButtonLabel; + this.helpLink = clientRegistrationTemplate.helpLink; + } + + @Override + public String getName() { + return providerId; + } + + @Override + public String getSearchText() { + return getName(); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientsDomainParams.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientsDomainParams.java new file mode 100644 index 0000000000..4e932fad85 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientsDomainParams.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.oauth2; + +import lombok.*; + +import java.util.List; +import java.util.Set; + +@EqualsAndHashCode +@Data +@ToString +@Builder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2ClientsDomainParams { + private List domainInfos; + private List clientRegistrations; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientsParams.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientsParams.java new file mode 100644 index 0000000000..ad5d5305bd --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientsParams.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.oauth2; + +import lombok.*; + +import java.util.List; +import java.util.Set; + +@EqualsAndHashCode +@Data +@ToString +@Builder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2ClientsParams { + private boolean enabled; + private List domainsParams; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2CustomMapperConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2CustomMapperConfig.java new file mode 100644 index 0000000000..cead19c39c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2CustomMapperConfig.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.oauth2; + +import lombok.*; + +@Builder(toBuilder = true) +@EqualsAndHashCode +@Data +@ToString(exclude = {"password"}) +public class OAuth2CustomMapperConfig { + private final String url; + private final String username; + private final String password; + private final boolean sendToken; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MapperConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MapperConfig.java new file mode 100644 index 0000000000..15b3067cda --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MapperConfig.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.oauth2; + +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Builder(toBuilder = true) +@EqualsAndHashCode +@Data +@ToString +public class OAuth2MapperConfig { + private boolean allowUserCreation; + private boolean activateUser; + private MapperType type; + private OAuth2BasicMapperConfig basic; + private OAuth2CustomMapperConfig custom; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/SchemeType.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/SchemeType.java new file mode 100644 index 0000000000..80d0362b7d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/SchemeType.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.oauth2; + +public enum SchemeType { + HTTP, HTTPS, MIXED; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/TenantNameStrategyType.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/TenantNameStrategyType.java new file mode 100644 index 0000000000..91562a883a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/TenantNameStrategyType.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.oauth2; + +public enum TenantNameStrategyType { + DOMAIN, EMAIL, CUSTOM; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/BasePageLink.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/BasePageLink.java deleted file mode 100644 index 5021824ad2..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/page/BasePageLink.java +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.common.data.page; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.Setter; - -import java.io.Serializable; -import java.util.Arrays; -import java.util.UUID; - -@RequiredArgsConstructor -@AllArgsConstructor -public abstract class BasePageLink implements Serializable { - - private static final long serialVersionUID = -4189954843653250481L; - - @Getter protected final int limit; - - @Getter @Setter protected UUID idOffset; - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/PageData.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageData.java new file mode 100644 index 0000000000..a1f2765ac9 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageData.java @@ -0,0 +1,64 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.page; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.thingsboard.server.common.data.BaseData; + +import java.util.Collections; +import java.util.List; + +public class PageData { + + private final List data; + private final int totalPages; + private final long totalElements; + private final boolean hasNext; + + public PageData() { + this(Collections.emptyList(), 0, 0, false); + } + + @JsonCreator + public PageData(@JsonProperty("data") List data, + @JsonProperty("totalPages") int totalPages, + @JsonProperty("totalElements") long totalElements, + @JsonProperty("hasNext") boolean hasNext) { + this.data = data; + this.totalPages = totalPages; + this.totalElements = totalElements; + this.hasNext = hasNext; + } + + public List getData() { + return data; + } + + public int getTotalPages() { + return totalPages; + } + + public long getTotalElements() { + return totalElements; + } + + @JsonProperty("hasNext") + public boolean hasNext() { + return hasNext; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/PageDataIterable.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageDataIterable.java index f71b206b1f..75ecc2ba6d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/page/PageDataIterable.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageDataIterable.java @@ -19,11 +19,12 @@ import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; +import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.SearchTextBased; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.UUIDBased; -public class PageDataIterable> implements Iterable, Iterator { +public class PageDataIterable implements Iterable, Iterator { private final FetchFunction function; private final int fetchSize; @@ -31,7 +32,7 @@ public class PageDataIterable> im private List currentItems; private int currentIdx; private boolean hasNextPack; - private TextPageLink nextPackLink; + private PageLink nextPackLink; private boolean initialized; public PageDataIterable(FetchFunction function, int fetchSize) { @@ -48,7 +49,7 @@ public class PageDataIterable> im @Override public boolean hasNext() { if(!initialized){ - fetch(new TextPageLink(fetchSize)); + fetch(new PageLink(fetchSize)); initialized = true; } if(currentIdx == currentItems.size()){ @@ -59,12 +60,12 @@ public class PageDataIterable> im return currentIdx < currentItems.size(); } - private void fetch(TextPageLink link) { - TextPageData pageData = function.fetch(link); + private void fetch(PageLink link) { + PageData pageData = function.fetch(link); currentIdx = 0; currentItems = pageData.getData(); hasNextPack = pageData.hasNext(); - nextPackLink = pageData.getNextPageLink(); + nextPackLink = link.nextPageLink(); } @Override @@ -75,9 +76,9 @@ public class PageDataIterable> im return currentItems.get(currentIdx++); } - public static interface FetchFunction> { + public static interface FetchFunction { - TextPageData fetch(TextPageLink link); + PageData fetch(PageLink link); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/PageLink.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageLink.java new file mode 100644 index 0000000000..7926e9cec0 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageLink.java @@ -0,0 +1,60 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.page; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; + +@Data +public class PageLink { + + private final String textSearch; + private final int pageSize; + private final int page; + private final SortOrder sortOrder; + + public PageLink(PageLink pageLink) { + this.pageSize = pageLink.getPageSize(); + this.page = pageLink.getPage(); + this.textSearch = pageLink.getTextSearch(); + this.sortOrder = pageLink.getSortOrder(); + } + + public PageLink(int pageSize) { + this(pageSize, 0); + } + + public PageLink(int pageSize, int page) { + this(pageSize, page, null, null); + } + + public PageLink(int pageSize, int page, String textSearch) { + this(pageSize, page, textSearch, null); + } + + public PageLink(int pageSize, int page, String textSearch, SortOrder sortOrder) { + this.pageSize = pageSize; + this.page = page; + this.textSearch = textSearch; + this.sortOrder = sortOrder; + } + + @JsonIgnore + public PageLink nextPageLink() { + return new PageLink(this.pageSize, this.page+1, this.textSearch, this.sortOrder); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/SortOrder.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/SortOrder.java new file mode 100644 index 0000000000..86483128b7 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/SortOrder.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.page; + +import lombok.Data; + +@Data +public class SortOrder { + + private final String property; + private final Direction direction; + + public SortOrder(String property) { + this(property, Direction.ASC); + } + + public SortOrder(String property, Direction direction) { + this.property = property; + this.direction = direction; + } + + public static enum Direction { + ASC, DESC + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/TextPageData.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/TextPageData.java deleted file mode 100644 index 643f5e1781..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/page/TextPageData.java +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.common.data.page; - -import java.util.List; -import java.util.UUID; - -import org.thingsboard.server.common.data.SearchTextBased; -import org.thingsboard.server.common.data.id.UUIDBased; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -public class TextPageData> { - - private final List data; - private final TextPageLink nextPageLink; - private final boolean hasNext; - - public TextPageData(List data, TextPageLink pageLink) { - super(); - this.data = data; - int limit = pageLink.getLimit(); - if (data != null && data.size() == limit) { - int index = data.size()-1; - UUID idOffset = data.get(index).getId().getId(); - String textOffset = data.get(index).getSearchText(); - nextPageLink = new TextPageLink(limit, pageLink.getTextSearch(), idOffset, textOffset); - hasNext = true; - } else { - nextPageLink = null; - hasNext = false; - } - } - - @JsonCreator - public TextPageData(@JsonProperty("data") List data, - @JsonProperty("nextPageLink") TextPageLink nextPageLink, - @JsonProperty("hasNext") boolean hasNext) { - this.data = data; - this.nextPageLink = nextPageLink; - this.hasNext = hasNext; - } - - public List getData() { - return data; - } - - @JsonProperty("hasNext") - public boolean hasNext() { - return hasNext; - } - - public TextPageLink getNextPageLink() { - return nextPageLink; - } - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/TextPageLink.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/TextPageLink.java deleted file mode 100644 index 14855dbe87..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/page/TextPageLink.java +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.common.data.page; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Getter; -import lombok.ToString; - -import java.io.Serializable; -import java.util.Arrays; -import java.util.UUID; - -@ToString -public class TextPageLink extends BasePageLink implements Serializable { - - private static final long serialVersionUID = -4189954843653250480L; - - @Getter private final String textSearch; - @Getter private final String textSearchBound; - @Getter private final String textOffset; - - public TextPageLink(int limit) { - this(limit, null, null, null); - } - - public TextPageLink(int limit, String textSearch) { - this(limit, textSearch, null, null); - } - - public TextPageLink(int limit, String textSearch, UUID idOffset, String textOffset) { - super(limit, idOffset); - this.textSearch = textSearch != null ? textSearch.toLowerCase() : null; - this.textSearchBound = nextSequence(this.textSearch); - this.textOffset = textOffset != null ? textOffset.toLowerCase() : null; - } - - @JsonCreator - public TextPageLink(@JsonProperty("limit") int limit, - @JsonProperty("textSearch") String textSearch, - @JsonProperty("textSearchBound") String textSearchBound, - @JsonProperty("textOffset") String textOffset, - @JsonProperty("idOffset") UUID idOffset) { - super(limit, idOffset); - this.textSearch = textSearch; - this.textSearchBound = textSearchBound; - this.textOffset = textOffset; - this.idOffset = idOffset; - } - - private static String nextSequence(String input) { - if (input != null && input.length() > 0) { - char[] chars = input.toCharArray(); - int i = chars.length - 1; - while (i >= 0 && ++chars[i--] == Character.MIN_VALUE) ; - if (i == -1 && (chars.length == 0 || chars[0] == Character.MIN_VALUE)) { - char buf[] = Arrays.copyOf(input.toCharArray(), input.length() + 1); - buf[buf.length - 1] = Character.MIN_VALUE; - return new String(buf); - } - return new String(chars); - } else { - return null; - } - } - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/TimePageData.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/TimePageData.java deleted file mode 100644 index 64e3288dcf..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/page/TimePageData.java +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.common.data.page; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import org.thingsboard.server.common.data.BaseData; -import org.thingsboard.server.common.data.SearchTextBased; -import org.thingsboard.server.common.data.id.UUIDBased; - -import java.util.List; -import java.util.UUID; - -public class TimePageData> { - - private final List data; - private final TimePageLink nextPageLink; - private final boolean hasNext; - - public TimePageData(List data, TimePageLink pageLink) { - super(); - this.data = data; - int limit = pageLink.getLimit(); - if (data != null && data.size() == limit) { - int index = data.size() - 1; - UUID idOffset = data.get(index).getId().getId(); - nextPageLink = new TimePageLink(limit, pageLink.getStartTime(), pageLink.getEndTime(), pageLink.isAscOrder(), idOffset); - hasNext = true; - } else { - nextPageLink = null; - hasNext = false; - } - } - - @JsonCreator - public TimePageData(@JsonProperty("data") List data, - @JsonProperty("nextPageLink") TimePageLink nextPageLink, - @JsonProperty("hasNext") boolean hasNext) { - this.data = data; - this.nextPageLink = nextPageLink; - this.hasNext = hasNext; - } - - public List getData() { - return data; - } - - @JsonProperty("hasNext") - public boolean hasNext() { - return hasNext; - } - - public TimePageLink getNextPageLink() { - return nextPageLink; - } - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/TimePageLink.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/TimePageLink.java index 7c8552dec4..96c231f525 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/page/TimePageLink.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/TimePageLink.java @@ -15,49 +15,46 @@ */ package org.thingsboard.server.common.data.page; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Getter; -import lombok.ToString; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; -import java.io.Serializable; -import java.util.Arrays; -import java.util.UUID; +@Data +public class TimePageLink extends PageLink { -@ToString -public class TimePageLink extends BasePageLink implements Serializable { + private final Long startTime; + private final Long endTime; - private static final long serialVersionUID = -4189954843653250480L; - - @Getter private final Long startTime; - @Getter private final Long endTime; - @Getter private final boolean ascOrder; + public TimePageLink(PageLink pageLink, Long startTime, Long endTime) { + super(pageLink); + this.startTime = startTime; + this.endTime = endTime; + } - public TimePageLink(int limit) { - this(limit, null, null, false, null); + public TimePageLink(int pageSize) { + this(pageSize, 0); } - public TimePageLink(int limit, Long startTime) { - this(limit, startTime, null, false, null); + public TimePageLink(int pageSize, int page) { + this(pageSize, page, null); } - public TimePageLink(int limit, Long startTime, Long endTime) { - this(limit, startTime, endTime, false, null); + public TimePageLink(int pageSize, int page, String textSearch) { + this(pageSize, page, textSearch, null, null, null); } - public TimePageLink(int limit, Long startTime, Long endTime, boolean ascOrder) { - this(limit, startTime, endTime, ascOrder, null); + public TimePageLink(int pageSize, int page, String textSearch, SortOrder sortOrder) { + this(pageSize, page, textSearch, sortOrder, null, null); } - @JsonCreator - public TimePageLink(@JsonProperty("limit") int limit, - @JsonProperty("startTime") Long startTime, - @JsonProperty("endTime") Long endTime, - @JsonProperty("ascOrder") boolean ascOrder, - @JsonProperty("idOffset") UUID idOffset) { - super(limit, idOffset); + public TimePageLink(int pageSize, int page, String textSearch, SortOrder sortOrder, Long startTime, Long endTime) { + super(pageSize, page, textSearch, sortOrder); this.startTime = startTime; this.endTime = endTime; - this.ascOrder = ascOrder; + } + + @JsonIgnore + public TimePageLink nextPageLink() { + return new TimePageLink(this.getPageSize(), this.getPage()+1, this.getTextSearch(), this.getSortOrder(), + this.startTime, this.endTime); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentLifecycleEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentLifecycleEvent.java index 87adc31713..1f242a0f88 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentLifecycleEvent.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentLifecycleEvent.java @@ -21,5 +21,5 @@ import java.io.Serializable; * @author Andrew Shvayka */ public enum ComponentLifecycleEvent implements Serializable { - CREATED, STARTED, ACTIVATED, SUSPENDED, UPDATED, STOPPED, DELETED + CREATED, STARTED, ACTIVATED, SUSPENDED, UPDATED, STOPPED, DELETED, ADDED_TO_ALLOW_LIST, ADDED_TO_DENY_LIST } \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AbstractDataQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AbstractDataQuery.java new file mode 100644 index 0000000000..325b1ba11f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AbstractDataQuery.java @@ -0,0 +1,55 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import lombok.Getter; +import lombok.ToString; + +import java.util.List; + +@ToString +public abstract class AbstractDataQuery extends EntityCountQuery { + + @Getter + protected T pageLink; + @Getter + protected List entityFields; + @Getter + protected List latestValues; + @Getter + protected List keyFilters; + + public AbstractDataQuery() { + super(); + } + + public AbstractDataQuery(EntityFilter entityFilter) { + super(entityFilter); + } + + public AbstractDataQuery(EntityFilter entityFilter, + T pageLink, + List entityFields, + List latestValues, + List keyFilters) { + super(entityFilter); + this.pageLink = pageLink; + this.entityFields = entityFields; + this.latestValues = latestValues; + this.keyFilters = keyFilters; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmData.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmData.java new file mode 100644 index 0000000000..c47b7bce45 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmData.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import lombok.Getter; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.id.EntityId; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class AlarmData extends AlarmInfo { + + @Getter + private final EntityId entityId; + @Getter + private final Map> latest; + + public AlarmData(Alarm alarm, String originatorName, EntityId entityId) { + super(alarm, originatorName); + this.entityId = entityId; + this.latest = new HashMap<>(); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmDataPageLink.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmDataPageLink.java new file mode 100644 index 0000000000..589cb2df2f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmDataPageLink.java @@ -0,0 +1,67 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; +import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.AlarmStatus; + +import java.util.List; + +@Data +@AllArgsConstructor +public class AlarmDataPageLink extends EntityDataPageLink { + + private long startTs; + private long endTs; + //TODO: handle this; + private long timeWindow; + private List typeList; + private List statusList; + private List severityList; + private boolean searchPropagatedAlarms; + + public AlarmDataPageLink() { + super(); + } + + public AlarmDataPageLink(int pageSize, int page, String textSearch, EntityDataSortOrder sortOrder, boolean dynamic, + boolean searchPropagatedAlarms, + long startTs, long endTs, long timeWindow, + List typeList, List statusList, List severityList) { + super(pageSize, page, textSearch, sortOrder, dynamic); + this.searchPropagatedAlarms = searchPropagatedAlarms; + this.startTs = startTs; + this.endTs = endTs; + this.timeWindow = timeWindow; + this.typeList = typeList; + this.statusList = statusList; + this.severityList = severityList; + } + + @JsonIgnore + public AlarmDataPageLink nextPageLink() { + return new AlarmDataPageLink(this.getPageSize(), this.getPage() + 1, this.getTextSearch(), this.getSortOrder(), this.isDynamic(), + this.searchPropagatedAlarms, + this.startTs, this.endTs, this.timeWindow, + this.typeList, this.statusList, this.severityList + ); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmDataQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmDataQuery.java new file mode 100644 index 0000000000..ef314416a2 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmDataQuery.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.ToString; + +import java.util.List; + +@ToString +public class AlarmDataQuery extends AbstractDataQuery { + + @Getter + protected List alarmFields; + + public AlarmDataQuery() { + } + + public AlarmDataQuery(EntityFilter entityFilter) { + super(entityFilter); + } + + public AlarmDataQuery(EntityFilter entityFilter, AlarmDataPageLink pageLink, List entityFields, List latestValues, List keyFilters, List alarmFields) { + super(entityFilter, pageLink, entityFields, latestValues, keyFilters); + this.alarmFields = alarmFields; + } + + @JsonIgnore + public AlarmDataQuery next() { + return new AlarmDataQuery(getEntityFilter(), getPageLink().nextPageLink(), entityFields, latestValues, keyFilters, alarmFields); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/ApiUsageStateFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/ApiUsageStateFilter.java new file mode 100644 index 0000000000..436aa44230 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/ApiUsageStateFilter.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import lombok.Data; +import org.thingsboard.server.common.data.id.EntityId; + +@Data +public class ApiUsageStateFilter implements EntityFilter { + @Override + public EntityFilterType getType() { + return EntityFilterType.API_USAGE_STATE; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AssetSearchQueryFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AssetSearchQueryFilter.java new file mode 100644 index 0000000000..1660fa6fbd --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AssetSearchQueryFilter.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import lombok.Data; + +import java.util.List; + +@Data +public class AssetSearchQueryFilter extends EntitySearchQueryFilter { + + @Override + public EntityFilterType getType() { + return EntityFilterType.ASSET_SEARCH_QUERY; + } + + private List assetTypes; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AssetTypeFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AssetTypeFilter.java new file mode 100644 index 0000000000..ee29a85d8d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AssetTypeFilter.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import lombok.Data; + +@Data +public class AssetTypeFilter implements EntityFilter { + + @Override + public EntityFilterType getType() { + return EntityFilterType.ASSET_TYPE; + } + + private String assetType; + + private String assetNameFilter; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/BooleanFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/BooleanFilterPredicate.java new file mode 100644 index 0000000000..7fdea11d83 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/BooleanFilterPredicate.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import lombok.Data; + +@Data +public class BooleanFilterPredicate implements SimpleKeyFilterPredicate { + + private BooleanOperation operation; + private FilterPredicateValue value; + + @Override + public FilterPredicateType getType() { + return FilterPredicateType.BOOLEAN; + } + + public enum BooleanOperation { + EQUAL, + NOT_EQUAL + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/ComplexFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/ComplexFilterPredicate.java new file mode 100644 index 0000000000..e69b51d862 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/ComplexFilterPredicate.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import lombok.Data; + +import java.util.List; + +@Data +public class ComplexFilterPredicate implements KeyFilterPredicate { + + private ComplexOperation operation; + private List predicates; + + @Override + public FilterPredicateType getType() { + return FilterPredicateType.COMPLEX; + } + + public enum ComplexOperation { + AND, + OR + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/DeviceSearchQueryFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/DeviceSearchQueryFilter.java new file mode 100644 index 0000000000..1c58891479 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/DeviceSearchQueryFilter.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import lombok.Data; + +import java.util.List; + +@Data +public class DeviceSearchQueryFilter extends EntitySearchQueryFilter { + + @Override + public EntityFilterType getType() { + return EntityFilterType.DEVICE_SEARCH_QUERY; + } + + private List deviceTypes; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/DeviceTypeFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/DeviceTypeFilter.java new file mode 100644 index 0000000000..f626003cb8 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/DeviceTypeFilter.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import lombok.Data; + +@Data +public class DeviceTypeFilter implements EntityFilter { + + @Override + public EntityFilterType getType() { + return EntityFilterType.DEVICE_TYPE; + } + + private String deviceType; + + private String deviceNameFilter; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java new file mode 100644 index 0000000000..d80a679d30 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import lombok.Getter; + +@Data +public class DynamicValue { + + @JsonIgnore + private T resolvedValue; + + @Getter + private final DynamicValueSourceType sourceType; + @Getter + private final String sourceAttribute; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValueSourceType.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValueSourceType.java new file mode 100644 index 0000000000..7da9ab00af --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValueSourceType.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +public enum DynamicValueSourceType { + CURRENT_TENANT, + CURRENT_CUSTOMER, + CURRENT_USER, + CURRENT_DEVICE +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java new file mode 100644 index 0000000000..8a65bd1b58 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import lombok.Getter; + +public class EntityCountQuery { + + @Getter + private EntityFilter entityFilter; + + public EntityCountQuery() {} + + public EntityCountQuery(EntityFilter entityFilter) { + this.entityFilter = entityFilter; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityData.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityData.java new file mode 100644 index 0000000000..b0fded411d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityData.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import lombok.Data; +import org.thingsboard.server.common.data.id.EntityId; + +import java.util.Map; + +@Data +public class EntityData { + + private final EntityId entityId; + private final Map> latest; + private final Map timeseries; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityDataPageLink.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityDataPageLink.java new file mode 100644 index 0000000000..bdf459ce1f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityDataPageLink.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class EntityDataPageLink { + + private int pageSize; + private int page; + private String textSearch; + private EntityDataSortOrder sortOrder; + private boolean dynamic = false; + + public EntityDataPageLink() { + } + + public EntityDataPageLink(int pageSize, int page, String textSearch, EntityDataSortOrder sortOrder) { + this(pageSize, page, textSearch, sortOrder, false); + } + + @JsonIgnore + public EntityDataPageLink nextPageLink() { + return new EntityDataPageLink(this.pageSize, this.page + 1, this.textSearch, this.sortOrder); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityDataQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityDataQuery.java new file mode 100644 index 0000000000..41b1551454 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityDataQuery.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.ToString; + +import java.util.List; + +@ToString +public class EntityDataQuery extends AbstractDataQuery { + + public EntityDataQuery() { + } + + public EntityDataQuery(EntityFilter entityFilter) { + super(entityFilter); + } + + public EntityDataQuery(EntityFilter entityFilter, EntityDataPageLink pageLink, List entityFields, List latestValues, List keyFilters) { + super(entityFilter, pageLink, entityFields, latestValues, keyFilters); + } + + @JsonIgnore + public EntityDataQuery next() { + return new EntityDataQuery(getEntityFilter(), getPageLink().nextPageLink(), entityFields, latestValues, keyFilters); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityDataSortOrder.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityDataSortOrder.java new file mode 100644 index 0000000000..02fdcb2773 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityDataSortOrder.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import lombok.Data; + +@Data +public class EntityDataSortOrder { + + private EntityKey key; + private Direction direction; + + public EntityDataSortOrder() {} + + public EntityDataSortOrder(EntityKey key) { + this(key, Direction.ASC); + } + + public EntityDataSortOrder(EntityKey key, Direction direction) { + this.key = key; + this.direction = direction; + } + + public enum Direction { + ASC, DESC + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java new file mode 100644 index 0000000000..f2a6dde626 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = SingleEntityFilter.class, name = "singleEntity"), + @JsonSubTypes.Type(value = EntityListFilter.class, name = "entityList"), + @JsonSubTypes.Type(value = EntityNameFilter.class, name = "entityName"), + @JsonSubTypes.Type(value = AssetTypeFilter.class, name = "assetType"), + @JsonSubTypes.Type(value = DeviceTypeFilter.class, name = "deviceType"), + @JsonSubTypes.Type(value = EntityViewTypeFilter.class, name = "entityViewType"), + @JsonSubTypes.Type(value = ApiUsageStateFilter.class, name = "apiUsageState"), + @JsonSubTypes.Type(value = RelationsQueryFilter.class, name = "relationsQuery"), + @JsonSubTypes.Type(value = AssetSearchQueryFilter.class, name = "assetSearchQuery"), + @JsonSubTypes.Type(value = DeviceSearchQueryFilter.class, name = "deviceSearchQuery"), + @JsonSubTypes.Type(value = EntityViewSearchQueryFilter.class, name = "entityViewSearchQuery")}) +public interface EntityFilter { + + @JsonIgnore + EntityFilterType getType(); +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilterType.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilterType.java new file mode 100644 index 0000000000..12860b8453 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilterType.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +public enum EntityFilterType { + SINGLE_ENTITY("singleEntity"), + ENTITY_LIST("entityList"), + ENTITY_NAME("entityName"), + ASSET_TYPE("assetType"), + DEVICE_TYPE("deviceType"), + ENTITY_VIEW_TYPE("entityViewType"), + RELATIONS_QUERY("relationsQuery"), + ASSET_SEARCH_QUERY("assetSearchQuery"), + DEVICE_SEARCH_QUERY("deviceSearchQuery"), + ENTITY_VIEW_SEARCH_QUERY("entityViewSearchQuery"), + API_USAGE_STATE("apiUsageState"); + + private final String label; + + EntityFilterType(String label) { + this.label = label; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKey.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKey.java new file mode 100644 index 0000000000..233dcb4f5b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKey.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import lombok.Data; + +@Data +public class EntityKey { + private final EntityKeyType type; + private final String key; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKeyType.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKeyType.java new file mode 100644 index 0000000000..9087927f8c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKeyType.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +public enum EntityKeyType { + ATTRIBUTE, + CLIENT_ATTRIBUTE, + SHARED_ATTRIBUTE, + SERVER_ATTRIBUTE, + TIME_SERIES, + ENTITY_FIELD, + ALARM_FIELD; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKeyValueType.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKeyValueType.java new file mode 100644 index 0000000000..0239d42451 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKeyValueType.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +public enum EntityKeyValueType { + STRING, + NUMERIC, + BOOLEAN, + DATE_TIME +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityListFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityListFilter.java new file mode 100644 index 0000000000..9eb80484e5 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityListFilter.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import lombok.Data; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.EntityId; + +import java.util.List; + +@Data +public class EntityListFilter implements EntityFilter { + @Override + public EntityFilterType getType() { + return EntityFilterType.ENTITY_LIST; + } + + private EntityType entityType; + + private List entityList; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityNameFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityNameFilter.java new file mode 100644 index 0000000000..414e32975c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityNameFilter.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import lombok.Data; +import org.thingsboard.server.common.data.EntityType; + +@Data +public class EntityNameFilter implements EntityFilter { + @Override + public EntityFilterType getType() { + return EntityFilterType.ENTITY_NAME; + } + + private EntityType entityType; + + private String entityNameFilter; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntitySearchQueryFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntitySearchQueryFilter.java new file mode 100644 index 0000000000..36e2d2e598 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntitySearchQueryFilter.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import lombok.Data; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; + +@Data +public abstract class EntitySearchQueryFilter implements EntityFilter { + + private EntityId rootEntity; + private String relationType; + private EntitySearchDirection direction; + private int maxLevel; + private boolean fetchLastLevelOnly; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityViewSearchQueryFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityViewSearchQueryFilter.java new file mode 100644 index 0000000000..455cc43206 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityViewSearchQueryFilter.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import lombok.Data; + +import java.util.List; + +@Data +public class EntityViewSearchQueryFilter extends EntitySearchQueryFilter { + + @Override + public EntityFilterType getType() { + return EntityFilterType.ENTITY_VIEW_SEARCH_QUERY; + } + + private List entityViewTypes; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityViewTypeFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityViewTypeFilter.java new file mode 100644 index 0000000000..6a4f6af2ae --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityViewTypeFilter.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import lombok.Data; + +@Data +public class EntityViewTypeFilter implements EntityFilter { + + @Override + public EntityFilterType getType() { + return EntityFilterType.ENTITY_VIEW_TYPE; + } + + private String entityViewType; + + private String entityViewNameFilter; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/FilterPredicateType.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/FilterPredicateType.java new file mode 100644 index 0000000000..afa3bcc681 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/FilterPredicateType.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +public enum FilterPredicateType { + STRING, + NUMERIC, + BOOLEAN, + COMPLEX +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/FilterPredicateValue.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/FilterPredicateValue.java new file mode 100644 index 0000000000..8897d35586 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/FilterPredicateValue.java @@ -0,0 +1,71 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.Getter; + +@Data +public class FilterPredicateValue { + + @Getter + private final T defaultValue; + @Getter + private final T userValue; + @Getter + private final DynamicValue dynamicValue; + + public FilterPredicateValue(T defaultValue) { + this(defaultValue, null, null); + } + + @JsonCreator + public FilterPredicateValue(@JsonProperty("defaultValue") T defaultValue, + @JsonProperty("userValue") T userValue, + @JsonProperty("dynamicValue") DynamicValue dynamicValue) { + this.defaultValue = defaultValue; + this.userValue = userValue; + this.dynamicValue = dynamicValue; + } + + @JsonIgnore + public T getValue() { + if (this.userValue != null) { + return this.userValue; + } else { + if (this.dynamicValue != null && this.dynamicValue.getResolvedValue() != null) { + return this.dynamicValue.getResolvedValue(); + } else { + return defaultValue; + } + } + } + + public static FilterPredicateValue fromDouble(double value) { + return new FilterPredicateValue<>(value); + } + + public static FilterPredicateValue fromString(String value) { + return new FilterPredicateValue<>(value); + } + + public static FilterPredicateValue fromBoolean(boolean value) { + return new FilterPredicateValue<>(value); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilter.java new file mode 100644 index 0000000000..6ab5ce7736 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilter.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import lombok.Data; + +@Data +public class KeyFilter { + + private EntityKey key; + private EntityKeyValueType valueType; + private KeyFilterPredicate predicate; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilterPredicate.java new file mode 100644 index 0000000000..81e0af6271 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilterPredicate.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = StringFilterPredicate.class, name = "STRING"), + @JsonSubTypes.Type(value = NumericFilterPredicate.class, name = "NUMERIC"), + @JsonSubTypes.Type(value = BooleanFilterPredicate.class, name = "BOOLEAN"), + @JsonSubTypes.Type(value = ComplexFilterPredicate.class, name = "COMPLEX")}) +public interface KeyFilterPredicate { + + @JsonIgnore + FilterPredicateType getType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/NumericFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/NumericFilterPredicate.java new file mode 100644 index 0000000000..22ca8b85a4 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/NumericFilterPredicate.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import lombok.Data; + +@Data +public class NumericFilterPredicate implements SimpleKeyFilterPredicate { + + private NumericOperation operation; + private FilterPredicateValue value; + + @Override + public FilterPredicateType getType() { + return FilterPredicateType.NUMERIC; + } + + public enum NumericOperation { + EQUAL, + NOT_EQUAL, + GREATER, + LESS, + GREATER_OR_EQUAL, + LESS_OR_EQUAL + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java new file mode 100644 index 0000000000..2cc43335c2 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import lombok.Data; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.EntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; + +import java.util.List; + +@Data +public class RelationsQueryFilter implements EntityFilter { + + @Override + public EntityFilterType getType() { + return EntityFilterType.RELATIONS_QUERY; + } + + private EntityId rootEntity; + private EntitySearchDirection direction; + private List filters; + private int maxLevel; + private boolean fetchLastLevelOnly; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/SimpleKeyFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/SimpleKeyFilterPredicate.java new file mode 100644 index 0000000000..0d796b90d5 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/SimpleKeyFilterPredicate.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +public interface SimpleKeyFilterPredicate extends KeyFilterPredicate { + + FilterPredicateValue getValue(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/SingleEntityFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/SingleEntityFilter.java new file mode 100644 index 0000000000..18d765e3e6 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/SingleEntityFilter.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import lombok.Data; +import org.thingsboard.server.common.data.id.EntityId; + +@Data +public class SingleEntityFilter implements EntityFilter { + @Override + public EntityFilterType getType() { + return EntityFilterType.SINGLE_ENTITY; + } + + private EntityId singleEntity; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/StringFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/StringFilterPredicate.java new file mode 100644 index 0000000000..77459529d0 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/StringFilterPredicate.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import lombok.Data; + +@Data +public class StringFilterPredicate implements SimpleKeyFilterPredicate { + + private StringOperation operation; + private FilterPredicateValue value; + private boolean ignoreCase; + + @Override + public FilterPredicateType getType() { + return FilterPredicateType.STRING; + } + + public enum StringOperation { + EQUAL, + NOT_EQUAL, + STARTS_WITH, + ENDS_WITH, + CONTAINS, + NOT_CONTAINS + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/TsValue.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/TsValue.java new file mode 100644 index 0000000000..b1b31f05b6 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/TsValue.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import lombok.Data; + +@Data +public class TsValue { + + private final long ts; + private final String value; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/DefaultRuleChainCreateRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/DefaultRuleChainCreateRequest.java new file mode 100644 index 0000000000..0a921c6526 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/DefaultRuleChainCreateRequest.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.rule; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.io.Serializable; + +@Data +@Slf4j +public class DefaultRuleChainCreateRequest implements Serializable { + + private static final long serialVersionUID = 5600333716030561537L; + + private String name; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainData.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainData.java new file mode 100644 index 0000000000..e4b9ec3442 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainData.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.rule; + +import lombok.Data; + +import java.util.List; + +@Data +public class RuleChainData { + + List ruleChains; + List metadata; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainImportResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainImportResult.java new file mode 100644 index 0000000000..53b899e04b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainImportResult.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.rule; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; + +@Data +@AllArgsConstructor +public class RuleChainImportResult { + + private TenantId tenantId; + private RuleChainId ruleChainId; + private ComponentLifecycleEvent lifecycleEvent; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNodeState.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNodeState.java new file mode 100644 index 0000000000..a3432760be --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNodeState.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.rule; + +import lombok.Data; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.id.RuleNodeStateId; + +@Data +public class RuleNodeState extends BaseData { + + private RuleNodeId ruleNodeId; + private EntityId entityId; + private String stateData; + + public RuleNodeState() { + super(); + } + + public RuleNodeState(RuleNodeStateId id) { + super(id); + } + + public RuleNodeState(RuleNodeState event) { + super(event); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsType.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsType.java index 8d647dcb1a..e7faf1b5ce 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsType.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.security; public enum DeviceCredentialsType { ACCESS_TOKEN, - X509_CERTIFICATE + X509_CERTIFICATE, + MQTT_BASIC } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java new file mode 100644 index 0000000000..250f9c75a5 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -0,0 +1,75 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.tenant.profile; + +import lombok.Data; +import org.thingsboard.server.common.data.ApiUsageRecordKey; +import org.thingsboard.server.common.data.TenantProfileType; + +@Data +public class DefaultTenantProfileConfiguration implements TenantProfileConfiguration { + + private long maxDevices; + private long maxAssets; + + private String transportTenantMsgRateLimit; + private String transportTenantTelemetryMsgRateLimit; + private String transportTenantTelemetryDataPointsRateLimit; + private String transportDeviceMsgRateLimit; + private String transportDeviceTelemetryMsgRateLimit; + private String transportDeviceTelemetryDataPointsRateLimit; + + private long maxTransportMessages; + private long maxTransportDataPoints; + private long maxREExecutions; + private long maxJSExecutions; + private long maxDPStorageDays; + private int maxRuleNodeExecutionsPerMessage; + + private double warnThreshold; + + @Override + public long getProfileThreshold(ApiUsageRecordKey key) { + switch (key) { + case TRANSPORT_MSG_COUNT: + return maxTransportMessages; + case TRANSPORT_DP_COUNT: + return maxTransportDataPoints; + case JS_EXEC_COUNT: + return maxJSExecutions; + case RE_EXEC_COUNT: + return maxREExecutions; + case STORAGE_DP_COUNT: + return maxDPStorageDays; + } + return 0L; + } + + @Override + public long getWarnThreshold(ApiUsageRecordKey key) { + return (long) (getProfileThreshold(key) * (warnThreshold > 0.0 ? warnThreshold : 0.8)); + } + + @Override + public TenantProfileType getType() { + return TenantProfileType.DEFAULT; + } + + @Override + public int getMaxRuleNodeExecsPerMessage() { + return maxRuleNodeExecutionsPerMessage; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileConfiguration.java new file mode 100644 index 0000000000..ea4a0e1fbe --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileConfiguration.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.tenant.profile; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.thingsboard.server.common.data.ApiUsageRecordKey; +import org.thingsboard.server.common.data.TenantProfileType; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = DefaultTenantProfileConfiguration.class, name = "DEFAULT")}) +public interface TenantProfileConfiguration { + + @JsonIgnore + TenantProfileType getType(); + + @JsonIgnore + long getProfileThreshold(ApiUsageRecordKey key); + + @JsonIgnore + long getWarnThreshold(ApiUsageRecordKey key); + + @JsonIgnore + int getMaxRuleNodeExecsPerMessage(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileData.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileData.java new file mode 100644 index 0000000000..297e562d26 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileData.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.tenant.profile; + +import lombok.Data; + +@Data +public class TenantProfileData { + + private TenantProfileConfiguration configuration; + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/UUIDConverterTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/UUIDConverterTest.java index 193687c240..b2ff5f9010 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/UUIDConverterTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/UUIDConverterTest.java @@ -15,14 +15,17 @@ */ package org.thingsboard.server.common.data; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.runners.MockitoJUnitRunner; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Random; import java.util.UUID; +import java.util.stream.Collectors; /** * Created by ashvayka on 14.07.17. @@ -37,6 +40,18 @@ public class UUIDConverterTest { Assert.assertEquals("1d8eebc58e0a7d796690800200c9a66", result); } + + @Test + public void basicUuid() { + System.out.println(UUIDConverter.fromString("1e746126eaaefa6a91992ebcb67fe33")); + } + + @Test + public void basicUuidConversion() { + UUID original = UUID.fromString("3dd11790-abf2-11ea-b151-83a091b9d4cc"); + Assert.assertEquals(Uuids.unixTimestamp(original), 1591886749577L); + } + @Test public void basicStringToUUIDTest() { UUID result = UUIDConverter.fromString("1d8eebc58e0a7d796690800200c9a66"); @@ -61,8 +76,8 @@ public class UUIDConverterTest { before = tmp; } - String beforeStr = UUIDConverter.fromTimeUUID(UUIDs.startOf(before)); - String afterStr = UUIDConverter.fromTimeUUID(UUIDs.startOf(after)); + String beforeStr = UUIDConverter.fromTimeUUID(Uuids.startOf(before)); + String afterStr = UUIDConverter.fromTimeUUID(Uuids.startOf(after)); if (afterStr.compareTo(beforeStr) < 0) { System.out.println("Before: " + before + " | " + beforeStr); diff --git a/common/message/pom.xml b/common/message/pom.xml index 62802d141c..17a0679630 100644 --- a/common/message/pom.xml +++ b/common/message/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT common org.thingsboard.common @@ -87,6 +87,25 @@ org.xolstice.maven.plugins protobuf-maven-plugin + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + false + + diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java b/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java index a309c5b830..f60d18fbd4 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.msg; import lombok.extern.slf4j.Slf4j; import org.bouncycastle.crypto.digests.SHA3Digest; import org.bouncycastle.pqc.math.linearalgebra.ByteUtils; + /** * @author Valerii Sosliuk */ @@ -30,8 +31,8 @@ public class EncryptionUtil { public static String trimNewLines(String input) { return input.replaceAll("-----BEGIN CERTIFICATE-----", "") .replaceAll("-----END CERTIFICATE-----", "") - .replaceAll("\n","") - .replaceAll("\r",""); + .replaceAll("\n", "") + .replaceAll("\r", ""); } public static String getSha3Hash(String data) { @@ -45,4 +46,20 @@ public class EncryptionUtil { String sha3Hash = ByteUtils.toHexString(hashedBytes); return sha3Hash; } + + public static String getSha3Hash(String delim, String... tokens) { + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (String token : tokens) { + if (token != null && !token.isEmpty()) { + if (first) { + first = false; + } else { + sb.append(delim); + } + sb.append(token); + } + } + return getSha3Hash(sb.toString()); + } } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java index 43c3294e5c..0b257d9d6e 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java @@ -15,10 +15,13 @@ */ package org.thingsboard.server.common.msg; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; +import lombok.AccessLevel; import lombok.Builder; import lombok.Data; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -28,15 +31,14 @@ import org.thingsboard.server.common.msg.gen.MsgProtos; import org.thingsboard.server.common.msg.queue.ServiceQueue; import org.thingsboard.server.common.msg.queue.TbMsgCallback; -import java.io.IOException; import java.io.Serializable; import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; /** * Created by ashvayka on 13.01.18. */ @Data -@Builder @Slf4j public final class TbMsg implements Serializable { @@ -50,45 +52,73 @@ public final class TbMsg implements Serializable { private final String data; private final RuleChainId ruleChainId; private final RuleNodeId ruleNodeId; + @Getter(value = AccessLevel.NONE) + private final AtomicInteger ruleNodeExecCounter; + + public int getAndIncrementRuleNodeCounter() { + return ruleNodeExecCounter.getAndIncrement(); + } + //This field is not serialized because we use queues and there is no need to do it + @JsonIgnore transient private final TbMsgCallback callback; public static TbMsg newMsg(String queueName, String type, EntityId originator, TbMsgMetaData metaData, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { - return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type, originator, metaData.copy(), TbMsgDataType.JSON, data, ruleChainId, ruleNodeId, TbMsgCallback.EMPTY); + return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type, originator, + metaData.copy(), TbMsgDataType.JSON, data, ruleChainId, ruleNodeId, 0, TbMsgCallback.EMPTY); } public static TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, String data) { - return new TbMsg(ServiceQueue.MAIN, UUID.randomUUID(), System.currentTimeMillis(), type, originator, metaData.copy(), TbMsgDataType.JSON, data, null, null, TbMsgCallback.EMPTY); + return new TbMsg(ServiceQueue.MAIN, UUID.randomUUID(), System.currentTimeMillis(), type, originator, metaData.copy(), TbMsgDataType.JSON, data, null, null, 0, TbMsgCallback.EMPTY); } + // REALLY NEW MSG + public static TbMsg newMsg(String queueName, String type, EntityId originator, TbMsgMetaData metaData, String data) { - return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type, originator, metaData.copy(), TbMsgDataType.JSON, data, null, null, TbMsgCallback.EMPTY); + return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type, originator, metaData.copy(), TbMsgDataType.JSON, data, null, null, 0, TbMsgCallback.EMPTY); } public static TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, TbMsgDataType dataType, String data) { - return new TbMsg(ServiceQueue.MAIN, UUID.randomUUID(), System.currentTimeMillis(), type, originator, metaData.copy(), dataType, data, null, null, TbMsgCallback.EMPTY); + return new TbMsg(ServiceQueue.MAIN, UUID.randomUUID(), System.currentTimeMillis(), type, originator, metaData.copy(), dataType, data, null, null, 0, TbMsgCallback.EMPTY); } + // For Tests only + public static TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, TbMsgDataType dataType, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { - return new TbMsg(ServiceQueue.MAIN, UUID.randomUUID(), System.currentTimeMillis(), type, originator, metaData.copy(), dataType, data, ruleChainId, ruleNodeId, TbMsgCallback.EMPTY); + return new TbMsg(ServiceQueue.MAIN, UUID.randomUUID(), System.currentTimeMillis(), type, originator, metaData.copy(), dataType, data, ruleChainId, ruleNodeId, 0, TbMsgCallback.EMPTY); } public static TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, String data, TbMsgCallback callback) { - return new TbMsg(ServiceQueue.MAIN, UUID.randomUUID(), System.currentTimeMillis(), type, originator, metaData.copy(), TbMsgDataType.JSON, data, null, null, callback); + return new TbMsg(ServiceQueue.MAIN, UUID.randomUUID(), System.currentTimeMillis(), type, originator, metaData.copy(), TbMsgDataType.JSON, data, null, null, 0, callback); + } + + public static TbMsg transformMsg(TbMsg tbMsg, String type, EntityId originator, TbMsgMetaData metaData, String data) { + return new TbMsg(tbMsg.getQueueName(), tbMsg.getId(), tbMsg.getTs(), type, originator, metaData.copy(), tbMsg.getDataType(), + data, tbMsg.getRuleChainId(), tbMsg.getRuleNodeId(), tbMsg.ruleNodeExecCounter.get(), tbMsg.getCallback()); + } + + public static TbMsg transformMsg(TbMsg tbMsg, RuleChainId ruleChainId) { + return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.metaData, tbMsg.dataType, + tbMsg.data, ruleChainId, null, tbMsg.ruleNodeExecCounter.get(), tbMsg.getCallback()); + } + + public static TbMsg transformMsg(TbMsg tbMsg, String queueName) { + return new TbMsg(queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.metaData, tbMsg.dataType, + tbMsg.data, tbMsg.getRuleChainId(), null, tbMsg.ruleNodeExecCounter.get(), tbMsg.getCallback()); } - public static TbMsg transformMsg(TbMsg origMsg, String type, EntityId originator, TbMsgMetaData metaData, String data) { - return new TbMsg(origMsg.getQueueName(), origMsg.getId(), origMsg.getTs(), type, originator, metaData.copy(), origMsg.getDataType(), - data, origMsg.getRuleChainId(), origMsg.getRuleNodeId(), origMsg.getCallback()); + public static TbMsg transformMsg(TbMsg tbMsg, RuleChainId ruleChainId, String queueName) { + return new TbMsg(queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.metaData, tbMsg.dataType, + tbMsg.data, ruleChainId, null, tbMsg.ruleNodeExecCounter.get(), tbMsg.getCallback()); } public static TbMsg newMsg(TbMsg tbMsg, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { return new TbMsg(tbMsg.getQueueName(), UUID.randomUUID(), tbMsg.getTs(), tbMsg.getType(), tbMsg.getOriginator(), tbMsg.getMetaData().copy(), - tbMsg.getDataType(), tbMsg.getData(), ruleChainId, ruleNodeId, TbMsgCallback.EMPTY); + tbMsg.getDataType(), tbMsg.getData(), ruleChainId, ruleNodeId, tbMsg.ruleNodeExecCounter.get(), TbMsgCallback.EMPTY); } private TbMsg(String queueName, UUID id, long ts, String type, EntityId originator, TbMsgMetaData metaData, TbMsgDataType dataType, String data, - RuleChainId ruleChainId, RuleNodeId ruleNodeId, TbMsgCallback callback) { + RuleChainId ruleChainId, RuleNodeId ruleNodeId, int ruleNodeExecCounter, TbMsgCallback callback) { this.id = id; this.queueName = queueName; if (ts > 0) { @@ -103,6 +133,7 @@ public final class TbMsg implements Serializable { this.data = data; this.ruleChainId = ruleChainId; this.ruleNodeId = ruleNodeId; + this.ruleNodeExecCounter = new AtomicInteger(ruleNodeExecCounter); if (callback != null) { this.callback = callback; } else { @@ -139,6 +170,7 @@ public final class TbMsg implements Serializable { builder.setDataType(msg.getDataType().ordinal()); builder.setData(msg.getData()); + builder.setRuleNodeExecCounter(msg.ruleNodeExecCounter.get()); return builder.build().toByteArray(); } @@ -156,18 +188,18 @@ public final class TbMsg implements Serializable { ruleNodeId = new RuleNodeId(new UUID(proto.getRuleNodeIdMSB(), proto.getRuleNodeIdLSB())); } TbMsgDataType dataType = TbMsgDataType.values()[proto.getDataType()]; - return new TbMsg(queueName, UUID.fromString(proto.getId()), proto.getTs(), proto.getType(), entityId, metaData, dataType, proto.getData(), ruleChainId, ruleNodeId, callback); + return new TbMsg(queueName, UUID.fromString(proto.getId()), proto.getTs(), proto.getType(), entityId, metaData, dataType, proto.getData(), ruleChainId, ruleNodeId, proto.getRuleNodeExecCounter(), callback); } catch (InvalidProtocolBufferException e) { throw new IllegalStateException("Could not parse protobuf for TbMsg", e); } } public TbMsg copyWithRuleChainId(RuleChainId ruleChainId) { - return new TbMsg(this.queueName, this.id, this.ts, this.type, this.originator, this.metaData, this.dataType, this.data, ruleChainId, null, callback); + return new TbMsg(this.queueName, this.id, this.ts, this.type, this.originator, this.metaData, this.dataType, this.data, ruleChainId, null, this.ruleNodeExecCounter.get(), callback); } public TbMsg copyWithRuleNodeId(RuleChainId ruleChainId, RuleNodeId ruleNodeId) { - return new TbMsg(this.queueName, this.id, this.ts, this.type, this.originator, this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, callback); + return new TbMsg(this.queueName, this.id, this.ts, this.type, this.originator, this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.ruleNodeExecCounter.get(), callback); } public TbMsgCallback getCallback() { diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java index 10b198ec28..f815dd6192 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java @@ -20,6 +20,7 @@ import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -31,6 +32,8 @@ import java.util.concurrent.ConcurrentHashMap; @NoArgsConstructor public final class TbMsgMetaData implements Serializable { + public static final TbMsgMetaData EMPTY = new TbMsgMetaData(Collections.emptyMap()); + private final Map data = new ConcurrentHashMap<>(); public TbMsgMetaData(Map data) { diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java index bf0fffe404..c2dec02612 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java @@ -50,6 +50,10 @@ public class TopicPartitionInfo { this.fullTopicName = tmp; } + public TopicPartitionInfo newByTopic(String topic) { + return new TopicPartitionInfo(topic, this.tenantId, this.partition, this.myPartition); + } + public String getTopic() { return topic; } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/FeatureType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/FeatureType.java index 9f421c0e73..58289cc94b 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/session/FeatureType.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/FeatureType.java @@ -16,5 +16,5 @@ package org.thingsboard.server.common.msg.session; public enum FeatureType { - ATTRIBUTES, TELEMETRY, RPC, CLAIM + ATTRIBUTES, TELEMETRY, RPC, CLAIM, PROVISION } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionContext.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionContext.java index b27b957500..d124fd48b0 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionContext.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionContext.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.msg.session; +import org.thingsboard.server.common.data.DeviceProfile; + import java.util.UUID; public interface SessionContext { @@ -22,4 +24,6 @@ public interface SessionContext { UUID getSessionId(); int nextMsgId(); + + void onProfileUpdate(DeviceProfile deviceProfile); } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/SchedulerUtils.java b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/SchedulerUtils.java new file mode 100644 index 0000000000..421066b30b --- /dev/null +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/SchedulerUtils.java @@ -0,0 +1,78 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.msg.tools; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAdjuster; +import java.time.temporal.TemporalAdjusters; +import java.time.temporal.TemporalUnit; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import static java.time.temporal.ChronoField.DAY_OF_MONTH; +import static java.time.temporal.ChronoUnit.MONTHS; + +public class SchedulerUtils { + + private final static ZoneId UTC = ZoneId.of("UTC"); + private static final ConcurrentMap tzMap = new ConcurrentHashMap<>(); + + public static ZoneId getZoneId(String tz) { + return tzMap.computeIfAbsent(tz == null || tz.isEmpty() ? "UTC" : tz, ZoneId::of); + } + + public static long getStartOfCurrentHour() { + return getStartOfCurrentHour(UTC); + } + + public static long getStartOfCurrentHour(ZoneId zoneId) { + return LocalDateTime.now(ZoneOffset.UTC).atZone(zoneId).truncatedTo(ChronoUnit.HOURS).toInstant().toEpochMilli(); + } + + public static long getStartOfCurrentMonth() { + return getStartOfCurrentMonth(UTC); + } + + public static long getStartOfCurrentMonth(ZoneId zoneId) { + return LocalDate.now().withDayOfMonth(1).atStartOfDay(zoneId).toInstant().toEpochMilli(); + } + + public static long getStartOfNextMonth() { + return getStartOfNextMonth(UTC); + } + + public static long getStartOfNextMonth(ZoneId zoneId) { + return LocalDate.now().with(TemporalAdjusters.firstDayOfNextMonth()).atStartOfDay(zoneId).toInstant().toEpochMilli(); + } + + public static long getStartOfNextNextMonth() { + return getStartOfNextNextMonth(UTC); + } + + public static long getStartOfNextNextMonth(ZoneId zoneId) { + return LocalDate.now().with(firstDayOfNextNextMonth()).atStartOfDay(zoneId).toInstant().toEpochMilli(); + } + + public static TemporalAdjuster firstDayOfNextNextMonth() { + return (temporal) -> temporal.with(DAY_OF_MONTH, 1).plus(2, MONTHS); + } + +} diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimits.java b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimits.java index 3c79895c8b..3550bc44a9 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimits.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimits.java @@ -50,4 +50,8 @@ public class TbRateLimits { return bucket.tryConsume(1); } + public boolean tryConsume(long number) { + return bucket.tryConsume(number); + } + } diff --git a/common/message/src/main/proto/tbmsg.proto b/common/message/src/main/proto/tbmsg.proto index 25e006121f..53cb90014b 100644 --- a/common/message/src/main/proto/tbmsg.proto +++ b/common/message/src/main/proto/tbmsg.proto @@ -45,4 +45,5 @@ message TbMsgProto { string data = 14; int64 ts = 15; + int32 ruleNodeExecCounter = 16; } \ No newline at end of file diff --git a/common/pom.xml b/common/pom.xml index e0a3865e08..52685d3891 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT thingsboard common diff --git a/common/queue/pom.xml b/common/queue/pom.xml index b302313baf..aaf46710f7 100644 --- a/common/queue/pom.xml +++ b/common/queue/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT common org.thingsboard.common diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/MultipleTbQueueCallbackWrapper.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/MultipleTbQueueCallbackWrapper.java new file mode 100644 index 0000000000..c28f3637b7 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/MultipleTbQueueCallbackWrapper.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.queue.common; + +import org.thingsboard.server.common.msg.queue.RuleEngineException; +import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueMsgMetadata; + +import java.util.concurrent.atomic.AtomicInteger; + +public class MultipleTbQueueCallbackWrapper implements TbQueueCallback { + + private final AtomicInteger tbQueueCallbackCount; + private final TbQueueCallback callback; + + public MultipleTbQueueCallbackWrapper(int tbQueueCallbackCount, TbQueueCallback callback) { + this.tbQueueCallbackCount = new AtomicInteger(tbQueueCallbackCount); + this.callback = callback; + } + + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + if (tbQueueCallbackCount.decrementAndGet() <= 0) { + callback.onSuccess(metadata); + } + } + + @Override + public void onFailure(Throwable t) { + callback.onFailure(new RuleEngineException(t.getMessage())); + } +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DummyDiscoveryService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DummyDiscoveryService.java index 9017823efb..6a54feedd5 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DummyDiscoveryService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DummyDiscoveryService.java @@ -20,6 +20,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.annotation.DependsOn; import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; import org.springframework.stereotype.Service; import java.util.Collections; @@ -40,6 +41,7 @@ public class DummyDiscoveryService implements DiscoveryService { } @EventListener(ApplicationReadyEvent.class) + @Order(value = 1) public void onApplicationEvent(ApplicationReadyEvent event) { partitionService.recalculatePartitions(serviceInfoProvider.getServiceInfo(), Collections.emptyList()); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java index e761a229c4..684c33f5a7 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java @@ -34,6 +34,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; import org.springframework.stereotype.Service; import org.springframework.util.Assert; import org.thingsboard.common.util.ThingsBoardThreadFactory; @@ -112,6 +113,7 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi } @EventListener(ApplicationReadyEvent.class) + @Order(value = 1) public void onApplicationEvent(ApplicationReadyEvent event) { if (stopped) { log.debug("Ignoring application ready event. Service is stopped."); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java index c1d724c207..0299720005 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java @@ -22,7 +22,7 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos.RemoteJsRequest; import org.thingsboard.server.gen.js.JsInvokeProtos.RemoteJsResponse; -import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.*; import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueProducer; @@ -91,64 +91,64 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng } @Override - public TbQueueProducer> createTransportNotificationsMsgProducer() { + public TbQueueProducer> createTransportNotificationsMsgProducer() { return new TbAwsSqsProducerTemplate<>(notificationAdmin, sqsSettings, transportNotificationSettings.getNotificationsTopic()); } @Override - public TbQueueProducer> createRuleEngineMsgProducer() { + public TbQueueProducer> createRuleEngineMsgProducer() { return new TbAwsSqsProducerTemplate<>(ruleEngineAdmin, sqsSettings, ruleEngineSettings.getTopic()); } @Override - public TbQueueProducer> createRuleEngineNotificationsMsgProducer() { + public TbQueueProducer> createRuleEngineNotificationsMsgProducer() { return new TbAwsSqsProducerTemplate<>(ruleEngineAdmin, sqsSettings, ruleEngineSettings.getTopic()); } @Override - public TbQueueProducer> createTbCoreMsgProducer() { + public TbQueueProducer> createTbCoreMsgProducer() { return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, coreSettings.getTopic()); } @Override - public TbQueueProducer> createTbCoreNotificationsMsgProducer() { + public TbQueueProducer> createTbCoreNotificationsMsgProducer() { return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, coreSettings.getTopic()); } @Override - public TbQueueConsumer> createToRuleEngineMsgConsumer(TbRuleEngineQueueConfiguration configuration) { + public TbQueueConsumer> createToRuleEngineMsgConsumer(TbRuleEngineQueueConfiguration configuration) { return new TbAwsSqsConsumerTemplate<>(ruleEngineAdmin, sqsSettings, ruleEngineSettings.getTopic(), - msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToRuleEngineMsg.parseFrom(msg.getData()), msg.getHeaders())); + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override - public TbQueueConsumer> createToRuleEngineNotificationsMsgConsumer() { + public TbQueueConsumer> createToRuleEngineNotificationsMsgConsumer() { return new TbAwsSqsConsumerTemplate<>(notificationAdmin, sqsSettings, partitionService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceInfoProvider.getServiceId()).getFullTopicName(), - msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToRuleEngineNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override - public TbQueueConsumer> createToCoreMsgConsumer() { + public TbQueueConsumer> createToCoreMsgConsumer() { return new TbAwsSqsConsumerTemplate<>(coreAdmin, sqsSettings, coreSettings.getTopic(), - msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToCoreMsg.parseFrom(msg.getData()), msg.getHeaders())); + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override - public TbQueueConsumer> createToCoreNotificationsMsgConsumer() { + public TbQueueConsumer> createToCoreNotificationsMsgConsumer() { return new TbAwsSqsConsumerTemplate<>(notificationAdmin, sqsSettings, partitionService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName(), - msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToCoreNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override - public TbQueueConsumer> createTransportApiRequestConsumer() { + public TbQueueConsumer> createTransportApiRequestConsumer() { return new TbAwsSqsConsumerTemplate<>(transportApiAdmin, sqsSettings, transportApiSettings.getRequestsTopic(), - msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.TransportApiRequestMsg.parseFrom(msg.getData()), msg.getHeaders())); + msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportApiRequestMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override - public TbQueueProducer> createTransportApiResponseProducer() { + public TbQueueProducer> createTransportApiResponseProducer() { return new TbAwsSqsProducerTemplate<>(transportApiAdmin, sqsSettings, transportApiSettings.getResponsesTopic()); } @@ -175,6 +175,17 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng return builder.build(); } + @Override + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, coreSettings.getUsageStatsTopic()); + } + + @Override + public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { + return new TbAwsSqsConsumerTemplate<>(transportApiAdmin, sqsSettings, coreSettings.getUsageStatsTopic(), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java index 4889a7182d..4625b9d4fc 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java @@ -21,6 +21,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; @@ -163,6 +164,17 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { return builder.build(); } + @Override + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, coreSettings.getUsageStatsTopic()); + } + + @Override + public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { + return new TbAwsSqsConsumerTemplate<>(transportApiAdmin, sqsSettings, coreSettings.getUsageStatsTopic(), + msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToUsageStatsServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java index cf43bc5fe4..851b8cc40e 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java @@ -143,6 +143,11 @@ public class AwsSqsTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory return builder.build(); } + @Override + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, coreSettings.getUsageStatsTopic()); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { @@ -158,4 +163,5 @@ public class AwsSqsTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory notificationAdmin.destroy(); } } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTransportQueueFactory.java index 351cd4058c..103711f36b 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTransportQueueFactory.java @@ -18,6 +18,7 @@ package org.thingsboard.server.queue.provider; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; @@ -42,7 +43,7 @@ import org.thingsboard.server.queue.sqs.TbAwsSqsSettings; import javax.annotation.PreDestroy; @Component -@ConditionalOnExpression("'${queue.type:null}'=='aws-sqs' && ('${service.type:null}'=='monolith' || '${service.type:null}'=='tb-transport')") +@ConditionalOnExpression("'${queue.type:null}'=='aws-sqs' && (('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true') || '${service.type:null}'=='tb-transport')") @Slf4j public class AwsSqsTransportQueueFactory implements TbTransportQueueFactory { private final TbQueueTransportApiSettings transportApiSettings; @@ -109,6 +110,11 @@ public class AwsSqsTransportQueueFactory implements TbTransportQueueFactory { msg -> new TbProtoQueueMsg<>(msg.getKey(), ToTransportMsg.parseFrom(msg.getData()), msg.getHeaders())); } + @Override + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, coreSettings.getUsageStatsTopic()); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java index 8ebda6e7b9..f7a47ecf3a 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java @@ -125,8 +125,20 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE return null; } + @Override + public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { + return new InMemoryTbQueueConsumer<>(coreSettings.getUsageStatsTopic()); + } + + @Override + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + return new InMemoryTbQueueProducer<>(coreSettings.getUsageStatsTopic()); + } + @Scheduled(fixedRateString = "${queue.in_memory.stats.print-interval-ms:60000}") private void printInMemoryStats() { storage.printStats(); } + + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java index 2855e577ee..9ddf72534c 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java @@ -18,6 +18,7 @@ package org.thingsboard.server.queue.provider; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; @@ -37,7 +38,7 @@ import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSettings; @Component -@ConditionalOnExpression("'${queue.type:null}'=='in-memory' && ('${service.type:null}'=='monolith' || '${service.type:null}'=='tb-transport')") +@ConditionalOnExpression("'${queue.type:null}'=='in-memory' && (('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true') || '${service.type:null}'=='tb-transport')") @Slf4j public class InMemoryTbTransportQueueFactory implements TbTransportQueueFactory { private final TbQueueTransportApiSettings transportApiSettings; @@ -96,4 +97,10 @@ public class InMemoryTbTransportQueueFactory implements TbTransportQueueFactory public TbQueueConsumer> createTransportNotificationsConsumer() { return new InMemoryTbQueueConsumer<>(transportNotificationSettings.getNotificationsTopic() + "." + serviceInfoProvider.getServiceId()); } + + @Override + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + return new InMemoryTbQueueProducer<>(coreSettings.getUsageStatsTopic()); + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java index 31ef4d3e45..538d91aa0f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java @@ -26,6 +26,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMs import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -249,6 +250,28 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi return builder.build(); } + @Override + public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { + TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); + consumerBuilder.settings(kafkaSettings); + consumerBuilder.topic(coreSettings.getUsageStatsTopic()); + consumerBuilder.clientId("monolith-us-consumer-" + serviceInfoProvider.getServiceId()); + consumerBuilder.groupId("monolith-us-consumer-" + serviceInfoProvider.getServiceId()); + consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); + consumerBuilder.admin(coreAdmin); + return consumerBuilder.build(); + } + + @Override + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + requestBuilder.settings(kafkaSettings); + requestBuilder.clientId("monolith-us-producer-" + serviceInfoProvider.getServiceId()); + requestBuilder.defaultTopic(coreSettings.getUsageStatsTopic()); + requestBuilder.admin(coreAdmin); + return requestBuilder.build(); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java index bf2ec6751e..5e1b398e91 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java @@ -26,6 +26,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMs import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -219,6 +220,28 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { return builder.build(); } + @Override + public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { + TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); + consumerBuilder.settings(kafkaSettings); + consumerBuilder.topic(coreSettings.getUsageStatsTopic()); + consumerBuilder.clientId("tb-core-us-consumer-" + serviceInfoProvider.getServiceId()); + consumerBuilder.groupId("tb-core-us-consumer-" + serviceInfoProvider.getServiceId()); + consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); + consumerBuilder.admin(coreAdmin); + return consumerBuilder.build(); + } + + @Override + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + requestBuilder.settings(kafkaSettings); + requestBuilder.clientId("tb-core-us-producer-" + serviceInfoProvider.getServiceId()); + requestBuilder.defaultTopic(coreSettings.getUsageStatsTopic()); + requestBuilder.admin(coreAdmin); + return requestBuilder.build(); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java index dc44caf3f0..118f0f3316 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java @@ -26,6 +26,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMs import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueProducer; @@ -192,6 +193,16 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { return builder.build(); } + @Override + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + requestBuilder.settings(kafkaSettings); + requestBuilder.clientId("tb-rule-engine-us-producer-" + serviceInfoProvider.getServiceId()); + requestBuilder.defaultTopic(coreSettings.getUsageStatsTopic()); + requestBuilder.admin(coreAdmin); + return requestBuilder.build(); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbTransportQueueFactory.java index 505aa203d7..7961bb97bb 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbTransportQueueFactory.java @@ -21,6 +21,7 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -43,7 +44,7 @@ import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSetting import javax.annotation.PreDestroy; @Component -@ConditionalOnExpression("'${queue.type:null}'=='kafka' && ('${service.type:null}'=='monolith' || '${service.type:null}'=='tb-transport')") +@ConditionalOnExpression("'${queue.type:null}'=='kafka' && (('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true') || '${service.type:null}'=='tb-transport')") @Slf4j public class KafkaTbTransportQueueFactory implements TbTransportQueueFactory { @@ -138,6 +139,16 @@ public class KafkaTbTransportQueueFactory implements TbTransportQueueFactory { return responseBuilder.build(); } + @Override + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + requestBuilder.settings(kafkaSettings); + requestBuilder.clientId("transport-node-us-producer-" + serviceInfoProvider.getServiceId()); + requestBuilder.defaultTopic(coreSettings.getUsageStatsTopic()); + requestBuilder.admin(coreAdmin); + return requestBuilder.build(); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java index 29a60af1e2..d7c7542626 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java @@ -27,6 +27,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMs import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -183,6 +184,17 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng return builder.build(); } + @Override + public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { + return new TbPubSubConsumerTemplate<>(coreAdmin, pubSubSettings, coreSettings.getUsageStatsTopic(), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, coreSettings.getUsageStatsTopic()); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java index 7d85b5c3ba..71a1ceaf9b 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java @@ -26,6 +26,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMs import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -157,6 +158,17 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { return builder.build(); } + @Override + public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { + return new TbPubSubConsumerTemplate<>(coreAdmin, pubSubSettings, coreSettings.getUsageStatsTopic(), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, coreSettings.getUsageStatsTopic()); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java index a6105fea0c..ad679039e4 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java @@ -26,6 +26,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMs import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueProducer; @@ -146,6 +147,11 @@ public class PubSubTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory return builder.build(); } + @Override + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, coreSettings.getUsageStatsTopic()); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTransportQueueFactory.java index 7e859e02ab..efde055271 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTransportQueueFactory.java @@ -21,6 +21,7 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -43,7 +44,7 @@ import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSetting import javax.annotation.PreDestroy; @Component -@ConditionalOnExpression("'${queue.type:null}'=='pubsub' && ('${service.type:null}'=='monolith' || '${service.type:null}'=='tb-transport')") +@ConditionalOnExpression("'${queue.type:null}'=='pubsub' && (('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true') || '${service.type:null}'=='tb-transport')") @Slf4j public class PubSubTransportQueueFactory implements TbTransportQueueFactory { @@ -114,6 +115,11 @@ public class PubSubTransportQueueFactory implements TbTransportQueueFactory { msg -> new TbProtoQueueMsg<>(msg.getKey(), ToTransportMsg.parseFrom(msg.getData()), msg.getHeaders())); } + @Override + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, coreSettings.getUsageStatsTopic()); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java index 1f812ea13c..e6eff652be 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java @@ -22,7 +22,14 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos.RemoteJsRequest; import org.thingsboard.server.gen.js.JsInvokeProtos.RemoteJsResponse; -import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueProducer; @@ -91,64 +98,64 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE } @Override - public TbQueueProducer> createTransportNotificationsMsgProducer() { + public TbQueueProducer> createTransportNotificationsMsgProducer() { return new TbRabbitMqProducerTemplate<>(notificationAdmin, rabbitMqSettings, transportNotificationSettings.getNotificationsTopic()); } @Override - public TbQueueProducer> createRuleEngineMsgProducer() { + public TbQueueProducer> createRuleEngineMsgProducer() { return new TbRabbitMqProducerTemplate<>(ruleEngineAdmin, rabbitMqSettings, ruleEngineSettings.getTopic()); } @Override - public TbQueueProducer> createRuleEngineNotificationsMsgProducer() { + public TbQueueProducer> createRuleEngineNotificationsMsgProducer() { return new TbRabbitMqProducerTemplate<>(ruleEngineAdmin, rabbitMqSettings, ruleEngineSettings.getTopic()); } @Override - public TbQueueProducer> createTbCoreMsgProducer() { + public TbQueueProducer> createTbCoreMsgProducer() { return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, coreSettings.getTopic()); } @Override - public TbQueueProducer> createTbCoreNotificationsMsgProducer() { + public TbQueueProducer> createTbCoreNotificationsMsgProducer() { return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, coreSettings.getTopic()); } @Override - public TbQueueConsumer> createToRuleEngineMsgConsumer(TbRuleEngineQueueConfiguration configuration) { + public TbQueueConsumer> createToRuleEngineMsgConsumer(TbRuleEngineQueueConfiguration configuration) { return new TbRabbitMqConsumerTemplate<>(ruleEngineAdmin, rabbitMqSettings, ruleEngineSettings.getTopic(), - msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToRuleEngineMsg.parseFrom(msg.getData()), msg.getHeaders())); + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override - public TbQueueConsumer> createToRuleEngineNotificationsMsgConsumer() { + public TbQueueConsumer> createToRuleEngineNotificationsMsgConsumer() { return new TbRabbitMqConsumerTemplate<>(notificationAdmin, rabbitMqSettings, partitionService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceInfoProvider.getServiceId()).getFullTopicName(), - msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToRuleEngineNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override - public TbQueueConsumer> createToCoreMsgConsumer() { + public TbQueueConsumer> createToCoreMsgConsumer() { return new TbRabbitMqConsumerTemplate<>(coreAdmin, rabbitMqSettings, coreSettings.getTopic(), - msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToCoreMsg.parseFrom(msg.getData()), msg.getHeaders())); + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override - public TbQueueConsumer> createToCoreNotificationsMsgConsumer() { + public TbQueueConsumer> createToCoreNotificationsMsgConsumer() { return new TbRabbitMqConsumerTemplate<>(notificationAdmin, rabbitMqSettings, partitionService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName(), - msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToCoreNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override - public TbQueueConsumer> createTransportApiRequestConsumer() { + public TbQueueConsumer> createTransportApiRequestConsumer() { return new TbRabbitMqConsumerTemplate<>(transportApiAdmin, rabbitMqSettings, transportApiSettings.getRequestsTopic(), - msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.TransportApiRequestMsg.parseFrom(msg.getData()), msg.getHeaders())); + msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportApiRequestMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override - public TbQueueProducer> createTransportApiResponseProducer() { + public TbQueueProducer> createTransportApiResponseProducer() { return new TbRabbitMqProducerTemplate<>(transportApiAdmin, rabbitMqSettings, transportApiSettings.getResponsesTopic()); } @@ -175,6 +182,17 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE return builder.build(); } + @Override + public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { + return new TbRabbitMqConsumerTemplate<>(coreAdmin, rabbitMqSettings, coreSettings.getUsageStatsTopic(), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, coreSettings.getUsageStatsTopic()); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java index 65d582c4bc..e3064dd62d 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java @@ -26,6 +26,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMs import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -163,6 +164,17 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { return builder.build(); } + @Override + public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { + return new TbRabbitMqConsumerTemplate<>(coreAdmin, rabbitMqSettings, coreSettings.getUsageStatsTopic(), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, coreSettings.getUsageStatsTopic()); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java index 26abf78164..a1097049db 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java @@ -26,6 +26,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMs import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueProducer; @@ -144,6 +145,11 @@ public class RabbitMqTbRuleEngineQueueFactory implements TbRuleEngineQueueFactor return builder.build(); } + @Override + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, coreSettings.getUsageStatsTopic()); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTransportQueueFactory.java index 841e004b3f..b28a2cb7e2 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTransportQueueFactory.java @@ -21,6 +21,7 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -42,7 +43,7 @@ import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSetting import javax.annotation.PreDestroy; @Component -@ConditionalOnExpression("'${queue.type:null}'=='rabbitmq' && ('${service.type:null}'=='monolith' || '${service.type:null}'=='tb-transport')") +@ConditionalOnExpression("'${queue.type:null}'=='rabbitmq' && (('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true') || '${service.type:null}'=='tb-transport')") @Slf4j public class RabbitMqTransportQueueFactory implements TbTransportQueueFactory { private final TbQueueTransportApiSettings transportApiSettings; @@ -111,6 +112,11 @@ public class RabbitMqTransportQueueFactory implements TbTransportQueueFactory { msg -> new TbProtoQueueMsg<>(msg.getKey(), ToTransportMsg.parseFrom(msg.getData()), msg.getHeaders())); } + @Override + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, coreSettings.getUsageStatsTopic()); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java index 3db6496b7d..a205f9b061 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java @@ -26,6 +26,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMs import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -180,6 +181,17 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul return builder.build(); } + @Override + public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { + return new TbServiceBusConsumerTemplate<>(coreAdmin, serviceBusSettings, coreSettings.getUsageStatsTopic(), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, coreSettings.getUsageStatsTopic()); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java index a3ba577f06..f00aee331b 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java @@ -21,10 +21,12 @@ import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -101,7 +103,7 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueProducer> createRuleEngineNotificationsMsgProducer() { + public TbQueueProducer> createRuleEngineNotificationsMsgProducer() { return new TbServiceBusProducerTemplate<>(ruleEngineAdmin, serviceBusSettings, ruleEngineSettings.getTopic()); } @@ -111,7 +113,7 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueProducer> createTbCoreNotificationsMsgProducer() { + public TbQueueProducer> createTbCoreNotificationsMsgProducer() { return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, coreSettings.getTopic()); } @@ -122,10 +124,10 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueConsumer> createToCoreNotificationsMsgConsumer() { + public TbQueueConsumer> createToCoreNotificationsMsgConsumer() { return new TbServiceBusConsumerTemplate<>(notificationAdmin, serviceBusSettings, partitionService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName(), - msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToCoreNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override @@ -162,6 +164,17 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { return builder.build(); } + @Override + public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { + return new TbServiceBusConsumerTemplate<>(coreAdmin, serviceBusSettings, coreSettings.getUsageStatsTopic(), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, coreSettings.getUsageStatsTopic()); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java index e4f6dbfbb4..40a183a254 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java @@ -21,10 +21,12 @@ import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueProducer; @@ -93,7 +95,7 @@ public class ServiceBusTbRuleEngineQueueFactory implements TbRuleEngineQueueFact } @Override - public TbQueueProducer> createRuleEngineNotificationsMsgProducer() { + public TbQueueProducer> createRuleEngineNotificationsMsgProducer() { return new TbServiceBusProducerTemplate<>(ruleEngineAdmin, serviceBusSettings, ruleEngineSettings.getTopic()); } @@ -103,7 +105,7 @@ public class ServiceBusTbRuleEngineQueueFactory implements TbRuleEngineQueueFact } @Override - public TbQueueProducer> createTbCoreNotificationsMsgProducer() { + public TbQueueProducer> createTbCoreNotificationsMsgProducer() { return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, coreSettings.getTopic()); } @@ -114,10 +116,10 @@ public class ServiceBusTbRuleEngineQueueFactory implements TbRuleEngineQueueFact } @Override - public TbQueueConsumer> createToRuleEngineNotificationsMsgConsumer() { + public TbQueueConsumer> createToRuleEngineNotificationsMsgConsumer() { return new TbServiceBusConsumerTemplate<>(notificationAdmin, serviceBusSettings, partitionService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceInfoProvider.getServiceId()).getFullTopicName(), - msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToRuleEngineNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override @@ -143,6 +145,11 @@ public class ServiceBusTbRuleEngineQueueFactory implements TbRuleEngineQueueFact return builder.build(); } + @Override + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, coreSettings.getUsageStatsTopic()); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTransportQueueFactory.java index 0b5640d384..4781d46583 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTransportQueueFactory.java @@ -18,19 +18,24 @@ package org.thingsboard.server.queue.provider; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component; -import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.azure.servicebus.TbServiceBusAdmin; +import org.thingsboard.server.queue.azure.servicebus.TbServiceBusConsumerTemplate; +import org.thingsboard.server.queue.azure.servicebus.TbServiceBusProducerTemplate; import org.thingsboard.server.queue.azure.servicebus.TbServiceBusQueueConfigs; +import org.thingsboard.server.queue.azure.servicebus.TbServiceBusSettings; import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; -import org.thingsboard.server.queue.azure.servicebus.TbServiceBusConsumerTemplate; -import org.thingsboard.server.queue.azure.servicebus.TbServiceBusProducerTemplate; -import org.thingsboard.server.queue.azure.servicebus.TbServiceBusSettings; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSettings; @@ -38,7 +43,7 @@ import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSetting import javax.annotation.PreDestroy; @Component -@ConditionalOnExpression("'${queue.type:null}'=='service-bus' && ('${service.type:null}'=='monolith' || '${service.type:null}'=='tb-transport')") +@ConditionalOnExpression("'${queue.type:null}'=='service-bus' && (('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true') || '${service.type:null}'=='tb-transport')") @Slf4j public class ServiceBusTransportQueueFactory implements TbTransportQueueFactory { private final TbQueueTransportApiSettings transportApiSettings; @@ -69,17 +74,17 @@ public class ServiceBusTransportQueueFactory implements TbTransportQueueFactory } @Override - public TbQueueRequestTemplate, TbProtoQueueMsg> createTransportApiRequestTemplate() { - TbQueueProducer> producerTemplate = + public TbQueueRequestTemplate, TbProtoQueueMsg> createTransportApiRequestTemplate() { + TbQueueProducer> producerTemplate = new TbServiceBusProducerTemplate<>(transportApiAdmin, serviceBusSettings, transportApiSettings.getRequestsTopic()); - TbQueueConsumer> consumerTemplate = + TbQueueConsumer> consumerTemplate = new TbServiceBusConsumerTemplate<>(transportApiAdmin, serviceBusSettings, transportApiSettings.getResponsesTopic() + "." + serviceInfoProvider.getServiceId(), - msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.TransportApiResponseMsg.parseFrom(msg.getData()), msg.getHeaders())); + msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportApiResponseMsg.parseFrom(msg.getData()), msg.getHeaders())); DefaultTbQueueRequestTemplate.DefaultTbQueueRequestTemplateBuilder - , TbProtoQueueMsg> templateBuilder = DefaultTbQueueRequestTemplate.builder(); + , TbProtoQueueMsg> templateBuilder = DefaultTbQueueRequestTemplate.builder(); templateBuilder.queueAdmin(transportApiAdmin); templateBuilder.requestTemplate(producerTemplate); templateBuilder.responseTemplate(consumerTemplate); @@ -90,20 +95,25 @@ public class ServiceBusTransportQueueFactory implements TbTransportQueueFactory } @Override - public TbQueueProducer> createRuleEngineMsgProducer() { + public TbQueueProducer> createRuleEngineMsgProducer() { return new TbServiceBusProducerTemplate<>(transportApiAdmin, serviceBusSettings, transportApiSettings.getRequestsTopic()); } @Override - public TbQueueProducer> createTbCoreMsgProducer() { + public TbQueueProducer> createTbCoreMsgProducer() { return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, coreSettings.getTopic()); } @Override - public TbQueueConsumer> createTransportNotificationsConsumer() { + public TbQueueConsumer> createTransportNotificationsConsumer() { return new TbServiceBusConsumerTemplate<>(notificationAdmin, serviceBusSettings, transportNotificationSettings.getNotificationsTopic() + "." + serviceInfoProvider.getServiceId(), - msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToTransportMsg.parseFrom(msg.getData()), msg.getHeaders())); + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToTransportMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, coreSettings.getUsageStatsTopic()); } @PreDestroy diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java index 59a8f449b0..7c19a1ba2f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java @@ -21,6 +21,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMs import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueConsumer; @@ -33,7 +34,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; * Responsible for initialization of various Producers and Consumers used by TB Core Node. * Implementation Depends on the queue queue.type from yml or TB_QUEUE_TYPE environment variable */ -public interface TbCoreQueueFactory { +public interface TbCoreQueueFactory extends TbUsageStatsClientQueueFactory { /** * Used to push messages to instances of TB Transport Service @@ -77,6 +78,13 @@ public interface TbCoreQueueFactory { */ TbQueueConsumer> createToCoreMsgConsumer(); + /** + * Used to consume messages about usage statistics by TB Core Service + * + * @return + */ + TbQueueConsumer> createToUsageStatsServiceMsgConsumer(); + /** * Used to consume high priority messages by TB Core Service * diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java index 9fe5de0bcc..e620259960 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java @@ -17,7 +17,12 @@ package org.thingsboard.server.queue.provider; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; -import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -28,11 +33,12 @@ import javax.annotation.PostConstruct; public class TbCoreQueueProducerProvider implements TbQueueProducerProvider { private final TbCoreQueueFactory tbQueueProvider; - private TbQueueProducer> toTransport; - private TbQueueProducer> toRuleEngine; - private TbQueueProducer> toTbCore; - private TbQueueProducer> toRuleEngineNotifications; - private TbQueueProducer> toTbCoreNotifications; + private TbQueueProducer> toTransport; + private TbQueueProducer> toRuleEngine; + private TbQueueProducer> toTbCore; + private TbQueueProducer> toRuleEngineNotifications; + private TbQueueProducer> toTbCoreNotifications; + private TbQueueProducer> toUsageStats; public TbCoreQueueProducerProvider(TbCoreQueueFactory tbQueueProvider) { this.tbQueueProvider = tbQueueProvider; @@ -45,30 +51,36 @@ public class TbCoreQueueProducerProvider implements TbQueueProducerProvider { this.toRuleEngine = tbQueueProvider.createRuleEngineMsgProducer(); this.toRuleEngineNotifications = tbQueueProvider.createRuleEngineNotificationsMsgProducer(); this.toTbCoreNotifications = tbQueueProvider.createTbCoreNotificationsMsgProducer(); + this.toUsageStats = tbQueueProvider.createToUsageStatsServiceMsgProducer(); } @Override - public TbQueueProducer> getTransportNotificationsMsgProducer() { + public TbQueueProducer> getTransportNotificationsMsgProducer() { return toTransport; } @Override - public TbQueueProducer> getRuleEngineMsgProducer() { + public TbQueueProducer> getRuleEngineMsgProducer() { return toRuleEngine; } @Override - public TbQueueProducer> getRuleEngineNotificationsMsgProducer() { + public TbQueueProducer> getRuleEngineNotificationsMsgProducer() { return toRuleEngineNotifications; } @Override - public TbQueueProducer> getTbCoreMsgProducer() { + public TbQueueProducer> getTbCoreMsgProducer() { return toTbCore; } @Override - public TbQueueProducer> getTbCoreNotificationsMsgProducer() { + public TbQueueProducer> getTbCoreNotificationsMsgProducer() { return toTbCoreNotifications; } + + @Override + public TbQueueProducer> getTbUsageStatsMsgProducer() { + return toUsageStats; + } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java index 34c6ee577f..8c3a0b6565 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java @@ -20,6 +20,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMs import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -63,4 +64,10 @@ public interface TbQueueProducerProvider { */ TbQueueProducer> getTbCoreNotificationsMsgProducer(); + /** + * Used to push messages to other instances of TB Core Service + * + * @return + */ + TbQueueProducer> getTbUsageStatsMsgProducer(); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java index 69de7ca3e2..309def1269 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java @@ -17,7 +17,12 @@ package org.thingsboard.server.queue.provider; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; -import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -28,12 +33,12 @@ import javax.annotation.PostConstruct; public class TbRuleEngineProducerProvider implements TbQueueProducerProvider { private final TbRuleEngineQueueFactory tbQueueProvider; - private TbQueueProducer> toTransport; - private TbQueueProducer> toRuleEngine; - private TbQueueProducer> toTbCore; - private TbQueueProducer> toRuleEngineNotifications; - private TbQueueProducer> toTbCoreNotifications; - + private TbQueueProducer> toTransport; + private TbQueueProducer> toRuleEngine; + private TbQueueProducer> toTbCore; + private TbQueueProducer> toRuleEngineNotifications; + private TbQueueProducer> toTbCoreNotifications; + private TbQueueProducer> toUsageStats; public TbRuleEngineProducerProvider(TbRuleEngineQueueFactory tbQueueProvider) { this.tbQueueProvider = tbQueueProvider; @@ -46,30 +51,36 @@ public class TbRuleEngineProducerProvider implements TbQueueProducerProvider { this.toRuleEngine = tbQueueProvider.createRuleEngineMsgProducer(); this.toRuleEngineNotifications = tbQueueProvider.createRuleEngineNotificationsMsgProducer(); this.toTbCoreNotifications = tbQueueProvider.createTbCoreNotificationsMsgProducer(); + this.toUsageStats = tbQueueProvider.createToUsageStatsServiceMsgProducer(); } @Override - public TbQueueProducer> getTransportNotificationsMsgProducer() { + public TbQueueProducer> getTransportNotificationsMsgProducer() { return toTransport; } @Override - public TbQueueProducer> getRuleEngineMsgProducer() { + public TbQueueProducer> getRuleEngineMsgProducer() { return toRuleEngine; } @Override - public TbQueueProducer> getRuleEngineNotificationsMsgProducer() { + public TbQueueProducer> getRuleEngineNotificationsMsgProducer() { return toRuleEngineNotifications; } @Override - public TbQueueProducer> getTbCoreMsgProducer() { + public TbQueueProducer> getTbCoreMsgProducer() { return toTbCore; } @Override - public TbQueueProducer> getTbCoreNotificationsMsgProducer() { + public TbQueueProducer> getTbCoreNotificationsMsgProducer() { return toTbCoreNotifications; } + + @Override + public TbQueueProducer> getTbUsageStatsMsgProducer() { + return toUsageStats; + } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java index 561b3e84ea..2f80bbce45 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java @@ -32,7 +32,7 @@ import org.thingsboard.server.queue.settings.TbRuleEngineQueueConfiguration; * Responsible for initialization of various Producers and Consumers used by TB Core Node. * Implementation Depends on the queue queue.type from yml or TB_QUEUE_TYPE environment variable */ -public interface TbRuleEngineQueueFactory { +public interface TbRuleEngineQueueFactory extends TbUsageStatsClientQueueFactory { /** * Used to push messages to instances of TB Transport Service diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueFactory.java index dc1d2c449c..bd6c6e0584 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueFactory.java @@ -15,17 +15,17 @@ */ package org.thingsboard.server.queue.provider; -import org.thingsboard.server.queue.TbQueueConsumer; -import org.thingsboard.server.queue.TbQueueProducer; -import org.thingsboard.server.queue.TbQueueRequestTemplate; -import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; +import org.thingsboard.server.queue.TbQueueConsumer; +import org.thingsboard.server.queue.TbQueueProducer; +import org.thingsboard.server.queue.TbQueueRequestTemplate; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; -public interface TbTransportQueueFactory { +public interface TbTransportQueueFactory extends TbUsageStatsClientQueueFactory { TbQueueRequestTemplate, TbProtoQueueMsg> createTransportApiRequestTemplate(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java index bced3f83ac..dee6508ef4 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java @@ -17,7 +17,12 @@ package org.thingsboard.server.queue.provider; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; -import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -28,8 +33,9 @@ import javax.annotation.PostConstruct; public class TbTransportQueueProducerProvider implements TbQueueProducerProvider { private final TbTransportQueueFactory tbQueueProvider; - private TbQueueProducer> toRuleEngine; - private TbQueueProducer> toTbCore; + private TbQueueProducer> toRuleEngine; + private TbQueueProducer> toTbCore; + private TbQueueProducer> toUsageStats; public TbTransportQueueProducerProvider(TbTransportQueueFactory tbQueueProvider) { this.tbQueueProvider = tbQueueProvider; @@ -39,30 +45,36 @@ public class TbTransportQueueProducerProvider implements TbQueueProducerProvider public void init() { this.toTbCore = tbQueueProvider.createTbCoreMsgProducer(); this.toRuleEngine = tbQueueProvider.createRuleEngineMsgProducer(); + this.toUsageStats = tbQueueProvider.createToUsageStatsServiceMsgProducer(); } @Override - public TbQueueProducer> getTransportNotificationsMsgProducer() { + public TbQueueProducer> getTransportNotificationsMsgProducer() { throw new RuntimeException("Not Implemented! Should not be used by Transport!"); } @Override - public TbQueueProducer> getRuleEngineMsgProducer() { + public TbQueueProducer> getRuleEngineMsgProducer() { return toRuleEngine; } @Override - public TbQueueProducer> getTbCoreMsgProducer() { + public TbQueueProducer> getTbCoreMsgProducer() { return toTbCore; } @Override - public TbQueueProducer> getRuleEngineNotificationsMsgProducer() { + public TbQueueProducer> getRuleEngineNotificationsMsgProducer() { throw new RuntimeException("Not Implemented! Should not be used by Transport!"); } @Override - public TbQueueProducer> getTbCoreNotificationsMsgProducer() { + public TbQueueProducer> getTbCoreNotificationsMsgProducer() { throw new RuntimeException("Not Implemented! Should not be used by Transport!"); } + + @Override + public TbQueueProducer> getTbUsageStatsMsgProducer() { + return toUsageStats; + } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbUsageStatsClientQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbUsageStatsClientQueueFactory.java new file mode 100644 index 0000000000..f2ce1a9fe5 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbUsageStatsClientQueueFactory.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.queue.provider; + +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.queue.TbQueueProducer; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; + +public interface TbUsageStatsClientQueueFactory { + + TbQueueProducer> createToUsageStatsServiceMsgProducer(); + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/scheduler/DefaultSchedulerComponent.java b/common/queue/src/main/java/org/thingsboard/server/queue/scheduler/DefaultSchedulerComponent.java new file mode 100644 index 0000000000..02ce60496e --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/scheduler/DefaultSchedulerComponent.java @@ -0,0 +1,61 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.queue.scheduler; + +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.ThingsBoardThreadFactory; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +@Component +public class DefaultSchedulerComponent implements SchedulerComponent{ + + protected ScheduledExecutorService schedulerExecutor; + + @PostConstruct + public void init(){ + this.schedulerExecutor = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("queue-scheduler")); + } + + @PreDestroy + public void destroy() { + if (schedulerExecutor != null) { + schedulerExecutor.shutdownNow(); + } + } + + public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) { + return schedulerExecutor.schedule(command, delay, unit); + } + + public ScheduledFuture schedule(Callable callable, long delay, TimeUnit unit) { + return schedulerExecutor.schedule(callable, delay, unit); + } + + public ScheduledFuture scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) { + return schedulerExecutor.scheduleAtFixedRate(command, initialDelay, period, unit); + } + + public ScheduledFuture scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) { + return schedulerExecutor.scheduleWithFixedDelay(command, initialDelay, delay, unit); + } +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/scheduler/SchedulerComponent.java b/common/queue/src/main/java/org/thingsboard/server/queue/scheduler/SchedulerComponent.java new file mode 100644 index 0000000000..3c12be43a5 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/scheduler/SchedulerComponent.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.queue.scheduler; + +import java.util.concurrent.Callable; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +public interface SchedulerComponent { + + ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit); + + ScheduledFuture schedule(Callable callable, long delay, TimeUnit unit); + + ScheduledFuture scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit); + + ScheduledFuture scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit); + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueCoreSettings.java b/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueCoreSettings.java index 1c260c754d..d78c6d1bad 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueCoreSettings.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueCoreSettings.java @@ -26,6 +26,9 @@ public class TbQueueCoreSettings { @Value("${queue.core.topic}") private String topic; + @Value("${queue.core.usage-stats-topic:tb_usage_stats}") + private String usageStatsTopic; + @Value("${queue.core.partitions}") private int partitions; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageClient.java b/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageClient.java new file mode 100644 index 0000000000..0702322838 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageClient.java @@ -0,0 +1,116 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.queue.usagestats; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.ApiUsageRecordKey; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.UsageStatsKVProto; +import org.thingsboard.server.queue.TbQueueProducer; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.provider.TbQueueProducerProvider; +import org.thingsboard.server.queue.scheduler.SchedulerComponent; + +import javax.annotation.PostConstruct; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +@Component +@Slf4j +public class DefaultTbApiUsageClient implements TbApiUsageClient { + + @Value("${usage.stats.report.enabled:true}") + private boolean enabled; + @Value("${usage.stats.report.interval:10}") + private int interval; + + private final ConcurrentMap[] values = new ConcurrentMap[ApiUsageRecordKey.values().length]; + private final PartitionService partitionService; + private final SchedulerComponent scheduler; + private final TbQueueProducerProvider producerProvider; + private TbQueueProducer> msgProducer; + + public DefaultTbApiUsageClient(PartitionService partitionService, SchedulerComponent scheduler, TbQueueProducerProvider producerProvider) { + this.partitionService = partitionService; + this.scheduler = scheduler; + this.producerProvider = producerProvider; + } + + @PostConstruct + private void init() { + if (enabled) { + msgProducer = this.producerProvider.getTbUsageStatsMsgProducer(); + for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { + values[key.ordinal()] = new ConcurrentHashMap<>(); + } + scheduler.scheduleWithFixedDelay(this::reportStats, new Random().nextInt(interval), interval, TimeUnit.SECONDS); + } + } + + private void reportStats() { + try { + ConcurrentMap report = new ConcurrentHashMap<>(); + + for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { + values[key.ordinal()].forEach(((tenantId, atomicLong) -> { + long value = atomicLong.getAndSet(0); + if (value > 0) { + ToUsageStatsServiceMsg.Builder msgBuilder = report.computeIfAbsent(tenantId, id -> { + ToUsageStatsServiceMsg.Builder msg = ToUsageStatsServiceMsg.newBuilder(); + msg.setTenantIdMSB(tenantId.getId().getMostSignificantBits()); + msg.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()); + return msg; + }); + msgBuilder.addValues(UsageStatsKVProto.newBuilder().setKey(key.name()).setValue(value).build()); + } + })); + } + + report.forEach(((tenantId, builder) -> { + //TODO: figure out how to minimize messages into the queue. Maybe group by 100s of messages? + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, tenantId).newByTopic(msgProducer.getDefaultTopic()); + msgProducer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), builder.build()), null); + })); + log.info("Report statistics for: {} tenants", report.size()); + } catch (Exception e) { + log.warn("Failed to report statistics: ", e); + } + } + + @Override + public void report(TenantId tenantId, ApiUsageRecordKey key, long value) { + if (enabled) { + ConcurrentMap map = values[key.ordinal()]; + AtomicLong atomicValue = map.computeIfAbsent(tenantId, id -> new AtomicLong()); + atomicValue.addAndGet(value); + } + } + + @Override + public void report(TenantId tenantId, ApiUsageRecordKey key) { + report(tenantId, key, 1L); + } +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/TbApiUsageClient.java b/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/TbApiUsageClient.java new file mode 100644 index 0000000000..31d85b27c7 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/TbApiUsageClient.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.queue.usagestats; + +import org.thingsboard.server.common.data.ApiUsageRecordKey; +import org.thingsboard.server.common.data.id.TenantId; + +public interface TbApiUsageClient { + + void report(TenantId tenantId, ApiUsageRecordKey key, long value); + + void report(TenantId tenantId, ApiUsageRecordKey key); + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/util/TbTransportComponent.java b/common/queue/src/main/java/org/thingsboard/server/queue/util/TbTransportComponent.java new file mode 100644 index 0000000000..dde1c6a620 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/util/TbTransportComponent.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.queue.util; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@ConditionalOnExpression("('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true') || '${service.type:null}'=='tb-transport'") +public @interface TbTransportComponent { +} diff --git a/common/queue/src/main/proto/queue.proto b/common/queue/src/main/proto/queue.proto index 59d45d7db6..4b5a6028e6 100644 --- a/common/queue/src/main/proto/queue.proto +++ b/common/queue/src/main/proto/queue.proto @@ -51,6 +51,8 @@ message SessionInfoProto { string deviceType = 9; int64 gwSessionIdMSB = 10; int64 gwSessionIdLSB = 11; + int64 deviceProfileIdMSB = 12; + int64 deviceProfileIdLSB = 13; } enum SessionEvent { @@ -71,6 +73,12 @@ enum KeyValueType { JSON_V = 4; } +enum CredentialsType { + ACCESS_TOKEN = 0; + X509_CERTIFICATE = 1; + MQTT_BASIC = 2; +} + message KeyValueProto { string key = 1; KeyValueType type = 2; @@ -99,6 +107,8 @@ message DeviceInfoProto { string deviceName = 5; string deviceType = 6; string additionalInfo = 7; + int64 deviceProfileIdMSB = 8; + int64 deviceProfileIdLSB = 9; } /** @@ -143,9 +153,16 @@ message ValidateDeviceX509CertRequestMsg { string hash = 1; } +message ValidateBasicMqttCredRequestMsg { + string clientId = 1; + string userName = 2; + string password = 3; +} + message ValidateDeviceCredentialsResponseMsg { DeviceInfoProto deviceInfo = 1; string credentialsBody = 2; + bytes profileBody = 3; } message GetOrCreateDeviceFromGatewayRequestMsg { @@ -157,16 +174,30 @@ message GetOrCreateDeviceFromGatewayRequestMsg { message GetOrCreateDeviceFromGatewayResponseMsg { DeviceInfoProto deviceInfo = 1; + bytes profileBody = 2; } -message GetTenantRoutingInfoRequestMsg { - int64 tenantIdMSB = 1; - int64 tenantIdLSB = 2; +message GetEntityProfileRequestMsg { + string entityType = 1; + int64 entityIdMSB = 2; + int64 entityIdLSB = 3; } -message GetTenantRoutingInfoResponseMsg { - bool isolatedTbCore = 1; - bool isolatedTbRuleEngine = 2; +message GetEntityProfileResponseMsg { + string entityType = 1; + bytes data = 2; + bytes apiState = 3; +} + +message EntityUpdateMsg { + string entityType = 1; + bytes data = 2; +} + +message EntityDeleteMsg { + string entityType = 1; + int64 entityIdMSB = 2; + int64 entityIdLSB = 3; } message SessionCloseNotificationProto { @@ -211,6 +242,43 @@ message ClaimDeviceMsg { int64 durationMs = 4; } +message DeviceCredentialsProto { + int64 deviceIdMSB = 1; + int64 deviceIdLSB = 2; + CredentialsType credentialsType = 3; + string credentialsId = 4; + string credentialsValue = 5; +} + +message CredentialsDataProto { + ValidateDeviceTokenRequestMsg validateDeviceTokenRequestMsg = 1; + ValidateDeviceX509CertRequestMsg validateDeviceX509CertRequestMsg = 2; + ValidateBasicMqttCredRequestMsg validateBasicMqttCredRequestMsg = 3; +} + +message ProvisionDeviceRequestMsg { + string deviceName = 1; + CredentialsType credentialsType = 2; + ProvisionDeviceCredentialsMsg provisionDeviceCredentialsMsg = 3; + CredentialsDataProto credentialsDataProto = 4; +} + +message ProvisionDeviceCredentialsMsg { + string provisionDeviceKey = 1; + string provisionDeviceSecret = 2; +} + +message ProvisionDeviceResponseMsg { + DeviceCredentialsProto deviceCredentials = 1; + ProvisionResponseStatus provisionResponseStatus = 2; +} + +enum ProvisionResponseStatus { + UNKNOWN = 0; + SUCCESS = 1; + NOT_FOUND = 2; + FAILURE = 3; +} //Used to report session state to tb-Service and persist this state in the cache on the tb-Service level. message SubscriptionInfoProto { int64 lastActivityTime = 1; @@ -236,6 +304,7 @@ message TransportToDeviceActorMsg { ToDeviceRpcResponseMsg toDeviceRPCCallResponse = 6; SubscriptionInfoProto subscriptionInfo = 7; ClaimDeviceMsg claimDevice = 8; + ProvisionDeviceRequestMsg provisionDevice = 9; } message TransportToRuleEngineMsg { @@ -276,6 +345,11 @@ message TbAttributeSubscriptionProto { string scope = 4; } +message TbAlarmSubscriptionProto { + TbSubscriptionProto sub = 1; + int64 ts = 2; +} + message TbSubscriptionUpdateProto { string sessionId = 1; int32 subscriptionId = 2; @@ -284,6 +358,15 @@ message TbSubscriptionUpdateProto { repeated TbSubscriptionUpdateValueListProto data = 5; } +message TbAlarmSubscriptionUpdateProto { + string sessionId = 1; + int32 subscriptionId = 2; + int32 errorCode = 3; + string errorMsg = 4; + string alarm = 5; + bool deleted = 6; +} + message TbAttributeUpdateProto { string entityType = 1; int64 entityIdMSB = 2; @@ -294,6 +377,34 @@ message TbAttributeUpdateProto { repeated TsKvProto data = 7; } +message TbAlarmUpdateProto { + string entityType = 1; + int64 entityIdMSB = 2; + int64 entityIdLSB = 3; + int64 tenantIdMSB = 4; + int64 tenantIdLSB = 5; + string alarm = 6; +} + +message TbAlarmDeleteProto { + string entityType = 1; + int64 entityIdMSB = 2; + int64 entityIdLSB = 3; + int64 tenantIdMSB = 4; + int64 tenantIdLSB = 5; + string alarm = 6; +} + +message TbAttributeDeleteProto { + string entityType = 1; + int64 entityIdMSB = 2; + int64 entityIdLSB = 3; + int64 tenantIdMSB = 4; + int64 tenantIdLSB = 5; + string scope = 6; + repeated string keys = 7; +} + message TbTimeSeriesUpdateProto { string entityType = 1; int64 entityIdMSB = 2; @@ -339,10 +450,15 @@ message SubscriptionMgrMsgProto { TbSubscriptionCloseProto subClose = 3; TbTimeSeriesUpdateProto tsUpdate = 4; TbAttributeUpdateProto attrUpdate = 5; + TbAttributeDeleteProto attrDelete = 6; + TbAlarmSubscriptionProto alarmSub = 7; + TbAlarmUpdateProto alarmUpdate = 8; + TbAlarmDeleteProto alarmDelete = 9; } message LocalSubscriptionServiceMsgProto { TbSubscriptionUpdateProto subUpdate = 1; + TbAlarmSubscriptionUpdateProto alarmSubUpdate = 2; } message FromDeviceRPCResponseProto { @@ -351,6 +467,7 @@ message FromDeviceRPCResponseProto { string response = 3; int32 error = 4; } + /** * Main messages; */ @@ -360,14 +477,17 @@ message TransportApiRequestMsg { ValidateDeviceTokenRequestMsg validateTokenRequestMsg = 1; ValidateDeviceX509CertRequestMsg validateX509CertRequestMsg = 2; GetOrCreateDeviceFromGatewayRequestMsg getOrCreateDeviceRequestMsg = 3; - GetTenantRoutingInfoRequestMsg getTenantRoutingInfoRequestMsg = 4; + GetEntityProfileRequestMsg entityProfileRequestMsg = 4; + ValidateBasicMqttCredRequestMsg validateBasicMqttCredRequestMsg = 6; + ProvisionDeviceRequestMsg provisionDeviceRequestMsg = 7; } /* Response from ThingsBoard Core Service to Transport Service */ message TransportApiResponseMsg { - ValidateDeviceCredentialsResponseMsg validateTokenResponseMsg = 1; + ValidateDeviceCredentialsResponseMsg validateCredResponseMsg = 1; GetOrCreateDeviceFromGatewayResponseMsg getOrCreateDeviceResponseMsg = 2; - GetTenantRoutingInfoResponseMsg getTenantRoutingInfoResponseMsg = 4; + GetEntityProfileResponseMsg entityProfileResponseMsg = 3; + ProvisionDeviceResponseMsg provisionDeviceResponseMsg = 4; } /* Messages that are handled by ThingsBoard Core Service */ @@ -408,4 +528,21 @@ message ToTransportMsg { AttributeUpdateNotificationMsg attributeUpdateNotification = 5; ToDeviceRpcRequestMsg toDeviceRequest = 6; ToServerRpcResponseMsg toServerResponse = 7; + /* For Tenant, TenantProfile and DeviceProfile */ + EntityUpdateMsg entityUpdateMsg = 8; + EntityDeleteMsg entityDeleteMsg = 9; + ProvisionDeviceResponseMsg provisionResponse = 10; +} + +message UsageStatsKVProto{ + string key = 1; + int64 value = 2; +} + +message ToUsageStatsServiceMsg { + int64 tenantIdMSB = 1; + int64 tenantIdLSB = 2; + int64 entityIdMSB = 3; + int64 entityIdLSB = 4; + repeated UsageStatsKVProto values = 5; } diff --git a/common/stats/pom.xml b/common/stats/pom.xml index 8ccf34961c..10c3ccb040 100644 --- a/common/stats/pom.xml +++ b/common/stats/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT common org.thingsboard.common diff --git a/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java b/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java index 8ee75c859e..acd66ff2f3 100644 --- a/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java +++ b/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java @@ -15,13 +15,13 @@ */ package org.thingsboard.server.common.stats; -import io.micrometer.core.instrument.Counter; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import javax.annotation.PostConstruct; import java.util.concurrent.atomic.AtomicInteger; @Service @@ -40,6 +40,23 @@ public class DefaultStatsFactory implements StatsFactory { @Value("${metrics.enabled:false}") private Boolean metricsEnabled; + @Value("${metrics.timer.percentiles:0.5}") + private String timerPercentilesStr; + + private double[] timerPercentiles; + + @PostConstruct + public void init() { + if (!StringUtils.isEmpty(timerPercentilesStr)) { + String[] split = timerPercentilesStr.split(","); + timerPercentiles = new double[split.length]; + for (int i = 0; i < split.length; i++) { + timerPercentiles[i] = Double.parseDouble(split[i]); + } + } + } + + @Override public StatsCounter createStatsCounter(String key, String statsName) { return new StatsCounter( @@ -74,9 +91,21 @@ public class DefaultStatsFactory implements StatsFactory { return new DefaultMessagesStats(totalCounter, successfulCounter, failedCounter); } + @Override + public Timer createTimer(String key, String... tags) { + Timer.Builder timerBuilder = Timer.builder(key) + .tags(tags) + .publishPercentiles(); + if (timerPercentiles != null && timerPercentiles.length > 0) { + timerBuilder.publishPercentiles(timerPercentiles); + } + return timerBuilder.register(meterRegistry); + } + private static class StubCounter implements Counter { @Override - public void increment(double amount) {} + public void increment(double amount) { + } @Override public double count() { diff --git a/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsFactory.java b/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsFactory.java index b979d61f35..416e94980c 100644 --- a/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsFactory.java +++ b/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsFactory.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.stats; +import io.micrometer.core.instrument.Timer; + public interface StatsFactory { StatsCounter createStatsCounter(String key, String statsName); @@ -23,4 +25,6 @@ public interface StatsFactory { T createGauge(String key, T number, String... tags); MessagesStats createMessagesStats(String key); + + Timer createTimer(String key, String... tags); } diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml index a237662b0d..3b04a20abe 100644 --- a/common/transport/coap/pom.xml +++ b/common/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java index 864e7b2ab9..d69cc1b25a 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java @@ -28,7 +28,7 @@ import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor; * Created by ashvayka on 18.10.18. */ @Slf4j -@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.coap.enabled}'=='true')") +@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.coap.enabled}'=='true')") @Component public class CoapTransportContext extends TransportContext { diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java index 3b4b26aa47..63af515c00 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java @@ -24,6 +24,8 @@ import org.eclipse.californium.core.network.ExchangeObserver; import org.eclipse.californium.core.server.resources.CoapExchange; import org.eclipse.californium.core.server.resources.Resource; import org.springframework.util.ReflectionUtils; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.security.DeviceTokenCredentials; import org.thingsboard.server.common.msg.session.FeatureType; import org.thingsboard.server.common.msg.session.SessionMsgType; @@ -32,7 +34,11 @@ import org.thingsboard.server.common.transport.TransportContext; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.adaptor.AdaptorException; +import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.common.transport.auth.SessionInfoCreator; +import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; import java.lang.reflect.Field; import java.util.List; @@ -127,10 +133,25 @@ public class CoapTransportResource extends CoapResource { case CLAIM: processRequest(exchange, SessionMsgType.CLAIM_REQUEST); break; + case PROVISION: + processProvision(exchange); + break; } } } + private void processProvision(CoapExchange exchange) { + log.trace("Processing {}", exchange.advanced().getRequest()); + exchange.accept(); + try { + transportService.process(transportContext.getAdaptor().convertToProvisionRequestMsg(UUID.randomUUID(), exchange.advanced().getRequest()), + new DeviceProvisionCallback(exchange)); + } catch (AdaptorException e) { + log.trace("Failed to decode message: ", e); + exchange.respond(ResponseCode.BAD_REQUEST); + } + } + private void processRequest(CoapExchange exchange, SessionMsgType type) { log.trace("Processing {}", exchange.advanced().getRequest()); exchange.accept(); @@ -143,7 +164,7 @@ public class CoapTransportResource extends CoapResource { return; } - transportService.process(TransportProtos.ValidateDeviceTokenRequestMsg.newBuilder().setToken(credentials.get().getCredentialsId()).build(), + transportService.process(DeviceTransportType.DEFAULT, TransportProtos.ValidateDeviceTokenRequestMsg.newBuilder().setToken(credentials.get().getCredentialsId()).build(), new DeviceAuthCallback(transportContext, exchange, sessionInfo -> { UUID sessionId = new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB()); try { @@ -271,6 +292,8 @@ public class CoapTransportResource extends CoapResource { try { if (uriPath.size() >= FEATURE_TYPE_POSITION) { return Optional.of(FeatureType.valueOf(uriPath.get(FEATURE_TYPE_POSITION - 1).toUpperCase())); + } else if (uriPath.size() == 3 && uriPath.contains(DataConstants.PROVISION)) { + return Optional.of(FeatureType.valueOf(DataConstants.PROVISION.toUpperCase())); } } catch (RuntimeException e) { log.warn("Failed to decode feature type: {}", uriPath); @@ -295,7 +318,7 @@ public class CoapTransportResource extends CoapResource { return this; } - private static class DeviceAuthCallback implements TransportServiceCallback { + private static class DeviceAuthCallback implements TransportServiceCallback { private final TransportContext transportContext; private final CoapExchange exchange; private final Consumer onSuccess; @@ -307,22 +330,9 @@ public class CoapTransportResource extends CoapResource { } @Override - public void onSuccess(TransportProtos.ValidateDeviceCredentialsResponseMsg msg) { + public void onSuccess(ValidateDeviceCredentialsResponse msg) { if (msg.hasDeviceInfo()) { - UUID sessionId = UUID.randomUUID(); - TransportProtos.DeviceInfoProto deviceInfoProto = msg.getDeviceInfo(); - TransportProtos.SessionInfoProto sessionInfo = TransportProtos.SessionInfoProto.newBuilder() - .setNodeId(transportContext.getNodeId()) - .setTenantIdMSB(deviceInfoProto.getTenantIdMSB()) - .setTenantIdLSB(deviceInfoProto.getTenantIdLSB()) - .setDeviceIdMSB(deviceInfoProto.getDeviceIdMSB()) - .setDeviceIdLSB(deviceInfoProto.getDeviceIdLSB()) - .setSessionIdMSB(sessionId.getMostSignificantBits()) - .setSessionIdLSB(sessionId.getLeastSignificantBits()) - .setDeviceName(msg.getDeviceInfo().getDeviceName()) - .setDeviceType(msg.getDeviceInfo().getDeviceType()) - .build(); - onSuccess.accept(sessionInfo); + onSuccess.accept(SessionInfoCreator.create(msg, transportContext, UUID.randomUUID())); } else { exchange.respond(ResponseCode.UNAUTHORIZED); } @@ -335,6 +345,25 @@ public class CoapTransportResource extends CoapResource { } } + private static class DeviceProvisionCallback implements TransportServiceCallback { + private final CoapExchange exchange; + + DeviceProvisionCallback(CoapExchange exchange) { + this.exchange = exchange; + } + + @Override + public void onSuccess(TransportProtos.ProvisionDeviceResponseMsg msg) { + exchange.respond(JsonConverter.toJson(msg).toString()); + } + + @Override + public void onError(Throwable e) { + log.warn("Failed to process request", e); + exchange.respond(ResponseCode.INTERNAL_SERVER_ERROR); + } + } + private static class CoapOkCallback implements TransportServiceCallback { private final CoapExchange exchange; diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java index fd4c8e2d95..afb83f513f 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java @@ -30,7 +30,7 @@ import java.net.InetSocketAddress; import java.net.UnknownHostException; @Service("CoapTransportService") -@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.coap.enabled}'=='true')") +@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.coap.enabled}'=='true')") @Slf4j public class CoapTransportService { diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapTransportAdaptor.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapTransportAdaptor.java index 82c0b80547..fd010a7141 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapTransportAdaptor.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapTransportAdaptor.java @@ -19,6 +19,7 @@ import org.eclipse.californium.core.coap.Request; import org.eclipse.californium.core.coap.Response; import org.thingsboard.server.common.transport.adaptor.AdaptorException; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg; import org.thingsboard.server.transport.coap.CoapTransportResource; import java.util.UUID; @@ -45,4 +46,6 @@ public interface CoapTransportAdaptor { Response convertToPublish(CoapTransportResource.CoapSessionListener coapSessionListener, TransportProtos.ToServerRpcResponseMsg msg) throws AdaptorException; + ProvisionDeviceRequestMsg convertToProvisionRequestMsg(UUID sessionId, Request inbound) throws AdaptorException; + } diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java index 5c9c471570..28292f6c20 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java @@ -123,6 +123,16 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor { return response; } + @Override + public TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(UUID sessionId, Request inbound) throws AdaptorException { + String payload = validatePayload(sessionId, inbound, false); + try { + return JsonConverter.convertToProvisionRequestMsg(payload); + } catch (IllegalStateException | JsonSyntaxException ex) { + throw new AdaptorException(ex); + } + } + @Override public Response convertToPublish(CoapTransportResource.CoapSessionListener session, TransportProtos.GetAttributeResponseMsg msg) throws AdaptorException { if (msg.getClientAttributeListCount() == 0 && msg.getSharedAttributeListCount() == 0) { diff --git a/common/transport/http/pom.xml b/common/transport/http/pom.xml index 715cb3d169..040f7c372e 100644 --- a/common/transport/http/pom.xml +++ b/common/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java index 727600a626..cdc2791839 100644 --- a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java +++ b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java @@ -30,17 +30,21 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; +import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.transport.SessionMsgListener; import org.thingsboard.server.common.transport.TransportContext; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.common.transport.auth.SessionInfoCreator; +import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto; import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.SessionCloseNotificationProto; import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; import org.thingsboard.server.gen.transport.TransportProtos.SubscribeToAttributeUpdatesMsg; @@ -62,7 +66,7 @@ import java.util.function.Consumer; * @author Andrew Shvayka */ @RestController -@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.http.enabled}'=='true')") +@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.http.enabled}'=='true')") @RequestMapping("/api/v1") @Slf4j public class DeviceApiController { @@ -76,7 +80,7 @@ public class DeviceApiController { @RequestParam(value = "sharedKeys", required = false, defaultValue = "") String sharedKeys, HttpServletRequest httpRequest) { DeferredResult responseWriter = new DeferredResult<>(); - transportContext.getTransportService().process(ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), + transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { GetAttributeRequestMsg.Builder request = GetAttributeRequestMsg.newBuilder().setRequestId(0); List clientKeySet = !StringUtils.isEmpty(clientKeys) ? Arrays.asList(clientKeys.split(",")) : null; @@ -98,7 +102,7 @@ public class DeviceApiController { public DeferredResult postDeviceAttributes(@PathVariable("deviceToken") String deviceToken, @RequestBody String json, HttpServletRequest request) { DeferredResult responseWriter = new DeferredResult<>(); - transportContext.getTransportService().process(ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), + transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { TransportService transportService = transportContext.getTransportService(); transportService.process(sessionInfo, JsonConverter.convertToAttributesProto(new JsonParser().parse(json)), @@ -112,7 +116,7 @@ public class DeviceApiController { public DeferredResult postTelemetry(@PathVariable("deviceToken") String deviceToken, @RequestBody String json, HttpServletRequest request) { DeferredResult responseWriter = new DeferredResult(); - transportContext.getTransportService().process(ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), + transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { TransportService transportService = transportContext.getTransportService(); transportService.process(sessionInfo, JsonConverter.convertToTelemetryProto(new JsonParser().parse(json)), @@ -126,7 +130,7 @@ public class DeviceApiController { public DeferredResult claimDevice(@PathVariable("deviceToken") String deviceToken, @RequestBody(required = false) String json, HttpServletRequest request) { DeferredResult responseWriter = new DeferredResult<>(); - transportContext.getTransportService().process(ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), + transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { TransportService transportService = transportContext.getTransportService(); DeviceId deviceId = new DeviceId(new UUID(sessionInfo.getDeviceIdMSB(), sessionInfo.getDeviceIdLSB())); @@ -141,7 +145,7 @@ public class DeviceApiController { @RequestParam(value = "timeout", required = false, defaultValue = "0") long timeout, HttpServletRequest httpRequest) { DeferredResult responseWriter = new DeferredResult<>(); - transportContext.getTransportService().process(ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), + transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { TransportService transportService = transportContext.getTransportService(); transportService.registerSyncSession(sessionInfo, new HttpSessionListener(responseWriter), @@ -158,7 +162,7 @@ public class DeviceApiController { @PathVariable("requestId") Integer requestId, @RequestBody String json, HttpServletRequest request) { DeferredResult responseWriter = new DeferredResult(); - transportContext.getTransportService().process(ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), + transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { TransportService transportService = transportContext.getTransportService(); transportService.process(sessionInfo, ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId).setPayload(json).build(), new HttpOkCallback(responseWriter)); @@ -170,7 +174,7 @@ public class DeviceApiController { public DeferredResult postRpcRequest(@PathVariable("deviceToken") String deviceToken, @RequestBody String json, HttpServletRequest httpRequest) { DeferredResult responseWriter = new DeferredResult(); - transportContext.getTransportService().process(ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), + transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { JsonObject request = new JsonParser().parse(json).getAsJsonObject(); TransportService transportService = transportContext.getTransportService(); @@ -188,7 +192,7 @@ public class DeviceApiController { @RequestParam(value = "timeout", required = false, defaultValue = "0") long timeout, HttpServletRequest httpRequest) { DeferredResult responseWriter = new DeferredResult<>(); - transportContext.getTransportService().process(ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), + transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { TransportService transportService = transportContext.getTransportService(); transportService.registerSyncSession(sessionInfo, new HttpSessionListener(responseWriter), @@ -200,7 +204,15 @@ public class DeviceApiController { return responseWriter; } - private static class DeviceAuthCallback implements TransportServiceCallback { + @RequestMapping(value = "/provision", method = RequestMethod.POST) + public DeferredResult provisionDevice(@RequestBody String json, HttpServletRequest httpRequest) { + DeferredResult responseWriter = new DeferredResult<>(); + transportContext.getTransportService().process(JsonConverter.convertToProvisionRequestMsg(json), + new DeviceProvisionCallback(responseWriter)); + return responseWriter; + } + + private static class DeviceAuthCallback implements TransportServiceCallback { private final TransportContext transportContext; private final DeferredResult responseWriter; private final Consumer onSuccess; @@ -212,22 +224,9 @@ public class DeviceApiController { } @Override - public void onSuccess(ValidateDeviceCredentialsResponseMsg msg) { + public void onSuccess(ValidateDeviceCredentialsResponse msg) { if (msg.hasDeviceInfo()) { - UUID sessionId = UUID.randomUUID(); - DeviceInfoProto deviceInfoProto = msg.getDeviceInfo(); - SessionInfoProto sessionInfo = SessionInfoProto.newBuilder() - .setNodeId(transportContext.getNodeId()) - .setTenantIdMSB(deviceInfoProto.getTenantIdMSB()) - .setTenantIdLSB(deviceInfoProto.getTenantIdLSB()) - .setDeviceIdMSB(deviceInfoProto.getDeviceIdMSB()) - .setDeviceIdLSB(deviceInfoProto.getDeviceIdLSB()) - .setSessionIdMSB(sessionId.getMostSignificantBits()) - .setSessionIdLSB(sessionId.getLeastSignificantBits()) - .setDeviceName(msg.getDeviceInfo().getDeviceName()) - .setDeviceType(msg.getDeviceInfo().getDeviceType()) - .build(); - onSuccess.accept(sessionInfo); + onSuccess.accept(SessionInfoCreator.create(msg, transportContext, UUID.randomUUID())); } else { responseWriter.setResult(new ResponseEntity<>(HttpStatus.UNAUTHORIZED)); } @@ -240,6 +239,25 @@ public class DeviceApiController { } } + private static class DeviceProvisionCallback implements TransportServiceCallback { + private final DeferredResult responseWriter; + + DeviceProvisionCallback(DeferredResult responseWriter) { + this.responseWriter = responseWriter; + } + + @Override + public void onSuccess(ProvisionDeviceResponseMsg msg) { + responseWriter.setResult(new ResponseEntity<>(JsonConverter.toJson(msg).toString(), HttpStatus.OK)); + } + + @Override + public void onError(Throwable e) { + log.warn("Failed to process request", e); + responseWriter.setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); + } + } + private static class SessionCloseOnErrorCallback implements TransportServiceCallback { private final TransportService transportService; private final SessionInfoProto sessionInfo; diff --git a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/HttpTransportContext.java b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/HttpTransportContext.java index 28c38bdda6..3fbe135efa 100644 --- a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/HttpTransportContext.java +++ b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/HttpTransportContext.java @@ -29,7 +29,7 @@ import javax.annotation.PostConstruct; * Created by ashvayka on 04.10.18. */ @Slf4j -@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.http.enabled}'=='true')") +@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.http.enabled}'=='true')") @Component public class HttpTransportContext extends TransportContext { diff --git a/common/transport/mqtt/pom.xml b/common/transport/mqtt/pom.xml index e3579876e4..8a02c940a7 100644 --- a/common/transport/mqtt/pom.xml +++ b/common/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java index 0c872c351f..119f841c1e 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java @@ -24,9 +24,11 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; +import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.mqtt.util.SslUtil; @@ -156,12 +158,12 @@ public class MqttSslHandlerProvider { String sha3Hash = EncryptionUtil.getSha3Hash(strCert); final String[] credentialsBodyHolder = new String[1]; CountDownLatch latch = new CountDownLatch(1); - transportService.process(TransportProtos.ValidateDeviceX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(), - new TransportServiceCallback() { + transportService.process(DeviceTransportType.MQTT, TransportProtos.ValidateDeviceX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(), + new TransportServiceCallback() { @Override - public void onSuccess(TransportProtos.ValidateDeviceCredentialsResponseMsg msg) { - if (!StringUtils.isEmpty(msg.getCredentialsBody())) { - credentialsBodyHolder[0] = msg.getCredentialsBody(); + public void onSuccess(ValidateDeviceCredentialsResponse msg) { + if (!StringUtils.isEmpty(msg.getCredentials())) { + credentialsBodyHolder[0] = msg.getCredentials(); } latch.countDown(); } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTopics.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTopics.java deleted file mode 100644 index 946b906871..0000000000 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTopics.java +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.transport.mqtt; - -/** - * Created by ashvayka on 19.01.17. - */ -public class MqttTopics { - - public static final String BASE_DEVICE_API_TOPIC = "v1/devices/me"; - public static final String DEVICE_RPC_RESPONSE_TOPIC = BASE_DEVICE_API_TOPIC + "/rpc/response/"; - public static final String DEVICE_RPC_RESPONSE_SUB_TOPIC = DEVICE_RPC_RESPONSE_TOPIC + "+"; - public static final String DEVICE_RPC_REQUESTS_TOPIC = BASE_DEVICE_API_TOPIC + "/rpc/request/"; - public static final String DEVICE_RPC_REQUESTS_SUB_TOPIC = DEVICE_RPC_REQUESTS_TOPIC + "+"; - public static final String DEVICE_ATTRIBUTES_RESPONSE_TOPIC_PREFIX = BASE_DEVICE_API_TOPIC + "/attributes/response/"; - public static final String DEVICE_ATTRIBUTES_RESPONSES_TOPIC = DEVICE_ATTRIBUTES_RESPONSE_TOPIC_PREFIX + "+"; - public static final String DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX = BASE_DEVICE_API_TOPIC + "/attributes/request/"; - public static final String DEVICE_TELEMETRY_TOPIC = BASE_DEVICE_API_TOPIC + "/telemetry"; - public static final String DEVICE_CLAIM_TOPIC = BASE_DEVICE_API_TOPIC + "/claim"; - public static final String DEVICE_ATTRIBUTES_TOPIC = BASE_DEVICE_API_TOPIC + "/attributes"; - - public static final String BASE_GATEWAY_API_TOPIC = "v1/gateway"; - public static final String GATEWAY_CONNECT_TOPIC = BASE_GATEWAY_API_TOPIC + "/connect"; - public static final String GATEWAY_DISCONNECT_TOPIC = BASE_GATEWAY_API_TOPIC + "/disconnect"; - public static final String GATEWAY_ATTRIBUTES_TOPIC = BASE_GATEWAY_API_TOPIC + "/attributes"; - public static final String GATEWAY_TELEMETRY_TOPIC = BASE_GATEWAY_API_TOPIC + "/telemetry"; - public static final String GATEWAY_CLAIM_TOPIC = BASE_GATEWAY_API_TOPIC + "/claim"; - public static final String GATEWAY_RPC_TOPIC = BASE_GATEWAY_API_TOPIC + "/rpc"; - public static final String GATEWAY_ATTRIBUTES_REQUEST_TOPIC = BASE_GATEWAY_API_TOPIC + "/attributes/request"; - public static final String GATEWAY_ATTRIBUTES_RESPONSE_TOPIC = BASE_GATEWAY_API_TOPIC + "/attributes/response"; - - - private MqttTopics() { - } -} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java index 285e252e46..6695fa24ac 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java @@ -24,14 +24,15 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component; import org.thingsboard.server.common.transport.TransportContext; -import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; +import org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor; +import org.thingsboard.server.transport.mqtt.adaptors.ProtoMqttAdaptor; /** * Created by ashvayka on 04.10.18. */ @Slf4j @Component -@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.mqtt.enabled}'=='true')") +@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.mqtt.enabled}'=='true')") public class MqttTransportContext extends TransportContext { @Getter @@ -40,7 +41,11 @@ public class MqttTransportContext extends TransportContext { @Getter @Autowired - private MqttTransportAdaptor adaptor; + private JsonMqttAdaptor jsonMqttAdaptor; + + @Getter + @Autowired + private ProtoMqttAdaptor protoMqttAdaptor; @Getter @Value("${transport.mqtt.netty.max_payload_size}") diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index abb8f629f1..13e166e3e3 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.mqtt; import com.fasterxml.jackson.databind.JsonNode; +import com.google.gson.JsonParseException; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.mqtt.MqttConnAckMessage; @@ -38,20 +39,26 @@ import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.common.msg.EncryptionUtil; +import org.thingsboard.server.common.msg.tools.TbRateLimitsException; import org.thingsboard.server.common.transport.SessionMsgListener; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.adaptor.AdaptorException; +import org.thingsboard.server.common.transport.auth.SessionInfoCreator; +import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; +import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.common.transport.service.DefaultTransportService; import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.SessionEvent; -import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; -import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; +import org.thingsboard.server.queue.scheduler.SchedulerComponent; import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; import org.thingsboard.server.transport.mqtt.session.DeviceSessionCtx; import org.thingsboard.server.transport.mqtt.session.GatewaySessionHandler; @@ -67,11 +74,12 @@ import java.util.List; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; import static io.netty.handler.codec.mqtt.MqttConnectReturnCode.CONNECTION_ACCEPTED; -import static io.netty.handler.codec.mqtt.MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD; import static io.netty.handler.codec.mqtt.MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED; import static io.netty.handler.codec.mqtt.MqttMessageType.CONNACK; +import static io.netty.handler.codec.mqtt.MqttMessageType.CONNECT; import static io.netty.handler.codec.mqtt.MqttMessageType.PINGRESP; import static io.netty.handler.codec.mqtt.MqttMessageType.PUBACK; import static io.netty.handler.codec.mqtt.MqttMessageType.SUBACK; @@ -90,24 +98,23 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private final UUID sessionId; private final MqttTransportContext context; - private final MqttTransportAdaptor adaptor; private final TransportService transportService; + private final SchedulerComponent scheduler; private final SslHandler sslHandler; private final ConcurrentMap mqttQoSMap; - private volatile SessionInfoProto sessionInfo; + private final DeviceSessionCtx deviceSessionCtx; private volatile InetSocketAddress address; - private volatile DeviceSessionCtx deviceSessionCtx; private volatile GatewaySessionHandler gatewaySessionHandler; MqttTransportHandler(MqttTransportContext context, SslHandler sslHandler) { this.sessionId = UUID.randomUUID(); this.context = context; this.transportService = context.getTransportService(); - this.adaptor = context.getAdaptor(); + this.scheduler = context.getScheduler(); this.sslHandler = sslHandler; this.mqttQoSMap = new ConcurrentHashMap<>(); - this.deviceSessionCtx = new DeviceSessionCtx(sessionId, mqttQoSMap); + this.deviceSessionCtx = new DeviceSessionCtx(sessionId, mqttQoSMap, context); } @Override @@ -138,10 +145,58 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement return; } deviceSessionCtx.setChannel(ctx); + if (CONNECT.equals(msg.fixedHeader().messageType())) { + processConnect(ctx, (MqttConnectMessage) msg); + } else if (deviceSessionCtx.isProvisionOnly()) { + processProvisionSessionMsg(ctx, msg); + } else { + processRegularSessionMsg(ctx, msg); + } + } + + private void processProvisionSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) { switch (msg.fixedHeader().messageType()) { - case CONNECT: - processConnect(ctx, (MqttConnectMessage) msg); + case PUBLISH: + MqttPublishMessage mqttMsg = (MqttPublishMessage) msg; + String topicName = mqttMsg.variableHeader().topicName(); + int msgId = mqttMsg.variableHeader().packetId(); + try { + if (topicName.equals(MqttTopics.DEVICE_PROVISION_REQUEST_TOPIC)) { + try { + TransportProtos.ProvisionDeviceRequestMsg provisionRequestMsg = deviceSessionCtx.getContext().getJsonMqttAdaptor().convertToProvisionRequestMsg(deviceSessionCtx, mqttMsg); + transportService.process(provisionRequestMsg, new DeviceProvisionCallback(ctx, msgId, provisionRequestMsg)); + log.trace("[{}][{}] Processing provision publish msg [{}][{}]!", sessionId, deviceSessionCtx.getDeviceId(), topicName, msgId); + } catch (Exception e) { + if (e instanceof JsonParseException || (e.getCause() != null && e.getCause() instanceof JsonParseException)) { + TransportProtos.ProvisionDeviceRequestMsg provisionRequestMsg = deviceSessionCtx.getContext().getProtoMqttAdaptor().convertToProvisionRequestMsg(deviceSessionCtx, mqttMsg); + transportService.process(provisionRequestMsg, new DeviceProvisionCallback(ctx, msgId, provisionRequestMsg)); + deviceSessionCtx.setProvisionPayloadType(TransportPayloadType.PROTOBUF); + log.trace("[{}][{}] Processing provision publish msg [{}][{}]!", sessionId, deviceSessionCtx.getDeviceId(), topicName, msgId); + } else { + throw e; + } + } + } else { + throw new RuntimeException("Unsupported topic for provisioning requests!"); + } + } catch (RuntimeException | AdaptorException e) { + log.warn("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); + ctx.close(); + } + break; + case PINGREQ: + ctx.writeAndFlush(new MqttMessage(new MqttFixedHeader(PINGRESP, false, AT_MOST_ONCE, false, 0))); + break; + case DISCONNECT: + if (checkConnected(ctx, msg)) { + processDisconnect(ctx); + } break; + } + } + + private void processRegularSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) { + switch (msg.fixedHeader().messageType()) { case PUBLISH: processPublish(ctx, (MqttPublishMessage) msg); break; @@ -154,7 +209,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement case PINGREQ: if (checkConnected(ctx, msg)) { ctx.writeAndFlush(new MqttMessage(new MqttFixedHeader(PINGRESP, false, AT_MOST_ONCE, false, 0))); - transportService.reportActivity(sessionInfo); + transportService.reportActivity(deviceSessionCtx.getSessionInfo()); } break; case DISCONNECT: @@ -178,7 +233,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement if (topicName.startsWith(MqttTopics.BASE_GATEWAY_API_TOPIC)) { if (gatewaySessionHandler != null) { handleGatewayPublishMsg(topicName, msgId, mqttMsg); - transportService.reportActivity(sessionInfo); + transportService.reportActivity(deviceSessionCtx.getSessionInfo()); } } else { processDevicePublish(ctx, mqttMsg, topicName, msgId); @@ -217,26 +272,27 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private void processDevicePublish(ChannelHandlerContext ctx, MqttPublishMessage mqttMsg, String topicName, int msgId) { try { - if (topicName.equals(MqttTopics.DEVICE_TELEMETRY_TOPIC)) { - TransportProtos.PostTelemetryMsg postTelemetryMsg = adaptor.convertToPostTelemetry(deviceSessionCtx, mqttMsg); - transportService.process(sessionInfo, postTelemetryMsg, getPubAckCallback(ctx, msgId, postTelemetryMsg)); - } else if (topicName.equals(MqttTopics.DEVICE_ATTRIBUTES_TOPIC)) { - TransportProtos.PostAttributeMsg postAttributeMsg = adaptor.convertToPostAttributes(deviceSessionCtx, mqttMsg); - transportService.process(sessionInfo, postAttributeMsg, getPubAckCallback(ctx, msgId, postAttributeMsg)); + MqttTransportAdaptor payloadAdaptor = deviceSessionCtx.getPayloadAdaptor(); + if (deviceSessionCtx.isDeviceAttributesTopic(topicName)) { + TransportProtos.PostAttributeMsg postAttributeMsg = payloadAdaptor.convertToPostAttributes(deviceSessionCtx, mqttMsg); + transportService.process(deviceSessionCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(ctx, msgId, postAttributeMsg)); + } else if (deviceSessionCtx.isDeviceTelemetryTopic(topicName)) { + TransportProtos.PostTelemetryMsg postTelemetryMsg = payloadAdaptor.convertToPostTelemetry(deviceSessionCtx, mqttMsg); + transportService.process(deviceSessionCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(ctx, msgId, postTelemetryMsg)); } else if (topicName.startsWith(MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX)) { - TransportProtos.GetAttributeRequestMsg getAttributeMsg = adaptor.convertToGetAttributes(deviceSessionCtx, mqttMsg); - transportService.process(sessionInfo, getAttributeMsg, getPubAckCallback(ctx, msgId, getAttributeMsg)); + TransportProtos.GetAttributeRequestMsg getAttributeMsg = payloadAdaptor.convertToGetAttributes(deviceSessionCtx, mqttMsg); + transportService.process(deviceSessionCtx.getSessionInfo(), getAttributeMsg, getPubAckCallback(ctx, msgId, getAttributeMsg)); } else if (topicName.startsWith(MqttTopics.DEVICE_RPC_RESPONSE_TOPIC)) { - TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = adaptor.convertToDeviceRpcResponse(deviceSessionCtx, mqttMsg); - transportService.process(sessionInfo, rpcResponseMsg, getPubAckCallback(ctx, msgId, rpcResponseMsg)); + TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = payloadAdaptor.convertToDeviceRpcResponse(deviceSessionCtx, mqttMsg); + transportService.process(deviceSessionCtx.getSessionInfo(), rpcResponseMsg, getPubAckCallback(ctx, msgId, rpcResponseMsg)); } else if (topicName.startsWith(MqttTopics.DEVICE_RPC_REQUESTS_TOPIC)) { - TransportProtos.ToServerRpcRequestMsg rpcRequestMsg = adaptor.convertToServerRpcRequest(deviceSessionCtx, mqttMsg); - transportService.process(sessionInfo, rpcRequestMsg, getPubAckCallback(ctx, msgId, rpcRequestMsg)); + TransportProtos.ToServerRpcRequestMsg rpcRequestMsg = payloadAdaptor.convertToServerRpcRequest(deviceSessionCtx, mqttMsg); + transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequestMsg, getPubAckCallback(ctx, msgId, rpcRequestMsg)); } else if (topicName.equals(MqttTopics.DEVICE_CLAIM_TOPIC)) { - TransportProtos.ClaimDeviceMsg claimDeviceMsg = adaptor.convertToClaimDevice(deviceSessionCtx, mqttMsg); - transportService.process(sessionInfo, claimDeviceMsg, getPubAckCallback(ctx, msgId, claimDeviceMsg)); + TransportProtos.ClaimDeviceMsg claimDeviceMsg = payloadAdaptor.convertToClaimDevice(deviceSessionCtx, mqttMsg); + transportService.process(deviceSessionCtx.getSessionInfo(), claimDeviceMsg, getPubAckCallback(ctx, msgId, claimDeviceMsg)); } else { - transportService.reportActivity(sessionInfo); + transportService.reportActivity(deviceSessionCtx.getSessionInfo()); } } catch (AdaptorException e) { log.warn("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); @@ -245,6 +301,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } + private TransportServiceCallback getPubAckCallback(final ChannelHandlerContext ctx, final int msgId, final T msg) { return new TransportServiceCallback() { @Override @@ -263,6 +320,42 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement }; } + private class DeviceProvisionCallback implements TransportServiceCallback { + private final ChannelHandlerContext ctx; + private final int msgId; + private final TransportProtos.ProvisionDeviceRequestMsg msg; + + DeviceProvisionCallback(ChannelHandlerContext ctx, int msgId, TransportProtos.ProvisionDeviceRequestMsg msg) { + this.ctx = ctx; + this.msgId = msgId; + this.msg = msg; + } + + @Override + public void onSuccess(TransportProtos.ProvisionDeviceResponseMsg provisionResponseMsg) { + log.trace("[{}] Published msg: {}", sessionId, msg); + if (msgId > 0) { + ctx.writeAndFlush(createMqttPubAckMsg(msgId)); + } + try { + if (deviceSessionCtx.getProvisionPayloadType().equals(TransportPayloadType.JSON)) { + deviceSessionCtx.getContext().getJsonMqttAdaptor().convertToPublish(deviceSessionCtx, provisionResponseMsg).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); + } else { + deviceSessionCtx.getContext().getProtoMqttAdaptor().convertToPublish(deviceSessionCtx, provisionResponseMsg).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); + } + scheduler.schedule(() -> processDisconnect(ctx), 60, TimeUnit.SECONDS); + } catch (Exception e) { + log.trace("[{}] Failed to convert device attributes response to MQTT msg", sessionId, e); + } + } + + @Override + public void onError(Throwable e) { + log.trace("[{}] Failed to publish msg: {}", sessionId, msg, e); + processDisconnect(ctx); + } + } + private void processSubscribe(ChannelHandlerContext ctx, MqttSubscribeMessage mqttMsg) { if (!checkConnected(ctx, mqttMsg)) { return; @@ -276,22 +369,23 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement try { switch (topic) { case MqttTopics.DEVICE_ATTRIBUTES_TOPIC: { - transportService.process(sessionInfo, TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().build(), null); + transportService.process(deviceSessionCtx.getSessionInfo(), TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().build(), null); registerSubQoS(topic, grantedQoSList, reqQoS); activityReported = true; break; } case MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC: { - transportService.process(sessionInfo, TransportProtos.SubscribeToRPCMsg.newBuilder().build(), null); + transportService.process(deviceSessionCtx.getSessionInfo(), TransportProtos.SubscribeToRPCMsg.newBuilder().build(), null); registerSubQoS(topic, grantedQoSList, reqQoS); activityReported = true; break; } case MqttTopics.DEVICE_RPC_RESPONSE_SUB_TOPIC: + case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC: case MqttTopics.GATEWAY_ATTRIBUTES_TOPIC: case MqttTopics.GATEWAY_RPC_TOPIC: case MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC: - case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC: + case MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC: registerSubQoS(topic, grantedQoSList, reqQoS); break; default: @@ -305,7 +399,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } if (!activityReported) { - transportService.reportActivity(sessionInfo); + transportService.reportActivity(deviceSessionCtx.getSessionInfo()); } ctx.writeAndFlush(createSubAckMessage(mqttMsg.variableHeader().messageId(), grantedQoSList)); } @@ -326,12 +420,14 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement try { switch (topicName) { case MqttTopics.DEVICE_ATTRIBUTES_TOPIC: { - transportService.process(sessionInfo, TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().setUnsubscribe(true).build(), null); + transportService.process(deviceSessionCtx.getSessionInfo(), + TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().setUnsubscribe(true).build(), null); activityReported = true; break; } case MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC: { - transportService.process(sessionInfo, TransportProtos.SubscribeToRPCMsg.newBuilder().setUnsubscribe(true).build(), null); + transportService.process(deviceSessionCtx.getSessionInfo(), + TransportProtos.SubscribeToRPCMsg.newBuilder().setUnsubscribe(true).build(), null); activityReported = true; break; } @@ -341,7 +437,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } if (!activityReported) { - transportService.reportActivity(sessionInfo); + transportService.reportActivity(deviceSessionCtx.getSessionInfo()); } ctx.writeAndFlush(createUnSubAckMessage(mqttMsg.variableHeader().messageId())); } @@ -355,49 +451,58 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private void processConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) { log.info("[{}] Processing connect msg for client: {}!", sessionId, msg.payload().clientIdentifier()); - X509Certificate cert; - if (sslHandler != null && (cert = getX509Certificate()) != null) { - processX509CertConnect(ctx, cert); + String userName = msg.payload().userName(); + String clientId = msg.payload().clientIdentifier(); + if (DataConstants.PROVISION.equals(userName) || DataConstants.PROVISION.equals(clientId)) { + deviceSessionCtx.setProvisionOnly(true); + ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED)); } else { - processAuthTokenConnect(ctx, msg); + X509Certificate cert; + if (sslHandler != null && (cert = getX509Certificate()) != null) { + processX509CertConnect(ctx, cert); + } else { + processAuthTokenConnect(ctx, msg); + } } } private void processAuthTokenConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) { String userName = msg.payload().userName(); log.info("[{}] Processing connect msg for client with user name: {}!", sessionId, userName); - if (StringUtils.isEmpty(userName)) { - ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD)); - ctx.close(); - } else { - transportService.process(ValidateDeviceTokenRequestMsg.newBuilder().setToken(userName).build(), - new TransportServiceCallback() { - @Override - public void onSuccess(ValidateDeviceCredentialsResponseMsg msg) { - onValidateDeviceResponse(msg, ctx); - } - - @Override - public void onError(Throwable e) { - log.trace("[{}] Failed to process credentials: {}", address, userName, e); - ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE)); - ctx.close(); - } - }); + TransportProtos.ValidateBasicMqttCredRequestMsg.Builder request = TransportProtos.ValidateBasicMqttCredRequestMsg.newBuilder() + .setClientId(msg.payload().clientIdentifier()) + .setUserName(userName); + String password = msg.payload().password(); + if (password != null) { + request.setPassword(password); } + transportService.process(DeviceTransportType.MQTT, request.build(), + new TransportServiceCallback() { + @Override + public void onSuccess(ValidateDeviceCredentialsResponse msg) { + onValidateDeviceResponse(msg, ctx); + } + + @Override + public void onError(Throwable e) { + log.trace("[{}] Failed to process credentials: {}", address, userName, e); + ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE)); + ctx.close(); + } + }); } private void processX509CertConnect(ChannelHandlerContext ctx, X509Certificate cert) { try { - if(!context.isSkipValidityCheckForClientCert()){ + if (!context.isSkipValidityCheckForClientCert()) { cert.checkValidity(); } String strCert = SslUtil.getX509CertificateString(cert); String sha3Hash = EncryptionUtil.getSha3Hash(strCert); - transportService.process(ValidateDeviceX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(), - new TransportServiceCallback() { + transportService.process(DeviceTransportType.MQTT, ValidateDeviceX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(), + new TransportServiceCallback() { @Override - public void onSuccess(ValidateDeviceCredentialsResponseMsg msg) { + public void onSuccess(ValidateDeviceCredentialsResponse msg) { onValidateDeviceResponse(msg, ctx); } @@ -483,13 +588,13 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } private void checkGatewaySession() { - DeviceInfoProto device = deviceSessionCtx.getDeviceInfo(); + TransportDeviceInfo device = deviceSessionCtx.getDeviceInfo(); try { JsonNode infoNode = context.getMapper().readTree(device.getAdditionalInfo()); if (infoNode != null) { JsonNode gatewayNode = infoNode.get("gateway"); if (gatewayNode != null && gatewayNode.asBoolean()) { - gatewaySessionHandler = new GatewaySessionHandler(context, deviceSessionCtx, sessionId); + gatewaySessionHandler = new GatewaySessionHandler(deviceSessionCtx, sessionId); } } } catch (IOException e) { @@ -504,8 +609,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private void doDisconnect() { if (deviceSessionCtx.isConnected()) { - transportService.process(sessionInfo, DefaultTransportService.getSessionEventMsg(SessionEvent.CLOSED), null); - transportService.deregisterSession(sessionInfo); + transportService.process(deviceSessionCtx.getSessionInfo(), DefaultTransportService.getSessionEventMsg(SessionEvent.CLOSED), null); + transportService.deregisterSession(deviceSessionCtx.getSessionInfo()); if (gatewaySessionHandler != null) { gatewaySessionHandler.onGatewayDisconnect(); } @@ -513,27 +618,18 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } - private void onValidateDeviceResponse(ValidateDeviceCredentialsResponseMsg msg, ChannelHandlerContext ctx) { + private void onValidateDeviceResponse(ValidateDeviceCredentialsResponse msg, ChannelHandlerContext ctx) { if (!msg.hasDeviceInfo()) { ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_REFUSED_NOT_AUTHORIZED)); ctx.close(); } else { deviceSessionCtx.setDeviceInfo(msg.getDeviceInfo()); - sessionInfo = SessionInfoProto.newBuilder() - .setNodeId(context.getNodeId()) - .setSessionIdMSB(sessionId.getMostSignificantBits()) - .setSessionIdLSB(sessionId.getLeastSignificantBits()) - .setDeviceIdMSB(msg.getDeviceInfo().getDeviceIdMSB()) - .setDeviceIdLSB(msg.getDeviceInfo().getDeviceIdLSB()) - .setTenantIdMSB(msg.getDeviceInfo().getTenantIdMSB()) - .setTenantIdLSB(msg.getDeviceInfo().getTenantIdLSB()) - .setDeviceName(msg.getDeviceInfo().getDeviceName()) - .setDeviceType(msg.getDeviceInfo().getDeviceType()) - .build(); - transportService.process(sessionInfo, DefaultTransportService.getSessionEventMsg(SessionEvent.OPEN), new TransportServiceCallback() { + deviceSessionCtx.setDeviceProfile(msg.getDeviceProfile()); + deviceSessionCtx.setSessionInfo(SessionInfoCreator.create(msg, context, sessionId)); + transportService.process(deviceSessionCtx.getSessionInfo(), DefaultTransportService.getSessionEventMsg(SessionEvent.OPEN), new TransportServiceCallback() { @Override public void onSuccess(Void msg) { - transportService.registerAsyncSession(sessionInfo, MqttTransportHandler.this); + transportService.registerAsyncSession(deviceSessionCtx.getSessionInfo(), MqttTransportHandler.this); checkGatewaySession(); ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED)); log.info("[{}] Client connected!", sessionId); @@ -541,7 +637,11 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onError(Throwable e) { - log.warn("[{}] Failed to submit session event", sessionId, e); + if (e instanceof TbRateLimitsException) { + log.trace("[{}] Failed to submit session event", sessionId, e); + } else { + log.warn("[{}] Failed to submit session event", sessionId, e); + } ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE)); ctx.close(); } @@ -552,7 +652,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onGetAttributesResponse(TransportProtos.GetAttributeResponseMsg response) { try { - adaptor.convertToPublish(deviceSessionCtx, response).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); + deviceSessionCtx.getPayloadAdaptor().convertToPublish(deviceSessionCtx, response).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); } catch (Exception e) { log.trace("[{}] Failed to convert device attributes response to MQTT msg", sessionId, e); } @@ -561,7 +661,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onAttributeUpdate(TransportProtos.AttributeUpdateNotificationMsg notification) { try { - adaptor.convertToPublish(deviceSessionCtx, notification).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); + deviceSessionCtx.getPayloadAdaptor().convertToPublish(deviceSessionCtx, notification).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); } catch (Exception e) { log.trace("[{}] Failed to convert device attributes update to MQTT msg", sessionId, e); } @@ -577,19 +677,24 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement public void onToDeviceRpcRequest(TransportProtos.ToDeviceRpcRequestMsg rpcRequest) { log.trace("[{}] Received RPC command to device", sessionId); try { - adaptor.convertToPublish(deviceSessionCtx, rpcRequest).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); + deviceSessionCtx.getPayloadAdaptor().convertToPublish(deviceSessionCtx, rpcRequest).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); } catch (Exception e) { - log.trace("[{}] Failed to convert device RPC commandto MQTT msg", sessionId, e); + log.trace("[{}] Failed to convert device RPC command to MQTT msg", sessionId, e); } } @Override public void onToServerRpcResponse(TransportProtos.ToServerRpcResponseMsg rpcResponse) { - log.trace("[{}] Received RPC command to device", sessionId); + log.trace("[{}] Received RPC command to server", sessionId); try { - adaptor.convertToPublish(deviceSessionCtx, rpcResponse).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); + deviceSessionCtx.getPayloadAdaptor().convertToPublish(deviceSessionCtx, rpcResponse).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); } catch (Exception e) { - log.trace("[{}] Failed to convert device RPC commandto MQTT msg", sessionId, e); + log.trace("[{}] Failed to convert device RPC command to MQTT msg", sessionId, e); } } + + @Override + public void onProfileUpdate(DeviceProfile deviceProfile) { + deviceSessionCtx.onProfileUpdate(deviceProfile); + } } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java index fb8a0b70be..dbe6328adb 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java @@ -36,7 +36,7 @@ import javax.annotation.PreDestroy; * @author Andrew Shvayka */ @Service("MqttTransportService") -@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.mqtt.enabled}'=='true')") +@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.mqtt.enabled}'=='true')") @Slf4j public class MqttTransportService { diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java index caa627451f..0799d29c57 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java @@ -34,10 +34,11 @@ import org.springframework.util.StringUtils; import org.thingsboard.server.common.transport.adaptor.AdaptorException; import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.transport.mqtt.MqttTopics; +import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.transport.mqtt.session.MqttDeviceAwareSessionContext; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.HashSet; import java.util.Optional; @@ -47,12 +48,13 @@ import java.util.UUID; /** * @author Andrew Shvayka */ -@Component("JsonMqttAdaptor") +@Component @Slf4j public class JsonMqttAdaptor implements MqttTransportAdaptor { + protected static final Charset UTF8 = StandardCharsets.UTF_8; + private static final Gson GSON = new Gson(); - private static final Charset UTF8 = Charset.forName("UTF-8"); private static final ByteBufAllocator ALLOCATOR = new UnpooledByteBufAllocator(false); @Override @@ -75,12 +77,97 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { } } + @Override + public TransportProtos.ClaimDeviceMsg convertToClaimDevice(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { + String payload = validatePayload(ctx.getSessionId(), inbound.payload(), true); + try { + return JsonConverter.convertToClaimDeviceProto(ctx.getDeviceId(), payload); + } catch (IllegalStateException | JsonSyntaxException ex) { + throw new AdaptorException(ex); + } + } + + @Override + public TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { + String payload = validatePayload(ctx.getSessionId(), inbound.payload(), false); + try { + return JsonConverter.convertToProvisionRequestMsg(payload); + } catch (IllegalStateException | JsonSyntaxException ex) { + throw new AdaptorException(ex); + } + } + @Override public TransportProtos.GetAttributeRequestMsg convertToGetAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { + return processGetAttributeRequestMsg(inbound, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX); + } + + @Override + public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { + return processToDeviceRpcResponseMsg(inbound, MqttTopics.DEVICE_RPC_RESPONSE_TOPIC); + } + + @Override + public TransportProtos.ToServerRpcRequestMsg convertToServerRpcRequest(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { + return processToServerRpcRequestMsg(ctx, inbound, MqttTopics.DEVICE_RPC_REQUESTS_TOPIC); + } + + @Override + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { + return processConvertFromAttributeResponseMsg(ctx, responseMsg, MqttTopics.DEVICE_ATTRIBUTES_RESPONSE_TOPIC_PREFIX); + } + + @Override + public Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { + return processConvertFromGatewayAttributeResponseMsg(ctx, deviceName, responseMsg, MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC); + } + + @Override + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.AttributeUpdateNotificationMsg notificationMsg) { + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_ATTRIBUTES_TOPIC, JsonConverter.toJson(notificationMsg))); + } + + @Override + public Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.AttributeUpdateNotificationMsg notificationMsg) { + JsonObject result = JsonConverter.getJsonObjectForGateway(deviceName, notificationMsg); + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.GATEWAY_ATTRIBUTES_TOPIC, result)); + } + + @Override + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) { + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_RPC_REQUESTS_TOPIC + rpcRequest.getRequestId(), JsonConverter.toJson(rpcRequest, false))); + } + + @Override + public Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) { + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.GATEWAY_RPC_TOPIC, JsonConverter.toGatewayJson(deviceName, rpcRequest))); + } + + @Override + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToServerRpcResponseMsg rpcResponse) { + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_RPC_RESPONSE_TOPIC + rpcResponse.getRequestId(), JsonConverter.toJson(rpcResponse))); + } + + @Override + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ProvisionDeviceResponseMsg provisionResponse) { + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC, JsonConverter.toJson(provisionResponse))); + } + + public static JsonElement validateJsonPayload(UUID sessionId, ByteBuf payloadData) throws AdaptorException { + String payload = validatePayload(sessionId, payloadData, false); + try { + return new JsonParser().parse(payload); + } catch (JsonSyntaxException ex) { + log.warn("Payload is in incorrect format: {}", payload); + throw new AdaptorException(ex); + } + } + + protected TransportProtos.GetAttributeRequestMsg processGetAttributeRequestMsg(MqttPublishMessage inbound, String topic) throws AdaptorException { String topicName = inbound.variableHeader().topicName(); try { TransportProtos.GetAttributeRequestMsg.Builder result = TransportProtos.GetAttributeRequestMsg.newBuilder(); - result.setRequestId(Integer.valueOf(topicName.substring(MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX.length()))); + result.setRequestId(getRequestId(topicName, topic)); String payload = inbound.payload().toString(UTF8); JsonElement requestBody = new JsonParser().parse(payload); Set clientKeys = toStringSet(requestBody, "clientKeys"); @@ -98,93 +185,53 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { } } - @Override - public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { + protected TransportProtos.ToDeviceRpcResponseMsg processToDeviceRpcResponseMsg(MqttPublishMessage inbound, String topic) throws AdaptorException { String topicName = inbound.variableHeader().topicName(); try { - Integer requestId = Integer.valueOf(topicName.substring(MqttTopics.DEVICE_RPC_RESPONSE_TOPIC.length())); + int requestId = getRequestId(topicName, topic); String payload = inbound.payload().toString(UTF8); return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId).setPayload(payload).build(); } catch (RuntimeException e) { - log.warn("Failed to decode get attributes request", e); + log.warn("Failed to decode Rpc response", e); throw new AdaptorException(e); } } - @Override - public TransportProtos.ToServerRpcRequestMsg convertToServerRpcRequest(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { + protected TransportProtos.ToServerRpcRequestMsg processToServerRpcRequestMsg(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound, String topic) throws AdaptorException { String topicName = inbound.variableHeader().topicName(); String payload = validatePayload(ctx.getSessionId(), inbound.payload(), false); try { - Integer requestId = Integer.valueOf(topicName.substring(MqttTopics.DEVICE_RPC_REQUESTS_TOPIC.length())); + int requestId = getRequestId(topicName, topic); return JsonConverter.convertToServerRpcRequest(new JsonParser().parse(payload), requestId); } catch (IllegalStateException | JsonSyntaxException ex) { throw new AdaptorException(ex); } } - @Override - public TransportProtos.ClaimDeviceMsg convertToClaimDevice(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { - String payload = validatePayload(ctx.getSessionId(), inbound.payload(), true); - try { - return JsonConverter.convertToClaimDeviceProto(ctx.getDeviceId(), payload); - } catch (IllegalStateException | JsonSyntaxException ex) { - throw new AdaptorException(ex); - } - } - - @Override - public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { + protected Optional processConvertFromAttributeResponseMsg(MqttDeviceAwareSessionContext ctx, TransportProtos.GetAttributeResponseMsg responseMsg, String topic) throws AdaptorException { if (!StringUtils.isEmpty(responseMsg.getError())) { throw new AdaptorException(responseMsg.getError()); } else { - Integer requestId = responseMsg.getRequestId(); + int requestId = responseMsg.getRequestId(); if (requestId >= 0) { return Optional.of(createMqttPublishMsg(ctx, - MqttTopics.DEVICE_ATTRIBUTES_RESPONSE_TOPIC_PREFIX + requestId, + topic + requestId, JsonConverter.toJson(responseMsg))); } return Optional.empty(); } } - @Override - public Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { + protected Optional processConvertFromGatewayAttributeResponseMsg(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg, String topic) throws AdaptorException { if (!StringUtils.isEmpty(responseMsg.getError())) { throw new AdaptorException(responseMsg.getError()); } else { JsonObject result = JsonConverter.getJsonObjectForGateway(deviceName, responseMsg); - return Optional.of(createMqttPublishMsg(ctx, MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC, result)); + return Optional.of(createMqttPublishMsg(ctx, topic, result)); } } - @Override - public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.AttributeUpdateNotificationMsg notificationMsg) throws AdaptorException { - return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_ATTRIBUTES_TOPIC, JsonConverter.toJson(notificationMsg))); - } - - @Override - public Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.AttributeUpdateNotificationMsg notificationMsg) throws AdaptorException { - JsonObject result = JsonConverter.getJsonObjectForGateway(deviceName, notificationMsg); - return Optional.of(createMqttPublishMsg(ctx, MqttTopics.GATEWAY_ATTRIBUTES_TOPIC, result)); - } - - @Override - public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) throws AdaptorException { - return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_RPC_REQUESTS_TOPIC + rpcRequest.getRequestId(), JsonConverter.toJson(rpcRequest, false))); - } - - @Override - public Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) throws AdaptorException { - return Optional.of(createMqttPublishMsg(ctx, MqttTopics.GATEWAY_RPC_TOPIC, JsonConverter.toGatewayJson(deviceName, rpcRequest))); - } - - @Override - public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToServerRpcResponseMsg rpcResponse) { - return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_RPC_RESPONSE_TOPIC + rpcResponse.getRequestId(), JsonConverter.toJson(rpcResponse))); - } - - private MqttPublishMessage createMqttPublishMsg(MqttDeviceAwareSessionContext ctx, String topic, JsonElement json) { + protected MqttPublishMessage createMqttPublishMsg(MqttDeviceAwareSessionContext ctx, String topic, JsonElement json) { MqttFixedHeader mqttFixedHeader = new MqttFixedHeader(MqttMessageType.PUBLISH, false, ctx.getQoSForTopic(topic), false, 0); MqttPublishVariableHeader header = new MqttPublishVariableHeader(topic, ctx.nextMsgId()); @@ -202,16 +249,6 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { } } - public static JsonElement validateJsonPayload(UUID sessionId, ByteBuf payloadData) throws AdaptorException { - String payload = validatePayload(sessionId, payloadData, false); - try { - return new JsonParser().parse(payload); - } catch (JsonSyntaxException ex) { - log.warn("Payload is in incorrect format: {}", payload); - throw new AdaptorException(ex); - } - } - private static String validatePayload(UUID sessionId, ByteBuf payloadData, boolean isEmptyPayloadAllowed) throws AdaptorException { String payload = payloadData.toString(UTF8); if (payload == null) { @@ -223,4 +260,8 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { return payload; } + private int getRequestId(String topicName, String topic) { + return Integer.parseInt(topicName.substring(topic.length())); + } + } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java index d4d36320f3..bcb2401ff7 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java @@ -24,6 +24,8 @@ import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestM import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.PostAttributeMsg; import org.thingsboard.server.gen.transport.TransportProtos.PostTelemetryMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcRequestMsg; @@ -63,4 +65,8 @@ public interface MqttTransportAdaptor { Optional convertToPublish(MqttDeviceAwareSessionContext ctx, ToServerRpcResponseMsg rpcResponse) throws AdaptorException; + ProvisionDeviceRequestMsg convertToProvisionRequestMsg(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException; + + Optional convertToPublish(MqttDeviceAwareSessionContext ctx, ProvisionDeviceResponseMsg provisionResponse) throws AdaptorException; + } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java new file mode 100644 index 0000000000..1a0d33cde4 --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java @@ -0,0 +1,210 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.adaptors; + +import com.google.protobuf.InvalidProtocolBufferException; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.UnpooledByteBufAllocator; +import io.netty.handler.codec.mqtt.MqttFixedHeader; +import io.netty.handler.codec.mqtt.MqttMessage; +import io.netty.handler.codec.mqtt.MqttMessageType; +import io.netty.handler.codec.mqtt.MqttPublishMessage; +import io.netty.handler.codec.mqtt.MqttPublishVariableHeader; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.common.transport.adaptor.AdaptorException; +import org.thingsboard.server.common.transport.adaptor.ProtoConverter; +import org.thingsboard.server.gen.transport.TransportApiProtos; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; +import org.thingsboard.server.transport.mqtt.session.MqttDeviceAwareSessionContext; + +import java.util.Optional; + +@Component +@Slf4j +public class ProtoMqttAdaptor implements MqttTransportAdaptor { + + private static final ByteBufAllocator ALLOCATOR = new UnpooledByteBufAllocator(false); + + @Override + public TransportProtos.PostTelemetryMsg convertToPostTelemetry(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { + byte[] bytes = toBytes(inbound.payload()); + try { + return ProtoConverter.convertToTelemetryProto(bytes); + } catch (InvalidProtocolBufferException | IllegalArgumentException e) { + throw new AdaptorException(e); + } + } + + @Override + public TransportProtos.PostAttributeMsg convertToPostAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { + byte[] bytes = toBytes(inbound.payload()); + try { + return ProtoConverter.validatePostAttributeMsg(bytes); + } catch (InvalidProtocolBufferException | IllegalArgumentException e) { + throw new AdaptorException(e); + } + } + + @Override + public TransportProtos.ClaimDeviceMsg convertToClaimDevice(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { + byte[] bytes = toBytes(inbound.payload()); + try { + return ProtoConverter.convertToClaimDeviceProto(ctx.getDeviceId(), bytes); + } catch (InvalidProtocolBufferException e) { + throw new AdaptorException(e); + } + } + + @Override + public TransportProtos.GetAttributeRequestMsg convertToGetAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { + byte[] bytes = toBytes(inbound.payload()); + String topicName = inbound.variableHeader().topicName(); + int requestId = getRequestId(topicName, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX); + try { + return ProtoConverter.convertToGetAttributeRequestMessage(bytes, requestId); + } catch (InvalidProtocolBufferException e) { + log.warn("Failed to decode get attributes request", e); + throw new AdaptorException(e); + } + } + + @Override + public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(MqttDeviceAwareSessionContext ctx, MqttPublishMessage mqttMsg) throws AdaptorException { + byte[] bytes = toBytes(mqttMsg.payload()); + try { + return TransportProtos.ToDeviceRpcResponseMsg.parseFrom(bytes); + } catch (RuntimeException | InvalidProtocolBufferException e) { + log.warn("Failed to decode Rpc response", e); + throw new AdaptorException(e); + } + } + + @Override + public TransportProtos.ToServerRpcRequestMsg convertToServerRpcRequest(MqttDeviceAwareSessionContext ctx, MqttPublishMessage mqttMsg) throws AdaptorException { + byte[] bytes = toBytes(mqttMsg.payload()); + String topicName = mqttMsg.variableHeader().topicName(); + try { + int requestId = getRequestId(topicName, MqttTopics.DEVICE_RPC_REQUESTS_TOPIC); + return ProtoConverter.convertToServerRpcRequest(bytes, requestId); + } catch (InvalidProtocolBufferException e) { + throw new AdaptorException(e); + } + } + + @Override + public TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(MqttDeviceAwareSessionContext ctx, MqttPublishMessage mqttMsg) throws AdaptorException { + byte[] bytes = toBytes(mqttMsg.payload()); + String topicName = mqttMsg.variableHeader().topicName(); + try { + return ProtoConverter.convertToProvisionRequestMsg(bytes); + } catch (InvalidProtocolBufferException ex) { + throw new AdaptorException(ex); + } + } + + @Override + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { + if (!StringUtils.isEmpty(responseMsg.getError())) { + throw new AdaptorException(responseMsg.getError()); + } else { + int requestId = responseMsg.getRequestId(); + if (requestId >= 0) { + return Optional.of(createMqttPublishMsg(ctx, + MqttTopics.DEVICE_ATTRIBUTES_RESPONSE_TOPIC_PREFIX + requestId, + responseMsg.toByteArray())); + } + return Optional.empty(); + } + } + + + @Override + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) { + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_RPC_REQUESTS_TOPIC + rpcRequest.getRequestId(), rpcRequest.toByteArray())); + } + + @Override + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToServerRpcResponseMsg rpcResponse) { + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_RPC_RESPONSE_TOPIC + rpcResponse.getRequestId(), rpcResponse.toByteArray())); + } + + @Override + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.AttributeUpdateNotificationMsg notificationMsg) { + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_ATTRIBUTES_TOPIC, notificationMsg.toByteArray())); + } + + @Override + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ProvisionDeviceResponseMsg provisionResponse) { + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC, provisionResponse.toByteArray())); + } + + @Override + public Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { + if (!StringUtils.isEmpty(responseMsg.getError())) { + throw new AdaptorException(responseMsg.getError()); + } else { + TransportApiProtos.GatewayAttributeResponseMsg.Builder responseMsgBuilder = TransportApiProtos.GatewayAttributeResponseMsg.newBuilder(); + responseMsgBuilder.setDeviceName(deviceName); + responseMsgBuilder.setResponseMsg(responseMsg); + byte[] payloadBytes = responseMsgBuilder.build().toByteArray(); + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC, payloadBytes)); + } + } + + @Override + public Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.AttributeUpdateNotificationMsg notificationMsg) { + TransportApiProtos.GatewayAttributeUpdateNotificationMsg.Builder builder = TransportApiProtos.GatewayAttributeUpdateNotificationMsg.newBuilder(); + builder.setDeviceName(deviceName); + builder.setNotificationMsg(notificationMsg); + byte[] payloadBytes = builder.build().toByteArray(); + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.GATEWAY_ATTRIBUTES_TOPIC, payloadBytes)); + } + + @Override + public Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) { + TransportApiProtos.GatewayDeviceRpcRequestMsg.Builder builder = TransportApiProtos.GatewayDeviceRpcRequestMsg.newBuilder(); + builder.setDeviceName(deviceName); + builder.setRpcRequestMsg(rpcRequest); + byte[] payloadBytes = builder.build().toByteArray(); + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.GATEWAY_RPC_TOPIC, payloadBytes)); + } + + public static byte[] toBytes(ByteBuf inbound) { + byte[] bytes = new byte[inbound.readableBytes()]; + int readerIndex = inbound.readerIndex(); + inbound.getBytes(readerIndex, bytes); + return bytes; + } + + private MqttPublishMessage createMqttPublishMsg(MqttDeviceAwareSessionContext ctx, String topic, byte[] payloadBytes) { + MqttFixedHeader mqttFixedHeader = + new MqttFixedHeader(MqttMessageType.PUBLISH, false, ctx.getQoSForTopic(topic), false, 0); + MqttPublishVariableHeader header = new MqttPublishVariableHeader(topic, ctx.nextMsgId()); + ByteBuf payload = ALLOCATOR.buffer(); + payload.writeBytes(payloadBytes); + return new MqttPublishMessage(mqttFixedHeader, header, payload); + } + + private int getRequestId(String topicName, String topic) { + return Integer.parseInt(topicName.substring(topic.length())); + } + +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java index d8732802db..20ddfb5a24 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java @@ -17,7 +17,17 @@ package org.thingsboard.server.transport.mqtt.session; import io.netty.channel.ChannelHandlerContext; import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; +import org.thingsboard.server.transport.mqtt.MqttTransportContext; +import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; +import org.thingsboard.server.transport.mqtt.util.MqttTopicFilter; +import org.thingsboard.server.transport.mqtt.util.MqttTopicFilterFactory; import java.util.UUID; import java.util.concurrent.ConcurrentMap; @@ -31,10 +41,27 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { @Getter private ChannelHandlerContext channel; - private AtomicInteger msgIdSeq = new AtomicInteger(0); - public DeviceSessionCtx(UUID sessionId, ConcurrentMap mqttQoSMap) { + @Getter + private MqttTransportContext context; + + private final AtomicInteger msgIdSeq = new AtomicInteger(0); + + @Getter + @Setter + private boolean provisionOnly = false; + + private volatile MqttTopicFilter telemetryTopicFilter = MqttTopicFilterFactory.getDefaultTelemetryFilter(); + private volatile MqttTopicFilter attributesTopicFilter = MqttTopicFilterFactory.getDefaultAttributesFilter(); + private volatile TransportPayloadType payloadType = TransportPayloadType.JSON; + + @Getter + @Setter + private TransportPayloadType provisionPayloadType = payloadType; + + public DeviceSessionCtx(UUID sessionId, ConcurrentMap mqttQoSMap, MqttTransportContext context) { super(sessionId, mqttQoSMap); + this.context = context; } public void setChannel(ChannelHandlerContext channel) { @@ -44,4 +71,46 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { public int nextMsgId() { return msgIdSeq.incrementAndGet(); } + + public boolean isDeviceTelemetryTopic(String topicName) { return telemetryTopicFilter.filter(topicName); } + + public boolean isDeviceAttributesTopic(String topicName) { + return attributesTopicFilter.filter(topicName); + } + + public MqttTransportAdaptor getPayloadAdaptor() { + return payloadType.equals(TransportPayloadType.JSON) ? context.getJsonMqttAdaptor() : context.getProtoMqttAdaptor(); + } + + public boolean isJsonPayloadType() { + return payloadType.equals(TransportPayloadType.JSON); + } + + @Override + public void setDeviceProfile(DeviceProfile deviceProfile) { + super.setDeviceProfile(deviceProfile); + updateTopicFilters(deviceProfile); + } + + @Override + public void onProfileUpdate(DeviceProfile deviceProfile) { + super.onProfileUpdate(deviceProfile); + updateTopicFilters(deviceProfile); + } + + + private void updateTopicFilters(DeviceProfile deviceProfile) { + DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); + if (transportConfiguration.getType().equals(DeviceTransportType.MQTT) && + transportConfiguration instanceof MqttDeviceProfileTransportConfiguration) { + MqttDeviceProfileTransportConfiguration mqttConfig = (MqttDeviceProfileTransportConfiguration) transportConfiguration; + payloadType = mqttConfig.getTransportPayloadType(); + telemetryTopicFilter = MqttTopicFilterFactory.toFilter(mqttConfig.getDeviceTelemetryTopic()); + attributesTopicFilter = MqttTopicFilterFactory.toFilter(mqttConfig.getDeviceAttributesTopic()); + } else { + telemetryTopicFilter = MqttTopicFilterFactory.getDefaultTelemetryFilter(); + attributesTopicFilter = MqttTopicFilterFactory.getDefaultAttributesFilter(); + } + } + } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java index c137da0b82..da93405e63 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java @@ -16,9 +16,10 @@ package org.thingsboard.server.transport.mqtt.session; import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.transport.SessionMsgListener; +import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto; import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; import java.util.UUID; @@ -31,25 +32,28 @@ import java.util.concurrent.ConcurrentMap; public class GatewayDeviceSessionCtx extends MqttDeviceAwareSessionContext implements SessionMsgListener { private final GatewaySessionHandler parent; - private final SessionInfoProto sessionInfo; - public GatewayDeviceSessionCtx(GatewaySessionHandler parent, DeviceInfoProto deviceInfo, ConcurrentMap mqttQoSMap) { + public GatewayDeviceSessionCtx(GatewaySessionHandler parent, TransportDeviceInfo deviceInfo, + DeviceProfile deviceProfile, ConcurrentMap mqttQoSMap) { super(UUID.randomUUID(), mqttQoSMap); this.parent = parent; - this.sessionInfo = SessionInfoProto.newBuilder() + setSessionInfo(SessionInfoProto.newBuilder() .setNodeId(parent.getNodeId()) .setSessionIdMSB(sessionId.getMostSignificantBits()) .setSessionIdLSB(sessionId.getLeastSignificantBits()) - .setDeviceIdMSB(deviceInfo.getDeviceIdMSB()) - .setDeviceIdLSB(deviceInfo.getDeviceIdLSB()) - .setTenantIdMSB(deviceInfo.getTenantIdMSB()) - .setTenantIdLSB(deviceInfo.getTenantIdLSB()) + .setDeviceIdMSB(deviceInfo.getDeviceId().getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceInfo.getDeviceId().getId().getLeastSignificantBits()) + .setTenantIdMSB(deviceInfo.getTenantId().getId().getMostSignificantBits()) + .setTenantIdLSB(deviceInfo.getTenantId().getId().getLeastSignificantBits()) .setDeviceName(deviceInfo.getDeviceName()) .setDeviceType(deviceInfo.getDeviceType()) .setGwSessionIdMSB(parent.getSessionId().getMostSignificantBits()) .setGwSessionIdLSB(parent.getSessionId().getLeastSignificantBits()) - .build(); + .setDeviceProfileIdMSB(deviceInfo.getDeviceProfileId().getId().getMostSignificantBits()) + .setDeviceProfileIdLSB(deviceInfo.getDeviceProfileId().getId().getLeastSignificantBits()) + .build()); setDeviceInfo(deviceInfo); + setDeviceProfile(deviceProfile); } @Override @@ -62,14 +66,10 @@ public class GatewayDeviceSessionCtx extends MqttDeviceAwareSessionContext imple return parent.nextMsgId(); } - SessionInfoProto getSessionInfo() { - return sessionInfo; - } - @Override public void onGetAttributesResponse(TransportProtos.GetAttributeResponseMsg response) { try { - parent.getAdaptor().convertToGatewayPublish(this, getDeviceInfo().getDeviceName(), response).ifPresent(parent::writeAndFlush); + parent.getPayloadAdaptor().convertToGatewayPublish(this, getDeviceInfo().getDeviceName(), response).ifPresent(parent::writeAndFlush); } catch (Exception e) { log.trace("[{}] Failed to convert device attributes response to MQTT msg", sessionId, e); } @@ -78,26 +78,26 @@ public class GatewayDeviceSessionCtx extends MqttDeviceAwareSessionContext imple @Override public void onAttributeUpdate(TransportProtos.AttributeUpdateNotificationMsg notification) { try { - parent.getAdaptor().convertToGatewayPublish(this, getDeviceInfo().getDeviceName(), notification).ifPresent(parent::writeAndFlush); + parent.getPayloadAdaptor().convertToGatewayPublish(this, getDeviceInfo().getDeviceName(), notification).ifPresent(parent::writeAndFlush); } catch (Exception e) { log.trace("[{}] Failed to convert device attributes response to MQTT msg", sessionId, e); } } - @Override - public void onRemoteSessionCloseCommand(TransportProtos.SessionCloseNotificationProto sessionCloseNotification) { - parent.deregisterSession(getDeviceInfo().getDeviceName()); - } - @Override public void onToDeviceRpcRequest(TransportProtos.ToDeviceRpcRequestMsg request) { try { - parent.getAdaptor().convertToGatewayPublish(this, getDeviceInfo().getDeviceName(), request).ifPresent(parent::writeAndFlush); + parent.getPayloadAdaptor().convertToGatewayPublish(this, getDeviceInfo().getDeviceName(), request).ifPresent(parent::writeAndFlush); } catch (Exception e) { log.trace("[{}] Failed to convert device attributes response to MQTT msg", sessionId, e); } } + @Override + public void onRemoteSessionCloseCommand(TransportProtos.SessionCloseNotificationProto sessionCloseNotification) { + parent.deregisterSession(getDeviceInfo().getDeviceName()); + } + @Override public void onToServerRpcResponse(TransportProtos.ToServerRpcResponseMsg toServerResponse) { // This feature is not supported in the TB IoT Gateway yet. diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java index caa434e4da..36f78da63a 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java @@ -25,30 +25,38 @@ import com.google.gson.JsonElement; import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.gson.JsonSyntaxException; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.ProtocolStringList; +import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.mqtt.MqttMessage; import io.netty.handler.codec.mqtt.MqttPublishMessage; import lombok.extern.slf4j.Slf4j; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.adaptor.AdaptorException; import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.common.transport.adaptor.ProtoConverter; +import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; +import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; import org.thingsboard.server.common.transport.service.DefaultTransportService; +import org.thingsboard.server.gen.transport.TransportApiProtos; import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto; import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg; -import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; import org.thingsboard.server.transport.mqtt.MqttTransportContext; import org.thingsboard.server.transport.mqtt.MqttTransportHandler; import org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor; import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; +import org.thingsboard.server.transport.mqtt.adaptors.ProtoMqttAdaptor; import javax.annotation.Nullable; import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; @@ -69,31 +77,130 @@ public class GatewaySessionHandler { private final MqttTransportContext context; private final TransportService transportService; - private final DeviceInfoProto gateway; + private final TransportDeviceInfo gateway; private final UUID sessionId; - private final Lock deviceCreationLock = new ReentrantLock(); + private final ConcurrentMap deviceCreationLockMap; private final ConcurrentMap devices; private final ConcurrentMap> deviceFutures; private final ConcurrentMap mqttQoSMap; private final ChannelHandlerContext channel; private final DeviceSessionCtx deviceSessionCtx; - public GatewaySessionHandler(MqttTransportContext context, DeviceSessionCtx deviceSessionCtx, UUID sessionId) { - this.context = context; + public GatewaySessionHandler(DeviceSessionCtx deviceSessionCtx, UUID sessionId) { + this.context = deviceSessionCtx.getContext(); this.transportService = context.getTransportService(); this.deviceSessionCtx = deviceSessionCtx; this.gateway = deviceSessionCtx.getDeviceInfo(); this.sessionId = sessionId; this.devices = new ConcurrentHashMap<>(); this.deviceFutures = new ConcurrentHashMap<>(); + this.deviceCreationLockMap = new ConcurrentHashMap<>(); this.mqttQoSMap = deviceSessionCtx.getMqttQoSMap(); this.channel = deviceSessionCtx.getChannel(); } - public void onDeviceConnect(MqttPublishMessage msg) throws AdaptorException { - JsonElement json = getJson(msg); - String deviceName = checkDeviceName(getDeviceName(json)); - String deviceType = getDeviceType(json); + public void onDeviceConnect(MqttPublishMessage mqttMsg) throws AdaptorException { + if (isJsonPayloadType()) { + onDeviceConnectJson(mqttMsg); + } else { + onDeviceConnectProto(mqttMsg); + } + } + + public void onDeviceDisconnect(MqttPublishMessage mqttMsg) throws AdaptorException { + if (isJsonPayloadType()) { + onDeviceDisconnectJson(mqttMsg); + } else { + onDeviceDisconnectProto(mqttMsg); + } + } + + public void onDeviceTelemetry(MqttPublishMessage mqttMsg) throws AdaptorException { + int msgId = getMsgId(mqttMsg); + ByteBuf payload = mqttMsg.payload(); + if (isJsonPayloadType()) { + onDeviceTelemetryJson(msgId, payload); + } else { + onDeviceTelemetryProto(msgId, payload); + } + } + + public void onDeviceClaim(MqttPublishMessage mqttMsg) throws AdaptorException { + int msgId = getMsgId(mqttMsg); + ByteBuf payload = mqttMsg.payload(); + if (isJsonPayloadType()) { + onDeviceClaimJson(msgId, payload); + } else { + onDeviceClaimProto(msgId, payload); + } + } + + public void onDeviceAttributes(MqttPublishMessage mqttMsg) throws AdaptorException { + int msgId = getMsgId(mqttMsg); + ByteBuf payload = mqttMsg.payload(); + if (isJsonPayloadType()) { + onDeviceAttributesJson(msgId, payload); + } else { + onDeviceAttributesProto(msgId, payload); + } + } + + public void onDeviceAttributesRequest(MqttPublishMessage mqttMsg) throws AdaptorException { + if (isJsonPayloadType()) { + onDeviceAttributesRequestJson(mqttMsg); + } else { + onDeviceAttributesRequestProto(mqttMsg); + } + } + + public void onDeviceRpcResponse(MqttPublishMessage mqttMsg) throws AdaptorException { + int msgId = getMsgId(mqttMsg); + ByteBuf payload = mqttMsg.payload(); + if (isJsonPayloadType()) { + onDeviceRpcResponseJson(msgId, payload); + } else { + onDeviceRpcResponseProto(msgId, payload); + } + } + + public void onGatewayDisconnect() { + devices.forEach(this::deregisterSession); + } + + public String getNodeId() { + return context.getNodeId(); + } + + public UUID getSessionId() { + return sessionId; + } + + public MqttTransportAdaptor getPayloadAdaptor() { + return deviceSessionCtx.getPayloadAdaptor(); + } + + void deregisterSession(String deviceName) { + GatewayDeviceSessionCtx deviceSessionCtx = devices.remove(deviceName); + if (deviceSessionCtx != null) { + deregisterSession(deviceName, deviceSessionCtx); + } else { + log.debug("[{}] Device [{}] was already removed from the gateway session", sessionId, deviceName); + } + } + + void writeAndFlush(MqttMessage mqttMessage) { + channel.writeAndFlush(mqttMessage); + } + + int nextMsgId() { + return deviceSessionCtx.nextMsgId(); + } + + private boolean isJsonPayloadType() { + return deviceSessionCtx.isJsonPayloadType(); + } + + private void processOnConnect(MqttPublishMessage msg, String deviceName, String deviceType) { log.trace("[{}] onDeviceConnect: {}", sessionId, deviceName); Futures.addCallback(onDeviceConnect(deviceName, deviceType), new FutureCallback() { @Override @@ -113,6 +220,7 @@ public class GatewaySessionHandler { private ListenableFuture onDeviceConnect(String deviceName, String deviceType) { GatewayDeviceSessionCtx result = devices.get(deviceName); if (result == null) { + Lock deviceCreationLock = deviceCreationLockMap.computeIfAbsent(deviceName, s -> new ReentrantLock()); deviceCreationLock.lock(); try { result = devices.get(deviceName); @@ -138,13 +246,14 @@ public class GatewaySessionHandler { transportService.process(GetOrCreateDeviceFromGatewayRequestMsg.newBuilder() .setDeviceName(deviceName) .setDeviceType(deviceType) - .setGatewayIdMSB(gateway.getDeviceIdMSB()) - .setGatewayIdLSB(gateway.getDeviceIdLSB()).build(), - new TransportServiceCallback() { + .setGatewayIdMSB(gateway.getDeviceId().getId().getMostSignificantBits()) + .setGatewayIdLSB(gateway.getDeviceId().getId().getLeastSignificantBits()).build(), + new TransportServiceCallback() { @Override - public void onSuccess(GetOrCreateDeviceFromGatewayResponseMsg msg) { - GatewayDeviceSessionCtx deviceSessionCtx = new GatewayDeviceSessionCtx(GatewaySessionHandler.this, msg.getDeviceInfo(), mqttQoSMap); + public void onSuccess(GetOrCreateDeviceFromGatewayResponse msg) { + GatewayDeviceSessionCtx deviceSessionCtx = new GatewayDeviceSessionCtx(GatewaySessionHandler.this, msg.getDeviceInfo(), msg.getDeviceProfile(), mqttQoSMap); if (devices.putIfAbsent(deviceName, deviceSessionCtx) == null) { + log.trace("[{}] First got or created device [{}], type [{}] for the gateway session", sessionId, deviceName, deviceType); SessionInfoProto deviceSessionInfo = deviceSessionCtx.getSessionInfo(); transportService.registerAsyncSession(deviceSessionInfo, deviceSessionCtx); transportService.process(deviceSessionInfo, DefaultTransportService.getSessionEventMsg(TransportProtos.SessionEvent.OPEN), null); @@ -178,28 +287,50 @@ public class GatewaySessionHandler { return future; } - public void onDeviceDisconnect(MqttPublishMessage msg) throws AdaptorException { + private int getMsgId(MqttPublishMessage mqttMsg) { + return mqttMsg.variableHeader().packetId(); + } + + private void onDeviceConnectJson(MqttPublishMessage mqttMsg) throws AdaptorException { + JsonElement json = getJson(mqttMsg); + String deviceName = checkDeviceName(getDeviceName(json)); + String deviceType = getDeviceType(json); + processOnConnect(mqttMsg, deviceName, deviceType); + } + + private void onDeviceConnectProto(MqttPublishMessage mqttMsg) throws AdaptorException { + try { + TransportApiProtos.ConnectMsg connectProto = TransportApiProtos.ConnectMsg.parseFrom(getBytes(mqttMsg.payload())); + String deviceName = checkDeviceName(connectProto.getDeviceName()); + String deviceType = StringUtils.isEmpty(connectProto.getDeviceType()) ? DEFAULT_DEVICE_TYPE : connectProto.getDeviceType(); + processOnConnect(mqttMsg, deviceName, deviceType); + } catch (RuntimeException | InvalidProtocolBufferException e) { + throw new AdaptorException(e); + } + } + + private void onDeviceDisconnectJson(MqttPublishMessage msg) throws AdaptorException { String deviceName = checkDeviceName(getDeviceName(getJson(msg))); - deregisterSession(deviceName); - ack(msg); + processOnDisconnect(msg, deviceName); } - void deregisterSession(String deviceName) { - GatewayDeviceSessionCtx deviceSessionCtx = devices.remove(deviceName); - if (deviceSessionCtx != null) { - deregisterSession(deviceName, deviceSessionCtx); - } else { - log.debug("[{}] Device [{}] was already removed from the gateway session", sessionId, deviceName); + private void onDeviceDisconnectProto(MqttPublishMessage mqttMsg) throws AdaptorException { + try { + TransportApiProtos.DisconnectMsg connectProto = TransportApiProtos.DisconnectMsg.parseFrom(getBytes(mqttMsg.payload())); + String deviceName = checkDeviceName(connectProto.getDeviceName()); + processOnDisconnect(mqttMsg, deviceName); + } catch (RuntimeException | InvalidProtocolBufferException e) { + throw new AdaptorException(e); } } - public void onGatewayDisconnect() { - devices.forEach(this::deregisterSession); + private void processOnDisconnect(MqttPublishMessage msg, String deviceName) { + deregisterSession(deviceName); + ack(msg); } - public void onDeviceTelemetry(MqttPublishMessage mqttMsg) throws AdaptorException { - JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, mqttMsg.payload()); - int msgId = mqttMsg.variableHeader().packetId(); + private void onDeviceTelemetryJson(int msgId, ByteBuf payload) throws AdaptorException { + JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); if (json.isJsonObject()) { JsonObject jsonObj = json.getAsJsonObject(); for (Map.Entry deviceEntry : jsonObj.entrySet()) { @@ -213,10 +344,9 @@ public class GatewaySessionHandler { } try { TransportProtos.PostTelemetryMsg postTelemetryMsg = JsonConverter.convertToTelemetryProto(deviceEntry.getValue().getAsJsonArray()); - transportService.process(deviceCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(channel, deviceName, msgId, postTelemetryMsg)); + processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId); } catch (Throwable e) { - UUID gatewayId = new UUID(gateway.getDeviceIdMSB(), gateway.getDeviceIdLSB()); - log.warn("[{}][{}] Failed to convert telemetry: {}", gatewayId, deviceName, deviceEntry.getValue(), e); + log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, deviceEntry.getValue(), e); } } @@ -231,9 +361,47 @@ public class GatewaySessionHandler { } } - public void onDeviceClaim(MqttPublishMessage mqttMsg) throws AdaptorException { - JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, mqttMsg.payload()); - int msgId = mqttMsg.variableHeader().packetId(); + private void onDeviceTelemetryProto(int msgId, ByteBuf payload) throws AdaptorException { + try { + TransportApiProtos.GatewayTelemetryMsg telemetryMsgProto = TransportApiProtos.GatewayTelemetryMsg.parseFrom(getBytes(payload)); + List deviceMsgList = telemetryMsgProto.getMsgList(); + if (!CollectionUtils.isEmpty(deviceMsgList)) { + deviceMsgList.forEach(telemetryMsg -> { + String deviceName = checkDeviceName(telemetryMsg.getDeviceName()); + Futures.addCallback(checkDeviceConnected(deviceName), + new FutureCallback() { + @Override + public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { + TransportProtos.PostTelemetryMsg msg = telemetryMsg.getMsg(); + try { + TransportProtos.PostTelemetryMsg postTelemetryMsg = ProtoConverter.validatePostTelemetryMsg(msg.toByteArray()); + processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId); + } catch (Throwable e) { + log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, msg, e); + } + } + + @Override + public void onFailure(Throwable t) { + log.debug("[{}] Failed to process device telemetry command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + }); + } else { + log.debug("[{}] Devices telemetry messages is empty for: [{}]", sessionId, gateway.getDeviceId()); + throw new IllegalArgumentException("[" + sessionId + "] Devices telemetry messages is empty for [" + gateway.getDeviceId() + "]"); + } + } catch (RuntimeException | InvalidProtocolBufferException e) { + throw new AdaptorException(e); + } + } + + private void processPostTelemetryMsg(GatewayDeviceSessionCtx deviceCtx, TransportProtos.PostTelemetryMsg postTelemetryMsg, String deviceName, int msgId) { + transportService.process(deviceCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(channel, deviceName, msgId, postTelemetryMsg)); + } + + private void onDeviceClaimJson(int msgId, ByteBuf payload) throws AdaptorException { + JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); if (json.isJsonObject()) { JsonObject jsonObj = json.getAsJsonObject(); for (Map.Entry deviceEntry : jsonObj.entrySet()) { @@ -248,10 +416,9 @@ public class GatewaySessionHandler { try { DeviceId deviceId = deviceCtx.getDeviceId(); TransportProtos.ClaimDeviceMsg claimDeviceMsg = JsonConverter.convertToClaimDeviceProto(deviceId, deviceEntry.getValue()); - transportService.process(deviceCtx.getSessionInfo(), claimDeviceMsg, getPubAckCallback(channel, deviceName, msgId, claimDeviceMsg)); + processClaimDeviceMsg(deviceCtx, claimDeviceMsg, deviceName, msgId); } catch (Throwable e) { - UUID gatewayId = new UUID(gateway.getDeviceIdMSB(), gateway.getDeviceIdLSB()); - log.warn("[{}][{}] Failed to convert claim message: {}", gatewayId, deviceName, deviceEntry.getValue(), e); + log.warn("[{}][{}] Failed to convert claim message: {}", gateway.getDeviceId(), deviceName, deviceEntry.getValue(), e); } } @@ -266,9 +433,51 @@ public class GatewaySessionHandler { } } - public void onDeviceAttributes(MqttPublishMessage mqttMsg) throws AdaptorException { - JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, mqttMsg.payload()); - int msgId = mqttMsg.variableHeader().packetId(); + private void onDeviceClaimProto(int msgId, ByteBuf payload) throws AdaptorException { + try { + TransportApiProtos.GatewayClaimMsg claimMsgProto = TransportApiProtos.GatewayClaimMsg.parseFrom(getBytes(payload)); + List claimMsgList = claimMsgProto.getMsgList(); + if (!CollectionUtils.isEmpty(claimMsgList)) { + claimMsgList.forEach(claimDeviceMsg -> { + String deviceName = checkDeviceName(claimDeviceMsg.getDeviceName()); + Futures.addCallback(checkDeviceConnected(deviceName), + new FutureCallback() { + @Override + public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { + TransportApiProtos.ClaimDevice claimRequest = claimDeviceMsg.getClaimRequest(); + if (claimRequest == null) { + throw new IllegalArgumentException("Claim request for device: " + deviceName + " is null!"); + } + try { + DeviceId deviceId = deviceCtx.getDeviceId(); + TransportProtos.ClaimDeviceMsg claimDeviceMsg = ProtoConverter.convertToClaimDeviceProto(deviceId, claimRequest.toByteArray()); + processClaimDeviceMsg(deviceCtx, claimDeviceMsg, deviceName, msgId); + } catch (Throwable e) { + log.warn("[{}][{}] Failed to convert claim message: {}", gateway.getDeviceId(), deviceName, claimRequest, e); + } + } + + @Override + public void onFailure(Throwable t) { + log.debug("[{}] Failed to process device claiming command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + }); + } else { + log.debug("[{}] Devices claim messages is empty for: [{}]", sessionId, gateway.getDeviceId()); + throw new IllegalArgumentException("[" + sessionId + "] Devices claim messages is empty for [" + gateway.getDeviceId() + "]"); + } + } catch (RuntimeException | InvalidProtocolBufferException e) { + throw new AdaptorException(e); + } + } + + private void processClaimDeviceMsg(GatewayDeviceSessionCtx deviceCtx, TransportProtos.ClaimDeviceMsg claimDeviceMsg, String deviceName, int msgId) { + transportService.process(deviceCtx.getSessionInfo(), claimDeviceMsg, getPubAckCallback(channel, deviceName, msgId, claimDeviceMsg)); + } + + private void onDeviceAttributesJson(int msgId, ByteBuf payload) throws AdaptorException { + JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); if (json.isJsonObject()) { JsonObject jsonObj = json.getAsJsonObject(); for (Map.Entry deviceEntry : jsonObj.entrySet()) { @@ -281,7 +490,7 @@ public class GatewaySessionHandler { throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); } TransportProtos.PostAttributeMsg postAttributeMsg = JsonConverter.convertToAttributesProto(deviceEntry.getValue().getAsJsonObject()); - transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(channel, deviceName, msgId, postAttributeMsg)); + processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId); } @Override @@ -295,34 +504,49 @@ public class GatewaySessionHandler { } } - public void onDeviceRpcResponse(MqttPublishMessage mqttMsg) throws AdaptorException { - JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, mqttMsg.payload()); - int msgId = mqttMsg.variableHeader().packetId(); - if (json.isJsonObject()) { - JsonObject jsonObj = json.getAsJsonObject(); - String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString(); - Futures.addCallback(checkDeviceConnected(deviceName), - new FutureCallback() { - @Override - public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { - Integer requestId = jsonObj.get("id").getAsInt(); - String data = jsonObj.get("data").toString(); - TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() - .setRequestId(requestId).setPayload(data).build(); - transportService.process(deviceCtx.getSessionInfo(), rpcResponseMsg, getPubAckCallback(channel, deviceName, msgId, rpcResponseMsg)); - } + private void onDeviceAttributesProto(int msgId, ByteBuf payload) throws AdaptorException { + try { + TransportApiProtos.GatewayAttributesMsg attributesMsgProto = TransportApiProtos.GatewayAttributesMsg.parseFrom(getBytes(payload)); + List attributesMsgList = attributesMsgProto.getMsgList(); + if (!CollectionUtils.isEmpty(attributesMsgList)) { + attributesMsgList.forEach(attributesMsg -> { + String deviceName = checkDeviceName(attributesMsg.getDeviceName()); + Futures.addCallback(checkDeviceConnected(deviceName), + new FutureCallback() { + @Override + public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { + TransportProtos.PostAttributeMsg kvListProto = attributesMsg.getMsg(); + if (kvListProto == null) { + throw new IllegalArgumentException("Attributes List for device: " + deviceName + " is empty!"); + } + try { + TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto.toByteArray()); + processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId); + } catch (Throwable e) { + log.warn("[{}][{}] Failed to process device attributes command: {}", gateway.getDeviceId(), deviceName, kvListProto, e); + } + } - @Override - public void onFailure(Throwable t) { - log.debug("[{}] Failed to process device teleemtry command: {}", sessionId, deviceName, t); - } - }, context.getExecutor()); - } else { - throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); + @Override + public void onFailure(Throwable t) { + log.debug("[{}] Failed to process device attributes command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + }); + } else { + log.debug("[{}] Devices attributes keys list is empty for: [{}]", sessionId, gateway.getDeviceId()); + throw new IllegalArgumentException("[" + sessionId + "] Devices attributes keys list is empty for [" + gateway.getDeviceId() + "]"); + } + } catch (RuntimeException | InvalidProtocolBufferException e) { + throw new AdaptorException(e); } } - public void onDeviceAttributesRequest(MqttPublishMessage msg) throws AdaptorException { + private void processPostAttributesMsg(GatewayDeviceSessionCtx deviceCtx, TransportProtos.PostAttributeMsg postAttributeMsg, String deviceName, int msgId) { + transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(channel, deviceName, msgId, postAttributeMsg)); + } + + private void onDeviceAttributesRequestJson(MqttPublishMessage msg) throws AdaptorException { JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, msg.payload()); if (json.isJsonObject()) { JsonObject jsonObj = json.getAsJsonObject(); @@ -339,27 +563,47 @@ public class GatewaySessionHandler { keys.add(keyObj.getAsString()); } } - TransportProtos.GetAttributeRequestMsg.Builder result = TransportProtos.GetAttributeRequestMsg.newBuilder(); - result.setRequestId(requestId); + TransportProtos.GetAttributeRequestMsg requestMsg = toGetAttributeRequestMsg(requestId, clientScope, keys); + processGetAttributeRequestMessage(msg, deviceName, requestMsg); + } else { + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); + } + } - if (clientScope) { - result.addAllClientAttributeNames(keys); - } else { - result.addAllSharedAttributeNames(keys); - } - TransportProtos.GetAttributeRequestMsg requestMsg = result.build(); - int msgId = msg.variableHeader().packetId(); + private void onDeviceAttributesRequestProto(MqttPublishMessage mqttMsg) throws AdaptorException { + try { + TransportApiProtos.GatewayAttributesRequestMsg gatewayAttributesRequestMsg = TransportApiProtos.GatewayAttributesRequestMsg.parseFrom(getBytes(mqttMsg.payload())); + String deviceName = checkDeviceName(gatewayAttributesRequestMsg.getDeviceName()); + int requestId = gatewayAttributesRequestMsg.getId(); + boolean clientScope = gatewayAttributesRequestMsg.getClient(); + ProtocolStringList keysList = gatewayAttributesRequestMsg.getKeysList(); + Set keys = new HashSet<>(keysList); + TransportProtos.GetAttributeRequestMsg requestMsg = toGetAttributeRequestMsg(requestId, clientScope, keys); + processGetAttributeRequestMessage(mqttMsg, deviceName, requestMsg); + } catch (RuntimeException | InvalidProtocolBufferException e) { + throw new AdaptorException(e); + } + } + + private void onDeviceRpcResponseJson(int msgId, ByteBuf payload) throws AdaptorException { + JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); + if (json.isJsonObject()) { + JsonObject jsonObj = json.getAsJsonObject(); + String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString(); Futures.addCallback(checkDeviceConnected(deviceName), new FutureCallback() { @Override public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { - transportService.process(deviceCtx.getSessionInfo(), requestMsg, getPubAckCallback(channel, deviceName, msgId, requestMsg)); + Integer requestId = jsonObj.get("id").getAsInt(); + String data = jsonObj.get("data").toString(); + TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() + .setRequestId(requestId).setPayload(data).build(); + processRpcResponseMsg(deviceCtx, rpcResponseMsg, deviceName, msgId); } @Override public void onFailure(Throwable t) { - ack(msg); - log.debug("[{}] Failed to process device attributes request command: {}", sessionId, deviceName, t); + log.debug("[{}] Failed to process device Rpc response command: {}", sessionId, deviceName, t); } }, context.getExecutor()); } else { @@ -367,6 +611,64 @@ public class GatewaySessionHandler { } } + private void onDeviceRpcResponseProto(int msgId, ByteBuf payload) throws AdaptorException { + try { + TransportApiProtos.GatewayRpcResponseMsg gatewayRpcResponseMsg = TransportApiProtos.GatewayRpcResponseMsg.parseFrom(getBytes(payload)); + String deviceName = checkDeviceName(gatewayRpcResponseMsg.getDeviceName()); + Futures.addCallback(checkDeviceConnected(deviceName), + new FutureCallback() { + @Override + public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { + Integer requestId = gatewayRpcResponseMsg.getId(); + String data = gatewayRpcResponseMsg.getData(); + TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() + .setRequestId(requestId).setPayload(data).build(); + processRpcResponseMsg(deviceCtx, rpcResponseMsg, deviceName, msgId); + } + + @Override + public void onFailure(Throwable t) { + log.debug("[{}] Failed to process device Rpc response command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + } catch (RuntimeException | InvalidProtocolBufferException e) { + throw new AdaptorException(e); + } + } + + private void processRpcResponseMsg(GatewayDeviceSessionCtx deviceCtx, TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg, String deviceName, int msgId) { + transportService.process(deviceCtx.getSessionInfo(), rpcResponseMsg, getPubAckCallback(channel, deviceName, msgId, rpcResponseMsg)); + } + + private void processGetAttributeRequestMessage(MqttPublishMessage mqttMsg, String deviceName, TransportProtos.GetAttributeRequestMsg requestMsg) { + int msgId = getMsgId(mqttMsg); + Futures.addCallback(checkDeviceConnected(deviceName), + new FutureCallback() { + @Override + public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { + transportService.process(deviceCtx.getSessionInfo(), requestMsg, getPubAckCallback(channel, deviceName, msgId, requestMsg)); + } + + @Override + public void onFailure(Throwable t) { + ack(mqttMsg); + log.debug("[{}] Failed to process device attributes request command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + } + + private TransportProtos.GetAttributeRequestMsg toGetAttributeRequestMsg(int requestId, boolean clientScope, Set keys) { + TransportProtos.GetAttributeRequestMsg.Builder result = TransportProtos.GetAttributeRequestMsg.newBuilder(); + result.setRequestId(requestId); + + if (clientScope) { + result.addAllClientAttributeNames(keys); + } else { + result.addAllSharedAttributeNames(keys); + } + return result.build(); + } + private ListenableFuture checkDeviceConnected(String deviceName) { GatewayDeviceSessionCtx ctx = devices.get(deviceName); if (ctx == null) { @@ -385,11 +687,11 @@ public class GatewaySessionHandler { } } - private String getDeviceName(JsonElement json) throws AdaptorException { + private String getDeviceName(JsonElement json) { return json.getAsJsonObject().get(DEVICE_PROPERTY).getAsString(); } - private String getDeviceType(JsonElement json) throws AdaptorException { + private String getDeviceType(JsonElement json) { JsonElement type = json.getAsJsonObject().get("type"); return type == null || type instanceof JsonNull ? DEFAULT_DEVICE_TYPE : type.getAsString(); } @@ -398,18 +700,15 @@ public class GatewaySessionHandler { return JsonMqttAdaptor.validateJsonPayload(sessionId, mqttMsg.payload()); } - private void ack(MqttPublishMessage msg) { - if (msg.variableHeader().packetId() > 0) { - writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(msg.variableHeader().packetId())); - } - } - - void writeAndFlush(MqttMessage mqttMessage) { - channel.writeAndFlush(mqttMessage); + private byte[] getBytes(ByteBuf payload) { + return ProtoMqttAdaptor.toBytes(payload); } - public String getNodeId() { - return context.getNodeId(); + private void ack(MqttPublishMessage msg) { + int msgId = getMsgId(msg); + if (msgId > 0) { + writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(msgId)); + } } private void deregisterSession(String deviceName, GatewayDeviceSessionCtx deviceSessionCtx) { @@ -430,25 +729,9 @@ public class GatewaySessionHandler { @Override public void onError(Throwable e) { - log.trace("[{}] Failed to publish msg: {}", sessionId, deviceName, msg, e); + log.trace("[{}] Failed to publish msg: {} for device: {}", sessionId, msg, deviceName, e); ctx.close(); } }; } - - public MqttTransportContext getContext() { - return context; - } - - MqttTransportAdaptor getAdaptor() { - return context.getAdaptor(); - } - - int nextMsgId() { - return deviceSessionCtx.nextMsgId(); - } - - public UUID getSessionId() { - return sessionId; - } } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java index ea5291d7bf..76e8843afa 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java @@ -16,7 +16,14 @@ package org.thingsboard.server.transport.mqtt.session; import io.netty.handler.codec.mqtt.MqttQoS; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; import org.thingsboard.server.common.transport.session.DeviceAwareSessionContext; +import org.thingsboard.server.transport.mqtt.util.MqttTopicFilter; +import org.thingsboard.server.transport.mqtt.util.MqttTopicFilterFactory; import java.util.List; import java.util.Map; @@ -52,5 +59,4 @@ public abstract class MqttDeviceAwareSessionContext extends DeviceAwareSessionCo return MqttQoS.AT_LEAST_ONCE; } } - } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/EqualsTopicFilter.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/EqualsTopicFilter.java new file mode 100644 index 0000000000..539c8f2b7e --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/EqualsTopicFilter.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.util; + +import lombok.Data; + +@Data +public class EqualsTopicFilter implements MqttTopicFilter { + + private final String filter; + + @Override + public boolean filter(String topic) { + return filter.equals(topic); + } +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/MqttTopicFilter.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/MqttTopicFilter.java new file mode 100644 index 0000000000..005deb5d44 --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/MqttTopicFilter.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.util; + +public interface MqttTopicFilter { + + boolean filter(String topic); + +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/MqttTopicFilterFactory.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/MqttTopicFilterFactory.java new file mode 100644 index 0000000000..4d5a9a7c2b --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/MqttTopicFilterFactory.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.util; + +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.device.profile.MqttTopics; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +@Slf4j +public class MqttTopicFilterFactory { + + private static final ConcurrentMap filters = new ConcurrentHashMap<>(); + private static final MqttTopicFilter DEFAULT_TELEMETRY_TOPIC_FILTER = toFilter(MqttTopics.DEVICE_TELEMETRY_TOPIC); + private static final MqttTopicFilter DEFAULT_ATTRIBUTES_TOPIC_FILTER = toFilter(MqttTopics.DEVICE_ATTRIBUTES_TOPIC); + + public static MqttTopicFilter toFilter(String topicFilter) { + if (topicFilter == null || topicFilter.isEmpty()) { + throw new IllegalArgumentException("Topic filter can't be empty!"); + } + return filters.computeIfAbsent(topicFilter, filter -> { + if (filter.contains("+") || filter.contains("#")) { + String regex = filter + .replace("\\", "\\\\") + .replace("+", "[^/]+") + .replace("/#", "($|/.*)"); + log.debug("Converting [{}] to [{}]", filter, regex); + return new RegexTopicFilter(regex); + } else { + return new EqualsTopicFilter(filter); + } + }); + } + + public static MqttTopicFilter getDefaultTelemetryFilter() { + return DEFAULT_TELEMETRY_TOPIC_FILTER; + } + + public static MqttTopicFilter getDefaultAttributesFilter() { + return DEFAULT_ATTRIBUTES_TOPIC_FILTER; + } +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/RegexTopicFilter.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/RegexTopicFilter.java new file mode 100644 index 0000000000..d5f50ae8c9 --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/RegexTopicFilter.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.util; + +import lombok.Data; + +import java.util.regex.Pattern; + +@Data +public class RegexTopicFilter implements MqttTopicFilter { + + private final Pattern regex; + + public RegexTopicFilter(String regex) { + this.regex = Pattern.compile(regex); + } + + @Override + public boolean filter(String topic) { + return regex.matcher(topic).matches(); + } +} diff --git a/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/util/MqttTopicFilterFactoryTest.java b/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/util/MqttTopicFilterFactoryTest.java new file mode 100644 index 0000000000..0b854d51ef --- /dev/null +++ b/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/util/MqttTopicFilterFactoryTest.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.util; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +import javax.script.ScriptException; +import java.util.regex.Pattern; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@RunWith(MockitoJUnitRunner.class) +public class MqttTopicFilterFactoryTest { + + private static String TEST_STR_1 = "Sensor/Temperature/House/48"; + private static String TEST_STR_2 = "Sensor/Temperature"; + private static String TEST_STR_3 = "Sensor/Temperature2/House/48"; + + @Test + public void metadataCanBeUpdated() throws ScriptException { + MqttTopicFilter filter = MqttTopicFilterFactory.toFilter("Sensor/Temperature/House/+"); + assertTrue(filter.filter(TEST_STR_1)); + assertFalse(filter.filter(TEST_STR_2)); + + filter = MqttTopicFilterFactory.toFilter("Sensor/+/House/#"); + assertTrue(filter.filter(TEST_STR_1)); + assertFalse(filter.filter(TEST_STR_2)); + + filter = MqttTopicFilterFactory.toFilter("Sensor/#"); + assertTrue(filter.filter(TEST_STR_1)); + assertTrue(filter.filter(TEST_STR_2)); + assertTrue(filter.filter(TEST_STR_3)); + + filter = MqttTopicFilterFactory.toFilter("Sensor/Temperature/#"); + assertTrue(filter.filter(TEST_STR_1)); + assertTrue(filter.filter(TEST_STR_2)); + assertFalse(filter.filter(TEST_STR_3)); + } + +} diff --git a/common/transport/pom.xml b/common/transport/pom.xml index e40f3e4009..fbdb10d081 100644 --- a/common/transport/pom.xml +++ b/common/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT common org.thingsboard.common diff --git a/common/transport/transport-api/pom.xml b/common/transport/transport-api/pom.xml index 739aa2b068..168c991eb6 100644 --- a/common/transport/transport-api/pom.xml +++ b/common/transport/transport-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT transport org.thingsboard.common.transport @@ -60,6 +60,10 @@ com.google.code.gson gson + + de.ruedigermoeller + fst + org.slf4j slf4j-api @@ -103,6 +107,19 @@ org.apache.commons commons-lang3 + + com.google.protobuf + protobuf-java + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + + + + diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/SessionMsgListener.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/SessionMsgListener.java index 8517bc9390..ccd63ca430 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/SessionMsgListener.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/SessionMsgListener.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.transport; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg; @@ -35,4 +36,8 @@ public interface SessionMsgListener { void onToDeviceRpcRequest(ToDeviceRpcRequestMsg toDeviceRequest); void onToServerRpcResponse(ToServerRpcResponseMsg toServerResponse); + + default void onProfileUpdate(DeviceProfile deviceProfile) { + } + } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportContext.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportContext.java index c49b9c5bd3..4b7677dd69 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportContext.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportContext.java @@ -23,6 +23,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.scheduler.SchedulerComponent; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; @@ -35,7 +36,7 @@ import java.util.concurrent.Executors; @Slf4j @Data @Service -@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || '${service.type:null}'=='monolith'") +@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true')") public abstract class TransportContext { protected final ObjectMapper mapper = new ObjectMapper(); @@ -44,6 +45,8 @@ public abstract class TransportContext { private TransportService transportService; @Autowired private TbServiceInfoProvider serviceInfoProvider; + @Autowired + private SchedulerComponent scheduler; @Getter private ExecutorService executor; diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportDeviceProfileCache.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportDeviceProfileCache.java new file mode 100644 index 0000000000..d56c0291b7 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportDeviceProfileCache.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport; + +import com.google.protobuf.ByteString; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.id.DeviceProfileId; + +import java.util.Optional; + +public interface TransportDeviceProfileCache { + + DeviceProfile getOrCreate(DeviceProfileId id, ByteString profileBody); + + DeviceProfile get(DeviceProfileId id); + + void put(DeviceProfile profile); + + DeviceProfile put(ByteString profileBody); + + void evict(DeviceProfileId id); + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java index 2af55c9206..980aa33221 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java @@ -15,14 +15,19 @@ */ package org.thingsboard.server.common.transport; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; +import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.gen.transport.TransportProtos.ClaimDeviceMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.GetEntityProfileRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.GetEntityProfileResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg; -import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayResponseMsg; -import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoRequestMsg; -import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.PostAttributeMsg; import org.thingsboard.server.gen.transport.TransportProtos.PostTelemetryMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.SessionEventMsg; import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; import org.thingsboard.server.gen.transport.TransportProtos.SubscribeToAttributeUpdatesMsg; @@ -30,7 +35,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.SubscribeToRPCMsg; import org.thingsboard.server.gen.transport.TransportProtos.SubscriptionInfoProto; import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcRequestMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ValidateBasicMqttCredRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; @@ -39,18 +44,22 @@ import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509Ce */ public interface TransportService { - GetTenantRoutingInfoResponseMsg getRoutingInfo(GetTenantRoutingInfoRequestMsg msg); + GetEntityProfileResponseMsg getEntityProfile(GetEntityProfileRequestMsg msg); - void process(ValidateDeviceTokenRequestMsg msg, - TransportServiceCallback callback); + void process(DeviceTransportType transportType, ValidateDeviceTokenRequestMsg msg, + TransportServiceCallback callback); - void process(ValidateDeviceX509CertRequestMsg msg, - TransportServiceCallback callback); + void process(DeviceTransportType transportType, ValidateBasicMqttCredRequestMsg msg, + TransportServiceCallback callback); + + void process(DeviceTransportType transportType, ValidateDeviceX509CertRequestMsg msg, + TransportServiceCallback callback); void process(GetOrCreateDeviceFromGatewayRequestMsg msg, - TransportServiceCallback callback); + TransportServiceCallback callback); - boolean checkLimits(SessionInfoProto sessionInfo, Object msg, TransportServiceCallback callback); + void process(ProvisionDeviceRequestMsg msg, + TransportServiceCallback callback); void process(SessionInfoProto sessionInfo, SessionEventMsg msg, TransportServiceCallback callback); @@ -79,5 +88,4 @@ public interface TransportService { void reportActivity(SessionInfoProto sessionInfo); void deregisterSession(SessionInfoProto sessionInfo); - } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportTenantProfileCache.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportTenantProfileCache.java new file mode 100644 index 0000000000..1f6b31a356 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportTenantProfileCache.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport; + +import com.google.protobuf.ByteString; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.common.transport.profile.TenantProfileUpdateResult; +import org.thingsboard.server.queue.discovery.TenantRoutingInfo; +import org.thingsboard.server.queue.discovery.TenantRoutingInfoService; + +import java.util.Set; + +public interface TransportTenantProfileCache { + + TenantProfile get(TenantId tenantId); + + TenantProfileUpdateResult put(ByteString profileBody); + + boolean put(TenantId tenantId, TenantProfileId profileId); + + Set remove(TenantProfileId profileId); + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java index a43437bb9d..321ed22baa 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java @@ -26,7 +26,6 @@ import org.apache.commons.lang3.math.NumberUtils; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.kv.AttributeKey; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry; @@ -38,13 +37,19 @@ import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ClaimDeviceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.CredentialsType; import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.KeyValueProto; import org.thingsboard.server.gen.transport.TransportProtos.KeyValueType; import org.thingsboard.server.gen.transport.TransportProtos.PostAttributeMsg; import org.thingsboard.server.gen.transport.TransportProtos.PostTelemetryMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionResponseStatus; import org.thingsboard.server.gen.transport.TransportProtos.TsKvListProto; import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto; +import org.thingsboard.server.gen.transport.TransportProtos.ValidateBasicMqttCredRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; import java.util.ArrayList; import java.util.HashMap; @@ -53,6 +58,8 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.TreeMap; +import java.util.UUID; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -397,6 +404,36 @@ public class JsonConverter { } } + public static JsonObject toJson(ProvisionDeviceResponseMsg payload) { + return toJson(payload, false, 0); + } + + public static JsonObject toJson(ProvisionDeviceResponseMsg payload, int requestId) { + return toJson(payload, true, requestId); + } + + private static JsonObject toJson(ProvisionDeviceResponseMsg payload, boolean toGateway, int requestId) { + JsonObject result = new JsonObject(); + if (payload.getProvisionResponseStatus() == TransportProtos.ProvisionResponseStatus.NOT_FOUND) { + result.addProperty("errorMsg", "Provision data was not found!"); + result.addProperty("provisionDeviceStatus", ProvisionResponseStatus.NOT_FOUND.name()); + } else if (payload.getProvisionResponseStatus() == TransportProtos.ProvisionResponseStatus.FAILURE) { + result.addProperty("errorMsg", "Failed to provision device!"); + result.addProperty("provisionDeviceStatus", ProvisionResponseStatus.FAILURE.name()); + } else { + if (toGateway) { + result.addProperty("id", requestId); + } + result.addProperty("deviceId", new UUID(payload.getDeviceCredentials().getDeviceIdMSB(), payload.getDeviceCredentials().getDeviceIdLSB()).toString()); + result.addProperty("credentialsType", payload.getDeviceCredentials().getCredentialsType().name()); + result.addProperty("credentialsId", payload.getDeviceCredentials().getCredentialsId()); + result.addProperty("credentialsValue", + StringUtils.isEmpty(payload.getDeviceCredentials().getCredentialsValue()) ? null : payload.getDeviceCredentials().getCredentialsValue()); + result.addProperty("provisionDeviceStatus", ProvisionResponseStatus.SUCCESS.name()); + } + return result; + } + public static JsonElement toErrorJson(String errorMsg) { JsonObject error = new JsonObject(); error.addProperty("error", errorMsg); @@ -410,6 +447,13 @@ public class JsonConverter { return result; } + public static JsonElement toGatewayJson(String deviceName, TransportProtos.ProvisionDeviceResponseMsg responseRequest) { + JsonObject result = new JsonObject(); + result.addProperty(DEVICE_PROPERTY, deviceName); + result.add("data", JsonConverter.toJson(responseRequest)); + return result; + } + public static Set convertToAttributes(JsonElement element) { Set result = new HashSet<>(); long ts = System.currentTimeMillis(); @@ -454,11 +498,20 @@ public class JsonConverter { } public static Map> convertToTelemetry(JsonElement jsonElement, long systemTs) throws JsonSyntaxException { - Map> result = new HashMap<>(); + return convertToTelemetry(jsonElement, systemTs, false); + } + + public static Map> convertToSortedTelemetry(JsonElement jsonElement, long systemTs) throws JsonSyntaxException { + return convertToTelemetry(jsonElement, systemTs, true); + } + + public static Map> convertToTelemetry(JsonElement jsonElement, long systemTs, boolean sorted) throws JsonSyntaxException { + Map> result = sorted ? new TreeMap<>() : new HashMap<>(); convertToTelemetry(jsonElement, systemTs, result, null); return result; } + private static void parseObject(Map> result, long systemTs, JsonObject jo) { if (jo.has("ts") && jo.has("values")) { parseWithTs(result, jo); @@ -489,4 +542,55 @@ public class JsonConverter { maxStringValueLength = length; } + public static TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(String json) { + JsonElement jsonElement = new JsonParser().parse(json); + if (jsonElement.isJsonObject()) { + return buildProvisionRequestMsg(jsonElement.getAsJsonObject()); + } else { + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + jsonElement); + } + } + + public static TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(JsonObject jo) { + return buildProvisionRequestMsg(jo); + } + + private static TransportProtos.ProvisionDeviceRequestMsg buildProvisionRequestMsg(JsonObject jo) { + return TransportProtos.ProvisionDeviceRequestMsg.newBuilder() + .setDeviceName(getStrValue(jo, DataConstants.DEVICE_NAME, true)) + .setCredentialsType(jo.get(DataConstants.CREDENTIALS_TYPE) != null ? TransportProtos.CredentialsType.valueOf(getStrValue(jo, DataConstants.CREDENTIALS_TYPE, false)) : CredentialsType.ACCESS_TOKEN) + .setCredentialsDataProto(TransportProtos.CredentialsDataProto.newBuilder() + .setValidateDeviceTokenRequestMsg(ValidateDeviceTokenRequestMsg.newBuilder().setToken(getStrValue(jo, DataConstants.TOKEN, false)).build()) + .setValidateBasicMqttCredRequestMsg(ValidateBasicMqttCredRequestMsg.newBuilder() + .setClientId(getStrValue(jo, DataConstants.CLIENT_ID, false)) + .setUserName(getStrValue(jo, DataConstants.USERNAME, false)) + .setPassword(getStrValue(jo, DataConstants.PASSWORD, false)) + .build()) + .setValidateDeviceX509CertRequestMsg(ValidateDeviceX509CertRequestMsg.newBuilder() + .setHash(getStrValue(jo, DataConstants.HASH, false)).build()) + .build()) + .setProvisionDeviceCredentialsMsg(buildProvisionDeviceCredentialsMsg( + getStrValue(jo, DataConstants.PROVISION_KEY, true), + getStrValue(jo, DataConstants.PROVISION_SECRET, true))) + .build(); + } + + private static TransportProtos.ProvisionDeviceCredentialsMsg buildProvisionDeviceCredentialsMsg(String provisionKey, String provisionSecret) { + return TransportProtos.ProvisionDeviceCredentialsMsg.newBuilder() + .setProvisionDeviceKey(provisionKey) + .setProvisionDeviceSecret(provisionSecret) + .build(); + } + + + private static String getStrValue(JsonObject jo, String field, boolean requiredField) { + if (jo.has(field)) { + return jo.get(field).getAsString(); + } else { + if (requiredField) { + throw new RuntimeException("Failed to find the field " + field + " in JSON body " + jo + "!"); + } + return ""; + } + } } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java new file mode 100644 index 0000000000..2c8ca7e3b5 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java @@ -0,0 +1,167 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.adaptor; + +import com.google.gson.JsonParser; +import com.google.protobuf.InvalidProtocolBufferException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.gen.transport.TransportApiProtos; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Slf4j +public class ProtoConverter { + + public static final JsonParser JSON_PARSER = new JsonParser(); + + public static TransportProtos.PostTelemetryMsg convertToTelemetryProto(byte[] payload) throws InvalidProtocolBufferException, IllegalArgumentException { + TransportProtos.TsKvListProto protoPayload = TransportProtos.TsKvListProto.parseFrom(payload); + TransportProtos.PostTelemetryMsg.Builder postTelemetryMsgBuilder = TransportProtos.PostTelemetryMsg.newBuilder(); + TransportProtos.TsKvListProto tsKvListProto = validateTsKvListProto(protoPayload); + postTelemetryMsgBuilder.addTsKvList(tsKvListProto); + return postTelemetryMsgBuilder.build(); + } + + public static TransportProtos.PostTelemetryMsg validatePostTelemetryMsg(byte[] payload) throws InvalidProtocolBufferException, IllegalArgumentException { + TransportProtos.PostTelemetryMsg msg = TransportProtos.PostTelemetryMsg.parseFrom(payload); + TransportProtos.PostTelemetryMsg.Builder postTelemetryMsgBuilder = TransportProtos.PostTelemetryMsg.newBuilder(); + List tsKvListProtoList = msg.getTsKvListList(); + if (!CollectionUtils.isEmpty(tsKvListProtoList)) { + List tsKvListProtos = new ArrayList<>(); + tsKvListProtoList.forEach(tsKvListProto -> { + TransportProtos.TsKvListProto transportTsKvListProto = validateTsKvListProto(tsKvListProto); + tsKvListProtos.add(transportTsKvListProto); + }); + postTelemetryMsgBuilder.addAllTsKvList(tsKvListProtos); + return postTelemetryMsgBuilder.build(); + } else { + throw new IllegalArgumentException("TsKv list is empty!"); + } + } + + public static TransportProtos.PostAttributeMsg validatePostAttributeMsg(byte[] bytes) throws IllegalArgumentException, InvalidProtocolBufferException { + TransportProtos.PostAttributeMsg proto = TransportProtos.PostAttributeMsg.parseFrom(bytes); + List kvList = proto.getKvList(); + if (!CollectionUtils.isEmpty(kvList)) { + List keyValueProtos = validateKeyValueProtos(kvList); + TransportProtos.PostAttributeMsg.Builder result = TransportProtos.PostAttributeMsg.newBuilder(); + result.addAllKv(keyValueProtos); + return result.build(); + } else { + throw new IllegalArgumentException("KeyValue list is empty!"); + } + } + + public static TransportProtos.ClaimDeviceMsg convertToClaimDeviceProto(DeviceId deviceId, byte[] bytes) throws InvalidProtocolBufferException { + TransportApiProtos.ClaimDevice proto = TransportApiProtos.ClaimDevice.parseFrom(bytes); + String secretKey = proto.getSecretKey() != null ? proto.getSecretKey() : DataConstants.DEFAULT_SECRET_KEY; + long durationMs = proto.getDurationMs(); + return buildClaimDeviceMsg(deviceId, secretKey, durationMs); + } + + public static TransportProtos.GetAttributeRequestMsg convertToGetAttributeRequestMessage(byte[] bytes, int requestId) throws InvalidProtocolBufferException, RuntimeException { + TransportApiProtos.AttributesRequest proto = TransportApiProtos.AttributesRequest.parseFrom(bytes); + TransportProtos.GetAttributeRequestMsg.Builder result = TransportProtos.GetAttributeRequestMsg.newBuilder(); + result.setRequestId(requestId); + String clientKeys = proto.getClientKeys(); + String sharedKeys = proto.getSharedKeys(); + if (!StringUtils.isEmpty(clientKeys)) { + List clientKeysList = Arrays.asList(clientKeys.split(",")); + result.addAllClientAttributeNames(clientKeysList); + } + if (!StringUtils.isEmpty(sharedKeys)) { + List sharedKeysList = Arrays.asList(sharedKeys.split(",")); + result.addAllSharedAttributeNames(sharedKeysList); + } + return result.build(); + } + + public static TransportProtos.ToServerRpcRequestMsg convertToServerRpcRequest(byte[] bytes, int requestId) throws InvalidProtocolBufferException { + TransportApiProtos.RpcRequest proto = TransportApiProtos.RpcRequest.parseFrom(bytes); + String method = proto.getMethod(); + String params = proto.getParams(); + return TransportProtos.ToServerRpcRequestMsg.newBuilder().setRequestId(requestId).setMethodName(method).setParams(params).build(); + } + + private static TransportProtos.ClaimDeviceMsg buildClaimDeviceMsg(DeviceId deviceId, String secretKey, long durationMs) { + TransportProtos.ClaimDeviceMsg.Builder result = TransportProtos.ClaimDeviceMsg.newBuilder(); + return result + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setSecretKey(secretKey) + .setDurationMs(durationMs) + .build(); + } + + private static TransportProtos.TsKvListProto validateTsKvListProto(TransportProtos.TsKvListProto tsKvListProto) { + TransportProtos.TsKvListProto.Builder tsKvListBuilder = TransportProtos.TsKvListProto.newBuilder(); + long ts = tsKvListProto.getTs(); + if (ts == 0) { + ts = System.currentTimeMillis(); + } + tsKvListBuilder.setTs(ts); + List kvList = tsKvListProto.getKvList(); + if (!CollectionUtils.isEmpty(kvList)) { + List keyValueListProtos = validateKeyValueProtos(kvList); + tsKvListBuilder.addAllKv(keyValueListProtos); + return tsKvListBuilder.build(); + } else { + throw new IllegalArgumentException("KeyValue list is empty!"); + } + } + + public static TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(byte[] bytes) throws InvalidProtocolBufferException { + return TransportProtos.ProvisionDeviceRequestMsg.parseFrom(bytes); + } + + private static List validateKeyValueProtos(List kvList) { + kvList.forEach(keyValueProto -> { + String key = keyValueProto.getKey(); + if (StringUtils.isEmpty(key)) { + throw new IllegalArgumentException("Invalid key value: " + key + "!"); + } + TransportProtos.KeyValueType type = keyValueProto.getType(); + switch (type) { + case BOOLEAN_V: + case LONG_V: + case DOUBLE_V: + break; + case STRING_V: + if (StringUtils.isEmpty(keyValueProto.getStringV())) { + throw new IllegalArgumentException("Value is empty for key: " + key + "!"); + } + break; + case JSON_V: + try { + JSON_PARSER.parse(keyValueProto.getJsonV()); + } catch (Exception e) { + throw new IllegalArgumentException("Can't parse value: " + keyValueProto.getJsonV() + " for key: " + key + "!"); + } + break; + case UNRECOGNIZED: + throw new IllegalArgumentException("Unsupported keyValueType: " + type + "!"); + } + }); + return kvList; + } +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/DeviceProfileAware.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/DeviceProfileAware.java new file mode 100644 index 0000000000..f4e96a375b --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/DeviceProfileAware.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.auth; + +import org.thingsboard.server.common.data.DeviceProfile; + +public interface DeviceProfileAware { + + DeviceProfile getDeviceProfile(); + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/GetOrCreateDeviceFromGatewayResponse.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/GetOrCreateDeviceFromGatewayResponse.java new file mode 100644 index 0000000000..985866c962 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/GetOrCreateDeviceFromGatewayResponse.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.auth; + +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.DeviceProfile; + +@Data +@Builder +public class GetOrCreateDeviceFromGatewayResponse implements DeviceProfileAware { + + private TransportDeviceInfo deviceInfo; + private DeviceProfile deviceProfile; + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/SessionInfoCreator.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/SessionInfoCreator.java new file mode 100644 index 0000000000..39bec0890f --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/SessionInfoCreator.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.auth; + +import org.thingsboard.server.common.transport.TransportContext; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.UUID; + +public class SessionInfoCreator { + + public static TransportProtos.SessionInfoProto create(ValidateDeviceCredentialsResponse msg, TransportContext context, UUID sessionId) { + return TransportProtos.SessionInfoProto.newBuilder() + .setNodeId(context.getNodeId()) + .setSessionIdMSB(sessionId.getMostSignificantBits()) + .setSessionIdLSB(sessionId.getLeastSignificantBits()) + .setDeviceIdMSB(msg.getDeviceInfo().getDeviceId().getId().getMostSignificantBits()) + .setDeviceIdLSB(msg.getDeviceInfo().getDeviceId().getId().getLeastSignificantBits()) + .setTenantIdMSB(msg.getDeviceInfo().getTenantId().getId().getMostSignificantBits()) + .setTenantIdLSB(msg.getDeviceInfo().getTenantId().getId().getLeastSignificantBits()) + .setDeviceName(msg.getDeviceInfo().getDeviceName()) + .setDeviceType(msg.getDeviceInfo().getDeviceType()) + .setDeviceProfileIdMSB(msg.getDeviceInfo().getDeviceProfileId().getId().getMostSignificantBits()) + .setDeviceProfileIdLSB(msg.getDeviceInfo().getDeviceProfileId().getId().getLeastSignificantBits()) + .build(); + } + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/TransportDeviceInfo.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/TransportDeviceInfo.java new file mode 100644 index 0000000000..9aa3336f7d --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/TransportDeviceInfo.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.auth; + +import lombok.Data; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +public class TransportDeviceInfo { + + private TenantId tenantId; + private DeviceProfileId deviceProfileId; + private DeviceId deviceId; + private String deviceName; + private String deviceType; + private String additionalInfo; + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/ValidateDeviceCredentialsResponse.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/ValidateDeviceCredentialsResponse.java new file mode 100644 index 0000000000..1ce33d7c94 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/ValidateDeviceCredentialsResponse.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.auth; + +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.DeviceProfile; + +@Data +@Builder +public class ValidateDeviceCredentialsResponse implements DeviceProfileAware { + + private final TransportDeviceInfo deviceInfo; + private final DeviceProfile deviceProfile; + private final String credentials; + + public boolean hasDeviceInfo() { + return deviceInfo != null; + } +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java new file mode 100644 index 0000000000..b6f160ad1e --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java @@ -0,0 +1,180 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.limits; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; +import org.thingsboard.server.common.msg.tools.TbRateLimits; +import org.thingsboard.server.common.transport.TransportTenantProfileCache; +import org.thingsboard.server.common.transport.profile.TenantProfileUpdateResult; +import org.thingsboard.server.queue.util.TbTransportComponent; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.BiConsumer; +import java.util.function.Function; + +@Service +@TbTransportComponent +@Slf4j +public class DefaultTransportRateLimitService implements TransportRateLimitService { + + private final static DummyTransportRateLimit ALLOW = new DummyTransportRateLimit(); + private final ConcurrentMap tenantAllowed = new ConcurrentHashMap<>(); + private final ConcurrentMap> tenantDevices = new ConcurrentHashMap<>(); + private final ConcurrentMap perTenantLimits = new ConcurrentHashMap<>(); + private final ConcurrentMap perDeviceLimits = new ConcurrentHashMap<>(); + + private final TransportTenantProfileCache tenantProfileCache; + + public DefaultTransportRateLimitService(TransportTenantProfileCache tenantProfileCache) { + this.tenantProfileCache = tenantProfileCache; + } + + @Override + public EntityType checkLimits(TenantId tenantId, DeviceId deviceId, int dataPoints) { + if (!tenantAllowed.getOrDefault(tenantId, Boolean.TRUE)) { + return EntityType.TENANT; + } + if (!checkEntityRateLimit(dataPoints, getTenantRateLimits(tenantId))) { + return EntityType.TENANT; + } + if (!checkEntityRateLimit(dataPoints, getDeviceRateLimits(tenantId, deviceId))) { + return EntityType.DEVICE; + } + return null; + } + + private boolean checkEntityRateLimit(int dataPoints, EntityTransportRateLimits tenantLimits) { + if (dataPoints > 0) { + return tenantLimits.getTelemetryMsgRateLimit().tryConsume() && tenantLimits.getTelemetryDataPointsRateLimit().tryConsume(dataPoints); + } else { + return tenantLimits.getRegularMsgRateLimit().tryConsume(); + } + } + + @Override + public void update(TenantProfileUpdateResult update) { + EntityTransportRateLimits tenantRateLimitPrototype = createRateLimits(update.getProfile(), true); + EntityTransportRateLimits deviceRateLimitPrototype = createRateLimits(update.getProfile(), false); + for (TenantId tenantId : update.getAffectedTenants()) { + mergeLimits(tenantId, tenantRateLimitPrototype, perTenantLimits::get, perTenantLimits::put); + tenantDevices.get(tenantId).forEach(deviceId -> { + mergeLimits(deviceId, deviceRateLimitPrototype, perDeviceLimits::get, perDeviceLimits::put); + }); + } + } + + @Override + public void update(TenantId tenantId) { + EntityTransportRateLimits tenantRateLimitPrototype = createRateLimits(tenantProfileCache.get(tenantId), true); + EntityTransportRateLimits deviceRateLimitPrototype = createRateLimits(tenantProfileCache.get(tenantId), false); + mergeLimits(tenantId, tenantRateLimitPrototype, perTenantLimits::get, perTenantLimits::put); + tenantDevices.get(tenantId).forEach(deviceId -> { + mergeLimits(deviceId, deviceRateLimitPrototype, perDeviceLimits::get, perDeviceLimits::put); + }); + } + + @Override + public void remove(TenantId tenantId) { + perTenantLimits.remove(tenantId); + tenantDevices.remove(tenantId); + } + + @Override + public void remove(DeviceId deviceId) { + perDeviceLimits.remove(deviceId); + tenantDevices.values().forEach(set -> set.remove(deviceId)); + } + + @Override + public void update(TenantId tenantId, boolean allowed) { + tenantAllowed.put(tenantId, allowed); + } + + private void mergeLimits(T deviceId, EntityTransportRateLimits newRateLimits, + Function getFunction, + BiConsumer putFunction) { + EntityTransportRateLimits oldRateLimits = getFunction.apply(deviceId); + if (oldRateLimits == null) { + putFunction.accept(deviceId, newRateLimits); + } else { + EntityTransportRateLimits updated = merge(oldRateLimits, newRateLimits); + if (updated != null) { + putFunction.accept(deviceId, updated); + } + } + } + + private EntityTransportRateLimits merge(EntityTransportRateLimits oldRateLimits, EntityTransportRateLimits newRateLimits) { + boolean regularUpdate = !oldRateLimits.getRegularMsgRateLimit().getConfiguration().equals(newRateLimits.getRegularMsgRateLimit().getConfiguration()); + boolean telemetryMsgRateUpdate = !oldRateLimits.getTelemetryMsgRateLimit().getConfiguration().equals(newRateLimits.getTelemetryMsgRateLimit().getConfiguration()); + boolean telemetryDataPointUpdate = !oldRateLimits.getTelemetryDataPointsRateLimit().getConfiguration().equals(newRateLimits.getTelemetryDataPointsRateLimit().getConfiguration()); + if (regularUpdate || telemetryMsgRateUpdate || telemetryDataPointUpdate) { + return new EntityTransportRateLimits( + regularUpdate ? newLimit(newRateLimits.getRegularMsgRateLimit().getConfiguration()) : oldRateLimits.getRegularMsgRateLimit(), + telemetryMsgRateUpdate ? newLimit(newRateLimits.getTelemetryMsgRateLimit().getConfiguration()) : oldRateLimits.getTelemetryMsgRateLimit(), + telemetryDataPointUpdate ? newLimit(newRateLimits.getTelemetryDataPointsRateLimit().getConfiguration()) : oldRateLimits.getTelemetryDataPointsRateLimit()); + } else { + return null; + } + } + + private EntityTransportRateLimits createRateLimits(TenantProfile tenantProfile, boolean tenant) { + TenantProfileData profileData = tenantProfile.getProfileData(); + DefaultTenantProfileConfiguration profile = (DefaultTenantProfileConfiguration) profileData.getConfiguration(); + if (profile == null) { + return new EntityTransportRateLimits(ALLOW, ALLOW, ALLOW); + } else { + TransportRateLimit regularMsgRateLimit = newLimit(tenant ? profile.getTransportTenantMsgRateLimit() : profile.getTransportDeviceMsgRateLimit()); + TransportRateLimit telemetryMsgRateLimit = newLimit(tenant ? profile.getTransportTenantTelemetryMsgRateLimit() : profile.getTransportDeviceTelemetryMsgRateLimit()); + TransportRateLimit telemetryDpRateLimit = newLimit(tenant ? profile.getTransportTenantTelemetryDataPointsRateLimit() : profile.getTransportTenantTelemetryDataPointsRateLimit()); + return new EntityTransportRateLimits(regularMsgRateLimit, telemetryMsgRateLimit, telemetryDpRateLimit); + } + } + + private static TransportRateLimit newLimit(String config) { + return StringUtils.isEmpty(config) ? ALLOW : new SimpleTransportRateLimit(config); + } + + private EntityTransportRateLimits getTenantRateLimits(TenantId tenantId) { + EntityTransportRateLimits limits = perTenantLimits.get(tenantId); + if (limits == null) { + limits = createRateLimits(tenantProfileCache.get(tenantId), true); + perTenantLimits.put(tenantId, limits); + } + return limits; + } + + private EntityTransportRateLimits getDeviceRateLimits(TenantId tenantId, DeviceId deviceId) { + EntityTransportRateLimits limits = perDeviceLimits.get(deviceId); + if (limits == null) { + limits = createRateLimits(tenantProfileCache.get(tenantId), false); + perDeviceLimits.put(deviceId, limits); + tenantDevices.computeIfAbsent(tenantId, id -> ConcurrentHashMap.newKeySet()).add(deviceId); + } + return limits; + } +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DummyTransportRateLimit.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DummyTransportRateLimit.java new file mode 100644 index 0000000000..a93487632f --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DummyTransportRateLimit.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.limits; + +public class DummyTransportRateLimit implements TransportRateLimit { + + @Override + public String getConfiguration() { + return ""; + } + + @Override + public boolean tryConsume(long number) { + return true; + } + + @Override + public boolean tryConsume() { + return true; + } + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/EntityTransportRateLimits.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/EntityTransportRateLimits.java new file mode 100644 index 0000000000..50831ccff7 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/EntityTransportRateLimits.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.limits; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class EntityTransportRateLimits { + + private TransportRateLimit regularMsgRateLimit; + private TransportRateLimit telemetryMsgRateLimit; + private TransportRateLimit telemetryDataPointsRateLimit; + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/SimpleTransportRateLimit.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/SimpleTransportRateLimit.java new file mode 100644 index 0000000000..0ea920a4ba --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/SimpleTransportRateLimit.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.limits; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.thingsboard.server.common.msg.tools.TbRateLimits; + +@RequiredArgsConstructor +public class SimpleTransportRateLimit implements TransportRateLimit { + + private final TbRateLimits rateLimit; + @Getter + private final String configuration; + + public SimpleTransportRateLimit(String configuration) { + this.configuration = configuration; + this.rateLimit = new TbRateLimits(configuration); + } + + @Override + public boolean tryConsume() { + return rateLimit.tryConsume(); + } + + @Override + public boolean tryConsume(long number) { + return number <= 0 || rateLimit.tryConsume(number); + } +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimit.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimit.java new file mode 100644 index 0000000000..a2eea81d3a --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimit.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.limits; + +public interface TransportRateLimit { + + String getConfiguration(); + + boolean tryConsume(); + + boolean tryConsume(long number); + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitService.java new file mode 100644 index 0000000000..3918927c0b --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitService.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.limits; + +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.transport.profile.TenantProfileUpdateResult; + +public interface TransportRateLimitService { + + EntityType checkLimits(TenantId tenantId, DeviceId deviceId, int dataPoints); + + void update(TenantProfileUpdateResult update); + + void update(TenantId tenantId); + + void remove(TenantId tenantId); + + void remove(DeviceId deviceId); + + void update(TenantId tenantId, boolean transportEnabled); +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/profile/TenantProfileUpdateResult.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/profile/TenantProfileUpdateResult.java new file mode 100644 index 0000000000..53950e6de8 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/profile/TenantProfileUpdateResult.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.profile; + +import lombok.Data; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.Set; + +@Data +public class TenantProfileUpdateResult { + + private final TenantProfile profile; + private final Set affectedTenants; + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportDeviceProfileCache.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportDeviceProfileCache.java new file mode 100644 index 0000000000..b12aab1a8c --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportDeviceProfileCache.java @@ -0,0 +1,83 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.service; + +import com.google.protobuf.ByteString; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.transport.TransportDeviceProfileCache; +import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; +import org.thingsboard.server.queue.util.TbTransportComponent; + +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +@Slf4j +@Component +@TbTransportComponent +public class DefaultTransportDeviceProfileCache implements TransportDeviceProfileCache { + + private final ConcurrentMap deviceProfiles = new ConcurrentHashMap<>(); + + private final DataDecodingEncodingService dataDecodingEncodingService; + + public DefaultTransportDeviceProfileCache(DataDecodingEncodingService dataDecodingEncodingService) { + this.dataDecodingEncodingService = dataDecodingEncodingService; + } + + @Override + public DeviceProfile getOrCreate(DeviceProfileId id, ByteString profileBody) { + DeviceProfile profile = deviceProfiles.get(id); + if (profile == null) { + Optional deviceProfile = dataDecodingEncodingService.decode(profileBody.toByteArray()); + if (deviceProfile.isPresent()) { + profile = deviceProfile.get(); + deviceProfiles.put(id, profile); + } + } + return profile; + } + + @Override + public DeviceProfile get(DeviceProfileId id) { + return deviceProfiles.get(id); + } + + @Override + public void put(DeviceProfile profile) { + deviceProfiles.put(profile.getId(), profile); + } + + @Override + public DeviceProfile put(ByteString profileBody) { + Optional deviceProfile = dataDecodingEncodingService.decode(profileBody.toByteArray()); + if (deviceProfile.isPresent()) { + put(deviceProfile.get()); + return deviceProfile.get(); + } else { + return null; + } + } + + @Override + public void evict(DeviceProfileId id) { + deviceProfiles.remove(id); + } +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index 6fbc5d8e8e..4af723f3a0 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -15,29 +15,51 @@ */ package org.thingsboard.server.common.transport.service; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import com.google.gson.Gson; import com.google.gson.JsonObject; +import com.google.protobuf.ByteString; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.ApiUsageRecordKey; +import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.queue.ServiceQueue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.session.SessionMsgType; -import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.common.msg.tools.TbRateLimitsException; +import org.thingsboard.server.common.stats.MessagesStats; +import org.thingsboard.server.common.stats.StatsFactory; +import org.thingsboard.server.common.stats.StatsType; import org.thingsboard.server.common.transport.SessionMsgListener; +import org.thingsboard.server.common.transport.TransportDeviceProfileCache; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; +import org.thingsboard.server.common.transport.TransportTenantProfileCache; +import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; +import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; +import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; +import org.thingsboard.server.common.transport.limits.TransportRateLimitService; +import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; import org.thingsboard.server.common.transport.util.JsonUtils; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; @@ -55,15 +77,16 @@ import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.provider.TbTransportQueueFactory; -import org.thingsboard.server.common.stats.MessagesStats; -import org.thingsboard.server.common.stats.StatsFactory; -import org.thingsboard.server.common.stats.StatsType; +import org.thingsboard.server.queue.scheduler.SchedulerComponent; +import org.thingsboard.server.queue.usagestats.TbApiUsageClient; +import org.thingsboard.server.queue.util.TbTransportComponent; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Random; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -71,7 +94,6 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -81,15 +103,9 @@ import java.util.concurrent.atomic.AtomicInteger; */ @Slf4j @Service -@ConditionalOnExpression("'${service.type:null}'=='monolith' || '${service.type:null}'=='tb-transport'") +@TbTransportComponent public class DefaultTransportService implements TransportService { - @Value("${transport.rate_limits.enabled}") - private boolean rateLimitEnabled; - @Value("${transport.rate_limits.tenant}") - private String perTenantLimitsConf; - @Value("${transport.rate_limits.device}") - private String perDevicesLimitsConf; @Value("${transport.sessions.inactivity_timeout}") private long sessionInactivityTimeout; @Value("${transport.sessions.report_timeout}") @@ -105,6 +121,12 @@ public class DefaultTransportService implements TransportService { private final PartitionService partitionService; private final TbServiceInfoProvider serviceInfoProvider; private final StatsFactory statsFactory; + private final TransportDeviceProfileCache deviceProfileCache; + private final TransportTenantProfileCache tenantProfileCache; + private final TbApiUsageClient apiUsageClient; + private final TransportRateLimitService rateLimitService; + private final DataDecodingEncodingService dataDecodingEncodingService; + private final SchedulerComponent scheduler; protected TbQueueRequestTemplate, TbProtoQueueMsg> transportApiRequestTemplate; protected TbQueueProducer> ruleEngineMsgProducer; @@ -115,39 +137,43 @@ public class DefaultTransportService implements TransportService { protected MessagesStats tbCoreProducerStats; protected MessagesStats transportApiStats; - protected ScheduledExecutorService schedulerExecutor; protected ExecutorService transportCallbackExecutor; + private ExecutorService mainConsumerExecutor; private final ConcurrentMap sessions = new ConcurrentHashMap<>(); private final Map toServerRpcPendingMap = new ConcurrentHashMap<>(); - //TODO: Implement cleanup of this maps. - private final ConcurrentMap perTenantLimits = new ConcurrentHashMap<>(); - private final ConcurrentMap perDeviceLimits = new ConcurrentHashMap<>(); - private ExecutorService mainConsumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("transport-consumer")); private volatile boolean stopped = false; - public DefaultTransportService(TbServiceInfoProvider serviceInfoProvider, TbTransportQueueFactory queueProvider, TbQueueProducerProvider producerProvider, PartitionService partitionService, StatsFactory statsFactory) { + public DefaultTransportService(TbServiceInfoProvider serviceInfoProvider, + TbTransportQueueFactory queueProvider, + TbQueueProducerProvider producerProvider, + PartitionService partitionService, + StatsFactory statsFactory, + TransportDeviceProfileCache deviceProfileCache, + TransportTenantProfileCache tenantProfileCache, + TbApiUsageClient apiUsageClient, TransportRateLimitService rateLimitService, + DataDecodingEncodingService dataDecodingEncodingService, SchedulerComponent scheduler) { this.serviceInfoProvider = serviceInfoProvider; this.queueProvider = queueProvider; this.producerProvider = producerProvider; this.partitionService = partitionService; this.statsFactory = statsFactory; + this.deviceProfileCache = deviceProfileCache; + this.tenantProfileCache = tenantProfileCache; + this.apiUsageClient = apiUsageClient; + this.rateLimitService = rateLimitService; + this.dataDecodingEncodingService = dataDecodingEncodingService; + this.scheduler = scheduler; } @PostConstruct public void init() { - if (rateLimitEnabled) { - //Just checking the configuration parameters - new TbRateLimits(perTenantLimitsConf); - new TbRateLimits(perDevicesLimitsConf); - } this.ruleEngineProducerStats = statsFactory.createMessagesStats(StatsType.RULE_ENGINE.getName() + ".producer"); this.tbCoreProducerStats = statsFactory.createMessagesStats(StatsType.CORE.getName() + ".producer"); this.transportApiStats = statsFactory.createMessagesStats(StatsType.TRANSPORT.getName() + ".producer"); - this.schedulerExecutor = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("transport-scheduler")); this.transportCallbackExecutor = Executors.newWorkStealingPool(20); - this.schedulerExecutor.scheduleAtFixedRate(this::checkInactivityAndReportActivity, new Random().nextInt((int) sessionReportTimeout), sessionReportTimeout, TimeUnit.MILLISECONDS); + this.scheduler.scheduleAtFixedRate(this::checkInactivityAndReportActivity, new Random().nextInt((int) sessionReportTimeout), sessionReportTimeout, TimeUnit.MILLISECONDS); transportApiRequestTemplate = queueProvider.createTransportApiRequestTemplate(); transportApiRequestTemplate.setMessagesStats(transportApiStats); ruleEngineMsgProducer = producerProvider.getRuleEngineMsgProducer(); @@ -156,6 +182,7 @@ public class DefaultTransportService implements TransportService { TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_TRANSPORT, serviceInfoProvider.getServiceId()); transportNotificationsConsumer.subscribe(Collections.singleton(tpi)); transportApiRequestTemplate.init(); + mainConsumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("transport-consumer")); mainConsumerExecutor.execute(() -> { while (!stopped) { try { @@ -187,18 +214,11 @@ public class DefaultTransportService implements TransportService { @PreDestroy public void destroy() { - if (rateLimitEnabled) { - perTenantLimits.clear(); - perDeviceLimits.clear(); - } stopped = true; if (transportNotificationsConsumer != null) { transportNotificationsConsumer.unsubscribe(); } - if (schedulerExecutor != null) { - schedulerExecutor.shutdownNow(); - } if (transportCallbackExecutor != null) { transportCallbackExecutor.shutdownNow(); } @@ -216,39 +236,106 @@ public class DefaultTransportService implements TransportService { } @Override - public TransportProtos.GetTenantRoutingInfoResponseMsg getRoutingInfo(TransportProtos.GetTenantRoutingInfoRequestMsg msg) { + public TransportProtos.GetEntityProfileResponseMsg getEntityProfile(TransportProtos.GetEntityProfileRequestMsg msg) { TbProtoQueueMsg protoMsg = - new TbProtoQueueMsg<>(UUID.randomUUID(), TransportProtos.TransportApiRequestMsg.newBuilder().setGetTenantRoutingInfoRequestMsg(msg).build()); + new TbProtoQueueMsg<>(UUID.randomUUID(), TransportProtos.TransportApiRequestMsg.newBuilder().setEntityProfileRequestMsg(msg).build()); try { TbProtoQueueMsg response = transportApiRequestTemplate.send(protoMsg).get(); - return response.getValue().getGetTenantRoutingInfoResponseMsg(); + return response.getValue().getEntityProfileResponseMsg(); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } } @Override - public void process(TransportProtos.ValidateDeviceTokenRequestMsg msg, TransportServiceCallback callback) { + public void process(DeviceTransportType transportType, TransportProtos.ValidateDeviceTokenRequestMsg msg, + TransportServiceCallback callback) { log.trace("Processing msg: {}", msg); - TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setValidateTokenRequestMsg(msg).build()); - AsyncCallbackTemplate.withCallback(transportApiRequestTemplate.send(protoMsg), - response -> callback.onSuccess(response.getValue().getValidateTokenResponseMsg()), callback::onError, transportCallbackExecutor); + TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), + TransportApiRequestMsg.newBuilder().setValidateTokenRequestMsg(msg).build()); + doProcess(transportType, protoMsg, callback); } @Override - public void process(TransportProtos.ValidateDeviceX509CertRequestMsg msg, TransportServiceCallback callback) { + public void process(DeviceTransportType transportType, TransportProtos.ValidateBasicMqttCredRequestMsg msg, + TransportServiceCallback callback) { log.trace("Processing msg: {}", msg); - TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setValidateX509CertRequestMsg(msg).build()); - AsyncCallbackTemplate.withCallback(transportApiRequestTemplate.send(protoMsg), - response -> callback.onSuccess(response.getValue().getValidateTokenResponseMsg()), callback::onError, transportCallbackExecutor); + TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), + TransportApiRequestMsg.newBuilder().setValidateBasicMqttCredRequestMsg(msg).build()); + doProcess(transportType, protoMsg, callback); } @Override - public void process(TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg msg, TransportServiceCallback callback) { + public void process(DeviceTransportType transportType, TransportProtos.ValidateDeviceX509CertRequestMsg msg, TransportServiceCallback callback) { log.trace("Processing msg: {}", msg); - TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setGetOrCreateDeviceRequestMsg(msg).build()); - AsyncCallbackTemplate.withCallback(transportApiRequestTemplate.send(protoMsg), - response -> callback.onSuccess(response.getValue().getGetOrCreateDeviceResponseMsg()), callback::onError, transportCallbackExecutor); + TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setValidateX509CertRequestMsg(msg).build()); + doProcess(transportType, protoMsg, callback); + } + + private void doProcess(DeviceTransportType transportType, TbProtoQueueMsg protoMsg, + TransportServiceCallback callback) { + ListenableFuture response = Futures.transform(transportApiRequestTemplate.send(protoMsg), tmp -> { + TransportProtos.ValidateDeviceCredentialsResponseMsg msg = tmp.getValue().getValidateCredResponseMsg(); + ValidateDeviceCredentialsResponse.ValidateDeviceCredentialsResponseBuilder result = ValidateDeviceCredentialsResponse.builder(); + if (msg.hasDeviceInfo()) { + result.credentials(msg.getCredentialsBody()); + TransportDeviceInfo tdi = getTransportDeviceInfo(msg.getDeviceInfo()); + result.deviceInfo(tdi); + ByteString profileBody = msg.getProfileBody(); + if (profileBody != null && !profileBody.isEmpty()) { + DeviceProfile profile = deviceProfileCache.getOrCreate(tdi.getDeviceProfileId(), profileBody); + if (transportType != DeviceTransportType.DEFAULT + && profile != null && profile.getTransportType() != DeviceTransportType.DEFAULT && profile.getTransportType() != transportType) { + log.debug("[{}] Device profile [{}] has different transport type: {}, expected: {}", tdi.getDeviceId(), tdi.getDeviceProfileId(), profile.getTransportType(), transportType); + throw new IllegalStateException("Device profile has different transport type: " + profile.getTransportType() + ". Expected: " + transportType); + } + result.deviceProfile(profile); + } + } + return result.build(); + }, MoreExecutors.directExecutor()); + AsyncCallbackTemplate.withCallback(response, callback::onSuccess, callback::onError, transportCallbackExecutor); + } + + @Override + public void process(TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg requestMsg, TransportServiceCallback callback) { + TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setGetOrCreateDeviceRequestMsg(requestMsg).build()); + log.trace("Processing msg: {}", requestMsg); + ListenableFuture response = Futures.transform(transportApiRequestTemplate.send(protoMsg), tmp -> { + TransportProtos.GetOrCreateDeviceFromGatewayResponseMsg msg = tmp.getValue().getGetOrCreateDeviceResponseMsg(); + GetOrCreateDeviceFromGatewayResponse.GetOrCreateDeviceFromGatewayResponseBuilder result = GetOrCreateDeviceFromGatewayResponse.builder(); + if (msg.hasDeviceInfo()) { + TransportDeviceInfo tdi = getTransportDeviceInfo(msg.getDeviceInfo()); + result.deviceInfo(tdi); + ByteString profileBody = msg.getProfileBody(); + if (profileBody != null && !profileBody.isEmpty()) { + result.deviceProfile(deviceProfileCache.getOrCreate(tdi.getDeviceProfileId(), profileBody)); + } + } + return result.build(); + }, MoreExecutors.directExecutor()); + AsyncCallbackTemplate.withCallback(response, callback::onSuccess, callback::onError, transportCallbackExecutor); + } + + private TransportDeviceInfo getTransportDeviceInfo(TransportProtos.DeviceInfoProto di) { + TransportDeviceInfo tdi = new TransportDeviceInfo(); + tdi.setTenantId(new TenantId(new UUID(di.getTenantIdMSB(), di.getTenantIdLSB()))); + tdi.setDeviceId(new DeviceId(new UUID(di.getDeviceIdMSB(), di.getDeviceIdLSB()))); + tdi.setDeviceProfileId(new DeviceProfileId(new UUID(di.getDeviceProfileIdMSB(), di.getDeviceProfileIdLSB()))); + tdi.setAdditionalInfo(di.getAdditionalInfo()); + tdi.setDeviceName(di.getDeviceName()); + tdi.setDeviceType(di.getDeviceType()); + return tdi; + } + + @Override + public void process(ProvisionDeviceRequestMsg requestMsg, TransportServiceCallback callback) { + log.trace("Processing msg: {}", requestMsg); + TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setProvisionDeviceRequestMsg(requestMsg).build()); + ListenableFuture response = Futures.transform(transportApiRequestTemplate.send(protoMsg), tmp -> + tmp.getValue().getProvisionDeviceResponseMsg() + , MoreExecutors.directExecutor()); + AsyncCallbackTemplate.withCallback(response, callback::onSuccess, callback::onError, transportCallbackExecutor); } @Override @@ -271,26 +358,30 @@ public class DefaultTransportService implements TransportService { @Override public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.PostTelemetryMsg msg, TransportServiceCallback callback) { - if (checkLimits(sessionInfo, msg, callback)) { + int dataPoints = 0; + for (TransportProtos.TsKvListProto tsKv : msg.getTsKvListList()) { + dataPoints += tsKv.getKvCount(); + } + if (checkLimits(sessionInfo, msg, callback, dataPoints)) { reportActivityInternal(sessionInfo); TenantId tenantId = new TenantId(new UUID(sessionInfo.getTenantIdMSB(), sessionInfo.getTenantIdLSB())); DeviceId deviceId = new DeviceId(new UUID(sessionInfo.getDeviceIdMSB(), sessionInfo.getDeviceIdLSB())); - MsgPackCallback packCallback = new MsgPackCallback(msg.getTsKvListCount(), callback); + MsgPackCallback packCallback = new MsgPackCallback(msg.getTsKvListCount(), new ApiStatsProxyCallback<>(tenantId, dataPoints, callback)); for (TransportProtos.TsKvListProto tsKv : msg.getTsKvListList()) { TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("deviceName", sessionInfo.getDeviceName()); metaData.putValue("deviceType", sessionInfo.getDeviceType()); metaData.putValue("ts", tsKv.getTs() + ""); JsonObject json = JsonUtils.getJsonObject(tsKv.getKvList()); - TbMsg tbMsg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, metaData, gson.toJson(json)); - sendToRuleEngine(tenantId, tbMsg, packCallback); + sendToRuleEngine(tenantId, deviceId, sessionInfo, json, metaData, SessionMsgType.POST_TELEMETRY_REQUEST, packCallback); + } } } @Override public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.PostAttributeMsg msg, TransportServiceCallback callback) { - if (checkLimits(sessionInfo, msg, callback)) { + if (checkLimits(sessionInfo, msg, callback, msg.getKvCount())) { reportActivityInternal(sessionInfo); TenantId tenantId = new TenantId(new UUID(sessionInfo.getTenantIdMSB(), sessionInfo.getTenantIdLSB())); DeviceId deviceId = new DeviceId(new UUID(sessionInfo.getDeviceIdMSB(), sessionInfo.getDeviceIdLSB())); @@ -299,8 +390,7 @@ public class DefaultTransportService implements TransportService { metaData.putValue("deviceName", sessionInfo.getDeviceName()); metaData.putValue("deviceType", sessionInfo.getDeviceType()); metaData.putValue("notifyDevice", "false"); - TbMsg tbMsg = TbMsg.newMsg(SessionMsgType.POST_ATTRIBUTES_REQUEST.name(), deviceId, metaData, gson.toJson(json)); - sendToRuleEngine(tenantId, tbMsg, new TransportTbQueueCallback(callback)); + sendToRuleEngine(tenantId, deviceId, sessionInfo, json, metaData, SessionMsgType.POST_ATTRIBUTES_REQUEST, new TransportTbQueueCallback(callback)); } } @@ -381,12 +471,11 @@ public class DefaultTransportService implements TransportService { metaData.putValue("requestId", Integer.toString(msg.getRequestId())); metaData.putValue("serviceId", serviceInfoProvider.getServiceId()); metaData.putValue("sessionId", sessionId.toString()); - TbMsg tbMsg = TbMsg.newMsg(SessionMsgType.TO_SERVER_RPC_REQUEST.name(), deviceId, metaData, TbMsgDataType.JSON, gson.toJson(json)); - sendToRuleEngine(tenantId, tbMsg, new TransportTbQueueCallback(callback)); - + sendToRuleEngine(tenantId, deviceId, sessionInfo, json, metaData, + SessionMsgType.TO_SERVER_RPC_REQUEST, new TransportTbQueueCallback(callback)); String requestId = sessionId + "-" + msg.getRequestId(); toServerRpcPendingMap.put(requestId, new RpcRequestMetadata(sessionId, msg.getRequestId())); - schedulerExecutor.schedule(() -> processTimeout(requestId), clientSideRpcTimeout, TimeUnit.MILLISECONDS); + scheduler.schedule(() -> processTimeout(requestId), clientSideRpcTimeout, TimeUnit.MILLISECONDS); } } @@ -459,7 +548,7 @@ public class DefaultTransportService implements TransportService { SessionMetaData currentSession = new SessionMetaData(sessionInfo, TransportProtos.SessionType.SYNC, listener); sessions.putIfAbsent(toSessionId(sessionInfo), currentSession); - ScheduledFuture executorFuture = schedulerExecutor.schedule(() -> { + ScheduledFuture executorFuture = scheduler.schedule(() -> { listener.onRemoteSessionCloseCommand(TransportProtos.SessionCloseNotificationProto.getDefaultInstance()); deregisterSession(sessionInfo); }, timeout, TimeUnit.MILLISECONDS); @@ -477,38 +566,26 @@ public class DefaultTransportService implements TransportService { sessions.remove(toSessionId(sessionInfo)); } - @Override - public boolean checkLimits(TransportProtos.SessionInfoProto sessionInfo, Object msg, TransportServiceCallback callback) { + private boolean checkLimits(TransportProtos.SessionInfoProto sessionInfo, Object msg, TransportServiceCallback callback) { + return checkLimits(sessionInfo, msg, callback, 0); + } + + private boolean checkLimits(TransportProtos.SessionInfoProto sessionInfo, Object msg, TransportServiceCallback callback, int dataPoints) { if (log.isTraceEnabled()) { log.trace("[{}] Processing msg: {}", toSessionId(sessionInfo), msg); } - if (!rateLimitEnabled) { - return true; - } TenantId tenantId = new TenantId(new UUID(sessionInfo.getTenantIdMSB(), sessionInfo.getTenantIdLSB())); - TbRateLimits rateLimits = perTenantLimits.computeIfAbsent(tenantId, id -> new TbRateLimits(perTenantLimitsConf)); - if (!rateLimits.tryConsume()) { - if (callback != null) { - callback.onError(new TbRateLimitsException(EntityType.TENANT)); - } - if (log.isTraceEnabled()) { - log.trace("[{}][{}] Tenant level rate limit detected: {}", toSessionId(sessionInfo), tenantId, msg); - } - return false; - } DeviceId deviceId = new DeviceId(new UUID(sessionInfo.getDeviceIdMSB(), sessionInfo.getDeviceIdLSB())); - rateLimits = perDeviceLimits.computeIfAbsent(deviceId, id -> new TbRateLimits(perDevicesLimitsConf)); - if (!rateLimits.tryConsume()) { + + EntityType rateLimitedEntityType = rateLimitService.checkLimits(tenantId, deviceId, dataPoints); + if (rateLimitedEntityType == null) { + return true; + } else { if (callback != null) { - callback.onError(new TbRateLimitsException(EntityType.DEVICE)); - } - if (log.isTraceEnabled()) { - log.trace("[{}][{}] Device level rate limit detected: {}", toSessionId(sessionInfo), deviceId, msg); + callback.onError(new TbRateLimitsException(rateLimitedEntityType)); } return false; } - - return true; } protected void processToTransportMsg(TransportProtos.ToTransportMsg toSessionMsg) { @@ -539,11 +616,64 @@ public class DefaultTransportService implements TransportService { deregisterSession(md.getSessionInfo()); } } else { - //TODO: should we notify the device actor about missed session? - log.debug("[{}] Missing session.", sessionId); + if (toSessionMsg.hasEntityUpdateMsg()) { + TransportProtos.EntityUpdateMsg msg = toSessionMsg.getEntityUpdateMsg(); + EntityType entityType = EntityType.valueOf(msg.getEntityType()); + if (EntityType.DEVICE_PROFILE.equals(entityType)) { + DeviceProfile deviceProfile = deviceProfileCache.put(msg.getData()); + if (deviceProfile != null) { + onProfileUpdate(deviceProfile); + } + } else if (EntityType.TENANT_PROFILE.equals(entityType)) { + rateLimitService.update(tenantProfileCache.put(msg.getData())); + } else if (EntityType.TENANT.equals(entityType)) { + Optional profileOpt = dataDecodingEncodingService.decode(msg.getData().toByteArray()); + if (profileOpt.isPresent()) { + Tenant tenant = profileOpt.get(); + boolean updated = tenantProfileCache.put(tenant.getId(), tenant.getTenantProfileId()); + if (updated) { + rateLimitService.update(tenant.getId()); + } + } + } else if (EntityType.API_USAGE_STATE.equals(entityType)) { + Optional stateOpt = dataDecodingEncodingService.decode(msg.getData().toByteArray()); + if (stateOpt.isPresent()) { + ApiUsageState apiUsageState = stateOpt.get(); + rateLimitService.update(apiUsageState.getTenantId(), apiUsageState.isTransportEnabled()); + //TODO: if transport is disabled, we should close all sessions and not to check credentials. + } + } + } else if (toSessionMsg.hasEntityDeleteMsg()) { + TransportProtos.EntityDeleteMsg msg = toSessionMsg.getEntityDeleteMsg(); + EntityType entityType = EntityType.valueOf(msg.getEntityType()); + UUID entityUuid = new UUID(msg.getEntityIdMSB(), msg.getEntityIdLSB()); + if (EntityType.DEVICE_PROFILE.equals(entityType)) { + deviceProfileCache.evict(new DeviceProfileId(new UUID(msg.getEntityIdMSB(), msg.getEntityIdLSB()))); + } else if (EntityType.TENANT_PROFILE.equals(entityType)) { + tenantProfileCache.remove(new TenantProfileId(entityUuid)); + } else if (EntityType.TENANT.equals(entityType)) { + rateLimitService.remove(new TenantId(entityUuid)); + } else if (EntityType.DEVICE.equals(entityType)) { + rateLimitService.remove(new DeviceId(entityUuid)); + } + } else { + //TODO: should we notify the device actor about missed session? + log.debug("[{}] Missing session.", sessionId); + } } } + private void onProfileUpdate(DeviceProfile deviceProfile) { + long deviceProfileIdMSB = deviceProfile.getId().getId().getMostSignificantBits(); + long deviceProfileIdLSB = deviceProfile.getId().getId().getLeastSignificantBits(); + sessions.forEach((id, md) -> { + if (md.getSessionInfo().getDeviceProfileIdMSB() == deviceProfileIdMSB + && md.getSessionInfo().getDeviceProfileIdLSB() == deviceProfileIdLSB) { + transportCallbackExecutor.submit(() -> md.getListener().onProfileUpdate(deviceProfile)); + } + }); + } + protected UUID toSessionId(TransportProtos.SessionInfoProto sessionInfo) { return new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB()); } @@ -594,6 +724,27 @@ public class DefaultTransportService implements TransportService { ruleEngineMsgProducer.send(tpi, new TbProtoQueueMsg<>(tbMsg.getId(), msg), wrappedCallback); } + protected void sendToRuleEngine(TenantId tenantId, DeviceId deviceId, TransportProtos.SessionInfoProto sessionInfo, JsonObject json, + TbMsgMetaData metaData, SessionMsgType sessionMsgType, TbQueueCallback callback) { + DeviceProfileId deviceProfileId = new DeviceProfileId(new UUID(sessionInfo.getDeviceProfileIdMSB(), sessionInfo.getDeviceProfileIdLSB())); + DeviceProfile deviceProfile = deviceProfileCache.get(deviceProfileId); + RuleChainId ruleChainId; + String queueName; + + if (deviceProfile == null) { + log.warn("[{}] Device profile is null!", deviceProfileId); + ruleChainId = null; + queueName = ServiceQueue.MAIN; + } else { + ruleChainId = deviceProfile.getDefaultRuleChainId(); + String defaultQueueName = deviceProfile.getDefaultQueueName(); + queueName = defaultQueueName != null ? defaultQueueName : ServiceQueue.MAIN; + } + + TbMsg tbMsg = TbMsg.newMsg(queueName, sessionMsgType.name(), deviceId, metaData, gson.toJson(json), ruleChainId, null); + sendToRuleEngine(tenantId, tbMsg, callback); + } + private class TransportTbQueueCallback implements TbQueueCallback { private final TransportServiceCallback callback; @@ -612,7 +763,7 @@ public class DefaultTransportService implements TransportService { } } - private class StatsCallback implements TbQueueCallback { + private static class StatsCallback implements TbQueueCallback { private final TbQueueCallback callback; private final MessagesStats stats; @@ -657,4 +808,31 @@ public class DefaultTransportService implements TransportService { callback.onError(t); } } + + private class ApiStatsProxyCallback implements TransportServiceCallback { + private final TenantId tenantId; + private final int dataPoints; + private final TransportServiceCallback callback; + + public ApiStatsProxyCallback(TenantId tenantId, int dataPoints, TransportServiceCallback callback) { + this.tenantId = tenantId; + this.dataPoints = dataPoints; + this.callback = callback; + } + + @Override + public void onSuccess(T msg) { + try { + apiUsageClient.report(tenantId, ApiUsageRecordKey.TRANSPORT_MSG_COUNT, 1); + apiUsageClient.report(tenantId, ApiUsageRecordKey.TRANSPORT_DP_COUNT, dataPoints); + } finally { + callback.onSuccess(msg); + } + } + + @Override + public void onError(Throwable e) { + callback.onError(e); + } + } } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportTenantProfileCache.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportTenantProfileCache.java new file mode 100644 index 0000000000..6773d09417 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportTenantProfileCache.java @@ -0,0 +1,163 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.service; + +import com.google.protobuf.ByteString; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.common.transport.TransportService; +import org.thingsboard.server.common.transport.TransportTenantProfileCache; +import org.thingsboard.server.common.transport.limits.TransportRateLimitService; +import org.thingsboard.server.common.transport.profile.TenantProfileUpdateResult; +import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.util.TbTransportComponent; + +import java.util.Collections; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@Component +@TbTransportComponent +@Slf4j +public class DefaultTransportTenantProfileCache implements TransportTenantProfileCache { + + private final Lock tenantProfileFetchLock = new ReentrantLock(); + private final ConcurrentMap profiles = new ConcurrentHashMap<>(); + private final ConcurrentMap tenantIds = new ConcurrentHashMap<>(); + private final ConcurrentMap> tenantProfileIds = new ConcurrentHashMap<>(); + private final DataDecodingEncodingService dataDecodingEncodingService; + + private TransportRateLimitService rateLimitService; + private TransportService transportService; + + @Lazy + @Autowired + public void setRateLimitService(TransportRateLimitService rateLimitService) { + this.rateLimitService = rateLimitService; + } + + @Lazy + @Autowired + public void setTransportService(TransportService transportService) { + this.transportService = transportService; + } + + public DefaultTransportTenantProfileCache(DataDecodingEncodingService dataDecodingEncodingService) { + this.dataDecodingEncodingService = dataDecodingEncodingService; + } + + @Override + public TenantProfile get(TenantId tenantId) { + return getTenantProfile(tenantId); + } + + @Override + public TenantProfileUpdateResult put(ByteString profileBody) { + Optional profileOpt = dataDecodingEncodingService.decode(profileBody.toByteArray()); + if (profileOpt.isPresent()) { + TenantProfile newProfile = profileOpt.get(); + log.trace("[{}] put: {}", newProfile.getId(), newProfile); + Set affectedTenants = tenantProfileIds.get(newProfile.getId()); + return new TenantProfileUpdateResult(newProfile, affectedTenants != null ? affectedTenants : Collections.emptySet()); + } else { + log.warn("Failed to decode profile: {}", profileBody.toString()); + return new TenantProfileUpdateResult(null, Collections.emptySet()); + } + } + + @Override + public boolean put(TenantId tenantId, TenantProfileId profileId) { + log.trace("[{}] put: {}", tenantId, profileId); + TenantProfileId oldProfileId = tenantIds.get(tenantId); + if (oldProfileId != null && !oldProfileId.equals(profileId)) { + tenantProfileIds.computeIfAbsent(oldProfileId, id -> ConcurrentHashMap.newKeySet()).remove(tenantId); + tenantIds.put(tenantId, profileId); + tenantProfileIds.computeIfAbsent(profileId, id -> ConcurrentHashMap.newKeySet()).add(tenantId); + return true; + } else { + return false; + } + } + + @Override + public Set remove(TenantProfileId profileId) { + Set tenants = tenantProfileIds.remove(profileId); + if (tenants != null) { + tenants.forEach(tenantIds::remove); + } + profiles.remove(profileId); + return tenants; + } + + private TenantProfile getTenantProfile(TenantId tenantId) { + TenantProfile profile = null; + TenantProfileId tenantProfileId = tenantIds.get(tenantId); + if (tenantProfileId != null) { + profile = profiles.get(tenantProfileId); + } + if (profile == null) { + tenantProfileFetchLock.lock(); + try { + tenantProfileId = tenantIds.get(tenantId); + if (tenantProfileId != null) { + profile = profiles.get(tenantProfileId); + } + if (profile == null) { + TransportProtos.GetEntityProfileRequestMsg msg = TransportProtos.GetEntityProfileRequestMsg.newBuilder() + .setEntityType(EntityType.TENANT.name()) + .setEntityIdMSB(tenantId.getId().getMostSignificantBits()) + .setEntityIdLSB(tenantId.getId().getLeastSignificantBits()) + .build(); + TransportProtos.GetEntityProfileResponseMsg entityProfileMsg = transportService.getEntityProfile(msg); + Optional profileOpt = dataDecodingEncodingService.decode(entityProfileMsg.getData().toByteArray()); + if (profileOpt.isPresent()) { + profile = profileOpt.get(); + TenantProfile existingProfile = profiles.get(profile.getId()); + if (existingProfile != null) { + profile = existingProfile; + } else { + profiles.put(profile.getId(), profile); + } + tenantProfileIds.computeIfAbsent(profile.getId(), id -> ConcurrentHashMap.newKeySet()).add(tenantId); + tenantIds.put(tenantId, profile.getId()); + } else { + log.warn("[{}] Can't decode tenant profile: {}", tenantId, entityProfileMsg.getData()); + throw new RuntimeException("Can't decode tenant profile!"); + } + Optional apiStateOpt = dataDecodingEncodingService.decode(entityProfileMsg.getApiState().toByteArray()); + apiStateOpt.ifPresent(apiUsageState -> rateLimitService.update(tenantId, apiUsageState.isTransportEnabled())); + } + } finally { + tenantProfileFetchLock.unlock(); + } + } + return profile; + } + + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportTenantRoutingInfoService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportTenantRoutingInfoService.java index f2534a64c9..55dee282ec 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportTenantRoutingInfoService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportTenantRoutingInfoService.java @@ -16,14 +16,11 @@ package org.thingsboard.server.common.transport.service; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; -import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.transport.TransportService; -import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoRequestMsg; -import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoResponseMsg; +import org.thingsboard.server.common.transport.TransportTenantProfileCache; import org.thingsboard.server.queue.discovery.TenantRoutingInfo; import org.thingsboard.server.queue.discovery.TenantRoutingInfoService; @@ -32,21 +29,16 @@ import org.thingsboard.server.queue.discovery.TenantRoutingInfoService; @ConditionalOnExpression("'${service.type:null}'=='tb-transport'") public class TransportTenantRoutingInfoService implements TenantRoutingInfoService { - private TransportService transportService; + private TransportTenantProfileCache tenantProfileCache; - @Lazy - @Autowired - public void setTransportService(TransportService transportService) { - this.transportService = transportService; + public TransportTenantRoutingInfoService(TransportTenantProfileCache tenantProfileCache) { + this.tenantProfileCache = tenantProfileCache; } @Override public TenantRoutingInfo getRoutingInfo(TenantId tenantId) { - GetTenantRoutingInfoRequestMsg msg = GetTenantRoutingInfoRequestMsg.newBuilder() - .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) - .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) - .build(); - GetTenantRoutingInfoResponseMsg routingInfo = transportService.getRoutingInfo(msg); - return new TenantRoutingInfo(tenantId, routingInfo.getIsolatedTbCore(), routingInfo.getIsolatedTbRuleEngine()); + TenantProfile profile = tenantProfileCache.get(tenantId); + return new TenantRoutingInfo(tenantId, profile.isIsolatedTbCore(), profile.isIsolatedTbRuleEngine()); } + } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java index c312b3a4d9..2f7f2d69e6 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java @@ -17,8 +17,12 @@ package org.thingsboard.server.common.transport.session; import lombok.Data; import lombok.Getter; +import lombok.Setter; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.msg.session.SessionContext; +import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto; import java.util.UUID; @@ -34,17 +38,31 @@ public abstract class DeviceAwareSessionContext implements SessionContext { @Getter private volatile DeviceId deviceId; @Getter - private volatile DeviceInfoProto deviceInfo; + protected volatile TransportDeviceInfo deviceInfo; + @Getter + @Setter + protected volatile DeviceProfile deviceProfile; + @Getter + @Setter + private volatile TransportProtos.SessionInfoProto sessionInfo; + private volatile boolean connected; public DeviceId getDeviceId() { return deviceId; } - public void setDeviceInfo(DeviceInfoProto deviceInfo) { + public void setDeviceInfo(TransportDeviceInfo deviceInfo) { this.deviceInfo = deviceInfo; this.connected = true; - this.deviceId = new DeviceId(new UUID(deviceInfo.getDeviceIdMSB(), deviceInfo.getDeviceIdLSB())); + this.deviceId = deviceInfo.getDeviceId(); + } + + @Override + public void onProfileUpdate(DeviceProfile deviceProfile) { + this.deviceProfile = deviceProfile; + this.deviceInfo.setDeviceType(deviceProfile.getName()); + this.sessionInfo = TransportProtos.SessionInfoProto.newBuilder().mergeFrom(sessionInfo).setDeviceType(deviceProfile.getName()).build(); } public boolean isConnected() { diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/DataDecodingEncodingService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/DataDecodingEncodingService.java new file mode 100644 index 0000000000..bf70fa5ef1 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/DataDecodingEncodingService.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.util; + +import java.util.Optional; + +public interface DataDecodingEncodingService { + + Optional decode(byte[] byteArray); + + byte[] encode(T msq); + +} + diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/ProtoWithFSTService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/ProtoWithFSTService.java new file mode 100644 index 0000000000..eee9dacfe4 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/ProtoWithFSTService.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.util; + +import lombok.extern.slf4j.Slf4j; +import org.nustaq.serialization.FSTConfiguration; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.msg.TbActorMsg; +import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; + +import java.util.Optional; + +@Slf4j +@Service +public class ProtoWithFSTService implements DataDecodingEncodingService { + + private final FSTConfiguration config = FSTConfiguration.createDefaultConfiguration(); + + @Override + public Optional decode(byte[] byteArray) { + try { + T msg = (T) config.asObject(byteArray); + return Optional.of(msg); + } catch (IllegalArgumentException e) { + log.error("Error during deserialization message, [{}]", e.getMessage()); + return Optional.empty(); + } + } + + @Override + public byte[] encode(T msq) { + return config.asByteArray(msq); + } + +} diff --git a/common/transport/transport-api/src/main/proto/transport.proto b/common/transport/transport-api/src/main/proto/transport.proto new file mode 100644 index 0000000000..b536c22198 --- /dev/null +++ b/common/transport/transport-api/src/main/proto/transport.proto @@ -0,0 +1,101 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +syntax = "proto3"; +package transportapi; + +option java_package = "org.thingsboard.server.gen.transport"; +option java_outer_classname = "TransportApiProtos"; + +import "queue.proto"; + +message ClaimDevice { + string secretKey = 1; + int64 durationMs = 2; +} + +message AttributesRequest { + string clientKeys = 1; + string sharedKeys = 2; +} + +message RpcRequest { + string method = 1; + string params = 2; +} + +message DisconnectMsg { + string deviceName = 1; +} + +message ConnectMsg { + string deviceName = 1; + string deviceType = 2; +} + +message TelemetryMsg { + string deviceName = 1; + transport.PostTelemetryMsg msg = 3; +} + +message AttributesMsg { + string deviceName = 1; + transport.PostAttributeMsg msg = 2; +} + +message ClaimDeviceMsg { + string deviceName = 1; + ClaimDevice claimRequest = 2; +} + +message GatewayTelemetryMsg { + repeated TelemetryMsg msg = 1; +} + +message GatewayClaimMsg { + repeated ClaimDeviceMsg msg = 1; +} + +message GatewayAttributesMsg { + repeated AttributesMsg msg = 1; +} + +message GatewayRpcResponseMsg { + string deviceName = 1; + int32 id = 2; + string data = 3; +} + +message GatewayAttributeResponseMsg { + string deviceName = 1; + transport.GetAttributeResponseMsg responseMsg = 2; +} + +message GatewayAttributeUpdateNotificationMsg { + string deviceName = 1; + transport.AttributeUpdateNotificationMsg notificationMsg = 2; +} + +message GatewayDeviceRpcRequestMsg { + string deviceName = 1; + transport.ToDeviceRpcRequestMsg rpcRequestMsg = 2; +} + +message GatewayAttributesRequestMsg { + int32 id = 1; + string deviceName = 2; + bool client = 3; + repeated string keys = 4; +} diff --git a/common/util/pom.xml b/common/util/pom.xml index fceed3ee0e..076eff152a 100644 --- a/common/util/pom.xml +++ b/common/util/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT common org.thingsboard.common diff --git a/dao/pom.xml b/dao/pom.xml index 8c1cd17bf4..ec4960eb5f 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT thingsboard dao @@ -125,16 +125,16 @@ spring-security-oauth2-client - com.datastax.cassandra - cassandra-driver-core + com.datastax.oss + java-driver-core - com.datastax.cassandra - cassandra-driver-mapping + com.datastax.oss + java-driver-query-builder - com.datastax.cassandra - cassandra-driver-extras + io.dropwizard.metrics + metrics-jmx io.takari.junit @@ -153,6 +153,10 @@ org.slf4j slf4j-log4j12 + + org.hibernate + hibernate-validator + test diff --git a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java index 79883875ab..26290fcae9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java @@ -15,16 +15,62 @@ */ package org.thingsboard.server.dao; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.thingsboard.server.common.data.id.UUIDBased; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.dao.model.ToData; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; public abstract class DaoUtil { private DaoUtil() { } + public static PageData toPageData(Page> page) { + List data = convertDataList(page.getContent()); + return new PageData(data, page.getTotalPages(), page.getTotalElements(), page.hasNext()); + } + + public static PageData pageToPageData(Page page) { + return new PageData(page.getContent(), page.getTotalPages(), page.getTotalElements(), page.hasNext()); + } + + public static Pageable toPageable(PageLink pageLink) { + return toPageable(pageLink, Collections.emptyMap()); + } + + public static Pageable toPageable(PageLink pageLink, Map columnMap) { + return PageRequest.of(pageLink.getPage(), pageLink.getPageSize(), toSort(pageLink.getSortOrder(), columnMap)); + } + + public static Sort toSort(SortOrder sortOrder) { + return toSort(sortOrder, Collections.emptyMap()); + } + + public static Sort toSort(SortOrder sortOrder, Map columnMap) { + if (sortOrder == null) { + return Sort.unsorted(); + } else { + String property = sortOrder.getProperty(); + if (columnMap.containsKey(property)) { + property = columnMap.get(property); + } + return Sort.by(Sort.Direction.fromString(sortOrder.getDirection().name()), property); + } + } + public static List convertDataList(Collection> toDataList) { List list = Collections.emptyList(); if (toDataList != null && !toDataList.isEmpty()) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/HsqlTsDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/HsqlTsDaoConfig.java index e2519191e9..3a54545952 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/HsqlTsDaoConfig.java +++ b/dao/src/main/java/org/thingsboard/server/dao/HsqlTsDaoConfig.java @@ -27,8 +27,8 @@ import org.thingsboard.server.dao.util.SqlTsDao; @Configuration @EnableAutoConfiguration @ComponentScan({"org.thingsboard.server.dao.sqlts.hsql"}) -@EnableJpaRepositories({"org.thingsboard.server.dao.sqlts.ts", "org.thingsboard.server.dao.sqlts.insert.hsql", "org.thingsboard.server.dao.sqlts.insert.latest.hsql", "org.thingsboard.server.dao.sqlts.latest", "org.thingsboard.server.dao.sqlts.dictionary"}) -@EntityScan({"org.thingsboard.server.dao.model.sqlts.ts", "org.thingsboard.server.dao.model.sqlts.latest", "org.thingsboard.server.dao.model.sqlts.dictionary"}) +@EnableJpaRepositories({"org.thingsboard.server.dao.sqlts.ts", "org.thingsboard.server.dao.sqlts.insert.hsql"}) +@EntityScan({"org.thingsboard.server.dao.model.sqlts.ts"}) @EnableTransactionManagement @SqlTsDao @HsqlDao diff --git a/dao/src/main/java/org/thingsboard/server/dao/HsqlTsLatestDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/HsqlTsLatestDaoConfig.java new file mode 100644 index 0000000000..c814baebfa --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/HsqlTsLatestDaoConfig.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.thingsboard.server.dao.util.HsqlDao; +import org.thingsboard.server.dao.util.SqlTsLatestDao; + +@Configuration +@EnableAutoConfiguration +@ComponentScan({"org.thingsboard.server.dao.sqlts.hsql"}) +@EnableJpaRepositories({"org.thingsboard.server.dao.sqlts.insert.latest.hsql", "org.thingsboard.server.dao.sqlts.latest"}) +@EntityScan({"org.thingsboard.server.dao.model.sqlts.latest"}) +@EnableTransactionManagement +@SqlTsLatestDao +@HsqlDao +public class HsqlTsLatestDaoConfig { + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/JpaDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/JpaDaoConfig.java index 796f98d238..32db43271c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/JpaDaoConfig.java +++ b/dao/src/main/java/org/thingsboard/server/dao/JpaDaoConfig.java @@ -21,7 +21,6 @@ import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.transaction.annotation.EnableTransactionManagement; -import org.thingsboard.server.dao.util.SqlDao; /** * @author Valerii Sosliuk @@ -32,7 +31,6 @@ import org.thingsboard.server.dao.util.SqlDao; @EnableJpaRepositories("org.thingsboard.server.dao.sql") @EntityScan("org.thingsboard.server.dao.model.sql") @EnableTransactionManagement -@SqlDao public class JpaDaoConfig { } diff --git a/dao/src/main/java/org/thingsboard/server/dao/NoSqlDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/NoSqlDaoConfig.java deleted file mode 100644 index b89e6f1bde..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/NoSqlDaoConfig.java +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao; - -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; -import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; -import org.springframework.context.annotation.Configuration; -import org.thingsboard.server.dao.util.NoSqlDao; - -@Configuration -@EnableAutoConfiguration( - exclude = { - DataSourceAutoConfiguration.class, - DataSourceTransactionManagerAutoConfiguration.class, - HibernateJpaAutoConfiguration.class}) -@NoSqlDao -public class NoSqlDaoConfig { -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/PsqlTsDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/PsqlTsDaoConfig.java index 65f17709ca..363b53713f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/PsqlTsDaoConfig.java +++ b/dao/src/main/java/org/thingsboard/server/dao/PsqlTsDaoConfig.java @@ -27,11 +27,11 @@ import org.thingsboard.server.dao.util.SqlTsDao; @Configuration @EnableAutoConfiguration @ComponentScan({"org.thingsboard.server.dao.sqlts.psql"}) -@EnableJpaRepositories({"org.thingsboard.server.dao.sqlts.ts", "org.thingsboard.server.dao.sqlts.insert.psql", "org.thingsboard.server.dao.sqlts.insert.latest.psql", "org.thingsboard.server.dao.sqlts.latest", "org.thingsboard.server.dao.sqlts.dictionary"}) -@EntityScan({"org.thingsboard.server.dao.model.sqlts.ts", "org.thingsboard.server.dao.model.sqlts.latest", "org.thingsboard.server.dao.model.sqlts.dictionary"}) +@EnableJpaRepositories({"org.thingsboard.server.dao.sqlts.ts", "org.thingsboard.server.dao.sqlts.insert.psql"}) +@EntityScan({"org.thingsboard.server.dao.model.sqlts.ts"}) @EnableTransactionManagement -@SqlTsDao @PsqlDao +@SqlTsDao public class PsqlTsDaoConfig { } diff --git a/dao/src/main/java/org/thingsboard/server/dao/PsqlTsLatestDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/PsqlTsLatestDaoConfig.java new file mode 100644 index 0000000000..338bd71e73 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/PsqlTsLatestDaoConfig.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.thingsboard.server.dao.util.PsqlDao; +import org.thingsboard.server.dao.util.SqlTsLatestDao; + +@Configuration +@EnableAutoConfiguration +@ComponentScan({"org.thingsboard.server.dao.sqlts.psql"}) +@EnableJpaRepositories({"org.thingsboard.server.dao.sqlts.insert.latest.psql", "org.thingsboard.server.dao.sqlts.latest"}) +@EntityScan({"org.thingsboard.server.dao.model.sqlts.latest"}) +@EnableTransactionManagement +@SqlTsLatestDao +@PsqlDao +public class PsqlTsLatestDaoConfig { + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/SqlTimeseriesDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/SqlTimeseriesDaoConfig.java new file mode 100644 index 0000000000..6b38df8284 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/SqlTimeseriesDaoConfig.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.thingsboard.server.dao.util.PsqlDao; +import org.thingsboard.server.dao.util.SqlTsOrTsLatestAnyDao; + +@Configuration +@EnableAutoConfiguration +@EnableJpaRepositories({"org.thingsboard.server.dao.sqlts.dictionary"}) +@EntityScan({"org.thingsboard.server.dao.model.sqlts.dictionary"}) +@EnableTransactionManagement +@SqlTsOrTsLatestAnyDao +public class SqlTimeseriesDaoConfig { + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/TimescaleDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/TimescaleDaoConfig.java index 19ae98c736..673ff314c5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/TimescaleDaoConfig.java +++ b/dao/src/main/java/org/thingsboard/server/dao/TimescaleDaoConfig.java @@ -27,8 +27,8 @@ import org.thingsboard.server.dao.util.TimescaleDBTsDao; @Configuration @EnableAutoConfiguration @ComponentScan({"org.thingsboard.server.dao.sqlts.timescale"}) -@EnableJpaRepositories({"org.thingsboard.server.dao.sqlts.timescale", "org.thingsboard.server.dao.sqlts.insert.latest.psql", "org.thingsboard.server.dao.sqlts.insert.timescale", "org.thingsboard.server.dao.sqlts.dictionary", "org.thingsboard.server.dao.sqlts.latest"}) -@EntityScan({"org.thingsboard.server.dao.model.sqlts.timescale", "org.thingsboard.server.dao.model.sqlts.dictionary", "org.thingsboard.server.dao.model.sqlts.latest"}) +@EnableJpaRepositories({"org.thingsboard.server.dao.sqlts.timescale", "org.thingsboard.server.dao.sqlts.insert.timescale"}) +@EntityScan({"org.thingsboard.server.dao.model.sqlts.timescale"}) @EnableTransactionManagement @TimescaleDBTsDao @PsqlDao diff --git a/dao/src/main/java/org/thingsboard/server/dao/TimescaleTsLatestDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/TimescaleTsLatestDaoConfig.java new file mode 100644 index 0000000000..9765dd6d11 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/TimescaleTsLatestDaoConfig.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.thingsboard.server.dao.util.PsqlDao; +import org.thingsboard.server.dao.util.TimescaleDBTsLatestDao; + +@Configuration +@EnableAutoConfiguration +@ComponentScan({"org.thingsboard.server.dao.sqlts.timescale"}) +@EnableJpaRepositories({"org.thingsboard.server.dao.sqlts.insert.latest.psql", "org.thingsboard.server.dao.sqlts.latest"}) +@EntityScan({"org.thingsboard.server.dao.model.sqlts.latest"}) +@EnableTransactionManagement +@TimescaleDBTsLatestDao +@PsqlDao +public class TimescaleTsLatestDaoConfig { + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java index 8a9db5e28a..ff308c49f5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java @@ -19,11 +19,16 @@ import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AlarmData; +import org.thingsboard.server.common.data.query.AlarmDataPageLink; +import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.dao.Dao; -import java.util.List; +import java.util.Collection; import java.util.UUID; /** @@ -39,5 +44,8 @@ public interface AlarmDao extends Dao { Alarm save(TenantId tenantId, Alarm alarm); - ListenableFuture> findAlarms(TenantId tenantId, AlarmQuery query); + PageData findAlarms(TenantId tenantId, AlarmQuery query); + + PageData findAlarmDataByQueryForEntities(TenantId tenantId, CustomerId customerId, + AlarmDataQuery query, Collection orderedEntityIds); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java index 41f8a2b630..f2e768e6b2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java @@ -29,16 +29,20 @@ import org.springframework.util.StringUtils; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.query.AlarmData; +import org.thingsboard.server.common.data.query.AlarmDataPageLink; +import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.EntitySearchDirection; @@ -54,7 +58,10 @@ import javax.annotation.Nullable; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.Comparator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutionException; @@ -69,7 +76,8 @@ import static org.thingsboard.server.dao.service.Validator.validateId; @Slf4j public class BaseAlarmService extends AbstractEntityService implements AlarmService { - public static final String ALARM_RELATION_PREFIX = "ALARM_"; + public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; + public static final String INCORRECT_CUSTOMER_ID = "Incorrect customerId "; @Autowired private AlarmDao alarmDao; @@ -95,7 +103,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ } @Override - public Alarm createOrUpdateAlarm(Alarm alarm) { + public AlarmOperationResult createOrUpdateAlarm(Alarm alarm) { alarmDataValidator.validate(alarm, Alarm::getTenantId); try { if (alarm.getStartTs() == 0L) { @@ -124,39 +132,56 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ } @Override - public Boolean deleteAlarm(TenantId tenantId, AlarmId alarmId) { + public PageData findAlarmDataByQueryForEntities(TenantId tenantId, CustomerId customerId, + AlarmDataQuery query, Collection orderedEntityIds) { + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + return alarmDao.findAlarmDataByQueryForEntities(tenantId, customerId, query, orderedEntityIds); + } + + @Override + public AlarmOperationResult deleteAlarm(TenantId tenantId, AlarmId alarmId) { try { log.debug("Deleting Alarm Id: {}", alarmId); Alarm alarm = alarmDao.findAlarmByIdAsync(tenantId, alarmId.getId()).get(); if (alarm == null) { - return false; + return new AlarmOperationResult(alarm, false); } + AlarmOperationResult result = new AlarmOperationResult(alarm, true, new ArrayList<>(getPropagationEntityIds(alarm))); deleteEntityRelations(tenantId, alarm.getId()); - return alarmDao.deleteAlarm(tenantId, alarm); + alarmDao.deleteAlarm(tenantId, alarm); + return result; } catch (ExecutionException | InterruptedException e) { throw new RuntimeException(e); } } - private Alarm createAlarm(Alarm alarm) throws InterruptedException, ExecutionException { + private AlarmOperationResult createAlarm(Alarm alarm) throws InterruptedException, ExecutionException { log.debug("New Alarm : {}", alarm); Alarm saved = alarmDao.save(alarm.getTenantId(), alarm); - createAlarmRelations(saved); - return saved; + List propagatedEntitiesList = createAlarmRelations(saved); + return new AlarmOperationResult(saved, true, propagatedEntitiesList); } - private void createAlarmRelations(Alarm alarm) throws InterruptedException, ExecutionException { + private List createAlarmRelations(Alarm alarm) throws InterruptedException, ExecutionException { + List propagatedEntitiesList; if (alarm.isPropagate()) { - List parentEntities = getParentEntities(alarm); + Set parentEntities = getParentEntities(alarm); + propagatedEntitiesList = new ArrayList<>(parentEntities.size() + 1); for (EntityId parentId : parentEntities) { - createAlarmRelation(alarm.getTenantId(), parentId, alarm.getId(), alarm.getStatus(), true); + propagatedEntitiesList.add(parentId); + createAlarmRelation(alarm.getTenantId(), parentId, alarm.getId()); } + propagatedEntitiesList.add(alarm.getOriginator()); + } else { + propagatedEntitiesList = Collections.singletonList(alarm.getOriginator()); } - createAlarmRelation(alarm.getTenantId(), alarm.getOriginator(), alarm.getId(), alarm.getStatus(), true); + return propagatedEntitiesList; } - private List getParentEntities(Alarm alarm) throws InterruptedException, ExecutionException { + private Set getParentEntities(Alarm alarm) throws InterruptedException, ExecutionException { EntityRelationsQuery query = new EntityRelationsQuery(); + //TODO 3.1: @dlandiak we need to fetch max 3 levels and then fetch more if needed and there is at least one non-duplicate. RelationsSearchParameters parameters = new RelationsSearchParameters(alarm.getOriginator(), EntitySearchDirection.TO, Integer.MAX_VALUE, false); query.setParameters(parameters); List propagateRelationTypes = alarm.getPropagateRelationTypes(); @@ -164,15 +189,15 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ if (!CollectionUtils.isEmpty(propagateRelationTypes)) { relations = relations.filter(entityRelation -> propagateRelationTypes.contains(entityRelation.getType())); } - return relations.map(EntityRelation::getFrom).collect(Collectors.toList()); + return relations.map(EntityRelation::getFrom).collect(Collectors.toCollection(LinkedHashSet::new)); } - private ListenableFuture updateAlarm(Alarm update) { + private ListenableFuture updateAlarm(Alarm update) { alarmDataValidator.validate(update, Alarm::getTenantId); - return getAndUpdate(update.getTenantId(), update.getId(), new Function() { + return getAndUpdate(update.getTenantId(), update.getId(), new Function() { @Nullable @Override - public Alarm apply(@Nullable Alarm alarm) { + public AlarmOperationResult apply(@Nullable Alarm alarm) { if (alarm == null) { return null; } else { @@ -182,54 +207,52 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ }); } - private Alarm updateAlarm(Alarm oldAlarm, Alarm newAlarm) { - AlarmStatus oldStatus = oldAlarm.getStatus(); - AlarmStatus newStatus = newAlarm.getStatus(); + private AlarmOperationResult updateAlarm(Alarm oldAlarm, Alarm newAlarm) { boolean oldPropagate = oldAlarm.isPropagate(); boolean newPropagate = newAlarm.isPropagate(); Alarm result = alarmDao.save(newAlarm.getTenantId(), merge(oldAlarm, newAlarm)); + List propagatedEntitiesList; if (!oldPropagate && newPropagate) { try { - createAlarmRelations(result); + propagatedEntitiesList = createAlarmRelations(result); } catch (InterruptedException | ExecutionException e) { log.warn("Failed to update alarm relations [{}]", result, e); throw new RuntimeException(e); } - } else if (oldStatus != newStatus) { - updateRelations(oldAlarm, oldStatus, newStatus); + } else { + propagatedEntitiesList = new ArrayList<>(getPropagationEntityIds(result)); } - return result; + return new AlarmOperationResult(result, true, propagatedEntitiesList); } @Override - public ListenableFuture ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTime) { - return getAndUpdate(tenantId, alarmId, new Function() { + public ListenableFuture ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTime) { + return getAndUpdate(tenantId, alarmId, new Function() { @Nullable @Override - public Boolean apply(@Nullable Alarm alarm) { + public AlarmOperationResult apply(@Nullable Alarm alarm) { if (alarm == null || alarm.getStatus().isAck()) { - return false; + return new AlarmOperationResult(alarm, false); } else { AlarmStatus oldStatus = alarm.getStatus(); AlarmStatus newStatus = oldStatus.isCleared() ? AlarmStatus.CLEARED_ACK : AlarmStatus.ACTIVE_ACK; alarm.setStatus(newStatus); alarm.setAckTs(ackTime); - alarmDao.save(alarm.getTenantId(), alarm); - updateRelations(alarm, oldStatus, newStatus); - return true; + alarm = alarmDao.save(alarm.getTenantId(), alarm); + return new AlarmOperationResult(alarm, true, new ArrayList<>(getPropagationEntityIds(alarm))); } } }); } @Override - public ListenableFuture clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTime) { - return getAndUpdate(tenantId, alarmId, new Function() { + public ListenableFuture clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTime) { + return getAndUpdate(tenantId, alarmId, new Function() { @Nullable @Override - public Boolean apply(@Nullable Alarm alarm) { + public AlarmOperationResult apply(@Nullable Alarm alarm) { if (alarm == null || alarm.getStatus().isCleared()) { - return false; + return new AlarmOperationResult(alarm, false); } else { AlarmStatus oldStatus = alarm.getStatus(); AlarmStatus newStatus = oldStatus.isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK; @@ -238,9 +261,8 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ if (details != null) { alarm.setDetails(details); } - alarmDao.save(alarm.getTenantId(), alarm); - updateRelations(alarm, oldStatus, newStatus); - return true; + alarm = alarmDao.save(alarm.getTenantId(), alarm); + return new AlarmOperationResult(alarm, true, new ArrayList<>(getPropagationEntityIds(alarm))); } } }); @@ -269,32 +291,26 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ } @Override - public ListenableFuture> findAlarms(TenantId tenantId, AlarmQuery query) { - ListenableFuture> alarms = alarmDao.findAlarms(tenantId, query); + public ListenableFuture> findAlarms(TenantId tenantId, AlarmQuery query) { + PageData alarms = alarmDao.findAlarms(tenantId, query); if (query.getFetchOriginator() != null && query.getFetchOriginator().booleanValue()) { - alarms = Futures.transformAsync(alarms, input -> { - List> alarmFutures = new ArrayList<>(input.size()); - for (AlarmInfo alarmInfo : input) { - alarmFutures.add(Futures.transform( - entityService.fetchEntityNameAsync(tenantId, alarmInfo.getOriginator()), originatorName -> { - if (originatorName == null) { - originatorName = "Deleted"; - } - alarmInfo.setOriginatorName(originatorName); - return alarmInfo; - }, MoreExecutors.directExecutor() - )); - } - return Futures.successfulAsList(alarmFutures); - }, MoreExecutors.directExecutor()); - } - return Futures.transform(alarms, new Function, TimePageData>() { - @Nullable - @Override - public TimePageData apply(@Nullable List alarms) { - return new TimePageData<>(alarms, query.getPageLink()); + List> alarmFutures = new ArrayList<>(alarms.getData().size()); + for (AlarmInfo alarmInfo : alarms.getData()) { + alarmFutures.add(Futures.transform( + entityService.fetchEntityNameAsync(tenantId, alarmInfo.getOriginator()), originatorName -> { + if (originatorName == null) { + originatorName = "Deleted"; + } + alarmInfo.setOriginatorName(originatorName); + return alarmInfo; + }, MoreExecutors.directExecutor() + )); } - }, MoreExecutors.directExecutor()); + return Futures.transform(Futures.successfulAsList(alarmFutures), + alarmInfos -> new PageData(alarmInfos, alarms.getTotalPages(), alarms.getTotalElements(), + alarms.hasNext()), MoreExecutors.directExecutor()); + } + return Futures.immediateFuture(alarms); } @Override @@ -305,20 +321,12 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ AlarmSeverity highestSeverity = null; AlarmQuery query; while (hasNext && AlarmSeverity.CRITICAL != highestSeverity) { - query = new AlarmQuery(entityId, nextPageLink, alarmSearchStatus, alarmStatus, false); - List alarms; - try { - alarms = alarmDao.findAlarms(tenantId, query).get(); - } catch (ExecutionException | InterruptedException e) { - log.warn("Failed to find highest alarm severity. EntityId: [{}], AlarmSearchStatus: [{}], AlarmStatus: [{}]", - entityId, alarmSearchStatus, alarmStatus); - throw new RuntimeException(e); + query = new AlarmQuery(entityId, nextPageLink, alarmSearchStatus, alarmStatus, false, null); + PageData alarms = alarmDao.findAlarms(tenantId, query); + if (alarms.hasNext()) { + nextPageLink = nextPageLink.nextPageLink(); } - hasNext = alarms.size() == nextPageLink.getLimit(); - if (hasNext) { - nextPageLink = new TimePageData<>(alarms, nextPageLink).getNextPageLink(); - } - AlarmSeverity severity = detectHighestSeverity(alarms); + AlarmSeverity severity = detectHighestSeverity(alarms.getData()); if (severity == null) { continue; } @@ -382,37 +390,17 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ return existing; } - private void updateRelations(Alarm alarm, AlarmStatus oldStatus, AlarmStatus newStatus) { - try { - List relations = relationService.findByToAsync(alarm.getTenantId(), alarm.getId(), RelationTypeGroup.ALARM).get(); - Set parents = relations.stream().map(EntityRelation::getFrom).collect(Collectors.toSet()); - for (EntityId parentId : parents) { - updateAlarmRelation(alarm.getTenantId(), parentId, alarm.getId(), oldStatus, newStatus); - } - } catch (ExecutionException | InterruptedException e) { - log.warn("[{}] Failed to update relations. Old status: [{}], New status: [{}]", alarm.getId(), oldStatus, newStatus); - throw new RuntimeException(e); - } - } - - private void createAlarmRelation(TenantId tenantId, EntityId entityId, EntityId alarmId, AlarmStatus status, boolean createAnyRelation) { - if (createAnyRelation) { - createRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + AlarmSearchStatus.ANY.name(), RelationTypeGroup.ALARM)); + private Set getPropagationEntityIds(Alarm alarm) { + if (alarm.isPropagate()) { + List relations = relationService.findByTo(alarm.getTenantId(), alarm.getId(), RelationTypeGroup.ALARM); + return relations.stream().map(EntityRelation::getFrom).collect(Collectors.toSet()); + } else { + return Collections.singleton(alarm.getOriginator()); } - createRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.name(), RelationTypeGroup.ALARM)); - createRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getClearSearchStatus().name(), RelationTypeGroup.ALARM)); - createRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getAckSearchStatus().name(), RelationTypeGroup.ALARM)); - } - - private void deleteAlarmRelation(TenantId tenantId, EntityId entityId, EntityId alarmId, AlarmStatus status) { - deleteRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.name(), RelationTypeGroup.ALARM)); - deleteRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getClearSearchStatus().name(), RelationTypeGroup.ALARM)); - deleteRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getAckSearchStatus().name(), RelationTypeGroup.ALARM)); } - private void updateAlarmRelation(TenantId tenantId, EntityId entityId, EntityId alarmId, AlarmStatus oldStatus, AlarmStatus newStatus) { - deleteAlarmRelation(tenantId, entityId, alarmId, oldStatus); - createAlarmRelation(tenantId, entityId, alarmId, newStatus, false); + private void createAlarmRelation(TenantId tenantId, EntityId entityId, EntityId alarmId) { + createRelation(tenantId, new EntityRelation(entityId, alarmId, AlarmSearchStatus.ANY.name(), RelationTypeGroup.ALARM)); } private ListenableFuture getAndUpdate(TenantId tenantId, AlarmId alarmId, Function function) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/CassandraAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/CassandraAlarmDao.java deleted file mode 100644 index e124b3e172..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/CassandraAlarmDao.java +++ /dev/null @@ -1,140 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.alarm; - -import com.datastax.driver.core.Statement; -import com.datastax.driver.core.querybuilder.QueryBuilder; -import com.datastax.driver.core.querybuilder.Select; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.alarm.AlarmInfo; -import org.thingsboard.server.common.data.alarm.AlarmQuery; -import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.relation.EntityRelation; -import org.thingsboard.server.common.data.relation.RelationTypeGroup; -import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.nosql.AlarmEntity; -import org.thingsboard.server.dao.nosql.CassandraAbstractModelDao; -import org.thingsboard.server.dao.relation.RelationDao; -import org.thingsboard.server.dao.util.NoSqlDao; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; -import static com.datastax.driver.core.querybuilder.QueryBuilder.select; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_BY_ID_VIEW_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ORIGINATOR_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ORIGINATOR_TYPE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_TENANT_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_TYPE_PROPERTY; - -@Component -@Slf4j -@NoSqlDao -public class CassandraAlarmDao extends CassandraAbstractModelDao implements AlarmDao { - - @Autowired - private RelationDao relationDao; - - @Override - protected Class getColumnFamilyClass() { - return AlarmEntity.class; - } - - @Override - protected String getColumnFamilyName() { - return ALARM_COLUMN_FAMILY_NAME; - } - - protected boolean isDeleteOnSave() { - return false; - } - - @Override - public Alarm save(TenantId tenantId, Alarm alarm) { - log.debug("Save asset [{}] ", alarm); - return super.save(tenantId, alarm); - } - - @Override - public Boolean deleteAlarm(TenantId tenantId, Alarm alarm) { - Statement delete = QueryBuilder.delete().all().from(getColumnFamilyName()).where(eq(ModelConstants.ID_PROPERTY, alarm.getId().getId())) - .and(eq(ALARM_TENANT_ID_PROPERTY, tenantId.getId())) - .and(eq(ALARM_ORIGINATOR_ID_PROPERTY, alarm.getOriginator().getId())) - .and(eq(ALARM_ORIGINATOR_TYPE_PROPERTY, alarm.getOriginator().getEntityType())) - .and(eq(ALARM_TYPE_PROPERTY, alarm.getType())); - log.debug("Remove request: {}", delete.toString()); - return executeWrite(tenantId, delete).wasApplied(); - } - - @Override - public ListenableFuture findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type) { - Select select = select().from(ALARM_COLUMN_FAMILY_NAME); - Select.Where query = select.where(); - query.and(eq(ALARM_TENANT_ID_PROPERTY, tenantId.getId())); - query.and(eq(ALARM_ORIGINATOR_ID_PROPERTY, originator.getId())); - query.and(eq(ALARM_ORIGINATOR_TYPE_PROPERTY, originator.getEntityType())); - query.and(eq(ALARM_TYPE_PROPERTY, type)); - query.limit(1); - query.orderBy(QueryBuilder.asc(ModelConstants.ALARM_TYPE_PROPERTY), QueryBuilder.desc(ModelConstants.ID_PROPERTY)); - return findOneByStatementAsync(tenantId, query); - } - - @Override - public ListenableFuture> findAlarms(TenantId tenantId, AlarmQuery query) { - log.trace("Try to find alarms by entity [{}], searchStatus [{}], status [{}] and pageLink [{}]", query.getAffectedEntityId(), query.getSearchStatus(), query.getStatus(), query.getPageLink()); - EntityId affectedEntity = query.getAffectedEntityId(); - String searchStatusName; - if (query.getSearchStatus() == null && query.getStatus() == null) { - searchStatusName = AlarmSearchStatus.ANY.name(); - } else if (query.getSearchStatus() != null) { - searchStatusName = query.getSearchStatus().name(); - } else { - searchStatusName = query.getStatus().name(); - } - String relationType = BaseAlarmService.ALARM_RELATION_PREFIX + searchStatusName; - ListenableFuture> relations = relationDao.findRelations(tenantId, affectedEntity, relationType, RelationTypeGroup.ALARM, EntityType.ALARM, query.getPageLink()); - return Futures.transformAsync(relations, input -> { - List> alarmFutures = new ArrayList<>(input.size()); - for (EntityRelation relation : input) { - alarmFutures.add(Futures.transform( - findAlarmByIdAsync(tenantId, relation.getTo().getId()), - AlarmInfo::new, MoreExecutors.directExecutor())); - } - return Futures.successfulAsList(alarmFutures); - }, MoreExecutors.directExecutor()); - } - - @Override - public ListenableFuture findAlarmByIdAsync(TenantId tenantId, UUID key) { - log.debug("Get alarm by id {}", key); - Select.Where query = select().from(ALARM_BY_ID_VIEW_NAME).where(eq(ModelConstants.ID_PROPERTY, key)); - query.limit(1); - log.trace("Execute query {}", query); - return findOneByStatementAsync(tenantId, query); - } -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java index 08071a8eb2..1366df3ac2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java @@ -18,8 +18,10 @@ package org.thingsboard.server.dao.asset; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; import java.util.List; @@ -32,6 +34,15 @@ import java.util.UUID; */ public interface AssetDao extends Dao { + /** + * Find asset info by id. + * + * @param tenantId the tenant id + * @param assetId the asset id + * @return the asset info object + */ + AssetInfo findAssetInfoById(TenantId tenantId, UUID assetId); + /** * Save or update asset object * @@ -47,7 +58,16 @@ public interface AssetDao extends Dao { * @param pageLink the page link * @return the list of asset objects */ - List findAssetsByTenantId(UUID tenantId, TextPageLink pageLink); + PageData findAssetsByTenantId(UUID tenantId, PageLink pageLink); + + /** + * Find asset infos by tenantId and page link. + * + * @param tenantId the tenantId + * @param pageLink the page link + * @return the list of asset info objects + */ + PageData findAssetInfosByTenantId(UUID tenantId, PageLink pageLink); /** * Find assets by tenantId, type and page link. @@ -57,7 +77,17 @@ public interface AssetDao extends Dao { * @param pageLink the page link * @return the list of asset objects */ - List findAssetsByTenantIdAndType(UUID tenantId, String type, TextPageLink pageLink); + PageData findAssetsByTenantIdAndType(UUID tenantId, String type, PageLink pageLink); + + /** + * Find asset infos by tenantId, type and page link. + * + * @param tenantId the tenantId + * @param type the type + * @param pageLink the page link + * @return the list of asset info objects + */ + PageData findAssetInfosByTenantIdAndType(UUID tenantId, String type, PageLink pageLink); /** * Find assets by tenantId and assets Ids. @@ -76,7 +106,17 @@ public interface AssetDao extends Dao { * @param pageLink the page link * @return the list of asset objects */ - List findAssetsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink); + PageData findAssetsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink); + + /** + * Find asset infos by tenantId, customerId and page link. + * + * @param tenantId the tenantId + * @param customerId the customerId + * @param pageLink the page link + * @return the list of asset info objects + */ + PageData findAssetInfosByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink); /** * Find assets by tenantId, customerId, type and page link. @@ -87,7 +127,18 @@ public interface AssetDao extends Dao { * @param pageLink the page link * @return the list of asset objects */ - List findAssetsByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, TextPageLink pageLink); + PageData findAssetsByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink); + + /** + * Find asset infos by tenantId, customerId, type and page link. + * + * @param tenantId the tenantId + * @param customerId the customerId + * @param type the type + * @param pageLink the page link + * @return the list of asset info objects + */ + PageData findAssetInfosByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink); /** * Find assets by tenantId, customerId and assets Ids. @@ -115,4 +166,6 @@ public interface AssetDao extends Dao { */ ListenableFuture> findTenantAssetTypesAsync(UUID tenantId); + Long countAssetsByTenantId(TenantId tenantId); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java index 6a71045851..923baa1df4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java @@ -26,6 +26,7 @@ import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Customer; @@ -34,21 +35,24 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetSearchQuery; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.customer.CustomerDao; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantDao; import java.util.ArrayList; @@ -89,6 +93,17 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ @Autowired private CacheManager cacheManager; + @Autowired + @Lazy + private TbTenantProfileCache tenantProfileCache; + + @Override + public AssetInfo findAssetInfoById(TenantId tenantId, AssetId assetId) { + log.trace("Executing findAssetInfoById [{}]", assetId); + validateId(assetId, INCORRECT_ASSET_ID + assetId); + return assetDao.findAssetInfoById(tenantId, assetId.getId()); + } + @Override public Asset findAssetById(TenantId tenantId, AssetId assetId) { log.trace("Executing findAssetById [{}]", assetId); @@ -118,18 +133,14 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ log.trace("Executing saveAsset [{}]", asset); assetValidator.validate(asset, Asset::getTenantId); Asset savedAsset; - if (!sqlDatabaseUsed) { + try { savedAsset = assetDao.save(asset.getTenantId(), asset); - } else { - try { - savedAsset = assetDao.save(asset.getTenantId(), asset); - } catch (Exception t) { - ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); - if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("asset_name_unq_key")) { - throw new DataValidationException("Asset with such name already exists!"); - } else { - throw t; - } + } catch (Exception t) { + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("asset_name_unq_key")) { + throw new DataValidationException("Asset with such name already exists!"); + } else { + throw t; } } return savedAsset; @@ -176,22 +187,37 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ } @Override - public TextPageData findAssetsByTenantId(TenantId tenantId, TextPageLink pageLink) { + public PageData findAssetsByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findAssetsByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validatePageLink(pageLink, INCORRECT_PAGE_LINK + pageLink); - List assets = assetDao.findAssetsByTenantId(tenantId.getId(), pageLink); - return new TextPageData<>(assets, pageLink); + validatePageLink(pageLink); + return assetDao.findAssetsByTenantId(tenantId.getId(), pageLink); } @Override - public TextPageData findAssetsByTenantIdAndType(TenantId tenantId, String type, TextPageLink pageLink) { + public PageData findAssetInfosByTenantId(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findAssetInfosByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validatePageLink(pageLink); + return assetDao.findAssetInfosByTenantId(tenantId.getId(), pageLink); + } + + @Override + public PageData findAssetsByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink) { log.trace("Executing findAssetsByTenantIdAndType, tenantId [{}], type [{}], pageLink [{}]", tenantId, type, pageLink); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); validateString(type, "Incorrect type " + type); - validatePageLink(pageLink, INCORRECT_PAGE_LINK + pageLink); - List assets = assetDao.findAssetsByTenantIdAndType(tenantId.getId(), type, pageLink); - return new TextPageData<>(assets, pageLink); + validatePageLink(pageLink); + return assetDao.findAssetsByTenantIdAndType(tenantId.getId(), type, pageLink); + } + + @Override + public PageData findAssetInfosByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink) { + log.trace("Executing findAssetInfosByTenantIdAndType, tenantId [{}], type [{}], pageLink [{}]", tenantId, type, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateString(type, "Incorrect type " + type); + validatePageLink(pageLink); + return assetDao.findAssetInfosByTenantIdAndType(tenantId.getId(), type, pageLink); } @Override @@ -210,24 +236,41 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ } @Override - public TextPageData findAssetsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TextPageLink pageLink) { + public PageData findAssetsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) { log.trace("Executing findAssetsByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); - validatePageLink(pageLink, INCORRECT_PAGE_LINK + pageLink); - List assets = assetDao.findAssetsByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink); - return new TextPageData<>(assets, pageLink); + validatePageLink(pageLink); + return assetDao.findAssetsByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink); + } + + @Override + public PageData findAssetInfosByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) { + log.trace("Executing findAssetInfosByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validatePageLink(pageLink); + return assetDao.findAssetInfosByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink); } @Override - public TextPageData findAssetsByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, TextPageLink pageLink) { + public PageData findAssetsByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink) { log.trace("Executing findAssetsByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}], type [{}], pageLink [{}]", tenantId, customerId, type, pageLink); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); validateString(type, "Incorrect type " + type); - validatePageLink(pageLink, INCORRECT_PAGE_LINK + pageLink); - List assets = assetDao.findAssetsByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink); - return new TextPageData<>(assets, pageLink); + validatePageLink(pageLink); + return assetDao.findAssetsByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink); + } + + @Override + public PageData findAssetInfosByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink) { + log.trace("Executing findAssetInfosByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}], type [{}], pageLink [{}]", tenantId, customerId, type, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validateString(type, "Incorrect type " + type); + validatePageLink(pageLink); + return assetDao.findAssetInfosByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink); } @Override @@ -284,26 +327,19 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ @Override protected void validateCreate(TenantId tenantId, Asset asset) { - if (!sqlDatabaseUsed) { - assetDao.findAssetsByTenantIdAndName(asset.getTenantId().getId(), asset.getName()).ifPresent( - d -> { - throw new DataValidationException("Asset with such name already exists!"); - } - ); + DefaultTenantProfileConfiguration profileConfiguration = + (DefaultTenantProfileConfiguration)tenantProfileCache.get(tenantId).getProfileData().getConfiguration(); + long maxAssets = profileConfiguration.getMaxAssets(); + if (maxAssets > 0) { + long currentAssetsCount = assetDao.countAssetsByTenantId(tenantId); + if (maxAssets >= currentAssetsCount) { + throw new DataValidationException("Can't create assets more then " + maxAssets); + } } } @Override protected void validateUpdate(TenantId tenantId, Asset asset) { - if (!sqlDatabaseUsed) { - assetDao.findAssetsByTenantIdAndName(asset.getTenantId().getId(), asset.getName()).ifPresent( - d -> { - if (!d.getId().equals(asset.getId())) { - throw new DataValidationException("Asset with such name already exists!"); - } - } - ); - } } @Override @@ -340,7 +376,7 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ new PaginatedRemover() { @Override - protected List findEntities(TenantId tenantId, TenantId id, TextPageLink pageLink) { + protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { return assetDao.findAssetsByTenantId(id.getId(), pageLink); } @@ -353,7 +389,7 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ private PaginatedRemover customerAssetsUnasigner = new PaginatedRemover() { @Override - protected List findEntities(TenantId tenantId, CustomerId id, TextPageLink pageLink) { + protected PageData findEntities(TenantId tenantId, CustomerId id, PageLink pageLink) { return assetDao.findAssetsByTenantIdAndCustomerId(tenantId.getId(), id.getId(), pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/CassandraAssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/asset/CassandraAssetDao.java deleted file mode 100644 index 9808e7b117..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/CassandraAssetDao.java +++ /dev/null @@ -1,192 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.asset; - -import com.datastax.driver.core.ResultSet; -import com.datastax.driver.core.ResultSetFuture; -import com.datastax.driver.core.Statement; -import com.datastax.driver.core.querybuilder.Select; -import com.datastax.driver.mapping.Result; -import com.google.common.base.Function; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.EntitySubtype; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.asset.Asset; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; -import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.model.EntitySubtypeEntity; -import org.thingsboard.server.dao.model.nosql.AssetEntity; -import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTextDao; -import org.thingsboard.server.dao.util.NoSqlDao; - -import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; -import static com.datastax.driver.core.querybuilder.QueryBuilder.in; -import static com.datastax.driver.core.querybuilder.QueryBuilder.select; -import static org.thingsboard.server.dao.model.ModelConstants.ASSET_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.ASSET_BY_CUSTOMER_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.ASSET_BY_TENANT_AND_NAME_VIEW_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.ASSET_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.ASSET_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.ASSET_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.ASSET_CUSTOMER_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ASSET_NAME_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TENANT_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TYPE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_ENTITY_TYPE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_TENANT_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY; - -@Component -@Slf4j -@NoSqlDao -public class CassandraAssetDao extends CassandraAbstractSearchTextDao implements AssetDao { - - @Override - protected Class getColumnFamilyClass() { - return AssetEntity.class; - } - - @Override - protected String getColumnFamilyName() { - return ASSET_COLUMN_FAMILY_NAME; - } - - @Override - public Asset save(TenantId tenantId, Asset domain) { - Asset savedAsset = super.save(tenantId, domain); - EntitySubtype entitySubtype = new EntitySubtype(savedAsset.getTenantId(), EntityType.ASSET, savedAsset.getType()); - EntitySubtypeEntity entitySubtypeEntity = new EntitySubtypeEntity(entitySubtype); - Statement saveStatement = cluster.getMapper(EntitySubtypeEntity.class).saveQuery(entitySubtypeEntity); - executeWrite(tenantId, saveStatement); - return savedAsset; - } - - @Override - public List findAssetsByTenantId(UUID tenantId, TextPageLink pageLink) { - log.debug("Try to find assets by tenantId [{}] and pageLink [{}]", tenantId, pageLink); - List assetEntities = findPageWithTextSearch(new TenantId(tenantId), ASSET_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME, - Collections.singletonList(eq(ASSET_TENANT_ID_PROPERTY, tenantId)), pageLink); - - log.trace("Found assets [{}] by tenantId [{}] and pageLink [{}]", assetEntities, tenantId, pageLink); - return DaoUtil.convertDataList(assetEntities); - } - - @Override - public List findAssetsByTenantIdAndType(UUID tenantId, String type, TextPageLink pageLink) { - log.debug("Try to find assets by tenantId [{}], type [{}] and pageLink [{}]", tenantId, type, pageLink); - List assetEntities = findPageWithTextSearch(new TenantId(tenantId), ASSET_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME, - Arrays.asList(eq(ASSET_TYPE_PROPERTY, type), - eq(ASSET_TENANT_ID_PROPERTY, tenantId)), pageLink); - log.trace("Found assets [{}] by tenantId [{}], type [{}] and pageLink [{}]", assetEntities, tenantId, type, pageLink); - return DaoUtil.convertDataList(assetEntities); - } - - public ListenableFuture> findAssetsByTenantIdAndIdsAsync(UUID tenantId, List assetIds) { - log.debug("Try to find assets by tenantId [{}] and asset Ids [{}]", tenantId, assetIds); - Select select = select().from(getColumnFamilyName()); - Select.Where query = select.where(); - query.and(eq(ASSET_TENANT_ID_PROPERTY, tenantId)); - query.and(in(ID_PROPERTY, assetIds)); - return findListByStatementAsync(new TenantId(tenantId), query); - } - - @Override - public List findAssetsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink) { - log.debug("Try to find assets by tenantId [{}], customerId[{}] and pageLink [{}]", tenantId, customerId, pageLink); - List assetEntities = findPageWithTextSearch(new TenantId(tenantId), ASSET_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME, - Arrays.asList(eq(ASSET_CUSTOMER_ID_PROPERTY, customerId), - eq(ASSET_TENANT_ID_PROPERTY, tenantId)), - pageLink); - - log.trace("Found assets [{}] by tenantId [{}], customerId [{}] and pageLink [{}]", assetEntities, tenantId, customerId, pageLink); - return DaoUtil.convertDataList(assetEntities); - } - - @Override - public List findAssetsByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, TextPageLink pageLink) { - log.debug("Try to find assets by tenantId [{}], customerId [{}], type [{}] and pageLink [{}]", tenantId, customerId, type, pageLink); - List assetEntities = findPageWithTextSearch(new TenantId(tenantId), ASSET_BY_CUSTOMER_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME, - Arrays.asList(eq(ASSET_TYPE_PROPERTY, type), - eq(ASSET_CUSTOMER_ID_PROPERTY, customerId), - eq(ASSET_TENANT_ID_PROPERTY, tenantId)), - pageLink); - - log.trace("Found assets [{}] by tenantId [{}], customerId [{}], type [{}] and pageLink [{}]", assetEntities, tenantId, customerId, type, pageLink); - return DaoUtil.convertDataList(assetEntities); - } - - @Override - public ListenableFuture> findAssetsByTenantIdAndCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List assetIds) { - log.debug("Try to find assets by tenantId [{}], customerId [{}] and asset Ids [{}]", tenantId, customerId, assetIds); - Select select = select().from(getColumnFamilyName()); - Select.Where query = select.where(); - query.and(eq(ASSET_TENANT_ID_PROPERTY, tenantId)); - query.and(eq(ASSET_CUSTOMER_ID_PROPERTY, customerId)); - query.and(in(ID_PROPERTY, assetIds)); - return findListByStatementAsync(new TenantId(tenantId), query); - } - - @Override - public Optional findAssetsByTenantIdAndName(UUID tenantId, String assetName) { - Select select = select().from(ASSET_BY_TENANT_AND_NAME_VIEW_NAME); - Select.Where query = select.where(); - query.and(eq(ASSET_TENANT_ID_PROPERTY, tenantId)); - query.and(eq(ASSET_NAME_PROPERTY, assetName)); - AssetEntity assetEntity = (AssetEntity) findOneByStatement(new TenantId(tenantId), query); - return Optional.ofNullable(DaoUtil.getData(assetEntity)); - } - - @Override - public ListenableFuture> findTenantAssetTypesAsync(UUID tenantId) { - Select select = select().from(ENTITY_SUBTYPE_COLUMN_FAMILY_NAME); - Select.Where query = select.where(); - query.and(eq(ENTITY_SUBTYPE_TENANT_ID_PROPERTY, tenantId)); - query.and(eq(ENTITY_SUBTYPE_ENTITY_TYPE_PROPERTY, EntityType.ASSET)); - query.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel()); - ResultSetFuture resultSetFuture = executeAsyncRead(new TenantId(tenantId), query); - return Futures.transform(resultSetFuture, new Function>() { - @Nullable - @Override - public List apply(@Nullable ResultSet resultSet) { - Result result = cluster.getMapper(EntitySubtypeEntity.class).map(resultSet); - if (result != null) { - List entitySubtypes = new ArrayList<>(); - result.all().forEach((entitySubtypeEntity) -> - entitySubtypes.add(entitySubtypeEntity.toEntitySubtype()) - ); - return entitySubtypes; - } else { - return Collections.emptyList(); - } - } - }, MoreExecutors.directExecutor()); - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CassandraBaseAttributesDao.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CassandraBaseAttributesDao.java deleted file mode 100644 index 481f2ac085..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CassandraBaseAttributesDao.java +++ /dev/null @@ -1,192 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.attributes; - -import com.datastax.driver.core.BoundStatement; -import com.datastax.driver.core.PreparedStatement; -import com.datastax.driver.core.ResultSet; -import com.datastax.driver.core.Row; -import com.datastax.driver.core.Statement; -import com.datastax.driver.core.querybuilder.QueryBuilder; -import com.datastax.driver.core.querybuilder.Select; -import com.google.common.base.Function; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; -import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.nosql.CassandraAbstractAsyncDao; -import org.thingsboard.server.dao.timeseries.CassandraBaseTimeseriesDao; -import org.thingsboard.server.dao.util.NoSqlDao; - -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; -import static com.datastax.driver.core.querybuilder.QueryBuilder.select; -import static org.thingsboard.server.dao.model.ModelConstants.ATTRIBUTES_KV_CF; -import static org.thingsboard.server.dao.model.ModelConstants.ATTRIBUTE_KEY_COLUMN; -import static org.thingsboard.server.dao.model.ModelConstants.ATTRIBUTE_TYPE_COLUMN; -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_ID_COLUMN; -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_TYPE_COLUMN; -import static org.thingsboard.server.dao.model.ModelConstants.LAST_UPDATE_TS_COLUMN; - -/** - * @author Andrew Shvayka - */ -@Component -@Slf4j -@NoSqlDao -public class CassandraBaseAttributesDao extends CassandraAbstractAsyncDao implements AttributesDao { - - private PreparedStatement saveStmt; - - @PostConstruct - public void init() { - super.startExecutor(); - } - - @PreDestroy - public void stop() { - super.stopExecutor(); - } - - @Override - public ListenableFuture> find(TenantId tenantId, EntityId entityId, String attributeType, String attributeKey) { - Select.Where select = select().from(ATTRIBUTES_KV_CF) - .where(eq(ENTITY_TYPE_COLUMN, entityId.getEntityType())) - .and(eq(ENTITY_ID_COLUMN, entityId.getId())) - .and(eq(ATTRIBUTE_TYPE_COLUMN, attributeType)) - .and(eq(ATTRIBUTE_KEY_COLUMN, attributeKey)); - log.trace("Generated query [{}] for entityId {} and key {}", select, entityId, attributeKey); - return Futures.transform(executeAsyncRead(tenantId, select), (Function>) input -> - Optional.ofNullable(convertResultToAttributesKvEntry(attributeKey, input.one())) - , readResultsProcessingExecutor); - } - - @Override - public ListenableFuture> find(TenantId tenantId, EntityId entityId, String attributeType, Collection attributeKeys) { - List>> entries = new ArrayList<>(); - attributeKeys.forEach(attributeKey -> entries.add(find(tenantId, entityId, attributeType, attributeKey))); - return Futures.transform(Futures.allAsList(entries), (Function>, ? extends List>) input -> { - List result = new ArrayList<>(); - input.stream().filter(opt -> opt.isPresent()).forEach(opt -> result.add(opt.get())); - return result; - }, readResultsProcessingExecutor); - } - - - @Override - public ListenableFuture> findAll(TenantId tenantId, EntityId entityId, String attributeType) { - Select.Where select = select().from(ATTRIBUTES_KV_CF) - .where(eq(ENTITY_TYPE_COLUMN, entityId.getEntityType())) - .and(eq(ENTITY_ID_COLUMN, entityId.getId())) - .and(eq(ATTRIBUTE_TYPE_COLUMN, attributeType)); - log.trace("Generated query [{}] for entityId {} and attributeType {}", select, entityId, attributeType); - return Futures.transform(executeAsyncRead(tenantId, select), (Function>) input -> - convertResultToAttributesKvEntryList(input) - , readResultsProcessingExecutor); - } - - @Override - public ListenableFuture save(TenantId tenantId, EntityId entityId, String attributeType, AttributeKvEntry attribute) { - BoundStatement stmt = getSaveStmt().bind() - .setString(0, entityId.getEntityType().name()) - .setUUID(1, entityId.getId()) - .setString(2, attributeType) - .setString(3, attribute.getKey()) - .setLong(4, attribute.getLastUpdateTs()) - .set(5, attribute.getStrValue().orElse(null), String.class) - .set(6, attribute.getBooleanValue().orElse(null), Boolean.class) - .set(7, attribute.getLongValue().orElse(null), Long.class) - .set(8, attribute.getDoubleValue().orElse(null), Double.class) - .set(9, attribute.getJsonValue().orElse(null), String.class); - - log.trace("Generated save stmt [{}] for entityId {} and attributeType {} and attribute", stmt, entityId, attributeType, attribute); - return getFuture(executeAsyncWrite(tenantId, stmt), rs -> null); - } - - @Override - public ListenableFuture> removeAll(TenantId tenantId, EntityId entityId, String attributeType, List keys) { - List> futures = keys - .stream() - .map(key -> delete(tenantId, entityId, attributeType, key)) - .collect(Collectors.toList()); - return Futures.allAsList(futures); - } - - private ListenableFuture delete(TenantId tenantId, EntityId entityId, String attributeType, String key) { - Statement delete = QueryBuilder.delete().all().from(ModelConstants.ATTRIBUTES_KV_CF) - .where(eq(ENTITY_TYPE_COLUMN, entityId.getEntityType())) - .and(eq(ENTITY_ID_COLUMN, entityId.getId())) - .and(eq(ATTRIBUTE_TYPE_COLUMN, attributeType)) - .and(eq(ATTRIBUTE_KEY_COLUMN, key)); - log.debug("Remove request: {}", delete.toString()); - return getFuture(executeAsyncWrite(tenantId, delete), rs -> null); - } - - private PreparedStatement getSaveStmt() { - if (saveStmt == null) { - saveStmt = prepare("INSERT INTO " + ModelConstants.ATTRIBUTES_KV_CF + - "(" + ENTITY_TYPE_COLUMN + - "," + ENTITY_ID_COLUMN + - "," + ATTRIBUTE_TYPE_COLUMN + - "," + ATTRIBUTE_KEY_COLUMN + - "," + LAST_UPDATE_TS_COLUMN + - "," + ModelConstants.STRING_VALUE_COLUMN + - "," + ModelConstants.BOOLEAN_VALUE_COLUMN + - "," + ModelConstants.LONG_VALUE_COLUMN + - "," + ModelConstants.DOUBLE_VALUE_COLUMN + - "," + ModelConstants.JSON_VALUE_COLUMN + - ")" + - " VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); - } - return saveStmt; - } - - private AttributeKvEntry convertResultToAttributesKvEntry(String key, Row row) { - AttributeKvEntry attributeEntry = null; - if (row != null) { - long lastUpdateTs = row.get(LAST_UPDATE_TS_COLUMN, Long.class); - attributeEntry = new BaseAttributeKvEntry(CassandraBaseTimeseriesDao.toKvEntry(row, key), lastUpdateTs); - } - return attributeEntry; - } - - private List convertResultToAttributesKvEntryList(ResultSet resultSet) { - List rows = resultSet.all(); - List entries = new ArrayList<>(rows.size()); - if (!rows.isEmpty()) { - rows.forEach(row -> { - String key = row.getString(ModelConstants.ATTRIBUTE_KEY_COLUMN); - AttributeKvEntry kvEntry = convertResultToAttributesKvEntry(key, row); - if (kvEntry != null) { - entries.add(kvEntry); - } - }); - } - return entries; - } -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java index f4efc2827f..34b5090179 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.audit.AuditLog; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.Dao; @@ -31,19 +32,11 @@ public interface AuditLogDao extends Dao { ListenableFuture saveByTenantId(AuditLog auditLog); - ListenableFuture saveByTenantIdAndEntityId(AuditLog auditLog); + PageData findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, List actionTypes, TimePageLink pageLink); - ListenableFuture saveByTenantIdAndCustomerId(AuditLog auditLog); + PageData findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, List actionTypes, TimePageLink pageLink); - ListenableFuture saveByTenantIdAndUserId(AuditLog auditLog); + PageData findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, List actionTypes, TimePageLink pageLink); - ListenableFuture savePartitionsByTenantId(AuditLog auditLog); - - List findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, List actionTypes, TimePageLink pageLink); - - List findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, List actionTypes, TimePageLink pageLink); - - List findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, List actionTypes, TimePageLink pageLink); - - List findAuditLogsByTenantId(UUID tenantId, List actionTypes, TimePageLink pageLink); + PageData findAuditLogsByTenantId(UUID tenantId, List actionTypes, TimePageLink pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogQueryCursor.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogQueryCursor.java deleted file mode 100644 index 5156f05aef..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogQueryCursor.java +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.audit; - -import lombok.Getter; -import org.thingsboard.server.common.data.page.TimePageLink; -import org.thingsboard.server.dao.model.nosql.AuditLogEntity; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -public class AuditLogQueryCursor { - @Getter - private final UUID tenantId; - @Getter - private final List data; - @Getter - private final TimePageLink pageLink; - - private final List partitions; - - private int partitionIndex; - private int currentLimit; - - public AuditLogQueryCursor(UUID tenantId, TimePageLink pageLink, List partitions) { - this.tenantId = tenantId; - this.partitions = partitions; - this.partitionIndex = partitions.size() - 1; - this.data = new ArrayList<>(); - this.currentLimit = pageLink.getLimit(); - this.pageLink = pageLink; - } - - public boolean hasNextPartition() { - return partitionIndex >= 0; - } - - public boolean isFull() { - return currentLimit <= 0; - } - - public long getNextPartition() { - long partition = partitions.get(partitionIndex); - partitionIndex--; - return partition; - } - - public int getCurrentLimit() { - return currentLimit; - } - - public void addData(List newData) { - currentLimit -= newData.size(); - data.addAll(newData); - } -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java index 0d1c708f68..6b3f1650a2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.dao.audit; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -39,7 +39,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.rule.RuleChainMetaData; @@ -47,11 +47,13 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.dao.audit.sink.AuditLogSink; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.device.provision.ProvisionRequest; import org.thingsboard.server.dao.service.DataValidator; import java.io.PrintWriter; import java.io.StringWriter; import java.util.List; +import java.util.UUID; import static org.thingsboard.server.dao.service.Validator.validateEntityId; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -79,38 +81,34 @@ public class AuditLogServiceImpl implements AuditLogService { private AuditLogSink auditLogSink; @Override - public TimePageData findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, List actionTypes, TimePageLink pageLink) { + public PageData findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, List actionTypes, TimePageLink pageLink) { log.trace("Executing findAuditLogsByTenantIdAndCustomerId [{}], [{}], [{}]", tenantId, customerId, pageLink); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); validateId(customerId, "Incorrect customerId " + customerId); - List auditLogs = auditLogDao.findAuditLogsByTenantIdAndCustomerId(tenantId.getId(), customerId, actionTypes, pageLink); - return new TimePageData<>(auditLogs, pageLink); + return auditLogDao.findAuditLogsByTenantIdAndCustomerId(tenantId.getId(), customerId, actionTypes, pageLink); } @Override - public TimePageData findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, List actionTypes, TimePageLink pageLink) { + public PageData findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, List actionTypes, TimePageLink pageLink) { log.trace("Executing findAuditLogsByTenantIdAndUserId [{}], [{}], [{}]", tenantId, userId, pageLink); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); validateId(userId, "Incorrect userId" + userId); - List auditLogs = auditLogDao.findAuditLogsByTenantIdAndUserId(tenantId.getId(), userId, actionTypes, pageLink); - return new TimePageData<>(auditLogs, pageLink); + return auditLogDao.findAuditLogsByTenantIdAndUserId(tenantId.getId(), userId, actionTypes, pageLink); } @Override - public TimePageData findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, List actionTypes, TimePageLink pageLink) { + public PageData findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, List actionTypes, TimePageLink pageLink) { log.trace("Executing findAuditLogsByTenantIdAndEntityId [{}], [{}], [{}]", tenantId, entityId, pageLink); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); validateEntityId(entityId, INCORRECT_TENANT_ID + entityId); - List auditLogs = auditLogDao.findAuditLogsByTenantIdAndEntityId(tenantId.getId(), entityId, actionTypes, pageLink); - return new TimePageData<>(auditLogs, pageLink); + return auditLogDao.findAuditLogsByTenantIdAndEntityId(tenantId.getId(), entityId, actionTypes, pageLink); } @Override - public TimePageData findAuditLogsByTenantId(TenantId tenantId, List actionTypes, TimePageLink pageLink) { + public PageData findAuditLogsByTenantId(TenantId tenantId, List actionTypes, TimePageLink pageLink) { log.trace("Executing findAuditLogs [{}]", pageLink); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - List auditLogs = auditLogDao.findAuditLogsByTenantId(tenantId.getId(), actionTypes, pageLink); - return new TimePageData<>(auditLogs, pageLink); + return auditLogDao.findAuditLogsByTenantId(tenantId.getId(), actionTypes, pageLink); } @Override @@ -260,6 +258,13 @@ public class AuditLogServiceImpl implements AuditLogService { actionData.put("os", os); actionData.put("device", device); break; + case PROVISION_SUCCESS: + case PROVISION_FAILURE: + ProvisionRequest request = extractParameter(ProvisionRequest.class, additionalInfo); + if (request != null) { + actionData.set("provisionRequest", objectMapper.valueToTree(request)); + } + break; } return actionData; } @@ -300,7 +305,9 @@ public class AuditLogServiceImpl implements AuditLogService { ActionStatus actionStatus, String actionFailureDetails) { AuditLog result = new AuditLog(); - result.setId(new AuditLogId(UUIDs.timeBased())); + UUID id = Uuids.timeBased(); + result.setId(new AuditLogId(id)); + result.setCreatedTime(Uuids.unixTimestamp(id)); result.setTenantId(tenantId); result.setEntityId(entityId); result.setEntityName(entityName); @@ -329,11 +336,7 @@ public class AuditLogServiceImpl implements AuditLogService { log.trace("Executing logAction [{}]", auditLogEntry); auditLogValidator.validate(auditLogEntry, AuditLog::getTenantId); List> futures = Lists.newArrayListWithExpectedSize(INSERTS_PER_ENTRY); - futures.add(auditLogDao.savePartitionsByTenantId(auditLogEntry)); futures.add(auditLogDao.saveByTenantId(auditLogEntry)); - futures.add(auditLogDao.saveByTenantIdAndEntityId(auditLogEntry)); - futures.add(auditLogDao.saveByTenantIdAndCustomerId(auditLogEntry)); - futures.add(auditLogDao.saveByTenantIdAndUserId(auditLogEntry)); auditLogSink.logAction(auditLogEntry); diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java deleted file mode 100644 index a2dc184936..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java +++ /dev/null @@ -1,361 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.audit; - -import com.datastax.driver.core.BoundStatement; -import com.datastax.driver.core.PreparedStatement; -import com.datastax.driver.core.ResultSet; -import com.datastax.driver.core.ResultSetFuture; -import com.datastax.driver.core.querybuilder.QueryBuilder; -import com.datastax.driver.core.querybuilder.Select; -import com.google.common.base.Function; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.env.Environment; -import org.springframework.stereotype.Component; -import org.thingsboard.common.util.ThingsBoardThreadFactory; -import org.thingsboard.server.common.data.audit.ActionType; -import org.thingsboard.server.common.data.audit.AuditLog; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.page.TimePageLink; -import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.nosql.AuditLogEntity; -import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTimeDao; -import org.thingsboard.server.dao.timeseries.NoSqlTsPartitionDate; -import org.thingsboard.server.dao.util.NoSqlDao; - -import javax.annotation.Nullable; -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.StringJoiner; -import java.util.UUID; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.stream.Collectors; - -import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; -import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_BY_CUSTOMER_ID_CF; -import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_BY_ENTITY_ID_CF; -import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_BY_TENANT_ID_CF; -import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_BY_USER_ID_CF; -import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_COLUMN_FAMILY_NAME; - -@Component -@Slf4j -@NoSqlDao -public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao implements AuditLogDao { - - private static final String INSERT_INTO = "INSERT INTO "; - - @Autowired - private Environment environment; - - @Override - protected Class getColumnFamilyClass() { - return AuditLogEntity.class; - } - - @Override - protected String getColumnFamilyName() { - return AUDIT_LOG_COLUMN_FAMILY_NAME; - } - - protected ExecutorService readResultsProcessingExecutor; - - @Value("${audit-log.by_tenant_partitioning}") - private String partitioning; - private NoSqlTsPartitionDate tsFormat; - - @Value("${audit-log.default_query_period}") - private Integer defaultQueryPeriodInDays; - - private PreparedStatement partitionInsertStmt; - private PreparedStatement saveByTenantStmt; - private PreparedStatement saveByTenantIdAndUserIdStmt; - private PreparedStatement saveByTenantIdAndEntityIdStmt; - private PreparedStatement saveByTenantIdAndCustomerIdStmt; - - private boolean isInstall() { - return environment.acceptsProfiles("install"); - } - - @PostConstruct - public void init() { - if (!isInstall()) { - Optional partition = NoSqlTsPartitionDate.parse(partitioning); - if (partition.isPresent()) { - tsFormat = partition.get(); - } else { - log.warn("Incorrect configuration of partitioning {}", partitioning); - throw new RuntimeException("Failed to parse partitioning property: " + partitioning + "!"); - } - } - readResultsProcessingExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("audit-log")); - } - - @PreDestroy - public void stopExecutor() { - if (readResultsProcessingExecutor != null) { - readResultsProcessingExecutor.shutdownNow(); - } - } - - private ListenableFuture getFuture(ResultSetFuture future, java.util.function.Function transformer) { - return Futures.transform(future, new Function() { - @Nullable - @Override - public T apply(@Nullable ResultSet input) { - return transformer.apply(input); - } - }, readResultsProcessingExecutor); - } - - @Override - public ListenableFuture saveByTenantId(AuditLog auditLog) { - log.debug("Save saveByTenantId [{}] ", auditLog); - - long partition = toPartitionTs(LocalDate.now().atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli()); - BoundStatement stmt = getSaveByTenantStmt().bind(); - stmt = setSaveStmtVariables(stmt, auditLog, partition); - return getFuture(executeAsyncWrite(auditLog.getTenantId(), stmt), rs -> null); - } - - @Override - public ListenableFuture saveByTenantIdAndEntityId(AuditLog auditLog) { - log.debug("Save saveByTenantIdAndEntityId [{}] ", auditLog); - - BoundStatement stmt = getSaveByTenantIdAndEntityIdStmt().bind(); - stmt = setSaveStmtVariables(stmt, auditLog, -1); - return getFuture(executeAsyncWrite(auditLog.getTenantId(), stmt), rs -> null); - } - - @Override - public ListenableFuture saveByTenantIdAndCustomerId(AuditLog auditLog) { - log.debug("Save saveByTenantIdAndCustomerId [{}] ", auditLog); - - BoundStatement stmt = getSaveByTenantIdAndCustomerIdStmt().bind(); - stmt = setSaveStmtVariables(stmt, auditLog, -1); - return getFuture(executeAsyncWrite(auditLog.getTenantId(), stmt), rs -> null); - } - - @Override - public ListenableFuture saveByTenantIdAndUserId(AuditLog auditLog) { - log.debug("Save saveByTenantIdAndUserId [{}] ", auditLog); - - BoundStatement stmt = getSaveByTenantIdAndUserIdStmt().bind(); - stmt = setSaveStmtVariables(stmt, auditLog, -1); - return getFuture(executeAsyncWrite(auditLog.getTenantId(), stmt), rs -> null); - } - - private BoundStatement setSaveStmtVariables(BoundStatement stmt, AuditLog auditLog, long partition) { - stmt.setUUID(0, auditLog.getId().getId()) - .setUUID(1, auditLog.getTenantId().getId()) - .setUUID(2, auditLog.getCustomerId().getId()) - .setUUID(3, auditLog.getEntityId().getId()) - .setString(4, auditLog.getEntityId().getEntityType().name()) - .setString(5, auditLog.getEntityName()) - .setUUID(6, auditLog.getUserId().getId()) - .setString(7, auditLog.getUserName()) - .setString(8, auditLog.getActionType().name()) - .setString(9, auditLog.getActionData() != null ? auditLog.getActionData().toString() : null) - .setString(10, auditLog.getActionStatus().name()) - .setString(11, auditLog.getActionFailureDetails()); - if (partition > -1) { - stmt.setLong(12, partition); - } - return stmt; - } - - @Override - public ListenableFuture savePartitionsByTenantId(AuditLog auditLog) { - log.debug("Save savePartitionsByTenantId [{}] ", auditLog); - - long partition = toPartitionTs(LocalDate.now().atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli()); - - BoundStatement stmt = getPartitionInsertStmt().bind(); - stmt = stmt.setUUID(0, auditLog.getTenantId().getId()) - .setLong(1, partition); - return getFuture(executeAsyncWrite(auditLog.getTenantId(), stmt), rs -> null); - } - - private PreparedStatement getSaveByTenantStmt() { - if (saveByTenantStmt == null) { - saveByTenantStmt = getSaveByTenantIdAndCFName(ModelConstants.AUDIT_LOG_BY_TENANT_ID_CF, true); - } - return saveByTenantStmt; - } - - private PreparedStatement getSaveByTenantIdAndEntityIdStmt() { - if (saveByTenantIdAndEntityIdStmt == null) { - saveByTenantIdAndEntityIdStmt = getSaveByTenantIdAndCFName(ModelConstants.AUDIT_LOG_BY_ENTITY_ID_CF, false); - } - return saveByTenantIdAndEntityIdStmt; - } - - private PreparedStatement getSaveByTenantIdAndCustomerIdStmt() { - if (saveByTenantIdAndCustomerIdStmt == null) { - saveByTenantIdAndCustomerIdStmt = getSaveByTenantIdAndCFName(ModelConstants.AUDIT_LOG_BY_CUSTOMER_ID_CF, false); - } - return saveByTenantIdAndCustomerIdStmt; - } - - private PreparedStatement getSaveByTenantIdAndUserIdStmt() { - if (saveByTenantIdAndUserIdStmt == null) { - saveByTenantIdAndUserIdStmt = getSaveByTenantIdAndCFName(ModelConstants.AUDIT_LOG_BY_USER_ID_CF, false); - } - return saveByTenantIdAndUserIdStmt; - } - - private PreparedStatement getSaveByTenantIdAndCFName(String cfName, boolean hasPartition) { - List columnsList = new ArrayList(); - columnsList.add(ModelConstants.AUDIT_LOG_ID_PROPERTY); - columnsList.add(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY); - columnsList.add(ModelConstants.AUDIT_LOG_CUSTOMER_ID_PROPERTY); - columnsList.add(ModelConstants.AUDIT_LOG_ENTITY_ID_PROPERTY); - columnsList.add(ModelConstants.AUDIT_LOG_ENTITY_TYPE_PROPERTY); - columnsList.add(ModelConstants.AUDIT_LOG_ENTITY_NAME_PROPERTY); - columnsList.add(ModelConstants.AUDIT_LOG_USER_ID_PROPERTY); - columnsList.add(ModelConstants.AUDIT_LOG_USER_NAME_PROPERTY); - columnsList.add(ModelConstants.AUDIT_LOG_ACTION_TYPE_PROPERTY); - columnsList.add(ModelConstants.AUDIT_LOG_ACTION_DATA_PROPERTY); - columnsList.add(ModelConstants.AUDIT_LOG_ACTION_STATUS_PROPERTY); - columnsList.add(ModelConstants.AUDIT_LOG_ACTION_FAILURE_DETAILS_PROPERTY); - if (hasPartition) { - columnsList.add(ModelConstants.AUDIT_LOG_PARTITION_PROPERTY); - } - StringJoiner values = new StringJoiner(","); - for (int i = 0; i < columnsList.size(); i++) { - values.add("?"); - } - String statementString = INSERT_INTO + cfName + " (" + String.join(",", columnsList) + ") VALUES (" + values.toString() + ")"; - return prepare(statementString); - } - - private PreparedStatement getPartitionInsertStmt() { - if (partitionInsertStmt == null) { - partitionInsertStmt = prepare(INSERT_INTO + ModelConstants.AUDIT_LOG_BY_TENANT_ID_PARTITIONS_CF + - "(" + ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY + - "," + ModelConstants.AUDIT_LOG_PARTITION_PROPERTY + ")" + - " VALUES(?, ?)"); - } - return partitionInsertStmt; - } - - private long toPartitionTs(long ts) { - LocalDateTime time = LocalDateTime.ofInstant(Instant.ofEpochMilli(ts), ZoneOffset.UTC); - return tsFormat.truncatedTo(time).toInstant(ZoneOffset.UTC).toEpochMilli(); - } - - @Override - public List findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, List actionTypes, TimePageLink pageLink) { - log.trace("Try to find audit logs by tenant [{}], entity [{}] and pageLink [{}]", tenantId, entityId, pageLink); - List entities = findPageWithTimeSearch(new TenantId(tenantId), AUDIT_LOG_BY_ENTITY_ID_CF, - Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId), - eq(ModelConstants.AUDIT_LOG_ENTITY_TYPE_PROPERTY, entityId.getEntityType()), - eq(ModelConstants.AUDIT_LOG_ENTITY_ID_PROPERTY, entityId.getId())), - pageLink); - log.trace("Found audit logs by tenant [{}], entity [{}] and pageLink [{}]", tenantId, entityId, pageLink); - return DaoUtil.convertDataList(entities); - } - - @Override - public List findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, List actionTypes, TimePageLink pageLink) { - log.trace("Try to find audit logs by tenant [{}], customer [{}] and pageLink [{}]", tenantId, customerId, pageLink); - List entities = findPageWithTimeSearch(new TenantId(tenantId), AUDIT_LOG_BY_CUSTOMER_ID_CF, - Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId), - eq(ModelConstants.AUDIT_LOG_CUSTOMER_ID_PROPERTY, customerId.getId())), - pageLink); - log.trace("Found audit logs by tenant [{}], customer [{}] and pageLink [{}]", tenantId, customerId, pageLink); - return DaoUtil.convertDataList(entities); - } - - @Override - public List findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, List actionTypes, TimePageLink pageLink) { - log.trace("Try to find audit logs by tenant [{}], user [{}] and pageLink [{}]", tenantId, userId, pageLink); - List entities = findPageWithTimeSearch(new TenantId(tenantId), AUDIT_LOG_BY_USER_ID_CF, - Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId), - eq(ModelConstants.AUDIT_LOG_USER_ID_PROPERTY, userId.getId())), - pageLink); - log.trace("Found audit logs by tenant [{}], user [{}] and pageLink [{}]", tenantId, userId, pageLink); - return DaoUtil.convertDataList(entities); - } - - @Override - public List findAuditLogsByTenantId(UUID tenantId, List actionTypes, TimePageLink pageLink) { - log.trace("Try to find audit logs by tenant [{}] and pageLink [{}]", tenantId, pageLink); - - long minPartition; - if (pageLink.getStartTime() != null && pageLink.getStartTime() != 0) { - minPartition = toPartitionTs(pageLink.getStartTime()); - } else { - minPartition = toPartitionTs(LocalDate.now().minusDays(defaultQueryPeriodInDays).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli()); - } - - long maxPartition; - if (pageLink.getEndTime() != null && pageLink.getEndTime() != 0) { - maxPartition = toPartitionTs(pageLink.getEndTime()); - } else { - maxPartition = toPartitionTs(LocalDate.now().atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli()); - } - - List partitions = fetchPartitions(tenantId, minPartition, maxPartition) - .all() - .stream() - .map(row -> row.getLong(ModelConstants.PARTITION_COLUMN)) - .collect(Collectors.toList()); - - AuditLogQueryCursor cursor = new AuditLogQueryCursor(tenantId, pageLink, partitions); - List entities = fetchSequentiallyWithLimit(cursor); - log.trace("Found audit logs by tenant [{}] and pageLink [{}]", tenantId, pageLink); - return DaoUtil.convertDataList(entities); - } - - private List fetchSequentiallyWithLimit(AuditLogQueryCursor cursor) { - if (cursor.isFull() || !cursor.hasNextPartition()) { - return cursor.getData(); - } else { - cursor.addData(findPageWithTimeSearch(new TenantId(cursor.getTenantId()), AUDIT_LOG_BY_TENANT_ID_CF, - Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, cursor.getTenantId()), - eq(ModelConstants.AUDIT_LOG_PARTITION_PROPERTY, cursor.getNextPartition())), - cursor.getPageLink())); - return fetchSequentiallyWithLimit(cursor); - } - } - - private ResultSet fetchPartitions(UUID tenantId, long minPartition, long maxPartition) { - Select.Where select = QueryBuilder.select(ModelConstants.AUDIT_LOG_PARTITION_PROPERTY).from(ModelConstants.AUDIT_LOG_BY_TENANT_ID_PARTITIONS_CF) - .where(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId)); - select.and(QueryBuilder.gte(ModelConstants.PARTITION_COLUMN, minPartition)); - select.and(QueryBuilder.lte(ModelConstants.PARTITION_COLUMN, maxPartition)); - return executeRead(new TenantId(tenantId), select); - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java index 30e28a1bf2..12acd08361 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java @@ -25,7 +25,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import java.util.List; @@ -35,23 +35,23 @@ import java.util.List; public class DummyAuditLogServiceImpl implements AuditLogService { @Override - public TimePageData findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, List actionTypes, TimePageLink pageLink) { - return new TimePageData<>(null, pageLink); + public PageData findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, List actionTypes, TimePageLink pageLink) { + return new PageData(); } @Override - public TimePageData findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, List actionTypes, TimePageLink pageLink) { - return new TimePageData<>(null, pageLink); + public PageData findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, List actionTypes, TimePageLink pageLink) { + return new PageData(); } @Override - public TimePageData findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, List actionTypes, TimePageLink pageLink) { - return new TimePageData<>(null, pageLink); + public PageData findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, List actionTypes, TimePageLink pageLink) { + return new PageData(); } @Override - public TimePageData findAuditLogsByTenantId(TenantId tenantId, List actionTypes, TimePageLink pageLink) { - return new TimePageData<>(null, pageLink); + public PageData findAuditLogsByTenantId(TenantId tenantId, List actionTypes, TimePageLink pageLink) { + return new PageData(); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java b/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java index ff669a8234..4acd766c58 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java @@ -27,8 +27,8 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.id.ComponentDescriptorId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentScope; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -37,7 +37,6 @@ import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.Validator; -import java.util.List; import java.util.Optional; /** @@ -70,17 +69,15 @@ public class BaseComponentDescriptorService implements ComponentDescriptorServic } @Override - public TextPageData findByTypeAndPageLink(TenantId tenantId, ComponentType type, TextPageLink pageLink) { - Validator.validatePageLink(pageLink, "Incorrect PageLink object for search plugin components request."); - List components = componentDescriptorDao.findByTypeAndPageLink(tenantId, type, pageLink); - return new TextPageData<>(components, pageLink); + public PageData findByTypeAndPageLink(TenantId tenantId, ComponentType type, PageLink pageLink) { + Validator.validatePageLink(pageLink); + return componentDescriptorDao.findByTypeAndPageLink(tenantId, type, pageLink); } @Override - public TextPageData findByScopeAndTypeAndPageLink(TenantId tenantId, ComponentScope scope, ComponentType type, TextPageLink pageLink) { - Validator.validatePageLink(pageLink, "Incorrect PageLink object for search plugin components request."); - List components = componentDescriptorDao.findByScopeAndTypeAndPageLink(tenantId, scope, type, pageLink); - return new TextPageData<>(components, pageLink); + public PageData findByScopeAndTypeAndPageLink(TenantId tenantId, ComponentScope scope, ComponentType type, PageLink pageLink) { + Validator.validatePageLink(pageLink); + return componentDescriptorDao.findByScopeAndTypeAndPageLink(tenantId, scope, type, pageLink); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/component/CassandraBaseComponentDescriptorDao.java b/dao/src/main/java/org/thingsboard/server/dao/component/CassandraBaseComponentDescriptorDao.java deleted file mode 100644 index 68e398131b..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/component/CassandraBaseComponentDescriptorDao.java +++ /dev/null @@ -1,175 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.component; - -import com.datastax.driver.core.ResultSet; -import com.datastax.driver.core.Statement; -import com.datastax.driver.core.querybuilder.QueryBuilder; -import com.datastax.driver.core.querybuilder.Select; -import com.datastax.driver.core.utils.UUIDs; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.id.ComponentDescriptorId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; -import org.thingsboard.server.common.data.plugin.ComponentDescriptor; -import org.thingsboard.server.common.data.plugin.ComponentScope; -import org.thingsboard.server.common.data.plugin.ComponentType; -import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.nosql.ComponentDescriptorEntity; -import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTextDao; -import org.thingsboard.server.dao.util.NoSqlDao; - -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; -import static com.datastax.driver.core.querybuilder.QueryBuilder.select; - -/** - * @author Andrew Shvayka - */ -@Component -@Slf4j -@NoSqlDao -public class CassandraBaseComponentDescriptorDao extends CassandraAbstractSearchTextDao implements ComponentDescriptorDao { - - public static final String SEARCH_RESULT = "Search result: [{}]"; - - @Override - protected Class getColumnFamilyClass() { - return ComponentDescriptorEntity.class; - } - - @Override - protected String getColumnFamilyName() { - return ModelConstants.COMPONENT_DESCRIPTOR_COLUMN_FAMILY_NAME; - } - - @Override - public Optional saveIfNotExist(TenantId tenantId, ComponentDescriptor component) { - ComponentDescriptorEntity entity = new ComponentDescriptorEntity(component); - log.debug("Save component entity [{}]", entity); - Optional result = saveIfNotExist(tenantId, entity); - if (log.isTraceEnabled()) { - log.trace("Saved result: [{}] for component entity [{}]", result.isPresent(), result.orElse(null)); - } else { - log.debug("Saved result: [{}]", result.isPresent()); - } - return result; - } - - @Override - public ComponentDescriptor findById(TenantId tenantId, ComponentDescriptorId componentId) { - log.debug("Search component entity by id [{}]", componentId); - ComponentDescriptor componentDescriptor = super.findById(tenantId, componentId.getId()); - if (log.isTraceEnabled()) { - log.trace("Search result: [{}] for component entity [{}]", componentDescriptor != null, componentDescriptor); - } else { - log.debug(SEARCH_RESULT, componentDescriptor != null); - } - return componentDescriptor; - } - - @Override - public ComponentDescriptor findByClazz(TenantId tenantId, String clazz) { - log.debug("Search component entity by clazz [{}]", clazz); - Select.Where query = select().from(getColumnFamilyName()).where(eq(ModelConstants.COMPONENT_DESCRIPTOR_CLASS_PROPERTY, clazz)); - log.trace("Execute query [{}]", query); - ComponentDescriptorEntity entity = findOneByStatement(tenantId, query); - if (log.isTraceEnabled()) { - log.trace("Search result: [{}] for component entity [{}]", entity != null, entity); - } else { - log.debug(SEARCH_RESULT, entity != null); - } - return DaoUtil.getData(entity); - } - - @Override - public List findByTypeAndPageLink(TenantId tenantId, ComponentType type, TextPageLink pageLink) { - log.debug("Try to find component by type [{}] and pageLink [{}]", type, pageLink); - List entities = findPageWithTextSearch(tenantId, ModelConstants.COMPONENT_DESCRIPTOR_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME, - Arrays.asList(eq(ModelConstants.COMPONENT_DESCRIPTOR_TYPE_PROPERTY, type)), pageLink); - if (log.isTraceEnabled()) { - log.trace(SEARCH_RESULT, Arrays.toString(entities.toArray())); - } else { - log.debug(SEARCH_RESULT, entities.size()); - } - return DaoUtil.convertDataList(entities); - } - - @Override - public List findByScopeAndTypeAndPageLink(TenantId tenantId, ComponentScope scope, ComponentType type, TextPageLink pageLink) { - log.debug("Try to find component by scope [{}] and type [{}] and pageLink [{}]", scope, type, pageLink); - List entities = findPageWithTextSearch(tenantId, ModelConstants.COMPONENT_DESCRIPTOR_BY_SCOPE_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME, - Arrays.asList(eq(ModelConstants.COMPONENT_DESCRIPTOR_TYPE_PROPERTY, type), - eq(ModelConstants.COMPONENT_DESCRIPTOR_SCOPE_PROPERTY, scope.name())), pageLink); - if (log.isTraceEnabled()) { - log.trace(SEARCH_RESULT, Arrays.toString(entities.toArray())); - } else { - log.debug(SEARCH_RESULT, entities.size()); - } - return DaoUtil.convertDataList(entities); - } - - public boolean removeById(TenantId tenantId, UUID key) { - Statement delete = QueryBuilder.delete().all().from(ModelConstants.COMPONENT_DESCRIPTOR_BY_ID).where(eq(ModelConstants.ID_PROPERTY, key)); - log.debug("Remove request: {}", delete.toString()); - return executeWrite(tenantId, delete).wasApplied(); - } - - @Override - public void deleteById(TenantId tenantId, ComponentDescriptorId id) { - log.debug("Delete plugin meta-data entity by id [{}]", id); - boolean result = removeById(tenantId, id.getId()); - log.debug("Delete result: [{}]", result); - } - - @Override - public void deleteByClazz(TenantId tenantId, String clazz) { - log.debug("Delete plugin meta-data entity by id [{}]", clazz); - Statement delete = QueryBuilder.delete().all().from(getColumnFamilyName()).where(eq(ModelConstants.COMPONENT_DESCRIPTOR_CLASS_PROPERTY, clazz)); - log.debug("Remove request: {}", delete.toString()); - ResultSet resultSet = executeWrite(tenantId, delete); - log.debug("Delete result: [{}]", resultSet.wasApplied()); - } - - private Optional saveIfNotExist(TenantId tenantId, ComponentDescriptorEntity entity) { - if (entity.getUuid() == null) { - entity.setUuid(UUIDs.timeBased()); - } - - ResultSet rs = executeRead(tenantId, QueryBuilder.insertInto(getColumnFamilyName()) - .value(ModelConstants.ID_PROPERTY, entity.getUuid()) - .value(ModelConstants.COMPONENT_DESCRIPTOR_NAME_PROPERTY, entity.getName()) - .value(ModelConstants.COMPONENT_DESCRIPTOR_CLASS_PROPERTY, entity.getClazz()) - .value(ModelConstants.COMPONENT_DESCRIPTOR_TYPE_PROPERTY, entity.getType()) - .value(ModelConstants.COMPONENT_DESCRIPTOR_SCOPE_PROPERTY, entity.getScope()) - .value(ModelConstants.COMPONENT_DESCRIPTOR_CONFIGURATION_DESCRIPTOR_PROPERTY, entity.getConfigurationDescriptor()) - .value(ModelConstants.COMPONENT_DESCRIPTOR_ACTIONS_PROPERTY, entity.getActions()) - .value(ModelConstants.SEARCH_TEXT_PROPERTY, entity.getSearchText()) - .ifNotExists() - ); - if (rs.wasApplied()) { - return Optional.of(DaoUtil.getData(entity)); - } else { - return Optional.empty(); - } - } -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/component/ComponentDescriptorDao.java b/dao/src/main/java/org/thingsboard/server/dao/component/ComponentDescriptorDao.java index b6d7731ccd..7c7a56c204 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/component/ComponentDescriptorDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/component/ComponentDescriptorDao.java @@ -17,13 +17,13 @@ package org.thingsboard.server.dao.component; import org.thingsboard.server.common.data.id.ComponentDescriptorId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentScope; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.dao.Dao; -import java.util.List; import java.util.Optional; /** @@ -37,9 +37,9 @@ public interface ComponentDescriptorDao extends Dao { ComponentDescriptor findByClazz(TenantId tenantId, String clazz); - List findByTypeAndPageLink(TenantId tenantId, ComponentType type, TextPageLink pageLink); + PageData findByTypeAndPageLink(TenantId tenantId, ComponentType type, PageLink pageLink); - List findByScopeAndTypeAndPageLink(TenantId tenantId, ComponentScope scope, ComponentType type, TextPageLink pageLink); + PageData findByScopeAndTypeAndPageLink(TenantId tenantId, ComponentScope scope, ComponentType type, PageLink pageLink); void deleteById(TenantId tenantId, ComponentDescriptorId componentId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CassandraCustomerDao.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CassandraCustomerDao.java deleted file mode 100644 index 79d013ba62..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CassandraCustomerDao.java +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.customer; - -import com.datastax.driver.core.querybuilder.Select; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; -import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.nosql.CustomerEntity; -import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTextDao; -import org.thingsboard.server.dao.util.NoSqlDao; - -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; -import static com.datastax.driver.core.querybuilder.QueryBuilder.select; -import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_BY_TENANT_AND_TITLE_VIEW_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_TENANT_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_TITLE_PROPERTY; - -@Component -@Slf4j -@NoSqlDao -public class CassandraCustomerDao extends CassandraAbstractSearchTextDao implements CustomerDao { - - @Override - protected Class getColumnFamilyClass() { - return CustomerEntity.class; - } - - @Override - protected String getColumnFamilyName() { - return ModelConstants.CUSTOMER_COLUMN_FAMILY_NAME; - } - - @Override - public List findCustomersByTenantId(UUID tenantId, TextPageLink pageLink) { - log.debug("Try to find customers by tenantId [{}] and pageLink [{}]", tenantId, pageLink); - List customerEntities = findPageWithTextSearch(new TenantId(tenantId), ModelConstants.CUSTOMER_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME, - Arrays.asList(eq(ModelConstants.CUSTOMER_TENANT_ID_PROPERTY, tenantId)), - pageLink); - log.trace("Found customers [{}] by tenantId [{}] and pageLink [{}]", customerEntities, tenantId, pageLink); - return DaoUtil.convertDataList(customerEntities); - } - - @Override - public Optional findCustomersByTenantIdAndTitle(UUID tenantId, String title) { - Select select = select().from(CUSTOMER_BY_TENANT_AND_TITLE_VIEW_NAME); - Select.Where query = select.where(); - query.and(eq(CUSTOMER_TENANT_ID_PROPERTY, tenantId)); - query.and(eq(CUSTOMER_TITLE_PROPERTY, title)); - CustomerEntity customerEntity = findOneByStatement(new TenantId(tenantId), query); - Customer customer = DaoUtil.getData(customerEntity); - return Optional.ofNullable(customer); - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java index 69d1aa075c..11fa5cb7cd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java @@ -17,10 +17,10 @@ package org.thingsboard.server.dao.customer; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; -import java.util.List; import java.util.Optional; import java.util.UUID; @@ -44,7 +44,7 @@ public interface CustomerDao extends Dao { * @param pageLink the page link * @return the list of customer objects */ - List findCustomersByTenantId(UUID tenantId, TextPageLink pageLink); + PageData findCustomersByTenantId(UUID tenantId, PageLink pageLink); /** * Find customers by tenantId and customer title. diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java index e274800e89..14d591e7e8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java @@ -26,8 +26,8 @@ import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceService; @@ -42,7 +42,6 @@ import org.thingsboard.server.dao.tenant.TenantDao; import org.thingsboard.server.dao.user.UserService; import java.io.IOException; -import java.util.List; import java.util.Optional; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -144,12 +143,11 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom } @Override - public TextPageData findCustomersByTenantId(TenantId tenantId, TextPageLink pageLink) { + public PageData findCustomersByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findCustomersByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); Validator.validateId(tenantId, "Incorrect tenantId " + tenantId); - Validator.validatePageLink(pageLink, "Incorrect page link " + pageLink); - List customers = customerDao.findCustomersByTenantId(tenantId.getId(), pageLink); - return new TextPageData<>(customers, pageLink); + Validator.validatePageLink(pageLink); + return customerDao.findCustomersByTenantId(tenantId.getId(), pageLink); } @Override @@ -208,7 +206,7 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom new PaginatedRemover() { @Override - protected List findEntities(TenantId tenantId, TenantId id, TextPageLink pageLink) { + protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { return customerDao.findCustomersByTenantId(id.getId(), pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/CassandraDashboardDao.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/CassandraDashboardDao.java deleted file mode 100644 index b129d3f122..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/CassandraDashboardDao.java +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.dashboard; - -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.Dashboard; -import org.thingsboard.server.dao.model.nosql.DashboardEntity; -import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTextDao; -import org.thingsboard.server.dao.util.NoSqlDao; - -import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_COLUMN_FAMILY_NAME; - -@Component -@NoSqlDao -public class CassandraDashboardDao extends CassandraAbstractSearchTextDao implements DashboardDao { - - @Override - protected Class getColumnFamilyClass() { - return DashboardEntity.class; - } - - @Override - protected String getColumnFamilyName() { - return DASHBOARD_COLUMN_FAMILY_NAME; - } -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/CassandraDashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/CassandraDashboardInfoDao.java deleted file mode 100644 index d83ca48558..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/CassandraDashboardInfoDao.java +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.dashboard; - -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.DashboardInfo; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; -import org.thingsboard.server.common.data.page.TimePageLink; -import org.thingsboard.server.common.data.relation.EntityRelation; -import org.thingsboard.server.common.data.relation.RelationTypeGroup; -import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.model.nosql.DashboardInfoEntity; -import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTextDao; -import org.thingsboard.server.dao.relation.RelationDao; -import org.thingsboard.server.dao.util.NoSqlDao; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.UUID; - -import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; -import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_TENANT_ID_PROPERTY; - -@Component -@Slf4j -@NoSqlDao -public class CassandraDashboardInfoDao extends CassandraAbstractSearchTextDao implements DashboardInfoDao { - - @Autowired - private RelationDao relationDao; - - @Override - protected Class getColumnFamilyClass() { - return DashboardInfoEntity.class; - } - - @Override - protected String getColumnFamilyName() { - return DASHBOARD_COLUMN_FAMILY_NAME; - } - - @Override - public List findDashboardsByTenantId(UUID tenantId, TextPageLink pageLink) { - log.debug("Try to find dashboards by tenantId [{}] and pageLink [{}]", tenantId, pageLink); - List dashboardEntities = findPageWithTextSearch(new TenantId(tenantId), DASHBOARD_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME, - Collections.singletonList(eq(DASHBOARD_TENANT_ID_PROPERTY, tenantId)), - pageLink); - - log.trace("Found dashboards [{}] by tenantId [{}] and pageLink [{}]", dashboardEntities, tenantId, pageLink); - return DaoUtil.convertDataList(dashboardEntities); - } - - @Override - public ListenableFuture> findDashboardsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TimePageLink pageLink) { - log.debug("Try to find dashboards by tenantId [{}], customerId[{}] and pageLink [{}]", tenantId, customerId, pageLink); - - ListenableFuture> relations = relationDao.findRelations(new TenantId(tenantId), new CustomerId(customerId), EntityRelation.CONTAINS_TYPE, RelationTypeGroup.DASHBOARD, EntityType.DASHBOARD, pageLink); - - return Futures.transformAsync(relations, input -> { - List> dashboardFutures = new ArrayList<>(input.size()); - for (EntityRelation relation : input) { - dashboardFutures.add(findByIdAsync(new TenantId(tenantId), relation.getTo().getId())); - } - return Futures.successfulAsList(dashboardFutures); - }, MoreExecutors.directExecutor()); - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java index 32a1164df0..a7a5934aef 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java @@ -15,13 +15,11 @@ */ package org.thingsboard.server.dao.dashboard; -import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.DashboardInfo; -import org.thingsboard.server.common.data.page.TextPageLink; -import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; -import java.util.List; import java.util.UUID; /** @@ -36,7 +34,7 @@ public interface DashboardInfoDao extends Dao { * @param pageLink the page link * @return the list of dashboard objects */ - List findDashboardsByTenantId(UUID tenantId, TextPageLink pageLink); + PageData findDashboardsByTenantId(UUID tenantId, PageLink pageLink); /** * Find dashboards by tenantId, customerId and page link. @@ -46,6 +44,6 @@ public interface DashboardInfoDao extends Dao { * @param pageLink the page link * @return the list of dashboard objects */ - ListenableFuture> findDashboardsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TimePageLink pageLink); + PageData findDashboardsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index a014a4fae4..112f998cd4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java @@ -15,10 +15,7 @@ */ package org.thingsboard.server.dao.dashboard; -import com.google.common.base.Function; -import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -30,10 +27,8 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; -import org.thingsboard.server.common.data.page.TimePageData; -import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.dao.customer.CustomerDao; @@ -41,12 +36,9 @@ import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; -import org.thingsboard.server.dao.service.TimePaginatedRemover; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.tenant.TenantDao; -import javax.annotation.Nullable; -import java.util.List; import java.util.concurrent.ExecutionException; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -175,12 +167,11 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb } @Override - public TextPageData findDashboardsByTenantId(TenantId tenantId, TextPageLink pageLink) { + public PageData findDashboardsByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findDashboardsByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - Validator.validatePageLink(pageLink, "Incorrect page link " + pageLink); - List dashboards = dashboardInfoDao.findDashboardsByTenantId(tenantId.getId(), pageLink); - return new TextPageData<>(dashboards, pageLink); + Validator.validatePageLink(pageLink); + return dashboardInfoDao.findDashboardsByTenantId(tenantId.getId(), pageLink); } @Override @@ -191,20 +182,12 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb } @Override - public ListenableFuture> findDashboardsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink) { + public PageData findDashboardsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) { log.trace("Executing findDashboardsByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink); Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); Validator.validateId(customerId, "Incorrect customerId " + customerId); - Validator.validatePageLink(pageLink, "Incorrect page link " + pageLink); - ListenableFuture> dashboards = dashboardInfoDao.findDashboardsByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink); - - return Futures.transform(dashboards, new Function, TimePageData>() { - @Nullable - @Override - public TimePageData apply(@Nullable List dashboards) { - return new TimePageData<>(dashboards, pageLink); - } - }, MoreExecutors.directExecutor()); + Validator.validatePageLink(pageLink); + return dashboardInfoDao.findDashboardsByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink); } @Override @@ -250,19 +233,19 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb private PaginatedRemover tenantDashboardsRemover = new PaginatedRemover() { - @Override - protected List findEntities(TenantId tenantId, TenantId id, TextPageLink pageLink) { - return dashboardInfoDao.findDashboardsByTenantId(id.getId(), pageLink); - } - - @Override - protected void removeEntity(TenantId tenantId, DashboardInfo entity) { - deleteDashboard(tenantId, new DashboardId(entity.getUuidId())); - } - }; - - private class CustomerDashboardsUnassigner extends TimePaginatedRemover { + @Override + protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { + return dashboardInfoDao.findDashboardsByTenantId(id.getId(), pageLink); + } + @Override + protected void removeEntity(TenantId tenantId, DashboardInfo entity) { + deleteDashboard(tenantId, new DashboardId(entity.getUuidId())); + } + }; + + private class CustomerDashboardsUnassigner extends PaginatedRemover { + private Customer customer; CustomerDashboardsUnassigner(Customer customer) { @@ -270,13 +253,8 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb } @Override - protected List findEntities(TenantId tenantId, Customer customer, TimePageLink pageLink) { - try { - return dashboardInfoDao.findDashboardsByTenantIdAndCustomerId(customer.getTenantId().getId(), customer.getId().getId(), pageLink).get(); - } catch (InterruptedException | ExecutionException e) { - log.warn("Failed to get dashboards by tenantId [{}] and customerId [{}].", customer.getTenantId().getId(), customer.getId().getId()); - throw new RuntimeException(e); - } + protected PageData findEntities(TenantId tenantId, Customer customer, PageLink pageLink) { + return dashboardInfoDao.findDashboardsByTenantIdAndCustomerId(customer.getTenantId().getId(), customer.getId().getId(), pageLink); } @Override @@ -286,7 +264,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb } - private class CustomerDashboardsUpdater extends TimePaginatedRemover { + private class CustomerDashboardsUpdater extends PaginatedRemover { private Customer customer; @@ -295,13 +273,8 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb } @Override - protected List findEntities(TenantId tenantId, Customer customer, TimePageLink pageLink) { - try { - return dashboardInfoDao.findDashboardsByTenantIdAndCustomerId(customer.getTenantId().getId(), customer.getId().getId(), pageLink).get(); - } catch (InterruptedException | ExecutionException e) { - log.warn("Failed to get dashboards by tenantId [{}] and customerId [{}].", customer.getTenantId().getId(), customer.getId().getId()); - throw new RuntimeException(e); - } + protected PageData findEntities(TenantId tenantId, Customer customer, PageLink pageLink) { + return dashboardInfoDao.findDashboardsByTenantIdAndCustomerId(customer.getTenantId().getId(), customer.getId().getId(), pageLink); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceCredentialsDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceCredentialsDao.java deleted file mode 100644 index b9d9ca35ca..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceCredentialsDao.java +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.device; - -import com.datastax.driver.core.querybuilder.Select.Where; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.security.DeviceCredentials; -import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.nosql.DeviceCredentialsEntity; -import org.thingsboard.server.dao.nosql.CassandraAbstractModelDao; -import org.thingsboard.server.dao.util.NoSqlDao; - -import java.util.UUID; - -import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; -import static com.datastax.driver.core.querybuilder.QueryBuilder.select; - -@Component -@Slf4j -@NoSqlDao -public class CassandraDeviceCredentialsDao extends CassandraAbstractModelDao implements DeviceCredentialsDao { - - @Override - protected Class getColumnFamilyClass() { - return DeviceCredentialsEntity.class; - } - - @Override - protected String getColumnFamilyName() { - return ModelConstants.DEVICE_CREDENTIALS_COLUMN_FAMILY_NAME; - } - - @Override - public DeviceCredentials findByDeviceId(TenantId tenantId, UUID deviceId) { - log.debug("Try to find device credentials by deviceId [{}] ", deviceId); - Where query = select().from(ModelConstants.DEVICE_CREDENTIALS_BY_DEVICE_COLUMN_FAMILY_NAME) - .where(eq(ModelConstants.DEVICE_CREDENTIALS_DEVICE_ID_PROPERTY, deviceId)); - log.trace("Execute query {}", query); - DeviceCredentialsEntity deviceCredentialsEntity = findOneByStatement(tenantId, query); - log.trace("Found device credentials [{}] by deviceId [{}]", deviceCredentialsEntity, deviceId); - return DaoUtil.getData(deviceCredentialsEntity); - } - - @Override - public DeviceCredentials findByCredentialsId(TenantId tenantId, String credentialsId) { - log.debug("Try to find device credentials by credentialsId [{}] ", credentialsId); - Where query = select().from(ModelConstants.DEVICE_CREDENTIALS_BY_CREDENTIALS_ID_COLUMN_FAMILY_NAME) - .where(eq(ModelConstants.DEVICE_CREDENTIALS_CREDENTIALS_ID_PROPERTY, credentialsId)); - log.trace("Execute query {}", query); - DeviceCredentialsEntity deviceCredentialsEntity = findOneByStatement(tenantId, query); - log.trace("Found device credentials [{}] by credentialsId [{}]", deviceCredentialsEntity, credentialsId); - return DaoUtil.getData(deviceCredentialsEntity); - } -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceDao.java deleted file mode 100644 index b010bbfdc2..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceDao.java +++ /dev/null @@ -1,208 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.device; - -import com.datastax.driver.core.ResultSet; -import com.datastax.driver.core.ResultSetFuture; -import com.datastax.driver.core.Statement; -import com.datastax.driver.core.querybuilder.Select; -import com.datastax.driver.mapping.Result; -import com.google.common.base.Function; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.EntitySubtype; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; -import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.model.EntitySubtypeEntity; -import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.nosql.DeviceEntity; -import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTextDao; -import org.thingsboard.server.dao.util.NoSqlDao; - -import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; -import static com.datastax.driver.core.querybuilder.QueryBuilder.in; -import static com.datastax.driver.core.querybuilder.QueryBuilder.select; -import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_BY_CUSTOMER_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_BY_TENANT_AND_NAME_VIEW_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_CUSTOMER_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_NAME_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_TENANT_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_TYPE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_ENTITY_TYPE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_TENANT_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY; - -@Component -@Slf4j -@NoSqlDao -public class CassandraDeviceDao extends CassandraAbstractSearchTextDao implements DeviceDao { - - @Override - protected Class getColumnFamilyClass() { - return DeviceEntity.class; - } - - @Override - protected String getColumnFamilyName() { - return DEVICE_COLUMN_FAMILY_NAME; - } - - @Override - public Device save(TenantId tenantId, Device domain) { - Device savedDevice = super.save(tenantId, domain); - EntitySubtype entitySubtype = new EntitySubtype(savedDevice.getTenantId(), EntityType.DEVICE, savedDevice.getType()); - EntitySubtypeEntity entitySubtypeEntity = new EntitySubtypeEntity(entitySubtype); - Statement saveStatement = cluster.getMapper(EntitySubtypeEntity.class).saveQuery(entitySubtypeEntity); - executeWrite(tenantId, saveStatement); - return savedDevice; - } - - @Override - public List findDevicesByTenantId(UUID tenantId, TextPageLink pageLink) { - log.debug("Try to find devices by tenantId [{}] and pageLink [{}]", tenantId, pageLink); - List deviceEntities = findPageWithTextSearch(new TenantId(tenantId), DEVICE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME, - Collections.singletonList(eq(DEVICE_TENANT_ID_PROPERTY, tenantId)), pageLink); - - log.trace("Found devices [{}] by tenantId [{}] and pageLink [{}]", deviceEntities, tenantId, pageLink); - return DaoUtil.convertDataList(deviceEntities); - } - - @Override - public List findDevicesByTenantIdAndType(UUID tenantId, String type, TextPageLink pageLink) { - log.debug("Try to find devices by tenantId [{}], type [{}] and pageLink [{}]", tenantId, type, pageLink); - List deviceEntities = findPageWithTextSearch(new TenantId(tenantId), DEVICE_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME, - Arrays.asList(eq(DEVICE_TYPE_PROPERTY, type), - eq(DEVICE_TENANT_ID_PROPERTY, tenantId)), pageLink); - log.trace("Found devices [{}] by tenantId [{}], type [{}] and pageLink [{}]", deviceEntities, tenantId, type, pageLink); - return DaoUtil.convertDataList(deviceEntities); - } - - @Override - public ListenableFuture> findDevicesByTenantIdAndIdsAsync(UUID tenantId, List deviceIds) { - log.debug("Try to find devices by tenantId [{}] and device Ids [{}]", tenantId, deviceIds); - Select select = select().from(getColumnFamilyName()); - Select.Where query = select.where(); - query.and(eq(DEVICE_TENANT_ID_PROPERTY, tenantId)); - query.and(in(ID_PROPERTY, deviceIds)); - return findListByStatementAsync(new TenantId(tenantId), query); - } - - @Override - public List findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink) { - log.debug("Try to find devices by tenantId [{}], customerId[{}] and pageLink [{}]", tenantId, customerId, pageLink); - List deviceEntities = findPageWithTextSearch(new TenantId(tenantId), DEVICE_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME, - Arrays.asList(eq(DEVICE_CUSTOMER_ID_PROPERTY, customerId), - eq(DEVICE_TENANT_ID_PROPERTY, tenantId)), - pageLink); - - log.trace("Found devices [{}] by tenantId [{}], customerId [{}] and pageLink [{}]", deviceEntities, tenantId, customerId, pageLink); - return DaoUtil.convertDataList(deviceEntities); - } - - @Override - public List findDevicesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, TextPageLink pageLink) { - log.debug("Try to find devices by tenantId [{}], customerId [{}], type [{}] and pageLink [{}]", tenantId, customerId, type, pageLink); - List deviceEntities = findPageWithTextSearch(new TenantId(tenantId), DEVICE_BY_CUSTOMER_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME, - Arrays.asList(eq(DEVICE_TYPE_PROPERTY, type), - eq(DEVICE_CUSTOMER_ID_PROPERTY, customerId), - eq(DEVICE_TENANT_ID_PROPERTY, tenantId)), - pageLink); - - log.trace("Found devices [{}] by tenantId [{}], customerId [{}], type [{}] and pageLink [{}]", deviceEntities, tenantId, customerId, type, pageLink); - return DaoUtil.convertDataList(deviceEntities); - } - - @Override - public ListenableFuture> findDevicesByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List deviceIds) { - log.debug("Try to find devices by tenantId [{}], customerId [{}] and device Ids [{}]", tenantId, customerId, deviceIds); - Select select = select().from(getColumnFamilyName()); - Select.Where query = select.where(); - query.and(eq(DEVICE_TENANT_ID_PROPERTY, tenantId)); - query.and(eq(DEVICE_CUSTOMER_ID_PROPERTY, customerId)); - query.and(in(ID_PROPERTY, deviceIds)); - return findListByStatementAsync(new TenantId(tenantId), query); - } - - @Override - public Optional findDeviceByTenantIdAndName(UUID tenantId, String deviceName) { - Select select = select().from(DEVICE_BY_TENANT_AND_NAME_VIEW_NAME); - Select.Where query = select.where(); - query.and(eq(DEVICE_TENANT_ID_PROPERTY, tenantId)); - query.and(eq(DEVICE_NAME_PROPERTY, deviceName)); - return Optional.ofNullable(DaoUtil.getData(findOneByStatement(new TenantId(tenantId), query))); - } - - @Override - public ListenableFuture> findTenantDeviceTypesAsync(UUID tenantId) { - Select select = select().from(ENTITY_SUBTYPE_COLUMN_FAMILY_NAME); - Select.Where query = select.where(); - query.and(eq(ENTITY_SUBTYPE_TENANT_ID_PROPERTY, tenantId)); - query.and(eq(ENTITY_SUBTYPE_ENTITY_TYPE_PROPERTY, EntityType.DEVICE)); - query.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel()); - ResultSetFuture resultSetFuture = executeAsyncRead(new TenantId(tenantId), query); - return Futures.transform(resultSetFuture, new Function>() { - @Nullable - @Override - public List apply(@Nullable ResultSet resultSet) { - Result result = cluster.getMapper(EntitySubtypeEntity.class).map(resultSet); - if (result != null) { - List entitySubtypes = new ArrayList<>(); - result.all().forEach((entitySubtypeEntity) -> - entitySubtypes.add(entitySubtypeEntity.toEntitySubtype()) - ); - return entitySubtypes; - } else { - return Collections.emptyList(); - } - } - }, MoreExecutors.directExecutor()); - } - - @Override - public Device findDeviceByTenantIdAndId(TenantId tenantId, UUID id) { - Select.Where query = select().from(getColumnFamilyName()).where(eq(ModelConstants.TENANT_ID_PROPERTY, tenantId.getId())).and(eq(ModelConstants.ID_PROPERTY, id)); - log.trace("Execute query {}", query); - DeviceEntity entity = findOneByStatement(tenantId, query); - return DaoUtil.getData(entity); - } - - @Override - public ListenableFuture findDeviceByTenantIdAndIdAsync(TenantId tenantId, UUID id) { - Select.Where query = select().from(getColumnFamilyName()).where(eq(ModelConstants.TENANT_ID_PROPERTY, tenantId.getId())).and(eq(ModelConstants.ID_PROPERTY, id)); - log.trace("Execute query {}", query); - return findOneByStatementAsync(tenantId, query); - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java index 22d42bb63a..ff1763f662 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java @@ -21,18 +21,20 @@ import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.DeviceCredentials; -import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; import static org.thingsboard.server.common.data.CacheConstants.DEVICE_CREDENTIALS_CACHE; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -75,25 +77,56 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen } private DeviceCredentials saveOrUpdate(TenantId tenantId, DeviceCredentials deviceCredentials) { - if (deviceCredentials.getCredentialsType() == DeviceCredentialsType.X509_CERTIFICATE) { - formatCertData(deviceCredentials); + if(deviceCredentials.getCredentialsType() == null){ + throw new DataValidationException("Device credentials type should be specified"); + } + switch (deviceCredentials.getCredentialsType()) { + case X509_CERTIFICATE: + formatCertData(deviceCredentials); + break; + case MQTT_BASIC: + formatSimpleMqttCredentials(deviceCredentials); + break; } log.trace("Executing updateDeviceCredentials [{}]", deviceCredentials); credentialsValidator.validate(deviceCredentials, id -> tenantId); - if (!sqlDatabaseUsed) { + try { return deviceCredentialsDao.save(tenantId, deviceCredentials); - } else { - try { - return deviceCredentialsDao.save(tenantId, deviceCredentials); - } catch (Exception t) { - ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); - if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_credentials_id_unq_key")) { - throw new DataValidationException("Specified credentials are already registered!"); - } else { - throw t; - } + } catch (Exception t) { + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); + if (e != null && e.getConstraintName() != null + && (e.getConstraintName().equalsIgnoreCase("device_credentials_id_unq_key") || e.getConstraintName().equalsIgnoreCase("device_credentials_device_id_unq_key"))) { + throw new DataValidationException("Specified credentials are already registered!"); + } else { + throw t; + } + } + } + + private void formatSimpleMqttCredentials(DeviceCredentials deviceCredentials) { + BasicMqttCredentials mqttCredentials; + try { + mqttCredentials = JacksonUtil.fromString(deviceCredentials.getCredentialsValue(), BasicMqttCredentials.class); + if (mqttCredentials == null) { + throw new IllegalArgumentException(); } + } catch (IllegalArgumentException e) { + throw new DataValidationException("Invalid credentials body for simple mqtt credentials!"); + } + if (StringUtils.isEmpty(mqttCredentials.getClientId()) && StringUtils.isEmpty(mqttCredentials.getUserName())) { + throw new DataValidationException("Both mqtt client id and user name are empty!"); + } + if (StringUtils.isEmpty(mqttCredentials.getClientId())) { + deviceCredentials.setCredentialsId(mqttCredentials.getUserName()); + } else if (StringUtils.isEmpty(mqttCredentials.getUserName())) { + deviceCredentials.setCredentialsId(EncryptionUtil.getSha3Hash(mqttCredentials.getClientId())); + } else { + deviceCredentials.setCredentialsId(EncryptionUtil.getSha3Hash("|", mqttCredentials.getClientId(), mqttCredentials.getUserName())); + } + if (!StringUtils.isEmpty(mqttCredentials.getPassword())) { + mqttCredentials.setPassword(mqttCredentials.getPassword()); } + deviceCredentials.setCredentialsValue(JacksonUtil.toString(mqttCredentials)); } private void formatCertData(DeviceCredentials deviceCredentials) { @@ -115,25 +148,22 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen @Override protected void validateCreate(TenantId tenantId, DeviceCredentials deviceCredentials) { - if (!sqlDatabaseUsed) { - DeviceCredentials existingCredentialsEntity = deviceCredentialsDao.findByCredentialsId(tenantId, deviceCredentials.getCredentialsId()); - if (existingCredentialsEntity != null) { - throw new DataValidationException("Create of existent device credentials!"); - } + if (deviceCredentialsDao.findByDeviceId(tenantId, deviceCredentials.getDeviceId().getId()) != null) { + throw new DataValidationException("Credentials for this device are already specified!"); + } + if (deviceCredentialsDao.findByCredentialsId(tenantId, deviceCredentials.getCredentialsId()) != null) { + throw new DataValidationException("Device credentials are already assigned to another device!"); } } @Override protected void validateUpdate(TenantId tenantId, DeviceCredentials deviceCredentials) { - DeviceCredentials existingCredentials = deviceCredentialsDao.findById(tenantId, deviceCredentials.getUuidId()); - if (existingCredentials == null) { + if (deviceCredentialsDao.findById(tenantId, deviceCredentials.getUuidId()) == null) { throw new DataValidationException("Unable to update non-existent device credentials!"); } - if (!sqlDatabaseUsed) { - DeviceCredentials sameCredentialsId = deviceCredentialsDao.findByCredentialsId(tenantId, deviceCredentials.getCredentialsId()); - if (sameCredentialsId != null && !sameCredentialsId.getUuidId().equals(deviceCredentials.getUuidId())) { - throw new DataValidationException("Specified credentials are already registered!"); - } + DeviceCredentials existingCredentials = deviceCredentialsDao.findByCredentialsId(tenantId, deviceCredentials.getCredentialsId()); + if (existingCredentials != null && !existingCredentials.getId().equals(deviceCredentials.getId())) { + throw new DataValidationException("Device credentials are already assigned to another device!"); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java index 4d5cf09775..2f321ff499 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java @@ -17,9 +17,11 @@ package org.thingsboard.server.dao.device; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; import java.util.List; @@ -32,6 +34,15 @@ import java.util.UUID; */ public interface DeviceDao extends Dao { + /** + * Find device info by id. + * + * @param tenantId the tenant id + * @param deviceId the device id + * @return the device info object + */ + DeviceInfo findDeviceInfoById(TenantId tenantId, UUID deviceId); + /** * Save or update device object * @@ -47,7 +58,16 @@ public interface DeviceDao extends Dao { * @param pageLink the page link * @return the list of device objects */ - List findDevicesByTenantId(UUID tenantId, TextPageLink pageLink); + PageData findDevicesByTenantId(UUID tenantId, PageLink pageLink); + + /** + * Find device infos by tenantId and page link. + * + * @param tenantId the tenantId + * @param pageLink the page link + * @return the list of device info objects + */ + PageData findDeviceInfosByTenantId(UUID tenantId, PageLink pageLink); /** * Find devices by tenantId, type and page link. @@ -57,7 +77,27 @@ public interface DeviceDao extends Dao { * @param pageLink the page link * @return the list of device objects */ - List findDevicesByTenantIdAndType(UUID tenantId, String type, TextPageLink pageLink); + PageData findDevicesByTenantIdAndType(UUID tenantId, String type, PageLink pageLink); + + /** + * Find device infos by tenantId, type and page link. + * + * @param tenantId the tenantId + * @param type the type + * @param pageLink the page link + * @return the list of device info objects + */ + PageData findDeviceInfosByTenantIdAndType(UUID tenantId, String type, PageLink pageLink); + + /** + * Find device infos by tenantId, deviceProfileId and page link. + * + * @param tenantId the tenantId + * @param deviceProfileId the deviceProfileId + * @param pageLink the page link + * @return the list of device info objects + */ + PageData findDeviceInfosByTenantIdAndDeviceProfileId(UUID tenantId, UUID deviceProfileId, PageLink pageLink); /** * Find devices by tenantId and devices Ids. @@ -76,7 +116,17 @@ public interface DeviceDao extends Dao { * @param pageLink the page link * @return the list of device objects */ - List findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink); + PageData findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink); + + /** + * Find device infos by tenantId, customerId and page link. + * + * @param tenantId the tenantId + * @param customerId the customerId + * @param pageLink the page link + * @return the list of device info objects + */ + PageData findDeviceInfosByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink); /** * Find devices by tenantId, customerId, type and page link. @@ -87,8 +137,29 @@ public interface DeviceDao extends Dao { * @param pageLink the page link * @return the list of device objects */ - List findDevicesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, TextPageLink pageLink); + PageData findDevicesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink); + /** + * Find device infos by tenantId, customerId, type and page link. + * + * @param tenantId the tenantId + * @param customerId the customerId + * @param type the type + * @param pageLink the page link + * @return the list of device info objects + */ + PageData findDeviceInfosByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink); + + /** + * Find device infos by tenantId, customerId, deviceProfileId and page link. + * + * @param tenantId the tenantId + * @param customerId the customerId + * @param deviceProfileId the deviceProfileId + * @param pageLink the page link + * @return the list of device info objects + */ + PageData findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(UUID tenantId, UUID customerId, UUID deviceProfileId, PageLink pageLink); /** * Find devices by tenantId, customerId and devices Ids. @@ -132,4 +203,17 @@ public interface DeviceDao extends Dao { */ ListenableFuture findDeviceByTenantIdAndIdAsync(TenantId tenantId, UUID id); + Long countDevicesByTenantId(TenantId tenantId); + + Long countDevicesByDeviceProfileId(TenantId tenantId, UUID deviceProfileId); + + /** + * Find devices by tenantId, profileId and page link. + * + * @param tenantId the tenantId + * @param profileId the profileId + * @param pageLink the page link + * @return the list of device objects + */ + PageData findDevicesByTenantIdAndProfileId(UUID tenantId, UUID profileId, PageLink pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java new file mode 100644 index 0000000000..e571edda31 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.device; + +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileInfo; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.Dao; + +import java.util.UUID; + +public interface DeviceProfileDao extends Dao { + + DeviceProfileInfo findDeviceProfileInfoById(TenantId tenantId, UUID deviceProfileId); + + DeviceProfile save(TenantId tenantId, DeviceProfile deviceProfile); + + PageData findDeviceProfiles(TenantId tenantId, PageLink pageLink); + + PageData findDeviceProfileInfos(TenantId tenantId, PageLink pageLink, String transportType); + + DeviceProfile findDefaultDeviceProfile(TenantId tenantId); + + DeviceProfileInfo findDefaultDeviceProfileInfo(TenantId tenantId); + + DeviceProfile findByProvisionDeviceKey(String provisionDeviceKey); + + DeviceProfile findByName(TenantId tenantId, String profileName); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java new file mode 100644 index 0000000000..f3857cacd5 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -0,0 +1,353 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.device; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.hibernate.exception.ConstraintViolationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileInfo; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.service.PaginatedRemover; +import org.thingsboard.server.dao.service.Validator; +import org.thingsboard.server.dao.tenant.TenantDao; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.thingsboard.server.common.data.CacheConstants.DEVICE_PROFILE_CACHE; +import static org.thingsboard.server.dao.service.Validator.validateId; + +@Service +@Slf4j +public class DeviceProfileServiceImpl extends AbstractEntityService implements DeviceProfileService { + + private static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; + private static final String INCORRECT_DEVICE_PROFILE_ID = "Incorrect deviceProfileId "; + private static final String INCORRECT_DEVICE_PROFILE_NAME = "Incorrect deviceProfileName "; + + @Autowired + private DeviceProfileDao deviceProfileDao; + + @Autowired + private DeviceDao deviceDao; + + @Autowired + private DeviceService deviceService; + + @Autowired + private TenantDao tenantDao; + + @Autowired + private CacheManager cacheManager; + + @Cacheable(cacheNames = DEVICE_PROFILE_CACHE, key = "{#deviceProfileId.id}") + @Override + public DeviceProfile findDeviceProfileById(TenantId tenantId, DeviceProfileId deviceProfileId) { + log.trace("Executing findDeviceProfileById [{}]", deviceProfileId); + Validator.validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId); + return deviceProfileDao.findById(tenantId, deviceProfileId.getId()); + } + + @Override + public DeviceProfile findDeviceProfileByName(TenantId tenantId, String profileName) { + log.trace("Executing findDeviceProfileByName [{}][{}]", tenantId, profileName); + Validator.validateString(profileName, INCORRECT_DEVICE_PROFILE_NAME + profileName); + return deviceProfileDao.findByName(tenantId, profileName); + } + + @Cacheable(cacheNames = DEVICE_PROFILE_CACHE, key = "{'info', #deviceProfileId.id}") + @Override + public DeviceProfileInfo findDeviceProfileInfoById(TenantId tenantId, DeviceProfileId deviceProfileId) { + log.trace("Executing findDeviceProfileById [{}]", deviceProfileId); + Validator.validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId); + return deviceProfileDao.findDeviceProfileInfoById(tenantId, deviceProfileId.getId()); + } + + @Override + public DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile) { + log.trace("Executing saveDeviceProfile [{}]", deviceProfile); + deviceProfileValidator.validate(deviceProfile, DeviceProfile::getTenantId); + DeviceProfile oldDeviceProfile = null; + if (deviceProfile.getId() != null) { + oldDeviceProfile = deviceProfileDao.findById(deviceProfile.getTenantId(), deviceProfile.getId().getId()); + } + DeviceProfile savedDeviceProfile; + try { + savedDeviceProfile = deviceProfileDao.save(deviceProfile.getTenantId(), deviceProfile); + } catch (Exception t) { + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_profile_name_unq_key")) { + throw new DataValidationException("Device profile with such name already exists!"); + } else if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_provision_key_unq_key")) { + throw new DataValidationException("Device profile with such provision device key already exists!"); + } else { + throw t; + } + } + Cache cache = cacheManager.getCache(DEVICE_PROFILE_CACHE); + cache.evict(Collections.singletonList(savedDeviceProfile.getId().getId())); + cache.evict(Arrays.asList("info", savedDeviceProfile.getId().getId())); + cache.evict(Arrays.asList(deviceProfile.getTenantId().getId(), deviceProfile.getName())); + if (savedDeviceProfile.isDefault()) { + cache.evict(Arrays.asList("default", savedDeviceProfile.getTenantId().getId())); + cache.evict(Arrays.asList("default", "info", savedDeviceProfile.getTenantId().getId())); + } + if (oldDeviceProfile != null && !oldDeviceProfile.getName().equals(deviceProfile.getName())) { + PageLink pageLink = new PageLink(100); + PageData pageData; + do { + pageData = deviceDao.findDevicesByTenantIdAndProfileId(deviceProfile.getTenantId().getId(), deviceProfile.getUuidId(), pageLink); + for (Device device : pageData.getData()) { + device.setType(deviceProfile.getName()); + deviceService.saveDevice(device); + } + pageLink = pageLink.nextPageLink(); + } while (pageData.hasNext()); + } + return savedDeviceProfile; + } + + @Override + public void deleteDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId) { + log.trace("Executing deleteDeviceProfile [{}]", deviceProfileId); + Validator.validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId); + DeviceProfile deviceProfile = deviceProfileDao.findById(tenantId, deviceProfileId.getId()); + if (deviceProfile != null && deviceProfile.isDefault()) { + throw new DataValidationException("Deletion of Default Device Profile is prohibited!"); + } + this.removeDeviceProfile(tenantId, deviceProfile); + } + + private void removeDeviceProfile(TenantId tenantId, DeviceProfile deviceProfile) { + DeviceProfileId deviceProfileId = deviceProfile.getId(); + try { + deviceProfileDao.removeById(tenantId, deviceProfileId.getId()); + } catch (Exception t) { + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_device_profile")) { + throw new DataValidationException("The device profile referenced by the devices cannot be deleted!"); + } else { + throw t; + } + } + deleteEntityRelations(tenantId, deviceProfileId); + Cache cache = cacheManager.getCache(DEVICE_PROFILE_CACHE); + cache.evict(Collections.singletonList(deviceProfileId.getId())); + cache.evict(Arrays.asList("info", deviceProfileId.getId())); + cache.evict(Arrays.asList(tenantId.getId(), deviceProfile.getName())); + } + + @Override + public PageData findDeviceProfiles(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findDeviceProfiles tenantId [{}], pageLink [{}]", tenantId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + Validator.validatePageLink(pageLink); + return deviceProfileDao.findDeviceProfiles(tenantId, pageLink); + } + + @Override + public PageData findDeviceProfileInfos(TenantId tenantId, PageLink pageLink, String transportType) { + log.trace("Executing findDeviceProfileInfos tenantId [{}], pageLink [{}]", tenantId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + Validator.validatePageLink(pageLink); + return deviceProfileDao.findDeviceProfileInfos(tenantId, pageLink, transportType); + } + + @Cacheable(cacheNames = DEVICE_PROFILE_CACHE, key = "{#tenantId.id, #name}") + @Override + public DeviceProfile findOrCreateDeviceProfile(TenantId tenantId, String name) { + log.trace("Executing findOrCreateDefaultDeviceProfile"); + DeviceProfile deviceProfile = findDeviceProfileByName(tenantId, name); + if (deviceProfile == null) { + deviceProfile = this.doCreateDefaultDeviceProfile(tenantId, name, name.equals("default")); + } + return deviceProfile; + } + + @Override + public DeviceProfile createDefaultDeviceProfile(TenantId tenantId) { + log.trace("Executing createDefaultDeviceProfile tenantId [{}]", tenantId); + return doCreateDefaultDeviceProfile(tenantId, "default", true); + } + + private DeviceProfile doCreateDefaultDeviceProfile(TenantId tenantId, String profileName, boolean defaultProfile) { + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + DeviceProfile deviceProfile = new DeviceProfile(); + deviceProfile.setTenantId(tenantId); + deviceProfile.setDefault(defaultProfile); + deviceProfile.setName(profileName); + deviceProfile.setType(DeviceProfileType.DEFAULT); + deviceProfile.setTransportType(DeviceTransportType.DEFAULT); + deviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED); + deviceProfile.setDescription("Default device profile"); + DeviceProfileData deviceProfileData = new DeviceProfileData(); + DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration(); + DefaultDeviceProfileTransportConfiguration transportConfiguration = new DefaultDeviceProfileTransportConfiguration(); + DisabledDeviceProfileProvisionConfiguration provisionConfiguration = new DisabledDeviceProfileProvisionConfiguration(null); + deviceProfileData.setConfiguration(configuration); + deviceProfileData.setTransportConfiguration(transportConfiguration); + deviceProfileData.setProvisionConfiguration(provisionConfiguration); + deviceProfile.setProfileData(deviceProfileData); + return saveDeviceProfile(deviceProfile); + } + + @Cacheable(cacheNames = DEVICE_PROFILE_CACHE, key = "{'default', #tenantId.id}") + @Override + public DeviceProfile findDefaultDeviceProfile(TenantId tenantId) { + log.trace("Executing findDefaultDeviceProfile tenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + return deviceProfileDao.findDefaultDeviceProfile(tenantId); + } + + @Cacheable(cacheNames = DEVICE_PROFILE_CACHE, key = "{'default', 'info', #tenantId.id}") + @Override + public DeviceProfileInfo findDefaultDeviceProfileInfo(TenantId tenantId) { + log.trace("Executing findDefaultDeviceProfileInfo tenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + return deviceProfileDao.findDefaultDeviceProfileInfo(tenantId); + } + + @Override + public boolean setDefaultDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId) { + log.trace("Executing setDefaultDeviceProfile [{}]", deviceProfileId); + Validator.validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId); + DeviceProfile deviceProfile = deviceProfileDao.findById(tenantId, deviceProfileId.getId()); + if (!deviceProfile.isDefault()) { + Cache cache = cacheManager.getCache(DEVICE_PROFILE_CACHE); + deviceProfile.setDefault(true); + DeviceProfile previousDefaultDeviceProfile = findDefaultDeviceProfile(tenantId); + boolean changed = false; + if (previousDefaultDeviceProfile == null) { + deviceProfileDao.save(tenantId, deviceProfile); + changed = true; + } else if (!previousDefaultDeviceProfile.getId().equals(deviceProfile.getId())) { + previousDefaultDeviceProfile.setDefault(false); + deviceProfileDao.save(tenantId, previousDefaultDeviceProfile); + deviceProfileDao.save(tenantId, deviceProfile); + cache.evict(Collections.singletonList(previousDefaultDeviceProfile.getId().getId())); + cache.evict(Arrays.asList("info", previousDefaultDeviceProfile.getId().getId())); + cache.evict(Arrays.asList(tenantId.getId(), previousDefaultDeviceProfile.getName())); + changed = true; + } + if (changed) { + cache.evict(Collections.singletonList(deviceProfile.getId().getId())); + cache.evict(Arrays.asList("info", deviceProfile.getId().getId())); + cache.evict(Arrays.asList("default", tenantId.getId())); + cache.evict(Arrays.asList("default", "info", tenantId.getId())); + cache.evict(Arrays.asList(tenantId.getId(), deviceProfile.getName())); + } + return changed; + } + return false; + } + + @Override + public void deleteDeviceProfilesByTenantId(TenantId tenantId) { + log.trace("Executing deleteDeviceProfilesByTenantId, tenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + tenantDeviceProfilesRemover.removeEntities(tenantId, tenantId); + } + + private DataValidator deviceProfileValidator = + new DataValidator() { + @Override + protected void validateDataImpl(TenantId tenantId, DeviceProfile deviceProfile) { + if (StringUtils.isEmpty(deviceProfile.getName())) { + throw new DataValidationException("Device profile name should be specified!"); + } + if (deviceProfile.getType() == null) { + throw new DataValidationException("Device profile type should be specified!"); + } + if (deviceProfile.getTransportType() == null) { + throw new DataValidationException("Device profile transport type should be specified!"); + } + if (deviceProfile.getTenantId() == null) { + throw new DataValidationException("Device profile should be assigned to tenant!"); + } else { + Tenant tenant = tenantDao.findById(deviceProfile.getTenantId(), deviceProfile.getTenantId().getId()); + if (tenant == null) { + throw new DataValidationException("Device profile is referencing to non-existent tenant!"); + } + } + if (deviceProfile.isDefault()) { + DeviceProfile defaultDeviceProfile = findDefaultDeviceProfile(tenantId); + if (defaultDeviceProfile != null && !defaultDeviceProfile.getId().equals(deviceProfile.getId())) { + throw new DataValidationException("Another default device profile is present in scope of current tenant!"); + } + } + } + + @Override + protected void validateUpdate(TenantId tenantId, DeviceProfile deviceProfile) { + DeviceProfile old = deviceProfileDao.findById(deviceProfile.getTenantId(), deviceProfile.getId().getId()); + if (old == null) { + throw new DataValidationException("Can't update non existing device profile!"); + } + boolean profileTypeChanged = !old.getType().equals(deviceProfile.getType()); + boolean transportTypeChanged = !old.getTransportType().equals(deviceProfile.getTransportType()); + if (profileTypeChanged || transportTypeChanged) { + Long profileDeviceCount = deviceDao.countDevicesByDeviceProfileId(deviceProfile.getTenantId(), deviceProfile.getId().getId()); + if (profileDeviceCount > 0) { + String message = null; + if (profileTypeChanged) { + message = "Can't change device profile type because devices referenced it!"; + } else if (transportTypeChanged) { + message = "Can't change device profile transport type because devices referenced it!"; + } + throw new DataValidationException(message); + } + } + } + }; + + private PaginatedRemover tenantDeviceProfilesRemover = + new PaginatedRemover() { + + @Override + protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { + return deviceProfileDao.findDeviceProfiles(id, pageLink); + } + + @Override + protected void removeEntity(TenantId tenantId, DeviceProfile entity) { + removeDeviceProfile(tenantId, entity); + } + }; + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index 8d29b9c447..24c597b4df 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -27,35 +27,51 @@ import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceInfo; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.device.DeviceSearchQuery; +import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; +import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; +import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; +import org.thingsboard.server.common.data.device.data.DeviceData; +import org.thingsboard.server.common.data.device.data.Lwm2mDeviceTransportConfiguration; +import org.thingsboard.server.common.data.device.data.MqttDeviceTransportConfiguration; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.customer.CustomerDao; +import org.thingsboard.server.dao.device.provision.ProvisionFailedException; +import org.thingsboard.server.dao.device.provision.ProvisionRequest; +import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantDao; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; import javax.annotation.Nullable; import java.util.ArrayList; @@ -79,6 +95,7 @@ import static org.thingsboard.server.dao.service.Validator.validateString; public class DeviceServiceImpl extends AbstractEntityService implements DeviceService { public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; + public static final String INCORRECT_DEVICE_PROFILE_ID = "Incorrect deviceProfileId "; public static final String INCORRECT_PAGE_LINK = "Incorrect page link "; public static final String INCORRECT_CUSTOMER_ID = "Incorrect customerId "; public static final String INCORRECT_DEVICE_ID = "Incorrect deviceId "; @@ -94,6 +111,9 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe @Autowired private DeviceCredentialsService deviceCredentialsService; + @Autowired + private DeviceProfileService deviceProfileService; + @Autowired private EntityViewService entityViewService; @@ -103,6 +123,17 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe @Autowired private EventService eventService; + @Autowired + @Lazy + private TbTenantProfileCache tenantProfileCache; + + @Override + public DeviceInfo findDeviceInfoById(TenantId tenantId, DeviceId deviceId) { + log.trace("Executing findDeviceInfoById [{}]", deviceId); + validateId(deviceId, INCORRECT_DEVICE_ID + deviceId); + return deviceDao.findDeviceInfoById(tenantId, deviceId.getId()); + } + @Override public Device findDeviceById(TenantId tenantId, DeviceId deviceId) { log.trace("Executing findDeviceById [{}]", deviceId); @@ -150,18 +181,31 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe log.trace("Executing saveDevice [{}]", device); deviceValidator.validate(device, Device::getTenantId); Device savedDevice; - if (!sqlDatabaseUsed) { - savedDevice = deviceDao.save(device.getTenantId(), device); - } else { - try { - savedDevice = deviceDao.save(device.getTenantId(), device); - } catch (Exception t) { - ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); - if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_name_unq_key")) { - throw new DataValidationException("Device with such name already exists!"); + try { + DeviceProfile deviceProfile; + if (device.getDeviceProfileId() == null) { + if (!StringUtils.isEmpty(device.getType())) { + deviceProfile = this.deviceProfileService.findOrCreateDeviceProfile(device.getTenantId(), device.getType()); } else { - throw t; + deviceProfile = this.deviceProfileService.findDefaultDeviceProfile(device.getTenantId()); } + device.setDeviceProfileId(new DeviceProfileId(deviceProfile.getId().getId())); + } else { + deviceProfile = this.deviceProfileService.findDeviceProfileById(device.getTenantId(), device.getDeviceProfileId()); + if (deviceProfile == null) { + throw new DataValidationException("Device is referencing non existing device profile!"); + } + } + device.setType(deviceProfile.getName()); + device.setDeviceData(syncDeviceData(deviceProfile, device.getDeviceData())); + + savedDevice = deviceDao.save(device.getTenantId(), device); + } catch (Exception t) { + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_name_unq_key")) { + throw new DataValidationException("Device with such name already exists!"); + } else { + throw t; } } if (device.getId() == null) { @@ -174,6 +218,33 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return savedDevice; } + private DeviceData syncDeviceData(DeviceProfile deviceProfile, DeviceData deviceData) { + if (deviceData == null) { + deviceData = new DeviceData(); + } + if (deviceData.getConfiguration() == null || !deviceProfile.getType().equals(deviceData.getConfiguration().getType())) { + switch (deviceProfile.getType()) { + case DEFAULT: + deviceData.setConfiguration(new DefaultDeviceConfiguration()); + break; + } + } + if (deviceData.getTransportConfiguration() == null || !deviceProfile.getTransportType().equals(deviceData.getTransportConfiguration().getType())) { + switch (deviceProfile.getTransportType()) { + case DEFAULT: + deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); + break; + case MQTT: + deviceData.setTransportConfiguration(new MqttDeviceTransportConfiguration()); + break; + case LWM2M: + deviceData.setTransportConfiguration(new Lwm2mDeviceTransportConfiguration()); + break; + } + } + return deviceData; + } + @Override public Device assignDeviceToCustomer(TenantId tenantId, DeviceId deviceId, CustomerId customerId) { Device device = findDeviceById(tenantId, deviceId); @@ -220,22 +291,46 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe } @Override - public TextPageData findDevicesByTenantId(TenantId tenantId, TextPageLink pageLink) { + public PageData findDevicesByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findDevicesByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validatePageLink(pageLink, INCORRECT_PAGE_LINK + pageLink); - List devices = deviceDao.findDevicesByTenantId(tenantId.getId(), pageLink); - return new TextPageData<>(devices, pageLink); + validatePageLink(pageLink); + return deviceDao.findDevicesByTenantId(tenantId.getId(), pageLink); + } + + @Override + public PageData findDeviceInfosByTenantId(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findDeviceInfosByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validatePageLink(pageLink); + return deviceDao.findDeviceInfosByTenantId(tenantId.getId(), pageLink); } @Override - public TextPageData findDevicesByTenantIdAndType(TenantId tenantId, String type, TextPageLink pageLink) { + public PageData findDevicesByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink) { log.trace("Executing findDevicesByTenantIdAndType, tenantId [{}], type [{}], pageLink [{}]", tenantId, type, pageLink); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); validateString(type, "Incorrect type " + type); - validatePageLink(pageLink, INCORRECT_PAGE_LINK + pageLink); - List devices = deviceDao.findDevicesByTenantIdAndType(tenantId.getId(), type, pageLink); - return new TextPageData<>(devices, pageLink); + validatePageLink(pageLink); + return deviceDao.findDevicesByTenantIdAndType(tenantId.getId(), type, pageLink); + } + + @Override + public PageData findDeviceInfosByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink) { + log.trace("Executing findDeviceInfosByTenantIdAndType, tenantId [{}], type [{}], pageLink [{}]", tenantId, type, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateString(type, "Incorrect type " + type); + validatePageLink(pageLink); + return deviceDao.findDeviceInfosByTenantIdAndType(tenantId.getId(), type, pageLink); + } + + @Override + public PageData findDeviceInfosByTenantIdAndDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId, PageLink pageLink) { + log.trace("Executing findDeviceInfosByTenantIdAndDeviceProfileId, tenantId [{}], deviceProfileId [{}], pageLink [{}]", tenantId, deviceProfileId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId); + validatePageLink(pageLink); + return deviceDao.findDeviceInfosByTenantIdAndDeviceProfileId(tenantId.getId(), deviceProfileId.getId(), pageLink); } @Override @@ -255,24 +350,51 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe } @Override - public TextPageData findDevicesByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TextPageLink pageLink) { + public PageData findDevicesByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) { log.trace("Executing findDevicesByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); - validatePageLink(pageLink, INCORRECT_PAGE_LINK + pageLink); - List devices = deviceDao.findDevicesByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink); - return new TextPageData<>(devices, pageLink); + validatePageLink(pageLink); + return deviceDao.findDevicesByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink); + } + + @Override + public PageData findDeviceInfosByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) { + log.trace("Executing findDeviceInfosByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validatePageLink(pageLink); + return deviceDao.findDeviceInfosByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink); } @Override - public TextPageData findDevicesByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, TextPageLink pageLink) { + public PageData findDevicesByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink) { log.trace("Executing findDevicesByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}], type [{}], pageLink [{}]", tenantId, customerId, type, pageLink); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); validateString(type, "Incorrect type " + type); - validatePageLink(pageLink, INCORRECT_PAGE_LINK + pageLink); - List devices = deviceDao.findDevicesByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink); - return new TextPageData<>(devices, pageLink); + validatePageLink(pageLink); + return deviceDao.findDevicesByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink); + } + + @Override + public PageData findDeviceInfosByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink) { + log.trace("Executing findDeviceInfosByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}], type [{}], pageLink [{}]", tenantId, customerId, type, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validateString(type, "Incorrect type " + type); + validatePageLink(pageLink); + return deviceDao.findDeviceInfosByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink); + } + + @Override + public PageData findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, PageLink pageLink) { + log.trace("Executing findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId, tenantId [{}], customerId [{}], deviceProfileId [{}], pageLink [{}]", tenantId, customerId, deviceProfileId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId); + validatePageLink(pageLink); + return deviceDao.findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(tenantId.getId(), customerId.getId(), deviceProfileId.getId(), pageLink); } @Override @@ -356,38 +478,76 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return doSaveDevice(device, null); } + @Override + @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#profile.tenantId, #provisionRequest.deviceName}") + @Transactional + public Device saveDevice(ProvisionRequest provisionRequest, DeviceProfile profile) { + Device device = new Device(); + device.setName(provisionRequest.getDeviceName()); + device.setType(profile.getName()); + device.setTenantId(profile.getTenantId()); + Device savedDevice = saveDevice(device); + if (!StringUtils.isEmpty(provisionRequest.getCredentialsData().getToken()) || + !StringUtils.isEmpty(provisionRequest.getCredentialsData().getX509CertHash()) || + !StringUtils.isEmpty(provisionRequest.getCredentialsData().getUsername()) || + !StringUtils.isEmpty(provisionRequest.getCredentialsData().getPassword()) || + !StringUtils.isEmpty(provisionRequest.getCredentialsData().getClientId())) { + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedDevice.getTenantId(), savedDevice.getId()); + if (deviceCredentials == null) { + deviceCredentials = new DeviceCredentials(); + } + deviceCredentials.setDeviceId(savedDevice.getId()); + deviceCredentials.setCredentialsType(provisionRequest.getCredentialsType()); + switch (provisionRequest.getCredentialsType()) { + case ACCESS_TOKEN: + deviceCredentials.setCredentialsId(provisionRequest.getCredentialsData().getToken()); + break; + case MQTT_BASIC: + BasicMqttCredentials mqttCredentials = new BasicMqttCredentials(); + mqttCredentials.setClientId(provisionRequest.getCredentialsData().getClientId()); + mqttCredentials.setUserName(provisionRequest.getCredentialsData().getUsername()); + mqttCredentials.setPassword(provisionRequest.getCredentialsData().getPassword()); + deviceCredentials.setCredentialsValue(JacksonUtil.toString(mqttCredentials)); + break; + case X509_CERTIFICATE: + deviceCredentials.setCredentialsValue(provisionRequest.getCredentialsData().getX509CertHash()); + break; + } + try { + deviceCredentialsService.updateDeviceCredentials(savedDevice.getTenantId(), deviceCredentials); + } catch (Exception e) { + throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); + } + } + return savedDevice; + } + private DataValidator deviceValidator = new DataValidator() { @Override protected void validateCreate(TenantId tenantId, Device device) { - if (!sqlDatabaseUsed) { - deviceDao.findDeviceByTenantIdAndName(device.getTenantId().getId(), device.getName()).ifPresent( - d -> { - throw new DataValidationException("Device with such name already exists!"); - } - ); + DefaultTenantProfileConfiguration profileConfiguration = + (DefaultTenantProfileConfiguration)tenantProfileCache.get(tenantId).getProfileData().getConfiguration(); + long maxDevices = profileConfiguration.getMaxDevices(); + if (maxDevices > 0) { + long currentDevicesCount = deviceDao.countDevicesByTenantId(tenantId); + if (maxDevices >= currentDevicesCount) { + throw new DataValidationException("Can't create devices more then " + maxDevices); + } } } @Override protected void validateUpdate(TenantId tenantId, Device device) { - if (!sqlDatabaseUsed) { - deviceDao.findDeviceByTenantIdAndName(device.getTenantId().getId(), device.getName()).ifPresent( - d -> { - if (!d.getUuidId().equals(device.getUuidId())) { - throw new DataValidationException("Device with such name already exists!"); - } - } - ); + Device old = deviceDao.findById(device.getTenantId(), device.getId().getId()); + if (old == null) { + throw new DataValidationException("Can't update non existing device!"); } } @Override protected void validateDataImpl(TenantId tenantId, Device device) { - if (StringUtils.isEmpty(device.getType())) { - throw new DataValidationException("Device type should be specified!"); - } if (StringUtils.isEmpty(device.getName()) || device.getName().trim().length() == 0) { throw new DataValidationException("Device name should be specified!"); } @@ -417,7 +577,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe new PaginatedRemover() { @Override - protected List findEntities(TenantId tenantId, TenantId id, TextPageLink pageLink) { + protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { return deviceDao.findDevicesByTenantId(id.getId(), pageLink); } @@ -430,7 +590,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe private PaginatedRemover customerDeviceUnasigner = new PaginatedRemover() { @Override - protected List findEntities(TenantId tenantId, CustomerId id, TextPageLink pageLink) { + protected PageData findEntities(TenantId tenantId, CustomerId id, PageLink pageLink) { return deviceDao.findDevicesByTenantIdAndCustomerId(tenantId.getId(), id.getId(), pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java index 5c7f6c88e2..a0f03b2a32 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java @@ -18,12 +18,10 @@ package org.thingsboard.server.dao.entity; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.relation.RelationService; -import javax.annotation.PostConstruct; import java.util.Optional; @Slf4j @@ -32,16 +30,6 @@ public abstract class AbstractEntityService { @Autowired protected RelationService relationService; - @Value("${database.entities.type:sql}") - private String databaseType; - - protected boolean sqlDatabaseUsed; - - @PostConstruct - public void init() { - sqlDatabaseUsed = "sql".equalsIgnoreCase(databaseType); - } - protected void deleteEntityRelations(TenantId tenantId, EntityId entityId) { log.trace("Executing deleteEntityRelations [{}]", entityId); relationService.deleteEntityRelations(tenantId, entityId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index 7080722056..5ff9d2f7c1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -33,16 +33,24 @@ import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.user.UserService; +import static org.thingsboard.server.dao.service.Validator.validateId; + /** * Created by ashvayka on 04.05.17. */ @@ -50,6 +58,9 @@ import org.thingsboard.server.dao.user.UserService; @Slf4j public class BaseEntityService extends AbstractEntityService implements EntityService { + public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; + public static final String INCORRECT_CUSTOMER_ID = "Incorrect customerId "; + @Autowired private AssetService assetService; @@ -77,11 +88,33 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe @Autowired private RuleChainService ruleChainService; + @Autowired + private EntityQueryDao entityQueryDao; + @Override public void deleteEntityRelations(TenantId tenantId, EntityId entityId) { super.deleteEntityRelations(tenantId, entityId); } + @Override + public long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query) { + log.trace("Executing countEntitiesByQuery, tenantId [{}], customerId [{}], query [{}]", tenantId, customerId, query); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validateEntityCountQuery(query); + return this.entityQueryDao.countEntitiesByQuery(tenantId, customerId, query); + } + + @Override + public PageData findEntityDataByQuery(TenantId tenantId, CustomerId customerId, EntityDataQuery query) { + log.trace("Executing findEntityDataByQuery, tenantId [{}], customerId [{}], query [{}]", tenantId, customerId, query); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validateEntityDataQuery(query); + return this.entityQueryDao.findEntityDataByQuery(tenantId, customerId, query); + } + + //TODO: 3.1 Remove this from project. @Override public ListenableFuture fetchEntityNameAsync(TenantId tenantId, EntityId entityId) { log.trace("Executing fetchEntityNameAsync [{}]", entityId); @@ -122,4 +155,29 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe return entityName; } + private static void validateEntityCountQuery(EntityCountQuery query) { + if (query == null) { + throw new IncorrectParameterException("Query must be specified."); + } else if (query.getEntityFilter() == null) { + throw new IncorrectParameterException("Query entity filter must be specified."); + } else if (query.getEntityFilter().getType() == null) { + throw new IncorrectParameterException("Query entity filter type must be specified."); + } + } + + private static void validateEntityDataQuery(EntityDataQuery query) { + validateEntityCountQuery(query); + validateEntityDataPageLink(query.getPageLink()); + } + + private static void validateEntityDataPageLink(EntityDataPageLink pageLink) { + if (pageLink == null) { + throw new IncorrectParameterException("Entity Data Page link must be specified."); + } else if (pageLink.getPageSize() < 1) { + throw new IncorrectParameterException("Incorrect entity data page link page size '"+pageLink.getPageSize()+"'. Page size must be greater than zero."); + } else if (pageLink.getPage() < 0) { + throw new IncorrectParameterException("Incorrect entity data page link page '"+pageLink.getPage()+"'. Page must be positive integer."); + } + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/EntityQueryDao.java b/dao/src/main/java/org/thingsboard/server/dao/entity/EntityQueryDao.java new file mode 100644 index 0000000000..c7f9883bfd --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/EntityQueryDao.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.entity; + +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityDataQuery; + +public interface EntityQueryDao { + + long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query); + + PageData findEntityDataByQuery(TenantId tenantId, CustomerId customerId, EntityDataQuery query); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/CassandraEntityViewDao.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/CassandraEntityViewDao.java deleted file mode 100644 index 05fac418d2..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/CassandraEntityViewDao.java +++ /dev/null @@ -1,187 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.entityview; - -import com.datastax.driver.core.ResultSet; -import com.datastax.driver.core.ResultSetFuture; -import com.datastax.driver.core.Statement; -import com.datastax.driver.core.querybuilder.Select; -import com.datastax.driver.mapping.Result; -import com.google.common.base.Function; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.EntitySubtype; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.EntityView; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; -import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.model.EntitySubtypeEntity; -import org.thingsboard.server.dao.model.nosql.EntityViewEntity; -import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTextDao; -import org.thingsboard.server.dao.util.NoSqlDao; - -import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; -import static com.datastax.driver.core.querybuilder.QueryBuilder.select; -import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_TYPE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_ID_COLUMN; -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_ENTITY_TYPE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_TENANT_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_BY_TENANT_AND_CUSTOMER_AND_TYPE_CF; -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_BY_TENANT_AND_CUSTOMER_CF; -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_BY_TENANT_AND_ENTITY_ID_CF; -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_BY_TENANT_AND_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_BY_TENANT_AND_SEARCH_TEXT_CF; -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_CF; -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_NAME_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_TABLE_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_TENANT_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_TYPE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.TENANT_ID_PROPERTY; - -/** - * Created by Victor Basanets on 9/06/2017. - */ -@Component -@Slf4j -@NoSqlDao -public class CassandraEntityViewDao extends CassandraAbstractSearchTextDao implements EntityViewDao { - - @Override - protected Class getColumnFamilyClass() { - return EntityViewEntity.class; - } - - @Override - protected String getColumnFamilyName() { - return ENTITY_VIEW_TABLE_FAMILY_NAME; - } - - @Override - public EntityView save(TenantId tenantId, EntityView domain) { - EntityView savedEntityView = super.save(domain.getTenantId(), domain); - EntitySubtype entitySubtype = new EntitySubtype(savedEntityView.getTenantId(), EntityType.ENTITY_VIEW, savedEntityView.getType()); - EntitySubtypeEntity entitySubtypeEntity = new EntitySubtypeEntity(entitySubtype); - Statement saveStatement = cluster.getMapper(EntitySubtypeEntity.class).saveQuery(entitySubtypeEntity); - executeWrite(tenantId, saveStatement); - return savedEntityView; - } - - @Override - public List findEntityViewsByTenantId(UUID tenantId, TextPageLink pageLink) { - log.debug("Try to find entity views by tenantId [{}] and pageLink [{}]", tenantId, pageLink); - List entityViewEntities = - findPageWithTextSearch(new TenantId(tenantId), ENTITY_VIEW_BY_TENANT_AND_SEARCH_TEXT_CF, - Collections.singletonList(eq(TENANT_ID_PROPERTY, tenantId)), pageLink); - log.trace("Found entity views [{}] by tenantId [{}] and pageLink [{}]", - entityViewEntities, tenantId, pageLink); - return DaoUtil.convertDataList(entityViewEntities); - } - - @Override - public List findEntityViewsByTenantIdAndType(UUID tenantId, String type, TextPageLink pageLink) { - log.debug("Try to find entity views by tenantId [{}], type [{}] and pageLink [{}]", tenantId, type, pageLink); - List entityViewEntities = - findPageWithTextSearch(new TenantId(tenantId), ENTITY_VIEW_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_CF, - Arrays.asList(eq(ENTITY_VIEW_TYPE_PROPERTY, type), - eq(TENANT_ID_PROPERTY, tenantId)), pageLink); - log.trace("Found entity views [{}] by tenantId [{}], type [{}] and pageLink [{}]", - entityViewEntities, tenantId, type, pageLink); - return DaoUtil.convertDataList(entityViewEntities); - } - - @Override - public Optional findEntityViewByTenantIdAndName(UUID tenantId, String name) { - Select.Where query = select().from(ENTITY_VIEW_BY_TENANT_AND_NAME).where(); - query.and(eq(ENTITY_VIEW_TENANT_ID_PROPERTY, tenantId)); - query.and(eq(ENTITY_VIEW_NAME_PROPERTY, name)); - return Optional.ofNullable(DaoUtil.getData(findOneByStatement(new TenantId(tenantId), query))); - } - - @Override - public List findEntityViewsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink) { - log.debug("Try to find entity views by tenantId [{}], customerId[{}] and pageLink [{}]", - tenantId, customerId, pageLink); - List entityViewEntities = findPageWithTextSearch(new TenantId(tenantId), - ENTITY_VIEW_BY_TENANT_AND_CUSTOMER_CF, - Arrays.asList(eq(CUSTOMER_ID_PROPERTY, customerId), eq(TENANT_ID_PROPERTY, tenantId)), - pageLink); - log.trace("Found find entity views [{}] by tenantId [{}], customerId [{}] and pageLink [{}]", - entityViewEntities, tenantId, customerId, pageLink); - return DaoUtil.convertDataList(entityViewEntities); - } - - @Override - public List findEntityViewsByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, TextPageLink pageLink) { - log.debug("Try to find entity views by tenantId [{}], customerId[{}], type [{}] and pageLink [{}]", - tenantId, customerId, type, pageLink); - List entityViewEntities = findPageWithTextSearch(new TenantId(tenantId), - ENTITY_VIEW_BY_TENANT_AND_CUSTOMER_AND_TYPE_CF, - Arrays.asList(eq(DEVICE_TYPE_PROPERTY, type), eq(CUSTOMER_ID_PROPERTY, customerId), eq(TENANT_ID_PROPERTY, tenantId)), - pageLink); - log.trace("Found find entity views [{}] by tenantId [{}], customerId [{}], type [{}] and pageLink [{}]", - entityViewEntities, tenantId, customerId, type, pageLink); - return DaoUtil.convertDataList(entityViewEntities); - } - - @Override - public ListenableFuture> findEntityViewsByTenantIdAndEntityIdAsync(UUID tenantId, UUID entityId) { - log.debug("Try to find entity views by tenantId [{}] and entityId [{}]", tenantId, entityId); - Select.Where query = select().from(ENTITY_VIEW_BY_TENANT_AND_ENTITY_ID_CF).where(); - query.and(eq(TENANT_ID_PROPERTY, tenantId)); - query.and(eq(ENTITY_ID_COLUMN, entityId)); - return findListByStatementAsync(new TenantId(tenantId), query); - } - - @Override - public ListenableFuture> findTenantEntityViewTypesAsync(UUID tenantId) { - Select select = select().from(ENTITY_SUBTYPE_COLUMN_FAMILY_NAME); - Select.Where query = select.where(); - query.and(eq(ENTITY_SUBTYPE_TENANT_ID_PROPERTY, tenantId)); - query.and(eq(ENTITY_SUBTYPE_ENTITY_TYPE_PROPERTY, EntityType.ENTITY_VIEW)); - query.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel()); - ResultSetFuture resultSetFuture = executeAsyncRead(new TenantId(tenantId), query); - return Futures.transform(resultSetFuture, new Function>() { - @Nullable - @Override - public List apply(@Nullable ResultSet resultSet) { - Result result = cluster.getMapper(EntitySubtypeEntity.class).map(resultSet); - if (result != null) { - List entitySubtypes = new ArrayList<>(); - result.all().forEach((entitySubtypeEntity) -> - entitySubtypes.add(entitySubtypeEntity.toEntitySubtype()) - ); - return entitySubtypes; - } else { - return Collections.emptyList(); - } - } - }, MoreExecutors.directExecutor()); - } -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java index 02ca8050cd..02c0fce3d4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java @@ -16,11 +16,12 @@ package org.thingsboard.server.dao.entityview; import com.google.common.util.concurrent.ListenableFuture; -import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.EntityViewInfo; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; import java.util.List; @@ -32,6 +33,15 @@ import java.util.UUID; */ public interface EntityViewDao extends Dao { + /** + * Find entity view info by id. + * + * @param tenantId the tenant id + * @param assetId the asset id + * @return the entity view info object + */ + EntityViewInfo findEntityViewInfoById(TenantId tenantId, UUID entityViewId); + /** * Save or update device object * @@ -47,7 +57,16 @@ public interface EntityViewDao extends Dao { * @param pageLink the page link * @return the list of entity view objects */ - List findEntityViewsByTenantId(UUID tenantId, TextPageLink pageLink); + PageData findEntityViewsByTenantId(UUID tenantId, PageLink pageLink); + + /** + * Find entity view infos by tenantId and page link. + * + * @param tenantId the tenantId + * @param pageLink the page link + * @return the list of entity view info objects + */ + PageData findEntityViewInfosByTenantId(UUID tenantId, PageLink pageLink); /** * Find entity views by tenantId, type and page link. @@ -57,7 +76,17 @@ public interface EntityViewDao extends Dao { * @param pageLink the page link * @return the list of entity view objects */ - List findEntityViewsByTenantIdAndType(UUID tenantId, String type, TextPageLink pageLink); + PageData findEntityViewsByTenantIdAndType(UUID tenantId, String type, PageLink pageLink); + + /** + * Find entity view infos by tenantId, type and page link. + * + * @param tenantId the tenantId + * @param type the type + * @param pageLink the page link + * @return the list of entity view info objects + */ + PageData findEntityViewInfosByTenantIdAndType(UUID tenantId, String type, PageLink pageLink); /** * Find entity views by tenantId and entity view name. @@ -76,9 +105,19 @@ public interface EntityViewDao extends Dao { * @param pageLink the page link * @return the list of entity view objects */ - List findEntityViewsByTenantIdAndCustomerId(UUID tenantId, + PageData findEntityViewsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, - TextPageLink pageLink); + PageLink pageLink); + + /** + * Find entity view infos by tenantId, customerId and page link. + * + * @param tenantId the tenantId + * @param customerId the customerId + * @param pageLink the page link + * @return the list of entity view info objects + */ + PageData findEntityViewInfosByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink); /** * Find entity views by tenantId, customerId, type and page link. @@ -89,10 +128,21 @@ public interface EntityViewDao extends Dao { * @param pageLink the page link * @return the list of entity view objects */ - List findEntityViewsByTenantIdAndCustomerIdAndType(UUID tenantId, + PageData findEntityViewsByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, - TextPageLink pageLink); + PageLink pageLink); + + /** + * Find entity view infos by tenantId, customerId, type and page link. + * + * @param tenantId the tenantId + * @param customerId the customerId + * @param type the type + * @param pageLink the page link + * @return the list of entity view info objects + */ + PageData findEntityViewInfosByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink); ListenableFuture> findEntityViewsByTenantIdAndEntityIdAsync(UUID tenantId, UUID entityId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java index 45e6ba823c..6f7fa11462 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java @@ -33,14 +33,15 @@ import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.EntityViewInfo; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.dao.customer.CustomerDao; @@ -125,6 +126,13 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti customerEntityViewsUnAssigner.removeEntities(tenantId, customerId); } + @Override + public EntityViewInfo findEntityViewInfoById(TenantId tenantId, EntityViewId entityViewId) { + log.trace("Executing findEntityViewInfoById [{}]", entityViewId); + validateId(entityViewId, INCORRECT_ENTITY_VIEW_ID + entityViewId); + return entityViewDao.findEntityViewInfoById(tenantId, entityViewId.getId()); + } + @Cacheable(cacheNames = ENTITY_VIEW_CACHE, key = "{#entityViewId}") @Override public EntityView findEntityViewById(TenantId tenantId, EntityViewId entityViewId) { @@ -143,48 +151,84 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti } @Override - public TextPageData findEntityViewByTenantId(TenantId tenantId, TextPageLink pageLink) { + public PageData findEntityViewByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findEntityViewsByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validatePageLink(pageLink, INCORRECT_PAGE_LINK + pageLink); - List entityViews = entityViewDao.findEntityViewsByTenantId(tenantId.getId(), pageLink); - return new TextPageData<>(entityViews, pageLink); + validatePageLink(pageLink); + return entityViewDao.findEntityViewsByTenantId(tenantId.getId(), pageLink); + } + + @Override + public PageData findEntityViewInfosByTenantId(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findEntityViewInfosByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validatePageLink(pageLink); + return entityViewDao.findEntityViewInfosByTenantId(tenantId.getId(), pageLink); } @Override - public TextPageData findEntityViewByTenantIdAndType(TenantId tenantId, TextPageLink pageLink, String type) { + public PageData findEntityViewByTenantIdAndType(TenantId tenantId, PageLink pageLink, String type) { log.trace("Executing findEntityViewByTenantIdAndType, tenantId [{}], pageLink [{}], type [{}]", tenantId, pageLink, type); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validatePageLink(pageLink, INCORRECT_PAGE_LINK + pageLink); + validatePageLink(pageLink); validateString(type, "Incorrect type " + type); - List entityViews = entityViewDao.findEntityViewsByTenantIdAndType(tenantId.getId(), type, pageLink); - return new TextPageData<>(entityViews, pageLink); + return entityViewDao.findEntityViewsByTenantIdAndType(tenantId.getId(), type, pageLink); } @Override - public TextPageData findEntityViewsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, - TextPageLink pageLink) { + public PageData findEntityViewInfosByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink) { + log.trace("Executing findEntityViewInfosByTenantIdAndType, tenantId [{}], pageLink [{}], type [{}]", tenantId, pageLink, type); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validatePageLink(pageLink); + validateString(type, "Incorrect type " + type); + return entityViewDao.findEntityViewInfosByTenantIdAndType(tenantId.getId(), type, pageLink); + } + + @Override + public PageData findEntityViewsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, + PageLink pageLink) { log.trace("Executing findEntityViewByTenantIdAndCustomerId, tenantId [{}], customerId [{}]," + " pageLink [{}]", tenantId, customerId, pageLink); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); - validatePageLink(pageLink, INCORRECT_PAGE_LINK + pageLink); - List entityViews = entityViewDao.findEntityViewsByTenantIdAndCustomerId(tenantId.getId(), + validatePageLink(pageLink); + return entityViewDao.findEntityViewsByTenantIdAndCustomerId(tenantId.getId(), + customerId.getId(), pageLink); + } + + @Override + public PageData findEntityViewInfosByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) { + log.trace("Executing findEntityViewInfosByTenantIdAndCustomerId, tenantId [{}], customerId [{}]," + + " pageLink [{}]", tenantId, customerId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validatePageLink(pageLink); + return entityViewDao.findEntityViewInfosByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink); - return new TextPageData<>(entityViews, pageLink); } @Override - public TextPageData findEntityViewsByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, TextPageLink pageLink, String type) { + public PageData findEntityViewsByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, PageLink pageLink, String type) { log.trace("Executing findEntityViewsByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}]," + " pageLink [{}], type [{}]", tenantId, customerId, pageLink, type); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); - validatePageLink(pageLink, INCORRECT_PAGE_LINK + pageLink); + validatePageLink(pageLink); + validateString(type, "Incorrect type " + type); + return entityViewDao.findEntityViewsByTenantIdAndCustomerIdAndType(tenantId.getId(), + customerId.getId(), type, pageLink); + } + + @Override + public PageData findEntityViewInfosByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink) { + log.trace("Executing findEntityViewInfosByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}]," + + " pageLink [{}], type [{}]", tenantId, customerId, pageLink, type); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validatePageLink(pageLink); validateString(type, "Incorrect type " + type); - List entityViews = entityViewDao.findEntityViewsByTenantIdAndCustomerIdAndType(tenantId.getId(), + return entityViewDao.findEntityViewInfosByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink); - return new TextPageData<>(entityViews, pageLink); } @Override @@ -336,7 +380,7 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti private PaginatedRemover tenantEntityViewRemover = new PaginatedRemover() { @Override - protected List findEntities(TenantId tenantId, TenantId id, TextPageLink pageLink) { + protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { return entityViewDao.findEntityViewsByTenantId(id.getId(), pageLink); } @@ -348,7 +392,7 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti private PaginatedRemover customerEntityViewsUnAssigner = new PaginatedRemover() { @Override - protected List findEntities(TenantId tenantId, CustomerId id, TextPageLink pageLink) { + protected PageData findEntities(TenantId tenantId, CustomerId id, PageLink pageLink) { return entityViewDao.findEntityViewsByTenantIdAndCustomerId(tenantId.getId(), id.getId(), pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java b/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java index da1c9a0817..e4bab842cb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java @@ -24,7 +24,7 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Event; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; @@ -95,15 +95,13 @@ public class BaseEventService implements EventService { } @Override - public TimePageData findEvents(TenantId tenantId, EntityId entityId, TimePageLink pageLink) { - List events = eventDao.findEvents(tenantId.getId(), entityId, pageLink); - return new TimePageData<>(events, pageLink); + public PageData findEvents(TenantId tenantId, EntityId entityId, TimePageLink pageLink) { + return eventDao.findEvents(tenantId.getId(), entityId, pageLink); } @Override - public TimePageData findEvents(TenantId tenantId, EntityId entityId, String eventType, TimePageLink pageLink) { - List events = eventDao.findEvents(tenantId.getId(), entityId, eventType, pageLink); - return new TimePageData<>(events, pageLink); + public PageData findEvents(TenantId tenantId, EntityId entityId, String eventType, TimePageLink pageLink) { + return eventDao.findEvents(tenantId.getId(), entityId, eventType, pageLink); } @Override @@ -113,7 +111,7 @@ public class BaseEventService implements EventService { @Override public void removeEvents(TenantId tenantId, EntityId entityId) { - TimePageData eventPageData; + PageData eventPageData; TimePageLink eventPageLink = new TimePageLink(1000); do { eventPageData = findEvents(tenantId, entityId, eventPageLink); @@ -121,7 +119,7 @@ public class BaseEventService implements EventService { eventDao.removeById(tenantId, event.getUuidId()); } if (eventPageData.hasNext()) { - eventPageLink = eventPageData.getNextPageLink(); + eventPageLink = eventPageLink.nextPageLink(); } } while (eventPageData.hasNext()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/CassandraBaseEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/event/CassandraBaseEventDao.java deleted file mode 100644 index f2660e512d..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/event/CassandraBaseEventDao.java +++ /dev/null @@ -1,218 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.event; - -import com.datastax.driver.core.ResultSetFuture; -import com.datastax.driver.core.querybuilder.Insert; -import com.datastax.driver.core.querybuilder.QueryBuilder; -import com.datastax.driver.core.querybuilder.Select; -import com.datastax.driver.core.utils.UUIDs; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.DataConstants; -import org.thingsboard.server.common.data.Event; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.EventId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TimePageLink; -import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.nosql.EventEntity; -import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTimeDao; -import org.thingsboard.server.dao.util.NoSqlDao; - -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.ExecutionException; - -import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; -import static com.datastax.driver.core.querybuilder.QueryBuilder.select; -import static com.datastax.driver.core.querybuilder.QueryBuilder.ttl; -import static org.thingsboard.server.dao.model.ModelConstants.EVENT_BY_ID_VIEW_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.EVENT_BY_TYPE_AND_ID_VIEW_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.EVENT_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; - -@Component -@Slf4j -@NoSqlDao -public class CassandraBaseEventDao extends CassandraAbstractSearchTimeDao implements EventDao { - - private final TenantId systemTenantId = new TenantId(NULL_UUID); - - @Override - protected Class getColumnFamilyClass() { - return EventEntity.class; - } - - @Override - protected String getColumnFamilyName() { - return EVENT_COLUMN_FAMILY_NAME; - } - - @Value("${cassandra.query.events_ttl:0}") - private int eventsTtl; - - @Value("${cassandra.query.debug_events_ttl:0}") - private int debugEventsTtl; - - @Override - public Event save(TenantId tenantId, Event event) { - try { - return saveAsync(event).get(); - } catch (InterruptedException | ExecutionException e) { - throw new IllegalStateException("Could not save EventEntity", e); - } - } - - @Override - public ListenableFuture saveAsync(Event event) { - log.debug("Save event [{}] ", event); - if (event.getTenantId() == null) { - log.trace("Save system event with predefined id {}", systemTenantId); - event.setTenantId(systemTenantId); - } - if (event.getId() == null) { - event.setId(new EventId(UUIDs.timeBased())); - } - if (StringUtils.isEmpty(event.getUid())) { - event.setUid(event.getId().toString()); - } - ListenableFuture> optionalSave = saveAsync(event.getTenantId(), new EventEntity(event), false, eventsTtl); - return Futures.transform(optionalSave, opt -> opt.orElse(null), MoreExecutors.directExecutor()); - } - - @Override - public Optional saveIfNotExists(Event event) { - if (event.getTenantId() == null) { - log.trace("Save system event with predefined id {}", systemTenantId); - event.setTenantId(systemTenantId); - } - if (event.getId() == null) { - event.setId(new EventId(UUIDs.timeBased())); - } - return save(event.getTenantId(), new EventEntity(event), true, eventsTtl); - } - - @Override - public Event findEvent(UUID tenantId, EntityId entityId, String eventType, String eventUid) { - log.debug("Search event entity by [{}][{}][{}][{}]", tenantId, entityId, eventType, eventUid); - Select.Where query = select().from(getColumnFamilyName()).where( - eq(ModelConstants.EVENT_TENANT_ID_PROPERTY, tenantId)) - .and(eq(ModelConstants.EVENT_ENTITY_TYPE_PROPERTY, entityId.getEntityType())) - .and(eq(ModelConstants.EVENT_ENTITY_ID_PROPERTY, entityId.getId())) - .and(eq(ModelConstants.EVENT_TYPE_PROPERTY, eventType)) - .and(eq(ModelConstants.EVENT_UID_PROPERTY, eventUid)); - log.trace("Execute query [{}]", query); - EventEntity entity = findOneByStatement(new TenantId(tenantId), query); - if (log.isTraceEnabled()) { - log.trace("Search result: [{}] for event entity [{}]", entity != null, entity); - } else { - log.debug("Search result: [{}]", entity != null); - } - return DaoUtil.getData(entity); - } - - @Override - public List findEvents(UUID tenantId, EntityId entityId, TimePageLink pageLink) { - log.trace("Try to find events by tenant [{}], entity [{}]and pageLink [{}]", tenantId, entityId, pageLink); - List entities = findPageWithTimeSearch(new TenantId(tenantId), EVENT_BY_ID_VIEW_NAME, - Arrays.asList(eq(ModelConstants.EVENT_TENANT_ID_PROPERTY, tenantId), - eq(ModelConstants.EVENT_ENTITY_TYPE_PROPERTY, entityId.getEntityType()), - eq(ModelConstants.EVENT_ENTITY_ID_PROPERTY, entityId.getId())), - pageLink); - log.trace("Found events by tenant [{}], entity [{}] and pageLink [{}]", tenantId, entityId, pageLink); - return DaoUtil.convertDataList(entities); - } - - @Override - public List findEvents(UUID tenantId, EntityId entityId, String eventType, TimePageLink pageLink) { - log.trace("Try to find events by tenant [{}], entity [{}], type [{}] and pageLink [{}]", tenantId, entityId, eventType, pageLink); - List entities = findPageWithTimeSearch(new TenantId(tenantId), EVENT_BY_TYPE_AND_ID_VIEW_NAME, - Arrays.asList(eq(ModelConstants.EVENT_TENANT_ID_PROPERTY, tenantId), - eq(ModelConstants.EVENT_ENTITY_TYPE_PROPERTY, entityId.getEntityType()), - eq(ModelConstants.EVENT_ENTITY_ID_PROPERTY, entityId.getId()), - eq(ModelConstants.EVENT_TYPE_PROPERTY, eventType)), - pageLink.isAscOrder() ? QueryBuilder.asc(ModelConstants.EVENT_TYPE_PROPERTY) : - QueryBuilder.desc(ModelConstants.EVENT_TYPE_PROPERTY), - pageLink); - log.trace("Found events by tenant [{}], entity [{}], type [{}] and pageLink [{}]", tenantId, entityId, eventType, pageLink); - return DaoUtil.convertDataList(entities); - } - - @Override - public List findLatestEvents(UUID tenantId, EntityId entityId, String eventType, int limit) { - log.trace("Try to find latest events by tenant [{}], entity [{}], type [{}] and limit [{}]", tenantId, entityId, eventType, limit); - Select select = select().from(EVENT_BY_TYPE_AND_ID_VIEW_NAME); - Select.Where query = select.where(); - query.and(eq(ModelConstants.EVENT_TENANT_ID_PROPERTY, tenantId)); - query.and(eq(ModelConstants.EVENT_ENTITY_TYPE_PROPERTY, entityId.getEntityType())); - query.and(eq(ModelConstants.EVENT_ENTITY_ID_PROPERTY, entityId.getId())); - query.and(eq(ModelConstants.EVENT_TYPE_PROPERTY, eventType)); - query.limit(limit); - query.orderBy(QueryBuilder.desc(ModelConstants.EVENT_TYPE_PROPERTY), QueryBuilder.desc(ModelConstants.ID_PROPERTY)); - List entities = findListByStatement(new TenantId(tenantId), query); - return DaoUtil.convertDataList(entities); - } - - private Optional save(TenantId tenantId, EventEntity entity, boolean ifNotExists, int ttl) { - try { - return saveAsync(tenantId, entity, ifNotExists, ttl).get(); - } catch (InterruptedException | ExecutionException e) { - throw new IllegalStateException("Could not save EventEntity", e); - } - } - - private ListenableFuture> saveAsync(TenantId tenantId, EventEntity entity, boolean ifNotExists, int ttl) { - if (entity.getUuid() == null) { - entity.setUuid(UUIDs.timeBased()); - } - Insert insert = QueryBuilder.insertInto(getColumnFamilyName()) - .value(ModelConstants.ID_PROPERTY, entity.getUuid()) - .value(ModelConstants.EVENT_TENANT_ID_PROPERTY, entity.getTenantId()) - .value(ModelConstants.EVENT_ENTITY_TYPE_PROPERTY, entity.getEntityType()) - .value(ModelConstants.EVENT_ENTITY_ID_PROPERTY, entity.getEntityId()) - .value(ModelConstants.EVENT_TYPE_PROPERTY, entity.getEventType()) - .value(ModelConstants.EVENT_UID_PROPERTY, entity.getEventUid()) - .value(ModelConstants.EVENT_BODY_PROPERTY, entity.getBody()); - - if (ifNotExists) { - insert = insert.ifNotExists(); - } - - int selectedTtl = (entity.getEventType().equals(DataConstants.DEBUG_RULE_NODE) || - entity.getEventType().equals(DataConstants.DEBUG_RULE_CHAIN)) ? debugEventsTtl : ttl; - - if (selectedTtl > 0) { - insert.using(ttl(selectedTtl)); - } - ResultSetFuture resultSetFuture = executeAsyncWrite(tenantId, insert); - return Futures.transform(resultSetFuture, rs -> { - if (rs.wasApplied()) { - return Optional.of(DaoUtil.getData(entity)); - } else { - return Optional.empty(); - } - }, MoreExecutors.directExecutor()); - } -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java b/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java index a5c4ad4dbb..54e8a434ea 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java @@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.Event; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.Dao; @@ -74,7 +75,7 @@ public interface EventDao extends Dao { * @param pageLink the pageLink * @return the event list */ - List findEvents(UUID tenantId, EntityId entityId, TimePageLink pageLink); + PageData findEvents(UUID tenantId, EntityId entityId, TimePageLink pageLink); /** * Find events by tenantId, entityId, eventType and pageLink. @@ -85,7 +86,7 @@ public interface EventDao extends Dao { * @param pageLink the pageLink * @return the event list */ - List findEvents(UUID tenantId, EntityId entityId, String eventType, TimePageLink pageLink); + PageData findEvents(UUID tenantId, EntityId entityId, String eventType, TimePageLink pageLink); /** * Find latest events by tenantId, entityId and eventType. diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/BaseEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/BaseEntity.java index b5bda06a47..c7f9a9b86b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/BaseEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/BaseEntity.java @@ -23,4 +23,8 @@ public interface BaseEntity extends ToData { void setUuid(UUID id); + long getCreatedTime(); + + void setCreatedTime(long createdTime); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/BaseSqlEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/BaseSqlEntity.java index 44fd70f977..3d1fd53679 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/BaseSqlEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/BaseSqlEntity.java @@ -16,7 +16,6 @@ package org.thingsboard.server.dao.model; import lombok.Data; -import org.thingsboard.server.common.data.UUIDConverter; import javax.persistence.Column; import javax.persistence.Id; @@ -31,28 +30,30 @@ import java.util.UUID; public abstract class BaseSqlEntity implements BaseEntity { @Id - @Column(name = ModelConstants.ID_PROPERTY) - protected String id; + @Column(name = ModelConstants.ID_PROPERTY, columnDefinition = "uuid") + protected UUID id; + + @Column(name = ModelConstants.CREATED_TIME_PROPERTY) + protected long createdTime; @Override public UUID getUuid() { - if (id == null) { - return null; - } - return UUIDConverter.fromString(id); + return id; } @Override public void setUuid(UUID id) { - this.id = UUIDConverter.fromTimeUUID(id); + this.id = id; } - protected UUID toUUID(String src){ - return UUIDConverter.fromString(src); + @Override + public long getCreatedTime() { + return createdTime; } - protected String toString(UUID timeUUID){ - return UUIDConverter.fromTimeUUID(timeUUID); + public void setCreatedTime(long createdTime) { + if (createdTime > 0) { + this.createdTime = createdTime; + } } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/EntitySubtypeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/EntitySubtypeEntity.java deleted file mode 100644 index b6c99042b3..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/EntitySubtypeEntity.java +++ /dev/null @@ -1,121 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model; - -import com.datastax.driver.mapping.annotations.Column; -import com.datastax.driver.mapping.annotations.PartitionKey; -import com.datastax.driver.mapping.annotations.Table; -import org.thingsboard.server.common.data.EntitySubtype; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.model.type.EntityTypeCodec; - -import java.util.UUID; - -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_ENTITY_TYPE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_TENANT_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_TYPE_PROPERTY; - -@Table(name = ENTITY_SUBTYPE_COLUMN_FAMILY_NAME) -public class EntitySubtypeEntity { - - @PartitionKey(value = 0) - @Column(name = ENTITY_SUBTYPE_TENANT_ID_PROPERTY) - private UUID tenantId; - - @PartitionKey(value = 1) - @Column(name = ENTITY_SUBTYPE_ENTITY_TYPE_PROPERTY, codec = EntityTypeCodec.class) - private EntityType entityType; - - @PartitionKey(value = 2) - @Column(name = ENTITY_SUBTYPE_TYPE_PROPERTY) - private String type; - - public EntitySubtypeEntity() { - super(); - } - - public EntitySubtypeEntity(EntitySubtype entitySubtype) { - this.tenantId = entitySubtype.getTenantId().getId(); - this.entityType = entitySubtype.getEntityType(); - this.type = entitySubtype.getType(); - } - - public UUID getTenantId() { - return tenantId; - } - - public void setTenantId(UUID tenantId) { - this.tenantId = tenantId; - } - - public EntityType getEntityType() { - return entityType; - } - - public void setEntityType(EntityType entityType) { - this.entityType = entityType; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - EntitySubtypeEntity that = (EntitySubtypeEntity) o; - - if (tenantId != null ? !tenantId.equals(that.tenantId) : that.tenantId != null) return false; - if (entityType != that.entityType) return false; - return type != null ? type.equals(that.type) : that.type == null; - - } - - @Override - public int hashCode() { - int result = tenantId != null ? tenantId.hashCode() : 0; - result = 31 * result + (entityType != null ? entityType.hashCode() : 0); - result = 31 * result + (type != null ? type.hashCode() : 0); - return result; - } - - @Override - public String toString() { - final StringBuilder sb = new StringBuilder("EntitySubtypeEntity{"); - sb.append("tenantId=").append(tenantId); - sb.append(", entityType=").append(entityType); - sb.append(", type='").append(type).append('\''); - sb.append('}'); - return sb.toString(); - } - - public EntitySubtype toEntitySubtype() { - EntitySubtype entitySubtype = new EntitySubtype(); - entitySubtype.setTenantId(new TenantId(tenantId)); - entitySubtype.setEntityType(entityType); - entitySubtype.setType(type); - return entitySubtype; - } -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 9f72567838..e863de1edc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -15,9 +15,8 @@ */ package org.thingsboard.server.dao.model; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import org.apache.commons.lang3.ArrayUtils; -import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.Aggregation; @@ -28,8 +27,7 @@ public class ModelConstants { private ModelConstants() { } - public static final UUID NULL_UUID = UUIDs.startOf(0); - public static final String NULL_UUID_STR = UUIDConverter.fromTimeUUID(NULL_UUID); + public static final UUID NULL_UUID = Uuids.startOf(0); public static final TenantId SYSTEM_TENANT = new TenantId(ModelConstants.NULL_UUID); // this is the difference between midnight October 15, 1582 UTC and midnight January 1, 1970 UTC as 100 nanosecond units @@ -39,6 +37,7 @@ public class ModelConstants { * Generic constants. */ public static final String ID_PROPERTY = "id"; + public static final String CREATED_TIME_PROPERTY = "created_time"; public static final String USER_ID_PROPERTY = "user_id"; public static final String TENANT_ID_PROPERTY = "tenant_id"; public static final String CUSTOMER_ID_PROPERTY = "customer_id"; @@ -115,11 +114,21 @@ public class ModelConstants { public static final String TENANT_TITLE_PROPERTY = TITLE_PROPERTY; public static final String TENANT_REGION_PROPERTY = "region"; public static final String TENANT_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; - public static final String TENANT_ISOLATED_TB_CORE = "isolated_tb_core"; - public static final String TENANT_ISOLATED_TB_RULE_ENGINE = "isolated_tb_rule_engine"; + public static final String TENANT_TENANT_PROFILE_ID_PROPERTY = "tenant_profile_id"; public static final String TENANT_BY_REGION_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "tenant_by_region_and_search_text"; + /** + * Tenant profile constants. + */ + public static final String TENANT_PROFILE_COLUMN_FAMILY_NAME = "tenant_profile"; + public static final String TENANT_PROFILE_NAME_PROPERTY = "name"; + public static final String TENANT_PROFILE_PROFILE_DATA_PROPERTY = "profile_data"; + public static final String TENANT_PROFILE_DESCRIPTION_PROPERTY = "description"; + public static final String TENANT_PROFILE_IS_DEFAULT_PROPERTY = "is_default"; + public static final String TENANT_PROFILE_ISOLATED_TB_CORE = "isolated_tb_core"; + public static final String TENANT_PROFILE_ISOLATED_TB_RULE_ENGINE = "isolated_tb_rule_engine"; + /** * Cassandra customer constants. */ @@ -142,6 +151,9 @@ public class ModelConstants { public static final String DEVICE_TYPE_PROPERTY = "type"; public static final String DEVICE_LABEL_PROPERTY = "label"; public static final String DEVICE_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; + public static final String DEVICE_DEVICE_PROFILE_ID_PROPERTY = "device_profile_id"; + public static final String DEVICE_DEVICE_DATA_PROPERTY = "device_data"; + public static final String DEVICE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_and_search_text"; public static final String DEVICE_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_by_type_and_search_text"; public static final String DEVICE_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_customer_and_search_text"; @@ -149,6 +161,22 @@ public class ModelConstants { public static final String DEVICE_BY_TENANT_AND_NAME_VIEW_NAME = "device_by_tenant_and_name"; public static final String DEVICE_TYPES_BY_TENANT_VIEW_NAME = "device_types_by_tenant"; + /** + * Device profile constants. + */ + public static final String DEVICE_PROFILE_COLUMN_FAMILY_NAME = "device_profile"; + public static final String DEVICE_PROFILE_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; + public static final String DEVICE_PROFILE_NAME_PROPERTY = "name"; + public static final String DEVICE_PROFILE_TYPE_PROPERTY = "type"; + public static final String DEVICE_PROFILE_TRANSPORT_TYPE_PROPERTY = "transport_type"; + public static final String DEVICE_PROFILE_PROVISION_TYPE_PROPERTY = "provision_type"; + public static final String DEVICE_PROFILE_PROFILE_DATA_PROPERTY = "profile_data"; + public static final String DEVICE_PROFILE_DESCRIPTION_PROPERTY = "description"; + public static final String DEVICE_PROFILE_IS_DEFAULT_PROPERTY = "is_default"; + public static final String DEVICE_PROFILE_DEFAULT_RULE_CHAIN_ID_PROPERTY = "default_rule_chain_id"; + public static final String DEVICE_PROFILE_DEFAULT_QUEUE_NAME_PROPERTY = "default_queue_name"; + public static final String DEVICE_PROFILE_PROVISION_DEVICE_KEY = "provision_device_key"; + /** * Cassandra entityView constants. */ @@ -228,6 +256,7 @@ public class ModelConstants { public static final String ALARM_TYPE_PROPERTY = "type"; public static final String ALARM_DETAILS_PROPERTY = "details"; public static final String ALARM_ORIGINATOR_ID_PROPERTY = "originator_id"; + public static final String ALARM_ORIGINATOR_NAME_PROPERTY = "originator_name"; public static final String ALARM_ORIGINATOR_TYPE_PROPERTY = "originator_type"; public static final String ALARM_SEVERITY_PROPERTY = "severity"; public static final String ALARM_STATUS_PROPERTY = "status"; @@ -354,6 +383,74 @@ public class ModelConstants { public static final String RULE_NODE_NAME_PROPERTY = "name"; public static final String RULE_NODE_CONFIGURATION_PROPERTY = "configuration"; + /** + * Rule node state constants. + */ + public static final String RULE_NODE_STATE_TABLE_NAME = "rule_node_state"; + public static final String RULE_NODE_STATE_NODE_ID_PROPERTY = "rule_node_id"; + public static final String RULE_NODE_STATE_ENTITY_TYPE_PROPERTY = "entity_type"; + public static final String RULE_NODE_STATE_ENTITY_ID_PROPERTY = "entity_id"; + public static final String RULE_NODE_STATE_DATA_PROPERTY = "state_data"; + + /** + * OAuth2 client registration constants. + */ + public static final String OAUTH2_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; + public static final String OAUTH2_CLIENT_REGISTRATION_INFO_COLUMN_FAMILY_NAME = "oauth2_client_registration_info"; + public static final String OAUTH2_CLIENT_REGISTRATION_COLUMN_FAMILY_NAME = "oauth2_client_registration"; + public static final String OAUTH2_CLIENT_REGISTRATION_TO_DOMAIN_COLUMN_FAMILY_NAME = "oauth2_client_registration_to_domain"; + public static final String OAUTH2_CLIENT_REGISTRATION_TEMPLATE_COLUMN_FAMILY_NAME = "oauth2_client_registration_template"; + public static final String OAUTH2_ENABLED_PROPERTY = "enabled"; + public static final String OAUTH2_TEMPLATE_PROVIDER_ID_PROPERTY = "provider_id"; + public static final String OAUTH2_CLIENT_REGISTRATION_INFO_ID_PROPERTY = "client_registration_info_id"; + public static final String OAUTH2_DOMAIN_NAME_PROPERTY = "domain_name"; + public static final String OAUTH2_DOMAIN_SCHEME_PROPERTY = "domain_scheme"; + public static final String OAUTH2_CLIENT_ID_PROPERTY = "client_id"; + public static final String OAUTH2_CLIENT_SECRET_PROPERTY = "client_secret"; + public static final String OAUTH2_AUTHORIZATION_URI_PROPERTY = "authorization_uri"; + public static final String OAUTH2_TOKEN_URI_PROPERTY = "token_uri"; + public static final String OAUTH2_REDIRECT_URI_TEMPLATE_PROPERTY = "redirect_uri_template"; + public static final String OAUTH2_SCOPE_PROPERTY = "scope"; + public static final String OAUTH2_USER_INFO_URI_PROPERTY = "user_info_uri"; + public static final String OAUTH2_USER_NAME_ATTRIBUTE_NAME_PROPERTY = "user_name_attribute_name"; + public static final String OAUTH2_JWK_SET_URI_PROPERTY = "jwk_set_uri"; + public static final String OAUTH2_CLIENT_AUTHENTICATION_METHOD_PROPERTY = "client_authentication_method"; + public static final String OAUTH2_LOGIN_BUTTON_LABEL_PROPERTY = "login_button_label"; + public static final String OAUTH2_LOGIN_BUTTON_ICON_PROPERTY = "login_button_icon"; + public static final String OAUTH2_ALLOW_USER_CREATION_PROPERTY = "allow_user_creation"; + public static final String OAUTH2_ACTIVATE_USER_PROPERTY = "activate_user"; + public static final String OAUTH2_MAPPER_TYPE_PROPERTY = "type"; + public static final String OAUTH2_EMAIL_ATTRIBUTE_KEY_PROPERTY = "basic_email_attribute_key"; + public static final String OAUTH2_FIRST_NAME_ATTRIBUTE_KEY_PROPERTY = "basic_first_name_attribute_key"; + public static final String OAUTH2_LAST_NAME_ATTRIBUTE_KEY_PROPERTY = "basic_last_name_attribute_key"; + public static final String OAUTH2_TENANT_NAME_STRATEGY_PROPERTY = "basic_tenant_name_strategy"; + public static final String OAUTH2_TENANT_NAME_PATTERN_PROPERTY = "basic_tenant_name_pattern"; + public static final String OAUTH2_CUSTOMER_NAME_PATTERN_PROPERTY = "basic_customer_name_pattern"; + public static final String OAUTH2_DEFAULT_DASHBOARD_NAME_PROPERTY = "basic_default_dashboard_name"; + public static final String OAUTH2_ALWAYS_FULL_SCREEN_PROPERTY = "basic_always_full_screen"; + public static final String OAUTH2_MAPPER_URL_PROPERTY = "custom_url"; + public static final String OAUTH2_MAPPER_USERNAME_PROPERTY = "custom_username"; + public static final String OAUTH2_MAPPER_PASSWORD_PROPERTY = "custom_password"; + public static final String OAUTH2_MAPPER_SEND_TOKEN_PROPERTY = "custom_send_token"; + public static final String OAUTH2_TEMPLATE_COMMENT_PROPERTY = "comment"; + public static final String OAUTH2_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; + public static final String OAUTH2_TEMPLATE_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; + public static final String OAUTH2_TEMPLATE_LOGIN_BUTTON_ICON_PROPERTY = OAUTH2_LOGIN_BUTTON_ICON_PROPERTY; + public static final String OAUTH2_TEMPLATE_LOGIN_BUTTON_LABEL_PROPERTY = OAUTH2_LOGIN_BUTTON_LABEL_PROPERTY; + public static final String OAUTH2_TEMPLATE_HELP_LINK_PROPERTY = "help_link"; + + /** + * Usage Record constants. + */ + public static final String API_USAGE_STATE_TABLE_NAME = "api_usage_state"; + public static final String API_USAGE_STATE_TENANT_ID_COLUMN = TENANT_ID_PROPERTY; + public static final String API_USAGE_STATE_ENTITY_TYPE_COLUMN = ENTITY_TYPE_COLUMN; + public static final String API_USAGE_STATE_ENTITY_ID_COLUMN = ENTITY_ID_COLUMN; + public static final String API_USAGE_STATE_TRANSPORT_COLUMN = "transport"; + public static final String API_USAGE_STATE_DB_STORAGE_COLUMN = "db_storage"; + public static final String API_USAGE_STATE_RE_EXEC_COLUMN = "re_exec"; + public static final String API_USAGE_STATE_JS_EXEC_COLUMN = "js_exec"; + /** * Cassandra attributes and timeseries constants. */ diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AdminSettingsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AdminSettingsEntity.java deleted file mode 100644 index 5d8f5d671f..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AdminSettingsEntity.java +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.nosql; - -import com.datastax.driver.core.utils.UUIDs; -import com.datastax.driver.mapping.annotations.Column; -import com.datastax.driver.mapping.annotations.PartitionKey; -import com.datastax.driver.mapping.annotations.Table; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.EqualsAndHashCode; -import lombok.ToString; -import org.thingsboard.server.common.data.AdminSettings; -import org.thingsboard.server.common.data.id.AdminSettingsId; -import org.thingsboard.server.dao.model.BaseEntity; -import org.thingsboard.server.dao.model.type.JsonCodec; - -import java.util.UUID; - -import static org.thingsboard.server.dao.model.ModelConstants.ADMIN_SETTINGS_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.ADMIN_SETTINGS_JSON_VALUE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ADMIN_SETTINGS_KEY_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY; - -@Table(name = ADMIN_SETTINGS_COLUMN_FAMILY_NAME) -@EqualsAndHashCode -@ToString -public final class AdminSettingsEntity implements BaseEntity { - - @PartitionKey(value = 0) - @Column(name = ID_PROPERTY) - private UUID id; - - @Column(name = ADMIN_SETTINGS_KEY_PROPERTY) - private String key; - - @Column(name = ADMIN_SETTINGS_JSON_VALUE_PROPERTY, codec = JsonCodec.class) - private JsonNode jsonValue; - - public AdminSettingsEntity() { - super(); - } - - public AdminSettingsEntity(AdminSettings adminSettings) { - if (adminSettings.getId() != null) { - this.id = adminSettings.getId().getId(); - } - this.key = adminSettings.getKey(); - this.jsonValue = adminSettings.getJsonValue(); - } - - public UUID getUuid() { - return id; - } - - public void setUuid(UUID id) { - this.id = id; - } - - public String getKey() { - return key; - } - - public void setKey(String key) { - this.key = key; - } - - public JsonNode getJsonValue() { - return jsonValue; - } - - public void setJsonValue(JsonNode jsonValue) { - this.jsonValue = jsonValue; - } - - @Override - public AdminSettings toData() { - AdminSettings adminSettings = new AdminSettings(new AdminSettingsId(id)); - adminSettings.setCreatedTime(UUIDs.unixTimestamp(id)); - adminSettings.setKey(key); - adminSettings.setJsonValue(jsonValue); - return adminSettings; - } - -} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AlarmEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AlarmEntity.java deleted file mode 100644 index 819daa800b..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AlarmEntity.java +++ /dev/null @@ -1,281 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.nosql; - -import com.datastax.driver.core.utils.UUIDs; -import com.datastax.driver.mapping.annotations.ClusteringColumn; -import com.datastax.driver.mapping.annotations.Column; -import com.datastax.driver.mapping.annotations.PartitionKey; -import com.datastax.driver.mapping.annotations.Table; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.EqualsAndHashCode; -import lombok.ToString; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.id.AlarmId; -import org.thingsboard.server.common.data.alarm.AlarmSeverity; -import org.thingsboard.server.common.data.alarm.AlarmStatus; -import org.thingsboard.server.common.data.id.EntityIdFactory; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.model.BaseEntity; -import org.thingsboard.server.dao.model.type.AlarmSeverityCodec; -import org.thingsboard.server.dao.model.type.AlarmStatusCodec; -import org.thingsboard.server.dao.model.type.EntityTypeCodec; -import org.thingsboard.server.dao.model.type.JsonCodec; - -import java.util.Arrays; -import java.util.Collections; -import java.util.UUID; - -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ACK_TS_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_CLEAR_TS_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_DETAILS_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_END_TS_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ORIGINATOR_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ORIGINATOR_TYPE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_PROPAGATE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_PROPAGATE_RELATION_TYPES; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_SEVERITY_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_START_TS_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_STATUS_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_TENANT_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_TYPE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY; - -@Table(name = ALARM_COLUMN_FAMILY_NAME) -@EqualsAndHashCode -@ToString -public final class AlarmEntity implements BaseEntity { - - @ClusteringColumn(value = 1) - @Column(name = ID_PROPERTY) - private UUID id; - - @PartitionKey(value = 0) - @Column(name = ALARM_TENANT_ID_PROPERTY) - private UUID tenantId; - - @PartitionKey(value = 1) - @Column(name = ALARM_ORIGINATOR_ID_PROPERTY) - private UUID originatorId; - - @PartitionKey(value = 2) - @Column(name = ALARM_ORIGINATOR_TYPE_PROPERTY, codec = EntityTypeCodec.class) - private EntityType originatorType; - - @ClusteringColumn(value = 0) - @Column(name = ALARM_TYPE_PROPERTY) - private String type; - - @Column(name = ALARM_SEVERITY_PROPERTY, codec = AlarmSeverityCodec.class) - private AlarmSeverity severity; - - @Column(name = ALARM_STATUS_PROPERTY, codec = AlarmStatusCodec.class) - private AlarmStatus status; - - @Column(name = ALARM_START_TS_PROPERTY) - private Long startTs; - - @Column(name = ALARM_END_TS_PROPERTY) - private Long endTs; - - @Column(name = ALARM_ACK_TS_PROPERTY) - private Long ackTs; - - @Column(name = ALARM_CLEAR_TS_PROPERTY) - private Long clearTs; - - @Column(name = ALARM_DETAILS_PROPERTY, codec = JsonCodec.class) - private JsonNode details; - - @Column(name = ALARM_PROPAGATE_PROPERTY) - private Boolean propagate; - - @Column(name = ALARM_PROPAGATE_RELATION_TYPES) - private String propagateRelationTypes; - - public AlarmEntity() { - super(); - } - - public AlarmEntity(Alarm alarm) { - if (alarm.getId() != null) { - this.id = alarm.getId().getId(); - } - if (alarm.getTenantId() != null) { - this.tenantId = alarm.getTenantId().getId(); - } - this.type = alarm.getType(); - this.originatorId = alarm.getOriginator().getId(); - this.originatorType = alarm.getOriginator().getEntityType(); - this.type = alarm.getType(); - this.severity = alarm.getSeverity(); - this.status = alarm.getStatus(); - this.propagate = alarm.isPropagate(); - this.startTs = alarm.getStartTs(); - this.endTs = alarm.getEndTs(); - this.ackTs = alarm.getAckTs(); - this.clearTs = alarm.getClearTs(); - this.details = alarm.getDetails(); - this.details = alarm.getDetails(); - if (!CollectionUtils.isEmpty(alarm.getPropagateRelationTypes())) { - this.propagateRelationTypes = String.join(",", alarm.getPropagateRelationTypes()); - } else { - this.propagateRelationTypes = null; - } - } - - public UUID getUuid() { - return id; - } - - public void setUuid(UUID id) { - this.id = id; - } - - public UUID getTenantId() { - return tenantId; - } - - public void setTenantId(UUID tenantId) { - this.tenantId = tenantId; - } - - public UUID getOriginatorId() { - return originatorId; - } - - public void setOriginatorId(UUID originatorId) { - this.originatorId = originatorId; - } - - public EntityType getOriginatorType() { - return originatorType; - } - - public void setOriginatorType(EntityType originatorType) { - this.originatorType = originatorType; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public AlarmSeverity getSeverity() { - return severity; - } - - public void setSeverity(AlarmSeverity severity) { - this.severity = severity; - } - - public AlarmStatus getStatus() { - return status; - } - - public void setStatus(AlarmStatus status) { - this.status = status; - } - - public Long getStartTs() { - return startTs; - } - - public void setStartTs(Long startTs) { - this.startTs = startTs; - } - - public Long getEndTs() { - return endTs; - } - - public void setEndTs(Long endTs) { - this.endTs = endTs; - } - - public Long getAckTs() { - return ackTs; - } - - public void setAckTs(Long ackTs) { - this.ackTs = ackTs; - } - - public Long getClearTs() { - return clearTs; - } - - public void setClearTs(Long clearTs) { - this.clearTs = clearTs; - } - - public JsonNode getDetails() { - return details; - } - - public void setDetails(JsonNode details) { - this.details = details; - } - - public Boolean getPropagate() { - return propagate; - } - - public void setPropagate(Boolean propagate) { - this.propagate = propagate; - } - - public String getPropagateRelationTypes() { - return propagateRelationTypes; - } - - public void setPropagateRelationTypes(String propagateRelationTypes) { - this.propagateRelationTypes = propagateRelationTypes; - } - - @Override - public Alarm toData() { - Alarm alarm = new Alarm(new AlarmId(id)); - alarm.setCreatedTime(UUIDs.unixTimestamp(id)); - if (tenantId != null) { - alarm.setTenantId(new TenantId(tenantId)); - } - alarm.setOriginator(EntityIdFactory.getByTypeAndUuid(originatorType, originatorId)); - alarm.setType(type); - alarm.setSeverity(severity); - alarm.setStatus(status); - alarm.setPropagate(propagate); - alarm.setStartTs(startTs); - alarm.setEndTs(endTs); - alarm.setAckTs(ackTs); - alarm.setClearTs(clearTs); - alarm.setDetails(details); - if (!StringUtils.isEmpty(propagateRelationTypes)) { - alarm.setPropagateRelationTypes(Arrays.asList(propagateRelationTypes.split(","))); - } else { - alarm.setPropagateRelationTypes(Collections.emptyList()); - } - return alarm; - } - -} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AssetEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AssetEntity.java deleted file mode 100644 index 57ec0ee511..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AssetEntity.java +++ /dev/null @@ -1,176 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.nosql; - -import com.datastax.driver.core.utils.UUIDs; -import com.datastax.driver.mapping.annotations.Column; -import com.datastax.driver.mapping.annotations.PartitionKey; -import com.datastax.driver.mapping.annotations.Table; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.EqualsAndHashCode; -import lombok.ToString; -import org.thingsboard.server.common.data.asset.Asset; -import org.thingsboard.server.common.data.id.AssetId; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.model.SearchTextEntity; -import org.thingsboard.server.dao.model.type.JsonCodec; - -import java.util.UUID; - -import static org.thingsboard.server.dao.model.ModelConstants.ASSET_ADDITIONAL_INFO_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ASSET_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.ASSET_CUSTOMER_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ASSET_NAME_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TENANT_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TYPE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ASSET_LABEL_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY; - -@Table(name = ASSET_COLUMN_FAMILY_NAME) -@EqualsAndHashCode -@ToString -public final class AssetEntity implements SearchTextEntity { - - @PartitionKey(value = 0) - @Column(name = ID_PROPERTY) - private UUID id; - - @PartitionKey(value = 1) - @Column(name = ASSET_TENANT_ID_PROPERTY) - private UUID tenantId; - - @PartitionKey(value = 2) - @Column(name = ASSET_CUSTOMER_ID_PROPERTY) - private UUID customerId; - - @PartitionKey(value = 3) - @Column(name = ASSET_TYPE_PROPERTY) - private String type; - - @Column(name = ASSET_NAME_PROPERTY) - private String name; - - @Column(name = ASSET_LABEL_PROPERTY) - private String label; - - @Column(name = SEARCH_TEXT_PROPERTY) - private String searchText; - - @Column(name = ASSET_ADDITIONAL_INFO_PROPERTY, codec = JsonCodec.class) - private JsonNode additionalInfo; - - public AssetEntity() { - super(); - } - - public AssetEntity(Asset asset) { - if (asset.getId() != null) { - this.id = asset.getId().getId(); - } - if (asset.getTenantId() != null) { - this.tenantId = asset.getTenantId().getId(); - } - if (asset.getCustomerId() != null) { - this.customerId = asset.getCustomerId().getId(); - } - this.name = asset.getName(); - this.type = asset.getType(); - this.label = asset.getLabel(); - this.additionalInfo = asset.getAdditionalInfo(); - } - - public UUID getUuid() { - return id; - } - - public void setUuid(UUID id) { - this.id = id; - } - - public UUID getTenantId() { - return tenantId; - } - - public void setTenantId(UUID tenantId) { - this.tenantId = tenantId; - } - - public UUID getCustomerId() { - return customerId; - } - - public void setCustomerId(UUID customerId) { - this.customerId = customerId; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public JsonNode getAdditionalInfo() { - return additionalInfo; - } - - public void setAdditionalInfo(JsonNode additionalInfo) { - this.additionalInfo = additionalInfo; - } - - @Override - public String getSearchTextSource() { - return getName(); - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; - } - - public String getSearchText() { - return searchText; - } - - @Override - public Asset toData() { - Asset asset = new Asset(new AssetId(id)); - asset.setCreatedTime(UUIDs.unixTimestamp(id)); - if (tenantId != null) { - asset.setTenantId(new TenantId(tenantId)); - } - if (customerId != null) { - asset.setCustomerId(new CustomerId(customerId)); - } - asset.setName(name); - asset.setType(type); - asset.setLabel(label); - asset.setAdditionalInfo(additionalInfo); - return asset; - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java deleted file mode 100644 index b406a6b341..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java +++ /dev/null @@ -1,155 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.nosql; - -import com.datastax.driver.core.utils.UUIDs; -import com.datastax.driver.mapping.annotations.Column; -import com.datastax.driver.mapping.annotations.Table; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.audit.ActionStatus; -import org.thingsboard.server.common.data.audit.ActionType; -import org.thingsboard.server.common.data.audit.AuditLog; -import org.thingsboard.server.common.data.id.AuditLogId; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.EntityIdFactory; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.dao.model.BaseEntity; -import org.thingsboard.server.dao.model.type.ActionStatusCodec; -import org.thingsboard.server.dao.model.type.ActionTypeCodec; -import org.thingsboard.server.dao.model.type.EntityTypeCodec; -import org.thingsboard.server.dao.model.type.JsonCodec; - -import java.util.UUID; - -import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ACTION_DATA_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ACTION_FAILURE_DETAILS_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ACTION_STATUS_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ACTION_TYPE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_BY_ENTITY_ID_CF; -import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_CUSTOMER_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ENTITY_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ENTITY_NAME_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ENTITY_TYPE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_USER_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_USER_NAME_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY; - -@Table(name = AUDIT_LOG_BY_ENTITY_ID_CF) -@Data -@NoArgsConstructor -public class AuditLogEntity implements BaseEntity { - - @Column(name = ID_PROPERTY) - private UUID id; - - @Column(name = AUDIT_LOG_TENANT_ID_PROPERTY) - private UUID tenantId; - - @Column(name = AUDIT_LOG_CUSTOMER_ID_PROPERTY) - private UUID customerId; - - @Column(name = AUDIT_LOG_ENTITY_TYPE_PROPERTY, codec = EntityTypeCodec.class) - private EntityType entityType; - - @Column(name = AUDIT_LOG_ENTITY_ID_PROPERTY) - private UUID entityId; - - @Column(name = AUDIT_LOG_ENTITY_NAME_PROPERTY) - private String entityName; - - @Column(name = AUDIT_LOG_USER_ID_PROPERTY) - private UUID userId; - - @Column(name = AUDIT_LOG_USER_NAME_PROPERTY) - private String userName; - - @Column(name = AUDIT_LOG_ACTION_TYPE_PROPERTY, codec = ActionTypeCodec.class) - private ActionType actionType; - - @Column(name = AUDIT_LOG_ACTION_DATA_PROPERTY, codec = JsonCodec.class) - private JsonNode actionData; - - @Column(name = AUDIT_LOG_ACTION_STATUS_PROPERTY, codec = ActionStatusCodec.class) - private ActionStatus actionStatus; - - @Column(name = AUDIT_LOG_ACTION_FAILURE_DETAILS_PROPERTY) - private String actionFailureDetails; - - @Override - public UUID getUuid() { - return id; - } - - @Override - public void setUuid(UUID id) { - this.id = id; - } - - public AuditLogEntity(AuditLog auditLog) { - if (auditLog.getId() != null) { - this.id = auditLog.getId().getId(); - } - if (auditLog.getTenantId() != null) { - this.tenantId = auditLog.getTenantId().getId(); - } - if (auditLog.getEntityId() != null) { - this.entityType = auditLog.getEntityId().getEntityType(); - this.entityId = auditLog.getEntityId().getId(); - } - if (auditLog.getCustomerId() != null) { - this.customerId = auditLog.getCustomerId().getId(); - } - if (auditLog.getUserId() != null) { - this.userId = auditLog.getUserId().getId(); - } - this.entityName = auditLog.getEntityName(); - this.userName = auditLog.getUserName(); - this.actionType = auditLog.getActionType(); - this.actionData = auditLog.getActionData(); - this.actionStatus = auditLog.getActionStatus(); - this.actionFailureDetails = auditLog.getActionFailureDetails(); - } - - @Override - public AuditLog toData() { - AuditLog auditLog = new AuditLog(new AuditLogId(id)); - auditLog.setCreatedTime(UUIDs.unixTimestamp(id)); - if (tenantId != null) { - auditLog.setTenantId(new TenantId(tenantId)); - } - if (entityId != null && entityType != null) { - auditLog.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId)); - } - if (customerId != null) { - auditLog.setCustomerId(new CustomerId(customerId)); - } - if (userId != null) { - auditLog.setUserId(new UserId(userId)); - } - auditLog.setEntityName(this.entityName); - auditLog.setUserName(this.userName); - auditLog.setActionType(this.actionType); - auditLog.setActionData(this.actionData); - auditLog.setActionStatus(this.actionStatus); - auditLog.setActionFailureDetails(this.actionFailureDetails); - return auditLog; - } -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/ComponentDescriptorEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/ComponentDescriptorEntity.java deleted file mode 100644 index 1af3b8a8fa..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/ComponentDescriptorEntity.java +++ /dev/null @@ -1,171 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.nosql; - -import com.datastax.driver.mapping.annotations.Column; -import com.datastax.driver.mapping.annotations.PartitionKey; -import com.datastax.driver.mapping.annotations.Table; -import com.fasterxml.jackson.databind.JsonNode; -import org.thingsboard.server.common.data.id.ComponentDescriptorId; -import org.thingsboard.server.common.data.plugin.ComponentDescriptor; -import org.thingsboard.server.common.data.plugin.ComponentScope; -import org.thingsboard.server.common.data.plugin.ComponentType; -import org.thingsboard.server.dao.model.SearchTextEntity; -import org.thingsboard.server.dao.model.type.JsonCodec; - -import java.util.UUID; - -import static org.thingsboard.server.dao.model.ModelConstants.COMPONENT_DESCRIPTOR_ACTIONS_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.COMPONENT_DESCRIPTOR_CLASS_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.COMPONENT_DESCRIPTOR_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.COMPONENT_DESCRIPTOR_CONFIGURATION_DESCRIPTOR_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.COMPONENT_DESCRIPTOR_NAME_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.COMPONENT_DESCRIPTOR_SCOPE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.COMPONENT_DESCRIPTOR_TYPE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY; - -/** - * @author Andrew Shvayka - */ -@Table(name = COMPONENT_DESCRIPTOR_COLUMN_FAMILY_NAME) -public class ComponentDescriptorEntity implements SearchTextEntity { - - @PartitionKey - @Column(name = ID_PROPERTY) - private UUID id; - - @Column(name = COMPONENT_DESCRIPTOR_TYPE_PROPERTY) - private ComponentType type; - - @Column(name = COMPONENT_DESCRIPTOR_SCOPE_PROPERTY) - private ComponentScope scope; - - @Column(name = COMPONENT_DESCRIPTOR_NAME_PROPERTY) - private String name; - - @Column(name = COMPONENT_DESCRIPTOR_CLASS_PROPERTY) - private String clazz; - - @Column(name = COMPONENT_DESCRIPTOR_CONFIGURATION_DESCRIPTOR_PROPERTY, codec = JsonCodec.class) - private JsonNode configurationDescriptor; - - @Column(name = COMPONENT_DESCRIPTOR_ACTIONS_PROPERTY) - private String actions; - - @Column(name = SEARCH_TEXT_PROPERTY) - private String searchText; - - public ComponentDescriptorEntity() { - } - - public ComponentDescriptorEntity(ComponentDescriptor component) { - if (component.getId() != null) { - this.id = component.getId().getId(); - } - this.actions = component.getActions(); - this.type = component.getType(); - this.scope = component.getScope(); - this.name = component.getName(); - this.clazz = component.getClazz(); - this.configurationDescriptor = component.getConfigurationDescriptor(); - this.searchText = component.getName(); - } - - @Override - public ComponentDescriptor toData() { - ComponentDescriptor data = new ComponentDescriptor(new ComponentDescriptorId(id)); - data.setType(type); - data.setScope(scope); - data.setName(this.getName()); - data.setClazz(this.getClazz()); - data.setActions(this.getActions()); - data.setConfigurationDescriptor(this.getConfigurationDescriptor()); - return data; - } - - @Override - public UUID getUuid() { - return id; - } - - @Override - public void setUuid(UUID id) { - this.id = id; - } - - public String getActions() { - return actions; - } - - public void setActions(String actions) { - this.actions = actions; - } - - public ComponentType getType() { - return type; - } - - public void setType(ComponentType type) { - this.type = type; - } - - public ComponentScope getScope() { - return scope; - } - - public void setScope(ComponentScope scope) { - this.scope = scope; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getClazz() { - return clazz; - } - - public void setClazz(String clazz) { - this.clazz = clazz; - } - - public JsonNode getConfigurationDescriptor() { - return configurationDescriptor; - } - - public void setConfigurationDescriptor(JsonNode configurationDescriptor) { - this.configurationDescriptor = configurationDescriptor; - } - - public String getSearchText() { - return searchText; - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; - } - - @Override - public String getSearchTextSource() { - return getSearchText(); - } -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/CustomerEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/CustomerEntity.java deleted file mode 100644 index 2716679ec2..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/CustomerEntity.java +++ /dev/null @@ -1,243 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.nosql; - -import com.datastax.driver.core.utils.UUIDs; -import com.datastax.driver.mapping.annotations.Column; -import com.datastax.driver.mapping.annotations.PartitionKey; -import com.datastax.driver.mapping.annotations.Table; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.EqualsAndHashCode; -import lombok.ToString; -import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.model.SearchTextEntity; -import org.thingsboard.server.dao.model.type.JsonCodec; - -import java.util.UUID; - -import static org.thingsboard.server.dao.model.ModelConstants.ADDRESS2_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ADDRESS_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.CITY_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.COUNTRY_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_ADDITIONAL_INFO_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_TENANT_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_TITLE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.EMAIL_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.PHONE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.STATE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ZIP_PROPERTY; - -@Table(name = CUSTOMER_COLUMN_FAMILY_NAME) -@EqualsAndHashCode -@ToString -public final class CustomerEntity implements SearchTextEntity { - - @PartitionKey(value = 0) - @Column(name = ID_PROPERTY) - private UUID id; - - @PartitionKey(value = 1) - @Column(name = CUSTOMER_TENANT_ID_PROPERTY) - private UUID tenantId; - - @Column(name = CUSTOMER_TITLE_PROPERTY) - private String title; - - @Column(name = SEARCH_TEXT_PROPERTY) - private String searchText; - - @Column(name = COUNTRY_PROPERTY) - private String country; - - @Column(name = STATE_PROPERTY) - private String state; - - @Column(name = CITY_PROPERTY) - private String city; - - @Column(name = ADDRESS_PROPERTY) - private String address; - - @Column(name = ADDRESS2_PROPERTY) - private String address2; - - @Column(name = ZIP_PROPERTY) - private String zip; - - @Column(name = PHONE_PROPERTY) - private String phone; - - @Column(name = EMAIL_PROPERTY) - private String email; - - @Column(name = CUSTOMER_ADDITIONAL_INFO_PROPERTY, codec = JsonCodec.class) - private JsonNode additionalInfo; - - public CustomerEntity() { - super(); - } - - public CustomerEntity(Customer customer) { - if (customer.getId() != null) { - this.id = customer.getId().getId(); - } - this.tenantId = customer.getTenantId().getId(); - this.title = customer.getTitle(); - this.country = customer.getCountry(); - this.state = customer.getState(); - this.city = customer.getCity(); - this.address = customer.getAddress(); - this.address2 = customer.getAddress2(); - this.zip = customer.getZip(); - this.phone = customer.getPhone(); - this.email = customer.getEmail(); - this.additionalInfo = customer.getAdditionalInfo(); - } - - public UUID getUuid() { - return id; - } - - public void setUuid(UUID id) { - this.id = id; - } - - public UUID getTenantId() { - return tenantId; - } - - public void setTenantId(UUID tenantId) { - this.tenantId = tenantId; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getCountry() { - return country; - } - - public void setCountry(String country) { - this.country = country; - } - - public String getState() { - return state; - } - - public void setState(String state) { - this.state = state; - } - - public String getCity() { - return city; - } - - public void setCity(String city) { - this.city = city; - } - - public String getAddress() { - return address; - } - - public void setAddress(String address) { - this.address = address; - } - - public String getAddress2() { - return address2; - } - - public void setAddress2(String address2) { - this.address2 = address2; - } - - public String getZip() { - return zip; - } - - public void setZip(String zip) { - this.zip = zip; - } - - public String getPhone() { - return phone; - } - - public void setPhone(String phone) { - this.phone = phone; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public JsonNode getAdditionalInfo() { - return additionalInfo; - } - - public void setAdditionalInfo(JsonNode additionalInfo) { - this.additionalInfo = additionalInfo; - } - - @Override - public String getSearchTextSource() { - return getTitle(); - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; - } - - public String getSearchText() { - return searchText; - } - - @Override - public Customer toData() { - Customer customer = new Customer(new CustomerId(id)); - customer.setCreatedTime(UUIDs.unixTimestamp(id)); - customer.setTenantId(new TenantId(tenantId)); - customer.setTitle(title); - customer.setCountry(country); - customer.setState(state); - customer.setCity(city); - customer.setAddress(address); - customer.setAddress2(address2); - customer.setZip(zip); - customer.setPhone(phone); - customer.setEmail(email); - customer.setAdditionalInfo(additionalInfo); - return customer; - } - -} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardEntity.java deleted file mode 100644 index a48305f630..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardEntity.java +++ /dev/null @@ -1,174 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.nosql; - -import com.datastax.driver.core.utils.UUIDs; -import com.datastax.driver.mapping.annotations.Column; -import com.datastax.driver.mapping.annotations.PartitionKey; -import com.datastax.driver.mapping.annotations.Table; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.EqualsAndHashCode; -import lombok.ToString; -import lombok.extern.slf4j.Slf4j; -import org.springframework.util.StringUtils; -import org.thingsboard.server.common.data.Dashboard; -import org.thingsboard.server.common.data.ShortCustomerInfo; -import org.thingsboard.server.common.data.id.DashboardId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.model.SearchTextEntity; -import org.thingsboard.server.dao.model.type.JsonCodec; - -import java.io.IOException; -import java.util.HashSet; -import java.util.UUID; - -import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_ASSIGNED_CUSTOMERS_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_CONFIGURATION_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_TENANT_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_TITLE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY; - -@Table(name = DASHBOARD_COLUMN_FAMILY_NAME) -@EqualsAndHashCode -@ToString -@Slf4j -public final class DashboardEntity implements SearchTextEntity { - - private static final ObjectMapper objectMapper = new ObjectMapper(); - private static final JavaType assignedCustomersType = - objectMapper.getTypeFactory().constructCollectionType(HashSet.class, ShortCustomerInfo.class); - - @PartitionKey(value = 0) - @Column(name = ID_PROPERTY) - private UUID id; - - @PartitionKey(value = 1) - @Column(name = DASHBOARD_TENANT_ID_PROPERTY) - private UUID tenantId; - - @Column(name = DASHBOARD_TITLE_PROPERTY) - private String title; - - @Column(name = SEARCH_TEXT_PROPERTY) - private String searchText; - - @Column(name = DASHBOARD_ASSIGNED_CUSTOMERS_PROPERTY) - private String assignedCustomers; - - @Column(name = DASHBOARD_CONFIGURATION_PROPERTY, codec = JsonCodec.class) - private JsonNode configuration; - - public DashboardEntity() { - super(); - } - - public DashboardEntity(Dashboard dashboard) { - if (dashboard.getId() != null) { - this.id = dashboard.getId().getId(); - } - if (dashboard.getTenantId() != null) { - this.tenantId = dashboard.getTenantId().getId(); - } - this.title = dashboard.getTitle(); - if (dashboard.getAssignedCustomers() != null) { - try { - this.assignedCustomers = objectMapper.writeValueAsString(dashboard.getAssignedCustomers()); - } catch (JsonProcessingException e) { - log.error("Unable to serialize assigned customers to string!", e); - } - } - this.configuration = dashboard.getConfiguration(); - } - - public UUID getUuid() { - return id; - } - - public void setUuid(UUID id) { - this.id = id; - } - - public UUID getTenantId() { - return tenantId; - } - - public void setTenantId(UUID tenantId) { - this.tenantId = tenantId; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getAssignedCustomers() { - return assignedCustomers; - } - - public void setAssignedCustomers(String assignedCustomers) { - this.assignedCustomers = assignedCustomers; - } - - public JsonNode getConfiguration() { - return configuration; - } - - public void setConfiguration(JsonNode configuration) { - this.configuration = configuration; - } - - @Override - public String getSearchTextSource() { - return getTitle(); - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; - } - - public String getSearchText() { - return searchText; - } - - @Override - public Dashboard toData() { - Dashboard dashboard = new Dashboard(new DashboardId(id)); - dashboard.setCreatedTime(UUIDs.unixTimestamp(id)); - if (tenantId != null) { - dashboard.setTenantId(new TenantId(tenantId)); - } - dashboard.setTitle(title); - if (!StringUtils.isEmpty(assignedCustomers)) { - try { - dashboard.setAssignedCustomers(objectMapper.readValue(assignedCustomers, assignedCustomersType)); - } catch (IOException e) { - log.warn("Unable to parse assigned customers!", e); - } - } - dashboard.setConfiguration(configuration); - return dashboard; - } - -} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardInfoEntity.java deleted file mode 100644 index 69eb43744c..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardInfoEntity.java +++ /dev/null @@ -1,158 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.nosql; - -import com.datastax.driver.core.utils.UUIDs; -import com.datastax.driver.mapping.annotations.Column; -import com.datastax.driver.mapping.annotations.PartitionKey; -import com.datastax.driver.mapping.annotations.Table; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.EqualsAndHashCode; -import lombok.ToString; -import lombok.extern.slf4j.Slf4j; -import org.springframework.util.StringUtils; -import org.thingsboard.server.common.data.DashboardInfo; -import org.thingsboard.server.common.data.ShortCustomerInfo; -import org.thingsboard.server.common.data.id.DashboardId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.model.SearchTextEntity; - -import java.io.IOException; -import java.util.HashSet; -import java.util.UUID; - -import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_ASSIGNED_CUSTOMERS_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_TENANT_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_TITLE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY; - -@Table(name = DASHBOARD_COLUMN_FAMILY_NAME) -@EqualsAndHashCode -@ToString -@Slf4j -public class DashboardInfoEntity implements SearchTextEntity { - - private static final ObjectMapper objectMapper = new ObjectMapper(); - private static final JavaType assignedCustomersType = - objectMapper.getTypeFactory().constructCollectionType(HashSet.class, ShortCustomerInfo.class); - - @PartitionKey(value = 0) - @Column(name = ID_PROPERTY) - private UUID id; - - @PartitionKey(value = 1) - @Column(name = DASHBOARD_TENANT_ID_PROPERTY) - private UUID tenantId; - - @Column(name = DASHBOARD_TITLE_PROPERTY) - private String title; - - @Column(name = SEARCH_TEXT_PROPERTY) - private String searchText; - - @Column(name = DASHBOARD_ASSIGNED_CUSTOMERS_PROPERTY) - private String assignedCustomers; - - public DashboardInfoEntity() { - super(); - } - - public DashboardInfoEntity(DashboardInfo dashboardInfo) { - if (dashboardInfo.getId() != null) { - this.id = dashboardInfo.getId().getId(); - } - if (dashboardInfo.getTenantId() != null) { - this.tenantId = dashboardInfo.getTenantId().getId(); - } - this.title = dashboardInfo.getTitle(); - if (dashboardInfo.getAssignedCustomers() != null) { - try { - this.assignedCustomers = objectMapper.writeValueAsString(dashboardInfo.getAssignedCustomers()); - } catch (JsonProcessingException e) { - log.error("Unable to serialize assigned customers to string!", e); - } - } - } - - public UUID getUuid() { - return id; - } - - public void setUuid(UUID id) { - this.id = id; - } - - public UUID getTenantId() { - return tenantId; - } - - public void setTenantId(UUID tenantId) { - this.tenantId = tenantId; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getAssignedCustomers() { - return assignedCustomers; - } - - public void setAssignedCustomers(String assignedCustomers) { - this.assignedCustomers = assignedCustomers; - } - - @Override - public String getSearchTextSource() { - return getTitle(); - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; - } - - public String getSearchText() { - return searchText; - } - - @Override - public DashboardInfo toData() { - DashboardInfo dashboardInfo = new DashboardInfo(new DashboardId(id)); - dashboardInfo.setCreatedTime(UUIDs.unixTimestamp(id)); - if (tenantId != null) { - dashboardInfo.setTenantId(new TenantId(tenantId)); - } - dashboardInfo.setTitle(title); - if (!StringUtils.isEmpty(assignedCustomers)) { - try { - dashboardInfo.setAssignedCustomers(objectMapper.readValue(assignedCustomers, assignedCustomersType)); - } catch (IOException e) { - log.warn("Unable to parse assigned customers!", e); - } - } - return dashboardInfo; - } - -} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceCredentialsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceCredentialsEntity.java deleted file mode 100644 index 316386f7c9..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceCredentialsEntity.java +++ /dev/null @@ -1,130 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.nosql; - -import com.datastax.driver.core.utils.UUIDs; -import com.datastax.driver.mapping.annotations.Column; -import com.datastax.driver.mapping.annotations.PartitionKey; -import com.datastax.driver.mapping.annotations.Table; -import lombok.EqualsAndHashCode; -import lombok.ToString; -import org.thingsboard.server.common.data.id.DeviceCredentialsId; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.security.DeviceCredentials; -import org.thingsboard.server.common.data.security.DeviceCredentialsType; -import org.thingsboard.server.dao.model.BaseEntity; -import org.thingsboard.server.dao.model.type.DeviceCredentialsTypeCodec; - -import java.util.UUID; - -import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_CREDENTIALS_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_CREDENTIALS_CREDENTIALS_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_CREDENTIALS_CREDENTIALS_TYPE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_CREDENTIALS_CREDENTIALS_VALUE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_CREDENTIALS_DEVICE_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY; - -@Table(name = DEVICE_CREDENTIALS_COLUMN_FAMILY_NAME) -@EqualsAndHashCode -@ToString -public final class DeviceCredentialsEntity implements BaseEntity { - - @PartitionKey(value = 0) - @Column(name = ID_PROPERTY) - private UUID id; - - @Column(name = DEVICE_CREDENTIALS_DEVICE_ID_PROPERTY) - private UUID deviceId; - - @Column(name = DEVICE_CREDENTIALS_CREDENTIALS_TYPE_PROPERTY, codec = DeviceCredentialsTypeCodec.class) - private DeviceCredentialsType credentialsType; - - @Column(name = DEVICE_CREDENTIALS_CREDENTIALS_ID_PROPERTY) - private String credentialsId; - - @Column(name = DEVICE_CREDENTIALS_CREDENTIALS_VALUE_PROPERTY) - private String credentialsValue; - - public DeviceCredentialsEntity() { - super(); - } - - public DeviceCredentialsEntity(DeviceCredentials deviceCredentials) { - if (deviceCredentials.getId() != null) { - this.id = deviceCredentials.getId().getId(); - } - if (deviceCredentials.getDeviceId() != null) { - this.deviceId = deviceCredentials.getDeviceId().getId(); - } - this.credentialsType = deviceCredentials.getCredentialsType(); - this.credentialsId = deviceCredentials.getCredentialsId(); - this.credentialsValue = deviceCredentials.getCredentialsValue(); - } - - public UUID getUuid() { - return id; - } - - public void setUuid(UUID id) { - this.id = id; - } - - public UUID getDeviceId() { - return deviceId; - } - - public void setDeviceId(UUID deviceId) { - this.deviceId = deviceId; - } - - public DeviceCredentialsType getCredentialsType() { - return credentialsType; - } - - public void setCredentialsType(DeviceCredentialsType credentialsType) { - this.credentialsType = credentialsType; - } - - public String getCredentialsId() { - return credentialsId; - } - - public void setCredentialsId(String credentialsId) { - this.credentialsId = credentialsId; - } - - public String getCredentialsValue() { - return credentialsValue; - } - - public void setCredentialsValue(String credentialsValue) { - this.credentialsValue = credentialsValue; - } - - @Override - public DeviceCredentials toData() { - DeviceCredentials deviceCredentials = new DeviceCredentials(new DeviceCredentialsId(id)); - deviceCredentials.setCreatedTime(UUIDs.unixTimestamp(id)); - if (deviceId != null) { - deviceCredentials.setDeviceId(new DeviceId(deviceId)); - } - deviceCredentials.setCredentialsType(credentialsType); - deviceCredentials.setCredentialsId(credentialsId); - deviceCredentials.setCredentialsValue(credentialsValue); - return deviceCredentials; - } - -} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceEntity.java deleted file mode 100644 index 37fe148958..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceEntity.java +++ /dev/null @@ -1,168 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.nosql; - -import com.datastax.driver.core.utils.UUIDs; -import com.datastax.driver.mapping.annotations.Column; -import com.datastax.driver.mapping.annotations.PartitionKey; -import com.datastax.driver.mapping.annotations.Table; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.EqualsAndHashCode; -import lombok.ToString; -import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.model.SearchTextEntity; -import org.thingsboard.server.dao.model.type.JsonCodec; - -import java.util.UUID; - -import static org.thingsboard.server.dao.model.ModelConstants.*; - -@Table(name = DEVICE_COLUMN_FAMILY_NAME) -@EqualsAndHashCode -@ToString -public final class DeviceEntity implements SearchTextEntity { - - @PartitionKey(value = 0) - @Column(name = ID_PROPERTY) - private UUID id; - - @PartitionKey(value = 1) - @Column(name = DEVICE_TENANT_ID_PROPERTY) - private UUID tenantId; - - @PartitionKey(value = 2) - @Column(name = DEVICE_CUSTOMER_ID_PROPERTY) - private UUID customerId; - - @PartitionKey(value = 3) - @Column(name = DEVICE_TYPE_PROPERTY) - private String type; - - @Column(name = DEVICE_NAME_PROPERTY) - private String name; - - @Column(name = DEVICE_LABEL_PROPERTY) - private String label; - - @Column(name = SEARCH_TEXT_PROPERTY) - private String searchText; - - @Column(name = DEVICE_ADDITIONAL_INFO_PROPERTY, codec = JsonCodec.class) - private JsonNode additionalInfo; - - public DeviceEntity() { - super(); - } - - public DeviceEntity(Device device) { - if (device.getId() != null) { - this.id = device.getId().getId(); - } - if (device.getTenantId() != null) { - this.tenantId = device.getTenantId().getId(); - } - if (device.getCustomerId() != null) { - this.customerId = device.getCustomerId().getId(); - } - this.name = device.getName(); - this.type = device.getType(); - this.label = device.getLabel(); - this.additionalInfo = device.getAdditionalInfo(); - } - - public UUID getUuid() { - return id; - } - - public void setUuid(UUID id) { - this.id = id; - } - - public UUID getTenantId() { - return tenantId; - } - - public void setTenantId(UUID tenantId) { - this.tenantId = tenantId; - } - - public UUID getCustomerId() { - return customerId; - } - - public void setCustomerId(UUID customerId) { - this.customerId = customerId; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public JsonNode getAdditionalInfo() { - return additionalInfo; - } - - public void setAdditionalInfo(JsonNode additionalInfo) { - this.additionalInfo = additionalInfo; - } - - @Override - public String getSearchTextSource() { - return getName(); - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; - } - - public String getSearchText() { - return searchText; - } - - @Override - public Device toData() { - Device device = new Device(new DeviceId(id)); - device.setCreatedTime(UUIDs.unixTimestamp(id)); - if (tenantId != null) { - device.setTenantId(new TenantId(tenantId)); - } - if (customerId != null) { - device.setCustomerId(new CustomerId(customerId)); - } - device.setName(name); - device.setType(type); - device.setLabel(label); - device.setAdditionalInfo(additionalInfo); - return device; - } - -} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EntityViewEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EntityViewEntity.java deleted file mode 100644 index 5cf13665b4..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EntityViewEntity.java +++ /dev/null @@ -1,174 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.nosql; - -import com.datastax.driver.core.utils.UUIDs; -import com.datastax.driver.mapping.annotations.Column; -import com.datastax.driver.mapping.annotations.PartitionKey; -import com.datastax.driver.mapping.annotations.Table; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.ToString; -import lombok.extern.slf4j.Slf4j; -import org.hibernate.annotations.Type; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.EntityView; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.EntityIdFactory; -import org.thingsboard.server.common.data.id.EntityViewId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.objects.TelemetryEntityView; -import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.SearchTextEntity; - -import javax.persistence.EnumType; -import javax.persistence.Enumerated; -import java.io.IOException; -import java.util.UUID; - -import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_TYPE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_TYPE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_TABLE_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY; - -/** - * Created by Victor Basanets on 8/31/2017. - */ -@Data -@Table(name = ENTITY_VIEW_TABLE_FAMILY_NAME) -@EqualsAndHashCode -@ToString -@Slf4j -public class EntityViewEntity implements SearchTextEntity { - - @PartitionKey(value = 0) - @Column(name = ID_PROPERTY) - private UUID id; - - @PartitionKey(value = 1) - @Column(name = ModelConstants.ENTITY_VIEW_TENANT_ID_PROPERTY) - private UUID tenantId; - - @PartitionKey(value = 2) - @Column(name = ModelConstants.ENTITY_VIEW_CUSTOMER_ID_PROPERTY) - private UUID customerId; - - @PartitionKey(value = 3) - @Column(name = DEVICE_TYPE_PROPERTY) - private String type; - - @Enumerated(EnumType.STRING) - @Column(name = ENTITY_TYPE_PROPERTY) - private EntityType entityType; - - @Column(name = ModelConstants.ENTITY_VIEW_ENTITY_ID_PROPERTY) - private UUID entityId; - - @Column(name = ModelConstants.ENTITY_VIEW_NAME_PROPERTY) - private String name; - - @Column(name = ModelConstants.ENTITY_VIEW_KEYS_PROPERTY) - private String keys; - - @Column(name = ModelConstants.ENTITY_VIEW_START_TS_PROPERTY) - private long startTs; - - @Column(name = ModelConstants.ENTITY_VIEW_END_TS_PROPERTY) - private long endTs; - - @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) - private String searchText; - - @Type(type = "json") - @Column(name = ModelConstants.ENTITY_VIEW_ADDITIONAL_INFO_PROPERTY) - private JsonNode additionalInfo; - - private static final ObjectMapper mapper = new ObjectMapper(); - - public EntityViewEntity() { - super(); - } - - public EntityViewEntity(EntityView entityView) { - if (entityView.getId() != null) { - this.id = entityView.getId().getId(); - } - if (entityView.getEntityId() != null) { - this.entityId = entityView.getEntityId().getId(); - this.entityType = entityView.getEntityId().getEntityType(); - } - if (entityView.getTenantId() != null) { - this.tenantId = entityView.getTenantId().getId(); - } - if (entityView.getCustomerId() != null) { - this.customerId = entityView.getCustomerId().getId(); - } - this.type = entityView.getType(); - this.name = entityView.getName(); - try { - this.keys = mapper.writeValueAsString(entityView.getKeys()); - } catch (IOException e) { - log.error("Unable to serialize entity view keys!", e); - } - this.startTs = entityView.getStartTimeMs(); - this.endTs = entityView.getEndTimeMs(); - this.searchText = entityView.getSearchText(); - this.additionalInfo = entityView.getAdditionalInfo(); - } - - @Override - public String getSearchTextSource() { - return name; - } - - @Override - public EntityView toData() { - EntityView entityView = new EntityView(new EntityViewId(id)); - entityView.setCreatedTime(UUIDs.unixTimestamp(id)); - if (entityId != null) { - entityView.setEntityId(EntityIdFactory.getByTypeAndId(entityType.name(), entityId.toString())); - } - if (tenantId != null) { - entityView.setTenantId(new TenantId(tenantId)); - } - if (customerId != null) { - entityView.setCustomerId(new CustomerId(customerId)); - } - entityView.setType(type); - entityView.setName(name); - try { - entityView.setKeys(mapper.readValue(keys, TelemetryEntityView.class)); - } catch (IOException e) { - log.error("Unable to read entity view keys!", e); - } - entityView.setStartTimeMs(startTs); - entityView.setEndTimeMs(endTs); - entityView.setAdditionalInfo(additionalInfo); - return entityView; - } - - @Override - public UUID getUuid() { - return getId(); - } - - @Override - public void setUuid(UUID id) { - this.id = id; - } -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EventEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EventEntity.java deleted file mode 100644 index 8c753a743a..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EventEntity.java +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.nosql; - -import com.datastax.driver.core.utils.UUIDs; -import com.datastax.driver.mapping.annotations.ClusteringColumn; -import com.datastax.driver.mapping.annotations.Column; -import com.datastax.driver.mapping.annotations.PartitionKey; -import com.datastax.driver.mapping.annotations.Table; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.Event; -import org.thingsboard.server.common.data.id.EntityIdFactory; -import org.thingsboard.server.common.data.id.EventId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.model.BaseEntity; -import org.thingsboard.server.dao.model.type.EntityTypeCodec; -import org.thingsboard.server.dao.model.type.JsonCodec; - -import java.util.UUID; - -import static org.thingsboard.server.dao.model.ModelConstants.EVENT_BODY_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.EVENT_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.EVENT_ENTITY_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.EVENT_ENTITY_TYPE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.EVENT_TENANT_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.EVENT_TYPE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.EVENT_UID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY; - -/** - * @author Andrew Shvayka - */ -@Data -@NoArgsConstructor -@Table(name = EVENT_COLUMN_FAMILY_NAME) -public class EventEntity implements BaseEntity { - - @Column(name = ID_PROPERTY) - private UUID id; - - @PartitionKey() - @Column(name = EVENT_TENANT_ID_PROPERTY) - private UUID tenantId; - - @PartitionKey(value = 1) - @Column(name = EVENT_ENTITY_TYPE_PROPERTY, codec = EntityTypeCodec.class) - private EntityType entityType; - - @PartitionKey(value = 2) - @Column(name = EVENT_ENTITY_ID_PROPERTY) - private UUID entityId; - - @ClusteringColumn() - @Column(name = EVENT_TYPE_PROPERTY) - private String eventType; - - @ClusteringColumn(value = 1) - @Column(name = EVENT_UID_PROPERTY) - private String eventUid; - - @Column(name = EVENT_BODY_PROPERTY, codec = JsonCodec.class) - private JsonNode body; - - public EventEntity(Event event) { - if (event.getId() != null) { - this.id = event.getId().getId(); - } - if (event.getTenantId() != null) { - this.tenantId = event.getTenantId().getId(); - } - if (event.getEntityId() != null) { - this.entityType = event.getEntityId().getEntityType(); - this.entityId = event.getEntityId().getId(); - } - this.eventType = event.getType(); - this.eventUid = event.getUid(); - this.body = event.getBody(); - } - - @Override - public UUID getUuid() { - return id; - } - - @Override - public void setUuid(UUID id) { - this.id = id; - } - - @Override - public Event toData() { - Event event = new Event(new EventId(id)); - event.setCreatedTime(UUIDs.unixTimestamp(id)); - event.setTenantId(new TenantId(tenantId)); - event.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId)); - event.setBody(body); - event.setType(eventType); - event.setUid(eventUid); - return event; - } -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/RuleChainEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/RuleChainEntity.java deleted file mode 100644 index 7021ff708e..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/RuleChainEntity.java +++ /dev/null @@ -1,182 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.nosql; - -import com.datastax.driver.core.utils.UUIDs; -import com.datastax.driver.mapping.annotations.ClusteringColumn; -import com.datastax.driver.mapping.annotations.Column; -import com.datastax.driver.mapping.annotations.PartitionKey; -import com.datastax.driver.mapping.annotations.Table; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; -import org.thingsboard.server.common.data.id.RuleChainId; -import org.thingsboard.server.common.data.id.RuleNodeId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.rule.RuleChain; -import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.model.SearchTextEntity; -import org.thingsboard.server.dao.model.type.JsonCodec; - -import java.util.UUID; - -import static org.thingsboard.server.dao.model.ModelConstants.ADDITIONAL_INFO_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.DEBUG_MODE; -import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.RULE_CHAIN_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.RULE_CHAIN_CONFIGURATION_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.RULE_CHAIN_FIRST_RULE_NODE_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.RULE_CHAIN_NAME_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.RULE_CHAIN_ROOT_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.RULE_CHAIN_TENANT_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY; - -@Table(name = RULE_CHAIN_COLUMN_FAMILY_NAME) -@EqualsAndHashCode -@ToString -public class RuleChainEntity implements SearchTextEntity { - - @PartitionKey - @Column(name = ID_PROPERTY) - private UUID id; - @ClusteringColumn - @Column(name = RULE_CHAIN_TENANT_ID_PROPERTY) - private UUID tenantId; - @Column(name = RULE_CHAIN_NAME_PROPERTY) - private String name; - @Column(name = SEARCH_TEXT_PROPERTY) - private String searchText; - @Column(name = RULE_CHAIN_FIRST_RULE_NODE_ID_PROPERTY) - private UUID firstRuleNodeId; - @Column(name = RULE_CHAIN_ROOT_PROPERTY) - private boolean root; - @Getter - @Setter - @Column(name = DEBUG_MODE) - private boolean debugMode; - @Column(name = RULE_CHAIN_CONFIGURATION_PROPERTY, codec = JsonCodec.class) - private JsonNode configuration; - @Column(name = ADDITIONAL_INFO_PROPERTY, codec = JsonCodec.class) - private JsonNode additionalInfo; - - public RuleChainEntity() { - } - - public RuleChainEntity(RuleChain ruleChain) { - if (ruleChain.getId() != null) { - this.id = ruleChain.getUuidId(); - } - this.tenantId = DaoUtil.getId(ruleChain.getTenantId()); - this.name = ruleChain.getName(); - this.searchText = ruleChain.getName(); - this.firstRuleNodeId = DaoUtil.getId(ruleChain.getFirstRuleNodeId()); - this.root = ruleChain.isRoot(); - this.debugMode = ruleChain.isDebugMode(); - this.configuration = ruleChain.getConfiguration(); - this.additionalInfo = ruleChain.getAdditionalInfo(); - } - - @Override - public String getSearchTextSource() { - return getSearchText(); - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; - } - - @Override - public UUID getUuid() { - return id; - } - - @Override - public void setUuid(UUID id) { - this.id = id; - } - - public UUID getTenantId() { - return tenantId; - } - - public void setTenantId(UUID tenantId) { - this.tenantId = tenantId; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public UUID getFirstRuleNodeId() { - return firstRuleNodeId; - } - - public void setFirstRuleNodeId(UUID firstRuleNodeId) { - this.firstRuleNodeId = firstRuleNodeId; - } - - public boolean isRoot() { - return root; - } - - public void setRoot(boolean root) { - this.root = root; - } - - public String getSearchText() { - return searchText; - } - - public JsonNode getConfiguration() { - return configuration; - } - - public void setConfiguration(JsonNode configuration) { - this.configuration = configuration; - } - - public JsonNode getAdditionalInfo() { - return additionalInfo; - } - - public void setAdditionalInfo(JsonNode additionalInfo) { - this.additionalInfo = additionalInfo; - } - - @Override - public RuleChain toData() { - RuleChain ruleChain = new RuleChain(new RuleChainId(id)); - ruleChain.setCreatedTime(UUIDs.unixTimestamp(id)); - ruleChain.setTenantId(new TenantId(tenantId)); - ruleChain.setName(name); - if (this.firstRuleNodeId != null) { - ruleChain.setFirstRuleNodeId(new RuleNodeId(this.firstRuleNodeId)); - } - ruleChain.setRoot(this.root); - ruleChain.setDebugMode(this.debugMode); - ruleChain.setConfiguration(this.configuration); - ruleChain.setAdditionalInfo(this.additionalInfo); - return ruleChain; - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/RuleNodeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/RuleNodeEntity.java deleted file mode 100644 index 9a086fe568..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/RuleNodeEntity.java +++ /dev/null @@ -1,167 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.nosql; - -import com.datastax.driver.core.utils.UUIDs; -import com.datastax.driver.mapping.annotations.Column; -import com.datastax.driver.mapping.annotations.PartitionKey; -import com.datastax.driver.mapping.annotations.Table; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; -import org.thingsboard.server.common.data.id.RuleChainId; -import org.thingsboard.server.common.data.id.RuleNodeId; -import org.thingsboard.server.common.data.rule.RuleNode; -import org.thingsboard.server.dao.model.SearchTextEntity; -import org.thingsboard.server.dao.model.type.JsonCodec; - -import java.util.UUID; - -import static org.thingsboard.server.dao.model.ModelConstants.ADDITIONAL_INFO_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.DEBUG_MODE; -import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.RULE_NODE_CHAIN_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.RULE_NODE_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.RULE_NODE_CONFIGURATION_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.RULE_NODE_NAME_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.RULE_NODE_TYPE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY; - -@Table(name = RULE_NODE_COLUMN_FAMILY_NAME) -@EqualsAndHashCode -@ToString -public class RuleNodeEntity implements SearchTextEntity { - - @PartitionKey - @Column(name = ID_PROPERTY) - private UUID id; - @Column(name = RULE_NODE_CHAIN_ID_PROPERTY) - private UUID ruleChainId; - @Column(name = RULE_NODE_TYPE_PROPERTY) - private String type; - @Column(name = RULE_NODE_NAME_PROPERTY) - private String name; - @Column(name = SEARCH_TEXT_PROPERTY) - private String searchText; - @Column(name = RULE_NODE_CONFIGURATION_PROPERTY, codec = JsonCodec.class) - private JsonNode configuration; - @Column(name = ADDITIONAL_INFO_PROPERTY, codec = JsonCodec.class) - private JsonNode additionalInfo; - @Getter - @Setter - @Column(name = DEBUG_MODE) - private boolean debugMode; - - public RuleNodeEntity() { - } - - public RuleNodeEntity(RuleNode ruleNode) { - if (ruleNode.getId() != null) { - this.id = ruleNode.getUuidId(); - } - if (ruleNode.getRuleChainId() != null) { - this.ruleChainId = ruleNode.getRuleChainId().getId(); - } - this.type = ruleNode.getType(); - this.name = ruleNode.getName(); - this.debugMode = ruleNode.isDebugMode(); - this.searchText = ruleNode.getName(); - this.configuration = ruleNode.getConfiguration(); - this.additionalInfo = ruleNode.getAdditionalInfo(); - } - - @Override - public String getSearchTextSource() { - return getSearchText(); - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; - } - - @Override - public UUID getUuid() { - return id; - } - - @Override - public void setUuid(UUID id) { - this.id = id; - } - - public UUID getRuleChainId() { - return ruleChainId; - } - - public void setRuleChainId(UUID ruleChainId) { - this.ruleChainId = ruleChainId; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getSearchText() { - return searchText; - } - - public JsonNode getConfiguration() { - return configuration; - } - - public void setConfiguration(JsonNode configuration) { - this.configuration = configuration; - } - - public JsonNode getAdditionalInfo() { - return additionalInfo; - } - - public void setAdditionalInfo(JsonNode additionalInfo) { - this.additionalInfo = additionalInfo; - } - - @Override - public RuleNode toData() { - RuleNode ruleNode = new RuleNode(new RuleNodeId(id)); - ruleNode.setCreatedTime(UUIDs.unixTimestamp(id)); - if (this.ruleChainId != null) { - ruleNode.setRuleChainId(new RuleChainId(this.ruleChainId)); - } - ruleNode.setType(this.type); - ruleNode.setName(this.name); - ruleNode.setDebugMode(this.debugMode); - ruleNode.setConfiguration(this.configuration); - ruleNode.setAdditionalInfo(this.additionalInfo); - return ruleNode; - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/TenantEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/TenantEntity.java deleted file mode 100644 index cae31c2a31..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/TenantEntity.java +++ /dev/null @@ -1,269 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.nosql; - -import com.datastax.driver.core.utils.UUIDs; -import com.datastax.driver.mapping.annotations.Column; -import com.datastax.driver.mapping.annotations.PartitionKey; -import com.datastax.driver.mapping.annotations.Table; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.EqualsAndHashCode; -import lombok.ToString; -import org.thingsboard.server.common.data.Tenant; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.SearchTextEntity; -import org.thingsboard.server.dao.model.type.JsonCodec; - -import java.util.UUID; - -import static org.thingsboard.server.dao.model.ModelConstants.ADDRESS2_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ADDRESS_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.CITY_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.COUNTRY_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.EMAIL_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.PHONE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.STATE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.TENANT_ADDITIONAL_INFO_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.TENANT_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.TENANT_REGION_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.TENANT_TITLE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ZIP_PROPERTY; - -@Table(name = TENANT_COLUMN_FAMILY_NAME) -@EqualsAndHashCode -@ToString -public final class TenantEntity implements SearchTextEntity { - - @PartitionKey(value = 0) - @Column(name = ID_PROPERTY) - private UUID id; - - @Column(name = TENANT_TITLE_PROPERTY) - private String title; - - @Column(name = SEARCH_TEXT_PROPERTY) - private String searchText; - - @Column(name = TENANT_REGION_PROPERTY) - private String region; - - @Column(name = COUNTRY_PROPERTY) - private String country; - - @Column(name = STATE_PROPERTY) - private String state; - - @Column(name = CITY_PROPERTY) - private String city; - - @Column(name = ADDRESS_PROPERTY) - private String address; - - @Column(name = ADDRESS2_PROPERTY) - private String address2; - - @Column(name = ZIP_PROPERTY) - private String zip; - - @Column(name = PHONE_PROPERTY) - private String phone; - - @Column(name = EMAIL_PROPERTY) - private String email; - - @Column(name = TENANT_ADDITIONAL_INFO_PROPERTY, codec = JsonCodec.class) - private JsonNode additionalInfo; - - @Column(name = ModelConstants.TENANT_ISOLATED_TB_CORE) - private boolean isolatedTbCore; - - @Column(name = ModelConstants.TENANT_ISOLATED_TB_RULE_ENGINE) - private boolean isolatedTbRuleEngine; - - public TenantEntity() { - super(); - } - - public TenantEntity(Tenant tenant) { - if (tenant.getId() != null) { - this.id = tenant.getId().getId(); - } - this.title = tenant.getTitle(); - this.region = tenant.getRegion(); - this.country = tenant.getCountry(); - this.state = tenant.getState(); - this.city = tenant.getCity(); - this.address = tenant.getAddress(); - this.address2 = tenant.getAddress2(); - this.zip = tenant.getZip(); - this.phone = tenant.getPhone(); - this.email = tenant.getEmail(); - this.additionalInfo = tenant.getAdditionalInfo(); - this.isolatedTbCore = tenant.isIsolatedTbCore(); - this.isolatedTbRuleEngine = tenant.isIsolatedTbRuleEngine(); - } - - public UUID getUuid() { - return id; - } - - public void setUuid(UUID id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getRegion() { - return region; - } - - public void setRegion(String region) { - this.region = region; - } - - public String getCountry() { - return country; - } - - public void setCountry(String country) { - this.country = country; - } - - public String getState() { - return state; - } - - public void setState(String state) { - this.state = state; - } - - public String getCity() { - return city; - } - - public void setCity(String city) { - this.city = city; - } - - public String getAddress() { - return address; - } - - public void setAddress(String address) { - this.address = address; - } - - public String getAddress2() { - return address2; - } - - public void setAddress2(String address2) { - this.address2 = address2; - } - - public String getZip() { - return zip; - } - - public void setZip(String zip) { - this.zip = zip; - } - - public String getPhone() { - return phone; - } - - public void setPhone(String phone) { - this.phone = phone; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public JsonNode getAdditionalInfo() { - return additionalInfo; - } - - public void setAdditionalInfo(JsonNode additionalInfo) { - this.additionalInfo = additionalInfo; - } - - public boolean isIsolatedTbCore() { - return isolatedTbCore; - } - - public void setIsolatedTbCore(boolean isolatedTbCore) { - this.isolatedTbCore = isolatedTbCore; - } - - public boolean isIsolatedTbRuleEngine() { - return isolatedTbRuleEngine; - } - - public void setIsolatedTbRuleEngine(boolean isolatedTbRuleEngine) { - this.isolatedTbRuleEngine = isolatedTbRuleEngine; - } - - @Override - public String getSearchTextSource() { - return getTitle(); - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; - } - - public String getSearchText() { - return searchText; - } - - @Override - public Tenant toData() { - Tenant tenant = new Tenant(new TenantId(id)); - tenant.setCreatedTime(UUIDs.unixTimestamp(id)); - tenant.setTitle(title); - tenant.setRegion(region); - tenant.setCountry(country); - tenant.setState(state); - tenant.setCity(city); - tenant.setAddress(address); - tenant.setAddress2(address2); - tenant.setZip(zip); - tenant.setPhone(phone); - tenant.setEmail(email); - tenant.setAdditionalInfo(additionalInfo); - tenant.setIsolatedTbCore(isolatedTbCore); - tenant.setIsolatedTbRuleEngine(isolatedTbRuleEngine); - return tenant; - } - - -} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/UserCredentialsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/UserCredentialsEntity.java deleted file mode 100644 index 6854d61f39..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/UserCredentialsEntity.java +++ /dev/null @@ -1,140 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.nosql; - -import com.datastax.driver.core.utils.UUIDs; -import com.datastax.driver.mapping.annotations.Column; -import com.datastax.driver.mapping.annotations.PartitionKey; -import com.datastax.driver.mapping.annotations.Table; -import lombok.EqualsAndHashCode; -import org.thingsboard.server.common.data.id.UserCredentialsId; -import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.security.UserCredentials; -import org.thingsboard.server.dao.model.BaseEntity; - -import java.util.UUID; - -import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.USER_CREDENTIALS_ACTIVATE_TOKEN_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.USER_CREDENTIALS_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.USER_CREDENTIALS_ENABLED_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.USER_CREDENTIALS_PASSWORD_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.USER_CREDENTIALS_RESET_TOKEN_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.USER_CREDENTIALS_USER_ID_PROPERTY; - -@Table(name = USER_CREDENTIALS_COLUMN_FAMILY_NAME) -@EqualsAndHashCode -public final class UserCredentialsEntity implements BaseEntity { - - @PartitionKey(value = 0) - @Column(name = ID_PROPERTY) - private UUID id; - - @Column(name = USER_CREDENTIALS_USER_ID_PROPERTY) - private UUID userId; - - @Column(name = USER_CREDENTIALS_ENABLED_PROPERTY) - private boolean enabled; - - @Column(name = USER_CREDENTIALS_PASSWORD_PROPERTY) - private String password; - - @Column(name = USER_CREDENTIALS_ACTIVATE_TOKEN_PROPERTY) - private String activateToken; - - @Column(name = USER_CREDENTIALS_RESET_TOKEN_PROPERTY) - private String resetToken; - - public UserCredentialsEntity() { - super(); - } - - public UserCredentialsEntity(UserCredentials userCredentials) { - if (userCredentials.getId() != null) { - this.id = userCredentials.getId().getId(); - } - if (userCredentials.getUserId() != null) { - this.userId = userCredentials.getUserId().getId(); - } - this.enabled = userCredentials.isEnabled(); - this.password = userCredentials.getPassword(); - this.activateToken = userCredentials.getActivateToken(); - this.resetToken = userCredentials.getResetToken(); - } - - public UUID getUuid() { - return id; - } - - public void setUuid(UUID id) { - this.id = id; - } - - public UUID getUserId() { - return userId; - } - - public void setUserId(UUID userId) { - this.userId = userId; - } - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - public String getActivateToken() { - return activateToken; - } - - public void setActivateToken(String activateToken) { - this.activateToken = activateToken; - } - - public String getResetToken() { - return resetToken; - } - - public void setResetToken(String resetToken) { - this.resetToken = resetToken; - } - - @Override - public UserCredentials toData() { - UserCredentials userCredentials = new UserCredentials(new UserCredentialsId(id)); - userCredentials.setCreatedTime(UUIDs.unixTimestamp(id)); - if (userId != null) { - userCredentials.setUserId(new UserId(userId)); - } - userCredentials.setEnabled(enabled); - userCredentials.setPassword(password); - userCredentials.setActivateToken(activateToken); - userCredentials.setResetToken(resetToken); - return userCredentials; - } - -} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/UserEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/UserEntity.java deleted file mode 100644 index 0c7625da78..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/UserEntity.java +++ /dev/null @@ -1,200 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.nosql; - -import com.datastax.driver.core.utils.UUIDs; -import com.datastax.driver.mapping.annotations.Column; -import com.datastax.driver.mapping.annotations.PartitionKey; -import com.datastax.driver.mapping.annotations.Table; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.EqualsAndHashCode; -import lombok.ToString; -import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.security.Authority; -import org.thingsboard.server.dao.model.SearchTextEntity; -import org.thingsboard.server.dao.model.type.AuthorityCodec; -import org.thingsboard.server.dao.model.type.JsonCodec; - -import java.util.UUID; - -import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.USER_ADDITIONAL_INFO_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.USER_AUTHORITY_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.USER_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.USER_CUSTOMER_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.USER_EMAIL_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.USER_FIRST_NAME_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.USER_LAST_NAME_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.USER_TENANT_ID_PROPERTY; - -@Table(name = USER_COLUMN_FAMILY_NAME) -@EqualsAndHashCode -@ToString -public final class UserEntity implements SearchTextEntity { - - @PartitionKey(value = 0) - @Column(name = ID_PROPERTY) - private UUID id; - - @PartitionKey(value = 1) - @Column(name = USER_TENANT_ID_PROPERTY) - private UUID tenantId; - - @PartitionKey(value = 2) - @Column(name = USER_CUSTOMER_ID_PROPERTY) - private UUID customerId; - - @PartitionKey(value = 3) - @Column(name = USER_AUTHORITY_PROPERTY, codec = AuthorityCodec.class) - private Authority authority; - - @Column(name = USER_EMAIL_PROPERTY) - private String email; - - @Column(name = SEARCH_TEXT_PROPERTY) - private String searchText; - - @Column(name = USER_FIRST_NAME_PROPERTY) - private String firstName; - - @Column(name = USER_LAST_NAME_PROPERTY) - private String lastName; - - @Column(name = USER_ADDITIONAL_INFO_PROPERTY, codec = JsonCodec.class) - private JsonNode additionalInfo; - - public UserEntity() { - super(); - } - - public UserEntity(User user) { - if (user.getId() != null) { - this.id = user.getId().getId(); - } - this.authority = user.getAuthority(); - if (user.getTenantId() != null) { - this.tenantId = user.getTenantId().getId(); - } - if (user.getCustomerId() != null) { - this.customerId = user.getCustomerId().getId(); - } - this.email = user.getEmail(); - this.firstName = user.getFirstName(); - this.lastName = user.getLastName(); - this.additionalInfo = user.getAdditionalInfo(); - } - - public UUID getUuid() { - return id; - } - - public void setUuid(UUID id) { - this.id = id; - } - - public Authority getAuthority() { - return authority; - } - - public void setAuthority(Authority authority) { - this.authority = authority; - } - - public UUID getTenantId() { - return tenantId; - } - - public void setTenantId(UUID tenantId) { - this.tenantId = tenantId; - } - - public UUID getCustomerId() { - return customerId; - } - - public void setCustomerId(UUID customerId) { - this.customerId = customerId; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getFirstName() { - return firstName; - } - - public void setFirstName(String firstName) { - this.firstName = firstName; - } - - public String getLastName() { - return lastName; - } - - public void setLastName(String lastName) { - this.lastName = lastName; - } - - public JsonNode getAdditionalInfo() { - return additionalInfo; - } - - public void setAdditionalInfo(JsonNode additionalInfo) { - this.additionalInfo = additionalInfo; - } - - @Override - public String getSearchTextSource() { - return getEmail(); - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; - } - - public String getSearchText() { - return searchText; - } - - @Override - public User toData() { - User user = new User(new UserId(id)); - user.setCreatedTime(UUIDs.unixTimestamp(id)); - user.setAuthority(authority); - if (tenantId != null) { - user.setTenantId(new TenantId(tenantId)); - } - if (customerId != null) { - user.setCustomerId(new CustomerId(customerId)); - } - user.setEmail(email); - user.setFirstName(firstName); - user.setLastName(lastName); - user.setAdditionalInfo(additionalInfo); - return user; - } - -} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/WidgetTypeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/WidgetTypeEntity.java deleted file mode 100644 index 24ac3980cf..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/WidgetTypeEntity.java +++ /dev/null @@ -1,148 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.nosql; - -import com.datastax.driver.core.utils.UUIDs; -import com.datastax.driver.mapping.annotations.Column; -import com.datastax.driver.mapping.annotations.PartitionKey; -import com.datastax.driver.mapping.annotations.Table; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.EqualsAndHashCode; -import lombok.ToString; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.WidgetTypeId; -import org.thingsboard.server.common.data.widget.WidgetType; -import org.thingsboard.server.dao.model.BaseEntity; -import org.thingsboard.server.dao.model.type.JsonCodec; - -import java.util.UUID; - -import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_ALIAS_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_BUNDLE_ALIAS_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_DESCRIPTOR_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_NAME_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_TENANT_ID_PROPERTY; - -@Table(name = WIDGET_TYPE_COLUMN_FAMILY_NAME) -@EqualsAndHashCode -@ToString -public final class WidgetTypeEntity implements BaseEntity { - - @PartitionKey(value = 0) - @Column(name = ID_PROPERTY) - private UUID id; - - @PartitionKey(value = 1) - @Column(name = WIDGET_TYPE_TENANT_ID_PROPERTY) - private UUID tenantId; - - @PartitionKey(value = 2) - @Column(name = WIDGET_TYPE_BUNDLE_ALIAS_PROPERTY) - private String bundleAlias; - - @Column(name = WIDGET_TYPE_ALIAS_PROPERTY) - private String alias; - - @Column(name = WIDGET_TYPE_NAME_PROPERTY) - private String name; - - @Column(name = WIDGET_TYPE_DESCRIPTOR_PROPERTY, codec = JsonCodec.class) - private JsonNode descriptor; - - public WidgetTypeEntity() { - super(); - } - - public WidgetTypeEntity(WidgetType widgetType) { - if (widgetType.getId() != null) { - this.id = widgetType.getId().getId(); - } - if (widgetType.getTenantId() != null) { - this.tenantId = widgetType.getTenantId().getId(); - } - this.bundleAlias = widgetType.getBundleAlias(); - this.alias = widgetType.getAlias(); - this.name = widgetType.getName(); - this.descriptor = widgetType.getDescriptor(); - } - - @Override - public UUID getUuid() { - return id; - } - - @Override - public void setUuid(UUID id) { - this.id = id; - } - - public UUID getTenantId() { - return tenantId; - } - - public void setTenantId(UUID tenantId) { - this.tenantId = tenantId; - } - - public String getBundleAlias() { - return bundleAlias; - } - - public void setBundleAlias(String bundleAlias) { - this.bundleAlias = bundleAlias; - } - - public String getAlias() { - return alias; - } - - public void setAlias(String alias) { - this.alias = alias; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public JsonNode getDescriptor() { - return descriptor; - } - - public void setDescriptor(JsonNode descriptor) { - this.descriptor = descriptor; - } - - @Override - public WidgetType toData() { - WidgetType widgetType = new WidgetType(new WidgetTypeId(id)); - widgetType.setCreatedTime(UUIDs.unixTimestamp(id)); - if (tenantId != null) { - widgetType.setTenantId(new TenantId(tenantId)); - } - widgetType.setBundleAlias(bundleAlias); - widgetType.setAlias(alias); - widgetType.setName(name); - widgetType.setDescriptor(descriptor); - return widgetType; - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/WidgetsBundleEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/WidgetsBundleEntity.java deleted file mode 100644 index 8c11c05685..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/WidgetsBundleEntity.java +++ /dev/null @@ -1,156 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.nosql; - - -import com.datastax.driver.core.utils.UUIDs; -import com.datastax.driver.mapping.annotations.Column; -import com.datastax.driver.mapping.annotations.PartitionKey; -import com.datastax.driver.mapping.annotations.Table; -import lombok.EqualsAndHashCode; -import lombok.ToString; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.WidgetsBundleId; -import org.thingsboard.server.common.data.widget.WidgetsBundle; -import org.thingsboard.server.dao.model.SearchTextEntity; - -import java.nio.ByteBuffer; -import java.util.UUID; - -import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.WIDGETS_BUNDLE_ALIAS_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.WIDGETS_BUNDLE_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.WIDGETS_BUNDLE_IMAGE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.WIDGETS_BUNDLE_TENANT_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.WIDGETS_BUNDLE_TITLE_PROPERTY; - -@Table(name = WIDGETS_BUNDLE_COLUMN_FAMILY_NAME) -@EqualsAndHashCode -@ToString -public final class WidgetsBundleEntity implements SearchTextEntity { - - @PartitionKey(value = 0) - @Column(name = ID_PROPERTY) - private UUID id; - - @PartitionKey(value = 1) - @Column(name = WIDGETS_BUNDLE_TENANT_ID_PROPERTY) - private UUID tenantId; - - @Column(name = WIDGETS_BUNDLE_ALIAS_PROPERTY) - private String alias; - - @Column(name = WIDGETS_BUNDLE_TITLE_PROPERTY) - private String title; - - @Column(name = SEARCH_TEXT_PROPERTY) - private String searchText; - - @Column(name = WIDGETS_BUNDLE_IMAGE_PROPERTY) - private ByteBuffer image; - - public WidgetsBundleEntity() { - super(); - } - - public WidgetsBundleEntity(WidgetsBundle widgetsBundle) { - if (widgetsBundle.getId() != null) { - this.id = widgetsBundle.getId().getId(); - } - if (widgetsBundle.getTenantId() != null) { - this.tenantId = widgetsBundle.getTenantId().getId(); - } - this.alias = widgetsBundle.getAlias(); - this.title = widgetsBundle.getTitle(); - if (widgetsBundle.getImage() != null) { - this.image = ByteBuffer.wrap(widgetsBundle.getImage()); - } - } - - @Override - public UUID getUuid() { - return id; - } - - @Override - public void setUuid(UUID id) { - this.id = id; - } - - public UUID getTenantId() { - return tenantId; - } - - public void setTenantId(UUID tenantId) { - this.tenantId = tenantId; - } - - public String getAlias() { - return alias; - } - - public void setAlias(String alias) { - this.alias = alias; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public ByteBuffer getImage() { - return image; - } - - public void setImage(ByteBuffer image) { - this.image = image; - } - - @Override - public String getSearchTextSource() { - return getTitle(); - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; - } - - public String getSearchText() { - return searchText; - } - - @Override - public WidgetsBundle toData() { - WidgetsBundle widgetsBundle = new WidgetsBundle(new WidgetsBundleId(id)); - widgetsBundle.setCreatedTime(UUIDs.unixTimestamp(id)); - if (tenantId != null) { - widgetsBundle.setTenantId(new TenantId(tenantId)); - } - widgetsBundle.setAlias(alias); - widgetsBundle.setTitle(title); - if (image != null) { - byte[] imageByteArray = new byte[image.remaining()]; - image.get(imageByteArray); - widgetsBundle.setImage(imageByteArray); - } - return widgetsBundle; - } -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmEntity.java new file mode 100644 index 0000000000..19eb19760d --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmEntity.java @@ -0,0 +1,179 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.model.BaseEntity; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.Column; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.MappedSuperclass; +import java.util.Arrays; +import java.util.Collections; +import java.util.UUID; + +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ACK_TS_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_CLEAR_TS_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_END_TS_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ORIGINATOR_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ORIGINATOR_TYPE_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_PROPAGATE_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_PROPAGATE_RELATION_TYPES; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_SEVERITY_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_START_TS_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_STATUS_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_TENANT_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_TYPE_PROPERTY; + +@Data +@EqualsAndHashCode(callSuper = true) +@TypeDef(name = "json", typeClass = JsonStringType.class) +@MappedSuperclass +public abstract class AbstractAlarmEntity extends BaseSqlEntity implements BaseEntity { + + @Column(name = ALARM_TENANT_ID_PROPERTY) + private UUID tenantId; + + @Column(name = ALARM_ORIGINATOR_ID_PROPERTY) + private UUID originatorId; + + @Column(name = ALARM_ORIGINATOR_TYPE_PROPERTY) + private EntityType originatorType; + + @Column(name = ALARM_TYPE_PROPERTY) + private String type; + + @Enumerated(EnumType.STRING) + @Column(name = ALARM_SEVERITY_PROPERTY) + private AlarmSeverity severity; + + @Enumerated(EnumType.STRING) + @Column(name = ALARM_STATUS_PROPERTY) + private AlarmStatus status; + + @Column(name = ALARM_START_TS_PROPERTY) + private Long startTs; + + @Column(name = ALARM_END_TS_PROPERTY) + private Long endTs; + + @Column(name = ALARM_ACK_TS_PROPERTY) + private Long ackTs; + + @Column(name = ALARM_CLEAR_TS_PROPERTY) + private Long clearTs; + + @Type(type = "json") + @Column(name = ModelConstants.ASSET_ADDITIONAL_INFO_PROPERTY) + private JsonNode details; + + @Column(name = ALARM_PROPAGATE_PROPERTY) + private Boolean propagate; + + @Column(name = ALARM_PROPAGATE_RELATION_TYPES) + private String propagateRelationTypes; + + public AbstractAlarmEntity() { + super(); + } + + public AbstractAlarmEntity(Alarm alarm) { + if (alarm.getId() != null) { + this.setUuid(alarm.getUuidId()); + } + this.setCreatedTime(alarm.getCreatedTime()); + if (alarm.getTenantId() != null) { + this.tenantId = alarm.getTenantId().getId(); + } + this.type = alarm.getType(); + this.originatorId = alarm.getOriginator().getId(); + this.originatorType = alarm.getOriginator().getEntityType(); + this.type = alarm.getType(); + this.severity = alarm.getSeverity(); + this.status = alarm.getStatus(); + this.propagate = alarm.isPropagate(); + this.startTs = alarm.getStartTs(); + this.endTs = alarm.getEndTs(); + this.ackTs = alarm.getAckTs(); + this.clearTs = alarm.getClearTs(); + this.details = alarm.getDetails(); + if (!CollectionUtils.isEmpty(alarm.getPropagateRelationTypes())) { + this.propagateRelationTypes = String.join(",", alarm.getPropagateRelationTypes()); + } else { + this.propagateRelationTypes = null; + } + } + + public AbstractAlarmEntity(AlarmEntity alarmEntity) { + this.setId(alarmEntity.getId()); + this.setCreatedTime(alarmEntity.getCreatedTime()); + this.tenantId = alarmEntity.getTenantId(); + this.type = alarmEntity.getType(); + this.originatorId = alarmEntity.getOriginatorId(); + this.originatorType = alarmEntity.getOriginatorType(); + this.type = alarmEntity.getType(); + this.severity = alarmEntity.getSeverity(); + this.status = alarmEntity.getStatus(); + this.propagate = alarmEntity.getPropagate(); + this.startTs = alarmEntity.getStartTs(); + this.endTs = alarmEntity.getEndTs(); + this.ackTs = alarmEntity.getAckTs(); + this.clearTs = alarmEntity.getClearTs(); + this.details = alarmEntity.getDetails(); + this.propagateRelationTypes = alarmEntity.getPropagateRelationTypes(); + } + + protected Alarm toAlarm() { + Alarm alarm = new Alarm(new AlarmId(id)); + alarm.setCreatedTime(createdTime); + if (tenantId != null) { + alarm.setTenantId(new TenantId(tenantId)); + } + alarm.setOriginator(EntityIdFactory.getByTypeAndUuid(originatorType, originatorId)); + alarm.setType(type); + alarm.setSeverity(severity); + alarm.setStatus(status); + alarm.setPropagate(propagate); + alarm.setStartTs(startTs); + alarm.setEndTs(endTs); + alarm.setAckTs(ackTs); + alarm.setClearTs(clearTs); + alarm.setDetails(details); + if (!StringUtils.isEmpty(propagateRelationTypes)) { + alarm.setPropagateRelationTypes(Arrays.asList(propagateRelationTypes.split(","))); + } else { + alarm.setPropagateRelationTypes(Collections.emptyList()); + } + return alarm; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAssetEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAssetEntity.java new file mode 100644 index 0000000000..3e9c636e3b --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAssetEntity.java @@ -0,0 +1,134 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.model.SearchTextEntity; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.Column; +import javax.persistence.MappedSuperclass; +import java.util.UUID; + +import static org.thingsboard.server.dao.model.ModelConstants.ASSET_CUSTOMER_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ASSET_LABEL_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ASSET_NAME_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TENANT_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TYPE_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY; + +@Data +@EqualsAndHashCode(callSuper = true) +@TypeDef(name = "json", typeClass = JsonStringType.class) +@MappedSuperclass +public abstract class AbstractAssetEntity extends BaseSqlEntity implements SearchTextEntity { + + @Column(name = ASSET_TENANT_ID_PROPERTY) + private UUID tenantId; + + @Column(name = ASSET_CUSTOMER_ID_PROPERTY) + private UUID customerId; + + @Column(name = ASSET_NAME_PROPERTY) + private String name; + + @Column(name = ASSET_TYPE_PROPERTY) + private String type; + + @Column(name = ASSET_LABEL_PROPERTY) + private String label; + + @Column(name = SEARCH_TEXT_PROPERTY) + private String searchText; + + @Type(type = "json") + @Column(name = ModelConstants.ASSET_ADDITIONAL_INFO_PROPERTY) + private JsonNode additionalInfo; + + public AbstractAssetEntity() { + super(); + } + + public AbstractAssetEntity(Asset asset) { + if (asset.getId() != null) { + this.setUuid(asset.getId().getId()); + } + this.setCreatedTime(asset.getCreatedTime()); + if (asset.getTenantId() != null) { + this.tenantId = asset.getTenantId().getId(); + } + if (asset.getCustomerId() != null) { + this.customerId = asset.getCustomerId().getId(); + } + this.name = asset.getName(); + this.type = asset.getType(); + this.label = asset.getLabel(); + this.additionalInfo = asset.getAdditionalInfo(); + } + + public AbstractAssetEntity(AssetEntity assetEntity) { + this.setId(assetEntity.getId()); + this.setCreatedTime(assetEntity.getCreatedTime()); + this.tenantId = assetEntity.getTenantId(); + this.customerId = assetEntity.getCustomerId(); + this.type = assetEntity.getType(); + this.name = assetEntity.getName(); + this.label = assetEntity.getLabel(); + this.searchText = assetEntity.getSearchText(); + this.additionalInfo = assetEntity.getAdditionalInfo(); + } + + @Override + public String getSearchTextSource() { + return name; + } + + @Override + public void setSearchText(String searchText) { + this.searchText = searchText; + } + + public String getSearchText() { + return searchText; + } + + protected Asset toAsset() { + Asset asset = new Asset(new AssetId(id)); + asset.setCreatedTime(createdTime); + if (tenantId != null) { + asset.setTenantId(new TenantId(tenantId)); + } + if (customerId != null) { + asset.setCustomerId(new CustomerId(customerId)); + } + asset.setName(name); + asset.setType(type); + asset.setLabel(label); + asset.setAdditionalInfo(additionalInfo); + return asset; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractDeviceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractDeviceEntity.java new file mode 100644 index 0000000000..4f8c0f9e45 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractDeviceEntity.java @@ -0,0 +1,149 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.hibernate.annotations.TypeDefs; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.device.data.DeviceData; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.model.SearchTextEntity; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.server.dao.util.mapping.JsonBinaryType; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.Column; +import javax.persistence.MappedSuperclass; +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = true) +@TypeDefs({ + @TypeDef(name = "json", typeClass = JsonStringType.class), + @TypeDef(name = "jsonb", typeClass = JsonBinaryType.class) +}) +@MappedSuperclass +public abstract class AbstractDeviceEntity extends BaseSqlEntity implements SearchTextEntity { + + @Column(name = ModelConstants.DEVICE_TENANT_ID_PROPERTY, columnDefinition = "uuid") + private UUID tenantId; + + @Column(name = ModelConstants.DEVICE_CUSTOMER_ID_PROPERTY, columnDefinition = "uuid") + private UUID customerId; + + @Column(name = ModelConstants.DEVICE_TYPE_PROPERTY) + private String type; + + @Column(name = ModelConstants.DEVICE_NAME_PROPERTY) + private String name; + + @Column(name = ModelConstants.DEVICE_LABEL_PROPERTY) + private String label; + + @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) + private String searchText; + + @Type(type = "json") + @Column(name = ModelConstants.DEVICE_ADDITIONAL_INFO_PROPERTY) + private JsonNode additionalInfo; + + @Column(name = ModelConstants.DEVICE_DEVICE_PROFILE_ID_PROPERTY, columnDefinition = "uuid") + private UUID deviceProfileId; + + @Type(type = "jsonb") + @Column(name = ModelConstants.DEVICE_DEVICE_DATA_PROPERTY, columnDefinition = "jsonb") + private JsonNode deviceData; + + public AbstractDeviceEntity() { + super(); + } + + public AbstractDeviceEntity(Device device) { + if (device.getId() != null) { + this.setUuid(device.getUuidId()); + } + this.setCreatedTime(device.getCreatedTime()); + if (device.getTenantId() != null) { + this.tenantId = device.getTenantId().getId(); + } + if (device.getCustomerId() != null) { + this.customerId = device.getCustomerId().getId(); + } + if (device.getDeviceProfileId() != null) { + this.deviceProfileId = device.getDeviceProfileId().getId(); + } + this.deviceData = JacksonUtil.convertValue(device.getDeviceData(), ObjectNode.class); + this.name = device.getName(); + this.type = device.getType(); + this.label = device.getLabel(); + this.additionalInfo = device.getAdditionalInfo(); + } + + public AbstractDeviceEntity(DeviceEntity deviceEntity) { + this.setId(deviceEntity.getId()); + this.setCreatedTime(deviceEntity.getCreatedTime()); + this.tenantId = deviceEntity.getTenantId(); + this.customerId = deviceEntity.getCustomerId(); + this.deviceProfileId = deviceEntity.getDeviceProfileId(); + this.deviceData = deviceEntity.getDeviceData(); + this.type = deviceEntity.getType(); + this.name = deviceEntity.getName(); + this.label = deviceEntity.getLabel(); + this.searchText = deviceEntity.getSearchText(); + this.additionalInfo = deviceEntity.getAdditionalInfo(); + } + + @Override + public String getSearchTextSource() { + return name; + } + + @Override + public void setSearchText(String searchText) { + this.searchText = searchText; + } + + protected Device toDevice() { + Device device = new Device(new DeviceId(getUuid())); + device.setCreatedTime(createdTime); + if (tenantId != null) { + device.setTenantId(new TenantId(tenantId)); + } + if (customerId != null) { + device.setCustomerId(new CustomerId(customerId)); + } + if (deviceProfileId != null) { + device.setDeviceProfileId(new DeviceProfileId(deviceProfileId)); + } + device.setDeviceData(JacksonUtil.convertValue(deviceData, DeviceData.class)); + device.setName(name); + device.setType(type); + device.setLabel(label); + device.setAdditionalInfo(additionalInfo); + return device; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEntityViewEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEntityViewEntity.java new file mode 100644 index 0000000000..5ecbb6758e --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEntityViewEntity.java @@ -0,0 +1,177 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.objects.TelemetryEntityView; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.model.SearchTextEntity; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.Column; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.MappedSuperclass; +import java.io.IOException; +import java.util.UUID; + +import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_TYPE_PROPERTY; + +/** + * Created by Victor Basanets on 8/30/2017. + */ + +@Data +@EqualsAndHashCode(callSuper = true) +@TypeDef(name = "json", typeClass = JsonStringType.class) +@MappedSuperclass +@Slf4j +public abstract class AbstractEntityViewEntity extends BaseSqlEntity implements SearchTextEntity { + + @Column(name = ModelConstants.ENTITY_VIEW_ENTITY_ID_PROPERTY) + private UUID entityId; + + @Enumerated(EnumType.STRING) + @Column(name = ENTITY_TYPE_PROPERTY) + private EntityType entityType; + + @Column(name = ModelConstants.ENTITY_VIEW_TENANT_ID_PROPERTY) + private UUID tenantId; + + @Column(name = ModelConstants.ENTITY_VIEW_CUSTOMER_ID_PROPERTY) + private UUID customerId; + + @Column(name = ModelConstants.DEVICE_TYPE_PROPERTY) + private String type; + + @Column(name = ModelConstants.ENTITY_VIEW_NAME_PROPERTY) + private String name; + + @Column(name = ModelConstants.ENTITY_VIEW_KEYS_PROPERTY) + private String keys; + + @Column(name = ModelConstants.ENTITY_VIEW_START_TS_PROPERTY) + private long startTs; + + @Column(name = ModelConstants.ENTITY_VIEW_END_TS_PROPERTY) + private long endTs; + + @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) + private String searchText; + + @Type(type = "json") + @Column(name = ModelConstants.ENTITY_VIEW_ADDITIONAL_INFO_PROPERTY) + private JsonNode additionalInfo; + + private static final ObjectMapper mapper = new ObjectMapper(); + + public AbstractEntityViewEntity() { + super(); + } + + public AbstractEntityViewEntity(EntityView entityView) { + if (entityView.getId() != null) { + this.setUuid(entityView.getId().getId()); + } + this.setCreatedTime(entityView.getCreatedTime()); + if (entityView.getEntityId() != null) { + this.entityId = entityView.getEntityId().getId(); + this.entityType = entityView.getEntityId().getEntityType(); + } + if (entityView.getTenantId() != null) { + this.tenantId = entityView.getTenantId().getId(); + } + if (entityView.getCustomerId() != null) { + this.customerId = entityView.getCustomerId().getId(); + } + this.type = entityView.getType(); + this.name = entityView.getName(); + try { + this.keys = mapper.writeValueAsString(entityView.getKeys()); + } catch (IOException e) { + log.error("Unable to serialize entity view keys!", e); + } + this.startTs = entityView.getStartTimeMs(); + this.endTs = entityView.getEndTimeMs(); + this.searchText = entityView.getSearchText(); + this.additionalInfo = entityView.getAdditionalInfo(); + } + + public AbstractEntityViewEntity(EntityViewEntity entityViewEntity) { + this.setId(entityViewEntity.getId()); + this.setCreatedTime(entityViewEntity.getCreatedTime()); + this.entityId = entityViewEntity.getEntityId(); + this.entityType = entityViewEntity.getEntityType(); + this.tenantId = entityViewEntity.getTenantId(); + this.customerId = entityViewEntity.getCustomerId(); + this.type = entityViewEntity.getType(); + this.name = entityViewEntity.getName(); + this.keys = entityViewEntity.getKeys(); + this.startTs = entityViewEntity.getStartTs(); + this.endTs = entityViewEntity.getEndTs(); + this.searchText = entityViewEntity.getSearchText(); + this.additionalInfo = entityViewEntity.getAdditionalInfo(); + } + + @Override + public String getSearchTextSource() { + return name; + } + + @Override + public void setSearchText(String searchText) { + this.searchText = searchText; + } + + protected EntityView toEntityView() { + EntityView entityView = new EntityView(new EntityViewId(getUuid())); + entityView.setCreatedTime(createdTime); + + if (entityId != null) { + entityView.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType.name(), entityId)); + } + if (tenantId != null) { + entityView.setTenantId(new TenantId(tenantId)); + } + if (customerId != null) { + entityView.setCustomerId(new CustomerId(customerId)); + } + entityView.setType(type); + entityView.setName(name); + try { + entityView.setKeys(mapper.readValue(keys, TelemetryEntityView.class)); + } catch (IOException e) { + log.error("Unable to read entity view keys!", e); + } + entityView.setStartTimeMs(startTs); + entityView.setEndTimeMs(endTs); + entityView.setAdditionalInfo(additionalInfo); + return entityView; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractOAuth2ClientRegistrationInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractOAuth2ClientRegistrationInfoEntity.java new file mode 100644 index 0000000000..6e9ed4822a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractOAuth2ClientRegistrationInfoEntity.java @@ -0,0 +1,231 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationInfoId; +import org.thingsboard.server.common.data.oauth2.*; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.*; +import java.util.Arrays; + +@Data +@EqualsAndHashCode(callSuper = true) +@TypeDef(name = "json", typeClass = JsonStringType.class) +@MappedSuperclass +public abstract class AbstractOAuth2ClientRegistrationInfoEntity extends BaseSqlEntity { + + @Column(name = ModelConstants.OAUTH2_ENABLED_PROPERTY) + private Boolean enabled; + @Column(name = ModelConstants.OAUTH2_CLIENT_ID_PROPERTY) + private String clientId; + @Column(name = ModelConstants.OAUTH2_CLIENT_SECRET_PROPERTY) + private String clientSecret; + @Column(name = ModelConstants.OAUTH2_AUTHORIZATION_URI_PROPERTY) + private String authorizationUri; + @Column(name = ModelConstants.OAUTH2_TOKEN_URI_PROPERTY) + private String tokenUri; + @Column(name = ModelConstants.OAUTH2_SCOPE_PROPERTY) + private String scope; + @Column(name = ModelConstants.OAUTH2_USER_INFO_URI_PROPERTY) + private String userInfoUri; + @Column(name = ModelConstants.OAUTH2_USER_NAME_ATTRIBUTE_NAME_PROPERTY) + private String userNameAttributeName; + @Column(name = ModelConstants.OAUTH2_JWK_SET_URI_PROPERTY) + private String jwkSetUri; + @Column(name = ModelConstants.OAUTH2_CLIENT_AUTHENTICATION_METHOD_PROPERTY) + private String clientAuthenticationMethod; + @Column(name = ModelConstants.OAUTH2_LOGIN_BUTTON_LABEL_PROPERTY) + private String loginButtonLabel; + @Column(name = ModelConstants.OAUTH2_LOGIN_BUTTON_ICON_PROPERTY) + private String loginButtonIcon; + @Column(name = ModelConstants.OAUTH2_ALLOW_USER_CREATION_PROPERTY) + private Boolean allowUserCreation; + @Column(name = ModelConstants.OAUTH2_ACTIVATE_USER_PROPERTY) + private Boolean activateUser; + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.OAUTH2_MAPPER_TYPE_PROPERTY) + private MapperType type; + @Column(name = ModelConstants.OAUTH2_EMAIL_ATTRIBUTE_KEY_PROPERTY) + private String emailAttributeKey; + @Column(name = ModelConstants.OAUTH2_FIRST_NAME_ATTRIBUTE_KEY_PROPERTY) + private String firstNameAttributeKey; + @Column(name = ModelConstants.OAUTH2_LAST_NAME_ATTRIBUTE_KEY_PROPERTY) + private String lastNameAttributeKey; + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.OAUTH2_TENANT_NAME_STRATEGY_PROPERTY) + private TenantNameStrategyType tenantNameStrategy; + @Column(name = ModelConstants.OAUTH2_TENANT_NAME_PATTERN_PROPERTY) + private String tenantNamePattern; + @Column(name = ModelConstants.OAUTH2_CUSTOMER_NAME_PATTERN_PROPERTY) + private String customerNamePattern; + @Column(name = ModelConstants.OAUTH2_DEFAULT_DASHBOARD_NAME_PROPERTY) + private String defaultDashboardName; + @Column(name = ModelConstants.OAUTH2_ALWAYS_FULL_SCREEN_PROPERTY) + private Boolean alwaysFullScreen; + @Column(name = ModelConstants.OAUTH2_MAPPER_URL_PROPERTY) + private String url; + @Column(name = ModelConstants.OAUTH2_MAPPER_USERNAME_PROPERTY) + private String username; + @Column(name = ModelConstants.OAUTH2_MAPPER_PASSWORD_PROPERTY) + private String password; + @Column(name = ModelConstants.OAUTH2_MAPPER_SEND_TOKEN_PROPERTY) + private Boolean sendToken; + + @Type(type = "json") + @Column(name = ModelConstants.OAUTH2_ADDITIONAL_INFO_PROPERTY) + private JsonNode additionalInfo; + + public AbstractOAuth2ClientRegistrationInfoEntity() { + super(); + } + + public AbstractOAuth2ClientRegistrationInfoEntity(OAuth2ClientRegistrationInfo clientRegistrationInfo) { + if (clientRegistrationInfo.getId() != null) { + this.setUuid(clientRegistrationInfo.getId().getId()); + } + this.createdTime = clientRegistrationInfo.getCreatedTime(); + this.enabled = clientRegistrationInfo.isEnabled(); + this.clientId = clientRegistrationInfo.getClientId(); + this.clientSecret = clientRegistrationInfo.getClientSecret(); + this.authorizationUri = clientRegistrationInfo.getAuthorizationUri(); + this.tokenUri = clientRegistrationInfo.getAccessTokenUri(); + this.scope = clientRegistrationInfo.getScope().stream().reduce((result, element) -> result + "," + element).orElse(""); + this.userInfoUri = clientRegistrationInfo.getUserInfoUri(); + this.userNameAttributeName = clientRegistrationInfo.getUserNameAttributeName(); + this.jwkSetUri = clientRegistrationInfo.getJwkSetUri(); + this.clientAuthenticationMethod = clientRegistrationInfo.getClientAuthenticationMethod(); + this.loginButtonLabel = clientRegistrationInfo.getLoginButtonLabel(); + this.loginButtonIcon = clientRegistrationInfo.getLoginButtonIcon(); + this.additionalInfo = clientRegistrationInfo.getAdditionalInfo(); + OAuth2MapperConfig mapperConfig = clientRegistrationInfo.getMapperConfig(); + if (mapperConfig != null) { + this.allowUserCreation = mapperConfig.isAllowUserCreation(); + this.activateUser = mapperConfig.isActivateUser(); + this.type = mapperConfig.getType(); + OAuth2BasicMapperConfig basicConfig = mapperConfig.getBasic(); + if (basicConfig != null) { + this.emailAttributeKey = basicConfig.getEmailAttributeKey(); + this.firstNameAttributeKey = basicConfig.getFirstNameAttributeKey(); + this.lastNameAttributeKey = basicConfig.getLastNameAttributeKey(); + this.tenantNameStrategy = basicConfig.getTenantNameStrategy(); + this.tenantNamePattern = basicConfig.getTenantNamePattern(); + this.customerNamePattern = basicConfig.getCustomerNamePattern(); + this.defaultDashboardName = basicConfig.getDefaultDashboardName(); + this.alwaysFullScreen = basicConfig.isAlwaysFullScreen(); + } + OAuth2CustomMapperConfig customConfig = mapperConfig.getCustom(); + if (customConfig != null) { + this.url = customConfig.getUrl(); + this.username = customConfig.getUsername(); + this.password = customConfig.getPassword(); + this.sendToken = customConfig.isSendToken(); + } + } + } + + public AbstractOAuth2ClientRegistrationInfoEntity(OAuth2ClientRegistrationInfoEntity oAuth2ClientRegistrationInfoEntity) { + this.setId(oAuth2ClientRegistrationInfoEntity.getId()); + this.setCreatedTime(oAuth2ClientRegistrationInfoEntity.getCreatedTime()); + this.enabled = oAuth2ClientRegistrationInfoEntity.getEnabled(); + this.clientId = oAuth2ClientRegistrationInfoEntity.getClientId(); + this.clientSecret = oAuth2ClientRegistrationInfoEntity.getClientSecret(); + this.authorizationUri = oAuth2ClientRegistrationInfoEntity.getAuthorizationUri(); + this.tokenUri = oAuth2ClientRegistrationInfoEntity.getTokenUri(); + this.scope = oAuth2ClientRegistrationInfoEntity.getScope(); + this.userInfoUri = oAuth2ClientRegistrationInfoEntity.getUserInfoUri(); + this.userNameAttributeName = oAuth2ClientRegistrationInfoEntity.getUserNameAttributeName(); + this.jwkSetUri = oAuth2ClientRegistrationInfoEntity.getJwkSetUri(); + this.clientAuthenticationMethod = oAuth2ClientRegistrationInfoEntity.getClientAuthenticationMethod(); + this.loginButtonLabel = oAuth2ClientRegistrationInfoEntity.getLoginButtonLabel(); + this.loginButtonIcon = oAuth2ClientRegistrationInfoEntity.getLoginButtonIcon(); + this.additionalInfo = oAuth2ClientRegistrationInfoEntity.getAdditionalInfo(); + this.allowUserCreation = oAuth2ClientRegistrationInfoEntity.getAllowUserCreation(); + this.activateUser = oAuth2ClientRegistrationInfoEntity.getActivateUser(); + this.type = oAuth2ClientRegistrationInfoEntity.getType(); + this.emailAttributeKey = oAuth2ClientRegistrationInfoEntity.getEmailAttributeKey(); + this.firstNameAttributeKey = oAuth2ClientRegistrationInfoEntity.getFirstNameAttributeKey(); + this.lastNameAttributeKey = oAuth2ClientRegistrationInfoEntity.getLastNameAttributeKey(); + this.tenantNameStrategy = oAuth2ClientRegistrationInfoEntity.getTenantNameStrategy(); + this.tenantNamePattern = oAuth2ClientRegistrationInfoEntity.getTenantNamePattern(); + this.customerNamePattern = oAuth2ClientRegistrationInfoEntity.getCustomerNamePattern(); + this.defaultDashboardName = oAuth2ClientRegistrationInfoEntity.getDefaultDashboardName(); + this.alwaysFullScreen = oAuth2ClientRegistrationInfoEntity.getAlwaysFullScreen(); + this.url = oAuth2ClientRegistrationInfoEntity.getUrl(); + this.username = oAuth2ClientRegistrationInfoEntity.getUsername(); + this.password = oAuth2ClientRegistrationInfoEntity.getPassword(); + this.sendToken = oAuth2ClientRegistrationInfoEntity.getSendToken(); + } + + + protected OAuth2ClientRegistrationInfo toOAuth2ClientRegistrationInfo() { + OAuth2ClientRegistrationInfo clientRegistrationInfo = new OAuth2ClientRegistrationInfo(); + clientRegistrationInfo.setId(new OAuth2ClientRegistrationInfoId(id)); + clientRegistrationInfo.setEnabled(enabled); + clientRegistrationInfo.setCreatedTime(createdTime); + clientRegistrationInfo.setAdditionalInfo(additionalInfo); + clientRegistrationInfo.setMapperConfig( + OAuth2MapperConfig.builder() + .allowUserCreation(allowUserCreation) + .activateUser(activateUser) + .type(type) + .basic( + (type == MapperType.BASIC || type == MapperType.GITHUB) ? + OAuth2BasicMapperConfig.builder() + .emailAttributeKey(emailAttributeKey) + .firstNameAttributeKey(firstNameAttributeKey) + .lastNameAttributeKey(lastNameAttributeKey) + .tenantNameStrategy(tenantNameStrategy) + .tenantNamePattern(tenantNamePattern) + .customerNamePattern(customerNamePattern) + .defaultDashboardName(defaultDashboardName) + .alwaysFullScreen(alwaysFullScreen) + .build() + : null + ) + .custom( + type == MapperType.CUSTOM ? + OAuth2CustomMapperConfig.builder() + .url(url) + .username(username) + .password(password) + .sendToken(sendToken) + .build() + : null + ) + .build() + ); + clientRegistrationInfo.setClientId(clientId); + clientRegistrationInfo.setClientSecret(clientSecret); + clientRegistrationInfo.setAuthorizationUri(authorizationUri); + clientRegistrationInfo.setAccessTokenUri(tokenUri); + clientRegistrationInfo.setScope(Arrays.asList(scope.split(","))); + clientRegistrationInfo.setUserInfoUri(userInfoUri); + clientRegistrationInfo.setUserNameAttributeName(userNameAttributeName); + clientRegistrationInfo.setJwkSetUri(jwkSetUri); + clientRegistrationInfo.setClientAuthenticationMethod(clientAuthenticationMethod); + clientRegistrationInfo.setLoginButtonLabel(loginButtonLabel); + clientRegistrationInfo.setLoginButtonIcon(loginButtonIcon); + return clientRegistrationInfo; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTenantEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTenantEntity.java new file mode 100644 index 0000000000..7cd5c6778e --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTenantEntity.java @@ -0,0 +1,161 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.model.SearchTextEntity; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.MappedSuperclass; +import javax.persistence.Table; +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = true) +@TypeDef(name = "json", typeClass = JsonStringType.class) +@MappedSuperclass +public abstract class AbstractTenantEntity extends BaseSqlEntity implements SearchTextEntity { + + @Column(name = ModelConstants.TENANT_TITLE_PROPERTY) + private String title; + + @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) + private String searchText; + + @Column(name = ModelConstants.TENANT_REGION_PROPERTY) + private String region; + + @Column(name = ModelConstants.COUNTRY_PROPERTY) + private String country; + + @Column(name = ModelConstants.STATE_PROPERTY) + private String state; + + @Column(name = ModelConstants.CITY_PROPERTY) + private String city; + + @Column(name = ModelConstants.ADDRESS_PROPERTY) + private String address; + + @Column(name = ModelConstants.ADDRESS2_PROPERTY) + private String address2; + + @Column(name = ModelConstants.ZIP_PROPERTY) + private String zip; + + @Column(name = ModelConstants.PHONE_PROPERTY) + private String phone; + + @Column(name = ModelConstants.EMAIL_PROPERTY) + private String email; + + @Type(type = "json") + @Column(name = ModelConstants.TENANT_ADDITIONAL_INFO_PROPERTY) + private JsonNode additionalInfo; + + @Column(name = ModelConstants.TENANT_TENANT_PROFILE_ID_PROPERTY, columnDefinition = "uuid") + private UUID tenantProfileId; + + public AbstractTenantEntity() { + super(); + } + + public AbstractTenantEntity(Tenant tenant) { + if (tenant.getId() != null) { + this.setUuid(tenant.getId().getId()); + } + this.setCreatedTime(tenant.getCreatedTime()); + this.title = tenant.getTitle(); + this.region = tenant.getRegion(); + this.country = tenant.getCountry(); + this.state = tenant.getState(); + this.city = tenant.getCity(); + this.address = tenant.getAddress(); + this.address2 = tenant.getAddress2(); + this.zip = tenant.getZip(); + this.phone = tenant.getPhone(); + this.email = tenant.getEmail(); + this.additionalInfo = tenant.getAdditionalInfo(); + if (tenant.getTenantProfileId() != null) { + this.tenantProfileId = tenant.getTenantProfileId().getId(); + } + } + + public AbstractTenantEntity(TenantEntity tenantEntity) { + this.setId(tenantEntity.getId()); + this.setCreatedTime(tenantEntity.getCreatedTime()); + this.title = tenantEntity.getTitle(); + this.region = tenantEntity.getRegion(); + this.country = tenantEntity.getCountry(); + this.state = tenantEntity.getState(); + this.city = tenantEntity.getCity(); + this.address = tenantEntity.getAddress(); + this.address2 = tenantEntity.getAddress2(); + this.zip = tenantEntity.getZip(); + this.phone = tenantEntity.getPhone(); + this.email = tenantEntity.getEmail(); + this.additionalInfo = tenantEntity.getAdditionalInfo(); + this.tenantProfileId = tenantEntity.getTenantProfileId(); + } + + @Override + public String getSearchTextSource() { + return title; + } + + @Override + public void setSearchText(String searchText) { + this.searchText = searchText; + } + + public String getSearchText() { + return searchText; + } + + protected Tenant toTenant() { + Tenant tenant = new Tenant(new TenantId(this.getUuid())); + tenant.setCreatedTime(createdTime); + tenant.setTitle(title); + tenant.setRegion(region); + tenant.setCountry(country); + tenant.setState(state); + tenant.setCity(city); + tenant.setAddress(address); + tenant.setAddress2(address2); + tenant.setZip(zip); + tenant.setPhone(phone); + tenant.setEmail(email); + tenant.setAdditionalInfo(additionalInfo); + if (tenantProfileId != null) { + tenant.setTenantProfileId(new TenantProfileId(tenantProfileId)); + } + return tenant; + } + + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AdminSettingsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AdminSettingsEntity.java index 1b1a27a04f..7bd303dfa1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AdminSettingsEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AdminSettingsEntity.java @@ -15,14 +15,12 @@ */ package org.thingsboard.server.dao.model.sql; -import com.datastax.driver.core.utils.UUIDs; import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import lombok.EqualsAndHashCode; import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; import org.thingsboard.server.common.data.AdminSettings; -import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.common.data.id.AdminSettingsId; import org.thingsboard.server.dao.model.BaseEntity; import org.thingsboard.server.dao.model.BaseSqlEntity; @@ -58,17 +56,18 @@ public final class AdminSettingsEntity extends BaseSqlEntity impl if (adminSettings.getId() != null) { this.setUuid(adminSettings.getId().getId()); } + this.setCreatedTime(adminSettings.getCreatedTime()); this.key = adminSettings.getKey(); this.jsonValue = adminSettings.getJsonValue(); } @Override public AdminSettings toData() { - AdminSettings adminSettings = new AdminSettings(new AdminSettingsId(UUIDConverter.fromString(id))); - adminSettings.setCreatedTime(UUIDs.unixTimestamp(UUIDConverter.fromString(id))); + AdminSettings adminSettings = new AdminSettings(new AdminSettingsId(id)); + adminSettings.setCreatedTime(createdTime); adminSettings.setKey(key); adminSettings.setJsonValue(jsonValue); return adminSettings; } -} \ No newline at end of file +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmEntity.java index 8d29800022..fa9490747a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmEntity.java @@ -15,152 +15,34 @@ */ package org.thingsboard.server.dao.model.sql; -import com.datastax.driver.core.utils.UUIDs; -import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import lombok.EqualsAndHashCode; -import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.id.AlarmId; -import org.thingsboard.server.common.data.alarm.AlarmSeverity; -import org.thingsboard.server.common.data.alarm.AlarmStatus; -import org.thingsboard.server.common.data.id.EntityIdFactory; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.model.BaseEntity; -import org.thingsboard.server.dao.model.BaseSqlEntity; -import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.util.mapping.JsonStringType; -import javax.persistence.Column; import javax.persistence.Entity; -import javax.persistence.EnumType; -import javax.persistence.Enumerated; import javax.persistence.Table; -import java.util.Arrays; -import java.util.Collections; - -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ACK_TS_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_CLEAR_TS_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ALARM_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_END_TS_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ORIGINATOR_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ORIGINATOR_TYPE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_PROPAGATE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_PROPAGATE_RELATION_TYPES; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_SEVERITY_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_START_TS_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_STATUS_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_TENANT_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_TYPE_PROPERTY; @Data @EqualsAndHashCode(callSuper = true) @Entity @TypeDef(name = "json", typeClass = JsonStringType.class) @Table(name = ALARM_COLUMN_FAMILY_NAME) -public final class AlarmEntity extends BaseSqlEntity implements BaseEntity { - - @Column(name = ALARM_TENANT_ID_PROPERTY) - private String tenantId; - - @Column(name = ALARM_ORIGINATOR_ID_PROPERTY) - private String originatorId; - - @Column(name = ALARM_ORIGINATOR_TYPE_PROPERTY) - private EntityType originatorType; - - @Column(name = ALARM_TYPE_PROPERTY) - private String type; - - @Enumerated(EnumType.STRING) - @Column(name = ALARM_SEVERITY_PROPERTY) - private AlarmSeverity severity; - - @Enumerated(EnumType.STRING) - @Column(name = ALARM_STATUS_PROPERTY) - private AlarmStatus status; - - @Column(name = ALARM_START_TS_PROPERTY) - private Long startTs; - - @Column(name = ALARM_END_TS_PROPERTY) - private Long endTs; - - @Column(name = ALARM_ACK_TS_PROPERTY) - private Long ackTs; - - @Column(name = ALARM_CLEAR_TS_PROPERTY) - private Long clearTs; - - @Type(type = "json") - @Column(name = ModelConstants.ASSET_ADDITIONAL_INFO_PROPERTY) - private JsonNode details; - - @Column(name = ALARM_PROPAGATE_PROPERTY) - private Boolean propagate; - - @Column(name = ALARM_PROPAGATE_RELATION_TYPES) - private String propagateRelationTypes; +public final class AlarmEntity extends AbstractAlarmEntity { public AlarmEntity() { super(); } public AlarmEntity(Alarm alarm) { - if (alarm.getId() != null) { - this.setUuid(alarm.getId().getId()); - } - if (alarm.getTenantId() != null) { - this.tenantId = UUIDConverter.fromTimeUUID(alarm.getTenantId().getId()); - } - this.type = alarm.getType(); - this.originatorId = UUIDConverter.fromTimeUUID(alarm.getOriginator().getId()); - this.originatorType = alarm.getOriginator().getEntityType(); - this.type = alarm.getType(); - this.severity = alarm.getSeverity(); - this.status = alarm.getStatus(); - this.propagate = alarm.isPropagate(); - this.startTs = alarm.getStartTs(); - this.endTs = alarm.getEndTs(); - this.ackTs = alarm.getAckTs(); - this.clearTs = alarm.getClearTs(); - this.details = alarm.getDetails(); - if (!CollectionUtils.isEmpty(alarm.getPropagateRelationTypes())) { - this.propagateRelationTypes = String.join(",", alarm.getPropagateRelationTypes()); - } else { - this.propagateRelationTypes = null; - } + super(alarm); } @Override public Alarm toData() { - Alarm alarm = new Alarm(new AlarmId(UUIDConverter.fromString(id))); - alarm.setCreatedTime(UUIDs.unixTimestamp(UUIDConverter.fromString(id))); - if (tenantId != null) { - alarm.setTenantId(new TenantId(UUIDConverter.fromString(tenantId))); - } - alarm.setOriginator(EntityIdFactory.getByTypeAndUuid(originatorType, UUIDConverter.fromString(originatorId))); - alarm.setType(type); - alarm.setSeverity(severity); - alarm.setStatus(status); - alarm.setPropagate(propagate); - alarm.setStartTs(startTs); - alarm.setEndTs(endTs); - alarm.setAckTs(ackTs); - alarm.setClearTs(clearTs); - alarm.setDetails(details); - if(!StringUtils.isEmpty(propagateRelationTypes)) { - alarm.setPropagateRelationTypes(Arrays.asList(propagateRelationTypes.split(","))); - } else { - alarm.setPropagateRelationTypes(Collections.emptyList()); - } - return alarm; + return super.toAlarm(); } - -} \ No newline at end of file +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmInfoEntity.java new file mode 100644 index 0000000000..0a73f96adb --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmInfoEntity.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.alarm.AlarmInfo; + +@Data +@EqualsAndHashCode(callSuper = true) +public class AlarmInfoEntity extends AbstractAlarmEntity { + + private String originatorName; + + public AlarmInfoEntity() { + super(); + } + + public AlarmInfoEntity(AlarmEntity alarmEntity) { + super(alarmEntity); + } + + @Override + public AlarmInfo toData() { + return new AlarmInfo(super.toAlarm(), this.originatorName); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiUsageStateEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiUsageStateEntity.java new file mode 100644 index 0000000000..bc4aed6398 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiUsageStateEntity.java @@ -0,0 +1,105 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.ApiUsageStateValue; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.ApiUsageStateId; +import org.thingsboard.server.dao.model.BaseEntity; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Table; +import java.util.UUID; + +/** + * Created by Valerii Sosliuk on 4/21/2017. + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@TypeDef(name = "json", typeClass = JsonStringType.class) +@Table(name = ModelConstants.API_USAGE_STATE_TABLE_NAME) +public class ApiUsageStateEntity extends BaseSqlEntity implements BaseEntity { + + @Column(name = ModelConstants.API_USAGE_STATE_TENANT_ID_COLUMN) + private UUID tenantId; + @Column(name = ModelConstants.API_USAGE_STATE_ENTITY_TYPE_COLUMN) + private String entityType; + @Column(name = ModelConstants.API_USAGE_STATE_ENTITY_ID_COLUMN) + private UUID entityId; + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.API_USAGE_STATE_TRANSPORT_COLUMN) + private ApiUsageStateValue transportState = ApiUsageStateValue.ENABLED; + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.API_USAGE_STATE_DB_STORAGE_COLUMN) + private ApiUsageStateValue dbStorageState = ApiUsageStateValue.ENABLED; + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.API_USAGE_STATE_RE_EXEC_COLUMN) + private ApiUsageStateValue reExecState = ApiUsageStateValue.ENABLED; + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.API_USAGE_STATE_JS_EXEC_COLUMN) + private ApiUsageStateValue jsExecState = ApiUsageStateValue.ENABLED; + + public ApiUsageStateEntity() { + } + + public ApiUsageStateEntity(ApiUsageState ur) { + if (ur.getId() != null) { + this.setUuid(ur.getId().getId()); + } + this.setCreatedTime(ur.getCreatedTime()); + if (ur.getTenantId() != null) { + this.tenantId = ur.getTenantId().getId(); + } + if (ur.getEntityId() != null) { + this.entityType = ur.getEntityId().getEntityType().name(); + this.entityId = ur.getEntityId().getId(); + } + this.transportState = ur.getTransportState(); + this.dbStorageState = ur.getDbStorageState(); + this.reExecState = ur.getReExecState(); + this.jsExecState = ur.getJsExecState(); + } + + @Override + public ApiUsageState toData() { + ApiUsageState ur = new ApiUsageState(new ApiUsageStateId(this.getUuid())); + ur.setCreatedTime(createdTime); + if (tenantId != null) { + ur.setTenantId(new TenantId(tenantId)); + } + if (entityId != null) { + ur.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId)); + } + ur.setTransportState(transportState); + ur.setDbStorageState(dbStorageState); + ur.setReExecState(reExecState); + ur.setJsExecState(jsExecState); + return ur; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetEntity.java index 6eff4cd521..bdf763424a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetEntity.java @@ -15,112 +15,35 @@ */ package org.thingsboard.server.dao.model.sql; -import com.datastax.driver.core.utils.UUIDs; -import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import lombok.EqualsAndHashCode; -import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; -import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.common.data.asset.Asset; -import org.thingsboard.server.common.data.id.AssetId; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.model.BaseSqlEntity; -import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.SearchTextEntity; import org.thingsboard.server.dao.util.mapping.JsonStringType; -import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Table; import static org.thingsboard.server.dao.model.ModelConstants.ASSET_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.ASSET_CUSTOMER_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ASSET_NAME_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TENANT_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TYPE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ASSET_LABEL_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY; @Data @EqualsAndHashCode(callSuper = true) @Entity @TypeDef(name = "json", typeClass = JsonStringType.class) @Table(name = ASSET_COLUMN_FAMILY_NAME) -public final class AssetEntity extends BaseSqlEntity implements SearchTextEntity { - - @Column(name = ASSET_TENANT_ID_PROPERTY) - private String tenantId; - - @Column(name = ASSET_CUSTOMER_ID_PROPERTY) - private String customerId; - - @Column(name = ASSET_NAME_PROPERTY) - private String name; - - @Column(name = ASSET_TYPE_PROPERTY) - private String type; - - @Column(name = ASSET_LABEL_PROPERTY) - private String label; - - @Column(name = SEARCH_TEXT_PROPERTY) - private String searchText; - - @Type(type = "json") - @Column(name = ModelConstants.ASSET_ADDITIONAL_INFO_PROPERTY) - private JsonNode additionalInfo; +public final class AssetEntity extends AbstractAssetEntity { public AssetEntity() { super(); } public AssetEntity(Asset asset) { - if (asset.getId() != null) { - this.setUuid(asset.getId().getId()); - } - if (asset.getTenantId() != null) { - this.tenantId = UUIDConverter.fromTimeUUID(asset.getTenantId().getId()); - } - if (asset.getCustomerId() != null) { - this.customerId = UUIDConverter.fromTimeUUID(asset.getCustomerId().getId()); - } - this.name = asset.getName(); - this.type = asset.getType(); - this.label = asset.getLabel(); - this.additionalInfo = asset.getAdditionalInfo(); - } - - @Override - public String getSearchTextSource() { - return name; - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; - } - - public String getSearchText() { - return searchText; + super(asset); } @Override public Asset toData() { - Asset asset = new Asset(new AssetId(UUIDConverter.fromString(id))); - asset.setCreatedTime(UUIDs.unixTimestamp(UUIDConverter.fromString(id))); - if (tenantId != null) { - asset.setTenantId(new TenantId(UUIDConverter.fromString(tenantId))); - } - if (customerId != null) { - asset.setCustomerId(new CustomerId(UUIDConverter.fromString(customerId))); - } - asset.setName(name); - asset.setType(type); - asset.setLabel(label); - asset.setAdditionalInfo(additionalInfo); - return asset; + return super.toAsset(); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetInfoEntity.java new file mode 100644 index 0000000000..403cbb6608 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetInfoEntity.java @@ -0,0 +1,58 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.asset.AssetInfo; + +import java.util.HashMap; +import java.util.Map; + +@Data +@EqualsAndHashCode(callSuper = true) +public class AssetInfoEntity extends AbstractAssetEntity { + + public static final Map assetInfoColumnMap = new HashMap<>(); + static { + assetInfoColumnMap.put("customerTitle", "c.title"); + } + + private String customerTitle; + private boolean customerIsPublic; + + public AssetInfoEntity() { + super(); + } + + public AssetInfoEntity(AssetEntity assetEntity, + String customerTitle, + Object customerAdditionalInfo) { + super(assetEntity); + this.customerTitle = customerTitle; + if (customerAdditionalInfo != null && ((JsonNode)customerAdditionalInfo).has("isPublic")) { + this.customerIsPublic = ((JsonNode)customerAdditionalInfo).get("isPublic").asBoolean(); + } else { + this.customerIsPublic = false; + } + } + + @Override + public AssetInfo toData() { + return new AssetInfo(super.toAsset(), customerTitle, customerIsPublic); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AttributeKvCompositeKey.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AttributeKvCompositeKey.java index 31a667de71..a13685ea3c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AttributeKvCompositeKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AttributeKvCompositeKey.java @@ -25,6 +25,7 @@ import javax.persistence.Embeddable; import javax.persistence.EnumType; import javax.persistence.Enumerated; import java.io.Serializable; +import java.util.UUID; import static org.thingsboard.server.dao.model.ModelConstants.ATTRIBUTE_KEY_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.ATTRIBUTE_TYPE_COLUMN; @@ -39,8 +40,8 @@ public class AttributeKvCompositeKey implements Serializable { @Enumerated(EnumType.STRING) @Column(name = ENTITY_TYPE_COLUMN) private EntityType entityType; - @Column(name = ENTITY_ID_COLUMN) - private String entityId; + @Column(name = ENTITY_ID_COLUMN, columnDefinition = "uuid") + private UUID entityId; @Column(name = ATTRIBUTE_TYPE_COLUMN) private String attributeType; @Column(name = ATTRIBUTE_KEY_COLUMN) diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java index e2573ea0d1..99c526c611 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.model.sql; -import com.datastax.driver.core.utils.UUIDs; import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import lombok.EqualsAndHashCode; @@ -40,6 +39,7 @@ import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.persistence.Table; +import java.util.UUID; import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ACTION_DATA_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ACTION_FAILURE_DETAILS_PROPERTY; @@ -61,23 +61,23 @@ import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_USER_NAM public class AuditLogEntity extends BaseSqlEntity implements BaseEntity { @Column(name = AUDIT_LOG_TENANT_ID_PROPERTY) - private String tenantId; + private UUID tenantId; @Column(name = AUDIT_LOG_CUSTOMER_ID_PROPERTY) - private String customerId; + private UUID customerId; @Enumerated(EnumType.STRING) @Column(name = AUDIT_LOG_ENTITY_TYPE_PROPERTY) private EntityType entityType; @Column(name = AUDIT_LOG_ENTITY_ID_PROPERTY) - private String entityId; + private UUID entityId; @Column(name = AUDIT_LOG_ENTITY_NAME_PROPERTY) private String entityName; @Column(name = AUDIT_LOG_USER_ID_PROPERTY) - private String userId; + private UUID userId; @Column(name = AUDIT_LOG_USER_NAME_PROPERTY) private String userName; @@ -105,18 +105,19 @@ public class AuditLogEntity extends BaseSqlEntity implements BaseEntit if (auditLog.getId() != null) { this.setUuid(auditLog.getId().getId()); } + this.setCreatedTime(auditLog.getCreatedTime()); if (auditLog.getTenantId() != null) { - this.tenantId = toString(auditLog.getTenantId().getId()); + this.tenantId = auditLog.getTenantId().getId(); } if (auditLog.getCustomerId() != null) { - this.customerId = toString(auditLog.getCustomerId().getId()); + this.customerId = auditLog.getCustomerId().getId(); } if (auditLog.getEntityId() != null) { - this.entityId = toString(auditLog.getEntityId().getId()); + this.entityId = auditLog.getEntityId().getId(); this.entityType = auditLog.getEntityId().getEntityType(); } if (auditLog.getUserId() != null) { - this.userId = toString(auditLog.getUserId().getId()); + this.userId = auditLog.getUserId().getId(); } this.entityName = auditLog.getEntityName(); this.userName = auditLog.getUserName(); @@ -129,18 +130,18 @@ public class AuditLogEntity extends BaseSqlEntity implements BaseEntit @Override public AuditLog toData() { AuditLog auditLog = new AuditLog(new AuditLogId(this.getUuid())); - auditLog.setCreatedTime(UUIDs.unixTimestamp(this.getUuid())); + auditLog.setCreatedTime(createdTime); if (tenantId != null) { - auditLog.setTenantId(new TenantId(toUUID(tenantId))); + auditLog.setTenantId(new TenantId(tenantId)); } if (customerId != null) { - auditLog.setCustomerId(new CustomerId(toUUID(customerId))); + auditLog.setCustomerId(new CustomerId(customerId)); } if (entityId != null) { - auditLog.setEntityId(EntityIdFactory.getByTypeAndId(entityType.name(), toUUID(entityId).toString())); + auditLog.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType.name(), entityId)); } if (userId != null) { - auditLog.setUserId(new UserId(toUUID(userId))); + auditLog.setUserId(new UserId(userId)); } auditLog.setEntityName(this.entityName); auditLog.setUserName(this.userName); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java index d5b287fe9b..381610a82c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java @@ -73,6 +73,7 @@ public class ComponentDescriptorEntity extends BaseSqlEntity implements SearchTextEntity { @Column(name = ModelConstants.CUSTOMER_TENANT_ID_PROPERTY) - private String tenantId; + private UUID tenantId; @Column(name = ModelConstants.CUSTOMER_TITLE_PROPERTY) private String title; @@ -86,7 +85,8 @@ public final class CustomerEntity extends BaseSqlEntity implements Sea if (customer.getId() != null) { this.setUuid(customer.getId().getId()); } - this.tenantId = UUIDConverter.fromTimeUUID(customer.getTenantId().getId()); + this.setCreatedTime(customer.getCreatedTime()); + this.tenantId = customer.getTenantId().getId(); this.title = customer.getTitle(); this.country = customer.getCountry(); this.state = customer.getState(); @@ -112,8 +112,8 @@ public final class CustomerEntity extends BaseSqlEntity implements Sea @Override public Customer toData() { Customer customer = new Customer(new CustomerId(this.getUuid())); - customer.setCreatedTime(UUIDs.unixTimestamp(this.getUuid())); - customer.setTenantId(new TenantId(UUIDConverter.fromString(tenantId))); + customer.setCreatedTime(createdTime); + customer.setTenantId(new TenantId(tenantId)); customer.setTitle(title); customer.setCountry(country); customer.setState(state); @@ -126,4 +126,5 @@ public final class CustomerEntity extends BaseSqlEntity implements Sea customer.setAdditionalInfo(additionalInfo); return customer; } -} \ No newline at end of file + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardEntity.java index 07cec4d12f..41eb90a5d1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardEntity.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.model.sql; -import com.datastax.driver.core.utils.UUIDs; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonNode; @@ -40,6 +39,7 @@ import javax.persistence.Entity; import javax.persistence.Table; import java.io.IOException; import java.util.HashSet; +import java.util.UUID; @Data @Slf4j @@ -54,7 +54,7 @@ public final class DashboardEntity extends BaseSqlEntity implements S objectMapper.getTypeFactory().constructCollectionType(HashSet.class, ShortCustomerInfo.class); @Column(name = ModelConstants.DASHBOARD_TENANT_ID_PROPERTY) - private String tenantId; + private UUID tenantId; @Column(name = ModelConstants.DASHBOARD_TITLE_PROPERTY) private String title; @@ -77,8 +77,9 @@ public final class DashboardEntity extends BaseSqlEntity implements S if (dashboard.getId() != null) { this.setUuid(dashboard.getId().getId()); } + this.setCreatedTime(dashboard.getCreatedTime()); if (dashboard.getTenantId() != null) { - this.tenantId = toString(dashboard.getTenantId().getId()); + this.tenantId = dashboard.getTenantId().getId(); } this.title = dashboard.getTitle(); if (dashboard.getAssignedCustomers() != null) { @@ -104,9 +105,9 @@ public final class DashboardEntity extends BaseSqlEntity implements S @Override public Dashboard toData() { Dashboard dashboard = new Dashboard(new DashboardId(this.getUuid())); - dashboard.setCreatedTime(UUIDs.unixTimestamp(this.getUuid())); + dashboard.setCreatedTime(this.getCreatedTime()); if (tenantId != null) { - dashboard.setTenantId(new TenantId(toUUID(tenantId))); + dashboard.setTenantId(new TenantId(tenantId)); } dashboard.setTitle(title); if (!StringUtils.isEmpty(assignedCustomers)) { @@ -119,4 +120,4 @@ public final class DashboardEntity extends BaseSqlEntity implements S dashboard.setConfiguration(configuration); return dashboard; } -} \ No newline at end of file +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardInfoEntity.java index f26cf21934..d0e7721142 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardInfoEntity.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.model.sql; -import com.datastax.driver.core.utils.UUIDs; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; @@ -36,6 +35,7 @@ import javax.persistence.Entity; import javax.persistence.Table; import java.io.IOException; import java.util.HashSet; +import java.util.UUID; @Data @Slf4j @@ -49,7 +49,7 @@ public class DashboardInfoEntity extends BaseSqlEntity implements objectMapper.getTypeFactory().constructCollectionType(HashSet.class, ShortCustomerInfo.class); @Column(name = ModelConstants.DASHBOARD_TENANT_ID_PROPERTY) - private String tenantId; + private UUID tenantId; @Column(name = ModelConstants.DASHBOARD_TITLE_PROPERTY) private String title; @@ -68,8 +68,9 @@ public class DashboardInfoEntity extends BaseSqlEntity implements if (dashboardInfo.getId() != null) { this.setUuid(dashboardInfo.getId().getId()); } + this.setCreatedTime(dashboardInfo.getCreatedTime()); if (dashboardInfo.getTenantId() != null) { - this.tenantId = toString(dashboardInfo.getTenantId().getId()); + this.tenantId = dashboardInfo.getTenantId().getId(); } this.title = dashboardInfo.getTitle(); if (dashboardInfo.getAssignedCustomers() != null) { @@ -98,9 +99,9 @@ public class DashboardInfoEntity extends BaseSqlEntity implements @Override public DashboardInfo toData() { DashboardInfo dashboardInfo = new DashboardInfo(new DashboardId(this.getUuid())); - dashboardInfo.setCreatedTime(UUIDs.unixTimestamp(this.getUuid())); + dashboardInfo.setCreatedTime(createdTime); if (tenantId != null) { - dashboardInfo.setTenantId(new TenantId(toUUID(tenantId))); + dashboardInfo.setTenantId(new TenantId(tenantId)); } dashboardInfo.setTitle(title); if (!StringUtils.isEmpty(assignedCustomers)) { @@ -113,4 +114,4 @@ public class DashboardInfoEntity extends BaseSqlEntity implements return dashboardInfo; } -} \ No newline at end of file +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceCredentialsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceCredentialsEntity.java index ba9d7ce637..ab438add62 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceCredentialsEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceCredentialsEntity.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.model.sql; -import com.datastax.driver.core.utils.UUIDs; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.id.DeviceCredentialsId; @@ -31,6 +30,7 @@ import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.persistence.Table; +import java.util.UUID; @Data @EqualsAndHashCode(callSuper = true) @@ -39,7 +39,7 @@ import javax.persistence.Table; public final class DeviceCredentialsEntity extends BaseSqlEntity implements BaseEntity { @Column(name = ModelConstants.DEVICE_CREDENTIALS_DEVICE_ID_PROPERTY) - private String deviceId; + private UUID deviceId; @Enumerated(EnumType.STRING) @Column(name = ModelConstants.DEVICE_CREDENTIALS_CREDENTIALS_TYPE_PROPERTY) @@ -59,8 +59,9 @@ public final class DeviceCredentialsEntity extends BaseSqlEntity implements SearchTextEntity { - - @Column(name = ModelConstants.DEVICE_TENANT_ID_PROPERTY) - private String tenantId; - - @Column(name = ModelConstants.DEVICE_CUSTOMER_ID_PROPERTY) - private String customerId; - - @Column(name = ModelConstants.DEVICE_TYPE_PROPERTY) - private String type; - - @Column(name = ModelConstants.DEVICE_NAME_PROPERTY) - private String name; - - @Column(name = ModelConstants.DEVICE_LABEL_PROPERTY) - private String label; - - @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) - private String searchText; - - @Type(type = "json") - @Column(name = ModelConstants.DEVICE_ADDITIONAL_INFO_PROPERTY) - private JsonNode additionalInfo; +public final class DeviceEntity extends AbstractDeviceEntity { public DeviceEntity() { super(); } public DeviceEntity(Device device) { - if (device.getId() != null) { - this.setUuid(device.getId().getId()); - } - if (device.getTenantId() != null) { - this.tenantId = toString(device.getTenantId().getId()); - } - if (device.getCustomerId() != null) { - this.customerId = toString(device.getCustomerId().getId()); - } - this.name = device.getName(); - this.type = device.getType(); - this.label = device.getLabel(); - this.additionalInfo = device.getAdditionalInfo(); - } - - @Override - public String getSearchTextSource() { - return name; - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; + super(device); } @Override public Device toData() { - Device device = new Device(new DeviceId(this.getUuid())); - device.setCreatedTime(UUIDs.unixTimestamp(this.getUuid())); - if (tenantId != null) { - device.setTenantId(new TenantId(toUUID(tenantId))); - } - if (customerId != null) { - device.setCustomerId(new CustomerId(toUUID(customerId))); - } - device.setName(name); - device.setType(type); - device.setLabel(label); - device.setAdditionalInfo(additionalInfo); - return device; + return super.toDevice(); } -} \ No newline at end of file +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceInfoEntity.java new file mode 100644 index 0000000000..b3c42945fb --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceInfoEntity.java @@ -0,0 +1,62 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.DeviceInfo; + +import java.util.HashMap; +import java.util.Map; + +@Data +@EqualsAndHashCode(callSuper = true) +public class DeviceInfoEntity extends AbstractDeviceEntity { + + public static final Map deviceInfoColumnMap = new HashMap<>(); + static { + deviceInfoColumnMap.put("customerTitle", "c.title"); + deviceInfoColumnMap.put("deviceProfileName", "p.name"); + } + + private String customerTitle; + private boolean customerIsPublic; + private String deviceProfileName; + + public DeviceInfoEntity() { + super(); + } + + public DeviceInfoEntity(DeviceEntity deviceEntity, + String customerTitle, + Object customerAdditionalInfo, + String deviceProfileName) { + super(deviceEntity); + this.customerTitle = customerTitle; + if (customerAdditionalInfo != null && ((JsonNode)customerAdditionalInfo).has("isPublic")) { + this.customerIsPublic = ((JsonNode)customerAdditionalInfo).get("isPublic").asBoolean(); + } else { + this.customerIsPublic = false; + } + this.deviceProfileName = deviceProfileName; + } + + @Override + public DeviceInfo toData() { + return new DeviceInfo(super.toDevice(), customerTitle, customerIsPublic, deviceProfileName); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java new file mode 100644 index 0000000000..d70268c349 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java @@ -0,0 +1,153 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.model.SearchTextEntity; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.server.dao.util.mapping.JsonBinaryType; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Table; +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@TypeDef(name = "jsonb", typeClass = JsonBinaryType.class) +@Table(name = ModelConstants.DEVICE_PROFILE_COLUMN_FAMILY_NAME) +public final class DeviceProfileEntity extends BaseSqlEntity implements SearchTextEntity { + + @Column(name = ModelConstants.DEVICE_PROFILE_TENANT_ID_PROPERTY) + private UUID tenantId; + + @Column(name = ModelConstants.DEVICE_PROFILE_NAME_PROPERTY) + private String name; + + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.DEVICE_PROFILE_TYPE_PROPERTY) + private DeviceProfileType type; + + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.DEVICE_PROFILE_TRANSPORT_TYPE_PROPERTY) + private DeviceTransportType transportType; + + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.DEVICE_PROFILE_PROVISION_TYPE_PROPERTY) + private DeviceProfileProvisionType provisionType; + + @Column(name = ModelConstants.DEVICE_PROFILE_DESCRIPTION_PROPERTY) + private String description; + + @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) + private String searchText; + + @Column(name = ModelConstants.DEVICE_PROFILE_IS_DEFAULT_PROPERTY) + private boolean isDefault; + + @Column(name = ModelConstants.DEVICE_PROFILE_DEFAULT_RULE_CHAIN_ID_PROPERTY, columnDefinition = "uuid") + private UUID defaultRuleChainId; + + @Column(name = ModelConstants.DEVICE_PROFILE_DEFAULT_QUEUE_NAME_PROPERTY) + private String defaultQueueName; + + @Type(type = "jsonb") + @Column(name = ModelConstants.DEVICE_PROFILE_PROFILE_DATA_PROPERTY, columnDefinition = "jsonb") + private JsonNode profileData; + + @Column(name=ModelConstants.DEVICE_PROFILE_PROVISION_DEVICE_KEY) + private String provisionDeviceKey; + + public DeviceProfileEntity() { + super(); + } + + public DeviceProfileEntity(DeviceProfile deviceProfile) { + if (deviceProfile.getId() != null) { + this.setUuid(deviceProfile.getId().getId()); + } + if (deviceProfile.getTenantId() != null) { + this.tenantId = deviceProfile.getTenantId().getId(); + } + this.setCreatedTime(deviceProfile.getCreatedTime()); + this.name = deviceProfile.getName(); + this.type = deviceProfile.getType(); + this.transportType = deviceProfile.getTransportType(); + this.provisionType = deviceProfile.getProvisionType(); + this.description = deviceProfile.getDescription(); + this.isDefault = deviceProfile.isDefault(); + this.profileData = JacksonUtil.convertValue(deviceProfile.getProfileData(), ObjectNode.class); + if (deviceProfile.getDefaultRuleChainId() != null) { + this.defaultRuleChainId = deviceProfile.getDefaultRuleChainId().getId(); + } + this.defaultQueueName = deviceProfile.getDefaultQueueName(); + this.provisionDeviceKey = deviceProfile.getProvisionDeviceKey(); + } + + @Override + public String getSearchTextSource() { + return name; + } + + @Override + public void setSearchText(String searchText) { + this.searchText = searchText; + } + + public String getSearchText() { + return searchText; + } + + @Override + public DeviceProfile toData() { + DeviceProfile deviceProfile = new DeviceProfile(new DeviceProfileId(this.getUuid())); + deviceProfile.setCreatedTime(createdTime); + if (tenantId != null) { + deviceProfile.setTenantId(new TenantId(tenantId)); + } + deviceProfile.setName(name); + deviceProfile.setType(type); + deviceProfile.setTransportType(transportType); + deviceProfile.setProvisionType(provisionType); + deviceProfile.setDescription(description); + deviceProfile.setDefault(isDefault); + deviceProfile.setProfileData(JacksonUtil.convertValue(profileData, DeviceProfileData.class)); + if (defaultRuleChainId != null) { + deviceProfile.setDefaultRuleChainId(new RuleChainId(defaultRuleChainId)); + } + deviceProfile.setDefaultQueueName(defaultQueueName); + deviceProfile.setProvisionDeviceKey(provisionDeviceKey); + return deviceProfile; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityViewEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityViewEntity.java index fd446cc6ef..e8907c155e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityViewEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityViewEntity.java @@ -15,34 +15,15 @@ */ package org.thingsboard.server.dao.model.sql; -import com.datastax.driver.core.utils.UUIDs; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import lombok.Data; import lombok.EqualsAndHashCode; -import lombok.extern.slf4j.Slf4j; -import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.EntityIdFactory; -import org.thingsboard.server.common.data.id.EntityViewId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.objects.TelemetryEntityView; -import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.SearchTextEntity; import org.thingsboard.server.dao.util.mapping.JsonStringType; -import javax.persistence.Column; import javax.persistence.Entity; -import javax.persistence.EnumType; -import javax.persistence.Enumerated; import javax.persistence.Table; -import java.io.IOException; - -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_TYPE_PROPERTY; /** * Created by Victor Basanets on 8/30/2017. @@ -53,111 +34,18 @@ import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_TYPE_PROPER @Entity @TypeDef(name = "json", typeClass = JsonStringType.class) @Table(name = ModelConstants.ENTITY_VIEW_TABLE_FAMILY_NAME) -@Slf4j -public class EntityViewEntity extends BaseSqlEntity implements SearchTextEntity { - - @Column(name = ModelConstants.ENTITY_VIEW_ENTITY_ID_PROPERTY) - private String entityId; - - @Enumerated(EnumType.STRING) - @Column(name = ENTITY_TYPE_PROPERTY) - private EntityType entityType; - - @Column(name = ModelConstants.ENTITY_VIEW_TENANT_ID_PROPERTY) - private String tenantId; - - @Column(name = ModelConstants.ENTITY_VIEW_CUSTOMER_ID_PROPERTY) - private String customerId; - - @Column(name = ModelConstants.DEVICE_TYPE_PROPERTY) - private String type; - - @Column(name = ModelConstants.ENTITY_VIEW_NAME_PROPERTY) - private String name; - - @Column(name = ModelConstants.ENTITY_VIEW_KEYS_PROPERTY) - private String keys; - - @Column(name = ModelConstants.ENTITY_VIEW_START_TS_PROPERTY) - private long startTs; - - @Column(name = ModelConstants.ENTITY_VIEW_END_TS_PROPERTY) - private long endTs; - - @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) - private String searchText; - - @Type(type = "json") - @Column(name = ModelConstants.ENTITY_VIEW_ADDITIONAL_INFO_PROPERTY) - private JsonNode additionalInfo; - - private static final ObjectMapper mapper = new ObjectMapper(); +public class EntityViewEntity extends AbstractEntityViewEntity { public EntityViewEntity() { super(); } public EntityViewEntity(EntityView entityView) { - if (entityView.getId() != null) { - this.setUuid(entityView.getId().getId()); - } - if (entityView.getEntityId() != null) { - this.entityId = toString(entityView.getEntityId().getId()); - this.entityType = entityView.getEntityId().getEntityType(); - } - if (entityView.getTenantId() != null) { - this.tenantId = toString(entityView.getTenantId().getId()); - } - if (entityView.getCustomerId() != null) { - this.customerId = toString(entityView.getCustomerId().getId()); - } - this.type = entityView.getType(); - this.name = entityView.getName(); - try { - this.keys = mapper.writeValueAsString(entityView.getKeys()); - } catch (IOException e) { - log.error("Unable to serialize entity view keys!", e); - } - this.startTs = entityView.getStartTimeMs(); - this.endTs = entityView.getEndTimeMs(); - this.searchText = entityView.getSearchText(); - this.additionalInfo = entityView.getAdditionalInfo(); - } - - @Override - public String getSearchTextSource() { - return name; - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; + super(entityView); } @Override public EntityView toData() { - EntityView entityView = new EntityView(new EntityViewId(this.getUuid())); - entityView.setCreatedTime(UUIDs.unixTimestamp(this.getUuid())); - - if (entityId != null) { - entityView.setEntityId(EntityIdFactory.getByTypeAndId(entityType.name(), toUUID(entityId).toString())); - } - if (tenantId != null) { - entityView.setTenantId(new TenantId(toUUID(tenantId))); - } - if (customerId != null) { - entityView.setCustomerId(new CustomerId(toUUID(customerId))); - } - entityView.setType(type); - entityView.setName(name); - try { - entityView.setKeys(mapper.readValue(keys, TelemetryEntityView.class)); - } catch (IOException e) { - log.error("Unable to read entity view keys!", e); - } - entityView.setStartTimeMs(startTs); - entityView.setEndTimeMs(endTs); - entityView.setAdditionalInfo(additionalInfo); - return entityView; + return super.toEntityView(); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityViewInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityViewInfoEntity.java new file mode 100644 index 0000000000..00012354c1 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityViewInfoEntity.java @@ -0,0 +1,58 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.EntityViewInfo; + +import java.util.HashMap; +import java.util.Map; + +@Data +@EqualsAndHashCode(callSuper = true) +public class EntityViewInfoEntity extends AbstractEntityViewEntity { + + public static final Map entityViewInfoColumnMap = new HashMap<>(); + static { + entityViewInfoColumnMap.put("customerTitle", "c.title"); + } + + private String customerTitle; + private boolean customerIsPublic; + + public EntityViewInfoEntity() { + super(); + } + + public EntityViewInfoEntity(EntityViewEntity entityViewEntity, + String customerTitle, + Object customerAdditionalInfo) { + super(entityViewEntity); + this.customerTitle = customerTitle; + if (customerAdditionalInfo != null && ((JsonNode)customerAdditionalInfo).has("isPublic")) { + this.customerIsPublic = ((JsonNode)customerAdditionalInfo).get("isPublic").asBoolean(); + } else { + this.customerIsPublic = false; + } + } + + @Override + public EntityViewInfo toData() { + return new EntityViewInfo(super.toEntityView(), customerTitle, customerIsPublic); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EventEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EventEntity.java index d25d37c175..af9b46de07 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EventEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EventEntity.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.model.sql; -import com.datastax.driver.core.utils.UUIDs; import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import lombok.EqualsAndHashCode; @@ -36,7 +35,6 @@ import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.persistence.Table; - import java.util.UUID; import static org.thingsboard.server.dao.model.ModelConstants.EPOCH_DIFF; @@ -55,17 +53,17 @@ import static org.thingsboard.server.dao.model.ModelConstants.TS_COLUMN; @TypeDef(name = "json", typeClass = JsonStringType.class) @Table(name = EVENT_COLUMN_FAMILY_NAME) @NoArgsConstructor -public class EventEntity extends BaseSqlEntity implements BaseEntity { +public class EventEntity extends BaseSqlEntity implements BaseEntity { @Column(name = EVENT_TENANT_ID_PROPERTY) - private String tenantId; + private UUID tenantId; @Enumerated(EnumType.STRING) @Column(name = EVENT_ENTITY_TYPE_PROPERTY) private EntityType entityType; @Column(name = EVENT_ENTITY_ID_PROPERTY) - private String entityId; + private UUID entityId; @Column(name = EVENT_TYPE_PROPERTY) private String eventType; @@ -87,12 +85,13 @@ public class EventEntity extends BaseSqlEntity implements BaseEntity implements BaseEntity { + + private String domainName; + private SchemeType domainScheme; + + public ExtendedOAuth2ClientRegistrationInfoEntity() { + super(); + } + + public ExtendedOAuth2ClientRegistrationInfoEntity(OAuth2ClientRegistrationInfoEntity oAuth2ClientRegistrationInfoEntity, + String domainName, + SchemeType domainScheme) { + super(oAuth2ClientRegistrationInfoEntity); + this.domainName = domainName; + this.domainScheme = domainScheme; + } + + @Override + public ExtendedOAuth2ClientRegistrationInfo toData() { + return new ExtendedOAuth2ClientRegistrationInfo(super.toOAuth2ClientRegistrationInfo(), + domainScheme, + domainName); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationEntity.java new file mode 100644 index 0000000000..466034e31c --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationEntity.java @@ -0,0 +1,75 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationId; +import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationInfoId; +import org.thingsboard.server.common.data.oauth2.*; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.*; +import java.util.Arrays; +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@TypeDef(name = "json", typeClass = JsonStringType.class) +@Table(name = ModelConstants.OAUTH2_CLIENT_REGISTRATION_COLUMN_FAMILY_NAME) +public class OAuth2ClientRegistrationEntity extends BaseSqlEntity { + + @Column(name = ModelConstants.OAUTH2_CLIENT_REGISTRATION_INFO_ID_PROPERTY, columnDefinition = "uuid") + private UUID clientRegistrationInfoId; + + @Column(name = ModelConstants.OAUTH2_DOMAIN_NAME_PROPERTY) + private String domainName; + + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.OAUTH2_DOMAIN_SCHEME_PROPERTY) + private SchemeType domainScheme; + + public OAuth2ClientRegistrationEntity() { + super(); + } + + public OAuth2ClientRegistrationEntity(OAuth2ClientRegistration clientRegistration) { + if (clientRegistration.getId() != null) { + this.setUuid(clientRegistration.getId().getId()); + } + if (clientRegistration.getClientRegistrationId() != null){ + this.clientRegistrationInfoId = clientRegistration.getClientRegistrationId().getId(); + } + this.createdTime = clientRegistration.getCreatedTime(); + this.domainName = clientRegistration.getDomainName(); + this.domainScheme = clientRegistration.getDomainScheme(); + } + + @Override + public OAuth2ClientRegistration toData() { + OAuth2ClientRegistration clientRegistration = new OAuth2ClientRegistration(); + clientRegistration.setId(new OAuth2ClientRegistrationId(id)); + clientRegistration.setClientRegistrationId(new OAuth2ClientRegistrationInfoId(clientRegistrationInfoId)); + clientRegistration.setCreatedTime(createdTime); + clientRegistration.setDomainName(domainName); + clientRegistration.setDomainScheme(domainScheme); + return clientRegistration; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationInfoEntity.java new file mode 100644 index 0000000000..7fd68e2716 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationInfoEntity.java @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.Entity; +import javax.persistence.Table; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@TypeDef(name = "json", typeClass = JsonStringType.class) +@Table(name = ModelConstants.OAUTH2_CLIENT_REGISTRATION_INFO_COLUMN_FAMILY_NAME) +public class OAuth2ClientRegistrationInfoEntity extends AbstractOAuth2ClientRegistrationInfoEntity { + + public OAuth2ClientRegistrationInfoEntity() { + super(); + } + + public OAuth2ClientRegistrationInfoEntity(OAuth2ClientRegistrationInfo clientRegistration) { + super(clientRegistration); + } + + public OAuth2ClientRegistrationInfoEntity(OAuth2ClientRegistrationInfoEntity oAuth2ClientRegistrationInfoEntity) { + super(oAuth2ClientRegistrationInfoEntity); + } + + @Override + public OAuth2ClientRegistrationInfo toData() { + return super.toOAuth2ClientRegistrationInfo(); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationTemplateEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationTemplateEntity.java new file mode 100644 index 0000000000..d4fb6bfebe --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationTemplateEntity.java @@ -0,0 +1,166 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.oauth2.*; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.util.mapping.JsonStringType; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; + +import javax.persistence.*; +import java.util.Arrays; +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@TypeDef(name = "json", typeClass = JsonStringType.class) +@Table(name = ModelConstants.OAUTH2_CLIENT_REGISTRATION_TEMPLATE_COLUMN_FAMILY_NAME) +public class OAuth2ClientRegistrationTemplateEntity extends BaseSqlEntity { + + @Column(name = ModelConstants.OAUTH2_TEMPLATE_PROVIDER_ID_PROPERTY) + private String providerId; + @Column(name = ModelConstants.OAUTH2_AUTHORIZATION_URI_PROPERTY) + private String authorizationUri; + @Column(name = ModelConstants.OAUTH2_TOKEN_URI_PROPERTY) + private String tokenUri; + @Column(name = ModelConstants.OAUTH2_SCOPE_PROPERTY) + private String scope; + @Column(name = ModelConstants.OAUTH2_USER_INFO_URI_PROPERTY) + private String userInfoUri; + @Column(name = ModelConstants.OAUTH2_USER_NAME_ATTRIBUTE_NAME_PROPERTY) + private String userNameAttributeName; + @Column(name = ModelConstants.OAUTH2_JWK_SET_URI_PROPERTY) + private String jwkSetUri; + @Column(name = ModelConstants.OAUTH2_CLIENT_AUTHENTICATION_METHOD_PROPERTY) + private String clientAuthenticationMethod; + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.OAUTH2_MAPPER_TYPE_PROPERTY) + private MapperType type; + @Column(name = ModelConstants.OAUTH2_EMAIL_ATTRIBUTE_KEY_PROPERTY) + private String emailAttributeKey; + @Column(name = ModelConstants.OAUTH2_FIRST_NAME_ATTRIBUTE_KEY_PROPERTY) + private String firstNameAttributeKey; + @Column(name = ModelConstants.OAUTH2_LAST_NAME_ATTRIBUTE_KEY_PROPERTY) + private String lastNameAttributeKey; + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.OAUTH2_TENANT_NAME_STRATEGY_PROPERTY) + private TenantNameStrategyType tenantNameStrategy; + @Column(name = ModelConstants.OAUTH2_TENANT_NAME_PATTERN_PROPERTY) + private String tenantNamePattern; + @Column(name = ModelConstants.OAUTH2_CUSTOMER_NAME_PATTERN_PROPERTY) + private String customerNamePattern; + @Column(name = ModelConstants.OAUTH2_DEFAULT_DASHBOARD_NAME_PROPERTY) + private String defaultDashboardName; + @Column(name = ModelConstants.OAUTH2_ALWAYS_FULL_SCREEN_PROPERTY) + private Boolean alwaysFullScreen; + @Column(name = ModelConstants.OAUTH2_TEMPLATE_COMMENT_PROPERTY) + private String comment; + @Column(name = ModelConstants.OAUTH2_TEMPLATE_LOGIN_BUTTON_ICON_PROPERTY) + private String loginButtonIcon; + @Column(name = ModelConstants.OAUTH2_TEMPLATE_LOGIN_BUTTON_LABEL_PROPERTY) + private String loginButtonLabel; + @Column(name = ModelConstants.OAUTH2_TEMPLATE_HELP_LINK_PROPERTY) + private String helpLink; + + @Type(type = "json") + @Column(name = ModelConstants.OAUTH2_TEMPLATE_ADDITIONAL_INFO_PROPERTY) + private JsonNode additionalInfo; + + public OAuth2ClientRegistrationTemplateEntity() { + } + + public OAuth2ClientRegistrationTemplateEntity(OAuth2ClientRegistrationTemplate clientRegistrationTemplate) { + if (clientRegistrationTemplate.getId() != null) { + this.setUuid(clientRegistrationTemplate.getId().getId()); + } + this.createdTime = clientRegistrationTemplate.getCreatedTime(); + this.providerId = clientRegistrationTemplate.getProviderId(); + this.authorizationUri = clientRegistrationTemplate.getAuthorizationUri(); + this.tokenUri = clientRegistrationTemplate.getAccessTokenUri(); + this.scope = clientRegistrationTemplate.getScope().stream().reduce((result, element) -> result + "," + element).orElse(""); + this.userInfoUri = clientRegistrationTemplate.getUserInfoUri(); + this.userNameAttributeName = clientRegistrationTemplate.getUserNameAttributeName(); + this.jwkSetUri = clientRegistrationTemplate.getJwkSetUri(); + this.clientAuthenticationMethod = clientRegistrationTemplate.getClientAuthenticationMethod(); + this.comment = clientRegistrationTemplate.getComment(); + this.loginButtonIcon = clientRegistrationTemplate.getLoginButtonIcon(); + this.loginButtonLabel = clientRegistrationTemplate.getLoginButtonLabel(); + this.helpLink = clientRegistrationTemplate.getHelpLink(); + this.additionalInfo = clientRegistrationTemplate.getAdditionalInfo(); + OAuth2MapperConfig mapperConfig = clientRegistrationTemplate.getMapperConfig(); + if (mapperConfig != null){ + this.type = mapperConfig.getType(); + OAuth2BasicMapperConfig basicConfig = mapperConfig.getBasic(); + if (basicConfig != null) { + this.emailAttributeKey = basicConfig.getEmailAttributeKey(); + this.firstNameAttributeKey = basicConfig.getFirstNameAttributeKey(); + this.lastNameAttributeKey = basicConfig.getLastNameAttributeKey(); + this.tenantNameStrategy = basicConfig.getTenantNameStrategy(); + this.tenantNamePattern = basicConfig.getTenantNamePattern(); + this.customerNamePattern = basicConfig.getCustomerNamePattern(); + this.defaultDashboardName = basicConfig.getDefaultDashboardName(); + this.alwaysFullScreen = basicConfig.isAlwaysFullScreen(); + } + } + } + + @Override + public OAuth2ClientRegistrationTemplate toData() { + OAuth2ClientRegistrationTemplate clientRegistrationTemplate = new OAuth2ClientRegistrationTemplate(); + clientRegistrationTemplate.setId(new OAuth2ClientRegistrationTemplateId(id)); + clientRegistrationTemplate.setCreatedTime(createdTime); + clientRegistrationTemplate.setAdditionalInfo(additionalInfo); + + clientRegistrationTemplate.setProviderId(providerId); + clientRegistrationTemplate.setMapperConfig( + OAuth2MapperConfig.builder() + .type(type) + .basic(OAuth2BasicMapperConfig.builder() + .emailAttributeKey(emailAttributeKey) + .firstNameAttributeKey(firstNameAttributeKey) + .lastNameAttributeKey(lastNameAttributeKey) + .tenantNameStrategy(tenantNameStrategy) + .tenantNamePattern(tenantNamePattern) + .customerNamePattern(customerNamePattern) + .defaultDashboardName(defaultDashboardName) + .alwaysFullScreen(alwaysFullScreen) + .build() + ) + .build() + ); + clientRegistrationTemplate.setAuthorizationUri(authorizationUri); + clientRegistrationTemplate.setAccessTokenUri(tokenUri); + clientRegistrationTemplate.setScope(Arrays.asList(scope.split(","))); + clientRegistrationTemplate.setUserInfoUri(userInfoUri); + clientRegistrationTemplate.setUserNameAttributeName(userNameAttributeName); + clientRegistrationTemplate.setJwkSetUri(jwkSetUri); + clientRegistrationTemplate.setClientAuthenticationMethod(clientAuthenticationMethod); + clientRegistrationTemplate.setComment(comment); + clientRegistrationTemplate.setLoginButtonIcon(loginButtonIcon); + clientRegistrationTemplate.setLoginButtonLabel(loginButtonLabel); + clientRegistrationTemplate.setHelpLink(helpLink); + return clientRegistrationTemplate; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RelationCompositeKey.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RelationCompositeKey.java index 974e394dda..67f9fa1833 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RelationCompositeKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RelationCompositeKey.java @@ -18,11 +18,11 @@ package org.thingsboard.server.dao.model.sql; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.common.data.relation.EntityRelation; import javax.persistence.Transient; import java.io.Serializable; +import java.util.UUID; @NoArgsConstructor @AllArgsConstructor @@ -32,17 +32,17 @@ public class RelationCompositeKey implements Serializable { @Transient private static final long serialVersionUID = -4089175869616037592L; - private String fromId; + private UUID fromId; private String fromType; - private String toId; + private UUID toId; private String toType; private String relationType; private String relationTypeGroup; public RelationCompositeKey(EntityRelation relation) { - this.fromId = UUIDConverter.fromTimeUUID(relation.getFrom().getId()); + this.fromId = relation.getFrom().getId(); this.fromType = relation.getFrom().getEntityType().name(); - this.toId = UUIDConverter.fromTimeUUID(relation.getTo().getId()); + this.toId = relation.getTo().getId(); this.toType = relation.getTo().getEntityType().name(); this.relationType = relation.getType(); this.relationTypeGroup = relation.getTypeGroup().name(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RelationEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RelationEntity.java index a8e5235354..c2b9061096 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RelationEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RelationEntity.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; -import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; @@ -31,6 +30,7 @@ import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.IdClass; import javax.persistence.Table; +import java.util.UUID; import static org.thingsboard.server.dao.model.ModelConstants.ADDITIONAL_INFO_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.RELATION_COLUMN_FAMILY_NAME; @@ -49,16 +49,16 @@ import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TYPE_PROP public final class RelationEntity implements ToData { @Id - @Column(name = RELATION_FROM_ID_PROPERTY) - private String fromId; + @Column(name = RELATION_FROM_ID_PROPERTY, columnDefinition = "uuid") + private UUID fromId; @Id @Column(name = RELATION_FROM_TYPE_PROPERTY) private String fromType; @Id - @Column(name = RELATION_TO_ID_PROPERTY) - private String toId; + @Column(name = RELATION_TO_ID_PROPERTY, columnDefinition = "uuid") + private UUID toId; @Id @Column(name = RELATION_TO_TYPE_PROPERTY) @@ -82,11 +82,11 @@ public final class RelationEntity implements ToData { public RelationEntity(EntityRelation relation) { if (relation.getTo() != null) { - this.toId = UUIDConverter.fromTimeUUID(relation.getTo().getId()); + this.toId = relation.getTo().getId(); this.toType = relation.getTo().getEntityType().name(); } if (relation.getFrom() != null) { - this.fromId = UUIDConverter.fromTimeUUID(relation.getFrom().getId()); + this.fromId = relation.getFrom().getId(); this.fromType = relation.getFrom().getEntityType().name(); } this.relationType = relation.getType(); @@ -98,10 +98,10 @@ public final class RelationEntity implements ToData { public EntityRelation toData() { EntityRelation relation = new EntityRelation(); if (toId != null && toType != null) { - relation.setTo(EntityIdFactory.getByTypeAndUuid(toType, UUIDConverter.fromString(toId))); + relation.setTo(EntityIdFactory.getByTypeAndUuid(toType, toId)); } if (fromId != null && fromType != null) { - relation.setFrom(EntityIdFactory.getByTypeAndUuid(fromType, UUIDConverter.fromString(fromId))); + relation.setFrom(EntityIdFactory.getByTypeAndUuid(fromType, fromId)); } relation.setType(relationType); relation.setTypeGroup(RelationTypeGroup.valueOf(relationTypeGroup)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java index 0675289b03..bad6017314 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java @@ -15,13 +15,11 @@ */ package org.thingsboard.server.dao.model.sql; -import com.datastax.driver.core.utils.UUIDs; import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import lombok.EqualsAndHashCode; import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; -import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; @@ -35,6 +33,7 @@ import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Table; +import java.util.UUID; @Data @EqualsAndHashCode(callSuper = true) @@ -44,7 +43,7 @@ import javax.persistence.Table; public class RuleChainEntity extends BaseSqlEntity implements SearchTextEntity { @Column(name = ModelConstants.RULE_CHAIN_TENANT_ID_PROPERTY) - private String tenantId; + private UUID tenantId; @Column(name = ModelConstants.RULE_CHAIN_NAME_PROPERTY) private String name; @@ -53,7 +52,7 @@ public class RuleChainEntity extends BaseSqlEntity implements SearchT private String searchText; @Column(name = ModelConstants.RULE_CHAIN_FIRST_RULE_NODE_ID_PROPERTY) - private String firstRuleNodeId; + private UUID firstRuleNodeId; @Column(name = ModelConstants.RULE_CHAIN_ROOT_PROPERTY) private boolean root; @@ -76,11 +75,12 @@ public class RuleChainEntity extends BaseSqlEntity implements SearchT if (ruleChain.getId() != null) { this.setUuid(ruleChain.getUuidId()); } - this.tenantId = toString(DaoUtil.getId(ruleChain.getTenantId())); + this.setCreatedTime(ruleChain.getCreatedTime()); + this.tenantId = DaoUtil.getId(ruleChain.getTenantId()); this.name = ruleChain.getName(); this.searchText = ruleChain.getName(); if (ruleChain.getFirstRuleNodeId() != null) { - this.firstRuleNodeId = UUIDConverter.fromTimeUUID(ruleChain.getFirstRuleNodeId().getId()); + this.firstRuleNodeId = ruleChain.getFirstRuleNodeId().getId(); } this.root = ruleChain.isRoot(); this.debugMode = ruleChain.isDebugMode(); @@ -101,11 +101,11 @@ public class RuleChainEntity extends BaseSqlEntity implements SearchT @Override public RuleChain toData() { RuleChain ruleChain = new RuleChain(new RuleChainId(this.getUuid())); - ruleChain.setCreatedTime(UUIDs.unixTimestamp(this.getUuid())); - ruleChain.setTenantId(new TenantId(toUUID(tenantId))); + ruleChain.setCreatedTime(createdTime); + ruleChain.setTenantId(new TenantId(tenantId)); ruleChain.setName(name); if (firstRuleNodeId != null) { - ruleChain.setFirstRuleNodeId(new RuleNodeId(UUIDConverter.fromString(firstRuleNodeId))); + ruleChain.setFirstRuleNodeId(new RuleNodeId(firstRuleNodeId)); } ruleChain.setRoot(root); ruleChain.setDebugMode(debugMode); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java index 1a7866418a..c9a72c405d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.model.sql; -import com.datastax.driver.core.utils.UUIDs; import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import lombok.EqualsAndHashCode; @@ -33,6 +32,7 @@ import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Table; +import java.util.UUID; @Data @EqualsAndHashCode(callSuper = true) @@ -42,7 +42,7 @@ import javax.persistence.Table; public class RuleNodeEntity extends BaseSqlEntity implements SearchTextEntity { @Column(name = ModelConstants.RULE_NODE_CHAIN_ID_PROPERTY) - private String ruleChainId; + private UUID ruleChainId; @Column(name = ModelConstants.RULE_NODE_TYPE_PROPERTY) private String type; @@ -71,8 +71,9 @@ public class RuleNodeEntity extends BaseSqlEntity implements SearchTex if (ruleNode.getId() != null) { this.setUuid(ruleNode.getUuidId()); } + this.setCreatedTime(ruleNode.getCreatedTime()); if (ruleNode.getRuleChainId() != null) { - this.ruleChainId = toString(DaoUtil.getId(ruleNode.getRuleChainId())); + this.ruleChainId = DaoUtil.getId(ruleNode.getRuleChainId()); } this.type = ruleNode.getType(); this.name = ruleNode.getName(); @@ -95,9 +96,9 @@ public class RuleNodeEntity extends BaseSqlEntity implements SearchTex @Override public RuleNode toData() { RuleNode ruleNode = new RuleNode(new RuleNodeId(this.getUuid())); - ruleNode.setCreatedTime(UUIDs.unixTimestamp(this.getUuid())); + ruleNode.setCreatedTime(createdTime); if (ruleChainId != null) { - ruleNode.setRuleChainId(new RuleChainId(toUUID(ruleChainId))); + ruleNode.setRuleChainId(new RuleChainId(ruleChainId)); } ruleNode.setType(type); ruleNode.setName(name); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeStateEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeStateEntity.java new file mode 100644 index 0000000000..a416034fab --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeStateEntity.java @@ -0,0 +1,79 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.id.RuleNodeStateId; +import org.thingsboard.server.common.data.rule.RuleNodeState; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@TypeDef(name = "json", typeClass = JsonStringType.class) +@Table(name = ModelConstants.RULE_NODE_STATE_TABLE_NAME) +public class RuleNodeStateEntity extends BaseSqlEntity { + + @Column(name = ModelConstants.RULE_NODE_STATE_NODE_ID_PROPERTY) + private UUID ruleNodeId; + + @Column(name = ModelConstants.RULE_NODE_STATE_ENTITY_TYPE_PROPERTY) + private String entityType; + + @Column(name = ModelConstants.RULE_NODE_STATE_ENTITY_ID_PROPERTY) + private UUID entityId; + + @Column(name = ModelConstants.RULE_NODE_STATE_DATA_PROPERTY) + private String stateData; + + public RuleNodeStateEntity() { + } + + public RuleNodeStateEntity(RuleNodeState ruleNodeState) { + if (ruleNodeState.getId() != null) { + this.setUuid(ruleNodeState.getUuidId()); + } + this.setCreatedTime(ruleNodeState.getCreatedTime()); + this.ruleNodeId = DaoUtil.getId(ruleNodeState.getRuleNodeId()); + this.entityId = ruleNodeState.getEntityId().getId(); + this.entityType = ruleNodeState.getEntityId().getEntityType().name(); + this.stateData = ruleNodeState.getStateData(); + } + + @Override + public RuleNodeState toData() { + RuleNodeState ruleNode = new RuleNodeState(new RuleNodeStateId(this.getUuid())); + ruleNode.setCreatedTime(createdTime); + ruleNode.setRuleNodeId(new RuleNodeId(ruleNodeId)); + ruleNode.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId)); + ruleNode.setStateData(stateData); + return ruleNode; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantEntity.java index 1da71224f0..2d78763f71 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantEntity.java @@ -15,20 +15,13 @@ */ package org.thingsboard.server.dao.model.sql; -import com.datastax.driver.core.utils.UUIDs; -import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import lombok.EqualsAndHashCode; -import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; import org.thingsboard.server.common.data.Tenant; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.SearchTextEntity; import org.thingsboard.server.dao.util.mapping.JsonStringType; -import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Table; @@ -37,107 +30,18 @@ import javax.persistence.Table; @Entity @TypeDef(name = "json", typeClass = JsonStringType.class) @Table(name = ModelConstants.TENANT_COLUMN_FAMILY_NAME) -public final class TenantEntity extends BaseSqlEntity implements SearchTextEntity { - - @Column(name = ModelConstants.TENANT_TITLE_PROPERTY) - private String title; - - @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) - private String searchText; - - @Column(name = ModelConstants.TENANT_REGION_PROPERTY) - private String region; - - @Column(name = ModelConstants.COUNTRY_PROPERTY) - private String country; - - @Column(name = ModelConstants.STATE_PROPERTY) - private String state; - - @Column(name = ModelConstants.CITY_PROPERTY) - private String city; - - @Column(name = ModelConstants.ADDRESS_PROPERTY) - private String address; - - @Column(name = ModelConstants.ADDRESS2_PROPERTY) - private String address2; - - @Column(name = ModelConstants.ZIP_PROPERTY) - private String zip; - - @Column(name = ModelConstants.PHONE_PROPERTY) - private String phone; - - @Column(name = ModelConstants.EMAIL_PROPERTY) - private String email; - - @Column(name = ModelConstants.TENANT_ISOLATED_TB_CORE) - private boolean isolatedTbCore; - - @Column(name = ModelConstants.TENANT_ISOLATED_TB_RULE_ENGINE) - private boolean isolatedTbRuleEngine; - - @Type(type = "json") - @Column(name = ModelConstants.TENANT_ADDITIONAL_INFO_PROPERTY) - private JsonNode additionalInfo; +public final class TenantEntity extends AbstractTenantEntity { public TenantEntity() { super(); } public TenantEntity(Tenant tenant) { - if (tenant.getId() != null) { - this.setUuid(tenant.getId().getId()); - } - this.title = tenant.getTitle(); - this.region = tenant.getRegion(); - this.country = tenant.getCountry(); - this.state = tenant.getState(); - this.city = tenant.getCity(); - this.address = tenant.getAddress(); - this.address2 = tenant.getAddress2(); - this.zip = tenant.getZip(); - this.phone = tenant.getPhone(); - this.email = tenant.getEmail(); - this.additionalInfo = tenant.getAdditionalInfo(); - this.isolatedTbCore = tenant.isIsolatedTbCore(); - this.isolatedTbRuleEngine = tenant.isIsolatedTbRuleEngine(); - } - - @Override - public String getSearchTextSource() { - return title; - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; - } - - public String getSearchText() { - return searchText; + super(tenant); } @Override public Tenant toData() { - Tenant tenant = new Tenant(new TenantId(this.getUuid())); - tenant.setCreatedTime(UUIDs.unixTimestamp(this.getUuid())); - tenant.setTitle(title); - tenant.setRegion(region); - tenant.setCountry(country); - tenant.setState(state); - tenant.setCity(city); - tenant.setAddress(address); - tenant.setAddress2(address2); - tenant.setZip(zip); - tenant.setPhone(phone); - tenant.setEmail(email); - tenant.setAdditionalInfo(additionalInfo); - tenant.setIsolatedTbCore(isolatedTbCore); - tenant.setIsolatedTbRuleEngine(isolatedTbRuleEngine); - return tenant; + return super.toTenant(); } - - -} \ No newline at end of file +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantInfoEntity.java new file mode 100644 index 0000000000..02e7ce9ebc --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantInfoEntity.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.TenantInfo; + +import java.util.HashMap; +import java.util.Map; + +@Data +@EqualsAndHashCode(callSuper = true) +public class TenantInfoEntity extends AbstractTenantEntity { + + public static final Map tenantInfoColumnMap = new HashMap<>(); + static { + tenantInfoColumnMap.put("tenantProfileName", "p.name"); + } + + private String tenantProfileName; + + public TenantInfoEntity() { + super(); + } + + public TenantInfoEntity(TenantEntity tenantEntity, String tenantProfileName) { + super(tenantEntity); + this.tenantProfileName = tenantProfileName; + } + + @Override + public TenantInfo toData() { + return new TenantInfo(super.toTenant(), this.tenantProfileName); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java new file mode 100644 index 0000000000..7663682496 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java @@ -0,0 +1,110 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.model.SearchTextEntity; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.server.dao.util.mapping.JsonBinaryType; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@TypeDef(name = "jsonb", typeClass = JsonBinaryType.class) +@Table(name = ModelConstants.TENANT_PROFILE_COLUMN_FAMILY_NAME) +public final class TenantProfileEntity extends BaseSqlEntity implements SearchTextEntity { + + @Column(name = ModelConstants.TENANT_PROFILE_NAME_PROPERTY) + private String name; + + @Column(name = ModelConstants.TENANT_PROFILE_DESCRIPTION_PROPERTY) + private String description; + + @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) + private String searchText; + + @Column(name = ModelConstants.TENANT_PROFILE_IS_DEFAULT_PROPERTY) + private boolean isDefault; + + @Column(name = ModelConstants.TENANT_PROFILE_ISOLATED_TB_CORE) + private boolean isolatedTbCore; + + @Column(name = ModelConstants.TENANT_PROFILE_ISOLATED_TB_RULE_ENGINE) + private boolean isolatedTbRuleEngine; + + @Type(type = "jsonb") + @Column(name = ModelConstants.TENANT_PROFILE_PROFILE_DATA_PROPERTY, columnDefinition = "jsonb") + private JsonNode profileData; + + public TenantProfileEntity() { + super(); + } + + public TenantProfileEntity(TenantProfile tenantProfile) { + if (tenantProfile.getId() != null) { + this.setUuid(tenantProfile.getId().getId()); + } + this.setCreatedTime(tenantProfile.getCreatedTime()); + this.name = tenantProfile.getName(); + this.description = tenantProfile.getDescription(); + this.isDefault = tenantProfile.isDefault(); + this.isolatedTbCore = tenantProfile.isIsolatedTbCore(); + this.isolatedTbRuleEngine = tenantProfile.isIsolatedTbRuleEngine(); + this.profileData = JacksonUtil.convertValue(tenantProfile.getProfileData(), ObjectNode.class); + } + + @Override + public String getSearchTextSource() { + return name; + } + + @Override + public void setSearchText(String searchText) { + this.searchText = searchText; + } + + public String getSearchText() { + return searchText; + } + + @Override + public TenantProfile toData() { + TenantProfile tenantProfile = new TenantProfile(new TenantProfileId(this.getUuid())); + tenantProfile.setCreatedTime(createdTime); + tenantProfile.setName(name); + tenantProfile.setDescription(description); + tenantProfile.setDefault(isDefault); + tenantProfile.setIsolatedTbCore(isolatedTbCore); + tenantProfile.setIsolatedTbRuleEngine(isolatedTbRuleEngine); + tenantProfile.setProfileData(JacksonUtil.convertValue(profileData, TenantProfileData.class)); + return tenantProfile; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserCredentialsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserCredentialsEntity.java index 6957f87961..3e30471391 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserCredentialsEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserCredentialsEntity.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.model.sql; -import com.datastax.driver.core.utils.UUIDs; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.id.UserCredentialsId; @@ -28,6 +27,7 @@ import org.thingsboard.server.dao.model.ModelConstants; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Table; +import java.util.UUID; @Data @EqualsAndHashCode(callSuper = true) @@ -36,7 +36,7 @@ import javax.persistence.Table; public final class UserCredentialsEntity extends BaseSqlEntity implements BaseEntity { @Column(name = ModelConstants.USER_CREDENTIALS_USER_ID_PROPERTY, unique = true) - private String userId; + private UUID userId; @Column(name = ModelConstants.USER_CREDENTIALS_ENABLED_PROPERTY) private boolean enabled; @@ -58,8 +58,9 @@ public final class UserCredentialsEntity extends BaseSqlEntity if (userCredentials.getId() != null) { this.setUuid(userCredentials.getId().getId()); } + this.setCreatedTime(userCredentials.getCreatedTime()); if (userCredentials.getUserId() != null) { - this.userId = toString(userCredentials.getUserId().getId()); + this.userId = userCredentials.getUserId().getId(); } this.enabled = userCredentials.isEnabled(); this.password = userCredentials.getPassword(); @@ -70,9 +71,9 @@ public final class UserCredentialsEntity extends BaseSqlEntity @Override public UserCredentials toData() { UserCredentials userCredentials = new UserCredentials(new UserCredentialsId(this.getUuid())); - userCredentials.setCreatedTime(UUIDs.unixTimestamp(this.getUuid())); + userCredentials.setCreatedTime(createdTime); if (userId != null) { - userCredentials.setUserId(new UserId(toUUID(userId))); + userCredentials.setUserId(new UserId(userId)); } userCredentials.setEnabled(enabled); userCredentials.setPassword(password); @@ -81,4 +82,4 @@ public final class UserCredentialsEntity extends BaseSqlEntity return userCredentials; } -} \ No newline at end of file +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserEntity.java index 3af671583e..5715588278 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserEntity.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.model.sql; -import com.datastax.driver.core.utils.UUIDs; import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import lombok.EqualsAndHashCode; @@ -36,9 +35,7 @@ import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.persistence.Table; - -import static org.thingsboard.server.common.data.UUIDConverter.fromString; -import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID; +import java.util.UUID; /** * Created by Valerii Sosliuk on 4/21/2017. @@ -51,10 +48,10 @@ import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID; public class UserEntity extends BaseSqlEntity implements SearchTextEntity { @Column(name = ModelConstants.USER_TENANT_ID_PROPERTY) - private String tenantId; + private UUID tenantId; @Column(name = ModelConstants.USER_CUSTOMER_ID_PROPERTY) - private String customerId; + private UUID customerId; @Enumerated(EnumType.STRING) @Column(name = ModelConstants.USER_AUTHORITY_PROPERTY) @@ -83,12 +80,13 @@ public class UserEntity extends BaseSqlEntity implements SearchTextEntity< if (user.getId() != null) { this.setUuid(user.getId().getId()); } + this.setCreatedTime(user.getCreatedTime()); this.authority = user.getAuthority(); if (user.getTenantId() != null) { - this.tenantId = fromTimeUUID(user.getTenantId().getId()); + this.tenantId = user.getTenantId().getId(); } if (user.getCustomerId() != null) { - this.customerId = fromTimeUUID(user.getCustomerId().getId()); + this.customerId = user.getCustomerId().getId(); } this.email = user.getEmail(); this.firstName = user.getFirstName(); @@ -109,13 +107,13 @@ public class UserEntity extends BaseSqlEntity implements SearchTextEntity< @Override public User toData() { User user = new User(new UserId(this.getUuid())); - user.setCreatedTime(UUIDs.unixTimestamp(this.getUuid())); + user.setCreatedTime(createdTime); user.setAuthority(authority); if (tenantId != null) { - user.setTenantId(new TenantId(fromString(tenantId))); + user.setTenantId(new TenantId(tenantId)); } if (customerId != null) { - user.setCustomerId(new CustomerId(fromString(customerId))); + user.setCustomerId(new CustomerId(customerId)); } user.setEmail(email); user.setFirstName(firstName); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetTypeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetTypeEntity.java index 0cb69b9d8b..d0b875968e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetTypeEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetTypeEntity.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.model.sql; -import com.datastax.driver.core.utils.UUIDs; import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import lombok.EqualsAndHashCode; @@ -32,6 +31,7 @@ import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Table; +import java.util.UUID; @Data @EqualsAndHashCode(callSuper = true) @@ -41,7 +41,7 @@ import javax.persistence.Table; public final class WidgetTypeEntity extends BaseSqlEntity implements BaseEntity { @Column(name = ModelConstants.WIDGET_TYPE_TENANT_ID_PROPERTY) - private String tenantId; + private UUID tenantId; @Column(name = ModelConstants.WIDGET_TYPE_BUNDLE_ALIAS_PROPERTY) private String bundleAlias; @@ -64,8 +64,9 @@ public final class WidgetTypeEntity extends BaseSqlEntity implement if (widgetType.getId() != null) { this.setUuid(widgetType.getId().getId()); } + this.setCreatedTime(widgetType.getCreatedTime()); if (widgetType.getTenantId() != null) { - this.tenantId = toString(widgetType.getTenantId().getId()); + this.tenantId = widgetType.getTenantId().getId(); } this.bundleAlias = widgetType.getBundleAlias(); this.alias = widgetType.getAlias(); @@ -76,9 +77,9 @@ public final class WidgetTypeEntity extends BaseSqlEntity implement @Override public WidgetType toData() { WidgetType widgetType = new WidgetType(new WidgetTypeId(this.getUuid())); - widgetType.setCreatedTime(UUIDs.unixTimestamp(this.getUuid())); + widgetType.setCreatedTime(createdTime); if (tenantId != null) { - widgetType.setTenantId(new TenantId(toUUID(tenantId))); + widgetType.setTenantId(new TenantId(tenantId)); } widgetType.setBundleAlias(bundleAlias); widgetType.setAlias(alias); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetsBundleEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetsBundleEntity.java index 75ea383090..e2d2855b2a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetsBundleEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetsBundleEntity.java @@ -16,10 +16,8 @@ package org.thingsboard.server.dao.model.sql; -import com.datastax.driver.core.utils.UUIDs; import lombok.Data; import lombok.EqualsAndHashCode; -import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.widget.WidgetsBundle; @@ -30,6 +28,7 @@ import org.thingsboard.server.dao.model.SearchTextEntity; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Table; +import java.util.UUID; @Data @EqualsAndHashCode(callSuper = true) @@ -38,7 +37,7 @@ import javax.persistence.Table; public final class WidgetsBundleEntity extends BaseSqlEntity implements SearchTextEntity { @Column(name = ModelConstants.WIDGETS_BUNDLE_TENANT_ID_PROPERTY) - private String tenantId; + private UUID tenantId; @Column(name = ModelConstants.WIDGETS_BUNDLE_ALIAS_PROPERTY) private String alias; @@ -57,8 +56,9 @@ public final class WidgetsBundleEntity extends BaseSqlEntity impl if (widgetsBundle.getId() != null) { this.setUuid(widgetsBundle.getId().getId()); } + this.setCreatedTime(widgetsBundle.getCreatedTime()); if (widgetsBundle.getTenantId() != null) { - this.tenantId = UUIDConverter.fromTimeUUID(widgetsBundle.getTenantId().getId()); + this.tenantId = widgetsBundle.getTenantId().getId(); } this.alias = widgetsBundle.getAlias(); this.title = widgetsBundle.getTitle(); @@ -76,10 +76,10 @@ public final class WidgetsBundleEntity extends BaseSqlEntity impl @Override public WidgetsBundle toData() { - WidgetsBundle widgetsBundle = new WidgetsBundle(new WidgetsBundleId(UUIDConverter.fromString(id))); - widgetsBundle.setCreatedTime(UUIDs.unixTimestamp(UUIDConverter.fromString(id))); + WidgetsBundle widgetsBundle = new WidgetsBundle(new WidgetsBundleId(id)); + widgetsBundle.setCreatedTime(createdTime); if (tenantId != null) { - widgetsBundle.setTenantId(new TenantId(UUIDConverter.fromString(tenantId))); + widgetsBundle.setTenantId(new TenantId(tenantId)); } widgetsBundle.setAlias(alias); widgetsBundle.setTitle(title); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/latest/TsKvLatestCompositeKey.java b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/latest/TsKvLatestCompositeKey.java index 69c9c26a9b..77f2fa29d6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/latest/TsKvLatestCompositeKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/latest/TsKvLatestCompositeKey.java @@ -18,7 +18,6 @@ package org.thingsboard.server.dao.model.sqlts.latest; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import org.thingsboard.server.common.data.EntityType; import javax.persistence.Transient; import java.io.Serializable; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/latest/TsKvLatestEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/latest/TsKvLatestEntity.java index e7de4afa67..7894e0e4b0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/latest/TsKvLatestEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/latest/TsKvLatestEntity.java @@ -19,11 +19,9 @@ import lombok.Data; import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity; import org.thingsboard.server.dao.sqlts.latest.SearchTsKvLatestRepository; -import javax.persistence.Column; import javax.persistence.ColumnResult; import javax.persistence.ConstructorResult; import javax.persistence.Entity; -import javax.persistence.Id; import javax.persistence.IdClass; import javax.persistence.NamedNativeQueries; import javax.persistence.NamedNativeQuery; @@ -32,8 +30,6 @@ import javax.persistence.SqlResultSetMappings; import javax.persistence.Table; import java.util.UUID; -import static org.thingsboard.server.dao.model.ModelConstants.KEY_COLUMN; - @Data @Entity @Table(name = "ts_kv_latest") diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvEntity.java index 3a14d0c957..ab1df90b39 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvEntity.java @@ -18,14 +18,10 @@ package org.thingsboard.server.dao.model.sqlts.ts; import lombok.Data; import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity; -import javax.persistence.Column; import javax.persistence.Entity; -import javax.persistence.Id; import javax.persistence.IdClass; import javax.persistence.Table; -import static org.thingsboard.server.dao.model.ModelConstants.KEY_COLUMN; - @Data @Entity @Table(name = "ts_kv") diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/type/ActionStatusCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/ActionStatusCodec.java deleted file mode 100644 index 9dfbff08db..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/type/ActionStatusCodec.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.type; - -import com.datastax.driver.extras.codecs.enums.EnumNameCodec; -import org.thingsboard.server.common.data.audit.ActionStatus; - -public class ActionStatusCodec extends EnumNameCodec { - - public ActionStatusCodec() { - super(ActionStatus.class); - } -} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/type/ActionTypeCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/ActionTypeCodec.java deleted file mode 100644 index ad162eb78b..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/type/ActionTypeCodec.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.type; - -import com.datastax.driver.extras.codecs.enums.EnumNameCodec; -import org.thingsboard.server.common.data.audit.ActionType; - -public class ActionTypeCodec extends EnumNameCodec { - - public ActionTypeCodec() { - super(ActionType.class); - } -} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/type/AlarmSeverityCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/AlarmSeverityCodec.java deleted file mode 100644 index c43915383a..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/type/AlarmSeverityCodec.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.type; - -import com.datastax.driver.extras.codecs.enums.EnumNameCodec; -import org.thingsboard.server.common.data.alarm.AlarmSeverity; - -public class AlarmSeverityCodec extends EnumNameCodec { - - public AlarmSeverityCodec() { - super(AlarmSeverity.class); - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/type/AlarmStatusCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/AlarmStatusCodec.java deleted file mode 100644 index 9af8cdfd02..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/type/AlarmStatusCodec.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.type; - -import com.datastax.driver.extras.codecs.enums.EnumNameCodec; -import org.thingsboard.server.common.data.alarm.AlarmStatus; - -public class AlarmStatusCodec extends EnumNameCodec { - - public AlarmStatusCodec() { - super(AlarmStatus.class); - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/type/AuthorityCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/AuthorityCodec.java deleted file mode 100644 index 0101a36ea9..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/type/AuthorityCodec.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.type; - -import com.datastax.driver.extras.codecs.enums.EnumNameCodec; -import org.thingsboard.server.common.data.security.Authority; - -public class AuthorityCodec extends EnumNameCodec { - - public AuthorityCodec() { - super(Authority.class); - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/type/ComponentLifecycleStateCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/ComponentLifecycleStateCodec.java deleted file mode 100644 index 6d7f4bb8bf..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/type/ComponentLifecycleStateCodec.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.type; - -import com.datastax.driver.extras.codecs.enums.EnumNameCodec; -import org.thingsboard.server.common.data.plugin.ComponentLifecycleState; - -public class ComponentLifecycleStateCodec extends EnumNameCodec { - - public ComponentLifecycleStateCodec() { - super(ComponentLifecycleState.class); - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/type/ComponentScopeCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/ComponentScopeCodec.java deleted file mode 100644 index 1351974d25..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/type/ComponentScopeCodec.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.type; - -import com.datastax.driver.extras.codecs.enums.EnumNameCodec; -import org.thingsboard.server.common.data.plugin.ComponentScope; - -public class ComponentScopeCodec extends EnumNameCodec { - - public ComponentScopeCodec() { - super(ComponentScope.class); - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/type/ComponentTypeCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/ComponentTypeCodec.java deleted file mode 100644 index 367758ac3d..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/type/ComponentTypeCodec.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.type; - -import com.datastax.driver.extras.codecs.enums.EnumNameCodec; -import org.thingsboard.server.common.data.plugin.ComponentType; - -public class ComponentTypeCodec extends EnumNameCodec { - - public ComponentTypeCodec() { - super(ComponentType.class); - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/type/DeviceCredentialsTypeCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/DeviceCredentialsTypeCodec.java deleted file mode 100644 index 2a5a05a831..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/type/DeviceCredentialsTypeCodec.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.type; - -import com.datastax.driver.extras.codecs.enums.EnumNameCodec; -import org.thingsboard.server.common.data.security.DeviceCredentialsType; - -public class DeviceCredentialsTypeCodec extends EnumNameCodec { - - public DeviceCredentialsTypeCodec() { - super(DeviceCredentialsType.class); - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/type/EntityTypeCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/EntityTypeCodec.java deleted file mode 100644 index 3ec7668c14..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/type/EntityTypeCodec.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.type; - -import com.datastax.driver.extras.codecs.enums.EnumNameCodec; -import org.thingsboard.server.common.data.EntityType; - -public class EntityTypeCodec extends EnumNameCodec { - - public EntityTypeCodec() { - super(EntityType.class); - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/type/JsonCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/JsonCodec.java deleted file mode 100644 index fae62c73fe..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/type/JsonCodec.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.type; - -import com.datastax.driver.extras.codecs.json.JacksonJsonCodec; -import com.fasterxml.jackson.databind.JsonNode; - -public class JsonCodec extends JacksonJsonCodec { - - public JsonCodec() { - super(JsonNode.class); - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/type/RelationTypeGroupCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/RelationTypeGroupCodec.java deleted file mode 100644 index bb45e21f8d..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/type/RelationTypeGroupCodec.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.type; - -import com.datastax.driver.extras.codecs.enums.EnumNameCodec; -import org.thingsboard.server.common.data.relation.RelationTypeGroup; - -public class RelationTypeGroupCodec extends EnumNameCodec { - - public RelationTypeGroupCodec() { - super(RelationTypeGroup.class); - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/wrapper/EntityResultSet.java b/dao/src/main/java/org/thingsboard/server/dao/model/wrapper/EntityResultSet.java deleted file mode 100644 index 41d11d7cb5..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/wrapper/EntityResultSet.java +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.wrapper; - - -import com.datastax.driver.core.ResultSet; - -public class EntityResultSet { - - private ResultSet resultSet; - private T entity; - - public EntityResultSet(ResultSet resultSet, T entity) { - this.resultSet = resultSet; - this.entity = entity; - } - - public T getEntity() { - return entity; - } - - public boolean wasApplied() { - return resultSet.wasApplied(); - } -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractAsyncDao.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractAsyncDao.java index 702df73614..76ed6d2cc6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractAsyncDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractAsyncDao.java @@ -15,9 +15,8 @@ */ package org.thingsboard.server.dao.nosql; -import com.datastax.driver.core.ResultSet; -import com.datastax.driver.core.ResultSetFuture; import com.google.common.base.Function; +import com.google.common.util.concurrent.AsyncFunction; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.common.util.ThingsBoardThreadFactory; @@ -47,13 +46,28 @@ public abstract class CassandraAbstractAsyncDao extends CassandraAbstractDao { } } - protected ListenableFuture getFuture(ResultSetFuture future, java.util.function.Function transformer) { - return Futures.transform(future, new Function() { + protected ListenableFuture getFuture(TbResultSetFuture future, java.util.function.Function transformer) { + return Futures.transform(future, new Function() { @Nullable @Override - public T apply(@Nullable ResultSet input) { + public T apply(@Nullable TbResultSet input) { return transformer.apply(input); } }, readResultsProcessingExecutor); } + + protected ListenableFuture getFutureAsync(TbResultSetFuture future, com.google.common.util.concurrent.AsyncFunction transformer) { + return Futures.transformAsync(future, new AsyncFunction() { + @Nullable + @Override + public ListenableFuture apply(@Nullable TbResultSet input) { + try { + return transformer.apply(input); + } catch (Exception e) { + return Futures.immediateFailedFuture(e); + } + } + }, readResultsProcessingExecutor); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java index 2f6f8626fd..aa711146f8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java @@ -15,28 +15,17 @@ */ package org.thingsboard.server.dao.nosql; -import com.datastax.driver.core.BoundStatement; -import com.datastax.driver.core.CodecRegistry; -import com.datastax.driver.core.ConsistencyLevel; -import com.datastax.driver.core.PreparedStatement; -import com.datastax.driver.core.ResultSet; -import com.datastax.driver.core.ResultSetFuture; -import com.datastax.driver.core.Session; -import com.datastax.driver.core.Statement; -import com.datastax.driver.core.TypeCodec; -import com.datastax.driver.core.exceptions.CodecNotFoundException; +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.BoundStatement; +import com.datastax.oss.driver.api.core.cql.PreparedStatement; +import com.datastax.oss.driver.api.core.cql.Statement; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.cassandra.CassandraCluster; -import org.thingsboard.server.dao.model.type.AuthorityCodec; -import org.thingsboard.server.dao.model.type.ComponentLifecycleStateCodec; -import org.thingsboard.server.dao.model.type.ComponentScopeCodec; -import org.thingsboard.server.dao.model.type.ComponentTypeCodec; -import org.thingsboard.server.dao.model.type.DeviceCredentialsTypeCodec; -import org.thingsboard.server.dao.model.type.EntityTypeCodec; -import org.thingsboard.server.dao.model.type.JsonCodec; +import org.thingsboard.server.dao.cassandra.guava.GuavaSession; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -53,24 +42,16 @@ public abstract class CassandraAbstractDao { @Autowired private CassandraBufferedRateExecutor rateLimiter; - private Session session; + private GuavaSession session; private ConsistencyLevel defaultReadLevel; private ConsistencyLevel defaultWriteLevel; - private Session getSession() { + private GuavaSession getSession() { if (session == null) { session = cluster.getSession(); defaultReadLevel = cluster.getDefaultReadConsistencyLevel(); defaultWriteLevel = cluster.getDefaultWriteConsistencyLevel(); - CodecRegistry registry = session.getCluster().getConfiguration().getCodecRegistry(); - registerCodecIfNotFound(registry, new JsonCodec()); - registerCodecIfNotFound(registry, new DeviceCredentialsTypeCodec()); - registerCodecIfNotFound(registry, new AuthorityCodec()); - registerCodecIfNotFound(registry, new ComponentLifecycleStateCodec()); - registerCodecIfNotFound(registry, new ComponentTypeCodec()); - registerCodecIfNotFound(registry, new ComponentScopeCodec()); - registerCodecIfNotFound(registry, new EntityTypeCodec()); } return session; } @@ -79,38 +60,30 @@ public abstract class CassandraAbstractDao { return preparedStatementMap.computeIfAbsent(query, i -> getSession().prepare(i)); } - private void registerCodecIfNotFound(CodecRegistry registry, TypeCodec codec) { - try { - registry.codecFor(codec.getCqlType(), codec.getJavaType()); - } catch (CodecNotFoundException e) { - registry.register(codec); - } - } - - protected ResultSet executeRead(TenantId tenantId, Statement statement) { + protected AsyncResultSet executeRead(TenantId tenantId, Statement statement) { return execute(tenantId, statement, defaultReadLevel); } - protected ResultSet executeWrite(TenantId tenantId, Statement statement) { + protected AsyncResultSet executeWrite(TenantId tenantId, Statement statement) { return execute(tenantId, statement, defaultWriteLevel); } - protected ResultSetFuture executeAsyncRead(TenantId tenantId, Statement statement) { + protected TbResultSetFuture executeAsyncRead(TenantId tenantId, Statement statement) { return executeAsync(tenantId, statement, defaultReadLevel); } - protected ResultSetFuture executeAsyncWrite(TenantId tenantId, Statement statement) { + protected TbResultSetFuture executeAsyncWrite(TenantId tenantId, Statement statement) { return executeAsync(tenantId, statement, defaultWriteLevel); } - private ResultSet execute(TenantId tenantId, Statement statement, ConsistencyLevel level) { + private AsyncResultSet execute(TenantId tenantId, Statement statement, ConsistencyLevel level) { if (log.isDebugEnabled()) { log.debug("Execute cassandra statement {}", statementToString(statement)); } return executeAsync(tenantId, statement, level).getUninterruptibly(); } - private ResultSetFuture executeAsync(TenantId tenantId, Statement statement, ConsistencyLevel level) { + private TbResultSetFuture executeAsync(TenantId tenantId, Statement statement, ConsistencyLevel level) { if (log.isDebugEnabled()) { log.debug("Execute cassandra async statement {}", statementToString(statement)); } @@ -122,9 +95,9 @@ public abstract class CassandraAbstractDao { private static String statementToString(Statement statement) { if (statement instanceof BoundStatement) { - return ((BoundStatement) statement).preparedStatement().getQueryString(); + return ((BoundStatement) statement).getPreparedStatement().getQuery(); } else { return statement.toString(); } } -} \ No newline at end of file +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractModelDao.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractModelDao.java deleted file mode 100644 index 2da9915bb2..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractModelDao.java +++ /dev/null @@ -1,196 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.nosql; - -import com.datastax.driver.core.ResultSet; -import com.datastax.driver.core.ResultSetFuture; -import com.datastax.driver.core.Statement; -import com.datastax.driver.core.querybuilder.QueryBuilder; -import com.datastax.driver.core.querybuilder.Select; -import com.datastax.driver.core.utils.UUIDs; -import com.datastax.driver.mapping.Mapper; -import com.datastax.driver.mapping.Result; -import com.google.common.base.Function; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; -import lombok.extern.slf4j.Slf4j; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.Dao; -import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.model.BaseEntity; -import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.wrapper.EntityResultSet; - -import javax.annotation.Nullable; -import java.util.Collections; -import java.util.List; -import java.util.UUID; - -import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; -import static com.datastax.driver.core.querybuilder.QueryBuilder.select; - -@Slf4j -public abstract class CassandraAbstractModelDao, D> extends CassandraAbstractDao implements Dao { - - protected abstract Class getColumnFamilyClass(); - - protected abstract String getColumnFamilyName(); - - protected E updateSearchTextIfPresent(E entity) { - return entity; - } - - protected Mapper getMapper() { - return cluster.getMapper(getColumnFamilyClass()); - } - - protected List findListByStatement(TenantId tenantId, Statement statement) { - List list = Collections.emptyList(); - if (statement != null) { - statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel()); - ResultSet resultSet = executeRead(tenantId, statement); - Result result = getMapper().map(resultSet); - if (result != null) { - list = result.all(); - } - } - return list; - } - - protected ListenableFuture> findListByStatementAsync(TenantId tenantId, Statement statement) { - if (statement != null) { - statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel()); - ResultSetFuture resultSetFuture = executeAsyncRead(tenantId, statement); - return Futures.transform(resultSetFuture, new Function>() { - @Nullable - @Override - public List apply(@Nullable ResultSet resultSet) { - Result result = getMapper().map(resultSet); - if (result != null) { - List entities = result.all(); - return DaoUtil.convertDataList(entities); - } else { - return Collections.emptyList(); - } - } - }, MoreExecutors.directExecutor()); - } - return Futures.immediateFuture(Collections.emptyList()); - } - - protected E findOneByStatement(TenantId tenantId, Statement statement) { - E object = null; - if (statement != null) { - statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel()); - ResultSet resultSet = executeRead(tenantId, statement); - Result result = getMapper().map(resultSet); - if (result != null) { - object = result.one(); - } - } - return object; - } - - protected ListenableFuture findOneByStatementAsync(TenantId tenantId, Statement statement) { - if (statement != null) { - statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel()); - ResultSetFuture resultSetFuture = executeAsyncRead(tenantId, statement); - return Futures.transform(resultSetFuture, new Function() { - @Nullable - @Override - public D apply(@Nullable ResultSet resultSet) { - Result result = getMapper().map(resultSet); - if (result != null) { - E entity = result.one(); - return DaoUtil.getData(entity); - } else { - return null; - } - } - }, MoreExecutors.directExecutor()); - } - return Futures.immediateFuture(null); - } - - protected Statement getSaveQuery(E dto) { - return getMapper().saveQuery(dto); - } - - protected EntityResultSet saveWithResult(TenantId tenantId, E entity) { - log.debug("Save entity {}", entity); - if (entity.getUuid() == null) { - entity.setUuid(UUIDs.timeBased()); - } else if (isDeleteOnSave()) { - removeById(tenantId, entity.getUuid()); - } - Statement saveStatement = getSaveQuery(entity); - saveStatement.setConsistencyLevel(cluster.getDefaultWriteConsistencyLevel()); - ResultSet resultSet = executeWrite(tenantId, saveStatement); - return new EntityResultSet<>(resultSet, entity); - } - - protected boolean isDeleteOnSave() { - return true; - } - - @Override - public D save(TenantId tenantId, D domain) { - E entity; - try { - entity = getColumnFamilyClass().getConstructor(domain.getClass()).newInstance(domain); - } catch (Exception e) { - log.error("Can't create entity for domain object {}", domain, e); - throw new IllegalArgumentException("Can't create entity for domain object {" + domain + "}", e); - } - entity = updateSearchTextIfPresent(entity); - log.debug("Saving entity {}", entity); - entity = saveWithResult(tenantId, entity).getEntity(); - return DaoUtil.getData(entity); - } - - @Override - public D findById(TenantId tenantId, UUID key) { - log.debug("Get entity by key {}", key); - Select.Where query = select().from(getColumnFamilyName()).where(eq(ModelConstants.ID_PROPERTY, key)); - log.trace("Execute query {}", query); - E entity = findOneByStatement(tenantId, query); - return DaoUtil.getData(entity); - } - - @Override - public ListenableFuture findByIdAsync(TenantId tenantId, UUID key) { - log.debug("Get entity by key {}", key); - Select.Where query = select().from(getColumnFamilyName()).where(eq(ModelConstants.ID_PROPERTY, key)); - log.trace("Execute query {}", query); - return findOneByStatementAsync(tenantId, query); - } - - @Override - public boolean removeById(TenantId tenantId, UUID key) { - Statement delete = QueryBuilder.delete().all().from(getColumnFamilyName()).where(eq(ModelConstants.ID_PROPERTY, key)); - log.debug("Remove request: {}", delete.toString()); - return executeWrite(tenantId, delete).wasApplied(); - } - - @Override - public List find(TenantId tenantId) { - log.debug("Get all entities from column family {}", getColumnFamilyName()); - List entities = findListByStatement(tenantId, QueryBuilder.select().all().from(getColumnFamilyName()).setConsistencyLevel(cluster.getDefaultReadConsistencyLevel())); - return DaoUtil.convertDataList(entities); - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractSearchTextDao.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractSearchTextDao.java deleted file mode 100644 index e921cc279a..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractSearchTextDao.java +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.nosql; - -import com.datastax.driver.core.querybuilder.Clause; -import com.datastax.driver.core.querybuilder.QueryBuilder; -import com.datastax.driver.core.querybuilder.Select; -import com.datastax.driver.core.querybuilder.Select.Where; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; -import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.SearchTextEntity; - -import java.util.List; - -import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; -import static com.datastax.driver.core.querybuilder.QueryBuilder.select; - -@Slf4j -public abstract class CassandraAbstractSearchTextDao, D> extends CassandraAbstractModelDao { - - @Override - protected E updateSearchTextIfPresent(E entity) { - if (entity.getSearchTextSource() != null) { - entity.setSearchText(entity.getSearchTextSource().toLowerCase()); - } else { - log.trace("Entity [{}] has null SearchTextSource", entity); - } - return entity; - } - - protected List findPageWithTextSearch(TenantId tenantId, String searchView, List clauses, TextPageLink pageLink) { - Select select = select().from(searchView); - Where query = select.where(); - for (Clause clause : clauses) { - query.and(clause); - } - query.limit(pageLink.getLimit()); - if (!StringUtils.isEmpty(pageLink.getTextOffset())) { - query.and(eq(ModelConstants.SEARCH_TEXT_PROPERTY, pageLink.getTextOffset())); - query.and(QueryBuilder.lt(ModelConstants.ID_PROPERTY, pageLink.getIdOffset())); - List result = findListByStatement(tenantId, query); - if (result.size() < pageLink.getLimit()) { - select = select().from(searchView); - query = select.where(); - for (Clause clause : clauses) { - query.and(clause); - } - query.and(QueryBuilder.gt(ModelConstants.SEARCH_TEXT_PROPERTY, pageLink.getTextOffset())); - if (!StringUtils.isEmpty(pageLink.getTextSearch())) { - query.and(QueryBuilder.lt(ModelConstants.SEARCH_TEXT_PROPERTY, pageLink.getTextSearchBound())); - } - int limit = pageLink.getLimit() - result.size(); - query.limit(limit); - result.addAll(findListByStatement(tenantId, query)); - } - return result; - } else if (!StringUtils.isEmpty(pageLink.getTextSearch())) { - query.and(QueryBuilder.gte(ModelConstants.SEARCH_TEXT_PROPERTY, pageLink.getTextSearch())); - query.and(QueryBuilder.lt(ModelConstants.SEARCH_TEXT_PROPERTY, pageLink.getTextSearchBound())); - return findListByStatement(tenantId, query); - } else { - return findListByStatement(tenantId, query); - } - } - - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractSearchTimeDao.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractSearchTimeDao.java deleted file mode 100644 index 5b9e873a5a..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractSearchTimeDao.java +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.nosql; - -import com.datastax.driver.core.querybuilder.Clause; -import com.datastax.driver.core.querybuilder.Ordering; -import com.datastax.driver.core.querybuilder.QueryBuilder; -import com.datastax.driver.core.querybuilder.Select; -import com.datastax.driver.core.querybuilder.Select.Where; -import com.datastax.driver.core.utils.UUIDs; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TimePageLink; -import org.thingsboard.server.dao.model.BaseEntity; -import org.thingsboard.server.dao.model.ModelConstants; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.UUID; - -import static com.datastax.driver.core.querybuilder.QueryBuilder.select; - -public abstract class CassandraAbstractSearchTimeDao, D> extends CassandraAbstractModelDao { - - - protected List findPageWithTimeSearch(TenantId tenantId, String searchView, List clauses, TimePageLink pageLink) { - return findPageWithTimeSearch(tenantId, searchView, clauses, Collections.emptyList(), pageLink); - } - - protected List findPageWithTimeSearch(TenantId tenantId, String searchView, List clauses, Ordering ordering, TimePageLink pageLink) { - return findPageWithTimeSearch(tenantId, searchView, clauses, Collections.singletonList(ordering), pageLink); - } - - protected List findPageWithTimeSearch(TenantId tenantId, String searchView, List clauses, List topLevelOrderings, TimePageLink pageLink) { - return findPageWithTimeSearch(tenantId, searchView, clauses, topLevelOrderings, pageLink, ModelConstants.ID_PROPERTY); - } - - protected List findPageWithTimeSearch(TenantId tenantId, String searchView, List clauses, TimePageLink pageLink, String idColumn) { - return findPageWithTimeSearch(tenantId, searchView, clauses, Collections.emptyList(), pageLink, idColumn); - } - - protected List findPageWithTimeSearch(TenantId tenantId, String searchView, List clauses, List topLevelOrderings, TimePageLink pageLink, String idColumn) { - return findListByStatement(tenantId, buildQuery(searchView, clauses, topLevelOrderings, pageLink, idColumn)); - } - - public static Where buildQuery(String searchView, List clauses, TimePageLink pageLink, String idColumn) { - return buildQuery(searchView, clauses, Collections.emptyList(), pageLink, idColumn); - } - - public static Where buildQuery(String searchView, List clauses, Ordering order, TimePageLink pageLink, String idColumn) { - return buildQuery(searchView, clauses, Collections.singletonList(order), pageLink, idColumn); - } - - public static Where buildQuery(String searchView, List clauses, List topLevelOrderings, TimePageLink pageLink, String idColumn) { - Select select = select().from(searchView); - Where query = select.where(); - for (Clause clause : clauses) { - query.and(clause); - } - query.limit(pageLink.getLimit()); - if (pageLink.isAscOrder()) { - if (pageLink.getIdOffset() != null) { - query.and(QueryBuilder.gt(idColumn, pageLink.getIdOffset())); - } else if (pageLink.getStartTime() != null) { - final UUID startOf = UUIDs.startOf(pageLink.getStartTime()); - query.and(QueryBuilder.gte(idColumn, startOf)); - } - if (pageLink.getEndTime() != null) { - final UUID endOf = UUIDs.endOf(pageLink.getEndTime()); - query.and(QueryBuilder.lte(idColumn, endOf)); - } - } else { - if (pageLink.getIdOffset() != null) { - query.and(QueryBuilder.lt(idColumn, pageLink.getIdOffset())); - } else if (pageLink.getEndTime() != null) { - final UUID endOf = UUIDs.endOf(pageLink.getEndTime()); - query.and(QueryBuilder.lte(idColumn, endOf)); - } - if (pageLink.getStartTime() != null) { - final UUID startOf = UUIDs.startOf(pageLink.getStartTime()); - query.and(QueryBuilder.gte(idColumn, startOf)); - } - } - List orderings = new ArrayList<>(topLevelOrderings); - if (pageLink.isAscOrder()) { - orderings.add(QueryBuilder.asc(idColumn)); - } else { - orderings.add(QueryBuilder.desc(idColumn)); - } - query.orderBy(orderings.toArray(new Ordering[orderings.size()])); - return query; - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateExecutor.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateExecutor.java index 597b2ecbcd..8bdac7a777 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateExecutor.java +++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateExecutor.java @@ -15,8 +15,7 @@ */ package org.thingsboard.server.dao.nosql; -import com.datastax.driver.core.ResultSet; -import com.datastax.driver.core.ResultSetFuture; +import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -42,7 +41,7 @@ import java.util.Map; @Component @Slf4j @NoSqlAnyDao -public class CassandraBufferedRateExecutor extends AbstractBufferedRateExecutor { +public class CassandraBufferedRateExecutor extends AbstractBufferedRateExecutor { @Autowired private EntityService entityService; @@ -120,19 +119,22 @@ public class CassandraBufferedRateExecutor extends AbstractBufferedRateExecutor< } @Override - protected SettableFuture create() { + protected SettableFuture create() { return SettableFuture.create(); } @Override - protected ResultSetFuture wrap(CassandraStatementTask task, SettableFuture future) { + protected TbResultSetFuture wrap(CassandraStatementTask task, SettableFuture future) { return new TbResultSetFuture(future); } @Override - protected ResultSetFuture execute(AsyncTaskContext taskCtx) { + protected ListenableFuture execute(AsyncTaskContext taskCtx) { CassandraStatementTask task = taskCtx.getTask(); - return task.getSession().executeAsync(task.getStatement()); + return task.executeAsync( + statement -> + this.submit(new CassandraStatementTask(task.getTenantId(), task.getSession(), statement)) + ); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFuture.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFuture.java deleted file mode 100644 index ebbe451b01..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFuture.java +++ /dev/null @@ -1,156 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.nosql; - -import com.datastax.driver.core.ResultSet; -import com.datastax.driver.core.ResultSetFuture; -import com.datastax.driver.core.Session; -import com.datastax.driver.core.Statement; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; -import com.google.common.util.concurrent.Uninterruptibles; -import org.thingsboard.server.dao.exception.BufferLimitException; -import org.thingsboard.server.dao.util.AsyncRateLimiter; - -import javax.annotation.Nullable; -import java.util.concurrent.CancellationException; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -public class RateLimitedResultSetFuture implements ResultSetFuture { - - private final ListenableFuture originalFuture; - private final ListenableFuture rateLimitFuture; - - public RateLimitedResultSetFuture(Session session, AsyncRateLimiter rateLimiter, Statement statement) { - this.rateLimitFuture = Futures.catchingAsync(rateLimiter.acquireAsync(), Throwable.class, t -> { - if (!(t instanceof BufferLimitException)) { - rateLimiter.release(); - } - return Futures.immediateFailedFuture(t); - }, MoreExecutors.directExecutor()); - this.originalFuture = Futures.transform(rateLimitFuture, - i -> executeAsyncWithRelease(rateLimiter, session, statement), MoreExecutors.directExecutor()); - - } - - @Override - public ResultSet getUninterruptibly() { - return safeGet().getUninterruptibly(); - } - - @Override - public ResultSet getUninterruptibly(long timeout, TimeUnit unit) throws TimeoutException { - long rateLimitStart = System.nanoTime(); - ResultSetFuture resultSetFuture = null; - try { - resultSetFuture = originalFuture.get(timeout, unit); - } catch (InterruptedException | ExecutionException e) { - throw new IllegalStateException(e); - } - long rateLimitDurationNano = System.nanoTime() - rateLimitStart; - long innerTimeoutNano = unit.toNanos(timeout) - rateLimitDurationNano; - if (innerTimeoutNano > 0) { - return resultSetFuture.getUninterruptibly(innerTimeoutNano, TimeUnit.NANOSECONDS); - } - throw new TimeoutException("Timeout waiting for task."); - } - - @Override - public boolean cancel(boolean mayInterruptIfRunning) { - if (originalFuture.isDone()) { - return safeGet().cancel(mayInterruptIfRunning); - } else { - return originalFuture.cancel(mayInterruptIfRunning); - } - } - - @Override - public boolean isCancelled() { - if (originalFuture.isDone()) { - return safeGet().isCancelled(); - } - - return originalFuture.isCancelled(); - } - - @Override - public boolean isDone() { - return originalFuture.isDone() && safeGet().isDone(); - } - - @Override - public ResultSet get() throws InterruptedException, ExecutionException { - return safeGet().get(); - } - - @Override - public ResultSet get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { - long rateLimitStart = System.nanoTime(); - ResultSetFuture resultSetFuture = originalFuture.get(timeout, unit); - long rateLimitDurationNano = System.nanoTime() - rateLimitStart; - long innerTimeoutNano = unit.toNanos(timeout) - rateLimitDurationNano; - if (innerTimeoutNano > 0) { - return resultSetFuture.get(innerTimeoutNano, TimeUnit.NANOSECONDS); - } - throw new TimeoutException("Timeout waiting for task."); - } - - @Override - public void addListener(Runnable listener, Executor executor) { - originalFuture.addListener(() -> { - try { - ResultSetFuture resultSetFuture = Uninterruptibles.getUninterruptibly(originalFuture); - resultSetFuture.addListener(listener, executor); - } catch (CancellationException | ExecutionException e) { - Futures.immediateFailedFuture(e).addListener(listener, executor); - } - }, executor); - } - - private ResultSetFuture safeGet() { - try { - return originalFuture.get(); - } catch (InterruptedException | ExecutionException e) { - throw new IllegalStateException(e); - } - } - - private ResultSetFuture executeAsyncWithRelease(AsyncRateLimiter rateLimiter, Session session, Statement statement) { - try { - ResultSetFuture resultSetFuture = session.executeAsync(statement); - Futures.addCallback(resultSetFuture, new FutureCallback() { - @Override - public void onSuccess(@Nullable ResultSet result) { - rateLimiter.release(); - } - - @Override - public void onFailure(Throwable t) { - rateLimiter.release(); - } - }, MoreExecutors.directExecutor()); - return resultSetFuture; - } catch (RuntimeException re) { - rateLimiter.release(); - throw re; - } - } -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/TbResultSetFuture.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/TbResultSetFuture.java deleted file mode 100644 index d0d2f0dc7e..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/nosql/TbResultSetFuture.java +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.nosql; - -import com.datastax.driver.core.ResultSet; -import com.datastax.driver.core.ResultSetFuture; -import com.google.common.util.concurrent.SettableFuture; - -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -/** - * Created by ashvayka on 24.10.18. - */ -public class TbResultSetFuture implements ResultSetFuture { - - private final SettableFuture mainFuture; - - public TbResultSetFuture(SettableFuture mainFuture) { - this.mainFuture = mainFuture; - } - - @Override - public ResultSet getUninterruptibly() { - return getSafe(); - } - - @Override - public ResultSet getUninterruptibly(long timeout, TimeUnit unit) throws TimeoutException { - return getSafe(timeout, unit); - } - - @Override - public boolean cancel(boolean mayInterruptIfRunning) { - return mainFuture.cancel(mayInterruptIfRunning); - } - - @Override - public boolean isCancelled() { - return mainFuture.isCancelled(); - } - - @Override - public boolean isDone() { - return mainFuture.isDone(); - } - - @Override - public ResultSet get() throws InterruptedException, ExecutionException { - return mainFuture.get(); - } - - @Override - public ResultSet get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { - return mainFuture.get(timeout, unit); - } - - @Override - public void addListener(Runnable listener, Executor executor) { - mainFuture.addListener(listener, executor); - } - - private ResultSet getSafe() { - try { - return mainFuture.get(); - } catch (InterruptedException | ExecutionException e) { - throw new IllegalStateException(e); - } - } - - private ResultSet getSafe(long timeout, TimeUnit unit) throws TimeoutException { - try { - return mainFuture.get(timeout, unit); - } catch (InterruptedException | ExecutionException e) { - throw new IllegalStateException(e); - } - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java new file mode 100644 index 0000000000..4c9d44a5bc --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java @@ -0,0 +1,59 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.oauth2; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; + +import java.util.UUID; + +@Component +public class HybridClientRegistrationRepository implements ClientRegistrationRepository { + private static final String defaultRedirectUriTemplate = "{baseUrl}/login/oauth2/code/{registrationId}"; + + @Autowired + private OAuth2Service oAuth2Service; + + @Override + public ClientRegistration findByRegistrationId(String registrationId) { + OAuth2ClientRegistrationInfo oAuth2ClientRegistrationInfo = oAuth2Service.findClientRegistrationInfo(UUID.fromString(registrationId)); + return oAuth2ClientRegistrationInfo == null ? + null : toSpringClientRegistration(oAuth2ClientRegistrationInfo); + } + + private ClientRegistration toSpringClientRegistration(OAuth2ClientRegistrationInfo localClientRegistration){ + String registrationId = localClientRegistration.getUuidId().toString(); + return ClientRegistration.withRegistrationId(registrationId) + .clientName(localClientRegistration.getName()) + .clientId(localClientRegistration.getClientId()) + .authorizationUri(localClientRegistration.getAuthorizationUri()) + .clientSecret(localClientRegistration.getClientSecret()) + .tokenUri(localClientRegistration.getAccessTokenUri()) + .scope(localClientRegistration.getScope()) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .userInfoUri(localClientRegistration.getUserInfoUri()) + .userNameAttributeName(localClientRegistration.getUserNameAttributeName()) + .jwkSetUri(localClientRegistration.getJwkSetUri()) + .clientAuthenticationMethod(new ClientAuthenticationMethod(localClientRegistration.getClientAuthenticationMethod())) + .redirectUriTemplate(defaultRedirectUriTemplate) + .build(); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Client.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Client.java deleted file mode 100644 index 9676d55f5f..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Client.java +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.oauth2; - -import lombok.Data; - -@Data -public class OAuth2Client { - - private String loginButtonLabel; - private String loginButtonIcon; - private String clientName; - private String clientId; - private String clientSecret; - private String accessTokenUri; - private String authorizationUri; - private String scope; - private String redirectUriTemplate; - private String jwkSetUri; - private String authorizationGrantType; - private String clientAuthenticationMethod; - private String userInfoUri; - private String userNameAttributeName; - private OAuth2ClientMapperConfig mapperConfig; - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientMapperConfig.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientMapperConfig.java deleted file mode 100644 index f2f23843f2..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientMapperConfig.java +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.oauth2; - -import lombok.Data; - -@Data -public class OAuth2ClientMapperConfig { - - private boolean allowUserCreation; - private boolean activateUser; - private String type; - private BasicOAuth2ClientMapperConfig basic; - private CustomOAuth2ClientMapperConfig custom; - - @Data - public static class BasicOAuth2ClientMapperConfig { - private String emailAttributeKey; - private String firstNameAttributeKey; - private String lastNameAttributeKey; - private String tenantNameStrategy; - private String tenantNamePattern; - private String customerNamePattern; - private boolean alwaysFullScreen; - private String defaultDashboardName; - } - - @Data - public static class CustomOAuth2ClientMapperConfig { - private String url; - private String username; - private String password; - } -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationDao.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationDao.java new file mode 100644 index 0000000000..d56ff34354 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationDao.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.oauth2; + +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistration; +import org.thingsboard.server.dao.Dao; + +public interface OAuth2ClientRegistrationDao extends Dao { + void deleteAll(); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationInfoDao.java new file mode 100644 index 0000000000..a907aefe6b --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationInfoDao.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.oauth2; + +import org.thingsboard.server.common.data.oauth2.ExtendedOAuth2ClientRegistrationInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; +import org.thingsboard.server.common.data.oauth2.SchemeType; +import org.thingsboard.server.dao.Dao; + +import java.util.List; +import java.util.Set; + +public interface OAuth2ClientRegistrationInfoDao extends Dao { + List findAll(); + + List findAllExtended(); + + List findByDomainSchemesAndDomainName(List domainSchemes, String domainName); + + void deleteAll(); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationTemplateDao.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationTemplateDao.java new file mode 100644 index 0000000000..6c377bc067 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationTemplateDao.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.oauth2; + +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; +import org.thingsboard.server.dao.Dao; + +import java.util.List; +import java.util.Optional; + +public interface OAuth2ClientRegistrationTemplateDao extends Dao { + + Optional findByProviderId(String providerId); + + List findAll(); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateServiceImpl.java new file mode 100644 index 0000000000..381d704b51 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateServiceImpl.java @@ -0,0 +1,118 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.oauth2; + +import lombok.extern.slf4j.Slf4j; +import org.hibernate.exception.ConstraintViolationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.oauth2.*; +import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.DataValidator; + +import java.util.List; +import java.util.Optional; + +import static org.thingsboard.server.dao.service.Validator.validateId; +import static org.thingsboard.server.dao.service.Validator.validateString; + +@Slf4j +@Service +public class OAuth2ConfigTemplateServiceImpl extends AbstractEntityService implements OAuth2ConfigTemplateService { + public static final String INCORRECT_CLIENT_REGISTRATION_TEMPLATE_ID = "Incorrect clientRegistrationTemplateId "; + public static final String INCORRECT_CLIENT_REGISTRATION_PROVIDER_ID = "Incorrect clientRegistrationProviderId "; + + @Autowired + private OAuth2ClientRegistrationTemplateDao clientRegistrationTemplateDao; + + @Override + public OAuth2ClientRegistrationTemplate saveClientRegistrationTemplate(OAuth2ClientRegistrationTemplate clientRegistrationTemplate) { + log.trace("Executing saveClientRegistrationTemplate [{}]", clientRegistrationTemplate); + clientRegistrationTemplateValidator.validate(clientRegistrationTemplate, o -> TenantId.SYS_TENANT_ID); + OAuth2ClientRegistrationTemplate savedClientRegistrationTemplate; + try { + savedClientRegistrationTemplate = clientRegistrationTemplateDao.save(TenantId.SYS_TENANT_ID, clientRegistrationTemplate); + } catch (Exception t) { + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("oauth2_template_provider_id_unq_key")) { + throw new DataValidationException("Client registration template with such providerId already exists!"); + } else { + throw t; + } + } + return savedClientRegistrationTemplate; + } + + @Override + public Optional findClientRegistrationTemplateByProviderId(String providerId) { + log.trace("Executing findClientRegistrationTemplateByProviderId [{}]", providerId); + validateString(providerId, INCORRECT_CLIENT_REGISTRATION_PROVIDER_ID + providerId); + return clientRegistrationTemplateDao.findByProviderId(providerId); + } + + @Override + public OAuth2ClientRegistrationTemplate findClientRegistrationTemplateById(OAuth2ClientRegistrationTemplateId templateId) { + log.trace("Executing findClientRegistrationTemplateById [{}]", templateId); + validateId(templateId, INCORRECT_CLIENT_REGISTRATION_TEMPLATE_ID + templateId); + return clientRegistrationTemplateDao.findById(TenantId.SYS_TENANT_ID, templateId.getId()); + } + + @Override + public List findAllClientRegistrationTemplates() { + log.trace("Executing findAllClientRegistrationTemplates"); + return clientRegistrationTemplateDao.findAll(); + } + + @Override + public void deleteClientRegistrationTemplateById(OAuth2ClientRegistrationTemplateId templateId) { + log.trace("Executing deleteClientRegistrationTemplateById [{}]", templateId); + validateId(templateId, INCORRECT_CLIENT_REGISTRATION_TEMPLATE_ID + templateId); + clientRegistrationTemplateDao.removeById(TenantId.SYS_TENANT_ID, templateId.getId()); + } + + private final DataValidator clientRegistrationTemplateValidator = + new DataValidator() { + + @Override + protected void validateCreate(TenantId tenantId, OAuth2ClientRegistrationTemplate clientRegistrationTemplate) { + } + + @Override + protected void validateUpdate(TenantId tenantId, OAuth2ClientRegistrationTemplate clientRegistrationTemplate) { + } + + @Override + protected void validateDataImpl(TenantId tenantId, OAuth2ClientRegistrationTemplate clientRegistrationTemplate) { + if (StringUtils.isEmpty(clientRegistrationTemplate.getProviderId())) { + throw new DataValidationException("Provider ID should be specified!"); + } + if (clientRegistrationTemplate.getMapperConfig() == null) { + throw new DataValidationException("Mapper config should be specified!"); + } + if (clientRegistrationTemplate.getMapperConfig().getType() == null) { + throw new DataValidationException("Mapper type should be specified!"); + } + if (clientRegistrationTemplate.getMapperConfig().getBasic() == null) { + throw new DataValidationException("Basic mapper config should be specified!"); + } + } + }; +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Configuration.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Configuration.java index e54f9a5b3e..8bd620ac2d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Configuration.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Configuration.java @@ -17,66 +17,15 @@ package org.thingsboard.server.dao.oauth2; import lombok.Data; import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; -import org.springframework.security.oauth2.core.AuthorizationGrantType; -import org.springframework.security.oauth2.core.ClientAuthenticationMethod; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; import java.util.Map; @Configuration -@ConditionalOnProperty(prefix = "security.oauth2", value = "enabled", havingValue = "true") @ConfigurationProperties(prefix = "security.oauth2") @Data -@Slf4j public class OAuth2Configuration { - - private boolean enabled; private String loginProcessingUrl; - private Map clients = new HashMap<>(); - - @Bean - public ClientRegistrationRepository clientRegistrationRepository() { - List result = new ArrayList<>(); - for (Map.Entry entry : clients.entrySet()) { - OAuth2Client client = entry.getValue(); - ClientRegistration registration = ClientRegistration.withRegistrationId(entry.getKey()) - .clientId(client.getClientId()) - .authorizationUri(client.getAuthorizationUri()) - .clientSecret(client.getClientSecret()) - .tokenUri(client.getAccessTokenUri()) - .redirectUriTemplate(client.getRedirectUriTemplate()) - .scope(client.getScope().split(",")) - .clientName(client.getClientName()) - .authorizationGrantType(new AuthorizationGrantType(client.getAuthorizationGrantType())) - .userInfoUri(client.getUserInfoUri()) - .userNameAttributeName(client.getUserNameAttributeName()) - .jwkSetUri(client.getJwkSetUri()) - .clientAuthenticationMethod(new ClientAuthenticationMethod(client.getClientAuthenticationMethod())) - .build(); - result.add(registration); - } - return new InMemoryClientRegistrationRepository(result); - } - - public OAuth2Client getClientByRegistrationId(String registrationId) { - OAuth2Client result = null; - if (clients != null && !clients.isEmpty()) { - for (String key : clients.keySet()) { - if (key.equals(registrationId)) { - result = clients.get(key); - break; - } - } - } - return result; - } + private Map githubMapper; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java index 43e11244b0..9fda0e3c2f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java @@ -18,33 +18,198 @@ package org.thingsboard.server.dao.oauth2; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; +import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.oauth2.*; +import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.exception.IncorrectParameterException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import javax.transaction.Transactional; +import java.util.*; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import static org.thingsboard.server.dao.service.Validator.validateId; +import static org.thingsboard.server.dao.service.Validator.validateString; @Slf4j @Service -public class OAuth2ServiceImpl implements OAuth2Service { +public class OAuth2ServiceImpl extends AbstractEntityService implements OAuth2Service { + public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; + public static final String INCORRECT_CLIENT_REGISTRATION_ID = "Incorrect clientRegistrationId "; + public static final String INCORRECT_DOMAIN_NAME = "Incorrect domainName "; + public static final String INCORRECT_DOMAIN_SCHEME = "Incorrect domainScheme "; - @Autowired(required = false) - OAuth2Configuration oauth2Configuration; + @Autowired + private OAuth2ClientRegistrationInfoDao clientRegistrationInfoDao; + @Autowired + private OAuth2ClientRegistrationDao clientRegistrationDao; @Override - public List getOAuth2Clients() { - if (oauth2Configuration == null || !oauth2Configuration.isEnabled()) { - return Collections.emptyList(); + public List getOAuth2Clients(String domainSchemeStr, String domainName) { + log.trace("Executing getOAuth2Clients [{}://{}]", domainSchemeStr, domainName); + if (domainSchemeStr == null) { + throw new IncorrectParameterException(INCORRECT_DOMAIN_SCHEME); } - List result = new ArrayList<>(); - for (Map.Entry entry : oauth2Configuration.getClients().entrySet()) { - OAuth2ClientInfo client = new OAuth2ClientInfo(); - client.setName(entry.getValue().getLoginButtonLabel()); - client.setUrl(String.format("/oauth2/authorization/%s", entry.getKey())); - client.setIcon(entry.getValue().getLoginButtonIcon()); - result.add(client); + SchemeType domainScheme; + try { + domainScheme = SchemeType.valueOf(domainSchemeStr.toUpperCase()); + } catch (IllegalArgumentException e){ + throw new IncorrectParameterException(INCORRECT_DOMAIN_SCHEME); } - return result; + validateString(domainName, INCORRECT_DOMAIN_NAME + domainName); + return clientRegistrationInfoDao.findByDomainSchemesAndDomainName(Arrays.asList(domainScheme, SchemeType.MIXED), domainName).stream() + .filter(OAuth2ClientRegistrationInfo::isEnabled) + .map(OAuth2Utils::toClientInfo) + .collect(Collectors.toList()); + } + + @Override + @Transactional + public void saveOAuth2Params(OAuth2ClientsParams oauth2Params) { + log.trace("Executing saveOAuth2Params [{}]", oauth2Params); + clientParamsValidator.accept(oauth2Params); + clientRegistrationDao.deleteAll(); + clientRegistrationInfoDao.deleteAll(); + oauth2Params.getDomainsParams().forEach(domainParams -> { + domainParams.getClientRegistrations().forEach(clientRegistrationDto -> { + OAuth2ClientRegistrationInfo oAuth2ClientRegistrationInfo = OAuth2Utils.toClientRegistrationInfo(oauth2Params.isEnabled(), clientRegistrationDto); + OAuth2ClientRegistrationInfo savedClientRegistrationInfo = clientRegistrationInfoDao.save(TenantId.SYS_TENANT_ID, oAuth2ClientRegistrationInfo); + domainParams.getDomainInfos().forEach(domainInfo -> { + OAuth2ClientRegistration oAuth2ClientRegistration = OAuth2Utils.toClientRegistration(savedClientRegistrationInfo.getId(), + domainInfo.getScheme(), domainInfo.getName()); + clientRegistrationDao.save(TenantId.SYS_TENANT_ID, oAuth2ClientRegistration); + }); + }); + }); + } + + @Override + public OAuth2ClientsParams findOAuth2Params() { + log.trace("Executing findOAuth2Params"); + List extendedInfos = clientRegistrationInfoDao.findAllExtended(); + return OAuth2Utils.toOAuth2Params(extendedInfos); + } + + @Override + public OAuth2ClientRegistrationInfo findClientRegistrationInfo(UUID id) { + log.trace("Executing findClientRegistrationInfo [{}]", id); + validateId(id, INCORRECT_CLIENT_REGISTRATION_ID + id); + return clientRegistrationInfoDao.findById(null, id); + } + + @Override + public List findAllClientRegistrationInfos() { + log.trace("Executing findAllClientRegistrationInfos"); + return clientRegistrationInfoDao.findAll(); } + + private final Consumer clientParamsValidator = oauth2Params -> { + if (oauth2Params == null + || oauth2Params.getDomainsParams() == null) { + throw new DataValidationException("Domain params should be specified!"); + } + for (OAuth2ClientsDomainParams domainParams : oauth2Params.getDomainsParams()) { + if (domainParams.getDomainInfos() == null + || domainParams.getDomainInfos().isEmpty()) { + throw new DataValidationException("List of domain configuration should be specified!"); + } + for (DomainInfo domainInfo : domainParams.getDomainInfos()) { + if (StringUtils.isEmpty(domainInfo.getName())) { + throw new DataValidationException("Domain name should be specified!"); + } + if (domainInfo.getScheme() == null) { + throw new DataValidationException("Domain scheme should be specified!"); + } + } + domainParams.getDomainInfos().stream() + .collect(Collectors.groupingBy(DomainInfo::getName)) + .forEach((domainName, domainInfos) -> { + if (domainInfos.size() > 1 && domainInfos.stream().anyMatch(domainInfo -> domainInfo.getScheme() == SchemeType.MIXED)) { + throw new DataValidationException("MIXED scheme type shouldn't be combined with another scheme type!"); + } + }); + if (domainParams.getClientRegistrations() == null || domainParams.getClientRegistrations().isEmpty()) { + throw new DataValidationException("Client registrations should be specified!"); + } + for (ClientRegistrationDto clientRegistration : domainParams.getClientRegistrations()) { + if (StringUtils.isEmpty(clientRegistration.getClientId())) { + throw new DataValidationException("Client ID should be specified!"); + } + if (StringUtils.isEmpty(clientRegistration.getClientSecret())) { + throw new DataValidationException("Client secret should be specified!"); + } + if (StringUtils.isEmpty(clientRegistration.getAuthorizationUri())) { + throw new DataValidationException("Authorization uri should be specified!"); + } + if (StringUtils.isEmpty(clientRegistration.getAccessTokenUri())) { + throw new DataValidationException("Token uri should be specified!"); + } + if (StringUtils.isEmpty(clientRegistration.getScope())) { + throw new DataValidationException("Scope should be specified!"); + } + if (StringUtils.isEmpty(clientRegistration.getUserInfoUri())) { + throw new DataValidationException("User info uri should be specified!"); + } + if (StringUtils.isEmpty(clientRegistration.getUserNameAttributeName())) { + throw new DataValidationException("User name attribute name should be specified!"); + } + if (StringUtils.isEmpty(clientRegistration.getClientAuthenticationMethod())) { + throw new DataValidationException("Client authentication method should be specified!"); + } + if (StringUtils.isEmpty(clientRegistration.getLoginButtonLabel())) { + throw new DataValidationException("Login button label should be specified!"); + } + OAuth2MapperConfig mapperConfig = clientRegistration.getMapperConfig(); + if (mapperConfig == null) { + throw new DataValidationException("Mapper config should be specified!"); + } + if (mapperConfig.getType() == null) { + throw new DataValidationException("Mapper config type should be specified!"); + } + if (mapperConfig.getType() == MapperType.BASIC) { + OAuth2BasicMapperConfig basicConfig = mapperConfig.getBasic(); + if (basicConfig == null) { + throw new DataValidationException("Basic config should be specified!"); + } + if (StringUtils.isEmpty(basicConfig.getEmailAttributeKey())) { + throw new DataValidationException("Email attribute key should be specified!"); + } + if (basicConfig.getTenantNameStrategy() == null) { + throw new DataValidationException("Tenant name strategy should be specified!"); + } + if (basicConfig.getTenantNameStrategy() == TenantNameStrategyType.CUSTOM + && StringUtils.isEmpty(basicConfig.getTenantNamePattern())) { + throw new DataValidationException("Tenant name pattern should be specified!"); + } + } + if (mapperConfig.getType() == MapperType.GITHUB) { + OAuth2BasicMapperConfig basicConfig = mapperConfig.getBasic(); + if (basicConfig == null) { + throw new DataValidationException("Basic config should be specified!"); + } + if (!StringUtils.isEmpty(basicConfig.getEmailAttributeKey())) { + throw new DataValidationException("Email attribute key cannot be configured for GITHUB mapper type!"); + } + if (basicConfig.getTenantNameStrategy() == null) { + throw new DataValidationException("Tenant name strategy should be specified!"); + } + if (basicConfig.getTenantNameStrategy() == TenantNameStrategyType.CUSTOM + && StringUtils.isEmpty(basicConfig.getTenantNamePattern())) { + throw new DataValidationException("Tenant name pattern should be specified!"); + } + } + if (mapperConfig.getType() == MapperType.CUSTOM) { + OAuth2CustomMapperConfig customConfig = mapperConfig.getCustom(); + if (customConfig == null) { + throw new DataValidationException("Custom config should be specified!"); + } + if (StringUtils.isEmpty(customConfig.getUrl())) { + throw new DataValidationException("Custom mapper URL should be specified!"); + } + } + } + } + }; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java new file mode 100644 index 0000000000..00b4f7e941 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java @@ -0,0 +1,102 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.oauth2; + +import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationInfoId; +import org.thingsboard.server.common.data.oauth2.*; + +import java.util.*; + +public class OAuth2Utils { + public static final String OAUTH2_AUTHORIZATION_PATH_TEMPLATE = "/oauth2/authorization/%s"; + + public static OAuth2ClientInfo toClientInfo(OAuth2ClientRegistrationInfo clientRegistrationInfo) { + OAuth2ClientInfo client = new OAuth2ClientInfo(); + client.setName(clientRegistrationInfo.getLoginButtonLabel()); + client.setUrl(String.format(OAUTH2_AUTHORIZATION_PATH_TEMPLATE, clientRegistrationInfo.getUuidId().toString())); + client.setIcon(clientRegistrationInfo.getLoginButtonIcon()); + return client; + } + + public static OAuth2ClientsParams toOAuth2Params(List extendedOAuth2ClientRegistrationInfos) { + Map> domainsByInfoId = new LinkedHashMap<>(); + Map infoById = new LinkedHashMap<>(); + for (ExtendedOAuth2ClientRegistrationInfo extendedClientRegistrationInfo : extendedOAuth2ClientRegistrationInfos) { + String domainName = extendedClientRegistrationInfo.getDomainName(); + SchemeType domainScheme = extendedClientRegistrationInfo.getDomainScheme(); + domainsByInfoId.computeIfAbsent(extendedClientRegistrationInfo.getId(), key -> new ArrayList<>()) + .add(new DomainInfo(domainScheme, domainName)); + infoById.put(extendedClientRegistrationInfo.getId(), extendedClientRegistrationInfo); + } + Map, OAuth2ClientsDomainParams> domainParamsMap = new LinkedHashMap<>(); + domainsByInfoId.forEach((clientRegistrationInfoId, domainInfos) -> { + domainParamsMap.computeIfAbsent(domainInfos, + key -> new OAuth2ClientsDomainParams(key, new ArrayList<>()) + ) + .getClientRegistrations() + .add(toClientRegistrationDto(infoById.get(clientRegistrationInfoId))); + }); + boolean enabled = extendedOAuth2ClientRegistrationInfos.stream() + .map(OAuth2ClientRegistrationInfo::isEnabled) + .findFirst().orElse(false); + return new OAuth2ClientsParams(enabled, new ArrayList<>(domainParamsMap.values())); + } + + public static ClientRegistrationDto toClientRegistrationDto(OAuth2ClientRegistrationInfo oAuth2ClientRegistrationInfo) { + return ClientRegistrationDto.builder() + .mapperConfig(oAuth2ClientRegistrationInfo.getMapperConfig()) + .clientId(oAuth2ClientRegistrationInfo.getClientId()) + .clientSecret(oAuth2ClientRegistrationInfo.getClientSecret()) + .authorizationUri(oAuth2ClientRegistrationInfo.getAuthorizationUri()) + .accessTokenUri(oAuth2ClientRegistrationInfo.getAccessTokenUri()) + .scope(oAuth2ClientRegistrationInfo.getScope()) + .userInfoUri(oAuth2ClientRegistrationInfo.getUserInfoUri()) + .userNameAttributeName(oAuth2ClientRegistrationInfo.getUserNameAttributeName()) + .jwkSetUri(oAuth2ClientRegistrationInfo.getJwkSetUri()) + .clientAuthenticationMethod(oAuth2ClientRegistrationInfo.getClientAuthenticationMethod()) + .loginButtonLabel(oAuth2ClientRegistrationInfo.getLoginButtonLabel()) + .loginButtonIcon(oAuth2ClientRegistrationInfo.getLoginButtonIcon()) + .additionalInfo(oAuth2ClientRegistrationInfo.getAdditionalInfo()) + .build(); + } + + public static OAuth2ClientRegistrationInfo toClientRegistrationInfo(boolean enabled, ClientRegistrationDto clientRegistrationDto) { + OAuth2ClientRegistrationInfo clientRegistrationInfo = new OAuth2ClientRegistrationInfo(); + clientRegistrationInfo.setEnabled(enabled); + clientRegistrationInfo.setMapperConfig(clientRegistrationDto.getMapperConfig()); + clientRegistrationInfo.setClientId(clientRegistrationDto.getClientId()); + clientRegistrationInfo.setClientSecret(clientRegistrationDto.getClientSecret()); + clientRegistrationInfo.setAuthorizationUri(clientRegistrationDto.getAuthorizationUri()); + clientRegistrationInfo.setAccessTokenUri(clientRegistrationDto.getAccessTokenUri()); + clientRegistrationInfo.setScope(clientRegistrationDto.getScope()); + clientRegistrationInfo.setUserInfoUri(clientRegistrationDto.getUserInfoUri()); + clientRegistrationInfo.setUserNameAttributeName(clientRegistrationDto.getUserNameAttributeName()); + clientRegistrationInfo.setJwkSetUri(clientRegistrationDto.getJwkSetUri()); + clientRegistrationInfo.setClientAuthenticationMethod(clientRegistrationDto.getClientAuthenticationMethod()); + clientRegistrationInfo.setLoginButtonLabel(clientRegistrationDto.getLoginButtonLabel()); + clientRegistrationInfo.setLoginButtonIcon(clientRegistrationDto.getLoginButtonIcon()); + clientRegistrationInfo.setAdditionalInfo(clientRegistrationDto.getAdditionalInfo()); + return clientRegistrationInfo; + } + + public static OAuth2ClientRegistration toClientRegistration(OAuth2ClientRegistrationInfoId clientRegistrationInfoId, SchemeType domainScheme, String domainName) { + OAuth2ClientRegistration clientRegistration = new OAuth2ClientRegistration(); + clientRegistration.setClientRegistrationId(clientRegistrationInfoId); + clientRegistration.setDomainName(domainName); + clientRegistration.setDomainScheme(domainScheme); + return clientRegistration; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java deleted file mode 100644 index 9dd574b5e1..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java +++ /dev/null @@ -1,381 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.relation; - -import com.datastax.driver.core.BoundStatement; -import com.datastax.driver.core.PreparedStatement; -import com.datastax.driver.core.ResultSet; -import com.datastax.driver.core.ResultSetFuture; -import com.datastax.driver.core.Row; -import com.datastax.driver.core.querybuilder.QueryBuilder; -import com.datastax.driver.core.querybuilder.Select; -import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.EntityIdFactory; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TimePageLink; -import org.thingsboard.server.common.data.relation.EntityRelation; -import org.thingsboard.server.common.data.relation.RelationTypeGroup; -import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.type.RelationTypeGroupCodec; -import org.thingsboard.server.dao.nosql.CassandraAbstractAsyncDao; -import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTimeDao; -import org.thingsboard.server.dao.util.NoSqlDao; - -import javax.annotation.PostConstruct; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; - -/** - * Created by ashvayka on 25.04.17. - */ -@Component -@Slf4j -@NoSqlDao -public class BaseRelationDao extends CassandraAbstractAsyncDao implements RelationDao { - - private static final String SELECT_COLUMNS = "SELECT " + - ModelConstants.RELATION_FROM_ID_PROPERTY + "," + - ModelConstants.RELATION_FROM_TYPE_PROPERTY + "," + - ModelConstants.RELATION_TO_ID_PROPERTY + "," + - ModelConstants.RELATION_TO_TYPE_PROPERTY + "," + - ModelConstants.RELATION_TYPE_GROUP_PROPERTY + "," + - ModelConstants.RELATION_TYPE_PROPERTY + "," + - ModelConstants.ADDITIONAL_INFO_PROPERTY; - public static final String FROM = " FROM "; - public static final String WHERE = " WHERE "; - public static final String AND = " AND "; - - private static final RelationTypeGroupCodec relationTypeGroupCodec = new RelationTypeGroupCodec(); - public static final String EQUAL_TO_PARAM = " = ? "; - - private PreparedStatement saveStmt; - private PreparedStatement findAllByFromStmt; - private PreparedStatement findAllByFromAndTypeStmt; - private PreparedStatement findAllByToStmt; - private PreparedStatement findAllByToAndTypeStmt; - private PreparedStatement checkRelationStmt; - private PreparedStatement deleteStmt; - private PreparedStatement deleteAllByEntityStmt; - - @PostConstruct - public void init() { - super.startExecutor(); - } - - @Override - public ListenableFuture> findAllByFrom(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup) { - BoundStatement stmt = getFindAllByFromStmt().bind() - .setUUID(0, from.getId()) - .setString(1, from.getEntityType().name()) - .set(2, typeGroup, relationTypeGroupCodec); - return executeAsyncRead(tenantId, from, stmt); - } - - @Override - public ListenableFuture> findAllByFromAndType(TenantId tenantId, EntityId from, String relationType, RelationTypeGroup typeGroup) { - BoundStatement stmt = getFindAllByFromAndTypeStmt().bind() - .setUUID(0, from.getId()) - .setString(1, from.getEntityType().name()) - .set(2, typeGroup, relationTypeGroupCodec) - .setString(3, relationType); - return executeAsyncRead(tenantId, from, stmt); - } - - @Override - public ListenableFuture> findAllByTo(TenantId tenantId, EntityId to, RelationTypeGroup typeGroup) { - BoundStatement stmt = getFindAllByToStmt().bind() - .setUUID(0, to.getId()) - .setString(1, to.getEntityType().name()) - .set(2, typeGroup, relationTypeGroupCodec); - return executeAsyncRead(tenantId, to, stmt); - } - - @Override - public ListenableFuture> findAllByToAndType(TenantId tenantId, EntityId to, String relationType, RelationTypeGroup typeGroup) { - BoundStatement stmt = getFindAllByToAndTypeStmt().bind() - .setUUID(0, to.getId()) - .setString(1, to.getEntityType().name()) - .set(2, typeGroup, relationTypeGroupCodec) - .setString(3, relationType); - return executeAsyncRead(tenantId, to, stmt); - } - - @Override - public ListenableFuture checkRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { - BoundStatement stmt = getCheckRelationStmt().bind() - .setUUID(0, from.getId()) - .setString(1, from.getEntityType().name()) - .setUUID(2, to.getId()) - .setString(3, to.getEntityType().name()) - .set(4, typeGroup, relationTypeGroupCodec) - .setString(5, relationType); - return getFuture(executeAsyncRead(tenantId, stmt), rs -> rs != null ? rs.one() != null : false); - } - - @Override - public ListenableFuture getRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { - BoundStatement stmt = getCheckRelationStmt().bind() - .setUUID(0, from.getId()) - .setString(1, from.getEntityType().name()) - .setUUID(2, to.getId()) - .setString(3, to.getEntityType().name()) - .set(4, typeGroup, relationTypeGroupCodec) - .setString(5, relationType); - return getFuture(executeAsyncRead(tenantId, stmt), rs -> rs != null ? getEntityRelation(rs.one()) : null); - } - - @Override - public boolean saveRelation(TenantId tenantId, EntityRelation relation) { - BoundStatement stmt = getSaveRelationStatement(tenantId, relation); - ResultSet rs = executeWrite(tenantId, stmt); - return rs.wasApplied(); - } - - @Override - public ListenableFuture saveRelationAsync(TenantId tenantId, EntityRelation relation) { - BoundStatement stmt = getSaveRelationStatement(tenantId, relation); - ResultSetFuture future = executeAsyncWrite(tenantId, stmt); - return getBooleanListenableFuture(future); - } - - private BoundStatement getSaveRelationStatement(TenantId tenantId, EntityRelation relation) { - BoundStatement stmt = getSaveStmt().bind() - .setUUID(0, relation.getFrom().getId()) - .setString(1, relation.getFrom().getEntityType().name()) - .setUUID(2, relation.getTo().getId()) - .setString(3, relation.getTo().getEntityType().name()) - .set(4, relation.getTypeGroup(), relationTypeGroupCodec) - .setString(5, relation.getType()) - .set(6, relation.getAdditionalInfo(), JsonNode.class); - return stmt; - } - - @Override - public boolean deleteRelation(TenantId tenantId, EntityRelation relation) { - return deleteRelation(tenantId, relation.getFrom(), relation.getTo(), relation.getType(), relation.getTypeGroup()); - } - - @Override - public ListenableFuture deleteRelationAsync(TenantId tenantId, EntityRelation relation) { - return deleteRelationAsync(tenantId, relation.getFrom(), relation.getTo(), relation.getType(), relation.getTypeGroup()); - } - - @Override - public boolean deleteRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { - BoundStatement stmt = getDeleteRelationStatement(tenantId, from, to, relationType, typeGroup); - ResultSet rs = executeWrite(tenantId, stmt); - return rs.wasApplied(); - } - - @Override - public ListenableFuture deleteRelationAsync(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { - BoundStatement stmt = getDeleteRelationStatement(tenantId, from, to, relationType, typeGroup); - ResultSetFuture future = executeAsyncWrite(tenantId, stmt); - return getBooleanListenableFuture(future); - } - - private BoundStatement getDeleteRelationStatement(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { - BoundStatement stmt = getDeleteStmt().bind() - .setUUID(0, from.getId()) - .setString(1, from.getEntityType().name()) - .setUUID(2, to.getId()) - .setString(3, to.getEntityType().name()) - .set(4, typeGroup, relationTypeGroupCodec) - .setString(5, relationType); - return stmt; - } - - @Override - public boolean deleteOutboundRelations(TenantId tenantId, EntityId entity) { - BoundStatement stmt = getDeleteAllByEntityStmt().bind() - .setUUID(0, entity.getId()) - .setString(1, entity.getEntityType().name()); - ResultSet rs = executeWrite(tenantId, stmt); - return rs.wasApplied(); - } - - - @Override - public ListenableFuture deleteOutboundRelationsAsync(TenantId tenantId, EntityId entity) { - BoundStatement stmt = getDeleteAllByEntityStmt().bind() - .setUUID(0, entity.getId()) - .setString(1, entity.getEntityType().name()); - ResultSetFuture future = executeAsyncWrite(tenantId, stmt); - return getBooleanListenableFuture(future); - } - - @Override - public ListenableFuture> findRelations(TenantId tenantId, EntityId from, String relationType, RelationTypeGroup typeGroup, EntityType childType, TimePageLink pageLink) { - Select.Where query = CassandraAbstractSearchTimeDao.buildQuery(ModelConstants.RELATION_BY_TYPE_AND_CHILD_TYPE_VIEW_NAME, - Arrays.asList(eq(ModelConstants.RELATION_FROM_ID_PROPERTY, from.getId()), - eq(ModelConstants.RELATION_FROM_TYPE_PROPERTY, from.getEntityType().name()), - eq(ModelConstants.RELATION_TYPE_GROUP_PROPERTY, typeGroup.name()), - eq(ModelConstants.RELATION_TYPE_PROPERTY, relationType), - eq(ModelConstants.RELATION_TO_TYPE_PROPERTY, childType.name())), - Arrays.asList( - pageLink.isAscOrder() ? QueryBuilder.desc(ModelConstants.RELATION_TYPE_GROUP_PROPERTY) : - QueryBuilder.asc(ModelConstants.RELATION_TYPE_GROUP_PROPERTY), - pageLink.isAscOrder() ? QueryBuilder.desc(ModelConstants.RELATION_TYPE_PROPERTY) : - QueryBuilder.asc(ModelConstants.RELATION_TYPE_PROPERTY), - pageLink.isAscOrder() ? QueryBuilder.desc(ModelConstants.RELATION_TO_TYPE_PROPERTY) : - QueryBuilder.asc(ModelConstants.RELATION_TO_TYPE_PROPERTY) - ), - pageLink, ModelConstants.RELATION_TO_ID_PROPERTY); - return getFuture(executeAsyncRead(tenantId, query), this::getEntityRelations); - } - - private PreparedStatement getSaveStmt() { - if (saveStmt == null) { - saveStmt = prepare("INSERT INTO " + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " + - "(" + ModelConstants.RELATION_FROM_ID_PROPERTY + - "," + ModelConstants.RELATION_FROM_TYPE_PROPERTY + - "," + ModelConstants.RELATION_TO_ID_PROPERTY + - "," + ModelConstants.RELATION_TO_TYPE_PROPERTY + - "," + ModelConstants.RELATION_TYPE_GROUP_PROPERTY + - "," + ModelConstants.RELATION_TYPE_PROPERTY + - "," + ModelConstants.ADDITIONAL_INFO_PROPERTY + ")" + - " VALUES(?, ?, ?, ?, ?, ?, ?)"); - } - return saveStmt; - } - - private PreparedStatement getDeleteStmt() { - if (deleteStmt == null) { - deleteStmt = prepare("DELETE FROM " + ModelConstants.RELATION_COLUMN_FAMILY_NAME + - WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + " = ?" + - AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + " = ?" + - AND + ModelConstants.RELATION_TO_ID_PROPERTY + " = ?" + - AND + ModelConstants.RELATION_TO_TYPE_PROPERTY + " = ?" + - AND + ModelConstants.RELATION_TYPE_GROUP_PROPERTY + " = ?" + - AND + ModelConstants.RELATION_TYPE_PROPERTY + " = ?"); - } - return deleteStmt; - } - - private PreparedStatement getDeleteAllByEntityStmt() { - if (deleteAllByEntityStmt == null) { - deleteAllByEntityStmt = prepare("DELETE FROM " + ModelConstants.RELATION_COLUMN_FAMILY_NAME + - WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + " = ?" + - AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + " = ?"); - } - return deleteAllByEntityStmt; - } - - private PreparedStatement getFindAllByFromStmt() { - if (findAllByFromStmt == null) { - findAllByFromStmt = prepare(SELECT_COLUMNS + " " + - FROM + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " + - WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + EQUAL_TO_PARAM + - AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + EQUAL_TO_PARAM + - AND + ModelConstants.RELATION_TYPE_GROUP_PROPERTY + EQUAL_TO_PARAM); - } - return findAllByFromStmt; - } - - private PreparedStatement getFindAllByFromAndTypeStmt() { - if (findAllByFromAndTypeStmt == null) { - findAllByFromAndTypeStmt = prepare(SELECT_COLUMNS + " " + - FROM + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " + - WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + EQUAL_TO_PARAM + - AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + EQUAL_TO_PARAM + - AND + ModelConstants.RELATION_TYPE_GROUP_PROPERTY + EQUAL_TO_PARAM + - AND + ModelConstants.RELATION_TYPE_PROPERTY + EQUAL_TO_PARAM); - } - return findAllByFromAndTypeStmt; - } - - - private PreparedStatement getFindAllByToStmt() { - if (findAllByToStmt == null) { - findAllByToStmt = prepare(SELECT_COLUMNS + " " + - FROM + ModelConstants.RELATION_REVERSE_VIEW_NAME + " " + - WHERE + ModelConstants.RELATION_TO_ID_PROPERTY + EQUAL_TO_PARAM + - AND + ModelConstants.RELATION_TO_TYPE_PROPERTY + EQUAL_TO_PARAM + - AND + ModelConstants.RELATION_TYPE_GROUP_PROPERTY + EQUAL_TO_PARAM); - } - return findAllByToStmt; - } - - private PreparedStatement getFindAllByToAndTypeStmt() { - if (findAllByToAndTypeStmt == null) { - findAllByToAndTypeStmt = prepare(SELECT_COLUMNS + " " + - FROM + ModelConstants.RELATION_REVERSE_VIEW_NAME + " " + - WHERE + ModelConstants.RELATION_TO_ID_PROPERTY + EQUAL_TO_PARAM + - AND + ModelConstants.RELATION_TO_TYPE_PROPERTY + EQUAL_TO_PARAM + - AND + ModelConstants.RELATION_TYPE_GROUP_PROPERTY + EQUAL_TO_PARAM + - AND + ModelConstants.RELATION_TYPE_PROPERTY + EQUAL_TO_PARAM); - } - return findAllByToAndTypeStmt; - } - - - private PreparedStatement getCheckRelationStmt() { - if (checkRelationStmt == null) { - checkRelationStmt = prepare(SELECT_COLUMNS + " " + - FROM + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " + - WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + EQUAL_TO_PARAM + - AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + EQUAL_TO_PARAM + - AND + ModelConstants.RELATION_TO_ID_PROPERTY + EQUAL_TO_PARAM + - AND + ModelConstants.RELATION_TO_TYPE_PROPERTY + EQUAL_TO_PARAM + - AND + ModelConstants.RELATION_TYPE_GROUP_PROPERTY + EQUAL_TO_PARAM + - AND + ModelConstants.RELATION_TYPE_PROPERTY + EQUAL_TO_PARAM); - } - return checkRelationStmt; - } - - private EntityId toEntity(Row row, String uuidColumn, String typeColumn) { - return EntityIdFactory.getByTypeAndUuid(row.getString(typeColumn), row.getUUID(uuidColumn)); - } - - private ListenableFuture> executeAsyncRead(TenantId tenantId, EntityId from, BoundStatement stmt) { - log.debug("Generated query [{}] for entity {}", stmt, from); - return getFuture(executeAsyncRead(tenantId, stmt), rs -> getEntityRelations(rs)); - } - - private ListenableFuture getBooleanListenableFuture(ResultSetFuture rsFuture) { - return getFuture(rsFuture, rs -> rs != null ? rs.wasApplied() : false); - } - - private List getEntityRelations(ResultSet rs) { - List rows = rs.all(); - List entries = new ArrayList<>(rows.size()); - if (!rows.isEmpty()) { - rows.forEach(row -> { - entries.add(getEntityRelation(row)); - }); - } - return entries; - } - - private EntityRelation getEntityRelation(Row row) { - EntityRelation relation = new EntityRelation(); - relation.setTypeGroup(row.get(ModelConstants.RELATION_TYPE_GROUP_PROPERTY, relationTypeGroupCodec)); - relation.setType(row.getString(ModelConstants.RELATION_TYPE_PROPERTY)); - relation.setAdditionalInfo(row.get(ModelConstants.ADDITIONAL_INFO_PROPERTY, JsonNode.class)); - relation.setFrom(toEntity(row, ModelConstants.RELATION_FROM_ID_PROPERTY, ModelConstants.RELATION_FROM_TYPE_PROPERTY)); - relation.setTo(toEntity(row, ModelConstants.RELATION_TO_ID_PROPERTY, ModelConstants.RELATION_TO_TYPE_PROPERTY)); - return relation; - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java index 28918c63dd..e67590d677 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java @@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; @@ -58,6 +59,4 @@ public interface RelationDao { ListenableFuture deleteOutboundRelationsAsync(TenantId tenantId, EntityId entity); - ListenableFuture> findRelations(TenantId tenantId, EntityId from, String relationType, RelationTypeGroup typeGroup, EntityType toType, TimePageLink pageLink); - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index a99ad20ea6..5997839119 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -15,10 +15,14 @@ */ package org.thingsboard.server.dao.rule; +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.BaseData; @@ -28,13 +32,16 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.NodeConnectionInfo; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo; +import org.thingsboard.server.common.data.rule.RuleChainData; +import org.thingsboard.server.common.data.rule.RuleChainImportResult; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.dao.entity.AbstractEntityService; @@ -47,10 +54,15 @@ import org.thingsboard.server.dao.tenant.TenantDao; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +import static org.thingsboard.server.common.data.DataConstants.TENANT; /** * Created by igor on 3/12/18. @@ -59,6 +71,7 @@ import java.util.concurrent.ExecutionException; @Slf4j public class BaseRuleChainService extends AbstractEntityService implements RuleChainService { + private static final int DEFAULT_PAGE_SIZE = 1000; @Autowired private RuleChainDao ruleChainDao; @@ -367,11 +380,10 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } @Override - public TextPageData findTenantRuleChains(TenantId tenantId, TextPageLink pageLink) { + public PageData findTenantRuleChains(TenantId tenantId, PageLink pageLink) { Validator.validateId(tenantId, "Incorrect tenant id for search rule chain request."); - Validator.validatePageLink(pageLink, "Incorrect PageLink object for search rule chain request."); - List ruleChains = ruleChainDao.findRuleChainsByTenantId(tenantId.getId(), pageLink); - return new TextPageData<>(ruleChains, pageLink); + Validator.validatePageLink(pageLink); + return ruleChainDao.findRuleChainsByTenantId(tenantId.getId(), pageLink); } @Override @@ -390,13 +402,157 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC tenantRuleChainsRemover.removeEntities(tenantId, tenantId); } + @Override + public RuleChainData exportTenantRuleChains(TenantId tenantId, PageLink pageLink) { + Validator.validateId(tenantId, "Incorrect tenant id for search rule chain request."); + Validator.validatePageLink(pageLink); + PageData ruleChainData = ruleChainDao.findRuleChainsByTenantId(tenantId.getId(), pageLink); + List ruleChains = ruleChainData.getData(); + List metadata = ruleChains.stream().map(rc -> loadRuleChainMetaData(tenantId, rc.getId())).collect(Collectors.toList()); + RuleChainData rcData = new RuleChainData(); + rcData.setRuleChains(ruleChains); + rcData.setMetadata(metadata); + setRandomRuleChainIds(rcData); + resetRuleNodeIds(metadata); + return rcData; + } + + @Override + public List importTenantRuleChains(TenantId tenantId, RuleChainData ruleChainData, boolean overwrite) { + List importResults = new ArrayList<>(); + setRandomRuleChainIds(ruleChainData); + resetRuleNodeIds(ruleChainData.getMetadata()); + resetRuleChainMetadataTenantIds(tenantId, ruleChainData.getMetadata()); + if (overwrite) { + List persistentRuleChains = findAllTenantRuleChains(tenantId); + for (RuleChain ruleChain : ruleChainData.getRuleChains()) { + ComponentLifecycleEvent lifecycleEvent; + Optional persistentRuleChainOpt = persistentRuleChains.stream().filter(rc -> rc.getName().equals(ruleChain.getName())).findFirst(); + if (persistentRuleChainOpt.isPresent()) { + setNewRuleChainId(ruleChain, ruleChainData.getMetadata(), ruleChain.getId(), persistentRuleChainOpt.get().getId()); + ruleChain.setRoot(persistentRuleChainOpt.get().isRoot()); + lifecycleEvent = ComponentLifecycleEvent.UPDATED; + } else { + ruleChain.setRoot(false); + lifecycleEvent = ComponentLifecycleEvent.CREATED; + } + ruleChain.setTenantId(tenantId); + ruleChainDao.save(tenantId, ruleChain); + importResults.add(new RuleChainImportResult(tenantId, ruleChain.getId(), lifecycleEvent)); + } + } else { + if (!CollectionUtils.isEmpty(ruleChainData.getRuleChains())) { + ruleChainData.getRuleChains().forEach(rc -> { + rc.setTenantId(tenantId); + rc.setRoot(false); + RuleChain savedRc = ruleChainDao.save(tenantId, rc); + importResults.add(new RuleChainImportResult(tenantId, savedRc.getId(), ComponentLifecycleEvent.CREATED)); + }); + } + } + if (!CollectionUtils.isEmpty(ruleChainData.getMetadata())) { + ruleChainData.getMetadata().forEach(md -> saveRuleChainMetaData(tenantId, md)); + } + return importResults; + } + + private void resetRuleChainMetadataTenantIds(TenantId tenantId, List metaData) { + for (RuleChainMetaData md : metaData) { + for (RuleNode node : md.getNodes()) { + JsonNode nodeConfiguration = node.getConfiguration(); + searchTenantIdRecursive(tenantId, nodeConfiguration); + } + } + } + + private void searchTenantIdRecursive(TenantId tenantId, JsonNode node) { + Iterator iter = node.fieldNames(); + boolean isTenantId = false; + while (iter.hasNext()) { + String field = iter.next(); + if ("entityType".equals(field) && TENANT.equals(node.get(field).asText())) { + isTenantId = true; + break; + } + } + if (isTenantId) { + ObjectNode objNode = (ObjectNode) node; + objNode.put("id", tenantId.getId().toString()); + } else { + Iterator childIter = node.iterator(); + while (childIter.hasNext()) { + searchTenantIdRecursive(tenantId, childIter.next()); + } + } + } + + private void setRandomRuleChainIds(RuleChainData ruleChainData) { + for (RuleChain ruleChain : ruleChainData.getRuleChains()) { + RuleChainId oldRuleChainId = ruleChain.getId(); + RuleChainId newRuleChainId = new RuleChainId(Uuids.timeBased()); + setNewRuleChainId(ruleChain, ruleChainData.getMetadata(), oldRuleChainId, newRuleChainId); + ruleChain.setTenantId(null); + } + } + + private void resetRuleNodeIds(List metaData) { + for (RuleChainMetaData md : metaData) { + for (RuleNode node : md.getNodes()) { + node.setId(null); + node.setRuleChainId(null); + } + } + } + + private List findAllTenantRuleChains(TenantId tenantId) { + PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE); + return findAllTenantRuleChainsRecursive(tenantId, new ArrayList<>(), pageLink); + } + + private List findAllTenantRuleChainsRecursive(TenantId tenantId, List accumulator, PageLink pageLink) { + PageData persistentRuleChainData = findTenantRuleChains(tenantId, pageLink); + List ruleChains = persistentRuleChainData.getData(); + if (!CollectionUtils.isEmpty(ruleChains)) { + accumulator.addAll(ruleChains); + } + if (persistentRuleChainData.hasNext()) { + return findAllTenantRuleChainsRecursive(tenantId, accumulator, pageLink.nextPageLink()); + } + return accumulator; + } + + private void setNewRuleChainId(RuleChain ruleChain, List metadata, RuleChainId oldRuleChainId, RuleChainId newRuleChainId) { + ruleChain.setId(newRuleChainId); + for (RuleChainMetaData metaData : metadata) { + if (metaData.getRuleChainId().equals(oldRuleChainId)) { + metaData.setRuleChainId(newRuleChainId); + } + if (!CollectionUtils.isEmpty(metaData.getRuleChainConnections())) { + for (RuleChainConnectionInfo rcConnInfo : metaData.getRuleChainConnections()) { + if (rcConnInfo.getTargetRuleChainId().equals(oldRuleChainId)) { + rcConnInfo.setTargetRuleChainId(newRuleChainId); + } + } + } + } + } + private void checkRuleNodesAndDelete(TenantId tenantId, RuleChainId ruleChainId) { + try{ + ruleChainDao.removeById(tenantId, ruleChainId.getId()); + } catch (Exception t) { + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_default_rule_chain_device_profile")) { + throw new DataValidationException("The rule chain referenced by the device profiles cannot be deleted!"); + } else { + throw t; + } + } List nodeRelations = getRuleChainToNodeRelations(tenantId, ruleChainId); for (EntityRelation relation : nodeRelations) { deleteRuleNode(tenantId, relation.getTo()); } deleteEntityRelations(tenantId, ruleChainId); - ruleChainDao.removeById(tenantId, ruleChainId.getId()); } private List getRuleChainToNodeRelations(TenantId tenantId, RuleChainId ruleChainId) { @@ -449,7 +605,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC new PaginatedRemover() { @Override - protected List findEntities(TenantId tenantId, TenantId id, TextPageLink pageLink) { + protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { return ruleChainDao.findRuleChainsByTenantId(id.getId(), pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleNodeStateService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleNodeStateService.java new file mode 100644 index 0000000000..7bc77e14a3 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleNodeStateService.java @@ -0,0 +1,119 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.rule; + +import lombok.extern.slf4j.Slf4j; +import org.hibernate.exception.ConstraintViolationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.rule.RuleNodeState; +import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.exception.DataValidationException; + +@Service +@Slf4j +public class BaseRuleNodeStateService extends AbstractEntityService implements RuleNodeStateService { + + @Autowired + private RuleNodeStateDao ruleNodeStateDao; + + @Override + public PageData findByRuleNodeId(TenantId tenantId, RuleNodeId ruleNodeId, PageLink pageLink) { + if (tenantId == null) { + throw new DataValidationException("Tenant id should be specified!."); + } + if (ruleNodeId == null) { + throw new DataValidationException("RuleNode id should be specified!."); + } + return ruleNodeStateDao.findByRuleNodeId(ruleNodeId.getId(), pageLink); + } + + @Override + public RuleNodeState findByRuleNodeIdAndEntityId(TenantId tenantId, RuleNodeId ruleNodeId, EntityId entityId) { + if (tenantId == null) { + throw new DataValidationException("Tenant id should be specified!."); + } + if (ruleNodeId == null) { + throw new DataValidationException("RuleNode id should be specified!."); + } + if (entityId == null) { + throw new DataValidationException("Entity id should be specified!."); + } + return ruleNodeStateDao.findByRuleNodeIdAndEntityId(ruleNodeId.getId(), entityId.getId()); + } + + @Override + public RuleNodeState save(TenantId tenantId, RuleNodeState ruleNodeState) { + if (tenantId == null) { + throw new DataValidationException("Tenant id should be specified!."); + } + return saveOrUpdate(tenantId, ruleNodeState, false); + } + + @Override + public void removeByRuleNodeId(TenantId tenantId, RuleNodeId ruleNodeId) { + if (tenantId == null) { + throw new DataValidationException("Tenant id should be specified!."); + } + if (ruleNodeId == null) { + throw new DataValidationException("Rule node id should be specified!."); + } + ruleNodeStateDao.removeByRuleNodeId(ruleNodeId.getId()); + } + + @Override + public void removeByRuleNodeIdAndEntityId(TenantId tenantId, RuleNodeId ruleNodeId, EntityId entityId) { + if (tenantId == null) { + throw new DataValidationException("Tenant id should be specified!."); + } + if (ruleNodeId == null) { + throw new DataValidationException("Rule node id should be specified!."); + } + if (entityId == null) { + throw new DataValidationException("Entity id should be specified!."); + } + ruleNodeStateDao.removeByRuleNodeIdAndEntityId(ruleNodeId.getId(), entityId.getId()); + } + + public RuleNodeState saveOrUpdate(TenantId tenantId, RuleNodeState ruleNodeState, boolean update) { + try { + if (update) { + RuleNodeState old = ruleNodeStateDao.findByRuleNodeIdAndEntityId(ruleNodeState.getRuleNodeId().getId(), ruleNodeState.getEntityId().getId()); + if (old != null && !old.getId().equals(ruleNodeState.getId())) { + ruleNodeState.setId(old.getId()); + ruleNodeState.setCreatedTime(old.getCreatedTime()); + } + } + return ruleNodeStateDao.save(tenantId, ruleNodeState); + } catch (Exception t) { + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("rule_node_state_unq_key")) { + if (!update) { + return saveOrUpdate(tenantId, ruleNodeState, true); + } else { + throw new DataValidationException("Rule node state for such rule node id and entity id already exists!"); + } + } else { + throw t; + } + } + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/CassandraRuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/rule/CassandraRuleChainDao.java deleted file mode 100644 index 811bb68e66..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/CassandraRuleChainDao.java +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.rule; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; -import org.thingsboard.server.common.data.rule.RuleChain; -import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.model.nosql.RuleChainEntity; -import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTextDao; -import org.thingsboard.server.dao.util.NoSqlDao; - -import java.util.Collections; -import java.util.List; -import java.util.UUID; - -import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; -import static org.thingsboard.server.dao.model.ModelConstants.RULE_CHAIN_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.RULE_CHAIN_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.RULE_CHAIN_TENANT_ID_PROPERTY; - -@Component -@Slf4j -@NoSqlDao -public class CassandraRuleChainDao extends CassandraAbstractSearchTextDao implements RuleChainDao { - - @Override - protected Class getColumnFamilyClass() { - return RuleChainEntity.class; - } - - @Override - protected String getColumnFamilyName() { - return RULE_CHAIN_COLUMN_FAMILY_NAME; - } - - @Override - public List findRuleChainsByTenantId(UUID tenantId, TextPageLink pageLink) { - log.debug("Try to find rule chains by tenantId [{}] and pageLink [{}]", tenantId, pageLink); - List ruleChainEntities = findPageWithTextSearch(new TenantId(tenantId), RULE_CHAIN_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME, - Collections.singletonList(eq(RULE_CHAIN_TENANT_ID_PROPERTY, tenantId)), - pageLink); - - log.trace("Found rule chains [{}] by tenantId [{}] and pageLink [{}]", ruleChainEntities, tenantId, pageLink); - return DaoUtil.convertDataList(ruleChainEntities); - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/CassandraRuleNodeDao.java b/dao/src/main/java/org/thingsboard/server/dao/rule/CassandraRuleNodeDao.java deleted file mode 100644 index 53212e1096..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/CassandraRuleNodeDao.java +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.rule; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.rule.RuleNode; -import org.thingsboard.server.dao.model.nosql.RuleNodeEntity; -import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTextDao; -import org.thingsboard.server.dao.util.NoSqlDao; - -import static org.thingsboard.server.dao.model.ModelConstants.RULE_NODE_COLUMN_FAMILY_NAME; - -@Component -@Slf4j -@NoSqlDao -public class CassandraRuleNodeDao extends CassandraAbstractSearchTextDao implements RuleNodeDao { - - @Override - protected Class getColumnFamilyClass() { - return RuleNodeEntity.class; - } - - @Override - protected String getColumnFamilyName() { - return RULE_NODE_COLUMN_FAMILY_NAME; - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java index 831df1cd15..c3214425fe 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java @@ -15,11 +15,11 @@ */ package org.thingsboard.server.dao.rule; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.dao.Dao; -import java.util.List; import java.util.UUID; /** @@ -34,6 +34,6 @@ public interface RuleChainDao extends Dao { * @param pageLink the page link * @return the list of rule chain objects */ - List findRuleChainsByTenantId(UUID tenantId, TextPageLink pageLink); + PageData findRuleChainsByTenantId(UUID tenantId, PageLink pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleNodeStateDao.java b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleNodeStateDao.java new file mode 100644 index 0000000000..9f304dd828 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleNodeStateDao.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.rule; + +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.rule.RuleNodeState; +import org.thingsboard.server.dao.Dao; + +import java.util.UUID; + +/** + * Created by igor on 3/12/18. + */ +public interface RuleNodeStateDao extends Dao { + + PageData findByRuleNodeId(UUID ruleNodeId, PageLink pageLink); + + RuleNodeState findByRuleNodeIdAndEntityId(UUID ruleNodeId, UUID entityId); + + void removeByRuleNodeId(UUID ruleNodeId); + + void removeByRuleNodeIdAndEntityId(UUID ruleNodeId, UUID entityId); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/PaginatedRemover.java b/dao/src/main/java/org/thingsboard/server/dao/service/PaginatedRemover.java index c6157516e1..1d4eedf1f6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/PaginatedRemover.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/PaginatedRemover.java @@ -17,33 +17,26 @@ package org.thingsboard.server.dao.service; import org.thingsboard.server.common.data.id.IdBased; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; - -import java.util.List; -import java.util.UUID; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; public abstract class PaginatedRemover> { private static final int DEFAULT_LIMIT = 100; public void removeEntities(TenantId tenantId, I id) { - TextPageLink pageLink = new TextPageLink(DEFAULT_LIMIT); + PageLink pageLink = new PageLink(DEFAULT_LIMIT); boolean hasNext = true; while (hasNext) { - List entities = findEntities(tenantId, id, pageLink); - for (D entity : entities) { + PageData entities = findEntities(tenantId, id, pageLink); + for (D entity : entities.getData()) { removeEntity(tenantId, entity); } - hasNext = entities.size() == pageLink.getLimit(); - if (hasNext) { - int index = entities.size() - 1; - UUID idOffset = entities.get(index).getUuidId(); - pageLink.setIdOffset(idOffset); - } + hasNext = entities.hasNext(); } } - protected abstract List findEntities(TenantId tenantId,I id, TextPageLink pageLink); + protected abstract PageData findEntities(TenantId tenantId, I id, PageLink pageLink); protected abstract void removeEntity(TenantId tenantId, D entity); diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/TimePaginatedRemover.java b/dao/src/main/java/org/thingsboard/server/dao/service/TimePaginatedRemover.java index 355103ac97..43716fda25 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/TimePaginatedRemover.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/TimePaginatedRemover.java @@ -17,11 +17,9 @@ package org.thingsboard.server.dao.service; import org.thingsboard.server.common.data.id.IdBased; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; -import java.util.List; -import java.util.UUID; - public abstract class TimePaginatedRemover> { private static final int DEFAULT_LIMIT = 100; @@ -30,20 +28,15 @@ public abstract class TimePaginatedRemover> { TimePageLink pageLink = new TimePageLink(DEFAULT_LIMIT); boolean hasNext = true; while (hasNext) { - List entities = findEntities(tenantId, id, pageLink); - for (D entity : entities) { + PageData entities = findEntities(tenantId, id, pageLink); + for (D entity : entities.getData()) { removeEntity(tenantId, entity); } - hasNext = entities.size() == pageLink.getLimit(); - if (hasNext) { - int index = entities.size() - 1; - UUID idOffset = entities.get(index).getUuidId(); - pageLink.setIdOffset(idOffset); - } + hasNext = entities.hasNext(); } } - protected abstract List findEntities(TenantId tenantId, I id, TimePageLink pageLink); + protected abstract PageData findEntities(TenantId tenantId, I id, TimePageLink pageLink); protected abstract void removeEntity(TenantId tenantId, D entity); diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java b/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java index 20bacc659b..de200b425a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java @@ -17,7 +17,7 @@ package org.thingsboard.server.dao.service; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.UUIDBased; -import org.thingsboard.server.common.data.page.BasePageLink; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.exception.IncorrectParameterException; import java.util.List; @@ -114,11 +114,14 @@ public class Validator { * IncorrectParameterException exception * * @param pageLink the page link - * @param errorMessage the error message for exception */ - public static void validatePageLink(BasePageLink pageLink, String errorMessage) { - if (pageLink == null || pageLink.getLimit() < 1 || (pageLink.getIdOffset() != null && pageLink.getIdOffset().version() != 1)) { - throw new IncorrectParameterException(errorMessage); + public static void validatePageLink(PageLink pageLink) { + if (pageLink == null) { + throw new IncorrectParameterException("Page link must be specified."); + } else if (pageLink.getPageSize() < 1) { + throw new IncorrectParameterException("Incorrect page link page size '"+pageLink.getPageSize()+"'. Page size must be greater than zero."); + } else if (pageLink.getPage() < 0) { + throw new IncorrectParameterException("Incorrect page link page '"+pageLink.getPage()+"'. Page must be positive integer."); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java index 4374e314cf..ec57567a1a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java @@ -21,7 +21,6 @@ import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.AdminSettings; -import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.AdminSettingsId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.exception.DataValidationException; diff --git a/dao/src/main/java/org/thingsboard/server/dao/settings/CassandraAdminSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/settings/CassandraAdminSettingsDao.java deleted file mode 100644 index 2cc218a678..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/settings/CassandraAdminSettingsDao.java +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.settings; - -import com.datastax.driver.core.querybuilder.Select.Where; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.AdminSettings; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.model.nosql.AdminSettingsEntity; -import org.thingsboard.server.dao.nosql.CassandraAbstractModelDao; -import org.thingsboard.server.dao.util.NoSqlDao; - -import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; -import static com.datastax.driver.core.querybuilder.QueryBuilder.select; -import static org.thingsboard.server.dao.model.ModelConstants.ADMIN_SETTINGS_BY_KEY_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.ADMIN_SETTINGS_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.ADMIN_SETTINGS_KEY_PROPERTY; - -@Component -@Slf4j -@NoSqlDao -public class CassandraAdminSettingsDao extends CassandraAbstractModelDao implements AdminSettingsDao { - - @Override - protected Class getColumnFamilyClass() { - return AdminSettingsEntity.class; - } - - @Override - protected String getColumnFamilyName() { - return ADMIN_SETTINGS_COLUMN_FAMILY_NAME; - } - - @Override - public AdminSettings findByKey(TenantId tenantId, String key) { - log.debug("Try to find admin settings by key [{}] ", key); - Where query = select().from(ADMIN_SETTINGS_BY_KEY_COLUMN_FAMILY_NAME).where(eq(ADMIN_SETTINGS_KEY_PROPERTY, key)); - log.trace("Execute query {}", query); - AdminSettingsEntity adminSettingsEntity = findOneByStatement(tenantId, query); - log.trace("Found admin settings [{}] by key [{}]", adminSettingsEntity, key); - return DaoUtil.getData(adminSettingsEntity); - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java index edc9c97a9a..a7608f9861 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.dao.sql; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import com.google.common.collect.Lists; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; @@ -30,8 +30,6 @@ import java.util.List; import java.util.Optional; import java.util.UUID; -import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID; - /** * @author Valerii Sosliuk */ @@ -42,9 +40,10 @@ public abstract class JpaAbstractDao, D> protected abstract Class getEntityClass(); - protected abstract CrudRepository getCrudRepository(); + protected abstract CrudRepository getCrudRepository(); - protected void setSearchText(E entity) {} + protected void setSearchText(E entity) { + } @Override @Transactional @@ -59,7 +58,9 @@ public abstract class JpaAbstractDao, D> setSearchText(entity); log.debug("Saving entity {}", entity); if (entity.getUuid() == null) { - entity.setUuid(UUIDs.timeBased()); + UUID uuid = Uuids.timeBased(); + entity.setUuid(uuid); + entity.setCreatedTime(Uuids.unixTimestamp(uuid)); } entity = getCrudRepository().save(entity); return DaoUtil.getData(entity); @@ -68,23 +69,22 @@ public abstract class JpaAbstractDao, D> @Override public D findById(TenantId tenantId, UUID key) { log.debug("Get entity by key {}", key); - Optional entity = getCrudRepository().findById(fromTimeUUID(key)); + Optional entity = getCrudRepository().findById(key); return DaoUtil.getData(entity); } @Override public ListenableFuture findByIdAsync(TenantId tenantId, UUID key) { log.debug("Get entity by key async {}", key); - return service.submit(() -> DaoUtil.getData(getCrudRepository().findById(fromTimeUUID(key)))); + return service.submit(() -> DaoUtil.getData(getCrudRepository().findById(key))); } @Override @Transactional public boolean removeById(TenantId tenantId, UUID id) { - String key = fromTimeUUID(id); - getCrudRepository().deleteById(key); - log.debug("Remove request: {}", key); - return !getCrudRepository().existsById(key); + getCrudRepository().deleteById(id); + log.debug("Remove request: {}", id); + return !getCrudRepository().existsById(id); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractSearchTimeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractSearchTimeDao.java deleted file mode 100644 index 9c6b2648a5..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractSearchTimeDao.java +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.sql; - -import com.datastax.driver.core.utils.UUIDs; -import org.springframework.data.jpa.domain.Specification; -import org.thingsboard.server.common.data.UUIDConverter; -import org.thingsboard.server.common.data.page.TimePageLink; -import org.thingsboard.server.dao.model.BaseEntity; - -import javax.persistence.criteria.CriteriaBuilder; -import javax.persistence.criteria.CriteriaQuery; -import javax.persistence.criteria.Predicate; -import javax.persistence.criteria.Root; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -/** - * Created by Valerii Sosliuk on 5/4/2017. - */ -public abstract class JpaAbstractSearchTimeDao, D> extends JpaAbstractDao { - - public static Specification getTimeSearchPageSpec(TimePageLink pageLink, String idColumn) { - return new Specification() { - @Override - public Predicate toPredicate(Root root, CriteriaQuery criteriaQuery, CriteriaBuilder criteriaBuilder) { - List predicates; - if (pageLink.isAscOrder()) { - predicates = createAscPredicates(pageLink, idColumn, root, criteriaBuilder); - } else { - predicates = createDescPredicates(pageLink, idColumn, root, criteriaBuilder); - } - return criteriaBuilder.and(predicates.toArray(new Predicate[0])); - } - }; - } - - private static List createAscPredicates(TimePageLink pageLink, String idColumn, Root root, CriteriaBuilder criteriaBuilder) { - List predicates = new ArrayList<>(); - if (pageLink.getIdOffset() != null) { - Predicate lowerBound = criteriaBuilder.greaterThan(root.get(idColumn), UUIDConverter.fromTimeUUID(pageLink.getIdOffset())); - predicates.add(lowerBound); - } else if (pageLink.getStartTime() != null) { - UUID startOf = UUIDs.startOf(pageLink.getStartTime()); - Predicate lowerBound = criteriaBuilder.greaterThanOrEqualTo(root.get(idColumn), UUIDConverter.fromTimeUUID(startOf)); - predicates.add(lowerBound); - } - if (pageLink.getEndTime() != null) { - UUID endOf = UUIDs.endOf(pageLink.getEndTime()); - Predicate upperBound = criteriaBuilder.lessThanOrEqualTo(root.get(idColumn), UUIDConverter.fromTimeUUID(endOf)); - predicates.add(upperBound); - } - return predicates; - } - - private static List createDescPredicates(TimePageLink pageLink, String idColumn, Root root, CriteriaBuilder criteriaBuilder) { - List predicates = new ArrayList<>(); - if (pageLink.getIdOffset() != null) { - Predicate lowerBound = criteriaBuilder.lessThan(root.get(idColumn), UUIDConverter.fromTimeUUID(pageLink.getIdOffset())); - predicates.add(lowerBound); - } else if (pageLink.getEndTime() != null) { - UUID endOf = UUIDs.endOf(pageLink.getEndTime()); - Predicate lowerBound = criteriaBuilder.lessThanOrEqualTo(root.get(idColumn), UUIDConverter.fromTimeUUID(endOf)); - predicates.add(lowerBound); - } - if (pageLink.getStartTime() != null) { - UUID startOf = UUIDs.startOf(pageLink.getStartTime()); - Predicate upperBound = criteriaBuilder.greaterThanOrEqualTo(root.get(idColumn), UUIDConverter.fromTimeUUID(startOf)); - predicates.add(upperBound); - } - return predicates; - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaExecutorService.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaExecutorService.java index ef9d287299..ace621cd6a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaExecutorService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaExecutorService.java @@ -18,10 +18,8 @@ package org.thingsboard.server.dao.sql; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.thingsboard.common.util.AbstractListeningExecutor; -import org.thingsboard.server.dao.util.SqlDao; @Component -@SqlDao public class JpaExecutorService extends AbstractListeningExecutor { @Value("${spring.datasource.hikari.maximumPoolSize}") diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java index 22fda66759..6a3a66a320 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java @@ -22,6 +22,7 @@ import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.stats.MessagesStats; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; @@ -30,6 +31,7 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.stream.Collectors; +import java.util.stream.Stream; @Slf4j public class TbSqlBlockingQueue implements TbSqlQueue { @@ -46,7 +48,7 @@ public class TbSqlBlockingQueue implements TbSqlQueue { } @Override - public void init(ScheduledLogExecutorComponent logExecutor, Consumer> saveFunction, int index) { + public void init(ScheduledLogExecutorComponent logExecutor, Consumer> saveFunction, Comparator batchUpdateComparator, int index) { executor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("sql-queue-" + index + "-" + params.getLogName().toLowerCase())); executor.submit(() -> { String logName = params.getLogName(); @@ -65,7 +67,11 @@ public class TbSqlBlockingQueue implements TbSqlQueue { queue.drainTo(entities, batchSize - 1); boolean fullPack = entities.size() == batchSize; log.debug("[{}] Going to save {} entities", logName, entities.size()); - saveFunction.accept(entities.stream().map(TbSqlQueueElement::getEntity).collect(Collectors.toList())); + Stream entitiesStream = entities.stream().map(TbSqlQueueElement::getEntity); + saveFunction.accept( + (params.isBatchSortEnabled() ? entitiesStream.sorted(batchUpdateComparator) : entitiesStream) + .collect(Collectors.toList()) + ); entities.forEach(v -> v.getFuture().set(null)); stats.incrementSuccessful(entities.size()); if (!fullPack) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueParams.java b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueParams.java index a63461787e..6100405711 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueParams.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueParams.java @@ -31,4 +31,5 @@ public class TbSqlBlockingQueueParams { private final long maxDelay; private final long statsPrintIntervalMs; private final String statsNamePrefix; + private final boolean batchSortEnabled; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueWrapper.java b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueWrapper.java index d9596a6efd..884d4c7a3d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueWrapper.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueWrapper.java @@ -21,6 +21,7 @@ import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.stats.MessagesStats; import org.thingsboard.server.common.stats.StatsFactory; +import java.util.Comparator; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Consumer; @@ -36,12 +37,20 @@ public class TbSqlBlockingQueueWrapper { private final int maxThreads; private final StatsFactory statsFactory; - public void init(ScheduledLogExecutorComponent logExecutor, Consumer> saveFunction) { + /** + * Starts TbSqlBlockingQueues. + * + * @param logExecutor executor that will be printing logs and statistics + * @param saveFunction function to save entities in database + * @param batchUpdateComparator comparator to sort entities by primary key to avoid deadlocks in cluster mode + * NOTE: you must use all of primary key parts in your comparator + */ + public void init(ScheduledLogExecutorComponent logExecutor, Consumer> saveFunction, Comparator batchUpdateComparator) { for (int i = 0; i < maxThreads; i++) { MessagesStats stats = statsFactory.createMessagesStats(params.getStatsNamePrefix() + ".queue." + i); TbSqlBlockingQueue queue = new TbSqlBlockingQueue<>(params, stats); queues.add(queue); - queue.init(logExecutor, saveFunction, i); + queue.init(logExecutor, saveFunction, batchUpdateComparator, i); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueue.java b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueue.java index c3955a811c..135e636ea5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueue.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueue.java @@ -17,12 +17,13 @@ package org.thingsboard.server.dao.sql; import com.google.common.util.concurrent.ListenableFuture; +import java.util.Comparator; import java.util.List; import java.util.function.Consumer; public interface TbSqlQueue { - void init(ScheduledLogExecutorComponent logExecutor, Consumer> saveFunction, int queueIndex); + void init(ScheduledLogExecutorComponent logExecutor, Consumer> saveFunction, Comparator batchUpdateComparator, int queueIndex); void destroy(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java index 18159e6fca..550d727e06 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java @@ -15,24 +15,64 @@ */ package org.thingsboard.server.dao.sql.alarm; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; -import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.dao.model.sql.AlarmEntity; -import org.thingsboard.server.dao.util.SqlDao; +import org.thingsboard.server.dao.model.sql.AlarmInfoEntity; import java.util.List; +import java.util.Set; +import java.util.UUID; /** * Created by Valerii Sosliuk on 5/21/2017. */ -@SqlDao -public interface AlarmRepository extends CrudRepository { +public interface AlarmRepository extends CrudRepository { - @Query("SELECT a FROM AlarmEntity a WHERE a.originatorId = :originatorId AND a.type = :alarmType ORDER BY startTs DESC") - List findLatestByOriginatorAndType(@Param("originatorId") String originatorId, + @Query("SELECT a FROM AlarmEntity a WHERE a.originatorId = :originatorId AND a.type = :alarmType ORDER BY a.startTs DESC") + List findLatestByOriginatorAndType(@Param("originatorId") UUID originatorId, @Param("alarmType") String alarmType, Pageable pageable); + + @Query(value = "SELECT new org.thingsboard.server.dao.model.sql.AlarmInfoEntity(a) FROM AlarmEntity a " + + "LEFT JOIN RelationEntity re ON a.id = re.toId " + + "AND re.relationTypeGroup = 'ALARM' " + + "AND re.toType = 'ALARM' " + + "AND re.fromId = :affectedEntityId " + + "AND re.fromType = :affectedEntityType " + + "WHERE a.tenantId = :tenantId " + + "AND (a.originatorId = :affectedEntityId or re.fromId IS NOT NULL) " + + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + + "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + + "AND (LOWER(a.type) LIKE LOWER(CONCAT(:searchText, '%'))" + + "OR LOWER(a.severity) LIKE LOWER(CONCAT(:searchText, '%'))" + + "OR LOWER(a.status) LIKE LOWER(CONCAT(:searchText, '%')))", + countQuery = "SELECT count(a) FROM AlarmEntity a " + + "LEFT JOIN RelationEntity re ON a.id = re.toId " + + "AND re.relationTypeGroup = 'ALARM' " + + "AND re.toType = 'ALARM' " + + "AND re.fromId = :affectedEntityId " + + "AND re.fromType = :affectedEntityType " + + "WHERE a.tenantId = :tenantId " + + "AND (a.originatorId = :affectedEntityId or re.fromId IS NOT NULL) " + + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + + "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + + "AND (LOWER(a.type) LIKE LOWER(CONCAT(:searchText, '%'))" + + "OR LOWER(a.severity) LIKE LOWER(CONCAT(:searchText, '%'))" + + "OR LOWER(a.status) LIKE LOWER(CONCAT(:searchText, '%')))") + Page findAlarms(@Param("tenantId") UUID tenantId, + @Param("affectedEntityId") UUID affectedEntityId, + @Param("affectedEntityType") String affectedEntityType, + @Param("startTime") Long startTime, + @Param("endTime") Long endTime, + @Param("alarmStatuses") Set alarmStatuses, + @Param("searchText") String searchText, + Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java index 356781d297..45acb3efcc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java @@ -15,34 +15,34 @@ */ package org.thingsboard.server.dao.sql.alarm; -import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; -import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; +import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.relation.EntityRelation; -import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AlarmData; +import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.alarm.AlarmDao; -import org.thingsboard.server.dao.alarm.BaseAlarmService; import org.thingsboard.server.dao.model.sql.AlarmEntity; import org.thingsboard.server.dao.relation.RelationDao; import org.thingsboard.server.dao.sql.JpaAbstractDao; -import org.thingsboard.server.dao.util.SqlDao; +import org.thingsboard.server.dao.sql.query.AlarmQueryRepository; -import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; +import java.util.Objects; +import java.util.Set; import java.util.UUID; /** @@ -50,12 +50,14 @@ import java.util.UUID; */ @Slf4j @Component -@SqlDao public class JpaAlarmDao extends JpaAbstractDao implements AlarmDao { @Autowired private AlarmRepository alarmRepository; + @Autowired + private AlarmQueryRepository alarmQueryRepository; + @Autowired private RelationDao relationDao; @@ -65,7 +67,7 @@ public class JpaAlarmDao extends JpaAbstractDao implements A } @Override - protected CrudRepository getCrudRepository() { + protected CrudRepository getCrudRepository() { return alarmRepository; } @@ -78,7 +80,7 @@ public class JpaAlarmDao extends JpaAbstractDao implements A public ListenableFuture findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type) { return service.submit(() -> { List latest = alarmRepository.findLatestByOriginatorAndType( - UUIDConverter.fromTimeUUID(originator.getId()), + originator.getId(), type, PageRequest.of(0, 1)); return latest.isEmpty() ? null : DaoUtil.getData(latest.get(0)); @@ -91,27 +93,31 @@ public class JpaAlarmDao extends JpaAbstractDao implements A } @Override - public ListenableFuture> findAlarms(TenantId tenantId, AlarmQuery query) { + public PageData findAlarms(TenantId tenantId, AlarmQuery query) { log.trace("Try to find alarms by entity [{}], status [{}] and pageLink [{}]", query.getAffectedEntityId(), query.getStatus(), query.getPageLink()); EntityId affectedEntity = query.getAffectedEntityId(); - String searchStatusName; - if (query.getSearchStatus() == null && query.getStatus() == null) { - searchStatusName = AlarmSearchStatus.ANY.name(); - } else if (query.getSearchStatus() != null) { - searchStatusName = query.getSearchStatus().name(); - } else { - searchStatusName = query.getStatus().name(); + Set statusSet = null; + if (query.getSearchStatus() != null) { + statusSet = query.getSearchStatus().getStatuses(); + } else if (query.getStatus() != null) { + statusSet = Collections.singleton(query.getStatus()); } - String relationType = BaseAlarmService.ALARM_RELATION_PREFIX + searchStatusName; - ListenableFuture> relations = relationDao.findRelations(tenantId, affectedEntity, relationType, RelationTypeGroup.ALARM, EntityType.ALARM, query.getPageLink()); - return Futures.transformAsync(relations, input -> { - List> alarmFutures = new ArrayList<>(input.size()); - for (EntityRelation relation : input) { - alarmFutures.add(Futures.transform( - findAlarmByIdAsync(tenantId, relation.getTo().getId()), - AlarmInfo::new, MoreExecutors.directExecutor())); - } - return Futures.successfulAsList(alarmFutures); - }, MoreExecutors.directExecutor()); + return DaoUtil.toPageData( + alarmRepository.findAlarms( + tenantId.getId(), + affectedEntity.getId(), + affectedEntity.getEntityType().name(), + query.getPageLink().getStartTime(), + query.getPageLink().getEndTime(), + statusSet, + Objects.toString(query.getPageLink().getTextSearch(), ""), + DaoUtil.toPageable(query.getPageLink()) + ) + ); + } + + @Override + public PageData findAlarmDataByQueryForEntities(TenantId tenantId, CustomerId customerId, AlarmDataQuery query, Collection orderedEntityIds) { + return alarmQueryRepository.findAlarmDataByQueryForEntities(tenantId, customerId, query, orderedEntityIds); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java index aef9558cab..a6e016c418 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java @@ -15,67 +15,112 @@ */ package org.thingsboard.server.dao.sql.asset; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; import org.thingsboard.server.dao.model.sql.AssetEntity; -import org.thingsboard.server.dao.util.SqlDao; +import org.thingsboard.server.dao.model.sql.AssetInfoEntity; import java.util.List; +import java.util.UUID; /** * Created by Valerii Sosliuk on 5/21/2017. */ -@SqlDao -public interface AssetRepository extends CrudRepository { +public interface AssetRepository extends PagingAndSortingRepository { + + @Query("SELECT new org.thingsboard.server.dao.model.sql.AssetInfoEntity(a, c.title, c.additionalInfo) " + + "FROM AssetEntity a " + + "LEFT JOIN CustomerEntity c on c.id = a.customerId " + + "WHERE a.id = :assetId") + AssetInfoEntity findAssetInfoById(@Param("assetId") UUID assetId); @Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " + - "AND LOWER(a.searchText) LIKE LOWER(CONCAT(:textSearch, '%')) " + - "AND a.id > :idOffset ORDER BY a.id") - List findByTenantId(@Param("tenantId") String tenantId, + "AND LOWER(a.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, - @Param("idOffset") String idOffset, Pageable pageable); + @Query("SELECT new org.thingsboard.server.dao.model.sql.AssetInfoEntity(a, c.title, c.additionalInfo) " + + "FROM AssetEntity a " + + "LEFT JOIN CustomerEntity c on c.id = a.customerId " + + "WHERE a.tenantId = :tenantId " + + "AND LOWER(a.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findAssetInfosByTenantId(@Param("tenantId") UUID tenantId, + @Param("textSearch") String textSearch, + Pageable pageable); + @Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " + "AND a.customerId = :customerId " + - "AND LOWER(a.searchText) LIKE LOWER(CONCAT(:textSearch, '%')) " + - "AND a.id > :idOffset ORDER BY a.id") - List findByTenantIdAndCustomerId(@Param("tenantId") String tenantId, - @Param("customerId") String customerId, + "AND LOWER(a.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, + @Param("customerId") UUID customerId, @Param("textSearch") String textSearch, - @Param("idOffset") String idOffset, Pageable pageable); - List findByTenantIdAndIdIn(String tenantId, List assetIds); + @Query("SELECT new org.thingsboard.server.dao.model.sql.AssetInfoEntity(a, c.title, c.additionalInfo) " + + "FROM AssetEntity a " + + "LEFT JOIN CustomerEntity c on c.id = a.customerId " + + "WHERE a.tenantId = :tenantId " + + "AND a.customerId = :customerId " + + "AND LOWER(a.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + Page findAssetInfosByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, + @Param("customerId") UUID customerId, + @Param("searchText") String searchText, + Pageable pageable); - List findByTenantIdAndCustomerIdAndIdIn(String tenantId, String customerId, List assetIds); + List findByTenantIdAndIdIn(UUID tenantId, List assetIds); - AssetEntity findByTenantIdAndName(String tenantId, String name); + List findByTenantIdAndCustomerIdAndIdIn(UUID tenantId, UUID customerId, List assetIds); + + AssetEntity findByTenantIdAndName(UUID tenantId, String name); @Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " + "AND a.type = :type " + - "AND LOWER(a.searchText) LIKE LOWER(CONCAT(:textSearch, '%')) " + - "AND a.id > :idOffset ORDER BY a.id") - List findByTenantIdAndType(@Param("tenantId") String tenantId, + "AND LOWER(a.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findByTenantIdAndType(@Param("tenantId") UUID tenantId, @Param("type") String type, @Param("textSearch") String textSearch, - @Param("idOffset") String idOffset, Pageable pageable); + @Query("SELECT new org.thingsboard.server.dao.model.sql.AssetInfoEntity(a, c.title, c.additionalInfo) " + + "FROM AssetEntity a " + + "LEFT JOIN CustomerEntity c on c.id = a.customerId " + + "WHERE a.tenantId = :tenantId " + + "AND a.type = :type " + + "AND LOWER(a.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findAssetInfosByTenantIdAndType(@Param("tenantId") UUID tenantId, + @Param("type") String type, + @Param("textSearch") String textSearch, + Pageable pageable); + + @Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " + "AND a.customerId = :customerId AND a.type = :type " + - "AND LOWER(a.searchText) LIKE LOWER(CONCAT(:textSearch, '%')) " + - "AND a.id > :idOffset ORDER BY a.id") - List findByTenantIdAndCustomerIdAndType(@Param("tenantId") String tenantId, - @Param("customerId") String customerId, + "AND LOWER(a.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId, + @Param("customerId") UUID customerId, @Param("type") String type, @Param("textSearch") String textSearch, - @Param("idOffset") String idOffset, Pageable pageable); + @Query("SELECT new org.thingsboard.server.dao.model.sql.AssetInfoEntity(a, c.title, c.additionalInfo) " + + "FROM AssetEntity a " + + "LEFT JOIN CustomerEntity c on c.id = a.customerId " + + "WHERE a.tenantId = :tenantId " + + "AND a.customerId = :customerId " + + "AND a.type = :type " + + "AND LOWER(a.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findAssetInfosByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId, + @Param("customerId") UUID customerId, + @Param("type") String type, + @Param("textSearch") String textSearch, + Pageable pageable); + @Query("SELECT DISTINCT a.type FROM AssetEntity a WHERE a.tenantId = :tenantId") - List findTenantAssetTypes(@Param("tenantId") String tenantId); + List findTenantAssetTypes(@Param("tenantId") UUID tenantId); + Long countByTenantId(UUID tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java index c4482a3247..698183ea0a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java @@ -17,19 +17,20 @@ package org.thingsboard.server.dao.sql.asset; import com.google.common.util.concurrent.ListenableFuture; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.PageRequest; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.asset.AssetDao; import org.thingsboard.server.dao.model.sql.AssetEntity; +import org.thingsboard.server.dao.model.sql.AssetInfoEntity; import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; -import org.thingsboard.server.dao.util.SqlDao; import java.util.ArrayList; import java.util.Collections; @@ -38,15 +39,10 @@ import java.util.Objects; import java.util.Optional; import java.util.UUID; -import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID; -import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUIDs; -import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID_STR; - /** * Created by Valerii Sosliuk on 5/19/2017. */ @Component -@SqlDao public class JpaAssetDao extends JpaAbstractSearchTextDao implements AssetDao { @Autowired @@ -58,75 +54,116 @@ public class JpaAssetDao extends JpaAbstractSearchTextDao im } @Override - protected CrudRepository getCrudRepository() { + protected CrudRepository getCrudRepository() { return assetRepository; } @Override - public List findAssetsByTenantId(UUID tenantId, TextPageLink pageLink) { - return DaoUtil.convertDataList(assetRepository + public AssetInfo findAssetInfoById(TenantId tenantId, UUID assetId) { + return DaoUtil.getData(assetRepository.findAssetInfoById(assetId)); + } + + @Override + public PageData findAssetsByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.toPageData(assetRepository .findByTenantId( - fromTimeUUID(tenantId), + tenantId, Objects.toString(pageLink.getTextSearch(), ""), - pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()), - PageRequest.of(0, pageLink.getLimit()))); + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findAssetInfosByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.toPageData( + assetRepository.findAssetInfosByTenantId( + tenantId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink, AssetInfoEntity.assetInfoColumnMap))); } @Override public ListenableFuture> findAssetsByTenantIdAndIdsAsync(UUID tenantId, List assetIds) { return service.submit(() -> - DaoUtil.convertDataList(assetRepository.findByTenantIdAndIdIn(fromTimeUUID(tenantId), fromTimeUUIDs(assetIds)))); + DaoUtil.convertDataList(assetRepository.findByTenantIdAndIdIn(tenantId, assetIds))); } @Override - public List findAssetsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink) { - return DaoUtil.convertDataList(assetRepository + public PageData findAssetsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink) { + return DaoUtil.toPageData(assetRepository .findByTenantIdAndCustomerId( - fromTimeUUID(tenantId), - fromTimeUUID(customerId), + tenantId, + customerId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findAssetInfosByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink) { + return DaoUtil.toPageData( + assetRepository.findAssetInfosByTenantIdAndCustomerId( + tenantId, + customerId, Objects.toString(pageLink.getTextSearch(), ""), - pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()), - PageRequest.of(0, pageLink.getLimit()))); + DaoUtil.toPageable(pageLink, AssetInfoEntity.assetInfoColumnMap))); } @Override public ListenableFuture> findAssetsByTenantIdAndCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List assetIds) { return service.submit(() -> - DaoUtil.convertDataList(assetRepository.findByTenantIdAndCustomerIdAndIdIn(fromTimeUUID(tenantId), fromTimeUUID(customerId), fromTimeUUIDs(assetIds)))); + DaoUtil.convertDataList(assetRepository.findByTenantIdAndCustomerIdAndIdIn(tenantId, customerId, assetIds))); } @Override public Optional findAssetsByTenantIdAndName(UUID tenantId, String name) { - Asset asset = DaoUtil.getData(assetRepository.findByTenantIdAndName(fromTimeUUID(tenantId), name)); + Asset asset = DaoUtil.getData(assetRepository.findByTenantIdAndName(tenantId, name)); return Optional.ofNullable(asset); } @Override - public List findAssetsByTenantIdAndType(UUID tenantId, String type, TextPageLink pageLink) { - return DaoUtil.convertDataList(assetRepository + public PageData findAssetsByTenantIdAndType(UUID tenantId, String type, PageLink pageLink) { + return DaoUtil.toPageData(assetRepository .findByTenantIdAndType( - fromTimeUUID(tenantId), + tenantId, + type, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findAssetInfosByTenantIdAndType(UUID tenantId, String type, PageLink pageLink) { + return DaoUtil.toPageData( + assetRepository.findAssetInfosByTenantIdAndType( + tenantId, type, Objects.toString(pageLink.getTextSearch(), ""), - pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()), - PageRequest.of(0, pageLink.getLimit()))); + DaoUtil.toPageable(pageLink, AssetInfoEntity.assetInfoColumnMap))); } @Override - public List findAssetsByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, TextPageLink pageLink) { - return DaoUtil.convertDataList(assetRepository + public PageData findAssetsByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink) { + return DaoUtil.toPageData(assetRepository .findByTenantIdAndCustomerIdAndType( - fromTimeUUID(tenantId), - fromTimeUUID(customerId), + tenantId, + customerId, type, Objects.toString(pageLink.getTextSearch(), ""), - pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()), - PageRequest.of(0, pageLink.getLimit()))); + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findAssetInfosByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink) { + return DaoUtil.toPageData( + assetRepository.findAssetInfosByTenantIdAndCustomerIdAndType( + tenantId, + customerId, + type, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink, AssetInfoEntity.assetInfoColumnMap))); } @Override public ListenableFuture> findTenantAssetTypesAsync(UUID tenantId) { - return service.submit(() -> convertTenantAssetTypesToDto(tenantId, assetRepository.findTenantAssetTypes(fromTimeUUID(tenantId)))); + return service.submit(() -> convertTenantAssetTypesToDto(tenantId, assetRepository.findTenantAssetTypes(tenantId))); } private List convertTenantAssetTypesToDto(UUID tenantId, List types) { @@ -139,4 +176,10 @@ public class JpaAssetDao extends JpaAbstractSearchTextDao im } return list; } + + @Override + public Long countAssetsByTenantId(TenantId tenantId) { + return assetRepository.countByTenantId(tenantId.getId()); + + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java index f667587c73..01bfaa99b0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java @@ -25,16 +25,15 @@ import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; import org.thingsboard.server.dao.model.sql.AttributeKvEntity; -import org.thingsboard.server.dao.util.SqlDao; import java.sql.PreparedStatement; import java.sql.SQLException; +import java.sql.SQLType; import java.sql.Types; import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; -@SqlDao @Repository @Slf4j public abstract class AttributeKvInsertRepository { @@ -92,7 +91,7 @@ public abstract class AttributeKvInsertRepository { ps.setLong(6, kvEntity.getLastUpdateTs()); ps.setString(7, kvEntity.getId().getEntityType().name()); - ps.setString(8, kvEntity.getId().getEntityId()); + ps.setObject(8, kvEntity.getId().getEntityId()); ps.setString(9, kvEntity.getId().getAttributeType()); ps.setString(10, kvEntity.getId().getAttributeKey()); } @@ -122,7 +121,7 @@ public abstract class AttributeKvInsertRepository { public void setValues(PreparedStatement ps, int i) throws SQLException { AttributeKvEntity kvEntity = insertEntities.get(i); ps.setString(1, kvEntity.getId().getEntityType().name()); - ps.setString(2, kvEntity.getId().getEntityId()); + ps.setObject(2, kvEntity.getId().getEntityId()); ps.setString(3, kvEntity.getId().getAttributeType()); ps.setString(4, kvEntity.getId().getAttributeKey()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java index 0bd667b790..f3ef44e6c7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java @@ -23,18 +23,17 @@ import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.dao.model.sql.AttributeKvCompositeKey; import org.thingsboard.server.dao.model.sql.AttributeKvEntity; -import org.thingsboard.server.dao.util.SqlDao; import java.util.List; +import java.util.UUID; -@SqlDao public interface AttributeKvRepository extends CrudRepository { @Query("SELECT a FROM AttributeKvEntity a WHERE a.id.entityType = :entityType " + "AND a.id.entityId = :entityId " + "AND a.id.attributeType = :attributeType") List findAllByEntityTypeAndEntityIdAndAttributeType(@Param("entityType") EntityType entityType, - @Param("entityId") String entityId, + @Param("entityId") UUID entityId, @Param("attributeType") String attributeType); @Transactional @@ -44,7 +43,7 @@ public interface AttributeKvRepository extends CrudRepository { jdbcTemplate.update(INSERT_OR_UPDATE, ps -> { ps.setString(1, entity.getId().getEntityType().name()); - ps.setString(2, entity.getId().getEntityId()); + ps.setObject(2, entity.getId().getEntityId()); ps.setString(3, entity.getId().getAttributeType()); ps.setString(4, entity.getId().getAttributeKey()); ps.setString(5, entity.getStrValue()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java index 2a38c6a0f1..69616155d2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java @@ -22,7 +22,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; @@ -35,21 +34,18 @@ import org.thingsboard.server.dao.sql.JpaAbstractDaoListeningExecutorService; import org.thingsboard.server.dao.sql.ScheduledLogExecutorComponent; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper; -import org.thingsboard.server.dao.util.SqlDao; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.Collection; +import java.util.Comparator; import java.util.List; import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; -import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID; - @Component @Slf4j -@SqlDao public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService implements AttributesDao { @Autowired @@ -76,6 +72,9 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl @Value("${sql.attributes.batch_threads:4}") private int batchThreads; + @Value("${sql.batch_sort:false}") + private boolean batchSortEnabled; + private TbSqlBlockingQueueWrapper queue; @PostConstruct @@ -86,11 +85,17 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl .maxDelay(maxDelay) .statsPrintIntervalMs(statsPrintIntervalMs) .statsNamePrefix("attributes") + .batchSortEnabled(batchSortEnabled) .build(); Function hashcodeFunction = entity -> entity.getId().getEntityId().hashCode(); queue = new TbSqlBlockingQueueWrapper<>(params, hashcodeFunction, batchThreads, statsFactory); - queue.init(logExecutor, v -> attributeKvInsertRepository.saveOrUpdate(v)); + queue.init(logExecutor, v -> attributeKvInsertRepository.saveOrUpdate(v), + Comparator.comparing((AttributeKvEntity attributeKvEntity) -> attributeKvEntity.getId().getEntityId()) + .thenComparing(attributeKvEntity -> attributeKvEntity.getId().getEntityType().name()) + .thenComparing(attributeKvEntity -> attributeKvEntity.getId().getAttributeType()) + .thenComparing(attributeKvEntity -> attributeKvEntity.getId().getAttributeKey()) + ); } @PreDestroy @@ -126,14 +131,14 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl DaoUtil.convertDataList(Lists.newArrayList( attributeKvRepository.findAllByEntityTypeAndEntityIdAndAttributeType( entityId.getEntityType(), - UUIDConverter.fromTimeUUID(entityId.getId()), + entityId.getId(), attributeType)))); } @Override public ListenableFuture save(TenantId tenantId, EntityId entityId, String attributeType, AttributeKvEntry attribute) { AttributeKvEntity entity = new AttributeKvEntity(); - entity.setId(new AttributeKvCompositeKey(entityId.getEntityType(), fromTimeUUID(entityId.getId()), attributeType, attribute.getKey())); + entity.setId(new AttributeKvCompositeKey(entityId.getEntityType(), entityId.getId(), attributeType, attribute.getKey())); entity.setLastUpdateTs(attribute.getLastUpdateTs()); entity.setStrValue(attribute.getStrValue().orElse(null)); entity.setDoubleValue(attribute.getDoubleValue().orElse(null)); @@ -151,7 +156,7 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl public ListenableFuture> removeAll(TenantId tenantId, EntityId entityId, String attributeType, List keys) { return service.submit(() -> { keys.forEach(key -> - attributeKvRepository.delete(entityId.getEntityType(), UUIDConverter.fromTimeUUID(entityId.getId()), attributeType, key) + attributeKvRepository.delete(entityId.getEntityType(), entityId.getId(), attributeType, key) ); return null; }); @@ -160,7 +165,7 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl private AttributeKvCompositeKey getAttributeKvCompositeKey(EntityId entityId, String attributeType, String attributeKey) { return new AttributeKvCompositeKey( entityId.getEntityType(), - fromTimeUUID(entityId.getId()), + entityId.getId(), attributeType, attributeKey); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/PsqlAttributesInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/PsqlAttributesInsertRepository.java index 020e63cd36..076a472da1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/PsqlAttributesInsertRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/PsqlAttributesInsertRepository.java @@ -18,9 +18,7 @@ package org.thingsboard.server.dao.sql.attributes; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.dao.util.PsqlDao; -import org.thingsboard.server.dao.util.SqlDao; -@SqlDao @PsqlDao @Repository @Transactional diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java index c89d35da6c..b5374e318c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java @@ -15,10 +15,96 @@ */ package org.thingsboard.server.dao.sql.audit; -import org.springframework.data.jpa.repository.JpaSpecificationExecutor; -import org.springframework.data.repository.CrudRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.dao.model.sql.AuditLogEntity; -public interface AuditLogRepository extends CrudRepository, JpaSpecificationExecutor { +import java.util.List; +import java.util.UUID; + +public interface AuditLogRepository extends PagingAndSortingRepository { + + @Query("SELECT a FROM AuditLogEntity a WHERE " + + "a.tenantId = :tenantId " + + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + + "AND ((:actionTypes) IS NULL OR a.actionType in (:actionTypes)) " + + "AND (LOWER(a.entityType) LIKE LOWER(CONCAT(:textSearch, '%'))" + + "OR LOWER(a.entityName) LIKE LOWER(CONCAT(:textSearch, '%'))" + + "OR LOWER(a.userName) LIKE LOWER(CONCAT(:textSearch, '%'))" + + "OR LOWER(a.actionType) LIKE LOWER(CONCAT(:textSearch, '%'))" + + "OR LOWER(a.actionStatus) LIKE LOWER(CONCAT(:textSearch, '%')))" + ) + Page findByTenantId( + @Param("tenantId") UUID tenantId, + @Param("textSearch") String textSearch, + @Param("startTime") Long startTime, + @Param("endTime") Long endTime, + @Param("actionTypes") List actionTypes, + Pageable pageable); + + @Query("SELECT a FROM AuditLogEntity a WHERE " + + "a.tenantId = :tenantId " + + "AND a.entityType = :entityType AND a.entityId = :entityId " + + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + + "AND ((:actionTypes) IS NULL OR a.actionType in (:actionTypes)) " + + "AND (LOWER(a.entityName) LIKE LOWER(CONCAT(:textSearch, '%'))" + + "OR LOWER(a.userName) LIKE LOWER(CONCAT(:textSearch, '%'))" + + "OR LOWER(a.actionType) LIKE LOWER(CONCAT(:textSearch, '%'))" + + "OR LOWER(a.actionStatus) LIKE LOWER(CONCAT(:textSearch, '%')))" + ) + Page findAuditLogsByTenantIdAndEntityId(@Param("tenantId") UUID tenantId, + @Param("entityType") EntityType entityType, + @Param("entityId") UUID entityId, + @Param("textSearch") String textSearch, + @Param("startTime") Long startTime, + @Param("endTime") Long endTime, + @Param("actionTypes") List actionTypes, + Pageable pageable); + + @Query("SELECT a FROM AuditLogEntity a WHERE " + + "a.tenantId = :tenantId " + + "AND a.customerId = :customerId " + + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + + "AND ((:actionTypes) IS NULL OR a.actionType in (:actionTypes)) " + + "AND (LOWER(a.entityType) LIKE LOWER(CONCAT(:textSearch, '%'))" + + "OR LOWER(a.entityName) LIKE LOWER(CONCAT(:textSearch, '%'))" + + "OR LOWER(a.userName) LIKE LOWER(CONCAT(:textSearch, '%'))" + + "OR LOWER(a.actionType) LIKE LOWER(CONCAT(:textSearch, '%'))" + + "OR LOWER(a.actionStatus) LIKE LOWER(CONCAT(:textSearch, '%')))" + ) + Page findAuditLogsByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, + @Param("customerId") UUID customerId, + @Param("textSearch") String textSearch, + @Param("startTime") Long startTime, + @Param("endTime") Long endTime, + @Param("actionTypes") List actionTypes, + Pageable pageable); + + @Query("SELECT a FROM AuditLogEntity a WHERE " + + "a.tenantId = :tenantId " + + "AND a.userId = :userId " + + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + + "AND ((:actionTypes) IS NULL OR a.actionType in (:actionTypes)) " + + "AND (LOWER(a.entityType) LIKE LOWER(CONCAT(:textSearch, '%'))" + + "OR LOWER(a.entityName) LIKE LOWER(CONCAT(:textSearch, '%'))" + + "OR LOWER(a.actionType) LIKE LOWER(CONCAT(:textSearch, '%'))" + + "OR LOWER(a.actionStatus) LIKE LOWER(CONCAT(:textSearch, '%')))" + ) + Page findAuditLogsByTenantIdAndUserId(@Param("tenantId") UUID tenantId, + @Param("userId") UUID userId, + @Param("textSearch") String textSearch, + @Param("startTime") Long startTime, + @Param("endTime") Long endTime, + @Param("actionTypes") List actionTypes, + Pageable pageable); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java index 566d9c3225..714b6eebd4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java @@ -17,35 +17,25 @@ package org.thingsboard.server.dao.sql.audit; import com.google.common.util.concurrent.ListenableFuture; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.jpa.domain.Specification; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.AuditLog; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.audit.AuditLogDao; import org.thingsboard.server.dao.model.sql.AuditLogEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; -import org.thingsboard.server.dao.sql.JpaAbstractSearchTimeDao; -import org.thingsboard.server.dao.util.SqlDao; -import javax.persistence.criteria.Predicate; -import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.UUID; -import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY; - @Component -@SqlDao public class JpaAuditLogDao extends JpaAbstractDao implements AuditLogDao { @Autowired @@ -57,7 +47,7 @@ public class JpaAuditLogDao extends JpaAbstractDao imp } @Override - protected CrudRepository getCrudRepository() { + protected CrudRepository getCrudRepository() { return auditLogRepository; } @@ -70,78 +60,57 @@ public class JpaAuditLogDao extends JpaAbstractDao imp } @Override - public ListenableFuture saveByTenantIdAndEntityId(AuditLog auditLog) { - return service.submit(() -> null); + public PageData findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, List actionTypes, TimePageLink pageLink) { + return DaoUtil.toPageData( + auditLogRepository + .findAuditLogsByTenantIdAndEntityId( + tenantId, + entityId.getEntityType(), + entityId.getId(), + Objects.toString(pageLink.getTextSearch(), ""), + pageLink.getStartTime(), + pageLink.getEndTime(), + actionTypes, + DaoUtil.toPageable(pageLink))); } @Override - public ListenableFuture saveByTenantIdAndCustomerId(AuditLog auditLog) { - return service.submit(() -> null); + public PageData findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, List actionTypes, TimePageLink pageLink) { + return DaoUtil.toPageData( + auditLogRepository + .findAuditLogsByTenantIdAndCustomerId( + tenantId, + customerId.getId(), + Objects.toString(pageLink.getTextSearch(), ""), + pageLink.getStartTime(), + pageLink.getEndTime(), + actionTypes, + DaoUtil.toPageable(pageLink))); } @Override - public ListenableFuture saveByTenantIdAndUserId(AuditLog auditLog) { - return service.submit(() -> null); + public PageData findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, List actionTypes, TimePageLink pageLink) { + return DaoUtil.toPageData( + auditLogRepository + .findAuditLogsByTenantIdAndUserId( + tenantId, + userId.getId(), + Objects.toString(pageLink.getTextSearch(), ""), + pageLink.getStartTime(), + pageLink.getEndTime(), + actionTypes, + DaoUtil.toPageable(pageLink))); } @Override - public ListenableFuture savePartitionsByTenantId(AuditLog auditLog) { - return service.submit(() -> null); - } - - @Override - public List findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, List actionTypes, TimePageLink pageLink) { - return findAuditLogs(tenantId, entityId, null, null, actionTypes, pageLink); - } - - @Override - public List findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, List actionTypes, TimePageLink pageLink) { - return findAuditLogs(tenantId, null, customerId, null, actionTypes, pageLink); - } - - @Override - public List findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, List actionTypes, TimePageLink pageLink) { - return findAuditLogs(tenantId, null, null, userId, actionTypes, pageLink); - } - - @Override - public List findAuditLogsByTenantId(UUID tenantId, List actionTypes, TimePageLink pageLink) { - return findAuditLogs(tenantId, null, null, null, actionTypes, pageLink); - } - - private List findAuditLogs(UUID tenantId, EntityId entityId, CustomerId customerId, UserId userId, List actionTypes, TimePageLink pageLink) { - Specification timeSearchSpec = JpaAbstractSearchTimeDao.getTimeSearchPageSpec(pageLink, "id"); - Specification fieldsSpec = getEntityFieldsSpec(tenantId, entityId, customerId, userId, actionTypes); - Sort.Direction sortDirection = pageLink.isAscOrder() ? Sort.Direction.ASC : Sort.Direction.DESC; - Pageable pageable = PageRequest.of(0, pageLink.getLimit(), sortDirection, ID_PROPERTY); - return DaoUtil.convertDataList(auditLogRepository.findAll(Specification.where(timeSearchSpec).and(fieldsSpec), pageable).getContent()); - } - - private Specification getEntityFieldsSpec(UUID tenantId, EntityId entityId, CustomerId customerId, UserId userId, List actionTypes) { - return (root, criteriaQuery, criteriaBuilder) -> { - List predicates = new ArrayList<>(); - if (tenantId != null) { - Predicate tenantIdPredicate = criteriaBuilder.equal(root.get("tenantId"), UUIDConverter.fromTimeUUID(tenantId)); - predicates.add(tenantIdPredicate); - } - if (entityId != null) { - Predicate entityTypePredicate = criteriaBuilder.equal(root.get("entityType"), entityId.getEntityType()); - predicates.add(entityTypePredicate); - Predicate entityIdPredicate = criteriaBuilder.equal(root.get("entityId"), UUIDConverter.fromTimeUUID(entityId.getId())); - predicates.add(entityIdPredicate); - } - if (customerId != null) { - Predicate customerIdPredicate = criteriaBuilder.equal(root.get("customerId"), UUIDConverter.fromTimeUUID(customerId.getId())); - predicates.add(customerIdPredicate); - } - if (userId != null) { - Predicate userIdPredicate = criteriaBuilder.equal(root.get("userId"), UUIDConverter.fromTimeUUID(userId.getId())); - predicates.add(userIdPredicate); - } - if (actionTypes != null && !actionTypes.isEmpty()) { - predicates.add(root.get("actionType").in(actionTypes)); - } - return criteriaBuilder.and(predicates.toArray(new Predicate[]{})); - }; + public PageData findAuditLogsByTenantId(UUID tenantId, List actionTypes, TimePageLink pageLink) { + return DaoUtil.toPageData( + auditLogRepository.findByTenantId( + tenantId, + Objects.toString(pageLink.getTextSearch(), ""), + pageLink.getStartTime(), + pageLink.getEndTime(), + actionTypes, + DaoUtil.toPageable(pageLink))); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java index d8272e3a57..5ea12ca85c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java @@ -23,7 +23,6 @@ import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.DefaultTransactionDefinition; -import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.dao.model.sql.ComponentDescriptorEntity; import javax.persistence.EntityManager; @@ -69,7 +68,8 @@ public abstract class AbstractComponentDescriptorInsertRepository implements Com protected Query getQuery(ComponentDescriptorEntity entity, String query) { return entityManager.createNativeQuery(query, ComponentDescriptorEntity.class) - .setParameter("id", UUIDConverter.fromTimeUUID(entity.getUuid())) + .setParameter("id", entity.getUuid()) + .setParameter("created_time", entity.getCreatedTime()) .setParameter("actions", entity.getActions()) .setParameter("clazz", entity.getClazz()) .setParameter("configuration_descriptor", entity.getConfigurationDescriptor().toString()) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/component/ComponentDescriptorRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/component/ComponentDescriptorRepository.java index 87da21ef3f..668b31c84a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/component/ComponentDescriptorRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/component/ComponentDescriptorRepository.java @@ -15,40 +15,35 @@ */ package org.thingsboard.server.dao.sql.component; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; import org.thingsboard.server.common.data.plugin.ComponentScope; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.dao.model.sql.ComponentDescriptorEntity; -import org.thingsboard.server.dao.util.SqlDao; -import java.util.List; +import java.util.UUID; /** * Created by Valerii Sosliuk on 5/6/2017. */ -@SqlDao -public interface ComponentDescriptorRepository extends CrudRepository { +public interface ComponentDescriptorRepository extends PagingAndSortingRepository { ComponentDescriptorEntity findByClazz(String clazz); @Query("SELECT cd FROM ComponentDescriptorEntity cd WHERE cd.type = :type " + - "AND LOWER(cd.searchText) LIKE LOWER(CONCAT(:textSearch, '%')) " + - "AND cd.id > :idOffset ORDER BY cd.id") - List findByType(@Param("type") ComponentType type, + "AND LOWER(cd.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findByType(@Param("type") ComponentType type, @Param("textSearch") String textSearch, - @Param("idOffset") String idOffset, Pageable pageable); @Query("SELECT cd FROM ComponentDescriptorEntity cd WHERE cd.type = :type " + - "AND cd.scope = :scope AND LOWER(cd.searchText) LIKE LOWER(CONCAT(:textSearch, '%')) " + - "AND cd.id > :idOffset ORDER BY cd.id") - List findByScopeAndType(@Param("type") ComponentType type, + "AND cd.scope = :scope AND LOWER(cd.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findByScopeAndType(@Param("type") ComponentType type, @Param("scope") ComponentScope scope, @Param("textSearch") String textSearch, - @Param("idOffset") String idOffset, Pageable pageable); void deleteByClazz(String clazz); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/component/HsqlComponentDescriptorInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/component/HsqlComponentDescriptorInsertRepository.java index c33dd4f5e1..05e28f637c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/component/HsqlComponentDescriptorInsertRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/component/HsqlComponentDescriptorInsertRepository.java @@ -16,17 +16,16 @@ package org.thingsboard.server.dao.sql.component; import org.springframework.stereotype.Repository; -import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.dao.model.sql.ComponentDescriptorEntity; import org.thingsboard.server.dao.util.HsqlDao; -import org.thingsboard.server.dao.util.SqlDao; -@SqlDao +import javax.persistence.Query; + @HsqlDao @Repository public class HsqlComponentDescriptorInsertRepository extends AbstractComponentDescriptorInsertRepository { - private static final String P_KEY_CONFLICT_STATEMENT = "(component_descriptor.id=I.id)"; + private static final String P_KEY_CONFLICT_STATEMENT = "(component_descriptor.id=UUID(I.id))"; private static final String UNQ_KEY_CONFLICT_STATEMENT = "(component_descriptor.clazz=I.clazz)"; private static final String INSERT_OR_UPDATE_ON_P_KEY_CONFLICT = getInsertString(P_KEY_CONFLICT_STATEMENT); @@ -37,14 +36,30 @@ public class HsqlComponentDescriptorInsertRepository extends AbstractComponentDe return saveAndGet(entity, INSERT_OR_UPDATE_ON_P_KEY_CONFLICT, INSERT_OR_UPDATE_ON_UNQ_KEY_CONFLICT); } + @Override + protected Query getQuery(ComponentDescriptorEntity entity, String query) { + return entityManager.createNativeQuery(query, ComponentDescriptorEntity.class) + .setParameter("id", entity.getUuid().toString()) + .setParameter("created_time", entity.getCreatedTime()) + .setParameter("actions", entity.getActions()) + .setParameter("clazz", entity.getClazz()) + .setParameter("configuration_descriptor", entity.getConfigurationDescriptor().toString()) + .setParameter("name", entity.getName()) + .setParameter("scope", entity.getScope().name()) + .setParameter("search_text", entity.getSearchText()) + .setParameter("type", entity.getType().name()); + } + @Override protected ComponentDescriptorEntity doProcessSaveOrUpdate(ComponentDescriptorEntity entity, String query) { getQuery(entity, query).executeUpdate(); - return entityManager.find(ComponentDescriptorEntity.class, UUIDConverter.fromTimeUUID(entity.getUuid())); + return entityManager.find(ComponentDescriptorEntity.class, entity.getUuid()); } private static String getInsertString(String conflictStatement) { - return "MERGE INTO component_descriptor USING (VALUES :id, :actions, :clazz, :configuration_descriptor, :name, :scope, :search_text, :type) I (id, actions, clazz, configuration_descriptor, name, scope, search_text, type) ON " + conflictStatement + " WHEN MATCHED THEN UPDATE SET component_descriptor.id = I.id, component_descriptor.actions = I.actions, component_descriptor.clazz = I.clazz, component_descriptor.configuration_descriptor = I.configuration_descriptor, component_descriptor.name = I.name, component_descriptor.scope = I.scope, component_descriptor.search_text = I.search_text, component_descriptor.type = I.type" + - " WHEN NOT MATCHED THEN INSERT (id, actions, clazz, configuration_descriptor, name, scope, search_text, type) VALUES (I.id, I.actions, I.clazz, I.configuration_descriptor, I.name, I.scope, I.search_text, I.type)"; + return "MERGE INTO component_descriptor USING (VALUES :id, :created_time, :actions, :clazz, :configuration_descriptor, :name, :scope, :search_text, :type) I (id, created_time, actions, clazz, configuration_descriptor, name, scope, search_text, type) ON " + + conflictStatement + + " WHEN MATCHED THEN UPDATE SET component_descriptor.id = UUID(I.id), component_descriptor.actions = I.actions, component_descriptor.clazz = I.clazz, component_descriptor.configuration_descriptor = I.configuration_descriptor, component_descriptor.name = I.name, component_descriptor.scope = I.scope, component_descriptor.search_text = I.search_text, component_descriptor.type = I.type" + + " WHEN NOT MATCHED THEN INSERT (id, created_time, actions, clazz, configuration_descriptor, name, scope, search_text, type) VALUES (UUID(I.id), I.created_time, I.actions, I.clazz, I.configuration_descriptor, I.name, I.scope, I.search_text, I.type)"; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDao.java index e66937166f..d27eb7d3d1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDao.java @@ -15,16 +15,15 @@ */ package org.thingsboard.server.dao.sql.component; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.PageRequest; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.common.data.id.ComponentDescriptorId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentScope; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -32,19 +31,15 @@ import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.component.ComponentDescriptorDao; import org.thingsboard.server.dao.model.sql.ComponentDescriptorEntity; import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; -import org.thingsboard.server.dao.util.SqlDao; -import java.util.List; import java.util.Objects; import java.util.Optional; - -import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID_STR; +import java.util.UUID; /** * Created by Valerii Sosliuk on 5/6/2017. */ @Component -@SqlDao public class JpaBaseComponentDescriptorDao extends JpaAbstractSearchTextDao implements ComponentDescriptorDao { @@ -60,16 +55,18 @@ public class JpaBaseComponentDescriptorDao extends JpaAbstractSearchTextDao getCrudRepository() { + protected CrudRepository getCrudRepository() { return componentDescriptorRepository; } @Override public Optional saveIfNotExist(TenantId tenantId, ComponentDescriptor component) { if (component.getId() == null) { - component.setId(new ComponentDescriptorId(UUIDs.timeBased())); + UUID uuid = Uuids.timeBased(); + component.setId(new ComponentDescriptorId(uuid)); + component.setCreatedTime(Uuids.unixTimestamp(uuid)); } - if (!componentDescriptorRepository.existsById(UUIDConverter.fromTimeUUID(component.getId().getId()))) { + if (!componentDescriptorRepository.existsById(component.getId().getId())) { ComponentDescriptorEntity componentDescriptorEntity = new ComponentDescriptorEntity(component); ComponentDescriptorEntity savedEntity = componentDescriptorInsertRepository.saveOrUpdate(componentDescriptorEntity); return Optional.of(savedEntity.toData()); @@ -88,24 +85,22 @@ public class JpaBaseComponentDescriptorDao extends JpaAbstractSearchTextDao findByTypeAndPageLink(TenantId tenantId, ComponentType type, TextPageLink pageLink) { - return DaoUtil.convertDataList(componentDescriptorRepository + public PageData findByTypeAndPageLink(TenantId tenantId, ComponentType type, PageLink pageLink) { + return DaoUtil.toPageData(componentDescriptorRepository .findByType( type, Objects.toString(pageLink.getTextSearch(), ""), - pageLink.getIdOffset() == null ? NULL_UUID_STR : UUIDConverter.fromTimeUUID(pageLink.getIdOffset()), - PageRequest.of(0, pageLink.getLimit()))); + DaoUtil.toPageable(pageLink))); } @Override - public List findByScopeAndTypeAndPageLink(TenantId tenantId, ComponentScope scope, ComponentType type, TextPageLink pageLink) { - return DaoUtil.convertDataList(componentDescriptorRepository + public PageData findByScopeAndTypeAndPageLink(TenantId tenantId, ComponentScope scope, ComponentType type, PageLink pageLink) { + return DaoUtil.toPageData(componentDescriptorRepository .findByScopeAndType( type, scope, Objects.toString(pageLink.getTextSearch(), ""), - pageLink.getIdOffset() == null ? NULL_UUID_STR : UUIDConverter.fromTimeUUID(pageLink.getIdOffset()), - PageRequest.of(0, pageLink.getLimit()))); + DaoUtil.toPageable(pageLink))); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/component/PsqlComponentDescriptorInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/component/PsqlComponentDescriptorInsertRepository.java index 97808398f7..44a1cf8312 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/component/PsqlComponentDescriptorInsertRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/component/PsqlComponentDescriptorInsertRepository.java @@ -18,10 +18,7 @@ package org.thingsboard.server.dao.sql.component; import org.springframework.stereotype.Repository; import org.thingsboard.server.dao.model.sql.ComponentDescriptorEntity; import org.thingsboard.server.dao.util.PsqlDao; -import org.thingsboard.server.dao.util.SqlDao; -import org.thingsboard.server.dao.util.SqlTsDao; -@SqlDao @PsqlDao @Repository public class PsqlComponentDescriptorInsertRepository extends AbstractComponentDescriptorInsertRepository { @@ -49,10 +46,10 @@ public class PsqlComponentDescriptorInsertRepository extends AbstractComponentDe } private static String getInsertOrUpdateStatement(String conflictKeyStatement, String updateKeyStatement) { - return "INSERT INTO component_descriptor (id, actions, clazz, configuration_descriptor, name, scope, search_text, type) VALUES (:id, :actions, :clazz, :configuration_descriptor, :name, :scope, :search_text, :type) ON CONFLICT " + conflictKeyStatement + " DO UPDATE SET " + updateKeyStatement + " returning *"; + return "INSERT INTO component_descriptor (id, created_time, actions, clazz, configuration_descriptor, name, scope, search_text, type) VALUES (:id, :created_time, :actions, :clazz, :configuration_descriptor, :name, :scope, :search_text, :type) ON CONFLICT " + conflictKeyStatement + " DO UPDATE SET " + updateKeyStatement + " returning *"; } private static String getUpdateStatement(String id) { - return "actions = :actions, " + id + ", configuration_descriptor = :configuration_descriptor, name = :name, scope = :scope, search_text = :search_text, type = :type"; + return "actions = :actions, " + id + ",created_time = :created_time, configuration_descriptor = :configuration_descriptor, name = :name, scope = :scope, search_text = :search_text, type = :type"; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java index 3159007561..bf2a845ec0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java @@ -15,29 +15,26 @@ */ package org.thingsboard.server.dao.sql.customer; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; import org.thingsboard.server.dao.model.sql.CustomerEntity; -import org.thingsboard.server.dao.util.SqlDao; -import java.util.List; +import java.util.UUID; /** * Created by Valerii Sosliuk on 5/6/2017. */ -@SqlDao -public interface CustomerRepository extends CrudRepository { +public interface CustomerRepository extends PagingAndSortingRepository { @Query("SELECT c FROM CustomerEntity c WHERE c.tenantId = :tenantId " + - "AND LOWER(c.searchText) LIKE LOWER(CONCAT(:textSearch, '%')) " + - "AND c.id > :idOffset ORDER BY c.id") - List findByTenantId(@Param("tenantId") String tenantId, + "AND LOWER(c.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, - @Param("idOffset") String idOffset, Pageable pageable); - CustomerEntity findByTenantIdAndTitle(String tenantId, String title); + CustomerEntity findByTenantIdAndTitle(UUID tenantId, String title); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java index ae6c152c4b..14c025de42 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java @@ -16,30 +16,24 @@ package org.thingsboard.server.dao.sql.customer; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.PageRequest; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.UUIDConverter; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.customer.CustomerDao; import org.thingsboard.server.dao.model.sql.CustomerEntity; import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; -import org.thingsboard.server.dao.util.SqlDao; -import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.UUID; -import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID_STR; - /** * Created by Valerii Sosliuk on 5/6/2017. */ @Component -@SqlDao public class JpaCustomerDao extends JpaAbstractSearchTextDao implements CustomerDao { @Autowired @@ -51,22 +45,21 @@ public class JpaCustomerDao extends JpaAbstractSearchTextDao getCrudRepository() { + protected CrudRepository getCrudRepository() { return customerRepository; } @Override - public List findCustomersByTenantId(UUID tenantId, TextPageLink pageLink) { - return DaoUtil.convertDataList(customerRepository.findByTenantId( - UUIDConverter.fromTimeUUID(tenantId), + public PageData findCustomersByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.toPageData(customerRepository.findByTenantId( + tenantId, Objects.toString(pageLink.getTextSearch(), ""), - pageLink.getIdOffset() == null ? NULL_UUID_STR : UUIDConverter.fromTimeUUID(pageLink.getIdOffset()), - PageRequest.of(0, pageLink.getLimit()))); + DaoUtil.toPageable(pageLink))); } @Override public Optional findCustomersByTenantIdAndTitle(UUID tenantId, String title) { - Customer customer = DaoUtil.getData(customerRepository.findByTenantIdAndTitle(UUIDConverter.fromTimeUUID(tenantId), title)); + Customer customer = DaoUtil.getData(customerRepository.findByTenantIdAndTitle(tenantId, title)); return Optional.ofNullable(customer); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java index 2ebd03da78..b0eb8f4d7b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java @@ -15,27 +15,33 @@ */ package org.thingsboard.server.dao.sql.dashboard; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; import org.thingsboard.server.dao.model.sql.DashboardInfoEntity; -import org.thingsboard.server.dao.util.SqlDao; -import java.util.List; +import java.util.UUID; /** * Created by Valerii Sosliuk on 5/6/2017. */ -@SqlDao -public interface DashboardInfoRepository extends CrudRepository { +public interface DashboardInfoRepository extends PagingAndSortingRepository { @Query("SELECT di FROM DashboardInfoEntity di WHERE di.tenantId = :tenantId " + - "AND LOWER(di.searchText) LIKE LOWER(CONCAT(:searchText, '%')) " + - "AND di.id > :idOffset ORDER BY di.id") - List findByTenantId(@Param("tenantId") String tenantId, + "AND LOWER(di.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + Page findByTenantId(@Param("tenantId") UUID tenantId, @Param("searchText") String searchText, - @Param("idOffset") String idOffset, Pageable pageable); + @Query("SELECT di FROM DashboardInfoEntity di, RelationEntity re WHERE di.tenantId = :tenantId " + + "AND di.id = re.toId AND re.toType = 'DASHBOARD' AND re.relationTypeGroup = 'DASHBOARD' " + + "AND re.relationType = 'Contains' AND re.fromId = :customerId AND re.fromType = 'CUSTOMER' " + + "AND LOWER(di.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + Page findByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, + @Param("customerId") UUID customerId, + @Param("searchText") String searchText, + Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardRepository.java index bcb8e774cd..bc2dcf19f6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardRepository.java @@ -17,11 +17,11 @@ package org.thingsboard.server.dao.sql.dashboard; import org.springframework.data.repository.CrudRepository; import org.thingsboard.server.dao.model.sql.DashboardEntity; -import org.thingsboard.server.dao.util.SqlDao; + +import java.util.UUID; /** * Created by Valerii Sosliuk on 5/6/2017. */ -@SqlDao -public interface DashboardRepository extends CrudRepository { +public interface DashboardRepository extends CrudRepository { } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java index bdee891c10..8d637f0cbe 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java @@ -22,13 +22,13 @@ import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.dao.dashboard.DashboardDao; import org.thingsboard.server.dao.model.sql.DashboardEntity; import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; -import org.thingsboard.server.dao.util.SqlDao; + +import java.util.UUID; /** * Created by Valerii Sosliuk on 5/6/2017. */ @Component -@SqlDao public class JpaDashboardDao extends JpaAbstractSearchTextDao implements DashboardDao { @Autowired @@ -40,7 +40,7 @@ public class JpaDashboardDao extends JpaAbstractSearchTextDao getCrudRepository() { + protected CrudRepository getCrudRepository() { return dashboardRepository; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java index 6a5a6ea804..6fadc233d4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java @@ -15,43 +15,27 @@ */ package org.thingsboard.server.dao.sql.dashboard; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.PageRequest; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.DashboardInfo; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.UUIDConverter; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; -import org.thingsboard.server.common.data.page.TimePageLink; -import org.thingsboard.server.common.data.relation.EntityRelation; -import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.dashboard.DashboardInfoDao; import org.thingsboard.server.dao.model.sql.DashboardInfoEntity; import org.thingsboard.server.dao.relation.RelationDao; import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; -import org.thingsboard.server.dao.util.SqlDao; -import java.util.ArrayList; -import java.util.List; import java.util.Objects; import java.util.UUID; -import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID_STR; - /** * Created by Valerii Sosliuk on 5/6/2017. */ @Slf4j @Component -@SqlDao public class JpaDashboardInfoDao extends JpaAbstractSearchTextDao implements DashboardInfoDao { @Autowired @@ -71,27 +55,21 @@ public class JpaDashboardInfoDao extends JpaAbstractSearchTextDao findDashboardsByTenantId(UUID tenantId, TextPageLink pageLink) { - return DaoUtil.convertDataList(dashboardInfoRepository + public PageData findDashboardsByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.toPageData(dashboardInfoRepository .findByTenantId( - UUIDConverter.fromTimeUUID(tenantId), + tenantId, Objects.toString(pageLink.getTextSearch(), ""), - pageLink.getIdOffset() == null ? NULL_UUID_STR : UUIDConverter.fromTimeUUID(pageLink.getIdOffset()), - PageRequest.of(0, pageLink.getLimit()))); + DaoUtil.toPageable(pageLink))); } @Override - public ListenableFuture> findDashboardsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TimePageLink pageLink) { - log.debug("Try to find dashboards by tenantId [{}], customerId[{}] and pageLink [{}]", tenantId, customerId, pageLink); - - ListenableFuture> relations = relationDao.findRelations(new TenantId(tenantId), new CustomerId(customerId), EntityRelation.CONTAINS_TYPE, RelationTypeGroup.DASHBOARD, EntityType.DASHBOARD, pageLink); - - return Futures.transformAsync(relations, input -> { - List> dashboardFutures = new ArrayList<>(input.size()); - for (EntityRelation relation : input) { - dashboardFutures.add(findByIdAsync(new TenantId(tenantId), relation.getTo().getId())); - } - return Futures.successfulAsList(dashboardFutures); - }, MoreExecutors.directExecutor()); + public PageData findDashboardsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink) { + return DaoUtil.toPageData(dashboardInfoRepository + .findByTenantIdAndCustomerId( + tenantId, + customerId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceCredentialsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceCredentialsRepository.java index c3a6a162ba..c4577dd1e7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceCredentialsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceCredentialsRepository.java @@ -17,15 +17,15 @@ package org.thingsboard.server.dao.sql.device; import org.springframework.data.repository.CrudRepository; import org.thingsboard.server.dao.model.sql.DeviceCredentialsEntity; -import org.thingsboard.server.dao.util.SqlDao; + +import java.util.UUID; /** * Created by Valerii Sosliuk on 5/6/2017. */ -@SqlDao -public interface DeviceCredentialsRepository extends CrudRepository { +public interface DeviceCredentialsRepository extends CrudRepository { - DeviceCredentialsEntity findByDeviceId(String deviceId); + DeviceCredentialsEntity findByDeviceId(UUID deviceId); DeviceCredentialsEntity findByCredentialsId(String credentialsId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java new file mode 100644 index 0000000000..8385c9e371 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java @@ -0,0 +1,69 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.device; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.DeviceProfileInfo; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.dao.model.sql.DeviceProfileEntity; + +import java.util.UUID; + +public interface DeviceProfileRepository extends PagingAndSortingRepository { + + @Query("SELECT new org.thingsboard.server.common.data.DeviceProfileInfo(d.id, d.name, d.type, d.transportType) " + + "FROM DeviceProfileEntity d " + + "WHERE d.id = :deviceProfileId") + DeviceProfileInfo findDeviceProfileInfoById(@Param("deviceProfileId") UUID deviceProfileId); + + @Query("SELECT d FROM DeviceProfileEntity d WHERE " + + "d.tenantId = :tenantId AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findDeviceProfiles(@Param("tenantId") UUID tenantId, + @Param("textSearch") String textSearch, + Pageable pageable); + + @Query("SELECT new org.thingsboard.server.common.data.DeviceProfileInfo(d.id, d.name, d.type, d.transportType) " + + "FROM DeviceProfileEntity d WHERE " + + "d.tenantId = :tenantId AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findDeviceProfileInfos(@Param("tenantId") UUID tenantId, + @Param("textSearch") String textSearch, + Pageable pageable); + + @Query("SELECT new org.thingsboard.server.common.data.DeviceProfileInfo(d.id, d.name, d.type, d.transportType) " + + "FROM DeviceProfileEntity d WHERE " + + "d.tenantId = :tenantId AND d.transportType = :transportType AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findDeviceProfileInfos(@Param("tenantId") UUID tenantId, + @Param("textSearch") String textSearch, + @Param("transportType") DeviceTransportType transportType, + Pageable pageable); + + @Query("SELECT d FROM DeviceProfileEntity d " + + "WHERE d.tenantId = :tenantId AND d.isDefault = true") + DeviceProfileEntity findByDefaultTrueAndTenantId(@Param("tenantId") UUID tenantId); + + @Query("SELECT new org.thingsboard.server.common.data.DeviceProfileInfo(d.id, d.name, d.type, d.transportType) " + + "FROM DeviceProfileEntity d " + + "WHERE d.tenantId = :tenantId AND d.isDefault = true") + DeviceProfileInfo findDefaultDeviceProfileInfo(@Param("tenantId") UUID tenantId); + + DeviceProfileEntity findByTenantIdAndName(UUID id, String profileName); + + DeviceProfileEntity findByProvisionDeviceKey(@Param("provisionDeviceKey") String provisionDeviceKey); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java index baf34dc59b..9f3cc5b5a7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java @@ -15,77 +15,159 @@ */ package org.thingsboard.server.dao.sql.device; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; import org.thingsboard.server.dao.model.sql.DeviceEntity; -import org.thingsboard.server.dao.util.SqlDao; +import org.thingsboard.server.dao.model.sql.DeviceInfoEntity; import java.util.List; +import java.util.UUID; /** * Created by Valerii Sosliuk on 5/6/2017. */ -@SqlDao -public interface DeviceRepository extends CrudRepository { +public interface DeviceRepository extends PagingAndSortingRepository { + @Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo, p.name) " + + "FROM DeviceEntity d " + + "LEFT JOIN CustomerEntity c on c.id = d.customerId " + + "LEFT JOIN DeviceProfileEntity p on p.id = d.deviceProfileId " + + "WHERE d.id = :deviceId") + DeviceInfoEntity findDeviceInfoById(@Param("deviceId") UUID deviceId); @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " + "AND d.customerId = :customerId " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:searchText, '%')) " + - "AND d.id > :idOffset ORDER BY d.id") - List findByTenantIdAndCustomerId(@Param("tenantId") String tenantId, - @Param("customerId") String customerId, + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + Page findByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, + @Param("customerId") UUID customerId, @Param("searchText") String searchText, - @Param("idOffset") String idOffset, Pageable pageable); @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " + - "AND d.id > :idOffset ORDER BY d.id") - List findByTenantId(@Param("tenantId") String tenantId, - @Param("idOffset") String idOffset, + "AND d.deviceProfileId = :profileId " + + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + Page findByTenantIdAndProfileId(@Param("tenantId") UUID tenantId, + @Param("profileId") UUID profileId, + @Param("searchText") String searchText, + Pageable pageable); + + @Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo, p.name) " + + "FROM DeviceEntity d " + + "LEFT JOIN CustomerEntity c on c.id = d.customerId " + + "LEFT JOIN DeviceProfileEntity p on p.id = d.deviceProfileId " + + "WHERE d.tenantId = :tenantId " + + "AND d.customerId = :customerId " + + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + Page findDeviceInfosByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, + @Param("customerId") UUID customerId, + @Param("searchText") String searchText, + Pageable pageable); + + @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId") + Page findByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%')) " + - "AND d.id > :idOffset ORDER BY d.id") - List findByTenantId(@Param("tenantId") String tenantId, + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, - @Param("idOffset") String idOffset, Pageable pageable); + @Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo, p.name) " + + "FROM DeviceEntity d " + + "LEFT JOIN CustomerEntity c on c.id = d.customerId " + + "LEFT JOIN DeviceProfileEntity p on p.id = d.deviceProfileId " + + "WHERE d.tenantId = :tenantId " + + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findDeviceInfosByTenantId(@Param("tenantId") UUID tenantId, + @Param("textSearch") String textSearch, + Pageable pageable); + @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " + "AND d.type = :type " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%')) " + - "AND d.id > :idOffset ORDER BY d.id") - List findByTenantIdAndType(@Param("tenantId") String tenantId, + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findByTenantIdAndType(@Param("tenantId") UUID tenantId, @Param("type") String type, @Param("textSearch") String textSearch, - @Param("idOffset") String idOffset, Pageable pageable); + @Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo, p.name) " + + "FROM DeviceEntity d " + + "LEFT JOIN CustomerEntity c on c.id = d.customerId " + + "LEFT JOIN DeviceProfileEntity p on p.id = d.deviceProfileId " + + "WHERE d.tenantId = :tenantId " + + "AND d.type = :type " + + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findDeviceInfosByTenantIdAndType(@Param("tenantId") UUID tenantId, + @Param("type") String type, + @Param("textSearch") String textSearch, + Pageable pageable); + + @Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo, p.name) " + + "FROM DeviceEntity d " + + "LEFT JOIN CustomerEntity c on c.id = d.customerId " + + "LEFT JOIN DeviceProfileEntity p on p.id = d.deviceProfileId " + + "WHERE d.tenantId = :tenantId " + + "AND d.deviceProfileId = :deviceProfileId " + + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findDeviceInfosByTenantIdAndDeviceProfileId(@Param("tenantId") UUID tenantId, + @Param("deviceProfileId") UUID deviceProfileId, + @Param("textSearch") String textSearch, + Pageable pageable); + @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " + "AND d.customerId = :customerId " + "AND d.type = :type " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%')) " + - "AND d.id > :idOffset ORDER BY d.id") - List findByTenantIdAndCustomerIdAndType(@Param("tenantId") String tenantId, - @Param("customerId") String customerId, + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId, + @Param("customerId") UUID customerId, @Param("type") String type, @Param("textSearch") String textSearch, - @Param("idOffset") String idOffset, Pageable pageable); + @Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo, p.name) " + + "FROM DeviceEntity d " + + "LEFT JOIN CustomerEntity c on c.id = d.customerId " + + "LEFT JOIN DeviceProfileEntity p on p.id = d.deviceProfileId " + + "WHERE d.tenantId = :tenantId " + + "AND d.customerId = :customerId " + + "AND d.type = :type " + + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findDeviceInfosByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId, + @Param("customerId") UUID customerId, + @Param("type") String type, + @Param("textSearch") String textSearch, + Pageable pageable); + + @Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo, p.name) " + + "FROM DeviceEntity d " + + "LEFT JOIN CustomerEntity c on c.id = d.customerId " + + "LEFT JOIN DeviceProfileEntity p on p.id = d.deviceProfileId " + + "WHERE d.tenantId = :tenantId " + + "AND d.customerId = :customerId " + + "AND d.deviceProfileId = :deviceProfileId " + + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(@Param("tenantId") UUID tenantId, + @Param("customerId") UUID customerId, + @Param("deviceProfileId") UUID deviceProfileId, + @Param("textSearch") String textSearch, + Pageable pageable); + @Query("SELECT DISTINCT d.type FROM DeviceEntity d WHERE d.tenantId = :tenantId") - List findTenantDeviceTypes(@Param("tenantId") String tenantId); + List findTenantDeviceTypes(@Param("tenantId") UUID tenantId); + + DeviceEntity findByTenantIdAndName(UUID tenantId, String name); - DeviceEntity findByTenantIdAndName(String tenantId, String name); + List findDevicesByTenantIdAndCustomerIdAndIdIn(UUID tenantId, UUID customerId, List deviceIds); - List findDevicesByTenantIdAndCustomerIdAndIdIn(String tenantId, String customerId, List deviceIds); + List findDevicesByTenantIdAndIdIn(UUID tenantId, List deviceIds); - List findDevicesByTenantIdAndIdIn(String tenantId, List deviceIds); + DeviceEntity findByTenantIdAndId(UUID tenantId, UUID id); - DeviceEntity findByTenantIdAndId(String tenantId, String id); + Long countByDeviceProfileId(UUID deviceProfileId); + Long countByTenantId(UUID tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceCredentialsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceCredentialsDao.java index f88f578e9e..a2cde30fd7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceCredentialsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceCredentialsDao.java @@ -18,14 +18,12 @@ package org.thingsboard.server.dao.sql.device; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.device.DeviceCredentialsDao; import org.thingsboard.server.dao.model.sql.DeviceCredentialsEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; -import org.thingsboard.server.dao.util.SqlDao; import java.util.UUID; @@ -33,7 +31,6 @@ import java.util.UUID; * Created by Valerii Sosliuk on 5/6/2017. */ @Component -@SqlDao public class JpaDeviceCredentialsDao extends JpaAbstractDao implements DeviceCredentialsDao { @Autowired @@ -45,13 +42,13 @@ public class JpaDeviceCredentialsDao extends JpaAbstractDao getCrudRepository() { + protected CrudRepository getCrudRepository() { return deviceCredentialsRepository; } @Override public DeviceCredentials findByDeviceId(TenantId tenantId, UUID deviceId) { - return DaoUtil.getData(deviceCredentialsRepository.findByDeviceId(UUIDConverter.fromTimeUUID(deviceId))); + return DaoUtil.getData(deviceCredentialsRepository.findByDeviceId(deviceId)); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java index 57d0ef6bb4..fceefcadf9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java @@ -17,21 +17,21 @@ package org.thingsboard.server.dao.sql.device; import com.google.common.util.concurrent.ListenableFuture; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.PageRequest; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.device.DeviceDao; import org.thingsboard.server.dao.model.sql.DeviceEntity; +import org.thingsboard.server.dao.model.sql.DeviceInfoEntity; import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; -import org.thingsboard.server.dao.util.SqlDao; import java.util.ArrayList; import java.util.Collections; @@ -40,15 +40,10 @@ import java.util.Objects; import java.util.Optional; import java.util.UUID; -import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID; -import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUIDs; -import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID_STR; - /** * Created by Valerii Sosliuk on 5/6/2017. */ @Component -@SqlDao public class JpaDeviceDao extends JpaAbstractSearchTextDao implements DeviceDao { @Autowired @@ -60,92 +55,173 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao } @Override - protected CrudRepository getCrudRepository() { + protected CrudRepository getCrudRepository() { return deviceRepository; } @Override - public List findDevicesByTenantId(UUID tenantId, TextPageLink pageLink) { + public DeviceInfo findDeviceInfoById(TenantId tenantId, UUID deviceId) { + return DaoUtil.getData(deviceRepository.findDeviceInfoById(deviceId)); + } + + @Override + public PageData findDevicesByTenantId(UUID tenantId, PageLink pageLink) { if (StringUtils.isEmpty(pageLink.getTextSearch())) { - return DaoUtil.convertDataList( + return DaoUtil.toPageData( deviceRepository.findByTenantId( - fromTimeUUID(tenantId), - pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()), - PageRequest.of(0, pageLink.getLimit()))); + tenantId, + DaoUtil.toPageable(pageLink))); } else { - return DaoUtil.convertDataList( + return DaoUtil.toPageData( deviceRepository.findByTenantId( - fromTimeUUID(tenantId), + tenantId, Objects.toString(pageLink.getTextSearch(), ""), - pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()), - PageRequest.of(0, pageLink.getLimit()))); + DaoUtil.toPageable(pageLink))); } } + @Override + public PageData findDeviceInfosByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.toPageData( + deviceRepository.findDeviceInfosByTenantId( + tenantId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap))); + } + @Override public ListenableFuture> findDevicesByTenantIdAndIdsAsync(UUID tenantId, List deviceIds) { - return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findDevicesByTenantIdAndIdIn(UUIDConverter.fromTimeUUID(tenantId), fromTimeUUIDs(deviceIds)))); + return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findDevicesByTenantIdAndIdIn(tenantId, deviceIds))); } @Override - public List findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink) { - return DaoUtil.convertDataList( + public PageData findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink) { + return DaoUtil.toPageData( deviceRepository.findByTenantIdAndCustomerId( - fromTimeUUID(tenantId), - fromTimeUUID(customerId), + tenantId, + customerId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findDevicesByTenantIdAndProfileId(UUID tenantId, UUID profileId, PageLink pageLink) { + return DaoUtil.toPageData( + deviceRepository.findByTenantIdAndProfileId( + tenantId, + profileId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findDeviceInfosByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink) { + return DaoUtil.toPageData( + deviceRepository.findDeviceInfosByTenantIdAndCustomerId( + tenantId, + customerId, Objects.toString(pageLink.getTextSearch(), ""), - pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()), - PageRequest.of(0, pageLink.getLimit()))); + DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap))); } @Override public ListenableFuture> findDevicesByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List deviceIds) { return service.submit(() -> DaoUtil.convertDataList( - deviceRepository.findDevicesByTenantIdAndCustomerIdAndIdIn(fromTimeUUID(tenantId), fromTimeUUID(customerId), fromTimeUUIDs(deviceIds)))); + deviceRepository.findDevicesByTenantIdAndCustomerIdAndIdIn(tenantId, customerId, deviceIds))); } @Override public Optional findDeviceByTenantIdAndName(UUID tenantId, String name) { - Device device = DaoUtil.getData(deviceRepository.findByTenantIdAndName(fromTimeUUID(tenantId), name)); + Device device = DaoUtil.getData(deviceRepository.findByTenantIdAndName(tenantId, name)); return Optional.ofNullable(device); } @Override - public List findDevicesByTenantIdAndType(UUID tenantId, String type, TextPageLink pageLink) { - return DaoUtil.convertDataList( + public PageData findDevicesByTenantIdAndType(UUID tenantId, String type, PageLink pageLink) { + return DaoUtil.toPageData( deviceRepository.findByTenantIdAndType( - fromTimeUUID(tenantId), + tenantId, + type, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findDeviceInfosByTenantIdAndType(UUID tenantId, String type, PageLink pageLink) { + return DaoUtil.toPageData( + deviceRepository.findDeviceInfosByTenantIdAndType( + tenantId, type, Objects.toString(pageLink.getTextSearch(), ""), - pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()), - PageRequest.of(0, pageLink.getLimit()))); + DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap))); + } + + @Override + public PageData findDeviceInfosByTenantIdAndDeviceProfileId(UUID tenantId, UUID deviceProfileId, PageLink pageLink) { + return DaoUtil.toPageData( + deviceRepository.findDeviceInfosByTenantIdAndDeviceProfileId( + tenantId, + deviceProfileId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap))); } @Override - public List findDevicesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, TextPageLink pageLink) { - return DaoUtil.convertDataList( + public PageData findDevicesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink) { + return DaoUtil.toPageData( deviceRepository.findByTenantIdAndCustomerIdAndType( - fromTimeUUID(tenantId), - fromTimeUUID(customerId), + tenantId, + customerId, + type, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findDeviceInfosByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink) { + return DaoUtil.toPageData( + deviceRepository.findDeviceInfosByTenantIdAndCustomerIdAndType( + tenantId, + customerId, type, Objects.toString(pageLink.getTextSearch(), ""), - pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()), - PageRequest.of(0, pageLink.getLimit()))); + DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap))); + } + + @Override + public PageData findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(UUID tenantId, UUID customerId, UUID deviceProfileId, PageLink pageLink) { + return DaoUtil.toPageData( + deviceRepository.findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId( + tenantId, + customerId, + deviceProfileId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap))); } @Override public ListenableFuture> findTenantDeviceTypesAsync(UUID tenantId) { - return service.submit(() -> convertTenantDeviceTypesToDto(tenantId, deviceRepository.findTenantDeviceTypes(fromTimeUUID(tenantId)))); + return service.submit(() -> convertTenantDeviceTypesToDto(tenantId, deviceRepository.findTenantDeviceTypes(tenantId))); } @Override public Device findDeviceByTenantIdAndId(TenantId tenantId, UUID id) { - return DaoUtil.getData(deviceRepository.findByTenantIdAndId(fromTimeUUID(tenantId.getId()), fromTimeUUID(id))); + return DaoUtil.getData(deviceRepository.findByTenantIdAndId(tenantId.getId(), id)); } @Override public ListenableFuture findDeviceByTenantIdAndIdAsync(TenantId tenantId, UUID id) { - return service.submit(() -> DaoUtil.getData(deviceRepository.findByTenantIdAndId(fromTimeUUID(tenantId.getId()), fromTimeUUID(id)))); + return service.submit(() -> DaoUtil.getData(deviceRepository.findByTenantIdAndId(tenantId.getId(), id))); + } + + @Override + public Long countDevicesByDeviceProfileId(TenantId tenantId, UUID deviceProfileId) { + return deviceRepository.countByDeviceProfileId(deviceProfileId); + } + + @Override + public Long countDevicesByTenantId(TenantId tenantId) { + return deviceRepository.countByTenantId(tenantId.getId()); } private List convertTenantDeviceTypesToDto(UUID tenantId, List types) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java new file mode 100644 index 0000000000..824d7c93bb --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java @@ -0,0 +1,103 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.device; + +import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileInfo; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.device.DeviceProfileDao; +import org.thingsboard.server.dao.model.sql.DeviceProfileEntity; +import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; + +import java.util.Objects; +import java.util.UUID; + +@Component +public class JpaDeviceProfileDao extends JpaAbstractSearchTextDao implements DeviceProfileDao { + + @Autowired + private DeviceProfileRepository deviceProfileRepository; + + @Override + protected Class getEntityClass() { + return DeviceProfileEntity.class; + } + + @Override + protected CrudRepository getCrudRepository() { + return deviceProfileRepository; + } + + @Override + public DeviceProfileInfo findDeviceProfileInfoById(TenantId tenantId, UUID deviceProfileId) { + return deviceProfileRepository.findDeviceProfileInfoById(deviceProfileId); + } + + @Override + public PageData findDeviceProfiles(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData( + deviceProfileRepository.findDeviceProfiles( + tenantId.getId(), + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findDeviceProfileInfos(TenantId tenantId, PageLink pageLink, String transportType) { + if (StringUtils.isNotEmpty(transportType)) { + return DaoUtil.pageToPageData( + deviceProfileRepository.findDeviceProfileInfos( + tenantId.getId(), + Objects.toString(pageLink.getTextSearch(), ""), + DeviceTransportType.valueOf(transportType), + DaoUtil.toPageable(pageLink))); + } else { + return DaoUtil.pageToPageData( + deviceProfileRepository.findDeviceProfileInfos( + tenantId.getId(), + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + } + + @Override + public DeviceProfile findDefaultDeviceProfile(TenantId tenantId) { + return DaoUtil.getData(deviceProfileRepository.findByDefaultTrueAndTenantId(tenantId.getId())); + } + + @Override + public DeviceProfileInfo findDefaultDeviceProfileInfo(TenantId tenantId) { + return deviceProfileRepository.findDefaultDeviceProfileInfo(tenantId.getId()); + } + + @Override + public DeviceProfile findByProvisionDeviceKey(String provisionDeviceKey) { + return DaoUtil.getData(deviceProfileRepository.findByProvisionDeviceKey(provisionDeviceKey)); + } + + @Override + public DeviceProfile findByName(TenantId tenantId, String profileName) { + return DaoUtil.getData(deviceProfileRepository.findByTenantIdAndName(tenantId.getId(), profileName)); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java index bc246e7dcc..0c7c55426e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java @@ -15,65 +15,108 @@ */ package org.thingsboard.server.dao.sql.entityview; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; import org.thingsboard.server.dao.model.sql.EntityViewEntity; -import org.thingsboard.server.dao.util.SqlDao; +import org.thingsboard.server.dao.model.sql.EntityViewInfoEntity; import java.util.List; +import java.util.UUID; /** * Created by Victor Basanets on 8/31/2017. */ -@SqlDao -public interface EntityViewRepository extends CrudRepository { +public interface EntityViewRepository extends PagingAndSortingRepository { + + @Query("SELECT new org.thingsboard.server.dao.model.sql.EntityViewInfoEntity(e, c.title, c.additionalInfo) " + + "FROM EntityViewEntity e " + + "LEFT JOIN CustomerEntity c on c.id = e.customerId " + + "WHERE e.id = :entityViewId") + EntityViewInfoEntity findEntityViewInfoById(@Param("entityViewId") UUID entityViewId); @Query("SELECT e FROM EntityViewEntity e WHERE e.tenantId = :tenantId " + - "AND LOWER(e.searchText) LIKE LOWER(CONCAT(:textSearch, '%')) " + - "AND e.id > :idOffset ORDER BY e.id") - List findByTenantId(@Param("tenantId") String tenantId, + "AND LOWER(e.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, - @Param("idOffset") String idOffset, Pageable pageable); + @Query("SELECT new org.thingsboard.server.dao.model.sql.EntityViewInfoEntity(e, c.title, c.additionalInfo) " + + "FROM EntityViewEntity e " + + "LEFT JOIN CustomerEntity c on c.id = e.customerId " + + "WHERE e.tenantId = :tenantId " + + "AND LOWER(e.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findEntityViewInfosByTenantId(@Param("tenantId") UUID tenantId, + @Param("textSearch") String textSearch, + Pageable pageable); + @Query("SELECT e FROM EntityViewEntity e WHERE e.tenantId = :tenantId " + "AND e.type = :type " + - "AND LOWER(e.searchText) LIKE LOWER(CONCAT(:textSearch, '%')) " + - "AND e.id > :idOffset ORDER BY e.id") - List findByTenantIdAndType(@Param("tenantId") String tenantId, + "AND LOWER(e.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findByTenantIdAndType(@Param("tenantId") UUID tenantId, @Param("type") String type, @Param("textSearch") String textSearch, - @Param("idOffset") String idOffset, Pageable pageable); + @Query("SELECT new org.thingsboard.server.dao.model.sql.EntityViewInfoEntity(e, c.title, c.additionalInfo) " + + "FROM EntityViewEntity e " + + "LEFT JOIN CustomerEntity c on c.id = e.customerId " + + "WHERE e.tenantId = :tenantId " + + "AND e.type = :type " + + "AND LOWER(e.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findEntityViewInfosByTenantIdAndType(@Param("tenantId") UUID tenantId, + @Param("type") String type, + @Param("textSearch") String textSearch, + Pageable pageable); + @Query("SELECT e FROM EntityViewEntity e WHERE e.tenantId = :tenantId " + "AND e.customerId = :customerId " + - "AND LOWER(e.searchText) LIKE LOWER(CONCAT(:searchText, '%')) " + - "AND e.id > :idOffset ORDER BY e.id") - List findByTenantIdAndCustomerId(@Param("tenantId") String tenantId, - @Param("customerId") String customerId, + "AND LOWER(e.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + Page findByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, + @Param("customerId") UUID customerId, @Param("searchText") String searchText, - @Param("idOffset") String idOffset, Pageable pageable); + @Query("SELECT new org.thingsboard.server.dao.model.sql.EntityViewInfoEntity(e, c.title, c.additionalInfo) " + + "FROM EntityViewEntity e " + + "LEFT JOIN CustomerEntity c on c.id = e.customerId " + + "WHERE e.tenantId = :tenantId " + + "AND e.customerId = :customerId " + + "AND LOWER(e.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + Page findEntityViewInfosByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, + @Param("customerId") UUID customerId, + @Param("searchText") String searchText, + Pageable pageable); + @Query("SELECT e FROM EntityViewEntity e WHERE e.tenantId = :tenantId " + "AND e.customerId = :customerId " + "AND e.type = :type " + - "AND LOWER(e.searchText) LIKE LOWER(CONCAT(:searchText, '%')) " + - "AND e.id > :idOffset ORDER BY e.id") - List findByTenantIdAndCustomerIdAndType(@Param("tenantId") String tenantId, - @Param("customerId") String customerId, + "AND LOWER(e.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + Page findByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId, + @Param("customerId") UUID customerId, @Param("type") String type, @Param("searchText") String searchText, - @Param("idOffset") String idOffset, Pageable pageable); - EntityViewEntity findByTenantIdAndName(String tenantId, String name); + @Query("SELECT new org.thingsboard.server.dao.model.sql.EntityViewInfoEntity(e, c.title, c.additionalInfo) " + + "FROM EntityViewEntity e " + + "LEFT JOIN CustomerEntity c on c.id = e.customerId " + + "WHERE e.tenantId = :tenantId " + + "AND e.customerId = :customerId " + + "AND e.type = :type " + + "AND LOWER(e.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findEntityViewInfosByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId, + @Param("customerId") UUID customerId, + @Param("type") String type, + @Param("textSearch") String textSearch, + Pageable pageable); + + EntityViewEntity findByTenantIdAndName(UUID tenantId, String name); - List findAllByTenantIdAndEntityId(String tenantId, String entityId); + List findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId); @Query("SELECT DISTINCT ev.type FROM EntityViewEntity ev WHERE ev.tenantId = :tenantId") - List findTenantEntityViewTypes(@Param("tenantId") String tenantId); + List findTenantEntityViewTypes(@Param("tenantId") UUID tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java index 385a901dc9..0e42b579fd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java @@ -17,20 +17,20 @@ package org.thingsboard.server.dao.sql.entityview; import com.google.common.util.concurrent.ListenableFuture; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.PageRequest; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; -import org.thingsboard.server.common.data.UUIDConverter; +import org.thingsboard.server.common.data.EntityViewInfo; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.entityview.EntityViewDao; import org.thingsboard.server.dao.model.sql.EntityViewEntity; +import org.thingsboard.server.dao.model.sql.EntityViewInfoEntity; import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; -import org.thingsboard.server.dao.util.SqlDao; import java.util.ArrayList; import java.util.Collections; @@ -39,14 +39,10 @@ import java.util.Objects; import java.util.Optional; import java.util.UUID; -import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID; -import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID_STR; - /** * Created by Victor Basanets on 8/31/2017. */ @Component -@SqlDao public class JpaEntityViewDao extends JpaAbstractSearchTextDao implements EntityViewDao { @@ -59,73 +55,114 @@ public class JpaEntityViewDao extends JpaAbstractSearchTextDao getCrudRepository() { + protected CrudRepository getCrudRepository() { return entityViewRepository; } @Override - public List findEntityViewsByTenantId(UUID tenantId, TextPageLink pageLink) { - return DaoUtil.convertDataList( + public EntityViewInfo findEntityViewInfoById(TenantId tenantId, UUID entityViewId) { + return DaoUtil.getData(entityViewRepository.findEntityViewInfoById(entityViewId)); + } + + @Override + public PageData findEntityViewsByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.toPageData( entityViewRepository.findByTenantId( - fromTimeUUID(tenantId), + tenantId, Objects.toString(pageLink.getTextSearch(), ""), - pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()), - PageRequest.of(0, pageLink.getLimit()))); + DaoUtil.toPageable(pageLink))); } @Override - public List findEntityViewsByTenantIdAndType(UUID tenantId, String type, TextPageLink pageLink) { - return DaoUtil.convertDataList( + public PageData findEntityViewInfosByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.toPageData( + entityViewRepository.findEntityViewInfosByTenantId( + tenantId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink, EntityViewInfoEntity.entityViewInfoColumnMap))); + } + + @Override + public PageData findEntityViewsByTenantIdAndType(UUID tenantId, String type, PageLink pageLink) { + return DaoUtil.toPageData( entityViewRepository.findByTenantIdAndType( - fromTimeUUID(tenantId), + tenantId, + type, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findEntityViewInfosByTenantIdAndType(UUID tenantId, String type, PageLink pageLink) { + return DaoUtil.toPageData( + entityViewRepository.findEntityViewInfosByTenantIdAndType( + tenantId, type, Objects.toString(pageLink.getTextSearch(), ""), - pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()), - PageRequest.of(0, pageLink.getLimit()))); + DaoUtil.toPageable(pageLink, EntityViewInfoEntity.entityViewInfoColumnMap))); } @Override public Optional findEntityViewByTenantIdAndName(UUID tenantId, String name) { return Optional.ofNullable( - DaoUtil.getData(entityViewRepository.findByTenantIdAndName(fromTimeUUID(tenantId), name))); + DaoUtil.getData(entityViewRepository.findByTenantIdAndName(tenantId, name))); } @Override - public List findEntityViewsByTenantIdAndCustomerId(UUID tenantId, - UUID customerId, - TextPageLink pageLink) { - return DaoUtil.convertDataList( + public PageData findEntityViewsByTenantIdAndCustomerId(UUID tenantId, + UUID customerId, + PageLink pageLink) { + return DaoUtil.toPageData( entityViewRepository.findByTenantIdAndCustomerId( - fromTimeUUID(tenantId), - fromTimeUUID(customerId), + tenantId, + customerId, Objects.toString(pageLink.getTextSearch(), ""), - pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()), - PageRequest.of(0, pageLink.getLimit()) + DaoUtil.toPageable(pageLink) )); } @Override - public List findEntityViewsByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, TextPageLink pageLink) { - return DaoUtil.convertDataList( + public PageData findEntityViewInfosByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink) { + return DaoUtil.toPageData( + entityViewRepository.findEntityViewInfosByTenantIdAndCustomerId( + tenantId, + customerId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink, EntityViewInfoEntity.entityViewInfoColumnMap))); + } + + @Override + public PageData findEntityViewsByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink) { + return DaoUtil.toPageData( entityViewRepository.findByTenantIdAndCustomerIdAndType( - fromTimeUUID(tenantId), - fromTimeUUID(customerId), + tenantId, + customerId, type, Objects.toString(pageLink.getTextSearch(), ""), - pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()), - PageRequest.of(0, pageLink.getLimit()) + DaoUtil.toPageable(pageLink) )); } + @Override + public PageData findEntityViewInfosByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink) { + return DaoUtil.toPageData( + entityViewRepository.findEntityViewInfosByTenantIdAndCustomerIdAndType( + tenantId, + customerId, + type, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink, EntityViewInfoEntity.entityViewInfoColumnMap))); + } + @Override public ListenableFuture> findEntityViewsByTenantIdAndEntityIdAsync(UUID tenantId, UUID entityId) { return service.submit(() -> DaoUtil.convertDataList( - entityViewRepository.findAllByTenantIdAndEntityId(UUIDConverter.fromTimeUUID(tenantId), UUIDConverter.fromTimeUUID(entityId)))); + entityViewRepository.findAllByTenantIdAndEntityId(tenantId, entityId))); } @Override public ListenableFuture> findTenantEntityViewTypesAsync(UUID tenantId) { - return service.submit(() -> convertTenantEntityViewTypesToDto(tenantId, entityViewRepository.findTenantEntityViewTypes(fromTimeUUID(tenantId)))); + return service.submit(() -> convertTenantEntityViewTypesToDto(tenantId, entityViewRepository.findTenantEntityViewTypes(tenantId))); } private List convertTenantEntityViewTypesToDto(UUID tenantId, List types) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/AbstractEventInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/AbstractEventInsertRepository.java index 8a341e0f81..81f931fc58 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/AbstractEventInsertRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/AbstractEventInsertRepository.java @@ -23,7 +23,6 @@ import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.DefaultTransactionDefinition; -import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.dao.model.sql.EventEntity; import javax.persistence.EntityManager; @@ -69,7 +68,8 @@ public abstract class AbstractEventInsertRepository implements EventInsertReposi protected Query getQuery(EventEntity entity, String query) { return entityManager.createNativeQuery(query, EventEntity.class) - .setParameter("id", UUIDConverter.fromTimeUUID(entity.getUuid())) + .setParameter("id", entity.getUuid()) + .setParameter("created_time", entity.getCreatedTime()) .setParameter("body", entity.getBody().toString()) .setParameter("entity_id", entity.getEntityId()) .setParameter("entity_type", entity.getEntityType().name()) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java index 2ac8789c9b..5696bfbf50 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java @@ -15,40 +15,69 @@ */ package org.thingsboard.server.dao.sql.event; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.dao.model.sql.EventEntity; -import org.thingsboard.server.dao.util.SqlDao; import java.util.List; +import java.util.UUID; /** * Created by Valerii Sosliuk on 5/3/2017. */ -@SqlDao -public interface EventRepository extends CrudRepository, JpaSpecificationExecutor { +public interface EventRepository extends PagingAndSortingRepository { - EventEntity findByTenantIdAndEntityTypeAndEntityIdAndEventTypeAndEventUid(String tenantId, + EventEntity findByTenantIdAndEntityTypeAndEntityIdAndEventTypeAndEventUid(UUID tenantId, EntityType entityType, - String entityId, + UUID entityId, String eventType, String eventUid); - EventEntity findByTenantIdAndEntityTypeAndEntityId(String tenantId, + EventEntity findByTenantIdAndEntityTypeAndEntityId(UUID tenantId, EntityType entityType, - String entityId); + UUID entityId); @Query("SELECT e FROM EventEntity e WHERE e.tenantId = :tenantId AND e.entityType = :entityType " + "AND e.entityId = :entityId AND e.eventType = :eventType ORDER BY e.eventType DESC, e.id DESC") List findLatestByTenantIdAndEntityTypeAndEntityIdAndEventType( - @Param("tenantId") String tenantId, + @Param("tenantId") UUID tenantId, @Param("entityType") EntityType entityType, - @Param("entityId") String entityId, + @Param("entityId") UUID entityId, @Param("eventType") String eventType, Pageable pageable); + @Query("SELECT e FROM EventEntity e WHERE " + + "e.tenantId = :tenantId " + + "AND e.entityType = :entityType AND e.entityId = :entityId " + + "AND (:startTime IS NULL OR e.createdTime >= :startTime) " + + "AND (:endTime IS NULL OR e.createdTime <= :endTime) " + + "AND LOWER(e.eventType) LIKE LOWER(CONCAT(:textSearch, '%'))" + ) + Page findEventsByTenantIdAndEntityId(@Param("tenantId") UUID tenantId, + @Param("entityType") EntityType entityType, + @Param("entityId") UUID entityId, + @Param("textSearch") String textSearch, + @Param("startTime") Long startTime, + @Param("endTime") Long endTime, + Pageable pageable); + + @Query("SELECT e FROM EventEntity e WHERE " + + "e.tenantId = :tenantId " + + "AND e.entityType = :entityType AND e.entityId = :entityId " + + "AND e.eventType = :eventType " + + "AND (:startTime IS NULL OR e.createdTime >= :startTime) " + + "AND (:endTime IS NULL OR e.createdTime <= :endTime)" + ) + Page findEventsByTenantIdAndEntityIdAndEventType(@Param("tenantId") UUID tenantId, + @Param("entityType") EntityType entityType, + @Param("entityId") UUID entityId, + @Param("eventType") String eventType, + @Param("startTime") Long startTime, + @Param("endTime") Long endTime, + Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/HsqlEventInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/HsqlEventInsertRepository.java index f5a00fe7f7..722c370ac9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/HsqlEventInsertRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/HsqlEventInsertRepository.java @@ -16,12 +16,11 @@ package org.thingsboard.server.dao.sql.event; import org.springframework.stereotype.Repository; -import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.dao.model.sql.EventEntity; import org.thingsboard.server.dao.util.HsqlDao; -import org.thingsboard.server.dao.util.SqlDao; -@SqlDao +import javax.persistence.Query; + @HsqlDao @Repository public class HsqlEventInsertRepository extends AbstractEventInsertRepository { @@ -40,11 +39,25 @@ public class HsqlEventInsertRepository extends AbstractEventInsertRepository { @Override protected EventEntity doProcessSaveOrUpdate(EventEntity entity, String query) { getQuery(entity, query).executeUpdate(); - return entityManager.find(EventEntity.class, UUIDConverter.fromTimeUUID(entity.getUuid())); + return entityManager.find(EventEntity.class, entity.getUuid()); + } + + protected Query getQuery(EventEntity entity, String query) { + return entityManager.createNativeQuery(query, EventEntity.class) + .setParameter("id", entity.getUuid().toString()) + .setParameter("created_time", entity.getCreatedTime()) + .setParameter("body", entity.getBody().toString()) + .setParameter("entity_id", entity.getEntityId().toString()) + .setParameter("entity_type", entity.getEntityType().name()) + .setParameter("event_type", entity.getEventType()) + .setParameter("event_uid", entity.getEventUid()) + .setParameter("tenant_id", entity.getTenantId().toString()) + .setParameter("ts", entity.getTs()); } private static String getInsertString(String conflictStatement) { - return "MERGE INTO event USING (VALUES :id, :body, :entity_id, :entity_type, :event_type, :event_uid, :tenant_id, :ts) I (id, body, entity_id, entity_type, event_type, event_uid, tenant_id, ts) ON " + conflictStatement + " WHEN MATCHED THEN UPDATE SET event.id = I.id, event.body = I.body, event.entity_id = I.entity_id, event.entity_type = I.entity_type, event.event_type = I.event_type, event.event_uid = I.event_uid, event.tenant_id = I.tenant_id, event.ts = I.ts" + - " WHEN NOT MATCHED THEN INSERT (id, body, entity_id, entity_type, event_type, event_uid, tenant_id, ts) VALUES (I.id, I.body, I.entity_id, I.entity_type, I.event_type, I.event_uid, I.tenant_id, I.ts)"; + return "MERGE INTO event USING (VALUES UUID(:id), :created_time, :body, UUID(:entity_id), :entity_type, :event_type, :event_uid, UUID(:tenant_id), :ts) I (id, created_time, body, entity_id, entity_type, event_type, event_uid, tenant_id, ts) ON " + conflictStatement + + " WHEN MATCHED THEN UPDATE SET event.id = I.id, event.created_time = I.created_time, event.body = I.body, event.entity_id = I.entity_id, event.entity_type = I.entity_type, event.event_type = I.event_type, event.event_uid = I.event_uid, event.tenant_id = I.tenant_id, event.ts = I.ts" + + " WHEN NOT MATCHED THEN INSERT (id, created_time, body, entity_id, entity_type, event_type, event_uid, tenant_id, ts) VALUES (I.id, I.created_time, I.body, I.entity_id, I.entity_type, I.event_type, I.event_uid, I.tenant_id, I.ts)"; } } \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java index b922c949b2..20f49ac8d8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java @@ -15,36 +15,30 @@ */ package org.thingsboard.server.dao.sql.event; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.jpa.domain.Specification; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.Event; -import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EventId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.event.EventDao; import org.thingsboard.server.dao.model.sql.EventEntity; -import org.thingsboard.server.dao.sql.JpaAbstractSearchTimeDao; -import org.thingsboard.server.dao.util.SqlDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; -import javax.persistence.criteria.Predicate; -import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.UUID; -import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; /** @@ -52,8 +46,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; */ @Slf4j @Component -@SqlDao -public class JpaBaseEventDao extends JpaAbstractSearchTimeDao implements EventDao { +public class JpaBaseEventDao extends JpaAbstractDao implements EventDao { private final UUID systemTenantId = NULL_UUID; @@ -69,7 +62,7 @@ public class JpaBaseEventDao extends JpaAbstractSearchTimeDao getCrudRepository() { + protected CrudRepository getCrudRepository() { return eventRepository; } @@ -77,7 +70,16 @@ public class JpaBaseEventDao extends JpaAbstractSearchTimeDao saveAsync(Event event) { log.debug("Save event [{}] ", event); if (event.getId() == null) { - event.setId(new EventId(UUIDs.timeBased())); + UUID timeBased = Uuids.timeBased(); + event.setId(new EventId(timeBased)); + event.setCreatedTime(Uuids.unixTimestamp(timeBased)); + } else if (event.getCreatedTime() == 0L) { + UUID eventId = event.getId().getId(); + if (eventId.version() == 1) { + event.setCreatedTime(Uuids.unixTimestamp(eventId)); + } else { + event.setCreatedTime(System.currentTimeMillis()); + } } if (StringUtils.isEmpty(event.getUid())) { event.setUid(event.getId().toString()); @@ -105,29 +116,43 @@ public class JpaBaseEventDao extends JpaAbstractSearchTimeDao findEvents(UUID tenantId, EntityId entityId, TimePageLink pageLink) { - return findEvents(tenantId, entityId, null, pageLink); + public PageData findEvents(UUID tenantId, EntityId entityId, TimePageLink pageLink) { + return DaoUtil.toPageData( + eventRepository + .findEventsByTenantIdAndEntityId( + tenantId, + entityId.getEntityType(), + entityId.getId(), + Objects.toString(pageLink.getTextSearch(), ""), + pageLink.getStartTime(), + pageLink.getEndTime(), + DaoUtil.toPageable(pageLink))); } @Override - public List findEvents(UUID tenantId, EntityId entityId, String eventType, TimePageLink pageLink) { - Specification timeSearchSpec = JpaAbstractSearchTimeDao.getTimeSearchPageSpec(pageLink, "id"); - Specification fieldsSpec = getEntityFieldsSpec(tenantId, entityId, eventType); - Sort.Direction sortDirection = pageLink.isAscOrder() ? Sort.Direction.ASC : Sort.Direction.DESC; - Pageable pageable = PageRequest.of(0, pageLink.getLimit(), sortDirection, ID_PROPERTY); - return DaoUtil.convertDataList(eventRepository.findAll(Specification.where(timeSearchSpec).and(fieldsSpec), pageable).getContent()); + public PageData findEvents(UUID tenantId, EntityId entityId, String eventType, TimePageLink pageLink) { + return DaoUtil.toPageData( + eventRepository + .findEventsByTenantIdAndEntityIdAndEventType( + tenantId, + entityId.getEntityType(), + entityId.getId(), + eventType, + pageLink.getStartTime(), + pageLink.getEndTime(), + DaoUtil.toPageable(pageLink))); } @Override public List findLatestEvents(UUID tenantId, EntityId entityId, String eventType, int limit) { List latest = eventRepository.findLatestByTenantIdAndEntityTypeAndEntityIdAndEventType( - UUIDConverter.fromTimeUUID(tenantId), + tenantId, entityId.getEntityType(), - UUIDConverter.fromTimeUUID(entityId.getId()), + entityId.getId(), eventType, PageRequest.of(0, limit)); return DaoUtil.convertDataList(latest); @@ -137,10 +162,10 @@ public class JpaBaseEventDao extends JpaAbstractSearchTimeDao getEntityFieldsSpec(UUID tenantId, EntityId entityId, String eventType) { - return (root, criteriaQuery, criteriaBuilder) -> { - List predicates = new ArrayList<>(); - if (tenantId != null) { - Predicate tenantIdPredicate = criteriaBuilder.equal(root.get("tenantId"), UUIDConverter.fromTimeUUID(tenantId)); - predicates.add(tenantIdPredicate); - } - if (entityId != null) { - Predicate entityTypePredicate = criteriaBuilder.equal(root.get("entityType"), entityId.getEntityType()); - predicates.add(entityTypePredicate); - Predicate entityIdPredicate = criteriaBuilder.equal(root.get("entityId"), UUIDConverter.fromTimeUUID(entityId.getId())); - predicates.add(entityIdPredicate); - } - if (eventType != null) { - Predicate eventTypePredicate = criteriaBuilder.equal(root.get("eventType"), eventType); - predicates.add(eventTypePredicate); - } - return criteriaBuilder.and(predicates.toArray(new Predicate[]{})); - }; - } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/PsqlEventInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/PsqlEventInsertRepository.java index 937262241a..1f401e5eb2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/PsqlEventInsertRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/PsqlEventInsertRepository.java @@ -19,19 +19,17 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Repository; import org.thingsboard.server.dao.model.sql.EventEntity; import org.thingsboard.server.dao.util.PsqlDao; -import org.thingsboard.server.dao.util.SqlDao; @Slf4j -@SqlDao @PsqlDao @Repository public class PsqlEventInsertRepository extends AbstractEventInsertRepository { private static final String P_KEY_CONFLICT_STATEMENT = "(id)"; - private static final String UNQ_KEY_CONFLICT_STATEMENT = "(tenant_id, entity_type, entity_id, event_type, event_uid)"; + private static final String UNQ_KEY_CONFLICT_STATEMENT = "(tenant_id, created_time, entity_type, entity_id, event_type, event_uid)"; private static final String UPDATE_P_KEY_STATEMENT = "id = :id"; - private static final String UPDATE_UNQ_KEY_STATEMENT = "tenant_id = :tenant_id, entity_type = :entity_type, entity_id = :entity_id, event_type = :event_type, event_uid = :event_uid"; + private static final String UPDATE_UNQ_KEY_STATEMENT = "created_time = :created_time, tenant_id = :tenant_id, entity_type = :entity_type, entity_id = :entity_id, event_type = :event_type, event_uid = :event_uid"; private static final String INSERT_OR_UPDATE_ON_P_KEY_CONFLICT = getInsertOrUpdateString(P_KEY_CONFLICT_STATEMENT, UPDATE_UNQ_KEY_STATEMENT); private static final String INSERT_OR_UPDATE_ON_UNQ_KEY_CONFLICT = getInsertOrUpdateString(UNQ_KEY_CONFLICT_STATEMENT, UPDATE_P_KEY_STATEMENT); @@ -48,6 +46,8 @@ public class PsqlEventInsertRepository extends AbstractEventInsertRepository { } private static String getInsertOrUpdateString(String eventKeyStatement, String updateKeyStatement) { - return "INSERT INTO event (id, body, entity_id, entity_type, event_type, event_uid, tenant_id, ts) VALUES (:id, :body, :entity_id, :entity_type, :event_type, :event_uid, :tenant_id, :ts) ON CONFLICT " + eventKeyStatement + " DO UPDATE SET body = :body, ts = :ts," + updateKeyStatement + " returning *"; + return "INSERT INTO event (id, created_time, body, entity_id, entity_type, event_type, event_uid, tenant_id, ts) " + + "VALUES (:id, :created_time, :body, :entity_id, :entity_type, :event_type, :event_uid, :tenant_id, :ts) " + + "ON CONFLICT " + eventKeyStatement + " DO UPDATE SET body = :body, ts = :ts," + updateKeyStatement + " returning *"; } } \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationDao.java new file mode 100644 index 0000000000..1fd901b4a3 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationDao.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.oauth2; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistration; +import org.thingsboard.server.dao.model.sql.OAuth2ClientRegistrationEntity; +import org.thingsboard.server.dao.oauth2.OAuth2ClientRegistrationDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; + +import java.util.UUID; + +@Component +@RequiredArgsConstructor +public class JpaOAuth2ClientRegistrationDao extends JpaAbstractDao implements OAuth2ClientRegistrationDao { + private final OAuth2ClientRegistrationRepository repository; + + @Override + protected Class getEntityClass() { + return OAuth2ClientRegistrationEntity.class; + } + + @Override + protected CrudRepository getCrudRepository() { + return repository; + } + + @Override + public void deleteAll() { + repository.deleteAll(); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationInfoDao.java new file mode 100644 index 0000000000..ec8651bc3a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationInfoDao.java @@ -0,0 +1,76 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.oauth2; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.oauth2.ExtendedOAuth2ClientRegistrationInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; +import org.thingsboard.server.common.data.oauth2.SchemeType; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.OAuth2ClientRegistrationInfoEntity; +import org.thingsboard.server.dao.oauth2.OAuth2ClientRegistrationInfoDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +@Component +@RequiredArgsConstructor +public class JpaOAuth2ClientRegistrationInfoDao extends JpaAbstractDao implements OAuth2ClientRegistrationInfoDao { + private final OAuth2ClientRegistrationInfoRepository repository; + + @Override + protected Class getEntityClass() { + return OAuth2ClientRegistrationInfoEntity.class; + } + + @Override + protected CrudRepository getCrudRepository() { + return repository; + } + + @Override + public List findAll() { + Iterable entities = repository.findAll(); + List result = new ArrayList<>(); + entities.forEach(entity -> { + result.add(DaoUtil.getData(entity)); + }); + return result; + } + + @Override + public List findAllExtended() { + return repository.findAllExtended().stream() + .map(DaoUtil::getData) + .collect(Collectors.toList()); + } + + @Override + public List findByDomainSchemesAndDomainName(List domainSchemes, String domainName) { + List entities = repository.findAllByDomainSchemesAndName(domainSchemes, domainName); + return entities.stream().map(DaoUtil::getData).collect(Collectors.toList()); + } + + @Override + public void deleteAll() { + repository.deleteAll(); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationTemplateDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationTemplateDao.java new file mode 100644 index 0000000000..4540a9b3d1 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationTemplateDao.java @@ -0,0 +1,60 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.oauth2; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.OAuth2ClientRegistrationTemplateEntity; +import org.thingsboard.server.dao.oauth2.OAuth2ClientRegistrationTemplateDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Component +@RequiredArgsConstructor +public class JpaOAuth2ClientRegistrationTemplateDao extends JpaAbstractDao implements OAuth2ClientRegistrationTemplateDao { + private final OAuth2ClientRegistrationTemplateRepository repository; + + @Override + protected Class getEntityClass() { + return OAuth2ClientRegistrationTemplateEntity.class; + } + + @Override + protected CrudRepository getCrudRepository() { + return repository; + } + + @Override + public Optional findByProviderId(String providerId) { + OAuth2ClientRegistrationTemplate oAuth2ClientRegistrationTemplate = DaoUtil.getData(repository.findByProviderId(providerId)); + return Optional.ofNullable(oAuth2ClientRegistrationTemplate); + } + + @Override + public List findAll() { + Iterable entities = repository.findAll(); + List result = new ArrayList<>(); + entities.forEach(entity -> result.add(DaoUtil.getData(entity))); + return result; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationInfoRepository.java new file mode 100644 index 0000000000..96094bd2fe --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationInfoRepository.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.oauth2; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.oauth2.SchemeType; +import org.thingsboard.server.dao.model.sql.ExtendedOAuth2ClientRegistrationInfoEntity; +import org.thingsboard.server.dao.model.sql.OAuth2ClientRegistrationInfoEntity; + +import java.util.List; +import java.util.UUID; + +public interface OAuth2ClientRegistrationInfoRepository extends CrudRepository { + @Query("SELECT new OAuth2ClientRegistrationInfoEntity(cr_info) " + + "FROM OAuth2ClientRegistrationInfoEntity cr_info " + + "LEFT JOIN OAuth2ClientRegistrationEntity cr on cr_info.id = cr.clientRegistrationInfoId " + + "WHERE cr.domainName = :domainName " + + "AND cr.domainScheme IN (:domainSchemes)") + List findAllByDomainSchemesAndName(@Param("domainSchemes") List domainSchemes, + @Param("domainName") String domainName); + + @Query("SELECT new org.thingsboard.server.dao.model.sql.ExtendedOAuth2ClientRegistrationInfoEntity(cr_info, cr.domainName, cr.domainScheme) " + + "FROM OAuth2ClientRegistrationInfoEntity cr_info " + + "LEFT JOIN OAuth2ClientRegistrationEntity cr on cr_info.id = cr.clientRegistrationInfoId ") + List findAllExtended(); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationRepository.java new file mode 100644 index 0000000000..438f73c7d1 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationRepository.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.oauth2; + +import org.springframework.data.repository.CrudRepository; +import org.thingsboard.server.dao.model.sql.OAuth2ClientRegistrationEntity; + +import java.util.UUID; + +public interface OAuth2ClientRegistrationRepository extends CrudRepository { +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationTemplateRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationTemplateRepository.java new file mode 100644 index 0000000000..cba85280d7 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationTemplateRepository.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.oauth2; + +import org.springframework.data.repository.CrudRepository; +import org.thingsboard.server.dao.model.sql.OAuth2ClientRegistrationTemplateEntity; + +import java.util.UUID; + +public interface OAuth2ClientRegistrationTemplateRepository extends CrudRepository { + + OAuth2ClientRegistrationTemplateEntity findByProviderId(String providerId); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java new file mode 100644 index 0000000000..0adbcb31b0 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java @@ -0,0 +1,106 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.query; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AlarmData; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.dao.model.ModelConstants; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Slf4j +public class AlarmDataAdapter { + + private final static ObjectMapper mapper = new ObjectMapper(); + + public static PageData createAlarmData(EntityDataPageLink pageLink, + List> rows, + int totalElements, Collection orderedEntityIds) { + Map entityIdMap = orderedEntityIds.stream().collect(Collectors.toMap(EntityId::getId, Function.identity())); + int totalPages = pageLink.getPageSize() > 0 ? (int) Math.ceil((float) totalElements / pageLink.getPageSize()) : 1; + int startIndex = pageLink.getPageSize() * pageLink.getPage(); + boolean hasNext = pageLink.getPageSize() > 0 && totalElements > startIndex + rows.size(); + List entitiesData = convertListToAlarmData(rows, entityIdMap); + return new PageData<>(entitiesData, totalPages, totalElements, hasNext); + } + + private static List convertListToAlarmData(List> result, Map entityIdMap) { + return result.stream().map(tmp -> toEntityData(tmp, entityIdMap)).collect(Collectors.toList()); + } + + private static AlarmData toEntityData(Map row, Map entityIdMap) { + Alarm alarm = new Alarm(); + alarm.setId(new AlarmId((UUID) row.get(ModelConstants.ID_PROPERTY))); + alarm.setCreatedTime((long) row.get(ModelConstants.CREATED_TIME_PROPERTY)); + alarm.setAckTs((long) row.get(ModelConstants.ALARM_ACK_TS_PROPERTY)); + alarm.setClearTs((long) row.get(ModelConstants.ALARM_CLEAR_TS_PROPERTY)); + alarm.setStartTs((long) row.get(ModelConstants.ALARM_START_TS_PROPERTY)); + alarm.setEndTs((long) row.get(ModelConstants.ALARM_END_TS_PROPERTY)); + Object additionalInfo = row.get(ModelConstants.ADDITIONAL_INFO_PROPERTY); + if (additionalInfo != null) { + try { + alarm.setDetails(mapper.readTree(additionalInfo.toString())); + } catch (JsonProcessingException e) { + log.warn("Failed to parse json: {}", row.get(ModelConstants.ADDITIONAL_INFO_PROPERTY), e); + } + } + EntityType originatorType = EntityType.values()[(int) row.get(ModelConstants.ALARM_ORIGINATOR_TYPE_PROPERTY)]; + UUID originatorId = (UUID) row.get(ModelConstants.ALARM_ORIGINATOR_ID_PROPERTY); + alarm.setOriginator(EntityIdFactory.getByTypeAndUuid(originatorType, originatorId)); + alarm.setPropagate((boolean) row.get(ModelConstants.ALARM_PROPAGATE_PROPERTY)); + alarm.setType(row.get(ModelConstants.ALARM_TYPE_PROPERTY).toString()); + alarm.setSeverity(AlarmSeverity.valueOf(row.get(ModelConstants.ALARM_SEVERITY_PROPERTY).toString())); + alarm.setStatus(AlarmStatus.valueOf(row.get(ModelConstants.ALARM_STATUS_PROPERTY).toString())); + alarm.setTenantId(new TenantId((UUID) row.get(ModelConstants.TENANT_ID_PROPERTY))); + if (row.get(ModelConstants.ALARM_PROPAGATE_RELATION_TYPES) != null) { + String propagateRelationTypes = row.get(ModelConstants.ALARM_PROPAGATE_RELATION_TYPES).toString(); + if (!StringUtils.isEmpty(propagateRelationTypes)) { + alarm.setPropagateRelationTypes(Arrays.asList(propagateRelationTypes.split(","))); + } else { + alarm.setPropagateRelationTypes(Collections.emptyList()); + } + } else { + alarm.setPropagateRelationTypes(Collections.emptyList()); + } + UUID entityUuid = (UUID) row.get(ModelConstants.ENTITY_ID_COLUMN); + EntityId entityId = entityIdMap.get(entityUuid); + Object originatorNameObj = row.get(ModelConstants.ALARM_ORIGINATOR_NAME_PROPERTY); + String originatorName = originatorNameObj != null ? originatorNameObj.toString() : null; + return new AlarmData(alarm, originatorName, entityId); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmQueryRepository.java new file mode 100644 index 0000000000..767b227acc --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmQueryRepository.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.query; + +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AlarmData; +import org.thingsboard.server.common.data.query.AlarmDataPageLink; +import org.thingsboard.server.common.data.query.AlarmDataQuery; + +import java.util.Collection; + +public interface AlarmQueryRepository { + + PageData findAlarmDataByQueryForEntities(TenantId tenantId, CustomerId customerId, + AlarmDataQuery query, Collection orderedEntityIds); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java new file mode 100644 index 0000000000..e3aa614bfe --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java @@ -0,0 +1,342 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.query; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AlarmData; +import org.thingsboard.server.common.data.query.AlarmDataPageLink; +import org.thingsboard.server.common.data.query.AlarmDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.dao.model.ModelConstants; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +@Repository +@Slf4j +public class DefaultAlarmQueryRepository implements AlarmQueryRepository { + + private static final Map alarmFieldColumnMap = new HashMap<>(); + + static { + alarmFieldColumnMap.put("createdTime", ModelConstants.CREATED_TIME_PROPERTY); + alarmFieldColumnMap.put("ackTs", ModelConstants.ALARM_ACK_TS_PROPERTY); + alarmFieldColumnMap.put("ackTime", ModelConstants.ALARM_ACK_TS_PROPERTY); + alarmFieldColumnMap.put("clearTs", ModelConstants.ALARM_CLEAR_TS_PROPERTY); + alarmFieldColumnMap.put("clearTime", ModelConstants.ALARM_CLEAR_TS_PROPERTY); + alarmFieldColumnMap.put("details", ModelConstants.ADDITIONAL_INFO_PROPERTY); + alarmFieldColumnMap.put("endTs", ModelConstants.ALARM_END_TS_PROPERTY); + alarmFieldColumnMap.put("endTime", ModelConstants.ALARM_END_TS_PROPERTY); + alarmFieldColumnMap.put("startTs", ModelConstants.ALARM_START_TS_PROPERTY); + alarmFieldColumnMap.put("startTime", ModelConstants.ALARM_START_TS_PROPERTY); + alarmFieldColumnMap.put("status", ModelConstants.ALARM_STATUS_PROPERTY); + alarmFieldColumnMap.put("type", ModelConstants.ALARM_TYPE_PROPERTY); + alarmFieldColumnMap.put("severity", ModelConstants.ALARM_SEVERITY_PROPERTY); + alarmFieldColumnMap.put("originator_id", ModelConstants.ALARM_ORIGINATOR_ID_PROPERTY); + alarmFieldColumnMap.put("originator_type", ModelConstants.ALARM_ORIGINATOR_TYPE_PROPERTY); + alarmFieldColumnMap.put("originator", "originator_name"); + } + + private static final String SELECT_ORIGINATOR_NAME = " COALESCE(CASE" + + " WHEN a.originator_type = " + EntityType.TENANT.ordinal() + + " THEN (select title from tenant where id = a.originator_id)" + + " WHEN a.originator_type = " + EntityType.CUSTOMER.ordinal() + + " THEN (select title from customer where id = a.originator_id)" + + " WHEN a.originator_type = " + EntityType.USER.ordinal() + + " THEN (select email from tb_user where id = a.originator_id)" + + " WHEN a.originator_type = " + EntityType.DASHBOARD.ordinal() + + " THEN (select title from dashboard where id = a.originator_id)" + + " WHEN a.originator_type = " + EntityType.ASSET.ordinal() + + " THEN (select name from asset where id = a.originator_id)" + + " WHEN a.originator_type = " + EntityType.DEVICE.ordinal() + + " THEN (select name from device where id = a.originator_id)" + + " WHEN a.originator_type = " + EntityType.ENTITY_VIEW.ordinal() + + " THEN (select name from entity_view where id = a.originator_id)" + + " END, 'Deleted') as originator_name"; + + private static final String FIELDS_SELECTION = "select a.id as id," + + " a.created_time as created_time," + + " a.ack_ts as ack_ts," + + " a.clear_ts as clear_ts," + + " a.additional_info as additional_info," + + " a.end_ts as end_ts," + + " a.originator_id as originator_id," + + " a.originator_type as originator_type," + + " a.propagate as propagate," + + " a.severity as severity," + + " a.start_ts as start_ts," + + " a.status as status, " + + " a.tenant_id as tenant_id, " + + " a.propagate_relation_types as propagate_relation_types, " + + " a.type as type," + SELECT_ORIGINATOR_NAME + ", "; + + private static final String JOIN_RELATIONS = "left join relation r on r.relation_type_group = 'ALARM' and r.relation_type = 'ANY' and a.id = r.to_id and r.from_id in (:entity_ids)"; + + protected final NamedParameterJdbcTemplate jdbcTemplate; + private final TransactionTemplate transactionTemplate; + + private final DefaultQueryLogComponent queryLog; + + public DefaultAlarmQueryRepository(NamedParameterJdbcTemplate jdbcTemplate, TransactionTemplate transactionTemplate, DefaultQueryLogComponent queryLog) { + this.jdbcTemplate = jdbcTemplate; + this.transactionTemplate = transactionTemplate; + this.queryLog = queryLog; + } + + @Override + public PageData findAlarmDataByQueryForEntities(TenantId tenantId, CustomerId customerId, + AlarmDataQuery query, Collection orderedEntityIds) { + return transactionTemplate.execute(status -> { + AlarmDataPageLink pageLink = query.getPageLink(); + QueryContext ctx = new QueryContext(new QuerySecurityContext(tenantId, customerId, EntityType.ALARM)); + ctx.addUuidListParameter("entity_ids", orderedEntityIds.stream().map(EntityId::getId).collect(Collectors.toList())); + + StringBuilder selectPart = new StringBuilder(FIELDS_SELECTION); + StringBuilder fromPart = new StringBuilder(" from alarm a "); + StringBuilder wherePart = new StringBuilder(" where "); + StringBuilder sortPart = new StringBuilder(" order by "); + boolean addAnd = false; + if (pageLink.isSearchPropagatedAlarms()) { + selectPart.append(" CASE WHEN r.from_id IS NULL THEN a.originator_id ELSE r.from_id END as entity_id "); + fromPart.append(JOIN_RELATIONS); + wherePart.append(buildPermissionsQuery(tenantId, customerId, ctx)); + addAnd = true; + } else { + selectPart.append(" a.originator_id as entity_id "); + } + EntityDataSortOrder sortOrder = pageLink.getSortOrder(); + if (sortOrder != null && sortOrder.getKey().getType().equals(EntityKeyType.ALARM_FIELD)) { + String sortOrderKey = sortOrder.getKey().getKey(); + sortPart.append(alarmFieldColumnMap.getOrDefault(sortOrderKey, sortOrderKey)) + .append(" ").append(sortOrder.getDirection().name()); + if (pageLink.isSearchPropagatedAlarms()) { + wherePart.append(" and (a.originator_id in (:entity_ids) or r.from_id IS NOT NULL)"); + } else { + addAndIfNeeded(wherePart, addAnd); + addAnd = true; + wherePart.append(" a.originator_id in (:entity_ids)"); + } + } else { + fromPart.append(" left join (select * from (VALUES"); + int entityIdIdx = 0; + int lastEntityIdIdx = orderedEntityIds.size() - 1; + for (EntityId entityId : orderedEntityIds) { + fromPart.append("(uuid('").append(entityId.getId().toString()).append("'), ").append(entityIdIdx).append(")"); + if (entityIdIdx != lastEntityIdIdx) { + fromPart.append(","); + } else { + fromPart.append(")"); + } + entityIdIdx++; + } + fromPart.append(" as e(id, priority)) e "); + if (pageLink.isSearchPropagatedAlarms()) { + fromPart.append("on (r.from_id IS NULL and a.originator_id = e.id) or (r.from_id IS NOT NULL and r.from_id = e.id)"); + } else { + fromPart.append("on a.originator_id = e.id"); + } + sortPart.append("e.priority"); + } + + long startTs; + long endTs; + if (pageLink.getTimeWindow() > 0) { + endTs = System.currentTimeMillis(); + startTs = endTs - pageLink.getTimeWindow(); + } else { + startTs = pageLink.getStartTs(); + endTs = pageLink.getEndTs(); + } + + if (startTs > 0) { + addAndIfNeeded(wherePart, addAnd); + addAnd = true; + ctx.addLongParameter("startTime", startTs); + wherePart.append("a.created_time >= :startTime"); + } + + if (endTs > 0) { + addAndIfNeeded(wherePart, addAnd); + addAnd = true; + ctx.addLongParameter("endTime", endTs); + wherePart.append("a.created_time <= :endTime"); + } + + if (pageLink.getTypeList() != null && !pageLink.getTypeList().isEmpty()) { + addAndIfNeeded(wherePart, addAnd); + addAnd = true; + ctx.addStringListParameter("alarmTypes", pageLink.getTypeList()); + wherePart.append("a.type in (:alarmTypes)"); + } + + if (pageLink.getSeverityList() != null && !pageLink.getSeverityList().isEmpty()) { + addAndIfNeeded(wherePart, addAnd); + addAnd = true; + ctx.addStringListParameter("alarmSeverities", pageLink.getSeverityList().stream().map(AlarmSeverity::name).collect(Collectors.toList())); + wherePart.append("a.severity in (:alarmSeverities)"); + } + + if (pageLink.getStatusList() != null && !pageLink.getStatusList().isEmpty()) { + Set statusSet = toStatusSet(pageLink.getStatusList()); + if (!statusSet.isEmpty()) { + addAndIfNeeded(wherePart, addAnd); + addAnd = true; + ctx.addStringListParameter("alarmStatuses", statusSet.stream().map(AlarmStatus::name).collect(Collectors.toList())); + wherePart.append(" a.status in (:alarmStatuses)"); + } + } + + String textSearchQuery = buildTextSearchQuery(ctx, query.getAlarmFields(), pageLink.getTextSearch()); + String mainQuery = selectPart.toString() + fromPart.toString() + wherePart.toString(); + if (!textSearchQuery.isEmpty()) { + mainQuery = String.format("select * from (%s) a WHERE %s", mainQuery, textSearchQuery); + } + String countQuery = String.format("select count(*) from (%s) result", mainQuery); + long queryTs = System.currentTimeMillis(); + int totalElements; + try { + totalElements = jdbcTemplate.queryForObject(countQuery, ctx, Integer.class); + } finally { + queryLog.logQuery(ctx, countQuery, System.currentTimeMillis() - queryTs); + } + if (totalElements == 0) { + return AlarmDataAdapter.createAlarmData(pageLink, Collections.emptyList(), totalElements, orderedEntityIds); + } + + String dataQuery = mainQuery + sortPart; + + int startIndex = pageLink.getPageSize() * pageLink.getPage(); + if (pageLink.getPageSize() > 0) { + dataQuery = String.format("%s limit %s offset %s", dataQuery, pageLink.getPageSize(), startIndex); + } + queryTs = System.currentTimeMillis(); + List> rows; + try { + rows = jdbcTemplate.queryForList(dataQuery, ctx); + } finally { + queryLog.logQuery(ctx, dataQuery, System.currentTimeMillis() - queryTs); + } + return AlarmDataAdapter.createAlarmData(pageLink, rows, totalElements, orderedEntityIds); + }); + } + + private String buildTextSearchQuery(QueryContext ctx, List selectionMapping, String searchText) { + if (!StringUtils.isEmpty(searchText) && selectionMapping != null && !selectionMapping.isEmpty()) { + String lowerSearchText = searchText.toLowerCase() + "%"; + List searchPredicates = selectionMapping.stream() + .map(mapping -> alarmFieldColumnMap.get(mapping.getKey())) + .filter(Objects::nonNull) + .map(mapping -> { + String paramName = mapping + "_lowerSearchText"; + ctx.addStringParameter(paramName, lowerSearchText); + return String.format("LOWER(cast(%s as varchar)) LIKE concat('%%', :%s, '%%')", mapping, paramName); + } + ).collect(Collectors.toList()); + return String.format("%s", String.join(" or ", searchPredicates)); + } else { + return ""; + } + } + + private String buildPermissionsQuery(TenantId tenantId, CustomerId customerId, QueryContext ctx) { + StringBuilder permissionsQuery = new StringBuilder(); + ctx.addUuidParameter("permissions_tenant_id", tenantId.getId()); + permissionsQuery.append(" a.tenant_id = :permissions_tenant_id "); + if (customerId != null && !customerId.isNullUid()) { + ctx.addUuidParameter("permissions_customer_id", customerId.getId()); + ctx.addUuidParameter("permissions_device_customer_id", customerId.getId()); + ctx.addUuidParameter("permissions_asset_customer_id", customerId.getId()); + ctx.addUuidParameter("permissions_user_customer_id", customerId.getId()); + ctx.addUuidParameter("permissions_entity_view_customer_id", customerId.getId()); + permissionsQuery.append(" and ("); + permissionsQuery.append("(a.originator_type = '").append(EntityType.DEVICE.ordinal()).append("' and exists (select 1 from device cd where cd.id = a.originator_id and cd.customer_id = :permissions_device_customer_id))"); + permissionsQuery.append(" or "); + permissionsQuery.append("(a.originator_type = '").append(EntityType.ASSET.ordinal()).append("' and exists (select 1 from asset ca where ca.id = a.originator_id and ca.customer_id = :permissions_device_customer_id))"); + permissionsQuery.append(" or "); + permissionsQuery.append("(a.originator_type = '").append(EntityType.CUSTOMER.ordinal()).append("' and exists (select 1 from customer cc where cc.id = a.originator_id and cc.id = :permissions_customer_id))"); + permissionsQuery.append(" or "); + permissionsQuery.append("(a.originator_type = '").append(EntityType.USER.ordinal()).append("' and exists (select 1 from tb_user cu where cu.id = a.originator_id and cu.customer_id = :permissions_user_customer_id))"); + permissionsQuery.append(" or "); + permissionsQuery.append("(a.originator_type = '").append(EntityType.ENTITY_VIEW.ordinal()).append("' and exists (select 1 from entity_view cv where cv.id = a.originator_id and cv.customer_id = :permissions_entity_view_customer_id))"); + permissionsQuery.append(")"); + } + return permissionsQuery.toString(); + } + + private Set toStatusSet(List statusList) { + Set result = new HashSet<>(); + for (AlarmSearchStatus searchStatus : statusList) { + switch (searchStatus) { + case ACK: + result.add(AlarmStatus.ACTIVE_ACK); + result.add(AlarmStatus.CLEARED_ACK); + break; + case UNACK: + result.add(AlarmStatus.ACTIVE_UNACK); + result.add(AlarmStatus.CLEARED_UNACK); + break; + case CLEARED: + result.add(AlarmStatus.CLEARED_ACK); + result.add(AlarmStatus.CLEARED_UNACK); + break; + case ACTIVE: + result.add(AlarmStatus.ACTIVE_ACK); + result.add(AlarmStatus.ACTIVE_UNACK); + break; + default: + break; + } + if (searchStatus == AlarmSearchStatus.ANY || result.size() == AlarmStatus.values().length) { + result.clear(); + return result; + } + } + return result; + } + + private void addAndIfNeeded(StringBuilder wherePart, boolean addAnd) { + if (addAnd) { + wherePart.append(" and "); + } + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java new file mode 100644 index 0000000000..380e166aa5 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java @@ -0,0 +1,693 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.query; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.support.TransactionTemplate; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AssetSearchQueryFilter; +import org.thingsboard.server.common.data.query.AssetTypeFilter; +import org.thingsboard.server.common.data.query.DeviceSearchQueryFilter; +import org.thingsboard.server.common.data.query.DeviceTypeFilter; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityFilter; +import org.thingsboard.server.common.data.query.EntityFilterType; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityListFilter; +import org.thingsboard.server.common.data.query.EntityNameFilter; +import org.thingsboard.server.common.data.query.EntitySearchQueryFilter; +import org.thingsboard.server.common.data.query.EntityViewSearchQueryFilter; +import org.thingsboard.server.common.data.query.EntityViewTypeFilter; +import org.thingsboard.server.common.data.query.RelationsQueryFilter; +import org.thingsboard.server.common.data.query.SingleEntityFilter; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.EntityTypeFilter; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +@Repository +@Slf4j +public class DefaultEntityQueryRepository implements EntityQueryRepository { + private static final Map entityTableMap = new HashMap<>(); + private static final String SELECT_PHONE = " CASE WHEN entity.entity_type = 'TENANT' THEN (select phone from tenant where id = entity_id)" + + " WHEN entity.entity_type = 'CUSTOMER' THEN (select phone from customer where id = entity_id) END as phone"; + private static final String SELECT_ZIP = " CASE WHEN entity.entity_type = 'TENANT' THEN (select zip from tenant where id = entity_id)" + + " WHEN entity.entity_type = 'CUSTOMER' THEN (select zip from customer where id = entity_id) END as zip"; + private static final String SELECT_ADDRESS_2 = " CASE WHEN entity.entity_type = 'TENANT'" + + " THEN (select address2 from tenant where id = entity_id) WHEN entity.entity_type = 'CUSTOMER' " + + " THEN (select address2 from customer where id = entity_id) END as address2"; + private static final String SELECT_ADDRESS = " CASE WHEN entity.entity_type = 'TENANT'" + + " THEN (select address from tenant where id = entity_id) WHEN entity.entity_type = 'CUSTOMER' " + + " THEN (select address from customer where id = entity_id) END as address"; + private static final String SELECT_CITY = " CASE WHEN entity.entity_type = 'TENANT'" + + " THEN (select city from tenant where id = entity_id) WHEN entity.entity_type = 'CUSTOMER' " + + " THEN (select city from customer where id = entity_id) END as city"; + private static final String SELECT_STATE = " CASE WHEN entity.entity_type = 'TENANT'" + + " THEN (select state from tenant where id = entity_id) WHEN entity.entity_type = 'CUSTOMER' " + + " THEN (select state from customer where id = entity_id) END as state"; + private static final String SELECT_COUNTRY = " CASE WHEN entity.entity_type = 'TENANT'" + + " THEN (select country from tenant where id = entity_id) WHEN entity.entity_type = 'CUSTOMER' " + + " THEN (select country from customer where id = entity_id) END as country"; + private static final String SELECT_TITLE = " CASE WHEN entity.entity_type = 'TENANT'" + + " THEN (select title from tenant where id = entity_id) WHEN entity.entity_type = 'CUSTOMER' " + + " THEN (select title from customer where id = entity_id) END as title"; + private static final String SELECT_LAST_NAME = " CASE WHEN entity.entity_type = 'USER'" + + " THEN (select last_name from tb_user where id = entity_id) END as last_name"; + private static final String SELECT_FIRST_NAME = " CASE WHEN entity.entity_type = 'USER'" + + " THEN (select first_name from tb_user where id = entity_id) END as first_name"; + private static final String SELECT_REGION = " CASE WHEN entity.entity_type = 'TENANT'" + + " THEN (select region from tenant where id = entity_id) END as region"; + private static final String SELECT_EMAIL = " CASE" + + " WHEN entity.entity_type = 'TENANT'" + + " THEN (select email from tenant where id = entity_id)" + + " WHEN entity.entity_type = 'CUSTOMER' " + + " THEN (select email from customer where id = entity_id)" + + " WHEN entity.entity_type = 'USER'" + + " THEN (select email from tb_user where id = entity_id)" + + " END as email"; + private static final String SELECT_CUSTOMER_ID = "CASE" + + " WHEN entity.entity_type = 'TENANT'" + + " THEN UUID('" + TenantId.NULL_UUID + "')" + + " WHEN entity.entity_type = 'CUSTOMER' THEN entity_id" + + " WHEN entity.entity_type = 'USER'" + + " THEN (select customer_id from tb_user where id = entity_id)" + + " WHEN entity.entity_type = 'DASHBOARD'" + + //TODO: parse assigned customers or use contains? + " THEN NULL" + + " WHEN entity.entity_type = 'ASSET'" + + " THEN (select customer_id from asset where id = entity_id)" + + " WHEN entity.entity_type = 'DEVICE'" + + " THEN (select customer_id from device where id = entity_id)" + + " WHEN entity.entity_type = 'ENTITY_VIEW'" + + " THEN (select customer_id from entity_view where id = entity_id)" + + " END as customer_id"; + private static final String SELECT_TENANT_ID = "SELECT CASE" + + " WHEN entity.entity_type = 'TENANT' THEN entity_id" + + " WHEN entity.entity_type = 'CUSTOMER'" + + " THEN (select tenant_id from customer where id = entity_id)" + + " WHEN entity.entity_type = 'USER'" + + " THEN (select tenant_id from tb_user where id = entity_id)" + + " WHEN entity.entity_type = 'DASHBOARD'" + + " THEN (select tenant_id from dashboard where id = entity_id)" + + " WHEN entity.entity_type = 'ASSET'" + + " THEN (select tenant_id from asset where id = entity_id)" + + " WHEN entity.entity_type = 'DEVICE'" + + " THEN (select tenant_id from device where id = entity_id)" + + " WHEN entity.entity_type = 'ENTITY_VIEW'" + + " THEN (select tenant_id from entity_view where id = entity_id)" + + " END as tenant_id"; + private static final String SELECT_CREATED_TIME = " CASE" + + " WHEN entity.entity_type = 'TENANT'" + + " THEN (select created_time from tenant where id = entity_id)" + + " WHEN entity.entity_type = 'CUSTOMER' " + + " THEN (select created_time from customer where id = entity_id)" + + " WHEN entity.entity_type = 'USER'" + + " THEN (select created_time from tb_user where id = entity_id)" + + " WHEN entity.entity_type = 'DASHBOARD'" + + " THEN (select created_time from dashboard where id = entity_id)" + + " WHEN entity.entity_type = 'ASSET'" + + " THEN (select created_time from asset where id = entity_id)" + + " WHEN entity.entity_type = 'DEVICE'" + + " THEN (select created_time from device where id = entity_id)" + + " WHEN entity.entity_type = 'ENTITY_VIEW'" + + " THEN (select created_time from entity_view where id = entity_id)" + + " END as created_time"; + private static final String SELECT_NAME = " CASE" + + " WHEN entity.entity_type = 'TENANT'" + + " THEN (select title from tenant where id = entity_id)" + + " WHEN entity.entity_type = 'CUSTOMER' " + + " THEN (select title from customer where id = entity_id)" + + " WHEN entity.entity_type = 'USER'" + + " THEN (select CONCAT (first_name, ' ', last_name) from tb_user where id = entity_id)" + + " WHEN entity.entity_type = 'DASHBOARD'" + + " THEN (select title from dashboard where id = entity_id)" + + " WHEN entity.entity_type = 'ASSET'" + + " THEN (select name from asset where id = entity_id)" + + " WHEN entity.entity_type = 'DEVICE'" + + " THEN (select name from device where id = entity_id)" + + " WHEN entity.entity_type = 'ENTITY_VIEW'" + + " THEN (select name from entity_view where id = entity_id)" + + " END as name"; + private static final String SELECT_TYPE = " CASE" + + " WHEN entity.entity_type = 'USER'" + + " THEN (select authority from tb_user where id = entity_id)" + + " WHEN entity.entity_type = 'ASSET'" + + " THEN (select type from asset where id = entity_id)" + + " WHEN entity.entity_type = 'DEVICE'" + + " THEN (select type from device where id = entity_id)" + + " WHEN entity.entity_type = 'ENTITY_VIEW'" + + " THEN (select type from entity_view where id = entity_id)" + + " ELSE entity.entity_type END as type"; + private static final String SELECT_LABEL = " CASE" + + " WHEN entity.entity_type = 'TENANT'" + + " THEN (select title from tenant where id = entity_id)" + + " WHEN entity.entity_type = 'CUSTOMER' " + + " THEN (select title from customer where id = entity_id)" + + " WHEN entity.entity_type = 'USER'" + + " THEN (select CONCAT (first_name, ' ', last_name) from tb_user where id = entity_id)" + + " WHEN entity.entity_type = 'DASHBOARD'" + + " THEN (select title from dashboard where id = entity_id)" + + " WHEN entity.entity_type = 'ASSET'" + + " THEN (select label from asset where id = entity_id)" + + " WHEN entity.entity_type = 'DEVICE'" + + " THEN (select label from device where id = entity_id)" + + " WHEN entity.entity_type = 'ENTITY_VIEW'" + + " THEN (select name from entity_view where id = entity_id)" + + " END as label"; + private static final String SELECT_ADDITIONAL_INFO = " CASE" + + " WHEN entity.entity_type = 'TENANT'" + + " THEN (select additional_info from tenant where id = entity_id)" + + " WHEN entity.entity_type = 'CUSTOMER' " + + " THEN (select additional_info from customer where id = entity_id)" + + " WHEN entity.entity_type = 'USER'" + + " THEN (select additional_info from tb_user where id = entity_id)" + + " WHEN entity.entity_type = 'DASHBOARD'" + + " THEN (select '' from dashboard where id = entity_id)" + + " WHEN entity.entity_type = 'ASSET'" + + " THEN (select additional_info from asset where id = entity_id)" + + " WHEN entity.entity_type = 'DEVICE'" + + " THEN (select additional_info from device where id = entity_id)" + + " WHEN entity.entity_type = 'ENTITY_VIEW'" + + " THEN (select additional_info from entity_view where id = entity_id)" + + " END as additional_info"; + + static { + entityTableMap.put(EntityType.ASSET, "asset"); + entityTableMap.put(EntityType.DEVICE, "device"); + entityTableMap.put(EntityType.ENTITY_VIEW, "entity_view"); + entityTableMap.put(EntityType.DASHBOARD, "dashboard"); + entityTableMap.put(EntityType.CUSTOMER, "customer"); + entityTableMap.put(EntityType.USER, "tb_user"); + entityTableMap.put(EntityType.TENANT, "tenant"); + entityTableMap.put(EntityType.API_USAGE_STATE, "(select aus.id, aus.created_time, aus.tenant_id, '' as name, '' as additional_info from api_usage_state as aus)"); + } + + public static EntityType[] RELATION_QUERY_ENTITY_TYPES = new EntityType[]{ + EntityType.TENANT, EntityType.CUSTOMER, EntityType.USER, EntityType.DASHBOARD, EntityType.ASSET, EntityType.DEVICE, EntityType.ENTITY_VIEW}; + + private static final String HIERARCHICAL_QUERY_TEMPLATE = " FROM (WITH RECURSIVE related_entities(from_id, from_type, to_id, to_type, relation_type, lvl) AS (" + + " SELECT from_id, from_type, to_id, to_type, relation_type, 1 as lvl" + + " FROM relation" + + " WHERE $in_id = :relation_root_id and $in_type = :relation_root_type and relation_type_group = 'COMMON'" + + " UNION ALL" + + " SELECT r.from_id, r.from_type, r.to_id, r.to_type, r.relation_type, lvl + 1" + + " FROM relation r" + + " INNER JOIN related_entities re ON" + + " r.$in_id = re.$out_id and r.$in_type = re.$out_type and" + + " relation_type_group = 'COMMON' %s)" + + " SELECT re.$out_id entity_id, re.$out_type entity_type, max(re.lvl) lvl" + + " from related_entities re" + + " %s GROUP BY entity_id, entity_type) entity"; + private static final String HIERARCHICAL_TO_QUERY_TEMPLATE = HIERARCHICAL_QUERY_TEMPLATE.replace("$in", "to").replace("$out", "from"); + private static final String HIERARCHICAL_FROM_QUERY_TEMPLATE = HIERARCHICAL_QUERY_TEMPLATE.replace("$in", "from").replace("$out", "to"); + + private final NamedParameterJdbcTemplate jdbcTemplate; + private final TransactionTemplate transactionTemplate; + private final DefaultQueryLogComponent queryLog; + + public DefaultEntityQueryRepository(NamedParameterJdbcTemplate jdbcTemplate, TransactionTemplate transactionTemplate, DefaultQueryLogComponent queryLog) { + this.jdbcTemplate = jdbcTemplate; + this.transactionTemplate = transactionTemplate; + this.queryLog = queryLog; + } + + @Override + public long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query) { + EntityType entityType = resolveEntityType(query.getEntityFilter()); + QueryContext ctx = new QueryContext(new QuerySecurityContext(tenantId, customerId, entityType)); + ctx.append("select count(e.id) from "); + ctx.append(addEntityTableQuery(ctx, query.getEntityFilter())); + ctx.append(" e where "); + ctx.append(buildEntityWhere(ctx, query.getEntityFilter(), Collections.emptyList())); + return transactionTemplate.execute(status -> { + long startTs = System.currentTimeMillis(); + try { + return jdbcTemplate.queryForObject(ctx.getQuery(), ctx, Long.class); + } finally { + queryLog.logQuery(ctx, ctx.getQuery(), System.currentTimeMillis() - startTs); + } + }); + } + + @Override + public PageData findEntityDataByQuery(TenantId tenantId, CustomerId customerId, EntityDataQuery query) { + return transactionTemplate.execute(status -> { + EntityType entityType = resolveEntityType(query.getEntityFilter()); + QueryContext ctx = new QueryContext(new QuerySecurityContext(tenantId, customerId, entityType)); + EntityDataPageLink pageLink = query.getPageLink(); + + List mappings = EntityKeyMapping.prepareKeyMapping(query); + + List selectionMapping = mappings.stream().filter(EntityKeyMapping::isSelection) + .collect(Collectors.toList()); + List entityFieldsSelectionMapping = selectionMapping.stream().filter(mapping -> !mapping.isLatest()) + .collect(Collectors.toList()); + List latestSelectionMapping = selectionMapping.stream().filter(EntityKeyMapping::isLatest) + .collect(Collectors.toList()); + + List filterMapping = mappings.stream().filter(EntityKeyMapping::hasFilter) + .collect(Collectors.toList()); + List entityFieldsFiltersMapping = filterMapping.stream().filter(mapping -> !mapping.isLatest()) + .collect(Collectors.toList()); + + List allLatestMappings = mappings.stream().filter(EntityKeyMapping::isLatest) + .collect(Collectors.toList()); + + + String entityWhereClause = DefaultEntityQueryRepository.this.buildEntityWhere(ctx, query.getEntityFilter(), entityFieldsFiltersMapping); + String latestJoinsCnt = EntityKeyMapping.buildLatestJoins(ctx, query.getEntityFilter(), entityType, allLatestMappings, true); + String latestJoinsData = EntityKeyMapping.buildLatestJoins(ctx, query.getEntityFilter(), entityType, allLatestMappings, false); + String textSearchQuery = DefaultEntityQueryRepository.this.buildTextSearchQuery(ctx, selectionMapping, pageLink.getTextSearch()); + String entityFieldsSelection = EntityKeyMapping.buildSelections(entityFieldsSelectionMapping, query.getEntityFilter().getType(), entityType); + String entityTypeStr; + if (query.getEntityFilter().getType().equals(EntityFilterType.RELATIONS_QUERY)) { + entityTypeStr = "e.entity_type"; + } else { + entityTypeStr = "'" + entityType.name() + "'"; + } + if (!StringUtils.isEmpty(entityFieldsSelection)) { + entityFieldsSelection = String.format("e.id id, %s entity_type, %s", entityTypeStr, entityFieldsSelection); + } else { + entityFieldsSelection = String.format("e.id id, %s entity_type", entityTypeStr); + } + String latestSelection = EntityKeyMapping.buildSelections(latestSelectionMapping, query.getEntityFilter().getType(), entityType); + String topSelection = "entities.*"; + if (!StringUtils.isEmpty(latestSelection)) { + topSelection = topSelection + ", " + latestSelection; + } + + String fromClauseCount = String.format("from (select %s from (select %s from %s e where %s) entities %s ) result %s", + "entities.*", + entityFieldsSelection, + addEntityTableQuery(ctx, query.getEntityFilter()), + entityWhereClause, + latestJoinsCnt, + textSearchQuery); + + String fromClauseData = String.format("from (select %s from (select %s from %s e where %s) entities %s ) result %s", + topSelection, + entityFieldsSelection, + addEntityTableQuery(ctx, query.getEntityFilter()), + entityWhereClause, + latestJoinsData, + textSearchQuery); + + if (!StringUtils.isEmpty(pageLink.getTextSearch())) { + //Unfortunately, we need to sacrifice performance in case of full text search, because it is applied to all joined records. + fromClauseCount = fromClauseData; + } + String countQuery = String.format("select count(id) %s", fromClauseCount); + + long startTs = System.currentTimeMillis(); + int totalElements; + try { + totalElements = jdbcTemplate.queryForObject(countQuery, ctx, Integer.class); + } finally { + queryLog.logQuery(ctx, countQuery, System.currentTimeMillis() - startTs); + } + + if (totalElements == 0) { + return new PageData<>(); + } + String dataQuery = String.format("select * %s", fromClauseData); + + EntityDataSortOrder sortOrder = pageLink.getSortOrder(); + if (sortOrder != null) { + Optional sortOrderMappingOpt = mappings.stream().filter(EntityKeyMapping::isSortOrder).findFirst(); + if (sortOrderMappingOpt.isPresent()) { + EntityKeyMapping sortOrderMapping = sortOrderMappingOpt.get(); + String direction = sortOrder.getDirection() == EntityDataSortOrder.Direction.ASC ? "asc" : "desc"; + if (sortOrderMapping.getEntityKey().getType() == EntityKeyType.ENTITY_FIELD) { + dataQuery = String.format("%s order by %s %s", dataQuery, sortOrderMapping.getValueAlias(), direction); + } else { + dataQuery = String.format("%s order by %s %s, %s %s", dataQuery, + sortOrderMapping.getSortOrderNumAlias(), direction, sortOrderMapping.getSortOrderStrAlias(), direction); + } + } + } + int startIndex = pageLink.getPageSize() * pageLink.getPage(); + if (pageLink.getPageSize() > 0) { + dataQuery = String.format("%s limit %s offset %s", dataQuery, pageLink.getPageSize(), startIndex); + } + startTs = System.currentTimeMillis(); + List> rows; + try { + rows = jdbcTemplate.queryForList(dataQuery, ctx); + } finally { + queryLog.logQuery(ctx, countQuery, System.currentTimeMillis() - startTs); + } + return EntityDataAdapter.createEntityData(pageLink, selectionMapping, rows, totalElements); + }); + } + + private String buildEntityWhere(QueryContext ctx, EntityFilter entityFilter, List entityFieldsFilters) { + String permissionQuery = this.buildPermissionQuery(ctx, entityFilter); + String entityFilterQuery = this.buildEntityFilterQuery(ctx, entityFilter); + String entityFieldsQuery = EntityKeyMapping.buildQuery(ctx, entityFieldsFilters, entityFilter.getType()); + String result = permissionQuery; + if (!entityFilterQuery.isEmpty()) { + result += " and (" + entityFilterQuery + ")"; + } + if (!entityFieldsQuery.isEmpty()) { + result += " and (" + entityFieldsQuery + ")"; + } + return result; + } + + private String buildPermissionQuery(QueryContext ctx, EntityFilter entityFilter) { + switch (entityFilter.getType()) { + case RELATIONS_QUERY: + case DEVICE_SEARCH_QUERY: + case ASSET_SEARCH_QUERY: + case ENTITY_VIEW_SEARCH_QUERY: + return this.defaultPermissionQuery(ctx); + default: + if (ctx.getEntityType() == EntityType.TENANT) { + ctx.addUuidParameter("permissions_tenant_id", ctx.getTenantId().getId()); + return "e.id=:permissions_tenant_id"; + } else { + return this.defaultPermissionQuery(ctx); + } + } + } + + private String defaultPermissionQuery(QueryContext ctx) { + ctx.addUuidParameter("permissions_tenant_id", ctx.getTenantId().getId()); + if (ctx.getCustomerId() != null && !ctx.getCustomerId().isNullUid()) { + ctx.addUuidParameter("permissions_customer_id", ctx.getCustomerId().getId()); + if (ctx.getEntityType() == EntityType.CUSTOMER) { + return "e.tenant_id=:permissions_tenant_id and e.id=:permissions_customer_id"; + } else { + return "e.tenant_id=:permissions_tenant_id and e.customer_id=:permissions_customer_id"; + } + } else { + return "e.tenant_id=:permissions_tenant_id"; + } + } + + private String buildEntityFilterQuery(QueryContext ctx, EntityFilter entityFilter) { + switch (entityFilter.getType()) { + case SINGLE_ENTITY: + return this.singleEntityQuery(ctx, (SingleEntityFilter) entityFilter); + case ENTITY_LIST: + return this.entityListQuery(ctx, (EntityListFilter) entityFilter); + case ENTITY_NAME: + return this.entityNameQuery(ctx, (EntityNameFilter) entityFilter); + case ASSET_TYPE: + case DEVICE_TYPE: + case ENTITY_VIEW_TYPE: + return this.typeQuery(ctx, entityFilter); + case RELATIONS_QUERY: + case DEVICE_SEARCH_QUERY: + case ASSET_SEARCH_QUERY: + case ENTITY_VIEW_SEARCH_QUERY: + case API_USAGE_STATE: + return ""; + default: + throw new RuntimeException("Not implemented!"); + } + } + + private String addEntityTableQuery(QueryContext ctx, EntityFilter entityFilter) { + switch (entityFilter.getType()) { + case RELATIONS_QUERY: + return relationQuery(ctx, (RelationsQueryFilter) entityFilter); + case DEVICE_SEARCH_QUERY: + DeviceSearchQueryFilter deviceQuery = (DeviceSearchQueryFilter) entityFilter; + return entitySearchQuery(ctx, deviceQuery, EntityType.DEVICE, deviceQuery.getDeviceTypes()); + case ASSET_SEARCH_QUERY: + AssetSearchQueryFilter assetQuery = (AssetSearchQueryFilter) entityFilter; + return entitySearchQuery(ctx, assetQuery, EntityType.ASSET, assetQuery.getAssetTypes()); + case ENTITY_VIEW_SEARCH_QUERY: + EntityViewSearchQueryFilter entityViewQuery = (EntityViewSearchQueryFilter) entityFilter; + return entitySearchQuery(ctx, entityViewQuery, EntityType.ENTITY_VIEW, entityViewQuery.getEntityViewTypes()); + default: + return entityTableMap.get(ctx.getEntityType()); + } + } + + private String entitySearchQuery(QueryContext ctx, EntitySearchQueryFilter entityFilter, EntityType entityType, List types) { + EntityId rootId = entityFilter.getRootEntity(); + String lvlFilter = getLvlFilter(entityFilter.getMaxLevel()); + String selectFields = "SELECT tenant_id, customer_id, id, created_time, type, name, additional_info " + + (entityType.equals(EntityType.ENTITY_VIEW) ? "" : ", label ") + + "FROM " + entityType.name() + " WHERE id in ( SELECT entity_id"; + String from = getQueryTemplate(entityFilter.getDirection()); + String whereFilter = " WHERE"; + if (!StringUtils.isEmpty(entityFilter.getRelationType())) { + ctx.addStringParameter("where_relation_type", entityFilter.getRelationType()); + whereFilter += " re.relation_type = :where_relation_type AND"; + } + String toOrFrom = (entityFilter.getDirection().equals(EntitySearchDirection.FROM) ? "to" : "from"); + whereFilter += " re." + (entityFilter.getDirection().equals(EntitySearchDirection.FROM) ? "to" : "from") + "_type = :where_entity_type"; + if (entityFilter.isFetchLastLevelOnly()) { + String fromOrTo = (entityFilter.getDirection().equals(EntitySearchDirection.FROM) ? "from" : "to"); + StringBuilder notExistsPart = new StringBuilder(); + notExistsPart.append(" NOT EXISTS (SELECT 1 from relation nr ") + .append(whereFilter.replaceAll("re\\.", "nr\\.")) + .append(" and ") + .append("nr.").append(fromOrTo).append("_id").append(" = re.").append(toOrFrom).append("_id") + .append(" and ") + .append("nr.").append(fromOrTo).append("_type").append(" = re.").append(toOrFrom).append("_type"); + + notExistsPart.append(")"); + whereFilter += " and ( re.lvl = " + entityFilter.getMaxLevel() + " OR " + notExistsPart.toString() + ")"; + } + from = String.format(from, lvlFilter, whereFilter); + String query = "( " + selectFields + from + ")"; + if (types != null && !types.isEmpty()) { + query += " and type in (:relation_sub_types)"; + ctx.addStringListParameter("relation_sub_types", types); + } + query += " )"; + ctx.addUuidParameter("relation_root_id", rootId.getId()); + ctx.addStringParameter("relation_root_type", rootId.getEntityType().name()); + ctx.addStringParameter("where_entity_type", entityType.name()); + return query; + } + + private String relationQuery(QueryContext ctx, RelationsQueryFilter entityFilter) { + EntityId rootId = entityFilter.getRootEntity(); + String lvlFilter = getLvlFilter(entityFilter.getMaxLevel()); + String selectFields = SELECT_TENANT_ID + ", " + SELECT_CUSTOMER_ID + + ", " + SELECT_CREATED_TIME + ", " + + " entity.entity_id as id," + + SELECT_TYPE + ", " + SELECT_NAME + ", " + SELECT_LABEL + ", " + + SELECT_FIRST_NAME + ", " + SELECT_LAST_NAME + ", " + SELECT_EMAIL + ", " + SELECT_REGION + ", " + + SELECT_TITLE + ", " + SELECT_COUNTRY + ", " + SELECT_STATE + ", " + SELECT_CITY + ", " + + SELECT_ADDRESS + ", " + SELECT_ADDRESS_2 + ", " + SELECT_ZIP + ", " + SELECT_PHONE + ", " + SELECT_ADDITIONAL_INFO + + ", entity.entity_type as entity_type"; + String from = getQueryTemplate(entityFilter.getDirection()); + ctx.addUuidParameter("relation_root_id", rootId.getId()); + ctx.addStringParameter("relation_root_type", rootId.getEntityType().name()); + + StringBuilder whereFilter = new StringBuilder(); + + boolean noConditions = true; + boolean single = entityFilter.getFilters() != null && entityFilter.getFilters().size() == 1; + if (entityFilter.getFilters() != null && !entityFilter.getFilters().isEmpty()) { + int entityTypeFilterIdx = 0; + for (EntityTypeFilter etf : entityFilter.getFilters()) { + String etfCondition = buildEtfCondition(ctx, etf, entityFilter.getDirection(), entityTypeFilterIdx++); + if (!etfCondition.isEmpty()) { + if (noConditions) { + noConditions = false; + } else { + whereFilter.append(" OR "); + } + if (!single) { + whereFilter.append(" ("); + } + whereFilter.append(etfCondition); + if (!single) { + whereFilter.append(" )"); + } + } + } + } + if (noConditions) { + whereFilter.append(" re.") + .append(entityFilter.getDirection().equals(EntitySearchDirection.FROM) ? "to" : "from") + .append("_type in (:where_entity_types").append(")"); + ctx.addStringListParameter("where_entity_types", Arrays.stream(RELATION_QUERY_ENTITY_TYPES).map(EntityType::name).collect(Collectors.toList())); + } + + if (!noConditions && !single) { + whereFilter = new StringBuilder().append("(").append(whereFilter).append(")"); + } + + if (entityFilter.isFetchLastLevelOnly()) { + String toOrFrom = (entityFilter.getDirection().equals(EntitySearchDirection.FROM) ? "to" : "from"); + String fromOrTo = (entityFilter.getDirection().equals(EntitySearchDirection.FROM) ? "from" : "to"); + + StringBuilder notExistsPart = new StringBuilder(); + notExistsPart.append(" NOT EXISTS (SELECT 1 from relation nr WHERE "); + notExistsPart + .append("nr.").append(fromOrTo).append("_id").append(" = re.").append(toOrFrom).append("_id") + .append(" and ") + .append("nr.").append(fromOrTo).append("_type").append(" = re.").append(toOrFrom).append("_type") + .append(" and ") + .append(whereFilter.toString().replaceAll("re\\.", "nr\\.")); + + notExistsPart.append(")"); + whereFilter.append(" and ( re.lvl = ").append(entityFilter.getMaxLevel()).append(" OR ").append(notExistsPart.toString()).append(")"); + } + from = String.format(from, lvlFilter, " WHERE " + whereFilter); + return "( " + selectFields + from + ")"; + } + + private String buildEtfCondition(QueryContext ctx, EntityTypeFilter etf, EntitySearchDirection direction, int entityTypeFilterIdx) { + StringBuilder whereFilter = new StringBuilder(); + String relationType = etf.getRelationType(); + List entityTypes = etf.getEntityTypes(); + List whereEntityTypes; + if (entityTypes == null || entityTypes.isEmpty()) { + whereEntityTypes = Collections.emptyList(); + } else { + whereEntityTypes = etf.getEntityTypes().stream().map(EntityType::name).collect(Collectors.toList()); + } + boolean hasRelationType = !StringUtils.isEmpty(relationType); + if (hasRelationType) { + ctx.addStringParameter("where_relation_type" + entityTypeFilterIdx, relationType); + whereFilter + .append("re.relation_type = :where_relation_type").append(entityTypeFilterIdx); + } + if (!whereEntityTypes.isEmpty()) { + if (hasRelationType) { + whereFilter.append(" and "); + } + whereFilter.append("re.") + .append(direction.equals(EntitySearchDirection.FROM) ? "to" : "from") + .append("_type in (:where_entity_types").append(entityTypeFilterIdx).append(")"); + ctx.addStringListParameter("where_entity_types" + entityTypeFilterIdx, whereEntityTypes); + } + return whereFilter.toString(); + } + + private String getLvlFilter(int maxLevel) { + return maxLevel > 0 ? ("and lvl <= " + (maxLevel - 1)) : ""; + } + + private String getQueryTemplate(EntitySearchDirection direction) { + String from; + if (direction.equals(EntitySearchDirection.FROM)) { + from = HIERARCHICAL_FROM_QUERY_TEMPLATE; + } else { + from = HIERARCHICAL_TO_QUERY_TEMPLATE; + } + return from; + } + + private String buildTextSearchQuery(QueryContext ctx, List selectionMapping, String searchText) { + if (!StringUtils.isEmpty(searchText) && !selectionMapping.isEmpty()) { + String lowerSearchText = "%" + searchText.toLowerCase() + "%"; + ctx.addStringParameter("lowerSearchTextParam", lowerSearchText); + List searchAliases = selectionMapping.stream().filter(EntityKeyMapping::isSearchable).map(EntityKeyMapping::getValueAlias).collect(Collectors.toList()); + String searchAliasesExpression; + if (searchAliases.size() > 1) { + searchAliasesExpression = "CONCAT(" + String.join(" , ", searchAliases) + ")"; + } else { + searchAliasesExpression = searchAliases.get(0); + } + return String.format(" WHERE LOWER(%s) LIKE :%s", searchAliasesExpression, "lowerSearchTextParam"); + } else { + return ""; + } + } + + private String singleEntityQuery(QueryContext ctx, SingleEntityFilter filter) { + ctx.addUuidParameter("entity_filter_single_entity_id", filter.getSingleEntity().getId()); + return "e.id=:entity_filter_single_entity_id"; + } + + private String entityListQuery(QueryContext ctx, EntityListFilter filter) { + ctx.addUuidListParameter("entity_filter_entity_ids", filter.getEntityList().stream().map(UUID::fromString).collect(Collectors.toList())); + return "e.id in (:entity_filter_entity_ids)"; + } + + private String entityNameQuery(QueryContext ctx, EntityNameFilter filter) { + ctx.addStringParameter("entity_filter_name_filter", filter.getEntityNameFilter()); + return "lower(e.search_text) like lower(concat(:entity_filter_name_filter, '%%'))"; + } + + private String typeQuery(QueryContext ctx, EntityFilter filter) { + String type; + String name; + switch (filter.getType()) { + case ASSET_TYPE: + type = ((AssetTypeFilter) filter).getAssetType(); + name = ((AssetTypeFilter) filter).getAssetNameFilter(); + break; + case DEVICE_TYPE: + type = ((DeviceTypeFilter) filter).getDeviceType(); + name = ((DeviceTypeFilter) filter).getDeviceNameFilter(); + break; + case ENTITY_VIEW_TYPE: + type = ((EntityViewTypeFilter) filter).getEntityViewType(); + name = ((EntityViewTypeFilter) filter).getEntityViewNameFilter(); + break; + default: + throw new RuntimeException("Not supported!"); + } + ctx.addStringParameter("entity_filter_type_query_type", type); + ctx.addStringParameter("entity_filter_type_query_name", name); + return "e.type = :entity_filter_type_query_type and lower(e.search_text) like lower(concat(:entity_filter_type_query_name, '%%'))"; + } + + private EntityType resolveEntityType(EntityFilter entityFilter) { + switch (entityFilter.getType()) { + case SINGLE_ENTITY: + return ((SingleEntityFilter) entityFilter).getSingleEntity().getEntityType(); + case ENTITY_LIST: + return ((EntityListFilter) entityFilter).getEntityType(); + case ENTITY_NAME: + return ((EntityNameFilter) entityFilter).getEntityType(); + case ASSET_TYPE: + case ASSET_SEARCH_QUERY: + return EntityType.ASSET; + case DEVICE_TYPE: + case DEVICE_SEARCH_QUERY: + return EntityType.DEVICE; + case ENTITY_VIEW_TYPE: + case ENTITY_VIEW_SEARCH_QUERY: + return EntityType.ENTITY_VIEW; + case RELATIONS_QUERY: + return ((RelationsQueryFilter) entityFilter).getRootEntity().getEntityType(); + case API_USAGE_STATE: + return EntityType.API_USAGE_STATE; + default: + throw new RuntimeException("Not implemented!"); + } + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultQueryLogComponent.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultQueryLogComponent.java new file mode 100644 index 0000000000..7cece0cf3a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultQueryLogComponent.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.query; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.Arrays; + +@Component +@Slf4j +public class DefaultQueryLogComponent implements QueryLogComponent { + + @Value("${sql.log_queries:false}") + private boolean logSqlQueries; + @Value("${sql.log_queries_threshold:5000}") + private long logQueriesThreshold; + + @Override + public void logQuery(QueryContext ctx, String query, long duration) { + if (logSqlQueries && duration > logQueriesThreshold) { + log.info("QUERY: {} took {}ms", query, duration); + Arrays.asList(ctx.getParameterNames()).forEach(param -> log.info("QUERY PARAM: {} -> {}", param, ctx.getValue(param))); + } + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityDataAdapter.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityDataAdapter.java new file mode 100644 index 0000000000..c7ab5f122e --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityDataAdapter.java @@ -0,0 +1,106 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.query; + +import org.apache.commons.lang3.math.NumberUtils; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.UUIDConverter; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.TsValue; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +public class EntityDataAdapter { + + public static PageData createEntityData(EntityDataPageLink pageLink, + List selectionMapping, + List> rows, + int totalElements) { + int totalPages = pageLink.getPageSize() > 0 ? (int) Math.ceil((float) totalElements / pageLink.getPageSize()) : 1; + int startIndex = pageLink.getPageSize() * pageLink.getPage(); + boolean hasNext = pageLink.getPageSize() > 0 && totalElements > startIndex + rows.size(); + List entitiesData = convertListToEntityData(rows, selectionMapping); + return new PageData<>(entitiesData, totalPages, totalElements, hasNext); + } + + private static List convertListToEntityData(List> result, List selectionMapping) { + return result.stream().map(row -> toEntityData(row, selectionMapping)).collect(Collectors.toList()); + } + + private static EntityData toEntityData(Map row, List selectionMapping) { + UUID id = (UUID)row.get("id"); + EntityType entityType = EntityType.valueOf((String) row.get("entity_type")); + EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, id); + Map> latest = new HashMap<>(); + Map timeseries = new HashMap<>(); + EntityData entityData = new EntityData(entityId, latest, timeseries); + for (EntityKeyMapping mapping : selectionMapping) { + if (!mapping.isIgnore()) { + EntityKey entityKey = mapping.getEntityKey(); + Object value = row.get(mapping.getValueAlias()); + String strValue; + long ts; + if (entityKey.getType().equals(EntityKeyType.ENTITY_FIELD)) { + strValue = value != null ? value.toString() : ""; + ts = System.currentTimeMillis(); + } else { + strValue = convertValue(value); + Object tsObject = row.get(mapping.getTsAlias()); + ts = tsObject != null ? Long.parseLong(tsObject.toString()) : 0; + } + TsValue tsValue = new TsValue(ts, strValue); + latest.computeIfAbsent(entityKey.getType(), entityKeyType -> new HashMap<>()).put(entityKey.getKey(), tsValue); + } + } + return entityData; + } + + private static String convertValue(Object value) { + if (value != null) { + String strVal = value.toString(); + // check number + if (strVal.length() > 0 && NumberUtils.isParsable(strVal)) { + try { + long longVal = Long.parseLong(strVal); + return Long.toString(longVal); + } catch (NumberFormatException ignored) { + } + try { + double dblVal = Double.parseDouble(strVal); + if (!Double.isInfinite(dblVal)) { + return Double.toString(dblVal); + } + } catch (NumberFormatException ignored) { + } + } + return strVal; + } else { + return ""; + } + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java new file mode 100644 index 0000000000..525483f0a0 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java @@ -0,0 +1,579 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.query; + +import lombok.Data; +import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.query.BooleanFilterPredicate; +import org.thingsboard.server.common.data.query.ComplexFilterPredicate; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityFilter; +import org.thingsboard.server.common.data.query.EntityFilterType; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.FilterPredicateType; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.KeyFilterPredicate; +import org.thingsboard.server.common.data.query.NumericFilterPredicate; +import org.thingsboard.server.common.data.query.StringFilterPredicate; +import org.thingsboard.server.dao.model.ModelConstants; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Data +public class EntityKeyMapping { + + private static final Map> allowedEntityFieldMap = new HashMap<>(); + private static final Map entityFieldColumnMap = new HashMap<>(); + private static final Map> aliases = new HashMap<>(); + + public static final String CREATED_TIME = "createdTime"; + public static final String ENTITY_TYPE = "entityType"; + public static final String NAME = "name"; + public static final String TYPE = "type"; + public static final String LABEL = "label"; + public static final String FIRST_NAME = "firstName"; + public static final String LAST_NAME = "lastName"; + public static final String EMAIL = "email"; + public static final String TITLE = "title"; + public static final String REGION = "region"; + public static final String COUNTRY = "country"; + public static final String STATE = "state"; + public static final String CITY = "city"; + public static final String ADDRESS = "address"; + public static final String ADDRESS_2 = "address2"; + public static final String ZIP = "zip"; + public static final String PHONE = "phone"; + public static final String ADDITIONAL_INFO = "additionalInfo"; + + public static final List typedEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, TYPE, ADDITIONAL_INFO); + public static final List widgetEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME); + public static final List commonEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, ADDITIONAL_INFO); + public static final List dashboardEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, TITLE); + public static final List labeledEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, TYPE, LABEL, ADDITIONAL_INFO); + public static final List contactBasedEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, EMAIL, TITLE, COUNTRY, STATE, CITY, ADDRESS, ADDRESS_2, ZIP, PHONE, ADDITIONAL_INFO); + + public static final Set commonEntityFieldsSet = new HashSet<>(commonEntityFields); + public static final Set relationQueryEntityFieldsSet = new HashSet<>(Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, TYPE, LABEL, FIRST_NAME, LAST_NAME, EMAIL, REGION, TITLE, COUNTRY, STATE, CITY, ADDRESS, ADDRESS_2, ZIP, PHONE, ADDITIONAL_INFO)); + + static { + allowedEntityFieldMap.put(EntityType.DEVICE, new HashSet<>(labeledEntityFields)); + allowedEntityFieldMap.put(EntityType.ASSET, new HashSet<>(labeledEntityFields)); + allowedEntityFieldMap.put(EntityType.ENTITY_VIEW, new HashSet<>(typedEntityFields)); + + allowedEntityFieldMap.put(EntityType.TENANT, new HashSet<>(contactBasedEntityFields)); + allowedEntityFieldMap.get(EntityType.TENANT).add(REGION); + allowedEntityFieldMap.put(EntityType.CUSTOMER, new HashSet<>(contactBasedEntityFields)); + + allowedEntityFieldMap.put(EntityType.USER, new HashSet<>(Arrays.asList(CREATED_TIME, FIRST_NAME, LAST_NAME, EMAIL, ADDITIONAL_INFO))); + + allowedEntityFieldMap.put(EntityType.DASHBOARD, new HashSet<>(dashboardEntityFields)); + allowedEntityFieldMap.put(EntityType.RULE_CHAIN, new HashSet<>(commonEntityFields)); + allowedEntityFieldMap.put(EntityType.RULE_NODE, new HashSet<>(commonEntityFields)); + allowedEntityFieldMap.put(EntityType.WIDGET_TYPE, new HashSet<>(widgetEntityFields)); + allowedEntityFieldMap.put(EntityType.WIDGETS_BUNDLE, new HashSet<>(widgetEntityFields)); + + entityFieldColumnMap.put(CREATED_TIME, ModelConstants.CREATED_TIME_PROPERTY); + entityFieldColumnMap.put(ENTITY_TYPE, ModelConstants.ENTITY_TYPE_PROPERTY); + entityFieldColumnMap.put(REGION, ModelConstants.TENANT_REGION_PROPERTY); + entityFieldColumnMap.put(NAME, "name"); + entityFieldColumnMap.put(TYPE, "type"); + entityFieldColumnMap.put(LABEL, "label"); + entityFieldColumnMap.put(FIRST_NAME, ModelConstants.USER_FIRST_NAME_PROPERTY); + entityFieldColumnMap.put(LAST_NAME, ModelConstants.USER_LAST_NAME_PROPERTY); + entityFieldColumnMap.put(EMAIL, ModelConstants.EMAIL_PROPERTY); + entityFieldColumnMap.put(TITLE, ModelConstants.TITLE_PROPERTY); + entityFieldColumnMap.put(COUNTRY, ModelConstants.COUNTRY_PROPERTY); + entityFieldColumnMap.put(STATE, ModelConstants.STATE_PROPERTY); + entityFieldColumnMap.put(CITY, ModelConstants.CITY_PROPERTY); + entityFieldColumnMap.put(ADDRESS, ModelConstants.ADDRESS_PROPERTY); + entityFieldColumnMap.put(ADDRESS_2, ModelConstants.ADDRESS2_PROPERTY); + entityFieldColumnMap.put(ZIP, ModelConstants.ZIP_PROPERTY); + entityFieldColumnMap.put(PHONE, ModelConstants.PHONE_PROPERTY); + entityFieldColumnMap.put(ADDITIONAL_INFO, ModelConstants.ADDITIONAL_INFO_PROPERTY); + + Map contactBasedAliases = new HashMap<>(); + contactBasedAliases.put(NAME, TITLE); + contactBasedAliases.put(LABEL, TITLE); + aliases.put(EntityType.TENANT, contactBasedAliases); + aliases.put(EntityType.CUSTOMER, contactBasedAliases); + aliases.put(EntityType.DASHBOARD, contactBasedAliases); + Map commonEntityAliases = new HashMap<>(); + commonEntityAliases.put(TITLE, NAME); + aliases.put(EntityType.DEVICE, commonEntityAliases); + aliases.put(EntityType.ASSET, commonEntityAliases); + aliases.put(EntityType.ENTITY_VIEW, commonEntityAliases); + aliases.put(EntityType.WIDGETS_BUNDLE, commonEntityAliases); + + Map userEntityAliases = new HashMap<>(); + userEntityAliases.put(TITLE, EMAIL); + userEntityAliases.put(LABEL, EMAIL); + userEntityAliases.put(NAME, EMAIL); + aliases.put(EntityType.USER, userEntityAliases); + } + + private int index; + private String alias; + private boolean isLatest; + private boolean isSelection; + private boolean isSearchable; + private boolean isSortOrder; + private boolean ignore = false; + private List keyFilters; + private EntityKey entityKey; + private int paramIdx = 0; + + public boolean hasFilter() { + return keyFilters != null && !keyFilters.isEmpty(); + } + + public String getValueAlias() { + if (entityKey.getType().equals(EntityKeyType.ENTITY_FIELD)) { + return alias; + } else { + return alias + "_value"; + } + } + + public String getTsAlias() { + return alias + "_ts"; + } + + public String toSelection(EntityFilterType filterType, EntityType entityType) { + if (entityKey.getType().equals(EntityKeyType.ENTITY_FIELD)) { + if (entityKey.getKey().equals("entityType") && !filterType.equals(EntityFilterType.RELATIONS_QUERY)) { + return String.format("'%s' as %s", entityType.name(), getValueAlias()); + } else { + Set existingEntityFields = getExistingEntityFields(filterType, entityType); + String alias = getEntityFieldAlias(filterType, entityType); + if (existingEntityFields.contains(alias)) { + String column = entityFieldColumnMap.get(alias); + return String.format("cast(e.%s as varchar) as %s", column, getValueAlias()); + } else { + return String.format("'' as %s", getValueAlias()); + } + } + } else if (entityKey.getType().equals(EntityKeyType.TIME_SERIES)) { + return buildTimeSeriesSelection(); + } else { + return buildAttributeSelection(); + } + } + + private String getEntityFieldAlias(EntityFilterType filterType, EntityType entityType) { + String alias; + if (filterType.equals(EntityFilterType.RELATIONS_QUERY)) { + alias = entityKey.getKey(); + } else { + alias = getAliasByEntityKeyAndType(entityKey.getKey(), entityType); + } + return alias; + } + + private Set getExistingEntityFields(EntityFilterType filterType, EntityType entityType) { + Set existingEntityFields; + if (filterType.equals(EntityFilterType.RELATIONS_QUERY)) { + existingEntityFields = relationQueryEntityFieldsSet; + } else { + existingEntityFields = allowedEntityFieldMap.get(entityType); + if (existingEntityFields == null) { + existingEntityFields = commonEntityFieldsSet; + } + } + return existingEntityFields; + } + + private String getAliasByEntityKeyAndType(String key, EntityType entityType) { + String alias; + Map entityAliases = aliases.get(entityType); + if (entityAliases != null) { + alias = entityAliases.get(key); + } else { + alias = null; + } + if (alias == null) { + alias = key; + } + return alias; + } + + public Stream toQueries(QueryContext ctx, EntityFilterType filterType) { + if (hasFilter()) { + String keyAlias = entityKey.getType().equals(EntityKeyType.ENTITY_FIELD) ? "e" : alias; + return keyFilters.stream().map(keyFilter -> + this.buildKeyQuery(ctx, keyAlias, keyFilter, filterType)); + } else { + return Stream.empty(); + } + } + + public String toLatestJoin(QueryContext ctx, EntityFilter entityFilter, EntityType entityType) { + String entityTypeStr; + if (entityFilter.getType().equals(EntityFilterType.RELATIONS_QUERY)) { + entityTypeStr = "entities.entity_type"; + } else { + entityTypeStr = "'" + entityType.name() + "'"; + } + ctx.addStringParameter(alias + "_key_id", entityKey.getKey()); + String filterQuery = toQueries(ctx, entityFilter.getType()).filter(Objects::nonNull).collect( + Collectors.joining(" and ")); + if (StringUtils.isEmpty(filterQuery)) { + filterQuery = ""; + } else { + filterQuery = " AND (" + filterQuery + ")"; + } + if (entityKey.getType().equals(EntityKeyType.TIME_SERIES)) { + String join = hasFilter() ? "inner join" : "left join"; + return String.format("%s ts_kv_latest %s ON %s.entity_id=entities.id AND %s.key = (select key_id from ts_kv_dictionary where key = :%s_key_id) %s", + join, alias, alias, alias, alias, filterQuery); + } else { + String query; + if (!entityKey.getType().equals(EntityKeyType.ATTRIBUTE)) { + String join = hasFilter() ? "inner join" : "left join"; + query = String.format("%s attribute_kv %s ON %s.entity_id=entities.id AND %s.entity_type=%s AND %s.attribute_key=:%s_key_id ", + join, alias, alias, alias, entityTypeStr, alias, alias); + String scope; + if (entityKey.getType().equals(EntityKeyType.CLIENT_ATTRIBUTE)) { + scope = DataConstants.CLIENT_SCOPE; + } else if (entityKey.getType().equals(EntityKeyType.SHARED_ATTRIBUTE)) { + scope = DataConstants.SHARED_SCOPE; + } else { + scope = DataConstants.SERVER_SCOPE; + } + query = String.format("%s AND %s.attribute_type='%s' %s", query, alias, scope, filterQuery); + } else { + String join = hasFilter() ? "join LATERAL" : "left join LATERAL"; + query = String.format("%s (select * from attribute_kv %s WHERE %s.entity_id=entities.id AND %s.entity_type=%s AND %s.attribute_key=:%s_key_id %s " + + "ORDER BY %s.last_update_ts DESC limit 1) as %s ON true", + join, alias, alias, alias, entityTypeStr, alias, alias, filterQuery, alias, alias); + } + return query; + } + } + + public static String buildSelections(List mappings, EntityFilterType filterType, EntityType entityType) { + return mappings.stream().map(mapping -> mapping.toSelection(filterType, entityType)).collect( + Collectors.joining(", ")); + } + + public static String buildLatestJoins(QueryContext ctx, EntityFilter entityFilter, EntityType entityType, List latestMappings, boolean countQuery) { + return latestMappings.stream().filter(mapping -> !countQuery || mapping.hasFilter()) + .map(mapping -> mapping.toLatestJoin(ctx, entityFilter, entityType)).collect( + Collectors.joining(" ")); + } + + public static String buildQuery(QueryContext ctx, List mappings, EntityFilterType filterType) { + return mappings.stream().flatMap(mapping -> mapping.toQueries(ctx, filterType)).filter(Objects::nonNull).collect( + Collectors.joining(" AND ")); + } + + public static List prepareKeyMapping(EntityDataQuery query) { + List entityFields = query.getEntityFields() != null ? query.getEntityFields() : Collections.emptyList(); + List latestValues = query.getLatestValues() != null ? query.getLatestValues() : Collections.emptyList(); + Map> filters = + query.getKeyFilters() != null ? + query.getKeyFilters().stream().collect(Collectors.groupingBy(KeyFilter::getKey)) : Collections.emptyMap(); + EntityDataSortOrder sortOrder = query.getPageLink().getSortOrder(); + EntityKey sortOrderKey = sortOrder != null ? sortOrder.getKey() : null; + int index = 2; + List entityFieldsMappings = entityFields.stream().map( + key -> { + EntityKeyMapping mapping = new EntityKeyMapping(); + mapping.setLatest(false); + mapping.setSelection(true); + mapping.setSearchable(!key.getKey().equals(ADDITIONAL_INFO)); + mapping.setEntityKey(key); + return mapping; + } + ).collect(Collectors.toList()); + List latestMappings = latestValues.stream().map( + key -> { + EntityKeyMapping mapping = new EntityKeyMapping(); + mapping.setLatest(true); + mapping.setSearchable(true); + mapping.setSelection(true); + mapping.setEntityKey(key); + return mapping; + } + ).collect(Collectors.toList()); + if (sortOrderKey != null) { + Optional existing; + if (sortOrderKey.getType().equals(EntityKeyType.ENTITY_FIELD)) { + existing = + entityFieldsMappings.stream().filter(mapping -> mapping.entityKey.equals(sortOrderKey)).findFirst(); + } else { + existing = + latestMappings.stream().filter(mapping -> mapping.entityKey.equals(sortOrderKey)).findFirst(); + } + if (existing.isPresent()) { + existing.get().setSortOrder(true); + } else { + EntityKeyMapping sortOrderMapping = new EntityKeyMapping(); + sortOrderMapping.setLatest(!sortOrderKey.getType().equals(EntityKeyType.ENTITY_FIELD)); + sortOrderMapping.setSelection(true); + sortOrderMapping.setEntityKey(sortOrderKey); + sortOrderMapping.setSortOrder(true); + sortOrderMapping.setIgnore(true); + if (sortOrderKey.getType().equals(EntityKeyType.ENTITY_FIELD)) { + entityFieldsMappings.add(sortOrderMapping); + } else { + latestMappings.add(sortOrderMapping); + } + } + } + List mappings = new ArrayList<>(); + mappings.addAll(entityFieldsMappings); + mappings.addAll(latestMappings); + for (EntityKeyMapping mapping : mappings) { + mapping.setIndex(index); + mapping.setAlias(String.format("alias%s", index)); + mapping.setKeyFilters(filters.remove(mapping.entityKey)); + if (mapping.getEntityKey().getType().equals(EntityKeyType.ENTITY_FIELD)) { + index++; + } else { + index += 2; + } + } + if (!filters.isEmpty()) { + for (EntityKey filterField : filters.keySet()) { + EntityKeyMapping mapping = new EntityKeyMapping(); + mapping.setIndex(index); + mapping.setAlias(String.format("alias%s", index)); + mapping.setKeyFilters(filters.get(filterField)); + mapping.setLatest(!filterField.getType().equals(EntityKeyType.ENTITY_FIELD)); + mapping.setSelection(false); + mapping.setEntityKey(filterField); + mappings.add(mapping); + index += 1; + } + } + + return mappings; + } + + private String buildAttributeSelection() { + return buildTimeSeriesOrAttrSelection(true); + } + + private String buildTimeSeriesSelection() { + return buildTimeSeriesOrAttrSelection(false); + } + + private String buildTimeSeriesOrAttrSelection(boolean attr) { + String attrValAlias = getValueAlias(); + String attrTsAlias = getTsAlias(); + String attrValSelection = + String.format("(coalesce(cast(%s.bool_v as varchar), '') || " + + "coalesce(%s.str_v, '') || " + + "coalesce(cast(%s.long_v as varchar), '') || " + + "coalesce(cast(%s.dbl_v as varchar), '') || " + + "coalesce(cast(%s.json_v as varchar), '')) as %s", alias, alias, alias, alias, alias, attrValAlias); + String attrTsSelection = String.format("%s.%s as %s", alias, attr ? "last_update_ts" : "ts", attrTsAlias); + if (this.isSortOrder) { + String attrNumAlias = getSortOrderNumAlias(); + String attrVarcharAlias = getSortOrderStrAlias(); + String attrSortOrderSelection = + String.format("coalesce(%s.dbl_v, cast(%s.long_v as double precision), (case when %s.bool_v then 1 else 0 end)) %s," + + "coalesce(%s.str_v, cast(%s.json_v as varchar), '') %s", alias, alias, alias, attrNumAlias, alias, alias, attrVarcharAlias); + return String.join(", ", attrValSelection, attrTsSelection, attrSortOrderSelection); + } else { + return String.join(", ", attrValSelection, attrTsSelection); + } + } + + public String getSortOrderStrAlias() { + return getValueAlias() + "_so_varchar"; + } + + public String getSortOrderNumAlias() { + return getValueAlias() + "_so_num"; + } + + private String buildKeyQuery(QueryContext ctx, String alias, KeyFilter keyFilter, + EntityFilterType filterType) { + return this.buildPredicateQuery(ctx, alias, keyFilter.getKey(), keyFilter.getPredicate(), filterType); + } + + private String buildPredicateQuery(QueryContext ctx, String alias, EntityKey key, + KeyFilterPredicate predicate, EntityFilterType filterType) { + if (predicate.getType().equals(FilterPredicateType.COMPLEX)) { + return this.buildComplexPredicateQuery(ctx, alias, key, (ComplexFilterPredicate) predicate, filterType); + } else { + return this.buildSimplePredicateQuery(ctx, alias, key, predicate, filterType); + } + } + + private String buildComplexPredicateQuery(QueryContext ctx, String alias, EntityKey key, + ComplexFilterPredicate predicate, EntityFilterType filterType) { + String result = predicate.getPredicates().stream() + .map(keyFilterPredicate -> this.buildPredicateQuery(ctx, alias, key, keyFilterPredicate, filterType)) + .filter(Objects::nonNull).collect(Collectors.joining( + " " + predicate.getOperation().name() + " " + )); + if (!result.trim().isEmpty()) { + result = "( " + result + " )"; + } + return result; + } + + private String buildSimplePredicateQuery(QueryContext ctx, String alias, EntityKey key, + KeyFilterPredicate predicate, EntityFilterType filterType) { + if (key.getType().equals(EntityKeyType.ENTITY_FIELD)) { + Set existingEntityFields = getExistingEntityFields(filterType, ctx.getEntityType()); + String entityFieldAlias = getEntityFieldAlias(filterType, ctx.getEntityType()); + String column = null; + if (existingEntityFields.contains(entityFieldAlias)) { + column = entityFieldColumnMap.get(entityFieldAlias); + } + if (column != null) { + String field = alias + "." + column; + if (predicate.getType().equals(FilterPredicateType.NUMERIC)) { + return this.buildNumericPredicateQuery(ctx, field, (NumericFilterPredicate) predicate); + } else if (predicate.getType().equals(FilterPredicateType.STRING)) { + if (key.getKey().equals("entityType") && !filterType.equals(EntityFilterType.RELATIONS_QUERY)) { + field = ctx.getEntityType().toString(); + return this.buildStringPredicateQuery(ctx, field, (StringFilterPredicate) predicate) + .replace("lower(" + field, "lower('" + field + "'") + .replace(field + " ", "'" + field + "' "); + } else { + return this.buildStringPredicateQuery(ctx, field, (StringFilterPredicate) predicate); + } + } else { + return this.buildBooleanPredicateQuery(ctx, field, (BooleanFilterPredicate) predicate); + } + } else { + return null; + } + } else { + if (predicate.getType().equals(FilterPredicateType.NUMERIC)) { + String longQuery = this.buildNumericPredicateQuery(ctx, alias + ".long_v", (NumericFilterPredicate) predicate); + String doubleQuery = this.buildNumericPredicateQuery(ctx, alias + ".dbl_v", (NumericFilterPredicate) predicate); + return String.format("(%s or %s)", longQuery, doubleQuery); + } else { + String column = predicate.getType().equals(FilterPredicateType.STRING) ? "str_v" : "bool_v"; + String field = alias + "." + column; + if (predicate.getType().equals(FilterPredicateType.STRING)) { + return this.buildStringPredicateQuery(ctx, field, (StringFilterPredicate) predicate); + } else { + return this.buildBooleanPredicateQuery(ctx, field, (BooleanFilterPredicate) predicate); + } + } + } + } + + private String buildStringPredicateQuery(QueryContext ctx, String field, StringFilterPredicate stringFilterPredicate) { + String operationField = field; + String paramName = getNextParameterName(field); + String value = stringFilterPredicate.getValue().getValue(); + String stringOperationQuery = ""; + if (stringFilterPredicate.isIgnoreCase()) { + value = value.toLowerCase(); + operationField = String.format("lower(%s)", operationField); + } + switch (stringFilterPredicate.getOperation()) { + case EQUAL: + stringOperationQuery = String.format("%s = :%s) or (%s is null and :%s = '')", operationField, paramName, operationField, paramName); + break; + case NOT_EQUAL: + stringOperationQuery = String.format("%s != :%s) or (%s is null and :%s != '')", operationField, paramName, operationField, paramName); + break; + case STARTS_WITH: + value += "%"; + stringOperationQuery = String.format("%s like :%s) or (%s is null and :%s = '%%')", operationField, paramName, operationField, paramName); + break; + case ENDS_WITH: + value = "%" + value; + stringOperationQuery = String.format("%s like :%s) or (%s is null and :%s = '%%')", operationField, paramName, operationField, paramName); + break; + case CONTAINS: + if (value.length() > 0) { + value = "%" + value + "%"; + } + stringOperationQuery = String.format("%s like :%s) or (%s is null and :%s = '')", operationField, paramName, operationField, paramName); + break; + case NOT_CONTAINS: + if (value.length() > 0) { + value = "%" + value + "%"; + } + stringOperationQuery = String.format("%s not like :%s) or (%s is null and :%s != '')", operationField, paramName, operationField, paramName); + break; + } + ctx.addStringParameter(paramName, value); + return String.format("((%s is not null and %s)", field, stringOperationQuery); + } + + private String buildNumericPredicateQuery(QueryContext ctx, String field, NumericFilterPredicate numericFilterPredicate) { + String paramName = getNextParameterName(field); + ctx.addDoubleParameter(paramName, numericFilterPredicate.getValue().getValue()); + String numericOperationQuery = ""; + switch (numericFilterPredicate.getOperation()) { + case EQUAL: + numericOperationQuery = String.format("%s = :%s", field, paramName); + break; + case NOT_EQUAL: + numericOperationQuery = String.format("%s != :%s", field, paramName); + break; + case GREATER: + numericOperationQuery = String.format("%s > :%s", field, paramName); + break; + case GREATER_OR_EQUAL: + numericOperationQuery = String.format("%s >= :%s", field, paramName); + break; + case LESS: + numericOperationQuery = String.format("%s < :%s", field, paramName); + break; + case LESS_OR_EQUAL: + numericOperationQuery = String.format("%s <= :%s", field, paramName); + break; + } + return String.format("(%s is not null and %s)", field, numericOperationQuery); + } + + private String buildBooleanPredicateQuery(QueryContext ctx, String field, + BooleanFilterPredicate booleanFilterPredicate) { + String paramName = getNextParameterName(field); + ctx.addBooleanParameter(paramName, booleanFilterPredicate.getValue().getValue()); + String booleanOperationQuery = ""; + switch (booleanFilterPredicate.getOperation()) { + case EQUAL: + booleanOperationQuery = String.format("%s = :%s", field, paramName); + break; + case NOT_EQUAL: + booleanOperationQuery = String.format("%s != :%s", field, paramName); + break; + } + return String.format("(%s is not null and %s)", field, booleanOperationQuery); + } + + private String getNextParameterName(String field) { + paramIdx++; + return field.replace(".", "_") + "_" + paramIdx; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityQueryRepository.java new file mode 100644 index 0000000000..fbb0b83c44 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityQueryRepository.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.query; + +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityDataQuery; + +public interface EntityQueryRepository { + + long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query); + + PageData findEntityDataByQuery(TenantId tenantId, CustomerId customerId, EntityDataQuery query); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/JpaEntityQueryDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/JpaEntityQueryDao.java new file mode 100644 index 0000000000..2c77c5d833 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/JpaEntityQueryDao.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.query; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.dao.entity.EntityQueryDao; + +@Component +public class JpaEntityQueryDao implements EntityQueryDao { + + @Autowired + private EntityQueryRepository entityQueryRepository; + + @Override + public long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query) { + return entityQueryRepository.countEntitiesByQuery(tenantId, customerId, query); + } + + @Override + public PageData findEntityDataByQuery(TenantId tenantId, CustomerId customerId, EntityDataQuery query) { + return entityQueryRepository.findEntityDataByQuery(tenantId, customerId, query); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/QueryContext.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/QueryContext.java new file mode 100644 index 0000000000..ec862e56d6 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/QueryContext.java @@ -0,0 +1,149 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.query; + +import lombok.extern.slf4j.Slf4j; +import org.hibernate.type.PostgresUUIDType; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.sql.Types; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@Slf4j +public class QueryContext implements SqlParameterSource { + private static final PostgresUUIDType UUID_TYPE = new PostgresUUIDType(); + + private final QuerySecurityContext securityCtx; + private final StringBuilder query; + private final Map params; + + public QueryContext(QuerySecurityContext securityCtx) { + this.securityCtx = securityCtx; + query = new StringBuilder(); + params = new HashMap<>(); + } + + void addParameter(String name, Object value, int type, String typeName) { + Parameter newParam = new Parameter(value, type, typeName); + Parameter oldParam = params.put(name, newParam); + if (oldParam != null && oldParam.value != null && !oldParam.value.equals(newParam.value)) { + throw new RuntimeException("Parameter with name: " + name + " was already registered!"); + } + if(value == null){ + log.warn("[{}][{}][{}] Trying to set null value", getTenantId(), getCustomerId(), name); + } + } + + public void append(String s) { + query.append(s); + } + + @Override + public boolean hasValue(String paramName) { + return params.containsKey(paramName); + } + + @Override + public Object getValue(String paramName) throws IllegalArgumentException { + return checkParameter(paramName).value; + } + + @Override + public int getSqlType(String paramName) { + return checkParameter(paramName).type; + } + + private Parameter checkParameter(String paramName) { + Parameter param = params.get(paramName); + if (param == null) { + throw new RuntimeException("Parameter with name: " + paramName + " is not set!"); + } + return param; + } + + @Override + public String getTypeName(String paramName) { + return params.get(paramName).name; + } + + @Override + public String[] getParameterNames() { + return params.keySet().toArray(new String[]{}); + } + + public void addUuidParameter(String name, UUID value) { + addParameter(name, value, UUID_TYPE.sqlType(), UUID_TYPE.getName()); + } + + public void addStringParameter(String name, String value) { + addParameter(name, value, Types.VARCHAR, "VARCHAR"); + } + + public void addDoubleParameter(String name, double value) { + addParameter(name, value, Types.DOUBLE, "DOUBLE"); + } + + public void addLongParameter(String name, long value) { + addParameter(name, value, Types.BIGINT, "BIGINT"); + } + + public void addStringListParameter(String name, List value) { + addParameter(name, value, Types.VARCHAR, "VARCHAR"); + } + + public void addBooleanParameter(String name, boolean value) { + addParameter(name, value, Types.BOOLEAN, "BOOLEAN"); + } + + public void addUuidListParameter(String name, List value) { + addParameter(name, value, UUID_TYPE.sqlType(), UUID_TYPE.getName()); + } + + public String getQuery() { + return query.toString(); + } + + + public static class Parameter { + private final Object value; + private final int type; + private final String name; + + public Parameter(Object value, int type, String name) { + this.value = value; + this.type = type; + this.name = name; + } + } + + public TenantId getTenantId() { + return securityCtx.getTenantId(); + } + + public CustomerId getCustomerId() { + return securityCtx.getCustomerId(); + } + + public EntityType getEntityType() { + return securityCtx.getEntityType(); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/QueryLogComponent.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/QueryLogComponent.java new file mode 100644 index 0000000000..a775626766 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/QueryLogComponent.java @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.query; + +public interface QueryLogComponent { + + void logQuery(QueryContext ctx, String query, long duration); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/QuerySecurityContext.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/QuerySecurityContext.java new file mode 100644 index 0000000000..7cabb693c1 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/QuerySecurityContext.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.query; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.hibernate.type.PostgresUUIDType; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.sql.Types; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@AllArgsConstructor +public class QuerySecurityContext { + + @Getter + private final TenantId tenantId; + @Getter + private final CustomerId customerId; + @Getter + private final EntityType entityType; + +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/HsqlRelationInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/HsqlRelationInsertRepository.java index 8438999302..e017831607 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/HsqlRelationInsertRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/HsqlRelationInsertRepository.java @@ -20,19 +20,35 @@ import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.dao.model.sql.RelationCompositeKey; import org.thingsboard.server.dao.model.sql.RelationEntity; import org.thingsboard.server.dao.util.HsqlDao; -import org.thingsboard.server.dao.util.SqlDao; + +import javax.persistence.Query; @HsqlDao -@SqlDao @Repository @Transactional public class HsqlRelationInsertRepository extends AbstractRelationInsertRepository implements RelationInsertRepository { private static final String INSERT_ON_CONFLICT_DO_UPDATE = "MERGE INTO relation USING (VALUES :fromId, :fromType, :toId, :toType, :relationTypeGroup, :relationType, :additionalInfo) R " + "(from_id, from_type, to_id, to_type, relation_type_group, relation_type, additional_info) " + - "ON (relation.from_id = R.from_id AND relation.from_type = R.from_type AND relation.relation_type_group = R.relation_type_group AND relation.relation_type = R.relation_type AND relation.to_id = R.to_id AND relation.to_type = R.to_type) " + + "ON (relation.from_id = UUID(R.from_id) AND relation.from_type = R.from_type AND relation.relation_type_group = R.relation_type_group AND relation.relation_type = R.relation_type AND relation.to_id = UUID(R.to_id) AND relation.to_type = R.to_type) " + "WHEN MATCHED THEN UPDATE SET relation.additional_info = R.additional_info " + - "WHEN NOT MATCHED THEN INSERT (from_id, from_type, to_id, to_type, relation_type_group, relation_type, additional_info) VALUES (R.from_id, R.from_type, R.to_id, R.to_type, R.relation_type_group, R.relation_type, R.additional_info)"; + "WHEN NOT MATCHED THEN INSERT (from_id, from_type, to_id, to_type, relation_type_group, relation_type, additional_info) VALUES (UUID(R.from_id), R.from_type, UUID(R.to_id), R.to_type, R.relation_type_group, R.relation_type, R.additional_info)"; + + protected Query getQuery(RelationEntity entity, String query) { + Query nativeQuery = entityManager.createNativeQuery(query, RelationEntity.class); + if (entity.getAdditionalInfo() == null) { + nativeQuery.setParameter("additionalInfo", null); + } else { + nativeQuery.setParameter("additionalInfo", entity.getAdditionalInfo().toString()); + } + return nativeQuery + .setParameter("fromId", entity.getFromId().toString()) + .setParameter("fromType", entity.getFromType()) + .setParameter("toId", entity.getToId().toString()) + .setParameter("toType", entity.getToType()) + .setParameter("relationTypeGroup", entity.getRelationTypeGroup()) + .setParameter("relationType", entity.getRelationType()); + } @Override public RelationEntity saveOrUpdate(RelationEntity entity) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java index d55a9944b6..3b6e7a83cb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java @@ -18,16 +18,11 @@ package org.thingsboard.server.dao.sql.relation; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.dao.DaoUtil; @@ -35,21 +30,16 @@ import org.thingsboard.server.dao.model.sql.RelationCompositeKey; import org.thingsboard.server.dao.model.sql.RelationEntity; import org.thingsboard.server.dao.relation.RelationDao; import org.thingsboard.server.dao.sql.JpaAbstractDaoListeningExecutorService; -import org.thingsboard.server.dao.sql.JpaAbstractSearchTimeDao; -import org.thingsboard.server.dao.util.SqlDao; import javax.persistence.criteria.Predicate; import java.util.ArrayList; import java.util.List; -import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID; - /** * Created by Valerii Sosliuk on 5/29/2017. */ @Slf4j @Component -@SqlDao public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService implements RelationDao { @Autowired @@ -62,7 +52,7 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple public ListenableFuture> findAllByFrom(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup) { return service.submit(() -> DaoUtil.convertDataList( relationRepository.findAllByFromIdAndFromTypeAndRelationTypeGroup( - UUIDConverter.fromTimeUUID(from.getId()), + from.getId(), from.getEntityType().name(), typeGroup.name()))); } @@ -71,7 +61,7 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple public ListenableFuture> findAllByFromAndType(TenantId tenantId, EntityId from, String relationType, RelationTypeGroup typeGroup) { return service.submit(() -> DaoUtil.convertDataList( relationRepository.findAllByFromIdAndFromTypeAndRelationTypeAndRelationTypeGroup( - UUIDConverter.fromTimeUUID(from.getId()), + from.getId(), from.getEntityType().name(), relationType, typeGroup.name()))); @@ -81,7 +71,7 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple public ListenableFuture> findAllByTo(TenantId tenantId, EntityId to, RelationTypeGroup typeGroup) { return service.submit(() -> DaoUtil.convertDataList( relationRepository.findAllByToIdAndToTypeAndRelationTypeGroup( - UUIDConverter.fromTimeUUID(to.getId()), + to.getId(), to.getEntityType().name(), typeGroup.name()))); } @@ -90,7 +80,7 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple public ListenableFuture> findAllByToAndType(TenantId tenantId, EntityId to, String relationType, RelationTypeGroup typeGroup) { return service.submit(() -> DaoUtil.convertDataList( relationRepository.findAllByToIdAndToTypeAndRelationTypeAndRelationTypeGroup( - UUIDConverter.fromTimeUUID(to.getId()), + to.getId(), to.getEntityType().name(), relationType, typeGroup.name()))); @@ -109,9 +99,9 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple } private RelationCompositeKey getRelationCompositeKey(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { - return new RelationCompositeKey(fromTimeUUID(from.getId()), + return new RelationCompositeKey(from.getId(), from.getEntityType().name(), - fromTimeUUID(to.getId()), + to.getId(), to.getEntityType().name(), relationType, typeGroup.name()); @@ -164,10 +154,10 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple @Override public boolean deleteOutboundRelations(TenantId tenantId, EntityId entity) { boolean relationExistsBeforeDelete = relationRepository - .findAllByFromIdAndFromType(UUIDConverter.fromTimeUUID(entity.getId()), entity.getEntityType().name()) + .findAllByFromIdAndFromType(entity.getId(), entity.getEntityType().name()) .size() > 0; if (relationExistsBeforeDelete) { - relationRepository.deleteByFromIdAndFromType(UUIDConverter.fromTimeUUID(entity.getId()), entity.getEntityType().name()); + relationRepository.deleteByFromIdAndFromType(entity.getId(), entity.getEntityType().name()); } return relationExistsBeforeDelete; } @@ -177,30 +167,20 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple return service.submit( () -> { boolean relationExistsBeforeDelete = relationRepository - .findAllByFromIdAndFromType(UUIDConverter.fromTimeUUID(entity.getId()), entity.getEntityType().name()) + .findAllByFromIdAndFromType(entity.getId(), entity.getEntityType().name()) .size() > 0; if (relationExistsBeforeDelete) { - relationRepository.deleteByFromIdAndFromType(UUIDConverter.fromTimeUUID(entity.getId()), entity.getEntityType().name()); + relationRepository.deleteByFromIdAndFromType(entity.getId(), entity.getEntityType().name()); } return relationExistsBeforeDelete; }); } - @Override - public ListenableFuture> findRelations(TenantId tenantId, EntityId from, String relationType, RelationTypeGroup typeGroup, EntityType childType, TimePageLink pageLink) { - Specification timeSearchSpec = JpaAbstractSearchTimeDao.getTimeSearchPageSpec(pageLink, "toId"); - Specification fieldsSpec = getEntityFieldsSpec(from, relationType, typeGroup, childType); - Sort.Direction sortDirection = pageLink.isAscOrder() ? Sort.Direction.ASC : Sort.Direction.DESC; - Pageable pageable = PageRequest.of(0, pageLink.getLimit(), sortDirection, "toId"); - return service.submit(() -> - DaoUtil.convertDataList(relationRepository.findAll(Specification.where(timeSearchSpec).and(fieldsSpec), pageable).getContent())); - } - private Specification getEntityFieldsSpec(EntityId from, String relationType, RelationTypeGroup typeGroup, EntityType childType) { return (root, criteriaQuery, criteriaBuilder) -> { List predicates = new ArrayList<>(); if (from != null) { - Predicate fromIdPredicate = criteriaBuilder.equal(root.get("fromId"), UUIDConverter.fromTimeUUID(from.getId())); + Predicate fromIdPredicate = criteriaBuilder.equal(root.get("fromId"), from.getId()); predicates.add(fromIdPredicate); Predicate fromEntityTypePredicate = criteriaBuilder.equal(root.get("fromType"), from.getEntityType().name()); predicates.add(fromEntityTypePredicate); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/PsqlRelationInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/PsqlRelationInsertRepository.java index dbef233811..7325fc0fbc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/PsqlRelationInsertRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/PsqlRelationInsertRepository.java @@ -19,10 +19,8 @@ import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.dao.model.sql.RelationEntity; import org.thingsboard.server.dao.util.PsqlDao; -import org.thingsboard.server.dao.util.SqlDao; @PsqlDao -@SqlDao @Repository @Transactional public class PsqlRelationInsertRepository extends AbstractRelationInsertRepository implements RelationInsertRepository { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java index 8eee388176..ada7a44565 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java @@ -20,33 +20,32 @@ import org.springframework.data.repository.CrudRepository; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.dao.model.sql.RelationCompositeKey; import org.thingsboard.server.dao.model.sql.RelationEntity; -import org.thingsboard.server.dao.util.SqlDao; import java.util.List; +import java.util.UUID; -@SqlDao public interface RelationRepository extends CrudRepository, JpaSpecificationExecutor { - List findAllByFromIdAndFromTypeAndRelationTypeGroup(String fromId, + List findAllByFromIdAndFromTypeAndRelationTypeGroup(UUID fromId, String fromType, String relationTypeGroup); - List findAllByFromIdAndFromTypeAndRelationTypeAndRelationTypeGroup(String fromId, + List findAllByFromIdAndFromTypeAndRelationTypeAndRelationTypeGroup(UUID fromId, String fromType, String relationType, String relationTypeGroup); - List findAllByToIdAndToTypeAndRelationTypeGroup(String toId, + List findAllByToIdAndToTypeAndRelationTypeGroup(UUID toId, String toType, String relationTypeGroup); - List findAllByToIdAndToTypeAndRelationTypeAndRelationTypeGroup(String toId, + List findAllByToIdAndToTypeAndRelationTypeAndRelationTypeGroup(UUID toId, String toType, String relationType, String relationTypeGroup); - List findAllByFromIdAndFromType(String fromId, + List findAllByFromIdAndFromType(UUID fromId, String fromType); @Transactional @@ -56,5 +55,5 @@ public interface RelationRepository void deleteById(RelationCompositeKey id); @Transactional - void deleteByFromIdAndFromType(String fromId, String fromType); + void deleteByFromIdAndFromType(UUID fromId, String fromType); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java index b1e4745223..57d1545a94 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java @@ -17,27 +17,21 @@ package org.thingsboard.server.dao.sql.rule; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.PageRequest; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.UUIDConverter; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.sql.RuleChainEntity; import org.thingsboard.server.dao.rule.RuleChainDao; import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; -import org.thingsboard.server.dao.util.SqlDao; -import java.util.List; import java.util.Objects; import java.util.UUID; -import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID_STR; - @Slf4j @Component -@SqlDao public class JpaRuleChainDao extends JpaAbstractSearchTextDao implements RuleChainDao { @Autowired @@ -54,13 +48,12 @@ public class JpaRuleChainDao extends JpaAbstractSearchTextDao findRuleChainsByTenantId(UUID tenantId, TextPageLink pageLink) { - return DaoUtil.convertDataList(ruleChainRepository + public PageData findRuleChainsByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.toPageData(ruleChainRepository .findByTenantId( - UUIDConverter.fromTimeUUID(tenantId), + tenantId, Objects.toString(pageLink.getTextSearch(), ""), - pageLink.getIdOffset() == null ? NULL_UUID_STR : UUIDConverter.fromTimeUUID(pageLink.getIdOffset()), - PageRequest.of(0, pageLink.getLimit()))); + DaoUtil.toPageable(pageLink))); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java index e29a988fc1..e5302e2618 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java @@ -23,11 +23,9 @@ import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.dao.model.sql.RuleNodeEntity; import org.thingsboard.server.dao.rule.RuleNodeDao; import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; -import org.thingsboard.server.dao.util.SqlDao; @Slf4j @Component -@SqlDao public class JpaRuleNodeDao extends JpaAbstractSearchTextDao implements RuleNodeDao { @Autowired diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeStateDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeStateDao.java new file mode 100644 index 0000000000..e32f90a645 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeStateDao.java @@ -0,0 +1,72 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.rule; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.rule.RuleNodeState; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.RuleNodeStateEntity; +import org.thingsboard.server.dao.rule.RuleNodeStateDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; + +import java.util.UUID; + +@Slf4j +@Component +public class JpaRuleNodeStateDao extends JpaAbstractDao implements RuleNodeStateDao { + + @Autowired + private RuleNodeStateRepository ruleNodeStateRepository; + + @Override + protected Class getEntityClass() { + return RuleNodeStateEntity.class; + } + + @Override + protected CrudRepository getCrudRepository() { + return ruleNodeStateRepository; + } + + @Override + public PageData findByRuleNodeId(UUID ruleNodeId, PageLink pageLink) { + return DaoUtil.toPageData(ruleNodeStateRepository.findByRuleNodeId(ruleNodeId, DaoUtil.toPageable(pageLink))); + } + + @Override + public RuleNodeState findByRuleNodeIdAndEntityId(UUID ruleNodeId, UUID entityId) { + return DaoUtil.getData(ruleNodeStateRepository.findByRuleNodeIdAndEntityId(ruleNodeId, entityId)); + } + + @Transactional + @Override + public void removeByRuleNodeId(UUID ruleNodeId) { + ruleNodeStateRepository.removeByRuleNodeId(ruleNodeId); + } + + @Transactional + @Override + public void removeByRuleNodeIdAndEntityId(UUID ruleNodeId, UUID entityId) { + ruleNodeStateRepository.removeByRuleNodeIdAndEntityId(ruleNodeId, entityId); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java index a5d5f6bbee..228d4d26f4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java @@ -15,24 +15,21 @@ */ package org.thingsboard.server.dao.sql.rule; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; import org.thingsboard.server.dao.model.sql.RuleChainEntity; -import org.thingsboard.server.dao.util.SqlDao; -import java.util.List; +import java.util.UUID; -@SqlDao -public interface RuleChainRepository extends CrudRepository { +public interface RuleChainRepository extends PagingAndSortingRepository { @Query("SELECT rc FROM RuleChainEntity rc WHERE rc.tenantId = :tenantId " + - "AND LOWER(rc.searchText) LIKE LOWER(CONCAT(:searchText, '%')) " + - "AND rc.id > :idOffset ORDER BY rc.id") - List findByTenantId(@Param("tenantId") String tenantId, + "AND LOWER(rc.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + Page findByTenantId(@Param("tenantId") UUID tenantId, @Param("searchText") String searchText, - @Param("idOffset") String idOffset, Pageable pageable); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java index d3f51ec4f6..58982f6f73 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java @@ -17,9 +17,7 @@ package org.thingsboard.server.dao.sql.rule; import org.springframework.data.repository.CrudRepository; import org.thingsboard.server.dao.model.sql.RuleNodeEntity; -import org.thingsboard.server.dao.util.SqlDao; -@SqlDao public interface RuleNodeRepository extends CrudRepository { } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeStateRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeStateRepository.java new file mode 100644 index 0000000000..7bc35adaae --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeStateRepository.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.rule; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.dao.model.sql.EventEntity; +import org.thingsboard.server.dao.model.sql.RuleNodeStateEntity; + +import java.util.UUID; + +public interface RuleNodeStateRepository extends PagingAndSortingRepository { + + @Query("SELECT e FROM RuleNodeStateEntity e WHERE e.ruleNodeId = :ruleNodeId") + Page findByRuleNodeId(@Param("ruleNodeId") UUID ruleNodeId, Pageable pageable); + + @Query("SELECT e FROM RuleNodeStateEntity e WHERE e.ruleNodeId = :ruleNodeId and e.entityId = :entityId") + RuleNodeStateEntity findByRuleNodeIdAndEntityId(@Param("ruleNodeId") UUID ruleNodeId, @Param("entityId") UUID entityId); + + void removeByRuleNodeId(@Param("ruleNodeId") UUID ruleNodeId); + + void removeByRuleNodeIdAndEntityId(@Param("ruleNodeId") UUID ruleNodeId, @Param("entityId") UUID entityId); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/settings/AdminSettingsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/settings/AdminSettingsRepository.java index 799e71a9b5..b1871fe785 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/settings/AdminSettingsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/settings/AdminSettingsRepository.java @@ -18,10 +18,12 @@ package org.thingsboard.server.dao.sql.settings; import org.springframework.data.repository.CrudRepository; import org.thingsboard.server.dao.model.sql.AdminSettingsEntity; +import java.util.UUID; + /** * Created by Valerii Sosliuk on 5/6/2017. */ -public interface AdminSettingsRepository extends CrudRepository { +public interface AdminSettingsRepository extends CrudRepository { AdminSettingsEntity findByKey(String key); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java index ab4c34781a..4e1d44d7f2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java @@ -25,11 +25,11 @@ import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.sql.AdminSettingsEntity; import org.thingsboard.server.dao.settings.AdminSettingsDao; import org.thingsboard.server.dao.sql.JpaAbstractDao; -import org.thingsboard.server.dao.util.SqlDao; + +import java.util.UUID; @Component @Slf4j -@SqlDao public class JpaAdminSettingsDao extends JpaAbstractDao implements AdminSettingsDao { @Autowired @@ -41,7 +41,7 @@ public class JpaAdminSettingsDao extends JpaAbstractDao getCrudRepository() { + protected CrudRepository getCrudRepository() { return adminSettingsRepository; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java index fb6f42669b..7352f8d0a5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java @@ -16,29 +16,27 @@ package org.thingsboard.server.dao.sql.tenant; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.PageRequest; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.Tenant; -import org.thingsboard.server.common.data.UUIDConverter; +import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.sql.TenantEntity; +import org.thingsboard.server.dao.model.sql.TenantInfoEntity; import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; import org.thingsboard.server.dao.tenant.TenantDao; -import org.thingsboard.server.dao.util.SqlDao; -import java.util.List; import java.util.Objects; +import java.util.UUID; -import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID_STR; /** * Created by Valerii Sosliuk on 4/30/2017. */ @Component -@SqlDao public class JpaTenantDao extends JpaAbstractSearchTextDao implements TenantDao { @Autowired @@ -50,17 +48,30 @@ public class JpaTenantDao extends JpaAbstractSearchTextDao } @Override - protected CrudRepository getCrudRepository() { + protected CrudRepository getCrudRepository() { return tenantRepository; } @Override - public List findTenantsByRegion(TenantId tenantId, String region, TextPageLink pageLink) { - return DaoUtil.convertDataList(tenantRepository + public TenantInfo findTenantInfoById(TenantId tenantId, UUID id) { + return DaoUtil.getData(tenantRepository.findTenantInfoById(id)); + } + + @Override + public PageData findTenantsByRegion(TenantId tenantId, String region, PageLink pageLink) { + return DaoUtil.toPageData(tenantRepository .findByRegionNextPage( region, Objects.toString(pageLink.getTextSearch(), ""), - pageLink.getIdOffset() == null ? NULL_UUID_STR : UUIDConverter.fromTimeUUID(pageLink.getIdOffset()), - PageRequest.of(0, pageLink.getLimit()))); + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findTenantInfosByRegion(TenantId tenantId, String region, PageLink pageLink) { + return DaoUtil.toPageData(tenantRepository + .findTenantInfoByRegionNextPage( + region, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink, TenantInfoEntity.tenantInfoColumnMap))); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantProfileDao.java new file mode 100644 index 0000000000..f80f4d9f2c --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantProfileDao.java @@ -0,0 +1,80 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.tenant; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.TenantProfileEntity; +import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; +import org.thingsboard.server.dao.tenant.TenantProfileDao; + +import java.util.Objects; +import java.util.UUID; + +@Component +public class JpaTenantProfileDao extends JpaAbstractSearchTextDao implements TenantProfileDao { + + @Autowired + private TenantProfileRepository tenantProfileRepository; + + @Override + protected Class getEntityClass() { + return TenantProfileEntity.class; + } + + @Override + protected CrudRepository getCrudRepository() { + return tenantProfileRepository; + } + + @Override + public EntityInfo findTenantProfileInfoById(TenantId tenantId, UUID tenantProfileId) { + return tenantProfileRepository.findTenantProfileInfoById(tenantProfileId); + } + + @Override + public PageData findTenantProfiles(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData( + tenantProfileRepository.findTenantProfiles( + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findTenantProfileInfos(TenantId tenantId, PageLink pageLink) { + return DaoUtil.pageToPageData( + tenantProfileRepository.findTenantProfileInfos( + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public TenantProfile findDefaultTenantProfile(TenantId tenantId) { + return DaoUtil.getData(tenantProfileRepository.findByDefaultTrue()); + } + + @Override + public EntityInfo findDefaultTenantProfileInfo(TenantId tenantId) { + return tenantProfileRepository.findDefaultTenantProfileInfo(); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantProfileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantProfileRepository.java new file mode 100644 index 0000000000..0deae8f729 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantProfileRepository.java @@ -0,0 +1,55 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.tenant; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.dao.model.sql.TenantProfileEntity; + +import java.util.UUID; + +public interface TenantProfileRepository extends PagingAndSortingRepository { + + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(t.id, 'TENANT_PROFILE', t.name) " + + "FROM TenantProfileEntity t " + + "WHERE t.id = :tenantProfileId") + EntityInfo findTenantProfileInfoById(@Param("tenantProfileId") UUID tenantProfileId); + + @Query("SELECT t FROM TenantProfileEntity t WHERE " + + "LOWER(t.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findTenantProfiles(@Param("textSearch") String textSearch, + Pageable pageable); + + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(t.id, 'TENANT_PROFILE', t.name) " + + "FROM TenantProfileEntity t " + + "WHERE LOWER(t.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findTenantProfileInfos(@Param("textSearch") String textSearch, + Pageable pageable); + + @Query("SELECT t FROM TenantProfileEntity t " + + "WHERE t.isDefault = true") + TenantProfileEntity findByDefaultTrue(); + + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(t.id, 'TENANT_PROFILE', t.name) " + + "FROM TenantProfileEntity t " + + "WHERE t.isDefault = true") + EntityInfo findDefaultTenantProfileInfo(); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java index 265836c851..e1ff50eb2a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java @@ -15,26 +15,39 @@ */ package org.thingsboard.server.dao.sql.tenant; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; import org.thingsboard.server.dao.model.sql.TenantEntity; -import org.thingsboard.server.dao.util.SqlDao; +import org.thingsboard.server.dao.model.sql.TenantInfoEntity; -import java.util.List; +import java.util.UUID; /** * Created by Valerii Sosliuk on 4/30/2017. */ -@SqlDao -public interface TenantRepository extends CrudRepository { +public interface TenantRepository extends PagingAndSortingRepository { + + @Query("SELECT new org.thingsboard.server.dao.model.sql.TenantInfoEntity(t, p.name) " + + "FROM TenantEntity t " + + "LEFT JOIN TenantProfileEntity p on p.id = t.tenantProfileId " + + "WHERE t.id = :tenantId") + TenantInfoEntity findTenantInfoById(@Param("tenantId") UUID tenantId); @Query("SELECT t FROM TenantEntity t WHERE t.region = :region " + - "AND LOWER(t.searchText) LIKE LOWER(CONCAT(:textSearch, '%')) " + - "AND t.id > :idOffset ORDER BY t.id") - List findByRegionNextPage(@Param("region") String region, + "AND LOWER(t.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findByRegionNextPage(@Param("region") String region, @Param("textSearch") String textSearch, - @Param("idOffset") String idOffset, Pageable pageable); + + @Query("SELECT new org.thingsboard.server.dao.model.sql.TenantInfoEntity(t, p.name) " + + "FROM TenantEntity t " + + "LEFT JOIN TenantProfileEntity p on p.id = t.tenantProfileId " + + "WHERE t.region = :region " + + "AND LOWER(t.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findTenantInfoByRegionNextPage(@Param("region") String region, + @Param("textSearch") String textSearch, + Pageable pageable); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/usagerecord/ApiUsageStateRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/usagerecord/ApiUsageStateRepository.java new file mode 100644 index 0000000000..122bb9d966 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/usagerecord/ApiUsageStateRepository.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.usagerecord; + +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.dao.model.sql.ApiUsageStateEntity; + +import java.util.UUID; + +/** + * @author Valerii Sosliuk + */ +public interface ApiUsageStateRepository extends CrudRepository { + + @Query("SELECT ur FROM ApiUsageStateEntity ur WHERE ur.tenantId = :tenantId " + + "AND ur.entityId = :tenantId AND ur.entityType = 'TENANT' ") + ApiUsageStateEntity findByTenantId(@Param("tenantId") UUID tenantId); + + @Transactional + @Modifying + @Query("DELETE FROM ApiUsageStateEntity ur WHERE ur.tenantId = :tenantId") + void deleteApiUsageStateByTenantId(@Param("tenantId") UUID tenantId); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/usagerecord/JpaApiUsageStateDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/usagerecord/JpaApiUsageStateDao.java new file mode 100644 index 0000000000..2c6b2e924d --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/usagerecord/JpaApiUsageStateDao.java @@ -0,0 +1,60 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.usagerecord; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.ApiUsageStateEntity; +import org.thingsboard.server.dao.sql.JpaAbstractDao; +import org.thingsboard.server.dao.usagerecord.ApiUsageStateDao; + +import java.util.UUID; + +/** + * @author Andrii Shvaika + */ +@Component +public class JpaApiUsageStateDao extends JpaAbstractDao implements ApiUsageStateDao { + + private final ApiUsageStateRepository apiUsageStateRepository; + + public JpaApiUsageStateDao(ApiUsageStateRepository apiUsageStateRepository) { + this.apiUsageStateRepository = apiUsageStateRepository; + } + + @Override + protected Class getEntityClass() { + return ApiUsageStateEntity.class; + } + + @Override + protected CrudRepository getCrudRepository() { + return apiUsageStateRepository; + } + + @Override + public ApiUsageState findTenantApiUsageState(UUID tenantId) { + return DaoUtil.getData(apiUsageStateRepository.findByTenantId(tenantId)); + } + + @Override + public void deleteApiUsageStateByTenantId(TenantId tenantId) { + apiUsageStateRepository.deleteApiUsageStateByTenantId(tenantId.getId()); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserCredentialsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserCredentialsDao.java index dfa57d87cf..87e5a496d9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserCredentialsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserCredentialsDao.java @@ -18,14 +18,12 @@ package org.thingsboard.server.dao.sql.user; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.sql.UserCredentialsEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.user.UserCredentialsDao; -import org.thingsboard.server.dao.util.SqlDao; import java.util.UUID; @@ -33,7 +31,6 @@ import java.util.UUID; * Created by Valerii Sosliuk on 4/22/2017. */ @Component -@SqlDao public class JpaUserCredentialsDao extends JpaAbstractDao implements UserCredentialsDao { @Autowired @@ -45,13 +42,13 @@ public class JpaUserCredentialsDao extends JpaAbstractDao getCrudRepository() { + protected CrudRepository getCrudRepository() { return userCredentialsRepository; } @Override public UserCredentials findByUserId(TenantId tenantId, UUID userId) { - return DaoUtil.getData(userCredentialsRepository.findByUserId(UUIDConverter.fromTimeUUID(userId))); + return DaoUtil.getData(userCredentialsRepository.findByUserId(userId)); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java index f30f6ba6d1..42092f7ebe 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java @@ -16,31 +16,27 @@ package org.thingsboard.server.dao.sql.user; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.PageRequest; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.sql.UserEntity; import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; import org.thingsboard.server.dao.user.UserDao; -import org.thingsboard.server.dao.util.SqlDao; -import java.util.List; import java.util.Objects; import java.util.UUID; -import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID; -import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID_STR; +import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; /** * @author Valerii Sosliuk */ @Component -@SqlDao public class JpaUserDao extends JpaAbstractSearchTextDao implements UserDao { @Autowired @@ -52,7 +48,7 @@ public class JpaUserDao extends JpaAbstractSearchTextDao imple } @Override - protected CrudRepository getCrudRepository() { + protected CrudRepository getCrudRepository() { return userRepository; } @@ -62,29 +58,37 @@ public class JpaUserDao extends JpaAbstractSearchTextDao imple } @Override - public List findTenantAdmins(UUID tenantId, TextPageLink pageLink) { - return DaoUtil.convertDataList( + public PageData findByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.toPageData( + userRepository + .findByTenantId( + tenantId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findTenantAdmins(UUID tenantId, PageLink pageLink) { + return DaoUtil.toPageData( userRepository .findUsersByAuthority( - fromTimeUUID(tenantId), - NULL_UUID_STR, - pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()), + tenantId, + NULL_UUID, Objects.toString(pageLink.getTextSearch(), ""), Authority.TENANT_ADMIN, - PageRequest.of(0, pageLink.getLimit()))); + DaoUtil.toPageable(pageLink))); } @Override - public List findCustomerUsers(UUID tenantId, UUID customerId, TextPageLink pageLink) { - return DaoUtil.convertDataList( + public PageData findCustomerUsers(UUID tenantId, UUID customerId, PageLink pageLink) { + return DaoUtil.toPageData( userRepository .findUsersByAuthority( - fromTimeUUID(tenantId), - fromTimeUUID(customerId), - pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()), + tenantId, + customerId, Objects.toString(pageLink.getTextSearch(), ""), Authority.CUSTOMER_USER, - PageRequest.of(0, pageLink.getLimit()))); + DaoUtil.toPageable(pageLink))); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserCredentialsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserCredentialsRepository.java index bcb5faaef4..4b1791ddcb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserCredentialsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserCredentialsRepository.java @@ -17,15 +17,15 @@ package org.thingsboard.server.dao.sql.user; import org.springframework.data.repository.CrudRepository; import org.thingsboard.server.dao.model.sql.UserCredentialsEntity; -import org.thingsboard.server.dao.util.SqlDao; + +import java.util.UUID; /** * Created by Valerii Sosliuk on 4/22/2017. */ -@SqlDao -public interface UserCredentialsRepository extends CrudRepository { +public interface UserCredentialsRepository extends CrudRepository { - UserCredentialsEntity findByUserId(String userId); + UserCredentialsEntity findByUserId(UUID userId); UserCredentialsEntity findByActivateToken(String activateToken); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java index 180c4b0421..1d512a7944 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java @@ -15,33 +15,36 @@ */ package org.thingsboard.server.dao.sql.user; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.model.sql.UserEntity; -import org.thingsboard.server.dao.util.SqlDao; -import java.util.List; +import java.util.UUID; /** * @author Valerii Sosliuk */ -@SqlDao -public interface UserRepository extends CrudRepository { +public interface UserRepository extends PagingAndSortingRepository { UserEntity findByEmail(String email); @Query("SELECT u FROM UserEntity u WHERE u.tenantId = :tenantId " + "AND u.customerId = :customerId AND u.authority = :authority " + - "AND LOWER(u.searchText) LIKE LOWER(CONCAT(:searchText, '%'))" + - "AND u.id > :idOffset ORDER BY u.id") - List findUsersByAuthority(@Param("tenantId") String tenantId, - @Param("customerId") String customerId, - @Param("idOffset") String idOffset, + "AND LOWER(u.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + Page findUsersByAuthority(@Param("tenantId") UUID tenantId, + @Param("customerId") UUID customerId, @Param("searchText") String searchText, @Param("authority") Authority authority, Pageable pageable); -} \ No newline at end of file + @Query("SELECT u FROM UserEntity u WHERE u.tenantId = :tenantId " + + "AND LOWER(u.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + Page findByTenantId(@Param("tenantId") UUID tenantId, + @Param("searchText") String searchText, + Pageable pageable); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java index ad141bc11c..805897caf4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java @@ -18,12 +18,10 @@ package org.thingsboard.server.dao.sql.widget; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.sql.WidgetTypeEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; -import org.thingsboard.server.dao.util.SqlDao; import org.thingsboard.server.dao.widget.WidgetTypeDao; import java.util.List; @@ -33,7 +31,6 @@ import java.util.UUID; * Created by Valerii Sosliuk on 4/29/2017. */ @Component -@SqlDao public class JpaWidgetTypeDao extends JpaAbstractDao implements WidgetTypeDao { @Autowired @@ -45,17 +42,17 @@ public class JpaWidgetTypeDao extends JpaAbstractDao getCrudRepository() { + protected CrudRepository getCrudRepository() { return widgetTypeRepository; } @Override public List findWidgetTypesByTenantIdAndBundleAlias(UUID tenantId, String bundleAlias) { - return DaoUtil.convertDataList(widgetTypeRepository.findByTenantIdAndBundleAlias(UUIDConverter.fromTimeUUID(tenantId), bundleAlias)); + return DaoUtil.convertDataList(widgetTypeRepository.findByTenantIdAndBundleAlias(tenantId, bundleAlias)); } @Override public WidgetType findByTenantIdBundleAliasAndAlias(UUID tenantId, String bundleAlias, String alias) { - return DaoUtil.getData(widgetTypeRepository.findByTenantIdAndBundleAliasAndAlias(UUIDConverter.fromTimeUUID(tenantId), bundleAlias, alias)); + return DaoUtil.getData(widgetTypeRepository.findByTenantIdAndBundleAliasAndAlias(tenantId, bundleAlias, alias)); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java index af6785e69e..e0611a6263 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java @@ -16,30 +16,26 @@ package org.thingsboard.server.dao.sql.widget; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.PageRequest; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.sql.WidgetsBundleEntity; import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; -import org.thingsboard.server.dao.util.SqlDao; import org.thingsboard.server.dao.widget.WidgetsBundleDao; -import java.util.List; import java.util.Objects; import java.util.UUID; -import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID_STR; +import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; /** * Created by Valerii Sosliuk on 4/23/2017. */ @Component -@SqlDao public class JpaWidgetsBundleDao extends JpaAbstractSearchTextDao implements WidgetsBundleDao { @Autowired @@ -51,46 +47,43 @@ public class JpaWidgetsBundleDao extends JpaAbstractSearchTextDao getCrudRepository() { + protected CrudRepository getCrudRepository() { return widgetsBundleRepository; } @Override public WidgetsBundle findWidgetsBundleByTenantIdAndAlias(UUID tenantId, String alias) { - return DaoUtil.getData(widgetsBundleRepository.findWidgetsBundleByTenantIdAndAlias(UUIDConverter.fromTimeUUID(tenantId), alias)); + return DaoUtil.getData(widgetsBundleRepository.findWidgetsBundleByTenantIdAndAlias(tenantId, alias)); } @Override - public List findSystemWidgetsBundles(TenantId tenantId, TextPageLink pageLink) { - return DaoUtil.convertDataList( + public PageData findSystemWidgetsBundles(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData( widgetsBundleRepository .findSystemWidgetsBundles( - NULL_UUID_STR, + NULL_UUID, Objects.toString(pageLink.getTextSearch(), ""), - pageLink.getIdOffset() == null ? NULL_UUID_STR : UUIDConverter.fromTimeUUID(pageLink.getIdOffset()), - PageRequest.of(0, pageLink.getLimit()))); + DaoUtil.toPageable(pageLink))); } @Override - public List findTenantWidgetsBundlesByTenantId(UUID tenantId, TextPageLink pageLink) { - return DaoUtil.convertDataList( + public PageData findTenantWidgetsBundlesByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.toPageData( widgetsBundleRepository .findTenantWidgetsBundlesByTenantId( - UUIDConverter.fromTimeUUID(tenantId), + tenantId, Objects.toString(pageLink.getTextSearch(), ""), - pageLink.getIdOffset() == null ? NULL_UUID_STR : UUIDConverter.fromTimeUUID(pageLink.getIdOffset()), - PageRequest.of(0, pageLink.getLimit()))); + DaoUtil.toPageable(pageLink))); } @Override - public List findAllTenantWidgetsBundlesByTenantId(UUID tenantId, TextPageLink pageLink) { - return DaoUtil.convertDataList( + public PageData findAllTenantWidgetsBundlesByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.toPageData( widgetsBundleRepository .findAllTenantWidgetsBundlesByTenantId( - UUIDConverter.fromTimeUUID(tenantId), - NULL_UUID_STR, + tenantId, + NULL_UUID, Objects.toString(pageLink.getTextSearch(), ""), - pageLink.getIdOffset() == null ? NULL_UUID_STR : UUIDConverter.fromTimeUUID(pageLink.getIdOffset()), - PageRequest.of(0, pageLink.getLimit()))); + DaoUtil.toPageable(pageLink))); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java index e6092bcb5d..6167e64234 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java @@ -17,17 +17,16 @@ package org.thingsboard.server.dao.sql.widget; import org.springframework.data.repository.CrudRepository; import org.thingsboard.server.dao.model.sql.WidgetTypeEntity; -import org.thingsboard.server.dao.util.SqlDao; import java.util.List; +import java.util.UUID; /** * Created by Valerii Sosliuk on 4/29/2017. */ -@SqlDao -public interface WidgetTypeRepository extends CrudRepository { +public interface WidgetTypeRepository extends CrudRepository { - List findByTenantIdAndBundleAlias(String tenantId, String bundleAlias); + List findByTenantIdAndBundleAlias(UUID tenantId, String bundleAlias); - WidgetTypeEntity findByTenantIdAndBundleAliasAndAlias(String tenantId, String bundleAlias, String alias); + WidgetTypeEntity findByTenantIdAndBundleAliasAndAlias(UUID tenantId, String bundleAlias, String alias); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java index fb887e5241..b4030f90de 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java @@ -15,45 +15,38 @@ */ package org.thingsboard.server.dao.sql.widget; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; import org.thingsboard.server.dao.model.sql.WidgetsBundleEntity; -import org.thingsboard.server.dao.util.SqlDao; -import java.util.List; +import java.util.UUID; /** * Created by Valerii Sosliuk on 4/23/2017. */ -@SqlDao -public interface WidgetsBundleRepository extends CrudRepository { +public interface WidgetsBundleRepository extends PagingAndSortingRepository { - WidgetsBundleEntity findWidgetsBundleByTenantIdAndAlias(String tenantId, String alias); + WidgetsBundleEntity findWidgetsBundleByTenantIdAndAlias(UUID tenantId, String alias); @Query("SELECT wb FROM WidgetsBundleEntity wb WHERE wb.tenantId = :systemTenantId " + - "AND LOWER(wb.searchText) LIKE LOWER(CONCAT(:searchText, '%')) " + - "AND wb.id > :idOffset ORDER BY wb.id") - List findSystemWidgetsBundles(@Param("systemTenantId") String systemTenantId, + "AND LOWER(wb.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + Page findSystemWidgetsBundles(@Param("systemTenantId") UUID systemTenantId, @Param("searchText") String searchText, - @Param("idOffset") String idOffset, Pageable pageable); @Query("SELECT wb FROM WidgetsBundleEntity wb WHERE wb.tenantId = :tenantId " + - "AND LOWER(wb.searchText) LIKE LOWER(CONCAT(:textSearch, '%')) " + - "AND wb.id > :idOffset ORDER BY wb.id") - List findTenantWidgetsBundlesByTenantId(@Param("tenantId") String tenantId, + "AND LOWER(wb.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findTenantWidgetsBundlesByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, - @Param("idOffset") String idOffset, Pageable pageable); @Query("SELECT wb FROM WidgetsBundleEntity wb WHERE wb.tenantId IN (:tenantId, :nullTenantId) " + - "AND LOWER(wb.searchText) LIKE LOWER(CONCAT(:textSearch, '%')) " + - "AND wb.id > :idOffset ORDER BY wb.id") - List findAllTenantWidgetsBundlesByTenantId(@Param("tenantId") String tenantId, - @Param("nullTenantId") String nullTenantId, + "AND LOWER(wb.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findAllTenantWidgetsBundlesByTenantId(@Param("tenantId") UUID tenantId, + @Param("nullTenantId") UUID nullTenantId, @Param("textSearch") String textSearch, - @Param("idOffset") String idOffset, Pageable pageable); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java index 459a0bc015..616260ba45 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java @@ -17,11 +17,11 @@ package org.thingsboard.server.dao.sqlts; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.thingsboard.server.common.data.id.EntityId; @@ -32,8 +32,8 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity; import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity; -import org.thingsboard.server.dao.sql.TbSqlBlockingQueue; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper; import org.thingsboard.server.dao.sqlts.insert.InsertTsRepository; @@ -42,9 +42,7 @@ import org.thingsboard.server.dao.timeseries.TimeseriesDao; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.function.Function; import java.util.stream.Collectors; @@ -64,23 +62,26 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq @PostConstruct protected void init() { - super.init(); TbSqlBlockingQueueParams tsParams = TbSqlBlockingQueueParams.builder() .logName("TS") .batchSize(tsBatchSize) .maxDelay(tsMaxDelay) .statsPrintIntervalMs(tsStatsPrintIntervalMs) .statsNamePrefix("ts") + .batchSortEnabled(batchSortEnabled) .build(); Function hashcodeFunction = entity -> entity.getEntityId().hashCode(); tsQueue = new TbSqlBlockingQueueWrapper<>(tsParams, hashcodeFunction, tsBatchThreads, statsFactory); - tsQueue.init(logExecutor, v -> insertRepository.saveOrUpdate(v)); + tsQueue.init(logExecutor, v -> insertRepository.saveOrUpdate(v), + Comparator.comparing((Function) AbstractTsKvEntity::getEntityId) + .thenComparing(AbstractTsKvEntity::getKey) + .thenComparing(AbstractTsKvEntity::getTs) + ); } @PreDestroy protected void destroy() { - super.destroy(); if (tsQueue != null) { tsQueue.destroy(); } @@ -99,27 +100,7 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq } @Override - public ListenableFuture saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { - return getSaveLatestFuture(entityId, tsKvEntry); - } - - @Override - public ListenableFuture removeLatest(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { - return getRemoveLatestFuture(entityId, query); - } - - @Override - public ListenableFuture findLatest(TenantId tenantId, EntityId entityId, String key) { - return getFindLatestFuture(entityId, key); - } - - @Override - public ListenableFuture> findAllLatest(TenantId tenantId, EntityId entityId) { - return getFindAllLatestFuture(entityId); - } - - @Override - public ListenableFuture savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key, long ttl) { + public ListenableFuture savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key, long ttl) { return Futures.immediateFuture(null); } @@ -134,7 +115,7 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq } @Override - protected ListenableFuture> findAllAsync(EntityId entityId, ReadTsKvQuery query) { + public ListenableFuture> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) { if (query.getAggregation() == Aggregation.NONE) { return findAllAsyncWithLimit(entityId, query); } else { @@ -151,8 +132,7 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq } } - @Override - protected ListenableFuture> findAllAsyncWithLimit(EntityId entityId, ReadTsKvQuery query) { + private ListenableFuture> findAllAsyncWithLimit(EntityId entityId, ReadTsKvQuery query) { Integer keyId = getOrSaveKeyId(query.getKey()); List tsKvEntities = tsKvRepository.findAllWithLimit( entityId.getId(), @@ -161,7 +141,7 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq query.getEndTs(), PageRequest.of(0, query.getLimit(), Sort.by(Sort.Direction.fromString( - query.getOrderBy()), "ts"))); + query.getOrder()), "ts"))); tsKvEntities.forEach(tsKvEntity -> tsKvEntity.setStrKey(query.getKey())); return Futures.immediateFuture(DaoUtil.convertDataList(tsKvEntities)); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java index 9710edad4a..c585804b82 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java @@ -16,96 +16,31 @@ package org.thingsboard.server.dao.sqlts; import com.google.common.base.Function; -import com.google.common.collect.Lists; -import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; -import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.kv.Aggregation; -import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; -import org.thingsboard.server.common.data.kv.BasicTsKvEntry; -import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; -import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; -import org.thingsboard.server.common.stats.StatsFactory; -import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.model.sqlts.dictionary.TsKvDictionary; -import org.thingsboard.server.dao.model.sqlts.dictionary.TsKvDictionaryCompositeKey; -import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestCompositeKey; -import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; -import org.thingsboard.server.dao.sql.JpaAbstractDaoListeningExecutorService; import org.thingsboard.server.dao.sql.ScheduledLogExecutorComponent; -import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams; -import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper; -import org.thingsboard.server.dao.sqlts.dictionary.TsKvDictionaryRepository; -import org.thingsboard.server.dao.sqlts.insert.latest.InsertLatestTsRepository; -import org.thingsboard.server.dao.sqlts.latest.SearchTsKvLatestRepository; -import org.thingsboard.server.dao.sqlts.latest.TsKvLatestRepository; -import org.thingsboard.server.dao.timeseries.SimpleListenableFuture; import javax.annotation.Nullable; -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @Slf4j -public abstract class AbstractSqlTimeseriesDao extends JpaAbstractDaoListeningExecutorService { +public abstract class AbstractSqlTimeseriesDao extends BaseAbstractSqlTimeseriesDao implements AggregationTimeseriesDao { - private static final String DESC_ORDER = "DESC"; - - private final ConcurrentMap tsKvDictionaryMap = new ConcurrentHashMap<>(); - - private static final ReentrantLock tsCreationLock = new ReentrantLock(); - - @Autowired - private TsKvLatestRepository tsKvLatestRepository; - - @Autowired - private SearchTsKvLatestRepository searchTsKvLatestRepository; - - @Autowired - private InsertLatestTsRepository insertLatestTsRepository; - - @Autowired - private TsKvDictionaryRepository dictionaryRepository; - - private TbSqlBlockingQueueWrapper tsLatestQueue; - - @Value("${sql.ts_latest.batch_size:1000}") - private int tsLatestBatchSize; - - @Value("${sql.ts_latest.batch_max_delay:100}") - private long tsLatestMaxDelay; - - @Value("${sql.ts_latest.stats_print_interval_ms:1000}") - private long tsLatestStatsPrintIntervalMs; - - @Value("${sql.ts_latest.batch_threads:4}") - private int tsLatestBatchThreads; + protected static final long SECONDS_IN_DAY = TimeUnit.DAYS.toSeconds(1); @Autowired protected ScheduledLogExecutorComponent logExecutor; - @Autowired - private StatsFactory statsFactory; - @Value("${sql.ts.batch_size:1000}") protected int tsBatchSize; @@ -121,44 +56,16 @@ public abstract class AbstractSqlTimeseriesDao extends JpaAbstractDaoListeningEx @Value("${sql.timescale.batch_threads:4}") protected int timescaleBatchThreads; - @PostConstruct - protected void init() { - TbSqlBlockingQueueParams tsLatestParams = TbSqlBlockingQueueParams.builder() - .logName("TS Latest") - .batchSize(tsLatestBatchSize) - .maxDelay(tsLatestMaxDelay) - .statsPrintIntervalMs(tsLatestStatsPrintIntervalMs) - .statsNamePrefix("ts.latest") - .build(); + @Value("${sql.batch_sort:false}") + protected boolean batchSortEnabled; - java.util.function.Function hashcodeFunction = entity -> entity.getEntityId().hashCode(); - tsLatestQueue = new TbSqlBlockingQueueWrapper<>(tsLatestParams, hashcodeFunction, tsLatestBatchThreads, statsFactory); - - tsLatestQueue.init(logExecutor, v -> { - Map trueLatest = new HashMap<>(); - v.forEach(ts -> { - TsKey key = new TsKey(ts.getEntityId(), ts.getKey()); - TsKvLatestEntity old = trueLatest.get(key); - if (old == null || old.getTs() < ts.getTs()) { - trueLatest.put(key, ts); - } - }); - List latestEntities = new ArrayList<>(trueLatest.values()); - insertLatestTsRepository.saveOrUpdate(latestEntities); - }); - } - - @PreDestroy - protected void destroy() { - if (tsLatestQueue != null) { - tsLatestQueue.destroy(); - } - } + @Value("${sql.ttl.ts.ts_key_value_ttl:0}") + private long systemTtl; protected ListenableFuture> processFindAllAsync(TenantId tenantId, EntityId entityId, List queries) { List>> futures = queries .stream() - .map(query -> findAllAsync(entityId, query)) + .map(query -> findAllAsync(tenantId, entityId, query)) .collect(Collectors.toList()); return Futures.transform(Futures.allAsList(futures), new Function>, List>() { @Nullable @@ -175,167 +82,18 @@ public abstract class AbstractSqlTimeseriesDao extends JpaAbstractDaoListeningEx }, service); } - protected abstract ListenableFuture> findAllAsync(EntityId entityId, ReadTsKvQuery query); - - protected abstract ListenableFuture> findAllAsyncWithLimit(EntityId entityId, ReadTsKvQuery query); - - protected ListenableFuture> getTskvEntriesFuture(ListenableFuture>> future) { - return Futures.transform(future, new Function>, List>() { - @Nullable - @Override - public List apply(@Nullable List> results) { - if (results == null || results.isEmpty()) { - return null; - } - return results.stream() - .filter(Optional::isPresent) - .map(Optional::get) - .collect(Collectors.toList()); - } - }, service); - } - - protected ListenableFuture> findNewLatestEntryFuture(EntityId entityId, DeleteTsKvQuery query) { - long startTs = 0; - long endTs = query.getStartTs() - 1; - ReadTsKvQuery findNewLatestQuery = new BaseReadTsKvQuery(query.getKey(), startTs, endTs, endTs - startTs, 1, - Aggregation.NONE, DESC_ORDER); - return findAllAsync(entityId, findNewLatestQuery); - } - - protected ListenableFuture getFindLatestFuture(EntityId entityId, String key) { - TsKvLatestCompositeKey compositeKey = - new TsKvLatestCompositeKey( - entityId.getId(), - getOrSaveKeyId(key)); - Optional entry = tsKvLatestRepository.findById(compositeKey); - TsKvEntry result; - if (entry.isPresent()) { - TsKvLatestEntity tsKvLatestEntity = entry.get(); - tsKvLatestEntity.setStrKey(key); - result = DaoUtil.getData(tsKvLatestEntity); - } else { - result = new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry(key, null)); - } - return Futures.immediateFuture(result); - } - - protected ListenableFuture getRemoveLatestFuture(EntityId entityId, DeleteTsKvQuery query) { - ListenableFuture latestFuture = getFindLatestFuture(entityId, query.getKey()); - - ListenableFuture booleanFuture = Futures.transform(latestFuture, tsKvEntry -> { - long ts = tsKvEntry.getTs(); - return ts > query.getStartTs() && ts <= query.getEndTs(); - }, service); - - ListenableFuture removedLatestFuture = Futures.transformAsync(booleanFuture, isRemove -> { - if (isRemove) { - TsKvLatestEntity latestEntity = new TsKvLatestEntity(); - latestEntity.setEntityId(entityId.getId()); - latestEntity.setKey(getOrSaveKeyId(query.getKey())); - return service.submit(() -> { - tsKvLatestRepository.delete(latestEntity); - return null; - }); - } - return Futures.immediateFuture(null); - }, service); - - final SimpleListenableFuture resultFuture = new SimpleListenableFuture<>(); - Futures.addCallback(removedLatestFuture, new FutureCallback() { - @Override - public void onSuccess(@Nullable Void result) { - if (query.getRewriteLatestIfDeleted()) { - ListenableFuture savedLatestFuture = Futures.transformAsync(booleanFuture, isRemove -> { - if (isRemove) { - return getNewLatestEntryFuture(entityId, query); - } - return Futures.immediateFuture(null); - }, service); - - try { - resultFuture.set(savedLatestFuture.get()); - } catch (InterruptedException | ExecutionException e) { - log.warn("Could not get latest saved value for [{}], {}", entityId, query.getKey(), e); - } - } else { - resultFuture.set(null); - } - } - - @Override - public void onFailure(Throwable t) { - log.warn("[{}] Failed to process remove of the latest value", entityId, t); - } - }, MoreExecutors.directExecutor()); - return resultFuture; - } - - protected ListenableFuture> getFindAllLatestFuture(EntityId entityId) { - return Futures.immediateFuture( - DaoUtil.convertDataList(Lists.newArrayList( - searchTsKvLatestRepository.findAllByEntityId(entityId.getId())))); - } - - protected ListenableFuture getSaveLatestFuture(EntityId entityId, TsKvEntry tsKvEntry) { - TsKvLatestEntity latestEntity = new TsKvLatestEntity(); - latestEntity.setEntityId(entityId.getId()); - latestEntity.setTs(tsKvEntry.getTs()); - latestEntity.setKey(getOrSaveKeyId(tsKvEntry.getKey())); - latestEntity.setStrValue(tsKvEntry.getStrValue().orElse(null)); - latestEntity.setDoubleValue(tsKvEntry.getDoubleValue().orElse(null)); - latestEntity.setLongValue(tsKvEntry.getLongValue().orElse(null)); - latestEntity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null)); - latestEntity.setJsonValue(tsKvEntry.getJsonValue().orElse(null)); - - return tsLatestQueue.add(latestEntity); - } - - protected Integer getOrSaveKeyId(String strKey) { - Integer keyId = tsKvDictionaryMap.get(strKey); - if (keyId == null) { - Optional tsKvDictionaryOptional; - tsKvDictionaryOptional = dictionaryRepository.findById(new TsKvDictionaryCompositeKey(strKey)); - if (!tsKvDictionaryOptional.isPresent()) { - tsCreationLock.lock(); - try { - tsKvDictionaryOptional = dictionaryRepository.findById(new TsKvDictionaryCompositeKey(strKey)); - if (!tsKvDictionaryOptional.isPresent()) { - TsKvDictionary tsKvDictionary = new TsKvDictionary(); - tsKvDictionary.setKey(strKey); - try { - TsKvDictionary saved = dictionaryRepository.save(tsKvDictionary); - tsKvDictionaryMap.put(saved.getKey(), saved.getKeyId()); - keyId = saved.getKeyId(); - } catch (ConstraintViolationException e) { - tsKvDictionaryOptional = dictionaryRepository.findById(new TsKvDictionaryCompositeKey(strKey)); - TsKvDictionary dictionary = tsKvDictionaryOptional.orElseThrow(() -> new RuntimeException("Failed to get TsKvDictionary entity from DB!")); - tsKvDictionaryMap.put(dictionary.getKey(), dictionary.getKeyId()); - keyId = dictionary.getKeyId(); - } - } else { - keyId = tsKvDictionaryOptional.get().getKeyId(); - } - } finally { - tsCreationLock.unlock(); - } + protected long computeTtl(long ttl) { + if (systemTtl > 0) { + if (ttl == 0) { + ttl = systemTtl; } else { - keyId = tsKvDictionaryOptional.get().getKeyId(); - tsKvDictionaryMap.put(strKey, keyId); + ttl = Math.min(systemTtl, ttl); } } - return keyId; + return ttl; } - private ListenableFuture getNewLatestEntryFuture(EntityId entityId, DeleteTsKvQuery query) { - ListenableFuture> future = findNewLatestEntryFuture(entityId, query); - return Futures.transformAsync(future, entryList -> { - if (entryList.size() == 1) { - return getSaveLatestFuture(entityId, entryList.get(0)); - } else { - log.trace("Could not find new latest value for [{}], key - {}", entityId, query.getKey()); - } - return Futures.immediateFuture(null); - }, service); + protected int getDataPointDays(TsKvEntry tsKvEntry, long ttl) { + return tsKvEntry.getDataPoints() * Math.max(1, (int) (ttl / SECONDS_IN_DAY)); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AggregationTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AggregationTimeseriesDao.java new file mode 100644 index 0000000000..345dc26e48 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AggregationTimeseriesDao.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sqlts; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.ReadTsKvQuery; +import org.thingsboard.server.common.data.kv.TsKvEntry; + +import java.util.List; + +public interface AggregationTimeseriesDao { + + ListenableFuture> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/BaseAbstractSqlTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/BaseAbstractSqlTimeseriesDao.java new file mode 100644 index 0000000000..1d7fbbaac0 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/BaseAbstractSqlTimeseriesDao.java @@ -0,0 +1,99 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sqlts; + +import com.google.common.base.Function; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.exception.ConstraintViolationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.dao.model.sqlts.dictionary.TsKvDictionary; +import org.thingsboard.server.dao.model.sqlts.dictionary.TsKvDictionaryCompositeKey; +import org.thingsboard.server.dao.sql.JpaAbstractDaoListeningExecutorService; +import org.thingsboard.server.dao.sqlts.dictionary.TsKvDictionaryRepository; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; + +@Slf4j +public abstract class BaseAbstractSqlTimeseriesDao extends JpaAbstractDaoListeningExecutorService { + + private final ConcurrentMap tsKvDictionaryMap = new ConcurrentHashMap<>(); + + protected static final ReentrantLock tsCreationLock = new ReentrantLock(); + + @Autowired + protected TsKvDictionaryRepository dictionaryRepository; + + protected Integer getOrSaveKeyId(String strKey) { + Integer keyId = tsKvDictionaryMap.get(strKey); + if (keyId == null) { + Optional tsKvDictionaryOptional; + tsKvDictionaryOptional = dictionaryRepository.findById(new TsKvDictionaryCompositeKey(strKey)); + if (!tsKvDictionaryOptional.isPresent()) { + tsCreationLock.lock(); + try { + tsKvDictionaryOptional = dictionaryRepository.findById(new TsKvDictionaryCompositeKey(strKey)); + if (!tsKvDictionaryOptional.isPresent()) { + TsKvDictionary tsKvDictionary = new TsKvDictionary(); + tsKvDictionary.setKey(strKey); + try { + TsKvDictionary saved = dictionaryRepository.save(tsKvDictionary); + tsKvDictionaryMap.put(saved.getKey(), saved.getKeyId()); + keyId = saved.getKeyId(); + } catch (ConstraintViolationException e) { + tsKvDictionaryOptional = dictionaryRepository.findById(new TsKvDictionaryCompositeKey(strKey)); + TsKvDictionary dictionary = tsKvDictionaryOptional.orElseThrow(() -> new RuntimeException("Failed to get TsKvDictionary entity from DB!")); + tsKvDictionaryMap.put(dictionary.getKey(), dictionary.getKeyId()); + keyId = dictionary.getKeyId(); + } + } else { + keyId = tsKvDictionaryOptional.get().getKeyId(); + } + } finally { + tsCreationLock.unlock(); + } + } else { + keyId = tsKvDictionaryOptional.get().getKeyId(); + tsKvDictionaryMap.put(strKey, keyId); + } + } + return keyId; + } + + protected ListenableFuture> getTskvEntriesFuture(ListenableFuture>> future) { + return Futures.transform(future, new Function>, List>() { + @Nullable + @Override + public List apply(@Nullable List> results) { + if (results == null || results.isEmpty()) { + return null; + } + return results.stream() + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + } + }, service); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java new file mode 100644 index 0000000000..f6e5e8aa40 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java @@ -0,0 +1,265 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sqlts; + +import com.google.common.collect.Lists; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.Aggregation; +import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; +import org.thingsboard.server.common.data.kv.ReadTsKvQuery; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.stats.StatsFactory; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity; +import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestCompositeKey; +import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; +import org.thingsboard.server.dao.sql.ScheduledLogExecutorComponent; +import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams; +import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper; +import org.thingsboard.server.dao.sqlts.insert.latest.InsertLatestTsRepository; +import org.thingsboard.server.dao.sqlts.latest.SearchTsKvLatestRepository; +import org.thingsboard.server.dao.sqlts.latest.TsKvLatestRepository; +import org.thingsboard.server.dao.timeseries.SimpleListenableFuture; +import org.thingsboard.server.dao.timeseries.TimeseriesLatestDao; +import org.thingsboard.server.dao.util.SqlTsLatestAnyDao; + +import javax.annotation.Nullable; +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Slf4j +@Component +@SqlTsLatestAnyDao +public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao implements TimeseriesLatestDao { + + private static final String DESC_ORDER = "DESC"; + + @Autowired + private TsKvLatestRepository tsKvLatestRepository; + + @Autowired + protected AggregationTimeseriesDao aggregationTimeseriesDao; + + @Autowired + private SearchTsKvLatestRepository searchTsKvLatestRepository; + + @Autowired + private InsertLatestTsRepository insertLatestTsRepository; + + private TbSqlBlockingQueueWrapper tsLatestQueue; + + @Value("${sql.ts_latest.batch_size:1000}") + private int tsLatestBatchSize; + + @Value("${sql.ts_latest.batch_max_delay:100}") + private long tsLatestMaxDelay; + + @Value("${sql.ts_latest.stats_print_interval_ms:1000}") + private long tsLatestStatsPrintIntervalMs; + + @Value("${sql.ts_latest.batch_threads:4}") + private int tsLatestBatchThreads; + + @Value("${sql.batch_sort:false}") + protected boolean batchSortEnabled; + + @Autowired + protected ScheduledLogExecutorComponent logExecutor; + + @Autowired + private StatsFactory statsFactory; + + @PostConstruct + protected void init() { + TbSqlBlockingQueueParams tsLatestParams = TbSqlBlockingQueueParams.builder() + .logName("TS Latest") + .batchSize(tsLatestBatchSize) + .maxDelay(tsLatestMaxDelay) + .statsPrintIntervalMs(tsLatestStatsPrintIntervalMs) + .statsNamePrefix("ts.latest") + .batchSortEnabled(false) + .build(); + + java.util.function.Function hashcodeFunction = entity -> entity.getEntityId().hashCode(); + tsLatestQueue = new TbSqlBlockingQueueWrapper<>(tsLatestParams, hashcodeFunction, tsLatestBatchThreads, statsFactory); + + tsLatestQueue.init(logExecutor, v -> { + Map trueLatest = new HashMap<>(); + v.forEach(ts -> { + TsKey key = new TsKey(ts.getEntityId(), ts.getKey()); + trueLatest.merge(key, ts, (oldTs, newTs) -> oldTs.getTs() < newTs.getTs() ? newTs : oldTs); + }); + List latestEntities = new ArrayList<>(trueLatest.values()); + if (batchSortEnabled) { + latestEntities.sort(Comparator.comparing((Function) AbstractTsKvEntity::getEntityId) + .thenComparingInt(AbstractTsKvEntity::getKey)); + } + insertLatestTsRepository.saveOrUpdate(latestEntities); + }, (l, r) -> 0); + } + + @PreDestroy + protected void destroy() { + if (tsLatestQueue != null) { + tsLatestQueue.destroy(); + } + } + + @Override + public ListenableFuture saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { + return getSaveLatestFuture(entityId, tsKvEntry); + } + + @Override + public ListenableFuture removeLatest(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { + return getRemoveLatestFuture(tenantId, entityId, query); + } + + @Override + public ListenableFuture findLatest(TenantId tenantId, EntityId entityId, String key) { + return getFindLatestFuture(entityId, key); + } + + @Override + public ListenableFuture> findAllLatest(TenantId tenantId, EntityId entityId) { + return getFindAllLatestFuture(entityId); + } + + private ListenableFuture getNewLatestEntryFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { + ListenableFuture> future = findNewLatestEntryFuture(tenantId, entityId, query); + return Futures.transformAsync(future, entryList -> { + if (entryList.size() == 1) { + return getSaveLatestFuture(entityId, entryList.get(0)); + } else { + log.trace("Could not find new latest value for [{}], key - {}", entityId, query.getKey()); + } + return Futures.immediateFuture(null); + }, service); + } + + private ListenableFuture> findNewLatestEntryFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { + long startTs = 0; + long endTs = query.getStartTs() - 1; + ReadTsKvQuery findNewLatestQuery = new BaseReadTsKvQuery(query.getKey(), startTs, endTs, endTs - startTs, 1, + Aggregation.NONE, DESC_ORDER); + return aggregationTimeseriesDao.findAllAsync(tenantId, entityId, findNewLatestQuery); + } + + protected ListenableFuture getFindLatestFuture(EntityId entityId, String key) { + TsKvLatestCompositeKey compositeKey = + new TsKvLatestCompositeKey( + entityId.getId(), + getOrSaveKeyId(key)); + Optional entry = tsKvLatestRepository.findById(compositeKey); + TsKvEntry result; + if (entry.isPresent()) { + TsKvLatestEntity tsKvLatestEntity = entry.get(); + tsKvLatestEntity.setStrKey(key); + result = DaoUtil.getData(tsKvLatestEntity); + } else { + result = new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry(key, null)); + } + return Futures.immediateFuture(result); + } + + protected ListenableFuture getRemoveLatestFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { + ListenableFuture latestFuture = getFindLatestFuture(entityId, query.getKey()); + + ListenableFuture booleanFuture = Futures.transform(latestFuture, tsKvEntry -> { + long ts = tsKvEntry.getTs(); + return ts > query.getStartTs() && ts <= query.getEndTs(); + }, service); + + ListenableFuture removedLatestFuture = Futures.transformAsync(booleanFuture, isRemove -> { + if (isRemove) { + TsKvLatestEntity latestEntity = new TsKvLatestEntity(); + latestEntity.setEntityId(entityId.getId()); + latestEntity.setKey(getOrSaveKeyId(query.getKey())); + return service.submit(() -> { + tsKvLatestRepository.delete(latestEntity); + return null; + }); + } + return Futures.immediateFuture(null); + }, service); + + final SimpleListenableFuture resultFuture = new SimpleListenableFuture<>(); + Futures.addCallback(removedLatestFuture, new FutureCallback() { + @Override + public void onSuccess(@Nullable Void result) { + if (query.getRewriteLatestIfDeleted()) { + ListenableFuture savedLatestFuture = Futures.transformAsync(booleanFuture, isRemove -> { + if (isRemove) { + return getNewLatestEntryFuture(tenantId, entityId, query); + } + return Futures.immediateFuture(null); + }, service); + + try { + resultFuture.set(savedLatestFuture.get()); + } catch (InterruptedException | ExecutionException e) { + log.warn("Could not get latest saved value for [{}], {}", entityId, query.getKey(), e); + } + } else { + resultFuture.set(null); + } + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}] Failed to process remove of the latest value", entityId, t); + } + }, MoreExecutors.directExecutor()); + return resultFuture; + } + + protected ListenableFuture> getFindAllLatestFuture(EntityId entityId) { + return Futures.immediateFuture( + DaoUtil.convertDataList(Lists.newArrayList( + searchTsKvLatestRepository.findAllByEntityId(entityId.getId())))); + } + + protected ListenableFuture getSaveLatestFuture(EntityId entityId, TsKvEntry tsKvEntry) { + TsKvLatestEntity latestEntity = new TsKvLatestEntity(); + latestEntity.setEntityId(entityId.getId()); + latestEntity.setTs(tsKvEntry.getTs()); + latestEntity.setKey(getOrSaveKeyId(tsKvEntry.getKey())); + latestEntity.setStrValue(tsKvEntry.getStrValue().orElse(null)); + latestEntity.setDoubleValue(tsKvEntry.getDoubleValue().orElse(null)); + latestEntity.setLongValue(tsKvEntry.getLongValue().orElse(null)); + latestEntity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null)); + latestEntity.setJsonValue(tsKvEntry.getJsonValue().orElse(null)); + + return tsLatestQueue.add(latestEntity); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/TsKvDictionaryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/TsKvDictionaryRepository.java index 55d2d031d9..76872ab176 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/TsKvDictionaryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/TsKvDictionaryRepository.java @@ -18,11 +18,11 @@ package org.thingsboard.server.dao.sqlts.dictionary; import org.springframework.data.repository.CrudRepository; import org.thingsboard.server.dao.model.sqlts.dictionary.TsKvDictionary; import org.thingsboard.server.dao.model.sqlts.dictionary.TsKvDictionaryCompositeKey; -import org.thingsboard.server.dao.util.SqlTsAnyDao; +import org.thingsboard.server.dao.util.SqlTsOrTsLatestAnyDao; import java.util.Optional; -@SqlTsAnyDao +@SqlTsOrTsLatestAnyDao public interface TsKvDictionaryRepository extends CrudRepository { Optional findByKeyId(int keyId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/hsql/JpaHsqlTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/hsql/JpaHsqlTimeseriesDao.java index 5ea387328d..025134e187 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/hsql/JpaHsqlTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/hsql/JpaHsqlTimeseriesDao.java @@ -15,7 +15,9 @@ */ package org.thingsboard.server.dao.sqlts.hsql; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.id.EntityId; @@ -35,7 +37,8 @@ import org.thingsboard.server.dao.util.SqlTsDao; public class JpaHsqlTimeseriesDao extends AbstractChunkedAggregationTimeseriesDao implements TimeseriesDao { @Override - public ListenableFuture save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl) { + public ListenableFuture save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl) { + int dataPointDays = getDataPointDays(tsKvEntry, computeTtl(ttl)); String strKey = tsKvEntry.getKey(); Integer keyId = getOrSaveKeyId(strKey); TsKvEntity entity = new TsKvEntity(); @@ -48,7 +51,7 @@ public class JpaHsqlTimeseriesDao extends AbstractChunkedAggregationTimeseriesDa entity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null)); entity.setJsonValue(tsKvEntry.getJsonValue().orElse(null)); log.trace("Saving entity: {}", entity); - return tsQueue.add(entity); + return Futures.transform(tsQueue.add(entity), v -> dataPointDays, MoreExecutors.directExecutor()); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/hsql/HsqlLatestInsertTsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/hsql/HsqlLatestInsertTsRepository.java index 429d5de31b..9144147152 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/hsql/HsqlLatestInsertTsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/hsql/HsqlLatestInsertTsRepository.java @@ -22,14 +22,14 @@ import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; import org.thingsboard.server.dao.sqlts.insert.AbstractInsertRepository; import org.thingsboard.server.dao.sqlts.insert.latest.InsertLatestTsRepository; import org.thingsboard.server.dao.util.HsqlDao; -import org.thingsboard.server.dao.util.SqlTsDao; +import org.thingsboard.server.dao.util.SqlTsLatestDao; import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Types; import java.util.List; -@SqlTsDao +@SqlTsLatestDao @HsqlDao @Repository @Transactional diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/psql/PsqlLatestInsertTsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/psql/PsqlLatestInsertTsRepository.java index 41abae52f8..034ef26c00 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/psql/PsqlLatestInsertTsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/psql/PsqlLatestInsertTsRepository.java @@ -23,7 +23,7 @@ import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; import org.thingsboard.server.dao.sqlts.insert.AbstractInsertRepository; import org.thingsboard.server.dao.sqlts.insert.latest.InsertLatestTsRepository; -import org.thingsboard.server.dao.util.PsqlTsAnyDao; +import org.thingsboard.server.dao.util.PsqlTsLatestAnyDao; import java.sql.PreparedStatement; import java.sql.SQLException; @@ -32,7 +32,7 @@ import java.util.ArrayList; import java.util.List; -@PsqlTsAnyDao +@PsqlTsLatestAnyDao @Repository @Transactional public class PsqlLatestInsertTsRepository extends AbstractInsertRepository implements InsertLatestTsRepository { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/SearchTsKvLatestRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/SearchTsKvLatestRepository.java index b5da093b6f..19687a94ca 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/SearchTsKvLatestRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/SearchTsKvLatestRepository.java @@ -17,14 +17,14 @@ package org.thingsboard.server.dao.sqlts.latest; import org.springframework.stereotype.Repository; import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; -import org.thingsboard.server.dao.util.SqlTsAnyDao; +import org.thingsboard.server.dao.util.SqlTsLatestAnyDao; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import java.util.List; import java.util.UUID; -@SqlTsAnyDao +@SqlTsLatestAnyDao @Repository public class SearchTsKvLatestRepository { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/TsKvLatestRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/TsKvLatestRepository.java index 5232b88489..bb53106254 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/TsKvLatestRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/TsKvLatestRepository.java @@ -18,9 +18,7 @@ package org.thingsboard.server.dao.sqlts.latest; import org.springframework.data.repository.CrudRepository; import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestCompositeKey; import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; -import org.thingsboard.server.dao.util.SqlDao; -@SqlDao public interface TsKvLatestRepository extends CrudRepository { } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/psql/JpaPsqlTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/psql/JpaPsqlTimeseriesDao.java index 7d0aefb62c..3d971f61c6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/psql/JpaPsqlTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/psql/JpaPsqlTimeseriesDao.java @@ -15,7 +15,9 @@ */ package org.thingsboard.server.dao.sqlts.psql; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -41,11 +43,10 @@ import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReentrantLock; - @Component @Slf4j -@SqlTsDao @PsqlDao +@SqlTsDao public class JpaPsqlTimeseriesDao extends AbstractChunkedAggregationTimeseriesDao { private final Map partitions = new ConcurrentHashMap<>(); @@ -72,7 +73,8 @@ public class JpaPsqlTimeseriesDao extends AbstractChunkedAggregationTimeseriesDa } @Override - public ListenableFuture save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl) { + public ListenableFuture save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl) { + int dataPointDays = getDataPointDays(tsKvEntry, computeTtl(ttl)); savePartitionIfNotExist(tsKvEntry.getTs()); String strKey = tsKvEntry.getKey(); Integer keyId = getOrSaveKeyId(strKey); @@ -86,7 +88,7 @@ public class JpaPsqlTimeseriesDao extends AbstractChunkedAggregationTimeseriesDa entity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null)); entity.setJsonValue(tsKvEntry.getJsonValue().orElse(null)); log.trace("Saving entity: {}", entity); - return tsQueue.add(entity); + return Futures.transform(tsQueue.add(entity), v -> dataPointDays, MoreExecutors.directExecutor()); } private void savePartitionIfNotExist(long ts) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/AggregationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/AggregationRepository.java index 28b666b03b..e12a0219d3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/AggregationRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/AggregationRepository.java @@ -18,7 +18,7 @@ package org.thingsboard.server.dao.sqlts.timescale; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Repository; import org.thingsboard.server.dao.model.sqlts.timescale.ts.TimescaleTsKvEntity; -import org.thingsboard.server.dao.util.TimescaleDBTsDao; +import org.thingsboard.server.dao.util.TimescaleDBTsOrTsLatestDao; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; @@ -27,7 +27,7 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; @Repository -@TimescaleDBTsDao +@TimescaleDBTsOrTsLatestDao public class AggregationRepository { public static final String FIND_AVG = "findAvg"; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java index e3ad6bd2bc..f6fb027d7e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java @@ -21,6 +21,7 @@ import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Component; @@ -33,6 +34,7 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity; import org.thingsboard.server.dao.model.sqlts.timescale.ts.TimescaleTsKvEntity; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper; @@ -43,11 +45,7 @@ import org.thingsboard.server.dao.util.TimescaleDBTsDao; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.UUID; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.function.Function; @@ -72,31 +70,81 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements @PostConstruct protected void init() { - super.init(); TbSqlBlockingQueueParams tsParams = TbSqlBlockingQueueParams.builder() .logName("TS Timescale") .batchSize(tsBatchSize) .maxDelay(tsMaxDelay) .statsPrintIntervalMs(tsStatsPrintIntervalMs) .statsNamePrefix("ts.timescale") + .batchSortEnabled(batchSortEnabled) .build(); Function hashcodeFunction = entity -> entity.getEntityId().hashCode(); tsQueue = new TbSqlBlockingQueueWrapper<>(tsParams, hashcodeFunction, timescaleBatchThreads, statsFactory); - tsQueue.init(logExecutor, v -> insertRepository.saveOrUpdate(v)); + tsQueue.init(logExecutor, v -> insertRepository.saveOrUpdate(v), + Comparator.comparing((Function) AbstractTsKvEntity::getEntityId) + .thenComparing(AbstractTsKvEntity::getKey) + .thenComparing(AbstractTsKvEntity::getTs) + ); } @PreDestroy protected void destroy() { - super.destroy(); if (tsQueue != null) { tsQueue.destroy(); } } @Override - protected ListenableFuture> findAllAsync(EntityId entityId, ReadTsKvQuery query) { + public ListenableFuture> findAllAsync(TenantId tenantId, EntityId entityId, List queries) { + return processFindAllAsync(tenantId, entityId, queries); + } + + @Override + public ListenableFuture save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl) { + int dataPointDays = getDataPointDays(tsKvEntry, computeTtl(ttl)); + String strKey = tsKvEntry.getKey(); + Integer keyId = getOrSaveKeyId(strKey); + TimescaleTsKvEntity entity = new TimescaleTsKvEntity(); + entity.setEntityId(entityId.getId()); + entity.setTs(tsKvEntry.getTs()); + entity.setKey(keyId); + entity.setStrValue(tsKvEntry.getStrValue().orElse(null)); + entity.setDoubleValue(tsKvEntry.getDoubleValue().orElse(null)); + entity.setLongValue(tsKvEntry.getLongValue().orElse(null)); + entity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null)); + entity.setJsonValue(tsKvEntry.getJsonValue().orElse(null)); + log.trace("Saving entity to timescale db: {}", entity); + return Futures.transform(tsQueue.add(entity), v -> dataPointDays, MoreExecutors.directExecutor()); + } + + @Override + public ListenableFuture savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key, long ttl) { + return Futures.immediateFuture(0); + } + + @Override + public ListenableFuture remove(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { + String strKey = query.getKey(); + Integer keyId = getOrSaveKeyId(strKey); + return service.submit(() -> { + tsKvRepository.delete( + entityId.getId(), + keyId, + query.getStartTs(), + query.getEndTs()); + return null; + }); + } + + @Override + public ListenableFuture removePartition(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { + return service.submit(() -> null); + } + + @Override + public ListenableFuture> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) { if (query.getAggregation() == Aggregation.NONE) { return findAllAsyncWithLimit(entityId, query); } else { @@ -108,8 +156,7 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements } } - @Override - protected ListenableFuture> findAllAsyncWithLimit(EntityId entityId, ReadTsKvQuery query) { + private ListenableFuture> findAllAsyncWithLimit(EntityId entityId, ReadTsKvQuery query) { String strKey = query.getKey(); Integer keyId = getOrSaveKeyId(strKey); List timescaleTsKvEntities = tsKvRepository.findAllWithLimit( @@ -119,7 +166,7 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements query.getEndTs(), PageRequest.of(0, query.getLimit(), Sort.by(Sort.Direction.fromString( - query.getOrderBy()), "ts"))); + query.getOrder()), "ts"))); timescaleTsKvEntities.forEach(tsKvEntity -> tsKvEntity.setStrKey(strKey)); return Futures.immediateFuture(DaoUtil.convertDataList(timescaleTsKvEntities)); } @@ -153,73 +200,6 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements }, MoreExecutors.directExecutor()); } - @Override - public ListenableFuture> findAllAsync(TenantId tenantId, EntityId entityId, List queries) { - return processFindAllAsync(tenantId, entityId, queries); - } - - @Override - public ListenableFuture findLatest(TenantId tenantId, EntityId entityId, String key) { - return getFindLatestFuture(entityId, key); - } - - @Override - public ListenableFuture> findAllLatest(TenantId tenantId, EntityId entityId) { - return getFindAllLatestFuture(entityId); - } - - @Override - public ListenableFuture save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl) { - String strKey = tsKvEntry.getKey(); - Integer keyId = getOrSaveKeyId(strKey); - TimescaleTsKvEntity entity = new TimescaleTsKvEntity(); - entity.setEntityId(entityId.getId()); - entity.setTs(tsKvEntry.getTs()); - entity.setKey(keyId); - entity.setStrValue(tsKvEntry.getStrValue().orElse(null)); - entity.setDoubleValue(tsKvEntry.getDoubleValue().orElse(null)); - entity.setLongValue(tsKvEntry.getLongValue().orElse(null)); - entity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null)); - entity.setJsonValue(tsKvEntry.getJsonValue().orElse(null)); - - log.trace("Saving entity to timescale db: {}", entity); - return tsQueue.add(entity); - } - - @Override - public ListenableFuture savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key, long ttl) { - return Futures.immediateFuture(null); - } - - @Override - public ListenableFuture saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { - return getSaveLatestFuture(entityId, tsKvEntry); - } - - @Override - public ListenableFuture remove(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { - String strKey = query.getKey(); - Integer keyId = getOrSaveKeyId(strKey); - return service.submit(() -> { - tsKvRepository.delete( - entityId.getId(), - keyId, - query.getStartTs(), - query.getEndTs()); - return null; - }); - } - - @Override - public ListenableFuture removeLatest(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { - return getRemoveLatestFuture(entityId, query); - } - - @Override - public ListenableFuture removePartition(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { - return service.submit(() -> null); - } - private CompletableFuture> switchAggregation(String key, long startTs, long endTs, long timeBucket, Aggregation aggregation, UUID entityId) { switch (aggregation) { case AVG: diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TsKvTimescaleRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TsKvTimescaleRepository.java index d4e80dc1f5..c6fb3574a0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TsKvTimescaleRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TsKvTimescaleRepository.java @@ -23,12 +23,12 @@ import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.dao.model.sqlts.timescale.ts.TimescaleTsKvCompositeKey; import org.thingsboard.server.dao.model.sqlts.timescale.ts.TimescaleTsKvEntity; -import org.thingsboard.server.dao.util.TimescaleDBTsDao; +import org.thingsboard.server.dao.util.TimescaleDBTsOrTsLatestDao; import java.util.List; import java.util.UUID; -@TimescaleDBTsDao +@TimescaleDBTsOrTsLatestDao public interface TsKvTimescaleRepository extends CrudRepository { @Query("SELECT tskv FROM TimescaleTsKvEntity tskv WHERE tskv.entityId = :entityId " + diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/TsKvRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/TsKvRepository.java index 5b446129a8..881de11fa5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/TsKvRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/TsKvRepository.java @@ -24,13 +24,11 @@ import org.springframework.scheduling.annotation.Async; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.dao.model.sqlts.ts.TsKvCompositeKey; import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity; -import org.thingsboard.server.dao.util.SqlDao; import java.util.List; import java.util.UUID; import java.util.concurrent.CompletableFuture; -@SqlDao public interface TsKvRepository extends CrudRepository { @Query("SELECT tskv FROM TsKvEntity tskv WHERE tskv.entityId = :entityId " + diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/CassandraTenantDao.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/CassandraTenantDao.java deleted file mode 100644 index b8896d0a80..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/CassandraTenantDao.java +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.tenant; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.Tenant; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; -import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.model.nosql.TenantEntity; -import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTextDao; -import org.thingsboard.server.dao.util.NoSqlDao; - -import java.util.Arrays; -import java.util.List; - -import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; -import static org.thingsboard.server.dao.model.ModelConstants.TENANT_BY_REGION_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.TENANT_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.TENANT_REGION_PROPERTY; - -@Component -@Slf4j -@NoSqlDao -public class CassandraTenantDao extends CassandraAbstractSearchTextDao implements TenantDao { - - @Override - protected Class getColumnFamilyClass() { - return TenantEntity.class; - } - - @Override - protected String getColumnFamilyName() { - return TENANT_COLUMN_FAMILY_NAME; - } - - @Override - public List findTenantsByRegion(TenantId tenantId, String region, TextPageLink pageLink) { - log.debug("Try to find tenants by region [{}] and pageLink [{}]", region, pageLink); - List tenantEntities = findPageWithTextSearch(tenantId, TENANT_BY_REGION_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME, - Arrays.asList(eq(TENANT_REGION_PROPERTY, region)), - pageLink); - log.trace("Found tenants [{}] by region [{}] and pageLink [{}]", tenantEntities, region, pageLink); - return DaoUtil.convertDataList(tenantEntities); - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/DefaultTbTenantProfileCache.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/DefaultTbTenantProfileCache.java new file mode 100644 index 0000000000..adf05e0f74 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/DefaultTbTenantProfileCache.java @@ -0,0 +1,101 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.tenant; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import org.thingsboard.server.dao.tenant.TenantProfileService; +import org.thingsboard.server.dao.tenant.TenantService; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@Service +@Slf4j +public class DefaultTbTenantProfileCache implements TbTenantProfileCache { + + private final Lock tenantProfileFetchLock = new ReentrantLock(); + private final TenantProfileService tenantProfileService; + private final TenantService tenantService; + + private final ConcurrentMap tenantProfilesMap = new ConcurrentHashMap<>(); + private final ConcurrentMap tenantsMap = new ConcurrentHashMap<>(); + + public DefaultTbTenantProfileCache(TenantProfileService tenantProfileService, TenantService tenantService) { + this.tenantProfileService = tenantProfileService; + this.tenantService = tenantService; + } + + @Override + public TenantProfile get(TenantProfileId tenantProfileId) { + TenantProfile profile = tenantProfilesMap.get(tenantProfileId); + if (profile == null) { + tenantProfileFetchLock.lock(); + try { + profile = tenantProfilesMap.get(tenantProfileId); + if (profile == null) { + profile = tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, tenantProfileId); + if (profile != null) { + tenantProfilesMap.put(tenantProfileId, profile); + } + } + } finally { + tenantProfileFetchLock.unlock(); + } + } + return profile; + } + + @Override + public TenantProfile get(TenantId tenantId) { + TenantProfileId profileId = tenantsMap.get(tenantId); + if (profileId == null) { + Tenant tenant = tenantService.findTenantById(tenantId); + if (tenant != null) { + profileId = tenant.getTenantProfileId(); + tenantsMap.put(tenantId, profileId); + } else { + return null; + } + } + return get(profileId); + } + + @Override + public void put(TenantProfile profile) { + if (profile.getId() != null) { + tenantProfilesMap.put(profile.getId(), profile); + } + } + + @Override + public void evict(TenantProfileId profileId) { + tenantProfilesMap.remove(profileId); + } + + @Override + public void evict(TenantId tenantId) { + tenantsMap.remove(tenantId); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantDao.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantDao.java index 22c67fb362..18fd3ff73e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantDao.java @@ -16,14 +16,18 @@ package org.thingsboard.server.dao.tenant; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; -import java.util.List; +import java.util.UUID; public interface TenantDao extends Dao { + TenantInfo findTenantInfoById(TenantId tenantId, UUID id); + /** * Save or update tenant object * @@ -39,6 +43,8 @@ public interface TenantDao extends Dao { * @param pageLink the page link * @return the list of tenant objects */ - List findTenantsByRegion(TenantId tenantId, String region, TextPageLink pageLink); + PageData findTenantsByRegion(TenantId tenantId, String region, PageLink pageLink); + + PageData findTenantInfosByRegion(TenantId tenantId, String region, PageLink pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileDao.java new file mode 100644 index 0000000000..497c21720d --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileDao.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.tenant; + +import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.Dao; + +import java.util.UUID; + +public interface TenantProfileDao extends Dao { + + EntityInfo findTenantProfileInfoById(TenantId tenantId, UUID tenantProfileId); + + TenantProfile save(TenantId tenantId, TenantProfile tenantProfile); + + PageData findTenantProfiles(TenantId tenantId, PageLink pageLink); + + PageData findTenantProfileInfos(TenantId tenantId, PageLink pageLink); + + TenantProfile findDefaultTenantProfile(TenantId tenantId); + + EntityInfo findDefaultTenantProfileInfo(TenantId tenantId); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java new file mode 100644 index 0000000000..95dd0ab45a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java @@ -0,0 +1,260 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.tenant; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.hibernate.exception.ConstraintViolationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; +import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.service.PaginatedRemover; +import org.thingsboard.server.dao.service.Validator; + +import java.util.Arrays; +import java.util.Collections; + +import static org.thingsboard.server.common.data.CacheConstants.TENANT_PROFILE_CACHE; +import static org.thingsboard.server.dao.service.Validator.validateId; + +@Service +@Slf4j +public class TenantProfileServiceImpl extends AbstractEntityService implements TenantProfileService { + + private static final String INCORRECT_TENANT_PROFILE_ID = "Incorrect tenantProfileId "; + + @Autowired + private TenantProfileDao tenantProfileDao; + + @Autowired + private CacheManager cacheManager; + + @Cacheable(cacheNames = TENANT_PROFILE_CACHE, key = "{#tenantProfileId.id}") + @Override + public TenantProfile findTenantProfileById(TenantId tenantId, TenantProfileId tenantProfileId) { + log.trace("Executing findTenantProfileById [{}]", tenantProfileId); + Validator.validateId(tenantProfileId, INCORRECT_TENANT_PROFILE_ID + tenantProfileId); + return tenantProfileDao.findById(tenantId, tenantProfileId.getId()); + } + + @Cacheable(cacheNames = TENANT_PROFILE_CACHE, key = "{'info', #tenantProfileId.id}") + @Override + public EntityInfo findTenantProfileInfoById(TenantId tenantId, TenantProfileId tenantProfileId) { + log.trace("Executing findTenantProfileInfoById [{}]", tenantProfileId); + Validator.validateId(tenantProfileId, INCORRECT_TENANT_PROFILE_ID + tenantProfileId); + return tenantProfileDao.findTenantProfileInfoById(tenantId, tenantProfileId.getId()); + } + + @Override + public TenantProfile saveTenantProfile(TenantId tenantId, TenantProfile tenantProfile) { + log.trace("Executing saveTenantProfile [{}]", tenantProfile); + tenantProfileValidator.validate(tenantProfile, (tenantProfile1) -> TenantId.SYS_TENANT_ID); + TenantProfile savedTenantProfile; + try { + savedTenantProfile = tenantProfileDao.save(tenantId, tenantProfile); + } catch (Exception t) { + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("tenant_profile_name_unq_key")) { + throw new DataValidationException("Tenant profile with such name already exists!"); + } else { + throw t; + } + } + Cache cache = cacheManager.getCache(TENANT_PROFILE_CACHE); + cache.evict(Collections.singletonList(savedTenantProfile.getId().getId())); + cache.evict(Arrays.asList("info", savedTenantProfile.getId().getId())); + if (savedTenantProfile.isDefault()) { + cache.evict(Collections.singletonList("default")); + cache.evict(Arrays.asList("default", "info")); + } + return savedTenantProfile; + } + + @Override + public void deleteTenantProfile(TenantId tenantId, TenantProfileId tenantProfileId) { + log.trace("Executing deleteTenantProfile [{}]", tenantProfileId); + validateId(tenantId, INCORRECT_TENANT_PROFILE_ID + tenantProfileId); + TenantProfile tenantProfile = tenantProfileDao.findById(tenantId, tenantProfileId.getId()); + if (tenantProfile != null && tenantProfile.isDefault()) { + throw new DataValidationException("Deletion of Default Tenant Profile is prohibited!"); + } + this.removeTenantProfile(tenantId, tenantProfileId); + } + + private void removeTenantProfile(TenantId tenantId, TenantProfileId tenantProfileId) { + try { + tenantProfileDao.removeById(tenantId, tenantProfileId.getId()); + } catch (Exception t) { + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_tenant_profile")) { + throw new DataValidationException("The tenant profile referenced by the tenants cannot be deleted!"); + } else { + throw t; + } + } + deleteEntityRelations(tenantId, tenantProfileId); + Cache cache = cacheManager.getCache(TENANT_PROFILE_CACHE); + cache.evict(Collections.singletonList(tenantProfileId.getId())); + cache.evict(Arrays.asList("info", tenantProfileId.getId())); + } + + @Override + public PageData findTenantProfiles(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findTenantProfiles pageLink [{}]", pageLink); + Validator.validatePageLink(pageLink); + return tenantProfileDao.findTenantProfiles(tenantId, pageLink); + } + + @Override + public PageData findTenantProfileInfos(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findTenantProfileInfos pageLink [{}]", pageLink); + Validator.validatePageLink(pageLink); + return tenantProfileDao.findTenantProfileInfos(tenantId, pageLink); + } + + @Override + public TenantProfile findOrCreateDefaultTenantProfile(TenantId tenantId) { + log.trace("Executing findOrCreateDefaultTenantProfile"); + TenantProfile defaultTenantProfile = findDefaultTenantProfile(tenantId); + if (defaultTenantProfile == null) { + defaultTenantProfile = new TenantProfile(); + defaultTenantProfile.setDefault(true); + defaultTenantProfile.setName("Default"); + TenantProfileData profileData = new TenantProfileData(); + profileData.setConfiguration(new DefaultTenantProfileConfiguration()); + defaultTenantProfile.setProfileData(profileData); + defaultTenantProfile.setDescription("Default tenant profile"); + defaultTenantProfile.setIsolatedTbCore(false); + defaultTenantProfile.setIsolatedTbRuleEngine(false); + defaultTenantProfile = saveTenantProfile(tenantId, defaultTenantProfile); + } + return defaultTenantProfile; + } + + @Cacheable(cacheNames = TENANT_PROFILE_CACHE, key = "{'default'}") + @Override + public TenantProfile findDefaultTenantProfile(TenantId tenantId) { + log.trace("Executing findDefaultTenantProfile"); + return tenantProfileDao.findDefaultTenantProfile(tenantId); + } + + @Cacheable(cacheNames = TENANT_PROFILE_CACHE, key = "{'default', 'info'}") + @Override + public EntityInfo findDefaultTenantProfileInfo(TenantId tenantId) { + log.trace("Executing findDefaultTenantProfileInfo"); + return tenantProfileDao.findDefaultTenantProfileInfo(tenantId); + } + + @Override + public boolean setDefaultTenantProfile(TenantId tenantId, TenantProfileId tenantProfileId) { + log.trace("Executing setDefaultTenantProfile [{}]", tenantProfileId); + validateId(tenantId, INCORRECT_TENANT_PROFILE_ID + tenantProfileId); + TenantProfile tenantProfile = tenantProfileDao.findById(tenantId, tenantProfileId.getId()); + if (!tenantProfile.isDefault()) { + Cache cache = cacheManager.getCache(TENANT_PROFILE_CACHE); + tenantProfile.setDefault(true); + TenantProfile previousDefaultTenantProfile = findDefaultTenantProfile(tenantId); + boolean changed = false; + if (previousDefaultTenantProfile == null) { + tenantProfileDao.save(tenantId, tenantProfile); + changed = true; + } else if (!previousDefaultTenantProfile.getId().equals(tenantProfile.getId())) { + previousDefaultTenantProfile.setDefault(false); + tenantProfileDao.save(tenantId, previousDefaultTenantProfile); + tenantProfileDao.save(tenantId, tenantProfile); + cache.evict(Collections.singletonList(previousDefaultTenantProfile.getId().getId())); + cache.evict(Arrays.asList("info", previousDefaultTenantProfile.getId().getId())); + changed = true; + } + if (changed) { + cache.evict(Collections.singletonList(tenantProfile.getId().getId())); + cache.evict(Arrays.asList("info", tenantProfile.getId().getId())); + cache.evict(Collections.singletonList("default")); + cache.evict(Arrays.asList("default", "info")); + } + return changed; + } + return false; + } + + @Override + public void deleteTenantProfiles(TenantId tenantId) { + log.trace("Executing deleteTenantProfiles"); + tenantProfilesRemover.removeEntities(tenantId, null); + } + + private DataValidator tenantProfileValidator = + new DataValidator() { + @Override + protected void validateDataImpl(TenantId tenantId, TenantProfile tenantProfile) { + if (StringUtils.isEmpty(tenantProfile.getName())) { + throw new DataValidationException("Tenant profile name should be specified!"); + } + if (tenantProfile.getProfileData() == null) { + throw new DataValidationException("Tenant profile data should be specified!"); + } + if (tenantProfile.getProfileData().getConfiguration() == null) { + throw new DataValidationException("Tenant profile data configuration should be specified!"); + } + if (tenantProfile.isDefault()) { + TenantProfile defaultTenantProfile = findDefaultTenantProfile(tenantId); + if (defaultTenantProfile != null && !defaultTenantProfile.getId().equals(tenantProfile.getId())) { + throw new DataValidationException("Another default tenant profile is present!"); + } + } + } + + @Override + protected void validateUpdate(TenantId tenantId, TenantProfile tenantProfile) { + TenantProfile old = tenantProfileDao.findById(TenantId.SYS_TENANT_ID, tenantProfile.getId().getId()); + if (old == null) { + throw new DataValidationException("Can't update non existing tenant profile!"); + } else if (old.isIsolatedTbRuleEngine() != tenantProfile.isIsolatedTbRuleEngine()) { + throw new DataValidationException("Can't update isolatedTbRuleEngine property!"); + } else if (old.isIsolatedTbCore() != tenantProfile.isIsolatedTbCore()) { + throw new DataValidationException("Can't update isolatedTbCore property!"); + } + } + }; + + private PaginatedRemover tenantProfilesRemover = + new PaginatedRemover() { + + @Override + protected PageData findEntities(TenantId tenantId, String id, PageLink pageLink) { + return tenantProfileDao.findTenantProfiles(tenantId, pageLink); + } + + @Override + protected void removeEntity(TenantId tenantId, TenantProfile entity) { + removeTenantProfile(tenantId, entity.getId()); + } + }; + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index eb916cdb83..3a0edf5eea 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -21,14 +21,16 @@ import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Tenant; -import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.TenantInfo; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; +import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.entityview.EntityViewService; @@ -37,11 +39,10 @@ import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; +import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.widget.WidgetsBundleService; -import java.util.List; - import static org.thingsboard.server.dao.service.Validator.validateId; @Service @@ -54,6 +55,9 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe @Autowired private TenantDao tenantDao; + @Autowired + private TenantProfileService tenantProfileService; + @Autowired private UserService userService; @@ -66,6 +70,12 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe @Autowired private DeviceService deviceService; + @Autowired + private DeviceProfileService deviceProfileService; + + @Autowired + private ApiUsageStateService apiUsageStateService; + @Autowired private EntityViewService entityViewService; @@ -85,6 +95,13 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe return tenantDao.findById(tenantId, tenantId.getId()); } + @Override + public TenantInfo findTenantInfoById(TenantId tenantId) { + log.trace("Executing findTenantInfoById [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + return tenantDao.findTenantInfoById(tenantId, tenantId.getId()); + } + @Override public ListenableFuture findTenantByIdAsync(TenantId callerId, TenantId tenantId) { log.trace("Executing TenantIdAsync [{}]", tenantId); @@ -96,8 +113,17 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe public Tenant saveTenant(Tenant tenant) { log.trace("Executing saveTenant [{}]", tenant); tenant.setRegion(DEFAULT_TENANT_REGION); + if (tenant.getTenantProfileId() == null) { + TenantProfile tenantProfile = this.tenantProfileService.findOrCreateDefaultTenantProfile(TenantId.SYS_TENANT_ID); + tenant.setTenantProfileId(tenantProfile.getId()); + } tenantValidator.validate(tenant, Tenant::getId); - return tenantDao.save(tenant.getId(), tenant); + Tenant savedTenant = tenantDao.save(tenant.getId(), tenant); + if (tenant.getId() == null) { + deviceProfileService.createDefaultDeviceProfile(savedTenant.getId()); + apiUsageStateService.createDefaultApiUsageState(savedTenant.getId()); + } + return savedTenant; } @Override @@ -110,18 +136,26 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe entityViewService.deleteEntityViewsByTenantId(tenantId); assetService.deleteAssetsByTenantId(tenantId); deviceService.deleteDevicesByTenantId(tenantId); + deviceProfileService.deleteDeviceProfilesByTenantId(tenantId); userService.deleteTenantAdmins(tenantId); ruleChainService.deleteRuleChainsByTenantId(tenantId); + apiUsageStateService.deleteApiUsageStateByTenantId(tenantId); tenantDao.removeById(tenantId, tenantId.getId()); deleteEntityRelations(tenantId, tenantId); } @Override - public TextPageData findTenants(TextPageLink pageLink) { + public PageData findTenants(PageLink pageLink) { log.trace("Executing findTenants pageLink [{}]", pageLink); - Validator.validatePageLink(pageLink, "Incorrect page link " + pageLink); - List tenants = tenantDao.findTenantsByRegion(new TenantId(EntityId.NULL_UUID), DEFAULT_TENANT_REGION, pageLink); - return new TextPageData<>(tenants, pageLink); + Validator.validatePageLink(pageLink); + return tenantDao.findTenantsByRegion(new TenantId(EntityId.NULL_UUID), DEFAULT_TENANT_REGION, pageLink); + } + + @Override + public PageData findTenantInfos(PageLink pageLink) { + log.trace("Executing findTenantInfos pageLink [{}]", pageLink); + Validator.validatePageLink(pageLink); + return tenantDao.findTenantInfosByRegion(new TenantId(EntityId.NULL_UUID), DEFAULT_TENANT_REGION, pageLink); } @Override @@ -147,10 +181,6 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe Tenant old = tenantDao.findById(TenantId.SYS_TENANT_ID, tenantId.getId()); if (old == null) { throw new DataValidationException("Can't update non existing tenant!"); - } else if (old.isIsolatedTbRuleEngine() != tenant.isIsolatedTbRuleEngine()) { - throw new DataValidationException("Can't update isolatedTbRuleEngine property!"); - } else if (old.isIsolatedTbCore() != tenant.isIsolatedTbCore()) { - throw new DataValidationException("Can't update isolatedTbCore property!"); } } }; @@ -159,7 +189,7 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe new PaginatedRemover() { @Override - protected List findEntities(TenantId tenantId, String region, TextPageLink pageLink) { + protected PageData findEntities(TenantId tenantId, String region, PageLink pageLink) { return tenantDao.findTenantsByRegion(tenantId, region, pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/AbstractCassandraBaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/AbstractCassandraBaseTimeseriesDao.java new file mode 100644 index 0000000000..d27d306b31 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/AbstractCassandraBaseTimeseriesDao.java @@ -0,0 +1,107 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.timeseries; + +import com.datastax.oss.driver.api.core.cql.Row; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.BooleanDataEntry; +import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.nosql.CassandraAbstractAsyncDao; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Slf4j +public abstract class AbstractCassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao { + public static final String DESC_ORDER = "DESC"; + public static final String GENERATED_QUERY_FOR_ENTITY_TYPE_AND_ENTITY_ID = "Generated query [{}] for entityType {} and entityId {}"; + public static final String INSERT_INTO = "INSERT INTO "; + public static final String SELECT_PREFIX = "SELECT "; + public static final String EQUALS_PARAM = " = ? "; + + public static KvEntry toKvEntry(Row row, String key) { + KvEntry kvEntry = null; + String strV = row.get(ModelConstants.STRING_VALUE_COLUMN, String.class); + if (strV != null) { + kvEntry = new StringDataEntry(key, strV); + } else { + Long longV = row.get(ModelConstants.LONG_VALUE_COLUMN, Long.class); + if (longV != null) { + kvEntry = new LongDataEntry(key, longV); + } else { + Double doubleV = row.get(ModelConstants.DOUBLE_VALUE_COLUMN, Double.class); + if (doubleV != null) { + kvEntry = new DoubleDataEntry(key, doubleV); + } else { + Boolean boolV = row.get(ModelConstants.BOOLEAN_VALUE_COLUMN, Boolean.class); + if (boolV != null) { + kvEntry = new BooleanDataEntry(key, boolV); + } else { + String jsonV = row.get(ModelConstants.JSON_VALUE_COLUMN, String.class); + if (StringUtils.isNoneEmpty(jsonV)) { + kvEntry = new JsonDataEntry(key, jsonV); + } else { + log.warn("All values in key-value row are nullable "); + } + } + } + } + } + return kvEntry; + } + + protected List convertResultToTsKvEntryList(List rows) { + List entries = new ArrayList<>(rows.size()); + if (!rows.isEmpty()) { + rows.forEach(row -> entries.add(convertResultToTsKvEntry(row))); + } + return entries; + } + + private TsKvEntry convertResultToTsKvEntry(Row row) { + String key = row.getString(ModelConstants.KEY_COLUMN); + long ts = row.getLong(ModelConstants.TS_COLUMN); + return new BasicTsKvEntry(ts, toKvEntry(row, key)); + } + + protected TsKvEntry convertResultToTsKvEntry(String key, Row row) { + if (row != null) { + Optional foundKeyOpt = getKey(row); + long ts = row.getLong(ModelConstants.TS_COLUMN); + return new BasicTsKvEntry(ts, toKvEntry(row, foundKeyOpt.orElse(key))); + } else { + return new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry(key, null)); + } + } + + private Optional getKey(Row row){ + try{ + return Optional.ofNullable(row.getString(ModelConstants.KEY_COLUMN)); + } catch (IllegalArgumentException e){ + return Optional.empty(); + } + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/AggregatePartitionsFunction.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/AggregatePartitionsFunction.java index 6229f68818..43b221d315 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/AggregatePartitionsFunction.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/AggregatePartitionsFunction.java @@ -15,8 +15,9 @@ */ package org.thingsboard.server.dao.timeseries; -import com.datastax.driver.core.ResultSet; -import com.datastax.driver.core.Row; +import com.datastax.oss.driver.api.core.cql.Row; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; @@ -27,16 +28,19 @@ import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.dao.nosql.TbResultSet; import javax.annotation.Nullable; import java.util.List; import java.util.Optional; +import java.util.concurrent.Executor; +import java.util.stream.Collectors; /** * Created by ashvayka on 20.02.17. */ @Slf4j -public class AggregatePartitionsFunction implements com.google.common.base.Function, Optional> { +public class AggregatePartitionsFunction implements com.google.common.util.concurrent.AsyncFunction, Optional> { private static final int LONG_CNT_POS = 0; private static final int DOUBLE_CNT_POS = 1; @@ -52,33 +56,39 @@ public class AggregatePartitionsFunction implements com.google.common.base.Funct private final Aggregation aggregation; private final String key; private final long ts; + private final Executor executor; - public AggregatePartitionsFunction(Aggregation aggregation, String key, long ts) { + public AggregatePartitionsFunction(Aggregation aggregation, String key, long ts, Executor executor) { this.aggregation = aggregation; this.key = key; this.ts = ts; + this.executor = executor; } @Override - public Optional apply(@Nullable List rsList) { - try { - log.trace("[{}][{}][{}] Going to aggregate data", key, ts, aggregation); - if (rsList == null || rsList.isEmpty()) { - return Optional.empty(); - } - - AggregationResult aggResult = new AggregationResult(); - - for (ResultSet rs : rsList) { - for (Row row : rs.all()) { - processResultSetRow(row, aggResult); + public ListenableFuture> apply(@Nullable List rsList) { + log.trace("[{}][{}][{}] Going to aggregate data", key, ts, aggregation); + if (rsList == null || rsList.isEmpty()) { + return Futures.immediateFuture(Optional.empty()); + } + return Futures.transform( + Futures.allAsList( + rsList.stream().map(rs -> rs.allRows(this.executor)) + .collect(Collectors.toList())), + rowsList -> { + try { + AggregationResult aggResult = new AggregationResult(); + for (List rs : rowsList) { + for (Row row : rs) { + processResultSetRow(row, aggResult); + } } + return processAggregationResult(aggResult); + } catch (Exception e) { + log.error("[{}][{}][{}] Failed to aggregate data", key, ts, aggregation, e); + return Optional.empty(); } - return processAggregationResult(aggResult); - } catch (Exception e) { - log.error("[{}][{}][{}] Failed to aggregate data", key, ts, aggregation, e); - return Optional.empty(); - } + }, this.executor); } private void processResultSetRow(Row row, AggregationResult aggResult) { @@ -181,7 +191,7 @@ public class AggregatePartitionsFunction implements com.google.common.base.Funct private Boolean getBooleanValue(Row row) { if (aggregation == Aggregation.MIN || aggregation == Aggregation.MAX) { - return row.getBool(BOOL_POS); + return row.getBoolean(BOOL_POS); } else { return null; //NOSONAR, null is used for further comparison } diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java index 3486369999..3b97e2bb82 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java @@ -15,10 +15,13 @@ */ package org.thingsboard.server.dao.timeseries; +import com.google.common.base.Function; import com.google.common.collect.Lists; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -28,6 +31,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.Aggregation; +import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; @@ -38,6 +42,7 @@ import org.thingsboard.server.dao.service.Validator; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -52,6 +57,20 @@ public class BaseTimeseriesService implements TimeseriesService { private static final int INSERTS_PER_ENTRY = 3; private static final int DELETES_PER_ENTRY = INSERTS_PER_ENTRY; + public static final Function, Integer> SUM_ALL_INTEGERS = new Function, Integer>() { + @Override + public @Nullable Integer apply(@Nullable List input) { + int result = 0; + if (input != null) { + for (Integer tmp : input) { + if (tmp != null) { + result += tmp; + } + } + } + return result; + } + }; @Value("${database.ts_max_intervals}") private long maxTsIntervals; @@ -59,6 +78,9 @@ public class BaseTimeseriesService implements TimeseriesService { @Autowired private TimeseriesDao timeseriesDao; + @Autowired + private TimeseriesLatestDao timeseriesLatestDao; + @Autowired private EntityViewService entityViewService; @@ -68,9 +90,11 @@ public class BaseTimeseriesService implements TimeseriesService { queries.forEach(this::validate); if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) { EntityView entityView = entityViewService.findEntityViewById(tenantId, (EntityViewId) entityId); + List keys = entityView.getKeys() != null && entityView.getKeys().getTimeseries() != null ? + entityView.getKeys().getTimeseries() : Collections.emptyList(); List filteredQueries = queries.stream() - .filter(query -> entityView.getKeys().getTimeseries().isEmpty() || entityView.getKeys().getTimeseries().contains(query.getKey())) + .filter(query -> keys.isEmpty() || keys.contains(query.getKey())) .collect(Collectors.toList()); return timeseriesDao.findAllAsync(tenantId, entityView.getEntityId(), updateQueriesForEntityView(entityView, filteredQueries)); } @@ -82,76 +106,57 @@ public class BaseTimeseriesService implements TimeseriesService { validate(entityId); List> futures = Lists.newArrayListWithExpectedSize(keys.size()); keys.forEach(key -> Validator.validateString(key, "Incorrect key " + key)); - if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) { - EntityView entityView = entityViewService.findEntityViewById(tenantId, (EntityViewId) entityId); - List filteredKeys = new ArrayList<>(keys); - if (entityView.getKeys() != null && entityView.getKeys().getTimeseries() != null && - !entityView.getKeys().getTimeseries().isEmpty()) { - filteredKeys.retainAll(entityView.getKeys().getTimeseries()); - } - List queries = - filteredKeys.stream() - .map(key -> { - long endTs = entityView.getEndTimeMs() != 0 ? entityView.getEndTimeMs() : Long.MAX_VALUE; - return new BaseReadTsKvQuery(key, entityView.getStartTimeMs(), endTs, 1, "DESC"); - }) - .collect(Collectors.toList()); - - if (queries.size() > 0) { - return timeseriesDao.findAllAsync(tenantId, entityView.getEntityId(), queries); - } else { - return Futures.immediateFuture(new ArrayList<>()); - } - } - keys.forEach(key -> futures.add(timeseriesDao.findLatest(tenantId, entityId, key))); + keys.forEach(key -> futures.add(timeseriesLatestDao.findLatest(tenantId, entityId, key))); return Futures.allAsList(futures); } @Override public ListenableFuture> findAllLatest(TenantId tenantId, EntityId entityId) { validate(entityId); - if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) { - EntityView entityView = entityViewService.findEntityViewById(tenantId, (EntityViewId) entityId); - if (entityView.getKeys() != null && entityView.getKeys().getTimeseries() != null && - !entityView.getKeys().getTimeseries().isEmpty()) { - return findLatest(tenantId, entityId, entityView.getKeys().getTimeseries()); - } else { - return Futures.immediateFuture(new ArrayList<>()); - } - } else { - return timeseriesDao.findAllLatest(tenantId, entityId); - } + return timeseriesLatestDao.findAllLatest(tenantId, entityId); } @Override - public ListenableFuture> save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { + public ListenableFuture save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { validate(entityId); if (tsKvEntry == null) { throw new IncorrectParameterException("Key value entry can't be null"); } - List> futures = Lists.newArrayListWithExpectedSize(INSERTS_PER_ENTRY); + List> futures = Lists.newArrayListWithExpectedSize(INSERTS_PER_ENTRY); saveAndRegisterFutures(tenantId, futures, entityId, tsKvEntry, 0L); - return Futures.allAsList(futures); + return Futures.transform(Futures.allAsList(futures), SUM_ALL_INTEGERS, MoreExecutors.directExecutor()); } @Override - public ListenableFuture> save(TenantId tenantId, EntityId entityId, List tsKvEntries, long ttl) { - List> futures = Lists.newArrayListWithExpectedSize(tsKvEntries.size() * INSERTS_PER_ENTRY); + public ListenableFuture save(TenantId tenantId, EntityId entityId, List tsKvEntries, long ttl) { + List> futures = Lists.newArrayListWithExpectedSize(tsKvEntries.size() * INSERTS_PER_ENTRY); for (TsKvEntry tsKvEntry : tsKvEntries) { if (tsKvEntry == null) { throw new IncorrectParameterException("Key value entry can't be null"); } saveAndRegisterFutures(tenantId, futures, entityId, tsKvEntry, ttl); } + return Futures.transform(Futures.allAsList(futures), SUM_ALL_INTEGERS, MoreExecutors.directExecutor()); + } + + @Override + public ListenableFuture> saveLatest(TenantId tenantId, EntityId entityId, List tsKvEntries) { + List> futures = Lists.newArrayListWithExpectedSize(tsKvEntries.size()); + for (TsKvEntry tsKvEntry : tsKvEntries) { + if (tsKvEntry == null) { + throw new IncorrectParameterException("Key value entry can't be null"); + } + futures.add(timeseriesLatestDao.saveLatest(tenantId, entityId, tsKvEntry)); + } return Futures.allAsList(futures); } - private void saveAndRegisterFutures(TenantId tenantId, List> futures, EntityId entityId, TsKvEntry tsKvEntry, long ttl) { + private void saveAndRegisterFutures(TenantId tenantId, List> futures, EntityId entityId, TsKvEntry tsKvEntry, long ttl) { if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) { throw new IncorrectParameterException("Telemetry data can't be stored for entity view. Read only"); } futures.add(timeseriesDao.savePartition(tenantId, entityId, tsKvEntry.getTs(), tsKvEntry.getKey(), ttl)); - futures.add(timeseriesDao.saveLatest(tenantId, entityId, tsKvEntry)); + futures.add(Futures.transform(timeseriesLatestDao.saveLatest(tenantId, entityId, tsKvEntry), v -> 0, MoreExecutors.directExecutor())); futures.add(timeseriesDao.save(tenantId, entityId, tsKvEntry, ttl)); } @@ -170,7 +175,7 @@ public class BaseTimeseriesService implements TimeseriesService { } else { endTs = query.getEndTs(); } - return new BaseReadTsKvQuery(query.getKey(), startTs, endTs, query.getInterval(), query.getLimit(), query.getAggregation(), query.getOrderBy()); + return new BaseReadTsKvQuery(query.getKey(), startTs, endTs, query.getInterval(), query.getLimit(), query.getAggregation(), query.getOrder()); }).collect(Collectors.toList()); } @@ -185,9 +190,33 @@ public class BaseTimeseriesService implements TimeseriesService { return Futures.allAsList(futures); } + @Override + public ListenableFuture> removeLatest(TenantId tenantId, EntityId entityId, Collection keys) { + validate(entityId); + List> futures = Lists.newArrayListWithExpectedSize(keys.size()); + for (String key : keys) { + DeleteTsKvQuery query = new BaseDeleteTsKvQuery(key, 0, System.currentTimeMillis(), false); + futures.add(timeseriesLatestDao.removeLatest(tenantId, entityId, query)); + } + return Futures.allAsList(futures); + } + + @Override + public ListenableFuture> removeAllLatest(TenantId tenantId, EntityId entityId) { + validate(entityId); + return Futures.transformAsync(this.findAllLatest(tenantId, entityId), latest -> { + if (!latest.isEmpty()) { + Collection keys = latest.stream().map(TsKvEntry::getKey).collect(Collectors.toList()); + return Futures.transform(this.removeLatest(tenantId, entityId, keys), res -> keys, MoreExecutors.directExecutor()); + } else { + return Futures.immediateFuture(Collections.emptyList()); + } + }, MoreExecutors.directExecutor()); + } + private void deleteAndRegisterFutures(TenantId tenantId, List> futures, EntityId entityId, DeleteTsKvQuery query) { futures.add(timeseriesDao.remove(tenantId, entityId, query)); - futures.add(timeseriesDao.removeLatest(tenantId, entityId, query)); + futures.add(timeseriesLatestDao.removeLatest(tenantId, entityId, query)); futures.add(timeseriesDao.removePartition(tenantId, entityId, query)); } @@ -203,7 +232,7 @@ public class BaseTimeseriesService implements TimeseriesService { } else if (query.getAggregation() == null) { throw new IncorrectParameterException("Incorrect ReadTsKvQuery. Aggregation can't be empty"); } - if(!Aggregation.NONE.equals(query.getAggregation())) { + if (!Aggregation.NONE.equals(query.getAggregation())) { long step = Math.max(query.getInterval(), 1000); long intervalCounts = (query.getEndTs() - query.getStartTs()) / step; if (intervalCounts > maxTsIntervals || intervalCounts < 0) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java index b96462e350..b9d8c62833 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java @@ -15,14 +15,13 @@ */ package org.thingsboard.server.dao.timeseries; -import com.datastax.driver.core.BoundStatement; -import com.datastax.driver.core.PreparedStatement; -import com.datastax.driver.core.ResultSet; -import com.datastax.driver.core.ResultSetFuture; -import com.datastax.driver.core.Row; -import com.datastax.driver.core.Statement; -import com.datastax.driver.core.querybuilder.QueryBuilder; -import com.datastax.driver.core.querybuilder.Select; +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.BoundStatement; +import com.datastax.oss.driver.api.core.cql.BoundStatementBuilder; +import com.datastax.oss.driver.api.core.cql.PreparedStatement; +import com.datastax.oss.driver.api.core.cql.Row; +import com.datastax.oss.driver.api.querybuilder.QueryBuilder; +import com.datastax.oss.driver.api.querybuilder.select.Select; import com.google.common.base.Function; import com.google.common.util.concurrent.AsyncFunction; import com.google.common.util.concurrent.FutureCallback; @@ -30,7 +29,6 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.env.Environment; @@ -39,19 +37,15 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; -import org.thingsboard.server.common.data.kv.BasicTsKvEntry; -import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; -import org.thingsboard.server.common.data.kv.DoubleDataEntry; -import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.common.data.kv.KvEntry; -import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; -import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.nosql.CassandraAbstractAsyncDao; +import org.thingsboard.server.dao.nosql.TbResultSet; +import org.thingsboard.server.dao.nosql.TbResultSetFuture; +import org.thingsboard.server.dao.sqlts.AggregationTimeseriesDao; import org.thingsboard.server.dao.util.NoSqlTsDao; import javax.annotation.Nullable; @@ -66,10 +60,10 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal; /** * @author Andrew Shvayka @@ -77,16 +71,13 @@ import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; @Component @Slf4j @NoSqlTsDao -public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implements TimeseriesDao { +public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesDao implements TimeseriesDao, AggregationTimeseriesDao { - private static final int MIN_AGGREGATION_STEP_MS = 1000; - public static final String INSERT_INTO = "INSERT INTO "; - public static final String GENERATED_QUERY_FOR_ENTITY_TYPE_AND_ENTITY_ID = "Generated query [{}] for entityType {} and entityId {}"; - public static final String SELECT_PREFIX = "SELECT "; - public static final String EQUALS_PARAM = " = ? "; + protected static final int MIN_AGGREGATION_STEP_MS = 1000; public static final String ASC_ORDER = "ASC"; - public static final String DESC_ORDER = "DESC"; - private static List FIXED_PARTITION = Arrays.asList(new Long[]{0L}); + public static final long SECONDS_IN_DAY = TimeUnit.DAYS.toSeconds(1); + + protected static List FIXED_PARTITION = Arrays.asList(new Long[]{0L}); @Autowired private Environment environment; @@ -104,13 +95,10 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem private PreparedStatement partitionInsertStmt; private PreparedStatement partitionInsertTtlStmt; - private PreparedStatement latestInsertStmt; private PreparedStatement[] saveStmts; private PreparedStatement[] saveTtlStmts; private PreparedStatement[] fetchStmtsAsc; private PreparedStatement[] fetchStmtsDesc; - private PreparedStatement findLatestStmt; - private PreparedStatement findAllLatestStmt; private PreparedStatement deleteStmt; private PreparedStatement deletePartitionStmt; @@ -123,13 +111,13 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem super.startExecutor(); if (!isInstall()) { getFetchStmt(Aggregation.NONE, DESC_ORDER); - Optional partition = NoSqlTsPartitionDate.parse(partitioning); - if (partition.isPresent()) { - tsFormat = partition.get(); - } else { - log.warn("Incorrect configuration of partitioning {}", partitioning); - throw new RuntimeException("Failed to parse partitioning property: " + partitioning + "!"); - } + } + Optional partition = NoSqlTsPartitionDate.parse(partitioning); + if (partition.isPresent()) { + tsFormat = partition.get(); + } else { + log.warn("Incorrect configuration of partitioning {}", partitioning); + throw new RuntimeException("Failed to parse partitioning property: " + partitioning + "!"); } } @@ -155,8 +143,114 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem }, readResultsProcessingExecutor); } + @Override + public ListenableFuture save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl) { + List> futures = new ArrayList<>(); + ttl = computeTtl(ttl); + int dataPointDays = tsKvEntry.getDataPoints() * Math.max(1, (int) (ttl / SECONDS_IN_DAY)); + long partition = toPartitionTs(tsKvEntry.getTs()); + DataType type = tsKvEntry.getDataType(); + if (setNullValuesEnabled) { + processSetNullValues(tenantId, entityId, tsKvEntry, ttl, futures, partition, type); + } + BoundStatementBuilder stmtBuilder = new BoundStatementBuilder((ttl == 0 ? getSaveStmt(type) : getSaveTtlStmt(type)).bind()); + stmtBuilder.setString(0, entityId.getEntityType().name()) + .setUuid(1, entityId.getId()) + .setString(2, tsKvEntry.getKey()) + .setLong(3, partition) + .setLong(4, tsKvEntry.getTs()); + addValue(tsKvEntry, stmtBuilder, 5); + if (ttl > 0) { + stmtBuilder.setInt(6, (int) ttl); + } + BoundStatement stmt = stmtBuilder.build(); + futures.add(getFuture(executeAsyncWrite(tenantId, stmt), rs -> null)); + return Futures.transform(Futures.allAsList(futures), result -> dataPointDays, MoreExecutors.directExecutor()); + } + + @Override + public ListenableFuture savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key, long ttl) { + if (isFixedPartitioning()) { + return Futures.immediateFuture(null); + } + ttl = computeTtl(ttl); + long partition = toPartitionTs(tsKvEntryTs); + log.debug("Saving partition {} for the entity [{}-{}] and key {}", partition, entityId.getEntityType(), entityId.getId(), key); + BoundStatementBuilder stmtBuilder = new BoundStatementBuilder((ttl == 0 ? getPartitionInsertStmt() : getPartitionInsertTtlStmt()).bind()); + stmtBuilder.setString(0, entityId.getEntityType().name()) + .setUuid(1, entityId.getId()) + .setLong(2, partition) + .setString(3, key); + if (ttl > 0) { + stmtBuilder.setInt(4, (int) ttl); + } + BoundStatement stmt = stmtBuilder.build(); + return getFuture(executeAsyncWrite(tenantId, stmt), rs -> 0); + } + + @Override + public ListenableFuture remove(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { + long minPartition = toPartitionTs(query.getStartTs()); + long maxPartition = toPartitionTs(query.getEndTs()); + + TbResultSetFuture partitionsFuture = fetchPartitions(tenantId, entityId, query.getKey(), minPartition, maxPartition); + + final SimpleListenableFuture resultFuture = new SimpleListenableFuture<>(); + final ListenableFuture> partitionsListFuture = Futures.transformAsync(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor); - private ListenableFuture> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) { + Futures.addCallback(partitionsListFuture, new FutureCallback>() { + @Override + public void onSuccess(@Nullable List partitions) { + QueryCursor cursor = new QueryCursor(entityId.getEntityType().name(), entityId.getId(), query, partitions); + deleteAsync(tenantId, cursor, resultFuture); + } + + @Override + public void onFailure(Throwable t) { + log.error("[{}][{}] Failed to fetch partitions for interval {}-{}", entityId.getEntityType().name(), entityId.getId(), minPartition, maxPartition, t); + } + }, readResultsProcessingExecutor); + return resultFuture; + } + + @Override + public ListenableFuture removePartition(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { + long minPartition = toPartitionTs(query.getStartTs()); + long maxPartition = toPartitionTs(query.getEndTs()); + if (minPartition == maxPartition) { + return Futures.immediateFuture(null); + } else { + TbResultSetFuture partitionsFuture = fetchPartitions(tenantId, entityId, query.getKey(), minPartition, maxPartition); + + final SimpleListenableFuture resultFuture = new SimpleListenableFuture<>(); + final ListenableFuture> partitionsListFuture = Futures.transformAsync(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor); + + Futures.addCallback(partitionsListFuture, new FutureCallback>() { + @Override + public void onSuccess(@Nullable List partitions) { + int index = 0; + if (minPartition != query.getStartTs()) { + index = 1; + } + List partitionsToDelete = new ArrayList<>(); + for (int i = index; i < partitions.size() - 1; i++) { + partitionsToDelete.add(partitions.get(i)); + } + QueryCursor cursor = new QueryCursor(entityId.getEntityType().name(), entityId.getId(), query, partitionsToDelete); + deletePartitionAsync(tenantId, cursor, resultFuture); + } + + @Override + public void onFailure(Throwable t) { + log.error("[{}][{}] Failed to fetch partitions for interval {}-{}", entityId.getEntityType().name(), entityId.getId(), minPartition, maxPartition, t); + } + }, readResultsProcessingExecutor); + return resultFuture; + } + } + + @Override + public ListenableFuture> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) { if (query.getAggregation() == Aggregation.NONE) { return findAllAsyncWithLimit(tenantId, entityId, query); } else { @@ -166,7 +260,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem while (stepTs < query.getEndTs()) { long startTs = stepTs; long endTs = stepTs + step; - ReadTsKvQuery subQuery = new BaseReadTsKvQuery(query.getKey(), startTs, endTs, step, 1, query.getAggregation(), query.getOrderBy()); + ReadTsKvQuery subQuery = new BaseReadTsKvQuery(query.getKey(), startTs, endTs, step, 1, query.getAggregation(), query.getOrder()); futures.add(findAndAggregateAsync(tenantId, entityId, subQuery, toPartitionTs(startTs), toPartitionTs(endTs))); stepTs = endTs; } @@ -181,18 +275,6 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem } } - public boolean isFixedPartitioning() { - return tsFormat.getTruncateUnit().equals(ChronoUnit.FOREVER); - } - - private ListenableFuture> getPartitionsFuture(TenantId tenantId, ReadTsKvQuery query, EntityId entityId, long minPartition, long maxPartition) { - if (isFixedPartitioning()) { //no need to fetch partitions from DB - return Futures.immediateFuture(FIXED_PARTITION); - } - ResultSetFuture partitionsFuture = fetchPartitions(tenantId, entityId, query.getKey(), minPartition, maxPartition); - return Futures.transform(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor); - } - private ListenableFuture> findAllAsyncWithLimit(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) { long minPartition = toPartitionTs(query.getStartTs()); long maxPartition = toPartitionTs(query.getEndTs()); @@ -225,20 +307,41 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem resultFuture.set(cursor.getData()); } else { PreparedStatement proto = getFetchStmt(Aggregation.NONE, cursor.getOrderBy()); - BoundStatement stmt = proto.bind(); - stmt.setString(0, cursor.getEntityType()); - stmt.setUUID(1, cursor.getEntityId()); - stmt.setString(2, cursor.getKey()); - stmt.setLong(3, cursor.getNextPartition()); - stmt.setLong(4, cursor.getStartTs()); - stmt.setLong(5, cursor.getEndTs()); - stmt.setInt(6, cursor.getCurrentLimit()); - - Futures.addCallback(executeAsyncRead(tenantId, stmt), new FutureCallback() { + BoundStatementBuilder stmtBuilder = new BoundStatementBuilder(proto.bind()); + + stmtBuilder.setString(0, cursor.getEntityType()); + stmtBuilder.setUuid(1, cursor.getEntityId()); + stmtBuilder.setString(2, cursor.getKey()); + stmtBuilder.setLong(3, cursor.getNextPartition()); + stmtBuilder.setLong(4, cursor.getStartTs()); + stmtBuilder.setLong(5, cursor.getEndTs()); + stmtBuilder.setInt(6, cursor.getCurrentLimit()); + + BoundStatement stmt = stmtBuilder.build(); + + Futures.addCallback(executeAsyncRead(tenantId, stmt), new FutureCallback() { @Override - public void onSuccess(@Nullable ResultSet result) { - cursor.addData(convertResultToTsKvEntryList(result == null ? Collections.emptyList() : result.all())); - findAllAsyncSequentiallyWithLimit(tenantId, cursor, resultFuture); + public void onSuccess(@Nullable TbResultSet result) { + if (result == null) { + cursor.addData(convertResultToTsKvEntryList(Collections.emptyList())); + findAllAsyncSequentiallyWithLimit(tenantId, cursor, resultFuture); + } else { + Futures.addCallback(result.allRows(readResultsProcessingExecutor), new FutureCallback>() { + + @Override + public void onSuccess(@Nullable List result) { + cursor.addData(convertResultToTsKvEntryList(result == null ? Collections.emptyList() : result)); + findAllAsyncSequentiallyWithLimit(tenantId, cursor, resultFuture); + } + + @Override + public void onFailure(Throwable t) { + log.error("[{}][{}] Failed to fetch data for query {}-{}", stmt, t); + } + }, readResultsProcessingExecutor); + + + } } @Override @@ -256,31 +359,43 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem final long endTs = query.getEndTs(); final long ts = startTs + (endTs - startTs) / 2; ListenableFuture> partitionsListFuture = getPartitionsFuture(tenantId, query, entityId, minPartition, maxPartition); - ListenableFuture> aggregationChunks = Futures.transformAsync(partitionsListFuture, + ListenableFuture> aggregationChunks = Futures.transformAsync(partitionsListFuture, getFetchChunksAsyncFunction(tenantId, entityId, key, aggregation, startTs, endTs), readResultsProcessingExecutor); - return Futures.transform(aggregationChunks, new AggregatePartitionsFunction(aggregation, key, ts), readResultsProcessingExecutor); + return Futures.transformAsync(aggregationChunks, new AggregatePartitionsFunction(aggregation, key, ts, readResultsProcessingExecutor), readResultsProcessingExecutor); } - private Function> getPartitionsArrayFunction() { - return rows -> rows.all().stream() - .map(row -> row.getLong(ModelConstants.PARTITION_COLUMN)).collect(Collectors.toList()); + private AsyncFunction> getPartitionsArrayFunction() { + return rs -> + Futures.transform(rs.allRows(readResultsProcessingExecutor), rows -> + rows.stream() + .map(row -> row.getLong(ModelConstants.PARTITION_COLUMN)).collect(Collectors.toList()), + readResultsProcessingExecutor); + } + + private ListenableFuture> getPartitionsFuture(TenantId tenantId, ReadTsKvQuery query, EntityId entityId, long minPartition, long maxPartition) { + if (isFixedPartitioning()) { //no need to fetch partitions from DB + return Futures.immediateFuture(FIXED_PARTITION); + } + TbResultSetFuture partitionsFuture = fetchPartitions(tenantId, entityId, query.getKey(), minPartition, maxPartition); + return Futures.transformAsync(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor); } - private AsyncFunction, List> getFetchChunksAsyncFunction(TenantId tenantId, EntityId entityId, String key, Aggregation aggregation, long startTs, long endTs) { + private AsyncFunction, List> getFetchChunksAsyncFunction(TenantId tenantId, EntityId entityId, String key, Aggregation aggregation, long startTs, long endTs) { return partitions -> { try { PreparedStatement proto = getFetchStmt(aggregation, DESC_ORDER); - List futures = new ArrayList<>(partitions.size()); + List futures = new ArrayList<>(partitions.size()); for (Long partition : partitions) { log.trace("Fetching data for partition [{}] for entityType {} and entityId {}", partition, entityId.getEntityType(), entityId.getId()); - BoundStatement stmt = proto.bind(); - stmt.setString(0, entityId.getEntityType().name()); - stmt.setUUID(1, entityId.getId()); - stmt.setString(2, key); - stmt.setLong(3, partition); - stmt.setLong(4, startTs); - stmt.setLong(5, endTs); + BoundStatementBuilder stmtBuilder = new BoundStatementBuilder(proto.bind()); + stmtBuilder.setString(0, entityId.getEntityType().name()); + stmtBuilder.setUuid(1, entityId.getId()); + stmtBuilder.setString(2, key); + stmtBuilder.setLong(3, partition); + stmtBuilder.setLong(4, startTs); + stmtBuilder.setLong(5, endTs); + BoundStatement stmt = stmtBuilder.build(); log.debug(GENERATED_QUERY_FOR_ENTITY_TYPE_AND_ENTITY_ID, stmt, entityId.getEntityType(), entityId.getId()); futures.add(executeAsyncRead(tenantId, stmt)); } @@ -292,46 +407,8 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem }; } - @Override - public ListenableFuture findLatest(TenantId tenantId, EntityId entityId, String key) { - BoundStatement stmt = getFindLatestStmt().bind(); - stmt.setString(0, entityId.getEntityType().name()); - stmt.setUUID(1, entityId.getId()); - stmt.setString(2, key); - log.debug(GENERATED_QUERY_FOR_ENTITY_TYPE_AND_ENTITY_ID, stmt, entityId.getEntityType(), entityId.getId()); - return getFuture(executeAsyncRead(tenantId, stmt), rs -> convertResultToTsKvEntry(key, rs.one())); - } - - @Override - public ListenableFuture> findAllLatest(TenantId tenantId, EntityId entityId) { - BoundStatement stmt = getFindAllLatestStmt().bind(); - stmt.setString(0, entityId.getEntityType().name()); - stmt.setUUID(1, entityId.getId()); - log.debug(GENERATED_QUERY_FOR_ENTITY_TYPE_AND_ENTITY_ID, stmt, entityId.getEntityType(), entityId.getId()); - return getFuture(executeAsyncRead(tenantId, stmt), rs -> convertResultToTsKvEntryList(rs.all())); - } - - @Override - public ListenableFuture save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl) { - List> futures = new ArrayList<>(); - ttl = computeTtl(ttl); - long partition = toPartitionTs(tsKvEntry.getTs()); - DataType type = tsKvEntry.getDataType(); - if (setNullValuesEnabled) { - processSetNullValues(tenantId, entityId, tsKvEntry, ttl, futures, partition, type); - } - BoundStatement stmt = (ttl == 0 ? getSaveStmt(type) : getSaveTtlStmt(type)).bind(); - stmt.setString(0, entityId.getEntityType().name()) - .setUUID(1, entityId.getId()) - .setString(2, tsKvEntry.getKey()) - .setLong(3, partition) - .setLong(4, tsKvEntry.getTs()); - addValue(tsKvEntry, stmt, 5); - if (ttl > 0) { - stmt.setInt(6, (int) ttl); - } - futures.add(getFuture(executeAsyncWrite(tenantId, stmt), rs -> null)); - return Futures.transform(Futures.allAsList(futures), result -> null, MoreExecutors.directExecutor()); + private boolean isFixedPartitioning() { + return tsFormat.getTruncateUnit().equals(ChronoUnit.FOREVER); } private void processSetNullValues(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl, List> futures, long partition, DataType type) { @@ -370,35 +447,17 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem } private ListenableFuture saveNull(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl, long partition, DataType type) { - BoundStatement stmt = (ttl == 0 ? getSaveStmt(type) : getSaveTtlStmt(type)).bind(); - stmt.setString(0, entityId.getEntityType().name()) - .setUUID(1, entityId.getId()) + BoundStatementBuilder stmtBuilder = new BoundStatementBuilder((ttl == 0 ? getSaveStmt(type) : getSaveTtlStmt(type)).bind()); + stmtBuilder.setString(0, entityId.getEntityType().name()) + .setUuid(1, entityId.getId()) .setString(2, tsKvEntry.getKey()) .setLong(3, partition) .setLong(4, tsKvEntry.getTs()); - stmt.setToNull(getColumnName(type)); + stmtBuilder.setToNull(getColumnName(type)); if (ttl > 0) { - stmt.setInt(6, (int) ttl); - } - return getFuture(executeAsyncWrite(tenantId, stmt), rs -> null); - } - - @Override - public ListenableFuture savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key, long ttl) { - if (isFixedPartitioning()) { - return Futures.immediateFuture(null); - } - ttl = computeTtl(ttl); - long partition = toPartitionTs(tsKvEntryTs); - log.debug("Saving partition {} for the entity [{}-{}] and key {}", partition, entityId.getEntityType(), entityId.getId(), key); - BoundStatement stmt = (ttl == 0 ? getPartitionInsertStmt() : getPartitionInsertTtlStmt()).bind(); - stmt = stmt.setString(0, entityId.getEntityType().name()) - .setUUID(1, entityId.getId()) - .setLong(2, partition) - .setString(3, key); - if (ttl > 0) { - stmt.setInt(4, (int) ttl); + stmtBuilder.setInt(6, (int) ttl); } + BoundStatement stmt = stmtBuilder.build(); return getFuture(executeAsyncWrite(tenantId, stmt), rs -> null); } @@ -413,68 +472,24 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem return ttl; } - @Override - public ListenableFuture saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { - BoundStatement stmt = getLatestStmt().bind() - .setString(0, entityId.getEntityType().name()) - .setUUID(1, entityId.getId()) - .setString(2, tsKvEntry.getKey()) - .setLong(3, tsKvEntry.getTs()) - .set(4, tsKvEntry.getBooleanValue().orElse(null), Boolean.class) - .set(5, tsKvEntry.getStrValue().orElse(null), String.class) - .set(6, tsKvEntry.getLongValue().orElse(null), Long.class) - .set(7, tsKvEntry.getDoubleValue().orElse(null), Double.class); - Optional jsonV = tsKvEntry.getJsonValue(); - if (jsonV.isPresent()) { - stmt.setString(8, tsKvEntry.getJsonValue().get()); - } else { - stmt.setToNull(8); - } - - return getFuture(executeAsyncWrite(tenantId, stmt), rs -> null); - } - - @Override - public ListenableFuture remove(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { - long minPartition = toPartitionTs(query.getStartTs()); - long maxPartition = toPartitionTs(query.getEndTs()); - - ResultSetFuture partitionsFuture = fetchPartitions(tenantId, entityId, query.getKey(), minPartition, maxPartition); - - final SimpleListenableFuture resultFuture = new SimpleListenableFuture<>(); - final ListenableFuture> partitionsListFuture = Futures.transform(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor); - - Futures.addCallback(partitionsListFuture, new FutureCallback>() { - @Override - public void onSuccess(@Nullable List partitions) { - QueryCursor cursor = new QueryCursor(entityId.getEntityType().name(), entityId.getId(), query, partitions); - deleteAsync(tenantId, cursor, resultFuture); - } - - @Override - public void onFailure(Throwable t) { - log.error("[{}][{}] Failed to fetch partitions for interval {}-{}", entityId.getEntityType().name(), entityId.getId(), minPartition, maxPartition, t); - } - }, readResultsProcessingExecutor); - return resultFuture; - } - private void deleteAsync(TenantId tenantId, final QueryCursor cursor, final SimpleListenableFuture resultFuture) { if (!cursor.hasNextPartition()) { resultFuture.set(null); } else { PreparedStatement proto = getDeleteStmt(); - BoundStatement stmt = proto.bind(); - stmt.setString(0, cursor.getEntityType()); - stmt.setUUID(1, cursor.getEntityId()); - stmt.setString(2, cursor.getKey()); - stmt.setLong(3, cursor.getNextPartition()); - stmt.setLong(4, cursor.getStartTs()); - stmt.setLong(5, cursor.getEndTs()); - - Futures.addCallback(executeAsyncWrite(tenantId, stmt), new FutureCallback() { + BoundStatementBuilder stmtBuilder = new BoundStatementBuilder(proto.bind()); + stmtBuilder.setString(0, cursor.getEntityType()); + stmtBuilder.setUuid(1, cursor.getEntityId()); + stmtBuilder.setString(2, cursor.getKey()); + stmtBuilder.setLong(3, cursor.getNextPartition()); + stmtBuilder.setLong(4, cursor.getStartTs()); + stmtBuilder.setLong(5, cursor.getEndTs()); + + BoundStatement stmt = stmtBuilder.build(); + + Futures.addCallback(executeAsyncWrite(tenantId, stmt), new FutureCallback() { @Override - public void onSuccess(@Nullable ResultSet result) { + public void onSuccess(@Nullable AsyncResultSet result) { deleteAsync(tenantId, cursor, resultFuture); } @@ -499,133 +514,22 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem return deleteStmt; } - @Override - public ListenableFuture removeLatest(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { - ListenableFuture latestEntryFuture = findLatest(tenantId, entityId, query.getKey()); - - ListenableFuture booleanFuture = Futures.transform(latestEntryFuture, latestEntry -> { - long ts = latestEntry.getTs(); - if (ts > query.getStartTs() && ts <= query.getEndTs()) { - return true; - } else { - log.trace("Won't be deleted latest value for [{}], key - {}", entityId, query.getKey()); - } - return false; - }, readResultsProcessingExecutor); - - ListenableFuture removedLatestFuture = Futures.transformAsync(booleanFuture, isRemove -> { - if (isRemove) { - return deleteLatest(tenantId, entityId, query.getKey()); - } - return Futures.immediateFuture(null); - }, readResultsProcessingExecutor); - - final SimpleListenableFuture resultFuture = new SimpleListenableFuture<>(); - Futures.addCallback(removedLatestFuture, new FutureCallback() { - @Override - public void onSuccess(@Nullable Void result) { - if (query.getRewriteLatestIfDeleted()) { - ListenableFuture savedLatestFuture = Futures.transformAsync(booleanFuture, isRemove -> { - if (isRemove) { - return getNewLatestEntryFuture(tenantId, entityId, query); - } - return Futures.immediateFuture(null); - }, readResultsProcessingExecutor); - - try { - resultFuture.set(savedLatestFuture.get()); - } catch (InterruptedException | ExecutionException e) { - log.warn("Could not get latest saved value for [{}], {}", entityId, query.getKey(), e); - } - } else { - resultFuture.set(null); - } - } - - @Override - public void onFailure(Throwable t) { - log.warn("[{}] Failed to process remove of the latest value", entityId, t); - } - }, MoreExecutors.directExecutor()); - return resultFuture; - } - - private ListenableFuture getNewLatestEntryFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { - long startTs = 0; - long endTs = query.getStartTs() - 1; - ReadTsKvQuery findNewLatestQuery = new BaseReadTsKvQuery(query.getKey(), startTs, endTs, endTs - startTs, 1, - Aggregation.NONE, DESC_ORDER); - ListenableFuture> future = findAllAsync(tenantId, entityId, findNewLatestQuery); - - return Futures.transformAsync(future, entryList -> { - if (entryList.size() == 1) { - return saveLatest(tenantId, entityId, entryList.get(0)); - } else { - log.trace("Could not find new latest value for [{}], key - {}", entityId, query.getKey()); - } - return Futures.immediateFuture(null); - }, readResultsProcessingExecutor); - } - - private ListenableFuture deleteLatest(TenantId tenantId, EntityId entityId, String key) { - Statement delete = QueryBuilder.delete().all().from(ModelConstants.TS_KV_LATEST_CF) - .where(eq(ModelConstants.ENTITY_TYPE_COLUMN, entityId.getEntityType())) - .and(eq(ModelConstants.ENTITY_ID_COLUMN, entityId.getId())) - .and(eq(ModelConstants.KEY_COLUMN, key)); - log.debug("Remove request: {}", delete.toString()); - return getFuture(executeAsyncWrite(tenantId, delete), rs -> null); - } - - @Override - public ListenableFuture removePartition(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { - long minPartition = toPartitionTs(query.getStartTs()); - long maxPartition = toPartitionTs(query.getEndTs()); - if (minPartition == maxPartition) { - return Futures.immediateFuture(null); - } else { - ResultSetFuture partitionsFuture = fetchPartitions(tenantId, entityId, query.getKey(), minPartition, maxPartition); - - final SimpleListenableFuture resultFuture = new SimpleListenableFuture<>(); - final ListenableFuture> partitionsListFuture = Futures.transform(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor); - - Futures.addCallback(partitionsListFuture, new FutureCallback>() { - @Override - public void onSuccess(@Nullable List partitions) { - int index = 0; - if (minPartition != query.getStartTs()) { - index = 1; - } - List partitionsToDelete = new ArrayList<>(); - for (int i = index; i < partitions.size() - 1; i++) { - partitionsToDelete.add(partitions.get(i)); - } - QueryCursor cursor = new QueryCursor(entityId.getEntityType().name(), entityId.getId(), query, partitionsToDelete); - deletePartitionAsync(tenantId, cursor, resultFuture); - } - - @Override - public void onFailure(Throwable t) { - log.error("[{}][{}] Failed to fetch partitions for interval {}-{}", entityId.getEntityType().name(), entityId.getId(), minPartition, maxPartition, t); - } - }, readResultsProcessingExecutor); - return resultFuture; - } - } - private void deletePartitionAsync(TenantId tenantId, final QueryCursor cursor, final SimpleListenableFuture resultFuture) { if (!cursor.hasNextPartition()) { resultFuture.set(null); } else { PreparedStatement proto = getDeletePartitionStmt(); - BoundStatement stmt = proto.bind(); - stmt.setString(0, cursor.getEntityType()); - stmt.setUUID(1, cursor.getEntityId()); - stmt.setLong(2, cursor.getNextPartition()); - stmt.setString(3, cursor.getKey()); + BoundStatementBuilder stmtBuilder = new BoundStatementBuilder(proto.bind()); + stmtBuilder.setString(0, cursor.getEntityType()); + stmtBuilder.setUuid(1, cursor.getEntityId()); + stmtBuilder.setLong(2, cursor.getNextPartition()); + stmtBuilder.setString(3, cursor.getKey()); + + BoundStatement stmt = stmtBuilder.build(); - Futures.addCallback(executeAsyncWrite(tenantId, stmt), new FutureCallback() { + Futures.addCallback(executeAsyncWrite(tenantId, stmt), new FutureCallback() { @Override - public void onSuccess(@Nullable ResultSet result) { + public void onSuccess(@Nullable AsyncResultSet result) { deletePartitionAsync(tenantId, cursor, resultFuture); } @@ -648,72 +552,6 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem return deletePartitionStmt; } - private List convertResultToTsKvEntryList(List rows) { - List entries = new ArrayList<>(rows.size()); - if (!rows.isEmpty()) { - rows.forEach(row -> entries.add(convertResultToTsKvEntry(row))); - } - return entries; - } - - private TsKvEntry convertResultToTsKvEntry(String key, Row row) { - if (row != null) { - long ts = row.getLong(ModelConstants.TS_COLUMN); - return new BasicTsKvEntry(ts, toKvEntry(row, key)); - } else { - return new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry(key, null)); - } - } - - private TsKvEntry convertResultToTsKvEntry(Row row) { - String key = row.getString(ModelConstants.KEY_COLUMN); - long ts = row.getLong(ModelConstants.TS_COLUMN); - return new BasicTsKvEntry(ts, toKvEntry(row, key)); - } - - public static KvEntry toKvEntry(Row row, String key) { - KvEntry kvEntry = null; - String strV = row.get(ModelConstants.STRING_VALUE_COLUMN, String.class); - if (strV != null) { - kvEntry = new StringDataEntry(key, strV); - } else { - Long longV = row.get(ModelConstants.LONG_VALUE_COLUMN, Long.class); - if (longV != null) { - kvEntry = new LongDataEntry(key, longV); - } else { - Double doubleV = row.get(ModelConstants.DOUBLE_VALUE_COLUMN, Double.class); - if (doubleV != null) { - kvEntry = new DoubleDataEntry(key, doubleV); - } else { - Boolean boolV = row.get(ModelConstants.BOOLEAN_VALUE_COLUMN, Boolean.class); - if (boolV != null) { - kvEntry = new BooleanDataEntry(key, boolV); - } else { - String jsonV = row.get(ModelConstants.JSON_VALUE_COLUMN, String.class); - if (StringUtils.isNoneEmpty(jsonV)) { - kvEntry = new JsonDataEntry(key, jsonV); - } else { - log.warn("All values in key-value row are nullable "); - } - } - } - } - } - return kvEntry; - } - - /** - * Select existing partitions from the table - * {@link ModelConstants#TS_KV_PARTITIONS_CF} for the given entity - */ - private ResultSetFuture fetchPartitions(TenantId tenantId, EntityId entityId, String key, long minPartition, long maxPartition) { - Select.Where select = QueryBuilder.select(ModelConstants.PARTITION_COLUMN).from(ModelConstants.TS_KV_PARTITIONS_CF).where(eq(ModelConstants.ENTITY_TYPE_COLUMN, entityId.getEntityType().name())) - .and(eq(ModelConstants.ENTITY_ID_COLUMN, entityId.getId())).and(eq(ModelConstants.KEY_COLUMN, key)); - select.and(QueryBuilder.gte(ModelConstants.PARTITION_COLUMN, minPartition)); - select.and(QueryBuilder.lte(ModelConstants.PARTITION_COLUMN, maxPartition)); - return executeAsyncRead(tenantId, select); - } - private PreparedStatement getSaveStmt(DataType dataType) { if (saveStmts == null) { saveStmts = new PreparedStatement[DataType.values().length]; @@ -748,63 +586,6 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem return saveTtlStmts[dataType.ordinal()]; } - private PreparedStatement getFetchStmt(Aggregation aggType, String orderBy) { - switch (orderBy) { - case ASC_ORDER: - if (fetchStmtsAsc == null) { - fetchStmtsAsc = initFetchStmt(orderBy); - } - return fetchStmtsAsc[aggType.ordinal()]; - case DESC_ORDER: - if (fetchStmtsDesc == null) { - fetchStmtsDesc = initFetchStmt(orderBy); - } - return fetchStmtsDesc[aggType.ordinal()]; - default: - throw new RuntimeException("Not supported" + orderBy + "order!"); - } - } - - private PreparedStatement[] initFetchStmt(String orderBy) { - PreparedStatement[] fetchStmts = new PreparedStatement[Aggregation.values().length]; - for (Aggregation type : Aggregation.values()) { - if (type == Aggregation.SUM && fetchStmts[Aggregation.AVG.ordinal()] != null) { - fetchStmts[type.ordinal()] = fetchStmts[Aggregation.AVG.ordinal()]; - } else if (type == Aggregation.AVG && fetchStmts[Aggregation.SUM.ordinal()] != null) { - fetchStmts[type.ordinal()] = fetchStmts[Aggregation.SUM.ordinal()]; - } else { - fetchStmts[type.ordinal()] = prepare(SELECT_PREFIX + - String.join(", ", ModelConstants.getFetchColumnNames(type)) + " FROM " + ModelConstants.TS_KV_CF - + " WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM - + "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM - + "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM - + "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM - + "AND " + ModelConstants.TS_COLUMN + " > ? " - + "AND " + ModelConstants.TS_COLUMN + " <= ?" - + (type == Aggregation.NONE ? " ORDER BY " + ModelConstants.TS_COLUMN + " " + orderBy + " LIMIT ?" : "")); - } - } - return fetchStmts; - } - - private PreparedStatement getLatestStmt() { - if (latestInsertStmt == null) { - latestInsertStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_LATEST_CF + - "(" + ModelConstants.ENTITY_TYPE_COLUMN + - "," + ModelConstants.ENTITY_ID_COLUMN + - "," + ModelConstants.KEY_COLUMN + - "," + ModelConstants.TS_COLUMN + - "," + ModelConstants.BOOLEAN_VALUE_COLUMN + - "," + ModelConstants.STRING_VALUE_COLUMN + - "," + ModelConstants.LONG_VALUE_COLUMN + - "," + ModelConstants.DOUBLE_VALUE_COLUMN + - "," + ModelConstants.JSON_VALUE_COLUMN + ")" + - " VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)"); - } - return latestInsertStmt; - } - - private PreparedStatement getPartitionInsertStmt() { if (partitionInsertStmt == null) { partitionInsertStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF + @@ -829,42 +610,6 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem return partitionInsertTtlStmt; } - - private PreparedStatement getFindLatestStmt() { - if (findLatestStmt == null) { - findLatestStmt = prepare(SELECT_PREFIX + - ModelConstants.KEY_COLUMN + "," + - ModelConstants.TS_COLUMN + "," + - ModelConstants.STRING_VALUE_COLUMN + "," + - ModelConstants.BOOLEAN_VALUE_COLUMN + "," + - ModelConstants.LONG_VALUE_COLUMN + "," + - ModelConstants.DOUBLE_VALUE_COLUMN + "," + - ModelConstants.JSON_VALUE_COLUMN + " " + - "FROM " + ModelConstants.TS_KV_LATEST_CF + " " + - "WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM + - "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM + - "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM); - } - return findLatestStmt; - } - - private PreparedStatement getFindAllLatestStmt() { - if (findAllLatestStmt == null) { - findAllLatestStmt = prepare(SELECT_PREFIX + - ModelConstants.KEY_COLUMN + "," + - ModelConstants.TS_COLUMN + "," + - ModelConstants.STRING_VALUE_COLUMN + "," + - ModelConstants.BOOLEAN_VALUE_COLUMN + "," + - ModelConstants.LONG_VALUE_COLUMN + "," + - ModelConstants.DOUBLE_VALUE_COLUMN + "," + - ModelConstants.JSON_VALUE_COLUMN + " " + - "FROM " + ModelConstants.TS_KV_LATEST_CF + " " + - "WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM + - "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM); - } - return findAllLatestStmt; - } - private static String getColumnName(DataType type) { switch (type) { case BOOLEAN: @@ -882,11 +627,11 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem } } - private static void addValue(KvEntry kvEntry, BoundStatement stmt, int column) { + private static void addValue(KvEntry kvEntry, BoundStatementBuilder stmt, int column) { switch (kvEntry.getDataType()) { case BOOLEAN: Optional booleanValue = kvEntry.getBooleanValue(); - booleanValue.ifPresent(b -> stmt.setBool(column, b)); + booleanValue.ifPresent(b -> stmt.setBoolean(column, b)); break; case STRING: Optional stringValue = kvEntry.getStrValue(); @@ -907,4 +652,57 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem } } + /** + * // * Select existing partitions from the table + * // * {@link ModelConstants#TS_KV_PARTITIONS_CF} for the given entity + * // + */ + private TbResultSetFuture fetchPartitions(TenantId tenantId, EntityId entityId, String key, long minPartition, long maxPartition) { + Select select = QueryBuilder.selectFrom(ModelConstants.TS_KV_PARTITIONS_CF).column(ModelConstants.PARTITION_COLUMN) + .whereColumn(ModelConstants.ENTITY_TYPE_COLUMN).isEqualTo(literal(entityId.getEntityType().name())) + .whereColumn(ModelConstants.ENTITY_ID_COLUMN).isEqualTo(literal(entityId.getId())) + .whereColumn(ModelConstants.KEY_COLUMN).isEqualTo(literal(key)) + .whereColumn(ModelConstants.PARTITION_COLUMN).isGreaterThanOrEqualTo(literal(minPartition)) + .whereColumn(ModelConstants.PARTITION_COLUMN).isLessThanOrEqualTo(literal(maxPartition)); + return executeAsyncRead(tenantId, select.build()); + } + + private PreparedStatement getFetchStmt(Aggregation aggType, String orderBy) { + switch (orderBy) { + case ASC_ORDER: + if (fetchStmtsAsc == null) { + fetchStmtsAsc = initFetchStmt(orderBy); + } + return fetchStmtsAsc[aggType.ordinal()]; + case DESC_ORDER: + if (fetchStmtsDesc == null) { + fetchStmtsDesc = initFetchStmt(orderBy); + } + return fetchStmtsDesc[aggType.ordinal()]; + default: + throw new RuntimeException("Not supported" + orderBy + "order!"); + } + } + + private PreparedStatement[] initFetchStmt(String orderBy) { + PreparedStatement[] fetchStmts = new PreparedStatement[Aggregation.values().length]; + for (Aggregation type : Aggregation.values()) { + if (type == Aggregation.SUM && fetchStmts[Aggregation.AVG.ordinal()] != null) { + fetchStmts[type.ordinal()] = fetchStmts[Aggregation.AVG.ordinal()]; + } else if (type == Aggregation.AVG && fetchStmts[Aggregation.SUM.ordinal()] != null) { + fetchStmts[type.ordinal()] = fetchStmts[Aggregation.SUM.ordinal()]; + } else { + fetchStmts[type.ordinal()] = prepare(SELECT_PREFIX + + String.join(", ", ModelConstants.getFetchColumnNames(type)) + " FROM " + ModelConstants.TS_KV_CF + + " WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM + + "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM + + "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM + + "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM + + "AND " + ModelConstants.TS_COLUMN + " > ? " + + "AND " + ModelConstants.TS_COLUMN + " <= ?" + + (type == Aggregation.NONE ? " ORDER BY " + ModelConstants.TS_COLUMN + " " + orderBy + " LIMIT ?" : "")); + } + } + return fetchStmts; + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java new file mode 100644 index 0000000000..27842dcf18 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java @@ -0,0 +1,240 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.timeseries; + +import com.datastax.oss.driver.api.core.cql.BoundStatement; +import com.datastax.oss.driver.api.core.cql.BoundStatementBuilder; +import com.datastax.oss.driver.api.core.cql.PreparedStatement; +import com.datastax.oss.driver.api.core.cql.Row; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.datastax.oss.driver.api.querybuilder.QueryBuilder; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.Aggregation; +import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; +import org.thingsboard.server.common.data.kv.ReadTsKvQuery; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.nosql.TbResultSet; +import org.thingsboard.server.dao.sqlts.AggregationTimeseriesDao; +import org.thingsboard.server.dao.util.NoSqlTsLatestDao; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; + +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal; + +@Component +@Slf4j +@NoSqlTsLatestDao +public class CassandraBaseTimeseriesLatestDao extends AbstractCassandraBaseTimeseriesDao implements TimeseriesLatestDao { + + @Autowired + protected AggregationTimeseriesDao aggregationTimeseriesDao; + + private PreparedStatement latestInsertStmt; + private PreparedStatement findLatestStmt; + private PreparedStatement findAllLatestStmt; + + @Override + public ListenableFuture findLatest(TenantId tenantId, EntityId entityId, String key) { + BoundStatementBuilder stmtBuilder = new BoundStatementBuilder(getFindLatestStmt().bind()); + stmtBuilder.setString(0, entityId.getEntityType().name()); + stmtBuilder.setUuid(1, entityId.getId()); + stmtBuilder.setString(2, key); + BoundStatement stmt = stmtBuilder.build(); + log.debug(GENERATED_QUERY_FOR_ENTITY_TYPE_AND_ENTITY_ID, stmt, entityId.getEntityType(), entityId.getId()); + return getFuture(executeAsyncRead(tenantId, stmt), rs -> convertResultToTsKvEntry(key, rs.one())); + } + + @Override + public ListenableFuture> findAllLatest(TenantId tenantId, EntityId entityId) { + BoundStatementBuilder stmtBuilder = new BoundStatementBuilder(getFindAllLatestStmt().bind()); + stmtBuilder.setString(0, entityId.getEntityType().name()); + stmtBuilder.setUuid(1, entityId.getId()); + BoundStatement stmt = stmtBuilder.build(); + log.debug(GENERATED_QUERY_FOR_ENTITY_TYPE_AND_ENTITY_ID, stmt, entityId.getEntityType(), entityId.getId()); + return getFutureAsync(executeAsyncRead(tenantId, stmt), rs -> convertAsyncResultSetToTsKvEntryList(rs)); + } + + @Override + public ListenableFuture saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { + BoundStatementBuilder stmtBuilder = new BoundStatementBuilder(getLatestStmt().bind()); + stmtBuilder.setString(0, entityId.getEntityType().name()) + .setUuid(1, entityId.getId()) + .setString(2, tsKvEntry.getKey()) + .setLong(3, tsKvEntry.getTs()) + .set(4, tsKvEntry.getBooleanValue().orElse(null), Boolean.class) + .set(5, tsKvEntry.getStrValue().orElse(null), String.class) + .set(6, tsKvEntry.getLongValue().orElse(null), Long.class) + .set(7, tsKvEntry.getDoubleValue().orElse(null), Double.class); + Optional jsonV = tsKvEntry.getJsonValue(); + if (jsonV.isPresent()) { + stmtBuilder.setString(8, tsKvEntry.getJsonValue().get()); + } else { + stmtBuilder.setToNull(8); + } + BoundStatement stmt = stmtBuilder.build(); + + return getFuture(executeAsyncWrite(tenantId, stmt), rs -> null); + } + + @Override + public ListenableFuture removeLatest(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { + ListenableFuture latestEntryFuture = findLatest(tenantId, entityId, query.getKey()); + + ListenableFuture booleanFuture = Futures.transform(latestEntryFuture, latestEntry -> { + long ts = latestEntry.getTs(); + if (ts > query.getStartTs() && ts <= query.getEndTs()) { + return true; + } else { + log.trace("Won't be deleted latest value for [{}], key - {}", entityId, query.getKey()); + } + return false; + }, readResultsProcessingExecutor); + + ListenableFuture removedLatestFuture = Futures.transformAsync(booleanFuture, isRemove -> { + if (isRemove) { + return deleteLatest(tenantId, entityId, query.getKey()); + } + return Futures.immediateFuture(null); + }, readResultsProcessingExecutor); + + final SimpleListenableFuture resultFuture = new SimpleListenableFuture<>(); + Futures.addCallback(removedLatestFuture, new FutureCallback() { + @Override + public void onSuccess(@Nullable Void result) { + if (query.getRewriteLatestIfDeleted()) { + ListenableFuture savedLatestFuture = Futures.transformAsync(booleanFuture, isRemove -> { + if (isRemove) { + return getNewLatestEntryFuture(tenantId, entityId, query); + } + return Futures.immediateFuture(null); + }, readResultsProcessingExecutor); + + try { + resultFuture.set(savedLatestFuture.get()); + } catch (InterruptedException | ExecutionException e) { + log.warn("Could not get latest saved value for [{}], {}", entityId, query.getKey(), e); + } + } else { + resultFuture.set(null); + } + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}] Failed to process remove of the latest value", entityId, t); + } + }, MoreExecutors.directExecutor()); + return resultFuture; + } + + private ListenableFuture getNewLatestEntryFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { + long startTs = 0; + long endTs = query.getStartTs() - 1; + ReadTsKvQuery findNewLatestQuery = new BaseReadTsKvQuery(query.getKey(), startTs, endTs, endTs - startTs, 1, + Aggregation.NONE, DESC_ORDER); + ListenableFuture> future = aggregationTimeseriesDao.findAllAsync(tenantId, entityId, findNewLatestQuery); + + return Futures.transformAsync(future, entryList -> { + if (entryList.size() == 1) { + return saveLatest(tenantId, entityId, entryList.get(0)); + } else { + log.trace("Could not find new latest value for [{}], key - {}", entityId, query.getKey()); + } + return Futures.immediateFuture(null); + }, readResultsProcessingExecutor); + } + + private ListenableFuture deleteLatest(TenantId tenantId, EntityId entityId, String key) { + Statement delete = QueryBuilder.deleteFrom(ModelConstants.TS_KV_LATEST_CF) + .whereColumn(ModelConstants.ENTITY_TYPE_COLUMN).isEqualTo(literal(entityId.getEntityType().name())) + .whereColumn(ModelConstants.ENTITY_ID_COLUMN).isEqualTo(literal(entityId.getId())) + .whereColumn(ModelConstants.KEY_COLUMN).isEqualTo(literal(key)).build(); + log.debug("Remove request: {}", delete.toString()); + return getFuture(executeAsyncWrite(tenantId, delete), rs -> null); + } + + private ListenableFuture> convertAsyncResultSetToTsKvEntryList(TbResultSet rs) { + return Futures.transform(rs.allRows(readResultsProcessingExecutor), + rows -> this.convertResultToTsKvEntryList(rows), readResultsProcessingExecutor); + } + + private PreparedStatement getLatestStmt() { + if (latestInsertStmt == null) { + latestInsertStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_LATEST_CF + + "(" + ModelConstants.ENTITY_TYPE_COLUMN + + "," + ModelConstants.ENTITY_ID_COLUMN + + "," + ModelConstants.KEY_COLUMN + + "," + ModelConstants.TS_COLUMN + + "," + ModelConstants.BOOLEAN_VALUE_COLUMN + + "," + ModelConstants.STRING_VALUE_COLUMN + + "," + ModelConstants.LONG_VALUE_COLUMN + + "," + ModelConstants.DOUBLE_VALUE_COLUMN + + "," + ModelConstants.JSON_VALUE_COLUMN + ")" + + " VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)"); + } + return latestInsertStmt; + } + + private PreparedStatement getFindLatestStmt() { + if (findLatestStmt == null) { + findLatestStmt = prepare(SELECT_PREFIX + + ModelConstants.KEY_COLUMN + "," + + ModelConstants.TS_COLUMN + "," + + ModelConstants.STRING_VALUE_COLUMN + "," + + ModelConstants.BOOLEAN_VALUE_COLUMN + "," + + ModelConstants.LONG_VALUE_COLUMN + "," + + ModelConstants.DOUBLE_VALUE_COLUMN + "," + + ModelConstants.JSON_VALUE_COLUMN + " " + + "FROM " + ModelConstants.TS_KV_LATEST_CF + " " + + "WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM + + "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM + + "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM); + } + return findLatestStmt; + } + + private PreparedStatement getFindAllLatestStmt() { + if (findAllLatestStmt == null) { + findAllLatestStmt = prepare(SELECT_PREFIX + + ModelConstants.KEY_COLUMN + "," + + ModelConstants.TS_COLUMN + "," + + ModelConstants.STRING_VALUE_COLUMN + "," + + ModelConstants.BOOLEAN_VALUE_COLUMN + "," + + ModelConstants.LONG_VALUE_COLUMN + "," + + ModelConstants.DOUBLE_VALUE_COLUMN + "," + + ModelConstants.JSON_VALUE_COLUMN + " " + + "FROM " + ModelConstants.TS_KV_LATEST_CF + " " + + "WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM + + "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM); + } + return findAllLatestStmt; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java index 39d965630e..2dd52580b6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java @@ -31,19 +31,11 @@ public interface TimeseriesDao { ListenableFuture> findAllAsync(TenantId tenantId, EntityId entityId, List queries); - ListenableFuture findLatest(TenantId tenantId, EntityId entityId, String key); + ListenableFuture save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl); - ListenableFuture> findAllLatest(TenantId tenantId, EntityId entityId); - - ListenableFuture save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl); - - ListenableFuture savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key, long ttl); - - ListenableFuture saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry); + ListenableFuture savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key, long ttl); ListenableFuture remove(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query); - ListenableFuture removeLatest(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query); - ListenableFuture removePartition(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java new file mode 100644 index 0000000000..7392e0e3d4 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.timeseries; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; +import org.thingsboard.server.common.data.kv.TsKvEntry; + +import java.util.List; + +public interface TimeseriesLatestDao { + + ListenableFuture findLatest(TenantId tenantId, EntityId entityId, String key); + + ListenableFuture> findAllLatest(TenantId tenantId, EntityId entityId); + + ListenableFuture saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry); + + ListenableFuture removeLatest(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsKvQueryCursor.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsKvQueryCursor.java index d332cddc26..ccddddb5e1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsKvQueryCursor.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsKvQueryCursor.java @@ -40,7 +40,7 @@ public class TsKvQueryCursor extends QueryCursor { public TsKvQueryCursor(String entityType, UUID entityId, ReadTsKvQuery baseQuery, List partitions) { super(entityType, entityId, baseQuery, partitions); - this.orderBy = baseQuery.getOrderBy(); + this.orderBy = baseQuery.getOrder(); this.partitionIndex = isDesc() ? partitions.size() - 1 : 0; this.data = new ArrayList<>(); this.currentLimit = baseQuery.getLimit(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateDao.java b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateDao.java new file mode 100644 index 0000000000..1521da525e --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateDao.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.usagerecord; + +import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.Dao; + +import java.util.UUID; + +public interface ApiUsageStateDao extends Dao { + + /** + * Save or update usage record object + * + * @param apiUsageState the usage record + * @return saved usage record entity + */ + ApiUsageState save(TenantId tenantId, ApiUsageState apiUsageState); + + /** + * Find usage record by tenantId. + * + * @param tenantId the tenantId + * @return the corresponding usage record + */ + ApiUsageState findTenantApiUsageState(UUID tenantId); + + /** + * Delete usage record by tenantId. + * + * @param tenantId the tenantId + */ + void deleteApiUsageStateByTenantId(TenantId tenantId); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java new file mode 100644 index 0000000000..ed02bf7726 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java @@ -0,0 +1,139 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.usagerecord; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.ApiUsageRecordKey; +import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.ApiUsageStateValue; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.ApiUsageStateId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileConfiguration; +import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.tenant.TenantDao; +import org.thingsboard.server.dao.tenant.TenantProfileDao; +import org.thingsboard.server.dao.timeseries.TimeseriesService; + +import java.util.ArrayList; +import java.util.List; + +import static org.thingsboard.server.dao.service.Validator.validateId; + +@Service +@Slf4j +public class ApiUsageStateServiceImpl extends AbstractEntityService implements ApiUsageStateService { + public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; + + private final ApiUsageStateDao apiUsageStateDao; + private final TenantProfileDao tenantProfileDao; + private final TenantDao tenantDao; + private final TimeseriesService tsService; + + public ApiUsageStateServiceImpl(TenantDao tenantDao, ApiUsageStateDao apiUsageStateDao, TenantProfileDao tenantProfileDao, TimeseriesService tsService) { + this.tenantDao = tenantDao; + this.apiUsageStateDao = apiUsageStateDao; + this.tenantProfileDao = tenantProfileDao; + this.tsService = tsService; + } + + @Override + public void deleteApiUsageStateByTenantId(TenantId tenantId) { + log.trace("Executing deleteUsageRecordsByTenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + apiUsageStateDao.deleteApiUsageStateByTenantId(tenantId); + } + + @Override + public ApiUsageState createDefaultApiUsageState(TenantId tenantId) { + log.trace("Executing createDefaultUsageRecord [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + ApiUsageState apiUsageState = new ApiUsageState(); + apiUsageState.setTenantId(tenantId); + apiUsageState.setEntityId(tenantId); + apiUsageState.setTransportState(ApiUsageStateValue.ENABLED); + apiUsageState.setReExecState(ApiUsageStateValue.ENABLED); + apiUsageState.setJsExecState(ApiUsageStateValue.ENABLED); + apiUsageState.setDbStorageState(ApiUsageStateValue.ENABLED); + apiUsageStateValidator.validate(apiUsageState, ApiUsageState::getTenantId); + + ApiUsageState saved = apiUsageStateDao.save(apiUsageState.getTenantId(), apiUsageState); + + Tenant tenant = tenantDao.findById(tenantId, tenantId.getId()); + TenantProfile tenantProfile = tenantProfileDao.findById(tenantId, tenant.getTenantProfileId().getId()); + TenantProfileConfiguration configuration = tenantProfile.getProfileData().getConfiguration(); + List profileThresholds = new ArrayList<>(); + for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { + profileThresholds.add(new BasicTsKvEntry(saved.getCreatedTime(), new LongDataEntry(key.getApiLimitKey(), configuration.getProfileThreshold(key)))); + } + tsService.save(tenantId, saved.getId(), profileThresholds, 0L); + return saved; + } + + @Override + public ApiUsageState update(ApiUsageState apiUsageState) { + log.trace("Executing save [{}]", apiUsageState.getTenantId()); + validateId(apiUsageState.getTenantId(), INCORRECT_TENANT_ID + apiUsageState.getTenantId()); + validateId(apiUsageState.getId(), "Can't save new usage state. Only update is allowed!"); + return apiUsageStateDao.save(apiUsageState.getTenantId(), apiUsageState); + } + + @Override + public ApiUsageState findTenantApiUsageState(TenantId tenantId) { + log.trace("Executing findTenantUsageRecord, tenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + return apiUsageStateDao.findTenantApiUsageState(tenantId.getId()); + } + + @Override + public ApiUsageState findApiUsageStateById(TenantId tenantId, ApiUsageStateId id) { + log.trace("Executing findApiUsageStateById, tenantId [{}], apiUsageStateId [{}]", tenantId, id); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(id, "Incorrect apiUsageStateId " + id); + return apiUsageStateDao.findById(tenantId, id.getId()); + } + + private DataValidator apiUsageStateValidator = + new DataValidator() { + @Override + protected void validateDataImpl(TenantId requestTenantId, ApiUsageState apiUsageState) { + if (apiUsageState.getTenantId() == null) { + throw new DataValidationException("ApiUsageState should be assigned to tenant!"); + } else { + Tenant tenant = tenantDao.findById(requestTenantId, apiUsageState.getTenantId().getId()); + if (tenant == null) { + throw new DataValidationException("Asset is referencing to non-existent tenant!"); + } + } + if (apiUsageState.getEntityId() == null) { + throw new DataValidationException("UsageRecord should be assigned to entity!"); + } else if (!EntityType.TENANT.equals(apiUsageState.getEntityId().getEntityType())) { + throw new DataValidationException("Only Tenant Usage Records are supported!"); + } else if (!apiUsageState.getTenantId().getId().equals(apiUsageState.getEntityId().getId())) { + throw new DataValidationException("Can't assign one Usage Record to multiple tenants!"); + } + } + }; + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/CassandraUserCredentialsDao.java b/dao/src/main/java/org/thingsboard/server/dao/user/CassandraUserCredentialsDao.java deleted file mode 100644 index 1e458fba9d..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/user/CassandraUserCredentialsDao.java +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.user; - -import com.datastax.driver.core.querybuilder.Select.Where; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.security.UserCredentials; -import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.nosql.UserCredentialsEntity; -import org.thingsboard.server.dao.nosql.CassandraAbstractModelDao; -import org.thingsboard.server.dao.util.NoSqlDao; - -import java.util.UUID; - -import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; -import static com.datastax.driver.core.querybuilder.QueryBuilder.select; - -@Component -@Slf4j -@NoSqlDao -public class CassandraUserCredentialsDao extends CassandraAbstractModelDao implements UserCredentialsDao { - - public static final String EXECUTE_QUERY = "Execute query {}"; - - @Override - protected Class getColumnFamilyClass() { - return UserCredentialsEntity.class; - } - - @Override - protected String getColumnFamilyName() { - return ModelConstants.USER_CREDENTIALS_COLUMN_FAMILY_NAME; - } - - @Override - public UserCredentials findByUserId(TenantId tenantId, UUID userId) { - log.debug("Try to find user credentials by userId [{}] ", userId); - Where query = select().from(ModelConstants.USER_CREDENTIALS_BY_USER_COLUMN_FAMILY_NAME).where(eq(ModelConstants.USER_CREDENTIALS_USER_ID_PROPERTY, userId)); - log.trace(EXECUTE_QUERY, query); - UserCredentialsEntity userCredentialsEntity = findOneByStatement(tenantId, query); - log.trace("Found user credentials [{}] by userId [{}]", userCredentialsEntity, userId); - return DaoUtil.getData(userCredentialsEntity); - } - - @Override - public UserCredentials findByActivateToken(TenantId tenantId, String activateToken) { - log.debug("Try to find user credentials by activateToken [{}] ", activateToken); - Where query = select().from(ModelConstants.USER_CREDENTIALS_BY_ACTIVATE_TOKEN_COLUMN_FAMILY_NAME) - .where(eq(ModelConstants.USER_CREDENTIALS_ACTIVATE_TOKEN_PROPERTY, activateToken)); - log.trace(EXECUTE_QUERY, query); - UserCredentialsEntity userCredentialsEntity = findOneByStatement(tenantId, query); - log.trace("Found user credentials [{}] by activateToken [{}]", userCredentialsEntity, activateToken); - return DaoUtil.getData(userCredentialsEntity); - } - - @Override - public UserCredentials findByResetToken(TenantId tenantId, String resetToken) { - log.debug("Try to find user credentials by resetToken [{}] ", resetToken); - Where query = select().from(ModelConstants.USER_CREDENTIALS_BY_RESET_TOKEN_COLUMN_FAMILY_NAME) - .where(eq(ModelConstants.USER_CREDENTIALS_RESET_TOKEN_PROPERTY, resetToken)); - log.trace(EXECUTE_QUERY, query); - UserCredentialsEntity userCredentialsEntity = findOneByStatement(tenantId, query); - log.trace("Found user credentials [{}] by resetToken [{}]", userCredentialsEntity, resetToken); - return DaoUtil.getData(userCredentialsEntity); - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/CassandraUserDao.java b/dao/src/main/java/org/thingsboard/server/dao/user/CassandraUserDao.java deleted file mode 100644 index 2b4546676a..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/user/CassandraUserDao.java +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.user; - -import com.datastax.driver.core.querybuilder.Select.Where; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; -import org.thingsboard.server.common.data.security.Authority; -import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.nosql.UserEntity; -import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTextDao; -import org.thingsboard.server.dao.util.NoSqlDao; - -import java.util.Arrays; -import java.util.List; -import java.util.UUID; - -import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; -import static com.datastax.driver.core.querybuilder.QueryBuilder.select; - -@Component -@Slf4j -@NoSqlDao -public class CassandraUserDao extends CassandraAbstractSearchTextDao implements UserDao { - - @Override - protected Class getColumnFamilyClass() { - return UserEntity.class; - } - - @Override - protected String getColumnFamilyName() { - return ModelConstants.USER_COLUMN_FAMILY_NAME; - } - - @Override - public User findByEmail(TenantId tenantId, String email) { - log.debug("Try to find user by email [{}] ", email); - Where query = select().from(ModelConstants.USER_BY_EMAIL_COLUMN_FAMILY_NAME).where(eq(ModelConstants.USER_EMAIL_PROPERTY, email)); - log.trace("Execute query {}", query); - UserEntity userEntity = findOneByStatement(tenantId, query); - log.trace("Found user [{}] by email [{}]", userEntity, email); - return DaoUtil.getData(userEntity); - } - - @Override - public List findTenantAdmins(UUID tenantId, TextPageLink pageLink) { - log.debug("Try to find tenant admin users by tenantId [{}] and pageLink [{}]", tenantId, pageLink); - List userEntities = findPageWithTextSearch(new TenantId(tenantId), - ModelConstants.USER_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME, - Arrays.asList(eq(ModelConstants.USER_TENANT_ID_PROPERTY, tenantId), - eq(ModelConstants.USER_CUSTOMER_ID_PROPERTY, ModelConstants.NULL_UUID), - eq(ModelConstants.USER_AUTHORITY_PROPERTY, Authority.TENANT_ADMIN.name())), - pageLink); - log.trace("Found tenant admin users [{}] by tenantId [{}] and pageLink [{}]", userEntities, tenantId, pageLink); - return DaoUtil.convertDataList(userEntities); - } - - @Override - public List findCustomerUsers(UUID tenantId, UUID customerId, TextPageLink pageLink) { - log.debug("Try to find customer users by tenantId [{}], customerId [{}] and pageLink [{}]", tenantId, customerId, pageLink); - List userEntities = findPageWithTextSearch(new TenantId(tenantId), - ModelConstants.USER_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME, - Arrays.asList(eq(ModelConstants.USER_TENANT_ID_PROPERTY, tenantId), - eq(ModelConstants.USER_CUSTOMER_ID_PROPERTY, customerId), - eq(ModelConstants.USER_AUTHORITY_PROPERTY, Authority.CUSTOMER_USER.name())), - pageLink); - log.trace("Found customer users [{}] by tenantId [{}], customerId [{}] and pageLink [{}]", userEntities, tenantId, customerId, pageLink); - return DaoUtil.convertDataList(userEntities); - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java index 4e7b222c66..12bebe9cec 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java @@ -17,10 +17,10 @@ package org.thingsboard.server.dao.user; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; -import java.util.List; import java.util.UUID; public interface UserDao extends Dao { @@ -40,6 +40,15 @@ public interface UserDao extends Dao { * @return the user entity */ User findByEmail(TenantId tenantId, String email); + + /** + * Find users by tenantId and page link. + * + * @param tenantId the tenantId + * @param pageLink the page link + * @return the list of user entities + */ + PageData findByTenantId(UUID tenantId, PageLink pageLink); /** * Find tenant admin users by tenantId and page link. @@ -48,7 +57,7 @@ public interface UserDao extends Dao { * @param pageLink the page link * @return the list of user entities */ - List findTenantAdmins(UUID tenantId, TextPageLink pageLink); + PageData findTenantAdmins(UUID tenantId, PageLink pageLink); /** * Find customer users by tenantId, customerId and page link. @@ -58,6 +67,6 @@ public interface UserDao extends Dao { * @param pageLink the page link * @return the list of user entities */ - List findCustomerUsers(UUID tenantId, UUID customerId, TextPageLink pageLink); + PageData findCustomerUsers(UUID tenantId, UUID customerId, PageLink pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index f07e939cda..314684c3fe 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -32,8 +32,8 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserCredentialsId; import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.dao.customer.CustomerDao; @@ -46,7 +46,6 @@ import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.tenant.TenantDao; import java.util.HashMap; -import java.util.List; import java.util.Map; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -221,12 +220,19 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic } @Override - public TextPageData findTenantAdmins(TenantId tenantId, TextPageLink pageLink) { + public PageData findUsersByTenantId(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findUsersByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validatePageLink(pageLink); + return userDao.findByTenantId(tenantId.getId(), pageLink); + } + + @Override + public PageData findTenantAdmins(TenantId tenantId, PageLink pageLink) { log.trace("Executing findTenantAdmins, tenantId [{}], pageLink [{}]", tenantId, pageLink); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validatePageLink(pageLink, "Incorrect page link " + pageLink); - List users = userDao.findTenantAdmins(tenantId.getId(), pageLink); - return new TextPageData<>(users, pageLink); + validatePageLink(pageLink); + return userDao.findTenantAdmins(tenantId.getId(), pageLink); } @Override @@ -237,13 +243,12 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic } @Override - public TextPageData findCustomerUsers(TenantId tenantId, CustomerId customerId, TextPageLink pageLink) { + public PageData findCustomerUsers(TenantId tenantId, CustomerId customerId, PageLink pageLink) { log.trace("Executing findCustomerUsers, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); validateId(customerId, "Incorrect customerId " + customerId); - validatePageLink(pageLink, "Incorrect page link " + pageLink); - List users = userDao.findCustomerUsers(tenantId.getId(), customerId.getId(), pageLink); - return new TextPageData<>(users, pageLink); + validatePageLink(pageLink); + return userDao.findCustomerUsers(tenantId.getId(), customerId.getId(), pageLink); } @Override @@ -462,7 +467,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic private PaginatedRemover tenantAdminsRemover = new PaginatedRemover() { @Override - protected List findEntities(TenantId tenantId, TenantId id, TextPageLink pageLink) { + protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { return userDao.findTenantAdmins(id.getId(), pageLink); } @@ -474,7 +479,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic private PaginatedRemover customerUsersRemover = new PaginatedRemover() { @Override - protected List findEntities(TenantId tenantId, CustomerId id, TextPageLink pageLink) { + protected PageData findEntities(TenantId tenantId, CustomerId id, PageLink pageLink) { return userDao.findCustomerUsers(tenantId.getId(), id.getId(), pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java b/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java index e0ce4e2ed5..d3a8ebbac6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java @@ -15,7 +15,14 @@ */ package org.thingsboard.server.dao.util; -import com.datastax.driver.core.*; +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.cql.BoundStatement; +import com.datastax.oss.driver.api.core.cql.ColumnDefinition; +import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; +import com.datastax.oss.driver.api.core.cql.PreparedStatement; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -30,7 +37,15 @@ import org.thingsboard.server.dao.nosql.CassandraStatementTask; import javax.annotation.Nullable; import java.util.UUID; -import java.util.concurrent.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; @@ -214,7 +229,7 @@ public abstract class AbstractBufferedRateExecutor codec = codecRegistry.codecFor(type); if (boundStatement.getBytesUnsafe(index) != null) { - Object value = codec.deserialize(boundStatement.getBytesUnsafe(index), protocolVersion); + Object value = codec.decode(boundStatement.getBytesUnsafe(index), protocolVersion); String replacement = Matcher.quoteReplacement(codec.format(value)); query = query.replaceFirst("\\?", replacement); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/mapping/JacksonUtil.java b/dao/src/main/java/org/thingsboard/server/dao/util/mapping/JacksonUtil.java index d654e85d5c..907a17157b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/mapping/JacksonUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/mapping/JacksonUtil.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.util.mapping; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import java.io.IOException; @@ -30,28 +31,28 @@ public class JacksonUtil { public static T convertValue(Object fromValue, Class toValueType) { try { - return OBJECT_MAPPER.convertValue(fromValue, toValueType); + return fromValue != null ? OBJECT_MAPPER.convertValue(fromValue, toValueType) : null; } catch (IllegalArgumentException e) { throw new IllegalArgumentException("The given object value: " - + fromValue + " cannot be converted to " + toValueType); + + fromValue + " cannot be converted to " + toValueType, e); } } public static T fromString(String string, Class clazz) { try { - return OBJECT_MAPPER.readValue(string, clazz); + return string != null ? OBJECT_MAPPER.readValue(string, clazz) : null; } catch (IOException e) { throw new IllegalArgumentException("The given string value: " - + string + " cannot be transformed to Json object"); + + string + " cannot be transformed to Json object", e); } } public static String toString(Object value) { try { - return OBJECT_MAPPER.writeValueAsString(value); + return value != null ? OBJECT_MAPPER.writeValueAsString(value) : null; } catch (JsonProcessingException e) { throw new IllegalArgumentException("The given Json object value: " - + value + " cannot be transformed to a String"); + + value + " cannot be transformed to a String", e); } } @@ -65,8 +66,16 @@ public class JacksonUtil { throw new IllegalArgumentException(e); } } + + public static ObjectNode newObjectNode(){ + return OBJECT_MAPPER.createObjectNode(); + } public static T clone(T value) { return fromString(toString(value), (Class) value.getClass()); } + + public static JsonNode valueToTree(T value) { + return OBJECT_MAPPER.valueToTree(value); + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/mapping/JsonBinarySqlTypeDescriptor.java b/dao/src/main/java/org/thingsboard/server/dao/util/mapping/JsonBinarySqlTypeDescriptor.java new file mode 100644 index 0000000000..05d0315101 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/util/mapping/JsonBinarySqlTypeDescriptor.java @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.util.mapping; + +import com.fasterxml.jackson.databind.JsonNode; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaTypeDescriptor; +import org.hibernate.type.descriptor.sql.BasicBinder; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; + +public class JsonBinarySqlTypeDescriptor extends AbstractJsonSqlTypeDescriptor { + + public static final JsonBinarySqlTypeDescriptor INSTANCE = new JsonBinarySqlTypeDescriptor(); + + @Override + public int getSqlType() { + return Types.OTHER; + } + + @Override + public ValueBinder getBinder(final JavaTypeDescriptor javaTypeDescriptor) { + return new BasicBinder(javaTypeDescriptor, this) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { + st.setObject(index, javaTypeDescriptor.unwrap(value, JsonNode.class, options), getSqlType()); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) throws SQLException { + st.setObject(name, javaTypeDescriptor.unwrap(value, JsonNode.class, options), getSqlType()); + } + }; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/mapping/JsonBinaryType.java b/dao/src/main/java/org/thingsboard/server/dao/util/mapping/JsonBinaryType.java new file mode 100644 index 0000000000..2eea7e7dc7 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/util/mapping/JsonBinaryType.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.util.mapping; + +import org.hibernate.type.AbstractSingleColumnStandardBasicType; +import org.hibernate.usertype.DynamicParameterizedType; + +import java.util.Properties; + +public class JsonBinaryType extends AbstractSingleColumnStandardBasicType implements DynamicParameterizedType { + + public JsonBinaryType() { + super( + JsonBinarySqlTypeDescriptor.INSTANCE, + new JsonTypeDescriptor() + ); + } + + public String getName() { + return "jsonb"; + } + + @Override + protected boolean registerUnderJavaType() { + return true; + } + + @Override + public void setParameterValues(Properties parameters) { + ((JsonTypeDescriptor) getJavaTypeDescriptor()) + .setParameterValues(parameters); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/CassandraWidgetTypeDao.java b/dao/src/main/java/org/thingsboard/server/dao/widget/CassandraWidgetTypeDao.java deleted file mode 100644 index cb4fc83d21..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/CassandraWidgetTypeDao.java +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.widget; - -import com.datastax.driver.core.querybuilder.Select.Where; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.widget.WidgetType; -import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.model.nosql.WidgetTypeEntity; -import org.thingsboard.server.dao.nosql.CassandraAbstractModelDao; -import org.thingsboard.server.dao.util.NoSqlDao; - -import java.util.List; -import java.util.UUID; - -import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; -import static com.datastax.driver.core.querybuilder.QueryBuilder.select; -import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_ALIAS_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_BUNDLE_ALIAS_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_BY_TENANT_AND_ALIASES_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_TENANT_ID_PROPERTY; - -@Component -@Slf4j -@NoSqlDao -public class CassandraWidgetTypeDao extends CassandraAbstractModelDao implements WidgetTypeDao { - - @Override - protected Class getColumnFamilyClass() { - return WidgetTypeEntity.class; - } - - @Override - protected String getColumnFamilyName() { - return WIDGET_TYPE_COLUMN_FAMILY_NAME; - } - - @Override - public List findWidgetTypesByTenantIdAndBundleAlias(UUID tenantId, String bundleAlias) { - log.debug("Try to find widget types by tenantId [{}] and bundleAlias [{}]", tenantId, bundleAlias); - Where query = select().from(WIDGET_TYPE_BY_TENANT_AND_ALIASES_COLUMN_FAMILY_NAME) - .where() - .and(eq(WIDGET_TYPE_TENANT_ID_PROPERTY, tenantId)) - .and(eq(WIDGET_TYPE_BUNDLE_ALIAS_PROPERTY, bundleAlias)); - List widgetTypesEntities = findListByStatement(new TenantId(tenantId), query); - log.trace("Found widget types [{}] by tenantId [{}] and bundleAlias [{}]", widgetTypesEntities, tenantId, bundleAlias); - return DaoUtil.convertDataList(widgetTypesEntities); - } - - @Override - public WidgetType findByTenantIdBundleAliasAndAlias(UUID tenantId, String bundleAlias, String alias) { - log.debug("Try to find widget type by tenantId [{}], bundleAlias [{}] and alias [{}]", tenantId, bundleAlias, alias); - Where query = select().from(WIDGET_TYPE_BY_TENANT_AND_ALIASES_COLUMN_FAMILY_NAME) - .where() - .and(eq(WIDGET_TYPE_TENANT_ID_PROPERTY, tenantId)) - .and(eq(WIDGET_TYPE_BUNDLE_ALIAS_PROPERTY, bundleAlias)) - .and(eq(WIDGET_TYPE_ALIAS_PROPERTY, alias)); - log.trace("Execute query {}", query); - WidgetTypeEntity widgetTypeEntity = findOneByStatement(new TenantId(tenantId), query); - log.trace("Found widget type [{}] by tenantId [{}], bundleAlias [{}] and alias [{}]", - widgetTypeEntity, tenantId, bundleAlias, alias); - return DaoUtil.getData(widgetTypeEntity); - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/CassandraWidgetsBundleDao.java b/dao/src/main/java/org/thingsboard/server/dao/widget/CassandraWidgetsBundleDao.java deleted file mode 100644 index 8d574c17a2..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/CassandraWidgetsBundleDao.java +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.widget; - -import com.datastax.driver.core.querybuilder.Select; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.Tenant; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; -import org.thingsboard.server.common.data.widget.WidgetsBundle; -import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.model.nosql.WidgetsBundleEntity; -import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTextDao; -import org.thingsboard.server.dao.util.NoSqlDao; - -import java.util.Arrays; -import java.util.List; -import java.util.UUID; - -import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; -import static com.datastax.driver.core.querybuilder.QueryBuilder.in; -import static com.datastax.driver.core.querybuilder.QueryBuilder.select; -import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; -import static org.thingsboard.server.dao.model.ModelConstants.WIDGETS_BUNDLE_ALIAS_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.WIDGETS_BUNDLE_BY_TENANT_AND_ALIAS_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.WIDGETS_BUNDLE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.WIDGETS_BUNDLE_COLUMN_FAMILY_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.WIDGETS_BUNDLE_TENANT_ID_PROPERTY; - -@Component -@Slf4j -@NoSqlDao -public class CassandraWidgetsBundleDao extends CassandraAbstractSearchTextDao implements WidgetsBundleDao { - - @Override - protected Class getColumnFamilyClass() { - return WidgetsBundleEntity.class; - } - - @Override - protected String getColumnFamilyName() { - return WIDGETS_BUNDLE_COLUMN_FAMILY_NAME; - } - - @Override - public WidgetsBundle findWidgetsBundleByTenantIdAndAlias(UUID tenantId, String alias) { - log.debug("Try to find widgets bundle by tenantId [{}] and alias [{}]", tenantId, alias); - Select.Where query = select().from(WIDGETS_BUNDLE_BY_TENANT_AND_ALIAS_COLUMN_FAMILY_NAME) - .where() - .and(eq(WIDGETS_BUNDLE_TENANT_ID_PROPERTY, tenantId)) - .and(eq(WIDGETS_BUNDLE_ALIAS_PROPERTY, alias)); - log.trace("Execute query {}", query); - WidgetsBundleEntity widgetsBundleEntity = findOneByStatement(new TenantId(tenantId), query); - log.trace("Found widgets bundle [{}] by tenantId [{}] and alias [{}]", - widgetsBundleEntity, tenantId, alias); - return DaoUtil.getData(widgetsBundleEntity); - } - - @Override - public List findSystemWidgetsBundles(TenantId tenantId, TextPageLink pageLink) { - log.debug("Try to find system widgets bundles by pageLink [{}]", pageLink); - List widgetsBundlesEntities = findPageWithTextSearch(tenantId, WIDGETS_BUNDLE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME, - Arrays.asList(eq(WIDGETS_BUNDLE_TENANT_ID_PROPERTY, NULL_UUID)), - pageLink); - log.trace("Found system widgets bundles [{}] by pageLink [{}]", widgetsBundlesEntities, pageLink); - return DaoUtil.convertDataList(widgetsBundlesEntities); - } - - @Override - public List findTenantWidgetsBundlesByTenantId(UUID tenantId, TextPageLink pageLink) { - log.debug("Try to find tenant widgets bundles by tenantId [{}] and pageLink [{}]", tenantId, pageLink); - List widgetsBundlesEntities = findPageWithTextSearch(new TenantId(tenantId), WIDGETS_BUNDLE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME, - Arrays.asList(eq(WIDGETS_BUNDLE_TENANT_ID_PROPERTY, tenantId)), - pageLink); - log.trace("Found tenant widgets bundles [{}] by tenantId [{}] and pageLink [{}]", widgetsBundlesEntities, tenantId, pageLink); - return DaoUtil.convertDataList(widgetsBundlesEntities); - } - - @Override - public List findAllTenantWidgetsBundlesByTenantId(UUID tenantId, TextPageLink pageLink) { - log.debug("Try to find all tenant widgets bundles by tenantId [{}] and pageLink [{}]", tenantId, pageLink); - List widgetsBundlesEntities = findPageWithTextSearch(new TenantId(tenantId), WIDGETS_BUNDLE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME, - Arrays.asList(in(WIDGETS_BUNDLE_TENANT_ID_PROPERTY, Arrays.asList(NULL_UUID, tenantId))), - pageLink); - log.trace("Found all tenant widgets bundles [{}] by tenantId [{}] and pageLink [{}]", widgetsBundlesEntities, tenantId, pageLink); - return DaoUtil.convertDataList(widgetsBundlesEntities); - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java index 6de767c687..9a43c8cd08 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java @@ -16,11 +16,11 @@ package org.thingsboard.server.dao.widget; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.Dao; -import java.util.List; import java.util.UUID; /** @@ -52,7 +52,7 @@ public interface WidgetsBundleDao extends Dao { * @param pageLink the page link * @return the list of widgets bundles objects */ - List findSystemWidgetsBundles(TenantId tenantId, TextPageLink pageLink); + PageData findSystemWidgetsBundles(TenantId tenantId, PageLink pageLink); /** * Find tenant widgets bundles by tenantId and page link. @@ -61,7 +61,7 @@ public interface WidgetsBundleDao extends Dao { * @param pageLink the page link * @return the list of widgets bundles objects */ - List findTenantWidgetsBundlesByTenantId(UUID tenantId, TextPageLink pageLink); + PageData findTenantWidgetsBundlesByTenantId(UUID tenantId, PageLink pageLink); /** * Find all tenant widgets bundles (including system) by tenantId and page link. @@ -70,7 +70,7 @@ public interface WidgetsBundleDao extends Dao { * @param pageLink the page link * @return the list of widgets bundles objects */ - List findAllTenantWidgetsBundlesByTenantId(UUID tenantId, TextPageLink pageLink); + PageData findAllTenantWidgetsBundlesByTenantId(UUID tenantId, PageLink pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java index ffdb03ef17..75fea9e3e5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java @@ -22,8 +22,8 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetsBundleId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; @@ -88,42 +88,42 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { } @Override - public TextPageData findSystemWidgetsBundlesByPageLink(TenantId tenantId, TextPageLink pageLink) { + public PageData findSystemWidgetsBundlesByPageLink(TenantId tenantId, PageLink pageLink) { log.trace("Executing findSystemWidgetsBundles, pageLink [{}]", pageLink); - Validator.validatePageLink(pageLink, INCORRECT_PAGE_LINK + pageLink); - return new TextPageData<>(widgetsBundleDao.findSystemWidgetsBundles(tenantId, pageLink), pageLink); + Validator.validatePageLink(pageLink); + return widgetsBundleDao.findSystemWidgetsBundles(tenantId, pageLink); } @Override public List findSystemWidgetsBundles(TenantId tenantId) { log.trace("Executing findSystemWidgetsBundles"); List widgetsBundles = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(DEFAULT_WIDGETS_BUNDLE_LIMIT); - TextPageData pageData; + PageLink pageLink = new PageLink(DEFAULT_WIDGETS_BUNDLE_LIMIT); + PageData pageData; do { pageData = findSystemWidgetsBundlesByPageLink(tenantId, pageLink); widgetsBundles.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); return widgetsBundles; } @Override - public TextPageData findTenantWidgetsBundlesByTenantId(TenantId tenantId, TextPageLink pageLink) { + public PageData findTenantWidgetsBundlesByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findTenantWidgetsBundlesByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - Validator.validatePageLink(pageLink, INCORRECT_PAGE_LINK + pageLink); - return new TextPageData<>(widgetsBundleDao.findTenantWidgetsBundlesByTenantId(tenantId.getId(), pageLink), pageLink); + Validator.validatePageLink(pageLink); + return widgetsBundleDao.findTenantWidgetsBundlesByTenantId(tenantId.getId(), pageLink); } @Override - public TextPageData findAllTenantWidgetsBundlesByTenantIdAndPageLink(TenantId tenantId, TextPageLink pageLink) { + public PageData findAllTenantWidgetsBundlesByTenantIdAndPageLink(TenantId tenantId, PageLink pageLink) { log.trace("Executing findAllTenantWidgetsBundlesByTenantIdAndPageLink, tenantId [{}], pageLink [{}]", tenantId, pageLink); Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - Validator.validatePageLink(pageLink, INCORRECT_PAGE_LINK + pageLink); - return new TextPageData<>(widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId.getId(), pageLink), pageLink); + Validator.validatePageLink(pageLink); + return widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId.getId(), pageLink); } @Override @@ -131,13 +131,13 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { log.trace("Executing findAllTenantWidgetsBundlesByTenantId, tenantId [{}]", tenantId); Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); List widgetsBundles = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(DEFAULT_WIDGETS_BUNDLE_LIMIT); - TextPageData pageData; + PageLink pageLink = new PageLink(DEFAULT_WIDGETS_BUNDLE_LIMIT); + PageData pageData; do { pageData = findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, pageLink); widgetsBundles.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); return widgetsBundles; @@ -204,7 +204,7 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { new PaginatedRemover() { @Override - protected List findEntities(TenantId tenantId, TenantId id, TextPageLink pageLink) { + protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { return widgetsBundleDao.findTenantWidgetsBundlesByTenantId(id.getId(), pageLink); } diff --git a/dao/src/main/resources/cassandra/schema-entities.cql b/dao/src/main/resources/cassandra/schema-entities.cql deleted file mode 100644 index 6d23ca8122..0000000000 --- a/dao/src/main/resources/cassandra/schema-entities.cql +++ /dev/null @@ -1,719 +0,0 @@ --- --- Copyright © 2016-2020 The Thingsboard Authors --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. --- - -CREATE KEYSPACE IF NOT EXISTS thingsboard -WITH replication = { - 'class' : 'SimpleStrategy', - 'replication_factor' : 1 -}; - -CREATE TABLE IF NOT EXISTS thingsboard.user ( - id timeuuid, - tenant_id timeuuid, - customer_id timeuuid, - email text, - search_text text, - authority text, - first_name text, - last_name text, - additional_info text, - PRIMARY KEY (id, tenant_id, customer_id, authority) -); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.user_by_email AS - SELECT * - from thingsboard.user - WHERE email IS NOT NULL AND tenant_id IS NOT NULL AND customer_id IS NOT NULL AND id IS NOT NULL AND authority IS NOT - NULL - PRIMARY KEY ( email, tenant_id, customer_id, id, authority ); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.user_by_tenant_and_search_text AS - SELECT * - from thingsboard.user - WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND authority IS NOT NULL AND search_text IS NOT NULL AND id - IS NOT NULL - PRIMARY KEY ( tenant_id, customer_id, authority, search_text, id ) - WITH CLUSTERING ORDER BY ( customer_id DESC, authority DESC, search_text ASC, id DESC ); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.user_by_customer_and_search_text AS - SELECT * - from thingsboard.user - WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND authority IS NOT NULL AND search_text IS NOT NULL AND id - IS NOT NULL - PRIMARY KEY ( customer_id, tenant_id, authority, search_text, id ) - WITH CLUSTERING ORDER BY ( tenant_id DESC, authority DESC, search_text ASC, id DESC ); - -CREATE TABLE IF NOT EXISTS thingsboard.user_credentials ( - id timeuuid PRIMARY KEY, - user_id timeuuid, - enabled boolean, - password text, - activate_token text, - reset_token text -); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.user_credentials_by_user AS - SELECT * - from thingsboard.user_credentials - WHERE user_id IS NOT NULL AND id IS NOT NULL - PRIMARY KEY ( user_id, id ); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.user_credentials_by_activate_token AS - SELECT * - from thingsboard.user_credentials - WHERE activate_token IS NOT NULL AND id IS NOT NULL - PRIMARY KEY ( activate_token, id ); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.user_credentials_by_reset_token AS - SELECT * - from thingsboard.user_credentials - WHERE reset_token IS NOT NULL AND id IS NOT NULL - PRIMARY KEY ( reset_token, id ); - -CREATE TABLE IF NOT EXISTS thingsboard.admin_settings ( - id timeuuid PRIMARY KEY, - key text, - json_value text -); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.admin_settings_by_key AS - SELECT * - from thingsboard.admin_settings - WHERE key IS NOT NULL AND id IS NOT NULL - PRIMARY KEY ( key, id ) - WITH CLUSTERING ORDER BY ( id DESC ); - -CREATE TABLE IF NOT EXISTS thingsboard.tenant ( - id timeuuid, - title text, - search_text text, - region text, - country text, - state text, - city text, - address text, - address2 text, - zip text, - phone text, - email text, - additional_info text, - isolated_tb_core boolean, - isolated_tb_rule_engine boolean, - PRIMARY KEY (id, region) -); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.tenant_by_region_and_search_text AS - SELECT * - from thingsboard.tenant - WHERE region IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL - PRIMARY KEY ( region, search_text, id ) - WITH CLUSTERING ORDER BY ( search_text ASC, id DESC ); - -CREATE TABLE IF NOT EXISTS thingsboard.customer ( - id timeuuid, - tenant_id timeuuid, - title text, - search_text text, - country text, - state text, - city text, - address text, - address2 text, - zip text, - phone text, - email text, - additional_info text, - PRIMARY KEY (id, tenant_id) -); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.customer_by_tenant_and_title AS - SELECT * - from thingsboard.customer - WHERE tenant_id IS NOT NULL AND title IS NOT NULL AND id IS NOT NULL - PRIMARY KEY ( tenant_id, title, id ) - WITH CLUSTERING ORDER BY ( title ASC, id DESC ); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.customer_by_tenant_and_search_text AS - SELECT * - from thingsboard.customer - WHERE tenant_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL - PRIMARY KEY ( tenant_id, search_text, id ) - WITH CLUSTERING ORDER BY ( search_text ASC, id DESC ); - -CREATE TABLE IF NOT EXISTS thingsboard.device ( - id timeuuid, - tenant_id timeuuid, - customer_id timeuuid, - name text, - type text, - label text, - search_text text, - additional_info text, - PRIMARY KEY (id, tenant_id, customer_id, type) -); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_tenant_and_name AS - SELECT * - from thingsboard.device - WHERE tenant_id IS NOT NULL - AND customer_id IS NOT NULL - AND type IS NOT NULL - AND name IS NOT NULL - AND id IS NOT NULL - PRIMARY KEY ( tenant_id, name, id, customer_id, type) - WITH CLUSTERING ORDER BY ( name ASC, id DESC, customer_id DESC); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_tenant_and_search_text AS - SELECT * - from thingsboard.device - WHERE tenant_id IS NOT NULL - AND customer_id IS NOT NULL - AND type IS NOT NULL - AND search_text IS NOT NULL - AND id IS NOT NULL - PRIMARY KEY ( tenant_id, search_text, id, customer_id, type) - WITH CLUSTERING ORDER BY ( search_text ASC, id DESC, customer_id DESC); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_tenant_by_type_and_search_text AS - SELECT * - from thingsboard.device - WHERE tenant_id IS NOT NULL - AND customer_id IS NOT NULL - AND type IS NOT NULL - AND search_text IS NOT NULL - AND id IS NOT NULL - PRIMARY KEY ( tenant_id, type, search_text, id, customer_id) - WITH CLUSTERING ORDER BY ( type ASC, search_text ASC, id DESC, customer_id DESC); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_customer_and_search_text AS - SELECT * - from thingsboard.device - WHERE tenant_id IS NOT NULL - AND customer_id IS NOT NULL - AND type IS NOT NULL - AND search_text IS NOT NULL - AND id IS NOT NULL - PRIMARY KEY ( customer_id, tenant_id, search_text, id, type ) - WITH CLUSTERING ORDER BY ( tenant_id DESC, search_text ASC, id DESC ); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_customer_by_type_and_search_text AS - SELECT * - from thingsboard.device - WHERE tenant_id IS NOT NULL - AND customer_id IS NOT NULL - AND type IS NOT NULL - AND search_text IS NOT NULL - AND id IS NOT NULL - PRIMARY KEY ( customer_id, tenant_id, type, search_text, id ) - WITH CLUSTERING ORDER BY ( tenant_id DESC, type ASC, search_text ASC, id DESC ); - -CREATE TABLE IF NOT EXISTS thingsboard.device_credentials ( - id timeuuid PRIMARY KEY, - device_id timeuuid, - credentials_type text, - credentials_id text, - credentials_value text -); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_credentials_by_device AS - SELECT * - from thingsboard.device_credentials - WHERE device_id IS NOT NULL AND id IS NOT NULL - PRIMARY KEY ( device_id, id ); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_credentials_by_credentials_id AS - SELECT * - from thingsboard.device_credentials - WHERE credentials_id IS NOT NULL AND id IS NOT NULL - PRIMARY KEY ( credentials_id, id ); - -CREATE TABLE IF NOT EXISTS thingsboard.asset ( - id timeuuid, - tenant_id timeuuid, - customer_id timeuuid, - name text, - type text, - label text, - search_text text, - additional_info text, - PRIMARY KEY (id, tenant_id, customer_id, type) -); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.asset_by_tenant_and_name AS - SELECT * - from thingsboard.asset - WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND name IS NOT NULL AND id IS NOT NULL - PRIMARY KEY ( tenant_id, name, id, customer_id, type) - WITH CLUSTERING ORDER BY ( name ASC, id DESC, customer_id DESC); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.asset_by_tenant_and_search_text AS - SELECT * - from thingsboard.asset - WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL - PRIMARY KEY ( tenant_id, search_text, id, customer_id, type) - WITH CLUSTERING ORDER BY ( search_text ASC, id DESC, customer_id DESC); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.asset_by_tenant_by_type_and_search_text AS - SELECT * - from thingsboard.asset - WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL - PRIMARY KEY ( tenant_id, type, search_text, id, customer_id) - WITH CLUSTERING ORDER BY ( type ASC, search_text ASC, id DESC, customer_id DESC); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.asset_by_customer_and_search_text AS - SELECT * - from thingsboard.asset - WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL - PRIMARY KEY ( customer_id, tenant_id, search_text, id, type ) - WITH CLUSTERING ORDER BY ( tenant_id DESC, search_text ASC, id DESC ); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.asset_by_customer_by_type_and_search_text AS - SELECT * - from thingsboard.asset - WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL - PRIMARY KEY ( customer_id, tenant_id, type, search_text, id ) - WITH CLUSTERING ORDER BY ( tenant_id DESC, type ASC, search_text ASC, id DESC ); - -CREATE TABLE IF NOT EXISTS thingsboard.entity_subtype ( - tenant_id timeuuid, - entity_type text, // (DEVICE, ASSET) - type text, - PRIMARY KEY (tenant_id, entity_type, type) -); - -CREATE TABLE IF NOT EXISTS thingsboard.alarm ( - id timeuuid, - tenant_id timeuuid, - type text, - originator_id timeuuid, - originator_type text, - severity text, - status text, - start_ts bigint, - end_ts bigint, - ack_ts bigint, - clear_ts bigint, - details text, - propagate boolean, - propagate_relation_types text, - PRIMARY KEY ((tenant_id, originator_id, originator_type), type, id) -) WITH CLUSTERING ORDER BY ( type ASC, id DESC); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.alarm_by_id AS - SELECT * - from thingsboard.alarm - WHERE tenant_id IS NOT NULL AND originator_id IS NOT NULL AND originator_type IS NOT NULL AND type IS NOT NULL - AND type IS NOT NULL AND id IS NOT NULL - PRIMARY KEY (id, tenant_id, originator_id, originator_type, type) - WITH CLUSTERING ORDER BY ( tenant_id ASC, originator_id ASC, originator_type ASC, type ASC); - -CREATE TABLE IF NOT EXISTS thingsboard.relation ( - from_id timeuuid, - from_type text, - to_id timeuuid, - to_type text, - relation_type_group text, - relation_type text, - additional_info text, - PRIMARY KEY ((from_id, from_type), relation_type_group, relation_type, to_id, to_type) -) WITH CLUSTERING ORDER BY ( relation_type_group ASC, relation_type ASC, to_id ASC, to_type ASC); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.relation_by_type_and_child_type AS - SELECT * - from thingsboard.relation - WHERE from_id IS NOT NULL AND from_type IS NOT NULL AND relation_type_group IS NOT NULL AND relation_type IS NOT NULL AND to_id IS NOT NULL AND to_type IS NOT NULL - PRIMARY KEY ((from_id, from_type), relation_type_group, relation_type, to_type, to_id) - WITH CLUSTERING ORDER BY ( relation_type_group ASC, relation_type ASC, to_type ASC, to_id DESC); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.reverse_relation AS - SELECT * - from thingsboard.relation - WHERE from_id IS NOT NULL AND from_type IS NOT NULL AND relation_type_group IS NOT NULL AND relation_type IS NOT NULL AND to_id IS NOT NULL AND to_type IS NOT NULL - PRIMARY KEY ((to_id, to_type), relation_type_group, relation_type, from_id, from_type) - WITH CLUSTERING ORDER BY ( relation_type_group ASC, relation_type ASC, from_id ASC, from_type ASC); - -CREATE TABLE IF NOT EXISTS thingsboard.widgets_bundle ( - id timeuuid, - tenant_id timeuuid, - alias text, - title text, - search_text text, - image blob, - PRIMARY KEY (id, tenant_id) -); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.widgets_bundle_by_tenant_and_search_text AS - SELECT * - from thingsboard.widgets_bundle - WHERE tenant_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL - PRIMARY KEY ( tenant_id, search_text, id ) - WITH CLUSTERING ORDER BY ( search_text ASC, id DESC ); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.widgets_bundle_by_tenant_and_alias AS - SELECT * - from thingsboard.widgets_bundle - WHERE tenant_id IS NOT NULL AND alias IS NOT NULL AND id IS NOT NULL - PRIMARY KEY ( tenant_id, alias, id ) - WITH CLUSTERING ORDER BY ( alias ASC, id DESC ); - -CREATE TABLE IF NOT EXISTS thingsboard.widget_type ( - id timeuuid, - tenant_id timeuuid, - bundle_alias text, - alias text, - name text, - descriptor text, - PRIMARY KEY (id, tenant_id, bundle_alias) -); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.widget_type_by_tenant_and_aliases AS - SELECT * - from thingsboard.widget_type - WHERE tenant_id IS NOT NULL AND bundle_alias IS NOT NULL AND alias IS NOT NULL AND id IS NOT NULL - PRIMARY KEY ( tenant_id, bundle_alias, alias, id ) - WITH CLUSTERING ORDER BY ( bundle_alias ASC, alias ASC, id DESC ); - -CREATE TABLE IF NOT EXISTS thingsboard.dashboard ( - id timeuuid, - tenant_id timeuuid, - title text, - search_text text, - assigned_customers text, - configuration text, - PRIMARY KEY (id, tenant_id) -); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.dashboard_by_tenant_and_search_text AS - SELECT * - from thingsboard.dashboard - WHERE tenant_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL - PRIMARY KEY ( tenant_id, search_text, id ) - WITH CLUSTERING ORDER BY ( search_text ASC, id DESC ); - -CREATE TABLE IF NOT EXISTS thingsboard.attributes_kv_cf ( - entity_type text, // (DEVICE, CUSTOMER, TENANT) - entity_id timeuuid, - attribute_type text, // (CLIENT_SIDE, SHARED, SERVER_SIDE) - attribute_key text, - bool_v boolean, - str_v text, - long_v bigint, - dbl_v double, - json_v text, - last_update_ts bigint, - PRIMARY KEY ((entity_type, entity_id, attribute_type), attribute_key) -) WITH compaction = { 'class' : 'LeveledCompactionStrategy' }; - -CREATE TABLE IF NOT EXISTS thingsboard.component_descriptor ( - id timeuuid, - type text, - scope text, - name text, - search_text text, - clazz text, - configuration_descriptor text, - actions text, - PRIMARY KEY (clazz, id, type, scope) -); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.component_desc_by_type_search_text AS - SELECT * - from thingsboard.component_descriptor - WHERE type IS NOT NULL AND scope IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL AND clazz IS NOT NULL - PRIMARY KEY ( type, search_text, id, clazz, scope) - WITH CLUSTERING ORDER BY ( search_text DESC); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.component_desc_by_scope_type_search_text AS - SELECT * - from thingsboard.component_descriptor - WHERE type IS NOT NULL AND scope IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL AND clazz IS NOT NULL - PRIMARY KEY ( (scope, type), search_text, id, clazz) - WITH CLUSTERING ORDER BY ( search_text DESC); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.component_desc_by_id AS - SELECT * - from thingsboard.component_descriptor - WHERE type IS NOT NULL AND scope IS NOT NULL AND id IS NOT NULL AND clazz IS NOT NULL - PRIMARY KEY ( id, clazz, scope, type ) - WITH CLUSTERING ORDER BY ( clazz ASC, scope ASC, type DESC); - -CREATE TABLE IF NOT EXISTS thingsboard.event ( - tenant_id timeuuid, // tenant or system - id timeuuid, - event_type text, - event_uid text, - entity_type text, - entity_id timeuuid, - body text, - PRIMARY KEY ((tenant_id, entity_type, entity_id), event_type, event_uid) -); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.event_by_type_and_id AS - SELECT * - FROM thingsboard.event - WHERE tenant_id IS NOT NULL AND entity_type IS NOT NULL AND entity_id IS NOT NULL AND id IS NOT NULL - AND event_type IS NOT NULL AND event_uid IS NOT NULL - PRIMARY KEY ((tenant_id, entity_type, entity_id), event_type, id, event_uid) - WITH CLUSTERING ORDER BY (event_type ASC, id ASC, event_uid ASC); - - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.event_by_id AS - SELECT * - FROM thingsboard.event - WHERE tenant_id IS NOT NULL AND entity_type IS NOT NULL AND entity_id IS NOT NULL AND id IS NOT NULL - AND event_type IS NOT NULL AND event_uid IS NOT NULL - PRIMARY KEY ((tenant_id, entity_type, entity_id), id, event_type, event_uid) - WITH CLUSTERING ORDER BY (id ASC, event_type ASC, event_uid ASC); - -CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_entity_id ( - tenant_id timeuuid, - id timeuuid, - customer_id timeuuid, - entity_id timeuuid, - entity_type text, - entity_name text, - user_id timeuuid, - user_name text, - action_type text, - action_data text, - action_status text, - action_failure_details text, - PRIMARY KEY ((tenant_id, entity_id, entity_type), id) -); - -CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_customer_id ( - tenant_id timeuuid, - id timeuuid, - customer_id timeuuid, - entity_id timeuuid, - entity_type text, - entity_name text, - user_id timeuuid, - user_name text, - action_type text, - action_data text, - action_status text, - action_failure_details text, - PRIMARY KEY ((tenant_id, customer_id), id) -); - -CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_user_id ( - tenant_id timeuuid, - id timeuuid, - customer_id timeuuid, - entity_id timeuuid, - entity_type text, - entity_name text, - user_id timeuuid, - user_name text, - action_type text, - action_data text, - action_status text, - action_failure_details text, - PRIMARY KEY ((tenant_id, user_id), id) -); - -CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_tenant_id ( - tenant_id timeuuid, - id timeuuid, - partition bigint, - customer_id timeuuid, - entity_id timeuuid, - entity_type text, - entity_name text, - user_id timeuuid, - user_name text, - action_type text, - action_data text, - action_status text, - action_failure_details text, - PRIMARY KEY ((tenant_id, partition), id) -); - -CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_tenant_id_partitions ( - tenant_id timeuuid, - partition bigint, - PRIMARY KEY (( tenant_id ), partition) -) WITH CLUSTERING ORDER BY ( partition ASC ) -AND compaction = { 'class' : 'LeveledCompactionStrategy' }; - -CREATE TABLE IF NOT EXISTS thingsboard.msg_queue ( - node_id timeuuid, - cluster_partition bigint, - ts_partition bigint, - ts bigint, - msg blob, - PRIMARY KEY ((node_id, cluster_partition, ts_partition), ts)) -WITH CLUSTERING ORDER BY (ts DESC) -AND compaction = { - 'class': 'org.apache.cassandra.db.compaction.DateTieredCompactionStrategy', - 'min_threshold': '5', - 'base_time_seconds': '43200', - 'max_window_size_seconds': '43200', - 'tombstone_threshold': '0.9', - 'unchecked_tombstone_compaction': 'true' -}; - -CREATE TABLE IF NOT EXISTS thingsboard.msg_ack_queue ( - node_id timeuuid, - cluster_partition bigint, - ts_partition bigint, - msg_id timeuuid, - PRIMARY KEY ((node_id, cluster_partition, ts_partition), msg_id)) -WITH CLUSTERING ORDER BY (msg_id DESC) -AND compaction = { - 'class': 'org.apache.cassandra.db.compaction.DateTieredCompactionStrategy', - 'min_threshold': '5', - 'base_time_seconds': '43200', - 'max_window_size_seconds': '43200', - 'tombstone_threshold': '0.9', - 'unchecked_tombstone_compaction': 'true' -}; - -CREATE TABLE IF NOT EXISTS thingsboard.processed_msg_partitions ( - node_id timeuuid, - cluster_partition bigint, - ts_partition bigint, - PRIMARY KEY ((node_id, cluster_partition), ts_partition)) -WITH CLUSTERING ORDER BY (ts_partition DESC) -AND compaction = { - 'class': 'org.apache.cassandra.db.compaction.DateTieredCompactionStrategy', - 'min_threshold': '5', - 'base_time_seconds': '43200', - 'max_window_size_seconds': '43200', - 'tombstone_threshold': '0.9', - 'unchecked_tombstone_compaction': 'true' -}; - -CREATE TABLE IF NOT EXISTS thingsboard.rule_chain ( - id uuid, - tenant_id uuid, - name text, - search_text text, - first_rule_node_id uuid, - root boolean, - debug_mode boolean, - configuration text, - additional_info text, - PRIMARY KEY (id, tenant_id) -); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.rule_chain_by_tenant_and_search_text AS - SELECT * - from thingsboard.rule_chain - WHERE tenant_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL - PRIMARY KEY ( tenant_id, search_text, id ) - WITH CLUSTERING ORDER BY ( search_text ASC, id DESC ); - -CREATE TABLE IF NOT EXISTS thingsboard.rule_node ( - id uuid, - rule_chain_id uuid, - type text, - name text, - debug_mode boolean, - search_text text, - configuration text, - additional_info text, - PRIMARY KEY (id) -); - -CREATE TABLE IF NOT EXISTS thingsboard.entity_view ( - id timeuuid, - entity_id timeuuid, - entity_type text, - tenant_id timeuuid, - customer_id timeuuid, - name text, - type text, - keys text, - start_ts bigint, - end_ts bigint, - search_text text, - additional_info text, - PRIMARY KEY (id, entity_id, tenant_id, customer_id, type) -); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_name AS - SELECT * - from thingsboard.entity_view - WHERE tenant_id IS NOT NULL - AND entity_id IS NOT NULL - AND customer_id IS NOT NULL - AND type IS NOT NULL - AND name IS NOT NULL - AND id IS NOT NULL - PRIMARY KEY (tenant_id, name, id, customer_id, entity_id, type) - WITH CLUSTERING ORDER BY (name ASC, id DESC, customer_id DESC); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_search_text AS - SELECT * - from thingsboard.entity_view - WHERE tenant_id IS NOT NULL - AND entity_id IS NOT NULL - AND customer_id IS NOT NULL - AND type IS NOT NULL - AND search_text IS NOT NULL - AND id IS NOT NULL - PRIMARY KEY (tenant_id, search_text, id, customer_id, entity_id, type) - WITH CLUSTERING ORDER BY (search_text ASC, id DESC, customer_id DESC); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_by_type_and_search_text AS - SELECT * - from thingsboard.entity_view - WHERE tenant_id IS NOT NULL - AND entity_id IS NOT NULL - AND customer_id IS NOT NULL - AND type IS NOT NULL - AND search_text IS NOT NULL - AND id IS NOT NULL - PRIMARY KEY (tenant_id, type, search_text, id, customer_id, entity_id) - WITH CLUSTERING ORDER BY (type ASC, search_text ASC, id DESC, customer_id DESC); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_customer AS - SELECT * - from thingsboard.entity_view - WHERE tenant_id IS NOT NULL - AND customer_id IS NOT NULL - AND entity_id IS NOT NULL - AND type IS NOT NULL - AND search_text IS NOT NULL - AND id IS NOT NULL - PRIMARY KEY (tenant_id, customer_id, search_text, id, entity_id, type) - WITH CLUSTERING ORDER BY (customer_id DESC, search_text ASC, id DESC); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_customer_and_type AS - SELECT * - from thingsboard.entity_view - WHERE tenant_id IS NOT NULL - AND customer_id IS NOT NULL - AND entity_id IS NOT NULL - AND type IS NOT NULL - AND search_text IS NOT NULL - AND id IS NOT NULL - PRIMARY KEY (tenant_id, type, customer_id, search_text, id, entity_id) - WITH CLUSTERING ORDER BY (type ASC, customer_id DESC, search_text ASC, id DESC); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_entity_id AS - SELECT * - from thingsboard.entity_view - WHERE tenant_id IS NOT NULL - AND customer_id IS NOT NULL - AND entity_id IS NOT NULL - AND type IS NOT NULL - AND search_text IS NOT NULL - AND id IS NOT NULL - PRIMARY KEY (tenant_id, entity_id, customer_id, search_text, id, type) - WITH CLUSTERING ORDER BY (entity_id DESC, customer_id DESC, search_text ASC, id DESC); diff --git a/dao/src/main/resources/cassandra/schema-ts-latest.cql b/dao/src/main/resources/cassandra/schema-ts-latest.cql new file mode 100644 index 0000000000..623f530888 --- /dev/null +++ b/dao/src/main/resources/cassandra/schema-ts-latest.cql @@ -0,0 +1,34 @@ +-- +-- Copyright © 2016-2020 The Thingsboard Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +CREATE KEYSPACE IF NOT EXISTS thingsboard +WITH replication = { + 'class' : 'SimpleStrategy', + 'replication_factor' : 1 +}; + +CREATE TABLE IF NOT EXISTS thingsboard.ts_kv_latest_cf ( + entity_type text, // (DEVICE, CUSTOMER, TENANT) + entity_id timeuuid, + key text, + ts bigint, + bool_v boolean, + str_v text, + long_v bigint, + dbl_v double, + json_v text, + PRIMARY KEY (( entity_type, entity_id ), key) +) WITH compaction = { 'class' : 'LeveledCompactionStrategy' }; diff --git a/dao/src/main/resources/cassandra/schema-ts.cql b/dao/src/main/resources/cassandra/schema-ts.cql index c0f4b74467..957c6ee6eb 100644 --- a/dao/src/main/resources/cassandra/schema-ts.cql +++ b/dao/src/main/resources/cassandra/schema-ts.cql @@ -42,16 +42,3 @@ CREATE TABLE IF NOT EXISTS thingsboard.ts_kv_partitions_cf ( PRIMARY KEY (( entity_type, entity_id, key ), partition) ) WITH CLUSTERING ORDER BY ( partition ASC ) AND compaction = { 'class' : 'LeveledCompactionStrategy' }; - -CREATE TABLE IF NOT EXISTS thingsboard.ts_kv_latest_cf ( - entity_type text, // (DEVICE, CUSTOMER, TENANT) - entity_id timeuuid, - key text, - ts bigint, - bool_v boolean, - str_v text, - long_v bigint, - dbl_v double, - json_v text, - PRIMARY KEY (( entity_type, entity_id ), key) -) WITH compaction = { 'class' : 'LeveledCompactionStrategy' }; diff --git a/dao/src/main/resources/cassandra/system-data.cql b/dao/src/main/resources/cassandra/system-data.cql deleted file mode 100644 index 96446449f3..0000000000 --- a/dao/src/main/resources/cassandra/system-data.cql +++ /dev/null @@ -1,45 +0,0 @@ --- --- Copyright © 2016-2020 The Thingsboard Authors --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. --- - -/** SYSTEM **/ - -/** System admin **/ -INSERT INTO thingsboard.user ( id, tenant_id, customer_id, email, search_text, authority ) -VALUES ( minTimeuuid ( '2016-11-01 01:01:01+0000' ), minTimeuuid ( 0 ), minTimeuuid ( 0 ), 'sysadmin@thingsboard.org', -'sysadmin@thingsboard.org', 'SYS_ADMIN' ); - -INSERT INTO thingsboard.user_credentials ( id, user_id, enabled, password ) -VALUES ( now ( ), minTimeuuid ( '2016-11-01 01:01:01+0000' ), true, -'$2a$10$5JTB8/hxWc9WAy62nCGSxeefl3KWmipA9nFpVdDa0/xfIseeBB4Bu' ); - -/** System settings **/ -INSERT INTO thingsboard.admin_settings ( id, key, json_value ) -VALUES ( now ( ), 'general', '{ - "baseUrl": "http://localhost:8080" -}' ); - -INSERT INTO thingsboard.admin_settings ( id, key, json_value ) -VALUES ( now ( ), 'mail', '{ - "mailFrom": "Thingsboard ", - "smtpProtocol": "smtp", - "smtpHost": "localhost", - "smtpPort": "25", - "timeout": "10000", - "enableTls": false, - "tlsVersion": "TLSv1.2", - "username": "", - "password": "" -}' ); \ No newline at end of file diff --git a/dao/src/main/resources/sql/schema-entities-hsql.sql b/dao/src/main/resources/sql/schema-entities-hsql.sql index 697e34930a..0e8f8a7791 100644 --- a/dao/src/main/resources/sql/schema-entities-hsql.sql +++ b/dao/src/main/resources/sql/schema-entities-hsql.sql @@ -14,50 +14,53 @@ -- limitations under the License. -- - CREATE TABLE IF NOT EXISTS admin_settings ( - id varchar(31) NOT NULL CONSTRAINT admin_settings_pkey PRIMARY KEY, + id uuid NOT NULL CONSTRAINT admin_settings_pkey PRIMARY KEY, + created_time bigint NOT NULL, json_value varchar, key varchar(255) ); CREATE TABLE IF NOT EXISTS alarm ( - id varchar(31) NOT NULL CONSTRAINT alarm_pkey PRIMARY KEY, + id uuid NOT NULL CONSTRAINT alarm_pkey PRIMARY KEY, + created_time bigint NOT NULL, ack_ts bigint, clear_ts bigint, additional_info varchar, end_ts bigint, - originator_id varchar(31), + originator_id uuid, originator_type integer, propagate boolean, severity varchar(255), start_ts bigint, status varchar(255), - tenant_id varchar(31), + tenant_id uuid, propagate_relation_types varchar, type varchar(255) ); CREATE TABLE IF NOT EXISTS asset ( - id varchar(31) NOT NULL CONSTRAINT asset_pkey PRIMARY KEY, + id uuid NOT NULL CONSTRAINT asset_pkey PRIMARY KEY, + created_time bigint NOT NULL, additional_info varchar, - customer_id varchar(31), + customer_id uuid, name varchar(255), label varchar(255), search_text varchar(255), - tenant_id varchar(31), + tenant_id uuid, type varchar(255), CONSTRAINT asset_name_unq_key UNIQUE (tenant_id, name) ); CREATE TABLE IF NOT EXISTS audit_log ( - id varchar(31) NOT NULL CONSTRAINT audit_log_pkey PRIMARY KEY, - tenant_id varchar(31), - customer_id varchar(31), - entity_id varchar(31), + id uuid NOT NULL CONSTRAINT audit_log_pkey PRIMARY KEY, + created_time bigint NOT NULL, + tenant_id uuid, + customer_id uuid, + entity_id uuid, entity_type varchar(255), entity_name varchar(255), - user_id varchar(31), + user_id uuid, user_name varchar(255), action_type varchar(255), action_data varchar(1000000), @@ -67,7 +70,7 @@ CREATE TABLE IF NOT EXISTS audit_log ( CREATE TABLE IF NOT EXISTS attribute_kv ( entity_type varchar(255), - entity_id varchar(31), + entity_id uuid, attribute_type varchar(255), attribute_key varchar(255), bool_v boolean, @@ -80,7 +83,8 @@ CREATE TABLE IF NOT EXISTS attribute_kv ( ); CREATE TABLE IF NOT EXISTS component_descriptor ( - id varchar(31) NOT NULL CONSTRAINT component_descriptor_pkey PRIMARY KEY, + id uuid NOT NULL CONSTRAINT component_descriptor_pkey PRIMARY KEY, + created_time bigint NOT NULL, actions varchar(255), clazz varchar UNIQUE, configuration_descriptor varchar, @@ -91,7 +95,8 @@ CREATE TABLE IF NOT EXISTS component_descriptor ( ); CREATE TABLE IF NOT EXISTS customer ( - id varchar(31) NOT NULL CONSTRAINT customer_pkey PRIMARY KEY, + id uuid NOT NULL CONSTRAINT customer_pkey PRIMARY KEY, + created_time bigint NOT NULL, additional_info varchar, address varchar, address2 varchar, @@ -101,57 +106,121 @@ CREATE TABLE IF NOT EXISTS customer ( phone varchar(255), search_text varchar(255), state varchar(255), - tenant_id varchar(31), + tenant_id uuid, title varchar(255), zip varchar(255) ); CREATE TABLE IF NOT EXISTS dashboard ( - id varchar(31) NOT NULL CONSTRAINT dashboard_pkey PRIMARY KEY, + id uuid NOT NULL CONSTRAINT dashboard_pkey PRIMARY KEY, + created_time bigint NOT NULL, configuration varchar(10000000), assigned_customers varchar(1000000), search_text varchar(255), - tenant_id varchar(31), + tenant_id uuid, title varchar(255) ); +CREATE TABLE IF NOT EXISTS rule_chain ( + id uuid NOT NULL CONSTRAINT rule_chain_pkey PRIMARY KEY, + created_time bigint NOT NULL, + additional_info varchar, + configuration varchar(10000000), + name varchar(255), + first_rule_node_id uuid, + root boolean, + debug_mode boolean, + search_text varchar(255), + tenant_id uuid +); + +CREATE TABLE IF NOT EXISTS rule_node ( + id uuid NOT NULL CONSTRAINT rule_node_pkey PRIMARY KEY, + created_time bigint NOT NULL, + rule_chain_id uuid, + additional_info varchar, + configuration varchar(10000000), + type varchar(255), + name varchar(255), + debug_mode boolean, + search_text varchar(255) +); + +CREATE TABLE IF NOT EXISTS rule_node_state ( + id uuid NOT NULL CONSTRAINT rule_node_state_pkey PRIMARY KEY, + created_time bigint NOT NULL, + rule_node_id uuid NOT NULL, + entity_type varchar(32) NOT NULL, + entity_id uuid NOT NULL, + state_data varchar(16384) NOT NULL, + CONSTRAINT rule_node_state_unq_key UNIQUE (rule_node_id, entity_id), + CONSTRAINT fk_rule_node_state_node_id FOREIGN KEY (rule_node_id) REFERENCES rule_node(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS device_profile ( + id uuid NOT NULL CONSTRAINT device_profile_pkey PRIMARY KEY, + created_time bigint NOT NULL, + name varchar(255), + type varchar(255), + transport_type varchar(255), + provision_type varchar(255), + profile_data jsonb, + description varchar, + search_text varchar(255), + is_default boolean, + tenant_id uuid, + default_rule_chain_id uuid, + default_queue_name varchar(255), + provision_device_key varchar, + CONSTRAINT device_profile_name_unq_key UNIQUE (tenant_id, name), + CONSTRAINT device_provision_key_unq_key UNIQUE (provision_device_key), + CONSTRAINT fk_default_rule_chain_device_profile FOREIGN KEY (default_rule_chain_id) REFERENCES rule_chain(id) +); + CREATE TABLE IF NOT EXISTS device ( - id varchar(31) NOT NULL CONSTRAINT device_pkey PRIMARY KEY, + id uuid NOT NULL CONSTRAINT device_pkey PRIMARY KEY, + created_time bigint NOT NULL, additional_info varchar, - customer_id varchar(31), + customer_id uuid, + device_profile_id uuid NOT NULL, + device_data jsonb, type varchar(255), name varchar(255), label varchar(255), search_text varchar(255), - tenant_id varchar(31), - CONSTRAINT device_name_unq_key UNIQUE (tenant_id, name) + tenant_id uuid, + CONSTRAINT device_name_unq_key UNIQUE (tenant_id, name), + CONSTRAINT fk_device_profile FOREIGN KEY (device_profile_id) REFERENCES device_profile(id) ); CREATE TABLE IF NOT EXISTS device_credentials ( - id varchar(31) NOT NULL CONSTRAINT device_credentials_pkey PRIMARY KEY, + id uuid NOT NULL CONSTRAINT device_credentials_pkey PRIMARY KEY, + created_time bigint NOT NULL, credentials_id varchar, credentials_type varchar(255), credentials_value varchar, - device_id varchar(31), - CONSTRAINT device_credentials_id_unq_key UNIQUE (credentials_id) + device_id uuid, + CONSTRAINT device_credentials_id_unq_key UNIQUE (credentials_id), + CONSTRAINT device_credentials_device_id_unq_key UNIQUE (device_id) ); CREATE TABLE IF NOT EXISTS event ( - id varchar(31) NOT NULL CONSTRAINT event_pkey PRIMARY KEY, + id uuid NOT NULL CONSTRAINT event_pkey PRIMARY KEY, + created_time bigint NOT NULL, body varchar(10000000), - entity_id varchar(31), + entity_id uuid, entity_type varchar(255), event_type varchar(255), event_uid varchar(255), - tenant_id varchar(31), + tenant_id uuid, ts bigint NOT NULL, CONSTRAINT event_unq_key UNIQUE (tenant_id, entity_type, entity_id, event_type, event_uid) ); CREATE TABLE IF NOT EXISTS relation ( - from_id varchar(31), + from_id uuid, from_type varchar(255), - to_id varchar(31), + to_id uuid, to_type varchar(255), relation_type_group varchar(255), relation_type varchar(255), @@ -160,20 +229,36 @@ CREATE TABLE IF NOT EXISTS relation ( ); CREATE TABLE IF NOT EXISTS tb_user ( - id varchar(31) NOT NULL CONSTRAINT tb_user_pkey PRIMARY KEY, + id uuid NOT NULL CONSTRAINT tb_user_pkey PRIMARY KEY, + created_time bigint NOT NULL, additional_info varchar, authority varchar(255), - customer_id varchar(31), + customer_id uuid, email varchar(255) UNIQUE, first_name varchar(255), last_name varchar(255), search_text varchar(255), - tenant_id varchar(31) + tenant_id uuid +); + +CREATE TABLE IF NOT EXISTS tenant_profile ( + id uuid NOT NULL CONSTRAINT tenant_profile_pkey PRIMARY KEY, + created_time bigint NOT NULL, + name varchar(255), + profile_data jsonb, + description varchar, + search_text varchar(255), + is_default boolean, + isolated_tb_core boolean, + isolated_tb_rule_engine boolean, + CONSTRAINT tenant_profile_name_unq_key UNIQUE (name) ); CREATE TABLE IF NOT EXISTS tenant ( - id varchar(31) NOT NULL CONSTRAINT tenant_pkey PRIMARY KEY, + id uuid NOT NULL CONSTRAINT tenant_pkey PRIMARY KEY, + created_time bigint NOT NULL, additional_info varchar, + tenant_profile_id uuid NOT NULL, address varchar, address2 varchar, city varchar(255), @@ -185,65 +270,45 @@ CREATE TABLE IF NOT EXISTS tenant ( state varchar(255), title varchar(255), zip varchar(255), - isolated_tb_core boolean, - isolated_tb_rule_engine boolean + CONSTRAINT fk_tenant_profile FOREIGN KEY (tenant_profile_id) REFERENCES tenant_profile(id) ); CREATE TABLE IF NOT EXISTS user_credentials ( - id varchar(31) NOT NULL CONSTRAINT user_credentials_pkey PRIMARY KEY, + id uuid NOT NULL CONSTRAINT user_credentials_pkey PRIMARY KEY, + created_time bigint NOT NULL, activate_token varchar(255) UNIQUE, enabled boolean, password varchar(255), reset_token varchar(255) UNIQUE, - user_id varchar(31) UNIQUE + user_id uuid UNIQUE ); CREATE TABLE IF NOT EXISTS widget_type ( - id varchar(31) NOT NULL CONSTRAINT widget_type_pkey PRIMARY KEY, + id uuid NOT NULL CONSTRAINT widget_type_pkey PRIMARY KEY, + created_time bigint NOT NULL, alias varchar(255), bundle_alias varchar(255), descriptor varchar(1000000), name varchar(255), - tenant_id varchar(31) + tenant_id uuid ); CREATE TABLE IF NOT EXISTS widgets_bundle ( - id varchar(31) NOT NULL CONSTRAINT widgets_bundle_pkey PRIMARY KEY, + id uuid NOT NULL CONSTRAINT widgets_bundle_pkey PRIMARY KEY, + created_time bigint NOT NULL, alias varchar(255), search_text varchar(255), - tenant_id varchar(31), + tenant_id uuid, title varchar(255) ); -CREATE TABLE IF NOT EXISTS rule_chain ( - id varchar(31) NOT NULL CONSTRAINT rule_chain_pkey PRIMARY KEY, - additional_info varchar, - configuration varchar(10000000), - name varchar(255), - first_rule_node_id varchar(31), - root boolean, - debug_mode boolean, - search_text varchar(255), - tenant_id varchar(31) -); - -CREATE TABLE IF NOT EXISTS rule_node ( - id varchar(31) NOT NULL CONSTRAINT rule_node_pkey PRIMARY KEY, - rule_chain_id varchar(31), - additional_info varchar, - configuration varchar(10000000), - type varchar(255), - name varchar(255), - debug_mode boolean, - search_text varchar(255) -); - CREATE TABLE IF NOT EXISTS entity_view ( - id varchar(31) NOT NULL CONSTRAINT entity_view_pkey PRIMARY KEY, - entity_id varchar(31), + id uuid NOT NULL CONSTRAINT entity_view_pkey PRIMARY KEY, + created_time bigint NOT NULL, + entity_id uuid, entity_type varchar(255), - tenant_id varchar(31), - customer_id varchar(31), + tenant_id uuid, + customer_id uuid, type varchar(255), name varchar(255), keys varchar(10000000), @@ -253,3 +318,103 @@ CREATE TABLE IF NOT EXISTS entity_view ( additional_info varchar ); +CREATE TABLE IF NOT EXISTS ts_kv_latest ( + entity_id uuid NOT NULL, + key int NOT NULL, + ts bigint NOT NULL, + bool_v boolean, + str_v varchar(10000000), + long_v bigint, + dbl_v double precision, + json_v varchar(10000000), + CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key) +); + +CREATE TABLE IF NOT EXISTS ts_kv_dictionary ( + key varchar(255) NOT NULL, + key_id int GENERATED BY DEFAULT AS IDENTITY(start with 0 increment by 1) UNIQUE, + CONSTRAINT ts_key_id_pkey PRIMARY KEY (key) +); + + +CREATE TABLE IF NOT EXISTS oauth2_client_registration_info ( + id uuid NOT NULL CONSTRAINT oauth2_client_registration_info_pkey PRIMARY KEY, + enabled boolean, + created_time bigint NOT NULL, + additional_info varchar, + client_id varchar(255), + client_secret varchar(255), + authorization_uri varchar(255), + token_uri varchar(255), + scope varchar(255), + user_info_uri varchar(255), + user_name_attribute_name varchar(255), + jwk_set_uri varchar(255), + client_authentication_method varchar(255), + login_button_label varchar(255), + login_button_icon varchar(255), + allow_user_creation boolean, + activate_user boolean, + type varchar(31), + basic_email_attribute_key varchar(31), + basic_first_name_attribute_key varchar(31), + basic_last_name_attribute_key varchar(31), + basic_tenant_name_strategy varchar(31), + basic_tenant_name_pattern varchar(255), + basic_customer_name_pattern varchar(255), + basic_default_dashboard_name varchar(255), + basic_always_full_screen boolean, + custom_url varchar(255), + custom_username varchar(255), + custom_password varchar(255), + custom_send_token boolean +); + +CREATE TABLE IF NOT EXISTS oauth2_client_registration ( + id uuid NOT NULL CONSTRAINT oauth2_client_registration_pkey PRIMARY KEY, + created_time bigint NOT NULL, + domain_name varchar(255), + domain_scheme varchar(31), + client_registration_info_id uuid +); + +CREATE TABLE IF NOT EXISTS oauth2_client_registration_template ( + id uuid NOT NULL CONSTRAINT oauth2_client_registration_template_pkey PRIMARY KEY, + created_time bigint NOT NULL, + additional_info varchar, + provider_id varchar(255), + authorization_uri varchar(255), + token_uri varchar(255), + scope varchar(255), + user_info_uri varchar(255), + user_name_attribute_name varchar(255), + jwk_set_uri varchar(255), + client_authentication_method varchar(255), + type varchar(31), + basic_email_attribute_key varchar(31), + basic_first_name_attribute_key varchar(31), + basic_last_name_attribute_key varchar(31), + basic_tenant_name_strategy varchar(31), + basic_tenant_name_pattern varchar(255), + basic_customer_name_pattern varchar(255), + basic_default_dashboard_name varchar(255), + basic_always_full_screen boolean, + comment varchar, + login_button_icon varchar(255), + login_button_label varchar(255), + help_link varchar(255), + CONSTRAINT oauth2_template_provider_id_unq_key UNIQUE (provider_id) +); + +CREATE TABLE IF NOT EXISTS api_usage_state ( + id uuid NOT NULL CONSTRAINT usage_record_pkey PRIMARY KEY, + created_time bigint NOT NULL, + tenant_id uuid, + entity_type varchar(32), + entity_id uuid, + transport varchar(32), + db_storage varchar(32), + re_exec varchar(32), + js_exec varchar(32), + CONSTRAINT api_usage_state_unq_key UNIQUE (tenant_id, entity_id) +); diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql index 5b6df2080d..97134a8e19 100644 --- a/dao/src/main/resources/sql/schema-entities-idx.sql +++ b/dao/src/main/resources/sql/schema-entities-idx.sql @@ -16,6 +16,12 @@ CREATE INDEX IF NOT EXISTS idx_alarm_originator_alarm_type ON alarm(originator_id, type, start_ts DESC); +CREATE INDEX IF NOT EXISTS idx_alarm_originator_created_time ON alarm(originator_id, created_time DESC); + +CREATE INDEX IF NOT EXISTS idx_alarm_tenant_created_time ON alarm(tenant_id, created_time DESC); + +CREATE INDEX IF NOT EXISTS idx_alarm_tenant_alarm_type_created_time ON alarm(tenant_id, type, created_time DESC); + CREATE INDEX IF NOT EXISTS idx_event_type_entity_id ON event(tenant_id, event_type, entity_type, entity_id); CREATE INDEX IF NOT EXISTS idx_relation_to_id ON relation(relation_type_group, to_type, to_id); @@ -32,4 +38,6 @@ CREATE INDEX IF NOT EXISTS idx_asset_customer_id ON asset(tenant_id, customer_id CREATE INDEX IF NOT EXISTS idx_asset_customer_id_and_type ON asset(tenant_id, customer_id, type); -CREATE INDEX IF NOT EXISTS idx_asset_type ON asset(tenant_id, type); \ No newline at end of file +CREATE INDEX IF NOT EXISTS idx_asset_type ON asset(tenant_id, type); + +CREATE INDEX IF NOT EXISTS idx_attribute_kv_by_key_and_last_update_ts ON attribute_kv(entity_id, attribute_key, last_update_ts desc); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 931856e1df..db0362a877 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -14,50 +14,71 @@ -- limitations under the License. -- +CREATE TABLE IF NOT EXISTS tb_schema_settings +( + schema_version bigint NOT NULL, + CONSTRAINT tb_schema_settings_pkey PRIMARY KEY (schema_version) +); + +CREATE OR REPLACE PROCEDURE insert_tb_schema_settings() + LANGUAGE plpgsql AS +$$ +BEGIN + IF (SELECT COUNT(*) FROM tb_schema_settings) = 0 THEN + INSERT INTO tb_schema_settings (schema_version) VALUES (3002000); + END IF; +END; +$$; + +call insert_tb_schema_settings(); CREATE TABLE IF NOT EXISTS admin_settings ( - id varchar(31) NOT NULL CONSTRAINT admin_settings_pkey PRIMARY KEY, + id uuid NOT NULL CONSTRAINT admin_settings_pkey PRIMARY KEY, + created_time bigint NOT NULL, json_value varchar, key varchar(255) ); CREATE TABLE IF NOT EXISTS alarm ( - id varchar(31) NOT NULL CONSTRAINT alarm_pkey PRIMARY KEY, + id uuid NOT NULL CONSTRAINT alarm_pkey PRIMARY KEY, + created_time bigint NOT NULL, ack_ts bigint, clear_ts bigint, additional_info varchar, end_ts bigint, - originator_id varchar(31), + originator_id uuid, originator_type integer, propagate boolean, severity varchar(255), start_ts bigint, status varchar(255), - tenant_id varchar(31), + tenant_id uuid, propagate_relation_types varchar, type varchar(255) ); CREATE TABLE IF NOT EXISTS asset ( - id varchar(31) NOT NULL CONSTRAINT asset_pkey PRIMARY KEY, + id uuid NOT NULL CONSTRAINT asset_pkey PRIMARY KEY, + created_time bigint NOT NULL, additional_info varchar, - customer_id varchar(31), + customer_id uuid, name varchar(255), label varchar(255), search_text varchar(255), - tenant_id varchar(31), + tenant_id uuid, type varchar(255), CONSTRAINT asset_name_unq_key UNIQUE (tenant_id, name) ); CREATE TABLE IF NOT EXISTS audit_log ( - id varchar(31) NOT NULL CONSTRAINT audit_log_pkey PRIMARY KEY, - tenant_id varchar(31), - customer_id varchar(31), - entity_id varchar(31), + id uuid NOT NULL CONSTRAINT audit_log_pkey PRIMARY KEY, + created_time bigint NOT NULL, + tenant_id uuid, + customer_id uuid, + entity_id uuid, entity_type varchar(255), entity_name varchar(255), - user_id varchar(31), + user_id uuid, user_name varchar(255), action_type varchar(255), action_data varchar(1000000), @@ -67,7 +88,7 @@ CREATE TABLE IF NOT EXISTS audit_log ( CREATE TABLE IF NOT EXISTS attribute_kv ( entity_type varchar(255), - entity_id varchar(31), + entity_id uuid, attribute_type varchar(255), attribute_key varchar(255), bool_v boolean, @@ -80,7 +101,8 @@ CREATE TABLE IF NOT EXISTS attribute_kv ( ); CREATE TABLE IF NOT EXISTS component_descriptor ( - id varchar(31) NOT NULL CONSTRAINT component_descriptor_pkey PRIMARY KEY, + id uuid NOT NULL CONSTRAINT component_descriptor_pkey PRIMARY KEY, + created_time bigint NOT NULL, actions varchar(255), clazz varchar UNIQUE, configuration_descriptor varchar, @@ -91,7 +113,8 @@ CREATE TABLE IF NOT EXISTS component_descriptor ( ); CREATE TABLE IF NOT EXISTS customer ( - id varchar(31) NOT NULL CONSTRAINT customer_pkey PRIMARY KEY, + id uuid NOT NULL CONSTRAINT customer_pkey PRIMARY KEY, + created_time bigint NOT NULL, additional_info varchar, address varchar, address2 varchar, @@ -101,79 +124,166 @@ CREATE TABLE IF NOT EXISTS customer ( phone varchar(255), search_text varchar(255), state varchar(255), - tenant_id varchar(31), + tenant_id uuid, title varchar(255), zip varchar(255) ); CREATE TABLE IF NOT EXISTS dashboard ( - id varchar(31) NOT NULL CONSTRAINT dashboard_pkey PRIMARY KEY, + id uuid NOT NULL CONSTRAINT dashboard_pkey PRIMARY KEY, + created_time bigint NOT NULL, configuration varchar(10000000), assigned_customers varchar(1000000), search_text varchar(255), - tenant_id varchar(31), + tenant_id uuid, title varchar(255) ); +CREATE TABLE IF NOT EXISTS rule_chain ( + id uuid NOT NULL CONSTRAINT rule_chain_pkey PRIMARY KEY, + created_time bigint NOT NULL, + additional_info varchar, + configuration varchar(10000000), + name varchar(255), + first_rule_node_id uuid, + root boolean, + debug_mode boolean, + search_text varchar(255), + tenant_id uuid +); + +CREATE TABLE IF NOT EXISTS rule_node ( + id uuid NOT NULL CONSTRAINT rule_node_pkey PRIMARY KEY, + created_time bigint NOT NULL, + rule_chain_id uuid, + additional_info varchar, + configuration varchar(10000000), + type varchar(255), + name varchar(255), + debug_mode boolean, + search_text varchar(255) +); + +CREATE TABLE IF NOT EXISTS rule_node_state ( + id uuid NOT NULL CONSTRAINT rule_node_state_pkey PRIMARY KEY, + created_time bigint NOT NULL, + rule_node_id uuid NOT NULL, + entity_type varchar(32) NOT NULL, + entity_id uuid NOT NULL, + state_data varchar(16384) NOT NULL, + CONSTRAINT rule_node_state_unq_key UNIQUE (rule_node_id, entity_id), + CONSTRAINT fk_rule_node_state_node_id FOREIGN KEY (rule_node_id) REFERENCES rule_node(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS device_profile ( + id uuid NOT NULL CONSTRAINT device_profile_pkey PRIMARY KEY, + created_time bigint NOT NULL, + name varchar(255), + type varchar(255), + transport_type varchar(255), + provision_type varchar(255), + profile_data jsonb, + description varchar, + search_text varchar(255), + is_default boolean, + tenant_id uuid, + default_rule_chain_id uuid, + default_queue_name varchar(255), + provision_device_key varchar, + CONSTRAINT device_profile_name_unq_key UNIQUE (tenant_id, name), + CONSTRAINT device_provision_key_unq_key UNIQUE (provision_device_key), + CONSTRAINT fk_default_rule_chain_device_profile FOREIGN KEY (default_rule_chain_id) REFERENCES rule_chain(id) +); + CREATE TABLE IF NOT EXISTS device ( - id varchar(31) NOT NULL CONSTRAINT device_pkey PRIMARY KEY, + id uuid NOT NULL CONSTRAINT device_pkey PRIMARY KEY, + created_time bigint NOT NULL, additional_info varchar, - customer_id varchar(31), + customer_id uuid, + device_profile_id uuid NOT NULL, + device_data jsonb, type varchar(255), name varchar(255), label varchar(255), search_text varchar(255), - tenant_id varchar(31), - CONSTRAINT device_name_unq_key UNIQUE (tenant_id, name) + tenant_id uuid, + CONSTRAINT device_name_unq_key UNIQUE (tenant_id, name), + CONSTRAINT fk_device_profile FOREIGN KEY (device_profile_id) REFERENCES device_profile(id) ); CREATE TABLE IF NOT EXISTS device_credentials ( - id varchar(31) NOT NULL CONSTRAINT device_credentials_pkey PRIMARY KEY, + id uuid NOT NULL CONSTRAINT device_credentials_pkey PRIMARY KEY, + created_time bigint NOT NULL, credentials_id varchar, credentials_type varchar(255), credentials_value varchar, - device_id varchar(31), - CONSTRAINT device_credentials_id_unq_key UNIQUE (credentials_id) + device_id uuid, + CONSTRAINT device_credentials_id_unq_key UNIQUE (credentials_id), + CONSTRAINT device_credentials_device_id_unq_key UNIQUE (device_id) ); CREATE TABLE IF NOT EXISTS event ( - id varchar(31) NOT NULL CONSTRAINT event_pkey PRIMARY KEY, + id uuid NOT NULL CONSTRAINT event_pkey PRIMARY KEY, + created_time bigint NOT NULL, body varchar(10000000), - entity_id varchar(31), + entity_id uuid, entity_type varchar(255), event_type varchar(255), event_uid varchar(255), - tenant_id varchar(31), + tenant_id uuid, ts bigint NOT NULL, CONSTRAINT event_unq_key UNIQUE (tenant_id, entity_type, entity_id, event_type, event_uid) ); CREATE TABLE IF NOT EXISTS relation ( - from_id varchar(31), + from_id uuid, from_type varchar(255), - to_id varchar(31), + to_id uuid, to_type varchar(255), relation_type_group varchar(255), relation_type varchar(255), additional_info varchar, CONSTRAINT relation_pkey PRIMARY KEY (from_id, from_type, relation_type_group, relation_type, to_id, to_type) ); +-- ) PARTITION BY LIST (relation_type_group); +-- +-- CREATE TABLE other_relations PARTITION OF relation DEFAULT; +-- CREATE TABLE common_relations PARTITION OF relation FOR VALUES IN ('COMMON'); +-- CREATE TABLE alarm_relations PARTITION OF relation FOR VALUES IN ('ALARM'); +-- CREATE TABLE dashboard_relations PARTITION OF relation FOR VALUES IN ('DASHBOARD'); +-- CREATE TABLE rule_relations PARTITION OF relation FOR VALUES IN ('RULE_CHAIN', 'RULE_NODE'); CREATE TABLE IF NOT EXISTS tb_user ( - id varchar(31) NOT NULL CONSTRAINT tb_user_pkey PRIMARY KEY, + id uuid NOT NULL CONSTRAINT tb_user_pkey PRIMARY KEY, + created_time bigint NOT NULL, additional_info varchar, authority varchar(255), - customer_id varchar(31), + customer_id uuid, email varchar(255) UNIQUE, first_name varchar(255), last_name varchar(255), search_text varchar(255), - tenant_id varchar(31) + tenant_id uuid +); + +CREATE TABLE IF NOT EXISTS tenant_profile ( + id uuid NOT NULL CONSTRAINT tenant_profile_pkey PRIMARY KEY, + created_time bigint NOT NULL, + name varchar(255), + profile_data jsonb, + description varchar, + search_text varchar(255), + is_default boolean, + isolated_tb_core boolean, + isolated_tb_rule_engine boolean, + CONSTRAINT tenant_profile_name_unq_key UNIQUE (name) ); CREATE TABLE IF NOT EXISTS tenant ( - id varchar(31) NOT NULL CONSTRAINT tenant_pkey PRIMARY KEY, + id uuid NOT NULL CONSTRAINT tenant_pkey PRIMARY KEY, + created_time bigint NOT NULL, additional_info varchar, + tenant_profile_id uuid NOT NULL, address varchar, address2 varchar, city varchar(255), @@ -185,65 +295,45 @@ CREATE TABLE IF NOT EXISTS tenant ( state varchar(255), title varchar(255), zip varchar(255), - isolated_tb_core boolean, - isolated_tb_rule_engine boolean + CONSTRAINT fk_tenant_profile FOREIGN KEY (tenant_profile_id) REFERENCES tenant_profile(id) ); CREATE TABLE IF NOT EXISTS user_credentials ( - id varchar(31) NOT NULL CONSTRAINT user_credentials_pkey PRIMARY KEY, + id uuid NOT NULL CONSTRAINT user_credentials_pkey PRIMARY KEY, + created_time bigint NOT NULL, activate_token varchar(255) UNIQUE, enabled boolean, password varchar(255), reset_token varchar(255) UNIQUE, - user_id varchar(31) UNIQUE + user_id uuid UNIQUE ); CREATE TABLE IF NOT EXISTS widget_type ( - id varchar(31) NOT NULL CONSTRAINT widget_type_pkey PRIMARY KEY, + id uuid NOT NULL CONSTRAINT widget_type_pkey PRIMARY KEY, + created_time bigint NOT NULL, alias varchar(255), bundle_alias varchar(255), descriptor varchar(1000000), name varchar(255), - tenant_id varchar(31) + tenant_id uuid ); CREATE TABLE IF NOT EXISTS widgets_bundle ( - id varchar(31) NOT NULL CONSTRAINT widgets_bundle_pkey PRIMARY KEY, + id uuid NOT NULL CONSTRAINT widgets_bundle_pkey PRIMARY KEY, + created_time bigint NOT NULL, alias varchar(255), search_text varchar(255), - tenant_id varchar(31), + tenant_id uuid, title varchar(255) ); -CREATE TABLE IF NOT EXISTS rule_chain ( - id varchar(31) NOT NULL CONSTRAINT rule_chain_pkey PRIMARY KEY, - additional_info varchar, - configuration varchar(10000000), - name varchar(255), - first_rule_node_id varchar(31), - root boolean, - debug_mode boolean, - search_text varchar(255), - tenant_id varchar(31) -); - -CREATE TABLE IF NOT EXISTS rule_node ( - id varchar(31) NOT NULL CONSTRAINT rule_node_pkey PRIMARY KEY, - rule_chain_id varchar(31), - additional_info varchar, - configuration varchar(10000000), - type varchar(255), - name varchar(255), - debug_mode boolean, - search_text varchar(255) -); - CREATE TABLE IF NOT EXISTS entity_view ( - id varchar(31) NOT NULL CONSTRAINT entity_view_pkey PRIMARY KEY, - entity_id varchar(31), + id uuid NOT NULL CONSTRAINT entity_view_pkey PRIMARY KEY, + created_time bigint NOT NULL, + entity_id uuid, entity_type varchar(255), - tenant_id varchar(31), - customer_id varchar(31), + tenant_id uuid, + customer_id uuid, type varchar(255), name varchar(255), keys varchar(10000000), @@ -253,6 +343,108 @@ CREATE TABLE IF NOT EXISTS entity_view ( additional_info varchar ); +CREATE TABLE IF NOT EXISTS ts_kv_latest +( + entity_id uuid NOT NULL, + key int NOT NULL, + ts bigint NOT NULL, + bool_v boolean, + str_v varchar(10000000), + long_v bigint, + dbl_v double precision, + json_v json, + CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key) +); + +CREATE TABLE IF NOT EXISTS ts_kv_dictionary +( + key varchar(255) NOT NULL, + key_id serial UNIQUE, + CONSTRAINT ts_key_id_pkey PRIMARY KEY (key) +); + +CREATE TABLE IF NOT EXISTS oauth2_client_registration_info ( + id uuid NOT NULL CONSTRAINT oauth2_client_registration_info_pkey PRIMARY KEY, + enabled boolean, + created_time bigint NOT NULL, + additional_info varchar, + client_id varchar(255), + client_secret varchar(255), + authorization_uri varchar(255), + token_uri varchar(255), + scope varchar(255), + user_info_uri varchar(255), + user_name_attribute_name varchar(255), + jwk_set_uri varchar(255), + client_authentication_method varchar(255), + login_button_label varchar(255), + login_button_icon varchar(255), + allow_user_creation boolean, + activate_user boolean, + type varchar(31), + basic_email_attribute_key varchar(31), + basic_first_name_attribute_key varchar(31), + basic_last_name_attribute_key varchar(31), + basic_tenant_name_strategy varchar(31), + basic_tenant_name_pattern varchar(255), + basic_customer_name_pattern varchar(255), + basic_default_dashboard_name varchar(255), + basic_always_full_screen boolean, + custom_url varchar(255), + custom_username varchar(255), + custom_password varchar(255), + custom_send_token boolean +); + +CREATE TABLE IF NOT EXISTS oauth2_client_registration ( + id uuid NOT NULL CONSTRAINT oauth2_client_registration_pkey PRIMARY KEY, + created_time bigint NOT NULL, + domain_name varchar(255), + domain_scheme varchar(31), + client_registration_info_id uuid +); + +CREATE TABLE IF NOT EXISTS oauth2_client_registration_template ( + id uuid NOT NULL CONSTRAINT oauth2_client_registration_template_pkey PRIMARY KEY, + created_time bigint NOT NULL, + additional_info varchar, + provider_id varchar(255), + authorization_uri varchar(255), + token_uri varchar(255), + scope varchar(255), + user_info_uri varchar(255), + user_name_attribute_name varchar(255), + jwk_set_uri varchar(255), + client_authentication_method varchar(255), + type varchar(31), + basic_email_attribute_key varchar(31), + basic_first_name_attribute_key varchar(31), + basic_last_name_attribute_key varchar(31), + basic_tenant_name_strategy varchar(31), + basic_tenant_name_pattern varchar(255), + basic_customer_name_pattern varchar(255), + basic_default_dashboard_name varchar(255), + basic_always_full_screen boolean, + comment varchar, + login_button_icon varchar(255), + login_button_label varchar(255), + help_link varchar(255), + CONSTRAINT oauth2_template_provider_id_unq_key UNIQUE (provider_id) +); + +CREATE TABLE IF NOT EXISTS api_usage_state ( + id uuid NOT NULL CONSTRAINT usage_record_pkey PRIMARY KEY, + created_time bigint NOT NULL, + tenant_id uuid, + entity_type varchar(32), + entity_id uuid, + transport varchar(32), + db_storage varchar(32), + re_exec varchar(32), + js_exec varchar(32), + CONSTRAINT api_usage_state_unq_key UNIQUE (tenant_id, entity_id) +); + CREATE OR REPLACE PROCEDURE cleanup_events_by_ttl(IN ttl bigint, IN debug_ttl bigint, INOUT deleted bigint) LANGUAGE plpgsql AS $$ @@ -277,3 +469,12 @@ BEGIN deleted := ttl_deleted_count + debug_ttl_deleted_count; END $$; + +CREATE OR REPLACE FUNCTION to_uuid(IN entity_id varchar, OUT uuid_id uuid) AS +$$ +BEGIN + uuid_id := substring(entity_id, 8, 8) || '-' || substring(entity_id, 4, 4) || '-1' || substring(entity_id, 1, 3) || + '-' || substring(entity_id, 16, 4) || '-' || substring(entity_id, 20, 12); +END; +$$ LANGUAGE plpgsql; + diff --git a/dao/src/main/resources/sql/schema-timescale.sql b/dao/src/main/resources/sql/schema-timescale.sql index 926f9ee1a6..a9816f4a7a 100644 --- a/dao/src/main/resources/sql/schema-timescale.sql +++ b/dao/src/main/resources/sql/schema-timescale.sql @@ -46,14 +46,6 @@ CREATE TABLE IF NOT EXISTS ts_kv_latest ( CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key) ); -CREATE TABLE IF NOT EXISTS tb_schema_settings -( - schema_version bigint NOT NULL, - CONSTRAINT tb_schema_settings_pkey PRIMARY KEY (schema_version) -); - -INSERT INTO tb_schema_settings (schema_version) VALUES (2005001) ON CONFLICT (schema_version) DO UPDATE SET schema_version = 2005001; - CREATE OR REPLACE FUNCTION to_uuid(IN entity_id varchar, OUT uuid_id uuid) AS $$ BEGIN @@ -62,37 +54,37 @@ BEGIN END; $$ LANGUAGE plpgsql; -CREATE OR REPLACE FUNCTION delete_device_records_from_ts_kv(tenant_id varchar, customer_id varchar, ttl bigint, +CREATE OR REPLACE FUNCTION delete_device_records_from_ts_kv(tenant_id uuid, customer_id uuid, ttl bigint, OUT deleted bigint) AS $$ BEGIN EXECUTE format( - 'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT to_uuid(device.id) as entity_id FROM device WHERE tenant_id = %L and customer_id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted', + 'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT device.id as entity_id FROM device WHERE tenant_id = %L and customer_id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted', tenant_id, customer_id, ttl) into deleted; END; $$ LANGUAGE plpgsql; -CREATE OR REPLACE FUNCTION delete_asset_records_from_ts_kv(tenant_id varchar, customer_id varchar, ttl bigint, +CREATE OR REPLACE FUNCTION delete_asset_records_from_ts_kv(tenant_id uuid, customer_id uuid, ttl bigint, OUT deleted bigint) AS $$ BEGIN EXECUTE format( - 'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT to_uuid(asset.id) as entity_id FROM asset WHERE tenant_id = %L and customer_id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted', + 'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT asset.id as entity_id FROM asset WHERE tenant_id = %L and customer_id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted', tenant_id, customer_id, ttl) into deleted; END; $$ LANGUAGE plpgsql; -CREATE OR REPLACE FUNCTION delete_customer_records_from_ts_kv(tenant_id varchar, customer_id varchar, ttl bigint, +CREATE OR REPLACE FUNCTION delete_customer_records_from_ts_kv(tenant_id uuid, customer_id uuid, ttl bigint, OUT deleted bigint) AS $$ BEGIN EXECUTE format( - 'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT to_uuid(customer.id) as entity_id FROM customer WHERE tenant_id = %L and id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted', + 'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT customer.id as entity_id FROM customer WHERE tenant_id = %L and id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted', tenant_id, customer_id, ttl) into deleted; END; $$ LANGUAGE plpgsql; -CREATE OR REPLACE PROCEDURE cleanup_timeseries_by_ttl(IN null_uuid varchar(31), +CREATE OR REPLACE PROCEDURE cleanup_timeseries_by_ttl(IN null_uuid uuid, IN system_ttl bigint, INOUT deleted bigint) LANGUAGE plpgsql AS $$ diff --git a/dao/src/main/resources/sql/schema-ts-hsql.sql b/dao/src/main/resources/sql/schema-ts-hsql.sql index eb053a7a84..eee09516fc 100644 --- a/dao/src/main/resources/sql/schema-ts-hsql.sql +++ b/dao/src/main/resources/sql/schema-ts-hsql.sql @@ -28,20 +28,13 @@ CREATE TABLE IF NOT EXISTS ts_kv ( CONSTRAINT ts_kv_pkey PRIMARY KEY (entity_id, key, ts) ); -CREATE TABLE IF NOT EXISTS ts_kv_latest ( - entity_id uuid NOT NULL, - key int NOT NULL, - ts bigint NOT NULL, - bool_v boolean, - str_v varchar(10000000), - long_v bigint, - dbl_v double precision, - json_v varchar(10000000), - CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key) -); - CREATE TABLE IF NOT EXISTS ts_kv_dictionary ( key varchar(255) NOT NULL, key_id int GENERATED BY DEFAULT AS IDENTITY(start with 0 increment by 1) UNIQUE, CONSTRAINT ts_key_id_pkey PRIMARY KEY (key) ); + +CREATE FUNCTION to_uuid(IN entity_id varchar) + RETURNS UUID + RETURN UUID(substring(entity_id, 8, 8) || '-' || substring(entity_id, 4, 4) || '-1' || substring(entity_id, 1, 3) || + '-' || substring(entity_id, 16, 4) || '-' || substring(entity_id, 20, 12)); diff --git a/dao/src/main/resources/sql/schema-ts-psql.sql b/dao/src/main/resources/sql/schema-ts-psql.sql index 80f111869d..48f74b17da 100644 --- a/dao/src/main/resources/sql/schema-ts-psql.sql +++ b/dao/src/main/resources/sql/schema-ts-psql.sql @@ -27,19 +27,6 @@ CREATE TABLE IF NOT EXISTS ts_kv CONSTRAINT ts_kv_pkey PRIMARY KEY (entity_id, key, ts) ) PARTITION BY RANGE (ts); -CREATE TABLE IF NOT EXISTS ts_kv_latest -( - entity_id uuid NOT NULL, - key int NOT NULL, - ts bigint NOT NULL, - bool_v boolean, - str_v varchar(10000000), - long_v bigint, - dbl_v double precision, - json_v json, - CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key) -); - CREATE TABLE IF NOT EXISTS ts_kv_dictionary ( key varchar(255) NOT NULL, @@ -47,14 +34,6 @@ CREATE TABLE IF NOT EXISTS ts_kv_dictionary CONSTRAINT ts_key_id_pkey PRIMARY KEY (key) ); -CREATE TABLE IF NOT EXISTS tb_schema_settings -( - schema_version bigint NOT NULL, - CONSTRAINT tb_schema_settings_pkey PRIMARY KEY (schema_version) -); - -INSERT INTO tb_schema_settings (schema_version) VALUES (2005001) ON CONFLICT (schema_version) DO UPDATE SET schema_version = 2005001; - CREATE OR REPLACE PROCEDURE drop_partitions_by_max_ttl(IN partition_type varchar, IN system_ttl bigint, INOUT deleted bigint) LANGUAGE plpgsql AS $$ @@ -172,45 +151,45 @@ BEGIN END; $$ LANGUAGE plpgsql; -CREATE OR REPLACE FUNCTION delete_device_records_from_ts_kv(tenant_id varchar, customer_id varchar, ttl bigint, +CREATE OR REPLACE FUNCTION delete_device_records_from_ts_kv(tenant_id uuid, customer_id uuid, ttl bigint, OUT deleted bigint) AS $$ BEGIN EXECUTE format( - 'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT to_uuid(device.id) as entity_id FROM device WHERE tenant_id = %L and customer_id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted', + 'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT device.id as entity_id FROM device WHERE tenant_id = %L and customer_id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted', tenant_id, customer_id, ttl) into deleted; END; $$ LANGUAGE plpgsql; -CREATE OR REPLACE FUNCTION delete_asset_records_from_ts_kv(tenant_id varchar, customer_id varchar, ttl bigint, +CREATE OR REPLACE FUNCTION delete_asset_records_from_ts_kv(tenant_id uuid, customer_id uuid, ttl bigint, OUT deleted bigint) AS $$ BEGIN EXECUTE format( - 'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT to_uuid(asset.id) as entity_id FROM asset WHERE tenant_id = %L and customer_id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted', + 'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT asset.id as entity_id FROM asset WHERE tenant_id = %L and customer_id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted', tenant_id, customer_id, ttl) into deleted; END; $$ LANGUAGE plpgsql; -CREATE OR REPLACE FUNCTION delete_customer_records_from_ts_kv(tenant_id varchar, customer_id varchar, ttl bigint, +CREATE OR REPLACE FUNCTION delete_customer_records_from_ts_kv(tenant_id uuid, customer_id uuid, ttl bigint, OUT deleted bigint) AS $$ BEGIN EXECUTE format( - 'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT to_uuid(customer.id) as entity_id FROM customer WHERE tenant_id = %L and id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted', + 'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT customer.id as entity_id FROM customer WHERE tenant_id = %L and id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted', tenant_id, customer_id, ttl) into deleted; END; $$ LANGUAGE plpgsql; -CREATE OR REPLACE PROCEDURE cleanup_timeseries_by_ttl(IN null_uuid varchar(31), +CREATE OR REPLACE PROCEDURE cleanup_timeseries_by_ttl(IN null_uuid uuid, IN system_ttl bigint, INOUT deleted bigint) LANGUAGE plpgsql AS $$ DECLARE tenant_cursor CURSOR FOR select tenant.id as tenant_id from tenant; - tenant_id_record varchar; - customer_id_record varchar; + tenant_id_record uuid; + customer_id_record uuid; tenant_ttl bigint; customer_ttl bigint; deleted_for_entities bigint; diff --git a/dao/src/main/resources/sql/schema-types-hsql.sql b/dao/src/main/resources/sql/schema-types-hsql.sql new file mode 100644 index 0000000000..74095c4ed9 --- /dev/null +++ b/dao/src/main/resources/sql/schema-types-hsql.sql @@ -0,0 +1,20 @@ +-- +-- Copyright © 2016-2020 The Thingsboard Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +DROP TYPE json IF EXISTS; +CREATE TYPE json AS varchar; +DROP TYPE jsonb IF EXISTS; +CREATE TYPE jsonb AS other; diff --git a/dao/src/main/resources/sql/system-data.sql b/dao/src/main/resources/sql/system-data.sql deleted file mode 100644 index f261eb2c04..0000000000 --- a/dao/src/main/resources/sql/system-data.sql +++ /dev/null @@ -1,45 +0,0 @@ --- --- Copyright © 2016-2020 The Thingsboard Authors --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. --- - -/** SYSTEM **/ - -/** System admin **/ -INSERT INTO tb_user ( id, tenant_id, customer_id, email, search_text, authority ) -VALUES ( '1e746125a797660a91992ebcb67fe33', '1b21dd2138140008080808080808080', '1b21dd2138140008080808080808080', 'sysadmin@thingsboard.org', - 'sysadmin@thingsboard.org', 'SYS_ADMIN' ); - -INSERT INTO user_credentials ( id, user_id, enabled, password ) -VALUES ( '1e7461261441950a91992ebcb67fe33', '1e746125a797660a91992ebcb67fe33', true, - '$2a$10$5JTB8/hxWc9WAy62nCGSxeefl3KWmipA9nFpVdDa0/xfIseeBB4Bu' ); - -/** System settings **/ -INSERT INTO admin_settings ( id, key, json_value ) -VALUES ( '1e746126a2266e4a91992ebcb67fe33', 'general', '{ - "baseUrl": "http://localhost:8080" -}' ); - -INSERT INTO admin_settings ( id, key, json_value ) -VALUES ( '1e746126eaaefa6a91992ebcb67fe33', 'mail', '{ - "mailFrom": "Thingsboard ", - "smtpProtocol": "smtp", - "smtpHost": "localhost", - "smtpPort": "25", - "timeout": "10000", - "enableTls": false, - "tlsVersion": "TLSv1.2", - "username": "", - "password": "" -}' ); diff --git a/dao/src/test/java/org/apache/cassandra/io/sstable/Descriptor.java b/dao/src/test/java/org/apache/cassandra/io/sstable/Descriptor.java new file mode 100644 index 0000000000..a5e6122537 --- /dev/null +++ b/dao/src/test/java/org/apache/cassandra/io/sstable/Descriptor.java @@ -0,0 +1,364 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.io.sstable; + +import java.io.File; +import java.io.IOError; +import java.io.IOException; +import java.util.*; +import java.util.regex.Pattern; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.CharMatcher; +import com.google.common.base.Objects; + +import org.apache.cassandra.db.Directories; +import org.apache.cassandra.io.sstable.format.SSTableFormat; +import org.apache.cassandra.io.sstable.format.Version; +import org.apache.cassandra.io.sstable.metadata.IMetadataSerializer; +import org.apache.cassandra.io.sstable.metadata.LegacyMetadataSerializer; +import org.apache.cassandra.io.sstable.metadata.MetadataSerializer; +import org.apache.cassandra.utils.Pair; + +import static org.apache.cassandra.io.sstable.Component.separator; + +/** + * A SSTable is described by the keyspace and column family it contains data + * for, a generation (where higher generations contain more recent data) and + * an alphabetic version string. + * + * A descriptor can be marked as temporary, which influences generated filenames. + */ +public class Descriptor +{ + public static String TMP_EXT = ".tmp"; + + /** canonicalized path to the directory where SSTable resides */ + public final File directory; + /** version has the following format: [a-z]+ */ + public final Version version; + public final String ksname; + public final String cfname; + public final int generation; + public final SSTableFormat.Type formatType; + /** digest component - might be {@code null} for old, legacy sstables */ + public final Component digestComponent; + private final int hashCode; + + /** + * A descriptor that assumes CURRENT_VERSION. + */ + @VisibleForTesting + public Descriptor(File directory, String ksname, String cfname, int generation) + { + this(SSTableFormat.Type.current().info.getLatestVersion(), directory, ksname, cfname, generation, SSTableFormat.Type.current(), null); + } + + /** + * Constructor for sstable writers only. + */ + public Descriptor(File directory, String ksname, String cfname, int generation, SSTableFormat.Type formatType) + { + this(formatType.info.getLatestVersion(), directory, ksname, cfname, generation, formatType, Component.digestFor(formatType.info.getLatestVersion().uncompressedChecksumType())); + } + + @VisibleForTesting + public Descriptor(String version, File directory, String ksname, String cfname, int generation, SSTableFormat.Type formatType) + { + this(formatType.info.getVersion(version), directory, ksname, cfname, generation, formatType, Component.digestFor(formatType.info.getLatestVersion().uncompressedChecksumType())); + } + + public Descriptor(Version version, File directory, String ksname, String cfname, int generation, SSTableFormat.Type formatType, Component digestComponent) + { + assert version != null && directory != null && ksname != null && cfname != null && formatType.info.getLatestVersion().getClass().equals(version.getClass()); + this.version = version; + try + { + this.directory = directory.getCanonicalFile(); + } + catch (IOException e) + { + throw new IOError(e); + } + this.ksname = ksname; + this.cfname = cfname; + this.generation = generation; + this.formatType = formatType; + this.digestComponent = digestComponent; + + hashCode = Objects.hashCode(version, this.directory, generation, ksname, cfname, formatType); + } + + public Descriptor withGeneration(int newGeneration) + { + return new Descriptor(version, directory, ksname, cfname, newGeneration, formatType, digestComponent); + } + + public Descriptor withFormatType(SSTableFormat.Type newType) + { + return new Descriptor(newType.info.getLatestVersion(), directory, ksname, cfname, generation, newType, digestComponent); + } + + public Descriptor withDigestComponent(Component newDigestComponent) + { + return new Descriptor(version, directory, ksname, cfname, generation, formatType, newDigestComponent); + } + + public String tmpFilenameFor(Component component) + { + return filenameFor(component) + TMP_EXT; + } + + public String filenameFor(Component component) + { + return baseFilename() + separator + component.name(); + } + + public String baseFilename() + { + StringBuilder buff = new StringBuilder(); + buff.append(directory).append(File.separatorChar); + appendFileName(buff); + return buff.toString(); + } + + private void appendFileName(StringBuilder buff) + { + if (!version.hasNewFileName()) + { + buff.append(ksname).append(separator); + buff.append(cfname).append(separator); + } + buff.append(version).append(separator); + buff.append(generation); + if (formatType != SSTableFormat.Type.LEGACY) + buff.append(separator).append(formatType.name); + } + + public String relativeFilenameFor(Component component) + { + final StringBuilder buff = new StringBuilder(); + appendFileName(buff); + buff.append(separator).append(component.name()); + return buff.toString(); + } + + public SSTableFormat getFormat() + { + return formatType.info; + } + + /** Return any temporary files found in the directory */ + public List getTemporaryFiles() + { + List ret = new ArrayList<>(); + File[] tmpFiles = directory.listFiles((dir, name) -> + name.endsWith(Descriptor.TMP_EXT)); + + for (File tmpFile : tmpFiles) + ret.add(tmpFile); + + return ret; + } + + /** + * Files obsoleted by CASSANDRA-7066 : temporary files and compactions_in_progress. We support + * versions 2.1 (ka) and 2.2 (la). + * Temporary files have tmp- or tmplink- at the beginning for 2.2 sstables or after ks-cf- for 2.1 sstables + */ + + private final static String LEGACY_COMP_IN_PROG_REGEX_STR = "^compactions_in_progress(\\-[\\d,a-f]{32})?$"; + private final static Pattern LEGACY_COMP_IN_PROG_REGEX = Pattern.compile(LEGACY_COMP_IN_PROG_REGEX_STR); + private final static String LEGACY_TMP_REGEX_STR = "^((.*)\\-(.*)\\-)?tmp(link)?\\-((?:l|k).)\\-(\\d)*\\-(.*)$"; + private final static Pattern LEGACY_TMP_REGEX = Pattern.compile(LEGACY_TMP_REGEX_STR); + + public static boolean isLegacyFile(File file) + { + if (file.isDirectory()) + return file.getParentFile() != null && + file.getParentFile().getName().equalsIgnoreCase("system") && + LEGACY_COMP_IN_PROG_REGEX.matcher(file.getName()).matches(); + else + return LEGACY_TMP_REGEX.matcher(file.getName()).matches(); + } + + public static boolean isValidFile(String fileName) + { + return fileName.endsWith(".db") && !LEGACY_TMP_REGEX.matcher(fileName).matches(); + } + + /** + * @see #fromFilename(File directory, String name) + * @param filename The SSTable filename + * @return Descriptor of the SSTable initialized from filename + */ + public static Descriptor fromFilename(String filename) + { + return fromFilename(filename, false); + } + + public static Descriptor fromFilename(String filename, SSTableFormat.Type formatType) + { + return fromFilename(filename).withFormatType(formatType); + } + + public static Descriptor fromFilename(String filename, boolean skipComponent) + { + File file = new File(filename).getAbsoluteFile(); + return fromFilename(file.getParentFile(), file.getName(), skipComponent).left; + } + + public static Pair fromFilename(File directory, String name) + { + return fromFilename(directory, name, false); + } + + /** + * Filename of the form is vary by version: + * + *
    + *
  • <ksname>-<cfname>-(tmp-)?<version>-<gen>-<component> for cassandra 2.0 and before
  • + *
  • (<tmp marker>-)?<version>-<gen>-<component> for cassandra 3.0 and later
  • + *
+ * + * If this is for SSTable of secondary index, directory should ends with index name for 2.1+. + * + * @param directory The directory of the SSTable files + * @param name The name of the SSTable file + * @param skipComponent true if the name param should not be parsed for a component tag + * + * @return A Descriptor for the SSTable, and the Component remainder. + */ + public static Pair fromFilename(File directory, String name, boolean skipComponent) + { + File parentDirectory = directory != null ? directory : new File("."); + + // tokenize the filename + StringTokenizer st = new StringTokenizer(name, String.valueOf(separator)); + String nexttok; + + // read tokens backwards to determine version + Deque tokenStack = new ArrayDeque<>(); + while (st.hasMoreTokens()) + { + tokenStack.push(st.nextToken()); + } + + // component suffix + String component = skipComponent ? null : tokenStack.pop(); + + nexttok = tokenStack.pop(); + // generation OR format type + SSTableFormat.Type fmt = SSTableFormat.Type.LEGACY; + if (!CharMatcher.digit().matchesAllOf(nexttok)) + { + fmt = SSTableFormat.Type.validate(nexttok); + nexttok = tokenStack.pop(); + } + + // generation + int generation = Integer.parseInt(nexttok); + + // version + nexttok = tokenStack.pop(); + + if (!Version.validate(nexttok)) + throw new UnsupportedOperationException("SSTable " + name + " is too old to open. Upgrade to 2.0 first, and run upgradesstables"); + + Version version = fmt.info.getVersion(nexttok); + + // ks/cf names + String ksname, cfname; + if (version.hasNewFileName()) + { + // for 2.1+ read ks and cf names from directory + File cfDirectory = parentDirectory; + // check if this is secondary index + String indexName = ""; + if (cfDirectory.getName().startsWith(Directories.SECONDARY_INDEX_NAME_SEPARATOR)) + { + indexName = cfDirectory.getName(); + cfDirectory = cfDirectory.getParentFile(); + } + if (cfDirectory.getName().equals(Directories.BACKUPS_SUBDIR)) + { + cfDirectory = cfDirectory.getParentFile(); + } + else if (cfDirectory.getParentFile().getName().equals(Directories.SNAPSHOT_SUBDIR)) + { + cfDirectory = cfDirectory.getParentFile().getParentFile(); + } + cfname = cfDirectory.getName().split("-")[0] + indexName; + ksname = cfDirectory.getParentFile().getName(); + } + else + { + cfname = tokenStack.pop(); + ksname = tokenStack.pop(); + } + assert tokenStack.isEmpty() : "Invalid file name " + name + " in " + directory; + + return Pair.create(new Descriptor(version, parentDirectory, ksname, cfname, generation, fmt, + // _assume_ version from version + Component.digestFor(version.uncompressedChecksumType())), + component); + } + + public IMetadataSerializer getMetadataSerializer() + { + if (version.hasNewStatsFile()) + return new MetadataSerializer(); + else + return new LegacyMetadataSerializer(); + } + + /** + * @return true if the current Cassandra version can read the given sstable version + */ + public boolean isCompatible() + { + return version.isCompatible(); + } + + @Override + public String toString() + { + return baseFilename(); + } + + @Override + public boolean equals(Object o) + { + if (o == this) + return true; + if (!(o instanceof Descriptor)) + return false; + Descriptor that = (Descriptor)o; + return that.directory.equals(this.directory) + && that.generation == this.generation + && that.ksname.equals(this.ksname) + && that.cfname.equals(this.cfname) + && that.formatType == this.formatType; + } + + @Override + public int hashCode() + { + return hashCode; + } +} diff --git a/dao/src/test/java/org/apache/cassandra/io/sstable/format/SSTableFormat.java b/dao/src/test/java/org/apache/cassandra/io/sstable/format/SSTableFormat.java new file mode 100644 index 0000000000..af6af442a3 --- /dev/null +++ b/dao/src/test/java/org/apache/cassandra/io/sstable/format/SSTableFormat.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.io.sstable.format; + +import com.google.common.base.CharMatcher; +import org.apache.cassandra.config.CFMetaData; +import org.apache.cassandra.db.RowIndexEntry; +import org.apache.cassandra.db.SerializationHeader; +import org.apache.cassandra.io.sstable.format.big.BigFormat; + +/** + * Provides the accessors to data on disk. + */ +public interface SSTableFormat +{ + static boolean enableSSTableDevelopmentTestMode = Boolean.getBoolean("cassandra.test.sstableformatdevelopment"); + + + Version getLatestVersion(); + Version getVersion(String version); + + SSTableWriter.Factory getWriterFactory(); + SSTableReader.Factory getReaderFactory(); + + RowIndexEntry.IndexSerializer getIndexSerializer(CFMetaData cfm, Version version, SerializationHeader header); + + public static enum Type + { + //Used internally to refer to files with no + //format flag in the filename + LEGACY("big", BigFormat.instance), + + //The original sstable format + BIG("big", BigFormat.instance); + + public final SSTableFormat info; + public final String name; + + public static Type current() + { + return BIG; + } + + private Type(String name, SSTableFormat info) + { + //Since format comes right after generation + //we disallow formats with numeric names + // We have removed this check for compatibility with the embedded cassandra used for tests. + assert !CharMatcher.digit().matchesAllOf(name); + + this.name = name; + this.info = info; + } + + public static Type validate(String name) + { + for (Type valid : Type.values()) + { + //This is used internally for old sstables + if (valid == LEGACY) + continue; + + if (valid.name.equalsIgnoreCase(name)) + return valid; + } + + throw new IllegalArgumentException("No Type constant " + name); + } + } +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/CustomCassandraCQLUnit.java b/dao/src/test/java/org/thingsboard/server/dao/CustomCassandraCQLUnit.java index 6a14117e8a..8b984bcd01 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/CustomCassandraCQLUnit.java +++ b/dao/src/test/java/org/thingsboard/server/dao/CustomCassandraCQLUnit.java @@ -15,8 +15,7 @@ */ package org.thingsboard.server.dao; -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.Session; +import com.datastax.oss.driver.api.core.CqlSession; import org.cassandraunit.BaseCassandraUnit; import org.cassandraunit.CQLDataLoader; import org.cassandraunit.dataset.CQLDataSet; @@ -27,8 +26,7 @@ import java.util.List; public class CustomCassandraCQLUnit extends BaseCassandraUnit { protected List dataSets; - public Session session; - public Cluster cluster; + public CqlSession session; public CustomCassandraCQLUnit(List dataSets) { this.dataSets = dataSets; @@ -65,33 +63,26 @@ public class CustomCassandraCQLUnit extends BaseCassandraUnit { @Override protected void load() { - String hostIp = EmbeddedCassandraServerHelper.getHost(); - int port = EmbeddedCassandraServerHelper.getNativeTransportPort(); - cluster = new Cluster.Builder().addContactPoints(hostIp).withPort(port).withSocketOptions(getSocketOptions()) - .build(); - session = cluster.connect(); + session = EmbeddedCassandraServerHelper.getSession(); CQLDataLoader dataLoader = new CQLDataLoader(session); dataSets.forEach(dataLoader::load); session = dataLoader.getSession(); + System.setSecurityManager(null); } @Override protected void after() { super.after(); - try (Cluster c = cluster; Session s = session) { + try (CqlSession s = session) { session = null; - cluster = null; } System.setSecurityManager(null); } // Getters for those who do not like to directly access fields - public Session getSession() { + public CqlSession getSession() { return session; } - public Cluster getCluster() { - return cluster; - } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/JpaDbunitTestConfig.java b/dao/src/test/java/org/thingsboard/server/dao/JpaDbunitTestConfig.java index 3dae53bb83..f7d45ff13d 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/JpaDbunitTestConfig.java +++ b/dao/src/test/java/org/thingsboard/server/dao/JpaDbunitTestConfig.java @@ -22,7 +22,6 @@ import org.dbunit.ext.hsqldb.HsqldbDataTypeFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.thingsboard.server.dao.util.SqlDao; import javax.sql.DataSource; import java.io.IOException; @@ -32,7 +31,6 @@ import java.sql.SQLException; * Created by Valerii Sosliuk on 5/6/2017. */ @Configuration -@SqlDao public class JpaDbunitTestConfig { @Autowired diff --git a/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java b/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java index f00c000505..1faa036f93 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java +++ b/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java @@ -29,14 +29,20 @@ import java.util.Arrays; }) public class NoSqlDaoServiceTestSuite { + @ClassRule + public static CustomSqlUnit sqlUnit = new CustomSqlUnit( + Arrays.asList("sql/schema-types-hsql.sql", "sql/schema-entities-hsql.sql", "sql/schema-entities-idx.sql", "sql/system-data.sql", "sql/system-test.sql"), + "sql/hsql/drop-all-tables.sql", + "nosql-test.properties" + ); + @ClassRule public static CustomCassandraCQLUnit cassandraUnit = new CustomCassandraCQLUnit( Arrays.asList( new ClassPathCQLDataSet("cassandra/schema-ts.cql", false, false), - new ClassPathCQLDataSet("cassandra/schema-entities.cql", false, false), - new ClassPathCQLDataSet("cassandra/system-data.cql", false, false), - new ClassPathCQLDataSet("cassandra/system-test.cql", false, false)), + new ClassPathCQLDataSet("cassandra/schema-ts-latest.cql", false, false) + ), "cassandra-test.yaml", 30000L); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/SqlDaoServiceTestSuite.java b/dao/src/test/java/org/thingsboard/server/dao/SqlDaoServiceTestSuite.java index 32fd45c188..06f1e70a20 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/SqlDaoServiceTestSuite.java +++ b/dao/src/test/java/org/thingsboard/server/dao/SqlDaoServiceTestSuite.java @@ -24,20 +24,26 @@ import java.util.Arrays; @RunWith(ClasspathSuite.class) @ClassnameFilters({ - "org.thingsboard.server.dao.service.*ServiceSqlTest" + "org.thingsboard.server.dao.service.sql.*SqlTest" }) public class SqlDaoServiceTestSuite { @ClassRule public static CustomSqlUnit sqlUnit = new CustomSqlUnit( - Arrays.asList("sql/schema-ts-hsql.sql", "sql/schema-entities-hsql.sql", "sql/schema-entities-idx.sql", "sql/system-data.sql", "sql/system-test.sql"), + Arrays.asList("sql/schema-types-hsql.sql", "sql/schema-ts-hsql.sql", "sql/schema-entities-hsql.sql", "sql/schema-entities-idx.sql" + , "sql/system-data.sql" + , "sql/system-test.sql" + ), "sql/hsql/drop-all-tables.sql", "sql-test.properties" ); // @ClassRule // public static CustomSqlUnit sqlUnit = new CustomSqlUnit( -// Arrays.asList("sql/schema-ts-psql.sql", "sql/schema-entities.sql", "sql/schema-entities-idx.sql", "sql/system-data.sql", "sql/system-test.sql"), +// Arrays.asList("sql/schema-ts-psql.sql" +// , "sql/schema-entities.sql", "sql/schema-entities-idx.sql" +// , "sql/system-data.sql", "sql/system-test.sql" +// ), // "sql/psql/drop-all-tables.sql", // "sql-test.properties" // ); diff --git a/dao/src/test/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFutureTest.java b/dao/src/test/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFutureTest.java deleted file mode 100644 index bb4a08a8f1..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFutureTest.java +++ /dev/null @@ -1,195 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.nosql; - -import com.datastax.driver.core.ProtocolVersion; -import com.datastax.driver.core.ResultSet; -import com.datastax.driver.core.ResultSetFuture; -import com.datastax.driver.core.Row; -import com.datastax.driver.core.Session; -import com.datastax.driver.core.Statement; -import com.datastax.driver.core.exceptions.UnsupportedFeatureException; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.runners.MockitoJUnitRunner; -import org.mockito.stubbing.Answer; -import org.thingsboard.server.dao.exception.BufferLimitException; -import org.thingsboard.server.dao.util.AsyncRateLimiter; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeoutException; - -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; - -@RunWith(MockitoJUnitRunner.class) -public class RateLimitedResultSetFutureTest { - - private RateLimitedResultSetFuture resultSetFuture; - - @Mock - private AsyncRateLimiter rateLimiter; - @Mock - private Session session; - @Mock - private Statement statement; - @Mock - private ResultSetFuture realFuture; - @Mock - private ResultSet rows; - @Mock - private Row row; - - @Test - public void doNotReleasePermissionIfRateLimitFutureFailed() throws InterruptedException { - when(rateLimiter.acquireAsync()).thenReturn(Futures.immediateFailedFuture(new BufferLimitException())); - resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement); - Thread.sleep(1000L); - verify(rateLimiter).acquireAsync(); - try { - assertTrue(resultSetFuture.isDone()); - fail(); - } catch (Exception e) { - assertTrue(e instanceof IllegalStateException); - Throwable actualCause = e.getCause(); - assertTrue(actualCause instanceof ExecutionException); - } - verifyNoMoreInteractions(session, rateLimiter, statement); - - } - - @Test - public void getUninterruptiblyDelegateToCassandra() throws InterruptedException, ExecutionException { - when(rateLimiter.acquireAsync()).thenReturn(Futures.immediateFuture(null)); - when(session.executeAsync(statement)).thenReturn(realFuture); - Mockito.doAnswer((Answer) invocation -> { - Object[] args = invocation.getArguments(); - Runnable task = (Runnable) args[0]; - task.run(); - return null; - }).when(realFuture).addListener(Mockito.any(), Mockito.any()); - - when(realFuture.getUninterruptibly()).thenReturn(rows); - - resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement); - ResultSet actual = resultSetFuture.getUninterruptibly(); - assertSame(rows, actual); - verify(rateLimiter, times(1)).acquireAsync(); - verify(rateLimiter, times(1)).release(); - } - - @Test - public void addListenerAllowsFutureTransformation() throws InterruptedException, ExecutionException { - when(rateLimiter.acquireAsync()).thenReturn(Futures.immediateFuture(null)); - when(session.executeAsync(statement)).thenReturn(realFuture); - Mockito.doAnswer((Answer) invocation -> { - Object[] args = invocation.getArguments(); - Runnable task = (Runnable) args[0]; - task.run(); - return null; - }).when(realFuture).addListener(Mockito.any(), Mockito.any()); - - when(realFuture.get()).thenReturn(rows); - when(rows.one()).thenReturn(row); - - resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement); - - ListenableFuture transform = Futures.transform(resultSetFuture, ResultSet::one, MoreExecutors.directExecutor()); - Row actualRow = transform.get(); - - assertSame(row, actualRow); - verify(rateLimiter, times(1)).acquireAsync(); - verify(rateLimiter, times(1)).release(); - } - - @Test - public void immidiateCassandraExceptionReturnsPermit() throws InterruptedException, ExecutionException { - when(rateLimiter.acquireAsync()).thenReturn(Futures.immediateFuture(null)); - when(session.executeAsync(statement)).thenThrow(new UnsupportedFeatureException(ProtocolVersion.V3, "hjg")); - resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement); - ListenableFuture transform = Futures.transform(resultSetFuture, ResultSet::one, MoreExecutors.directExecutor()); - try { - transform.get(); - fail(); - } catch (Exception e) { - assertTrue(e instanceof ExecutionException); - } - verify(rateLimiter, times(1)).acquireAsync(); - verify(rateLimiter, times(1)).release(); - } - - @Test - public void queryTimeoutReturnsPermit() throws InterruptedException, ExecutionException { - when(rateLimiter.acquireAsync()).thenReturn(Futures.immediateFuture(null)); - when(session.executeAsync(statement)).thenReturn(realFuture); - Mockito.doAnswer((Answer) invocation -> { - Object[] args = invocation.getArguments(); - Runnable task = (Runnable) args[0]; - task.run(); - return null; - }).when(realFuture).addListener(Mockito.any(), Mockito.any()); - - when(realFuture.get()).thenThrow(new ExecutionException("Fail", new TimeoutException("timeout"))); - resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement); - ListenableFuture transform = Futures.transform(resultSetFuture, ResultSet::one, MoreExecutors.directExecutor()); - try { - transform.get(); - fail(); - } catch (Exception e) { - assertTrue(e instanceof ExecutionException); - } - verify(rateLimiter, times(1)).acquireAsync(); - verify(rateLimiter, times(1)).release(); - } - - @Test - public void expiredQueryReturnPermit() throws InterruptedException, ExecutionException { - CountDownLatch latch = new CountDownLatch(1); - ListenableFuture future = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(1)).submit(() -> { - latch.await(); - return null; - }); - when(rateLimiter.acquireAsync()).thenReturn(future); - resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement); - - ListenableFuture transform = Futures.transform(resultSetFuture, ResultSet::one, MoreExecutors.directExecutor()); -// TimeUnit.MILLISECONDS.sleep(200); - future.cancel(false); - latch.countDown(); - - try { - transform.get(); - fail(); - } catch (Exception e) { - assertTrue(e instanceof ExecutionException); - } - verify(rateLimiter, times(1)).acquireAsync(); - verify(rateLimiter, times(1)).release(); - } - -} \ No newline at end of file diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java index 343b2074fd..2a99e868a9 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.dao.service; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.runner.RunWith; @@ -27,15 +27,17 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.context.support.AnnotationConfigContextLoader; -import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Event; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UUIDBased; -import org.thingsboard.server.common.data.plugin.ComponentDescriptor; -import org.thingsboard.server.common.data.plugin.ComponentScope; -import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.audit.AuditLogLevelFilter; @@ -44,14 +46,18 @@ import org.thingsboard.server.dao.component.ComponentDescriptorService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.settings.AdminSettingsService; +import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; +import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; @@ -76,6 +82,9 @@ public abstract class AbstractServiceTest { @Autowired protected UserService userService; + @Autowired + protected ApiUsageStateService apiUsageStateService; + @Autowired protected AdminSettingsService adminSettingsService; @@ -94,6 +103,9 @@ public abstract class AbstractServiceTest { @Autowired protected EntityViewService entityViewService; + @Autowired + protected EntityService entityService; + @Autowired protected DeviceCredentialsService deviceCredentialsService; @@ -124,7 +136,13 @@ public abstract class AbstractServiceTest { @Autowired private ComponentDescriptorService componentDescriptorService; - class IdComparator> implements Comparator { + @Autowired + protected TenantProfileService tenantProfileService; + + @Autowired + protected DeviceProfileService deviceProfileService; + + class IdComparator implements Comparator { @Override public int compare(D o1, D o2) { return o1.getId().getId().compareTo(o2.getId().getId()); @@ -134,7 +152,7 @@ public abstract class AbstractServiceTest { protected Event generateEvent(TenantId tenantId, EntityId entityId, String eventType, String eventUid) throws IOException { if (tenantId == null) { - tenantId = new TenantId(UUIDs.timeBased()); + tenantId = new TenantId(Uuids.timeBased()); } Event event = new Event(); event.setTenantId(tenantId); @@ -177,4 +195,22 @@ public abstract class AbstractServiceTest { return new AuditLogLevelFilter(mask); } + protected DeviceProfile createDeviceProfile(TenantId tenantId, String name) { + DeviceProfile deviceProfile = new DeviceProfile(); + deviceProfile.setTenantId(tenantId); + deviceProfile.setName(name); + deviceProfile.setType(DeviceProfileType.DEFAULT); + deviceProfile.setTransportType(DeviceTransportType.DEFAULT); + deviceProfile.setDescription(name + " Test"); + DeviceProfileData deviceProfileData = new DeviceProfileData(); + DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration(); + DefaultDeviceProfileTransportConfiguration transportConfiguration = new DefaultDeviceProfileTransportConfiguration(); + deviceProfileData.setConfiguration(configuration); + deviceProfileData.setTransportConfiguration(transportConfiguration); + deviceProfile.setProfileData(deviceProfileData); + deviceProfile.setDefault(false); + deviceProfile.setDefaultRuleChainId(null); + return deviceProfile; + } + } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java index 0ec919eed0..15bd4eb905 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java @@ -15,24 +15,39 @@ */ package org.thingsboard.server.dao.service; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; +import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.query.AlarmData; +import org.thingsboard.server.common.data.query.AlarmDataPageLink; +import org.thingsboard.server.common.data.query.AlarmDataQuery; +import org.thingsboard.server.common.data.query.DeviceTypeFilter; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.dao.alarm.AlarmOperationResult; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutionException; @@ -58,8 +73,8 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { @Test public void testSaveAndFetchAlarm() throws ExecutionException, InterruptedException { - AssetId parentId = new AssetId(UUIDs.timeBased()); - AssetId childId = new AssetId(UUIDs.timeBased()); + AssetId parentId = new AssetId(Uuids.timeBased()); + AssetId childId = new AssetId(Uuids.timeBased()); EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE); @@ -71,7 +86,8 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK) .startTs(ts).build(); - Alarm created = alarmService.createOrUpdateAlarm(alarm); + AlarmOperationResult result = alarmService.createOrUpdateAlarm(alarm); + Alarm created = result.getAlarm(); Assert.assertNotNull(created); Assert.assertNotNull(created.getId()); @@ -95,8 +111,8 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { @Test public void testFindAlarm() throws ExecutionException, InterruptedException { - AssetId parentId = new AssetId(UUIDs.timeBased()); - AssetId childId = new AssetId(UUIDs.timeBased()); + AssetId parentId = new AssetId(Uuids.timeBased()); + AssetId childId = new AssetId(Uuids.timeBased()); EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE); @@ -109,13 +125,15 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK) .startTs(ts).build(); - Alarm created = alarmService.createOrUpdateAlarm(alarm); + AlarmOperationResult result = alarmService.createOrUpdateAlarm(alarm); + Alarm created = result.getAlarm(); // Check child relation - TimePageData alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() + PageData alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() .affectedEntityId(childId) .status(AlarmStatus.ACTIVE_UNACK).pageLink( - new TimePageLink(1, 0L, System.currentTimeMillis(), false) + new TimePageLink(1, 0, "", + new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) ).build()).get(); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); @@ -125,19 +143,22 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() .affectedEntityId(parentId) .status(AlarmStatus.ACTIVE_UNACK).pageLink( - new TimePageLink(1, 0L, System.currentTimeMillis(), false) + new TimePageLink(1, 0, "", + new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) ).build()).get(); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(0, alarms.getData().size()); created.setPropagate(true); - created = alarmService.createOrUpdateAlarm(created); + result = alarmService.createOrUpdateAlarm(created); + created = result.getAlarm(); // Check child relation alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() .affectedEntityId(childId) .status(AlarmStatus.ACTIVE_UNACK).pageLink( - new TimePageLink(1, 0L, System.currentTimeMillis(), false) + new TimePageLink(1, 0, "", + new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) ).build()).get(); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); @@ -147,7 +168,8 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() .affectedEntityId(parentId) .status(AlarmStatus.ACTIVE_UNACK).pageLink( - new TimePageLink(1, 0L, System.currentTimeMillis(), false) + new TimePageLink(1, 0, "", + new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) ).build()).get(); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); @@ -159,7 +181,8 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() .affectedEntityId(childId) .status(AlarmStatus.ACTIVE_ACK).pageLink( - new TimePageLink(1, 0L, System.currentTimeMillis(), false) + new TimePageLink(1, 0, "", + new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) ).build()).get(); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); @@ -169,7 +192,8 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() .affectedEntityId(childId) .status(AlarmStatus.ACTIVE_UNACK).pageLink( - new TimePageLink(1, 0L, System.currentTimeMillis(), false) + new TimePageLink(1, 0, "", + new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) ).build()).get(); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(0, alarms.getData().size()); @@ -180,17 +204,256 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() .affectedEntityId(childId) .status(AlarmStatus.CLEARED_ACK).pageLink( - new TimePageLink(1, 0L, System.currentTimeMillis(), false) + new TimePageLink(1, 0, "", + new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) ).build()).get(); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); Assert.assertEquals(created, alarms.getData().get(0)); } + @Test + public void testFindCustomerAlarm() throws ExecutionException, InterruptedException { + Customer customer = new Customer(); + customer.setTitle("TestCustomer"); + customer.setTenantId(tenantId); + customer = customerService.saveCustomer(customer); + + Device tenantDevice = new Device(); + tenantDevice.setName("TestTenantDevice"); + tenantDevice.setType("default"); + tenantDevice.setTenantId(tenantId); + tenantDevice = deviceService.saveDevice(tenantDevice); + + Device customerDevice = new Device(); + customerDevice.setName("TestCustomerDevice"); + customerDevice.setType("default"); + customerDevice.setTenantId(tenantId); + customerDevice.setCustomerId(customer.getId()); + customerDevice = deviceService.saveDevice(customerDevice); + + long ts = System.currentTimeMillis(); + Alarm tenantAlarm = Alarm.builder().tenantId(tenantId) + .originator(tenantDevice.getId()) + .type(TEST_ALARM) + .propagate(true) + .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK) + .startTs(ts).build(); + AlarmOperationResult result = alarmService.createOrUpdateAlarm(tenantAlarm); + tenantAlarm = result.getAlarm(); + + Alarm deviceAlarm = Alarm.builder().tenantId(tenantId) + .originator(customerDevice.getId()) + .type(TEST_ALARM) + .propagate(true) + .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK) + .startTs(ts).build(); + result = alarmService.createOrUpdateAlarm(deviceAlarm); + deviceAlarm = result.getAlarm(); + + AlarmDataPageLink pageLink = new AlarmDataPageLink(); + pageLink.setPage(0); + pageLink.setPageSize(10); + pageLink.setSortOrder(new EntityDataSortOrder(new EntityKey(EntityKeyType.ALARM_FIELD, "createdTime"))); + + pageLink.setStartTs(0L); + pageLink.setEndTs(System.currentTimeMillis()); + pageLink.setSearchPropagatedAlarms(true); + pageLink.setSeverityList(Arrays.asList(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)); + pageLink.setStatusList(Arrays.asList(AlarmSearchStatus.ACTIVE)); + + PageData tenantAlarms = alarmService.findAlarmDataByQueryForEntities(tenantId, new CustomerId(CustomerId.NULL_UUID), toQuery(pageLink), Arrays.asList(tenantDevice.getId(), customerDevice.getId())); + Assert.assertEquals(2, tenantAlarms.getData().size()); + + PageData customerAlarms = alarmService.findAlarmDataByQueryForEntities(tenantId, customer.getId(), toQuery(pageLink), Arrays.asList(tenantDevice.getId(), customerDevice.getId())); + Assert.assertEquals(1, customerAlarms.getData().size()); + Assert.assertEquals(deviceAlarm, customerAlarms.getData().get(0)); + + PageData alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() + .affectedEntityId(tenantDevice.getId()) + .status(AlarmStatus.ACTIVE_UNACK).pageLink( + new TimePageLink(10, 0, "", + new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) + ).build()).get(); + Assert.assertNotNull(alarms.getData()); + Assert.assertEquals(1, alarms.getData().size()); + Assert.assertEquals(tenantAlarm, alarms.getData().get(0)); + } + + private AlarmDataQuery toQuery(AlarmDataPageLink pageLink){ + return toQuery(pageLink, Collections.EMPTY_LIST); + } + + private AlarmDataQuery toQuery(AlarmDataPageLink pageLink, List alarmFields){ + return new AlarmDataQuery(new DeviceTypeFilter(), pageLink, null, null, null, alarmFields); + } + + @Test + public void testFindAlarmUsingAlarmDataQuery() throws ExecutionException, InterruptedException { + AssetId parentId = new AssetId(Uuids.timeBased()); + AssetId parentId2 = new AssetId(Uuids.timeBased()); + AssetId childId = new AssetId(Uuids.timeBased()); + + EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE); + EntityRelation relation2 = new EntityRelation(parentId2, childId, EntityRelation.CONTAINS_TYPE); + + Assert.assertTrue(relationService.saveRelationAsync(tenantId, relation).get()); + Assert.assertTrue(relationService.saveRelationAsync(tenantId, relation2).get()); + + long ts = System.currentTimeMillis(); + Alarm alarm = Alarm.builder().tenantId(tenantId).originator(childId) + .type(TEST_ALARM) + .propagate(false) + .severity(AlarmSeverity.CRITICAL) + .status(AlarmStatus.ACTIVE_UNACK) + .startTs(ts).build(); + + AlarmOperationResult result = alarmService.createOrUpdateAlarm(alarm); + Alarm created = result.getAlarm(); + + AlarmDataPageLink pageLink = new AlarmDataPageLink(); + pageLink.setPage(0); + pageLink.setPageSize(10); + pageLink.setSortOrder(new EntityDataSortOrder(new EntityKey(EntityKeyType.ALARM_FIELD, "createdTime"))); + + pageLink.setStartTs(0L); + pageLink.setEndTs(System.currentTimeMillis()); + pageLink.setSearchPropagatedAlarms(false); + pageLink.setSeverityList(Arrays.asList(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)); + pageLink.setStatusList(Arrays.asList(AlarmSearchStatus.ACTIVE)); + + PageData alarms = alarmService.findAlarmDataByQueryForEntities(tenantId, new CustomerId(CustomerId.NULL_UUID), toQuery(pageLink), Collections.singletonList(childId)); + + Assert.assertNotNull(alarms.getData()); + Assert.assertEquals(1, alarms.getData().size()); + Assert.assertEquals(created, alarms.getData().get(0)); + + pageLink.setPage(0); + pageLink.setPageSize(10); + pageLink.setSortOrder(new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"))); + + pageLink.setStartTs(0L); + pageLink.setEndTs(System.currentTimeMillis()); + pageLink.setSearchPropagatedAlarms(false); + pageLink.setSeverityList(Arrays.asList(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)); + pageLink.setStatusList(Arrays.asList(AlarmSearchStatus.ACTIVE)); + + alarms = alarmService.findAlarmDataByQueryForEntities(tenantId, new CustomerId(CustomerId.NULL_UUID), toQuery(pageLink), Collections.singletonList(childId)); + Assert.assertNotNull(alarms.getData()); + Assert.assertEquals(1, alarms.getData().size()); + Assert.assertEquals(created, new Alarm(alarms.getData().get(0))); + + pageLink.setSearchPropagatedAlarms(true); + alarms = alarmService.findAlarmDataByQueryForEntities(tenantId, new CustomerId(CustomerId.NULL_UUID), toQuery(pageLink), Collections.singletonList(childId)); + Assert.assertNotNull(alarms.getData()); + Assert.assertEquals(1, alarms.getData().size()); + Assert.assertEquals(created, new Alarm(alarms.getData().get(0))); + + // Check child relation + created.setPropagate(true); + result = alarmService.createOrUpdateAlarm(created); + created = result.getAlarm(); + + // Check child relation + pageLink.setPage(0); + pageLink.setPageSize(10); + pageLink.setSortOrder(new EntityDataSortOrder(new EntityKey(EntityKeyType.ALARM_FIELD, "createdTime"))); + + pageLink.setStartTs(0L); + pageLink.setEndTs(System.currentTimeMillis()); + pageLink.setSearchPropagatedAlarms(true); + pageLink.setSeverityList(Arrays.asList(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)); + pageLink.setStatusList(Arrays.asList(AlarmSearchStatus.ACTIVE)); + + alarms = alarmService.findAlarmDataByQueryForEntities(tenantId, new CustomerId(CustomerId.NULL_UUID), toQuery(pageLink), Collections.singletonList(childId)); + Assert.assertNotNull(alarms.getData()); + Assert.assertEquals(1, alarms.getData().size()); + Assert.assertEquals(created, alarms.getData().get(0)); + + // Check parent relation + pageLink.setPage(0); + pageLink.setPageSize(10); + pageLink.setSortOrder(new EntityDataSortOrder(new EntityKey(EntityKeyType.ALARM_FIELD, "createdTime"))); + + pageLink.setStartTs(0L); + pageLink.setEndTs(System.currentTimeMillis()); + pageLink.setSearchPropagatedAlarms(true); + pageLink.setSeverityList(Arrays.asList(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)); + pageLink.setStatusList(Arrays.asList(AlarmSearchStatus.ACTIVE)); + + alarms = alarmService.findAlarmDataByQueryForEntities(tenantId, new CustomerId(CustomerId.NULL_UUID), toQuery(pageLink), Collections.singletonList(parentId)); + Assert.assertNotNull(alarms.getData()); + Assert.assertEquals(1, alarms.getData().size()); + Assert.assertEquals(created, alarms.getData().get(0)); + + PageData alarmsInfoData = alarmService.findAlarms(tenantId, AlarmQuery.builder() + .affectedEntityId(childId) + .status(AlarmStatus.ACTIVE_UNACK).pageLink( + new TimePageLink(10, 0, "", + new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) + ).build()).get(); + Assert.assertNotNull(alarmsInfoData.getData()); + Assert.assertEquals(1, alarmsInfoData.getData().size()); + Assert.assertEquals(created, alarmsInfoData.getData().get(0)); + + alarmsInfoData = alarmService.findAlarms(tenantId, AlarmQuery.builder() + .affectedEntityId(parentId) + .status(AlarmStatus.ACTIVE_UNACK).pageLink( + new TimePageLink(10, 0, "", + new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) + ).build()).get(); + Assert.assertNotNull(alarmsInfoData.getData()); + Assert.assertEquals(1, alarmsInfoData.getData().size()); + Assert.assertEquals(created, alarmsInfoData.getData().get(0)); + + alarmsInfoData = alarmService.findAlarms(tenantId, AlarmQuery.builder() + .affectedEntityId(parentId2) + .status(AlarmStatus.ACTIVE_UNACK).pageLink( + new TimePageLink(10, 0, "", + new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) + ).build()).get(); + Assert.assertNotNull(alarmsInfoData.getData()); + Assert.assertEquals(1, alarmsInfoData.getData().size()); + Assert.assertEquals(created, alarmsInfoData.getData().get(0)); + + pageLink.setPage(0); + pageLink.setPageSize(10); + pageLink.setSortOrder(new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"))); + + pageLink.setStartTs(0L); + pageLink.setEndTs(System.currentTimeMillis()); + pageLink.setSearchPropagatedAlarms(true); + pageLink.setSeverityList(Arrays.asList(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)); + pageLink.setStatusList(Arrays.asList(AlarmSearchStatus.ACTIVE)); + + alarms = alarmService.findAlarmDataByQueryForEntities(tenantId, new CustomerId(CustomerId.NULL_UUID), toQuery(pageLink), Collections.singletonList(parentId)); + Assert.assertNotNull(alarms.getData()); + Assert.assertEquals(1, alarms.getData().size()); + Assert.assertEquals(created, alarms.getData().get(0)); + + alarmService.ackAlarm(tenantId, created.getId(), System.currentTimeMillis()).get(); + created = alarmService.findAlarmByIdAsync(tenantId, created.getId()).get(); + + pageLink.setPage(0); + pageLink.setPageSize(10); + pageLink.setSortOrder(new EntityDataSortOrder(new EntityKey(EntityKeyType.ALARM_FIELD, "createdTime"))); + + pageLink.setStartTs(0L); + pageLink.setEndTs(System.currentTimeMillis()); + pageLink.setSearchPropagatedAlarms(true); + pageLink.setSeverityList(Arrays.asList(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)); + pageLink.setStatusList(Arrays.asList(AlarmSearchStatus.ACTIVE)); + + alarms = alarmService.findAlarmDataByQueryForEntities(tenantId, new CustomerId(CustomerId.NULL_UUID), toQuery(pageLink), Collections.singletonList(childId)); + Assert.assertNotNull(alarms.getData()); + Assert.assertEquals(1, alarms.getData().size()); + Assert.assertEquals(created, alarms.getData().get(0)); + } + @Test public void testDeleteAlarm() throws ExecutionException, InterruptedException { - AssetId parentId = new AssetId(UUIDs.timeBased()); - AssetId childId = new AssetId(UUIDs.timeBased()); + AssetId parentId = new AssetId(Uuids.timeBased()); + AssetId childId = new AssetId(Uuids.timeBased()); EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE); @@ -203,12 +466,14 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK) .startTs(ts).build(); - Alarm created = alarmService.createOrUpdateAlarm(alarm); + AlarmOperationResult result = alarmService.createOrUpdateAlarm(alarm); + Alarm created = result.getAlarm(); - TimePageData alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() + PageData alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() .affectedEntityId(childId) .status(AlarmStatus.ACTIVE_UNACK).pageLink( - new TimePageLink(1, 0L, System.currentTimeMillis(), false) + new TimePageLink(1, 0, "", + new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) ).build()).get(); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); @@ -218,23 +483,24 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() .affectedEntityId(parentId) .status(AlarmStatus.ACTIVE_UNACK).pageLink( - new TimePageLink(1, 0L, System.currentTimeMillis(), false) + new TimePageLink(1, 0, "", + new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) ).build()).get(); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); Assert.assertEquals(created, alarms.getData().get(0)); List toAlarmRelations = relationService.findByTo(tenantId, created.getId(), RelationTypeGroup.ALARM); - Assert.assertEquals(8, toAlarmRelations.size()); + Assert.assertEquals(1, toAlarmRelations.size()); List fromChildRelations = relationService.findByFrom(tenantId, childId, RelationTypeGroup.ALARM); - Assert.assertEquals(4, fromChildRelations.size()); + Assert.assertEquals(0, fromChildRelations.size()); - List fromParentRelations = relationService.findByFrom(tenantId, childId, RelationTypeGroup.ALARM); - Assert.assertEquals(4, fromParentRelations.size()); + List fromParentRelations = relationService.findByFrom(tenantId, parentId, RelationTypeGroup.ALARM); + Assert.assertEquals(1, fromParentRelations.size()); - Assert.assertTrue("Alarm was not deleted when expected", alarmService.deleteAlarm(tenantId, created.getId())); + Assert.assertTrue("Alarm was not deleted when expected", alarmService.deleteAlarm(tenantId, created.getId()).isSuccessful()); Alarm fetched = alarmService.findAlarmByIdAsync(tenantId, created.getId()).get(); @@ -243,7 +509,8 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() .affectedEntityId(childId) .status(AlarmStatus.ACTIVE_UNACK).pageLink( - new TimePageLink(1, 0L, System.currentTimeMillis(), false) + new TimePageLink(1, 0, "", + new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) ).build()).get(); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(0, alarms.getData().size()); @@ -252,7 +519,8 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() .affectedEntityId(parentId) .status(AlarmStatus.ACTIVE_UNACK).pageLink( - new TimePageLink(1, 0L, System.currentTimeMillis(), false) + new TimePageLink(1, 0, "", + new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) ).build()).get(); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(0, alarms.getData().size()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseApiUsageStateServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseApiUsageStateServiceTest.java new file mode 100644 index 0000000000..024d3dd5c3 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseApiUsageStateServiceTest.java @@ -0,0 +1,65 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.ApiUsageStateValue; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.id.TenantId; + + +public abstract class BaseApiUsageStateServiceTest extends AbstractServiceTest { + + private TenantId tenantId; + + @Before + public void before() { + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + Tenant savedTenant = tenantService.saveTenant(tenant); + Assert.assertNotNull(savedTenant); + tenantId = savedTenant.getId(); + } + + @After + public void after() { + tenantService.deleteTenant(tenantId); + } + + @Test + public void testFindApiUsageStateByTenantId() { + ApiUsageState apiUsageState = apiUsageStateService.findTenantApiUsageState(tenantId); + Assert.assertNotNull(apiUsageState); + } + + @Test + public void testUpdateApiUsageState(){ + ApiUsageState apiUsageState = apiUsageStateService.findTenantApiUsageState(tenantId); + Assert.assertNotNull(apiUsageState); + Assert.assertTrue(apiUsageState.isTransportEnabled()); + apiUsageState.setTransportState(ApiUsageStateValue.DISABLED); + apiUsageState = apiUsageStateService.update(apiUsageState); + Assert.assertNotNull(apiUsageState); + apiUsageState = apiUsageStateService.findTenantApiUsageState(tenantId); + Assert.assertNotNull(apiUsageState); + Assert.assertFalse(apiUsageState.isTransportEnabled()); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetServiceTest.java index 01fddfb987..65cd2c1536 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetServiceTest.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.dao.service; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; @@ -25,10 +25,11 @@ import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.exception.DataValidationException; import java.util.ArrayList; @@ -103,7 +104,7 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { Asset asset = new Asset(); asset.setName("My asset"); asset.setType("default"); - asset.setTenantId(new TenantId(UUIDs.timeBased())); + asset.setTenantId(new TenantId(Uuids.timeBased())); assetService.saveAsset(asset); } @@ -115,7 +116,7 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { asset.setTenantId(tenantId); asset = assetService.saveAsset(asset); try { - assetService.assignAssetToCustomer(tenantId, asset.getId(), new CustomerId(UUIDs.timeBased())); + assetService.assignAssetToCustomer(tenantId, asset.getId(), new CustomerId(Uuids.timeBased())); } finally { assetService.deleteAsset(tenantId, asset.getId()); } @@ -224,13 +225,13 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { } List loadedAssets = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(23); - TextPageData pageData = null; + PageLink pageLink = new PageLink(23); + PageData pageData = null; do { pageData = assetService.findAssetsByTenantId(tenantId, pageLink); loadedAssets.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -241,7 +242,7 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { assetService.deleteAssetsByTenantId(tenantId); - pageLink = new TextPageLink(33); + pageLink = new PageLink(33); pageData = assetService.findAssetsByTenantId(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertTrue(pageData.getData().isEmpty()); @@ -252,7 +253,7 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { @Test public void testFindAssetsByTenantIdAndName() { String title1 = "Asset title 1"; - List assetsTitle1 = new ArrayList<>(); + List assetsTitle1 = new ArrayList<>(); for (int i=0;i<143;i++) { Asset asset = new Asset(); asset.setTenantId(tenantId); @@ -261,10 +262,10 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); asset.setName(name); asset.setType("default"); - assetsTitle1.add(assetService.saveAsset(asset)); + assetsTitle1.add(new AssetInfo(assetService.saveAsset(asset), null, false)); } String title2 = "Asset title 2"; - List assetsTitle2 = new ArrayList<>(); + List assetsTitle2 = new ArrayList<>(); for (int i=0;i<175;i++) { Asset asset = new Asset(); asset.setTenantId(tenantId); @@ -273,17 +274,17 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); asset.setName(name); asset.setType("default"); - assetsTitle2.add(assetService.saveAsset(asset)); + assetsTitle2.add(new AssetInfo(assetService.saveAsset(asset), null, false)); } - List loadedAssetsTitle1 = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(15, title1); - TextPageData pageData = null; + List loadedAssetsTitle1 = new ArrayList<>(); + PageLink pageLink = new PageLink(15, 0, title1); + PageData pageData = null; do { - pageData = assetService.findAssetsByTenantId(tenantId, pageLink); + pageData = assetService.findAssetInfosByTenantId(tenantId, pageLink); loadedAssetsTitle1.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -292,13 +293,13 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { Assert.assertEquals(assetsTitle1, loadedAssetsTitle1); - List loadedAssetsTitle2 = new ArrayList<>(); - pageLink = new TextPageLink(4, title2); + List loadedAssetsTitle2 = new ArrayList<>(); + pageLink = new PageLink(4, 0, title2); do { - pageData = assetService.findAssetsByTenantId(tenantId, pageLink); + pageData = assetService.findAssetInfosByTenantId(tenantId, pageLink); loadedAssetsTitle2.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -311,8 +312,8 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { assetService.deleteAsset(tenantId, asset.getId()); } - pageLink = new TextPageLink(4, title1); - pageData = assetService.findAssetsByTenantId(tenantId, pageLink); + pageLink = new PageLink(4, 0, title1); + pageData = assetService.findAssetInfosByTenantId(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -320,8 +321,8 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { assetService.deleteAsset(tenantId, asset.getId()); } - pageLink = new TextPageLink(4, title2); - pageData = assetService.findAssetsByTenantId(tenantId, pageLink); + pageLink = new PageLink(4, 0, title2); + pageData = assetService.findAssetInfosByTenantId(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); } @@ -356,13 +357,13 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { } List loadedAssetsType1 = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(15); - TextPageData pageData = null; + PageLink pageLink = new PageLink(15); + PageData pageData = null; do { pageData = assetService.findAssetsByTenantIdAndType(tenantId, type1, pageLink); loadedAssetsType1.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -372,12 +373,12 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { Assert.assertEquals(assetsType1, loadedAssetsType1); List loadedAssetsType2 = new ArrayList<>(); - pageLink = new TextPageLink(4); + pageLink = new PageLink(4); do { pageData = assetService.findAssetsByTenantIdAndType(tenantId, type2, pageLink); loadedAssetsType2.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -390,7 +391,7 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { assetService.deleteAsset(tenantId, asset.getId()); } - pageLink = new TextPageLink(4); + pageLink = new PageLink(4); pageData = assetService.findAssetsByTenantIdAndType(tenantId, type1, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -399,7 +400,7 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { assetService.deleteAsset(tenantId, asset.getId()); } - pageLink = new TextPageLink(4); + pageLink = new PageLink(4); pageData = assetService.findAssetsByTenantIdAndType(tenantId, type2, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -419,24 +420,24 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { customer = customerService.saveCustomer(customer); CustomerId customerId = customer.getId(); - List assets = new ArrayList<>(); + List assets = new ArrayList<>(); for (int i=0;i<278;i++) { Asset asset = new Asset(); asset.setTenantId(tenantId); asset.setName("Asset"+i); asset.setType("default"); asset = assetService.saveAsset(asset); - assets.add(assetService.assignAssetToCustomer(tenantId, asset.getId(), customerId)); + assets.add(new AssetInfo(assetService.assignAssetToCustomer(tenantId, asset.getId(), customerId), customer.getTitle(), customer.isPublic())); } - List loadedAssets = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(23); - TextPageData pageData = null; + List loadedAssets = new ArrayList<>(); + PageLink pageLink = new PageLink(23); + PageData pageData = null; do { - pageData = assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink); + pageData = assetService.findAssetInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink); loadedAssets.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -447,8 +448,8 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { assetService.unassignCustomerAssets(tenantId, customerId); - pageLink = new TextPageLink(33); - pageData = assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink); + pageLink = new PageLink(33); + pageData = assetService.findAssetInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertTrue(pageData.getData().isEmpty()); @@ -492,13 +493,13 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { } List loadedAssetsTitle1 = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(15, title1); - TextPageData pageData = null; + PageLink pageLink = new PageLink(15, 0, title1); + PageData pageData = null; do { pageData = assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink); loadedAssetsTitle1.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -508,12 +509,12 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { Assert.assertEquals(assetsTitle1, loadedAssetsTitle1); List loadedAssetsTitle2 = new ArrayList<>(); - pageLink = new TextPageLink(4, title2); + pageLink = new PageLink(4, 0, title2); do { pageData = assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink); loadedAssetsTitle2.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -526,7 +527,7 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { assetService.deleteAsset(tenantId, asset.getId()); } - pageLink = new TextPageLink(4, title1); + pageLink = new PageLink(4, 0, title1); pageData = assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -535,7 +536,7 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { assetService.deleteAsset(tenantId, asset.getId()); } - pageLink = new TextPageLink(4, title2); + pageLink = new PageLink(4, 0, title2); pageData = assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -581,13 +582,13 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { } List loadedAssetsType1 = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(15); - TextPageData pageData = null; + PageLink pageLink = new PageLink(15); + PageData pageData = null; do { pageData = assetService.findAssetsByTenantIdAndCustomerIdAndType(tenantId, customerId, type1, pageLink); loadedAssetsType1.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -597,12 +598,12 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { Assert.assertEquals(assetsType1, loadedAssetsType1); List loadedAssetsType2 = new ArrayList<>(); - pageLink = new TextPageLink(4); + pageLink = new PageLink(4); do { pageData = assetService.findAssetsByTenantIdAndCustomerIdAndType(tenantId, customerId, type2, pageLink); loadedAssetsType2.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -615,7 +616,7 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { assetService.deleteAsset(tenantId, asset.getId()); } - pageLink = new TextPageLink(4); + pageLink = new PageLink(4); pageData = assetService.findAssetsByTenantIdAndCustomerIdAndType(tenantId, customerId, type1, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -624,7 +625,7 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { assetService.deleteAsset(tenantId, asset.getId()); } - pageLink = new TextPageLink(4); + pageLink = new PageLink(4); pageData = assetService.findAssetsByTenantIdAndCustomerIdAndType(tenantId, customerId, type2, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseCustomerServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseCustomerServiceTest.java index 18db6ee744..36e8aeb702 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseCustomerServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseCustomerServiceTest.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.dao.service; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; @@ -24,8 +24,8 @@ import org.junit.Test; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.exception.DataValidationException; import java.util.ArrayList; @@ -105,7 +105,7 @@ public abstract class BaseCustomerServiceTest extends AbstractServiceTest { public void testSaveCustomerWithInvalidTenant() { Customer customer = new Customer(); customer.setTitle("My customer"); - customer.setTenantId(new TenantId(UUIDs.timeBased())); + customer.setTenantId(new TenantId(Uuids.timeBased())); customerService.saveCustomer(customer); } @@ -146,13 +146,13 @@ public abstract class BaseCustomerServiceTest extends AbstractServiceTest { } List loadedCustomers = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(23); - TextPageData pageData = null; + PageLink pageLink = new PageLink(23); + PageData pageData = null; do { pageData = customerService.findCustomersByTenantId(tenantId, pageLink); loadedCustomers.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -163,7 +163,7 @@ public abstract class BaseCustomerServiceTest extends AbstractServiceTest { customerService.deleteCustomersByTenantId(tenantId); - pageLink = new TextPageLink(33); + pageLink = new PageLink(33); pageData = customerService.findCustomersByTenantId(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertTrue(pageData.getData().isEmpty()); @@ -197,13 +197,13 @@ public abstract class BaseCustomerServiceTest extends AbstractServiceTest { } List loadedCustomersTitle1 = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(15, title1); - TextPageData pageData = null; + PageLink pageLink = new PageLink(15, 0, title1); + PageData pageData = null; do { pageData = customerService.findCustomersByTenantId(tenantId, pageLink); loadedCustomersTitle1.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -213,12 +213,12 @@ public abstract class BaseCustomerServiceTest extends AbstractServiceTest { Assert.assertEquals(customersTitle1, loadedCustomersTitle1); List loadedCustomersTitle2 = new ArrayList<>(); - pageLink = new TextPageLink(4, title2); + pageLink = new PageLink(4, 0, title2); do { pageData = customerService.findCustomersByTenantId(tenantId, pageLink); loadedCustomersTitle2.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -231,7 +231,7 @@ public abstract class BaseCustomerServiceTest extends AbstractServiceTest { customerService.deleteCustomer(tenantId, customer.getId()); } - pageLink = new TextPageLink(4, title1); + pageLink = new PageLink(4, 0, title1); pageData = customerService.findCustomersByTenantId(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -240,7 +240,7 @@ public abstract class BaseCustomerServiceTest extends AbstractServiceTest { customerService.deleteCustomer(tenantId, customer.getId()); } - pageLink = new TextPageLink(4, title2); + pageLink = new PageLink(4, 0, title2); pageData = customerService.findCustomersByTenantId(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java index 93d7cea37c..e6a7528c96 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.dao.service; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; @@ -27,9 +27,8 @@ import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; -import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.exception.DataValidationException; @@ -99,7 +98,7 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest { public void testSaveDashboardWithInvalidTenant() { Dashboard dashboard = new Dashboard(); dashboard.setTitle("My dashboard"); - dashboard.setTenantId(new TenantId(UUIDs.timeBased())); + dashboard.setTenantId(new TenantId(Uuids.timeBased())); dashboardService.saveDashboard(dashboard); } @@ -110,7 +109,7 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest { dashboard.setTenantId(tenantId); dashboard = dashboardService.saveDashboard(dashboard); try { - dashboardService.assignDashboardToCustomer(tenantId, dashboard.getId(), new CustomerId(UUIDs.timeBased())); + dashboardService.assignDashboardToCustomer(tenantId, dashboard.getId(), new CustomerId(Uuids.timeBased())); } finally { dashboardService.deleteDashboard(tenantId, dashboard.getId()); } @@ -179,13 +178,13 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest { } List loadedDashboards = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(16); - TextPageData pageData = null; + PageLink pageLink = new PageLink(16); + PageData pageData = null; do { pageData = dashboardService.findDashboardsByTenantId(tenantId, pageLink); loadedDashboards.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -196,7 +195,7 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest { dashboardService.deleteDashboardsByTenantId(tenantId); - pageLink = new TextPageLink(31); + pageLink = new PageLink(31); pageData = dashboardService.findDashboardsByTenantId(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertTrue(pageData.getData().isEmpty()); @@ -230,13 +229,13 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest { } List loadedDashboardsTitle1 = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(19, title1); - TextPageData pageData = null; + PageLink pageLink = new PageLink(19, 0, title1); + PageData pageData = null; do { pageData = dashboardService.findDashboardsByTenantId(tenantId, pageLink); loadedDashboardsTitle1.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -246,12 +245,12 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest { Assert.assertEquals(dashboardsTitle1, loadedDashboardsTitle1); List loadedDashboardsTitle2 = new ArrayList<>(); - pageLink = new TextPageLink(4, title2); + pageLink = new PageLink(4, 0, title2); do { pageData = dashboardService.findDashboardsByTenantId(tenantId, pageLink); loadedDashboardsTitle2.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -264,7 +263,7 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest { dashboardService.deleteDashboard(tenantId, dashboard.getId()); } - pageLink = new TextPageLink(4, title1); + pageLink = new PageLink(4, 0, title1); pageData = dashboardService.findDashboardsByTenantId(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -273,7 +272,7 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest { dashboardService.deleteDashboard(tenantId, dashboard.getId()); } - pageLink = new TextPageLink(4, title2); + pageLink = new PageLink(4, 0, title2); pageData = dashboardService.findDashboardsByTenantId(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -303,13 +302,13 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest { } List loadedDashboards = new ArrayList<>(); - TimePageLink pageLink = new TimePageLink(23); - TimePageData pageData = null; + PageLink pageLink = new PageLink(23); + PageData pageData = null; do { - pageData = dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink).get(); + pageData = dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink); loadedDashboards.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -320,8 +319,8 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest { dashboardService.unassignCustomerDashboards(tenantId, customerId); - pageLink = new TimePageLink(42); - pageData = dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink).get(); + pageLink = new PageLink(42); + pageData = dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertTrue(pageData.getData().isEmpty()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsServiceTest.java index f014f80d29..aed6a509b7 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsServiceTest.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.dao.service; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -109,7 +109,7 @@ public abstract class BaseDeviceCredentialsServiceTest extends AbstractServiceTe device.setTenantId(tenantId); device = deviceService.saveDevice(device); DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, device.getId()); - DeviceCredentials newDeviceCredentials = new DeviceCredentials(new DeviceCredentialsId(UUIDs.timeBased())); + DeviceCredentials newDeviceCredentials = new DeviceCredentials(new DeviceCredentialsId(Uuids.timeBased())); newDeviceCredentials.setCreatedTime(deviceCredentials.getCreatedTime()); newDeviceCredentials.setDeviceId(deviceCredentials.getDeviceId()); newDeviceCredentials.setCredentialsType(deviceCredentials.getCredentialsType()); @@ -129,7 +129,7 @@ public abstract class BaseDeviceCredentialsServiceTest extends AbstractServiceTe device.setTenantId(tenantId); device = deviceService.saveDevice(device); DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, device.getId()); - deviceCredentials.setDeviceId(new DeviceId(UUIDs.timeBased())); + deviceCredentials.setDeviceId(new DeviceId(Uuids.timeBased())); try { deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); } finally { diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceProfileServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceProfileServiceTest.java new file mode 100644 index 0000000000..132e016f5f --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceProfileServiceTest.java @@ -0,0 +1,291 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileInfo; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.exception.DataValidationException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class BaseDeviceProfileServiceTest extends AbstractServiceTest { + + private IdComparator idComparator = new IdComparator<>(); + private IdComparator deviceProfileInfoIdComparator = new IdComparator<>(); + + private TenantId tenantId; + + @Before + public void before() { + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + Tenant savedTenant = tenantService.saveTenant(tenant); + Assert.assertNotNull(savedTenant); + tenantId = savedTenant.getId(); + } + + @After + public void after() { + tenantService.deleteTenant(tenantId); + } + + @Test + public void testSaveDeviceProfile() { + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId, "Device Profile"); + DeviceProfile savedDeviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); + Assert.assertNotNull(savedDeviceProfile); + Assert.assertNotNull(savedDeviceProfile.getId()); + Assert.assertTrue(savedDeviceProfile.getCreatedTime() > 0); + Assert.assertEquals(deviceProfile.getName(), savedDeviceProfile.getName()); + Assert.assertEquals(deviceProfile.getDescription(), savedDeviceProfile.getDescription()); + Assert.assertEquals(deviceProfile.getProfileData(), savedDeviceProfile.getProfileData()); + Assert.assertEquals(deviceProfile.isDefault(), savedDeviceProfile.isDefault()); + Assert.assertEquals(deviceProfile.getDefaultRuleChainId(), savedDeviceProfile.getDefaultRuleChainId()); + savedDeviceProfile.setName("New device profile"); + deviceProfileService.saveDeviceProfile(savedDeviceProfile); + DeviceProfile foundDeviceProfile = deviceProfileService.findDeviceProfileById(tenantId, savedDeviceProfile.getId()); + Assert.assertEquals(savedDeviceProfile.getName(), foundDeviceProfile.getName()); + } + + @Test + public void testFindDeviceProfileById() { + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId,"Device Profile"); + DeviceProfile savedDeviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); + DeviceProfile foundDeviceProfile = deviceProfileService.findDeviceProfileById(tenantId, savedDeviceProfile.getId()); + Assert.assertNotNull(foundDeviceProfile); + Assert.assertEquals(savedDeviceProfile, foundDeviceProfile); + } + + @Test + public void testFindDeviceProfileInfoById() { + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId,"Device Profile"); + DeviceProfile savedDeviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); + DeviceProfileInfo foundDeviceProfileInfo = deviceProfileService.findDeviceProfileInfoById(tenantId, savedDeviceProfile.getId()); + Assert.assertNotNull(foundDeviceProfileInfo); + Assert.assertEquals(savedDeviceProfile.getId(), foundDeviceProfileInfo.getId()); + Assert.assertEquals(savedDeviceProfile.getName(), foundDeviceProfileInfo.getName()); + Assert.assertEquals(savedDeviceProfile.getType(), foundDeviceProfileInfo.getType()); + } + + @Test + public void testFindDefaultDeviceProfile() { + DeviceProfile foundDefaultDeviceProfile = deviceProfileService.findDefaultDeviceProfile(tenantId); + Assert.assertNotNull(foundDefaultDeviceProfile); + Assert.assertNotNull(foundDefaultDeviceProfile.getId()); + Assert.assertNotNull(foundDefaultDeviceProfile.getName()); + } + + @Test + public void testFindDefaultDeviceProfileInfo() { + DeviceProfileInfo foundDefaultDeviceProfileInfo = deviceProfileService.findDefaultDeviceProfileInfo(tenantId); + Assert.assertNotNull(foundDefaultDeviceProfileInfo); + Assert.assertNotNull(foundDefaultDeviceProfileInfo.getId()); + Assert.assertNotNull(foundDefaultDeviceProfileInfo.getName()); + Assert.assertNotNull(foundDefaultDeviceProfileInfo.getType()); + } + + @Test + public void testSetDefaultDeviceProfile() { + DeviceProfile deviceProfile1 = this.createDeviceProfile(tenantId,"Device Profile 1"); + DeviceProfile deviceProfile2 = this.createDeviceProfile(tenantId,"Device Profile 2"); + + DeviceProfile savedDeviceProfile1 = deviceProfileService.saveDeviceProfile(deviceProfile1); + DeviceProfile savedDeviceProfile2 = deviceProfileService.saveDeviceProfile(deviceProfile2); + + boolean result = deviceProfileService.setDefaultDeviceProfile(tenantId, savedDeviceProfile1.getId()); + Assert.assertTrue(result); + DeviceProfile defaultDeviceProfile = deviceProfileService.findDefaultDeviceProfile(tenantId); + Assert.assertNotNull(defaultDeviceProfile); + Assert.assertEquals(savedDeviceProfile1.getId(), defaultDeviceProfile.getId()); + result = deviceProfileService.setDefaultDeviceProfile(tenantId, savedDeviceProfile2.getId()); + Assert.assertTrue(result); + defaultDeviceProfile = deviceProfileService.findDefaultDeviceProfile(tenantId); + Assert.assertNotNull(defaultDeviceProfile); + Assert.assertEquals(savedDeviceProfile2.getId(), defaultDeviceProfile.getId()); + } + + @Test(expected = DataValidationException.class) + public void testSaveDeviceProfileWithEmptyName() { + DeviceProfile deviceProfile = new DeviceProfile(); + deviceProfile.setTenantId(tenantId); + deviceProfileService.saveDeviceProfile(deviceProfile); + } + + @Test(expected = DataValidationException.class) + public void testSaveDeviceProfileWithSameName() { + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId,"Device Profile"); + deviceProfileService.saveDeviceProfile(deviceProfile); + DeviceProfile deviceProfile2 = this.createDeviceProfile(tenantId,"Device Profile"); + deviceProfileService.saveDeviceProfile(deviceProfile2); + } + + @Ignore + @Test(expected = DataValidationException.class) + public void testChangeDeviceProfileTypeWithExistingDevices() { + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId,"Device Profile"); + DeviceProfile savedDeviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); + Device device = new Device(); + device.setTenantId(tenantId); + device.setName("Test device"); + device.setType("default"); + device.setDeviceProfileId(savedDeviceProfile.getId()); + deviceService.saveDevice(device); + //TODO: once we have more profile types, we should test that we can not change profile type in runtime and uncomment the @Ignore. +// savedDeviceProfile.setType(DeviceProfileType.LWM2M); + deviceProfileService.saveDeviceProfile(savedDeviceProfile); + } + + @Test(expected = DataValidationException.class) + public void testChangeDeviceProfileTransportTypeWithExistingDevices() { + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId,"Device Profile"); + DeviceProfile savedDeviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); + Device device = new Device(); + device.setTenantId(tenantId); + device.setName("Test device"); + device.setType("default"); + device.setDeviceProfileId(savedDeviceProfile.getId()); + deviceService.saveDevice(device); + savedDeviceProfile.setTransportType(DeviceTransportType.MQTT); + deviceProfileService.saveDeviceProfile(savedDeviceProfile); + } + + @Test(expected = DataValidationException.class) + public void testDeleteDeviceProfileWithExistingDevice() { + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId,"Device Profile"); + DeviceProfile savedDeviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); + Device device = new Device(); + device.setTenantId(tenantId); + device.setName("Test device"); + device.setType("default"); + device.setDeviceProfileId(savedDeviceProfile.getId()); + deviceService.saveDevice(device); + deviceProfileService.deleteDeviceProfile(tenantId, savedDeviceProfile.getId()); + } + + @Test + public void testDeleteDeviceProfile() { + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId,"Device Profile"); + DeviceProfile savedDeviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); + deviceProfileService.deleteDeviceProfile(tenantId, savedDeviceProfile.getId()); + DeviceProfile foundDeviceProfile = deviceProfileService.findDeviceProfileById(tenantId, savedDeviceProfile.getId()); + Assert.assertNull(foundDeviceProfile); + } + + @Test + public void testFindDeviceProfiles() { + + List deviceProfiles = new ArrayList<>(); + PageLink pageLink = new PageLink(17); + PageData pageData = deviceProfileService.findDeviceProfiles(tenantId, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(1, pageData.getTotalElements()); + deviceProfiles.addAll(pageData.getData()); + + for (int i=0;i<28;i++) { + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId,"Device Profile"+i); + deviceProfiles.add(deviceProfileService.saveDeviceProfile(deviceProfile)); + } + + List loadedDeviceProfiles = new ArrayList<>(); + pageLink = new PageLink(17); + do { + pageData = deviceProfileService.findDeviceProfiles(tenantId, pageLink); + loadedDeviceProfiles.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(deviceProfiles, idComparator); + Collections.sort(loadedDeviceProfiles, idComparator); + + Assert.assertEquals(deviceProfiles, loadedDeviceProfiles); + + for (DeviceProfile deviceProfile : loadedDeviceProfiles) { + if (!deviceProfile.isDefault()) { + deviceProfileService.deleteDeviceProfile(tenantId, deviceProfile.getId()); + } + } + + pageLink = new PageLink(17); + pageData = deviceProfileService.findDeviceProfiles(tenantId, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(1, pageData.getTotalElements()); + } + + @Test + public void testFindDeviceProfileInfos() { + + List deviceProfiles = new ArrayList<>(); + PageLink pageLink = new PageLink(17); + PageData deviceProfilePageData = deviceProfileService.findDeviceProfiles(tenantId, pageLink); + Assert.assertFalse(deviceProfilePageData.hasNext()); + Assert.assertEquals(1, deviceProfilePageData.getTotalElements()); + deviceProfiles.addAll(deviceProfilePageData.getData()); + + for (int i=0;i<28;i++) { + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId,"Device Profile"+i); + deviceProfiles.add(deviceProfileService.saveDeviceProfile(deviceProfile)); + } + + List loadedDeviceProfileInfos = new ArrayList<>(); + pageLink = new PageLink(17); + PageData pageData; + do { + pageData = deviceProfileService.findDeviceProfileInfos(tenantId, pageLink, null); + loadedDeviceProfileInfos.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + + Collections.sort(deviceProfiles, idComparator); + Collections.sort(loadedDeviceProfileInfos, deviceProfileInfoIdComparator); + + List deviceProfileInfos = deviceProfiles.stream() + .map(deviceProfile -> new DeviceProfileInfo(deviceProfile.getId(), + deviceProfile.getName(), deviceProfile.getType(), deviceProfile.getTransportType())).collect(Collectors.toList()); + + Assert.assertEquals(deviceProfileInfos, loadedDeviceProfileInfos); + + for (DeviceProfile deviceProfile : deviceProfiles) { + if (!deviceProfile.isDefault()) { + deviceProfileService.deleteDeviceProfile(tenantId, deviceProfile.getId()); + } + } + + pageLink = new PageLink(17); + pageData = deviceProfileService.findDeviceProfileInfos(tenantId, pageLink, null); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(1, pageData.getTotalElements()); + } +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java index 6b4ef959b0..389ddbf638 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java @@ -15,20 +15,17 @@ */ package org.thingsboard.server.dao.service; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.EntitySubtype; -import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.*; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.dao.exception.DataValidationException; @@ -113,7 +110,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { Device device = new Device(); device.setName("My device"); device.setType("default"); - device.setTenantId(new TenantId(UUIDs.timeBased())); + device.setTenantId(new TenantId(Uuids.timeBased())); deviceService.saveDevice(device); } @@ -125,12 +122,12 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { device.setTenantId(tenantId); device = deviceService.saveDevice(device); try { - deviceService.assignDeviceToCustomer(tenantId, device.getId(), new CustomerId(UUIDs.timeBased())); + deviceService.assignDeviceToCustomer(tenantId, device.getId(), new CustomerId(Uuids.timeBased())); } finally { deviceService.deleteDevice(tenantId, device.getId()); } } - + @Test(expected = DataValidationException.class) public void testAssignDeviceToCustomerFromDifferentTenant() { Device device = new Device(); @@ -236,13 +233,13 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { } List loadedDevices = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(23); - TextPageData pageData = null; + PageLink pageLink = new PageLink(23); + PageData pageData = null; do { pageData = deviceService.findDevicesByTenantId(tenantId, pageLink); loadedDevices.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -253,7 +250,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { deviceService.deleteDevicesByTenantId(tenantId); - pageLink = new TextPageLink(33); + pageLink = new PageLink(33); pageData = deviceService.findDevicesByTenantId(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertTrue(pageData.getData().isEmpty()); @@ -264,7 +261,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { @Test public void testFindDevicesByTenantIdAndName() { String title1 = "Device title 1"; - List devicesTitle1 = new ArrayList<>(); + List devicesTitle1 = new ArrayList<>(); for (int i=0;i<143;i++) { Device device = new Device(); device.setTenantId(tenantId); @@ -273,10 +270,10 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); device.setName(name); device.setType("default"); - devicesTitle1.add(deviceService.saveDevice(device)); + devicesTitle1.add(new DeviceInfo(deviceService.saveDevice(device), null, false, "default")); } String title2 = "Device title 2"; - List devicesTitle2 = new ArrayList<>(); + List devicesTitle2 = new ArrayList<>(); for (int i=0;i<175;i++) { Device device = new Device(); device.setTenantId(tenantId); @@ -285,17 +282,17 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); device.setName(name); device.setType("default"); - devicesTitle2.add(deviceService.saveDevice(device)); + devicesTitle2.add(new DeviceInfo(deviceService.saveDevice(device), null, false, "default")); } - List loadedDevicesTitle1 = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(15, title1); - TextPageData pageData = null; + List loadedDevicesTitle1 = new ArrayList<>(); + PageLink pageLink = new PageLink(15, 0, title1); + PageData pageData = null; do { - pageData = deviceService.findDevicesByTenantId(tenantId, pageLink); + pageData = deviceService.findDeviceInfosByTenantId(tenantId, pageLink); loadedDevicesTitle1.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -304,13 +301,13 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { Assert.assertEquals(devicesTitle1, loadedDevicesTitle1); - List loadedDevicesTitle2 = new ArrayList<>(); - pageLink = new TextPageLink(4, title2); + List loadedDevicesTitle2 = new ArrayList<>(); + pageLink = new PageLink(4, 0, title2); do { - pageData = deviceService.findDevicesByTenantId(tenantId, pageLink); + pageData = deviceService.findDeviceInfosByTenantId(tenantId, pageLink); loadedDevicesTitle2.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -323,8 +320,8 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { deviceService.deleteDevice(tenantId, device.getId()); } - pageLink = new TextPageLink(4, title1); - pageData = deviceService.findDevicesByTenantId(tenantId, pageLink); + pageLink = new PageLink(4, 0, title1); + pageData = deviceService.findDeviceInfosByTenantId(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -332,8 +329,8 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { deviceService.deleteDevice(tenantId, device.getId()); } - pageLink = new TextPageLink(4, title2); - pageData = deviceService.findDevicesByTenantId(tenantId, pageLink); + pageLink = new PageLink(4, 0, title2); + pageData = deviceService.findDeviceInfosByTenantId(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); } @@ -368,13 +365,13 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { } List loadedDevicesType1 = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(15); - TextPageData pageData = null; + PageLink pageLink = new PageLink(15); + PageData pageData = null; do { pageData = deviceService.findDevicesByTenantIdAndType(tenantId, type1, pageLink); loadedDevicesType1.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -384,12 +381,12 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { Assert.assertEquals(devicesType1, loadedDevicesType1); List loadedDevicesType2 = new ArrayList<>(); - pageLink = new TextPageLink(4); + pageLink = new PageLink(4); do { pageData = deviceService.findDevicesByTenantIdAndType(tenantId, type2, pageLink); loadedDevicesType2.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -402,7 +399,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { deviceService.deleteDevice(tenantId, device.getId()); } - pageLink = new TextPageLink(4); + pageLink = new PageLink(4); pageData = deviceService.findDevicesByTenantIdAndType(tenantId, type1, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -411,7 +408,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { deviceService.deleteDevice(tenantId, device.getId()); } - pageLink = new TextPageLink(4); + pageLink = new PageLink(4); pageData = deviceService.findDevicesByTenantIdAndType(tenantId, type2, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -431,24 +428,24 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { customer = customerService.saveCustomer(customer); CustomerId customerId = customer.getId(); - List devices = new ArrayList<>(); + List devices = new ArrayList<>(); for (int i=0;i<278;i++) { Device device = new Device(); device.setTenantId(tenantId); device.setName("Device"+i); device.setType("default"); device = deviceService.saveDevice(device); - devices.add(deviceService.assignDeviceToCustomer(tenantId, device.getId(), customerId)); + devices.add(new DeviceInfo(deviceService.assignDeviceToCustomer(tenantId, device.getId(), customerId), customer.getTitle(), customer.isPublic(), "default")); } - List loadedDevices = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(23); - TextPageData pageData = null; + List loadedDevices = new ArrayList<>(); + PageLink pageLink = new PageLink(23); + PageData pageData = null; do { - pageData = deviceService.findDevicesByTenantIdAndCustomerId(tenantId, customerId, pageLink); + pageData = deviceService.findDeviceInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink); loadedDevices.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -459,8 +456,8 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { deviceService.unassignCustomerDevices(tenantId, customerId); - pageLink = new TextPageLink(33); - pageData = deviceService.findDevicesByTenantIdAndCustomerId(tenantId, customerId, pageLink); + pageLink = new PageLink(33); + pageData = deviceService.findDeviceInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertTrue(pageData.getData().isEmpty()); @@ -504,13 +501,13 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { } List loadedDevicesTitle1 = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(15, title1); - TextPageData pageData = null; + PageLink pageLink = new PageLink(15, 0, title1); + PageData pageData = null; do { pageData = deviceService.findDevicesByTenantIdAndCustomerId(tenantId, customerId, pageLink); loadedDevicesTitle1.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -520,12 +517,12 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { Assert.assertEquals(devicesTitle1, loadedDevicesTitle1); List loadedDevicesTitle2 = new ArrayList<>(); - pageLink = new TextPageLink(4, title2); + pageLink = new PageLink(4, 0, title2); do { pageData = deviceService.findDevicesByTenantIdAndCustomerId(tenantId, customerId, pageLink); loadedDevicesTitle2.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -538,7 +535,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { deviceService.deleteDevice(tenantId, device.getId()); } - pageLink = new TextPageLink(4, title1); + pageLink = new PageLink(4, 0, title1); pageData = deviceService.findDevicesByTenantIdAndCustomerId(tenantId, customerId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -547,7 +544,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { deviceService.deleteDevice(tenantId, device.getId()); } - pageLink = new TextPageLink(4, title2); + pageLink = new PageLink(4, 0, title2); pageData = deviceService.findDevicesByTenantIdAndCustomerId(tenantId, customerId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -593,13 +590,13 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { } List loadedDevicesType1 = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(15); - TextPageData pageData = null; + PageLink pageLink = new PageLink(15); + PageData pageData = null; do { pageData = deviceService.findDevicesByTenantIdAndCustomerIdAndType(tenantId, customerId, type1, pageLink); loadedDevicesType1.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -609,12 +606,12 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { Assert.assertEquals(devicesType1, loadedDevicesType1); List loadedDevicesType2 = new ArrayList<>(); - pageLink = new TextPageLink(4); + pageLink = new PageLink(4); do { pageData = deviceService.findDevicesByTenantIdAndCustomerIdAndType(tenantId, customerId, type2, pageLink); loadedDevicesType2.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -627,7 +624,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { deviceService.deleteDevice(tenantId, device.getId()); } - pageLink = new TextPageLink(4); + pageLink = new PageLink(4); pageData = deviceService.findDevicesByTenantIdAndCustomerIdAndType(tenantId, customerId, type1, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -636,7 +633,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { deviceService.deleteDevice(tenantId, device.getId()); } - pageLink = new TextPageLink(4); + pageLink = new PageLink(4); pageData = deviceService.findDevicesByTenantIdAndCustomerIdAndType(tenantId, customerId, type2, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java new file mode 100644 index 0000000000..ff87e17d56 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java @@ -0,0 +1,1283 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.id.*; +import org.thingsboard.server.common.data.kv.*; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.*; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.EntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity; +import org.thingsboard.server.dao.rule.RuleChainService; +import org.thingsboard.server.dao.timeseries.TimeseriesService; +import org.thingsboard.server.dao.util.DaoTestUtil; +import org.thingsboard.server.dao.util.SqlDbType; + +import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +public abstract class BaseEntityServiceTest extends AbstractServiceTest { + + @Autowired + private AttributesService attributesService; + + @Autowired + private TimeseriesService timeseriesService; + + private TenantId tenantId; + + @Autowired + private JdbcTemplate template; + + @Before + public void before() { + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + Tenant savedTenant = tenantService.saveTenant(tenant); + Assert.assertNotNull(savedTenant); + tenantId = savedTenant.getId(); + } + + @After + public void after() { + tenantService.deleteTenant(tenantId); + } + + + @Test + public void testCountEntitiesByQuery() throws InterruptedException { + List devices = new ArrayList<>(); + for (int i = 0; i < 97; i++) { + Device device = new Device(); + device.setTenantId(tenantId); + device.setName("Device" + i); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + devices.add(deviceService.saveDevice(device)); + } + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceType("default"); + filter.setDeviceNameFilter(""); + + EntityCountQuery countQuery = new EntityCountQuery(filter); + + long count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(97, count); + + filter.setDeviceType("unknown"); + count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(0, count); + + filter.setDeviceType("default"); + filter.setDeviceNameFilter("Device1"); + count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(11, count); + + EntityListFilter entityListFilter = new EntityListFilter(); + entityListFilter.setEntityType(EntityType.DEVICE); + entityListFilter.setEntityList(devices.stream().map(Device::getId).map(DeviceId::toString).collect(Collectors.toList())); + + countQuery = new EntityCountQuery(entityListFilter); + count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(97, count); + + deviceService.deleteDevicesByTenantId(tenantId); + count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(0, count); + } + + + @Test + public void testCountHierarchicalEntitiesByQuery() throws InterruptedException { + List assets = new ArrayList<>(); + List devices = new ArrayList<>(); + createTestHierarchy(assets, devices, new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>()); + + RelationsQueryFilter filter = new RelationsQueryFilter(); + filter.setRootEntity(tenantId); + filter.setDirection(EntitySearchDirection.FROM); + + EntityCountQuery countQuery = new EntityCountQuery(filter); + + long count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(30, count); + + filter.setFilters(Collections.singletonList(new EntityTypeFilter("Contains", Collections.singletonList(EntityType.DEVICE)))); + count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(25, count); + + filter.setRootEntity(devices.get(0).getId()); + filter.setDirection(EntitySearchDirection.TO); + filter.setFilters(Collections.singletonList(new EntityTypeFilter("Manages", Collections.singletonList(EntityType.TENANT)))); + count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(1, count); + + DeviceSearchQueryFilter filter2 = new DeviceSearchQueryFilter(); + filter2.setRootEntity(tenantId); + filter2.setDirection(EntitySearchDirection.FROM); + filter2.setRelationType("Contains"); + + countQuery = new EntityCountQuery(filter2); + + count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(25, count); + + filter2.setDeviceTypes(Arrays.asList("default0", "default1")); + count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(10, count); + + filter2.setRootEntity(devices.get(0).getId()); + filter2.setDirection(EntitySearchDirection.TO); + count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(0, count); + + AssetSearchQueryFilter filter3 = new AssetSearchQueryFilter(); + filter3.setRootEntity(tenantId); + filter3.setDirection(EntitySearchDirection.FROM); + filter3.setRelationType("Manages"); + + countQuery = new EntityCountQuery(filter3); + + count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(5, count); + + filter3.setAssetTypes(Arrays.asList("type0", "type1")); + count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(2, count); + + filter3.setRootEntity(devices.get(0).getId()); + filter3.setDirection(EntitySearchDirection.TO); + count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(0, count); + } + + + @Test + public void testHierarchicalFindEntityDataWithAttributesByQuery() throws ExecutionException, InterruptedException { + List assets = new ArrayList<>(); + List devices = new ArrayList<>(); + List temperatures = new ArrayList<>(); + List highTemperatures = new ArrayList<>(); + createTestHierarchy(assets, devices, new ArrayList<>(), new ArrayList<>(), temperatures, highTemperatures); + + List>> attributeFutures = new ArrayList<>(); + for (int i = 0; i < devices.size(); i++) { + Device device = devices.get(i); + attributeFutures.add(saveLongAttribute(device.getId(), "temperature", temperatures.get(i), DataConstants.CLIENT_SCOPE)); + } + Futures.successfulAsList(attributeFutures).get(); + + RelationsQueryFilter filter = new RelationsQueryFilter(); + filter.setRootEntity(tenantId); + filter.setDirection(EntitySearchDirection.FROM); + filter.setFilters(Collections.singletonList(new EntityTypeFilter("Contains", Collections.singletonList(EntityType.DEVICE)))); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC + ); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + List latestValues = Collections.singletonList(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null); + PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + List loadedEntities = new ArrayList<>(data.getData()); + while (data.hasNext()) { + query = query.next(); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities.addAll(data.getData()); + } + Assert.assertEquals(25, loadedEntities.size()); + List loadedTemperatures = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList()); + List deviceTemperatures = temperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); + Assert.assertEquals(deviceTemperatures, loadedTemperatures); + + pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + KeyFilter highTemperatureFilter = new KeyFilter(); + highTemperatureFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + NumericFilterPredicate predicate = new NumericFilterPredicate(); + predicate.setValue(FilterPredicateValue.fromDouble(45)); + predicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + highTemperatureFilter.setPredicate(predicate); + List keyFilters = Collections.singletonList(highTemperatureFilter); + + query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters); + + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + + loadedEntities = new ArrayList<>(data.getData()); + while (data.hasNext()) { + query = query.next(); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities.addAll(data.getData()); + } + Assert.assertEquals(highTemperatures.size(), loadedEntities.size()); + + List loadedHighTemperatures = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList()); + List deviceHighTemperatures = highTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); + + Assert.assertEquals(deviceHighTemperatures, loadedHighTemperatures); + + deviceService.deleteDevicesByTenantId(tenantId); + } + + + @Test + public void testHierarchicalFindDevicesWithAttributesByQuery() throws ExecutionException, InterruptedException { + List assets = new ArrayList<>(); + List devices = new ArrayList<>(); + List temperatures = new ArrayList<>(); + List highTemperatures = new ArrayList<>(); + createTestHierarchy(assets, devices, new ArrayList<>(), new ArrayList<>(), temperatures, highTemperatures); + + List>> attributeFutures = new ArrayList<>(); + for (int i = 0; i < devices.size(); i++) { + Device device = devices.get(i); + attributeFutures.add(saveLongAttribute(device.getId(), "temperature", temperatures.get(i), DataConstants.CLIENT_SCOPE)); + } + Futures.successfulAsList(attributeFutures).get(); + + DeviceSearchQueryFilter filter = new DeviceSearchQueryFilter(); + filter.setRootEntity(tenantId); + filter.setDirection(EntitySearchDirection.FROM); + filter.setRelationType("Contains"); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC + ); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + List latestValues = Collections.singletonList(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null); + PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + List loadedEntities = new ArrayList<>(data.getData()); + while (data.hasNext()) { + query = query.next(); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities.addAll(data.getData()); + } + Assert.assertEquals(25, loadedEntities.size()); + loadedEntities.forEach(entity -> Assert.assertTrue(devices.stream().map(Device::getId).collect(Collectors.toSet()).contains(entity.getEntityId()))); + List loadedTemperatures = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList()); + List deviceTemperatures = temperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); + Assert.assertEquals(deviceTemperatures, loadedTemperatures); + + pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + KeyFilter highTemperatureFilter = new KeyFilter(); + highTemperatureFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + NumericFilterPredicate predicate = new NumericFilterPredicate(); + predicate.setValue(FilterPredicateValue.fromDouble(45)); + predicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + highTemperatureFilter.setPredicate(predicate); + List keyFilters = Collections.singletonList(highTemperatureFilter); + + query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters); + + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + + loadedEntities = new ArrayList<>(data.getData()); + while (data.hasNext()) { + query = query.next(); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities.addAll(data.getData()); + } + Assert.assertEquals(highTemperatures.size(), loadedEntities.size()); + + List loadedHighTemperatures = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList()); + List deviceHighTemperatures = highTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); + + Assert.assertEquals(deviceHighTemperatures, loadedHighTemperatures); + + deviceService.deleteDevicesByTenantId(tenantId); + } + + + @Test + public void testHierarchicalFindAssetsWithAttributesByQuery() throws ExecutionException, InterruptedException { + List assets = new ArrayList<>(); + List devices = new ArrayList<>(); + List consumptions = new ArrayList<>(); + List highConsumptions = new ArrayList<>(); + createTestHierarchy(assets, devices, consumptions, highConsumptions, new ArrayList<>(), new ArrayList<>()); + + List>> attributeFutures = new ArrayList<>(); + for (int i = 0; i < assets.size(); i++) { + Asset asset = assets.get(i); + attributeFutures.add(saveLongAttribute(asset.getId(), "consumption", consumptions.get(i), DataConstants.SERVER_SCOPE)); + } + Futures.successfulAsList(attributeFutures).get(); + + AssetSearchQueryFilter filter = new AssetSearchQueryFilter(); + filter.setRootEntity(tenantId); + filter.setDirection(EntitySearchDirection.FROM); + filter.setRelationType("Manages"); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC + ); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + List latestValues = Collections.singletonList(new EntityKey(EntityKeyType.ATTRIBUTE, "consumption")); + + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null); + PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + List loadedEntities = new ArrayList<>(data.getData()); + while (data.hasNext()) { + query = query.next(); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities.addAll(data.getData()); + } + Assert.assertEquals(5, loadedEntities.size()); + List loadedTemperatures = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.ATTRIBUTE).get("consumption").getValue()).collect(Collectors.toList()); + List deviceTemperatures = consumptions.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); + Assert.assertEquals(deviceTemperatures, loadedTemperatures); + + pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + KeyFilter highTemperatureFilter = new KeyFilter(); + highTemperatureFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "consumption")); + NumericFilterPredicate predicate = new NumericFilterPredicate(); + predicate.setValue(FilterPredicateValue.fromDouble(50)); + predicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + highTemperatureFilter.setPredicate(predicate); + List keyFilters = Collections.singletonList(highTemperatureFilter); + + query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters); + + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + + loadedEntities = new ArrayList<>(data.getData()); + while (data.hasNext()) { + query = query.next(); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities.addAll(data.getData()); + } + Assert.assertEquals(highConsumptions.size(), loadedEntities.size()); + + List loadedHighTemperatures = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.ATTRIBUTE).get("consumption").getValue()).collect(Collectors.toList()); + List deviceHighTemperatures = highConsumptions.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); + + Assert.assertEquals(deviceHighTemperatures, loadedHighTemperatures); + + deviceService.deleteDevicesByTenantId(tenantId); + } + + private void createTestHierarchy(List assets, List devices, List consumptions, List highConsumptions, List temperatures, List highTemperatures) throws InterruptedException { + for (int i = 0; i < 5; i++) { + Asset asset = new Asset(); + asset.setTenantId(tenantId); + asset.setName("Asset" + i); + asset.setType("type" + i); + asset.setLabel("AssetLabel" + i); + asset = assetService.saveAsset(asset); + //TO make sure devices have different created time + Thread.sleep(1); + assets.add(asset); + EntityRelation er = new EntityRelation(); + er.setFrom(tenantId); + er.setTo(asset.getId()); + er.setType("Manages"); + er.setTypeGroup(RelationTypeGroup.COMMON); + relationService.saveRelation(tenantId, er); + long consumption = (long) (Math.random() * 100); + consumptions.add(consumption); + if (consumption > 50) { + highConsumptions.add(consumption); + } + for (int j = 0; j < 5; j++) { + Device device = new Device(); + device.setTenantId(tenantId); + device.setName("A" + i + "Device" + j); + device.setType("default" + j); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + device = deviceService.saveDevice(device); + //TO make sure devices have different created time + Thread.sleep(1); + devices.add(device); + er = new EntityRelation(); + er.setFrom(asset.getId()); + er.setTo(device.getId()); + er.setType("Contains"); + er.setTypeGroup(RelationTypeGroup.COMMON); + relationService.saveRelation(tenantId, er); + long temperature = (long) (Math.random() * 100); + temperatures.add(temperature); + if (temperature > 45) { + highTemperatures.add(temperature); + } + } + } + } + + + @Test + public void testSimpleFindEntityDataByQuery() throws InterruptedException { + List devices = new ArrayList<>(); + for (int i = 0; i < 97; i++) { + Device device = new Device(); + device.setTenantId(tenantId); + device.setName("Device" + i); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + //TO make sure devices have different created time + Thread.sleep(1); + devices.add(deviceService.saveDevice(device)); + } + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceType("default"); + filter.setDeviceNameFilter(""); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC + ); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, null); + PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + + Assert.assertEquals(97, data.getTotalElements()); + Assert.assertEquals(10, data.getTotalPages()); + Assert.assertTrue(data.hasNext()); + Assert.assertEquals(10, data.getData().size()); + + List loadedEntities = new ArrayList<>(data.getData()); + while (data.hasNext()) { + query = query.next(); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities.addAll(data.getData()); + } + Assert.assertEquals(97, loadedEntities.size()); + + List loadedIds = loadedEntities.stream().map(EntityData::getEntityId).collect(Collectors.toList()); + List deviceIds = devices.stream().map(Device::getId).collect(Collectors.toList()); + deviceIds.sort(Comparator.comparing(EntityId::getId)); + loadedIds.sort(Comparator.comparing(EntityId::getId)); + Assert.assertEquals(deviceIds, loadedIds); + + List loadedNames = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()).collect(Collectors.toList()); + List deviceNames = devices.stream().map(Device::getName).collect(Collectors.toList()); + + Collections.sort(loadedNames); + Collections.sort(deviceNames); + Assert.assertEquals(deviceNames, loadedNames); + + sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), EntityDataSortOrder.Direction.DESC + ); + + pageLink = new EntityDataPageLink(10, 0, "device1", sortOrder); + query = new EntityDataQuery(filter, pageLink, entityFields, null, null); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + Assert.assertEquals(11, data.getTotalElements()); + Assert.assertEquals("Device19", data.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); + + deviceService.deleteDevicesByTenantId(tenantId); + } + + @Test + public void testFindEntityDataByQueryWithAttributes() throws ExecutionException, InterruptedException { + + List attributesEntityTypes = new ArrayList<>(Arrays.asList(EntityKeyType.CLIENT_ATTRIBUTE, EntityKeyType.SHARED_ATTRIBUTE, EntityKeyType.SERVER_ATTRIBUTE)); + + List devices = new ArrayList<>(); + List temperatures = new ArrayList<>(); + List highTemperatures = new ArrayList<>(); + for (int i = 0; i < 67; i++) { + Device device = new Device(); + device.setTenantId(tenantId); + device.setName("Device" + i); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + devices.add(deviceService.saveDevice(device)); + //TO make sure devices have different created time + Thread.sleep(1); + long temperature = (long) (Math.random() * 100); + temperatures.add(temperature); + if (temperature > 45) { + highTemperatures.add(temperature); + } + } + + List>> attributeFutures = new ArrayList<>(); + for (int i = 0; i < devices.size(); i++) { + Device device = devices.get(i); + for (String currentScope : DataConstants.allScopes()) { + attributeFutures.add(saveLongAttribute(device.getId(), "temperature", temperatures.get(i), currentScope)); + } + } + Futures.successfulAsList(attributeFutures).get(); + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceType("default"); + filter.setDeviceNameFilter(""); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC + ); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + for (EntityKeyType currentAttributeKeyType : attributesEntityTypes) { + List latestValues = Collections.singletonList(new EntityKey(currentAttributeKeyType, "temperature")); + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null); + PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + List loadedEntities = new ArrayList<>(data.getData()); + while (data.hasNext()) { + query = query.next(); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities.addAll(data.getData()); + } + Assert.assertEquals(67, loadedEntities.size()); + List loadedTemperatures = new ArrayList<>(); + for (Device device : devices) { + loadedTemperatures.add(loadedEntities.stream().filter(entityData -> entityData.getEntityId().equals(device.getId())).findFirst().orElse(null) + .getLatest().get(currentAttributeKeyType).get("temperature").getValue()); + } + List deviceTemperatures = temperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); + Assert.assertEquals(deviceTemperatures, loadedTemperatures); + + pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + KeyFilter highTemperatureFilter = createNumericKeyFilter("temperature", currentAttributeKeyType, NumericFilterPredicate.NumericOperation.GREATER, 45); + List keyFiltersHighTemperature = Collections.singletonList(highTemperatureFilter); + + query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersHighTemperature); + + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + + loadedEntities = new ArrayList<>(data.getData()); + + while (data.hasNext()) { + query = query.next(); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities.addAll(data.getData()); + } + Assert.assertEquals(highTemperatures.size(), loadedEntities.size()); + + List loadedHighTemperatures = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(currentAttributeKeyType).get("temperature").getValue()).collect(Collectors.toList()); + List deviceHighTemperatures = highTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); + + Assert.assertEquals(deviceHighTemperatures, loadedHighTemperatures); + + } + deviceService.deleteDevicesByTenantId(tenantId); + } + + @Test + public void testBuildNumericPredicateQueryOperations() throws ExecutionException, InterruptedException{ + + List devices = new ArrayList<>(); + List temperatures = new ArrayList<>(); + List equalTemperatures = new ArrayList<>(); + List notEqualTemperatures = new ArrayList<>(); + List greaterTemperatures = new ArrayList<>(); + List greaterOrEqualTemperatures = new ArrayList<>(); + List lessTemperatures = new ArrayList<>(); + List lessOrEqualTemperatures = new ArrayList<>(); + + for (int i = 0; i < 10; i++) { + Device device = new Device(); + device.setTenantId(tenantId); + device.setName("Device" + i); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + devices.add(deviceService.saveDevice(device)); + //TO make sure devices have different created time + Thread.sleep(1); + long temperature = (long) (Math.random() * 100); + temperatures.add(temperature); + if (temperature == 45) { + greaterOrEqualTemperatures.add(temperature); + lessOrEqualTemperatures.add(temperature); + equalTemperatures.add(temperature); + } else if (temperature > 45) { + greaterTemperatures.add(temperature); + greaterOrEqualTemperatures.add(temperature); + notEqualTemperatures.add(temperature); + } else { + lessTemperatures.add(temperature); + lessOrEqualTemperatures.add(temperature); + notEqualTemperatures.add(temperature); + } + } + + List>> attributeFutures = new ArrayList<>(); + for (int i = 0; i < devices.size(); i++) { + Device device = devices.get(i); + attributeFutures.add(saveLongAttribute(device.getId(), "temperature", temperatures.get(i), DataConstants.CLIENT_SCOPE)); + } + Futures.successfulAsList(attributeFutures).get(); + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceType("default"); + filter.setDeviceNameFilter(""); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC + ); + + List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + List latestValues = Collections.singletonList(new EntityKey(EntityKeyType.CLIENT_ATTRIBUTE, "temperature")); + + KeyFilter greaterTemperatureFilter = createNumericKeyFilter("temperature", EntityKeyType.CLIENT_ATTRIBUTE, NumericFilterPredicate.NumericOperation.GREATER, 45); + List keyFiltersGreaterTemperature = Collections.singletonList(greaterTemperatureFilter); + + KeyFilter greaterOrEqualTemperatureFilter = createNumericKeyFilter("temperature", EntityKeyType.CLIENT_ATTRIBUTE, NumericFilterPredicate.NumericOperation.GREATER_OR_EQUAL, 45); + List keyFiltersGreaterOrEqualTemperature = Collections.singletonList(greaterOrEqualTemperatureFilter); + + KeyFilter lessTemperatureFilter = createNumericKeyFilter("temperature", EntityKeyType.CLIENT_ATTRIBUTE, NumericFilterPredicate.NumericOperation.LESS, 45); + List keyFiltersLessTemperature = Collections.singletonList(lessTemperatureFilter); + + KeyFilter lessOrEqualTemperatureFilter = createNumericKeyFilter("temperature", EntityKeyType.CLIENT_ATTRIBUTE, NumericFilterPredicate.NumericOperation.LESS_OR_EQUAL, 45); + List keyFiltersLessOrEqualTemperature = Collections.singletonList(lessOrEqualTemperatureFilter); + + KeyFilter equalTemperatureFilter = createNumericKeyFilter("temperature", EntityKeyType.CLIENT_ATTRIBUTE, NumericFilterPredicate.NumericOperation.EQUAL, 45); + List keyFiltersEqualTemperature = Collections.singletonList(equalTemperatureFilter); + + KeyFilter notEqualTemperatureFilter = createNumericKeyFilter("temperature", EntityKeyType.CLIENT_ATTRIBUTE, NumericFilterPredicate.NumericOperation.NOT_EQUAL, 45); + List keyFiltersNotEqualTemperature = Collections.singletonList(notEqualTemperatureFilter); + + //Greater Operation + + EntityDataPageLink pageLink = new EntityDataPageLink(100, 0, null, sortOrder); + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersGreaterTemperature); + PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + List loadedEntities = getLoadedEntities(data, query); + Assert.assertEquals(greaterTemperatures.size(), loadedEntities.size()); + + List loadedTemperatures = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList()); + List deviceTemperatures = greaterTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); + + Assert.assertEquals(deviceTemperatures, loadedTemperatures); + + //Greater or equal Operation + + pageLink = new EntityDataPageLink(100, 0, null, sortOrder); + query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersGreaterOrEqualTemperature); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities = getLoadedEntities(data, query); + Assert.assertEquals(greaterOrEqualTemperatures.size(), loadedEntities.size()); + + loadedTemperatures = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList()); + deviceTemperatures = greaterOrEqualTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); + + Assert.assertEquals(deviceTemperatures, loadedTemperatures); + + //Less Operation + + pageLink = new EntityDataPageLink(100, 0, null, sortOrder); + query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersLessTemperature); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities = getLoadedEntities(data, query); + Assert.assertEquals(lessTemperatures.size(), loadedEntities.size()); + + loadedTemperatures = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList()); + deviceTemperatures = lessTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); + + Assert.assertEquals(deviceTemperatures, loadedTemperatures); + + //Less or equal Operation + + pageLink = new EntityDataPageLink(100, 0, null, sortOrder); + query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersLessOrEqualTemperature); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities = getLoadedEntities(data, query); + Assert.assertEquals(lessOrEqualTemperatures.size(), loadedEntities.size()); + + loadedTemperatures = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList()); + deviceTemperatures = lessOrEqualTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); + + Assert.assertEquals(deviceTemperatures, loadedTemperatures); + + //Equal Operation + + pageLink = new EntityDataPageLink(100, 0, null, sortOrder); + query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersEqualTemperature); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities = getLoadedEntities(data, query); + Assert.assertEquals(equalTemperatures.size(), loadedEntities.size()); + + loadedTemperatures = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList()); + deviceTemperatures = equalTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); + + Assert.assertEquals(deviceTemperatures, loadedTemperatures); + + //Not equal Operation + + pageLink = new EntityDataPageLink(100, 0, null, sortOrder); + query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersNotEqualTemperature); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities = getLoadedEntities(data, query); + Assert.assertEquals(notEqualTemperatures.size(), loadedEntities.size()); + + loadedTemperatures = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList()); + deviceTemperatures = notEqualTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); + + Assert.assertEquals(deviceTemperatures, loadedTemperatures); + + + deviceService.deleteDevicesByTenantId(tenantId); + } + + @Test + public void testFindEntityDataByQueryWithTimeseries() throws ExecutionException, InterruptedException { + + List devices = new ArrayList<>(); + List temperatures = new ArrayList<>(); + List highTemperatures = new ArrayList<>(); + for (int i = 0; i < 67; i++) { + Device device = new Device(); + device.setTenantId(tenantId); + device.setName("Device" + i); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + devices.add(deviceService.saveDevice(device)); + //TO make sure devices have different created time + Thread.sleep(1); + double temperature = (double) (Math.random() * 100.0); + temperatures.add(temperature); + if (temperature > 45.0) { + highTemperatures.add(temperature); + } + } + + List> timeseriesFutures = new ArrayList<>(); + for (int i = 0; i < devices.size(); i++) { + Device device = devices.get(i); + timeseriesFutures.add(saveLongTimeseries(device.getId(), "temperature", temperatures.get(i))); + } + Futures.successfulAsList(timeseriesFutures).get(); + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceType("default"); + filter.setDeviceNameFilter(""); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC + ); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + List latestValues = Collections.singletonList(new EntityKey(EntityKeyType.TIME_SERIES, "temperature")); + + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null); + PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + + List loadedEntities = new ArrayList<>(data.getData()); + while (data.hasNext()) { + query = query.next(); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities.addAll(data.getData()); + } + Assert.assertEquals(67, loadedEntities.size()); + List loadedTemperatures = new ArrayList<>(); + for (Device device : devices) { + loadedTemperatures.add(loadedEntities.stream().filter(entityData -> entityData.getEntityId().equals(device.getId())).findFirst().orElse(null) + .getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()); + } + List deviceTemperatures = temperatures.stream().map(aDouble -> Double.toString(aDouble)).collect(Collectors.toList()); + if (DaoTestUtil.getSqlDbType(template) == SqlDbType.H2) { + // in H2 double values are stored with E0 in the end of the string + loadedTemperatures = loadedTemperatures.stream().map(s -> s.substring(0, s.length() - 2)).collect(Collectors.toList()); + } + Assert.assertEquals(deviceTemperatures, loadedTemperatures); + + pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + KeyFilter highTemperatureFilter = new KeyFilter(); + highTemperatureFilter.setKey(new EntityKey(EntityKeyType.TIME_SERIES, "temperature")); + NumericFilterPredicate predicate = new NumericFilterPredicate(); + predicate.setValue(FilterPredicateValue.fromDouble(45)); + predicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + highTemperatureFilter.setPredicate(predicate); + List keyFilters = Collections.singletonList(highTemperatureFilter); + + query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters); + + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + + loadedEntities = new ArrayList<>(data.getData()); + while (data.hasNext()) { + query = query.next(); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities.addAll(data.getData()); + } + Assert.assertEquals(highTemperatures.size(), loadedEntities.size()); + + List loadedHighTemperatures = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).collect(Collectors.toList()); + List deviceHighTemperatures = highTemperatures.stream().map(aDouble -> Double.toString(aDouble)).collect(Collectors.toList()); + + if (DaoTestUtil.getSqlDbType(template) == SqlDbType.H2) { + // in H2 double values are stored with E0 in the end of the string + loadedHighTemperatures = loadedHighTemperatures.stream().map(s -> s.substring(0, s.length() - 2)).collect(Collectors.toList()); + } + Assert.assertEquals(deviceHighTemperatures, loadedHighTemperatures); + + deviceService.deleteDevicesByTenantId(tenantId); + } + + @Test + public void testBuildStringPredicateQueryOperations() throws ExecutionException, InterruptedException{ + + List devices = new ArrayList<>(); + List attributeStrings = new ArrayList<>(); + List equalStrings = new ArrayList<>(); + List notEqualStrings = new ArrayList<>(); + List startsWithStrings = new ArrayList<>(); + List endsWithStrings = new ArrayList<>(); + List containsStrings = new ArrayList<>(); + List notContainsStrings = new ArrayList<>(); + + for (int i = 0; i < 10; i++) { + Device device = new Device(); + device.setTenantId(tenantId); + device.setName("Device" + i); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + devices.add(deviceService.saveDevice(device)); + //TO make sure devices have different created time + Thread.sleep(1); + List operationValues= Arrays.asList(StringFilterPredicate.StringOperation.values()); + StringFilterPredicate.StringOperation operation = operationValues.get(new Random().nextInt(operationValues.size())); + String operationName = operation.name(); + attributeStrings.add(operationName); + switch(operation){ + case EQUAL: + equalStrings.add(operationName); + notContainsStrings.add(operationName); + notEqualStrings.add(operationName); + break; + case NOT_EQUAL: + notContainsStrings.add(operationName); + break; + case STARTS_WITH: + notEqualStrings.add(operationName); + startsWithStrings.add(operationName); + endsWithStrings.add(operationName); + notContainsStrings.add(operationName); + break; + case ENDS_WITH: + notEqualStrings.add(operationName); + endsWithStrings.add(operationName); + notContainsStrings.add(operationName); + break; + case CONTAINS: + notEqualStrings.add(operationName); + notContainsStrings.add(operationName); + containsStrings.add(operationName); + break; + case NOT_CONTAINS: + notEqualStrings.add(operationName); + containsStrings.add(operationName); + break; + } + } + + List>> attributeFutures = new ArrayList<>(); + for (int i = 0; i < devices.size(); i++) { + Device device = devices.get(i); + attributeFutures.add(saveStringAttribute(device.getId(), "attributeString", attributeStrings.get(i), DataConstants.CLIENT_SCOPE)); + } + Futures.successfulAsList(attributeFutures).get(); + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceType("default"); + filter.setDeviceNameFilter(""); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.DESC + ); + + List entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), + new EntityKey(EntityKeyType.ENTITY_FIELD, "entityType")); + + List latestValues = Collections.singletonList(new EntityKey(EntityKeyType.CLIENT_ATTRIBUTE, "attributeString")); + + List keyFiltersEqualString = createStringKeyFilters("attributeString", EntityKeyType.CLIENT_ATTRIBUTE, StringFilterPredicate.StringOperation.EQUAL, "equal"); + + List keyFiltersNotEqualString = createStringKeyFilters("attributeString", EntityKeyType.CLIENT_ATTRIBUTE, StringFilterPredicate.StringOperation.NOT_EQUAL, "NOT_EQUAL"); + + List keyFiltersStartsWithString = createStringKeyFilters("attributeString", EntityKeyType.CLIENT_ATTRIBUTE, StringFilterPredicate.StringOperation.STARTS_WITH, "starts_"); + + List keyFiltersEndsWithString = createStringKeyFilters("attributeString", EntityKeyType.CLIENT_ATTRIBUTE, StringFilterPredicate.StringOperation.ENDS_WITH, "_WITH"); + + List keyFiltersContainsString = createStringKeyFilters("attributeString", EntityKeyType.CLIENT_ATTRIBUTE, StringFilterPredicate.StringOperation.CONTAINS, "contains"); + + List keyFiltersNotContainsString = createStringKeyFilters("attributeString", EntityKeyType.CLIENT_ATTRIBUTE, StringFilterPredicate.StringOperation.NOT_CONTAINS, "NOT_CONTAINS"); + + List deviceTypeFilters = createStringKeyFilters("entityType", EntityKeyType.ENTITY_FIELD, StringFilterPredicate.StringOperation.NOT_EQUAL, "NOT_EQUAL"); + + // Equal Operation + + EntityDataPageLink pageLink = new EntityDataPageLink(100, 0, null, sortOrder); + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersEqualString); + PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + List loadedEntities = getLoadedEntities(data, query); + Assert.assertEquals(equalStrings.size(), loadedEntities.size()); + + List loadedStrings = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("attributeString").getValue()).collect(Collectors.toList()); + + Assert.assertTrue(listEqualWithoutOrder(equalStrings, loadedStrings)); + + // Not equal Operation + + pageLink = new EntityDataPageLink(100, 0, null, sortOrder); + query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersNotEqualString); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities = getLoadedEntities(data, query); + Assert.assertEquals(notEqualStrings.size(), loadedEntities.size()); + + loadedStrings = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("attributeString").getValue()).collect(Collectors.toList()); + + Assert.assertTrue(listEqualWithoutOrder(notEqualStrings, loadedStrings)); + + // Starts with Operation + + pageLink = new EntityDataPageLink(100, 0, null, sortOrder); + query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersStartsWithString); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities = getLoadedEntities(data, query); + Assert.assertEquals(startsWithStrings.size(), loadedEntities.size()); + + loadedStrings = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("attributeString").getValue()).collect(Collectors.toList()); + + Assert.assertTrue(listEqualWithoutOrder(startsWithStrings, loadedStrings)); + + // Ends with Operation + + pageLink = new EntityDataPageLink(100, 0, null, sortOrder); + query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersEndsWithString); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities = getLoadedEntities(data, query); + Assert.assertEquals(endsWithStrings.size(), loadedEntities.size()); + + loadedStrings = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("attributeString").getValue()).collect(Collectors.toList()); + + Assert.assertTrue(listEqualWithoutOrder(endsWithStrings, loadedStrings)); + + // Contains Operation + + pageLink = new EntityDataPageLink(100, 0, null, sortOrder); + query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersContainsString); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities = getLoadedEntities(data, query); + Assert.assertEquals(containsStrings.size(), loadedEntities.size()); + + loadedStrings = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("attributeString").getValue()).collect(Collectors.toList()); + + Assert.assertTrue(listEqualWithoutOrder(containsStrings, loadedStrings)); + + // Not contains Operation + + pageLink = new EntityDataPageLink(100, 0, null, sortOrder); + query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersNotContainsString); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities = getLoadedEntities(data, query); + Assert.assertEquals(notContainsStrings.size(), loadedEntities.size()); + + loadedStrings = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("attributeString").getValue()).collect(Collectors.toList()); + + Assert.assertTrue(listEqualWithoutOrder(notContainsStrings, loadedStrings)); + + // Device type filters Operation + + pageLink = new EntityDataPageLink(100, 0, null, sortOrder); + query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, deviceTypeFilters); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities = getLoadedEntities(data, query); + Assert.assertEquals(devices.size(), loadedEntities.size()); + + deviceService.deleteDevicesByTenantId(tenantId); + } + + @Test + public void testBuildStringPredicateQueryOperationsForEntityType() throws ExecutionException, InterruptedException{ + + List devices = new ArrayList<>(); + + for (int i = 0; i < 10; i++) { + Device device = new Device(); + device.setTenantId(tenantId); + device.setName("Device" + i); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + devices.add(deviceService.saveDevice(device)); + //TO make sure devices have different created time + Thread.sleep(1); + } + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceType("default"); + filter.setDeviceNameFilter(""); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.DESC + ); + + List entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), + new EntityKey(EntityKeyType.ENTITY_FIELD, "entityType")); + + List keyFiltersEqualString = createStringKeyFilters("entityType", EntityKeyType.ENTITY_FIELD, StringFilterPredicate.StringOperation.EQUAL, "device"); + List keyFiltersNotEqualString = createStringKeyFilters("entityType", EntityKeyType.ENTITY_FIELD, StringFilterPredicate.StringOperation.NOT_EQUAL, "asset"); + List keyFiltersStartsWithString = createStringKeyFilters("entityType", EntityKeyType.ENTITY_FIELD, StringFilterPredicate.StringOperation.STARTS_WITH, "dev"); + List keyFiltersEndsWithString = createStringKeyFilters("entityType", EntityKeyType.ENTITY_FIELD, StringFilterPredicate.StringOperation.ENDS_WITH, "ice"); + List keyFiltersContainsString = createStringKeyFilters("entityType", EntityKeyType.ENTITY_FIELD, StringFilterPredicate.StringOperation.CONTAINS, "vic"); + List keyFiltersNotContainsString = createStringKeyFilters("entityType", EntityKeyType.ENTITY_FIELD, StringFilterPredicate.StringOperation.NOT_CONTAINS, "dolphin"); + + // Equal Operation + + EntityDataPageLink pageLink = new EntityDataPageLink(100, 0, null, sortOrder); + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, keyFiltersEqualString); + PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + List loadedEntities = getLoadedEntities(data, query); + Assert.assertEquals(devices.size(), loadedEntities.size()); + + List loadedStrings = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()).collect(Collectors.toList()); + + List devicesNames = devices.stream().map(Device::getName).collect(Collectors.toList()); + + Assert.assertTrue(listEqualWithoutOrder(devicesNames, loadedStrings)); + + // Not equal Operation + + pageLink = new EntityDataPageLink(100, 0, null, sortOrder); + query = new EntityDataQuery(filter, pageLink, entityFields, null, keyFiltersNotEqualString); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities = getLoadedEntities(data, query); + Assert.assertEquals(devices.size(), loadedEntities.size()); + + loadedStrings = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()).collect(Collectors.toList()); + + Assert.assertTrue(listEqualWithoutOrder(devicesNames, loadedStrings)); + + // Starts with Operation + + pageLink = new EntityDataPageLink(100, 0, null, sortOrder); + query = new EntityDataQuery(filter, pageLink, entityFields, null, keyFiltersStartsWithString); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities = getLoadedEntities(data, query); + Assert.assertEquals(devices.size(), loadedEntities.size()); + + loadedStrings = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()).collect(Collectors.toList()); + + Assert.assertTrue(listEqualWithoutOrder(devicesNames, loadedStrings)); + + // Ends with Operation + + pageLink = new EntityDataPageLink(100, 0, null, sortOrder); + query = new EntityDataQuery(filter, pageLink, entityFields, null, keyFiltersEndsWithString); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities = getLoadedEntities(data, query); + Assert.assertEquals(devices.size(), loadedEntities.size()); + + loadedStrings = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()).collect(Collectors.toList()); + + Assert.assertTrue(listEqualWithoutOrder(devicesNames, loadedStrings)); + + // Contains Operation + + pageLink = new EntityDataPageLink(100, 0, null, sortOrder); + query = new EntityDataQuery(filter, pageLink, entityFields, null, keyFiltersContainsString); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities = getLoadedEntities(data, query); + Assert.assertEquals(devices.size(), loadedEntities.size()); + + loadedStrings = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()).collect(Collectors.toList()); + + Assert.assertTrue(listEqualWithoutOrder(devicesNames, loadedStrings)); + + // Not contains Operation + + pageLink = new EntityDataPageLink(100, 0, null, sortOrder); + query = new EntityDataQuery(filter, pageLink, entityFields, null, keyFiltersNotContainsString); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities = getLoadedEntities(data, query); + Assert.assertEquals(devices.size(), loadedEntities.size()); + + loadedStrings = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()).collect(Collectors.toList()); + + Assert.assertTrue(listEqualWithoutOrder(devicesNames, loadedStrings)); + + deviceService.deleteDevicesByTenantId(tenantId); + } + + @Test + public void testBuildSimplePredicateQueryOperations() throws InterruptedException{ + + List devices = new ArrayList<>(); + + for (int i = 0; i < 10; i++) { + Device device = new Device(); + device.setTenantId(tenantId); + device.setName("Device" + i); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + devices.add(deviceService.saveDevice(device)); + //TO make sure devices have different created time + Thread.sleep(1); + } + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceType("default"); + filter.setDeviceNameFilter(""); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), EntityDataSortOrder.Direction.DESC); + + List deviceTypeFilters = createStringKeyFilters("type", EntityKeyType.ENTITY_FIELD, StringFilterPredicate.StringOperation.EQUAL, "default"); + + KeyFilter createdTimeFilter = createNumericKeyFilter("createdTime", EntityKeyType.ENTITY_FIELD, NumericFilterPredicate.NumericOperation.GREATER, 1L); + List createdTimeFilters = Collections.singletonList(createdTimeFilter); + + List nameFilters = createStringKeyFilters("name", EntityKeyType.ENTITY_FIELD, StringFilterPredicate.StringOperation.CONTAINS, "Device"); + + List entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), + new EntityKey(EntityKeyType.ENTITY_FIELD, "type")); + + // Device type filters + + EntityDataPageLink pageLink = new EntityDataPageLink(100, 0, null, sortOrder); + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, deviceTypeFilters); + PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + List loadedEntities = getLoadedEntities(data, query); + Assert.assertEquals(devices.size(), loadedEntities.size()); + + // Device create time filters + + pageLink = new EntityDataPageLink(100, 0, null, sortOrder); + query = new EntityDataQuery(filter, pageLink, entityFields, null, createdTimeFilters); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities = getLoadedEntities(data, query); + Assert.assertEquals(devices.size(), loadedEntities.size()); + + // Device name filters + + pageLink = new EntityDataPageLink(100, 0, null, null); + query = new EntityDataQuery(filter, pageLink, entityFields, null, nameFilters); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities = getLoadedEntities(data, query); + Assert.assertEquals(devices.size(), loadedEntities.size()); + + deviceService.deleteDevicesByTenantId(tenantId); + } + + private Boolean listEqualWithoutOrder(List A, List B) { + return A.containsAll(B) && B.containsAll(A); + } + + private List getLoadedEntities(PageData data, EntityDataQuery query) { + List loadedEntities = new ArrayList<>(data.getData()); + + while (data.hasNext()) { + query = query.next(); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities.addAll(data.getData()); + } + return loadedEntities; + } + + private List createStringKeyFilters(String key, EntityKeyType keyType, StringFilterPredicate.StringOperation operation, String value){ + KeyFilter filter = new KeyFilter(); + filter.setKey(new EntityKey(keyType, key)); + StringFilterPredicate predicate = new StringFilterPredicate(); + predicate.setValue(FilterPredicateValue.fromString(value)); + predicate.setOperation(operation); + predicate.setIgnoreCase(true); + filter.setPredicate(predicate); + return Collections.singletonList(filter); + } + + private KeyFilter createNumericKeyFilter(String key, EntityKeyType keyType, NumericFilterPredicate.NumericOperation operation, double value){ + KeyFilter filter = new KeyFilter(); + filter.setKey(new EntityKey(keyType, key)); + NumericFilterPredicate predicate = new NumericFilterPredicate(); + predicate.setValue(FilterPredicateValue.fromDouble(value)); + predicate.setOperation(operation); + filter.setPredicate(predicate); + + return filter; + } + + private ListenableFuture> saveLongAttribute(EntityId entityId, String key, long value, String scope) { + KvEntry attrValue = new LongDataEntry(key, value); + AttributeKvEntry attr = new BaseAttributeKvEntry(attrValue, 42L); + return attributesService.save(SYSTEM_TENANT_ID, entityId, scope, Collections.singletonList(attr)); + } + + private ListenableFuture> saveStringAttribute(EntityId entityId, String key, String value, String scope) { + KvEntry attrValue = new StringDataEntry(key, value); + AttributeKvEntry attr = new BaseAttributeKvEntry(attrValue, 42L); + return attributesService.save(SYSTEM_TENANT_ID, entityId, scope, Collections.singletonList(attr)); + } + + private ListenableFuture saveLongTimeseries(EntityId entityId, String key, Double value) { + TsKvEntity tsKv = new TsKvEntity(); + tsKv.setStrKey(key); + tsKv.setDoubleValue(value); + KvEntry telemetryValue = new DoubleDataEntry(key, value); + BasicTsKvEntry timeseries = new BasicTsKvEntry(42L, telemetryValue); + return timeseriesService.save(SYSTEM_TENANT_ID, entityId, timeseries); + } +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ConfigTemplateServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ConfigTemplateServiceTest.java new file mode 100644 index 0000000000..15c9172e83 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ConfigTemplateServiceTest.java @@ -0,0 +1,135 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.oauth2.MapperType; +import org.thingsboard.server.common.data.oauth2.OAuth2BasicMapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; +import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; + +import java.util.Arrays; +import java.util.UUID; + +public class BaseOAuth2ConfigTemplateServiceTest extends AbstractServiceTest { + + @Autowired + protected OAuth2ConfigTemplateService oAuth2ConfigTemplateService; + + @Before + public void beforeRun() throws Exception { + Assert.assertTrue(oAuth2ConfigTemplateService.findAllClientRegistrationTemplates().isEmpty()); + } + + @After + public void after() throws Exception { + oAuth2ConfigTemplateService.findAllClientRegistrationTemplates().forEach(clientRegistrationTemplate -> { + oAuth2ConfigTemplateService.deleteClientRegistrationTemplateById(clientRegistrationTemplate.getId()); + }); + + Assert.assertTrue(oAuth2ConfigTemplateService.findAllClientRegistrationTemplates().isEmpty()); + } + + + @Test(expected = DataValidationException.class) + public void testSaveDuplicateProviderId() { + OAuth2ClientRegistrationTemplate first = validClientRegistrationTemplate("providerId"); + OAuth2ClientRegistrationTemplate second = validClientRegistrationTemplate("providerId"); + oAuth2ConfigTemplateService.saveClientRegistrationTemplate(first); + oAuth2ConfigTemplateService.saveClientRegistrationTemplate(second); + } + + @Test + public void testCreateNewTemplate() { + OAuth2ClientRegistrationTemplate clientRegistrationTemplate = validClientRegistrationTemplate(UUID.randomUUID().toString()); + OAuth2ClientRegistrationTemplate savedClientRegistrationTemplate = oAuth2ConfigTemplateService.saveClientRegistrationTemplate(clientRegistrationTemplate); + + Assert.assertNotNull(savedClientRegistrationTemplate); + Assert.assertNotNull(savedClientRegistrationTemplate.getId()); + clientRegistrationTemplate.setId(savedClientRegistrationTemplate.getId()); + clientRegistrationTemplate.setCreatedTime(savedClientRegistrationTemplate.getCreatedTime()); + Assert.assertEquals(clientRegistrationTemplate, savedClientRegistrationTemplate); + } + + @Test + public void testFindTemplate() { + OAuth2ClientRegistrationTemplate clientRegistrationTemplate = validClientRegistrationTemplate(UUID.randomUUID().toString()); + OAuth2ClientRegistrationTemplate savedClientRegistrationTemplate = oAuth2ConfigTemplateService.saveClientRegistrationTemplate(clientRegistrationTemplate); + + OAuth2ClientRegistrationTemplate foundClientRegistrationTemplate = oAuth2ConfigTemplateService.findClientRegistrationTemplateById(savedClientRegistrationTemplate.getId()); + Assert.assertEquals(savedClientRegistrationTemplate, foundClientRegistrationTemplate); + } + + @Test + public void testFindAll() { + oAuth2ConfigTemplateService.saveClientRegistrationTemplate(validClientRegistrationTemplate(UUID.randomUUID().toString())); + oAuth2ConfigTemplateService.saveClientRegistrationTemplate(validClientRegistrationTemplate(UUID.randomUUID().toString())); + + Assert.assertEquals(2, oAuth2ConfigTemplateService.findAllClientRegistrationTemplates().size()); + } + + @Test + public void testDeleteTemplate() { + oAuth2ConfigTemplateService.saveClientRegistrationTemplate(validClientRegistrationTemplate(UUID.randomUUID().toString())); + oAuth2ConfigTemplateService.saveClientRegistrationTemplate(validClientRegistrationTemplate(UUID.randomUUID().toString())); + OAuth2ClientRegistrationTemplate saved = oAuth2ConfigTemplateService.saveClientRegistrationTemplate(validClientRegistrationTemplate(UUID.randomUUID().toString())); + + Assert.assertEquals(3, oAuth2ConfigTemplateService.findAllClientRegistrationTemplates().size()); + Assert.assertNotNull(oAuth2ConfigTemplateService.findClientRegistrationTemplateById(saved.getId())); + + oAuth2ConfigTemplateService.deleteClientRegistrationTemplateById(saved.getId()); + + Assert.assertEquals(2, oAuth2ConfigTemplateService.findAllClientRegistrationTemplates().size()); + Assert.assertNull(oAuth2ConfigTemplateService.findClientRegistrationTemplateById(saved.getId())); + } + + private OAuth2ClientRegistrationTemplate validClientRegistrationTemplate(String providerId) { + OAuth2ClientRegistrationTemplate clientRegistrationTemplate = new OAuth2ClientRegistrationTemplate(); + clientRegistrationTemplate.setProviderId(providerId); + clientRegistrationTemplate.setAdditionalInfo(mapper.createObjectNode().put(UUID.randomUUID().toString(), UUID.randomUUID().toString())); + clientRegistrationTemplate.setMapperConfig(OAuth2MapperConfig.builder() + .type(MapperType.BASIC) + .basic(OAuth2BasicMapperConfig.builder() + .firstNameAttributeKey("firstName") + .lastNameAttributeKey("lastName") + .emailAttributeKey("email") + .tenantNamePattern("tenant") + .defaultDashboardName("Test") + .alwaysFullScreen(true) + .build() + ) + .build()); + clientRegistrationTemplate.setAuthorizationUri("authorizationUri"); + clientRegistrationTemplate.setAccessTokenUri("tokenUri"); + clientRegistrationTemplate.setScope(Arrays.asList("scope1", "scope2")); + clientRegistrationTemplate.setUserInfoUri("userInfoUri"); + clientRegistrationTemplate.setUserNameAttributeName("userNameAttributeName"); + clientRegistrationTemplate.setJwkSetUri("jwkSetUri"); + clientRegistrationTemplate.setClientAuthenticationMethod("clientAuthenticationMethod"); + clientRegistrationTemplate.setComment("comment"); + clientRegistrationTemplate.setLoginButtonIcon("icon"); + clientRegistrationTemplate.setLoginButtonLabel("label"); + clientRegistrationTemplate.setHelpLink("helpLink"); + return clientRegistrationTemplate; + } +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java new file mode 100644 index 0000000000..a927f1e78c --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java @@ -0,0 +1,525 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.common.data.oauth2.*; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.oauth2.OAuth2Service; + +import java.util.*; +import java.util.stream.Collectors; + +public class BaseOAuth2ServiceTest extends AbstractServiceTest { + private static final OAuth2ClientsParams EMPTY_PARAMS = new OAuth2ClientsParams(false, new ArrayList<>()); + + @Autowired + protected OAuth2Service oAuth2Service; + + @Before + public void beforeRun() { + Assert.assertTrue(oAuth2Service.findAllClientRegistrationInfos().isEmpty()); + } + + @After + public void after() { + oAuth2Service.saveOAuth2Params(EMPTY_PARAMS); + Assert.assertTrue(oAuth2Service.findAllClientRegistrationInfos().isEmpty()); + Assert.assertTrue(oAuth2Service.findOAuth2Params().getDomainsParams().isEmpty()); + } + + @Test(expected = DataValidationException.class) + public void testSaveHttpAndMixedDomainsTogether() { + OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Lists.newArrayList( + OAuth2ClientsDomainParams.builder() + .domainInfos(Lists.newArrayList( + DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), + DomainInfo.builder().name("first-domain").scheme(SchemeType.MIXED).build(), + DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() + )) + .clientRegistrations(Lists.newArrayList( + validClientRegistrationDto(), + validClientRegistrationDto(), + validClientRegistrationDto() + )) + .build() + )); + oAuth2Service.saveOAuth2Params(clientsParams); + } + + @Test(expected = DataValidationException.class) + public void testSaveHttpsAndMixedDomainsTogether() { + OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Lists.newArrayList( + OAuth2ClientsDomainParams.builder() + .domainInfos(Lists.newArrayList( + DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTPS).build(), + DomainInfo.builder().name("first-domain").scheme(SchemeType.MIXED).build(), + DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() + )) + .clientRegistrations(Lists.newArrayList( + validClientRegistrationDto(), + validClientRegistrationDto(), + validClientRegistrationDto() + )) + .build() + )); + oAuth2Service.saveOAuth2Params(clientsParams); + } + + @Test + public void testCreateAndFindParams() { + OAuth2ClientsParams clientsParams = createDefaultClientsParams(); + oAuth2Service.saveOAuth2Params(clientsParams); + OAuth2ClientsParams foundClientsParams = oAuth2Service.findOAuth2Params(); + Assert.assertNotNull(foundClientsParams); + // TODO ask if it's safe to check equality on AdditionalProperties + Assert.assertEquals(clientsParams, foundClientsParams); + } + + @Test + public void testDisableParams() { + OAuth2ClientsParams clientsParams = createDefaultClientsParams(); + clientsParams.setEnabled(true); + oAuth2Service.saveOAuth2Params(clientsParams); + OAuth2ClientsParams foundClientsParams = oAuth2Service.findOAuth2Params(); + Assert.assertNotNull(foundClientsParams); + Assert.assertEquals(clientsParams, foundClientsParams); + + clientsParams.setEnabled(false); + oAuth2Service.saveOAuth2Params(clientsParams); + OAuth2ClientsParams foundDisabledClientsParams = oAuth2Service.findOAuth2Params(); + Assert.assertEquals(clientsParams, foundDisabledClientsParams); + } + + @Test + public void testClearDomainParams() { + OAuth2ClientsParams clientsParams = createDefaultClientsParams(); + oAuth2Service.saveOAuth2Params(clientsParams); + OAuth2ClientsParams foundClientsParams = oAuth2Service.findOAuth2Params(); + Assert.assertNotNull(foundClientsParams); + Assert.assertEquals(clientsParams, foundClientsParams); + + oAuth2Service.saveOAuth2Params(EMPTY_PARAMS); + OAuth2ClientsParams foundAfterClearClientsParams = oAuth2Service.findOAuth2Params(); + Assert.assertNotNull(foundAfterClearClientsParams); + Assert.assertEquals(EMPTY_PARAMS, foundAfterClearClientsParams); + } + + @Test + public void testUpdateClientsParams() { + OAuth2ClientsParams clientsParams = createDefaultClientsParams(); + oAuth2Service.saveOAuth2Params(clientsParams); + OAuth2ClientsParams foundClientsParams = oAuth2Service.findOAuth2Params(); + Assert.assertNotNull(foundClientsParams); + Assert.assertEquals(clientsParams, foundClientsParams); + + OAuth2ClientsParams newClientsParams = new OAuth2ClientsParams(true, Lists.newArrayList( + OAuth2ClientsDomainParams.builder() + .domainInfos(Lists.newArrayList( + DomainInfo.builder().name("another-domain").scheme(SchemeType.HTTPS).build() + )) + .clientRegistrations(Lists.newArrayList( + validClientRegistrationDto() + )) + .build(), + OAuth2ClientsDomainParams.builder() + .domainInfos(Lists.newArrayList( + DomainInfo.builder().name("test-domain").scheme(SchemeType.MIXED).build() + )) + .clientRegistrations(Lists.newArrayList( + validClientRegistrationDto() + )) + .build() + )); + oAuth2Service.saveOAuth2Params(newClientsParams); + OAuth2ClientsParams foundAfterUpdateClientsParams = oAuth2Service.findOAuth2Params(); + Assert.assertNotNull(foundAfterUpdateClientsParams); + Assert.assertEquals(newClientsParams, foundAfterUpdateClientsParams); + } + + @Test + public void testGetOAuth2Clients() { + List firstGroup = Lists.newArrayList( + validClientRegistrationDto(), + validClientRegistrationDto(), + validClientRegistrationDto(), + validClientRegistrationDto() + ); + List secondGroup = Lists.newArrayList( + validClientRegistrationDto(), + validClientRegistrationDto() + ); + List thirdGroup = Lists.newArrayList( + validClientRegistrationDto() + ); + OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Lists.newArrayList( + OAuth2ClientsDomainParams.builder() + .domainInfos(Lists.newArrayList( + DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), + DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), + DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() + )) + .clientRegistrations(firstGroup) + .build(), + OAuth2ClientsDomainParams.builder() + .domainInfos(Lists.newArrayList( + DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(), + DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() + )) + .clientRegistrations(secondGroup) + .build(), + OAuth2ClientsDomainParams.builder() + .domainInfos(Lists.newArrayList( + DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTPS).build(), + DomainInfo.builder().name("fifth-domain").scheme(SchemeType.HTTP).build() + )) + .clientRegistrations(thirdGroup) + .build() + )); + + oAuth2Service.saveOAuth2Params(clientsParams); + OAuth2ClientsParams foundClientsParams = oAuth2Service.findOAuth2Params(); + Assert.assertNotNull(foundClientsParams); + Assert.assertEquals(clientsParams, foundClientsParams); + + List firstGroupClientInfos = firstGroup.stream() + .map(clientRegistrationDto -> new OAuth2ClientInfo( + clientRegistrationDto.getLoginButtonLabel(), clientRegistrationDto.getLoginButtonIcon(), null)) + .collect(Collectors.toList()); + List secondGroupClientInfos = secondGroup.stream() + .map(clientRegistrationDto -> new OAuth2ClientInfo( + clientRegistrationDto.getLoginButtonLabel(), clientRegistrationDto.getLoginButtonIcon(), null)) + .collect(Collectors.toList()); + List thirdGroupClientInfos = thirdGroup.stream() + .map(clientRegistrationDto -> new OAuth2ClientInfo( + clientRegistrationDto.getLoginButtonLabel(), clientRegistrationDto.getLoginButtonIcon(), null)) + .collect(Collectors.toList()); + + List nonExistentDomainClients = oAuth2Service.getOAuth2Clients("http", "non-existent-domain"); + Assert.assertTrue(nonExistentDomainClients.isEmpty()); + + List firstDomainHttpClients = oAuth2Service.getOAuth2Clients("http", "first-domain"); + Assert.assertEquals(firstGroupClientInfos.size(), firstDomainHttpClients.size()); + firstGroupClientInfos.forEach(firstGroupClientInfo -> { + Assert.assertTrue( + firstDomainHttpClients.stream().anyMatch(clientInfo -> + clientInfo.getIcon().equals(firstGroupClientInfo.getIcon()) + && clientInfo.getName().equals(firstGroupClientInfo.getName())) + ); + }); + + List firstDomainHttpsClients = oAuth2Service.getOAuth2Clients("https", "first-domain"); + Assert.assertTrue(firstDomainHttpsClients.isEmpty()); + + List fourthDomainHttpClients = oAuth2Service.getOAuth2Clients("http", "fourth-domain"); + Assert.assertEquals(secondGroupClientInfos.size(), fourthDomainHttpClients.size()); + secondGroupClientInfos.forEach(secondGroupClientInfo -> { + Assert.assertTrue( + fourthDomainHttpClients.stream().anyMatch(clientInfo -> + clientInfo.getIcon().equals(secondGroupClientInfo.getIcon()) + && clientInfo.getName().equals(secondGroupClientInfo.getName())) + ); + }); + List fourthDomainHttpsClients = oAuth2Service.getOAuth2Clients("https", "fourth-domain"); + Assert.assertEquals(secondGroupClientInfos.size(), fourthDomainHttpsClients.size()); + secondGroupClientInfos.forEach(secondGroupClientInfo -> { + Assert.assertTrue( + fourthDomainHttpsClients.stream().anyMatch(clientInfo -> + clientInfo.getIcon().equals(secondGroupClientInfo.getIcon()) + && clientInfo.getName().equals(secondGroupClientInfo.getName())) + ); + }); + + List secondDomainHttpClients = oAuth2Service.getOAuth2Clients("http", "second-domain"); + Assert.assertEquals(firstGroupClientInfos.size() + secondGroupClientInfos.size(), secondDomainHttpClients.size()); + firstGroupClientInfos.forEach(firstGroupClientInfo -> { + Assert.assertTrue( + secondDomainHttpClients.stream().anyMatch(clientInfo -> + clientInfo.getIcon().equals(firstGroupClientInfo.getIcon()) + && clientInfo.getName().equals(firstGroupClientInfo.getName())) + ); + }); + secondGroupClientInfos.forEach(secondGroupClientInfo -> { + Assert.assertTrue( + secondDomainHttpClients.stream().anyMatch(clientInfo -> + clientInfo.getIcon().equals(secondGroupClientInfo.getIcon()) + && clientInfo.getName().equals(secondGroupClientInfo.getName())) + ); + }); + + List secondDomainHttpsClients = oAuth2Service.getOAuth2Clients("https", "second-domain"); + Assert.assertEquals(firstGroupClientInfos.size() + thirdGroupClientInfos.size(), secondDomainHttpsClients.size()); + firstGroupClientInfos.forEach(firstGroupClientInfo -> { + Assert.assertTrue( + secondDomainHttpsClients.stream().anyMatch(clientInfo -> + clientInfo.getIcon().equals(firstGroupClientInfo.getIcon()) + && clientInfo.getName().equals(firstGroupClientInfo.getName())) + ); + }); + thirdGroupClientInfos.forEach(thirdGroupClientInfo -> { + Assert.assertTrue( + secondDomainHttpsClients.stream().anyMatch(clientInfo -> + clientInfo.getIcon().equals(thirdGroupClientInfo.getIcon()) + && clientInfo.getName().equals(thirdGroupClientInfo.getName())) + ); + }); + } + + @Test + public void testGetOAuth2ClientsForHttpAndHttps() { + List firstGroup = Lists.newArrayList( + validClientRegistrationDto(), + validClientRegistrationDto(), + validClientRegistrationDto(), + validClientRegistrationDto() + ); + OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Lists.newArrayList( + OAuth2ClientsDomainParams.builder() + .domainInfos(Lists.newArrayList( + DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), + DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), + DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTPS).build() + )) + .clientRegistrations(firstGroup) + .build() + )); + + oAuth2Service.saveOAuth2Params(clientsParams); + OAuth2ClientsParams foundClientsParams = oAuth2Service.findOAuth2Params(); + Assert.assertNotNull(foundClientsParams); + Assert.assertEquals(clientsParams, foundClientsParams); + + List firstGroupClientInfos = firstGroup.stream() + .map(clientRegistrationDto -> new OAuth2ClientInfo( + clientRegistrationDto.getLoginButtonLabel(), clientRegistrationDto.getLoginButtonIcon(), null)) + .collect(Collectors.toList()); + + List firstDomainHttpClients = oAuth2Service.getOAuth2Clients("http", "first-domain"); + Assert.assertEquals(firstGroupClientInfos.size(), firstDomainHttpClients.size()); + firstGroupClientInfos.forEach(firstGroupClientInfo -> { + Assert.assertTrue( + firstDomainHttpClients.stream().anyMatch(clientInfo -> + clientInfo.getIcon().equals(firstGroupClientInfo.getIcon()) + && clientInfo.getName().equals(firstGroupClientInfo.getName())) + ); + }); + + List firstDomainHttpsClients = oAuth2Service.getOAuth2Clients("https", "first-domain"); + Assert.assertEquals(firstGroupClientInfos.size(), firstDomainHttpsClients.size()); + firstGroupClientInfos.forEach(firstGroupClientInfo -> { + Assert.assertTrue( + firstDomainHttpsClients.stream().anyMatch(clientInfo -> + clientInfo.getIcon().equals(firstGroupClientInfo.getIcon()) + && clientInfo.getName().equals(firstGroupClientInfo.getName())) + ); + }); + } + + @Test + public void testGetDisabledOAuth2Clients() { + OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Lists.newArrayList( + OAuth2ClientsDomainParams.builder() + .domainInfos(Lists.newArrayList( + DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), + DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), + DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() + )) + .clientRegistrations(Lists.newArrayList( + validClientRegistrationDto(), + validClientRegistrationDto(), + validClientRegistrationDto() + )) + .build(), + OAuth2ClientsDomainParams.builder() + .domainInfos(Lists.newArrayList( + DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(), + DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() + )) + .clientRegistrations(Lists.newArrayList( + validClientRegistrationDto(), + validClientRegistrationDto() + )) + .build() + )); + + oAuth2Service.saveOAuth2Params(clientsParams); + + List secondDomainHttpClients = oAuth2Service.getOAuth2Clients("http", "second-domain"); + Assert.assertEquals(5, secondDomainHttpClients.size()); + + clientsParams.setEnabled(false); + oAuth2Service.saveOAuth2Params(clientsParams); + + List secondDomainHttpDisabledClients = oAuth2Service.getOAuth2Clients("http", "second-domain"); + Assert.assertEquals(0, secondDomainHttpDisabledClients.size()); + } + + @Test + public void testFindAllClientRegistrationInfos() { + OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Lists.newArrayList( + OAuth2ClientsDomainParams.builder() + .domainInfos(Lists.newArrayList( + DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), + DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), + DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() + )) + .clientRegistrations(Lists.newArrayList( + validClientRegistrationDto(), + validClientRegistrationDto(), + validClientRegistrationDto() + )) + .build(), + OAuth2ClientsDomainParams.builder() + .domainInfos(Lists.newArrayList( + DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(), + DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() + )) + .clientRegistrations(Lists.newArrayList( + validClientRegistrationDto(), + validClientRegistrationDto() + )) + .build(), + OAuth2ClientsDomainParams.builder() + .domainInfos(Lists.newArrayList( + DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTPS).build(), + DomainInfo.builder().name("fifth-domain").scheme(SchemeType.HTTP).build() + )) + .clientRegistrations(Lists.newArrayList( + validClientRegistrationDto() + )) + .build() + )); + + oAuth2Service.saveOAuth2Params(clientsParams); + List foundClientRegistrationInfos = oAuth2Service.findAllClientRegistrationInfos(); + Assert.assertEquals(6, foundClientRegistrationInfos.size()); + clientsParams.getDomainsParams().stream() + .flatMap(domainParams -> domainParams.getClientRegistrations().stream()) + .forEach(clientRegistrationDto -> + Assert.assertTrue( + foundClientRegistrationInfos.stream() + .anyMatch(clientRegistrationInfo -> clientRegistrationInfo.getClientId().equals(clientRegistrationDto.getClientId())) + ) + ); + } + + @Test + public void testFindClientRegistrationById() { + OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Lists.newArrayList( + OAuth2ClientsDomainParams.builder() + .domainInfos(Lists.newArrayList( + DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), + DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), + DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() + )) + .clientRegistrations(Lists.newArrayList( + validClientRegistrationDto(), + validClientRegistrationDto(), + validClientRegistrationDto() + )) + .build(), + OAuth2ClientsDomainParams.builder() + .domainInfos(Lists.newArrayList( + DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(), + DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() + )) + .clientRegistrations(Lists.newArrayList( + validClientRegistrationDto(), + validClientRegistrationDto() + )) + .build(), + OAuth2ClientsDomainParams.builder() + .domainInfos(Lists.newArrayList( + DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTPS).build(), + DomainInfo.builder().name("fifth-domain").scheme(SchemeType.HTTP).build() + )) + .clientRegistrations(Lists.newArrayList( + validClientRegistrationDto() + )) + .build() + )); + + oAuth2Service.saveOAuth2Params(clientsParams); + List clientRegistrationInfos = oAuth2Service.findAllClientRegistrationInfos(); + clientRegistrationInfos.forEach(clientRegistrationInfo -> { + OAuth2ClientRegistrationInfo foundClientRegistrationInfo = oAuth2Service.findClientRegistrationInfo(clientRegistrationInfo.getUuidId()); + Assert.assertEquals(clientRegistrationInfo, foundClientRegistrationInfo); + }); + } + + private OAuth2ClientsParams createDefaultClientsParams() { + return new OAuth2ClientsParams(true, Lists.newArrayList( + OAuth2ClientsDomainParams.builder() + .domainInfos(Lists.newArrayList( + DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), + DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), + DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() + )) + .clientRegistrations(Lists.newArrayList( + validClientRegistrationDto(), + validClientRegistrationDto(), + validClientRegistrationDto(), + validClientRegistrationDto() + )) + .build(), + OAuth2ClientsDomainParams.builder() + .domainInfos(Lists.newArrayList( + DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), + DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() + )) + .clientRegistrations(Lists.newArrayList( + validClientRegistrationDto(), + validClientRegistrationDto() + )) + .build() + )); + } + + private ClientRegistrationDto validClientRegistrationDto() { + return ClientRegistrationDto.builder() + .clientId(UUID.randomUUID().toString()) + .clientSecret(UUID.randomUUID().toString()) + .authorizationUri(UUID.randomUUID().toString()) + .accessTokenUri(UUID.randomUUID().toString()) + .scope(Arrays.asList(UUID.randomUUID().toString(), UUID.randomUUID().toString())) + .userInfoUri(UUID.randomUUID().toString()) + .userNameAttributeName(UUID.randomUUID().toString()) + .jwkSetUri(UUID.randomUUID().toString()) + .clientAuthenticationMethod(UUID.randomUUID().toString()) + .loginButtonLabel(UUID.randomUUID().toString()) + .loginButtonIcon(UUID.randomUUID().toString()) + .additionalInfo(mapper.createObjectNode().put(UUID.randomUUID().toString(), UUID.randomUUID().toString())) + .mapperConfig( + OAuth2MapperConfig.builder() + .allowUserCreation(true) + .activateUser(true) + .type(MapperType.CUSTOM) + .custom( + OAuth2CustomMapperConfig.builder() + .url(UUID.randomUUID().toString()) + .build() + ) + .build() + ) + .build(); + } +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java index 928bd40f77..5e05e6cf81 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.dao.service; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -47,8 +47,8 @@ public abstract class BaseRelationServiceTest extends AbstractServiceTest { @Test public void testSaveRelation() throws ExecutionException, InterruptedException { - AssetId parentId = new AssetId(UUIDs.timeBased()); - AssetId childId = new AssetId(UUIDs.timeBased()); + AssetId parentId = new AssetId(Uuids.timeBased()); + AssetId childId = new AssetId(Uuids.timeBased()); EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE); @@ -65,9 +65,9 @@ public abstract class BaseRelationServiceTest extends AbstractServiceTest { @Test public void testDeleteRelation() throws ExecutionException, InterruptedException { - AssetId parentId = new AssetId(UUIDs.timeBased()); - AssetId childId = new AssetId(UUIDs.timeBased()); - AssetId subChildId = new AssetId(UUIDs.timeBased()); + AssetId parentId = new AssetId(Uuids.timeBased()); + AssetId childId = new AssetId(Uuids.timeBased()); + AssetId subChildId = new AssetId(Uuids.timeBased()); EntityRelation relationA = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE); EntityRelation relationB = new EntityRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE); @@ -86,9 +86,9 @@ public abstract class BaseRelationServiceTest extends AbstractServiceTest { @Test public void testDeleteEntityRelations() throws ExecutionException, InterruptedException { - AssetId parentId = new AssetId(UUIDs.timeBased()); - AssetId childId = new AssetId(UUIDs.timeBased()); - AssetId subChildId = new AssetId(UUIDs.timeBased()); + AssetId parentId = new AssetId(Uuids.timeBased()); + AssetId childId = new AssetId(Uuids.timeBased()); + AssetId subChildId = new AssetId(Uuids.timeBased()); EntityRelation relationA = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE); EntityRelation relationB = new EntityRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE); @@ -105,10 +105,10 @@ public abstract class BaseRelationServiceTest extends AbstractServiceTest { @Test public void testFindFrom() throws ExecutionException, InterruptedException { - AssetId parentA = new AssetId(UUIDs.timeBased()); - AssetId parentB = new AssetId(UUIDs.timeBased()); - AssetId childA = new AssetId(UUIDs.timeBased()); - AssetId childB = new AssetId(UUIDs.timeBased()); + AssetId parentA = new AssetId(Uuids.timeBased()); + AssetId parentB = new AssetId(Uuids.timeBased()); + AssetId childA = new AssetId(Uuids.timeBased()); + AssetId childB = new AssetId(Uuids.timeBased()); EntityRelation relationA1 = new EntityRelation(parentA, childA, EntityRelation.CONTAINS_TYPE); EntityRelation relationA2 = new EntityRelation(parentA, childB, EntityRelation.CONTAINS_TYPE); @@ -157,10 +157,10 @@ public abstract class BaseRelationServiceTest extends AbstractServiceTest { @Test public void testFindTo() throws ExecutionException, InterruptedException { - AssetId parentA = new AssetId(UUIDs.timeBased()); - AssetId parentB = new AssetId(UUIDs.timeBased()); - AssetId childA = new AssetId(UUIDs.timeBased()); - AssetId childB = new AssetId(UUIDs.timeBased()); + AssetId parentA = new AssetId(Uuids.timeBased()); + AssetId parentB = new AssetId(Uuids.timeBased()); + AssetId childA = new AssetId(Uuids.timeBased()); + AssetId childB = new AssetId(Uuids.timeBased()); EntityRelation relationA1 = new EntityRelation(parentA, childA, EntityRelation.CONTAINS_TYPE); EntityRelation relationA2 = new EntityRelation(parentA, childB, EntityRelation.CONTAINS_TYPE); @@ -207,9 +207,9 @@ public abstract class BaseRelationServiceTest extends AbstractServiceTest { @Test public void testCyclicRecursiveRelation() throws ExecutionException, InterruptedException { // A -> B -> C -> A - AssetId assetA = new AssetId(UUIDs.timeBased()); - AssetId assetB = new AssetId(UUIDs.timeBased()); - AssetId assetC = new AssetId(UUIDs.timeBased()); + AssetId assetA = new AssetId(Uuids.timeBased()); + AssetId assetB = new AssetId(Uuids.timeBased()); + AssetId assetC = new AssetId(Uuids.timeBased()); EntityRelation relationA = new EntityRelation(assetA, assetB, EntityRelation.CONTAINS_TYPE); EntityRelation relationB = new EntityRelation(assetB, assetC, EntityRelation.CONTAINS_TYPE); @@ -239,10 +239,10 @@ public abstract class BaseRelationServiceTest extends AbstractServiceTest { @Test public void testRecursiveRelation() throws ExecutionException, InterruptedException { // A -> B -> [C,D] - AssetId assetA = new AssetId(UUIDs.timeBased()); - AssetId assetB = new AssetId(UUIDs.timeBased()); - AssetId assetC = new AssetId(UUIDs.timeBased()); - DeviceId deviceD = new DeviceId(UUIDs.timeBased()); + AssetId assetA = new AssetId(Uuids.timeBased()); + AssetId assetB = new AssetId(Uuids.timeBased()); + AssetId assetC = new AssetId(Uuids.timeBased()); + DeviceId deviceD = new DeviceId(Uuids.timeBased()); EntityRelation relationAB = new EntityRelation(assetA, assetB, EntityRelation.CONTAINS_TYPE); EntityRelation relationBC = new EntityRelation(assetB, assetC, EntityRelation.CONTAINS_TYPE); @@ -272,7 +272,7 @@ public abstract class BaseRelationServiceTest extends AbstractServiceTest { @Test(expected = DataValidationException.class) public void testSaveRelationWithEmptyFrom() throws ExecutionException, InterruptedException { EntityRelation relation = new EntityRelation(); - relation.setTo(new AssetId(UUIDs.timeBased())); + relation.setTo(new AssetId(Uuids.timeBased())); relation.setType(EntityRelation.CONTAINS_TYPE); Assert.assertTrue(saveRelation(relation)); } @@ -280,7 +280,7 @@ public abstract class BaseRelationServiceTest extends AbstractServiceTest { @Test(expected = DataValidationException.class) public void testSaveRelationWithEmptyTo() throws ExecutionException, InterruptedException { EntityRelation relation = new EntityRelation(); - relation.setFrom(new AssetId(UUIDs.timeBased())); + relation.setFrom(new AssetId(Uuids.timeBased())); relation.setType(EntityRelation.CONTAINS_TYPE); Assert.assertTrue(saveRelation(relation)); } @@ -288,8 +288,8 @@ public abstract class BaseRelationServiceTest extends AbstractServiceTest { @Test(expected = DataValidationException.class) public void testSaveRelationWithEmptyType() throws ExecutionException, InterruptedException { EntityRelation relation = new EntityRelation(); - relation.setFrom(new AssetId(UUIDs.timeBased())); - relation.setTo(new AssetId(UUIDs.timeBased())); + relation.setFrom(new AssetId(Uuids.timeBased())); + relation.setTo(new AssetId(Uuids.timeBased())); Assert.assertTrue(saveRelation(relation)); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java index 8c9e9c37e3..bcd1366513 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.dao.service; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; @@ -24,8 +24,8 @@ import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; @@ -94,7 +94,7 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { public void testSaveRuleChainWithInvalidTenant() { RuleChain ruleChain = new RuleChain(); ruleChain.setName("My RuleChain"); - ruleChain.setTenantId(new TenantId(UUIDs.timeBased())); + ruleChain.setTenantId(new TenantId(Uuids.timeBased())); ruleChainService.saveRuleChain(ruleChain); } @@ -140,13 +140,13 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { } List loadedRuleChains = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(16); - TextPageData pageData = null; + PageLink pageLink = new PageLink(16); + PageData pageData = null; do { pageData = ruleChainService.findTenantRuleChains(tenantId, pageLink); loadedRuleChains.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -157,7 +157,7 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { ruleChainService.deleteRuleChainsByTenantId(tenantId); - pageLink = new TextPageLink(31); + pageLink = new PageLink(31); pageData = ruleChainService.findTenantRuleChains(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertTrue(pageData.getData().isEmpty()); @@ -191,13 +191,13 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { } List loadedRuleChainsName1 = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(19, name1); - TextPageData pageData = null; + PageLink pageLink = new PageLink(19, 0, name1); + PageData pageData = null; do { pageData = ruleChainService.findTenantRuleChains(tenantId, pageLink); loadedRuleChainsName1.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -207,12 +207,12 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { Assert.assertEquals(ruleChainsName1, loadedRuleChainsName1); List loadedRuleChainsName2 = new ArrayList<>(); - pageLink = new TextPageLink(4, name2); + pageLink = new PageLink(4, 0, name2); do { pageData = ruleChainService.findTenantRuleChains(tenantId, pageLink); loadedRuleChainsName2.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -225,7 +225,7 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { ruleChainService.deleteRuleChainById(tenantId, ruleChain.getId()); } - pageLink = new TextPageLink(4, name1); + pageLink = new PageLink(4, 0, name1); pageData = ruleChainService.findTenantRuleChains(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -234,7 +234,7 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { ruleChainService.deleteRuleChainById(tenantId, ruleChain.getId()); } - pageLink = new TextPageLink(4, name2); + pageLink = new PageLink(4, 0, name2); pageData = ruleChainService.findTenantRuleChains(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantProfileServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantProfileServiceTest.java new file mode 100644 index 0000000000..f93fc2fb1c --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantProfileServiceTest.java @@ -0,0 +1,275 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; +import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; +import org.thingsboard.server.dao.exception.DataValidationException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class BaseTenantProfileServiceTest extends AbstractServiceTest { + + private IdComparator idComparator = new IdComparator<>(); + private IdComparator tenantProfileInfoIdComparator = new IdComparator<>(); + + @After + public void after() { + tenantProfileService.deleteTenantProfiles(TenantId.SYS_TENANT_ID); + } + + @Test + public void testSaveTenantProfile() { + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + TenantProfile savedTenantProfile = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + Assert.assertNotNull(savedTenantProfile); + Assert.assertNotNull(savedTenantProfile.getId()); + Assert.assertTrue(savedTenantProfile.getCreatedTime() > 0); + Assert.assertEquals(tenantProfile.getName(), savedTenantProfile.getName()); + Assert.assertEquals(tenantProfile.getDescription(), savedTenantProfile.getDescription()); + Assert.assertEquals(tenantProfile.getProfileData(), savedTenantProfile.getProfileData()); + Assert.assertEquals(tenantProfile.isDefault(), savedTenantProfile.isDefault()); + Assert.assertEquals(tenantProfile.isIsolatedTbCore(), savedTenantProfile.isIsolatedTbCore()); + Assert.assertEquals(tenantProfile.isIsolatedTbRuleEngine(), savedTenantProfile.isIsolatedTbRuleEngine()); + + savedTenantProfile.setName("New tenant profile"); + tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, savedTenantProfile); + TenantProfile foundTenantProfile = tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, savedTenantProfile.getId()); + Assert.assertEquals(foundTenantProfile.getName(), savedTenantProfile.getName()); + } + + @Test + public void testFindTenantProfileById() { + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + TenantProfile savedTenantProfile = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + TenantProfile foundTenantProfile = tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, savedTenantProfile.getId()); + Assert.assertNotNull(foundTenantProfile); + Assert.assertEquals(savedTenantProfile, foundTenantProfile); + } + + @Test + public void testFindTenantProfileInfoById() { + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + TenantProfile savedTenantProfile = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + EntityInfo foundTenantProfileInfo = tenantProfileService.findTenantProfileInfoById(TenantId.SYS_TENANT_ID, savedTenantProfile.getId()); + Assert.assertNotNull(foundTenantProfileInfo); + Assert.assertEquals(savedTenantProfile.getId(), foundTenantProfileInfo.getId()); + Assert.assertEquals(savedTenantProfile.getName(), foundTenantProfileInfo.getName()); + } + + @Test + public void testFindDefaultTenantProfile() { + TenantProfile tenantProfile = this.createTenantProfile("Default Tenant Profile"); + tenantProfile.setDefault(true); + TenantProfile savedTenantProfile = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + TenantProfile foundDefaultTenantProfile = tenantProfileService.findDefaultTenantProfile(TenantId.SYS_TENANT_ID); + Assert.assertNotNull(foundDefaultTenantProfile); + Assert.assertEquals(savedTenantProfile, foundDefaultTenantProfile); + } + + @Test + public void testFindDefaultTenantProfileInfo() { + TenantProfile tenantProfile = this.createTenantProfile("Default Tenant Profile"); + tenantProfile.setDefault(true); + TenantProfile savedTenantProfile = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + EntityInfo foundDefaultTenantProfileInfo = tenantProfileService.findDefaultTenantProfileInfo(TenantId.SYS_TENANT_ID); + Assert.assertNotNull(foundDefaultTenantProfileInfo); + Assert.assertEquals(savedTenantProfile.getId(), foundDefaultTenantProfileInfo.getId()); + Assert.assertEquals(savedTenantProfile.getName(), foundDefaultTenantProfileInfo.getName()); + } + + @Test + public void testSetDefaultTenantProfile() { + TenantProfile tenantProfile1 = this.createTenantProfile("Tenant Profile 1"); + TenantProfile tenantProfile2 = this.createTenantProfile("Tenant Profile 2"); + + TenantProfile savedTenantProfile1 = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile1); + TenantProfile savedTenantProfile2 = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile2); + + boolean result = tenantProfileService.setDefaultTenantProfile(TenantId.SYS_TENANT_ID, savedTenantProfile1.getId()); + Assert.assertTrue(result); + TenantProfile defaultTenantProfile = tenantProfileService.findDefaultTenantProfile(TenantId.SYS_TENANT_ID); + Assert.assertNotNull(defaultTenantProfile); + Assert.assertEquals(savedTenantProfile1.getId(), defaultTenantProfile.getId()); + result = tenantProfileService.setDefaultTenantProfile(TenantId.SYS_TENANT_ID, savedTenantProfile2.getId()); + Assert.assertTrue(result); + defaultTenantProfile = tenantProfileService.findDefaultTenantProfile(TenantId.SYS_TENANT_ID); + Assert.assertNotNull(defaultTenantProfile); + Assert.assertEquals(savedTenantProfile2.getId(), defaultTenantProfile.getId()); + } + + @Test(expected = DataValidationException.class) + public void testSaveTenantProfileWithEmptyName() { + TenantProfile tenantProfile = new TenantProfile(); + tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + } + + @Test(expected = DataValidationException.class) + public void testSaveTenantProfileWithSameName() { + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + TenantProfile tenantProfile2 = this.createTenantProfile("Tenant Profile"); + tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile2); + } + + @Test(expected = DataValidationException.class) + public void testSaveSameTenantProfileWithDifferentIsolatedTbRuleEngine() { + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + TenantProfile savedTenantProfile = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + savedTenantProfile.setIsolatedTbRuleEngine(true); + tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, savedTenantProfile); + } + + @Test(expected = DataValidationException.class) + public void testSaveSameTenantProfileWithDifferentIsolatedTbCore() { + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + TenantProfile savedTenantProfile = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + savedTenantProfile.setIsolatedTbCore(true); + tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, savedTenantProfile); + } + + @Test(expected = DataValidationException.class) + public void testDeleteTenantProfileWithExistingTenant() { + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + TenantProfile savedTenantProfile = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + Tenant tenant = new Tenant(); + tenant.setTitle("Test tenant"); + tenant.setTenantProfileId(savedTenantProfile.getId()); + tenant = tenantService.saveTenant(tenant); + try { + tenantProfileService.deleteTenantProfile(TenantId.SYS_TENANT_ID, savedTenantProfile.getId()); + } finally { + tenantService.deleteTenant(tenant.getId()); + } + } + + @Test + public void testDeleteTenantProfile() { + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + TenantProfile savedTenantProfile = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + tenantProfileService.deleteTenantProfile(TenantId.SYS_TENANT_ID, savedTenantProfile.getId()); + TenantProfile foundTenantProfile = tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, savedTenantProfile.getId()); + Assert.assertNull(foundTenantProfile); + } + + @Test + public void testFindTenantProfiles() { + + List tenantProfiles = new ArrayList<>(); + PageLink pageLink = new PageLink(17); + PageData pageData = tenantProfileService.findTenantProfiles(TenantId.SYS_TENANT_ID, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertTrue(pageData.getData().isEmpty()); + tenantProfiles.addAll(pageData.getData()); + + for (int i = 0; i < 28; i++) { + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile" + i); + tenantProfiles.add(tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile)); + } + + List loadedTenantProfiles = new ArrayList<>(); + pageLink = new PageLink(17); + do { + pageData = tenantProfileService.findTenantProfiles(TenantId.SYS_TENANT_ID, pageLink); + loadedTenantProfiles.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(tenantProfiles, idComparator); + Collections.sort(loadedTenantProfiles, idComparator); + + Assert.assertEquals(tenantProfiles, loadedTenantProfiles); + + for (TenantProfile tenantProfile : loadedTenantProfiles) { + tenantProfileService.deleteTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile.getId()); + } + + pageLink = new PageLink(17); + pageData = tenantProfileService.findTenantProfiles(TenantId.SYS_TENANT_ID, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertTrue(pageData.getData().isEmpty()); + + } + + @Test + public void testFindTenantProfileInfos() { + + List tenantProfiles = new ArrayList<>(); + + for (int i = 0; i < 28; i++) { + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile" + i); + tenantProfiles.add(tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile)); + } + + List loadedTenantProfileInfos = new ArrayList<>(); + PageLink pageLink = new PageLink(17); + PageData pageData; + do { + pageData = tenantProfileService.findTenantProfileInfos(TenantId.SYS_TENANT_ID, pageLink); + loadedTenantProfileInfos.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(tenantProfiles, idComparator); + Collections.sort(loadedTenantProfileInfos, tenantProfileInfoIdComparator); + + List tenantProfileInfos = tenantProfiles.stream().map(tenantProfile -> new EntityInfo(tenantProfile.getId(), + tenantProfile.getName())).collect(Collectors.toList()); + + Assert.assertEquals(tenantProfileInfos, loadedTenantProfileInfos); + + for (EntityInfo tenantProfile : loadedTenantProfileInfos) { + tenantProfileService.deleteTenantProfile(TenantId.SYS_TENANT_ID, new TenantProfileId(tenantProfile.getId().getId())); + } + + pageLink = new PageLink(17); + pageData = tenantProfileService.findTenantProfileInfos(TenantId.SYS_TENANT_ID, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertTrue(pageData.getData().isEmpty()); + + } + + private TenantProfile createTenantProfile(String name) { + TenantProfile tenantProfile = new TenantProfile(); + tenantProfile.setName(name); + tenantProfile.setDescription(name + " Test"); + TenantProfileData profileData = new TenantProfileData(); + profileData.setConfiguration(new DefaultTenantProfileConfiguration()); + tenantProfile.setProfileData(profileData); + tenantProfile.setDefault(false); + tenantProfile.setIsolatedTbCore(false); + tenantProfile.setIsolatedTbRuleEngine(false); + return tenantProfile; + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantServiceTest.java index d3da8305dd..e71788258b 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantServiceTest.java @@ -19,13 +19,15 @@ import org.apache.commons.lang3.RandomStringUtils; import org.junit.Assert; import org.junit.Test; import org.thingsboard.server.common.data.Tenant; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.TenantInfo; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.exception.DataValidationException; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; public abstract class BaseTenantServiceTest extends AbstractServiceTest { @@ -59,6 +61,17 @@ public abstract class BaseTenantServiceTest extends AbstractServiceTest { Assert.assertEquals(savedTenant, foundTenant); tenantService.deleteTenant(savedTenant.getId()); } + + @Test + public void testFindTenantInfoById() { + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + Tenant savedTenant = tenantService.saveTenant(tenant); + TenantInfo foundTenant = tenantService.findTenantInfoById(savedTenant.getId()); + Assert.assertNotNull(foundTenant); + Assert.assertEquals(new TenantInfo(savedTenant, "Default"), foundTenant); + tenantService.deleteTenant(savedTenant.getId()); + } @Test(expected = DataValidationException.class) public void testSaveTenantWithEmptyTitle() { @@ -88,8 +101,8 @@ public abstract class BaseTenantServiceTest extends AbstractServiceTest { public void testFindTenants() { List tenants = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(17); - TextPageData pageData = tenantService.findTenants(pageLink); + PageLink pageLink = new PageLink(17); + PageData pageData = tenantService.findTenants(pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertTrue(pageData.getData().isEmpty()); tenants.addAll(pageData.getData()); @@ -101,12 +114,12 @@ public abstract class BaseTenantServiceTest extends AbstractServiceTest { } List loadedTenants = new ArrayList<>(); - pageLink = new TextPageLink(17); + pageLink = new PageLink(17); do { pageData = tenantService.findTenants(pageLink); loadedTenants.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -116,12 +129,10 @@ public abstract class BaseTenantServiceTest extends AbstractServiceTest { Assert.assertEquals(tenants, loadedTenants); for (Tenant tenant : loadedTenants) { - if (!tenant.getTitle().equals("Tenant")) { - tenantService.deleteTenant(tenant.getId()); - } + tenantService.deleteTenant(tenant.getId()); } - pageLink = new TextPageLink(17); + pageLink = new PageLink(17); pageData = tenantService.findTenants(pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertTrue(pageData.getData().isEmpty()); @@ -152,13 +163,13 @@ public abstract class BaseTenantServiceTest extends AbstractServiceTest { } List loadedTenantsTitle1 = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(15, title1); - TextPageData pageData = null; + PageLink pageLink = new PageLink(15, 0, title1); + PageData pageData = null; do { pageData = tenantService.findTenants(pageLink); loadedTenantsTitle1.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -168,12 +179,12 @@ public abstract class BaseTenantServiceTest extends AbstractServiceTest { Assert.assertEquals(tenantsTitle1, loadedTenantsTitle1); List loadedTenantsTitle2 = new ArrayList<>(); - pageLink = new TextPageLink(4, title2); + pageLink = new PageLink(4, 0, title2); do { pageData = tenantService.findTenants(pageLink); loadedTenantsTitle2.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -186,7 +197,7 @@ public abstract class BaseTenantServiceTest extends AbstractServiceTest { tenantService.deleteTenant(tenant.getId()); } - pageLink = new TextPageLink(4, title1); + pageLink = new PageLink(4, 0, title1); pageData = tenantService.findTenants(pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -195,9 +206,51 @@ public abstract class BaseTenantServiceTest extends AbstractServiceTest { tenantService.deleteTenant(tenant.getId()); } - pageLink = new TextPageLink(4, title2); + pageLink = new PageLink(4, 0, title2); pageData = tenantService.findTenants(pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); } + + @Test + public void testFindTenantInfos() { + + List tenants = new ArrayList<>(); + PageLink pageLink = new PageLink(17); + PageData pageData = tenantService.findTenantInfos(pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertTrue(pageData.getData().isEmpty()); + tenants.addAll(pageData.getData()); + + for (int i=0;i<156;i++) { + Tenant tenant = new Tenant(); + tenant.setTitle("Tenant"+i); + tenants.add(new TenantInfo(tenantService.saveTenant(tenant), "Default")); + } + + List loadedTenants = new ArrayList<>(); + pageLink = new PageLink(17); + do { + pageData = tenantService.findTenantInfos(pageLink); + loadedTenants.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(tenants, idComparator); + Collections.sort(loadedTenants, idComparator); + + Assert.assertEquals(tenants, loadedTenants); + + for (TenantInfo tenant : loadedTenants) { + tenantService.deleteTenant(tenant.getId()); + } + + pageLink = new PageLink(17); + pageData = tenantService.findTenantInfos(pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertTrue(pageData.getData().isEmpty()); + + } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java index 7186d3aced..e6241c1c70 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java @@ -25,8 +25,8 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.dao.exception.DataValidationException; @@ -188,7 +188,7 @@ public abstract class BaseUserServiceTest extends AbstractServiceTest { @Test public void testFindTenantAdmins() { User tenantAdminUser = userService.findUserByEmail(tenantId, "tenant@thingsboard.org"); - TextPageData pageData = userService.findTenantAdmins(tenantAdminUser.getTenantId(), new TextPageLink(10)); + PageData pageData = userService.findTenantAdmins(tenantAdminUser.getTenantId(), new PageLink(10)); Assert.assertFalse(pageData.hasNext()); List users = pageData.getData(); Assert.assertEquals(1, users.size()); @@ -210,12 +210,12 @@ public abstract class BaseUserServiceTest extends AbstractServiceTest { } List loadedTenantAdmins = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(33); + PageLink pageLink = new PageLink(33); do { pageData = userService.findTenantAdmins(tenantId, pageLink); loadedTenantAdmins.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -226,7 +226,7 @@ public abstract class BaseUserServiceTest extends AbstractServiceTest { tenantService.deleteTenant(tenantId); - pageLink = new TextPageLink(33); + pageLink = new PageLink(33); pageData = userService.findTenantAdmins(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertTrue(pageData.getData().isEmpty()); @@ -270,13 +270,13 @@ public abstract class BaseUserServiceTest extends AbstractServiceTest { } List loadedTenantAdminsEmail1 = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(33, email1); - TextPageData pageData = null; + PageLink pageLink = new PageLink(33, 0, email1); + PageData pageData = null; do { pageData = userService.findTenantAdmins(tenantId, pageLink); loadedTenantAdminsEmail1.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -286,12 +286,12 @@ public abstract class BaseUserServiceTest extends AbstractServiceTest { Assert.assertEquals(tenantAdminsEmail1, loadedTenantAdminsEmail1); List loadedTenantAdminsEmail2 = new ArrayList<>(); - pageLink = new TextPageLink(16, email2); + pageLink = new PageLink(16, 0, email2); do { pageData = userService.findTenantAdmins(tenantId, pageLink); loadedTenantAdminsEmail2.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -304,7 +304,7 @@ public abstract class BaseUserServiceTest extends AbstractServiceTest { userService.deleteUser(tenantId, user.getId()); } - pageLink = new TextPageLink(4, email1); + pageLink = new PageLink(4, 0, email1); pageData = userService.findTenantAdmins(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -313,7 +313,7 @@ public abstract class BaseUserServiceTest extends AbstractServiceTest { userService.deleteUser(tenantId, user.getId()); } - pageLink = new TextPageLink(4, email2); + pageLink = new PageLink(4, 0, email2); pageData = userService.findTenantAdmins(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -324,8 +324,8 @@ public abstract class BaseUserServiceTest extends AbstractServiceTest { @Test public void testFindCustomerUsers() { User customerUser = userService.findUserByEmail(tenantId, "customer@thingsboard.org"); - TextPageData pageData = userService.findCustomerUsers(customerUser.getTenantId(), - customerUser.getCustomerId(), new TextPageLink(10)); + PageData pageData = userService.findCustomerUsers(customerUser.getTenantId(), + customerUser.getCustomerId(), new PageLink(10)); Assert.assertFalse(pageData.hasNext()); List users = pageData.getData(); Assert.assertEquals(1, users.size()); @@ -355,12 +355,12 @@ public abstract class BaseUserServiceTest extends AbstractServiceTest { } List loadedCustomerUsers = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(33); + PageLink pageLink = new PageLink(33); do { pageData = userService.findCustomerUsers(tenantId, customerId, pageLink); loadedCustomerUsers.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -423,13 +423,13 @@ public abstract class BaseUserServiceTest extends AbstractServiceTest { } List loadedCustomerUsersEmail1 = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(33, email1); - TextPageData pageData = null; + PageLink pageLink = new PageLink(33, 0, email1); + PageData pageData = null; do { pageData = userService.findCustomerUsers(tenantId, customerId, pageLink); loadedCustomerUsersEmail1.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -439,12 +439,12 @@ public abstract class BaseUserServiceTest extends AbstractServiceTest { Assert.assertEquals(customerUsersEmail1, loadedCustomerUsersEmail1); List loadedCustomerUsersEmail2 = new ArrayList<>(); - pageLink = new TextPageLink(16, email2); + pageLink = new PageLink(16, 0, email2); do { pageData = userService.findCustomerUsers(tenantId, customerId, pageLink); loadedCustomerUsersEmail2.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -457,7 +457,7 @@ public abstract class BaseUserServiceTest extends AbstractServiceTest { userService.deleteUser(tenantId, user.getId()); } - pageLink = new TextPageLink(4, email1); + pageLink = new PageLink(4, 0, email1); pageData = userService.findCustomerUsers(tenantId, customerId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -466,7 +466,7 @@ public abstract class BaseUserServiceTest extends AbstractServiceTest { userService.deleteUser(tenantId, user.getId()); } - pageLink = new TextPageLink(4, email2); + pageLink = new PageLink(4, 0, email2); pageData = userService.findCustomerUsers(tenantId, customerId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetTypeServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetTypeServiceTest.java index 8254a3ebf3..227033cfe6 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetTypeServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetTypeServiceTest.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.dao.service; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.After; @@ -141,7 +141,7 @@ public abstract class BaseWidgetTypeServiceTest extends AbstractServiceTest { WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle); WidgetType widgetType = new WidgetType(); - widgetType.setTenantId(new TenantId(UUIDs.timeBased())); + widgetType.setTenantId(new TenantId(Uuids.timeBased())); widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); widgetType.setName("Widget Type"); widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class)); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetsBundleServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetsBundleServiceTest.java index 0005c07d95..7860615a84 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetsBundleServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetsBundleServiceTest.java @@ -15,15 +15,15 @@ */ package org.thingsboard.server.dao.service; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.model.ModelConstants; @@ -88,7 +88,7 @@ public abstract class BaseWidgetsBundleServiceTest extends AbstractServiceTest { public void testSaveWidgetsBundleWithInvalidTenant() { WidgetsBundle widgetsBundle = new WidgetsBundle(); widgetsBundle.setTitle("My widgets bundle"); - widgetsBundle.setTenantId(new TenantId(UUIDs.timeBased())); + widgetsBundle.setTenantId(new TenantId(Uuids.timeBased())); widgetsBundleService.saveWidgetsBundle(widgetsBundle); } @@ -175,13 +175,13 @@ public abstract class BaseWidgetsBundleServiceTest extends AbstractServiceTest { widgetsBundles.addAll(systemWidgetsBundles); List loadedWidgetsBundles = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(19); - TextPageData pageData = null; + PageLink pageLink = new PageLink(19); + PageData pageData = null; do { pageData = widgetsBundleService.findSystemWidgetsBundlesByPageLink(tenantId, pageLink); loadedWidgetsBundles.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -255,13 +255,13 @@ public abstract class BaseWidgetsBundleServiceTest extends AbstractServiceTest { } List loadedWidgetsBundles = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(11); - TextPageData pageData = null; + PageLink pageLink = new PageLink(11); + PageData pageData = null; do { pageData = widgetsBundleService.findTenantWidgetsBundlesByTenantId(tenantId, pageLink); loadedWidgetsBundles.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -272,7 +272,7 @@ public abstract class BaseWidgetsBundleServiceTest extends AbstractServiceTest { widgetsBundleService.deleteWidgetsBundlesByTenantId(tenantId); - pageLink = new TextPageLink(15); + pageLink = new PageLink(15); pageData = widgetsBundleService.findTenantWidgetsBundlesByTenantId(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertTrue(pageData.getData().isEmpty()); @@ -309,13 +309,13 @@ public abstract class BaseWidgetsBundleServiceTest extends AbstractServiceTest { widgetsBundles.addAll(systemWidgetsBundles); List loadedWidgetsBundles = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(17); - TextPageData pageData = null; + PageLink pageLink = new PageLink(17); + PageData pageData = null; do { pageData = widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, pageLink); loadedWidgetsBundles.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -327,12 +327,12 @@ public abstract class BaseWidgetsBundleServiceTest extends AbstractServiceTest { widgetsBundleService.deleteWidgetsBundlesByTenantId(tenantId); loadedWidgetsBundles.clear(); - pageLink = new TextPageLink(14); + pageLink = new PageLink(14); do { pageData = widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, pageLink); loadedWidgetsBundles.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); @@ -349,12 +349,12 @@ public abstract class BaseWidgetsBundleServiceTest extends AbstractServiceTest { } loadedWidgetsBundles.clear(); - pageLink = new TextPageLink(18); + pageLink = new PageLink(18); do { pageData = widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, pageLink); loadedWidgetsBundles.addAll(pageData.getData()); if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); + pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java index 2ddac24520..16535915fc 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.dao.service.attributes; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -47,7 +47,7 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest { @Test public void saveAndFetch() throws Exception { - DeviceId deviceId = new DeviceId(UUIDs.timeBased()); + DeviceId deviceId = new DeviceId(Uuids.timeBased()); KvEntry attrValue = new StringDataEntry("attribute1", "value1"); AttributeKvEntry attr = new BaseAttributeKvEntry(attrValue, 42L); attributesService.save(SYSTEM_TENANT_ID, deviceId, DataConstants.CLIENT_SCOPE, Collections.singletonList(attr)).get(); @@ -58,7 +58,7 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest { @Test public void saveMultipleTypeAndFetch() throws Exception { - DeviceId deviceId = new DeviceId(UUIDs.timeBased()); + DeviceId deviceId = new DeviceId(Uuids.timeBased()); KvEntry attrOldValue = new StringDataEntry("attribute1", "value1"); AttributeKvEntry attrOld = new BaseAttributeKvEntry(attrOldValue, 42L); @@ -78,7 +78,7 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest { @Test public void findAll() throws Exception { - DeviceId deviceId = new DeviceId(UUIDs.timeBased()); + DeviceId deviceId = new DeviceId(Uuids.timeBased()); KvEntry attrAOldValue = new StringDataEntry("A", "value1"); AttributeKvEntry attrAOld = new BaseAttributeKvEntry(attrAOldValue, 42L); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/attributes/nosql/AttributesServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/attributes/nosql/AttributesServiceNoSqlTest.java deleted file mode 100644 index 7f352f1b4f..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/service/attributes/nosql/AttributesServiceNoSqlTest.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.service.attributes.nosql; - -import org.thingsboard.server.dao.service.DaoNoSqlTest; -import org.thingsboard.server.dao.service.attributes.BaseAttributesServiceTest; - -@DaoNoSqlTest -public class AttributesServiceNoSqlTest extends BaseAttributesServiceTest { -} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java index 595b6fba02..7231988daf 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.dao.service.event; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import org.junit.Assert; import org.junit.Test; import org.thingsboard.server.common.data.DataConstants; @@ -25,7 +25,8 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EventId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.service.AbstractServiceTest; @@ -39,8 +40,8 @@ public abstract class BaseEventServiceTest extends AbstractServiceTest { @Test public void saveEvent() throws Exception { - DeviceId devId = new DeviceId(UUIDs.timeBased()); - Event event = generateEvent(null, devId, "ALARM", UUIDs.timeBased().toString()); + DeviceId devId = new DeviceId(Uuids.timeBased()); + Event event = generateEvent(null, devId, "ALARM", Uuids.timeBased().toString()); Event saved = eventService.save(event); Optional loaded = eventService.findEvent(event.getTenantId(), event.getEntityId(), event.getType(), event.getUid()); Assert.assertTrue(loaded.isPresent()); @@ -50,8 +51,8 @@ public abstract class BaseEventServiceTest extends AbstractServiceTest { @Test public void saveEventIfNotExists() throws Exception { - DeviceId devId = new DeviceId(UUIDs.timeBased()); - Event event = generateEvent(null, devId, "ALARM", UUIDs.timeBased().toString()); + DeviceId devId = new DeviceId(Uuids.timeBased()); + Event event = generateEvent(null, devId, "ALARM", Uuids.timeBased().toString()); Optional saved = eventService.saveIfNotExists(event); Assert.assertTrue(saved.isPresent()); saved = eventService.saveIfNotExists(event); @@ -66,31 +67,31 @@ public abstract class BaseEventServiceTest extends AbstractServiceTest { long endTime = LocalDateTime.of(2016, Month.NOVEMBER, 1, 13, 0).toEpochSecond(ZoneOffset.UTC); long timeAfterEndTime = LocalDateTime.of(2016, Month.NOVEMBER, 1, 13, 30).toEpochSecond(ZoneOffset.UTC); - CustomerId customerId = new CustomerId(UUIDs.timeBased()); - TenantId tenantId = new TenantId(UUIDs.timeBased()); + CustomerId customerId = new CustomerId(Uuids.timeBased()); + TenantId tenantId = new TenantId(Uuids.timeBased()); saveEventWithProvidedTime(timeBeforeStartTime, customerId, tenantId); Event savedEvent = saveEventWithProvidedTime(eventTime, customerId, tenantId); Event savedEvent2 = saveEventWithProvidedTime(eventTime+1, customerId, tenantId); Event savedEvent3 = saveEventWithProvidedTime(eventTime+2, customerId, tenantId); saveEventWithProvidedTime(timeAfterEndTime, customerId, tenantId); - TimePageData events = eventService.findEvents(tenantId, customerId, DataConstants.STATS, - new TimePageLink(2, startTime, endTime, true)); + TimePageLink timePageLink = new TimePageLink(2, 0, "", new SortOrder("createdTime"), startTime, endTime); + + PageData events = eventService.findEvents(tenantId, customerId, DataConstants.STATS, + timePageLink); Assert.assertNotNull(events.getData()); Assert.assertTrue(events.getData().size() == 2); Assert.assertTrue(events.getData().get(0).getUuidId().equals(savedEvent.getUuidId())); Assert.assertTrue(events.getData().get(1).getUuidId().equals(savedEvent2.getUuidId())); Assert.assertTrue(events.hasNext()); - Assert.assertNotNull(events.getNextPageLink()); - events = eventService.findEvents(tenantId, customerId, DataConstants.STATS, events.getNextPageLink()); + events = eventService.findEvents(tenantId, customerId, DataConstants.STATS, timePageLink.nextPageLink()); Assert.assertNotNull(events.getData()); Assert.assertTrue(events.getData().size() == 1); Assert.assertTrue(events.getData().get(0).getUuidId().equals(savedEvent3.getUuidId())); Assert.assertFalse(events.hasNext()); - Assert.assertNull(events.getNextPageLink()); } @Test @@ -101,36 +102,36 @@ public abstract class BaseEventServiceTest extends AbstractServiceTest { long endTime = LocalDateTime.of(2016, Month.NOVEMBER, 1, 13, 0).toEpochSecond(ZoneOffset.UTC); long timeAfterEndTime = LocalDateTime.of(2016, Month.NOVEMBER, 1, 13, 30).toEpochSecond(ZoneOffset.UTC); - CustomerId customerId = new CustomerId(UUIDs.timeBased()); - TenantId tenantId = new TenantId(UUIDs.timeBased()); + CustomerId customerId = new CustomerId(Uuids.timeBased()); + TenantId tenantId = new TenantId(Uuids.timeBased()); saveEventWithProvidedTime(timeBeforeStartTime, customerId, tenantId); Event savedEvent = saveEventWithProvidedTime(eventTime, customerId, tenantId); Event savedEvent2 = saveEventWithProvidedTime(eventTime+1, customerId, tenantId); Event savedEvent3 = saveEventWithProvidedTime(eventTime+2, customerId, tenantId); saveEventWithProvidedTime(timeAfterEndTime, customerId, tenantId); - TimePageData events = eventService.findEvents(tenantId, customerId, DataConstants.STATS, - new TimePageLink(2, startTime, endTime, false)); + TimePageLink timePageLink = new TimePageLink(2, 0, "", new SortOrder("createdTime", SortOrder.Direction.DESC), startTime, endTime); + + PageData events = eventService.findEvents(tenantId, customerId, DataConstants.STATS, + timePageLink); Assert.assertNotNull(events.getData()); Assert.assertTrue(events.getData().size() == 2); Assert.assertTrue(events.getData().get(0).getUuidId().equals(savedEvent3.getUuidId())); Assert.assertTrue(events.getData().get(1).getUuidId().equals(savedEvent2.getUuidId())); Assert.assertTrue(events.hasNext()); - Assert.assertNotNull(events.getNextPageLink()); - events = eventService.findEvents(tenantId, customerId, DataConstants.STATS, events.getNextPageLink()); + events = eventService.findEvents(tenantId, customerId, DataConstants.STATS, timePageLink.nextPageLink()); Assert.assertNotNull(events.getData()); Assert.assertTrue(events.getData().size() == 1); Assert.assertTrue(events.getData().get(0).getUuidId().equals(savedEvent.getUuidId())); Assert.assertFalse(events.hasNext()); - Assert.assertNull(events.getNextPageLink()); } private Event saveEventWithProvidedTime(long time, EntityId entityId, TenantId tenantId) throws IOException { Event event = generateEvent(tenantId, entityId, DataConstants.STATS, null); - event.setId(new EventId(UUIDs.startOf(time))); + event.setId(new EventId(Uuids.startOf(time))); return eventService.save(event); } -} \ No newline at end of file +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/event/nosql/EventServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/event/nosql/EventServiceNoSqlTest.java deleted file mode 100644 index 58eeef7b4a..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/service/event/nosql/EventServiceNoSqlTest.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.service.event.nosql; - -import org.thingsboard.server.dao.service.DaoNoSqlTest; -import org.thingsboard.server.dao.service.event.BaseEventServiceTest; - -@DaoNoSqlTest -public class EventServiceNoSqlTest extends BaseEventServiceTest { -} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AdminSettingsServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AdminSettingsServiceNoSqlTest.java deleted file mode 100644 index fd9e4c0a96..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AdminSettingsServiceNoSqlTest.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.service.nosql; - -import org.thingsboard.server.dao.service.BaseAdminSettingsServiceTest; -import org.thingsboard.server.dao.service.DaoNoSqlTest; - -@DaoNoSqlTest -public class AdminSettingsServiceNoSqlTest extends BaseAdminSettingsServiceTest { -} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AlarmServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AlarmServiceNoSqlTest.java deleted file mode 100644 index ffd3110404..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AlarmServiceNoSqlTest.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.service.nosql; - -import org.thingsboard.server.dao.service.BaseAlarmServiceTest; -import org.thingsboard.server.dao.service.DaoNoSqlTest; - -@DaoNoSqlTest -public class AlarmServiceNoSqlTest extends BaseAlarmServiceTest { -} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AssetServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AssetServiceNoSqlTest.java deleted file mode 100644 index aae85b1595..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AssetServiceNoSqlTest.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.service.nosql; - -import org.thingsboard.server.dao.service.BaseAssetServiceTest; -import org.thingsboard.server.dao.service.DaoNoSqlTest; - -@DaoNoSqlTest -public class AssetServiceNoSqlTest extends BaseAssetServiceTest { -} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/CustomerServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/CustomerServiceNoSqlTest.java deleted file mode 100644 index e17f1e0fce..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/CustomerServiceNoSqlTest.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.service.nosql; - -import org.thingsboard.server.dao.service.BaseCustomerServiceTest; -import org.thingsboard.server.dao.service.DaoNoSqlTest; - -@DaoNoSqlTest -public class CustomerServiceNoSqlTest extends BaseCustomerServiceTest { -} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DashboardServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DashboardServiceNoSqlTest.java deleted file mode 100644 index babcf8612a..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DashboardServiceNoSqlTest.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.service.nosql; - -import org.thingsboard.server.dao.service.BaseDashboardServiceTest; -import org.thingsboard.server.dao.service.DaoNoSqlTest; - -@DaoNoSqlTest -public class DashboardServiceNoSqlTest extends BaseDashboardServiceTest { -} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceCredentialCacheServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceCredentialCacheServiceNoSqlTest.java deleted file mode 100644 index 521c4c018f..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceCredentialCacheServiceNoSqlTest.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.service.nosql; - -import org.thingsboard.server.dao.service.BaseDeviceCredentialsCacheTest; -import org.thingsboard.server.dao.service.DaoNoSqlTest; - -@DaoNoSqlTest -public class DeviceCredentialCacheServiceNoSqlTest extends BaseDeviceCredentialsCacheTest { -} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceCredentialServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceCredentialServiceNoSqlTest.java deleted file mode 100644 index 6fe90ad103..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceCredentialServiceNoSqlTest.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.service.nosql; - -import org.thingsboard.server.dao.service.BaseDeviceCredentialsServiceTest; -import org.thingsboard.server.dao.service.DaoNoSqlTest; - -@DaoNoSqlTest -public class DeviceCredentialServiceNoSqlTest extends BaseDeviceCredentialsServiceTest { -} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceServiceNoSqlTest.java deleted file mode 100644 index 7bd57ece28..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceServiceNoSqlTest.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.service.nosql; - -import org.thingsboard.server.dao.service.BaseDeviceServiceTest; -import org.thingsboard.server.dao.service.DaoNoSqlTest; - -@DaoNoSqlTest -public class DeviceServiceNoSqlTest extends BaseDeviceServiceTest { -} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/RelationCacheNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/RelationCacheNoSqlTest.java deleted file mode 100644 index e5c08a1bac..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/RelationCacheNoSqlTest.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.service.nosql; - -import org.thingsboard.server.dao.service.BaseRelationCacheTest; -import org.thingsboard.server.dao.service.DaoNoSqlTest; - -@DaoNoSqlTest -public class RelationCacheNoSqlTest extends BaseRelationCacheTest { -} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/RelationServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/RelationServiceNoSqlTest.java deleted file mode 100644 index 7bcc3d7a6f..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/RelationServiceNoSqlTest.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.service.nosql; - -import org.thingsboard.server.dao.service.BaseRelationServiceTest; -import org.thingsboard.server.dao.service.DaoNoSqlTest; - -@DaoNoSqlTest -public class RelationServiceNoSqlTest extends BaseRelationServiceTest { -} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/RuleChainServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/RuleChainServiceNoSqlTest.java deleted file mode 100644 index c2b5a7a00a..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/RuleChainServiceNoSqlTest.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.service.nosql; - -import org.thingsboard.server.dao.service.BaseRuleChainServiceTest; -import org.thingsboard.server.dao.service.DaoNoSqlTest; - -@DaoNoSqlTest -public class RuleChainServiceNoSqlTest extends BaseRuleChainServiceTest { -} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/TenantServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/TenantServiceNoSqlTest.java deleted file mode 100644 index 6d878995f0..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/TenantServiceNoSqlTest.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.service.nosql; - -import org.thingsboard.server.dao.service.BaseTenantServiceTest; -import org.thingsboard.server.dao.service.DaoNoSqlTest; - -@DaoNoSqlTest -public class TenantServiceNoSqlTest extends BaseTenantServiceTest { -} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/UserServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/UserServiceNoSqlTest.java deleted file mode 100644 index e065c28afa..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/UserServiceNoSqlTest.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.service.nosql; - -import org.thingsboard.server.dao.service.BaseUserServiceTest; -import org.thingsboard.server.dao.service.DaoNoSqlTest; - -@DaoNoSqlTest -public class UserServiceNoSqlTest extends BaseUserServiceTest { -} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/WidgetTypeServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/WidgetTypeServiceNoSqlTest.java deleted file mode 100644 index 766e6e8bc2..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/WidgetTypeServiceNoSqlTest.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.service.nosql; - -import org.thingsboard.server.dao.service.BaseWidgetTypeServiceTest; -import org.thingsboard.server.dao.service.DaoNoSqlTest; - -@DaoNoSqlTest -public class WidgetTypeServiceNoSqlTest extends BaseWidgetTypeServiceTest { -} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/WidgetsBundleServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/WidgetsBundleServiceNoSqlTest.java deleted file mode 100644 index 5ba50a1484..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/WidgetsBundleServiceNoSqlTest.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.service.nosql; - -import org.thingsboard.server.dao.service.BaseWidgetsBundleServiceTest; -import org.thingsboard.server.dao.service.DaoNoSqlTest; - -@DaoNoSqlTest -public class WidgetsBundleServiceNoSqlTest extends BaseWidgetsBundleServiceTest { -} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/ApiUsageStateServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/ApiUsageStateServiceSqlTest.java new file mode 100644 index 0000000000..bbaae0a5a9 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/ApiUsageStateServiceSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.sql; + +import org.thingsboard.server.dao.service.BaseApiUsageStateServiceTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class ApiUsageStateServiceSqlTest extends BaseApiUsageStateServiceTest { +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceProfileServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceProfileServiceSqlTest.java new file mode 100644 index 0000000000..3acf858929 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceProfileServiceSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.sql; + +import org.thingsboard.server.dao.service.BaseDeviceProfileServiceTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class DeviceProfileServiceSqlTest extends BaseDeviceProfileServiceTest { +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/EntityServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/EntityServiceSqlTest.java new file mode 100644 index 0000000000..ea8affa5af --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/EntityServiceSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.sql; + +import org.thingsboard.server.dao.service.BaseEntityServiceTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class EntityServiceSqlTest extends BaseEntityServiceTest { +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/OAuth2ConfigTemplateServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/OAuth2ConfigTemplateServiceSqlTest.java new file mode 100644 index 0000000000..53fd2e37b4 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/OAuth2ConfigTemplateServiceSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.sql; + +import org.thingsboard.server.dao.service.BaseOAuth2ConfigTemplateServiceTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class OAuth2ConfigTemplateServiceSqlTest extends BaseOAuth2ConfigTemplateServiceTest { +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/OAuth2ServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/OAuth2ServiceSqlTest.java new file mode 100644 index 0000000000..64eba8f89a --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/OAuth2ServiceSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.sql; + +import org.thingsboard.server.dao.service.BaseOAuth2ServiceTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class OAuth2ServiceSqlTest extends BaseOAuth2ServiceTest { +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/TenantProfileServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/TenantProfileServiceSqlTest.java new file mode 100644 index 0000000000..6caf3242be --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/TenantProfileServiceSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.sql; + +import org.thingsboard.server.dao.service.BaseTenantProfileServiceTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class TenantProfileServiceSqlTest extends BaseTenantProfileServiceTest { +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java index 2e35fb5345..872d4749e4 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.dao.service.timeseries; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import lombok.extern.slf4j.Slf4j; import org.junit.*; import org.thingsboard.server.common.data.EntityView; @@ -84,17 +84,13 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { @Test public void testFindAllLatest() throws Exception { - DeviceId deviceId = new DeviceId(UUIDs.timeBased()); + DeviceId deviceId = new DeviceId(Uuids.timeBased()); saveEntries(deviceId, TS - 2); saveEntries(deviceId, TS - 1); saveEntries(deviceId, TS); testLatestTsAndVerify(deviceId); - - EntityView entityView = saveAndCreateEntityView(deviceId, Arrays.asList(STRING_KEY, DOUBLE_KEY, LONG_KEY, BOOLEAN_KEY)); - - testLatestTsAndVerify(entityView.getId()); } private void testLatestTsAndVerify(EntityId entityId) throws ExecutionException, InterruptedException { @@ -132,7 +128,7 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { @Test public void testFindLatest() throws Exception { - DeviceId deviceId = new DeviceId(UUIDs.timeBased()); + DeviceId deviceId = new DeviceId(Uuids.timeBased()); saveEntries(deviceId, TS - 2); saveEntries(deviceId, TS - 1); @@ -141,17 +137,11 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { List entries = tsService.findLatest(tenantId, deviceId, Collections.singleton(STRING_KEY)).get(); Assert.assertEquals(1, entries.size()); Assert.assertEquals(toTsEntry(TS, stringKvEntry), entries.get(0)); - - EntityView entityView = saveAndCreateEntityView(deviceId, Arrays.asList(STRING_KEY)); - - entries = tsService.findLatest(tenantId, entityView.getId(), Collections.singleton(STRING_KEY)).get(); - Assert.assertEquals(1, entries.size()); - Assert.assertEquals(toTsEntry(TS, stringKvEntry), entries.get(0)); } @Test public void testFindByQueryAscOrder() throws Exception { - DeviceId deviceId = new DeviceId(UUIDs.timeBased()); + DeviceId deviceId = new DeviceId(Uuids.timeBased()); saveEntries(deviceId, TS - 2); saveEntries(deviceId, TS - 1); @@ -177,7 +167,7 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { @Test public void testFindByQueryDescOrder() throws Exception { - DeviceId deviceId = new DeviceId(UUIDs.timeBased()); + DeviceId deviceId = new DeviceId(Uuids.timeBased()); saveEntries(deviceId, TS - 2); saveEntries(deviceId, TS - 1); @@ -203,7 +193,7 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { @Test public void testDeleteDeviceTsDataWithOverwritingLatest() throws Exception { - DeviceId deviceId = new DeviceId(UUIDs.timeBased()); + DeviceId deviceId = new DeviceId(Uuids.timeBased()); saveEntries(deviceId, 10000); saveEntries(deviceId, 20000); @@ -223,7 +213,7 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { @Test public void testFindDeviceTsData() throws Exception { - DeviceId deviceId = new DeviceId(UUIDs.timeBased()); + DeviceId deviceId = new DeviceId(Uuids.timeBased()); List entries = new ArrayList<>(); entries.add(save(deviceId, 5000, 100)); @@ -374,7 +364,7 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { @Test public void testFindDeviceLongAndDoubleTsData() throws Exception { - DeviceId deviceId = new DeviceId(UUIDs.timeBased()); + DeviceId deviceId = new DeviceId(Uuids.timeBased()); List entries = new ArrayList<>(); entries.add(save(deviceId, 5000, 100)); diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java index a35e6f8b63..99aafa08ef 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.dao.sql.asset; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import com.google.common.util.concurrent.ListenableFuture; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -24,7 +24,8 @@ import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.AbstractJpaDaoTest; import org.thingsboard.server.dao.asset.AssetDao; @@ -49,64 +50,64 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest { @Test public void testFindAssetsByTenantId() { - UUID tenantId1 = UUIDs.timeBased(); - UUID tenantId2 = UUIDs.timeBased(); - UUID customerId1 = UUIDs.timeBased(); - UUID customerId2 = UUIDs.timeBased(); + UUID tenantId1 = Uuids.timeBased(); + UUID tenantId2 = Uuids.timeBased(); + UUID customerId1 = Uuids.timeBased(); + UUID customerId2 = Uuids.timeBased(); for (int i = 0; i < 60; i++) { - UUID assetId = UUIDs.timeBased(); + UUID assetId = Uuids.timeBased(); UUID tenantId = i % 2 == 0 ? tenantId1 : tenantId2; UUID customerId = i % 2 == 0 ? customerId1 : customerId2; saveAsset(assetId, tenantId, customerId, "ASSET_" + i, "TYPE_1"); } assertEquals(60, assetDao.find(new TenantId(tenantId1)).size()); - TextPageLink pageLink1 = new TextPageLink(20, "ASSET_"); - List assets1 = assetDao.findAssetsByTenantId(tenantId1, pageLink1); - assertEquals(20, assets1.size()); + PageLink pageLink = new PageLink(20, 0, "ASSET_"); + PageData assets1 = assetDao.findAssetsByTenantId(tenantId1, pageLink); + assertEquals(20, assets1.getData().size()); - TextPageLink pageLink2 = new TextPageLink(20, "ASSET_", assets1.get(19).getId().getId(), null); - List assets2 = assetDao.findAssetsByTenantId(tenantId1, pageLink2); - assertEquals(10, assets2.size()); + pageLink = pageLink.nextPageLink(); + PageData assets2 = assetDao.findAssetsByTenantId(tenantId1, pageLink); + assertEquals(10, assets2.getData().size()); - TextPageLink pageLink3 = new TextPageLink(20, "ASSET_", assets2.get(9).getId().getId(), null); - List assets3 = assetDao.findAssetsByTenantId(tenantId1, pageLink3); - assertEquals(0, assets3.size()); + pageLink = pageLink.nextPageLink(); + PageData assets3 = assetDao.findAssetsByTenantId(tenantId1, pageLink); + assertEquals(0, assets3.getData().size()); } @Test public void testFindAssetsByTenantIdAndCustomerId() { - UUID tenantId1 = UUIDs.timeBased(); - UUID tenantId2 = UUIDs.timeBased(); - UUID customerId1 = UUIDs.timeBased(); - UUID customerId2 = UUIDs.timeBased(); + UUID tenantId1 = Uuids.timeBased(); + UUID tenantId2 = Uuids.timeBased(); + UUID customerId1 = Uuids.timeBased(); + UUID customerId2 = Uuids.timeBased(); for (int i = 0; i < 60; i++) { - UUID assetId = UUIDs.timeBased(); + UUID assetId = Uuids.timeBased(); UUID tenantId = i % 2 == 0 ? tenantId1 : tenantId2; UUID customerId = i % 2 == 0 ? customerId1 : customerId2; saveAsset(assetId, tenantId, customerId, "ASSET_" + i, "TYPE_1"); } - TextPageLink pageLink1 = new TextPageLink(20, "ASSET_"); - List assets1 = assetDao.findAssetsByTenantIdAndCustomerId(tenantId1, customerId1, pageLink1); - assertEquals(20, assets1.size()); + PageLink pageLink = new PageLink(20, 0, "ASSET_"); + PageData assets1 = assetDao.findAssetsByTenantIdAndCustomerId(tenantId1, customerId1, pageLink); + assertEquals(20, assets1.getData().size()); - TextPageLink pageLink2 = new TextPageLink(20, "ASSET_", assets1.get(19).getId().getId(), null); - List assets2 = assetDao.findAssetsByTenantIdAndCustomerId(tenantId1, customerId1, pageLink2); - assertEquals(10, assets2.size()); + pageLink = pageLink.nextPageLink(); + PageData assets2 = assetDao.findAssetsByTenantIdAndCustomerId(tenantId1, customerId1, pageLink); + assertEquals(10, assets2.getData().size()); - TextPageLink pageLink3 = new TextPageLink(20, "ASSET_", assets2.get(9).getId().getId(), null); - List assets3 = assetDao.findAssetsByTenantIdAndCustomerId(tenantId1, customerId1, pageLink3); - assertEquals(0, assets3.size()); + pageLink = pageLink.nextPageLink(); + PageData assets3 = assetDao.findAssetsByTenantIdAndCustomerId(tenantId1, customerId1, pageLink); + assertEquals(0, assets3.getData().size()); } @Test public void testFindAssetsByTenantIdAndIdsAsync() throws ExecutionException, InterruptedException { - UUID tenantId = UUIDs.timeBased(); - UUID customerId = UUIDs.timeBased(); + UUID tenantId = Uuids.timeBased(); + UUID customerId = Uuids.timeBased(); List searchIds = new ArrayList<>(); for (int i = 0; i < 30; i++) { - UUID assetId = UUIDs.timeBased(); + UUID assetId = Uuids.timeBased(); saveAsset(assetId, tenantId, customerId, "ASSET_" + i, "TYPE_1"); if (i % 3 == 0) { searchIds.add(assetId); @@ -122,12 +123,12 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest { @Test public void testFindAssetsByTenantIdCustomerIdAndIdsAsync() throws ExecutionException, InterruptedException { - UUID tenantId = UUIDs.timeBased(); - UUID customerId1 = UUIDs.timeBased(); - UUID customerId2 = UUIDs.timeBased(); + UUID tenantId = Uuids.timeBased(); + UUID customerId1 = Uuids.timeBased(); + UUID customerId2 = Uuids.timeBased(); List searchIds = new ArrayList<>(); for (int i = 0; i < 30; i++) { - UUID assetId = UUIDs.timeBased(); + UUID assetId = Uuids.timeBased(); UUID customerId = i%2 == 0 ? customerId1 : customerId2; saveAsset(assetId, tenantId, customerId, "ASSET_" + i, "TYPE_1"); if (i % 3 == 0) { @@ -144,12 +145,12 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest { @Test public void testFindAssetsByTenantIdAndName() { - UUID assetId1 = UUIDs.timeBased(); - UUID assetId2 = UUIDs.timeBased(); - UUID tenantId1 = UUIDs.timeBased(); - UUID tenantId2 = UUIDs.timeBased(); - UUID customerId1 = UUIDs.timeBased(); - UUID customerId2 = UUIDs.timeBased(); + UUID assetId1 = Uuids.timeBased(); + UUID assetId2 = Uuids.timeBased(); + UUID tenantId1 = Uuids.timeBased(); + UUID tenantId2 = Uuids.timeBased(); + UUID customerId1 = Uuids.timeBased(); + UUID customerId2 = Uuids.timeBased(); String name = "TEST_ASSET"; saveAsset(assetId1, tenantId1, customerId1, name, "TYPE_1"); saveAsset(assetId2, tenantId2, customerId2, name, "TYPE_1"); @@ -174,22 +175,22 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest { @Test public void testFindTenantAssetTypesAsync() throws ExecutionException, InterruptedException { - UUID assetId1 = UUIDs.timeBased(); - UUID assetId2 = UUIDs.timeBased(); - UUID tenantId1 = UUIDs.timeBased(); - UUID tenantId2 = UUIDs.timeBased(); - UUID customerId1 = UUIDs.timeBased(); - UUID customerId2 = UUIDs.timeBased(); - saveAsset(UUIDs.timeBased(), tenantId1, customerId1, "TEST_ASSET_1", "TYPE_1"); - saveAsset(UUIDs.timeBased(), tenantId1, customerId1, "TEST_ASSET_2", "TYPE_1"); - saveAsset(UUIDs.timeBased(), tenantId1, customerId1, "TEST_ASSET_3", "TYPE_2"); - saveAsset(UUIDs.timeBased(), tenantId1, customerId1, "TEST_ASSET_4", "TYPE_3"); - saveAsset(UUIDs.timeBased(), tenantId1, customerId1, "TEST_ASSET_5", "TYPE_3"); - saveAsset(UUIDs.timeBased(), tenantId1, customerId1, "TEST_ASSET_6", "TYPE_3"); - - saveAsset(UUIDs.timeBased(), tenantId2, customerId2, "TEST_ASSET_7", "TYPE_4"); - saveAsset(UUIDs.timeBased(), tenantId2, customerId2, "TEST_ASSET_8", "TYPE_1"); - saveAsset(UUIDs.timeBased(), tenantId2, customerId2, "TEST_ASSET_9", "TYPE_1"); + UUID assetId1 = Uuids.timeBased(); + UUID assetId2 = Uuids.timeBased(); + UUID tenantId1 = Uuids.timeBased(); + UUID tenantId2 = Uuids.timeBased(); + UUID customerId1 = Uuids.timeBased(); + UUID customerId2 = Uuids.timeBased(); + saveAsset(Uuids.timeBased(), tenantId1, customerId1, "TEST_ASSET_1", "TYPE_1"); + saveAsset(Uuids.timeBased(), tenantId1, customerId1, "TEST_ASSET_2", "TYPE_1"); + saveAsset(Uuids.timeBased(), tenantId1, customerId1, "TEST_ASSET_3", "TYPE_2"); + saveAsset(Uuids.timeBased(), tenantId1, customerId1, "TEST_ASSET_4", "TYPE_3"); + saveAsset(Uuids.timeBased(), tenantId1, customerId1, "TEST_ASSET_5", "TYPE_3"); + saveAsset(Uuids.timeBased(), tenantId1, customerId1, "TEST_ASSET_6", "TYPE_3"); + + saveAsset(Uuids.timeBased(), tenantId2, customerId2, "TEST_ASSET_7", "TYPE_4"); + saveAsset(Uuids.timeBased(), tenantId2, customerId2, "TEST_ASSET_8", "TYPE_1"); + saveAsset(Uuids.timeBased(), tenantId2, customerId2, "TEST_ASSET_9", "TYPE_1"); List tenant1Types = assetDao.findTenantAssetTypesAsync(tenantId1).get(); assertNotNull(tenant1Types); diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDaoTest.java index 3a377d6cc4..951ac89659 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDaoTest.java @@ -15,11 +15,12 @@ */ package org.thingsboard.server.dao.sql.component; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.id.ComponentDescriptorId; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentScope; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -46,13 +47,13 @@ public class JpaBaseComponentDescriptorDaoTest extends AbstractJpaDaoTest { createComponentDescriptor(ComponentType.ACTION, ComponentScope.TENANT, i + 20); } - TextPageLink pageLink1 = new TextPageLink(15, "COMPONENT_"); - List components1 = componentDescriptorDao.findByTypeAndPageLink(AbstractServiceTest.SYSTEM_TENANT_ID, ComponentType.FILTER, pageLink1); - assertEquals(15, components1.size()); + PageLink pageLink = new PageLink(15, 0, "COMPONENT_"); + PageData components1 = componentDescriptorDao.findByTypeAndPageLink(AbstractServiceTest.SYSTEM_TENANT_ID, ComponentType.FILTER, pageLink); + assertEquals(15, components1.getData().size()); - TextPageLink pageLink2 = new TextPageLink(15, "COMPONENT_", components1.get(14).getId().getId(), null); - List components2 = componentDescriptorDao.findByTypeAndPageLink(AbstractServiceTest.SYSTEM_TENANT_ID,ComponentType.FILTER, pageLink2); - assertEquals(5, components2.size()); + pageLink = pageLink.nextPageLink(); + PageData components2 = componentDescriptorDao.findByTypeAndPageLink(AbstractServiceTest.SYSTEM_TENANT_ID,ComponentType.FILTER, pageLink); + assertEquals(5, components2.getData().size()); } @Test @@ -63,20 +64,20 @@ public class JpaBaseComponentDescriptorDaoTest extends AbstractJpaDaoTest { createComponentDescriptor(ComponentType.FILTER, ComponentScope.SYSTEM, i + 40); } - TextPageLink pageLink1 = new TextPageLink(15, "COMPONENT_"); - List components1 = componentDescriptorDao.findByScopeAndTypeAndPageLink(AbstractServiceTest.SYSTEM_TENANT_ID, - ComponentScope.SYSTEM, ComponentType.FILTER, pageLink1); - assertEquals(15, components1.size()); + PageLink pageLink = new PageLink(15, 0, "COMPONENT_"); + PageData components1 = componentDescriptorDao.findByScopeAndTypeAndPageLink(AbstractServiceTest.SYSTEM_TENANT_ID, + ComponentScope.SYSTEM, ComponentType.FILTER, pageLink); + assertEquals(15, components1.getData().size()); - TextPageLink pageLink2 = new TextPageLink(15, "COMPONENT_", components1.get(14).getId().getId(), null); - List components2 = componentDescriptorDao.findByScopeAndTypeAndPageLink(AbstractServiceTest.SYSTEM_TENANT_ID, - ComponentScope.SYSTEM, ComponentType.FILTER, pageLink2); - assertEquals(5, components2.size()); + pageLink = pageLink.nextPageLink(); + PageData components2 = componentDescriptorDao.findByScopeAndTypeAndPageLink(AbstractServiceTest.SYSTEM_TENANT_ID, + ComponentScope.SYSTEM, ComponentType.FILTER, pageLink); + assertEquals(5, components2.getData().size()); } private void createComponentDescriptor(ComponentType type, ComponentScope scope, int index) { ComponentDescriptor component = new ComponentDescriptor(); - component.setId(new ComponentDescriptorId(UUIDs.timeBased())); + component.setId(new ComponentDescriptorId(Uuids.timeBased())); component.setType(type); component.setScope(scope); component.setName("COMPONENT_" + index); diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDaoTest.java index db4fb05a6d..1772377e56 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDaoTest.java @@ -15,13 +15,14 @@ */ package org.thingsboard.server.dao.sql.customer; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.AbstractJpaDaoTest; import org.thingsboard.server.dao.customer.CustomerDao; @@ -42,26 +43,26 @@ public class JpaCustomerDaoTest extends AbstractJpaDaoTest { @Test public void testFindByTenantId() { - UUID tenantId1 = UUIDs.timeBased(); - UUID tenantId2 = UUIDs.timeBased(); + UUID tenantId1 = Uuids.timeBased(); + UUID tenantId2 = Uuids.timeBased(); for (int i = 0; i < 20; i++) { createCustomer(tenantId1, i); createCustomer(tenantId2, i * 2); } - TextPageLink pageLink1 = new TextPageLink(15, "CUSTOMER"); - List customers1 = customerDao.findCustomersByTenantId(tenantId1, pageLink1); - assertEquals(15, customers1.size()); + PageLink pageLink = new PageLink(15, 0, "CUSTOMER"); + PageData customers1 = customerDao.findCustomersByTenantId(tenantId1, pageLink); + assertEquals(15, customers1.getData().size()); - TextPageLink pageLink2 = new TextPageLink(15, "CUSTOMER", customers1.get(14).getId().getId(), null); - List customers2 = customerDao.findCustomersByTenantId(tenantId1, pageLink2); - assertEquals(5, customers2.size()); + pageLink = pageLink.nextPageLink(); + PageData customers2 = customerDao.findCustomersByTenantId(tenantId1, pageLink); + assertEquals(5, customers2.getData().size()); } @Test public void testFindCustomersByTenantIdAndTitle() { - UUID tenantId = UUIDs.timeBased(); + UUID tenantId = Uuids.timeBased(); for (int i = 0; i < 10; i++) { createCustomer(tenantId, i); @@ -74,7 +75,7 @@ public class JpaCustomerDaoTest extends AbstractJpaDaoTest { private void createCustomer(UUID tenantId, int index) { Customer customer = new Customer(); - customer.setId(new CustomerId(UUIDs.timeBased())); + customer.setId(new CustomerId(Uuids.timeBased())); customer.setTenantId(new TenantId(tenantId)); customer.setTitle("CUSTOMER_" + index); customerDao.save(new TenantId(tenantId), customer); diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDaoTest.java index 7dbffc05cb..f93cde707d 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDaoTest.java @@ -15,14 +15,15 @@ */ package org.thingsboard.server.dao.sql.dashboard; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.AbstractJpaDaoTest; import org.thingsboard.server.dao.dashboard.DashboardInfoDao; import org.thingsboard.server.dao.service.AbstractServiceTest; @@ -40,26 +41,25 @@ public class JpaDashboardInfoDaoTest extends AbstractJpaDaoTest { @Test public void testFindDashboardsByTenantId() { - UUID tenantId1 = UUIDs.timeBased(); - UUID tenantId2 = UUIDs.timeBased(); + UUID tenantId1 = Uuids.timeBased(); + UUID tenantId2 = Uuids.timeBased(); for (int i = 0; i < 20; i++) { createDashboard(tenantId1, i); createDashboard(tenantId2, i * 2); } - TextPageLink pageLink1 = new TextPageLink(15, "DASHBOARD"); - List dashboardInfos1 = dashboardInfoDao.findDashboardsByTenantId(tenantId1, pageLink1); - Assert.assertEquals(15, dashboardInfos1.size()); + PageLink pageLink = new PageLink(15, 0, "DASHBOARD"); + PageData dashboardInfos1 = dashboardInfoDao.findDashboardsByTenantId(tenantId1, pageLink); + Assert.assertEquals(15, dashboardInfos1.getData().size()); - TextPageLink pageLink2 = new TextPageLink(15, "DASHBOARD", dashboardInfos1.get(14).getId().getId(), null); - List dashboardInfos2 = dashboardInfoDao.findDashboardsByTenantId(tenantId1, pageLink2); - Assert.assertEquals(5, dashboardInfos2.size()); + PageData dashboardInfos2 = dashboardInfoDao.findDashboardsByTenantId(tenantId1, pageLink.nextPageLink()); + Assert.assertEquals(5, dashboardInfos2.getData().size()); } private void createDashboard(UUID tenantId, int index) { DashboardInfo dashboardInfo = new DashboardInfo(); - dashboardInfo.setId(new DashboardId(UUIDs.timeBased())); + dashboardInfo.setId(new DashboardId(Uuids.timeBased())); dashboardInfo.setTenantId(new TenantId(tenantId)); dashboardInfo.setTitle("DASHBOARD_" + index); dashboardInfoDao.save(AbstractServiceTest.SYSTEM_TENANT_ID, dashboardInfo); diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/device/JpaDeviceDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/device/JpaDeviceDaoTest.java index 3ee89a7387..9e523299a7 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/device/JpaDeviceDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/device/JpaDeviceDaoTest.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.dao.sql.device; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; @@ -25,7 +25,8 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.AbstractJpaDaoTest; import org.thingsboard.server.dao.device.DeviceDao; @@ -48,25 +49,26 @@ public class JpaDeviceDaoTest extends AbstractJpaDaoTest { @Test public void testFindDevicesByTenantId() { - UUID tenantId1 = UUIDs.timeBased(); - UUID tenantId2 = UUIDs.timeBased(); - UUID customerId1 = UUIDs.timeBased(); - UUID customerId2 = UUIDs.timeBased(); + UUID tenantId1 = Uuids.timeBased(); + UUID tenantId2 = Uuids.timeBased(); + UUID customerId1 = Uuids.timeBased(); + UUID customerId2 = Uuids.timeBased(); createDevices(tenantId1, tenantId2, customerId1, customerId2, 40); - TextPageLink pageLink1 = new TextPageLink(15, "SEARCH_TEXT"); - List devices1 = deviceDao.findDevicesByTenantId(tenantId1, pageLink1); - assertEquals(15, devices1.size()); + PageLink pageLink = new PageLink(15, 0, "SEARCH_TEXT"); + PageData devices1 = deviceDao.findDevicesByTenantId(tenantId1, pageLink); + assertEquals(15, devices1.getData().size()); - TextPageLink pageLink2 = new TextPageLink(15, "SEARCH_TEXT", devices1.get(14).getId().getId(), null); - List devices2 = deviceDao.findDevicesByTenantId(tenantId1, pageLink2); - assertEquals(5, devices2.size()); + pageLink = pageLink.nextPageLink(); + + PageData devices2 = deviceDao.findDevicesByTenantId(tenantId1, pageLink); + assertEquals(5, devices2.getData().size()); } @Test public void testFindAsync() throws ExecutionException, InterruptedException { - UUID tenantId = UUIDs.timeBased(); - UUID customerId = UUIDs.timeBased(); + UUID tenantId = Uuids.timeBased(); + UUID customerId = Uuids.timeBased(); Device device = getDevice(tenantId, customerId); deviceDao.save(new TenantId(tenantId), device); @@ -83,16 +85,16 @@ public class JpaDeviceDaoTest extends AbstractJpaDaoTest { @Test public void testFindDevicesByTenantIdAndIdsAsync() throws ExecutionException, InterruptedException { - UUID tenantId1 = UUIDs.timeBased(); - UUID customerId1 = UUIDs.timeBased(); - UUID tenantId2 = UUIDs.timeBased(); - UUID customerId2 = UUIDs.timeBased(); + UUID tenantId1 = Uuids.timeBased(); + UUID customerId1 = Uuids.timeBased(); + UUID tenantId2 = Uuids.timeBased(); + UUID customerId2 = Uuids.timeBased(); List deviceIds = new ArrayList<>(); for(int i = 0; i < 5; i++) { - UUID deviceId1 = UUIDs.timeBased(); - UUID deviceId2 = UUIDs.timeBased(); + UUID deviceId1 = Uuids.timeBased(); + UUID deviceId2 = Uuids.timeBased(); deviceDao.save(new TenantId(tenantId1), getDevice(tenantId1, customerId1, deviceId1)); deviceDao.save(new TenantId(tenantId2), getDevice(tenantId2, customerId2, deviceId2)); deviceIds.add(deviceId1); @@ -106,16 +108,16 @@ public class JpaDeviceDaoTest extends AbstractJpaDaoTest { @Test public void testFindDevicesByTenantIdAndCustomerIdAndIdsAsync() throws ExecutionException, InterruptedException { - UUID tenantId1 = UUIDs.timeBased(); - UUID customerId1 = UUIDs.timeBased(); - UUID tenantId2 = UUIDs.timeBased(); - UUID customerId2 = UUIDs.timeBased(); + UUID tenantId1 = Uuids.timeBased(); + UUID customerId1 = Uuids.timeBased(); + UUID tenantId2 = Uuids.timeBased(); + UUID customerId2 = Uuids.timeBased(); List deviceIds = new ArrayList<>(); for(int i = 0; i < 20; i++) { - UUID deviceId1 = UUIDs.timeBased(); - UUID deviceId2 = UUIDs.timeBased(); + UUID deviceId1 = Uuids.timeBased(); + UUID deviceId2 = Uuids.timeBased(); deviceDao.save(new TenantId(tenantId1), getDevice(tenantId1, customerId1, deviceId1)); deviceDao.save(new TenantId(tenantId2), getDevice(tenantId2, customerId2, deviceId2)); deviceIds.add(deviceId1); @@ -135,7 +137,7 @@ public class JpaDeviceDaoTest extends AbstractJpaDaoTest { } private Device getDevice(UUID tenantId, UUID customerID) { - return getDevice(tenantId, customerID, UUIDs.timeBased()); + return getDevice(tenantId, customerID, Uuids.timeBased()); } private Device getDevice(UUID tenantId, UUID customerID, UUID deviceId) { diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java index 8f417f61a0..87c5544470 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.dao.sql.event; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.springtestdbunit.annotation.DatabaseSetup; @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EventId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.AbstractJpaDaoTest; import org.thingsboard.server.dao.event.EventDao; @@ -56,9 +57,9 @@ public class JpaBaseEventDaoTest extends AbstractJpaDaoTest { @Test public void testSaveIfNotExists() { - UUID eventId = UUIDs.timeBased(); - UUID tenantId = UUIDs.timeBased(); - UUID entityId = UUIDs.timeBased(); + UUID eventId = Uuids.timeBased(); + UUID tenantId = Uuids.timeBased(); + UUID entityId = Uuids.timeBased(); Event event = getEvent(eventId, tenantId, entityId); Optional optEvent1 = eventDao.saveIfNotExists(event); assertTrue("Optional is expected to be non-empty", optEvent1.isPresent()); @@ -82,72 +83,74 @@ public class JpaBaseEventDaoTest extends AbstractJpaDaoTest { @Test public void findEventsByEntityIdAndPageLink() { - UUID tenantId = UUIDs.timeBased(); - UUID entityId1 = UUIDs.timeBased(); - UUID entityId2 = UUIDs.timeBased(); + UUID tenantId = Uuids.timeBased(); + UUID entityId1 = Uuids.timeBased(); + UUID entityId2 = Uuids.timeBased(); long startTime = System.currentTimeMillis(); long endTime = createEventsTwoEntities(tenantId, entityId1, entityId2, startTime, 20); - TimePageLink pageLink1 = new TimePageLink(30, null, null, true); - List events1 = eventDao.findEvents(tenantId, new DeviceId(entityId1), pageLink1); - assertEquals(10, events1.size()); + TimePageLink pageLink1 = new TimePageLink(30); + PageData events1 = eventDao.findEvents(tenantId, new DeviceId(entityId1), pageLink1); + assertEquals(10, events1.getData().size()); - TimePageLink pageLink2 = new TimePageLink(30, startTime, null, true); - List events2 = eventDao.findEvents(tenantId, new DeviceId(entityId1), pageLink2); - assertEquals(10, events2.size()); + TimePageLink pageLink2 = new TimePageLink(30, 0, "", null, startTime, null); + PageData events2 = eventDao.findEvents(tenantId, new DeviceId(entityId1), pageLink2); + assertEquals(10, events2.getData().size()); - TimePageLink pageLink3 = new TimePageLink(30, startTime, endTime, true); - List events3 = eventDao.findEvents(tenantId, new DeviceId(entityId1), pageLink3); - assertEquals(10, events3.size()); + TimePageLink pageLink3 = new TimePageLink(30, 0, "", null, startTime, endTime); + PageData events3 = eventDao.findEvents(tenantId, new DeviceId(entityId1), pageLink3); + assertEquals(10, events3.getData().size()); - TimePageLink pageLink4 = new TimePageLink(5, startTime, endTime, true); - List events4 = eventDao.findEvents(tenantId, new DeviceId(entityId1), pageLink4); - assertEquals(5, events4.size()); + TimePageLink pageLink4 = new TimePageLink(5, 0, "", null, startTime, endTime); + PageData events4 = eventDao.findEvents(tenantId, new DeviceId(entityId1), pageLink4); + assertEquals(5, events4.getData().size()); - UUID idOffset = events4.get(4).getId().getId(); - TimePageLink pageLink5 = new TimePageLink(10, startTime, endTime, true, idOffset); - List events5 = eventDao.findEvents(tenantId, new DeviceId(entityId1), pageLink5); - assertEquals(5, events5.size()); + pageLink4 = pageLink4.nextPageLink(); + PageData events5 = eventDao.findEvents(tenantId, new DeviceId(entityId1), pageLink4); + assertEquals(5, events5.getData().size()); + + pageLink4 = pageLink4.nextPageLink(); + PageData events6 = eventDao.findEvents(tenantId, new DeviceId(entityId1), pageLink4); + assertEquals(0, events6.getData().size()); } @Test public void findEventsByEntityIdAndEventTypeAndPageLink() { - UUID tenantId = UUIDs.timeBased(); - UUID entityId1 = UUIDs.timeBased(); - UUID entityId2 = UUIDs.timeBased(); + UUID tenantId = Uuids.timeBased(); + UUID entityId1 = Uuids.timeBased(); + UUID entityId2 = Uuids.timeBased(); long startTime = System.currentTimeMillis(); long endTime = createEventsTwoEntitiesTwoTypes(tenantId, entityId1, entityId2, startTime, 20); - TimePageLink pageLink1 = new TimePageLink(30, null, null, true); - List events1 = eventDao.findEvents(tenantId, new DeviceId(entityId1), ALARM, pageLink1); - assertEquals(5, events1.size()); + TimePageLink pageLink1 = new TimePageLink(30); + PageData events1 = eventDao.findEvents(tenantId, new DeviceId(entityId1), ALARM, pageLink1); + assertEquals(5, events1.getData().size()); - TimePageLink pageLink2 = new TimePageLink(30, startTime, null, true); - List events2 = eventDao.findEvents(tenantId, new DeviceId(entityId1), ALARM, pageLink2); - assertEquals(5, events2.size()); + TimePageLink pageLink2 = new TimePageLink(30, 0, "", null, startTime, null); + PageData events2 = eventDao.findEvents(tenantId, new DeviceId(entityId1), ALARM, pageLink2); + assertEquals(5, events2.getData().size()); - TimePageLink pageLink3 = new TimePageLink(30, startTime, endTime, true); - List events3 = eventDao.findEvents(tenantId, new DeviceId(entityId1), ALARM, pageLink3); - assertEquals(5, events3.size()); + TimePageLink pageLink3 = new TimePageLink(30, 0, "", null, startTime, endTime); + PageData events3 = eventDao.findEvents(tenantId, new DeviceId(entityId1), ALARM, pageLink3); + assertEquals(5, events3.getData().size()); - TimePageLink pageLink4 = new TimePageLink(4, startTime, endTime, true); - List events4 = eventDao.findEvents(tenantId, new DeviceId(entityId1), ALARM, pageLink4); - assertEquals(4, events4.size()); + TimePageLink pageLink4 = new TimePageLink(4, 0, "", null, startTime, endTime); + PageData events4 = eventDao.findEvents(tenantId, new DeviceId(entityId1), ALARM, pageLink4); + assertEquals(4, events4.getData().size()); - UUID idOffset = events3.get(2).getId().getId(); - TimePageLink pageLink5 = new TimePageLink(10, startTime, endTime, true, idOffset); - List events5 = eventDao.findEvents(tenantId, new DeviceId(entityId1), ALARM, pageLink5); - assertEquals(2, events5.size()); + pageLink4 = pageLink4.nextPageLink(); + PageData events5 = eventDao.findEvents(tenantId, new DeviceId(entityId1), ALARM, pageLink4); + assertEquals(2, events5.getData().size()); } private long createEventsTwoEntitiesTwoTypes(UUID tenantId, UUID entityId1, UUID entityId2, long startTime, int count) { for (int i = 0; i < count / 2; i++) { String type = i % 2 == 0 ? STATS : ALARM; - UUID eventId1 = UUIDs.timeBased(); + UUID eventId1 = Uuids.timeBased(); Event event1 = getEvent(eventId1, tenantId, entityId1, type); eventDao.save(new TenantId(tenantId), event1); - UUID eventId2 = UUIDs.timeBased(); + UUID eventId2 = Uuids.timeBased(); Event event2 = getEvent(eventId2, tenantId, entityId2, type); eventDao.save(new TenantId(tenantId), event2); } @@ -156,10 +159,10 @@ public class JpaBaseEventDaoTest extends AbstractJpaDaoTest { private long createEventsTwoEntities(UUID tenantId, UUID entityId1, UUID entityId2, long startTime, int count) { for (int i = 0; i < count / 2; i++) { - UUID eventId1 = UUIDs.timeBased(); + UUID eventId1 = Uuids.timeBased(); Event event1 = getEvent(eventId1, tenantId, entityId1); eventDao.save(new TenantId(tenantId), event1); - UUID eventId2 = UUIDs.timeBased(); + UUID eventId2 = Uuids.timeBased(); Event event2 = getEvent(eventId2, tenantId, entityId2); eventDao.save(new TenantId(tenantId), event2); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDaoTest.java index 182927de63..850b72d6fa 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDaoTest.java @@ -15,13 +15,14 @@ */ package org.thingsboard.server.dao.sql.tenant; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import com.github.springtestdbunit.annotation.DatabaseSetup; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.AbstractJpaDaoTest; import org.thingsboard.server.dao.service.AbstractServiceTest; import org.thingsboard.server.dao.tenant.TenantDao; @@ -43,14 +44,20 @@ public class JpaTenantDaoTest extends AbstractJpaDaoTest { public void testFindTenantsByRegion() { createTenants(); assertEquals(60, tenantDao.find(AbstractServiceTest.SYSTEM_TENANT_ID).size()); - List tenants1 = tenantDao.findTenantsByRegion(AbstractServiceTest.SYSTEM_TENANT_ID, "REGION_1", new TextPageLink(20,"title")); - assertEquals(20, tenants1.size()); - List tenants2 = tenantDao.findTenantsByRegion(AbstractServiceTest.SYSTEM_TENANT_ID,"REGION_1", - new TextPageLink(20,"title", tenants1.get(19).getId().getId(), null)); - assertEquals(10, tenants2.size()); - List tenants3 = tenantDao.findTenantsByRegion(AbstractServiceTest.SYSTEM_TENANT_ID,"REGION_1", - new TextPageLink(20,"title", tenants2.get(9).getId().getId(), null)); - assertEquals(0, tenants3.size()); + + PageLink pageLink = new PageLink(20, 0, "title"); + PageData tenants1 = tenantDao.findTenantsByRegion(AbstractServiceTest.SYSTEM_TENANT_ID, "REGION_1", pageLink); + assertEquals(20, tenants1.getData().size()); + + pageLink = pageLink.nextPageLink(); + PageData tenants2 = tenantDao.findTenantsByRegion(AbstractServiceTest.SYSTEM_TENANT_ID,"REGION_1", + pageLink); + assertEquals(10, tenants2.getData().size()); + + pageLink = pageLink.nextPageLink(); + PageData tenants3 = tenantDao.findTenantsByRegion(AbstractServiceTest.SYSTEM_TENANT_ID,"REGION_1", + pageLink); + assertEquals(0, tenants3.getData().size()); } private void createTenants() { @@ -62,7 +69,7 @@ public class JpaTenantDaoTest extends AbstractJpaDaoTest { private void createTenant(String region, String title, int index) { Tenant tenant = new Tenant(); - tenant.setId(new TenantId(UUIDs.timeBased())); + tenant.setId(new TenantId(Uuids.timeBased())); tenant.setRegion(region); tenant.setTitle(title + "_" + index); tenantDao.save(AbstractServiceTest.SYSTEM_TENANT_ID, tenant); diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserDaoTest.java index d03f6cc42d..eb13bc1924 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserDaoTest.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.dao.sql.user; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.springtestdbunit.annotation.DatabaseSetup; @@ -25,7 +25,8 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.AbstractJpaDaoTest; import org.thingsboard.server.dao.service.AbstractServiceTest; @@ -70,33 +71,39 @@ public class JpaUserDaoTest extends AbstractJpaDaoTest { @Test @DatabaseSetup("classpath:dbunit/empty_dataset.xml") public void testFindTenantAdmins() { - UUID tenantId = UUIDs.timeBased(); - UUID customerId = UUIDs.timeBased(); + UUID tenantId = Uuids.timeBased(); + UUID customerId = Uuids.timeBased(); create30Adminsand60Users(tenantId, customerId); - List tenantAdmins1 = userDao.findTenantAdmins(tenantId, new TextPageLink(20)); - assertEquals(20, tenantAdmins1.size()); - List tenantAdmins2 = userDao.findTenantAdmins(tenantId, - new TextPageLink(20, null, tenantAdmins1.get(19).getId().getId(), null)); - assertEquals(10, tenantAdmins2.size()); - List tenantAdmins3 = userDao.findTenantAdmins(tenantId, - new TextPageLink(20, null, tenantAdmins2.get(9).getId().getId(), null)); - assertEquals(0, tenantAdmins3.size()); + PageLink pageLink = new PageLink(20); + PageData tenantAdmins1 = userDao.findTenantAdmins(tenantId, pageLink); + assertEquals(20, tenantAdmins1.getData().size()); + pageLink = pageLink.nextPageLink(); + PageData tenantAdmins2 = userDao.findTenantAdmins(tenantId, + pageLink); + assertEquals(10, tenantAdmins2.getData().size()); + pageLink = pageLink.nextPageLink(); + PageData tenantAdmins3 = userDao.findTenantAdmins(tenantId, + pageLink); + assertEquals(0, tenantAdmins3.getData().size()); } @Test @DatabaseSetup("classpath:dbunit/empty_dataset.xml") public void testFindCustomerUsers() { - UUID tenantId = UUIDs.timeBased(); - UUID customerId = UUIDs.timeBased(); + UUID tenantId = Uuids.timeBased(); + UUID customerId = Uuids.timeBased(); create30Adminsand60Users(tenantId, customerId); - List customerUsers1 = userDao.findCustomerUsers(tenantId, customerId, new TextPageLink(40)); - assertEquals(40, customerUsers1.size()); - List customerUsers2 = userDao.findCustomerUsers(tenantId, customerId, - new TextPageLink(20, null, customerUsers1.get(39).getId().getId(), null)); - assertEquals(20, customerUsers2.size()); - List customerUsers3 = userDao.findCustomerUsers(tenantId, customerId, - new TextPageLink(20, null, customerUsers2.get(19).getId().getId(), null)); - assertEquals(0, customerUsers3.size()); + PageLink pageLink = new PageLink(40); + PageData customerUsers1 = userDao.findCustomerUsers(tenantId, customerId, pageLink); + assertEquals(40, customerUsers1.getData().size()); + pageLink = pageLink.nextPageLink(); + PageData customerUsers2 = userDao.findCustomerUsers(tenantId, customerId, + pageLink); + assertEquals(20, customerUsers2.getData().size()); + pageLink = pageLink.nextPageLink(); + PageData customerUsers3 = userDao.findCustomerUsers(tenantId, customerId, + pageLink); + assertEquals(0, customerUsers3.getData().size()); } @Test @@ -131,7 +138,7 @@ public class JpaUserDaoTest extends AbstractJpaDaoTest { private void saveUser(UUID tenantId, UUID customerId) { User user = new User(); - UUID id = UUIDs.timeBased(); + UUID id = Uuids.timeBased(); user.setId(new UserId(id)); user.setTenantId(new TenantId(tenantId)); user.setCustomerId(new CustomerId(customerId)); diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java index c4ff5fad3d..c6ce8854ac 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.dao.sql.widget; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import com.github.springtestdbunit.annotation.DatabaseOperation; import com.github.springtestdbunit.annotation.DatabaseSetup; import com.github.springtestdbunit.annotation.DatabaseTearDown; @@ -23,7 +23,8 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetsBundleId; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.AbstractJpaDaoTest; import org.thingsboard.server.dao.service.AbstractServiceTest; @@ -65,20 +66,20 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest { createSystemWidgetBundles(30, "WB_"); assertEquals(30, widgetsBundleDao.find(AbstractServiceTest.SYSTEM_TENANT_ID).size()); // Get first page - TextPageLink textPageLink1 = new TextPageLink(10, "WB"); - List widgetsBundles1 = widgetsBundleDao.findSystemWidgetsBundles(AbstractServiceTest.SYSTEM_TENANT_ID, textPageLink1); - assertEquals(10, widgetsBundles1.size()); + PageLink pageLink = new PageLink(10, 0, "WB"); + PageData widgetsBundles1 = widgetsBundleDao.findSystemWidgetsBundles(AbstractServiceTest.SYSTEM_TENANT_ID, pageLink); + assertEquals(10, widgetsBundles1.getData().size()); // Get next page - TextPageLink textPageLink2 = new TextPageLink(10, "WB", widgetsBundles1.get(9).getId().getId(), null); - List widgetsBundles2 = widgetsBundleDao.findSystemWidgetsBundles(AbstractServiceTest.SYSTEM_TENANT_ID, textPageLink2); - assertEquals(10, widgetsBundles2.size()); + pageLink = pageLink.nextPageLink(); + PageData widgetsBundles2 = widgetsBundleDao.findSystemWidgetsBundles(AbstractServiceTest.SYSTEM_TENANT_ID, pageLink); + assertEquals(10, widgetsBundles2.getData().size()); } @Test @DatabaseSetup(value = "classpath:dbunit/widgets_bundle.xml", type= DatabaseOperation.DELETE_ALL) public void testFindWidgetsBundlesByTenantId() { - UUID tenantId1 = UUIDs.timeBased(); - UUID tenantId2 = UUIDs.timeBased(); + UUID tenantId1 = Uuids.timeBased(); + UUID tenantId2 = Uuids.timeBased(); // Create a bunch of widgetBundles for (int i= 0; i < 10; i++) { createWidgetBundles(3, tenantId1, "WB1_"); @@ -87,25 +88,24 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest { } assertEquals(180, widgetsBundleDao.find(AbstractServiceTest.SYSTEM_TENANT_ID).size()); - TextPageLink textPageLink1 = new TextPageLink(40, "WB"); - List widgetsBundles1 = widgetsBundleDao.findTenantWidgetsBundlesByTenantId(tenantId1, textPageLink1); - assertEquals(30, widgetsBundles1.size()); + PageLink pageLink1 = new PageLink(40, 0, "WB"); + PageData widgetsBundles1 = widgetsBundleDao.findTenantWidgetsBundlesByTenantId(tenantId1, pageLink1); + assertEquals(30, widgetsBundles1.getData().size()); - TextPageLink textPageLink2 = new TextPageLink(40, "WB"); - List widgetsBundles2 = widgetsBundleDao.findTenantWidgetsBundlesByTenantId(tenantId2, textPageLink2); - assertEquals(40, widgetsBundles2.size()); + PageLink pageLink2 = new PageLink(40, 0, "WB"); + PageData widgetsBundles2 = widgetsBundleDao.findTenantWidgetsBundlesByTenantId(tenantId2, pageLink2); + assertEquals(40, widgetsBundles2.getData().size()); - TextPageLink textPageLink3 = new TextPageLink(40, "WB", - widgetsBundles2.get(39).getId().getId(), null); - List widgetsBundles3 = widgetsBundleDao.findTenantWidgetsBundlesByTenantId(tenantId2, textPageLink3); - assertEquals(10, widgetsBundles3.size()); + pageLink2 = pageLink2.nextPageLink(); + PageData widgetsBundles3 = widgetsBundleDao.findTenantWidgetsBundlesByTenantId(tenantId2, pageLink2); + assertEquals(10, widgetsBundles3.getData().size()); } @Test @DatabaseSetup(value = "classpath:dbunit/widgets_bundle.xml", type= DatabaseOperation.DELETE_ALL) public void testFindAllWidgetsBundlesByTenantId() { - UUID tenantId1 = UUIDs.timeBased(); - UUID tenantId2 = UUIDs.timeBased(); + UUID tenantId1 = Uuids.timeBased(); + UUID tenantId2 = Uuids.timeBased(); // Create a bunch of widgetBundles for (int i= 0; i < 10; i++) { createWidgetBundles( 5, tenantId1,"WB1_"); @@ -113,38 +113,34 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest { createSystemWidgetBundles(2, "WB_SYS_"); } - TextPageLink textPageLink1 = new TextPageLink(30, "WB"); - List widgetsBundles1 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, textPageLink1); - assertEquals(30, widgetsBundles1.size()); + PageLink pageLink = new PageLink(30, 0, "WB"); + PageData widgetsBundles1 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, pageLink); + assertEquals(30, widgetsBundles1.getData().size()); - TextPageLink textPageLink2 = new TextPageLink(30, "WB", - widgetsBundles1.get(29).getId().getId(), null); - List widgetsBundles2 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, textPageLink2); + pageLink = pageLink.nextPageLink(); + PageData widgetsBundles2 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, pageLink); + assertEquals(30, widgetsBundles2.getData().size()); - assertEquals(30, widgetsBundles2.size()); + pageLink = pageLink.nextPageLink(); + PageData widgetsBundles3 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, pageLink); + assertEquals(10, widgetsBundles3.getData().size()); - TextPageLink textPageLink3 = new TextPageLink(30, "WB", - widgetsBundles2.get(29).getId().getId(), null); - List widgetsBundles3 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, textPageLink3); - assertEquals(10, widgetsBundles3.size()); - - TextPageLink textPageLink4 = new TextPageLink(30, "WB", - widgetsBundles3.get(9).getId().getId(), null); - List widgetsBundles4 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, textPageLink4); - assertEquals(0, widgetsBundles4.size()); + pageLink = pageLink.nextPageLink(); + PageData widgetsBundles4 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, pageLink); + assertEquals(0, widgetsBundles4.getData().size()); } @Test @DatabaseSetup("classpath:dbunit/empty_dataset.xml") @DatabaseTearDown(value = "classpath:dbunit/empty_dataset.xml", type= DatabaseOperation.DELETE_ALL) public void testSearchTextNotFound() { - UUID tenantId = UUIDs.timeBased(); + UUID tenantId = Uuids.timeBased(); createWidgetBundles(5, tenantId, "ABC_"); createSystemWidgetBundles(5, "SYS_"); - TextPageLink textPageLink = new TextPageLink(30, "TEXT_NOT_FOUND"); - List widgetsBundles4 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId, textPageLink); - assertEquals(0, widgetsBundles4.size()); + PageLink textPageLink = new PageLink(30, 0, "TEXT_NOT_FOUND"); + PageData widgetsBundles4 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId, textPageLink); + assertEquals(0, widgetsBundles4.getData().size()); } private void createWidgetBundles(int count, UUID tenantId, String prefix) { @@ -152,7 +148,7 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest { WidgetsBundle widgetsBundle = new WidgetsBundle(); widgetsBundle.setAlias(prefix + i); widgetsBundle.setTitle(prefix + i); - widgetsBundle.setId(new WidgetsBundleId(UUIDs.timeBased())); + widgetsBundle.setId(new WidgetsBundleId(Uuids.timeBased())); widgetsBundle.setTenantId(new TenantId(tenantId)); widgetsBundleDao.save(AbstractServiceTest.SYSTEM_TENANT_ID, widgetsBundle); } @@ -163,7 +159,7 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest { widgetsBundle.setAlias(prefix + i); widgetsBundle.setTitle(prefix + i); widgetsBundle.setTenantId(new TenantId(NULL_UUID)); - widgetsBundle.setId(new WidgetsBundleId(UUIDs.timeBased())); + widgetsBundle.setId(new WidgetsBundleId(Uuids.timeBased())); widgetsBundleDao.save(AbstractServiceTest.SYSTEM_TENANT_ID, widgetsBundle); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/util/DaoTestUtil.java b/dao/src/test/java/org/thingsboard/server/dao/util/DaoTestUtil.java new file mode 100644 index 0000000000..9484610fc4 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/util/DaoTestUtil.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.util; + +import org.springframework.jdbc.core.JdbcTemplate; + +import java.sql.DriverManager; + +public class DaoTestUtil { + private static final String POSTGRES_DRIVER_CLASS = "org.postgresql.Driver"; + private static final String H2_DRIVER_CLASS = "org.hsqldb.jdbc.JDBCDriver"; + + + public static SqlDbType getSqlDbType(JdbcTemplate template){ + try { + String driverName = DriverManager.getDriver(template.getDataSource().getConnection().getMetaData().getURL()).getClass().getName(); + if (POSTGRES_DRIVER_CLASS.equals(driverName)) { + return SqlDbType.POSTGRES; + } else if (H2_DRIVER_CLASS.equals(driverName)) { + return SqlDbType.H2; + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/util/SqlDbType.java b/dao/src/test/java/org/thingsboard/server/dao/util/SqlDbType.java new file mode 100644 index 0000000000..4e38a48189 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/util/SqlDbType.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.util; + +public enum SqlDbType { + POSTGRES, H2; +} diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index caec9e0e66..36d73a96ca 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -30,6 +30,12 @@ caffeine.specs.entityViews.maxSize=100000 caffeine.specs.claimDevices.timeToLiveInMinutes=1440 caffeine.specs.claimDevices.maxSize=100000 +caffeine.specs.tenantProfiles.timeToLiveInMinutes=1440 +caffeine.specs.tenantProfiles.maxSize=100000 + +caffeine.specs.deviceProfiles.timeToLiveInMinutes=1440 +caffeine.specs.deviceProfiles.maxSize=100000 + redis.connection.host=localhost redis.connection.port=6379 redis.connection.db=0 diff --git a/dao/src/test/resources/cassandra-test.properties b/dao/src/test/resources/cassandra-test.properties index 51f34a08d6..43a78abac4 100644 --- a/dao/src/test/resources/cassandra-test.properties +++ b/dao/src/test/resources/cassandra-test.properties @@ -4,11 +4,19 @@ cassandra.keyspace_name=thingsboard cassandra.url=127.0.0.1:9142 -cassandra.ssl=false +cassandra.local_datacenter=datacenter1 + +cassandra.ssl.enabled=false +cassandra.ssl.hostname_validation=false +cassandra.ssl.trust_store= +cassandra.ssl.trust_store_password= +cassandra.ssl.key_store= +cassandra.ssl.key_store_password= +cassandra.ssl.cipher_suites= cassandra.jmx=false -cassandra.metrics=true +cassandra.metrics=false cassandra.compression=none @@ -60,4 +68,4 @@ cassandra.query.tenant_rate_limits.enabled=false cassandra.query.tenant_rate_limits.configuration=5000:1,100000:60 cassandra.query.tenant_rate_limits.print_tenant_names=false -service.type=monolith \ No newline at end of file +service.type=monolith diff --git a/dao/src/test/resources/cassandra/system-test.cql b/dao/src/test/resources/cassandra/system-test.cql deleted file mode 100644 index de4e325745..0000000000 --- a/dao/src/test/resources/cassandra/system-test.cql +++ /dev/null @@ -1,27 +0,0 @@ -TRUNCATE thingsboard.rule_chain; -TRUNCATE thingsboard.rule_node; - --- msg_queue dataset - -INSERT INTO thingsboard.msg_queue (node_id, cluster_partition, ts_partition, ts, msg) - VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 200, 201, null); -INSERT INTO thingsboard.msg_queue (node_id, cluster_partition, ts_partition, ts, msg) - VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 200, 202, null); -INSERT INTO thingsboard.msg_queue (node_id, cluster_partition, ts_partition, ts, msg) - VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 300, 301, null); - --- ack_queue dataset -INSERT INTO thingsboard.msg_ack_queue (node_id, cluster_partition, ts_partition, msg_id) - VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 300, bebaeb60-1888-11e8-bf21-65b5d5335ba9); -INSERT INTO thingsboard.msg_ack_queue (node_id, cluster_partition, ts_partition, msg_id) - VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 300, 12baeb60-1888-11e8-bf21-65b5d5335ba9); -INSERT INTO thingsboard.msg_ack_queue (node_id, cluster_partition, ts_partition, msg_id) - VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 200, 32baeb60-1888-11e8-bf21-65b5d5335ba9); - --- processed partition dataset -INSERT INTO thingsboard.processed_msg_partitions (node_id, cluster_partition, ts_partition) - VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 100); -INSERT INTO thingsboard.processed_msg_partitions (node_id, cluster_partition, ts_partition) - VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 777); -INSERT INTO thingsboard.processed_msg_partitions (node_id, cluster_partition, ts_partition) - VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 202, 200); \ No newline at end of file diff --git a/dao/src/test/resources/nosql-test.properties b/dao/src/test/resources/nosql-test.properties index 7c3ec51e8a..eefa4f0b95 100644 --- a/dao/src/test/resources/nosql-test.properties +++ b/dao/src/test/resources/nosql-test.properties @@ -1,2 +1,17 @@ -database.entities.type=cassandra database.ts.type=cassandra +database.ts_latest.type=cassandra + +sql.ts_inserts_executor_type=fixed +sql.ts_inserts_fixed_thread_pool_size=10 + +spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true +spring.jpa.properties.hibernate.order_by.default_null_ordering=last +spring.jpa.show-sql=false +spring.jpa.hibernate.ddl-auto=none +spring.jpa.database-platform=org.hibernate.dialect.HSQLDialect + +spring.datasource.username=sa +spring.datasource.password= +spring.datasource.url=jdbc:hsqldb:file:/tmp/testDb;sql.enforce_size=false +spring.datasource.driverClassName=org.hsqldb.jdbc.JDBCDriver +spring.datasource.hikari.maximumPoolSize = 50 diff --git a/dao/src/test/resources/sql-test.properties b/dao/src/test/resources/sql-test.properties index 8c8f3aa174..058d7ac056 100644 --- a/dao/src/test/resources/sql-test.properties +++ b/dao/src/test/resources/sql-test.properties @@ -1,11 +1,12 @@ database.ts.type=sql -database.entities.type=sql +database.ts_latest.type=sql sql.ts_inserts_executor_type=fixed sql.ts_inserts_fixed_thread_pool_size=200 sql.ts_key_value_partitioning=MONTHS - +# spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true +spring.jpa.properties.hibernate.order_by.default_null_ordering=last spring.jpa.show-sql=false spring.jpa.hibernate.ddl-auto=validate spring.jpa.database-platform=org.hibernate.dialect.HSQLDialect @@ -46,4 +47,6 @@ queue.rule-engine.queues[0].poll-interval=25 queue.rule-engine.queues[0].partitions=3 queue.rule-engine.queues[0].pack-processing-timeout=3000 queue.rule-engine.queues[0].processing-strategy.type=SKIP_ALL_FAILURES -queue.rule-engine.queues[0].submit-strategy.type=BURST \ No newline at end of file +queue.rule-engine.queues[0].submit-strategy.type=BURST + +sql.log_entity_queries=true \ No newline at end of file diff --git a/dao/src/test/resources/sql/hsql/drop-all-tables.sql b/dao/src/test/resources/sql/hsql/drop-all-tables.sql index 778a4bbd9a..b5629b0560 100644 --- a/dao/src/test/resources/sql/hsql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/hsql/drop-all-tables.sql @@ -18,6 +18,14 @@ DROP TABLE IF EXISTS ts_kv_latest; DROP TABLE IF EXISTS user_credentials; DROP TABLE IF EXISTS widget_type; DROP TABLE IF EXISTS widgets_bundle; +DROP TABLE IF EXISTS entity_view; +DROP TABLE IF EXISTS device_profile; +DROP TABLE IF EXISTS tenant_profile; +DROP TABLE IF EXISTS rule_node_state; DROP TABLE IF EXISTS rule_node; DROP TABLE IF EXISTS rule_chain; -DROP TABLE IF EXISTS entity_view; +DROP TABLE IF EXISTS oauth2_client_registration; +DROP TABLE IF EXISTS oauth2_client_registration_info; +DROP TABLE IF EXISTS oauth2_client_registration_template; +DROP TABLE IF EXISTS api_usage_state; +DROP FUNCTION IF EXISTS to_uuid; diff --git a/dao/src/test/resources/sql/psql/drop-all-tables.sql b/dao/src/test/resources/sql/psql/drop-all-tables.sql index b2e4a27963..34985883d7 100644 --- a/dao/src/test/resources/sql/psql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/psql/drop-all-tables.sql @@ -18,7 +18,14 @@ DROP TABLE IF EXISTS ts_kv_dictionary; DROP TABLE IF EXISTS user_credentials; DROP TABLE IF EXISTS widget_type; DROP TABLE IF EXISTS widgets_bundle; +DROP TABLE IF EXISTS entity_view; +DROP TABLE IF EXISTS device_profile; +DROP TABLE IF EXISTS tenant_profile; +DROP TABLE IF EXISTS rule_node_state; DROP TABLE IF EXISTS rule_node; DROP TABLE IF EXISTS rule_chain; -DROP TABLE IF EXISTS entity_view; -DROP TABLE IF EXISTS tb_schema_settings; \ No newline at end of file +DROP TABLE IF EXISTS tb_schema_settings; +DROP TABLE IF EXISTS oauth2_client_registration; +DROP TABLE IF EXISTS oauth2_client_registration_info; +DROP TABLE IF EXISTS oauth2_client_registration_template; +DROP TABLE IF EXISTS api_usage_state; \ No newline at end of file diff --git a/dao/src/test/resources/sql/system-data.sql b/dao/src/test/resources/sql/system-data.sql new file mode 100644 index 0000000000..339d6e94ba --- /dev/null +++ b/dao/src/test/resources/sql/system-data.sql @@ -0,0 +1,45 @@ +-- +-- Copyright © 2016-2020 The Thingsboard Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +/** SYSTEM **/ + +/** System admin **/ +INSERT INTO tb_user ( id, created_time, tenant_id, customer_id, email, search_text, authority ) +VALUES ( '5a797660-4612-11e7-a919-92ebcb67fe33', 1592576748000, '13814000-1dd2-11b2-8080-808080808080', '13814000-1dd2-11b2-8080-808080808080', 'sysadmin@thingsboard.org', + 'sysadmin@thingsboard.org', 'SYS_ADMIN' ); + +INSERT INTO user_credentials ( id, created_time, user_id, enabled, password ) +VALUES ( '61441950-4612-11e7-a919-92ebcb67fe33', 1592576748000, '5a797660-4612-11e7-a919-92ebcb67fe33', true, + '$2a$10$5JTB8/hxWc9WAy62nCGSxeefl3KWmipA9nFpVdDa0/xfIseeBB4Bu' ); + +/** System settings **/ +INSERT INTO admin_settings ( id, created_time, key, json_value ) +VALUES ( '6a2266e4-4612-11e7-a919-92ebcb67fe33', 1592576748000, 'general', '{ + "baseUrl": "http://localhost:8080" +}' ); + +INSERT INTO admin_settings ( id, created_time, key, json_value ) +VALUES ( '6eaaefa6-4612-11e7-a919-92ebcb67fe33', 1592576748000, 'mail', '{ + "mailFrom": "Thingsboard ", + "smtpProtocol": "smtp", + "smtpHost": "localhost", + "smtpPort": "25", + "timeout": "10000", + "enableTls": false, + "tlsVersion": "TLSv1.2", + "username": "", + "password": "" +}' ); diff --git a/dao/src/test/resources/sql/system-test.sql b/dao/src/test/resources/sql/system-test.sql index e59afd914c..68a0e0efa3 100644 --- a/dao/src/test/resources/sql/system-test.sql +++ b/dao/src/test/resources/sql/system-test.sql @@ -1,2 +1,6 @@ +TRUNCATE TABLE device_credentials; +TRUNCATE TABLE device; +TRUNCATE TABLE device_profile; +TRUNCATE TABLE rule_node_state; TRUNCATE TABLE rule_node; TRUNCATE TABLE rule_chain; \ No newline at end of file diff --git a/dao/src/test/resources/sql/timescale/drop-all-tables.sql b/dao/src/test/resources/sql/timescale/drop-all-tables.sql index b2e4a27963..211cebb2ad 100644 --- a/dao/src/test/resources/sql/timescale/drop-all-tables.sql +++ b/dao/src/test/resources/sql/timescale/drop-all-tables.sql @@ -18,7 +18,14 @@ DROP TABLE IF EXISTS ts_kv_dictionary; DROP TABLE IF EXISTS user_credentials; DROP TABLE IF EXISTS widget_type; DROP TABLE IF EXISTS widgets_bundle; +DROP TABLE IF EXISTS rule_node_state; DROP TABLE IF EXISTS rule_node; DROP TABLE IF EXISTS rule_chain; DROP TABLE IF EXISTS entity_view; -DROP TABLE IF EXISTS tb_schema_settings; \ No newline at end of file +DROP TABLE IF EXISTS device_profile; +DROP TABLE IF EXISTS tenant_profile; +DROP TABLE IF EXISTS tb_schema_settings; +DROP TABLE IF EXISTS oauth2_client_registration; +DROP TABLE IF EXISTS oauth2_client_registration_info; +DROP TABLE IF EXISTS oauth2_client_registration_template; +DROP TABLE IF EXISTS api_usage_state; \ No newline at end of file diff --git a/docker/.env b/docker/.env index bd4b4730a6..b632300d5f 100644 --- a/docker/.env +++ b/docker/.env @@ -11,8 +11,8 @@ COAP_TRANSPORT_DOCKER_NAME=tb-coap-transport TB_VERSION=latest -# Database used by ThingsBoard, can be either postgres (PostgreSQL) or cassandra (Cassandra). -# According to the database type corresponding docker service will be deployed (see docker-compose.postgres.yml, docker-compose.cassandra.yml for details). +# Database used by ThingsBoard, can be either postgres (PostgreSQL) or hybrid (PostgreSQL for entities database and Cassandra for timeseries database). +# According to the database type corresponding docker service will be deployed (see docker-compose.postgres.yml, docker-compose.hybrid.yml for details). DATABASE=postgres diff --git a/docker/README.md b/docker/README.md index 9436386010..a1051c039e 100644 --- a/docker/README.md +++ b/docker/README.md @@ -13,9 +13,9 @@ Before performing initial installation you can configure the type of database to In order to set database type change the value of `DATABASE` variable in `.env` file to one of the following: - `postgres` - use PostgreSQL database; -- `cassandra` - use Cassandra database; +- `hybrid` - use PostgreSQL for entities database and Cassandra for timeseries database; -**NOTE**: According to the database type corresponding docker service will be deployed (see `docker-compose.postgres.yml`, `docker-compose.cassandra.yml` for details). +**NOTE**: According to the database type corresponding docker service will be deployed (see `docker-compose.postgres.yml`, `docker-compose.hybrid.yml` for details). Execute the following command to create log folders for the services and chown of these folders to the docker container users. To be able to change user, **chown** command is used, which requires sudo permissions (script will request password for a sudo access): diff --git a/docker/compose-utils.sh b/docker/compose-utils.sh index a1fe74ae3f..31d4e0a00f 100755 --- a/docker/compose-utils.sh +++ b/docker/compose-utils.sh @@ -22,11 +22,11 @@ function additionalComposeArgs() { postgres) ADDITIONAL_COMPOSE_ARGS="-f docker-compose.postgres.yml" ;; - cassandra) - ADDITIONAL_COMPOSE_ARGS="-f docker-compose.cassandra.yml" + hybrid) + ADDITIONAL_COMPOSE_ARGS="-f docker-compose.hybrid.yml" ;; *) - echo "Unknown DATABASE value specified: '${DATABASE}'. Should be either postgres or cassandra." >&2 + echo "Unknown DATABASE value specified: '${DATABASE}'. Should be either postgres or hybrid." >&2 exit 1 esac echo $ADDITIONAL_COMPOSE_ARGS @@ -68,11 +68,11 @@ function additionalStartupServices() { postgres) ADDITIONAL_STARTUP_SERVICES=postgres ;; - cassandra) - ADDITIONAL_STARTUP_SERVICES=cassandra + hybrid) + ADDITIONAL_STARTUP_SERVICES="postgres cassandra" ;; *) - echo "Unknown DATABASE value specified: '${DATABASE}'. Should be either postgres or cassandra." >&2 + echo "Unknown DATABASE value specified: '${DATABASE}'. Should be either postgres or hybrid." >&2 exit 1 esac echo $ADDITIONAL_STARTUP_SERVICES diff --git a/docker/docker-compose.cassandra.yml b/docker/docker-compose.cassandra.yml deleted file mode 100644 index d45fd6e9df..0000000000 --- a/docker/docker-compose.cassandra.yml +++ /dev/null @@ -1,54 +0,0 @@ -# -# Copyright © 2016-2020 The Thingsboard Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -version: '2.2' - -services: - cassandra: - restart: always - image: "cassandra:3.11.3" - ports: - - "9042" - volumes: - - ./tb-node/cassandra:/var/lib/cassandra - tb-core1: - env_file: - - tb-node.cassandra.env - depends_on: - - zookeeper - - redis - - cassandra - tb-core2: - env_file: - - tb-node.cassandra.env - depends_on: - - zookeeper - - redis - - cassandra - tb-rule-engine1: - env_file: - - tb-node.cassandra.env - depends_on: - - zookeeper - - redis - - cassandra - tb-rule-engine2: - env_file: - - tb-node.cassandra.env - depends_on: - - zookeeper - - redis - - cassandra diff --git a/docker/docker-compose.hybrid.yml b/docker/docker-compose.hybrid.yml new file mode 100644 index 0000000000..20231292ce --- /dev/null +++ b/docker/docker-compose.hybrid.yml @@ -0,0 +1,68 @@ +# +# Copyright © 2016-2020 The Thingsboard Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +version: '2.2' + +services: + postgres: + restart: always + image: "postgres:11.6" + ports: + - "5432" + environment: + POSTGRES_DB: thingsboard + POSTGRES_PASSWORD: postgres + volumes: + - ./tb-node/postgres:/var/lib/postgresql/data + cassandra: + restart: always + image: "cassandra:3.11.3" + ports: + - "9042" + volumes: + - ./tb-node/cassandra:/var/lib/cassandra + tb-core1: + env_file: + - tb-node.hybrid.env + depends_on: + - zookeeper + - redis + - postgres + - cassandra + tb-core2: + env_file: + - tb-node.hybrid.env + depends_on: + - zookeeper + - redis + - postgres + - cassandra + tb-rule-engine1: + env_file: + - tb-node.hybrid.env + depends_on: + - zookeeper + - redis + - postgres + - cassandra + tb-rule-engine2: + env_file: + - tb-node.hybrid.env + depends_on: + - zookeeper + - redis + - postgres + - cassandra diff --git a/docker/haproxy/config/haproxy.cfg b/docker/haproxy/config/haproxy.cfg index dfe49247f5..63c566086e 100644 --- a/docker/haproxy/config/haproxy.cfg +++ b/docker/haproxy/config/haproxy.cfg @@ -56,20 +56,17 @@ frontend http-in reqadd X-Forwarded-Proto:\ http - acl acl_static path_beg /static/ /index.html - acl acl_static path / - acl acl_static_rulenode path_beg /static/rulenode/ - acl transport_http_acl path_beg /api/v1/ acl letsencrypt_http_acl path_beg /.well-known/acme-challenge/ + acl tb_api_acl path_beg /api/ /swagger /webjars /v2/ /static/rulenode/ /oauth2/ /login/oauth2/ redirect scheme https if !letsencrypt_http_acl !transport_http_acl { env(FORCE_HTTPS_REDIRECT) -m str true } use_backend letsencrypt_http if letsencrypt_http_acl use_backend tb-http-backend if transport_http_acl - use_backend tb-web-backend if acl_static !acl_static_rulenode + use_backend tb-api-backend if tb_api_acl - default_backend tb-api-backend + default_backend tb-web-backend frontend https_in bind *:${HTTPS_PORT} ssl crt /usr/local/etc/haproxy/default.pem crt /usr/local/etc/haproxy/certs.d ciphers ECDHE-RSA-AES256-SHA:RC4-SHA:RC4:HIGH:!MD5:!aNULL:!EDH:!AESGCM @@ -79,15 +76,12 @@ frontend https_in reqadd X-Forwarded-Proto:\ https acl transport_http_acl path_beg /api/v1/ - - acl acl_static path_beg /static/ /index.html - acl acl_static path / - acl acl_static_rulenode path_beg /static/rulenode/ + acl tb_api_acl path_beg /api/ /swagger /webjars /v2/ /static/rulenode/ /oauth2/ /login/oauth2/ use_backend tb-http-backend if transport_http_acl - use_backend tb-web-backend if acl_static !acl_static_rulenode + use_backend tb-api-backend if tb_api_acl - default_backend tb-api-backend + default_backend tb-web-backend backend letsencrypt_http server letsencrypt_http_srv 127.0.0.1:8080 diff --git a/docker/kafka.env b/docker/kafka.env index 81c01dae93..bd23f99ad1 100644 --- a/docker/kafka.env +++ b/docker/kafka.env @@ -4,7 +4,7 @@ KAFKA_LISTENERS=INSIDE://:9093,OUTSIDE://:9092 KAFKA_ADVERTISED_LISTENERS=INSIDE://:9093,OUTSIDE://kafka:9092 KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT KAFKA_INTER_BROKER_LISTENER_NAME=INSIDE -KAFKA_CREATE_TOPICS=js_eval.requests:3:1:delete --config=retention.ms=60000 --config=segment.bytes=26214400 --config=retention.bytes=104857600,tb_transport.api.requests:3:1:delete --config=retention.ms=60000 --config=segment.bytes=26214400 --config=retention.bytes=104857600 +KAFKA_CREATE_TOPICS=js_eval.requests:100:1:delete --config=retention.ms=60000 --config=segment.bytes=26214400 --config=retention.bytes=104857600,tb_transport.api.requests:3:1:delete --config=retention.ms=60000 --config=segment.bytes=26214400 --config=retention.bytes=104857600 KAFKA_AUTO_CREATE_TOPICS_ENABLE=false KAFKA_LOG_RETENTION_BYTES=1073741824 KAFKA_LOG_SEGMENT_BYTES=268435456 diff --git a/docker/tb-node.cassandra.env b/docker/tb-node.cassandra.env deleted file mode 100644 index 8d813b9059..0000000000 --- a/docker/tb-node.cassandra.env +++ /dev/null @@ -1,5 +0,0 @@ -# ThingsBoard server configuration for Cassandra database - -DATABASE_TS_TYPE=cassandra -DATABASE_ENTITIES_TYPE=cassandra -CASSANDRA_URL=cassandra:9042 diff --git a/docker/tb-node.hybrid.env b/docker/tb-node.hybrid.env new file mode 100644 index 0000000000..2f5dcee2e3 --- /dev/null +++ b/docker/tb-node.hybrid.env @@ -0,0 +1,9 @@ +# ThingsBoard server configuration for Cassandra database + +DATABASE_TS_TYPE=cassandra +CASSANDRA_URL=cassandra:9042 +SPRING_JPA_DATABASE_PLATFORM=org.hibernate.dialect.PostgreSQLDialect +SPRING_DRIVER_CLASS_NAME=org.postgresql.Driver +SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/thingsboard +SPRING_DATASOURCE_USERNAME=postgres +SPRING_DATASOURCE_PASSWORD=postgres diff --git a/docker/tb-node.postgres.env b/docker/tb-node.postgres.env index 9fa79e369c..63e8d11889 100644 --- a/docker/tb-node.postgres.env +++ b/docker/tb-node.postgres.env @@ -1,7 +1,6 @@ # ThingsBoard server configuration for PostgreSQL database DATABASE_TS_TYPE=sql -DATABASE_ENTITIES_TYPE=sql SPRING_JPA_DATABASE_PLATFORM=org.hibernate.dialect.PostgreSQLDialect SPRING_DRIVER_CLASS_NAME=org.postgresql.Driver SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/thingsboard diff --git a/k8s/.env b/k8s/.env index 5f6b634b45..4f0478683b 100644 --- a/k8s/.env +++ b/k8s/.env @@ -1,8 +1,8 @@ # Can be either basic (with single instance of Zookeeper, Kafka and Redis) or high-availability (with Zookeeper, Kafka and Redis in cluster modes). # According to the deployment type corresponding kubernetes resources will be deployed (see content of the directories ./basic and ./high-availability for details). -DEPLOYMENT_TYPE=high-availability +DEPLOYMENT_TYPE=basic -# Database used by ThingsBoard, can be either postgres (PostgreSQL) or cassandra (Cassandra). +# Database used by ThingsBoard, can be either postgres (PostgreSQL) or hybrid (PostgreSQL for entities database and Cassandra for timeseries database). # According to the database type corresponding kubernetes resources will be deployed (see postgres.yml, cassandra.yml for details). -DATABASE=cassandra +DATABASE=postgres diff --git a/k8s/README.md b/k8s/README.md index 5e563039aa..9d91c8c0e8 100644 --- a/k8s/README.md +++ b/k8s/README.md @@ -24,7 +24,7 @@ Before performing initial installation you can configure the type of database to To set database type change the value of `DATABASE` variable in `.env` file to one of the following: - `postgres` - use PostgreSQL database; -- `cassandra` - use Cassandra database; +- `hybrid` - use PostgreSQL for entities database and Cassandra for timeseries database; **NOTE**: According to the database type corresponding kubernetes resources will be deployed (see `postgres.yml`, `cassandra.yml` for details). diff --git a/k8s/common/cassandra.yml b/k8s/common/cassandra.yml index cefeb8ed18..32bc8e5271 100644 --- a/k8s/common/cassandra.yml +++ b/k8s/common/cassandra.yml @@ -114,7 +114,7 @@ spec: - name: CASSANDRA_CLUSTER_NAME value: "Thingsboard Cluster" - name: CASSANDRA_DC - value: "DC1-Thingsboard-Cluster" + value: "datacenter1" - name: CASSANDRA_RACK value: "Rack-Thingsboard-Cluster" - name: CASSANDRA_AUTO_BOOTSTRAP diff --git a/k8s/common/tb-node-cassandra-configmap.yml b/k8s/common/tb-node-cassandra-configmap.yml deleted file mode 100644 index 3891d6b6c5..0000000000 --- a/k8s/common/tb-node-cassandra-configmap.yml +++ /dev/null @@ -1,28 +0,0 @@ -# -# Copyright © 2016-2020 The Thingsboard Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -apiVersion: v1 -kind: ConfigMap -metadata: - name: tb-node-db-config - namespace: thingsboard - labels: - name: tb-node-db-config -data: - DATABASE_TS_TYPE: cassandra - DATABASE_ENTITIES_TYPE: cassandra - CASSANDRA_URL: cassandra:9042 - CASSANDRA_SOCKET_READ_TIMEOUT: "60000" diff --git a/k8s/common/tb-node-hybrid-configmap.yml b/k8s/common/tb-node-hybrid-configmap.yml new file mode 100644 index 0000000000..43f78b36a5 --- /dev/null +++ b/k8s/common/tb-node-hybrid-configmap.yml @@ -0,0 +1,32 @@ +# +# Copyright © 2016-2020 The Thingsboard Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +apiVersion: v1 +kind: ConfigMap +metadata: + name: tb-node-db-config + namespace: thingsboard + labels: + name: tb-node-db-config +data: + SPRING_JPA_DATABASE_PLATFORM: org.hibernate.dialect.PostgreSQLDialect + SPRING_DRIVER_CLASS_NAME: org.postgresql.Driver + SPRING_DATASOURCE_URL: jdbc:postgresql://tb-database:5432/thingsboard + SPRING_DATASOURCE_USERNAME: postgres + SPRING_DATASOURCE_PASSWORD: postgres + DATABASE_TS_TYPE: cassandra + CASSANDRA_URL: cassandra:9042 + CASSANDRA_SOCKET_READ_TIMEOUT: "60000" diff --git a/k8s/common/tb-node-postgres-configmap.yml b/k8s/common/tb-node-postgres-configmap.yml index 468536bc82..300ac676d0 100644 --- a/k8s/common/tb-node-postgres-configmap.yml +++ b/k8s/common/tb-node-postgres-configmap.yml @@ -23,7 +23,6 @@ metadata: name: tb-node-db-config data: DATABASE_TS_TYPE: sql - DATABASE_ENTITIES_TYPE: sql SPRING_JPA_DATABASE_PLATFORM: org.hibernate.dialect.PostgreSQLDialect SPRING_DRIVER_CLASS_NAME: org.postgresql.Driver SPRING_DATASOURCE_URL: jdbc:postgresql://tb-database:5432/thingsboard diff --git a/k8s/common/thingsboard.yml b/k8s/common/thingsboard.yml index bb58ded9dd..af4ff0d678 100644 --- a/k8s/common/thingsboard.yml +++ b/k8s/common/thingsboard.yml @@ -338,17 +338,33 @@ spec: backend: serviceName: tb-http-transport servicePort: 8080 + - path: /api/.* + backend: + serviceName: tb-node + servicePort: 8080 + - path: /swagger.* + backend: + serviceName: tb-node + servicePort: 8080 + - path: /webjars.* + backend: + serviceName: tb-node + servicePort: 8080 + - path: /v2/.* + backend: + serviceName: tb-node + servicePort: 8080 - path: /static/rulenode/.* backend: serviceName: tb-node servicePort: 8080 - - path: /static/.* + - path: /oauth2/.* backend: - serviceName: tb-web-ui + serviceName: tb-node servicePort: 8080 - - path: /index.html.* + - path: /login/oauth2/.* backend: - serviceName: tb-web-ui + serviceName: tb-node servicePort: 8080 - path: / backend: @@ -356,6 +372,6 @@ spec: servicePort: 8080 - path: /.* backend: - serviceName: tb-node + serviceName: tb-web-ui servicePort: 8080 ---- \ No newline at end of file +--- diff --git a/k8s/k8s-install-tb.sh b/k8s/k8s-install-tb.sh index 50ea6db443..4559dd4560 100755 --- a/k8s/k8s-install-tb.sh +++ b/k8s/k8s-install-tb.sh @@ -36,11 +36,13 @@ function installPostgres() { kubectl rollout status deployment/postgres } -function installCassandra() { +function installHybrid() { + kubectl apply -f common/postgres.yml kubectl apply -f common/cassandra.yml - kubectl apply -f common/tb-node-cassandra-configmap.yml + kubectl apply -f common/tb-node-hybrid-configmap.yml + kubectl rollout status deployment/postgres kubectl rollout status statefulset/cassandra kubectl exec -it cassandra-0 -- bash -c "cqlsh -e \ @@ -93,12 +95,12 @@ case $DATABASE in installPostgres installTb ${loadDemo} ;; - cassandra) - installCassandra + hybrid) + installHybrid installTb ${loadDemo} ;; *) - echo "Unknown DATABASE value specified: '${DATABASE}'. Should be either postgres or cassandra." >&2 + echo "Unknown DATABASE value specified: '${DATABASE}'. Should be either postgres or hybrid." >&2 exit 1 esac diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index d24f38bc36..74948b0e84 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT msa org.thingsboard.msa @@ -37,7 +37,6 @@ true 1.9.1 1.10 - 1.3.9 4.5.6 @@ -55,7 +54,6 @@ org.java-websocket Java-WebSocket - ${java-websocket.version} org.apache.httpcomponents diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/WsClient.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/WsClient.java index 8aaaa36189..0a65a91eff 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/WsClient.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/WsClient.java @@ -21,6 +21,7 @@ import org.java_websocket.client.WebSocketClient; import org.java_websocket.handshake.ServerHandshake; import org.thingsboard.server.msa.mapper.WsTelemetryResponse; +import javax.net.ssl.SSLParameters; import java.io.IOException; import java.net.URI; import java.util.concurrent.CountDownLatch; @@ -89,4 +90,9 @@ public class WsClient extends WebSocketClient { throw new RuntimeException(e); } } + + @Override + protected void onSetSSLParameters(SSLParameters sslParameters) { + sslParameters.setEndpointIdentificationAlgorithm(null); + } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java index 0342d512cc..d9dce3626c 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java @@ -39,7 +39,7 @@ import org.thingsboard.mqtt.MqttClientConfig; import org.thingsboard.mqtt.MqttHandler; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.id.RuleChainId; -import org.thingsboard.server.common.data.page.TextPageData; +import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.rule.NodeConnectionInfo; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; @@ -372,11 +372,11 @@ public class MqttClientTest extends AbstractContainerTest { } private RuleChainId getDefaultRuleChainId() { - ResponseEntity> ruleChains = restClient.getRestTemplate().exchange( - HTTPS_URL + "/api/ruleChains?limit=40&textSearch=", + ResponseEntity> ruleChains = restClient.getRestTemplate().exchange( + HTTPS_URL + "/api/ruleChains?pageSize=40&page=0&textSearch=", HttpMethod.GET, null, - new ParameterizedTypeReference>() { + new ParameterizedTypeReference>() { }); Optional defaultRuleChain = ruleChains.getBody().getData() diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java index d39a09f050..71ec297204 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java @@ -16,12 +16,10 @@ package org.thingsboard.server.msa.connectivity; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.Sets; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import io.netty.buffer.ByteBuf; @@ -30,13 +28,10 @@ import io.netty.handler.codec.mqtt.MqttQoS; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.RandomStringUtils; -import org.checkerframework.checker.units.qual.A; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.thingsboard.mqtt.MqttClient; import org.thingsboard.mqtt.MqttClientConfig; @@ -44,25 +39,24 @@ import org.thingsboard.mqtt.MqttHandler; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.RuleChainId; -import org.thingsboard.server.common.data.page.TextPageData; import org.thingsboard.server.common.data.relation.EntityRelation; -import org.thingsboard.server.common.data.relation.EntityRelationInfo; import org.thingsboard.server.common.data.relation.RelationTypeGroup; -import org.thingsboard.server.common.data.rule.NodeConnectionInfo; -import org.thingsboard.server.common.data.rule.RuleChain; -import org.thingsboard.server.common.data.rule.RuleChainMetaData; -import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.msa.AbstractContainerTest; import org.thingsboard.server.msa.WsClient; -import org.thingsboard.server.msa.mapper.AttributesResponse; import org.thingsboard.server.msa.mapper.WsTelemetryResponse; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.*; -import java.util.concurrent.*; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Random; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; @Slf4j public class MqttGatewayClientTest extends AbstractContainerTest { @@ -183,6 +177,9 @@ public class MqttGatewayClientTest extends AbstractContainerTest { String sharedAttributeValue = RandomStringUtils.randomAlphanumeric(8); sharedAttributes.addProperty("sharedAttr", sharedAttributeValue); + // Subscribe for attribute update event + mqttClient.on("v1/gateway/attributes", listener, MqttQoS.AT_LEAST_ONCE).get(); + ResponseEntity sharedAttributesResponse = restClient.getRestTemplate() .postForEntity(HTTPS_URL + "/api/plugins/telemetry/DEVICE/{deviceId}/SHARED_SCOPE", mapper.readTree(sharedAttributes.toString()), ResponseEntity.class, diff --git a/msa/js-executor/docker/Dockerfile b/msa/js-executor/docker/Dockerfile index 701d44b6c7..e2ba102435 100644 --- a/msa/js-executor/docker/Dockerfile +++ b/msa/js-executor/docker/Dockerfile @@ -22,6 +22,7 @@ RUN chmod a+x /tmp/*.sh \ && mv /tmp/start-js-executor.sh /usr/bin RUN yes | dpkg -i /tmp/${pkg.name}.deb +RUN rm /tmp/${pkg.name}.deb RUN systemctl --no-reload disable --now ${pkg.name}.service > /dev/null 2>&1 || : diff --git a/msa/js-executor/package-lock.json b/msa/js-executor/package-lock.json deleted file mode 100644 index 80023b25cc..0000000000 --- a/msa/js-executor/package-lock.json +++ /dev/null @@ -1,4399 +0,0 @@ -{ - "name": "thingsboard-js-executor", - "version": "2.5.5", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@azure/abort-controller": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.0.1.tgz", - "integrity": "sha512-wP2Jw6uPp8DEDy0n4KNidvwzDjyVV2xnycEIq7nPzj1rHyb/r+t3OPeNT1INZePP2wy5ZqlwyuyOMTi0ePyY1A==", - "requires": { - "tslib": "^1.9.3" - } - }, - "@azure/amqp-common": { - "version": "1.0.0-preview.17", - "resolved": "https://registry.npmjs.org/@azure/amqp-common/-/amqp-common-1.0.0-preview.17.tgz", - "integrity": "sha512-mrvRLvBYbylOBAhU0AeZk2jKDZ63nnleJs2KBYzs3wFx/Vz7J9YLtMYq9/s1CX1T2RFERj9w6lRXY42wGjC4iw==", - "requires": { - "@types/async-lock": "^1.1.0", - "@types/is-buffer": "^2.0.0", - "async-lock": "^1.1.3", - "buffer": "^5.2.1", - "debug": "^3.1.0", - "events": "^3.0.0", - "is-buffer": "^2.0.3", - "jssha": "^2.3.1", - "process": "^0.11.10", - "rhea": "^1.0.18", - "rhea-promise": "^0.1.15", - "stream-browserify": "^2.0.2", - "tslib": "^1.9.3", - "url": "^0.11.0", - "util": "^0.11.1" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "@azure/core-auth": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.1.3.tgz", - "integrity": "sha512-A4xigW0YZZpkj1zK7dKuzbBpGwnhEcRk6WWuIshdHC32raR3EQ1j6VA9XZqE+RFsUgH6OAmIK5BWIz+mZjnd6Q==", - "requires": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-tracing": "1.0.0-preview.8", - "@opentelemetry/api": "^0.6.1", - "tslib": "^2.0.0" - }, - "dependencies": { - "@azure/core-tracing": { - "version": "1.0.0-preview.8", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.8.tgz", - "integrity": "sha512-ZKUpCd7Dlyfn7bdc+/zC/sf0aRIaNQMDuSj2RhYRFe3p70hVAnYGp3TX4cnG2yoEALp/LTj/XnZGQ8Xzf6Ja/Q==", - "requires": { - "@opencensus/web-types": "0.0.7", - "@opentelemetry/api": "^0.6.1", - "tslib": "^1.10.0" - }, - "dependencies": { - "tslib": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", - "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" - } - } - }, - "@opentelemetry/api": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-0.6.1.tgz", - "integrity": "sha512-wpufGZa7tTxw7eAsjXJtiyIQ42IWQdX9iUQp7ACJcKo1hCtuhLU+K2Nv1U6oRwT1oAlZTE6m4CgWKZBhOiau3Q==", - "requires": { - "@opentelemetry/context-base": "^0.6.1" - } - }, - "tslib": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", - "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==" - } - } - }, - "@azure/core-http": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-1.1.8.tgz", - "integrity": "sha512-hJ9ZblU99sY2dTD6U5EqZ5zjd0QmwwvSp8RYp2zS9s5mhsNobLQFI09bIE6yo891bOySCEepNCE5tL15dLYhIA==", - "requires": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-auth": "^1.1.3", - "@azure/core-tracing": "1.0.0-preview.9", - "@azure/logger": "^1.0.0", - "@opentelemetry/api": "^0.10.2", - "@types/node-fetch": "^2.5.0", - "@types/tunnel": "^0.0.1", - "form-data": "^3.0.0", - "node-fetch": "^2.6.0", - "process": "^0.11.10", - "tough-cookie": "^4.0.0", - "tslib": "^2.0.0", - "tunnel": "^0.0.6", - "uuid": "^8.1.0", - "xml2js": "^0.4.19" - }, - "dependencies": { - "tslib": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", - "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==" - } - } - }, - "@azure/core-tracing": { - "version": "1.0.0-preview.9", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.9.tgz", - "integrity": "sha512-zczolCLJ5QG42AEPQ+Qg9SRYNUyB+yZ5dzof4YEc+dyWczO9G2sBqbAjLB7IqrsdHN2apkiB2oXeDKCsq48jug==", - "requires": { - "@opencensus/web-types": "0.0.7", - "@opentelemetry/api": "^0.10.2", - "tslib": "^2.0.0" - }, - "dependencies": { - "tslib": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", - "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==" - } - } - }, - "@azure/logger": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.0.tgz", - "integrity": "sha512-g2qLDgvmhyIxR3JVS8N67CyIOeFRKQlX/llxYJQr1OSGQqM3HTpVP8MjmjcEKbL/OIt2N9C9UFaNQuKOw1laOA==", - "requires": { - "tslib": "^1.9.3" - } - }, - "@azure/service-bus": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@azure/service-bus/-/service-bus-1.1.10.tgz", - "integrity": "sha512-QSL8Tt5/Kjn3yvxhKeacVvqocRxLvHtOU20iELriUkZwA9CWNZmm0cTzPrDC8XHtmeEUgk0iWyfmMaGnjQPW0Q==", - "requires": { - "@azure/amqp-common": "1.0.0-preview.17", - "@azure/core-http": "^1.0.0", - "@opentelemetry/types": "^0.2.0", - "@types/is-buffer": "^2.0.0", - "@types/long": "^4.0.0", - "buffer": "^5.2.1", - "debug": "^4.1.1", - "is-buffer": "^2.0.3", - "long": "^4.0.0", - "process": "^0.11.10", - "rhea": "^1.0.23", - "rhea-promise": "^0.1.15", - "tslib": "^1.10.0" - } - }, - "@babel/parser": { - "version": "7.11.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", - "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", - "dev": true - }, - "@babel/runtime": { - "version": "7.11.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz", - "integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, - "@dabh/diagnostics": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz", - "integrity": "sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q==", - "requires": { - "colorspace": "1.1.x", - "enabled": "2.0.x", - "kuler": "^2.0.0" - } - }, - "@google-cloud/paginator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-2.0.3.tgz", - "integrity": "sha512-kp/pkb2p/p0d8/SKUu4mOq8+HGwF8NPzHWkj+VKrIPQPyMRw8deZtrO/OcSiy9C/7bpfU5Txah5ltUNfPkgEXg==", - "requires": { - "arrify": "^2.0.0", - "extend": "^3.0.2" - } - }, - "@google-cloud/precise-date": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@google-cloud/precise-date/-/precise-date-1.0.3.tgz", - "integrity": "sha512-wWnDGh9y3cJHLuVEY8t6un78vizzMWsS7oIWKeFtPj+Ndy+dXvHW0HTx29ZUhen+tswSlQYlwFubvuRP5kKdzQ==" - }, - "@google-cloud/projectify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-1.0.4.tgz", - "integrity": "sha512-ZdzQUN02eRsmTKfBj9FDL0KNDIFNjBn/d6tHQmA/+FImH5DO6ZV8E7FzxMgAUiVAUq41RFAkb25p1oHOZ8psfg==" - }, - "@google-cloud/promisify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-1.0.4.tgz", - "integrity": "sha512-VccZDcOql77obTnFh0TbNED/6ZbbmHDf8UMNnzO1d5g9V0Htfm4k5cllY8P1tJsRKC3zWYGRLaViiupcgVjBoQ==" - }, - "@google-cloud/pubsub": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@google-cloud/pubsub/-/pubsub-1.7.3.tgz", - "integrity": "sha512-v+KdeaOS17WtHnsDf2bPGxKDT9HIRPYo3n+WsAEmvAzDHnh8q65mFcuYoQxuy2iRhmN/1ql2a0UU2tAAL7XZ8Q==", - "requires": { - "@google-cloud/paginator": "^2.0.0", - "@google-cloud/precise-date": "^1.0.0", - "@google-cloud/projectify": "^1.0.0", - "@google-cloud/promisify": "^1.0.0", - "@types/duplexify": "^3.6.0", - "@types/long": "^4.0.0", - "arrify": "^2.0.0", - "async-each": "^1.0.1", - "extend": "^3.0.2", - "google-auth-library": "^5.5.0", - "google-gax": "^1.14.2", - "is-stream-ended": "^0.1.4", - "lodash.snakecase": "^4.1.1", - "p-defer": "^3.0.0", - "protobufjs": "^6.8.1" - } - }, - "@grpc/grpc-js": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.0.5.tgz", - "integrity": "sha512-Hm+xOiqAhcpT9RYM8lc15dbQD7aQurM7ZU8ulmulepiPlN7iwBXXwP3vSBUimoFoApRqz7pSIisXU8pZaCB4og==", - "requires": { - "semver": "^6.2.0" - } - }, - "@grpc/proto-loader": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.5.tgz", - "integrity": "sha512-WwN9jVNdHRQoOBo9FDH7qU+mgfjPc8GygPYms3M+y3fbQLfnCe/Kv/E01t7JRgnrsOHH8euvSbed3mIalXhwqQ==", - "requires": { - "lodash.camelcase": "^4.3.0", - "protobufjs": "^6.8.6" - } - }, - "@nodelib/fs.scandir": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", - "integrity": "sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.3", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", - "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz", - "integrity": "sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.3", - "fastq": "^1.6.0" - } - }, - "@opencensus/web-types": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/@opencensus/web-types/-/web-types-0.0.7.tgz", - "integrity": "sha512-xB+w7ZDAu3YBzqH44rCmG9/RlrOmFuDPt/bpf17eJr8eZSrLt7nc7LnWdxM9Mmoj/YKMHpxRg28txu3TcpiL+g==" - }, - "@opentelemetry/api": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-0.10.2.tgz", - "integrity": "sha512-GtpMGd6vkzDMYcpu2t9LlhEgMy/SzBwRnz48EejlRArYqZzqSzAsKmegUK7zHgl+EOIaK9mKHhnRaQu3qw20cA==", - "requires": { - "@opentelemetry/context-base": "^0.10.2" - }, - "dependencies": { - "@opentelemetry/context-base": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-base/-/context-base-0.10.2.tgz", - "integrity": "sha512-hZNKjKOYsckoOEgBziGMnBcX0M7EtstnCmwz5jZUOUYwlZ+/xxX6z3jPu1XVO2Jivk0eLfuP9GP+vFD49CMetw==" - } - } - }, - "@opentelemetry/context-base": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-base/-/context-base-0.6.1.tgz", - "integrity": "sha512-5bHhlTBBq82ti3qPT15TRxkYTFPPQWbnkkQkmHPtqiS1XcTB69cEKd3Jm7Cfi/vkPoyxapmePE9tyA7EzLt8SQ==" - }, - "@opentelemetry/types": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/types/-/types-0.2.0.tgz", - "integrity": "sha512-GtwNB6BNDdsIPAYEdpp3JnOGO/3AJxjPvny53s3HERBdXSJTGQw8IRhiaTEX0b3w9P8+FwFZde4k+qkjn67aVw==" - }, - "@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" - }, - "@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" - }, - "@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" - }, - "@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" - }, - "@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", - "requires": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" - }, - "@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" - }, - "@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" - }, - "@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" - }, - "@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" - }, - "@types/async-lock": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@types/async-lock/-/async-lock-1.1.2.tgz", - "integrity": "sha512-j9n4bb6RhgFIydBe0+kpjnBPYumDaDyU8zvbWykyVMkku+c2CSu31MZkLeaBfqIwU+XCxlDpYDfyMQRkM0AkeQ==" - }, - "@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", - "dev": true - }, - "@types/duplexify": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@types/duplexify/-/duplexify-3.6.0.tgz", - "integrity": "sha512-5zOA53RUlzN74bvrSGwjudssD9F3a797sDZQkiYpUOxW+WHaXTCPz4/d5Dgi6FKnOqZ2CpaTo0DhgIfsXAOE/A==", - "requires": { - "@types/node": "*" - } - }, - "@types/fs-extra": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.1.tgz", - "integrity": "sha512-TcUlBem321DFQzBNuz8p0CLLKp0VvF/XH9E4KHNmgwyp4E3AfgI5cjiIVZWlbfThBop2qxFIh4+LeY6hVWWZ2w==", - "requires": { - "@types/node": "*" - } - }, - "@types/is-buffer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/is-buffer/-/is-buffer-2.0.0.tgz", - "integrity": "sha512-0f7N/e3BAz32qDYvgB4d2cqv1DqUwvGxHkXsrucICn8la1Vb6Yl6Eg8mPScGwUiqHJeE7diXlzaK+QMA9m4Gxw==", - "requires": { - "@types/node": "*" - } - }, - "@types/long": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", - "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" - }, - "@types/node": { - "version": "14.11.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.2.tgz", - "integrity": "sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA==" - }, - "@types/node-fetch": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.7.tgz", - "integrity": "sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==", - "requires": { - "@types/node": "*", - "form-data": "^3.0.0" - } - }, - "@types/tunnel": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.1.tgz", - "integrity": "sha512-AOqu6bQu5MSWwYvehMXLukFHnupHrpZ8nvgae5Ggie9UwzDR1CCwoXgSSWNZJuyOlCdfdsWMA5F2LlmvyoTv8A==", - "requires": { - "@types/node": "*" - } - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "requires": { - "event-target-shim": "^5.0.0" - } - }, - "agent-base": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz", - "integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==", - "requires": { - "debug": "4" - } - }, - "ajv": { - "version": "6.12.5", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.5.tgz", - "integrity": "sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "amqplib": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.5.6.tgz", - "integrity": "sha512-J4TR0WAMPBHN+tgTuhNsSObfM9eTVTZm/FNw0LyaGfbiLsBxqSameDNYpChUFXW4bnTKHDXy0ab+nuLhumnRrQ==", - "requires": { - "bitsyntax": "~0.1.0", - "bluebird": "^3.5.2", - "buffer-more-ints": "~1.0.0", - "readable-stream": "1.x >=1.1.9", - "safe-buffer": "~5.1.2", - "url-parse": "~1.4.3" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } - } - }, - "ansi-align": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", - "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", - "dev": true, - "requires": { - "string-width": "^2.0.0" - } - }, - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - }, - "dependencies": { - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true - }, - "async": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", - "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" - }, - "async-each": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", - "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==" - }, - "async-lock": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.2.4.tgz", - "integrity": "sha512-UBQJC2pbeyGutIfYmErGc9RaJYnpZ1FHaxuKwb0ahvGiiCkPUf3p67Io+YLPmmv3RHY+mF6JEtNW8FlHsraAaA==" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true - }, - "aws-sdk": { - "version": "2.761.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.761.0.tgz", - "integrity": "sha512-mSzdiqlnruFlJYrQVWPMyPQ8ynJe9P5QVD+edv8HFlYDQNOwpPCjlqI9kE1VE3cVcxkh0j2Q2kfLQa/pAY2w7Q==", - "requires": { - "buffer": "4.9.2", - "events": "1.1.1", - "ieee754": "1.1.13", - "jmespath": "0.15.0", - "querystring": "0.2.0", - "sax": "1.2.1", - "url": "0.10.3", - "uuid": "3.3.2", - "xml2js": "0.4.19" - }, - "dependencies": { - "buffer": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", - "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, - "events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" - }, - "sax": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", - "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" - }, - "url": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", - "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - } - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" - }, - "xml2js": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", - "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~9.0.1" - } - }, - "xmlbuilder": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", - "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" - } - } - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "aws4": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.1.tgz", - "integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==" - }, - "azure-common": { - "version": "0.9.25", - "resolved": "https://registry.npmjs.org/azure-common/-/azure-common-0.9.25.tgz", - "integrity": "sha512-L7YO3DUQ0iwiaUyD9Wy6B66Y6HmCzMb9vxUqKklgzU+gFRRBKIMSVR4oZS6IkQfFCSm9eKwHuH2p3UdDPszd7g==", - "requires": { - "dateformat": "1.0.2-1.2.3", - "duplexer": "~0.1.1", - "envconf": "~0.0.4", - "request": "^2.81.0", - "through": "~2.3.4", - "tunnel": "~0.0.2", - "underscore": "1.4.x", - "validator": "^9.4.1", - "xml2js": "^0.4.19", - "xmlbuilder": "15.1.1" - }, - "dependencies": { - "underscore": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz", - "integrity": "sha1-YaajIBBiKvoHljvzJSA88SI51gQ=" - }, - "xmlbuilder": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==" - } - } - }, - "azure-sb": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/azure-sb/-/azure-sb-0.11.1.tgz", - "integrity": "sha512-ZYgPeSDMD99i/Em+6wT78zvBkJ/dbh2ypb4DbqQ1Flaif5vWJFzC/iKxxcq/vq+THWoO3+UbqWa0JNXnW3zAvw==", - "requires": { - "azure-common": "^0.9.22", - "mpns": "2.1.3", - "underscore": "^1.8.3", - "wns": "~0.5.3" - } - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "base64-js": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "bignumber.js": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", - "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==" - }, - "binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "dev": true - }, - "bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "optional": true, - "requires": { - "file-uri-to-path": "1.0.0" - } - }, - "bitsyntax": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/bitsyntax/-/bitsyntax-0.1.0.tgz", - "integrity": "sha512-ikAdCnrloKmFOugAfxWws89/fPc+nw0OOG1IzIE72uSOg/A3cYptKCjSUhDTuj7fhsJtzkzlv7l3b8PzRHLN0Q==", - "requires": { - "buffer-more-ints": "~1.0.0", - "debug": "~2.6.9", - "safe-buffer": "~5.1.2" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } - } - }, - "bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" - }, - "boxen": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", - "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", - "dev": true, - "requires": { - "ansi-align": "^2.0.0", - "camelcase": "^4.0.0", - "chalk": "^2.0.1", - "cli-boxes": "^1.0.0", - "string-width": "^2.0.0", - "term-size": "^1.2.0", - "widest-line": "^2.0.0" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "buffer": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", - "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" - } - }, - "buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" - }, - "buffer-more-ints": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz", - "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==" - }, - "byline": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", - "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=", - "dev": true - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - } - }, - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", - "dev": true - }, - "capture-stack-trace": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz", - "integrity": "sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==", - "dev": true - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - } - }, - "ci-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", - "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", - "dev": true - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "cli-boxes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", - "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", - "dev": true - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", - "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", - "requires": { - "color-convert": "^1.9.1", - "color-string": "^1.5.2" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "color-string": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", - "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", - "requires": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" - }, - "colorspace": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz", - "integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==", - "requires": { - "color": "3.0.x", - "text-hex": "1.0.x" - } - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "config": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/config/-/config-3.3.2.tgz", - "integrity": "sha512-NlGfBn2565YA44Irn7GV5KHlIGC3KJbf0062/zW5ddP9VXIuRj0m7HVyFAWvMZvaHPEglyGfwmevGz3KosIpCg==", - "requires": { - "json5": "^2.1.1" - } - }, - "configstore": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.5.tgz", - "integrity": "sha512-nlOhI4+fdzoK5xmJ+NY+1gZK56bwEaWZr8fYuXohZ9Vkc1o3a4T/R3M+yE/w7x/ZVJ1zF8c+oaOvF0dztdUgmA==", - "dev": true, - "requires": { - "dot-prop": "^4.2.1", - "graceful-fs": "^4.1.2", - "make-dir": "^1.0.0", - "unique-string": "^1.0.0", - "write-file-atomic": "^2.0.0", - "xdg-basedir": "^3.0.0" - } - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "create-error-class": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", - "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", - "dev": true, - "requires": { - "capture-stack-trace": "^1.0.0" - } - }, - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "dependencies": { - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true - } - } - }, - "crypto-random-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", - "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", - "dev": true - }, - "cycle": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", - "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=" - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "dateformat": { - "version": "1.0.2-1.2.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz", - "integrity": "sha1-sCIMAt6YYXQztyhRz0fePfLNvuk=" - }, - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "requires": { - "ms": "2.1.2" - } - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "dot-prop": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz", - "integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==", - "dev": true, - "requires": { - "is-obj": "^1.0.0" - } - }, - "duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" - }, - "duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", - "dev": true - }, - "duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", - "requires": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "enabled": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", - "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "requires": { - "once": "^1.4.0" - } - }, - "envconf": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/envconf/-/envconf-0.0.4.tgz", - "integrity": "sha1-hWda+6I3xD+Y3i1GrcDlMqTc9Is=" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "escodegen": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", - "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", - "dev": true, - "requires": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true - } - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" - }, - "events": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", - "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==" - }, - "execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", - "dev": true, - "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "dependencies": { - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - } - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "fast-glob": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz", - "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.0", - "merge2": "^1.3.0", - "micromatch": "^4.0.2", - "picomatch": "^2.2.1" - }, - "dependencies": { - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "fast-safe-stringify": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", - "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" - }, - "fast-text-encoding": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", - "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==" - }, - "fastq": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.8.0.tgz", - "integrity": "sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q==", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "fecha": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.0.tgz", - "integrity": "sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg==" - }, - "file-stream-rotator": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.4.1.tgz", - "integrity": "sha512-W3aa3QJEc8BS2MmdVpQiYLKHj3ijpto1gMDlsgCRSKfIUe6MwkcpODGPQ3vZfb0XvCeCqlu9CBQTN7oQri2TZQ==", - "requires": { - "moment": "^2.11.2" - } - }, - "file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "optional": true - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fn.name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", - "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "requires": { - "map-cache": "^0.2.2" - } - }, - "from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - } - }, - "fs-extra": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz", - "integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "fsevents": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", - "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "dev": true, - "optional": true, - "requires": { - "bindings": "^1.5.0", - "nan": "^2.12.1" - } - }, - "gaxios": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-2.3.4.tgz", - "integrity": "sha512-US8UMj8C5pRnao3Zykc4AAVr+cffoNKRTg9Rsf2GiuZCW69vgJj38VK2PzlPuQU73FZ/nTk9/Av6/JGcE1N9vA==", - "requires": { - "abort-controller": "^3.0.0", - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.3.0" - } - }, - "gcp-metadata": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-3.5.0.tgz", - "integrity": "sha512-ZQf+DLZ5aKcRpLzYUyBS3yo3N0JSa82lNDO8rj3nMSlovLcz2riKFBsYgDzeXcv75oo5eqB2lx+B14UvPoCRnA==", - "requires": { - "gaxios": "^2.1.0", - "json-bigint": "^0.3.0" - } - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "global-dirs": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", - "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", - "dev": true, - "requires": { - "ini": "^1.3.4" - } - }, - "globby": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz", - "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.1.1", - "ignore": "^5.1.4", - "merge2": "^1.3.0", - "slash": "^3.0.0" - } - }, - "google-auth-library": { - "version": "5.10.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-5.10.1.tgz", - "integrity": "sha512-rOlaok5vlpV9rSiUu5EpR0vVpc+PhN62oF4RyX/6++DG1VsaulAFEMlDYBLjJDDPI6OcNOCGAKy9UVB/3NIDXg==", - "requires": { - "arrify": "^2.0.0", - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "fast-text-encoding": "^1.0.0", - "gaxios": "^2.1.0", - "gcp-metadata": "^3.4.0", - "gtoken": "^4.1.0", - "jws": "^4.0.0", - "lru-cache": "^5.0.0" - } - }, - "google-gax": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-1.15.3.tgz", - "integrity": "sha512-3JKJCRumNm3x2EksUTw4P1Rad43FTpqrtW9jzpf3xSMYXx+ogaqTM1vGo7VixHB4xkAyATXVIa3OcNSh8H9zsQ==", - "requires": { - "@grpc/grpc-js": "~1.0.3", - "@grpc/proto-loader": "^0.5.1", - "@types/fs-extra": "^8.0.1", - "@types/long": "^4.0.0", - "abort-controller": "^3.0.0", - "duplexify": "^3.6.0", - "google-auth-library": "^5.0.0", - "is-stream-ended": "^0.1.4", - "lodash.at": "^4.6.0", - "lodash.has": "^4.5.2", - "node-fetch": "^2.6.0", - "protobufjs": "^6.8.9", - "retry-request": "^4.0.0", - "semver": "^6.0.0", - "walkdir": "^0.4.0" - } - }, - "google-p12-pem": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-2.0.4.tgz", - "integrity": "sha512-S4blHBQWZRnEW44OcR7TL9WR+QCqByRvhNDZ/uuQfpxywfupikf/miba8js1jZi6ZOGv5slgSuoshCWh6EMDzg==", - "requires": { - "node-forge": "^0.9.0" - } - }, - "got": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", - "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", - "dev": true, - "requires": { - "create-error-class": "^3.0.0", - "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "is-redirect": "^1.0.0", - "is-retry-allowed": "^1.0.0", - "is-stream": "^1.0.0", - "lowercase-keys": "^1.0.0", - "safe-buffer": "^5.0.1", - "timed-out": "^4.0.0", - "unzip-response": "^2.0.1", - "url-parse-lax": "^1.0.0" - }, - "dependencies": { - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - } - } - }, - "graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", - "dev": true - }, - "gtoken": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-4.1.4.tgz", - "integrity": "sha512-VxirzD0SWoFUo5p8RDP8Jt2AGyOmyYcT/pOUgDKJCK+iSw0TMqwrVfY37RXTNmoKwrzmDHSk0GMT9FsgVmnVSA==", - "requires": { - "gaxios": "^2.1.0", - "google-p12-pem": "^2.0.0", - "jws": "^4.0.0", - "mime": "^2.2.0" - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "requires": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "requires": { - "agent-base": "6", - "debug": "4" - } - }, - "ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" - }, - "ignore": { - "version": "5.1.8", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", - "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", - "dev": true - }, - "ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", - "dev": true - }, - "import-lazy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", - "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", - "dev": true - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "dev": true - }, - "into-stream": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-5.1.1.tgz", - "integrity": "sha512-krrAJ7McQxGGmvaYbB7Q1mcA+cRwg9Ij2RfWIeVesNBgVDZmzY/Fa4IpZUT3bmdRzMzdf/mzltCG2Dq99IZGBA==", - "dev": true, - "requires": { - "from2": "^2.3.0", - "p-is-promise": "^3.0.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "dev": true, - "requires": { - "binary-extensions": "^1.0.0" - } - }, - "is-buffer": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", - "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==" - }, - "is-ci": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", - "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", - "dev": true, - "requires": { - "ci-info": "^1.5.0" - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-installed-globally": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", - "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", - "dev": true, - "requires": { - "global-dirs": "^0.1.0", - "is-path-inside": "^1.0.0" - } - }, - "is-npm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", - "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", - "dev": true - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", - "dev": true - }, - "is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", - "dev": true, - "requires": { - "path-is-inside": "^1.0.1" - } - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "is-redirect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", - "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", - "dev": true - }, - "is-retry-allowed": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", - "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", - "dev": true - }, - "is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" - }, - "is-stream-ended": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", - "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==" - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "jmespath": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", - "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" - }, - "js-yaml": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", - "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" - }, - "json-bigint": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.1.tgz", - "integrity": "sha512-DGWnSzmusIreWlEupsUelHrhwmPPE+FiQvg+drKfk2p+bdEYa5mp4PJ8JsCWqae0M2jQNb0HPvnwvf1qOTThzQ==", - "requires": { - "bignumber.js": "^9.0.0" - } - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "json5": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", - "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", - "requires": { - "minimist": "^1.2.5" - } - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "jssha": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jssha/-/jssha-2.4.2.tgz", - "integrity": "sha512-/jsi/9C0S70zfkT/4UlKQa5E1xKurDnXcQizcww9JSR/Fv+uIbWM2btG+bFcL3iNoK9jIGS0ls9HWLr1iw0kFg==" - }, - "jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "requires": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - }, - "kafkajs": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/kafkajs/-/kafkajs-1.14.0.tgz", - "integrity": "sha512-W+WCekiooY5rJP3Me5N3gWcQ8O6uG6lw0vv9t+sI+WqXKjKwj2+CWIXJy241x+ITE+1M1D19ABSiL2J8lKja5A==" - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, - "kuler": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", - "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" - }, - "latest-version": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", - "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", - "dev": true, - "requires": { - "package-json": "^4.0.0" - } - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "lodash.at": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.at/-/lodash.at-4.6.0.tgz", - "integrity": "sha1-k83OZk8KGZTqM9181A4jr9EbD/g=" - }, - "lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" - }, - "lodash.has": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz", - "integrity": "sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=" - }, - "lodash.snakecase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", - "integrity": "sha1-OdcUo1NXFHg3rv1ktdy7Fr7Nj40=" - }, - "logform": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.2.0.tgz", - "integrity": "sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg==", - "requires": { - "colors": "^1.2.1", - "fast-safe-stringify": "^2.0.4", - "fecha": "^4.2.0", - "ms": "^2.1.1", - "triple-beam": "^1.3.0" - } - }, - "long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, - "lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", - "dev": true - }, - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "requires": { - "yallist": "^3.0.2" - } - }, - "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "requires": { - "object-visit": "^1.0.0" - } - }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "mime": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", - "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==" - }, - "mime-db": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", - "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" - }, - "mime-types": { - "version": "2.1.27", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", - "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", - "requires": { - "mime-db": "1.44.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "moment": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.0.tgz", - "integrity": "sha512-z6IJ5HXYiuxvFTI6eiQ9dm77uE0gyy1yXNApVHqTcnIKfY9tIwEjlzsZ6u1LQXvVgKeTnv9Xm7NDvJ7lso3MtA==" - }, - "mpns": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/mpns/-/mpns-2.1.3.tgz", - "integrity": "sha512-gPLNoVqwYoKUmNYZ2shMSdaE2XvHSRxWNzyG4DUi6Av7MSujyeOw/nj61nnQeuV/vke5E0Dni468xn0qxTHIZQ==" - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "multistream": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/multistream/-/multistream-2.1.1.tgz", - "integrity": "sha512-xasv76hl6nr1dEy3lPvy7Ej7K/Lx3O/FCvwge8PeVJpciPPoNCbaANcNiBug3IpdvTveZUcAV0DJzdnUDMesNQ==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.5" - } - }, - "nan": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", - "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", - "dev": true, - "optional": true - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - } - }, - "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" - }, - "node-forge": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.2.tgz", - "integrity": "sha512-naKSScof4Wn+aoHU6HBsifh92Zeicm1GDQKd1vp3Y/kOi8ub0DozCa9KpvYNCXslFHYRmLNiqRopGdTGwNLpNw==" - }, - "nodemon": { - "version": "1.19.4", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.19.4.tgz", - "integrity": "sha512-VGPaqQBNk193lrJFotBU8nvWZPqEZY2eIzymy2jjY0fJ9qIsxA0sxQ8ATPl0gZC645gijYEc1jtZvpS8QWzJGQ==", - "dev": true, - "requires": { - "chokidar": "^2.1.8", - "debug": "^3.2.6", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.0.4", - "pstree.remy": "^1.1.7", - "semver": "^5.7.1", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.2", - "update-notifier": "^2.5.0" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", - "dev": true, - "requires": { - "abbrev": "1" - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "object-hash": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz", - "integrity": "sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==" - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "requires": { - "isobject": "^3.0.0" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "one-time": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", - "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", - "requires": { - "fn.name": "1.x.x" - } - }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, - "p-defer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-3.0.0.tgz", - "integrity": "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==" - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, - "p-is-promise": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz", - "integrity": "sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==", - "dev": true - }, - "package-json": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", - "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", - "dev": true, - "requires": { - "got": "^6.7.1", - "registry-auth-token": "^3.0.1", - "registry-url": "^3.0.3", - "semver": "^5.1.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", - "dev": true - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - }, - "pkg": { - "version": "4.4.9", - "resolved": "https://registry.npmjs.org/pkg/-/pkg-4.4.9.tgz", - "integrity": "sha512-FK4GqHtcCY2PPPVaKViU0NyRzpo6gCS7tPKN5b7AkElqjAOCH1bsRKgohEnxThr6DWfTGByGqba2YHGR/BqbmA==", - "dev": true, - "requires": { - "@babel/parser": "^7.9.4", - "@babel/runtime": "^7.9.2", - "chalk": "^3.0.0", - "escodegen": "^1.14.1", - "fs-extra": "^8.1.0", - "globby": "^11.0.0", - "into-stream": "^5.1.1", - "minimist": "^1.2.5", - "multistream": "^2.1.1", - "pkg-fetch": "^2.6.9", - "progress": "^2.0.3", - "resolve": "^1.15.1", - "stream-meter": "^1.0.4" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "pkg-fetch": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-2.6.9.tgz", - "integrity": "sha512-EnVR8LRILXBvaNP+wJOSY02c3+qDDfyEyR+aqAHLhcc9PBnbxFT9UZ1+If49goPQzQPn26TzF//fc6KXZ0aXEg==", - "dev": true, - "requires": { - "@babel/runtime": "^7.9.2", - "byline": "^5.0.0", - "chalk": "^3.0.0", - "expand-template": "^2.0.3", - "fs-extra": "^8.1.0", - "minimist": "^1.2.5", - "progress": "^2.0.3", - "request": "^2.88.0", - "request-progress": "^3.0.0", - "semver": "^6.3.0", - "unique-temp-dir": "^1.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", - "dev": true - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true - }, - "protobufjs": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.1.tgz", - "integrity": "sha512-pb8kTchL+1Ceg4lFd5XUpK8PdWacbvV5SK2ULH2ebrYtl4GjJmS24m6CKME67jzV53tbJxHlnNOSqQHbTsR9JQ==", - "requires": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.1", - "@types/node": "^13.7.0", - "long": "^4.0.0" - }, - "dependencies": { - "@types/node": { - "version": "13.13.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.21.tgz", - "integrity": "sha512-tlFWakSzBITITJSxHV4hg4KvrhR/7h3xbJdSFbYJBVzKubrASbnnIFuSgolUh7qKGo/ZeJPKUfbZ0WS6Jp14DQ==" - } - } - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true - }, - "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" - }, - "pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true - }, - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" - }, - "querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - } - }, - "regenerator-runtime": { - "version": "0.13.7", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", - "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", - "dev": true - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, - "registry-auth-token": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", - "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", - "dev": true, - "requires": { - "rc": "^1.1.6", - "safe-buffer": "^5.0.1" - } - }, - "registry-url": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", - "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", - "dev": true, - "requires": { - "rc": "^1.0.1" - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" - } - } - }, - "request-progress": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", - "integrity": "sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=", - "dev": true, - "requires": { - "throttleit": "^1.0.0" - } - }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" - }, - "resolve": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", - "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, - "retry-request": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.1.3.tgz", - "integrity": "sha512-QnRZUpuPNgX0+D1xVxul6DbJ9slvo4Rm6iV/dn63e048MvGbUZiKySVt6Tenp04JqmchxjiLltGerOJys7kJYQ==", - "requires": { - "debug": "^4.1.1" - } - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true - }, - "rhea": { - "version": "1.0.24", - "resolved": "https://registry.npmjs.org/rhea/-/rhea-1.0.24.tgz", - "integrity": "sha512-PEl62U2EhxCO5wMUZ2/bCBcXAVKN9AdMSNQOrp3+R5b77TEaOSiy16MQ0sIOmzj/iqsgIAgPs1mt3FYfu1vIXA==", - "requires": { - "debug": "0.8.0 - 3.5.0" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "rhea-promise": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/rhea-promise/-/rhea-promise-0.1.15.tgz", - "integrity": "sha512-+6uilZXSJGyiqVeHQI3Krv6NTAd8cWRCY2uyCxmzR4/5IFtBqqFem1HV2OiwSj0Gu7OFChIJDfH2JyjN7J0vRA==", - "requires": { - "debug": "^3.1.0", - "rhea": "^1.0.4", - "tslib": "^1.9.3" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "run-parallel": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz", - "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==", - "dev": true - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - }, - "semver-diff": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", - "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", - "dev": true, - "requires": { - "semver": "^5.0.3" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", - "dev": true - }, - "simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", - "requires": { - "is-arrayish": "^0.3.1" - } - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - }, - "source-map-resolve": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", - "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", - "dev": true, - "requires": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "dev": true - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "stream-browserify": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", - "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", - "requires": { - "inherits": "~2.0.1", - "readable-stream": "^2.0.2" - } - }, - "stream-meter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/stream-meter/-/stream-meter-1.0.4.tgz", - "integrity": "sha1-Uq+Vql6nYKJJFxZwTb/5D3Ov3R0=", - "dev": true, - "requires": { - "readable-stream": "^2.1.4" - } - }, - "stream-shift": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", - "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "term-size": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", - "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", - "dev": true, - "requires": { - "execa": "^0.7.0" - } - }, - "text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" - }, - "throttleit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", - "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=", - "dev": true - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" - }, - "timed-out": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", - "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", - "dev": true - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - }, - "touch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", - "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", - "dev": true, - "requires": { - "nopt": "~1.0.10" - } - }, - "tough-cookie": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", - "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", - "requires": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.1.2" - }, - "dependencies": { - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - } - } - }, - "triple-beam": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", - "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" - }, - "tslib": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", - "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" - }, - "tunnel": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "uid2": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", - "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=", - "dev": true - }, - "undefsafe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz", - "integrity": "sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A==", - "dev": true, - "requires": { - "debug": "^2.2.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "underscore": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.11.0.tgz", - "integrity": "sha512-xY96SsN3NA461qIRKZ/+qox37YXPtSBswMGfiNptr+wrt6ds4HaMw23TP612fEyGekRE6LNRiLYr/aqbHXNedw==" - }, - "union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - } - }, - "unique-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", - "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", - "dev": true, - "requires": { - "crypto-random-string": "^1.0.0" - } - }, - "unique-temp-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unique-temp-dir/-/unique-temp-dir-1.0.0.tgz", - "integrity": "sha1-bc6VsmgcoAPuv7MEpBX5y6vMU4U=", - "dev": true, - "requires": { - "mkdirp": "^0.5.1", - "os-tmpdir": "^1.0.1", - "uid2": "0.0.3" - } - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true - } - } - }, - "unzip-response": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", - "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", - "dev": true - }, - "upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", - "dev": true - }, - "update-notifier": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", - "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", - "dev": true, - "requires": { - "boxen": "^1.2.1", - "chalk": "^2.0.1", - "configstore": "^3.0.0", - "import-lazy": "^2.1.0", - "is-ci": "^1.0.10", - "is-installed-globally": "^0.1.0", - "is-npm": "^1.0.0", - "latest-version": "^3.0.0", - "semver-diff": "^2.0.0", - "xdg-basedir": "^3.0.0" - } - }, - "uri-js": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", - "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", - "requires": { - "punycode": "^2.1.0" - }, - "dependencies": { - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - } - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, - "url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - } - }, - "url-parse": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", - "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", - "requires": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, - "url-parse-lax": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", - "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", - "dev": true, - "requires": { - "prepend-http": "^1.0.1" - } - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true - }, - "util": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", - "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", - "requires": { - "inherits": "2.0.3" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - } - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "uuid": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz", - "integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ==" - }, - "uuid-parse": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/uuid-parse/-/uuid-parse-1.1.0.tgz", - "integrity": "sha512-OdmXxA8rDsQ7YpNVbKSJkNzTw2I+S5WsbMDnCtIWSQaosNAcWtFuI/YK1TjzUI6nbkgiqEyh8gWngfcv8Asd9A==" - }, - "uuid-random": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/uuid-random/-/uuid-random-1.3.2.tgz", - "integrity": "sha512-UOzej0Le/UgkbWEO8flm+0y+G+ljUon1QWTEZOq1rnMAsxo2+SckbiZdKzAHHlVh6gJqI1TjC/xwgR50MuCrBQ==" - }, - "validator": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/validator/-/validator-9.4.1.tgz", - "integrity": "sha512-YV5KjzvRmSyJ1ee/Dm5UED0G+1L4GZnLN3w6/T+zZm8scVua4sOhYKWTUrKa0H/tMiJyO9QLHMPN+9mB/aMunA==" - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "walkdir": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz", - "integrity": "sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==" - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "widest-line": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", - "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", - "dev": true, - "requires": { - "string-width": "^2.1.1" - } - }, - "winston": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz", - "integrity": "sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==", - "requires": { - "@dabh/diagnostics": "^2.0.2", - "async": "^3.1.0", - "is-stream": "^2.0.0", - "logform": "^2.2.0", - "one-time": "^1.0.0", - "readable-stream": "^3.4.0", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.4.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "winston-compat": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/winston-compat/-/winston-compat-0.1.5.tgz", - "integrity": "sha512-EPvPcHT604AV3Ji6d3+vX8ENKIml9VSxMRnPQ+cuK/FX6f3hvPP2hxyoeeCOCFvDrJEujalfcKWlWPvAnFyS9g==", - "requires": { - "cycle": "~1.0.3", - "logform": "^1.6.0", - "triple-beam": "^1.2.0" - }, - "dependencies": { - "fecha": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz", - "integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==" - }, - "logform": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-1.10.0.tgz", - "integrity": "sha512-em5ojIhU18fIMOw/333mD+ZLE2fis0EzXl1ZwHx4iQzmpQi6odNiY/t+ITNr33JZhT9/KEaH+UPIipr6a9EjWg==", - "requires": { - "colors": "^1.2.1", - "fast-safe-stringify": "^2.0.4", - "fecha": "^2.3.3", - "ms": "^2.1.1", - "triple-beam": "^1.2.0" - } - } - } - }, - "winston-daily-rotate-file": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-3.10.0.tgz", - "integrity": "sha512-KO8CfbI2CvdR3PaFApEH02GPXiwJ+vbkF1mCkTlvRIoXFI8EFlf1ACcuaahXTEiDEKCii6cNe95gsL4ZkbnphA==", - "requires": { - "file-stream-rotator": "^0.4.1", - "object-hash": "^1.3.0", - "semver": "^6.2.0", - "triple-beam": "^1.3.0", - "winston-compat": "^0.1.4", - "winston-transport": "^4.2.0" - } - }, - "winston-transport": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz", - "integrity": "sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==", - "requires": { - "readable-stream": "^2.3.7", - "triple-beam": "^1.2.0" - } - }, - "wns": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/wns/-/wns-0.5.4.tgz", - "integrity": "sha512-WYiJ7khIwUGBD5KAm+YYmwJDDRzFRs4YGAjtbFSoRIdbn9Jcix3p9khJmpvBTXGommaKkvduAn+pc9l4d9yzVQ==" - }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "write-file-atomic": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", - "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - }, - "xdg-basedir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", - "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", - "dev": true - }, - "xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - } - }, - "xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - } - } -} diff --git a/msa/js-executor/package.json b/msa/js-executor/package.json index 405d582f04..1f498922cd 100644 --- a/msa/js-executor/package.json +++ b/msa/js-executor/package.json @@ -1,33 +1,33 @@ { "name": "thingsboard-js-executor", "private": true, - "version": "2.5.5", + "version": "3.2.0", "description": "ThingsBoard JavaScript Executor Microservice", "main": "server.js", "bin": "server.js", "scripts": { - "install": "pkg -t node10-linux-x64,node10-win-x64 --out-path ./target . && node install.js", + "install": "pkg -t node12-linux-x64,node12-win-x64 --out-path ./target . && node install.js", "test": "echo \"Error: no test specified\" && exit 1", "start": "nodemon server.js", "start-prod": "NODE_ENV=production nodemon server.js" }, "dependencies": { - "config": "^3.2.2", - "js-yaml": "^3.12.0", - "kafkajs": "^1.14.0", - "@google-cloud/pubsub": "^1.7.1", - "aws-sdk": "^2.663.0", - "amqplib": "^0.5.5", - "@azure/service-bus": "^1.1.6", + "@azure/service-bus": "^1.1.9", + "@google-cloud/pubsub": "^2.5.0", + "amqplib": "^0.6.0", + "aws-sdk": "^2.741.0", "azure-sb": "^0.11.1", + "config": "^3.3.1", + "js-yaml": "^3.14.0", + "kafkajs": "^1.14.0", "long": "^4.0.0", - "uuid-parse": "^1.0.0", - "uuid-random": "^1.3.0", - "winston": "^3.0.0", - "winston-daily-rotate-file": "^3.2.1" + "uuid-parse": "^1.1.0", + "uuid-random": "^1.3.2", + "winston": "^3.3.3", + "winston-daily-rotate-file": "^4.5.0" }, "engines": { - "node": ">=8.0.0 <11.0.0" + "node": ">=12.0.0 <14.0.0" }, "nyc": { "exclude": [ @@ -38,9 +38,9 @@ ] }, "devDependencies": { - "fs-extra": "^6.0.1", - "nodemon": "^1.17.5", - "pkg": "^4.4.0" + "fs-extra": "^9.0.1", + "nodemon": "^2.0.4", + "pkg": "^4.4.9" }, "pkg": { "assets": [ diff --git a/msa/js-executor/pom.xml b/msa/js-executor/pom.xml index 60ccd9d5bb..ea286c58ba 100644 --- a/msa/js-executor/pom.xml +++ b/msa/js-executor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT msa org.thingsboard.msa @@ -59,26 +59,26 @@ com.github.eirslett frontend-maven-plugin - 1.0 + 1.7.5 target ${basedir} - install node and npm + install node and yarn - install-node-and-npm + install-node-and-yarn - v10.16.0 - 6.4.1 + v12.16.1 + v1.22.4 - npm install + yarn install - npm + yarn install @@ -138,10 +138,10 @@ - npm-start + yarn-start - npm-start + yarn-start @@ -149,16 +149,16 @@ com.github.eirslett frontend-maven-plugin - 1.0 + 1.7.5 target ${basedir} - npm start + yarn start - npm + yarn diff --git a/msa/js-executor/yarn.lock b/msa/js-executor/yarn.lock new file mode 100644 index 0000000000..a2e65d950d --- /dev/null +++ b/msa/js-executor/yarn.lock @@ -0,0 +1,2900 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@azure/abort-controller@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@azure/abort-controller/-/abort-controller-1.0.1.tgz#8510935b25ac051e58920300e9d7b511ca6e656a" + integrity sha512-wP2Jw6uPp8DEDy0n4KNidvwzDjyVV2xnycEIq7nPzj1rHyb/r+t3OPeNT1INZePP2wy5ZqlwyuyOMTi0ePyY1A== + dependencies: + tslib "^1.9.3" + +"@azure/amqp-common@1.0.0-preview.16": + version "1.0.0-preview.16" + resolved "https://registry.yarnpkg.com/@azure/amqp-common/-/amqp-common-1.0.0-preview.16.tgz#3b7a9d39f7503347530fe9afddf92fa7bfd1ec5e" + integrity sha512-rMTBh54lV6/SBujXfPX0OqN+UNpG1zR4EYY+JP66QM0D2jzNdxLiv6gart5Ersy4Tmd89MRlgikaj5+t5NsDgA== + dependencies: + "@types/async-lock" "^1.1.0" + "@types/is-buffer" "^2.0.0" + async-lock "^1.1.3" + buffer "^5.2.1" + debug "^3.1.0" + events "^3.0.0" + is-buffer "^2.0.3" + jssha "^2.3.1" + process "^0.11.10" + rhea "^1.0.18" + rhea-promise "^0.1.15" + stream-browserify "^2.0.2" + tslib "^1.9.3" + url "^0.11.0" + util "^0.11.1" + +"@azure/core-auth@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@azure/core-auth/-/core-auth-1.1.3.tgz#94e7bbc207010e7a2fdba61565443e4e1cf1e131" + integrity sha512-A4xigW0YZZpkj1zK7dKuzbBpGwnhEcRk6WWuIshdHC32raR3EQ1j6VA9XZqE+RFsUgH6OAmIK5BWIz+mZjnd6Q== + dependencies: + "@azure/abort-controller" "^1.0.0" + "@azure/core-tracing" "1.0.0-preview.8" + "@opentelemetry/api" "^0.6.1" + tslib "^2.0.0" + +"@azure/core-http@^1.0.0": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@azure/core-http/-/core-http-1.1.6.tgz#9d76bc569c9907e3224bd09c09b4ac08bde9faf8" + integrity sha512-/C+qNzhwlLKt0F6SjaBEyY2pwZvwL2LviyS5PHlCh77qWuTF1sETmYAINM88BCN+kke+UlECK4YOQaAjJwyHvQ== + dependencies: + "@azure/abort-controller" "^1.0.0" + "@azure/core-auth" "^1.1.3" + "@azure/core-tracing" "1.0.0-preview.9" + "@azure/logger" "^1.0.0" + "@opentelemetry/api" "^0.10.2" + "@types/node-fetch" "^2.5.0" + "@types/tunnel" "^0.0.1" + form-data "^3.0.0" + node-fetch "^2.6.0" + process "^0.11.10" + tough-cookie "^4.0.0" + tslib "^2.0.0" + tunnel "^0.0.6" + uuid "^8.1.0" + xml2js "^0.4.19" + +"@azure/core-tracing@1.0.0-preview.8": + version "1.0.0-preview.8" + resolved "https://registry.yarnpkg.com/@azure/core-tracing/-/core-tracing-1.0.0-preview.8.tgz#1e0ff857e855edb774ffd33476003c27b5bb2705" + integrity sha512-ZKUpCd7Dlyfn7bdc+/zC/sf0aRIaNQMDuSj2RhYRFe3p70hVAnYGp3TX4cnG2yoEALp/LTj/XnZGQ8Xzf6Ja/Q== + dependencies: + "@opencensus/web-types" "0.0.7" + "@opentelemetry/api" "^0.6.1" + tslib "^1.10.0" + +"@azure/core-tracing@1.0.0-preview.9": + version "1.0.0-preview.9" + resolved "https://registry.yarnpkg.com/@azure/core-tracing/-/core-tracing-1.0.0-preview.9.tgz#84f3b85572013f9d9b85e1e5d89787aa180787eb" + integrity sha512-zczolCLJ5QG42AEPQ+Qg9SRYNUyB+yZ5dzof4YEc+dyWczO9G2sBqbAjLB7IqrsdHN2apkiB2oXeDKCsq48jug== + dependencies: + "@opencensus/web-types" "0.0.7" + "@opentelemetry/api" "^0.10.2" + tslib "^2.0.0" + +"@azure/logger@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@azure/logger/-/logger-1.0.0.tgz#48b371dfb34288c8797e5c104f6c4fb45bf1772c" + integrity sha512-g2qLDgvmhyIxR3JVS8N67CyIOeFRKQlX/llxYJQr1OSGQqM3HTpVP8MjmjcEKbL/OIt2N9C9UFaNQuKOw1laOA== + dependencies: + tslib "^1.9.3" + +"@azure/service-bus@^1.1.9": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@azure/service-bus/-/service-bus-1.1.9.tgz#856cec37d0fbd8069fd9b717afa3aa9fdb48c95b" + integrity sha512-7i+7h1gh8vUeeaHddEVg+x7lXSVqhk/f2z4x4ErZByrE0QsyibFVPpBqS3q1RIjpDgmKPxcj7Wzu7O8vJLSaRw== + dependencies: + "@azure/amqp-common" "1.0.0-preview.16" + "@azure/core-http" "^1.0.0" + "@opentelemetry/types" "^0.2.0" + "@types/is-buffer" "^2.0.0" + "@types/long" "^4.0.0" + buffer "^5.2.1" + debug "^4.1.1" + is-buffer "^2.0.3" + long "^4.0.0" + process "^0.11.10" + rhea "^1.0.23" + rhea-promise "^0.1.15" + tslib "^1.10.0" + +"@babel/parser@^7.9.4": + version "7.11.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.4.tgz#6fa1a118b8b0d80d0267b719213dc947e88cc0ca" + integrity sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA== + +"@babel/runtime@^7.9.2": + version "7.11.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" + integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== + dependencies: + regenerator-runtime "^0.13.4" + +"@dabh/diagnostics@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.2.tgz#290d08f7b381b8f94607dc8f471a12c675f9db31" + integrity sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q== + dependencies: + colorspace "1.1.x" + enabled "2.0.x" + kuler "^2.0.0" + +"@google-cloud/paginator@^3.0.0": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@google-cloud/paginator/-/paginator-3.0.4.tgz#4106cadd8153d157c8afd16d66dfb89a38ba8748" + integrity sha512-fKI+jYQdV1F9jtG6tSRro3ilNSeBWVmTzxc8Z0kiPRXcj8eshh9fiF8TtxfDefyUKgTdWgHpzGBwLbZ/OGikJg== + dependencies: + arrify "^2.0.0" + extend "^3.0.2" + +"@google-cloud/precise-date@^2.0.0": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@google-cloud/precise-date/-/precise-date-2.0.3.tgz#14f6f28ce35dabf3882e7aeab1c9d51bd473faed" + integrity sha512-+SDJ3ZvGkF7hzo6BGa8ZqeK3F6Z4+S+KviC9oOK+XCs3tfMyJCh/4j93XIWINgMMDIh9BgEvlw4306VxlXIlYA== + +"@google-cloud/projectify@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@google-cloud/projectify/-/projectify-2.0.1.tgz#13350ee609346435c795bbfe133a08dfeab78d65" + integrity sha512-ZDG38U/Yy6Zr21LaR3BTiiLtpJl6RkPS/JwoRT453G+6Q1DhlV0waNf8Lfu+YVYGIIxgKnLayJRfYlFJfiI8iQ== + +"@google-cloud/promisify@^2.0.0": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@google-cloud/promisify/-/promisify-2.0.2.tgz#81d654b4cb227c65c7ad2f9a7715262febd409ed" + integrity sha512-EvuabjzzZ9E2+OaYf+7P9OAiiwbTxKYL0oGLnREQd+Su2NTQBpomkdlkBowFvyWsaV0d1sSGxrKpSNcrhPqbxg== + +"@google-cloud/pubsub@^2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@google-cloud/pubsub/-/pubsub-2.5.0.tgz#6c696d9b448f2e1689be9a37ef0362ed173731fd" + integrity sha512-7bbbQqa+LSTopVjt20EZ8maO6rEpbO7v8EvDImHMsbRS30HJ5+kClbaQTRvhNzhc1qy221A1GbHPHMCQ/U5E3Q== + dependencies: + "@google-cloud/paginator" "^3.0.0" + "@google-cloud/precise-date" "^2.0.0" + "@google-cloud/projectify" "^2.0.0" + "@google-cloud/promisify" "^2.0.0" + "@opentelemetry/api" "^0.10.0" + "@opentelemetry/tracing" "^0.10.0" + "@types/duplexify" "^3.6.0" + "@types/long" "^4.0.0" + arrify "^2.0.0" + extend "^3.0.2" + google-auth-library "^6.0.0" + google-gax "^2.7.0" + is-stream-ended "^0.1.4" + lodash.snakecase "^4.1.1" + p-defer "^3.0.0" + +"@grpc/grpc-js@~1.1.1": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.1.5.tgz#2d0b261cd54a529f6b78ac0de9d6fd91a9a3129c" + integrity sha512-2huf5z85TdZI4nLmJQ9Zdfd+6vmIyBDs7B4L71bTaHKA9pRsGKAH24XaktMk/xneKJIqAgeIZtg1cyivVZtvrg== + dependencies: + "@grpc/proto-loader" "^0.6.0-pre14" + "@types/node" "^12.12.47" + google-auth-library "^6.0.0" + semver "^6.2.0" + +"@grpc/proto-loader@^0.5.1": + version "0.5.5" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.5.5.tgz#6725e7a1827bdf8e92e29fbf4e9ef0203c0906a9" + integrity sha512-WwN9jVNdHRQoOBo9FDH7qU+mgfjPc8GygPYms3M+y3fbQLfnCe/Kv/E01t7JRgnrsOHH8euvSbed3mIalXhwqQ== + dependencies: + lodash.camelcase "^4.3.0" + protobufjs "^6.8.6" + +"@grpc/proto-loader@^0.6.0-pre14": + version "0.6.0-pre9" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.6.0-pre9.tgz#0c6fe42f6c5ef9ce1b3cef7be64d5b09d6fe4d6d" + integrity sha512-oM+LjpEjNzW5pNJjt4/hq1HYayNeQT+eGrOPABJnYHv7TyNPDNzkQ76rDYZF86X5swJOa4EujEMzQ9iiTdPgww== + dependencies: + "@types/long" "^4.0.1" + lodash.camelcase "^4.3.0" + long "^4.0.0" + protobufjs "^6.9.0" + yargs "^15.3.1" + +"@nodelib/fs.scandir@2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" + integrity sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw== + dependencies: + "@nodelib/fs.stat" "2.0.3" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3" + integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976" + integrity sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ== + dependencies: + "@nodelib/fs.scandir" "2.1.3" + fastq "^1.6.0" + +"@opencensus/web-types@0.0.7": + version "0.0.7" + resolved "https://registry.yarnpkg.com/@opencensus/web-types/-/web-types-0.0.7.tgz#4426de1fe5aa8f624db395d2152b902874f0570a" + integrity sha512-xB+w7ZDAu3YBzqH44rCmG9/RlrOmFuDPt/bpf17eJr8eZSrLt7nc7LnWdxM9Mmoj/YKMHpxRg28txu3TcpiL+g== + +"@opentelemetry/api@^0.10.0", "@opentelemetry/api@^0.10.2": + version "0.10.2" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-0.10.2.tgz#9647b881f3e1654089ff7ea59d587b2d35060654" + integrity sha512-GtpMGd6vkzDMYcpu2t9LlhEgMy/SzBwRnz48EejlRArYqZzqSzAsKmegUK7zHgl+EOIaK9mKHhnRaQu3qw20cA== + dependencies: + "@opentelemetry/context-base" "^0.10.2" + +"@opentelemetry/api@^0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-0.6.1.tgz#a00b504801f408230b9ad719716fe91ad888c642" + integrity sha512-wpufGZa7tTxw7eAsjXJtiyIQ42IWQdX9iUQp7ACJcKo1hCtuhLU+K2Nv1U6oRwT1oAlZTE6m4CgWKZBhOiau3Q== + dependencies: + "@opentelemetry/context-base" "^0.6.1" + +"@opentelemetry/context-base@^0.10.2": + version "0.10.2" + resolved "https://registry.yarnpkg.com/@opentelemetry/context-base/-/context-base-0.10.2.tgz#55bea904b2b91aa8a8675df9eaba5961bddb1def" + integrity sha512-hZNKjKOYsckoOEgBziGMnBcX0M7EtstnCmwz5jZUOUYwlZ+/xxX6z3jPu1XVO2Jivk0eLfuP9GP+vFD49CMetw== + +"@opentelemetry/context-base@^0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/context-base/-/context-base-0.6.1.tgz#b260e454ee4f9635ea024fc83be225e397f15363" + integrity sha512-5bHhlTBBq82ti3qPT15TRxkYTFPPQWbnkkQkmHPtqiS1XcTB69cEKd3Jm7Cfi/vkPoyxapmePE9tyA7EzLt8SQ== + +"@opentelemetry/core@^0.10.2": + version "0.10.2" + resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-0.10.2.tgz#86b9e94bbcaf8e07bb86e8205aa1d53af854e7de" + integrity sha512-DhkiTp5eje2zTGd+HAIKWpGE6IR6lq7tUpYt4nnkhOi6Hq9WQAANVDCWEZEbYOw57LkdXbE50FZ/kMvHDm450Q== + dependencies: + "@opentelemetry/api" "^0.10.2" + "@opentelemetry/context-base" "^0.10.2" + semver "^7.1.3" + +"@opentelemetry/resources@^0.10.2": + version "0.10.2" + resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-0.10.2.tgz#6e291d525450359c615aac013fd977047f2c26d7" + integrity sha512-5JGC2TPSAIHth615IURt+sSsTljY43zTfJD0JE9PHC6ipZPiQ0dpQDZOrLn8NAMfOHY1jeWwpIuLASjqbXUfuw== + dependencies: + "@opentelemetry/api" "^0.10.2" + "@opentelemetry/core" "^0.10.2" + gcp-metadata "^3.5.0" + +"@opentelemetry/tracing@^0.10.0": + version "0.10.2" + resolved "https://registry.yarnpkg.com/@opentelemetry/tracing/-/tracing-0.10.2.tgz#384779a6e1be988200cc316a97030d95bd8f2129" + integrity sha512-mNAhARn4dEdOjTa9OdysjI4fRHMbvr4YSbPuH7jhkyPzgoa+DnvnbY3GGpEay6kpuYJsrW8Ef9OIKAV/GndhbQ== + dependencies: + "@opentelemetry/api" "^0.10.2" + "@opentelemetry/context-base" "^0.10.2" + "@opentelemetry/core" "^0.10.2" + "@opentelemetry/resources" "^0.10.2" + +"@opentelemetry/types@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/types/-/types-0.2.0.tgz#2a0afd40fa7026e39ea56a454642bda72b172f80" + integrity sha512-GtwNB6BNDdsIPAYEdpp3JnOGO/3AJxjPvny53s3HERBdXSJTGQw8IRhiaTEX0b3w9P8+FwFZde4k+qkjn67aVw== + +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha1-m4sMxmPWaafY9vXQiToU00jzD78= + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha1-NVy8mLr61ZePntCV85diHx0Ga3A= + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU= + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E= + +"@protobufjs/inquire@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + integrity sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik= + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha1-bMKyDFya1q0NzP0hynZz2Nf79o0= + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q= + +"@protobufjs/utf8@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= + +"@sindresorhus/is@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" + integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== + +"@szmarczak/http-timer@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" + integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== + dependencies: + defer-to-connect "^1.0.1" + +"@types/async-lock@^1.1.0": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@types/async-lock/-/async-lock-1.1.2.tgz#cbc26a34b11b83b28f7783a843c393b443ef8bef" + integrity sha512-j9n4bb6RhgFIydBe0+kpjnBPYumDaDyU8zvbWykyVMkku+c2CSu31MZkLeaBfqIwU+XCxlDpYDfyMQRkM0AkeQ== + +"@types/color-name@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" + integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== + +"@types/duplexify@^3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@types/duplexify/-/duplexify-3.6.0.tgz#dfc82b64bd3a2168f5bd26444af165bf0237dcd8" + integrity sha512-5zOA53RUlzN74bvrSGwjudssD9F3a797sDZQkiYpUOxW+WHaXTCPz4/d5Dgi6FKnOqZ2CpaTo0DhgIfsXAOE/A== + dependencies: + "@types/node" "*" + +"@types/is-buffer@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/is-buffer/-/is-buffer-2.0.0.tgz#94de4d23540646de5f5df2cd346a1c162067ea5a" + integrity sha512-0f7N/e3BAz32qDYvgB4d2cqv1DqUwvGxHkXsrucICn8la1Vb6Yl6Eg8mPScGwUiqHJeE7diXlzaK+QMA9m4Gxw== + dependencies: + "@types/node" "*" + +"@types/long@^4.0.0", "@types/long@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" + integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== + +"@types/node-fetch@^2.5.0": + version "2.5.7" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.7.tgz#20a2afffa882ab04d44ca786449a276f9f6bbf3c" + integrity sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw== + dependencies: + "@types/node" "*" + form-data "^3.0.0" + +"@types/node@*": + version "14.6.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.6.1.tgz#fdf6f6c6c73d3d8eee9c98a9a0485bc524b048d7" + integrity sha512-HnYlg/BRF8uC1FyKRFZwRaCPTPYKa+6I8QiUZFLredaGOou481cgFS4wKRFyKvQtX8xudqkSdBczJHIYSQYKrQ== + +"@types/node@^12.12.47": + version "12.12.54" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.54.tgz#a4b58d8df3a4677b6c08bfbc94b7ad7a7a5f82d1" + integrity sha512-ge4xZ3vSBornVYlDnk7yZ0gK6ChHf/CHB7Gl1I0Jhah8DDnEQqBzgohYG4FX4p81TNirSETOiSyn+y1r9/IR6w== + +"@types/node@^13.7.0": + version "13.13.15" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.15.tgz#fe1cc3aa465a3ea6858b793fd380b66c39919766" + integrity sha512-kwbcs0jySLxzLsa2nWUAGOd/s21WU1jebrEdtzhsj1D4Yps1EOuyI1Qcu+FD56dL7NRNIJtDDjcqIG22NwkgLw== + +"@types/tunnel@^0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@types/tunnel/-/tunnel-0.0.1.tgz#0d72774768b73df26f25df9184273a42da72b19c" + integrity sha512-AOqu6bQu5MSWwYvehMXLukFHnupHrpZ8nvgae5Ggie9UwzDR1CCwoXgSSWNZJuyOlCdfdsWMA5F2LlmvyoTv8A== + dependencies: + "@types/node" "*" + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + +agent-base@6: + version "6.0.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.1.tgz#808007e4e5867decb0ab6ab2f928fbdb5a596db4" + integrity sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg== + dependencies: + debug "4" + +ajv@^6.12.3: + version "6.12.4" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.4.tgz#0614facc4522127fa713445c6bfd3ebd376e2234" + integrity sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +amqplib@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/amqplib/-/amqplib-0.6.0.tgz#87857c7c95d56d22438ced4cf1f7e5f0dc43b309" + integrity sha512-zXCh4jQ77TBZe1YtvZ1n7sUxnTjnNagpy8MVi2yc1ive239pS3iLwm4e4d5o4XZGx1BdTKQ/U0ZmaDU3c8MxYQ== + dependencies: + bitsyntax "~0.1.0" + bluebird "^3.5.2" + buffer-more-ints "~1.0.0" + readable-stream "1.x >=1.1.9" + safe-buffer "~5.1.2" + url-parse "~1.4.3" + +ansi-align@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.0.tgz#b536b371cf687caaef236c18d3e21fe3797467cb" + integrity sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw== + dependencies: + string-width "^3.0.0" + +ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" + integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== + dependencies: + "@types/color-name" "^1.1.1" + color-convert "^2.0.1" + +anymatch@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" + integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +arrify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" + integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +async-lock@^1.1.3: + version "1.2.4" + resolved "https://registry.yarnpkg.com/async-lock/-/async-lock-1.2.4.tgz#80d0d612383045dd0c30eb5aad08510c1397cb91" + integrity sha512-UBQJC2pbeyGutIfYmErGc9RaJYnpZ1FHaxuKwb0ahvGiiCkPUf3p67Io+YLPmmv3RHY+mF6JEtNW8FlHsraAaA== + +async@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" + integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + +aws-sdk@^2.741.0: + version "2.741.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.741.0.tgz#b832d838e5f1ef9cdb2b4901ade4555b1ca4c085" + integrity sha512-bpk5VvlBSvKu3Lg30AxX+PHk+0TD69K3tYe6D6VeEFl/3XzuZ5RKTGCIl96isSMyYc5bBMhLS9pC9glrxT7OTw== + dependencies: + buffer "4.9.2" + events "1.1.1" + ieee754 "1.1.13" + jmespath "0.15.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "3.3.2" + xml2js "0.4.19" + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.10.1" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.1.tgz#e1e82e4f3e999e2cfd61b161280d16a111f86428" + integrity sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA== + +azure-common@^0.9.22: + version "0.9.25" + resolved "https://registry.yarnpkg.com/azure-common/-/azure-common-0.9.25.tgz#fc7cfb08c65398b3c90f9b9bc14a99e204319b9a" + integrity sha512-L7YO3DUQ0iwiaUyD9Wy6B66Y6HmCzMb9vxUqKklgzU+gFRRBKIMSVR4oZS6IkQfFCSm9eKwHuH2p3UdDPszd7g== + dependencies: + dateformat "1.0.2-1.2.3" + duplexer "~0.1.1" + envconf "~0.0.4" + request "^2.81.0" + through "~2.3.4" + tunnel "~0.0.2" + underscore "1.4.x" + validator "^9.4.1" + xml2js "^0.4.19" + xmlbuilder "15.1.1" + +azure-sb@^0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/azure-sb/-/azure-sb-0.11.1.tgz#15cba11c1a3b4aca68c344460b99bceec14be11a" + integrity sha512-ZYgPeSDMD99i/Em+6wT78zvBkJ/dbh2ypb4DbqQ1Flaif5vWJFzC/iKxxcq/vq+THWoO3+UbqWa0JNXnW3zAvw== + dependencies: + azure-common "^0.9.22" + mpns "2.1.3" + underscore "^1.8.3" + wns "~0.5.3" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +base64-js@^1.0.2, base64-js@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" + integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +bignumber.js@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.0.tgz#805880f84a329b5eac6e7cb6f8274b6d82bdf075" + integrity sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A== + +binary-extensions@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" + integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ== + +bitsyntax@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/bitsyntax/-/bitsyntax-0.1.0.tgz#b0c59acef03505de5a2ed62a2f763c56ae1d6205" + integrity sha512-ikAdCnrloKmFOugAfxWws89/fPc+nw0OOG1IzIE72uSOg/A3cYptKCjSUhDTuj7fhsJtzkzlv7l3b8PzRHLN0Q== + dependencies: + buffer-more-ints "~1.0.0" + debug "~2.6.9" + safe-buffer "~5.1.2" + +bluebird@^3.5.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +boxen@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64" + integrity sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ== + dependencies: + ansi-align "^3.0.0" + camelcase "^5.3.1" + chalk "^3.0.0" + cli-boxes "^2.2.0" + string-width "^4.1.0" + term-size "^2.1.0" + type-fest "^0.8.1" + widest-line "^3.1.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.1, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= + +buffer-more-ints@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz#ef4f8e2dddbad429ed3828a9c55d44f05c611422" + integrity sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg== + +buffer@4.9.2: + version "4.9.2" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" + integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + +buffer@^5.2.1: + version "5.6.0" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" + integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + +byline@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" + integrity sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE= + +cacheable-request@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" + integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^3.0.0" + lowercase-keys "^2.0.0" + normalize-url "^4.1.0" + responselike "^1.0.2" + +camelcase@^5.0.0, camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chokidar@^3.2.2: + version "3.4.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.2.tgz#38dc8e658dec3809741eb3ef7bb0a47fe424232d" + integrity sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.4.0" + optionalDependencies: + fsevents "~2.1.2" + +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + +cli-boxes@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.0.tgz#538ecae8f9c6ca508e3c3c95b453fe93cb4c168d" + integrity sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w== + +cliui@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" + integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^6.2.0" + +clone-response@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" + integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= + dependencies: + mimic-response "^1.0.0" + +color-convert@^1.9.1: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +color-name@^1.0.0, color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-string@^1.5.2: + version "1.5.3" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc" + integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@3.0.x: + version "3.0.0" + resolved "https://registry.yarnpkg.com/color/-/color-3.0.0.tgz#d920b4328d534a3ac8295d68f7bd4ba6c427be9a" + integrity sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w== + dependencies: + color-convert "^1.9.1" + color-string "^1.5.2" + +colors@^1.2.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" + integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== + +colorspace@1.1.x: + version "1.1.2" + resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.2.tgz#e0128950d082b86a2168580796a0aa5d6c68d8c5" + integrity sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ== + dependencies: + color "3.0.x" + text-hex "1.0.x" + +combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +config@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/config/-/config-3.3.1.tgz#b6a70e2908a43b98ed20be7e367edf0cc8ed5a19" + integrity sha512-+2/KaaaAzdwUBE3jgZON11L1ggLLhpf2FsGrfqYFHZW22ySGv/HqYIXrBwKKvn+XZh1UBUjHwAcrfsSkSygT+Q== + dependencies: + json5 "^2.1.1" + +configstore@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" + integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== + dependencies: + dot-prop "^5.2.0" + graceful-fs "^4.1.2" + make-dir "^3.0.0" + unique-string "^2.0.0" + write-file-atomic "^3.0.0" + xdg-basedir "^4.0.0" + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +crypto-random-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" + integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +dateformat@1.0.2-1.2.3: + version "1.0.2-1.2.3" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.2-1.2.3.tgz#b0220c02de98617433b72851cf47de3df2cdbee9" + integrity sha1-sCIMAt6YYXQztyhRz0fePfLNvuk= + +"debug@0.8.0 - 3.5.0", debug@^3.1.0, debug@^3.2.6: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +debug@4, debug@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +debug@^2.2.0, debug@~2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + +decompress-response@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" + integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M= + dependencies: + mimic-response "^1.0.0" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + +defer-to-connect@^1.0.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" + integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +dot-prop@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.2.0.tgz#c34ecc29556dc45f1f4c22697b6f4904e0cc4fcb" + integrity sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A== + dependencies: + is-obj "^2.0.0" + +duplexer3@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" + integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= + +duplexer@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" + integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== + +duplexify@^3.6.0: + version "3.7.1" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" + integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +enabled@2.0.x: + version "2.0.0" + resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" + integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== + +end-of-stream@^1.0.0, end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +envconf@~0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/envconf/-/envconf-0.0.4.tgz#85675afba237c43f98de2d46adc0e532a4dcf48b" + integrity sha1-hWda+6I3xD+Y3i1GrcDlMqTc9Is= + +escape-goat@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" + integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== + +escodegen@^1.14.1: + version "1.14.3" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" + integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== + dependencies: + esprima "^4.0.1" + estraverse "^4.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + +esprima@^4.0.0, esprima@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +estraverse@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +events@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= + +events@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379" + integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg== + +expand-template@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" + integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== + +extend@^3.0.2, extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.1.1: + version "3.2.4" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3" + integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.0" + merge2 "^1.3.0" + micromatch "^4.0.2" + picomatch "^2.2.1" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +fast-safe-stringify@^2.0.4: + version "2.0.7" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" + integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== + +fast-text-encoding@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53" + integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig== + +fastq@^1.6.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481" + integrity sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q== + dependencies: + reusify "^1.0.4" + +fecha@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.0.tgz#3ffb6395453e3f3efff850404f0a59b6747f5f41" + integrity sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg== + +file-stream-rotator@^0.5.7: + version "0.5.7" + resolved "https://registry.yarnpkg.com/file-stream-rotator/-/file-stream-rotator-0.5.7.tgz#868a2e5966f7640a17dd86eda0e4467c089f6286" + integrity sha512-VYb3HZ/GiAGUCrfeakO8Mp54YGswNUHvL7P09WQcXAJNSj3iQ5QraYSp3cIn1MUyw6uzfgN/EFOarCNa4JvUHQ== + dependencies: + moment "^2.11.2" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +fn.name@1.x.x: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" + integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682" + integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +from2@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" + integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8= + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.0" + +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-extra@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc" + integrity sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^1.0.0" + +fsevents@~2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" + integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== + +gaxios@^2.1.0: + version "2.3.4" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-2.3.4.tgz#eea99353f341c270c5f3c29fc46b8ead56f0a173" + integrity sha512-US8UMj8C5pRnao3Zykc4AAVr+cffoNKRTg9Rsf2GiuZCW69vgJj38VK2PzlPuQU73FZ/nTk9/Av6/JGcE1N9vA== + dependencies: + abort-controller "^3.0.0" + extend "^3.0.2" + https-proxy-agent "^5.0.0" + is-stream "^2.0.0" + node-fetch "^2.3.0" + +gaxios@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-3.1.0.tgz#95f65f5a335f61aff602fe124cfdba8524f765fa" + integrity sha512-DDTn3KXVJJigtz+g0J3vhcfbDbKtAroSTxauWsdnP57sM5KZ3d2c/3D9RKFJ86s43hfw6WULg6TXYw/AYiBlpA== + dependencies: + abort-controller "^3.0.0" + extend "^3.0.2" + https-proxy-agent "^5.0.0" + is-stream "^2.0.0" + node-fetch "^2.3.0" + +gcp-metadata@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-3.5.0.tgz#6d28343f65a6bbf8449886a0c0e4a71c77577055" + integrity sha512-ZQf+DLZ5aKcRpLzYUyBS3yo3N0JSa82lNDO8rj3nMSlovLcz2riKFBsYgDzeXcv75oo5eqB2lx+B14UvPoCRnA== + dependencies: + gaxios "^2.1.0" + json-bigint "^0.3.0" + +gcp-metadata@^4.1.0: + version "4.1.4" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-4.1.4.tgz#3adadb9158c716c325849ee893741721a3c09e7e" + integrity sha512-5J/GIH0yWt/56R3dNaNWPGQ/zXsZOddYECfJaqxFWgrZ9HC2Kvc5vl9upOgUUHKzURjAVf2N+f6tEJiojqXUuA== + dependencies: + gaxios "^3.0.0" + json-bigint "^1.0.0" + +get-caller-file@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-stream@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +get-stream@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +glob-parent@^5.1.0, glob-parent@~5.1.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" + integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== + dependencies: + is-glob "^4.0.1" + +global-dirs@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-2.0.1.tgz#acdf3bb6685bcd55cb35e8a052266569e9469201" + integrity sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A== + dependencies: + ini "^1.3.5" + +globby@^11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357" + integrity sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.1.1" + ignore "^5.1.4" + merge2 "^1.3.0" + slash "^3.0.0" + +google-auth-library@^6.0.0: + version "6.0.6" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-6.0.6.tgz#5102e5c643baab45b4c16e9752cd56b8861f3a82" + integrity sha512-fWYdRdg55HSJoRq9k568jJA1lrhg9i2xgfhVIMJbskUmbDpJGHsbv9l41DGhCDXM21F9Kn4kUwdysgxSYBYJUw== + dependencies: + arrify "^2.0.0" + base64-js "^1.3.0" + ecdsa-sig-formatter "^1.0.11" + fast-text-encoding "^1.0.0" + gaxios "^3.0.0" + gcp-metadata "^4.1.0" + gtoken "^5.0.0" + jws "^4.0.0" + lru-cache "^6.0.0" + +google-gax@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/google-gax/-/google-gax-2.7.0.tgz#5aaff7bdbe32730f975f416dd4897c558c89ab84" + integrity sha512-0dBATy8mMVlfOBrT85Q+NzBpZ4OJZUMrPI9wJULpiIDq2w1zlN30Duor+fQUcMEjanYEc72G58M4iUVve0jfXw== + dependencies: + "@grpc/grpc-js" "~1.1.1" + "@grpc/proto-loader" "^0.5.1" + "@types/long" "^4.0.0" + abort-controller "^3.0.0" + duplexify "^3.6.0" + google-auth-library "^6.0.0" + is-stream-ended "^0.1.4" + lodash.at "^4.6.0" + lodash.has "^4.5.2" + node-fetch "^2.6.0" + protobufjs "^6.9.0" + retry-request "^4.0.0" + semver "^6.0.0" + walkdir "^0.4.0" + +google-p12-pem@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-3.0.2.tgz#12d443994b6f4cd8c9e4ac479f2f18d4694cbdb8" + integrity sha512-tbjzndQvSIHGBLzHnhDs3cL4RBjLbLXc2pYvGH+imGVu5b4RMAttUTdnmW2UH0t11QeBTXZ7wlXPS7hrypO/tg== + dependencies: + node-forge "^0.9.0" + +got@^9.6.0: + version "9.6.0" + resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" + integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== + dependencies: + "@sindresorhus/is" "^0.14.0" + "@szmarczak/http-timer" "^1.1.2" + cacheable-request "^6.0.0" + decompress-response "^3.3.0" + duplexer3 "^0.1.4" + get-stream "^4.1.0" + lowercase-keys "^1.0.1" + mimic-response "^1.0.1" + p-cancelable "^1.0.0" + to-readable-stream "^1.0.0" + url-parse-lax "^3.0.0" + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.4" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" + integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== + +gtoken@^5.0.0: + version "5.0.3" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-5.0.3.tgz#b76ef8e9a2fed6fef165e47f7d05b60c498e4d05" + integrity sha512-Nyd1wZCMRc2dj/mAD0LlfQLcAO06uKdpKJXvK85SGrF5+5+Bpfil9u/2aw35ltvEHjvl0h5FMKN5knEU+9JrOg== + dependencies: + gaxios "^3.0.0" + google-p12-pem "^3.0.0" + jws "^4.0.0" + mime "^2.2.0" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.3: + version "5.1.5" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" + integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== + dependencies: + ajv "^6.12.3" + har-schema "^2.0.0" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-yarn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77" + integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw== + +http-cache-semantics@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" + integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +https-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" + integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== + dependencies: + agent-base "6" + debug "4" + +ieee754@1.1.13, ieee754@^1.1.4: + version "1.1.13" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== + +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= + +ignore@^5.1.4: + version "5.1.8" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" + integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== + +import-lazy@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" + integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM= + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@^1.3.5, ini@~1.3.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + +into-stream@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-5.1.1.tgz#f9a20a348a11f3c13face22763f2d02e127f4db8" + integrity sha512-krrAJ7McQxGGmvaYbB7Q1mcA+cRwg9Ij2RfWIeVesNBgVDZmzY/Fa4IpZUT3bmdRzMzdf/mzltCG2Dq99IZGBA== + dependencies: + from2 "^2.3.0" + p-is-promise "^3.0.0" + +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-buffer@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" + integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A== + +is-ci@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" + integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== + dependencies: + ci-info "^2.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + +is-installed-globally@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141" + integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g== + dependencies: + global-dirs "^2.0.1" + is-path-inside "^3.0.1" + +is-npm@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d" + integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig== + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" + integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== + +is-path-inside@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017" + integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg== + +is-stream-ended@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-stream-ended/-/is-stream-ended-0.1.4.tgz#f50224e95e06bce0e356d440a4827cd35b267eda" + integrity sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw== + +is-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" + integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== + +is-typedarray@^1.0.0, is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +is-yarn-global@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" + integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + +isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +jmespath@0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" + integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc= + +js-yaml@^3.14.0: + version "3.14.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" + integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +json-bigint@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-0.3.1.tgz#0c1729d679f580d550899d6a2226c228564afe60" + integrity sha512-DGWnSzmusIreWlEupsUelHrhwmPPE+FiQvg+drKfk2p+bdEYa5mp4PJ8JsCWqae0M2jQNb0HPvnwvf1qOTThzQ== + dependencies: + bignumber.js "^9.0.0" + +json-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" + integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== + dependencies: + bignumber.js "^9.0.0" + +json-buffer@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" + integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +json5@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" + integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== + dependencies: + minimist "^1.2.5" + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= + optionalDependencies: + graceful-fs "^4.1.6" + +jsonfile@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.0.1.tgz#98966cba214378c8c84b82e085907b40bf614179" + integrity sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg== + dependencies: + universalify "^1.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +jssha@^2.3.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/jssha/-/jssha-2.4.2.tgz#d950b095634928bd6b2bda1d42da9a3a762d65e9" + integrity sha512-/jsi/9C0S70zfkT/4UlKQa5E1xKurDnXcQizcww9JSR/Fv+uIbWM2btG+bFcL3iNoK9jIGS0ls9HWLr1iw0kFg== + +jwa@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc" + integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" + integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== + dependencies: + jwa "^2.0.0" + safe-buffer "^5.0.1" + +kafkajs@^1.14.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/kafkajs/-/kafkajs-1.14.0.tgz#3d998a77bfde54dc502e8e88690eedf0b21a1ed6" + integrity sha512-W+WCekiooY5rJP3Me5N3gWcQ8O6uG6lw0vv9t+sI+WqXKjKwj2+CWIXJy241x+ITE+1M1D19ABSiL2J8lKja5A== + +keyv@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" + integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== + dependencies: + json-buffer "3.0.0" + +kuler@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" + integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== + +latest-version@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" + integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA== + dependencies: + package-json "^6.3.0" + +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +lodash.at@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.at/-/lodash.at-4.6.0.tgz#93cdce664f0a1994ea33dd7cd40e23afd11b0ff8" + integrity sha1-k83OZk8KGZTqM9181A4jr9EbD/g= + +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= + +lodash.has@^4.5.2: + version "4.5.2" + resolved "https://registry.yarnpkg.com/lodash.has/-/lodash.has-4.5.2.tgz#d19f4dc1095058cccbe2b0cdf4ee0fe4aa37c862" + integrity sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI= + +lodash.snakecase@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz#39d714a35357147837aefd64b5dcbb16becd8f8d" + integrity sha1-OdcUo1NXFHg3rv1ktdy7Fr7Nj40= + +logform@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/logform/-/logform-2.2.0.tgz#40f036d19161fc76b68ab50fdc7fe495544492f2" + integrity sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg== + dependencies: + colors "^1.2.1" + fast-safe-stringify "^2.0.4" + fecha "^4.2.0" + ms "^2.1.1" + triple-beam "^1.3.0" + +long@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" + integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== + +lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" + integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== + +lowercase-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" + integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +make-dir@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" + integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + dependencies: + braces "^3.0.1" + picomatch "^2.0.5" + +mime-db@1.44.0: + version "1.44.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" + integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.27" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" + integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== + dependencies: + mime-db "1.44.0" + +mime@^2.2.0: + version "2.4.6" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" + integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== + +mimic-response@^1.0.0, mimic-response@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" + integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.0, minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +mkdirp@^0.5.1: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + +moment@^2.11.2: + version "2.27.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" + integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ== + +mpns@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/mpns/-/mpns-2.1.3.tgz#4328bb23ca79669e3383389074c898713e22ccb9" + integrity sha512-gPLNoVqwYoKUmNYZ2shMSdaE2XvHSRxWNzyG4DUi6Av7MSujyeOw/nj61nnQeuV/vke5E0Dni468xn0qxTHIZQ== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +multistream@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/multistream/-/multistream-2.1.1.tgz#629d3a29bd76623489980d04519a2c365948148c" + integrity sha512-xasv76hl6nr1dEy3lPvy7Ej7K/Lx3O/FCvwge8PeVJpciPPoNCbaANcNiBug3IpdvTveZUcAV0DJzdnUDMesNQ== + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.5" + +node-fetch@^2.3.0, node-fetch@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" + integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== + +node-forge@^0.9.0: + version "0.9.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.1.tgz#775368e6846558ab6676858a4d8c6e8d16c677b5" + integrity sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ== + +nodemon@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.4.tgz#55b09319eb488d6394aa9818148c0c2d1c04c416" + integrity sha512-Ltced+hIfTmaS28Zjv1BM552oQ3dbwPqI4+zI0SLgq+wpJhSyqgYude/aZa/3i31VCQWMfXJVxvu86abcam3uQ== + dependencies: + chokidar "^3.2.2" + debug "^3.2.6" + ignore-by-default "^1.0.1" + minimatch "^3.0.4" + pstree.remy "^1.1.7" + semver "^5.7.1" + supports-color "^5.5.0" + touch "^3.1.0" + undefsafe "^2.0.2" + update-notifier "^4.0.0" + +nopt@~1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" + integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4= + dependencies: + abbrev "1" + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-url@^4.1.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" + integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-hash@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.0.3.tgz#d12db044e03cd2ca3d77c0570d87225b02e1e6ea" + integrity sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg== + +once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +one-time@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45" + integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g== + dependencies: + fn.name "1.x.x" + +optionator@^0.8.1: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + +os-tmpdir@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + +p-cancelable@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" + integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== + +p-defer@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-3.0.0.tgz#d1dceb4ee9b2b604b1d94ffec83760175d4e6f83" + integrity sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw== + +p-is-promise@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-3.0.0.tgz#58e78c7dfe2e163cf2a04ff869e7c1dba64a5971" + integrity sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ== + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +package-json@^6.3.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0" + integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ== + dependencies: + got "^9.6.0" + registry-auth-token "^4.0.0" + registry-url "^5.0.0" + semver "^6.2.0" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" + integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== + +pkg-fetch@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/pkg-fetch/-/pkg-fetch-2.6.9.tgz#c18c5fa9604c57a3df3d9630afb64e176bc8732d" + integrity sha512-EnVR8LRILXBvaNP+wJOSY02c3+qDDfyEyR+aqAHLhcc9PBnbxFT9UZ1+If49goPQzQPn26TzF//fc6KXZ0aXEg== + dependencies: + "@babel/runtime" "^7.9.2" + byline "^5.0.0" + chalk "^3.0.0" + expand-template "^2.0.3" + fs-extra "^8.1.0" + minimist "^1.2.5" + progress "^2.0.3" + request "^2.88.0" + request-progress "^3.0.0" + semver "^6.3.0" + unique-temp-dir "^1.0.0" + +pkg@^4.4.9: + version "4.4.9" + resolved "https://registry.yarnpkg.com/pkg/-/pkg-4.4.9.tgz#be04f8d03795772b7c4394724ae7252d7c2a4519" + integrity sha512-FK4GqHtcCY2PPPVaKViU0NyRzpo6gCS7tPKN5b7AkElqjAOCH1bsRKgohEnxThr6DWfTGByGqba2YHGR/BqbmA== + dependencies: + "@babel/parser" "^7.9.4" + "@babel/runtime" "^7.9.2" + chalk "^3.0.0" + escodegen "^1.14.1" + fs-extra "^8.1.0" + globby "^11.0.0" + into-stream "^5.1.1" + minimist "^1.2.5" + multistream "^2.1.1" + pkg-fetch "^2.6.9" + progress "^2.0.3" + resolve "^1.15.1" + stream-meter "^1.0.4" + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= + +prepend-http@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" + integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= + +progress@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +protobufjs@^6.8.6, protobufjs@^6.9.0: + version "6.10.1" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.10.1.tgz#e6a484dd8f04b29629e9053344e3970cccf13cd2" + integrity sha512-pb8kTchL+1Ceg4lFd5XUpK8PdWacbvV5SK2ULH2ebrYtl4GjJmS24m6CKME67jzV53tbJxHlnNOSqQHbTsR9JQ== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/long" "^4.0.1" + "@types/node" "^13.7.0" + long "^4.0.0" + +psl@^1.1.28, psl@^1.1.33: + version "1.8.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" + integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== + +pstree.remy@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= + +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +pupa@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.0.1.tgz#dbdc9ff48ffbea4a26a069b6f9f7abb051008726" + integrity sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA== + dependencies: + escape-goat "^2.0.0" + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= + +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + +rc@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +"readable-stream@1.x >=1.1.9": + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.1.4, readable-stream@^2.3.7: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.4.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada" + integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ== + dependencies: + picomatch "^2.2.1" + +regenerator-runtime@^0.13.4: + version "0.13.7" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" + integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== + +registry-auth-token@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.0.tgz#1d37dffda72bbecd0f581e4715540213a65eb7da" + integrity sha512-P+lWzPrsgfN+UEpDS3U8AQKg/UjZX6mQSJueZj3EK+vNESoqBSpBUD3gmu4sF9lOsjXWjF11dQKUqemf3veq1w== + dependencies: + rc "^1.2.8" + +registry-url@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009" + integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw== + dependencies: + rc "^1.2.8" + +request-progress@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe" + integrity sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4= + dependencies: + throttleit "^1.0.0" + +request@^2.81.0, request@^2.88.0: + version "2.88.2" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" + integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= + +resolve@^1.15.1: + version "1.17.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" + integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== + dependencies: + path-parse "^1.0.6" + +responselike@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" + integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= + dependencies: + lowercase-keys "^1.0.0" + +retry-request@^4.0.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-4.1.3.tgz#d5f74daf261372cff58d08b0a1979b4d7cab0fde" + integrity sha512-QnRZUpuPNgX0+D1xVxul6DbJ9slvo4Rm6iV/dn63e048MvGbUZiKySVt6Tenp04JqmchxjiLltGerOJys7kJYQ== + dependencies: + debug "^4.1.1" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rhea-promise@^0.1.15: + version "0.1.15" + resolved "https://registry.yarnpkg.com/rhea-promise/-/rhea-promise-0.1.15.tgz#00175324352224424d59b7712faf14097a84e8f2" + integrity sha512-+6uilZXSJGyiqVeHQI3Krv6NTAd8cWRCY2uyCxmzR4/5IFtBqqFem1HV2OiwSj0Gu7OFChIJDfH2JyjN7J0vRA== + dependencies: + debug "^3.1.0" + rhea "^1.0.4" + tslib "^1.9.3" + +rhea@^1.0.18, rhea@^1.0.23, rhea@^1.0.4: + version "1.0.24" + resolved "https://registry.yarnpkg.com/rhea/-/rhea-1.0.24.tgz#7084e4c635aa8ba0faf2d9602ac9f97d26aedc00" + integrity sha512-PEl62U2EhxCO5wMUZ2/bCBcXAVKN9AdMSNQOrp3+R5b77TEaOSiy16MQ0sIOmzj/iqsgIAgPs1mt3FYfu1vIXA== + dependencies: + debug "0.8.0 - 3.5.0" + +run-parallel@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" + integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== + +safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1, safe-buffer@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sax@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" + integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o= + +sax@>=0.6.0: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + +semver-diff@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" + integrity sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg== + dependencies: + semver "^6.3.0" + +semver@^5.7.1: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +semver@^7.1.3: + version "7.3.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" + integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + +signal-exit@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" + integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== + +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= + dependencies: + is-arrayish "^0.3.1" + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +sshpk@^1.7.0: + version "1.16.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +stack-trace@0.0.x: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" + integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= + +stream-browserify@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" + integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg== + dependencies: + inherits "~2.0.1" + readable-stream "^2.0.2" + +stream-meter@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/stream-meter/-/stream-meter-1.0.4.tgz#52af95aa5ea760a2491716704dbff90f73afdd1d" + integrity sha1-Uq+Vql6nYKJJFxZwTb/5D3Ov3R0= + dependencies: + readable-stream "^2.1.4" + +stream-shift@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" + integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== + +string-width@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= + +supports-color@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + +term-size@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.0.tgz#1f16adedfe9bdc18800e1776821734086fcc6753" + integrity sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw== + +text-hex@1.0.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" + integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== + +throttleit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" + integrity sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw= + +through@~2.3.4: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +to-readable-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" + integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +touch@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" + integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== + dependencies: + nopt "~1.0.10" + +tough-cookie@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4" + integrity sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg== + dependencies: + psl "^1.1.33" + punycode "^2.1.1" + universalify "^0.1.2" + +tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +triple-beam@^1.2.0, triple-beam@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" + integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== + +tslib@^1.10.0, tslib@^1.9.3: + version "1.13.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" + integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== + +tslib@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e" + integrity sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ== + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tunnel@^0.0.6, tunnel@~0.0.2: + version "0.0.6" + resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" + integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= + dependencies: + prelude-ls "~1.1.2" + +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + +typedarray-to-buffer@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" + +uid2@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.3.tgz#483126e11774df2f71b8b639dcd799c376162b82" + integrity sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I= + +undefsafe@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.3.tgz#6b166e7094ad46313b2202da7ecc2cd7cc6e7aae" + integrity sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A== + dependencies: + debug "^2.2.0" + +underscore@1.4.x: + version "1.4.4" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.4.4.tgz#61a6a32010622afa07963bf325203cf12239d604" + integrity sha1-YaajIBBiKvoHljvzJSA88SI51gQ= + +underscore@^1.8.3: + version "1.10.2" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.10.2.tgz#73d6aa3668f3188e4adb0f1943bd12cfd7efaaaf" + integrity sha512-N4P+Q/BuyuEKFJ43B9gYuOj4TQUHXX+j2FqguVOpjkssLUUrnJofCcBccJSCoeturDoZU6GorDTHSvUDlSQbTg== + +unique-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" + integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== + dependencies: + crypto-random-string "^2.0.0" + +unique-temp-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unique-temp-dir/-/unique-temp-dir-1.0.0.tgz#6dce95b2681ca003eebfb304a415f9cbabcc5385" + integrity sha1-bc6VsmgcoAPuv7MEpBX5y6vMU4U= + dependencies: + mkdirp "^0.5.1" + os-tmpdir "^1.0.1" + uid2 "0.0.3" + +universalify@^0.1.0, universalify@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +universalify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" + integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== + +update-notifier@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-4.1.1.tgz#895fc8562bbe666179500f9f2cebac4f26323746" + integrity sha512-9y+Kds0+LoLG6yN802wVXoIfxYEwh3FlZwzMwpCZp62S2i1/Jzeqb9Eeeju3NSHccGGasfGlK5/vEHbAifYRDg== + dependencies: + boxen "^4.2.0" + chalk "^3.0.0" + configstore "^5.0.1" + has-yarn "^2.1.0" + import-lazy "^2.1.0" + is-ci "^2.0.0" + is-installed-globally "^0.3.1" + is-npm "^4.0.0" + is-yarn-global "^0.3.0" + latest-version "^5.0.0" + pupa "^2.0.1" + semver-diff "^3.1.1" + xdg-basedir "^4.0.0" + +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + dependencies: + punycode "^2.1.0" + +url-parse-lax@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" + integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww= + dependencies: + prepend-http "^2.0.0" + +url-parse@~1.4.3: + version "1.4.7" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" + integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + +url@0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" + integrity sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ= + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +url@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +util@^0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" + integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ== + dependencies: + inherits "2.0.3" + +uuid-parse@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/uuid-parse/-/uuid-parse-1.1.0.tgz#7061c5a1384ae0e1f943c538094597e1b5f3a65b" + integrity sha512-OdmXxA8rDsQ7YpNVbKSJkNzTw2I+S5WsbMDnCtIWSQaosNAcWtFuI/YK1TjzUI6nbkgiqEyh8gWngfcv8Asd9A== + +uuid-random@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/uuid-random/-/uuid-random-1.3.2.tgz#96715edbaef4e84b1dcf5024b00d16f30220e2d0" + integrity sha512-UOzej0Le/UgkbWEO8flm+0y+G+ljUon1QWTEZOq1rnMAsxo2+SckbiZdKzAHHlVh6gJqI1TjC/xwgR50MuCrBQ== + +uuid@3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + +uuid@^3.3.2: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +uuid@^8.1.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea" + integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ== + +validator@^9.4.1: + version "9.4.1" + resolved "https://registry.yarnpkg.com/validator/-/validator-9.4.1.tgz#abf466d398b561cd243050112c6ff1de6cc12663" + integrity sha512-YV5KjzvRmSyJ1ee/Dm5UED0G+1L4GZnLN3w6/T+zZm8scVua4sOhYKWTUrKa0H/tMiJyO9QLHMPN+9mB/aMunA== + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +walkdir@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/walkdir/-/walkdir-0.4.1.tgz#dc119f83f4421df52e3061e514228a2db20afa39" + integrity sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ== + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= + +widest-line@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" + integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== + dependencies: + string-width "^4.0.0" + +winston-daily-rotate-file@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/winston-daily-rotate-file/-/winston-daily-rotate-file-4.5.0.tgz#3914ac57c4bdae1138170bec85af0c2217b253b1" + integrity sha512-/HqeWiU48dzGqcrABRlxYWVMdL6l3uKCtFSJyrqK+E2rLnSFNsgYpvwx15EgTitBLNzH69lQd/+z2ASryV2aqw== + dependencies: + file-stream-rotator "^0.5.7" + object-hash "^2.0.1" + triple-beam "^1.3.0" + winston-transport "^4.2.0" + +winston-transport@^4.2.0, winston-transport@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.4.0.tgz#17af518daa690d5b2ecccaa7acf7b20ca7925e59" + integrity sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw== + dependencies: + readable-stream "^2.3.7" + triple-beam "^1.2.0" + +winston@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/winston/-/winston-3.3.3.tgz#ae6172042cafb29786afa3d09c8ff833ab7c9170" + integrity sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw== + dependencies: + "@dabh/diagnostics" "^2.0.2" + async "^3.1.0" + is-stream "^2.0.0" + logform "^2.2.0" + one-time "^1.0.0" + readable-stream "^3.4.0" + stack-trace "0.0.x" + triple-beam "^1.3.0" + winston-transport "^4.4.0" + +wns@~0.5.3: + version "0.5.4" + resolved "https://registry.yarnpkg.com/wns/-/wns-0.5.4.tgz#ad8e2ee60e675557da9610d94444a7f59eceaf78" + integrity sha512-WYiJ7khIwUGBD5KAm+YYmwJDDRzFRs4YGAjtbFSoRIdbn9Jcix3p9khJmpvBTXGommaKkvduAn+pc9l4d9yzVQ== + +word-wrap@~1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +write-file-atomic@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" + integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== + dependencies: + imurmurhash "^0.1.4" + is-typedarray "^1.0.0" + signal-exit "^3.0.2" + typedarray-to-buffer "^3.1.5" + +xdg-basedir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" + integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== + +xml2js@0.4.19: + version "0.4.19" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" + integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== + dependencies: + sax ">=0.6.0" + xmlbuilder "~9.0.1" + +xml2js@^0.4.19: + version "0.4.23" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" + integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + +xmlbuilder@15.1.1: + version "15.1.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5" + integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg== + +xmlbuilder@~11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" + integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== + +xmlbuilder@~9.0.1: + version "9.0.7" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" + integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= + +y18n@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" + integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yargs-parser@^18.1.2: + version "18.1.3" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" + integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs@^15.3.1: + version "15.4.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" + integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== + dependencies: + cliui "^6.0.0" + decamelize "^1.2.0" + find-up "^4.1.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^4.2.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^18.1.2" diff --git a/msa/pom.xml b/msa/pom.xml index c627dca51a..7419141995 100644 --- a/msa/pom.xml +++ b/msa/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT thingsboard msa diff --git a/msa/tb-node/docker/Dockerfile b/msa/tb-node/docker/Dockerfile index b7a2dbf346..0b8c73fc1b 100644 --- a/msa/tb-node/docker/Dockerfile +++ b/msa/tb-node/docker/Dockerfile @@ -22,6 +22,7 @@ RUN chmod a+x /tmp/*.sh \ && mv /tmp/start-tb-node.sh /usr/bin RUN yes | dpkg -i /tmp/${pkg.name}.deb +RUN rm /tmp/${pkg.name}.deb RUN systemctl --no-reload disable --now ${pkg.name}.service > /dev/null 2>&1 || : diff --git a/msa/tb-node/pom.xml b/msa/tb-node/pom.xml index 944521f87e..2d42d8f6dd 100644 --- a/msa/tb-node/pom.xml +++ b/msa/tb-node/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/tb/README.md b/msa/tb/README.md index e4b7400b50..488ead2d85 100644 --- a/msa/tb/README.md +++ b/msa/tb/README.md @@ -4,10 +4,19 @@ This project provides the build for the ThingsBoard single docker images. * `thingsboard/tb` - single instance of ThingsBoard with embedded HSQLDB database. * `thingsboard/tb-postgres` - single instance of ThingsBoard with PostgreSQL database. -* `thingsboard/tb-cassandra` - single instance of ThingsBoard with Cassandra database. +* `thingsboard/tb-cassandra` - single instance of ThingsBoard with Hybrid PostgreSQL (entities) and Cassandra (timeseries) database. ## Running +Before starting Docker container run following command to create a directory for storing data and change its owner to docker container user. +To be able to change user, **chown** command is used, which requires sudo permissions (command will request password for a sudo access): + +` +$ mkdir -p ~/.mytb-data && sudo chown -R 799:799 ~/.mytb-data +` + +**NOTE**: replace directory `~/.mytb-data` with directory you're planning to use on container creation. + In this example `thingsboard/tb` image will be used. You can choose any other images with different databases (see above). Execute the following command to run this docker directly: @@ -28,7 +37,7 @@ Where: > **NOTE**: **Windows** users should use docker managed volume instead of host's dir. Create docker volume (for ex. `mytb-data`) before executing `docker run` command: > ``` -> $ docker create volume mytb-data +> $ docker volume create mytb-data > ``` > After you can execute docker run command using `mytb-data` volume instead of `~/.mytb-data`. > In order to get access to necessary resources from external IP/Host on **Windows** machine, please execute the following commands: diff --git a/msa/tb/docker-cassandra/Dockerfile b/msa/tb/docker-cassandra/Dockerfile index 3a761398fd..ebb52b0186 100644 --- a/msa/tb/docker-cassandra/Dockerfile +++ b/msa/tb/docker-cassandra/Dockerfile @@ -18,11 +18,15 @@ FROM thingsboard/openjdk8 RUN apt-get update RUN apt-get install -y curl nmap procps +RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null +RUN curl -L https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - RUN echo 'deb http://www.apache.org/dist/cassandra/debian 311x main' | tee --append /etc/apt/sources.list.d/cassandra.list > /dev/null RUN curl -L https://www.apache.org/dist/cassandra/KEYS | apt-key add - +ENV PG_MAJOR=11 RUN apt-get update -RUN apt-get install -y cassandra cassandra-tools +RUN apt-get install -y cassandra cassandra-tools postgresql-11 RUN update-rc.d cassandra disable +RUN update-rc.d postgresql disable RUN sed -i.old '/ulimit/d' /etc/init.d/cassandra 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/ @@ -35,6 +39,7 @@ RUN chmod a+x /tmp/*.sh \ && mv /tmp/stop-db.sh /usr/bin RUN dpkg -i /tmp/${pkg.name}.deb +RUN rm /tmp/${pkg.name}.deb RUN systemctl --no-reload disable --now ${pkg.name}.service > /dev/null 2>&1 || : @@ -45,10 +50,45 @@ ENV DATA_FOLDER=/data ENV HTTP_BIND_PORT=9090 ENV DATABASE_TS_TYPE=cassandra -ENV DATABASE_ENTITIES_TYPE=cassandra + +ENV PGDATA=/data/db +ENV CASSANDRA_DATA=/data/cassandra + +ENV SPRING_JPA_DATABASE_PLATFORM=org.hibernate.dialect.PostgreSQLDialect +ENV SPRING_DRIVER_CLASS_NAME=org.postgresql.Driver +ENV SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/thingsboard +ENV SPRING_DATASOURCE_USERNAME=${pkg.user} +ENV SPRING_DATASOURCE_PASSWORD=postgres ENV CASSANDRA_HOST=localhost ENV CASSANDRA_PORT=9042 +ENV CASSANDRA_HOME=/opt/cassandra + +ENV PATH=$CASSANDRA_HOME/bin:$PATH +ENV PATH=$PATH:/usr/lib/postgresql/$PG_MAJOR/bin + +ENV PGLOG=/var/log/postgres +ENV CASSANDRA_LOG=/var/log/cassandra + +# postgres config +RUN mkdir -p $PGLOG +RUN chown -R ${pkg.user}:${pkg.user} $PGLOG +RUN chown -R ${pkg.user}:${pkg.user} /var/log/postgresql +RUN chown -R ${pkg.user}:${pkg.user} /var/run/postgresql + +# cassandra config +RUN rm -rf /var/lib/cassandra +RUN chmod a+w /var/lib +RUN chown -R ${pkg.user}:${pkg.user} $CASSANDRA_LOG + + +RUN mkdir -p $DATA_FOLDER +RUN chown -R ${pkg.user}:${pkg.user} $DATA_FOLDER +RUN chown -R ${pkg.user}:${pkg.user} /var/log/${pkg.name} + +RUN chmod 555 ${pkg.installFolder}/bin/${pkg.name}.jar + +USER ${pkg.user} EXPOSE 9090 EXPOSE 1883 diff --git a/msa/tb/docker-cassandra/start-db.sh b/msa/tb/docker-cassandra/start-db.sh index d65edf71b1..fa80f6db57 100644 --- a/msa/tb/docker-cassandra/start-db.sh +++ b/msa/tb/docker-cassandra/start-db.sh @@ -15,21 +15,32 @@ # limitations under the License. # -cassandra_data_dir=${DATA_FOLDER}/db +firstlaunch=${DATA_FOLDER}/.firstlaunch + +PG_CTL=$(find /usr/lib/postgresql/ -name pg_ctl) + +if [ ! -d ${PGDATA} ]; then + mkdir -p ${PGDATA} + ${PG_CTL} initdb +fi + +exec setsid nohup postgres >> ${PGLOG}/postgres.log 2>&1 & + +if [ ! -f ${firstlaunch} ]; then + psql -U ${pkg.user} -d postgres -c "CREATE DATABASE thingsboard" +fi + +cassandra_data_dir=${CASSANDRA_DATA} cassandra_data_link=/var/lib/cassandra if [ ! -L ${cassandra_data_link} ]; then - if [ -d ${cassandra_data_link} ]; then - rm -rf ${cassandra_data_link} - fi if [ ! -d ${cassandra_data_dir} ]; then mkdir -p ${cassandra_data_dir} - chown -R cassandra:cassandra ${cassandra_data_dir} fi ln -s ${cassandra_data_dir} ${cassandra_data_link} fi -service cassandra start +exec setsid nohup cassandra >> ${CASSANDRA_LOG}/cassandra.log 2>&1 & until nmap $CASSANDRA_HOST -p $CASSANDRA_PORT | grep "$CASSANDRA_PORT/tcp open" do diff --git a/msa/tb/docker-cassandra/stop-db.sh b/msa/tb/docker-cassandra/stop-db.sh index 24cf570999..43630fc1d3 100644 --- a/msa/tb/docker-cassandra/stop-db.sh +++ b/msa/tb/docker-cassandra/stop-db.sh @@ -15,4 +15,18 @@ # limitations under the License. # -service cassandra stop +CASSANDRA_PID=$(ps aux | grep '[c]assandra' | awk '{print $2}') + +echo "Stopping cassandra (pid ${CASSANDRA_PID})." +kill -SIGTERM ${CASSANDRA_PID} + +PG_CTL=$(find /usr/lib/postgresql/ -name pg_ctl) +echo "Stopping postgres." +${PG_CTL} stop + +while [ -e /proc/${CASSANDRA_PID} ] +do + echo "Waiting for cassandra to stop." + sleep 2 +done +echo "Cassandra was stopped." diff --git a/msa/tb/docker-postgres/Dockerfile b/msa/tb/docker-postgres/Dockerfile index a2a24442e4..0c4ef5ed68 100644 --- a/msa/tb/docker-postgres/Dockerfile +++ b/msa/tb/docker-postgres/Dockerfile @@ -20,13 +20,11 @@ RUN apt-get update RUN apt-get install -y curl RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null RUN curl -L https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - +ENV PG_MAJOR 11 RUN apt-get update RUN apt-get install -y postgresql-11 RUN update-rc.d postgresql disable -RUN mkdir -p /var/log/postgres -RUN chown -R postgres:postgres /var/log/postgres - 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/ RUN chmod a+x /tmp/*.sh \ @@ -37,6 +35,7 @@ RUN chmod a+x /tmp/*.sh \ && mv /tmp/stop-db.sh /usr/bin RUN dpkg -i /tmp/${pkg.name}.deb +RUN rm /tmp/${pkg.name}.deb RUN systemctl --no-reload disable --now ${pkg.name}.service > /dev/null 2>&1 || : @@ -47,16 +46,31 @@ ENV DATA_FOLDER=/data ENV HTTP_BIND_PORT=9090 ENV DATABASE_TS_TYPE=sql -ENV DATABASE_ENTITIES_TYPE=sql ENV PGDATA=/data/db +ENV PATH=$PATH:/usr/lib/postgresql/$PG_MAJOR/bin ENV SPRING_JPA_DATABASE_PLATFORM=org.hibernate.dialect.PostgreSQLDialect ENV SPRING_DRIVER_CLASS_NAME=org.postgresql.Driver ENV SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/thingsboard -ENV SPRING_DATASOURCE_USERNAME=postgres +ENV SPRING_DATASOURCE_USERNAME=${pkg.user} ENV SPRING_DATASOURCE_PASSWORD=postgres + +ENV PGLOG=/var/log/postgres +RUN mkdir -p $PGLOG +RUN chown -R ${pkg.user}:${pkg.user} $PGLOG + +RUN chown -R ${pkg.user}:${pkg.user} /var/run/postgresql + +RUN mkdir -p /data +RUN chown -R ${pkg.user}:${pkg.user} /data + +RUN chown -R ${pkg.user}:${pkg.user} /var/log/${pkg.name} +RUN chmod 555 ${pkg.installFolder}/bin/${pkg.name}.jar + +USER ${pkg.user} + EXPOSE 9090 EXPOSE 1883 EXPOSE 5683/udp diff --git a/msa/tb/docker-postgres/start-db.sh b/msa/tb/docker-postgres/start-db.sh index dfbfc1dd68..6119daf2f9 100644 --- a/msa/tb/docker-postgres/start-db.sh +++ b/msa/tb/docker-postgres/start-db.sh @@ -17,16 +17,15 @@ firstlaunch=${DATA_FOLDER}/.firstlaunch -export PG_CTL=$(find /usr/lib/postgresql/ -name pg_ctl) +PG_CTL=$(find /usr/lib/postgresql/ -name pg_ctl) if [ ! -d ${PGDATA} ]; then mkdir -p ${PGDATA} - chown -R postgres:postgres ${PGDATA} - su postgres -c '${PG_CTL} initdb -U postgres' + ${PG_CTL} initdb fi -su postgres -c '${PG_CTL} -l /var/log/postgres/postgres.log -w start' +exec setsid nohup postgres >> ${PGLOG}/postgres.log 2>&1 & if [ ! -f ${firstlaunch} ]; then - su postgres -c 'psql -U postgres -d postgres -c "CREATE DATABASE thingsboard"' + psql -U ${pkg.user} -d postgres -c "CREATE DATABASE thingsboard" fi diff --git a/msa/tb/docker-postgres/stop-db.sh b/msa/tb/docker-postgres/stop-db.sh index 66596d13c8..b3d151dd16 100644 --- a/msa/tb/docker-postgres/stop-db.sh +++ b/msa/tb/docker-postgres/stop-db.sh @@ -15,6 +15,6 @@ # limitations under the License. # -export PG_CTL=$(find /usr/lib/postgresql/ -name pg_ctl) +PG_CTL=$(find /usr/lib/postgresql/ -name pg_ctl) -su postgres -c '${PG_CTL} stop' +${PG_CTL} stop diff --git a/msa/tb/docker-tb/Dockerfile b/msa/tb/docker-tb/Dockerfile index 3f9b26bec4..6e7d2a294a 100644 --- a/msa/tb/docker-tb/Dockerfile +++ b/msa/tb/docker-tb/Dockerfile @@ -26,6 +26,7 @@ RUN chmod a+x /tmp/*.sh \ && mv /tmp/stop-db.sh /usr/bin RUN dpkg -i /tmp/${pkg.name}.deb +RUN rm /tmp/${pkg.name}.deb RUN systemctl --no-reload disable --now ${pkg.name}.service > /dev/null 2>&1 || : @@ -36,7 +37,6 @@ ENV DATA_FOLDER=/data ENV HTTP_BIND_PORT=9090 ENV DATABASE_TS_TYPE=sql -ENV DATABASE_ENTITIES_TYPE=sql ENV SPRING_JPA_DATABASE_PLATFORM=org.hibernate.dialect.HSQLDialect ENV SPRING_DRIVER_CLASS_NAME=org.hsqldb.jdbc.JDBCDriver @@ -44,6 +44,13 @@ ENV SPRING_DATASOURCE_URL=jdbc:hsqldb:file:/data/db/thingsboardDb;sql.enforce_si ENV SPRING_DATASOURCE_USERNAME=sa ENV SPRING_DATASOURCE_PASSWORD= +RUN mkdir -p /data +RUN chown -R ${pkg.user}:${pkg.user} /data + +RUN chmod 555 ${pkg.installFolder}/bin/${pkg.name}.jar + +USER ${pkg.user} + EXPOSE 9090 EXPOSE 1883 EXPOSE 5683/udp diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml index 349cc36337..f41a3afd1e 100644 --- a/msa/tb/pom.xml +++ b/msa/tb/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/transport/coap/docker/Dockerfile b/msa/transport/coap/docker/Dockerfile index ad92c10d42..05c120af29 100644 --- a/msa/transport/coap/docker/Dockerfile +++ b/msa/transport/coap/docker/Dockerfile @@ -22,6 +22,7 @@ RUN chmod a+x /tmp/*.sh \ && mv /tmp/start-tb-coap-transport.sh /usr/bin RUN yes | dpkg -i /tmp/${pkg.name}.deb +RUN rm /tmp/${pkg.name}.deb RUN systemctl --no-reload disable --now ${pkg.name}.service > /dev/null 2>&1 || : diff --git a/msa/transport/coap/pom.xml b/msa/transport/coap/pom.xml index 3e956736a1..0b3ab420e8 100644 --- a/msa/transport/coap/pom.xml +++ b/msa/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/http/docker/Dockerfile b/msa/transport/http/docker/Dockerfile index a8257e49a7..db551e536b 100644 --- a/msa/transport/http/docker/Dockerfile +++ b/msa/transport/http/docker/Dockerfile @@ -22,6 +22,7 @@ RUN chmod a+x /tmp/*.sh \ && mv /tmp/start-tb-http-transport.sh /usr/bin RUN yes | dpkg -i /tmp/${pkg.name}.deb +RUN rm /tmp/${pkg.name}.deb RUN systemctl --no-reload disable --now ${pkg.name}.service > /dev/null 2>&1 || : diff --git a/msa/transport/http/pom.xml b/msa/transport/http/pom.xml index bc8ccc3f1f..5621fd6bdc 100644 --- a/msa/transport/http/pom.xml +++ b/msa/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/mqtt/docker/Dockerfile b/msa/transport/mqtt/docker/Dockerfile index c2ce4f5629..55aeff2211 100644 --- a/msa/transport/mqtt/docker/Dockerfile +++ b/msa/transport/mqtt/docker/Dockerfile @@ -22,6 +22,7 @@ RUN chmod a+x /tmp/*.sh \ && mv /tmp/start-tb-mqtt-transport.sh /usr/bin RUN yes | dpkg -i /tmp/${pkg.name}.deb +RUN rm /tmp/${pkg.name}.deb RUN systemctl --no-reload disable --now ${pkg.name}.service > /dev/null 2>&1 || : diff --git a/msa/transport/mqtt/pom.xml b/msa/transport/mqtt/pom.xml index b2bc3ff8d9..24f8350706 100644 --- a/msa/transport/mqtt/pom.xml +++ b/msa/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/pom.xml b/msa/transport/pom.xml index d00683c492..e000241608 100644 --- a/msa/transport/pom.xml +++ b/msa/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/web-ui/docker/Dockerfile b/msa/web-ui/docker/Dockerfile index f31504d3dc..2508365d48 100644 --- a/msa/web-ui/docker/Dockerfile +++ b/msa/web-ui/docker/Dockerfile @@ -22,6 +22,7 @@ RUN chmod a+x /tmp/*.sh \ && mv /tmp/start-web-ui.sh /usr/bin RUN yes | dpkg -i /tmp/${pkg.name}.deb +RUN rm /tmp/${pkg.name}.deb RUN systemctl --no-reload disable --now ${pkg.name}.service > /dev/null 2>&1 || : diff --git a/msa/web-ui/package-lock.json b/msa/web-ui/package-lock.json deleted file mode 100644 index b7785ef48b..0000000000 --- a/msa/web-ui/package-lock.json +++ /dev/null @@ -1,4055 +0,0 @@ -{ - "name": "thingsboard-web-ui", - "version": "2.5.3", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@babel/parser": { - "version": "7.4.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.4.5.tgz", - "integrity": "sha512-9mUqkL1FF5T7f0WDFfAoDdiMVPWsdD1gZYzSnaXsxUCUqzuch/8of9G3VUSNiZmMBoRxT3neyVsqeiL/ZPcjew==", - "dev": true - }, - "@babel/runtime": { - "version": "7.4.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.5.tgz", - "integrity": "sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.13.2" - } - }, - "@mrmlnc/readdir-enhanced": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", - "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", - "dev": true, - "requires": { - "call-me-maybe": "^1.0.1", - "glob-to-regexp": "^0.3.0" - } - }, - "@nodelib/fs.stat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", - "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", - "dev": true - }, - "@types/events": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", - "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", - "dev": true - }, - "@types/glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", - "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", - "dev": true, - "requires": { - "@types/events": "*", - "@types/minimatch": "*", - "@types/node": "*" - } - }, - "@types/minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", - "dev": true - }, - "@types/node": { - "version": "12.6.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.6.8.tgz", - "integrity": "sha512-aX+gFgA5GHcDi89KG5keey2zf0WfZk/HAQotEamsK2kbey+8yGKcson0hbK8E+v0NArlCJQCqMP161YhV6ZXLg==", - "dev": true - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "accepts": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", - "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", - "requires": { - "mime-types": "~2.1.18", - "negotiator": "0.6.1" - } - }, - "ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", - "dev": true, - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-align": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", - "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", - "dev": true, - "requires": { - "string-width": "^2.0.0" - } - }, - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "dev": true, - "requires": { - "array-uniq": "^1.0.1" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dev": true, - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true - }, - "async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", - "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", - "requires": { - "lodash": "^4.17.10" - } - }, - "async-each": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", - "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "binary-extensions": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.12.0.tgz", - "integrity": "sha512-DYWGk01lDcxeS/K9IHPGWfT8PsJmbXRtRd2Sx72Tnb8pcYZQFF1oSDb8hJtS1vhp212q1Rzi5dUf9+nq0o9UIg==", - "dev": true - }, - "body-parser": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", - "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", - "requires": { - "bytes": "3.0.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "~1.6.3", - "iconv-lite": "0.4.23", - "on-finished": "~2.3.0", - "qs": "6.5.2", - "raw-body": "2.3.3", - "type-is": "~1.6.16" - } - }, - "boxen": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", - "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", - "dev": true, - "requires": { - "ansi-align": "^2.0.0", - "camelcase": "^4.0.0", - "chalk": "^2.0.1", - "cli-boxes": "^1.0.0", - "string-width": "^2.0.0", - "term-size": "^1.2.0", - "widest-line": "^2.0.0" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "byline": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", - "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=", - "dev": true - }, - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - } - }, - "call-me-maybe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", - "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", - "dev": true - }, - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", - "dev": true - }, - "capture-stack-trace": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz", - "integrity": "sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==", - "dev": true - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "chokidar": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", - "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.0", - "braces": "^2.3.0", - "fsevents": "^1.2.2", - "glob-parent": "^3.1.0", - "inherits": "^2.0.1", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "lodash.debounce": "^4.0.8", - "normalize-path": "^2.1.1", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.0.0", - "upath": "^1.0.5" - } - }, - "ci-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", - "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", - "dev": true - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "cli-boxes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", - "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", - "dev": true - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", - "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", - "requires": { - "color-convert": "^1.9.1", - "color-string": "^1.5.2" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "color-string": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", - "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", - "requires": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "colornames": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/colornames/-/colornames-1.1.1.tgz", - "integrity": "sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y=" - }, - "colors": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.2.tgz", - "integrity": "sha512-rhP0JSBGYvpcNQj4s5AdShMeE5ahMop96cTeDl/v9qQQm2fYClE2QXZRi8wLzc+GmXSxdIqqbOIAhyObEXDbfQ==" - }, - "colorspace": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.1.tgz", - "integrity": "sha512-pI3btWyiuz7Ken0BWh9Elzsmv2bM9AhA7psXib4anUXy/orfZ/E0MbQwhSOG/9L8hLlalqrU0UhOuqxW1YjmVw==", - "requires": { - "color": "3.0.x", - "text-hex": "1.0.x" - } - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "compressible": { - "version": "2.0.15", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.15.tgz", - "integrity": "sha512-4aE67DL33dSW9gw4CI2H/yTxqHLNcxp0yS6jB+4h+wr3e43+1z7vm0HU9qXOH8j+qjKuL8+UtkOxYQSMq60Ylw==", - "requires": { - "mime-db": ">= 1.36.0 < 2" - } - }, - "compression": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.3.tgz", - "integrity": "sha512-HSjyBG5N1Nnz7tF2+O7A9XUhyjru71/fwgNb7oIsEVHR0WShfs2tIS/EySLgiTe98aOK18YDlMXpzjCXY/n9mg==", - "requires": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.14", - "debug": "2.6.9", - "on-headers": "~1.0.1", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "config": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/config/-/config-3.2.2.tgz", - "integrity": "sha512-rOsfIOAcG82AWouK4/vBS/OKz3UPl2T/kP0irExmXJJOoWg2CmdfPLdx56bCoMUMFNh+7soQkQWCUC8DyemiwQ==", - "requires": { - "json5": "^1.0.1" - } - }, - "configstore": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", - "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", - "dev": true, - "requires": { - "dot-prop": "^4.1.0", - "graceful-fs": "^4.1.2", - "make-dir": "^1.0.0", - "unique-string": "^1.0.0", - "write-file-atomic": "^2.0.0", - "xdg-basedir": "^3.0.0" - } - }, - "connect-history-api-fallback": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz", - "integrity": "sha1-sGhzk0vF40T+9hGhlqb6rgruAVo=" - }, - "content-disposition": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" - }, - "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "create-error-class": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", - "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", - "dev": true, - "requires": { - "capture-stack-trace": "^1.0.0" - } - }, - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "crypto-random-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", - "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", - "dev": true - }, - "cycle": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", - "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=" - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "diagnostics": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz", - "integrity": "sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ==", - "requires": { - "colorspace": "1.1.x", - "enabled": "1.0.x", - "kuler": "1.0.x" - } - }, - "dir-glob": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", - "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", - "dev": true, - "requires": { - "path-type": "^3.0.0" - } - }, - "dot-prop": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", - "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", - "dev": true, - "requires": { - "is-obj": "^1.0.0" - } - }, - "duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", - "dev": true - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dev": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "enabled": { - "version": "1.0.2", - "resolved": "http://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz", - "integrity": "sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M=", - "requires": { - "env-variable": "0.0.x" - } - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" - }, - "env-variable": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.5.tgz", - "integrity": "sha512-zoB603vQReOFvTg5xMl9I1P2PnHsHQQKTEowsKKD7nseUfJq6UWzK+4YtlWUO1nhiQUxe6XMkk+JleSZD1NZFA==" - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "escodegen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.1.tgz", - "integrity": "sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw==", - "dev": true, - "requires": { - "esprima": "^3.1.3", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - }, - "dependencies": { - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true - } - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" - }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", - "dev": true - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" - }, - "eventemitter3": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz", - "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==" - }, - "execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", - "dev": true, - "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true - }, - "express": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", - "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", - "requires": { - "accepts": "~1.3.5", - "array-flatten": "1.1.1", - "body-parser": "1.18.3", - "content-disposition": "0.5.2", - "content-type": "~1.0.4", - "cookie": "0.3.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.1.1", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.4", - "qs": "6.5.2", - "range-parser": "~1.2.0", - "safe-buffer": "5.1.2", - "send": "0.16.2", - "serve-static": "1.13.2", - "setprototypeof": "1.1.0", - "statuses": "~1.4.0", - "type-is": "~1.6.16", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true - }, - "fast-glob": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", - "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==", - "dev": true, - "requires": { - "@mrmlnc/readdir-enhanced": "^2.2.1", - "@nodelib/fs.stat": "^1.1.2", - "glob-parent": "^3.1.0", - "is-glob": "^4.0.0", - "merge2": "^1.2.3", - "micromatch": "^3.1.10" - } - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "fast-safe-stringify": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.6.tgz", - "integrity": "sha512-q8BZ89jjc+mz08rSxROs8VsrBBcn1SIw1kq9NjolL509tkABRk9io01RAjSaEv1Xb2uFLt8VtRiZbGp5H8iDtg==" - }, - "fecha": { - "version": "2.3.3", - "resolved": "http://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz", - "integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==" - }, - "file-stream-rotator": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.4.1.tgz", - "integrity": "sha512-W3aa3QJEc8BS2MmdVpQiYLKHj3ijpto1gMDlsgCRSKfIUe6MwkcpODGPQ3vZfb0XvCeCqlu9CBQTN7oQri2TZQ==", - "requires": { - "moment": "^2.11.2" - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "finalhandler": { - "version": "1.1.1", - "resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", - "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "statuses": "~1.4.0", - "unpipe": "~1.0.0" - } - }, - "follow-redirects": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", - "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", - "requires": { - "debug": "=3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - } - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "requires": { - "map-cache": "^0.2.2" - } - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" - }, - "from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - } - }, - "fs-extra": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz", - "integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", - "dev": true, - "optional": true, - "requires": { - "nan": "^2.9.2", - "node-pre-gyp": "^0.10.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.21", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": "^2.1.0" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, - "minipass": { - "version": "2.2.4", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "^5.1.1", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.2.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^2.1.2", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.10.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.0", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.1.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.1.10", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.5.1", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.0.5" - } - }, - "safe-buffer": { - "version": "5.1.1", - "bundled": true, - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.5.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.0.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.2.4", - "minizlib": "^1.1.0", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.1", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "yallist": { - "version": "3.0.2", - "bundled": true, - "dev": true - } - } - }, - "get-stream": { - "version": "3.0.0", - "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "glob-to-regexp": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", - "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=", - "dev": true - }, - "global-dirs": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", - "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", - "dev": true, - "requires": { - "ini": "^1.3.4" - } - }, - "globby": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-9.2.0.tgz", - "integrity": "sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==", - "dev": true, - "requires": { - "@types/glob": "^7.1.1", - "array-union": "^1.0.2", - "dir-glob": "^2.2.2", - "fast-glob": "^2.2.6", - "glob": "^7.1.3", - "ignore": "^4.0.3", - "pify": "^4.0.1", - "slash": "^2.0.0" - }, - "dependencies": { - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - } - } - }, - "got": { - "version": "6.7.1", - "resolved": "http://registry.npmjs.org/got/-/got-6.7.1.tgz", - "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", - "dev": true, - "requires": { - "create-error-class": "^3.0.0", - "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "is-redirect": "^1.0.0", - "is-retry-allowed": "^1.0.0", - "is-stream": "^1.0.0", - "lowercase-keys": "^1.0.0", - "safe-buffer": "^5.0.1", - "timed-out": "^4.0.0", - "unzip-response": "^2.0.1", - "url-parse-lax": "^1.0.0" - } - }, - "graceful-fs": { - "version": "4.1.15", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", - "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", - "dev": true - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true - }, - "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "dev": true, - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "http": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/http/-/http-0.0.0.tgz", - "integrity": "sha1-huYybSnF0Dnen6xYSkVon5KfT3I=" - }, - "http-errors": { - "version": "1.6.3", - "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - } - }, - "http-proxy": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz", - "integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==", - "requires": { - "eventemitter3": "^3.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "iconv-lite": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", - "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true - }, - "ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", - "dev": true - }, - "import-lazy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", - "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", - "dev": true - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "dev": true - }, - "into-stream": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-5.1.0.tgz", - "integrity": "sha512-cbDhb8qlxKMxPBk/QxTtYg1DQ4CwXmadu7quG3B7nrJsgSncEreF2kwWKZFdnjc/lSNNIkFPsjI7SM0Cx/QXPw==", - "dev": true, - "requires": { - "from2": "^2.3.0", - "p-is-promise": "^2.0.0" - } - }, - "ipaddr.js": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", - "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "dev": true, - "requires": { - "binary-extensions": "^1.0.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-ci": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", - "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", - "dev": true, - "requires": { - "ci-info": "^1.5.0" - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "is-glob": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", - "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-installed-globally": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", - "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", - "dev": true, - "requires": { - "global-dirs": "^0.1.0", - "is-path-inside": "^1.0.0" - } - }, - "is-npm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", - "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", - "dev": true - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-obj": { - "version": "1.0.1", - "resolved": "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", - "dev": true - }, - "is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", - "dev": true, - "requires": { - "path-is-inside": "^1.0.1" - } - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "is-redirect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", - "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", - "dev": true - }, - "is-retry-allowed": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", - "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=", - "dev": true - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, - "js-yaml": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", - "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "requires": { - "minimist": "^1.2.0" - } - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "kuler": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz", - "integrity": "sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ==", - "requires": { - "colornames": "^1.1.1" - } - }, - "latest-version": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", - "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", - "dev": true, - "requires": { - "package-json": "^4.0.0" - } - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" - }, - "lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", - "dev": true - }, - "logform": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-1.10.0.tgz", - "integrity": "sha512-em5ojIhU18fIMOw/333mD+ZLE2fis0EzXl1ZwHx4iQzmpQi6odNiY/t+ITNr33JZhT9/KEaH+UPIipr6a9EjWg==", - "requires": { - "colors": "^1.2.1", - "fast-safe-stringify": "^2.0.4", - "fecha": "^2.3.3", - "ms": "^2.1.1", - "triple-beam": "^1.2.0" - }, - "dependencies": { - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - } - } - }, - "lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", - "dev": true - }, - "lru-cache": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.4.tgz", - "integrity": "sha512-EPstzZ23znHUVLKj+lcXO1KvZkrlw+ZirdwvOmnAnA/1PB4ggyXJ77LRkCqkff+ShQ+cqoxCxLQOh4cKITO5iA==", - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^3.0.2" - } - }, - "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "requires": { - "object-visit": "^1.0.0" - } - }, - "media-typer": { - "version": "0.3.0", - "resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" - }, - "merge2": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.2.3.tgz", - "integrity": "sha512-gdUU1Fwj5ep4kplwcmftruWofEFt6lfpkkr3h860CXbAB9c3hGb55EOL2ali0Td5oebvW0E1+3Sr+Ur7XfKpRA==", - "dev": true - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" - }, - "mime-db": { - "version": "1.37.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", - "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==" - }, - "mime-types": { - "version": "2.1.21", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", - "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", - "requires": { - "mime-db": "~1.37.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - }, - "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", - "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - } - } - }, - "moment": { - "version": "2.22.2", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", - "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=" - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "multistream": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/multistream/-/multistream-2.1.1.tgz", - "integrity": "sha512-xasv76hl6nr1dEy3lPvy7Ej7K/Lx3O/FCvwge8PeVJpciPPoNCbaANcNiBug3IpdvTveZUcAV0DJzdnUDMesNQ==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.5" - } - }, - "nan": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.1.tgz", - "integrity": "sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==", - "dev": true, - "optional": true - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - } - }, - "negotiator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" - }, - "nodemon": { - "version": "1.18.7", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.18.7.tgz", - "integrity": "sha512-xuC1V0F5EcEyKQ1VhHYD13owznQbUw29JKvZ8bVH7TmuvVNHvvbp9pLgE4PjTMRJVe0pJ8fGRvwR2nMiosIsPQ==", - "dev": true, - "requires": { - "chokidar": "^2.0.4", - "debug": "^3.1.0", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.0.4", - "pstree.remy": "^1.1.2", - "semver": "^5.5.0", - "supports-color": "^5.2.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.2", - "update-notifier": "^2.3.0" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - } - } - }, - "nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", - "dev": true, - "requires": { - "abbrev": "1" - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "object-hash": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz", - "integrity": "sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==" - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "requires": { - "isobject": "^3.0.0" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "1.1.1" - } - }, - "on-headers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz", - "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "one-time": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz", - "integrity": "sha1-+M33eISCb+Tf+T46nMN7HkSAdC4=" - }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, - "p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true - }, - "package-json": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", - "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", - "dev": true, - "requires": { - "got": "^6.7.1", - "registry-auth-token": "^3.0.1", - "registry-url": "^3.0.3", - "semver": "^5.1.0" - } - }, - "parseurl": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", - "dev": true - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" - }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - }, - "pkg": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/pkg/-/pkg-4.4.0.tgz", - "integrity": "sha512-bFNJ3v56QwqB6JtAl/YrczlmEKBPBVJ3n5nW905kgvG1ex9DajODpTs0kLAFxyLwoubDQux/RPJFL6WrnD/vpg==", - "dev": true, - "requires": { - "@babel/parser": "~7.4.4", - "@babel/runtime": "~7.4.4", - "chalk": "~2.4.2", - "escodegen": "~1.11.1", - "fs-extra": "~7.0.1", - "globby": "~9.2.0", - "into-stream": "~5.1.0", - "minimist": "~1.2.0", - "multistream": "~2.1.1", - "pkg-fetch": "~2.6.2", - "progress": "~2.0.3", - "resolve": "1.6.0", - "stream-meter": "~1.0.4" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - } - } - }, - "pkg-fetch": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-2.6.2.tgz", - "integrity": "sha512-7DN6YYP1Kct02mSkhfblK0HkunJ7BJjGBkSkFdIW/QKIovtAMaICidS7feX+mHfnZ98OP7xFJvBluVURlrHJxA==", - "dev": true, - "requires": { - "@babel/runtime": "~7.4.4", - "byline": "~5.0.0", - "chalk": "~2.4.1", - "expand-template": "~2.0.3", - "fs-extra": "~7.0.1", - "minimist": "~1.2.0", - "progress": "~2.0.0", - "request": "~2.88.0", - "request-progress": "~3.0.0", - "semver": "~6.0.0", - "unique-temp-dir": "~1.0.0" - }, - "dependencies": { - "fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "semver": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.0.0.tgz", - "integrity": "sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ==", - "dev": true - } - } - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" - }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true - }, - "proxy-addr": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", - "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", - "requires": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.8.0" - } - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true - }, - "psl": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.2.0.tgz", - "integrity": "sha512-GEn74ZffufCmkDDLNcl3uuyF/aSD6exEyh1v/ZSdAomB82t6G9hzJVRx0jBmLDW+VfZqks3aScmMw9DszwUalA==", - "dev": true - }, - "pstree.remy": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.2.tgz", - "integrity": "sha512-vL6NLxNHzkNTjGJUpMm5PLC+94/0tTlC1vkP9bdU0pOHih+EujMjgMTwfZopZvHWRFbqJ5Y73OMoau50PewDDA==", - "dev": true - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" - }, - "raw-body": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", - "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", - "requires": { - "bytes": "3.0.0", - "http-errors": "1.6.3", - "iconv-lite": "0.4.23", - "unpipe": "1.0.0" - } - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - } - }, - "regenerator-runtime": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", - "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==", - "dev": true - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, - "registry-auth-token": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", - "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", - "dev": true, - "requires": { - "rc": "^1.1.6", - "safe-buffer": "^5.0.1" - } - }, - "registry-url": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", - "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", - "dev": true, - "requires": { - "rc": "^1.0.1" - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } - }, - "request-progress": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", - "integrity": "sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=", - "dev": true, - "requires": { - "throttleit": "^1.0.0" - } - }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" - }, - "resolve": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.6.0.tgz", - "integrity": "sha512-mw7JQNu5ExIkcw4LPih0owX/TZXjD/ZUF/ZQ/pDnkw3ZKhDcZZw5klmBlj6gVMwjQ3Pz5Jgu7F3d0jcDVuEWdw==", - "dev": true, - "requires": { - "path-parse": "^1.0.5" - } - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "semver": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", - "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" - }, - "semver-diff": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", - "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", - "dev": true, - "requires": { - "semver": "^5.0.3" - } - }, - "send": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", - "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.6.2", - "mime": "1.4.1", - "ms": "2.0.0", - "on-finished": "~2.3.0", - "range-parser": "~1.2.0", - "statuses": "~1.4.0" - } - }, - "serve-static": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", - "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.2", - "send": "0.16.2" - } - }, - "set-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true - }, - "simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", - "requires": { - "is-arrayish": "^0.3.1" - } - }, - "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - }, - "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "dev": true, - "requires": { - "atob": "^2.1.1", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "dev": true - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" - }, - "stream-meter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/stream-meter/-/stream-meter-1.0.4.tgz", - "integrity": "sha1-Uq+Vql6nYKJJFxZwTb/5D3Ov3R0=", - "dev": true, - "requires": { - "readable-stream": "^2.1.4" - } - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "term-size": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", - "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", - "dev": true, - "requires": { - "execa": "^0.7.0" - } - }, - "text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" - }, - "throttleit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", - "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=", - "dev": true - }, - "timed-out": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", - "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", - "dev": true - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - }, - "touch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", - "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", - "dev": true, - "requires": { - "nopt": "~1.0.10" - } - }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "dev": true, - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - } - } - }, - "triple-beam": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", - "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "type-is": { - "version": "1.6.16", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", - "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.18" - } - }, - "uid2": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", - "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=", - "dev": true - }, - "undefsafe": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.2.tgz", - "integrity": "sha1-Il9rngM3Zj4Njnz9aG/Cg2zKznY=", - "dev": true, - "requires": { - "debug": "^2.2.0" - } - }, - "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^0.4.3" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.1", - "to-object-path": "^0.3.0" - } - } - } - }, - "unique-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", - "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", - "dev": true, - "requires": { - "crypto-random-string": "^1.0.0" - } - }, - "unique-temp-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unique-temp-dir/-/unique-temp-dir-1.0.0.tgz", - "integrity": "sha1-bc6VsmgcoAPuv7MEpBX5y6vMU4U=", - "dev": true, - "requires": { - "mkdirp": "^0.5.1", - "os-tmpdir": "^1.0.1", - "uid2": "0.0.3" - } - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true - } - } - }, - "unzip-response": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", - "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", - "dev": true - }, - "upath": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", - "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==", - "dev": true - }, - "update-notifier": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", - "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", - "dev": true, - "requires": { - "boxen": "^1.2.1", - "chalk": "^2.0.1", - "configstore": "^3.0.0", - "import-lazy": "^2.1.0", - "is-ci": "^1.0.10", - "is-installed-globally": "^0.1.0", - "is-npm": "^1.0.0", - "latest-version": "^3.0.0", - "semver-diff": "^2.0.0", - "xdg-basedir": "^3.0.0" - } - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, - "url-parse-lax": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", - "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", - "dev": true, - "requires": { - "prepend-http": "^1.0.1" - } - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", - "dev": true - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "widest-line": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", - "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", - "dev": true, - "requires": { - "string-width": "^2.1.1" - } - }, - "winston": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.1.0.tgz", - "integrity": "sha512-FsQfEE+8YIEeuZEYhHDk5cILo1HOcWkGwvoidLrDgPog0r4bser1lEIOco2dN9zpDJ1M88hfDgZvxe5z4xNcwg==", - "requires": { - "async": "^2.6.0", - "diagnostics": "^1.1.1", - "is-stream": "^1.1.0", - "logform": "^1.9.1", - "one-time": "0.0.4", - "readable-stream": "^2.3.6", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.2.0" - } - }, - "winston-compat": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/winston-compat/-/winston-compat-0.1.4.tgz", - "integrity": "sha512-mMEfFsSm6GmkFF+f4/0UJtG4N1vSaczGmXLVJYmS/+u2zUaIPcw2ZRuwUg2TvVBjswgiraN+vNnAG8z4fRUZ4w==", - "requires": { - "cycle": "~1.0.3", - "logform": "^1.6.0", - "triple-beam": "^1.2.0" - } - }, - "winston-daily-rotate-file": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-3.5.1.tgz", - "integrity": "sha512-Y5CECbcJro55HWcWJSzI1DiQrbrfwwvKHdCCJn9wWsWCGfnCPDl5SWIokS2M0EvOKtbZUrlm5DPG522mvjdUBQ==", - "requires": { - "file-stream-rotator": "^0.4.1", - "object-hash": "^1.3.0", - "semver": "^5.6.0", - "triple-beam": "^1.3.0", - "winston-compat": "^0.1.4", - "winston-transport": "^4.2.0" - } - }, - "winston-transport": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.2.0.tgz", - "integrity": "sha512-0R1bvFqxSlK/ZKTH86nymOuKv/cT1PQBMuDdA7k7f0S9fM44dNH6bXnuxwXPrN8lefJgtZq08BKdyZ0DZIy/rg==", - "requires": { - "readable-stream": "^2.3.6", - "triple-beam": "^1.2.0" - } - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "write-file-atomic": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", - "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - }, - "xdg-basedir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", - "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", - "dev": true - }, - "yallist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", - "dev": true - } - } -} diff --git a/msa/web-ui/package.json b/msa/web-ui/package.json index 2eec7556f9..d839f97a33 100644 --- a/msa/web-ui/package.json +++ b/msa/web-ui/package.json @@ -1,29 +1,29 @@ { "name": "thingsboard-web-ui", "private": true, - "version": "2.5.5", + "version": "3.2.0", "description": "ThingsBoard Web UI Microservice", "main": "server.js", "bin": "server.js", "scripts": { - "install": "pkg -t node10-linux-x64,node10-win-x64 --out-path ./target . && node install.js", + "install": "pkg -t node12-linux-x64,node12-win-x64 --out-path ./target . && node install.js", "test": "echo \"Error: no test specified\" && exit 1", "start": "WEB_FOLDER=./target/web nodemon server.js", "start-prod": "NODE_ENV=production nodemon server.js" }, "dependencies": { - "compression": "^1.7.3", - "config": "^3.2.2", - "connect-history-api-fallback": "^1.5.0", - "express": "^4.16.3", + "compression": "^1.7.4", + "config": "^3.3.1", + "connect-history-api-fallback": "^1.6.0", + "express": "^4.17.1", "http": "0.0.0", - "http-proxy": "^1.17.0", - "js-yaml": "^3.12.0", - "winston": "^3.0.0", - "winston-daily-rotate-file": "^3.2.1" + "http-proxy": "^1.18.1", + "js-yaml": "^3.14.0", + "winston": "^3.3.3", + "winston-daily-rotate-file": "^4.5.0" }, "engines": { - "node": ">=8.0.0 <11.0.0" + "node": ">=12.0.0 <14.0.0" }, "nyc": { "exclude": [ @@ -34,9 +34,9 @@ ] }, "devDependencies": { - "fs-extra": "^6.0.1", - "nodemon": "^1.17.5", - "pkg": "^4.4.0" + "fs-extra": "^9.0.1", + "nodemon": "^2.0.4", + "pkg": "^4.4.9" }, "pkg": { "assets": [ diff --git a/msa/web-ui/pom.xml b/msa/web-ui/pom.xml index 660920835b..f9ef249ce1 100644 --- a/msa/web-ui/pom.xml +++ b/msa/web-ui/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT msa org.thingsboard.msa @@ -49,7 +49,7 @@ org.thingsboard - ui + ui-ngx ${project.version} jar provided @@ -68,26 +68,26 @@ com.github.eirslett frontend-maven-plugin - 1.0 + 1.7.5 target ${basedir} - install node and npm + install node and yarn - install-node-and-npm + install-node-and-yarn - v10.16.0 - 6.4.1 + v12.16.1 + v1.22.4 - npm install + yarn install - npm + yarn install @@ -108,7 +108,7 @@ org.thingsboard - ui + ui-ngx jar false ${project.build.directory}/web @@ -149,176 +149,6 @@ org.apache.maven.plugins maven-assembly-plugin - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - com.spotify dockerfile-maven-plugin @@ -355,10 +185,10 @@ - npm-start + yarn-start - npm-start + yarn-start @@ -366,16 +196,16 @@ com.github.eirslett frontend-maven-plugin - 1.0 + 1.7.5 target ${basedir} - npm start + yarn start - npm + yarn diff --git a/msa/web-ui/server.js b/msa/web-ui/server.js index eea9afc333..6c69d49597 100644 --- a/msa/web-ui/server.js +++ b/msa/web-ui/server.js @@ -65,23 +65,26 @@ var server; apiProxy.on('error', function (err, req, res) { logger.warn('API proxy error: %s', err.message); - res.writeHead(500); - if (err.code && err.code === 'ECONNREFUSED') { + if (res.writeHead) { + res.writeHead(500); + if (err.code && err.code === 'ECONNREFUSED') { res.end('Unable to connect to ThingsBoard server.'); - } else { + } else { res.end('Thingsboard server connection error: ' + err.code ? err.code : ''); + } } }); - } - - if (useApiProxy) { app.all('/api/*', (req, res) => { - logger.debug(req.method + ' ' + req.originalUrl); - apiProxy.web(req, res); + logger.debug(req.method + ' ' + req.originalUrl); + apiProxy.web(req, res); }); app.all('/static/rulenode/*', (req, res) => { - apiProxy.web(req, res); + apiProxy.web(req, res); + }); + + server.on('upgrade', (req, socket, head) => { + apiProxy.ws(req, socket, head); }); } @@ -92,16 +95,6 @@ var server; app.use(express.static(root)); - if (useApiProxy) { - app.get('*', (req, res) => { - apiProxy.web(req, res); - }); - - server.on('upgrade', (req, socket, head) => { - apiProxy.ws(req, socket, head); - }); - } - server.listen(bindPort, bindAddress, (error) => { if (error) { logger.error('Failed to start ThingsBoard Web UI Microservice: %s', e.message); diff --git a/msa/web-ui/yarn.lock b/msa/web-ui/yarn.lock new file mode 100644 index 0000000000..49e72f5140 --- /dev/null +++ b/msa/web-ui/yarn.lock @@ -0,0 +1,2155 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/parser@^7.9.4": + version "7.11.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.4.tgz#6fa1a118b8b0d80d0267b719213dc947e88cc0ca" + integrity sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA== + +"@babel/runtime@^7.9.2": + version "7.11.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" + integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== + dependencies: + regenerator-runtime "^0.13.4" + +"@dabh/diagnostics@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.2.tgz#290d08f7b381b8f94607dc8f471a12c675f9db31" + integrity sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q== + dependencies: + colorspace "1.1.x" + enabled "2.0.x" + kuler "^2.0.0" + +"@nodelib/fs.scandir@2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" + integrity sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw== + dependencies: + "@nodelib/fs.stat" "2.0.3" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3" + integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976" + integrity sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ== + dependencies: + "@nodelib/fs.scandir" "2.1.3" + fastq "^1.6.0" + +"@sindresorhus/is@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" + integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== + +"@szmarczak/http-timer@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" + integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== + dependencies: + defer-to-connect "^1.0.1" + +"@types/color-name@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" + integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +accepts@~1.3.5, accepts@~1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + +ajv@^6.12.3: + version "6.12.4" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.4.tgz#0614facc4522127fa713445c6bfd3ebd376e2234" + integrity sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-align@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.0.tgz#b536b371cf687caaef236c18d3e21fe3797467cb" + integrity sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw== + dependencies: + string-width "^3.0.0" + +ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + +ansi-styles@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" + integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== + dependencies: + "@types/color-name" "^1.1.1" + color-convert "^2.0.1" + +anymatch@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" + integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +async@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" + integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.10.1" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.1.tgz#e1e82e4f3e999e2cfd61b161280d16a111f86428" + integrity sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA== + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +binary-extensions@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" + integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ== + +body-parser@1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== + dependencies: + bytes "3.1.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.7.2" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.7.0" + raw-body "2.4.0" + type-is "~1.6.17" + +boxen@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64" + integrity sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ== + dependencies: + ansi-align "^3.0.0" + camelcase "^5.3.1" + chalk "^3.0.0" + cli-boxes "^2.2.0" + string-width "^4.1.0" + term-size "^2.1.0" + type-fest "^0.8.1" + widest-line "^3.1.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.1, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +byline@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" + integrity sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE= + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= + +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + +cacheable-request@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" + integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^3.0.0" + lowercase-keys "^2.0.0" + normalize-url "^4.1.0" + responselike "^1.0.2" + +camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chokidar@^3.2.2: + version "3.4.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.2.tgz#38dc8e658dec3809741eb3ef7bb0a47fe424232d" + integrity sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.4.0" + optionalDependencies: + fsevents "~2.1.2" + +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + +cli-boxes@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.0.tgz#538ecae8f9c6ca508e3c3c95b453fe93cb4c168d" + integrity sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w== + +clone-response@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" + integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= + dependencies: + mimic-response "^1.0.0" + +color-convert@^1.9.1: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +color-name@^1.0.0, color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-string@^1.5.2: + version "1.5.3" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc" + integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@3.0.x: + version "3.0.0" + resolved "https://registry.yarnpkg.com/color/-/color-3.0.0.tgz#d920b4328d534a3ac8295d68f7bd4ba6c427be9a" + integrity sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w== + dependencies: + color-convert "^1.9.1" + color-string "^1.5.2" + +colors@^1.2.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" + integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== + +colorspace@1.1.x: + version "1.1.2" + resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.2.tgz#e0128950d082b86a2168580796a0aa5d6c68d8c5" + integrity sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ== + dependencies: + color "3.0.x" + text-hex "1.0.x" + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +compressible@~2.0.16: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +config@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/config/-/config-3.3.1.tgz#b6a70e2908a43b98ed20be7e367edf0cc8ed5a19" + integrity sha512-+2/KaaaAzdwUBE3jgZON11L1ggLLhpf2FsGrfqYFHZW22ySGv/HqYIXrBwKKvn+XZh1UBUjHwAcrfsSkSygT+Q== + dependencies: + json5 "^2.1.1" + +configstore@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" + integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== + dependencies: + dot-prop "^5.2.0" + graceful-fs "^4.1.2" + make-dir "^3.0.0" + unique-string "^2.0.0" + write-file-atomic "^3.0.0" + xdg-basedir "^4.0.0" + +connect-history-api-fallback@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" + integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== + +content-disposition@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +crypto-random-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" + integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +debug@2.6.9, debug@^2.2.0: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^3.2.6: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +decompress-response@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" + integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M= + dependencies: + mimic-response "^1.0.0" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + +defer-to-connect@^1.0.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" + integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +dot-prop@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.2.0.tgz#c34ecc29556dc45f1f4c22697b6f4904e0cc4fcb" + integrity sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A== + dependencies: + is-obj "^2.0.0" + +duplexer3@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" + integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +enabled@2.0.x: + version "2.0.0" + resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" + integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +escape-goat@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" + integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +escodegen@^1.14.1: + version "1.14.3" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" + integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== + dependencies: + esprima "^4.0.1" + estraverse "^4.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + +esprima@^4.0.0, esprima@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +estraverse@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +expand-template@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" + integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== + +express@^4.17.1: + version "4.17.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== + dependencies: + accepts "~1.3.7" + array-flatten "1.1.1" + body-parser "1.19.0" + content-disposition "0.5.3" + content-type "~1.0.4" + cookie "0.4.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.1.2" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.5" + qs "6.7.0" + range-parser "~1.2.1" + safe-buffer "5.1.2" + send "0.17.1" + serve-static "1.14.1" + setprototypeof "1.1.1" + statuses "~1.5.0" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.1.1: + version "3.2.4" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3" + integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.0" + merge2 "^1.3.0" + micromatch "^4.0.2" + picomatch "^2.2.1" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +fast-safe-stringify@^2.0.4: + version "2.0.7" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" + integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== + +fastq@^1.6.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481" + integrity sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q== + dependencies: + reusify "^1.0.4" + +fecha@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.0.tgz#3ffb6395453e3f3efff850404f0a59b6747f5f41" + integrity sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg== + +file-stream-rotator@^0.5.7: + version "0.5.7" + resolved "https://registry.yarnpkg.com/file-stream-rotator/-/file-stream-rotator-0.5.7.tgz#868a2e5966f7640a17dd86eda0e4467c089f6286" + integrity sha512-VYb3HZ/GiAGUCrfeakO8Mp54YGswNUHvL7P09WQcXAJNSj3iQ5QraYSp3cIn1MUyw6uzfgN/EFOarCNa4JvUHQ== + dependencies: + moment "^2.11.2" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +fn.name@1.x.x: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" + integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== + +follow-redirects@^1.0.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db" + integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA== + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + +from2@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" + integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8= + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.0" + +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-extra@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc" + integrity sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^1.0.0" + +fsevents@~2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" + integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== + +get-stream@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +get-stream@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +glob-parent@^5.1.0, glob-parent@~5.1.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" + integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== + dependencies: + is-glob "^4.0.1" + +global-dirs@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-2.0.1.tgz#acdf3bb6685bcd55cb35e8a052266569e9469201" + integrity sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A== + dependencies: + ini "^1.3.5" + +globby@^11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357" + integrity sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.1.1" + ignore "^5.1.4" + merge2 "^1.3.0" + slash "^3.0.0" + +got@^9.6.0: + version "9.6.0" + resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" + integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== + dependencies: + "@sindresorhus/is" "^0.14.0" + "@szmarczak/http-timer" "^1.1.2" + cacheable-request "^6.0.0" + decompress-response "^3.3.0" + duplexer3 "^0.1.4" + get-stream "^4.1.0" + lowercase-keys "^1.0.1" + mimic-response "^1.0.1" + p-cancelable "^1.0.0" + to-readable-stream "^1.0.0" + url-parse-lax "^3.0.0" + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.4" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" + integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.3: + version "5.1.5" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" + integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== + dependencies: + ajv "^6.12.3" + har-schema "^2.0.0" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-yarn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77" + integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw== + +http-cache-semantics@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" + integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-errors@~1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +http@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/http/-/http-0.0.0.tgz#86e6326d29c5d039de9fac584a45689f929f4f72" + integrity sha1-huYybSnF0Dnen6xYSkVon5KfT3I= + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= + +ignore@^5.1.4: + version "5.1.8" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" + integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== + +import-lazy@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" + integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM= + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@^1.3.5, ini@~1.3.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + +into-stream@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-5.1.1.tgz#f9a20a348a11f3c13face22763f2d02e127f4db8" + integrity sha512-krrAJ7McQxGGmvaYbB7Q1mcA+cRwg9Ij2RfWIeVesNBgVDZmzY/Fa4IpZUT3bmdRzMzdf/mzltCG2Dq99IZGBA== + dependencies: + from2 "^2.3.0" + p-is-promise "^3.0.0" + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-ci@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" + integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== + dependencies: + ci-info "^2.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + +is-installed-globally@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141" + integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g== + dependencies: + global-dirs "^2.0.1" + is-path-inside "^3.0.1" + +is-npm@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d" + integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig== + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" + integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== + +is-path-inside@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017" + integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg== + +is-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" + integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== + +is-typedarray@^1.0.0, is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +is-yarn-global@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" + integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +js-yaml@^3.14.0: + version "3.14.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" + integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +json-buffer@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" + integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +json5@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" + integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== + dependencies: + minimist "^1.2.5" + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= + optionalDependencies: + graceful-fs "^4.1.6" + +jsonfile@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.0.1.tgz#98966cba214378c8c84b82e085907b40bf614179" + integrity sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg== + dependencies: + universalify "^1.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +keyv@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" + integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== + dependencies: + json-buffer "3.0.0" + +kuler@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" + integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== + +latest-version@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" + integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA== + dependencies: + package-json "^6.3.0" + +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +logform@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/logform/-/logform-2.2.0.tgz#40f036d19161fc76b68ab50fdc7fe495544492f2" + integrity sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg== + dependencies: + colors "^1.2.1" + fast-safe-stringify "^2.0.4" + fecha "^4.2.0" + ms "^2.1.1" + triple-beam "^1.3.0" + +lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" + integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== + +lowercase-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" + integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== + +make-dir@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + +micromatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" + integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + dependencies: + braces "^3.0.1" + picomatch "^2.0.5" + +mime-db@1.44.0, "mime-db@>= 1.43.0 < 2": + version "1.44.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" + integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== + +mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24: + version "2.1.27" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" + integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== + dependencies: + mime-db "1.44.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mimic-response@^1.0.0, mimic-response@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" + integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.0, minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +mkdirp@^0.5.1: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + +moment@^2.11.2: + version "2.27.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" + integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + +ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +multistream@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/multistream/-/multistream-2.1.1.tgz#629d3a29bd76623489980d04519a2c365948148c" + integrity sha512-xasv76hl6nr1dEy3lPvy7Ej7K/Lx3O/FCvwge8PeVJpciPPoNCbaANcNiBug3IpdvTveZUcAV0DJzdnUDMesNQ== + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.5" + +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + +nodemon@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.4.tgz#55b09319eb488d6394aa9818148c0c2d1c04c416" + integrity sha512-Ltced+hIfTmaS28Zjv1BM552oQ3dbwPqI4+zI0SLgq+wpJhSyqgYude/aZa/3i31VCQWMfXJVxvu86abcam3uQ== + dependencies: + chokidar "^3.2.2" + debug "^3.2.6" + ignore-by-default "^1.0.1" + minimatch "^3.0.4" + pstree.remy "^1.1.7" + semver "^5.7.1" + supports-color "^5.5.0" + touch "^3.1.0" + undefsafe "^2.0.2" + update-notifier "^4.0.0" + +nopt@~1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" + integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4= + dependencies: + abbrev "1" + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-url@^4.1.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" + integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-hash@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.0.3.tgz#d12db044e03cd2ca3d77c0570d87225b02e1e6ea" + integrity sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg== + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +one-time@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45" + integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g== + dependencies: + fn.name "1.x.x" + +optionator@^0.8.1: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + +os-tmpdir@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + +p-cancelable@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" + integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== + +p-is-promise@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-3.0.0.tgz#58e78c7dfe2e163cf2a04ff869e7c1dba64a5971" + integrity sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ== + +package-json@^6.3.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0" + integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ== + dependencies: + got "^9.6.0" + registry-auth-token "^4.0.0" + registry-url "^5.0.0" + semver "^6.2.0" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" + integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== + +pkg-fetch@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/pkg-fetch/-/pkg-fetch-2.6.9.tgz#c18c5fa9604c57a3df3d9630afb64e176bc8732d" + integrity sha512-EnVR8LRILXBvaNP+wJOSY02c3+qDDfyEyR+aqAHLhcc9PBnbxFT9UZ1+If49goPQzQPn26TzF//fc6KXZ0aXEg== + dependencies: + "@babel/runtime" "^7.9.2" + byline "^5.0.0" + chalk "^3.0.0" + expand-template "^2.0.3" + fs-extra "^8.1.0" + minimist "^1.2.5" + progress "^2.0.3" + request "^2.88.0" + request-progress "^3.0.0" + semver "^6.3.0" + unique-temp-dir "^1.0.0" + +pkg@^4.4.9: + version "4.4.9" + resolved "https://registry.yarnpkg.com/pkg/-/pkg-4.4.9.tgz#be04f8d03795772b7c4394724ae7252d7c2a4519" + integrity sha512-FK4GqHtcCY2PPPVaKViU0NyRzpo6gCS7tPKN5b7AkElqjAOCH1bsRKgohEnxThr6DWfTGByGqba2YHGR/BqbmA== + dependencies: + "@babel/parser" "^7.9.4" + "@babel/runtime" "^7.9.2" + chalk "^3.0.0" + escodegen "^1.14.1" + fs-extra "^8.1.0" + globby "^11.0.0" + into-stream "^5.1.1" + minimist "^1.2.5" + multistream "^2.1.1" + pkg-fetch "^2.6.9" + progress "^2.0.3" + resolve "^1.15.1" + stream-meter "^1.0.4" + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= + +prepend-http@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" + integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +progress@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +proxy-addr@~2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" + integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.9.1" + +psl@^1.1.28: + version "1.8.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" + integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== + +pstree.remy@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +pupa@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.0.1.tgz#dbdc9ff48ffbea4a26a069b6f9f7abb051008726" + integrity sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA== + dependencies: + escape-goat "^2.0.0" + +qs@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== + dependencies: + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" + unpipe "1.0.0" + +rc@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +readable-stream@^2.0.0, readable-stream@^2.0.5, readable-stream@^2.1.4, readable-stream@^2.3.7: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.4.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada" + integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ== + dependencies: + picomatch "^2.2.1" + +regenerator-runtime@^0.13.4: + version "0.13.7" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" + integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== + +registry-auth-token@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.0.tgz#1d37dffda72bbecd0f581e4715540213a65eb7da" + integrity sha512-P+lWzPrsgfN+UEpDS3U8AQKg/UjZX6mQSJueZj3EK+vNESoqBSpBUD3gmu4sF9lOsjXWjF11dQKUqemf3veq1w== + dependencies: + rc "^1.2.8" + +registry-url@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009" + integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw== + dependencies: + rc "^1.2.8" + +request-progress@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe" + integrity sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4= + dependencies: + throttleit "^1.0.0" + +request@^2.88.0: + version "2.88.2" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" + integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= + +resolve@^1.15.1: + version "1.17.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" + integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== + dependencies: + path-parse "^1.0.6" + +responselike@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" + integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= + dependencies: + lowercase-keys "^1.0.0" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +run-parallel@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" + integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semver-diff@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" + integrity sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg== + dependencies: + semver "^6.3.0" + +semver@^5.7.1: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +send@0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" + integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.7.2" + mime "1.6.0" + ms "2.1.1" + on-finished "~2.3.0" + range-parser "~1.2.1" + statuses "~1.5.0" + +serve-static@1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" + integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.17.1" + +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + +signal-exit@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" + integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== + +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= + dependencies: + is-arrayish "^0.3.1" + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +sshpk@^1.7.0: + version "1.16.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +stack-trace@0.0.x: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" + integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= + +"statuses@>= 1.5.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +stream-meter@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/stream-meter/-/stream-meter-1.0.4.tgz#52af95aa5ea760a2491716704dbff90f73afdd1d" + integrity sha1-Uq+Vql6nYKJJFxZwTb/5D3Ov3R0= + dependencies: + readable-stream "^2.1.4" + +string-width@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string-width@^4.0.0, string-width@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= + +supports-color@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + +term-size@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.0.tgz#1f16adedfe9bdc18800e1776821734086fcc6753" + integrity sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw== + +text-hex@1.0.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" + integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== + +throttleit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" + integrity sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw= + +to-readable-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" + integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + +touch@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" + integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== + dependencies: + nopt "~1.0.10" + +tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +triple-beam@^1.2.0, triple-beam@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" + integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= + dependencies: + prelude-ls "~1.1.2" + +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + +type-is@~1.6.17, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typedarray-to-buffer@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" + +uid2@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.3.tgz#483126e11774df2f71b8b639dcd799c376162b82" + integrity sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I= + +undefsafe@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.3.tgz#6b166e7094ad46313b2202da7ecc2cd7cc6e7aae" + integrity sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A== + dependencies: + debug "^2.2.0" + +unique-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" + integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== + dependencies: + crypto-random-string "^2.0.0" + +unique-temp-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unique-temp-dir/-/unique-temp-dir-1.0.0.tgz#6dce95b2681ca003eebfb304a415f9cbabcc5385" + integrity sha1-bc6VsmgcoAPuv7MEpBX5y6vMU4U= + dependencies: + mkdirp "^0.5.1" + os-tmpdir "^1.0.1" + uid2 "0.0.3" + +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +universalify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" + integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + +update-notifier@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-4.1.1.tgz#895fc8562bbe666179500f9f2cebac4f26323746" + integrity sha512-9y+Kds0+LoLG6yN802wVXoIfxYEwh3FlZwzMwpCZp62S2i1/Jzeqb9Eeeju3NSHccGGasfGlK5/vEHbAifYRDg== + dependencies: + boxen "^4.2.0" + chalk "^3.0.0" + configstore "^5.0.1" + has-yarn "^2.1.0" + import-lazy "^2.1.0" + is-ci "^2.0.0" + is-installed-globally "^0.3.1" + is-npm "^4.0.0" + is-yarn-global "^0.3.0" + latest-version "^5.0.0" + pupa "^2.0.1" + semver-diff "^3.1.1" + xdg-basedir "^4.0.0" + +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + dependencies: + punycode "^2.1.0" + +url-parse-lax@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" + integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww= + dependencies: + prepend-http "^2.0.0" + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + +uuid@^3.3.2: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +widest-line@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" + integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== + dependencies: + string-width "^4.0.0" + +winston-daily-rotate-file@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/winston-daily-rotate-file/-/winston-daily-rotate-file-4.5.0.tgz#3914ac57c4bdae1138170bec85af0c2217b253b1" + integrity sha512-/HqeWiU48dzGqcrABRlxYWVMdL6l3uKCtFSJyrqK+E2rLnSFNsgYpvwx15EgTitBLNzH69lQd/+z2ASryV2aqw== + dependencies: + file-stream-rotator "^0.5.7" + object-hash "^2.0.1" + triple-beam "^1.3.0" + winston-transport "^4.2.0" + +winston-transport@^4.2.0, winston-transport@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.4.0.tgz#17af518daa690d5b2ecccaa7acf7b20ca7925e59" + integrity sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw== + dependencies: + readable-stream "^2.3.7" + triple-beam "^1.2.0" + +winston@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/winston/-/winston-3.3.3.tgz#ae6172042cafb29786afa3d09c8ff833ab7c9170" + integrity sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw== + dependencies: + "@dabh/diagnostics" "^2.0.2" + async "^3.1.0" + is-stream "^2.0.0" + logform "^2.2.0" + one-time "^1.0.0" + readable-stream "^3.4.0" + stack-trace "0.0.x" + triple-beam "^1.3.0" + winston-transport "^4.4.0" + +word-wrap@~1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +write-file-atomic@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" + integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== + dependencies: + imurmurhash "^0.1.4" + is-typedarray "^1.0.0" + signal-exit "^3.0.2" + typedarray-to-buffer "^3.1.5" + +xdg-basedir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" + integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml index 58cbcf4e6a..58524a1a08 100644 --- a/netty-mqtt/pom.xml +++ b/netty-mqtt/pom.xml @@ -19,11 +19,11 @@ 4.0.0 org.thingsboard - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT thingsboard netty-mqtt - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT jar Netty MQTT Client diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java index cce1fb0df9..5e191becfd 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java @@ -198,10 +198,9 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(message.variableHeader().packetId()); MqttMessage pubrecMessage = new MqttMessage(fixedHeader, variableHeader); - MqttIncomingQos2Publish incomingQos2Publish = new MqttIncomingQos2Publish(message, pubrecMessage); + MqttIncomingQos2Publish incomingQos2Publish = new MqttIncomingQos2Publish(message); this.client.getQos2PendingIncomingPublishes().put(message.variableHeader().packetId(), incomingQos2Publish); message.payload().retain(); - incomingQos2Publish.startPubrecRetransmitTimer(this.client.getEventLoop().next(), this.client::sendAndFlushPacket); channel.writeAndFlush(pubrecMessage); } @@ -248,7 +247,6 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler if (this.client.getQos2PendingIncomingPublishes().containsKey(((MqttMessageIdVariableHeader) message.variableHeader()).messageId())) { MqttIncomingQos2Publish incomingQos2Publish = this.client.getQos2PendingIncomingPublishes().get(((MqttMessageIdVariableHeader) message.variableHeader()).messageId()); this.invokeHandlersForIncomingPublish(incomingQos2Publish.getIncomingPublish()); - incomingQos2Publish.onPubrelReceived(); this.client.getQos2PendingIncomingPublishes().remove(incomingQos2Publish.getIncomingPublish().variableHeader().packetId()); } MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBCOMP, false, MqttQoS.AT_MOST_ONCE, false, 0); diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java index aef2cad684..289f73dc52 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java @@ -19,18 +19,40 @@ import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableSet; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBuf; -import io.netty.channel.*; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; -import io.netty.handler.codec.mqtt.*; +import io.netty.handler.codec.mqtt.MqttDecoder; +import io.netty.handler.codec.mqtt.MqttEncoder; +import io.netty.handler.codec.mqtt.MqttFixedHeader; +import io.netty.handler.codec.mqtt.MqttMessage; +import io.netty.handler.codec.mqtt.MqttMessageIdVariableHeader; +import io.netty.handler.codec.mqtt.MqttMessageType; +import io.netty.handler.codec.mqtt.MqttPublishMessage; +import io.netty.handler.codec.mqtt.MqttPublishVariableHeader; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.netty.handler.codec.mqtt.MqttSubscribeMessage; +import io.netty.handler.codec.mqtt.MqttSubscribePayload; +import io.netty.handler.codec.mqtt.MqttTopicSubscription; +import io.netty.handler.codec.mqtt.MqttUnsubscribeMessage; +import io.netty.handler.codec.mqtt.MqttUnsubscribePayload; import io.netty.handler.ssl.SslContext; import io.netty.handler.timeout.IdleStateHandler; -import io.netty.util.collection.IntObjectHashMap; import io.netty.util.concurrent.DefaultPromise; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.Promise; -import java.util.*; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -41,11 +63,11 @@ import java.util.concurrent.atomic.AtomicInteger; final class MqttClientImpl implements MqttClient { private final Set serverSubscriptions = new HashSet<>(); - private final IntObjectHashMap pendingServerUnsubscribes = new IntObjectHashMap<>(); - private final IntObjectHashMap qos2PendingIncomingPublishes = new IntObjectHashMap<>(); - private final IntObjectHashMap pendingPublishes = new IntObjectHashMap<>(); + private final ConcurrentMap pendingServerUnsubscribes = new ConcurrentHashMap<>(); + private final ConcurrentMap qos2PendingIncomingPublishes = new ConcurrentHashMap<>(); + private final ConcurrentMap pendingPublishes = new ConcurrentHashMap<>(); private final HashMultimap subscriptions = HashMultimap.create(); - private final IntObjectHashMap pendingSubscriptions = new IntObjectHashMap<>(); + private final ConcurrentMap pendingSubscriptions = new ConcurrentHashMap<>(); private final Set pendingSubscribeTopics = new HashSet<>(); private final HashMultimap handlerToSubscribtion = HashMultimap.create(); private final AtomicInteger nextMessageId = new AtomicInteger(1); @@ -340,6 +362,7 @@ final class MqttClientImpl implements MqttClient { MqttPublishVariableHeader variableHeader = new MqttPublishVariableHeader(topic, getNewMessageId().messageId()); MqttPublishMessage message = new MqttPublishMessage(fixedHeader, variableHeader, payload); MqttPendingPublish pendingPublish = new MqttPendingPublish(variableHeader.packetId(), future, payload.retain(), message, qos); + this.pendingPublishes.put(pendingPublish.getMessageId(), pendingPublish); ChannelFuture channelFuture = this.sendAndFlushPacket(message); if (channelFuture != null) { @@ -350,10 +373,12 @@ final class MqttClientImpl implements MqttClient { } } if (pendingPublish.isSent() && pendingPublish.getQos() == MqttQoS.AT_MOST_ONCE) { + this.pendingPublishes.remove(pendingPublish.getMessageId()); pendingPublish.getFuture().setSuccess(null); //We don't get an ACK for QOS 0 } else if (pendingPublish.isSent()) { - this.pendingPublishes.put(pendingPublish.getMessageId(), pendingPublish); pendingPublish.startPublishRetransmissionTimer(this.eventLoop.next(), this::sendAndFlushPacket); + } else { + this.pendingPublishes.remove(pendingPublish.getMessageId()); } return future; } @@ -465,7 +490,7 @@ final class MqttClientImpl implements MqttClient { } } - IntObjectHashMap getPendingSubscriptions() { + ConcurrentMap getPendingSubscriptions() { return pendingSubscriptions; } @@ -485,15 +510,15 @@ final class MqttClientImpl implements MqttClient { return serverSubscriptions; } - IntObjectHashMap getPendingServerUnsubscribes() { + ConcurrentMap getPendingServerUnsubscribes() { return pendingServerUnsubscribes; } - IntObjectHashMap getPendingPublishes() { + ConcurrentMap getPendingPublishes() { return pendingPublishes; } - IntObjectHashMap getQos2PendingIncomingPublishes() { + ConcurrentMap getQos2PendingIncomingPublishes() { return qos2PendingIncomingPublishes; } diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttIncomingQos2Publish.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttIncomingQos2Publish.java index 00f13ac4ad..704443f7c5 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttIncomingQos2Publish.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttIncomingQos2Publish.java @@ -15,34 +15,17 @@ */ package org.thingsboard.mqtt; -import io.netty.channel.EventLoop; -import io.netty.handler.codec.mqtt.*; - -import java.util.function.Consumer; +import io.netty.handler.codec.mqtt.MqttPublishMessage; final class MqttIncomingQos2Publish { private final MqttPublishMessage incomingPublish; - private final RetransmissionHandler retransmissionHandler = new RetransmissionHandler<>(); - - MqttIncomingQos2Publish(MqttPublishMessage incomingPublish, MqttMessage originalMessage) { + MqttIncomingQos2Publish(MqttPublishMessage incomingPublish) { this.incomingPublish = incomingPublish; - - this.retransmissionHandler.setOriginalMessage(originalMessage); } MqttPublishMessage getIncomingPublish() { return incomingPublish; } - - void startPubrecRetransmitTimer(EventLoop eventLoop, Consumer sendPacket) { - this.retransmissionHandler.setHandle((fixedHeader, originalMessage) -> - sendPacket.accept(new MqttMessage(fixedHeader, originalMessage.variableHeader()))); - this.retransmissionHandler.start(eventLoop); - } - - void onPubrelReceived() { - this.retransmissionHandler.stop(); - } } diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/RetransmissionHandler.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/RetransmissionHandler.java index 7d8d063d89..a77fd1f83c 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/RetransmissionHandler.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/RetransmissionHandler.java @@ -18,6 +18,8 @@ package org.thingsboard.mqtt; import io.netty.channel.EventLoop; import io.netty.handler.codec.mqtt.MqttFixedHeader; import io.netty.handler.codec.mqtt.MqttMessage; +import io.netty.handler.codec.mqtt.MqttMessageType; +import io.netty.handler.codec.mqtt.MqttQoS; import io.netty.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -44,7 +46,11 @@ final class RetransmissionHandler { private void startTimer(EventLoop eventLoop){ this.timer = eventLoop.schedule(() -> { this.timeout += 5; - MqttFixedHeader fixedHeader = new MqttFixedHeader(this.originalMessage.fixedHeader().messageType(), true, this.originalMessage.fixedHeader().qosLevel(), this.originalMessage.fixedHeader().isRetain(), this.originalMessage.fixedHeader().remainingLength()); + boolean isDup = this.originalMessage.fixedHeader().isDup(); + if(this.originalMessage.fixedHeader().messageType() == MqttMessageType.PUBLISH && this.originalMessage.fixedHeader().qosLevel() != MqttQoS.AT_MOST_ONCE){ + isDup = true; + } + MqttFixedHeader fixedHeader = new MqttFixedHeader(this.originalMessage.fixedHeader().messageType(), isDup, this.originalMessage.fixedHeader().qosLevel(), this.originalMessage.fixedHeader().isRetain(), this.originalMessage.fixedHeader().remainingLength()); handler.accept(fixedHeader, originalMessage); startTimer(eventLoop); }, timeout, TimeUnit.SECONDS); diff --git a/pom.xml b/pom.xml index 335e6c7eba..cc5bbce51b 100755 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard thingsboard - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT pom Thingsboard @@ -49,8 +49,9 @@ 1.2.3 1.9.5 0.10 - 3.6.0 - 3.5.0.1 + 4.6.0 + 4.0.5 + 4.3.1.0 1.2.7 28.2-jre 2.6.1 @@ -63,15 +64,14 @@ 2.2.6 1.0.2 2.6.2 - 1.7 - 2.0 + 2.3.30 1.6.2 4.2.0 3.5.5 3.11.4 1.22.1 1.16.18 - 1.1.0 + 1.2.4 4.1.49.Final 1.5.0 4.8.0 @@ -97,7 +97,6 @@ 2.57 2.7.7 1.25 - 1.3.10 1.11.747 1.105.0 3.2.0 @@ -105,6 +104,7 @@ 1.4.3 1.9.4 3.2.2 + 1.5.0 1.5.2 @@ -114,7 +114,7 @@ rule-engine dao transport - ui + ui-ngx tools application msa @@ -725,12 +725,17 @@ **/*.proto.js docker/haproxy/** docker/tb-node/** + ui/** + src/browserslist + **/*.raw + **/apache/cassandra/io/** JAVADOC_STYLE DOUBLEDASHES_STYLE JAVADOC_STYLE SLASHSTAR_STYLE + SLASHSTAR_STYLE SCRIPT_STYLE JAVADOC_STYLE @@ -968,28 +973,9 @@ ${jjwt.version} - org.apache.velocity - velocity - ${velocity.version} - - - org.apache.velocity - velocity-tools - ${velocity-tools.version} - - - javax.servlet - servlet-api - - - dom4j - dom4j - - - antlr - antlr - - + org.freemarker + freemarker + ${freemarker.version} org.yaml @@ -1061,19 +1047,19 @@ ${netty.version} - com.datastax.cassandra - cassandra-driver-core + com.datastax.oss + java-driver-core ${cassandra.version} - com.datastax.cassandra - cassandra-driver-mapping + com.datastax.oss + java-driver-query-builder ${cassandra.version} - com.datastax.cassandra - cassandra-driver-extras - ${cassandra.version} + io.dropwizard.metrics + metrics-jmx + ${metrics.version} org.apache.commons @@ -1363,19 +1349,10 @@ ${commons-collections.version} - org.apache.struts - struts-core - ${struts.version} - - - org.apache.struts - struts-taglib - ${struts.version} - - - org.apache.struts - struts-tiles - ${struts.version} + org.java-websocket + Java-WebSocket + ${java-websocket.version} + test org.springframework.boot diff --git a/rest-client/pom.xml b/rest-client/pom.xml index 061efa390b..a7b97610d1 100644 --- a/rest-client/pom.xml +++ b/rest-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT thingsboard rest-client @@ -45,4 +45,27 @@ + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + false + + + + diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index e14996d12f..0a92d4b1a8 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -38,50 +38,72 @@ import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceInfo; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileInfo; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.EntityViewInfo; import org.thingsboard.server.common.data.Event; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantInfo; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.UpdateMessage; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetSearchQuery; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.AuditLog; import org.thingsboard.server.common.data.device.DeviceSearchQuery; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; +import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; -import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientsParams; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.query.AlarmData; +import org.thingsboard.server.common.data.query.AlarmDataQuery; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationInfo; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.rule.DefaultRuleChainCreateRequest; import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainData; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; @@ -271,20 +293,20 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { restTemplate.postForLocation(baseURL + "/api/alarm/{alarmId}/clear", null, alarmId.getId()); } - public TimePageData getAlarms(EntityId entityId, AlarmSearchStatus searchStatus, AlarmStatus status, TimePageLink pageLink, Boolean fetchOriginator) { + public PageData getAlarms(EntityId entityId, AlarmSearchStatus searchStatus, AlarmStatus status, TimePageLink pageLink, Boolean fetchOriginator) { Map params = new HashMap<>(); params.put("entityType", entityId.getEntityType().name()); params.put("entityId", entityId.getId().toString()); params.put("searchStatus", searchStatus.name()); params.put("status", status.name()); params.put("fetchOriginator", String.valueOf(fetchOriginator)); - addPageLinkToParam(params, pageLink); + addTimePageLinkToParam(params, pageLink); return restTemplate.exchange( - baseURL + "/api/alarm/{entityType}/{entityId}?searchStatus={searchStatus}&status={status}&fetchOriginator={fetchOriginator}&" + getUrlParams(pageLink), + baseURL + "/api/alarm/{entityType}/{entityId}?searchStatus={searchStatus}&status={status}&fetchOriginator={fetchOriginator}&" + getTimeUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, - new ParameterizedTypeReference>() { + new ParameterizedTypeReference>() { }, params).getBody(); } @@ -325,6 +347,19 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } } + public Optional getAssetInfoById(AssetId assetId) { + try { + ResponseEntity asset = restTemplate.getForEntity(baseURL + "/api/asset/info/{assetId}", AssetInfo.class, assetId.getId()); + return Optional.ofNullable(asset.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + public Asset saveAsset(Asset asset) { return restTemplate.postForEntity(baseURL + "/api/asset", asset, Asset.class).getBody(); } @@ -376,15 +411,29 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } } - public TextPageData getTenantAssets(TextPageLink pageLink, String assetType) { + public PageData getTenantAssets(PageLink pageLink, String assetType) { Map params = new HashMap<>(); params.put("type", assetType); addPageLinkToParam(params, pageLink); - ResponseEntity> assets = restTemplate.exchange( + ResponseEntity> assets = restTemplate.exchange( baseURL + "/api/tenant/assets?type={type}&" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, - new ParameterizedTypeReference>() { + new ParameterizedTypeReference>() { + }, + params); + return assets.getBody(); + } + + public PageData getTenantAssetInfos(PageLink pageLink, String assetType) { + Map params = new HashMap<>(); + params.put("type", assetType); + addPageLinkToParam(params, pageLink); + + ResponseEntity> assets = restTemplate.exchange( + baseURL + "/api/tenant/assetInfos?type={type}&" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { }, params); return assets.getBody(); @@ -403,17 +452,33 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } } - public TextPageData getCustomerAssets(CustomerId customerId, TextPageLink pageLink, String assetType) { + public PageData getCustomerAssets(CustomerId customerId, PageLink pageLink, String assetType) { Map params = new HashMap<>(); params.put("customerId", customerId.getId().toString()); params.put("type", assetType); addPageLinkToParam(params, pageLink); - ResponseEntity> assets = restTemplate.exchange( + ResponseEntity> assets = restTemplate.exchange( baseURL + "/api/customer/{customerId}/assets?type={type}&" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, - new ParameterizedTypeReference>() { + new ParameterizedTypeReference>() { + }, + params); + return assets.getBody(); + } + + public PageData getCustomerAssetInfos(CustomerId customerId, PageLink pageLink, String assetType) { + Map params = new HashMap<>(); + params.put("customerId", customerId.getId().toString()); + params.put("type", assetType); + addPageLinkToParam(params, pageLink); + + ResponseEntity> assets = restTemplate.exchange( + baseURL + "/api/customer/{customerId}/assetInfos?type={type}&" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { }, params); return assets.getBody(); @@ -483,65 +548,65 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { customerId.toString(), assetId.toString()).getBody(); } - public TimePageData getAuditLogsByCustomerId(CustomerId customerId, TimePageLink pageLink, List actionTypes) { + public PageData getAuditLogsByCustomerId(CustomerId customerId, TimePageLink pageLink, List actionTypes) { Map params = new HashMap<>(); params.put("customerId", customerId.getId().toString()); params.put("actionTypes", listEnumToString(actionTypes)); - addPageLinkToParam(params, pageLink); + addTimePageLinkToParam(params, pageLink); - ResponseEntity> auditLog = restTemplate.exchange( - baseURL + "/api/audit/logs/customer/{customerId}?actionTypes={actionTypes}&" + getUrlParams(pageLink), + ResponseEntity> auditLog = restTemplate.exchange( + baseURL + "/api/audit/logs/customer/{customerId}?actionTypes={actionTypes}&" + getTimeUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, - new ParameterizedTypeReference>() { + new ParameterizedTypeReference>() { }, params); return auditLog.getBody(); } - public TimePageData getAuditLogsByUserId(UserId userId, TimePageLink pageLink, List actionTypes) { + public PageData getAuditLogsByUserId(UserId userId, TimePageLink pageLink, List actionTypes) { Map params = new HashMap<>(); params.put("userId", userId.getId().toString()); params.put("actionTypes", listEnumToString(actionTypes)); - addPageLinkToParam(params, pageLink); + addTimePageLinkToParam(params, pageLink); - ResponseEntity> auditLog = restTemplate.exchange( - baseURL + "/api/audit/logs/user/{userId}?actionTypes={actionTypes}&" + getUrlParams(pageLink), + ResponseEntity> auditLog = restTemplate.exchange( + baseURL + "/api/audit/logs/user/{userId}?actionTypes={actionTypes}&" + getTimeUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, - new ParameterizedTypeReference>() { + new ParameterizedTypeReference>() { }, params); return auditLog.getBody(); } - public TimePageData getAuditLogsByEntityId(EntityId entityId, List actionTypes, TimePageLink pageLink) { + public PageData getAuditLogsByEntityId(EntityId entityId, List actionTypes, TimePageLink pageLink) { Map params = new HashMap<>(); params.put("entityType", entityId.getEntityType().name()); params.put("entityId", entityId.getId().toString()); params.put("actionTypes", listEnumToString(actionTypes)); - addPageLinkToParam(params, pageLink); + addTimePageLinkToParam(params, pageLink); - ResponseEntity> auditLog = restTemplate.exchange( - baseURL + "/api/audit/logs/entity/{entityType}/{entityId}?actionTypes={actionTypes}&" + getUrlParams(pageLink), + ResponseEntity> auditLog = restTemplate.exchange( + baseURL + "/api/audit/logs/entity/{entityType}/{entityId}?actionTypes={actionTypes}&" + getTimeUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, - new ParameterizedTypeReference>() { + new ParameterizedTypeReference>() { }, params); return auditLog.getBody(); } - public TimePageData getAuditLogs(TimePageLink pageLink, List actionTypes) { + public PageData getAuditLogs(TimePageLink pageLink, List actionTypes) { Map params = new HashMap<>(); params.put("actionTypes", listEnumToString(actionTypes)); - addPageLinkToParam(params, pageLink); + addTimePageLinkToParam(params, pageLink); - ResponseEntity> auditLog = restTemplate.exchange( - baseURL + "/api/audit/logs?actionTypes={actionTypes}&" + getUrlParams(pageLink), + ResponseEntity> auditLog = restTemplate.exchange( + baseURL + "/api/audit/logs?actionTypes={actionTypes}&" + getTimeUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, - new ParameterizedTypeReference>() { + new ParameterizedTypeReference>() { }, params); return auditLog.getBody(); @@ -593,11 +658,15 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } public Optional activateUser(UserId userId, String password) { + return activateUser(userId, password, true); + } + + public Optional activateUser(UserId userId, String password, boolean sendActivationMail) { ObjectNode activateRequest = objectMapper.createObjectNode(); activateRequest.put("activateToken", getActivateToken(userId)); activateRequest.put("password", password); try { - ResponseEntity jsonNode = restTemplate.postForEntity(baseURL + "/api/noauth/activate", activateRequest, JsonNode.class); + ResponseEntity jsonNode = restTemplate.postForEntity(baseURL + "/api/noauth/activate?sendActivationMail={sendActivationMail}", activateRequest, JsonNode.class, sendActivationMail); return Optional.ofNullable(jsonNode.getBody()); } catch (HttpClientErrorException exception) { if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { @@ -679,15 +748,15 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { restTemplate.delete(baseURL + "/api/customer/{customerId}", customerId.getId()); } - public TextPageData getCustomers(TextPageLink pageLink) { + public PageData getCustomers(PageLink pageLink) { Map params = new HashMap<>(); addPageLinkToParam(params, pageLink); - ResponseEntity> customer = restTemplate.exchange( + ResponseEntity> customer = restTemplate.exchange( baseURL + "/api/customers?" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, - new ParameterizedTypeReference>() { + new ParameterizedTypeReference>() { }, params); return customer.getBody(); @@ -870,35 +939,35 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } } - public TextPageData getTenantDashboards(TenantId tenantId, TextPageLink pageLink) { + public PageData getTenantDashboards(TenantId tenantId, PageLink pageLink) { Map params = new HashMap<>(); params.put("tenantId", tenantId.getId().toString()); addPageLinkToParam(params, pageLink); return restTemplate.exchange( baseURL + "/api/tenant/{tenantId}/dashboards?" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, - new ParameterizedTypeReference>() { + new ParameterizedTypeReference>() { }, params).getBody(); } - public TextPageData getTenantDashboards(TextPageLink pageLink) { + public PageData getTenantDashboards(PageLink pageLink) { Map params = new HashMap<>(); addPageLinkToParam(params, pageLink); return restTemplate.exchange( baseURL + "/api/tenant/dashboards?" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, - new ParameterizedTypeReference>() { + new ParameterizedTypeReference>() { }, params).getBody(); } - public TimePageData getCustomerDashboards(CustomerId customerId, TimePageLink pageLink) { + public PageData getCustomerDashboards(CustomerId customerId, PageLink pageLink) { Map params = new HashMap<>(); params.put("customerId", customerId.getId().toString()); addPageLinkToParam(params, pageLink); return restTemplate.exchange( baseURL + "/api/customer/{customerId}/dashboards?" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, - new ParameterizedTypeReference>() { + new ParameterizedTypeReference>() { }, params).getBody(); } @@ -910,8 +979,8 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { @Deprecated public List findTenantDashboards() { try { - ResponseEntity> dashboards = - restTemplate.exchange(baseURL + "/api/tenant/dashboards?limit=100000", HttpMethod.GET, null, new ParameterizedTypeReference>() { + ResponseEntity> dashboards = + restTemplate.exchange(baseURL + "/api/tenant/dashboards?pageSize=100000", HttpMethod.GET, null, new ParameterizedTypeReference>() { }); return dashboards.getBody().getData(); } catch (HttpClientErrorException exception) { @@ -936,8 +1005,25 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } } + public Optional getDeviceInfoById(DeviceId deviceId) { + try { + ResponseEntity device = restTemplate.getForEntity(baseURL + "/api/device/info/{deviceId}", DeviceInfo.class, deviceId); + return Optional.ofNullable(device.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + public Device saveDevice(Device device) { - return restTemplate.postForEntity(baseURL + "/api/device", device, Device.class).getBody(); + return saveDevice(device, null); + } + + public Device saveDevice(Device device, String accessToken) { + return restTemplate.postForEntity(baseURL + "/api/device?accessToken={accessToken}", device, Device.class, accessToken).getBody(); } public void deleteDevice(DeviceId deviceId) { @@ -1000,14 +1086,26 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { return restTemplate.postForEntity(baseURL + "/api/device/credentials", deviceCredentials, DeviceCredentials.class).getBody(); } - public TextPageData getTenantDevices(String type, TextPageLink pageLink) { + public PageData getTenantDevices(String type, PageLink pageLink) { Map params = new HashMap<>(); params.put("type", type); addPageLinkToParam(params, pageLink); return restTemplate.exchange( baseURL + "/api/tenant/devices?type={type}&" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, - new ParameterizedTypeReference>() { + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public PageData getTenantDeviceInfos(String type, DeviceProfileId deviceProfileId, PageLink pageLink) { + Map params = new HashMap<>(); + params.put("type", type); + params.put("deviceProfileId", deviceProfileId != null ? deviceProfileId.toString() : null); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/tenant/deviceInfos?type={type}&deviceProfileId={deviceProfileId}&" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { }, params).getBody(); } @@ -1024,7 +1122,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } } - public TextPageData getCustomerDevices(CustomerId customerId, String deviceType, TextPageLink pageLink) { + public PageData getCustomerDevices(CustomerId customerId, String deviceType, PageLink pageLink) { Map params = new HashMap<>(); params.put("customerId", customerId.getId().toString()); params.put("type", deviceType); @@ -1032,7 +1130,20 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { return restTemplate.exchange( baseURL + "/api/customer/{customerId}/devices?type={type}&" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, - new ParameterizedTypeReference>() { + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public PageData getCustomerDeviceInfos(CustomerId customerId, String deviceType, DeviceProfileId deviceProfileId, PageLink pageLink) { + Map params = new HashMap<>(); + params.put("customerId", customerId.toString()); + params.put("type", deviceType); + params.put("deviceProfileId", deviceProfileId != null ? deviceProfileId.toString() : null); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/customer/{customerId}/devices?type={type}&deviceProfileId={deviceProfileId}&" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { }, params).getBody(); } @@ -1074,6 +1185,12 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { restTemplate.delete(baseURL + "/api/customer/device/{deviceName}/claim", deviceName); } + public Device assignDeviceToTenant(TenantId tenantId, DeviceId deviceId) { + return restTemplate.postForEntity( + baseURL + "/api/tenant/{tenantId}/device/{deviceId}", + HttpEntity.EMPTY, Device.class, tenantId, deviceId).getBody(); + } + @Deprecated public Device createDevice(String name, String type) { Device device = new Device(); @@ -1138,6 +1255,91 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { customerId.toString(), deviceId.toString()).getBody(); } + public Optional getDeviceProfileById(DeviceProfileId deviceProfileId) { + try { + ResponseEntity deviceProfile = restTemplate.getForEntity(baseURL + "/api/deviceProfile/{deviceProfileId}", DeviceProfile.class, deviceProfileId); + return Optional.ofNullable(deviceProfile.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional getDeviceProfileInfoById(DeviceProfileId deviceProfileId) { + try { + ResponseEntity deviceProfileInfo = restTemplate.getForEntity(baseURL + "/api/deviceProfileInfo/{deviceProfileId}", DeviceProfileInfo.class, deviceProfileId); + return Optional.ofNullable(deviceProfileInfo.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public DeviceProfileInfo getDefaultDeviceProfileInfo() { + return restTemplate.getForEntity(baseURL + "/api/deviceProfileInfo/default", DeviceProfileInfo.class).getBody(); + } + + public DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile) { + return restTemplate.postForEntity(baseURL + "/api/deviceProfile", deviceProfile, DeviceProfile.class).getBody(); + } + + public void deleteDeviceProfile(DeviceProfileId deviceProfileId) { + restTemplate.delete(baseURL + "/api/deviceProfile/{deviceProfileId}", deviceProfileId); + } + + public DeviceProfile setDefaultDeviceProfile(DeviceProfileId deviceProfileId) { + return restTemplate.postForEntity( + baseURL + "/api/deviceProfile/{deviceProfileId}/default", + HttpEntity.EMPTY, DeviceProfile.class, deviceProfileId).getBody(); + } + + public PageData getTenantDevices(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/deviceProfiles?" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public PageData getDeviceProfileInfos(PageLink pageLink, DeviceTransportType deviceTransportType) { + Map params = new HashMap<>(); + params.put("deviceTransportType", deviceTransportType != null ? deviceTransportType.name() : null); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/deviceProfileInfos?deviceTransportType={deviceTransportType}&" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public Long countEntitiesByQuery(EntityCountQuery query) { + return restTemplate.postForObject(baseURL + "/api/entitiesQuery/count", query, Long.class); + } + + public PageData findEntityDataByQuery(EntityDataQuery query) { + return restTemplate.exchange( + baseURL + "/api/entitiesQuery/find", + HttpMethod.POST, new HttpEntity<>(query), + new ParameterizedTypeReference>() { + }).getBody(); + } + + public PageData findAlarmDataByQuery(AlarmDataQuery query) { + return restTemplate.exchange( + baseURL + "/api/alarmsQuery/find", + HttpMethod.POST, new HttpEntity<>(query), + new ParameterizedTypeReference>() { + }).getBody(); + } + public void saveRelation(EntityRelation relation) { restTemplate.postForLocation(baseURL + "/api/relation", relation); } @@ -1313,6 +1515,19 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } } + public Optional getEntityViewInfoById(EntityViewId entityViewId) { + try { + ResponseEntity entityView = restTemplate.getForEntity(baseURL + "/api/entityView/info/{entityViewId}", EntityViewInfo.class, entityViewId); + return Optional.ofNullable(entityView.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + public EntityView saveEntityView(EntityView entityView) { return restTemplate.postForEntity(baseURL + "/api/entityView", entityView, EntityView.class).getBody(); } @@ -1360,7 +1575,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } } - public TextPageData getCustomerEntityViews(CustomerId customerId, String entityViewType, TextPageLink pageLink) { + public PageData getCustomerEntityViews(CustomerId customerId, String entityViewType, PageLink pageLink) { Map params = new HashMap<>(); params.put("customerId", customerId.getId().toString()); params.put("type", entityViewType); @@ -1369,11 +1584,24 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { baseURL + "/api/customer/{customerId}/entityViews?type={type}&" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, - new ParameterizedTypeReference>() { + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public PageData getCustomerEntityViewInfos(CustomerId customerId, String entityViewType, PageLink pageLink) { + Map params = new HashMap<>(); + params.put("customerId", customerId.toString()); + params.put("type", entityViewType); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/customer/{customerId}/entityViewInfos?type={type}&" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { }, params).getBody(); } - public TextPageData getTenantEntityViews(String entityViewType, TextPageLink pageLink) { + public PageData getTenantEntityViews(String entityViewType, PageLink pageLink) { Map params = new HashMap<>(); params.put("type", entityViewType); addPageLinkToParam(params, pageLink); @@ -1381,7 +1609,19 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { baseURL + "/api/tenant/entityViews?type={type}&" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, - new ParameterizedTypeReference>() { + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public PageData getTenantEntityViewInfos(String entityViewType, PageLink pageLink) { + Map params = new HashMap<>(); + params.put("type", entityViewType); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/tenant/entityViewInfos?type={type}&" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { }, params).getBody(); } @@ -1408,37 +1648,70 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } } - public TimePageData getEvents(EntityId entityId, String eventType, TenantId tenantId, TimePageLink pageLink) { + public PageData getEvents(EntityId entityId, String eventType, TenantId tenantId, TimePageLink pageLink) { Map params = new HashMap<>(); params.put("entityType", entityId.getEntityType().name()); params.put("entityId", entityId.getId().toString()); params.put("eventType", eventType); params.put("tenantId", tenantId.getId().toString()); - addPageLinkToParam(params, pageLink); + addTimePageLinkToParam(params, pageLink); return restTemplate.exchange( - baseURL + "/api/events/{entityType}/{entityId}/{eventType}?tenantId={tenantId}&" + getUrlParams(pageLink), + baseURL + "/api/events/{entityType}/{entityId}/{eventType}?tenantId={tenantId}&" + getTimeUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, - new ParameterizedTypeReference>() { + new ParameterizedTypeReference>() { }, params).getBody(); } - public TimePageData getEvents(EntityId entityId, TenantId tenantId, TimePageLink pageLink) { + public PageData getEvents(EntityId entityId, TenantId tenantId, TimePageLink pageLink) { Map params = new HashMap<>(); params.put("entityType", entityId.getEntityType().name()); params.put("entityId", entityId.getId().toString()); params.put("tenantId", tenantId.getId().toString()); - addPageLinkToParam(params, pageLink); + addTimePageLinkToParam(params, pageLink); return restTemplate.exchange( - baseURL + "/api/events/{entityType}/{entityId}?tenantId={tenantId}&" + getUrlParams(pageLink), + baseURL + "/api/events/{entityType}/{entityId}?tenantId={tenantId}&" + getTimeUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, - new ParameterizedTypeReference>() { - }, - params).getBody(); + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public OAuth2ClientRegistrationTemplate saveClientRegistrationTemplate(OAuth2ClientRegistrationTemplate clientRegistrationTemplate) { + return restTemplate.postForEntity(baseURL + "/api/oauth2/config/template", clientRegistrationTemplate, OAuth2ClientRegistrationTemplate.class).getBody(); + } + + public void deleteClientRegistrationTemplate(OAuth2ClientRegistrationTemplateId oAuth2ClientRegistrationTemplateId) { + restTemplate.delete(baseURL + "/api/oauth2/config/template/{clientRegistrationTemplateId}", oAuth2ClientRegistrationTemplateId); + } + + public List getClientRegistrationTemplates() { + return restTemplate.exchange( + baseURL + "/api/oauth2/config/template", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }).getBody(); + } + + public List getOAuth2Clients() { + return restTemplate.exchange( + baseURL + "/api/noauth/oauth2Clients", + HttpMethod.POST, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }).getBody(); + } + + public OAuth2ClientsParams getCurrentOAuth2Params() { + return restTemplate.getForEntity(baseURL + "/api/oauth2/config", OAuth2ClientsParams.class).getBody(); + } + + public OAuth2ClientsParams saveOAuth2Params(OAuth2ClientsParams oauth2Params) { + return restTemplate.postForEntity(baseURL + "/api/oauth2/config", oauth2Params, OAuth2ClientsParams.class).getBody(); } public void handleOneWayDeviceRPCRequest(DeviceId deviceId, JsonNode requestBody) { @@ -1485,6 +1758,10 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { return restTemplate.postForEntity(baseURL + "/api/ruleChain", ruleChain, RuleChain.class).getBody(); } + public RuleChain saveRuleChain(DefaultRuleChainCreateRequest request) { + return restTemplate.postForEntity(baseURL + "/api/ruleChain/device/default", request, RuleChain.class).getBody(); + } + public Optional setRootRuleChain(RuleChainId ruleChainId) { try { ResponseEntity ruleChain = restTemplate.postForEntity(baseURL + "/api/ruleChain/{ruleChainId}/root", null, RuleChain.class, ruleChainId.getId()); @@ -1502,14 +1779,14 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { return restTemplate.postForEntity(baseURL + "/api/ruleChain/metadata", ruleChainMetaData, RuleChainMetaData.class).getBody(); } - public TextPageData getRuleChains(TextPageLink pageLink) { + public PageData getRuleChains(PageLink pageLink) { Map params = new HashMap<>(); addPageLinkToParam(params, pageLink); return restTemplate.exchange( baseURL + "/api/ruleChains?" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, - new ParameterizedTypeReference>() { + new ParameterizedTypeReference>() { }, params).getBody(); } @@ -1544,6 +1821,14 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } } + public RuleChainData exportRuleChains(int limit) { + return restTemplate.getForEntity(baseURL + "/api/ruleChains/export?limit=" + limit, RuleChainData.class).getBody(); + } + + public void importRuleChains(RuleChainData ruleChainData, boolean overwrite) { + restTemplate.postForLocation(baseURL + "/api/ruleChains/import?overwrite=" + overwrite, ruleChainData); + } + public List getAttributeKeys(EntityId entityId) { return restTemplate.exchange( baseURL + "/api/plugins/telemetry/{entityType}/{entityId}/keys/attributes", @@ -1630,22 +1915,42 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { return RestJsonConverter.toTimeseries(timeseries); } + @Deprecated public List getTimeseries(EntityId entityId, List keys, Long interval, Aggregation agg, TimePageLink pageLink) { return getTimeseries(entityId, keys, interval, agg, pageLink, true); } + @Deprecated public List getTimeseries(EntityId entityId, List keys, Long interval, Aggregation agg, TimePageLink pageLink, boolean useStrictDataTypes) { + SortOrder sortOrder = pageLink.getSortOrder(); + return getTimeseries(entityId, keys, interval, agg, sortOrder != null ? sortOrder.getDirection() : null, pageLink.getStartTime(), pageLink.getEndTime(), 100, useStrictDataTypes); + } + + public List getTimeseries(EntityId entityId, List keys, Long interval, Aggregation agg, SortOrder.Direction sortOrder, Long startTime, Long endTime, Integer limit, boolean useStrictDataTypes) { Map params = new HashMap<>(); params.put("entityType", entityId.getEntityType().name()); params.put("entityId", entityId.getId().toString()); params.put("keys", listToString(keys)); params.put("interval", interval == null ? "0" : interval.toString()); params.put("agg", agg == null ? "NONE" : agg.name()); + params.put("limit", limit != null ? limit.toString() : "100"); + params.put("orderBy", sortOrder != null ? sortOrder.name() : "DESC"); params.put("useStrictDataTypes", Boolean.toString(useStrictDataTypes)); - addPageLinkToParam(params, pageLink); + + StringBuilder urlBuilder = new StringBuilder(baseURL); + urlBuilder.append("/api/plugins/telemetry/{entityType}/{entityId}/values/timeseries?keys={keys}&interval={interval}&agg={agg}&useStrictDataTypes={useStrictDataTypes}&orderBy={orderBy}"); + + if (startTime != null) { + urlBuilder.append("&startTs={startTs}"); + params.put("startTs", String.valueOf(startTime)); + } + if (endTime != null) { + urlBuilder.append("&endTs={endTs}"); + params.put("endTs", String.valueOf(endTime)); + } Map> timeseries = restTemplate.exchange( - baseURL + "/api/plugins/telemetry/{entityType}/{entityId}/values/timeseries?keys={keys}&interval={interval}&agg={agg}&useStrictDataTypes={useStrictDataTypes}&" + getUrlParamsTs(pageLink), + urlBuilder.toString(), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>>() { @@ -1785,6 +2090,19 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } } + public Optional getTenantInfoById(TenantId tenantId) { + try { + ResponseEntity tenant = restTemplate.getForEntity(baseURL + "/api/tenant/info/{tenantId}", TenantInfo.class, tenantId); + return Optional.ofNullable(tenant.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + public Tenant saveTenant(Tenant tenant) { return restTemplate.postForEntity(baseURL + "/api/tenant", tenant, Tenant.class).getBody(); } @@ -1793,14 +2111,89 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { restTemplate.delete(baseURL + "/api/tenant/{tenantId}", tenantId.getId()); } - public TextPageData getTenants(TextPageLink pageLink) { + public PageData getTenants(PageLink pageLink) { Map params = new HashMap<>(); addPageLinkToParam(params, pageLink); return restTemplate.exchange( baseURL + "/api/tenants?" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, - new ParameterizedTypeReference>() { + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public PageData getTenantInfos(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/tenantInfos?" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public Optional getTenantProfileById(TenantProfileId tenantProfileId) { + try { + ResponseEntity tenantProfile = restTemplate.getForEntity(baseURL + "/api/tenantProfile/{tenantProfileId}", TenantProfile.class, tenantProfileId); + return Optional.ofNullable(tenantProfile.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional getTenantProfileInfoById(TenantProfileId tenantProfileId) { + try { + ResponseEntity entityInfo = restTemplate.getForEntity(baseURL + "/api/tenantProfileInfo/{tenantProfileId}", EntityInfo.class, tenantProfileId); + return Optional.ofNullable(entityInfo.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public EntityInfo getDefaultTenantProfileInfo() { + return restTemplate.getForEntity(baseURL + "/api/tenantProfileInfo/default", EntityInfo.class).getBody(); + } + + public TenantProfile saveTenantProfile(TenantProfile tenantProfile) { + return restTemplate.postForEntity(baseURL + "/api/tenantProfile", tenantProfile, TenantProfile.class).getBody(); + } + + public void deleteTenantProfile(TenantProfileId tenantProfileId) { + restTemplate.delete(baseURL + "/api/tenantProfile/{tenantProfileId}", tenantProfileId); + } + + public TenantProfile setDefaultTenantProfile(TenantProfileId tenantProfileId) { + return restTemplate.exchange(baseURL + "/api/tenantProfile/{tenantProfileId}/default", HttpMethod.POST, HttpEntity.EMPTY, TenantProfile.class, tenantProfileId).getBody(); + } + + public PageData getTenantProfiles(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/tenantProfiles" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public PageData getTenantProfileInfos(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/tenantProfileInfos" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { }, params).getBody(); } @@ -1850,7 +2243,18 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { restTemplate.delete(baseURL + "/api/user/{userId}", userId.getId()); } - public TextPageData getTenantAdmins(TenantId tenantId, TextPageLink pageLink) { + public PageData getUsers(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/users" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public PageData getTenantAdmins(TenantId tenantId, PageLink pageLink) { Map params = new HashMap<>(); params.put("tenantId", tenantId.getId().toString()); addPageLinkToParam(params, pageLink); @@ -1859,11 +2263,11 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { baseURL + "/api/tenant/{tenantId}/users?" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, - new ParameterizedTypeReference>() { + new ParameterizedTypeReference>() { }, params).getBody(); } - public TextPageData getCustomerUsers(CustomerId customerId, TextPageLink pageLink) { + public PageData getCustomerUsers(CustomerId customerId, PageLink pageLink) { Map params = new HashMap<>(); params.put("customerId", customerId.getId().toString()); addPageLinkToParam(params, pageLink); @@ -1872,7 +2276,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { baseURL + "/api/customer/{customerId}/users?" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, - new ParameterizedTypeReference>() { + new ParameterizedTypeReference>() { }, params).getBody(); } @@ -1906,14 +2310,14 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { restTemplate.delete(baseURL + "/api/widgetsBundle/{widgetsBundleId}", widgetsBundleId.getId()); } - public TextPageData getWidgetsBundles(TextPageLink pageLink) { + public PageData getWidgetsBundles(PageLink pageLink) { Map params = new HashMap<>(); addPageLinkToParam(params, pageLink); return restTemplate.exchange( baseURL + "/api/widgetsBundles?" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, - new ParameterizedTypeReference>() { + new ParameterizedTypeReference>() { }, params).getBody(); } @@ -1996,68 +2400,47 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } } - private String getUrlParams(TimePageLink pageLink) { - return getUrlParams(pageLink, "startTime", "endTime"); - } - - private String getUrlParamsTs(TimePageLink pageLink) { - return getUrlParams(pageLink, "startTs", "endTs"); - } - - private String getUrlParams(TimePageLink pageLink, String startTime, String endTime) { + private String getTimeUrlParams(TimePageLink pageLink) { String urlParams = "limit={limit}&ascOrder={ascOrder}"; if (pageLink.getStartTime() != null) { - urlParams += "&" + startTime + "={startTime}"; + urlParams += "&startTime={startTime}"; } if (pageLink.getEndTime() != null) { - urlParams += "&" + endTime + "={endTime}"; - } - if (pageLink.getIdOffset() != null) { - urlParams += "&offset={offset}"; + urlParams += "&endTime={endTime}"; } return urlParams; } - private String getUrlParams(TextPageLink pageLink) { - String urlParams = "limit={limit}"; + private String getUrlParams(PageLink pageLink) { + String urlParams = "pageSize={pageSize}&page={page}"; if (!isEmpty(pageLink.getTextSearch())) { urlParams += "&textSearch={textSearch}"; } - if (!isEmpty(pageLink.getIdOffset())) { - urlParams += "&idOffset={idOffset}"; - } - if (!isEmpty(pageLink.getTextOffset())) { - urlParams += "&textOffset={textOffset}"; + if (pageLink.getSortOrder() != null) { + urlParams += "&sortProperty={sortProperty}&sortOrder={sortOrder}"; } return urlParams; } - private void addPageLinkToParam(Map params, TimePageLink pageLink) { - params.put("limit", String.valueOf(pageLink.getLimit())); + private void addTimePageLinkToParam(Map params, TimePageLink pageLink) { + this.addPageLinkToParam(params, pageLink); if (pageLink.getStartTime() != null) { params.put("startTime", String.valueOf(pageLink.getStartTime())); } if (pageLink.getEndTime() != null) { params.put("endTime", String.valueOf(pageLink.getEndTime())); } - params.put("ascOrder", String.valueOf(pageLink.isAscOrder())); - if (pageLink.getIdOffset() != null) { - params.put("offset", pageLink.getIdOffset().toString()); - } } - private void addPageLinkToParam(Map params, TextPageLink pageLink) { - params.put("limit", String.valueOf(pageLink.getLimit())); - if (pageLink.getTextSearch() != null) { + private void addPageLinkToParam(Map params, PageLink pageLink) { + params.put("pageSize", String.valueOf(pageLink.getPageSize())); + params.put("page", String.valueOf(pageLink.getPage())); + if (!isEmpty(pageLink.getTextSearch())) { params.put("textSearch", pageLink.getTextSearch()); } - - if (pageLink.getIdOffset() != null) { - params.put("idOffset", pageLink.getIdOffset().toString()); - - } - if (pageLink.getTextOffset() != null) { - params.put("textOffset", pageLink.getTextOffset()); + if (pageLink.getSortOrder() != null) { + params.put("sortProperty", pageLink.getSortOrder().getProperty()); + params.put("sortOrder", pageLink.getSortOrder().getDirection().name()); } } diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml index 00d445cc3c..197c4e500d 100644 --- a/rule-engine/pom.xml +++ b/rule-engine/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT thingsboard rule-engine diff --git a/rule-engine/rule-engine-api/pom.xml b/rule-engine/rule-engine-api/pom.xml index 2c1e323165..1a4c0b8506 100644 --- a/rule-engine/rule-engine-api/pom.xml +++ b/rule-engine/rule-engine-api/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT rule-engine org.thingsboard.rule-engine @@ -74,18 +74,8 @@ provided - com.datastax.cassandra - cassandra-driver-core - provided - - - com.datastax.cassandra - cassandra-driver-mapping - provided - - - com.datastax.cassandra - cassandra-driver-extras + com.datastax.oss + java-driver-core provided @@ -99,4 +89,28 @@ provided + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + false + + + + diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java new file mode 100644 index 0000000000..719a9f40e5 --- /dev/null +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java @@ -0,0 +1,65 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.api; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmQuery; +import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AlarmData; +import org.thingsboard.server.common.data.query.AlarmDataPageLink; +import org.thingsboard.server.common.data.query.AlarmDataQuery; + +import java.util.Collection; +import java.util.List; + +/** + * Created by ashvayka on 02.04.18. + */ +public interface RuleEngineAlarmService { + + Alarm createOrUpdateAlarm(Alarm alarm); + + Boolean deleteAlarm(TenantId tenantId, AlarmId alarmId); + + ListenableFuture ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTs); + + ListenableFuture clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs); + + ListenableFuture findAlarmByIdAsync(TenantId tenantId, AlarmId alarmId); + + ListenableFuture findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type); + + ListenableFuture findAlarmInfoByIdAsync(TenantId tenantId, AlarmId alarmId); + + ListenableFuture> findAlarms(TenantId tenantId, AlarmQuery query); + + AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus); + + PageData findAlarmDataByQueryForEntities(TenantId tenantId, CustomerId customerId, AlarmDataQuery query, Collection orderedEntityIds); +} diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceProfileCache.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceProfileCache.java new file mode 100644 index 0000000000..b8e291ca74 --- /dev/null +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceProfileCache.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.api; + +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * Created by ashvayka on 02.04.18. + */ +public interface RuleEngineDeviceProfileCache { + + DeviceProfile get(TenantId tenantId, DeviceProfileId deviceProfileId); + + DeviceProfile get(TenantId tenantId, DeviceId deviceId); + + void addListener(TenantId tenantId, EntityId listenerId, Consumer profileListener, BiConsumer devicelistener); + + void removeListener(TenantId tenantId, EntityId listenerId); + +} diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java index 5b54370e49..ffef48d590 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java @@ -16,14 +16,13 @@ package org.thingsboard.rule.engine.api; import com.google.common.util.concurrent.FutureCallback; -import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import java.util.Collection; import java.util.List; -import java.util.Set; /** * Created by ashvayka on 02.04.18. @@ -36,7 +35,9 @@ public interface RuleEngineTelemetryService { void saveAndNotify(TenantId tenantId, EntityId entityId, String scope, List attributes, FutureCallback callback); - void saveAndNotify(TenantId tenantId, EntityId entityId, String scope, List attributes, FutureCallback callback, boolean notifyDevice); + void saveAndNotify(TenantId tenantId, EntityId entityId, String scope, List attributes, boolean notifyDevice, FutureCallback callback); + + void saveLatestAndNotify(TenantId tenantId, EntityId entityId, List ts, FutureCallback callback); void saveAttrAndNotify(TenantId tenantId, EntityId entityId, String scope, String key, long value, FutureCallback callback); @@ -46,4 +47,11 @@ public interface RuleEngineTelemetryService { void saveAttrAndNotify(TenantId tenantId, EntityId entityId, String scope, String key, boolean value, FutureCallback callback); + void deleteAndNotify(TenantId tenantId, EntityId entityId, String scope, List keys, FutureCallback callback); + + void deleteLatest(TenantId tenantId, EntityId entityId, List keys, FutureCallback callback); + + void deleteAllLatest(TenantId tenantId, EntityId entityId, FutureCallback> callback); + + } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index 006fb1351b..081a84eef0 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -15,20 +15,23 @@ */ package org.thingsboard.rule.engine.api; -import com.datastax.driver.core.ResultSetFuture; import io.netty.channel.EventLoopGroup; import org.springframework.data.redis.core.RedisTemplate; import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.cassandra.CassandraCluster; @@ -37,6 +40,7 @@ import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.nosql.CassandraStatementTask; +import org.thingsboard.server.dao.nosql.TbResultSetFuture; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantService; @@ -44,6 +48,7 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.user.UserService; import java.util.Set; +import java.util.function.BiConsumer; import java.util.function.Consumer; /** @@ -169,7 +174,7 @@ public interface TbContext { DashboardService getDashboardService(); - AlarmService getAlarmService(); + RuleEngineAlarmService getAlarmService(); RuleChainService getRuleChainService(); @@ -183,6 +188,8 @@ public interface TbContext { EntityViewService getEntityViewService(); + RuleEngineDeviceProfileCache getDeviceProfileCache(); + ListeningExecutor getJsExecutor(); ListeningExecutor getMailExecutor(); @@ -207,9 +214,22 @@ public interface TbContext { CassandraCluster getCassandraCluster(); - ResultSetFuture submitCassandraTask(CassandraStatementTask task); + TbResultSetFuture submitCassandraTask(CassandraStatementTask task); @Deprecated RedisTemplate getRedisTemplate(); + PageData findRuleNodeStates(PageLink pageLink); + + RuleNodeState findRuleNodeStateForEntity(EntityId entityId); + + void removeRuleNodeStateForEntity(EntityId entityId); + + RuleNodeState saveRuleNodeState(RuleNodeState state); + + void clearRuleNodeStates(); + + void addDeviceProfileListeners(Consumer listener, BiConsumer deviceListener); + + void removeProfileListener(); } diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index 00d8101e5a..c4d2b66d15 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT rule-engine org.thingsboard.rule-engine @@ -134,6 +134,10 @@ org.slf4j slf4j-log4j12 + + org.hibernate + hibernate-validator + test diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java index 6ed5a05589..def7afecd2 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java @@ -26,9 +26,9 @@ import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.DataConstants; -import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; import static org.thingsboard.common.util.DonAsynchron.withCallback; @@ -38,10 +38,6 @@ public abstract class TbAbstractAlarmNode ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor()); } - protected abstract ListenableFuture processAlarm(TbContext ctx, TbMsg msg); + protected abstract ListenableFuture processAlarm(TbContext ctx, TbMsg msg); protected ListenableFuture buildAlarmDetails(TbContext ctx, TbMsg msg, JsonNode previousDetails) { try { @@ -90,21 +86,20 @@ public abstract class TbAbstractAlarmNode ctx.tellNext(toAlarmMsg(ctx, alarmResult, msg), alarmResultMsgType), + private void tellNext(TbContext ctx, TbMsg msg, TbAlarmResult alarmResult, String entityAction, String alarmAction) { + ctx.enqueue(ctx.alarmActionMsg(alarmResult.alarm, ctx.getSelfId(), entityAction), + () -> ctx.tellNext(toAlarmMsg(ctx, alarmResult, msg), alarmAction), throwable -> ctx.tellFailure(toAlarmMsg(ctx, alarmResult, msg), throwable)); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java index 46e4e3e396..ca8aeb9066 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java @@ -35,11 +35,12 @@ import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; -import org.thingsboard.server.common.data.page.TextPageData; -import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationTypeGroup; @@ -49,6 +50,7 @@ import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.server.dao.user.UserService; import java.util.List; import java.util.Optional; @@ -238,13 +240,20 @@ public abstract class TbAbstractRelationActionNode dashboardInfoTextPageData = dashboardService.findDashboardsByTenantId(ctx.getTenantId(), new TextPageLink(200, entitykey.getEntityName())); + PageData dashboardInfoTextPageData = dashboardService.findDashboardsByTenantId(ctx.getTenantId(), new PageLink(200, 0, entitykey.getEntityName())); for (DashboardInfo dashboardInfo : dashboardInfoTextPageData.getData()) { if (dashboardInfo.getTitle().equals(entitykey.getEntityName())) { targetEntity.setEntityId(dashboardInfo.getId()); } } break; + case USER: + UserService userService = ctx.getUserService(); + User user = userService.findUserByEmail(ctx.getTenantId(), entitykey.getEntityName()); + if(user != null){ + targetEntity.setEntityId(user.getId()); + } + break; default: return targetEntity; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmResult.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmResult.java new file mode 100644 index 0000000000..dba96be4a6 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmResult.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.action; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.thingsboard.server.common.data.alarm.Alarm; + +@Data +@AllArgsConstructor +public class TbAlarmResult { + boolean isCreated; + boolean isUpdated; + boolean isSeverityUpdated; + boolean isCleared; + Alarm alarm; + + public TbAlarmResult(boolean isCreated, boolean isUpdated, boolean isCleared, Alarm alarm) { + this.isCreated = isCreated; + this.isUpdated = isUpdated; + this.isCleared = isCleared; + this.alarm = alarm; + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java index 30bf2d4d6e..67e9063ac8 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java @@ -55,7 +55,7 @@ public class TbClearAlarmNode extends TbAbstractAlarmNode processAlarm(TbContext ctx, TbMsg msg) { + protected ListenableFuture processAlarm(TbContext ctx, TbMsg msg) { String alarmType = TbNodeUtils.processPattern(this.config.getAlarmType(), msg.getMetaData()); ListenableFuture alarmFuture; if (msg.getOriginator().getEntityType().equals(EntityType.ALARM)) { @@ -67,11 +67,11 @@ public class TbClearAlarmNode extends TbAbstractAlarmNode clearAlarm(TbContext ctx, TbMsg msg, Alarm alarm) { + private ListenableFuture clearAlarm(TbContext ctx, TbMsg msg, Alarm alarm) { ctx.logJsEvalRequest(); ListenableFuture asyncDetails = buildAlarmDetails(ctx, msg, alarm.getDetails()); return Futures.transformAsync(asyncDetails, details -> { @@ -86,7 +86,7 @@ public class TbClearAlarmNode extends TbAbstractAlarmNode processAlarm(TbContext ctx, TbMsg msg) { + protected ListenableFuture processAlarm(TbContext ctx, TbMsg msg) { String alarmType; final Alarm msgAlarm; @@ -106,7 +106,7 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode createNewAlarm(TbContext ctx, TbMsg msg, Alarm msgAlarm) { + private ListenableFuture createNewAlarm(TbContext ctx, TbMsg msg, Alarm msgAlarm) { ListenableFuture asyncAlarm; if (msgAlarm != null) { asyncAlarm = Futures.immediateFuture(msgAlarm); @@ -120,10 +120,10 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode asyncCreated = Futures.transform(asyncAlarm, alarm -> ctx.getAlarmService().createOrUpdateAlarm(alarm), ctx.getDbCallbackExecutor()); - return Futures.transform(asyncCreated, alarm -> new AlarmResult(true, false, false, alarm), MoreExecutors.directExecutor()); + return Futures.transform(asyncCreated, alarm -> new TbAlarmResult(true, false, false, alarm), MoreExecutors.directExecutor()); } - private ListenableFuture updateAlarm(TbContext ctx, TbMsg msg, Alarm existingAlarm, Alarm msgAlarm) { + private ListenableFuture updateAlarm(TbContext ctx, TbMsg msg, Alarm existingAlarm, Alarm msgAlarm) { ctx.logJsEvalRequest(); ListenableFuture asyncUpdated = Futures.transform(buildAlarmDetails(ctx, msg, existingAlarm.getDetails()), (Function) details -> { ctx.logJsEvalResponse(); @@ -141,7 +141,7 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode new AlarmResult(false, true, false, a), MoreExecutors.directExecutor()); + return Futures.transform(asyncUpdated, a -> new TbAlarmResult(false, true, false, a), MoreExecutors.directExecutor()); } private Alarm buildAlarm(TbMsg msg, JsonNode details, TenantId tenantId) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java index c151d4adb2..d2daf9870c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java @@ -15,7 +15,6 @@ */ package org.thingsboard.rule.engine.action; -import com.datastax.driver.core.utils.UUIDs; import com.google.gson.Gson; import com.google.gson.JsonObject; import lombok.extern.slf4j.Slf4j; @@ -100,4 +99,4 @@ public class TbMsgCountNode implements TbNode { @Override public void destroy() { } -} \ No newline at end of file +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java index aa7bfa81d1..03c95202b1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java @@ -15,16 +15,12 @@ */ package org.thingsboard.rule.engine.action; -import com.datastax.driver.core.BoundStatement; -import com.datastax.driver.core.CodecRegistry; -import com.datastax.driver.core.ConsistencyLevel; -import com.datastax.driver.core.PreparedStatement; -import com.datastax.driver.core.ResultSet; -import com.datastax.driver.core.ResultSetFuture; -import com.datastax.driver.core.Session; -import com.datastax.driver.core.Statement; -import com.datastax.driver.core.TypeCodec; -import com.datastax.driver.core.exceptions.CodecNotFoundException; +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.BoundStatement; +import com.datastax.oss.driver.api.core.cql.BoundStatementBuilder; +import com.datastax.oss.driver.api.core.cql.PreparedStatement; +import com.datastax.oss.driver.api.core.cql.Statement; import com.google.common.base.Function; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -33,34 +29,23 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.rule.engine.api.RuleNode; -import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.rule.engine.api.TbNode; -import org.thingsboard.rule.engine.api.TbNodeConfiguration; -import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.api.*; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.dao.cassandra.CassandraCluster; -import org.thingsboard.server.dao.model.type.AuthorityCodec; -import org.thingsboard.server.dao.model.type.ComponentLifecycleStateCodec; -import org.thingsboard.server.dao.model.type.ComponentScopeCodec; -import org.thingsboard.server.dao.model.type.ComponentTypeCodec; -import org.thingsboard.server.dao.model.type.DeviceCredentialsTypeCodec; -import org.thingsboard.server.dao.model.type.EntityTypeCodec; -import org.thingsboard.server.dao.model.type.JsonCodec; +import org.thingsboard.server.dao.cassandra.guava.GuavaSession; import org.thingsboard.server.dao.nosql.CassandraStatementTask; +import org.thingsboard.server.dao.nosql.TbResultSetFuture; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; -import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; import static org.thingsboard.common.util.DonAsynchron.withCallback; @Slf4j @@ -85,7 +70,7 @@ public class TbSaveToCustomCassandraTableNode implements TbNode { private static final String ENTITY_ID = "$entityId"; private TbSaveToCustomCassandraTableNodeConfiguration config; - private Session session; + private GuavaSession session; private CassandraCluster cassandraCluster; private ConsistencyLevel defaultWriteLevel; private PreparedStatement saveStmt; @@ -129,31 +114,14 @@ public class TbSaveToCustomCassandraTableNode implements TbNode { return getSession().prepare(query); } - private Session getSession() { + private GuavaSession getSession() { if (session == null) { session = cassandraCluster.getSession(); defaultWriteLevel = cassandraCluster.getDefaultWriteConsistencyLevel(); - CodecRegistry registry = session.getCluster().getConfiguration().getCodecRegistry(); - registerCodecIfNotFound(registry, new JsonCodec()); - registerCodecIfNotFound(registry, new DeviceCredentialsTypeCodec()); - registerCodecIfNotFound(registry, new AuthorityCodec()); - registerCodecIfNotFound(registry, new ComponentLifecycleStateCodec()); - registerCodecIfNotFound(registry, new ComponentTypeCodec()); - registerCodecIfNotFound(registry, new ComponentScopeCodec()); - registerCodecIfNotFound(registry, new EntityTypeCodec()); } return session; } - private void registerCodecIfNotFound(CodecRegistry registry, TypeCodec codec) { - try { - registry.codecFor(codec.getCqlType(), codec.getJavaType()); - } catch (CodecNotFoundException e) { - registry.register(codec); - } - } - - private PreparedStatement getSaveStmt() { fieldsMap = config.getFieldsMapping(); if (fieldsMap.isEmpty()) { @@ -199,22 +167,22 @@ public class TbSaveToCustomCassandraTableNode implements TbNode { throw new IllegalStateException("Invalid message structure, it is not a JSON Object:" + data); } else { JsonObject dataAsObject = data.getAsJsonObject(); - BoundStatement stmt = saveStmt.bind(); + BoundStatementBuilder stmtBuilder = new BoundStatementBuilder(saveStmt.bind()); AtomicInteger i = new AtomicInteger(0); fieldsMap.forEach((key, value) -> { if (key.equals(ENTITY_ID)) { - stmt.setUUID(i.get(), msg.getOriginator().getId()); + stmtBuilder.setUuid(i.get(), msg.getOriginator().getId()); } else if (dataAsObject.has(key)) { if (dataAsObject.get(key).isJsonPrimitive()) { JsonPrimitive primitive = dataAsObject.get(key).getAsJsonPrimitive(); if (primitive.isNumber()) { - stmt.setLong(i.get(), dataAsObject.get(key).getAsLong()); + stmtBuilder.setLong(i.get(), dataAsObject.get(key).getAsLong()); } else if (primitive.isBoolean()) { - stmt.setBool(i.get(), dataAsObject.get(key).getAsBoolean()); + stmtBuilder.setBoolean(i.get(), dataAsObject.get(key).getAsBoolean()); } else if (primitive.isString()) { - stmt.setString(i.get(), dataAsObject.get(key).getAsString()); + stmtBuilder.setString(i.get(), dataAsObject.get(key).getAsString()); } else { - stmt.setToNull(i.get()); + stmtBuilder.setToNull(i.get()); } } else { throw new IllegalStateException("Message data key: '" + key + "' with value: '" + value + "' is not a JSON Primitive!"); @@ -224,15 +192,15 @@ public class TbSaveToCustomCassandraTableNode implements TbNode { } i.getAndIncrement(); }); - return getFuture(executeAsyncWrite(ctx, stmt), rs -> null); + return getFuture(executeAsyncWrite(ctx, stmtBuilder.build()), rs -> null); } } - private ResultSetFuture executeAsyncWrite(TbContext ctx, Statement statement) { + private TbResultSetFuture executeAsyncWrite(TbContext ctx, Statement statement) { return executeAsync(ctx, statement, defaultWriteLevel); } - private ResultSetFuture executeAsync(TbContext ctx, Statement statement, ConsistencyLevel level) { + private TbResultSetFuture executeAsync(TbContext ctx, Statement statement, ConsistencyLevel level) { if (log.isDebugEnabled()) { log.debug("Execute cassandra async statement {}", statementToString(statement)); } @@ -244,20 +212,20 @@ public class TbSaveToCustomCassandraTableNode implements TbNode { private static String statementToString(Statement statement) { if (statement instanceof BoundStatement) { - return ((BoundStatement) statement).preparedStatement().getQueryString(); + return ((BoundStatement) statement).getPreparedStatement().getQuery(); } else { return statement.toString(); } } - private ListenableFuture getFuture(ResultSetFuture future, java.util.function.Function transformer) { - return Futures.transform(future, new Function() { + private ListenableFuture getFuture(TbResultSetFuture future, java.util.function.Function transformer) { + return Futures.transform(future, new Function() { @Nullable @Override - public T apply(@Nullable ResultSet input) { + public T apply(@Nullable AsyncResultSet input) { return transformer.apply(input); } }, readResultsProcessingExecutor); } -} \ No newline at end of file +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java index 7b917b9b20..20e0fa9881 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java @@ -42,7 +42,7 @@ import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; nodeDescription = "Periodically generates messages", nodeDetails = "Generates messages with configurable period. Javascript function used for message generation.", inEnabled = false, - uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"}, + uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbActionNodeGeneratorConfig", icon = "repeat" ) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java index 3799e60f73..e5e84ba0b6 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java @@ -32,7 +32,7 @@ import org.thingsboard.server.common.msg.TbMsg; relationTypes = {"True", "False"}, nodeDescription = "Filter incoming messages by Message Type", nodeDetails = "If incoming MessageType is expected - send Message via True chain, otherwise False chain is used.", - uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"}, + uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbFilterNodeMessageTypeConfig") public class TbMsgTypeFilterNode implements TbNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java index de3a48142e..5dd11c9318 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java @@ -30,7 +30,7 @@ import org.thingsboard.server.common.msg.TbMsg; relationTypes = {"True", "False"}, nodeDescription = "Filter incoming messages by message Originator Type", nodeDetails = "If Originator Type of incoming message is expected - send Message via True chain, otherwise False chain is used.", - uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"}, + uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbFilterNodeOriginatorTypeConfig") public class TbOriginatorTypeFilterNode implements TbNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java index 20ee85d133..82764790a0 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java @@ -27,7 +27,7 @@ import org.thingsboard.server.common.msg.TbMsg; type = ComponentType.FILTER, name = "originator type switch", configClazz = EmptyNodeConfiguration.class, - relationTypes = {"Device", "Asset", "Entity View", "Tenant", "Customer", "User", "Dashboard", "Rule chain", "Rule node"}, + relationTypes = {"Device", "Asset", "Alarm", "Entity View", "Tenant", "Customer", "User", "Dashboard", "Rule chain", "Rule node"}, nodeDescription = "Route incoming messages by Message Originator Type", nodeDetails = "Routes messages to chain according to the originator type ('Device', 'Asset', etc.).", uiResources = {"static/rulenode/rulenode-core-config.js"}, @@ -73,6 +73,9 @@ public class TbOriginatorTypeSwitchNode implements TbNode { case RULE_NODE: relationType = "Rule node"; break; + case ALARM: + relationType = "Alarm"; + break; default: throw new TbNodeException("Unsupported originator type: " + originatorType); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java index 7fb51bc00d..f1c2b57fe6 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java @@ -32,7 +32,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType; "If Latest Telemetry enrichment configured, latest telemetry added into metadata. " + "To access those attributes in other nodes this template can be used " + "metadata.temperature.", - uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"}, + uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbEnrichmentNodeCustomerAttributesConfig") public class TbGetCustomerAttributeNode extends TbEntityGetAttrNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java index 639659fd4b..7c9c7c730e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java @@ -34,7 +34,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType; "If Latest Telemetry enrichment configured, latest telemetry added into metadata. " + "To access those attributes in other nodes this template can be used " + "metadata.temperature.", - uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"}, + uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbEnrichmentNodeRelatedAttributesConfig") public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java index 3b69f0eb04..e5e4cb5667 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java @@ -34,7 +34,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType; "If Latest Telemetry enrichment configured, latest telemetry added into metadata. " + "To access those attributes in other nodes this template can be used " + "metadata.temperature.", - uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"}, + uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbEnrichmentNodeTenantAttributesConfig") public class TbGetTenantAttributeNode extends TbEntityGetAttrNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java index f8e63b8674..1f82a0dc81 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java @@ -44,7 +44,7 @@ import java.util.concurrent.TimeoutException; configClazz = TbMqttNodeConfiguration.class, nodeDescription = "Publish messages to the MQTT broker", nodeDetails = "Will publish message payload to the MQTT broker with QoS AT_LEAST_ONCE.", - uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"}, + uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbActionNodeMqttConfig", icon = "call_split" ) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java index 7701f45a3d..4ab01a864d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java @@ -40,7 +40,7 @@ import java.util.Optional; configClazz = TbAzureIotHubNodeConfiguration.class, nodeDescription = "Publish messages to the Azure IoT Hub", nodeDetails = "Will publish message payload to the Azure IoT Hub with QoS AT_LEAST_ONCE.", - uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"}, + uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbActionNodeAzureIotHubConfig" ) public class TbAzureIotHubNode extends TbMqttNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/CertPemClientCredentials.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/CertPemClientCredentials.java index 42992a6485..3dcfc2dff8 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/CertPemClientCredentials.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/CertPemClientCredentials.java @@ -38,6 +38,7 @@ import javax.crypto.spec.PBEKeySpec; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.TrustManagerFactory; import java.io.ByteArrayInputStream; +import java.io.InputStream; import java.security.AlgorithmParameters; import java.security.Key; import java.security.KeyFactory; @@ -142,7 +143,9 @@ public class CertPemClientCredentials implements MqttClientCredentials { .replaceAll("\\s", ""); byte[] decoded = Base64.decodeBase64(fileContent); CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); - certificate = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(decoded)); + try (InputStream inStream = new ByteArrayInputStream(decoded)) { + certificate = (X509Certificate) certFactory.generateCertificate(inStream); + } } return certificate; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmEvalResult.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmEvalResult.java new file mode 100644 index 0000000000..7d510fe5fe --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmEvalResult.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.profile; + +public enum AlarmEvalResult { + + FALSE, NOT_YET_TRUE, TRUE; + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java new file mode 100644 index 0000000000..b1eeb82b15 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java @@ -0,0 +1,469 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.profile; + +import lombok.Data; +import org.thingsboard.rule.engine.profile.state.PersistedAlarmRuleState; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.device.profile.AlarmCondition; +import org.thingsboard.server.common.data.device.profile.AlarmConditionSpec; +import org.thingsboard.server.common.data.device.profile.AlarmRule; +import org.thingsboard.server.common.data.device.profile.CustomTimeSchedule; +import org.thingsboard.server.common.data.device.profile.CustomTimeScheduleItem; +import org.thingsboard.server.common.data.device.profile.DurationAlarmConditionSpec; +import org.thingsboard.server.common.data.device.profile.RepeatingAlarmConditionSpec; +import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec; +import org.thingsboard.server.common.data.device.profile.SpecificTimeSchedule; +import org.thingsboard.server.common.data.query.BooleanFilterPredicate; +import org.thingsboard.server.common.data.query.ComplexFilterPredicate; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.KeyFilterPredicate; +import org.thingsboard.server.common.data.query.NumericFilterPredicate; +import org.thingsboard.server.common.data.query.StringFilterPredicate; +import org.thingsboard.server.common.msg.tools.SchedulerUtils; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Set; +import java.util.function.Function; + +@Data +class AlarmRuleState { + + private final AlarmSeverity severity; + private final AlarmRule alarmRule; + private final AlarmConditionSpec spec; + private final long requiredDurationInMs; + private final long requiredRepeats; + private final Set entityKeys; + private PersistedAlarmRuleState state; + private boolean updateFlag; + + AlarmRuleState(AlarmSeverity severity, AlarmRule alarmRule, Set entityKeys, PersistedAlarmRuleState state) { + this.severity = severity; + this.alarmRule = alarmRule; + this.entityKeys = entityKeys; + if (state != null) { + this.state = state; + } else { + this.state = new PersistedAlarmRuleState(0L, 0L, 0L); + } + this.spec = getSpec(alarmRule); + long requiredDurationInMs = 0; + long requiredRepeats = 0; + switch (spec.getType()) { + case DURATION: + DurationAlarmConditionSpec duration = (DurationAlarmConditionSpec) spec; + requiredDurationInMs = duration.getUnit().toMillis(duration.getValue()); + break; + case REPEATING: + RepeatingAlarmConditionSpec repeating = (RepeatingAlarmConditionSpec) spec; + requiredRepeats = repeating.getCount(); + break; + } + this.requiredDurationInMs = requiredDurationInMs; + this.requiredRepeats = requiredRepeats; + } + + public boolean validateTsUpdate(Set changedKeys) { + for (EntityKey key : changedKeys) { + if (entityKeys.contains(key)) { + return true; + } + } + return false; + } + + public boolean validateAttrUpdate(Set changedKeys) { + //If the attribute was updated, but no new telemetry arrived - we ignore this until new telemetry is there. + for (EntityKey key : entityKeys) { + if (key.getType().equals(EntityKeyType.TIME_SERIES)) { + return false; + } + } + for (EntityKey key : changedKeys) { + EntityKeyType keyType = key.getType(); + if (EntityKeyType.CLIENT_ATTRIBUTE.equals(keyType) || EntityKeyType.SERVER_ATTRIBUTE.equals(keyType) || EntityKeyType.SHARED_ATTRIBUTE.equals(keyType)) { + key = new EntityKey(EntityKeyType.ATTRIBUTE, key.getKey()); + } + if (entityKeys.contains(key)) { + return true; + } + } + return false; + } + + public AlarmConditionSpec getSpec(AlarmRule alarmRule) { + AlarmConditionSpec spec = alarmRule.getCondition().getSpec(); + if (spec == null) { + spec = new SimpleAlarmConditionSpec(); + } + return spec; + } + + public boolean checkUpdate() { + if (updateFlag) { + updateFlag = false; + return true; + } else { + return false; + } + } + + public AlarmEvalResult eval(DataSnapshot data) { + boolean active = isActive(data.getTs()); + switch (spec.getType()) { + case SIMPLE: + return (active && eval(alarmRule.getCondition(), data)) ? AlarmEvalResult.TRUE : AlarmEvalResult.FALSE; + case DURATION: + return evalDuration(data, active); + case REPEATING: + return evalRepeating(data, active); + default: + return AlarmEvalResult.FALSE; + } + } + + private boolean isActive(long eventTs) { + if (eventTs == 0L) { + eventTs = System.currentTimeMillis(); + } + if (alarmRule.getSchedule() == null) { + return true; + } + switch (alarmRule.getSchedule().getType()) { + case ANY_TIME: + return true; + case SPECIFIC_TIME: + return isActiveSpecific((SpecificTimeSchedule) alarmRule.getSchedule(), eventTs); + case CUSTOM: + return isActiveCustom((CustomTimeSchedule) alarmRule.getSchedule(), eventTs); + default: + throw new RuntimeException("Unsupported schedule type: " + alarmRule.getSchedule().getType()); + } + } + + private boolean isActiveSpecific(SpecificTimeSchedule schedule, long eventTs) { + ZoneId zoneId = SchedulerUtils.getZoneId(schedule.getTimezone()); + ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(eventTs), zoneId); + if (schedule.getDaysOfWeek().size() != 7) { + int dayOfWeek = zdt.getDayOfWeek().getValue(); + if (!schedule.getDaysOfWeek().contains(dayOfWeek)) { + return false; + } + } + return isActive(eventTs, zoneId, zdt, schedule.getStartsOn(), schedule.getEndsOn()); + } + + private boolean isActiveCustom(CustomTimeSchedule schedule, long eventTs) { + ZoneId zoneId = SchedulerUtils.getZoneId(schedule.getTimezone()); + ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(eventTs), zoneId); + int dayOfWeek = zdt.toLocalDate().getDayOfWeek().getValue(); + for (CustomTimeScheduleItem item : schedule.getItems()) { + if (item.getDayOfWeek() == dayOfWeek) { + if (item.isEnabled()) { + return isActive(eventTs, zoneId, zdt, item.getStartsOn(), item.getEndsOn()); + } else { + return false; + } + } + } + return false; + } + + private boolean isActive(long eventTs, ZoneId zoneId, ZonedDateTime zdt, long startsOn, long endsOn) { + long startOfDay = zdt.toLocalDate().atStartOfDay(zoneId).toInstant().toEpochMilli(); + long msFromStartOfDay = eventTs - startOfDay; + if (startsOn <= endsOn) { + return startsOn <= msFromStartOfDay && endsOn > msFromStartOfDay; + } else { + return startsOn < msFromStartOfDay || (0 < msFromStartOfDay && msFromStartOfDay < endsOn); + } + } + + public void clear() { + if (state.getEventCount() > 0 || state.getLastEventTs() > 0 || state.getDuration() > 0) { + state.setEventCount(0L); + state.setLastEventTs(0L); + state.setDuration(0L); + updateFlag = true; + } + } + + private AlarmEvalResult evalRepeating(DataSnapshot data, boolean active) { + if (active && eval(alarmRule.getCondition(), data)) { + state.setEventCount(state.getEventCount() + 1); + updateFlag = true; + return state.getEventCount() >= requiredRepeats ? AlarmEvalResult.TRUE : AlarmEvalResult.NOT_YET_TRUE; + } else { + return AlarmEvalResult.FALSE; + } + } + + private AlarmEvalResult evalDuration(DataSnapshot data, boolean active) { + if (active && eval(alarmRule.getCondition(), data)) { + if (state.getLastEventTs() > 0) { + if (data.getTs() > state.getLastEventTs()) { + state.setDuration(state.getDuration() + (data.getTs() - state.getLastEventTs())); + state.setLastEventTs(data.getTs()); + updateFlag = true; + } + } else { + state.setLastEventTs(data.getTs()); + state.setDuration(0L); + updateFlag = true; + } + return state.getDuration() > requiredDurationInMs ? AlarmEvalResult.TRUE : AlarmEvalResult.NOT_YET_TRUE; + } else { + return AlarmEvalResult.FALSE; + } + } + + public AlarmEvalResult eval(long ts) { + switch (spec.getType()) { + case SIMPLE: + case REPEATING: + return AlarmEvalResult.NOT_YET_TRUE; + case DURATION: + if (requiredDurationInMs > 0 && state.getLastEventTs() > 0 && ts > state.getLastEventTs()) { + long duration = state.getDuration() + (ts - state.getLastEventTs()); + if (isActive(ts)) { + return duration > requiredDurationInMs ? AlarmEvalResult.TRUE : AlarmEvalResult.NOT_YET_TRUE; + } else { + return AlarmEvalResult.FALSE; + } + } + default: + return AlarmEvalResult.FALSE; + } + } + + private boolean eval(AlarmCondition condition, DataSnapshot data) { + boolean eval = true; + for (KeyFilter keyFilter : condition.getCondition()) { + EntityKeyValue value = data.getValue(keyFilter.getKey()); + if (value == null) { + return false; + } + eval = eval && eval(data, value, keyFilter.getPredicate()); + } + return eval; + } + + private boolean eval(DataSnapshot data, EntityKeyValue value, KeyFilterPredicate predicate) { + switch (predicate.getType()) { + case STRING: + return evalStrPredicate(data, value, (StringFilterPredicate) predicate); + case NUMERIC: + return evalNumPredicate(data, value, (NumericFilterPredicate) predicate); + case BOOLEAN: + return evalBoolPredicate(data, value, (BooleanFilterPredicate) predicate); + case COMPLEX: + return evalComplexPredicate(data, value, (ComplexFilterPredicate) predicate); + default: + return false; + } + } + + private boolean evalComplexPredicate(DataSnapshot data, EntityKeyValue ekv, ComplexFilterPredicate predicate) { + switch (predicate.getOperation()) { + case OR: + for (KeyFilterPredicate kfp : predicate.getPredicates()) { + if (eval(data, ekv, kfp)) { + return true; + } + } + return false; + case AND: + for (KeyFilterPredicate kfp : predicate.getPredicates()) { + if (!eval(data, ekv, kfp)) { + return false; + } + } + return true; + default: + throw new RuntimeException("Operation not supported: " + predicate.getOperation()); + } + } + + private boolean evalBoolPredicate(DataSnapshot data, EntityKeyValue ekv, BooleanFilterPredicate predicate) { + Boolean val = getBoolValue(ekv); + if (val == null) { + return false; + } + Boolean predicateValue = getPredicateValue(data, predicate.getValue(), AlarmRuleState::getBoolValue); + switch (predicate.getOperation()) { + case EQUAL: + return val.equals(predicateValue); + case NOT_EQUAL: + return !val.equals(predicateValue); + default: + throw new RuntimeException("Operation not supported: " + predicate.getOperation()); + } + } + + private boolean evalNumPredicate(DataSnapshot data, EntityKeyValue ekv, NumericFilterPredicate predicate) { + Double val = getDblValue(ekv); + if (val == null) { + return false; + } + Double predicateValue = getPredicateValue(data, predicate.getValue(), AlarmRuleState::getDblValue); + switch (predicate.getOperation()) { + case NOT_EQUAL: + return !val.equals(predicateValue); + case EQUAL: + return val.equals(predicateValue); + case GREATER: + return val > predicateValue; + case GREATER_OR_EQUAL: + return val >= predicateValue; + case LESS: + return val < predicateValue; + case LESS_OR_EQUAL: + return val <= predicateValue; + default: + throw new RuntimeException("Operation not supported: " + predicate.getOperation()); + } + } + + private boolean evalStrPredicate(DataSnapshot data, EntityKeyValue ekv, StringFilterPredicate predicate) { + String val = getStrValue(ekv); + if (val == null) { + return false; + } + String predicateValue = getPredicateValue(data, predicate.getValue(), AlarmRuleState::getStrValue); + if (predicate.isIgnoreCase()) { + val = val.toLowerCase(); + predicateValue = predicateValue.toLowerCase(); + } + switch (predicate.getOperation()) { + case CONTAINS: + return val.contains(predicateValue); + case EQUAL: + return val.equals(predicateValue); + case STARTS_WITH: + return val.startsWith(predicateValue); + case ENDS_WITH: + return val.endsWith(predicateValue); + case NOT_EQUAL: + return !val.equals(predicateValue); + case NOT_CONTAINS: + return !val.contains(predicateValue); + default: + throw new RuntimeException("Operation not supported: " + predicate.getOperation()); + } + } + + private T getPredicateValue(DataSnapshot data, FilterPredicateValue value, Function transformFunction) { + EntityKeyValue ekv = getDynamicPredicateValue(data, value); + if (ekv != null) { + T result = transformFunction.apply(ekv); + if (result != null) { + return result; + } + } + return value.getDefaultValue(); + } + + private EntityKeyValue getDynamicPredicateValue(DataSnapshot data, FilterPredicateValue value) { + EntityKeyValue ekv = null; + if (value.getDynamicValue() != null) { + ekv = data.getValue(new EntityKey(EntityKeyType.ATTRIBUTE, value.getDynamicValue().getSourceAttribute())); + if (ekv == null) { + ekv = data.getValue(new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, value.getDynamicValue().getSourceAttribute())); + if (ekv == null) { + ekv = data.getValue(new EntityKey(EntityKeyType.SHARED_ATTRIBUTE, value.getDynamicValue().getSourceAttribute())); + if (ekv == null) { + ekv = data.getValue(new EntityKey(EntityKeyType.CLIENT_ATTRIBUTE, value.getDynamicValue().getSourceAttribute())); + } + } + } + } + return ekv; + } + + private static String getStrValue(EntityKeyValue ekv) { + switch (ekv.getDataType()) { + case LONG: + return ekv.getLngValue() != null ? ekv.getLngValue().toString() : null; + case DOUBLE: + return ekv.getDblValue() != null ? ekv.getDblValue().toString() : null; + case BOOLEAN: + return ekv.getBoolValue() != null ? ekv.getBoolValue().toString() : null; + case STRING: + return ekv.getStrValue(); + case JSON: + return ekv.getJsonValue(); + default: + return null; + } + } + + private static Double getDblValue(EntityKeyValue ekv) { + switch (ekv.getDataType()) { + case LONG: + return ekv.getLngValue() != null ? ekv.getLngValue().doubleValue() : null; + case DOUBLE: + return ekv.getDblValue() != null ? ekv.getDblValue() : null; + case BOOLEAN: + return ekv.getBoolValue() != null ? (ekv.getBoolValue() ? 1.0 : 0.0) : null; + case STRING: + try { + return Double.parseDouble(ekv.getStrValue()); + } catch (RuntimeException e) { + return null; + } + case JSON: + try { + return Double.parseDouble(ekv.getJsonValue()); + } catch (RuntimeException e) { + return null; + } + default: + return null; + } + } + + private static Boolean getBoolValue(EntityKeyValue ekv) { + switch (ekv.getDataType()) { + case LONG: + return ekv.getLngValue() != null ? ekv.getLngValue() > 0 : null; + case DOUBLE: + return ekv.getDblValue() != null ? ekv.getDblValue() > 0 : null; + case BOOLEAN: + return ekv.getBoolValue(); + case STRING: + try { + return Boolean.parseBoolean(ekv.getStrValue()); + } catch (RuntimeException e) { + return null; + } + case JSON: + try { + return Boolean.parseBoolean(ekv.getJsonValue()); + } catch (RuntimeException e) { + return null; + } + default: + return null; + } + } + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java new file mode 100644 index 0000000000..c59dbb75ca --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java @@ -0,0 +1,282 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.profile; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.rule.engine.action.TbAlarmResult; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.profile.state.PersistedAlarmRuleState; +import org.thingsboard.rule.engine.profile.state.PersistedAlarmState; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.queue.ServiceQueue; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.function.BiFunction; + +@Data +@Slf4j +class AlarmState { + + private final ProfileState deviceProfile; + private final EntityId originator; + private DeviceProfileAlarm alarmDefinition; + private volatile List createRulesSortedBySeverityDesc; + private volatile AlarmRuleState clearState; + private volatile Alarm currentAlarm; + private volatile boolean initialFetchDone; + private volatile TbMsgMetaData lastMsgMetaData; + private volatile String lastMsgQueueName; + private volatile DataSnapshot dataSnapshot; + + AlarmState(ProfileState deviceProfile, EntityId originator, DeviceProfileAlarm alarmDefinition, PersistedAlarmState alarmState) { + this.deviceProfile = deviceProfile; + this.originator = originator; + this.updateState(alarmDefinition, alarmState); + } + + public boolean process(TbContext ctx, TbMsg msg, DataSnapshot data, SnapshotUpdate update) throws ExecutionException, InterruptedException { + initCurrentAlarm(ctx); + lastMsgMetaData = msg.getMetaData(); + lastMsgQueueName = msg.getQueueName(); + return createOrClearAlarms(ctx, data, update, AlarmRuleState::eval); + } + + public boolean process(TbContext ctx, long ts) throws ExecutionException, InterruptedException { + initCurrentAlarm(ctx); + return createOrClearAlarms(ctx, ts, null, AlarmRuleState::eval); + } + + public boolean createOrClearAlarms(TbContext ctx, T data, SnapshotUpdate update, BiFunction evalFunction) { + boolean stateUpdate = false; + AlarmRuleState resultState = null; + log.debug("[{}] processing update: {}", alarmDefinition.getId(), data); + for (AlarmRuleState state : createRulesSortedBySeverityDesc) { + if (!validateUpdate(update, state)) { + log.debug("[{}][{}] Update is not valid for current rule state", alarmDefinition.getId(), state.getSeverity()); + continue; + } + AlarmEvalResult evalResult = evalFunction.apply(state, data); + stateUpdate |= state.checkUpdate(); + if (AlarmEvalResult.TRUE.equals(evalResult)) { + resultState = state; + break; + } else if (AlarmEvalResult.FALSE.equals(evalResult)) { + state.clear(); + stateUpdate |= state.checkUpdate(); + } + } + if (resultState != null) { + TbAlarmResult result = calculateAlarmResult(ctx, resultState); + if (result != null) { + pushMsg(ctx, result); + } + } else if (currentAlarm != null && clearState != null) { + if (!validateUpdate(update, clearState)) { + log.debug("[{}] Update is not valid for current clear state", alarmDefinition.getId()); + return stateUpdate; + } + AlarmEvalResult evalResult = evalFunction.apply(clearState, data); + if (AlarmEvalResult.TRUE.equals(evalResult)) { + clearState.clear(); + stateUpdate |= clearState.checkUpdate(); + for (AlarmRuleState state : createRulesSortedBySeverityDesc) { + state.clear(); + stateUpdate |= state.checkUpdate(); + } + ctx.getAlarmService().clearAlarm(ctx.getTenantId(), currentAlarm.getId(), JacksonUtil.OBJECT_MAPPER.createObjectNode(), System.currentTimeMillis()); + pushMsg(ctx, new TbAlarmResult(false, false, true, currentAlarm)); + currentAlarm = null; + } else if (AlarmEvalResult.FALSE.equals(evalResult)) { + clearState.clear(); + stateUpdate |= clearState.checkUpdate(); + } + } + return stateUpdate; + } + + public boolean validateUpdate(SnapshotUpdate update, AlarmRuleState state) { + if (update != null) { + //Check that the update type and that keys match. + if (update.getType().equals(EntityKeyType.TIME_SERIES)) { + return state.validateTsUpdate(update.getKeys()); + } else if (update.getType().equals(EntityKeyType.ATTRIBUTE)) { + return state.validateAttrUpdate(update.getKeys()); + } + } + return true; + } + + public void initCurrentAlarm(TbContext ctx) throws InterruptedException, ExecutionException { + if (!initialFetchDone) { + Alarm alarm = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), originator, alarmDefinition.getAlarmType()).get(); + if (alarm != null && !alarm.getStatus().isCleared()) { + currentAlarm = alarm; + } + initialFetchDone = true; + } + } + + public void pushMsg(TbContext ctx, TbAlarmResult alarmResult) { + JsonNode jsonNodes = JacksonUtil.valueToTree(alarmResult.getAlarm()); + String data = jsonNodes.toString(); + TbMsgMetaData metaData = lastMsgMetaData != null ? lastMsgMetaData.copy() : new TbMsgMetaData(); + String relationType; + if (alarmResult.isCreated()) { + relationType = "Alarm Created"; + metaData.putValue(DataConstants.IS_NEW_ALARM, Boolean.TRUE.toString()); + } else if (alarmResult.isUpdated()) { + relationType = "Alarm Updated"; + metaData.putValue(DataConstants.IS_EXISTING_ALARM, Boolean.TRUE.toString()); + } else if (alarmResult.isSeverityUpdated()) { + relationType = "Alarm Severity Updated"; + metaData.putValue(DataConstants.IS_EXISTING_ALARM, Boolean.TRUE.toString()); + metaData.putValue(DataConstants.IS_SEVERITY_UPDATED_ALARM, Boolean.TRUE.toString()); + } else { + relationType = "Alarm Cleared"; + metaData.putValue(DataConstants.IS_CLEARED_ALARM, Boolean.TRUE.toString()); + } + TbMsg newMsg = ctx.newMsg(lastMsgQueueName != null ? lastMsgQueueName : ServiceQueue.MAIN, "ALARM", originator, metaData, data); + ctx.tellNext(newMsg, relationType); + } + + public void updateState(DeviceProfileAlarm alarm, PersistedAlarmState alarmState) { + this.alarmDefinition = alarm; + this.createRulesSortedBySeverityDesc = new ArrayList<>(); + alarmDefinition.getCreateRules().forEach((severity, rule) -> { + PersistedAlarmRuleState ruleState = null; + if (alarmState != null) { + ruleState = alarmState.getCreateRuleStates().get(severity); + if (ruleState == null) { + ruleState = new PersistedAlarmRuleState(); + alarmState.getCreateRuleStates().put(severity, ruleState); + } + } + createRulesSortedBySeverityDesc.add(new AlarmRuleState(severity, rule, + deviceProfile.getCreateAlarmKeys(alarm.getId(), severity), ruleState)); + }); + createRulesSortedBySeverityDesc.sort(Comparator.comparingInt(state -> state.getSeverity().ordinal())); + PersistedAlarmRuleState ruleState = alarmState == null ? null : alarmState.getClearRuleState(); + if (alarmDefinition.getClearRule() != null) { + clearState = new AlarmRuleState(null, alarmDefinition.getClearRule(), deviceProfile.getClearAlarmKeys(alarm.getId()), ruleState); + } + } + + private TbAlarmResult calculateAlarmResult(TbContext ctx, AlarmRuleState ruleState) { + AlarmSeverity severity = ruleState.getSeverity(); + if (currentAlarm != null) { + // TODO: In some extremely rare cases, we might miss the event of alarm clear (If one use in-mem queue and restarted the server) or (if one manipulated the rule chain). + // Maybe we should fetch alarm every time? + currentAlarm.setEndTs(System.currentTimeMillis()); + AlarmSeverity oldSeverity = currentAlarm.getSeverity(); + // Skip update if severity is decreased. + if (severity.ordinal() <= oldSeverity.ordinal()) { + if (!oldSeverity.equals(severity)) { + currentAlarm.setSeverity(severity); + currentAlarm = ctx.getAlarmService().createOrUpdateAlarm(currentAlarm); + return new TbAlarmResult(false, false, true, false, currentAlarm); + } else { + currentAlarm = ctx.getAlarmService().createOrUpdateAlarm(currentAlarm); + return new TbAlarmResult(false, true, false, false, currentAlarm); + } + } else { + return null; + } + } else { + currentAlarm = new Alarm(); + currentAlarm.setType(alarmDefinition.getAlarmType()); + currentAlarm.setStatus(AlarmStatus.ACTIVE_UNACK); + currentAlarm.setSeverity(severity); + currentAlarm.setStartTs(System.currentTimeMillis()); + currentAlarm.setEndTs(currentAlarm.getStartTs()); + currentAlarm.setDetails(createDetails(ruleState)); + currentAlarm.setOriginator(originator); + currentAlarm.setTenantId(ctx.getTenantId()); + currentAlarm.setPropagate(alarmDefinition.isPropagate()); + if (alarmDefinition.getPropagateRelationTypes() != null) { + currentAlarm.setPropagateRelationTypes(alarmDefinition.getPropagateRelationTypes()); + } + currentAlarm = ctx.getAlarmService().createOrUpdateAlarm(currentAlarm); + boolean updated = currentAlarm.getStartTs() != currentAlarm.getEndTs(); + return new TbAlarmResult(!updated, updated, false, false, currentAlarm); + } + } + + private JsonNode createDetails(AlarmRuleState ruleState) { + ObjectNode details = JacksonUtil.OBJECT_MAPPER.createObjectNode(); + String alarmDetails = ruleState.getAlarmRule().getAlarmDetails(); + + if (alarmDetails != null) { + for (KeyFilter keyFilter : ruleState.getAlarmRule().getCondition().getCondition()) { + EntityKeyValue entityKeyValue = dataSnapshot.getValue(keyFilter.getKey()); + alarmDetails = alarmDetails.replaceAll(String.format("\\$\\{%s}", keyFilter.getKey().getKey()), getValueAsString(entityKeyValue)); + } + + details.put("data", alarmDetails); + } + + return details; + } + + private static String getValueAsString(EntityKeyValue entityKeyValue) { + Object result = null; + switch (entityKeyValue.getDataType()) { + case STRING: + result = entityKeyValue.getStrValue(); + break; + case JSON: + result = entityKeyValue.getJsonValue(); + break; + case LONG: + result = entityKeyValue.getLngValue(); + break; + case DOUBLE: + result = entityKeyValue.getDblValue(); + break; + case BOOLEAN: + result = entityKeyValue.getBoolValue(); + break; + } + return String.valueOf(result); + } + + public boolean processAlarmClear(TbContext ctx, Alarm alarmNf) { + boolean updated = false; + if (currentAlarm != null && currentAlarm.getId().equals(alarmNf.getId())) { + currentAlarm = null; + for (AlarmRuleState state : createRulesSortedBySeverityDesc) { + state.clear(); + updated |= state.checkUpdate(); + } + } + return updated; + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmStateUpdateResult.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmStateUpdateResult.java new file mode 100644 index 0000000000..a84cf96710 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmStateUpdateResult.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.profile; + +enum AlarmStateUpdateResult { + + NONE, CREATED, UPDATED, SEVERITY_UPDATED, CLEARED; + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DataSnapshot.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DataSnapshot.java new file mode 100644 index 0000000000..0c3abc0ca3 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DataSnapshot.java @@ -0,0 +1,115 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.profile; + +import lombok.Getter; +import lombok.Setter; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +class DataSnapshot { + + private volatile boolean ready; + @Getter + @Setter + private long ts; + private final Set keys; + private final Map values = new ConcurrentHashMap<>(); + + DataSnapshot(Set entityKeysToFetch) { + this.keys = entityKeysToFetch; + } + + void removeValue(EntityKey key) { + switch (key.getType()) { + case ATTRIBUTE: + values.remove(key); + values.remove(getAttrKey(key, EntityKeyType.CLIENT_ATTRIBUTE)); + values.remove(getAttrKey(key, EntityKeyType.SHARED_ATTRIBUTE)); + values.remove(getAttrKey(key, EntityKeyType.SERVER_ATTRIBUTE)); + break; + case CLIENT_ATTRIBUTE: + case SHARED_ATTRIBUTE: + case SERVER_ATTRIBUTE: + values.remove(key); + values.remove(getAttrKey(key, EntityKeyType.ATTRIBUTE)); + break; + default: + values.remove(key); + } + } + + boolean putValue(EntityKey key, long newTs, EntityKeyValue value) { + boolean updateOfTs = ts != newTs; + boolean result = false; + switch (key.getType()) { + case ATTRIBUTE: + result |= putIfKeyExists(key, value, updateOfTs); + result |= putIfKeyExists(getAttrKey(key, EntityKeyType.CLIENT_ATTRIBUTE), value, updateOfTs); + result |= putIfKeyExists(getAttrKey(key, EntityKeyType.SHARED_ATTRIBUTE), value, updateOfTs); + result |= putIfKeyExists(getAttrKey(key, EntityKeyType.SERVER_ATTRIBUTE), value, updateOfTs); + break; + case CLIENT_ATTRIBUTE: + case SHARED_ATTRIBUTE: + case SERVER_ATTRIBUTE: + result |= putIfKeyExists(key, value, updateOfTs); + result |= putIfKeyExists(getAttrKey(key, EntityKeyType.ATTRIBUTE), value, updateOfTs); + break; + default: + result |= putIfKeyExists(key, value, updateOfTs); + } + return result; + } + + private boolean putIfKeyExists(EntityKey key, EntityKeyValue value, boolean updateOfTs) { + if (keys.contains(key)) { + EntityKeyValue oldValue = values.put(key, value); + if (updateOfTs) { + return true; + } else { + return oldValue == null || !oldValue.equals(value); + } + } else { + return false; + } + } + + EntityKeyValue getValue(EntityKey key) { + if (EntityKeyType.ATTRIBUTE.equals(key.getType())) { + EntityKeyValue value = values.get(key); + if (value == null) { + value = values.get(getAttrKey(key, EntityKeyType.CLIENT_ATTRIBUTE)); + if (value == null) { + value = values.get(getAttrKey(key, EntityKeyType.SHARED_ATTRIBUTE)); + if (value == null) { + value = values.get(getAttrKey(key, EntityKeyType.SERVER_ATTRIBUTE)); + } + } + } + return value; + } else { + return values.get(key); + } + } + + private EntityKey getAttrKey(EntityKey key, EntityKeyType clientAttribute) { + return new EntityKey(clientAttribute, key.getKey()); + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java new file mode 100644 index 0000000000..6824f56d35 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java @@ -0,0 +1,400 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.profile; + +import com.google.gson.JsonParser; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.profile.state.PersistedAlarmState; +import org.thingsboard.rule.engine.profile.state.PersistedDeviceState; +import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.rule.RuleNodeState; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.session.SessionMsgType; +import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.dao.sql.query.EntityKeyMapping; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +@Slf4j +class DeviceState { + + private final boolean persistState; + private final DeviceId deviceId; + private final ProfileState deviceProfile; + private RuleNodeState state; + private PersistedDeviceState pds; + private DataSnapshot latestValues; + private final ConcurrentMap alarmStates = new ConcurrentHashMap<>(); + + DeviceState(TbContext ctx, TbDeviceProfileNodeConfiguration config, DeviceId deviceId, ProfileState deviceProfile, RuleNodeState state) { + this.persistState = config.isPersistAlarmRulesState(); + this.deviceId = deviceId; + this.deviceProfile = deviceProfile; + if (config.isPersistAlarmRulesState()) { + if (state != null) { + this.state = state; + } else { + this.state = ctx.findRuleNodeStateForEntity(deviceId); + } + if (this.state != null) { + pds = JacksonUtil.fromString(this.state.getStateData(), PersistedDeviceState.class); + } else { + this.state = new RuleNodeState(); + this.state.setRuleNodeId(ctx.getSelfId()); + this.state.setEntityId(deviceId); + pds = new PersistedDeviceState(); + pds.setAlarmStates(new HashMap<>()); + } + } + if (pds != null) { + for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { + alarmStates.computeIfAbsent(alarm.getId(), + a -> new AlarmState(deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm))); + } + } + } + + public void updateProfile(TbContext ctx, DeviceProfile deviceProfile) throws ExecutionException, InterruptedException { + Set oldKeys = this.deviceProfile.getEntityKeys(); + this.deviceProfile.updateDeviceProfile(deviceProfile); + if (latestValues != null) { + Set keysToFetch = new HashSet<>(this.deviceProfile.getEntityKeys()); + keysToFetch.removeAll(oldKeys); + if (!keysToFetch.isEmpty()) { + addEntityKeysToSnapshot(ctx, deviceId, keysToFetch, latestValues); + } + } + Set newAlarmStateIds = this.deviceProfile.getAlarmSettings().stream().map(DeviceProfileAlarm::getId).collect(Collectors.toSet()); + alarmStates.keySet().removeIf(id -> !newAlarmStateIds.contains(id)); + for (DeviceProfileAlarm alarm : this.deviceProfile.getAlarmSettings()) { + if (alarmStates.containsKey(alarm.getId())) { + alarmStates.get(alarm.getId()).updateState(alarm, getOrInitPersistedAlarmState(alarm)); + } else { + alarmStates.putIfAbsent(alarm.getId(), new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm))); + } + } + } + + public void harvestAlarms(TbContext ctx, long ts) throws ExecutionException, InterruptedException { + log.debug("[{}] Going to harvest alarms: {}", ctx.getSelfId(), ts); + boolean stateChanged = false; + for (AlarmState state : alarmStates.values()) { + stateChanged |= state.process(ctx, ts); + } + if (persistState && stateChanged) { + state.setStateData(JacksonUtil.toString(pds)); + state = ctx.saveRuleNodeState(state); + } + } + + public void process(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { + if (latestValues == null) { + latestValues = fetchLatestValues(ctx, deviceId); + } + boolean stateChanged = false; + if (msg.getType().equals(SessionMsgType.POST_TELEMETRY_REQUEST.name())) { + stateChanged = processTelemetry(ctx, msg); + } else if (msg.getType().equals(SessionMsgType.POST_ATTRIBUTES_REQUEST.name())) { + stateChanged = processAttributesUpdateRequest(ctx, msg); + } else if (msg.getType().equals(DataConstants.ATTRIBUTES_UPDATED)) { + stateChanged = processAttributesUpdateNotification(ctx, msg); + } else if (msg.getType().equals(DataConstants.ATTRIBUTES_DELETED)) { + stateChanged = processAttributesDeleteNotification(ctx, msg); + } else if (msg.getType().equals(DataConstants.ALARM_CLEAR)) { + stateChanged = processAlarmClearNotification(ctx, msg); + } else { + ctx.tellSuccess(msg); + } + if (persistState && stateChanged) { + state.setStateData(JacksonUtil.toString(pds)); + state = ctx.saveRuleNodeState(state); + } + } + + private boolean processAlarmClearNotification(TbContext ctx, TbMsg msg) { + boolean stateChanged = false; + Alarm alarmNf = JacksonUtil.fromString(msg.getData(), Alarm.class); + for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { + AlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), + a -> new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm))); + stateChanged |= alarmState.processAlarmClear(ctx, alarmNf); + } + ctx.tellSuccess(msg); + return stateChanged; + } + + private boolean processAttributesUpdateNotification(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { + Set attributes = JsonConverter.convertToAttributes(new JsonParser().parse(msg.getData())); + String scope = msg.getMetaData().getValue("scope"); + if (StringUtils.isEmpty(scope)) { + scope = DataConstants.CLIENT_SCOPE; + } + return processAttributesUpdate(ctx, msg, attributes, scope); + } + + private boolean processAttributesDeleteNotification(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { + boolean stateChanged = false; + List keys = new ArrayList<>(); + new JsonParser().parse(msg.getData()).getAsJsonObject().get("attributes").getAsJsonArray().forEach(e -> keys.add(e.getAsString())); + String scope = msg.getMetaData().getValue("scope"); + if (StringUtils.isEmpty(scope)) { + scope = DataConstants.CLIENT_SCOPE; + } + if (!keys.isEmpty()) { + EntityKeyType keyType = getKeyTypeFromScope(scope); + keys.forEach(key -> latestValues.removeValue(new EntityKey(keyType, key))); + for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { + AlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), + a -> new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm))); + stateChanged |= alarmState.process(ctx, msg, latestValues, null); + } + } + ctx.tellSuccess(msg); + return stateChanged; + } + + protected boolean processAttributesUpdateRequest(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { + Set attributes = JsonConverter.convertToAttributes(new JsonParser().parse(msg.getData())); + return processAttributesUpdate(ctx, msg, attributes, DataConstants.CLIENT_SCOPE); + } + + private boolean processAttributesUpdate(TbContext ctx, TbMsg msg, Set attributes, String scope) throws ExecutionException, InterruptedException { + boolean stateChanged = false; + if (!attributes.isEmpty()) { + SnapshotUpdate update = merge(latestValues, attributes, scope); + for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { + AlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), + a -> new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm))); + stateChanged |= alarmState.process(ctx, msg, latestValues, update); + } + } + ctx.tellSuccess(msg); + return stateChanged; + } + + protected boolean processTelemetry(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { + boolean stateChanged = false; + Map> tsKvMap = JsonConverter.convertToSortedTelemetry(new JsonParser().parse(msg.getData()), TbMsgTimeseriesNode.getTs(msg)); + // iterate over data by ts (ASC order). + for (Map.Entry> entry : tsKvMap.entrySet()) { + Long ts = entry.getKey(); + List data = entry.getValue(); + SnapshotUpdate update = merge(latestValues, ts, data); + if (update.hasUpdate()) { + for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { + AlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), + a -> new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm))); + stateChanged |= alarmState.process(ctx, msg, latestValues, update); + } + } + } + ctx.tellSuccess(msg); + return stateChanged; + } + + private SnapshotUpdate merge(DataSnapshot latestValues, Long newTs, List data) { + Set keys = new HashSet<>(); + for (KvEntry entry : data) { + EntityKey entityKey = new EntityKey(EntityKeyType.TIME_SERIES, entry.getKey()); + if (latestValues.putValue(entityKey, newTs, toEntityValue(entry))) { + keys.add(entityKey); + } + } + latestValues.setTs(newTs); + return new SnapshotUpdate(EntityKeyType.TIME_SERIES, keys); + } + + private SnapshotUpdate merge(DataSnapshot latestValues, Set attributes, String scope) { + long newTs = 0; + Set keys = new HashSet<>(); + for (AttributeKvEntry entry : attributes) { + newTs = Math.max(newTs, entry.getLastUpdateTs()); + EntityKey entityKey = new EntityKey(getKeyTypeFromScope(scope), entry.getKey()); + if (latestValues.putValue(entityKey, newTs, toEntityValue(entry))) { + keys.add(entityKey); + } + } + latestValues.setTs(newTs); + return new SnapshotUpdate(EntityKeyType.ATTRIBUTE, keys); + } + + private static EntityKeyType getKeyTypeFromScope(String scope) { + switch (scope) { + case DataConstants.CLIENT_SCOPE: + return EntityKeyType.CLIENT_ATTRIBUTE; + case DataConstants.SHARED_SCOPE: + return EntityKeyType.SHARED_ATTRIBUTE; + case DataConstants.SERVER_SCOPE: + return EntityKeyType.SERVER_ATTRIBUTE; + } + return EntityKeyType.ATTRIBUTE; + } + + private DataSnapshot fetchLatestValues(TbContext ctx, EntityId originator) throws ExecutionException, InterruptedException { + Set entityKeysToFetch = deviceProfile.getEntityKeys(); + DataSnapshot result = new DataSnapshot(entityKeysToFetch); + addEntityKeysToSnapshot(ctx, originator, entityKeysToFetch, result); + return result; + } + + private void addEntityKeysToSnapshot(TbContext ctx, EntityId originator, Set entityKeysToFetch, DataSnapshot result) throws InterruptedException, ExecutionException { + Set serverAttributeKeys = new HashSet<>(); + Set clientAttributeKeys = new HashSet<>(); + Set sharedAttributeKeys = new HashSet<>(); + Set commonAttributeKeys = new HashSet<>(); + Set latestTsKeys = new HashSet<>(); + + Device device = null; + for (EntityKey entityKey : entityKeysToFetch) { + String key = entityKey.getKey(); + switch (entityKey.getType()) { + case SERVER_ATTRIBUTE: + serverAttributeKeys.add(key); + break; + case CLIENT_ATTRIBUTE: + clientAttributeKeys.add(key); + break; + case SHARED_ATTRIBUTE: + sharedAttributeKeys.add(key); + break; + case ATTRIBUTE: + serverAttributeKeys.add(key); + clientAttributeKeys.add(key); + sharedAttributeKeys.add(key); + commonAttributeKeys.add(key); + break; + case TIME_SERIES: + latestTsKeys.add(key); + break; + case ENTITY_FIELD: + if (device == null) { + device = ctx.getDeviceService().findDeviceById(ctx.getTenantId(), new DeviceId(originator.getId())); + } + if (device != null) { + switch (key) { + case EntityKeyMapping.NAME: + result.putValue(entityKey, device.getCreatedTime(), EntityKeyValue.fromString(device.getName())); + break; + case EntityKeyMapping.TYPE: + result.putValue(entityKey, device.getCreatedTime(), EntityKeyValue.fromString(device.getType())); + break; + case EntityKeyMapping.CREATED_TIME: + result.putValue(entityKey, device.getCreatedTime(), EntityKeyValue.fromLong(device.getCreatedTime())); + break; + case EntityKeyMapping.LABEL: + result.putValue(entityKey, device.getCreatedTime(), EntityKeyValue.fromString(device.getLabel())); + break; + } + } + break; + } + } + + if (!latestTsKeys.isEmpty()) { + List data = ctx.getTimeseriesService().findLatest(ctx.getTenantId(), originator, latestTsKeys).get(); + for (TsKvEntry entry : data) { + if (entry.getValue() != null) { + result.putValue(new EntityKey(EntityKeyType.TIME_SERIES, entry.getKey()), entry.getTs(), toEntityValue(entry)); + } + } + } + if (!clientAttributeKeys.isEmpty()) { + addToSnapshot(result, commonAttributeKeys, + ctx.getAttributesService().find(ctx.getTenantId(), originator, DataConstants.CLIENT_SCOPE, clientAttributeKeys).get()); + } + if (!sharedAttributeKeys.isEmpty()) { + addToSnapshot(result, commonAttributeKeys, + ctx.getAttributesService().find(ctx.getTenantId(), originator, DataConstants.SHARED_SCOPE, sharedAttributeKeys).get()); + } + if (!serverAttributeKeys.isEmpty()) { + addToSnapshot(result, commonAttributeKeys, + ctx.getAttributesService().find(ctx.getTenantId(), originator, DataConstants.SERVER_SCOPE, serverAttributeKeys).get()); + } + } + + private void addToSnapshot(DataSnapshot snapshot, Set commonAttributeKeys, List data) { + for (AttributeKvEntry entry : data) { + if (entry.getValue() != null) { + EntityKeyValue value = toEntityValue(entry); + snapshot.putValue(new EntityKey(EntityKeyType.CLIENT_ATTRIBUTE, entry.getKey()), entry.getLastUpdateTs(), value); + if (commonAttributeKeys.contains(entry.getKey())) { + snapshot.putValue(new EntityKey(EntityKeyType.ATTRIBUTE, entry.getKey()), entry.getLastUpdateTs(), value); + } + } + } + } + + private EntityKeyValue toEntityValue(KvEntry entry) { + switch (entry.getDataType()) { + case STRING: + return EntityKeyValue.fromString(entry.getStrValue().get()); + case LONG: + return EntityKeyValue.fromLong(entry.getLongValue().get()); + case DOUBLE: + return EntityKeyValue.fromDouble(entry.getDoubleValue().get()); + case BOOLEAN: + return EntityKeyValue.fromBool(entry.getBooleanValue().get()); + case JSON: + return EntityKeyValue.fromJson(entry.getJsonValue().get()); + default: + throw new RuntimeException("Can't parse entry: " + entry.getDataType()); + } + } + + public DeviceProfileId getProfileId() { + return deviceProfile.getProfileId(); + } + + private PersistedAlarmState getOrInitPersistedAlarmState(DeviceProfileAlarm alarm) { + if (pds != null) { + PersistedAlarmState alarmState = pds.getAlarmStates().get(alarm.getId()); + if (alarmState == null) { + alarmState = new PersistedAlarmState(); + alarmState.setCreateRuleStates(new HashMap<>()); + pds.getAlarmStates().put(alarm.getId(), alarmState); + } + return alarmState; + } else { + return null; + } + } + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/EntityKeyValue.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/EntityKeyValue.java new file mode 100644 index 0000000000..73a4db63b0 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/EntityKeyValue.java @@ -0,0 +1,111 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.profile; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.thingsboard.server.common.data.kv.DataType; + +@EqualsAndHashCode +class EntityKeyValue { + + @Getter + private DataType dataType; + private Long lngValue; + private Double dblValue; + private Boolean boolValue; + private String strValue; + + public Long getLngValue() { + return dataType == DataType.LONG ? lngValue : null; + } + + public void setLngValue(Long lngValue) { + this.dataType = DataType.LONG; + this.lngValue = lngValue; + } + + public Double getDblValue() { + return dataType == DataType.DOUBLE ? dblValue : null; + } + + public void setDblValue(Double dblValue) { + this.dataType = DataType.DOUBLE; + this.dblValue = dblValue; + } + + public Boolean getBoolValue() { + return dataType == DataType.BOOLEAN ? boolValue : null; + } + + public void setBoolValue(Boolean boolValue) { + this.dataType = DataType.BOOLEAN; + this.boolValue = boolValue; + } + + public String getStrValue() { + return dataType == DataType.STRING ? strValue : null; + } + + public void setStrValue(String strValue) { + this.dataType = DataType.STRING; + this.strValue = strValue; + } + + public void setJsonValue(String jsonValue) { + this.dataType = DataType.JSON; + this.strValue = jsonValue; + } + + public String getJsonValue() { + return dataType == DataType.JSON ? strValue : null; + } + + boolean isSet() { + return dataType != null; + } + + static EntityKeyValue fromString(String s) { + EntityKeyValue result = new EntityKeyValue(); + result.setStrValue(s); + return result; + } + + static EntityKeyValue fromBool(boolean b) { + EntityKeyValue result = new EntityKeyValue(); + result.setBoolValue(b); + return result; + } + + static EntityKeyValue fromLong(long l) { + EntityKeyValue result = new EntityKeyValue(); + result.setLngValue(l); + return result; + } + + static EntityKeyValue fromDouble(double d) { + EntityKeyValue result = new EntityKeyValue(); + result.setDblValue(d); + return result; + } + + static EntityKeyValue fromJson(String s) { + EntityKeyValue result = new EntityKeyValue(); + result.setJsonValue(s); + return result; + } + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/ProfileState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/ProfileState.java new file mode 100644 index 0000000000..b758a96c39 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/ProfileState.java @@ -0,0 +1,137 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.profile; + +import lombok.AccessLevel; +import lombok.Getter; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.device.profile.AlarmRule; +import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.query.ComplexFilterPredicate; +import org.thingsboard.server.common.data.query.DynamicValue; +import org.thingsboard.server.common.data.query.DynamicValueSourceType; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.KeyFilterPredicate; +import org.thingsboard.server.common.data.query.SimpleKeyFilterPredicate; +import org.thingsboard.server.common.data.query.StringFilterPredicate; + +import javax.print.attribute.standard.Severity; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + + +class ProfileState { + + private DeviceProfile deviceProfile; + @Getter(AccessLevel.PACKAGE) + private final List alarmSettings = new CopyOnWriteArrayList<>(); + @Getter(AccessLevel.PACKAGE) + private final Set entityKeys = ConcurrentHashMap.newKeySet(); + + private final Map>> alarmCreateKeys = new HashMap<>(); + private final Map> alarmClearKeys = new HashMap<>(); + + ProfileState(DeviceProfile deviceProfile) { + updateDeviceProfile(deviceProfile); + } + + void updateDeviceProfile(DeviceProfile deviceProfile) { + this.deviceProfile = deviceProfile; + alarmSettings.clear(); + alarmCreateKeys.clear(); + alarmClearKeys.clear(); + if (deviceProfile.getProfileData().getAlarms() != null) { + alarmSettings.addAll(deviceProfile.getProfileData().getAlarms()); + for (DeviceProfileAlarm alarm : deviceProfile.getProfileData().getAlarms()) { + Map> createAlarmKeys = alarmCreateKeys.computeIfAbsent(alarm.getId(), id -> new HashMap<>()); + alarm.getCreateRules().forEach(((severity, alarmRule) -> { + Set ruleKeys = createAlarmKeys.computeIfAbsent(severity, id -> new HashSet<>()); + for (KeyFilter keyFilter : alarmRule.getCondition().getCondition()) { + entityKeys.add(keyFilter.getKey()); + ruleKeys.add(keyFilter.getKey()); + addDynamicValuesRecursively(keyFilter.getPredicate(), entityKeys, ruleKeys); + } + })); + if (alarm.getClearRule() != null) { + Set clearAlarmKeys = alarmClearKeys.computeIfAbsent(alarm.getId(), id -> new HashSet<>()); + for (KeyFilter keyFilter : alarm.getClearRule().getCondition().getCondition()) { + entityKeys.add(keyFilter.getKey()); + clearAlarmKeys.add(keyFilter.getKey()); + addDynamicValuesRecursively(keyFilter.getPredicate(), entityKeys, clearAlarmKeys); + } + } + } + } + } + + private void addDynamicValuesRecursively(KeyFilterPredicate predicate, Set entityKeys, Set ruleKeys) { + switch (predicate.getType()) { + case STRING: + case NUMERIC: + case BOOLEAN: + DynamicValue value = ((SimpleKeyFilterPredicate) predicate).getValue().getDynamicValue(); + if (value != null && value.getSourceType() == DynamicValueSourceType.CURRENT_DEVICE) { + EntityKey entityKey = new EntityKey(EntityKeyType.ATTRIBUTE, value.getSourceAttribute()); + entityKeys.add(entityKey); + ruleKeys.add(entityKey); + } + break; + case COMPLEX: + for (KeyFilterPredicate child : ((ComplexFilterPredicate) predicate).getPredicates()) { + addDynamicValuesRecursively(child, entityKeys, ruleKeys); + } + break; + } + } + + DeviceProfileId getProfileId() { + return deviceProfile.getId(); + } + + Set getCreateAlarmKeys(String id, AlarmSeverity severity) { + Map> sKeys = alarmCreateKeys.get(id); + if (sKeys == null) { + return Collections.emptySet(); + } else { + Set keys = sKeys.get(severity); + if (keys == null) { + return Collections.emptySet(); + } else { + return keys; + } + } + } + + Set getClearAlarmKeys(String id) { + Set keys = alarmClearKeys.get(id); + if (keys == null) { + return Collections.emptySet(); + } else { + return keys; + } + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/SnapshotUpdate.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/SnapshotUpdate.java new file mode 100644 index 0000000000..e52a784c9c --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/SnapshotUpdate.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.profile; + +import lombok.Getter; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; + +import java.util.Set; + +class SnapshotUpdate { + + @Getter + private final EntityKeyType type; + @Getter + private final Set keys; + + SnapshotUpdate(EntityKeyType type, Set keys) { + this.type = type; + this.keys = keys; + } + + boolean hasUpdate(){ + return !keys.isEmpty(); + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java new file mode 100644 index 0000000000..fa7538bce6 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java @@ -0,0 +1,232 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.profile; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.rule.RuleNodeState; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +@Slf4j +@RuleNode( + type = ComponentType.ACTION, + name = "device profile", + customRelations = true, + relationTypes = {"Alarm Created", "Alarm Updated", "Alarm Severity Updated", "Alarm Cleared", "Success", "Failure"}, + configClazz = TbDeviceProfileNodeConfiguration.class, + nodeDescription = "Process device messages based on device profile settings", + nodeDetails = "Create and clear alarms based on alarm rules defined in device profile. Generates ", + uiResources = {"static/rulenode/rulenode-core-config.js"}, + configDirective = "tbDeviceProfileConfig" +) +public class TbDeviceProfileNode implements TbNode { + private static final String PERIODIC_MSG_TYPE = "TbDeviceProfilePeriodicMsg"; + private static final String PROFILE_UPDATE_MSG_TYPE = "TbDeviceProfileUpdateMsg"; + private static final String DEVICE_UPDATE_MSG_TYPE = "TbDeviceUpdateMsg"; + + private TbDeviceProfileNodeConfiguration config; + private RuleEngineDeviceProfileCache cache; + private TbContext ctx; + private final Map deviceStates = new ConcurrentHashMap<>(); + + @Override + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + this.config = TbNodeUtils.convert(configuration, TbDeviceProfileNodeConfiguration.class); + this.cache = ctx.getDeviceProfileCache(); + this.ctx = ctx; + scheduleAlarmHarvesting(ctx); + ctx.addDeviceProfileListeners(this::onProfileUpdate, this::onDeviceUpdate); + if (config.isFetchAlarmRulesStateOnStart()) { + log.info("[{}] Fetching alarm rule state", ctx.getSelfId()); + int fetchCount = 0; + PageLink pageLink = new PageLink(1024); + while (true) { + PageData states = ctx.findRuleNodeStates(pageLink); + if (!states.getData().isEmpty()) { + for (RuleNodeState rns : states.getData()) { + fetchCount++; + if (rns.getEntityId().getEntityType().equals(EntityType.DEVICE) && ctx.isLocalEntity(rns.getEntityId())) { + getOrCreateDeviceState(ctx, new DeviceId(rns.getEntityId().getId()), rns); + } + } + } + if (!states.hasNext()) { + break; + } else { + pageLink = pageLink.nextPageLink(); + } + } + log.info("[{}] Fetched alarm rule state for {} entities", ctx.getSelfId(), fetchCount); + } + if (!config.isPersistAlarmRulesState() && ctx.isLocalEntity(ctx.getSelfId())) { + log.info("[{}] Going to cleanup rule node states", ctx.getSelfId()); + ctx.clearRuleNodeStates(); + } + } + + @Override + public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { + EntityType originatorType = msg.getOriginator().getEntityType(); + if (msg.getType().equals(PERIODIC_MSG_TYPE)) { + scheduleAlarmHarvesting(ctx); + harvestAlarms(ctx, System.currentTimeMillis()); + } else if (msg.getType().equals(PROFILE_UPDATE_MSG_TYPE)) { + updateProfile(ctx, new DeviceProfileId(UUID.fromString(msg.getData()))); + } else if (msg.getType().equals(DEVICE_UPDATE_MSG_TYPE)) { + JsonNode data = JacksonUtil.toJsonNode(msg.getData()); + DeviceId deviceId = new DeviceId(UUID.fromString(data.get("deviceId").asText())); + if (data.has("profileId")) { + invalidateDeviceProfileCache(deviceId, new DeviceProfileId(UUID.fromString(data.get("deviceProfileId").asText()))); + } else { + removeDeviceState(deviceId); + } + + } else { + if (EntityType.DEVICE.equals(originatorType)) { + DeviceId deviceId = new DeviceId(msg.getOriginator().getId()); + if (msg.getType().equals(DataConstants.ENTITY_UPDATED)) { + invalidateDeviceProfileCache(deviceId, msg.getData()); + } else if (msg.getType().equals(DataConstants.ENTITY_DELETED)) { + removeDeviceState(deviceId); + } else { + DeviceState deviceState = getOrCreateDeviceState(ctx, deviceId, null); + if (deviceState != null) { + deviceState.process(ctx, msg); + } else { + ctx.tellFailure(msg, new IllegalStateException("Device profile for device [" + deviceId + "] not found!")); + } + } + } else { + ctx.tellSuccess(msg); + } + } + } + + @Override + public void onPartitionChangeMsg(TbContext ctx, PartitionChangeMsg msg) { + // Cleanup the cache for all entities that are no longer assigned to current server partitions + deviceStates.entrySet().removeIf(entry -> !ctx.isLocalEntity(entry.getKey())); + } + + @Override + public void destroy() { + ctx.removeProfileListener(); + deviceStates.clear(); + } + + protected DeviceState getOrCreateDeviceState(TbContext ctx, DeviceId deviceId, RuleNodeState rns) { + DeviceState deviceState = deviceStates.get(deviceId); + if (deviceState == null) { + DeviceProfile deviceProfile = cache.get(ctx.getTenantId(), deviceId); + if (deviceProfile != null) { + deviceState = new DeviceState(ctx, config, deviceId, new ProfileState(deviceProfile), rns); + deviceStates.put(deviceId, deviceState); + } + } + return deviceState; + } + + protected void scheduleAlarmHarvesting(TbContext ctx) { + TbMsg periodicCheck = TbMsg.newMsg(PERIODIC_MSG_TYPE, ctx.getTenantId(), TbMsgMetaData.EMPTY, "{}"); + ctx.tellSelf(periodicCheck, TimeUnit.MINUTES.toMillis(1)); + } + + protected void harvestAlarms(TbContext ctx, long ts) throws ExecutionException, InterruptedException { + for (DeviceState state : deviceStates.values()) { + state.harvestAlarms(ctx, ts); + } + } + + protected void updateProfile(TbContext ctx, DeviceProfileId deviceProfileId) throws ExecutionException, InterruptedException { + DeviceProfile deviceProfile = cache.get(ctx.getTenantId(), deviceProfileId); + if (deviceProfile != null) { + log.debug("[{}] Received device profile update notification: {}", ctx.getSelfId(), deviceProfile); + for (DeviceState state : deviceStates.values()) { + if (deviceProfile.getId().equals(state.getProfileId())) { + state.updateProfile(ctx, deviceProfile); + } + } + } else { + log.debug("[{}] Received stale profile update notification: [{}]", ctx.getSelfId(), deviceProfileId); + } + } + + protected void onProfileUpdate(DeviceProfile profile) { + ctx.tellSelf(TbMsg.newMsg(PROFILE_UPDATE_MSG_TYPE, ctx.getTenantId(), TbMsgMetaData.EMPTY, profile.getId().getId().toString()), 0L); + } + + private void onDeviceUpdate(DeviceId deviceId, DeviceProfile deviceProfile) { + ObjectNode msgData = JacksonUtil.newObjectNode(); + msgData.put("deviceId", deviceId.getId().toString()); + if (deviceProfile != null) { + msgData.put("deviceProfileId", deviceProfile.getId().getId().toString()); + } + ctx.tellSelf(TbMsg.newMsg(DEVICE_UPDATE_MSG_TYPE, ctx.getTenantId(), TbMsgMetaData.EMPTY, JacksonUtil.toString(msgData)), 0L); + } + + protected void invalidateDeviceProfileCache(DeviceId deviceId, String deviceJson) { + DeviceState deviceState = deviceStates.get(deviceId); + if (deviceState != null) { + DeviceProfileId currentProfileId = deviceState.getProfileId(); + Device device = JacksonUtil.fromString(deviceJson, Device.class); + if (!currentProfileId.equals(device.getDeviceProfileId())) { + removeDeviceState(deviceId); + } + } + } + + protected void invalidateDeviceProfileCache(DeviceId deviceId, DeviceProfileId deviceProfileId) { + DeviceState deviceState = deviceStates.get(deviceId); + if (deviceState != null) { + if (!deviceState.getProfileId().equals(deviceProfileId)) { + removeDeviceState(deviceId); + } + } + } + + private void removeDeviceState(DeviceId deviceId) { + DeviceState state = deviceStates.remove(deviceId); + if (config.isPersistAlarmRulesState() && (state != null || !config.isFetchAlarmRulesStateOnStart())) { + ctx.removeRuleNodeStateForEntity(deviceId); + } + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeConfiguration.java new file mode 100644 index 0000000000..0b32893c90 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeConfiguration.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.profile; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.codehaus.jackson.annotate.JsonIgnoreProperties; +import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; +import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class TbDeviceProfileNodeConfiguration implements NodeConfiguration { + + private boolean persistAlarmRulesState; + private boolean fetchAlarmRulesStateOnStart; + + @Override + public TbDeviceProfileNodeConfiguration defaultConfiguration() { + return new TbDeviceProfileNodeConfiguration(); + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedAlarmRuleState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedAlarmRuleState.java new file mode 100644 index 0000000000..57bc424874 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedAlarmRuleState.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.profile.state; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PersistedAlarmRuleState { + + private long lastEventTs; + private long duration; + private long eventCount; + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedAlarmState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedAlarmState.java new file mode 100644 index 0000000000..b8a657c4bb --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedAlarmState.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.profile.state; + +import lombok.Data; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; + +import java.util.Map; + +@Data +public class PersistedAlarmState { + + private Map createRuleStates; + private PersistedAlarmRuleState clearRuleState; + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedDeviceState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedDeviceState.java new file mode 100644 index 0000000000..c0acfc0768 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedDeviceState.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.profile.state; + +import lombok.Data; + +import java.util.Map; + +@Data +public class PersistedDeviceState { + + Map alarmStates; + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java index 57bde77f9c..f25d246a2e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java @@ -58,7 +58,7 @@ import java.util.concurrent.TimeUnit; @Data @Slf4j -class TbHttpClient { +public class TbHttpClient { private static final String STATUS = "status"; private static final String STATUS_CODE = "statusCode"; @@ -162,7 +162,7 @@ class TbHttpClient { } } - void processMessage(TbContext ctx, TbMsg msg) { + public void processMessage(TbContext ctx, TbMsg msg) { String endpointUrl = TbNodeUtils.processPattern(config.getRestEndpointUrlPattern(), msg.getMetaData()); HttpHeaders headers = prepareHeaders(msg.getMetaData()); HttpMethod method = HttpMethod.valueOf(config.getRequestMethod()); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java index 51363866bf..773fe4f9bf 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java @@ -46,7 +46,7 @@ import org.thingsboard.server.common.msg.TbMsg; public class TbRestApiCallNode implements TbNode { private boolean useRedisQueueForMsgPersistence; - private TbHttpClient httpClient; + protected TbHttpClient httpClient; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java index 3cccb2c2c5..bcc6ad60c8 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java @@ -15,12 +15,14 @@ */ package org.thingsboard.rule.engine.rest; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; import org.thingsboard.rule.engine.api.NodeConfiguration; import java.util.Collections; import java.util.Map; +@JsonIgnoreProperties(ignoreUnknown = true) @Data public class TbRestApiCallNodeConfiguration implements NodeConfiguration { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java index 2bd7f315fe..9f2c35b7ad 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java @@ -15,7 +15,7 @@ */ package org.thingsboard.rule.engine.rpc; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -82,7 +82,7 @@ public class TbSendRPCRequestNode implements TbNode { boolean oneway = !StringUtils.isEmpty(tmp) && Boolean.parseBoolean(tmp); tmp = msg.getMetaData().getValue("requestUUID"); - UUID requestUUID = !StringUtils.isEmpty(tmp) ? UUID.fromString(tmp) : UUIDs.timeBased(); + UUID requestUUID = !StringUtils.isEmpty(tmp) ? UUID.fromString(tmp) : Uuids.timeBased(); tmp = msg.getMetaData().getValue("originServiceId"); String originServiceId = !StringUtils.isEmpty(tmp) ? tmp : null; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java index b47e391b72..72c0686fee 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java @@ -40,7 +40,7 @@ import java.util.Set; configClazz = TbMsgAttributesNodeConfiguration.class, nodeDescription = "Saves attributes data", nodeDetails = "Saves entity attributes based on configurable scope parameter. Expects messages with 'POST_ATTRIBUTES_REQUEST' message type", - uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"}, + uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbActionNodeAttributesConfig", icon = "file_upload" ) @@ -70,8 +70,8 @@ public class TbMsgAttributesNode implements TbNode { msg.getOriginator(), config.getScope(), new ArrayList<>(attributes), - new TelemetryNodeCallback(ctx, msg), - config.getNotifyDevice() || StringUtils.isEmpty(notifyDeviceStr) || Boolean.parseBoolean(notifyDeviceStr) + config.getNotifyDevice() || StringUtils.isEmpty(notifyDeviceStr) || Boolean.parseBoolean(notifyDeviceStr), + new TelemetryNodeCallback(ctx, msg) ); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java index 912a830dd3..a415e08695 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java @@ -43,7 +43,7 @@ import java.util.Map; configClazz = TbMsgTimeseriesNodeConfiguration.class, nodeDescription = "Saves timeseries data", nodeDetails = "Saves timeseries telemetry data based on configurable TTL parameter. Expects messages with 'POST_TELEMETRY_REQUEST' message type", - uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"}, + uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbActionNodeTimeseriesConfig", icon = "file_upload" ) @@ -62,16 +62,7 @@ public class TbMsgTimeseriesNode implements TbNode { ctx.tellFailure(msg, new IllegalArgumentException("Unsupported msg type: " + msg.getType())); return; } - long ts = -1; - String tsStr = msg.getMetaData().getValue("ts"); - if (!StringUtils.isEmpty(tsStr)) { - try { - ts = Long.parseLong(tsStr); - } catch (NumberFormatException e) { - } - } else { - ts = msg.getTs(); - } + long ts = getTs(msg); String src = msg.getData(); Map> tsKvMap = JsonConverter.convertToTelemetry(new JsonParser().parse(src), ts); if (tsKvMap.isEmpty()) { @@ -89,6 +80,20 @@ public class TbMsgTimeseriesNode implements TbNode { ctx.getTelemetryService().saveAndNotify(ctx.getTenantId(), msg.getOriginator(), tsKvEntryList, ttl, new TelemetryNodeCallback(ctx, msg)); } + public static long getTs(TbMsg msg) { + long ts = -1; + String tsStr = msg.getMetaData().getValue("ts"); + if (!StringUtils.isEmpty(tsStr)) { + try { + ts = Long.parseLong(tsStr); + } catch (NumberFormatException e) { + } + } else { + ts = msg.getTs(); + } + return ts; + } + @Override public void destroy() { } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java index 15cd0c96ef..5d505f51b2 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java @@ -44,7 +44,7 @@ import java.util.HashSet; nodeDetails = "Related Entity found using configured relation direction and Relation Type. " + "If multiple Related Entities are found, only first Entity is used as new Originator, other entities are discarded.
" + "Alarm Originator found only in case original Originator is Alarm entity.", - uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"}, + uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbTransformationNodeChangeOriginatorConfig", icon = "find_replace" ) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java index 1cfdf058bd..56f15f42de 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java @@ -36,7 +36,7 @@ import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; "Should return the following structure:
" + "{ msg: new payload,
   metadata: new metadata,
   msgType: new msgType }

" + "All fields in resulting object are optional and will be taken from original message if not specified.", - uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"}, + uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbTransformationNodeScriptConfig") public class TbTransformMsgNode extends TbAbstractTransformNode { diff --git a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.css b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.css deleted file mode 100644 index b5109b5f90..0000000000 --- a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.css +++ /dev/null @@ -1,2 +0,0 @@ -.tb-message-type-autocomplete .tb-not-found{display:block;line-height:1.5;height:48px}.tb-message-type-autocomplete .tb-not-found .tb-no-entries{line-height:48px}.tb-message-type-autocomplete li{height:auto!important;white-space:normal!important}.tb-generator-config tb-json-content.tb-message-body,.tb-generator-config tb-json-object-edit.tb-metadata-json{height:200px;display:block}.tb-mqtt-config .tb-credentials-panel-group .tb-panel-title{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;min-width:90px}@media (min-width:960px){.tb-mqtt-config .tb-credentials-panel-group .tb-panel-title{min-width:180px}}.tb-mqtt-config .tb-credentials-panel-group .tb-panel-prompt{font-size:14px;color:rgba(0,0,0,.87);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tb-mqtt-config .tb-credentials-panel-group.disabled .tb-panel-prompt,.tb-mqtt-config .tb-credentials-panel-group.disabled .tb-panel-title{color:rgba(0,0,0,.38)}.tb-mqtt-config .tb-credentials-panel-group md-icon.md-expansion-panel-icon{margin-right:0}.tb-mqtt-config .tb-container{width:100%}.tb-mqtt-config .dropdown-messages .tb-error-message{padding:5px 0 0}.tb-kv-map-config .header{padding-left:5px;padding-right:5px;padding-bottom:5px}.tb-kv-map-config .header .cell{padding-left:5px;padding-right:5px;color:rgba(0,0,0,.54);font-size:12px;font-weight:700;white-space:nowrap}.tb-kv-map-config .body{padding-left:5px;padding-right:5px;padding-bottom:20px;max-height:300px;overflow:auto}.tb-kv-map-config .body .row{padding-top:5px;max-height:40px}.tb-kv-map-config .body .cell{padding-left:5px;padding-right:5px}.tb-kv-map-config .body md-input-container.cell{margin:0;max-height:40px}.tb-kv-map-config .body .md-button{margin:0} -/*# sourceMappingURL=rulenode-core-config.css.map*/ \ No newline at end of file diff --git a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js index fb9c4f5bf0..a8bd79d68b 100644 --- a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js +++ b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js @@ -1,6 +1,16 @@ -!function(e){function t(a){if(n[a])return n[a].exports;var i=n[a]={exports:{},id:a,loaded:!1};return e[a].call(i.exports,i,i.exports,t),i.loaded=!0,i.exports}var n={};return t.m=e,t.c=n,t.p="/static/",t(0)}(function(e){for(var t in e)if(Object.prototype.hasOwnProperty.call(e,t))switch(typeof e[t]){case"function":break;case"object":e[t]=function(t){var n=t.slice(1),a=e[t[0]];return function(e,t,i){a.apply(this,[e,t,i].concat(n))}}(e[t]);break;default:e[t]=e[e[t]]}return e}([function(e,t,n){e.exports=n(107)},function(e,t){},1,1,1,1,function(e,t){e.exports="
tb.rulenode.customer-name-pattern-required
tb.rulenode.customer-name-pattern-hint
{{ 'tb.rulenode.create-customer-if-not-exists' | translate }}
tb.rulenode.customer-cache-expiration-required
tb.rulenode.customer-cache-expiration-range
tb.rulenode.customer-cache-expiration-hint
"},function(e,t){e.exports="
{{scope.name | translate}} {{ 'tb.rulenode.notify-device' | translate }}
tb.rulenode.notify-device-hint
"},function(e,t){e.exports='
tb.rulenode.topic-required
tb.rulenode.hostname-required
tb.rulenode.device-id-required
{{ \'tb.rulenode.credentials\' | translate }}
{{ ruleNodeTypes.azureIotHubCredentialTypes[configuration.credentials.type].name | translate }}
{{ \'tb.rulenode.credentials\' | translate }}
{{ ruleNodeTypes.azureIotHubCredentialTypes[configuration.credentials.type].name | translate }}
{{credentialsValue.name | translate}}
tb.rulenode.credentials-type-required
tb.rulenode.sas-key-required
{{ \'action.remove\' | translate }} close
'},function(e,t){e.exports="
tb.rulenode.select-queue-hint
"},function(e,t){e.exports="
{{ 'tb.rulenode.test-details-function' | translate }}
tb.rulenode.alarm-type-required
tb.rulenode.entity-type-pattern-hint
"},function(e,t){e.exports="
{{ 'tb.rulenode.test-details-function' | translate }}
{{ 'tb.rulenode.use-message-alarm-data' | translate }}
tb.rulenode.alarm-type-required
tb.rulenode.entity-type-pattern-hint
{{ severity.name | translate}}
tb.rulenode.alarm-severity-required
{{ 'tb.rulenode.propagate' | translate }}
tb.rulenode.relation-types-list-hint
"},function(e,t){e.exports="
{{ ('relation.search-direction.' + direction) | translate}}
tb.rulenode.entity-name-pattern-required
tb.rulenode.entity-name-pattern-hint
tb.rulenode.entity-type-pattern-required
tb.rulenode.entity-type-pattern-hint
tb.rulenode.relation-type-pattern-required
tb.rulenode.relation-type-pattern-hint
{{ 'tb.rulenode.create-entity-if-not-exists' | translate }}
tb.rulenode.create-entity-if-not-exists-hint
{{ 'tb.rulenode.remove-current-relations' | translate }}
tb.rulenode.remove-current-relations-hint
{{ 'tb.rulenode.change-originator-to-related-entity' | translate }}
tb.rulenode.change-originator-to-related-entity-hint
tb.rulenode.entity-cache-expiration-required
tb.rulenode.entity-cache-expiration-range
tb.rulenode.entity-cache-expiration-hint
"},function(e,t){e.exports="
{{ 'tb.rulenode.delete-relation-to-specific-entity' | translate }}
tb.rulenode.delete-relation-hint
{{ ('relation.search-direction.' + direction) | translate}}
tb.rulenode.entity-name-pattern-required
tb.rulenode.entity-name-pattern-hint
tb.rulenode.relation-type-pattern-required
tb.rulenode.relation-type-pattern-hint
tb.rulenode.entity-cache-expiration-required
tb.rulenode.entity-cache-expiration-range
tb.rulenode.entity-cache-expiration-hint
"},function(e,t){e.exports="
tb.rulenode.message-count-required
tb.rulenode.min-message-count-message
tb.rulenode.period-seconds-required
tb.rulenode.min-period-seconds-message
{{ 'tb.rulenode.test-generator-function' | translate }}
"},function(e,t){e.exports='
tb.rulenode.latitude-key-name-required
tb.rulenode.longitude-key-name-required
{{ \'tb.rulenode.fetch-perimeter-info-from-message-metadata\' | translate }}
{{ type.name | translate}}
tb.rulenode.circle-center-latitude-required
tb.rulenode.circle-center-longitude-required
tb.rulenode.range-required
{{ type.name | translate}}
tb.rulenode.polygon-definition-required
tb.rulenode.polygon-definition-hint
tb.rulenode.min-inside-duration-value-required
tb.rulenode.time-value-range
tb.rulenode.time-value-range
{{timeUnit.name | translate}}
tb.rulenode.min-outside-duration-value-required
tb.rulenode.time-value-range
tb.rulenode.time-value-range
{{timeUnit.name | translate}}
'},function(e,t){e.exports='
tb.rulenode.topic-pattern-required
tb.rulenode.bootstrap-servers-required
tb.rulenode.min-retries-message
tb.rulenode.min-batch-size-bytes-message
tb.rulenode.min-linger-ms-message
tb.rulenode.min-buffer-memory-bytes-message
{{ ackValue }}
tb.rulenode.key-serializer-required
tb.rulenode.value-serializer-required
{{ \'tb.rulenode.add-metadata-key-values-as-kafka-headers\' | translate }}
tb.rulenode.add-metadata-key-values-as-kafka-headers-hint
{{charset.name | translate}}
'; -},function(e,t){e.exports="
{{ 'tb.rulenode.test-to-string-function' | translate }}
"},function(e,t){e.exports='
tb.rulenode.topic-pattern-required
tb.rulenode.mqtt-topic-pattern-hint
tb.rulenode.host-required
tb.rulenode.port-required
tb.rulenode.port-range
tb.rulenode.port-range
tb.rulenode.connect-timeout-required
tb.rulenode.connect-timeout-range
tb.rulenode.connect-timeout-range
{{ \'tb.rulenode.clean-session\' | translate }} {{ \'tb.rulenode.enable-ssl\' | translate }}
{{ \'tb.rulenode.credentials\' | translate }}
{{ ruleNodeTypes.mqttCredentialTypes[configuration.credentials.type].name | translate }}
{{ \'tb.rulenode.credentials\' | translate }}
{{ ruleNodeTypes.mqttCredentialTypes[configuration.credentials.type].name | translate }}
{{credentialsValue.name | translate}}
tb.rulenode.credentials-type-required
tb.rulenode.username-required
tb.rulenode.password-required
'},function(e,t){e.exports="
tb.rulenode.interval-seconds-required
tb.rulenode.min-interval-seconds-message
tb.rulenode.output-timeseries-key-prefix-required
"},function(e,t){e.exports='
{{ \'tb.rulenode.use-metadata-period-in-seconds-patterns\' | translate }}
tb.rulenode.use-metadata-period-in-seconds-patterns-hint
tb.rulenode.period-seconds-required
tb.rulenode.min-period-0-seconds-message
tb.rulenode.period-in-seconds-pattern-required
tb.rulenode.period-in-seconds-pattern-hint
tb.rulenode.max-pending-messages-required
tb.rulenode.max-pending-messages-range
tb.rulenode.max-pending-messages-range
'},function(e,t){e.exports="
tb.rulenode.gcp-project-id-required
tb.rulenode.pubsub-topic-name-required
{{ 'action.remove' | translate }} close
tb.rulenode.message-attributes-hint
"},function(e,t){e.exports='
{{ property }}
tb.rulenode.host-required
tb.rulenode.port-required
tb.rulenode.port-range
tb.rulenode.port-range
{{ \'tb.rulenode.automatic-recovery\' | translate }}
tb.rulenode.min-connection-timeout-ms-message
tb.rulenode.min-handshake-timeout-ms-message
'},function(e,t){e.exports='
tb.rulenode.endpoint-url-pattern-required
tb.rulenode.endpoint-url-pattern-hint
{{ type }} {{ \'tb.rulenode.enable-proxy\' | translate }} {{ \'tb.rulenode.use-simple-client-http-factory\' | translate }}
{{ \'tb.rulenode.use-system-proxy-properties\' | translate }}
{{proxyScheme}}
tb.rulenode.proxy-host-required
tb.rulenode.proxy-port-required
tb.rulenode.proxy-port-range
tb.rulenode.proxy-port-range
tb.rulenode.read-timeout-hint
tb.rulenode.max-parallel-requests-count-hint
tb.rulenode.headers-hint
{{ \'tb.rulenode.use-redis-queue\' | translate }}
{{ \'tb.rulenode.trim-redis-queue\' | translate }}
'},function(e,t){e.exports="
"},function(e,t){e.exports="
tb.rulenode.timeout-required
tb.rulenode.min-timeout-message
"},function(e,t){e.exports='
tb.rulenode.custom-table-name-required
tb.rulenode.custom-table-hint
'},function(e,t){e.exports='
{{ \'tb.rulenode.use-system-smtp-settings\' | translate }}
{{smtpProtocol.toUpperCase()}}
tb.rulenode.smtp-host-required
tb.rulenode.smtp-port-required
tb.rulenode.smtp-port-range
tb.rulenode.smtp-port-range
tb.rulenode.timeout-required
tb.rulenode.min-timeout-msec-message
{{ \'tb.rulenode.enable-tls\' | translate }} {{tlsVersion}} {{ \'tb.rulenode.enable-proxy\' | translate }}
tb.rulenode.proxy-host-required
tb.rulenode.proxy-port-required
tb.rulenode.proxy-port-range
tb.rulenode.proxy-port-range
'},function(e,t){e.exports="
tb.rulenode.topic-arn-pattern-required
tb.rulenode.topic-arn-pattern-hint
tb.rulenode.aws-access-key-id-required
tb.rulenode.aws-secret-access-key-required
tb.rulenode.aws-region-required
"},function(e,t){e.exports='
{{ type.name | translate }}
tb.rulenode.queue-url-pattern-required
tb.rulenode.queue-url-pattern-hint
tb.rulenode.min-delay-seconds-message
tb.rulenode.max-delay-seconds-message
tb.rulenode.message-attributes-hint
tb.rulenode.aws-access-key-id-required
tb.rulenode.aws-secret-access-key-required
tb.rulenode.aws-region-required
'; -},function(e,t){e.exports="
tb.rulenode.default-ttl-required
tb.rulenode.min-default-ttl-message
"},function(e,t){e.exports="
tb.rulenode.customer-name-pattern-required
tb.rulenode.customer-name-pattern-hint
tb.rulenode.customer-cache-expiration-required
tb.rulenode.customer-cache-expiration-range
tb.rulenode.customer-cache-expiration-hint
"},function(e,t){e.exports="
{{ 'alias.last-level-relation' | translate}}
{{ ('relation.search-direction.' + direction) | translate}}
relation.relation-type
device.device-types
"},function(e,t){e.exports="
{{ 'tb.rulenode.latest-telemetry' | translate }}
"},function(e,t){e.exports='
{{ \'tb.rulenode.tell-failure-if-absent\' | translate }}
tb.rulenode.tell-failure-if-absent-hint
{{ \'tb.rulenode.get-latest-value-with-ts\' | translate }}
tb.rulenode.get-latest-value-with-ts-hint
'},function(e,t){e.exports='
{{\'tb.rulenode.entity-details-\'+item.toLowerCase() | translate}} tb.rulenode.no-entity-details-matching {{\'tb.rulenode.entity-details-\'+$chip.toLowerCase() | translate}} {{ \'tb.rulenode.add-to-metadata\' | translate }}
tb.rulenode.add-to-metadata-hint
'},function(e,t){e.exports='
{{ type }}
tb.rulenode.fetch-mode-hint
{{ type }}
tb.rulenode.order-by-hint
tb.rulenode.limit-hint
{{ \'tb.rulenode.use-metadata-interval-patterns\' | translate }}
tb.rulenode.use-metadata-interval-patterns-hint
tb.rulenode.start-interval-value-required
tb.rulenode.time-value-range
tb.rulenode.time-value-range
{{timeUnit.name | translate}}
tb.rulenode.end-interval-value-required
tb.rulenode.time-value-range
tb.rulenode.time-value-range
{{timeUnit.name | translate}}
tb.rulenode.start-interval-pattern-required
tb.rulenode.start-interval-pattern-hint
tb.rulenode.end-interval-pattern-required
tb.rulenode.end-interval-pattern-hint
'},function(e,t){e.exports='
{{ \'tb.rulenode.tell-failure-if-absent\' | translate }}
tb.rulenode.tell-failure-if-absent-hint
{{ \'tb.rulenode.get-latest-value-with-ts\' | translate }}
tb.rulenode.get-latest-value-with-ts-hint
'},function(e,t){e.exports='
'},function(e,t){e.exports="
{{ 'tb.rulenode.latest-telemetry' | translate }}
"},33,function(e,t){e.exports="
{{'alarm.display-status.' + item | translate}} {{'alarm.display-status.' + $chip | translate}}
"},function(e,t){e.exports='
tb.rulenode.separator-hint
tb.rulenode.separator-hint
{{ \'tb.rulenode.check-all-keys\' | translate }}
tb.rulenode.check-all-keys-hint
'},function(e,t){e.exports="
{{ 'tb.rulenode.check-relation-to-specific-entity' | translate }}
tb.rulenode.check-relation-hint
{{ ('relation.search-direction.' + direction) | translate}}
"},function(e,t){e.exports='
tb.rulenode.latitude-key-name-required
tb.rulenode.longitude-key-name-required
{{ \'tb.rulenode.fetch-perimeter-info-from-message-metadata\' | translate }}
{{ type.name | translate}}
tb.rulenode.circle-center-latitude-required
tb.rulenode.circle-center-longitude-required
tb.rulenode.range-required
{{ type.name | translate}}
tb.rulenode.polygon-definition-required
tb.rulenode.polygon-definition-hint
'},function(e,t){e.exports='
{{item}}
tb.rulenode.no-message-types-found
tb.rulenode.no-message-type-matching tb.rulenode.create-new-message-type
{{$chip.name}}
'},function(e,t){e.exports='
'},function(e,t){e.exports="
{{ 'tb.rulenode.test-filter-function' | translate }}
"},function(e,t){e.exports="
{{ 'tb.rulenode.test-switch-function' | translate }}
"},function(e,t){e.exports='
{{ keyText }} {{ valText }}  
{{keyRequiredText}}
{{valRequiredText}}
{{ \'tb.key-val.remove-entry\' | translate }} close
{{ \'tb.key-val.add-entry\' | translate }} add {{ \'action.add\' | translate }}
'},function(e,t){e.exports="
{{ 'alias.last-level-relation' | translate}}
{{ ('relation.search-direction.' + direction) | translate}}
relation.relation-filters
"},function(e,t){e.exports='
{{ source.name | translate}}
'},function(e,t){e.exports="
{{ 'tb.rulenode.test-transformer-function' | translate }}
"},function(e,t){e.exports="
tb.rulenode.from-template-required
tb.rulenode.from-template-hint
tb.rulenode.to-template-required
tb.rulenode.mail-address-list-template-hint
tb.rulenode.mail-address-list-template-hint
tb.rulenode.mail-address-list-template-hint
tb.rulenode.subject-template-required
tb.rulenode.subject-template-hint
tb.rulenode.body-template-required
tb.rulenode.body-template-hint
"; -},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t){var n=function(n,a,i,r){var l=o.default;a.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}i.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(6),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t){var n=function(n,a,i,r){var l=o.default;a.html(l),n.types=t,n.$watch("configuration",function(e,t){e&&angular.isUndefined(e.notifyDevice)&&(n.configuration.notifyDevice=!0),angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}i.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(7),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t,n){var a=function(a,i,r,l){var s=o.default;i.html(s),a.$mdExpansionPanel=t,a.ruleNodeTypes=n,a.credentialsTypeChanged=function(){var e=a.configuration.credentials.type;a.configuration.credentials={},a.configuration.credentials.type=e,a.updateValidity()},a.certFileAdded=function(e,t){var n=new FileReader;n.onload=function(n){a.$apply(function(){if(n.target.result){l.$setDirty();var i=n.target.result;if(i&&i.length>0)switch(t){case"caCert":a.configuration.credentials.caCertFileName=e.name,a.configuration.credentials.caCert=i;break;case"privateKey":a.configuration.credentials.privateKeyFileName=e.name,a.configuration.credentials.privateKey=i;break;case"Cert":a.configuration.credentials.certFileName=e.name,a.configuration.credentials.cert=i}a.updateValidity()}})},n.readAsText(e.file)},a.clearCertFile=function(e){switch(l.$setDirty(),e){case"caCert":a.configuration.credentials.caCertFileName=null,a.configuration.credentials.caCert=null;break;case"privateKey":a.configuration.credentials.privateKeyFileName=null,a.configuration.credentials.privateKey=null;break;case"Cert":a.configuration.credentials.certFileName=null,a.configuration.credentials.cert=null}a.updateValidity()},a.updateValidity=function(){var e=!0,t=a.configuration.credentials;t.type==n.mqttCredentialTypes["cert.PEM"].value&&(t.cert&&t.privateKey||(e=!1)),l.$setValidity("Certs",e)},a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},e(i.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:a}}i.$inject=["$compile","$mdExpansionPanel","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i,n(1);var r=n(8),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t){var n=function(n,a,i,r){var l=o.default;a.html(l),n.types=t,n.serviceType="TB_RULE_ENGINE",n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}i.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(9),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t,n,a){var i=function(i,r,l,s){var d=o.default;r.html(d),i.types=n,i.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(i.configuration)}),s.$render=function(){i.configuration=s.$viewValue},i.testDetailsBuildJs=function(e){var n=angular.copy(i.configuration.alarmDetailsBuildJs);a.testNodeScript(e,n,"json",t.instant("tb.rulenode.details")+"","Details",["msg","metadata","msgType"],i.ruleNodeId).then(function(e){i.configuration.alarmDetailsBuildJs=e,s.$setDirty()})},e(r.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:i}}i.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(10),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t,n,a){var i=function(i,r,l,s){var d=o.default;r.html(d),i.types=n,i.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(i.configuration)}),s.$render=function(){i.configuration=s.$viewValue,i.configuration.hasOwnProperty("relationTypes")||(i.configuration.relationTypes=[])},i.testDetailsBuildJs=function(e){var n=angular.copy(i.configuration.alarmDetailsBuildJs);a.testNodeScript(e,n,"json",t.instant("tb.rulenode.details")+"","Details",["msg","metadata","msgType"],i.ruleNodeId).then(function(e){i.configuration.alarmDetailsBuildJs=e,s.$setDirty()})},e(r.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:i}}i.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(11),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t){var n=function(n,a,i,r){var l=o.default;a.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}i.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(12),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t){var n=function(n,a,i,r){var l=o.default;a.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}i.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(13),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t,n,a){var i=function(i,r,l,s){var d=o.default;r.html(d),i.types=n,i.originator=null,i.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(i.configuration)}),s.$render=function(){i.configuration=s.$viewValue,i.configuration.originatorId&&i.configuration.originatorType?i.originator={id:i.configuration.originatorId,entityType:i.configuration.originatorType}:i.originator=null,i.$watch("originator",function(e,t){angular.equals(e,t)||(i.originator?(s.$viewValue.originatorId=i.originator.id,s.$viewValue.originatorType=i.originator.entityType):(s.$viewValue.originatorId=null,s.$viewValue.originatorType=null))},!0)},i.testScript=function(e){var n=angular.copy(i.configuration.jsScript);a.testNodeScript(e,n,"generate",t.instant("tb.rulenode.generator")+"","Generate",["prevMsg","prevMetadata","prevMsgType"],i.ruleNodeId).then(function(e){i.configuration.jsScript=e,s.$setDirty()})},e(r.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:i}}i.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i,n(2);var r=n(14),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t){var n=function(n,a,i,r){var l=o.default;a.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}i.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(15),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var i=n(79),r=a(i),o=n(55),l=a(o),s=n(62),d=a(s),u=n(59),c=a(u),m=n(58),g=a(m),p=n(66),f=a(p),b=n(73),v=a(b),y=n(74),h=a(y),q=n(72),x=a(q),k=n(65),$=a(k),T=n(77),C=a(T),w=n(78),M=a(w),N=n(71),S=a(N),F=n(67),P=a(F),_=n(76),E=a(_),A=n(69),V=a(A),I=n(68),j=a(I),O=n(54),D=a(O),K=n(80),L=a(K),R=n(61),U=a(R),z=n(60),H=a(z),B=n(75),G=a(B),Y=n(63),Q=a(Y),W=n(70),J=a(W),Z=n(57),X=a(Z),ee=n(56),te=a(ee);t.default=angular.module("thingsboard.ruleChain.config.action",[]).directive("tbActionNodeTimeseriesConfig",r.default).directive("tbActionNodeAttributesConfig",l.default).directive("tbActionNodeGeneratorConfig",d.default).directive("tbActionNodeCreateAlarmConfig",c.default).directive("tbActionNodeClearAlarmConfig",g.default).directive("tbActionNodeLogConfig",f.default).directive("tbActionNodeRpcReplyConfig",v.default).directive("tbActionNodeRpcRequestConfig",h.default).directive("tbActionNodeRestApiCallConfig",x.default).directive("tbActionNodeKafkaConfig",$.default).directive("tbActionNodeSnsConfig",C.default).directive("tbActionNodeSqsConfig",M.default).directive("tbActionNodeRabbitMqConfig",S.default).directive("tbActionNodeMqttConfig",P.default).directive("tbActionNodeSendEmailConfig",E.default).directive("tbActionNodeMsgDelayConfig",V.default).directive("tbActionNodeMsgCountConfig",j.default).directive("tbActionNodeAssignToCustomerConfig",D.default).directive("tbActionNodeUnAssignToCustomerConfig",L.default).directive("tbActionNodeDeleteRelationConfig",U.default).directive("tbActionNodeCreateRelationConfig",H.default).directive("tbActionNodeCustomTableConfig",G.default).directive("tbActionNodeGpsGeofencingConfig",Q.default).directive("tbActionNodePubSubConfig",J.default).directive("tbActionNodeCheckPointConfig",X.default).directive("tbActionNodeAzureIotHubConfig",te.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t){var n=function(n,a,i,r){var l=o.default;a.html(l),n.ackValues=["all","-1","0","1"],n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue,n.configuration.hasOwnProperty("kafkaHeadersCharset")||(n.configuration.kafkaHeadersCharset="UTF-8")},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}i.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(16),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t,n){var a=function(a,i,r,l){var s=o.default;i.html(s),a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},a.testScript=function(e){var i=angular.copy(a.configuration.jsScript);n.testNodeScript(e,i,"string",t.instant("tb.rulenode.to-string")+"","ToString",["msg","metadata","msgType"],a.ruleNodeId).then(function(e){a.configuration.jsScript=e,l.$setDirty()})},e(i.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}i.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(17),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t,n){var a=function(a,i,r,l){var s=o.default;i.html(s),a.$mdExpansionPanel=t,a.ruleNodeTypes=n,a.credentialsTypeChanged=function(){var e=a.configuration.credentials.type;a.configuration.credentials={},a.configuration.credentials.type=e,a.updateValidity()},a.certFileAdded=function(e,t){var n=new FileReader;n.onload=function(n){a.$apply(function(){if(n.target.result){l.$setDirty();var i=n.target.result;i&&i.length>0&&("caCert"==t&&(a.configuration.credentials.caCertFileName=e.name,a.configuration.credentials.caCert=i),"privateKey"==t&&(a.configuration.credentials.privateKeyFileName=e.name,a.configuration.credentials.privateKey=i),"Cert"==t&&(a.configuration.credentials.certFileName=e.name,a.configuration.credentials.cert=i)),a.updateValidity()}})},n.readAsText(e.file)},a.clearCertFile=function(e){l.$setDirty(),"caCert"==e&&(a.configuration.credentials.caCertFileName=null,a.configuration.credentials.caCert=null),"privateKey"==e&&(a.configuration.credentials.privateKeyFileName=null,a.configuration.credentials.privateKey=null),"Cert"==e&&(a.configuration.credentials.certFileName=null,a.configuration.credentials.cert=null),a.updateValidity()},a.updateValidity=function(){var e=!0,t=a.configuration.credentials;t.type==n.mqttCredentialTypes["cert.PEM"].value&&(t.caCert&&t.cert&&t.privateKey||(e=!1)),l.$setValidity("Certs",e)},a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},e(i.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:a}}i.$inject=["$compile","$mdExpansionPanel","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i,n(1);var r=n(18),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e){var t=function(t,n,a,i){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||i.$setViewValue(t.configuration)}),i.$render=function(){t.configuration=i.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}i.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(19),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e){var t=function(t,n,a,i){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||i.$setViewValue(t.configuration)}),i.$render=function(){t.configuration=i.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}i.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(20),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t){var n=function(n,a,i,r){var l=o.default;a.html(l),n.ruleNodeTypes=t,n.serviceAccountFileAdded=function(e){var t=new FileReader;t.onload=function(t){n.$apply(function(){if(t.target.result){r.$setDirty();var a=t.target.result;a&&a.length>0&&(n.configuration.serviceAccountKeyFileName=e.name,n.configuration.serviceAccountKey=a),n.updateValidity()}})},t.readAsText(e.file)},n.clearServiceAccountFile=function(){r.$setDirty(),n.configuration.serviceAccountKeyFileName=null,n.configuration.serviceAccountKey=null,n.updateValidity()},n.updateValidity=function(){var e=!0,t=n.configuration;t.serviceAccountKeyFileName&&t.serviceAccountKey||(e=!1),r.$setValidity("SAKey",e)},n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}i.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(21),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e){var t=function(t,n,a,i){var r=o.default;n.html(r),t.messageProperties=[null,"BASIC","TEXT_PLAIN","MINIMAL_BASIC","MINIMAL_PERSISTENT_BASIC","PERSISTENT_BASIC","PERSISTENT_TEXT_PLAIN"],t.$watch("configuration",function(e,n){angular.equals(e,n)||i.$setViewValue(t.configuration)}),i.$render=function(){t.configuration=i.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:t}}i.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(22),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t){var n=function(n,a,i,r){var l=o.default;a.html(l),n.ruleNodeTypes=t,n.proxySchemes=["http","https"],n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}i.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(23),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e){var t=function(t,n,a,i){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||i.$setViewValue(t.configuration)}),i.$render=function(){t.configuration=i.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}i.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(24),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e){var t=function(t,n,a,i){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||i.$setViewValue(t.configuration)}),i.$render=function(){t.configuration=i.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}i.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(25),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e){var t=function(t,n,a,i){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||i.$setViewValue(t.configuration)}),i.$render=function(){t.configuration=i.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}i.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(26),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e){var t=function(t,n,a,i){var r=o.default;n.html(r),t.smtpProtocols=["smtp","smtps"],t.tlsVersions=["TLSv1","TLSv1.1","TLSv1.2","TLSv1.3"],t.$watch("configuration",function(e,n){angular.equals(e,n)||i.$setViewValue(t.configuration)}),i.$render=function(){t.configuration=i.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:t}}i.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(27),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e){var t=function(t,n,a,i){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||i.$setViewValue(t.configuration)}),i.$render=function(){t.configuration=i.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}i.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(28),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t){var n=function(n,a,i,r){var l=o.default;a.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}i.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(29),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e){var t=function(t,n,a,i){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||i.$setViewValue(t.configuration)}),i.$render=function(){t.configuration=i.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}i.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(30),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t){var n=function(n,a,i,r){var l=o.default;a.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}i.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(31),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t){var n=function(n,a,i,r){var l=o.default;a.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||r.$setViewValue(n.query)}),r.$render=function(){n.query=r.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}i.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(32),o=a(r)},function(e,t){"use strict";function n(e){var t=function(t,n,a,i){n.html("
"),t.$watch("configuration",function(e,n){angular.equals(e,n)||i.$setViewValue(t.configuration)}),i.$render=function(){t.configuration=i.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}n.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e){var t=function(t,n,a,i){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||i.$setViewValue(t.configuration)}),i.$render=function(){t.configuration=i.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}i.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(33),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t){var n=function(n,a,i,r){var l=o.default;a.html(l);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}i.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(34),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t){var n=function(n,a,i,r){var l=o.default;a.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),n.entityDetailsList=[];for(var s in t.entityDetails){var d=s;n.entityDetailsList.push(d)}r.$render=function(){n.configuration=r.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}i.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(35),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t,n){var a=function(a,i,r,l){var s=o.default;i.html(s);var d=186;a.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,d],a.ruleNodeTypes=n,a.aggPeriodTimeUnits={},a.aggPeriodTimeUnits.MINUTES=n.timeUnit.MINUTES,a.aggPeriodTimeUnits.HOURS=n.timeUnit.HOURS,a.aggPeriodTimeUnits.DAYS=n.timeUnit.DAYS,a.aggPeriodTimeUnits.MILLISECONDS=n.timeUnit.MILLISECONDS,a.aggPeriodTimeUnits.SECONDS=n.timeUnit.SECONDS,a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},e(i.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{},link:a}}i.$inject=["$compile","$mdConstant","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(36),o=a(r);n(3)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var i=n(88),r=a(i),o=n(89),l=a(o),s=n(84),d=a(s),u=n(90),c=a(u),m=n(83),g=a(m),p=n(91),f=a(p),b=n(86),v=a(b),y=n(85),h=a(y);t.default=angular.module("thingsboard.ruleChain.config.enrichment",[]).directive("tbEnrichmentNodeOriginatorAttributesConfig",r.default).directive("tbEnrichmentNodeOriginatorFieldsConfig",l.default).directive("tbEnrichmentNodeDeviceAttributesConfig",d.default).directive("tbEnrichmentNodeRelatedAttributesConfig",c.default).directive("tbEnrichmentNodeCustomerAttributesConfig",g.default).directive("tbEnrichmentNodeTenantAttributesConfig",f.default).directive("tbEnrichmentNodeGetTelemetryFromDatabase",v.default).directive("tbEnrichmentNodeEntityDetailsConfig",h.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t){var n=function(n,a,i,r){var l=o.default;a.html(l);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}i.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(37),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e){var t=function(t,n,a,i){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||i.$setViewValue(t.configuration)}),i.$render=function(){t.configuration=i.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}i.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(38),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e){var t=function(t,n,a,i){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||i.$setViewValue(t.configuration)}),i.$render=function(){t.configuration=i.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}i.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(39),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e){var t=function(t,n,a,i){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||i.$setViewValue(t.configuration)}),i.$render=function(){t.configuration=i.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}i.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(40),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t){var n=function(n,a,i,r){var l=o.default;a.html(l),n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),n.alarmStatusList=[];for(var s in t.alarmStatus)n.alarmStatusList.push(t.alarmStatus[s]);r.$render=function(){n.configuration=r.$viewValue},n.getAlarmStatusList=function(){return n.alarmStatusList.filter(function(e){return n.configuration.alarmStatusList.indexOf(e)===-1})},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",readonly:"=ngReadonly"},link:n}}i.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(41),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t){var n=function(n,a,i,r){var l=o.default;a.html(l);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}i.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(42),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t){var n=function(n,a,i,r){var l=o.default;a.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}i.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(43),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t){var n=function(n,a,i,r){var l=o.default;a.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}i.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(44),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var i=n(99),r=a(i),o=n(97),l=a(o),s=n(100),d=a(s),u=n(94),c=a(u),m=n(98),g=a(m),p=n(93),f=a(p),b=n(95),v=a(b),y=n(92),h=a(y);t.default=angular.module("thingsboard.ruleChain.config.filter",[]).directive("tbFilterNodeScriptConfig",r.default).directive("tbFilterNodeMessageTypeConfig",l.default).directive("tbFilterNodeSwitchConfig",d.default).directive("tbFilterNodeCheckRelationConfig",c.default).directive("tbFilterNodeOriginatorTypeConfig",g.default).directive("tbFilterNodeCheckMessageConfig",f.default).directive("tbFilterNodeGpsGeofencingConfig",v.default).directive("tbFilterNodeCheckAlarmStatusConfig",h.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t,n){var a=function(a,i,r,l){function s(){if(l.$viewValue){for(var e=[],t=0;t-1&&t.kvList.splice(e,1)}function l(){t.kvList||(t.kvList=[]),t.kvList.push({key:"",value:""})}function s(){var e={};t.kvList.forEach(function(t){t.key&&(e[t.key]=t.value)}),i.$setViewValue(e),d()}function d(){var e=!0;t.required&&!t.kvList.length&&(e=!1),i.$setValidity("kvMap",e)}var u=o.default;n.html(u),t.ngModelCtrl=i,t.removeKeyVal=r,t.addKeyVal=l,t.kvList=[],t.$watch("query",function(e,n){angular.equals(e,n)||i.$setViewValue(t.query)}),i.$render=function(){if(i.$viewValue){var e=i.$viewValue;t.kvList.length=0;for(var n in e)t.kvList.push({key:n,value:e[n]})}t.$watch("kvList",function(e,t){angular.equals(e,t)||s()},!0),d()},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",disabled:"=ngDisabled",requiredText:"=",keyText:"=",keyRequiredText:"=",valText:"=",valRequiredText:"="},link:t}}i.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(49),o=a(r);n(5)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t){var n=function(n,a,i,r){var l=o.default;a.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||r.$setViewValue(n.query)}),r.$render=function(){n.query=r.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}i.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(50),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t){var n=function(n,a,i,r){var l=o.default;a.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}i.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(51),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var i=n(103),r=a(i),o=n(105),l=a(o),s=n(106),d=a(s);t.default=angular.module("thingsboard.ruleChain.config.transform",[]).directive("tbTransformationNodeChangeOriginatorConfig",r.default).directive("tbTransformationNodeScriptConfig",l.default).directive("tbTransformationNodeToEmailConfig",d.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t,n){var a=function(a,i,r,l){var s=o.default;i.html(s),a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},a.testScript=function(e){var i=angular.copy(a.configuration.jsScript);n.testNodeScript(e,i,"update",t.instant("tb.rulenode.transformer")+"","Transform",["msg","metadata","msgType"],a.ruleNodeId).then(function(e){a.configuration.jsScript=e,l.$setDirty()})},e(i.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}i.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(52),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e){var t=function(t,n,a,i){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||i.$setViewValue(t.configuration)}),i.$render=function(){t.configuration=i.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}i.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(53),o=a(r)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var i=n(110),r=a(i),o=n(96),l=a(o),s=n(87),d=a(s),u=n(104),c=a(u),m=n(64),g=a(m),p=n(82),f=a(p),b=n(102),v=a(b),y=n(81),h=a(y),q=n(101),x=a(q),k=n(109),$=a(k);t.default=angular.module("thingsboard.ruleChain.config",[r.default,l.default,d.default,c.default,g.default]).directive("tbNodeEmptyConfig",f.default).directive("tbRelationsQueryConfig",v.default).directive("tbDeviceRelationsQueryConfig",h.default).directive("tbKvMapConfig",x.default).config($.default).name},function(e,t){"use strict";function n(e){var t={tb:{rulenode:{"create-entity-if-not-exists":"Create new entity if not exists","create-entity-if-not-exists-hint":"Create a new entity set above if it does not exist.","entity-name-pattern":"Name pattern","entity-name-pattern-required":"Name pattern is required","entity-name-pattern-hint":"Name pattern, use ${metaKeyName} to substitute variables from metadata","entity-type-pattern":"Type pattern","entity-type-pattern-required":"Type pattern is required","entity-type-pattern-hint":"Type pattern, use ${metaKeyName} to substitute variables from metadata","entity-cache-expiration":"Entities cache expiration time (sec)","entity-cache-expiration-hint":"Specifies maximum time interval allowed to store found entity records. 0 value means that records will never expire.","entity-cache-expiration-required":"Entities cache expiration time is required.","entity-cache-expiration-range":"Entities cache expiration time should be greater than or equal to 0.","customer-name-pattern":"Customer name pattern","customer-name-pattern-required":"Customer name pattern is required","create-customer-if-not-exists":"Create new customer if not exists","customer-cache-expiration":"Customers cache expiration time (sec)","customer-name-pattern-hint":"Customer name pattern, use ${metaKeyName} to substitute variables from metadata","customer-cache-expiration-hint":"Specifies maximum time interval allowed to store found customer records. 0 value means that records will never expire.","customer-cache-expiration-required":"Customers cache expiration time is required.","customer-cache-expiration-range":"Customers cache expiration time should be greater than or equal to 0.","start-interval":"Start Interval","end-interval":"End Interval","start-interval-time-unit":"Start Interval Time Unit","end-interval-time-unit":"End Interval Time Unit","fetch-mode":"Fetch mode","fetch-mode-hint":"If selected fetch mode 'ALL' you able to choose telemetry sampling order.","order-by":"Order by","order-by-hint":"Select to choose telemetry sampling order.",limit:"Limit","limit-hint":"Min limit value is 2, max - 1000. In case you want to fetch a single entry, select fetch mode 'FIRST' or 'LAST'.","time-unit-milliseconds":"Milliseconds","time-unit-seconds":"Seconds","time-unit-minutes":"Minutes","time-unit-hours":"Hours","time-unit-days":"Days","time-value-range":"Time value should be in a range from 1 to 2147483647'.","start-interval-value-required":"Start interval value is required.","end-interval-value-required":"End interval value is required.",filter:"Filter",switch:"Switch","message-type":"Message type","message-type-required":"Message type is required.","message-types-filter":"Message types filter","no-message-types-found":"No message types found","no-message-type-matching":"'{{messageType}}' not found.","create-new-message-type":"Create a new one!","message-types-required":"Message types are required.","client-attributes":"Client attributes","shared-attributes":"Shared attributes","server-attributes":"Server attributes","notify-device":"Notify Device","notify-device-hint":"If the message arrives from the device, we will push it back to the device by default.","latest-timeseries":"Latest timeseries","data-keys":"Message data","metadata-keys":"Message metadata","relations-query":"Relations query","device-relations-query":"Device relations query","max-relation-level":"Max relation level","relation-type-pattern":"Relation type pattern","relation-type-pattern-hint":"Relation type pattern, use ${metaKeyName} to substitute variables from metadata","relation-type-pattern-required":"Relation type pattern is required","relation-types-list":"Relation types to propagate","relation-types-list-hint":"If Propagate relation types are not selected, alarms will be propagated without filtering by relation type.","unlimited-level":"Unlimited level","latest-telemetry":"Latest telemetry","attr-mapping":"Attributes mapping","source-attribute":"Source attribute","source-attribute-required":"Source attribute is required.","source-telemetry":"Source telemetry","source-telemetry-required":"Source telemetry is required.","target-attribute":"Target attribute","target-attribute-required":"Target attribute is required.","attr-mapping-required":"At least one attribute mapping should be specified.","fields-mapping":"Fields mapping","fields-mapping-required":"At least one field mapping should be specified.","source-field":"Source field","source-field-required":"Source field is required.","originator-source":"Originator source","originator-customer":"Customer","originator-tenant":"Tenant","originator-related":"Related","originator-alarm-originator":"Alarm Originator","clone-message":"Clone message",transform:"Transform","default-ttl":"Default TTL in seconds","default-ttl-required":"Default TTL is required.","min-default-ttl-message":"Only 0 minimum TTL is allowed.","message-count":"Message count (0 - unlimited)","message-count-required":"Message count is required.","min-message-count-message":"Only 0 minimum message count is allowed.","period-seconds":"Period in seconds","period-seconds-required":"Period is required.","use-metadata-period-in-seconds-patterns":"Use metadata period in seconds pattern","use-metadata-period-in-seconds-patterns-hint":"If selected, rule node use period in seconds interval pattern from message metadata assuming that intervals are in the seconds.","period-in-seconds-pattern":"Period in seconds metadata pattern","period-in-seconds-pattern-required":"Period in seconds pattern is required","period-in-seconds-pattern-hint":"Period in seconds pattern, use ${metaKeyName} to substitute variables from metadata","min-period-seconds-message":"Only 1 second minimum period is allowed.",originator:"Originator","message-body":"Message body","message-metadata":"Message metadata",generate:"Generate","test-generator-function":"Test generator function",generator:"Generator","test-filter-function":"Test filter function","test-switch-function":"Test switch function","test-transformer-function":"Test transformer function",transformer:"Transformer","alarm-create-condition":"Alarm create condition","test-condition-function":"Test condition function","alarm-clear-condition":"Alarm clear condition","alarm-details-builder":"Alarm details builder","test-details-function":"Test details function","alarm-type":"Alarm type","alarm-type-required":"Alarm type is required.","alarm-severity":"Alarm severity","alarm-severity-required":"Alarm severity is required","alarm-statuses-filter":"Alarm statuses filter","alarm-statuses-required":"Alarm statuses is required",propagate:"Propagate",condition:"Condition",details:"Details","to-string":"To string","test-to-string-function":"Test to string function","from-template":"From Template","from-template-required":"From Template is required","from-template-hint":"From address template, use ${metaKeyName} to substitute variables from metadata","to-template":"To Template","to-template-required":"To Template is required","mail-address-list-template-hint":"Comma separated address list, use ${metaKeyName} to substitute variables from metadata","cc-template":"Cc Template","bcc-template":"Bcc Template","subject-template":"Subject Template","subject-template-required":"Subject Template is required","subject-template-hint":"Mail subject template, use ${metaKeyName} to substitute variables from metadata","body-template":"Body Template","body-template-required":"Body Template is required","body-template-hint":"Mail body template, use ${metaKeyName} to substitute variables from metadata","request-id-metadata-attribute":"Request Id Metadata attribute name","timeout-sec":"Timeout in seconds","timeout-required":"Timeout is required","min-timeout-message":"Only 0 minimum timeout value is allowed.","endpoint-url-pattern":"Endpoint URL pattern","endpoint-url-pattern-required":"Endpoint URL pattern is required","endpoint-url-pattern-hint":"HTTP URL address pattern, use ${metaKeyName} to substitute variables from metadata","request-method":"Request method","use-simple-client-http-factory":"Use simple client HTTP factory","read-timeout":"Read timeout in millis","read-timeout-hint":"The value of 0 means an infinite timeout","max-parallel-requests-count":"Max number of parallel requests","max-parallel-requests-count-hint":"The value of 0 specifies no limit in parallel processing",headers:"Headers","headers-hint":"Use ${metaKeyName} in header/value fields to substitute variables from metadata",header:"Header","header-required":"Header is required",value:"Value","value-required":"Value is required","topic-pattern":"Topic pattern","topic-pattern-required":"Topic pattern is required",topic:"Topic","topic-required":"Topic is required","mqtt-topic-pattern-hint":"MQTT topic pattern, use ${metaKeyName} to substitute variables from metadata","bootstrap-servers":"Bootstrap servers","bootstrap-servers-required":"Bootstrap servers value is required","other-properties":"Other properties",key:"Key","key-required":"Key is required",retries:"Automatically retry times if fails","min-retries-message":"Only 0 minimum retries is allowed.","batch-size-bytes":"Produces batch size in bytes","min-batch-size-bytes-message":"Only 0 minimum batch size is allowed.","linger-ms":"Time to buffer locally (ms)","min-linger-ms-message":"Only 0 ms minimum value is allowed.","buffer-memory-bytes":"Client buffer max size in bytes","min-buffer-memory-message":"Only 0 minimum buffer size is allowed.",acks:"Number of acknowledgments","key-serializer":"Key serializer","key-serializer-required":"Key serializer is required","value-serializer":"Value serializer","value-serializer-required":"Value serializer is required","topic-arn-pattern":"Topic ARN pattern","topic-arn-pattern-required":"Topic ARN pattern is required","topic-arn-pattern-hint":"Topic ARN pattern, use ${metaKeyName} to substitute variables from metadata","aws-access-key-id":"AWS Access Key ID","aws-access-key-id-required":"AWS Access Key ID is required","aws-secret-access-key":"AWS Secret Access Key","aws-secret-access-key-required":"AWS Secret Access Key is required","aws-region":"AWS Region","aws-region-required":"AWS Region is required","exchange-name-pattern":"Exchange name pattern","routing-key-pattern":"Routing key pattern","message-properties":"Message properties",host:"Host","host-required":"Host is required",port:"Port","port-required":"Port is required","port-range":"Port should be in a range from 1 to 65535.","virtual-host":"Virtual host",username:"Username",password:"Password","automatic-recovery":"Automatic recovery","connection-timeout-ms":"Connection timeout (ms)","min-connection-timeout-ms-message":"Only 0 ms minimum value is allowed.","handshake-timeout-ms":"Handshake timeout (ms)","min-handshake-timeout-ms-message":"Only 0 ms minimum value is allowed.","client-properties":"Client properties","queue-url-pattern":"Queue URL pattern","queue-url-pattern-required":"Queue URL pattern is required","queue-url-pattern-hint":"Queue URL pattern, use ${metaKeyName} to substitute variables from metadata","delay-seconds":"Delay (seconds)","min-delay-seconds-message":"Only 0 seconds minimum value is allowed.","max-delay-seconds-message":"Only 900 seconds maximum value is allowed.",name:"Name","name-required":"Name is required","queue-type":"Queue type","sqs-queue-standard":"Standard","sqs-queue-fifo":"FIFO","gcp-project-id":"GCP project ID","gcp-project-id-required":"GCP project ID is required","gcp-service-account-key":"GCP service account key file","gcp-service-account-key-required":"GCP service account key file is required","pubsub-topic-name":"Topic name","pubsub-topic-name-required":"Topic name is required","message-attributes":"Message attributes","message-attributes-hint":"Use ${metaKeyName} in name/value fields to substitute variables from metadata","connect-timeout":"Connection timeout (sec)","connect-timeout-required":"Connection timeout is required.","connect-timeout-range":"Connection timeout should be in a range from 1 to 200.","client-id":"Client ID","device-id":"Device ID","device-id-required":"Device ID is required.","clean-session":"Clean session","enable-ssl":"Enable SSL",credentials:"Credentials","credentials-type":"Credentials type","credentials-type-required":"Credentials type is required.","credentials-anonymous":"Anonymous","credentials-basic":"Basic","credentials-pem":"PEM","credentials-sas":"Shared Access Signature","sas-key":"SAS Key","sas-key-required":"SAS Key is required.",hostname:"Hostname","hostname-required":"Hostname is required","azure-ca-cert":"CA certificate file","username-required":"Username is required.","password-required":"Password is required.","ca-cert":"CA certificate file *","private-key":"Private key file *",cert:"Certificate file *","no-file":"No file selected.","drop-file":"Drop a file or click to select a file to upload.","private-key-password":"Private key password","use-system-smtp-settings":"Use system SMTP settings","use-metadata-interval-patterns":"Use metadata interval patterns","use-metadata-interval-patterns-hint":"If selected, rule node use start and end interval patterns from message metadata assuming that intervals are in the milliseconds.","use-message-alarm-data":"Use message alarm data","check-all-keys":"Check that all selected keys are present","check-all-keys-hint":"If selected, checks that all specified keys are present in the message data and metadata.","check-relation-to-specific-entity":"Check relation to specific entity","check-relation-hint":"Checks existence of relation to specific entity or to any entity based on direction and relation type.","delete-relation-to-specific-entity":"Delete relation to specific entity","delete-relation-hint":"Deletes relation from the originator of the incoming message to the specified entity or list of entities based on direction and type.","remove-current-relations":"Remove current relations","remove-current-relations-hint":"Removes current relations from the originator of the incoming message based on direction and type.","change-originator-to-related-entity":"Change originator to related entity","change-originator-to-related-entity-hint":"Used to process submitted message as a message from another entity.","start-interval-pattern":"Start interval pattern","end-interval-pattern":"End interval pattern","start-interval-pattern-required":"Start interval pattern is required","end-interval-pattern-required":"End interval pattern is required","start-interval-pattern-hint":"Start interval pattern, use ${metaKeyName} to substitute variables from metadata","end-interval-pattern-hint":"End interval pattern, use ${metaKeyName} to substitute variables from metadata","smtp-protocol":"Protocol","smtp-host":"SMTP host","smtp-host-required":"SMTP host is required.","smtp-port":"SMTP port","smtp-port-required":"You must supply a smtp port.","smtp-port-range":"SMTP port should be in a range from 1 to 65535.","timeout-msec":"Timeout ms","min-timeout-msec-message":"Only 0 ms minimum value is allowed.","enter-username":"Enter username","enter-password":"Enter password","enable-tls":"Enable TLS","enable-proxy":"Enable proxy","use-system-proxy-properties":"Use system proxy properties","proxy-host":"Proxy host","proxy-host-required":"Proxy host is required.","proxy-port":"Proxy port","proxy-port-required":"You must supply a proxy port.","proxy-port-range":"Proxy port should be in a range from 1 to 65535.","proxy-user":"Proxy user","proxy-password":"Proxy password","proxy-scheme":"Proxy scheme","tls-version":"TLS version","min-period-0-seconds-message":"Only 0 second minimum period is allowed.","max-pending-messages":"Maximum pending messages","max-pending-messages-required":"Maximum pending messages is required.","max-pending-messages-range":"Maximum pending messages should be in a range from 1 to 100000.","originator-types-filter":"Originator types filter","interval-seconds":"Interval in seconds","interval-seconds-required":"Interval is required.","min-interval-seconds-message":"Only 1 second minimum interval is allowed.","output-timeseries-key-prefix":"Output timeseries key prefix","output-timeseries-key-prefix-required":"Output timeseries key prefix required.","separator-hint":'You should press "enter" to complete field input.',"entity-details":"Select entity details:","entity-details-title":"Title","entity-details-country":"Country","entity-details-state":"State","entity-details-zip":"Zip","entity-details-address":"Address","entity-details-address2":"Address2","entity-details-additional_info":"Additional Info","entity-details-phone":"Phone","entity-details-email":"Email","add-to-metadata":"Add selected details to message metadata","add-to-metadata-hint":"If selected, adds the selected details keys to the message metadata instead of message data.","entity-details-list-empty":"No entity details selected.","no-entity-details-matching":"No entity details matching were found.","custom-table-name":"Custom table name","custom-table-name-required":"Table Name is required","custom-table-hint":"You should enter the table name without prefix 'cs_tb_'.","message-field":"Message field","message-field-required":"Message field is required.","table-col":"Table column","table-col-required":"Table column is required.","latitude-key-name":"Latitude key name","longitude-key-name":"Longitude key name","latitude-key-name-required":"Latitude key name is required.","longitude-key-name-required":"Longitude key name is required.","fetch-perimeter-info-from-message-metadata":"Fetch perimeter information from message metadata","perimeter-circle":"Circle","perimeter-polygon":"Polygon","perimeter-type":"Perimeter type","circle-center-latitude":"Center latitude","circle-center-latitude-required":"Center latitude is required.","circle-center-longitude":"Center longitude","circle-center-longitude-required":"Center longitude is required.","range-unit-meter":"Meter","range-unit-kilometer":"Kilometer","range-unit-foot":"Foot","range-unit-mile":"Mile","range-unit-nautical-mile":"Nautical mile","range-units":"Range units",range:"Range","range-required":"Range is required.","polygon-definition":"Polygon definition","polygon-definition-required":"Polygon definition is required.","polygon-definition-hint":"Please, use the following format for manual definition of polygon: [[lat1,lon1],[lat2,lon2], ... ,[latN,lonN]].","min-inside-duration":"Minimal inside duration","min-inside-duration-value-required":"Minimal inside duration is required","min-inside-duration-time-unit":"Minimal inside duration time unit","min-outside-duration":"Minimal outside duration","min-outside-duration-value-required":"Minimal outside duration is required","min-outside-duration-time-unit":"Minimal outside duration time unit","tell-failure-if-absent":"Tell Failure","tell-failure-if-absent-hint":'If at least one selected key doesn\'t exist the outbound message will report "Failure".',"get-latest-value-with-ts":"Fetch Latest telemetry with Timestamp","get-latest-value-with-ts-hint":'If selected, latest telemetry values will be added to the outbound message metadata with timestamp, e.g: "temp": "{\\"ts\\":1574329385897,\\"value\\":42}"',"use-redis-queue":"Use redis queue for message persistence","trim-redis-queue":"Trim redis queue","redis-queue-max-size":"Redis queue max size","add-metadata-key-values-as-kafka-headers":"Add Message metadata key-value pairs to Kafka record headers","add-metadata-key-values-as-kafka-headers-hint":"If selected, key-value pairs from message metadata will be added to the outgoing records headers as byte arrays with predefined charset encoding.","charset-encoding":"Charset encoding","charset-encoding-required":"Charset encoding is required.","charset-us-ascii":"US-ASCII","charset-iso-8859-1":"ISO-8859-1","charset-utf-8":"UTF-8","charset-utf-16be":"UTF-16BE","charset-utf-16le":"UTF-16LE","charset-utf-16":"UTF-16","select-queue-hint":"The queue name can be selected from a drop-down list or add a custom name."},"key-val":{key:"Key",value:"Value","remove-entry":"Remove entry","add-entry":"Add entry"}}};e.translations("en_US",t)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function i(e){(0,o.default)(e)}i.$inject=["$translateProvider"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var r=n(108),o=a(r)},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=angular.module("thingsboard.ruleChain.config.types",[]).constant("ruleNodeTypes",{originatorSource:{CUSTOMER:{name:"tb.rulenode.originator-customer",value:"CUSTOMER"},TENANT:{name:"tb.rulenode.originator-tenant",value:"TENANT"},RELATED:{name:"tb.rulenode.originator-related",value:"RELATED"},ALARM_ORIGINATOR:{name:"tb.rulenode.originator-alarm-originator",value:"ALARM_ORIGINATOR"}},fetchModeType:["FIRST","LAST","ALL"],samplingOrder:["ASC","DESC"],httpRequestType:["GET","POST","PUT","DELETE"],entityDetails:{TITLE:{name:"tb.rulenode.entity-details-title",value:"TITLE"},COUNTRY:{name:"tb.rulenode.entity-details-country",value:"COUNTRY"},STATE:{name:"tb.rulenode.entity-details-state",value:"STATE"},ZIP:{name:"tb.rulenode.entity-details-zip",value:"ZIP"},ADDRESS:{name:"tb.rulenode.entity-details-address",value:"ADDRESS"},ADDRESS2:{name:"tb.rulenode.entity-details-address2",value:"ADDRESS2"},PHONE:{name:"tb.rulenode.entity-details-phone",value:"PHONE"},EMAIL:{name:"tb.rulenode.entity-details-email",value:"EMAIL"},ADDITIONAL_INFO:{name:"tb.rulenode.entity-details-additional_info",value:"ADDITIONAL_INFO"}},sqsQueueType:{STANDARD:{name:"tb.rulenode.sqs-queue-standard",value:"STANDARD"},FIFO:{name:"tb.rulenode.sqs-queue-fifo",value:"FIFO"}},perimeterType:{CIRCLE:{name:"tb.rulenode.perimeter-circle",value:"CIRCLE"},POLYGON:{name:"tb.rulenode.perimeter-polygon",value:"POLYGON"}},timeUnit:{MILLISECONDS:{value:"MILLISECONDS",name:"tb.rulenode.time-unit-milliseconds"},SECONDS:{value:"SECONDS",name:"tb.rulenode.time-unit-seconds"},MINUTES:{value:"MINUTES",name:"tb.rulenode.time-unit-minutes"},HOURS:{value:"HOURS",name:"tb.rulenode.time-unit-hours"},DAYS:{value:"DAYS",name:"tb.rulenode.time-unit-days"}},rangeUnit:{METER:{value:"METER",name:"tb.rulenode.range-unit-meter"},KILOMETER:{value:"KILOMETER",name:"tb.rulenode.range-unit-kilometer"},FOOT:{value:"FOOT",name:"tb.rulenode.range-unit-foot"},MILE:{value:"MILE",name:"tb.rulenode.range-unit-mile"},NAUTICAL_MILE:{value:"NAUTICAL_MILE",name:"tb.rulenode.range-unit-nautical-mile"}},mqttCredentialTypes:{anonymous:{value:"anonymous",name:"tb.rulenode.credentials-anonymous"},basic:{value:"basic",name:"tb.rulenode.credentials-basic"},"cert.PEM":{value:"cert.PEM",name:"tb.rulenode.credentials-pem"}},azureIotHubCredentialTypes:{sas:{value:"sas",name:"tb.rulenode.credentials-sas"},"cert.PEM":{value:"cert.PEM",name:"tb.rulenode.credentials-pem"}},toBytesStandartCharsetTypes:{"US-ASCII":{value:"US-ASCII",name:"tb.rulenode.charset-us-ascii"},"ISO-8859-1":{value:"ISO-8859-1",name:"tb.rulenode.charset-iso-8859-1"},"UTF-8":{value:"UTF-8",name:"tb.rulenode.charset-utf-8"},"UTF-16BE":{value:"UTF-16BE",name:"tb.rulenode.charset-utf-16be"},"UTF-16LE":{value:"UTF-16LE",name:"tb.rulenode.charset-utf-16le"},"UTF-16":{value:"UTF-16",name:"tb.rulenode.charset-utf-16"}}}).name}])); -//# sourceMappingURL=rulenode-core-config.js.map \ No newline at end of file +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("@angular/core"),require("@angular/common"),require("@ngx-translate/core"),require("@shared/public-api"),require("@ngrx/store"),require("@angular/forms"),require("@core/public-api"),require("@angular/cdk/keycodes"),require("@home/components/public-api"),require("@angular/cdk/coercion"),require("@angular/material/chips"),require("@angular/material/autocomplete"),require("rxjs"),require("rxjs/operators")):"function"==typeof define&&define.amd?define("rulenode-core-config",["exports","@angular/core","@angular/common","@ngx-translate/core","@shared/public-api","@ngrx/store","@angular/forms","@core/public-api","@angular/cdk/keycodes","@home/components/public-api","@angular/cdk/coercion","@angular/material/chips","@angular/material/autocomplete","rxjs","rxjs/operators"],t):t((e=e||self)["rulenode-core-config"]={},e.ng.core,e.ng.common,e["ngx-translate"],e.shared,e["ngrx-store"],e.ng.forms,e.core,e.ng.cdk.keycodes,e.publicApi$2,e.ng.cdk.coercion,e.ng.material.chips,e.ng.material.autocomplete,e.rxjs,e.rxjs.operators)}(this,(function(e,t,r,n,a,o,i,l,s,m,u,d,p,c,f){"use strict"; +/*! ***************************************************************************** + Copyright (c) Microsoft Corporation. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. + ***************************************************************************** */var g=function(e,t){return(g=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(e[r]=t[r])})(e,t)};function y(e,t){function r(){this.constructor=e}g(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)}function b(e,t,r,n){var a,o=arguments.length,i=o<3?t:null===n?n=Object.getOwnPropertyDescriptor(t,r):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)i=Reflect.decorate(e,t,r,n);else for(var l=e.length-1;l>=0;l--)(a=e[l])&&(i=(o<3?a(i):o>3?a(t,r,i):a(t,r))||i);return o>3&&i&&Object.defineProperty(t,r,i),i}function h(e,t){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(e,t)}Object.create;function C(e){var t="function"==typeof Symbol&&Symbol.iterator,r=t&&e[t],n=0;if(r)return r.call(e);if(e&&"number"==typeof e.length)return{next:function(){return e&&n>=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}Object.create;var v,F=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.emptyConfigForm},r.prototype.onConfigurationSet=function(e){this.emptyConfigForm=this.fb.group({})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-node-empty-config",template:"
"}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),x=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.attributeScopes=Object.keys(a.AttributeScope),n.telemetryTypeTranslationsMap=a.telemetryTypeTranslations,n}return y(r,e),r.prototype.configForm=function(){return this.attributesConfigForm},r.prototype.onConfigurationSet=function(e){this.attributesConfigForm=this.fb.group({scope:[e?e.scope:null,[i.Validators.required]],notifyDevice:[!e||e.scope,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-attributes-config",template:'
\n \n attribute.attributes-scope\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n
\n \n {{ \'tb.rulenode.notify-device\' | translate }}\n \n
tb.rulenode.notify-device-hint
\n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),T=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.timeseriesConfigForm},r.prototype.onConfigurationSet=function(e){this.timeseriesConfigForm=this.fb.group({defaultTTL:[e?e.defaultTTL:null,[i.Validators.required,i.Validators.min(0)]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-timeseries-config",template:'
\n \n tb.rulenode.default-ttl\n \n \n {{ \'tb.rulenode.default-ttl-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-default-ttl-message\' | translate }}\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),q=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.rpcRequestConfigForm},r.prototype.onConfigurationSet=function(e){this.rpcRequestConfigForm=this.fb.group({timeoutInSeconds:[e?e.timeoutInSeconds:null,[i.Validators.required,i.Validators.min(0)]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-rpc-request-config",template:'
\n \n tb.rulenode.timeout-sec\n \n \n {{ \'tb.rulenode.timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-timeout-message\' | translate }}\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),S=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return y(r,e),r.prototype.configForm=function(){return this.logConfigForm},r.prototype.onConfigurationSet=function(e){this.logConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.logConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"string",this.translate.instant("tb.rulenode.to-string"),"ToString",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.logConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:l.NodeScriptTestService},{type:n.TranslateService}]},b([t.ViewChild("jsFuncComponent",{static:!0}),h("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=b([t.Component({selector:"tb-action-node-log-config",template:'
\n \n \n \n
\n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder,l.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),I=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.assignCustomerConfigForm},r.prototype.onConfigurationSet=function(e){this.assignCustomerConfigForm=this.fb.group({customerNamePattern:[e?e.customerNamePattern:null,[i.Validators.required]],createCustomerIfNotExists:[!!e&&e.createCustomerIfNotExists,[]],customerCacheExpiration:[e?e.customerCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-assign-to-customer-config",template:'
\n \n tb.rulenode.customer-name-pattern\n \n \n {{ \'tb.rulenode.customer-name-pattern-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.create-customer-if-not-exists\' | translate }}\n \n \n tb.rulenode.customer-cache-expiration\n \n \n {{ \'tb.rulenode.customer-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.customer-cache-expiration-range\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),k=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return y(r,e),r.prototype.configForm=function(){return this.clearAlarmConfigForm},r.prototype.onConfigurationSet=function(e){this.clearAlarmConfigForm=this.fb.group({alarmDetailsBuildJs:[e?e.alarmDetailsBuildJs:null,[i.Validators.required]],alarmType:[e?e.alarmType:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.clearAlarmConfigForm.get("alarmDetailsBuildJs").value;this.nodeScriptTestService.testNodeScript(t,"json",this.translate.instant("tb.rulenode.details"),"Details",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.clearAlarmConfigForm.get("alarmDetailsBuildJs").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:l.NodeScriptTestService},{type:n.TranslateService}]},b([t.ViewChild("jsFuncComponent",{static:!0}),h("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=b([t.Component({selector:"tb-action-node-clear-alarm-config",template:'
\n \n \n \n
\n \n
\n \n tb.rulenode.alarm-type\n \n \n {{ \'tb.rulenode.alarm-type-required\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder,l.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),N=function(e){function r(t,r,n,o){var i=e.call(this,t)||this;return i.store=t,i.fb=r,i.nodeScriptTestService=n,i.translate=o,i.alarmSeverities=Object.keys(a.AlarmSeverity),i.alarmSeverityTranslationMap=a.alarmSeverityTranslations,i.separatorKeysCodes=[s.ENTER,s.COMMA,s.SEMICOLON],i}return y(r,e),r.prototype.configForm=function(){return this.createAlarmConfigForm},r.prototype.onConfigurationSet=function(e){this.createAlarmConfigForm=this.fb.group({alarmDetailsBuildJs:[e?e.alarmDetailsBuildJs:null,[i.Validators.required]],useMessageAlarmData:[!!e&&e.useMessageAlarmData,[]],alarmType:[e?e.alarmType:null,[]],severity:[e?e.severity:null,[]],propagate:[!!e&&e.propagate,[]],relationTypes:[e?e.relationTypes:null,[]]})},r.prototype.validatorTriggers=function(){return["useMessageAlarmData"]},r.prototype.updateValidators=function(e){this.createAlarmConfigForm.get("useMessageAlarmData").value?(this.createAlarmConfigForm.get("alarmType").setValidators([]),this.createAlarmConfigForm.get("severity").setValidators([])):(this.createAlarmConfigForm.get("alarmType").setValidators([i.Validators.required]),this.createAlarmConfigForm.get("severity").setValidators([i.Validators.required])),this.createAlarmConfigForm.get("alarmType").updateValueAndValidity({emitEvent:e}),this.createAlarmConfigForm.get("severity").updateValueAndValidity({emitEvent:e})},r.prototype.testScript=function(){var e=this,t=this.createAlarmConfigForm.get("alarmDetailsBuildJs").value;this.nodeScriptTestService.testNodeScript(t,"json",this.translate.instant("tb.rulenode.details"),"Details",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.createAlarmConfigForm.get("alarmDetailsBuildJs").setValue(t)}))},r.prototype.removeKey=function(e,t){var r=this.createAlarmConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.createAlarmConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.createAlarmConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.createAlarmConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:l.NodeScriptTestService},{type:n.TranslateService}]},b([t.ViewChild("jsFuncComponent",{static:!0}),h("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=b([t.Component({selector:"tb-action-node-create-alarm-config",template:'
\n \n \n \n
\n \n
\n \n {{ \'tb.rulenode.use-message-alarm-data\' | translate }}\n \n
\n
\n \n tb.rulenode.alarm-type\n \n \n {{ \'tb.rulenode.alarm-type-required\' | translate }}\n \n \n \n \n tb.rulenode.alarm-severity\n \n \n {{ alarmSeverityTranslationMap.get(severity) | translate }}\n \n \n \n {{ \'tb.rulenode.alarm-severity-required\' | translate }}\n \n \n
\n \n {{ \'tb.rulenode.propagate\' | translate }}\n \n
\n \n tb.rulenode.relation-types-list\n \n \n {{key}}\n close\n \n \n \n \n \n
\n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder,l.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),V=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.entityType=a.EntityType,n}return y(r,e),r.prototype.configForm=function(){return this.createRelationConfigForm},r.prototype.onConfigurationSet=function(e){this.createRelationConfigForm=this.fb.group({direction:[e?e.direction:null,[i.Validators.required]],entityType:[e?e.entityType:null,[i.Validators.required]],entityNamePattern:[e?e.entityNamePattern:null,[]],entityTypePattern:[e?e.entityTypePattern:null,[]],relationType:[e?e.relationType:null,[i.Validators.required]],createEntityIfNotExists:[!!e&&e.createEntityIfNotExists,[]],removeCurrentRelations:[!!e&&e.removeCurrentRelations,[]],changeOriginatorToRelatedEntity:[!!e&&e.changeOriginatorToRelatedEntity,[]],entityCacheExpiration:[e?e.entityCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.prototype.validatorTriggers=function(){return["entityType"]},r.prototype.updateValidators=function(e){var t=this.createRelationConfigForm.get("entityType").value;t?this.createRelationConfigForm.get("entityNamePattern").setValidators([i.Validators.required]):this.createRelationConfigForm.get("entityNamePattern").setValidators([]),!t||t!==a.EntityType.DEVICE&&t!==a.EntityType.ASSET?this.createRelationConfigForm.get("entityTypePattern").setValidators([]):this.createRelationConfigForm.get("entityTypePattern").setValidators([i.Validators.required]),this.createRelationConfigForm.get("entityNamePattern").updateValueAndValidity({emitEvent:e}),this.createRelationConfigForm.get("entityTypePattern").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-create-relation-config",template:'
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.entity-name-pattern\n \n \n {{ \'tb.rulenode.entity-name-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.entity-type-pattern\n \n \n {{ \'tb.rulenode.entity-type-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.relation-type-pattern\n \n \n {{ \'tb.rulenode.relation-type-pattern-required\' | translate }}\n \n \n \n
\n \n {{ \'tb.rulenode.create-entity-if-not-exists\' | translate }}\n \n
tb.rulenode.create-entity-if-not-exists-hint
\n
\n \n {{ \'tb.rulenode.remove-current-relations\' | translate }}\n \n
tb.rulenode.remove-current-relations-hint
\n \n {{ \'tb.rulenode.change-originator-to-related-entity\' | translate }}\n \n
tb.rulenode.change-originator-to-related-entity-hint
\n \n tb.rulenode.entity-cache-expiration\n \n \n {{ \'tb.rulenode.entity-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.entity-cache-expiration-range\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),E=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.msgDelayConfigForm},r.prototype.onConfigurationSet=function(e){this.msgDelayConfigForm=this.fb.group({useMetadataPeriodInSecondsPatterns:[!!e&&e.useMetadataPeriodInSecondsPatterns,[]],periodInSeconds:[e?e.periodInSeconds:null,[]],periodInSecondsPattern:[e?e.periodInSecondsPattern:null,[]],maxPendingMsgs:[e?e.maxPendingMsgs:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(1e5)]]})},r.prototype.validatorTriggers=function(){return["useMetadataPeriodInSecondsPatterns"]},r.prototype.updateValidators=function(e){this.msgDelayConfigForm.get("useMetadataPeriodInSecondsPatterns").value?(this.msgDelayConfigForm.get("periodInSecondsPattern").setValidators([i.Validators.required]),this.msgDelayConfigForm.get("periodInSeconds").setValidators([])):(this.msgDelayConfigForm.get("periodInSecondsPattern").setValidators([]),this.msgDelayConfigForm.get("periodInSeconds").setValidators([i.Validators.required,i.Validators.min(0)])),this.msgDelayConfigForm.get("periodInSecondsPattern").updateValueAndValidity({emitEvent:e}),this.msgDelayConfigForm.get("periodInSeconds").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-msg-delay-config",template:'
\n \n {{ \'tb.rulenode.use-metadata-period-in-seconds-patterns\' | translate }}\n \n
tb.rulenode.use-metadata-period-in-seconds-patterns-hint
\n \n tb.rulenode.period-seconds\n \n \n {{ \'tb.rulenode.period-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-period-0-seconds-message\' | translate }}\n \n \n \n \n tb.rulenode.period-in-seconds-pattern\n \n \n {{ \'tb.rulenode.period-in-seconds-pattern-required\' | translate }}\n \n \n \n \n \n tb.rulenode.max-pending-messages\n \n \n {{ \'tb.rulenode.max-pending-messages-required\' | translate }}\n \n \n {{ \'tb.rulenode.max-pending-messages-range\' | translate }}\n \n \n {{ \'tb.rulenode.max-pending-messages-range\' | translate }}\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),A=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.entityType=a.EntityType,n}return y(r,e),r.prototype.configForm=function(){return this.deleteRelationConfigForm},r.prototype.onConfigurationSet=function(e){this.deleteRelationConfigForm=this.fb.group({deleteForSingleEntity:[!!e&&e.deleteForSingleEntity,[]],direction:[e?e.direction:null,[i.Validators.required]],entityType:[e?e.entityType:null,[]],entityNamePattern:[e?e.entityNamePattern:null,[]],relationType:[e?e.relationType:null,[i.Validators.required]],entityCacheExpiration:[e?e.entityCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.prototype.validatorTriggers=function(){return["deleteForSingleEntity","entityType"]},r.prototype.updateValidators=function(e){var t=this.deleteRelationConfigForm.get("deleteForSingleEntity").value,r=this.deleteRelationConfigForm.get("entityType").value;t?this.deleteRelationConfigForm.get("entityType").setValidators([i.Validators.required]):this.deleteRelationConfigForm.get("entityType").setValidators([]),t&&r?this.deleteRelationConfigForm.get("entityNamePattern").setValidators([i.Validators.required]):this.deleteRelationConfigForm.get("entityNamePattern").setValidators([]),this.deleteRelationConfigForm.get("entityType").updateValueAndValidity({emitEvent:!1}),this.deleteRelationConfigForm.get("entityNamePattern").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-delete-relation-config",template:'
\n \n {{ \'tb.rulenode.delete-relation-to-specific-entity\' | translate }}\n \n
tb.rulenode.delete-relation-hint
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.entity-name-pattern\n \n \n {{ \'tb.rulenode.entity-name-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.relation-type-pattern\n \n \n {{ \'tb.rulenode.relation-type-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.entity-cache-expiration\n \n \n {{ \'tb.rulenode.entity-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.entity-cache-expiration-range\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),L=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return y(r,e),r.prototype.configForm=function(){return this.generatorConfigForm},r.prototype.onConfigurationSet=function(e){this.generatorConfigForm=this.fb.group({msgCount:[e?e.msgCount:null,[i.Validators.required,i.Validators.min(0)]],periodInSeconds:[e?e.periodInSeconds:null,[i.Validators.required,i.Validators.min(1)]],originator:[e?e.originator:null,[]],jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.prepareInputConfig=function(e){return e&&(e.originatorId&&e.originatorType?e.originator={id:e.originatorId,entityType:e.originatorType}:e.originator=null,delete e.originatorId,delete e.originatorType),e},r.prototype.prepareOutputConfig=function(e){return e.originator?(e.originatorId=e.originator.id,e.originatorType=e.originator.entityType):(e.originatorId=null,e.originatorType=null),delete e.originator,e},r.prototype.testScript=function(){var e=this,t=this.generatorConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"generate",this.translate.instant("tb.rulenode.generator"),"Generate",["prevMsg","prevMetadata","prevMsgType"],this.ruleNodeId).subscribe((function(t){t&&e.generatorConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:l.NodeScriptTestService},{type:n.TranslateService}]},b([t.ViewChild("jsFuncComponent",{static:!0}),h("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=b([t.Component({selector:"tb-action-node-generator-config",template:'
\n \n tb.rulenode.message-count\n \n \n {{ \'tb.rulenode.message-count-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-message-count-message\' | translate }}\n \n \n \n tb.rulenode.period-seconds\n \n \n {{ \'tb.rulenode.period-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-period-seconds-message\' | translate }}\n \n \n
\n \n \n \n
\n \n \n \n
\n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder,l.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent);!function(e){e.CUSTOMER="CUSTOMER",e.TENANT="TENANT",e.RELATED="RELATED",e.ALARM_ORIGINATOR="ALARM_ORIGINATOR"}(v||(v={}));var M,P=new Map([[v.CUSTOMER,"tb.rulenode.originator-customer"],[v.TENANT,"tb.rulenode.originator-tenant"],[v.RELATED,"tb.rulenode.originator-related"],[v.ALARM_ORIGINATOR,"tb.rulenode.originator-alarm-originator"]]);!function(e){e.CIRCLE="CIRCLE",e.POLYGON="POLYGON"}(M||(M={}));var R,w=new Map([[M.CIRCLE,"tb.rulenode.perimeter-circle"],[M.POLYGON,"tb.rulenode.perimeter-polygon"]]);!function(e){e.MILLISECONDS="MILLISECONDS",e.SECONDS="SECONDS",e.MINUTES="MINUTES",e.HOURS="HOURS",e.DAYS="DAYS"}(R||(R={}));var O,D=new Map([[R.MILLISECONDS,"tb.rulenode.time-unit-milliseconds"],[R.SECONDS,"tb.rulenode.time-unit-seconds"],[R.MINUTES,"tb.rulenode.time-unit-minutes"],[R.HOURS,"tb.rulenode.time-unit-hours"],[R.DAYS,"tb.rulenode.time-unit-days"]]);!function(e){e.METER="METER",e.KILOMETER="KILOMETER",e.FOOT="FOOT",e.MILE="MILE",e.NAUTICAL_MILE="NAUTICAL_MILE"}(O||(O={}));var K,B=new Map([[O.METER,"tb.rulenode.range-unit-meter"],[O.KILOMETER,"tb.rulenode.range-unit-kilometer"],[O.FOOT,"tb.rulenode.range-unit-foot"],[O.MILE,"tb.rulenode.range-unit-mile"],[O.NAUTICAL_MILE,"tb.rulenode.range-unit-nautical-mile"]]);!function(e){e.TITLE="TITLE",e.COUNTRY="COUNTRY",e.STATE="STATE",e.ZIP="ZIP",e.ADDRESS="ADDRESS",e.ADDRESS2="ADDRESS2",e.PHONE="PHONE",e.EMAIL="EMAIL",e.ADDITIONAL_INFO="ADDITIONAL_INFO"}(K||(K={}));var U,j,H,G=new Map([[K.TITLE,"tb.rulenode.entity-details-title"],[K.COUNTRY,"tb.rulenode.entity-details-country"],[K.STATE,"tb.rulenode.entity-details-state"],[K.ZIP,"tb.rulenode.entity-details-zip"],[K.ADDRESS,"tb.rulenode.entity-details-address"],[K.ADDRESS2,"tb.rulenode.entity-details-address2"],[K.PHONE,"tb.rulenode.entity-details-phone"],[K.EMAIL,"tb.rulenode.entity-details-email"],[K.ADDITIONAL_INFO,"tb.rulenode.entity-details-additional_info"]]);!function(e){e.FIRST="FIRST",e.LAST="LAST",e.ALL="ALL"}(U||(U={})),function(e){e.ASC="ASC",e.DESC="DESC"}(j||(j={})),function(e){e.STANDARD="STANDARD",e.FIFO="FIFO"}(H||(H={}));var z,$=new Map([[H.STANDARD,"tb.rulenode.sqs-queue-standard"],[H.FIFO,"tb.rulenode.sqs-queue-fifo"]]),Q=["anonymous","basic","cert.PEM"],_=new Map([["anonymous","tb.rulenode.credentials-anonymous"],["basic","tb.rulenode.credentials-basic"],["cert.PEM","tb.rulenode.credentials-pem"]]),W=["sas","cert.PEM"],J=new Map([["sas","tb.rulenode.credentials-sas"],["cert.PEM","tb.rulenode.credentials-pem"]]);!function(e){e.GET="GET",e.POST="POST",e.PUT="PUT",e.DELETE="DELETE"}(z||(z={}));var Y=["US-ASCII","ISO-8859-1","UTF-8","UTF-16BE","UTF-16LE","UTF-16"],Z=new Map([["US-ASCII","tb.rulenode.charset-us-ascii"],["ISO-8859-1","tb.rulenode.charset-iso-8859-1"],["UTF-8","tb.rulenode.charset-utf-8"],["UTF-16BE","tb.rulenode.charset-utf-16be"],["UTF-16LE","tb.rulenode.charset-utf-16le"],["UTF-16","tb.rulenode.charset-utf-16"]]),X=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.perimeterType=M,n.perimeterTypes=Object.keys(M),n.perimeterTypeTranslationMap=w,n.rangeUnits=Object.keys(O),n.rangeUnitTranslationMap=B,n.timeUnits=Object.keys(R),n.timeUnitsTranslationMap=D,n}return y(r,e),r.prototype.configForm=function(){return this.geoActionConfigForm},r.prototype.onConfigurationSet=function(e){this.geoActionConfigForm=this.fb.group({latitudeKeyName:[e?e.latitudeKeyName:null,[i.Validators.required]],longitudeKeyName:[e?e.longitudeKeyName:null,[i.Validators.required]],fetchPerimeterInfoFromMessageMetadata:[!!e&&e.fetchPerimeterInfoFromMessageMetadata,[]],perimeterType:[e?e.perimeterType:null,[]],centerLatitude:[e?e.centerLatitude:null,[]],centerLongitude:[e?e.centerLatitude:null,[]],range:[e?e.range:null,[]],rangeUnit:[e?e.rangeUnit:null,[]],polygonsDefinition:[e?e.polygonsDefinition:null,[]],minInsideDuration:[e?e.minInsideDuration:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]],minInsideDurationTimeUnit:[e?e.minInsideDurationTimeUnit:null,[i.Validators.required]],minOutsideDuration:[e?e.minOutsideDuration:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]],minOutsideDurationTimeUnit:[e?e.minOutsideDurationTimeUnit:null,[i.Validators.required]]})},r.prototype.validatorTriggers=function(){return["fetchPerimeterInfoFromMessageMetadata","perimeterType"]},r.prototype.updateValidators=function(e){var t=this.geoActionConfigForm.get("fetchPerimeterInfoFromMessageMetadata").value,r=this.geoActionConfigForm.get("perimeterType").value;t?this.geoActionConfigForm.get("perimeterType").setValidators([]):this.geoActionConfigForm.get("perimeterType").setValidators([i.Validators.required]),t||r!==M.CIRCLE?(this.geoActionConfigForm.get("centerLatitude").setValidators([]),this.geoActionConfigForm.get("centerLongitude").setValidators([]),this.geoActionConfigForm.get("range").setValidators([]),this.geoActionConfigForm.get("rangeUnit").setValidators([])):(this.geoActionConfigForm.get("centerLatitude").setValidators([i.Validators.required,i.Validators.min(-90),i.Validators.max(90)]),this.geoActionConfigForm.get("centerLongitude").setValidators([i.Validators.required,i.Validators.min(-180),i.Validators.max(180)]),this.geoActionConfigForm.get("range").setValidators([i.Validators.required,i.Validators.min(0)]),this.geoActionConfigForm.get("rangeUnit").setValidators([i.Validators.required])),t||r!==M.POLYGON?this.geoActionConfigForm.get("polygonsDefinition").setValidators([]):this.geoActionConfigForm.get("polygonsDefinition").setValidators([i.Validators.required]),this.geoActionConfigForm.get("perimeterType").updateValueAndValidity({emitEvent:!1}),this.geoActionConfigForm.get("centerLatitude").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("centerLongitude").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("range").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("rangeUnit").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("polygonsDefinition").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-gps-geofencing-config",template:'
\n \n tb.rulenode.latitude-key-name\n \n \n {{ \'tb.rulenode.latitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.longitude-key-name\n \n \n {{ \'tb.rulenode.longitude-key-name-required\' | translate }}\n \n \n \n {{ \'tb.rulenode.fetch-perimeter-info-from-message-metadata\' | translate }}\n \n
\n \n tb.rulenode.perimeter-type\n \n \n {{ perimeterTypeTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n \n tb.rulenode.circle-center-latitude\n \n \n {{ \'tb.rulenode.circle-center-latitude-required\' | translate }}\n \n \n \n tb.rulenode.circle-center-longitude\n \n \n {{ \'tb.rulenode.circle-center-longitude-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.range\n \n \n {{ \'tb.rulenode.range-required\' | translate }}\n \n \n \n tb.rulenode.range-units\n \n \n {{ rangeUnitTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n \n tb.rulenode.polygon-definition\n \n \n {{ \'tb.rulenode.polygon-definition-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.min-inside-duration\n \n \n {{ \'tb.rulenode.min-inside-duration-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.min-inside-duration-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n tb.rulenode.min-outside-duration\n \n \n {{ \'tb.rulenode.min-outside-duration-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.min-outside-duration-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ee=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.msgCountConfigForm},r.prototype.onConfigurationSet=function(e){this.msgCountConfigForm=this.fb.group({interval:[e?e.interval:null,[i.Validators.required,i.Validators.min(1)]],telemetryPrefix:[e?e.telemetryPrefix:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-msg-count-config",template:'
\n \n tb.rulenode.interval-seconds\n \n \n {{ \'tb.rulenode.interval-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-interval-seconds-message\' | translate }}\n \n \n \n tb.rulenode.output-timeseries-key-prefix\n \n \n {{ \'tb.rulenode.output-timeseries-key-prefix-required\' | translate }}\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),te=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.rpcReplyConfigForm},r.prototype.onConfigurationSet=function(e){this.rpcReplyConfigForm=this.fb.group({requestIdMetaDataAttribute:[e?e.requestIdMetaDataAttribute:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-rpc-reply-config",template:'
\n \n tb.rulenode.request-id-metadata-attribute\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),re=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.saveToCustomTableConfigForm},r.prototype.onConfigurationSet=function(e){this.saveToCustomTableConfigForm=this.fb.group({tableName:[e?e.tableName:null,[i.Validators.required]],fieldsMapping:[e?e.fieldsMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-custom-table-config",template:'
\n \n tb.rulenode.custom-table-name\n \n \n {{ \'tb.rulenode.custom-table-name-required\' | translate }}\n \n \n \n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ne=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.translate=r,o.injector=n,o.fb=a,o.propagateChange=null,o.valueChangeSubscription=null,o}var a;return y(r,e),a=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){this.ngControl=this.injector.get(i.NgControl),null!=this.ngControl&&(this.ngControl.valueAccessor=this),this.kvListFormGroup=this.fb.group({}),this.kvListFormGroup.addControl("keyVals",this.fb.array([]))},r.prototype.keyValsFormArray=function(){return this.kvListFormGroup.get("keyVals")},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.kvListFormGroup.disable({emitEvent:!1}):this.kvListFormGroup.enable({emitEvent:!1})},r.prototype.writeValue=function(e){var t,r,n=this;this.valueChangeSubscription&&this.valueChangeSubscription.unsubscribe();var a=[];if(e)try{for(var o=C(Object.keys(e)),l=o.next();!l.done;l=o.next()){var s=l.value;Object.prototype.hasOwnProperty.call(e,s)&&a.push(this.fb.group({key:[s,[i.Validators.required]],value:[e[s],[i.Validators.required]]}))}}catch(e){t={error:e}}finally{try{l&&!l.done&&(r=o.return)&&r.call(o)}finally{if(t)throw t.error}}this.kvListFormGroup.setControl("keyVals",this.fb.array(a)),this.valueChangeSubscription=this.kvListFormGroup.valueChanges.subscribe((function(){n.updateModel()}))},r.prototype.removeKeyVal=function(e){this.kvListFormGroup.get("keyVals").removeAt(e)},r.prototype.addKeyVal=function(){this.kvListFormGroup.get("keyVals").push(this.fb.group({key:["",[i.Validators.required]],value:["",[i.Validators.required]]}))},r.prototype.validate=function(e){return!this.kvListFormGroup.get("keyVals").value.length&&this.required?{kvMapRequired:!0}:this.kvListFormGroup.valid?null:{kvFieldsRequired:!0}},r.prototype.updateModel=function(){var e=this.kvListFormGroup.get("keyVals").value;if(this.required&&!e.length||!this.kvListFormGroup.valid)this.propagateChange(null);else{var t={};e.forEach((function(e){t[e.key]=e.value})),this.propagateChange(t)}},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:t.Injector},{type:i.FormBuilder}]},b([t.Input(),h("design:type",Boolean)],r.prototype,"disabled",void 0),b([t.Input(),h("design:type",String)],r.prototype,"requiredText",void 0),b([t.Input(),h("design:type",String)],r.prototype,"keyText",void 0),b([t.Input(),h("design:type",String)],r.prototype,"keyRequiredText",void 0),b([t.Input(),h("design:type",String)],r.prototype,"valText",void 0),b([t.Input(),h("design:type",String)],r.prototype,"valRequiredText",void 0),b([t.Input(),h("design:type",Boolean),h("design:paramtypes",[Boolean])],r.prototype,"required",null),r=a=b([t.Component({selector:"tb-kv-map-config",template:'
\n
\n {{ keyText }}\n {{ valText }}\n \n
\n
\n
\n \n \n \n \n {{ keyRequiredText | translate }}\n \n \n \n \n \n \n {{ valRequiredText | translate }}\n \n \n \n
\n
\n \n
\n \n
\n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return a})),multi:!0},{provide:i.NG_VALIDATORS,useExisting:t.forwardRef((function(){return a})),multi:!0}],styles:[":host .tb-kv-map-config{margin-bottom:16px}:host .tb-kv-map-config .header{padding-left:5px;padding-right:5px;padding-bottom:5px}:host .tb-kv-map-config .header .cell{padding-left:5px;padding-right:5px;color:rgba(0,0,0,.54);font-size:12px;font-weight:700;white-space:nowrap}:host .tb-kv-map-config .body{padding-left:5px;padding-right:5px;padding-bottom:20px;max-height:300px;overflow:auto}:host .tb-kv-map-config .body .row{padding-top:5px;max-height:40px}:host .tb-kv-map-config .body .cell{padding-left:5px;padding-right:5px}:host ::ng-deep .tb-kv-map-config .body mat-form-field.cell{margin:0;max-height:40px}:host ::ng-deep .tb-kv-map-config .body mat-form-field.cell .mat-form-field-infix{border-top:0}:host ::ng-deep .tb-kv-map-config .body button.mat-button{margin:0}"]}),h("design:paramtypes",[o.Store,n.TranslateService,t.Injector,i.FormBuilder])],r)}(a.PageComponent),ae=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.entityType=a.EntityType,n.propagateChange=null,n}var n;return y(r,e),n=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){var e=this;this.deviceRelationsQueryFormGroup=this.fb.group({fetchLastLevelOnly:[!1,[]],direction:[null,[i.Validators.required]],maxLevel:[null,[]],relationType:[null],deviceTypes:[null,[i.Validators.required]]}),this.deviceRelationsQueryFormGroup.valueChanges.subscribe((function(t){e.deviceRelationsQueryFormGroup.valid?e.propagateChange(t):e.propagateChange(null)}))},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.deviceRelationsQueryFormGroup.disable({emitEvent:!1}):this.deviceRelationsQueryFormGroup.enable({emitEvent:!1})},r.prototype.writeValue=function(e){this.deviceRelationsQueryFormGroup.reset(e,{emitEvent:!1})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},b([t.Input(),h("design:type",Boolean)],r.prototype,"disabled",void 0),b([t.Input(),h("design:type",Boolean),h("design:paramtypes",[Boolean])],r.prototype,"required",null),r=n=b([t.Component({selector:"tb-device-relations-query-config",template:'
\n \n {{ \'alias.last-level-relation\' | translate }}\n \n
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n \n tb.rulenode.max-relation-level\n \n \n
\n
relation.relation-type
\n \n \n
device.device-types
\n \n \n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return n})),multi:!0}]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.PageComponent),oe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.propagateChange=null,n}var n;return y(r,e),n=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){var e=this;this.relationsQueryFormGroup=this.fb.group({fetchLastLevelOnly:[!1,[]],direction:[null,[i.Validators.required]],maxLevel:[null,[]],filters:[null]}),this.relationsQueryFormGroup.valueChanges.subscribe((function(t){e.relationsQueryFormGroup.valid?e.propagateChange(t):e.propagateChange(null)}))},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.relationsQueryFormGroup.disable({emitEvent:!1}):this.relationsQueryFormGroup.enable({emitEvent:!1})},r.prototype.writeValue=function(e){this.relationsQueryFormGroup.reset(e||{},{emitEvent:!1})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},b([t.Input(),h("design:type",Boolean)],r.prototype,"disabled",void 0),b([t.Input(),h("design:type",Boolean),h("design:paramtypes",[Boolean])],r.prototype,"required",null),r=n=b([t.Component({selector:"tb-relations-query-config",template:'
\n \n {{ \'alias.last-level-relation\' | translate }}\n \n
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n \n tb.rulenode.max-relation-level\n \n \n
\n
relation.relation-filters
\n \n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return n})),multi:!0}]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.PageComponent),ie=function(e){function r(t,r,n,o){var i,l,m=e.call(this,t)||this;m.store=t,m.translate=r,m.truncate=n,m.fb=o,m.placeholder="tb.rulenode.message-type",m.separatorKeysCodes=[s.ENTER,s.COMMA,s.SEMICOLON],m.messageTypes=[],m.messageTypesList=[],m.searchText="",m.propagateChange=function(e){},m.messageTypeConfigForm=m.fb.group({messageType:[null]});try{for(var u=C(Object.keys(a.MessageType)),d=u.next();!d.done;d=u.next()){var p=d.value;m.messageTypesList.push({name:a.messageTypeNames.get(a.MessageType[p]),value:p})}}catch(e){i={error:e}}finally{try{d&&!d.done&&(l=u.return)&&l.call(u)}finally{if(i)throw i.error}}return m}var l;return y(r,e),l=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.ngOnInit=function(){var e=this;this.filteredMessageTypes=this.messageTypeConfigForm.get("messageType").valueChanges.pipe(f.startWith(""),f.map((function(e){return e||""})),f.mergeMap((function(t){return e.fetchMessageTypes(t)})),f.share())},r.prototype.ngAfterViewInit=function(){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.messageTypeConfigForm.disable({emitEvent:!1}):this.messageTypeConfigForm.enable({emitEvent:!1})},r.prototype.writeValue=function(e){var t=this;this.searchText="",this.messageTypes.length=0,e&&e.forEach((function(e){var r=t.messageTypesList.find((function(t){return t.value===e}));r?t.messageTypes.push({name:r.name,value:r.value}):t.messageTypes.push({name:e,value:e})}))},r.prototype.displayMessageTypeFn=function(e){return e?e.name:void 0},r.prototype.textIsNotEmpty=function(e){return!!(e&&null!=e&&e.length>0)},r.prototype.createMessageType=function(e,t){e.preventDefault(),this.transformMessageType(t)},r.prototype.add=function(e){this.transformMessageType(e.value)},r.prototype.fetchMessageTypes=function(e){if(this.searchText=e,this.searchText&&this.searchText.length){var t=this.searchText.toUpperCase();return c.of(this.messageTypesList.filter((function(e){return e.name.toUpperCase().includes(t)})))}return c.of(this.messageTypesList)},r.prototype.transformMessageType=function(e){if((e||"").trim()){var t=null,r=e.trim(),n=this.messageTypesList.find((function(e){return e.name===r}));(t=n?{name:n.name,value:n.value}:{name:r,value:r})&&this.addMessageType(t)}this.clear("")},r.prototype.remove=function(e){var t=this.messageTypes.indexOf(e);t>=0&&(this.messageTypes.splice(t,1),this.updateModel())},r.prototype.selected=function(e){this.addMessageType(e.option.value),this.clear("")},r.prototype.addMessageType=function(e){-1===this.messageTypes.findIndex((function(t){return t.value===e.value}))&&(this.messageTypes.push(e),this.updateModel())},r.prototype.onFocus=function(){this.messageTypeConfigForm.get("messageType").updateValueAndValidity({onlySelf:!0,emitEvent:!0})},r.prototype.clear=function(e){var t=this;void 0===e&&(e=""),this.messageTypeInput.nativeElement.value=e,this.messageTypeConfigForm.get("messageType").patchValue(null,{emitEvent:!0}),setTimeout((function(){t.messageTypeInput.nativeElement.blur(),t.messageTypeInput.nativeElement.focus()}),0)},r.prototype.updateModel=function(){var e=this.messageTypes.map((function(e){return e.value}));this.required?(this.chipList.errorState=!e.length,this.propagateChange(e.length>0?e:null)):(this.chipList.errorState=!1,this.propagateChange(e))},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:a.TruncatePipe},{type:i.FormBuilder}]},b([t.Input(),h("design:type",Boolean),h("design:paramtypes",[Boolean])],r.prototype,"required",null),b([t.Input(),h("design:type",String)],r.prototype,"label",void 0),b([t.Input(),h("design:type",Object)],r.prototype,"placeholder",void 0),b([t.Input(),h("design:type",Boolean)],r.prototype,"disabled",void 0),b([t.ViewChild("chipList",{static:!1}),h("design:type",d.MatChipList)],r.prototype,"chipList",void 0),b([t.ViewChild("messageTypeAutocomplete",{static:!1}),h("design:type",p.MatAutocomplete)],r.prototype,"matAutocomplete",void 0),b([t.ViewChild("messageTypeInput",{static:!1}),h("design:type",t.ElementRef)],r.prototype,"messageTypeInput",void 0),r=l=b([t.Component({selector:"tb-message-types-config",template:'\n {{ label }}\n \n \n {{messageType.name}}\n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-message-types-found\n
\n \n \n {{ translate.get(\'tb.rulenode.no-message-type-matching\',\n {messageType: truncate.transform(searchText, true, 6, '...')}) | async }}\n \n \n \n tb.rulenode.create-new-message-type\n \n
\n
\n
\n \n {{ \'tb.rulenode.message-types-required\' | translate }}\n \n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return l})),multi:!0}]}),h("design:paramtypes",[o.Store,n.TranslateService,a.TruncatePipe,i.FormBuilder])],r)}(a.PageComponent),le=function(){function e(){}return e=b([t.NgModule({declarations:[ne,ae,oe,ie],imports:[r.CommonModule,a.SharedModule,m.HomeComponentsModule],exports:[ne,ae,oe,ie]})],e)}(),se=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.unassignCustomerConfigForm},r.prototype.onConfigurationSet=function(e){this.unassignCustomerConfigForm=this.fb.group({customerNamePattern:[e?e.customerNamePattern:null,[i.Validators.required]],customerCacheExpiration:[e?e.customerCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-un-assign-to-customer-config",template:'
\n \n tb.rulenode.customer-name-pattern\n \n \n {{ \'tb.rulenode.customer-name-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.customer-cache-expiration\n \n \n {{ \'tb.rulenode.customer-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.customer-cache-expiration-range\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),me=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.snsConfigForm},r.prototype.onConfigurationSet=function(e){this.snsConfigForm=this.fb.group({topicArnPattern:[e?e.topicArnPattern:null,[i.Validators.required]],accessKeyId:[e?e.accessKeyId:null,[i.Validators.required]],secretAccessKey:[e?e.secretAccessKey:null,[i.Validators.required]],region:[e?e.region:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-sns-config",template:'
\n \n tb.rulenode.topic-arn-pattern\n \n \n {{ \'tb.rulenode.topic-arn-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.aws-access-key-id\n \n \n {{ \'tb.rulenode.aws-access-key-id-required\' | translate }}\n \n \n \n tb.rulenode.aws-secret-access-key\n \n \n {{ \'tb.rulenode.aws-secret-access-key-required\' | translate }}\n \n \n \n tb.rulenode.aws-region\n \n \n {{ \'tb.rulenode.aws-region-required\' | translate }}\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ue=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.sqsQueueType=H,n.sqsQueueTypes=Object.keys(H),n.sqsQueueTypeTranslationsMap=$,n}return y(r,e),r.prototype.configForm=function(){return this.sqsConfigForm},r.prototype.onConfigurationSet=function(e){this.sqsConfigForm=this.fb.group({queueType:[e?e.queueType:null,[i.Validators.required]],queueUrlPattern:[e?e.queueUrlPattern:null,[i.Validators.required]],delaySeconds:[e?e.delaySeconds:null,[i.Validators.min(0),i.Validators.max(900)]],messageAttributes:[e?e.messageAttributes:null,[]],accessKeyId:[e?e.accessKeyId:null,[i.Validators.required]],secretAccessKey:[e?e.secretAccessKey:null,[i.Validators.required]],region:[e?e.region:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-sqs-config",template:'
\n \n tb.rulenode.queue-type\n \n \n {{ sqsQueueTypeTranslationsMap.get(type) | translate }}\n \n \n \n \n tb.rulenode.queue-url-pattern\n \n \n {{ \'tb.rulenode.queue-url-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.delay-seconds\n \n \n {{ \'tb.rulenode.min-delay-seconds-message\' | translate }}\n \n \n {{ \'tb.rulenode.max-delay-seconds-message\' | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.aws-access-key-id\n \n \n {{ \'tb.rulenode.aws-access-key-id-required\' | translate }}\n \n \n \n tb.rulenode.aws-secret-access-key\n \n \n {{ \'tb.rulenode.aws-secret-access-key-required\' | translate }}\n \n \n \n tb.rulenode.aws-region\n \n \n {{ \'tb.rulenode.aws-region-required\' | translate }}\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),de=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.pubSubConfigForm},r.prototype.onConfigurationSet=function(e){this.pubSubConfigForm=this.fb.group({projectId:[e?e.projectId:null,[i.Validators.required]],topicName:[e?e.topicName:null,[i.Validators.required]],serviceAccountKey:[e?e.serviceAccountKey:null,[i.Validators.required]],serviceAccountKeyFileName:[e?e.serviceAccountKeyFileName:null,[i.Validators.required]],messageAttributes:[e?e.messageAttributes:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-pub-sub-config",template:'
\n \n tb.rulenode.gcp-project-id\n \n \n {{ \'tb.rulenode.gcp-project-id-required\' | translate }}\n \n \n \n tb.rulenode.pubsub-topic-name\n \n \n {{ \'tb.rulenode.pubsub-topic-name-required\' | translate }}\n \n \n \n \n \n
\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),pe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.ackValues=["all","-1","0","1"],n.ToByteStandartCharsetTypesValues=Y,n.ToByteStandartCharsetTypeTranslationMap=Z,n}return y(r,e),r.prototype.configForm=function(){return this.kafkaConfigForm},r.prototype.onConfigurationSet=function(e){this.kafkaConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[i.Validators.required]],bootstrapServers:[e?e.bootstrapServers:null,[i.Validators.required]],retries:[e?e.retries:null,[i.Validators.min(0)]],batchSize:[e?e.batchSize:null,[i.Validators.min(0)]],linger:[e?e.linger:null,[i.Validators.min(0)]],bufferMemory:[e?e.bufferMemory:null,[i.Validators.min(0)]],acks:[e?e.acks:null,[i.Validators.required]],keySerializer:[e?e.keySerializer:null,[i.Validators.required]],valueSerializer:[e?e.valueSerializer:null,[i.Validators.required]],otherProperties:[e?e.otherProperties:null,[]],addMetadataKeyValuesAsKafkaHeaders:[!!e&&e.addMetadataKeyValuesAsKafkaHeaders,[]],kafkaHeadersCharset:[e?e.kafkaHeadersCharset:null,[]]})},r.prototype.validatorTriggers=function(){return["addMetadataKeyValuesAsKafkaHeaders"]},r.prototype.updateValidators=function(e){this.kafkaConfigForm.get("addMetadataKeyValuesAsKafkaHeaders").value?this.kafkaConfigForm.get("kafkaHeadersCharset").setValidators([i.Validators.required]):this.kafkaConfigForm.get("kafkaHeadersCharset").setValidators([]),this.kafkaConfigForm.get("kafkaHeadersCharset").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-kafka-config",template:'
\n \n tb.rulenode.topic-pattern\n \n \n {{ \'tb.rulenode.topic-pattern-required\' | translate }}\n \n \n \n tb.rulenode.bootstrap-servers\n \n \n {{ \'tb.rulenode.bootstrap-servers-required\' | translate }}\n \n \n \n tb.rulenode.retries\n \n \n {{ \'tb.rulenode.min-retries-message\' | translate }}\n \n \n \n tb.rulenode.batch-size-bytes\n \n \n {{ \'tb.rulenode.min-batch-size-bytes-message\' | translate }}\n \n \n \n tb.rulenode.linger-ms\n \n \n {{ \'tb.rulenode.min-linger-ms-message\' | translate }}\n \n \n \n tb.rulenode.buffer-memory-bytes\n \n \n {{ \'tb.rulenode.min-buffer-memory-bytes-message\' | translate }}\n \n \n \n tb.rulenode.acks\n \n \n {{ ackValue }}\n \n \n \n \n tb.rulenode.key-serializer\n \n \n {{ \'tb.rulenode.key-serializer-required\' | translate }}\n \n \n \n tb.rulenode.value-serializer\n \n \n {{ \'tb.rulenode.value-serializer-required\' | translate }}\n \n \n \n \n \n \n {{ \'tb.rulenode.add-metadata-key-values-as-kafka-headers\' | translate }}\n \n
tb.rulenode.add-metadata-key-values-as-kafka-headers-hint
\n \n tb.rulenode.charset-encoding\n \n \n {{ ToByteStandartCharsetTypeTranslationMap.get(charset) | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ce=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.allMqttCredentialsTypes=Q,n.mqttCredentialsTypeTranslationsMap=_,n}return y(r,e),r.prototype.configForm=function(){return this.mqttConfigForm},r.prototype.onConfigurationSet=function(e){this.mqttConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[i.Validators.required]],host:[e?e.host:null,[i.Validators.required]],port:[e?e.port:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]],connectTimeoutSec:[e?e.connectTimeoutSec:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(200)]],clientId:[e?e.clientId:null,[]],cleanSession:[!!e&&e.cleanSession,[]],ssl:[!!e&&e.ssl,[]],credentials:this.fb.group({type:[e&&e.credentials?e.credentials.type:null,[i.Validators.required]],username:[e&&e.credentials?e.credentials.username:null,[]],password:[e&&e.credentials?e.credentials.password:null,[]],caCert:[e&&e.credentials?e.credentials.caCert:null,[]],caCertFileName:[e&&e.credentials?e.credentials.caCertFileName:null,[]],privateKey:[e&&e.credentials?e.credentials.privateKey:null,[]],privateKeyFileName:[e&&e.credentials?e.credentials.privateKeyFileName:null,[]],cert:[e&&e.credentials?e.credentials.cert:null,[]],certFileName:[e&&e.credentials?e.credentials.certFileName:null,[]]})})},r.prototype.prepareOutputConfig=function(e){var t=e.credentials.type;switch(t){case"anonymous":e.credentials={type:t};break;case"basic":e.credentials={type:t,username:e.credentials.username,password:e.credentials.password};break;case"cert.PEM":delete e.credentials.username}return e},r.prototype.validatorTriggers=function(){return["credentials.type"]},r.prototype.updateValidators=function(e){var t=this.mqttConfigForm.get("credentials"),r=t.get("type").value;switch(e&&t.reset({type:r},{emitEvent:!1}),t.get("username").setValidators([]),t.get("password").setValidators([]),t.get("caCert").setValidators([]),t.get("caCertFileName").setValidators([]),t.get("privateKey").setValidators([]),t.get("privateKeyFileName").setValidators([]),t.get("cert").setValidators([]),t.get("certFileName").setValidators([]),r){case"anonymous":break;case"basic":t.get("username").setValidators([i.Validators.required]),t.get("password").setValidators([i.Validators.required]);break;case"cert.PEM":t.get("caCert").setValidators([i.Validators.required]),t.get("caCertFileName").setValidators([i.Validators.required]),t.get("privateKey").setValidators([i.Validators.required]),t.get("privateKeyFileName").setValidators([i.Validators.required]),t.get("cert").setValidators([i.Validators.required]),t.get("certFileName").setValidators([i.Validators.required])}t.get("username").updateValueAndValidity({emitEvent:e}),t.get("password").updateValueAndValidity({emitEvent:e}),t.get("caCert").updateValueAndValidity({emitEvent:e}),t.get("caCertFileName").updateValueAndValidity({emitEvent:e}),t.get("privateKey").updateValueAndValidity({emitEvent:e}),t.get("privateKeyFileName").updateValueAndValidity({emitEvent:e}),t.get("cert").updateValueAndValidity({emitEvent:e}),t.get("certFileName").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-mqtt-config",template:'
\n \n tb.rulenode.topic-pattern\n \n \n {{ \'tb.rulenode.topic-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.host\n \n \n {{ \'tb.rulenode.host-required\' | translate }}\n \n \n \n tb.rulenode.port\n \n \n {{ \'tb.rulenode.port-required\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n \n tb.rulenode.connect-timeout\n \n \n {{ \'tb.rulenode.connect-timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.connect-timeout-range\' | translate }}\n \n \n {{ \'tb.rulenode.connect-timeout-range\' | translate }}\n \n \n
\n \n tb.rulenode.client-id\n \n \n \n {{ \'tb.rulenode.clean-session\' | translate }}\n \n \n {{ \'tb.rulenode.enable-ssl\' | translate }}\n \n \n \n tb.rulenode.credentials\n \n {{ mqttCredentialsTypeTranslationsMap.get(mqttConfigForm.get(\'credentials\').get(\'type\').value) | translate }}\n \n \n
\n \n tb.rulenode.credentials-type\n \n \n {{ mqttCredentialsTypeTranslationsMap.get(credentialsType) | translate }}\n \n \n \n {{ \'tb.rulenode.credentials-type-required\' | translate }}\n \n \n
\n \n \n \n \n tb.rulenode.username\n \n \n {{ \'tb.rulenode.username-required\' | translate }}\n \n \n \n tb.rulenode.password\n \n \n {{ \'tb.rulenode.password-required\' | translate }}\n \n \n \n \n \n \n \n \n \n \n \n tb.rulenode.private-key-password\n \n \n \n
\n
\n
\n
\n',styles:[":host .tb-mqtt-credentials-panel-group{margin:0 6px}"]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),fe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.messageProperties=[null,"BASIC","TEXT_PLAIN","MINIMAL_BASIC","MINIMAL_PERSISTENT_BASIC","PERSISTENT_BASIC","PERSISTENT_TEXT_PLAIN"],n}return y(r,e),r.prototype.configForm=function(){return this.rabbitMqConfigForm},r.prototype.onConfigurationSet=function(e){this.rabbitMqConfigForm=this.fb.group({exchangeNamePattern:[e?e.exchangeNamePattern:null,[]],routingKeyPattern:[e?e.routingKeyPattern:null,[]],messageProperties:[e?e.messageProperties:null,[]],host:[e?e.host:null,[i.Validators.required]],port:[e?e.port:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]],virtualHost:[e?e.virtualHost:null,[]],username:[e?e.username:null,[]],password:[e?e.password:null,[]],automaticRecoveryEnabled:[!!e&&e.automaticRecoveryEnabled,[]],connectionTimeout:[e?e.connectionTimeout:null,[i.Validators.min(0)]],handshakeTimeout:[e?e.handshakeTimeout:null,[i.Validators.min(0)]],clientProperties:[e?e.clientProperties:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-rabbit-mq-config",template:'
\n \n tb.rulenode.exchange-name-pattern\n \n \n \n tb.rulenode.routing-key-pattern\n \n \n \n tb.rulenode.message-properties\n \n \n {{ property }}\n \n \n \n
\n \n tb.rulenode.host\n \n \n {{ \'tb.rulenode.host-required\' | translate }}\n \n \n \n tb.rulenode.port\n \n \n {{ \'tb.rulenode.port-required\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n
\n \n tb.rulenode.virtual-host\n \n \n \n tb.rulenode.username\n \n \n \n tb.rulenode.password\n \n \n \n {{ \'tb.rulenode.automatic-recovery\' | translate }}\n \n \n tb.rulenode.connection-timeout-ms\n \n \n {{ \'tb.rulenode.min-connection-timeout-ms-message\' | translate }}\n \n \n \n tb.rulenode.handshake-timeout-ms\n \n \n {{ \'tb.rulenode.min-handshake-timeout-ms-message\' | translate }}\n \n \n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ge=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.proxySchemes=["http","https"],n.httpRequestTypes=Object.keys(z),n}return y(r,e),r.prototype.configForm=function(){return this.restApiCallConfigForm},r.prototype.onConfigurationSet=function(e){this.restApiCallConfigForm=this.fb.group({restEndpointUrlPattern:[e?e.restEndpointUrlPattern:null,[i.Validators.required]],requestMethod:[e?e.requestMethod:null,[i.Validators.required]],useSimpleClientHttpFactory:[!!e&&e.useSimpleClientHttpFactory,[]],enableProxy:[!!e&&e.enableProxy,[]],useSystemProxyProperties:[!!e&&e.enableProxy,[]],proxyScheme:[e?e.proxyHost:null,[]],proxyHost:[e?e.proxyHost:null,[]],proxyPort:[e?e.proxyPort:null,[]],proxyUser:[e?e.proxyUser:null,[]],proxyPassword:[e?e.proxyPassword:null,[]],readTimeoutMs:[e?e.readTimeoutMs:null,[]],maxParallelRequestsCount:[e?e.maxParallelRequestsCount:null,[i.Validators.min(0)]],headers:[e?e.headers:null,[]],useRedisQueueForMsgPersistence:[!!e&&e.useRedisQueueForMsgPersistence,[]],trimQueue:[!!e&&e.trimQueue,[]],maxQueueSize:[e?e.maxQueueSize:null,[]]})},r.prototype.validatorTriggers=function(){return["useSimpleClientHttpFactory","useRedisQueueForMsgPersistence","enableProxy","useSystemProxyProperties"]},r.prototype.updateValidators=function(e){var t=this.restApiCallConfigForm.get("useSimpleClientHttpFactory").value,r=this.restApiCallConfigForm.get("useRedisQueueForMsgPersistence").value,n=this.restApiCallConfigForm.get("enableProxy").value,a=this.restApiCallConfigForm.get("useSystemProxyProperties").value;n&&!a?(this.restApiCallConfigForm.get("proxyHost").setValidators(n?[i.Validators.required]:[]),this.restApiCallConfigForm.get("proxyPort").setValidators(n?[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]:[])):(this.restApiCallConfigForm.get("proxyHost").setValidators([]),this.restApiCallConfigForm.get("proxyPort").setValidators([]),t?this.restApiCallConfigForm.get("readTimeoutMs").setValidators([]):this.restApiCallConfigForm.get("readTimeoutMs").setValidators([i.Validators.min(0)])),r?this.restApiCallConfigForm.get("maxQueueSize").setValidators([i.Validators.min(0)]):this.restApiCallConfigForm.get("maxQueueSize").setValidators([]),this.restApiCallConfigForm.get("readTimeoutMs").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("maxQueueSize").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("proxyHost").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("proxyPort").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-rest-api-call-config",template:'
\n \n tb.rulenode.endpoint-url-pattern\n \n \n {{ \'tb.rulenode.endpoint-url-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.request-method\n \n \n {{ requestType }}\n \n \n \n \n {{ \'tb.rulenode.enable-proxy\' | translate }}\n \n \n {{ \'tb.rulenode.use-simple-client-http-factory\' | translate }}\n \n
\n \n {{ \'tb.rulenode.use-system-proxy-properties\' | translate }}\n \n
\n
\n \n tb.rulenode.proxy-scheme\n \n \n {{ proxyScheme }}\n \n \n \n \n tb.rulenode.proxy-host\n \n \n {{ \'tb.rulenode.proxy-host-required\' | translate }}\n \n \n \n tb.rulenode.proxy-port\n \n \n {{ \'tb.rulenode.proxy-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.proxy-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.proxy-user\n \n \n \n tb.rulenode.proxy-password\n \n \n
\n
\n \n tb.rulenode.read-timeout\n \n \n \n \n tb.rulenode.max-parallel-requests-count\n \n \n \n \n
\n \n \n \n {{ \'tb.rulenode.use-redis-queue\' | translate }}\n \n
\n \n {{ \'tb.rulenode.trim-redis-queue\' | translate }}\n \n \n tb.rulenode.redis-queue-max-size\n \n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ye=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.smtpProtocols=["smtp","smtps"],n.tlsVersions=["TLSv1","TLSv1.1","TLSv1.2","TLSv1.3"],n}return y(r,e),r.prototype.configForm=function(){return this.sendEmailConfigForm},r.prototype.onConfigurationSet=function(e){this.sendEmailConfigForm=this.fb.group({useSystemSmtpSettings:[!!e&&e.useSystemSmtpSettings,[]],smtpProtocol:[e?e.smtpProtocol:null,[]],smtpHost:[e?e.smtpHost:null,[]],smtpPort:[e?e.smtpPort:null,[]],timeout:[e?e.timeout:null,[]],enableTls:[!!e&&e.enableTls,[]],tlsVersion:[e?e.tlsVersion:null,[]],enableProxy:[!!e&&e.enableProxy,[]],proxyHost:[e?e.proxyHost:null,[]],proxyPort:[e?e.proxyPort:null,[]],proxyUser:[e?e.proxyUser:null,[]],proxyPassword:[e?e.proxyPassword:null,[]],username:[e?e.username:null,[]],password:[e?e.password:null,[]]})},r.prototype.validatorTriggers=function(){return["useSystemSmtpSettings","enableProxy"]},r.prototype.updateValidators=function(e){var t=this.sendEmailConfigForm.get("useSystemSmtpSettings").value,r=this.sendEmailConfigForm.get("enableProxy").value;t?(this.sendEmailConfigForm.get("smtpProtocol").setValidators([]),this.sendEmailConfigForm.get("smtpHost").setValidators([]),this.sendEmailConfigForm.get("smtpPort").setValidators([]),this.sendEmailConfigForm.get("timeout").setValidators([]),this.sendEmailConfigForm.get("proxyHost").setValidators([]),this.sendEmailConfigForm.get("proxyPort").setValidators([])):(this.sendEmailConfigForm.get("smtpProtocol").setValidators([i.Validators.required]),this.sendEmailConfigForm.get("smtpHost").setValidators([i.Validators.required]),this.sendEmailConfigForm.get("smtpPort").setValidators([i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]),this.sendEmailConfigForm.get("timeout").setValidators([i.Validators.required,i.Validators.min(0)]),this.sendEmailConfigForm.get("proxyHost").setValidators(r?[i.Validators.required]:[]),this.sendEmailConfigForm.get("proxyPort").setValidators(r?[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]:[])),this.sendEmailConfigForm.get("smtpProtocol").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("smtpHost").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("smtpPort").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("timeout").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("proxyHost").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("proxyPort").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-send-email-config",template:'
\n \n {{ \'tb.rulenode.use-system-smtp-settings\' | translate }}\n \n
\n \n tb.rulenode.smtp-protocol\n \n \n {{ smtpProtocol.toUpperCase() }}\n \n \n \n
\n \n tb.rulenode.smtp-host\n \n \n {{ \'tb.rulenode.smtp-host-required\' | translate }}\n \n \n \n tb.rulenode.smtp-port\n \n \n {{ \'tb.rulenode.smtp-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.smtp-port-range\' | translate }}\n \n \n {{ \'tb.rulenode.smtp-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.timeout-msec\n \n \n {{ \'tb.rulenode.timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-timeout-msec-message\' | translate }}\n \n \n \n {{ \'tb.rulenode.enable-tls\' | translate }}\n \n \n tb.rulenode.tls-version\n \n \n {{ tlsVersion }}\n \n \n \n \n {{ \'tb.rulenode.enable-proxy\' | translate }}\n \n
\n
\n \n tb.rulenode.proxy-host\n \n \n {{ \'tb.rulenode.proxy-host-required\' | translate }}\n \n \n \n tb.rulenode.proxy-port\n \n \n {{ \'tb.rulenode.proxy-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.proxy-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.proxy-user\n \n \n \n tb.rulenode.proxy-password\n \n \n
\n \n tb.rulenode.username\n \n \n \n tb.rulenode.password\n \n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),be=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.serviceType=a.ServiceType.TB_RULE_ENGINE,n}return y(r,e),r.prototype.configForm=function(){return this.checkPointConfigForm},r.prototype.onConfigurationSet=function(e){this.checkPointConfigForm=this.fb.group({queueName:[e?e.queueName:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-check-point-config",template:'
\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),he=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.allAzureIotHubCredentialsTypes=W,n.azureIotHubCredentialsTypeTranslationsMap=J,n}return y(r,e),r.prototype.configForm=function(){return this.azureIotHubConfigForm},r.prototype.onConfigurationSet=function(e){this.azureIotHubConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[i.Validators.required]],host:[e?e.host:null,[i.Validators.required]],port:[e?e.port:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]],connectTimeoutSec:[e?e.connectTimeoutSec:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(200)]],clientId:[e?e.clientId:null,[i.Validators.required]],cleanSession:[!!e&&e.cleanSession,[]],ssl:[!!e&&e.ssl,[]],credentials:this.fb.group({type:[e&&e.credentials?e.credentials.type:null,[i.Validators.required]],sasKey:[e&&e.credentials?e.credentials.sasKey:null,[]],caCert:[e&&e.credentials?e.credentials.caCert:null,[]],caCertFileName:[e&&e.credentials?e.credentials.caCertFileName:null,[]],privateKey:[e&&e.credentials?e.credentials.privateKey:null,[]],privateKeyFileName:[e&&e.credentials?e.credentials.privateKeyFileName:null,[]],cert:[e&&e.credentials?e.credentials.cert:null,[]],certFileName:[e&&e.credentials?e.credentials.certFileName:null,[]],password:[e&&e.credentials?e.credentials.password:null,[]]})})},r.prototype.prepareOutputConfig=function(e){var t=e.credentials.type;return"sas"===t&&(e.credentials={type:t,sasKey:e.credentials.sasKey,caCert:e.credentials.caCert,caCertFileName:e.credentials.caCertFileName}),e},r.prototype.validatorTriggers=function(){return["credentials.type"]},r.prototype.updateValidators=function(e){var t=this.azureIotHubConfigForm.get("credentials"),r=t.get("type").value;switch(e&&t.reset({type:r},{emitEvent:!1}),t.get("sasKey").setValidators([]),t.get("privateKey").setValidators([]),t.get("privateKeyFileName").setValidators([]),t.get("cert").setValidators([]),t.get("certFileName").setValidators([]),r){case"sas":t.get("sasKey").setValidators([i.Validators.required]);break;case"cert.PEM":t.get("privateKey").setValidators([i.Validators.required]),t.get("privateKeyFileName").setValidators([i.Validators.required]),t.get("cert").setValidators([i.Validators.required]),t.get("certFileName").setValidators([i.Validators.required])}t.get("sasKey").updateValueAndValidity({emitEvent:e}),t.get("privateKey").updateValueAndValidity({emitEvent:e}),t.get("privateKeyFileName").updateValueAndValidity({emitEvent:e}),t.get("cert").updateValueAndValidity({emitEvent:e}),t.get("certFileName").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-azure-iot-hub-config",template:'
\n \n tb.rulenode.topic\n \n \n {{ \'tb.rulenode.topic-required\' | translate }}\n \n \n \n tb.rulenode.hostname\n \n \n {{ \'tb.rulenode.hostname-required\' | translate }}\n \n \n \n tb.rulenode.device-id\n \n \n {{ \'tb.rulenode.device-id-required\' | translate }}\n \n \n \n \n \n tb.rulenode.credentials\n \n {{ azureIotHubCredentialsTypeTranslationsMap.get(azureIotHubConfigForm.get(\'credentials.type\').value) | translate }}\n \n \n
\n \n tb.rulenode.credentials-type\n \n \n {{ azureIotHubCredentialsTypeTranslationsMap.get(credentialsType) | translate }}\n \n \n \n {{ \'tb.rulenode.credentials-type-required\' | translate }}\n \n \n
\n \n \n \n \n tb.rulenode.sas-key\n \n \n {{ \'tb.rulenode.sas-key-required\' | translate }}\n \n \n \n \n \n \n \n \n \n \n \n \n \n tb.rulenode.private-key-password\n \n \n \n
\n
\n
\n
\n
\n',styles:[":host .tb-mqtt-credentials-panel-group{margin:0 6px}"]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ce=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.deviceProfile},r.prototype.onConfigurationSet=function(e){this.deviceProfile=this.fb.group({persistAlarmRulesState:[!!e&&e.persistAlarmRulesState,i.Validators.required],fetchAlarmRulesStateOnStart:[!!e&&e.fetchAlarmRulesStateOnStart,i.Validators.required]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-device-profile-config",template:'
\n \n {{ \'tb.rulenode.persist-alarm-rules\' | translate }}\n \n \n {{ \'tb.rulenode.fetch-alarm-rules\' | translate }}\n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ve=function(){function e(){}return e=b([t.NgModule({declarations:[x,T,q,S,I,k,N,V,E,A,L,X,ee,te,re,se,me,ue,de,pe,ce,fe,ge,ye,be,he,Ce],imports:[r.CommonModule,a.SharedModule,le],exports:[x,T,q,S,I,k,N,V,E,A,L,X,ee,te,re,se,me,ue,de,pe,ce,fe,ge,ye,be,he,Ce]})],e)}(),Fe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[s.ENTER,s.COMMA,s.SEMICOLON],n}return y(r,e),r.prototype.configForm=function(){return this.checkMessageConfigForm},r.prototype.onConfigurationSet=function(e){this.checkMessageConfigForm=this.fb.group({messageNames:[e?e.messageNames:null,[]],metadataNames:[e?e.metadataNames:null,[]],checkAllKeys:[!!e&&e.checkAllKeys,[]]})},r.prototype.validateConfig=function(){var e=this.checkMessageConfigForm.get("messageNames").value,t=this.checkMessageConfigForm.get("metadataNames").value;return e.length>0||t.length>0},r.prototype.removeMessageName=function(e){var t=this.checkMessageConfigForm.get("messageNames").value,r=t.indexOf(e);r>=0&&(t.splice(r,1),this.checkMessageConfigForm.get("messageNames").setValue(t,{emitEvent:!0}))},r.prototype.removeMetadataName=function(e){var t=this.checkMessageConfigForm.get("metadataNames").value,r=t.indexOf(e);r>=0&&(t.splice(r,1),this.checkMessageConfigForm.get("metadataNames").setValue(t,{emitEvent:!0}))},r.prototype.addMessageName=function(e){var t=e.input,r=e.value;if((r||"").trim()){r=r.trim();var n=this.checkMessageConfigForm.get("messageNames").value;n&&-1!==n.indexOf(r)||(n||(n=[]),n.push(r),this.checkMessageConfigForm.get("messageNames").setValue(n,{emitEvent:!0}))}t&&(t.value="")},r.prototype.addMetadataName=function(e){var t=e.input,r=e.value;if((r||"").trim()){r=r.trim();var n=this.checkMessageConfigForm.get("metadataNames").value;n&&-1!==n.indexOf(r)||(n||(n=[]),n.push(r),this.checkMessageConfigForm.get("metadataNames").setValue(n,{emitEvent:!0}))}t&&(t.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-filter-node-check-message-config",template:'
\n \n \n \n \n \n {{messageName}}\n close\n \n \n \n \n
tb.rulenode.separator-hint
\n \n \n \n \n \n {{metadataName}}\n close\n \n \n \n \n
tb.rulenode.separator-hint
\n \n {{ \'tb.rulenode.check-all-keys\' | translate }}\n \n
tb.rulenode.check-all-keys-hint
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),xe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.entitySearchDirection=Object.keys(a.EntitySearchDirection),n.entitySearchDirectionTranslationsMap=a.entitySearchDirectionTranslations,n}return y(r,e),r.prototype.configForm=function(){return this.checkRelationConfigForm},r.prototype.onConfigurationSet=function(e){this.checkRelationConfigForm=this.fb.group({checkForSingleEntity:[!!e&&e.checkForSingleEntity,[]],direction:[e?e.direction:null,[]],entityType:[e?e.entityType:null,e&&e.checkForSingleEntity?[i.Validators.required]:[]],entityId:[e?e.entityId:null,e&&e.checkForSingleEntity?[i.Validators.required]:[]],relationType:[e?e.relationType:null,[i.Validators.required]]})},r.prototype.validatorTriggers=function(){return["checkForSingleEntity"]},r.prototype.updateValidators=function(e){var t=this.checkRelationConfigForm.get("checkForSingleEntity").value;this.checkRelationConfigForm.get("entityType").setValidators(t?[i.Validators.required]:[]),this.checkRelationConfigForm.get("entityType").updateValueAndValidity({emitEvent:e}),this.checkRelationConfigForm.get("entityId").setValidators(t?[i.Validators.required]:[]),this.checkRelationConfigForm.get("entityId").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-filter-node-check-relation-config",template:'
\n \n {{ \'tb.rulenode.check-relation-to-specific-entity\' | translate }}\n \n
tb.rulenode.check-relation-hint
\n \n relation.direction\n \n \n {{ entitySearchDirectionTranslationsMap.get(direction) | translate }}\n \n \n \n
\n \n \n \n \n
\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Te=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.perimeterType=M,n.perimeterTypes=Object.keys(M),n.perimeterTypeTranslationMap=w,n.rangeUnits=Object.keys(O),n.rangeUnitTranslationMap=B,n}return y(r,e),r.prototype.configForm=function(){return this.geoFilterConfigForm},r.prototype.onConfigurationSet=function(e){this.geoFilterConfigForm=this.fb.group({latitudeKeyName:[e?e.latitudeKeyName:null,[i.Validators.required]],longitudeKeyName:[e?e.longitudeKeyName:null,[i.Validators.required]],fetchPerimeterInfoFromMessageMetadata:[!!e&&e.fetchPerimeterInfoFromMessageMetadata,[]],perimeterType:[e?e.perimeterType:null,[]],centerLatitude:[e?e.centerLatitude:null,[]],centerLongitude:[e?e.centerLatitude:null,[]],range:[e?e.range:null,[]],rangeUnit:[e?e.rangeUnit:null,[]],polygonsDefinition:[e?e.polygonsDefinition:null,[]]})},r.prototype.validatorTriggers=function(){return["fetchPerimeterInfoFromMessageMetadata","perimeterType"]},r.prototype.updateValidators=function(e){var t=this.geoFilterConfigForm.get("fetchPerimeterInfoFromMessageMetadata").value,r=this.geoFilterConfigForm.get("perimeterType").value;t?this.geoFilterConfigForm.get("perimeterType").setValidators([]):this.geoFilterConfigForm.get("perimeterType").setValidators([i.Validators.required]),t||r!==M.CIRCLE?(this.geoFilterConfigForm.get("centerLatitude").setValidators([]),this.geoFilterConfigForm.get("centerLongitude").setValidators([]),this.geoFilterConfigForm.get("range").setValidators([]),this.geoFilterConfigForm.get("rangeUnit").setValidators([])):(this.geoFilterConfigForm.get("centerLatitude").setValidators([i.Validators.required,i.Validators.min(-90),i.Validators.max(90)]),this.geoFilterConfigForm.get("centerLongitude").setValidators([i.Validators.required,i.Validators.min(-180),i.Validators.max(180)]),this.geoFilterConfigForm.get("range").setValidators([i.Validators.required,i.Validators.min(0)]),this.geoFilterConfigForm.get("rangeUnit").setValidators([i.Validators.required])),t||r!==M.POLYGON?this.geoFilterConfigForm.get("polygonsDefinition").setValidators([]):this.geoFilterConfigForm.get("polygonsDefinition").setValidators([i.Validators.required]),this.geoFilterConfigForm.get("perimeterType").updateValueAndValidity({emitEvent:!1}),this.geoFilterConfigForm.get("centerLatitude").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("centerLongitude").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("range").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("rangeUnit").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("polygonsDefinition").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-filter-node-gps-geofencing-config",template:'
\n \n tb.rulenode.latitude-key-name\n \n \n {{ \'tb.rulenode.latitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.longitude-key-name\n \n \n {{ \'tb.rulenode.longitude-key-name-required\' | translate }}\n \n \n \n {{ \'tb.rulenode.fetch-perimeter-info-from-message-metadata\' | translate }}\n \n
\n \n tb.rulenode.perimeter-type\n \n \n {{ perimeterTypeTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n \n tb.rulenode.circle-center-latitude\n \n \n {{ \'tb.rulenode.circle-center-latitude-required\' | translate }}\n \n \n \n tb.rulenode.circle-center-longitude\n \n \n {{ \'tb.rulenode.circle-center-longitude-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.range\n \n \n {{ \'tb.rulenode.range-required\' | translate }}\n \n \n \n tb.rulenode.range-units\n \n \n {{ rangeUnitTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n
\n \n tb.rulenode.polygon-definition\n \n \n {{ \'tb.rulenode.polygon-definition-required\' | translate }}\n \n \n
\n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),qe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.messageTypeConfigForm},r.prototype.onConfigurationSet=function(e){this.messageTypeConfigForm=this.fb.group({messageTypes:[e?e.messageTypes:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-filter-node-message-type-config",template:'
\n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Se=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.allowedEntityTypes=[a.EntityType.DEVICE,a.EntityType.ASSET,a.EntityType.ENTITY_VIEW,a.EntityType.TENANT,a.EntityType.CUSTOMER,a.EntityType.USER,a.EntityType.DASHBOARD,a.EntityType.RULE_CHAIN,a.EntityType.RULE_NODE],n}return y(r,e),r.prototype.configForm=function(){return this.originatorTypeConfigForm},r.prototype.onConfigurationSet=function(e){this.originatorTypeConfigForm=this.fb.group({originatorTypes:[e?e.originatorTypes:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-filter-node-originator-type-config",template:'
\n \n \n \n
\n',styles:[":host ::ng-deep tb-entity-type-list .mat-form-field-flex{padding-top:0}:host ::ng-deep tb-entity-type-list .mat-form-field-infix{border-top:0}"]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ie=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return y(r,e),r.prototype.configForm=function(){return this.scriptConfigForm},r.prototype.onConfigurationSet=function(e){this.scriptConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.scriptConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"filter",this.translate.instant("tb.rulenode.filter"),"Filter",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.scriptConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:l.NodeScriptTestService},{type:n.TranslateService}]},b([t.ViewChild("jsFuncComponent",{static:!0}),h("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=b([t.Component({selector:"tb-filter-node-script-config",template:'
\n \n \n \n
\n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder,l.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),ke=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return y(r,e),r.prototype.configForm=function(){return this.switchConfigForm},r.prototype.onConfigurationSet=function(e){this.switchConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.switchConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"switch",this.translate.instant("tb.rulenode.switch"),"Switch",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.switchConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:l.NodeScriptTestService},{type:n.TranslateService}]},b([t.ViewChild("jsFuncComponent",{static:!0}),h("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=b([t.Component({selector:"tb-filter-node-switch-config",template:'
\n \n \n \n
\n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder,l.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),Ne=function(e){function r(t,r,n){var o,l,s=e.call(this,t)||this;s.store=t,s.translate=r,s.fb=n,s.alarmStatusTranslationsMap=a.alarmStatusTranslations,s.alarmStatusList=[],s.searchText="",s.displayStatusFn=s.displayStatus.bind(s);try{for(var m=C(Object.keys(a.AlarmStatus)),u=m.next();!u.done;u=m.next()){var d=u.value;s.alarmStatusList.push(a.AlarmStatus[d])}}catch(e){o={error:e}}finally{try{u&&!u.done&&(l=m.return)&&l.call(m)}finally{if(o)throw o.error}}return s.statusFormControl=new i.FormControl(""),s.filteredAlarmStatus=s.statusFormControl.valueChanges.pipe(f.startWith(""),f.map((function(e){return e||""})),f.mergeMap((function(e){return s.fetchAlarmStatus(e)})),f.share()),s}return y(r,e),r.prototype.ngOnInit=function(){e.prototype.ngOnInit.call(this)},r.prototype.configForm=function(){return this.alarmStatusConfigForm},r.prototype.prepareInputConfig=function(e){return this.searchText="",this.statusFormControl.patchValue("",{emitEvent:!0}),e},r.prototype.onConfigurationSet=function(e){this.alarmStatusConfigForm=this.fb.group({alarmStatusList:[e?e.alarmStatusList:null,[i.Validators.required]]})},r.prototype.displayStatus=function(e){return e?this.translate.instant(a.alarmStatusTranslations.get(e)):void 0},r.prototype.fetchAlarmStatus=function(e){var t=this,r=this.getAlarmStatusList();if(this.searchText=e,this.searchText&&this.searchText.length){var n=this.searchText.toUpperCase();return c.of(r.filter((function(e){return t.translate.instant(a.alarmStatusTranslations.get(a.AlarmStatus[e])).toUpperCase().includes(n)})))}return c.of(r)},r.prototype.alarmStatusSelected=function(e){this.addAlarmStatus(e.option.value),this.clear("")},r.prototype.removeAlarmStatus=function(e){var t=this.alarmStatusConfigForm.get("alarmStatusList").value;if(t){var r=t.indexOf(e);r>=0&&(t.splice(r,1),this.alarmStatusConfigForm.get("alarmStatusList").setValue(t))}},r.prototype.addAlarmStatus=function(e){var t=this.alarmStatusConfigForm.get("alarmStatusList").value;t||(t=[]),-1===t.indexOf(e)&&(t.push(e),this.alarmStatusConfigForm.get("alarmStatusList").setValue(t))},r.prototype.getAlarmStatusList=function(){var e=this;return this.alarmStatusList.filter((function(t){return-1===e.alarmStatusConfigForm.get("alarmStatusList").value.indexOf(t)}))},r.prototype.onAlarmStatusInputFocus=function(){this.statusFormControl.updateValueAndValidity({onlySelf:!0,emitEvent:!0})},r.prototype.clear=function(e){var t=this;void 0===e&&(e=""),this.alarmStatusInput.nativeElement.value=e,this.statusFormControl.patchValue(null,{emitEvent:!0}),setTimeout((function(){t.alarmStatusInput.nativeElement.blur(),t.alarmStatusInput.nativeElement.focus()}),0)},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:i.FormBuilder}]},b([t.ViewChild("alarmStatusInput",{static:!1}),h("design:type",t.ElementRef)],r.prototype,"alarmStatusInput",void 0),r=b([t.Component({selector:"tb-filter-node-check-alarm-status-config",template:'
\n \n tb.rulenode.alarm-status-filter\n \n \n \n {{alarmStatusTranslationsMap.get(alarmStatus) | translate}}\n \n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-alarm-status-matching\n
\n
\n
\n
\n
\n \n
\n\n\n\n'}),h("design:paramtypes",[o.Store,n.TranslateService,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ve=function(){function e(){}return e=b([t.NgModule({declarations:[Fe,xe,Te,qe,Se,Ie,ke,Ne],imports:[r.CommonModule,a.SharedModule,le],exports:[Fe,xe,Te,qe,Se,Ie,ke,Ne]})],e)}(),Ee=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.customerAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.customerAttributesConfigForm=this.fb.group({telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-enrichment-node-customer-attributes-config",template:'
\n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ae=function(e){function r(t,r,n){var a,o,l=e.call(this,t)||this;l.store=t,l.translate=r,l.fb=n,l.entityDetailsTranslationsMap=G,l.entityDetailsList=[],l.searchText="",l.displayDetailsFn=l.displayDetails.bind(l);try{for(var s=C(Object.keys(K)),m=s.next();!m.done;m=s.next()){var u=m.value;l.entityDetailsList.push(K[u])}}catch(e){a={error:e}}finally{try{m&&!m.done&&(o=s.return)&&o.call(s)}finally{if(a)throw a.error}}return l.detailsFormControl=new i.FormControl(""),l.filteredEntityDetails=l.detailsFormControl.valueChanges.pipe(f.startWith(""),f.map((function(e){return e||""})),f.mergeMap((function(e){return l.fetchEntityDetails(e)})),f.share()),l}return y(r,e),r.prototype.ngOnInit=function(){e.prototype.ngOnInit.call(this)},r.prototype.configForm=function(){return this.entityDetailsConfigForm},r.prototype.prepareInputConfig=function(e){return this.searchText="",this.detailsFormControl.patchValue("",{emitEvent:!0}),e},r.prototype.onConfigurationSet=function(e){this.entityDetailsConfigForm=this.fb.group({detailsList:[e?e.detailsList:null,[i.Validators.required]],addToMetadata:[!!e&&e.addToMetadata,[]]})},r.prototype.displayDetails=function(e){return e?this.translate.instant(G.get(e)):void 0},r.prototype.fetchEntityDetails=function(e){var t=this;if(this.searchText=e,this.searchText&&this.searchText.length){var r=this.searchText.toUpperCase();return c.of(this.entityDetailsList.filter((function(e){return t.translate.instant(G.get(K[e])).toUpperCase().includes(r)})))}return c.of(this.entityDetailsList)},r.prototype.detailsFieldSelected=function(e){this.addDetailsField(e.option.value),this.clear("")},r.prototype.removeDetailsField=function(e){var t=this.entityDetailsConfigForm.get("detailsList").value;if(t){var r=t.indexOf(e);r>=0&&(t.splice(r,1),this.entityDetailsConfigForm.get("detailsList").setValue(t))}},r.prototype.addDetailsField=function(e){var t=this.entityDetailsConfigForm.get("detailsList").value;t||(t=[]),-1===t.indexOf(e)&&(t.push(e),this.entityDetailsConfigForm.get("detailsList").setValue(t))},r.prototype.onEntityDetailsInputFocus=function(){this.detailsFormControl.updateValueAndValidity({onlySelf:!0,emitEvent:!0})},r.prototype.clear=function(e){var t=this;void 0===e&&(e=""),this.detailsInput.nativeElement.value=e,this.detailsFormControl.patchValue(null,{emitEvent:!0}),setTimeout((function(){t.detailsInput.nativeElement.blur(),t.detailsInput.nativeElement.focus()}),0)},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:i.FormBuilder}]},b([t.ViewChild("detailsInput",{static:!1}),h("design:type",t.ElementRef)],r.prototype,"detailsInput",void 0),r=b([t.Component({selector:"tb-enrichment-node-entity-details-config",template:'
\n \n tb.rulenode.entity-details\n \n \n \n {{entityDetailsTranslationsMap.get(details) | translate}}\n \n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-entity-details-matching\n
\n
\n
\n
\n
\n \n \n {{ \'tb.rulenode.add-to-metadata\' | translate }}\n \n
tb.rulenode.add-to-metadata-hint
\n
\n',styles:[":host ::ng-deep mat-form-field.entity-fields-list .mat-form-field-wrapper{margin-bottom:-1.25em}"]}),h("design:paramtypes",[o.Store,n.TranslateService,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Le=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[s.ENTER,s.COMMA,s.SEMICOLON],n}return y(r,e),r.prototype.configForm=function(){return this.deviceAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.deviceAttributesConfigForm=this.fb.group({deviceRelationsQuery:[e?e.deviceRelationsQuery:null,[i.Validators.required]],tellFailureIfAbsent:[!!e&&e.tellFailureIfAbsent,[]],clientAttributeNames:[e?e.clientAttributeNames:null,[]],sharedAttributeNames:[e?e.sharedAttributeNames:null,[]],serverAttributeNames:[e?e.serverAttributeNames:null,[]],latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],getLatestValueWithTs:[!!e&&e.getLatestValueWithTs,[]]})},r.prototype.removeKey=function(e,t){var r=this.deviceAttributesConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.deviceAttributesConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.deviceAttributesConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.deviceAttributesConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-enrichment-node-device-attributes-config",template:'
\n \n \n \n \n {{ \'tb.rulenode.tell-failure-if-absent\' | translate }}\n \n
tb.rulenode.tell-failure-if-absent-hint
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n {{ \'tb.rulenode.get-latest-value-with-ts\' | translate }}\n \n
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Me=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[s.ENTER,s.COMMA,s.SEMICOLON],n}return y(r,e),r.prototype.configForm=function(){return this.originatorAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.originatorAttributesConfigForm=this.fb.group({tellFailureIfAbsent:[!!e&&e.tellFailureIfAbsent,[]],clientAttributeNames:[e?e.clientAttributeNames:null,[]],sharedAttributeNames:[e?e.sharedAttributeNames:null,[]],serverAttributeNames:[e?e.serverAttributeNames:null,[]],latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],getLatestValueWithTs:[!!e&&e.getLatestValueWithTs,[]]})},r.prototype.removeKey=function(e,t){var r=this.originatorAttributesConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.originatorAttributesConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.originatorAttributesConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.originatorAttributesConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-enrichment-node-originator-attributes-config",template:'
\n \n {{ \'tb.rulenode.tell-failure-if-absent\' | translate }}\n \n
tb.rulenode.tell-failure-if-absent-hint
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n {{ \'tb.rulenode.get-latest-value-with-ts\' | translate }}\n \n
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Pe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.originatorFieldsConfigForm},r.prototype.onConfigurationSet=function(e){this.originatorFieldsConfigForm=this.fb.group({fieldsMapping:[e?e.fieldsMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-enrichment-node-originator-fields-config",template:'
\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Re=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[s.ENTER,s.COMMA,s.SEMICOLON],n.fetchMode=U,n.fetchModes=Object.keys(U),n.samplingOrders=Object.keys(j),n.timeUnits=Object.keys(R),n.timeUnitsTranslationMap=D,n}return y(r,e),r.prototype.configForm=function(){return this.getTelemetryFromDatabaseConfigForm},r.prototype.onConfigurationSet=function(e){this.getTelemetryFromDatabaseConfigForm=this.fb.group({latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],fetchMode:[e?e.fetchMode:null,[i.Validators.required]],orderBy:[e?e.orderBy:null,[]],limit:[e?e.limit:null,[]],useMetadataIntervalPatterns:[!!e&&e.useMetadataIntervalPatterns,[]],startInterval:[e?e.startInterval:null,[]],startIntervalTimeUnit:[e?e.startIntervalTimeUnit:null,[]],endInterval:[e?e.endInterval:null,[]],endIntervalTimeUnit:[e?e.endIntervalTimeUnit:null,[]],startIntervalPattern:[e?e.startIntervalPattern:null,[]],endIntervalPattern:[e?e.endIntervalPattern:null,[]]})},r.prototype.validatorTriggers=function(){return["fetchMode","useMetadataIntervalPatterns"]},r.prototype.updateValidators=function(e){var t=this.getTelemetryFromDatabaseConfigForm.get("fetchMode").value,r=this.getTelemetryFromDatabaseConfigForm.get("useMetadataIntervalPatterns").value;t&&t===U.ALL?(this.getTelemetryFromDatabaseConfigForm.get("orderBy").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("limit").setValidators([i.Validators.required,i.Validators.min(2),i.Validators.max(1e3)])):(this.getTelemetryFromDatabaseConfigForm.get("orderBy").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("limit").setValidators([])),r?(this.getTelemetryFromDatabaseConfigForm.get("startInterval").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endInterval").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").setValidators([i.Validators.required])):(this.getTelemetryFromDatabaseConfigForm.get("startInterval").setValidators([i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("endInterval").setValidators([i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").setValidators([])),this.getTelemetryFromDatabaseConfigForm.get("orderBy").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("limit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startInterval").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endInterval").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").updateValueAndValidity({emitEvent:e})},r.prototype.removeKey=function(e,t){var r=this.getTelemetryFromDatabaseConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.getTelemetryFromDatabaseConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.getTelemetryFromDatabaseConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.getTelemetryFromDatabaseConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-enrichment-node-get-telemetry-from-database",template:'
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n tb.rulenode.fetch-mode\n \n \n {{ mode }}\n \n \n tb.rulenode.fetch-mode-hint\n \n
\n \n tb.rulenode.order-by\n \n \n {{ order }}\n \n \n tb.rulenode.order-by-hint\n \n \n tb.rulenode.limit\n \n tb.rulenode.limit-hint\n \n
\n \n {{ \'tb.rulenode.use-metadata-interval-patterns\' | translate }}\n \n
tb.rulenode.use-metadata-interval-patterns-hint
\n
\n
\n \n tb.rulenode.start-interval\n \n \n {{ \'tb.rulenode.start-interval-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.start-interval-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n tb.rulenode.end-interval\n \n \n {{ \'tb.rulenode.end-interval-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.end-interval-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n \n tb.rulenode.start-interval-pattern\n \n \n {{ \'tb.rulenode.start-interval-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.end-interval-pattern\n \n \n {{ \'tb.rulenode.end-interval-pattern-required\' | translate }}\n \n \n \n \n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),we=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.relatedAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.relatedAttributesConfigForm=this.fb.group({relationsQuery:[e?e.relationsQuery:null,[i.Validators.required]],telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-enrichment-node-related-attributes-config",template:'
\n \n \n \n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Oe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.tenantAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.tenantAttributesConfigForm=this.fb.group({telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-enrichment-node-tenant-attributes-config",template:'
\n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),De=function(){function e(){}return e=b([t.NgModule({declarations:[Ee,Ae,Le,Me,Pe,Re,we,Oe],imports:[r.CommonModule,a.SharedModule,le],exports:[Ee,Ae,Le,Me,Pe,Re,we,Oe]})],e)}(),Ke=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.originatorSource=v,n.originatorSources=Object.keys(v),n.originatorSourceTranslationMap=P,n}return y(r,e),r.prototype.configForm=function(){return this.changeOriginatorConfigForm},r.prototype.onConfigurationSet=function(e){this.changeOriginatorConfigForm=this.fb.group({originatorSource:[e?e.originatorSource:null,[i.Validators.required]],relationsQuery:[e?e.relationsQuery:null,[]]})},r.prototype.validatorTriggers=function(){return["originatorSource"]},r.prototype.updateValidators=function(e){var t=this.changeOriginatorConfigForm.get("originatorSource").value;t&&t===v.RELATED?this.changeOriginatorConfigForm.get("relationsQuery").setValidators([i.Validators.required]):this.changeOriginatorConfigForm.get("relationsQuery").setValidators([]),this.changeOriginatorConfigForm.get("relationsQuery").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-transformation-node-change-originator-config",template:'
\n \n tb.rulenode.originator-source\n \n \n {{ originatorSourceTranslationMap.get(source) | translate }}\n \n \n \n
\n \n \n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Be=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return y(r,e),r.prototype.configForm=function(){return this.scriptConfigForm},r.prototype.onConfigurationSet=function(e){this.scriptConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.scriptConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"update",this.translate.instant("tb.rulenode.transformer"),"Transform",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.scriptConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:l.NodeScriptTestService},{type:n.TranslateService}]},b([t.ViewChild("jsFuncComponent",{static:!0}),h("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=b([t.Component({selector:"tb-transformation-node-script-config",template:'
\n \n \n \n
\n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder,l.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),Ue=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.toEmailConfigForm},r.prototype.onConfigurationSet=function(e){this.toEmailConfigForm=this.fb.group({fromTemplate:[e?e.fromTemplate:null,[i.Validators.required]],toTemplate:[e?e.toTemplate:null,[i.Validators.required]],ccTemplate:[e?e.ccTemplate:null,[]],bccTemplate:[e?e.bccTemplate:null,[]],subjectTemplate:[e?e.subjectTemplate:null,[i.Validators.required]],bodyTemplate:[e?e.bodyTemplate:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-transformation-node-to-email-config",template:'
\n \n tb.rulenode.from-template\n \n \n {{ \'tb.rulenode.from-template-required\' | translate }}\n \n \n \n \n tb.rulenode.to-template\n \n \n {{ \'tb.rulenode.to-template-required\' | translate }}\n \n \n \n \n tb.rulenode.cc-template\n \n \n \n \n tb.rulenode.bcc-template\n \n \n \n \n tb.rulenode.subject-template\n \n \n {{ \'tb.rulenode.subject-template-required\' | translate }}\n \n \n \n \n tb.rulenode.body-template\n \n \n {{ \'tb.rulenode.body-template-required\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),je=function(){function e(){}return e=b([t.NgModule({declarations:[Ke,Be,Ue],imports:[r.CommonModule,a.SharedModule,le],exports:[Ke,Be,Ue]})],e)}(),He=function(){function e(e){!function(e){e.setTranslation("en_US",{tb:{rulenode:{"create-entity-if-not-exists":"Create new entity if not exists","create-entity-if-not-exists-hint":"Create a new entity set above if it does not exist.","entity-name-pattern":"Name pattern","entity-name-pattern-required":"Name pattern is required","entity-name-pattern-hint":"Name pattern, use ${metaKeyName} to substitute variables from metadata","entity-type-pattern":"Type pattern","entity-type-pattern-required":"Type pattern is required","entity-type-pattern-hint":"Type pattern, use ${metaKeyName} to substitute variables from metadata","entity-cache-expiration":"Entities cache expiration time (sec)","entity-cache-expiration-hint":"Specifies maximum time interval allowed to store found entity records. 0 value means that records will never expire.","entity-cache-expiration-required":"Entities cache expiration time is required.","entity-cache-expiration-range":"Entities cache expiration time should be greater than or equal to 0.","customer-name-pattern":"Customer name pattern","customer-name-pattern-required":"Customer name pattern is required","create-customer-if-not-exists":"Create new customer if not exists","customer-cache-expiration":"Customers cache expiration time (sec)","customer-name-pattern-hint":"Customer name pattern, use ${metaKeyName} to substitute variables from metadata","customer-cache-expiration-hint":"Specifies maximum time interval allowed to store found customer records. 0 value means that records will never expire.","customer-cache-expiration-required":"Customers cache expiration time is required.","customer-cache-expiration-range":"Customers cache expiration time should be greater than or equal to 0.","start-interval":"Start Interval","end-interval":"End Interval","start-interval-time-unit":"Start Interval Time Unit","end-interval-time-unit":"End Interval Time Unit","fetch-mode":"Fetch mode","fetch-mode-hint":"If selected fetch mode 'ALL' you able to choose telemetry sampling order.","order-by":"Order by","order-by-hint":"Select to choose telemetry sampling order.",limit:"Limit","limit-hint":"Min limit value is 2, max - 1000. In case you want to fetch a single entry, select fetch mode 'FIRST' or 'LAST'.","time-unit-milliseconds":"Milliseconds","time-unit-seconds":"Seconds","time-unit-minutes":"Minutes","time-unit-hours":"Hours","time-unit-days":"Days","time-value-range":"Time value should be in a range from 1 to 2147483647.","start-interval-value-required":"Start interval value is required.","end-interval-value-required":"End interval value is required.",filter:"Filter",switch:"Switch","message-type":"Message type","message-type-required":"Message type is required.","message-types-filter":"Message types filter","no-message-types-found":"No message types found","no-message-type-matching":"'{{messageType}}' not found.","create-new-message-type":"Create a new one!","message-types-required":"Message types are required.","client-attributes":"Client attributes","client-attributes-hint":"Client attributes, use ${metaKeyName} to substitute variables from metadata","shared-attributes":"Shared attributes","shared-attributes-hint":"Shared attributes, use ${metaKeyName} to substitute variables from metadata","server-attributes":"Server attributes","server-attributes-hint":"Server attributes, use ${metaKeyName} to substitute variables from metadata","notify-device":"Notify Device","notify-device-hint":"If the message arrives from the device, we will push it back to the device by default.","latest-timeseries":"Latest timeseries","latest-timeseries-hint":"Latest timeseries, use ${metaKeyName} to substitute variables from metadata","data-keys":"Message data","metadata-keys":"Message metadata","relations-query":"Relations query","device-relations-query":"Device relations query","max-relation-level":"Max relation level","relation-type-pattern":"Relation type pattern","relation-type-pattern-hint":"Relation type pattern, use ${metaKeyName} to substitute variables from metadata","relation-type-pattern-required":"Relation type pattern is required","relation-types-list":"Relation types to propagate","relation-types-list-hint":"If Propagate relation types are not selected, alarms will be propagated without filtering by relation type.","unlimited-level":"Unlimited level","latest-telemetry":"Latest telemetry","attr-mapping":"Attributes mapping","source-attribute":"Source attribute","source-attribute-required":"Source attribute is required.","source-telemetry":"Source telemetry","source-telemetry-required":"Source telemetry is required.","target-attribute":"Target attribute","target-attribute-required":"Target attribute is required.","attr-mapping-required":"At least one attribute mapping should be specified.","fields-mapping":"Fields mapping","fields-mapping-required":"At least one field mapping should be specified.","source-field":"Source field","source-field-required":"Source field is required.","originator-source":"Originator source","originator-customer":"Customer","originator-tenant":"Tenant","originator-related":"Related","originator-alarm-originator":"Alarm Originator","clone-message":"Clone message",transform:"Transform","default-ttl":"Default TTL in seconds","default-ttl-required":"Default TTL is required.","min-default-ttl-message":"Only 0 minimum TTL is allowed.","message-count":"Message count (0 - unlimited)","message-count-required":"Message count is required.","min-message-count-message":"Only 0 minimum message count is allowed.","period-seconds":"Period in seconds","period-seconds-required":"Period is required.","use-metadata-period-in-seconds-patterns":"Use metadata period in seconds pattern","use-metadata-period-in-seconds-patterns-hint":"If selected, rule node use period in seconds interval pattern from message metadata assuming that intervals are in the seconds.","period-in-seconds-pattern":"Period in seconds metadata pattern","period-in-seconds-pattern-required":"Period in seconds pattern is required","period-in-seconds-pattern-hint":"Period in seconds pattern, use ${metaKeyName} to substitute variables from metadata","min-period-seconds-message":"Only 1 second minimum period is allowed.",originator:"Originator","message-body":"Message body","message-metadata":"Message metadata",generate:"Generate","test-generator-function":"Test generator function",generator:"Generator","test-filter-function":"Test filter function","test-switch-function":"Test switch function","test-transformer-function":"Test transformer function",transformer:"Transformer","alarm-create-condition":"Alarm create condition","test-condition-function":"Test condition function","alarm-clear-condition":"Alarm clear condition","alarm-details-builder":"Alarm details builder","test-details-function":"Test details function","alarm-type":"Alarm type","alarm-type-required":"Alarm type is required.","alarm-severity":"Alarm severity","alarm-severity-required":"Alarm severity is required","alarm-status-filter":"Alarm status filter","alarm-status-list-empty":"Alarm status list is empty","no-alarm-status-matching":"No alarm status matching were found.",propagate:"Propagate",condition:"Condition",details:"Details","to-string":"To string","test-to-string-function":"Test to string function","from-template":"From Template","from-template-required":"From Template is required","from-template-hint":"From address template, use ${metaKeyName} to substitute variables from metadata","to-template":"To Template","to-template-required":"To Template is required","mail-address-list-template-hint":"Comma separated address list, use ${metaKeyName} to substitute variables from metadata","cc-template":"Cc Template","bcc-template":"Bcc Template","subject-template":"Subject Template","subject-template-required":"Subject Template is required","subject-template-hint":"Mail subject template, use ${metaKeyName} to substitute variables from metadata","body-template":"Body Template","body-template-required":"Body Template is required","body-template-hint":"Mail body template, use ${metaKeyName} to substitute variables from metadata","request-id-metadata-attribute":"Request Id Metadata attribute name","timeout-sec":"Timeout in seconds","timeout-required":"Timeout is required","min-timeout-message":"Only 0 minimum timeout value is allowed.","endpoint-url-pattern":"Endpoint URL pattern","endpoint-url-pattern-required":"Endpoint URL pattern is required","endpoint-url-pattern-hint":"HTTP URL address pattern, use ${metaKeyName} to substitute variables from metadata","request-method":"Request method","use-simple-client-http-factory":"Use simple client HTTP factory","read-timeout":"Read timeout in millis","read-timeout-hint":"The value of 0 means an infinite timeout","max-parallel-requests-count":"Max number of parallel requests","max-parallel-requests-count-hint":"The value of 0 specifies no limit in parallel processing",headers:"Headers","headers-hint":"Use ${metaKeyName} in header/value fields to substitute variables from metadata",header:"Header","header-required":"Header is required",value:"Value","value-required":"Value is required","topic-pattern":"Topic pattern","topic-pattern-required":"Topic pattern is required","mqtt-topic-pattern-hint":"MQTT topic pattern, use ${metaKeyName} to substitute variables from metadata",topic:"Topic","topic-required":"Topic is required","bootstrap-servers":"Bootstrap servers","bootstrap-servers-required":"Bootstrap servers value is required","other-properties":"Other properties",key:"Key","key-required":"Key is required",retries:"Automatically retry times if fails","min-retries-message":"Only 0 minimum retries is allowed.","batch-size-bytes":"Produces batch size in bytes","min-batch-size-bytes-message":"Only 0 minimum batch size is allowed.","linger-ms":"Time to buffer locally (ms)","min-linger-ms-message":"Only 0 ms minimum value is allowed.","buffer-memory-bytes":"Client buffer max size in bytes","min-buffer-memory-message":"Only 0 minimum buffer size is allowed.",acks:"Number of acknowledgments","key-serializer":"Key serializer","key-serializer-required":"Key serializer is required","value-serializer":"Value serializer","value-serializer-required":"Value serializer is required","topic-arn-pattern":"Topic ARN pattern","topic-arn-pattern-required":"Topic ARN pattern is required","topic-arn-pattern-hint":"Topic ARN pattern, use ${metaKeyName} to substitute variables from metadata","aws-access-key-id":"AWS Access Key ID","aws-access-key-id-required":"AWS Access Key ID is required","aws-secret-access-key":"AWS Secret Access Key","aws-secret-access-key-required":"AWS Secret Access Key is required","aws-region":"AWS Region","aws-region-required":"AWS Region is required","exchange-name-pattern":"Exchange name pattern","routing-key-pattern":"Routing key pattern","message-properties":"Message properties",host:"Host","host-required":"Host is required",port:"Port","port-required":"Port is required","port-range":"Port should be in a range from 1 to 65535.","virtual-host":"Virtual host",username:"Username",password:"Password","automatic-recovery":"Automatic recovery","connection-timeout-ms":"Connection timeout (ms)","min-connection-timeout-ms-message":"Only 0 ms minimum value is allowed.","handshake-timeout-ms":"Handshake timeout (ms)","min-handshake-timeout-ms-message":"Only 0 ms minimum value is allowed.","client-properties":"Client properties","queue-url-pattern":"Queue URL pattern","queue-url-pattern-required":"Queue URL pattern is required","queue-url-pattern-hint":"Queue URL pattern, use ${metaKeyName} to substitute variables from metadata","delay-seconds":"Delay (seconds)","min-delay-seconds-message":"Only 0 seconds minimum value is allowed.","max-delay-seconds-message":"Only 900 seconds maximum value is allowed.",name:"Name","name-required":"Name is required","queue-type":"Queue type","sqs-queue-standard":"Standard","sqs-queue-fifo":"FIFO","gcp-project-id":"GCP project ID","gcp-project-id-required":"GCP project ID is required","gcp-service-account-key":"GCP service account key file","gcp-service-account-key-required":"GCP service account key file is required","pubsub-topic-name":"Topic name","pubsub-topic-name-required":"Topic name is required","message-attributes":"Message attributes","message-attributes-hint":"Use ${metaKeyName} in name/value fields to substitute variables from metadata","connect-timeout":"Connection timeout (sec)","connect-timeout-required":"Connection timeout is required.","connect-timeout-range":"Connection timeout should be in a range from 1 to 200.","client-id":"Client ID","device-id":"Device ID","device-id-required":"Device ID is required.","clean-session":"Clean session","enable-ssl":"Enable SSL",credentials:"Credentials","credentials-type":"Credentials type","credentials-type-required":"Credentials type is required.","credentials-anonymous":"Anonymous","credentials-basic":"Basic","credentials-pem":"PEM","credentials-sas":"Shared Access Signature","sas-key":"SAS Key","sas-key-required":"SAS Key is required.",hostname:"Hostname","hostname-required":"Hostname is required.","azure-ca-cert":"CA certificate file","username-required":"Username is required.","password-required":"Password is required.","ca-cert":"CA certificate file *","private-key":"Private key file *",cert:"Certificate file *","no-file":"No file selected.","drop-file":"Drop a file or click to select a file to upload.","private-key-password":"Private key password","use-system-smtp-settings":"Use system SMTP settings","use-metadata-interval-patterns":"Use metadata interval patterns","use-metadata-interval-patterns-hint":"If selected, rule node use start and end interval patterns from message metadata assuming that intervals are in the milliseconds.","use-message-alarm-data":"Use message alarm data","check-all-keys":"Check that all selected keys are present","check-all-keys-hint":"If selected, checks that all specified keys are present in the message data and metadata.","check-relation-to-specific-entity":"Check relation to specific entity","check-relation-hint":"Checks existence of relation to specific entity or to any entity based on direction and relation type.","delete-relation-to-specific-entity":"Delete relation to specific entity","delete-relation-hint":"Deletes relation from the originator of the incoming message to the specified entity or list of entities based on direction and type.","remove-current-relations":"Remove current relations","remove-current-relations-hint":"Removes current relations from the originator of the incoming message based on direction and type.","change-originator-to-related-entity":"Change originator to related entity","change-originator-to-related-entity-hint":"Used to process submitted message as a message from another entity.","start-interval-pattern":"Start interval pattern","end-interval-pattern":"End interval pattern","start-interval-pattern-required":"Start interval pattern is required","end-interval-pattern-required":"End interval pattern is required","start-interval-pattern-hint":"Start interval pattern, use ${metaKeyName} to substitute variables from metadata","end-interval-pattern-hint":"End interval pattern, use ${metaKeyName} to substitute variables from metadata","smtp-protocol":"Protocol","smtp-host":"SMTP host","smtp-host-required":"SMTP host is required.","smtp-port":"SMTP port","smtp-port-required":"You must supply a smtp port.","smtp-port-range":"SMTP port should be in a range from 1 to 65535.","timeout-msec":"Timeout ms","min-timeout-msec-message":"Only 0 ms minimum value is allowed.","enter-username":"Enter username","enter-password":"Enter password","enable-tls":"Enable TLS","tls-version":"TLS version","enable-proxy":"Enable proxy","use-system-proxy-properties":"Use system proxy properties","proxy-host":"Proxy host","proxy-host-required":"Proxy host is required.","proxy-port":"Proxy port","proxy-port-required":"Proxy port is required.","proxy-port-range":"Proxy port should be in a range from 1 to 65535.","proxy-user":"Proxy user","proxy-password":"Proxy password","proxy-scheme":"Proxy scheme","min-period-0-seconds-message":"Only 0 second minimum period is allowed.","max-pending-messages":"Maximum pending messages","max-pending-messages-required":"Maximum pending messages is required.","max-pending-messages-range":"Maximum pending messages should be in a range from 1 to 100000.","originator-types-filter":"Originator types filter","interval-seconds":"Interval in seconds","interval-seconds-required":"Interval is required.","min-interval-seconds-message":"Only 1 second minimum interval is allowed.","output-timeseries-key-prefix":"Output timeseries key prefix","output-timeseries-key-prefix-required":"Output timeseries key prefix required.","separator-hint":'You should press "enter" to complete field input.',"entity-details":"Select entity details:","entity-details-title":"Title","entity-details-country":"Country","entity-details-state":"State","entity-details-zip":"Zip","entity-details-address":"Address","entity-details-address2":"Address2","entity-details-additional_info":"Additional Info","entity-details-phone":"Phone","entity-details-email":"Email","add-to-metadata":"Add selected details to message metadata","add-to-metadata-hint":"If selected, adds the selected details keys to the message metadata instead of message data.","entity-details-list-empty":"No entity details selected.","no-entity-details-matching":"No entity details matching were found.","custom-table-name":"Custom table name","custom-table-name-required":"Table Name is required","custom-table-hint":"You should enter the table name without prefix 'cs_tb_'.","message-field":"Message field","message-field-required":"Message field is required.","table-col":"Table column","table-col-required":"Table column is required.","latitude-key-name":"Latitude key name","longitude-key-name":"Longitude key name","latitude-key-name-required":"Latitude key name is required.","longitude-key-name-required":"Longitude key name is required.","fetch-perimeter-info-from-message-metadata":"Fetch perimeter information from message metadata","perimeter-circle":"Circle","perimeter-polygon":"Polygon","perimeter-type":"Perimeter type","circle-center-latitude":"Center latitude","circle-center-latitude-required":"Center latitude is required.","circle-center-longitude":"Center longitude","circle-center-longitude-required":"Center longitude is required.","range-unit-meter":"Meter","range-unit-kilometer":"Kilometer","range-unit-foot":"Foot","range-unit-mile":"Mile","range-unit-nautical-mile":"Nautical mile","range-units":"Range units",range:"Range","range-required":"Range is required.","polygon-definition":"Polygon definition","polygon-definition-required":"Polygon definition is required.","polygon-definition-hint":"Please, use the following format for manual definition of polygon: [[lat1,lon1],[lat2,lon2], ... ,[latN,lonN]].","min-inside-duration":"Minimal inside duration","min-inside-duration-value-required":"Minimal inside duration is required","min-inside-duration-time-unit":"Minimal inside duration time unit","min-outside-duration":"Minimal outside duration","min-outside-duration-value-required":"Minimal outside duration is required","min-outside-duration-time-unit":"Minimal outside duration time unit","tell-failure-if-absent":"Tell Failure","tell-failure-if-absent-hint":'If at least one selected key doesn\'t exist the outbound message will report "Failure".',"get-latest-value-with-ts":"Fetch Latest telemetry with Timestamp","get-latest-value-with-ts-hint":'If selected, latest telemetry values will be added to the outbound message metadata with timestamp, e.g: "temp": "{\\"ts\\":1574329385897,\\"value\\":42}"',"use-redis-queue":"Use redis queue for message persistence","trim-redis-queue":"Trim redis queue","redis-queue-max-size":"Redis queue max size","add-metadata-key-values-as-kafka-headers":"Add Message metadata key-value pairs to Kafka record headers","add-metadata-key-values-as-kafka-headers-hint":"If selected, key-value pairs from message metadata will be added to the outgoing records headers as byte arrays with predefined charset encoding.","charset-encoding":"Charset encoding","charset-encoding-required":"Charset encoding is required.","charset-us-ascii":"US-ASCII","charset-iso-8859-1":"ISO-8859-1","charset-utf-8":"UTF-8","charset-utf-16be":"UTF-16BE","charset-utf-16le":"UTF-16LE","charset-utf-16":"UTF-16","select-queue-hint":"The queue name can be selected from a drop-down list or add a custom name.","persist-alarm-rules":"Persist state of alarm rules","fetch-alarm-rules":"Fetch state of alarm rules"},"key-val":{key:"Key",value:"Value","remove-entry":"Remove entry","add-entry":"Add entry"}}},!0)}(e)}return e.ctorParameters=function(){return[{type:n.TranslateService}]},e=b([t.NgModule({declarations:[F],imports:[r.CommonModule,a.SharedModule],exports:[ve,Ve,De,je,F]}),h("design:paramtypes",[n.TranslateService])],e)}();e.RuleNodeCoreConfigModule=He,e.ɵa=F,e.ɵb=ve,e.ɵba=be,e.ɵbb=he,e.ɵbc=Ce,e.ɵbd=le,e.ɵbe=ne,e.ɵbf=ae,e.ɵbg=oe,e.ɵbh=ie,e.ɵbi=Ve,e.ɵbj=Fe,e.ɵbk=xe,e.ɵbl=Te,e.ɵbm=qe,e.ɵbn=Se,e.ɵbo=Ie,e.ɵbp=ke,e.ɵbq=Ne,e.ɵbr=De,e.ɵbs=Ee,e.ɵbt=Ae,e.ɵbu=Le,e.ɵbv=Me,e.ɵbw=Pe,e.ɵbx=Re,e.ɵby=we,e.ɵbz=Oe,e.ɵc=x,e.ɵca=je,e.ɵcb=Ke,e.ɵcc=Be,e.ɵcd=Ue,e.ɵd=T,e.ɵe=q,e.ɵf=S,e.ɵg=I,e.ɵh=k,e.ɵi=N,e.ɵj=V,e.ɵk=E,e.ɵl=A,e.ɵm=L,e.ɵn=X,e.ɵo=ee,e.ɵp=te,e.ɵq=re,e.ɵr=se,e.ɵs=me,e.ɵt=ue,e.ɵu=de,e.ɵv=pe,e.ɵw=ce,e.ɵx=fe,e.ɵy=ge,e.ɵz=ye,Object.defineProperty(e,"__esModule",{value:!0})})); +//# sourceMappingURL=rulenode-core-config.umd.min.js.map \ No newline at end of file diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java index ba6a98afb1..fa2eeb7ba3 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java @@ -15,7 +15,7 @@ */ package org.thingsboard.rule.engine.action; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.util.concurrent.Futures; @@ -30,6 +30,7 @@ import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.mockito.stubbing.Answer; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.api.RuleEngineAlarmService; import org.thingsboard.rule.engine.api.ScriptEngine; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -45,7 +46,6 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.alarm.AlarmService; import javax.script.ScriptException; import java.io.IOException; @@ -64,9 +64,9 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; -import static org.thingsboard.rule.engine.action.TbAbstractAlarmNode.IS_CLEARED_ALARM; -import static org.thingsboard.rule.engine.action.TbAbstractAlarmNode.IS_EXISTING_ALARM; -import static org.thingsboard.rule.engine.action.TbAbstractAlarmNode.IS_NEW_ALARM; +import static org.thingsboard.server.common.data.DataConstants.IS_CLEARED_ALARM; +import static org.thingsboard.server.common.data.DataConstants.IS_EXISTING_ALARM; +import static org.thingsboard.server.common.data.DataConstants.IS_NEW_ALARM; import static org.thingsboard.server.common.data.alarm.AlarmSeverity.CRITICAL; import static org.thingsboard.server.common.data.alarm.AlarmSeverity.WARNING; import static org.thingsboard.server.common.data.alarm.AlarmStatus.ACTIVE_UNACK; @@ -81,7 +81,7 @@ public class TbAlarmNodeTest { @Mock private TbContext ctx; @Mock - private AlarmService alarmService; + private RuleEngineAlarmService alarmService; @Mock private ScriptEngine detailsJs; @@ -91,14 +91,14 @@ public class TbAlarmNodeTest { @Captor private ArgumentCaptor> failureCaptor; - private RuleChainId ruleChainId = new RuleChainId(UUIDs.timeBased()); - private RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased()); + private RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); + private RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); private ListeningExecutor dbExecutor; - private EntityId originator = new DeviceId(UUIDs.timeBased()); - private EntityId alarmOriginator = new AlarmId(UUIDs.timeBased()); - private TenantId tenantId = new TenantId(UUIDs.timeBased()); + private EntityId originator = new DeviceId(Uuids.timeBased()); + private EntityId alarmOriginator = new AlarmId(Uuids.timeBased()); + private TenantId tenantId = new TenantId(Uuids.timeBased()); private TbMsgMetaData metaData = new TbMsgMetaData(); private String rawJson = "{\"name\": \"Vit\", \"passed\": 5}"; @@ -294,7 +294,8 @@ public class TbAlarmNodeTest { when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(activeAlarm)); - when(alarmService.clearAlarm(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()), org.mockito.Mockito.any(JsonNode.class), anyLong())).thenReturn(Futures.immediateFuture(true)); + when(alarmService.clearAlarm(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()), org.mockito.Mockito.any(JsonNode.class), anyLong())) + .thenReturn(Futures.immediateFuture( false)); when(alarmService.findAlarmByIdAsync(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()))).thenReturn(Futures.immediateFuture(activeAlarm)); // doAnswer((Answer) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(activeAlarm); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java index b0f64fb980..2b71d5d14f 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java @@ -15,7 +15,7 @@ */ package org.thingsboard.rule.engine.filter; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -24,6 +24,7 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Matchers; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.runners.MockitoJUnitRunner; import org.mockito.stubbing.Answer; import org.thingsboard.common.util.ListeningExecutor; @@ -58,8 +59,8 @@ public class TbJsFilterNodeTest { @Mock private ScriptEngine scriptEngine; - private RuleChainId ruleChainId = new RuleChainId(UUIDs.timeBased()); - private RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased()); + private RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); + private RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); @Test public void falseEvaluationDoNotSendMsg() throws TbNodeException, ScriptException { @@ -90,7 +91,7 @@ public class TbJsFilterNodeTest { public void metadataConditionCanBeTrue() throws TbNodeException, ScriptException { initWithScript(); TbMsgMetaData metaData = new TbMsgMetaData(); - TbMsg msg = TbMsg.newMsg( "USER", null, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg("USER", null, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); mockJsExecutor(); when(scriptEngine.executeFilterAsync(msg)).thenReturn(Futures.immediateFuture(true)); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java index 9286a06da6..960b1ec0c4 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java @@ -15,7 +15,7 @@ */ package org.thingsboard.rule.engine.filter; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.Sets; import com.google.common.util.concurrent.Futures; @@ -60,8 +60,8 @@ public class TbJsSwitchNodeTest { @Mock private ScriptEngine scriptEngine; - private RuleChainId ruleChainId = new RuleChainId(UUIDs.timeBased()); - private RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased()); + private RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); + private RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); @Test public void multipleRoutesAreAllowed() throws TbNodeException, ScriptException { diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java index 70f58c5989..137a2817c6 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java @@ -15,7 +15,7 @@ */ package org.thingsboard.rule.engine.mail; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.Test; import org.junit.runner.RunWith; @@ -47,12 +47,12 @@ public class TbMsgToEmailNodeTest { @Mock private TbContext ctx; - private EntityId originator = new DeviceId(UUIDs.timeBased()); + private EntityId originator = new DeviceId(Uuids.timeBased()); private TbMsgMetaData metaData = new TbMsgMetaData(); private String rawJson = "{\"name\": \"temp\", \"passed\": 5 , \"complex\": {\"val\":12, \"count\":100}}"; - private RuleChainId ruleChainId = new RuleChainId(UUIDs.timeBased()); - private RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased()); + private RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); + private RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); @Test public void msgCanBeConverted() throws IOException { @@ -107,4 +107,4 @@ public class TbMsgToEmailNodeTest { } } -} \ No newline at end of file +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java index a0a61ae30f..ffd7c2439e 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java @@ -15,7 +15,7 @@ */ package org.thingsboard.rule.engine.metadata; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.Lists; import com.google.common.util.concurrent.Futures; @@ -88,8 +88,8 @@ public class TbGetCustomerAttributeNodeTest { private TbMsg msg; - private RuleChainId ruleChainId = new RuleChainId(UUIDs.timeBased()); - private RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased()); + private RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); + private RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); @Before public void init() throws TbNodeException { @@ -107,8 +107,8 @@ public class TbGetCustomerAttributeNodeTest { @Test public void errorThrownIfCannotLoadAttributes() { - UserId userId = new UserId(UUIDs.timeBased()); - CustomerId customerId = new CustomerId(UUIDs.timeBased()); + UserId userId = new UserId(Uuids.timeBased()); + CustomerId customerId = new CustomerId(Uuids.timeBased()); User user = new User(); user.setCustomerId(customerId); @@ -132,8 +132,8 @@ public class TbGetCustomerAttributeNodeTest { @Test public void errorThrownIfCannotLoadAttributesAsync() { - UserId userId = new UserId(UUIDs.timeBased()); - CustomerId customerId = new CustomerId(UUIDs.timeBased()); + UserId userId = new UserId(Uuids.timeBased()); + CustomerId customerId = new CustomerId(Uuids.timeBased()); User user = new User(); user.setCustomerId(customerId); @@ -157,8 +157,8 @@ public class TbGetCustomerAttributeNodeTest { @Test public void failedChainUsedIfCustomerCannotBeFound() { - UserId userId = new UserId(UUIDs.timeBased()); - CustomerId customerId = new CustomerId(UUIDs.timeBased()); + UserId userId = new UserId(Uuids.timeBased()); + CustomerId customerId = new CustomerId(Uuids.timeBased()); User user = new User(); user.setCustomerId(customerId); @@ -175,15 +175,15 @@ public class TbGetCustomerAttributeNodeTest { @Test public void customerAttributeAddedInMetadata() { - CustomerId customerId = new CustomerId(UUIDs.timeBased()); + CustomerId customerId = new CustomerId(Uuids.timeBased()); msg = TbMsg.newMsg( "CUSTOMER", customerId, new TbMsgMetaData(), TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); entityAttributeFetched(customerId); } @Test public void usersCustomerAttributesFetched() { - UserId userId = new UserId(UUIDs.timeBased()); - CustomerId customerId = new CustomerId(UUIDs.timeBased()); + UserId userId = new UserId(Uuids.timeBased()); + CustomerId customerId = new CustomerId(Uuids.timeBased()); User user = new User(); user.setCustomerId(customerId); @@ -197,8 +197,8 @@ public class TbGetCustomerAttributeNodeTest { @Test public void assetsCustomerAttributesFetched() { - AssetId assetId = new AssetId(UUIDs.timeBased()); - CustomerId customerId = new CustomerId(UUIDs.timeBased()); + AssetId assetId = new AssetId(Uuids.timeBased()); + CustomerId customerId = new CustomerId(Uuids.timeBased()); Asset asset = new Asset(); asset.setCustomerId(customerId); @@ -212,8 +212,8 @@ public class TbGetCustomerAttributeNodeTest { @Test public void deviceCustomerAttributesFetched() { - DeviceId deviceId = new DeviceId(UUIDs.timeBased()); - CustomerId customerId = new CustomerId(UUIDs.timeBased()); + DeviceId deviceId = new DeviceId(Uuids.timeBased()); + CustomerId customerId = new CustomerId(Uuids.timeBased()); Device device = new Device(); device.setCustomerId(customerId); @@ -239,8 +239,8 @@ public class TbGetCustomerAttributeNodeTest { node.init(null, nodeConfiguration); - DeviceId deviceId = new DeviceId(UUIDs.timeBased()); - CustomerId customerId = new CustomerId(UUIDs.timeBased()); + DeviceId deviceId = new DeviceId(Uuids.timeBased()); + CustomerId customerId = new CustomerId(Uuids.timeBased()); Device device = new Device(); device.setCustomerId(customerId); @@ -271,4 +271,4 @@ public class TbGetCustomerAttributeNodeTest { verify(ctx).tellSuccess(msg); assertEquals(msg.getMetaData().getValue("tempo"), "high"); } -} \ No newline at end of file +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java new file mode 100644 index 0000000000..ae6bd7ebbd --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java @@ -0,0 +1,201 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.profile; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.Futures; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.AdditionalAnswers; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; +import org.springframework.util.StringUtils; +import org.thingsboard.rule.engine.api.RuleEngineAlarmService; +import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.device.profile.AlarmCondition; +import org.thingsboard.server.common.data.device.profile.AlarmRule; +import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityKeyValueType; +import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.NumericFilterPredicate; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgDataType; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.session.SessionMsgType; +import org.thingsboard.server.dao.alarm.AlarmService; +import org.thingsboard.server.dao.timeseries.TimeseriesService; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.UUID; + +import static org.mockito.Mockito.verify; + +@RunWith(MockitoJUnitRunner.class) +public class TbDeviceProfileNodeTest { + + private static final ObjectMapper mapper = new ObjectMapper(); + + private TbDeviceProfileNode node; + + @Mock + private TbContext ctx; + @Mock + private RuleEngineDeviceProfileCache cache; + @Mock + private TimeseriesService timeseriesService; + @Mock + private RuleEngineAlarmService alarmService; + + private TenantId tenantId = new TenantId(UUID.randomUUID()); + private DeviceId deviceId = new DeviceId(UUID.randomUUID()); + private DeviceProfileId deviceProfileId = new DeviceProfileId(UUID.randomUUID()); + + @Test + public void testRandomMessageType() throws Exception { + init(); + + DeviceProfile deviceProfile = new DeviceProfile(); + DeviceProfileData deviceProfileData = new DeviceProfileData(); + deviceProfileData.setAlarms(Collections.emptyList()); + deviceProfile.setProfileData(deviceProfileData); + + Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + ObjectNode data = mapper.createObjectNode(); + data.put("temperature", 42); + TbMsg msg = TbMsg.newMsg("123456789", deviceId, new TbMsgMetaData(), + TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); + node.onMsg(ctx, msg); + verify(ctx).tellSuccess(msg); + verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); + } + + @Test + public void testEmptyProfile() throws Exception { + init(); + + DeviceProfile deviceProfile = new DeviceProfile(); + DeviceProfileData deviceProfileData = new DeviceProfileData(); + deviceProfileData.setAlarms(Collections.emptyList()); + deviceProfile.setProfileData(deviceProfileData); + + Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + ObjectNode data = mapper.createObjectNode(); + data.put("temperature", 42); + TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); + node.onMsg(ctx, msg); + verify(ctx).tellSuccess(msg); + verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); + } + + @Test + public void testAlarmCreate() throws Exception { + init(); + + DeviceProfile deviceProfile = new DeviceProfile(); + DeviceProfileData deviceProfileData = new DeviceProfileData(); + + KeyFilter highTempFilter = new KeyFilter(); + highTempFilter.setKey(new EntityKey(EntityKeyType.TIME_SERIES, "temperature")); + highTempFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate highTemperaturePredicate = new NumericFilterPredicate(); + highTemperaturePredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + highTemperaturePredicate.setValue(new FilterPredicateValue<>(30.0)); + highTempFilter.setPredicate(highTemperaturePredicate); + AlarmCondition alarmCondition = new AlarmCondition(); + alarmCondition.setCondition(Collections.singletonList(highTempFilter)); + AlarmRule alarmRule = new AlarmRule(); + alarmRule.setCondition(alarmCondition); + DeviceProfileAlarm dpa = new DeviceProfileAlarm(); + dpa.setId("highTemperatureAlarmID"); + dpa.setAlarmType("highTemperatureAlarm"); + dpa.setCreateRules(new LinkedHashMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + + KeyFilter lowTempFilter = new KeyFilter(); + lowTempFilter.setKey(new EntityKey(EntityKeyType.TIME_SERIES, "temperature")); + lowTempFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate lowTemperaturePredicate = new NumericFilterPredicate(); + lowTemperaturePredicate.setOperation(NumericFilterPredicate.NumericOperation.LESS); + lowTemperaturePredicate.setValue(new FilterPredicateValue<>(10.0)); + lowTempFilter.setPredicate(lowTemperaturePredicate); + AlarmRule clearRule = new AlarmRule(); + AlarmCondition clearCondition = new AlarmCondition(); + clearCondition.setCondition(Collections.singletonList(lowTempFilter)); + clearRule.setCondition(clearCondition); + dpa.setClearRule(clearRule); + + deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfile.setProfileData(deviceProfileData); + + Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) + .thenReturn(Futures.immediateFuture(Collections.emptyList())); + Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")).thenReturn(Futures.immediateFuture(null)); + Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); + + TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); + Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.anyString())).thenReturn(theMsg); + + ObjectNode data = mapper.createObjectNode(); + data.put("temperature", 42); + TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); + node.onMsg(ctx, msg); + verify(ctx).tellSuccess(msg); + verify(ctx).tellNext(theMsg, "Alarm Created"); + verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); + + TbMsg theMsg2 = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), "2"); + Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.anyString())).thenReturn(theMsg2); + + + TbMsg msg2 = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); + node.onMsg(ctx, msg2); + verify(ctx).tellSuccess(msg2); + verify(ctx).tellNext(theMsg2, "Alarm Updated"); + + } + + private void init() throws TbNodeException { + Mockito.when(ctx.getTenantId()).thenReturn(tenantId); + Mockito.when(ctx.getDeviceProfileCache()).thenReturn(cache); + Mockito.when(ctx.getTimeseriesService()).thenReturn(timeseriesService); + Mockito.when(ctx.getAlarmService()).thenReturn(alarmService); + TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.createObjectNode()); + node = new TbDeviceProfileNode(); + node.init(ctx, nodeConfiguration); + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java index fa845c4a18..a8226f2253 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java @@ -15,7 +15,7 @@ */ package org.thingsboard.rule.engine.transform; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -84,13 +84,13 @@ public class TbChangeOriginatorNodeTest { @Test public void originatorCanBeChangedToCustomerId() throws TbNodeException { init(); - AssetId assetId = new AssetId(UUIDs.timeBased()); - CustomerId customerId = new CustomerId(UUIDs.timeBased()); + AssetId assetId = new AssetId(Uuids.timeBased()); + CustomerId customerId = new CustomerId(Uuids.timeBased()); Asset asset = new Asset(); asset.setCustomerId(customerId); - RuleChainId ruleChainId = new RuleChainId(UUIDs.timeBased()); - RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased()); + RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); + RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); TbMsg msg = TbMsg.newMsg( "ASSET", assetId, new TbMsgMetaData(), TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); @@ -112,13 +112,13 @@ public class TbChangeOriginatorNodeTest { @Test public void newChainCanBeStarted() throws TbNodeException { init(); - AssetId assetId = new AssetId(UUIDs.timeBased()); - CustomerId customerId = new CustomerId(UUIDs.timeBased()); + AssetId assetId = new AssetId(Uuids.timeBased()); + CustomerId customerId = new CustomerId(Uuids.timeBased()); Asset asset = new Asset(); asset.setCustomerId(customerId); - RuleChainId ruleChainId = new RuleChainId(UUIDs.timeBased()); - RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased()); + RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); + RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); TbMsg msg = TbMsg.newMsg( "ASSET", assetId, new TbMsgMetaData(), TbMsgDataType.JSON,"{}", ruleChainId, ruleNodeId); @@ -139,13 +139,13 @@ public class TbChangeOriginatorNodeTest { @Test public void exceptionThrownIfCannotFindNewOriginator() throws TbNodeException { init(); - AssetId assetId = new AssetId(UUIDs.timeBased()); - CustomerId customerId = new CustomerId(UUIDs.timeBased()); + AssetId assetId = new AssetId(Uuids.timeBased()); + CustomerId customerId = new CustomerId(Uuids.timeBased()); Asset asset = new Asset(); asset.setCustomerId(customerId); - RuleChainId ruleChainId = new RuleChainId(UUIDs.timeBased()); - RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased()); + RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); + RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); TbMsg msg = TbMsg.newMsg( "ASSET", assetId, new TbMsgMetaData(), TbMsgDataType.JSON,"{}", ruleChainId, ruleNodeId); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java index a00e97cf1c..bc56815468 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java @@ -15,7 +15,7 @@ */ package org.thingsboard.rule.engine.transform; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -67,8 +67,8 @@ public class TbTransformMsgNodeTest { metaData.putValue("temp", "7"); String rawJson = "{\"passed\": 5}"; - RuleChainId ruleChainId = new RuleChainId(UUIDs.timeBased()); - RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased()); + RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); + RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); TbMsg msg = TbMsg.newMsg( "USER", null, metaData, TbMsgDataType.JSON,rawJson, ruleChainId, ruleNodeId); TbMsg transformedMsg = TbMsg.newMsg( "USER", null, metaData, TbMsgDataType.JSON, "{new}", ruleChainId, ruleNodeId); mockJsExecutor(); @@ -89,8 +89,8 @@ public class TbTransformMsgNodeTest { metaData.putValue("temp", "7"); String rawJson = "{\"passed\": 5"; - RuleChainId ruleChainId = new RuleChainId(UUIDs.timeBased()); - RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased()); + RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); + RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); TbMsg msg = TbMsg.newMsg( "USER", null, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); mockJsExecutor(); when(scriptEngine.executeUpdateAsync(msg)).thenReturn(Futures.immediateFailedFuture(new IllegalStateException("error"))); diff --git a/tools/pom.xml b/tools/pom.xml index 91bf319b0e..8d596c8493 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT thingsboard tools @@ -57,8 +57,8 @@ 3.11.6 - com.datastax.cassandra - cassandra-driver-core + com.datastax.oss + java-driver-core commons-io diff --git a/tools/src/main/java/org/thingsboard/client/tools/MqttSslClient.java b/tools/src/main/java/org/thingsboard/client/tools/MqttSslClient.java index 38d28d684b..cb810ac59a 100644 --- a/tools/src/main/java/org/thingsboard/client/tools/MqttSslClient.java +++ b/tools/src/main/java/org/thingsboard/client/tools/MqttSslClient.java @@ -25,6 +25,7 @@ import lombok.extern.slf4j.Slf4j; import org.eclipse.paho.client.mqttv3.MqttAsyncClient; import org.eclipse.paho.client.mqttv3.MqttConnectOptions; import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; import javax.net.ssl.*; import java.io.File; @@ -71,7 +72,7 @@ public class MqttSslClient { MqttConnectOptions options = new MqttConnectOptions(); options.setSocketFactory(sslContext.getSocketFactory()); - MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, CLIENT_ID); + MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, CLIENT_ID, new MemoryPersistence()); client.connect(options); Thread.sleep(3000); MqttMessage message = new MqttMessage(); diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml index eeea5a378a..1b7abf06db 100644 --- a/transport/coap/pom.xml +++ b/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index 3f57b32fab..3a1d0e7414 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -49,10 +49,6 @@ transport: sessions: inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}" report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:30000}" - rate_limits: - enabled: "${TB_TRANSPORT_RATE_LIMITS_ENABLED:false}" - tenant: "${TB_TRANSPORT_RATE_LIMITS_TENANT:1000:1,20000:60}" - device: "${TB_TRANSPORT_RATE_LIMITS_DEVICE:10:1,300:60}" json: # Cast String data types to Numeric if possible when processing Telemetry/Attributes JSON type_cast_enabled: "${JSON_TYPE_CAST_ENABLED:true}" @@ -149,6 +145,7 @@ queue: poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}" partitions: "${TB_QUEUE_CORE_PARTITIONS:10}" pack-processing-timeout: "${TB_QUEUE_CORE_PACK_PROCESSING_TIMEOUT_MS:60000}" + usage-stats-topic: "${TB_QUEUE_US_TOPIC:tb_usage_stats}" stats: enabled: "${TB_QUEUE_CORE_STATS_ENABLED:false}" print-interval-ms: "${TB_QUEUE_CORE_STATS_PRINT_INTERVAL_MS:10000}" diff --git a/transport/http/pom.xml b/transport/http/pom.xml index 7540b78507..f5ae869c75 100644 --- a/transport/http/pom.xml +++ b/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index 77d5f30fa7..ef152bb47d 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -42,10 +42,6 @@ transport: sessions: inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}" report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:30000}" - rate_limits: - enabled: "${TB_TRANSPORT_RATE_LIMITS_ENABLED:false}" - tenant: "${TB_TRANSPORT_RATE_LIMITS_TENANT:1000:1,20000:60}" - device: "${TB_TRANSPORT_RATE_LIMITS_DEVICE:10:1,300:60}" json: # Cast String data types to Numeric if possible when processing Telemetry/Attributes JSON type_cast_enabled: "${JSON_TYPE_CAST_ENABLED:true}" @@ -142,6 +138,7 @@ queue: poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}" partitions: "${TB_QUEUE_CORE_PARTITIONS:10}" pack-processing-timeout: "${TB_QUEUE_CORE_PACK_PROCESSING_TIMEOUT_MS:60000}" + usage-stats-topic: "${TB_QUEUE_US_TOPIC:tb_usage_stats}" stats: enabled: "${TB_QUEUE_CORE_STATS_ENABLED:false}" print-interval-ms: "${TB_QUEUE_CORE_STATS_PRINT_INTERVAL_MS:10000}" diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml index 6f4edd87b1..a1a45f54b8 100644 --- a/transport/mqtt/pom.xml +++ b/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index ba8d3f1ad1..fc8d970638 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -45,7 +45,6 @@ transport: mqtt: bind_address: "${MQTT_BIND_ADDRESS:0.0.0.0}" bind_port: "${MQTT_BIND_PORT:1883}" - adaptor: "${MQTT_ADAPTOR_NAME:JsonMqttAdaptor}" timeout: "${MQTT_TIMEOUT:10000}" netty: leak_detector_level: "${NETTY_LEAK_DETECTOR_LVL:DISABLED}" @@ -72,10 +71,6 @@ transport: sessions: inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}" report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:30000}" - rate_limits: - enabled: "${TB_TRANSPORT_RATE_LIMITS_ENABLED:false}" - tenant: "${TB_TRANSPORT_RATE_LIMITS_TENANT:1000:1,20000:60}" - device: "${TB_TRANSPORT_RATE_LIMITS_DEVICE:10:1,300:60}" json: # Cast String data types to Numeric if possible when processing Telemetry/Attributes JSON type_cast_enabled: "${JSON_TYPE_CAST_ENABLED:true}" @@ -172,6 +167,7 @@ queue: poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}" partitions: "${TB_QUEUE_CORE_PARTITIONS:10}" pack-processing-timeout: "${TB_QUEUE_CORE_PACK_PROCESSING_TIMEOUT_MS:60000}" + usage-stats-topic: "${TB_QUEUE_US_TOPIC:tb_usage_stats}" stats: enabled: "${TB_QUEUE_CORE_STATS_ENABLED:false}" print-interval-ms: "${TB_QUEUE_CORE_STATS_PRINT_INTERVAL_MS:10000}" diff --git a/transport/pom.xml b/transport/pom.xml index 59b5b2470b..429c953a46 100644 --- a/transport/pom.xml +++ b/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.5.5-SNAPSHOT + 3.2.0-SNAPSHOT thingsboard transport diff --git a/ui-ngx/.editorconfig b/ui-ngx/.editorconfig new file mode 100644 index 0000000000..9f4a13760a --- /dev/null +++ b/ui-ngx/.editorconfig @@ -0,0 +1,29 @@ +# +# Copyright © 2016-2020 The Thingsboard Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/ui-ngx/.gitignore b/ui-ngx/.gitignore new file mode 100644 index 0000000000..f4f46a5fee --- /dev/null +++ b/ui-ngx/.gitignore @@ -0,0 +1,46 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +/dist +/tmp +/out-tsc +# Only exists if Bazel was run +/bazel-out + +# dependencies +/node_modules + +# profiling files +chrome-profiler-events.json +speed-measure-plugin.json + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# misc +/.sass-cache +/connect.lock +/coverage +/libpeerconnection.log +npm-debug.log +yarn-error.log +testem.log +/typings + +# System Files +.DS_Store +Thumbs.db diff --git a/ui-ngx/LICENSE b/ui-ngx/LICENSE new file mode 100644 index 0000000000..c8f142f0bf --- /dev/null +++ b/ui-ngx/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2016 The Thingsboard Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/ui-ngx/angular.json b/ui-ngx/angular.json new file mode 100644 index 0000000000..3cad30304a --- /dev/null +++ b/ui-ngx/angular.json @@ -0,0 +1,252 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "thingsboard": { + "root": "", + "sourceRoot": "src", + "projectType": "application", + "prefix": "tb", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + } + }, + "architect": { + "build": { + "builder": "@angular-builders/custom-webpack:browser", + "options": { + "outputPath": "target/generated-resources/public", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tsconfig.app.json", + "assets": [ + "src/thingsboard.ico", + "src/assets", + { + "glob": "worker-html.js", + "input": "./node_modules/ace-builds/src-min/", + "output": "/" + }, + { + "glob": "worker-css.js", + "input": "./node_modules/ace-builds/src-min/", + "output": "/" + }, + { + "glob": "worker-json.js", + "input": "./node_modules/ace-builds/src-min/", + "output": "/" + }, + { + "glob": "worker-javascript.js", + "input": "./node_modules/ace-builds/src-min/", + "output": "/" + }, + { + "glob": "marker-icon-2x.png", + "input": "node_modules/leaflet/dist/images/", + "output": "/" + }, + { + "glob": "marker-icon.png", + "input": "node_modules/leaflet/dist/images/", + "output": "/" + }, + { + "glob": "marker-shadow.png", + "input": "node_modules/leaflet/dist/images/", + "output": "/" + } + ], + "styles": [ + "src/styles.scss", + "node_modules/jquery.terminal/css/jquery.terminal.min.css", + "node_modules/tooltipster/dist/css/tooltipster.bundle.min.css", + "node_modules/tooltipster/dist/css/plugins/tooltipster/sideTip/themes/tooltipster-sideTip-shadow.min.css", + "src/app/shared/components/json-form/react/json-form.scss", + "node_modules/rc-select/assets/index.less", + "node_modules/jstree-bootstrap-theme/dist/themes/proton/style.min.css", + "node_modules/leaflet/dist/leaflet.css", + "src/app/modules/home/components/widget/lib/maps/markers.scss", + "node_modules/leaflet.markercluster/dist/MarkerCluster.css", + "node_modules/leaflet.markercluster/dist/MarkerCluster.Default.css" + ], + "stylePreprocessorOptions": { + "includePaths": [ + "src/scss" + ] + }, + "scripts": [ + "node_modules/jquery/dist/jquery.min.js", + "node_modules/jquery.terminal/js/jquery.terminal.min.js", + "node_modules/flot/lib/jquery.colorhelpers.js", + "node_modules/flot/src/jquery.flot.js", + "node_modules/flot/src/plugins/jquery.flot.time.js", + "node_modules/flot/src/plugins/jquery.flot.selection.js", + "node_modules/flot/src/plugins/jquery.flot.pie.js", + "node_modules/flot/src/plugins/jquery.flot.crosshair.js", + "node_modules/flot/src/plugins/jquery.flot.stack.js", + "node_modules/flot/src/plugins/jquery.flot.symbol.js", + "node_modules/flot.curvedlines/curvedLines.js", + "node_modules/tinycolor2/dist/tinycolor-min.js", + "node_modules/tooltipster/dist/js/tooltipster.bundle.min.js", + "node_modules/split.js/dist/split.js", + "node_modules/js-beautify/js/lib/beautify.js", + "node_modules/js-beautify/js/lib/beautify-css.js", + "node_modules/js-beautify/js/lib/beautify-html.js", + "node_modules/ace-builds/src-min/ace.js", + "node_modules/ace-builds/src-min/ext-language_tools.js", + "node_modules/ace-builds/src-min/ext-searchbox.js", + "node_modules/ace-builds/src-min/theme-github.js", + "node_modules/ace-builds/src-min/mode-text.js", + "node_modules/ace-builds/src-min/mode-markdown.js", + "node_modules/ace-builds/src-min/mode-html.js", + "node_modules/ace-builds/src-min/mode-css.js", + "node_modules/ace-builds/src-min/mode-json.js", + "node_modules/ace-builds/src-min/mode-java.js", + "node_modules/ace-builds/src-min/mode-javascript.js", + "node_modules/ace-builds/src-min/snippets/text.js", + "node_modules/ace-builds/src-min/snippets/markdown.js", + "node_modules/ace-builds/src-min/snippets/html.js", + "node_modules/ace-builds/src-min/snippets/css.js", + "node_modules/ace-builds/src-min/snippets/json.js", + "node_modules/ace-builds/src-min/snippets/java.js", + "node_modules/ace-builds/src-min/snippets/javascript.js", + "node_modules/systemjs/dist/system.js", + "node_modules/jstree/dist/jstree.min.js" + ], + "customWebpackConfig": { + "path": "./extra-webpack.config.js" + }, + "allowedCommonJsDependencies": [ + "hammerjs", + "react", + "react-dom", + "reactcss", + "react-ace", + "schema-inspector", + "@flowjs/flow.js", + "@material-ui/icons/Add", + "@material-ui/icons/Clear", + "js-beautify", + "mousetrap", + "prop-types", + "react-is", + "hoist-non-react-statics", + "classnames", + "raf", + "moment-timezone" + ] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "extractCss": true, + "namedChunks": false, + "extractLicenses": true, + "vendorChunk": false, + "buildOptimizer": false, + "budgets": [ + { + "type": "initial", + "maximumWarning": "9mb", + "maximumError": "12mb" + } + ] + } + } + }, + "serve": { + "builder": "@angular-builders/custom-webpack:dev-server", + "options": { + "browserTarget": "thingsboard:build", + "proxyConfig": "proxy.conf.js" + }, + "configurations": { + "production": { + "browserTarget": "thingsboard:build:production" + } + } + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "thingsboard:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tsconfig.spec.json", + "karmaConfig": "src/karma.conf.js", + "styles": [ + "src/styles.scss" + ], + "scripts": [], + "assets": [ + "src/favicon.ico", + "src/assets" + ] + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "src/tsconfig.app.json", + "src/tsconfig.spec.json" + ], + "exclude": [ + "**/node_modules/**" + ] + } + } + } + }, + "thingsboard-e2e": { + "root": "e2e/", + "projectType": "application", + "prefix": "", + "architect": { + "e2e": { + "builder": "@angular-devkit/build-angular:protractor", + "options": { + "protractorConfig": "e2e/protractor.conf.js", + "devServerTarget": "thingsboard:serve" + }, + "configurations": { + "production": { + "devServerTarget": "thingsboard:serve:production" + } + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": "e2e/tsconfig.e2e.json", + "exclude": [ + "**/node_modules/**" + ] + } + } + } + } + }, + "defaultProject": "thingsboard", + "cli": { + "packageManager": "yarn" + } +} diff --git a/ui-ngx/e2e/protractor.conf.js b/ui-ngx/e2e/protractor.conf.js new file mode 100644 index 0000000000..e125764451 --- /dev/null +++ b/ui-ngx/e2e/protractor.conf.js @@ -0,0 +1,43 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// Protractor configuration file, see link for more information +// https://github.com/angular/protractor/blob/master/lib/config.ts + +const { SpecReporter } = require("jasmine-spec-reporter"); + +exports.config = { + allScriptsTimeout: 11000, + specs: [ + "./src/**/*.e2e-spec.ts", + ], + capabilities: { + "browserName": "chrome", + }, + directConnect: true, + baseUrl: "http://localhost:4200/", + framework: "jasmine", + jasmineNodeOpts: { + showColors: true, + defaultTimeoutInterval: 30000, + print: function() {}, + }, + onPrepare() { + require("ts-node").register({ + project: require("path").join(__dirname, "./tsconfig.e2e.json"), + }); + jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); + }, +}; diff --git a/ui-ngx/e2e/src/app.e2e-spec.ts b/ui-ngx/e2e/src/app.e2e-spec.ts new file mode 100644 index 0000000000..ee5a699b85 --- /dev/null +++ b/ui-ngx/e2e/src/app.e2e-spec.ts @@ -0,0 +1,39 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AppPage } from './app.po'; +import { browser, logging } from 'protractor'; + +describe('workspace-project App', () => { + let page: AppPage; + + beforeEach(() => { + page = new AppPage(); + }); + + it('should display welcome message', () => { + page.navigateTo(); + expect(page.getTitleText()).toEqual('Welcome to tb-license-server!'); + }); + + afterEach(async () => { + // Assert that there are no errors emitted from the browser + const logs = await browser.manage().logs().get(logging.Type.BROWSER); + expect(logs).not.toContain(jasmine.objectContaining({ + level: logging.Level.SEVERE, + } as logging.Entry)); + }); +}); diff --git a/ui-ngx/e2e/src/app.po.ts b/ui-ngx/e2e/src/app.po.ts new file mode 100644 index 0000000000..f38f906b65 --- /dev/null +++ b/ui-ngx/e2e/src/app.po.ts @@ -0,0 +1,27 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { browser, by, element } from 'protractor'; + +export class AppPage { + navigateTo() { + return browser.get(browser.baseUrl) as Promise; + } + + getTitleText() { + return element(by.css('tb-root h1')).getText() as Promise; + } +} diff --git a/ui-ngx/e2e/tsconfig.e2e.json b/ui-ngx/e2e/tsconfig.e2e.json new file mode 100644 index 0000000000..77d311e88d --- /dev/null +++ b/ui-ngx/e2e/tsconfig.e2e.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/app", + "module": "commonjs", + "target": "es5", + "types": [ + "jasmine", + "jasminewd2", + "node" + ] + } +} diff --git a/ui-ngx/extra-webpack.config.js b/ui-ngx/extra-webpack.config.js new file mode 100644 index 0000000000..9adc0e5852 --- /dev/null +++ b/ui-ngx/extra-webpack.config.js @@ -0,0 +1,43 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const CompressionPlugin = require("compression-webpack-plugin"); +const webpack = require("webpack"); +const dirTree = require("directory-tree"); + +var langs = []; + +dirTree("./src/assets/locale/", {extensions: /\.json$/}, (item) => { + /* It is expected what the name of a locale file has the following format: */ + /* 'locale.constant-LANG_CODE[_REGION_CODE].json', e.g. locale.constant-es.json or locale.constant-zh_CN.json*/ + langs.push(item.name.slice(item.name.lastIndexOf("-") + 1, -5)); +}); + +module.exports = { + plugins: [ + new webpack.DefinePlugin({ + TB_VERSION: JSON.stringify(require("./package.json").version), + SUPPORTED_LANGS: JSON.stringify(langs), + }), + new CompressionPlugin({ + filename: "[path][base].gz[query]", + algorithm: "gzip", + test: /\.js$|\.css$|\.html$|\.svg?.+$|\.jpg$|\.ttf?.+$|\.woff?.+$|\.eot?.+$|\.json$/, + threshold: 10240, + minRatio: 0.8, + deleteOriginalAssets: false, + }), + ], +}; diff --git a/ui-ngx/package.json b/ui-ngx/package.json new file mode 100644 index 0000000000..8e3ede2934 --- /dev/null +++ b/ui-ngx/package.json @@ -0,0 +1,138 @@ +{ + "name": "thingsboard", + "version": "3.2.0", + "scripts": { + "ng": "ng", + "start": "node --max_old_space_size=8048 ./node_modules/@angular/cli/bin/ng serve --host 0.0.0.0 --open", + "build": "ng build", + "build:prod": "ng build --prod --vendor-chunk", + "test": "ng test", + "lint": "ng lint", + "e2e": "ng e2e" + }, + "private": true, + "dependencies": { + "@angular/animations": "^10.1.5", + "@angular/cdk": "^10.2.4", + "@angular/common": "^10.1.5", + "@angular/compiler": "^10.1.5", + "@angular/core": "^10.1.5", + "@angular/flex-layout": "^10.0.0-beta.32", + "@angular/forms": "^10.1.5", + "@angular/material": "^10.2.4", + "@angular/platform-browser": "^10.1.5", + "@angular/platform-browser-dynamic": "^10.1.5", + "@angular/router": "^10.1.5", + "@auth0/angular-jwt": "^5.0.1", + "@date-io/date-fns": "^2.6.1", + "@flowjs/flow.js": "^2.14.1", + "@flowjs/ngx-flow": "^0.4.4", + "@juggle/resize-observer": "^3.1.3", + "@mat-datetimepicker/core": "^5.1.0", + "@material-ui/core": "^4.11.0", + "@material-ui/icons": "^4.9.1", + "@material-ui/pickers": "^3.2.10", + "@ngrx/effects": "^10.0.1", + "@ngrx/store": "^10.0.1", + "@ngrx/store-devtools": "^10.0.1", + "@ngx-translate/core": "^13.0.0", + "@ngx-translate/http-loader": "^6.0.0", + "ace-builds": "^1.4.12", + "angular-gridster2": "^10.1.6", + "angular2-hotkeys": "^2.2.0", + "canvas-gauges": "^2.1.7", + "compass-sass-mixins": "^0.12.7", + "core-js": "^3.6.5", + "date-fns": "^2.15.0", + "flot": "git://github.com/thingsboard/flot.git#0.9-work", + "flot.curvedlines": "git://github.com/MichaelZinsmaier/CurvedLines.git#master", + "font-awesome": "^4.7.0", + "jquery": "^3.5.1", + "jquery.terminal": "^2.18.3", + "js-beautify": "^1.13.0", + "json-schema-defaults": "^0.4.0", + "jstree": "^3.3.10", + "jstree-bootstrap-theme": "^1.0.1", + "jszip": "^3.5.0", + "leaflet": "^1.7.1", + "leaflet-editable": "^1.2.0", + "leaflet-polylinedecorator": "^1.6.0", + "leaflet-providers": "^1.10.2", + "leaflet.gridlayer.googlemutant": "0.10.0", + "leaflet.markercluster": "^1.4.1", + "material-design-icons": "^3.0.1", + "messageformat": "^2.3.0", + "moment": "^2.29.1", + "moment-timezone": "^0.5.31", + "mousetrap": "1.6.3", + "ngx-clipboard": "^13.0.1", + "ngx-color-picker": "^10.1.0", + "ngx-daterangepicker-material": "^4.0.1", + "ngx-flowchart": "git://github.com/thingsboard/ngx-flowchart.git#master", + "ngx-hm-carousel": "^2.0.0-rc.1", + "ngx-sharebuttons": "^8.0.1", + "ngx-translate-messageformat-compiler": "^4.8.0", + "objectpath": "^2.0.0", + "prettier": "^2.1.2", + "prop-types": "^15.7.2", + "raphael": "^2.3.0", + "rc-select": "~10.5.1", + "react": "~16.14.0", + "react-ace": "^9.1.4", + "react-dom": "^16.13.1", + "react-dropzone": "^11.2.0", + "reactcss": "^1.2.3", + "rxjs": "^6.6.3", + "schema-inspector": "^1.7.0", + "screenfull": "^5.0.2", + "split.js": "^1.6.2", + "systemjs": "0.21.5", + "tinycolor2": "^1.4.2", + "tooltipster": "^4.2.8", + "tslib": "^2.0.2", + "tv4": "^1.3.0", + "typeface-roboto": "^1.1.13", + "zone.js": "~0.10.3" + }, + "devDependencies": { + "@angular-builders/custom-webpack": "^10.0.1", + "@angular-devkit/build-angular": "^0.1001.5", + "@angular/cli": "^10.1.5", + "@angular/compiler-cli": "^10.1.5", + "@angular/language-service": "^10.1.5", + "@types/canvas-gauges": "^2.1.2", + "@types/flot": "^0.0.31", + "@types/jasmine": "^3.5.12", + "@types/jasminewd2": "^2.0.8", + "@types/jquery": "^3.5.2", + "@types/js-beautify": "^1.11.0", + "@types/jstree": "^3.3.40", + "@types/leaflet": "^1.5.17", + "@types/leaflet-polylinedecorator": "^1.6.0", + "@types/leaflet.markercluster": "^1.4.3", + "@types/lodash": "^4.14.161", + "@types/moment-timezone": "^0.5.30", + "@types/mousetrap": "1.6.3", + "@types/raphael": "^2.3.0", + "@types/react": "^16.9.51", + "@types/react-dom": "^16.9.8", + "@types/tinycolor2": "^1.4.2", + "@types/tooltipster": "^0.0.30", + "codelyzer": "^6.0.1", + "compression-webpack-plugin": "^6.0.2", + "directory-tree": "^2.2.4", + "jasmine-core": "~3.6.0", + "jasmine-spec-reporter": "~5.0.2", + "karma": "~5.1.1", + "karma-chrome-launcher": "~3.1.0", + "karma-coverage-istanbul-reporter": "~3.0.3", + "karma-jasmine": "~4.0.1", + "karma-jasmine-html-reporter": "^1.5.4", + "ngrx-store-freeze": "^0.2.4", + "protractor": "~7.0.0", + "ts-node": "^9.0.0", + "tslint": "~6.1.3", + "typescript": "~4.0.3", + "webpack": "^4.44.2" + } +} diff --git a/ui-ngx/pom.xml b/ui-ngx/pom.xml new file mode 100644 index 0000000000..14b9317e4a --- /dev/null +++ b/ui-ngx/pom.xml @@ -0,0 +1,141 @@ + + + 4.0.0 + + org.thingsboard + 3.2.0-SNAPSHOT + thingsboard + + org.thingsboard + ui-ngx + jar + + ThingsBoard Server UI + https://thingsboard.io + + + UTF-8 + ${basedir}/.. + + + + + + ${project.build.directory}/generated-resources + + + + + com.github.eirslett + frontend-maven-plugin + 1.7.5 + + target + ${basedir} + + + + install node and npm + + install-node-and-yarn + + + v12.16.1 + v1.22.4 + + + + yarn install + + yarn + + + install + + + + + + + + + yarn-build + + true + + + + + com.github.eirslett + frontend-maven-plugin + 1.7.5 + + target + ${basedir} + + + + yarn build + + yarn + + + run build:prod + + + + + + + + + yarn-start + + + yarn-start + + + + + + com.github.eirslett + frontend-maven-plugin + 1.7.5 + + target + ${basedir} + + + + yarn start + + yarn + + + start + + + + + + + + + diff --git a/ui-ngx/proxy.conf.js b/ui-ngx/proxy.conf.js new file mode 100644 index 0000000000..5be6bde1d1 --- /dev/null +++ b/ui-ngx/proxy.conf.js @@ -0,0 +1,49 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const forwardUrl = "http://localhost:8080"; +const wsForwardUrl = "ws://localhost:8080"; +const ruleNodeUiforwardUrl = forwardUrl; + +const PROXY_CONFIG = { + "/api": { + "target": forwardUrl, + "secure": false, + }, + "/static/rulenode": { + "target": ruleNodeUiforwardUrl, + "secure": false, + }, + "/oauth2": { + "target": forwardUrl, + "secure": false, + }, + "/login/oauth2": { + "target": forwardUrl, + "secure": false, + }, + "/static": { + "target": forwardUrl, + "secure": false, + }, + "/api/ws": { + "target": wsForwardUrl, + "ws": true, + "secure": false + }, +}; + +module.exports = PROXY_CONFIG; diff --git a/ui-ngx/src/.browserslistrc b/ui-ngx/src/.browserslistrc new file mode 100644 index 0000000000..b744b0a114 --- /dev/null +++ b/ui-ngx/src/.browserslistrc @@ -0,0 +1,11 @@ +# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries +# +# For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed + +> 0.5% +last 2 versions +Firefox ESR +not dead +IE 10-11 diff --git a/ui-ngx/src/app/app-routing.module.ts b/ui-ngx/src/app/app-routing.module.ts new file mode 100644 index 0000000000..3e784284d4 --- /dev/null +++ b/ui-ngx/src/app/app-routing.module.ts @@ -0,0 +1,36 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +const routes: Routes = [ + { path: '', + redirectTo: 'home', + pathMatch: 'full', + data: { + breadcrumb: { + skip: true + } + } + } +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule] +}) +export class AppRoutingModule { } diff --git a/ui-ngx/src/app/app.component.html b/ui-ngx/src/app/app.component.html new file mode 100644 index 0000000000..6521e99533 --- /dev/null +++ b/ui-ngx/src/app/app.component.html @@ -0,0 +1,20 @@ + + + + diff --git a/ui/src/app/components/queue/queue-type-list.scss b/ui-ngx/src/app/app.component.scss similarity index 100% rename from ui/src/app/components/queue/queue-type-list.scss rename to ui-ngx/src/app/app.component.scss diff --git a/ui-ngx/src/app/app.component.ts b/ui-ngx/src/app/app.component.ts new file mode 100644 index 0000000000..f7d290c5ca --- /dev/null +++ b/ui-ngx/src/app/app.component.ts @@ -0,0 +1,128 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import 'hammerjs'; + +import { Component, OnInit } from '@angular/core'; + +import { environment as env } from '@env/environment'; + +import { TranslateService } from '@ngx-translate/core'; +import { select, Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { LocalStorageService } from '@core/local-storage/local-storage.service'; +import { DomSanitizer } from '@angular/platform-browser'; +import { MatIconRegistry } from '@angular/material/icon'; +import { combineLatest } from 'rxjs'; +import { selectIsAuthenticated, selectIsUserLoaded } from '@core/auth/auth.selectors'; +import { distinctUntilChanged, filter, map, skip } from 'rxjs/operators'; +import { AuthService } from '@core/auth/auth.service'; + +@Component({ + selector: 'tb-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'] +}) +export class AppComponent implements OnInit { + + constructor(private store: Store, + private storageService: LocalStorageService, + private translate: TranslateService, + private matIconRegistry: MatIconRegistry, + private domSanitizer: DomSanitizer, + private authService: AuthService) { + + console.log(`ThingsBoard Version: ${env.tbVersion}`); + + this.matIconRegistry.addSvgIconSetInNamespace('mdi', + this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/mdi.svg')); + + this.matIconRegistry.addSvgIconLiteral( + 'alpha-a-circle-outline', + this.domSanitizer.bypassSecurityTrustHtml( + '' + ) + ); + this.matIconRegistry.addSvgIconLiteral( + 'alpha-e-circle-outline', + this.domSanitizer.bypassSecurityTrustHtml( + '' + ) + ); + + this.matIconRegistry.addSvgIconLiteral( + 'google-logo', + this.domSanitizer.bypassSecurityTrustHtml( + '' + ) + ); + + this.matIconRegistry.addSvgIconLiteral( + 'github-logo', + this.domSanitizer.bypassSecurityTrustHtml( + '' + ) + ); + + this.matIconRegistry.addSvgIconLiteral( + 'facebook-logo', + this.domSanitizer.bypassSecurityTrustHtml( + '' + ) + ); + + this.storageService.testLocalStorage(); + + this.setupTranslate(); + this.setupAuth(); + } + + setupTranslate() { + if (!env.production) { + console.log(`Supported Langs: ${env.supportedLangs}`); + } + this.translate.addLangs(env.supportedLangs); + if (!env.production) { + console.log(`Default Lang: ${env.defaultLang}`); + } + this.translate.setDefaultLang(env.defaultLang); + } + + setupAuth() { + combineLatest([ + this.store.pipe(select(selectIsAuthenticated)), + this.store.pipe(select(selectIsUserLoaded))] + ).pipe( + map(results => ({isAuthenticated: results[0], isUserLoaded: results[1]})), + distinctUntilChanged(), + filter((data) => data.isUserLoaded ), + skip(1), + ).subscribe((data) => { + this.authService.gotoDefaultPlace(data.isAuthenticated); + }); + this.authService.reloadUser(); + } + + ngOnInit() { + } + +} + diff --git a/ui-ngx/src/app/app.module.ts b/ui-ngx/src/app/app.module.ts new file mode 100644 index 0000000000..64ec2095a5 --- /dev/null +++ b/ui-ngx/src/app/app.module.ts @@ -0,0 +1,61 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { NgModule } from '@angular/core'; + +import { AppRoutingModule } from './app-routing.module'; +import { CoreModule } from '@core/core.module'; +import { LoginModule } from '@modules/login/login.module'; +import { HomeModule } from '@home/home.module'; + +import { AppComponent } from './app.component'; +import { DashboardRoutingModule } from '@modules/dashboard/dashboard-routing.module'; +import { RouterModule, Routes } from '@angular/router'; + +const routes: Routes = [ + { path: '**', + redirectTo: 'home' + } +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class PageNotFoundRoutingModule { } + + +@NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule, + BrowserAnimationsModule, + AppRoutingModule, + CoreModule, + LoginModule, + HomeModule, + DashboardRoutingModule, + PageNotFoundRoutingModule + ], + providers: [], + bootstrap: [AppComponent] +}) +export class AppModule { } diff --git a/ui-ngx/src/app/core/api/alarm-data-subscription.ts b/ui-ngx/src/app/core/api/alarm-data-subscription.ts new file mode 100644 index 0000000000..35a43c3f0f --- /dev/null +++ b/ui-ngx/src/app/core/api/alarm-data-subscription.ts @@ -0,0 +1,180 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AlarmDataCmd, + DataKeyType, + TelemetryService, + TelemetrySubscriber +} from '@shared/models/telemetry/telemetry.models'; +import { DatasourceType } from '@shared/models/widget.models'; +import { + AlarmData, + AlarmDataPageLink, + EntityFilter, + EntityKey, + EntityKeyType, + KeyFilter +} from '@shared/models/query/query.models'; +import { SubscriptionTimewindow } from '@shared/models/time/time.models'; +import { AlarmDataListener } from '@core/api/alarm-data.service'; +import { PageData } from '@shared/models/page/page-data'; +import { deepClone, isDefined, isDefinedAndNotNull, isObject } from '@core/utils'; +import { simulatedAlarm } from '@shared/models/alarm.models'; + +export interface AlarmSubscriptionDataKey { + name: string; + type: DataKeyType; +} + +export interface AlarmDataSubscriptionOptions { + datasourceType: DatasourceType; + dataKeys: Array; + entityFilter?: EntityFilter; + pageLink?: AlarmDataPageLink; + keyFilters?: Array; + additionalKeyFilters?: Array; + subscriptionTimewindow?: SubscriptionTimewindow; +} + +export class AlarmDataSubscription { + + private alarmDataSubscriptionOptions = this.listener.alarmDataSubscriptionOptions; + private datasourceType: DatasourceType = this.alarmDataSubscriptionOptions.datasourceType; + + private history: boolean; + private realtime: boolean; + + private subscriber: TelemetrySubscriber; + private alarmDataCommand: AlarmDataCmd; + + private pageData: PageData; + private alarmIdToDataIndex: {[id: string]: number}; + + private subsTw: SubscriptionTimewindow; + + constructor(private listener: AlarmDataListener, + private telemetryService: TelemetryService) { + } + + public unsubscribe() { + if (this.datasourceType === DatasourceType.entity) { + if (this.subscriber) { + this.subscriber.unsubscribe(); + this.subscriber = null; + } + } + } + + public subscribe() { + this.subsTw = this.alarmDataSubscriptionOptions.subscriptionTimewindow; + this.history = this.alarmDataSubscriptionOptions.subscriptionTimewindow && + isObject(this.alarmDataSubscriptionOptions.subscriptionTimewindow.fixedWindow); + this.realtime = this.alarmDataSubscriptionOptions.subscriptionTimewindow && + isDefinedAndNotNull(this.alarmDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs); + if (this.datasourceType === DatasourceType.entity) { + this.subscriber = new TelemetrySubscriber(this.telemetryService); + this.alarmDataCommand = new AlarmDataCmd(); + + const alarmFields: Array = + this.alarmDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.alarm).map( + dataKey => ({ type: EntityKeyType.ALARM_FIELD, key: dataKey.name }) + ); + + const entityFields: Array = + this.alarmDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.entityField).map( + dataKey => ({ type: EntityKeyType.ENTITY_FIELD, key: dataKey.name }) + ); + + const attrFields = this.alarmDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.attribute).map( + dataKey => ({ type: EntityKeyType.ATTRIBUTE, key: dataKey.name }) + ); + const tsFields = this.alarmDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.timeseries).map( + dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name }) + ); + const latestValues = attrFields.concat(tsFields); + + let keyFilters = this.alarmDataSubscriptionOptions.keyFilters; + if (this.alarmDataSubscriptionOptions.additionalKeyFilters) { + if (keyFilters) { + keyFilters = keyFilters.concat(this.alarmDataSubscriptionOptions.additionalKeyFilters); + } else { + keyFilters = this.alarmDataSubscriptionOptions.additionalKeyFilters; + } + } + this.alarmDataCommand.query = { + entityFilter: this.alarmDataSubscriptionOptions.entityFilter, + pageLink: deepClone(this.alarmDataSubscriptionOptions.pageLink), + keyFilters, + alarmFields, + entityFields, + latestValues + }; + if (this.history) { + this.alarmDataCommand.query.pageLink.startTs = this.subsTw.fixedWindow.startTimeMs; + this.alarmDataCommand.query.pageLink.endTs = this.subsTw.fixedWindow.endTimeMs; + } else { + this.alarmDataCommand.query.pageLink.timeWindow = this.subsTw.realtimeWindowMs; + } + + this.subscriber.subscriptionCommands.push(this.alarmDataCommand); + + this.subscriber.alarmData$.subscribe((alarmDataUpdate) => { + if (alarmDataUpdate.data) { + this.onPageData(alarmDataUpdate.data, alarmDataUpdate.allowedEntities, alarmDataUpdate.totalEntities); + } else if (alarmDataUpdate.update) { + this.onDataUpdate(alarmDataUpdate.update); + } + }); + + this.subscriber.subscribe(); + + } else if (this.datasourceType === DatasourceType.function) { + const pageData: PageData = { + data: [{...simulatedAlarm, entityId: '1', latest: {}}], + hasNext: false, + totalElements: 1, + totalPages: 1 + }; + this.onPageData(pageData, 1024, 1); + } + } + + private resetData() { + this.alarmIdToDataIndex = {}; + for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) { + const alarmData = this.pageData.data[dataIndex]; + this.alarmIdToDataIndex[alarmData.id.id] = dataIndex; + } + } + + private onPageData(pageData: PageData, allowedEntities: number, totalEntities: number) { + this.pageData = pageData; + this.resetData(); + this.listener.alarmsLoaded(pageData, allowedEntities, totalEntities); + } + + private onDataUpdate(update: Array) { + for (const alarmData of update) { + const dataIndex = this.alarmIdToDataIndex[alarmData.id.id]; + if (isDefined(dataIndex) && dataIndex >= 0) { + this.pageData.data[dataIndex] = alarmData; + } + } + this.listener.alarmsUpdated(update, this.pageData); + } + +} diff --git a/ui-ngx/src/app/core/api/alarm-data.service.ts b/ui-ngx/src/app/core/api/alarm-data.service.ts new file mode 100644 index 0000000000..3454036db7 --- /dev/null +++ b/ui-ngx/src/app/core/api/alarm-data.service.ts @@ -0,0 +1,91 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { SubscriptionTimewindow } from '@shared/models/time/time.models'; +import { Datasource, DatasourceType } from '@shared/models/widget.models'; +import { PageData } from '@shared/models/page/page-data'; +import { AlarmData, AlarmDataPageLink, KeyFilter } from '@shared/models/query/query.models'; +import { Injectable } from '@angular/core'; +import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service'; +import { + AlarmDataSubscription, + AlarmDataSubscriptionOptions, + AlarmSubscriptionDataKey +} from '@core/api/alarm-data-subscription'; +import { deepClone } from '@core/utils'; + +export interface AlarmDataListener { + subscriptionTimewindow?: SubscriptionTimewindow; + alarmSource: Datasource; + alarmsLoaded: (pageData: PageData, allowedEntities: number, totalEntities: number) => void; + alarmsUpdated: (update: Array, pageData: PageData) => void; + alarmDataSubscriptionOptions?: AlarmDataSubscriptionOptions; + subscription?: AlarmDataSubscription; +} + +@Injectable({ + providedIn: 'root' +}) +export class AlarmDataService { + + constructor(private telemetryService: TelemetryWebsocketService) {} + + + public subscribeForAlarms(listener: AlarmDataListener, + pageLink: AlarmDataPageLink, + keyFilters: KeyFilter[]) { + const alarmSource = listener.alarmSource; + listener.alarmDataSubscriptionOptions = this.createAlarmSubscriptionOptions(listener, pageLink, keyFilters); + if (alarmSource.type === DatasourceType.entity && (!alarmSource.entityFilter || !pageLink)) { + return; + } + listener.subscription = new AlarmDataSubscription(listener, this.telemetryService); + return listener.subscription.subscribe(); + } + + public stopSubscription(listener: AlarmDataListener) { + if (listener.subscription) { + listener.subscription.unsubscribe(); + } + } + + private createAlarmSubscriptionOptions(listener: AlarmDataListener, + pageLink: AlarmDataPageLink, + additionalKeyFilters: KeyFilter[]): AlarmDataSubscriptionOptions { + const alarmSource = listener.alarmSource; + const alarmSubscriptionDataKeys: Array = []; + alarmSource.dataKeys.forEach((dataKey) => { + const alarmSubscriptionDataKey: AlarmSubscriptionDataKey = { + name: dataKey.name, + type: dataKey.type + }; + alarmSubscriptionDataKeys.push(alarmSubscriptionDataKey); + }); + const alarmDataSubscriptionOptions: AlarmDataSubscriptionOptions = { + datasourceType: alarmSource.type, + dataKeys: alarmSubscriptionDataKeys, + subscriptionTimewindow: deepClone(listener.subscriptionTimewindow) + }; + if (alarmDataSubscriptionOptions.datasourceType === DatasourceType.entity) { + alarmDataSubscriptionOptions.entityFilter = alarmSource.entityFilter; + alarmDataSubscriptionOptions.pageLink = pageLink; + alarmDataSubscriptionOptions.keyFilters = alarmSource.keyFilters; + alarmDataSubscriptionOptions.additionalKeyFilters = additionalKeyFilters; + } + return alarmDataSubscriptionOptions; + } + +} diff --git a/ui-ngx/src/app/core/api/alias-controller.ts b/ui-ngx/src/app/core/api/alias-controller.ts new file mode 100644 index 0000000000..9389cc8227 --- /dev/null +++ b/ui-ngx/src/app/core/api/alias-controller.ts @@ -0,0 +1,385 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AliasInfo, IAliasController, StateControllerHolder, StateEntityInfo } from '@core/api/widget-api.models'; +import { forkJoin, Observable, of, ReplaySubject, Subject } from 'rxjs'; +import { Datasource, DatasourceType } from '@app/shared/models/widget.models'; +import { deepClone, isEqual } from '@core/utils'; +import { EntityService } from '@core/http/entity.service'; +import { UtilsService } from '@core/services/utils.service'; +import { AliasFilterType, EntityAliases, SingleEntityFilter } from '@shared/models/alias.models'; +import { EntityInfo } from '@shared/models/entity.models'; +import { map, mergeMap } from 'rxjs/operators'; +import { + defaultEntityDataPageLink, Filter, FilterInfo, filterInfoToKeyFilters, Filters, KeyFilter, singleEntityDataPageLink, + updateDatasourceFromEntityInfo +} from '@shared/models/query/query.models'; + +export class AliasController implements IAliasController { + + entityAliasesChangedSubject = new Subject>(); + entityAliasesChanged: Observable> = this.entityAliasesChangedSubject.asObservable(); + + filtersChangedSubject = new Subject>(); + filtersChanged: Observable> = this.filtersChangedSubject.asObservable(); + + private entityAliasResolvedSubject = new Subject(); + entityAliasResolved: Observable = this.entityAliasResolvedSubject.asObservable(); + + entityAliases: EntityAliases; + filters: Filters; + userFilters: Filters; + + resolvedAliases: {[aliasId: string]: AliasInfo} = {}; + resolvedAliasesObservable: {[aliasId: string]: Observable} = {}; + + resolvedAliasesToStateEntities: {[aliasId: string]: StateEntityInfo} = {}; + + constructor(private utils: UtilsService, + private entityService: EntityService, + private stateControllerHolder: StateControllerHolder, + private origEntityAliases: EntityAliases, + private origFilters: Filters) { + this.entityAliases = deepClone(this.origEntityAliases); + this.filters = deepClone(this.origFilters); + this.userFilters = {}; + } + + updateEntityAliases(newEntityAliases: EntityAliases) { + const changedAliasIds: Array = []; + for (const aliasId of Object.keys(newEntityAliases)) { + const newEntityAlias = newEntityAliases[aliasId]; + const prevEntityAlias = this.entityAliases[aliasId]; + if (!isEqual(newEntityAlias, prevEntityAlias)) { + changedAliasIds.push(aliasId); + this.setAliasUnresolved(aliasId); + } + } + for (const aliasId of Object.keys(this.entityAliases)) { + if (!newEntityAliases[aliasId]) { + changedAliasIds.push(aliasId); + this.setAliasUnresolved(aliasId); + } + } + this.entityAliases = deepClone(newEntityAliases); + if (changedAliasIds.length) { + this.entityAliasesChangedSubject.next(changedAliasIds); + } + } + + updateFilters(newFilters: Filters) { + const changedFilterIds: Array = []; + for (const filterId of Object.keys(newFilters)) { + const newFilter = newFilters[filterId]; + const prevFilter = this.filters[filterId]; + if (!isEqual(newFilter, prevFilter)) { + changedFilterIds.push(filterId); + } + } + for (const filterId of Object.keys(this.filters)) { + if (!newFilters[filterId]) { + changedFilterIds.push(filterId); + } + } + this.filters = deepClone(newFilters); + if (changedFilterIds.length) { + for (const filterId of changedFilterIds) { + delete this.userFilters[filterId]; + } + this.filtersChangedSubject.next(changedFilterIds); + } + } + + updateAliases(aliasIds?: Array) { + if (!aliasIds) { + aliasIds = []; + for (const aliasId of Object.keys(this.resolvedAliases)) { + aliasIds.push(aliasId); + } + } + const tasks: Observable[] = []; + for (const aliasId of aliasIds) { + this.setAliasUnresolved(aliasId); + tasks.push(this.getAliasInfo(aliasId)); + } + forkJoin(tasks).subscribe(() => { + this.entityAliasesChangedSubject.next(aliasIds); + }); + } + + dashboardStateChanged() { + const changedAliasIds: Array = []; + for (const aliasId of Object.keys(this.resolvedAliasesToStateEntities)) { + const stateEntityInfo = this.resolvedAliasesToStateEntities[aliasId]; + const newEntityId = this.stateControllerHolder().getEntityId(stateEntityInfo.entityParamName); + const prevEntityId = stateEntityInfo.entityId; + if (!isEqual(newEntityId, prevEntityId)) { + changedAliasIds.push(aliasId); + this.setAliasUnresolved(aliasId); + } + } + if (changedAliasIds.length) { + this.entityAliasesChangedSubject.next(changedAliasIds); + } + } + + setAliasUnresolved(aliasId: string) { + delete this.resolvedAliases[aliasId]; + delete this.resolvedAliasesObservable[aliasId]; + delete this.resolvedAliasesToStateEntities[aliasId]; + } + + getEntityAliases(): EntityAliases { + return this.entityAliases; + } + + getFilters(): Filters { + return this.filters; + } + + getFilterInfo(filterId: string): FilterInfo { + if (this.userFilters[filterId]) { + return this.userFilters[filterId]; + } else { + return this.filters[filterId]; + } + } + + getKeyFilters(filterId: string): Array { + const filter = this.getFilterInfo(filterId); + if (filter) { + return filterInfoToKeyFilters(filter); + } else { + return []; + } + } + + getEntityAliasId(aliasName: string): string { + for (const aliasId of Object.keys(this.entityAliases)) { + const alias = this.entityAliases[aliasId]; + if (alias.alias === aliasName) { + return aliasId; + } + } + return null; + } + + getAliasInfo(aliasId: string): Observable { + let aliasInfo = this.resolvedAliases[aliasId]; + if (aliasInfo) { + return of(aliasInfo); + } else if (this.resolvedAliasesObservable[aliasId]) { + return this.resolvedAliasesObservable[aliasId]; + } else { + const resolvedAliasSubject = new ReplaySubject(); + this.resolvedAliasesObservable[aliasId] = resolvedAliasSubject.asObservable(); + const entityAlias = this.entityAliases[aliasId]; + if (entityAlias) { + this.entityService.resolveAlias(entityAlias, this.stateControllerHolder().getStateParams()).subscribe( + (resolvedAliasInfo) => { + this.resolvedAliases[aliasId] = resolvedAliasInfo; + delete this.resolvedAliasesObservable[aliasId]; + if (resolvedAliasInfo.stateEntity) { + this.resolvedAliasesToStateEntities[aliasId] = { + entityParamName: resolvedAliasInfo.entityParamName, + entityId: this.stateControllerHolder().getEntityId(resolvedAliasInfo.entityParamName) + }; + } + this.entityAliasResolvedSubject.next(aliasId); + resolvedAliasSubject.next(resolvedAliasInfo); + resolvedAliasSubject.complete(); + }, + () => { + resolvedAliasSubject.error(null); + delete this.resolvedAliasesObservable[aliasId]; + } + ); + } else { + resolvedAliasSubject.error(null); + const res = this.resolvedAliasesObservable[aliasId]; + delete this.resolvedAliasesObservable[aliasId]; + return res; + } + aliasInfo = this.resolvedAliases[aliasId]; + if (aliasInfo) { + return of(aliasInfo); + } else { + return this.resolvedAliasesObservable[aliasId]; + } + } + } + + resolveSingleEntityInfo(aliasId: string): Observable { + return this.getAliasInfo(aliasId).pipe( + mergeMap((aliasInfo) => { + if (aliasInfo.resolveMultiple) { + if (aliasInfo.entityFilter) { + return this.entityService.findSingleEntityInfoByEntityFilter(aliasInfo.entityFilter, + {ignoreLoading: true, ignoreErrors: true}); + } else { + return of(null); + } + } else { + return of(aliasInfo.currentEntity); + } + }) + ); + } + + private resolveDatasource(datasource: Datasource, forceFilter = false): Observable { + const newDatasource = deepClone(datasource); + if (newDatasource.type === DatasourceType.entity) { + if (newDatasource.filterId) { + newDatasource.keyFilters = this.getKeyFilters(newDatasource.filterId); + } + if (newDatasource.entityAliasId) { + return this.getAliasInfo(newDatasource.entityAliasId).pipe( + mergeMap((aliasInfo) => { + newDatasource.aliasName = aliasInfo.alias; + if (!aliasInfo.entityFilter) { + newDatasource.unresolvedStateEntity = true; + newDatasource.name = 'Unresolved'; + newDatasource.entityName = 'Unresolved'; + return of(newDatasource); + } + if (aliasInfo.resolveMultiple) { + newDatasource.entityFilter = aliasInfo.entityFilter; + if (forceFilter) { + return this.entityService.findSingleEntityInfoByEntityFilter(aliasInfo.entityFilter, + {ignoreLoading: true, ignoreErrors: true}).pipe( + map((entity) => { + if (entity) { + updateDatasourceFromEntityInfo(newDatasource, entity, true); + } + return newDatasource; + }) + ); + } else { + return of(newDatasource); + } + } else { + if (aliasInfo.currentEntity) { + updateDatasourceFromEntityInfo(newDatasource, aliasInfo.currentEntity, true); + } else if (aliasInfo.stateEntity) { + newDatasource.unresolvedStateEntity = true; + newDatasource.name = 'Unresolved'; + newDatasource.entityName = 'Unresolved'; + } + return of(newDatasource); + } + }) + ); + } else if (newDatasource.entityId && !newDatasource.entityFilter) { + newDatasource.entityFilter = { + singleEntity: { + id: newDatasource.entityId, + entityType: newDatasource.entityType, + }, + type: AliasFilterType.singleEntity + } as SingleEntityFilter; + return of(newDatasource); + } else { + newDatasource.aliasName = newDatasource.entityName; + newDatasource.name = newDatasource.entityName; + return of(newDatasource); + } + } else { + return of(newDatasource); + } + } + + resolveAlarmSource(alarmSource: Datasource): Observable { + return this.resolveDatasource(alarmSource).pipe( + map((datasource) => { + if (datasource.type === DatasourceType.function) { + let name: string; + if (datasource.name && datasource.name.length) { + name = datasource.name; + } else { + name = DatasourceType.function; + } + datasource.name = name; + datasource.aliasName = name; + datasource.entityName = name; + } + return datasource; + }) + ); + } + + resolveDatasources(datasources: Array, singleEntity?: boolean): Observable> { + const toResolve = singleEntity ? [datasources[0]] : datasources; + const observables = new Array>(); + toResolve.forEach((datasource) => { + observables.push(this.resolveDatasource(datasource)); + }); + return forkJoin(observables).pipe( + map((result) => { + let functionIndex = 0; + result.forEach((datasource) => { + if (datasource.type === DatasourceType.function) { + let name: string; + if (datasource.name && datasource.name.length) { + name = datasource.name; + } else { + functionIndex++; + name = DatasourceType.function; + if (functionIndex > 1) { + name += ' ' + functionIndex; + } + } + datasource.name = name; + datasource.aliasName = name; + datasource.entityName = name; + } else { + if (singleEntity) { + datasource.pageLink = deepClone(singleEntityDataPageLink); + } else if (!datasource.pageLink) { + datasource.pageLink = deepClone(defaultEntityDataPageLink); + } + } + }); + return result; + }) + ); + } + + getInstantAliasInfo(aliasId: string): AliasInfo { + return this.resolvedAliases[aliasId]; + } + + updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo) { + const aliasInfo = this.resolvedAliases[aliasId]; + if (aliasInfo) { + const prevCurrentEntity = aliasInfo.currentEntity; + if (!isEqual(currentEntity, prevCurrentEntity)) { + aliasInfo.currentEntity = currentEntity; + this.entityAliasesChangedSubject.next([aliasId]); + } + } + } + + updateUserFilter(filter: Filter) { + let prevUserFilter = this.userFilters[filter.id]; + if (!prevUserFilter) { + prevUserFilter = this.filters[filter.id]; + } + if (prevUserFilter && !isEqual(prevUserFilter, filter)) { + this.userFilters[filter.id] = filter; + this.filtersChangedSubject.next([filter.id]); + } + } +} diff --git a/ui-ngx/src/app/core/api/data-aggregator.ts b/ui-ngx/src/app/core/api/data-aggregator.ts new file mode 100644 index 0000000000..7b31b7c6c1 --- /dev/null +++ b/ui-ngx/src/app/core/api/data-aggregator.ts @@ -0,0 +1,338 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { SubscriptionData, SubscriptionDataHolder } from '@app/shared/models/telemetry/telemetry.models'; +import { AggregationType } from '@shared/models/time/time.models'; +import { UtilsService } from '@core/services/utils.service'; +import { deepClone } from '@core/utils'; +import Timeout = NodeJS.Timeout; + +export declare type onAggregatedData = (data: SubscriptionData, detectChanges: boolean) => void; + +interface AggData { + count: number; + sum: number; + aggValue: any; +} + +interface AggregationMap { + [key: string]: Map; +} + +declare type AggFunction = (aggData: AggData, value?: any) => void; + +const avg: AggFunction = (aggData: AggData, value?: any) => { + aggData.count++; + aggData.sum += value; + aggData.aggValue = aggData.sum / aggData.count; +}; + +const min: AggFunction = (aggData: AggData, value?: any) => { + aggData.aggValue = Math.min(aggData.aggValue, value); +}; + +const max: AggFunction = (aggData: AggData, value?: any) => { + aggData.aggValue = Math.max(aggData.aggValue, value); +}; + +const sum: AggFunction = (aggData: AggData, value?: any) => { + aggData.aggValue = aggData.aggValue + value; +}; + +const count: AggFunction = (aggData: AggData) => { + aggData.count++; + aggData.aggValue = aggData.count; +}; + +const none: AggFunction = (aggData: AggData, value?: any) => { + aggData.aggValue = value; +}; + +export class DataAggregator { + + private dataBuffer: SubscriptionData = {}; + private data: SubscriptionData; + private readonly lastPrevKvPairData: {[key: string]: [number, any]}; + + private aggregationMap: AggregationMap; + + private dataReceived = false; + private resetPending = false; + + private noAggregation = this.aggregationType === AggregationType.NONE; + private aggregationTimeout = Math.max(this.interval, 1000); + private readonly aggFunction: AggFunction; + + private intervalTimeoutHandle: Timeout; + private intervalScheduledTime: number; + + private endTs: number; + private elapsed: number; + + constructor(private onDataCb: onAggregatedData, + private tsKeyNames: string[], + private startTs: number, + private limit: number, + private aggregationType: AggregationType, + private timeWindow: number, + private interval: number, + private stateData: boolean, + private utils: UtilsService) { + this.tsKeyNames.forEach((key) => { + this.dataBuffer[key] = []; + }); + if (this.stateData) { + this.lastPrevKvPairData = {}; + } + switch (this.aggregationType) { + case AggregationType.MIN: + this.aggFunction = min; + break; + case AggregationType.MAX: + this.aggFunction = max; + break; + case AggregationType.AVG: + this.aggFunction = avg; + break; + case AggregationType.SUM: + this.aggFunction = sum; + break; + case AggregationType.COUNT: + this.aggFunction = count; + break; + case AggregationType.NONE: + this.aggFunction = none; + break; + default: + this.aggFunction = avg; + } + } + + public updateOnDataCb(newOnDataCb: onAggregatedData): onAggregatedData { + const prevOnDataCb = this.onDataCb; + this.onDataCb = newOnDataCb; + return prevOnDataCb; + } + + public reset(startTs: number, timeWindow: number, interval: number) { + if (this.intervalTimeoutHandle) { + clearTimeout(this.intervalTimeoutHandle); + this.intervalTimeoutHandle = null; + } + this.intervalScheduledTime = this.utils.currentPerfTime(); + this.startTs = startTs; + this.timeWindow = timeWindow; + this.interval = interval; + this.endTs = this.startTs + this.timeWindow; + this.elapsed = 0; + this.aggregationTimeout = Math.max(this.interval, 1000); + this.resetPending = true; + this.intervalTimeoutHandle = setTimeout(this.onInterval.bind(this), this.aggregationTimeout); + } + + public destroy() { + if (this.intervalTimeoutHandle) { + clearTimeout(this.intervalTimeoutHandle); + this.intervalTimeoutHandle = null; + } + this.aggregationMap = null; + } + + public onData(data: SubscriptionDataHolder, update: boolean, history: boolean, detectChanges: boolean) { + if (!this.dataReceived || this.resetPending) { + let updateIntervalScheduledTime = true; + if (!this.dataReceived) { + this.elapsed = 0; + this.dataReceived = true; + this.endTs = this.startTs + this.timeWindow; + } + if (this.resetPending) { + this.resetPending = false; + updateIntervalScheduledTime = false; + } + if (update) { + this.aggregationMap = {}; + this.updateAggregatedData(data.data); + } else { + this.aggregationMap = this.processAggregatedData(data.data); + } + if (updateIntervalScheduledTime) { + this.intervalScheduledTime = this.utils.currentPerfTime(); + } + this.onInterval(history, detectChanges); + } else { + this.updateAggregatedData(data.data); + if (history) { + this.intervalScheduledTime = this.utils.currentPerfTime(); + this.onInterval(history, detectChanges); + } + } + } + + private onInterval(history?: boolean, detectChanges?: boolean) { + const now = this.utils.currentPerfTime(); + this.elapsed += now - this.intervalScheduledTime; + this.intervalScheduledTime = now; + if (this.intervalTimeoutHandle) { + clearTimeout(this.intervalTimeoutHandle); + this.intervalTimeoutHandle = null; + } + if (!history) { + const delta = Math.floor(this.elapsed / this.interval); + if (delta || !this.data) { + this.startTs += delta * this.interval; + this.endTs += delta * this.interval; + this.data = this.updateData(); + this.elapsed = this.elapsed - delta * this.interval; + } + } else { + this.data = this.updateData(); + } + if (this.onDataCb) { + this.onDataCb(this.data, detectChanges); + } + if (!history) { + this.intervalTimeoutHandle = setTimeout(this.onInterval.bind(this), this.aggregationTimeout); + } + } + + private updateData(): SubscriptionData { + this.tsKeyNames.forEach((key) => { + this.dataBuffer[key] = []; + }); + for (const key of Object.keys(this.aggregationMap)) { + const aggKeyData = this.aggregationMap[key]; + let keyData = this.dataBuffer[key]; + aggKeyData.forEach((aggData, aggTimestamp) => { + if (aggTimestamp <= this.startTs) { + if (this.stateData && + (!this.lastPrevKvPairData[key] || this.lastPrevKvPairData[key][0] < aggTimestamp)) { + this.lastPrevKvPairData[key] = [aggTimestamp, aggData.aggValue]; + } + aggKeyData.delete(aggTimestamp); + } else if (aggTimestamp <= this.endTs) { + const kvPair: [number, any] = [aggTimestamp, aggData.aggValue]; + keyData.push(kvPair); + } + }); + keyData.sort((set1, set2) => set1[0] - set2[0]); + if (this.stateData) { + this.updateStateBounds(keyData, deepClone(this.lastPrevKvPairData[key])); + } + if (keyData.length > this.limit) { + keyData = keyData.slice(keyData.length - this.limit); + } + this.dataBuffer[key] = keyData; + } + return this.dataBuffer; + } + + private updateStateBounds(keyData: [number, any][], lastPrevKvPair: [number, any]) { + if (lastPrevKvPair) { + lastPrevKvPair[0] = this.startTs; + } + let firstKvPair; + if (!keyData.length) { + if (lastPrevKvPair) { + firstKvPair = lastPrevKvPair; + keyData.push(firstKvPair); + } + } else { + firstKvPair = keyData[0]; + } + if (firstKvPair && firstKvPair[0] > this.startTs) { + if (lastPrevKvPair) { + keyData.unshift(lastPrevKvPair); + } + } + if (keyData.length) { + let lastKvPair = keyData[keyData.length - 1]; + if (lastKvPair[0] < this.endTs) { + lastKvPair = deepClone(lastKvPair); + lastKvPair[0] = this.endTs; + keyData.push(lastKvPair); + } + } + } + + private processAggregatedData(data: SubscriptionData): AggregationMap { + const isCount = this.aggregationType === AggregationType.COUNT; + const aggregationMap: AggregationMap = {}; + for (const key of Object.keys(data)) { + let aggKeyData = aggregationMap[key]; + if (!aggKeyData) { + aggKeyData = new Map(); + aggregationMap[key] = aggKeyData; + } + const keyData = data[key]; + keyData.forEach((kvPair) => { + const timestamp = kvPair[0]; + const value = this.convertValue(kvPair[1]); + const aggKey = timestamp; + const aggData = { + count: isCount ? value : 1, + sum: value, + aggValue: value + }; + aggKeyData.set(aggKey, aggData); + }); + } + return aggregationMap; + } + + private updateAggregatedData(data: SubscriptionData) { + const isCount = this.aggregationType === AggregationType.COUNT; + for (const key of Object.keys(data)) { + let aggKeyData = this.aggregationMap[key]; + if (!aggKeyData) { + aggKeyData = new Map(); + this.aggregationMap[key] = aggKeyData; + } + const keyData = data[key]; + keyData.forEach((kvPair) => { + const timestamp = kvPair[0]; + const value = this.convertValue(kvPair[1]); + const aggTimestamp = this.noAggregation ? timestamp : (this.startTs + + Math.floor((timestamp - this.startTs) / this.interval) * this.interval + this.interval / 2); + let aggData = aggKeyData.get(aggTimestamp); + if (!aggData) { + aggData = { + count: 1, + sum: value, + aggValue: isCount ? 1 : value + }; + aggKeyData.set(aggTimestamp, aggData); + } else { + this.aggFunction(aggData, value); + } + }); + } + } + + private isNumeric(val: any): boolean { + return (val - parseFloat( val ) + 1) >= 0; + } + + private convertValue(val: string): any { + if (!this.noAggregation || val && this.isNumeric(val)) { + return Number(val); + } else { + return val; + } + } + +} + diff --git a/ui-ngx/src/app/core/api/entity-data-subscription.ts b/ui-ngx/src/app/core/api/entity-data-subscription.ts new file mode 100644 index 0000000000..6d2f764d95 --- /dev/null +++ b/ui-ngx/src/app/core/api/entity-data-subscription.ts @@ -0,0 +1,778 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { DataSet, DataSetHolder, DatasourceType, widgetType } from '@shared/models/widget.models'; +import { AggregationType, SubscriptionTimewindow } from '@shared/models/time/time.models'; +import { + EntityData, + EntityDataPageLink, + EntityFilter, + EntityKey, + EntityKeyType, + entityKeyTypeToDataKeyType, entityPageDataChanged, + KeyFilter, + TsValue +} from '@shared/models/query/query.models'; +import { + DataKeyType, + EntityDataCmd, + SubscriptionData, + SubscriptionDataHolder, + TelemetryService, + TelemetrySubscriber +} from '@shared/models/telemetry/telemetry.models'; +import { UtilsService } from '@core/services/utils.service'; +import { EntityDataListener, EntityDataLoadResult } from '@core/api/entity-data.service'; +import { deepClone, isDefined, isDefinedAndNotNull, isObject, objectHashCode } from '@core/utils'; +import { PageData } from '@shared/models/page/page-data'; +import { DataAggregator } from '@core/api/data-aggregator'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { EntityType } from '@shared/models/entity-type.models'; +import { Observable, of, ReplaySubject, Subject } from 'rxjs'; +import Timeout = NodeJS.Timeout; + +declare type DataKeyFunction = (time: number, prevValue: any) => any; +declare type DataKeyPostFunction = (time: number, value: any, prevValue: any, timePrev: number, prevOrigValue: any) => any; +declare type DataUpdatedCb = (data: DataSetHolder, dataIndex: number, dataKeyIndex: number, detectChanges: boolean) => void; + +export interface SubscriptionDataKey { + name: string; + type: DataKeyType; + funcBody: string; + func?: DataKeyFunction; + postFuncBody: string; + postFunc?: DataKeyPostFunction; + index?: number; + key?: string; + lastUpdateTime?: number; +} + +export interface EntityDataSubscriptionOptions { + datasourceType: DatasourceType; + dataKeys: Array; + type: widgetType; + entityFilter?: EntityFilter; + isPaginatedDataSubscription?: boolean; + pageLink?: EntityDataPageLink; + keyFilters?: Array; + additionalKeyFilters?: Array; + subscriptionTimewindow?: SubscriptionTimewindow; +} + +export class EntityDataSubscription { + + private entityDataSubscriptionOptions = this.listener.subscriptionOptions; + private datasourceType: DatasourceType = this.entityDataSubscriptionOptions.datasourceType; + private history: boolean; + private realtime: boolean; + + private subscriber: TelemetrySubscriber; + private dataCommand: EntityDataCmd; + private subsCommand: EntityDataCmd; + + private attrFields: Array; + private tsFields: Array; + private latestValues: Array; + + private entityDataResolveSubject: Subject; + private pageData: PageData; + private subsTw: SubscriptionTimewindow; + private dataAggregators: Array; + private dataKeys: {[key: string]: Array | SubscriptionDataKey} = {} + private datasourceData: {[index: number]: {[key: string]: DataSetHolder}}; + private datasourceOrigData: {[index: number]: {[key: string]: DataSetHolder}}; + private entityIdToDataIndex: {[id: string]: number}; + + private frequency: number; + private tickScheduledTime = 0; + private tickElapsed = 0; + private timer: Timeout; + + private dataResolved = false; + private started = false; + + constructor(private listener: EntityDataListener, + private telemetryService: TelemetryService, + private utils: UtilsService) { + this.initializeSubscription(); + } + + private initializeSubscription() { + for (let i = 0; i < this.entityDataSubscriptionOptions.dataKeys.length; i++) { + const dataKey = deepClone(this.entityDataSubscriptionOptions.dataKeys[i]); + dataKey.index = i; + if (this.datasourceType === DatasourceType.function) { + if (!dataKey.func) { + dataKey.func = new Function('time', 'prevValue', dataKey.funcBody) as DataKeyFunction; + } + } else { + if (dataKey.postFuncBody && !dataKey.postFunc) { + dataKey.postFunc = new Function('time', 'value', 'prevValue', 'timePrev', 'prevOrigValue', + dataKey.postFuncBody) as DataKeyPostFunction; + } + } + let key: string; + if (this.datasourceType === DatasourceType.entity || this.entityDataSubscriptionOptions.type === widgetType.timeseries) { + if (this.datasourceType === DatasourceType.function) { + key = `${dataKey.name}_${dataKey.index}_${dataKey.type}`; + } else { + key = `${dataKey.name}_${dataKey.type}`; + } + let dataKeysList = this.dataKeys[key] as Array; + if (!dataKeysList) { + dataKeysList = []; + this.dataKeys[key] = dataKeysList; + } + dataKeysList.push(dataKey); + } else { + key = String(objectHashCode(dataKey)); + this.dataKeys[key] = dataKey; + } + dataKey.key = key; + } + } + + public unsubscribe() { + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + if (this.datasourceType === DatasourceType.entity) { + if (this.subscriber) { + this.subscriber.unsubscribe(); + this.subscriber = null; + } + } + if (this.dataAggregators) { + this.dataAggregators.forEach((aggregator) => { + aggregator.destroy(); + }) + this.dataAggregators = null; + } + this.pageData = null; + } + + public subscribe(): Observable { + this.entityDataResolveSubject = new ReplaySubject(1); + if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { + this.started = true; + this.dataResolved = true; + this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow; + this.history = this.entityDataSubscriptionOptions.subscriptionTimewindow && + isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow); + this.realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow && + isDefinedAndNotNull(this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs); + } + if (this.datasourceType === DatasourceType.entity) { + const entityFields: Array = + this.entityDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.entityField).map( + dataKey => ({ type: EntityKeyType.ENTITY_FIELD, key: dataKey.name }) + ); + if (!entityFields.find(key => key.key === 'name')) { + entityFields.push({ + type: EntityKeyType.ENTITY_FIELD, + key: 'name' + }); + } + if (!entityFields.find(key => key.key === 'label')) { + entityFields.push({ + type: EntityKeyType.ENTITY_FIELD, + key: 'label' + }); + } + if (!entityFields.find(key => key.key === 'additionalInfo')) { + entityFields.push({ + type: EntityKeyType.ENTITY_FIELD, + key: 'additionalInfo' + }); + } + + this.attrFields = this.entityDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.attribute).map( + dataKey => ({ type: EntityKeyType.ATTRIBUTE, key: dataKey.name }) + ); + + this.tsFields = this.entityDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.timeseries).map( + dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name }) + ); + + this.latestValues = this.attrFields.concat(this.tsFields); + + this.subscriber = new TelemetrySubscriber(this.telemetryService); + this.dataCommand = new EntityDataCmd(); + + let keyFilters = this.entityDataSubscriptionOptions.keyFilters; + if (this.entityDataSubscriptionOptions.additionalKeyFilters) { + if (keyFilters) { + keyFilters = keyFilters.concat(this.entityDataSubscriptionOptions.additionalKeyFilters); + } else { + keyFilters = this.entityDataSubscriptionOptions.additionalKeyFilters; + } + } + + this.dataCommand.query = { + entityFilter: this.entityDataSubscriptionOptions.entityFilter, + pageLink: this.entityDataSubscriptionOptions.pageLink, + keyFilters, + entityFields, + latestValues: this.latestValues + }; + + if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { + this.prepareSubscriptionCommands(this.dataCommand); + } + + this.subscriber.subscriptionCommands.push(this.dataCommand); + + this.subscriber.entityData$.subscribe( + (entityDataUpdate) => { + if (entityDataUpdate.data) { + this.onPageData(entityDataUpdate.data); + } else if (entityDataUpdate.update) { + this.onDataUpdate(entityDataUpdate.update); + } + } + ); + + this.subscriber.reconnect$.subscribe(() => { + if (this.started) { + const targetCommand = this.entityDataSubscriptionOptions.isPaginatedDataSubscription ? this.dataCommand : this.subsCommand; + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && + !this.history && this.tsFields.length) { + const newSubsTw: SubscriptionTimewindow = this.listener.updateRealtimeSubscription(); + this.subsTw = newSubsTw; + targetCommand.tsCmd.startTs = this.subsTw.startTs; + targetCommand.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow; + targetCommand.tsCmd.interval = this.subsTw.aggregation.interval; + targetCommand.tsCmd.limit = this.subsTw.aggregation.limit; + targetCommand.tsCmd.agg = this.subsTw.aggregation.type; + targetCommand.tsCmd.fetchLatestPreviousPoint = this.subsTw.aggregation.stateData; + this.dataAggregators.forEach((dataAggregator) => { + dataAggregator.reset(newSubsTw.startTs, newSubsTw.aggregation.timeWindow, newSubsTw.aggregation.interval); + }); + } + targetCommand.query = this.dataCommand.query; + this.subscriber.subscriptionCommands = [targetCommand]; + } else { + this.subscriber.subscriptionCommands = [this.dataCommand]; + } + }); + + this.subscriber.subscribe(); + } else if (this.datasourceType === DatasourceType.function) { + const entityData: EntityData = { + entityId: { + id: NULL_UUID, + entityType: EntityType.DEVICE + }, + timeseries: {}, + latest: {} + }; + const name = DatasourceType.function; + entityData.latest[EntityKeyType.ENTITY_FIELD] = { + name: {ts: Date.now(), value: name} + }; + const pageData: PageData = { + data: [entityData], + hasNext: false, + totalElements: 1, + totalPages: 1 + }; + this.onPageData(pageData); + } + if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { + return of(null); + } else { + return this.entityDataResolveSubject.asObservable(); + } + } + + public start() { + if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { + return; + } + this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow; + this.history = this.entityDataSubscriptionOptions.subscriptionTimewindow && + isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow); + this.realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow && + isDefinedAndNotNull(this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs); + + this.prepareData(); + + if (this.datasourceType === DatasourceType.entity) { + this.subsCommand = new EntityDataCmd(); + this.subsCommand.cmdId = this.dataCommand.cmdId; + this.prepareSubscriptionCommands(this.subsCommand); + if (!this.subsCommand.isEmpty()) { + this.subscriber.subscriptionCommands = [this.subsCommand]; + this.subscriber.update(); + } + } else if (this.datasourceType === DatasourceType.function) { + this.startFunction(); + } + this.started = true; + } + + private prepareSubscriptionCommands(cmd: EntityDataCmd) { + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { + if (this.tsFields.length > 0) { + if (this.history) { + cmd.historyCmd = { + keys: this.tsFields.map(key => key.key), + startTs: this.subsTw.fixedWindow.startTimeMs, + endTs: this.subsTw.fixedWindow.endTimeMs, + interval: this.subsTw.aggregation.interval, + limit: this.subsTw.aggregation.limit, + agg: this.subsTw.aggregation.type, + fetchLatestPreviousPoint: this.subsTw.aggregation.stateData + }; + } else { + cmd.tsCmd = { + keys: this.tsFields.map(key => key.key), + startTs: this.subsTw.startTs, + timeWindow: this.subsTw.aggregation.timeWindow, + interval: this.subsTw.aggregation.interval, + limit: this.subsTw.aggregation.limit, + agg: this.subsTw.aggregation.type, + fetchLatestPreviousPoint: this.subsTw.aggregation.stateData + } + } + } + } else if (this.entityDataSubscriptionOptions.type === widgetType.latest) { + if (this.latestValues.length > 0) { + cmd.latestCmd = { + keys: this.latestValues + }; + } + } + } + + private startFunction() { + this.frequency = 1000; + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { + this.frequency = Math.min(this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.interval, 5000); + } + this.tickScheduledTime = this.utils.currentPerfTime(); + if (this.history) { + this.onTick(true); + } else { + this.timer = setTimeout(this.onTick.bind(this, true), 0); + } + } + + private prepareData() { + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + + if (this.dataAggregators) { + this.dataAggregators.forEach((aggregator) => { + aggregator.destroy(); + }) + } + this.dataAggregators = []; + this.resetData(); + + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { + let tsKeyNames = []; + if (this.datasourceType === DatasourceType.function) { + for (const key of Object.keys(this.dataKeys)) { + const dataKeysList = this.dataKeys[key] as Array; + dataKeysList.forEach((subscriptionDataKey) => { + tsKeyNames.push(`${subscriptionDataKey.name}_${subscriptionDataKey.index}`); + }); + } + } else { + tsKeyNames = this.tsFields ? this.tsFields.map(field => field.key) : []; + } + for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) { + if (this.datasourceType === DatasourceType.function) { + this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, tsKeyNames, + DataKeyType.function, dataIndex, this.notifyListener.bind(this)); + } else if (tsKeyNames.length) { + this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, tsKeyNames, + DataKeyType.timeseries, dataIndex, this.notifyListener.bind(this)); + } + } + } + } + + private resetData() { + this.datasourceData = []; + this.entityIdToDataIndex = {}; + for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) { + const entityData = this.pageData.data[dataIndex]; + this.entityIdToDataIndex[entityData.entityId.id] = dataIndex; + this.datasourceData[dataIndex] = {}; + for (const key of Object.keys(this.dataKeys)) { + const dataKey = this.dataKeys[key]; + if (this.datasourceType === DatasourceType.entity || this.entityDataSubscriptionOptions.type === widgetType.timeseries) { + const dataKeysList = dataKey as Array; + for (let index = 0; index < dataKeysList.length; index++) { + this.datasourceData[dataIndex][key + '_' + index] = { + data: [] + }; + } + } else { + this.datasourceData[dataIndex][key] = { + data: [] + }; + } + } + } + this.datasourceOrigData = deepClone(this.datasourceData); + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { + for (const key of Object.keys(this.dataKeys)) { + const dataKeyList = this.dataKeys[key] as Array; + dataKeyList.forEach((dataKey) => { + delete dataKey.lastUpdateTime; + }); + } + } else if (this.entityDataSubscriptionOptions.type === widgetType.latest) { + for (const key of Object.keys(this.dataKeys)) { + delete (this.dataKeys[key] as SubscriptionDataKey).lastUpdateTime; + } + } + } + + private onPageData(pageData: PageData) { + const isInitialData = !this.pageData; + if (!isInitialData && !this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { + if (entityPageDataChanged(this.pageData, pageData)) { + if (this.listener.initialPageDataChanged) { + this.listener.initialPageDataChanged(pageData); + } + return; + } + } + this.pageData = pageData; + + if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { + this.prepareData(); + } else if (isInitialData) { + this.resetData(); + } + const data: Array> = []; + for (let dataIndex = 0; dataIndex < pageData.data.length; dataIndex++) { + const entityData = pageData.data[dataIndex]; + this.processEntityData(entityData, dataIndex, false, + (data1, dataIndex1, dataKeyIndex) => { + if (!data[dataIndex1]) { + data[dataIndex1] = []; + } + data[dataIndex1][dataKeyIndex] = data1; + } + ); + } + if (!this.dataResolved) { + this.dataResolved = true; + this.entityDataResolveSubject.next( + { + pageData, + data, + datasourceIndex: this.listener.configDatasourceIndex, + pageLink: this.entityDataSubscriptionOptions.pageLink + } + ); + this.entityDataResolveSubject.complete(); + } else { + if (isInitialData || this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { + this.listener.dataLoaded(pageData, data, + this.listener.configDatasourceIndex, this.entityDataSubscriptionOptions.pageLink); + } + if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription && isInitialData) { + if (this.datasourceType === DatasourceType.function) { + this.startFunction(); + } + this.entityDataResolveSubject.next( + { + pageData, + data, + datasourceIndex: this.listener.configDatasourceIndex, + pageLink: this.entityDataSubscriptionOptions.pageLink + } + ); + this.entityDataResolveSubject.complete(); + } + } + } + + private onDataUpdate(update: Array) { + for (const entityData of update) { + const dataIndex = this.entityIdToDataIndex[entityData.entityId.id]; + if (isDefined(dataIndex) && dataIndex >= 0) { + this.processEntityData(entityData, dataIndex, true, this.notifyListener.bind(this)); + } + } + } + + private notifyListener(data: DataSetHolder, dataIndex: number, dataKeyIndex: number, detectChanges: boolean) { + this.listener.dataUpdated(data, + this.listener.configDatasourceIndex, + dataIndex, dataKeyIndex, detectChanges); + } + + private processEntityData(entityData: EntityData, dataIndex: number, isUpdate: boolean, + dataUpdatedCb: DataUpdatedCb) { + if (this.entityDataSubscriptionOptions.type === widgetType.latest && entityData.latest) { + for (const type of Object.keys(entityData.latest)) { + const subscriptionData = this.toSubscriptionData(entityData.latest[type], false); + const dataKeyType = entityKeyTypeToDataKeyType(EntityKeyType[type]); + this.onData(subscriptionData, dataKeyType, dataIndex, true, dataUpdatedCb); + } + } + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && entityData.timeseries) { + const subscriptionData = this.toSubscriptionData(entityData.timeseries, true); + if (this.dataAggregators && this.dataAggregators[dataIndex]) { + const dataAggregator = this.dataAggregators[dataIndex]; + let prevDataCb; + if (!isUpdate) { + prevDataCb = dataAggregator.updateOnDataCb((data, detectChanges) => { + this.onData(data, this.datasourceType === DatasourceType.function ? + DataKeyType.function : DataKeyType.timeseries, dataIndex, detectChanges, dataUpdatedCb); + }); + } + dataAggregator.onData({data: subscriptionData}, false, this.history, true); + if (prevDataCb) { + dataAggregator.updateOnDataCb(prevDataCb); + } + } + if (!this.history && !isUpdate) { + this.onData(subscriptionData, DataKeyType.timeseries, dataIndex, true, dataUpdatedCb); + } + } + } + + private onData(sourceData: SubscriptionData, type: DataKeyType, dataIndex: number, detectChanges: boolean, + dataUpdatedCb: DataUpdatedCb) { + for (const keyName of Object.keys(sourceData)) { + const keyData = sourceData[keyName]; + const key = `${keyName}_${type}`; + const dataKeyList = this.dataKeys[key] as Array; + for (let keyIndex = 0; dataKeyList && keyIndex < dataKeyList.length; keyIndex++) { + const datasourceKey = `${key}_${keyIndex}`; + if (this.datasourceData[dataIndex][datasourceKey].data) { + const dataKey = dataKeyList[keyIndex]; + const data: DataSet = []; + let prevSeries: [number, any]; + let prevOrigSeries: [number, any]; + let datasourceKeyData: DataSet; + let datasourceOrigKeyData: DataSet; + let update = false; + if (this.realtime) { + datasourceKeyData = []; + datasourceOrigKeyData = []; + } else { + datasourceKeyData = this.datasourceData[dataIndex][datasourceKey].data; + datasourceOrigKeyData = this.datasourceOrigData[dataIndex][datasourceKey].data; + } + if (datasourceKeyData.length > 0) { + prevSeries = datasourceKeyData[datasourceKeyData.length - 1]; + prevOrigSeries = datasourceOrigKeyData[datasourceOrigKeyData.length - 1]; + } else { + prevSeries = [0, 0]; + prevOrigSeries = [0, 0]; + } + this.datasourceOrigData[dataIndex][datasourceKey].data = []; + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { + keyData.forEach((keySeries) => { + let series = keySeries; + const time = series[0]; + this.datasourceOrigData[dataIndex][datasourceKey].data.push(series); + let value = this.convertValue(series[1]); + if (dataKey.postFunc) { + value = dataKey.postFunc(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]); + } + prevOrigSeries = series; + series = [time, value]; + data.push(series); + prevSeries = series; + }); + update = true; + } else if (this.entityDataSubscriptionOptions.type === widgetType.latest) { + if (keyData.length > 0) { + let series = keyData[0]; + const time = series[0]; + this.datasourceOrigData[dataIndex][datasourceKey].data.push(series); + let value = this.convertValue(series[1]); + if (dataKey.postFunc) { + value = dataKey.postFunc(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]); + } + series = [time, value]; + data.push(series); + } + update = true; + } + if (update) { + this.datasourceData[dataIndex][datasourceKey].data = data; + dataUpdatedCb(this.datasourceData[dataIndex][datasourceKey], dataIndex, dataKey.index, detectChanges); + } + } + } + } + } + + private isNumeric(val: any): boolean { + return (val - parseFloat( val ) + 1) >= 0; + } + + private convertValue(val: string): any { + if (val && this.isNumeric(val) && Number(val).toString() === val) { + return Number(val); + } else { + return val; + } + } + + private toSubscriptionData(sourceData: {[key: string]: TsValue | TsValue[]}, isTs: boolean): SubscriptionData { + const subsData: SubscriptionData = {}; + for (const keyName of Object.keys(sourceData)) { + const values = sourceData[keyName]; + const dataSet: [number, any][] = []; + if (isTs) { + (values as TsValue[]).forEach((keySeries) => { + dataSet.push([keySeries.ts, keySeries.value]); + }); + } else { + const tsValue = values as TsValue; + dataSet.push([tsValue.ts, tsValue.value]); + } + subsData[keyName] = dataSet; + } + return subsData; + } + + private createRealtimeDataAggregator(subsTw: SubscriptionTimewindow, + tsKeyNames: Array, + dataKeyType: DataKeyType, + dataIndex: number, + dataUpdatedCb: DataUpdatedCb): DataAggregator { + return new DataAggregator( + (data, detectChanges) => { + this.onData(data, dataKeyType, dataIndex, detectChanges, dataUpdatedCb); + }, + tsKeyNames, + subsTw.startTs, + subsTw.aggregation.limit, + subsTw.aggregation.type, + subsTw.aggregation.timeWindow, + subsTw.aggregation.interval, + subsTw.aggregation.stateData, + this.utils + ); + } + + private generateSeries(dataKey: SubscriptionDataKey, index: number, startTime: number, endTime: number): [number, any][] { + const data: [number, any][] = []; + let prevSeries: [number, any]; + const datasourceDataKey = `${dataKey.key}_${index}`; + const datasourceKeyData = this.datasourceData[0][datasourceDataKey].data; + if (datasourceKeyData.length > 0) { + prevSeries = datasourceKeyData[datasourceKeyData.length - 1]; + } else { + prevSeries = [0, 0]; + } + for (let time = startTime; time <= endTime && (this.timer || this.history); time += this.frequency) { + const value = dataKey.func(time, prevSeries[1]); + const series: [number, any] = [time, value]; + data.push(series); + prevSeries = series; + } + if (data.length > 0) { + dataKey.lastUpdateTime = data[data.length - 1][0]; + } + return data; + } + + private generateLatest(dataKey: SubscriptionDataKey, detectChanges: boolean) { + let prevSeries: [number, any]; + const datasourceKeyData = this.datasourceData[0][dataKey.key].data; + if (datasourceKeyData.length > 0) { + prevSeries = datasourceKeyData[datasourceKeyData.length - 1]; + } else { + prevSeries = [0, 0]; + } + const time = Date.now(); + const value = dataKey.func(time, prevSeries[1]); + const series: [number, any] = [time, value]; + this.datasourceData[0][dataKey.key].data = [series]; + this.listener.dataUpdated(this.datasourceData[0][dataKey.key], + this.listener.configDatasourceIndex, + 0, + dataKey.index, detectChanges); + } + + private onTick(detectChanges: boolean) { + const now = this.utils.currentPerfTime(); + this.tickElapsed += now - this.tickScheduledTime; + this.tickScheduledTime = now; + + if (this.timer) { + clearTimeout(this.timer); + } + let key: string; + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { + let startTime: number; + let endTime: number; + let delta: number; + const generatedData: SubscriptionDataHolder = { + data: {} + }; + if (!this.history) { + delta = Math.floor(this.tickElapsed / this.frequency); + } + const deltaElapsed = this.history ? this.frequency : delta * this.frequency; + this.tickElapsed = this.tickElapsed - deltaElapsed; + for (key of Object.keys(this.dataKeys)) { + const dataKeyList = this.dataKeys[key] as Array; + for (let index = 0; index < dataKeyList.length && (this.timer || this.history); index ++) { + const dataKey = dataKeyList[index]; + if (!startTime) { + if (this.realtime) { + if (dataKey.lastUpdateTime) { + startTime = dataKey.lastUpdateTime + this.frequency; + endTime = dataKey.lastUpdateTime + deltaElapsed; + } else { + startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.startTs; + endTime = startTime + this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs + this.frequency; + if (this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.type === AggregationType.NONE) { + const time = endTime - this.frequency * this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.limit; + startTime = Math.max(time, startTime); + } + } + } else { + startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.startTimeMs; + endTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.endTimeMs; + } + } + generatedData.data[`${dataKey.name}_${dataKey.index}`] = this.generateSeries(dataKey, index, startTime, endTime); + } + } + if (this.dataAggregators && this.dataAggregators.length) { + this.dataAggregators[0].onData(generatedData, true, this.history, detectChanges); + } + } else if (this.entityDataSubscriptionOptions.type === widgetType.latest) { + for (key of Object.keys(this.dataKeys)) { + this.generateLatest(this.dataKeys[key] as SubscriptionDataKey, detectChanges); + } + } + + if (!this.history) { + this.timer = setTimeout(this.onTick.bind(this, true), this.frequency); + } + } + +} diff --git a/ui-ngx/src/app/core/api/entity-data.service.ts b/ui-ngx/src/app/core/api/entity-data.service.ts new file mode 100644 index 0000000000..a5ef7c2c02 --- /dev/null +++ b/ui-ngx/src/app/core/api/entity-data.service.ts @@ -0,0 +1,147 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { DataSetHolder, Datasource, DatasourceType, widgetType } from '@shared/models/widget.models'; +import { SubscriptionTimewindow } from '@shared/models/time/time.models'; +import { EntityData, EntityDataPageLink, KeyFilter } from '@shared/models/query/query.models'; +import { emptyPageData, PageData } from '@shared/models/page/page-data'; +import { Injectable } from '@angular/core'; +import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service'; +import { UtilsService } from '@core/services/utils.service'; +import { deepClone } from '@core/utils'; +import { + EntityDataSubscription, + EntityDataSubscriptionOptions, + SubscriptionDataKey +} from '@core/api/entity-data-subscription'; +import { Observable, of } from 'rxjs'; + +export interface EntityDataListener { + subscriptionType: widgetType; + subscriptionTimewindow?: SubscriptionTimewindow; + configDatasource: Datasource; + configDatasourceIndex: number; + dataLoaded: (pageData: PageData, + data: Array>, + datasourceIndex: number, pageLink: EntityDataPageLink) => void; + dataUpdated: (data: DataSetHolder, datasourceIndex: number, dataIndex: number, dataKeyIndex: number, detectChanges: boolean) => void; + initialPageDataChanged?: (nextPageData: PageData) => void; + updateRealtimeSubscription?: () => SubscriptionTimewindow; + setRealtimeSubscription?: (subscriptionTimewindow: SubscriptionTimewindow) => void; + subscriptionOptions?: EntityDataSubscriptionOptions; + subscription?: EntityDataSubscription; +} + +export interface EntityDataLoadResult { + pageData: PageData; + data: Array>; + datasourceIndex: number; + pageLink: EntityDataPageLink; +} + +@Injectable({ + providedIn: 'root' +}) +export class EntityDataService { + + constructor(private telemetryService: TelemetryWebsocketService, + private utils: UtilsService) {} + + public prepareSubscription(listener: EntityDataListener): Observable { + const datasource = listener.configDatasource; + listener.subscriptionOptions = this.createSubscriptionOptions( + datasource, + listener.subscriptionType, + datasource.pageLink, + datasource.keyFilters, + null, + false); + if (datasource.type === DatasourceType.entity && (!datasource.entityFilter || !datasource.pageLink)) { + return of(null); + } + listener.subscription = new EntityDataSubscription(listener, this.telemetryService, this.utils); + return listener.subscription.subscribe(); + } + + public startSubscription(listener: EntityDataListener) { + if (listener.subscription) { + if (listener.subscriptionType === widgetType.timeseries) { + listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow); + } + listener.subscription.start(); + } + } + + public subscribeForPaginatedData(listener: EntityDataListener, + pageLink: EntityDataPageLink, + keyFilters: KeyFilter[]): Observable { + const datasource = listener.configDatasource; + listener.subscriptionOptions = this.createSubscriptionOptions( + datasource, + listener.subscriptionType, + pageLink, + datasource.keyFilters, + keyFilters, + true); + if (datasource.type === DatasourceType.entity && (!datasource.entityFilter || !pageLink)) { + listener.dataLoaded(emptyPageData(), [], + listener.configDatasourceIndex, listener.subscriptionOptions.pageLink); + return of(null); + } + listener.subscription = new EntityDataSubscription(listener, this.telemetryService, this.utils); + if (listener.subscriptionType === widgetType.timeseries) { + listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow); + } + return listener.subscription.subscribe(); + } + + public stopSubscription(listener: EntityDataListener) { + if (listener.subscription) { + listener.subscription.unsubscribe(); + } + } + + private createSubscriptionOptions(datasource: Datasource, + subscriptionType: widgetType, + pageLink: EntityDataPageLink, + keyFilters: KeyFilter[], + additionalKeyFilters: KeyFilter[], + isPaginatedDataSubscription: boolean): EntityDataSubscriptionOptions { + const subscriptionDataKeys: Array = []; + datasource.dataKeys.forEach((dataKey) => { + const subscriptionDataKey: SubscriptionDataKey = { + name: dataKey.name, + type: dataKey.type, + funcBody: dataKey.funcBody, + postFuncBody: dataKey.postFuncBody + }; + subscriptionDataKeys.push(subscriptionDataKey); + }); + const entityDataSubscriptionOptions: EntityDataSubscriptionOptions = { + datasourceType: datasource.type, + dataKeys: subscriptionDataKeys, + type: subscriptionType + }; + if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity) { + entityDataSubscriptionOptions.entityFilter = datasource.entityFilter; + entityDataSubscriptionOptions.pageLink = pageLink; + entityDataSubscriptionOptions.keyFilters = keyFilters; + entityDataSubscriptionOptions.additionalKeyFilters = additionalKeyFilters; + } + entityDataSubscriptionOptions.isPaginatedDataSubscription = isPaginatedDataSubscription; + return entityDataSubscriptionOptions; + } +} diff --git a/ui-ngx/src/app/core/api/public-api.ts b/ui-ngx/src/app/core/api/public-api.ts new file mode 100644 index 0000000000..a2bf6769b2 --- /dev/null +++ b/ui-ngx/src/app/core/api/public-api.ts @@ -0,0 +1,22 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export * from './alias-controller'; +export * from './data-aggregator'; +export * from './entity-data.service'; +export * from './entity-data-subscription'; +export * from './widget-api.models'; +export * from './widget-subscription'; diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts new file mode 100644 index 0000000000..e4b9deae89 --- /dev/null +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -0,0 +1,319 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Observable } from 'rxjs'; +import { EntityId } from '@app/shared/models/id/entity-id'; +import { + DataSet, + Datasource, + DatasourceData, + DatasourceType, + KeyInfo, + LegendConfig, + LegendData, + WidgetActionDescriptor, + widgetType +} from '@shared/models/widget.models'; +import { TimeService } from '../services/time.service'; +import { DeviceService } from '../http/device.service'; +import { UtilsService } from '@core/services/utils.service'; +import { Timewindow, WidgetTimewindow } from '@shared/models/time/time.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { HttpErrorResponse } from '@angular/common/http'; +import { RafService } from '@core/services/raf.service'; +import { EntityAliases } from '@shared/models/alias.models'; +import { EntityInfo } from '@app/shared/models/entity.models'; +import { IDashboardComponent } from '@home/models/dashboard-component.models'; +import * as moment_ from 'moment'; +import { + AlarmData, + AlarmDataPageLink, + EntityData, + EntityDataPageLink, + EntityFilter, + Filter, + FilterInfo, + Filters, + KeyFilter +} from '@shared/models/query/query.models'; +import { EntityDataService } from '@core/api/entity-data.service'; +import { PageData } from '@shared/models/page/page-data'; +import { TranslateService } from '@ngx-translate/core'; +import { AlarmDataService } from '@core/api/alarm-data.service'; + +export interface TimewindowFunctions { + onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void; + onResetTimewindow: () => void; +} + +export interface WidgetSubscriptionApi { + createSubscription: (options: WidgetSubscriptionOptions, subscribe?: boolean) => Observable; + createSubscriptionFromInfo: (type: widgetType, subscriptionsInfo: Array, + options: WidgetSubscriptionOptions, useDefaultComponents: boolean, subscribe: boolean) + => Observable; + removeSubscription: (id: string) => void; +} + +export interface RpcApi { + sendOneWayCommand: (method: string, params?: any, timeout?: number) => Observable; + sendTwoWayCommand: (method: string, params?: any, timeout?: number) => Observable; +} + +export interface IWidgetUtils { + formatValue: (value: any, dec?: number, units?: string, showZeroDecimals?: boolean) => string | undefined; +} + +export interface WidgetActionsApi { + actionDescriptorsBySourceId: {[sourceId: string]: Array}; + getActionDescriptors: (actionSourceId: string) => Array; + handleWidgetAction: ($event: Event, descriptor: WidgetActionDescriptor, + entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) => void; + elementClick: ($event: Event) => void; + getActiveEntityInfo: () => SubscriptionEntityInfo; +} + +export interface AliasInfo { + alias?: string; + stateEntity?: boolean; + entityFilter?: EntityFilter; + currentEntity?: EntityInfo; + entityParamName?: string; + resolveMultiple?: boolean; +} + +export interface StateEntityInfo { + entityParamName: string; + entityId: EntityId; +} + +export interface IAliasController { + entityAliasesChanged: Observable>; + entityAliasResolved: Observable; + filtersChanged: Observable>; + getAliasInfo(aliasId: string): Observable; + getEntityAliasId(aliasName: string): string; + getInstantAliasInfo(aliasId: string): AliasInfo; + resolveSingleEntityInfo(aliasId: string): Observable; + resolveDatasources(datasources: Array, singleEntity?: boolean): Observable>; + resolveAlarmSource(alarmSource: Datasource): Observable; + getEntityAliases(): EntityAliases; + getFilters(): Filters; + getFilterInfo(filterId: string): FilterInfo; + getKeyFilters(filterId: string): Array; + updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo); + updateUserFilter(filter: Filter); + updateEntityAliases(entityAliases: EntityAliases); + updateFilters(filters: Filters); + updateAliases(aliasIds?: Array); + dashboardStateChanged(); +} + +export interface StateObject { + id?: string; + params?: StateParams; +} + +export interface StateParams { + entityName?: string; + entityLabel?: string; + targetEntityParamName?: string; + entityId?: EntityId; + [key: string]: any | null; +} + +export type StateControllerHolder = () => IStateController; + +export interface IStateController { + getStateParams(): StateParams; + getStateParamsByStateId(stateId: string): StateParams; + openState(id: string, params?: StateParams, openRightLayout?: boolean): void; + updateState(id?: string, params?: StateParams, openRightLayout?: boolean): void; + resetState(): void; + openRightLayout(): void; + preserveState(): void; + cleanupPreservedStates(): void; + navigatePrevState(index: number): void; + getStateId(): string; + getStateIndex(): number; + getStateIdAtIndex(index: number): string; + getEntityId(entityParamName: string): EntityId; +} + +export interface SubscriptionInfo { + type: DatasourceType; + name?: string; + entityType?: EntityType; + entityId?: string; + entityIds?: Array; + entityName?: string; + entityNamePrefix?: string; + timeseries?: Array; + attributes?: Array; + functions?: Array; + alarmFields?: Array; + + deviceId?: string; + deviceName?: string; + deviceNamePrefix?: string; + deviceIds?: Array; +} + +export class WidgetSubscriptionContext { + + constructor(private dashboard: IDashboardComponent) {} + + get aliasController(): IAliasController { + return this.dashboard.aliasController; + } + + dashboardTimewindowApi: TimewindowFunctions = { + onResetTimewindow: this.dashboard.onResetTimewindow.bind(this.dashboard), + onUpdateTimewindow: this.dashboard.onUpdateTimewindow.bind(this.dashboard) + }; + + timeService: TimeService; + deviceService: DeviceService; + translate: TranslateService; + entityDataService: EntityDataService; + alarmDataService: AlarmDataService; + utils: UtilsService; + raf: RafService; + widgetUtils: IWidgetUtils; + getServerTimeDiff: () => Observable; +} + +export type SubscriptionMessageSeverity = 'info' | 'warn' | 'error' | 'success'; + +export interface SubscriptionMessage { + severity: SubscriptionMessageSeverity, + message: string; +} + +export interface WidgetSubscriptionCallbacks { + onDataUpdated?: (subscription: IWidgetSubscription, detectChanges: boolean) => void; + onDataUpdateError?: (subscription: IWidgetSubscription, e: any) => void; + onSubscriptionMessage?: (subscription: IWidgetSubscription, message: SubscriptionMessage) => void; + onInitialPageDataChanged?: (subscription: IWidgetSubscription, nextPageData: PageData) => void; + dataLoading?: (subscription: IWidgetSubscription) => void; + legendDataUpdated?: (subscription: IWidgetSubscription, detectChanges: boolean) => void; + timeWindowUpdated?: (subscription: IWidgetSubscription, timeWindowConfig: Timewindow) => void; + rpcStateChanged?: (subscription: IWidgetSubscription) => void; + onRpcSuccess?: (subscription: IWidgetSubscription) => void; + onRpcFailed?: (subscription: IWidgetSubscription) => void; + onRpcErrorCleared?: (subscription: IWidgetSubscription) => void; +} + +export interface WidgetSubscriptionOptions { + type?: widgetType; + stateData?: boolean; + alarmSource?: Datasource; + datasources?: Array; + hasDataPageLink?: boolean; + singleEntity?: boolean; + warnOnPageDataOverflow?: boolean; + targetDeviceAliasIds?: Array; + targetDeviceIds?: Array; + useDashboardTimewindow?: boolean; + displayTimewindow?: boolean; + timeWindowConfig?: Timewindow; + dashboardTimewindow?: Timewindow; + legendConfig?: LegendConfig; + comparisonEnabled?: boolean; + timeForComparison?: moment_.unitOfTime.DurationConstructor; + decimals?: number; + units?: string; + callbacks?: WidgetSubscriptionCallbacks; +} + +export interface SubscriptionEntityInfo { + entityId: EntityId; + entityName: string; + entityLabel: string; + entityDescription: string; +} + +export interface IWidgetSubscription { + + options: WidgetSubscriptionOptions; + id: string; + init$: Observable; + ctx: WidgetSubscriptionContext; + type: widgetType; + callbacks: WidgetSubscriptionCallbacks; + + loadingData: boolean; + useDashboardTimewindow: boolean; + + legendData: LegendData; + + datasourcePages?: PageData[]; + dataPages?: PageData>[]; + datasources?: Array; + data?: Array; + hiddenData?: Array<{data: DataSet}>; + timeWindowConfig?: Timewindow; + timeWindow?: WidgetTimewindow; + comparisonTimeWindow?: WidgetTimewindow; + + alarms?: PageData; + alarmSource?: Datasource; + + targetDeviceAliasIds?: Array; + targetDeviceIds?: Array; + + rpcEnabled?: boolean; + executingRpcRequest?: boolean; + rpcErrorText?: string; + rpcRejection?: HttpErrorResponse; + + getFirstEntityInfo(): SubscriptionEntityInfo; + + onAliasesChanged(aliasIds: Array): boolean; + + onFiltersChanged(filterIds: Array): boolean; + + onDashboardTimewindowChanged(dashboardTimewindow: Timewindow): void; + + updateDataVisibility(index: number): void; + + onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval?: number): void; + onResetTimewindow(): void; + updateTimewindowConfig(newTimewindow: Timewindow): void; + + sendOneWayCommand(method: string, params?: any, timeout?: number): Observable; + sendTwoWayCommand(method: string, params?: any, timeout?: number): Observable; + clearRpcError(): void; + + subscribe(): void; + + subscribeAllForPaginatedData(pageLink: EntityDataPageLink, + keyFilters: KeyFilter[]): void; + + subscribeForPaginatedData(datasourceIndex: number, + pageLink: EntityDataPageLink, + keyFilters: KeyFilter[]): Observable; + + subscribeForAlarms(pageLink: AlarmDataPageLink, + keyFilters: KeyFilter[]): void; + + isDataResolved(): boolean; + + destroy(): void; + + update(): void; + + [key: string]: any; +} diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts new file mode 100644 index 0000000000..d3066784eb --- /dev/null +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -0,0 +1,1419 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + IWidgetSubscription, + SubscriptionEntityInfo, + SubscriptionMessage, + WidgetSubscriptionCallbacks, + WidgetSubscriptionContext, + WidgetSubscriptionOptions +} from '@core/api/widget-api.models'; +import { + DataKey, + DataSet, + DataSetHolder, + Datasource, + DatasourceData, + DatasourceType, + LegendConfig, + LegendData, + LegendKey, + LegendKeyData, + widgetType +} from '@app/shared/models/widget.models'; +import { HttpErrorResponse } from '@angular/common/http'; +import { + createSubscriptionTimewindow, + createTimewindowForComparison, + SubscriptionTimewindow, + Timewindow, + toHistoryTimewindow, + WidgetTimewindow +} from '@app/shared/models/time/time.models'; +import { forkJoin, Observable, of, ReplaySubject, Subject, throwError } from 'rxjs'; +import { CancelAnimationFrame } from '@core/services/raf.service'; +import { EntityType } from '@shared/models/entity-type.models'; +import { createLabelFromDatasource, deepClone, isDefined, isDefinedAndNotNull, isEqual } from '@core/utils'; +import { EntityId } from '@app/shared/models/id/entity-id'; +import * as moment_ from 'moment'; +import { emptyPageData, PageData } from '@shared/models/page/page-data'; +import { EntityDataListener } from '@core/api/entity-data.service'; +import { + AlarmData, + AlarmDataPageLink, + EntityData, + EntityDataPageLink, + entityDataToEntityInfo, + EntityKeyType, + KeyFilter, + updateDatasourceFromEntityInfo +} from '@shared/models/query/query.models'; +import { map } from 'rxjs/operators'; +import { AlarmDataListener } from '@core/api/alarm-data.service'; + +const moment = moment_; + +export class WidgetSubscription implements IWidgetSubscription { + + id: string; + ctx: WidgetSubscriptionContext; + type: widgetType; + callbacks: WidgetSubscriptionCallbacks; + + timeWindow: WidgetTimewindow; + originalTimewindow: Timewindow; + timeWindowConfig: Timewindow; + subscriptionTimewindow: SubscriptionTimewindow; + useDashboardTimewindow: boolean; + + hasDataPageLink: boolean; + singleEntity: boolean; + warnOnPageDataOverflow: boolean; + + datasourcePages: PageData[]; + dataPages: PageData>[]; + entityDataListeners: Array; + configuredDatasources: Array; + + data: Array; + datasources: Array; + hiddenData: Array; + legendData: LegendData; + legendConfig: LegendConfig; + caulculateLegendData: boolean; + displayLegend: boolean; + stateData: boolean; + decimals: number; + units: string; + comparisonEnabled: boolean; + timeForComparison: moment_.unitOfTime.DurationConstructor; + comparisonTimeWindow: WidgetTimewindow; + timewindowForComparison: SubscriptionTimewindow; + + alarms: PageData; + alarmSource: Datasource; + alarmDataListener: AlarmDataListener; + + loadingData: boolean; + + targetDeviceAliasIds?: Array; + targetDeviceIds?: Array; + + executingRpcRequest: boolean; + rpcEnabled: boolean; + rpcErrorText: string; + rpcRejection: HttpErrorResponse; + + init$: Observable; + + cafs: {[cafId: string]: CancelAnimationFrame} = {}; + hasResolvedData = false; + + targetDeviceAliasId: string; + targetDeviceId: string; + targetDeviceName: string; + executingSubjects: Array>; + + subscribed = false; + + constructor(subscriptionContext: WidgetSubscriptionContext, public options: WidgetSubscriptionOptions) { + const subscriptionSubject = new ReplaySubject(); + this.init$ = subscriptionSubject.asObservable(); + this.ctx = subscriptionContext; + this.type = options.type; + this.id = this.ctx.utils.guid(); + this.callbacks = options.callbacks; + + if (this.type === widgetType.rpc) { + this.callbacks.rpcStateChanged = this.callbacks.rpcStateChanged || (() => {}); + this.callbacks.onRpcSuccess = this.callbacks.onRpcSuccess || (() => {}); + this.callbacks.onRpcFailed = this.callbacks.onRpcFailed || (() => {}); + this.callbacks.onRpcErrorCleared = this.callbacks.onRpcErrorCleared || (() => {}); + + this.targetDeviceAliasIds = options.targetDeviceAliasIds; + this.targetDeviceIds = options.targetDeviceIds; + + this.targetDeviceAliasId = null; + this.targetDeviceId = null; + + this.rpcRejection = null; + this.rpcErrorText = null; + this.rpcEnabled = false; + this.executingRpcRequest = false; + this.executingSubjects = []; + this.initRpc().subscribe(() => { + subscriptionSubject.next(this); + subscriptionSubject.complete(); + }); + } else if (this.type === widgetType.alarm) { + this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || (() => {}); + this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || (() => {}); + this.callbacks.onSubscriptionMessage = this.callbacks.onSubscriptionMessage || (() => {}); + this.callbacks.dataLoading = this.callbacks.dataLoading || (() => {}); + this.callbacks.timeWindowUpdated = this.callbacks.timeWindowUpdated || (() => {}); + this.alarmSource = options.alarmSource; + this.alarmDataListener = null; + this.alarms = emptyPageData(); + this.originalTimewindow = null; + this.timeWindow = {}; + this.useDashboardTimewindow = options.useDashboardTimewindow; + if (this.useDashboardTimewindow) { + this.timeWindowConfig = deepClone(options.dashboardTimewindow); + } else { + this.timeWindowConfig = deepClone(options.timeWindowConfig); + } + this.subscriptionTimewindow = null; + this.loadingData = false; + this.displayLegend = false; + this.initAlarmSubscription().subscribe(() => { + subscriptionSubject.next(this); + subscriptionSubject.complete(); + }, + () => { + subscriptionSubject.error(null); + }); + } else { + this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || (() => {}); + this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || (() => {}); + this.callbacks.onSubscriptionMessage = this.callbacks.onSubscriptionMessage || (() => {}); + this.callbacks.onInitialPageDataChanged = this.callbacks.onInitialPageDataChanged || (() => {}); + this.callbacks.dataLoading = this.callbacks.dataLoading || (() => {}); + this.callbacks.legendDataUpdated = this.callbacks.legendDataUpdated || (() => {}); + this.callbacks.timeWindowUpdated = this.callbacks.timeWindowUpdated || (() => {}); + + this.configuredDatasources = this.ctx.utils.validateDatasources(options.datasources); + this.entityDataListeners = []; + this.hasDataPageLink = options.hasDataPageLink; + this.singleEntity = options.singleEntity; + this.warnOnPageDataOverflow = options.warnOnPageDataOverflow; + this.datasourcePages = []; + this.datasources = []; + this.dataPages = []; + this.data = []; + this.hiddenData = []; + this.originalTimewindow = null; + this.timeWindow = {}; + this.useDashboardTimewindow = options.useDashboardTimewindow; + this.stateData = options.stateData; + if (this.useDashboardTimewindow) { + this.timeWindowConfig = deepClone(options.dashboardTimewindow); + } else { + this.timeWindowConfig = deepClone(options.timeWindowConfig); + } + + this.subscriptionTimewindow = null; + this.comparisonEnabled = options.comparisonEnabled; + if (this.comparisonEnabled) { + this.timeForComparison = options.timeForComparison; + + this.comparisonTimeWindow = {}; + this.timewindowForComparison = null; + } + + this.units = options.units || ''; + this.decimals = isDefined(options.decimals) ? options.decimals : 2; + + this.loadingData = false; + + if (options.legendConfig) { + this.legendConfig = options.legendConfig; + this.legendData = { + keys: [], + data: [] + }; + this.displayLegend = true; + } else { + this.displayLegend = false; + } + this.caulculateLegendData = this.displayLegend && + this.type === widgetType.timeseries && + (this.legendConfig.showMin === true || + this.legendConfig.showMax === true || + this.legendConfig.showAvg === true || + this.legendConfig.showTotal === true); + this.initDataSubscription().subscribe(() => { + subscriptionSubject.next(this); + subscriptionSubject.complete(); + }, + (err) => { + subscriptionSubject.error(err); + }); + } + } + + private initRpc(): Observable { + const initRpcSubject = new ReplaySubject(); + if (this.targetDeviceAliasIds && this.targetDeviceAliasIds.length > 0) { + this.targetDeviceAliasId = this.targetDeviceAliasIds[0]; + this.ctx.aliasController.resolveSingleEntityInfo(this.targetDeviceAliasId).subscribe( + (entityInfo) => { + if (entityInfo && entityInfo.entityType === EntityType.DEVICE) { + this.targetDeviceId = entityInfo.id; + this.targetDeviceName = entityInfo.name; + if (this.targetDeviceId) { + this.rpcEnabled = true; + } else { + this.rpcEnabled = this.ctx.utils.widgetEditMode; + } + this.hasResolvedData = this.rpcEnabled; + this.callbacks.rpcStateChanged(this); + initRpcSubject.next(); + initRpcSubject.complete(); + } else { + this.rpcEnabled = false; + this.callbacks.rpcStateChanged(this); + initRpcSubject.next(); + initRpcSubject.complete(); + } + }, + () => { + this.rpcEnabled = false; + this.callbacks.rpcStateChanged(this); + initRpcSubject.next(); + initRpcSubject.complete(); + } + ); + } else { + if (this.targetDeviceIds && this.targetDeviceIds.length > 0) { + this.targetDeviceId = this.targetDeviceIds[0]; + } + if (this.targetDeviceId) { + this.rpcEnabled = true; + } else { + this.rpcEnabled = this.ctx.utils.widgetEditMode; + } + this.hasResolvedData = true; + this.callbacks.rpcStateChanged(this); + initRpcSubject.next(); + initRpcSubject.complete(); + } + return initRpcSubject.asObservable(); + } + + private initAlarmSubscription(): Observable { + const initAlarmSubscriptionSubject = new ReplaySubject(1); + this.loadStDiff().subscribe(() => { + if (!this.ctx.aliasController) { + this.hasResolvedData = true; + this.configureAlarmsData(); + initAlarmSubscriptionSubject.next(); + initAlarmSubscriptionSubject.complete(); + } else { + this.ctx.aliasController.resolveAlarmSource(this.alarmSource).subscribe( + (alarmSource) => { + this.alarmSource = alarmSource; + if (alarmSource) { + this.hasResolvedData = true; + } + this.configureAlarmsData(); + initAlarmSubscriptionSubject.next(); + initAlarmSubscriptionSubject.complete(); + }, + (err) => { + initAlarmSubscriptionSubject.error(err); + } + ); + } + }); + return initAlarmSubscriptionSubject.asObservable(); + } + + private configureAlarmsData() { + this.notifyDataLoaded(); + } + + private initDataSubscription(): Observable { + this.notifyDataLoading(); + const initDataSubscriptionSubject = new ReplaySubject(1); + this.loadStDiff().subscribe(() => { + if (!this.ctx.aliasController) { + this.hasResolvedData = true; + this.prepareDataSubscriptions().subscribe( + () => { + initDataSubscriptionSubject.next(); + initDataSubscriptionSubject.complete(); + } + ); + } else { + this.ctx.aliasController.resolveDatasources(this.configuredDatasources, this.singleEntity).subscribe( + (datasources) => { + this.configuredDatasources = datasources; + this.prepareDataSubscriptions().subscribe( + () => { + initDataSubscriptionSubject.next(); + initDataSubscriptionSubject.complete(); + } + ); + }, + (err) => { + this.notifyDataLoaded(); + initDataSubscriptionSubject.error(err); + } + ); + } + }); + return initDataSubscriptionSubject.asObservable(); + } + + private prepareDataSubscriptions(): Observable { + if (this.hasDataPageLink) { + this.hasResolvedData = true; + this.notifyDataLoaded(); + return of(null); + } + if (this.comparisonEnabled) { + const additionalDatasources: Datasource[] = []; + this.configuredDatasources.forEach((datasource, datasourceIndex) => { + const additionalDataKeys: DataKey[] = []; + datasource.dataKeys.forEach((dataKey, dataKeyIndex) => { + if (dataKey.settings.comparisonSettings && dataKey.settings.comparisonSettings.showValuesForComparison) { + const additionalDataKey = deepClone(dataKey); + additionalDataKey.isAdditional = true; + additionalDataKey.origDataKeyIndex = dataKeyIndex; + additionalDataKeys.push(additionalDataKey); + } + }); + if (additionalDataKeys.length) { + const additionalDatasource: Datasource = deepClone(datasource); + additionalDatasource.dataKeys = additionalDataKeys; + additionalDatasource.isAdditional = true; + additionalDatasource.origDatasourceIndex = datasourceIndex; + additionalDatasources.push(additionalDatasource); + } + }); + this.configuredDatasources = this.configuredDatasources.concat(additionalDatasources); + } + const resolveResultObservables = this.configuredDatasources.map((datasource, index) => { + const listener: EntityDataListener = { + subscriptionType: this.type, + configDatasource: datasource, + configDatasourceIndex: index, + dataLoaded: (pageData, data1, datasourceIndex, pageLink) => { + this.dataLoaded(pageData, data1, datasourceIndex, pageLink, true); + }, + initialPageDataChanged: this.initialPageDataChanged.bind(this), + dataUpdated: this.dataUpdated.bind(this), + updateRealtimeSubscription: () => { + if (this.comparisonEnabled && datasource.isAdditional) { + return this.updateSubscriptionForComparison(); + } else { + return this.updateRealtimeSubscription(); + } + }, + setRealtimeSubscription: (subscriptionTimewindow) => { + if (this.comparisonEnabled && datasource.isAdditional) { + this.updateSubscriptionForComparison(subscriptionTimewindow); + } else { + this.updateRealtimeSubscription(deepClone(subscriptionTimewindow)); + } + } + }; + this.entityDataListeners.push(listener); + return this.ctx.entityDataService.prepareSubscription(listener); + }); + return forkJoin(resolveResultObservables).pipe( + map((resolveResults) => { + resolveResults.forEach((resolveResult) => { + if (resolveResult) { + this.dataLoaded(resolveResult.pageData, resolveResult.data, resolveResult.datasourceIndex, resolveResult.pageLink, false); + } + }); + this.configureLoadedData(); + this.hasResolvedData = this.datasources.length > 0; + this.updateDataTimewindow(); + this.notifyDataLoaded(); + this.onDataUpdated(true); + }) + ); + } + + private resetData() { + this.data.length = 0; + this.hiddenData.length = 0; + if (this.displayLegend) { + this.legendData.keys.length = 0; + this.legendData.data.length = 0; + } + this.onDataUpdated(); + } + + getFirstEntityInfo(): SubscriptionEntityInfo { + let entityId: EntityId; + let entityName: string; + let entityLabel: string; + let entityDescription: string; + if (this.type === widgetType.rpc) { + if (this.targetDeviceId) { + entityId = { + entityType: EntityType.DEVICE, + id: this.targetDeviceId + }; + entityName = this.targetDeviceName; + } + } else if (this.type === widgetType.alarm) { + if (this.alarms && this.alarms.data.length) { + const data = this.alarms.data[0]; + entityId = data.originator; + entityName = data.originatorName; + if (data.latest && data.latest[EntityKeyType.ENTITY_FIELD]) { + const entityFields = data.latest[EntityKeyType.ENTITY_FIELD]; + const labelValue = entityFields.label; + if (labelValue) { + entityLabel = labelValue.value; + } + const additionalInfoValue = entityFields.additionalInfo; + if (additionalInfoValue) { + const additionalInfo = additionalInfoValue.value; + if (additionalInfo && additionalInfo.length) { + try { + const additionalInfoJson = JSON.parse(additionalInfo); + if (additionalInfoJson && additionalInfoJson.description) { + entityDescription = additionalInfoJson.description; + } + } catch (e) {} + } + } + } + } + } else { + for (const datasource of this.datasources) { + if (datasource && datasource.entityType && datasource.entityId) { + entityId = { + entityType: datasource.entityType, + id: datasource.entityId + }; + entityName = datasource.entityName; + entityLabel = datasource.entityLabel; + entityDescription = datasource.entityDescription; + break; + } + } + } + if (entityId) { + return { + entityId, + entityName, + entityLabel, + entityDescription + }; + } else { + return null; + } + } + + onAliasesChanged(aliasIds: Array): boolean { + if (this.type === widgetType.rpc) { + return this.checkRpcTarget(aliasIds); + } else if (this.type === widgetType.alarm) { + return this.checkAlarmSource(aliasIds); + } else { + return this.checkSubscriptions(aliasIds); + } + } + + onFiltersChanged(filterIds: Array): boolean { + if (this.type !== widgetType.rpc) { + if (this.type === widgetType.alarm) { + return this.checkAlarmSourceFilters(filterIds); + } else { + return this.checkSubscriptionsFilters(filterIds); + } + } + return false; + } + + private onDataUpdated(detectChanges?: boolean) { + if (this.cafs.dataUpdated) { + this.cafs.dataUpdated(); + this.cafs.dataUpdated = null; + } + this.cafs.dataUpdated = this.ctx.raf.raf(() => { + try { + this.callbacks.onDataUpdated(this, detectChanges); + } catch (e) { + this.callbacks.onDataUpdateError(this, e); + } + }); + } + + private onSubscriptionMessage(message: SubscriptionMessage) { + if (this.cafs.message) { + this.cafs.message(); + this.cafs.message = null; + } + this.cafs.message = this.ctx.raf.raf(() => { + this.callbacks.onSubscriptionMessage(this, message); + }); + } + + onDashboardTimewindowChanged(newDashboardTimewindow: Timewindow) { + if (this.type === widgetType.timeseries || this.type === widgetType.alarm) { + if (this.useDashboardTimewindow) { + if (!isEqual(this.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) { + this.timeWindowConfig = deepClone(newDashboardTimewindow); + this.update(); + return true; + } + } + } + return false; + } + + updateDataVisibility(index: number): void { + if (this.displayLegend) { + const hidden = this.legendData.keys.find(key => key.dataIndex === index).dataKey.hidden; + if (hidden) { + this.hiddenData[index].data = this.data[index].data; + this.data[index].data = []; + } else { + this.data[index].data = this.hiddenData[index].data; + this.hiddenData[index].data = []; + } + this.onDataUpdated(); + } + } + + updateTimewindowConfig(newTimewindow: Timewindow): void { + if (!this.useDashboardTimewindow) { + this.timeWindowConfig = newTimewindow; + this.update(); + } + } + + onResetTimewindow(): void { + if (this.useDashboardTimewindow) { + this.ctx.dashboardTimewindowApi.onResetTimewindow(); + } else { + if (this.originalTimewindow) { + this.timeWindowConfig = deepClone(this.originalTimewindow); + this.originalTimewindow = null; + this.callbacks.timeWindowUpdated(this, this.timeWindowConfig); + this.update(); + } + } + } + + onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval?: number): void { + if (this.useDashboardTimewindow) { + this.ctx.dashboardTimewindowApi.onUpdateTimewindow(startTimeMs, endTimeMs); + } else { + if (!this.originalTimewindow) { + this.originalTimewindow = deepClone(this.timeWindowConfig); + } + this.timeWindowConfig = toHistoryTimewindow(this.timeWindowConfig, startTimeMs, endTimeMs, interval, this.ctx.timeService); + this.callbacks.timeWindowUpdated(this, this.timeWindowConfig); + this.update(); + } + } + + sendOneWayCommand(method: string, params?: any, timeout?: number): Observable { + return this.sendCommand(true, method, params, timeout); + } + + sendTwoWayCommand(method: string, params?: any, timeout?: number): Observable { + return this.sendCommand(false, method, params, timeout); + } + + clearRpcError(): void { + this.rpcRejection = null; + this.rpcErrorText = null; + this.callbacks.onRpcErrorCleared(this); + } + + sendCommand(oneWayElseTwoWay: boolean, method: string, params?: any, timeout?: number): Observable { + if (!this.rpcEnabled) { + return throwError(new Error('Rpc disabled!')); + } else { + if (this.rpcRejection && this.rpcRejection.status !== 408) { + this.rpcRejection = null; + this.rpcErrorText = null; + this.callbacks.onRpcErrorCleared(this); + } + const requestBody: any = { + method, + params + }; + if (timeout && timeout > 0) { + requestBody.timeout = timeout; + } + const rpcSubject: Subject = new ReplaySubject(); + this.executingRpcRequest = true; + this.callbacks.rpcStateChanged(this); + if (this.ctx.utils.widgetEditMode) { + setTimeout(() => { + this.executingRpcRequest = false; + this.callbacks.rpcStateChanged(this); + if (oneWayElseTwoWay) { + rpcSubject.next(); + rpcSubject.complete(); + } else { + rpcSubject.next(requestBody); + rpcSubject.complete(); + } + }, 500); + } else { + this.executingSubjects.push(rpcSubject); + (oneWayElseTwoWay ? this.ctx.deviceService.sendOneWayRpcCommand(this.targetDeviceId, requestBody) : + this.ctx.deviceService.sendTwoWayRpcCommand(this.targetDeviceId, requestBody)) + .subscribe((responseBody) => { + this.rpcRejection = null; + this.rpcErrorText = null; + const index = this.executingSubjects.indexOf(rpcSubject); + if (index >= 0) { + this.executingSubjects.splice( index, 1 ); + } + this.executingRpcRequest = this.executingSubjects.length > 0; + this.callbacks.onRpcSuccess(this); + rpcSubject.next(responseBody); + rpcSubject.complete(); + }, + (rejection: HttpErrorResponse) => { + const index = this.executingSubjects.indexOf(rpcSubject); + if (index >= 0) { + this.executingSubjects.splice( index, 1 ); + } + this.executingRpcRequest = this.executingSubjects.length > 0; + this.callbacks.rpcStateChanged(this); + if (!this.executingRpcRequest || rejection.status === 408) { + this.rpcRejection = rejection; + if (rejection.status === 408) { + this.rpcErrorText = 'Request Timeout.'; + } else if (rejection.status === 409) { + this.rpcErrorText = 'Device is offline.'; + } else { + this.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText; + const error = this.extractRejectionErrorText(rejection); + if (error) { + this.rpcErrorText += '
'; + this.rpcErrorText += error; + } + } + this.callbacks.onRpcFailed(this); + } + rpcSubject.error(rejection); + }); + } + return rpcSubject.asObservable(); + } + } + + private extractRejectionErrorText(rejection: HttpErrorResponse) { + let error = null; + if (rejection.error) { + error = rejection.error; + try { + error = rejection.error ? JSON.parse(rejection.error) : null; + } catch (e) {} + } + if (error && !error.message) { + error = this.prepareMessageFromData(error); + } else if (error && error.message) { + error = error.message; + } + return error; + } + + private prepareMessageFromData(data) { + if (typeof data === 'object' && data.constructor === ArrayBuffer) { + const msg = String.fromCharCode.apply(null, new Uint8Array(data)); + try { + const msgObj = JSON.parse(msg); + if (msgObj.message) { + return msgObj.message; + } else { + return msg; + } + } catch (e) { + return msg; + } + } else { + return data; + } + } + + update() { + if (this.type !== widgetType.rpc) { + if (this.type === widgetType.alarm) { + this.updateAlarmDataSubscription(); + } else { + if (this.hasDataPageLink) { + this.updateDataSubscriptions(); + } else { + this.notifyDataLoading(); + this.dataSubscribe(); + } + } + } + } + + subscribe(): void { + if (!this.subscribed) { + this.subscribed = true; + if (this.cafs.subscribe) { + this.cafs.subscribe(); + this.cafs.subscribe = null; + } + this.cafs.subscribe = this.ctx.raf.raf(() => { + this.doSubscribe(); + }); + } + } + + subscribeAllForPaginatedData(pageLink: EntityDataPageLink, + keyFilters: KeyFilter[]): Observable { + const observables: Observable[] = []; + this.configuredDatasources.forEach((datasource, datasourceIndex) => { + observables.push(this.subscribeForPaginatedData(datasourceIndex, pageLink, keyFilters)); + }); + if (observables.length) { + return forkJoin(observables); + } else { + return of(null); + } + } + + subscribeForPaginatedData(datasourceIndex: number, + pageLink: EntityDataPageLink, + keyFilters: KeyFilter[]): Observable { + let entityDataListener = this.entityDataListeners[datasourceIndex]; + if (entityDataListener) { + this.ctx.entityDataService.stopSubscription(entityDataListener); + } + const datasource = this.configuredDatasources[datasourceIndex]; + if (datasource) { + if (this.type === widgetType.timeseries && this.timeWindowConfig) { + this.updateRealtimeSubscription(); + } + entityDataListener = { + subscriptionType: this.type, + configDatasource: datasource, + configDatasourceIndex: datasourceIndex, + subscriptionTimewindow: this.subscriptionTimewindow, + dataLoaded: (pageData, data1, datasourceIndex1, pageLink1) => { + this.dataLoaded(pageData, data1, datasourceIndex1, pageLink1, true); + }, + dataUpdated: this.dataUpdated.bind(this), + updateRealtimeSubscription: () => { + return this.updateRealtimeSubscription(); + }, + setRealtimeSubscription: (subscriptionTimewindow) => { + this.updateRealtimeSubscription(deepClone(subscriptionTimewindow)); + } + }; + this.entityDataListeners[datasourceIndex] = entityDataListener; + return this.ctx.entityDataService.subscribeForPaginatedData(entityDataListener, pageLink, keyFilters); + } else { + return of(null); + } + } + + subscribeForAlarms(pageLink: AlarmDataPageLink, + keyFilters: KeyFilter[]) { + if (this.alarmDataListener) { + this.ctx.alarmDataService.stopSubscription(this.alarmDataListener); + } + if (this.timeWindowConfig) { + this.updateRealtimeSubscription(); + } + this.alarmDataListener = { + subscriptionTimewindow: this.subscriptionTimewindow, + alarmSource: this.alarmSource, + alarmsLoaded: this.alarmsLoaded.bind(this), + alarmsUpdated: this.alarmsUpdated.bind(this) + }; + + this.alarms = emptyPageData(); + + this.ctx.alarmDataService.subscribeForAlarms(this.alarmDataListener, pageLink, keyFilters); + + let forceUpdate = false; + if (this.alarmSource.unresolvedStateEntity) { + forceUpdate = true; + } + if (forceUpdate) { + this.onDataUpdated(); + } + } + + private doSubscribe() { + if (this.type !== widgetType.rpc && this.type !== widgetType.alarm) { + this.dataSubscribe(); + } + } + + private updateDataTimewindow() { + if (!this.hasDataPageLink) { + if (this.type === widgetType.timeseries && this.timeWindowConfig) { + this.updateRealtimeSubscription(); + if (this.comparisonEnabled) { + this.updateSubscriptionForComparison(); + } + } + } + } + + private dataSubscribe() { + if (!this.hasDataPageLink) { + if (this.type === widgetType.timeseries && this.timeWindowConfig) { + this.updateDataTimewindow(); + if (this.subscriptionTimewindow.fixedWindow) { + this.onDataUpdated(); + } + } + const forceUpdate = !this.datasources.length; + this.entityDataListeners.forEach((listener) => { + if (this.comparisonEnabled && listener.configDatasource.isAdditional) { + listener.subscriptionTimewindow = this.timewindowForComparison; + } else { + listener.subscriptionTimewindow = this.subscriptionTimewindow; + } + this.ctx.entityDataService.startSubscription(listener); + }); + if (forceUpdate) { + this.onDataUpdated(); + } + } + } + + unsubscribe() { + if (this.type !== widgetType.rpc) { + if (this.type === widgetType.alarm) { + this.alarmsUnsubscribe(); + } else { + this.entityDataListeners.forEach((listener) => { + if (listener != null) { + this.ctx.entityDataService.stopSubscription(listener); + } + }); + this.entityDataListeners.length = 0; + this.resetData(); + } + } + this.subscribed = false; + } + + private alarmsUnsubscribe() { + if (this.alarmDataListener) { + this.ctx.alarmDataService.stopSubscription(this.alarmDataListener); + this.alarmDataListener = null; + } + } + + private checkRpcTarget(aliasIds: Array): boolean { + return aliasIds.indexOf(this.targetDeviceAliasId) > -1; + } + + private checkAlarmSource(aliasIds: Array): boolean { + if (this.options.alarmSource && this.options.alarmSource.entityAliasId) { + if (aliasIds.indexOf(this.options.alarmSource.entityAliasId) > -1) { + this.updateAlarmSubscription(); + } + } + return false; + } + + private checkAlarmSourceFilters(filterIds: Array): boolean { + if (this.options.alarmSource && this.options.alarmSource.filterId) { + if (filterIds.indexOf(this.options.alarmSource.filterId) > -1) { + this.updateAlarmSubscription(); + } + } + return false; + } + + private updateAlarmSubscription() { + this.alarmSource = this.options.alarmSource; + if (!this.ctx.aliasController) { + this.hasResolvedData = true; + this.configureAlarmsData(); + this.updateAlarmDataSubscription(); + } else { + this.ctx.aliasController.resolveAlarmSource(this.alarmSource).subscribe( + (alarmSource) => { + this.alarmSource = alarmSource; + if (alarmSource) { + this.hasResolvedData = true; + } + this.configureAlarmsData(); + this.updateAlarmDataSubscription(); + }, + () => { + this.notifyDataLoaded(); + } + ); + } + } + + private updateAlarmDataSubscription() { + if (this.alarmDataListener) { + const pageLink = this.alarmDataListener.alarmDataSubscriptionOptions.pageLink; + const keyFilters = this.alarmDataListener.alarmDataSubscriptionOptions.additionalKeyFilters; + this.subscribeForAlarms(pageLink, keyFilters); + } + } + + private checkSubscriptions(aliasIds: Array): boolean { + let subscriptionsChanged = false; + const datasources = this.options.datasources; + if (datasources) { + for (const datasource of datasources) { + if (datasource.entityAliasId) { + if (aliasIds.indexOf(datasource.entityAliasId) > -1) { + subscriptionsChanged = true; + break; + } + } + } + } + if (subscriptionsChanged && this.hasDataPageLink) { + subscriptionsChanged = false; + this.updateDataSubscriptions(); + } + return subscriptionsChanged; + } + + private checkSubscriptionsFilters(filterIds: Array): boolean { + let subscriptionsChanged = false; + const datasources = this.options.datasources; + if (datasources) { + for (const datasource of datasources) { + if (datasource.filterId) { + if (filterIds.indexOf(datasource.filterId) > -1) { + subscriptionsChanged = true; + break; + } + } + } + } + if (subscriptionsChanged && this.hasDataPageLink) { + subscriptionsChanged = false; + this.updateDataSubscriptions(); + } + return subscriptionsChanged; + } + + private updateDataSubscriptions() { + this.configuredDatasources = this.ctx.utils.validateDatasources(this.options.datasources); + if (!this.ctx.aliasController) { + this.hasResolvedData = true; + this.prepareDataSubscriptions().subscribe( + () => { + this.updatePaginatedDataSubscriptions(); + } + ); + } else { + this.ctx.aliasController.resolveDatasources(this.configuredDatasources, this.singleEntity).subscribe( + (datasources) => { + this.configuredDatasources = datasources; + this.prepareDataSubscriptions().subscribe( + () => { + this.updatePaginatedDataSubscriptions(); + } + ); + }, + () => { + this.notifyDataLoaded(); + } + ); + } + } + + private updatePaginatedDataSubscriptions() { + for (let datasourceIndex = 0; datasourceIndex < this.entityDataListeners.length; datasourceIndex++) { + const entityDataListener = this.entityDataListeners[datasourceIndex]; + if (entityDataListener) { + const pageLink = entityDataListener.subscriptionOptions.pageLink; + const keyFilters = entityDataListener.subscriptionOptions.additionalKeyFilters; + this.subscribeForPaginatedData(datasourceIndex, pageLink, keyFilters); + } + } + } + + isDataResolved(): boolean { + return this.hasResolvedData; + } + + destroy(): void { + this.unsubscribe(); + for (const cafId of Object.keys(this.cafs)) { + if (this.cafs[cafId]) { + this.cafs[cafId](); + this.cafs[cafId] = null; + } + } + } + + private notifyDataLoading() { + this.loadingData = true; + this.callbacks.dataLoading(this); + } + + private notifyDataLoaded() { + this.loadingData = false; + this.callbacks.dataLoading(this); + } + + private updateTimewindow() { + this.timeWindow.interval = this.subscriptionTimewindow.aggregation.interval || 1000; + if (this.subscriptionTimewindow.realtimeWindowMs) { + this.timeWindow.maxTime = moment().valueOf() + this.timeWindow.stDiff; + this.timeWindow.minTime = this.timeWindow.maxTime - this.subscriptionTimewindow.realtimeWindowMs; + } else if (this.subscriptionTimewindow.fixedWindow) { + this.timeWindow.maxTime = this.subscriptionTimewindow.fixedWindow.endTimeMs; + this.timeWindow.minTime = this.subscriptionTimewindow.fixedWindow.startTimeMs; + } + } + + private updateRealtimeSubscription(subscriptionTimewindow?: SubscriptionTimewindow): SubscriptionTimewindow { + if (subscriptionTimewindow) { + this.subscriptionTimewindow = subscriptionTimewindow; + } else { + this.subscriptionTimewindow = + createSubscriptionTimewindow(this.timeWindowConfig, this.timeWindow.stDiff, + this.stateData, this.ctx.timeService); + } + this.updateTimewindow(); + return this.subscriptionTimewindow; + } + + private updateComparisonTimewindow() { + this.comparisonTimeWindow.interval = this.timewindowForComparison.aggregation.interval || 1000; + if (this.timewindowForComparison.realtimeWindowMs) { + this.comparisonTimeWindow.maxTime = moment(this.timeWindow.maxTime).subtract(1, this.timeForComparison).valueOf(); + this.comparisonTimeWindow.minTime = this.comparisonTimeWindow.maxTime - this.timewindowForComparison.realtimeWindowMs; + } else if (this.timewindowForComparison.fixedWindow) { + this.comparisonTimeWindow.maxTime = this.timewindowForComparison.fixedWindow.endTimeMs; + this.comparisonTimeWindow.minTime = this.timewindowForComparison.fixedWindow.startTimeMs; + } + } + + private updateSubscriptionForComparison(subscriptionTimewindow?: SubscriptionTimewindow): SubscriptionTimewindow { + if (subscriptionTimewindow) { + this.timewindowForComparison = subscriptionTimewindow; + } else { + if (!this.subscriptionTimewindow) { + this.subscriptionTimewindow = this.updateRealtimeSubscription(); + } + this.timewindowForComparison = createTimewindowForComparison(this.subscriptionTimewindow, this.timeForComparison); + } + this.updateComparisonTimewindow(); + return this.timewindowForComparison; + } + + private initialPageDataChanged(nextPageData: PageData) { + this.callbacks.onInitialPageDataChanged(this, nextPageData); + } + + private dataLoaded(pageData: PageData, + data: Array>, + datasourceIndex: number, pageLink: EntityDataPageLink, isUpdate: boolean) { + const datasource = this.configuredDatasources[datasourceIndex]; + datasource.dataReceived = true; + const datasources = pageData.data.map((entityData, index) => + this.entityDataToDatasource(datasource, entityData, index) + ); + this.datasourcePages[datasourceIndex] = { + data: datasources, + hasNext: pageData.hasNext, + totalElements: pageData.totalElements, + totalPages: pageData.totalPages + }; + const datasourceData = datasources.map((datasourceElement, index) => + this.entityDataToDatasourceData(datasourceElement, data[index]) + ); + this.dataPages[datasourceIndex] = { + data: datasourceData, + hasNext: pageData.hasNext, + totalElements: pageData.totalElements, + totalPages: pageData.totalPages + }; + if (datasource.type === DatasourceType.entity && + pageData.hasNext && pageLink.pageSize > 1) { + if (this.warnOnPageDataOverflow) { + const message = this.ctx.translate.instant('widget.data-overflow', + {count: pageData.data.length, total: pageData.totalElements}); + this.onSubscriptionMessage({ + severity: 'warn', + message + }); + } + } + if (isUpdate) { + this.configureLoadedData(); + this.onDataUpdated(true); + } + } + + private configureLoadedData() { + this.datasources.length = 0; + this.data.length = 0; + this.hiddenData.length = 0; + if (this.displayLegend) { + this.legendData.keys.length = 0; + this.legendData.data.length = 0; + } + + let dataKeyIndex = 0; + this.configuredDatasources.forEach((configuredDatasource, datasourceIndex) => { + configuredDatasource.dataKeyStartIndex = dataKeyIndex; + const datasourcesPage = this.datasourcePages[datasourceIndex]; + const datasourceDataPage = this.dataPages[datasourceIndex]; + if (datasourcesPage) { + datasourcesPage.data.forEach((datasource, currentDatasourceIndex) => { + datasource.dataKeys.forEach((dataKey, currentDataKeyIndex) => { + const datasourceData = datasourceDataPage.data[currentDatasourceIndex][currentDataKeyIndex]; + this.data.push(datasourceData); + this.hiddenData.push({data: []}); + if (this.displayLegend) { + const legendKey: LegendKey = { + dataKey, + dataIndex: dataKeyIndex + }; + this.legendData.keys.push(legendKey); + const legendKeyData: LegendKeyData = { + min: null, + max: null, + avg: null, + total: null, + hidden: false + }; + this.legendData.data.push(legendKeyData); + } + dataKeyIndex++; + }); + this.datasources.push(datasource); + }); + } + } + ); + let index = 0; + this.datasources.forEach((datasource) => { + datasource.dataKeys.forEach((dataKey) => { + if (datasource.generated || datasource.isAdditional) { + dataKey._hash = Math.random(); + dataKey.color = this.ctx.utils.getMaterialColor(index); + } + index++; + }); + }); + if (this.comparisonEnabled) { + this.datasourcePages.forEach(datasourcePage => { + datasourcePage.data.forEach((datasource, dIndex) => { + if (datasource.isAdditional) { + const origDatasource = this.datasourcePages[datasource.origDatasourceIndex].data[dIndex]; + datasource.dataKeys.forEach((dataKey) => { + if (dataKey.settings.comparisonSettings.color) { + dataKey.color = dataKey.settings.comparisonSettings.color; + } + const origDataKey = origDatasource.dataKeys[dataKey.origDataKeyIndex]; + origDataKey.settings.comparisonSettings.color = dataKey.color; + }); + } + }); + }); + } + if (this.caulculateLegendData) { + this.data.forEach((dataSetHolder, keyIndex) => { + this.updateLegend(keyIndex, dataSetHolder.data, false); + }); + this.callbacks.legendDataUpdated(this, true); + } + } + + private entityDataToDatasourceData(datasource: Datasource, data: Array): Array { + return datasource.dataKeys.map((dataKey, keyIndex) => { + dataKey.hidden = !!dataKey.settings.hideDataByDefault; + dataKey.inLegend = !dataKey.settings.removeFromLegend; + dataKey.label = this.ctx.utils.customTranslation(dataKey.label, dataKey.label); + if (this.comparisonEnabled && dataKey.isAdditional && dataKey.settings.comparisonSettings.comparisonValuesLabel) { + dataKey.label = createLabelFromDatasource(datasource, dataKey.settings.comparisonSettings.comparisonValuesLabel); + } else { + if (this.comparisonEnabled && dataKey.isAdditional) { + dataKey.label = dataKey.label + ' ' + this.ctx.translate.instant('legend.comparison-time-ago.' + this.timeForComparison); + } + dataKey.pattern = dataKey.label; + dataKey.label = createLabelFromDatasource(datasource, dataKey.pattern); + } + const datasourceData: DatasourceData = { + datasource, + dataKey, + data: [] + }; + if (data && data[keyIndex] && data[keyIndex].data) { + datasourceData.data = data[keyIndex].data; + } + return datasourceData; + }); + } + + private entityDataToDatasource(configDatasource: Datasource, entityData: EntityData, index: number): Datasource { + const newDatasource = deepClone(configDatasource); + const entityInfo = entityDataToEntityInfo(entityData); + updateDatasourceFromEntityInfo(newDatasource, entityInfo); + newDatasource.generated = index > 0; + return newDatasource; + } + + private dataUpdated(data: DataSetHolder, datasourceIndex: number, dataIndex: number, dataKeyIndex: number, detectChanges: boolean) { + const configuredDatasource = this.configuredDatasources[datasourceIndex]; + const startIndex = configuredDatasource.dataKeyStartIndex; + const dataKeysCount = configuredDatasource.dataKeys.length; + const index = startIndex + dataIndex * dataKeysCount + dataKeyIndex; + let update = true; + let currentData: DataSetHolder; + if (this.displayLegend && this.legendData.keys.find(key => key.dataIndex === index).dataKey.hidden) { + currentData = this.hiddenData[index]; + } else { + currentData = this.data[index]; + } + if (this.type === widgetType.latest) { + const prevData = currentData.data; + if (!data.data.length) { + update = false; + } else if (prevData && prevData[0] && prevData[0].length > 1 && data.data.length > 0) { + const prevTs = prevData[0][0]; + const prevValue = prevData[0][1]; + if (prevTs === data.data[0][0] && prevValue === data.data[0][1]) { + update = false; + } + } + } + if (update) { + if (this.subscriptionTimewindow && this.subscriptionTimewindow.realtimeWindowMs) { + this.updateTimewindow(); + if (this.timewindowForComparison && this.timewindowForComparison.realtimeWindowMs) { + this.updateComparisonTimewindow(); + } + } + currentData.data = data.data; + if (this.caulculateLegendData) { + this.updateLegend(index, data.data, detectChanges); + } + this.notifyDataLoaded(); + this.onDataUpdated(detectChanges); + } + } + + private alarmsLoaded(alarms: PageData, allowedEntities: number, totalEntities: number) { + this.alarms = alarms; + if (totalEntities > allowedEntities) { + const message = this.ctx.translate.instant('widget.alarm-data-overflow', + { allowedEntities, totalEntities }); + this.onSubscriptionMessage({ + severity: 'warn', + message + }); + } + if (this.subscriptionTimewindow && this.subscriptionTimewindow.realtimeWindowMs) { + this.updateTimewindow(); + } + this.onDataUpdated(); + } + + private alarmsUpdated(_updated: Array, alarms: PageData) { + this.alarmsLoaded(alarms, 0, 0); + } + + private updateLegend(dataIndex: number, data: DataSet, detectChanges: boolean) { + const dataKey = this.legendData.keys.find(key => key.dataIndex === dataIndex).dataKey; + const decimals = isDefinedAndNotNull(dataKey.decimals) ? dataKey.decimals : this.decimals; + const units = dataKey.units && dataKey.units.length ? dataKey.units : this.units; + const legendKeyData = this.legendData.data[dataIndex]; + if (this.legendConfig.showMin) { + legendKeyData.min = this.ctx.widgetUtils.formatValue(calculateMin(data), decimals, units); + } + if (this.legendConfig.showMax) { + legendKeyData.max = this.ctx.widgetUtils.formatValue(calculateMax(data), decimals, units); + } + if (this.legendConfig.showAvg) { + legendKeyData.avg = this.ctx.widgetUtils.formatValue(calculateAvg(data), decimals, units); + } + if (this.legendConfig.showTotal) { + legendKeyData.total = this.ctx.widgetUtils.formatValue(calculateTotal(data), decimals, units); + } + this.callbacks.legendDataUpdated(this, detectChanges !== false); + } + + private loadStDiff(): Observable { + const loadSubject = new ReplaySubject(1); + if (this.ctx.getServerTimeDiff && this.timeWindow) { + this.ctx.getServerTimeDiff().subscribe( + (stDiff) => { + this.timeWindow.stDiff = stDiff; + loadSubject.next(); + loadSubject.complete(); + }, + () => { + this.timeWindow.stDiff = 0; + loadSubject.next(); + loadSubject.complete(); + } + ); + } else { + if (this.timeWindow) { + this.timeWindow.stDiff = 0; + } + loadSubject.next(); + loadSubject.complete(); + } + return loadSubject.asObservable(); + } +} + +function calculateMin(data: DataSet): number { + if (data.length > 0) { + let result = Number(data[0][1]); + for (let i = 1; i < data.length; i++) { + result = Math.min(result, Number(data[i][1])); + } + return result; + } else { + return null; + } +} + +function calculateMax(data: DataSet): number { + if (data.length > 0) { + let result = Number(data[0][1]); + for (let i = 1; i < data.length; i++) { + result = Math.max(result, Number(data[i][1])); + } + return result; + } else { + return null; + } +} + +function calculateAvg(data: DataSet): number { + if (data.length > 0) { + return calculateTotal(data) / data.length; + } else { + return null; + } +} + +function calculateTotal(data: DataSet): number { + if (data.length > 0) { + let result = 0; + data.forEach((dataRow) => { + result += Number(dataRow[1]); + }); + return result; + } else { + return null; + } +} diff --git a/ui-ngx/src/app/core/auth/auth.actions.ts b/ui-ngx/src/app/core/auth/auth.actions.ts new file mode 100644 index 0000000000..5ad856d99a --- /dev/null +++ b/ui-ngx/src/app/core/auth/auth.actions.ts @@ -0,0 +1,58 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Action } from '@ngrx/store'; +import { User } from '@shared/models/user.model'; +import { AuthPayload } from '@core/auth/auth.models'; + +export enum AuthActionTypes { + AUTHENTICATED = '[Auth] Authenticated', + UNAUTHENTICATED = '[Auth] Unauthenticated', + LOAD_USER = '[Auth] Load User', + UPDATE_USER_DETAILS = '[Auth] Update User Details', + UPDATE_LAST_PUBLIC_DASHBOARD_ID = '[Auth] Update Last Public Dashboard Id' +} + +export class ActionAuthAuthenticated implements Action { + readonly type = AuthActionTypes.AUTHENTICATED; + + constructor(readonly payload: AuthPayload) {} +} + +export class ActionAuthUnauthenticated implements Action { + readonly type = AuthActionTypes.UNAUTHENTICATED; +} + +export class ActionAuthLoadUser implements Action { + readonly type = AuthActionTypes.LOAD_USER; + + constructor(readonly payload: { isUserLoaded: boolean }) {} +} + +export class ActionAuthUpdateUserDetails implements Action { + readonly type = AuthActionTypes.UPDATE_USER_DETAILS; + + constructor(readonly payload: { userDetails: User }) {} +} + +export class ActionAuthUpdateLastPublicDashboardId implements Action { + readonly type = AuthActionTypes.UPDATE_LAST_PUBLIC_DASHBOARD_ID; + + constructor(readonly payload: { lastPublicDashboardId: string }) {} +} + +export type AuthActions = ActionAuthAuthenticated | ActionAuthUnauthenticated | + ActionAuthLoadUser | ActionAuthUpdateUserDetails | ActionAuthUpdateLastPublicDashboardId; diff --git a/ui-ngx/src/app/core/auth/auth.models.ts b/ui-ngx/src/app/core/auth/auth.models.ts new file mode 100644 index 0000000000..c7210da6ed --- /dev/null +++ b/ui-ngx/src/app/core/auth/auth.models.ts @@ -0,0 +1,36 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AuthUser, User } from '@shared/models/user.model'; + +export interface AuthPayload { + authUser: AuthUser; + userDetails: User; + userTokenAccessEnabled: boolean; + allowedDashboardIds: string[]; + forceFullscreen: boolean; +} + +export interface AuthState { + isAuthenticated: boolean; + isUserLoaded: boolean; + authUser: AuthUser; + userDetails: User; + userTokenAccessEnabled: boolean; + allowedDashboardIds: string[]; + forceFullscreen: boolean; + lastPublicDashboardId: string; +} diff --git a/ui-ngx/src/app/core/auth/auth.reducer.ts b/ui-ngx/src/app/core/auth/auth.reducer.ts new file mode 100644 index 0000000000..6e02d03191 --- /dev/null +++ b/ui-ngx/src/app/core/auth/auth.reducer.ts @@ -0,0 +1,59 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AuthPayload, AuthState } from './auth.models'; +import { AuthActions, AuthActionTypes } from './auth.actions'; + +const emptyUserAuthState: AuthPayload = { + authUser: null, + userDetails: null, + userTokenAccessEnabled: false, + forceFullscreen: false, + allowedDashboardIds: [] +}; + +export const initialState: AuthState = { + isAuthenticated: false, + isUserLoaded: false, + lastPublicDashboardId: null, + ...emptyUserAuthState +}; + +export function authReducer( + state: AuthState = initialState, + action: AuthActions +): AuthState { + switch (action.type) { + case AuthActionTypes.AUTHENTICATED: + return { ...state, isAuthenticated: true, ...action.payload }; + + case AuthActionTypes.UNAUTHENTICATED: + return { ...state, isAuthenticated: false, ...emptyUserAuthState }; + + case AuthActionTypes.LOAD_USER: + return { ...state, ...action.payload, isAuthenticated: action.payload.isUserLoaded ? state.isAuthenticated : false, + ...action.payload.isUserLoaded ? {} : emptyUserAuthState }; + + case AuthActionTypes.UPDATE_USER_DETAILS: + return { ...state, ...action.payload}; + + case AuthActionTypes.UPDATE_LAST_PUBLIC_DASHBOARD_ID: + return { ...state, ...action.payload}; + + default: + return state; + } +} diff --git a/ui-ngx/src/app/core/auth/auth.selectors.ts b/ui-ngx/src/app/core/auth/auth.selectors.ts new file mode 100644 index 0000000000..76dffe659e --- /dev/null +++ b/ui-ngx/src/app/core/auth/auth.selectors.ts @@ -0,0 +1,72 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { createFeatureSelector, createSelector, select, Store } from '@ngrx/store'; + +import { AppState } from '../core.state'; +import { AuthState } from './auth.models'; +import { take } from 'rxjs/operators'; +import { AuthUser } from '@shared/models/user.model'; + +export const selectAuthState = createFeatureSelector( + 'auth' +); + +export const selectAuth = createSelector( + selectAuthState, + (state: AuthState) => state +); + +export const selectIsAuthenticated = createSelector( + selectAuthState, + (state: AuthState) => state.isAuthenticated +); + +export const selectIsUserLoaded = createSelector( + selectAuthState, + (state: AuthState) => state.isUserLoaded +); + +export const selectAuthUser = createSelector( + selectAuthState, + (state: AuthState) => state.authUser +); + +export const selectUserDetails = createSelector( + selectAuthState, + (state: AuthState) => state.userDetails +); + +export const selectUserTokenAccessEnabled = createSelector( + selectAuthState, + (state: AuthState) => state.userTokenAccessEnabled +); + +export function getCurrentAuthState(store: Store): AuthState { + let state: AuthState; + store.pipe(select(selectAuth), take(1)).subscribe( + val => state = val + ); + return state; +} + +export function getCurrentAuthUser(store: Store): AuthUser { + let authUser: AuthUser; + store.pipe(select(selectAuthUser), take(1)).subscribe( + val => authUser = val + ); + return authUser; +} diff --git a/ui-ngx/src/app/core/auth/auth.service.spec.ts b/ui-ngx/src/app/core/auth/auth.service.spec.ts new file mode 100644 index 0000000000..7457dbda7f --- /dev/null +++ b/ui-ngx/src/app/core/auth/auth.service.spec.ts @@ -0,0 +1,28 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { TestBed } from '@angular/core/testing'; + +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: AuthService = TestBed.get(AuthService); + expect(service).toBeTruthy(); + }); +}); diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts new file mode 100644 index 0000000000..1f8788d821 --- /dev/null +++ b/ui-ngx/src/app/core/auth/auth.service.ts @@ -0,0 +1,614 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable, NgZone } from '@angular/core'; +import { JwtHelperService } from '@auth0/angular-jwt'; +import { HttpClient } from '@angular/common/http'; + +import { forkJoin, Observable, of, ReplaySubject, throwError } from 'rxjs'; +import { catchError, map, mergeMap, tap } from 'rxjs/operators'; + +import { LoginRequest, LoginResponse, PublicLoginRequest } from '@shared/models/login.models'; +import { ActivatedRoute, Router, UrlTree } from '@angular/router'; +import { defaultHttpOptions } from '../http/http-utils'; +import { UserService } from '../http/user.service'; +import { Store } from '@ngrx/store'; +import { AppState } from '../core.state'; +import { ActionAuthAuthenticated, ActionAuthLoadUser, ActionAuthUnauthenticated } from './auth.actions'; +import { getCurrentAuthState, getCurrentAuthUser } from './auth.selectors'; +import { Authority } from '@shared/models/authority.enum'; +import { ActionSettingsChangeLanguage } from '@app/core/settings/settings.actions'; +import { AuthPayload, AuthState } from '@core/auth/auth.models'; +import { TranslateService } from '@ngx-translate/core'; +import { AuthUser } from '@shared/models/user.model'; +import { TimeService } from '@core/services/time.service'; +import { UtilsService } from '@core/services/utils.service'; +import { DashboardService } from '@core/http/dashboard.service'; +import { PageLink } from '@shared/models/page/page-link'; +import { DashboardInfo } from '@shared/models/dashboard.models'; +import { PageData } from '@app/shared/models/page/page-data'; +import { AdminService } from '@core/http/admin.service'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; +import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component'; +import { OAuth2ClientInfo } from '@shared/models/oauth2.models'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthService { + + constructor( + private store: Store, + private http: HttpClient, + private userService: UserService, + private timeService: TimeService, + private router: Router, + private route: ActivatedRoute, + private zone: NgZone, + private utils: UtilsService, + private dashboardService: DashboardService, + private adminService: AdminService, + private translate: TranslateService, + private dialog: MatDialog + ) { + } + + redirectUrl: string; + oauth2Clients: Array = null; + + private refreshTokenSubject: ReplaySubject = null; + private jwtHelper = new JwtHelperService(); + + private static _storeGet(key) { + return localStorage.getItem(key); + } + + private static isTokenValid(prefix) { + const clientExpiration = AuthService._storeGet(prefix + '_expiration'); + return clientExpiration && Number(clientExpiration) > (new Date().valueOf() + 2000); + } + + public static isJwtTokenValid() { + return AuthService.isTokenValid('jwt_token'); + } + + private static clearTokenData() { + localStorage.removeItem('jwt_token'); + localStorage.removeItem('jwt_token_expiration'); + localStorage.removeItem('refresh_token'); + localStorage.removeItem('refresh_token_expiration'); + } + + public static getJwtToken() { + return AuthService._storeGet('jwt_token'); + } + + public reloadUser() { + this.loadUser(true).subscribe( + (authPayload) => { + this.notifyAuthenticated(authPayload); + this.notifyUserLoaded(true); + }, + () => { + this.notifyUnauthenticated(); + this.notifyUserLoaded(true); + } + ); + } + + + public login(loginRequest: LoginRequest): Observable { + return this.http.post('/api/auth/login', loginRequest, defaultHttpOptions()).pipe( + tap((loginResponse: LoginResponse) => { + this.setUserFromJwtToken(loginResponse.token, loginResponse.refreshToken, true); + } + )); + } + + public publicLogin(publicId: string): Observable { + const publicLoginRequest: PublicLoginRequest = { + publicId + }; + return this.http.post('/api/auth/login/public', publicLoginRequest, defaultHttpOptions()); + } + + public sendResetPasswordLink(email: string) { + return this.http.post('/api/noauth/resetPasswordByEmail', + {email}, defaultHttpOptions()); + } + + public activate(activateToken: string, password: string, sendActivationMail: boolean): Observable { + return this.http.post(`/api/noauth/activate?sendActivationMail=${sendActivationMail}`, + {activateToken, password}, defaultHttpOptions()).pipe( + tap((loginResponse: LoginResponse) => { + this.setUserFromJwtToken(loginResponse.token, loginResponse.refreshToken, true); + } + )); + } + + public resetPassword(resetToken: string, password: string): Observable { + return this.http.post('/api/noauth/resetPassword', {resetToken, password}, defaultHttpOptions()).pipe( + tap((loginResponse: LoginResponse) => { + this.setUserFromJwtToken(loginResponse.token, loginResponse.refreshToken, true); + } + )); + } + + public changePassword(currentPassword: string, newPassword: string) { + return this.http.post('/api/auth/changePassword', + {currentPassword, newPassword}, defaultHttpOptions()); + } + + public activateByEmailCode(emailCode: string): Observable { + return this.http.post(`/api/noauth/activateByEmailCode?emailCode=${emailCode}`, + null, defaultHttpOptions()); + } + + public resendEmailActivation(email: string) { + const encodeEmail = encodeURIComponent(email); + return this.http.post(`/api/noauth/resendEmailActivation?email=${encodeEmail}`, + null, defaultHttpOptions()); + } + + public loginAsUser(userId: string) { + return this.http.get(`/api/user/${userId}/token`, defaultHttpOptions()).pipe( + tap((loginResponse: LoginResponse) => { + this.setUserFromJwtToken(loginResponse.token, loginResponse.refreshToken, true); + } + )); + } + + public logout(captureLastUrl: boolean = false) { + if (captureLastUrl) { + this.redirectUrl = this.router.url; + } + this.http.post('/api/auth/logout', null, defaultHttpOptions(true, true)) + .subscribe(() => { + this.clearJwtToken(); + }, + () => { + this.clearJwtToken(); + } + ); + } + + private notifyUserLoaded(isUserLoaded: boolean) { + this.store.dispatch(new ActionAuthLoadUser({isUserLoaded})); + } + + public gotoDefaultPlace(isAuthenticated: boolean) { + const authState = getCurrentAuthState(this.store); + const url = this.defaultUrl(isAuthenticated, authState); + this.zone.run(() => { + this.router.navigateByUrl(url); + }); + } + + public loadOAuth2Clients(): Observable> { + return this.http.post>(`/api/noauth/oauth2Clients`, + null, defaultHttpOptions()).pipe( + catchError(err => of([])), + tap((OAuth2Clients) => { + this.oauth2Clients = OAuth2Clients; + }) + ); + } + + private forceDefaultPlace(authState?: AuthState, path?: string, params?: any): boolean { + if (authState && authState.authUser) { + if (authState.authUser.authority === Authority.TENANT_ADMIN || authState.authUser.authority === Authority.CUSTOMER_USER) { + if ((this.userHasDefaultDashboard(authState) && authState.forceFullscreen) || authState.authUser.isPublic) { + if (path === 'profile') { + if (this.userHasProfile(authState.authUser)) { + return false; + } else { + return true; + } + } else if (path.startsWith('dashboard.') || path.startsWith('dashboards.') && + authState.allowedDashboardIds.indexOf(params.dashboardId) > -1) { + return false; + } else { + return true; + } + } + } + } + return false; + } + + public defaultUrl(isAuthenticated: boolean, authState?: AuthState, path?: string, params?: any): UrlTree { + let result: UrlTree = null; + if (isAuthenticated) { + if (!path || path === 'login' || this.forceDefaultPlace(authState, path, params)) { + if (this.redirectUrl) { + const redirectUrl = this.redirectUrl; + this.redirectUrl = null; + result = this.router.parseUrl(redirectUrl); + } else { + result = this.router.parseUrl('home'); + } + if (authState.authUser.authority === Authority.TENANT_ADMIN || authState.authUser.authority === Authority.CUSTOMER_USER) { + if (this.userHasDefaultDashboard(authState)) { + const dashboardId = authState.userDetails.additionalInfo.defaultDashboardId; + if (authState.forceFullscreen) { + result = this.router.parseUrl(`dashboard/${dashboardId}`); + } else { + result = this.router.parseUrl(`dashboards/${dashboardId}`); + } + } else if (authState.authUser.isPublic) { + result = this.router.parseUrl(`dashboard/${authState.lastPublicDashboardId}`); + } + } else if (authState.authUser.authority === Authority.SYS_ADMIN) { + this.adminService.checkUpdates().subscribe((updateMessage) => { + if (updateMessage && updateMessage.updateAvailable) { + this.store.dispatch(new ActionNotificationShow( + {message: updateMessage.message, + type: 'info', + verticalPosition: 'bottom', + horizontalPosition: 'right'})); + } + }); + } + } + } else { + result = this.router.parseUrl('login'); + } + return result; + } + + private loadUser(doTokenRefresh): Observable { + const authUser = getCurrentAuthUser(this.store); + if (!authUser) { + const publicId = this.utils.getQueryParam('publicId'); + const accessToken = this.utils.getQueryParam('accessToken'); + const refreshToken = this.utils.getQueryParam('refreshToken'); + const username = this.utils.getQueryParam('username'); + const password = this.utils.getQueryParam('password'); + const loginError = this.utils.getQueryParam('loginError'); + if (publicId) { + return this.publicLogin(publicId).pipe( + mergeMap((response) => { + this.updateAndValidateToken(response.token, 'jwt_token', false); + this.updateAndValidateToken(response.refreshToken, 'refresh_token', false); + return this.procceedJwtTokenValidate(); + }), + catchError((err) => { + this.utils.updateQueryParam('publicId', null); + throw Error(); + }) + ); + } else if (accessToken) { + this.utils.updateQueryParam('accessToken', null); + if (refreshToken) { + this.utils.updateQueryParam('refreshToken', null); + } + try { + this.updateAndValidateToken(accessToken, 'jwt_token', false); + if (refreshToken) { + this.updateAndValidateToken(refreshToken, 'refresh_token', false); + } else { + localStorage.removeItem('refresh_token'); + localStorage.removeItem('refresh_token_expiration'); + } + } catch (e) { + return throwError(e); + } + return this.procceedJwtTokenValidate(); + } else if (username && password) { + this.utils.updateQueryParam('username', null); + this.utils.updateQueryParam('password', null); + const loginRequest: LoginRequest = { + username, + password + }; + return this.http.post('/api/auth/login', loginRequest, defaultHttpOptions()).pipe( + mergeMap((loginResponse: LoginResponse) => { + this.updateAndValidateToken(loginResponse.token, 'jwt_token', false); + this.updateAndValidateToken(loginResponse.refreshToken, 'refresh_token', false); + return this.procceedJwtTokenValidate(); + } + ) + ); + } else if (loginError) { + this.showLoginErrorDialog(loginError); + this.utils.updateQueryParam('loginError', null); + return throwError(Error()); + } + return this.procceedJwtTokenValidate(doTokenRefresh); + } else { + return of({} as AuthPayload); + } + } + + private showLoginErrorDialog(loginError: string) { + this.translate.get(['login.error', 'action.close']).subscribe( + (translations) => { + const dialogConfig: MatDialogConfig = { + disableClose: true, + data: { + title: translations['login.error'], + message: loginError, + ok: translations['action.close'] + } + }; + this.dialog.open(AlertDialogComponent, dialogConfig); + } + ); + } + + private procceedJwtTokenValidate(doTokenRefresh?: boolean): Observable { + const loadUserSubject = new ReplaySubject(); + this.validateJwtToken(doTokenRefresh).subscribe( + () => { + let authPayload = {} as AuthPayload; + const jwtToken = AuthService._storeGet('jwt_token'); + authPayload.authUser = this.jwtHelper.decodeToken(jwtToken); + if (authPayload.authUser && authPayload.authUser.scopes && authPayload.authUser.scopes.length) { + authPayload.authUser.authority = Authority[authPayload.authUser.scopes[0]]; + } else if (authPayload.authUser) { + authPayload.authUser.authority = Authority.ANONYMOUS; + } + if (authPayload.authUser.isPublic) { + authPayload.forceFullscreen = true; + } + if (authPayload.authUser.isPublic) { + this.loadSystemParams(authPayload).subscribe( + (sysParams) => { + authPayload = {...authPayload, ...sysParams}; + loadUserSubject.next(authPayload); + loadUserSubject.complete(); + }, + (err) => { + loadUserSubject.error(err); + } + ); + } else if (authPayload.authUser.userId) { + this.userService.getUser(authPayload.authUser.userId).subscribe( + (user) => { + authPayload.userDetails = user; + authPayload.forceFullscreen = false; + if (this.userForceFullscreen(authPayload)) { + authPayload.forceFullscreen = true; + } + this.loadSystemParams(authPayload).subscribe( + (sysParams) => { + authPayload = {...authPayload, ...sysParams}; + let userLang; + if (authPayload.userDetails.additionalInfo && authPayload.userDetails.additionalInfo.lang) { + userLang = authPayload.userDetails.additionalInfo.lang; + } else { + userLang = null; + } + this.notifyUserLang(userLang); + loadUserSubject.next(authPayload); + loadUserSubject.complete(); + }, + (err) => { + loadUserSubject.error(err); + this.logout(); + }); + }, + (err) => { + loadUserSubject.error(err); + this.logout(); + } + ); + } else { + loadUserSubject.error(null); + } + }, + (err) => { + loadUserSubject.error(err); + } + ); + return loadUserSubject; + } + + private loadIsUserTokenAccessEnabled(authUser: AuthUser): Observable { + if (authUser.authority === Authority.SYS_ADMIN || + authUser.authority === Authority.TENANT_ADMIN) { + return this.http.get('/api/user/tokenAccessEnabled', defaultHttpOptions()); + } else { + return of(false); + } + } + + private loadSystemParams(authPayload: AuthPayload): Observable { + const sources: Array> = [this.loadIsUserTokenAccessEnabled(authPayload.authUser), + this.fetchAllowedDashboardIds(authPayload), + this.timeService.loadMaxDatapointsLimit()]; + return forkJoin(sources) + .pipe(map((data) => { + const userTokenAccessEnabled: boolean = data[0]; + const allowedDashboardIds: string[] = data[1]; + return {userTokenAccessEnabled, allowedDashboardIds}; + })); + } + + public refreshJwtToken(): Observable { + let response: Observable = this.refreshTokenSubject; + if (this.refreshTokenSubject === null) { + this.refreshTokenSubject = new ReplaySubject(1); + response = this.refreshTokenSubject; + const refreshToken = AuthService._storeGet('refresh_token'); + const refreshTokenValid = AuthService.isTokenValid('refresh_token'); + this.setUserFromJwtToken(null, null, false); + if (!refreshTokenValid) { + this.refreshTokenSubject.error(new Error(this.translate.instant('access.refresh-token-expired'))); + this.refreshTokenSubject = null; + } else { + const refreshTokenRequest = { + refreshToken + }; + const refreshObservable = this.http.post('/api/auth/token', refreshTokenRequest, defaultHttpOptions()); + refreshObservable.subscribe((loginResponse: LoginResponse) => { + this.setUserFromJwtToken(loginResponse.token, loginResponse.refreshToken, false); + this.refreshTokenSubject.next(loginResponse); + this.refreshTokenSubject.complete(); + this.refreshTokenSubject = null; + }, () => { + this.clearJwtToken(); + this.refreshTokenSubject.error(new Error(this.translate.instant('access.refresh-token-failed'))); + this.refreshTokenSubject = null; + }); + } + } + return response; + } + + private validateJwtToken(doRefresh): Observable { + const subject = new ReplaySubject(); + if (!AuthService.isTokenValid('jwt_token')) { + if (doRefresh) { + this.refreshJwtToken().subscribe( + () => { + subject.next(); + subject.complete(); + }, + (err) => { + subject.error(err); + } + ); + } else { + this.clearJwtToken(); + subject.error(null); + } + } else { + subject.next(); + subject.complete(); + } + return subject; + } + + public refreshTokenPending() { + return this.refreshTokenSubject !== null; + } + + public setUserFromJwtToken(jwtToken, refreshToken, notify) { + if (!jwtToken) { + AuthService.clearTokenData(); + if (notify) { + this.notifyUnauthenticated(); + } + } else { + this.updateAndValidateToken(jwtToken, 'jwt_token', true); + this.updateAndValidateToken(refreshToken, 'refresh_token', true); + if (notify) { + this.notifyUserLoaded(false); + this.loadUser(false).subscribe( + (authPayload) => { + this.notifyUserLoaded(true); + this.notifyAuthenticated(authPayload); + }, + () => { + this.notifyUserLoaded(true); + this.notifyUnauthenticated(); + } + ); + } else { + this.loadUser(false).subscribe(); + } + } + } + + public parsePublicId(): string { + const token = AuthService.getJwtToken(); + if (token) { + const tokenData = this.jwtHelper.decodeToken(token); + if (tokenData && tokenData.isPublic) { + return tokenData.sub; + } + } + return null; + } + + private notifyUnauthenticated() { + this.store.dispatch(new ActionAuthUnauthenticated()); + } + + private notifyAuthenticated(authPayload: AuthPayload) { + this.store.dispatch(new ActionAuthAuthenticated(authPayload)); + } + + private notifyUserLang(userLang: string) { + this.store.dispatch(new ActionSettingsChangeLanguage({userLang})); + } + + private updateAndValidateToken(token, prefix, notify) { + let valid = false; + const tokenData = this.jwtHelper.decodeToken(token); + const issuedAt = tokenData.iat; + const expTime = tokenData.exp; + if (issuedAt && expTime) { + const ttl = expTime - issuedAt; + if (ttl > 0) { + const clientExpiration = new Date().valueOf() + ttl * 1000; + localStorage.setItem(prefix, token); + localStorage.setItem(prefix + '_expiration', '' + clientExpiration); + valid = true; + } + } + if (!valid && notify) { + this.notifyUnauthenticated(); + } + } + + private clearJwtToken() { + this.setUserFromJwtToken(null, null, true); + } + + private userForceFullscreen(authPayload: AuthPayload): boolean { + return (authPayload.authUser && authPayload.authUser.isPublic) || + (authPayload.userDetails && authPayload.userDetails.additionalInfo && + authPayload.userDetails.additionalInfo.defaultDashboardFullscreen && + authPayload.userDetails.additionalInfo.defaultDashboardFullscreen === true); + } + + private userHasProfile(authUser: AuthUser): boolean { + return authUser && !authUser.isPublic; + } + + private userHasDefaultDashboard(authState: AuthState): boolean { + if (authState && authState.userDetails && authState.userDetails.additionalInfo + && authState.userDetails.additionalInfo.defaultDashboardId) { + return true; + } else { + return false; + } + } + + private fetchAllowedDashboardIds(authPayload: AuthPayload): Observable { + if (authPayload.forceFullscreen && (authPayload.authUser.authority === Authority.TENANT_ADMIN || + authPayload.authUser.authority === Authority.CUSTOMER_USER)) { + const pageLink = new PageLink(100); + let fetchDashboardsObservable: Observable>; + if (authPayload.authUser.authority === Authority.TENANT_ADMIN) { + fetchDashboardsObservable = this.dashboardService.getTenantDashboards(pageLink); + } else { + fetchDashboardsObservable = this.dashboardService.getCustomerDashboards(authPayload.authUser.customerId, pageLink); + } + return fetchDashboardsObservable.pipe( + map((result) => { + const dashboards = result.data; + return dashboards.map(dashboard => dashboard.id.id); + }) + ); + } else { + return of([]); + } + } +} diff --git a/ui-ngx/src/app/core/core.module.ts b/ui-ngx/src/app/core/core.module.ts new file mode 100644 index 0000000000..3defaea872 --- /dev/null +++ b/ui-ngx/src/app/core/core.module.ts @@ -0,0 +1,108 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule } from '@angular/common/http'; +import { StoreModule } from '@ngrx/store'; +import { EffectsModule } from '@ngrx/effects'; +import { StoreDevtoolsModule } from '@ngrx/store-devtools'; +import { GlobalHttpInterceptor } from './interceptors/global-http-interceptor'; +import { effects, metaReducers, reducers } from './core.state'; +import { environment as env } from '@env/environment'; + +import { + MissingTranslationHandler, + TranslateCompiler, + TranslateLoader, + TranslateModule, + TranslateParser +} from '@ngx-translate/core'; +import { TranslateHttpLoader } from '@ngx-translate/http-loader'; +import { TbMissingTranslationHandler } from './translate/missing-translate-handler'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { FlexLayoutModule } from '@angular/flex-layout'; +import { TranslateDefaultCompiler } from '@core/translate/translate-default-compiler'; +import { WINDOW_PROVIDERS } from '@core/services/window.service'; +import { HotkeyModule } from 'angular2-hotkeys'; +import { TranslateDefaultParser } from '@core/translate/translate-default-parser'; + +export function HttpLoaderFactory(http: HttpClient) { + return new TranslateHttpLoader(http, './assets/locale/locale.constant-', '.json'); +} + +@NgModule({ + imports: [ + CommonModule, + HttpClientModule, + FlexLayoutModule.withConfig({addFlexToParent: false}), + MatDialogModule, + MatButtonModule, + MatSnackBarModule, + + // ngx-translate + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: HttpLoaderFactory, + deps: [HttpClient] + }, + missingTranslationHandler: { + provide: MissingTranslationHandler, + useClass: TbMissingTranslationHandler + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateDefaultCompiler + }, + parser: { + provide: TranslateParser, + useClass: TranslateDefaultParser + } + }), + HotkeyModule.forRoot(), + + // ngrx + StoreModule.forRoot(reducers, + { metaReducers, + runtimeChecks: { + strictStateImmutability: true, + strictActionImmutability: true, + strictStateSerializability: true, + strictActionSerializability: true + }} + ), + EffectsModule.forRoot(effects), + env.production + ? [] + : StoreDevtoolsModule.instrument({ + name: env.appTitle + }) + ], + providers: [ + { + provide: HTTP_INTERCEPTORS, + useClass: GlobalHttpInterceptor, + multi: true + }, + WINDOW_PROVIDERS + ], + exports: [] +}) +export class CoreModule { +} diff --git a/ui-ngx/src/app/core/core.state.ts b/ui-ngx/src/app/core/core.state.ts new file mode 100644 index 0000000000..6dc03f3b0a --- /dev/null +++ b/ui-ngx/src/app/core/core.state.ts @@ -0,0 +1,61 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ActionReducerMap, MetaReducer } from '@ngrx/store'; +import { storeFreeze } from 'ngrx-store-freeze'; + +import { environment as env } from '@env/environment'; + +import { initStateFromLocalStorage } from './meta-reducers/init-state-from-local-storage.reducer'; +import { debug } from './meta-reducers/debug.reducer'; +import { LoadState } from './interceptors/load.models'; +import { loadReducer } from './interceptors/load.reducer'; +import { AuthState } from './auth/auth.models'; +import { authReducer } from './auth/auth.reducer'; +import { settingsReducer } from '@app/core/settings/settings.reducer'; +import { SettingsState } from '@app/core/settings/settings.models'; +import { Type } from '@angular/core'; +import { SettingsEffects } from '@app/core/settings/settings.effects'; +import { NotificationState } from '@app/core/notification/notification.models'; +import { notificationReducer } from '@app/core/notification/notification.reducer'; +import { NotificationEffects } from '@app/core/notification/notification.effects'; + +export const reducers: ActionReducerMap = { + load: loadReducer, + auth: authReducer, + settings: settingsReducer, + notification: notificationReducer +}; + +export const metaReducers: MetaReducer[] = [ + initStateFromLocalStorage +]; +if (!env.production) { + metaReducers.unshift(storeFreeze); + metaReducers.unshift(debug); +} + +export const effects: Type[] = [ + SettingsEffects, + NotificationEffects +]; + +export interface AppState { + load: LoadState; + auth: AuthState; + settings: SettingsState; + notification: NotificationState; +} diff --git a/ui-ngx/src/app/core/css/css.js b/ui-ngx/src/app/core/css/css.js new file mode 100644 index 0000000000..29b8ba164d --- /dev/null +++ b/ui-ngx/src/app/core/css/css.js @@ -0,0 +1,686 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* eslint-disable */ + +/* jshint unused:false */ +/* global base64_decode, CSSWizardView, window, console, jQuery */ +var fi = function() { + + this.cssImportStatements = []; + this.cssKeyframeStatements = []; + + this.cssRegex = new RegExp("([\\s\\S]*?){([\\s\\S]*?)}", "gi"); + this.cssMediaQueryRegex = "((@media [\\s\\S]*?){([\\s\\S]*?}\\s*?)})"; + this.cssKeyframeRegex = "((@.*?keyframes [\\s\\S]*?){([\\s\\S]*?}\\s*?)})"; + this.combinedCSSRegex = "((\\s*?@media[\\s\\S]*?){([\\s\\S]*?)}\\s*?})|(([\\s\\S]*?){([\\s\\S]*?)})"; //to match css & media queries together + this.cssCommentsRegex = "(\\/\\*[\\s\\S]*?\\*\\/)"; + this.cssImportStatementRegex = new RegExp("@import .*?;", "gi"); +}; + +/* + Strip outs css comments and returns cleaned css string + + @param css, the original css string to be stipped out of comments + + @return cleanedCSS contains no css comments + */ +fi.prototype.stripComments = function(cssString) { + var regex = new RegExp(this.cssCommentsRegex, "gi"); + + return cssString.replace(regex, ""); +}; + +/* + Parses given css string, and returns css object + keys as selectors and values are css rules + eliminates all css comments before parsing + + @param source css string to be parsed + + @return object css + */ +fi.prototype.parseCSS = function(source) { + + if (source === undefined) { + return []; + } + + var css = []; + //strip out comments + //source = this.stripComments(source); + + //get import statements + + while (true) { + var imports = this.cssImportStatementRegex.exec(source); + if (imports !== null) { + this.cssImportStatements.push(imports[0]); + css.push({ + selector: "@imports", + type: "imports", + styles: imports[0], + }); + } else { + break; + } + } + source = source.replace(this.cssImportStatementRegex, ""); + //get keyframe statements + var keyframesRegex = new RegExp(this.cssKeyframeRegex, "gi"); + var arr; + while (true) { + arr = keyframesRegex.exec(source); + if (arr === null) { + break; + } + css.push({ + selector: "@keyframes", + type: "keyframes", + styles: arr[0], + }); + } + source = source.replace(keyframesRegex, ""); + + //unified regex + var unified = new RegExp(this.combinedCSSRegex, "gi"); + + while (true) { + arr = unified.exec(source); + if (arr === null) { + break; + } + var selector = ""; + if (arr[2] === undefined) { + selector = arr[5].split("\r\n").join("\n").trim(); + } else { + selector = arr[2].split("\r\n").join("\n").trim(); + } + + /* + fetch comments and associate it with current selector + */ + var commentsRegex = new RegExp(this.cssCommentsRegex, "gi"); + var comments = commentsRegex.exec(selector); + if (comments !== null) { + selector = selector.replace(commentsRegex, "").trim(); + } + + //determine the type + if (selector.indexOf("@media") !== -1) { + //we have a media query + var cssObject = { + selector: selector, + type: "media", + subStyles: this.parseCSS(arr[3] + "\n}"), //recursively parse media query inner css + }; + if (comments !== null) { + cssObject.comments = comments[0]; + } + css.push(cssObject); + } else { + //we have standart css + var rules = this.parseRules(arr[6]); + var style = { + selector: selector, + rules: rules, + }; + if (selector === "@font-face") { + style.type = "font-face"; + } + if (comments !== null) { + style.comments = comments[0]; + } + css.push(style); + } + } + + return css; +}; + +/* + parses given string containing css directives + and returns an array of objects containing ruleName:ruleValue pairs + + @param rules, css directive string example + \n\ncolor:white;\n font-size:18px;\n + */ +fi.prototype.parseRules = function(rules) { + //convert all windows style line endings to unix style line endings + rules = rules.split("\r\n").join("\n"); + var ret = []; + + // Split all rules but keep semicolon for base64 url data + rules = rules.split(/;(?![^\(]*\))/); + + //proccess rules line by line + for (var i = 0; i < rules.length; i++) { + var line = rules[i]; + + //determine if line is a valid css directive, ie color:white; + line = line.trim(); + if (line.indexOf(":") !== -1) { + //line contains : + line = line.split(":"); + var cssDirective = line[0].trim(); + var cssValue = line.slice(1).join(":").trim(); + + //more checks + if (cssDirective.length < 1 || cssValue.length < 1) { + continue; //there is no css directive or value that is of length 1 or 0 + // PLAIN WRONG WHAT ABOUT margin:0; ? + } + + //push rule + ret.push({ + directive: cssDirective, + value: cssValue, + }); + } else { + //if there is no ':', but what if it was mis splitted value which starts with base64 + if (line.trim().substr(0, 7) == "base64,") { //hack :) + ret[ret.length - 1].value += line.trim(); + } else { + //add rule, even if it is defective + if (line.length > 0) { + ret.push({ + directive: "", + value: line, + defective: true, + }); + } + } + } + } + + return ret; //we are done! +}; +/* + just returns the rule having given directive + if not found returns false; + */ +fi.prototype.findCorrespondingRule = function(rules, directive, value) { + if (value === undefined) { + value = false; + } + var ret = false; + for (var i = 0; i < rules.length; i++) { + if (rules[i].directive == directive) { + ret = rules[i]; + if (value === rules[i].value) { + break; + } + } + } + return ret; +}; + +/* + Finds styles that have given selector, compress them, + and returns them + */ +fi.prototype.findBySelector = function(cssObjectArray, selector, contains) { + if (contains === undefined) { + contains = false; + } + + var found = []; + for (var i = 0; i < cssObjectArray.length; i++) { + if (contains === false) { + if (cssObjectArray[i].selector === selector) { + found.push(cssObjectArray[i]); + } + } else { + if (cssObjectArray[i].selector.indexOf(selector) !== -1) { + found.push(cssObjectArray[i]); + } + } + + } + if (found.length < 2) { + return found; + } else { + var base = found[0]; + for (i = 1; i < found.length; i++) { + this.intelligentCSSPush([base], found[i]); + } + return [base]; //we are done!! all properties merged into base! + } +}; + +/* + deletes cssObjects having given selector, and returns new array + */ +fi.prototype.deleteBySelector = function(cssObjectArray, selector) { + var ret = []; + for (var i = 0; i < cssObjectArray.length; i++) { + if (cssObjectArray[i].selector !== selector) { + ret.push(cssObjectArray[i]); + } + } + return ret; +}; + +/* + Compresses given cssObjectArray and tries to minimize + selector redundence. + */ +fi.prototype.compressCSS = function(cssObjectArray) { + var compressed = []; + var done = {}; + for (var i = 0; i < cssObjectArray.length; i++) { + var obj = cssObjectArray[i]; + if (done[obj.selector] === true) { + continue; + } + + var found = this.findBySelector(cssObjectArray, obj.selector); //found compressed + if (found.length !== 0) { + compressed.push(found[0]); + done[obj.selector] = true; + } + } + return compressed; +}; + +/* + Received 2 css objects with following structure + { + rules : [{directive:"", value:""}, {directive:"", value:""}, ...] + selector : "SOMESELECTOR" + } + + returns the changed(new,removed,updated) values on css1 parameter, on same structure + + if two css objects are the same, then returns false + + if a css directive exists in css1 and css2, and its value is different, it is included in diff + if a css directive exists in css1 and not css2, it is then included in diff + if a css directive exists in css2 but not css1, then it is deleted in css1, it would be included in diff but will be marked as type='DELETED' + + @object css1 css object + @object css2 css object + + @return diff css object contains changed values in css1 in regards to css2 see test input output in /test/data/css.js + */ +fi.prototype.cssDiff = function(css1, css2) { + if (css1.selector !== css2.selector) { + return false; + } + + //if one of them is media query return false, because diff function can not operate on media queries + if ((css1.type === "media" || css2.type === "media")) { + return false; + } + + var diff = { + selector: css1.selector, + rules: [], + }; + var rule1, rule2; + for (var i = 0; i < css1.rules.length; i++) { + rule1 = css1.rules[i]; + //find rule2 which has the same directive as rule1 + rule2 = this.findCorrespondingRule(css2.rules, rule1.directive, rule1.value); + if (rule2 === false) { + //rule1 is a new rule in css1 + diff.rules.push(rule1); + } else { + //rule2 was found only push if its value is different too + if (rule1.value !== rule2.value) { + diff.rules.push(rule1); + } + } + } + + //now for rules exists in css2 but not in css1, which means deleted rules + for (var ii = 0; ii < css2.rules.length; ii++) { + rule2 = css2.rules[ii]; + //find rule2 which has the same directive as rule1 + rule1 = this.findCorrespondingRule(css1.rules, rule2.directive); + if (rule1 === false) { + //rule1 is a new rule + rule2.type = "DELETED"; //mark it as a deleted rule, so that other merge operations could be true + diff.rules.push(rule2); + } + } + + if (diff.rules.length === 0) { + return false; + } + return diff; +}; + +/* + Merges 2 different css objects together + using intelligentCSSPush, + + @param cssObjectArray, target css object array + @param newArray, source array that will be pushed into cssObjectArray parameter + @param reverse, [optional], if given true, first parameter will be traversed on reversed order + effectively giving priority to the styles in newArray + */ +fi.prototype.intelligentMerge = function(cssObjectArray, newArray, reverse) { + if (reverse === undefined) { + reverse = false; + } + + for (var i = 0; i < newArray.length; i++) { + this.intelligentCSSPush(cssObjectArray, newArray[i], reverse); + } + for (i = 0; i < cssObjectArray.length; i++) { + var cobj = cssObjectArray[i]; + if (cobj.type === "media" || (cobj.type === "keyframes")) { + continue; + } + cobj.rules = this.compactRules(cobj.rules); + } +}; + +/* + inserts new css objects into a bigger css object + with same selectors groupped together + + @param cssObjectArray, array of bigger css object to be pushed into + @param minimalObject, single css object + @param reverse [optional] default is false, if given, cssObjectArray will be reversly traversed + resulting more priority in minimalObject's styles + */ +fi.prototype.intelligentCSSPush = function(cssObjectArray, minimalObject, reverse) { + var pushSelector = minimalObject.selector; + //find correct selector if not found just push minimalObject into cssObject + var cssObject = false; + + if (reverse === undefined) { + reverse = false; + } + + if (reverse === false) { + for (var i = 0; i < cssObjectArray.length; i++) { + if (cssObjectArray[i].selector === minimalObject.selector) { + cssObject = cssObjectArray[i]; + break; + } + } + } else { + for (var j = cssObjectArray.length - 1; j > -1; j--) { + if (cssObjectArray[j].selector === minimalObject.selector) { + cssObject = cssObjectArray[j]; + break; + } + } + } + + if (cssObject === false) { + cssObjectArray.push(minimalObject); //just push, because cssSelector is new + } else { + if (minimalObject.type !== "media") { + for (var ii = 0; ii < minimalObject.rules.length; ii++) { + var rule = minimalObject.rules[ii]; + //find rule inside cssObject + var oldRule = this.findCorrespondingRule(cssObject.rules, rule.directive); + if (oldRule === false) { + cssObject.rules.push(rule); + } else if (rule.type == "DELETED") { + oldRule.type = "DELETED"; + } else { + //rule found just update value + + oldRule.value = rule.value; + } + } + } else { + cssObject.subStyles = minimalObject.subStyles; //TODO, make this intelligent too + } + + } +}; + +/* + filter outs rule objects whose type param equal to DELETED + + @param rules, array of rules + + @returns rules array, compacted by deleting all unneccessary rules + */ +fi.prototype.compactRules = function(rules) { + var newRules = []; + for (var i = 0; i < rules.length; i++) { + if (rules[i].type !== "DELETED") { + newRules.push(rules[i]); + } + } + return newRules; +}; +/* + computes string for ace editor using this.css or given cssBase optional parameter + + @param [optional] cssBase, if given computes cssString from cssObject array + */ +fi.prototype.getCSSForEditor = function(cssBase, depth) { + if (depth === undefined) { + depth = 0; + } + var ret = ""; + if (cssBase === undefined) { + cssBase = this.css; + } + //append imports + for (var i = 0; i < cssBase.length; i++) { + if (cssBase[i].type == "imports") { + ret += cssBase[i].styles + "\n\n"; + } + } + for (i = 0; i < cssBase.length; i++) { + var tmp = cssBase[i]; + if (tmp.selector === undefined) { //temporarily omit media queries + continue; + } + var comments = ""; + if (tmp.comments !== undefined) { + comments = tmp.comments + "\n"; + } + + if (tmp.type == "media") { //also put media queries to output + ret += comments + tmp.selector + "{\n"; + ret += this.getCSSForEditor(tmp.subStyles, depth + 1); + ret += "}\n\n"; + } else if (tmp.type !== "keyframes" && tmp.type !== "imports") { + ret += this.getSpaces(depth) + comments + tmp.selector + " {\n"; + ret += this.getCSSOfRules(tmp.rules, depth + 1); + ret += this.getSpaces(depth) + "}\n\n"; + } + } + + //append keyFrames + for (i = 0; i < cssBase.length; i++) { + if (cssBase[i].type == "keyframes") { + ret += cssBase[i].styles + "\n\n"; + } + } + + return ret; +}; + +fi.prototype.getImports = function(cssObjectArray) { + var imps = []; + for (var i = 0; i < cssObjectArray.length; i++) { + if (cssObjectArray[i].type == "imports") { + imps.push(cssObjectArray[i].styles); + } + } + return imps; +}; +/* + given rules array, returns visually formatted css string + to be used inside editor + */ +fi.prototype.getCSSOfRules = function(rules, depth) { + var ret = ""; + for (var i = 0; i < rules.length; i++) { + if (rules[i] === undefined) { + continue; + } + if (rules[i].defective === undefined) { + ret += this.getSpaces(depth) + rules[i].directive + " : " + rules[i].value + ";\n"; + } else { + ret += this.getSpaces(depth) + rules[i].value + ";\n"; + } + + } + return ret || "\n"; +}; + +/* + A very simple helper function returns number of spaces appended in a single string, + the number depends input parameter, namely input*2 + */ +fi.prototype.getSpaces = function(num) { + var ret = ""; + for (var i = 0; i < num * 4; i++) { + ret += " "; + } + return ret; +}; + +/* + Given css string or objectArray, parses it and then for every selector, + prepends this.cssPreviewNamespace to prevent css collision issues + + @returns css string in which this.cssPreviewNamespace prepended + */ +fi.prototype.applyNamespacing = function(css, forcedNamespace) { + var cssObjectArray = css; + var namespaceClass = "." + this.cssPreviewNamespace; + if (forcedNamespace !== undefined) { + namespaceClass = forcedNamespace; + } + + if (typeof css === "string") { + cssObjectArray = this.parseCSS(css); + } + + for (var i = 0; i < cssObjectArray.length; i++) { + var obj = cssObjectArray[i]; + + //bypass namespacing for @font-face @keyframes @import + if (obj.selector.indexOf("@font-face") > -1 || obj.selector.indexOf("keyframes") > -1 || obj.selector.indexOf("@import") > -1 || obj.selector.indexOf(".form-all") > -1 || obj.selector.indexOf("#stage") > -1) { + continue; + } + + if (obj.type !== "media") { + var selector = obj.selector.split(","); + var newSelector = []; + for (var j = 0; j < selector.length; j++) { + if (selector[j].indexOf(".supernova") === -1) { //do not apply namespacing to selectors including supernova + newSelector.push(namespaceClass + " " + selector[j]); + } else { + newSelector.push(selector[j]); + } + } + obj.selector = newSelector.join(","); + } else { + obj.subStyles = this.applyNamespacing(obj.subStyles, forcedNamespace); //handle media queries as well + } + } + + return cssObjectArray; +}; + +/* + given css string or object array, clears possible namespacing from + all of the selectors inside the css + */ +fi.prototype.clearNamespacing = function(css, returnObj) { + if (returnObj === undefined) { + returnObj = false; + } + var cssObjectArray = css; + var namespaceClass = "." + this.cssPreviewNamespace; + if (typeof css === "string") { + cssObjectArray = this.parseCSS(css); + } + + for (var i = 0; i < cssObjectArray.length; i++) { + var obj = cssObjectArray[i]; + + if (obj.type !== "media") { + var selector = obj.selector.split(","); + var newSelector = []; + for (var j = 0; j < selector.length; j++) { + newSelector.push(selector[j].split(namespaceClass + " ").join("")); + } + obj.selector = newSelector.join(","); + } else { + obj.subStyles = this.clearNamespacing(obj.subStyles, true); //handle media queries as well + } + } + if (returnObj === false) { + return this.getCSSForEditor(cssObjectArray); + } else { + return cssObjectArray; + } + +}; + +/* + creates a new style tag (also destroys the previous one) + and injects given css string into that css tag + */ +fi.prototype.createStyleElement = function(id, css, format) { + if (format === undefined) { + format = false; + } + + if (this.testMode === false && format !== "nonamespace") { + //apply namespacing classes + css = this.applyNamespacing(css); + } + + if (typeof css != "string") { + css = this.getCSSForEditor(css); + } + //apply formatting for css + if (format === true) { + css = this.getCSSForEditor(this.parseCSS(css)); + } + + if (this.testMode !== false) { + return this.testMode("create style #" + id, css); //if test mode, just pass result to callback + } + + var __el = document.getElementById(id); + if (__el) { + __el.parentNode.removeChild(__el); + } + + var head = document.head || document.getElementsByTagName("head")[0], + style = document.createElement("style"); + + style.id = id; + style.type = "text/css"; + + head.appendChild(style); + + if (style.styleSheet && !style.sheet) { + style.styleSheet.cssText = css; + } else { + style.appendChild(document.createTextNode(css)); + } +}; + +export default fi; + +/* eslint-enable */ diff --git a/ui-ngx/src/app/core/guards/auth.guard.ts b/ui-ngx/src/app/core/guards/auth.guard.ts new file mode 100644 index 0000000000..5be6b78206 --- /dev/null +++ b/ui-ngx/src/app/core/guards/auth.guard.ts @@ -0,0 +1,133 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable, NgZone } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, RouterStateSnapshot } from '@angular/router'; +import { AuthService } from '../auth/auth.service'; +import { select, Store } from '@ngrx/store'; +import { AppState } from '../core.state'; +import { selectAuth } from '../auth/auth.selectors'; +import { catchError, map, mergeMap, skipWhile, take } from 'rxjs/operators'; +import { AuthState } from '../auth/auth.models'; +import { forkJoin, Observable, of } from 'rxjs'; +import { enterZone } from '@core/operator/enterZone'; +import { Authority } from '@shared/models/authority.enum'; +import { DialogService } from '@core/services/dialog.service'; +import { TranslateService } from '@ngx-translate/core'; +import { UtilsService } from '@core/services/utils.service'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthGuard implements CanActivate, CanActivateChild { + + constructor(private store: Store, + private authService: AuthService, + private dialogService: DialogService, + private utils: UtilsService, + private translate: TranslateService, + private zone: NgZone) {} + + getAuthState(): Observable { + return this.store.pipe( + select(selectAuth), + skipWhile((authState) => !authState || !authState.isUserLoaded), + take(1), + enterZone(this.zone) + ); + } + + canActivate(next: ActivatedRouteSnapshot, + state: RouterStateSnapshot) { + + return this.getAuthState().pipe( + mergeMap((authState) => { + const url: string = state.url; + + let lastChild = state.root; + const urlSegments: string[] = []; + if (lastChild.url) { + urlSegments.push(...lastChild.url.map(segment => segment.path)); + } + while (lastChild.children.length) { + lastChild = lastChild.children[0]; + if (lastChild.url) { + urlSegments.push(...lastChild.url.map(segment => segment.path)); + } + } + const path = urlSegments.join('.'); + const publicId = this.utils.getQueryParam('publicId'); + const data = lastChild.data || {}; + const params = lastChild.params || {}; + const isPublic = data.module === 'public'; + + if (!authState.isAuthenticated) { + if (publicId && publicId.length > 0) { + this.authService.setUserFromJwtToken(null, null, false); + this.authService.reloadUser(); + return of(false); + } else if (!isPublic) { + this.authService.redirectUrl = url; + // this.authService.gotoDefaultPlace(false); + return of(this.authService.defaultUrl(false)); + } else { + if (path === 'login') { + return forkJoin([this.authService.loadOAuth2Clients()]).pipe( + map(() => { + return true; + }) + ); + } else { + return of(true); + } + } + } else { + if (authState.authUser.isPublic) { + if (this.authService.parsePublicId() !== publicId) { + if (publicId && publicId.length > 0) { + this.authService.setUserFromJwtToken(null, null, false); + this.authService.reloadUser(); + } else { + this.authService.logout(); + } + return of(false); + } + } + const defaultUrl = this.authService.defaultUrl(true, authState, path, params); + if (defaultUrl) { + // this.authService.gotoDefaultPlace(true); + return of(defaultUrl); + } else { + const authority = Authority[authState.authUser.authority]; + if (data.auth && data.auth.indexOf(authority) === -1) { + this.dialogService.forbidden(); + return of(false); + } else { + return of(true); + } + } + } + }), + catchError((err => { console.error(err); return of(false); } )) + ); + } + + canActivateChild( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot) { + return this.canActivate(route, state); + } +} diff --git a/ui-ngx/src/app/core/guards/confirm-on-exit.guard.ts b/ui-ngx/src/app/core/guards/confirm-on-exit.guard.ts new file mode 100644 index 0000000000..e3ee9a5330 --- /dev/null +++ b/ui-ngx/src/app/core/guards/confirm-on-exit.guard.ts @@ -0,0 +1,77 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, CanDeactivate, RouterStateSnapshot } from '@angular/router'; +import { FormGroup } from '@angular/forms'; +import { select, Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { AuthState } from '@core/auth/auth.models'; +import { selectAuth } from '@core/auth/auth.selectors'; +import { take } from 'rxjs/operators'; +import { DialogService } from '@core/services/dialog.service'; +import { TranslateService } from '@ngx-translate/core'; +import { isDefined } from '../utils'; + +export interface HasConfirmForm { + confirmForm(): FormGroup; +} + +export interface HasDirtyFlag { + isDirty: boolean; +} + +@Injectable({ + providedIn: 'root' +}) +export class ConfirmOnExitGuard implements CanDeactivate { + + constructor(private store: Store, + private dialogService: DialogService, + private translate: TranslateService) { } + + canDeactivate(component: HasConfirmForm & HasDirtyFlag, + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot) { + + + let auth: AuthState = null; + this.store.pipe(select(selectAuth), take(1)).subscribe( + (authState: AuthState) => { + auth = authState; + } + ); + + if (auth && auth.isAuthenticated) { + let isDirty = false; + if (component.confirmForm) { + const confirmForm = component.confirmForm(); + if (confirmForm) { + isDirty = confirmForm.dirty; + } + } else if (isDefined(component.isDirty)) { + isDirty = component.isDirty; + } + if (isDirty) { + return this.dialogService.confirm( + this.translate.instant('confirm-on-exit.title'), + this.translate.instant('confirm-on-exit.html-message') + ); + } + } + return true; + } +} diff --git a/ui-ngx/src/app/core/http/admin.service.ts b/ui-ngx/src/app/core/http/admin.service.ts new file mode 100644 index 0000000000..f575f56906 --- /dev/null +++ b/ui-ngx/src/app/core/http/admin.service.ts @@ -0,0 +1,59 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { Observable } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { AdminSettings, MailServerSettings, SecuritySettings, UpdateMessage } from '@shared/models/settings.models'; + +@Injectable({ + providedIn: 'root' +}) +export class AdminService { + + constructor( + private http: HttpClient + ) { } + + public getAdminSettings(key: string, config?: RequestConfig): Observable> { + return this.http.get>(`/api/admin/settings/${key}`, defaultHttpOptionsFromConfig(config)); + } + + public saveAdminSettings(adminSettings: AdminSettings, + config?: RequestConfig): Observable> { + return this.http.post>('/api/admin/settings', adminSettings, defaultHttpOptionsFromConfig(config)); + } + + public sendTestMail(adminSettings: AdminSettings, + config?: RequestConfig): Observable { + return this.http.post('/api/admin/settings/testMail', adminSettings, defaultHttpOptionsFromConfig(config)); + } + + public getSecuritySettings(config?: RequestConfig): Observable { + return this.http.get(`/api/admin/securitySettings`, defaultHttpOptionsFromConfig(config)); + } + + public saveSecuritySettings(securitySettings: SecuritySettings, + config?: RequestConfig): Observable { + return this.http.post('/api/admin/securitySettings', securitySettings, + defaultHttpOptionsFromConfig(config)); + } + + public checkUpdates(config?: RequestConfig): Observable { + return this.http.get(`/api/admin/updates`, defaultHttpOptionsFromConfig(config)); + } +} diff --git a/ui-ngx/src/app/core/http/alarm.service.ts b/ui-ngx/src/app/core/http/alarm.service.ts new file mode 100644 index 0000000000..211fc413e9 --- /dev/null +++ b/ui-ngx/src/app/core/http/alarm.service.ts @@ -0,0 +1,85 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { Observable } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { PageData } from '@shared/models/page/page-data'; +import { EntityId } from '@shared/models/id/entity-id'; +import { + Alarm, + AlarmInfo, + AlarmQuery, + AlarmSearchStatus, + AlarmSeverity, + AlarmStatus +} from '@shared/models/alarm.models'; +import { UtilsService } from '@core/services/utils.service'; + +@Injectable({ + providedIn: 'root' +}) +export class AlarmService { + + constructor( + private http: HttpClient, + private utils: UtilsService + ) { } + + public getAlarm(alarmId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/alarm/${alarmId}`, defaultHttpOptionsFromConfig(config)); + } + + public getAlarmInfo(alarmId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/alarm/info/${alarmId}`, defaultHttpOptionsFromConfig(config)); + } + + public saveAlarm(alarm: Alarm, config?: RequestConfig): Observable { + return this.http.post('/api/alarm', alarm, defaultHttpOptionsFromConfig(config)); + } + + public ackAlarm(alarmId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/alarm/${alarmId}/ack`, null, defaultHttpOptionsFromConfig(config)); + } + + public clearAlarm(alarmId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/alarm/${alarmId}/clear`, null, defaultHttpOptionsFromConfig(config)); + } + + public deleteAlarm(alarmId: string, config?: RequestConfig): Observable { + return this.http.delete(`/api/alarm/${alarmId}`, defaultHttpOptionsFromConfig(config)); + } + + public getAlarms(query: AlarmQuery, + config?: RequestConfig): Observable> { + return this.http.get>(`/api/alarm${query.toQuery()}`, + defaultHttpOptionsFromConfig(config)); + } + + public getHighestAlarmSeverity(entityId: EntityId, alarmSearchStatus: AlarmSearchStatus, alarmStatus: AlarmStatus, + config?: RequestConfig): Observable { + let url = `/api/alarm/highestSeverity/${entityId.entityType}/${entityId.entityType}`; + if (alarmSearchStatus) { + url += `?searchStatus=${alarmSearchStatus}`; + } else if (alarmStatus) { + url += `?status=${alarmStatus}`; + } + return this.http.get(url, + defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/http/asset.service.ts b/ui-ngx/src/app/core/http/asset.service.ts new file mode 100644 index 0000000000..f272dccfc0 --- /dev/null +++ b/ui-ngx/src/app/core/http/asset.service.ts @@ -0,0 +1,92 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { Observable } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { PageLink } from '@shared/models/page/page-link'; +import { PageData } from '@shared/models/page/page-data'; +import { EntitySubtype } from '@app/shared/models/entity-type.models'; +import { Asset, AssetInfo, AssetSearchQuery } from '@app/shared/models/asset.models'; + +@Injectable({ + providedIn: 'root' +}) +export class AssetService { + + constructor( + private http: HttpClient + ) { } + + public getTenantAssetInfos(pageLink: PageLink, type: string = '', config?: RequestConfig): Observable> { + return this.http.get>(`/api/tenant/assetInfos${pageLink.toQuery()}&type=${type}`, + defaultHttpOptionsFromConfig(config)); + } + + public getCustomerAssetInfos(customerId: string, pageLink: PageLink, type: string = '', + config?: RequestConfig): Observable> { + return this.http.get>(`/api/customer/${customerId}/assetInfos${pageLink.toQuery()}&type=${type}`, + defaultHttpOptionsFromConfig(config)); + } + + public getAsset(assetId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/asset/${assetId}`, defaultHttpOptionsFromConfig(config)); + } + + public getAssets(assetIds: Array, config?: RequestConfig): Observable> { + return this.http.get>(`/api/assets?assetIds=${assetIds.join(',')}`, defaultHttpOptionsFromConfig(config)); + } + + public getAssetInfo(assetId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/asset/info/${assetId}`, defaultHttpOptionsFromConfig(config)); + } + + public saveAsset(asset: Asset, config?: RequestConfig): Observable { + return this.http.post('/api/asset', asset, defaultHttpOptionsFromConfig(config)); + } + + public deleteAsset(assetId: string, config?: RequestConfig) { + return this.http.delete(`/api/asset/${assetId}`, defaultHttpOptionsFromConfig(config)); + } + + public getAssetTypes(config?: RequestConfig): Observable> { + return this.http.get>('/api/asset/types', defaultHttpOptionsFromConfig(config)); + } + + public makeAssetPublic(assetId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/customer/public/asset/${assetId}`, null, defaultHttpOptionsFromConfig(config)); + } + + public assignAssetToCustomer(customerId: string, assetId: string, + config?: RequestConfig): Observable { + return this.http.post(`/api/customer/${customerId}/asset/${assetId}`, null, defaultHttpOptionsFromConfig(config)); + } + + public unassignAssetFromCustomer(assetId: string, config?: RequestConfig) { + return this.http.delete(`/api/customer/asset/${assetId}`, defaultHttpOptionsFromConfig(config)); + } + + public findByQuery(query: AssetSearchQuery, + config?: RequestConfig): Observable> { + return this.http.post>('/api/assets', query, defaultHttpOptionsFromConfig(config)); + } + + public findByName(assetName: string, config?: RequestConfig): Observable { + return this.http.get(`/api/tenant/assets?assetName=${assetName}`, defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/http/attribute.service.ts b/ui-ngx/src/app/core/http/attribute.service.ts new file mode 100644 index 0000000000..c9e4246d86 --- /dev/null +++ b/ui-ngx/src/app/core/http/attribute.service.ts @@ -0,0 +1,138 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { forkJoin, Observable, of } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { EntityId } from '@shared/models/id/entity-id'; +import { AttributeData, AttributeScope, DataSortOrder, TimeseriesData } from '@shared/models/telemetry/telemetry.models'; +import { isDefinedAndNotNull } from '@core/utils'; +import { AggregationType } from '@shared/models/time/time.models'; + +@Injectable({ + providedIn: 'root' +}) +export class AttributeService { + + constructor( + private http: HttpClient + ) { } + + public getEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, + keys?: Array, config?: RequestConfig): Observable> { + let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/attributes/${attributeScope}`; + if (keys && keys.length) { + url += `?keys=${keys.join(',')}`; + } + return this.http.get>(url, defaultHttpOptionsFromConfig(config)); + } + + public deleteEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: Array, + config?: RequestConfig): Observable { + const keys = attributes.map(attribute => encodeURI(attribute.key)).join(','); + return this.http.delete(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/${attributeScope}` + + `?keys=${keys}`, + defaultHttpOptionsFromConfig(config)); + } + + public deleteEntityTimeseries(entityId: EntityId, timeseries: Array, deleteAllDataForKeys = false, + config?: RequestConfig): Observable { + const keys = timeseries.map(attribute => encodeURI(attribute.key)).join(','); + return this.http.delete(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete` + + `?keys=${keys}&deleteAllDataForKeys=${deleteAllDataForKeys}`, + defaultHttpOptionsFromConfig(config)); + } + + public saveEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: Array, + config?: RequestConfig): Observable { + const attributesData: {[key: string]: any} = {}; + const deleteAttributes: AttributeData[] = []; + attributes.forEach((attribute) => { + if (isDefinedAndNotNull(attribute.value)) { + attributesData[attribute.key] = attribute.value; + } else { + deleteAttributes.push(attribute); + } + }); + let deleteEntityAttributesObservable: Observable; + if (deleteAttributes.length) { + deleteEntityAttributesObservable = this.deleteEntityAttributes(entityId, attributeScope, deleteAttributes, config); + } else { + deleteEntityAttributesObservable = of(null); + } + let saveEntityAttributesObservable: Observable; + if (Object.keys(attributesData).length) { + saveEntityAttributesObservable = this.http.post(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/${attributeScope}`, + attributesData, defaultHttpOptionsFromConfig(config)); + } else { + saveEntityAttributesObservable = of(null); + } + return forkJoin([saveEntityAttributesObservable, deleteEntityAttributesObservable]); + } + + public saveEntityTimeseries(entityId: EntityId, timeseriesScope: string, timeseries: Array, + config?: RequestConfig): Observable { + const timeseriesData: {[key: string]: any} = {}; + const deleteTimeseries: AttributeData[] = []; + timeseries.forEach((attribute) => { + if (isDefinedAndNotNull(attribute.value)) { + timeseriesData[attribute.key] = attribute.value; + } else { + deleteTimeseries.push(attribute); + } + }); + let deleteEntityTimeseriesObservable: Observable; + if (deleteTimeseries.length) { + deleteEntityTimeseriesObservable = this.deleteEntityTimeseries(entityId, deleteTimeseries, true, config); + } else { + deleteEntityTimeseriesObservable = of(null); + } + let saveEntityTimeseriesObservable: Observable; + if (Object.keys(timeseriesData).length) { + saveEntityTimeseriesObservable = + this.http.post(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/${timeseriesScope}`, + timeseriesData, defaultHttpOptionsFromConfig(config)); + } else { + saveEntityTimeseriesObservable = of(null); + } + return forkJoin([saveEntityTimeseriesObservable, deleteEntityTimeseriesObservable]); + } + + public getEntityTimeseries(entityId: EntityId, keys: Array, startTs: number, endTs: number, + limit: number = 100, agg: AggregationType = AggregationType.NONE, interval?: number, + orderBy: DataSortOrder = DataSortOrder.DESC, useStrictDataTypes: boolean = false, + config?: RequestConfig): Observable { + let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/timeseries?keys=${keys.join(',')}&startTs=${startTs}&endTs=${endTs}`; + if (isDefinedAndNotNull(limit)) { + url += `&limit=${limit}`; + } + if (isDefinedAndNotNull(agg)) { + url += `&agg=${agg}`; + } + if (isDefinedAndNotNull(interval)) { + url += `&interval=${interval}`; + } + if (isDefinedAndNotNull(orderBy)) { + url += `&orderBy=${orderBy}`; + } + if (isDefinedAndNotNull(useStrictDataTypes)) { + url += `&useStrictDataTypes=${useStrictDataTypes}`; + } + + return this.http.get(url, defaultHttpOptionsFromConfig(config)); + } +} diff --git a/ui-ngx/src/app/core/http/audit-log.service.ts b/ui-ngx/src/app/core/http/audit-log.service.ts new file mode 100644 index 0000000000..1b485ff15d --- /dev/null +++ b/ui-ngx/src/app/core/http/audit-log.service.ts @@ -0,0 +1,59 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { Observable } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { TimePageLink } from '@shared/models/page/page-link'; +import { PageData } from '@shared/models/page/page-data'; +import { AuditLog } from '@shared/models/audit-log.models'; +import { EntityId } from '@shared/models/id/entity-id'; + +@Injectable({ + providedIn: 'root' +}) +export class AuditLogService { + + constructor( + private http: HttpClient + ) { } + + public getAuditLogs(pageLink: TimePageLink, + config?: RequestConfig): Observable> { + return this.http.get>(`/api/audit/logs${pageLink.toQuery()}`, + defaultHttpOptionsFromConfig(config)); + } + + public getAuditLogsByCustomerId(customerId: string, pageLink: TimePageLink, + config?: RequestConfig): Observable> { + return this.http.get>(`/api/audit/logs/customer/${customerId}${pageLink.toQuery()}`, + defaultHttpOptionsFromConfig(config)); + } + + public getAuditLogsByUserId(userId: string, pageLink: TimePageLink, + config?: RequestConfig): Observable> { + return this.http.get>(`/api/audit/logs/user/${userId}${pageLink.toQuery()}`, + defaultHttpOptionsFromConfig(config)); + } + + public getAuditLogsByEntityId(entityId: EntityId, pageLink: TimePageLink, + config?: RequestConfig): Observable> { + return this.http.get>(`/api/audit/logs/entity/${entityId.entityType}/${entityId.id}${pageLink.toQuery()}`, + defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/http/component-descriptor.service.ts b/ui-ngx/src/app/core/http/component-descriptor.service.ts new file mode 100644 index 0000000000..a3198b862a --- /dev/null +++ b/ui-ngx/src/app/core/http/component-descriptor.service.ts @@ -0,0 +1,102 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { ComponentDescriptor, ComponentType } from '@shared/models/component-descriptor.models'; +import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; +import { Observable, of } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { RuleNodeType } from '@shared/models/rule-node.models'; + +@Injectable({ + providedIn: 'root' +}) +export class ComponentDescriptorService { + + private componentsByType: Map> = + new Map>(); + private componentsByClazz: Map = new Map(); + + constructor( + private http: HttpClient + ) { + } + + public getComponentDescriptorsByType(componentType: ComponentType, config?: RequestConfig): Observable> { + const existing = this.componentsByType.get(componentType); + if (existing) { + return of(existing); + } else { + return this.http.get>(`/api/components/${componentType}`, defaultHttpOptionsFromConfig(config)).pipe( + map((componentDescriptors) => { + this.componentsByType.set(componentType, componentDescriptors); + componentDescriptors.forEach((componentDescriptor) => { + this.componentsByClazz.set(componentDescriptor.clazz, componentDescriptor); + }); + return componentDescriptors; + }) + ); + } + } + + public getComponentDescriptorsByTypes(componentTypes: Array, + config?: RequestConfig): Observable> { + let result: ComponentDescriptor[] = []; + for (let i = componentTypes.length - 1; i >= 0; i--) { + const componentType = componentTypes[i]; + const componentDescriptors = this.componentsByType.get(componentType); + if (componentDescriptors) { + result = result.concat(componentDescriptors); + componentTypes.splice(i, 1); + } + } + if (!componentTypes.length) { + return of(result); + } else { + return this.http.get>(`/api/components?componentTypes=${componentTypes.join(',')}`, + defaultHttpOptionsFromConfig(config)).pipe( + map((componentDescriptors) => { + componentDescriptors.forEach((componentDescriptor) => { + let componentsList = this.componentsByType.get(componentDescriptor.type); + if (!componentsList) { + componentsList = new Array(); + this.componentsByType.set(componentDescriptor.type, componentsList); + } + componentsList.push(componentDescriptor); + this.componentsByClazz.set(componentDescriptor.clazz, componentDescriptor); + }); + result = result.concat(componentDescriptors); + return result; + }) + ); + } + } + + public getComponentDescriptorByClazz(componentDescriptorClazz: string, config?: RequestConfig): Observable { + const existing = this.componentsByClazz.get(componentDescriptorClazz); + if (existing) { + return of(existing); + } else { + return this.http.get(`/api/component/${componentDescriptorClazz}`, defaultHttpOptionsFromConfig(config)).pipe( + map((componentDescriptor) => { + this.componentsByClazz.set(componentDescriptorClazz, componentDescriptor); + return componentDescriptor; + }) + ); + } + } +} diff --git a/ui-ngx/src/app/core/http/customer.service.ts b/ui-ngx/src/app/core/http/customer.service.ts new file mode 100644 index 0000000000..4623ac7438 --- /dev/null +++ b/ui-ngx/src/app/core/http/customer.service.ts @@ -0,0 +1,51 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { Observable } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { PageLink } from '@shared/models/page/page-link'; +import { PageData } from '@shared/models/page/page-data'; +import { Customer } from '@shared/models/customer.model'; + +@Injectable({ + providedIn: 'root' +}) +export class CustomerService { + + constructor( + private http: HttpClient + ) { } + + public getCustomers(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/customers${pageLink.toQuery()}`, + defaultHttpOptionsFromConfig(config)); + } + + public getCustomer(customerId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/customer/${customerId}`, defaultHttpOptionsFromConfig(config)); + } + + public saveCustomer(customer: Customer, config?: RequestConfig): Observable { + return this.http.post('/api/customer', customer, defaultHttpOptionsFromConfig(config)); + } + + public deleteCustomer(customerId: string, config?: RequestConfig) { + return this.http.delete(`/api/customer/${customerId}`, defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/http/dashboard.service.ts b/ui-ngx/src/app/core/http/dashboard.service.ts new file mode 100644 index 0000000000..42ba3834c9 --- /dev/null +++ b/ui-ngx/src/app/core/http/dashboard.service.ts @@ -0,0 +1,160 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Inject, Injectable } from '@angular/core'; +import { defaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { Observable } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { PageLink } from '@shared/models/page/page-link'; +import { PageData } from '@shared/models/page/page-data'; +import { Dashboard, DashboardInfo } from '@shared/models/dashboard.models'; +import { WINDOW } from '@core/services/window.service'; +import { NavigationEnd, Router } from '@angular/router'; +import { filter, map, publishReplay, refCount } from 'rxjs/operators'; + +// @dynamic +@Injectable({ + providedIn: 'root' +}) +export class DashboardService { + + stDiffObservable: Observable; + currentUrl: string; + + constructor( + private http: HttpClient, + private router: Router, + @Inject(WINDOW) private window: Window + ) { + this.currentUrl = this.router.url.split('?')[0]; + this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe( + () => { + const newUrl = this.router.url.split('?')[0]; + if (this.currentUrl !== newUrl) { + this.stDiffObservable = null; + this.currentUrl = newUrl; + } + } + ); + } + + public getTenantDashboards(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/tenant/dashboards${pageLink.toQuery()}`, + defaultHttpOptionsFromConfig(config)); + } + + public getTenantDashboardsByTenantId(tenantId: string, pageLink: PageLink, + config?: RequestConfig): Observable> { + return this.http.get>(`/api/tenant/${tenantId}/dashboards${pageLink.toQuery()}`, + defaultHttpOptionsFromConfig(config)); + } + + public getCustomerDashboards(customerId: string, pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/customer/${customerId}/dashboards${pageLink.toQuery()}`, + defaultHttpOptionsFromConfig(config)); + } + + public getDashboard(dashboardId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/dashboard/${dashboardId}`, defaultHttpOptionsFromConfig(config)); + } + + public getDashboardInfo(dashboardId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/dashboard/info/${dashboardId}`, defaultHttpOptionsFromConfig(config)); + } + + public saveDashboard(dashboard: Dashboard, config?: RequestConfig): Observable { + return this.http.post('/api/dashboard', dashboard, defaultHttpOptionsFromConfig(config)); + } + + public deleteDashboard(dashboardId: string, config?: RequestConfig) { + return this.http.delete(`/api/dashboard/${dashboardId}`, defaultHttpOptionsFromConfig(config)); + } + + public assignDashboardToCustomer(customerId: string, dashboardId: string, + config?: RequestConfig): Observable { + return this.http.post(`/api/customer/${customerId}/dashboard/${dashboardId}`, + null, defaultHttpOptionsFromConfig(config)); + } + + public unassignDashboardFromCustomer(customerId: string, dashboardId: string, + config?: RequestConfig) { + return this.http.delete(`/api/customer/${customerId}/dashboard/${dashboardId}`, defaultHttpOptionsFromConfig(config)); + } + + public makeDashboardPublic(dashboardId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/customer/public/dashboard/${dashboardId}`, null, + defaultHttpOptionsFromConfig(config)); + } + + public makeDashboardPrivate(dashboardId: string, config?: RequestConfig): Observable { + return this.http.delete(`/api/customer/public/dashboard/${dashboardId}`, + defaultHttpOptionsFromConfig(config)); + } + + public updateDashboardCustomers(dashboardId: string, customerIds: Array, + config?: RequestConfig): Observable { + return this.http.post(`/api/dashboard/${dashboardId}/customers`, customerIds, + defaultHttpOptionsFromConfig(config)); + } + + public addDashboardCustomers(dashboardId: string, customerIds: Array, + config?: RequestConfig): Observable { + return this.http.post(`/api/dashboard/${dashboardId}/customers/add`, customerIds, + defaultHttpOptionsFromConfig(config)); + } + + public removeDashboardCustomers(dashboardId: string, customerIds: Array, + config?: RequestConfig): Observable { + return this.http.post(`/api/dashboard/${dashboardId}/customers/remove`, customerIds, + defaultHttpOptionsFromConfig(config)); + } + + public getPublicDashboardLink(dashboard: DashboardInfo): string | null { + if (dashboard && dashboard.assignedCustomers && dashboard.assignedCustomers.length > 0) { + const publicCustomers = dashboard.assignedCustomers + .filter(customerInfo => customerInfo.public); + if (publicCustomers.length > 0) { + const publicCustomerId = publicCustomers[0].customerId.id; + let url = this.window.location.protocol + '//' + this.window.location.hostname; + const port = this.window.location.port; + if (port !== '80' && port !== '443') { + url += ':' + port; + } + url += `/dashboard/${dashboard.id.id}?publicId=${publicCustomerId}`; + return url; + } + } + return null; + } + + public getServerTimeDiff(): Observable { + if (!this.stDiffObservable) { + const url = '/api/dashboard/serverTime'; + const ct1 = Date.now(); + this.stDiffObservable = this.http.get(url, defaultHttpOptions(true)).pipe( + map((st) => { + const ct2 = Date.now(); + const stDiff = Math.ceil(st - (ct1 + ct2) / 2); + return stDiff; + }), + publishReplay(1), + refCount() + ); + } + return this.stDiffObservable; + } + +} diff --git a/ui-ngx/src/app/core/http/device-profile.service.ts b/ui-ngx/src/app/core/http/device-profile.service.ts new file mode 100644 index 0000000000..c535878fd2 --- /dev/null +++ b/ui-ngx/src/app/core/http/device-profile.service.ts @@ -0,0 +1,72 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { PageLink } from '@shared/models/page/page-link'; +import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { Observable } from 'rxjs'; +import { PageData } from '@shared/models/page/page-data'; +import { DeviceProfile, DeviceProfileInfo, DeviceTransportType } from '@shared/models/device.models'; +import { isDefinedAndNotNull } from '@core/utils'; + +@Injectable({ + providedIn: 'root' +}) +export class DeviceProfileService { + + constructor( + private http: HttpClient + ) { } + + public getDeviceProfiles(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/deviceProfiles${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + + public getDeviceProfile(deviceProfileId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/deviceProfile/${deviceProfileId}`, defaultHttpOptionsFromConfig(config)); + } + + public saveDeviceProfile(deviceProfile: DeviceProfile, config?: RequestConfig): Observable { + return this.http.post('/api/deviceProfile', deviceProfile, defaultHttpOptionsFromConfig(config)); + } + + public deleteDeviceProfile(deviceProfileId: string, config?: RequestConfig) { + return this.http.delete(`/api/deviceProfile/${deviceProfileId}`, defaultHttpOptionsFromConfig(config)); + } + + public setDefaultDeviceProfile(deviceProfileId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/deviceProfile/${deviceProfileId}/default`, defaultHttpOptionsFromConfig(config)); + } + + public getDefaultDeviceProfileInfo(config?: RequestConfig): Observable { + return this.http.get('/api/deviceProfileInfo/default', defaultHttpOptionsFromConfig(config)); + } + + public getDeviceProfileInfo(deviceProfileId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/deviceProfileInfo/${deviceProfileId}`, defaultHttpOptionsFromConfig(config)); + } + + public getDeviceProfileInfos(pageLink: PageLink, transportType?: DeviceTransportType, + config?: RequestConfig): Observable> { + let url = `/api/deviceProfileInfos${pageLink.toQuery()}`; + if (isDefinedAndNotNull(transportType)) { + url += `&transportType=${transportType}`; + } + return this.http.get>(url, defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/http/device.service.ts b/ui-ngx/src/app/core/http/device.service.ts new file mode 100644 index 0000000000..9d842d79a6 --- /dev/null +++ b/ui-ngx/src/app/core/http/device.service.ts @@ -0,0 +1,158 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { Observable, ReplaySubject } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { PageLink } from '@shared/models/page/page-link'; +import { PageData } from '@shared/models/page/page-data'; +import { + ClaimRequest, + ClaimResult, + Device, + DeviceCredentials, + DeviceInfo, + DeviceSearchQuery +} from '@app/shared/models/device.models'; +import { EntitySubtype } from '@app/shared/models/entity-type.models'; +import { AuthService } from '@core/auth/auth.service'; + +@Injectable({ + providedIn: 'root' +}) +export class DeviceService { + + constructor( + private http: HttpClient + ) { } + + public getTenantDeviceInfos(pageLink: PageLink, type: string = '', + config?: RequestConfig): Observable> { + return this.http.get>(`/api/tenant/deviceInfos${pageLink.toQuery()}&type=${type}`, + defaultHttpOptionsFromConfig(config)); + } + + public getTenantDeviceInfosByDeviceProfileId(pageLink: PageLink, deviceProfileId: string = '', + config?: RequestConfig): Observable> { + return this.http.get>(`/api/tenant/deviceInfos${pageLink.toQuery()}&deviceProfileId=${deviceProfileId}`, + defaultHttpOptionsFromConfig(config)); + } + + public getCustomerDeviceInfos(customerId: string, pageLink: PageLink, type: string = '', + config?: RequestConfig): Observable> { + return this.http.get>(`/api/customer/${customerId}/deviceInfos${pageLink.toQuery()}&type=${type}`, + defaultHttpOptionsFromConfig(config)); + } + + public getCustomerDeviceInfosByDeviceProfileId(customerId: string, pageLink: PageLink, deviceProfileId: string = '', + config?: RequestConfig): Observable> { + return this.http.get>(`/api/customer/${customerId}/deviceInfos${pageLink.toQuery()}&deviceProfileId=${deviceProfileId}`, + defaultHttpOptionsFromConfig(config)); + } + + public getDevice(deviceId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/device/${deviceId}`, defaultHttpOptionsFromConfig(config)); + } + + public getDevices(deviceIds: Array, config?: RequestConfig): Observable> { + return this.http.get>(`/api/devices?deviceIds=${deviceIds.join(',')}`, defaultHttpOptionsFromConfig(config)); + } + + public getDeviceInfo(deviceId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/device/info/${deviceId}`, defaultHttpOptionsFromConfig(config)); + } + + public saveDevice(device: Device, config?: RequestConfig): Observable { + return this.http.post('/api/device', device, defaultHttpOptionsFromConfig(config)); + } + + public deleteDevice(deviceId: string, config?: RequestConfig) { + return this.http.delete(`/api/device/${deviceId}`, defaultHttpOptionsFromConfig(config)); + } + + public getDeviceTypes(config?: RequestConfig): Observable> { + return this.http.get>('/api/device/types', defaultHttpOptionsFromConfig(config)); + } + + public getDeviceCredentials(deviceId: string, sync: boolean = false, config?: RequestConfig): Observable { + const url = `/api/device/${deviceId}/credentials`; + if (sync) { + const responseSubject = new ReplaySubject(); + const request = new XMLHttpRequest(); + request.open('GET', url, false); + request.setRequestHeader('Accept', 'application/json, text/plain, */*'); + const jwtToken = AuthService.getJwtToken(); + if (jwtToken) { + request.setRequestHeader('X-Authorization', 'Bearer ' + jwtToken); + } + request.send(null); + if (request.status === 200) { + const credentials = JSON.parse(request.responseText) as DeviceCredentials; + responseSubject.next(credentials); + } else { + responseSubject.error(null); + } + return responseSubject.asObservable(); + } else { + return this.http.get(url, defaultHttpOptionsFromConfig(config)); + } + } + + public saveDeviceCredentials(deviceCredentials: DeviceCredentials, config?: RequestConfig): Observable { + return this.http.post('/api/device/credentials', deviceCredentials, defaultHttpOptionsFromConfig(config)); + } + + public makeDevicePublic(deviceId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/customer/public/device/${deviceId}`, null, defaultHttpOptionsFromConfig(config)); + } + + public assignDeviceToCustomer(customerId: string, deviceId: string, + config?: RequestConfig): Observable { + return this.http.post(`/api/customer/${customerId}/device/${deviceId}`, null, defaultHttpOptionsFromConfig(config)); + } + + public unassignDeviceFromCustomer(deviceId: string, config?: RequestConfig) { + return this.http.delete(`/api/customer/device/${deviceId}`, defaultHttpOptionsFromConfig(config)); + } + + public sendOneWayRpcCommand(deviceId: string, requestBody: any, config?: RequestConfig): Observable { + return this.http.post(`/api/plugins/rpc/oneway/${deviceId}`, requestBody, defaultHttpOptionsFromConfig(config)); + } + + public sendTwoWayRpcCommand(deviceId: string, requestBody: any, config?: RequestConfig): Observable { + return this.http.post(`/api/plugins/rpc/twoway/${deviceId}`, requestBody, defaultHttpOptionsFromConfig(config)); + } + + public findByQuery(query: DeviceSearchQuery, + config?: RequestConfig): Observable> { + return this.http.post>('/api/devices', query, defaultHttpOptionsFromConfig(config)); + } + + public findByName(deviceName: string, config?: RequestConfig): Observable { + return this.http.get(`/api/tenant/devices?deviceName=${deviceName}`, defaultHttpOptionsFromConfig(config)); + } + + public claimDevice(deviceName: string, claimRequest: ClaimRequest, + config?: RequestConfig): Observable { + return this.http.post(`/api/customer/device/${deviceName}/claim`, claimRequest, defaultHttpOptionsFromConfig(config)); + } + + public unclaimDevice(deviceName: string, config?: RequestConfig) { + return this.http.delete(`/api/customer/device/${deviceName}/claim`, defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/http/entity-relation.service.ts b/ui-ngx/src/app/core/http/entity-relation.service.ts new file mode 100644 index 0000000000..08f2fc9e98 --- /dev/null +++ b/ui-ngx/src/app/core/http/entity-relation.service.ts @@ -0,0 +1,113 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { Observable } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { EntityRelation, EntityRelationInfo, EntityRelationsQuery } from '@shared/models/relation.models'; +import { EntityId } from '@app/shared/models/id/entity-id'; + +@Injectable({ + providedIn: 'root' +}) +export class EntityRelationService { + + constructor( + private http: HttpClient + ) { } + + public saveRelation(relation: EntityRelation, config?: RequestConfig): Observable { + return this.http.post('/api/relation', relation, defaultHttpOptionsFromConfig(config)); + } + + public deleteRelation(fromId: EntityId, relationType: string, toId: EntityId, + config?: RequestConfig) { + return this.http.delete(`/api/relation?fromId=${fromId.id}&fromType=${fromId.entityType}` + + `&relationType=${relationType}&toId=${toId.id}&toType=${toId.entityType}`, + defaultHttpOptionsFromConfig(config)); + } + + public deleteRelations(entityId: EntityId, + config?: RequestConfig) { + return this.http.delete(`/api/relations?entityId=${entityId.id}&entityType=${entityId.entityType}`, + defaultHttpOptionsFromConfig(config)); + } + + public getRelation(fromId: EntityId, relationType: string, toId: EntityId, + config?: RequestConfig): Observable { + return this.http.get(`/api/relation?fromId=${fromId.id}&fromType=${fromId.entityType}` + + `&relationType=${relationType}&toId=${toId.id}&toType=${toId.entityType}`, + defaultHttpOptionsFromConfig(config)); + } + + public findByFrom(fromId: EntityId, + config?: RequestConfig): Observable> { + return this.http.get>( + `/api/relations?fromId=${fromId.id}&fromType=${fromId.entityType}`, + defaultHttpOptionsFromConfig(config)); + } + + public findInfoByFrom(fromId: EntityId, + config?: RequestConfig): Observable> { + return this.http.get>( + `/api/relations/info?fromId=${fromId.id}&fromType=${fromId.entityType}`, + defaultHttpOptionsFromConfig(config)); + } + + public findByFromAndType(fromId: EntityId, relationType: string, + config?: RequestConfig): Observable> { + return this.http.get>( + `/api/relations?fromId=${fromId.id}&fromType=${fromId.entityType}&relationType=${relationType}`, + defaultHttpOptionsFromConfig(config)); + } + + public findByTo(toId: EntityId, + config?: RequestConfig): Observable> { + return this.http.get>( + `/api/relations?toId=${toId.id}&toType=${toId.entityType}`, + defaultHttpOptionsFromConfig(config)); + } + + public findInfoByTo(toId: EntityId, + config?: RequestConfig): Observable> { + return this.http.get>( + `/api/relations/info?toId=${toId.id}&toType=${toId.entityType}`, + defaultHttpOptionsFromConfig(config)); + } + + public findByToAndType(toId: EntityId, relationType: string, + config?: RequestConfig): Observable> { + return this.http.get>( + `/api/relations?toId=${toId.id}&toType=${toId.entityType}&relationType=${relationType}`, + defaultHttpOptionsFromConfig(config)); + } + + public findByQuery(query: EntityRelationsQuery, + config?: RequestConfig): Observable> { + return this.http.post>( + '/api/relations', query, + defaultHttpOptionsFromConfig(config)); + } + + public findInfoByQuery(query: EntityRelationsQuery, + config?: RequestConfig): Observable> { + return this.http.post>( + '/api/relations/info', query, + defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/http/entity-view.service.ts b/ui-ngx/src/app/core/http/entity-view.service.ts new file mode 100644 index 0000000000..d7fd2b2ab4 --- /dev/null +++ b/ui-ngx/src/app/core/http/entity-view.service.ts @@ -0,0 +1,86 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { Observable } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { PageLink } from '@shared/models/page/page-link'; +import { PageData } from '@shared/models/page/page-data'; +import { EntitySubtype } from '@app/shared/models/entity-type.models'; +import { EntityView, EntityViewInfo, EntityViewSearchQuery } from '@app/shared/models/entity-view.models'; + +@Injectable({ + providedIn: 'root' +}) +export class EntityViewService { + + constructor( + private http: HttpClient + ) { } + + public getTenantEntityViewInfos(pageLink: PageLink, type: string = '', config?: RequestConfig): Observable> { + return this.http.get>(`/api/tenant/entityViewInfos${pageLink.toQuery()}&type=${type}`, + defaultHttpOptionsFromConfig(config)); + } + + public getCustomerEntityViewInfos(customerId: string, pageLink: PageLink, type: string = '', + config?: RequestConfig): Observable> { + return this.http.get>(`/api/customer/${customerId}/entityViewInfos${pageLink.toQuery()}&type=${type}`, + defaultHttpOptionsFromConfig(config)); + } + + public getEntityView(entityViewId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/entityView/${entityViewId}`, defaultHttpOptionsFromConfig(config)); + } + + public getEntityViewInfo(entityViewId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/entityView/info/${entityViewId}`, defaultHttpOptionsFromConfig(config)); + } + + public saveEntityView(entityView: EntityView, config?: RequestConfig): Observable { + return this.http.post('/api/entityView', entityView, defaultHttpOptionsFromConfig(config)); + } + + public deleteEntityView(entityViewId: string, config?: RequestConfig) { + return this.http.delete(`/api/entityView/${entityViewId}`, defaultHttpOptionsFromConfig(config)); + } + + public getEntityViewTypes(config?: RequestConfig): Observable> { + return this.http.get>('/api/entityView/types', defaultHttpOptionsFromConfig(config)); + } + + public makeEntityViewPublic(entityViewId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/customer/public/entityView/${entityViewId}`, null, + defaultHttpOptionsFromConfig(config)); + } + + public assignEntityViewToCustomer(customerId: string, entityViewId: string, + config?: RequestConfig): Observable { + return this.http.post(`/api/customer/${customerId}/entityView/${entityViewId}`, null, + defaultHttpOptionsFromConfig(config)); + } + + public unassignEntityViewFromCustomer(entityViewId: string, config?: RequestConfig) { + return this.http.delete(`/api/customer/entityView/${entityViewId}`, defaultHttpOptionsFromConfig(config)); + } + + public findByQuery(query: EntityViewSearchQuery, + config?: RequestConfig): Observable> { + return this.http.post>('/api/entityViews', query, defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts new file mode 100644 index 0000000000..acd89e3c1b --- /dev/null +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -0,0 +1,1113 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { EMPTY, forkJoin, Observable, of, throwError } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { PageLink } from '@shared/models/page/page-link'; +import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; +import { BaseData } from '@shared/models/base-data'; +import { EntityId } from '@shared/models/id/entity-id'; +import { DeviceService } from '@core/http/device.service'; +import { TenantService } from '@core/http/tenant.service'; +import { CustomerService } from '@core/http/customer.service'; +import { UserService } from './user.service'; +import { DashboardService } from '@core/http/dashboard.service'; +import { Direction } from '@shared/models/page/sort-order'; +import { PageData } from '@shared/models/page/page-data'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Authority } from '@shared/models/authority.enum'; +import { Tenant } from '@shared/models/tenant.model'; +import { catchError, concatMap, expand, map, mergeMap, toArray } from 'rxjs/operators'; +import { Customer } from '@app/shared/models/customer.model'; +import { AssetService } from '@core/http/asset.service'; +import { EntityViewService } from '@core/http/entity-view.service'; +import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; +import { RuleChainService } from '@core/http/rule-chain.service'; +import { AliasInfo, StateParams, SubscriptionInfo } from '@core/api/widget-api.models'; +import { Datasource, DatasourceType, KeyInfo } from '@app/shared/models/widget.models'; +import { UtilsService } from '@core/services/utils.service'; +import { AliasFilterType, EntityAlias, EntityAliasFilter, EntityAliasFilterResult } from '@shared/models/alias.models'; +import { entityFields, EntityInfo, ImportEntitiesResultInfo, ImportEntityData } from '@shared/models/entity.models'; +import { EntityRelationService } from '@core/http/entity-relation.service'; +import { deepClone, isDefined, isDefinedAndNotNull } from '@core/utils'; +import { Asset } from '@shared/models/asset.models'; +import { Device, DeviceCredentialsType } from '@shared/models/device.models'; +import { AttributeService } from '@core/http/attribute.service'; +import { + AlarmData, + AlarmDataQuery, + createDefaultEntityDataPageLink, + defaultEntityDataPageLink, + EntityData, + EntityDataQuery, + entityDataToEntityInfo, + EntityFilter, + entityInfoFields, + EntityKey, + EntityKeyType, + EntityKeyValueType, + FilterPredicateType, + singleEntityDataPageLink, + StringOperation +} from '@shared/models/query/query.models'; +import { alarmFields } from '@shared/models/alarm.models'; + +@Injectable({ + providedIn: 'root' +}) +export class EntityService { + + constructor( + private http: HttpClient, + private store: Store, + private deviceService: DeviceService, + private assetService: AssetService, + private entityViewService: EntityViewService, + private tenantService: TenantService, + private customerService: CustomerService, + private userService: UserService, + private ruleChainService: RuleChainService, + private dashboardService: DashboardService, + private entityRelationService: EntityRelationService, + private attributeService: AttributeService, + private utils: UtilsService + ) { } + + private getEntityObservable(entityType: EntityType, entityId: string, + config?: RequestConfig): Observable> { + + let observable: Observable>; + switch (entityType) { + case EntityType.DEVICE: + observable = this.deviceService.getDevice(entityId, config); + break; + case EntityType.ASSET: + observable = this.assetService.getAsset(entityId, config); + break; + case EntityType.ENTITY_VIEW: + observable = this.entityViewService.getEntityView(entityId, config); + break; + case EntityType.TENANT: + observable = this.tenantService.getTenant(entityId, config); + break; + case EntityType.CUSTOMER: + observable = this.customerService.getCustomer(entityId, config); + break; + case EntityType.DASHBOARD: + observable = this.dashboardService.getDashboardInfo(entityId, config); + break; + case EntityType.USER: + observable = this.userService.getUser(entityId, config); + break; + case EntityType.RULE_CHAIN: + observable = this.ruleChainService.getRuleChain(entityId, config); + break; + case EntityType.ALARM: + console.error('Get Alarm Entity is not implemented!'); + break; + } + return observable; + } + public getEntity(entityType: EntityType, entityId: string, + config?: RequestConfig): Observable> { + const entityObservable = this.getEntityObservable(entityType, entityId, config); + if (entityObservable) { + return entityObservable; + } else { + return throwError(null); + } + } + + private getEntitiesByIdsObservable(fetchEntityFunction: (entityId: string) => Observable>, + entityIds: Array): Observable>> { + const tasks: Observable>[] = []; + entityIds.forEach((entityId) => { + tasks.push(fetchEntityFunction(entityId)); + }); + return forkJoin(tasks).pipe( + map((entities) => { + if (entities) { + entities.sort((entity1, entity2) => { + const index1 = entityIds.indexOf(entity1.id.id); + const index2 = entityIds.indexOf(entity2.id.id); + return index1 - index2; + }); + return entities; + } else { + return []; + } + }) + ); + } + + + private getEntitiesObservable(entityType: EntityType, entityIds: Array, + config?: RequestConfig): Observable>> { + let observable: Observable>>; + switch (entityType) { + case EntityType.DEVICE: + observable = this.deviceService.getDevices(entityIds, config); + break; + case EntityType.ASSET: + observable = this.assetService.getAssets(entityIds, config); + break; + case EntityType.ENTITY_VIEW: + observable = this.getEntitiesByIdsObservable( + (id) => this.entityViewService.getEntityView(id, config), + entityIds); + break; + case EntityType.TENANT: + observable = this.getEntitiesByIdsObservable( + (id) => this.tenantService.getTenant(id, config), + entityIds); + break; + case EntityType.CUSTOMER: + observable = this.getEntitiesByIdsObservable( + (id) => this.customerService.getCustomer(id, config), + entityIds); + break; + case EntityType.DASHBOARD: + observable = this.getEntitiesByIdsObservable( + (id) => this.dashboardService.getDashboardInfo(id, config), + entityIds); + break; + case EntityType.USER: + observable = this.getEntitiesByIdsObservable( + (id) => this.userService.getUser(id, config), + entityIds); + break; + case EntityType.ALARM: + console.error('Get Alarm Entity is not implemented!'); + break; + } + return observable; + } + + public getEntities(entityType: EntityType, entityIds: Array, + config?: RequestConfig): Observable>> { + const entitiesObservable = this.getEntitiesObservable(entityType, entityIds, config); + if (entitiesObservable) { + return entitiesObservable; + } else { + return throwError(null); + } + } + + private getSingleTenantByPageLinkObservable(pageLink: PageLink, + config?: RequestConfig): Observable> { + const authUser = getCurrentAuthUser(this.store); + const tenantId = authUser.tenantId; + return this.tenantService.getTenant(tenantId, config).pipe( + map((tenant) => { + const result = { + data: [], + totalPages: 0, + totalElements: 0, + hasNext: false + } as PageData; + if (tenant.title.toLowerCase().startsWith(pageLink.textSearch.toLowerCase())) { + result.data.push(tenant); + result.totalPages = 1; + result.totalElements = 1; + } + return result; + }) + ); + } + + private getSingleCustomerByPageLinkObservable(pageLink: PageLink, + config?: RequestConfig): Observable> { + const authUser = getCurrentAuthUser(this.store); + const customerId = authUser.customerId; + return this.customerService.getCustomer(customerId, config).pipe( + map((customer) => { + const result = { + data: [], + totalPages: 0, + totalElements: 0, + hasNext: false + } as PageData; + if (customer.title.toLowerCase().startsWith(pageLink.textSearch.toLowerCase())) { + result.data.push(customer); + result.totalPages = 1; + result.totalElements = 1; + } + return result; + }) + ); + } + + private getEntitiesByPageLinkObservable(entityType: EntityType, pageLink: PageLink, subType: string = '', + config?: RequestConfig): Observable>> { + let entitiesObservable: Observable>>; + const authUser = getCurrentAuthUser(this.store); + const customerId = authUser.customerId; + switch (entityType) { + case EntityType.DEVICE: + pageLink.sortOrder.property = 'name'; + if (authUser.authority === Authority.CUSTOMER_USER) { + entitiesObservable = this.deviceService.getCustomerDeviceInfos(customerId, pageLink, subType, config); + } else { + entitiesObservable = this.deviceService.getTenantDeviceInfos(pageLink, subType, config); + } + break; + case EntityType.ASSET: + pageLink.sortOrder.property = 'name'; + if (authUser.authority === Authority.CUSTOMER_USER) { + entitiesObservable = this.assetService.getCustomerAssetInfos(customerId, pageLink, subType, config); + } else { + entitiesObservable = this.assetService.getTenantAssetInfos(pageLink, subType, config); + } + break; + case EntityType.ENTITY_VIEW: + pageLink.sortOrder.property = 'name'; + if (authUser.authority === Authority.CUSTOMER_USER) { + entitiesObservable = this.entityViewService.getCustomerEntityViewInfos(customerId, pageLink, + subType, config); + } else { + entitiesObservable = this.entityViewService.getTenantEntityViewInfos(pageLink, subType, config); + } + break; + case EntityType.TENANT: + pageLink.sortOrder.property = 'title'; + if (authUser.authority === Authority.TENANT_ADMIN) { + entitiesObservable = this.getSingleTenantByPageLinkObservable(pageLink, config); + } else { + entitiesObservable = this.tenantService.getTenants(pageLink, config); + } + break; + case EntityType.CUSTOMER: + pageLink.sortOrder.property = 'title'; + if (authUser.authority === Authority.CUSTOMER_USER) { + entitiesObservable = this.getSingleCustomerByPageLinkObservable(pageLink, config); + } else { + entitiesObservable = this.customerService.getCustomers(pageLink, config); + } + break; + case EntityType.RULE_CHAIN: + pageLink.sortOrder.property = 'name'; + entitiesObservable = this.ruleChainService.getRuleChains(pageLink, config); + break; + case EntityType.DASHBOARD: + pageLink.sortOrder.property = 'title'; + if (authUser.authority === Authority.CUSTOMER_USER) { + entitiesObservable = this.dashboardService.getCustomerDashboards(customerId, pageLink, config); + } else { + entitiesObservable = this.dashboardService.getTenantDashboards(pageLink, config); + } + break; + case EntityType.USER: + pageLink.sortOrder.property = 'email'; + entitiesObservable = this.userService.getUsers(pageLink); + break; + case EntityType.ALARM: + console.error('Get Alarm Entities is not implemented!'); + break; + } + return entitiesObservable; + } + + private getEntitiesByPageLink(entityType: EntityType, pageLink: PageLink, subType: string = '', + config?: RequestConfig): Observable>> { + const entitiesObservable: Observable>> = + this.getEntitiesByPageLinkObservable(entityType, pageLink, subType, config); + if (entitiesObservable) { + return entitiesObservable.pipe( + expand((data) => { + if (data.hasNext) { + pageLink.page += 1; + return this.getEntitiesByPageLinkObservable(entityType, pageLink, subType, config); + } else { + return EMPTY; + } + }), + map((data) => data.data), + concatMap((data) => data), + toArray() + ); + } else { + return of(null); + } + } + + public getEntitiesByNameFilter(entityType: EntityType, entityNameFilter: string, + pageSize: number, subType: string = '', + config?: RequestConfig): Observable>> { + const pageLink = new PageLink(pageSize, 0, entityNameFilter, { + property: 'name', + direction: Direction.ASC + }); + if (pageSize === -1) { // all + pageLink.pageSize = 100; + return this.getEntitiesByPageLink(entityType, pageLink, subType, config).pipe( + map((data) => data && data.length ? data : null) + ); + } else { + const entitiesObservable: Observable>> = + this.getEntitiesByPageLinkObservable(entityType, pageLink, subType, config); + if (entitiesObservable) { + return entitiesObservable.pipe( + map((data) => data && data.data.length ? data.data : null) + ); + } else { + return of(null); + } + } + } + + public findEntityDataByQuery(query: EntityDataQuery, config?: RequestConfig): Observable> { + return this.http.post>('/api/entitiesQuery/find', query, defaultHttpOptionsFromConfig(config)); + } + + public findAlarmDataByQuery(query: AlarmDataQuery, config?: RequestConfig): Observable> { + return this.http.post>('/api/alarmsQuery/find', query, defaultHttpOptionsFromConfig(config)); + } + + public findEntityInfosByFilterAndName(filter: EntityFilter, + searchText: string, config?: RequestConfig): Observable> { + const nameField: EntityKey = { + type: EntityKeyType.ENTITY_FIELD, + key: 'name' + }; + const query: EntityDataQuery = { + entityFilter: filter, + pageLink: { + pageSize: 10, + page: 0, + sortOrder: { + key: nameField, + direction: Direction.ASC + } + }, + entityFields: entityInfoFields, + keyFilters: searchText && searchText.length ? [ + { + key: nameField, + valueType: EntityKeyValueType.STRING, + predicate: { + type: FilterPredicateType.STRING, + operation: StringOperation.STARTS_WITH, + ignoreCase: true, + value: { + defaultValue: searchText + } + } + } + ] : null + }; + return this.findEntityDataByQuery(query, config).pipe( + map((data) => { + const entityInfos = data.data.map(entityData => entityDataToEntityInfo(entityData)); + return { + data: entityInfos, + hasNext: data.hasNext, + totalElements: data.totalElements, + totalPages: data.totalPages + }; + }) + ); + } + + public findSingleEntityInfoByEntityFilter(filter: EntityFilter, config?: RequestConfig): Observable { + const query: EntityDataQuery = { + entityFilter: filter, + pageLink: createDefaultEntityDataPageLink(1), + entityFields: entityInfoFields + }; + return this.findEntityDataByQuery(query, config).pipe( + map((data) => { + if (data.data.length) { + const entityData = data.data[0]; + return entityDataToEntityInfo(entityData); + } else { + return null; + } + }) + ); + } + + public getAliasFilterTypesByEntityTypes(entityTypes: Array): Array { + const allAliasFilterTypes: Array = Object.keys(AliasFilterType).map((key) => AliasFilterType[key]); + if (!entityTypes || !entityTypes.length) { + return allAliasFilterTypes; + } + const result = []; + for (const aliasFilterType of allAliasFilterTypes) { + if (this.filterAliasFilterTypeByEntityTypes(aliasFilterType, entityTypes)) { + result.push(aliasFilterType); + } + } + return result; + } + + public filterAliasByEntityTypes(entityAlias: EntityAlias, entityTypes: Array): boolean { + const filter = entityAlias.filter; + if (this.filterAliasFilterTypeByEntityTypes(filter.type, entityTypes)) { + switch (filter.type) { + case AliasFilterType.singleEntity: + return entityTypes.indexOf(filter.singleEntity.entityType) > -1 ? true : false; + case AliasFilterType.entityList: + return entityTypes.indexOf(filter.entityType) > -1 ? true : false; + case AliasFilterType.entityName: + return entityTypes.indexOf(filter.entityType) > -1 ? true : false; + case AliasFilterType.stateEntity: + return true; + case AliasFilterType.assetType: + return entityTypes.indexOf(EntityType.ASSET) > -1 ? true : false; + case AliasFilterType.deviceType: + return entityTypes.indexOf(EntityType.DEVICE) > -1 ? true : false; + case AliasFilterType.entityViewType: + return entityTypes.indexOf(EntityType.ENTITY_VIEW) > -1 ? true : false; + case AliasFilterType.relationsQuery: + if (filter.filters && filter.filters.length) { + let match = false; + for (const relationFilter of filter.filters) { + if (relationFilter.entityTypes && relationFilter.entityTypes.length) { + for (const relationFilterEntityType of relationFilter.entityTypes) { + if (entityTypes.indexOf(relationFilterEntityType) > -1) { + match = true; + break; + } + } + } else { + match = true; + break; + } + } + return match; + } else { + return true; + } + case AliasFilterType.assetSearchQuery: + return entityTypes.indexOf(EntityType.ASSET) > -1 ? true : false; + case AliasFilterType.deviceSearchQuery: + return entityTypes.indexOf(EntityType.DEVICE) > -1 ? true : false; + case AliasFilterType.entityViewSearchQuery: + return entityTypes.indexOf(EntityType.ENTITY_VIEW) > -1 ? true : false; + } + } + return false; + } + + private filterAliasFilterTypeByEntityTypes(aliasFilterType: AliasFilterType, + entityTypes: Array): boolean { + if (!entityTypes || !entityTypes.length) { + return true; + } + let valid = false; + entityTypes.forEach((entityType) => { + valid = valid || this.filterAliasFilterTypeByEntityType(aliasFilterType, entityType); + }); + return valid; + } + + private filterAliasFilterTypeByEntityType(aliasFilterType: AliasFilterType, entityType: EntityType | AliasEntityType): boolean { + switch (aliasFilterType) { + case AliasFilterType.singleEntity: + return true; + case AliasFilterType.entityList: + return true; + case AliasFilterType.entityName: + return true; + case AliasFilterType.stateEntity: + return true; + case AliasFilterType.assetType: + return entityType === EntityType.ASSET; + case AliasFilterType.deviceType: + return entityType === EntityType.DEVICE; + case AliasFilterType.entityViewType: + return entityType === EntityType.ENTITY_VIEW; + case AliasFilterType.relationsQuery: + return true; + case AliasFilterType.assetSearchQuery: + return entityType === EntityType.ASSET; + case AliasFilterType.deviceSearchQuery: + return entityType === EntityType.DEVICE; + case AliasFilterType.entityViewSearchQuery: + return entityType === EntityType.ENTITY_VIEW; + } + return false; + } + + public prepareAllowedEntityTypesList(allowedEntityTypes: Array, + useAliasEntityTypes?: boolean): Array { + const authUser = getCurrentAuthUser(this.store); + const entityTypes: Array = []; + switch (authUser.authority) { + case Authority.SYS_ADMIN: + entityTypes.push(EntityType.TENANT); + break; + case Authority.TENANT_ADMIN: + entityTypes.push(EntityType.DEVICE); + entityTypes.push(EntityType.ASSET); + entityTypes.push(EntityType.ENTITY_VIEW); + entityTypes.push(EntityType.TENANT); + entityTypes.push(EntityType.CUSTOMER); + entityTypes.push(EntityType.USER); + entityTypes.push(EntityType.DASHBOARD); + if (useAliasEntityTypes) { + entityTypes.push(AliasEntityType.CURRENT_CUSTOMER); + entityTypes.push(AliasEntityType.CURRENT_TENANT); + } + break; + case Authority.CUSTOMER_USER: + entityTypes.push(EntityType.DEVICE); + entityTypes.push(EntityType.ASSET); + entityTypes.push(EntityType.ENTITY_VIEW); + entityTypes.push(EntityType.CUSTOMER); + entityTypes.push(EntityType.USER); + entityTypes.push(EntityType.DASHBOARD); + if (useAliasEntityTypes) { + entityTypes.push(AliasEntityType.CURRENT_CUSTOMER); + } + break; + } + if (useAliasEntityTypes) { + entityTypes.push(AliasEntityType.CURRENT_USER); + if (authUser.authority !== Authority.SYS_ADMIN) { + entityTypes.push(AliasEntityType.CURRENT_USER_OWNER); + } + } + if (allowedEntityTypes && allowedEntityTypes.length) { + for (let index = entityTypes.length - 1; index >= 0; index--) { + if (allowedEntityTypes.indexOf(entityTypes[index]) === -1) { + entityTypes.splice(index, 1); + } + } + } + return entityTypes; + } + + private getEntityFieldKeys(entityType: EntityType, searchText: string): Array { + const entityFieldKeys: string[] = [entityFields.createdTime.keyName]; + const query = searchText.toLowerCase(); + switch (entityType) { + case EntityType.USER: + entityFieldKeys.push(entityFields.name.keyName); + entityFieldKeys.push(entityFields.email.keyName); + entityFieldKeys.push(entityFields.firstName.keyName); + entityFieldKeys.push(entityFields.lastName.keyName); + break; + case EntityType.TENANT: + case EntityType.CUSTOMER: + entityFieldKeys.push(entityFields.title.keyName); + entityFieldKeys.push(entityFields.email.keyName); + entityFieldKeys.push(entityFields.country.keyName); + entityFieldKeys.push(entityFields.state.keyName); + entityFieldKeys.push(entityFields.city.keyName); + entityFieldKeys.push(entityFields.address.keyName); + entityFieldKeys.push(entityFields.address2.keyName); + entityFieldKeys.push(entityFields.zip.keyName); + entityFieldKeys.push(entityFields.phone.keyName); + break; + case EntityType.ENTITY_VIEW: + entityFieldKeys.push(entityFields.name.keyName); + entityFieldKeys.push(entityFields.type.keyName); + break; + case EntityType.DEVICE: + case EntityType.ASSET: + entityFieldKeys.push(entityFields.name.keyName); + entityFieldKeys.push(entityFields.type.keyName); + entityFieldKeys.push(entityFields.label.keyName); + break; + case EntityType.DASHBOARD: + entityFieldKeys.push(entityFields.title.keyName); + break; + } + return query ? entityFieldKeys.filter((entityField) => entityField.toLowerCase().indexOf(query) === 0) : entityFieldKeys; + } + + private getAlarmKeys(searchText: string): Array { + const alarmKeys: string[] = Object.keys(alarmFields); + const query = searchText.toLowerCase(); + return query ? alarmKeys.filter((alarmField) => alarmField.toLowerCase().indexOf(query) === 0) : alarmKeys; + } + + public getEntityKeys(entityId: EntityId, query: string, type: DataKeyType, + config?: RequestConfig): Observable> { + if (type === DataKeyType.entityField) { + return of(this.getEntityFieldKeys(entityId.entityType as EntityType, query)); + } else if (type === DataKeyType.alarm) { + return of(this.getAlarmKeys(query)); + } + let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/keys/`; + if (type === DataKeyType.timeseries) { + url += 'timeseries'; + } else if (type === DataKeyType.attribute) { + url += 'attributes'; + } + return this.http.get>(url, + defaultHttpOptionsFromConfig(config)) + .pipe( + map( + (dataKeys) => { + if (query) { + const lowercaseQuery = query.toLowerCase(); + return dataKeys.filter((dataKey) => dataKey.toLowerCase().indexOf(lowercaseQuery) === 0); + } else { + return dataKeys; + } + } + ) + ); + } + + public createDatasourcesFromSubscriptionsInfo(subscriptionsInfo: Array): Array { + const datasources = subscriptionsInfo.map(subscriptionInfo => this.createDatasourceFromSubscriptionInfo(subscriptionInfo)); + this.utils.generateColors(datasources); + return datasources; + } + + public createAlarmSourceFromSubscriptionInfo(subscriptionInfo: SubscriptionInfo): Datasource { + if (subscriptionInfo.entityId && subscriptionInfo.entityType) { + const alarmSource = this.createDatasourceFromSubscriptionInfo(subscriptionInfo); + this.utils.generateColors([alarmSource]); + return alarmSource; + } else { + throw new Error('Can\'t crate alarm source without entityId information!'); + } + } + + public resolveAlias(entityAlias: EntityAlias, stateParams: StateParams): Observable { + const filter = entityAlias.filter; + return this.resolveAliasFilter(filter, stateParams).pipe( + mergeMap((result) => { + const aliasInfo: AliasInfo = { + alias: entityAlias.alias, + entityFilter: result.entityFilter, + stateEntity: result.stateEntity, + entityParamName: result.entityParamName, + resolveMultiple: filter.resolveMultiple + }; + aliasInfo.currentEntity = null; + if (!aliasInfo.resolveMultiple && aliasInfo.entityFilter) { + return this.findSingleEntityInfoByEntityFilter(aliasInfo.entityFilter, + {ignoreLoading: true, ignoreErrors: true}).pipe( + map((entity) => { + aliasInfo.currentEntity = entity; + return aliasInfo; + }) + ); + } + return of(aliasInfo); + }) + ); + } + + public resolveAliasFilter(filter: EntityAliasFilter, stateParams: StateParams): Observable { + const result: EntityAliasFilterResult = { + entityFilter: null, + stateEntity: false + }; + if (filter.stateEntityParamName && filter.stateEntityParamName.length) { + result.entityParamName = filter.stateEntityParamName; + } + const stateEntityInfo = this.getStateEntityInfo(filter, stateParams); + const stateEntityId = stateEntityInfo.entityId; + switch (filter.type) { + case AliasFilterType.singleEntity: + const aliasEntityId = this.resolveAliasEntityId(filter.singleEntity.entityType, filter.singleEntity.id); + result.entityFilter = { + type: AliasFilterType.singleEntity, + singleEntity: aliasEntityId + }; + return of(result); + case AliasFilterType.entityList: + result.entityFilter = deepClone(filter); + return of(result); + case AliasFilterType.entityName: + result.entityFilter = deepClone(filter); + return of(result); + case AliasFilterType.stateEntity: + result.stateEntity = true; + if (stateEntityId) { + result.entityFilter = { + type: AliasFilterType.singleEntity, + singleEntity: stateEntityId + }; + } + return of(result); + case AliasFilterType.assetType: + result.entityFilter = deepClone(filter); + return of(result); + case AliasFilterType.deviceType: + result.entityFilter = deepClone(filter); + return of(result); + case AliasFilterType.entityViewType: + result.entityFilter = deepClone(filter); + return of(result); + case AliasFilterType.apiUsageState: + result.entityFilter = deepClone(filter); + return of(result); + case AliasFilterType.relationsQuery: + result.stateEntity = filter.rootStateEntity; + let rootEntityType; + let rootEntityId; + if (result.stateEntity && stateEntityId) { + rootEntityType = stateEntityId.entityType; + rootEntityId = stateEntityId.id; + } else if (!result.stateEntity) { + rootEntityType = filter.rootEntity.entityType; + rootEntityId = filter.rootEntity.id; + } + if (rootEntityType && rootEntityId) { + const relationQueryRootEntityId = this.resolveAliasEntityId(rootEntityType, rootEntityId); + result.entityFilter = deepClone(filter); + result.entityFilter.rootEntity = relationQueryRootEntityId; + return of(result); + } else { + return of(result); + } + case AliasFilterType.assetSearchQuery: + case AliasFilterType.deviceSearchQuery: + case AliasFilterType.entityViewSearchQuery: + result.stateEntity = filter.rootStateEntity; + if (result.stateEntity && stateEntityId) { + rootEntityType = stateEntityId.entityType; + rootEntityId = stateEntityId.id; + } else if (!result.stateEntity) { + rootEntityType = filter.rootEntity.entityType; + rootEntityId = filter.rootEntity.id; + } + if (rootEntityType && rootEntityId) { + const searchQueryRootEntityId = this.resolveAliasEntityId(rootEntityType, rootEntityId); + result.entityFilter = deepClone(filter); + result.entityFilter.rootEntity = searchQueryRootEntityId; + return of(result); + } else { + return of(result); + } + } + } + + public checkEntityAlias(entityAlias: EntityAlias): Observable { + return this.resolveAliasFilter(entityAlias.filter, null).pipe( + map((result) => { + if (result.stateEntity) { + return true; + } else { + return isDefinedAndNotNull(result.entityFilter); + } + }), + catchError(err => of(false)) + ); + } + + public saveEntityParameters(entityType: EntityType, entityData: ImportEntityData, update: boolean, + config?: RequestConfig): Observable { + let saveEntityObservable: Observable>; + switch (entityType) { + case EntityType.DEVICE: + const device: Device = { + name: entityData.name, + type: entityData.type, + label: entityData.label, + additionalInfo: { + description: entityData.description + } + }; + if (entityData.gateway !== null) { + device.additionalInfo = { + ...device.additionalInfo, + gateway: entityData.gateway + }; + } + saveEntityObservable = this.deviceService.saveDevice(device, config); + break; + case EntityType.ASSET: + const asset: Asset = { + name: entityData.name, + type: entityData.type, + label: entityData.label, + additionalInfo: { + description: entityData.description + } + }; + saveEntityObservable = this.assetService.saveAsset(asset, config); + break; + } + return saveEntityObservable.pipe( + mergeMap((entity) => { + return this.saveEntityData(entity.id, entityData, config).pipe( + map(() => { + return { create: { entity: 1 } } as ImportEntitiesResultInfo; + }), + catchError(err => of({ error: { entity: 1 } } as ImportEntitiesResultInfo)) + ); + }), + catchError(err => { + if (update) { + let findEntityObservable: Observable>; + switch (entityType) { + case EntityType.DEVICE: + findEntityObservable = this.deviceService.findByName(entityData.name, config); + break; + case EntityType.ASSET: + findEntityObservable = this.assetService.findByName(entityData.name, config); + break; + } + return findEntityObservable.pipe( + mergeMap((entity) => { + const tasks: Observable[] = []; + const result: Device | Asset = entity as (Device | Asset); + const additionalInfo = result.additionalInfo || {}; + if (result.label !== entityData.label || + result.type !== entityData.type || + additionalInfo.description !== entityData.description || + (result.id.entityType === EntityType.DEVICE && (additionalInfo.gateway !== entityData.gateway)) ) { + result.label = entityData.label; + result.type = entityData.type; + result.additionalInfo = additionalInfo; + result.additionalInfo.description = entityData.description; + if (result.id.entityType === EntityType.DEVICE) { + result.additionalInfo.gateway = entityData.gateway; + } + switch (result.id.entityType) { + case EntityType.DEVICE: + tasks.push(this.deviceService.saveDevice(result, config)); + break; + case EntityType.ASSET: + tasks.push(this.assetService.saveAsset(result, config)); + break; + } + } + tasks.push(this.saveEntityData(entity.id, entityData, config)); + return forkJoin(tasks).pipe( + map(() => { + return { update: { entity: 1 } } as ImportEntitiesResultInfo; + }), + catchError(updateError => of({ error: { entity: 1 } } as ImportEntitiesResultInfo)) + ); + }), + catchError(findErr => of({ error: { entity: 1 } } as ImportEntitiesResultInfo)) + ); + } else { + return of({ error: { entity: 1 } } as ImportEntitiesResultInfo); + } + }) + ); + } + + public saveEntityData(entityId: EntityId, entityData: ImportEntityData, config?: RequestConfig): Observable { + const observables: Observable[] = []; + let observable: Observable; + if (entityData.accessToken && entityData.accessToken !== '') { + observable = this.deviceService.getDeviceCredentials(entityId.id, false, config).pipe( + mergeMap((credentials) => { + credentials.credentialsId = entityData.accessToken; + credentials.credentialsType = DeviceCredentialsType.ACCESS_TOKEN; + credentials.credentialsValue = null; + return this.deviceService.saveDeviceCredentials(credentials, config).pipe( + map(() => 'ok'), + catchError(err => of('error')) + ); + }) + ); + observables.push(observable); + } + if (entityData.attributes.shared && entityData.attributes.shared.length) { + observable = this.attributeService.saveEntityAttributes(entityId, AttributeScope.SHARED_SCOPE, + entityData.attributes.shared, config).pipe( + map(() => 'ok'), + catchError(err => of('error')) + ); + observables.push(observable); + } + if (entityData.attributes.server && entityData.attributes.server.length) { + observable = this.attributeService.saveEntityAttributes(entityId, AttributeScope.SERVER_SCOPE, + entityData.attributes.server, config).pipe( + map(() => 'ok'), + catchError(err => of('error')) + ); + observables.push(observable); + } + if (entityData.timeseries && entityData.timeseries.length) { + observable = this.attributeService.saveEntityTimeseries(entityId, 'time', entityData.timeseries, config).pipe( + map(() => 'ok'), + catchError(err => of('error')) + ); + observables.push(observable); + } + if (observables.length) { + return forkJoin(observables).pipe( + map((response) => { + const hasError = response.filter((status) => status === 'error').length > 0; + if (hasError) { + throw Error(); + } else { + return response; + } + }) + ); + } else { + return of(null); + } + } + + private getStateEntityInfo(filter: EntityAliasFilter, stateParams: StateParams): {entityId: EntityId} { + let entityId: EntityId = null; + if (stateParams) { + if (filter.stateEntityParamName && filter.stateEntityParamName.length) { + if (stateParams[filter.stateEntityParamName]) { + entityId = stateParams[filter.stateEntityParamName].entityId; + } + } else { + entityId = stateParams.entityId; + } + } + if (!entityId) { + entityId = filter.defaultStateEntity; + } + if (entityId) { + entityId = this.resolveAliasEntityId(entityId.entityType, entityId.id); + } + return {entityId}; + } + + private resolveAliasEntityId(entityType: EntityType | AliasEntityType, id: string): EntityId { + const entityId: EntityId = { + entityType, + id + }; + if (entityType === AliasEntityType.CURRENT_CUSTOMER) { + const authUser = getCurrentAuthUser(this.store); + entityId.entityType = EntityType.CUSTOMER; + if (authUser.authority === Authority.CUSTOMER_USER) { + entityId.id = authUser.customerId; + } + } else if (entityType === AliasEntityType.CURRENT_TENANT){ + const authUser = getCurrentAuthUser(this.store); + entityId.entityType = EntityType.TENANT; + entityId.id = authUser.tenantId; + } else if (entityType === AliasEntityType.CURRENT_USER){ + const authUser = getCurrentAuthUser(this.store); + entityId.entityType = EntityType.USER; + entityId.id = authUser.userId; + } else if (entityType === AliasEntityType.CURRENT_USER_OWNER){ + const authUser = getCurrentAuthUser(this.store); + if (authUser.authority === Authority.TENANT_ADMIN) { + entityId.entityType = EntityType.TENANT; + entityId.id = authUser.tenantId; + } else if (authUser.authority === Authority.CUSTOMER_USER) { + entityId.entityType = EntityType.CUSTOMER; + entityId.id = authUser.customerId; + } + } + return entityId; + } + + private createDatasourceFromSubscriptionInfo(subscriptionInfo: SubscriptionInfo): Datasource { + subscriptionInfo = this.validateSubscriptionInfo(subscriptionInfo); + let datasource: Datasource = null; + if (subscriptionInfo.type === DatasourceType.entity) { + datasource = { + type: subscriptionInfo.type, + entityName: subscriptionInfo.entityName, + name: subscriptionInfo.entityName, + entityType: subscriptionInfo.entityType, + entityId: subscriptionInfo.entityId, + dataKeys: [] + }; + this.prepareEntityFilterFromSubscriptionInfo(datasource, subscriptionInfo); + } else if (subscriptionInfo.type === DatasourceType.function) { + datasource = { + type: subscriptionInfo.type, + name: subscriptionInfo.name || DatasourceType.function, + dataKeys: [] + }; + } + if (datasource !== null) { + if (subscriptionInfo.timeseries) { + this.createDatasourceKeys(subscriptionInfo.timeseries, DataKeyType.timeseries, datasource); + } + if (subscriptionInfo.attributes) { + this.createDatasourceKeys(subscriptionInfo.attributes, DataKeyType.attribute, datasource); + } + if (subscriptionInfo.functions) { + this.createDatasourceKeys(subscriptionInfo.functions, DataKeyType.function, datasource); + } + if (subscriptionInfo.alarmFields) { + this.createDatasourceKeys(subscriptionInfo.alarmFields, DataKeyType.alarm, datasource); + } + } + return datasource; + } + + private validateSubscriptionInfo(subscriptionInfo: SubscriptionInfo): SubscriptionInfo { + // @ts-ignore + if (subscriptionInfo.type === 'device') { + subscriptionInfo.type = DatasourceType.entity; + subscriptionInfo.entityType = EntityType.DEVICE; + if (subscriptionInfo.deviceId) { + subscriptionInfo.entityId = subscriptionInfo.deviceId; + } else if (subscriptionInfo.deviceName) { + subscriptionInfo.entityName = subscriptionInfo.deviceName; + } else if (subscriptionInfo.deviceNamePrefix) { + subscriptionInfo.entityNamePrefix = subscriptionInfo.deviceNamePrefix; + } else if (subscriptionInfo.deviceIds) { + subscriptionInfo.entityIds = subscriptionInfo.deviceIds; + } + } + return subscriptionInfo; + } + + private prepareEntityFilterFromSubscriptionInfo(datasource: Datasource, subscriptionInfo: SubscriptionInfo) { + if (subscriptionInfo.entityId) { + datasource.entityFilter = { + type: AliasFilterType.singleEntity, + singleEntity: { + entityType: subscriptionInfo.entityType, + id: subscriptionInfo.entityId + } + }; + datasource.pageLink = singleEntityDataPageLink; + } else if (subscriptionInfo.entityName || subscriptionInfo.entityNamePrefix) { + let nameFilter; + let pageLink; + if (isDefined(subscriptionInfo.entityName) && subscriptionInfo.entityName.length) { + nameFilter = subscriptionInfo.entityName; + pageLink = deepClone(singleEntityDataPageLink); + } else { + nameFilter = subscriptionInfo.entityNamePrefix; + pageLink = deepClone(defaultEntityDataPageLink); + } + datasource.entityFilter = { + type: AliasFilterType.entityName, + entityType: subscriptionInfo.entityType, + entityNameFilter: nameFilter + }; + datasource.pageLink = pageLink; + } else if (subscriptionInfo.entityIds) { + datasource.entityFilter = { + type: AliasFilterType.entityList, + entityType: subscriptionInfo.entityType, + entityList: subscriptionInfo.entityIds + }; + datasource.pageLink = deepClone(defaultEntityDataPageLink); + } + } + + private createDatasourceKeys(keyInfos: Array, type: DataKeyType, datasource: Datasource) { + keyInfos.forEach((keyInfo) => { + const dataKey = this.utils.createKey(keyInfo, type); + datasource.dataKeys.push(dataKey); + }); + } +} diff --git a/ui-ngx/src/app/core/http/event.service.ts b/ui-ngx/src/app/core/http/event.service.ts new file mode 100644 index 0000000000..5c66a10c03 --- /dev/null +++ b/ui-ngx/src/app/core/http/event.service.ts @@ -0,0 +1,41 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { Observable } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { TimePageLink } from '@shared/models/page/page-link'; +import { PageData } from '@shared/models/page/page-data'; +import { EntityId } from '@shared/models/id/entity-id'; +import { DebugEventType, Event, EventType } from '@shared/models/event.models'; + +@Injectable({ + providedIn: 'root' +}) +export class EventService { + + constructor( + private http: HttpClient + ) { } + + public getEvents(entityId: EntityId, eventType: EventType | DebugEventType, tenantId: string, pageLink: TimePageLink, + config?: RequestConfig): Observable> { + return this.http.get>(`/api/events/${entityId.entityType}/${entityId.id}/${eventType}` + + `${pageLink.toQuery()}&tenantId=${tenantId}`, + defaultHttpOptionsFromConfig(config)); + } +} diff --git a/ui-ngx/src/app/core/http/http-utils.ts b/ui-ngx/src/app/core/http/http-utils.ts new file mode 100644 index 0000000000..187767b6b1 --- /dev/null +++ b/ui-ngx/src/app/core/http/http-utils.ts @@ -0,0 +1,41 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { InterceptorHttpParams } from '../interceptors/interceptor-http-params'; +import { HttpHeaders } from '@angular/common/http'; +import { InterceptorConfig } from '../interceptors/interceptor-config'; + +export interface RequestConfig { + ignoreLoading?: boolean; + ignoreErrors?: boolean; + resendRequest?: boolean; +} + +export function defaultHttpOptionsFromConfig(config?: RequestConfig) { + if (!config) { + config = {}; + } + return defaultHttpOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest); +} + +export function defaultHttpOptions(ignoreLoading: boolean = false, + ignoreErrors: boolean = false, + resendRequest: boolean = false) { + return { + headers: new HttpHeaders({'Content-Type': 'application/json'}), + params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest)) + }; +} diff --git a/ui-ngx/src/app/core/http/oauth2.service.ts b/ui-ngx/src/app/core/http/oauth2.service.ts new file mode 100644 index 0000000000..2eff9fa339 --- /dev/null +++ b/ui-ngx/src/app/core/http/oauth2.service.ts @@ -0,0 +1,48 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; +import { Observable } from 'rxjs'; +import { OAuth2ClientRegistrationTemplate, OAuth2ClientsParams } from '@shared/models/oauth2.models'; + +@Injectable({ + providedIn: 'root' +}) +export class OAuth2Service { + + constructor( + private http: HttpClient + ) { } + + public getOAuth2Settings(config?: RequestConfig): Observable { + return this.http.get(`/api/oauth2/config`, defaultHttpOptionsFromConfig(config)); + } + + public getOAuth2Template(config?: RequestConfig): Observable> { + return this.http.get>(`/api/oauth2/config/template`, defaultHttpOptionsFromConfig(config)); + } + + public saveOAuth2Settings(OAuth2Setting: OAuth2ClientsParams, config?: RequestConfig): Observable { + return this.http.post('/api/oauth2/config', OAuth2Setting, + defaultHttpOptionsFromConfig(config)); + } + + public getLoginProcessingUrl(config?: RequestConfig): Observable { + return this.http.get(`/api/oauth2/loginProcessingUrl`, defaultHttpOptionsFromConfig(config)); + } +} diff --git a/ui-ngx/src/app/core/http/public-api.ts b/ui-ngx/src/app/core/http/public-api.ts new file mode 100644 index 0000000000..10c6b5429e --- /dev/null +++ b/ui-ngx/src/app/core/http/public-api.ts @@ -0,0 +1,36 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export * from './admin.service'; +export * from './alarm.service'; +export * from './asset.service'; +export * from './attribute.service'; +export * from './audit-log.service'; +export * from './component-descriptor.service'; +export * from './customer.service'; +export * from './dashboard.service'; +export * from './device.service'; +export * from './entity.service'; +export * from './entity-relation.service'; +export * from './entity-view.service'; +export * from './event.service'; +export * from './http-utils'; +export * from './oauth2.service'; +export * from './queue.service'; +export * from './rule-chain.service'; +export * from './tenant.service'; +export * from './user.service'; +export * from './widget.service'; diff --git a/ui-ngx/src/app/core/http/queue.service.ts b/ui-ngx/src/app/core/http/queue.service.ts new file mode 100644 index 0000000000..42beadb81a --- /dev/null +++ b/ui-ngx/src/app/core/http/queue.service.ts @@ -0,0 +1,36 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; +import { Observable } from 'rxjs'; +import { ServiceType } from '@shared/models/queue.models'; + +@Injectable({ + providedIn: 'root' +}) +export class QueueService { + + constructor( + private http: HttpClient + ) { } + + public getTenantQueuesByServiceType(serviceType: ServiceType, config?: RequestConfig): Observable> { + return this.http.get>(`/api/tenant/queues?serviceType=${serviceType}`, + defaultHttpOptionsFromConfig(config)); + } +} diff --git a/ui-ngx/src/app/core/http/rule-chain.service.ts b/ui-ngx/src/app/core/http/rule-chain.service.ts new file mode 100644 index 0000000000..e7ab664ae9 --- /dev/null +++ b/ui-ngx/src/app/core/http/rule-chain.service.ts @@ -0,0 +1,295 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ComponentFactory, Injectable } from '@angular/core'; +import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { forkJoin, Observable, of } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { PageLink } from '@shared/models/page/page-link'; +import { PageData } from '@shared/models/page/page-data'; +import { + ResolvedRuleChainMetaData, + RuleChain, + RuleChainConnectionInfo, + RuleChainMetaData, + ruleChainNodeComponent, + ruleNodeTypeComponentTypes, + unknownNodeComponent +} from '@shared/models/rule-chain.models'; +import { ComponentDescriptorService } from './component-descriptor.service'; +import { + IRuleNodeConfigurationComponent, + LinkLabel, + RuleNodeComponentDescriptor, + TestScriptInputParams, + TestScriptResult +} from '@app/shared/models/rule-node.models'; +import { ResourcesService } from '../services/resources.service'; +import { catchError, map, mergeMap } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; +import { EntityType } from '@shared/models/entity-type.models'; +import { deepClone, snakeCase } from '@core/utils'; +import { DebugRuleNodeEventBody } from '@app/shared/models/event.models'; + +@Injectable({ + providedIn: 'root' +}) +export class RuleChainService { + + private ruleNodeComponents: Array; + private ruleNodeConfigFactories: {[directive: string]: ComponentFactory} = {}; + + constructor( + private http: HttpClient, + private componentDescriptorService: ComponentDescriptorService, + private resourcesService: ResourcesService, + private translate: TranslateService + ) { } + + public getRuleChains(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/ruleChains${pageLink.toQuery()}`, + defaultHttpOptionsFromConfig(config)); + } + + public getRuleChain(ruleChainId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/ruleChain/${ruleChainId}`, defaultHttpOptionsFromConfig(config)); + } + + public createDefaultRuleChain(ruleChainName: string, config?: RequestConfig): Observable { + return this.http.post('/api/ruleChain/device/default', { + name: ruleChainName + }, defaultHttpOptionsFromConfig(config)); + } + + public saveRuleChain(ruleChain: RuleChain, config?: RequestConfig): Observable { + return this.http.post('/api/ruleChain', ruleChain, defaultHttpOptionsFromConfig(config)); + } + + public deleteRuleChain(ruleChainId: string, config?: RequestConfig) { + return this.http.delete(`/api/ruleChain/${ruleChainId}`, defaultHttpOptionsFromConfig(config)); + } + + public setRootRuleChain(ruleChainId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/ruleChain/${ruleChainId}/root`, null, defaultHttpOptionsFromConfig(config)); + } + + public getRuleChainMetadata(ruleChainId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/ruleChain/${ruleChainId}/metadata`, defaultHttpOptionsFromConfig(config)); + } + + public getResolvedRuleChainMetadata(ruleChainId: string, config?: RequestConfig): Observable { + return this.getRuleChainMetadata(ruleChainId, config).pipe( + mergeMap((ruleChainMetaData) => this.resolveRuleChainMetadata(ruleChainMetaData)) + ); + } + + public saveRuleChainMetadata(ruleChainMetaData: RuleChainMetaData, config?: RequestConfig): Observable { + return this.http.post('/api/ruleChain/metadata', ruleChainMetaData, defaultHttpOptionsFromConfig(config)); + } + + public saveAndGetResolvedRuleChainMetadata(ruleChainMetaData: RuleChainMetaData, + config?: RequestConfig): Observable { + return this.saveRuleChainMetadata(ruleChainMetaData, config).pipe( + mergeMap((savedRuleChainMetaData) => this.resolveRuleChainMetadata(savedRuleChainMetaData)) + ); + } + + public resolveRuleChainMetadata(ruleChainMetaData: RuleChainMetaData): Observable { + return this.resolveTargetRuleChains(ruleChainMetaData.ruleChainConnections).pipe( + map((targetRuleChainsMap) => { + const resolvedRuleChainMetadata: ResolvedRuleChainMetaData = {...ruleChainMetaData, targetRuleChainsMap}; + return resolvedRuleChainMetadata; + }) + ); + } + + public getRuleNodeComponents(ruleNodeConfigResourcesModulesMap: {[key: string]: any}, config?: RequestConfig): + Observable> { + if (this.ruleNodeComponents) { + return of(this.ruleNodeComponents); + } else { + return this.loadRuleNodeComponents(config).pipe( + mergeMap((components) => { + return this.resolveRuleNodeComponentsUiResources(components, ruleNodeConfigResourcesModulesMap).pipe( + map((ruleNodeComponents) => { + this.ruleNodeComponents = ruleNodeComponents; + this.ruleNodeComponents.push(ruleChainNodeComponent); + this.ruleNodeComponents.sort( + (comp1, comp2) => { + let result = comp1.type.toString().localeCompare(comp2.type.toString()); + if (result === 0) { + result = comp1.name.localeCompare(comp2.name); + } + return result; + } + ); + return this.ruleNodeComponents; + }) + ); + }) + ); + } + } + + public getRuleNodeConfigFactory(directive: string): ComponentFactory { + return this.ruleNodeConfigFactories[directive]; + } + + public getRuleNodeComponentByClazz(clazz: string): RuleNodeComponentDescriptor { + const found = this.ruleNodeComponents.filter((component) => component.clazz === clazz); + if (found && found.length) { + return found[0]; + } else { + const unknownComponent = deepClone(unknownNodeComponent); + unknownComponent.clazz = clazz; + unknownComponent.configurationDescriptor.nodeDefinition.details = 'Unknown Rule Node class: ' + clazz; + return unknownComponent; + } + } + + public getRuleNodeSupportedLinks(component: RuleNodeComponentDescriptor): {[label: string]: LinkLabel} { + const relationTypes = component.configurationDescriptor.nodeDefinition.relationTypes; + const linkLabels: {[label: string]: LinkLabel} = {}; + relationTypes.forEach((label) => { + linkLabels[label] = { + name: label, + value: label + }; + }); + return linkLabels; + } + + public ruleNodeAllowCustomLinks(component: RuleNodeComponentDescriptor): boolean { + return component.configurationDescriptor.nodeDefinition.customRelations; + } + + public getLatestRuleNodeDebugInput(ruleNodeId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/ruleNode/${ruleNodeId}/debugIn`, defaultHttpOptionsFromConfig(config)); + } + + public testScript(inputParams: TestScriptInputParams, config?: RequestConfig): Observable { + return this.http.post('/api/ruleChain/testScript', inputParams, defaultHttpOptionsFromConfig(config)); + } + + private resolveTargetRuleChains(ruleChainConnections: Array): Observable<{[ruleChainId: string]: RuleChain}> { + if (ruleChainConnections && ruleChainConnections.length) { + const tasks: Observable[] = []; + ruleChainConnections.forEach((connection) => { + tasks.push(this.resolveRuleChain(connection.targetRuleChainId.id)); + }); + return forkJoin(tasks).pipe( + map((ruleChains) => { + const ruleChainsMap: {[ruleChainId: string]: RuleChain} = {}; + ruleChains.forEach((ruleChain) => { + ruleChainsMap[ruleChain.id.id] = ruleChain; + }); + return ruleChainsMap; + }) + ); + } else { + return of({} as {[ruleChainId: string]: RuleChain}); + } + } + + private loadRuleNodeComponents(config?: RequestConfig): Observable> { + return this.componentDescriptorService.getComponentDescriptorsByTypes(ruleNodeTypeComponentTypes, config).pipe( + map((components) => { + const ruleNodeComponents: RuleNodeComponentDescriptor[] = []; + components.forEach((component) => { + ruleNodeComponents.push(component as RuleNodeComponentDescriptor); + }); + return ruleNodeComponents; + }) + ); + } + + private resolveRuleNodeComponentsUiResources(components: Array, + ruleNodeConfigResourcesModulesMap: {[key: string]: any}): + Observable> { + const tasks: Observable[] = []; + components.forEach((component) => { + tasks.push(this.resolveRuleNodeComponentUiResources(component, ruleNodeConfigResourcesModulesMap)); + }); + return forkJoin(tasks).pipe( + catchError((err) => { + return of(components); + }) + ); + } + + private resolveRuleNodeComponentUiResources(component: RuleNodeComponentDescriptor, + ruleNodeConfigResourcesModulesMap: {[key: string]: any}): + Observable { + const nodeDefinition = component.configurationDescriptor.nodeDefinition; + const uiResources = nodeDefinition.uiResources; + if (uiResources && uiResources.length) { + const commonResources = uiResources.filter((resource) => !resource.endsWith('.js')); + const moduleResource = uiResources.find((resource) => resource.endsWith('.js')); + const tasks: Observable[] = []; + if (commonResources && commonResources.length) { + commonResources.forEach((resource) => { + tasks.push(this.resourcesService.loadResource(resource)); + }); + } + if (moduleResource) { + tasks.push(this.resourcesService.loadFactories(moduleResource, ruleNodeConfigResourcesModulesMap).pipe( + map((res) => { + if (nodeDefinition.configDirective && nodeDefinition.configDirective.length) { + const selector = snakeCase(nodeDefinition.configDirective, '-'); + const componentFactory = res.find((factory) => + factory.selector === selector); + if (componentFactory) { + this.ruleNodeConfigFactories[nodeDefinition.configDirective] = componentFactory; + } else { + component.configurationDescriptor.nodeDefinition.uiResourceLoadError = + this.translate.instant('rulenode.directive-is-not-loaded', + {directiveName: nodeDefinition.configDirective}); + } + } + return of(component); + }) + )); + } + return forkJoin(tasks).pipe( + map((res) => { + return component; + }), + catchError(() => { + component.configurationDescriptor.nodeDefinition.uiResourceLoadError = this.translate.instant('rulenode.ui-resources-load-error'); + return of(component); + }) + ); + } else { + return of(component); + } + } + + private resolveRuleChain(ruleChainId: string): Observable { + return this.getRuleChain(ruleChainId, {ignoreErrors: true}).pipe( + map(ruleChain => ruleChain), + catchError((err) => { + const ruleChain = { + id: { + entityType: EntityType.RULE_CHAIN, + id: ruleChainId + } + } as RuleChain; + return of(ruleChain); + }) + ); + } + +} diff --git a/ui-ngx/src/app/core/http/tenant-profile.service.ts b/ui-ngx/src/app/core/http/tenant-profile.service.ts new file mode 100644 index 0000000000..f7d0b7ccdf --- /dev/null +++ b/ui-ngx/src/app/core/http/tenant-profile.service.ts @@ -0,0 +1,67 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { PageLink } from '@shared/models/page/page-link'; +import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { Observable } from 'rxjs'; +import { PageData } from '@shared/models/page/page-data'; +import { TenantProfile } from '@shared/models/tenant.model'; +import { EntityInfoData } from '@shared/models/entity.models'; + +@Injectable({ + providedIn: 'root' +}) +export class TenantProfileService { + + constructor( + private http: HttpClient + ) { } + + public getTenantProfiles(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/tenantProfiles${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + + public getTenantProfile(tenantProfileId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/tenantProfile/${tenantProfileId}`, defaultHttpOptionsFromConfig(config)); + } + + public saveTenantProfile(tenantProfile: TenantProfile, config?: RequestConfig): Observable { + return this.http.post('/api/tenantProfile', tenantProfile, defaultHttpOptionsFromConfig(config)); + } + + public deleteTenantProfile(tenantProfileId: string, config?: RequestConfig) { + return this.http.delete(`/api/tenantProfile/${tenantProfileId}`, defaultHttpOptionsFromConfig(config)); + } + + public setDefaultTenantProfile(tenantProfileId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/tenantProfile/${tenantProfileId}/default`, defaultHttpOptionsFromConfig(config)); + } + + public getDefaultTenantProfileInfo(config?: RequestConfig): Observable { + return this.http.get('/api/tenantProfileInfo/default', defaultHttpOptionsFromConfig(config)); + } + + public getTenantProfileInfo(tenantProfileId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/tenantProfileInfo/${tenantProfileId}`, defaultHttpOptionsFromConfig(config)); + } + + public getTenantProfileInfos(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/tenantProfileInfos${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/http/tenant.service.ts b/ui-ngx/src/app/core/http/tenant.service.ts new file mode 100644 index 0000000000..2306c4852f --- /dev/null +++ b/ui-ngx/src/app/core/http/tenant.service.ts @@ -0,0 +1,58 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { Observable } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { PageLink } from '@shared/models/page/page-link'; +import { PageData } from '@shared/models/page/page-data'; +import { Tenant, TenantInfo } from '@shared/models/tenant.model'; + +@Injectable({ + providedIn: 'root' +}) +export class TenantService { + + constructor( + private http: HttpClient + ) { } + + public getTenants(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/tenants${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + + public getTenantInfos(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/tenantInfos${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + + public getTenant(tenantId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/tenant/${tenantId}`, defaultHttpOptionsFromConfig(config)); + } + + public getTenantInfo(tenantId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/tenant/info/${tenantId}`, defaultHttpOptionsFromConfig(config)); + } + + public saveTenant(tenant: Tenant, config?: RequestConfig): Observable { + return this.http.post('/api/tenant', tenant, defaultHttpOptionsFromConfig(config)); + } + + public deleteTenant(tenantId: string, config?: RequestConfig) { + return this.http.delete(`/api/tenant/${tenantId}`, defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/http/user.service.ts b/ui-ngx/src/app/core/http/user.service.ts new file mode 100644 index 0000000000..d8a2d42c8c --- /dev/null +++ b/ui-ngx/src/app/core/http/user.service.ts @@ -0,0 +1,87 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { User } from '@shared/models/user.model'; +import { Observable } from 'rxjs'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { PageLink } from '@shared/models/page/page-link'; +import { PageData } from '@shared/models/page/page-data'; +import { isDefined } from '@core/utils'; +import { InterceptorHttpParams } from '@core/interceptors/interceptor-http-params'; + +@Injectable({ + providedIn: 'root' +}) +export class UserService { + + constructor( + private http: HttpClient + ) { } + + public getUsers(pageLink: PageLink, + config?: RequestConfig): Observable> { + return this.http.get>(`/api/users${pageLink.toQuery()}`, + defaultHttpOptionsFromConfig(config)); + } + + public getTenantAdmins(tenantId: string, pageLink: PageLink, + config?: RequestConfig): Observable> { + return this.http.get>(`/api/tenant/${tenantId}/users${pageLink.toQuery()}`, + defaultHttpOptionsFromConfig(config)); + } + + public getCustomerUsers(customerId: string, pageLink: PageLink, + config?: RequestConfig): Observable> { + return this.http.get>(`/api/customer/${customerId}/users${pageLink.toQuery()}`, + defaultHttpOptionsFromConfig(config)); + } + + public getUser(userId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/user/${userId}`, defaultHttpOptionsFromConfig(config)); + } + + public saveUser(user: User, sendActivationMail: boolean = false, + config?: RequestConfig): Observable { + let url = '/api/user'; + url += '?sendActivationMail=' + sendActivationMail; + return this.http.post(url, user, defaultHttpOptionsFromConfig(config)); + } + + public deleteUser(userId: string, config?: RequestConfig) { + return this.http.delete(`/api/user/${userId}`, defaultHttpOptionsFromConfig(config)); + } + + public getActivationLink(userId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/user/${userId}/activationLink`, + {...{responseType: 'text'}, ...defaultHttpOptionsFromConfig(config)}); + } + + public sendActivationEmail(email: string, config?: RequestConfig) { + const encodeEmail = encodeURIComponent(email); + return this.http.post(`/api/user/sendActivationMail?email=${encodeEmail}`, null, defaultHttpOptionsFromConfig(config)); + } + + public setUserCredentialsEnabled(userId: string, userCredentialsEnabled?: boolean, config?: RequestConfig): Observable { + let url = `/api/user/${userId}/userCredentialsEnabled`; + if (isDefined(userCredentialsEnabled)) { + url += `?userCredentialsEnabled=${userCredentialsEnabled}`; + } + return this.http.post(url, null, defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/http/widget.service.ts b/ui-ngx/src/app/core/http/widget.service.ts new file mode 100644 index 0000000000..4440d8a4a9 --- /dev/null +++ b/ui-ngx/src/app/core/http/widget.service.ts @@ -0,0 +1,285 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { Observable, of, ReplaySubject, Subject } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { PageLink } from '@shared/models/page/page-link'; +import { PageData } from '@shared/models/page/page-data'; +import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; +import { Widget, WidgetType, widgetType, widgetTypesData } from '@shared/models/widget.models'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { ResourcesService } from '../services/resources.service'; +import { toWidgetInfo, toWidgetType, WidgetInfo } from '@app/modules/home/models/widget-component.models'; +import { filter, map, mergeMap, tap } from 'rxjs/operators'; +import { WidgetTypeId } from '@shared/models/id/widget-type-id'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { ActivationEnd, Router } from '@angular/router'; + +@Injectable({ + providedIn: 'root' +}) +export class WidgetService { + + private widgetTypeUpdatedSubject = new Subject(); + private widgetsBundleDeletedSubject = new Subject(); + + private allWidgetsBundles: Array; + private systemWidgetsBundles: Array; + private tenantWidgetsBundles: Array; + + private loadWidgetsBundleCacheSubject: ReplaySubject; + + constructor( + private http: HttpClient, + private utils: UtilsService, + private resources: ResourcesService, + private translate: TranslateService, + private router: Router + ) { + this.router.events.pipe(filter(event => event instanceof ActivationEnd)).subscribe( + () => { + this.invalidateWidgetsBundleCache(); + } + ); + } + + public getAllWidgetsBundles(config?: RequestConfig): Observable> { + return this.loadWidgetsBundleCache(config).pipe( + map(() => this.allWidgetsBundles) + ); + } + + public getSystemWidgetsBundles(config?: RequestConfig): Observable> { + return this.loadWidgetsBundleCache(config).pipe( + map(() => this.systemWidgetsBundles) + ); + } + + public getTenantWidgetsBundles(config?: RequestConfig): Observable> { + return this.loadWidgetsBundleCache(config).pipe( + map(() => this.tenantWidgetsBundles) + ); + } + + public getWidgetBundles(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/widgetsBundles${pageLink.toQuery()}`, + defaultHttpOptionsFromConfig(config)); + } + + public getWidgetsBundle(widgetsBundleId: string, + config?: RequestConfig): Observable { + return this.http.get(`/api/widgetsBundle/${widgetsBundleId}`, defaultHttpOptionsFromConfig(config)); + } + + public saveWidgetsBundle(widgetsBundle: WidgetsBundle, + config?: RequestConfig): Observable { + return this.http.post('/api/widgetsBundle', widgetsBundle, + defaultHttpOptionsFromConfig(config)).pipe( + tap(() => { + this.invalidateWidgetsBundleCache(); + }) + ); + } + + public deleteWidgetsBundle(widgetsBundleId: string, config?: RequestConfig) { + return this.getWidgetsBundle(widgetsBundleId, config).pipe( + mergeMap((widgetsBundle) => { + return this.http.delete(`/api/widgetsBundle/${widgetsBundleId}`, + defaultHttpOptionsFromConfig(config)).pipe( + tap(() => { + this.invalidateWidgetsBundleCache(); + this.widgetsBundleDeletedSubject.next(widgetsBundle); + }) + ); + } + )); + } + + public getBundleWidgetTypes(bundleAlias: string, isSystem: boolean, + config?: RequestConfig): Observable> { + return this.http.get>(`/api/widgetTypes?isSystem=${isSystem}&bundleAlias=${bundleAlias}`, + defaultHttpOptionsFromConfig(config)); + } + + public loadBundleLibraryWidgets(bundleAlias: string, isSystem: boolean, + config?: RequestConfig): Observable> { + return this.getBundleWidgetTypes(bundleAlias, isSystem, config).pipe( + map((types) => { + types = types.sort((a, b) => { + let result = widgetType[b.descriptor.type].localeCompare(widgetType[a.descriptor.type]); + if (result === 0) { + result = b.createdTime - a.createdTime; + } + return result; + }); + const widgetTypes = new Array(); + let top = 0; + const lastTop = [0, 0, 0]; + let col = 0; + let column = 0; + types.forEach((type) => { + const widgetTypeInfo = toWidgetInfo(type); + const sizeX = 8; + const sizeY = Math.floor(widgetTypeInfo.sizeY); + const widget: Widget = { + typeId: type.id, + isSystemType: isSystem, + bundleAlias, + typeAlias: widgetTypeInfo.alias, + type: widgetTypeInfo.type, + title: widgetTypeInfo.widgetName, + sizeX, + sizeY, + row: top, + col, + config: JSON.parse(widgetTypeInfo.defaultConfig) + }; + + widget.config.title = widgetTypeInfo.widgetName; + + widgetTypes.push(widget); + top += sizeY; + if (top > lastTop[column] + 10) { + lastTop[column] = top; + column++; + if (column > 2) { + column = 0; + } + top = lastTop[column]; + col = column * 8; + } + }); + return widgetTypes; + }) + ); + } + + public getWidgetType(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean, + config?: RequestConfig): Observable { + return this.http.get(`/api/widgetType?isSystem=${isSystem}&bundleAlias=${bundleAlias}&alias=${widgetTypeAlias}`, + defaultHttpOptionsFromConfig(config)); + } + + public saveWidgetType(widgetInfo: WidgetInfo, + id: WidgetTypeId, + bundleAlias: string, + config?: RequestConfig): Observable { + const widgetTypeInstance = toWidgetType(widgetInfo, id, undefined, bundleAlias); + return this.http.post('/api/widgetType', widgetTypeInstance, + defaultHttpOptionsFromConfig(config)).pipe( + tap((savedWidgetType) => { + this.widgetTypeUpdatedSubject.next(savedWidgetType); + })); + } + + public saveImportedWidgetType(widgetTypeInstance: WidgetType, + config?: RequestConfig): Observable { + return this.http.post('/api/widgetType', widgetTypeInstance, + defaultHttpOptionsFromConfig(config)).pipe( + tap((savedWidgetType) => { + this.widgetTypeUpdatedSubject.next(savedWidgetType); + })); + } + + public deleteWidgetType(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean, + config?: RequestConfig) { + return this.getWidgetType(bundleAlias, widgetTypeAlias, isSystem, config).pipe( + mergeMap((widgetTypeInstance) => { + return this.http.delete(`/api/widgetType/${widgetTypeInstance.id.id}`, + defaultHttpOptionsFromConfig(config)).pipe( + tap(() => { + this.widgetTypeUpdatedSubject.next(widgetTypeInstance); + }) + ); + } + )); + } + + public getWidgetTypeById(widgetTypeId: string, + config?: RequestConfig): Observable { + return this.http.get(`/api/widgetType/${widgetTypeId}`, + defaultHttpOptionsFromConfig(config)); + } + + public getWidgetTemplate(widgetTypeParam: widgetType, + config?: RequestConfig): Observable { + const templateWidgetType = widgetTypesData.get(widgetTypeParam); + return this.getWidgetType(templateWidgetType.template.bundleAlias, templateWidgetType.template.alias, true, + config).pipe( + map((result) => { + const widgetInfo = toWidgetInfo(result); + widgetInfo.alias = undefined; + return widgetInfo; + }) + ); + } + + public onWidgetTypeUpdated(): Observable { + return this.widgetTypeUpdatedSubject.asObservable(); + } + + public onWidgetBundleDeleted(): Observable { + return this.widgetsBundleDeletedSubject.asObservable(); + } + + private loadWidgetsBundleCache(config?: RequestConfig): Observable { + if (!this.allWidgetsBundles) { + if (!this.loadWidgetsBundleCacheSubject) { + this.loadWidgetsBundleCacheSubject = new ReplaySubject(); + this.http.get>('/api/widgetsBundles', + defaultHttpOptionsFromConfig(config)).subscribe( + (allWidgetsBundles) => { + this.allWidgetsBundles = allWidgetsBundles; + this.systemWidgetsBundles = new Array(); + this.tenantWidgetsBundles = new Array(); + this.allWidgetsBundles = this.allWidgetsBundles.sort((wb1, wb2) => { + let res = wb1.title.localeCompare(wb2.title); + if (res === 0) { + res = wb2.createdTime - wb1.createdTime; + } + return res; + }); + this.allWidgetsBundles.forEach((widgetsBundle) => { + if (widgetsBundle.tenantId.id === NULL_UUID) { + this.systemWidgetsBundles.push(widgetsBundle); + } else { + this.tenantWidgetsBundles.push(widgetsBundle); + } + }); + this.loadWidgetsBundleCacheSubject.next(); + this.loadWidgetsBundleCacheSubject.complete(); + }, + () => { + this.loadWidgetsBundleCacheSubject.error(null); + }); + } + return this.loadWidgetsBundleCacheSubject.asObservable(); + } else { + return of(null); + } + } + + private invalidateWidgetsBundleCache() { + this.allWidgetsBundles = undefined; + this.systemWidgetsBundles = undefined; + this.tenantWidgetsBundles = undefined; + this.loadWidgetsBundleCacheSubject = undefined; + } + +} diff --git a/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts b/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts new file mode 100644 index 0000000000..2874ed6cda --- /dev/null +++ b/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts @@ -0,0 +1,273 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + HttpErrorResponse, + HttpEvent, + HttpHandler, + HttpInterceptor, + HttpRequest, + HttpResponseBase +} from '@angular/common/http'; +import { Observable } from 'rxjs/internal/Observable'; +import { Inject, Injectable } from '@angular/core'; +import { AuthService } from '@core/auth/auth.service'; +import { Constants } from '@shared/models/constants'; +import { InterceptorHttpParams } from './interceptor-http-params'; +import { catchError, delay, mergeMap, switchMap, tap } from 'rxjs/operators'; +import { throwError, of } from 'rxjs'; +import { InterceptorConfig } from './interceptor-config'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ActionLoadFinish, ActionLoadStart } from './load.actions'; +import { ActionNotificationShow } from '@app/core/notification/notification.actions'; +import { DialogService } from '@core/services/dialog.service'; +import { TranslateService } from '@ngx-translate/core'; + +let tmpHeaders = {}; + +@Injectable() +export class GlobalHttpInterceptor implements HttpInterceptor { + + private AUTH_SCHEME = 'Bearer '; + private AUTH_HEADER_NAME = 'X-Authorization'; + + private internalUrlPrefixes = [ + '/api/auth/token', + '/api/plugins/rpc' + ]; + + private activeRequests = 0; + + constructor(@Inject(Store) private store: Store, + @Inject(DialogService) private dialogService: DialogService, + @Inject(TranslateService) private translate: TranslateService, + @Inject(AuthService) private authService: AuthService) { + } + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + if (req.url.startsWith('/api/')) { + const config = this.getInterceptorConfig(req); + const isLoading = !this.isInternalUrlPrefix(req.url); + this.updateLoadingState(config, isLoading); + if (this.isTokenBasedAuthEntryPoint(req.url)) { + if (!AuthService.getJwtToken() && !this.authService.refreshTokenPending()) { + return this.handleResponseError(req, next, new HttpErrorResponse({error: {message: 'Unauthorized!'}, status: 401})); + } else if (!AuthService.isJwtTokenValid()) { + return this.handleResponseError(req, next, new HttpErrorResponse({error: {refreshTokenPending: true}})); + } else { + return this.jwtIntercept(req, next); + } + } else { + return this.handleRequest(req, next); + } + } else { + return next.handle(req); + } + } + + private jwtIntercept(req: HttpRequest, next: HttpHandler): Observable> { + const newReq = this.updateAuthorizationHeader(req); + if (newReq) { + return this.handleRequest(newReq, next); + } else { + return this.handleRequestError(req, new Error('Could not get JWT token from store.')); + } + } + + private handleRequest(req: HttpRequest, next: HttpHandler): Observable> { + return next.handle(req).pipe( + tap((event: HttpEvent) => { + if (event instanceof HttpResponseBase) { + this.handleResponse(req, event as HttpResponseBase); + } + }), + catchError((err) => { + const errorResponse = err as HttpErrorResponse; + return this.handleResponseError(req, next, errorResponse); + })); + } + + private handleRequestError(req: HttpRequest, err): Observable> { + const config = this.getInterceptorConfig(req); + if (req.url.startsWith('/api/')) { + this.updateLoadingState(config, false); + } + return throwError(err); + } + + private handleResponse(req: HttpRequest, response: HttpResponseBase) { + const config = this.getInterceptorConfig(req); + if (req.url.startsWith('/api/')) { + this.updateLoadingState(config, false); + } + } + + private handleResponseError(req: HttpRequest, next: HttpHandler, errorResponse: HttpErrorResponse): Observable> { + const config = this.getInterceptorConfig(req); + if (req.url.startsWith('/api/')) { + this.updateLoadingState(config, false); + } + let unhandled = false; + const ignoreErrors = config.ignoreErrors; + const resendRequest = config.resendRequest; + const errorCode = errorResponse.error ? errorResponse.error.errorCode : null; + if (errorResponse.error && errorResponse.error.refreshTokenPending || errorResponse.status === 401) { + if (errorResponse.error && errorResponse.error.refreshTokenPending || + errorCode && errorCode === Constants.serverErrorCode.jwtTokenExpired) { + return this.refreshTokenAndRetry(req, next); + } else if (errorCode !== Constants.serverErrorCode.credentialsExpired) { + unhandled = true; + } + } else if (errorResponse.status === 429) { + if (resendRequest) { + return this.retryRequest(req, next); + } + } else if (errorResponse.status === 403) { + if (!ignoreErrors) { + this.dialogService.forbidden(); + } + } else if (errorResponse.status === 0 || errorResponse.status === -1) { + this.showError('Unable to connect'); + } else if (!req.url.startsWith('/api/plugins/rpc')) { + if (errorResponse.status === 404) { + if (!ignoreErrors) { + this.showError(req.method + ': ' + req.url + '
' + + errorResponse.status + ': ' + errorResponse.statusText); + } + } else { + unhandled = true; + } + } + + if (unhandled && !ignoreErrors) { + let error = null; + if (req.responseType === 'text') { + try { + error = errorResponse.error ? JSON.parse(errorResponse.error) : null; + } catch (e) {} + } else { + error = errorResponse.error; + } + if (error && !error.message) { + this.showError(this.prepareMessageFromData(error)); + } else if (error && error.message) { + this.showError(error.message, error.timeout ? error.timeout : 0); + } else { + this.showError('Unhandled error code ' + (error ? error.status : '\'Unknown\'')); + } + } + return throwError(errorResponse); + } + + private prepareMessageFromData(data) { + if (typeof data === 'object' && data.constructor === ArrayBuffer) { + const msg = String.fromCharCode.apply(null, new Uint8Array(data)); + try { + const msgObj = JSON.parse(msg); + if (msgObj.message) { + return msgObj.message; + } else { + return msg; + } + } catch (e) { + return msg; + } + } else { + return data; + } + } + + private retryRequest(req: HttpRequest, next: HttpHandler): Observable> { + const thisTimeout = 1000 + Math.random() * 3000; + return of(null).pipe( + delay(thisTimeout), + mergeMap(() => { + return this.jwtIntercept(req, next); + } + )); + } + + private refreshTokenAndRetry(req: HttpRequest, next: HttpHandler): Observable> { + return this.authService.refreshJwtToken().pipe(switchMap(() => { + return this.jwtIntercept(req, next); + }), + catchError((err: Error) => { + this.authService.logout(true); + const message = err ? err.message : 'Unauthorized!'; + return this.handleResponseError(req, next, new HttpErrorResponse({error: {message, timeout: 200}, status: 401})); + })); + } + + private updateAuthorizationHeader(req: HttpRequest): HttpRequest { + const jwtToken = AuthService.getJwtToken(); + if (jwtToken) { + req = req.clone({ + setHeaders: (tmpHeaders = {}, + tmpHeaders[this.AUTH_HEADER_NAME] = '' + this.AUTH_SCHEME + jwtToken, + tmpHeaders) + }); + return req; + } else { + return null; + } + } + + private isInternalUrlPrefix(url): boolean { + for (const index in this.internalUrlPrefixes) { + if (url.startsWith(this.internalUrlPrefixes[index])) { + return true; + } + } + return false; + } + + private isTokenBasedAuthEntryPoint(url): boolean { + return url.startsWith('/api/') && + !url.startsWith(Constants.entryPoints.login) && + !url.startsWith(Constants.entryPoints.tokenRefresh) && + !url.startsWith(Constants.entryPoints.nonTokenBased); + } + + private updateLoadingState(config: InterceptorConfig, isLoading: boolean) { + if (!config.ignoreLoading) { + if (isLoading) { + this.activeRequests++; + } else { + this.activeRequests--; + } + if (this.activeRequests === 1 && isLoading) { + this.store.dispatch(new ActionLoadStart()); + } else if (this.activeRequests === 0) { + this.store.dispatch(new ActionLoadFinish()); + } + } + } + + private getInterceptorConfig(req: HttpRequest): InterceptorConfig { + if (req.params && req.params instanceof InterceptorHttpParams) { + return (req.params as InterceptorHttpParams).interceptorConfig; + } else { + return new InterceptorConfig(false, false); + } + } + + private showError(error: string, timeout: number = 0) { + setTimeout(() => { + this.store.dispatch(new ActionNotificationShow({message: error, type: 'error'})); + }, timeout); + } +} diff --git a/ui-ngx/src/app/core/interceptors/interceptor-config.ts b/ui-ngx/src/app/core/interceptors/interceptor-config.ts new file mode 100644 index 0000000000..967d0764e7 --- /dev/null +++ b/ui-ngx/src/app/core/interceptors/interceptor-config.ts @@ -0,0 +1,21 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export class InterceptorConfig { + constructor(public ignoreLoading: boolean = false, + public ignoreErrors: boolean = false, + public resendRequest: boolean = false) {} +} diff --git a/ui-ngx/src/app/core/interceptors/interceptor-http-params.ts b/ui-ngx/src/app/core/interceptors/interceptor-http-params.ts new file mode 100644 index 0000000000..7a6f8468a4 --- /dev/null +++ b/ui-ngx/src/app/core/interceptors/interceptor-http-params.ts @@ -0,0 +1,27 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { HttpParams } from '@angular/common/http'; +import { InterceptorConfig } from './interceptor-config'; + +export class InterceptorHttpParams extends HttpParams { + constructor( + public interceptorConfig: InterceptorConfig, + params?: { [param: string]: string | string[] } + ) { + super({ fromObject: params }); + } +} diff --git a/ui-ngx/src/app/core/interceptors/load.actions.ts b/ui-ngx/src/app/core/interceptors/load.actions.ts new file mode 100644 index 0000000000..7ae98d1a35 --- /dev/null +++ b/ui-ngx/src/app/core/interceptors/load.actions.ts @@ -0,0 +1,32 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Action } from '@ngrx/store'; + +export enum LoadActionTypes { + START_LOAD = '[Load] Start', + FINISH_LOAD = '[Load] Finish' +} + +export class ActionLoadStart implements Action { + readonly type = LoadActionTypes.START_LOAD; +} + +export class ActionLoadFinish implements Action { + readonly type = LoadActionTypes.FINISH_LOAD; +} + +export type LoadActions = ActionLoadStart | ActionLoadFinish; diff --git a/ui-ngx/src/app/core/interceptors/load.models.ts b/ui-ngx/src/app/core/interceptors/load.models.ts new file mode 100644 index 0000000000..10a07afd98 --- /dev/null +++ b/ui-ngx/src/app/core/interceptors/load.models.ts @@ -0,0 +1,19 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export interface LoadState { + isLoading: boolean; +} diff --git a/ui-ngx/src/app/core/interceptors/load.reducer.ts b/ui-ngx/src/app/core/interceptors/load.reducer.ts new file mode 100644 index 0000000000..6223f392e7 --- /dev/null +++ b/ui-ngx/src/app/core/interceptors/load.reducer.ts @@ -0,0 +1,38 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { LoadState } from './load.models'; +import { LoadActions, LoadActionTypes } from './load.actions'; + +export const initialState: LoadState = { + isLoading: false +}; + +export function loadReducer( + state: LoadState = initialState, + action: LoadActions +): LoadState { + switch (action.type) { + case LoadActionTypes.START_LOAD: + return { ...state, isLoading: true }; + + case LoadActionTypes.FINISH_LOAD: + return { ...state, isLoading: false }; + + default: + return state; + } +} diff --git a/ui-ngx/src/app/core/interceptors/load.selectors.ts b/ui-ngx/src/app/core/interceptors/load.selectors.ts new file mode 100644 index 0000000000..a423d6c9ab --- /dev/null +++ b/ui-ngx/src/app/core/interceptors/load.selectors.ts @@ -0,0 +1,43 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { createFeatureSelector, createSelector, select, Store } from '@ngrx/store'; + +import { AppState } from '../core.state'; +import { LoadState } from './load.models'; +import { take } from 'rxjs/operators'; + +export const selectLoadState = createFeatureSelector( + 'load' +); + +export const selectLoad = createSelector( + selectLoadState, + (state: LoadState) => state +); + +export const selectIsLoading = createSelector( + selectLoadState, + (state: LoadState) => state.isLoading +); + +export function getCurrentIsLoading(store: Store): boolean { + let isLoading: boolean; + store.pipe(select(selectIsLoading), take(1)).subscribe( + val => isLoading = val + ); + return isLoading; +} diff --git a/ui-ngx/src/app/core/local-storage/local-storage.service.ts b/ui-ngx/src/app/core/local-storage/local-storage.service.ts new file mode 100644 index 0000000000..37ea7ad062 --- /dev/null +++ b/ui-ngx/src/app/core/local-storage/local-storage.service.ts @@ -0,0 +1,88 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; + +const APP_PREFIX = 'TB-'; + +// @dynamic +@Injectable( + { + providedIn: 'root' + } +) +export class LocalStorageService { + constructor() {} + + static loadInitialState() { + return Object.keys(localStorage).reduce((state: any, storageKey) => { + if (storageKey.includes(APP_PREFIX)) { + const stateKeys = storageKey + .replace(APP_PREFIX, '') + .toLowerCase() + .split('.') + .map(key => + key + .split('-') + .map( + (token, index) => + index === 0 + ? token + : token.charAt(0).toUpperCase() + token.slice(1) + ) + .join('') + ); + let currentStateRef = state; + stateKeys.forEach((key, index) => { + if (index === stateKeys.length - 1) { + currentStateRef[key] = JSON.parse(localStorage.getItem(storageKey)); + return; + } + currentStateRef[key] = currentStateRef[key] || {}; + currentStateRef = currentStateRef[key]; + }); + } + return state; + }, {}); + } + + setItem(key: string, value: any) { + localStorage.setItem(`${APP_PREFIX}${key}`, JSON.stringify(value)); + } + + getItem(key: string) { + return JSON.parse(localStorage.getItem(`${APP_PREFIX}${key}`)); + } + + removeItem(key: string) { + localStorage.removeItem(`${APP_PREFIX}${key}`); + } + /** Tests that localStorage exists, can be written to, and read from. */ + testLocalStorage() { + const testValue = 'testValue'; + const testKey = 'testKey'; + let retrievedValue: string; + const errorMessage = 'localStorage did not return expected value'; + + this.setItem(testKey, testValue); + retrievedValue = this.getItem(testKey); + this.removeItem(testKey); + + if (retrievedValue !== testValue) { + throw new Error(errorMessage); + } + } +} diff --git a/ui-ngx/src/app/core/meta-reducers/debug.reducer.ts b/ui-ngx/src/app/core/meta-reducers/debug.reducer.ts new file mode 100644 index 0000000000..a056ba0294 --- /dev/null +++ b/ui-ngx/src/app/core/meta-reducers/debug.reducer.ts @@ -0,0 +1,33 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ActionReducer } from '@ngrx/store'; + +import { AppState } from '../core.state'; + +export function debug( + reducer: ActionReducer +): ActionReducer { + return (state, action) => { + const newState = reducer(state, action); + console.log(`[DEBUG] action: ${action.type}`, { + payload: (action as any).payload, + oldState: state, + newState + }); + return newState; + }; +} diff --git a/ui-ngx/src/app/core/meta-reducers/init-state-from-local-storage.reducer.ts b/ui-ngx/src/app/core/meta-reducers/init-state-from-local-storage.reducer.ts new file mode 100644 index 0000000000..2e02ea94d2 --- /dev/null +++ b/ui-ngx/src/app/core/meta-reducers/init-state-from-local-storage.reducer.ts @@ -0,0 +1,32 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ActionReducer, INIT, UPDATE } from '@ngrx/store'; + +import { LocalStorageService } from '../local-storage/local-storage.service'; +import { AppState } from '../core.state'; + +export function initStateFromLocalStorage( + reducer: ActionReducer +): ActionReducer { + return (state, action) => { + const newState = reducer(state, action); + if ([INIT.toString(), UPDATE.toString()].includes(action.type)) { + return { ...newState, ...LocalStorageService.loadInitialState() }; + } + return newState; + }; +} diff --git a/ui-ngx/src/app/core/notification/notification.actions.ts b/ui-ngx/src/app/core/notification/notification.actions.ts new file mode 100644 index 0000000000..098e5cd2c8 --- /dev/null +++ b/ui-ngx/src/app/core/notification/notification.actions.ts @@ -0,0 +1,38 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Action } from '@ngrx/store'; +import { HideNotification, NotificationMessage } from '@app/core/notification/notification.models'; + +export enum NotificationActionTypes { + SHOW_NOTIFICATION = '[Notification] Show', + HIDE_NOTIFICATION = '[Notification] Hide' +} + +export class ActionNotificationShow implements Action { + readonly type = NotificationActionTypes.SHOW_NOTIFICATION; + + constructor(readonly notification: NotificationMessage ) {} +} + +export class ActionNotificationHide implements Action { + readonly type = NotificationActionTypes.HIDE_NOTIFICATION; + + constructor(readonly hideNotification: HideNotification ) {} +} + +export type NotificationActions = + | ActionNotificationShow | ActionNotificationHide; diff --git a/ui-ngx/src/app/core/notification/notification.effects.ts b/ui-ngx/src/app/core/notification/notification.effects.ts new file mode 100644 index 0000000000..a7dd698172 --- /dev/null +++ b/ui-ngx/src/app/core/notification/notification.effects.ts @@ -0,0 +1,51 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { Actions, Effect, ofType } from '@ngrx/effects'; +import { map } from 'rxjs/operators'; + +import { NotificationActions, NotificationActionTypes } from '@app/core/notification/notification.actions'; +import { NotificationService } from '@app/core/services/notification.service'; + +@Injectable() +export class NotificationEffects { + constructor( + private actions$: Actions, + private notificationService: NotificationService + ) { + } + + @Effect({dispatch: false}) + dispatchNotification = this.actions$.pipe( + ofType( + NotificationActionTypes.SHOW_NOTIFICATION, + ), + map(({ notification }) => { + this.notificationService.dispatchNotification(notification); + }) + ); + + @Effect({dispatch: false}) + hideNotification = this.actions$.pipe( + ofType( + NotificationActionTypes.HIDE_NOTIFICATION, + ), + map(({ hideNotification }) => { + this.notificationService.hideNotification(hideNotification); + }) + ); +} diff --git a/ui-ngx/src/app/core/notification/notification.models.ts b/ui-ngx/src/app/core/notification/notification.models.ts new file mode 100644 index 0000000000..69e9f0bb1d --- /dev/null +++ b/ui-ngx/src/app/core/notification/notification.models.ts @@ -0,0 +1,40 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + + +export interface NotificationState { + notification: NotificationMessage; + hideNotification: HideNotification; +} + +export declare type NotificationType = 'info' | 'warn' | 'success' | 'error'; +export declare type NotificationHorizontalPosition = 'start' | 'center' | 'end' | 'left' | 'right'; +export declare type NotificationVerticalPosition = 'top' | 'bottom'; + +export class NotificationMessage { + message: string; + type: NotificationType; + target?: string; + duration?: number; + forceDismiss?: boolean; + horizontalPosition?: NotificationHorizontalPosition; + verticalPosition?: NotificationVerticalPosition; + panelClass?: string | string[]; +} + +export class HideNotification { + target?: string; +} diff --git a/ui-ngx/src/app/core/notification/notification.reducer.ts b/ui-ngx/src/app/core/notification/notification.reducer.ts new file mode 100644 index 0000000000..74de03af58 --- /dev/null +++ b/ui-ngx/src/app/core/notification/notification.reducer.ts @@ -0,0 +1,37 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NotificationState } from './notification.models'; +import { NotificationActions, NotificationActionTypes } from './notification.actions'; + +export const initialState: NotificationState = { + notification: null, + hideNotification: null +}; + +export function notificationReducer( + state: NotificationState = initialState, + action: NotificationActions +): NotificationState { + switch (action.type) { + case NotificationActionTypes.SHOW_NOTIFICATION: + return { ...state, notification: action.notification }; + case NotificationActionTypes.HIDE_NOTIFICATION: + return { ...state, hideNotification: action.hideNotification }; + default: + return state; + } +} diff --git a/ui-ngx/src/app/core/operator/enterZone.ts b/ui-ngx/src/app/core/operator/enterZone.ts new file mode 100644 index 0000000000..b7c005bd96 --- /dev/null +++ b/ui-ngx/src/app/core/operator/enterZone.ts @@ -0,0 +1,44 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + + +import { MonoTypeOperatorFunction, Observable, Operator, Subscriber } from 'rxjs'; + +export type EnterZoneSignature = (zone: { run: (fn: any) => any }) => Observable; + +export function enterZone(zone: { run: (fn: any) => any }): MonoTypeOperatorFunction { + return (source: Observable) => { + return source.lift(new EnterZoneOperator(zone)); + }; +} + +export class EnterZoneOperator implements Operator { + constructor(private zone: { run: (fn: any) => any }) { } + + call(subscriber: Subscriber, source: any): any { + return source._subscribe(new EnterZoneSubscriber(subscriber, this.zone)); + } +} + +class EnterZoneSubscriber extends Subscriber { + constructor(destination: Subscriber, private zone: { run: (fn: any) => any }) { + super(destination); + } + + protected _next(value: T) { + this.zone.run(() => this.destination.next(value)); + } +} diff --git a/ui-ngx/src/app/core/public-api.ts b/ui-ngx/src/app/core/public-api.ts new file mode 100644 index 0000000000..ea8b21b934 --- /dev/null +++ b/ui-ngx/src/app/core/public-api.ts @@ -0,0 +1,24 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export * from './api/public-api'; +export * from './http/public-api'; +export * from './local-storage/local-storage.service'; +export * from './services/public-api'; +export * from './ws/telemetry-websocket.service'; +export * from './core.state'; +export * from './core.module'; +export * from './utils'; diff --git a/ui-ngx/src/app/core/schema-utils.ts b/ui-ngx/src/app/core/schema-utils.ts new file mode 100644 index 0000000000..97a525acd5 --- /dev/null +++ b/ui-ngx/src/app/core/schema-utils.ts @@ -0,0 +1,84 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { JsonSettingsSchema } from '@shared/models/widget.models'; + +export function initSchema(): JsonSettingsSchema { + return { + schema: { + type: 'object', + properties: {}, + required: [] + }, + form: [], + groupInfoes: [] + }; +} + +export function addGroupInfo(schema: JsonSettingsSchema, title: string) { + schema.groupInfoes.push({ + formIndex: schema.groupInfoes?.length || 0, + GroupTitle: title + }); +} + +export function addToSchema(schema: JsonSettingsSchema, newSchema: JsonSettingsSchema) { + Object.assign(schema.schema.properties, newSchema.schema.properties); + schema.schema.required = schema.schema.required.concat(newSchema.schema.required); + schema.form.push(newSchema.form); +} + +export function mergeSchemes(schemes: JsonSettingsSchema[]): JsonSettingsSchema { + return schemes.reduce((finalSchema: JsonSettingsSchema, schema: JsonSettingsSchema) => { + return { + schema: { + properties: { + ...finalSchema.schema.properties, + ...schema.schema.properties + }, + required: [ + ...finalSchema.schema.required, + ...schema.schema.required + ] + }, + form: [ + ...finalSchema.form, + ...schema.form + ] + } as JsonSettingsSchema; + }, initSchema()); +} + +export function addCondition(schema: JsonSettingsSchema, condition: string, exclude: string[] = []): JsonSettingsSchema { + schema.form = schema.form.map(element => { + if (!exclude.includes(element) && !exclude.includes(element.key)) { + if (typeof element === 'string') { + return { + key: element, + condition + } + } + if (typeof element === 'object') { + if (element.condition) { + element.condition += ' && ' + condition + } + else element.condition = condition; + } + } + return element; + }); + return schema; +} diff --git a/ui-ngx/src/app/core/services/broadcast.models.ts b/ui-ngx/src/app/core/services/broadcast.models.ts new file mode 100644 index 0000000000..99c0a0a905 --- /dev/null +++ b/ui-ngx/src/app/core/services/broadcast.models.ts @@ -0,0 +1,27 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + + +export interface BroadcastMessage { + name: string; + args?: Array; +} + +export interface BroadcastEvent { + name: string; +} + +export type BroadcastListener = (event: BroadcastEvent, ...args: Array) => void; diff --git a/ui-ngx/src/app/core/services/broadcast.service.ts b/ui-ngx/src/app/core/services/broadcast.service.ts new file mode 100644 index 0000000000..92dc2ef98e --- /dev/null +++ b/ui-ngx/src/app/core/services/broadcast.service.ts @@ -0,0 +1,50 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { Subject, Subscription } from 'rxjs'; +import { BroadcastEvent, BroadcastListener, BroadcastMessage } from '@core/services/broadcast.models'; +import { filter } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root' +}) +export class BroadcastService { + + private broadcastSubject: Subject = new Subject(); + + broadcast(name: string, ...args: Array) { + const message = { + name, + args + } as BroadcastMessage; + this.broadcastSubject.next(message); + } + + on(name: string, listener: BroadcastListener): Subscription { + return this.broadcastSubject.asObservable().pipe( + filter((message) => message.name === name) + ).subscribe( + (message) => { + const event = { + name: message.name + } as BroadcastEvent; + listener(event, message.args); + } + ); + } + +} diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts new file mode 100644 index 0000000000..0b21a3f9be --- /dev/null +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -0,0 +1,625 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { UtilsService } from '@core/services/utils.service'; +import { TimeService } from '@core/services/time.service'; +import { + Dashboard, + DashboardConfiguration, + DashboardLayout, + DashboardLayoutId, + DashboardLayoutInfo, + DashboardLayoutsInfo, + DashboardState, + DashboardStateLayouts, + GridSettings, + WidgetLayout +} from '@shared/models/dashboard.models'; +import { isDefined, isString, isUndefined } from '@core/utils'; +import { Datasource, DatasourceType, Widget } from '@app/shared/models/widget.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { AliasFilterType, EntityAlias, EntityAliasFilter } from '@app/shared/models/alias.models'; +import { EntityId } from '@app/shared/models/id/entity-id'; + +@Injectable({ + providedIn: 'root' +}) +export class DashboardUtilsService { + + constructor(private utils: UtilsService, + private timeService: TimeService) { + } + + public validateAndUpdateDashboard(dashboard: Dashboard): Dashboard { + if (!dashboard.configuration) { + dashboard.configuration = {}; + } + if (isUndefined(dashboard.configuration.widgets)) { + dashboard.configuration.widgets = {}; + } else if (Array.isArray(dashboard.configuration.widgets)) { + const widgetsMap: {[id: string]: Widget} = {}; + dashboard.configuration.widgets.forEach((widget) => { + if (!widget.id) { + widget.id = this.utils.guid(); + } + widgetsMap[widget.id] = widget; + }); + dashboard.configuration.widgets = widgetsMap; + } + for (const id of Object.keys(dashboard.configuration.widgets)) { + const widget = dashboard.configuration.widgets[id]; + dashboard.configuration.widgets[id] = this.validateAndUpdateWidget(widget); + } + if (isUndefined(dashboard.configuration.states)) { + dashboard.configuration.states = { + default: this.createDefaultState(dashboard.title, true) + }; + + const mainLayout = dashboard.configuration.states.default.layouts.main; + for (const id of Object.keys(dashboard.configuration.widgets)) { + const widget = dashboard.configuration.widgets[id]; + mainLayout.widgets[id] = { + sizeX: widget.sizeX, + sizeY: widget.sizeY, + row: widget.row, + col: widget.col + }; + if (isDefined(widget.config.mobileHeight)) { + mainLayout.widgets[id].mobileHeight = widget.config.mobileHeight; + } + if (isDefined(widget.config.mobileOrder)) { + mainLayout.widgets[id].mobileOrder = widget.config.mobileOrder; + } + } + } else { + const states = dashboard.configuration.states; + let rootFound = false; + for (const stateId of Object.keys(states)) { + const state = states[stateId]; + if (isUndefined(state.root)) { + state.root = false; + } else if (state.root) { + rootFound = true; + } + this.validateAndUpdateState(state); + } + if (!rootFound) { + const firstStateId = Object.keys(states)[0]; + states[firstStateId].root = true; + } + } + const datasourcesByAliasId: {[aliasId: string]: Array} = {}; + const targetDevicesByAliasId: {[aliasId: string]: Array>} = {}; + for (const widgetId of Object.keys(dashboard.configuration.widgets)) { + const widget = dashboard.configuration.widgets[widgetId]; + widget.config.datasources.forEach((datasource) => { + if (datasource.entityAliasId) { + const aliasId = datasource.entityAliasId; + let aliasDatasources = datasourcesByAliasId[aliasId]; + if (!aliasDatasources) { + aliasDatasources = []; + datasourcesByAliasId[aliasId] = aliasDatasources; + } + aliasDatasources.push(datasource); + } + }); + if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length) { + const aliasId = widget.config.targetDeviceAliasIds[0]; + let targetDeviceAliasIdsList = targetDevicesByAliasId[aliasId]; + if (!targetDeviceAliasIdsList) { + targetDeviceAliasIdsList = []; + targetDevicesByAliasId[aliasId] = targetDeviceAliasIdsList; + } + targetDeviceAliasIdsList.push(widget.config.targetDeviceAliasIds); + } + } + + dashboard.configuration = this.validateAndUpdateEntityAliases(dashboard.configuration, datasourcesByAliasId, targetDevicesByAliasId); + if (!dashboard.configuration.filters) { + dashboard.configuration.filters = {}; + } + + if (isUndefined(dashboard.configuration.timewindow)) { + dashboard.configuration.timewindow = this.timeService.defaultTimewindow(); + } + if (isUndefined(dashboard.configuration.settings)) { + dashboard.configuration.settings = {}; + dashboard.configuration.settings.stateControllerId = 'entity'; + dashboard.configuration.settings.showTitle = false; + dashboard.configuration.settings.showDashboardsSelect = true; + dashboard.configuration.settings.showEntitiesSelect = true; + dashboard.configuration.settings.showDashboardTimewindow = true; + dashboard.configuration.settings.showDashboardExport = true; + dashboard.configuration.settings.toolbarAlwaysOpen = true; + } else { + if (isUndefined(dashboard.configuration.settings.stateControllerId)) { + dashboard.configuration.settings.stateControllerId = 'entity'; + } + } + if (isDefined(dashboard.configuration.gridSettings)) { + const gridSettings = dashboard.configuration.gridSettings; + if (isDefined(gridSettings.showTitle)) { + dashboard.configuration.settings.showTitle = gridSettings.showTitle; + delete gridSettings.showTitle; + } + if (isDefined(gridSettings.titleColor)) { + dashboard.configuration.settings.titleColor = gridSettings.titleColor; + delete gridSettings.titleColor; + } + if (isDefined(gridSettings.showDevicesSelect)) { + dashboard.configuration.settings.showEntitiesSelect = gridSettings.showDevicesSelect; + delete gridSettings.showDevicesSelect; + } + if (isDefined(gridSettings.showEntitiesSelect)) { + dashboard.configuration.settings.showEntitiesSelect = gridSettings.showEntitiesSelect; + delete gridSettings.showEntitiesSelect; + } + if (isDefined(gridSettings.showDashboardTimewindow)) { + dashboard.configuration.settings.showDashboardTimewindow = gridSettings.showDashboardTimewindow; + delete gridSettings.showDashboardTimewindow; + } + if (isDefined(gridSettings.showDashboardExport)) { + dashboard.configuration.settings.showDashboardExport = gridSettings.showDashboardExport; + delete gridSettings.showDashboardExport; + } + dashboard.configuration.states.default.layouts.main.gridSettings = gridSettings; + delete dashboard.configuration.gridSettings; + } + return dashboard; + } + + public createSingleWidgetDashboard(widget: Widget): Dashboard { + if (!widget.id) { + widget.id = this.utils.guid(); + } + let dashboard: Dashboard = {}; + dashboard = this.validateAndUpdateDashboard(dashboard); + dashboard.configuration.widgets[widget.id] = widget; + dashboard.configuration.states.default.layouts.main.widgets[widget.id] = { + sizeX: widget.sizeX, + sizeY: widget.sizeY, + row: widget.row, + col: widget.col, + }; + return dashboard; + } + + public validateAndUpdateWidget(widget: Widget): Widget { + if (!widget.config) { + widget.config = {}; + } + if (!widget.config.datasources) { + widget.config.datasources = []; + } + widget.config.datasources.forEach((datasource) => { + if (datasource.type === 'device') { + datasource.type = DatasourceType.entity; + } + if (datasource.deviceAliasId) { + datasource.entityAliasId = datasource.deviceAliasId; + delete datasource.deviceAliasId; + } + }); + // Temp workaround + if (widget.isSystemType && widget.bundleAlias === 'charts' && widget.typeAlias === 'timeseries') { + widget.typeAlias = 'basic_timeseries'; + } + return widget; + } + + public createDefaultLayoutData(): DashboardLayout { + return { + widgets: {}, + gridSettings: this.createDefaultGridSettings() + }; + } + + private createDefaultGridSettings(): GridSettings { + return { + backgroundColor: '#eeeeee', + color: 'rgba(0,0,0,0.870588)', + columns: 24, + margin: 10, + backgroundSizeMode: '100%' + }; + } + + public createDefaultLayouts(): DashboardStateLayouts { + return { + main: this.createDefaultLayoutData() + }; + } + + public createDefaultState(name: string, root: boolean): DashboardState { + return { + name, + root, + layouts: this.createDefaultLayouts() + }; + } + + public createSingleEntityFilter(entityId: EntityId): EntityAliasFilter { + return { + type: AliasFilterType.singleEntity, + singleEntity: entityId, + resolveMultiple: false + }; + } + + private validateAndUpdateState(state: DashboardState) { + if (!state.layouts) { + state.layouts = this.createDefaultLayouts(); + } + for (const l of Object.keys(state.layouts)) { + const layout = state.layouts[l as DashboardLayoutId]; + this.validateAndUpdateLayout(layout); + } + } + + private validateAndUpdateLayout(layout: DashboardLayout) { + if (!layout.gridSettings) { + layout.gridSettings = this.createDefaultGridSettings(); + } + if (layout.gridSettings.margins && layout.gridSettings.margins.length === 2) { + layout.gridSettings.margin = layout.gridSettings.margins[0]; + delete layout.gridSettings.margins; + } + layout.gridSettings.margin = isDefined(layout.gridSettings.margin) ? layout.gridSettings.margin : 10; + } + + public setLayouts(dashboard: Dashboard, targetState: string, newLayouts: DashboardStateLayouts) { + const dashboardConfiguration = dashboard.configuration; + const states = dashboardConfiguration.states; + const state = states[targetState]; + let addedCount = 0; + let removedCount = 0; + for (const l of Object.keys(state.layouts)) { + if (!newLayouts[l]) { + removedCount++; + } + } + for (const l of Object.keys(newLayouts)) { + if (!state.layouts[l]) { + addedCount++; + } + } + state.layouts = newLayouts; + const layoutsCount = Object.keys(state.layouts).length; + let newColumns; + if (addedCount) { + for (const l of Object.keys(state.layouts)) { + newColumns = state.layouts[l].gridSettings.columns * (layoutsCount - addedCount) / layoutsCount; + if (newColumns > 0) { + state.layouts[l].gridSettings.columns = newColumns; + } + } + } + if (removedCount) { + for (const l of Object.keys(state.layouts)) { + newColumns = state.layouts[l].gridSettings.columns * (layoutsCount + removedCount) / layoutsCount; + if (newColumns > 0) { + state.layouts[l].gridSettings.columns = newColumns; + } + } + } + this.removeUnusedWidgets(dashboard); + } + + public getRootStateId(states: {[id: string]: DashboardState }): string { + for (const stateId of Object.keys(states)) { + const state = states[stateId]; + if (state.root) { + return stateId; + } + } + return Object.keys(states)[0]; + } + + public getStateLayoutsData(dashboard: Dashboard, targetState: string): DashboardLayoutsInfo { + const dashboardConfiguration = dashboard.configuration; + const states = dashboardConfiguration.states; + const state = states[targetState]; + if (state) { + const result: DashboardLayoutsInfo = {}; + for (const l of Object.keys(state.layouts)) { + const layout: DashboardLayout = state.layouts[l]; + if (layout) { + result[l] = { + widgetIds: [], + widgetLayouts: {}, + gridSettings: {} + } as DashboardLayoutInfo; + for (const id of Object.keys(layout.widgets)) { + result[l].widgetIds.push(id); + } + result[l].widgetLayouts = layout.widgets; + result[l].gridSettings = layout.gridSettings; + } + } + return result; + } else { + return null; + } + } + + public getWidgetsArray(dashboard: Dashboard): Array { + const widgetsArray: Array = []; + const dashboardConfiguration = dashboard.configuration; + const widgets = dashboardConfiguration.widgets; + for (const widgetId of Object.keys(widgets)) { + const widget = widgets[widgetId]; + widgetsArray.push(widget); + } + return widgetsArray; + } + + public addWidgetToLayout(dashboard: Dashboard, + targetState: string, + targetLayout: DashboardLayoutId, + widget: Widget, + originalColumns?: number, + originalSize?: {sizeX: number, sizeY: number}, + row?: number, + column?: number): void { + const dashboardConfiguration = dashboard.configuration; + const states = dashboardConfiguration.states; + const state = states[targetState]; + const layout = state.layouts[targetLayout]; + const layoutCount = Object.keys(state.layouts).length; + if (!widget.id) { + widget.id = this.utils.guid(); + } + if (!dashboardConfiguration.widgets[widget.id]) { + dashboardConfiguration.widgets[widget.id] = widget; + } + const widgetLayout: WidgetLayout = { + sizeX: originalSize ? originalSize.sizeX : widget.sizeX, + sizeY: originalSize ? originalSize.sizeY : widget.sizeY, + mobileOrder: widget.config.mobileOrder, + mobileHeight: widget.config.mobileHeight + }; + if (isUndefined(originalColumns)) { + originalColumns = 24; + } + const gridSettings = layout.gridSettings; + let columns = 24; + if (gridSettings && gridSettings.columns) { + columns = gridSettings.columns; + } + columns = columns * layoutCount; + if (columns !== originalColumns) { + const ratio = columns / originalColumns; + widgetLayout.sizeX *= ratio; + widgetLayout.sizeY *= ratio; + } + + if (row > -1 && column > - 1) { + widgetLayout.row = row; + widgetLayout.col = column; + } else { + row = 0; + for (const w of Object.keys(layout.widgets)) { + const existingLayout = layout.widgets[w]; + const wRow = existingLayout.row ? existingLayout.row : 0; + const wSizeY = existingLayout.sizeY ? existingLayout.sizeY : 1; + const bottom = wRow + wSizeY; + row = Math.max(row, bottom); + } + widgetLayout.row = row; + widgetLayout.col = 0; + } + layout.widgets[widget.id] = widgetLayout; + } + + public removeWidgetFromLayout(dashboard: Dashboard, + targetState: string, + targetLayout: DashboardLayoutId, + widgetId: string) { + const dashboardConfiguration = dashboard.configuration; + const states = dashboardConfiguration.states; + const state = states[targetState]; + const layout = state.layouts[targetLayout]; + delete layout.widgets[widgetId]; + this.removeUnusedWidgets(dashboard); + } + + public isSingleLayoutDashboard(dashboard: Dashboard): {state: string, layout: DashboardLayoutId} { + const dashboardConfiguration = dashboard.configuration; + const states = dashboardConfiguration.states; + const stateKeys = Object.keys(states); + if (stateKeys.length === 1) { + const state = states[stateKeys[0]]; + const layouts = state.layouts; + const layoutKeys = Object.keys(layouts); + if (layoutKeys.length === 1) { + return { + state: stateKeys[0], + layout: layoutKeys[0] as DashboardLayoutId + }; + } + } + return null; + } + + public updateLayoutSettings(layout: DashboardLayout, gridSettings: GridSettings) { + const prevGridSettings = layout.gridSettings; + let prevColumns = prevGridSettings ? prevGridSettings.columns : 24; + if (!prevColumns) { + prevColumns = 24; + } + const columns = gridSettings.columns || 24; + const ratio = columns / prevColumns; + layout.gridSettings = gridSettings; + let maxRow = 0; + for (const w of Object.keys(layout.widgets)) { + const widget = layout.widgets[w]; + if (!widget.sizeX) { + widget.sizeX = 1; + } + if (!widget.sizeY) { + widget.sizeY = 1; + } + maxRow = Math.max(maxRow, widget.row + widget.sizeY); + } + const newMaxRow = Math.round(maxRow * ratio); + for (const w of Object.keys(layout.widgets)) { + const widget = layout.widgets[w]; + if (widget.row + widget.sizeY === maxRow) { + widget.row = Math.round(widget.row * ratio); + widget.sizeY = newMaxRow - widget.row; + } else { + widget.row = Math.round(widget.row * ratio); + widget.sizeY = Math.round(widget.sizeY * ratio); + } + widget.sizeX = Math.round(widget.sizeX * ratio); + widget.col = Math.round(widget.col * ratio); + if (widget.col + widget.sizeX > columns) { + widget.sizeX = columns - widget.col; + } + } + } + + public removeUnusedWidgets(dashboard: Dashboard) { + const dashboardConfiguration = dashboard.configuration; + const states = dashboardConfiguration.states; + const widgets = dashboardConfiguration.widgets; + for (const widgetId of Object.keys(widgets)) { + let found = false; + for (const s of Object.keys(states)) { + const state = states[s]; + for (const l of Object.keys(state.layouts)) { + const layout = state.layouts[l]; + if (layout.widgets[widgetId]) { + found = true; + break; + } + } + } + if (!found) { + delete dashboardConfiguration.widgets[widgetId]; + } + } + } + + private validateAndUpdateEntityAliases(configuration: DashboardConfiguration, + datasourcesByAliasId: {[aliasId: string]: Array}, + targetDevicesByAliasId: {[aliasId: string]: Array>}): DashboardConfiguration { + let entityAlias: EntityAlias; + if (isUndefined(configuration.entityAliases)) { + configuration.entityAliases = {}; + if (configuration.deviceAliases) { + const deviceAliases = configuration.deviceAliases; + for (const aliasId of Object.keys(deviceAliases)) { + const deviceAlias = deviceAliases[aliasId]; + entityAlias = this.validateAndUpdateDeviceAlias(aliasId, deviceAlias, datasourcesByAliasId, targetDevicesByAliasId); + configuration.entityAliases[entityAlias.id] = entityAlias; + } + delete configuration.deviceAliases; + } + } else { + const entityAliases = configuration.entityAliases; + for (const aliasId of Object.keys(entityAliases)) { + entityAlias = entityAliases[aliasId]; + entityAlias = this.validateAndUpdateEntityAlias(aliasId, entityAlias, datasourcesByAliasId, targetDevicesByAliasId); + if (aliasId !== entityAlias.id) { + delete entityAliases[aliasId]; + } + entityAliases[entityAlias.id] = entityAlias; + } + } + return configuration; + } + + private validateAndUpdateDeviceAlias(aliasId: string, + deviceAlias: any, + datasourcesByAliasId: {[aliasId: string]: Array}, + targetDevicesByAliasId: {[aliasId: string]: Array>}): EntityAlias { + aliasId = this.validateAliasId(aliasId, datasourcesByAliasId, targetDevicesByAliasId); + const alias = deviceAlias.alias; + const entityAlias: EntityAlias = { + id: aliasId, + alias, + filter: { + type: null, + entityType: EntityType.DEVICE, + resolveMultiple: false + }, + }; + if (deviceAlias.deviceFilter) { + entityAlias.filter.type = + deviceAlias.deviceFilter.useFilter ? AliasFilterType.entityName : AliasFilterType.entityList; + if (entityAlias.filter.type === AliasFilterType.entityList) { + entityAlias.filter.entityList = deviceAlias.deviceFilter.deviceList; + } else { + entityAlias.filter.entityNameFilter = deviceAlias.deviceFilter.deviceNameFilter; + } + } else { + entityAlias.filter.type = AliasFilterType.entityList; + entityAlias.filter.entityList = [deviceAlias.deviceId]; + } + return entityAlias; + } + + private validateAndUpdateEntityAlias(aliasId: string, entityAlias: EntityAlias, + datasourcesByAliasId: {[aliasId: string]: Array}, + targetDevicesByAliasId: {[aliasId: string]: Array>}): EntityAlias { + entityAlias.id = this.validateAliasId(aliasId, datasourcesByAliasId, targetDevicesByAliasId); + if (!entityAlias.filter) { + entityAlias.filter = { + type: entityAlias.entityFilter.useFilter ? AliasFilterType.entityName : AliasFilterType.entityList, + entityType: entityAlias.entityType, + resolveMultiple: false + }; + if (entityAlias.filter.type === AliasFilterType.entityList) { + entityAlias.filter.entityList = entityAlias.entityFilter.entityList; + } else { + entityAlias.filter.entityNameFilter = entityAlias.entityFilter.entityNameFilter; + } + delete entityAlias.entityType; + delete entityAlias.entityFilter; + } + return entityAlias; + } + + private validateAliasId(aliasId: string, + datasourcesByAliasId: {[aliasId: string]: Array}, + targetDevicesByAliasId: {[aliasId: string]: Array>}): string { + if (!aliasId || !isString(aliasId) || aliasId.length !== 36) { + const newAliasId = this.utils.guid(); + const aliasDatasources = datasourcesByAliasId[aliasId]; + if (aliasDatasources) { + aliasDatasources.forEach( + (datasource) => { + datasource.entityAliasId = newAliasId; + } + ); + } + const targetDeviceAliasIdsList = targetDevicesByAliasId[aliasId]; + if (targetDeviceAliasIdsList) { + targetDeviceAliasIdsList.forEach( + (targetDeviceAliasIds) => { + targetDeviceAliasIds[0] = newAliasId; + } + ); + } + return newAliasId; + } else { + return aliasId; + } + } + +} diff --git a/ui-ngx/src/app/core/services/dialog.service.ts b/ui-ngx/src/app/core/services/dialog.service.ts new file mode 100644 index 0000000000..bca75da362 --- /dev/null +++ b/ui-ngx/src/app/core/services/dialog.service.ts @@ -0,0 +1,136 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; +import { TranslateService } from '@ngx-translate/core'; +import { AuthService } from '@core/auth/auth.service'; +import { + ColorPickerDialogComponent, + ColorPickerDialogData +} from '@shared/components/dialog/color-picker-dialog.component'; +import { + MaterialIconsDialogComponent, + MaterialIconsDialogData +} from '@shared/components/dialog/material-icons-dialog.component'; +import { ConfirmDialogComponent } from '@shared/components/dialog/confirm-dialog.component'; +import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component'; +import { TodoDialogComponent } from '@shared/components/dialog/todo-dialog.component'; + +@Injectable( + { + providedIn: 'root' + } +) +export class DialogService { + + constructor( + private translate: TranslateService, + private authService: AuthService, + public dialog: MatDialog + ) { + } + + confirm(title: string, message: string, cancel: string = null, ok: string = null, fullscreen: boolean = false): Observable { + const dialogConfig: MatDialogConfig = { + disableClose: true, + data: { + title, + message, + cancel: cancel || this.translate.instant('action.cancel'), + ok: ok || this.translate.instant('action.ok') + } + }; + if (fullscreen) { + dialogConfig.panelClass = ['tb-fullscreen-dialog']; + } + const dialogRef = this.dialog.open(ConfirmDialogComponent, dialogConfig); + return dialogRef.afterClosed(); + } + + alert(title: string, message: string, ok: string = null, fullscreen: boolean = false): Observable { + const dialogConfig: MatDialogConfig = { + disableClose: true, + data: { + title, + message, + ok: ok || this.translate.instant('action.ok') + } + }; + if (fullscreen) { + dialogConfig.panelClass = ['tb-fullscreen-dialog']; + } + const dialogRef = this.dialog.open(AlertDialogComponent, dialogConfig); + return dialogRef.afterClosed(); + } + + colorPicker(color: string): Observable { + return this.dialog.open(ColorPickerDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + color + } + }).afterClosed(); + } + + materialIconPicker(icon: string): Observable { + return this.dialog.open(MaterialIconsDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + icon + } + }).afterClosed(); + } + + private permissionDenied() { + this.alert( + this.translate.instant('access.permission-denied'), + this.translate.instant('access.permission-denied-text'), + this.translate.instant('action.close') + ); + } + + forbidden(): Observable { + const observable = this.confirm( + this.translate.instant('access.access-forbidden'), + this.translate.instant('access.access-forbidden-text'), + this.translate.instant('action.cancel'), + this.translate.instant('action.sign-in'), + true + ); + observable.subscribe((res) => { + if (res) { + this.authService.logout(); + } + }); + return observable; + } + + todo(): Observable { + const dialogConfig: MatDialogConfig = { + disableClose: true, + panelClass: ['tb-fullscreen-dialog'] + }; + const dialogRef = this.dialog.open(TodoDialogComponent, dialogConfig); + return dialogRef.afterClosed(); + } + +} diff --git a/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts b/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts new file mode 100644 index 0000000000..53a72ad2cc --- /dev/null +++ b/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts @@ -0,0 +1,112 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + Compiler, + Component, + ComponentFactory, + Injectable, + Injector, + NgModule, + NgModuleRef, + OnDestroy, + Type +} from '@angular/core'; +import { Observable, ReplaySubject } from 'rxjs'; +import { CommonModule } from '@angular/common'; + +@NgModule() +export abstract class DynamicComponentModule implements OnDestroy { + + ngOnDestroy(): void { + } + +} + +interface DynamicComponentModuleData { + moduleRef: NgModuleRef; + moduleType: Type; +} + +@Injectable( + { + providedIn: 'root' + } +) +export class DynamicComponentFactoryService { + + private dynamicComponentModulesMap = new Map, DynamicComponentModuleData>(); + + constructor(private compiler: Compiler, + private injector: Injector) { + } + + public createDynamicComponentFactory( + componentType: Type, + template: string, + modules?: Type[]): Observable> { + const dymamicComponentFactorySubject = new ReplaySubject>(); + const comp = this.createDynamicComponent(componentType, template); + let moduleImports: Type[] = [CommonModule]; + if (modules) { + moduleImports = [...moduleImports, ...modules]; + } + // noinspection AngularInvalidImportedOrDeclaredSymbol + @NgModule({ + declarations: [comp], + imports: moduleImports + }) + class DynamicComponentInstanceModule extends DynamicComponentModule {} + try { + this.compiler.compileModuleAsync(DynamicComponentInstanceModule).then( + (module) => { + const moduleRef = module.create(this.injector); + const factory = moduleRef.componentFactoryResolver.resolveComponentFactory(comp); + this.dynamicComponentModulesMap.set(factory, { + moduleRef, + moduleType: module.moduleType + }); + dymamicComponentFactorySubject.next(factory); + dymamicComponentFactorySubject.complete(); + } + ).catch( + (e) => { + dymamicComponentFactorySubject.error(e); + } + ); + } catch (e) { + dymamicComponentFactorySubject.error(e); + } + return dymamicComponentFactorySubject.asObservable(); + } + + public destroyDynamicComponentFactory(factory: ComponentFactory) { + const moduleData = this.dynamicComponentModulesMap.get(factory); + if (moduleData) { + moduleData.moduleRef.destroy(); + this.compiler.clearCacheFor(moduleData.moduleType); + this.dynamicComponentModulesMap.delete(factory); + } + } + + private createDynamicComponent(componentType: Type, template: string): Type { + // noinspection AngularMissingOrInvalidDeclarationInModule + return Component({ + template + })(componentType); + } + +} diff --git a/ui-ngx/src/app/core/services/item-buffer.service.ts b/ui-ngx/src/app/core/services/item-buffer.service.ts new file mode 100644 index 0000000000..d779a7a9b6 --- /dev/null +++ b/ui-ngx/src/app/core/services/item-buffer.service.ts @@ -0,0 +1,572 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { Dashboard, DashboardLayoutId } from '@app/shared/models/dashboard.models'; +import { AliasesInfo, EntityAlias, EntityAliases, EntityAliasInfo } from '@shared/models/alias.models'; +import { DatasourceType, Widget, WidgetPosition, WidgetSize } from '@shared/models/widget.models'; +import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; +import { deepClone, isEqual } from '@core/utils'; +import { UtilsService } from '@core/services/utils.service'; +import { Observable, of, throwError } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { FcRuleNode, ruleNodeTypeDescriptors } from '@shared/models/rule-node.models'; +import { RuleChainService } from '@core/http/rule-chain.service'; +import { RuleChainImport } from '@shared/models/rule-chain.models'; +import { Filter, FilterInfo, Filters, FiltersInfo } from '@shared/models/query/query.models'; + +const WIDGET_ITEM = 'widget_item'; +const WIDGET_REFERENCE = 'widget_reference'; +const RULE_NODES = 'rule_nodes'; +const RULE_CHAIN_IMPORT = 'rule_chain_import'; + +export interface WidgetItem { + widget: Widget; + aliasesInfo: AliasesInfo; + filtersInfo: FiltersInfo; + originalSize: WidgetSize; + originalColumns: number; +} + +export interface WidgetReference { + dashboardId: string; + sourceState: string; + sourceLayout: DashboardLayoutId; + widgetId: string; + originalSize: WidgetSize; + originalColumns: number; +} + +export interface RuleNodeConnection { + isInputSource: boolean; + fromIndex: number; + toIndex: number; + label: string; + labels: string[]; +} + +export interface RuleNodesReference { + nodes: FcRuleNode[]; + connections: RuleNodeConnection[]; + originX?: number; + originY?: number; +} + +@Injectable({ + providedIn: 'root' +}) +export class ItemBufferService { + + private namespace = 'tbBufferStore'; + private delimiter = '.'; + + constructor(private dashboardUtils: DashboardUtilsService, + private ruleChainService: RuleChainService, + private utils: UtilsService) {} + + public prepareWidgetItem(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): WidgetItem { + const aliasesInfo: AliasesInfo = { + datasourceAliases: {}, + targetDeviceAliases: {} + }; + const filtersInfo: FiltersInfo = { + datasourceFilters: {} + }; + const originalColumns = this.getOriginalColumns(dashboard, sourceState, sourceLayout); + const originalSize = this.getOriginalSize(dashboard, sourceState, sourceLayout, widget); + if (widget.config && dashboard.configuration + && dashboard.configuration.entityAliases) { + let entityAlias: EntityAlias; + if (widget.config.datasources) { + for (let i = 0; i < widget.config.datasources.length; i++) { + const datasource = widget.config.datasources[i]; + if (datasource.type === DatasourceType.entity && datasource.entityAliasId) { + entityAlias = dashboard.configuration.entityAliases[datasource.entityAliasId]; + if (entityAlias) { + aliasesInfo.datasourceAliases[i] = this.prepareAliasInfo(entityAlias); + } + } + } + } + if (widget.config.targetDeviceAliasIds) { + for (let i = 0; i < widget.config.targetDeviceAliasIds.length; i++) { + const targetDeviceAliasId = widget.config.targetDeviceAliasIds[i]; + if (targetDeviceAliasId) { + entityAlias = dashboard.configuration.entityAliases[targetDeviceAliasId]; + if (entityAlias) { + aliasesInfo.targetDeviceAliases[i] = this.prepareAliasInfo(entityAlias); + } + } + } + } + } + if (widget.config && dashboard.configuration + && dashboard.configuration.filters) { + let filter: Filter; + if (widget.config.datasources) { + for (let i = 0; i < widget.config.datasources.length; i++) { + const datasource = widget.config.datasources[i]; + if (datasource.type === DatasourceType.entity && datasource.filterId) { + filter = dashboard.configuration.filters[datasource.filterId]; + if (filter) { + filtersInfo.datasourceFilters[i] = this.prepareFilterInfo(filter); + } + } + } + } + } + return { + widget, + aliasesInfo, + filtersInfo, + originalSize, + originalColumns + }; + } + + public copyWidget(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): void { + const widgetItem = this.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget); + this.storeSet(WIDGET_ITEM, widgetItem); + } + + public copyWidgetReference(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): void { + const widgetReference = this.prepareWidgetReference(dashboard, sourceState, sourceLayout, widget); + this.storeSet(WIDGET_REFERENCE, widgetReference); + } + + public hasWidget(): boolean { + return this.storeHas(WIDGET_ITEM); + } + + public canPasteWidgetReference(dashboard: Dashboard, state: string, layout: DashboardLayoutId): boolean { + const widgetReference: WidgetReference = this.storeGet(WIDGET_REFERENCE); + if (widgetReference) { + if (widgetReference.dashboardId === dashboard.id.id) { + if ((widgetReference.sourceState !== state || widgetReference.sourceLayout !== layout) + && dashboard.configuration.widgets[widgetReference.widgetId]) { + return true; + } + } + } + return false; + } + + public pasteWidget(targetDashboard: Dashboard, targetState: string, + targetLayout: DashboardLayoutId, position: WidgetPosition, + onAliasesUpdateFunction: () => void, + onFiltersUpdateFunction: () => void): Observable { + const widgetItem: WidgetItem = this.storeGet(WIDGET_ITEM); + if (widgetItem) { + const widget = widgetItem.widget; + const aliasesInfo = widgetItem.aliasesInfo; + const filtersInfo = widgetItem.filtersInfo; + const originalColumns = widgetItem.originalColumns; + const originalSize = widgetItem.originalSize; + let targetRow = -1; + let targetColumn = -1; + if (position) { + targetRow = position.row; + targetColumn = position.column; + } + widget.id = this.utils.guid(); + return this.addWidgetToDashboard(targetDashboard, targetState, + targetLayout, widget, aliasesInfo, filtersInfo, + onAliasesUpdateFunction, onFiltersUpdateFunction, + originalColumns, originalSize, targetRow, targetColumn).pipe( + map(() => widget) + ); + } else { + return throwError('Failed to read widget from buffer!'); + } + } + + public pasteWidgetReference(targetDashboard: Dashboard, targetState: string, + targetLayout: DashboardLayoutId, position: WidgetPosition): Observable { + const widgetReference: WidgetReference = this.storeGet(WIDGET_REFERENCE); + if (widgetReference) { + const widget = targetDashboard.configuration.widgets[widgetReference.widgetId]; + if (widget) { + const originalColumns = widgetReference.originalColumns; + const originalSize = widgetReference.originalSize; + let targetRow = -1; + let targetColumn = -1; + if (position) { + targetRow = position.row; + targetColumn = position.column; + } + return this.addWidgetToDashboard(targetDashboard, targetState, + targetLayout, widget, null, + null, null, null, originalColumns, + originalSize, targetRow, targetColumn).pipe( + map(() => widget) + ); + } else { + return throwError('Failed to read widget reference from buffer!'); + } + } else { + return throwError('Failed to read widget reference from buffer!'); + } + } + + public addWidgetToDashboard(dashboard: Dashboard, targetState: string, + targetLayout: DashboardLayoutId, widget: Widget, + aliasesInfo: AliasesInfo, + filtersInfo: FiltersInfo, + onAliasesUpdateFunction: () => void, + onFiltersUpdateFunction: () => void, + originalColumns: number, + originalSize: WidgetSize, + row: number, + column: number): Observable { + let theDashboard: Dashboard; + if (dashboard) { + theDashboard = dashboard; + } else { + theDashboard = {}; + } + theDashboard = this.dashboardUtils.validateAndUpdateDashboard(theDashboard); + let callAliasUpdateFunction = false; + let callFilterUpdateFunction = false; + if (aliasesInfo) { + const newEntityAliases = this.updateAliases(theDashboard, widget, aliasesInfo); + const aliasesUpdated = !isEqual(newEntityAliases, theDashboard.configuration.entityAliases); + if (aliasesUpdated) { + theDashboard.configuration.entityAliases = newEntityAliases; + if (onAliasesUpdateFunction) { + callAliasUpdateFunction = true; + } + } + } + if (filtersInfo) { + const newFilters = this.updateFilters(theDashboard, widget, filtersInfo); + const filtersUpdated = !isEqual(newFilters, theDashboard.configuration.filters); + if (filtersUpdated) { + theDashboard.configuration.filters = newFilters; + if (onFiltersUpdateFunction) { + callFilterUpdateFunction = true; + } + } + } + this.dashboardUtils.addWidgetToLayout(theDashboard, targetState, targetLayout, widget, + originalColumns, originalSize, row, column); + if (callAliasUpdateFunction) { + onAliasesUpdateFunction(); + } + if (callFilterUpdateFunction) { + onFiltersUpdateFunction(); + } + return of(theDashboard); + } + + public copyRuleNodes(nodes: FcRuleNode[], connections: RuleNodeConnection[]) { + const ruleNodes: RuleNodesReference = { + nodes: [], + connections: [] + }; + let top = -1; + let left = -1; + let bottom = -1; + let right = -1; + for (let i = 0; i < nodes.length; i++) { + const origNode = nodes[i]; + const node: FcRuleNode = { + id: '', + connectors: [], + additionalInfo: origNode.additionalInfo, + configuration: origNode.configuration, + debugMode: origNode.debugMode, + x: origNode.x, + y: origNode.y, + name: origNode.name, + componentClazz: origNode.component.clazz, + }; + if (origNode.targetRuleChainId) { + node.targetRuleChainId = origNode.targetRuleChainId; + } + if (origNode.error) { + node.error = origNode.error; + } + ruleNodes.nodes.push(node); + if (i === 0) { + top = node.y; + left = node.x; + bottom = node.y + 50; + right = node.x + 170; + } else { + top = Math.min(top, node.y); + left = Math.min(left, node.x); + bottom = Math.max(bottom, node.y + 50); + right = Math.max(right, node.x + 170); + } + } + ruleNodes.originX = left + (right - left) / 2; + ruleNodes.originY = top + (bottom - top) / 2; + connections.forEach(connection => { + ruleNodes.connections.push(connection); + }); + this.storeSet(RULE_NODES, ruleNodes); + } + + public hasRuleNodes(): boolean { + return this.storeHas(RULE_NODES); + } + + public pasteRuleNodes(x: number, y: number): RuleNodesReference { + const ruleNodes: RuleNodesReference = this.storeGet(RULE_NODES); + if (ruleNodes) { + const deltaX = x - ruleNodes.originX; + const deltaY = y - ruleNodes.originY; + for (const node of ruleNodes.nodes) { + const component = this.ruleChainService.getRuleNodeComponentByClazz(node.componentClazz); + if (component) { + let icon = ruleNodeTypeDescriptors.get(component.type).icon; + let iconUrl: string = null; + if (component.configurationDescriptor.nodeDefinition.icon) { + icon = component.configurationDescriptor.nodeDefinition.icon; + } + if (component.configurationDescriptor.nodeDefinition.iconUrl) { + iconUrl = component.configurationDescriptor.nodeDefinition.iconUrl; + } + delete node.componentClazz; + node.component = component; + node.nodeClass = ruleNodeTypeDescriptors.get(component.type).nodeClass; + node.icon = icon; + node.iconUrl = iconUrl; + node.connectors = []; + node.x = Math.round(node.x + deltaX); + node.y = Math.round(node.y + deltaY); + } else { + return null; + } + } + return ruleNodes; + } + return null; + } + + public hasRuleChainImport(): boolean { + return this.storeHas(RULE_CHAIN_IMPORT); + } + + public storeRuleChainImport(ruleChainImport: RuleChainImport): void { + this.storeSet(RULE_CHAIN_IMPORT, ruleChainImport); + } + + public getRuleChainImport(): RuleChainImport { + const ruleChainImport: RuleChainImport = this.storeGet(RULE_CHAIN_IMPORT); + this.storeRemove(RULE_CHAIN_IMPORT); + return ruleChainImport; + } + + private getOriginalColumns(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId): number { + let originalColumns = 24; + let gridSettings = null; + const state = dashboard.configuration.states[sourceState]; + const layoutCount = Object.keys(state.layouts).length; + if (state) { + const layout = state.layouts[sourceLayout]; + if (layout) { + gridSettings = layout.gridSettings; + + } + } + if (gridSettings && + gridSettings.columns) { + originalColumns = gridSettings.columns; + } + originalColumns = originalColumns * layoutCount; + return originalColumns; + } + + private getOriginalSize(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): WidgetSize { + const layout = dashboard.configuration.states[sourceState].layouts[sourceLayout]; + const widgetLayout = layout.widgets[widget.id]; + return { + sizeX: widgetLayout.sizeX, + sizeY: widgetLayout.sizeY + }; + } + + private prepareAliasInfo(entityAlias: EntityAlias): EntityAliasInfo { + return { + alias: entityAlias.alias, + filter: entityAlias.filter + }; + } + + private prepareFilterInfo(filter: Filter): FilterInfo { + return { + filter: filter.filter, + keyFilters: filter.keyFilters, + editable: filter.editable + }; + } + + private prepareWidgetReference(dashboard: Dashboard, sourceState: string, + sourceLayout: DashboardLayoutId, widget: Widget): WidgetReference { + const originalColumns = this.getOriginalColumns(dashboard, sourceState, sourceLayout); + const originalSize = this.getOriginalSize(dashboard, sourceState, sourceLayout, widget); + return { + dashboardId: dashboard.id.id, + sourceState, + sourceLayout, + widgetId: widget.id, + originalSize, + originalColumns + }; + } + + private updateAliases(dashboard: Dashboard, widget: Widget, aliasesInfo: AliasesInfo): EntityAliases { + const entityAliases = deepClone(dashboard.configuration.entityAliases); + let aliasInfo: EntityAliasInfo; + let newAliasId: string; + for (const datasourceIndexStr of Object.keys(aliasesInfo.datasourceAliases)) { + const datasourceIndex = Number(datasourceIndexStr); + aliasInfo = aliasesInfo.datasourceAliases[datasourceIndex]; + newAliasId = this.getEntityAliasId(entityAliases, aliasInfo); + widget.config.datasources[datasourceIndex].entityAliasId = newAliasId; + } + for (const targetDeviceAliasIndexStr of Object.keys(aliasesInfo.targetDeviceAliases)) { + const targetDeviceAliasIndex = Number(targetDeviceAliasIndexStr); + aliasInfo = aliasesInfo.targetDeviceAliases[targetDeviceAliasIndex]; + newAliasId = this.getEntityAliasId(entityAliases, aliasInfo); + widget.config.targetDeviceAliasIds[targetDeviceAliasIndex] = newAliasId; + } + return entityAliases; + } + + private updateFilters(dashboard: Dashboard, widget: Widget, filtersInfo: FiltersInfo): Filters { + const filters = deepClone(dashboard.configuration.filters); + let filterInfo: FilterInfo; + let newFilterId: string; + for (const datasourceIndexStr of Object.keys(filtersInfo.datasourceFilters)) { + const datasourceIndex = Number(datasourceIndexStr); + filterInfo = filtersInfo.datasourceFilters[datasourceIndex]; + newFilterId = this.getFilterId(filters, filterInfo); + widget.config.datasources[datasourceIndex].filterId = newFilterId; + } + return filters; + } + + private isEntityAliasEqual(alias1: EntityAliasInfo, alias2: EntityAliasInfo): boolean { + return isEqual(alias1.filter, alias2.filter); + } + + private getEntityAliasId(entityAliases: EntityAliases, aliasInfo: EntityAliasInfo): string { + let newAliasId: string; + for (const aliasId of Object.keys(entityAliases)) { + if (this.isEntityAliasEqual(entityAliases[aliasId], aliasInfo)) { + newAliasId = aliasId; + break; + } + } + if (!newAliasId) { + const newAliasName = this.createEntityAliasName(entityAliases, aliasInfo.alias); + newAliasId = this.utils.guid(); + entityAliases[newAliasId] = {id: newAliasId, alias: newAliasName, filter: aliasInfo.filter}; + } + return newAliasId; + } + + private createEntityAliasName(entityAliases: EntityAliases, alias: string): string { + let c = 0; + let newAlias = alias; + let unique = false; + while (!unique) { + unique = true; + for (const entAliasId of Object.keys(entityAliases)) { + const entAlias = entityAliases[entAliasId]; + if (newAlias === entAlias.alias) { + c++; + newAlias = alias + c; + unique = false; + } + } + } + return newAlias; + } + + private isFilterEqual(filter1: FilterInfo, filter2: FilterInfo): boolean { + return isEqual(filter1.keyFilters, filter2.keyFilters); + } + + private getFilterId(filters: Filters, filterInfo: FilterInfo): string { + let newFilterId: string; + for (const filterId of Object.keys(filters)) { + if (this.isFilterEqual(filters[filterId], filterInfo)) { + newFilterId = filterId; + break; + } + } + if (!newFilterId) { + const newFilterName = this.createFilterName(filters, filterInfo.filter); + newFilterId = this.utils.guid(); + filters[newFilterId] = {id: newFilterId, filter: newFilterName, + keyFilters: filterInfo.keyFilters, editable: filterInfo.editable}; + } + return newFilterId; + } + + private createFilterName(filters: Filters, filter: string): string { + let c = 0; + let newFilter = filter; + let unique = false; + while (!unique) { + unique = true; + for (const entFilterId of Object.keys(filters)) { + const entFilter = filters[entFilterId]; + if (newFilter === entFilter.filter) { + c++; + newFilter = filter + c; + unique = false; + } + } + } + return newFilter; + } + + private storeSet(key: string, elem: any) { + localStorage.setItem(this.getNamespacedKey(key), JSON.stringify(elem)); + } + + private storeGet(key: string): any { + let obj = null; + const saved = localStorage.getItem(this.getNamespacedKey(key)); + try { + if (typeof saved === 'undefined' || saved === 'undefined') { + obj = undefined; + } else { + obj = JSON.parse(saved); + } + } catch (e) { + this.storeRemove(key); + } + return obj; + } + + private storeHas(key: string): boolean { + const saved = localStorage.getItem(this.getNamespacedKey(key)); + return typeof saved !== 'undefined' && saved !== 'undefined' && saved !== null; + } + + private storeRemove(key: string) { + localStorage.removeItem(this.getNamespacedKey(key)); + } + + private getNamespacedKey(key: string): string { + return [this.namespace, key].join(this.delimiter); + } +} diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts new file mode 100644 index 0000000000..028a0f5380 --- /dev/null +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -0,0 +1,41 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { HasUUID } from '@shared/models/id/has-uuid'; + +export declare type MenuSectionType = 'link' | 'toggle'; + +export interface MenuSection extends HasUUID{ + name: string; + type: MenuSectionType; + path: string; + icon: string; + isMdiIcon?: boolean; + height?: string; + pages?: Array; +} + +export interface HomeSection { + name: string; + places: Array; +} + +export interface HomeSectionPlace { + name: string; + icon: string; + isMdiIcon?: boolean; + path: string; +} diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts new file mode 100644 index 0000000000..4e98f375c2 --- /dev/null +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -0,0 +1,469 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { AuthService } from '../auth/auth.service'; +import { select, Store } from '@ngrx/store'; +import { AppState } from '../core.state'; +import { selectAuthUser, selectIsAuthenticated } from '../auth/auth.selectors'; +import { take } from 'rxjs/operators'; +import { HomeSection, MenuSection } from '@core/services/menu.models'; +import { BehaviorSubject, Observable, Subject } from 'rxjs'; +import { Authority } from '@shared/models/authority.enum'; +import { AuthUser } from '@shared/models/user.model'; +import { guid } from '@core/utils'; + +@Injectable({ + providedIn: 'root' +}) +export class MenuService { + + menuSections$: Subject> = new BehaviorSubject>([]); + homeSections$: Subject> = new BehaviorSubject>([]); + + constructor(private store: Store, private authService: AuthService) { + this.store.pipe(select(selectIsAuthenticated)).subscribe( + (authenticated: boolean) => { + if (authenticated) { + this.buildMenu(); + } + } + ); + } + + private buildMenu() { + this.store.pipe(select(selectAuthUser), take(1)).subscribe( + (authUser: AuthUser) => { + if (authUser) { + let menuSections: Array; + let homeSections: Array; + switch (authUser.authority) { + case Authority.SYS_ADMIN: + menuSections = this.buildSysAdminMenu(authUser); + homeSections = this.buildSysAdminHome(authUser); + break; + case Authority.TENANT_ADMIN: + menuSections = this.buildTenantAdminMenu(authUser); + homeSections = this.buildTenantAdminHome(authUser); + break; + case Authority.CUSTOMER_USER: + menuSections = this.buildCustomerUserMenu(authUser); + homeSections = this.buildCustomerUserHome(authUser); + break; + } + this.menuSections$.next(menuSections); + this.homeSections$.next(homeSections); + } + } + ); + } + + private buildSysAdminMenu(authUser: any): Array { + const sections: Array = []; + sections.push( + { + id: guid(), + name: 'home.home', + type: 'link', + path: '/home', + icon: 'home' + }, + { + id: guid(), + name: 'tenant.tenants', + type: 'link', + path: '/tenants', + icon: 'supervisor_account' + }, + { + id: guid(), + name: 'tenant-profile.tenant-profiles', + type: 'link', + path: '/tenantProfiles', + icon: 'mdi:alpha-t-box', + isMdiIcon: true + }, + { + id: guid(), + name: 'widget.widget-library', + type: 'link', + path: '/widgets-bundles', + icon: 'now_widgets' + }, + { + id: guid(), + name: 'admin.system-settings', + type: 'toggle', + path: '/settings', + height: '160px', + icon: 'settings', + pages: [ + { + id: guid(), + name: 'admin.general', + type: 'link', + path: '/settings/general', + icon: 'settings_applications' + }, + { + id: guid(), + name: 'admin.outgoing-mail', + type: 'link', + path: '/settings/outgoing-mail', + icon: 'mail' + }, + { + id: guid(), + name: 'admin.security-settings', + type: 'link', + path: '/settings/security-settings', + icon: 'security' + }, + { + id: guid(), + name: 'admin.oauth2.oauth2', + type: 'link', + path: '/settings/oauth2', + icon: 'security' + } + ] + } + ); + return sections; + } + + private buildSysAdminHome(authUser: any): Array { + const homeSections: Array = []; + homeSections.push( + { + name: 'tenant.management', + places: [ + { + name: 'tenant.tenants', + icon: 'supervisor_account', + path: '/tenants' + }, + { + name: 'tenant-profile.tenant-profiles', + icon: 'mdi:alpha-t-box', + isMdiIcon: true, + path: '/tenantProfiles' + }, + ] + }, + { + name: 'widget.management', + places: [ + { + name: 'widget.widget-library', + icon: 'now_widgets', + path: '/widgets-bundles' + } + ] + }, + { + name: 'admin.system-settings', + places: [ + { + name: 'admin.general', + icon: 'settings_applications', + path: '/settings/general' + }, + { + name: 'admin.outgoing-mail', + icon: 'mail', + path: '/settings/outgoing-mail' + }, + { + name: 'admin.security-settings', + icon: 'security', + path: '/settings/security-settings' + }, + { + name: 'admin.oauth2.oauth2', + icon: 'security', + path: '/settings/oauth2' + } + ] + } + ); + return homeSections; + } + + private buildTenantAdminMenu(authUser: any): Array { + const sections: Array = []; + sections.push( + { + id: guid(), + name: 'home.home', + type: 'link', + path: '/home', + icon: 'home' + }, + { + id: guid(), + name: 'rulechain.rulechains', + type: 'link', + path: '/ruleChains', + icon: 'settings_ethernet' + }, + { + id: guid(), + name: 'customer.customers', + type: 'link', + path: '/customers', + icon: 'supervisor_account' + }, + { + id: guid(), + name: 'asset.assets', + type: 'link', + path: '/assets', + icon: 'domain' + }, + { + id: guid(), + name: 'device.devices', + type: 'link', + path: '/devices', + icon: 'devices_other' + }, + { + id: guid(), + name: 'device-profile.device-profiles', + type: 'link', + path: '/deviceProfiles', + icon: 'mdi:alpha-d-box', + isMdiIcon: true + }, + { + id: guid(), + name: 'entity-view.entity-views', + type: 'link', + path: '/entityViews', + icon: 'view_quilt' + }, + { + id: guid(), + name: 'widget.widget-library', + type: 'link', + path: '/widgets-bundles', + icon: 'now_widgets' + }, + { + id: guid(), + name: 'dashboard.dashboards', + type: 'link', + path: '/dashboards', + icon: 'dashboards' + }, + { + id: guid(), + name: 'audit-log.audit-logs', + type: 'link', + path: '/auditLogs', + icon: 'track_changes' + } + ); + return sections; + } + + private buildTenantAdminHome(authUser: any): Array { + const homeSections: Array = []; + homeSections.push( + { + name: 'rulechain.management', + places: [ + { + name: 'rulechain.rulechains', + icon: 'settings_ethernet', + path: '/ruleChains' + } + ] + }, + { + name: 'customer.management', + places: [ + { + name: 'customer.customers', + icon: 'supervisor_account', + path: '/customers' + } + ] + }, + { + name: 'asset.management', + places: [ + { + name: 'asset.assets', + icon: 'domain', + path: '/assets' + } + ] + }, + { + name: 'device.management', + places: [ + { + name: 'device.devices', + icon: 'devices_other', + path: '/devices' + }, + { + name: 'device-profile.device-profiles', + icon: 'mdi:alpha-d-box', + isMdiIcon: true, + path: '/deviceProfiles' + } + ] + }, + { + name: 'entity-view.management', + places: [ + { + name: 'entity-view.entity-views', + icon: 'view_quilt', + path: '/entityViews' + } + ] + }, + { + name: 'dashboard.management', + places: [ + { + name: 'widget.widget-library', + icon: 'now_widgets', + path: '/widgets-bundles' + }, + { + name: 'dashboard.dashboards', + icon: 'dashboard', + path: '/dashboards' + } + ] + }, + { + name: 'audit-log.audit', + places: [ + { + name: 'audit-log.audit-logs', + icon: 'track_changes', + path: '/auditLogs' + } + ] + } + ); + return homeSections; + } + + private buildCustomerUserMenu(authUser: any): Array { + const sections: Array = []; + sections.push( + { + id: guid(), + name: 'home.home', + type: 'link', + path: '/home', + icon: 'home' + }, + { + id: guid(), + name: 'asset.assets', + type: 'link', + path: '/assets', + icon: 'domain' + }, + { + id: guid(), + name: 'device.devices', + type: 'link', + path: '/devices', + icon: 'devices_other' + }, + { + id: guid(), + name: 'entity-view.entity-views', + type: 'link', + path: '/entityViews', + icon: 'view_quilt' + }, + { + id: guid(), + name: 'dashboard.dashboards', + type: 'link', + path: '/dashboards', + icon: 'dashboard' + } + ); + return sections; + } + + private buildCustomerUserHome(authUser: any): Array { + const homeSections: Array = [ + { + name: 'asset.view-assets', + places: [ + { + name: 'asset.assets', + icon: 'domain', + path: '/assets' + } + ] + }, + { + name: 'device.view-devices', + places: [ + { + name: 'device.devices', + icon: 'devices_other', + path: '/devices' + } + ] + }, + { + name: 'entity-view.management', + places: [ + { + name: 'entity-view.entity-views', + icon: 'view_quilt', + path: '/entityViews' + } + ] + }, + { + name: 'dashboard.view-dashboards', + places: [ + { + name: 'dashboard.dashboards', + icon: 'dashboard', + path: '/dashboards' + } + ] + } + ]; + return homeSections; + } + + public menuSections(): Observable> { + return this.menuSections$; + } + + public homeSections(): Observable> { + return this.homeSections$; + } + +} + diff --git a/ui-ngx/src/app/core/services/notification.service.ts b/ui-ngx/src/app/core/services/notification.service.ts new file mode 100644 index 0000000000..78a2cd198d --- /dev/null +++ b/ui-ngx/src/app/core/services/notification.service.ts @@ -0,0 +1,52 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { HideNotification, NotificationMessage } from '@app/core/notification/notification.models'; +import { Observable, Subject } from 'rxjs'; + + +@Injectable( + { + providedIn: 'root' + } +) +export class NotificationService { + + private notificationSubject: Subject = new Subject(); + + private hideNotificationSubject: Subject = new Subject(); + + constructor( + ) { + } + + dispatchNotification(notification: NotificationMessage) { + this.notificationSubject.next(notification); + } + + hideNotification(hideNotification: HideNotification) { + this.hideNotificationSubject.next(hideNotification); + } + + getNotification(): Observable { + return this.notificationSubject.asObservable(); + } + + getHideNotification(): Observable { + return this.hideNotificationSubject.asObservable(); + } +} diff --git a/ui-ngx/src/app/core/services/public-api.ts b/ui-ngx/src/app/core/services/public-api.ts new file mode 100644 index 0000000000..9a57872758 --- /dev/null +++ b/ui-ngx/src/app/core/services/public-api.ts @@ -0,0 +1,32 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export * from './script/node-script-test.service'; +export * from './broadcast.models'; +export * from './broadcast.service'; +export * from './dashboard-utils.service'; +export * from './dialog.service'; +export * from './dynamic-component-factory.service'; +export * from './item-buffer.service'; +export * from './menu.models'; +export * from './menu.service'; +export * from './notification.service'; +export * from './raf.service'; +export * from './resources.service'; +export * from './time.service'; +export * from './title.service'; +export * from './utils.service'; +export * from './window.service'; diff --git a/ui-ngx/src/app/core/services/raf.service.ts b/ui-ngx/src/app/core/services/raf.service.ts new file mode 100644 index 0000000000..3461cbd9ee --- /dev/null +++ b/ui-ngx/src/app/core/services/raf.service.ts @@ -0,0 +1,68 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Inject, Injectable, NgZone } from '@angular/core'; +import { WINDOW } from '@core/services/window.service'; + +export type CancelAnimationFrame = () => void; + +// @dynamic +@Injectable({ + providedIn: 'root' +}) +export class RafService { + + private readonly rafFunction: (frameCallback: () => void) => CancelAnimationFrame; + private readonly rafSupported: boolean; + + constructor( + @Inject(WINDOW) private window: Window, + private ngZone: NgZone + ) { + const requestAnimationFrame: (frameCallback: () => void) => number = window.requestAnimationFrame || + window.webkitRequestAnimationFrame; + const cancelAnimationFrame = window.cancelAnimationFrame || + window.webkitCancelAnimationFrame || + // @ts-ignore + window.webkitCancelRequestAnimationFrame; + + this.rafSupported = !!requestAnimationFrame; + + if (this.rafSupported) { + this.rafFunction = (frameCallback: () => void) => { + const id = requestAnimationFrame(frameCallback); + return () => { + cancelAnimationFrame(id); + }; + }; + } else { + this.rafFunction = (frameCallback: () => void) => { + const timeoutId = setTimeout(frameCallback, 16.66); + return () => { + clearTimeout(timeoutId); + }; + }; + } + } + + public raf(frameCallback: () => void, runInZone = false): CancelAnimationFrame { + if (runInZone) { + return this.rafFunction(frameCallback); + } else { + return this.ngZone.runOutsideAngular(() => this.rafFunction(frameCallback)); + } + } +} diff --git a/ui-ngx/src/app/core/services/resources.service.ts b/ui-ngx/src/app/core/services/resources.service.ts new file mode 100644 index 0000000000..97dd480e2e --- /dev/null +++ b/ui-ngx/src/app/core/services/resources.service.ts @@ -0,0 +1,217 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + Compiler, + ComponentFactory, + Inject, + Injectable, + Injector, + ModuleWithComponentFactories, + Type +} from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { forkJoin, Observable, ReplaySubject, throwError } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; + +declare const SystemJS; + +@Injectable({ + providedIn: 'root' +}) +export class ResourcesService { + + private loadedResources: { [url: string]: ReplaySubject } = {}; + private loadedModules: { [url: string]: ReplaySubject[]> } = {}; + private loadedFactories: { [url: string]: ReplaySubject[]> } = {}; + + private anchor = this.document.getElementsByTagName('head')[0] || this.document.getElementsByTagName('body')[0]; + + constructor(@Inject(DOCUMENT) private readonly document: any, + private compiler: Compiler, + private http: HttpClient, + private injector: Injector) {} + + public loadResource(url: string): Observable { + if (this.loadedResources[url]) { + return this.loadedResources[url].asObservable(); + } + + let fileType; + const match = /[./](css|less|html|htm|js)?(([?#]).*)?$/.exec(url); + if (match !== null) { + fileType = match[1]; + } + if (!fileType) { + return throwError(new Error(`Unable to detect file type from url: ${url}`)); + } else if (fileType !== 'css' && fileType !== 'js') { + return throwError(new Error(`Unsupported file type: ${fileType}`)); + } + return this.loadResourceByType(fileType, url); + } + + public loadFactories(url: string, modulesMap: {[key: string]: any}): Observable[]> { + if (this.loadedFactories[url]) { + return this.loadedFactories[url].asObservable(); + } + const subject = new ReplaySubject[]>(); + this.loadedFactories[url] = subject; + if (modulesMap) { + for (const moduleId of Object.keys(modulesMap)) { + SystemJS.set(moduleId, modulesMap[moduleId]); + } + } + SystemJS.import(url).then( + (module) => { + const modules = this.extractNgModules(module); + if (modules.length) { + const tasks: Promise>[] = []; + for (const m of modules) { + tasks.push(this.compiler.compileModuleAndAllComponentsAsync(m)); + } + forkJoin(tasks).subscribe((compiled) => { + try { + const componentFactories: ComponentFactory[] = []; + for (const c of compiled) { + c.ngModuleFactory.create(this.injector); + componentFactories.push(...c.componentFactories); + } + this.loadedFactories[url].next(componentFactories); + this.loadedFactories[url].complete(); + } catch (e) { + this.loadedFactories[url].error(new Error(`Unable to init module from url: ${url}`)); + delete this.loadedFactories[url]; + } + }, + (e) => { + this.loadedFactories[url].error(new Error(`Unable to compile module from url: ${url}`)); + delete this.loadedFactories[url]; + }); + } else { + this.loadedFactories[url].error(new Error(`Module '${url}' doesn't have default export!`)); + delete this.loadedFactories[url]; + } + }, + (e) => { + this.loadedFactories[url].error(new Error(`Unable to load module from url: ${url}`)); + delete this.loadedFactories[url]; + } + ); + return subject.asObservable(); + } + + public loadModules(url: string, modulesMap: {[key: string]: any}): Observable[]> { + if (this.loadedModules[url]) { + return this.loadedModules[url].asObservable(); + } + const subject = new ReplaySubject[]>(); + this.loadedModules[url] = subject; + if (modulesMap) { + for (const moduleId of Object.keys(modulesMap)) { + SystemJS.set(moduleId, modulesMap[moduleId]); + } + } + SystemJS.import(url).then( + (module) => { + try { + let modules; + try { + modules = this.extractNgModules(module); + } catch (e) { + } + if (modules && modules.length) { + const tasks: Promise>[] = []; + for (const m of modules) { + tasks.push(this.compiler.compileModuleAndAllComponentsAsync(m)); + } + forkJoin(tasks).subscribe((compiled) => { + try { + for (const c of compiled) { + c.ngModuleFactory.create(this.injector); + } + this.loadedModules[url].next(modules); + this.loadedModules[url].complete(); + } catch (e) { + this.loadedModules[url].error(new Error(`Unable to init module from url: ${url}`)); + delete this.loadedModules[url]; + } + }, + (e) => { + this.loadedModules[url].error(new Error(`Unable to compile module from url: ${url}`)); + delete this.loadedModules[url]; + }); + } else { + this.loadedModules[url].error(new Error(`Module '${url}' doesn't have default export or not NgModule!`)); + delete this.loadedModules[url]; + } + } catch (e) { + this.loadedModules[url].error(new Error(`Unable to load module from url: ${url}`)); + delete this.loadedModules[url]; + } + }, + (e) => { + this.loadedModules[url].error(new Error(`Unable to load module from url: ${url}`)); + delete this.loadedModules[url]; + } + ); + return subject.asObservable(); + } + + private extractNgModules(module: any, modules: Type[] = [] ): Type[] { + if (module && 'ɵmod' in module) { + modules.push(module); + } else { + for (const k of Object.keys(module)) { + this.extractNgModules(module[k], modules); + } + } + return modules; + } + + private loadResourceByType(type: 'css' | 'js', url: string): Observable { + const subject = new ReplaySubject(); + this.loadedResources[url] = subject; + let el; + let loaded = false; + switch (type) { + case 'js': + el = this.document.createElement('script'); + el.type = 'text/javascript'; + el.async = true; + el.src = url; + break; + case 'css': + el = this.document.createElement('link'); + el.type = 'text/css'; + el.rel = 'stylesheet'; + el.href = url; + break; + } + el.onload = el.onreadystatechange = (e) => { + if (el.readyState && !/^c|loade/.test(el.readyState) || loaded) { return; } + el.onload = el.onreadystatechange = null; + loaded = true; + this.loadedResources[url].next(); + this.loadedResources[url].complete(); + }; + el.onerror = () => { + this.loadedResources[url].error(new Error(`Unable to load ${url}`)); + delete this.loadedResources[url]; + }; + this.anchor.appendChild(el); + return subject.asObservable(); + } +} diff --git a/ui-ngx/src/app/core/services/script/node-script-test.service.ts b/ui-ngx/src/app/core/services/script/node-script-test.service.ts new file mode 100644 index 0000000000..600c290edc --- /dev/null +++ b/ui-ngx/src/app/core/services/script/node-script-test.service.ts @@ -0,0 +1,102 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { RuleChainService } from '@core/http/rule-chain.service'; +import { switchMap } from 'rxjs/operators'; +import { MatDialog } from '@angular/material/dialog'; +import { + NodeScriptTestDialogComponent, + NodeScriptTestDialogData +} from '@shared/components/dialog/node-script-test-dialog.component'; +import { sortObjectKeys } from '@core/utils'; + +@Injectable({ + providedIn: 'root' +}) +export class NodeScriptTestService { + + constructor(private ruleChainService: RuleChainService, + public dialog: MatDialog) { + } + + testNodeScript(script: string, scriptType: string, functionTitle: string, + functionName: string, argNames: string[], ruleNodeId: string): Observable { + if (ruleNodeId) { + return this.ruleChainService.getLatestRuleNodeDebugInput(ruleNodeId).pipe( + switchMap((debugIn) => { + let msg: any; + let metadata: {[key: string]: string}; + let msgType: string; + if (debugIn) { + if (debugIn.data) { + msg = JSON.parse(debugIn.data); + } + if (debugIn.metadata) { + metadata = JSON.parse(debugIn.metadata); + } + msgType = debugIn.msgType; + } + return this.openTestScriptDialog(script, scriptType, functionTitle, + functionName, argNames, msg, metadata, msgType); + }) + ); + } else { + return this.openTestScriptDialog(script, scriptType, functionTitle, + functionName, argNames); + } + } + + private openTestScriptDialog(script: string, scriptType: string, + functionTitle: string, functionName: string, argNames: string[], + msg?: any, metadata?: {[key: string]: string}, msgType?: string): Observable { + if (!msg) { + msg = { + temperature: 22.4, + humidity: 78 + }; + } + if (!metadata) { + metadata = { + deviceName: 'Test Device', + deviceType: 'default', + ts: new Date().getTime() + '' + }; + } else { + metadata = sortObjectKeys(metadata); + } + if (!msgType) { + msgType = 'POST_TELEMETRY_REQUEST'; + } + return this.dialog.open(NodeScriptTestDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog', 'tb-fullscreen-dialog-gt-sm'], + data: { + msg, + metadata, + msgType, + functionTitle, + functionName, + script, + scriptType, + argNames + } + }).afterClosed(); + } + +} diff --git a/ui-ngx/src/app/core/services/time.service.ts b/ui-ngx/src/app/core/services/time.service.ts new file mode 100644 index 0000000000..f946cf5cd1 --- /dev/null +++ b/ui-ngx/src/app/core/services/time.service.ts @@ -0,0 +1,163 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { + AggregationType, + DAY, + defaultTimeIntervals, + defaultTimewindow, + SECOND, + Timewindow +} from '@shared/models/time/time.models'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { defaultHttpOptions } from '@core/http/http-utils'; +import { map } from 'rxjs/operators'; +import { isDefined } from '@core/utils'; + +export interface TimeInterval { + name: string; + translateParams: {[key: string]: any}; + value: number; +} + +const MIN_INTERVAL = SECOND; +const MAX_INTERVAL = 365 * 20 * DAY; + +const MIN_LIMIT = 7; + +const MAX_DATAPOINTS_LIMIT = 500; + +@Injectable({ + providedIn: 'root' +}) +export class TimeService { + + private maxDatapointsLimit = MAX_DATAPOINTS_LIMIT; + + constructor( + private http: HttpClient + ) {} + + public loadMaxDatapointsLimit(): Observable { + return this.http.get('/api/dashboard/maxDatapointsLimit', + defaultHttpOptions(true)).pipe( + map( (limit) => { + this.maxDatapointsLimit = limit; + if (!this.maxDatapointsLimit || this.maxDatapointsLimit <= MIN_LIMIT) { + this.maxDatapointsLimit = MIN_LIMIT + 1; + } + return this.maxDatapointsLimit; + }) + ); + } + + public matchesExistingInterval(min: number, max: number, intervalMs: number): boolean { + const intervals = this.getIntervals(min, max); + return intervals.findIndex(interval => interval.value === intervalMs) > -1; + } + + public getIntervals(min: number, max: number): Array { + min = this.boundMinInterval(min); + max = this.boundMaxInterval(max); + return defaultTimeIntervals.filter((interval) => interval.value >= min && interval.value <= max); + } + + public boundMinInterval(min: number): number { + if (isDefined(min)) { + min = Math.floor(min / 1000) * 1000; + } + return this.toBound(min, MIN_INTERVAL, MAX_INTERVAL, MIN_INTERVAL); + } + + public boundMaxInterval(max: number): number { + if (isDefined(max)) { + max = Math.floor(max / 1000) * 1000; + } + return this.toBound(max, MIN_INTERVAL, MAX_INTERVAL, MAX_INTERVAL); + } + + public boundToPredefinedInterval(min: number, max: number, intervalMs: number): number { + const intervals = this.getIntervals(min, max); + let minDelta = MAX_INTERVAL; + const boundedInterval = intervalMs || min; + if (!intervals.length) { + return boundedInterval; + } + let matchedInterval: TimeInterval = intervals[0]; + intervals.forEach((interval) => { + const delta = Math.abs(interval.value - boundedInterval); + if (delta < minDelta) { + matchedInterval = interval; + minDelta = delta; + } + }); + return matchedInterval.value; + } + + public boundIntervalToTimewindow(timewindow: number, intervalMs: number, aggType: AggregationType): number { + if (aggType === AggregationType.NONE) { + return SECOND; + } else { + const min = this.minIntervalLimit(timewindow); + const max = this.maxIntervalLimit(timewindow); + if (intervalMs) { + return this.toBound(intervalMs, min, max, intervalMs); + } else { + return this.boundToPredefinedInterval(min, max, this.avgInterval(timewindow)); + } + } + } + + public getMaxDatapointsLimit(): number { + return this.maxDatapointsLimit; + } + + public getMinDatapointsLimit(): number { + return MIN_LIMIT; + } + + public avgInterval(timewindow: number): number { + const avg = timewindow / 200; + return this.boundMinInterval(avg); + } + + public minIntervalLimit(timewindowMs: number): number { + const min = timewindowMs / 500; + return this.boundMinInterval(min); + } + + public maxIntervalLimit(timewindowMs: number): number { + const max = timewindowMs / MIN_LIMIT; + return this.boundMaxInterval(max); + } + + public defaultTimewindow(): Timewindow { + return defaultTimewindow(this); + } + + private toBound(value: number, min: number, max: number, defValue: number): number { + if (isDefined(value)) { + value = Math.max(value, min); + value = Math.min(value, max); + return value; + } else { + return defValue; + } + } + +} diff --git a/ui-ngx/src/app/core/services/title.service.ts b/ui-ngx/src/app/core/services/title.service.ts new file mode 100644 index 0000000000..9e26c73690 --- /dev/null +++ b/ui-ngx/src/app/core/services/title.service.ts @@ -0,0 +1,55 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Title } from '@angular/platform-browser'; +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot } from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; +import { filter } from 'rxjs/operators'; + +import { environment as env } from '@env/environment'; + +@Injectable({ + providedIn: 'root' +}) +export class TitleService { + constructor( + private translate: TranslateService, + private title: Title + ) {} + + setTitle( + snapshot: ActivatedRouteSnapshot, + lazyTranslate?: TranslateService + ) { + let lastChild = snapshot; + while (lastChild.children.length) { + lastChild = lastChild.children[0]; + } + const { title } = lastChild.data; + const translate = lazyTranslate || this.translate; + if (title) { + translate + .get(title) + .pipe(filter(translatedTitle => translatedTitle !== title)) + .subscribe(translatedTitle => + this.title.setTitle(`${env.appTitle} | ${translatedTitle}`) + ); + } else { + this.title.setTitle(env.appTitle); + } + } +} diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts new file mode 100644 index 0000000000..3613c0f484 --- /dev/null +++ b/ui-ngx/src/app/core/services/utils.service.ts @@ -0,0 +1,429 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +// tslint:disable-next-line:no-reference +/// + +import { Inject, Injectable, NgZone } from '@angular/core'; +import { WINDOW } from '@core/services/window.service'; +import { ExceptionData } from '@app/shared/models/error.models'; +import { + createLabelFromDatasource, + deepClone, + deleteNullProperties, + guid, + isDefined, + isDefinedAndNotNull, + isUndefined +} from '@core/utils'; +import { WindowMessage } from '@shared/models/window-message.model'; +import { TranslateService } from '@ngx-translate/core'; +import { customTranslationsPrefix } from '@app/shared/models/constants'; +import { DataKey, Datasource, DatasourceType, KeyInfo } from '@shared/models/widget.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { DataKeyType } from '@app/shared/models/telemetry/telemetry.models'; +import { alarmFields } from '@shared/models/alarm.models'; +import { materialColors } from '@app/shared/models/material.models'; +import { WidgetInfo } from '@home/models/widget-component.models'; +import jsonSchemaDefaults from 'json-schema-defaults'; +import materialIconsCodepoints from '!raw-loader!material-design-icons/iconfont/codepoints'; +import { Observable, of, ReplaySubject } from 'rxjs'; + +const predefinedFunctions: { [func: string]: string } = { + Sin: 'return Math.round(1000*Math.sin(time/5000));', + Cos: 'return Math.round(1000*Math.cos(time/5000));', + Random: 'var value = prevValue + Math.random() * 100 - 50;\n' + + 'var multiplier = Math.pow(10, 2 || 0);\n' + + 'var value = Math.round(value * multiplier) / multiplier;\n' + + 'if (value < -1000) {\n' + + ' value = -1000;\n' + + '} else if (value > 1000) {\n' + + ' value = 1000;\n' + + '}\n' + + 'return value;' +}; + +const predefinedFunctionsList: Array = []; +for (const func of Object.keys(predefinedFunctions)) { + predefinedFunctionsList.push(func); +} + +const defaultAlarmFields: Array = [ + alarmFields.createdTime.keyName, + alarmFields.originator.keyName, + alarmFields.type.keyName, + alarmFields.severity.keyName, + alarmFields.status.keyName +]; + +const commonMaterialIcons: Array = ['more_horiz', 'more_vert', 'open_in_new', + 'visibility', 'play_arrow', 'arrow_back', 'arrow_downward', + 'arrow_forward', 'arrow_upwards', 'close', 'refresh', 'menu', 'show_chart', 'multiline_chart', 'pie_chart', 'insert_chart', 'people', + 'person', 'domain', 'devices_other', 'now_widgets', 'dashboards', 'map', 'pin_drop', 'my_location', 'extension', 'search', + 'settings', 'notifications', 'notifications_active', 'info', 'info_outline', 'warning', 'list', 'file_download', 'import_export', + 'share', 'add', 'edit', 'done']; + +// @dynamic +@Injectable({ + providedIn: 'root' +}) +export class UtilsService { + + iframeMode = false; + widgetEditMode = false; + editWidgetInfo: WidgetInfo = null; + + defaultDataKey: DataKey = { + name: 'f(x)', + type: DataKeyType.function, + label: 'Sin', + color: this.getMaterialColor(0), + funcBody: this.getPredefinedFunctionBody('Sin'), + settings: {}, + _hash: Math.random() + }; + + defaultDatasource: Datasource = { + type: DatasourceType.function, + name: DatasourceType.function, + dataKeys: [deepClone(this.defaultDataKey)] + }; + + defaultAlarmDataKeys: Array = []; + + materialIcons: Array = []; + + constructor(@Inject(WINDOW) private window: Window, + private zone: NgZone, + private translate: TranslateService) { + let frame: Element = null; + try { + frame = window.frameElement; + } catch (e) { + // ie11 fix + } + if (frame) { + this.iframeMode = true; + const dataWidgetAttr = frame.getAttribute('data-widget'); + if (dataWidgetAttr && dataWidgetAttr.length) { + this.editWidgetInfo = JSON.parse(dataWidgetAttr); + this.widgetEditMode = true; + } + } + } + + public getPredefinedFunctionsList(): Array { + return predefinedFunctionsList; + } + + public getPredefinedFunctionBody(func: string): string { + return predefinedFunctions[func]; + } + + public getDefaultDatasource(dataKeySchema: any): Datasource { + const datasource = deepClone(this.defaultDatasource); + if (isDefined(dataKeySchema)) { + datasource.dataKeys[0].settings = this.generateObjectFromJsonSchema(dataKeySchema); + } + return datasource; + } + + private initDefaultAlarmDataKeys() { + for (let i = 0; i < defaultAlarmFields.length; i++) { + const name = defaultAlarmFields[i]; + const dataKey: DataKey = { + name, + type: DataKeyType.alarm, + label: this.translate.instant(alarmFields[name].name), + color: this.getMaterialColor(i), + settings: {}, + _hash: Math.random() + }; + this.defaultAlarmDataKeys.push(dataKey); + } + } + + public getDefaultAlarmDataKeys(): Array { + if (!this.defaultAlarmDataKeys.length) { + this.initDefaultAlarmDataKeys(); + } + return deepClone(this.defaultAlarmDataKeys); + } + + public generateObjectFromJsonSchema(schema: any): any { + const obj = jsonSchemaDefaults(schema); + deleteNullProperties(obj); + return obj; + } + + public processWidgetException(exception: any): ExceptionData { + const data = this.parseException(exception, -6); + if (this.widgetEditMode) { + const message: WindowMessage = { + type: 'widgetException', + data + }; + this.window.parent.postMessage(JSON.stringify(message), '*'); + } + return data; + } + + public parseException(exception: any, lineOffset?: number): ExceptionData { + const data: ExceptionData = {}; + if (exception) { + if (typeof exception === 'string') { + data.message = exception; + } else if (exception instanceof String) { + data.message = exception.toString(); + } else { + if (exception.name) { + data.name = exception.name; + } else { + data.name = 'UnknownError'; + } + if (exception.message) { + data.message = exception.message; + } + if (exception.lineNumber) { + data.lineNumber = exception.lineNumber; + if (exception.columnNumber) { + data.columnNumber = exception.columnNumber; + } + } else if (exception.stack) { + const lineInfoRegexp = /(.*):(\d*)(:)?(\d*)?/g; + const lineInfoGroups = lineInfoRegexp.exec(exception.stack); + if (lineInfoGroups != null && lineInfoGroups.length >= 3) { + if (isUndefined(lineOffset)) { + lineOffset = -2; + } + data.lineNumber = Number(lineInfoGroups[2]) + lineOffset; + if (lineInfoGroups.length >= 5) { + data.columnNumber = Number(lineInfoGroups[4]); + } + } + } + } + } + return data; + } + + public customTranslation(translationValue: string, defaultValue: string): string { + let result: string; + const translationId = customTranslationsPrefix + translationValue; + const translation = this.translate.instant(translationId); + if (translation !== translationId) { + result = translation + ''; + } else { + result = defaultValue; + } + return result; + } + + public guid(): string { + return guid(); + } + + public validateDatasources(datasources: Array): Array { + datasources.forEach((datasource) => { + // @ts-ignore + if (datasource.type === 'device') { + datasource.type = DatasourceType.entity; + datasource.entityType = EntityType.DEVICE; + if (datasource.deviceId) { + datasource.entityId = datasource.deviceId; + } else if (datasource.deviceAliasId) { + datasource.entityAliasId = datasource.deviceAliasId; + } + if (datasource.deviceName) { + datasource.entityName = datasource.deviceName; + } + } + if (datasource.type === DatasourceType.entity && datasource.entityId) { + datasource.name = datasource.entityName; + } + if (!datasource.dataKeys) { + datasource.dataKeys = []; + } + }); + return datasources; + } + + public getMaterialIcons(): Observable> { + if (this.materialIcons.length) { + return of(this.materialIcons); + } else { + const materialIconsSubject = new ReplaySubject>(); + this.zone.runOutsideAngular(() => { + const codepointsArray = materialIconsCodepoints + .split('\n') + .filter((codepoint) => codepoint && codepoint.length); + codepointsArray.forEach((codepoint) => { + const values = codepoint.split(' '); + if (values && values.length === 2) { + this.materialIcons.push(values[0]); + } + }); + materialIconsSubject.next(this.materialIcons); + }); + return materialIconsSubject.asObservable(); + } + } + + public getCommonMaterialIcons(): Array { + return commonMaterialIcons; + } + + public getMaterialColor(index: number) { + const colorIndex = index % materialColors.length; + return materialColors[colorIndex].value; + } + + public createKey(keyInfo: KeyInfo, type: DataKeyType, index: number = -1): DataKey { + let label; + if (type === DataKeyType.alarm && !keyInfo.label) { + const alarmField = alarmFields[keyInfo.name]; + if (alarmField) { + label = this.translate.instant(alarmField.name); + } + } + if (!label) { + label = keyInfo.label || keyInfo.name; + } + const dataKey: DataKey = { + name: keyInfo.name, + type, + label, + funcBody: keyInfo.funcBody, + settings: {}, + _hash: Math.random() + }; + if (keyInfo.units) { + dataKey.units = keyInfo.units; + } + if (isDefined(keyInfo.decimals)) { + dataKey.decimals = keyInfo.decimals; + } + if (keyInfo.color) { + dataKey.color = keyInfo.color; + } else if (index > -1) { + dataKey.color = this.getMaterialColor(index); + } + if (keyInfo.postFuncBody && keyInfo.postFuncBody.length) { + dataKey.usePostProcessing = true; + dataKey.postFuncBody = keyInfo.postFuncBody; + } + return dataKey; + } + + /* public createAdditionalDataKey(dataKey: DataKey, datasource: Datasource, timeUnit: string, + datasources: Datasource[], additionalKeysNumber: number): DataKey { + const additionalDataKey = deepClone(dataKey); + if (dataKey.settings.comparisonSettings.comparisonValuesLabel) { + additionalDataKey.label = createLabelFromDatasource(datasource, dataKey.settings.comparisonSettings.comparisonValuesLabel); + } else { + additionalDataKey.label = dataKey.label + ' ' + this.translate.instant('legend.comparison-time-ago.' + timeUnit); + } + additionalDataKey.pattern = additionalDataKey.label; + if (dataKey.settings.comparisonSettings.color) { + additionalDataKey.color = dataKey.settings.comparisonSettings.color; + } else { + const index = datasources.map((_datasource) => datasource.dataKeys.length) + .reduce((previousValue, currentValue) => previousValue + currentValue, 0); + additionalDataKey.color = this.getMaterialColor(index + additionalKeysNumber); + } + additionalDataKey._hash = Math.random(); + return additionalDataKey; + }*/ + + public createLabelFromDatasource(datasource: Datasource, pattern: string): string { + return createLabelFromDatasource(datasource, pattern); + } + + public generateColors(datasources: Array) { + let index = 0; + datasources.forEach((datasource) => { + datasource.dataKeys.forEach((dataKey) => { + if (!dataKey.color) { + dataKey.color = this.getMaterialColor(index); + } + index++; + }); + }); + } + + public currentPerfTime(): number { + return this.window.performance && this.window.performance.now ? + this.window.performance.now() : Date.now(); + } + + public getQueryParam(name: string): string { + const url = this.window.location.href; + name = name.replace(/[\[\]]/g, '\\$&'); + const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'); + const results = regex.exec(url); + if (!results) { + return null; + } + if (!results[2]) { + return ''; + } + return decodeURIComponent(results[2].replace(/\+/g, ' ')); + } + + public updateQueryParam(name: string, value: string | null) { + const baseUrl = [this.window.location.protocol, '//', this.window.location.host, this.window.location.pathname].join(''); + const urlQueryString = this.window.location.search; + let newParam = ''; + let params = ''; + if (value !== null) { + newParam = name + '=' + value; + } + if (urlQueryString) { + const keyRegex = new RegExp('([\?&])' + name + '[^&]*'); + if (urlQueryString.match(keyRegex) !== null) { + if (newParam) { + newParam = '$1' + newParam; + } + params = urlQueryString.replace(keyRegex, newParam); + } else if (newParam) { + params = urlQueryString + '&' + newParam; + } + } else if (newParam) { + params = '?' + newParam; + } + this.window.history.replaceState({}, '', baseUrl + params); + } + + public deepClone(target: T, ignoreFields?: string[]): T { + return deepClone(target, ignoreFields); + } + + public isUndefined(value: any): boolean { + return isUndefined(value); + } + + public isDefined(value: any): boolean { + return isDefined(value); + } + + public defaultValue(value: any, defaultValue: any): any { + if (isDefinedAndNotNull(value)) { + return value; + } else { + return defaultValue; + } + } +} diff --git a/ui-ngx/src/app/core/services/window.service.ts b/ui-ngx/src/app/core/services/window.service.ts new file mode 100644 index 0000000000..acb11f7e9e --- /dev/null +++ b/ui-ngx/src/app/core/services/window.service.ts @@ -0,0 +1,72 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + + +import { isPlatformBrowser } from '@angular/common'; +import { ClassProvider, FactoryProvider, Injectable, InjectionToken, PLATFORM_ID } from '@angular/core'; + +/* Create a new injection token for injecting the window into a component. */ +export const WINDOW = new InjectionToken('WindowToken'); + +/* Define abstract class for obtaining reference to the global window object. */ +export abstract class WindowRef { + + get nativeWindow(): Window | object { + throw new Error('Not implemented.'); + } + +} + +/* Define class that implements the abstract class and returns the native window object. */ +@Injectable() +export class BrowserWindowRef extends WindowRef { + + constructor() { + super(); + } + + get nativeWindow(): Window | object { + return window; + } + +} + +/* Create an factory function that returns the native window object. */ +export function windowFactory(browserWindowRef: BrowserWindowRef, platformId: object): Window | object { + if (isPlatformBrowser(platformId)) { + return browserWindowRef.nativeWindow; + } + return new Object(); +} + +/* Create a injectable provider for the WindowRef token that uses the BrowserWindowRef class. */ +export const browserWindowProvider: ClassProvider = { + provide: WindowRef, + useClass: BrowserWindowRef +}; + +/* Create an injectable provider that uses the windowFactory function for returning the native window object. */ +export const windowProvider: FactoryProvider = { + provide: WINDOW, + useFactory: windowFactory, + deps: [ WindowRef, PLATFORM_ID ] +}; + +/* Create an array of providers. */ +export const WINDOW_PROVIDERS = [ + browserWindowProvider, + windowProvider +]; diff --git a/ui-ngx/src/app/core/settings/settings.actions.ts b/ui-ngx/src/app/core/settings/settings.actions.ts new file mode 100644 index 0000000000..6d4c737197 --- /dev/null +++ b/ui-ngx/src/app/core/settings/settings.actions.ts @@ -0,0 +1,30 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Action } from '@ngrx/store'; + +export enum SettingsActionTypes { + CHANGE_LANGUAGE = '[Settings] Change Language' +} + +export class ActionSettingsChangeLanguage implements Action { + readonly type = SettingsActionTypes.CHANGE_LANGUAGE; + + constructor(readonly payload: { userLang: string }) {} +} + +export type SettingsActions = + | ActionSettingsChangeLanguage; diff --git a/ui-ngx/src/app/core/settings/settings.effects.ts b/ui-ngx/src/app/core/settings/settings.effects.ts new file mode 100644 index 0000000000..e16e7b862b --- /dev/null +++ b/ui-ngx/src/app/core/settings/settings.effects.ts @@ -0,0 +1,99 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ActivationEnd, Router } from '@angular/router'; +import { Injectable } from '@angular/core'; +import { select, Store } from '@ngrx/store'; +import { Actions, Effect, ofType } from '@ngrx/effects'; +import { TranslateService } from '@ngx-translate/core'; +import { merge } from 'rxjs'; +import { distinctUntilChanged, filter, map, tap, withLatestFrom } from 'rxjs/operators'; + +import { SettingsActions, SettingsActionTypes, } from './settings.actions'; +import { selectSettingsState } from './settings.selectors'; +import { AppState } from '@app/core/core.state'; +import { LocalStorageService } from '@app/core/local-storage/local-storage.service'; +import { TitleService } from '@app/core/services/title.service'; +import { updateUserLang } from '@app/core/settings/settings.utils'; +import { AuthService } from '@core/auth/auth.service'; +import { UtilsService } from '@core/services/utils.service'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { ActionAuthUpdateLastPublicDashboardId } from '../auth/auth.actions'; + +export const SETTINGS_KEY = 'SETTINGS'; + +@Injectable() +export class SettingsEffects { + constructor( + private actions$: Actions, + private store: Store, + private authService: AuthService, + private utils: UtilsService, + private router: Router, + private localStorageService: LocalStorageService, + private titleService: TitleService, + private translate: TranslateService + ) { + } + + @Effect({dispatch: false}) + persistSettings = this.actions$.pipe( + ofType( + SettingsActionTypes.CHANGE_LANGUAGE, + ), + withLatestFrom(this.store.pipe(select(selectSettingsState))), + tap(([action, settings]) => + this.localStorageService.setItem(SETTINGS_KEY, settings) + ) + ); + + @Effect({dispatch: false}) + setTranslateServiceLanguage = this.store.pipe( + select(selectSettingsState), + map(settings => settings.userLang), + distinctUntilChanged(), + tap(userLang => updateUserLang(this.translate, userLang)) + ); + + @Effect({dispatch: false}) + setTitle = merge( + this.actions$.pipe(ofType(SettingsActionTypes.CHANGE_LANGUAGE)), + this.router.events.pipe(filter(event => event instanceof ActivationEnd)) + ).pipe( + tap(() => { + this.titleService.setTitle( + this.router.routerState.snapshot.root, + this.translate + ); + }) + ); + + @Effect({dispatch: false}) + setPublicId = merge( + this.router.events.pipe(filter(event => event instanceof ActivationEnd)) + ).pipe( + tap((event) => { + const authUser = getCurrentAuthUser(this.store); + const snapshot = (event as ActivationEnd).snapshot; + if (authUser && authUser.isPublic && snapshot.url && snapshot.url.length + && snapshot.url[0].path === 'dashboard') { + this.utils.updateQueryParam('publicId', authUser.sub); + this.store.dispatch(new ActionAuthUpdateLastPublicDashboardId( + { lastPublicDashboardId: snapshot.params.dashboardId})); + } + }) + ); +} diff --git a/ui-ngx/src/app/core/settings/settings.models.ts b/ui-ngx/src/app/core/settings/settings.models.ts new file mode 100644 index 0000000000..7489a9a031 --- /dev/null +++ b/ui-ngx/src/app/core/settings/settings.models.ts @@ -0,0 +1,20 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + + +export interface SettingsState { + userLang: string; +} diff --git a/ui-ngx/src/app/core/settings/settings.reducer.ts b/ui-ngx/src/app/core/settings/settings.reducer.ts new file mode 100644 index 0000000000..9ae9518519 --- /dev/null +++ b/ui-ngx/src/app/core/settings/settings.reducer.ts @@ -0,0 +1,34 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { SettingsState } from './settings.models'; +import { SettingsActions, SettingsActionTypes } from './settings.actions'; + +export const initialState: SettingsState = { + userLang: null +}; + +export function settingsReducer( + state: SettingsState = initialState, + action: SettingsActions +): SettingsState { + switch (action.type) { + case SettingsActionTypes.CHANGE_LANGUAGE: + return { ...state, ...action.payload }; + default: + return state; + } +} diff --git a/ui-ngx/src/app/core/settings/settings.selectors.ts b/ui-ngx/src/app/core/settings/settings.selectors.ts new file mode 100644 index 0000000000..db6ce50165 --- /dev/null +++ b/ui-ngx/src/app/core/settings/settings.selectors.ts @@ -0,0 +1,34 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { createFeatureSelector, createSelector } from '@ngrx/store'; + +import { SettingsState } from './settings.models'; +import { AppState } from '@app/core/core.state'; + +export const selectSettingsState = createFeatureSelector( + 'settings' +); + +export const selectSettings = createSelector( + selectSettingsState, + (state: SettingsState) => state +); + +export const selectUserLang = createSelector( + selectSettings, + (state: SettingsState) => state.userLang +); diff --git a/ui-ngx/src/app/core/settings/settings.utils.ts b/ui-ngx/src/app/core/settings/settings.utils.ts new file mode 100644 index 0000000000..d44dc267e0 --- /dev/null +++ b/ui-ngx/src/app/core/settings/settings.utils.ts @@ -0,0 +1,65 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { environment as env } from '@env/environment'; +import { TranslateService } from '@ngx-translate/core'; +import * as _moment from 'moment'; + +export function updateUserLang(translate: TranslateService, userLang: string) { + let targetLang = userLang; + if (!env.production) { + console.log(`User lang: ${targetLang}`); + } + if (!targetLang) { + targetLang = translate.getBrowserCultureLang(); + if (!env.production) { + console.log(`Fallback to browser lang: ${targetLang}`); + } + } + const detectedSupportedLang = detectSupportedLang(targetLang); + if (!env.production) { + console.log(`Detected supported lang: ${detectedSupportedLang}`); + } + translate.use(detectedSupportedLang); + _moment.locale([detectedSupportedLang]); +} + +function detectSupportedLang(targetLang: string): string { + const langTag = (targetLang || '').split('-').join('_'); + if (langTag.length) { + if (env.supportedLangs.indexOf(langTag) > -1) { + return langTag; + } else { + const parts = langTag.split('_'); + let lang; + if (parts.length === 2) { + lang = parts[0]; + } else { + lang = langTag; + } + const foundLangs = env.supportedLangs.filter( + (supportedLang: string) => { + const supportedLangParts = supportedLang.split('_'); + return supportedLangParts[0] === lang; + } + ); + if (foundLangs.length) { + return foundLangs[0]; + } + } + } + return env.defaultLang; +} diff --git a/ui-ngx/src/app/core/translate/missing-translate-handler.ts b/ui-ngx/src/app/core/translate/missing-translate-handler.ts new file mode 100644 index 0000000000..32353fa2cf --- /dev/null +++ b/ui-ngx/src/app/core/translate/missing-translate-handler.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { MissingTranslationHandler, MissingTranslationHandlerParams } from '@ngx-translate/core'; +import { customTranslationsPrefix } from '@app/shared/models/constants'; + +export class TbMissingTranslationHandler implements MissingTranslationHandler { + handle(params: MissingTranslationHandlerParams) { + if (params.key && !params.key.startsWith(customTranslationsPrefix)) { + console.warn('Translation for ' + params.key + ' doesn\'t exist'); + } + } +} diff --git a/ui-ngx/src/app/core/translate/translate-default-compiler.ts b/ui-ngx/src/app/core/translate/translate-default-compiler.ts new file mode 100644 index 0000000000..a5bf1bbc17 --- /dev/null +++ b/ui-ngx/src/app/core/translate/translate-default-compiler.ts @@ -0,0 +1,75 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + MESSAGE_FORMAT_CONFIG, + MessageFormatConfig, + TranslateMessageFormatCompiler +} from 'ngx-translate-messageformat-compiler'; +import { Inject, Injectable, Optional } from '@angular/core'; +import messageFormatParser from 'messageformat-parser'; + +@Injectable({ providedIn: 'root' }) +export class TranslateDefaultCompiler extends TranslateMessageFormatCompiler { + + constructor( + @Optional() + @Inject(MESSAGE_FORMAT_CONFIG) + config?: MessageFormatConfig + ) { + super(config); + } + + public compile(value: string, lang: string): (params: any) => string { + return this.defaultCompile(value, lang); + } + + public compileTranslations(translations: any, lang: string): any { + return this.defaultCompile(translations, lang); + } + + private defaultCompile(src: any, lang: string): any { + if (typeof src !== 'object') { + if (this.checkIsPlural(src)) { + return super.compile(src, lang); + } else { + return src; + } + } else { + const result = {}; + for (const key of Object.keys(src)) { + result[key] = this.defaultCompile(src[key], lang); + } + return result; + } + } + + private checkIsPlural(src: string): boolean { + let tokens: any[]; + try { + tokens = messageFormatParser.parse(src.replace(/\{\{/g, '{').replace(/\}\}/g, '}'), + {cardinal: [], ordinal: []}); + } catch (e) { + console.warn(`Failed to parse source: ${src}`); + console.error(e); + } + const res = tokens.filter( + (value) => typeof value !== 'string' && value.type === 'plural' + ); + return res.length > 0; + } + +} diff --git a/ui-ngx/src/app/core/translate/translate-default-parser.ts b/ui-ngx/src/app/core/translate/translate-default-parser.ts new file mode 100644 index 0000000000..1468ab195b --- /dev/null +++ b/ui-ngx/src/app/core/translate/translate-default-parser.ts @@ -0,0 +1,76 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { TranslateParser } from '@ngx-translate/core'; +import { isDefinedAndNotNull } from '@core/utils'; + +@Injectable({ providedIn: 'root' }) +export class TranslateDefaultParser extends TranslateParser { + templateMatcher: RegExp = /{{\s?([^{}\s]*)\s?}}/g; + + // tslint:disable-next-line:ban-types + public interpolate(expr: string | Function, params?: any): string { + let result: string; + + if (typeof expr === 'string') { + result = this.interpolateString(expr, params); + } else if (typeof expr === 'function') { + result = this.interpolateFunction(expr, params); + if (typeof result === 'string') { + result = this.interpolateString(result, params); + } + } else { + result = expr as string; + } + + return result; + } + + getValue(target: any, key: string): any { + const keys = typeof key === 'string' ? key.split('.') : [key]; + key = ''; + do { + key += keys.shift(); + if (isDefinedAndNotNull(target) && isDefinedAndNotNull(target[key]) && (typeof target[key] === 'object' || !keys.length)) { + target = target[key]; + key = ''; + } else if (!keys.length) { + target = undefined; + } else { + key += '.'; + } + } while (keys.length); + + return target; + } + + // tslint:disable-next-line:ban-types + private interpolateFunction(fn: Function, params?: any) { + return fn(params); + } + + private interpolateString(expr: string, params?: any) { + if (!params) { + return expr; + } + + return expr.replace(this.templateMatcher, (substring: string, b: string) => { + const r = this.getValue(params, b); + return isDefinedAndNotNull(r) ? r : substring; + }); + } +} diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts new file mode 100644 index 0000000000..4650caca77 --- /dev/null +++ b/ui-ngx/src/app/core/utils.ts @@ -0,0 +1,417 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import _ from 'lodash'; +import { Observable, Subject } from 'rxjs'; +import { finalize, share } from 'rxjs/operators'; +import { Datasource } from '@app/shared/models/widget.models'; + +const varsRegex = /\${([^}]*)}/g; + +export function onParentScrollOrWindowResize(el: Node): Observable { + const scrollSubject = new Subject(); + const scrollParentNodes = scrollParents(el); + const eventListenerObject: EventListenerObject = { + handleEvent(evt: Event) { + scrollSubject.next(evt); + } + }; + scrollParentNodes.forEach((scrollParentNode) => { + scrollParentNode.addEventListener('scroll', eventListenerObject); + }); + window.addEventListener('resize', eventListenerObject); + return scrollSubject.pipe( + finalize(() => { + scrollParentNodes.forEach((scrollParentNode) => { + scrollParentNode.removeEventListener('scroll', eventListenerObject); + }); + window.removeEventListener('resize', eventListenerObject); + }), + share() + ); +} + +export function isLocalUrl(url: string): boolean { + const parser = document.createElement('a'); + parser.href = url; + const host = parser.hostname; + return host === 'localhost' || host === '127.0.0.1'; +} + +export function animatedScroll(element: HTMLElement, scrollTop: number, delay?: number) { + let currentTime = 0; + const increment = 20; + const start = element.scrollTop; + const to = scrollTop; + const duration = delay ? delay : 0; + const remaining = to - start; + const animateScroll = () => { + if (duration === 0) { + element.scrollTop = to; + } else { + currentTime += increment; + element.scrollTop = easeInOut(currentTime, start, remaining, duration); + if (currentTime < duration) { + setTimeout(animateScroll, increment); + } + } + }; + animateScroll(); +} + +export function isUndefined(value: any): boolean { + return typeof value === 'undefined'; +} + +export function isUndefinedOrNull(value: any): boolean { + return typeof value === 'undefined' || value === null; +} + +export function isDefined(value: any): boolean { + return typeof value !== 'undefined'; +} + +export function isDefinedAndNotNull(value: any): boolean { + return typeof value !== 'undefined' && value !== null; +} + +export function isEmptyStr(value: any): boolean { + return value === ''; +} + +export function isFunction(value: any): boolean { + return typeof value === 'function'; +} + +export function isObject(value: any): boolean { + return value !== null && typeof value === 'object'; +} + +export function isNumber(value: any): boolean { + return typeof value === 'number'; +} + +export function isNumeric(value: any): boolean { + return (value - parseFloat(value) + 1) >= 0; +} + +export function isString(value: any): boolean { + return typeof value === 'string'; +} + +export function isEmpty(obj: any): boolean { + for (const key of Object.keys(obj)) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + return false; + } + } + return true; +} + +export function formatValue(value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined { + if (isDefinedAndNotNull(value) && isNumeric(value) && + (isDefinedAndNotNull(dec) || isDefinedAndNotNull(units) || Number(value).toString() === value)) { + let formatted: string | number = Number(value); + if (isDefinedAndNotNull(dec)) { + formatted = formatted.toFixed(dec); + } + if (!showZeroDecimals) { + formatted = (Number(formatted)); + } + formatted = formatted.toString(); + if (isDefinedAndNotNull(units) && units.length > 0) { + formatted += ' ' + units; + } + return formatted; + } else { + return value !== null ? value : ''; + } +} + +export function objectValues(obj: any): any[] { + return Object.keys(obj).map(e => obj[e]); +} + +export function deleteNullProperties(obj: any) { + if (isUndefined(obj) || obj == null) { + return; + } + Object.keys(obj).forEach((propName) => { + if (obj[propName] === null || isUndefined(obj[propName])) { + delete obj[propName]; + } else if (isObject(obj[propName])) { + deleteNullProperties(obj[propName]); + } else if (obj[propName] instanceof Array) { + (obj[propName] as any[]).forEach((elem) => { + deleteNullProperties(elem); + }); + } + }); +} + +export function objToBase64(obj: any): string { + const json = JSON.stringify(obj); + return btoa(encodeURIComponent(json).replace(/%([0-9A-F]{2})/g, + function toSolidBytes(match, p1) { + return String.fromCharCode(Number('0x' + p1)); + })); +} + +export function objToBase64URI(obj: any): string { + return encodeURIComponent(objToBase64(obj)); +} + +export function base64toObj(b64Encoded: string): any { + const json = decodeURIComponent(atob(b64Encoded).split('').map((c) => { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }).join('')); + return JSON.parse(json); +} + +const scrollRegex = /(auto|scroll)/; + +function parentNodes(node: Node, nodes: Node[]): Node[] { + if (node.parentNode === null) { + return nodes; + } + return parentNodes(node.parentNode, nodes.concat([node])); +} + +function style(el: Element, prop: string): string { + return getComputedStyle(el, null).getPropertyValue(prop); +} + +function overflow(el: Element): string { + return style(el, 'overflow') + style(el, 'overflow-y') + style(el, 'overflow-x'); +} + +function isScrollNode(node: Node): boolean { + if (node instanceof Element) { + return scrollRegex.test(overflow(node)); + } else { + return false; + } +} + +function scrollParents(node: Node): Node[] { + if (!(node instanceof HTMLElement || node instanceof SVGElement)) { + return []; + } + const scrollParentNodes = []; + const nodeParents = parentNodes(node, []); + nodeParents.forEach((nodeParent) => { + if (isScrollNode(nodeParent)) { + scrollParentNodes.push(nodeParent); + } + }); + if (document.scrollingElement) { + scrollParentNodes.push(document.scrollingElement); + } else if (document.documentElement) { + scrollParentNodes.push(document.documentElement); + } + return scrollParentNodes; +} + +export function hashCode(str: string): number { + let hash = 0; + let i: number; + let char: number; + if (str.length === 0) { + return hash; + } + for (i = 0; i < str.length; i++) { + char = str.charCodeAt(i); + // tslint:disable-next-line:no-bitwise + hash = ((hash << 5) - hash) + char; + // tslint:disable-next-line:no-bitwise + hash = hash & hash; // Convert to 32bit integer + } + return hash; +} + +export function objectHashCode(obj: any): number { + let hash = 0; + if (obj) { + const str = JSON.stringify(obj); + hash = hashCode(str); + } + return hash; +} + +function easeInOut( + currentTime: number, + startTime: number, + remainingTime: number, + duration: number) { + currentTime /= duration / 2; + + if (currentTime < 1) { + return (remainingTime / 2) * currentTime * currentTime + startTime; + } + + currentTime--; + return ( + (-remainingTime / 2) * (currentTime * (currentTime - 2) - 1) + startTime + ); +} + +export function deepClone(target: T, ignoreFields?: string[]): T { + if (target === null) { + return target; + } + if (target instanceof Date) { + return new Date(target.getTime()) as any; + } + if (target instanceof Array) { + const cp = [] as any[]; + (target as any[]).forEach((v) => { cp.push(v); }); + return cp.map((n: any) => deepClone(n)) as any; + } + if (typeof target === 'object' && target !== {}) { + const cp = { ...(target as { [key: string]: any }) } as { [key: string]: any }; + Object.keys(cp).forEach(k => { + if (!ignoreFields || ignoreFields.indexOf(k) === -1) { + cp[k] = deepClone(cp[k]); + } + }); + return cp as T; + } + return target; +} + +export function isEqual(a: any, b: any): boolean { + return _.isEqual(a, b); +} + +export function mergeDeep(target: T, ...sources: T[]): T { + return _.merge(target, ...sources); +} + +export function guid(): string { + function s4(): string { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + } + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + + s4() + '-' + s4() + s4() + s4(); +} + +const SNAKE_CASE_REGEXP = /[A-Z]/g; + +export function snakeCase(name: string, separator: string): string { + separator = separator || '_'; + return name.replace(SNAKE_CASE_REGEXP, (letter, pos) => { + return (pos ? separator : '') + letter.toLowerCase(); + }); +} + +export function getDescendantProp(obj: any, path: string): any { + return path.split('.').reduce((acc, part) => acc && acc[part], obj); +} + +export function insertVariable(pattern: string, name: string, value: any): string { + let result = pattern; + let match = varsRegex.exec(pattern); + while (match !== null) { + const variable = match[0]; + const variableName = match[1]; + if (variableName === name) { + result = result.replace(variable, value); + } + match = varsRegex.exec(pattern); + } + return result; +} + +export function createLabelFromDatasource(datasource: Datasource, pattern: string): string { + let label = pattern; + if (!datasource) { + return label; + } + let match = varsRegex.exec(pattern); + while (match !== null) { + const variable = match[0]; + const variableName = match[1]; + if (variableName === 'dsName') { + label = label.replace(variable, datasource.name); + } else if (variableName === 'entityName') { + label = label.replace(variable, datasource.entityName); + } else if (variableName === 'deviceName') { + label = label.replace(variable, datasource.entityName); + } else if (variableName === 'entityLabel') { + label = label.replace(variable, datasource.entityLabel || datasource.entityName); + } else if (variableName === 'aliasName') { + label = label.replace(variable, datasource.aliasName); + } else if (variableName === 'entityDescription') { + label = label.replace(variable, datasource.entityDescription); + } + match = varsRegex.exec(pattern); + } + return label; +} + +export function padValue(val: any, dec: number): string { + let strVal; + let n; + + val = parseFloat(val); + n = (val < 0); + val = Math.abs(val); + + if (dec > 0) { + strVal = val.toFixed(dec); + } else { + strVal = Math.round(val).toString(); + } + strVal = (n ? '-' : '') + strVal; + return strVal; +} + +export function sortObjectKeys(obj: T): T { + return Object.keys(obj).sort().reduce((acc, key) => { + acc[key] = obj[key]; + return acc; + }, {} as T); +} + +export function deepTrim(obj: T): T { + if (isNumber(obj) || isUndefined(obj) || isString(obj) || obj === null) { + return obj; + } + return Object.keys(obj).reduce((acc, curr) => { + if (isString(obj[curr])) { + acc[curr] = obj[curr].trim(); + } else if (isObject(obj[curr])) { + acc[curr] = deepTrim(obj[curr]); + } else { + acc[curr] = obj[curr]; + } + return acc; + }, (Array.isArray(obj) ? [] : {}) as T); +} + +export function generateSecret(length?: number): string { + if (isUndefined(length) || length == null) { + length = 1; + } + const l = length > 10 ? 10 : length; + const str = Math.random().toString(36).substr(2, l); + if (str.length >= length) { + return str; + } + return str.concat(generateSecret(length - str.length)); +} diff --git a/ui-ngx/src/app/core/ws/telemetry-websocket.service.ts b/ui-ngx/src/app/core/ws/telemetry-websocket.service.ts new file mode 100644 index 0000000000..a79672be74 --- /dev/null +++ b/ui-ngx/src/app/core/ws/telemetry-websocket.service.ts @@ -0,0 +1,349 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Inject, Injectable, NgZone } from '@angular/core'; +import { + AlarmDataCmd, AlarmDataUnsubscribeCmd, + AlarmDataUpdate, + AttributesSubscriptionCmd, EntityDataCmd, EntityDataUnsubscribeCmd, EntityDataUpdate, + GetHistoryCmd, isAlarmDataUpdateMsg, isEntityDataUpdateMsg, + SubscriptionCmd, + SubscriptionUpdate, + SubscriptionUpdateMsg, + TelemetryFeature, + TelemetryPluginCmdsWrapper, + TelemetryService, + TelemetrySubscriber, + TimeseriesSubscriptionCmd, WebsocketDataMsg +} from '@app/shared/models/telemetry/telemetry.models'; +import { select, Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { AuthService } from '@core/auth/auth.service'; +import { selectIsAuthenticated } from '@core/auth/auth.selectors'; +import { WINDOW } from '@core/services/window.service'; +import { webSocket, WebSocketSubject } from 'rxjs/webSocket'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import Timeout = NodeJS.Timeout; + +const RECONNECT_INTERVAL = 2000; +const WS_IDLE_TIMEOUT = 90000; +const MAX_PUBLISH_COMMANDS = 10; + +// @dynamic +@Injectable({ + providedIn: 'root' +}) +export class TelemetryWebsocketService implements TelemetryService { + + isActive = false; + isOpening = false; + isOpened = false; + isReconnect = false; + + socketCloseTimer: Timeout; + reconnectTimer: Timeout; + + lastCmdId = 0; + subscribersCount = 0; + subscribersMap = new Map(); + + reconnectSubscribers = new Set(); + + cmdsWrapper = new TelemetryPluginCmdsWrapper(); + telemetryUri: string; + + dataStream: WebSocketSubject; + + constructor(private store: Store, + private authService: AuthService, + private ngZone: NgZone, + @Inject(WINDOW) private window: Window) { + this.store.pipe(select(selectIsAuthenticated)).subscribe( + () => { + this.reset(true); + } + ); + + let port = this.window.location.port; + if (this.window.location.protocol === 'https:') { + if (!port) { + port = '443'; + } + this.telemetryUri = 'wss:'; + } else { + if (!port) { + port = '80'; + } + this.telemetryUri = 'ws:'; + } + this.telemetryUri += `//${this.window.location.hostname}:${port}/api/ws/plugins/telemetry`; + } + + public subscribe(subscriber: TelemetrySubscriber) { + this.isActive = true; + subscriber.subscriptionCommands.forEach( + (subscriptionCommand) => { + const cmdId = this.nextCmdId(); + this.subscribersMap.set(cmdId, subscriber); + subscriptionCommand.cmdId = cmdId; + if (subscriptionCommand instanceof SubscriptionCmd) { + if (subscriptionCommand.getType() === TelemetryFeature.TIMESERIES) { + this.cmdsWrapper.tsSubCmds.push(subscriptionCommand as TimeseriesSubscriptionCmd); + } else { + this.cmdsWrapper.attrSubCmds.push(subscriptionCommand as AttributesSubscriptionCmd); + } + } else if (subscriptionCommand instanceof GetHistoryCmd) { + this.cmdsWrapper.historyCmds.push(subscriptionCommand); + } else if (subscriptionCommand instanceof EntityDataCmd) { + this.cmdsWrapper.entityDataCmds.push(subscriptionCommand); + } else if (subscriptionCommand instanceof AlarmDataCmd) { + this.cmdsWrapper.alarmDataCmds.push(subscriptionCommand); + } + } + ); + this.subscribersCount++; + this.publishCommands(); + } + + public update(subscriber: TelemetrySubscriber) { + if (!this.isReconnect) { + subscriber.subscriptionCommands.forEach( + (subscriptionCommand) => { + if (subscriptionCommand.cmdId && subscriptionCommand instanceof EntityDataCmd) { + this.cmdsWrapper.entityDataCmds.push(subscriptionCommand); + } + } + ); + this.publishCommands(); + } + } + + public unsubscribe(subscriber: TelemetrySubscriber) { + if (this.isActive) { + subscriber.subscriptionCommands.forEach( + (subscriptionCommand) => { + if (subscriptionCommand instanceof SubscriptionCmd) { + subscriptionCommand.unsubscribe = true; + if (subscriptionCommand.getType() === TelemetryFeature.TIMESERIES) { + this.cmdsWrapper.tsSubCmds.push(subscriptionCommand as TimeseriesSubscriptionCmd); + } else { + this.cmdsWrapper.attrSubCmds.push(subscriptionCommand as AttributesSubscriptionCmd); + } + } else if (subscriptionCommand instanceof EntityDataCmd) { + const entityDataUnsubscribeCmd = new EntityDataUnsubscribeCmd(); + entityDataUnsubscribeCmd.cmdId = subscriptionCommand.cmdId; + this.cmdsWrapper.entityDataUnsubscribeCmds.push(entityDataUnsubscribeCmd); + } else if (subscriptionCommand instanceof AlarmDataCmd) { + const alarmDataUnsubscribeCmd = new AlarmDataUnsubscribeCmd(); + alarmDataUnsubscribeCmd.cmdId = subscriptionCommand.cmdId; + this.cmdsWrapper.alarmDataUnsubscribeCmds.push(alarmDataUnsubscribeCmd); + } + const cmdId = subscriptionCommand.cmdId; + if (cmdId) { + this.subscribersMap.delete(cmdId); + } + } + ); + this.reconnectSubscribers.delete(subscriber); + this.subscribersCount--; + this.publishCommands(); + } + } + + private nextCmdId(): number { + this.lastCmdId++; + return this.lastCmdId; + } + + private publishCommands() { + while (this.isOpened && this.cmdsWrapper.hasCommands()) { + this.dataStream.next(this.cmdsWrapper.preparePublishCommands(MAX_PUBLISH_COMMANDS)); + this.checkToClose(); + } + this.tryOpenSocket(); + } + + private checkToClose() { + if (this.subscribersCount === 0 && this.isOpened) { + if (!this.socketCloseTimer) { + this.socketCloseTimer = setTimeout( + () => this.closeSocket(), WS_IDLE_TIMEOUT); + } + } + } + + private reset(close: boolean) { + if (this.socketCloseTimer) { + clearTimeout(this.socketCloseTimer); + this.socketCloseTimer = null; + } + this.lastCmdId = 0; + this.subscribersMap.clear(); + this.subscribersCount = 0; + this.cmdsWrapper.clear(); + if (close) { + this.closeSocket(); + } + } + + private closeSocket() { + this.isActive = false; + if (this.isOpened) { + this.dataStream.unsubscribe(); + } + } + + private tryOpenSocket() { + if (this.isActive) { + if (!this.isOpened && !this.isOpening) { + this.isOpening = true; + if (AuthService.isJwtTokenValid()) { + this.openSocket(AuthService.getJwtToken()); + } else { + this.authService.refreshJwtToken().subscribe(() => { + this.openSocket(AuthService.getJwtToken()); + }, + () => { + this.isOpening = false; + this.authService.logout(true); + } + ); + } + } + if (this.socketCloseTimer) { + clearTimeout(this.socketCloseTimer); + this.socketCloseTimer = null; + } + } + } + + private openSocket(token: string) { + const uri = `${this.telemetryUri}?token=${token}`; + this.dataStream = webSocket( + { + url: uri, + openObserver: { + next: (e: Event) => { + this.onOpen(); + } + }, + closeObserver: { + next: (e: CloseEvent) => { + this.onClose(e); + } + } + } + ); + + this.dataStream.subscribe((message) => { + this.ngZone.runOutsideAngular(() => { + this.onMessage(message as WebsocketDataMsg); + }); + }, + (error) => { + this.onError(error); + }); + } + + private onOpen() { + this.isOpening = false; + this.isOpened = true; + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; + } + if (this.isReconnect) { + this.isReconnect = false; + this.reconnectSubscribers.forEach( + (reconnectSubscriber) => { + reconnectSubscriber.onReconnected(); + this.subscribe(reconnectSubscriber); + } + ); + this.reconnectSubscribers.clear(); + } else { + this.publishCommands(); + } + } + + private onMessage(message: WebsocketDataMsg) { + if (message.errorCode) { + this.showWsError(message.errorCode, message.errorMsg); + } else { + let subscriber: TelemetrySubscriber; + if (isEntityDataUpdateMsg(message)) { + subscriber = this.subscribersMap.get(message.cmdId); + if (subscriber) { + subscriber.onEntityData(new EntityDataUpdate(message)); + } + } else if (isAlarmDataUpdateMsg(message)) { + subscriber = this.subscribersMap.get(message.cmdId); + if (subscriber) { + subscriber.onAlarmData(new AlarmDataUpdate(message)); + } + } else if (message.subscriptionId) { + subscriber = this.subscribersMap.get(message.subscriptionId); + if (subscriber) { + subscriber.onData(new SubscriptionUpdate(message)); + } + } + } + this.checkToClose(); + } + + private onError(errorEvent) { + if (errorEvent) { + console.warn('WebSocket error event', errorEvent); + } + this.isOpening = false; + } + + private onClose(closeEvent: CloseEvent) { + if (closeEvent && closeEvent.code > 1001 && closeEvent.code !== 1006) { + this.showWsError(closeEvent.code, closeEvent.reason); + } + this.isOpening = false; + this.isOpened = false; + if (this.isActive) { + if (!this.isReconnect) { + this.reconnectSubscribers.clear(); + this.subscribersMap.forEach( + (subscriber) => { + this.reconnectSubscribers.add(subscriber); + } + ); + this.reset(false); + this.isReconnect = true; + } + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer); + } + this.reconnectTimer = setTimeout(() => this.tryOpenSocket(), RECONNECT_INTERVAL); + } + } + + private showWsError(errorCode: number, errorMsg: string) { + let message = errorMsg; + if (!message) { + message += `WebSocket Error: error code - ${errorCode}.`; + } + this.store.dispatch(new ActionNotificationShow( + { + message, type: 'error' + })); + } + +} diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts new file mode 100644 index 0000000000..7c56df331e --- /dev/null +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -0,0 +1,133 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import * as AngularAnimations from '@angular/animations'; +import * as AngularCore from '@angular/core'; +import * as AngularCommon from '@angular/common'; +import * as AngularForms from '@angular/forms'; +import * as AngularFlexLayout from '@angular/flex-layout'; +import * as AngularPlatformBrowser from '@angular/platform-browser'; +import * as AngularRouter from '@angular/router'; +import * as AngularCdkCoercion from '@angular/cdk/coercion'; +import * as AngularCdkCollections from '@angular/cdk/collections'; +import * as AngularCdkKeycodes from '@angular/cdk/keycodes'; +import * as AngularCdkLayout from '@angular/cdk/layout'; +import * as AngularCdkOverlay from '@angular/cdk/overlay'; +import * as AngularCdkPortal from '@angular/cdk/portal'; +import * as AngularMaterialAutocomplete from '@angular/material/autocomplete'; +import * as AngularMaterialBadge from '@angular/material/badge'; +import * as AngularMaterialBottomSheet from '@angular/material/bottom-sheet'; +import * as AngularMaterialButton from '@angular/material/button'; +import * as AngularMaterialButtonToggle from '@angular/material/button-toggle'; +import * as AngularMaterialCard from '@angular/material/card'; +import * as AngularMaterialCheckbox from '@angular/material/checkbox'; +import * as AngularMaterialChips from '@angular/material/chips'; +import * as AngularMaterialCore from '@angular/material/core'; +import * as AngularMaterialDatepicker from '@angular/material/datepicker'; +import * as AngularMaterialDialog from '@angular/material/dialog'; +import * as AngularMaterialDivider from '@angular/material/divider'; +import * as AngularMaterialExpansion from '@angular/material/expansion'; +import * as AngularMaterialFormField from '@angular/material/form-field'; +import * as AngularMaterialGridList from '@angular/material/grid-list'; +import * as AngularMaterialIcon from '@angular/material/icon'; +import * as AngularMaterialInput from '@angular/material/input'; +import * as AngularMaterialList from '@angular/material/list'; +import * as AngularMaterialMenu from '@angular/material/menu'; +import * as AngularMaterialPaginator from '@angular/material/paginator'; +import * as AngularMaterialProgressBar from '@angular/material/progress-bar'; +import * as AngularMaterialProgressSpinner from '@angular/material/progress-spinner'; +import * as AngularMaterialRadio from '@angular/material/radio'; +import * as AngularMaterialSelect from '@angular/material/select'; +import * as AngularMaterialSidenav from '@angular/material/sidenav'; +import * as AngularMaterialSlideToggle from '@angular/material/slide-toggle'; +import * as AngularMaterialSlider from '@angular/material/slider'; +import * as AngularMaterialSnackBar from '@angular/material/snack-bar'; +import * as AngularMaterialSort from '@angular/material/sort'; +import * as AngularMaterialStepper from '@angular/material/stepper'; +import * as AngularMaterialTable from '@angular/material/table'; +import * as AngularMaterialTabs from '@angular/material/tabs'; +import * as AngularMaterialToolbar from '@angular/material/toolbar'; +import * as AngularMaterialTooltip from '@angular/material/tooltip'; +import * as AngularMaterialTree from '@angular/material/tree'; +import * as NgrxStore from '@ngrx/store'; +import * as RxJs from 'rxjs'; +import * as RxJsOperators from 'rxjs/operators'; +import * as TranslateCore from '@ngx-translate/core'; +import * as TbCore from '@core/public-api'; +import * as TbShared from '@shared/public-api'; +import * as TbHomeComponents from '@home/components/public-api'; +import * as _moment from 'moment'; + +declare const SystemJS; + +export const modulesMap: {[key: string]: any} = { + '@angular/animations': SystemJS.newModule(AngularAnimations), + '@angular/core': SystemJS.newModule(AngularCore), + '@angular/common': SystemJS.newModule(AngularCommon), + '@angular/forms': SystemJS.newModule(AngularForms), + '@angular/flex-layout': SystemJS.newModule(AngularFlexLayout), + '@angular/platform-browser': SystemJS.newModule(AngularPlatformBrowser), + '@angular/router': SystemJS.newModule(AngularRouter), + '@angular/cdk/coercion': SystemJS.newModule(AngularCdkCoercion), + '@angular/cdk/collections': SystemJS.newModule(AngularCdkCollections), + '@angular/cdk/keycodes': SystemJS.newModule(AngularCdkKeycodes), + '@angular/cdk/layout': SystemJS.newModule(AngularCdkLayout), + '@angular/cdk/overlay': SystemJS.newModule(AngularCdkOverlay), + '@angular/cdk/portal': SystemJS.newModule(AngularCdkPortal), + '@angular/material/autocomplete': SystemJS.newModule(AngularMaterialAutocomplete), + '@angular/material/badge': SystemJS.newModule(AngularMaterialBadge), + '@angular/material/bottom-sheet': SystemJS.newModule(AngularMaterialBottomSheet), + '@angular/material/button': SystemJS.newModule(AngularMaterialButton), + '@angular/material/button-toggle': SystemJS.newModule(AngularMaterialButtonToggle), + '@angular/material/card': SystemJS.newModule(AngularMaterialCard), + '@angular/material/checkbox': SystemJS.newModule(AngularMaterialCheckbox), + '@angular/material/chips': SystemJS.newModule(AngularMaterialChips), + '@angular/material/core': SystemJS.newModule(AngularMaterialCore), + '@angular/material/datepicker': SystemJS.newModule(AngularMaterialDatepicker), + '@angular/material/dialog': SystemJS.newModule(AngularMaterialDialog), + '@angular/material/divider': SystemJS.newModule(AngularMaterialDivider), + '@angular/material/expansion': SystemJS.newModule(AngularMaterialExpansion), + '@angular/material/form-field': SystemJS.newModule(AngularMaterialFormField), + '@angular/material/grid-list': SystemJS.newModule(AngularMaterialGridList), + '@angular/material/icon': SystemJS.newModule(AngularMaterialIcon), + '@angular/material/input': SystemJS.newModule(AngularMaterialInput), + '@angular/material/list': SystemJS.newModule(AngularMaterialList), + '@angular/material/menu': SystemJS.newModule(AngularMaterialMenu), + '@angular/material/paginator': SystemJS.newModule(AngularMaterialPaginator), + '@angular/material/progress-bar': SystemJS.newModule(AngularMaterialProgressBar), + '@angular/material/progress-spinner': SystemJS.newModule(AngularMaterialProgressSpinner), + '@angular/material/radio': SystemJS.newModule(AngularMaterialRadio), + '@angular/material/select': SystemJS.newModule(AngularMaterialSelect), + '@angular/material/sidenav': SystemJS.newModule(AngularMaterialSidenav), + '@angular/material/slide-toggle': SystemJS.newModule(AngularMaterialSlideToggle), + '@angular/material/slider': SystemJS.newModule(AngularMaterialSlider), + '@angular/material/snack-bar': SystemJS.newModule(AngularMaterialSnackBar), + '@angular/material/sort': SystemJS.newModule(AngularMaterialSort), + '@angular/material/stepper': SystemJS.newModule(AngularMaterialStepper), + '@angular/material/table': SystemJS.newModule(AngularMaterialTable), + '@angular/material/tabs': SystemJS.newModule(AngularMaterialTabs), + '@angular/material/toolbar': SystemJS.newModule(AngularMaterialToolbar), + '@angular/material/tooltip': SystemJS.newModule(AngularMaterialTooltip), + '@angular/material/tree': SystemJS.newModule(AngularMaterialTree), + '@ngrx/store': SystemJS.newModule(NgrxStore), + rxjs: SystemJS.newModule(RxJs), + 'rxjs/operators': SystemJS.newModule(RxJsOperators), + '@ngx-translate/core': SystemJS.newModule(TranslateCore), + '@core/public-api': SystemJS.newModule(TbCore), + '@shared/public-api': SystemJS.newModule(TbShared), + '@home/components/public-api': SystemJS.newModule(TbHomeComponents), + moment: SystemJS.newModule(_moment) +}; diff --git a/ui-ngx/src/app/modules/dashboard/dashboard-pages.module.ts b/ui-ngx/src/app/modules/dashboard/dashboard-pages.module.ts new file mode 100644 index 0000000000..1857563116 --- /dev/null +++ b/ui-ngx/src/app/modules/dashboard/dashboard-pages.module.ts @@ -0,0 +1,35 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { HomeComponentsModule } from '@modules/home/components/home-components.module'; +import { HomeDialogsModule } from '@app/modules/home/dialogs/home-dialogs.module'; +import { DashboardModule } from '@home/pages/dashboard/dashboard.module'; +import { DashboardPagesRoutingModule } from './dashboard-pages.routing.module'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + HomeDialogsModule, + DashboardModule, + DashboardPagesRoutingModule + ] +}) +export class DashboardPagesModule { } diff --git a/ui-ngx/src/app/modules/dashboard/dashboard-pages.routing.module.ts b/ui-ngx/src/app/modules/dashboard/dashboard-pages.routing.module.ts new file mode 100644 index 0000000000..03022dc376 --- /dev/null +++ b/ui-ngx/src/app/modules/dashboard/dashboard-pages.routing.module.ts @@ -0,0 +1,104 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable, NgModule } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve, RouterModule, Routes } from '@angular/router'; + +import { Authority } from '@shared/models/authority.enum'; +import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.component'; +import { Dashboard } from '@app/shared/models/dashboard.models'; +import { DashboardService } from '@core/http/dashboard.service'; +import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; +import { DashboardResolver } from '@app/modules/home/pages/dashboard/dashboard-routing.module'; +import { UtilsService } from '@core/services/utils.service'; +import { Widget } from '@app/shared/models/widget.models'; +import { MODULES_MAP } from '../../shared/models/constants'; +import { modulesMap } from '../common/modules-map'; + +@Injectable() +export class WidgetEditorDashboardResolver implements Resolve { + + constructor(private dashboardService: DashboardService, + private dashboardUtils: DashboardUtilsService, + private utils: UtilsService) { + } + + resolve(route: ActivatedRouteSnapshot): Dashboard { + const editWidgetInfo = this.utils.editWidgetInfo; + const widget: Widget = { + isSystemType: true, + bundleAlias: 'customWidgetBundle', + typeAlias: 'customWidget', + type: editWidgetInfo.type, + title: 'My widget', + sizeX: editWidgetInfo.sizeX * 2, + sizeY: editWidgetInfo.sizeY * 2, + row: 2, + col: 4, + config: JSON.parse(editWidgetInfo.defaultConfig) + }; + widget.config.title = widget.config.title || editWidgetInfo.widgetName; + return this.dashboardUtils.createSingleWidgetDashboard(widget); + } +} + +const routes: Routes = [ + { + path: 'dashboard/:dashboardId', + component: DashboardPageComponent, + data: { + breadcrumb: { + skip: true + }, + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + title: 'dashboard.dashboard', + widgetEditMode: false, + singlePageMode: true + }, + resolve: { + dashboard: DashboardResolver + } + }, + { + path: 'widget-editor', + component: DashboardPageComponent, + data: { + breadcrumb: { + skip: true + }, + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], + title: 'widget.editor', + widgetEditMode: true, + singlePageMode: true + }, + resolve: { + dashboard: WidgetEditorDashboardResolver + } + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [ + WidgetEditorDashboardResolver, + { + provide: MODULES_MAP, + useValue: modulesMap + } + ] +}) +export class DashboardPagesRoutingModule { } diff --git a/ui-ngx/src/app/modules/dashboard/dashboard-routing.module.ts b/ui-ngx/src/app/modules/dashboard/dashboard-routing.module.ts new file mode 100644 index 0000000000..6f0c208596 --- /dev/null +++ b/ui-ngx/src/app/modules/dashboard/dashboard-routing.module.ts @@ -0,0 +1,43 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { AuthGuard } from '@core/guards/auth.guard'; +import { StoreModule } from '@ngrx/store'; + +const routes: Routes = [ + { path: '', + data: { + title: 'dashboard.dashboard', + breadcrumb: { + skip: true + } + }, + canActivate: [AuthGuard], + canActivateChild: [AuthGuard], + loadChildren: () => import('./dashboard-pages.module').then(m => m.DashboardPagesModule) + } +]; + +@NgModule({ + imports: [ + StoreModule, + RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class DashboardRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html new file mode 100644 index 0000000000..64d40df581 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html @@ -0,0 +1,119 @@ + +
+ +

{{ 'alarm.alarm-details' | translate }}

+ + +
+ + +
+
+
+
+ + alarm.created-time + + + + alarm.originator + + +
+
+ + alarm.start-time + + + + alarm.end-time + + + +
+
+ + alarm.ack-time + + + + alarm.clear-time + + + +
+
+ + alarm.type + + + + alarm.severity + + + + alarm.status + + +
+ + +
+
+
+ + +
+ + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.ts new file mode 100644 index 0000000000..cedbb36564 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.ts @@ -0,0 +1,154 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Observable, ReplaySubject } from 'rxjs'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { + AlarmInfo, + alarmSeverityColors, + alarmSeverityTranslations, + AlarmStatus, + alarmStatusTranslations +} from '@app/shared/models/alarm.models'; +import { AlarmService } from '@core/http/alarm.service'; +import { tap } from 'rxjs/operators'; +import { DatePipe } from '@angular/common'; +import { TranslateService } from '@ngx-translate/core'; + +export interface AlarmDetailsDialogData { + alarmId: string; + allowAcknowledgment: boolean; + allowClear: boolean; + displayDetails: boolean; +} + +@Component({ + selector: 'tb-alarm-details-dialog', + templateUrl: './alarm-details-dialog.component.html', + styleUrls: [] +}) +export class AlarmDetailsDialogComponent extends DialogComponent implements OnInit { + + alarmFormGroup: FormGroup; + + allowAcknowledgment: boolean; + allowClear: boolean; + displayDetails: boolean; + + loadAlarmSubject = new ReplaySubject(); + alarm$: Observable = this.loadAlarmSubject.asObservable().pipe( + tap(alarm => this.loadAlarmFields(alarm)) + ); + + alarmSeverityColorsMap = alarmSeverityColors; + alarmStatuses = AlarmStatus; + + alarmUpdated = false; + + constructor(protected store: Store, + protected router: Router, + private datePipe: DatePipe, + private translate: TranslateService, + @Inject(MAT_DIALOG_DATA) public data: AlarmDetailsDialogData, + private alarmService: AlarmService, + public dialogRef: MatDialogRef, + public fb: FormBuilder) { + super(store, router, dialogRef); + + this.allowAcknowledgment = data.allowAcknowledgment; + this.allowClear = data.allowClear; + this.displayDetails = data.displayDetails; + + this.alarmFormGroup = this.fb.group( + { + createdTime: [''], + originatorName: [''], + startTime: [''], + endTime: [''], + ackTime: [''], + clearTime: [''], + type: [''], + alarmSeverity: [''], + alarmStatus: [''], + alarmDetails: [null] + } + ); + + this.loadAlarm(); + + } + + loadAlarm() { + this.alarmService.getAlarmInfo(this.data.alarmId).subscribe( + alarm => this.loadAlarmSubject.next(alarm) + ); + } + + loadAlarmFields(alarm: AlarmInfo) { + this.alarmFormGroup.get('createdTime') + .patchValue(this.datePipe.transform(alarm.createdTime, 'yyyy-MM-dd HH:mm:ss')); + this.alarmFormGroup.get('originatorName') + .patchValue(alarm.originatorName); + if (alarm.startTs) { + this.alarmFormGroup.get('startTime') + .patchValue(this.datePipe.transform(alarm.startTs, 'yyyy-MM-dd HH:mm:ss')); + } + if (alarm.endTs) { + this.alarmFormGroup.get('endTime') + .patchValue(this.datePipe.transform(alarm.endTs, 'yyyy-MM-dd HH:mm:ss')); + } + if (alarm.ackTs) { + this.alarmFormGroup.get('ackTime') + .patchValue(this.datePipe.transform(alarm.ackTs, 'yyyy-MM-dd HH:mm:ss')); + } + if (alarm.clearTs) { + this.alarmFormGroup.get('clearTime') + .patchValue(this.datePipe.transform(alarm.clearTs, 'yyyy-MM-dd HH:mm:ss')); + } + this.alarmFormGroup.get('type').patchValue(alarm.type); + this.alarmFormGroup.get('alarmSeverity') + .patchValue(this.translate.instant(alarmSeverityTranslations.get(alarm.severity))); + this.alarmFormGroup.get('alarmStatus') + .patchValue(this.translate.instant(alarmStatusTranslations.get(alarm.status))); + this.alarmFormGroup.get('alarmDetails').patchValue(alarm.details); + } + + ngOnInit(): void { + } + + close(): void { + this.dialogRef.close(this.alarmUpdated); + } + + acknowledge(): void { + this.alarmService.ackAlarm(this.data.alarmId).subscribe( + () => { this.alarmUpdated = true; this.loadAlarm(); } + ); + } + + clear(): void { + this.alarmService.clearAlarm(this.data.alarmId).subscribe( + () => { this.alarmUpdated = true; this.loadAlarm(); } + ); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts new file mode 100644 index 0000000000..2b0d0629a2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts @@ -0,0 +1,133 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { EntityType, EntityTypeResource, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { Direction } from '@shared/models/page/sort-order'; +import { MatDialog } from '@angular/material/dialog'; +import { TimePageLink } from '@shared/models/page/page-link'; +import { Observable } from 'rxjs'; +import { PageData } from '@shared/models/page/page-data'; +import { EntityId } from '@shared/models/id/entity-id'; +import { + AlarmInfo, + AlarmQuery, + AlarmSearchStatus, + alarmSeverityColors, + alarmSeverityTranslations, + alarmStatusTranslations +} from '@app/shared/models/alarm.models'; +import { AlarmService } from '@app/core/http/alarm.service'; +import { DialogService } from '@core/services/dialog.service'; +import { AlarmTableHeaderComponent } from '@home/components/alarm/alarm-table-header.component'; +import { + AlarmDetailsDialogComponent, + AlarmDetailsDialogData +} from '@home/components/alarm/alarm-details-dialog.component'; + +export class AlarmTableConfig extends EntityTableConfig { + + searchStatus: AlarmSearchStatus; + + constructor(private alarmService: AlarmService, + private dialogService: DialogService, + private translate: TranslateService, + private datePipe: DatePipe, + private dialog: MatDialog, + public entityId: EntityId = null, + private defaultSearchStatus: AlarmSearchStatus = AlarmSearchStatus.ANY) { + super(); + this.loadDataOnInit = false; + this.tableTitle = ''; + this.useTimePageLink = true; + this.detailsPanelEnabled = false; + this.selectionEnabled = false; + this.searchEnabled = true; + this.addEnabled = false; + this.entitiesDeleteEnabled = false; + this.actionsColumnTitle = 'alarm.details'; + this.entityType = EntityType.ALARM; + this.entityTranslations = entityTypeTranslations.get(EntityType.ALARM); + this.entityResources = { + } as EntityTypeResource; + this.searchStatus = defaultSearchStatus; + + this.headerComponent = AlarmTableHeaderComponent; + + this.entitiesFetchFunction = pageLink => this.fetchAlarms(pageLink); + + this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; + + this.columns.push( + new DateEntityTableColumn('createdTime', 'alarm.created-time', this.datePipe, '150px')); + this.columns.push( + new EntityTableColumn('originatorName', 'alarm.originator', '25%', + (entity) => entity.originatorName, entity => ({}), false)); + this.columns.push( + new EntityTableColumn('type', 'alarm.type', '25%')); + this.columns.push( + new EntityTableColumn('severity', 'alarm.severity', '25%', + (entity) => this.translate.instant(alarmSeverityTranslations.get(entity.severity)), + entity => ({ + fontWeight: 'bold', + color: alarmSeverityColors.get(entity.severity) + }))); + this.columns.push( + new EntityTableColumn('status', 'alarm.status', '25%', + (entity) => this.translate.instant(alarmStatusTranslations.get(entity.status)))); + + this.cellActionDescriptors.push( + { + name: this.translate.instant('alarm.details'), + icon: 'more_horiz', + isEnabled: () => true, + onAction: ($event, entity) => this.showAlarmDetails(entity) + } + ); + } + + fetchAlarms(pageLink: TimePageLink): Observable> { + const query = new AlarmQuery(this.entityId, pageLink, this.searchStatus, null, true, null); + return this.alarmService.getAlarms(query); + } + + showAlarmDetails(entity: AlarmInfo) { + this.dialog.open + (AlarmDetailsDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + alarmId: entity.id.id, + allowAcknowledgment: true, + allowClear: true, + displayDetails: true + } + }).afterClosed().subscribe( + (res) => { + if (res) { + this.table.updateData(); + } + } + ); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.html new file mode 100644 index 0000000000..2ef88b055e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.html @@ -0,0 +1,26 @@ + + + alarm.alarm-status + + + {{ alarmSearchStatusTranslationsMap.get(alarmSearchStatusEnum[status]) | translate }} + + + diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.scss b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.scss new file mode 100644 index 0000000000..759a4bbf01 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.scss @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../scss/constants'; + +:host { + /* flex: 1; + display: flex; + justify-content: flex-start; */ + padding-right: 8px; + min-width: 160px; +} + +:host ::ng-deep { + mat-form-field { + font-size: 16px; + width: 200px; + + .mat-form-field-wrapper { + padding-bottom: 0; + } + + .mat-form-field-underline { + bottom: 0; + } + + @media #{$mat-xs} { + width: 100%; + + .mat-form-field-infix { + width: auto !important; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.ts new file mode 100644 index 0000000000..ff27e6cf8e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.ts @@ -0,0 +1,48 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTableHeaderComponent } from '../../components/entity/entity-table-header.component'; +import { AlarmInfo, AlarmSearchStatus, alarmSearchStatusTranslations } from '@shared/models/alarm.models'; +import { AlarmTableConfig } from './alarm-table-config'; + +@Component({ + selector: 'tb-alarm-table-header', + templateUrl: './alarm-table-header.component.html', + styleUrls: ['./alarm-table-header.component.scss'] +}) +export class AlarmTableHeaderComponent extends EntityTableHeaderComponent { + + alarmSearchStatusTranslationsMap = alarmSearchStatusTranslations; + + alarmSearchStatusTypes = Object.keys(AlarmSearchStatus); + alarmSearchStatusEnum = AlarmSearchStatus; + + get alarmTableConfig(): AlarmTableConfig { + return this.entitiesTableConfig as AlarmTableConfig; + } + + constructor(protected store: Store) { + super(store); + } + + searchStatusChanged(searchStatus: AlarmSearchStatus) { + this.alarmTableConfig.searchStatus = searchStatus; + this.alarmTableConfig.table.resetSortAndFilter(true, true); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.html new file mode 100644 index 0000000000..4688d680b3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.html @@ -0,0 +1,18 @@ + + diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.scss b/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.scss new file mode 100644 index 0000000000..719941ef46 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.scss @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host ::ng-deep { + tb-entities-table { + .mat-drawer-container { + background-color: white; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.ts new file mode 100644 index 0000000000..610aa261ff --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.ts @@ -0,0 +1,87 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Input, OnInit, ViewChild } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { MatDialog } from '@angular/material/dialog'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; +import { DialogService } from '@core/services/dialog.service'; +import { AlarmTableConfig } from './alarm-table-config'; +import { AlarmSearchStatus } from '@shared/models/alarm.models'; +import { AlarmService } from '@app/core/http/alarm.service'; + +@Component({ + selector: 'tb-alarm-table', + templateUrl: './alarm-table.component.html', + styleUrls: ['./alarm-table.component.scss'] +}) +export class AlarmTableComponent implements OnInit { + + activeValue = false; + dirtyValue = false; + entityIdValue: EntityId; + + @Input() + set active(active: boolean) { + if (this.activeValue !== active) { + this.activeValue = active; + if (this.activeValue && this.dirtyValue) { + this.dirtyValue = false; + this.entitiesTable.updateData(); + } + } + } + + @Input() + set entityId(entityId: EntityId) { + this.entityIdValue = entityId; + if (this.alarmTableConfig && this.alarmTableConfig.entityId !== entityId) { + this.alarmTableConfig.searchStatus = AlarmSearchStatus.ANY; + this.alarmTableConfig.entityId = entityId; + this.entitiesTable.resetSortAndFilter(this.activeValue); + if (!this.activeValue) { + this.dirtyValue = true; + } + } + } + + @ViewChild(EntitiesTableComponent, {static: true}) entitiesTable: EntitiesTableComponent; + + alarmTableConfig: AlarmTableConfig; + + constructor(private alarmService: AlarmService, + private dialogService: DialogService, + private translate: TranslateService, + private datePipe: DatePipe, + private dialog: MatDialog) { + } + + ngOnInit() { + this.dirtyValue = !this.activeValue; + this.alarmTableConfig = new AlarmTableConfig( + this.alarmService, + this.dialogService, + this.translate, + this.datePipe, + this.dialog, + this.entityIdValue, + AlarmSearchStatus.ANY + ); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.html new file mode 100644 index 0000000000..8553f284a8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.html @@ -0,0 +1,43 @@ + + + + + + + + + + + {{ translate.get('entity.no-entities-matching', {entity: searchText}) | async }} + + + + diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.ts new file mode 100644 index 0000000000..cbfaef1aff --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.ts @@ -0,0 +1,156 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Observable } from 'rxjs'; +import { map, mergeMap, startWith, tap } from 'rxjs/operators'; +import { PageData } from '@shared/models/page/page-data'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { EntityInfo } from '@shared/models/entity.models'; +import { EntityFilter } from '@shared/models/query/query.models'; +import { EntityService } from '@core/http/entity.service'; + +@Component({ + selector: 'tb-aliases-entity-autocomplete', + templateUrl: './aliases-entity-autocomplete.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AliasesEntityAutocompleteComponent), + multi: true + }] +}) +export class AliasesEntityAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit { + + selectEntityInfoFormGroup: FormGroup; + + modelValue: EntityInfo | null; + + @Input() + alias: string; + + @Input() + entityFilter: EntityFilter; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @ViewChild('entityInfoInput', {static: true}) entityInfoInput: ElementRef; + + filteredEntityInfos: Observable>; + + searchText = ''; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + public translate: TranslateService, + private entityService: EntityService, + private fb: FormBuilder) { + this.selectEntityInfoFormGroup = this.fb.group({ + entityInfo: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.filteredEntityInfos = this.selectEntityInfoFormGroup.get('entityInfo').valueChanges + .pipe( + tap(value => { + let modelValue; + if (typeof value === 'string' || !value) { + modelValue = null; + } else { + modelValue = value; + } + this.updateView(modelValue); + }), + startWith(''), + map(value => value ? (typeof value === 'string' ? value : value.name) : ''), + mergeMap(name => this.fetchEntityInfos(name) ) + ); + } + + ngAfterViewInit(): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: EntityInfo | null): void { + this.searchText = ''; + if (value != null) { + this.modelValue = value; + this.selectEntityInfoFormGroup.get('entityInfo').patchValue(value, {emitEvent: true}); + } else { + this.modelValue = null; + this.selectEntityInfoFormGroup.get('entityInfo').patchValue(null, {emitEvent: true}); + } + } + + updateView(value: EntityInfo | null) { + if (this.modelValue !== value) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + displayEntityInfoFn(entityInfo?: EntityInfo): string | undefined { + return entityInfo ? entityInfo.name : undefined; + } + + fetchEntityInfos(searchText?: string): Observable> { + this.searchText = searchText; + return this.getEntityInfos(this.searchText).pipe( + map(pageData => { + return pageData.data; + }) + ); + } + + getEntityInfos(searchText: string): Observable> { + return this.entityService.findEntityInfosByFilterAndName(this.entityFilter, searchText, {ignoreLoading: true}); + } + + clear() { + this.selectEntityInfoFormGroup.get('entityInfo').patchValue(null, {emitEvent: true}); + setTimeout(() => { + this.entityInfoInput.nativeElement.blur(); + this.entityInfoInput.nativeElement.focus(); + }, 0); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.html b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.html new file mode 100644 index 0000000000..a0beb194b5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.html @@ -0,0 +1,28 @@ + +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.scss b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.scss new file mode 100644 index 0000000000..845b959302 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.scss @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + min-width: 300px; + max-height: 150px; + overflow-x: hidden; + overflow-y: auto; + background: #fff; + border-radius: 4px; + box-shadow: + 0 7px 8px -4px rgba(0, 0, 0, .2), + 0 13px 19px 2px rgba(0, 0, 0, .14), + 0 5px 24px 4px rgba(0, 0, 0, .12); + + @media (min-height: 350px) { + max-height: 250px; + } + + .mat-content { + background-color: #fff; + } +} diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.ts b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.ts new file mode 100644 index 0000000000..c05d4b844d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.ts @@ -0,0 +1,46 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, InjectionToken } from '@angular/core'; +import { AliasInfo, IAliasController } from '@core/api/widget-api.models'; +import { EntityInfo } from '@shared/models/entity.models'; + +export const ALIASES_ENTITY_SELECT_PANEL_DATA = new InjectionToken('AliasesEntitySelectPanelData'); + +export interface AliasesEntitySelectPanelData { + aliasController: IAliasController; + entityAliasesInfo: {[aliasId: string]: AliasInfo}; +} + +@Component({ + selector: 'tb-aliases-entity-select-panel', + templateUrl: './aliases-entity-select-panel.component.html', + styleUrls: ['./aliases-entity-select-panel.component.scss'] +}) +export class AliasesEntitySelectPanelComponent { + + entityAliasesInfo: {[aliasId: string]: AliasInfo}; + + constructor(@Inject(ALIASES_ENTITY_SELECT_PANEL_DATA) public data: AliasesEntitySelectPanelData) { + this.entityAliasesInfo = this.data.entityAliasesInfo; + } + + public currentAliasEntityChanged(aliasId: string, selected: EntityInfo | null) { + if (selected) { + this.data.aliasController.updateCurrentAliasEntity(aliasId, selected); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.html b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.html new file mode 100644 index 0000000000..0353c12436 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.html @@ -0,0 +1,32 @@ + +
+ + + {{displayValue}} + +
diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.scss b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.scss new file mode 100644 index 0000000000..b406a542a6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.scss @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../../../scss/constants"; + +:host { + min-width: 52px; + + section.tb-aliases-entity-select { + min-height: 32px; + padding: 0 6px; + + @media #{$mat-lt-md} { + padding: 0; + } + + span { + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + pointer-events: all; + cursor: pointer; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts new file mode 100644 index 0000000000..a9a7fc026a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts @@ -0,0 +1,189 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Input, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; +import { TooltipPosition } from '@angular/material/tooltip'; +import { AliasInfo, IAliasController } from '@core/api/widget-api.models'; +import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { TranslateService } from '@ngx-translate/core'; +import { Subscription } from 'rxjs'; +import { BreakpointObserver } from '@angular/cdk/layout'; +import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; +import { + ALIASES_ENTITY_SELECT_PANEL_DATA, + AliasesEntitySelectPanelComponent, + AliasesEntitySelectPanelData +} from './aliases-entity-select-panel.component'; +import { deepClone } from '@core/utils'; +import { AliasFilterType } from '@shared/models/alias.models'; + +@Component({ + selector: 'tb-aliases-entity-select', + templateUrl: './aliases-entity-select.component.html', + styleUrls: ['./aliases-entity-select.component.scss'] +}) +export class AliasesEntitySelectComponent implements OnInit, OnDestroy { + + aliasControllerValue: IAliasController; + + @Input() + set aliasController(aliasController: IAliasController) { + this.aliasControllerValue = aliasController; + this.setupAliasController(this.aliasControllerValue); + } + + get aliasController(): IAliasController { + return this.aliasControllerValue; + } + + @Input() + tooltipPosition: TooltipPosition = 'above'; + + @Input() disabled: boolean; + + @ViewChild('aliasEntitySelectPanelOrigin') aliasEntitySelectPanelOrigin: CdkOverlayOrigin; + + displayValue: string; + entityAliasesInfo: {[aliasId: string]: AliasInfo} = {}; + hasSelectableAliasEntities = false; + + private rxSubscriptions = new Array(); + + constructor(private translate: TranslateService, + private overlay: Overlay, + private breakpointObserver: BreakpointObserver, + private viewContainerRef: ViewContainerRef) { + } + + private setupAliasController(aliasController: IAliasController) { + this.rxSubscriptions.forEach((subscription) => { + subscription.unsubscribe(); + }); + this.rxSubscriptions.length = 0; + if (aliasController) { + this.rxSubscriptions.push(aliasController.entityAliasesChanged.subscribe( + () => { + setTimeout(() => { + this.updateDisplayValue(); + this.updateEntityAliasesInfo(); + }, 0); + } + )); + this.rxSubscriptions.push(aliasController.entityAliasResolved.subscribe( + () => { + setTimeout(() => { + this.updateDisplayValue(); + this.updateEntityAliasesInfo(); + }, 0); + } + )); + } + } + + ngOnInit(): void { + } + + ngOnDestroy(): void { + this.rxSubscriptions.forEach((subscription) => { + subscription.unsubscribe(); + }); + this.rxSubscriptions.length = 0; + } + + openEditMode() { + if (this.disabled || !this.hasSelectableAliasEntities) { + return; + } + const position = this.overlay.position(); + const config = new OverlayConfig({ + panelClass: 'tb-aliases-entity-select-panel', + backdropClass: 'cdk-overlay-transparent-backdrop', + hasBackdrop: true, + }); + const connectedPosition: ConnectedPosition = { + originX: 'start', + originY: 'bottom', + overlayX: 'start', + overlayY: 'top' + }; + config.positionStrategy = position.flexibleConnectedTo(this.aliasEntitySelectPanelOrigin.elementRef) + .withPositions([connectedPosition]); + const overlayRef = this.overlay.create(config); + overlayRef.backdropClick().subscribe(() => { + overlayRef.dispose(); + }); + + const injector = this._createAliasesEntitySelectPanelInjector( + overlayRef, + { + aliasController: this.aliasController, + entityAliasesInfo: deepClone(this.entityAliasesInfo) + } + ); + overlayRef.attach(new ComponentPortal(AliasesEntitySelectPanelComponent, this.viewContainerRef, injector)); + } + + private _createAliasesEntitySelectPanelInjector(overlayRef: OverlayRef, data: AliasesEntitySelectPanelData): PortalInjector { + const injectionTokens = new WeakMap([ + [ALIASES_ENTITY_SELECT_PANEL_DATA, data], + [OverlayRef, overlayRef] + ]); + return new PortalInjector(this.viewContainerRef.injector, injectionTokens); + } + + private updateDisplayValue() { + let displayValue; + let singleValue = true; + let currentAliasId; + const entityAliases = this.aliasController.getEntityAliases(); + for (const aliasId of Object.keys(entityAliases)) { + const entityAlias = entityAliases[aliasId]; + if (!entityAlias.filter.resolveMultiple) { + const resolvedAlias = this.aliasController.getInstantAliasInfo(aliasId); + if (resolvedAlias && resolvedAlias.currentEntity) { + if (!currentAliasId) { + currentAliasId = aliasId; + } else { + singleValue = false; + break; + } + } + } + } + if (singleValue && currentAliasId) { + const aliasInfo = this.aliasController.getInstantAliasInfo(currentAliasId); + displayValue = aliasInfo.currentEntity.name; + } else { + displayValue = this.translate.instant('entity.entities'); + } + this.displayValue = displayValue; + } + + private updateEntityAliasesInfo() { + const allEntityAliases = this.aliasController.getEntityAliases(); + this.entityAliasesInfo = {}; + this.hasSelectableAliasEntities = false; + for (const aliasId of Object.keys(allEntityAliases)) { + const aliasInfo = this.aliasController.getInstantAliasInfo(aliasId); + if (aliasInfo && !aliasInfo.resolveMultiple && aliasInfo.currentEntity + && aliasInfo.entityFilter && aliasInfo.entityFilter.type !== AliasFilterType.singleEntity) { + this.entityAliasesInfo[aliasId] = deepClone(aliasInfo); + this.hasSelectableAliasEntities = true; + } + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html b/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html new file mode 100644 index 0000000000..bb89ff479e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html @@ -0,0 +1,74 @@ + +
+ +

{{ (isAdd ? 'alias.add' : 'alias.edit') | translate }}

+ + +
+ + +
+
+
+
+ + alias.name + + + {{ 'alias.name-required' | translate }} + + + {{ 'alias.duplicate-alias' | translate }} + + +
+ + + +
+
+ + + +
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.scss b/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.scss new file mode 100644 index 0000000000..6de3900a64 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.scss @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .tb-resolve-multiple-switch { + padding-left: 10px; + + .resolve-multiple-switch { + margin: 0; + } + + .resolve-multiple-label { + margin: 5px 0; + } + } +} \ No newline at end of file diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.ts new file mode 100644 index 0000000000..39c50f6115 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.ts @@ -0,0 +1,161 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + FormBuilder, + FormControl, + FormGroup, + FormGroupDirective, + NgForm, + ValidatorFn, + Validators +} from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { EntityAlias, EntityAliases } from '@shared/models/alias.models'; +import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { EntityService } from '@core/http/entity.service'; +import { Observable } from 'rxjs'; + +export interface EntityAliasDialogData { + isAdd: boolean; + allowedEntityTypes: Array; + entityAliases: EntityAliases | Array; + alias?: EntityAlias; +} + +@Component({ + selector: 'tb-entity-alias-dialog', + templateUrl: './entity-alias-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: EntityAliasDialogComponent}], + styleUrls: ['./entity-alias-dialog.component.scss'] +}) +export class EntityAliasDialogComponent extends DialogComponent + implements OnInit, ErrorStateMatcher { + + isAdd: boolean; + allowedEntityTypes: Array; + entityAliases: Array; + + alias: EntityAlias; + + entityAliasFormGroup: FormGroup; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: EntityAliasDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + private utils: UtilsService, + public translate: TranslateService, + private entityService: EntityService) { + super(store, router, dialogRef); + this.isAdd = data.isAdd; + this.allowedEntityTypes = data.allowedEntityTypes; + if (Array.isArray(data.entityAliases)) { + this.entityAliases = data.entityAliases; + } else { + this.entityAliases = []; + for (const aliasId of Object.keys(data.entityAliases)) { + this.entityAliases.push(data.entityAliases[aliasId]); + } + } + if (this.isAdd && !this.data.alias) { + this.alias = { + id: null, + alias: '', + filter: { + resolveMultiple: false + } + }; + } else { + this.alias = data.alias; + } + + this.entityAliasFormGroup = this.fb.group({ + alias: [this.alias.alias, [this.validateDuplicateAliasName(), Validators.required]], + resolveMultiple: [this.alias.filter.resolveMultiple], + filter: [this.alias.filter, Validators.required] + }); + } + + validateDuplicateAliasName(): ValidatorFn { + return (c: FormControl) => { + const newAlias = c.value.trim(); + const found = this.entityAliases.find((entityAlias) => entityAlias.alias === newAlias); + if (found) { + if (this.isAdd || this.alias.id !== found.id) { + return { + duplicateAliasName: { + valid: false + } + }; + } + } + return null; + }; + } + + ngOnInit(): void { + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + private validate(): Observable { + return this.entityService.resolveAliasFilter(this.alias.filter, null); + } + + save(): void { + this.submitted = true; + this.alias.alias = this.entityAliasFormGroup.get('alias').value.trim(); + this.alias.filter = this.entityAliasFormGroup.get('filter').value; + this.alias.filter.resolveMultiple = this.entityAliasFormGroup.get('resolveMultiple').value; + this.validate().subscribe(() => { + if (this.isAdd) { + this.alias.id = this.utils.guid(); + } + this.dialogRef.close(this.alias); + }, + () => { + this.entityAliasFormGroup.setErrors({ + noEntityMatched: true + }); + const changesSubscriptuion = this.entityAliasFormGroup.valueChanges.subscribe(() => { + this.entityAliasFormGroup.setErrors(null); + changesSubscriptuion.unsubscribe(); + }); + } + ); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html b/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html new file mode 100644 index 0000000000..c9624f6d25 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html @@ -0,0 +1,61 @@ + + + + + + + + + + +
+
+ entity.no-aliases-found +
+ + + {{ translate.get('entity.no-alias-matching', + {alias: truncate.transform(searchText, true, 6, '...')}) | async }} + + + + entity.create-new-alias + +
+
+
+ + {{ 'entity.alias-required' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.models.ts b/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.models.ts new file mode 100644 index 0000000000..bea4198cdd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.models.ts @@ -0,0 +1,23 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityType } from '@shared/models/entity-type.models'; +import { Observable } from 'rxjs'; +import { EntityAlias } from '@shared/models/alias.models'; + +export interface EntityAliasSelectCallbacks { + createEntityAlias: (alias: string, allowedEntityTypes: Array) => Observable; +} diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.scss b/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.scss new file mode 100644 index 0000000000..c14adfc443 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.scss @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + +} + +:host ::ng-deep { + .mat-form-field-infix { + border-top: none; + } +} diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.ts b/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.ts new file mode 100644 index 0000000000..57f367c127 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.ts @@ -0,0 +1,260 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, SkipSelf, ViewChild } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + FormGroupDirective, + NG_VALUE_ACCESSOR, + NgForm +} from '@angular/forms'; +import { Observable, of } from 'rxjs'; +import { map, mergeMap, share, tap } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { EntityType } from '@shared/models/entity-type.models'; +import { EntityService } from '@core/http/entity.service'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { EntityAlias } from '@shared/models/alias.models'; +import { IAliasController } from '@core/api/widget-api.models'; +import { TruncatePipe } from '@shared/pipe/truncate.pipe'; +import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete'; +import { EntityAliasSelectCallbacks } from './entity-alias-select.component.models'; +import { ENTER } from '@angular/cdk/keycodes'; +import { ErrorStateMatcher } from '@angular/material/core'; + +@Component({ + selector: 'tb-entity-alias-select', + templateUrl: './entity-alias-select.component.html', + styleUrls: ['./entity-alias-select.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntityAliasSelectComponent), + multi: true + }, + { + provide: ErrorStateMatcher, + useExisting: EntityAliasSelectComponent + }] +}) +export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, ErrorStateMatcher { + + selectEntityAliasFormGroup: FormGroup; + + modelValue: string | null; + + @Input() + aliasController: IAliasController; + + @Input() + allowedEntityTypes: Array; + + @Input() + callbacks: EntityAliasSelectCallbacks; + + @Input() + showLabel: boolean; + + @ViewChild('entityAliasAutocomplete') entityAliasAutocomplete: MatAutocomplete; + @ViewChild('autocomplete', { read: MatAutocompleteTrigger }) autoCompleteTrigger: MatAutocompleteTrigger; + + + private requiredValue: boolean; + get tbRequired(): boolean { + return this.requiredValue; + } + @Input() + set tbRequired(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @ViewChild('entityAliasInput', {static: true}) entityAliasInput: ElementRef; + + entityAliasList: Array = []; + + filteredEntityAliases: Observable>; + + searchText = ''; + + private dirty = false; + + private creatingEntityAlias = false; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + private entityService: EntityService, + public translate: TranslateService, + public truncate: TruncatePipe, + private fb: FormBuilder) { + this.selectEntityAliasFormGroup = this.fb.group({ + entityAlias: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + const entityAliases = this.aliasController.getEntityAliases(); + for (const aliasId of Object.keys(entityAliases)) { + if (this.allowedEntityTypes && this.allowedEntityTypes.length) { + if (!this.entityService.filterAliasByEntityTypes(entityAliases[aliasId], this.allowedEntityTypes)) { + continue; + } + } + this.entityAliasList.push(entityAliases[aliasId]); + } + + this.filteredEntityAliases = this.selectEntityAliasFormGroup.get('entityAlias').valueChanges + .pipe( + tap(value => { + let modelValue; + if (typeof value === 'string' || !value) { + modelValue = null; + } else { + modelValue = value; + } + this.updateView(modelValue); + if (value === null) { + this.clear(); + } + }), + map(value => value ? (typeof value === 'string' ? value : value.alias) : ''), + mergeMap(name => this.fetchEntityAliases(name) ), + share() + ); + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = this.tbRequired && !this.modelValue; + return originalErrorState || customErrorState; + } + + ngAfterViewInit(): void {} + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.selectEntityAliasFormGroup.disable({emitEvent: false}); + } else { + this.selectEntityAliasFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: string | null): void { + this.searchText = ''; + let entityAlias = null; + if (value != null) { + const entityAliases = this.aliasController.getEntityAliases(); + if (entityAliases[value]) { + entityAlias = entityAliases[value]; + } + } + if (entityAlias != null) { + this.modelValue = entityAlias.id; + this.selectEntityAliasFormGroup.get('entityAlias').patchValue(entityAlias, {emitEvent: false}); + } else { + this.modelValue = null; + this.selectEntityAliasFormGroup.get('entityAlias').patchValue('', {emitEvent: false}); + } + this.dirty = true; + } + + onFocus() { + if (this.dirty) { + this.selectEntityAliasFormGroup.get('entityAlias').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + updateView(value: EntityAlias | null) { + const aliasId = value ? value.id : null; + if (this.modelValue !== aliasId) { + this.modelValue = aliasId; + this.propagateChange(this.modelValue); + } + } + + displayEntityAliasFn(entityAlias?: EntityAlias): string | undefined { + return entityAlias ? entityAlias.alias : undefined; + } + + fetchEntityAliases(searchText?: string): Observable> { + this.searchText = searchText; + let result = this.entityAliasList; + if (searchText && searchText.length) { + result = this.entityAliasList.filter((entityAlias) => entityAlias.alias.toLowerCase().includes(searchText.toLowerCase())); + } + return of(result); + } + + clear(value: string = '') { + this.entityAliasInput.nativeElement.value = value; + this.selectEntityAliasFormGroup.get('entityAlias').patchValue(value, {emitEvent: true}); + setTimeout(() => { + this.entityAliasInput.nativeElement.blur(); + this.entityAliasInput.nativeElement.focus(); + }, 0); + } + + textIsNotEmpty(text: string): boolean { + return (text && text != null && text.length > 0) ? true : false; + } + + entityAliasEnter($event: KeyboardEvent) { + if ($event.keyCode === ENTER) { + $event.preventDefault(); + if (!this.modelValue) { + this.createEntityAlias($event, this.searchText); + } + } + } + + createEntityAlias($event: Event, alias: string) { + $event.preventDefault(); + this.creatingEntityAlias = true; + if (this.callbacks && this.callbacks.createEntityAlias) { + this.callbacks.createEntityAlias(alias, this.allowedEntityTypes).subscribe((newAlias) => { + if (!newAlias) { + setTimeout(() => { + this.entityAliasInput.nativeElement.blur(); + this.entityAliasInput.nativeElement.focus(); + }, 0); + } else { + this.entityAliasList.push(newAlias); + this.modelValue = newAlias.id; + this.selectEntityAliasFormGroup.get('entityAlias').patchValue(newAlias, {emitEvent: true}); + this.propagateChange(this.modelValue); + } + } + ); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.html b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.html new file mode 100644 index 0000000000..18f3e1a795 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.html @@ -0,0 +1,111 @@ + +
+ +

{{ title | translate }}

+ + +
+ + +
+
+
+ +
+ alias.name + alias.entity-filter + alias.resolve-multiple + +
+
+
+ +
+ {{$index + 1}}. +
+ + + + + {{ 'entity.alias-required' | translate }} + + + + +
+ + +
+ + +
+
+
+
+
+ + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.scss b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.scss new file mode 100644 index 0000000000..655af02692 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.scss @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + + .tb-aliases-header { + min-height: 40px; + padding: 0 11px; + margin: 5px; + + .tb-header-label { + font-size: 14px; + color: rgba(0, 0, 0, .570588); + } + } + + mat-divider{ + margin: -1px -24px; + } + + .tb-alias { + padding: 0 0 0 10px; + margin: 5px; + + .tb-resolve-multiple-switch { + padding-left: 10px; + + .resolve-multiple-switch { + margin: 0; + } + } + } +} + +:host ::ng-deep { + .mat-dialog-content { + padding-top: 0 !important; + padding-bottom: 0 !important; + } +} diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts new file mode 100644 index 0000000000..f6133314c8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts @@ -0,0 +1,270 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + AbstractControl, + FormArray, + FormBuilder, + FormControl, + FormGroup, + FormGroupDirective, + NgForm, + Validators +} from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { EntityAlias, EntityAliases, EntityAliasFilter } from '@shared/models/alias.models'; +import { DatasourceType, Widget, widgetType } from '@shared/models/widget.models'; +import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { DialogService } from '@core/services/dialog.service'; +import { deepClone, isUndefined } from '@core/utils'; +import { EntityAliasDialogComponent, EntityAliasDialogData } from './entity-alias-dialog.component'; + +export interface EntityAliasesDialogData { + entityAliases: EntityAliases; + widgets: Array; + isSingleEntityAlias?: boolean; + isSingleWidget?: boolean; + allowedEntityTypes?: Array; + disableAdd?: boolean; + singleEntityAlias?: EntityAlias; + customTitle?: string; +} + +@Component({ + selector: 'tb-entity-aliases-dialog', + templateUrl: './entity-aliases-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: EntityAliasesDialogComponent}], + styleUrls: ['./entity-aliases-dialog.component.scss'] +}) +export class EntityAliasesDialogComponent extends DialogComponent + implements OnInit, ErrorStateMatcher { + + title: string; + disableAdd: boolean; + allowedEntityTypes: Array; + + aliasToWidgetsMap: {[aliasId: string]: Array} = {}; + + entityAliasesFormGroup: FormGroup; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: EntityAliasesDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + private utils: UtilsService, + private translate: TranslateService, + private dialogs: DialogService, + private dialog: MatDialog) { + super(store, router, dialogRef); + this.title = data.customTitle ? data.customTitle : 'entity.aliases'; + this.disableAdd = this.data.disableAdd; + this.allowedEntityTypes = this.data.allowedEntityTypes; + + if (data.widgets) { + let widgetsTitleList: Array; + if (this.data.isSingleWidget && this.data.widgets.length === 1) { + const widget = this.data.widgets[0]; + widgetsTitleList = [widget.config.title]; + for (const aliasId of Object.keys(this.data.entityAliases)) { + this.aliasToWidgetsMap[aliasId] = widgetsTitleList; + } + } else { + this.data.widgets.forEach((widget) => { + if (widget.type === widgetType.rpc) { + if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) { + const targetDeviceAliasId = widget.config.targetDeviceAliasIds[0]; + widgetsTitleList = this.aliasToWidgetsMap[targetDeviceAliasId]; + if (!widgetsTitleList) { + widgetsTitleList = []; + this.aliasToWidgetsMap[targetDeviceAliasId] = widgetsTitleList; + } + widgetsTitleList.push(widget.config.title); + } + } else { + const datasources = this.utils.validateDatasources(widget.config.datasources); + datasources.forEach((datasource) => { + if (datasource.type === DatasourceType.entity && datasource.entityAliasId) { + widgetsTitleList = this.aliasToWidgetsMap[datasource.entityAliasId]; + if (!widgetsTitleList) { + widgetsTitleList = []; + this.aliasToWidgetsMap[datasource.entityAliasId] = widgetsTitleList; + } + widgetsTitleList.push(widget.config.title); + } + }); + } + }); + } + } + const entityAliasControls: Array = []; + for (const aliasId of Object.keys(this.data.entityAliases)) { + const entityAlias = this.data.entityAliases[aliasId]; + if (!entityAlias.filter) { + entityAlias.filter = { + resolveMultiple: false + }; + } + if (isUndefined(entityAlias.filter.resolveMultiple)) { + entityAlias.filter.resolveMultiple = false; + } + entityAliasControls.push(this.createEntityAliasFormControl(aliasId, entityAlias)); + } + + this.entityAliasesFormGroup = this.fb.group({ + entityAliases: this.fb.array(entityAliasControls) + }); + } + + private createEntityAliasFormControl(aliasId: string, entityAlias: EntityAlias): AbstractControl { + const aliasFormControl = this.fb.group({ + id: [aliasId], + alias: [entityAlias ? entityAlias.alias : null, [Validators.required]], + filter: [entityAlias ? entityAlias.filter : null], + resolveMultiple: [entityAlias ? entityAlias.filter.resolveMultiple : false] + }); + aliasFormControl.get('resolveMultiple').valueChanges.subscribe((resolveMultiple: boolean) => { + (aliasFormControl.get('filter').value as EntityAliasFilter).resolveMultiple = resolveMultiple; + }); + return aliasFormControl; + } + + + entityAliasesFormArray(): FormArray { + return this.entityAliasesFormGroup.get('entityAliases') as FormArray; + } + + ngOnInit(): void { + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + removeAlias(index: number) { + const entityAlias = (this.entityAliasesFormGroup.get('entityAliases').value as any[])[index]; + const widgetsTitleList = this.aliasToWidgetsMap[entityAlias.id]; + if (widgetsTitleList) { + let widgetsListHtml = ''; + for (const widgetTitle of widgetsTitleList) { + widgetsListHtml += '
\'' + widgetTitle + '\''; + } + const message = this.translate.instant('entity.unable-delete-entity-alias-text', + {entityAlias: entityAlias.alias, widgetsList: widgetsListHtml}); + this.dialogs.alert(this.translate.instant('entity.unable-delete-entity-alias-title'), + message, this.translate.instant('action.close'), true); + } else { + (this.entityAliasesFormGroup.get('entityAliases') as FormArray).removeAt(index); + this.entityAliasesFormGroup.markAsDirty(); + } + } + + public addAlias() { + this.openAliasDialog(-1); + } + + public editAlias(index: number) { + this.openAliasDialog(index); + } + + private openAliasDialog(index: number) { + const isAdd = index === -1; + let alias; + const aliasesArray = this.entityAliasesFormGroup.get('entityAliases').value as any[]; + if (!isAdd) { + alias = aliasesArray[index]; + } + this.dialog.open(EntityAliasDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + isAdd, + allowedEntityTypes: this.allowedEntityTypes, + entityAliases: aliasesArray, + alias: isAdd ? null : deepClone(alias) + } + }).afterClosed().subscribe((entityAlias) => { + if (entityAlias) { + if (isAdd) { + (this.entityAliasesFormGroup.get('entityAliases') as FormArray) + .push(this.createEntityAliasFormControl(entityAlias.id, entityAlias)); + } else { + const aliasFormControl = (this.entityAliasesFormGroup.get('entityAliases') as FormArray).at(index); + aliasFormControl.get('alias').patchValue(entityAlias.alias); + aliasFormControl.get('filter').patchValue(entityAlias.filter); + aliasFormControl.get('resolveMultiple').patchValue(entityAlias.filter.resolveMultiple); + } + this.entityAliasesFormGroup.markAsDirty(); + } + }); + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + const entityAliases: EntityAliases = {}; + const uniqueAliasList: {[alias: string]: string} = {}; + + let valid = true; + let message: string; + + const aliasesArray = this.entityAliasesFormGroup.get('entityAliases').value as any[]; + for (const aliasValue of aliasesArray) { + const aliasId: string = aliasValue.id; + const alias: string = aliasValue.alias.trim(); + const filter: EntityAliasFilter = aliasValue.filter; + if (uniqueAliasList[alias]) { + valid = false; + message = this.translate.instant('entity.duplicate-alias-error', {alias}); + break; + } else if (!filter || !filter.type) { + valid = false; + message = this.translate.instant('entity.missing-entity-filter-error', {alias}); + break; + } else { + uniqueAliasList[alias] = alias; + entityAliases[aliasId] = {id: aliasId, alias, filter}; + } + } + if (valid) { + this.dialogRef.close(entityAliases); + } else { + this.store.dispatch(new ActionNotificationShow( + { + message, + type: 'error' + })); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html new file mode 100644 index 0000000000..f815da49e7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html @@ -0,0 +1,59 @@ + +
+ +

{{ 'attribute.add' | translate }}

+ + +
+ + +
+
+
+ + attribute.key + + + {{ 'attribute.key-required' | translate }} + + + + +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.ts b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.ts new file mode 100644 index 0000000000..02b6fe063d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.ts @@ -0,0 +1,88 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { EntityId } from '@shared/models/id/entity-id'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models'; +import { AttributeService } from '@core/http/attribute.service'; + +export interface AddAttributeDialogData { + entityId: EntityId; + attributeScope: AttributeScope; +} + +@Component({ + selector: 'tb-add-attribute-dialog', + templateUrl: './add-attribute-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: AddAttributeDialogComponent}], + styleUrls: [] +}) +export class AddAttributeDialogComponent extends DialogComponent + implements OnInit, ErrorStateMatcher { + + attributeFormGroup: FormGroup; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AddAttributeDialogData, + private attributeService: AttributeService, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + public fb: FormBuilder) { + super(store, router, dialogRef); + } + + ngOnInit(): void { + this.attributeFormGroup = this.fb.group({ + key: ['', [Validators.required]], + value: [null, [Validators.required]] + }); + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(false); + } + + add(): void { + this.submitted = true; + const attribute: AttributeData = { + lastUpdateTs: null, + key: this.attributeFormGroup.get('key').value.trim(), + value: this.attributeFormGroup.get('value').value + }; + this.attributeService.saveEntityAttributes(this.data.entityId, + this.data.attributeScope, [attribute]).subscribe( + () => { + this.dialogRef.close(true); + } + ); + } +} diff --git a/ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.html b/ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.html new file mode 100644 index 0000000000..4899a893e5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.html @@ -0,0 +1,76 @@ + +
+ +

attribute.add-widget-to-dashboard

+ + +
+ + +
+
+ + +
+ dashboard.select-existing + + +
+
+ +
+ dashboard.create-new + + dashboard.new-dashboard-title + + + {{ 'dashboard.title-required' | translate }} + + +
+
+
+
+
+
+ + {{ 'dashboard.open-dashboard' | translate }} + + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.scss b/ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.scss new file mode 100644 index 0000000000..8e2ed04d6a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.scss @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host ::ng-deep { + mat-radio-button { + display: block; + margin-bottom: 16px; + .mat-radio-label { + width: 100%; + align-items: start; + .mat-radio-label-content { + width: 100%; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.ts b/ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.ts new file mode 100644 index 0000000000..d3b8c36286 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.ts @@ -0,0 +1,219 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { UtilsService } from '@core/services/utils.service'; +import { Dashboard, DashboardLayoutId } from '@app/shared/models/dashboard.models'; +import { objToBase64URI } from '@core/utils'; +import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; +import { EntityId } from '@app/shared/models/id/entity-id'; +import { Widget } from '@app/shared/models/widget.models'; +import { DashboardService } from '@core/http/dashboard.service'; +import { forkJoin, Observable, of } from 'rxjs'; +import { SelectTargetLayoutDialogComponent } from '@home/components/dashboard/select-target-layout-dialog.component'; +import { + SelectTargetStateDialogComponent, + SelectTargetStateDialogData +} from '@home/components/dashboard/select-target-state-dialog.component'; +import { mergeMap } from 'rxjs/operators'; +import { AliasesInfo } from '@shared/models/alias.models'; +import { ItemBufferService } from '@core/services/item-buffer.service'; +import { StateObject } from '@core/api/widget-api.models'; +import { FiltersInfo } from '@shared/models/query/query.models'; + +export interface AddWidgetToDashboardDialogData { + entityId: EntityId; + entityName: string; + widget: Widget; +} + +@Component({ + selector: 'tb-add-widget-to-dashboard-dialog', + templateUrl: './add-widget-to-dashboard-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: AddWidgetToDashboardDialogComponent}], + styleUrls: ['./add-widget-to-dashboard-dialog.component.scss'] +}) +export class AddWidgetToDashboardDialogComponent extends + DialogComponent + implements OnInit, ErrorStateMatcher { + + addWidgetFormGroup: FormGroup; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AddWidgetToDashboardDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + private utils: UtilsService, + private dashboardUtils: DashboardUtilsService, + private dashboardService: DashboardService, + private itembuffer: ItemBufferService, + private dialog: MatDialog) { + super(store, router, dialogRef); + + this.addWidgetFormGroup = this.fb.group( + { + addToDashboardType: [0, []], + dashboardId: [null, [Validators.required]], + newDashboardTitle: [{value: null, disabled: true}, []], + openDashboard: [false, []] + } + ); + + this.addWidgetFormGroup.get('addToDashboardType').valueChanges.subscribe( + (addToDashboardType: number) => { + if (addToDashboardType === 0) { + this.addWidgetFormGroup.get('dashboardId').setValidators([Validators.required]); + this.addWidgetFormGroup.get('dashboardId').enable(); + this.addWidgetFormGroup.get('newDashboardTitle').setValidators([]); + this.addWidgetFormGroup.get('newDashboardTitle').disable(); + this.addWidgetFormGroup.get('dashboardId').updateValueAndValidity(); + this.addWidgetFormGroup.get('newDashboardTitle').updateValueAndValidity(); + } else { + this.addWidgetFormGroup.get('dashboardId').setValidators([]); + this.addWidgetFormGroup.get('dashboardId').disable(); + this.addWidgetFormGroup.get('newDashboardTitle').setValidators([Validators.required]); + this.addWidgetFormGroup.get('newDashboardTitle').enable(); + this.addWidgetFormGroup.get('dashboardId').updateValueAndValidity(); + this.addWidgetFormGroup.get('newDashboardTitle').updateValueAndValidity(); + } + } + ); + } + + ngOnInit(): void { + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + add(): void { + this.submitted = true; + const addToDashboardType: number = this.addWidgetFormGroup.get('addToDashboardType').value; + if (addToDashboardType === 0) { + const dashboardId: string = this.addWidgetFormGroup.get('dashboardId').value; + this.dashboardService.getDashboard(dashboardId).pipe( + mergeMap((dashboard) => { + dashboard = this.dashboardUtils.validateAndUpdateDashboard(dashboard); + return this.selectTargetState(dashboard).pipe( + mergeMap((targetState) => { + return forkJoin([of(dashboard), of(targetState), this.selectTargetLayout(dashboard, targetState)]); + }) + ); + }) + ).subscribe((res) => { + this.addWidgetToDashboard(res[0], res[1], res[2]); + }); + } else { + const dashboardTitle: string = this.addWidgetFormGroup.get('newDashboardTitle').value; + const newDashboard: Dashboard = { + title: dashboardTitle + }; + this.addWidgetToDashboard(newDashboard, 'default', 'main'); + } + } + + private selectTargetState(dashboard: Dashboard): Observable { + const states = dashboard.configuration.states; + const stateIds = Object.keys(states); + if (stateIds.length > 1) { + return this.dialog.open(SelectTargetStateDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + states + } + }).afterClosed(); + } else { + return of(stateIds[0]); + } + } + + private selectTargetLayout(dashboard: Dashboard, targetState: string): Observable { + const layouts = dashboard.configuration.states[targetState].layouts; + const layoutIds = Object.keys(layouts); + if (layoutIds.length > 1) { + return this.dialog.open(SelectTargetLayoutDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] + }).afterClosed(); + } else { + return of(layoutIds[0] as DashboardLayoutId); + } + } + + private addWidgetToDashboard(dashboard: Dashboard, targetState: string, targetLayout: DashboardLayoutId) { + const aliasesInfo: AliasesInfo = { + datasourceAliases: {}, + targetDeviceAliases: {} + }; + aliasesInfo.datasourceAliases[0] = { + alias: this.data.entityName, + filter: this.dashboardUtils.createSingleEntityFilter(this.data.entityId) + }; + const filtersInfo: FiltersInfo = { + datasourceFilters: {} + }; + this.itembuffer.addWidgetToDashboard(dashboard, targetState, + targetLayout, this.data.widget, aliasesInfo, filtersInfo, null, null, + 48, null, -1, -1).pipe( + mergeMap((theDashboard) => { + return this.dashboardService.saveDashboard(theDashboard); + }) + ).subscribe( + (theDashboard) => { + const openDashboard: boolean = this.addWidgetFormGroup.get('openDashboard').value; + this.dialogRef.close(); + if (openDashboard) { + let url; + const stateIds = Object.keys(dashboard.configuration.states); + const stateIndex = stateIds.indexOf(targetState); + if (stateIndex > 0) { + const stateObject: StateObject = { + id: targetState, + params: {} + }; + const state = objToBase64URI([ stateObject ]); + url = `/dashboards/${theDashboard.id.id}?state=${state}`; + } else { + url = `/dashboards/${theDashboard.id.id}`; + } + const urlTree = this.router.parseUrl(url); + this.router.navigateByUrl(url); + } + } + ); + } +} diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html new file mode 100644 index 0000000000..918227fc3e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html @@ -0,0 +1,256 @@ + +
+
+ +
+
+ {{telemetryTypeTranslationsMap.get(attributeScope) | translate}} + + attribute.attributes-scope + + + {{ telemetryTypeTranslationsMap.get(toTelemetryTypeFunc(scope)) | translate }} + + + +
+ + + + +
+
+ +
+ + +   + + + +
+
+ +
+ + {{ (attributeScope === latestTelemetryTypes.LATEST_TELEMETRY ? + 'attribute.selected-telemetry' : 'attribute.selected-attributes') | translate:{count: dataSource.selection.selected.length} }} + + + + +
+
+ +
+
+ {{ 'widgets-bundle.current' | translate }} + + +
+ + +
+
+
+ + + + + + + + + + + + + {{ 'attribute.last-update-time' | translate }} + + {{ attribute.lastUpdateTs | date:'yyyy-MM-dd HH:mm:ss' }} + + + + {{ 'attribute.key' | translate }} + + {{ attribute.key }} + + + + {{ 'attribute.value' | translate }} + +
+ {{attribute.value | tbJson}} + + edit + +
+
+
+ + +
+ {{ + attributeScope === latestTelemetryTypes.LATEST_TELEMETRY + ? 'attribute.no-telemetry-text' + : 'attribute.no-attributes-text' + }} +
+ + + +
+
+ + +
+
+ + + + + + + + + +
+
+ +
+ widgets-bundle.empty + widget.select-widgets-bundle +
+
diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss new file mode 100644 index 0000000000..b8b237ec13 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss @@ -0,0 +1,144 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../scss/constants'; + +:host { + width: 100%; + height: 100%; + .tb-entity-table { + .tb-entity-table-content { + width: 100%; + height: 100%; + background: #fff; + + .mat-toolbar-tools{ + min-height: auto; + } + + .title-container{ + overflow: hidden; + } + + .tb-entity-table-title { + padding-right: 20px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .table-container { + overflow: auto; + } + + .tb-entity-table-info{ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .button-widget-action{ + margin-left: auto; + overflow: hidden; + text-overflow: ellipsis; + } + } + } + + @media #{$mat-xs} { + .mat-toolbar { + height: auto; + min-height: 100px; + + .tb-entity-table-title{ + padding-bottom: 5px; + width: 100%; + } + } + } +} + +:host ::ng-deep { + .mat-sort-header-sorted .mat-sort-header-arrow { + opacity: 1 !important; + } + mat-form-field.tb-attribute-scope { + font-size: 16px; + width: 200px; + + .mat-form-field-wrapper { + padding-bottom: 0; + } + + .mat-form-field-underline { + bottom: 0; + } + + @media #{$mat-xs} { + width: 100%; + + .mat-form-field-infix { + width: auto !important; + } + } + } + mat-cell.tb-value-cell { + cursor: pointer; + mat-icon { + height: 24px; + width: 24px; + font-size: 24px; + color: #757575 + } + } + + .carousel { + .aniT { + transition: all 1s linear; + } + + .transition { + transition: all 0.3s ease-in-out !important; + } + .content { + display: flex; + height: 100%; + .item { + position: relative; + } + } + .direction { + width: 60px; + } + .ball { + width: 10px; + height: 10px; + border-radius: 50%; + background: black; + border: 2px solid; + opacity: 0.5; + + &.visible { + opacity: 1; + } + } + ul.indicators { + pointer-events: none; + li { + pointer-events: all; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts new file mode 100644 index 0000000000..55506c7cc2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts @@ -0,0 +1,507 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + ElementRef, + Input, + NgZone, + OnInit, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { PageLink } from '@shared/models/page/page-link'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { MatDialog } from '@angular/material/dialog'; +import { DialogService } from '@core/services/dialog.service'; +import { Direction, SortOrder } from '@shared/models/page/sort-order'; +import { fromEvent, merge } from 'rxjs'; +import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; +import { EntityId } from '@shared/models/id/entity-id'; +import { + AttributeData, + AttributeScope, + DataKeyType, + isClientSideTelemetryType, + LatestTelemetry, + TelemetryType, + telemetryTypeTranslations, + toTelemetryType +} from '@shared/models/telemetry/telemetry.models'; +import { AttributeDatasource } from '@home/models/datasource/attribute-datasource'; +import { AttributeService } from '@app/core/http/attribute.service'; +import { EntityType } from '@shared/models/entity-type.models'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { + AddAttributeDialogComponent, + AddAttributeDialogData +} from '@home/components/attribute/add-attribute-dialog.component'; +import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { + EDIT_ATTRIBUTE_VALUE_PANEL_DATA, + EditAttributeValuePanelComponent, + EditAttributeValuePanelData +} from './edit-attribute-value-panel.component'; +import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; +import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service'; +import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; +import { DataKey, Datasource, DatasourceType, Widget, widgetType } from '@shared/models/widget.models'; +import { IAliasController, IStateController, StateParams } from '@core/api/widget-api.models'; +import { AliasController } from '@core/api/alias-controller'; +import { EntityAlias, EntityAliases } from '@shared/models/alias.models'; +import { UtilsService } from '@core/services/utils.service'; +import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { WidgetService } from '@core/http/widget.service'; +import { toWidgetInfo } from '../../models/widget-component.models'; +import { EntityService } from '@core/http/entity.service'; +import { + AddWidgetToDashboardDialogComponent, + AddWidgetToDashboardDialogData +} from '@home/components/attribute/add-widget-to-dashboard-dialog.component'; +import { deepClone } from '@core/utils'; +import { Filters } from '@shared/models/query/query.models'; + + +@Component({ + selector: 'tb-attribute-table', + templateUrl: './attribute-table.component.html', + styleUrls: ['./attribute-table.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AttributeTableComponent extends PageComponent implements AfterViewInit, OnInit { + + telemetryTypeTranslationsMap = telemetryTypeTranslations; + isClientSideTelemetryTypeMap = isClientSideTelemetryType; + + latestTelemetryTypes = LatestTelemetry; + + mode: 'default' | 'widget' = 'default'; + + attributeScopes: Array = []; + attributeScope: TelemetryType; + toTelemetryTypeFunc = toTelemetryType; + + displayedColumns = ['select', 'lastUpdateTs', 'key', 'value']; + pageLink: PageLink; + textSearchMode = false; + dataSource: AttributeDatasource; + + activeValue = false; + dirtyValue = false; + entityIdValue: EntityId; + + attributeScopeSelectionReadonly = false; + + viewsInited = false; + + selectedWidgetsBundleAlias: string = null; + widgetsBundle: WidgetsBundle = null; + widgetsLoaded = false; + widgetsCarouselIndex = 0; + widgetsList: Array> = []; + widgetsListCache: Array> = []; + aliasController: IAliasController; + private widgetDatasource: Datasource; + + private disableAttributeScopeSelectionValue: boolean; + get disableAttributeScopeSelection(): boolean { + return this.disableAttributeScopeSelectionValue; + } + @Input() + set disableAttributeScopeSelection(value: boolean) { + this.disableAttributeScopeSelectionValue = coerceBooleanProperty(value); + } + + @Input() + defaultAttributeScope: TelemetryType; + + @Input() + set active(active: boolean) { + if (this.activeValue !== active) { + this.activeValue = active; + if (this.activeValue && this.dirtyValue) { + this.dirtyValue = false; + if (this.viewsInited) { + this.updateData(true); + } + } + } + } + + @Input() + set entityId(entityId: EntityId) { + if (this.entityIdValue !== entityId) { + this.entityIdValue = entityId; + this.resetSortAndFilter(this.activeValue); + if (!this.activeValue) { + this.dirtyValue = true; + } + } + } + + @Input() + entityName: string; + + @ViewChild('searchInput') searchInputField: ElementRef; + + @ViewChild(MatPaginator) paginator: MatPaginator; + @ViewChild(MatSort) sort: MatSort; + + constructor(protected store: Store, + private attributeService: AttributeService, + private telemetryWsService: TelemetryWebsocketService, + public translate: TranslateService, + public dialog: MatDialog, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, + private dialogService: DialogService, + private entityService: EntityService, + private utils: UtilsService, + private dashboardUtils: DashboardUtilsService, + private widgetService: WidgetService, + private zone: NgZone) { + super(store); + this.dirtyValue = !this.activeValue; + const sortOrder: SortOrder = { property: 'key', direction: Direction.ASC }; + this.pageLink = new PageLink(10, 0, null, sortOrder); + this.dataSource = new AttributeDatasource(this.attributeService, this.telemetryWsService, this.zone, this.translate); + } + + ngOnInit() { + } + + attributeScopeChanged(attributeScope: TelemetryType) { + this.attributeScope = attributeScope; + this.mode = 'default'; + this.updateData(true); + } + + ngAfterViewInit() { + + fromEvent(this.searchInputField.nativeElement, 'keyup') + .pipe( + debounceTime(150), + distinctUntilChanged(), + tap(() => { + this.paginator.pageIndex = 0; + this.updateData(); + }) + ) + .subscribe(); + + this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); + + merge(this.sort.sortChange, this.paginator.page) + .pipe( + tap(() => this.updateData()) + ) + .subscribe(); + + this.viewsInited = true; + if (this.activeValue && this.entityIdValue) { + this.updateData(true); + } + } + + updateData(reload: boolean = false) { + this.pageLink.page = this.paginator.pageIndex; + this.pageLink.pageSize = this.paginator.pageSize; + this.pageLink.sortOrder.property = this.sort.active; + this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; + this.dataSource.loadAttributes(this.entityIdValue, this.attributeScope, this.pageLink, reload); + } + + enterFilterMode() { + this.textSearchMode = true; + this.pageLink.textSearch = ''; + setTimeout(() => { + this.searchInputField.nativeElement.focus(); + this.searchInputField.nativeElement.setSelectionRange(0, 0); + }, 10); + } + + exitFilterMode() { + this.textSearchMode = false; + this.pageLink.textSearch = null; + this.paginator.pageIndex = 0; + this.updateData(); + } + + resetSortAndFilter(update: boolean = true) { + const entityType = this.entityIdValue.entityType; + if (entityType === EntityType.DEVICE || entityType === EntityType.ENTITY_VIEW) { + this.attributeScopes = Object.keys(AttributeScope); + this.attributeScopeSelectionReadonly = false; + } else { + this.attributeScopes = [AttributeScope.SERVER_SCOPE]; + this.attributeScopeSelectionReadonly = true; + } + this.mode = 'default'; + this.attributeScope = this.defaultAttributeScope; + this.pageLink.textSearch = null; + if (this.viewsInited) { + this.paginator.pageIndex = 0; + const sortable = this.sort.sortables.get('key'); + this.sort.active = sortable.id; + this.sort.direction = 'asc'; + if (update) { + this.updateData(true); + } + } + } + + reloadAttributes() { + this.updateData(true); + } + + addAttribute($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(AddAttributeDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + entityId: this.entityIdValue, + attributeScope: this.attributeScope as AttributeScope + } + }).afterClosed().subscribe( + (res) => { + if (res) { + this.reloadAttributes(); + } + } + ); + } + + editAttribute($event: Event, attribute: AttributeData) { + if ($event) { + $event.stopPropagation(); + } + if (this.isClientSideTelemetryTypeMap.get(this.attributeScope)) { + return; + } + const target = $event.target || $event.srcElement || $event.currentTarget; + const config = new OverlayConfig(); + config.backdropClass = 'cdk-overlay-transparent-backdrop'; + config.hasBackdrop = true; + const connectedPosition: ConnectedPosition = { + originX: 'end', + originY: 'center', + overlayX: 'end', + overlayY: 'center' + }; + config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) + .withPositions([connectedPosition]); + + const overlayRef = this.overlay.create(config); + overlayRef.backdropClick().subscribe(() => { + overlayRef.dispose(); + }); + const injectionTokens = new WeakMap([ + [EDIT_ATTRIBUTE_VALUE_PANEL_DATA, { + attributeValue: attribute.value + } as EditAttributeValuePanelData], + [OverlayRef, overlayRef] + ]); + const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens); + const componentRef = overlayRef.attach(new ComponentPortal(EditAttributeValuePanelComponent, + this.viewContainerRef, injector)); + componentRef.onDestroy(() => { + if (componentRef.instance.result !== null) { + const attributeValue = componentRef.instance.result; + const updatedAttribute = {...attribute}; + updatedAttribute.value = attributeValue; + this.attributeService.saveEntityAttributes(this.entityIdValue, + this.attributeScope as AttributeScope, [updatedAttribute]).subscribe( + () => { + this.reloadAttributes(); + } + ); + } + }); + } + + deleteAttributes($event: Event) { + if ($event) { + $event.stopPropagation(); + } + if (this.dataSource.selection.selected.length > 0) { + this.dialogService.confirm( + this.translate.instant('attribute.delete-attributes-title', {count: this.dataSource.selection.selected.length}), + this.translate.instant('attribute.delete-attributes-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((result) => { + if (result) { + this.attributeService.deleteEntityAttributes(this.entityIdValue, + this.attributeScope as AttributeScope, this.dataSource.selection.selected).subscribe( + () => { + this.reloadAttributes(); + } + ); + } + }); + } + } + + enterWidgetMode() { + this.mode = 'widget'; + this.widgetsList = []; + this.widgetsListCache = []; + this.widgetsLoaded = false; + this.widgetsCarouselIndex = 0; + this.widgetsBundle = null; + this.selectedWidgetsBundleAlias = 'cards'; + + const entityAlias: EntityAlias = { + id: this.utils.guid(), + alias: this.entityName, + filter: this.dashboardUtils.createSingleEntityFilter(this.entityIdValue) + }; + const entitiAliases: EntityAliases = {}; + entitiAliases[entityAlias.id] = entityAlias; + + // @ts-ignore + const stateController: IStateController = { + getStateParams(): StateParams { + return {}; + } + }; + + const filters: Filters = {}; + + this.aliasController = new AliasController(this.utils, + this.entityService, + () => stateController, entitiAliases, filters); + + const dataKeyType: DataKeyType = this.attributeScope === LatestTelemetry.LATEST_TELEMETRY ? + DataKeyType.timeseries : DataKeyType.attribute; + + this.widgetDatasource = { + type: DatasourceType.entity, + entityAliasId: entityAlias.id, + dataKeys: [] + }; + + for (let i = 0; i < this.dataSource.selection.selected.length; i++) { + const attribute = this.dataSource.selection.selected[i]; + const dataKey: DataKey = { + name: attribute.key, + label: attribute.key, + type: dataKeyType, + color: this.utils.getMaterialColor(i), + settings: {}, + _hash: Math.random() + }; + this.widgetDatasource.dataKeys.push(dataKey); + } + } + + onWidgetsCarouselIndexChanged() { + if (this.mode === 'widget') { + for (let i = 0; i < this.widgetsList.length; i++) { + this.widgetsList[i].splice(0, this.widgetsList[i].length); + if (i === this.widgetsCarouselIndex) { + this.widgetsList[i].push(this.widgetsListCache[i][0]); + } + } + } + } + + onWidgetsBundleChanged() { + if (this.mode === 'widget') { + this.widgetsList = []; + this.widgetsListCache = []; + this.widgetsCarouselIndex = 0; + if (this.widgetsBundle) { + this.widgetsLoaded = false; + const bundleAlias = this.widgetsBundle.alias; + const isSystem = this.widgetsBundle.tenantId.id === NULL_UUID; + this.widgetService.getBundleWidgetTypes(bundleAlias, isSystem).subscribe( + (widgetTypes) => { + widgetTypes = widgetTypes.sort((a, b) => { + let result = widgetType[b.descriptor.type].localeCompare(widgetType[a.descriptor.type]); + if (result === 0) { + result = b.createdTime - a.createdTime; + } + return result; + }); + for (const type of widgetTypes) { + const widgetInfo = toWidgetInfo(type); + if (widgetInfo.type !== widgetType.static) { + const sizeX = widgetInfo.sizeX * 2; + const sizeY = widgetInfo.sizeY * 2; + const col = Math.floor(Math.max(0, (20 - sizeX) / 2)); + const widget: Widget = { + isSystemType: isSystem, + bundleAlias, + typeAlias: widgetInfo.alias, + type: widgetInfo.type, + title: widgetInfo.widgetName, + sizeX, + sizeY, + row: 0, + col, + config: JSON.parse(widgetInfo.defaultConfig) + }; + widget.config.title = widgetInfo.widgetName; + widget.config.datasources = [this.widgetDatasource]; + if ((this.attributeScope === LatestTelemetry.LATEST_TELEMETRY && widgetInfo.type !== widgetType.rpc) || + widgetInfo.type === widgetType.latest) { + const length = this.widgetsListCache.push([widget]); + this.widgetsList.push(length === 1 ? [widget] : []); + } + } + } + this.widgetsLoaded = true; + } + ); + } + } + } + + addWidgetToDashboard() { + if (this.mode === 'widget' && this.widgetsListCache.length > 0) { + const widget = this.widgetsListCache[this.widgetsCarouselIndex][0]; + this.dialog.open + (AddWidgetToDashboardDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + entityId: this.entityIdValue, + entityName: this.entityName, + widget: deepClone(widget) + } + }).afterClosed(); + } + } + + exitWidgetMode() { + this.selectedWidgetsBundleAlias = null; + this.mode = 'default'; + } + +} diff --git a/ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.html b/ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.html new file mode 100644 index 0000000000..fe48694983 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.html @@ -0,0 +1,40 @@ + +
+
+ + +
+
+ + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.scss b/ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.scss new file mode 100644 index 0000000000..79792e2cd5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.scss @@ -0,0 +1,19 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + background-color: #f9f9f9; +} + diff --git a/ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.ts b/ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.ts new file mode 100644 index 0000000000..a7f82129c9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.ts @@ -0,0 +1,74 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, InjectionToken, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { OverlayRef } from '@angular/cdk/overlay'; + +export const EDIT_ATTRIBUTE_VALUE_PANEL_DATA = new InjectionToken('EditAttributeValuePanelData'); + +export interface EditAttributeValuePanelData { + attributeValue: any; +} + +@Component({ + selector: 'tb-edit-attribute-value-panel', + templateUrl: './edit-attribute-value-panel.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: EditAttributeValuePanelComponent}], + styleUrls: ['./edit-attribute-value-panel.component.scss'] +}) +export class EditAttributeValuePanelComponent extends PageComponent implements OnInit, ErrorStateMatcher { + + attributeFormGroup: FormGroup; + + result: any = null; + + submitted = false; + + constructor(protected store: Store, + @Inject(EDIT_ATTRIBUTE_VALUE_PANEL_DATA) public data: EditAttributeValuePanelData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public overlayRef: OverlayRef, + public fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.attributeFormGroup = this.fb.group({ + value: [this.data.attributeValue, [Validators.required]] + }); + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.overlayRef.dispose(); + } + + update(): void { + this.submitted = true; + this.result = this.attributeFormGroup.get('value').value; + this.overlayRef.dispose(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.html b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.html new file mode 100644 index 0000000000..3589e465fe --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.html @@ -0,0 +1,43 @@ + + +

audit-log.audit-log-details

+ + +
+
+ +
+
+ + +
+
+
+
+ +
diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.scss b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.scss new file mode 100644 index 0000000000..d984517821 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.scss @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .tb-audit-log-action-data, + .tb-audit-log-failure-details { + width: 100%; + min-width: 400px; + height: 100%; + min-height: 50px; + border: 1px solid #c0c0c0; + } +} diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.ts b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.ts new file mode 100644 index 0000000000..bdf7073e3e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.ts @@ -0,0 +1,116 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, Inject, OnInit, Renderer2, ViewChild } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ActionStatus, AuditLog } from '@shared/models/audit-log.models'; + +import * as ace from 'ace-builds'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; + +export interface AuditLogDetailsDialogData { + auditLog: AuditLog; +} + +@Component({ + selector: 'tb-audit-log-details-dialog', + templateUrl: './audit-log-details-dialog.component.html', + styleUrls: ['./audit-log-details-dialog.component.scss'] +}) +export class AuditLogDetailsDialogComponent extends DialogComponent implements OnInit { + + @ViewChild('actionDataEditor', {static: true}) + actionDataEditorElmRef: ElementRef; + private actionDataEditor: ace.Ace.Editor; + + @ViewChild('failureDetailsEditor', {static: true}) + failureDetailsEditorElmRef: ElementRef; + private failureDetailsEditor: ace.Ace.Editor; + + auditLog: AuditLog; + displayFailureDetails: boolean; + actionData: string; + actionFailureDetails: string; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AuditLogDetailsDialogData, + public dialogRef: MatDialogRef, + private renderer: Renderer2) { + super(store, router, dialogRef); + } + + ngOnInit(): void { + this.auditLog = this.data.auditLog; + this.displayFailureDetails = this.auditLog.actionStatus === ActionStatus.FAILURE; + this.actionData = this.auditLog.actionData ? JSON.stringify(this.auditLog.actionData, null, 2) : ''; + this.actionFailureDetails = this.auditLog.actionFailureDetails; + + this.actionDataEditor = this.createEditor(this.actionDataEditorElmRef, this.actionData); + if (this.displayFailureDetails) { + this.failureDetailsEditor = this.createEditor(this.failureDetailsEditorElmRef, this.actionFailureDetails); + } + } + + createEditor(editorElementRef: ElementRef, content: string): ace.Ace.Editor { + const editorElement = editorElementRef.nativeElement; + let editorOptions: Partial = { + mode: 'ace/mode/java', + theme: 'ace/theme/github', + showGutter: false, + showPrintMargin: false, + readOnly: true + }; + + const advancedOptions = { + enableSnippets: false, + enableBasicAutocompletion: false, + enableLiveAutocompletion: false + }; + + editorOptions = {...editorOptions, ...advancedOptions}; + const editor = ace.edit(editorElement, editorOptions); + editor.session.setUseWrapMode(false); + editor.setValue(content, -1); + this.updateEditorSize(editorElement, content, editor); + return editor; + } + + updateEditorSize(editorElement: any, content: string, editor: ace.Ace.Editor) { + let newHeight = 200; + let newWidth = 600; + if (content && content.length > 0) { + const lines = content.split('\n'); + newHeight = 16 * lines.length + 16; + let maxLineLength = 0; + lines.forEach((row) => { + const line = row.replace(/\t/g, ' ').replace(/\n/g, ''); + const lineLength = line.length; + maxLineLength = Math.max(maxLineLength, lineLength); + }); + newWidth = 8 * maxLineLength + 16; + } + // newHeight = Math.min(400, newHeight); + this.renderer.setStyle(editorElement, 'minHeight', newHeight.toString() + 'px'); + this.renderer.setStyle(editorElement, 'height', newHeight.toString() + 'px'); + this.renderer.setStyle(editorElement, 'width', newWidth.toString() + 'px'); + editor.resize(); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts new file mode 100644 index 0000000000..a50bb0cdaa --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts @@ -0,0 +1,134 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { + actionStatusTranslations, + actionTypeTranslations, + AuditLog, + AuditLogMode +} from '@shared/models/audit-log.models'; +import { EntityTypeResource, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { AuditLogService } from '@core/http/audit-log.service'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { Direction } from '@shared/models/page/sort-order'; +import { MatDialog } from '@angular/material/dialog'; +import { TimePageLink } from '@shared/models/page/page-link'; +import { Observable } from 'rxjs'; +import { PageData } from '@shared/models/page/page-data'; +import { EntityId } from '@shared/models/id/entity-id'; +import { UserId } from '@shared/models/id/user-id'; +import { CustomerId } from '@shared/models/id/customer-id'; +import { + AuditLogDetailsDialogComponent, + AuditLogDetailsDialogData +} from '@home/components/audit-log/audit-log-details-dialog.component'; + +export class AuditLogTableConfig extends EntityTableConfig { + + constructor(private auditLogService: AuditLogService, + private translate: TranslateService, + private datePipe: DatePipe, + private dialog: MatDialog, + private auditLogMode: AuditLogMode = AuditLogMode.TENANT, + public entityId: EntityId = null, + public userId: UserId = null, + public customerId: CustomerId = null, + updateOnInit = true) { + super(); + this.loadDataOnInit = updateOnInit; + this.tableTitle = ''; + this.useTimePageLink = true; + this.detailsPanelEnabled = false; + this.selectionEnabled = false; + this.searchEnabled = true; + this.addEnabled = false; + this.entitiesDeleteEnabled = false; + this.actionsColumnTitle = 'audit-log.details'; + this.entityTranslations = { + noEntities: 'audit-log.no-audit-logs-prompt', + search: 'audit-log.search' + }; + this.entityResources = { + } as EntityTypeResource; + + this.entitiesFetchFunction = pageLink => this.fetchAuditLogs(pageLink); + + this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; + + this.columns.push( + new DateEntityTableColumn('createdTime', 'audit-log.timestamp', this.datePipe, '150px')); + + if (this.auditLogMode !== AuditLogMode.ENTITY) { + this.columns.push( + new EntityTableColumn('entityType', 'audit-log.entity-type', '20%', + (entity) => translate.instant(entityTypeTranslations.get(entity.entityId.entityType).type)), + new EntityTableColumn('entityName', 'audit-log.entity-name', '20%'), + ); + } + + if (this.auditLogMode !== AuditLogMode.USER) { + this.columns.push( + new EntityTableColumn('userName', 'audit-log.user', '33%') + ); + } + + this.columns.push( + new EntityTableColumn('actionType', 'audit-log.type', '33%', + (entity) => translate.instant(actionTypeTranslations.get(entity.actionType))), + new EntityTableColumn('actionStatus', 'audit-log.status', '33%', + (entity) => translate.instant(actionStatusTranslations.get(entity.actionStatus))) + ); + + this.cellActionDescriptors.push( + { + name: this.translate.instant('audit-log.details'), + icon: 'more_horiz', + isEnabled: () => true, + onAction: ($event, entity) => this.showAuditLogDetails(entity) + } + ); + } + + fetchAuditLogs(pageLink: TimePageLink): Observable> { + switch (this.auditLogMode) { + case AuditLogMode.TENANT: + return this.auditLogService.getAuditLogs(pageLink); + case AuditLogMode.ENTITY: + return this.auditLogService.getAuditLogsByEntityId(this.entityId, pageLink); + case AuditLogMode.USER: + return this.auditLogService.getAuditLogsByUserId(this.userId.id, pageLink); + case AuditLogMode.CUSTOMER: + return this.auditLogService.getAuditLogsByCustomerId(this.customerId.id, pageLink); + } + } + + showAuditLogDetails(entity: AuditLog) { + this.dialog.open(AuditLogDetailsDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + auditLog: entity + } + }); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table.component.html b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table.component.html new file mode 100644 index 0000000000..d5cf0762af --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table.component.html @@ -0,0 +1,18 @@ + + diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table.component.scss b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table.component.scss new file mode 100644 index 0000000000..eb11e3b4eb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table.component.scss @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host ::ng-deep { + tb-entities-table.tb-details-mode { + .mat-drawer-container { + background-color: white; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table.component.ts b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table.component.ts new file mode 100644 index 0000000000..3c7609602e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table.component.ts @@ -0,0 +1,133 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Input, OnInit, ViewChild } from '@angular/core'; +import { AuditLogService } from '@core/http/audit-log.service'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { MatDialog } from '@angular/material/dialog'; +import { AuditLogMode } from '@shared/models/audit-log.models'; +import { EntityId } from '@shared/models/id/entity-id'; +import { UserId } from '@shared/models/id/user-id'; +import { CustomerId } from '@shared/models/id/customer-id'; +import { AuditLogTableConfig } from '@home/components/audit-log/audit-log-table-config'; +import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Authority } from '@shared/models/authority.enum'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; + +@Component({ + selector: 'tb-audit-log-table', + templateUrl: './audit-log-table.component.html', + styleUrls: ['./audit-log-table.component.scss'] +}) +export class AuditLogTableComponent implements OnInit { + + @Input() + auditLogMode: AuditLogMode; + + @Input() + detailsMode: boolean; + + activeValue = false; + dirtyValue = false; + entityIdValue: EntityId; + userIdValue: UserId; + customerIdValue: CustomerId; + + @Input() + set active(active: boolean) { + if (this.activeValue !== active) { + this.activeValue = active; + if (this.activeValue && this.dirtyValue) { + this.dirtyValue = false; + this.entitiesTable.updateData(); + } + } + } + + @Input() + set entityId(entityId: EntityId) { + this.entityIdValue = entityId; + if (this.auditLogTableConfig && this.auditLogTableConfig.entityId !== entityId) { + this.auditLogTableConfig.entityId = entityId; + this.entitiesTable.resetSortAndFilter(this.activeValue); + if (!this.activeValue) { + this.dirtyValue = true; + } + } + } + + @Input() + set userId(userId: UserId) { + this.userIdValue = userId; + if (this.auditLogTableConfig && this.auditLogTableConfig.userId !== userId) { + this.auditLogTableConfig.userId = userId; + this.entitiesTable.resetSortAndFilter(this.activeValue); + if (!this.activeValue) { + this.dirtyValue = true; + } + } + } + + @Input() + set customerId(customerId: CustomerId) { + this.customerIdValue = customerId; + if (this.auditLogTableConfig && this.auditLogTableConfig.customerId !== customerId) { + this.auditLogTableConfig.customerId = customerId; + this.entitiesTable.resetSortAndFilter(this.activeValue); + if (!this.activeValue) { + this.dirtyValue = true; + } + } + } + + @ViewChild(EntitiesTableComponent, {static: true}) entitiesTable: EntitiesTableComponent; + + auditLogTableConfig: AuditLogTableConfig; + + constructor(private auditLogService: AuditLogService, + private translate: TranslateService, + private datePipe: DatePipe, + private dialog: MatDialog, + private store: Store) { + } + + ngOnInit() { + let updateOnInit = this.activeValue; + this.dirtyValue = !this.activeValue; + if (!this.auditLogMode) { + const authUser = getCurrentAuthUser(this.store); + if (authUser.authority === Authority.TENANT_ADMIN) { + this.auditLogMode = AuditLogMode.TENANT; + } + updateOnInit = true; + } + this.auditLogTableConfig = new AuditLogTableConfig( + this.auditLogService, + this.translate, + this.datePipe, + this.dialog, + this.auditLogMode, + this.entityIdValue, + this.userIdValue, + this.customerIdValue, + updateOnInit + ); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html new file mode 100644 index 0000000000..653e7f841f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html @@ -0,0 +1,165 @@ + +
+ + +
+
+
+
+ + +
+ +
+
+
+
+
+ + +
+ +
+
+
+
+ + +
+
+
+ + {{widget.titleIcon}} + {{widget.title}} + + + +
+
+ + + + + + +
+
+
+ + +
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.scss b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.scss new file mode 100644 index 0000000000..3e1a274c9e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.scss @@ -0,0 +1,149 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../scss/constants'; + +:host { + + .tb-progress-cover { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 6; + opacity: 1; + } + + .tb-dashboard-content { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background: none; + outline: none; + + gridster-item { + transition: none; + overflow: visible; + background: none; + } + } + + #gridster-child { + background: none; + } +} + +tb-widget.tb-widget { + position: relative; + height: 100%; + margin: 0; + overflow: hidden; + outline: none; + + transition: all .2s ease-in-out; +} + +div.tb-widget { + position: relative; + height: 100%; + margin: 0; + overflow: hidden; + outline: none; + + transition: all .2s ease-in-out; + + .tb-widget-title { + max-height: 65px; + padding-top: 5px; + padding-left: 5px; + overflow: hidden; + + tb-timewindow { + font-size: 14px; + opacity: .85; + margin: 0; + } + + .title { + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + line-height: 24px; + letter-spacing: .01em; + margin: 0; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + + &.single-row{ + -webkit-line-clamp: 1; + } + } + } + + .tb-widget-actions { + z-index: 19; + margin: 5px 0 0; + + &-absolute { + position: absolute; + top: 3px; + right: 8px; + z-index: 150; + } + + button.mat-icon-button { + width: 32px; + min-width: 32px; + height: 32px; + min-height: 32px; + padding: 0 !important; + margin: 0 !important; + line-height: 20px; + + mat-icon { + width: 20px; + min-width: 20px; + height: 20px; + min-height: 20px; + font-size: 20px; + line-height: 20px; + } + } + } + + .tb-widget-content { + tb-widget { + position: relative; + width: 100%; + } + } + + &.tb-highlighted { + border: 1px solid #039be5; + box-shadow: 0 0 20px #039be5; + } + + &.tb-not-highlighted { + opacity: .5; + } +} + +.tb-dashboard-context-menu-items { + min-width: 256px; +} diff --git a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts new file mode 100644 index 0000000000..0289831a01 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts @@ -0,0 +1,556 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + Component, + DoCheck, + Input, + IterableDiffers, + KeyValueDiffers, + NgZone, + OnChanges, + OnDestroy, + OnInit, + SimpleChanges, + ViewChild +} from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { PageComponent } from '@shared/components/page.component'; +import { AuthUser } from '@shared/models/user.model'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { Timewindow, toHistoryTimewindow } from '@shared/models/time/time.models'; +import { TimeService } from '@core/services/time.service'; +import { GridsterComponent, GridsterConfig, GridType } from 'angular-gridster2'; +import { + DashboardCallbacks, + DashboardWidget, + DashboardWidgets, + IDashboardComponent +} from '../../models/dashboard-component.models'; +import { ReplaySubject, Subject, Subscription } from 'rxjs'; +import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models'; +import { DialogService } from '@core/services/dialog.service'; +import { animatedScroll, deepClone, isDefined } from '@app/core/utils'; +import { BreakpointObserver } from '@angular/cdk/layout'; +import { MediaBreakpoints } from '@shared/models/constants'; +import { IAliasController, IStateController } from '@app/core/api/widget-api.models'; +import { Widget, WidgetPosition } from '@app/shared/models/widget.models'; +import { MatMenuTrigger } from '@angular/material/menu'; +import { SafeStyle } from '@angular/platform-browser'; +import { distinct } from 'rxjs/operators'; +import { ResizeObserver } from '@juggle/resize-observer'; + +@Component({ + selector: 'tb-dashboard', + templateUrl: './dashboard.component.html', + styleUrls: ['./dashboard.component.scss'] +}) +export class DashboardComponent extends PageComponent implements IDashboardComponent, DoCheck, OnInit, OnDestroy, AfterViewInit, OnChanges { + + authUser: AuthUser; + + @Input() + widgets: Iterable; + + @Input() + widgetLayouts: WidgetLayouts; + + @Input() + callbacks: DashboardCallbacks; + + @Input() + aliasController: IAliasController; + + @Input() + stateController: IStateController; + + @Input() + columns: number; + + @Input() + margin: number; + + @Input() + isEdit: boolean; + + @Input() + autofillHeight: boolean; + + @Input() + mobileAutofillHeight: boolean; + + @Input() + mobileRowHeight: number; + + @Input() + isMobile: boolean; + + @Input() + isMobileDisabled: boolean; + + @Input() + isEditActionEnabled: boolean; + + @Input() + isExportActionEnabled: boolean; + + @Input() + isRemoveActionEnabled: boolean; + + @Input() + dashboardStyle: {[klass: string]: any}; + + @Input() + backgroundImage: SafeStyle | string; + + @Input() + dashboardClass: string; + + @Input() + ignoreLoading = true; + + @Input() + dashboardTimewindow: Timewindow; + + dashboardTimewindowChangedSubject: Subject = new ReplaySubject(); + + dashboardTimewindowChanged = this.dashboardTimewindowChangedSubject.asObservable().pipe( + distinct() + ); + + originalDashboardTimewindow: Timewindow; + + gridsterOpts: GridsterConfig; + + isWidgetExpanded = false; + isMobileSize = false; + + @ViewChild('gridster', {static: true}) gridster: GridsterComponent; + + @ViewChild('dashboardMenuTrigger', {static: true}) dashboardMenuTrigger: MatMenuTrigger; + + dashboardMenuPosition = { x: '0px', y: '0px' }; + + dashboardContextMenuEvent: MouseEvent; + + @ViewChild('widgetMenuTrigger', {static: true}) widgetMenuTrigger: MatMenuTrigger; + + widgetMenuPosition = { x: '0px', y: '0px' }; + + widgetContextMenuEvent: MouseEvent; + + dashboardWidgets = new DashboardWidgets(this, + this.differs.find([]).create((index, item) => { + return item; + }), + this.kvDiffers.find([]).create() + ); + + breakpointObserverSubscription: Subscription; + + private optionsChangeNotificationsPaused = false; + + private gridsterResize$: ResizeObserver; + + constructor(protected store: Store, + private timeService: TimeService, + private dialogService: DialogService, + private breakpointObserver: BreakpointObserver, + private differs: IterableDiffers, + private kvDiffers: KeyValueDiffers, + private ngZone: NgZone) { + super(store); + this.authUser = getCurrentAuthUser(store); + } + + ngOnInit(): void { + if (!this.dashboardTimewindow) { + this.dashboardTimewindow = this.timeService.defaultTimewindow(); + } + this.gridsterOpts = { + gridType: GridType.ScrollVertical, + keepFixedHeightInMobile: true, + disableWarnings: false, + disableAutoPositionOnConflict: false, + pushItems: false, + swap: false, + maxRows: 100, + minCols: this.columns ? this.columns : 24, + maxCols: 3000, + maxItemCols: 1000, + maxItemRows: 1000, + maxItemArea: 1000000, + outerMargin: true, + margin: isDefined(this.margin) ? this.margin : 10, + minItemCols: 1, + minItemRows: 1, + defaultItemCols: 8, + defaultItemRows: 6, + resizable: {enabled: this.isEdit}, + draggable: {enabled: this.isEdit}, + itemChangeCallback: item => this.dashboardWidgets.sortWidgets(), + itemInitCallback: (item, itemComponent) => { + (itemComponent.item as DashboardWidget).gridsterItemComponent = itemComponent; + } + }; + + this.updateMobileOpts(); + + this.breakpointObserverSubscription = this.breakpointObserver + .observe(MediaBreakpoints['gt-sm']).subscribe( + () => { + this.updateMobileOpts(); + this.notifyGridsterOptionsChanged(); + } + ); + + this.updateWidgets(); + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + if (this.gridsterResize$) { + this.gridsterResize$.disconnect(); + } + if (this.breakpointObserverSubscription) { + this.breakpointObserverSubscription.unsubscribe(); + } + this.gridster = null; + } + + ngDoCheck() { + if (!this.optionsChangeNotificationsPaused) { + this.dashboardWidgets.doCheck(); + } + } + + ngOnChanges(changes: SimpleChanges): void { + let updateMobileOpts = false; + let updateLayoutOpts = false; + let updateEditingOpts = false; + let updateWidgets = false; + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (['isMobile', 'isMobileDisabled', 'autofillHeight', 'mobileAutofillHeight', 'mobileRowHeight'].includes(propName)) { + updateMobileOpts = true; + } else if (['margin', 'columns'].includes(propName)) { + updateLayoutOpts = true; + } else if (propName === 'isEdit') { + updateEditingOpts = true; + } else if (['widgets', 'widgetLayouts'].includes(propName)) { + updateWidgets = true; + } else if (propName === 'dashboardTimewindow') { + this.dashboardTimewindowChangedSubject.next(this.dashboardTimewindow); + } + } + } + if (updateWidgets) { + this.updateWidgets(); + } + if (updateMobileOpts) { + this.updateMobileOpts(); + } + if (updateLayoutOpts) { + this.updateLayoutOpts(); + } + if (updateEditingOpts) { + this.updateEditingOpts(); + } + if (updateMobileOpts || updateLayoutOpts || updateEditingOpts) { + this.notifyGridsterOptionsChanged(); + } + } + + private updateWidgets() { + this.dashboardWidgets.setWidgets(this.widgets, this.widgetLayouts); + this.dashboardWidgets.doCheck(); + } + + private updateWidgetLayouts() { + this.dashboardWidgets.widgetLayoutsUpdated(); + } + + ngAfterViewInit(): void { + this.gridsterResize$ = new ResizeObserver(() => { + this.onGridsterParentResize(); + }); + this.gridsterResize$.observe(this.gridster.el); + } + + onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval?: number, persist?: boolean): void { + this.ngZone.run(() => { + if (!this.originalDashboardTimewindow && !persist) { + this.originalDashboardTimewindow = deepClone(this.dashboardTimewindow); + } + this.dashboardTimewindow = toHistoryTimewindow(this.dashboardTimewindow, + startTimeMs, endTimeMs, interval, this.timeService); + this.dashboardTimewindowChangedSubject.next(this.dashboardTimewindow); + }); + } + + onResetTimewindow(): void { + this.ngZone.run(() => { + if (this.originalDashboardTimewindow) { + this.dashboardTimewindow = deepClone(this.originalDashboardTimewindow); + this.originalDashboardTimewindow = null; + this.dashboardTimewindowChangedSubject.next(this.dashboardTimewindow); + } + }); + } + + isAutofillHeight(): boolean { + if (this.isMobileSize) { + return isDefined(this.mobileAutofillHeight) ? this.mobileAutofillHeight : false; + } else { + return isDefined(this.autofillHeight) ? this.autofillHeight : false; + } + } + + openDashboardContextMenu($event: MouseEvent) { + if (this.callbacks && this.callbacks.prepareDashboardContextMenu) { + const items = this.callbacks.prepareDashboardContextMenu($event); + if (items && items.length) { + $event.preventDefault(); + $event.stopPropagation(); + this.dashboardContextMenuEvent = $event; + this.dashboardMenuPosition.x = $event.clientX + 'px'; + this.dashboardMenuPosition.y = $event.clientY + 'px'; + this.dashboardMenuTrigger.menuData = { items }; + this.dashboardMenuTrigger.openMenu(); + } + } + } + + openWidgetContextMenu($event: MouseEvent, widget: DashboardWidget) { + if (this.callbacks && this.callbacks.prepareWidgetContextMenu) { + const items = this.callbacks.prepareWidgetContextMenu($event, widget.widget); + if (items && items.length) { + $event.preventDefault(); + $event.stopPropagation(); + this.widgetContextMenuEvent = $event; + this.widgetMenuPosition.x = $event.clientX + 'px'; + this.widgetMenuPosition.y = $event.clientY + 'px'; + this.widgetMenuTrigger.menuData = { items, widget: widget.widget }; + this.widgetMenuTrigger.openMenu(); + } + } + } + + onWidgetFullscreenChanged(expanded: boolean, widget: DashboardWidget) { + this.isWidgetExpanded = expanded; + } + + widgetMouseDown($event: Event, widget: DashboardWidget) { + if (this.callbacks && this.callbacks.onWidgetMouseDown) { + this.callbacks.onWidgetMouseDown($event, widget.widget); + } + } + + widgetClicked($event: Event, widget: DashboardWidget) { + if (this.callbacks && this.callbacks.onWidgetClicked) { + this.callbacks.onWidgetClicked($event, widget.widget); + } + } + + editWidget($event: Event, widget: DashboardWidget) { + if ($event) { + $event.stopPropagation(); + } + if (this.isEditActionEnabled && this.callbacks && this.callbacks.onEditWidget) { + this.callbacks.onEditWidget($event, widget.widget); + } + } + + exportWidget($event: Event, widget: DashboardWidget) { + if ($event) { + $event.stopPropagation(); + } + if (this.isExportActionEnabled && this.callbacks && this.callbacks.onExportWidget) { + this.callbacks.onExportWidget($event, widget.widget); + } + } + + removeWidget($event: Event, widget: DashboardWidget) { + if ($event) { + $event.stopPropagation(); + } + if (this.isRemoveActionEnabled && this.callbacks && this.callbacks.onRemoveWidget) { + this.callbacks.onRemoveWidget($event, widget.widget); + } + } + + highlightWidget(widgetId: string, delay?: number) { + const highlighted = this.dashboardWidgets.highlightWidget(widgetId); + if (highlighted) { + this.scrollToWidget(highlighted, delay); + } + } + + selectWidget(widgetId: string, delay?: number) { + const selected = this.dashboardWidgets.selectWidget(widgetId); + if (selected) { + this.scrollToWidget(selected, delay); + } + } + + getSelectedWidget(): Widget { + const dashboardWidget = this.dashboardWidgets.getSelectedWidget(); + return dashboardWidget ? dashboardWidget.widget : null; + } + + getEventGridPosition(event: Event): WidgetPosition { + const pos: WidgetPosition = { + row: 0, + column: 0 + }; + const parentElement = $(this.gridster.el); + let pageX = 0; + let pageY = 0; + if (event instanceof MouseEvent) { + pageX = event.pageX; + pageY = event.pageY; + } + const offset = parentElement.offset(); + const x = pageX - offset.left + parentElement.scrollLeft(); + const y = pageY - offset.top + parentElement.scrollTop(); + pos.row = this.gridster.pixelsToPositionY(y, Math.floor); + pos.column = this.gridster.pixelsToPositionX(x, Math.floor); + return pos; + } + + resetHighlight() { + const highlighted = this.dashboardWidgets.resetHighlight(); + if (highlighted) { + setTimeout(() => { + this.scrollToWidget(highlighted, 0); + }, 0); + } + } + + isHighlighted(widget: DashboardWidget) { + return this.dashboardWidgets.isHighlighted(widget); + } + + isNotHighlighted(widget: DashboardWidget) { + return this.dashboardWidgets.isNotHighlighted(widget); + } + + private scrollToWidget(widget: DashboardWidget, delay?: number) { + const parentElement = this.gridster.el as HTMLElement; + widget.gridsterItemComponent$().subscribe((gridsterItem) => { + const gridsterItemElement = gridsterItem.el as HTMLElement; + const offset = (parentElement.clientHeight - gridsterItemElement.clientHeight) / 2; + let scrollTop; + if (this.isMobileSize) { + scrollTop = gridsterItemElement.offsetTop; + } else { + scrollTop = scrollTop = gridsterItem.top; + } + if (offset > 0) { + scrollTop -= offset; + } + animatedScroll(parentElement, scrollTop, delay); + }); + } + + private updateMobileOpts(parentHeight?: number) { + this.isMobileSize = this.checkIsMobileSize(); + const autofillHeight = this.isAutofillHeight(); + if (autofillHeight) { + this.gridsterOpts.gridType = this.isMobileSize ? GridType.Fixed : GridType.Fit; + } else { + this.gridsterOpts.gridType = this.isMobileSize ? GridType.Fixed : GridType.ScrollVertical; + } + const mobileBreakPoint = this.isMobileSize ? 20000 : 0; + this.gridsterOpts.mobileBreakpoint = mobileBreakPoint; + const rowSize = this.detectRowSize(this.isMobileSize, autofillHeight, parentHeight); + if (this.gridsterOpts.fixedRowHeight !== rowSize) { + this.gridsterOpts.fixedRowHeight = rowSize; + } + } + + private onGridsterParentResize() { + const parentHeight = this.gridster.el.offsetHeight; + if (this.isMobileSize && this.mobileAutofillHeight && parentHeight) { + this.updateMobileOpts(parentHeight); + } + this.notifyGridsterOptionsChanged(); + } + + private updateLayoutOpts() { + this.gridsterOpts.minCols = this.columns ? this.columns : 24; + this.gridsterOpts.margin = isDefined(this.margin) ? this.margin : 10; + } + + private updateEditingOpts() { + this.gridsterOpts.resizable.enabled = this.isEdit; + this.gridsterOpts.draggable.enabled = this.isEdit; + } + + public notifyGridsterOptionsChanged() { + if (!this.optionsChangeNotificationsPaused) { + if (this.gridster && this.gridster.options) { + this.gridster.optionsChanged(); + } + } + } + + public pauseChangeNotifications() { + this.optionsChangeNotificationsPaused = true; + } + + public resumeChangeNotifications() { + this.optionsChangeNotificationsPaused = false; + } + + public notifyLayoutUpdated() { + this.updateWidgetLayouts(); + } + + private detectRowSize(isMobile: boolean, autofillHeight: boolean, parentHeight?: number): number | null { + let rowHeight = null; + if (!autofillHeight) { + if (isMobile) { + rowHeight = isDefined(this.mobileRowHeight) ? this.mobileRowHeight : 70; + } + } else if (autofillHeight && isMobile) { + if (!parentHeight) { + parentHeight = this.gridster.el.offsetHeight; + } + if (parentHeight) { + let totalRows = 0; + for (const widget of this.dashboardWidgets.dashboardWidgets) { + totalRows += widget.rows; + } + rowHeight = (parentHeight - this.gridsterOpts.margin * (this.dashboardWidgets.dashboardWidgets.length + 2)) / totalRows; + } + } + return rowHeight; + } + + private checkIsMobileSize(): boolean { + const isMobileDisabled = this.isMobileDisabled === true; + let isMobileSize = this.isMobile === true && !isMobileDisabled; + if (!isMobileSize && !isMobileDisabled) { + isMobileSize = !this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']); + } + return isMobileSize; + } + +} diff --git a/ui-ngx/src/app/modules/home/components/dashboard/layout-button.scss b/ui-ngx/src/app/modules/home/components/dashboard/layout-button.scss new file mode 100644 index 0000000000..7582107d0a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/dashboard/layout-button.scss @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + button.tb-layout-button { + width: 100%; + height: 100%; + padding: 40px 20px; + } + + &::ng-deep { + .mat-button-wrapper { + line-height: 18px; + span { + font-size: 18px; + font-weight: 400; + white-space: normal; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/dashboard/select-target-layout-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard/select-target-layout-dialog.component.html new file mode 100644 index 0000000000..65075eae26 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/dashboard/select-target-layout-dialog.component.html @@ -0,0 +1,50 @@ + +
+ +

layout.select

+ + +
+ + +
+
+
+ + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/dashboard/select-target-layout-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard/select-target-layout-dialog.component.ts new file mode 100644 index 0000000000..422ae95808 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/dashboard/select-target-layout-dialog.component.ts @@ -0,0 +1,50 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, OnInit } from '@angular/core'; +import { MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { DashboardLayoutId } from '@app/shared/models/dashboard.models'; + +@Component({ + selector: 'tb-select-target-layout-dialog', + templateUrl: './select-target-layout-dialog.component.html', + styleUrls: ['./layout-button.scss'] +}) +export class SelectTargetLayoutDialogComponent extends DialogComponent + implements OnInit { + + constructor(protected store: Store, + protected router: Router, + public dialogRef: MatDialogRef) { + super(store, router, dialogRef); + } + + ngOnInit(): void { + } + + selectLayout(layoutId: DashboardLayoutId) { + this.dialogRef.close(layoutId); + } + + cancel(): void { + this.dialogRef.close(null); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/dashboard/select-target-state-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard/select-target-state-dialog.component.html new file mode 100644 index 0000000000..031dfd1f63 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/dashboard/select-target-state-dialog.component.html @@ -0,0 +1,56 @@ + +
+ +

dashboard.select-state

+ + +
+ + +
+
+ + dashboard.state + + + {{ stateItem.value.name }} + + + +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/dashboard/select-target-state-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard/select-target-state-dialog.component.ts new file mode 100644 index 0000000000..d64894adf1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/dashboard/select-target-state-dialog.component.ts @@ -0,0 +1,83 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { DashboardState } from '@app/shared/models/dashboard.models'; +import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; + +export interface SelectTargetStateDialogData { + states: {[id: string]: DashboardState }; +} + +@Component({ + selector: 'tb-select-target-state-dialog', + templateUrl: './select-target-state-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: SelectTargetStateDialogComponent}], + styleUrls: [] +}) +export class SelectTargetStateDialogComponent extends + DialogComponent + implements OnInit, ErrorStateMatcher { + + states: {[id: string]: DashboardState }; + stateFormGroup: FormGroup; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: SelectTargetStateDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + private dashboardUtils: DashboardUtilsService) { + super(store, router, dialogRef); + + this.states = this.data.states; + + this.stateFormGroup = this.fb.group( + { + stateId: [this.dashboardUtils.getRootStateId(this.states), [Validators.required]] + } + ); + } + + ngOnInit(): void { + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + const stateId: string = this.stateFormGroup.get('stateId').value; + this.dialogRef.close(stateId); + } +} diff --git a/ui-ngx/src/app/modules/home/components/details-panel.component.html b/ui-ngx/src/app/modules/home/components/details-panel.component.html new file mode 100644 index 0000000000..9234812359 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/details-panel.component.html @@ -0,0 +1,56 @@ + +
+ +
+
+ {{ headerTitle }} + {{ headerSubtitle }} + + + +
+ + +
+
+ + +
+
+
+
+ +
diff --git a/ui-ngx/src/app/modules/home/components/details-panel.component.scss b/ui-ngx/src/app/modules/home/components/details-panel.component.scss new file mode 100644 index 0000000000..d53bd7864b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/details-panel.component.scss @@ -0,0 +1,66 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../scss/constants'; + +:host { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + .mat-toolbar.details-toolbar { + padding: 0; + } +} + +:host ::ng-deep { + .mat-toolbar-tools { + height: 100%; + min-height: 100px; + max-height: 120px; + &.tb-details-title-header { + min-width: 0; + } + } + .tb-details-title { + width: inherit; + margin: 20px 8px 0 0; + overflow: hidden; + font-size: 1rem; + font-weight: 400; + text-overflow: ellipsis; + white-space: nowrap; + + @media #{$mat-gt-sm} { + font-size: 1.5rem; + } + } + + .tb-details-subtitle { + width: inherit; + margin: 10px 0; + overflow: hidden; + font-size: 1rem; + text-overflow: ellipsis; + white-space: nowrap; + opacity: .8; + } + + tb-dashboard { + .tb-dashboard-content { + background-color: $primary-hue-3 !important; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/details-panel.component.ts b/ui-ngx/src/app/modules/home/components/details-panel.component.ts new file mode 100644 index 0000000000..ce573bd861 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/details-panel.component.ts @@ -0,0 +1,91 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormGroup } from '@angular/forms'; + +@Component({ + selector: 'tb-details-panel', + templateUrl: './details-panel.component.html', + styleUrls: ['./details-panel.component.scss'] +}) +export class DetailsPanelComponent extends PageComponent { + + @Input() headerHeightPx = 100; + @Input() headerTitle = ''; + @Input() headerSubtitle = ''; + @Input() isReadOnly = false; + @Input() isAlwaysEdit = false; + + theFormValue: FormGroup; + + @Input() + set theForm(value: FormGroup) { + this.theFormValue = value; + } + + get theForm(): FormGroup { + return this.theFormValue; + } + + @Output() + closeDetails = new EventEmitter(); + @Output() + toggleDetailsEditMode = new EventEmitter(); + @Output() + applyDetails = new EventEmitter(); + + isEditValue = false; + + @Output() + isEditChange = new EventEmitter(); + + @Input() + get isEdit() { + return this.isAlwaysEdit || this.isEditValue; + } + + set isEdit(val: boolean) { + this.isEditValue = val; + this.isEditChange.emit(this.isEditValue); + } + + + constructor(protected store: Store) { + super(store); + } + + onCloseDetails() { + this.closeDetails.emit(); + } + + onToggleDetailsEditMode() { + if (!this.isAlwaysEdit) { + this.isEdit = !this.isEdit; + } + this.toggleDetailsEditMode.emit(this.isEditValue); + } + + onApplyDetails() { + if (this.theForm && this.theForm.valid) { + this.applyDetails.emit(); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/device/device-credentials.component.html b/ui-ngx/src/app/modules/home/components/device/device-credentials.component.html new file mode 100644 index 0000000000..a8b48e30d1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials.component.html @@ -0,0 +1,77 @@ + +
+ + device.credentials-type + + + {{ credentialTypeNamesMap.get(deviceCredentialsType[credentialsType]) }} + + + + + device.access-token + + + {{ 'device.access-token-required' | translate }} + + + {{ 'device.access-token-invalid' | translate }} + + + + device.rsa-key + + + {{ 'device.rsa-key-required' | translate }} + + +
+ + device.client-id + + + {{ 'device.client-id-pattern' | translate }} + + + + device.user-name + + + {{ 'device.user-name-required' | translate }} + + + + device.password + + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts b/ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts new file mode 100644 index 0000000000..fd6680fe17 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts @@ -0,0 +1,226 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + ValidatorFn, + Validators +} from '@angular/forms'; +import { + credentialTypeNames, + DeviceCredentialMQTTBasic, + DeviceCredentials, + DeviceCredentialsType +} from '@shared/models/device.models'; +import { Subscription } from 'rxjs'; +import { isDefinedAndNotNull } from '@core/utils'; +import { distinctUntilChanged } from 'rxjs/operators'; + +@Component({ + selector: 'tb-device-credentials', + templateUrl: './device-credentials.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceCredentialsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DeviceCredentialsComponent), + multi: true, + }], + styleUrls: [] +}) +export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, Validator, OnDestroy { + + deviceCredentialsFormGroup: FormGroup; + + subscriptions: Subscription[] = []; + + @Input() + disabled: boolean; + + deviceCredentials: DeviceCredentials = null; + + submitted = false; + + deviceCredentialsType = DeviceCredentialsType; + + credentialsTypes = Object.keys(DeviceCredentialsType); + + credentialTypeNamesMap = credentialTypeNames; + + hidePassword = true; + + private propagateChange = (v: any) => {}; + + constructor(public fb: FormBuilder) { + this.deviceCredentialsFormGroup = this.fb.group({ + credentialsType: [DeviceCredentialsType.ACCESS_TOKEN], + credentialsId: [null], + credentialsValue: [null], + credentialsBasic: this.fb.group({ + clientId: [null, [Validators.pattern(/^[A-Za-z0-9]+$/)]], + userName: [null], + password: [null] + }, {validators: this.atLeastOne(Validators.required, ['clientId', 'userName'])}) + }); + this.deviceCredentialsFormGroup.get('credentialsBasic').disable(); + this.subscriptions.push( + this.deviceCredentialsFormGroup.valueChanges.pipe(distinctUntilChanged()).subscribe(() => { + this.updateView(); + }) + ); + this.subscriptions.push( + this.deviceCredentialsFormGroup.get('credentialsType').valueChanges.subscribe(() => { + this.credentialsTypeChanged(); + }) + ); + } + + ngOnInit(): void { + if (this.disabled) { + this.deviceCredentialsFormGroup.disable({emitEvent: false}); + } + } + + ngOnDestroy() { + this.subscriptions.forEach(s => s.unsubscribe()); + } + + writeValue(value: DeviceCredentials | null): void { + if (isDefinedAndNotNull(value)) { + this.deviceCredentials = value; + let credentialsBasic = {clientId: null, userName: null, password: null}; + let credentialsValue = null; + if (value.credentialsType === DeviceCredentialsType.MQTT_BASIC) { + credentialsBasic = JSON.parse(value.credentialsValue) as DeviceCredentialMQTTBasic; + } else { + credentialsValue = value.credentialsValue; + } + this.deviceCredentialsFormGroup.patchValue({ + credentialsType: value.credentialsType, + credentialsId: value.credentialsId, + credentialsValue, + credentialsBasic + }, {emitEvent: false}); + this.updateValidators(); + } + } + + updateView() { + const deviceCredentialsValue = this.deviceCredentialsFormGroup.value; + if (deviceCredentialsValue.credentialsType === DeviceCredentialsType.MQTT_BASIC) { + deviceCredentialsValue.credentialsValue = JSON.stringify(deviceCredentialsValue.credentialsBasic); + } + delete deviceCredentialsValue.credentialsBasic; + this.propagateChange(deviceCredentialsValue); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void {} + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.deviceCredentialsFormGroup.disable({emitEvent: false}); + } else { + this.deviceCredentialsFormGroup.enable({emitEvent: false}); + this.updateValidators(); + } + } + + public validate(c: FormControl) { + return this.deviceCredentialsFormGroup.valid ? null : { + deviceCredentials: { + valid: false, + }, + }; + } + + credentialsTypeChanged(): void { + this.deviceCredentialsFormGroup.patchValue({ + credentialsId: null, + credentialsValue: null, + credentialsBasic: {clientId: '', userName: '', password: ''} + }); + this.updateValidators(); + } + + updateValidators(): void { + this.hidePassword = true; + const credentialsType = this.deviceCredentialsFormGroup.get('credentialsType').value as DeviceCredentialsType; + switch (credentialsType) { + case DeviceCredentialsType.ACCESS_TOKEN: + this.deviceCredentialsFormGroup.get('credentialsId').setValidators([Validators.required, Validators.pattern(/^.{1,20}$/)]); + this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity({emitEvent: false}); + this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([]); + this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity({emitEvent: false}); + this.deviceCredentialsFormGroup.get('credentialsBasic').disable({emitEvent: false}); + break; + case DeviceCredentialsType.X509_CERTIFICATE: + this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([Validators.required]); + this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity({emitEvent: false}); + this.deviceCredentialsFormGroup.get('credentialsId').setValidators([]); + this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity({emitEvent: false}); + this.deviceCredentialsFormGroup.get('credentialsBasic').disable({emitEvent: false}); + break; + case DeviceCredentialsType.MQTT_BASIC: + this.deviceCredentialsFormGroup.get('credentialsBasic').enable({emitEvent: false}); + this.deviceCredentialsFormGroup.get('credentialsBasic').updateValueAndValidity({emitEvent: false}); + this.deviceCredentialsFormGroup.get('credentialsId').setValidators([]); + this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity({emitEvent: false}); + this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([]); + this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity({emitEvent: false}); + } + } + + private atLeastOne(validator: ValidatorFn, controls: string[] = null) { + return (group: FormGroup): ValidationErrors | null => { + if (!controls) { + controls = Object.keys(group.controls); + } + const hasAtLeastOne = group?.controls && controls.some(k => !validator(group.controls[k])); + + return hasAtLeastOne ? null : {atLeastOne: true}; + }; + } + + passwordChanged() { + const value = this.deviceCredentialsFormGroup.get('credentialsBasic.password').value; + if (value !== '') { + this.deviceCredentialsFormGroup.get('credentialsBasic.userName').setValidators([Validators.required]); + } else { + this.deviceCredentialsFormGroup.get('credentialsBasic.userName').setValidators([]); + } + this.deviceCredentialsFormGroup.get('credentialsBasic.userName').updateValueAndValidity({ + emitEvent: false, + onlySelf: true + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.html b/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.html new file mode 100644 index 0000000000..78cad36bc5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.html @@ -0,0 +1,49 @@ + +
+ +

{{ translations.add }}

+ +
+ +
+ + +
+
+ +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.scss b/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.scss new file mode 100644 index 0000000000..8f36fc9f4a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.scss @@ -0,0 +1,18 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host ::ng-deep .mat-padding{ + padding: 0; +} diff --git a/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.ts b/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.ts new file mode 100644 index 0000000000..871242c954 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.ts @@ -0,0 +1,122 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ComponentFactoryResolver, Inject, Injector, OnInit, SkipSelf, ViewChild } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms'; +import { EntityTypeResource, EntityTypeTranslation } from '@shared/models/entity-type.models'; +import { BaseData, HasId } from '@shared/models/base-data'; +import { EntityId } from '@shared/models/id/entity-id'; +import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; +import { EntityComponent } from './entity.component'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import { AddEntityDialogData } from '@home/models/entity/entity-component.models'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'tb-add-entity-dialog', + templateUrl: './add-entity-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: AddEntityDialogComponent}], + styleUrls: ['./add-entity-dialog.component.scss'] +}) +export class AddEntityDialogComponent extends + DialogComponent> implements OnInit, ErrorStateMatcher { + + entityComponent: EntityComponent>; + detailsForm: FormGroup; + + entitiesTableConfig: EntityTableConfig>; + translations: EntityTypeTranslation; + resources: EntityTypeResource>; + entity: BaseData; + + submitted = false; + + @ViewChild('entityDetailsForm', {static: true}) entityDetailsFormAnchor: TbAnchorComponent; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AddEntityDialogData>, + public dialogRef: MatDialogRef>, + private componentFactoryResolver: ComponentFactoryResolver, + private injector: Injector, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher) { + super(store, router, dialogRef); + } + + ngOnInit(): void { + this.entitiesTableConfig = this.data.entitiesTableConfig; + this.translations = this.entitiesTableConfig.entityTranslations; + this.resources = this.entitiesTableConfig.entityResources; + this.entity = {}; + const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.entitiesTableConfig.entityComponent); + const viewContainerRef = this.entityDetailsFormAnchor.viewContainerRef; + viewContainerRef.clear(); + const injector: Injector = Injector.create( + { + providers: [ + { + provide: 'entity', + useValue: this.entity + }, + { + provide: 'entitiesTableConfig', + useValue: this.entitiesTableConfig + } + ], + parent: this.injector + } + ); + const componentRef = viewContainerRef.createComponent(componentFactory, 0, injector); + this.entityComponent = componentRef.instance; + this.entityComponent.isEdit = true; + this.detailsForm = this.entityComponent.entityForm; + } + + helpLinkId(): string { + if (this.resources.helpLinkIdForEntity && this.entityComponent.entityForm) { + return this.resources.helpLinkIdForEntity(this.entityComponent.entityForm.getRawValue()); + } else { + return this.resources.helpLinkId; + } + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + add(): void { + this.submitted = true; + if (this.detailsForm.valid) { + this.entity = {...this.entity, ...this.entityComponent.entityFormValue()}; + this.entitiesTableConfig.saveEntity(this.entity).subscribe( + (entity) => { + this.dialogRef.close(entity); + } + ); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/entity/contact-based.component.ts b/ui-ngx/src/app/modules/home/components/entity/contact-based.component.ts new file mode 100644 index 0000000000..20deed0b7c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/contact-based.component.ts @@ -0,0 +1,88 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms'; +import { ContactBased } from '@shared/models/contact-based.model'; +import { AfterViewInit, Directive } from '@angular/core'; +import { POSTAL_CODE_PATTERNS } from '@home/models/contact.models'; +import { HasId } from '@shared/models/base-data'; +import { EntityComponent } from './entity.component'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; + +@Directive() +export abstract class ContactBasedComponent> extends EntityComponent implements AfterViewInit { + + protected constructor(protected store: Store, + protected fb: FormBuilder, + protected entityValue: T, + protected entitiesTableConfigValue: EntityTableConfig) { + super(store, fb, entityValue, entitiesTableConfigValue); + } + + buildForm(entity: T): FormGroup { + const entityForm = this.buildEntityForm(entity); + entityForm.addControl('country', this.fb.control(entity ? entity.country : '', [])); + entityForm.addControl('city', this.fb.control(entity ? entity.city : '', [])); + entityForm.addControl('state', this.fb.control(entity ? entity.state : '', [])); + entityForm.addControl('zip', this.fb.control(entity ? entity.zip : '', + this.zipValidators(entity ? entity.country : '') + )); + entityForm.addControl('address', this.fb.control(entity ? entity.address : '', [])); + entityForm.addControl('address2', this.fb.control(entity ? entity.address2 : '', [])); + entityForm.addControl('phone', this.fb.control(entity ? entity.phone : '', [])); + entityForm.addControl('email', this.fb.control(entity ? entity.email : '', [Validators.email])); + return entityForm; + } + + updateForm(entity: T) { + this.updateEntityForm(entity); + this.entityForm.patchValue({country: entity.country}); + this.entityForm.patchValue({city: entity.city}); + this.entityForm.patchValue({state: entity.state}); + this.entityForm.get('zip').setValidators(this.zipValidators(entity.country)); + this.entityForm.patchValue({zip: entity.zip}); + this.entityForm.patchValue({address: entity.address}); + this.entityForm.patchValue({address2: entity.address2}); + this.entityForm.patchValue({phone: entity.phone}); + this.entityForm.patchValue({email: entity.email}); + } + + ngAfterViewInit() { + this.entityForm.get('country').valueChanges.subscribe( + (country) => { + this.entityForm.get('zip').setValidators(this.zipValidators(country)); + this.entityForm.get('zip').updateValueAndValidity({onlySelf: true}); + this.entityForm.get('zip').markAsTouched({onlySelf: true}); + } + ); + } + + zipValidators(country: string): ValidatorFn[] { + const zipValidators = []; + if (country && POSTAL_CODE_PATTERNS[country]) { + const postalCodePattern = POSTAL_CODE_PATTERNS[country]; + zipValidators.push(Validators.pattern(postalCodePattern)); + } + return zipValidators; + } + + abstract buildEntityForm(entity: T): FormGroup; + + abstract updateEntityForm(entity: T); + +} diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html new file mode 100644 index 0000000000..cf409b2e83 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html @@ -0,0 +1,245 @@ + + + + + + + +
+
+ +
+
+ {{ entitiesTableConfig.tableTitle }} + + +
+ +
+ + + + + + + + + + +
+ + + +
+
+ +
+ + +   + + + +
+
+ +
+ + {{ translations.selectedEntities | translate:{count: dataSource.selection.selected.length} }} + + + +
+
+
+ + + + + + + + + + + + + + {{ column.ignoreTranslate ? column.title : (column.title | translate) }} + + + + + {{ column.title | translate }} + + + + + + + {{ entitiesTableConfig.actionsColumnTitle ? (entitiesTableConfig.actionsColumnTitle | translate) : '' }} + + +
+ +
+
+ + + + +
+
+
+ + +
+ {{ translations.noEntities | translate }} + {{ 'common.loading' | translate }} +
+ + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss new file mode 100644 index 0000000000..d5f8d53387 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss @@ -0,0 +1,69 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../scss/constants'; + +:host { + width: 100%; + height: 100%; + .tb-entity-table { + .tb-entity-table-content { + width: 100%; + height: 100%; + background: #fff; + + .mat-toolbar-tools{ + min-height: auto; + } + + .title-container{ + overflow: hidden; + } + + .tb-entity-table-title { + padding-right: 20px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .table-container { + overflow: auto; + } + + .tb-entity-table-info{ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + } + + @media #{$mat-xs} { + .mat-toolbar { + height: auto; + min-height: 100px; + + .tb-entity-table-title{ + padding-bottom: 5px; + width: 100%; + } + } + } +} + +:host ::ng-deep .mat-sort-header-sorted .mat-sort-header-arrow { + opacity: 1 !important; +} diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts new file mode 100644 index 0000000000..ac20e70128 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts @@ -0,0 +1,576 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ComponentFactoryResolver, + ElementRef, + EventEmitter, + Input, + OnChanges, + OnInit, + SimpleChanges, + ViewChild +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { MAX_SAFE_PAGE_SIZE, PageLink, TimePageLink } from '@shared/models/page/page-link'; +import { MatDialog } from '@angular/material/dialog'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; +import { EntitiesDataSource } from '@home/models/datasource/entity-datasource'; +import { catchError, debounceTime, distinctUntilChanged, map, tap } from 'rxjs/operators'; +import { Direction, SortOrder } from '@shared/models/page/sort-order'; +import { forkJoin, fromEvent, merge, Observable, of, Subscription } from 'rxjs'; +import { TranslateService } from '@ngx-translate/core'; +import { BaseData, HasId } from '@shared/models/base-data'; +import { ActivatedRoute } from '@angular/router'; +import { + CellActionDescriptor, + EntityActionTableColumn, + EntityColumn, + EntityTableColumn, + EntityTableConfig, + GroupActionDescriptor, + HeaderActionDescriptor +} from '@home/models/entity/entities-table-config.models'; +import { EntityTypeTranslation } from '@shared/models/entity-type.models'; +import { DialogService } from '@core/services/dialog.service'; +import { AddEntityDialogComponent } from './add-entity-dialog.component'; +import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models'; +import { DAY, historyInterval, HistoryWindowType, Timewindow } from '@shared/models/time/time.models'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; +import { isDefined, isUndefined } from '@core/utils'; +import { HasUUID } from '../../../../shared/models/id/has-uuid'; + +@Component({ + selector: 'tb-entities-table', + templateUrl: './entities-table.component.html', + styleUrls: ['./entities-table.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class EntitiesTableComponent extends PageComponent implements AfterViewInit, OnInit, OnChanges { + + @Input() + entitiesTableConfig: EntityTableConfig>; + + translations: EntityTypeTranslation; + + headerActionDescriptors: Array; + groupActionDescriptors: Array>>; + cellActionDescriptors: Array>>; + + actionColumns: Array>>; + entityColumns: Array>>; + + displayedColumns: string[]; + + headerCellStyleCache: Array = []; + + cellContentCache: Array = []; + cellTooltipCache: Array = []; + + cellStyleCache: Array = []; + + selectionEnabled; + + defaultPageSize = 10; + displayPagination = true; + pageSizeOptions; + pageLink: PageLink; + textSearchMode = false; + timewindow: Timewindow; + dataSource: EntitiesDataSource>; + + isDetailsOpen = false; + detailsPanelOpened = new EventEmitter(); + + @ViewChild('entityTableHeader', {static: true}) entityTableHeaderAnchor: TbAnchorComponent; + + @ViewChild('searchInput') searchInputField: ElementRef; + + @ViewChild(MatPaginator) paginator: MatPaginator; + @ViewChild(MatSort) sort: MatSort; + + private sortSubscription: Subscription; + private updateDataSubscription: Subscription; + private viewInited = false; + + constructor(protected store: Store, + public route: ActivatedRoute, + public translate: TranslateService, + public dialog: MatDialog, + private dialogService: DialogService, + private domSanitizer: DomSanitizer, + private cd: ChangeDetectorRef, + private componentFactoryResolver: ComponentFactoryResolver) { + super(store); + } + + ngOnInit() { + if (this.entitiesTableConfig) { + this.init(this.entitiesTableConfig); + } else { + this.init(this.route.snapshot.data.entitiesTableConfig); + } + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'entitiesTableConfig' && change.currentValue) { + this.init(change.currentValue); + } + } + } + } + + private init(entitiesTableConfig: EntityTableConfig>) { + this.isDetailsOpen = false; + this.entitiesTableConfig = entitiesTableConfig; + if (this.entitiesTableConfig.headerComponent) { + const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.entitiesTableConfig.headerComponent); + const viewContainerRef = this.entityTableHeaderAnchor.viewContainerRef; + viewContainerRef.clear(); + const componentRef = viewContainerRef.createComponent(componentFactory); + const headerComponent = componentRef.instance; + headerComponent.entitiesTableConfig = this.entitiesTableConfig; + } + + this.entitiesTableConfig.table = this; + this.translations = this.entitiesTableConfig.entityTranslations; + + this.headerActionDescriptors = [...this.entitiesTableConfig.headerActionDescriptors]; + this.groupActionDescriptors = [...this.entitiesTableConfig.groupActionDescriptors]; + this.cellActionDescriptors = [...this.entitiesTableConfig.cellActionDescriptors]; + + if (this.entitiesTableConfig.entitiesDeleteEnabled) { + this.cellActionDescriptors.push( + { + name: this.translate.instant('action.delete'), + icon: 'delete', + isEnabled: entity => this.entitiesTableConfig.deleteEnabled(entity), + onAction: ($event, entity) => this.deleteEntity($event, entity) + } + ); + this.groupActionDescriptors.push( + { + name: this.translate.instant('action.delete'), + icon: 'delete', + isEnabled: true, + onAction: ($event, entities) => this.deleteEntities($event, entities) + } + ); + } + + const enabledGroupActionDescriptors = + this.groupActionDescriptors.filter((descriptor) => descriptor.isEnabled); + + this.selectionEnabled = this.entitiesTableConfig.selectionEnabled && enabledGroupActionDescriptors.length; + + this.columnsUpdated(); + + let sortOrder: SortOrder = null; + if (this.entitiesTableConfig.defaultSortOrder) { + sortOrder = { + property: this.entitiesTableConfig.defaultSortOrder.property, + direction: this.entitiesTableConfig.defaultSortOrder.direction + }; + } + + this.displayPagination = this.entitiesTableConfig.displayPagination; + this.defaultPageSize = this.entitiesTableConfig.defaultPageSize; + this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3]; + + if (this.entitiesTableConfig.useTimePageLink) { + this.timewindow = historyInterval(DAY); + const currentTime = Date.now(); + this.pageLink = new TimePageLink(10, 0, null, sortOrder, + currentTime - this.timewindow.history.timewindowMs, currentTime); + } else { + this.pageLink = new PageLink(10, 0, null, sortOrder); + } + this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : MAX_SAFE_PAGE_SIZE; + this.dataSource = this.entitiesTableConfig.dataSource(this.dataLoaded.bind(this)); + if (this.entitiesTableConfig.onLoadAction) { + this.entitiesTableConfig.onLoadAction(this.route); + } + if (this.entitiesTableConfig.loadDataOnInit) { + this.dataSource.loadEntities(this.pageLink); + } + if (this.viewInited) { + setTimeout(() => { + this.updatePaginationSubscriptions(); + }, 0); + } + } + + ngAfterViewInit() { + + fromEvent(this.searchInputField.nativeElement, 'keyup') + .pipe( + debounceTime(150), + distinctUntilChanged(), + tap(() => { + if (this.displayPagination) { + this.paginator.pageIndex = 0; + } + this.updateData(); + }) + ) + .subscribe(); + + this.updatePaginationSubscriptions(); + this.viewInited = true; + } + + private updatePaginationSubscriptions() { + if (this.sortSubscription) { + this.sortSubscription.unsubscribe(); + this.sortSubscription = null; + } + if (this.updateDataSubscription) { + this.updateDataSubscription.unsubscribe(); + this.updateDataSubscription = null; + } + if (this.displayPagination) { + this.sortSubscription = this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); + } + this.updateDataSubscription = ((this.displayPagination ? merge(this.sort.sortChange, this.paginator.page) + : this.sort.sortChange) as Observable) + .pipe( + tap(() => this.updateData()) + ) + .subscribe(); + } + + addEnabled() { + return this.entitiesTableConfig.addEnabled; + } + + clearSelection() { + this.dataSource.selection.clear(); + this.cd.detectChanges(); + } + + updateData(closeDetails: boolean = true) { + if (closeDetails) { + this.isDetailsOpen = false; + } + if (this.displayPagination) { + this.pageLink.page = this.paginator.pageIndex; + this.pageLink.pageSize = this.paginator.pageSize; + } else { + this.pageLink.page = 0; + } + if (this.sort.active) { + this.pageLink.sortOrder = { + property: this.sort.active, + direction: Direction[this.sort.direction.toUpperCase()] + }; + } else { + this.pageLink.sortOrder = null; + } + if (this.entitiesTableConfig.useTimePageLink) { + const timePageLink = this.pageLink as TimePageLink; + if (this.timewindow.history.historyType === HistoryWindowType.LAST_INTERVAL) { + const currentTime = Date.now(); + timePageLink.startTime = currentTime - this.timewindow.history.timewindowMs; + timePageLink.endTime = currentTime; + } else { + timePageLink.startTime = this.timewindow.history.fixedTimewindow.startTimeMs; + timePageLink.endTime = this.timewindow.history.fixedTimewindow.endTimeMs; + } + } + this.dataSource.loadEntities(this.pageLink); + } + + private dataLoaded(col?: number, row?: number) { + if (isFinite(col) && isFinite(row)) { + this.clearCellCache(col, row); + } else { + this.headerCellStyleCache.length = 0; + this.cellContentCache.length = 0; + this.cellTooltipCache.length = 0; + this.cellStyleCache.length = 0; + } + } + + onRowClick($event: Event, entity) { + if (!this.entitiesTableConfig.handleRowClick($event, entity)) { + this.toggleEntityDetails($event, entity); + } + } + + toggleEntityDetails($event: Event, entity) { + if ($event) { + $event.stopPropagation(); + } + if (this.dataSource.toggleCurrentEntity(entity)) { + this.isDetailsOpen = true; + } else { + this.isDetailsOpen = !this.isDetailsOpen; + } + this.detailsPanelOpened.emit(this.isDetailsOpen); + } + + addEntity($event: Event) { + let entity$: Observable>; + if (this.entitiesTableConfig.addEntity) { + entity$ = this.entitiesTableConfig.addEntity(); + } else { + entity$ = this.dialog.open>, + BaseData>(AddEntityDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + entitiesTableConfig: this.entitiesTableConfig + } + }).afterClosed(); + } + entity$.subscribe( + (entity) => { + if (entity) { + this.updateData(); + this.entitiesTableConfig.entityAdded(entity); + } + } + ); + } + + onEntityUpdated(entity: BaseData) { + this.updateData(false); + this.entitiesTableConfig.entityUpdated(entity); + } + + onEntityAction(action: EntityAction>) { + if (action.action === 'delete') { + this.deleteEntity(action.event, action.entity); + } + } + + deleteEntity($event: Event, entity: BaseData) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.entitiesTableConfig.deleteEntityTitle(entity), + this.entitiesTableConfig.deleteEntityContent(entity), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((result) => { + if (result) { + this.entitiesTableConfig.deleteEntity(entity.id).subscribe( + () => { + this.updateData(); + this.entitiesTableConfig.entitiesDeleted([entity.id]); + } + ); + } + }); + } + + deleteEntities($event: Event, entities: BaseData[]) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.entitiesTableConfig.deleteEntitiesTitle(entities.length), + this.entitiesTableConfig.deleteEntitiesContent(entities.length), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((result) => { + if (result) { + const tasks: Observable[] = []; + entities.forEach((entity) => { + if (this.entitiesTableConfig.deleteEnabled(entity)) { + tasks.push(this.entitiesTableConfig.deleteEntity(entity.id).pipe( + map(() => entity.id), + catchError(() => of(null) + ))); + } + }); + forkJoin(tasks).subscribe( + (ids) => { + this.updateData(); + this.entitiesTableConfig.entitiesDeleted(ids.filter(id => id !== null)); + } + ); + } + }); + } + + onTimewindowChange() { + this.updateData(); + } + + enterFilterMode() { + this.textSearchMode = true; + this.pageLink.textSearch = ''; + setTimeout(() => { + this.searchInputField.nativeElement.focus(); + this.searchInputField.nativeElement.setSelectionRange(0, 0); + }, 10); + } + + exitFilterMode() { + this.textSearchMode = false; + this.pageLink.textSearch = null; + if (this.displayPagination) { + this.paginator.pageIndex = 0; + } + this.updateData(); + } + + resetSortAndFilter(update: boolean = true, preserveTimewindow: boolean = false) { + this.pageLink.textSearch = null; + if (this.entitiesTableConfig.useTimePageLink && !preserveTimewindow) { + this.timewindow = historyInterval(DAY); + } + if (this.displayPagination) { + this.paginator.pageIndex = 0; + } + const sortable = this.sort.sortables.get(this.entitiesTableConfig.defaultSortOrder.property); + this.sort.active = sortable.id; + this.sort.direction = this.entitiesTableConfig.defaultSortOrder.direction === Direction.ASC ? 'asc' : 'desc'; + if (update) { + this.updateData(); + } + } + + columnsUpdated(resetData: boolean = false) { + this.entityColumns = this.entitiesTableConfig.columns.filter( + (column) => column instanceof EntityTableColumn) + .map(column => column as EntityTableColumn>); + this.actionColumns = this.entitiesTableConfig.columns.filter( + (column) => column instanceof EntityActionTableColumn) + .map(column => column as EntityActionTableColumn>); + + this.displayedColumns = []; + + if (this.selectionEnabled) { + this.displayedColumns.push('select'); + } + this.entitiesTableConfig.columns.forEach( + (column) => { + this.displayedColumns.push(column.key); + } + ); + this.displayedColumns.push('actions'); + this.headerCellStyleCache.length = 0; + this.cellContentCache.length = 0; + this.cellTooltipCache.length = 0; + this.cellStyleCache.length = 0; + if (resetData) { + this.dataSource.reset(); + } + } + + headerCellStyle(column: EntityColumn>) { + const index = this.entitiesTableConfig.columns.indexOf(column); + let res = this.headerCellStyleCache[index]; + if (!res) { + const widthStyle: any = {width: column.width}; + if (column.width !== '0px') { + widthStyle.minWidth = column.width; + widthStyle.maxWidth = column.width; + } + if (column instanceof EntityTableColumn) { + res = {...column.headerCellStyleFunction(column.key), ...widthStyle}; + } else { + res = widthStyle; + } + this.headerCellStyleCache[index] = res; + } + return res; + } + + clearCellCache(col: number, row: number) { + const index = row * this.entitiesTableConfig.columns.length + col; + this.cellContentCache[index] = undefined; + this.cellTooltipCache[index] = undefined; + this.cellStyleCache[index] = undefined; + } + + cellContent(entity: BaseData, column: EntityColumn>, row: number) { + if (column instanceof EntityTableColumn) { + const col = this.entitiesTableConfig.columns.indexOf(column); + const index = row * this.entitiesTableConfig.columns.length + col; + let res = this.cellContentCache[index]; + if (!res) { + res = this.domSanitizer.bypassSecurityTrustHtml(column.cellContentFunction(entity, column.key)); + this.cellContentCache[index] = res; + } + return res; + } else { + return ''; + } + } + + cellTooltip(entity: BaseData, column: EntityColumn>, row: number) { + if (column instanceof EntityTableColumn) { + const col = this.entitiesTableConfig.columns.indexOf(column); + const index = row * this.entitiesTableConfig.columns.length + col; + let res = this.cellTooltipCache[index]; + if (isUndefined(res)) { + res = column.cellTooltipFunction(entity, column.key); + res = isDefined(res) ? res : null; + this.cellTooltipCache[index] = res; + } else { + return res !== null ? res : undefined; + } + } else { + return undefined; + } + } + + cellStyle(entity: BaseData, column: EntityColumn>, row: number) { + const col = this.entitiesTableConfig.columns.indexOf(column); + const index = row * this.entitiesTableConfig.columns.length + col; + let res = this.cellStyleCache[index]; + if (!res) { + const widthStyle: any = {width: column.width}; + if (column.width !== '0px') { + widthStyle.minWidth = column.width; + widthStyle.maxWidth = column.width; + } + if (column instanceof EntityTableColumn) { + res = {...column.cellStyleFunction(entity, column.key), ...widthStyle}; + } else { + res = widthStyle; + } + this.cellStyleCache[index] = res; + } + return res; + } + + trackByColumnKey(index, column: EntityTableColumn>) { + return column.key; + } + + trackByEntityId(index: number, entity: BaseData) { + return entity.id.id; + } + +} diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.html b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.html new file mode 100644 index 0000000000..e459150eb9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.html @@ -0,0 +1,36 @@ + + +
+
+
+ + + + + + +
diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.scss b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.scss new file mode 100644 index 0000000000..c7abb0aef1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.scss @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; +} + +:host ::ng-deep { + tb-details-panel { + > .mat-content { + > .mat-tab-group { + > .mat-tab-body-wrapper { + position: absolute; + top: 49px; + left: 0; + right: 0; + bottom: 0; + } + > .mat-tab-header { + .mat-tab-label { + min-width: 40px; + } + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts new file mode 100644 index 0000000000..8eb046c384 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts @@ -0,0 +1,307 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ComponentFactoryResolver, + ComponentRef, + EventEmitter, + Injector, + Input, + OnDestroy, + OnInit, + Output, + QueryList, + ViewChild, + ViewChildren +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import { BaseData, HasId } from '@shared/models/base-data'; +import { EntityType, EntityTypeResource, EntityTypeTranslation } from '@shared/models/entity-type.models'; +import { FormGroup } from '@angular/forms'; +import { EntityComponent } from './entity.component'; +import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { Subscription } from 'rxjs'; +import { MatTab, MatTabGroup } from '@angular/material/tabs'; +import { EntityTabsComponent } from '@home/components/entity/entity-tabs.component'; +import { deepClone } from '@core/utils'; + +@Component({ + selector: 'tb-entity-details-panel', + templateUrl: './entity-details-panel.component.html', + styleUrls: ['./entity-details-panel.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class EntityDetailsPanelComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy { + + @Output() + closeEntityDetails = new EventEmitter(); + + @Output() + entityUpdated = new EventEmitter>(); + + @Output() + entityAction = new EventEmitter>>(); + + entityComponentRef: ComponentRef>>; + entityComponent: EntityComponent>; + + entityTabsComponentRef: ComponentRef>>; + entityTabsComponent: EntityTabsComponent>; + detailsForm: FormGroup; + + entitiesTableConfigValue: EntityTableConfig>; + isEditValue = false; + selectedTab = 0; + + entityTypes = EntityType; + + @ViewChild('entityDetailsForm', {static: true}) entityDetailsFormAnchor: TbAnchorComponent; + + @ViewChild('entityTabs', {static: true}) entityTabsAnchor: TbAnchorComponent; + + @ViewChild(MatTabGroup, {static: true}) matTabGroup: MatTabGroup; + + @ViewChildren(MatTab) inclusiveTabs: QueryList; + + translations: EntityTypeTranslation; + resources: EntityTypeResource>; + entity: BaseData; + editingEntity: BaseData; + + private currentEntityId: HasId; + private subscriptions: Subscription[] = []; + private viewInited = false; + private pendingTabs: MatTab[]; + + constructor(protected store: Store, + private injector: Injector, + private cd: ChangeDetectorRef, + private componentFactoryResolver: ComponentFactoryResolver) { + super(store); + } + + @Input() + set entityId(entityId: HasId) { + if (entityId && entityId !== this.currentEntityId) { + this.currentEntityId = entityId; + this.reload(); + } + } + + @Input() + set entitiesTableConfig(entitiesTableConfig: EntityTableConfig>) { + if (this.entitiesTableConfigValue !== entitiesTableConfig) { + this.entitiesTableConfigValue = entitiesTableConfig; + if (this.entitiesTableConfigValue) { + this.currentEntityId = null; + this.isEdit = false; + this.entity = null; + this.init(); + } + } + } + + get entitiesTableConfig(): EntityTableConfig> { + return this.entitiesTableConfigValue; + } + + set isEdit(val: boolean) { + this.isEditValue = val; + if (this.entityComponent) { + this.entityComponent.isEdit = val; + } + if (this.entityTabsComponent) { + this.entityTabsComponent.isEdit = val; + } + } + + get isEdit() { + return this.isEditValue; + } + + ngOnInit(): void { + this.init(); + } + + private init() { + this.translations = this.entitiesTableConfig.entityTranslations; + this.resources = this.entitiesTableConfig.entityResources; + this.buildEntityComponent(); + } + + private clearSubscriptions() { + this.subscriptions.forEach((subscription) => { + subscription.unsubscribe(); + }); + this.subscriptions.length = 0; + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + this.clearSubscriptions(); + } + + buildEntityComponent() { + this.clearSubscriptions(); + if (this.entityComponentRef) { + this.entityComponentRef.destroy(); + this.entityComponentRef = null; + } + const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.entitiesTableConfig.entityComponent); + const viewContainerRef = this.entityDetailsFormAnchor.viewContainerRef; + viewContainerRef.clear(); + const injector: Injector = Injector.create( + { + providers: [ + { + provide: 'entity', + useValue: this.entity + }, + { + provide: 'entitiesTableConfig', + useValue: this.entitiesTableConfig + } + ], + parent: this.injector + } + ); + this.entityComponentRef = viewContainerRef.createComponent(componentFactory, 0, injector); + this.entityComponent = this.entityComponentRef.instance; + this.entityComponent.isEdit = this.isEdit; + this.detailsForm = this.entityComponent.entityForm; + this.subscriptions.push(this.entityComponent.entityAction.subscribe((action) => { + this.entityAction.emit(action); + })); + this.buildEntityTabsComponent(); + this.subscriptions.push(this.entityComponent.entityForm.valueChanges.subscribe(() => { + this.cd.detectChanges(); + })); + } + + buildEntityTabsComponent() { + if (this.entityTabsComponentRef) { + this.entityTabsComponentRef.destroy(); + this.entityTabsComponentRef = null; + } + const viewContainerRef = this.entityTabsAnchor.viewContainerRef; + viewContainerRef.clear(); + this.entityTabsComponent = null; + if (this.entitiesTableConfig.entityTabsComponent) { + const componentTabsFactory = this.componentFactoryResolver.resolveComponentFactory(this.entitiesTableConfig.entityTabsComponent); + this.entityTabsComponentRef = viewContainerRef.createComponent(componentTabsFactory); + this.entityTabsComponent = this.entityTabsComponentRef.instance; + this.entityTabsComponent.isEdit = this.isEdit; + this.entityTabsComponent.entitiesTableConfig = this.entitiesTableConfig; + this.entityTabsComponent.detailsForm = this.detailsForm; + this.subscriptions.push(this.entityTabsComponent.entityTabsChanged.subscribe( + (entityTabs) => { + if (entityTabs) { + if (this.viewInited) { + this.matTabGroup._tabs.reset([...this.inclusiveTabs.toArray(), ...entityTabs]); + this.matTabGroup._tabs.notifyOnChanges(); + } else { + this.pendingTabs = entityTabs; + } + } + } + )); + } + } + + hideDetailsTabs(): boolean { + return this.isEditValue && this.entitiesTableConfig.hideDetailsTabsOnEdit; + } + + reload(): void { + this.isEdit = false; + this.entitiesTableConfig.loadEntity(this.currentEntityId).subscribe( + (entity) => { + this.entity = entity; + this.entityComponent.entity = entity; + if (this.entityTabsComponent) { + this.entityTabsComponent.entity = entity; + } + } + ); + } + + onCloseEntityDetails() { + this.closeEntityDetails.emit(); + } + + onToggleEditMode(isEdit: boolean) { + if (!isEdit) { + this.entityComponent.entity = this.entity; + if (this.entityTabsComponent) { + this.entityTabsComponent.entity = this.entity; + } + this.isEdit = isEdit; + } else { + this.isEdit = isEdit; + this.editingEntity = deepClone(this.entity); + this.entityComponent.entity = this.editingEntity; + if (this.entityTabsComponent) { + this.entityTabsComponent.entity = this.editingEntity; + } + if (this.entitiesTableConfig.hideDetailsTabsOnEdit) { + this.selectedTab = 0; + } + } + } + + helpLinkId(): string { + if (this.resources.helpLinkIdForEntity && this.entityComponent.entityForm) { + return this.resources.helpLinkIdForEntity(this.entityComponent.entityForm.getRawValue()); + } else { + return this.resources.helpLinkId; + } + } + + saveEntity() { + if (this.detailsForm.valid) { + const editingEntity = {...this.editingEntity, ...this.entityComponent.entityFormValue()}; + this.entitiesTableConfig.saveEntity(editingEntity).subscribe( + (entity) => { + this.entity = entity; + this.entityComponent.entity = entity; + if (this.entityTabsComponent) { + this.entityTabsComponent.entity = entity; + } + this.isEdit = false; + this.entityUpdated.emit(this.entity); + } + ); + } + } + + ngAfterViewInit(): void { + this.viewInited = true; + if (this.pendingTabs) { + this.matTabGroup._tabs.reset([...this.inclusiveTabs.toArray(), ...this.pendingTabs]); + this.matTabGroup._tabs.notifyOnChanges(); + this.pendingTabs = null; + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.html b/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.html new file mode 100644 index 0000000000..7eb55aa802 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.html @@ -0,0 +1,25 @@ + +
+
alias.no-entity-filter-specified
+ +
+
{{ filterDisplayValue }}
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.scss b/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.scss new file mode 100644 index 0000000000..77ac4bcb53 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.scss @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .tb-entity-filter-view { + .entity-filter-empty { + font-size: 14px; + line-height: 16px; + color: rgba(221, 44, 0, .87); + } + + .entity-filter-type { + font-size: 14px; + line-height: 16px; + color: rgba(0, 0, 0, .570588); + } + + .entity-filter-value { + font-size: 14px; + line-height: 16px; + color: rgba(0, 0, 0, .570588); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts new file mode 100644 index 0000000000..afde26d619 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts @@ -0,0 +1,230 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { AliasFilterType, EntityAliasFilter } from '@shared/models/alias.models'; +import { AliasEntityType, EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { TranslateService } from '@ngx-translate/core'; + +@Component({ + selector: 'tb-entity-filter-view', + templateUrl: './entity-filter-view.component.html', + styleUrls: ['./entity-filter-view.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntityFilterViewComponent), + multi: true + } + ] +}) +export class EntityFilterViewComponent implements ControlValueAccessor { + + constructor(private translate: TranslateService) {} + + filterDisplayValue: string; + filter: EntityAliasFilter; + + registerOnChange(fn: any): void { + } + + registerOnTouched(fn: any): void { + } + + setDisabledState?(isDisabled: boolean): void { + } + + writeValue(filter: EntityAliasFilter): void { + this.filter = filter; + if (this.filter && this.filter.type) { + let entityType: EntityType | AliasEntityType; + let prefix: string; + let allEntitiesText; + let anyRelationText; + let relationTypeText; + let rootEntityText; + let directionText; + switch (this.filter.type) { + case AliasFilterType.singleEntity: + entityType = this.filter.singleEntity.entityType; + this.filterDisplayValue = this.translate.instant(entityTypeTranslations.get(entityType).list, + {count: 1}); + break; + case AliasFilterType.entityList: + entityType = this.filter.entityType; + const count = this.filter.entityList.length; + this.filterDisplayValue = this.translate.instant(entityTypeTranslations.get(entityType).list, + {count}); + break; + case AliasFilterType.entityName: + entityType = this.filter.entityType; + prefix = this.filter.entityNameFilter; + this.filterDisplayValue = this.translate.instant(entityTypeTranslations.get(entityType).nameStartsWith, + {prefix}); + break; + case AliasFilterType.stateEntity: + this.filterDisplayValue = this.translate.instant('alias.filter-type-state-entity-description'); + break; + case AliasFilterType.assetType: + const assetType = this.filter.assetType; + prefix = this.filter.assetNameFilter; + if (prefix && prefix.length) { + this.filterDisplayValue = this.translate.instant('alias.filter-type-asset-type-and-name-description', + {assetType, prefix}); + } else { + this.filterDisplayValue = this.translate.instant('alias.filter-type-asset-type-description', + {assetType}); + } + break; + case AliasFilterType.deviceType: + const deviceType = this.filter.deviceType; + prefix = this.filter.deviceNameFilter; + if (prefix && prefix.length) { + this.filterDisplayValue = this.translate.instant('alias.filter-type-device-type-and-name-description', + {deviceType, prefix}); + } else { + this.filterDisplayValue = this.translate.instant('alias.filter-type-device-type-description', + {deviceType}); + } + break; + case AliasFilterType.entityViewType: + const entityView = this.filter.entityViewType; + prefix = this.filter.entityViewNameFilter; + if (prefix && prefix.length) { + this.filterDisplayValue = this.translate.instant('alias.filter-type-entity-view-type-and-name-description', + {entityView, prefix}); + } else { + this.filterDisplayValue = this.translate.instant('alias.filter-type-entity-view-type-description', + {entityView}); + } + break; + case AliasFilterType.relationsQuery: + allEntitiesText = this.translate.instant('alias.all-entities'); + anyRelationText = this.translate.instant('alias.any-relation'); + if (this.filter.rootStateEntity) { + rootEntityText = this.translate.instant('alias.state-entity'); + } else { + rootEntityText = this.translate.instant(entityTypeTranslations.get(this.filter.rootEntity.entityType).type); + } + directionText = this.translate.instant('relation.direction-type.' + this.filter.direction); + const relationFilters = this.filter.filters; + if (relationFilters && relationFilters.length) { + const relationFiltersDisplayValues = []; + relationFilters.forEach((relationFilter) => { + let entitiesText; + if (relationFilter.entityTypes && relationFilter.entityTypes.length) { + const entitiesNamesList = []; + relationFilter.entityTypes.forEach((filterEntityType) => { + entitiesNamesList.push( + this.translate.instant(entityTypeTranslations.get(filterEntityType).typePlural) + ); + }); + entitiesText = entitiesNamesList.join(', '); + } else { + entitiesText = allEntitiesText; + } + if (relationFilter.relationType && relationFilter.relationType.length) { + relationTypeText = `'${relationFilter.relationType}'`; + } else { + relationTypeText = anyRelationText; + } + const relationFilterDisplayValue = this.translate.instant('alias.filter-type-relations-query-description', + { + entities: entitiesText, + relationType: relationTypeText, + direction: directionText, + rootEntity: rootEntityText + } + ); + relationFiltersDisplayValues.push(relationFilterDisplayValue); + }); + this.filterDisplayValue = relationFiltersDisplayValues.join(', '); + } else { + this.filterDisplayValue = this.translate.instant('alias.filter-type-relations-query-description', + { + entities: allEntitiesText, + relationType: anyRelationText, + direction: directionText, + rootEntity: rootEntityText + } + ); + } + break; + case AliasFilterType.assetSearchQuery: + case AliasFilterType.deviceSearchQuery: + case AliasFilterType.entityViewSearchQuery: + allEntitiesText = this.translate.instant('alias.all-entities'); + anyRelationText = this.translate.instant('alias.any-relation'); + if (this.filter.rootStateEntity) { + rootEntityText = this.translate.instant('alias.state-entity'); + } else { + rootEntityText = this.translate.instant(entityTypeTranslations.get(this.filter.rootEntity.entityType).type); + } + directionText = this.translate.instant('relation.direction-type.' + this.filter.direction); + if (this.filter.relationType && this.filter.relationType.length) { + relationTypeText = `'${filter.relationType}'`; + } else { + relationTypeText = anyRelationText; + } + + const translationValues: any = { + relationType: relationTypeText, + direction: directionText, + rootEntity: rootEntityText + }; + + if (this.filter.type === AliasFilterType.assetSearchQuery) { + const assetTypesQuoted = []; + this.filter.assetTypes.forEach((filterAssetType) => { + assetTypesQuoted.push(`'${filterAssetType}'`); + }); + const assetTypesText = assetTypesQuoted.join(', '); + translationValues.assetTypes = assetTypesText; + this.filterDisplayValue = this.translate.instant('alias.filter-type-asset-search-query-description', + translationValues + ); + } else if (this.filter.type === AliasFilterType.deviceSearchQuery) { + const deviceTypesQuoted = []; + this.filter.deviceTypes.forEach((filterDeviceType) => { + deviceTypesQuoted.push(`'${filterDeviceType}'`); + }); + const deviceTypesText = deviceTypesQuoted.join(', '); + translationValues.deviceTypes = deviceTypesText; + this.filterDisplayValue = this.translate.instant('alias.filter-type-device-search-query-description', + translationValues + ); + } else if (this.filter.type === AliasFilterType.entityViewSearchQuery) { + const entityViewTypesQuoted = []; + this.filter.entityViewTypes.forEach((filterEntityViewType) => { + entityViewTypesQuoted.push(`'${filterEntityViewType}'`); + }); + const entityViewTypesText = entityViewTypesQuoted.join(', '); + translationValues.entityViewTypes = entityViewTypesText; + this.filterDisplayValue = this.translate.instant('alias.filter-type-entity-view-search-query-description', + translationValues + ); + } + break; + default: + this.filterDisplayValue = this.filter.type; + break; + } + } else { + this.filterDisplayValue = ''; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html new file mode 100644 index 0000000000..d736917c89 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html @@ -0,0 +1,260 @@ + +
+ + alias.filter-type + + + {{aliasFilterTypeTranslations.get(type) | translate}} + + + + {{ 'alias.filter-type-required' | translate }} + + +
+ + + + + + + + + + + + + + + entity.name-starts-with + + + {{ 'entity.entity-name-filter-required' | translate }} + + + + + + alias.state-entity-parameter-name + + +
+ + + +
+
+ + + + + asset.name-starts-with + + + + + + + + device.name-starts-with + + + + + + + + entity-view.name-starts-with + + + + +
+ +
+ + + +
+
+ + +
+
+ + alias.state-entity-parameter-name + + +
+ + + +
+
+
+
+ + + +
+
+
+ + relation.direction + + + {{ directionTypeTranslations.get(directionTypeEnum[type]) | translate }} + + + + + alias.max-relation-level + + +
+
relation.relation-filters
+ + +
+
+ + +
+ + + +
+
+ + +
+
+ + alias.state-entity-parameter-name + + +
+ + + +
+
+
+
+ + + +
+
+
+ + relation.direction + + + {{ directionTypeTranslations.get(directionTypeEnum[type]) | translate }} + + + + + alias.max-relation-level + + +
+
relation.relation-type
+ + +
+ +
asset.asset-types
+ + +
+ +
device.device-types
+ + +
+ +
entity-view.entity-view-types
+ + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.scss b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.scss new file mode 100644 index 0000000000..01e8dc7cc8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.scss @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .tb-entity-filter { + #relationsQueryFilter { + padding-top: 20px; + + tb-entity-select { + min-height: 92px; + } + } + + .tb-root-state-entity-switch { + padding-left: 10px; + padding-bottom: 10px; + + .root-state-entity-switch { + margin: 0; + } + + .root-state-entity-label { + margin: 5px 0 5px 10px; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts new file mode 100644 index 0000000000..64100eacfc --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts @@ -0,0 +1,213 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { AliasFilterType, aliasFilterTypeTranslationMap, EntityAliasFilter } from '@shared/models/alias.models'; +import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; +import { TranslateService } from '@ngx-translate/core'; +import { EntityService } from '@core/http/entity.service'; +import { EntitySearchDirection, entitySearchDirectionTranslations } from '@shared/models/relation.models'; + +@Component({ + selector: 'tb-entity-filter', + templateUrl: './entity-filter.component.html', + styleUrls: ['./entity-filter.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntityFilterComponent), + multi: true + } + ] +}) +export class EntityFilterComponent implements ControlValueAccessor, OnInit { + + @Input() disabled: boolean; + + @Input() allowedEntityTypes: Array; + + @Input() resolveMultiple: boolean; + + @Output() resolveMultipleChanged: EventEmitter = new EventEmitter(); + + entityFilterFormGroup: FormGroup; + filterFormGroup: FormGroup; + + aliasFilterTypes: Array; + + aliasFilterType = AliasFilterType; + aliasFilterTypeTranslations = aliasFilterTypeTranslationMap; + entityType = EntityType; + + directionTypes = Object.keys(EntitySearchDirection); + directionTypeTranslations = entitySearchDirectionTranslations; + directionTypeEnum = EntitySearchDirection; + + private propagateChange = null; + + constructor(private translate: TranslateService, + private entityService: EntityService, + private fb: FormBuilder) { + } + + ngOnInit(): void { + + this.aliasFilterTypes = this.entityService.getAliasFilterTypesByEntityTypes(this.allowedEntityTypes); + + this.entityFilterFormGroup = this.fb.group({ + type: [null, [Validators.required]] + }); + this.entityFilterFormGroup.get('type').valueChanges.subscribe((type: AliasFilterType) => { + this.filterTypeChanged(type); + }); + this.entityFilterFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + this.filterFormGroup = this.fb.group({}); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState?(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(filter: EntityAliasFilter): void { + if (!filter) { + filter = { + type: null, + resolveMultiple: this.resolveMultiple + }; + } + this.entityFilterFormGroup.get('type').patchValue(filter.type, {emitEvent: false}); + if (filter && filter.type) { + this.updateFilterFormGroup(filter.type, filter); + } + } + + private updateFilterFormGroup(type: AliasFilterType, filter?: EntityAliasFilter) { + switch (type) { + case AliasFilterType.singleEntity: + this.filterFormGroup = this.fb.group({ + singleEntity: [filter ? filter.singleEntity : null, [Validators.required]] + }); + break; + case AliasFilterType.entityList: + this.filterFormGroup = this.fb.group({ + entityType: [filter ? filter.entityType : null, [Validators.required]], + entityList: [filter ? filter.entityList : [], [Validators.required]], + }); + break; + case AliasFilterType.entityName: + this.filterFormGroup = this.fb.group({ + entityType: [filter ? filter.entityType : null, [Validators.required]], + entityNameFilter: [filter ? filter.entityNameFilter : '', [Validators.required]], + }); + break; + case AliasFilterType.stateEntity: + this.filterFormGroup = this.fb.group({ + stateEntityParamName: [filter ? filter.stateEntityParamName : null, []], + defaultStateEntity: [filter ? filter.defaultStateEntity : null, []], + }); + break; + case AliasFilterType.assetType: + this.filterFormGroup = this.fb.group({ + assetType: [filter ? filter.assetType : null, [Validators.required]], + assetNameFilter: [filter ? filter.assetNameFilter : '', []], + }); + break; + case AliasFilterType.deviceType: + this.filterFormGroup = this.fb.group({ + deviceType: [filter ? filter.deviceType : null, [Validators.required]], + deviceNameFilter: [filter ? filter.deviceNameFilter : '', []], + }); + break; + case AliasFilterType.entityViewType: + this.filterFormGroup = this.fb.group({ + entityViewType: [filter ? filter.entityViewType : null, [Validators.required]], + entityViewNameFilter: [filter ? filter.entityViewNameFilter : '', []], + }); + break; + case AliasFilterType.relationsQuery: + case AliasFilterType.assetSearchQuery: + case AliasFilterType.deviceSearchQuery: + case AliasFilterType.entityViewSearchQuery: + this.filterFormGroup = this.fb.group({ + rootStateEntity: [filter ? filter.rootStateEntity : false, []], + stateEntityParamName: [filter ? filter.stateEntityParamName : null, []], + defaultStateEntity: [filter ? filter.defaultStateEntity : null, []], + rootEntity: [filter ? filter.rootEntity : null, (filter && filter.rootStateEntity) ? [] : [Validators.required]], + direction: [filter ? filter.direction : EntitySearchDirection.FROM, [Validators.required]], + maxLevel: [filter ? filter.maxLevel : 1, []], + fetchLastLevelOnly: [filter ? filter.fetchLastLevelOnly : false, []] + }); + this.filterFormGroup.get('rootStateEntity').valueChanges.subscribe((rootStateEntity: boolean) => { + this.filterFormGroup.get('rootEntity').setValidators(rootStateEntity ? [] : [Validators.required]); + this.filterFormGroup.get('rootEntity').updateValueAndValidity(); + }); + if (type === AliasFilterType.relationsQuery) { + this.filterFormGroup.addControl('filters', + this.fb.control(filter ? filter.filters : [], [])); + } else { + this.filterFormGroup.addControl('relationType', + this.fb.control(filter ? filter.relationType : null, [])); + if (type === AliasFilterType.assetSearchQuery) { + this.filterFormGroup.addControl('assetTypes', + this.fb.control(filter ? filter.assetTypes : [], [Validators.required])); + } else if (type === AliasFilterType.deviceSearchQuery) { + this.filterFormGroup.addControl('deviceTypes', + this.fb.control(filter ? filter.deviceTypes : [], [Validators.required])); + } else if (type === AliasFilterType.entityViewSearchQuery) { + this.filterFormGroup.addControl('entityViewTypes', + this.fb.control(filter ? filter.entityViewTypes : [], [Validators.required])); + } + } + break; + } + this.filterFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + private filterTypeChanged(type: AliasFilterType) { + let resolveMultiple = true; + if (type === AliasFilterType.singleEntity || type === AliasFilterType.stateEntity) { + resolveMultiple = false; + } + if (this.resolveMultiple !== resolveMultiple) { + this.resolveMultipleChanged.emit(resolveMultiple); + } + this.updateFilterFormGroup(type); + } + + private updateModel() { + let filter = null; + if (this.entityFilterFormGroup.valid && this.filterFormGroup.valid) { + filter = { + type: this.entityFilterFormGroup.get('type').value, + resolveMultiple: this.resolveMultiple + }; + filter = {...filter, ...this.filterFormGroup.value}; + } + this.propagateChange(filter); + } +} diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-table-header.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-table-header.component.ts new file mode 100644 index 0000000000..ba04af086f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/entity-table-header.component.ts @@ -0,0 +1,55 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { BaseData, HasId } from '@shared/models/base-data'; +import { PageComponent } from '@shared/components/page.component'; +import { Directive, Input, OnInit } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import { PageLink } from '@shared/models/page/page-link'; + +@Directive() +// tslint:disable-next-line:directive-class-suffix +export abstract class EntityTableHeaderComponent, + P extends PageLink = PageLink, + L extends BaseData = T, + C extends EntityTableConfig = EntityTableConfig> + extends PageComponent implements OnInit { + + entitiesTableConfigValue: C; + + @Input() + set entitiesTableConfig(entitiesTableConfig: C) { + this.setEntitiesTableConfig(entitiesTableConfig); + } + + get entitiesTableConfig(): C { + return this.entitiesTableConfigValue; + } + + protected constructor(protected store: Store) { + super(store); + } + + ngOnInit() { + } + + protected setEntitiesTableConfig(entitiesTableConfig: C) { + this.entitiesTableConfigValue = entitiesTableConfig; + } + +} diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-tabs.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-tabs.component.ts new file mode 100644 index 0000000000..46da11a9a2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/entity-tabs.component.ts @@ -0,0 +1,128 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { BaseData, HasId } from '@shared/models/base-data'; +import { PageComponent } from '@shared/components/page.component'; +import { AfterViewInit, Directive, Input, OnInit, QueryList, ViewChildren } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import { MatTab } from '@angular/material/tabs'; +import { BehaviorSubject } from 'rxjs'; +import { Authority } from '@app/shared/models/authority.enum'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { AuthUser } from '@shared/models/user.model'; +import { EntityType } from '@shared/models/entity-type.models'; +import { AuditLogMode } from '@shared/models/audit-log.models'; +import { DebugEventType, EventType } from '@shared/models/event.models'; +import { AttributeScope, LatestTelemetry } from '@shared/models/telemetry/telemetry.models'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { FormGroup } from '@angular/forms'; +import { PageLink } from '@shared/models/page/page-link'; + +@Directive() +// tslint:disable-next-line:directive-class-suffix +export abstract class EntityTabsComponent, + P extends PageLink = PageLink, + L extends BaseData = T, + C extends EntityTableConfig = EntityTableConfig> + extends PageComponent implements OnInit, AfterViewInit { + + attributeScopes = AttributeScope; + latestTelemetryTypes = LatestTelemetry; + + authorities = Authority; + + entityTypes = EntityType; + + auditLogModes = AuditLogMode; + + eventTypes = EventType; + + debugEventTypes = DebugEventType; + + authUser: AuthUser; + + nullUid = NULL_UUID; + + entityValue: T; + + entitiesTableConfigValue: C; + + @ViewChildren(MatTab) entityTabs: QueryList; + + isEditValue: boolean; + + @Input() + set isEdit(isEdit: boolean) { + this.isEditValue = isEdit; + } + + get isEdit() { + return this.isEditValue; + } + + @Input() + set entity(entity: T) { + this.setEntity(entity); + } + + get entity(): T { + return this.entityValue; + } + + @Input() + set entitiesTableConfig(entitiesTableConfig: C) { + this.setEntitiesTableConfig(entitiesTableConfig); + } + + get entitiesTableConfig(): C { + return this.entitiesTableConfigValue; + } + + @Input() + detailsForm: FormGroup; + + private entityTabsSubject = new BehaviorSubject>(null); + + entityTabsChanged = this.entityTabsSubject.asObservable(); + + protected constructor(protected store: Store) { + super(store); + this.authUser = getCurrentAuthUser(store); + } + + ngOnInit() { + } + + ngAfterViewInit(): void { + this.entityTabsSubject.next(this.entityTabs.toArray()); + this.entityTabs.changes.subscribe( + () => { + this.entityTabsSubject.next(this.entityTabs.toArray()); + } + ); + } + + protected setEntity(entity: T) { + this.entityValue = entity; + } + + protected setEntitiesTableConfig(entitiesTableConfig: C) { + this.entitiesTableConfigValue = entitiesTableConfig; + } + +} diff --git a/ui-ngx/src/app/modules/home/components/entity/entity.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity.component.ts new file mode 100644 index 0000000000..6a76f1e062 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/entity.component.ts @@ -0,0 +1,129 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { BaseData, HasId } from '@shared/models/base-data'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Directive, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import { PageLink } from '@shared/models/page/page-link'; +import { deepTrim } from '@core/utils'; + +// @dynamic +@Directive() +// tslint:disable-next-line:directive-class-suffix +export abstract class EntityComponent, + P extends PageLink = PageLink, + L extends BaseData = T, + C extends EntityTableConfig = EntityTableConfig> + extends PageComponent implements OnInit { + + entityForm: FormGroup; + + isEditValue: boolean; + + @Input() + set entitiesTableConfig(entitiesTableConfig: C) { + this.setEntitiesTableConfig(entitiesTableConfig); + } + + get entitiesTableConfig(): C { + return this.entitiesTableConfigValue; + } + + @Input() + set isEdit(isEdit: boolean) { + this.isEditValue = isEdit; + this.updateFormState(); + } + + get isEdit() { + return this.isEditValue; + } + + get isAdd(): boolean { + return this.entityValue && (!this.entityValue.id || !this.entityValue.id.id); + } + + @Input() + set entity(entity: T) { + this.entityValue = entity; + if (this.entityForm) { + this.entityForm.markAsPristine(); + this.updateForm(entity); + } + } + + get entity(): T { + return this.entityValue; + } + + @Output() + entityAction = new EventEmitter>(); + + protected constructor(protected store: Store, + protected fb: FormBuilder, + protected entityValue: T, + protected entitiesTableConfigValue: C) { + super(store); + this.entityForm = this.buildForm(this.entityValue); + } + + ngOnInit() { + } + + onEntityAction($event: Event, action: string) { + const entityAction = {event: $event, action, entity: this.entity} as EntityAction; + let handled = false; + if (this.entitiesTableConfig) { + handled = this.entitiesTableConfig.onEntityAction(entityAction); + } + if (!handled) { + this.entityAction.emit(entityAction); + } + } + + updateFormState() { + if (this.entityForm) { + if (this.isEditValue) { + this.entityForm.enable({emitEvent: false}); + } else { + this.entityForm.disable({emitEvent: false}); + } + } + } + + entityFormValue() { + const formValue = this.entityForm ? {...this.entityForm.getRawValue()} : {}; + return this.prepareFormValue(formValue); + } + + prepareFormValue(formValue: any): any { + return deepTrim(formValue); + } + + protected setEntitiesTableConfig(entitiesTableConfig: C) { + this.entitiesTableConfigValue = entitiesTableConfig; + } + + abstract buildForm(entity: T): FormGroup; + + abstract updateForm(entity: T); + +} diff --git a/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.html b/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.html new file mode 100644 index 0000000000..e4ee790806 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.html @@ -0,0 +1,39 @@ + + +

{{ title }}

+ + +
+
+
+
+
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.scss b/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.scss new file mode 100644 index 0000000000..b3765502d4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.scss @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .tb-event-content { + width: 100%; + min-width: 400px; + height: 100%; + min-height: 50px; + } +} diff --git a/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.ts b/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.ts new file mode 100644 index 0000000000..e0359354a9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.ts @@ -0,0 +1,116 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, Inject, OnInit, Renderer2, ViewChild } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +import * as ace from 'ace-builds'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; +import { ContentType, contentTypesMap } from '@shared/models/constants'; + +export interface EventContentDialogData { + content: string; + title: string; + contentType: ContentType; +} + +@Component({ + selector: 'tb-event-content-dialog', + templateUrl: './event-content-dialog.component.html', + styleUrls: ['./event-content-dialog.component.scss'] +}) +export class EventContentDialogComponent extends DialogComponent implements OnInit { + + @ViewChild('eventContentEditor', {static: true}) + eventContentEditorElmRef: ElementRef; + private eventContentEditor: ace.Ace.Editor; + + content: string; + title: string; + contentType: ContentType; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: EventContentDialogData, + public dialogRef: MatDialogRef, + private renderer: Renderer2) { + super(store, router, dialogRef); + } + + ngOnInit(): void { + this.content = this.data.content; + this.title = this.data.title; + this.contentType = this.data.contentType; + + this.eventContentEditor = this.createEditor(this.eventContentEditorElmRef, this.content); + } + + createEditor(editorElementRef: ElementRef, content: string): ace.Ace.Editor { + const editorElement = editorElementRef.nativeElement; + let mode = 'java'; + if (this.contentType) { + mode = contentTypesMap.get(this.contentType).code; + if (this.contentType === ContentType.JSON && content) { + content = js_beautify(content, {indent_size: 4}); + } + } + let editorOptions: Partial = { + mode: `ace/mode/${mode}`, + theme: 'ace/theme/github', + showGutter: false, + showPrintMargin: false, + readOnly: true + }; + + const advancedOptions = { + enableSnippets: false, + enableBasicAutocompletion: false, + enableLiveAutocompletion: false + }; + + editorOptions = {...editorOptions, ...advancedOptions}; + const editor = ace.edit(editorElement, editorOptions); + editor.session.setUseWrapMode(false); + editor.setValue(content, -1); + this.updateEditorSize(editorElement, content, editor); + return editor; + } + + updateEditorSize(editorElement: any, content: string, editor: ace.Ace.Editor) { + let newHeight = 400; + let newWidth = 600; + if (content && content.length > 0) { + const lines = content.split('\n'); + newHeight = 16 * lines.length + 16; + let maxLineLength = 0; + lines.forEach((row) => { + const line = row.replace(/\t/g, ' ').replace(/\n/g, ''); + const lineLength = line.length; + maxLineLength = Math.max(maxLineLength, lineLength); + }); + newWidth = 8 * maxLineLength + 16; + } + // newHeight = Math.min(400, newHeight); + this.renderer.setStyle(editorElement, 'minHeight', newHeight.toString() + 'px'); + this.renderer.setStyle(editorElement, 'height', newHeight.toString() + 'px'); + this.renderer.setStyle(editorElement, 'width', newWidth.toString() + 'px'); + editor.resize(); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts new file mode 100644 index 0000000000..a0bed03aad --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts @@ -0,0 +1,252 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + DateEntityTableColumn, + EntityActionTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { DebugEventType, Event, EventType } from '@shared/models/event.models'; +import { TimePageLink } from '@shared/models/page/page-link'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { MatDialog } from '@angular/material/dialog'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EventService } from '@app/core/http/event.service'; +import { EventTableHeaderComponent } from '@home/components/event/event-table-header.component'; +import { EntityTypeResource } from '@shared/models/entity-type.models'; +import { Observable } from 'rxjs'; +import { PageData } from '@shared/models/page/page-data'; +import { Direction } from '@shared/models/page/sort-order'; +import { DialogService } from '@core/services/dialog.service'; +import { ContentType } from '@shared/models/constants'; +import { + EventContentDialogComponent, + EventContentDialogData +} from '@home/components/event/event-content-dialog.component'; +import { sortObjectKeys } from '@core/utils'; + +export class EventTableConfig extends EntityTableConfig { + + eventTypeValue: EventType | DebugEventType; + + set eventType(eventType: EventType | DebugEventType) { + if (this.eventTypeValue !== eventType) { + this.eventTypeValue = eventType; + this.updateColumns(true); + } + } + + get eventType(): EventType | DebugEventType { + return this.eventTypeValue; + } + + eventTypes: Array; + + constructor(private eventService: EventService, + private dialogService: DialogService, + private translate: TranslateService, + private datePipe: DatePipe, + private dialog: MatDialog, + public entityId: EntityId, + public tenantId: string, + private defaultEventType: EventType | DebugEventType, + private disabledEventTypes: Array = null, + private debugEventTypes: Array = null) { + super(); + this.loadDataOnInit = false; + this.tableTitle = ''; + this.useTimePageLink = true; + this.detailsPanelEnabled = false; + this.selectionEnabled = false; + this.searchEnabled = false; + this.addEnabled = false; + this.entitiesDeleteEnabled = false; + + this.headerComponent = EventTableHeaderComponent; + + this.eventTypes = Object.keys(EventType).map(type => EventType[type]); + + if (disabledEventTypes && disabledEventTypes.length) { + this.eventTypes = this.eventTypes.filter(type => disabledEventTypes.indexOf(type) === -1); + } + + if (debugEventTypes && debugEventTypes.length) { + this.eventTypes = [...this.eventTypes, ...debugEventTypes]; + } + + this.eventTypeValue = defaultEventType; + + this.entityTranslations = { + noEntities: 'event.no-events-prompt' + }; + this.entityResources = { + } as EntityTypeResource; + this.entitiesFetchFunction = pageLink => this.fetchEvents(pageLink); + + this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; + + this.updateColumns(); + } + + fetchEvents(pageLink: TimePageLink): Observable> { + return this.eventService.getEvents(this.entityId, this.eventType, this.tenantId, pageLink); + } + + updateColumns(updateTableColumns: boolean = false): void { + this.columns = []; + this.columns.push( + new DateEntityTableColumn('createdTime', 'event.event-time', this.datePipe, '120px'), + new EntityTableColumn('server', 'event.server', '100px', + (entity) => entity.body.server, entity => ({}), false)); + switch (this.eventType) { + case EventType.ERROR: + this.columns.push( + new EntityTableColumn('method', 'event.method', '100%', + (entity) => entity.body.method, entity => ({}), false), + new EntityActionTableColumn('error', 'event.error', + { + name: this.translate.instant('action.view'), + icon: 'more_horiz', + isEnabled: (entity) => entity.body.error && entity.body.error.length > 0, + onAction: ($event, entity) => this.showContent($event, entity.body.error, 'event.error') + }, + '100px') + ); + break; + case EventType.LC_EVENT: + this.columns.push( + new EntityTableColumn('method', 'event.event', '100%', + (entity) => entity.body.event, entity => ({}), false), + new EntityTableColumn('status', 'event.status', '100%', + (entity) => + this.translate.instant(entity.body.success ? 'event.success' : 'event.failed'), entity => ({}), false), + new EntityActionTableColumn('error', 'event.error', + { + name: this.translate.instant('action.view'), + icon: 'more_horiz', + isEnabled: (entity) => entity.body.error && entity.body.error.length > 0, + onAction: ($event, entity) => this.showContent($event, entity.body.error, 'event.error') + }, + '100px') + ); + break; + case EventType.STATS: + this.columns.push( + new EntityTableColumn('messagesProcessed', 'event.messages-processed', '50%', + (entity) => entity.body.messagesProcessed + '', + () => ({}), + false, + () => ({}), () => undefined, true), + new EntityTableColumn('errorsOccurred', 'event.errors-occurred', '50%', + (entity) => entity.body.errorsOccurred + '', + () => ({}), + false, + () => ({}), () => undefined, true) + ); + break; + case DebugEventType.DEBUG_RULE_NODE: + case DebugEventType.DEBUG_RULE_CHAIN: + this.columns[0].width = '100px'; + this.columns.push( + new EntityTableColumn('type', 'event.type', '40px', + (entity) => entity.body.type, entity => ({ + padding: '0 12px 0 0', + }), false, key => ({ + padding: '0 12px 0 0' + })), + new EntityTableColumn('entity', 'event.entity', '100px', + (entity) => entity.body.entityName, entity => ({ + padding: '0 12px 0 0', + }), false, key => ({ + padding: '0 12px 0 0' + })), + new EntityTableColumn('msgId', 'event.message-id', '100px', + (entity) => entity.body.msgId, entity => ({ + whiteSpace: 'nowrap', + padding: '0 12px 0 0' + }), false, key => ({ + padding: '0 12px 0 0' + }), + entity => entity.body.msgId), + new EntityTableColumn('msgType', 'event.message-type', '100px', + (entity) => entity.body.msgType, entity => ({ + whiteSpace: 'nowrap', + padding: '0 12px 0 0' + }), false, key => ({ + padding: '0 12px 0 0' + }), + entity => entity.body.msgType), + new EntityTableColumn('relationType', 'event.relation-type', '100px', + (entity) => entity.body.relationType, entity => ({padding: '0 12px 0 0', }), false, key => ({ + padding: '0 12px 0 0' + })), + new EntityActionTableColumn('data', 'event.data', + { + name: this.translate.instant('action.view'), + icon: 'more_horiz', + isEnabled: (entity) => entity.body.data ? entity.body.data.length > 0 : false, + onAction: ($event, entity) => this.showContent($event, entity.body.data, + 'event.data', entity.body.dataType) + }, + '40px'), + new EntityActionTableColumn('metadata', 'event.metadata', + { + name: this.translate.instant('action.view'), + icon: 'more_horiz', + isEnabled: (entity) => entity.body.metadata ? entity.body.metadata.length > 0 : false, + onAction: ($event, entity) => this.showContent($event, entity.body.metadata, + 'event.metadata', ContentType.JSON, true) + }, + '40px'), + new EntityActionTableColumn('error', 'event.error', + { + name: this.translate.instant('action.view'), + icon: 'more_horiz', + isEnabled: (entity) => entity.body.error && entity.body.error.length > 0, + onAction: ($event, entity) => this.showContent($event, entity.body.error, + 'event.error') + }, + '40px') + ); + break; + } + if (updateTableColumns) { + this.table.columnsUpdated(true); + } + } + + showContent($event: MouseEvent, content: string, title: string, contentType: ContentType = null, sortKeys = false): void { + if ($event) { + $event.stopPropagation(); + } + if (contentType === ContentType.JSON && sortKeys) { + try { + content = JSON.stringify(sortObjectKeys(JSON.parse(content))); + } catch (e) {} + } + this.dialog.open(EventContentDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + content, + title, + contentType + } + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/event/event-table-header.component.html b/ui-ngx/src/app/modules/home/components/event/event-table-header.component.html new file mode 100644 index 0000000000..25c85d6b60 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/event/event-table-header.component.html @@ -0,0 +1,26 @@ + + + event.event-type + + + {{ eventTypeTranslationsMap.get(type) | translate }} + + + diff --git a/ui-ngx/src/app/modules/home/components/event/event-table-header.component.scss b/ui-ngx/src/app/modules/home/components/event/event-table-header.component.scss new file mode 100644 index 0000000000..416ac63175 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/event/event-table-header.component.scss @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../scss/constants'; + +:host { +/* flex: 1; + display: flex; + justify-content: flex-start; */ + padding-right: 8px; + min-width: 160px; +} + +:host ::ng-deep { + mat-form-field { + font-size: 16px; + width: 200px; + + .mat-form-field-wrapper { + padding-bottom: 0; + } + + .mat-form-field-underline { + bottom: 0; + } + + @media #{$mat-xs} { + width: 100%; + + .mat-form-field-infix { + width: auto !important; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/event/event-table-header.component.ts b/ui-ngx/src/app/modules/home/components/event/event-table-header.component.ts new file mode 100644 index 0000000000..769f1e8585 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/event/event-table-header.component.ts @@ -0,0 +1,45 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTableHeaderComponent } from '../../components/entity/entity-table-header.component'; +import { DebugEventType, Event, EventType, eventTypeTranslations } from '@app/shared/models/event.models'; +import { EventTableConfig } from '@home/components/event/event-table-config'; + +@Component({ + selector: 'tb-event-table-header', + templateUrl: './event-table-header.component.html', + styleUrls: ['./event-table-header.component.scss'] +}) +export class EventTableHeaderComponent extends EntityTableHeaderComponent { + + eventTypeTranslationsMap = eventTypeTranslations; + + get eventTableConfig(): EventTableConfig { + return this.entitiesTableConfig as EventTableConfig; + } + + constructor(protected store: Store) { + super(store); + } + + eventTypeChanged(eventType: EventType | DebugEventType) { + this.eventTableConfig.eventType = eventType; + this.eventTableConfig.table.resetSortAndFilter(true, true); + } +} diff --git a/ui-ngx/src/app/modules/home/components/event/event-table.component.html b/ui-ngx/src/app/modules/home/components/event/event-table.component.html new file mode 100644 index 0000000000..d8f0277a6c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/event/event-table.component.html @@ -0,0 +1,18 @@ + + diff --git a/ui-ngx/src/app/modules/home/components/event/event-table.component.scss b/ui-ngx/src/app/modules/home/components/event/event-table.component.scss new file mode 100644 index 0000000000..719941ef46 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/event/event-table.component.scss @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host ::ng-deep { + tb-entities-table { + .mat-drawer-container { + background-color: white; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/event/event-table.component.ts b/ui-ngx/src/app/modules/home/components/event/event-table.component.ts new file mode 100644 index 0000000000..aa6ce5dd77 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/event/event-table.component.ts @@ -0,0 +1,102 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Input, OnInit, ViewChild } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { MatDialog } from '@angular/material/dialog'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; +import { EventTableConfig } from './event-table-config'; +import { EventService } from '@core/http/event.service'; +import { DialogService } from '@core/services/dialog.service'; +import { DebugEventType, EventType } from '@shared/models/event.models'; + +@Component({ + selector: 'tb-event-table', + templateUrl: './event-table.component.html', + styleUrls: ['./event-table.component.scss'] +}) +export class EventTableComponent implements OnInit { + + @Input() + tenantId: string; + + @Input() + defaultEventType: EventType | DebugEventType; + + @Input() + disabledEventTypes: Array; + + @Input() + debugEventTypes: Array; + + activeValue = false; + dirtyValue = false; + entityIdValue: EntityId; + + @Input() + set active(active: boolean) { + if (this.activeValue !== active) { + this.activeValue = active; + if (this.activeValue && this.dirtyValue) { + this.dirtyValue = false; + this.entitiesTable.updateData(); + } + } + } + + @Input() + set entityId(entityId: EntityId) { + this.entityIdValue = entityId; + if (this.eventTableConfig && this.eventTableConfig.entityId !== entityId) { + this.eventTableConfig.eventType = this.defaultEventType; + this.eventTableConfig.entityId = entityId; + this.entitiesTable.resetSortAndFilter(this.activeValue); + if (!this.activeValue) { + this.dirtyValue = true; + } + } + } + + @ViewChild(EntitiesTableComponent, {static: true}) entitiesTable: EntitiesTableComponent; + + eventTableConfig: EventTableConfig; + + constructor(private eventService: EventService, + private dialogService: DialogService, + private translate: TranslateService, + private datePipe: DatePipe, + private dialog: MatDialog) { + } + + ngOnInit() { + this.dirtyValue = !this.activeValue; + this.eventTableConfig = new EventTableConfig( + this.eventService, + this.dialogService, + this.translate, + this.datePipe, + this.dialog, + this.entityIdValue, + this.tenantId, + this.defaultEventType, + this.disabledEventTypes, + this.debugEventTypes + ); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html new file mode 100644 index 0000000000..d05e4b3775 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html @@ -0,0 +1,32 @@ + +
+ + + + + {{booleanOperationTranslations.get(booleanOperationEnum[operation]) | translate}} + + + + + +
diff --git a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts new file mode 100644 index 0000000000..aa264697b6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts @@ -0,0 +1,97 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { + BooleanFilterPredicate, + BooleanOperation, + booleanOperationTranslationMap, EntityKeyValueType, + FilterPredicateType +} from '@shared/models/query/query.models'; + +@Component({ + selector: 'tb-boolean-filter-predicate', + templateUrl: './boolean-filter-predicate.component.html', + styleUrls: ['./filter-predicate.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => BooleanFilterPredicateComponent), + multi: true + } + ] +}) +export class BooleanFilterPredicateComponent implements ControlValueAccessor, OnInit { + + @Input() disabled: boolean; + + @Input() allowUserDynamicSource = true; + + valueTypeEnum = EntityKeyValueType; + + booleanFilterPredicateFormGroup: FormGroup; + + booleanOperations = Object.keys(BooleanOperation); + booleanOperationEnum = BooleanOperation; + booleanOperationTranslations = booleanOperationTranslationMap; + + private propagateChange = null; + + constructor(private fb: FormBuilder) { + } + + ngOnInit(): void { + this.booleanFilterPredicateFormGroup = this.fb.group({ + operation: [BooleanOperation.EQUAL, [Validators.required]], + value: [null, [Validators.required]] + }); + this.booleanFilterPredicateFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState?(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.booleanFilterPredicateFormGroup.disable({emitEvent: false}); + } else { + this.booleanFilterPredicateFormGroup.enable({emitEvent: false}); + } + } + + writeValue(predicate: BooleanFilterPredicate): void { + this.booleanFilterPredicateFormGroup.get('operation').patchValue(predicate.operation, {emitEvent: false}); + this.booleanFilterPredicateFormGroup.get('value').patchValue(predicate.value, {emitEvent: false}); + } + + private updateModel() { + let predicate: BooleanFilterPredicate = null; + if (this.booleanFilterPredicateFormGroup.valid) { + predicate = this.booleanFilterPredicateFormGroup.getRawValue(); + predicate.type = FilterPredicateType.BOOLEAN; + } + this.propagateChange(predicate); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html new file mode 100644 index 0000000000..97fe0d4988 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html @@ -0,0 +1,63 @@ + +
+ +

filter.complex-filter

+ + +
+
+
+ + filter.operation.operation + + + {{complexOperationTranslations.get(complexOperationEnum[operation]) | translate}} + + + + + +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts new file mode 100644 index 0000000000..8fe9e5a1b6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts @@ -0,0 +1,104 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { + BooleanOperation, booleanOperationTranslationMap, + ComplexFilterPredicate, ComplexFilterPredicateInfo, ComplexOperation, complexOperationTranslationMap, + EntityKeyValueType, + FilterPredicateType, KeyFilterPredicateInfo +} from '@shared/models/query/query.models'; + +export interface ComplexFilterPredicateDialogData { + complexPredicate: ComplexFilterPredicateInfo; + key: string; + readonly: boolean; + isAdd: boolean; + valueType: EntityKeyValueType; + displayUserParameters: boolean; + allowUserDynamicSource: boolean; +} + +@Component({ + selector: 'tb-complex-filter-predicate-dialog', + templateUrl: './complex-filter-predicate-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: ComplexFilterPredicateDialogComponent}], + styleUrls: [] +}) +export class ComplexFilterPredicateDialogComponent extends + DialogComponent + implements OnInit, ErrorStateMatcher { + + complexFilterFormGroup: FormGroup; + + complexOperations = Object.keys(ComplexOperation); + complexOperationEnum = ComplexOperation; + complexOperationTranslations = complexOperationTranslationMap; + + isAdd: boolean; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: ComplexFilterPredicateDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private fb: FormBuilder) { + super(store, router, dialogRef); + + this.isAdd = this.data.isAdd; + + this.complexFilterFormGroup = this.fb.group( + { + operation: [this.data.complexPredicate.operation, [Validators.required]], + predicates: [this.data.complexPredicate.predicates, [Validators.required]] + } + ); + if (this.data.readonly) { + this.complexFilterFormGroup.disable({emitEvent: false}); + } + } + + ngOnInit(): void { + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + if (this.complexFilterFormGroup.valid) { + const predicate: ComplexFilterPredicateInfo = this.complexFilterFormGroup.getRawValue(); + predicate.type = FilterPredicateType.COMPLEX; + this.dialogRef.close(predicate); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.html new file mode 100644 index 0000000000..1a6351be10 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.html @@ -0,0 +1,28 @@ + +
+ filter.complex-filter + +
diff --git a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts new file mode 100644 index 0000000000..87bad30d5a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts @@ -0,0 +1,108 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { + ComplexFilterPredicate, + ComplexFilterPredicateInfo, + EntityKeyValueType +} from '@shared/models/query/query.models'; +import { MatDialog } from '@angular/material/dialog'; +import { + ComplexFilterPredicateDialogComponent, + ComplexFilterPredicateDialogData +} from '@home/components/filter/complex-filter-predicate-dialog.component'; +import { deepClone } from '@core/utils'; + +@Component({ + selector: 'tb-complex-filter-predicate', + templateUrl: './complex-filter-predicate.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ComplexFilterPredicateComponent), + multi: true + } + ] +}) +export class ComplexFilterPredicateComponent implements ControlValueAccessor, OnInit { + + @Input() disabled: boolean; + + @Input() valueType: EntityKeyValueType; + + @Input() key: string; + + @Input() displayUserParameters = true; + + @Input() allowUserDynamicSource = true; + + private propagateChange = null; + + private complexFilterPredicate: ComplexFilterPredicateInfo; + + constructor(private dialog: MatDialog) { + } + + ngOnInit(): void { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState?(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(predicate: ComplexFilterPredicateInfo): void { + this.complexFilterPredicate = predicate; + } + + private openComplexFilterDialog() { + this.dialog.open(ComplexFilterPredicateDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + complexPredicate: this.disabled ? this.complexFilterPredicate : deepClone(this.complexFilterPredicate), + readonly: this.disabled, + valueType: this.valueType, + isAdd: false, + key: this.key, + displayUserParameters: this.displayUserParameters, + allowUserDynamicSource: this.allowUserDynamicSource + } + }).afterClosed().subscribe( + (result) => { + if (result) { + this.complexFilterPredicate = result; + this.updateModel(); + } + } + ); + } + + private updateModel() { + this.propagateChange(this.complexFilterPredicate); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html new file mode 100644 index 0000000000..717e8765de --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html @@ -0,0 +1,70 @@ + +
+ +

{{ (isAdd ? 'filter.add' : 'filter.edit') | translate }}

+ + +
+ + +
+
+
+
+ + filter.name + + + {{ 'filter.name-required' | translate }} + + + {{ 'filter.duplicate-filter' | translate }} + + +
+ + + +
+
+ + +
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.scss b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.scss new file mode 100644 index 0000000000..30d99016f0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.scss @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .tb-editable-switch { + padding-left: 10px; + + .editable-switch { + margin: 0; + } + + .editable-label { + margin: 5px 0; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts new file mode 100644 index 0000000000..d4d1c8a2a5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts @@ -0,0 +1,137 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + FormBuilder, + FormControl, + FormGroup, + FormGroupDirective, + NgForm, + ValidatorFn, + Validators +} from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { Filter, Filters } from '@shared/models/query/query.models'; + +export interface FilterDialogData { + isAdd: boolean; + filters: Filters | Array; + filter?: Filter; +} + +@Component({ + selector: 'tb-filter-dialog', + templateUrl: './filter-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: FilterDialogComponent}], + styleUrls: ['./filter-dialog.component.scss'] +}) +export class FilterDialogComponent extends DialogComponent + implements OnInit, ErrorStateMatcher { + + isAdd: boolean; + filters: Array; + + filter: Filter; + + filterFormGroup: FormGroup; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: FilterDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + private utils: UtilsService, + public translate: TranslateService) { + super(store, router, dialogRef); + this.isAdd = data.isAdd; + if (Array.isArray(data.filters)) { + this.filters = data.filters; + } else { + this.filters = []; + for (const filterId of Object.keys(data.filters)) { + this.filters.push(data.filters[filterId]); + } + } + if (this.isAdd && !this.data.filter) { + this.filter = { + id: null, + filter: '', + keyFilters: [], + editable: true + }; + } else { + this.filter = data.filter; + } + + this.filterFormGroup = this.fb.group({ + filter: [this.filter.filter, [this.validateDuplicateFilterName(), Validators.required]], + editable: [this.filter.editable], + keyFilters: [this.filter.keyFilters, Validators.required] + }); + } + + validateDuplicateFilterName(): ValidatorFn { + return (c: FormControl) => { + const newFilter = c.value.trim(); + const found = this.filters.find((filter) => filter.filter === newFilter); + if (found) { + if (this.isAdd || this.filter.id !== found.id) { + return { + duplicateFilterName: { + valid: false + } + }; + } + } + return null; + }; + } + + ngOnInit(): void { + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + this.filter.filter = this.filterFormGroup.get('filter').value.trim(); + this.filter.editable = this.filterFormGroup.get('editable').value; + this.filter.keyFilters = this.filterFormGroup.get('keyFilters').value; + if (this.isAdd) { + this.filter.id = this.utils.guid(); + } + this.dialogRef.close(this.filter); + } +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html new file mode 100644 index 0000000000..ffef5e2349 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html @@ -0,0 +1,93 @@ + +
+ + + +
filter.filters
+
+
+
+ +
+
+
+ + +
+ +
+ +   +
+
+ +
+
+
+ {{ complexOperationTranslations.get(operation) | translate }} +
+
+
+ + + +
+
+
+ filter.no-filters +
+
+ + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.scss b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.scss new file mode 100644 index 0000000000..335e755433 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.scss @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .predicate-list { + overflow: auto; + max-height: 350px; + .no-data-found { + height: 50px; + } + } + .filters-operation { + margin-top: -40px; + color: #666; + font-weight: 500; + } +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts new file mode 100644 index 0000000000..2e59e85123 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts @@ -0,0 +1,184 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + FormGroup, + NG_VALUE_ACCESSOR, + Validators +} from '@angular/forms'; +import { Observable, of, Subscription } from 'rxjs'; +import { + ComplexFilterPredicateInfo, + ComplexOperation, + complexOperationTranslationMap, + createDefaultFilterPredicateInfo, + EntityKeyValueType, + KeyFilterPredicateInfo +} from '@shared/models/query/query.models'; +import { + ComplexFilterPredicateDialogComponent, + ComplexFilterPredicateDialogData +} from '@home/components/filter/complex-filter-predicate-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; +import { map } from 'rxjs/operators'; + +@Component({ + selector: 'tb-filter-predicate-list', + templateUrl: './filter-predicate-list.component.html', + styleUrls: ['./filter-predicate-list.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FilterPredicateListComponent), + multi: true + } + ] +}) +export class FilterPredicateListComponent implements ControlValueAccessor, OnInit { + + @Input() disabled: boolean; + + @Input() valueType: EntityKeyValueType; + + @Input() key: string; + + @Input() operation: ComplexOperation = ComplexOperation.AND; + + @Input() displayUserParameters = true; + + @Input() allowUserDynamicSource = true; + + filterListFormGroup: FormGroup; + + valueTypeEnum = EntityKeyValueType; + + complexOperationTranslations = complexOperationTranslationMap; + + private propagateChange = null; + + private valueChangeSubscription: Subscription = null; + + constructor(private fb: FormBuilder, + private dialog: MatDialog) { + } + + ngOnInit(): void { + this.filterListFormGroup = this.fb.group({}); + this.filterListFormGroup.addControl('predicates', + this.fb.array([])); + } + + predicatesFormArray(): FormArray { + return this.filterListFormGroup.get('predicates') as FormArray; + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState?(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.filterListFormGroup.disable({emitEvent: false}); + } else { + this.filterListFormGroup.enable({emitEvent: false}); + } + } + + writeValue(predicates: Array): void { + if (this.valueChangeSubscription) { + this.valueChangeSubscription.unsubscribe(); + } + const predicateControls: Array = []; + if (predicates) { + for (const predicate of predicates) { + predicateControls.push(this.fb.control(predicate, [Validators.required])); + } + } + this.filterListFormGroup.setControl('predicates', this.fb.array(predicateControls)); + this.valueChangeSubscription = this.filterListFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + if (this.disabled) { + this.filterListFormGroup.disable({emitEvent: false}); + } else { + this.filterListFormGroup.enable({emitEvent: false}); + } + } + + public removePredicate(index: number) { + (this.filterListFormGroup.get('predicates') as FormArray).removeAt(index); + } + + public addPredicate(complex: boolean) { + const predicatesFormArray = this.filterListFormGroup.get('predicates') as FormArray; + const predicate = createDefaultFilterPredicateInfo(this.valueType, complex); + let observable: Observable; + if (complex) { + observable = this.openComplexFilterDialog(predicate); + } else { + observable = of(predicate); + } + observable.subscribe((result) => { + if (result) { + predicatesFormArray.push(this.fb.control(result, [Validators.required])); + } + }); + } + + private openComplexFilterDialog(predicate: KeyFilterPredicateInfo): Observable { + return this.dialog.open(ComplexFilterPredicateDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + complexPredicate: predicate.keyFilterPredicate as ComplexFilterPredicateInfo, + readonly: this.disabled, + valueType: this.valueType, + key: this.key, + isAdd: true, + displayUserParameters: this.displayUserParameters, + allowUserDynamicSource: this.allowUserDynamicSource + } + }).afterClosed().pipe( + map((result) => { + if (result) { + predicate.keyFilterPredicate = result; + return predicate; + } else { + return null; + } + }) + ); + } + + private updateModel() { + const predicates: Array = this.filterListFormGroup.getRawValue().predicates; + if (this.filterListFormGroup.valid && predicates.length) { + this.propagateChange(predicates); + } else { + this.propagateChange(null); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html new file mode 100644 index 0000000000..524285ea3f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html @@ -0,0 +1,82 @@ + +
+
+
+ + + + + + + + + + + + + + + + + + {{ (filterPredicateValueFormGroup.get('defaultValue').value ? 'value.true' : 'value.false') | translate }} + + +
+
filter.default-value
+
+
+
+
+ + + + + {{'filter.no-dynamic-value' | translate}} + + + {{dynamicValueSourceTypeTranslations.get(sourceType) | translate}} + + + +
filter.dynamic-source-type
+
+
+ + + + +
filter.source-attribute
+
+
+
+ +
diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts new file mode 100644 index 0000000000..9c55647c51 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts @@ -0,0 +1,161 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormGroup, + NG_VALUE_ACCESSOR, + ValidatorFn, + Validators +} from '@angular/forms'; +import { + DynamicValueSourceType, + dynamicValueSourceTypeTranslationMap, + EntityKeyValueType, + FilterPredicateValue +} from '@shared/models/query/query.models'; + +@Component({ + selector: 'tb-filter-predicate-value', + templateUrl: './filter-predicate-value.component.html', + styleUrls: ['./filter-predicate.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FilterPredicateValueComponent), + multi: true + } + ] +}) +export class FilterPredicateValueComponent implements ControlValueAccessor, OnInit { + + @Input() disabled: boolean; + + @Input() + set allowUserDynamicSource(allow: boolean) { + this.dynamicValueSourceTypes = [DynamicValueSourceType.CURRENT_TENANT, + DynamicValueSourceType.CURRENT_CUSTOMER]; + this.allow = allow; + if (allow) { + this.dynamicValueSourceTypes.push(DynamicValueSourceType.CURRENT_USER); + } else { + this.dynamicValueSourceTypes = [DynamicValueSourceType.CURRENT_DEVICE]; + } + } + + @Input() + valueType: EntityKeyValueType; + + valueTypeEnum = EntityKeyValueType; + + dynamicValueSourceTypes: DynamicValueSourceType[] = [DynamicValueSourceType.CURRENT_TENANT, + DynamicValueSourceType.CURRENT_CUSTOMER, DynamicValueSourceType.CURRENT_USER]; + + dynamicValueSourceTypeTranslations = dynamicValueSourceTypeTranslationMap; + + filterPredicateValueFormGroup: FormGroup; + + dynamicMode = false; + + allow = true; + + private propagateChange = null; + + constructor(private fb: FormBuilder) { + } + + ngOnInit(): void { + let defaultValue: string | number | boolean; + let defaultValueValidators: ValidatorFn[]; + switch (this.valueType) { + case EntityKeyValueType.STRING: + defaultValue = ''; + defaultValueValidators = []; + break; + case EntityKeyValueType.NUMERIC: + defaultValue = 0; + defaultValueValidators = [Validators.required]; + break; + case EntityKeyValueType.BOOLEAN: + defaultValue = false; + defaultValueValidators = []; + break; + case EntityKeyValueType.DATE_TIME: + defaultValue = Date.now(); + defaultValueValidators = [Validators.required]; + break; + } + this.filterPredicateValueFormGroup = this.fb.group({ + defaultValue: [defaultValue, defaultValueValidators], + dynamicValue: this.fb.group( + { + sourceType: [null], + sourceAttribute: [null] + } + ) + }); + this.filterPredicateValueFormGroup.get('dynamicValue').get('sourceType').valueChanges.subscribe( + (sourceType) => { + if (!sourceType) { + this.filterPredicateValueFormGroup.get('dynamicValue').get('sourceAttribute').patchValue(null, {emitEvent: false}); + } + } + ); + this.filterPredicateValueFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState?(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.filterPredicateValueFormGroup.disable({emitEvent: false}); + } else { + this.filterPredicateValueFormGroup.enable({emitEvent: false}); + } + } + + writeValue(predicateValue: FilterPredicateValue): void { + this.filterPredicateValueFormGroup.get('defaultValue').patchValue(predicateValue.defaultValue, {emitEvent: false}); + this.filterPredicateValueFormGroup.get('dynamicValue').get('sourceType').patchValue(predicateValue.dynamicValue ? + predicateValue.dynamicValue.sourceType : null, {emitEvent: false}); + this.filterPredicateValueFormGroup.get('dynamicValue').get('sourceAttribute').patchValue(predicateValue.dynamicValue ? + predicateValue.dynamicValue.sourceAttribute : null, {emitEvent: false}); + } + + private updateModel() { + let predicateValue: FilterPredicateValue = null; + if (this.filterPredicateValueFormGroup.valid) { + predicateValue = this.filterPredicateValueFormGroup.getRawValue(); + if (predicateValue.dynamicValue) { + if (!predicateValue.dynamicValue.sourceType || !predicateValue.dynamicValue.sourceAttribute) { + predicateValue.dynamicValue = null; + } + } + } + this.propagateChange(predicateValue); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.html new file mode 100644 index 0000000000..3b9d89008b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.html @@ -0,0 +1,58 @@ + + +
+
+ + + + + + + + + + + + + + + + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts new file mode 100644 index 0000000000..43933270c2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts @@ -0,0 +1,102 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { + EntityKeyValueType, + FilterPredicateType, KeyFilterPredicate, KeyFilterPredicateInfo +} from '@shared/models/query/query.models'; + +@Component({ + selector: 'tb-filter-predicate', + templateUrl: './filter-predicate.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FilterPredicateComponent), + multi: true + } + ] +}) +export class FilterPredicateComponent implements ControlValueAccessor, OnInit { + + @Input() disabled: boolean; + + @Input() valueType: EntityKeyValueType; + + @Input() key: string; + + @Input() displayUserParameters = true; + + @Input() allowUserDynamicSource = true; + + filterPredicateFormGroup: FormGroup; + + type: FilterPredicateType; + + filterPredicateType = FilterPredicateType; + + private propagateChange = null; + + constructor(private fb: FormBuilder) { + } + + ngOnInit(): void { + this.filterPredicateFormGroup = this.fb.group({ + predicate: [null, [Validators.required]], + userInfo: [null, []] + }); + this.filterPredicateFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState?(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.filterPredicateFormGroup.disable({emitEvent: false}); + } else { + this.filterPredicateFormGroup.enable({emitEvent: false}); + } + } + + writeValue(predicate: KeyFilterPredicateInfo): void { + this.type = predicate.keyFilterPredicate.type; + this.filterPredicateFormGroup.get('predicate').patchValue(predicate.keyFilterPredicate, {emitEvent: false}); + this.filterPredicateFormGroup.get('userInfo').patchValue(predicate.userInfo, {emitEvent: false}); + } + + private updateModel() { + let predicate: KeyFilterPredicateInfo = null; + if (this.filterPredicateFormGroup.valid) { + predicate = { + keyFilterPredicate: this.filterPredicateFormGroup.getRawValue().predicate, + userInfo: this.filterPredicateFormGroup.getRawValue().userInfo + }; + } + this.propagateChange(predicate); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate.scss b/ui-ngx/src/app/modules/home/components/filter/filter-predicate.scss new file mode 100644 index 0000000000..4b1b99f933 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate.scss @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:host { + .tb-hint { + padding-bottom: 0; + } +} + +:host ::ng-deep { + mat-form-field { + .mat-form-field-wrapper { + padding-bottom: 0; + .mat-form-field-infix { + border-top-width: 0.2em; + width: auto; + min-width: auto; + } + .mat-form-field-underline { + bottom: 0; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-select.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-select.component.html new file mode 100644 index 0000000000..65249441ed --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filter-select.component.html @@ -0,0 +1,61 @@ + + + + + + + + + + +
+
+ filter.no-filters-found +
+ + + {{ translate.get('filter.no-filter-matching', + {filter: truncate.transform(searchText, true, 6, '...')}) | async }} + + + + filter.create-new-filter + +
+
+
+ + {{ 'filter.filter-required' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-select.component.models.ts b/ui-ngx/src/app/modules/home/components/filter/filter-select.component.models.ts new file mode 100644 index 0000000000..db443aede9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filter-select.component.models.ts @@ -0,0 +1,22 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Observable } from 'rxjs'; +import { Filter } from '@shared/models/query/query.models'; + +export interface FilterSelectCallbacks { + createFilter: (filter: string) => Observable; +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-select.component.scss b/ui-ngx/src/app/modules/home/components/filter/filter-select.component.scss new file mode 100644 index 0000000000..c14adfc443 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filter-select.component.scss @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + +} + +:host ::ng-deep { + .mat-form-field-infix { + border-top: none; + } +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-select.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-select.component.ts new file mode 100644 index 0000000000..1759c02291 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filter-select.component.ts @@ -0,0 +1,249 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, SkipSelf, ViewChild } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + FormGroupDirective, + NG_VALUE_ACCESSOR, + NgForm +} from '@angular/forms'; +import { Observable, of } from 'rxjs'; +import { map, mergeMap, share, tap } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { IAliasController } from '@core/api/widget-api.models'; +import { TruncatePipe } from '@shared/pipe/truncate.pipe'; +import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete'; +import { ENTER } from '@angular/cdk/keycodes'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { FilterSelectCallbacks } from '@home/components/filter/filter-select.component.models'; +import { Filter } from '@shared/models/query/query.models'; + +@Component({ + selector: 'tb-filter-select', + templateUrl: './filter-select.component.html', + styleUrls: ['./filter-select.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FilterSelectComponent), + multi: true + }, + { + provide: ErrorStateMatcher, + useExisting: FilterSelectComponent + }] +}) +export class FilterSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, ErrorStateMatcher { + + selectFilterFormGroup: FormGroup; + + modelValue: string | null; + + @Input() + aliasController: IAliasController; + + @Input() + callbacks: FilterSelectCallbacks; + + @Input() + showLabel: boolean; + + @ViewChild('filterAutocomplete') filterAutocomplete: MatAutocomplete; + @ViewChild('autocomplete', { read: MatAutocompleteTrigger }) autoCompleteTrigger: MatAutocompleteTrigger; + + + private requiredValue: boolean; + get tbRequired(): boolean { + return this.requiredValue; + } + @Input() + set tbRequired(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @ViewChild('filterInput', {static: true}) filterInput: ElementRef; + + filterList: Array = []; + + filteredFilters: Observable>; + + searchText = ''; + + private dirty = false; + + private creatingFilter = false; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public translate: TranslateService, + public truncate: TruncatePipe, + private fb: FormBuilder) { + this.selectFilterFormGroup = this.fb.group({ + filter: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + const filters = this.aliasController.getFilters(); + for (const filterId of Object.keys(filters)) { + this.filterList.push(filters[filterId]); + } + + this.filteredFilters = this.selectFilterFormGroup.get('filter').valueChanges + .pipe( + tap(value => { + let modelValue; + if (typeof value === 'string' || !value) { + modelValue = null; + } else { + modelValue = value; + } + this.updateView(modelValue); + if (value === null) { + this.clear(); + } + }), + map(value => value ? (typeof value === 'string' ? value : value.filter) : ''), + mergeMap(name => this.fetchFilters(name) ), + share() + ); + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = this.tbRequired && !this.modelValue; + return originalErrorState || customErrorState; + } + + ngAfterViewInit(): void {} + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.selectFilterFormGroup.disable({emitEvent: false}); + } else { + this.selectFilterFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: string | null): void { + this.searchText = ''; + let filter = null; + if (value != null) { + const filters = this.aliasController.getFilters(); + if (filters[value]) { + filter = filters[value]; + } + } + if (filter != null) { + this.modelValue = filter.id; + this.selectFilterFormGroup.get('filter').patchValue(filter, {emitEvent: false}); + } else { + this.modelValue = null; + this.selectFilterFormGroup.get('filter').patchValue('', {emitEvent: false}); + } + this.dirty = true; + } + + onFocus() { + if (this.dirty) { + this.selectFilterFormGroup.get('filter').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + updateView(value: Filter | null) { + const filterId = value ? value.id : null; + if (this.modelValue !== filterId) { + this.modelValue = filterId; + this.propagateChange(this.modelValue); + } + } + + displayFilterFn(filter?: Filter): string | undefined { + return filter ? filter.filter : undefined; + } + + fetchFilters(searchText?: string): Observable> { + this.searchText = searchText; + let result = this.filterList; + if (searchText && searchText.length) { + result = this.filterList.filter((filter) => filter.filter.toLowerCase().includes(searchText.toLowerCase())); + } + return of(result); + } + + clear(value: string = '') { + this.filterInput.nativeElement.value = value; + this.selectFilterFormGroup.get('filter').patchValue(value, {emitEvent: true}); + setTimeout(() => { + this.filterInput.nativeElement.blur(); + this.filterInput.nativeElement.focus(); + }, 0); + } + + textIsNotEmpty(text: string): boolean { + return (text && text != null && text.length > 0) ? true : false; + } + + filterEnter($event: KeyboardEvent) { + if ($event.keyCode === ENTER) { + $event.preventDefault(); + if (!this.modelValue) { + this.createFilter($event, this.searchText); + } + } + } + + createFilter($event: Event, filter: string) { + $event.preventDefault(); + this.creatingFilter = true; + if (this.callbacks && this.callbacks.createFilter) { + this.callbacks.createFilter(filter).subscribe((newFilter) => { + if (!newFilter) { + setTimeout(() => { + this.filterInput.nativeElement.blur(); + this.filterInput.nativeElement.focus(); + }, 0); + } else { + this.filterList.push(newFilter); + this.modelValue = newFilter.id; + this.selectFilterFormGroup.get('filter').patchValue(newFilter, {emitEvent: true}); + this.propagateChange(this.modelValue); + } + } + ); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-text.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-text.component.html new file mode 100644 index 0000000000..b8d7103a3b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filter-text.component.html @@ -0,0 +1,19 @@ + +
diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-text.component.scss b/ui-ngx/src/app/modules/home/components/filter/filter-text.component.scss new file mode 100644 index 0000000000..aaa95f0f3f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filter-text.component.scss @@ -0,0 +1,83 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .tb-filter-text { + overflow-y: auto; + &.disabled { + opacity: 0.7; + } + &.required { + color: #f44336; + padding: 0 4px; + } + &.nowrap { + white-space: nowrap; + overflow: hidden; + } + } +} + +:host ::ng-deep { + .tb-filter-text { + line-height: 1.8em; + span { + display: inline-block; + vertical-align: middle; + line-height: 1.4em; + } + .tb-filter-predicate { + padding-right: 4px; + padding-left: 4px; + } + .tb-filter-entity-key, .tb-filter-value, .tb-filter-dynamic-source { + font-weight: bold; + border: 1px groove rgba(0, 0, 0, .25); + border-radius: 4px; + padding-left: 4px; + padding-right: 4px; + } + .tb-filter-entity-key, .tb-filter-value { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 150px; + } + .tb-filter-dynamic-source { + } + .tb-filter-entity-key { + color: #305680; + } + .tb-filter-value { + color: #ff5722; + } + .tb-filter-simple-operation { + font-size: 0.9em; + } + .tb-filter-complex-operation { + font-weight: bold; + } + .tb-filter-dynamic-value { + .tb-filter-dynamic-source, .tb-filter-value { + color: #0c959c; + } + } + .tb-filter-bracket { + .tb-left-bracket, .tb-right-bracket { + font-size: 1.2em; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-text.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-text.component.ts new file mode 100644 index 0000000000..5499cdc765 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filter-text.component.ts @@ -0,0 +1,104 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { KeyFilter, keyFiltersToText } from '@shared/models/query/query.models'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; + +@Component({ + selector: 'tb-filter-text', + templateUrl: './filter-text.component.html', + styleUrls: ['./filter-text.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FilterTextComponent), + multi: true + } + ] +}) +export class FilterTextComponent implements ControlValueAccessor, OnInit { + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @Input() + noFilterText = this.translate.instant('filter.no-filter-text'); + + @Input() + addFilterPrompt = this.translate.instant('filter.add-filter-prompt'); + + @Input() + nowrap = false; + + requiredClass = false; + + private filterText: string; + + private propagateChange = (v: any) => { }; + + constructor(private dialog: MatDialog, + private fb: FormBuilder, + private translate: TranslateService, + private datePipe: DatePipe) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: Array): void { + this.updateFilterText(value); + } + + private updateFilterText(value: Array) { + this.requiredClass = false; + if (value && value.length) { + this.filterText = keyFiltersToText(this.translate, this.datePipe, value); + } else { + if (this.required && !this.disabled) { + this.filterText = this.addFilterPrompt; + this.requiredClass = true; + } else { + this.filterText = this.noFilterText; + } + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.html new file mode 100644 index 0000000000..587504da45 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.html @@ -0,0 +1,63 @@ + +
+ +

{{(data.readonly ? 'filter.filter-user-params' : 'filter.edit-filter-user-params') | translate}}

+ + +
+
+
+ + {{ 'filter.editable' | translate }} + +
+ + filter.display-label + + + + {{ 'filter.autogenerated-label' | translate }} + +
+ + filter.order-priority + + +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.ts new file mode 100644 index 0000000000..58e644f725 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.ts @@ -0,0 +1,115 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { + BooleanOperation, createDefaultFilterPredicateUserInfo, + EntityKeyValueType, generateUserFilterValueLabel, + KeyFilterPredicateUserInfo, NumericOperation, + StringOperation +} from '@shared/models/query/query.models'; +import { TranslateService } from '@ngx-translate/core'; + +export interface FilterUserInfoDialogData { + key: string; + valueType: EntityKeyValueType; + operation: StringOperation | BooleanOperation | NumericOperation; + keyFilterPredicateUserInfo: KeyFilterPredicateUserInfo; + readonly: boolean; +} + +@Component({ + selector: 'tb-filter-user-info-dialog', + templateUrl: './filter-user-info-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: FilterUserInfoDialogComponent}], + styleUrls: [] +}) +export class FilterUserInfoDialogComponent extends + DialogComponent + implements OnInit, ErrorStateMatcher { + + filterUserInfoFormGroup: FormGroup; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: FilterUserInfoDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + private translate: TranslateService) { + super(store, router, dialogRef); + + const userInfo: KeyFilterPredicateUserInfo = this.data.keyFilterPredicateUserInfo || createDefaultFilterPredicateUserInfo(); + + this.filterUserInfoFormGroup = this.fb.group( + { + editable: [userInfo.editable], + label: [userInfo.label], + autogeneratedLabel: [userInfo.autogeneratedLabel], + order: [userInfo.order] + } + ); + this.onAutogeneratedLabelChange(); + if (!this.data.readonly) { + this.filterUserInfoFormGroup.get('autogeneratedLabel').valueChanges.subscribe(() => { + this.onAutogeneratedLabelChange(); + }); + } else { + this.filterUserInfoFormGroup.disable({emitEvent: false}); + } + } + + private onAutogeneratedLabelChange() { + const autogeneratedLabel: boolean = this.filterUserInfoFormGroup.get('autogeneratedLabel').value; + if (autogeneratedLabel) { + const generatedLabel = generateUserFilterValueLabel(this.data.key, this.data.valueType, this.data.operation, this.translate); + this.filterUserInfoFormGroup.get('label').patchValue(generatedLabel, {emitEvent: false}); + this.filterUserInfoFormGroup.get('label').disable({emitEvent: false}); + } else { + this.filterUserInfoFormGroup.get('label').enable({emitEvent: false}); + } + } + + ngOnInit(): void { + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + if (this.filterUserInfoFormGroup.valid) { + const keyFilterPredicateUserInfo: KeyFilterPredicateUserInfo = this.filterUserInfoFormGroup.getRawValue(); + this.dialogRef.close(keyFilterPredicateUserInfo); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-user-info.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-user-info.component.html new file mode 100644 index 0000000000..40cc46be35 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filter-user-info.component.html @@ -0,0 +1,25 @@ + + diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-user-info.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-user-info.component.ts new file mode 100644 index 0000000000..5bf5c2a82b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filter-user-info.component.ts @@ -0,0 +1,105 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { + BooleanOperation, + EntityKeyValueType, + KeyFilterPredicateUserInfo, NumericOperation, + StringOperation +} from '@shared/models/query/query.models'; +import { MatDialog } from '@angular/material/dialog'; +import { + FilterUserInfoDialogComponent, + FilterUserInfoDialogData +} from '@home/components/filter/filter-user-info-dialog.component'; +import { deepClone } from '@core/utils'; + +@Component({ + selector: 'tb-filter-user-info', + templateUrl: './filter-user-info.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FilterUserInfoComponent), + multi: true + } + ] +}) +export class FilterUserInfoComponent implements ControlValueAccessor, OnInit { + + @Input() disabled: boolean; + + @Input() key: string; + + @Input() operation: StringOperation | BooleanOperation | NumericOperation; + + @Input() valueType: EntityKeyValueType; + + private propagateChange = null; + + private keyFilterPredicateUserInfo: KeyFilterPredicateUserInfo; + + constructor(private dialog: MatDialog) { + } + + ngOnInit(): void { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState?(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(keyFilterPredicateUserInfo: KeyFilterPredicateUserInfo): void { + this.keyFilterPredicateUserInfo = keyFilterPredicateUserInfo; + } + + public openFilterUserInfoDialog() { + this.dialog.open(FilterUserInfoDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + keyFilterPredicateUserInfo: deepClone(this.keyFilterPredicateUserInfo), + valueType: this.valueType, + key: this.key, + operation: this.operation, + readonly: this.disabled + } + }).afterClosed().subscribe( + (result) => { + if (result) { + this.keyFilterPredicateUserInfo = result; + this.updateModel(); + } + } + ); + } + + private updateModel() { + this.propagateChange(this.keyFilterPredicateUserInfo); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html new file mode 100644 index 0000000000..c573267d2a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html @@ -0,0 +1,104 @@ + +
+ +

{{ title | translate }}

+ + +
+ + +
+
+
+ +
+
filter.filter
+
filter.editable
+
+
+
+
+ +
+ {{$index + 1}}. +
+ {{filterControl.get('filter').value}} +
+ + +
+ + +
+
+
+
+
+ + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.scss b/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.scss new file mode 100644 index 0000000000..dbacf5c09e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.scss @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + + .tb-filters-header { + min-height: 40px; + padding: 0 11px; + margin: 5px; + + .tb-header-label { + font-size: 14px; + color: rgba(0, 0, 0, .570588); + } + } + + mat-divider{ + margin: -1px -24px; + } + + .tb-filter { + padding: 0 0 0 10px; + margin: 5px; + + .tb-editable-switch { + padding-left: 10px; + + .editable-switch { + margin: 0; + } + } + } +} + +:host ::ng-deep { + .mat-dialog-content { + padding-top: 0 !important; + padding-bottom: 0 !important; + } +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.ts new file mode 100644 index 0000000000..cf48098a3f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.ts @@ -0,0 +1,246 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + AbstractControl, + FormArray, + FormBuilder, + FormControl, + FormGroup, + FormGroupDirective, + NgForm, + Validators +} from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { DatasourceType, Widget } from '@shared/models/widget.models'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { DialogService } from '@core/services/dialog.service'; +import { deepClone, isUndefined } from '@core/utils'; +import { Filter, Filters, KeyFilterInfo } from '@shared/models/query/query.models'; +import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component'; + +export interface FiltersDialogData { + filters: Filters; + widgets: Array; + isSingleFilter?: boolean; + isSingleWidget?: boolean; + disableAdd?: boolean; + singleFilter?: Filter; + customTitle?: string; +} + +@Component({ + selector: 'tb-filters-dialog', + templateUrl: './filters-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: FiltersDialogComponent}], + styleUrls: ['./filters-dialog.component.scss'] +}) +export class FiltersDialogComponent extends DialogComponent + implements OnInit, ErrorStateMatcher { + + title: string; + disableAdd: boolean; + + filterToWidgetsMap: {[filterId: string]: Array} = {}; + + filtersFormGroup: FormGroup; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: FiltersDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + private utils: UtilsService, + private translate: TranslateService, + private dialogs: DialogService, + private dialog: MatDialog) { + super(store, router, dialogRef); + this.title = data.customTitle ? data.customTitle : 'filter.filters'; + this.disableAdd = this.data.disableAdd; + + if (data.widgets) { + let widgetsTitleList: Array; + if (this.data.isSingleWidget && this.data.widgets.length === 1) { + const widget = this.data.widgets[0]; + widgetsTitleList = [widget.config.title]; + for (const filterId of Object.keys(this.data.filters)) { + this.filterToWidgetsMap[filterId] = widgetsTitleList; + } + } else { + this.data.widgets.forEach((widget) => { + const datasources = this.utils.validateDatasources(widget.config.datasources); + datasources.forEach((datasource) => { + if (datasource.type === DatasourceType.entity && datasource.filterId) { + widgetsTitleList = this.filterToWidgetsMap[datasource.filterId]; + if (!widgetsTitleList) { + widgetsTitleList = []; + this.filterToWidgetsMap[datasource.filterId] = widgetsTitleList; + } + widgetsTitleList.push(widget.config.title); + } + }); + }); + } + } + const filterControls: Array = []; + for (const filterId of Object.keys(this.data.filters)) { + const filter = this.data.filters[filterId]; + if (isUndefined(filter.editable)) { + filter.editable = true; + } + filterControls.push(this.createFilterFormControl(filterId, filter)); + } + + this.filtersFormGroup = this.fb.group({ + filters: this.fb.array(filterControls) + }); + } + + private createFilterFormControl(filterId: string, filter: Filter): AbstractControl { + const filterFormControl = this.fb.group({ + id: [filterId], + filter: [filter ? filter.filter : null, [Validators.required]], + keyFilters: [filter ? filter.keyFilters : [], [Validators.required]], + editable: [filter ? filter.editable : true] + }); + return filterFormControl; + } + + + filtersFormArray(): FormArray { + return this.filtersFormGroup.get('filters') as FormArray; + } + + ngOnInit(): void { + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + removeFilter(index: number) { + const filter = (this.filtersFormGroup.get('filters').value as any[])[index]; + const widgetsTitleList = this.filterToWidgetsMap[filter.id]; + if (widgetsTitleList) { + let widgetsListHtml = ''; + for (const widgetTitle of widgetsTitleList) { + widgetsListHtml += '
\'' + widgetTitle + '\''; + } + const message = this.translate.instant('filter.unable-delete-filter-text', + {filter: filter.filter, widgetsList: widgetsListHtml}); + this.dialogs.alert(this.translate.instant('filter.unable-delete-filter-title'), + message, this.translate.instant('action.close'), true); + } else { + (this.filtersFormGroup.get('filters') as FormArray).removeAt(index); + this.filtersFormGroup.markAsDirty(); + } + } + + public addFilter() { + this.openFilterDialog(-1); + } + + public editFilter(index: number) { + this.openFilterDialog(index); + } + + private openFilterDialog(index: number) { + const isAdd = index === -1; + let filter; + const filtersArray = this.filtersFormGroup.get('filters').value as any[]; + if (!isAdd) { + filter = filtersArray[index]; + } + this.dialog.open(FilterDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + isAdd, + filters: filtersArray, + filter: isAdd ? null : deepClone(filter) + } + }).afterClosed().subscribe((result) => { + if (result) { + if (isAdd) { + (this.filtersFormGroup.get('filters') as FormArray) + .push(this.createFilterFormControl(result.id, result)); + } else { + const filterFormControl = (this.filtersFormGroup.get('filters') as FormArray).at(index); + filterFormControl.get('filter').patchValue(result.filter); + filterFormControl.get('editable').patchValue(result.editable); + filterFormControl.get('keyFilters').patchValue(result.keyFilters); + } + this.filtersFormGroup.markAsDirty(); + } + }); + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + const filters: Filters = {}; + const uniqueFilterList: {[filter: string]: string} = {}; + + let valid = true; + let message: string; + + const filtersArray = this.filtersFormGroup.get('filters').value as any[]; + for (const filterValue of filtersArray) { + const filterId: string = filterValue.id; + const filter: string = filterValue.filter; + const keyFilters: Array = filterValue.keyFilters; + const editable: boolean = filterValue.editable; + if (uniqueFilterList[filter]) { + valid = false; + message = this.translate.instant('filter.duplicate-filter-error', {filter}); + break; + } else if (!keyFilters || !keyFilters.length) { + valid = false; + message = this.translate.instant('filter.missing-key-filters-error', {filter}); + break; + } else { + uniqueFilterList[filter] = filter; + filters[filterId] = {id: filterId, filter, keyFilters, editable}; + } + } + if (valid) { + this.dialogRef.close(filters); + } else { + this.store.dispatch(new ActionNotificationShow( + { + message, + type: 'error' + })); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.html b/ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.html new file mode 100644 index 0000000000..3de6b75bc4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.html @@ -0,0 +1,33 @@ + +
+
+
+ {{filter.value.filter}} + +
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.scss b/ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.scss new file mode 100644 index 0000000000..845b959302 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.scss @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + min-width: 300px; + max-height: 150px; + overflow-x: hidden; + overflow-y: auto; + background: #fff; + border-radius: 4px; + box-shadow: + 0 7px 8px -4px rgba(0, 0, 0, .2), + 0 13px 19px 2px rgba(0, 0, 0, .14), + 0 5px 24px 4px rgba(0, 0, 0, .12); + + @media (min-height: 350px) { + max-height: 250px; + } + + .mat-content { + background-color: #fff; + } +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.ts b/ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.ts new file mode 100644 index 0000000000..3477f32a99 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.ts @@ -0,0 +1,62 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, InjectionToken } from '@angular/core'; +import { IAliasController } from '@core/api/widget-api.models'; +import { Filter, FilterInfo } from '@shared/models/query/query.models'; +import { MatDialog } from '@angular/material/dialog'; +import { deepClone } from '@core/utils'; +import { UserFilterDialogComponent, UserFilterDialogData } from '@home/components/filter/user-filter-dialog.component'; + +export const FILTER_EDIT_PANEL_DATA = new InjectionToken('FiltersEditPanelData'); + +export interface FiltersEditPanelData { + aliasController: IAliasController; + filtersInfo: {[filterId: string]: FilterInfo}; +} + +@Component({ + selector: 'tb-filters-edit-panel', + templateUrl: './filters-edit-panel.component.html', + styleUrls: ['./filters-edit-panel.component.scss'] +}) +export class FiltersEditPanelComponent { + + filtersInfo: {[filterId: string]: FilterInfo}; + + constructor(@Inject(FILTER_EDIT_PANEL_DATA) public data: FiltersEditPanelData, + private dialog: MatDialog) { + this.filtersInfo = this.data.filtersInfo; + } + + public editFilter(filterId: string, filter: FilterInfo) { + const singleFilter: Filter = {id: filterId, ...deepClone(filter)}; + this.dialog.open(UserFilterDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + filter: singleFilter + } + }).afterClosed().subscribe( + (result) => { + if (result) { + this.filtersInfo[result.id] = result; + this.data.aliasController.updateUserFilter(result); + } + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.html b/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.html new file mode 100644 index 0000000000..4b090b214c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.html @@ -0,0 +1,32 @@ + +
+ + + {{ 'filter.filters' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.scss b/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.scss new file mode 100644 index 0000000000..a483ad1541 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.scss @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../../../scss/constants"; + +:host { + section.tb-filters-edit { + min-height: 32px; + padding: 0 6px; + + @media #{$mat-lt-md} { + padding: 0; + } + + span { + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + pointer-events: all; + cursor: pointer; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.ts b/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.ts new file mode 100644 index 0000000000..01028a21fb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.ts @@ -0,0 +1,177 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Input, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; +import { TooltipPosition } from '@angular/material/tooltip'; +import { IAliasController } from '@core/api/widget-api.models'; +import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { TranslateService } from '@ngx-translate/core'; +import { Subscription } from 'rxjs'; +import { BreakpointObserver } from '@angular/cdk/layout'; +import { deepClone } from '@core/utils'; +import { Filter, FilterInfo, isFilterEditable } from '@shared/models/query/query.models'; +import { + FILTER_EDIT_PANEL_DATA, + FiltersEditPanelComponent, + FiltersEditPanelData +} from '@home/components/filter/filters-edit-panel.component'; +import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; +import { UserFilterDialogComponent, UserFilterDialogData } from '@home/components/filter/user-filter-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; + +@Component({ + selector: 'tb-filters-edit', + templateUrl: './filters-edit.component.html', + styleUrls: ['./filters-edit.component.scss'] +}) +export class FiltersEditComponent implements OnInit, OnDestroy { + + aliasControllerValue: IAliasController; + + @Input() + set aliasController(aliasController: IAliasController) { + this.aliasControllerValue = aliasController; + this.setupAliasController(this.aliasControllerValue); + } + + get aliasController(): IAliasController { + return this.aliasControllerValue; + } + + @Input() + tooltipPosition: TooltipPosition = 'above'; + + @Input() disabled: boolean; + + @ViewChild('filtersEditPanelOrigin') filtersEditPanelOrigin: CdkOverlayOrigin; + + displayValue: string; + filtersInfo: {[filterId: string]: FilterInfo} = {}; + hasEditableFilters = false; + + private rxSubscriptions = new Array(); + + constructor(private translate: TranslateService, + private overlay: Overlay, + private breakpointObserver: BreakpointObserver, + private viewContainerRef: ViewContainerRef, + private dialog: MatDialog) { + } + + private setupAliasController(aliasController: IAliasController) { + this.rxSubscriptions.forEach((subscription) => { + subscription.unsubscribe(); + }); + this.rxSubscriptions.length = 0; + if (aliasController) { + this.rxSubscriptions.push(aliasController.filtersChanged.subscribe( + () => { + setTimeout(() => { + this.updateFiltersInfo(); + }, 0); + } + )); + setTimeout(() => { + this.updateFiltersInfo(); + }, 0); + } + } + + ngOnInit(): void { + } + + ngOnDestroy(): void { + this.rxSubscriptions.forEach((subscription) => { + subscription.unsubscribe(); + }); + this.rxSubscriptions.length = 0; + } + + openEditMode() { + if (this.disabled || !this.hasEditableFilters) { + return; + } + const filteredArray = Object.entries(this.filtersInfo); + + if (filteredArray.length === 1) { + const singleFilter: Filter = {id: filteredArray[0][0], ...filteredArray[0][1]}; + this.dialog.open(UserFilterDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + filter: singleFilter + } + }).afterClosed().subscribe( + (result) => { + if (result) { + this.filtersInfo[result.id] = result; + this.aliasController.updateUserFilter(result); + } + }); + } else { + const position = this.overlay.position(); + const config = new OverlayConfig({ + panelClass: 'tb-filters-edit-panel', + backdropClass: 'cdk-overlay-transparent-backdrop', + hasBackdrop: true, + }); + const connectedPosition: ConnectedPosition = { + originX: 'start', + originY: 'bottom', + overlayX: 'start', + overlayY: 'top' + }; + config.positionStrategy = position.flexibleConnectedTo(this.filtersEditPanelOrigin.elementRef) + .withPositions([connectedPosition]); + const overlayRef = this.overlay.create(config); + overlayRef.backdropClick().subscribe(() => { + overlayRef.dispose(); + }); + + const injector = this._createFiltersEditPanelInjector( + overlayRef, + { + aliasController: this.aliasController, + filtersInfo: deepClone(this.filtersInfo) + } + ); + overlayRef.attach(new ComponentPortal(FiltersEditPanelComponent, this.viewContainerRef, injector)); + } + } + + private _createFiltersEditPanelInjector(overlayRef: OverlayRef, data: FiltersEditPanelData): PortalInjector { + const injectionTokens = new WeakMap([ + [FILTER_EDIT_PANEL_DATA, data], + [OverlayRef, overlayRef] + ]); + return new PortalInjector(this.viewContainerRef.injector, injectionTokens); + } + + private updateFiltersInfo() { + const allFilters = this.aliasController.getFilters(); + this.filtersInfo = {}; + this.hasEditableFilters = false; + for (const filterId of Object.keys(allFilters)) { + const filterInfo = this.aliasController.getFilterInfo(filterId); + if (filterInfo && isFilterEditable(filterInfo)) { + this.filtersInfo[filterId] = deepClone(filterInfo); + this.hasEditableFilters = true; + } + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html new file mode 100644 index 0000000000..5a9f8858c3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html @@ -0,0 +1,96 @@ + +
+ +

{{(data.isAdd ? 'filter.add-key-filter' : (data.readonly ? 'filter.key-filter' : 'filter.edit-key-filter')) | translate}}

+ + +
+
+
+
+
+ + filter.key-type.key-type + + + {{entityKeyTypeTranslations.get(type) | translate}} + + + + + filter.key-name + + + + {{option}} + + + + {{ 'filter.key-name-required' | translate }} + + +
+ + filter.value-type.value-type + + + + {{ entityKeyValueTypes.get(keyFilterFormGroup.get('valueType').value)?.name | translate }} + + + + {{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).name | translate }} + + + + {{ 'filter.value-type-required' | translate }} + + +
+ + +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.scss b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.scss new file mode 100644 index 0000000000..ef6f184f7c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.scss @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host ::ng-deep { + .entity-key { + mat-form-field { + .mat-form-field-wrapper { + .mat-form-field-infix { + width: auto; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts new file mode 100644 index 0000000000..202d37be34 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts @@ -0,0 +1,167 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { + EntityKeyType, + entityKeyTypeTranslationMap, + EntityKeyValueType, + entityKeyValueTypesMap, + KeyFilterInfo, + KeyFilterPredicate +} from '@shared/models/query/query.models'; +import { DialogService } from '@core/services/dialog.service'; +import { TranslateService } from '@ngx-translate/core'; +import { EntityField, entityFields } from '@shared/models/entity.models'; +import { Observable } from 'rxjs'; +import { filter, map, startWith } from 'rxjs/operators'; + +export interface KeyFilterDialogData { + keyFilter: KeyFilterInfo; + isAdd: boolean; + displayUserParameters: boolean; + allowUserDynamicSource: boolean; + readonly: boolean; + telemetryKeysOnly: boolean; +} + +@Component({ + selector: 'tb-key-filter-dialog', + templateUrl: './key-filter-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: KeyFilterDialogComponent}], + styleUrls: ['./key-filter-dialog.component.scss'] +}) +export class KeyFilterDialogComponent extends + DialogComponent + implements OnInit, ErrorStateMatcher { + + keyFilterFormGroup: FormGroup; + + entityKeyTypes = + this.data.telemetryKeysOnly ? + [EntityKeyType.ATTRIBUTE, EntityKeyType.TIME_SERIES] : + [EntityKeyType.ENTITY_FIELD, EntityKeyType.ATTRIBUTE, EntityKeyType.TIME_SERIES]; + + entityKeyTypeTranslations = entityKeyTypeTranslationMap; + + entityKeyValueTypesKeys = Object.keys(EntityKeyValueType); + + entityKeyValueTypeEnum = EntityKeyValueType; + + entityKeyValueTypes = entityKeyValueTypesMap; + + submitted = false; + + entityFields: { [fieldName: string]: EntityField }; + + entityFieldsList: string[]; + + readonly entityField = EntityKeyType.ENTITY_FIELD; + + filteredEntityFields: Observable; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: KeyFilterDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private dialogs: DialogService, + private translate: TranslateService, + private fb: FormBuilder) { + super(store, router, dialogRef); + + this.keyFilterFormGroup = this.fb.group( + { + key: this.fb.group( + { + type: [this.data.keyFilter.key.type, [Validators.required]], + key: [this.data.keyFilter.key.key, [Validators.required]] + } + ), + valueType: [this.data.keyFilter.valueType, [Validators.required]], + predicates: [this.data.keyFilter.predicates, [Validators.required]] + } + ); + + if (!this.data.readonly) { + this.keyFilterFormGroup.get('valueType').valueChanges.subscribe((valueType: EntityKeyValueType) => { + const prevValue: EntityKeyValueType = this.keyFilterFormGroup.value.valueType; + const predicates: KeyFilterPredicate[] = this.keyFilterFormGroup.get('predicates').value; + if (prevValue && prevValue !== valueType && predicates && predicates.length) { + this.dialogs.confirm(this.translate.instant('filter.key-value-type-change-title'), + this.translate.instant('filter.key-value-type-change-message')).subscribe( + (result) => { + if (result) { + this.keyFilterFormGroup.get('predicates').setValue([]); + } else { + this.keyFilterFormGroup.get('valueType').setValue(prevValue, {emitEvent: false}); + } + } + ); + } + }); + + this.keyFilterFormGroup.get('key.key').valueChanges.pipe( + filter((keyName) => this.keyFilterFormGroup.get('key.type').value === this.entityField && this.entityFields.hasOwnProperty(keyName)) + ).subscribe((keyName: string) => { + const prevValueType: EntityKeyValueType = this.keyFilterFormGroup.value.valueType; + const newValueType = this.entityFields[keyName]?.time ? EntityKeyValueType.DATE_TIME : EntityKeyValueType.STRING; + if (prevValueType !== newValueType) { + this.keyFilterFormGroup.get('valueType').patchValue(newValueType, {emitEvent: false}); + } + }); + } else { + this.keyFilterFormGroup.disable({emitEvent: false}); + } + + this.entityFields = entityFields; + this.entityFieldsList = Object.values(entityFields).map(entityField => entityField.keyName).sort(); + } + + ngOnInit(): void { + this.filteredEntityFields = this.keyFilterFormGroup.get('key.key').valueChanges.pipe( + startWith(''), + map(value => { + return this.entityFieldsList.filter(option => option.startsWith(value)); + }) + ); + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + if (this.keyFilterFormGroup.valid) { + const keyFilter: KeyFilterInfo = this.keyFilterFormGroup.getRawValue(); + this.dialogRef.close(keyFilter); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html new file mode 100644 index 0000000000..26dbaaec4a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html @@ -0,0 +1,92 @@ + +
+ + + + +
filter.key-filters
+
+
+
+ +
+ + +   +   +
+
+ +
+
+
+ filter.operation.and +
+
+
+
{{ keyFilterControl.value.key.key }}
+
{{ entityKeyTypeTranslations.get(keyFilterControl.value.key.type) }}
+ + +
+ +
+
+ filter.no-key-filters +
+
+ +
+
+ + + +
filter.preview
+
+
+
+ +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.scss b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.scss new file mode 100644 index 0000000000..bc47f04915 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.scss @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .key-filter-list { + overflow: auto; + max-height: 300px; + .no-data-found { + height: 50px; + } + } + .filters-operation { + margin-top: -40px; + color: #666; + font-weight: 500; + } + .tb-filter-preview { + padding: 8px; + border: 1px groove rgba(0, 0, 0, .25); + border-radius: 4px; + } +} + +:host ::ng-deep { + .tb-filter-preview { + .tb-filter-text { + max-height: 200px; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts new file mode 100644 index 0000000000..2779a35be9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts @@ -0,0 +1,188 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, FormControl, + FormGroup, + NG_VALUE_ACCESSOR, + Validators +} from '@angular/forms'; +import { Observable, Subscription } from 'rxjs'; +import { + EntityKeyType, + entityKeyTypeTranslationMap, + KeyFilter, + KeyFilterInfo, keyFilterInfosToKeyFilters +} from '@shared/models/query/query.models'; +import { MatDialog } from '@angular/material/dialog'; +import { deepClone } from '@core/utils'; +import { KeyFilterDialogComponent, KeyFilterDialogData } from '@home/components/filter/key-filter-dialog.component'; + +@Component({ + selector: 'tb-key-filter-list', + templateUrl: './key-filter-list.component.html', + styleUrls: ['./key-filter-list.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => KeyFilterListComponent), + multi: true + } + ] +}) +export class KeyFilterListComponent implements ControlValueAccessor, OnInit { + + @Input() disabled: boolean; + + @Input() displayUserParameters = true; + + @Input() allowUserDynamicSource = true; + + @Input() telemetryKeysOnly = false; + + keyFilterListFormGroup: FormGroup; + + entityKeyTypeTranslations = entityKeyTypeTranslationMap; + + keyFiltersControl: FormControl; + + private propagateChange = null; + + private valueChangeSubscription: Subscription = null; + + constructor(private fb: FormBuilder, + private dialog: MatDialog) { + } + + ngOnInit(): void { + this.keyFilterListFormGroup = this.fb.group({}); + this.keyFilterListFormGroup.addControl('keyFilters', + this.fb.array([])); + this.keyFiltersControl = this.fb.control(null); + } + + keyFiltersFormArray(): FormArray { + return this.keyFilterListFormGroup.get('keyFilters') as FormArray; + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState?(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.keyFilterListFormGroup.disable({emitEvent: false}); + this.keyFiltersControl.disable({emitEvent: false}); + } else { + this.keyFilterListFormGroup.enable({emitEvent: false}); + this.keyFiltersControl.enable({emitEvent: false}); + } + } + + writeValue(keyFilters: Array): void { + if (this.valueChangeSubscription) { + this.valueChangeSubscription.unsubscribe(); + } + const keyFilterControls: Array = []; + if (keyFilters) { + for (const keyFilter of keyFilters) { + keyFilterControls.push(this.fb.control(keyFilter, [Validators.required])); + } + } + this.keyFilterListFormGroup.setControl('keyFilters', this.fb.array(keyFilterControls)); + this.valueChangeSubscription = this.keyFilterListFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + if (this.disabled) { + this.keyFilterListFormGroup.disable({emitEvent: false}); + } else { + this.keyFilterListFormGroup.enable({emitEvent: false}); + } + const keyFiltersArray = keyFilterInfosToKeyFilters(keyFilters); + this.keyFiltersControl.patchValue(keyFiltersArray, {emitEvent: false}); + } + + public removeKeyFilter(index: number) { + (this.keyFilterListFormGroup.get('keyFilters') as FormArray).removeAt(index); + } + + public addKeyFilter() { + const keyFiltersFormArray = this.keyFilterListFormGroup.get('keyFilters') as FormArray; + this.openKeyFilterDialog(null).subscribe((result) => { + if (result) { + keyFiltersFormArray.push(this.fb.control(result, [Validators.required])); + } + }); + } + + public editKeyFilter(index: number) { + const keyFilter: KeyFilterInfo = + (this.keyFilterListFormGroup.get('keyFilters') as FormArray).at(index).value; + this.openKeyFilterDialog(keyFilter).subscribe( + (result) => { + if (result) { + (this.keyFilterListFormGroup.get('keyFilters') as FormArray).at(index).patchValue(result); + } + } + ); + } + + private openKeyFilterDialog(keyFilter?: KeyFilterInfo): Observable { + const isAdd = !keyFilter; + if (!keyFilter) { + keyFilter = { + key: { + key: '', + type: EntityKeyType.ATTRIBUTE + }, + valueType: null, + predicates: [] + }; + } + return this.dialog.open(KeyFilterDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + keyFilter: keyFilter ? (this.disabled ? keyFilter : deepClone(keyFilter)) : null, + isAdd, + readonly: this.disabled, + displayUserParameters: this.displayUserParameters, + allowUserDynamicSource: this.allowUserDynamicSource, + telemetryKeysOnly: this.telemetryKeysOnly + } + }).afterClosed(); + } + + private updateModel() { + const keyFilters: Array = this.keyFilterListFormGroup.getRawValue().keyFilters; + if (keyFilters.length) { + this.propagateChange(keyFilters); + } else { + this.propagateChange(null); + } + const keyFiltersArray = keyFilterInfosToKeyFilters(keyFilters); + this.keyFiltersControl.patchValue(keyFiltersArray, {emitEvent: false}); + } +} diff --git a/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.html new file mode 100644 index 0000000000..71cbbd0335 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.html @@ -0,0 +1,32 @@ + +
+ + + + + {{numericOperationTranslations.get(numericOperationEnum[operation]) | translate}} + + + + + +
diff --git a/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts new file mode 100644 index 0000000000..32d5779c63 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts @@ -0,0 +1,100 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { + EntityKeyValueType, + FilterPredicateType, + NumericFilterPredicate, + NumericOperation, + numericOperationTranslationMap, +} from '@shared/models/query/query.models'; + +@Component({ + selector: 'tb-numeric-filter-predicate', + templateUrl: './numeric-filter-predicate.component.html', + styleUrls: ['./filter-predicate.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => NumericFilterPredicateComponent), + multi: true + } + ] +}) +export class NumericFilterPredicateComponent implements ControlValueAccessor, OnInit { + + @Input() disabled: boolean; + + @Input() allowUserDynamicSource = true; + + @Input() valueType: EntityKeyValueType; + + numericFilterPredicateFormGroup: FormGroup; + + valueTypeEnum = EntityKeyValueType; + + numericOperations = Object.keys(NumericOperation); + numericOperationEnum = NumericOperation; + numericOperationTranslations = numericOperationTranslationMap; + + private propagateChange = null; + + constructor(private fb: FormBuilder) { + } + + ngOnInit(): void { + this.numericFilterPredicateFormGroup = this.fb.group({ + operation: [NumericOperation.EQUAL, [Validators.required]], + value: [null, [Validators.required]] + }); + this.numericFilterPredicateFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState?(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.numericFilterPredicateFormGroup.disable({emitEvent: false}); + } else { + this.numericFilterPredicateFormGroup.enable({emitEvent: false}); + } + } + + writeValue(predicate: NumericFilterPredicate): void { + this.numericFilterPredicateFormGroup.get('operation').patchValue(predicate.operation, {emitEvent: false}); + this.numericFilterPredicateFormGroup.get('value').patchValue(predicate.value, {emitEvent: false}); + } + + private updateModel() { + let predicate: NumericFilterPredicate = null; + if (this.numericFilterPredicateFormGroup.valid) { + predicate = this.numericFilterPredicateFormGroup.getRawValue(); + predicate.type = FilterPredicateType.NUMERIC; + } + this.propagateChange(predicate); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html new file mode 100644 index 0000000000..92d470c6b8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html @@ -0,0 +1,36 @@ + +
+
+ + + + + {{stringOperationTranslations.get(stringOperationEnum[operation]) | translate}} + + + + + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts new file mode 100644 index 0000000000..dffd52b274 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts @@ -0,0 +1,100 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { + EntityKeyValueType, + FilterPredicateType, + StringFilterPredicate, + StringOperation, + stringOperationTranslationMap +} from '@shared/models/query/query.models'; + +@Component({ + selector: 'tb-string-filter-predicate', + templateUrl: './string-filter-predicate.component.html', + styleUrls: ['./filter-predicate.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => StringFilterPredicateComponent), + multi: true + } + ] +}) +export class StringFilterPredicateComponent implements ControlValueAccessor, OnInit { + + @Input() disabled: boolean; + + @Input() allowUserDynamicSource = true; + + valueTypeEnum = EntityKeyValueType; + + stringFilterPredicateFormGroup: FormGroup; + + stringOperations = Object.keys(StringOperation); + stringOperationEnum = StringOperation; + stringOperationTranslations = stringOperationTranslationMap; + + private propagateChange = null; + + constructor(private fb: FormBuilder) { + } + + ngOnInit(): void { + this.stringFilterPredicateFormGroup = this.fb.group({ + operation: [StringOperation.STARTS_WITH, [Validators.required]], + value: [null, [Validators.required]], + ignoreCase: [false] + }); + this.stringFilterPredicateFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState?(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.stringFilterPredicateFormGroup.disable({emitEvent: false}); + } else { + this.stringFilterPredicateFormGroup.enable({emitEvent: false}); + } + } + + writeValue(predicate: StringFilterPredicate): void { + this.stringFilterPredicateFormGroup.get('operation').patchValue(predicate.operation, {emitEvent: false}); + this.stringFilterPredicateFormGroup.get('value').patchValue(predicate.value, {emitEvent: false}); + this.stringFilterPredicateFormGroup.get('ignoreCase').patchValue(predicate.ignoreCase, {emitEvent: false}); + } + + private updateModel() { + let predicate: StringFilterPredicate = null; + if (this.stringFilterPredicateFormGroup.valid) { + predicate = this.stringFilterPredicateFormGroup.getRawValue(); + predicate.type = FilterPredicateType.STRING; + } + this.propagateChange(predicate); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html new file mode 100644 index 0000000000..bf38483244 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html @@ -0,0 +1,80 @@ + +
+ +

{{ filter.filter }}

+ + +
+ + +
+
+
+
+
+ + + {{ userInputControl.get('label').value }} + + + + + + {{ userInputControl.get('label').value }} + + + + + + + + + + {{ userInputControl.get('label').value }} + + +
+
+
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.ts new file mode 100644 index 0000000000..d743058de5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.ts @@ -0,0 +1,121 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + AbstractControl, FormArray, + FormBuilder, + FormControl, + FormGroup, + FormGroupDirective, + NgForm, + Validators +} from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { TranslateService } from '@ngx-translate/core'; +import { + EntityKeyValueType, + Filter, FilterPredicateValue, + filterToUserFilterInfoList, + UserFilterInputInfo +} from '@shared/models/query/query.models'; +import { isDefinedAndNotNull } from '@core/utils'; + +export interface UserFilterDialogData { + filter: Filter; +} + +@Component({ + selector: 'tb-user-filter-dialog', + templateUrl: './user-filter-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: UserFilterDialogComponent}], + styleUrls: [] +}) +export class UserFilterDialogComponent extends DialogComponent + implements OnInit, ErrorStateMatcher { + + filter: Filter; + + userFilterFormGroup: FormGroup; + + valueTypeEnum = EntityKeyValueType; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: UserFilterDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + public translate: TranslateService) { + super(store, router, dialogRef); + this.filter = data.filter; + const userInputs = filterToUserFilterInfoList(this.filter, translate); + + const userInputControls: Array = []; + for (const userInput of userInputs) { + userInputControls.push(this.createUserInputFormControl(userInput)); + } + + this.userFilterFormGroup = this.fb.group({ + userInputs: this.fb.array(userInputControls) + }); + } + + private createUserInputFormControl(userInput: UserFilterInputInfo): AbstractControl { + const predicateValue: FilterPredicateValue = (userInput.info.keyFilterPredicate as any).value; + const value = isDefinedAndNotNull(predicateValue.userValue) ? predicateValue.userValue : predicateValue.defaultValue; + const userInputControl = this.fb.group({ + label: [userInput.label], + valueType: [userInput.valueType], + value: [value, + userInput.valueType === EntityKeyValueType.NUMERIC || + userInput.valueType === EntityKeyValueType.DATE_TIME ? [Validators.required] : []] + }); + userInputControl.get('value').valueChanges.subscribe(userValue => { + (userInput.info.keyFilterPredicate as any).value.userValue = userValue; + }); + return userInputControl; + } + + userInputsFormArray(): FormArray { + return this.userFilterFormGroup.get('userInputs') as FormArray; + } + + ngOnInit(): void { + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + this.dialogRef.close(this.filter); + } +} diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts new file mode 100644 index 0000000000..ec7ccea660 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -0,0 +1,309 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@app/shared/shared.module'; +import { AddEntityDialogComponent } from '@home/components/entity/add-entity-dialog.component'; +import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; +import { DetailsPanelComponent } from '@home/components/details-panel.component'; +import { EntityDetailsPanelComponent } from '@home/components/entity/entity-details-panel.component'; +import { AuditLogDetailsDialogComponent } from '@home/components/audit-log/audit-log-details-dialog.component'; +import { AuditLogTableComponent } from '@home/components/audit-log/audit-log-table.component'; +import { EventTableHeaderComponent } from '@home/components/event/event-table-header.component'; +import { EventTableComponent } from '@home/components/event/event-table.component'; +import { RelationTableComponent } from '@home/components/relation/relation-table.component'; +import { RelationDialogComponent } from '@home/components/relation/relation-dialog.component'; +import { AlarmTableHeaderComponent } from '@home/components/alarm/alarm-table-header.component'; +import { AlarmTableComponent } from '@home/components/alarm/alarm-table.component'; +import { AttributeTableComponent } from '@home/components/attribute/attribute-table.component'; +import { AddAttributeDialogComponent } from '@home/components/attribute/add-attribute-dialog.component'; +import { EditAttributeValuePanelComponent } from '@home/components/attribute/edit-attribute-value-panel.component'; +import { DashboardComponent } from '@home/components/dashboard/dashboard.component'; +import { WidgetComponent } from '@home/components/widget/widget.component'; +import { WidgetComponentService } from '@home/components/widget/widget-component.service'; +import { LegendComponent } from '@home/components/widget/legend.component'; +import { AliasesEntitySelectPanelComponent } from '@home/components/alias/aliases-entity-select-panel.component'; +import { AliasesEntitySelectComponent } from '@home/components/alias/aliases-entity-select.component'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { EntityAliasesDialogComponent } from '@home/components/alias/entity-aliases-dialog.component'; +import { EntityFilterViewComponent } from '@home/components/entity/entity-filter-view.component'; +import { EntityAliasDialogComponent } from '@home/components/alias/entity-alias-dialog.component'; +import { EntityFilterComponent } from '@home/components/entity/entity-filter.component'; +import { RelationFiltersComponent } from '@home/components/relation/relation-filters.component'; +import { EntityAliasSelectComponent } from '@home/components/alias/entity-alias-select.component'; +import { DataKeysComponent } from '@home/components/widget/data-keys.component'; +import { DataKeyConfigDialogComponent } from '@home/components/widget/data-key-config-dialog.component'; +import { DataKeyConfigComponent } from '@home/components/widget/data-key-config.component'; +import { LegendConfigPanelComponent } from '@home/components/widget/legend-config-panel.component'; +import { LegendConfigComponent } from '@home/components/widget/legend-config.component'; +import { ManageWidgetActionsComponent } from '@home/components/widget/action/manage-widget-actions.component'; +import { WidgetActionDialogComponent } from '@home/components/widget/action/widget-action-dialog.component'; +import { CustomActionPrettyResourcesTabsComponent } from '@home/components/widget/action/custom-action-pretty-resources-tabs.component'; +import { CustomActionPrettyEditorComponent } from '@home/components/widget/action/custom-action-pretty-editor.component'; +import { CustomDialogService } from '@home/components/widget/dialog/custom-dialog.service'; +import { CustomDialogContainerComponent } from '@home/components/widget/dialog/custom-dialog-container.component'; +import { ImportExportService } from '@home/components/import-export/import-export.service'; +import { ImportDialogComponent } from '@home/components/import-export/import-dialog.component'; +import { AddWidgetToDashboardDialogComponent } from '@home/components/attribute/add-widget-to-dashboard-dialog.component'; +import { ImportDialogCsvComponent } from '@home/components/import-export/import-dialog-csv.component'; +import { TableColumnsAssignmentComponent } from '@home/components/import-export/table-columns-assignment.component'; +import { EventContentDialogComponent } from '@home/components/event/event-content-dialog.component'; +import { SharedHomeComponentsModule } from '@home/components/shared-home-components.module'; +import { SelectTargetLayoutDialogComponent } from '@home/components/dashboard/select-target-layout-dialog.component'; +import { SelectTargetStateDialogComponent } from '@home/components/dashboard/select-target-state-dialog.component'; +import { AliasesEntityAutocompleteComponent } from '@home/components/alias/aliases-entity-autocomplete.component'; +import { BooleanFilterPredicateComponent } from '@home/components/filter/boolean-filter-predicate.component'; +import { StringFilterPredicateComponent } from '@home/components/filter/string-filter-predicate.component'; +import { NumericFilterPredicateComponent } from '@home/components/filter/numeric-filter-predicate.component'; +import { ComplexFilterPredicateComponent } from '@home/components/filter/complex-filter-predicate.component'; +import { FilterPredicateComponent } from '@home/components/filter/filter-predicate.component'; +import { FilterPredicateListComponent } from '@home/components/filter/filter-predicate-list.component'; +import { KeyFilterListComponent } from '@home/components/filter/key-filter-list.component'; +import { ComplexFilterPredicateDialogComponent } from '@home/components/filter/complex-filter-predicate-dialog.component'; +import { KeyFilterDialogComponent } from '@home/components/filter/key-filter-dialog.component'; +import { FiltersDialogComponent } from '@home/components/filter/filters-dialog.component'; +import { FilterDialogComponent } from '@home/components/filter/filter-dialog.component'; +import { FilterSelectComponent } from './filter/filter-select.component'; +import { FiltersEditComponent } from '@home/components/filter/filters-edit.component'; +import { FiltersEditPanelComponent } from '@home/components/filter/filters-edit-panel.component'; +import { UserFilterDialogComponent } from '@home/components/filter/user-filter-dialog.component'; +import { FilterUserInfoComponent } from './filter/filter-user-info.component'; +import { FilterUserInfoDialogComponent } from './filter/filter-user-info-dialog.component'; +import { FilterPredicateValueComponent } from './filter/filter-predicate-value.component'; +import { TenantProfileAutocompleteComponent } from './profile/tenant-profile-autocomplete.component'; +import { TenantProfileComponent } from './profile/tenant-profile.component'; +import { TenantProfileDialogComponent } from './profile/tenant-profile-dialog.component'; +import { TenantProfileDataComponent } from './profile/tenant-profile-data.component'; +import { DefaultDeviceProfileConfigurationComponent } from './profile/device/default-device-profile-configuration.component'; +import { DeviceProfileConfigurationComponent } from './profile/device/device-profile-configuration.component'; +import { DeviceProfileComponent } from './profile/device-profile.component'; +import { DefaultDeviceProfileTransportConfigurationComponent } from './profile/device/default-device-profile-transport-configuration.component'; +import { DeviceProfileTransportConfigurationComponent } from './profile/device/device-profile-transport-configuration.component'; +import { DeviceProfileDialogComponent } from './profile/device-profile-dialog.component'; +import { DeviceProfileAutocompleteComponent } from './profile/device-profile-autocomplete.component'; +import { MqttDeviceProfileTransportConfigurationComponent } from './profile/device/mqtt-device-profile-transport-configuration.component'; +import { Lwm2mDeviceProfileTransportConfigurationComponent } from './profile/device/lwm2m-device-profile-transport-configuration.component'; +import { DeviceProfileAlarmsComponent } from './profile/alarm/device-profile-alarms.component'; +import { DeviceProfileAlarmComponent } from './profile/alarm/device-profile-alarm.component'; +import { CreateAlarmRulesComponent } from './profile/alarm/create-alarm-rules.component'; +import { AlarmRuleComponent } from './profile/alarm/alarm-rule.component'; +import { AlarmRuleConditionComponent } from './profile/alarm/alarm-rule-condition.component'; +import { FilterTextComponent } from './filter/filter-text.component'; +import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-dialog.component'; +import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomplete.component'; +import { DeviceProfileProvisionConfigurationComponent } from "./profile/device-profile-provision-configuration.component"; +import { AlarmScheduleComponent } from './profile/alarm/alarm-schedule.component'; +import { DeviceWizardDialogComponent } from './wizard/device-wizard-dialog.component'; +import { DeviceCredentialsComponent } from './device/device-credentials.component'; +import { AlarmScheduleInfoComponent } from './profile/alarm/alarm-schedule-info.component'; +import { AlarmScheduleDialogComponent } from '@home/components/profile/alarm/alarm-schedule-dialog.component'; +import { EditAlarmDetailsDialogComponent } from './profile/alarm/edit-alarm-details-dialog.component'; +import { AlarmRuleConditionDialogComponent } from '@home/components/profile/alarm/alarm-rule-condition-dialog.component'; +import { DefaultTenantProfileConfigurationComponent } from './profile/tenant/default-tenant-profile-configuration.component'; +import { TenantProfileConfigurationComponent } from './profile/tenant/tenant-profile-configuration.component'; + +@NgModule({ + declarations: + [ + EntitiesTableComponent, + AddEntityDialogComponent, + DetailsPanelComponent, + EntityDetailsPanelComponent, + AuditLogTableComponent, + AuditLogDetailsDialogComponent, + EventContentDialogComponent, + EventTableHeaderComponent, + EventTableComponent, + RelationTableComponent, + RelationDialogComponent, + RelationFiltersComponent, + AlarmTableHeaderComponent, + AlarmTableComponent, + AttributeTableComponent, + AddAttributeDialogComponent, + EditAttributeValuePanelComponent, + AliasesEntitySelectPanelComponent, + AliasesEntitySelectComponent, + AliasesEntityAutocompleteComponent, + EntityAliasesDialogComponent, + EntityAliasDialogComponent, + DashboardComponent, + WidgetComponent, + LegendComponent, + WidgetConfigComponent, + EntityFilterViewComponent, + EntityFilterComponent, + EntityAliasSelectComponent, + DataKeysComponent, + DataKeyConfigComponent, + DataKeyConfigDialogComponent, + LegendConfigPanelComponent, + LegendConfigComponent, + ManageWidgetActionsComponent, + WidgetActionDialogComponent, + CustomActionPrettyResourcesTabsComponent, + CustomActionPrettyEditorComponent, + CustomDialogContainerComponent, + ImportDialogComponent, + ImportDialogCsvComponent, + SelectTargetLayoutDialogComponent, + SelectTargetStateDialogComponent, + AddWidgetToDashboardDialogComponent, + TableColumnsAssignmentComponent, + BooleanFilterPredicateComponent, + StringFilterPredicateComponent, + NumericFilterPredicateComponent, + ComplexFilterPredicateComponent, + ComplexFilterPredicateDialogComponent, + FilterPredicateComponent, + FilterPredicateListComponent, + KeyFilterListComponent, + KeyFilterDialogComponent, + FilterDialogComponent, + FiltersDialogComponent, + FilterSelectComponent, + FilterTextComponent, + FiltersEditComponent, + FiltersEditPanelComponent, + UserFilterDialogComponent, + FilterUserInfoComponent, + FilterUserInfoDialogComponent, + FilterPredicateValueComponent, + TenantProfileAutocompleteComponent, + DefaultTenantProfileConfigurationComponent, + TenantProfileConfigurationComponent, + TenantProfileDataComponent, + TenantProfileComponent, + TenantProfileDialogComponent, + DeviceProfileAutocompleteComponent, + DefaultDeviceProfileConfigurationComponent, + DeviceProfileConfigurationComponent, + DefaultDeviceProfileTransportConfigurationComponent, + MqttDeviceProfileTransportConfigurationComponent, + Lwm2mDeviceProfileTransportConfigurationComponent, + DeviceProfileTransportConfigurationComponent, + CreateAlarmRulesComponent, + AlarmRuleComponent, + AlarmRuleConditionDialogComponent, + AlarmRuleConditionComponent, + DeviceProfileAlarmComponent, + DeviceProfileAlarmsComponent, + DeviceProfileComponent, + DeviceProfileDialogComponent, + AddDeviceProfileDialogComponent, + RuleChainAutocompleteComponent, + AlarmScheduleInfoComponent, + DeviceProfileProvisionConfigurationComponent, + AlarmScheduleComponent, + DeviceWizardDialogComponent, + DeviceCredentialsComponent, + AlarmScheduleDialogComponent, + EditAlarmDetailsDialogComponent + ], + imports: [ + CommonModule, + SharedModule, + SharedHomeComponentsModule + ], + exports: [ + EntitiesTableComponent, + AddEntityDialogComponent, + DetailsPanelComponent, + EntityDetailsPanelComponent, + AuditLogTableComponent, + EventTableComponent, + RelationTableComponent, + RelationFiltersComponent, + AlarmTableComponent, + AttributeTableComponent, + AliasesEntitySelectComponent, + AliasesEntityAutocompleteComponent, + EntityAliasesDialogComponent, + EntityAliasDialogComponent, + DashboardComponent, + WidgetComponent, + LegendComponent, + WidgetConfigComponent, + EntityFilterViewComponent, + EntityFilterComponent, + EntityAliasSelectComponent, + DataKeysComponent, + DataKeyConfigComponent, + DataKeyConfigDialogComponent, + LegendConfigComponent, + ManageWidgetActionsComponent, + WidgetActionDialogComponent, + CustomActionPrettyResourcesTabsComponent, + CustomActionPrettyEditorComponent, + CustomDialogContainerComponent, + ImportDialogComponent, + ImportDialogCsvComponent, + TableColumnsAssignmentComponent, + SelectTargetLayoutDialogComponent, + SelectTargetStateDialogComponent, + BooleanFilterPredicateComponent, + StringFilterPredicateComponent, + NumericFilterPredicateComponent, + ComplexFilterPredicateComponent, + ComplexFilterPredicateDialogComponent, + FilterPredicateComponent, + FilterPredicateListComponent, + KeyFilterListComponent, + KeyFilterDialogComponent, + FilterDialogComponent, + FiltersDialogComponent, + FilterSelectComponent, + FilterTextComponent, + FiltersEditComponent, + UserFilterDialogComponent, + TenantProfileAutocompleteComponent, + TenantProfileDataComponent, + TenantProfileComponent, + TenantProfileDialogComponent, + DeviceProfileAutocompleteComponent, + DefaultDeviceProfileConfigurationComponent, + DeviceProfileConfigurationComponent, + DefaultDeviceProfileTransportConfigurationComponent, + MqttDeviceProfileTransportConfigurationComponent, + Lwm2mDeviceProfileTransportConfigurationComponent, + DeviceProfileTransportConfigurationComponent, + CreateAlarmRulesComponent, + AlarmRuleComponent, + AlarmRuleConditionDialogComponent, + AlarmRuleConditionComponent, + DeviceProfileAlarmComponent, + DeviceProfileAlarmsComponent, + DeviceProfileComponent, + DeviceProfileDialogComponent, + AddDeviceProfileDialogComponent, + RuleChainAutocompleteComponent, + DeviceWizardDialogComponent, + DeviceCredentialsComponent, + AlarmScheduleInfoComponent, + AlarmScheduleComponent, + AlarmScheduleDialogComponent, + EditAlarmDetailsDialogComponent, + DeviceProfileProvisionConfigurationComponent, + AlarmScheduleComponent + ], + providers: [ + WidgetComponentService, + CustomDialogService, + ImportExportService + ] +}) +export class HomeComponentsModule { } diff --git a/ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.html b/ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.html new file mode 100644 index 0000000000..52d5e3a9d5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.html @@ -0,0 +1,141 @@ + +
+ +

{{ importTitle }}

+ +
+ +
+ + +
+ + + + {{ 'import.stepper-text.select-file' | translate }} +
+ + +
+ +
+ + +
+
+ +
+ {{ 'import.stepper-text.configuration' | translate }} +
+ + import.csv-delimiter + + + {{ delimiter.value }} + + + +
+ + {{ 'import.csv-first-line-header' | translate }} + + + {{ 'import.csv-update-data' | translate }} + +
+
+
+
+ +
+ + +
+
+
+ +
+ {{ 'import.stepper-text.column-type' | translate }} + +
+
+ +
+ + +
+
+
+ + {{ 'import.stepper-text.creat-entities' | translate }} + + + + + {{ 'import.stepper-text.done' | translate }} +
+

+ {{ translate.instant('import.message.create-entities', {count: this.statistical.create.entity}) }} +

+

+ {{ translate.instant('import.message.update-entities', {count: this.statistical.update.entity}) }} +

+

+ {{ translate.instant('import.message.error-entities', {count: this.statistical.error.entity}) }} +

+
+
+ +
+
+
+
+ diff --git a/ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.scss b/ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.scss new file mode 100644 index 0000000000..bed63393bb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.scss @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host ::ng-deep { + .mat-dialog-content { + .mat-vertical-content { + form { + padding-bottom: 12px; + } + } + .mat-vertical-stepper-header { + pointer-events: none !important; + } + .tb-import-progress{ + margin: 7px 0; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.ts b/ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.ts new file mode 100644 index 0000000000..7897d24feb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.ts @@ -0,0 +1,268 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, ViewChild } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { EntityType } from '@shared/models/entity-type.models'; +import { TranslateService } from '@ngx-translate/core'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { MatVerticalStepper } from '@angular/material/stepper'; +import { + convertCSVToJson, + CsvColumnParam, + CsvToJsonConfig, + CsvToJsonResult, + ImportEntityColumnType +} from '@home/components/import-export/import-export.models'; +import { ImportEntitiesResultInfo, ImportEntityData } from '@app/shared/models/entity.models'; +import { ImportExportService } from '@home/components/import-export/import-export.service'; + +export interface ImportDialogCsvData { + entityType: EntityType; + importTitle: string; + importFileLabel: string; +} + +@Component({ + selector: 'tb-import-csv-dialog', + templateUrl: './import-dialog-csv.component.html', + providers: [], + styleUrls: ['./import-dialog-csv.component.scss'] +}) +export class ImportDialogCsvComponent extends DialogComponent + implements OnInit { + + @ViewChild('importStepper', {static: true}) importStepper: MatVerticalStepper; + + entityType: EntityType; + importTitle: string; + importFileLabel: string; + + delimiters: {key: string, value: string}[] = [{ + key: ',', + value: ',' + }, { + key: ';', + value: ';' + }, { + key: '|', + value: '|' + }, { + key: '\t', + value: 'Tab' + }]; + + selectedIndex = 0; + + selectFileFormGroup: FormGroup; + importParametersFormGroup: FormGroup; + columnTypesFormGroup: FormGroup; + + isImportData = false; + progressCreate = 0; + statistical: ImportEntitiesResultInfo; + + private parseData: CsvToJsonResult; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: ImportDialogCsvData, + public dialogRef: MatDialogRef, + public translate: TranslateService, + private importExport: ImportExportService, + private fb: FormBuilder) { + super(store, router, dialogRef); + this.entityType = data.entityType; + this.importTitle = data.importTitle; + this.importFileLabel = data.importFileLabel; + + this.selectFileFormGroup = this.fb.group( + { + importData: [null, [Validators.required]] + } + ); + this.importParametersFormGroup = this.fb.group({ + delim: [',', [Validators.required]], + isHeader: [true, []], + isUpdate: [true, []], + }); + this.columnTypesFormGroup = this.fb.group({ + columnsParam: [[], []] + }); + } + + ngOnInit(): void { + } + + cancel(): void { + this.dialogRef.close(false); + } + + previousStep() { + this.importStepper.previous(); + } + + nextStep(step: number) { + switch (step) { + case 2: + this.importStepper.next(); + break; + case 3: + const importData: string = this.selectFileFormGroup.get('importData').value; + const parseData = this.parseCSV(importData); + if (parseData === -1) { + this.importStepper.previous(); + this.importStepper.selected.reset(); + } else { + this.parseData = parseData as CsvToJsonResult; + const columnsParam = this.createColumnsData(); + this.columnTypesFormGroup.patchValue({columnsParam}, {emitEvent: true}); + this.importStepper.next(); + } + break; + case 4: + this.importStepper.next(); + this.isImportData = true; + this.addEntities(); + break; + case 6: + this.dialogRef.close(true); + break; + } + } + + private parseCSV(importData: string): CsvToJsonResult | number { + const config: CsvToJsonConfig = { + delim: this.importParametersFormGroup.get('delim').value, + header: this.importParametersFormGroup.get('isHeader').value + }; + return convertCSVToJson(importData, config, + (messageId, params) => { + this.store.dispatch(new ActionNotificationShow( + {message: this.translate.instant(messageId, params), + type: 'error'})); + } + ); + } + + private createColumnsData(): CsvColumnParam[] { + const columnsParam: CsvColumnParam[] = []; + const isHeader: boolean = this.importParametersFormGroup.get('isHeader').value; + for (let i = 0; i < this.parseData.headers.length; i++) { + let columnParam: CsvColumnParam; + if (isHeader && this.parseData.headers[i].search(/^(name|type|label)$/im) === 0) { + columnParam = { + type: ImportEntityColumnType[this.parseData.headers[i].toLowerCase()], + key: this.parseData.headers[i].toLowerCase(), + sampleData: this.parseData.rows[0][i] + }; + } else { + columnParam = { + type: ImportEntityColumnType.serverAttribute, + key: isHeader ? this.parseData.headers[i] : '', + sampleData: this.parseData.rows[0][i] + }; + } + columnsParam.push(columnParam); + } + return columnsParam; + } + + private addEntities() { + const importData = this.parseData; + const parameterColumns: CsvColumnParam[] = this.columnTypesFormGroup.get('columnsParam').value; + const entitiesData: ImportEntityData[] = []; + let sentDataLength = 0; + for (let row = 0; row < importData.rows.length; row++) { + const entityData: ImportEntityData = { + name: '', + type: '', + description: '', + gateway: null, + label: '', + accessToken: '', + attributes: { + server: [], + shared: [] + }, + timeseries: [] + }; + const i = row; + for (let j = 0; j < parameterColumns.length; j++) { + switch (parameterColumns[j].type) { + case ImportEntityColumnType.serverAttribute: + entityData.attributes.server.push({ + key: parameterColumns[j].key, + value: importData.rows[i][j] + }); + break; + case ImportEntityColumnType.timeseries: + entityData.timeseries.push({ + key: parameterColumns[j].key, + value: importData.rows[i][j] + }); + break; + case ImportEntityColumnType.sharedAttribute: + entityData.attributes.shared.push({ + key: parameterColumns[j].key, + value: importData.rows[i][j] + }); + break; + case ImportEntityColumnType.accessToken: + entityData.accessToken = importData.rows[i][j]; + break; + case ImportEntityColumnType.name: + entityData.name = importData.rows[i][j]; + break; + case ImportEntityColumnType.type: + entityData.type = importData.rows[i][j]; + break; + case ImportEntityColumnType.label: + entityData.label = importData.rows[i][j]; + break; + case ImportEntityColumnType.isGateway: + entityData.gateway = importData.rows[i][j]; + break; + case ImportEntityColumnType.description: + entityData.description = importData.rows[i][j]; + break; + } + } + entitiesData.push(entityData); + } + const createImportEntityCompleted = () => { + sentDataLength++; + this.progressCreate = Math.round((sentDataLength / importData.rows.length) * 100); + }; + + const isUpdate: boolean = this.importParametersFormGroup.get('isUpdate').value; + + this.importExport.importEntities(entitiesData, this.entityType, isUpdate, + createImportEntityCompleted, {ignoreErrors: true, resendRequest: true}).subscribe( + (result) => { + this.statistical = result; + this.isImportData = false; + this.importStepper.next(); + } + ); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/import-export/import-dialog.component.html b/ui-ngx/src/app/modules/home/components/import-export/import-dialog.component.html new file mode 100644 index 0000000000..a8fe3a1f46 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/import-export/import-dialog.component.html @@ -0,0 +1,58 @@ + +
+ +

{{ importTitle }}

+ + +
+ + +
+
+
+ + +
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/import-export/import-dialog.component.ts b/ui-ngx/src/app/modules/home/components/import-export/import-dialog.component.ts new file mode 100644 index 0000000000..ba0e66fc2e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/import-export/import-dialog.component.ts @@ -0,0 +1,91 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; + +export interface ImportDialogData { + importTitle: string; + importFileLabel: string; +} + +@Component({ + selector: 'tb-import-dialog', + templateUrl: './import-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: ImportDialogComponent}], + styleUrls: [] +}) +export class ImportDialogComponent extends DialogComponent + implements OnInit, ErrorStateMatcher { + + importTitle: string; + importFileLabel: string; + + importFormGroup: FormGroup; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: ImportDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private fb: FormBuilder) { + super(store, router, dialogRef); + this.importTitle = data.importTitle; + this.importFileLabel = data.importFileLabel; + + this.importFormGroup = this.fb.group({ + jsonContent: [null, [Validators.required]] + }); + } + + ngOnInit(): void { + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + loadDataFromJsonContent(content: string): any { + try { + const importData = JSON.parse(content); + return importData; + } catch (err) { + this.store.dispatch(new ActionNotificationShow({message: err.message, type: 'error'})); + return null; + } + } + + cancel(): void { + this.dialogRef.close(null); + } + + importFromJson(): void { + this.submitted = true; + const importData = this.importFormGroup.get('jsonContent').value; + this.dialogRef.close(importData); + } +} diff --git a/ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts b/ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts new file mode 100644 index 0000000000..cd37c43223 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts @@ -0,0 +1,163 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Widget, WidgetType } from '@app/shared/models/widget.models'; +import { DashboardLayoutId } from '@shared/models/dashboard.models'; +import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; + +export interface ImportWidgetResult { + widget: Widget; + layoutId: DashboardLayoutId; +} + +export interface WidgetsBundleItem { + widgetsBundle: WidgetsBundle; + widgetTypes: WidgetType[]; +} + +export interface CsvToJsonConfig { + delim?: string; + header?: boolean; +} + +export interface CsvToJsonResult { + headers?: string[]; + rows?: any[][]; +} + +export enum ImportEntityColumnType { + name = 'NAME', + type = 'TYPE', + label = 'LABEL', + clientAttribute = 'CLIENT_ATTRIBUTE', + sharedAttribute = 'SHARED_ATTRIBUTE', + serverAttribute = 'SERVER_ATTRIBUTE', + timeseries = 'TIMESERIES', + entityField = 'ENTITY_FIELD', + accessToken = 'ACCESS_TOKEN', + isGateway = 'IS_GATEWAY', + description = 'DESCRIPTION' +} + +export const importEntityObjectColumns = + [ImportEntityColumnType.name, ImportEntityColumnType.type, ImportEntityColumnType.accessToken]; + +export const importEntityColumnTypeTranslations = new Map( + [ + [ImportEntityColumnType.name, 'import.column-type.name'], + [ImportEntityColumnType.type, 'import.column-type.type'], + [ImportEntityColumnType.label, 'import.column-type.label'], + [ImportEntityColumnType.clientAttribute, 'import.column-type.client-attribute'], + [ImportEntityColumnType.sharedAttribute, 'import.column-type.shared-attribute'], + [ImportEntityColumnType.serverAttribute, 'import.column-type.server-attribute'], + [ImportEntityColumnType.timeseries, 'import.column-type.timeseries'], + [ImportEntityColumnType.entityField, 'import.column-type.entity-field'], + [ImportEntityColumnType.accessToken, 'import.column-type.access-token'], + [ImportEntityColumnType.isGateway, 'import.column-type.isgateway'], + [ImportEntityColumnType.description, 'import.column-type.description'], + ] +); + +export interface CsvColumnParam { + type: ImportEntityColumnType; + key: string; + sampleData: any; +} + +export interface FileType { + mimeType: string; + extension: string; +} + +export const JSON_TYPE: FileType = { + mimeType: 'text/json', + extension: 'json' +}; + +export const ZIP_TYPE: FileType = { + mimeType: 'application/zip', + extension: 'zip' +}; + +export function convertCSVToJson(csvdata: string, config: CsvToJsonConfig, + onError: (messageId: string, params?: any) => void): CsvToJsonResult | number { + config = config || {}; + const delim = config.delim || ','; + const header = config.header || false; + const result: CsvToJsonResult = {}; + const csvlines = csvdata.split(/[\r\n]+/); + const csvheaders = splitCSV(csvlines[0], delim); + if (csvheaders.length < 2) { + onError('import.import-csv-number-columns-error'); + return -1; + } + const csvrows = header ? csvlines.slice(1, csvlines.length) : csvlines; + result.headers = csvheaders; + result.rows = []; + for (const row of csvrows) { + if (row.length === 0) { + break; + } + const rowitems: any[] = splitCSV(row, delim); + if (rowitems.length !== result.headers.length) { + onError('import.import-csv-invalid-format-error', {line: (header ? result.rows.length + 2 : result.rows.length + 1)}); + return -1; + } + for (let i = 0; i < rowitems.length; i++) { + rowitems[i] = convertStringToJSType(rowitems[i]); + } + result.rows.push(rowitems); + } + return result; +} + +function splitCSV(str: string, sep: string): string[] { + let foo: string[]; + let x: number; + let tl: string; + for (foo = str.split(sep = sep || ','), x = foo.length - 1, tl; x >= 0; x--) { + if (foo[x].replace(/"\s+$/, '"').charAt(foo[x].length - 1) === '"') { + if ((tl = foo[x].replace(/^\s+"/, '"')).length > 1 && tl.charAt(0) === '"') { + foo[x] = foo[x].replace(/^\s*"|"\s*$/g, '').replace(/""/g, '"'); + } else if (x) { + foo.splice(x - 1, 2, [foo[x - 1], foo[x]].join(sep)); + } else { + foo = foo.shift().split(sep).concat(foo); + } + } else { + foo[x].replace(/""/g, '"'); + } + } + return foo; +} + +function isNumeric(str: any): boolean { + str = str.replace(',', '.'); + return !isNaN(parseFloat(str)) && isFinite(str); +} + +function convertStringToJSType(str: string): any { + if (isNumeric(str.replace(',', '.'))) { + return parseFloat(str.replace(',', '.')); + } + if (str.search(/^(true|false)$/im) === 0) { + return str.toLowerCase() === 'true'; + } + if (str === '') { + return null; + } + return str; +} diff --git a/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts b/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts new file mode 100644 index 0000000000..53c70ce0ee --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts @@ -0,0 +1,758 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Inject, Injectable } from '@angular/core'; +import { DashboardService } from '@core/http/dashboard.service'; +import { TranslateService } from '@ngx-translate/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { Dashboard, DashboardLayoutId } from '@shared/models/dashboard.models'; +import { deepClone, isDefined, isObject, isUndefined } from '@core/utils'; +import { WINDOW } from '@core/services/window.service'; +import { DOCUMENT } from '@angular/common'; +import { + AliasesInfo, + AliasFilterType, + EntityAlias, + EntityAliases, + EntityAliasFilter, + EntityAliasInfo +} from '@shared/models/alias.models'; +import { MatDialog } from '@angular/material/dialog'; +import { ImportDialogComponent, ImportDialogData } from '@home/components/import-export/import-dialog.component'; +import { forkJoin, Observable, of } from 'rxjs'; +import { catchError, map, mergeMap, tap } from 'rxjs/operators'; +import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; +import { EntityService } from '@core/http/entity.service'; +import { Widget, WidgetSize, WidgetType } from '@shared/models/widget.models'; +import { + EntityAliasesDialogComponent, + EntityAliasesDialogData +} from '@home/components/alias/entity-aliases-dialog.component'; +import { ItemBufferService, WidgetItem } from '@core/services/item-buffer.service'; +import { FileType, ImportWidgetResult, JSON_TYPE, WidgetsBundleItem, ZIP_TYPE } from './import-export.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { UtilsService } from '@core/services/utils.service'; +import { WidgetService } from '@core/http/widget.service'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; +import { ImportEntitiesResultInfo, ImportEntityData } from '@shared/models/entity.models'; +import { RequestConfig } from '@core/http/http-utils'; +import { RuleChain, RuleChainImport, RuleChainMetaData } from '@shared/models/rule-chain.models'; +import { RuleChainService } from '@core/http/rule-chain.service'; +import * as JSZip from 'jszip'; +import { FiltersInfo } from '@shared/models/query/query.models'; + +// @dynamic +@Injectable() +export class ImportExportService { + + constructor(@Inject(WINDOW) private window: Window, + @Inject(DOCUMENT) private document: Document, + private store: Store, + private translate: TranslateService, + private dashboardService: DashboardService, + private dashboardUtils: DashboardUtilsService, + private widgetService: WidgetService, + private entityService: EntityService, + private ruleChainService: RuleChainService, + private utils: UtilsService, + private itembuffer: ItemBufferService, + private dialog: MatDialog) { + + } + + public exportDashboard(dashboardId: string) { + this.dashboardService.getDashboard(dashboardId).subscribe( + (dashboard) => { + let name = dashboard.title; + name = name.toLowerCase().replace(/\W/g, '_'); + this.exportToPc(this.prepareDashboardExport(dashboard), name); + }, + (e) => { + this.handleExportError(e, 'dashboard.export-failed-error'); + } + ); + } + + public importDashboard(): Observable { + return this.openImportDialog('dashboard.import', 'dashboard.dashboard-file').pipe( + mergeMap((dashboard: Dashboard) => { + if (!this.validateImportedDashboard(dashboard)) { + this.store.dispatch(new ActionNotificationShow( + {message: this.translate.instant('dashboard.invalid-dashboard-file-error'), + type: 'error'})); + throw new Error('Invalid dashboard file'); + } else { + dashboard = this.dashboardUtils.validateAndUpdateDashboard(dashboard); + let aliasIds = null; + const entityAliases = dashboard.configuration.entityAliases; + if (entityAliases) { + aliasIds = Object.keys(entityAliases); + } + if (aliasIds && aliasIds.length > 0) { + return this.processEntityAliases(entityAliases, aliasIds).pipe( + mergeMap((missingEntityAliases) => { + if (Object.keys(missingEntityAliases).length > 0) { + return this.editMissingAliases(this.dashboardUtils.getWidgetsArray(dashboard), + false, 'dashboard.dashboard-import-missing-aliases-title', + missingEntityAliases).pipe( + mergeMap((updatedEntityAliases) => { + for (const aliasId of Object.keys(updatedEntityAliases)) { + entityAliases[aliasId] = updatedEntityAliases[aliasId]; + } + return this.saveImportedDashboard(dashboard); + }) + ); + } else { + return this.saveImportedDashboard(dashboard); + } + } + )); + } else { + return this.saveImportedDashboard(dashboard); + } + } + }), + catchError((err) => { + return of(null); + }) + ); + } + + public exportWidget(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget) { + const widgetItem = this.itembuffer.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget); + let name = widgetItem.widget.config.title; + name = name.toLowerCase().replace(/\W/g, '_'); + this.exportToPc(this.prepareExport(widgetItem), name); + } + + public importWidget(dashboard: Dashboard, targetState: string, + targetLayoutFunction: () => Observable, + onAliasesUpdateFunction: () => void, + onFiltersUpdateFunction: () => void): Observable { + return this.openImportDialog('dashboard.import-widget', 'dashboard.widget-file').pipe( + mergeMap((widgetItem: WidgetItem) => { + if (!this.validateImportedWidget(widgetItem)) { + this.store.dispatch(new ActionNotificationShow( + {message: this.translate.instant('dashboard.invalid-widget-file-error'), + type: 'error'})); + throw new Error('Invalid widget file'); + } else { + let widget = widgetItem.widget; + widget = this.dashboardUtils.validateAndUpdateWidget(widget); + const aliasesInfo = this.prepareAliasesInfo(widgetItem.aliasesInfo); + const filtersInfo: FiltersInfo = widgetItem.filtersInfo || { + datasourceFilters: {} + }; + const originalColumns = widgetItem.originalColumns; + const originalSize = widgetItem.originalSize; + + const datasourceAliases = aliasesInfo.datasourceAliases; + const targetDeviceAliases = aliasesInfo.targetDeviceAliases; + if (datasourceAliases || targetDeviceAliases) { + const entityAliases: EntityAliases = {}; + const datasourceAliasesMap: {[aliasId: string]: number} = {}; + const targetDeviceAliasesMap: {[aliasId: string]: number} = {}; + let aliasId: string; + let datasourceIndex: number; + if (datasourceAliases) { + for (const strIndex of Object.keys(datasourceAliases)) { + datasourceIndex = Number(strIndex); + aliasId = this.utils.guid(); + datasourceAliasesMap[aliasId] = datasourceIndex; + entityAliases[aliasId] = {id: aliasId, ...datasourceAliases[datasourceIndex]}; + } + } + if (targetDeviceAliases) { + for (const strIndex of Object.keys(targetDeviceAliases)) { + datasourceIndex = Number(strIndex); + aliasId = this.utils.guid(); + targetDeviceAliasesMap[aliasId] = datasourceIndex; + entityAliases[aliasId] = {id: aliasId, ...targetDeviceAliases[datasourceIndex]}; + } + } + const aliasIds = Object.keys(entityAliases); + if (aliasIds.length > 0) { + return this.processEntityAliases(entityAliases, aliasIds).pipe( + mergeMap((missingEntityAliases) => { + if (Object.keys(missingEntityAliases).length > 0) { + return this.editMissingAliases([widget], + false, 'dashboard.widget-import-missing-aliases-title', + missingEntityAliases).pipe( + mergeMap((updatedEntityAliases) => { + for (const id of Object.keys(updatedEntityAliases)) { + const entityAlias = updatedEntityAliases[id]; + let index; + if (isDefined(datasourceAliasesMap[id])) { + index = datasourceAliasesMap[id]; + datasourceAliases[index] = entityAlias; + } else if (isDefined(targetDeviceAliasesMap[id])) { + index = targetDeviceAliasesMap[id]; + targetDeviceAliases[index] = entityAlias; + } + } + return this.addImportedWidget(dashboard, targetState, targetLayoutFunction, widget, + aliasesInfo, filtersInfo, onAliasesUpdateFunction, onFiltersUpdateFunction, originalColumns, originalSize); + } + )); + } else { + return this.addImportedWidget(dashboard, targetState, targetLayoutFunction, widget, + aliasesInfo, filtersInfo, onAliasesUpdateFunction, onFiltersUpdateFunction, originalColumns, originalSize); + } + } + ) + ); + } else { + return this.addImportedWidget(dashboard, targetState, targetLayoutFunction, widget, + aliasesInfo, filtersInfo, onAliasesUpdateFunction, onFiltersUpdateFunction, originalColumns, originalSize); + } + } else { + return this.addImportedWidget(dashboard, targetState, targetLayoutFunction, widget, + aliasesInfo, filtersInfo, onAliasesUpdateFunction, onFiltersUpdateFunction, originalColumns, originalSize); + } + } + }), + catchError((err) => { + return of(null); + }) + ); + } + + public exportWidgetType(widgetTypeId: string) { + this.widgetService.getWidgetTypeById(widgetTypeId).subscribe( + (widgetType) => { + if (isDefined(widgetType.bundleAlias)) { + delete widgetType.bundleAlias; + } + let name = widgetType.name; + name = name.toLowerCase().replace(/\W/g, '_'); + this.exportToPc(this.prepareExport(widgetType), name); + }, + (e) => { + this.handleExportError(e, 'widget-type.export-failed-error'); + } + ); + } + + public importWidgetType(bundleAlias: string): Observable { + return this.openImportDialog('widget-type.import', 'widget-type.widget-type-file').pipe( + mergeMap((widgetType: WidgetType) => { + if (!this.validateImportedWidgetType(widgetType)) { + this.store.dispatch(new ActionNotificationShow( + {message: this.translate.instant('widget-type.invalid-widget-type-file-error'), + type: 'error'})); + throw new Error('Invalid widget type file'); + } else { + widgetType.bundleAlias = bundleAlias; + return this.widgetService.saveImportedWidgetType(widgetType); + } + }), + catchError((err) => { + return of(null); + }) + ); + } + + public exportWidgetsBundle(widgetsBundleId: string) { + this.widgetService.getWidgetsBundle(widgetsBundleId).subscribe( + (widgetsBundle) => { + const bundleAlias = widgetsBundle.alias; + const isSystem = widgetsBundle.tenantId.id === NULL_UUID; + this.widgetService.getBundleWidgetTypes(bundleAlias, isSystem).subscribe( + (widgetTypes) => { + const widgetsBundleItem: WidgetsBundleItem = { + widgetsBundle: this.prepareExport(widgetsBundle), + widgetTypes: [] + }; + for (const widgetType of widgetTypes) { + if (isDefined(widgetType.bundleAlias)) { + delete widgetType.bundleAlias; + } + widgetsBundleItem.widgetTypes.push(this.prepareExport(widgetType)); + } + let name = widgetsBundle.title; + name = name.toLowerCase().replace(/\W/g, '_'); + this.exportToPc(widgetsBundleItem, name); + }, + (e) => { + this.handleExportError(e, 'widgets-bundle.export-failed-error'); + } + ); + }, + (e) => { + this.handleExportError(e, 'widgets-bundle.export-failed-error'); + } + ); + } + + public importWidgetsBundle(): Observable { + return this.openImportDialog('widgets-bundle.import', 'widgets-bundle.widgets-bundle-file').pipe( + mergeMap((widgetsBundleItem: WidgetsBundleItem) => { + if (!this.validateImportedWidgetsBundle(widgetsBundleItem)) { + this.store.dispatch(new ActionNotificationShow( + {message: this.translate.instant('widgets-bundle.invalid-widgets-bundle-file-error'), + type: 'error'})); + throw new Error('Invalid widgets bundle file'); + } else { + const widgetsBundle = widgetsBundleItem.widgetsBundle; + return this.widgetService.saveWidgetsBundle(widgetsBundle).pipe( + mergeMap((savedWidgetsBundle) => { + const bundleAlias = savedWidgetsBundle.alias; + const widgetTypes = widgetsBundleItem.widgetTypes; + if (widgetTypes.length) { + const saveWidgetTypesObservables: Array> = []; + for (const widgetType of widgetTypes) { + widgetType.bundleAlias = bundleAlias; + saveWidgetTypesObservables.push(this.widgetService.saveImportedWidgetType(widgetType)); + } + return forkJoin(saveWidgetTypesObservables).pipe( + map(() => savedWidgetsBundle) + ); + } else { + return of(savedWidgetsBundle); + } + } + )); + } + }), + catchError((err) => { + return of(null); + }) + ); + } + + public importEntities(entitiesData: ImportEntityData[], entityType: EntityType, updateData: boolean, + importEntityCompleted?: () => void, config?: RequestConfig): Observable { + let partSize = 100; + partSize = entitiesData.length > partSize ? partSize : entitiesData.length; + + let statisticalInfo: ImportEntitiesResultInfo = {}; + const importEntitiesObservables: Observable[] = []; + for (let i = 0; i < partSize; i++) { + const importEntityPromise = + this.entityService.saveEntityParameters(entityType, entitiesData[i], updateData, config).pipe( + tap((res) => { + if (importEntityCompleted) { + importEntityCompleted(); + } + }) + ); + importEntitiesObservables.push(importEntityPromise); + } + return forkJoin(importEntitiesObservables).pipe( + mergeMap((responses) => { + for (const response of responses) { + statisticalInfo = this.sumObject(statisticalInfo, response); + } + entitiesData.splice(0, partSize); + if (entitiesData.length) { + return this.importEntities(entitiesData, entityType, updateData, importEntityCompleted, config).pipe( + map((response) => { + return this.sumObject(statisticalInfo, response) as ImportEntitiesResultInfo; + }) + ); + } else { + return of(statisticalInfo); + } + }) + ); + } + + public exportRuleChain(ruleChainId: string) { + this.ruleChainService.getRuleChain(ruleChainId).pipe( + mergeMap(ruleChain => { + return this.ruleChainService.getRuleChainMetadata(ruleChainId).pipe( + map((ruleChainMetaData) => { + const ruleChainExport: RuleChainImport = { + ruleChain: this.prepareRuleChain(ruleChain), + metadata: this.prepareRuleChainMetaData(ruleChainMetaData) + }; + return ruleChainExport; + }) + ); + }) + ).subscribe((ruleChainExport) => { + let name = ruleChainExport.ruleChain.name; + name = name.toLowerCase().replace(/\W/g, '_'); + this.exportToPc(ruleChainExport, name); + }, + (e) => { + this.handleExportError(e, 'rulechain.export-failed-error'); + } + ); + } + + public importRuleChain(): Observable { + return this.openImportDialog('rulechain.import', 'rulechain.rulechain-file').pipe( + mergeMap((ruleChainImport: RuleChainImport) => { + if (!this.validateImportedRuleChain(ruleChainImport)) { + this.store.dispatch(new ActionNotificationShow( + {message: this.translate.instant('rulechain.invalid-rulechain-file-error'), + type: 'error'})); + throw new Error('Invalid rule chain file'); + } else { + return this.ruleChainService.resolveRuleChainMetadata(ruleChainImport.metadata).pipe( + map((resolvedMetadata) => { + ruleChainImport.resolvedMetadata = resolvedMetadata; + return ruleChainImport; + }) + ); + } + }), + catchError((err) => { + return of(null); + }) + ); + } + + public exportJSZip(data: object, filename: string) { + const jsZip = new JSZip(); + for (const keyName in data) { + if (data.hasOwnProperty(keyName)) { + const valueData = data[keyName]; + jsZip.file(keyName, valueData); + } + } + jsZip.generateAsync({type: 'blob'}).then(content => { + this.downloadFile(content, filename, ZIP_TYPE); + }); + } + + private prepareRuleChain(ruleChain: RuleChain): RuleChain { + ruleChain = this.prepareExport(ruleChain); + if (ruleChain.firstRuleNodeId) { + ruleChain.firstRuleNodeId = null; + } + ruleChain.root = false; + return ruleChain; + } + + private prepareRuleChainMetaData(ruleChainMetaData: RuleChainMetaData) { + delete ruleChainMetaData.ruleChainId; + for (let i = 0; i < ruleChainMetaData.nodes.length; i++) { + const node = ruleChainMetaData.nodes[i]; + delete node.ruleChainId; + ruleChainMetaData.nodes[i] = this.prepareExport(node); + } + return ruleChainMetaData; + } + + private validateImportedRuleChain(ruleChainImport: RuleChainImport): boolean { + if (isUndefined(ruleChainImport.ruleChain) + || isUndefined(ruleChainImport.metadata) + || isUndefined(ruleChainImport.ruleChain.name)) { + return false; + } + return true; + } + + private sumObject(obj1: any, obj2: any): any { + Object.keys(obj2).map((key) => { + if (isObject(obj2[key])) { + obj1[key] = obj1[key] || {}; + obj1[key] = {...obj1[key], ...this.sumObject(obj1[key], obj2[key])}; + } else { + obj1[key] = (obj1[key] || 0) + obj2[key]; + } + }); + return obj1; + } + + private handleExportError(e: any, errorDetailsMessageId: string) { + let message = e; + if (!message) { + message = this.translate.instant('error.unknown-error'); + } + this.store.dispatch(new ActionNotificationShow( + {message: this.translate.instant(errorDetailsMessageId, {error: message}), + type: 'error'})); + } + + private validateImportedDashboard(dashboard: Dashboard): boolean { + if (isUndefined(dashboard.title) || isUndefined(dashboard.configuration)) { + return false; + } + return true; + } + + private validateImportedWidget(widgetItem: WidgetItem): boolean { + if (isUndefined(widgetItem.widget) + || isUndefined(widgetItem.aliasesInfo) + || isUndefined(widgetItem.originalColumns)) { + return false; + } + const widget = widgetItem.widget; + if (isUndefined(widget.isSystemType) || + isUndefined(widget.bundleAlias) || + isUndefined(widget.typeAlias) || + isUndefined(widget.type)) { + return false; + } + return true; + } + + private validateImportedWidgetType(widgetType: WidgetType): boolean { + if (isUndefined(widgetType.name) + || isUndefined(widgetType.descriptor)) { + return false; + } + return true; + } + + private validateImportedWidgetsBundle(widgetsBundleItem: WidgetsBundleItem): boolean { + if (isUndefined(widgetsBundleItem.widgetsBundle)) { + return false; + } + if (isUndefined(widgetsBundleItem.widgetTypes)) { + return false; + } + const widgetsBundle = widgetsBundleItem.widgetsBundle; + if (isUndefined(widgetsBundle.title)) { + return false; + } + const widgetTypes = widgetsBundleItem.widgetTypes; + for (const widgetType of widgetTypes) { + if (!this.validateImportedWidgetType(widgetType)) { + return false; + } + } + return true; + } + + private saveImportedDashboard(dashboard: Dashboard): Observable { + return this.dashboardService.saveDashboard(dashboard); + } + + private addImportedWidget(dashboard: Dashboard, targetState: string, + targetLayoutFunction: () => Observable, + widget: Widget, aliasesInfo: AliasesInfo, + filtersInfo: FiltersInfo, + onAliasesUpdateFunction: () => void, + onFiltersUpdateFunction: () => void, + originalColumns: number, originalSize: WidgetSize): Observable { + return targetLayoutFunction().pipe( + mergeMap((targetLayout) => { + return this.itembuffer.addWidgetToDashboard(dashboard, targetState, targetLayout, + widget, aliasesInfo, filtersInfo, onAliasesUpdateFunction, onFiltersUpdateFunction, + originalColumns, originalSize, -1, -1).pipe( + map(() => ({widget, layoutId: targetLayout} as ImportWidgetResult)) + ); + } + )); + } + + private processEntityAliases(entityAliases: EntityAliases, aliasIds: string[]): Observable { + const tasks: Observable[] = []; + for (const aliasId of aliasIds) { + const entityAlias = entityAliases[aliasId]; + tasks.push( + this.entityService.checkEntityAlias(entityAlias).pipe( + map((result) => { + if (!result) { + const missingEntityAlias = deepClone(entityAlias); + missingEntityAlias.filter = null; + return missingEntityAlias; + } + return null; + } + ) + ) + ); + } + return forkJoin(tasks).pipe( + map((missingAliasesArray) => { + missingAliasesArray = missingAliasesArray.filter(alias => alias !== null); + const missingEntityAliases: EntityAliases = {}; + for (const missingAlias of missingAliasesArray) { + missingEntityAliases[missingAlias.id] = missingAlias; + } + return missingEntityAliases; + } + ) + ); + } + + private editMissingAliases(widgets: Array, isSingleWidget: boolean, + customTitle: string, missingEntityAliases: EntityAliases): Observable { + return this.dialog.open(EntityAliasesDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + entityAliases: missingEntityAliases, + widgets, + customTitle, + isSingleWidget, + disableAdd: true + } + }).afterClosed().pipe( + map((updatedEntityAliases) => { + if (updatedEntityAliases) { + return updatedEntityAliases; + } else { + throw new Error('Unable to resolve missing entity aliases!'); + } + } + )); + } + + private prepareAliasesInfo(aliasesInfo: AliasesInfo): AliasesInfo { + const datasourceAliases = aliasesInfo.datasourceAliases; + const targetDeviceAliases = aliasesInfo.targetDeviceAliases; + if (datasourceAliases || targetDeviceAliases) { + if (datasourceAliases) { + for (const strIndex of Object.keys(datasourceAliases)) { + const datasourceIndex = Number(strIndex); + datasourceAliases[datasourceIndex] = this.prepareEntityAlias(datasourceAliases[datasourceIndex]); + } + } + if (targetDeviceAliases) { + for (const strIndex of Object.keys(targetDeviceAliases)) { + const datasourceIndex = Number(strIndex); + targetDeviceAliases[datasourceIndex] = this.prepareEntityAlias(targetDeviceAliases[datasourceIndex]); + } + } + } + return aliasesInfo; + } + + private prepareEntityAlias(aliasInfo: EntityAliasInfo): EntityAliasInfo { + let alias: string; + let filter: EntityAliasFilter; + if (aliasInfo.deviceId) { + alias = aliasInfo.aliasName; + filter = { + type: AliasFilterType.entityList, + entityType: EntityType.DEVICE, + entityList: [aliasInfo.deviceId], + resolveMultiple: false + }; + } else if (aliasInfo.deviceFilter) { + alias = aliasInfo.aliasName; + filter = { + type: aliasInfo.deviceFilter.useFilter ? AliasFilterType.entityName : AliasFilterType.entityList, + entityType: EntityType.DEVICE, + resolveMultiple: false + }; + if (filter.type === AliasFilterType.entityList) { + filter.entityList = aliasInfo.deviceFilter.deviceList; + } else { + filter.entityNameFilter = aliasInfo.deviceFilter.deviceNameFilter; + } + } else if (aliasInfo.entityFilter) { + alias = aliasInfo.aliasName; + filter = { + type: aliasInfo.entityFilter.useFilter ? AliasFilterType.entityName : AliasFilterType.entityList, + entityType: aliasInfo.entityType, + resolveMultiple: false + }; + if (filter.type === AliasFilterType.entityList) { + filter.entityList = aliasInfo.entityFilter.entityList; + } else { + filter.entityNameFilter = aliasInfo.entityFilter.entityNameFilter; + } + } else { + alias = aliasInfo.alias; + filter = aliasInfo.filter; + } + return { + alias, + filter + }; + } + + private openImportDialog(importTitle: string, importFileLabel: string): Observable { + return this.dialog.open(ImportDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + importTitle, + importFileLabel + } + }).afterClosed().pipe( + map((importedData) => { + if (importedData) { + return importedData; + } else { + throw new Error('No file selected!'); + } + } + )); + } + + private exportToPc(data: any, filename: string) { + if (!data) { + console.error('No data'); + return; + } + this.exportJson(data, filename); + } + + private exportJson(data: any, filename: string) { + if (isObject(data)) { + data = JSON.stringify(data, null, 2); + } + this.downloadFile(data, filename, JSON_TYPE); + } + + private downloadFile(data: any, filename: string, fileType: FileType) { + if (!filename) { + filename = 'download'; + } + filename += '.' + fileType.extension; + const blob = new Blob([data], {type: fileType.mimeType}); + if (this.window.navigator && this.window.navigator.msSaveOrOpenBlob) { + this.window.navigator.msSaveOrOpenBlob(blob, filename); + } else { + const e = this.document.createEvent('MouseEvents'); + const a = this.document.createElement('a'); + a.download = filename; + a.href = URL.createObjectURL(blob); + a.dataset.downloadurl = [fileType.mimeType, a.download, a.href].join(':'); + // @ts-ignore + e.initEvent('click', true, false, this.window, + 0, 0, 0, 0, 0, false, false, false, false, 0, null); + a.dispatchEvent(e); + } + } + + private prepareDashboardExport(dashboard: Dashboard): Dashboard { + dashboard = this.prepareExport(dashboard); + delete dashboard.assignedCustomers; + return dashboard; + } + + private prepareExport(data: any): any { + const exportedData = deepClone(data); + if (isDefined(exportedData.id)) { + delete exportedData.id; + } + if (isDefined(exportedData.createdTime)) { + delete exportedData.createdTime; + } + if (isDefined(exportedData.tenantId)) { + delete exportedData.tenantId; + } + if (isDefined(exportedData.customerId)) { + delete exportedData.customerId; + } + return exportedData; + } + +} diff --git a/ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.html b/ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.html new file mode 100644 index 0000000000..d862a988ef --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.html @@ -0,0 +1,56 @@ + + + + + + {{ (i+1) }} + + + + {{ 'import.column-example' | translate }} + + {{column.sampleData}} + + + + {{ 'import.column-type.column-type' | translate }} + + + + {{ columnTypesTranslations.get(type.value) | translate }} + + + + + + {{ 'import.column-key' | translate }} + + + + + + + + + +
+ diff --git a/ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.scss b/ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.scss new file mode 100644 index 0000000000..b0a80ae965 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.scss @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .mat-column-order { + flex: 0 0 40px; + } + .mat-column-sampleData { + flex: 0 0 120px; + min-width: 120px; + } + .mat-column-type { + flex: 0 0 120px; + min-width: 120px; + } +} diff --git a/ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.ts b/ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.ts new file mode 100644 index 0000000000..3555efd1e3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.ts @@ -0,0 +1,196 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityType } from '@shared/models/entity-type.models'; +import { + CsvColumnParam, + ImportEntityColumnType, + importEntityColumnTypeTranslations, + importEntityObjectColumns +} from '@home/components/import-export/import-export.models'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { CollectionViewer, DataSource } from '@angular/cdk/collections'; + +@Component({ + selector: 'tb-table-columns-assignment', + templateUrl: './table-columns-assignment.component.html', + styleUrls: ['./table-columns-assignment.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TableColumnsAssignmentComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TableColumnsAssignmentComponent), + multi: true, + } + ] +}) +export class TableColumnsAssignmentComponent implements OnInit, ControlValueAccessor, Validator { + + @Input() entityType: EntityType; + + @Input() disabled: boolean; + + dataSource = new CsvColumnsDatasource(); + + displayedColumns = ['order', 'sampleData', 'type', 'key']; + + columnTypes: AssignmentColumnType[] = []; + + columnTypesTranslations = importEntityColumnTypeTranslations; + + private columns: CsvColumnParam[]; + + private valid = true; + + private propagateChangePending = false; + private propagateChange = null; + + constructor(public elementRef: ElementRef, + protected store: Store) { + } + + ngOnInit(): void { + this.columnTypes.push( + { value: ImportEntityColumnType.name }, + { value: ImportEntityColumnType.type }, + { value: ImportEntityColumnType.label }, + { value: ImportEntityColumnType.description }, + ); + switch (this.entityType) { + case EntityType.DEVICE: + this.columnTypes.push( + { value: ImportEntityColumnType.sharedAttribute }, + { value: ImportEntityColumnType.serverAttribute }, + { value: ImportEntityColumnType.timeseries }, + { value: ImportEntityColumnType.accessToken }, + { value: ImportEntityColumnType.isGateway } + ); + break; + case EntityType.ASSET: + this.columnTypes.push( + { value: ImportEntityColumnType.serverAttribute }, + { value: ImportEntityColumnType.timeseries } + ); + break; + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + if (this.propagateChangePending) { + this.propagateChange(this.columns); + this.propagateChangePending = false; + } + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + columnsUpdated() { + const isSelectName = this.columns.findIndex((column) => column.type === ImportEntityColumnType.name) > -1; + const isSelectType = this.columns.findIndex((column) => column.type === ImportEntityColumnType.type) > -1; + const isSelectLabel = this.columns.findIndex((column) => column.type === ImportEntityColumnType.label) > -1; + const isSelectCredentials = this.columns.findIndex((column) => column.type === ImportEntityColumnType.accessToken) > -1; + const isSelectGateway = this.columns.findIndex((column) => column.type === ImportEntityColumnType.isGateway) > -1; + const isSelectDescription = this.columns.findIndex((column) => column.type === ImportEntityColumnType.description) > -1; + const hasInvalidColumn = this.columns.findIndex((column) => !this.columnValid(column)) > -1; + + this.valid = isSelectName && isSelectType && !hasInvalidColumn; + + this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.name).disabled = isSelectName; + this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.type).disabled = isSelectType; + this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.label).disabled = isSelectLabel; + this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.description).disabled = isSelectDescription; + + const isGatewayColumnType = this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.isGateway); + if (isGatewayColumnType) { + isGatewayColumnType.disabled = isSelectGateway; + } + const accessTokenColumnType = this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.accessToken); + if (accessTokenColumnType) { + accessTokenColumnType.disabled = isSelectCredentials; + } + if (this.propagateChange) { + this.propagateChange(this.columns); + } else { + this.propagateChangePending = true; + } + } + + public isColumnTypeDiffers(columnType: ImportEntityColumnType): boolean { + return columnType === ImportEntityColumnType.clientAttribute || + columnType === ImportEntityColumnType.sharedAttribute || + columnType === ImportEntityColumnType.serverAttribute || + columnType === ImportEntityColumnType.timeseries; + } + + private columnValid(column: CsvColumnParam): boolean { + if (!importEntityObjectColumns.includes(column.type)) { + return column.key && column.key.trim().length > 0; + } else { + return true; + } + } + + public validate(c: FormControl) { + return (this.valid) ? null : { + columnsInvalid: true + }; + } + + writeValue(value: CsvColumnParam[]): void { + this.columns = value; + this.dataSource.setColumns(this.columns); + this.columnsUpdated(); + } +} + +interface AssignmentColumnType { + value: ImportEntityColumnType; + disabled?: boolean; +} + +class CsvColumnsDatasource implements DataSource { + + private columnsSubject = new BehaviorSubject([]); + + constructor() {} + + connect(collectionViewer: CollectionViewer): Observable> { + return this.columnsSubject.asObservable(); + } + + disconnect(collectionViewer: CollectionViewer): void { + this.columnsSubject.complete(); + } + + setColumns(columns: CsvColumnParam[]) { + this.columnsSubject.next(columns); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html new file mode 100644 index 0000000000..c696272d78 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html @@ -0,0 +1,134 @@ + +
+ +

device-profile.add

+ + +
+ + +
+
+ + +
+ {{ 'device-profile.device-profile-details' | translate }} +
+ + device-profile.name + + + {{ 'device-profile.name-required' | translate }} + + + + + + + + device-profile.type + + + {{deviceProfileTypeTranslations.get(type) | translate}} + + + + {{ 'device-profile.type-required' | translate }} + + + + device-profile.description + + +
+
+
+ +
+ {{ 'device-profile.transport-configuration' | translate }} + + device-profile.transport-type + + + {{deviceTransportTypeTranslations.get(type) | translate}} + + + + {{deviceTransportTypeHints.get(transportConfigFormGroup.get('transportType').value) | translate}} + + + {{ 'device-profile.transport-type-required' | translate }} + + + + +
+
+ +
+ {{'device-profile.alarm-rules-with-count' | translate: + {count: alarmRulesFormGroup.get('alarms').value ? + alarmRulesFormGroup.get('alarms').value.length : 0} }} + + +
+
+ +
+ {{ 'device-profile.device-provisioning' | translate }} + + +
+
+
+
+
+ + + +
+ +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss new file mode 100644 index 0000000000..d2a716bb50 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss @@ -0,0 +1,64 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../../../scss/constants"; + +:host-context(.tb-fullscreen-dialog .mat-dialog-container) { + @media #{$mat-lt-sm} { + .mat-dialog-content { + max-height: 75vh; + } + } +} + +:host ::ng-deep { + .mat-dialog-content { + display: flex; + flex-direction: column; + height: 100%; + padding: 0 !important; + + .mat-stepper-horizontal { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + @media #{$mat-lt-sm} { + .mat-step-label { + white-space: normal; + overflow: visible; + .mat-step-text-label { + overflow: visible; + } + } + } + .mat-horizontal-content-container { + height: 530px; + max-height: 100%; + width: 100%;; + overflow-y: auto; + @media #{$mat-gt-sm} { + min-width: 800px; + } + } + .mat-horizontal-stepper-content[aria-expanded=true] { + height: 100%; + form { + height: 100%; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts new file mode 100644 index 0000000000..07ce80aca6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts @@ -0,0 +1,245 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + Component, + ComponentFactoryResolver, + Inject, + Injector, + SkipSelf, + ViewChild +} from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; +import { + createDeviceProfileConfiguration, + createDeviceProfileTransportConfiguration, + DeviceProfile, + DeviceProfileType, + deviceProfileTypeTranslationMap, + DeviceProvisionConfiguration, + DeviceProvisionType, + DeviceTransportType, + deviceTransportTypeHintMap, + deviceTransportTypeTranslationMap +} from '@shared/models/device.models'; +import { DeviceProfileService } from '@core/http/device-profile.service'; +import { EntityType } from '@shared/models/entity-type.models'; +import { MatHorizontalStepper } from '@angular/material/stepper'; +import { RuleChainId } from '@shared/models/id/rule-chain-id'; +import { StepperSelectionEvent } from '@angular/cdk/stepper'; +import { deepTrim } from '@core/utils'; +import {ServiceType} from "@shared/models/queue.models"; + +export interface AddDeviceProfileDialogData { + deviceProfileName: string; + transportType: DeviceTransportType; +} + +@Component({ + selector: 'tb-add-device-profile-dialog', + templateUrl: './add-device-profile-dialog.component.html', + providers: [], + styleUrls: ['./add-device-profile-dialog.component.scss'] +}) +export class AddDeviceProfileDialogComponent extends + DialogComponent implements AfterViewInit { + + @ViewChild('addDeviceProfileStepper', {static: true}) addDeviceProfileStepper: MatHorizontalStepper; + + selectedIndex = 0; + + showNext = true; + + entityType = EntityType; + + deviceProfileTypes = Object.keys(DeviceProfileType); + + deviceProfileTypeTranslations = deviceProfileTypeTranslationMap; + + deviceTransportTypeHints = deviceTransportTypeHintMap; + + deviceTransportTypes = Object.keys(DeviceTransportType); + + deviceTransportTypeTranslations = deviceTransportTypeTranslationMap; + + deviceProfileDetailsFormGroup: FormGroup; + + transportConfigFormGroup: FormGroup; + + alarmRulesFormGroup: FormGroup; + + provisionConfigFormGroup: FormGroup; + + serviceType = ServiceType.TB_RULE_ENGINE; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AddDeviceProfileDialogData, + public dialogRef: MatDialogRef, + private componentFactoryResolver: ComponentFactoryResolver, + private injector: Injector, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + private deviceProfileService: DeviceProfileService, + private fb: FormBuilder) { + super(store, router, dialogRef); + this.deviceProfileDetailsFormGroup = this.fb.group( + { + name: [data.deviceProfileName, [Validators.required]], + type: [DeviceProfileType.DEFAULT, [Validators.required]], + defaultRuleChainId: [null, []], + defaultQueueName: ['', []], + description: ['', []] + } + ); + this.transportConfigFormGroup = this.fb.group( + { + transportType: [data.transportType ? data.transportType : DeviceTransportType.DEFAULT, [Validators.required]], + transportConfiguration: [createDeviceProfileTransportConfiguration(DeviceTransportType.DEFAULT), + [Validators.required]] + } + ); + this.transportConfigFormGroup.get('transportType').valueChanges.subscribe(() => { + this.deviceProfileTransportTypeChanged(); + }); + + this.alarmRulesFormGroup = this.fb.group( + { + alarms: [null] + } + ); + + this.provisionConfigFormGroup = this.fb.group( + { + provisionConfiguration: [{ + type: DeviceProvisionType.DISABLED + } as DeviceProvisionConfiguration, [Validators.required]] + } + ); + } + + private deviceProfileTransportTypeChanged() { + const deviceTransportType: DeviceTransportType = this.transportConfigFormGroup.get('transportType').value; + this.transportConfigFormGroup.patchValue( + {transportConfiguration: createDeviceProfileTransportConfiguration(deviceTransportType)}); + } + + ngAfterViewInit(): void { + } + + cancel(): void { + this.dialogRef.close(null); + } + + previousStep() { + this.addDeviceProfileStepper.previous(); + } + + nextStep() { + if (this.selectedIndex < 3) { + this.addDeviceProfileStepper.next(); + } else { + this.add(); + } + } + + selectedForm(): FormGroup { + switch (this.selectedIndex) { + case 0: + return this.deviceProfileDetailsFormGroup; + case 1: + return this.transportConfigFormGroup; + case 2: + return this.alarmRulesFormGroup; + case 3: + return this.provisionConfigFormGroup; + } + } + + add(): void { + if (this.allValid()) { + const deviceProvisionConfiguration: DeviceProvisionConfiguration = this.provisionConfigFormGroup.get('provisionConfiguration').value; + const provisionDeviceKey = deviceProvisionConfiguration.provisionDeviceKey; + delete deviceProvisionConfiguration.provisionDeviceKey; + const deviceProfile: DeviceProfile = { + name: this.deviceProfileDetailsFormGroup.get('name').value, + type: this.deviceProfileDetailsFormGroup.get('type').value, + transportType: this.transportConfigFormGroup.get('transportType').value, + provisionType: deviceProvisionConfiguration.type, + provisionDeviceKey, + description: this.deviceProfileDetailsFormGroup.get('description').value, + profileData: { + configuration: createDeviceProfileConfiguration(DeviceProfileType.DEFAULT), + transportConfiguration: this.transportConfigFormGroup.get('transportConfiguration').value, + alarms: this.alarmRulesFormGroup.get('alarms').value, + provisionConfiguration: deviceProvisionConfiguration + } + }; + if (this.deviceProfileDetailsFormGroup.get('defaultRuleChainId').value) { + deviceProfile.defaultRuleChainId = new RuleChainId(this.deviceProfileDetailsFormGroup.get('defaultRuleChainId').value); + } + this.deviceProfileService.saveDeviceProfile(deepTrim(deviceProfile)).subscribe( + (savedDeviceProfile) => { + this.dialogRef.close(savedDeviceProfile); + } + ); + } + } + + getFormLabel(index: number): string { + switch (index) { + case 0: + return 'device-profile.device-profile-details'; + case 1: + return 'device-profile.transport-configuration'; + case 2: + return 'device-profile.alarm-rules'; + case 3: + return 'device-profile.device-provisioning'; + } + } + + changeStep($event: StepperSelectionEvent): void { + this.selectedIndex = $event.selectedIndex; + if (this.selectedIndex === this.maxStepperIndex) { + this.showNext = false; + } else { + this.showNext = true; + } + } + + private get maxStepperIndex(): number { + return this.addDeviceProfileStepper?._steps?.length - 1; + } + + allValid(): boolean { + return !this.addDeviceProfileStepper.steps.find((item, index) => { + if (item.stepControl.invalid) { + item.interacted = true; + this.addDeviceProfileStepper.selectedIndex = index; + return true; + } else { + return false; + } + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html new file mode 100644 index 0000000000..493763756b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html @@ -0,0 +1,124 @@ + +
+ +

{{ (readonly ? 'device-profile.alarm-rule-condition' : 'device-profile.edit-alarm-rule-condition') | translate }}

+ + +
+ + +
+
+
+ + +
+ + device-profile.condition-type + + + {{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }} + + + + {{ 'device-profile.condition-type-required' | translate }} + + +
+ + + + + {{ 'device-profile.condition-duration-value-required' | translate }} + + + {{ 'device-profile.condition-duration-value-range' | translate }} + + + {{ 'device-profile.condition-duration-value-range' | translate }} + + + {{ 'device-profile.condition-duration-value-pattern' | translate }} + + + + + + + {{ timeUnitTranslations.get(timeUnit) | translate }} + + + + {{ 'device-profile.condition-duration-time-unit-required' | translate }} + + +
+
+ + + + + {{ 'device-profile.condition-repeating-value-required' | translate }} + + + {{ 'device-profile.condition-repeating-value-range' | translate }} + + + {{ 'device-profile.condition-repeating-value-range' | translate }} + + + {{ 'device-profile.condition-repeating-value-pattern' | translate }} + + +
+
+
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.scss new file mode 100644 index 0000000000..4af573d021 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.scss @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .row { + margin-top: 1em; + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts new file mode 100644 index 0000000000..4055560b38 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts @@ -0,0 +1,150 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { KeyFilter, keyFilterInfosToKeyFilters, keyFiltersToKeyFilterInfos } from '@shared/models/query/query.models'; +import { AlarmCondition, AlarmConditionType, AlarmConditionTypeTranslationMap } from '@shared/models/device.models'; +import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models'; + +export interface AlarmRuleConditionDialogData { + readonly: boolean; + condition: AlarmCondition; +} + +@Component({ + selector: 'tb-alarm-rule-condition-dialog', + templateUrl: './alarm-rule-condition-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: AlarmRuleConditionDialogComponent}], + styleUrls: ['/alarm-rule-condition-dialog.component.scss'] +}) +export class AlarmRuleConditionDialogComponent extends DialogComponent + implements OnInit, ErrorStateMatcher { + + timeUnits = Object.keys(TimeUnit); + timeUnitTranslations = timeUnitTranslationMap; + alarmConditionTypes = Object.keys(AlarmConditionType); + AlarmConditionType = AlarmConditionType; + alarmConditionTypeTranslation = AlarmConditionTypeTranslationMap; + + readonly = this.data.readonly; + condition = this.data.condition; + + conditionFormGroup: FormGroup; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AlarmRuleConditionDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + private utils: UtilsService, + public translate: TranslateService) { + super(store, router, dialogRef); + + this.conditionFormGroup = this.fb.group({ + keyFilters: [keyFiltersToKeyFilterInfos(this.condition?.condition), Validators.required], + spec: this.fb.group({ + type: [AlarmConditionType.SIMPLE, Validators.required], + unit: [{value: null, disable: true}, Validators.required], + value: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]], + count: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]] + }) + }); + this.conditionFormGroup.patchValue({spec: this.condition?.spec}); + this.conditionFormGroup.get('spec.type').valueChanges.subscribe((type) => { + this.updateValidators(type, true, true); + }); + if (this.readonly) { + this.conditionFormGroup.disable({emitEvent: false}); + } else { + this.updateValidators(this.condition?.spec?.type); + } + } + + ngOnInit(): void { + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + private updateValidators(type: AlarmConditionType, resetDuration = false, emitEvent = false) { + switch (type) { + case AlarmConditionType.DURATION: + this.conditionFormGroup.get('spec.value').enable(); + this.conditionFormGroup.get('spec.unit').enable(); + this.conditionFormGroup.get('spec.count').disable(); + if (resetDuration) { + this.conditionFormGroup.get('spec').patchValue({ + count: null + }); + } + break; + case AlarmConditionType.REPEATING: + this.conditionFormGroup.get('spec.count').enable(); + this.conditionFormGroup.get('spec.value').disable(); + this.conditionFormGroup.get('spec.unit').disable(); + if (resetDuration) { + this.conditionFormGroup.get('spec').patchValue({ + value: null, + unit: null + }); + } + break; + case AlarmConditionType.SIMPLE: + this.conditionFormGroup.get('spec.value').disable(); + this.conditionFormGroup.get('spec.unit').disable(); + this.conditionFormGroup.get('spec.count').disable(); + if (resetDuration) { + this.conditionFormGroup.get('spec').patchValue({ + value: null, + unit: null, + count: null + }); + } + break; + } + this.conditionFormGroup.get('spec.value').updateValueAndValidity({emitEvent}); + this.conditionFormGroup.get('spec.unit').updateValueAndValidity({emitEvent}); + this.conditionFormGroup.get('spec.count').updateValueAndValidity({emitEvent}); + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + this.condition = { + condition: keyFilterInfosToKeyFilters(this.conditionFormGroup.get('keyFilters').value), + spec: this.conditionFormGroup.get('spec').value + }; + this.dialogRef.close(this.condition); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html new file mode 100644 index 0000000000..7c09f3ba1c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html @@ -0,0 +1,36 @@ + +
+
+ + + + +
+ +
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.scss new file mode 100644 index 0000000000..733a80ada3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.scss @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + display: flex; + a.mat-button { + &:hover, &:focus { + border-bottom: none; + } + } + .tb-alarm-rule-condition { + cursor: pointer; + min-width: 0; + .tb-alarm-rule-condition-spec { + opacity: 0.7; + padding: 4px; + } + } +} + +:host ::ng-deep { + .tb-alarm-rule-condition { + .tb-filter-text { + max-height: 200px; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts new file mode 100644 index 0000000000..23e52cf8cd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts @@ -0,0 +1,182 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, Validators +} from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { deepClone, isUndefined } from '@core/utils'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { AlarmCondition, AlarmConditionSpec, AlarmConditionType } from '@shared/models/device.models'; +import { + AlarmRuleConditionDialogComponent, + AlarmRuleConditionDialogData +} from '@home/components/profile/alarm/alarm-rule-condition-dialog.component'; +import { TimeUnit } from '@shared/models/time/time.models'; + +@Component({ + selector: 'tb-alarm-rule-condition', + templateUrl: './alarm-rule-condition.component.html', + styleUrls: ['./alarm-rule-condition.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmRuleConditionComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AlarmRuleConditionComponent), + multi: true, + } + ] +}) +export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit, Validator { + + @Input() + disabled: boolean; + + alarmRuleConditionFormGroup: FormGroup; + + specText = ''; + + private modelValue: AlarmCondition; + + private propagateChange = (v: any) => { }; + + constructor(private dialog: MatDialog, + private fb: FormBuilder, + private translate: TranslateService, + private datePipe: DatePipe) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.alarmRuleConditionFormGroup = this.fb.group({ + condition: [null, Validators.required], + spec: [null, Validators.required] + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.alarmRuleConditionFormGroup.disable({emitEvent: false}); + } else { + this.alarmRuleConditionFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: AlarmCondition): void { + this.modelValue = value; + if (this.modelValue !== null && isUndefined(this.modelValue?.spec)) { + this.modelValue = Object.assign(this.modelValue, {spec: {type: AlarmConditionType.SIMPLE}}); + } + this.updateConditionInfo(); + } + + public conditionSet() { + return this.modelValue && this.modelValue.condition.length; + } + + public validate(c: FormControl) { + return this.conditionSet() ? null : { + alarmRuleCondition: { + valid: false, + }, + }; + } + + public openFilterDialog($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(AlarmRuleConditionDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + readonly: this.disabled, + condition: this.disabled ? this.modelValue : deepClone(this.modelValue) + } + }).afterClosed().subscribe((result) => { + if (result) { + this.modelValue = result; + this.updateModel(); + } + }); + } + + private updateConditionInfo() { + this.alarmRuleConditionFormGroup.patchValue( + { + condition: this.modelValue?.condition, + spec: this.modelValue?.spec + } + ); + this.updateSpecText(); + } + + private updateSpecText() { + this.specText = ''; + if (this.modelValue && this.modelValue.spec) { + const spec = this.modelValue.spec; + switch (spec.type) { + case AlarmConditionType.SIMPLE: + break; + case AlarmConditionType.DURATION: + let duringText = ''; + switch (spec.unit) { + case TimeUnit.SECONDS: + duringText = this.translate.instant('timewindow.seconds', {seconds: spec.value}); + break; + case TimeUnit.MINUTES: + duringText = this.translate.instant('timewindow.minutes', {minutes: spec.value}); + break; + case TimeUnit.HOURS: + duringText = this.translate.instant('timewindow.hours', {hours: spec.value}); + break; + case TimeUnit.DAYS: + duringText = this.translate.instant('timewindow.days', {days: spec.value}); + break; + } + this.specText = this.translate.instant('device-profile.condition-during', {during: duringText}); + break; + case AlarmConditionType.REPEATING: + this.specText = this.translate.instant('device-profile.condition-repeat-times', {count: spec.count}); + break; + } + } + } + + private updateModel() { + this.updateConditionInfo(); + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html new file mode 100644 index 0000000000..c7c77a7f3c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html @@ -0,0 +1,38 @@ + +
+ + + + +
+ + {{ alarmRuleFormGroup.get('alarmDetails').value ? ('device-profile.alarm-rule-details' | translate) + ': ' : ('device-profile.add-alarm-rule-details' | translate) }} + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss new file mode 100644 index 0000000000..f69af45170 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + min-width: 0; + .row { + margin-top: 1em; + } + .tb-alarm-rule-details { + padding: 4px; + cursor: pointer; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + &.title { + opacity: 0.7; + overflow: visible; + } + } +} + diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts new file mode 100644 index 0000000000..1e7806b455 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts @@ -0,0 +1,146 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { AlarmRule } from '@shared/models/device.models'; +import { MatDialog } from '@angular/material/dialog'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { isDefinedAndNotNull } from '@core/utils'; +import { + EditAlarmDetailsDialogComponent, + EditAlarmDetailsDialogData +} from '@home/components/profile/alarm/edit-alarm-details-dialog.component'; + +@Component({ + selector: 'tb-alarm-rule', + templateUrl: './alarm-rule.component.html', + styleUrls: ['./alarm-rule.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmRuleComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AlarmRuleComponent), + multi: true, + } + ] +}) +export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validator { + + @Input() + disabled: boolean; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + private modelValue: AlarmRule; + + alarmRuleFormGroup: FormGroup; + + expandAlarmDetails = false; + + private propagateChange = (v: any) => { }; + + constructor(private dialog: MatDialog, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.alarmRuleFormGroup = this.fb.group({ + condition: [null, [Validators.required]], + schedule: [null], + alarmDetails: [null] + }); + this.alarmRuleFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.alarmRuleFormGroup.disable({emitEvent: false}); + } else { + this.alarmRuleFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: AlarmRule): void { + this.modelValue = value; + this.alarmRuleFormGroup.reset(this.modelValue || undefined, {emitEvent: false}); + } + + public openEditDetailsDialog($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(EditAlarmDetailsDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + alarmDetails: this.alarmRuleFormGroup.get('alarmDetails').value, + readonly: this.disabled + } + }).afterClosed().subscribe((alarmDetails) => { + if (isDefinedAndNotNull(alarmDetails)) { + this.alarmRuleFormGroup.patchValue({alarmDetails}); + } + }); + } + + public validate(c: FormControl) { + return (!this.required && !this.modelValue || this.alarmRuleFormGroup.valid) ? null : { + alarmRule: { + valid: false, + }, + }; + } + + private updateModel() { + const value = this.alarmRuleFormGroup.value; + if (this.modelValue) { + this.modelValue = {...this.modelValue, ...value}; + this.propagateChange(this.modelValue); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.html new file mode 100644 index 0000000000..64a808fe98 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.html @@ -0,0 +1,53 @@ + +
+ +

{{ (readonly ? 'device-profile.schedule' : 'device-profile.edit-schedule') | translate }}

+ + +
+ + +
+
+
+ + +
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.ts new file mode 100644 index 0000000000..608ea65ab7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.ts @@ -0,0 +1,86 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { AlarmSchedule } from '@shared/models/device.models'; + +export interface AlarmScheduleDialogData { + readonly: boolean; + alarmSchedule: AlarmSchedule; +} + +@Component({ + selector: 'tb-alarm-schedule-dialog', + templateUrl: './alarm-schedule-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: AlarmScheduleDialogComponent}], + styleUrls: [] +}) +export class AlarmScheduleDialogComponent extends DialogComponent + implements OnInit, ErrorStateMatcher { + + readonly = this.data.readonly; + alarmSchedule = this.data.alarmSchedule; + + alarmScheduleFormGroup: FormGroup; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AlarmScheduleDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + private utils: UtilsService, + public translate: TranslateService) { + super(store, router, dialogRef); + + this.alarmScheduleFormGroup = this.fb.group({ + alarmSchedule: [this.alarmSchedule] + }); + if (this.readonly) { + this.alarmScheduleFormGroup.disable({emitEvent: false}); + } + } + + ngOnInit(): void { + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + this.alarmSchedule = this.alarmScheduleFormGroup.get('alarmSchedule').value; + this.dialogRef.close(this.alarmSchedule); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.html new file mode 100644 index 0000000000..c8991ccc65 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.html @@ -0,0 +1,30 @@ + +
+ {{('device-profile.schedule' | translate) + ': '}} + + + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.scss new file mode 100644 index 0000000000..ef5bb3ebad --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.scss @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + display: flex; + a.mat-button { + &:hover, &:focus { + border-bottom: none; + } + } + .tb-alarm-rule-schedule { + padding: 4px; + cursor: pointer; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + &.title { + opacity: 0.7; + overflow: visible; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.ts new file mode 100644 index 0000000000..0b9ce26163 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.ts @@ -0,0 +1,137 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { + AlarmSchedule, + AlarmScheduleType, + AlarmScheduleTypeTranslationMap, dayOfWeekTranslations, + getAlarmScheduleRangeText, utcTimestampToTimeOfDay +} from '@shared/models/device.models'; +import { MatDialog } from '@angular/material/dialog'; +import { + AlarmScheduleDialogComponent, + AlarmScheduleDialogData +} from '@home/components/profile/alarm/alarm-schedule-dialog.component'; +import { deepClone, isDefinedAndNotNull } from '@core/utils'; +import { TranslateService } from '@ngx-translate/core'; + +@Component({ + selector: 'tb-alarm-schedule-info', + templateUrl: './alarm-schedule-info.component.html', + styleUrls: ['./alarm-schedule-info.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmScheduleInfoComponent), + multi: true + }] +}) +export class AlarmScheduleInfoComponent implements ControlValueAccessor, OnInit { + + @Input() + disabled: boolean; + + private modelValue: AlarmSchedule; + + scheduleText = ''; + + private propagateChange = (v: any) => { }; + + constructor(private dialog: MatDialog, + private translate: TranslateService, + private cd: ChangeDetectorRef) { + } + + ngOnInit(): void { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: AlarmSchedule): void { + this.modelValue = value; + this.updateScheduleText(); + } + + private updateScheduleText() { + let schedule = this.modelValue; + if (!isDefinedAndNotNull(schedule)) { + schedule = { + type: AlarmScheduleType.ANY_TIME + }; + } + this.scheduleText = ''; + switch (schedule.type) { + case AlarmScheduleType.ANY_TIME: + this.scheduleText = this.translate.instant('device-profile.schedule-any-time'); + break; + case AlarmScheduleType.SPECIFIC_TIME: + for (const day of schedule.daysOfWeek) { + if (this.scheduleText.length) { + this.scheduleText += ', '; + } + this.scheduleText += this.translate.instant(dayOfWeekTranslations[day - 1]); + } + this.scheduleText += ' ' + getAlarmScheduleRangeText(utcTimestampToTimeOfDay(schedule.startsOn), + utcTimestampToTimeOfDay(schedule.endsOn)) + ''; + break; + case AlarmScheduleType.CUSTOM: + for (const item of schedule.items) { + if (item.enabled) { + if (this.scheduleText.length) { + this.scheduleText += ', '; + } + this.scheduleText += this.translate.instant(dayOfWeekTranslations[item.dayOfWeek - 1]); + this.scheduleText += ' ' + getAlarmScheduleRangeText(utcTimestampToTimeOfDay(item.startsOn), + utcTimestampToTimeOfDay(item.endsOn)) + ''; + } + } + break; + } + } + + public openScheduleDialog($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(AlarmScheduleDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + readonly: this.disabled, + alarmSchedule: this.disabled ? this.modelValue : deepClone(this.modelValue) + } + }).afterClosed().subscribe((result) => { + if (result) { + this.modelValue = result; + this.propagateChange(this.modelValue); + this.updateScheduleText(); + this.cd.detectChanges(); + } + }); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.html new file mode 100644 index 0000000000..ea4ad4097a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.html @@ -0,0 +1,108 @@ + +
+ + + + + {{ alarmScheduleTypeTranslate.get(alarmScheduleType) | translate }} + + + + {{ 'device-profile.schedule-type-required' | translate }} + + +
+ + +
+
device-profile.schedule-days
+
+
+ + {{ dayOfWeekTranslationsArray[day] | translate }} + +
+
+ + {{ dayOfWeekTranslationsArray[day] | translate }} + +
+
+ +
device-profile.schedule-time
+
+
+ + device-profile.schedule-time-from + + + + + + device-profile.schedule-time-to + + + + +
+
+
+
+
+
+
+
+
device-profile.schedule-days
+ +
+
+ + {{ dayOfWeekTranslationsArray[day] | translate }} + +
+ + device-profile.schedule-time-from + + + + + + device-profile.schedule-time-to + + + + +
+
+
+
+
+ + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.scss new file mode 100644 index 0000000000..1746a5fbf5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.scss @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +::ng-deep { + .nowrap { + white-space: nowrap; + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts new file mode 100644 index 0000000000..65009dbd0b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts @@ -0,0 +1,285 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { + AlarmSchedule, + AlarmScheduleType, + AlarmScheduleTypeTranslationMap, + dayOfWeekTranslations, getAlarmScheduleRangeText, timeOfDayToUTCTimestamp, utcTimestampToTimeOfDay +} from '@shared/models/device.models'; +import { isDefined, isDefinedAndNotNull } from '@core/utils'; +import * as _moment from 'moment-timezone'; +import { MatCheckboxChange } from '@angular/material/checkbox'; + +@Component({ + selector: 'tb-alarm-schedule', + templateUrl: './alarm-schedule.component.html', + styleUrls: ['./alarm-schedule.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmScheduleComponent), + multi: true + }, { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AlarmScheduleComponent), + multi: true + }] +}) +export class AlarmScheduleComponent implements ControlValueAccessor, Validator, OnInit { + @Input() + disabled: boolean; + + alarmScheduleForm: FormGroup; + + defaultTimezone = _moment.tz.guess(); + + alarmScheduleTypes = Object.keys(AlarmScheduleType); + alarmScheduleType = AlarmScheduleType; + alarmScheduleTypeTranslate = AlarmScheduleTypeTranslationMap; + + dayOfWeekTranslationsArray = dayOfWeekTranslations; + + allDays = Array(7).fill(0).map((x, i) => i); + + firstRowDays = Array(4).fill(0).map((x, i) => i); + secondRowDays = Array(3).fill(0).map((x, i) => i + 4); + + private modelValue: AlarmSchedule; + + private defaultItems = Array.from({length: 7}, (value, i) => ({ + enabled: true, + dayOfWeek: i + 1 + })); + + private propagateChange = (v: any) => { }; + + constructor(private fb: FormBuilder) { + } + + ngOnInit(): void { + this.alarmScheduleForm = this.fb.group({ + type: [AlarmScheduleType.ANY_TIME, Validators.required], + timezone: [null, Validators.required], + daysOfWeek: this.fb.array(new Array(7).fill(false), this.validateDayOfWeeks), + startsOn: [0, Validators.required], + endsOn: [0, Validators.required], + items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i)), this.validateItems) + }); + this.alarmScheduleForm.get('type').valueChanges.subscribe((type) => { + this.alarmScheduleForm.reset({type, items: this.defaultItems, timezone: this.defaultTimezone}, {emitEvent: false}); + this.updateValidators(type, true); + this.alarmScheduleForm.updateValueAndValidity(); + }); + this.alarmScheduleForm.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + validateDayOfWeeks(control: AbstractControl): ValidationErrors | null { + const dayOfWeeks: boolean[] = control.value; + if (!dayOfWeeks || !dayOfWeeks.length || !dayOfWeeks.find(v => v === true)) { + return { + dayOfWeeks: true + }; + } + return null; + } + + validateItems(control: AbstractControl): ValidationErrors | null { + const items: any[] = control.value; + if (!items || !items.length || !items.find(v => v.enabled === true)) { + return { + dayOfWeeks: true + }; + } + return null; + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.alarmScheduleForm.disable({emitEvent: false}); + } else { + this.alarmScheduleForm.enable({emitEvent: false}); + } + } + + writeValue(value: AlarmSchedule): void { + this.modelValue = value; + if (!isDefinedAndNotNull(this.modelValue)) { + this.modelValue = { + type: AlarmScheduleType.ANY_TIME + }; + } + switch (this.modelValue.type) { + case AlarmScheduleType.SPECIFIC_TIME: + let daysOfWeek = new Array(7).fill(false); + if (isDefined(this.modelValue.daysOfWeek)) { + daysOfWeek = daysOfWeek.map((item, index) => this.modelValue.daysOfWeek.indexOf(index + 1) > -1); + } + this.alarmScheduleForm.patchValue({ + type: this.modelValue.type, + timezone: this.modelValue.timezone, + daysOfWeek, + startsOn: utcTimestampToTimeOfDay(this.modelValue.startsOn), + endsOn: utcTimestampToTimeOfDay(this.modelValue.endsOn) + }, {emitEvent: false}); + break; + case AlarmScheduleType.CUSTOM: + if (this.modelValue.items) { + const alarmDays = []; + this.modelValue.items + .sort((a, b) => a.dayOfWeek - b.dayOfWeek) + .forEach((item, index) => { + this.disabledSelectedTime(item.enabled, index); + alarmDays.push({ + enabled: item.enabled, + startsOn: utcTimestampToTimeOfDay(item.startsOn), + endsOn: utcTimestampToTimeOfDay(item.endsOn) + }); + }); + this.alarmScheduleForm.patchValue({ + type: this.modelValue.type, + timezone: this.modelValue.timezone, + items: alarmDays + }, {emitEvent: false}); + } + break; + default: + this.alarmScheduleForm.patchValue(this.modelValue || undefined, {emitEvent: false}); + } + this.updateValidators(this.modelValue.type); + } + + validate(control: FormGroup): ValidationErrors | null { + return this.alarmScheduleForm.valid ? null : { + alarmScheduler: { + valid: false + } + }; + } + + weeklyRepeatControl(index: number): FormControl { + return (this.alarmScheduleForm.get('daysOfWeek') as FormArray).at(index) as FormControl; + } + + private updateValidators(type: AlarmScheduleType, changedType = false){ + switch (type){ + case AlarmScheduleType.ANY_TIME: + this.alarmScheduleForm.get('timezone').disable({emitEvent: false}); + this.alarmScheduleForm.get('daysOfWeek').disable({emitEvent: false}); + this.alarmScheduleForm.get('startsOn').disable({emitEvent: false}); + this.alarmScheduleForm.get('endsOn').disable({emitEvent: false}); + this.alarmScheduleForm.get('items').disable({emitEvent: false}); + break; + case AlarmScheduleType.SPECIFIC_TIME: + this.alarmScheduleForm.get('timezone').enable({emitEvent: false}); + this.alarmScheduleForm.get('daysOfWeek').enable({emitEvent: false}); + this.alarmScheduleForm.get('startsOn').enable({emitEvent: false}); + this.alarmScheduleForm.get('endsOn').enable({emitEvent: false}); + this.alarmScheduleForm.get('items').disable({emitEvent: false}); + break; + case AlarmScheduleType.CUSTOM: + this.alarmScheduleForm.get('timezone').enable({emitEvent: false}); + this.alarmScheduleForm.get('daysOfWeek').disable({emitEvent: false}); + this.alarmScheduleForm.get('startsOn').disable({emitEvent: false}); + this.alarmScheduleForm.get('endsOn').disable({emitEvent: false}); + if (changedType) { + this.alarmScheduleForm.get('items').enable({emitEvent: false}); + } + break; + } + } + + private updateModel() { + const value = this.alarmScheduleForm.value; + if (this.modelValue) { + if (isDefined(value.daysOfWeek)) { + value.daysOfWeek = value.daysOfWeek + .map((day: boolean, index: number) => day ? index + 1 : null) + .filter(day => !!day); + } + if (isDefined(value.startsOn) && value.startsOn !== 0) { + value.startsOn = timeOfDayToUTCTimestamp(value.startsOn); + } + if (isDefined(value.endsOn) && value.endsOn !== 0) { + value.endsOn = timeOfDayToUTCTimestamp(value.endsOn); + } + if (isDefined(value.items)){ + value.items = this.alarmScheduleForm.getRawValue().items; + value.items = value.items.map((item) => { + return { ...item, startsOn: timeOfDayToUTCTimestamp(item.startsOn), endsOn: timeOfDayToUTCTimestamp(item.endsOn)}; + }); + } + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + + private defaultItemsScheduler(index): FormGroup { + return this.fb.group({ + enabled: [true], + dayOfWeek: [index + 1], + startsOn: [0, Validators.required], + endsOn: [0, Validators.required] + }); + } + + changeCustomScheduler($event: MatCheckboxChange, index: number) { + const value = $event.checked; + this.disabledSelectedTime(value, index, true); + } + + private disabledSelectedTime(enable: boolean, index: number, emitEvent = false) { + if (enable) { + this.itemsSchedulerForm.at(index).get('startsOn').enable({emitEvent: false}); + this.itemsSchedulerForm.at(index).get('endsOn').enable({emitEvent}); + } else { + this.itemsSchedulerForm.at(index).get('startsOn').disable({emitEvent: false}); + this.itemsSchedulerForm.at(index).get('endsOn').disable({emitEvent}); + } + } + + getSchedulerRangeText(control: FormGroup | AbstractControl): string { + return getAlarmScheduleRangeText(control.get('startsOn').value, control.get('endsOn').value); + } + + get itemsSchedulerForm(): FormArray { + return this.alarmScheduleForm.get('items') as FormArray; + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html new file mode 100644 index 0000000000..5c992d9e04 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html @@ -0,0 +1,64 @@ + +
+
+
+ + alarm.severity + + + {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} + + + + {{ 'device-profile.alarm-severity-required' | translate }} + + + + + +
+ +
+
+ device-profile.add-create-alarm-rule-prompt +
+
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.scss new file mode 100644 index 0000000000..91335f53a4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.scss @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:host { + .create-alarm-rule { + border: 2px groove rgba(0, 0, 0, .45); + border-radius: 4px; + padding: 8px; + min-width: 0; + } +} + +:host ::ng-deep { + .mat-form-field.severity { + .mat-form-field-infix { + width: 160px; + } + } + .button-icon{ + font-size: 20px; + width: 20px; + height: 20px; + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.ts new file mode 100644 index 0000000000..62fcfa733d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.ts @@ -0,0 +1,193 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { AlarmRule, alarmRuleValidator } from '@shared/models/device.models'; +import { MatDialog } from '@angular/material/dialog'; +import { Subscription } from 'rxjs'; +import { AlarmSeverity, alarmSeverityTranslations } from '../../../../../shared/models/alarm.models'; + +@Component({ + selector: 'tb-create-alarm-rules', + templateUrl: './create-alarm-rules.component.html', + styleUrls: ['./create-alarm-rules.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CreateAlarmRulesComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CreateAlarmRulesComponent), + multi: true, + } + ] +}) +export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit, Validator { + + alarmSeverities = Object.keys(AlarmSeverity); + alarmSeverityEnum = AlarmSeverity; + + alarmSeverityTranslationMap = alarmSeverityTranslations; + + @Input() + disabled: boolean; + + createAlarmRulesFormGroup: FormGroup; + + private usedSeverities: AlarmSeverity[] = []; + + private valueChangeSubscription: Subscription = null; + + private propagateChange = (v: any) => { }; + + constructor(private dialog: MatDialog, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.createAlarmRulesFormGroup = this.fb.group({ + createAlarmRules: this.fb.array([]) + }); + } + + createAlarmRulesFormArray(): FormArray { + return this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.createAlarmRulesFormGroup.disable({emitEvent: false}); + } else { + this.createAlarmRulesFormGroup.enable({emitEvent: false}); + } + } + + writeValue(createAlarmRules: {[severity: string]: AlarmRule}): void { + if (this.valueChangeSubscription) { + this.valueChangeSubscription.unsubscribe(); + } + const createAlarmRulesControls: Array = []; + if (createAlarmRules) { + Object.keys(createAlarmRules).forEach((severity) => { + const createAlarmRule = createAlarmRules[severity]; + if (severity === 'empty') { + severity = null; + } + createAlarmRulesControls.push(this.fb.group({ + severity: [severity, Validators.required], + alarmRule: [createAlarmRule, Validators.required] + })); + }); + } + this.createAlarmRulesFormGroup.setControl('createAlarmRules', this.fb.array(createAlarmRulesControls)); + if (this.disabled) { + this.createAlarmRulesFormGroup.disable({emitEvent: false}); + } else { + this.createAlarmRulesFormGroup.enable({emitEvent: false}); + } + this.valueChangeSubscription = this.createAlarmRulesFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + this.updateUsedSeverities(); + if (!this.disabled && !this.createAlarmRulesFormGroup.valid) { + this.updateModel(); + } + } + + public removeCreateAlarmRule(index: number) { + (this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray).removeAt(index); + } + + public addCreateAlarmRule() { + const createAlarmRule: AlarmRule = { + condition: { + condition: [] + } + }; + const createAlarmRulesArray = this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray; + createAlarmRulesArray.push(this.fb.group({ + severity: [this.getFirstUnusedSeverity(), Validators.required], + alarmRule: [createAlarmRule, alarmRuleValidator] + })); + this.createAlarmRulesFormGroup.updateValueAndValidity(); + if (!this.createAlarmRulesFormGroup.valid) { + this.updateModel(); + } + } + + private getFirstUnusedSeverity(): AlarmSeverity { + for (const severityKey of Object.keys(AlarmSeverity)) { + const severity = AlarmSeverity[severityKey]; + if (this.usedSeverities.indexOf(severity) === -1) { + return severity; + } + } + return null; + } + + public validate(c: FormControl) { + return (this.createAlarmRulesFormGroup.valid) ? null : { + createAlarmRules: { + valid: false, + }, + }; + } + + public isDisabledSeverity(severity: AlarmSeverity, index: number): boolean { + const usedIndex = this.usedSeverities.indexOf(severity); + return usedIndex > -1 && usedIndex !== index; + } + + private updateUsedSeverities() { + this.usedSeverities = []; + const value: {severity: string, alarmRule: AlarmRule}[] = this.createAlarmRulesFormGroup.get('createAlarmRules').value; + value.forEach((rule, index) => { + this.usedSeverities[index] = AlarmSeverity[rule.severity]; + }); + } + + private updateModel() { + const value: {severity: string, alarmRule: AlarmRule}[] = this.createAlarmRulesFormGroup.get('createAlarmRules').value; + const createAlarmRules: {[severity: string]: AlarmRule} = {}; + value.forEach(v => { + createAlarmRules[v.severity] = v.alarmRule; + }); + this.updateUsedSeverities(); + this.propagateChange(createAlarmRules); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html new file mode 100644 index 0000000000..cb1f7afb06 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html @@ -0,0 +1,117 @@ + + + +
+ +
+ {{ alarmFormGroup.get('alarmType').value }} +
+
+ + +
+
+
+ + + {{'device-profile.alarm-type' | translate}} + + + {{ 'device-profile.alarm-type-required' | translate }} + + + +
+ + + +
+
device-profile.advanced-settings
+
+
+
+ + {{ 'device-profile.propagate-alarm' | translate }} + +
+ + device-profile.alarm-rule-relation-types-list + + + {{key}} + close + + + + + +
+
+
+
device-profile.create-alarm-rules
+ + +
device-profile.clear-alarm-rule
+
+
+ + +
+ +
+
+ device-profile.no-clear-alarm-rule +
+
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.scss new file mode 100644 index 0000000000..ab28ba6829 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.scss @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:host { + display: block; + .clear-alarm-rule { + border: 2px groove rgba(0, 0, 0, .45); + border-radius: 4px; + padding: 8px; + } + .mat-expansion-panel { + box-shadow: none; + &.device-profile-alarm { + border: 1px groove rgba(0, 0, 0, .25); + .mat-expansion-panel-header { + padding: 0 24px 0 8px; + &.mat-expanded { + height: 48px; + } + } + } + &.advanced-settings { + border: none; + padding: 0; + .mat-expansion-panel-header { + padding: 0 8px; + } + } + } +} + +:host ::ng-deep { + .mat-expansion-panel { + &.device-profile-alarm { + .mat-expansion-panel-body { + padding: 0 8px 8px; + } + } + &.advanced-settings { + .mat-expansion-panel-body { + padding: 0; + } + } + .button-icon{ + font-size: 20px; + width: 20px; + height: 20px; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts new file mode 100644 index 0000000000..cf4a55e6d8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts @@ -0,0 +1,181 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { AlarmRule, DeviceProfileAlarm, deviceProfileAlarmValidator } from '@shared/models/device.models'; +import { MatDialog } from '@angular/material/dialog'; +import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; +import { MatChipInputEvent } from '@angular/material/chips'; + +@Component({ + selector: 'tb-device-profile-alarm', + templateUrl: './device-profile-alarm.component.html', + styleUrls: ['./device-profile-alarm.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceProfileAlarmComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DeviceProfileAlarmComponent), + multi: true, + } + ] +}) +export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit, Validator { + + @Input() + disabled: boolean; + + @Output() + removeAlarm = new EventEmitter(); + + separatorKeysCodes = [ENTER, COMMA, SEMICOLON]; + + @Input() + expanded = false; + + private modelValue: DeviceProfileAlarm; + + alarmFormGroup: FormGroup; + + private propagateChange = null; + private propagateChangePending = false; + + constructor(private dialog: MatDialog, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + if (this.propagateChangePending) { + this.propagateChangePending = false; + setTimeout(() => { + this.propagateChange(this.modelValue); + }, 0); + } + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.alarmFormGroup = this.fb.group({ + id: [null, Validators.required], + alarmType: [null, Validators.required], + createRules: [null], + clearRule: [null], + propagate: [null], + propagateRelationTypes: [null] + }, { validators: deviceProfileAlarmValidator }); + this.alarmFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.alarmFormGroup.disable({emitEvent: false}); + } else { + this.alarmFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DeviceProfileAlarm): void { + this.propagateChangePending = false; + this.modelValue = value; + if (!this.modelValue.alarmType) { + this.expanded = true; + } + this.alarmFormGroup.reset(this.modelValue || undefined, {emitEvent: false}); + if (!this.disabled && !this.alarmFormGroup.valid) { + this.updateModel(); + } + } + + public addClearAlarmRule() { + const clearAlarmRule: AlarmRule = { + condition: { + condition: [] + } + }; + this.alarmFormGroup.patchValue({clearRule: clearAlarmRule}); + } + + public removeClearAlarmRule() { + this.alarmFormGroup.patchValue({clearRule: null}); + } + + public validate(c: FormControl) { + return (this.alarmFormGroup.valid) ? null : { + alarm: { + valid: false, + }, + }; + } + + removeRelationType(key: string): void { + const keys: string[] = this.alarmFormGroup.get('propagateRelationTypes').value; + const index = keys.indexOf(key); + if (index >= 0) { + keys.splice(index, 1); + this.alarmFormGroup.get('propagateRelationTypes').setValue(keys, {emitEvent: true}); + } + } + + addRelationType(event: MatChipInputEvent): void { + const input = event.input; + let value = event.value; + if ((value || '').trim()) { + value = value.trim(); + let keys: string[] = this.alarmFormGroup.get('propagateRelationTypes').value; + if (!keys || keys.indexOf(value) === -1) { + if (!keys) { + keys = []; + } + keys.push(value); + this.alarmFormGroup.get('propagateRelationTypes').setValue(keys, {emitEvent: true}); + } + } + if (input) { + input.value = ''; + } + } + + + private updateModel() { + const value = this.alarmFormGroup.value; + this.modelValue = {...this.modelValue, ...value}; + if (this.propagateChange) { + this.propagateChange(this.modelValue); + } else { + this.propagateChangePending = true; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html new file mode 100644 index 0000000000..e1b446c65a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html @@ -0,0 +1,43 @@ + +
+
+
+ + +
+
+
+ device-profile.no-alarm-rules +
+
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.scss new file mode 100644 index 0000000000..18883f943b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.scss @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../../scss/constants'; + +:host { + .tb-device-profile-alarms { + overflow-y: auto; + &.mat-padding { + padding: 8px; + @media #{$mat-gt-sm} { + padding: 16px; + } + } + } + + .tb-prompt{ + margin: 30px 0; + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.ts new file mode 100644 index 0000000000..312dc5346d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.ts @@ -0,0 +1,171 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { DeviceProfileAlarm, deviceProfileAlarmValidator } from '@shared/models/device.models'; +import { guid } from '@core/utils'; +import { Subscription } from 'rxjs'; +import { MatDialog } from '@angular/material/dialog'; + +@Component({ + selector: 'tb-device-profile-alarms', + templateUrl: './device-profile-alarms.component.html', + styleUrls: ['./device-profile-alarms.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceProfileAlarmsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DeviceProfileAlarmsComponent), + multi: true, + } + ] +}) +export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnInit, Validator { + + deviceProfileAlarmsFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private valueChangeSubscription: Subscription = null; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder, + private dialog: MatDialog) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.deviceProfileAlarmsFormGroup = this.fb.group({ + alarms: this.fb.array([]) + }); + } + + alarmsFormArray(): FormArray { + return this.deviceProfileAlarmsFormGroup.get('alarms') as FormArray; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.deviceProfileAlarmsFormGroup.disable({emitEvent: false}); + } else { + this.deviceProfileAlarmsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(alarms: Array | null): void { + if (this.valueChangeSubscription) { + this.valueChangeSubscription.unsubscribe(); + } + const alarmsControls: Array = []; + if (alarms) { + alarms.forEach((alarm) => { + alarmsControls.push(this.fb.control(alarm, [Validators.required])); + }); + } + this.deviceProfileAlarmsFormGroup.setControl('alarms', this.fb.array(alarmsControls)); + if (this.disabled) { + this.deviceProfileAlarmsFormGroup.disable({emitEvent: false}); + } else { + this.deviceProfileAlarmsFormGroup.enable({emitEvent: false}); + } + this.valueChangeSubscription = this.deviceProfileAlarmsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + public trackByAlarm(index: number, alarmControl: AbstractControl): string { + if (alarmControl) { + return alarmControl.value.id; + } else { + return null; + } + } + + public removeAlarm(index: number) { + (this.deviceProfileAlarmsFormGroup.get('alarms') as FormArray).removeAt(index); + } + + public addAlarm() { + const alarm: DeviceProfileAlarm = { + id: guid(), + alarmType: '', + createRules: { + CRITICAL: { + condition: { + condition: [] + } + } + } + }; + const alarmsArray = this.deviceProfileAlarmsFormGroup.get('alarms') as FormArray; + alarmsArray.push(this.fb.control(alarm, [deviceProfileAlarmValidator])); + this.deviceProfileAlarmsFormGroup.updateValueAndValidity(); + if (!this.deviceProfileAlarmsFormGroup.valid) { + this.updateModel(); + } + } + + public validate(c: FormControl) { + return (this.deviceProfileAlarmsFormGroup.valid) ? null : { + alarms: { + valid: false, + }, + }; + } + + private updateModel() { + const alarms: Array = this.deviceProfileAlarmsFormGroup.get('alarms').value; + this.propagateChange(alarms); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.html new file mode 100644 index 0000000000..9f4a4addf7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.html @@ -0,0 +1,53 @@ + +
+ +

{{ 'device-profile.alarm-rule-details' | translate }}

+ + +
+ + +
+
+
+ + device-profile.alarm-details + + +
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.ts new file mode 100644 index 0000000000..a6017a6d70 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.ts @@ -0,0 +1,84 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; + +export interface EditAlarmDetailsDialogData { + alarmDetails: string; + readonly: boolean; +} + +@Component({ + selector: 'tb-edit-alarm-details-dialog', + templateUrl: './edit-alarm-details-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: EditAlarmDetailsDialogComponent}], + styleUrls: [] +}) +export class EditAlarmDetailsDialogComponent extends DialogComponent + implements OnInit, ErrorStateMatcher { + + alarmDetails = this.data.alarmDetails; + + editDetailsFormGroup: FormGroup; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: EditAlarmDetailsDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + private utils: UtilsService, + public translate: TranslateService) { + super(store, router, dialogRef); + + this.editDetailsFormGroup = this.fb.group({ + alarmDetails: [this.alarmDetails] + }); + if (this.data.readonly) { + this.editDetailsFormGroup.disable(); + } + } + + ngOnInit(): void { + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + this.alarmDetails = this.editDetailsFormGroup.get('alarmDetails').value; + this.dialogRef.close(this.alarmDetails); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html new file mode 100644 index 0000000000..9941b1db94 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html @@ -0,0 +1,69 @@ + + + + + + + + + + +
+
+ device-profile.no-device-profiles-found +
+ + + {{ translate.get('device-profile.no-device-profiles-matching', + {entity: truncate.transform(searchText, true, 6, '...')}) | async }} + + + device-profile.create-new-device-profile + + +
+
+
+ + {{ 'device-profile.device-profile-required' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts new file mode 100644 index 0000000000..d7ed08ae9c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts @@ -0,0 +1,388 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + Component, + ElementRef, + EventEmitter, + forwardRef, + Input, + NgZone, OnChanges, + OnInit, + Output, SimpleChanges, + ViewChild +} from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Observable } from 'rxjs'; +import { PageLink } from '@shared/models/page/page-link'; +import { Direction } from '@shared/models/page/sort-order'; +import { map, mergeMap, share, tap } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { entityIdEquals } from '@shared/models/id/entity-id'; +import { TruncatePipe } from '@shared//pipe/truncate.pipe'; +import { ENTER } from '@angular/cdk/keycodes'; +import { MatDialog } from '@angular/material/dialog'; +import { DeviceProfileId } from '@shared/models/id/device-profile-id'; +import { DeviceProfile, DeviceProfileInfo, DeviceProfileType, DeviceTransportType } from '@shared/models/device.models'; +import { DeviceProfileService } from '@core/http/device-profile.service'; +import { DeviceProfileDialogComponent, DeviceProfileDialogData } from './device-profile-dialog.component'; +import { MatAutocomplete } from '@angular/material/autocomplete'; +import { AddDeviceProfileDialogComponent, AddDeviceProfileDialogData } from './add-device-profile-dialog.component'; + +@Component({ + selector: 'tb-device-profile-autocomplete', + templateUrl: './device-profile-autocomplete.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceProfileAutocompleteComponent), + multi: true + }] +}) +export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, OnInit, OnChanges { + + selectDeviceProfileFormGroup: FormGroup; + + modelValue: DeviceProfileId | null; + + @Input() + selectDefaultProfile = false; + + @Input() + selectFirstProfile = false; + + @Input() + displayAllOnEmpty = false; + + @Input() + editProfileEnabled = true; + + @Input() + addNewProfile = true; + + @Input() + transportType: DeviceTransportType = null; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @Output() + deviceProfileUpdated = new EventEmitter(); + + @Output() + deviceProfileChanged = new EventEmitter(); + + @ViewChild('deviceProfileInput', {static: true}) deviceProfileInput: ElementRef; + + @ViewChild('deviceProfileAutocomplete', {static: true}) deviceProfileAutocomplete: MatAutocomplete; + + filteredDeviceProfiles: Observable>; + + searchText = ''; + + private dirty = false; + + private ignoreClosedPanel = false; + + private allDeviceProfile: DeviceProfileInfo = { + name: this.translate.instant('device-profile.all-device-profiles'), + type: DeviceProfileType.DEFAULT, + transportType: DeviceTransportType.DEFAULT, + id: null + }; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + public translate: TranslateService, + public truncate: TruncatePipe, + private deviceProfileService: DeviceProfileService, + private fb: FormBuilder, + private zone: NgZone, + private dialog: MatDialog) { + this.selectDeviceProfileFormGroup = this.fb.group({ + deviceProfile: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.filteredDeviceProfiles = this.selectDeviceProfileFormGroup.get('deviceProfile').valueChanges + .pipe( + tap((value: DeviceProfileInfo | string) => { + let modelValue: DeviceProfileInfo | null; + if (typeof value === 'string' || !value) { + modelValue = null; + } else { + modelValue = value; + } + if (!this.displayAllOnEmpty || modelValue) { + this.updateView(modelValue); + } + }), + map(value => { + if (value) { + if (typeof value === 'string') { + return value; + } else { + if (this.displayAllOnEmpty && value === this.allDeviceProfile) { + return ''; + } else { + return value.name; + } + } + } else { + return ''; + } + }), + mergeMap(name => this.fetchDeviceProfiles(name) ), + share() + ); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'transportType') { + this.writeValue(null); + } + } + } + } + + selectDefaultDeviceProfileIfNeeded(): void { + if (this.selectDefaultProfile && !this.modelValue) { + this.deviceProfileService.getDefaultDeviceProfileInfo().subscribe( + (profile) => { + if (profile && (!this.transportType || (profile.transportType === this.transportType))) { + this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(profile, {emitEvent: false}); + this.updateView(profile); + } else { + this.selectFirstDeviceProfileIfNeeded(); + } + } + ); + } else { + this.selectFirstDeviceProfileIfNeeded(); + } + } + + selectFirstDeviceProfileIfNeeded(): void { + if (this.selectFirstProfile && !this.modelValue) { + const pageLink = new PageLink(1, 0, null, { + property: 'createdTime', + direction: Direction.DESC + }); + this.deviceProfileService.getDeviceProfileInfos(pageLink, this.transportType, {ignoreLoading: true}).subscribe( + (pageData => { + const data = pageData.data; + if (data.length) { + this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(data[0], {emitEvent: false}); + this.updateView(data[0]); + } + }) + ); + } + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.selectDeviceProfileFormGroup.disable(); + } else { + this.selectDeviceProfileFormGroup.enable(); + } + } + + writeValue(value: DeviceProfileId | null): void { + this.searchText = ''; + if (value != null) { + this.deviceProfileService.getDeviceProfileInfo(value.id).subscribe( + (profile) => { + this.modelValue = new DeviceProfileId(profile.id.id); + this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(profile, {emitEvent: false}); + this.deviceProfileChanged.emit(profile); + } + ); + } else if (this.displayAllOnEmpty) { + this.modelValue = null; + this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(this.allDeviceProfile, {emitEvent: false}); + } else { + this.modelValue = null; + this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(null, {emitEvent: false}); + this.selectDefaultDeviceProfileIfNeeded(); + } + this.dirty = true; + } + + onFocus() { + if (this.dirty) { + this.selectDeviceProfileFormGroup.get('deviceProfile').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + onPanelClosed() { + if (this.ignoreClosedPanel) { + this.ignoreClosedPanel = false; + } else { + if (this.displayAllOnEmpty && !this.selectDeviceProfileFormGroup.get('deviceProfile').value) { + this.zone.run(() => { + this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(this.allDeviceProfile, {emitEvent: true}); + }, 0); + } + } + } + + updateView(deviceProfile: DeviceProfileInfo | null) { + const idValue = deviceProfile && deviceProfile.id ? new DeviceProfileId(deviceProfile.id.id) : null; + if (!entityIdEquals(this.modelValue, idValue)) { + this.modelValue = idValue; + this.propagateChange(this.modelValue); + this.deviceProfileChanged.emit(deviceProfile); + } + } + + displayDeviceProfileFn(profile?: DeviceProfileInfo): string | undefined { + return profile ? profile.name : undefined; + } + + fetchDeviceProfiles(searchText?: string): Observable> { + this.searchText = searchText; + const pageLink = new PageLink(10, 0, searchText, { + property: 'name', + direction: Direction.ASC + }); + return this.deviceProfileService.getDeviceProfileInfos(pageLink, this.transportType, {ignoreLoading: true}).pipe( + map(pageData => { + let data = pageData.data; + if (this.displayAllOnEmpty) { + data = [this.allDeviceProfile, ...data]; + } + return data; + }) + ); + } + + clear() { + this.ignoreClosedPanel = true; + this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(null, {emitEvent: true}); + setTimeout(() => { + this.deviceProfileInput.nativeElement.blur(); + this.deviceProfileInput.nativeElement.focus(); + }, 0); + } + + textIsNotEmpty(text: string): boolean { + return (text && text.length > 0); + } + + deviceProfileEnter($event: KeyboardEvent) { + if (this.editProfileEnabled && $event.keyCode === ENTER) { + $event.preventDefault(); + if (!this.modelValue) { + this.createDeviceProfile($event, this.searchText); + } + } + } + + createDeviceProfile($event: Event, profileName: string) { + $event.preventDefault(); + const deviceProfile: DeviceProfile = { + name: profileName, + transportType: this.transportType + } as DeviceProfile; + if (this.addNewProfile) { + this.openDeviceProfileDialog(deviceProfile, true); + } + } + + editDeviceProfile($event: Event) { + $event.preventDefault(); + this.deviceProfileService.getDeviceProfile(this.modelValue.id).subscribe( + (deviceProfile) => { + this.openDeviceProfileDialog(deviceProfile, false); + } + ); + } + + openDeviceProfileDialog(deviceProfile: DeviceProfile, isAdd: boolean) { + let deviceProfileObservable: Observable; + if (!isAdd) { + deviceProfileObservable = this.dialog.open(DeviceProfileDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + isAdd: false, + deviceProfile + } + }).afterClosed(); + } else { + deviceProfileObservable = this.dialog.open(AddDeviceProfileDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + deviceProfileName: deviceProfile.name, + transportType: deviceProfile.transportType + } + }).afterClosed(); + } + deviceProfileObservable.subscribe( + (savedDeviceProfile) => { + if (!savedDeviceProfile) { + setTimeout(() => { + this.deviceProfileInput.nativeElement.blur(); + this.deviceProfileInput.nativeElement.focus(); + }, 0); + } else { + this.deviceProfileService.getDeviceProfileInfo(savedDeviceProfile.id.id).subscribe( + (profile) => { + this.modelValue = new DeviceProfileId(profile.id.id); + this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(profile, {emitEvent: true}); + if (isAdd) { + this.propagateChange(this.modelValue); + } else { + this.deviceProfileUpdated.next(savedDeviceProfile.id); + } + this.deviceProfileChanged.emit(profile); + } + ); + } + } + ); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.html new file mode 100644 index 0000000000..3e9a901fc9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.html @@ -0,0 +1,53 @@ + +
+ +

{{ (isAdd ? 'device-profile.add' : 'device-profile.edit' ) | translate }}

+ + +
+ + +
+
+ + +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.ts new file mode 100644 index 0000000000..b3e96c228d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.ts @@ -0,0 +1,101 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + Component, + ComponentFactoryResolver, + Inject, + Injector, + SkipSelf, + ViewChild +} from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormControl, FormGroupDirective, NgForm } from '@angular/forms'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; +import { DeviceProfile } from '@shared/models/device.models'; +import { DeviceProfileComponent } from './device-profile.component'; +import { DeviceProfileService } from '@core/http/device-profile.service'; + +export interface DeviceProfileDialogData { + deviceProfile: DeviceProfile; + isAdd: boolean; +} + +@Component({ + selector: 'tb-device-profile-dialog', + templateUrl: './device-profile-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: DeviceProfileDialogComponent}], + styleUrls: [] +}) +export class DeviceProfileDialogComponent extends + DialogComponent implements ErrorStateMatcher, AfterViewInit { + + isAdd: boolean; + deviceProfile: DeviceProfile; + + submitted = false; + + @ViewChild('deviceProfileComponent', {static: true}) deviceProfileComponent: DeviceProfileComponent; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: DeviceProfileDialogData, + public dialogRef: MatDialogRef, + private componentFactoryResolver: ComponentFactoryResolver, + private injector: Injector, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + private deviceProfileService: DeviceProfileService) { + super(store, router, dialogRef); + this.isAdd = this.data.isAdd; + this.deviceProfile = this.data.deviceProfile; + } + + ngAfterViewInit(): void { + if (this.isAdd) { + setTimeout(() => { + this.deviceProfileComponent.entityForm.markAsDirty(); + }, 0); + } + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + if (this.deviceProfileComponent.entityForm.valid) { + this.deviceProfile = {...this.deviceProfile, ...this.deviceProfileComponent.entityFormValue()}; + this.deviceProfileService.saveDeviceProfile(this.deviceProfile).subscribe( + (deviceProfile) => { + this.dialogRef.close(deviceProfile); + } + ); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html new file mode 100644 index 0000000000..2e9299f0cf --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html @@ -0,0 +1,46 @@ + +
+ + device-profile.provision-strategy + + + {{deviceProvisionTypeTranslateMap.get(type) | translate}} + + + + {{ 'device-profile.provision-strategy-required' | translate }} + + +
+ + device-profile.provision-device-key + + + {{ 'device-profile.provision-device-key-required' | translate }} + + + + device-profile.provision-device-secret + + + {{ 'device-profile.provision-device-secret-required' | translate }} + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts new file mode 100644 index 0000000000..e25c07e005 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts @@ -0,0 +1,152 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { + DeviceProvisionConfiguration, + DeviceProvisionType, + deviceProvisionTypeTranslationMap +} from '@shared/models/device.models'; +import { generateSecret, isDefinedAndNotNull } from '@core/utils'; + +@Component({ + selector: 'tb-device-profile-provision-configuration', + templateUrl: './device-profile-provision-configuration.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceProfileProvisionConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DeviceProfileProvisionConfigurationComponent), + multi: true, + } + ] +}) +export class DeviceProfileProvisionConfigurationComponent implements ControlValueAccessor, OnInit, Validator { + + provisionConfigurationFormGroup: FormGroup; + + deviceProvisionType = DeviceProvisionType; + deviceProvisionTypes = Object.keys(DeviceProvisionType); + deviceProvisionTypeTranslateMap = deviceProvisionTypeTranslationMap; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private propagateChange = (v: any) => { }; + + constructor(private fb: FormBuilder) { + } + + ngOnInit(): void { + this.provisionConfigurationFormGroup = this.fb.group({ + type: [DeviceProvisionType.DISABLED, Validators.required], + provisionDeviceSecret: [{value: null, disabled: true}, Validators.required], + provisionDeviceKey: [{value: null, disabled: true}, Validators.required] + }); + this.provisionConfigurationFormGroup.get('type').valueChanges.subscribe((type) => { + if (type === DeviceProvisionType.DISABLED) { + this.provisionConfigurationFormGroup.get('provisionDeviceSecret').disable({emitEvent: false}); + this.provisionConfigurationFormGroup.get('provisionDeviceSecret').patchValue(null, {emitEvent: false}); + this.provisionConfigurationFormGroup.get('provisionDeviceKey').disable({emitEvent: false}); + this.provisionConfigurationFormGroup.get('provisionDeviceKey').patchValue(null); + } else { + const provisionDeviceSecret: string = this.provisionConfigurationFormGroup.get('provisionDeviceSecret').value; + if (!provisionDeviceSecret || !provisionDeviceSecret.length) { + this.provisionConfigurationFormGroup.get('provisionDeviceSecret').patchValue(generateSecret(20), {emitEvent: false}); + } + const provisionDeviceKey: string = this.provisionConfigurationFormGroup.get('provisionDeviceKey').value; + if (!provisionDeviceKey || !provisionDeviceKey.length) { + this.provisionConfigurationFormGroup.get('provisionDeviceKey').patchValue(generateSecret(20), {emitEvent: false}); + } + this.provisionConfigurationFormGroup.get('provisionDeviceSecret').enable({emitEvent: false}); + this.provisionConfigurationFormGroup.get('provisionDeviceKey').enable({emitEvent: false}); + } + }); + this.provisionConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + writeValue(value: DeviceProvisionConfiguration | null): void { + if (isDefinedAndNotNull(value)){ + this.provisionConfigurationFormGroup.patchValue(value, {emitEvent: false}); + } else { + this.provisionConfigurationFormGroup.patchValue({type: DeviceProvisionType.DISABLED}); + } + } + + setDisabledState(isDisabled: boolean){ + this.disabled = isDisabled; + if (this.disabled){ + this.provisionConfigurationFormGroup.disable(); + } else { + if (this.provisionConfigurationFormGroup.get('type').value !== DeviceProvisionType.DISABLED) { + this.provisionConfigurationFormGroup.enable({emitEvent: false}); + } else { + this.provisionConfigurationFormGroup.get('type').enable({emitEvent: false}); + } + } + } + + validate(c: FormControl): ValidationErrors | null { + return (this.provisionConfigurationFormGroup.valid) ? null : { + provisionConfiguration: { + valid: false, + }, + }; + } + + private updateModel(): void { + let deviceProvisionConfiguration: DeviceProvisionConfiguration = null; + if (this.provisionConfigurationFormGroup.valid) { + deviceProvisionConfiguration = this.provisionConfigurationFormGroup.getRawValue(); + } + this.propagateChange(deviceProvisionConfiguration); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html new file mode 100644 index 0000000000..468e946a74 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html @@ -0,0 +1,136 @@ + +
+ + +
+ +
+
+
+
+
+ + device-profile.name + + + {{ 'device-profile.name-required' | translate }} + + + + + + + + device-profile.type + + + {{deviceProfileTypeTranslations.get(type) | translate}} + + + + {{ 'device-profile.type-required' | translate }} + + + + device-profile.description + + + + device-profile.transport-type + + + {{deviceTransportTypeTranslations.get(type) | translate}} + + + + {{ 'device-profile.transport-type-required' | translate }} + + +
+ + + + +
device-profile.profile-configuration
+
+
+ + +
+ + + +
device-profile.transport-configuration
+
+
+ + +
+ + + +
{{'device-profile.alarm-rules-with-count' | translate: + {count: entityForm.get('profileData.alarms').value ? + entityForm.get('profileData.alarms').value.length : 0} }}
+
+
+ + +
+ + + +
device-profile.device-provisioning
+
+
+ + +
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts new file mode 100644 index 0000000000..d30c8b47b9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts @@ -0,0 +1,207 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, Input, Optional } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { ActionNotificationShow } from '@app/core/notification/notification.actions'; +import { TranslateService } from '@ngx-translate/core'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import { EntityComponent } from '../entity/entity.component'; +import { + createDeviceProfileConfiguration, + createDeviceProfileTransportConfiguration, + DeviceProfile, + DeviceProfileData, + DeviceProfileType, + deviceProfileTypeConfigurationInfoMap, + deviceProfileTypeTranslationMap, + DeviceProvisionConfiguration, + DeviceProvisionType, + DeviceTransportType, + deviceTransportTypeConfigurationInfoMap, + deviceTransportTypeTranslationMap +} from '@shared/models/device.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { RuleChainId } from '@shared/models/id/rule-chain-id'; +import {ServiceType} from "@shared/models/queue.models"; + +@Component({ + selector: 'tb-device-profile', + templateUrl: './device-profile.component.html', + styleUrls: [] +}) +export class DeviceProfileComponent extends EntityComponent { + + @Input() + standalone = false; + + entityType = EntityType; + + deviceProfileTypes = Object.keys(DeviceProfileType); + + deviceProfileTypeTranslations = deviceProfileTypeTranslationMap; + + deviceTransportTypes = Object.keys(DeviceTransportType); + + deviceTransportTypeTranslations = deviceTransportTypeTranslationMap; + + displayProfileConfiguration: boolean; + + displayTransportConfiguration: boolean; + + serviceType = ServiceType.TB_RULE_ENGINE; + + constructor(protected store: Store, + protected translate: TranslateService, + @Optional() @Inject('entity') protected entityValue: DeviceProfile, + @Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + protected fb: FormBuilder) { + super(store, fb, entityValue, entitiesTableConfigValue); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + buildForm(entity: DeviceProfile): FormGroup { + this.displayProfileConfiguration = entity && entity.type && + deviceProfileTypeConfigurationInfoMap.get(entity.type).hasProfileConfiguration; + this.displayTransportConfiguration = entity && entity.transportType && + deviceTransportTypeConfigurationInfoMap.get(entity.transportType).hasProfileConfiguration; + const deviceProvisionConfiguration: DeviceProvisionConfiguration = { + type: entity?.provisionType ? entity?.provisionType : DeviceProvisionType.DISABLED, + provisionDeviceKey: entity?.provisionDeviceKey, + provisionDeviceSecret: entity?.profileData?.provisionConfiguration?.provisionDeviceSecret + }; + const form = this.fb.group( + { + name: [entity ? entity.name : '', [Validators.required]], + type: [entity ? entity.type : null, [Validators.required]], + transportType: [entity ? entity.transportType : null, [Validators.required]], + profileData: this.fb.group({ + configuration: [entity && !this.isAdd ? entity.profileData?.configuration : {}, Validators.required], + transportConfiguration: [entity && !this.isAdd ? entity.profileData?.transportConfiguration : {}, Validators.required], + alarms: [entity && !this.isAdd ? entity.profileData?.alarms : []], + provisionConfiguration: [deviceProvisionConfiguration, Validators.required] + }), + defaultRuleChainId: [entity && entity.defaultRuleChainId ? entity.defaultRuleChainId.id : null, []], + defaultQueueName: [entity ? entity.defaultQueueName : '', []], + description: [entity ? entity.description : '', []], + } + ); + form.get('type').valueChanges.subscribe(() => { + this.deviceProfileTypeChanged(form); + }); + form.get('transportType').valueChanges.subscribe(() => { + this.deviceProfileTransportTypeChanged(form); + }); + this.checkIsNewDeviceProfile(entity, form); + return form; + } + + private checkIsNewDeviceProfile(entity: DeviceProfile, form: FormGroup) { + if (entity && !entity.id) { + form.get('type').patchValue(DeviceProfileType.DEFAULT, {emitEvent: true}); + form.get('transportType').patchValue(DeviceTransportType.DEFAULT, {emitEvent: true}); + form.get('provisionType').patchValue(DeviceProvisionType.DISABLED, {emitEvent: true}); + } + } + + private deviceProfileTypeChanged(form: FormGroup) { + const deviceProfileType: DeviceProfileType = form.get('type').value; + this.displayProfileConfiguration = deviceProfileType && + deviceProfileTypeConfigurationInfoMap.get(deviceProfileType).hasProfileConfiguration; + let profileData: DeviceProfileData = form.getRawValue().profileData; + if (!profileData) { + profileData = { + configuration: null, + transportConfiguration: null + }; + } + profileData.configuration = createDeviceProfileConfiguration(deviceProfileType); + form.patchValue({profileData}); + } + + private deviceProfileTransportTypeChanged(form: FormGroup) { + const deviceTransportType: DeviceTransportType = form.get('transportType').value; + this.displayTransportConfiguration = deviceTransportType && + deviceTransportTypeConfigurationInfoMap.get(deviceTransportType).hasProfileConfiguration; + let profileData: DeviceProfileData = form.getRawValue().profileData; + if (!profileData) { + profileData = { + configuration: null, + transportConfiguration: null + }; + } + profileData.transportConfiguration = createDeviceProfileTransportConfiguration(deviceTransportType); + form.patchValue({profileData}); + } + + updateForm(entity: DeviceProfile) { + this.displayProfileConfiguration = entity.type && + deviceProfileTypeConfigurationInfoMap.get(entity.type).hasProfileConfiguration; + this.displayTransportConfiguration = entity.transportType && + deviceTransportTypeConfigurationInfoMap.get(entity.transportType).hasProfileConfiguration; + const deviceProvisionConfiguration: DeviceProvisionConfiguration = { + type: entity?.provisionType ? entity?.provisionType : DeviceProvisionType.DISABLED, + provisionDeviceKey: entity?.provisionDeviceKey, + provisionDeviceSecret: entity?.profileData?.provisionConfiguration?.provisionDeviceSecret + }; + this.entityForm.patchValue({name: entity.name}); + this.entityForm.patchValue({type: entity.type}, {emitEvent: false}); + this.entityForm.patchValue({transportType: entity.transportType}, {emitEvent: false}); + this.entityForm.patchValue({provisionType: entity.provisionType}, {emitEvent: false}); + this.entityForm.patchValue({provisionDeviceKey: entity.provisionDeviceKey}, {emitEvent: false}); + this.entityForm.patchValue({profileData: { + configuration: entity.profileData?.configuration, + transportConfiguration: entity.profileData?.transportConfiguration, + alarms: entity.profileData?.alarms, + provisionConfiguration: deviceProvisionConfiguration + }}); + this.entityForm.patchValue({defaultRuleChainId: entity.defaultRuleChainId ? entity.defaultRuleChainId.id : null}); + this.entityForm.patchValue({defaultQueueName: entity.defaultQueueName}); + this.entityForm.patchValue({description: entity.description}); + } + + prepareFormValue(formValue: any): any { + if (formValue.defaultRuleChainId) { + formValue.defaultRuleChainId = new RuleChainId(formValue.defaultRuleChainId); + } + const deviceProvisionConfiguration: DeviceProvisionConfiguration = formValue.profileData.provisionConfiguration; + formValue.provisionType = deviceProvisionConfiguration.type; + formValue.provisionDeviceKey = deviceProvisionConfiguration.provisionDeviceKey; + delete deviceProvisionConfiguration.provisionDeviceKey; + return super.prepareFormValue(formValue); + } + + onDeviceProfileIdCopied(event) { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('device-profile.idCopiedMessage'), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.html new file mode 100644 index 0000000000..796485d200 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.html @@ -0,0 +1,20 @@ + +
+ +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.ts new file mode 100644 index 0000000000..211cd5ada2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.ts @@ -0,0 +1,97 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { + DefaultDeviceProfileConfiguration, + DeviceProfileConfiguration, + DeviceProfileType +} from '@shared/models/device.models'; + +@Component({ + selector: 'tb-default-device-profile-configuration', + templateUrl: './default-device-profile-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DefaultDeviceProfileConfigurationComponent), + multi: true + }] +}) +export class DefaultDeviceProfileConfigurationComponent implements ControlValueAccessor, OnInit { + + defaultDeviceProfileConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.defaultDeviceProfileConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.defaultDeviceProfileConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.defaultDeviceProfileConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.defaultDeviceProfileConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DefaultDeviceProfileConfiguration | null): void { + this.defaultDeviceProfileConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceProfileConfiguration = null; + if (this.defaultDeviceProfileConfigurationFormGroup.valid) { + configuration = this.defaultDeviceProfileConfigurationFormGroup.getRawValue().configuration; + configuration.type = DeviceProfileType.DEFAULT; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-transport-configuration.component.html new file mode 100644 index 0000000000..061e72c4e4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-transport-configuration.component.html @@ -0,0 +1,20 @@ + +
+ +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-transport-configuration.component.ts new file mode 100644 index 0000000000..1d620f7b6e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-transport-configuration.component.ts @@ -0,0 +1,97 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { + DefaultDeviceProfileTransportConfiguration, + DeviceProfileTransportConfiguration, + DeviceTransportType +} from '@shared/models/device.models'; + +@Component({ + selector: 'tb-default-device-profile-transport-configuration', + templateUrl: './default-device-profile-transport-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DefaultDeviceProfileTransportConfigurationComponent), + multi: true + }] +}) +export class DefaultDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit { + + defaultDeviceProfileTransportConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.defaultDeviceProfileTransportConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.defaultDeviceProfileTransportConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.defaultDeviceProfileTransportConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.defaultDeviceProfileTransportConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DefaultDeviceProfileTransportConfiguration | null): void { + this.defaultDeviceProfileTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceProfileTransportConfiguration = null; + if (this.defaultDeviceProfileTransportConfigurationFormGroup.valid) { + configuration = this.defaultDeviceProfileTransportConfigurationFormGroup.getRawValue().configuration; + configuration.type = DeviceTransportType.DEFAULT; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.html new file mode 100644 index 0000000000..3b7879b933 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.html @@ -0,0 +1,27 @@ + +
+
+ + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.ts new file mode 100644 index 0000000000..766c78fb62 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.ts @@ -0,0 +1,103 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { DeviceProfileConfiguration, DeviceProfileType } from '@shared/models/device.models'; +import { deepClone } from '@core/utils'; + +@Component({ + selector: 'tb-device-profile-configuration', + templateUrl: './device-profile-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceProfileConfigurationComponent), + multi: true + }] +}) +export class DeviceProfileConfigurationComponent implements ControlValueAccessor, OnInit { + + deviceProfileType = DeviceProfileType; + + deviceProfileConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + type: DeviceProfileType; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.deviceProfileConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.deviceProfileConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.deviceProfileConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.deviceProfileConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DeviceProfileConfiguration | null): void { + this.type = value?.type; + const configuration = deepClone(value); + if (configuration) { + delete configuration.type; + } + this.deviceProfileConfigurationFormGroup.patchValue({configuration}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceProfileConfiguration = null; + if (this.deviceProfileConfigurationFormGroup.valid) { + configuration = this.deviceProfileConfigurationFormGroup.getRawValue().configuration; + configuration.type = this.type; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.html new file mode 100644 index 0000000000..ea6427addd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.html @@ -0,0 +1,39 @@ + +
+
+ + + + + + + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts new file mode 100644 index 0000000000..e85aebdd89 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts @@ -0,0 +1,103 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { DeviceProfileTransportConfiguration, DeviceTransportType } from '@shared/models/device.models'; +import { deepClone } from '@core/utils'; + +@Component({ + selector: 'tb-device-profile-transport-configuration', + templateUrl: './device-profile-transport-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceProfileTransportConfigurationComponent), + multi: true + }] +}) +export class DeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit { + + deviceTransportType = DeviceTransportType; + + deviceProfileTransportConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + transportType: DeviceTransportType; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.deviceProfileTransportConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.deviceProfileTransportConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.deviceProfileTransportConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.deviceProfileTransportConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DeviceProfileTransportConfiguration | null): void { + this.transportType = value?.type; + const configuration = deepClone(value); + if (configuration) { + delete configuration.type; + } + this.deviceProfileTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceProfileTransportConfiguration = null; + if (this.deviceProfileTransportConfigurationFormGroup.valid) { + configuration = this.deviceProfileTransportConfigurationFormGroup.getRawValue().configuration; + configuration.type = this.transportType; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m-device-profile-transport-configuration.component.html new file mode 100644 index 0000000000..03530c2b2e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m-device-profile-transport-configuration.component.html @@ -0,0 +1,24 @@ + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m-device-profile-transport-configuration.component.ts new file mode 100644 index 0000000000..7a7a1d285f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m-device-profile-transport-configuration.component.ts @@ -0,0 +1,96 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { + DeviceProfileTransportConfiguration, + DeviceTransportType, Lwm2mDeviceProfileTransportConfiguration +} from '@shared/models/device.models'; + +@Component({ + selector: 'tb-lwm2m-device-profile-transport-configuration', + templateUrl: './lwm2m-device-profile-transport-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => Lwm2mDeviceProfileTransportConfigurationComponent), + multi: true + }] +}) +export class Lwm2mDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit { + + lwm2mDeviceProfileTransportConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.lwm2mDeviceProfileTransportConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.lwm2mDeviceProfileTransportConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.lwm2mDeviceProfileTransportConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.lwm2mDeviceProfileTransportConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: Lwm2mDeviceProfileTransportConfiguration | null): void { + this.lwm2mDeviceProfileTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceProfileTransportConfiguration = null; + if (this.lwm2mDeviceProfileTransportConfigurationFormGroup.valid) { + configuration = this.lwm2mDeviceProfileTransportConfigurationFormGroup.getRawValue().configuration; + // configuration.type = DeviceTransportType.LWM2M; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html new file mode 100644 index 0000000000..00ff4760ab --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html @@ -0,0 +1,72 @@ + +
+
+
+ device-profile.mqtt-device-topic-filters +
+ + device-profile.mqtt-device-payload-type + + + {{mqttTransportPayloadTypeTranslations.get(type) | translate}} + + + + {{ 'device-profile.mqtt-payload-type-required' | translate }} + + +
+ + device-profile.telemetry-topic-filter + + + {{ 'device-profile.telemetry-topic-filter-required' | translate}} + + + {{ 'device-profile.not-valid-single-character' | translate}} + + + {{ 'device-profile.not-valid-multi-character' | translate}} + + + + device-profile.attributes-topic-filter + + + {{ 'device-profile.attributes-topic-filter-required' | translate}} + + + {{ 'device-profile.not-valid-single-character' | translate}} + + + {{ 'device-profile.not-valid-multi-character' | translate}} + + +
+
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.scss b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.scss new file mode 100644 index 0000000000..4e9bfe914b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.scss @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host{ + .fields-group { + padding: 8px; + margin: 10px 0; + border: 1px groove rgba(0, 0, 0, .25); + border-radius: 4px; + + legend { + color: rgba(0, 0, 0, .7); + } + + .tb-hint{ + padding: 0; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts new file mode 100644 index 0000000000..18dc1b2bf4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts @@ -0,0 +1,150 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALUE_ACCESSOR, + ValidatorFn, + Validators +} from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { + MqttTransportPayloadType, + DeviceProfileTransportConfiguration, + DeviceTransportType, + MqttDeviceProfileTransportConfiguration, mqttTransportPayloadTypeTranslationMap +} from '@shared/models/device.models'; +import { isDefinedAndNotNull } from '@core/utils'; + +@Component({ + selector: 'tb-mqtt-device-profile-transport-configuration', + templateUrl: './mqtt-device-profile-transport-configuration.component.html', + styleUrls: ['./mqtt-device-profile-transport-configuration.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MqttDeviceProfileTransportConfigurationComponent), + multi: true + }] +}) +export class MqttDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit { + + mqttTransportPayloadTypes = Object.keys(MqttTransportPayloadType); + + mqttTransportPayloadTypeTranslations = mqttTransportPayloadTypeTranslationMap; + + + mqttDeviceProfileTransportConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + + get required(): boolean { + return this.requiredValue; + } + + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.mqttDeviceProfileTransportConfigurationFormGroup = this.fb.group({ + configuration: this.fb.group({ + deviceAttributesTopic: [null, [Validators.required, this.validationMQTTTopic()]], + deviceTelemetryTopic: [null, [Validators.required, this.validationMQTTTopic()]], + transportPayloadType: [MqttTransportPayloadType.JSON, Validators.required] + }) + }); + this.mqttDeviceProfileTransportConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.mqttDeviceProfileTransportConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.mqttDeviceProfileTransportConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: MqttDeviceProfileTransportConfiguration | null): void { + if (isDefinedAndNotNull(value)) { + this.mqttDeviceProfileTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); + } + } + + private updateModel() { + let configuration: DeviceProfileTransportConfiguration = null; + if (this.mqttDeviceProfileTransportConfigurationFormGroup.valid) { + configuration = this.mqttDeviceProfileTransportConfigurationFormGroup.getRawValue().configuration; + configuration.type = DeviceTransportType.MQTT; + } + this.propagateChange(configuration); + } + + private validationMQTTTopic(): ValidatorFn { + return (c: FormControl) => { + const newTopic = c.value; + const wildcardSymbols = /[#+]/g; + let findSymbol = wildcardSymbols.exec(newTopic); + while (findSymbol) { + const index = findSymbol.index; + const currentSymbol = findSymbol[0]; + const prevSymbol = index > 0 ? newTopic[index - 1] : null; + const nextSymbol = index < (newTopic.length - 1) ? newTopic[index + 1] : null; + if (currentSymbol === '#' && (index !== (newTopic.length - 1) || (prevSymbol !== null && prevSymbol !== '/'))) { + return { + invalidMultiTopicCharacter: { + valid: false + } + }; + } + if (currentSymbol === '+' && ((prevSymbol !== null && prevSymbol !== '/') || (nextSymbol !== null && nextSymbol !== '/'))) { + return { + invalidSingleTopicCharacter: { + valid: false + } + }; + } + findSymbol = wildcardSymbols.exec(newTopic); + } + return null; + }; + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.html new file mode 100644 index 0000000000..9dd73c5efe --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.html @@ -0,0 +1,68 @@ + + + + + + + + + + +
+
+ tenant-profile.no-tenant-profiles-found +
+ + + {{ translate.get('tenant-profile.no-tenant-profiles-matching', + {entity: truncate.transform(searchText, true, 6, '...')}) | async }} + + + + tenant-profile.create-new-tenant-profile + +
+
+
+ + {{ 'tenant-profile.tenant-profile-required' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.ts new file mode 100644 index 0000000000..9553e23528 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.ts @@ -0,0 +1,253 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, EventEmitter, forwardRef, Input, OnInit, Output, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Observable } from 'rxjs'; +import { PageLink } from '@shared/models/page/page-link'; +import { Direction } from '@shared/models/page/sort-order'; +import { map, mergeMap, share, startWith, tap } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { TenantProfileId } from '@shared/models/id/tenant-profile-id'; +import { EntityInfoData } from '@shared/models/entity.models'; +import { TenantProfileService } from '@core/http/tenant-profile.service'; +import { entityIdEquals } from '@shared/models/id/entity-id'; +import { TruncatePipe } from '@shared//pipe/truncate.pipe'; +import { ENTER } from '@angular/cdk/keycodes'; +import { TenantProfile } from '@shared/models/tenant.model'; +import { MatDialog } from '@angular/material/dialog'; +import { TenantProfileDialogComponent, TenantProfileDialogData } from './tenant-profile-dialog.component'; + +@Component({ + selector: 'tb-tenant-profile-autocomplete', + templateUrl: './tenant-profile-autocomplete.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TenantProfileAutocompleteComponent), + multi: true + }] +}) +export class TenantProfileAutocompleteComponent implements ControlValueAccessor, OnInit { + + selectTenantProfileFormGroup: FormGroup; + + modelValue: TenantProfileId | null; + + @Input() + selectDefaultProfile = false; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @Output() + tenantProfileUpdated = new EventEmitter(); + + @ViewChild('tenantProfileInput', {static: true}) tenantProfileInput: ElementRef; + + filteredTenantProfiles: Observable>; + + searchText = ''; + + private dirty = false; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + public translate: TranslateService, + public truncate: TruncatePipe, + private tenantProfileService: TenantProfileService, + private fb: FormBuilder, + private dialog: MatDialog) { + this.selectTenantProfileFormGroup = this.fb.group({ + tenantProfile: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.filteredTenantProfiles = this.selectTenantProfileFormGroup.get('tenantProfile').valueChanges + .pipe( + tap((value: EntityInfoData | string) => { + let modelValue: TenantProfileId | null; + if (typeof value === 'string' || !value) { + modelValue = null; + } else { + modelValue = new TenantProfileId(value.id.id); + } + this.updateView(modelValue); + }), + map(value => value ? (typeof value === 'string' ? value : value.name) : ''), + mergeMap(name => this.fetchTenantProfiles(name) ), + share() + ); + } + + selectDefaultTenantProfileIfNeeded(): void { + if (this.selectDefaultProfile && !this.modelValue) { + this.tenantProfileService.getDefaultTenantProfileInfo().subscribe( + (profile) => { + if (profile) { + this.modelValue = new TenantProfileId(profile.id.id); + this.selectTenantProfileFormGroup.get('tenantProfile').patchValue(profile, {emitEvent: false}); + this.propagateChange(this.modelValue); + } + } + ); + } + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: TenantProfileId | null): void { + this.searchText = ''; + if (value != null) { + this.tenantProfileService.getTenantProfileInfo(value.id).subscribe( + (profile) => { + this.modelValue = new TenantProfileId(profile.id.id); + this.selectTenantProfileFormGroup.get('tenantProfile').patchValue(profile, {emitEvent: false}); + } + ); + } else { + this.modelValue = null; + this.selectTenantProfileFormGroup.get('tenantProfile').patchValue(null, {emitEvent: false}); + this.selectDefaultTenantProfileIfNeeded(); + } + this.dirty = true; + } + + onFocus() { + if (this.dirty) { + this.selectTenantProfileFormGroup.get('tenantProfile').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + updateView(value: TenantProfileId | null) { + if (!entityIdEquals(this.modelValue, value)) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + displayTenantProfileFn(profile?: EntityInfoData): string | undefined { + return profile ? profile.name : undefined; + } + + fetchTenantProfiles(searchText?: string): Observable> { + this.searchText = searchText; + const pageLink = new PageLink(10, 0, searchText, { + property: 'name', + direction: Direction.ASC + }); + return this.tenantProfileService.getTenantProfileInfos(pageLink, {ignoreLoading: true}).pipe( + map(pageData => { + return pageData.data; + }) + ); + } + + clear() { + this.selectTenantProfileFormGroup.get('tenantProfile').patchValue(null, {emitEvent: true}); + setTimeout(() => { + this.tenantProfileInput.nativeElement.blur(); + this.tenantProfileInput.nativeElement.focus(); + }, 0); + } + + textIsNotEmpty(text: string): boolean { + return (text && text.length > 0); + } + + tenantProfileEnter($event: KeyboardEvent) { + if ($event.keyCode === ENTER) { + $event.preventDefault(); + if (!this.modelValue) { + this.createTenantProfile($event, this.searchText); + } + } + } + + createTenantProfile($event: Event, profileName: string) { + $event.preventDefault(); + const tenantProfile: TenantProfile = { + name: profileName + }; + this.openTenantProfileDialog(tenantProfile, true); + } + + editTenantProfile($event: Event) { + $event.preventDefault(); + this.tenantProfileService.getTenantProfile(this.modelValue.id).subscribe( + (tenantProfile) => { + this.openTenantProfileDialog(tenantProfile, false); + } + ); + } + + openTenantProfileDialog(tenantProfile: TenantProfile, isAdd: boolean) { + this.dialog.open(TenantProfileDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + isAdd, + tenantProfile + } + }).afterClosed().subscribe( + (savedTenantProfile) => { + if (!savedTenantProfile) { + setTimeout(() => { + this.tenantProfileInput.nativeElement.blur(); + this.tenantProfileInput.nativeElement.focus(); + }, 0); + } else { + this.tenantProfileService.getTenantProfileInfo(savedTenantProfile.id.id).subscribe( + (profile) => { + this.modelValue = new TenantProfileId(profile.id.id); + this.selectTenantProfileFormGroup.get('tenantProfile').patchValue(profile, {emitEvent: true}); + if (isAdd) { + this.propagateChange(this.modelValue); + } else { + this.tenantProfileUpdated.next(savedTenantProfile.id); + } + } + ); + } + } + ); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.html new file mode 100644 index 0000000000..3d853940f4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.html @@ -0,0 +1,30 @@ + +
+ + + +
tenant-profile.profile-configuration
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.ts new file mode 100644 index 0000000000..cc6ce7a6ce --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.ts @@ -0,0 +1,93 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { TenantProfileData } from '@shared/models/tenant.model'; + +@Component({ + selector: 'tb-tenant-profile-data', + templateUrl: './tenant-profile-data.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TenantProfileDataComponent), + multi: true + }] +}) +export class TenantProfileDataComponent implements ControlValueAccessor, OnInit { + + tenantProfileDataFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.tenantProfileDataFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.tenantProfileDataFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.tenantProfileDataFormGroup.disable({emitEvent: false}); + } else { + this.tenantProfileDataFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: TenantProfileData | null): void { + this.tenantProfileDataFormGroup.patchValue({configuration: value?.configuration}, {emitEvent: false}); + } + + private updateModel() { + let tenantProfileData: TenantProfileData = null; + if (this.tenantProfileDataFormGroup.valid) { + tenantProfileData = this.tenantProfileDataFormGroup.getRawValue(); + } + this.propagateChange(tenantProfileData); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.html new file mode 100644 index 0000000000..75f75906d0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.html @@ -0,0 +1,53 @@ + +
+ +

{{ (isAdd ? 'tenant-profile.add' : 'tenant-profile.edit' ) | translate }}

+ + +
+ + +
+
+ + +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.ts new file mode 100644 index 0000000000..8134c4934d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.ts @@ -0,0 +1,101 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + Component, + ComponentFactoryResolver, + Inject, + Injector, + SkipSelf, + ViewChild +} from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormControl, FormGroupDirective, NgForm } from '@angular/forms'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; +import { TenantProfile } from '@shared/models/tenant.model'; +import { TenantProfileComponent } from './tenant-profile.component'; +import { TenantProfileService } from '@core/http/tenant-profile.service'; + +export interface TenantProfileDialogData { + tenantProfile: TenantProfile; + isAdd: boolean; +} + +@Component({ + selector: 'tb-tenant-profile-dialog', + templateUrl: './tenant-profile-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: TenantProfileDialogComponent}], + styleUrls: [] +}) +export class TenantProfileDialogComponent extends + DialogComponent implements ErrorStateMatcher, AfterViewInit { + + isAdd: boolean; + tenantProfile: TenantProfile; + + submitted = false; + + @ViewChild('tenantProfileComponent', {static: true}) tenantProfileComponent: TenantProfileComponent; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: TenantProfileDialogData, + public dialogRef: MatDialogRef, + private componentFactoryResolver: ComponentFactoryResolver, + private injector: Injector, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + private tenantProfileService: TenantProfileService) { + super(store, router, dialogRef); + this.isAdd = this.data.isAdd; + this.tenantProfile = this.data.tenantProfile; + } + + ngAfterViewInit(): void { + if (this.isAdd) { + setTimeout(() => { + this.tenantProfileComponent.entityForm.markAsDirty(); + }, 0); + } + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + if (this.tenantProfileComponent.entityForm.valid) { + this.tenantProfile = {...this.tenantProfile, ...this.tenantProfileComponent.entityFormValue()}; + this.tenantProfileService.saveTenantProfile(this.tenantProfile).subscribe( + (tenantProfile) => { + this.dialogRef.close(tenantProfile); + } + ); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html new file mode 100644 index 0000000000..744e460ed8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html @@ -0,0 +1,72 @@ + +
+ + +
+ +
+
+
+
+
+ + tenant-profile.name + + + {{ 'tenant-profile.name-required' | translate }} + + +
+ +
{{ 'tenant.isolated-tb-core' | translate }}
+
{{'tenant.isolated-tb-core-details' | translate}}
+
+ +
{{ 'tenant.isolated-tb-rule-engine' | translate }}
+
{{'tenant.isolated-tb-rule-engine-details' | translate}}
+
+
+ + + + tenant-profile.description + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.scss b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.scss new file mode 100644 index 0000000000..ff6264cda0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.scss @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host ::ng-deep { + .mat-checkbox{ + &.hinted-checkbox { + .mat-checkbox-inner-container { + margin-top: 4px; + } + } + + .mat-checkbox-layout{ + white-space: normal; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts new file mode 100644 index 0000000000..4e960e18a3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts @@ -0,0 +1,107 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, Input, Optional } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { + createTenantProfileConfiguration, + TenantProfile, + TenantProfileData, + TenantProfileType +} from '@shared/models/tenant.model'; +import { ActionNotificationShow } from '@app/core/notification/notification.actions'; +import { TranslateService } from '@ngx-translate/core'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import { EntityComponent } from '../entity/entity.component'; + +@Component({ + selector: 'tb-tenant-profile', + templateUrl: './tenant-profile.component.html', + styleUrls: ['./tenant-profile.component.scss'] +}) +export class TenantProfileComponent extends EntityComponent { + + @Input() + standalone = false; + + constructor(protected store: Store, + protected translate: TranslateService, + @Optional() @Inject('entity') protected entityValue: TenantProfile, + @Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + protected fb: FormBuilder) { + super(store, fb, entityValue, entitiesTableConfigValue); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + buildForm(entity: TenantProfile): FormGroup { + return this.fb.group( + { + name: [entity ? entity.name : '', [Validators.required]], + isolatedTbCore: [entity ? entity.isolatedTbCore : false, []], + isolatedTbRuleEngine: [entity ? entity.isolatedTbRuleEngine : false, []], + profileData: [entity && !this.isAdd ? entity.profileData : { + configuration: createTenantProfileConfiguration(TenantProfileType.DEFAULT) + } as TenantProfileData, []], + description: [entity ? entity.description : '', []], + } + ); + } + + updateForm(entity: TenantProfile) { + this.entityForm.patchValue({name: entity.name}); + this.entityForm.patchValue({isolatedTbCore: entity.isolatedTbCore}); + this.entityForm.patchValue({isolatedTbRuleEngine: entity.isolatedTbRuleEngine}); + this.entityForm.patchValue({profileData: !this.isAdd ? entity.profileData : { + configuration: createTenantProfileConfiguration(TenantProfileType.DEFAULT) + } as TenantProfileData}); + this.entityForm.patchValue({description: entity.description}); + } + + updateFormState() { + if (this.entityForm) { + if (this.isEditValue) { + this.entityForm.enable({emitEvent: false}); + if (!this.isAdd) { + this.entityForm.get('isolatedTbCore').disable({emitEvent: false}); + this.entityForm.get('isolatedTbRuleEngine').disable({emitEvent: false}); + } + } else { + this.entityForm.disable({emitEvent: false}); + } + } + } + + onTenantProfileIdCopied(event) { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('tenant-profile.idCopiedMessage'), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html new file mode 100644 index 0000000000..12258ffd5f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html @@ -0,0 +1,139 @@ + +
+ + tenant-profile.maximum-devices + + + {{ 'tenant-profile.maximum-devices-required' | translate}} + + + {{ 'tenant-profile.maximum-devices-range' | translate}} + + + + tenant-profile.maximum-assets + + + {{ 'tenant-profile.maximum-assets-required' | translate}} + + + {{ 'tenant-profile.maximum-assets-range' | translate}} + + + + tenant-profile.max-transport-messages + + + {{ 'tenant-profile.max-transport-messages-range' | translate}} + + + {{ 'tenant-profile.max-transport-messages-required' | translate}} + + + + tenant-profile.max-transport-data-points + + + {{ 'tenant-profile.max-transport-data-points-required' | translate}} + + + {{ 'tenant-profile.max-transport-data-points-range' | translate}} + + + + tenant-profile.max-r-e-executions + + + {{ 'tenant-profile.max-r-e-executions-required' | translate}} + + + {{ 'tenant-profile.max-r-e-executions-range' | translate}} + + + + tenant-profile.max-j-s-executions + + + {{ 'tenant-profile.max-j-s-executions-required' | translate}} + + + {{ 'tenant-profile.max-j-s-executions-range' | translate}} + + + + tenant-profile.max-d-p-storage-days + + + {{ 'tenant-profile.max-d-p-storage-days-required' | translate}} + + + {{ 'tenant-profile.max-d-p-storage-days-range' | translate}} + + + + tenant-profile.max-rule-node-executions-per-message + + + {{ 'tenant-profile.max-rule-node-executions-per-message-required' | translate}} + + + {{ 'tenant-profile.max-rule-node-executions-per-message-range' | translate}} + + + + tenant-profile.transport-tenant-msg-rate-limit + + + + tenant-profile.transport-tenant-telemetry-msg-rate-limit + + + + tenant-profile.transport-tenant-telemetry-data-points-rate-limit + + + + tenant-profile.transport-device-msg-rate-limit + + + + tenant-profile.transport-device-telemetry-msg-rate-limit + + + + tenant-profile.transport-device-telemetry-data-points-rate-limit + + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts new file mode 100644 index 0000000000..1b54126fd5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts @@ -0,0 +1,108 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { DefaultTenantProfileConfiguration, TenantProfileConfiguration } from '@shared/models/tenant.model'; +import { isDefinedAndNotNull } from '@core/utils'; + +@Component({ + selector: 'tb-default-tenant-profile-configuration', + templateUrl: './default-tenant-profile-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DefaultTenantProfileConfigurationComponent), + multi: true + }] +}) +export class DefaultTenantProfileConfigurationComponent implements ControlValueAccessor, OnInit { + + defaultTenantProfileConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + this.defaultTenantProfileConfigurationFormGroup = this.fb.group({ + maxDevices: [null, [Validators.required, Validators.min(0)]], + maxAssets: [null, [Validators.required, Validators.min(0)]], + transportTenantMsgRateLimit: [null, []], + transportTenantTelemetryMsgRateLimit: [null, []], + transportTenantTelemetryDataPointsRateLimit: [null, []], + transportDeviceMsgRateLimit: [null, []], + transportDeviceTelemetryMsgRateLimit: [null, []], + transportDeviceTelemetryDataPointsRateLimit: [null, []], + maxTransportMessages: [null, [Validators.required, Validators.min(0)]], + maxTransportDataPoints: [null, [Validators.required, Validators.min(0)]], + maxREExecutions: [null, [Validators.required, Validators.min(0)]], + maxJSExecutions: [null, [Validators.required, Validators.min(0)]], + maxDPStorageDays: [null, [Validators.required, Validators.min(0)]], + maxRuleNodeExecutionsPerMessage: [null, [Validators.required, Validators.min(0)]] + }); + this.defaultTenantProfileConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.defaultTenantProfileConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.defaultTenantProfileConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DefaultTenantProfileConfiguration | null): void { + if (isDefinedAndNotNull(value)) { + this.defaultTenantProfileConfigurationFormGroup.patchValue(value, {emitEvent: false}); + } + } + + private updateModel() { + let configuration: TenantProfileConfiguration = null; + if (this.defaultTenantProfileConfigurationFormGroup.valid) { + configuration = this.defaultTenantProfileConfigurationFormGroup.getRawValue(); + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/tenant-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant/tenant-profile-configuration.component.html new file mode 100644 index 0000000000..3204b7d2fb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/tenant-profile-configuration.component.html @@ -0,0 +1,27 @@ + +
+
+ + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/tenant-profile-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant/tenant-profile-configuration.component.ts new file mode 100644 index 0000000000..4f5fa989d4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/tenant-profile-configuration.component.ts @@ -0,0 +1,103 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { deepClone } from '@core/utils'; +import { TenantProfileConfiguration, TenantProfileType } from '@shared/models/tenant.model'; + +@Component({ + selector: 'tb-tenant-profile-configuration', + templateUrl: './tenant-profile-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TenantProfileConfigurationComponent), + multi: true + }] +}) +export class TenantProfileConfigurationComponent implements ControlValueAccessor, OnInit { + + tenantProfileType = TenantProfileType; + + tenantProfileConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + type: TenantProfileType; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.tenantProfileConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.tenantProfileConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.tenantProfileConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.tenantProfileConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: TenantProfileConfiguration | null): void { + this.type = value?.type; + const configuration = deepClone(value); + if (configuration) { + delete configuration.type; + } + this.tenantProfileConfigurationFormGroup.patchValue({configuration}, {emitEvent: false}); + } + + private updateModel() { + let configuration: TenantProfileConfiguration = null; + if (this.tenantProfileConfigurationFormGroup.valid) { + configuration = this.tenantProfileConfigurationFormGroup.getRawValue().configuration; + configuration.type = this.type; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/components/public-api.ts b/ui-ngx/src/app/modules/home/components/public-api.ts new file mode 100644 index 0000000000..5cd3d6a264 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/public-api.ts @@ -0,0 +1,17 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export * from './home-components.module'; diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.html b/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.html new file mode 100644 index 0000000000..f1d6790ae8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.html @@ -0,0 +1,68 @@ + +
+ +

{{ (isAdd ? 'relation.add' : 'relation.edit' ) | translate }}

+ + +
+ + +
+
+
+ + + {{(direction === entitySearchDirection.FROM ? + 'relation.to-entity' : 'relation.from-entity') | translate}} + + + + + +
+
+
+ + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.scss b/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.scss new file mode 100644 index 0000000000..bb3718c2da --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.scss @@ -0,0 +1,18 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + +} diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.ts b/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.ts new file mode 100644 index 0000000000..436cb72603 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.ts @@ -0,0 +1,142 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf, ViewChild } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { + CONTAINS_TYPE, + EntityRelation, + EntitySearchDirection, + RelationTypeGroup +} from '@shared/models/relation.models'; +import { EntityRelationService } from '@core/http/entity-relation.service'; +import { EntityId } from '@shared/models/id/entity-id'; +import { forkJoin, Observable } from 'rxjs'; +import { JsonObjectEditComponent } from '@shared/components/json-object-edit.component'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@shared/components/dialog.component'; + +export interface RelationDialogData { + isAdd: boolean; + direction: EntitySearchDirection; + relation: EntityRelation; +} + +@Component({ + selector: 'tb-relation-dialog', + templateUrl: './relation-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: RelationDialogComponent}], + styleUrls: ['./relation-dialog.component.scss'] +}) +export class RelationDialogComponent extends DialogComponent implements OnInit, ErrorStateMatcher { + + relationFormGroup: FormGroup; + + isAdd: boolean; + direction: EntitySearchDirection; + entitySearchDirection = EntitySearchDirection; + + additionalInfo: FormControl; + + @ViewChild('additionalInfoEdit', {static: true}) additionalInfoEdit: JsonObjectEditComponent; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: RelationDialogData, + private entityRelationService: EntityRelationService, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + public fb: FormBuilder) { + super(store, router, dialogRef); + this.isAdd = data.isAdd; + this.direction = data.direction; + } + + ngOnInit(): void { + this.relationFormGroup = this.fb.group({ + type: [this.isAdd ? CONTAINS_TYPE : this.data.relation.type, [Validators.required]], + targetEntityIds: [this.isAdd ? null : + [this.direction === EntitySearchDirection.FROM ? this.data.relation.to : this.data.relation.from], + [Validators.required]] + }); + this.additionalInfo = new FormControl(this.data.relation.additionalInfo ? {...this.data.relation.additionalInfo} : null); + if (!this.isAdd) { + this.relationFormGroup.get('type').disable(); + this.relationFormGroup.get('targetEntityIds').disable(); + } + this.additionalInfo.valueChanges.subscribe( + () => { + this.submitted = false; + } + ); + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(false); + } + + save(): void { + this.submitted = true; + this.additionalInfoEdit.validateOnSubmit(); + if (!this.relationFormGroup.invalid && !this.additionalInfo.invalid) { + if (this.isAdd) { + const tasks: Observable[] = []; + const type: string = this.relationFormGroup.get('type').value; + const entityIds: Array = this.relationFormGroup.get('targetEntityIds').value; + entityIds.forEach(entityId => { + const relation = { + type, + additionalInfo: this.additionalInfo.value, + typeGroup: RelationTypeGroup.COMMON + } as EntityRelation; + if (this.direction === EntitySearchDirection.FROM) { + relation.from = this.data.relation.from; + relation.to = entityId; + } else { + relation.from = entityId; + relation.to = this.data.relation.to; + } + tasks.push(this.entityRelationService.saveRelation(relation)); + }); + forkJoin(tasks).subscribe( + () => { + this.dialogRef.close(true); + } + ); + } else { + const relation: EntityRelation = {...this.data.relation}; + relation.additionalInfo = this.additionalInfo.value; + this.entityRelationService.saveRelation(relation).subscribe( + () => { + this.dialogRef.close(true); + } + ); + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.html b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.html new file mode 100644 index 0000000000..e3222cb4f3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.html @@ -0,0 +1,68 @@ + +
+
+
+
+ relation.type + entity.entity-types +   +
+
+
+
+
+ + + + + +
+
+
+
+
+ relation.any-relation +
+
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.scss b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.scss new file mode 100644 index 0000000000..27efcd3b4c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.scss @@ -0,0 +1,75 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .tb-relation-filters { + max-width: calc(100vw - 48px); + overflow: hidden; + + .container{ + width: 100%; + overflow: auto; + } + + .header { + width: max-content; + padding: 0 5px 5px; + + .cell { + padding-right: 5px; + padding-left: 5px; + font-size: 12px; + font-weight: 700; + color: rgba(0, 0, 0, .54); + white-space: nowrap; + } + } + + .body { + width: max-content; + padding: 0 5px 20px; + + .row { + padding-top: 5px; + } + + .cell { + padding-right: 5px; + padding-left: 5px; + } + } + + .any-filter{ + margin: 10px 0 20px; + } + } +} + +:host ::ng-deep { + .tb-relation-filters { + .cell { + .mat-form-field { + .mat-form-field-infix { + border-top: none; + } + &.mat-form-field-appearance-standard { + .mat-form-field-flex { + padding-top: 0; + } + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts new file mode 100644 index 0000000000..b5593e9b07 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts @@ -0,0 +1,123 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + FormGroup, + NG_VALUE_ACCESSOR +} from '@angular/forms'; +import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; +import { EntityTypeFilter } from '@shared/models/relation.models'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Subscription } from 'rxjs'; + +@Component({ + selector: 'tb-relation-filters', + templateUrl: './relation-filters.component.html', + styleUrls: ['./relation-filters.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RelationFiltersComponent), + multi: true + } + ] +}) +export class RelationFiltersComponent extends PageComponent implements ControlValueAccessor, OnInit { + + @Input() disabled: boolean; + + @Input() allowedEntityTypes: Array; + + relationFiltersFormGroup: FormGroup; + + private propagateChange = null; + + private valueChangeSubscription: Subscription = null; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.relationFiltersFormGroup = this.fb.group({}); + this.relationFiltersFormGroup.addControl('relationFilters', + this.fb.array([])); + } + + relationFiltersFormArray(): FormArray { + return this.relationFiltersFormGroup.get('relationFilters') as FormArray; + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState?(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(filters: Array): void { + if (this.valueChangeSubscription) { + this.valueChangeSubscription.unsubscribe(); + } + const relationFiltersControls: Array = []; + if (filters && filters.length) { + filters.forEach((filter) => { + relationFiltersControls.push(this.createRelationFilterFormGroup(filter)); + }); + } + this.relationFiltersFormGroup.setControl('relationFilters', this.fb.array(relationFiltersControls)); + this.valueChangeSubscription = this.relationFiltersFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + public removeFilter(index: number) { + (this.relationFiltersFormGroup.get('relationFilters') as FormArray).removeAt(index); + } + + public addFilter() { + const relationFiltersFormArray = this.relationFiltersFormGroup.get('relationFilters') as FormArray; + const filter: EntityTypeFilter = { + relationType: null, + entityTypes: [] + }; + relationFiltersFormArray.push(this.createRelationFilterFormGroup(filter)); + } + + private createRelationFilterFormGroup(filter: EntityTypeFilter): AbstractControl { + return this.fb.group({ + relationType: [filter ? filter.relationType : null], + entityTypes: [filter ? filter.entityTypes : []] + }); + } + + private updateModel() { + const filters: Array = this.relationFiltersFormGroup.get('relationFilters').value; + this.propagateChange(filters); + } +} diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html new file mode 100644 index 0000000000..ed8a0ef042 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html @@ -0,0 +1,170 @@ + +
+
+ +
+
+ + {{(direction == directions.FROM ? 'relation.from-relations' : 'relation.to-relations') | translate}} + + + relation.direction + + + {{ directionTypeTranslations.get(directions[type]) | translate }} + + + +
+ + + + +
+
+ +
+ + +   + + + +
+
+ +
+ + {{ 'relation.selected-relations' | translate:{count: dataSource.selection.selected.length} }} + + +
+
+
+ + + + + + + + + + + + + {{ 'relation.type' | translate }} + + {{ relation.type }} + + + + {{ 'relation.to-entity-type' | translate }} + + {{ relation.toEntityTypeName }} + + + + {{ 'relation.to-entity-name' | translate }} + + {{ relation.toName }} + + + + {{ 'relation.from-entity-type' | translate }} + + {{ relation.fromEntityTypeName }} + + + + {{ 'relation.from-entity-name' | translate }} + + {{ relation.fromName }} + + + + + + +
+ + +
+
+
+ + +
+ {{ 'relation.no-relations-text' }} +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss new file mode 100644 index 0000000000..968939a89e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss @@ -0,0 +1,91 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../scss/constants'; + +:host { + width: 100%; + height: 100%; + .tb-entity-table { + .tb-entity-table-content { + width: 100%; + height: 100%; + background: #fff; + + .mat-toolbar-tools{ + min-height: auto; + } + + .title-container{ + overflow: hidden; + } + + .tb-entity-table-title { + padding-right: 20px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .table-container { + overflow: auto; + } + + .tb-entity-table-info{ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + } + + @media #{$mat-xs} { + .mat-toolbar { + height: auto; + min-height: 100px; + + .tb-entity-table-title{ + padding-bottom: 5px; + width: 100%; + } + } + } +} + +:host ::ng-deep { + .mat-sort-header-sorted .mat-sort-header-arrow { + opacity: 1 !important; + } + mat-form-field.tb-relation-direction { + font-size: 16px; + width: 200px; + + .mat-form-field-wrapper { + padding-bottom: 0; + } + + .mat-form-field-underline { + bottom: 0; + } + + @media #{$mat-xs} { + width: 100%; + + .mat-form-field-infix { + width: auto !important; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts new file mode 100644 index 0000000000..158a41ed1b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts @@ -0,0 +1,321 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { PageLink } from '@shared/models/page/page-link'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { MatDialog } from '@angular/material/dialog'; +import { DialogService } from '@core/services/dialog.service'; +import { EntityRelationService } from '@core/http/entity-relation.service'; +import { Direction, SortOrder } from '@shared/models/page/sort-order'; +import { forkJoin, fromEvent, merge, Observable } from 'rxjs'; +import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; +import { + EntityRelation, + EntityRelationInfo, + EntitySearchDirection, + entitySearchDirectionTranslations, + RelationTypeGroup +} from '@shared/models/relation.models'; +import { EntityId } from '@shared/models/id/entity-id'; +import { RelationsDatasource } from '../../models/datasource/relation-datasource'; +import { RelationDialogComponent, RelationDialogData } from '@home/components/relation/relation-dialog.component'; + +@Component({ + selector: 'tb-relation-table', + templateUrl: './relation-table.component.html', + styleUrls: ['./relation-table.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class RelationTableComponent extends PageComponent implements AfterViewInit, OnInit { + + directions = EntitySearchDirection; + + directionTypes = Object.keys(EntitySearchDirection); + + directionTypeTranslations = entitySearchDirectionTranslations; + + displayedColumns: string[]; + direction: EntitySearchDirection; + pageLink: PageLink; + textSearchMode = false; + dataSource: RelationsDatasource; + + activeValue = false; + dirtyValue = false; + entityIdValue: EntityId; + + viewsInited = false; + + @Input() + set active(active: boolean) { + if (this.activeValue !== active) { + this.activeValue = active; + if (this.activeValue && this.dirtyValue) { + this.dirtyValue = false; + if (this.viewsInited) { + this.updateData(true); + } + } + } + } + + @Input() + set entityId(entityId: EntityId) { + if (this.entityIdValue !== entityId) { + this.entityIdValue = entityId; + if (this.viewsInited) { + this.resetSortAndFilter(this.activeValue); + if (!this.activeValue) { + this.dirtyValue = true; + } + } + } + } + + @ViewChild('searchInput') searchInputField: ElementRef; + + @ViewChild(MatPaginator) paginator: MatPaginator; + @ViewChild(MatSort) sort: MatSort; + + constructor(protected store: Store, + private entityRelationService: EntityRelationService, + public translate: TranslateService, + public dialog: MatDialog, + private dialogService: DialogService) { + super(store); + this.dirtyValue = !this.activeValue; + const sortOrder: SortOrder = { property: 'type', direction: Direction.ASC }; + this.direction = EntitySearchDirection.FROM; + this.pageLink = new PageLink(10, 0, null, sortOrder); + this.dataSource = new RelationsDatasource(this.entityRelationService, this.translate); + this.updateColumns(); + } + + ngOnInit() { + } + + updateColumns() { + if (this.direction === EntitySearchDirection.FROM) { + this.displayedColumns = ['select', 'type', 'toEntityTypeName', 'toName', 'actions']; + } else { + this.displayedColumns = ['select', 'type', 'fromEntityTypeName', 'fromName', 'actions']; + } + } + + directionChanged(direction: EntitySearchDirection) { + this.direction = direction; + this.updateColumns(); + this.updateData(true); + } + + ngAfterViewInit() { + + fromEvent(this.searchInputField.nativeElement, 'keyup') + .pipe( + debounceTime(150), + distinctUntilChanged(), + tap(() => { + this.paginator.pageIndex = 0; + this.updateData(); + }) + ) + .subscribe(); + + this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); + + merge(this.sort.sortChange, this.paginator.page) + .pipe( + tap(() => this.updateData()) + ) + .subscribe(); + + this.viewsInited = true; + if (this.activeValue && this.entityIdValue) { + this.updateData(true); + } + } + + updateData(reload: boolean = false) { + this.pageLink.page = this.paginator.pageIndex; + this.pageLink.pageSize = this.paginator.pageSize; + this.pageLink.sortOrder.property = this.sort.active; + this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; + this.dataSource.loadRelations(this.direction, this.entityIdValue, this.pageLink, reload); + } + + enterFilterMode() { + this.textSearchMode = true; + this.pageLink.textSearch = ''; + setTimeout(() => { + this.searchInputField.nativeElement.focus(); + this.searchInputField.nativeElement.setSelectionRange(0, 0); + }, 10); + } + + exitFilterMode() { + this.textSearchMode = false; + this.pageLink.textSearch = null; + this.paginator.pageIndex = 0; + this.updateData(); + } + + resetSortAndFilter(update: boolean = true) { + this.direction = EntitySearchDirection.FROM; + this.updateColumns(); + this.pageLink.textSearch = null; + this.paginator.pageIndex = 0; + const sortable = this.sort.sortables.get('type'); + this.sort.active = sortable.id; + this.sort.direction = 'asc'; + if (update) { + this.updateData(true); + } + } + + reloadRelations() { + this.updateData(true); + } + + addRelation($event: Event) { + this.openRelationDialog($event); + } + + editRelation($event: Event, relation: EntityRelationInfo) { + this.openRelationDialog($event, relation); + } + + deleteRelation($event: Event, relation: EntityRelationInfo) { + if ($event) { + $event.stopPropagation(); + } + let title; + let content; + if (this.direction === EntitySearchDirection.FROM) { + title = this.translate.instant('relation.delete-to-relation-title', {entityName: relation.toName}); + content = this.translate.instant('relation.delete-to-relation-text', {entityName: relation.toName}); + } else { + title = this.translate.instant('relation.delete-from-relation-title', {entityName: relation.fromName}); + content = this.translate.instant('relation.delete-from-relation-text', {entityName: relation.fromName}); + } + + this.dialogService.confirm( + title, + content, + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((result) => { + if (result) { + this.entityRelationService.deleteRelation( + relation.from, + relation.type, + relation.to + ).subscribe( + () => { + this.reloadRelations(); + } + ); + } + }); + } + + deleteRelations($event: Event) { + if ($event) { + $event.stopPropagation(); + } + if (this.dataSource.selection.selected.length > 0) { + let title; + let content; + + if (this.direction === EntitySearchDirection.FROM) { + title = this.translate.instant('relation.delete-to-relations-title', {count: this.dataSource.selection.selected.length}); + content = this.translate.instant('relation.delete-to-relations-text'); + } else { + title = this.translate.instant('relation.delete-from-relations-title', {count: this.dataSource.selection.selected.length}); + content = this.translate.instant('relation.delete-from-relations-text'); + } + + this.dialogService.confirm( + title, + content, + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((result) => { + if (result) { + const tasks: Observable[] = []; + this.dataSource.selection.selected.forEach((relation) => { + tasks.push(this.entityRelationService.deleteRelation( + relation.from, + relation.type, + relation.to + )); + }); + forkJoin(tasks).subscribe( + () => { + this.reloadRelations(); + } + ); + } + }); + } + } + + openRelationDialog($event: Event, relation: EntityRelation = null) { + if ($event) { + $event.stopPropagation(); + } + + let isAdd = false; + if (!relation) { + isAdd = true; + relation = { + from: null, + to: null, + type: null, + typeGroup: RelationTypeGroup.COMMON + }; + if (this.direction === EntitySearchDirection.FROM) { + relation.from = this.entityIdValue; + } else { + relation.to = this.entityIdValue; + } + } + + this.dialog.open(RelationDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + isAdd, + direction: this.direction, + relation: {...relation} + } + }).afterClosed().subscribe( + (res) => { + if (res) { + this.reloadRelations(); + } + } + ); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html new file mode 100644 index 0000000000..626b82aad3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html @@ -0,0 +1,57 @@ + + + + + + + + + +
+
+ device-profile.no-device-profiles-found +
+ + + {{ translate.get('rulechain.no-rulechains-matching', + {entity: truncate.transform(searchText, true, 6, '...')}) | async }} + + + + rulechain.create-new-rulechain + +
+
+
+ + {{ 'rulechain.rulechain-required' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts new file mode 100644 index 0000000000..de404f49e2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts @@ -0,0 +1,209 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Observable } from 'rxjs'; +import { map, mergeMap, share, tap } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; +import { BaseData } from '@shared/models/base-data'; +import { EntityService } from '@core/http/entity.service'; +import { TruncatePipe } from '@shared/pipe/truncate.pipe'; +import { RuleChainService } from '@core/http/rule-chain.service'; +import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; + +@Component({ + selector: 'tb-rule-chain-autocomplete', + templateUrl: './rule-chain-autocomplete.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RuleChainAutocompleteComponent), + multi: true + }] +}) +export class RuleChainAutocompleteComponent implements ControlValueAccessor, OnInit { + + selectRuleChainFormGroup: FormGroup; + + ruleChainLabel = 'rulechain.rulechain'; + + modelValue: string | null; + + @Input() + labelText: string; + + @Input() + requiredText: string; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @ViewChild('ruleChainInput', {static: true}) ruleChainInput: ElementRef; + @ViewChild('ruleChainInput', {read: MatAutocompleteTrigger}) ruleChainAutocomplete: MatAutocompleteTrigger; + + filteredRuleChains: Observable>>; + + searchText = ''; + + private dirty = false; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + public translate: TranslateService, + public truncate: TruncatePipe, + private entityService: EntityService, + private ruleChainService: RuleChainService, + private fb: FormBuilder) { + this.selectRuleChainFormGroup = this.fb.group({ + ruleChainId: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.filteredRuleChains = this.selectRuleChainFormGroup.get('ruleChainId').valueChanges + .pipe( + tap(value => { + let modelValue; + if (typeof value === 'string' || !value) { + modelValue = null; + } else { + modelValue = value.id.id; + } + this.updateView(modelValue); + if (value === null) { + this.clear(); + } + }), map(value => value ? (typeof value === 'string' ? value : value.name) : ''), + mergeMap(name => this.fetchRuleChain(name) ), + share() + ); + } + + ngAfterViewInit(): void {} + + getCurrentEntity(): BaseData | null { + const currentRuleChain = this.selectRuleChainFormGroup.get('ruleChainId').value; + if (currentRuleChain && typeof currentRuleChain !== 'string') { + return currentRuleChain as BaseData; + } else { + return null; + } + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.selectRuleChainFormGroup.disable({emitEvent: false}); + } else { + this.selectRuleChainFormGroup.enable({emitEvent: false}); + } + } + + textIsNotEmpty(text: string): boolean { + return (text && text.length > 0); + } + + writeValue(value: string | null): void { + this.searchText = ''; + if (value != null) { + const targetEntityType = EntityType.RULE_CHAIN; + this.entityService.getEntity(targetEntityType, value, {ignoreLoading: true, ignoreErrors: true}).subscribe( + (entity) => { + this.modelValue = entity.id.id; + this.selectRuleChainFormGroup.get('ruleChainId').patchValue(entity, {emitEvent: false}); + }, + () => { + this.modelValue = null; + this.selectRuleChainFormGroup.get('ruleChainId').patchValue('', {emitEvent: false}); + if (value !== null) { + this.propagateChange(this.modelValue); + } + } + ); + } else { + this.modelValue = null; + this.selectRuleChainFormGroup.get('ruleChainId').patchValue('', {emitEvent: false}); + } + this.dirty = true; + } + + onFocus() { + if (this.dirty) { + this.selectRuleChainFormGroup.get('ruleChainId').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + reset() { + this.selectRuleChainFormGroup.get('ruleChainId').patchValue('', {emitEvent: false}); + } + + updateView(value: string | null) { + if (this.modelValue !== value) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + displayRuleChainFn(ruleChain?: BaseData): string | undefined { + return ruleChain ? ruleChain.name : undefined; + } + + fetchRuleChain(searchText?: string): Observable>> { + this.searchText = searchText; + return this.entityService.getEntitiesByNameFilter(EntityType.RULE_CHAIN, searchText, + 50, null, {ignoreLoading: true}); + } + + clear() { + this.selectRuleChainFormGroup.get('ruleChainId').patchValue('', {emitEvent: true}); + setTimeout(() => { + this.ruleChainInput.nativeElement.blur(); + this.ruleChainInput.nativeElement.focus(); + }, 0); + } + + createDefaultRuleChain($event: Event, ruleChainName: string) { + $event.preventDefault(); + this.ruleChainAutocomplete.closePanel(); + this.ruleChainService.createDefaultRuleChain(ruleChainName.trim()).subscribe((ruleChain) => { + this.selectRuleChainFormGroup.get('ruleChainId').patchValue(ruleChain); + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/shared-home-components.module.ts b/ui-ngx/src/app/modules/home/components/shared-home-components.module.ts new file mode 100644 index 0000000000..0d0eeace91 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/shared-home-components.module.ts @@ -0,0 +1,35 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@app/shared/shared.module'; +import { AlarmDetailsDialogComponent } from '@home/components/alarm/alarm-details-dialog.component'; + +@NgModule({ + declarations: + [ + AlarmDetailsDialogComponent + ], + imports: [ + CommonModule, + SharedModule + ], + exports: [ + AlarmDetailsDialogComponent + ] +}) +export class SharedHomeComponentsModule { } diff --git a/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-editor.component.html b/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-editor.component.html new file mode 100644 index 0000000000..e0cff4d312 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-editor.component.html @@ -0,0 +1,54 @@ + +
+
+ +
+
+
+ + +
+ +
+
+ + +
+
+ + +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-editor.component.scss b/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-editor.component.scss new file mode 100644 index 0000000000..3fe4f15f89 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-editor.component.scss @@ -0,0 +1,107 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-custom-action-pretty { + box-sizing: border-box; + position: relative; + padding: 8px; + background-color: #fff; + + .tb-fullscreen-panel { + .tb-custom-action-editor-container { + height: calc(100% - 40px); + } + + .right-panel { + padding-top: 8px; + padding-left: 3px; + } + + tb-js-func .tb-js-func-panel { + box-sizing: border-box; + } + + mat-tab-group { + .mat-tab-body-wrapper { + height: 100%; + mat-tab-body { + height: 100%; + & > div { + height: 100%; + } + } + } + } + } + + .tb-split { + box-sizing: border-box; + overflow-x: hidden; + overflow-y: auto; + } + + .tb-content { + border: 1px solid #c0c0c0; + } + + .gutter { + background-color: #eee; + background-repeat: no-repeat; + background-position: 50%; + } + + .gutter.gutter-horizontal { + cursor: col-resize; + background-image: url("../../../../../../assets/split.js/grips/vertical.png"); + } + + .tb-split.tb-split-horizontal, + .gutter.gutter-horizontal { + float: left; + height: 100%; + } + + .tb-action-expand-button { + position: absolute; + right: 14px; + z-index: 2; + + &.tb-fullscreen-editor { + position: relative; + right: 0; + .mat-button { + .mat-icon { + margin-right: 5px; + } + } + } + + .mat-button { + min-width: 36px; + padding: 0; + .mat-icon { + margin-right: 0; + } + } + } + + .tb-custom-action-editor { + &.tb-fullscreen-editor { + height: 100%; + } + } +} + + diff --git a/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-editor.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-editor.component.ts new file mode 100644 index 0000000000..fe552b7d1c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-editor.component.ts @@ -0,0 +1,120 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +// tslint:disable-next-line:no-reference +/// + +import { + AfterViewInit, + Component, + ElementRef, + forwardRef, + Input, + OnDestroy, + OnInit, + QueryList, + ViewChildren, + ViewEncapsulation +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { combineLatest } from 'rxjs'; +import { CustomActionDescriptor } from '@shared/models/widget.models'; +import { CustomPrettyActionEditorCompleter } from '@home/components/widget/action/custom-action.models'; + +@Component({ + selector: 'tb-custom-action-pretty-editor', + templateUrl: './custom-action-pretty-editor.component.html', + styleUrls: ['./custom-action-pretty-editor.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CustomActionPrettyEditorComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class CustomActionPrettyEditorComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy, ControlValueAccessor { + + @Input() disabled: boolean; + + action: CustomActionDescriptor; + + fullscreen = false; + + @ViewChildren('leftPanel') + leftPanelElmRef: QueryList>; + + @ViewChildren('rightPanel') + rightPanelElmRef: QueryList>; + + customPrettyActionEditorCompleter = CustomPrettyActionEditorCompleter; + + private propagateChange = (_: any) => {}; + + constructor(protected store: Store) { + super(store); + } + + ngOnInit(): void { + } + + ngAfterViewInit(): void { + combineLatest(this.leftPanelElmRef.changes, this.rightPanelElmRef.changes).subscribe(() => { + if (this.leftPanelElmRef.length && this.rightPanelElmRef.length) { + this.initSplitLayout(this.leftPanelElmRef.first.nativeElement, + this.rightPanelElmRef.first.nativeElement); + } + }); + } + + private initSplitLayout(leftPanel: any, rightPanel: any) { + Split([leftPanel, rightPanel], { + sizes: [50, 50], + gutterSize: 8, + cursor: 'col-resize' + }); + } + + ngOnDestroy(): void { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(obj: CustomActionDescriptor): void { + this.action = obj; + } + + public onActionUpdated(valid: boolean = true) { + if (!valid) { + this.propagateChange(null); + } else { + this.propagateChange(this.action); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.html b/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.html new file mode 100644 index 0000000000..442143855d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.html @@ -0,0 +1,101 @@ + + + +
+
+
+ + + + +
+
+ +
+
+
+
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+
+
+
+ + + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.scss b/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.scss new file mode 100644 index 0000000000..6766f2e978 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.scss @@ -0,0 +1,78 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-custom-action-editor-container { + + mat-form-field.resource-field { + max-height: 40px; + margin: 10px 0 0; + .mat-form-field-wrapper { + padding-bottom: 0; + .mat-form-field-flex { + max-height: 40px; + .mat-form-field-infix { + border: 0; + } + } + .mat-form-field-underline { + bottom: 0; + } + } + } + + .html-panel, + .css-panel { + width: 100%; + min-width: 200px; + height: 100%; + min-height: 200px; + } + + div.tb-editor-area-title-panel { + position: absolute; + top: 5px; + right: 20px; + z-index: 5; + font-size: .8rem; + font-weight: 500; + + label { + padding: 4px; + color: #00acc1; + background: rgba(220, 220, 220, .35); + border-radius: 5px; + &:not(:last-child) { + margin-right: 4px; + } + } + + button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 { + align-items: center; + vertical-align: middle; + min-width: 32px; + min-height: 15px; + padding: 4px; + margin: 0; + font-size: .8rem; + line-height: 15px; + color: #7b7b7b; + background: rgba(220, 220, 220, .35); + &:not(:last-child) { + margin-right: 4px; + } + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.ts new file mode 100644 index 0000000000..54eab79b60 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.ts @@ -0,0 +1,216 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + Component, + ElementRef, + EventEmitter, + Input, + OnChanges, + OnDestroy, + OnInit, + Output, + SimpleChanges, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { CustomActionDescriptor } from '@shared/models/widget.models'; +import * as ace from 'ace-builds'; +import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; +import { css_beautify, html_beautify } from 'js-beautify'; +import { ResizeObserver } from '@juggle/resize-observer'; +import { CustomPrettyActionEditorCompleter } from '@home/components/widget/action/custom-action.models'; + +@Component({ + selector: 'tb-custom-action-pretty-resources-tabs', + templateUrl: './custom-action-pretty-resources-tabs.component.html', + styleUrls: ['./custom-action-pretty-resources-tabs.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class CustomActionPrettyResourcesTabsComponent extends PageComponent implements OnInit, OnChanges, OnDestroy { + + @Input() + action: CustomActionDescriptor; + + @Input() + hasCustomFunction: boolean; + + @Output() + actionUpdated: EventEmitter = new EventEmitter(); + + @ViewChild('htmlInput', {static: true}) + htmlInputElmRef: ElementRef; + + @ViewChild('cssInput', {static: true}) + cssInputElmRef: ElementRef; + + htmlFullscreen = false; + cssFullscreen = false; + + aceEditors: ace.Ace.Editor[] = []; + editorsResizeCafs: {[editorId: string]: CancelAnimationFrame} = {}; + aceResize$: ResizeObserver; + htmlEditor: ace.Ace.Editor; + cssEditor: ace.Ace.Editor; + setValuesPending = false; + + customPrettyActionEditorCompleter = CustomPrettyActionEditorCompleter; + + constructor(protected store: Store, + private translate: TranslateService, + private raf: RafService) { + super(store); + } + + ngOnInit(): void { + this.initAceEditors(); + if (this.setValuesPending) { + this.setAceEditorValues(); + this.setValuesPending = false; + } + } + + ngOnDestroy(): void { + this.aceResize$.disconnect(); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + if (propName === 'action') { + if (this.aceEditors.length) { + this.setAceEditorValues(); + } else { + this.setValuesPending = true; + } + } + } + } + + public notifyActionUpdated() { + this.actionUpdated.emit(this.validate() ? this.action : null); + } + + private validate(): boolean { + if (this.action.customResources) { + for (const resource of this.action.customResources) { + if (!resource.url) { + return false; + } + } + } + return true; + } + + public addResource() { + if (!this.action.customResources) { + this.action.customResources = []; + } + this.action.customResources.push({url: ''}); + this.notifyActionUpdated(); + } + + public removeResource(index: number) { + if (index > -1) { + if (this.action.customResources.splice(index, 1).length > 0) { + this.notifyActionUpdated(); + } + } + } + + public beautifyCss(): void { + const res = css_beautify(this.action.customCss, {indent_size: 4}); + if (this.action.customCss !== res) { + this.action.customCss = res; + this.cssEditor.setValue(this.action.customCss ? this.action.customCss : '', -1); + this.notifyActionUpdated(); + } + } + + public beautifyHtml(): void { + const res = html_beautify(this.action.customHtml, {indent_size: 4, wrap_line_length: 60}); + if (this.action.customHtml !== res) { + this.action.customHtml = res; + this.htmlEditor.setValue(this.action.customHtml ? this.action.customHtml : '', -1); + this.notifyActionUpdated(); + } + } + + private initAceEditors() { + this.aceResize$ = new ResizeObserver((entries) => { + entries.forEach((entry) => { + const editor = this.aceEditors.find(aceEditor => aceEditor.container === entry.target); + this.onAceEditorResize(editor); + }); + }); + this.htmlEditor = this.createAceEditor(this.htmlInputElmRef, 'html'); + this.htmlEditor.on('input', () => { + const editorValue = this.htmlEditor.getValue(); + if (this.action.customHtml !== editorValue) { + this.action.customHtml = editorValue; + this.notifyActionUpdated(); + } + }); + this.cssEditor = this.createAceEditor(this.cssInputElmRef, 'css'); + this.cssEditor.on('input', () => { + const editorValue = this.cssEditor.getValue(); + if (this.action.customCss !== editorValue) { + this.action.customCss = editorValue; + this.notifyActionUpdated(); + } + }); + } + + private createAceEditor(editorElementRef: ElementRef, mode: string): ace.Ace.Editor { + const editorElement = editorElementRef.nativeElement; + let editorOptions: Partial = { + mode: `ace/mode/${mode}`, + showGutter: true, + showPrintMargin: true + }; + const advancedOptions = { + enableSnippets: true, + enableBasicAutocompletion: true, + enableLiveAutocompletion: true + }; + editorOptions = {...editorOptions, ...advancedOptions}; + const aceEditor = ace.edit(editorElement, editorOptions); + aceEditor.session.setUseWrapMode(true); + this.aceEditors.push(aceEditor); + this.aceResize$.observe(editorElement); + return aceEditor; + } + + private setAceEditorValues() { + this.htmlEditor.setValue(this.action.customHtml ? this.action.customHtml : '', -1); + this.cssEditor.setValue(this.action.customCss ? this.action.customCss : '', -1); + } + + private onAceEditorResize(aceEditor: ace.Ace.Editor) { + if (this.editorsResizeCafs[aceEditor.id]) { + this.editorsResizeCafs[aceEditor.id](); + delete this.editorsResizeCafs[aceEditor.id]; + } + this.editorsResizeCafs[aceEditor.id] = this.raf.raf(() => { + aceEditor.resize(); + aceEditor.renderer.updateFull(); + }); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/action/custom-action.models.ts b/ui-ngx/src/app/modules/home/components/widget/action/custom-action.models.ts new file mode 100644 index 0000000000..6539780e02 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/action/custom-action.models.ts @@ -0,0 +1,77 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { TbEditorCompleter, TbEditorCompletions } from '@shared/models/ace/completion.models'; +import { widgetContextCompletions } from '@shared/models/ace/widget-completion.models'; +import { entityIdHref, entityTypeHref, serviceCompletions } from '@shared/models/ace/service-completion.models'; + +const customActionCompletions: TbEditorCompletions = { + ...{ + $event: { + meta: 'argument', + type: 'Event', + description: 'The DOM event that triggered this action.' + }, + widgetContext: widgetContextCompletions.ctx, + entityId: { + meta: 'argument', + type: entityIdHref, + description: 'Id of the entity for which the action was triggered.', + children: { + id: { + meta: 'property', + type: 'string', + description: 'UUID Id string' + }, + entityType: { + meta: 'property', + type: entityTypeHref, + description: 'Entity type' + } + } + }, + entityName: { + meta: 'argument', + type: 'string', + description: 'Name of the entity for which the action was triggered.' + }, + additionalParams: { + meta: 'argument', + type: 'object', + description: 'Optional object holding additional information.' + }, + entityLabel: { + meta: 'argument', + type: 'string', + description: 'Label of the entity for which the action was triggered.' + } + }, + ...serviceCompletions +}; + +const customPrettyActionCompletions: TbEditorCompletions = { + ...{ + htmlTemplate: { + meta: 'argument', + type: 'string', + description: 'HTML template used to render custom dialog.' + } + }, + ...customActionCompletions +} + +export const CustomActionEditorCompleter = new TbEditorCompleter(customActionCompletions); +export const CustomPrettyActionEditorCompleter = new TbEditorCompleter(customPrettyActionCompletions); diff --git a/ui-ngx/src/app/modules/home/components/widget/action/custom-sample-css.raw b/ui-ngx/src/app/modules/home/components/widget/action/custom-sample-css.raw new file mode 100644 index 0000000000..a8c69855c7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/action/custom-sample-css.raw @@ -0,0 +1,106 @@ +/*=======================================================================*/ +/*========== There are two examples: for edit and add entity ==========*/ +/*=======================================================================*/ +/*======================== Edit entity example ========================*/ +/*=======================================================================*/ +/* +.edit-entity-form .boolean-value-input { + padding-left: 5px; +} + +.edit-entity-form .boolean-value-input .checkbox-label { + margin-bottom: 8px; + color: rgba(0,0,0,0.54); + font-size: 12px; +} + +.relations-list .header { + padding-right: 5px; + padding-bottom: 5px; + padding-left: 5px; +} + +.relations-list .header .cell { + padding-right: 5px; + padding-left: 5px; + font-size: 12px; + font-weight: 700; + color: rgba(0, 0, 0, .54); + white-space: nowrap; +} + +.relations-list .mat-form-field-infix { + width: auto !important; +} + +.relations-list .body { + padding-right: 5px; + padding-bottom: 15px; + padding-left: 5px; +} + +.relations-list .body .row { + padding-top: 5px; +} + +.relations-list .body .cell { + padding-right: 5px; + padding-left: 5px; +} + +.relations-list .body .md-button { + margin: 0; +} +*/ +/*========================================================================*/ +/*========================= Add entity example =========================*/ +/*========================================================================*/ +/* +.add-entity-form .boolean-value-input { + padding-left: 5px; +} + +.add-entity-form .boolean-value-input .checkbox-label { + margin-bottom: 8px; + color: rgba(0,0,0,0.54); + font-size: 12px; +} + +.relations-list .header { + padding-right: 5px; + padding-bottom: 5px; + padding-left: 5px; +} + +.relations-list .header .cell { + padding-right: 5px; + padding-left: 5px; + font-size: 12px; + font-weight: 700; + color: rgba(0, 0, 0, .54); + white-space: nowrap; +} + +.relations-list .mat-form-field-infix { + width: auto !important; +} + +.relations-list .body { + padding-right: 5px; + padding-bottom: 15px; + padding-left: 5px; +} + +.relations-list .body .row { + padding-top: 5px; +} + +.relations-list .body .cell { + padding-right: 5px; + padding-left: 5px; +} + +.relations-list .body .md-button { + margin: 0; +} +*/ diff --git a/ui-ngx/src/app/modules/home/components/widget/action/custom-sample-html.raw b/ui-ngx/src/app/modules/home/components/widget/action/custom-sample-html.raw new file mode 100644 index 0000000000..468d31d189 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/action/custom-sample-html.raw @@ -0,0 +1,353 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/action/custom-sample-js.raw b/ui-ngx/src/app/modules/home/components/widget/action/custom-sample-js.raw new file mode 100644 index 0000000000..96dc54d998 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/action/custom-sample-js.raw @@ -0,0 +1,405 @@ +/*=======================================================================*/ +/*===== There are three examples: for delete, edit and add entity =====*/ +/*=======================================================================*/ +/*======================= Delete entity example =======================*/ +/*=======================================================================*/ +// +//let $injector = widgetContext.$scope.$injector; +//let dialogs = $injector.get(widgetContext.servicesMap.get('dialogs')); +//let assetService = $injector.get(widgetContext.servicesMap.get('assetService')); +//let deviceService = $injector.get(widgetContext.servicesMap.get('deviceService')); +// +//openDeleteEntityDialog(); +// +//function openDeleteEntityDialog() { +// let title = 'Delete ' + entityId.entityType.toLowerCase() + ' ' + +// entityName; +// let content = 'Are you sure you want to delete the ' + +// entityId.entityType.toLowerCase() + ' ' + entityName + '?'; +// dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe( +// function(result) { +// if (result) { +// deleteEntity(); +// } +// } +// ); +//} +// +//function deleteEntity() { +// deleteEntityObservable(entityId).subscribe( +// function success() { +// widgetContext.updateAliases(); +// }, +// function fail() { +// showErrorDialog(); +// } +// ); +//} +// +//function deleteEntityObservable(entityId) { +// if (entityId.entityType == "ASSET") { +// return assetService.deleteAsset(entityId.id); +// } else if (entityId.entityType == "DEVICE") { +// return deviceService.deleteDevice(entityId.id); +// } +//} +// +//function showErrorDialog() { +// let title = 'Error'; +// let content = 'An error occurred while deleting the entity. Please try again.'; +// dialogs.alert(title, content, 'CLOSE').subscribe( +// function(result) {} +// ); +//} +// +/*=======================================================================*/ +/*======================== Edit entity example ========================*/ +/*=======================================================================*/ +// +//let $injector = widgetContext.$scope.$injector; +//let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog')); +//let entityService = $injector.get(widgetContext.servicesMap.get('entityService')); +//let assetService = $injector.get(widgetContext.servicesMap.get('assetService')); +//let deviceService = $injector.get(widgetContext.servicesMap.get('deviceService')); +//let attributeService = $injector.get(widgetContext.servicesMap.get('attributeService')); +//let entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService')); +// +//openEditEntityDialog(); +// +//function openEditEntityDialog() { +// customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe(); +//} +// +//function EditEntityDialogController(instance) { +// let vm = instance; +// +// vm.entityName = entityName; +// vm.entityType = entityId.entityType; +// vm.entitySearchDirection = { +// from: "FROM", +// to: "TO" +// }; +// vm.attributes = {}; +// vm.oldRelationsData = []; +// vm.relationsToDelete = []; +// vm.entity = {}; +// +// vm.editEntityFormGroup = vm.fb.group({ +// entityName: ['', [vm.validators.required]], +// entityType: [null], +// entityLabel: [null], +// type: ['', [vm.validators.required]], +// attributes: vm.fb.group({ +// latitude: [null], +// longitude: [null], +// address: [null], +// owner: [null], +// number: [null, [vm.validators.pattern(/^-?[0-9]+$/)]], +// booleanValue: [false] +// }), +// oldRelations: vm.fb.array([]), +// relations: vm.fb.array([]) +// }); +// +// getEntityInfo(); +// +// vm.cancel = function() { +// vm.dialogRef.close(null); +// }; +// +// vm.relations = function() { +// return vm.editEntityFormGroup.get('relations'); +// }; +// +// vm.oldRelations = function() { +// return vm.editEntityFormGroup.get('oldRelations'); +// }; +// +// vm.addRelation = function() { +// vm.relations().push(vm.fb.group({ +// relatedEntity: [null, [vm.validators.required]], +// relationType: [null, [vm.validators.required]], +// direction: [null, [vm.validators.required]] +// })); +// }; +// +// function addOldRelation() { +// vm.oldRelations().push(vm.fb.group({ +// relatedEntity: [{value: null, disabled: true}, [vm.validators.required]], +// relationType: [{value: null, disabled: true}, [vm.validators.required]], +// direction: [{value: null, disabled: true}, [vm.validators.required]] +// })); +// } +// +// vm.removeRelation = function(index) { +// vm.relations().removeAt(index); +// vm.relations().markAsDirty(); +// }; +// +// vm.removeOldRelation = function(index) { +// vm.oldRelations().removeAt(index); +// vm.relationsToDelete.push(vm.oldRelationsData[index]); +// vm.oldRelations().markAsDirty(); +// }; +// +// vm.save = function() { +// vm.editEntityFormGroup.markAsPristine(); +// widgetContext.rxjs.forkJoin([ +// saveAttributes(entityId), +// saveRelations(entityId), +// saveEntity() +// ]).subscribe( +// function () { +// widgetContext.updateAliases(); +// vm.dialogRef.close(null); +// } +// ); +// }; +// +// function getEntityAttributes(attributes) { +// for (var i = 0; i < attributes.length; i++) { +// vm.attributes[attributes[i].key] = attributes[i].value; +// } +// } +// +// function getEntityRelations(relations) { +// let relationsFrom = relations[0]; +// let relationsTo = relations[1]; +// for (let i=0; i < relationsFrom.length; i++) { +// let relation = { +// direction: 'FROM', +// relationType: relationsFrom[i].type, +// relatedEntity: relationsFrom[i].to +// }; +// vm.oldRelationsData.push(relation); +// addOldRelation(); +// } +// for (let i=0; i < relationsTo.length; i++) { +// let relation = { +// direction: 'TO', +// relationType: relationsTo[i].type, +// relatedEntity: relationsTo[i].from +// }; +// vm.oldRelationsData.push(relation); +// addOldRelation(); +// } +// } +// +// function getEntityInfo() { +// widgetContext.rxjs.forkJoin([ +// entityRelationService.findInfoByFrom(entityId), +// entityRelationService.findInfoByTo(entityId), +// attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE'), +// entityService.getEntity(entityId.entityType, entityId.id) +// ]).subscribe( +// function (data) { +// getEntityRelations(data.slice(0,2)); +// getEntityAttributes(data[2]); +// vm.entity = data[3]; +// vm.editEntityFormGroup.patchValue({ +// entityName: vm.entity.name, +// entityType: vm.entityType, +// entityLabel: vm.entity.label, +// type: vm.entity.type, +// attributes: vm.attributes, +// oldRelations: vm.oldRelationsData +// }, {emitEvent: false}); +// } +// ); +// } +// +// function saveEntity() { +// const formValues = vm.editEntityFormGroup.value; +// if (vm.entity.label !== formValues.entityLabel){ +// vm.entity.label = formValues.entityLabel; +// if (formValues.entityType == 'ASSET') { +// return assetService.saveAsset(vm.entity); +// } else if (formValues.entityType == 'DEVICE') { +// return deviceService.saveDevice(vm.entity); +// } +// } +// return widgetContext.rxjs.of([]); +// } +// +// function saveAttributes(entityId) { +// let attributes = vm.editEntityFormGroup.get('attributes').value; +// let attributesArray = []; +// for (let key in attributes) { +// if (attributes[key] !== vm.attributes[key]) { +// attributesArray.push({key: key, value: attributes[key]}); +// } +// } +// if (attributesArray.length > 0) { +// return attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributesArray); +// } +// return widgetContext.rxjs.of([]); +// } +// +// function saveRelations(entityId) { +// let relations = vm.editEntityFormGroup.get('relations').value; +// let tasks = []; +// for(let i=0; i < relations.length; i++) { +// let relation = { +// type: relations[i].relationType, +// typeGroup: 'COMMON' +// }; +// if (relations[i].direction == 'FROM') { +// relation.to = relations[i].relatedEntity; +// relation.from = entityId; +// } else { +// relation.to = entityId; +// relation.from = relations[i].relatedEntity; +// } +// tasks.push(entityRelationService.saveRelation(relation)); +// } +// for (let i=0; i < vm.relationsToDelete.length; i++) { +// let relation = { +// type: vm.relationsToDelete[i].relationType +// }; +// if (vm.relationsToDelete[i].direction == 'FROM') { +// relation.to = vm.relationsToDelete[i].relatedEntity; +// relation.from = entityId; +// } else { +// relation.to = entityId; +// relation.from = vm.relationsToDelete[i].relatedEntity; +// } +// tasks.push(entityRelationService.deleteRelation(relation.from, relation.type, relation.to)); +// } +// if (tasks.length > 0) { +// return widgetContext.rxjs.forkJoin(tasks); +// } +// return widgetContext.rxjs.of([]); +// } +//} +// +/*========================================================================*/ +/*========================= Add entity example =========================*/ +/*========================================================================*/ +// +//let $injector = widgetContext.$scope.$injector; +//let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog')); +//let assetService = $injector.get(widgetContext.servicesMap.get('assetService')); +//let deviceService = $injector.get(widgetContext.servicesMap.get('deviceService')); +//let attributeService = $injector.get(widgetContext.servicesMap.get('attributeService')); +//let entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService')); +// +//openAddEntityDialog(); +// +//function openAddEntityDialog() { +// customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe(); +//} +// +//function AddEntityDialogController(instance) { +// let vm = instance; +// +// vm.allowedEntityTypes = ['ASSET', 'DEVICE']; +// vm.entitySearchDirection = { +// from: "FROM", +// to: "TO" +// } +// +// vm.addEntityFormGroup = vm.fb.group({ +// entityName: ['', [vm.validators.required]], +// entityType: ['DEVICE'], +// entityLabel: [null], +// type: ['', [vm.validators.required]], +// attributes: vm.fb.group({ +// latitude: [null], +// longitude: [null], +// address: [null], +// owner: [null], +// number: [null, [vm.validators.pattern(/^-?[0-9]+$/)]], +// booleanValue: [null] +// }), +// relations: vm.fb.array([]) +// }); +// +// vm.cancel = function() { +// vm.dialogRef.close(null); +// }; +// +// vm.relations = function() { +// return vm.addEntityFormGroup.get('relations'); +// }; +// +// vm.addRelation = function() { +// vm.relations().push(vm.fb.group({ +// relatedEntity: [null, [vm.validators.required]], +// relationType: [null, [vm.validators.required]], +// direction: [null, [vm.validators.required]] +// })); +// }; +// +// vm.removeRelation = function(index) { +// vm.relations().removeAt(index); +// vm.relations().markAsDirty(); +// }; +// +// vm.save = function() { +// vm.addEntityFormGroup.markAsPristine(); +// saveEntityObservable().subscribe( +// function (entity) { +// widgetContext.rxjs.forkJoin([ +// saveAttributes(entity.id), +// saveRelations(entity.id) +// ]).subscribe( +// function () { +// widgetContext.updateAliases(); +// vm.dialogRef.close(null); +// } +// ); +// } +// ); +// }; +// +// function saveEntityObservable() { +// const formValues = vm.addEntityFormGroup.value; +// let entity = { +// name: formValues.entityName, +// type: formValues.type, +// label: formValues.entityLabel +// }; +// if (formValues.entityType == 'ASSET') { +// return assetService.saveAsset(entity); +// } else if (formValues.entityType == 'DEVICE') { +// return deviceService.saveDevice(entity); +// } +// } +// +// function saveAttributes(entityId) { +// let attributes = vm.addEntityFormGroup.get('attributes').value; +// let attributesArray = []; +// for (let key in attributes) { +// if(attributes[key] !== null) { +// attributesArray.push({key: key, value: attributes[key]}); +// } +// } +// if (attributesArray.length > 0) { +// return attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributesArray); +// } +// return widgetContext.rxjs.of([]); +// } +// +// function saveRelations(entityId) { +// let relations = vm.addEntityFormGroup.get('relations').value; +// let tasks = []; +// for(let i=0; i < relations.length; i++) { +// let relation = { +// type: relations[i].relationType, +// typeGroup: 'COMMON' +// }; +// if (relations[i].direction == 'FROM') { +// relation.to = relations[i].relatedEntity; +// relation.from = entityId; +// } else { +// relation.to = entityId; +// relation.from = relations[i].relatedEntity; +// } +// tasks.push(entityRelationService.saveRelation(relation)); +// } +// if (tasks.length > 0) { +// return widgetContext.rxjs.forkJoin(tasks); +// } +// return widgetContext.rxjs.of([]); +// } +//} diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html new file mode 100644 index 0000000000..9778e5794f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html @@ -0,0 +1,124 @@ + +
+
+ +
+ widget-config.actions + + + +
+
+ +
+ + +   + + + +
+
+
+ + + {{ 'widget-config.action-source' | translate }} + + {{ action.actionSourceName }} + + + + {{ 'widget-config.action-name' | translate }} + + {{ action.name }} + + + + {{ 'widget-config.action-icon' | translate }} + + {{ action.icon }} + + + + {{ 'widget-config.action-type' | translate }} + + {{ action.typeName }} + + + + + + +
+ + +
+
+
+ + +
+ {{ 'widget-config.no-actions-text' }} +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.models.ts new file mode 100644 index 0000000000..596265452d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.models.ts @@ -0,0 +1,166 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + CustomActionDescriptor, + WidgetActionDescriptor, + WidgetActionSource, + widgetActionTypeTranslationMap +} from '@app/shared/models/widget.models'; +import { CollectionViewer, DataSource } from '@angular/cdk/collections'; +import { BehaviorSubject, Observable, of, ReplaySubject } from 'rxjs'; +import { emptyPageData, PageData } from '@shared/models/page/page-data'; +import { TranslateService } from '@ngx-translate/core'; +import { PageLink } from '@shared/models/page/page-link'; +import { catchError, map, publishReplay, refCount } from 'rxjs/operators'; +import { UtilsService } from '@core/services/utils.service'; +import { deepClone, isDefined, isUndefined } from '@core/utils'; + +import customSampleJs from '!raw-loader!./custom-sample-js.raw'; +import customSampleCss from '!raw-loader!./custom-sample-css.raw'; +import customSampleHtml from '!raw-loader!./custom-sample-html.raw'; + +export interface WidgetActionCallbacks { + fetchDashboardStates: (query: string) => Array; +} + +export interface WidgetActionsData { + actionsMap: {[actionSourceId: string]: Array}; + actionSources: {[actionSourceId: string]: WidgetActionSource}; +} + +export interface WidgetActionDescriptorInfo extends WidgetActionDescriptor { + actionSourceId?: string; + actionSourceName?: string; + typeName?: string; +} + +export function toWidgetActionDescriptor(action: WidgetActionDescriptorInfo): WidgetActionDescriptor { + const copy = deepClone(action); + delete copy.actionSourceId; + delete copy.actionSourceName; + delete copy.typeName; + return copy; +} + +export function toCustomAction(action: WidgetActionDescriptorInfo): CustomActionDescriptor { + let result: CustomActionDescriptor; + if (!action || (isUndefined(action.customFunction) && isUndefined(action.customHtml) && isUndefined(action.customCss))) { + result = { + customHtml: customSampleHtml, + customCss: customSampleCss, + customFunction: customSampleJs + }; + } else { + result = { + customHtml: action.customHtml, + customCss: action.customCss, + customFunction: action.customFunction + }; + } + result.customResources = action && isDefined(action.customResources) ? deepClone(action.customResources) : []; + return result; +} + +export class WidgetActionsDatasource implements DataSource { + + private actionsSubject = new BehaviorSubject([]); + private pageDataSubject = new BehaviorSubject>(emptyPageData()); + + public pageData$ = this.pageDataSubject.asObservable(); + + private allActions: Observable>; + + private actionsMap: {[actionSourceId: string]: Array}; + private actionSources: {[actionSourceId: string]: WidgetActionSource}; + + constructor(private translate: TranslateService, + private utils: UtilsService) {} + + connect(collectionViewer: CollectionViewer): Observable> { + return this.actionsSubject.asObservable(); + } + + disconnect(collectionViewer: CollectionViewer): void { + this.actionsSubject.complete(); + this.pageDataSubject.complete(); + } + + setActions(actionsData: WidgetActionsData) { + this.actionsMap = actionsData.actionsMap; + this.actionSources = actionsData.actionSources; + } + + loadActions(pageLink: PageLink, reload: boolean = false): Observable> { + if (reload) { + this.allActions = null; + } + const result = new ReplaySubject>(); + this.fetchActions(pageLink).pipe( + catchError(() => of(emptyPageData())), + ).subscribe( + (pageData) => { + this.actionsSubject.next(pageData.data); + this.pageDataSubject.next(pageData); + result.next(pageData); + } + ); + return result; + } + + fetchActions(pageLink: PageLink): Observable> { + return this.getAllActions().pipe( + map((data) => pageLink.filterData(data)) + ); + } + + getAllActions(): Observable> { + if (!this.allActions) { + const actions: WidgetActionDescriptorInfo[] = []; + for (const actionSourceId of Object.keys(this.actionsMap)) { + const descriptors = this.actionsMap[actionSourceId]; + descriptors.forEach((descriptor) => { + actions.push(this.toWidgetActionDescriptorInfo(actionSourceId, descriptor)); + }); + } + this.allActions = of(actions).pipe( + publishReplay(1), + refCount() + ); + } + return this.allActions; + } + + private toWidgetActionDescriptorInfo(actionSourceId: string, action: WidgetActionDescriptor): WidgetActionDescriptorInfo { + const actionSource = this.actionSources[actionSourceId]; + const actionSourceName = actionSource ? this.utils.customTranslation(actionSource.name, actionSource.name) : actionSourceId; + const typeName = this.translate.instant(widgetActionTypeTranslationMap.get(action.type)); + return { actionSourceId, actionSourceName, typeName, ...action}; + } + + isEmpty(): Observable { + return this.actionsSubject.pipe( + map((actions) => !actions.length) + ); + } + + total(): Observable { + return this.pageDataSubject.pipe( + map((pageData) => pageData.totalElements) + ); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss new file mode 100644 index 0000000000..b82b4f3ca6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + width: 100%; + height: 100%; + .tb-entity-table { + .tb-entity-table-content { + width: 100%; + height: 100%; + background: #fff; + + .tb-entity-table-title { + padding-right: 20px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .table-container { + overflow: auto; + } + } + } +} + +:host ::ng-deep { + .mat-sort-header-sorted .mat-sort-header-arrow { + opacity: 1 !important; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts new file mode 100644 index 0000000000..9aac46981c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts @@ -0,0 +1,307 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { TranslateService } from '@ngx-translate/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { MatDialog } from '@angular/material/dialog'; +import { DialogService } from '@core/services/dialog.service'; +import { PageLink } from '@shared/models/page/page-link'; +import { Direction, SortOrder } from '@shared/models/page/sort-order'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; +import { fromEvent, merge } from 'rxjs'; +import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; +import { + toWidgetActionDescriptor, + WidgetActionCallbacks, + WidgetActionDescriptorInfo, + WidgetActionsData, + WidgetActionsDatasource +} from '@home/components/widget/action/manage-widget-actions.component.models'; +import { UtilsService } from '@core/services/utils.service'; +import { WidgetActionDescriptor, WidgetActionSource } from '@shared/models/widget.models'; +import { + WidgetActionDialogComponent, + WidgetActionDialogData +} from '@home/components/widget/action/widget-action-dialog.component'; +import { deepClone } from '@core/utils'; + +@Component({ + selector: 'tb-manage-widget-actions', + templateUrl: './manage-widget-actions.component.html', + styleUrls: ['./manage-widget-actions.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ManageWidgetActionsComponent), + multi: true + } + ] +}) +export class ManageWidgetActionsComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy, ControlValueAccessor { + + @Input() disabled: boolean; + + @Input() callbacks: WidgetActionCallbacks; + + innerValue: WidgetActionsData; + + displayedColumns: string[]; + pageLink: PageLink; + textSearchMode = false; + dataSource: WidgetActionsDatasource; + + viewsInited = false; + dirtyValue = false; + + @ViewChild('searchInput') searchInputField: ElementRef; + + @ViewChild(MatPaginator) paginator: MatPaginator; + @ViewChild(MatSort) sort: MatSort; + + private propagateChange = (_: any) => {}; + + constructor(protected store: Store, + private translate: TranslateService, + private utils: UtilsService, + private dialog: MatDialog, + private dialogs: DialogService) { + super(store); + const sortOrder: SortOrder = { property: 'actionSourceName', direction: Direction.ASC }; + this.pageLink = new PageLink(10, 0, null, sortOrder); + this.dataSource = new WidgetActionsDatasource(this.translate, this.utils); + this.displayedColumns = ['actionSourceName', 'name', 'icon', 'typeName', 'actions']; + } + + ngOnInit(): void { + } + + ngOnDestroy(): void { + } + + ngAfterViewInit() { + + fromEvent(this.searchInputField.nativeElement, 'keyup') + .pipe( + debounceTime(150), + distinctUntilChanged(), + tap(() => { + this.paginator.pageIndex = 0; + this.updateData(); + }) + ) + .subscribe(); + + this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); + + merge(this.sort.sortChange, this.paginator.page) + .pipe( + tap(() => this.updateData()) + ) + .subscribe(); + + this.viewsInited = true; + if (this.dirtyValue) { + this.dirtyValue = false; + this.updateData(true); + } + + } + + updateData(reload: boolean = false) { + this.pageLink.page = this.paginator.pageIndex; + this.pageLink.pageSize = this.paginator.pageSize; + this.pageLink.sortOrder.property = this.sort.active; + this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; + this.dataSource.loadActions(this.pageLink, reload); + } + + addAction($event: Event) { + this.openWidgetActionDialog($event); + } + + editAction($event: Event, action: WidgetActionDescriptorInfo) { + this.openWidgetActionDialog($event, action); + } + + openWidgetActionDialog($event: Event, action: WidgetActionDescriptorInfo = null) { + if ($event) { + $event.stopPropagation(); + } + const isAdd = action === null; + let prevActionSourceId = null; + if (!isAdd) { + prevActionSourceId = action.actionSourceId; + } + const availableActionSources: {[actionSourceId: string]: WidgetActionSource} = {}; + for (const id of Object.keys(this.innerValue.actionSources)) { + const actionSource = this.innerValue.actionSources[id]; + if (actionSource.multiple) { + availableActionSources[id] = actionSource; + } else { + if (!isAdd && action.actionSourceId === id) { + availableActionSources[id] = actionSource; + } else { + const existing = this.innerValue.actionsMap[id]; + if (!existing || !existing.length) { + availableActionSources[id] = actionSource; + } + } + } + } + + const actionsData: WidgetActionsData = { + actionsMap: this.innerValue.actionsMap, + actionSources: availableActionSources + }; + + this.dialog.open(WidgetActionDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + isAdd, + callbacks: this.callbacks, + actionsData, + action: deepClone(action) + } + }).afterClosed().subscribe( + (res) => { + if (res) { + this.saveAction(res, isAdd, prevActionSourceId); + } + } + ); + } + + private saveAction(actionInfo: WidgetActionDescriptorInfo, isAdd: boolean, prevActionSourceId: string) { + const actionSourceId = actionInfo.actionSourceId; + const action = toWidgetActionDescriptor(actionInfo); + if (isAdd) { + const targetActions = this.getOrCreateTargetActions(actionSourceId); + targetActions.push(action); + } else { + if (actionSourceId !== prevActionSourceId) { + let targetActions = this.getOrCreateTargetActions(prevActionSourceId); + const targetIndex = targetActions.findIndex((targetAction) => targetAction.id === action.id); + if (targetIndex > -1) { + targetActions.splice(targetIndex, 1); + } + targetActions = this.getOrCreateTargetActions(actionSourceId); + targetActions.push(action); + } else { + const targetActions = this.getOrCreateTargetActions(actionSourceId); + const targetIndex = targetActions.findIndex((targetAction) => targetAction.id === action.id); + if (targetIndex > -1) { + targetActions[targetIndex] = action; + } + } + } + this.onActionsUpdated(); + } + + private getOrCreateTargetActions(actionSourceId: string): Array { + const actionsMap = this.innerValue.actionsMap; + let targetActions = actionsMap[actionSourceId]; + if (!targetActions) { + targetActions = []; + actionsMap[actionSourceId] = targetActions; + } + return targetActions; + } + + deleteAction($event: Event, action: WidgetActionDescriptorInfo) { + if ($event) { + $event.stopPropagation(); + } + const title = this.translate.instant('widget-config.delete-action-title'); + const content = this.translate.instant('widget-config.delete-action-text', {actionName: action.name}); + this.dialogs.confirm(title, content, + this.translate.instant('action.no'), + this.translate.instant('action.yes'), true).subscribe( + (res) => { + if (res) { + const targetActions = this.getOrCreateTargetActions(action.actionSourceId); + const targetIndex = targetActions.findIndex((targetAction) => targetAction.id === action.id); + if (targetIndex > -1) { + targetActions.splice(targetIndex, 1); + this.onActionsUpdated(); + } + } + }); + } + + enterFilterMode() { + this.textSearchMode = true; + this.pageLink.textSearch = ''; + setTimeout(() => { + this.searchInputField.nativeElement.focus(); + this.searchInputField.nativeElement.setSelectionRange(0, 0); + }, 10); + } + + exitFilterMode() { + this.textSearchMode = false; + this.pageLink.textSearch = null; + this.paginator.pageIndex = 0; + this.updateData(); + } + + resetSortAndFilter(update: boolean = true) { + this.pageLink.textSearch = null; + this.paginator.pageIndex = 0; + const sortable = this.sort.sortables.get('actionSourceName'); + this.sort.active = sortable.id; + this.sort.direction = 'asc'; + if (update) { + this.updateData(true); + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(obj: WidgetActionsData): void { + this.innerValue = obj; + if (this.innerValue) { + setTimeout(() => { + this.dataSource.setActions(this.innerValue); + if (this.viewsInited) { + this.resetSortAndFilter(true); + } else { + this.dirtyValue = true; + } + }, 0); + } + } + + private onActionsUpdated() { + this.updateData(true); + this.propagateChange(this.innerValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html new file mode 100644 index 0000000000..46a0d213a4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html @@ -0,0 +1,162 @@ + +
+ +

{{ (isAdd ? 'widget-config.add-action' : 'widget-config.edit-action' ) | translate }}

+ + +
+ + +
+
+
+ + widget-config.action-source + + + {{ actionSourceName(actionSourceItem.value) }} + + + + {{ 'widget-config.action-source-required' | translate }} + + + + widget-config.action-name + + + {{ 'widget-config.action-name-required' | translate }} + + + + + + + + widget-config.action-type + + + {{ widgetActionTypeTranslations.get(widgetActionType[actionType]) | translate }} + + + + {{ 'widget-config.action-type-required' | translate }} + + +
+ +
+
widget-action.target-dashboard
+ +
+
+ + + + + + + + + + + {{ 'widget-action.target-dashboard-state-required' | translate }} + + + + + + {{ 'widget-action.open-right-layout' | translate }} + + + +
+ + {{ 'widget-action.set-entity-from-widget' | translate }} + + + alias.state-entity-parameter-name + + +
+
+ + + + + + + +
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.scss new file mode 100644 index 0000000000..c139c7028b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.scss @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host ::ng-deep { + .mat-checkbox-label { + white-space: normal; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts new file mode 100644 index 0000000000..3d7e9ae595 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts @@ -0,0 +1,299 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, Inject, OnInit, SkipSelf, ViewChild } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + FormBuilder, + FormControl, + FormGroup, + FormGroupDirective, + NgForm, + ValidatorFn, + Validators +} from '@angular/forms'; +import { Observable, of } from 'rxjs'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { + toCustomAction, + WidgetActionCallbacks, + WidgetActionDescriptorInfo, + WidgetActionsData +} from '@home/components/widget/action/manage-widget-actions.component.models'; +import { UtilsService } from '@core/services/utils.service'; +import { WidgetActionSource, WidgetActionType, widgetActionTypeTranslationMap } from '@shared/models/widget.models'; +import { map, mergeMap, startWith, tap } from 'rxjs/operators'; +import { DashboardService } from '@core/http/dashboard.service'; +import { Dashboard } from '@shared/models/dashboard.models'; +import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; +import { CustomActionEditorCompleter } from '@home/components/widget/action/custom-action.models'; + +export interface WidgetActionDialogData { + isAdd: boolean; + callbacks: WidgetActionCallbacks; + actionsData: WidgetActionsData; + action?: WidgetActionDescriptorInfo; +} + +@Component({ + selector: 'tb-widget-action-dialog', + templateUrl: './widget-action-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: WidgetActionDialogComponent}], + styleUrls: ['./widget-action-dialog.component.scss'] +}) +export class WidgetActionDialogComponent extends DialogComponent implements OnInit, ErrorStateMatcher { + + @ViewChild('dashboardStateInput') dashboardStateInput: ElementRef; + + widgetActionFormGroup: FormGroup; + actionTypeFormGroup: FormGroup; + + isAdd: boolean; + action: WidgetActionDescriptorInfo; + + widgetActionTypes = Object.keys(WidgetActionType); + widgetActionTypeTranslations = widgetActionTypeTranslationMap; + widgetActionType = WidgetActionType; + + filteredDashboardStates: Observable>; + targetDashboardStateSearchText = ''; + selectedDashboardStateIds: Observable>; + + customActionEditorCompleter = CustomActionEditorCompleter; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + private utils: UtilsService, + private dashboardService: DashboardService, + private dashboardUtils: DashboardUtilsService, + @Inject(MAT_DIALOG_DATA) public data: WidgetActionDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + public fb: FormBuilder) { + super(store, router, dialogRef); + this.isAdd = data.isAdd; + if (this.isAdd) { + this.action = { + id: this.utils.guid(), + name: '', + icon: 'more_horiz', + type: null + }; + } else { + this.action = this.data.action; + } + } + + ngOnInit(): void { + this.widgetActionFormGroup = this.fb.group({}); + this.widgetActionFormGroup.addControl('actionSourceId', + this.fb.control(this.action.actionSourceId, [Validators.required])); + this.widgetActionFormGroup.addControl('name', + this.fb.control(this.action.name, [this.validateActionName(), Validators.required])); + this.widgetActionFormGroup.addControl('icon', + this.fb.control(this.action.icon, [Validators.required])); + this.widgetActionFormGroup.addControl('type', + this.fb.control(this.action.type, [Validators.required])); + this.updateActionTypeFormGroup(this.action.type, this.action); + this.widgetActionFormGroup.get('type').valueChanges.subscribe((type: WidgetActionType) => { + this.updateActionTypeFormGroup(type); + }); + this.widgetActionFormGroup.get('actionSourceId').valueChanges.subscribe(() => { + this.widgetActionFormGroup.get('name').updateValueAndValidity(); + }); + } + + private updateActionTypeFormGroup(type?: WidgetActionType, action?: WidgetActionDescriptorInfo) { + this.actionTypeFormGroup = this.fb.group({}); + if (type) { + switch (type) { + case WidgetActionType.openDashboard: + case WidgetActionType.openDashboardState: + case WidgetActionType.updateDashboardState: + this.actionTypeFormGroup.addControl( + 'targetDashboardStateId', + this.fb.control(action ? action.targetDashboardStateId : null, + type === WidgetActionType.openDashboardState ? [Validators.required] : []) + ); + this.actionTypeFormGroup.addControl( + 'setEntityId', + this.fb.control(action ? action.setEntityId : true, []) + ); + this.actionTypeFormGroup.addControl( + 'stateEntityParamName', + this.fb.control(action ? action.stateEntityParamName : null, []) + ); + if (type === WidgetActionType.openDashboard) { + this.actionTypeFormGroup.addControl( + 'targetDashboardId', + this.fb.control(action ? action.targetDashboardId : null, + [Validators.required]) + ); + this.setupSelectedDashboardStateIds(action ? action.targetDashboardId : null); + } else { + this.actionTypeFormGroup.addControl( + 'openRightLayout', + this.fb.control(action ? action.openRightLayout : false, []) + ); + } + this.setupFilteredDashboardStates(); + break; + case WidgetActionType.custom: + this.actionTypeFormGroup.addControl( + 'customFunction', + this.fb.control(action ? action.customFunction : null, []) + ); + break; + case WidgetActionType.customPretty: + this.actionTypeFormGroup.addControl( + 'customAction', + this.fb.control(toCustomAction(action), [Validators.required]) + ); + break; + } + } + } + + private setupSelectedDashboardStateIds(targetDashboardId?: string) { + this.selectedDashboardStateIds = + this.actionTypeFormGroup.get('targetDashboardId').valueChanges.pipe( + // startWith(targetDashboardId), + tap(() => { + this.targetDashboardStateSearchText = ''; + }), + mergeMap((dashboardId) => { + if (dashboardId) { + return this.dashboardService.getDashboard(dashboardId); + } else { + return of(null); + } + }), + map((dashboard: Dashboard) => { + if (dashboard) { + dashboard = this.dashboardUtils.validateAndUpdateDashboard(dashboard); + const states = dashboard.configuration.states; + return Object.keys(states); + } else { + return []; + } + }) + ); + } + + private setupFilteredDashboardStates() { + this.targetDashboardStateSearchText = ''; + this.filteredDashboardStates = this.actionTypeFormGroup.get('targetDashboardStateId').valueChanges + .pipe( + startWith(''), + map(value => value ? value : ''), + mergeMap(name => this.fetchDashboardStates(name) ) + ); + } + + private fetchDashboardStates(searchText?: string): Observable> { + this.targetDashboardStateSearchText = searchText; + if (this.widgetActionFormGroup.get('type').value === WidgetActionType.openDashboard) { + return this.selectedDashboardStateIds.pipe( + map(stateIds => { + const result = searchText ? stateIds.filter(this.createFilterForDashboardState(searchText)) : stateIds; + if (result && result.length) { + return result; + } else { + return [searchText]; + } + }) + ); + } else { + return of(this.data.callbacks.fetchDashboardStates(searchText)); + } + } + + private createFilterForDashboardState(query: string): (stateId: string) => boolean { + const lowercaseQuery = query.toLowerCase(); + return stateId => stateId.toLowerCase().indexOf(lowercaseQuery) === 0; + } + + public clearTargetDashboardState(value: string = '') { + this.dashboardStateInput.nativeElement.value = value; + this.actionTypeFormGroup.get('targetDashboardStateId').patchValue(value, {emitEvent: true}); + setTimeout(() => { + this.dashboardStateInput.nativeElement.blur(); + this.dashboardStateInput.nativeElement.focus(); + }, 0); + } + + private validateActionName(): ValidatorFn { + return (c: FormControl) => { + const newName = c.value; + const valid = this.checkActionName(newName, this.widgetActionFormGroup.get('actionSourceId').value); + return !valid ? { + actionNameNotUnique: true + } : null; + }; + } + + private checkActionName(name: string, actionSourceId: string): boolean { + let actionNameIsUnique = true; + if (name && actionSourceId) { + const sourceActions = this.data.actionsData.actionsMap[actionSourceId]; + if (sourceActions) { + const result = sourceActions.filter((sourceAction) => sourceAction.name === name); + if (result && result.length && result[0].id !== this.action.id) { + actionNameIsUnique = false; + } + } + } + return actionNameIsUnique; + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + public actionSourceName(actionSource: WidgetActionSource): string { + if (actionSource) { + return this.utils.customTranslation(actionSource.name, actionSource.name); + } else { + return ''; + } + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + const type: WidgetActionType = this.widgetActionFormGroup.get('type').value; + let result: WidgetActionDescriptorInfo; + if (type === WidgetActionType.customPretty) { + result = {...this.widgetActionFormGroup.value, ...this.actionTypeFormGroup.get('customAction').value}; + } else { + result = {...this.widgetActionFormGroup.value, ...this.actionTypeFormGroup.value}; + } + result.id = this.action.id; + this.dialogRef.close(result); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html new file mode 100644 index 0000000000..8127b77e5d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html @@ -0,0 +1,53 @@ + +
+ +

{{ 'datakey.configuration' | translate }}

+ + +
+ + +
+
+ + +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.scss new file mode 100644 index 0000000000..5babc43782 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.scss @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .mat-dialog-content { + padding: 0 !important; + margin: 0 !important; + overflow: hidden; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.ts new file mode 100644 index 0000000000..c05f226fac --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.ts @@ -0,0 +1,85 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf, ViewChild } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { DataKey } from '@shared/models/widget.models'; +import { DataKeysCallbacks } from './data-keys.component.models'; +import { DataKeyConfigComponent } from '@home/components/widget/data-key-config.component'; + +export interface DataKeyConfigDialogData { + dataKey: DataKey; + dataKeySettingsSchema: any; + entityAliasId?: string; + showPostProcessing?: boolean; + callbacks?: DataKeysCallbacks; +} + +@Component({ + selector: 'tb-data-key-config-dialog', + templateUrl: './data-key-config-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: DataKeyConfigDialogComponent}], + styleUrls: ['./data-key-config-dialog.component.scss'] +}) +export class DataKeyConfigDialogComponent extends DialogComponent + implements OnInit, ErrorStateMatcher { + + @ViewChild('dataKeyConfig', {static: true}) dataKeyConfig: DataKeyConfigComponent; + + dataKeyFormGroup: FormGroup; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: DataKeyConfigDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + public fb: FormBuilder) { + super(store, router, dialogRef); + } + + ngOnInit(): void { + this.dataKeyFormGroup = this.fb.group({ + dataKey: [this.data.dataKey, [Validators.required]] + }); + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + this.dataKeyConfig.validateOnSubmit(); + if (this.dataKeyFormGroup.valid) { + const dataKey: DataKey = this.dataKeyFormGroup.get('dataKey').value; + this.dialogRef.close(dataKey); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html new file mode 100644 index 0000000000..a0a1850cff --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html @@ -0,0 +1,102 @@ + + + +
+ + {{ 'entity.key' | translate }} + + + + + + + + +
+ + datakey.label + + + + +
+
+ + datakey.units + + + + datakey.decimals + + +
+
+ datakey.data-generation-func +
+ + +
+
+ + {{ 'datakey.use-data-post-processing-func' | translate }} + + + + +
+
+
+ +
+ + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.scss new file mode 100644 index 0000000000..e41ad6dd5e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.scss @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../scss/constants'; + +:host { + .tb-datakey-config { + height: 100%; + + .settings{ + &> :first-child{ + padding-top: 12px; + } + &> :nth-child(n){ + padding-right: 12px; + padding-left: 12px; + } + } + } +} + +:host ::ng-deep { + .tb-datakey-config { + .mat-tab-body.mat-tab-body-active { + .mat-tab-body-content > div { + height: calc(65vh - 50px); + overflow: initial; + @media #{$mat-xs} { + height: 500px; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts new file mode 100644 index 0000000000..fde4b1a32f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts @@ -0,0 +1,261 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { DataKey } from '@shared/models/widget.models'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { MatDialog } from '@angular/material/dialog'; +import { EntityService } from '@core/http/entity.service'; +import { DataKeysCallbacks } from '@home/components/widget/data-keys.component.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { Observable, of } from 'rxjs'; +import { map, mergeMap, tap } from 'rxjs/operators'; +import { alarmFields } from '@shared/models/alarm.models'; +import { JsFuncComponent } from '@shared/components/js-func.component'; +import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models'; + +@Component({ + selector: 'tb-data-key-config', + templateUrl: './data-key-config.component.html', + styleUrls: ['./data-key-config.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DataKeyConfigComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DataKeyConfigComponent), + multi: true, + } + ] +}) +export class DataKeyConfigComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { + + dataKeyTypes = DataKeyType; + + @Input() + entityAliasId: string; + + @Input() + callbacks: DataKeysCallbacks; + + @Input() + dataKeySettingsSchema: any; + + @Input() + showPostProcessing = true; + + @ViewChild('keyInput') keyInput: ElementRef; + + @ViewChild('funcBodyEdit') funcBodyEdit: JsFuncComponent; + @ViewChild('postFuncBodyEdit') postFuncBodyEdit: JsFuncComponent; + + displayAdvanced = false; + + modelValue: DataKey; + + private propagateChange = null; + + public dataKeyFormGroup: FormGroup; + + public dataKeySettingsFormGroup: FormGroup; + + private dataKeySettingsData: JsonFormComponentData; + + private alarmKeys: Array; + + filteredKeys: Observable>; + private latestKeySearchResult: Array = null; + + keySearchText = ''; + + constructor(protected store: Store, + private utils: UtilsService, + private entityService: EntityService, + private dialog: MatDialog, + private translate: TranslateService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.alarmKeys = []; + for (const name of Object.keys(alarmFields)) { + this.alarmKeys.push({ + name, + type: DataKeyType.alarm + }); + } + if (this.dataKeySettingsSchema && this.dataKeySettingsSchema.schema) { + this.displayAdvanced = true; + this.dataKeySettingsData = { + schema: this.dataKeySettingsSchema.schema, + form: this.dataKeySettingsSchema.form || ['*'] + }; + this.dataKeySettingsFormGroup = this.fb.group({ + settings: [null, []] + }); + this.dataKeySettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + this.dataKeyFormGroup = this.fb.group({ + name: [null, []], + label: [null, [Validators.required]], + color: [null, [Validators.required]], + units: [null, []], + decimals: [null, [Validators.min(0), Validators.max(15), Validators.pattern(/^\d*$/)]], + funcBody: [null, []], + usePostProcessing: [null, []], + postFuncBody: [null, []] + }); + + this.dataKeyFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + + this.dataKeyFormGroup.get('usePostProcessing').valueChanges.subscribe((usePostProcessing: boolean) => { + const postFuncBody: string = this.dataKeyFormGroup.get('postFuncBody').value; + if (usePostProcessing && (!postFuncBody || !postFuncBody.length)) { + this.dataKeyFormGroup.get('postFuncBody').patchValue('return value;'); + } else if (!usePostProcessing && postFuncBody && postFuncBody.length) { + this.dataKeyFormGroup.get('postFuncBody').patchValue(null); + } + }); + + this.filteredKeys = this.dataKeyFormGroup.get('name').valueChanges + .pipe( + map(value => value ? value : ''), + mergeMap(name => this.fetchKeys(name) ) + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + } + + writeValue(value: DataKey): void { + this.modelValue = value; + if (this.modelValue.postFuncBody && this.modelValue.postFuncBody.length) { + this.modelValue.usePostProcessing = true; + } + this.dataKeyFormGroup.patchValue(this.modelValue, {emitEvent: false}); + this.dataKeyFormGroup.get('name').setValidators(this.modelValue.type !== DataKeyType.function ? [Validators.required] : []); + this.dataKeyFormGroup.get('name').updateValueAndValidity({emitEvent: false}); + if (this.displayAdvanced) { + this.dataKeySettingsData.model = this.modelValue.settings; + this.dataKeySettingsFormGroup.patchValue({ + settings: this.dataKeySettingsData + }, {emitEvent: false}); + } + } + + private updateModel() { + this.modelValue = {...this.modelValue, ...this.dataKeyFormGroup.value}; + if (this.displayAdvanced) { + this.modelValue.settings = this.dataKeySettingsFormGroup.get('settings').value.model; + } + if (this.modelValue.name) { + this.modelValue.name = this.modelValue.name.trim(); + } + this.propagateChange(this.modelValue); + } + + clearKey() { + this.dataKeyFormGroup.get('name').patchValue(null, {emitEvent: true}); + setTimeout(() => { + this.keyInput.nativeElement.blur(); + this.keyInput.nativeElement.focus(); + }, 0); + } + + private fetchKeys(searchText?: string): Observable> { + if (this.latestKeySearchResult === null || this.keySearchText !== searchText) { + this.keySearchText = searchText; + let fetchObservable: Observable> = null; + if (this.modelValue.type === DataKeyType.alarm) { + const dataKeyFilter = this.createDataKeyFilter(this.keySearchText); + fetchObservable = of(this.alarmKeys.filter(dataKeyFilter)); + } else { + if (this.entityAliasId) { + const dataKeyTypes = [this.modelValue.type]; + fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, this.keySearchText, dataKeyTypes); + } else { + fetchObservable = of([]); + } + } + return fetchObservable.pipe( + map((dataKeys) => dataKeys.map((dataKey) => dataKey.name)), + tap(res => this.latestKeySearchResult = res) + ); + } + return of(this.latestKeySearchResult); + } + + private createDataKeyFilter(query: string): (key: DataKey) => boolean { + const lowercaseQuery = query.toLowerCase(); + return key => key.name.toLowerCase().indexOf(lowercaseQuery) === 0; + } + + public validateOnSubmit() { + if (this.modelValue.type === DataKeyType.function) { + this.funcBodyEdit.validateOnSubmit(); + } else if ((this.modelValue.type === DataKeyType.timeseries || + this.modelValue.type === DataKeyType.attribute) && this.dataKeyFormGroup.get('usePostProcessing').value) { + this.postFuncBodyEdit.validateOnSubmit(); + } + } + + public validate(c: FormControl) { + if (!this.dataKeyFormGroup.valid) { + return { + dataKey: { + valid: false + } + }; + } + if (this.displayAdvanced && !this.dataKeySettingsFormGroup.valid) { + return { + dataKeySettings: { + valid: false + } + }; + } + return null; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.html b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.html new file mode 100644 index 0000000000..4ae9af977b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.html @@ -0,0 +1,172 @@ + + + + +
+
+ drag_handle +
+
+
+
+
+
+ + + notifications + + + + + + + + + timeline + + + {{key.label}} +
+
:
+
+ f({{key.name}}) + + {{key.name}} + +
+
+ + close +
+
+ +
+ + + + + + notifications + + + + + + + + + timeline + + + + + + +
+
+ entity.no-keys-found +
+ + + {{ translate.get('entity.no-key-matching', + {key: truncate.transform(searchText, true, 6, '...')}) | async }} + + + entity.create-new-key + + + {{'entity.create-new-key' | translate }} + + notifications + + + + + + + + + timeline + + + +
+
+
+ + {{ requiredText }} + +
+
+ {{ maxDataKeysText() }} +
diff --git a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.models.ts new file mode 100644 index 0000000000..452e390faa --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.models.ts @@ -0,0 +1,24 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { DataKey } from '@shared/models/widget.models'; +import { Observable } from 'rxjs'; + +export interface DataKeysCallbacks { + generateDataKey: (chip: any, type: DataKeyType) => DataKey; + fetchEntityKeys: (entityAliasId: string, query: string, types: Array) => Observable>; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.scss b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.scss new file mode 100644 index 0000000000..1a58e8c6f8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.scss @@ -0,0 +1,69 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .mat-chip.mat-standard-chip { + overflow: hidden; + + .tb-attribute-chip { + max-width: 100%; + color: rgb(66, 66, 66); + font-weight: normal; + font-size: 16px; + + .tb-chip-drag-handle { + cursor: move; + + mat-icon { + pointer-events: none; + } + } + + .tb-chip-labels { + display: flex; + flex-direction: row; + align-items: flex-end; + min-width: 0; + .tb-chip-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .tb-chip-separator { + white-space: pre; + } + } + + .mat-chip-remove.mat-icon { + width: 24px; + height: 24px; + font-size: 24px; + margin-left: 0; + color: inherit; + opacity: inherit; + } + } + } +} + +:host ::ng-deep { + .mat-form-field-flex { + padding-top: 0; + .mat-form-field-infix { + border-top: 0; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts new file mode 100644 index 0000000000..22c9663be8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts @@ -0,0 +1,456 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; +import { + AfterViewInit, + Component, + ElementRef, + forwardRef, + Input, + OnChanges, + OnInit, + SimpleChanges, + SkipSelf, + ViewChild +} from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + FormGroupDirective, + NG_VALUE_ACCESSOR, + NgForm, + Validators +} from '@angular/forms'; +import { Observable, of } from 'rxjs'; +import { filter, map, mergeMap, share, tap } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { MatAutocomplete } from '@angular/material/autocomplete'; +import { MatChipInputEvent, MatChipList } from '@angular/material/chips'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { DataKey, DatasourceType, widgetType } from '@shared/models/widget.models'; +import { IAliasController } from '@core/api/widget-api.models'; +import { DataKeysCallbacks } from './data-keys.component.models'; +import { alarmFields } from '@shared/models/alarm.models'; +import { UtilsService } from '@core/services/utils.service'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { TruncatePipe } from '@shared/pipe/truncate.pipe'; +import { DialogService } from '@core/services/dialog.service'; +import { MatDialog } from '@angular/material/dialog'; +import { + DataKeyConfigDialogComponent, + DataKeyConfigDialogData +} from '@home/components/widget/data-key-config-dialog.component'; +import { deepClone } from '@core/utils'; +import { MatChipDropEvent } from '@app/shared/components/mat-chip-draggable.directive'; + +@Component({ + selector: 'tb-data-keys', + templateUrl: './data-keys.component.html', + styleUrls: ['./data-keys.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DataKeysComponent), + multi: true + }, + { + provide: ErrorStateMatcher, + useExisting: DataKeysComponent + } + ] +}) +export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnChanges, ErrorStateMatcher { + + datasourceTypes = DatasourceType; + widgetTypes = widgetType; + dataKeyTypes = DataKeyType; + + keysListFormGroup: FormGroup; + + modelValue: Array | null; + + @Input() + widgetType: widgetType; + + @Input() + datasourceType: DatasourceType; + + @Input() + maxDataKeys: number; + + @Input() + optDataKeys: boolean; + + @Input() + aliasController: IAliasController; + + @Input() + datakeySettingsSchema: any; + + @Input() + callbacks: DataKeysCallbacks; + + @Input() + entityAliasId: string; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue || !this.optDataKeys; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @ViewChild('keyInput') keyInput: ElementRef; + @ViewChild('keyAutocomplete') matAutocomplete: MatAutocomplete; + @ViewChild('chipList') chipList: MatChipList; + + keys: Array = []; + filteredKeys: Observable>; + + separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; + + alarmKeys: Array; + functionTypeKeys: Array; + + dataKeyType: DataKeyType; + placeholder: string; + requiredText: string; + + searchText = ''; + private latestSearchTextResult: Array = null; + + private dirty = false; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public translate: TranslateService, + private utils: UtilsService, + private dialogs: DialogService, + private dialog: MatDialog, + private fb: FormBuilder, + public truncate: TruncatePipe) { + } + + updateValidators() { + this.keysListFormGroup.get('keys').setValidators(this.required ? [Validators.required] : []); + this.keysListFormGroup.get('keys').updateValueAndValidity(); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.keysListFormGroup = this.fb.group({ + keys: [null, this.required ? [Validators.required] : []], + key: [null] + }); + this.alarmKeys = []; + for (const name of Object.keys(alarmFields)) { + this.alarmKeys.push({ + name, + type: DataKeyType.alarm + }); + } + this.functionTypeKeys = []; + for (const type of this.utils.getPredefinedFunctionsList()) { + this.functionTypeKeys.push({ + name: type, + type: DataKeyType.function + }); + } + this.updateParams(); + this.filteredKeys = this.keysListFormGroup.get('key').valueChanges + .pipe( + tap((value: string | DataKey) => { + if (value && typeof value !== 'string') { + this.addFromChipValue(value); + } else if (value === null) { + this.clear(this.keyInput.nativeElement.value); + } + }), + filter((value) => typeof value === 'string'), + map((value) => value ? (typeof value === 'string' ? value : value.name) : ''), + mergeMap(name => this.fetchKeys(name) ), + share() + ); + } + + public maxDataKeysText(): string { + if (this.maxDataKeys !== null && this.maxDataKeys > -1) { + if (this.datasourceType === DatasourceType.function) { + return this.translate.instant('datakey.maximum-function-types', {count: this.maxDataKeys}); + } else { + return this.translate.instant('datakey.maximum-timeseries-or-attributes', {count: this.maxDataKeys}); + } + } else { + return ''; + } + } + + private updateParams() { + if (this.datasourceType === DatasourceType.function) { + this.dataKeyType = DataKeyType.function; + this.placeholder = this.translate.instant('datakey.function-types'); + this.requiredText = this.translate.instant('datakey.function-types-required'); + } else { + if (this.widgetType === widgetType.latest) { + this.dataKeyType = null; + this.requiredText = this.translate.instant('datakey.timeseries-or-attributes-required'); + } else if (this.widgetType === widgetType.alarm) { + this.dataKeyType = null; + this.requiredText = this.translate.instant('datakey.alarm-fields-timeseries-or-attributes-required'); + } else { + this.dataKeyType = DataKeyType.timeseries; + this.requiredText = this.translate.instant('datakey.timeseries-required'); + } + this.placeholder = ''; + } + } + + private reset() { + if (this.widgetType === widgetType.alarm) { + this.keys = this.utils.getDefaultAlarmDataKeys(); + } else { + this.keys = []; + } + this.keysListFormGroup.get('keys').setValue(this.keys); + this.modelValue = this.keys.length ? [...this.keys] : null; + if (this.keyInput) { + this.keyInput.nativeElement.value = ''; + } + this.keysListFormGroup.get('key').patchValue('', {emitEvent: false}); + this.propagateChange(this.modelValue); + this.dirty = true; + this.latestSearchTextResult = null; + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'entityAliasId') { + this.searchText = ''; + this.latestSearchTextResult = null; + this.dirty = true; + } else if (['widgetType', 'datasourceType'].includes(propName)) { + this.updateParams(); + setTimeout(() => { + this.reset(); + }, 1); + } else if (['required', 'optDataKeys'].includes('propName')) { + this.updateValidators(); + } + } + } + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = this.required && !this.modelValue; + return originalErrorState || customErrorState; + } + + ngAfterViewInit(): void {} + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.keysListFormGroup.disable({emitEvent: false}); + } else { + this.keysListFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: Array | null): void { + this.searchText = ''; + if (value != null && value.length) { + this.modelValue = [...value]; + this.keys = [...value]; + this.keysListFormGroup.get('keys').setValue(this.keys); + } else { + this.keys = []; + this.keysListFormGroup.get('keys').setValue(this.keys); + this.modelValue = null; + } + this.dirty = true; + } + + onFocus() { + if (this.dirty) { + this.keysListFormGroup.get('key').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + private addFromChipValue(chip: DataKey) { + const key = this.callbacks.generateDataKey(chip.name, chip.type); + this.addKey(key); + } + + addKey(key: DataKey): void { + if (!this.maxDataKeys || this.maxDataKeys < 0 || + !this.modelValue || this.modelValue.length < this.maxDataKeys) { + if (!this.modelValue) { + this.modelValue = []; + } + this.modelValue.push(key); + this.keys.push(key); + this.keysListFormGroup.get('keys').setValue(this.keys); + } + this.propagateChange(this.modelValue); + this.clear(); + } + + add(event: MatChipInputEvent): void { + const value = event.value; + if ((value || '').trim()) { + if (this.dataKeyType) { + this.addFromChipValue({name: value.trim(), type: this.dataKeyType}); + } + } + this.clear(); + } + + remove(key: DataKey) { + const index = this.keys.indexOf(key); + if (index >= 0) { + this.keys.splice(index, 1); + this.keysListFormGroup.get('keys').setValue(this.keys); + this.modelValue.splice(index, 1); + if (!this.modelValue.length) { + this.modelValue = null; + } + this.propagateChange(this.modelValue); + this.clear(); + } + } + + onChipDrop(event: MatChipDropEvent) { + const from = event.from; + const to = event.to; + this.keys.splice(to, 0, this.keys.splice(from, 1)[0]); + this.keysListFormGroup.get('keys').setValue(this.keys); + this.modelValue.splice(to, 0, this.modelValue.splice(from, 1)[0]); + this.propagateChange(this.modelValue); + } + + showColorPicker(key: DataKey) { + this.dialogs.colorPicker(key.color).subscribe( + (color) => { + if (color && key.color !== color) { + key.color = color; + this.propagateChange(this.modelValue); + } + } + ); + } + + editDataKey(key: DataKey, index: number) { + this.dialog.open(DataKeyConfigDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + dataKey: deepClone(key), + dataKeySettingsSchema: this.datakeySettingsSchema, + entityAliasId: this.entityAliasId, + showPostProcessing: this.widgetType !== widgetType.alarm, + callbacks: this.callbacks + } + }).afterClosed().subscribe((updatedDataKey) => { + if (updatedDataKey) { + this.keys[index] = updatedDataKey; + this.keysListFormGroup.get('keys').setValue(this.keys); + this.modelValue[index] = updatedDataKey; + this.propagateChange(this.modelValue); + } + }); + } + + createKey(name: string, dataKeyType: DataKeyType = this.dataKeyType) { + this.addFromChipValue({name: name ? name.trim() : '', type: dataKeyType}); + } + + displayKeyFn(key?: DataKey): string | undefined { + return key ? key.name : undefined; + } + + fetchKeys(searchText?: string): Observable> { + if (this.latestSearchTextResult === null || this.searchText !== searchText) { + this.searchText = searchText; + let fetchObservable: Observable> = null; + if (this.datasourceType === DatasourceType.function) { + const dataKeyFilter = this.createDataKeyFilter(this.searchText); + const targetKeysList = this.widgetType === widgetType.alarm ? this.alarmKeys : this.functionTypeKeys; + fetchObservable = of(targetKeysList.filter(dataKeyFilter)); + } else { + if (this.entityAliasId) { + const dataKeyTypes = [DataKeyType.timeseries]; + if (this.widgetType === widgetType.latest || this.widgetType === widgetType.alarm) { + dataKeyTypes.push(DataKeyType.attribute); + dataKeyTypes.push(DataKeyType.entityField); + if (this.widgetType === widgetType.alarm) { + dataKeyTypes.push(DataKeyType.alarm); + } + } + fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, this.searchText, dataKeyTypes); + } else { + fetchObservable = of([]); + } + } + return fetchObservable.pipe( + tap(res => this.latestSearchTextResult = res) + ); + } + return of(this.latestSearchTextResult); + } + + private createDataKeyFilter(query: string): (key: DataKey) => boolean { + const lowercaseQuery = query.toLowerCase(); + return key => key.name.toLowerCase().indexOf(lowercaseQuery) === 0; + } + + textIsNotEmpty(text: string): boolean { + return (text && text != null && text.length > 0) ? true : false; + } + + clear(value: string = '') { + this.keyInput.nativeElement.value = value; + this.keysListFormGroup.get('key').patchValue(value, {emitEvent: true}); + setTimeout(() => { + this.keyInput.nativeElement.blur(); + this.keyInput.nativeElement.focus(); + }, 0); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts b/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts new file mode 100644 index 0000000000..804579bc9b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts @@ -0,0 +1,85 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { + Component, + ComponentFactory, + ComponentRef, HostBinding, + Inject, + Injector, + OnDestroy, + ViewContainerRef +} from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { + CUSTOM_DIALOG_DATA, + CustomDialogComponent, + CustomDialogData +} from '@home/components/widget/dialog/custom-dialog.component'; + +export interface CustomDialogContainerData { + controller: (instance: CustomDialogComponent) => void; + data?: any; + customComponentFactory: ComponentFactory; +} + +@Component({ + selector: 'tb-custom-dialog-container-component', + template: '' +}) +export class CustomDialogContainerComponent extends DialogComponent implements OnDestroy { + + @HostBinding('style.height') height = '0px'; + + private readonly customComponentRef: ComponentRef; + + constructor(protected store: Store, + protected router: Router, + public viewContainerRef: ViewContainerRef, + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: CustomDialogContainerData) { + super(store, router, dialogRef); + let customDialogData: CustomDialogData = { + controller: this.data.controller + }; + if (this.data.data) { + customDialogData = {...customDialogData, ...this.data.data}; + } + const injector: Injector = Injector.create({ + providers: [{ + provide: CUSTOM_DIALOG_DATA, + useValue: customDialogData + }, + { + provide: MatDialogRef, + useValue: dialogRef + }] + }); + this.customComponentRef = this.viewContainerRef.createComponent(this.data.customComponentFactory, 0, injector); + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + if (this.customComponentRef) { + this.customComponentRef.destroy(); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.component.ts new file mode 100644 index 0000000000..2e609eb617 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.component.ts @@ -0,0 +1,47 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { MatDialogRef } from '@angular/material/dialog'; +import { Inject, InjectionToken } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { PageComponent } from '@shared/components/page.component'; +import { CustomDialogContainerComponent } from './custom-dialog-container.component'; +import { FormBuilder, Validators } from '@angular/forms'; + +export const CUSTOM_DIALOG_DATA = new InjectionToken('ConfigDialogData'); + +export interface CustomDialogData { + controller: (instance: CustomDialogComponent) => void; + [key: string]: any; +} + +export class CustomDialogComponent extends PageComponent { + + [key: string]: any; + + constructor(protected store: Store, + protected router: Router, + public dialogRef: MatDialogRef, + public fb: FormBuilder, + @Inject(CUSTOM_DIALOG_DATA) public data: CustomDialogData) { + super(store); + // @ts-ignore + this.validators = Validators; + this.data.controller(this); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts b/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts new file mode 100644 index 0000000000..b9e4513279 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts @@ -0,0 +1,82 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable, NgModule } from '@angular/core'; +import { Observable } from 'rxjs'; +import { MatDialog } from '@angular/material/dialog'; +import { TranslateService } from '@ngx-translate/core'; +import { AuthService } from '@core/auth/auth.service'; +import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { mergeMap, tap } from 'rxjs/operators'; +import { CustomDialogComponent } from './custom-dialog.component'; +import { + CustomDialogContainerComponent, + CustomDialogContainerData +} from '@home/components/widget/dialog/custom-dialog-container.component'; + +@Injectable() +export class CustomDialogService { + + constructor( + private translate: TranslateService, + private authService: AuthService, + private dynamicComponentFactoryService: DynamicComponentFactoryService, + public dialog: MatDialog + ) { + } + + customDialog(template: string, controller: (instance: CustomDialogComponent) => void, data?: any): Observable { + return this.dynamicComponentFactoryService.createDynamicComponentFactory( + class CustomDialogComponentInstance extends CustomDialogComponent {}, + template, + [SharedModule, CustomDialogModule]).pipe( + mergeMap((factory) => { + const dialogData: CustomDialogContainerData = { + controller, + customComponentFactory: factory, + data + }; + return this.dialog.open( + CustomDialogContainerComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: dialogData + }).afterClosed().pipe( + tap(() => { + this.dynamicComponentFactoryService.destroyDynamicComponentFactory(factory); + }) + ); + } + )); + } + +} + +@NgModule({ + declarations: + [ + ], + imports: [ + CommonModule, + SharedModule + ], + exports: [ + ] +}) +class CustomDialogModule { } diff --git a/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts new file mode 100644 index 0000000000..dc1a48c3c3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts @@ -0,0 +1,127 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { PageComponent } from '@shared/components/page.component'; +import { Inject, Injector, OnDestroy, OnInit, Directive } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { IDynamicWidgetComponent, WidgetContext } from '@home/models/widget-component.models'; +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { RafService } from '@core/services/raf.service'; +import { + NotificationHorizontalPosition, + NotificationType, + NotificationVerticalPosition +} from '@core/notification/notification.models'; +import { FormBuilder, Validators } from '@angular/forms'; +import { DeviceService } from '@core/http/device.service'; +import { AssetService } from '@core/http/asset.service'; +import { EntityViewService } from '@core/http/entity-view.service'; +import { CustomerService } from '@core/http/customer.service'; +import { DashboardService } from '@core/http/dashboard.service'; +import { UserService } from '@core/http/user.service'; +import { AttributeService } from '@core/http/attribute.service'; +import { EntityRelationService } from '@core/http/entity-relation.service'; +import { EntityService } from '@core/http/entity.service'; +import { DialogService } from '@core/services/dialog.service'; +import { CustomDialogService } from '@home/components/widget/dialog/custom-dialog.service'; +import { DatePipe } from '@angular/common'; +import { TranslateService } from '@ngx-translate/core'; +import { DomSanitizer } from '@angular/platform-browser'; +import { Router } from '@angular/router'; + +@Directive() +export class DynamicWidgetComponent extends PageComponent implements IDynamicWidgetComponent, OnInit, OnDestroy { + + executingRpcRequest: boolean; + rpcEnabled: boolean; + rpcErrorText: string; + rpcRejection: HttpErrorResponse; + + [key: string]: any; + + validators = Validators; + + constructor(@Inject(RafService) public raf: RafService, + @Inject(Store) protected store: Store, + @Inject(FormBuilder) public fb: FormBuilder, + @Inject(Injector) public readonly $injector: Injector, + @Inject('widgetContext') public readonly ctx: WidgetContext, + @Inject('errorMessages') public readonly errorMessages: string[]) { + super(store); + this.ctx.$injector = $injector; + this.ctx.deviceService = $injector.get(DeviceService); + this.ctx.assetService = $injector.get(AssetService); + this.ctx.entityViewService = $injector.get(EntityViewService); + this.ctx.customerService = $injector.get(CustomerService); + this.ctx.dashboardService = $injector.get(DashboardService); + this.ctx.userService = $injector.get(UserService); + this.ctx.attributeService = $injector.get(AttributeService); + this.ctx.entityRelationService = $injector.get(EntityRelationService); + this.ctx.entityService = $injector.get(EntityService); + this.ctx.dialogs = $injector.get(DialogService); + this.ctx.customDialog = $injector.get(CustomDialogService); + this.ctx.date = $injector.get(DatePipe); + this.ctx.translate = $injector.get(TranslateService); + this.ctx.http = $injector.get(HttpClient); + this.ctx.sanitizer = $injector.get(DomSanitizer); + this.ctx.router = $injector.get(Router); + + this.ctx.$scope = this; + if (this.ctx.defaultSubscription) { + this.executingRpcRequest = this.ctx.defaultSubscription.executingRpcRequest; + this.rpcEnabled = this.ctx.defaultSubscription.rpcEnabled; + this.rpcErrorText = this.ctx.defaultSubscription.rpcErrorText; + this.rpcRejection = this.ctx.defaultSubscription.rpcRejection; + } + } + + ngOnInit() { + + } + + ngOnDestroy(): void { + + } + + clearRpcError() { + if (this.widgetContext.defaultSubscription) { + this.widgetContext.defaultSubscription.clearRpcError(); + } + } + + showSuccessToast(message: string, duration: number = 1000, + verticalPosition: NotificationVerticalPosition = 'bottom', + horizontalPosition: NotificationHorizontalPosition = 'left', + target?: string) { + this.ctx.showSuccessToast(message, duration, verticalPosition, horizontalPosition, target); + } + + showErrorToast(message: string, + verticalPosition: NotificationVerticalPosition = 'bottom', + horizontalPosition: NotificationHorizontalPosition = 'left', + target?: string) { + this.ctx.showErrorToast(message, verticalPosition, horizontalPosition, target); + } + + showToast(type: NotificationType, message: string, duration: number, + verticalPosition: NotificationVerticalPosition = 'bottom', + horizontalPosition: NotificationHorizontalPosition = 'left', + target?: string) { + this.ctx.showToast(type, message, duration, verticalPosition, horizontalPosition, target); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/legend-config-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/legend-config-panel.component.html new file mode 100644 index 0000000000..28cabf7cd8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/legend-config-panel.component.html @@ -0,0 +1,60 @@ + +
+
+
+
+
+ + legend.direction + + + {{ legendDirectionTranslations.get(legendDirection[direction]) | translate }} + + + + + legend.position + + + {{ legendPositionTranslations.get(legendPosition[pos]) | translate }} + + + + + {{ 'legend.sort-legend' | translate }} + + + {{ 'legend.show-min' | translate }} + + + {{ 'legend.show-max' | translate }} + + + {{ 'legend.show-avg' | translate }} + + + {{ 'legend.show-total' | translate }} + +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/legend-config-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/legend-config-panel.component.scss new file mode 100644 index 0000000000..b5fd794b9a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/legend-config-panel.component.scss @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + width: 100%; + height: 100%; + form, + fieldset { + height: 100%; + } + + .mat-content { + overflow: hidden; + background-color: #fff; + } + + .mat-padding { + padding: 16px; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/legend-config-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/legend-config-panel.component.ts new file mode 100644 index 0000000000..05e515100e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/legend-config-panel.component.ts @@ -0,0 +1,105 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, InjectionToken, OnInit, ViewContainerRef } from '@angular/core'; +import { OverlayRef } from '@angular/cdk/overlay'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { + LegendConfig, + LegendDirection, + legendDirectionTranslationMap, + LegendPosition, + legendPositionTranslationMap +} from '@shared/models/widget.models'; + +export const LEGEND_CONFIG_PANEL_DATA = new InjectionToken('LegendConfigPanelData'); + +export interface LegendConfigPanelData { + legendConfig: LegendConfig; + legendConfigUpdated: (legendConfig: LegendConfig) => void; +} + +@Component({ + selector: 'tb-legend-config-panel', + templateUrl: './legend-config-panel.component.html', + styleUrls: ['./legend-config-panel.component.scss'] +}) +export class LegendConfigPanelComponent extends PageComponent implements OnInit { + + legendConfigForm: FormGroup; + + legendDirection = LegendDirection; + + legendDirections = Object.keys(LegendDirection); + + legendDirectionTranslations = legendDirectionTranslationMap; + + legendPosition = LegendPosition; + + legendPositions = Object.keys(LegendPosition); + + legendPositionTranslations = legendPositionTranslationMap; + + constructor(@Inject(LEGEND_CONFIG_PANEL_DATA) public data: LegendConfigPanelData, + public overlayRef: OverlayRef, + protected store: Store, + public fb: FormBuilder, + public viewContainerRef: ViewContainerRef) { + super(store); + } + + ngOnInit(): void { + this.legendConfigForm = this.fb.group({ + direction: [this.data.legendConfig.direction, []], + position: [this.data.legendConfig.position, []], + sortDataKeys: [this.data.legendConfig.sortDataKeys, []], + showMin: [this.data.legendConfig.showMin, []], + showMax: [this.data.legendConfig.showMax, []], + showAvg: [this.data.legendConfig.showAvg, []], + showTotal: [this.data.legendConfig.showTotal, []] + }); + this.legendConfigForm.get('direction').valueChanges.subscribe((direction: LegendDirection) => { + this.onDirectionChanged(direction); + }); + this.onDirectionChanged(this.data.legendConfig.direction); + this.legendConfigForm.valueChanges.subscribe(() => { + this.update(); + }); + } + + private onDirectionChanged(direction: LegendDirection) { + if (direction === LegendDirection.row) { + let position: LegendPosition = this.legendConfigForm.get('position').value; + if (position !== LegendPosition.bottom && position !== LegendPosition.top) { + position = LegendPosition.bottom; + } + this.legendConfigForm.patchValue( + { + position + }, {emitEvent: false} + ); + } + } + + update() { + const newLegendConfig: LegendConfig = this.legendConfigForm.value; + this.data.legendConfigUpdated(newLegendConfig); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/legend-config.component.html b/ui-ngx/src/app/modules/home/components/widget/legend-config.component.html new file mode 100644 index 0000000000..bbfda6dd0f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/legend-config.component.html @@ -0,0 +1,23 @@ + + diff --git a/ui-ngx/src/app/modules/home/components/widget/legend-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/legend-config.component.ts new file mode 100644 index 0000000000..ccdee87802 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/legend-config.component.ts @@ -0,0 +1,170 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Inject, Input, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { DOCUMENT } from '@angular/common'; +import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; +import { MediaBreakpoints } from '@shared/models/constants'; +import { BreakpointObserver } from '@angular/cdk/layout'; +import { WINDOW } from '@core/services/window.service'; +import { deepClone } from '@core/utils'; +import { LegendConfig } from '@shared/models/widget.models'; +import { + LEGEND_CONFIG_PANEL_DATA, + LegendConfigPanelComponent, + LegendConfigPanelData +} from '@home/components/widget/legend-config-panel.component'; + +// @dynamic +@Component({ + selector: 'tb-legend-config', + templateUrl: './legend-config.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => LegendConfigComponent), + multi: true + } + ] +}) +export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAccessor { + + @Input() disabled: boolean; + + @ViewChild('legendConfigPanelOrigin') legendConfigPanelOrigin: CdkOverlayOrigin; + + innerValue: LegendConfig; + + private propagateChange = (_: any) => {}; + + constructor(private overlay: Overlay, + public viewContainerRef: ViewContainerRef, + public breakpointObserver: BreakpointObserver, + @Inject(DOCUMENT) private document: Document, + @Inject(WINDOW) private window: Window) { + } + + ngOnInit(): void { + } + + ngOnDestroy(): void { + } + + openEditMode() { + if (this.disabled) { + return; + } + const isGtSm = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']); + const position = this.overlay.position(); + const config = new OverlayConfig({ + panelClass: 'tb-legend-config-panel', + backdropClass: 'cdk-overlay-transparent-backdrop', + hasBackdrop: isGtSm, + }); + if (isGtSm) { + config.minWidth = '220px'; + config.maxHeight = '300px'; + const panelHeight = 220; + const panelWidth = 220; + const el = this.legendConfigPanelOrigin.elementRef.nativeElement; + const offset = el.getBoundingClientRect(); + const scrollTop = this.window.pageYOffset || this.document.documentElement.scrollTop || this.document.body.scrollTop || 0; + const scrollLeft = this.window.pageXOffset || this.document.documentElement.scrollLeft || this.document.body.scrollLeft || 0; + const bottomY = offset.bottom - scrollTop; + const leftX = offset.left - scrollLeft; + let originX; + let originY; + let overlayX; + let overlayY; + const wHeight = this.document.documentElement.clientHeight; + const wWidth = this.document.documentElement.clientWidth; + if (bottomY + panelHeight > wHeight) { + originY = 'top'; + overlayY = 'bottom'; + } else { + originY = 'bottom'; + overlayY = 'top'; + } + if (leftX + panelWidth > wWidth) { + originX = 'end'; + overlayX = 'end'; + } else { + originX = 'start'; + overlayX = 'start'; + } + const connectedPosition: ConnectedPosition = { + originX, + originY, + overlayX, + overlayY + }; + config.positionStrategy = position.flexibleConnectedTo(this.legendConfigPanelOrigin.elementRef) + .withPositions([connectedPosition]); + } else { + config.minWidth = '100%'; + config.minHeight = '100%'; + config.positionStrategy = position.global().top('0%').left('0%') + .right('0%').bottom('0%'); + } + + const overlayRef = this.overlay.create(config); + + overlayRef.backdropClick().subscribe(() => { + overlayRef.dispose(); + }); + + const injector = this._createLegendConfigPanelInjector( + overlayRef, + { + legendConfig: deepClone(this.innerValue), + legendConfigUpdated: this.legendConfigUpdated.bind(this) + } + ); + + overlayRef.attach(new ComponentPortal(LegendConfigPanelComponent, this.viewContainerRef, injector)); + } + + private _createLegendConfigPanelInjector(overlayRef: OverlayRef, data: LegendConfigPanelData): PortalInjector { + const injectionTokens = new WeakMap([ + [LEGEND_CONFIG_PANEL_DATA, data], + [OverlayRef, overlayRef] + ]); + return new PortalInjector(this.viewContainerRef.injector, injectionTokens); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(obj: LegendConfig): void { + this.innerValue = obj; + } + + private legendConfigUpdated(legendConfig: LegendConfig) { + this.innerValue = legendConfig; + this.propagateChange(this.innerValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/legend.component.html b/ui-ngx/src/app/modules/home/components/widget/legend.component.html new file mode 100644 index 0000000000..272e5fae64 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/legend.component.html @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'legend.min' | translate }}{{ 'legend.max' | translate }}{{ 'legend.avg' | translate }}{{ 'legend.total' | translate }}
+ {{ legendKey.dataKey.label }} + {{ legendData.data[legendKey.dataIndex].min }}{{ legendData.data[legendKey.dataIndex].max }}{{ legendData.data[legendKey.dataIndex].avg }}{{ legendData.data[legendKey.dataIndex].total }}
+ + + {{ legendKey.dataKey.label }} + +
{{ 'legend.min' | translate }} + {{ legendData.data[legendKey.dataIndex].min }} +
{{ 'legend.max' | translate }} + {{ legendData.data[legendKey.dataIndex].max }} +
{{ 'legend.avg' | translate }} + {{ legendData.data[legendKey.dataIndex].avg }} +
{{ 'legend.total' | translate }} + {{ legendData.data[legendKey.dataIndex].total }} +
diff --git a/ui-ngx/src/app/modules/home/components/widget/legend.component.scss b/ui-ngx/src/app/modules/home/components/widget/legend.component.scss new file mode 100644 index 0000000000..3df8afb224 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/legend.component.scss @@ -0,0 +1,90 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + table.tb-legend { + font-size: 12px; + + &.tb-legend-column { + width: 100%; + } + + &.tb-legend-row { + width: auto; + margin-left: auto; + } + + .tb-legend-header, + .tb-legend-value { + text-align: right; + } + + .tb-legend-header { + th { + padding: 0 10px 1px 0; + color: rgb(255, 110, 64); + white-space: nowrap; + font-size: 12px; + } + } + + .tb-legend-keys { + td { + font-size: 12px; + } + td.tb-legend-label, + td.tb-legend-value { + padding: 2px 10px; + white-space: nowrap; + } + + .tb-legend-type { + min-width: 50px; + padding: 0 10px 1px 0; + font-weight: 700; + color: rgb(255, 110, 64); + text-align: right; + white-space: nowrap; + } + + .tb-legend-line { + display: inline-block; + width: 15px; + height: 3px; + vertical-align: middle; + } + + .tb-legend-label { + padding: 2px 10px; + text-align: left; + white-space: nowrap; + outline: none; + + &.tb-horizontal { + width: 95%; + } + + &.tb-hidden-label { + text-decoration: line-through; + opacity: .6; + } + } + + &.tb-row-direction { + display: inline-block; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/legend.component.ts b/ui-ngx/src/app/modules/home/components/widget/legend.component.ts new file mode 100644 index 0000000000..3691742406 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/legend.component.ts @@ -0,0 +1,70 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { LegendConfig, LegendData, LegendDirection, LegendKey, LegendPosition } from '@shared/models/widget.models'; + +@Component({ + selector: 'tb-legend', + templateUrl: './legend.component.html', + styleUrls: ['./legend.component.scss'] +}) +export class LegendComponent implements OnInit { + + @Input() + legendConfig: LegendConfig; + + @Input() + legendData: LegendData; + + @Output() + legendKeyHiddenChange = new EventEmitter(); + + displayHeader: boolean; + + isHorizontal: boolean; + + isRowDirection: boolean; + + ngOnInit(): void { + this.displayHeader = this.legendConfig.showMin === true || + this.legendConfig.showMax === true || + this.legendConfig.showAvg === true || + this.legendConfig.showTotal === true; + + this.isHorizontal = this.legendConfig.position === LegendPosition.bottom || + this.legendConfig.position === LegendPosition.top; + + this.isRowDirection = this.legendConfig.direction === LegendDirection.row; + } + + toggleHideData(index: number) { + const dataKey = this.legendData.keys.find(key => key.dataIndex === index).dataKey; + if (!dataKey.settings.disableDataHiding) { + dataKey.hidden = !dataKey.hidden; + this.legendKeyHiddenChange.emit(index); + } + } + + legendKeys(): LegendKey[] { + let keys = this.legendData.keys; + if (this.legendConfig.sortDataKeys) { + keys = this.legendData.keys.sort((key1, key2) => key1.dataKey.label.localeCompare(key2.dataKey.label)); + } + return keys.filter(legendKey => this.legendData.keys[legendKey.dataIndex].dataKey.inLegend); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.html new file mode 100644 index 0000000000..3a834621e6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.html @@ -0,0 +1,65 @@ + +
+ + alarm.alarm-status-list + + + {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} + + + + + alarm.alarm-severity-list + + + {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} + + + + + alarm.alarm-type-list + + + {{type}} + cancel + + + + +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.scss new file mode 100644 index 0000000000..c951de64ff --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.scss @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + width: 100%; + height: 100%; + min-width: 300px; + overflow: hidden; + background: #fff; + border-radius: 4px; + box-shadow: + 0 7px 8px -4px rgba(0, 0, 0, .2), + 0 13px 19px 2px rgba(0, 0, 0, .14), + 0 5px 24px 4px rgba(0, 0, 0, .12); + + .mat-content { + overflow: hidden; + background-color: #fff; + } + + .mat-padding { + padding: 16px; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.ts new file mode 100644 index 0000000000..6304c9fa0e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.ts @@ -0,0 +1,119 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, InjectionToken } from '@angular/core'; +import { + AlarmSearchStatus, + alarmSearchStatusTranslations, + AlarmSeverity, + alarmSeverityTranslations +} from '@shared/models/alarm.models'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { MatChipInputEvent } from '@angular/material/chips'; +import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; +import { OverlayRef } from '@angular/cdk/overlay'; + +export const ALARM_FILTER_PANEL_DATA = new InjectionToken('AlarmFilterPanelData'); + +export interface AlarmFilterPanelData { + statusList: AlarmSearchStatus[]; + severityList: AlarmSeverity[]; + typeList: string[]; +} + +@Component({ + selector: 'tb-alarm-filter-panel', + templateUrl: './alarm-filter-panel.component.html', + styleUrls: ['./alarm-filter-panel.component.scss'] +}) +export class AlarmFilterPanelComponent { + + readonly separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; + + alarmFilterFormGroup: FormGroup; + + result: AlarmFilterPanelData; + + alarmSearchStatuses = [AlarmSearchStatus.ACTIVE, + AlarmSearchStatus.CLEARED, + AlarmSearchStatus.ACK, + AlarmSearchStatus.UNACK]; + + alarmSearchStatusTranslationMap = alarmSearchStatusTranslations; + + alarmSeverities = Object.keys(AlarmSeverity); + alarmSeverityEnum = AlarmSeverity; + + alarmSeverityTranslationMap = alarmSeverityTranslations; + + constructor(@Inject(ALARM_FILTER_PANEL_DATA) + public data: AlarmFilterPanelData, + public overlayRef: OverlayRef, + private fb: FormBuilder) { + this.alarmFilterFormGroup = this.fb.group( + { + alarmStatusList: [this.data.statusList], + alarmSeverityList: [this.data.severityList], + alarmTypeList: [this.data.typeList] + } + ); + } + + public alarmTypeList(): string[] { + return this.alarmFilterFormGroup.get('alarmTypeList').value; + } + + public removeAlarmType(type: string): void { + const types: string[] = this.alarmFilterFormGroup.get('alarmTypeList').value; + const index = types.indexOf(type); + if (index >= 0) { + types.splice(index, 1); + this.alarmFilterFormGroup.get('alarmTypeList').setValue(types); + this.alarmFilterFormGroup.get('alarmTypeList').markAsDirty(); + } + } + + public addAlarmType(event: MatChipInputEvent): void { + const input = event.input; + const value = event.value; + + const types: string[] = this.alarmFilterFormGroup.get('alarmTypeList').value; + + if ((value || '').trim()) { + types.push(value.trim()); + this.alarmFilterFormGroup.get('alarmTypeList').setValue(types); + this.alarmFilterFormGroup.get('alarmTypeList').markAsDirty(); + } + + if (input) { + input.value = ''; + } + } + + update() { + this.result = { + statusList: this.alarmFilterFormGroup.get('alarmStatusList').value, + severityList: this.alarmFilterFormGroup.get('alarmSeverityList').value, + typeList: this.alarmFilterFormGroup.get('alarmTypeList').value + }; + this.overlayRef.dispose(); + } + + cancel() { + this.overlayRef.dispose(); + } +} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html new file mode 100644 index 0000000000..d269fbbe92 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html @@ -0,0 +1,145 @@ + +
+
+ +
+ + +   + + + +
+
+ +
+ + {{ translate.get('alarm.selected-alarms', + {count: alarmsDatasource.selection.selected.length}) | async }} + + + + +
+
+
+ + + + + + + + + + + + + + {{ column.title }} + + + + + + + + +
+ +
+
+ + + + +
+
+
+ + +
+ alarm.no-alarms-prompt + {{ 'common.loading' | translate }} +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.scss new file mode 100644 index 0000000000..7fe357b7c2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.scss @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + width: 100%; + height: 100%; + .tb-table-widget { + .table-container { + position: relative; + } + .mat-table { + .mat-row { + &.invisible { + visibility: hidden; + } + } + } + span.no-data-found { + position: absolute; + top: 60px; + bottom: 0; + left: 0; + right: 0; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts new file mode 100644 index 0000000000..3e4b37dc79 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts @@ -0,0 +1,1045 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + Component, + ElementRef, + EventEmitter, + Input, + NgZone, + OnInit, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { WidgetAction, WidgetContext } from '@home/models/widget-component.models'; +import { DataKey, WidgetActionDescriptor, WidgetConfig } from '@shared/models/widget.models'; +import { IWidgetSubscription } from '@core/api/widget-api.models'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { createLabelFromDatasource, deepClone, hashCode, isDefined, isNumber } from '@core/utils'; +import cssjs from '@core/css/css'; +import { sortItems } from '@shared/models/page/page-link'; +import { Direction } from '@shared/models/page/sort-order'; +import { CollectionViewer, DataSource, SelectionModel } from '@angular/cdk/collections'; +import { BehaviorSubject, forkJoin, fromEvent, merge, Observable } from 'rxjs'; +import { emptyPageData, PageData } from '@shared/models/page/page-data'; +import { entityTypeTranslations } from '@shared/models/entity-type.models'; +import { debounceTime, distinctUntilChanged, map, take, tap } from 'rxjs/operators'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort, SortDirection } from '@angular/material/sort'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import { + CellContentInfo, + CellStyleInfo, + constructTableCssString, + DisplayColumn, + EntityColumn, + entityDataSortOrderFromString, + findColumnByEntityKey, + findEntityKeyByColumnDef, + fromEntityColumnDef, + getAlarmValue, + getCellContentInfo, + getCellStyleInfo, + getColumnWidth, + TableWidgetDataKeySettings, + TableWidgetSettings, + widthStyle +} from '@home/components/widget/lib/table-widget.models'; +import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; +import { + DISPLAY_COLUMNS_PANEL_DATA, + DisplayColumnsPanelComponent, + DisplayColumnsPanelData +} from '@home/components/widget/lib/display-columns-panel.component'; +import { + AlarmDataInfo, + alarmFields, + AlarmSearchStatus, + alarmSeverityColors, + alarmSeverityTranslations, + AlarmStatus, + alarmStatusTranslations +} from '@shared/models/alarm.models'; +import { DatePipe } from '@angular/common'; +import { + AlarmDetailsDialogComponent, + AlarmDetailsDialogData +} from '@home/components/alarm/alarm-details-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { DialogService } from '@core/services/dialog.service'; +import { AlarmService } from '@core/http/alarm.service'; +import { + AlarmData, + AlarmDataPageLink, + dataKeyToEntityKey, + dataKeyTypeToEntityKeyType, + entityDataPageLinkSortDirection, + KeyFilter +} from '@app/shared/models/query/query.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { + ALARM_FILTER_PANEL_DATA, + AlarmFilterPanelComponent, + AlarmFilterPanelData +} from '@home/components/widget/lib/alarm-filter-panel.component'; +import { entityFields } from '@shared/models/entity.models'; + +interface AlarmsTableWidgetSettings extends TableWidgetSettings { + alarmsTitle: string; + enableSelection: boolean; + enableStatusFilter?: boolean; + enableFilter: boolean; + enableStickyAction: boolean; + displayDetails: boolean; + allowAcknowledgment: boolean; + allowClear: boolean; +} + +interface AlarmWidgetActionDescriptor extends WidgetActionDescriptor { + details?: boolean; + acknowledge?: boolean; + clear?: boolean; +} + +@Component({ + selector: 'tb-alarms-table-widget', + templateUrl: './alarms-table-widget.component.html', + styleUrls: ['./alarms-table-widget.component.scss', './table-widget.scss'] +}) +export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit { + + @Input() + ctx: WidgetContext; + + @ViewChild('searchInput') searchInputField: ElementRef; + @ViewChild(MatPaginator) paginator: MatPaginator; + @ViewChild(MatSort) sort: MatSort; + + public enableSelection = true; + public displayPagination = true; + public enableStickyAction = false; + public pageSizeOptions; + public pageLink: AlarmDataPageLink; + public sortOrderProperty: string; + public textSearchMode = false; + public columns: Array = []; + public displayedColumns: string[] = []; + public actionCellDescriptors: AlarmWidgetActionDescriptor[] = []; + public alarmsDatasource: AlarmsDatasource; + + private settings: AlarmsTableWidgetSettings; + private widgetConfig: WidgetConfig; + private subscription: IWidgetSubscription; + + private alarmsTitlePattern: string; + + private displayDetails = true; + private allowAcknowledgment = true; + private allowClear = true; + + private defaultPageSize = 10; + private defaultSortOrder = '-' + alarmFields.createdTime.value; + + private contentsInfo: {[key: string]: CellContentInfo} = {}; + private stylesInfo: {[key: string]: CellStyleInfo} = {}; + private columnWidth: {[key: string]: string} = {}; + + private searchAction: WidgetAction = { + name: 'action.search', + show: true, + icon: 'search', + onAction: () => { + this.enterFilterMode(); + } + }; + + private columnDisplayAction: WidgetAction = { + name: 'entity.columns-to-display', + show: true, + icon: 'view_column', + onAction: ($event) => { + this.editColumnsToDisplay($event); + } + }; + + private alarmFilterAction: WidgetAction = { + name: 'alarm.alarm-filter', + show: true, + onAction: ($event) => { + this.editAlarmFilter($event); + }, + icon: 'filter_list' + }; + + constructor(protected store: Store, + private elementRef: ElementRef, + private ngZone: NgZone, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, + private utils: UtilsService, + public translate: TranslateService, + private domSanitizer: DomSanitizer, + private datePipe: DatePipe, + private dialog: MatDialog, + private dialogService: DialogService, + private alarmService: AlarmService) { + super(store); + this.pageLink = { + page: 0, + pageSize: this.defaultPageSize, + textSearch: null + }; + } + + ngOnInit(): void { + this.ctx.$scope.alarmsTableWidget = this; + this.settings = this.ctx.settings; + this.widgetConfig = this.ctx.widgetConfig; + this.subscription = this.ctx.defaultSubscription; + this.initializeConfig(); + this.updateAlarmSource(); + this.ctx.updateWidgetParams(); + } + + ngAfterViewInit(): void { + fromEvent(this.searchInputField.nativeElement, 'keyup') + .pipe( + debounceTime(150), + distinctUntilChanged(), + tap(() => { + if (this.displayPagination) { + this.paginator.pageIndex = 0; + } + this.updateData(); + }) + ) + .subscribe(); + + if (this.displayPagination) { + this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); + } + ((this.displayPagination ? merge(this.sort.sortChange, this.paginator.page) : this.sort.sortChange) as Observable) + .pipe( + tap(() => this.updateData()) + ) + .subscribe(); + this.updateData(); + } + + public onDataUpdated() { + this.updateTitle(true); + this.alarmsDatasource.updateAlarms(); + } + + public pageLinkSortDirection(): SortDirection { + return entityDataPageLinkSortDirection(this.pageLink); + } + + private initializeConfig() { + this.ctx.widgetActions = [this.searchAction, this.alarmFilterAction, this.columnDisplayAction]; + + this.displayDetails = isDefined(this.settings.displayDetails) ? this.settings.displayDetails : true; + this.allowAcknowledgment = isDefined(this.settings.allowAcknowledgment) ? this.settings.allowAcknowledgment : true; + this.allowClear = isDefined(this.settings.allowClear) ? this.settings.allowClear : true; + + if (this.displayDetails) { + this.actionCellDescriptors.push( + { + displayName: this.translate.instant('alarm.details'), + icon: 'more_horiz', + details: true + } as AlarmWidgetActionDescriptor + ); + } + + if (this.allowAcknowledgment) { + this.actionCellDescriptors.push( + { + displayName: this.translate.instant('alarm.acknowledge'), + icon: 'done', + acknowledge: true + } as AlarmWidgetActionDescriptor + ); + } + + if (this.allowClear) { + this.actionCellDescriptors.push( + { + displayName: this.translate.instant('alarm.clear'), + icon: 'clear', + clear: true + } as AlarmWidgetActionDescriptor + ); + } + + this.actionCellDescriptors = this.actionCellDescriptors.concat(this.ctx.actionsApi.getActionDescriptors('actionCellButton')); + + if (this.settings.alarmsTitle && this.settings.alarmsTitle.length) { + this.alarmsTitlePattern = this.utils.customTranslation(this.settings.alarmsTitle, this.settings.alarmsTitle); + } else { + this.alarmsTitlePattern = this.translate.instant('alarm.alarms'); + } + + this.updateTitle(false); + + this.enableSelection = isDefined(this.settings.enableSelection) ? this.settings.enableSelection : true; + if (!this.allowAcknowledgment && !this.allowClear) { + this.enableSelection = false; + } + + this.searchAction.show = isDefined(this.settings.enableSearch) ? this.settings.enableSearch : true; + this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true; + this.enableStickyAction = isDefined(this.settings.enableStickyAction) ? this.settings.enableStickyAction : false; + this.columnDisplayAction.show = isDefined(this.settings.enableSelectColumnDisplay) ? this.settings.enableSelectColumnDisplay : true; + let enableFilter; + if (isDefined(this.settings.enableFilter)) { + enableFilter = this.settings.enableFilter; + } else if (isDefined(this.settings.enableStatusFilter)) { + enableFilter = this.settings.enableStatusFilter; + } else { + enableFilter = true; + } + this.alarmFilterAction.show = enableFilter; + + const pageSize = this.settings.defaultPageSize; + if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) { + this.defaultPageSize = pageSize; + } + this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3]; + this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : 1024; + + this.pageLink.searchPropagatedAlarms = isDefined(this.widgetConfig.searchPropagatedAlarms) + ? this.widgetConfig.searchPropagatedAlarms : true; + let alarmStatusList: AlarmSearchStatus[] = []; + if (isDefined(this.widgetConfig.alarmStatusList) && this.widgetConfig.alarmStatusList.length) { + alarmStatusList = this.widgetConfig.alarmStatusList; + } else if (isDefined(this.widgetConfig.alarmSearchStatus) && this.widgetConfig.alarmSearchStatus !== AlarmSearchStatus.ANY) { + alarmStatusList = [this.widgetConfig.alarmSearchStatus]; + } + this.pageLink.statusList = alarmStatusList; + this.pageLink.severityList = isDefined(this.widgetConfig.alarmSeverityList) ? this.widgetConfig.alarmSeverityList : []; + this.pageLink.typeList = isDefined(this.widgetConfig.alarmTypeList) ? this.widgetConfig.alarmTypeList : []; + + const cssString = constructTableCssString(this.widgetConfig); + const cssParser = new cssjs(); + cssParser.testMode = false; + const namespace = 'alarms-table-' + hashCode(cssString); + cssParser.cssPreviewNamespace = namespace; + cssParser.createStyleElement(namespace, cssString); + $(this.elementRef.nativeElement).addClass(namespace); + } + + private updateTitle(updateWidgetParams = false) { + const newTitle = createLabelFromDatasource(this.subscription.alarmSource, this.alarmsTitlePattern); + if (this.ctx.widgetTitle !== newTitle) { + this.ctx.widgetTitle = newTitle; + if (updateWidgetParams) { + this.ctx.updateWidgetParams(); + } + } + } + + private updateAlarmSource() { + + if (this.enableSelection) { + this.displayedColumns.push('select'); + } + + const latestDataKeys: Array = []; + + if (this.subscription.alarmSource) { + this.subscription.alarmSource.dataKeys.forEach((alarmDataKey) => { + const dataKey: EntityColumn = deepClone(alarmDataKey) as EntityColumn; + dataKey.entityKey = dataKeyToEntityKey(alarmDataKey); + dataKey.label = this.utils.customTranslation(dataKey.label, dataKey.label); + dataKey.title = dataKey.label; + dataKey.def = 'def' + this.columns.length; + const keySettings: TableWidgetDataKeySettings = dataKey.settings; + if (dataKey.type === DataKeyType.alarm && !isDefined(keySettings.columnWidth)) { + const alarmField = alarmFields[dataKey.name]; + if (alarmField && alarmField.time) { + keySettings.columnWidth = '120px'; + } + } + this.stylesInfo[dataKey.def] = getCellStyleInfo(keySettings); + this.contentsInfo[dataKey.def] = getCellContentInfo(keySettings, 'value, alarm, ctx'); + this.contentsInfo[dataKey.def].units = dataKey.units; + this.contentsInfo[dataKey.def].decimals = dataKey.decimals; + this.columnWidth[dataKey.def] = getColumnWidth(keySettings); + this.columns.push(dataKey); + + if (dataKey.type !== DataKeyType.alarm) { + latestDataKeys.push(dataKey); + } + }); + this.displayedColumns.push(...this.columns.map(column => column.def)); + } + if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) { + this.defaultSortOrder = this.utils.customTranslation(this.settings.defaultSortOrder, this.settings.defaultSortOrder); + } + this.pageLink.sortOrder = entityDataSortOrderFromString(this.defaultSortOrder, this.columns); + let sortColumn: EntityColumn; + if (this.pageLink.sortOrder) { + sortColumn = findColumnByEntityKey(this.pageLink.sortOrder.key, this.columns); + } + this.sortOrderProperty = sortColumn ? sortColumn.def : null; + + if (this.actionCellDescriptors.length) { + this.displayedColumns.push('actions'); + } + + this.alarmsDatasource = new AlarmsDatasource(this.subscription, latestDataKeys); + if (this.enableSelection) { + this.alarmsDatasource.selectionModeChanged$.subscribe((selectionMode) => { + const hideTitlePanel = selectionMode || this.textSearchMode; + if (this.ctx.hideTitlePanel !== hideTitlePanel) { + this.ctx.hideTitlePanel = hideTitlePanel; + this.ctx.detectChanges(true); + } else { + this.ctx.detectChanges(); + } + }); + } + } + + private editColumnsToDisplay($event: Event) { + if ($event) { + $event.stopPropagation(); + } + const target = $event.target || $event.srcElement || $event.currentTarget; + const config = new OverlayConfig(); + config.backdropClass = 'cdk-overlay-transparent-backdrop'; + config.hasBackdrop = true; + const connectedPosition: ConnectedPosition = { + originX: 'end', + originY: 'bottom', + overlayX: 'end', + overlayY: 'top' + }; + config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) + .withPositions([connectedPosition]); + + const overlayRef = this.overlay.create(config); + overlayRef.backdropClick().subscribe(() => { + overlayRef.dispose(); + }); + + const columns: DisplayColumn[] = this.columns.map(column => { + return { + title: column.title, + def: column.def, + display: this.displayedColumns.indexOf(column.def) > -1 + }; + }); + + const injectionTokens = new WeakMap([ + [DISPLAY_COLUMNS_PANEL_DATA, { + columns, + columnsUpdated: (newColumns) => { + this.displayedColumns = newColumns.filter(column => column.display).map(column => column.def); + if (this.enableSelection) { + this.displayedColumns.unshift('select'); + } + this.displayedColumns.push('actions'); + } + } as DisplayColumnsPanelData], + [OverlayRef, overlayRef] + ]); + const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens); + overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent, + this.viewContainerRef, injector)); + this.ctx.detectChanges(); + } + + private editAlarmFilter($event: Event) { + if ($event) { + $event.stopPropagation(); + } + const target = $event.target || $event.srcElement || $event.currentTarget; + const config = new OverlayConfig(); + config.backdropClass = 'cdk-overlay-transparent-backdrop'; + config.hasBackdrop = true; + const connectedPosition: ConnectedPosition = { + originX: 'end', + originY: 'bottom', + overlayX: 'end', + overlayY: 'top' + }; + config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) + .withPositions([connectedPosition]); + + const overlayRef = this.overlay.create(config); + overlayRef.backdropClick().subscribe(() => { + overlayRef.dispose(); + }); + const injectionTokens = new WeakMap([ + [ALARM_FILTER_PANEL_DATA, { + statusList: this.pageLink.statusList, + severityList: this.pageLink.severityList, + typeList: this.pageLink.typeList + } as AlarmFilterPanelData], + [OverlayRef, overlayRef] + ]); + const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens); + const componentRef = overlayRef.attach(new ComponentPortal(AlarmFilterPanelComponent, + this.viewContainerRef, injector)); + componentRef.onDestroy(() => { + if (componentRef.instance.result) { + const result = componentRef.instance.result; + this.pageLink.statusList = result.statusList; + this.pageLink.severityList = result.severityList; + this.pageLink.typeList = result.typeList; + this.updateData(); + } + }); + this.ctx.detectChanges(); + } + + private enterFilterMode() { + this.textSearchMode = true; + this.pageLink.textSearch = ''; + this.ctx.hideTitlePanel = true; + this.ctx.detectChanges(true); + setTimeout(() => { + this.searchInputField.nativeElement.focus(); + this.searchInputField.nativeElement.setSelectionRange(0, 0); + }, 10); + } + + exitFilterMode() { + this.textSearchMode = false; + this.pageLink.textSearch = null; + if (this.displayPagination) { + this.paginator.pageIndex = 0; + } + this.updateData(); + this.ctx.hideTitlePanel = false; + this.ctx.detectChanges(true); + } + + private updateData() { + if (this.displayPagination) { + this.pageLink.page = this.paginator.pageIndex; + this.pageLink.pageSize = this.paginator.pageSize; + } else { + this.pageLink.page = 0; + } + const key = findEntityKeyByColumnDef(this.sort.active, this.columns); + if (key) { + this.pageLink.sortOrder = { + key, + direction: Direction[this.sort.direction.toUpperCase()] + }; + } else { + this.pageLink.sortOrder = null; + } + const sortOrderLabel = fromEntityColumnDef(this.sort.active, this.columns); + const keyFilters: KeyFilter[] = null; // TODO: + this.alarmsDatasource.loadAlarms(this.pageLink, sortOrderLabel, keyFilters); + this.ctx.detectChanges(); + } + + public trackByColumnDef(index, column: EntityColumn) { + return column.def; + } + + public trackByRowIndex(index: number) { + return index; + } + + public headerStyle(key: EntityColumn): any { + const columnWidth = this.columnWidth[key.def]; + return widthStyle(columnWidth); + } + + public cellStyle(alarm: AlarmDataInfo, key: EntityColumn): any { + let style: any = {}; + if (alarm && key) { + const styleInfo = this.stylesInfo[key.def]; + const value = getAlarmValue(alarm, key); + if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { + try { + style = styleInfo.cellStyleFunction(value); + } catch (e) { + style = {}; + } + } else { + style = this.defaultStyle(key, value); + } + } + if (!style.width) { + const columnWidth = this.columnWidth[key.def]; + style = {...style, ...widthStyle(columnWidth)}; + } + return style; + } + + public cellContent(alarm: AlarmDataInfo, key: EntityColumn): SafeHtml { + if (alarm && key) { + const contentInfo = this.contentsInfo[key.def]; + const value = getAlarmValue(alarm, key); + let content = ''; + if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { + try { + content = contentInfo.cellContentFunction(value, alarm, this.ctx); + } catch (e) { + content = '' + value; + } + } else { + content = this.defaultContent(key, contentInfo, value); + } + + if (!isDefined(content)) { + return ''; + + } else { + switch (typeof content) { + case 'string': + return this.domSanitizer.bypassSecurityTrustHtml(content); + default: + return content; + } + } + + } else { + return ''; + } + } + + public onRowClick($event: Event, alarm: AlarmDataInfo) { + if ($event) { + $event.stopPropagation(); + } + this.alarmsDatasource.toggleCurrentAlarm(alarm); + const descriptors = this.ctx.actionsApi.getActionDescriptors('rowClick'); + if (descriptors.length) { + let entityId; + let entityName; + if (alarm && alarm.originator) { + entityId = alarm.originator; + entityName = alarm.originatorName; + } + this.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName, {alarm}); + } + } + + public onActionButtonClick($event: Event, alarm: AlarmDataInfo, actionDescriptor: AlarmWidgetActionDescriptor) { + if (actionDescriptor.details) { + this.openAlarmDetails($event, alarm); + } else if (actionDescriptor.acknowledge) { + this.ackAlarm($event, alarm); + } else if (actionDescriptor.clear) { + this.clearAlarm($event, alarm); + } else { + if ($event) { + $event.stopPropagation(); + } + let entityId; + let entityName; + if (alarm && alarm.originator) { + entityId = alarm.originator; + entityName = alarm.originatorName; + } + this.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName, {alarm}); + } + } + + public actionEnabled(alarm: AlarmDataInfo, actionDescriptor: AlarmWidgetActionDescriptor): boolean { + if (actionDescriptor.acknowledge) { + return (alarm.status === AlarmStatus.ACTIVE_UNACK || + alarm.status === AlarmStatus.CLEARED_UNACK); + } else if (actionDescriptor.clear) { + return (alarm.status === AlarmStatus.ACTIVE_ACK || + alarm.status === AlarmStatus.ACTIVE_UNACK); + } + return true; + } + + private openAlarmDetails($event: Event, alarm: AlarmDataInfo) { + if ($event) { + $event.stopPropagation(); + } + if (alarm && alarm.id && alarm.id.id !== NULL_UUID) { + this.dialog.open + (AlarmDetailsDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + alarmId: alarm.id.id, + allowAcknowledgment: this.allowAcknowledgment, + allowClear: this.allowClear, + displayDetails: true + } + }).afterClosed().subscribe( + (res) => { + if (res) { + this.subscription.update(); + } + } + ); + } + } + + private ackAlarm($event: Event, alarm: AlarmDataInfo) { + if ($event) { + $event.stopPropagation(); + } + if (alarm && alarm.id && alarm.id.id !== NULL_UUID) { + this.dialogService.confirm( + this.translate.instant('alarm.aknowledge-alarm-title'), + this.translate.instant('alarm.aknowledge-alarm-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes') + ).subscribe((res) => { + if (res) { + if (res) { + this.alarmService.ackAlarm(alarm.id.id).subscribe(() => { + this.subscription.update(); + }); + } + } + }); + } + } + + public ackAlarms($event: Event) { + if ($event) { + $event.stopPropagation(); + } + if (this.alarmsDatasource.selection.hasValue()) { + const alarmIds = this.alarmsDatasource.selection.selected.filter( + (alarmId) => alarmId !== NULL_UUID + ); + if (alarmIds.length) { + const title = this.translate.instant('alarm.aknowledge-alarms-title', {count: alarmIds.length}); + const content = this.translate.instant('alarm.aknowledge-alarms-text', {count: alarmIds.length}); + this.dialogService.confirm( + title, + content, + this.translate.instant('action.no'), + this.translate.instant('action.yes') + ).subscribe((res) => { + if (res) { + if (res) { + const tasks: Observable[] = []; + for (const alarmId of alarmIds) { + tasks.push(this.alarmService.ackAlarm(alarmId)); + } + forkJoin(tasks).subscribe(() => { + this.alarmsDatasource.clearSelection(); + this.subscription.update(); + }); + } + } + }); + } + } + } + + private clearAlarm($event: Event, alarm: AlarmDataInfo) { + if ($event) { + $event.stopPropagation(); + } + if (alarm && alarm.id && alarm.id.id !== NULL_UUID) { + this.dialogService.confirm( + this.translate.instant('alarm.clear-alarm-title'), + this.translate.instant('alarm.clear-alarm-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes') + ).subscribe((res) => { + if (res) { + if (res) { + this.alarmService.clearAlarm(alarm.id.id).subscribe(() => { + this.subscription.update(); + }); + } + } + }); + } + } + + public clearAlarms($event: Event) { + if ($event) { + $event.stopPropagation(); + } + if (this.alarmsDatasource.selection.hasValue()) { + const alarmIds = this.alarmsDatasource.selection.selected.filter( + (alarmId) => alarmId !== NULL_UUID + ); + if (alarmIds.length) { + const title = this.translate.instant('alarm.clear-alarms-title', {count: alarmIds.length}); + const content = this.translate.instant('alarm.clear-alarms-text', {count: alarmIds.length}); + this.dialogService.confirm( + title, + content, + this.translate.instant('action.no'), + this.translate.instant('action.yes') + ).subscribe((res) => { + if (res) { + if (res) { + const tasks: Observable[] = []; + for (const alarmId of alarmIds) { + tasks.push(this.alarmService.clearAlarm(alarmId)); + } + forkJoin(tasks).subscribe(() => { + this.alarmsDatasource.clearSelection(); + this.subscription.update(); + }); + } + } + }); + } + } + } + + private defaultContent(key: EntityColumn, contentInfo: CellContentInfo, value: any): any { + if (isDefined(value)) { + const alarmField = alarmFields[key.name]; + if (alarmField) { + if (alarmField.time) { + return value ? this.datePipe.transform(value, 'yyyy-MM-dd HH:mm:ss') : ''; + } else if (alarmField.value === alarmFields.severity.value) { + return this.translate.instant(alarmSeverityTranslations.get(value)); + } else if (alarmField.value === alarmFields.status.value) { + return this.translate.instant(alarmStatusTranslations.get(value)); + } else if (alarmField.value === alarmFields.originatorType.value) { + return this.translate.instant(entityTypeTranslations.get(value).type); + } else { + return value; + } + } + const entityField = entityFields[key.name]; + if (entityField) { + if (entityField.time) { + return this.datePipe.transform(value, 'yyyy-MM-dd HH:mm:ss'); + } + } + const decimals = (contentInfo.decimals || contentInfo.decimals === 0) ? contentInfo.decimals : this.ctx.widgetConfig.decimals; + const units = contentInfo.units || this.ctx.widgetConfig.units; + return this.ctx.utils.formatValue(value, decimals, units, true); + } else { + return ''; + } + } + + private defaultStyle(key: EntityColumn, value: any): any { + if (isDefined(value)) { + const alarmField = alarmFields[key.name]; + if (alarmField) { + if (alarmField.value === alarmFields.severity.value) { + return { + fontWeight: 'bold', + color: alarmSeverityColors.get(value) + }; + } else { + return {}; + } + } else { + return {}; + } + } else { + return {}; + } + } + + isSorting(column: EntityColumn): boolean { + return column.type === DataKeyType.alarm && column.name.startsWith('details.'); + } +} + +class AlarmsDatasource implements DataSource { + + private alarmsSubject = new BehaviorSubject([]); + private pageDataSubject = new BehaviorSubject>(emptyPageData()); + + public selection = new SelectionModel(true, [], false); + + private selectionModeChanged = new EventEmitter(); + + public selectionModeChanged$ = this.selectionModeChanged.asObservable(); + + private currentAlarm: AlarmDataInfo = null; + + public dataLoading = true; + + private appliedPageLink: AlarmDataPageLink; + private appliedSortOrderLabel: string; + + constructor(private subscription: IWidgetSubscription, + private dataKeys: Array) { + } + + connect(collectionViewer: CollectionViewer): Observable> { + return this.alarmsSubject.asObservable(); + } + + disconnect(collectionViewer: CollectionViewer): void { + this.alarmsSubject.complete(); + this.pageDataSubject.complete(); + } + + loadAlarms(pageLink: AlarmDataPageLink, sortOrderLabel: string, keyFilters: KeyFilter[]) { + this.dataLoading = true; + // this.clear(); + this.appliedPageLink = pageLink; + this.appliedSortOrderLabel = sortOrderLabel; + this.subscription.subscribeForAlarms(pageLink, keyFilters); + } + + private clear() { + if (this.selection.hasValue()) { + this.selection.clear(); + this.onSelectionModeChanged(false); + } + this.alarmsSubject.next([]); + this.pageDataSubject.next(emptyPageData()); + } + + updateAlarms() { + const subscriptionAlarms = this.subscription.alarms; + let alarms = new Array(); + subscriptionAlarms.data.forEach((alarmData) => { + alarms.push(this.alarmDataToInfo(alarmData)); + }); + if (this.appliedSortOrderLabel && this.appliedSortOrderLabel.length) { + const asc = this.appliedPageLink.sortOrder.direction === Direction.ASC; + alarms = alarms.sort((a, b) => sortItems(a, b, this.appliedSortOrderLabel, asc)); + } + if (this.selection.hasValue()) { + const alarmIds = alarms.map((alarm) => alarm.id.id); + const toRemove = this.selection.selected.filter(alarmId => alarmIds.indexOf(alarmId) === -1); + this.selection.deselect(...toRemove); + if (this.selection.isEmpty()) { + this.onSelectionModeChanged(false); + } + } + const alarmsPageData: PageData = { + data: alarms, + totalPages: subscriptionAlarms.totalPages, + totalElements: subscriptionAlarms.totalElements, + hasNext: subscriptionAlarms.hasNext + }; + this.alarmsSubject.next(alarms); + this.pageDataSubject.next(alarmsPageData); + this.dataLoading = false; + } + + private alarmDataToInfo(alarmData: AlarmData): AlarmDataInfo { + const alarm: AlarmDataInfo = deepClone(alarmData); + delete alarm.latest; + const latest = alarmData.latest; + this.dataKeys.forEach((dataKey, index) => { + const type = dataKeyTypeToEntityKeyType(dataKey.type); + let value = ''; + if (type) { + if (latest && latest[type]) { + const tsVal = latest[type][dataKey.name]; + if (tsVal) { + value = tsVal.value; + } + } + } + alarm[dataKey.label] = value; + }); + return alarm; + } + + isAllSelected(): Observable { + const numSelected = this.selection.selected.length; + return this.alarmsSubject.pipe( + map((alarms) => numSelected === alarms.length) + ); + } + + isEmpty(): Observable { + return this.alarmsSubject.pipe( + map((alarms) => !alarms.length) + ); + } + + total(): Observable { + return this.pageDataSubject.pipe( + map((pageData) => pageData.totalElements) + ); + } + + toggleSelection(alarm: AlarmDataInfo) { + const hasValue = this.selection.hasValue(); + this.selection.toggle(alarm.id.id); + if (hasValue !== this.selection.hasValue()) { + this.onSelectionModeChanged(this.selection.hasValue()); + } + } + + isSelected(alarm: AlarmDataInfo): boolean { + return this.selection.isSelected(alarm.id.id); + } + + clearSelection() { + if (this.selection.hasValue()) { + this.selection.clear(); + this.onSelectionModeChanged(false); + } + } + + masterToggle() { + this.alarmsSubject.pipe( + tap((alarms) => { + const numSelected = this.selection.selected.length; + if (numSelected === alarms.length) { + this.selection.clear(); + if (numSelected > 0) { + this.onSelectionModeChanged(false); + } + } else { + alarms.forEach(row => { + this.selection.select(row.id.id); + }); + if (numSelected === 0) { + this.onSelectionModeChanged(true); + } + } + }), + take(1) + ).subscribe(); + } + + public toggleCurrentAlarm(alarm: AlarmDataInfo): boolean { + if (this.currentAlarm !== alarm) { + this.currentAlarm = alarm; + return true; + } else { + return false; + } + } + + public isCurrentAlarm(alarm: AlarmDataInfo): boolean { + return (this.currentAlarm && alarm && this.currentAlarm.id && alarm.id) && + (this.currentAlarm.id.id === alarm.id.id); + } + + private onSelectionModeChanged(selectionMode: boolean) { + this.selectionModeChanged.emit(selectionMode); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-compass.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-compass.models.ts new file mode 100644 index 0000000000..564065e9be --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-compass.models.ts @@ -0,0 +1,357 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { JsonSettingsSchema } from '@shared/models/widget.models'; +import { FontSettings } from '@home/components/widget/lib/settings.models'; +import { AnimationRule, AnimationTarget } from '@home/components/widget/lib/analogue-gauge.models'; + +export interface AnalogueCompassSettings { + majorTicks: string[]; + minorTicks: number; + showStrokeTicks: boolean; + needleCircleSize: number; + showBorder: boolean; + borderOuterWidth: number; + colorPlate: string; + colorMajorTicks: string; + colorMinorTicks: string; + colorNeedle: string; + colorNeedleCircle: string; + colorBorder: string; + majorTickFont: FontSettings; + animation: boolean; + animationDuration: number; + animationRule: AnimationRule; + animationTarget: AnimationTarget; +} + +export const analogueCompassSettingsSchema: JsonSettingsSchema = { + schema: { + type: 'object', + title: 'Settings', + properties: { + majorTicks: { + title: 'Major ticks names', + type: 'array', + items: { + title: 'Tick name', + type: 'string' + } + }, + minorTicks: { + title: 'Minor ticks count', + type: 'number', + default: 22 + }, + showStrokeTicks: { + title: 'Show ticks stroke', + type: 'boolean', + default: false + }, + needleCircleSize: { + title: 'Needle circle size', + type: 'number', + default: 15 + }, + showBorder: { + title: 'Show border', + type: 'boolean', + default: true + }, + borderOuterWidth: { + title: 'Border width', + type: 'number', + default: 10 + }, + colorPlate: { + title: 'Plate color', + type: 'string', + default: '#222' + }, + colorMajorTicks: { + title: 'Major ticks color', + type: 'string', + default: '#f5f5f5' + }, + colorMinorTicks: { + title: 'Minor ticks color', + type: 'string', + default: '#ddd' + }, + colorNeedle: { + title: 'Needle color', + type: 'string', + default: '#f08080' + }, + colorNeedleCircle: { + title: 'Needle circle color', + type: 'string', + default: '#e8e8e8' + }, + colorBorder: { + title: 'Border color', + type: 'string', + default: '#ccc' + }, + majorTickFont: { + title: 'Major tick font', + type: 'object', + properties: { + family: { + title: 'Font family', + type: 'string', + default: 'Roboto' + }, + size: { + title: 'Size', + type: 'number', + default: 20 + }, + style: { + title: 'Style', + type: 'string', + default: 'normal' + }, + weight: { + title: 'Weight', + type: 'string', + default: '500' + }, + color: { + title: 'color', + type: 'string', + default: '#ccc' + } + } + }, + animation: { + title: 'Enable animation', + type: 'boolean', + default: true + }, + animationDuration: { + title: 'Animation duration', + type: 'number', + default: 500 + }, + animationRule: { + title: 'Animation rule', + type: 'string', + default: 'cycle' + }, + animationTarget: { + title: 'Animation target', + type: 'string', + default: 'needle' + } + }, + required: [] + }, + form: [ + { + key: 'majorTicks', + items: [ + 'majorTicks[]' + ] + }, + 'minorTicks', + 'showStrokeTicks', + 'needleCircleSize', + 'showBorder', + 'borderOuterWidth', + { + key: 'colorPlate', + type: 'color' + }, + { + key: 'colorMajorTicks', + type: 'color' + }, + { + key: 'colorMinorTicks', + type: 'color' + }, + { + key: 'colorNeedle', + type: 'color' + }, + { + key: 'colorNeedleCircle', + type: 'color' + }, + { + key: 'colorBorder', + type: 'color' + }, + { + key: 'majorTickFont', + items: [ + 'majorTickFont.family', + 'majorTickFont.size', + { + key: 'majorTickFont.style', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'normal', + label: 'Normal' + }, + { + value: 'italic', + label: 'Italic' + }, + { + value: 'oblique', + label: 'Oblique' + } + ] + }, + { + key: 'majorTickFont.weight', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'normal', + label: 'Normal' + }, + { + value: 'bold', + label: 'Bold' + }, + { + value: 'bolder', + label: 'Bolder' + }, + { + value: 'lighter', + label: 'Lighter' + }, + { + value: '100', + label: '100' + }, + { + value: '200', + label: '200' + }, + { + value: '300', + label: '300' + }, + { + value: '400', + label: '400' + }, + { + value: '500', + label: '500' + }, + { + value: '600', + label: '600' + }, + { + value: '700', + label: '700' + }, + { + value: '800', + label: '800' + }, + { + value: '900', + label: '900' + } + ] + }, + { + key: 'majorTickFont.color', + type: 'color' + } + ] + }, + 'animation', + 'animationDuration', + { + key: 'animationRule', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'linear', + label: 'Linear' + }, + { + value: 'quad', + label: 'Quad' + }, + { + value: 'quint', + label: 'Quint' + }, + { + value: 'cycle', + label: 'Cycle' + }, + { + value: 'bounce', + label: 'Bounce' + }, + { + value: 'elastic', + label: 'Elastic' + }, + { + value: 'dequad', + label: 'Dequad' + }, + { + value: 'dequint', + label: 'Dequint' + }, + { + value: 'decycle', + label: 'Decycle' + }, + { + value: 'debounce', + label: 'Debounce' + }, + { + value: 'delastic', + label: 'Delastic' + } + ] + }, + { + key: 'animationTarget', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'needle', + label: 'Needle' + }, + { + value: 'plate', + label: 'Plate' + } + ] + } + ] +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-compass.ts b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-compass.ts new file mode 100644 index 0000000000..d3a8db18a7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-compass.ts @@ -0,0 +1,108 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { WidgetContext } from '@home/models/widget-component.models'; +import * as CanvasGauges from 'canvas-gauges'; +import { + AnalogueCompassSettings, + analogueCompassSettingsSchema +} from '@home/components/widget/lib/analogue-compass.models'; +import { deepClone, isDefined } from '@core/utils'; +import { JsonSettingsSchema } from '@shared/models/widget.models'; +import { getFontFamily } from '@home/components/widget/lib/settings.models'; +import { TbBaseGauge } from '@home/components/widget/lib/analogue-gauge.models'; +import RadialGaugeOptions = CanvasGauges.RadialGaugeOptions; +import BaseGauge = CanvasGauges.BaseGauge; +import RadialGauge = CanvasGauges.RadialGauge; + +const analogueCompassSettingsSchemaValue = analogueCompassSettingsSchema; + +export class TbAnalogueCompass extends TbBaseGauge { + + static get settingsSchema(): JsonSettingsSchema { + return analogueCompassSettingsSchemaValue; + } + + constructor(ctx: WidgetContext, canvasId: string) { + super(ctx, canvasId); + } + + protected createGaugeOptions(gaugeElement: HTMLElement, settings: AnalogueCompassSettings): RadialGaugeOptions { + + const majorTicks = (settings.majorTicks && settings.majorTicks.length > 0) ? deepClone(settings.majorTicks) : + ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW']; + majorTicks.push(majorTicks[0]); + + return { + renderTo: gaugeElement, + + // Generic options + minValue: 0, + maxValue: 360, + majorTicks, + minorTicks: settings.minorTicks || 22, + ticksAngle: 360, + startAngle: 180, + strokeTicks: settings.showStrokeTicks || false, + highlights: [], + valueBox: false, + + // needle + needleCircleSize: settings.needleCircleSize || 15, + needleType: 'line', + needleStart: 75, + needleEnd: 99, + needleWidth: 3, + needleCircleOuter: false, + + // borders + borders: settings.showBorder || false, + borderInnerWidth: 0, + borderMiddleWidth: 0, + borderOuterWidth: settings.borderOuterWidth || 10, + borderShadowWidth: 0, + + // colors + colorPlate: settings.colorPlate || '#222', + colorMajorTicks: settings.colorMajorTicks || '#f5f5f5', + colorMinorTicks: settings.colorMinorTicks || '#ddd', + colorNeedle: settings.colorNeedle || '#f08080', + colorNeedleEnd: settings.colorNeedle || '#f08080', + colorNeedleCircleInner: settings.colorNeedleCircle || '#e8e8e8', + colorNeedleCircleInnerEnd: settings.colorNeedleCircle || '#e8e8e8', + colorBorderOuter: settings.colorBorder || '#ccc', + colorBorderOuterEnd: settings.colorBorder || '#ccc', + colorNeedleShadowDown: '#222', + + // fonts + fontNumbers: getFontFamily(settings.majorTickFont), + fontNumbersSize: settings.majorTickFont && settings.majorTickFont.size ? settings.majorTickFont.size : 20, + fontNumbersStyle: settings.majorTickFont && settings.majorTickFont.style ? settings.majorTickFont.style : 'normal', + fontNumbersWeight: settings.majorTickFont && settings.majorTickFont.weight ? settings.majorTickFont.weight : '500', + colorNumbers: settings.majorTickFont && settings.majorTickFont.color ? settings.majorTickFont.color : '#ccc', + + // animations + animation: settings.animation !== false && !this.ctx.isMobile, + animationDuration: (isDefined(settings.animationDuration) && settings.animationDuration !== null) ? settings.animationDuration : 500, + animationRule: settings.animationRule || 'cycle', + animationTarget: settings.animationTarget || 'needle' + }; + } + + protected createGauge(gaugeData: RadialGaugeOptions): BaseGauge { + return new RadialGauge(gaugeData); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts new file mode 100644 index 0000000000..7038a2f18c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts @@ -0,0 +1,1040 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import * as CanvasGauges from 'canvas-gauges'; +import { FontSettings, getFontFamily } from '@home/components/widget/lib/settings.models'; +import { JsonSettingsSchema } from '@shared/models/widget.models'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { isDefined } from '@core/utils'; +import * as tinycolor_ from 'tinycolor2'; +import Highlight = CanvasGauges.Highlight; +import BaseGauge = CanvasGauges.BaseGauge; +import GenericOptions = CanvasGauges.GenericOptions; + +const tinycolor = tinycolor_; + +export type AnimationRule = 'linear' | 'quad' | 'quint' | 'cycle' + | 'bounce' | 'elastic' | 'dequad' | 'dequint' + | 'decycle' | 'debounce' | 'delastic'; + +export type AnimationTarget = 'needle' | 'plate'; + +export interface AnalogueGaugeSettings { + minValue: number; + maxValue: number; + unitTitle: string; + showUnitTitle: boolean; + majorTicksCount: number; + minorTicks: number; + valueBox: boolean; + valueInt: number; + valueDec?: number; + units?: string; + defaultColor: string; + colorPlate: string; + colorMajorTicks: string; + colorMinorTicks: string; + colorNeedle: string; + colorNeedleEnd: string; + colorNeedleShadowUp: string; + colorNeedleShadowDown: string; + colorValueBoxRect: string; + colorValueBoxRectEnd: string; + colorValueBoxBackground: string; + colorValueBoxShadow: string; + highlights: Highlight[]; + highlightsWidth: number; + showBorder: boolean; + numbersFont: FontSettings; + titleFont: FontSettings; + unitsFont: FontSettings; + valueFont: FontSettings; + animation: boolean; + animationDuration: number; + animationRule: AnimationRule; +} + +export const analogueGaugeSettingsSchema: JsonSettingsSchema = { + schema: { + type: 'object', + title: 'Settings', + properties: { + minValue: { + title: 'Minimum value', + type: 'number', + default: 0 + }, + maxValue: { + title: 'Maximum value', + type: 'number', + default: 100 + }, + unitTitle: { + title: 'Unit title', + type: 'string', + default: null + }, + showUnitTitle: { + title: 'Show unit title', + type: 'boolean', + default: true + }, + majorTicksCount: { + title: 'Major ticks count', + type: 'number', + default: null + }, + minorTicks: { + title: 'Minor ticks count', + type: 'number', + default: 2 + }, + valueBox: { + title: 'Show value box', + type: 'boolean', + default: true + }, + valueInt: { + title: 'Digits count for integer part of value', + type: 'number', + default: 3 + }, + defaultColor: { + title: 'Default color', + type: 'string', + default: null + }, + colorPlate: { + title: 'Plate color', + type: 'string', + default: '#fff' + }, + colorMajorTicks: { + title: 'Major ticks color', + type: 'string', + default: '#444' + }, + colorMinorTicks: { + title: 'Minor ticks color', + type: 'string', + default: '#666' + }, + colorNeedle: { + title: 'Needle color', + type: 'string', + default: null + }, + colorNeedleEnd: { + title: 'Needle color - end gradient', + type: 'string', + default: null + }, + colorNeedleShadowUp: { + title: 'Upper half of the needle shadow color', + type: 'string', + default: 'rgba(2,255,255,0.2)' + }, + colorNeedleShadowDown: { + title: 'Drop shadow needle color.', + type: 'string', + default: 'rgba(188,143,143,0.45)' + }, + colorValueBoxRect: { + title: 'Value box rectangle stroke color', + type: 'string', + default: '#888' + }, + colorValueBoxRectEnd: { + title: 'Value box rectangle stroke color - end gradient', + type: 'string', + default: '#666' + }, + colorValueBoxBackground: { + title: 'Value box background color', + type: 'string', + default: '#babab2' + }, + colorValueBoxShadow: { + title: 'Value box shadow color', + type: 'string', + default: 'rgba(0,0,0,1)' + }, + highlights: { + title: 'Highlights', + type: 'array', + items: { + title: 'Highlight', + type: 'object', + properties: { + from: { + title: 'From', + type: 'number' + }, + to: { + title: 'To', + type: 'number' + }, + color: { + title: 'Color', + type: 'string' + } + } + } + }, + highlightsWidth: { + title: 'Highlights width', + type: 'number', + default: 15 + }, + showBorder: { + title: 'Show border', + type: 'boolean', + default: true + }, + numbersFont: { + title: 'Tick numbers font', + type: 'object', + properties: { + family: { + title: 'Font family', + type: 'string', + default: 'Roboto' + }, + size: { + title: 'Size', + type: 'number', + default: 18 + }, + style: { + title: 'Style', + type: 'string', + default: 'normal' + }, + weight: { + title: 'Weight', + type: 'string', + default: '500' + }, + color: { + title: 'color', + type: 'string', + default: null + } + } + }, + titleFont: { + title: 'Title text font', + type: 'object', + properties: { + family: { + title: 'Font family', + type: 'string', + default: 'Roboto' + }, + size: { + title: 'Size', + type: 'number', + default: 24 + }, + style: { + title: 'Style', + type: 'string', + default: 'normal' + }, + weight: { + title: 'Weight', + type: 'string', + default: '500' + }, + color: { + title: 'color', + type: 'string', + default: '#888' + } + } + }, + unitsFont: { + title: 'Units text font', + type: 'object', + properties: { + family: { + title: 'Font family', + type: 'string', + default: 'Roboto' + }, + size: { + title: 'Size', + type: 'number', + default: 22 + }, + style: { + title: 'Style', + type: 'string', + default: 'normal' + }, + weight: { + title: 'Weight', + type: 'string', + default: '500' + }, + color: { + title: 'color', + type: 'string', + default: '#888' + } + } + }, + valueFont: { + title: 'Value text font', + type: 'object', + properties: { + family: { + title: 'Font family', + type: 'string', + default: 'Roboto' + }, + size: { + title: 'Size', + type: 'number', + default: 40 + }, + style: { + title: 'Style', + type: 'string', + default: 'normal' + }, + weight: { + title: 'Weight', + type: 'string', + default: '500' + }, + color: { + title: 'color', + type: 'string', + default: '#444' + }, + shadowColor: { + title: 'Shadow color', + type: 'string', + default: 'rgba(0,0,0,0.3)' + } + } + }, + animation: { + title: 'Enable animation', + type: 'boolean', + default: true + }, + animationDuration: { + title: 'Animation duration', + type: 'number', + default: 500 + }, + animationRule: { + title: 'Animation rule', + type: 'string', + default: 'cycle' + } + }, + required: [] + }, + form: [ + 'minValue', + 'maxValue', + 'unitTitle', + 'showUnitTitle', + 'majorTicksCount', + 'minorTicks', + 'valueBox', + 'valueInt', + { + key: 'defaultColor', + type: 'color' + }, + { + key: 'colorPlate', + type: 'color' + }, + { + key: 'colorMajorTicks', + type: 'color' + }, + { + key: 'colorMinorTicks', + type: 'color' + }, + { + key: 'colorNeedle', + type: 'color' + }, + { + key: 'colorNeedleEnd', + type: 'color' + }, + { + key: 'colorNeedleShadowUp', + type: 'color' + }, + { + key: 'colorNeedleShadowDown', + type: 'color' + }, + { + key: 'colorValueBoxRect', + type: 'color' + }, + { + key: 'colorValueBoxRectEnd', + type: 'color' + }, + { + key: 'colorValueBoxBackground', + type: 'color' + }, + { + key: 'colorValueBoxShadow', + type: 'color' + }, + { + key: 'highlights', + items: [ + 'highlights[].from', + 'highlights[].to', + { + key: 'highlights[].color', + type: 'color' + } + ] + }, + 'highlightsWidth', + 'showBorder', + { + key: 'numbersFont', + items: [ + 'numbersFont.family', + 'numbersFont.size', + { + key: 'numbersFont.style', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'normal', + label: 'Normal' + }, + { + value: 'italic', + label: 'Italic' + }, + { + value: 'oblique', + label: 'Oblique' + } + ] + }, + { + key: 'numbersFont.weight', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'normal', + label: 'Normal' + }, + { + value: 'bold', + label: 'Bold' + }, + { + value: 'bolder', + label: 'Bolder' + }, + { + value: 'lighter', + label: 'Lighter' + }, + { + value: '100', + label: '100' + }, + { + value: '200', + label: '200' + }, + { + value: '300', + label: '300' + }, + { + value: '400', + label: '400' + }, + { + value: '500', + label: '500' + }, + { + value: '600', + label: '600' + }, + { + value: '700', + label: '700' + }, + { + value: '800', + label: '800' + }, + { + value: '900', + label: '900' + } + ] + }, + { + key: 'numbersFont.color', + type: 'color' + } + ] + }, + { + key: 'titleFont', + items: [ + 'titleFont.family', + 'titleFont.size', + { + key: 'titleFont.style', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'normal', + label: 'Normal' + }, + { + value: 'italic', + label: 'Italic' + }, + { + value: 'oblique', + label: 'Oblique' + } + ] + }, + { + key: 'titleFont.weight', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'normal', + label: 'Normal' + }, + { + value: 'bold', + label: 'Bold' + }, + { + value: 'bolder', + label: 'Bolder' + }, + { + value: 'lighter', + label: 'Lighter' + }, + { + value: '100', + label: '100' + }, + { + value: '200', + label: '200' + }, + { + value: '300', + label: '300' + }, + { + value: '400', + label: '400' + }, + { + value: '500', + label: '500' + }, + { + value: '600', + label: '600' + }, + { + value: '700', + label: '700' + }, + { + value: '800', + label: '800' + }, + { + value: '900', + label: '900' + } + ] + }, + { + key: 'titleFont.color', + type: 'color' + } + ] + }, + { + key: 'unitsFont', + items: [ + 'unitsFont.family', + 'unitsFont.size', + { + key: 'unitsFont.style', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'normal', + label: 'Normal' + }, + { + value: 'italic', + label: 'Italic' + }, + { + value: 'oblique', + label: 'Oblique' + } + ] + }, + { + key: 'unitsFont.weight', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'normal', + label: 'Normal' + }, + { + value: 'bold', + label: 'Bold' + }, + { + value: 'bolder', + label: 'Bolder' + }, + { + value: 'lighter', + label: 'Lighter' + }, + { + value: '100', + label: '100' + }, + { + value: '200', + label: '200' + }, + { + value: '300', + label: '300' + }, + { + value: '400', + label: '400' + }, + { + value: '500', + label: '500' + }, + { + value: '600', + label: '600' + }, + { + value: '700', + label: '700' + }, + { + value: '800', + label: '800' + }, + { + value: '900', + label: '900' + } + ] + }, + { + key: 'unitsFont.color', + type: 'color' + } + ] + }, + { + key: 'valueFont', + items: [ + 'valueFont.family', + 'valueFont.size', + { + key: 'valueFont.style', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'normal', + label: 'Normal' + }, + { + value: 'italic', + label: 'Italic' + }, + { + value: 'oblique', + label: 'Oblique' + } + ] + }, + { + key: 'valueFont.weight', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'normal', + label: 'Normal' + }, + { + value: 'bold', + label: 'Bold' + }, + { + value: 'bolder', + label: 'Bolder' + }, + { + value: 'lighter', + label: 'Lighter' + }, + { + value: '100', + label: '100' + }, + { + value: '200', + label: '200' + }, + { + value: '300', + label: '300' + }, + { + value: '400', + label: '400' + }, + { + value: '500', + label: '500' + }, + { + value: '600', + label: '600' + }, + { + value: '700', + label: '700' + }, + { + value: '800', + label: '800' + }, + { + value: '900', + label: '900' + } + ] + }, + { + key: 'valueFont.color', + type: 'color' + }, + { + key: 'valueFont.shadowColor', + type: 'color' + } + ] + }, + 'animation', + 'animationDuration', + { + key: 'animationRule', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'linear', + label: 'Linear' + }, + { + value: 'quad', + label: 'Quad' + }, + { + value: 'quint', + label: 'Quint' + }, + { + value: 'cycle', + label: 'Cycle' + }, + { + value: 'bounce', + label: 'Bounce' + }, + { + value: 'elastic', + label: 'Elastic' + }, + { + value: 'dequad', + label: 'Dequad' + }, + { + value: 'dequint', + label: 'Dequint' + }, + { + value: 'decycle', + label: 'Decycle' + }, + { + value: 'debounce', + label: 'Debounce' + }, + { + value: 'delastic', + label: 'Delastic' + } + ] + } + ] +}; + +export abstract class TbBaseGauge { + + private gauge: BaseGauge; + + protected constructor(protected ctx: WidgetContext, canvasId: string) { + const gaugeElement = $('#' + canvasId, ctx.$container)[0]; + const settings: S = ctx.settings; + const gaugeData: O = this.createGaugeOptions(gaugeElement, settings); + this.gauge = this.createGauge(gaugeData as O).draw(); + } + + protected abstract createGaugeOptions(gaugeElement: HTMLElement, settings: S): O; + + protected abstract createGauge(gaugeData: O): BaseGauge; + + update() { + if (this.ctx.data.length > 0) { + const cellData = this.ctx.data[0]; + if (cellData.data.length > 0) { + const tvPair = cellData.data[cellData.data.length - + 1]; + const value = tvPair[1]; + if (value !== this.gauge.value) { + this.gauge.value = value; + } + } + } + } + + mobileModeChanged() { + const animation = this.ctx.settings.animation !== false && !this.ctx.isMobile; + this.gauge.update({animation} as GenericOptions); + } + + resize() { + this.gauge.update({width: this.ctx.width, height: this.ctx.height} as GenericOptions); + } +} + +export abstract class TbAnalogueGauge extends TbBaseGauge { + + protected constructor(ctx: WidgetContext, canvasId: string) { + super(ctx, canvasId); + } + + protected createGaugeOptions(gaugeElement: HTMLElement, settings: S): O { + + const minValue = settings.minValue || 0; + const maxValue = settings.maxValue || 100; + + const dataKey = this.ctx.data[0].dataKey; + const keyColor = settings.defaultColor || dataKey.color; + + const majorTicksCount = settings.majorTicksCount || 10; + const total = maxValue - minValue; + let step = (total / majorTicksCount); + + const valueInt = settings.valueInt || 3; + + const valueDec = getValueDec(this.ctx, settings); + + step = parseFloat(parseFloat(step + '').toFixed(valueDec)); + + const majorTicks: number[] = []; + const highlights: Highlight[] = []; + let tick = minValue; + + while (tick <= maxValue) { + majorTicks.push(tick); + let nextTick = tick + step; + nextTick = parseFloat(parseFloat(nextTick + '').toFixed(valueDec)); + if (tick < maxValue) { + const highlightColor = tinycolor(keyColor); + const percent = (tick - minValue) / total; + highlightColor.setAlpha(percent); + const highlight: Highlight = { + from: tick, + to: nextTick, + color: highlightColor.toRgbString() + }; + highlights.push(highlight); + } + tick = nextTick; + } + + const colorNumbers = tinycolor(keyColor).darken(20).toRgbString(); + + const gaugeData: O = { + renderTo: gaugeElement, + + /* Generic options */ + + minValue, + maxValue, + majorTicks, + minorTicks: settings.minorTicks || 2, + units: getUnits(this.ctx, settings), + title: ((settings.showUnitTitle !== false) ? + (settings.unitTitle && settings.unitTitle.length > 0 ? + settings.unitTitle : dataKey.label) : ''), + + borders: settings.showBorder !== false, + borderShadowWidth: (settings.showBorder !== false) ? 3 : 0, + + // number formats + valueInt, + valueDec, + majorTicksInt: 1, + majorTicksDec: 0, + + valueBox: settings.valueBox !== false, + valueBoxStroke: 5, + valueBoxWidth: 0, + valueText: '', + valueTextShadow: true, + valueBoxBorderRadius: 2.5, + + // highlights + highlights: (settings.highlights && settings.highlights.length > 0) ? settings.highlights : highlights, + highlightsWidth: (isDefined(settings.highlightsWidth) && settings.highlightsWidth !== null) ? settings.highlightsWidth : 15, + + // fonts + fontNumbers: getFontFamily(settings.numbersFont), + fontTitle: getFontFamily(settings.titleFont), + fontUnits: getFontFamily(settings.unitsFont), + fontValue: getFontFamily(settings.valueFont), + + fontNumbersSize: settings.numbersFont && settings.numbersFont.size ? settings.numbersFont.size : 18, + fontTitleSize: settings.titleFont && settings.titleFont.size ? settings.titleFont.size : 24, + fontUnitsSize: settings.unitsFont && settings.unitsFont.size ? settings.unitsFont.size : 22, + fontValueSize: settings.valueFont && settings.valueFont.size ? settings.valueFont.size : 40, + + fontNumbersStyle: settings.numbersFont && settings.numbersFont.style ? settings.numbersFont.style : 'normal', + fontTitleStyle: settings.titleFont && settings.titleFont.style ? settings.titleFont.style : 'normal', + fontUnitsStyle: settings.unitsFont && settings.unitsFont.style ? settings.unitsFont.style : 'normal', + fontValueStyle: settings.valueFont && settings.valueFont.style ? settings.valueFont.style : 'normal', + + fontNumbersWeight: settings.numbersFont && settings.numbersFont.weight ? settings.numbersFont.weight : '500', + fontTitleWeight: settings.titleFont && settings.titleFont.weight ? settings.titleFont.weight : '500', + fontUnitsWeight: settings.unitsFont && settings.unitsFont.weight ? settings.unitsFont.weight : '500', + fontValueWeight: settings.valueFont && settings.valueFont.weight ? settings.valueFont.weight : '500', + + colorNumbers: settings.numbersFont && settings.numbersFont.color ? settings.numbersFont.color : colorNumbers, + colorTitle: settings.titleFont && settings.titleFont.color ? settings.titleFont.color : '#888', + colorUnits: settings.unitsFont && settings.unitsFont.color ? settings.unitsFont.color : '#888', + colorValueText: settings.valueFont && settings.valueFont.color ? settings.valueFont.color : '#444', + colorValueTextShadow: settings.valueFont && settings.valueFont.shadowColor ? settings.valueFont.shadowColor : 'rgba(0,0,0,0.3)', + + // colors + colorPlate: settings.colorPlate || '#fff', + colorMajorTicks: settings.colorMajorTicks || '#444', + colorMinorTicks: settings.colorMinorTicks || '#666', + colorNeedle: settings.colorNeedle || keyColor, + colorNeedleEnd: settings.colorNeedleEnd || keyColor, + + colorValueBoxRect: settings.colorValueBoxRect || '#888', + colorValueBoxRectEnd: settings.colorValueBoxRectEnd || '#666', + colorValueBoxBackground: settings.colorValueBoxBackground || '#babab2', + colorValueBoxShadow: settings.colorValueBoxShadow || 'rgba(0,0,0,1)', + colorNeedleShadowUp: settings.colorNeedleShadowUp || 'rgba(2,255,255,0.2)', + colorNeedleShadowDown: settings.colorNeedleShadowDown || 'rgba(188,143,143,0.45)', + + // animations + animation: settings.animation !== false && !this.ctx.isMobile, + animationDuration: (isDefined(settings.animationDuration) && settings.animationDuration !== null) ? settings.animationDuration : 500, + animationRule: settings.animationRule || 'cycle', + animatedValue: true + } as O; + + this.prepareGaugeOptions(settings, gaugeData); + return gaugeData; + } + + protected abstract prepareGaugeOptions(settings: S, gaugeData: O); + +} + +function getValueDec(ctx: WidgetContext, settings: AnalogueGaugeSettings): number { + let dataKey; + if (ctx.data && ctx.data[0]) { + dataKey = ctx.data[0].dataKey; + } + if (dataKey && isDefined(dataKey.decimals)) { + return dataKey.decimals; + } else { + return (isDefined(settings.valueDec) && settings.valueDec !== null) + ? settings.valueDec : ctx.decimals; + } +} + +function getUnits(ctx: WidgetContext, settings: AnalogueGaugeSettings): string { + let dataKey; + if (ctx.data && ctx.data[0]) { + dataKey = ctx.data[0].dataKey; + } + if (dataKey && dataKey.units && dataKey.units.length) { + return dataKey.units; + } else { + return isDefined(settings.units) && settings.units.length > 0 ? settings.units : ctx.units; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-linear-gauge.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-linear-gauge.models.ts new file mode 100644 index 0000000000..bbbc751f0d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-linear-gauge.models.ts @@ -0,0 +1,88 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { JsonSettingsSchema } from '@shared/models/widget.models'; +import { AnalogueGaugeSettings, analogueGaugeSettingsSchema } from '@home/components/widget/lib/analogue-gauge.models'; +import { deepClone } from '@core/utils'; + +export interface AnalogueLinearGaugeSettings extends AnalogueGaugeSettings { + barStrokeWidth: number; + colorBarStroke: string; + colorBar: string; + colorBarEnd: string; + colorBarProgress: string; + colorBarProgressEnd: string; +} + +export function getAnalogueLinearGaugeSettingsSchema(): JsonSettingsSchema { + const analogueLinearGaugeSettingsSchema = deepClone(analogueGaugeSettingsSchema); + analogueLinearGaugeSettingsSchema.schema.properties = + {...analogueLinearGaugeSettingsSchema.schema.properties, ...{ + barStrokeWidth: { + title: 'Bar stroke width', + type: 'number', + default: 2.5 + }, + colorBarStroke: { + title: 'Bar stroke color', + type: 'string', + default: null + }, + colorBar: { + title: 'Bar background color', + type: 'string', + default: '#fff' + }, + colorBarEnd: { + title: 'Bar background color - end gradient', + type: 'string', + default: '#ddd' + }, + colorBarProgress: { + title: 'Progress bar color', + type: 'string', + default: null + }, + colorBarProgressEnd: { + title: 'Progress bar color - end gradient', + type: 'string', + default: null + }}}; + analogueLinearGaugeSettingsSchema.form.unshift( + 'barStrokeWidth', + { + key: 'colorBarStroke', + type: 'color' + }, + { + key: 'colorBar', + type: 'color' + }, + { + key: 'colorBarEnd', + type: 'color' + }, + { + key: 'colorBarProgress', + type: 'color' + }, + { + key: 'colorBarProgressEnd', + type: 'color' + } + ); + return analogueLinearGaugeSettingsSchema; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-linear-gauge.ts b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-linear-gauge.ts new file mode 100644 index 0000000000..8307069c86 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-linear-gauge.ts @@ -0,0 +1,65 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import * as CanvasGauges from 'canvas-gauges'; +import { JsonSettingsSchema } from '@shared/models/widget.models'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { TbAnalogueGauge } from '@home/components/widget/lib/analogue-gauge.models'; +import { + AnalogueLinearGaugeSettings, + getAnalogueLinearGaugeSettingsSchema +} from '@home/components/widget/lib/analogue-linear-gauge.models'; +import { isDefined } from '@core/utils'; +import * as tinycolor_ from 'tinycolor2'; +import LinearGaugeOptions = CanvasGauges.LinearGaugeOptions; +import LinearGauge = CanvasGauges.LinearGauge; +import BaseGauge = CanvasGauges.BaseGauge; + +const tinycolor = tinycolor_; + +const analogueLinearGaugeSettingsSchemaValue = getAnalogueLinearGaugeSettingsSchema(); + +export class TbAnalogueLinearGauge extends TbAnalogueGauge{ + + static get settingsSchema(): JsonSettingsSchema { + return analogueLinearGaugeSettingsSchemaValue; + } + + constructor(ctx: WidgetContext, canvasId: string) { + super(ctx, canvasId); + } + + protected prepareGaugeOptions(settings: AnalogueLinearGaugeSettings, gaugeData: LinearGaugeOptions) { + const dataKey = this.ctx.data[0].dataKey; + const keyColor = settings.defaultColor || dataKey.color; + + const barStrokeColor = tinycolor(keyColor).darken().setAlpha(0.6).toRgbString(); + const progressColorStart = tinycolor(keyColor).setAlpha(0.05).toRgbString(); + const progressColorEnd = tinycolor(keyColor).darken().toRgbString(); + + gaugeData.barStrokeWidth = (isDefined(settings.barStrokeWidth) && settings.barStrokeWidth !== null) ? settings.barStrokeWidth : 2.5; + gaugeData.colorBarStroke = settings.colorBarStroke || barStrokeColor; + gaugeData.colorBar = settings.colorBar || '#fff'; + gaugeData.colorBarEnd = settings.colorBarEnd || '#ddd'; + gaugeData.colorBarProgress = settings.colorBarProgress || progressColorStart; + gaugeData.colorBarProgressEnd = settings.colorBarProgressEnd || progressColorEnd; + } + + protected createGauge(gaugeData: LinearGaugeOptions): BaseGauge { + return new LinearGauge(gaugeData); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-radial-gauge.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-radial-gauge.models.ts new file mode 100644 index 0000000000..6b7a65e7e8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-radial-gauge.models.ts @@ -0,0 +1,52 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { JsonSettingsSchema } from '@shared/models/widget.models'; +import { AnalogueGaugeSettings, analogueGaugeSettingsSchema } from '@home/components/widget/lib/analogue-gauge.models'; +import { deepClone } from '@core/utils'; + +export interface AnalogueRadialGaugeSettings extends AnalogueGaugeSettings { + startAngle: number; + ticksAngle: number; + needleCircleSize: number; +} + +export function getAnalogueRadialGaugeSettingsSchema(): JsonSettingsSchema { + const analogueRadialGaugeSettingsSchema = deepClone(analogueGaugeSettingsSchema); + analogueRadialGaugeSettingsSchema.schema.properties = + {...analogueRadialGaugeSettingsSchema.schema.properties, ...{ + startAngle: { + title: 'Start ticks angle', + type: 'number', + default: 45 + }, + ticksAngle: { + title: 'Ticks angle', + type: 'number', + default: 270 + }, + needleCircleSize: { + title: 'Needle circle size', + type: 'number', + default: 10 + }}}; + analogueRadialGaugeSettingsSchema.form.unshift( + 'startAngle', + 'ticksAngle', + 'needleCircleSize' + ); + return analogueRadialGaugeSettingsSchema; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-radial-gauge.ts b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-radial-gauge.ts new file mode 100644 index 0000000000..05c1969657 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-radial-gauge.ts @@ -0,0 +1,65 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import * as CanvasGauges from 'canvas-gauges'; +import { + AnalogueRadialGaugeSettings, + getAnalogueRadialGaugeSettingsSchema +} from '@home/components/widget/lib/analogue-radial-gauge.models'; +import { JsonSettingsSchema } from '@shared/models/widget.models'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { TbAnalogueGauge } from '@home/components/widget/lib/analogue-gauge.models'; +import RadialGauge = CanvasGauges.RadialGauge; +import RadialGaugeOptions = CanvasGauges.RadialGaugeOptions; +import BaseGauge = CanvasGauges.BaseGauge; + +const analogueRadialGaugeSettingsSchemaValue = getAnalogueRadialGaugeSettingsSchema(); + +export class TbAnalogueRadialGauge extends TbAnalogueGauge{ + + static get settingsSchema(): JsonSettingsSchema { + return analogueRadialGaugeSettingsSchemaValue; + } + + constructor(ctx: WidgetContext, canvasId: string) { + super(ctx, canvasId); + } + + protected prepareGaugeOptions(settings: AnalogueRadialGaugeSettings, gaugeData: RadialGaugeOptions) { + gaugeData.ticksAngle = settings.ticksAngle || 270; + gaugeData.startAngle = settings.startAngle || 45; + + // colors + + gaugeData.colorNeedleCircleOuter = '#f0f0f0'; + gaugeData.colorNeedleCircleOuterEnd = '#ccc'; + gaugeData.colorNeedleCircleInner = '#e8e8e8'; // tinycolor(keyColor).lighten(30).toRgbString(),//'#e8e8e8', + gaugeData.colorNeedleCircleInnerEnd = '#f5f5f5'; + + // needle + gaugeData.needleCircleSize = settings.needleCircleSize || 10; + gaugeData.needleCircleInner = true; + gaugeData.needleCircleOuter = true; + + // custom animations + gaugeData.animationTarget = 'needle'; // 'needle' or 'plate' + } + + protected createGauge(gaugeData: RadialGaugeOptions): BaseGauge { + return new RadialGauge(gaugeData); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts b/ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts new file mode 100644 index 0000000000..82c0904a6c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts @@ -0,0 +1,1022 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import * as CanvasGauges from 'canvas-gauges'; +import { FontStyle, FontWeight } from '@home/components/widget/lib/settings.models'; +import * as tinycolor_ from 'tinycolor2'; +import { ColorFormats } from 'tinycolor2'; +import { isDefined, isDefinedAndNotNull, isString, isUndefined, padValue } from '@core/utils'; +import GenericOptions = CanvasGauges.GenericOptions; +import BaseGauge = CanvasGauges.BaseGauge; + +const tinycolor = tinycolor_; + +export type GaugeType = 'arc' | 'donut' | 'horizontalBar' | 'verticalBar'; + +export interface DigitalGaugeColorRange { + pct: number; + color: ColorFormats.RGBA; + rgbString: string; +} + +export interface ColorLevelSetting { + value: number; + color: string; +} + +export type levelColors = Array; + +export interface CanvasDigitalGaugeOptions extends GenericOptions { + gaugeType?: GaugeType; + gaugeWithScale?: number; + dashThickness?: number; + roundedLineCap?: boolean; + gaugeColor?: string; + levelColors?: levelColors; + symbol?: string; + label?: string; + hideValue?: boolean; + hideMinMax?: boolean; + fontTitle?: string; + fontValue?: string; + fontMinMaxSize?: number; + fontMinMaxStyle?: FontStyle; + fontMinMaxWeight?: FontWeight; + colorMinMax?: string; + fontMinMax?: string; + fontLabelSize?: number; + fontLabelStyle?: FontStyle; + fontLabelWeight?: FontWeight; + colorLabel?: string; + colorValue?: string; + fontLabel?: string; + neonGlowBrightness?: number; + isMobile?: boolean; + donutStartAngle?: number; + donutEndAngle?: number; + + colorsRange?: DigitalGaugeColorRange[]; + neonColorsRange?: DigitalGaugeColorRange[]; + neonColorTitle?: string; + neonColorLabel?: string; + neonColorValue?: string; + neonColorMinMax?: string; + timestamp?: number; + gaugeWidthScale?: number; + fontTitleHeight?: FontHeightInfo; + fontLabelHeight?: FontHeightInfo; + fontValueHeight?: FontHeightInfo; + fontMinMaxHeight?: FontHeightInfo; + + ticksValue?: number[]; + ticks?: number[]; + colorTicks?: string; + tickWidth?: number; + + showTimestamp?: boolean; +} + +const defaultDigitalGaugeOptions: CanvasDigitalGaugeOptions = { ...GenericOptions, + ...{ + gaugeType: 'arc', + gaugeWithScale: 0.75, + dashThickness: 0, + roundedLineCap: false, + + gaugeColor: '#777', + levelColors: ['blue'], + + symbol: '', + label: '', + hideValue: false, + hideMinMax: false, + + fontTitle: 'Roboto', + + fontValue: 'Roboto', + + fontMinMaxSize: 10, + fontMinMaxStyle: 'normal', + fontMinMaxWeight: '500', + colorMinMax: '#eee', + fontMinMax: 'Roboto', + + fontLabelSize: 8, + fontLabelStyle: 'normal', + fontLabelWeight: '500', + colorLabel: '#eee', + fontLabel: 'Roboto', + + neonGlowBrightness: 0, + + colorTicks: 'gray', + tickWidth: 4, + ticks: [], + + isMobile: false + } +}; + +BaseGauge.initialize('CanvasDigitalGauge', defaultDigitalGaugeOptions); + +interface HTMLCanvasElementClone extends HTMLCanvasElement { + initialized?: boolean; + renderedTimestamp?: number; + renderedValue?: number; + renderedProgress?: string; +} + +interface DigitalGaugeCanvasRenderingContext2D extends CanvasRenderingContext2D { + barDimensions?: BarDimensions; + currentColor?: string; +} + +interface BarDimensions { + baseX: number; + baseY: number; + width: number; + height: number; + origBaseX?: number; + origBaseY?: number; + fontSizeFactor?: number; + Ro?: number; + Cy?: number; + titleY?: number; + titleBottom?: number; + Ri?: number; + Cx?: number; + strokeWidth?: number; + Rm?: number; + fontValueBaseline?: CanvasTextBaseline; + fontMinMaxBaseline?: CanvasTextBaseline; + fontMinMaxAlign?: CanvasTextAlign; + labelY?: number; + valueY?: number; + minY?: number; + maxY?: number; + minX?: number; + maxX?: number; + barTop?: number; + barBottom?: number; + barLeft?: number; + barRight?: number; + dashLength?: number; +} + +interface FontHeightInfo { + ascent?: number; + height?: number; + descent?: number; +} + +export class Drawings { + static font(options: CanvasGauges.GenericOptions, target: string, baseSize: number): string { + return options['font' + target + 'Style'] + ' ' + + options['font' + target + 'Weight'] + ' ' + + options['font' + target + 'Size'] * baseSize + 'px ' + + options['font' + target]; + } + static normalizedValue(options: CanvasGauges.GenericOptions): {normal: number, indented: number} { + const value = options.value; + const min = options.minValue; + const max = options.maxValue; + const dt = (max - min) * 0.01; + return { + normal: value < min ? min : value > max ? max : value, + indented: value < min ? min - dt : value > max ? max + dt : value + }; + } + static verifyError(err: any) { + if (err instanceof DOMException && (err as any).result === 0x8053000b) { + return ; // ignore it + } + throw err; + } +} + +export class CanvasDigitalGauge extends BaseGauge { + + static heightCache: {[key: string]: FontHeightInfo} = {}; + + private elementValueClone: HTMLCanvasElementClone; + private contextValueClone: DigitalGaugeCanvasRenderingContext2D; + + private elementProgressClone: HTMLCanvasElementClone; + private contextProgressClone: DigitalGaugeCanvasRenderingContext2D; + + public _value: number; + + constructor(options: CanvasDigitalGaugeOptions) { + options = {...defaultDigitalGaugeOptions, ...(options || {})}; + super(CanvasDigitalGauge.configure(options)); + this.initValueClone(); + } + + static configure(options: CanvasDigitalGaugeOptions): CanvasDigitalGaugeOptions { + + if (options.value > options.maxValue) { + options.value = options.maxValue; + } + + if (options.value < options.minValue) { + options.value = options.minValue; + } + + if (options.gaugeType === 'donut') { + if (!isDefinedAndNotNull(options.donutStartAngle)) { + options.donutStartAngle = 1.5 * Math.PI; + } + if (!isDefinedAndNotNull(options.donutEndAngle)) { + options.donutEndAngle = options.donutStartAngle + 2 * Math.PI; + } + } + + const colorsCount = options.levelColors.length; + const isColorProperty = isString(options.levelColors[0]); + const inc = colorsCount > 1 ? (1 / (colorsCount - 1)) : 1; + options.colorsRange = []; + if (options.neonGlowBrightness) { + options.neonColorsRange = []; + } + for (let i = 0; i < options.levelColors.length; i++) { + const levelColor: any = options.levelColors[i]; + if (levelColor !== null) { + let percentage: number; + if (isColorProperty) { + percentage = inc * i; + } else { + percentage = CanvasDigitalGauge.normalizeValue(levelColor.value, options.minValue, options.maxValue); + } + let tColor = tinycolor(isColorProperty ? levelColor : levelColor.color); + options.colorsRange.push({ + pct: percentage, + color: tColor.toRgb(), + rgbString: tColor.toRgbString() + }); + if (options.neonGlowBrightness) { + tColor = tinycolor(isColorProperty ? levelColor : levelColor.color).brighten(options.neonGlowBrightness); + options.neonColorsRange.push({ + pct: percentage, + color: tColor.toRgb(), + rgbString: tColor.toRgbString() + }); + } + } + } + + options.ticksValue = []; + for (const tick of options.ticks) { + if (tick !== null) { + options.ticksValue.push(CanvasDigitalGauge.normalizeValue(tick, options.minValue, options.maxValue)); + } + } + + if (options.neonGlowBrightness) { + options.neonColorTitle = tinycolor(options.colorTitle).brighten(options.neonGlowBrightness).toHexString(); + options.neonColorLabel = tinycolor(options.colorLabel).brighten(options.neonGlowBrightness).toHexString(); + options.neonColorValue = tinycolor(options.colorValue).brighten(options.neonGlowBrightness).toHexString(); + options.neonColorMinMax = tinycolor(options.colorMinMax).brighten(options.neonGlowBrightness).toHexString(); + } + + return options; + } + + static normalizeValue(value: number, min: number, max: number): number { + const normalValue = (value - min) / (max - min); + if (normalValue <= 0) { + return 0; + } + if (normalValue >= 1) { + return 1; + } + return normalValue; + } + + private initValueClone() { + const canvas = this.canvas; + this.elementValueClone = canvas.element.cloneNode(true) as HTMLCanvasElementClone; + this.contextValueClone = this.elementValueClone.getContext('2d'); + this.elementValueClone.initialized = false; + + this.contextValueClone.translate(canvas.drawX, canvas.drawY); + this.contextValueClone.save(); + + this.elementProgressClone = canvas.element.cloneNode(true) as HTMLCanvasElementClone; + this.contextProgressClone = this.elementProgressClone.getContext('2d'); + this.elementProgressClone.initialized = false; + + this.contextProgressClone.translate(canvas.drawX, canvas.drawY); + this.contextProgressClone.save(); + } + + destroy() { + this.contextValueClone = null; + this.elementValueClone = null; + this.contextProgressClone = null; + this.elementProgressClone = null; + super.destroy(); + } + + update(options: GenericOptions): BaseGauge { + this.canvas.onRedraw = null; + const result = super.update(options); + this.initValueClone(); + this.canvas.onRedraw = this.draw.bind(this); + this.draw(); + return result; + } + + set timestamp(timestamp: number) { + (this.options as CanvasDigitalGaugeOptions).timestamp = timestamp; + this.draw(); + } + + get timestamp(): number { + return (this.options as CanvasDigitalGaugeOptions).timestamp; + } + + draw(): CanvasDigitalGauge { + try { + const canvas = this.canvas; + if (!canvas.drawWidth || !canvas.drawHeight) { + return this; + } + const [x, y, w, h] = [ + -canvas.drawX, + -canvas.drawY, + canvas.drawWidth, + canvas.drawHeight + ]; + const options = this.options as CanvasDigitalGaugeOptions; + const elementClone = canvas.elementClone as HTMLCanvasElementClone; + if (!elementClone.initialized) { + const context = canvas.contextClone; + + // clear the cache + context.clearRect(x, y, w, h); + context.save(); + + const canvasContext = canvas.context as DigitalGaugeCanvasRenderingContext2D; + + canvasContext.barDimensions = barDimensions(context, options, x, y, w, h); + this.contextValueClone.barDimensions = canvasContext.barDimensions; + this.contextProgressClone.barDimensions = canvasContext.barDimensions; + + drawBackground(context, options); + + drawDigitalTitle(context, options); + + if (!options.showTimestamp) { + drawDigitalLabel(context, options); + } + + drawDigitalMinMax(context, options); + + elementClone.initialized = true; + } + + let valueChanged = false; + if (!this.elementValueClone.initialized || + isDefined(this._value) && this.elementValueClone.renderedValue !== this._value || + (options.showTimestamp && this.elementValueClone.renderedTimestamp !== this.timestamp)) { + if (isDefined(this._value)) { + this.elementValueClone.renderedValue = this._value; + } + if (isUndefined(this.elementValueClone.renderedValue)) { + this.elementValueClone.renderedValue = this.value; + } + const context = this.contextValueClone; + // clear the cache + context.clearRect(x, y, w, h); + context.save(); + + context.drawImage(canvas.elementClone, x, y, w, h); + context.save(); + + drawDigitalValue(context, options, this.elementValueClone.renderedValue); + + if (options.showTimestamp) { + drawDigitalLabel(context, options); + this.elementValueClone.renderedTimestamp = this.timestamp; + } + + this.elementValueClone.initialized = true; + + valueChanged = true; + } + + const progress = (Drawings.normalizedValue(options).normal - options.minValue) / + (options.maxValue - options.minValue); + + const fixedProgress = progress.toFixed(3); + + if (!this.elementProgressClone.initialized || this.elementProgressClone.renderedProgress !== fixedProgress || valueChanged) { + const context = this.contextProgressClone; + // clear the cache + context.clearRect(x, y, w, h); + context.save(); + + context.drawImage(this.elementValueClone, x, y, w, h); + context.save(); + + if (Number(fixedProgress) > 0) { + drawProgress(context, options, progress); + } + + this.elementProgressClone.initialized = true; + this.elementProgressClone.renderedProgress = fixedProgress; + } + + this.canvas.commit(); + + // clear the canvas + canvas.context.clearRect(x, y, w, h); + canvas.context.save(); + + canvas.context.drawImage(this.elementProgressClone, x, y, w, h); + canvas.context.save(); + + // @ts-ignore + super.draw(); + + } catch (err) { + Drawings.verifyError(err); + } + return this; + } + + getValueColor() { + if (this.contextProgressClone) { + let color = this.contextProgressClone.currentColor; + const options = this.options as CanvasDigitalGaugeOptions; + if (!color) { + const progress = (Drawings.normalizedValue(options).normal - options.minValue) / + (options.maxValue - options.minValue); + if (options.neonGlowBrightness) { + color = getProgressColor(progress, options.neonColorsRange); + } else { + color = getProgressColor(progress, options.colorsRange); + } + } + return color; + } else { + return '#000'; + } + } +} + +function barDimensions(context: DigitalGaugeCanvasRenderingContext2D, + options: CanvasDigitalGaugeOptions, + x: number, y: number, w: number, h: number): BarDimensions { + context.barDimensions = { + baseX: x, + baseY: y, + width: w, + height: h + }; + + const bd = context.barDimensions; + + let aspect = 1; + + if (options.gaugeType === 'horizontalBar') { + aspect = options.title === '' ? 2.5 : 2; + } else if (options.gaugeType === 'verticalBar') { + aspect = options.hideMinMax ? 0.35 : 0.5; + } else if (options.gaugeType === 'arc') { + aspect = 1.5; + } + + const currentAspect = w / h; + if (currentAspect > aspect) { + bd.width = (h * aspect); + bd.height = h; + } else { + bd.width = w; + bd.height = w / aspect; + } + + bd.origBaseX = bd.baseX; + bd.origBaseY = bd.baseY; + bd.baseX += (w - bd.width) / 2; + bd.baseY += (h - bd.height) / 2; + + if (options.gaugeType === 'donut') { + bd.fontSizeFactor = Math.max(bd.width, bd.height) / 125; + } else if (options.gaugeType === 'verticalBar' || (options.gaugeType === 'arc' && options.title === '')) { + bd.fontSizeFactor = Math.max(bd.width, bd.height) / 150; + } else { + bd.fontSizeFactor = Math.max(bd.width, bd.height) / 200; + } + + const gws = options.gaugeWidthScale; + + if (options.neonGlowBrightness) { + options.fontTitleHeight = determineFontHeight(options, 'Title', bd.fontSizeFactor); + options.fontLabelHeight = determineFontHeight(options, 'Label', bd.fontSizeFactor); + options.fontValueHeight = determineFontHeight(options, 'Value', bd.fontSizeFactor); + options.fontMinMaxHeight = determineFontHeight(options, 'MinMax', bd.fontSizeFactor); + } + + if (options.gaugeType === 'donut') { + bd.Ro = bd.width / 2 - bd.width / 20; + bd.Cy = bd.baseY + bd.height / 2; + if (options.title && typeof options.title === 'string' && options.title.length > 0) { + let titleOffset = determineFontHeight(options, 'Title', bd.fontSizeFactor).height; + titleOffset += bd.fontSizeFactor * 2; + bd.titleY = bd.baseY + titleOffset; + titleOffset += bd.fontSizeFactor * 2; + bd.Cy += titleOffset / 2; + bd.Ro -= titleOffset / 2; + } + bd.Ri = bd.Ro - bd.width / 6.666666666666667 * gws * 1.2; + bd.Cx = bd.baseX + bd.width / 2; + } else if (options.gaugeType === 'arc') { + if (options.title && typeof options.title === 'string' && options.title.length > 0) { + bd.Ro = bd.width / 2 - bd.width / 7; + bd.Ri = bd.Ro - bd.width / 6.666666666666667 * gws; + } else { + bd.Ro = bd.width / 2 - bd.fontSizeFactor * 4; + bd.Ri = bd.Ro - bd.width / 6.666666666666667 * gws * 1.2; + } + bd.Cx = bd.baseX + bd.width / 2; + bd.Cy = bd.baseY + bd.height / 1.25; + } else if (options.gaugeType === 'verticalBar') { + bd.Ro = bd.width / 2 - bd.width / 10; + bd.Ri = bd.Ro - bd.width / 6.666666666666667 * gws * (options.hideMinMax ? 4 : 2.5); + } else { // horizontalBar + bd.Ro = bd.width / 2 - bd.width / 10; + bd.Ri = bd.Ro - bd.width / 6.666666666666667 * gws; + } + + bd.strokeWidth = bd.Ro - bd.Ri; + bd.Rm = bd.Ri + bd.strokeWidth * 0.5; + + bd.fontValueBaseline = 'alphabetic'; + bd.fontMinMaxBaseline = 'alphabetic'; + bd.fontMinMaxAlign = 'center'; + + if (options.gaugeType === 'donut') { + bd.fontValueBaseline = 'middle'; + if (options.label && options.label.length > 0) { + const valueHeight = determineFontHeight(options, 'Value', bd.fontSizeFactor).height; + const labelHeight = determineFontHeight(options, 'Label', bd.fontSizeFactor).height; + const total = valueHeight + labelHeight; + bd.labelY = bd.Cy + total / 2; + bd.valueY = bd.Cy - total / 2 + valueHeight / 2; + } else { + bd.valueY = bd.Cy; + } + } else if (options.gaugeType === 'arc') { + bd.titleY = bd.Cy - bd.Ro - 12 * bd.fontSizeFactor; + bd.valueY = bd.Cy; + bd.labelY = bd.Cy + (8 + options.fontLabelSize) * bd.fontSizeFactor; + bd.minY = bd.maxY = bd.labelY; + if (options.roundedLineCap) { + bd.minY += bd.strokeWidth / 2; + bd.maxY += bd.strokeWidth / 2; + } + bd.minX = bd.Cx - bd.Rm; + bd.maxX = bd.Cx + bd.Rm; + } else if (options.gaugeType === 'horizontalBar') { + bd.titleY = bd.baseY + 4 * bd.fontSizeFactor + + (options.title === '' ? 0 : options.fontTitleSize * bd.fontSizeFactor); + bd.titleBottom = bd.titleY + (options.title === '' ? 0 : 4) * bd.fontSizeFactor; + + bd.valueY = bd.titleBottom + + (options.hideValue ? 0 : options.fontValueSize * bd.fontSizeFactor); + + bd.barTop = bd.valueY + 8 * bd.fontSizeFactor; + bd.barBottom = bd.barTop + bd.strokeWidth; + + if (options.hideMinMax && options.label === '') { + bd.labelY = bd.barBottom; + bd.barLeft = bd.origBaseX + options.fontMinMaxSize / 3 * bd.fontSizeFactor; + bd.barRight = bd.origBaseX + w + /*bd.width*/ -options.fontMinMaxSize / 3 * bd.fontSizeFactor; + } else { + context.font = Drawings.font(options, 'MinMax', bd.fontSizeFactor); + const minTextWidth = context.measureText(options.minValue + '').width; + const maxTextWidth = context.measureText(options.maxValue + '').width; + const maxW = Math.max(minTextWidth, maxTextWidth); + bd.minX = bd.origBaseX + maxW / 2 + options.fontMinMaxSize / 3 * bd.fontSizeFactor; + bd.maxX = bd.origBaseX + w + /*bd.width*/ -maxW / 2 - options.fontMinMaxSize / 3 * bd.fontSizeFactor; + bd.barLeft = bd.minX; + bd.barRight = bd.maxX; + bd.labelY = bd.barBottom + (8 + options.fontLabelSize) * bd.fontSizeFactor; + bd.minY = bd.maxY = bd.labelY; + } + } else if (options.gaugeType === 'verticalBar') { + bd.titleY = bd.baseY + ((options.title === '' ? 0 : options.fontTitleSize) + 8) * bd.fontSizeFactor; + bd.titleBottom = bd.titleY + (options.title === '' ? 0 : 4) * bd.fontSizeFactor; + + bd.valueY = bd.titleBottom + (options.hideValue ? 0 : options.fontValueSize * bd.fontSizeFactor); + bd.barTop = bd.valueY + 8 * bd.fontSizeFactor; + + bd.labelY = bd.baseY + bd.height - 16; + if (options.label === '') { + bd.barBottom = bd.labelY; + } else { + bd.barBottom = bd.labelY - (8 + options.fontLabelSize) * bd.fontSizeFactor; + } + bd.minX = bd.maxX = + bd.baseX + bd.width / 2 + bd.strokeWidth / 2 + options.fontMinMaxSize / 3 * bd.fontSizeFactor; + bd.minY = bd.barBottom; + bd.maxY = bd.barTop; + bd.fontMinMaxBaseline = 'middle'; + bd.fontMinMaxAlign = 'left'; + } + + if (options.dashThickness) { + let circumference; + if (options.gaugeType === 'donut') { + circumference = Math.PI * bd.Rm * 2; + } else if (options.gaugeType === 'arc') { + circumference = Math.PI * bd.Rm; + } else if (options.gaugeType === 'horizontalBar') { + circumference = bd.barRight - bd.barLeft; + } else if (options.gaugeType === 'verticalBar') { + circumference = bd.barBottom - bd.barTop; + } + let dashCount = Math.floor(circumference / (options.dashThickness * bd.fontSizeFactor)); + if (options.gaugeType === 'donut') { + // tslint:disable-next-line:no-bitwise + dashCount = (dashCount | 1) - 1; + } else { + // tslint:disable-next-line:no-bitwise + dashCount = (dashCount - 1) | 1; + } + bd.dashLength = Math.ceil(circumference / dashCount); + } + + return bd; +} + +function determineFontHeight(options: CanvasDigitalGaugeOptions, target: string, baseSize: number): FontHeightInfo { + const fontStyleStr = 'font-style:' + options['font' + target + 'Style'] + ';font-weight:' + + options['font' + target + 'Weight'] + ';font-size:' + + options['font' + target + 'Size'] * baseSize + 'px;font-family:' + + options['font' + target]; + let result = CanvasDigitalGauge.heightCache[fontStyleStr]; + if (!result) { + const fontStyle = { + fontFamily: options['font' + target], + fontSize: options['font' + target + 'Size'] * baseSize + 'px', + fontWeight: options['font' + target + 'Weight'], + fontStyle: options['font' + target + 'Style'] + }; + const text = $('Hg').css(fontStyle); + const block = $('
'); + + const div = $('
'); + div.append(text, block); + + const body = $('body'); + body.append(div); + + try { + result = {}; + block.css({verticalAlign: 'baseline'}); + result.ascent = block.offset().top - text.offset().top; + block.css({verticalAlign: 'bottom'}); + result.height = block.offset().top - text.offset().top; + result.descent = result.height - result.ascent; + } finally { + div.remove(); + } + + CanvasDigitalGauge.heightCache[fontStyleStr] = result; + } + return result; +} + +function drawBackground(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions) { + const {barLeft, barRight, barTop, barBottom, width, baseX, strokeWidth} = + context.barDimensions; + if (context.barDimensions.dashLength) { + context.setLineDash([context.barDimensions.dashLength]); + } + context.beginPath(); + context.strokeStyle = options.gaugeColor; + context.lineWidth = strokeWidth; + if (options.roundedLineCap) { + context.lineCap = 'round'; + } + if (options.gaugeType === 'donut') { + context.arc(context.barDimensions.Cx, context.barDimensions.Cy, context.barDimensions.Rm, + options.donutStartAngle, options.donutEndAngle); + context.stroke(); + } else if (options.gaugeType === 'arc') { + context.arc(context.barDimensions.Cx, context.barDimensions.Cy, + context.barDimensions.Rm, Math.PI, 2 * Math.PI); + context.stroke(); + } else if (options.gaugeType === 'horizontalBar') { + context.moveTo(barLeft, barTop + strokeWidth / 2); + context.lineTo(barRight, barTop + strokeWidth / 2); + context.stroke(); + } else if (options.gaugeType === 'verticalBar') { + context.moveTo(baseX + width / 2, barBottom); + context.lineTo(baseX + width / 2, barTop); + context.stroke(); + } +} + +function drawText(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions, + target: string, text: string, textX: number, textY: number) { + context.fillStyle = options[(options.neonGlowBrightness ? 'neonColor' : 'color') + target]; + context.fillText(text, textX, textY); +} + +function drawDigitalTitle(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions) { + if (!options.title || typeof options.title !== 'string') { + return; + } + + const {titleY, width, baseX, fontSizeFactor} = + context.barDimensions; + + const textX = Math.round(baseX + width / 2); + const textY = titleY; + + context.save(); + context.textAlign = 'center'; + context.font = Drawings.font(options, 'Title', fontSizeFactor); + context.lineWidth = 0; + drawText(context, options, 'Title', options.title.toUpperCase(), textX, textY); +} + +function drawDigitalLabel(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions) { + if (!options.label || options.label === '') { + return; + } + + const {labelY, baseX, width, fontSizeFactor} = + context.barDimensions; + + const textX = Math.round(baseX + width / 2); + const textY = labelY; + + context.save(); + context.textAlign = 'center'; + context.font = Drawings.font(options, 'Label', fontSizeFactor); + context.lineWidth = 0; + drawText(context, options, 'Label', options.label.toUpperCase(), textX, textY); +} + +function drawDigitalMinMax(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions) { + if (options.hideMinMax || options.gaugeType === 'donut') { + return; + } + + const {minY, maxY, minX, maxX, fontSizeFactor, fontMinMaxAlign, fontMinMaxBaseline} = + context.barDimensions; + + context.save(); + context.textAlign = fontMinMaxAlign; + context.textBaseline = fontMinMaxBaseline; + context.font = Drawings.font(options, 'MinMax', fontSizeFactor); + context.lineWidth = 0; + drawText(context, options, 'MinMax', options.minValue + '', minX, minY); + drawText(context, options, 'MinMax', options.maxValue + '', maxX, maxY); +} + +function drawDigitalValue(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions, value: any) { + if (options.hideValue) { + return; + } + + const {valueY, baseX, width, fontSizeFactor, fontValueBaseline} = + context.barDimensions; + + const textX = Math.round(baseX + width / 2); + const textY = valueY; + + let text = options.valueText || padValue(value, options.valueDec); + text += options.symbol; + + context.save(); + context.textAlign = 'center'; + context.textBaseline = fontValueBaseline; + context.font = Drawings.font(options, 'Value', fontSizeFactor); + context.lineWidth = 0; + drawText(context, options, 'Value', text, textX, textY); +} + +function getProgressColor(progress: number, colorsRange: DigitalGaugeColorRange[]): string { + + if (progress === 0 || colorsRange.length === 1) { + return colorsRange[0].rgbString; + } + + for (let j = 0; j < colorsRange.length; j++) { + if (progress <= colorsRange[j].pct) { + const lower = colorsRange[j - 1]; + const upper = colorsRange[j]; + const range = upper.pct - lower.pct; + const rangePct = (progress - lower.pct) / range; + const pctLower = 1 - rangePct; + const pctUpper = rangePct; + const color = tinycolor({ + r: Math.floor(lower.color.r * pctLower + upper.color.r * pctUpper), + g: Math.floor(lower.color.g * pctLower + upper.color.g * pctUpper), + b: Math.floor(lower.color.b * pctLower + upper.color.b * pctUpper) + }); + return color.toRgbString(); + } + } +} + +function drawArcGlow(context: DigitalGaugeCanvasRenderingContext2D, + Cx: number, Cy: number, Ri: number, Rm: number, Ro: number, + color: string, progress: number, isDonut: boolean, + donutStartAngle?: number, donutEndAngle?: number) { + context.setLineDash([]); + const strokeWidth = Ro - Ri; + const blur = 0.55; + const edge = strokeWidth * blur; + context.lineWidth = strokeWidth + edge; + const stop = blur / (2 * blur + 2); + const glowGradient = context.createRadialGradient(Cx, Cy, Ri - edge / 2, Cx, Cy, Ro + edge / 2); + const color1 = tinycolor(color).setAlpha(0.5).toRgbString(); + const color2 = tinycolor(color).setAlpha(0).toRgbString(); + glowGradient.addColorStop(0, color2); + glowGradient.addColorStop(stop, color1); + glowGradient.addColorStop(1.0 - stop, color1); + glowGradient.addColorStop(1, color2); + context.strokeStyle = glowGradient; + context.beginPath(); + const e = 0.01 * Math.PI; + if (isDonut) { + context.arc(Cx, Cy, Rm, donutStartAngle - e, donutStartAngle + + (donutEndAngle - donutStartAngle) * progress + e); + } else { + context.arc(Cx, Cy, Rm, Math.PI - e, Math.PI + Math.PI * progress + e); + } + context.stroke(); +} + +function drawBarGlow(context: DigitalGaugeCanvasRenderingContext2D, startX: number, startY: number, + endX: number, endY: number, color: string, strokeWidth: number, isVertical: boolean) { + context.setLineDash([]); + const blur = 0.55; + const edge = strokeWidth * blur; + context.lineWidth = strokeWidth + edge; + const stop = blur / (2 * blur + 2); + const gradientStartX = isVertical ? startX - context.lineWidth / 2 : 0; + const gradientStartY = isVertical ? 0 : startY - context.lineWidth / 2; + const gradientStopX = isVertical ? startX + context.lineWidth / 2 : 0; + const gradientStopY = isVertical ? 0 : startY + context.lineWidth / 2; + + const glowGradient = context.createLinearGradient(gradientStartX, gradientStartY, gradientStopX, gradientStopY); + const color1 = tinycolor(color).setAlpha(0.5).toRgbString(); + const color2 = tinycolor(color).setAlpha(0).toRgbString(); + glowGradient.addColorStop(0, color2); + glowGradient.addColorStop(stop, color1); + glowGradient.addColorStop(1.0 - stop, color1); + glowGradient.addColorStop(1, color2); + context.strokeStyle = glowGradient; + const dx = isVertical ? 0 : 0.05 * context.lineWidth; + const dy = isVertical ? 0.05 * context.lineWidth : 0; + context.beginPath(); + context.moveTo(startX - dx, startY + dy); + context.lineTo(endX + dx, endY - dy); + context.stroke(); +} + +function drawTickArc(context: DigitalGaugeCanvasRenderingContext2D, tickValues: number[], Cx: number, Cy: number, + Ri: number, Rm: number, Ro: number, startAngle: number, endAngle: number, + color: string, tickWidth: number) { + if (!tickValues.length) { + return; + } + + const strokeWidth = Ro - Ri; + context.beginPath(); + context.lineWidth = tickWidth; + context.strokeStyle = color; + for (const tick of tickValues) { + const angle = startAngle + tick * endAngle; + const x1 = Cx + (Ri + strokeWidth) * Math.cos(angle); + const y1 = Cy + (Ri + strokeWidth) * Math.sin(angle); + const x2 = Cx + Ri * Math.cos(angle); + const y2 = Cy + Ri * Math.sin(angle); + context.moveTo(x1, y1); + context.lineTo(x2, y2); + } + context.stroke(); +} + +function drawTickBar(context: DigitalGaugeCanvasRenderingContext2D, tickValues: number[], startX: number, startY: number, + distanceBar: number, strokeWidth: number, isVertical: boolean, color: string, tickWidth: number) { + if (!tickValues.length) { + return; + } + + context.beginPath(); + context.lineWidth = tickWidth; + context.strokeStyle = color; + for (const tick of tickValues) { + const tickValue = tick * distanceBar; + if (isVertical) { + context.moveTo(startX - strokeWidth / 2, startY + tickValue - distanceBar); + context.lineTo(startX + strokeWidth / 2, startY + tickValue - distanceBar); + } else { + context.moveTo(startX + tickValue, startY); + context.lineTo(startX + tickValue, startY + strokeWidth); + } + } + context.stroke(); +} + + +function drawProgress(context: DigitalGaugeCanvasRenderingContext2D, + options: CanvasDigitalGaugeOptions, progress: number) { + let neonColor; + if (options.neonGlowBrightness) { + context.currentColor = neonColor = getProgressColor(progress, options.neonColorsRange); + } else { + context.currentColor = context.strokeStyle = getProgressColor(progress, options.colorsRange); + } + + const {barLeft, barRight, barTop, baseX, width, barBottom, Cx, Cy, Rm, Ro, Ri, strokeWidth} = + context.barDimensions; + + if (context.barDimensions.dashLength) { + context.setLineDash([context.barDimensions.dashLength]); + } + context.lineWidth = strokeWidth; + if (options.roundedLineCap) { + context.lineCap = 'round'; + } else { + context.lineCap = 'butt'; + } + if (options.gaugeType === 'donut') { + if (options.neonGlowBrightness) { + context.strokeStyle = neonColor; + } + context.beginPath(); + context.arc(Cx, Cy, Rm, options.donutStartAngle, options.donutStartAngle + + (options.donutEndAngle - options.donutStartAngle) * progress); + context.stroke(); + if (options.neonGlowBrightness && !options.isMobile) { + drawArcGlow(context, Cx, Cy, Ri, Rm, Ro, neonColor, progress, true, + options.donutStartAngle, options.donutEndAngle); + } + drawTickArc(context, options.ticksValue, Cx, Cy, Ri, Rm, Ro, options.donutStartAngle, + options.donutEndAngle - options.donutStartAngle, options.colorTicks, options.tickWidth); + } else if (options.gaugeType === 'arc') { + if (options.neonGlowBrightness) { + context.strokeStyle = neonColor; + } + context.beginPath(); + context.arc(Cx, Cy, Rm, Math.PI, Math.PI + Math.PI * progress); + context.stroke(); + if (options.neonGlowBrightness && !options.isMobile) { + drawArcGlow(context, Cx, Cy, Ri, Rm, Ro, neonColor, progress, false); + } + drawTickArc(context, options.ticksValue, Cx, Cy, Ri, Rm, Ro, Math.PI, Math.PI, options.colorTicks, options.tickWidth); + } else if (options.gaugeType === 'horizontalBar') { + if (options.neonGlowBrightness) { + context.strokeStyle = neonColor; + } + context.beginPath(); + context.moveTo(barLeft, barTop + strokeWidth / 2); + context.lineTo(barLeft + (barRight - barLeft) * progress, barTop + strokeWidth / 2); + context.stroke(); + if (options.neonGlowBrightness && !options.isMobile) { + drawBarGlow(context, barLeft, barTop + strokeWidth / 2, + barLeft + (barRight - barLeft) * progress, barTop + strokeWidth / 2, + neonColor, strokeWidth, false); + } + drawTickBar(context, options.ticksValue, barLeft, barTop, barRight - barLeft, strokeWidth, + false, options.colorTicks, options.tickWidth); + } else if (options.gaugeType === 'verticalBar') { + if (options.neonGlowBrightness) { + context.strokeStyle = neonColor; + } + context.beginPath(); + context.moveTo(baseX + width / 2, barBottom); + context.lineTo(baseX + width / 2, barBottom - (barBottom - barTop) * progress); + context.stroke(); + if (options.neonGlowBrightness && !options.isMobile) { + drawBarGlow(context, baseX + width / 2, barBottom, + baseX + width / 2, barBottom - (barBottom - barTop) * progress, + neonColor, strokeWidth, true); + } + drawTickBar(context, options.ticksValue, baseX + width / 2, barTop, barTop - barBottom, strokeWidth, + true, options.colorTicks, options.tickWidth); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator-panel.component.html new file mode 100644 index 0000000000..05aabbb7a7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator-panel.component.html @@ -0,0 +1,35 @@ + +
+ + + + + +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator-panel.component.scss new file mode 100644 index 0000000000..1dcd009e0a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator-panel.component.scss @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-date-range-navigator-panel { + overflow: auto; + background: #fff; + border-radius: 4px; + box-shadow: + 0 7px 8px -4px rgba(0, 0, 0, .2), + 0 13px 19px 2px rgba(0, 0, 0, .14), + 0 5px 24px 4px rgba(0, 0, 0, .12); + + .mat-content { + overflow: hidden; + background-color: #fff; + } + + .mat-padding { + padding: 16px; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.html new file mode 100644 index 0000000000..94d66e2a8a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.html @@ -0,0 +1,56 @@ + +
+ + widgets.date-range-navigator.localizationMap.Date picker + + + {{advancedModel.chosenLabel}} + + + + + widgets.date-range-navigator.localizationMap.Interval + + + {{'widgets.date-range-navigator.localizationMap.' + customInterval.label | translate}} + + + {{'widgets.date-range-navigator.localizationMap.' + date.value.label | translate}} + + + + +
+ + + widgets.date-range-navigator.localizationMap.Step size + + + {{'widgets.date-range-navigator.localizationMap.' + date.value.label | translate}} + + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.scss new file mode 100644 index 0000000000..e3505275e5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.scss @@ -0,0 +1,98 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + display: flex; + height: 100%; + + .date-range-navigator { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-evenly; + width: 100%; + margin: auto; + + .drn__element { + display: flex; + flex-direction: row; + align-items: center; + max-width: 100%; + height: 60px; + margin: 4px 0; + } + + .picker { + .picker__wrapper { + position: relative; + max-width: 100%; + padding: 2px; + + > label { + position: absolute; + right: -3px; + bottom: 100%; + left: 0; + padding-left: 3px; + color: #787878; + transform: scale(.75); + transform-origin: left bottom; + } + } + + .md-select-value { + min-width: 225px; + border-color: #e1e1e1; + + .md-select-icon { + color: #757575; + } + } + } + + &.short-mode { + display: block; + width: 90%; + + .drn__element { + width: 100%; + } + + .picker { + .picker__wrapper { + width: 100%; + } + + .md-select-value { + min-width: initial; + } + } + + &.labels-hidden { + .drn__element { + margin: 0; + } + } + } + + &.long-mode { + &.labels-hidden { + .drn__element { + height: 36px; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.ts new file mode 100644 index 0000000000..8d0cc7cb3a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.ts @@ -0,0 +1,307 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + Component, + Inject, + InjectionToken, + Input, + OnDestroy, + OnInit, + ViewChild, + ViewContainerRef, + ViewEncapsulation +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { UtilsService } from '@core/services/utils.service'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + cloneDateRangeNavigatorModel, + DateIntervalEntry, + dateIntervalsMap, + DateRangeNavigatorModel, + DateRangeNavigatorSettings, + getFormattedDate +} from '@home/components/widget/lib/date-range-navigator/date-range-navigator.models'; +import { KeyValue } from '@angular/common'; +import * as _moment from 'moment'; +import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; +import { MatSelect } from '@angular/material/select'; +import { Subscription } from 'rxjs'; +import { HistoryWindowType, TimewindowType } from '@shared/models/time/time.models'; +import { isDefined } from '@core/utils'; + +@Component({ + selector: 'tb-date-range-navigator-widget', + templateUrl: './date-range-navigator.component.html', + styleUrls: ['./date-range-navigator.component.scss'] +}) +export class DateRangeNavigatorWidgetComponent extends PageComponent implements OnInit, OnDestroy { + + @Input() + ctx: WidgetContext; + + @ViewChild('datePicker', {static: true}) datePickerSelect: MatSelect; + + settings: DateRangeNavigatorSettings; + + datesMap = dateIntervalsMap; + + advancedModel: DateRangeNavigatorModel = {}; + + selectedDateInterval: number; + customInterval: DateIntervalEntry; + selectedStepSize: number; + + private firstUpdate = true; + private dashboardTimewindowChangedSubscription: Subscription; + + originalOrder = (a: KeyValue, b: KeyValue): number => { + return 0; + }; + + constructor(private utils: UtilsService, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, + protected store: Store) { + super(store); + } + + ngOnInit(): void { + this.settings = this.ctx.settings; + this.settings.useSessionStorage = isDefined(this.settings.useSessionStorage) ? this.settings.useSessionStorage : true; + let selection; + if (this.settings.useSessionStorage) { + selection = this.readFromStorage('date-range'); + } + if (selection) { + this.advancedModel = { + chosenLabel: selection.name, + startDate: _moment(selection.start), + endDate: _moment(selection.end) + }; + } else { + const end = new Date(); + end.setHours(23, 59, 59, 999); + this.advancedModel = { + startDate: _moment((end.getTime() + 1) - this.datesMap[this.settings.initialInterval || 'week'].ts), + endDate: _moment(end.getTime()) + }; + this.advancedModel.chosenLabel = getFormattedDate(this.advancedModel); + } + this.selectedStepSize = this.datesMap[this.settings.stepSize || 'day'].ts; + this.widgetContextTimewindowSync(); + this.dashboardTimewindowChangedSubscription = this.ctx.dashboard.dashboardTimewindowChanged.subscribe(() => { + this.widgetContextTimewindowSync(); + }); + } + + ngOnDestroy(): void { + if (this.dashboardTimewindowChangedSubscription) { + this.dashboardTimewindowChangedSubscription.unsubscribe(); + this.dashboardTimewindowChangedSubscription = null; + } + } + + openNavigatorPanel($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.datePickerSelect.close(); + const target = $event.target || $event.srcElement || $event.currentTarget; + const config = new OverlayConfig(); + config.backdropClass = 'cdk-overlay-transparent-backdrop'; + config.hasBackdrop = true; + config.panelClass = 'tb-date-range-navigator-panel'; + const connectedPosition: ConnectedPosition = { + originX: 'end', + originY: 'bottom', + overlayX: 'end', + overlayY: 'top' + }; + config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) + .withPositions([connectedPosition]); + + const overlayRef = this.overlay.create(config); + overlayRef.backdropClick().subscribe(() => { + overlayRef.dispose(); + }); + const injectionTokens = new WeakMap([ + [DATE_RANGE_NAVIGATOR_PANEL_DATA, { + model: cloneDateRangeNavigatorModel(this.advancedModel), + settings: this.settings, + onChange: model => { + this.advancedModel = model; + this.triggerChange(); + } + } as DateRangeNavigatorPanelData], + [OverlayRef, overlayRef] + ]); + const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens); + overlayRef.attach(new ComponentPortal(DateRangeNavigatorPanelComponent, + this.viewContainerRef, injector)); + this.ctx.detectChanges(); + } + + private widgetContextTimewindowSync() { + if (!this.firstUpdate) { + this.updateAdvancedModel(); + } + this.updateDateInterval(); + if (this.settings.useSessionStorage) { + this.updateStorageDate(); + } + if (this.firstUpdate) { + this.firstUpdate = false; + this.updateTimewindow(this.advancedModel.startDate.valueOf(), this.advancedModel.endDate.valueOf()); + } + this.ctx.detectChanges(); + } + + private updateAdvancedModel() { + const timewindow = this.ctx.dashboardTimewindow; + if (timewindow.selectedTab === TimewindowType.HISTORY && timewindow.history.historyType === HistoryWindowType.FIXED) { + const fixedTimewindow = timewindow.history.fixedTimewindow; + this.advancedModel.startDate = _moment(fixedTimewindow.startTimeMs); + this.advancedModel.endDate = _moment(fixedTimewindow.endTimeMs); + this.advancedModel.chosenLabel = getFormattedDate(this.advancedModel); + } + } + + private updateTimewindow(startTime: number, endTime: number) { + this.ctx.dashboard.onUpdateTimewindow(startTime, endTime, 10, true); + } + + private updateDateInterval() { + const interval = this.advancedModel.endDate.valueOf() - this.advancedModel.startDate.valueOf(); + + for (const key of Object.keys(this.datesMap)) { + if (Object.prototype.hasOwnProperty.call(this.datesMap, key)) { + if (this.datesMap[key].ts === interval || this.datesMap[key].ts === interval + 1 || this.datesMap[key].ts === interval - 1) { + this.selectedDateInterval = this.datesMap[key].ts; + this.customInterval = undefined; + return; + } + } + } + + this.selectedDateInterval = interval; + this.customInterval = {ts: interval, label: 'Custom interval'}; + } + + triggerChange() { + this.updateTimewindow(this.advancedModel.startDate.valueOf(), this.advancedModel.endDate.valueOf()); + } + + changeInterval() { + const endTime = this.ctx.dashboard.dashboardTimewindow.history ? + this.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.endTimeMs : + this.advancedModel.endDate.valueOf(); + this.updateTimewindow(endTime - this.selectedDateInterval / 2, endTime + this.selectedDateInterval / 2); + } + + goBack() { + this.step(-1); + } + + goForth() { + this.step(1); + } + + private step(direction: number) { + const startTime = this.ctx.dashboard.dashboardTimewindow.history ? + this.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.startTimeMs : + this.advancedModel.startDate.valueOf(); + const endTime = this.ctx.dashboard.dashboardTimewindow.history ? + this.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.endTimeMs : + this.advancedModel.endDate.valueOf(); + this.updateTimewindow(startTime + this.selectedStepSize * direction, endTime + this.selectedStepSize * direction); + } + + private readFromStorage(itemKey: string): any { + if (window.sessionStorage.getItem(itemKey)) { + const selection = JSON.parse(window.sessionStorage.getItem(itemKey)); + selection.start = new Date(parseInt(selection.start, 10)); + selection.end = new Date(parseInt(selection.end, 10)); + return selection; + } + return undefined; + } + + private updateStorageDate() { + this.saveIntoStorage('date-range', { + start: this.advancedModel.startDate.valueOf(), + end: this.advancedModel.endDate.valueOf(), + name: this.advancedModel.chosenLabel + }); + } + + private saveIntoStorage(keyName: string, selection: any) { + if (selection) { + window.sessionStorage.setItem(keyName, JSON.stringify(selection)); + } + } +} + +const DATE_RANGE_NAVIGATOR_PANEL_DATA = new InjectionToken('DateRangeNavigatorPanelData'); + +export interface DateRangeNavigatorPanelData { + model: DateRangeNavigatorModel; + settings: DateRangeNavigatorSettings; + onChange: (model: DateRangeNavigatorModel) => void; +} + +@Component({ + selector: 'tb-date-range-navigator-panel', + templateUrl: './date-range-navigator-panel.component.html', + styleUrls: ['./date-range-navigator-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class DateRangeNavigatorPanelComponent { + + settings: DateRangeNavigatorSettings; + + model: DateRangeNavigatorModel; + + locale: any = {}; + + constructor(@Inject(DATE_RANGE_NAVIGATOR_PANEL_DATA) public data: DateRangeNavigatorPanelData, + private overlayRef: OverlayRef) { + this.model = data.model; + this.settings = data.settings; + this.locale.firstDay = this.settings.firstDayOfWeek || 1; + this.locale.daysOfWeek = _moment.weekdaysMin(); + this.locale.monthNames = _moment.monthsShort(); + } + + choosedDate($event) { + this.model = $event; + this.model.chosenLabel = getFormattedDate(this.model); + if (this.settings.autoConfirm) { + this.data.onChange(this.model); + this.overlayRef.dispose(); + } + } + + apply() { + this.data.onChange(this.model); + this.overlayRef.dispose(); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.models.ts new file mode 100644 index 0000000000..b621ebc535 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.models.ts @@ -0,0 +1,109 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import * as _moment from 'moment'; + +export type DateRangeInterval = 'hour' | 'day' | 'week' | 'twoWeeks' | 'month' | 'threeMonths' | 'sixMonths'; + +export interface DateRangeNavigatorSettings { + hidePicker: boolean; + onePanel: boolean; + autoConfirm: boolean; + showTemplate: boolean; + firstDayOfWeek: number; + hideInterval: boolean; + initialInterval: DateRangeInterval; + hideStepSize: boolean; + stepSize: DateRangeInterval; + hideLabels: boolean; + useSessionStorage: boolean; +} + +export interface DateIntervalEntry { + ts: number; + label: string; +} + +export interface DateRangeNavigatorModel { + chosenLabel?: string; + startDate?: _moment.Moment; + endDate?: _moment.Moment; +} + +export function cloneDateRangeNavigatorModel(model: DateRangeNavigatorModel): DateRangeNavigatorModel { + const cloned: DateRangeNavigatorModel = {}; + cloned.chosenLabel = model.chosenLabel; + cloned.startDate = model.startDate ? model.startDate.clone() : undefined; + cloned.endDate = model.endDate ? model.endDate.clone() : undefined; + return cloned; +} + +export function getFormattedDate(model: DateRangeNavigatorModel): string { + let template: string; + + const startDate = model.startDate; + const endDate = model.endDate; + + if (startDate.diff(endDate, 'days') === 0) { + template = startDate.format('DD MMM YYYY'); // datePipe.transform(startDate, 'dd MMM yyyy'); + } else { + let startDateFormat = 'DD'; + if (startDate.month() !== endDate.month() || startDate.year() !== endDate.year()) { + startDateFormat += ' MMM'; + } + if (startDate.year() !== endDate.year()) { + startDateFormat += ' YYYY'; + } + template = startDate.format(startDateFormat) + ' - ' + endDate.format('DD MMM YYYY'); + } + return template; +} + +const hour = 3600000; +const day = 86400000; +const week = 604800000; +const month = 2629743000; + +export const dateIntervalsMap: {[key: string]: DateIntervalEntry} = { + hour: { + ts: hour, + label: 'Hour' + }, + day: { + ts: day, + label: 'Day' + }, + week: { + ts: week, + label: 'Week' + }, + twoWeeks: { + ts: week * 2, + label: '2 weeks' + }, + month: { + ts: month, + label: 'Month' + }, + threeMonths: { + ts: month * 3, + label: '3 months' + }, + sixMonths: { + ts: month * 6, + label: '6 months' + } +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.models.ts new file mode 100644 index 0000000000..fa622cb2a5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.models.ts @@ -0,0 +1,983 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { JsonSettingsSchema } from '@shared/models/widget.models'; +import { GaugeType } from '@home/components/widget/lib/canvas-digital-gauge'; +import { AnimationRule } from '@home/components/widget/lib/analogue-gauge.models'; +import { FontSettings } from '@home/components/widget/lib/settings.models'; + +export interface AttributeSourceProperty { + valueSource: string; + entityAlias?: string; + attribute?: string; + value?: number; +} + +export interface FixedLevelColors { + from?: AttributeSourceProperty; + to?: AttributeSourceProperty; + color: string; +} + +export interface ColorLevelSetting { + value: number; + color: string; +} + +export type colorLevel = Array; + +export type attributesGaugeType = 'levelColors' | 'ticks'; + +export interface DigitalGaugeSettings { + minValue?: number; + maxValue?: number; + gaugeType?: GaugeType; + donutStartAngle?: number; + neonGlowBrightness?: number; + dashThickness?: number; + roundedLineCap?: boolean; + title?: string; + showTitle?: boolean; + unitTitle?: string; + showUnitTitle?: boolean; + showTimestamp?: boolean; + timestampFormat?: string; + showValue?: boolean; + showMinMax?: boolean; + gaugeWidthScale?: number; + defaultColor?: string; + gaugeColor?: string; + useFixedLevelColor?: boolean; + levelColors?: colorLevel; + fixedLevelColors?: FixedLevelColors[]; + animation?: boolean; + animationDuration?: number; + animationRule?: AnimationRule; + titleFont?: FontSettings; + labelFont?: FontSettings; + valueFont?: FontSettings; + minMaxFont?: FontSettings; + decimals?: number; + units?: string; + hideValue?: boolean; + hideMinMax?: boolean; + showTicks?: boolean; + ticksValue?: AttributeSourceProperty[]; + ticks?: number[]; + colorTicks?: string; + tickWidth?: number; +} + +export const digitalGaugeSettingsSchema: JsonSettingsSchema = { + schema: { + type: 'object', + title: 'Settings', + properties: { + minValue: { + title: 'Minimum value', + type: 'number', + default: 0 + }, + maxValue: { + title: 'Maximum value', + type: 'number', + default: 100 + }, + gaugeType: { + title: 'Gauge type', + type: 'string', + default: 'arc' + }, + donutStartAngle: { + title: 'Angle to start from when in donut mode', + type: 'number', + default: 90 + }, + neonGlowBrightness: { + title: 'Neon glow effect brightness, (0-100), 0 - disable effect', + type: 'number', + default: 0 + }, + dashThickness: { + title: 'Thickness of the stripes, 0 - no stripes', + type: 'number', + default: 0 + }, + roundedLineCap: { + title: 'Display rounded line cap', + type: 'boolean', + default: false + }, + title: { + title: 'Gauge title', + type: 'string', + default: null + }, + showTitle: { + title: 'Show gauge title', + type: 'boolean', + default: false + }, + unitTitle: { + title: 'Unit title', + type: 'string', + default: null + }, + showUnitTitle: { + title: 'Show unit title', + type: 'boolean', + default: false + }, + showTimestamp: { + title: 'Show value timestamp', + type: 'boolean', + default: false + }, + timestampFormat: { + title: 'Timestamp format', + type: 'string', + default: 'yyyy-MM-dd HH:mm:ss' + }, + showValue: { + title: 'Show value text', + type: 'boolean', + default: true + }, + showMinMax: { + title: 'Show min and max values', + type: 'boolean', + default: true + }, + gaugeWidthScale: { + title: 'Width of the gauge element', + type: 'number', + default: 0.75 + }, + defaultColor: { + title: 'Default color', + type: 'string', + default: null + }, + gaugeColor: { + title: 'Background color of the gauge element', + type: 'string', + default: null + }, + useFixedLevelColor: { + title: 'Use precise value for the color indicator', + type: 'boolean', + default: false + }, + levelColors: { + title: 'Colors of indicator, from lower to upper', + type: 'array', + items: { + title: 'Color', + type: 'string' + } + }, + fixedLevelColors: { + title: 'The colors for the indicator using boundary values', + type: 'array', + items: { + title: 'levelColor', + type: 'object', + properties: { + from: { + title: 'From', + type: 'object', + properties: { + valueSource: { + title: '[From] Value source', + type: 'string', + default: 'predefinedValue' + }, + entityAlias: { + title: '[From] Source entity alias', + type: 'string' + }, + attribute: { + title: '[From] Source entity attribute', + type: 'string' + }, + value: { + title: '[From] Value (if predefined value is selected)', + type: 'number' + } + } + }, + to: { + title: 'To', + type: 'object', + properties: { + valueSource: { + title: '[To] Value source', + type: 'string', + default: 'predefinedValue' + }, + entityAlias: { + title: '[To] Source entity alias', + type: 'string' + }, + attribute: { + title: '[To] Source entity attribute', + type: 'string' + }, + value: { + title: '[To] Value (if predefined value is selected)', + type: 'number' + } + } + }, + color: { + title: 'Color', + type: 'string' + } + } + } + }, + showTicks: { + title: 'Show ticks', + type: 'boolean', + default: false + }, + tickWidth: { + title: 'Width ticks', + type: 'number', + default: 4 + }, + colorTicks: { + title: 'Color ticks', + type: 'string', + default: '#666' + }, + ticksValue: { + title: 'The ticks predefined value', + type: 'array', + items: { + title: 'tickValue', + type: 'object', + properties: { + valueSource: { + title: 'Value source', + type: 'string', + default: 'predefinedValue' + }, + entityAlias: { + title: 'Source entity alias', + type: 'string' + }, + attribute: { + title: 'Source entity attribute', + type: 'string' + }, + value: { + title: 'Value (if predefined value is selected)', + type: 'number' + } + } + } + }, + animation: { + title: 'Enable animation', + type: 'boolean', + default: true + }, + animationDuration: { + title: 'Animation duration', + type: 'number', + default: 500 + }, + animationRule: { + title: 'Animation rule', + type: 'string', + default: 'linear' + }, + titleFont: { + title: 'Gauge title font', + type: 'object', + properties: { + family: { + title: 'Font family', + type: 'string', + default: 'Roboto' + }, + size: { + title: 'Size', + type: 'number', + default: 12 + }, + style: { + title: 'Style', + type: 'string', + default: 'normal' + }, + weight: { + title: 'Weight', + type: 'string', + default: '500' + }, + color: { + title: 'color', + type: 'string', + default: null + } + } + }, + labelFont: { + title: 'Font of label showing under value', + type: 'object', + properties: { + family: { + title: 'Font family', + type: 'string', + default: 'Roboto' + }, + size: { + title: 'Size', + type: 'number', + default: 8 + }, + style: { + title: 'Style', + type: 'string', + default: 'normal' + }, + weight: { + title: 'Weight', + type: 'string', + default: '500' + }, + color: { + title: 'color', + type: 'string', + default: null + } + } + }, + valueFont: { + title: 'Font of label showing current value', + type: 'object', + properties: { + family: { + title: 'Font family', + type: 'string', + default: 'Roboto' + }, + size: { + title: 'Size', + type: 'number', + default: 18 + }, + style: { + title: 'Style', + type: 'string', + default: 'normal' + }, + weight: { + title: 'Weight', + type: 'string', + default: '500' + }, + color: { + title: 'color', + type: 'string', + default: null + } + } + }, + minMaxFont: { + title: 'Font of minimum and maximum labels', + type: 'object', + properties: { + family: { + title: 'Font family', + type: 'string', + default: 'Roboto' + }, + size: { + title: 'Size', + type: 'number', + default: 10 + }, + style: { + title: 'Style', + type: 'string', + default: 'normal' + }, + weight: { + title: 'Weight', + type: 'string', + default: '500' + }, + color: { + title: 'color', + type: 'string', + default: null + } + } + } + } + }, + form: [ + 'minValue', + 'maxValue', + { + key: 'gaugeType', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'arc', + label: 'Arc' + }, + { + value: 'donut', + label: 'Donut' + }, + { + value: 'horizontalBar', + label: 'Horizontal bar' + }, + { + value: 'verticalBar', + label: 'Vertical bar' + } + ] + }, + 'donutStartAngle', + 'neonGlowBrightness', + 'dashThickness', + 'roundedLineCap', + 'title', + 'showTitle', + 'unitTitle', + 'showUnitTitle', + 'showTimestamp', + 'timestampFormat', + 'showValue', + 'showMinMax', + 'gaugeWidthScale', + { + key: 'defaultColor', + type: 'color' + }, + { + key: 'gaugeColor', + type: 'color' + }, + 'useFixedLevelColor', + { + key: 'levelColors', + condition: 'model.useFixedLevelColor !== true', + items: [ + { + key: 'levelColors[]', + type: 'color' + } + ] + }, + { + key: 'fixedLevelColors', + condition: 'model.useFixedLevelColor === true', + items: [ + { + key: 'fixedLevelColors[].from.valueSource', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'predefinedValue', + label: 'Predefined value (Default)' + }, + { + value: 'entityAttribute', + label: 'Value taken from entity attribute' + } + ] + }, + 'fixedLevelColors[].from.value', + 'fixedLevelColors[].from.entityAlias', + 'fixedLevelColors[].from.attribute', + { + key: 'fixedLevelColors[].to.valueSource', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'predefinedValue', + label: 'Predefined value (Default)' + }, + { + value: 'entityAttribute', + label: 'Value taken from entity attribute' + } + ] + }, + 'fixedLevelColors[].to.value', + 'fixedLevelColors[].to.entityAlias', + 'fixedLevelColors[].to.attribute', + { + key: 'fixedLevelColors[].color', + type: 'color' + } + ] + }, + 'showTicks', + { + key: 'tickWidth', + condition: 'model.showTicks === true' + }, + { + key: 'colorTicks', + condition: 'model.showTicks === true', + type: 'color' + }, + { + key: 'ticksValue', + condition: 'model.showTicks === true', + items: [ + { + key: 'ticksValue[].valueSource', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'predefinedValue', + label: 'Predefined value (Default)' + }, + { + value: 'entityAttribute', + label: 'Value taken from entity attribute' + } + ] + }, + 'ticksValue[].value', + 'ticksValue[].entityAlias', + 'ticksValue[].attribute' + ] + }, + 'animation', + 'animationDuration', + { + key: 'animationRule', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'linear', + label: 'Linear' + }, + { + value: 'quad', + label: 'Quad' + }, + { + value: 'quint', + label: 'Quint' + }, + { + value: 'cycle', + label: 'Cycle' + }, + { + value: 'bounce', + label: 'Bounce' + }, + { + value: 'elastic', + label: 'Elastic' + }, + { + value: 'dequad', + label: 'Dequad' + }, + { + value: 'dequint', + label: 'Dequint' + }, + { + value: 'decycle', + label: 'Decycle' + }, + { + value: 'debounce', + label: 'Debounce' + }, + { + value: 'delastic', + label: 'Delastic' + } + ] + }, + { + key: 'titleFont', + items: [ + 'titleFont.family', + 'titleFont.size', + { + key: 'titleFont.style', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'normal', + label: 'Normal' + }, + { + value: 'italic', + label: 'Italic' + }, + { + value: 'oblique', + label: 'Oblique' + } + ] + }, + { + key: 'titleFont.weight', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'normal', + label: 'Normal' + }, + { + value: 'bold', + label: 'Bold' + }, + { + value: 'bolder', + label: 'Bolder' + }, + { + value: 'lighter', + label: 'Lighter' + }, + { + value: '100', + label: '100' + }, + { + value: '200', + label: '200' + }, + { + value: '300', + label: '300' + }, + { + value: '400', + label: '400' + }, + { + value: '500', + label: '500' + }, + { + value: '600', + label: '600' + }, + { + value: '700', + label: '700' + }, + { + value: '800', + label: '800' + }, + { + value: '900', + label: '900' + } + ] + }, + { + key: 'titleFont.color', + type: 'color' + } + ] + }, + { + key: 'labelFont', + items: [ + 'labelFont.family', + 'labelFont.size', + { + key: 'labelFont.style', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'normal', + label: 'Normal' + }, + { + value: 'italic', + label: 'Italic' + }, + { + value: 'oblique', + label: 'Oblique' + } + ] + }, + { + key: 'labelFont.weight', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'normal', + label: 'Normal' + }, + { + value: 'bold', + label: 'Bold' + }, + { + value: 'bolder', + label: 'Bolder' + }, + { + value: 'lighter', + label: 'Lighter' + }, + { + value: '100', + label: '100' + }, + { + value: '200', + label: '200' + }, + { + value: '300', + label: '300' + }, + { + value: '400', + label: '400' + }, + { + value: '500', + label: '500' + }, + { + value: '600', + label: '600' + }, + { + value: '700', + label: '700' + }, + { + value: '800', + label: '800' + }, + { + value: '900', + label: '900' + } + ] + }, + { + key: 'labelFont.color', + type: 'color' + } + ] + }, + { + key: 'valueFont', + items: [ + 'valueFont.family', + 'valueFont.size', + { + key: 'valueFont.style', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'normal', + label: 'Normal' + }, + { + value: 'italic', + label: 'Italic' + }, + { + value: 'oblique', + label: 'Oblique' + } + ] + }, + { + key: 'valueFont.weight', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'normal', + label: 'Normal' + }, + { + value: 'bold', + label: 'Bold' + }, + { + value: 'bolder', + label: 'Bolder' + }, + { + value: 'lighter', + label: 'Lighter' + }, + { + value: '100', + label: '100' + }, + { + value: '200', + label: '200' + }, + { + value: '300', + label: '300' + }, + { + value: '400', + label: '400' + }, + { + value: '500', + label: '500' + }, + { + value: '600', + label: '600' + }, + { + value: '700', + label: '700' + }, + { + value: '800', + label: '800' + }, + { + value: '900', + label: '900' + } + ] + }, + { + key: 'valueFont.color', + type: 'color' + } + ] + }, + { + key: 'minMaxFont', + items: [ + 'minMaxFont.family', + 'minMaxFont.size', + { + key: 'minMaxFont.style', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'normal', + label: 'Normal' + }, + { + value: 'italic', + label: 'Italic' + }, + { + value: 'oblique', + label: 'Oblique' + } + ] + }, + { + key: 'minMaxFont.weight', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'normal', + label: 'Normal' + }, + { + value: 'bold', + label: 'Bold' + }, + { + value: 'bolder', + label: 'Bolder' + }, + { + value: 'lighter', + label: 'Lighter' + }, + { + value: '100', + label: '100' + }, + { + value: '200', + label: '200' + }, + { + value: '300', + label: '300' + }, + { + value: '400', + label: '400' + }, + { + value: '500', + label: '500' + }, + { + value: '600', + label: '600' + }, + { + value: '700', + label: '700' + }, + { + value: '800', + label: '800' + }, + { + value: '900', + label: '900' + } + ] + }, + { + key: 'minMaxFont.color', + type: 'color' + } + ] + } + ] +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts new file mode 100644 index 0000000000..11cc8dc732 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts @@ -0,0 +1,430 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import * as CanvasGauges from 'canvas-gauges'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { + attributesGaugeType, + AttributeSourceProperty, + ColorLevelSetting, + DigitalGaugeSettings, + digitalGaugeSettingsSchema, + FixedLevelColors +} from '@home/components/widget/lib/digital-gauge.models'; +import * as tinycolor_ from 'tinycolor2'; +import { isDefined, isDefinedAndNotNull } from '@core/utils'; +import { prepareFontSettings } from '@home/components/widget/lib/settings.models'; +import { CanvasDigitalGauge, CanvasDigitalGaugeOptions } from '@home/components/widget/lib/canvas-digital-gauge'; +import { DatePipe } from '@angular/common'; +import { + DataKey, + Datasource, + DatasourceData, + DatasourceType, + JsonSettingsSchema, + widgetType +} from '@shared/models/widget.models'; +import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { EMPTY, Observable } from 'rxjs'; +import GenericOptions = CanvasGauges.GenericOptions; + +const tinycolor = tinycolor_; + +const digitalGaugeSettingsSchemaValue = digitalGaugeSettingsSchema; + +export class TbCanvasDigitalGauge { + + static get settingsSchema(): JsonSettingsSchema { + return digitalGaugeSettingsSchemaValue; + } + + constructor(protected ctx: WidgetContext, canvasId: string) { + const gaugeElement = $('#' + canvasId, ctx.$container)[0]; + const settings: DigitalGaugeSettings = ctx.settings; + + this.localSettings = {}; + this.localSettings.minValue = settings.minValue || 0; + this.localSettings.maxValue = settings.maxValue || 100; + this.localSettings.gaugeType = settings.gaugeType || 'arc'; + this.localSettings.neonGlowBrightness = settings.neonGlowBrightness || 0; + this.localSettings.dashThickness = settings.dashThickness || 0; + this.localSettings.roundedLineCap = settings.roundedLineCap === true; + + const dataKey = ctx.data[0].dataKey; + const keyColor = settings.defaultColor || dataKey.color; + + this.localSettings.unitTitle = ((settings.showUnitTitle === true) ? + (settings.unitTitle && settings.unitTitle.length > 0 ? + settings.unitTitle : dataKey.label) : ''); + + this.localSettings.showTimestamp = settings.showTimestamp === true; + this.localSettings.timestampFormat = settings.timestampFormat && settings.timestampFormat.length ? + settings.timestampFormat : 'yyyy-MM-dd HH:mm:ss'; + + this.localSettings.gaugeWidthScale = settings.gaugeWidthScale || 0.75; + this.localSettings.gaugeColor = settings.gaugeColor || tinycolor(keyColor).setAlpha(0.2).toRgbString(); + + this.localSettings.useFixedLevelColor = settings.useFixedLevelColor || false; + if (!settings.useFixedLevelColor) { + if (!settings.levelColors || settings.levelColors.length === 0) { + this.localSettings.levelColors = [keyColor]; + } else { + this.localSettings.levelColors = settings.levelColors.slice(); + } + } else { + this.localSettings.levelColors = [keyColor]; + this.localSettings.fixedLevelColors = settings.fixedLevelColors || []; + } + + this.localSettings.showTicks = settings.showTicks || false; + this.localSettings.ticks = []; + this.localSettings.ticksValue = settings.ticksValue || []; + this.localSettings.tickWidth = settings.tickWidth || 4; + this.localSettings.colorTicks = settings.colorTicks || '#666'; + + this.localSettings.decimals = isDefined(dataKey.decimals) ? dataKey.decimals : + (isDefinedAndNotNull(settings.decimals) ? settings.decimals : ctx.decimals); + + this.localSettings.units = dataKey.units && dataKey.units.length ? dataKey.units : + (isDefined(settings.units) && settings.units.length > 0 ? settings.units : ctx.units); + + this.localSettings.hideValue = settings.showValue !== true; + this.localSettings.hideMinMax = settings.showMinMax !== true; + this.localSettings.donutStartAngle = isDefinedAndNotNull(settings.donutStartAngle) ? + -TbCanvasDigitalGauge.toRadians(settings.donutStartAngle) : null; + + this.localSettings.title = ((settings.showTitle === true) ? + (settings.title && settings.title.length > 0 ? + settings.title : dataKey.label) : ''); + + if (!this.localSettings.unitTitle && this.localSettings.showTimestamp) { + this.localSettings.unitTitle = ' '; + } + + this.localSettings.titleFont = prepareFontSettings(settings.titleFont, { + size: 12, + style: 'normal', + weight: '500', + color: keyColor + }); + + this.localSettings.valueFont = prepareFontSettings(settings.valueFont, { + size: 18, + style: 'normal', + weight: '500', + color: keyColor + }); + + this.localSettings.minMaxFont = prepareFontSettings(settings.minMaxFont, { + size: 10, + style: 'normal', + weight: '500', + color: keyColor + }); + + this.localSettings.labelFont = prepareFontSettings(settings.labelFont, { + size: 8, + style: 'normal', + weight: '500', + color: keyColor + }); + + const gaugeData: CanvasDigitalGaugeOptions = { + renderTo: gaugeElement, + + gaugeWidthScale: this.localSettings.gaugeWidthScale, + gaugeColor: this.localSettings.gaugeColor, + levelColors: this.localSettings.levelColors, + + colorTicks: this.localSettings.colorTicks, + tickWidth: this.localSettings.tickWidth, + ticks: this.localSettings.ticks, + + title: this.localSettings.title, + + fontTitleSize: this.localSettings.titleFont.size, + fontTitleStyle: this.localSettings.titleFont.style, + fontTitleWeight: this.localSettings.titleFont.weight, + colorTitle: this.localSettings.titleFont.color, + fontTitle: this.localSettings.titleFont.family, + + fontValueSize: this.localSettings.valueFont.size, + fontValueStyle: this.localSettings.valueFont.style, + fontValueWeight: this.localSettings.valueFont.weight, + colorValue: this.localSettings.valueFont.color, + fontValue: this.localSettings.valueFont.family, + + fontMinMaxSize: this.localSettings.minMaxFont.size, + fontMinMaxStyle: this.localSettings.minMaxFont.style, + fontMinMaxWeight: this.localSettings.minMaxFont.weight, + colorMinMax: this.localSettings.minMaxFont.color, + fontMinMax: this.localSettings.minMaxFont.family, + + fontLabelSize: this.localSettings.labelFont.size, + fontLabelStyle: this.localSettings.labelFont.style, + fontLabelWeight: this.localSettings.labelFont.weight, + colorLabel: this.localSettings.labelFont.color, + fontLabel: this.localSettings.labelFont.family, + + minValue: this.localSettings.minValue, + maxValue: this.localSettings.maxValue, + gaugeType: this.localSettings.gaugeType, + dashThickness: this.localSettings.dashThickness, + roundedLineCap: this.localSettings.roundedLineCap, + + symbol: this.localSettings.units, + label: this.localSettings.unitTitle, + showTimestamp: this.localSettings.showTimestamp, + hideValue: this.localSettings.hideValue, + hideMinMax: this.localSettings.hideMinMax, + + donutStartAngle: this.localSettings.donutStartAngle, + + valueDec: this.localSettings.decimals, + + neonGlowBrightness: this.localSettings.neonGlowBrightness, + + // animations + animation: settings.animation !== false && !ctx.isMobile, + animationDuration: isDefinedAndNotNull(settings.animationDuration) ? settings.animationDuration : 500, + animationRule: settings.animationRule || 'linear', + + isMobile: ctx.isMobile + }; + + this.gauge = new CanvasDigitalGauge(gaugeData).draw(); + this.init(); + } + + private localSettings: DigitalGaugeSettings; + private levelColorsSourcesSubscription: IWidgetSubscription; + private ticksSourcesSubscription: IWidgetSubscription; + + private gauge: CanvasDigitalGauge; + + static generateDatasource(ctx: WidgetContext, datasources: Datasource[], entityAlias: string, + attribute: string, settings: any): Datasource[]{ + const entityAliasId = ctx.aliasController.getEntityAliasId(entityAlias); + if (!entityAliasId) { + throw new Error('Not valid entity aliase name ' + entityAlias); + } + + const datasource = datasources.find((datasourceIteration) => { + return datasourceIteration.entityAliasId === entityAliasId; + }); + + const dataKey: DataKey = { + type: DataKeyType.attribute, + name: attribute, + label: attribute, + settings: [settings], + _hash: Math.random() + }; + + if (datasource) { + const findDataKey = datasource.dataKeys.find((dataKeyIteration) => { + return dataKeyIteration.name === attribute; + }); + + if (findDataKey) { + findDataKey.settings.push(settings); + } else { + datasource.dataKeys.push(dataKey); + } + } else { + const datasourceAttribute: Datasource = { + type: DatasourceType.entity, + name: entityAlias, + aliasName: entityAlias, + entityAliasId, + dataKeys: [dataKey] + }; + datasources.push(datasourceAttribute); + } + + return datasources; + } + + private static toRadians(angle: number): number { + return angle * (Math.PI / 180); + } + + init() { + let updateSetting = false; + + if (this.localSettings.useFixedLevelColor && this.localSettings.fixedLevelColors?.length > 0) { + this.localSettings.levelColors = this.settingLevelColorsSubscribe(this.localSettings.fixedLevelColors); + updateSetting = true; + } + if (this.localSettings.showTicks && this.localSettings.ticksValue?.length) { + this.localSettings.ticks = this.settingTicksSubscribe(this.localSettings.ticksValue); + updateSetting = true; + } + + if (updateSetting) { + this.updateSetting(); + } + } + + settingLevelColorsSubscribe(options: FixedLevelColors[]): ColorLevelSetting[] { + let levelColorsDatasource: Datasource[] = []; + const predefineLevelColors: ColorLevelSetting[] = []; + + function setLevelColor(levelSetting: AttributeSourceProperty, color: string) { + if (levelSetting.valueSource === 'predefinedValue' && isFinite(levelSetting.value)) { + predefineLevelColors.push({ + value: levelSetting.value, + color + }); + } else if (levelSetting.entityAlias && levelSetting.attribute) { + try { + levelColorsDatasource = TbCanvasDigitalGauge.generateDatasource(this.ctx, levelColorsDatasource, + levelSetting.entityAlias, levelSetting.attribute, {color, index: predefineLevelColors.length}); + } catch (e) { + return; + } + predefineLevelColors.push(null); + } + } + + for (const levelColor of options) { + if (levelColor.from) { + setLevelColor.call(this, levelColor.from, levelColor.color); + } + if (levelColor.to) { + setLevelColor.call(this, levelColor.to, levelColor.color); + } + } + + this.subscribeAttributes(levelColorsDatasource, 'levelColors').subscribe((subscription) => { + this.levelColorsSourcesSubscription = subscription; + }); + + return predefineLevelColors; + } + + settingTicksSubscribe(options: AttributeSourceProperty[]): number[] { + let ticksDatasource: Datasource[] = []; + const predefineTicks: number[] = []; + + for (const tick of options) { + if (tick.valueSource === 'predefinedValue' && isFinite(tick.value)) { + predefineTicks.push(tick.value); + } else if (tick.entityAlias && tick.attribute) { + try { + ticksDatasource = TbCanvasDigitalGauge + .generateDatasource(this.ctx, ticksDatasource, tick.entityAlias, tick.attribute, predefineTicks.length); + } catch (e) { + continue; + } + predefineTicks.push(null); + } + } + + this.subscribeAttributes(ticksDatasource, 'ticks').subscribe((subscription) => { + this.ticksSourcesSubscription = subscription; + }); + + return predefineTicks; + } + + subscribeAttributes(datasource: Datasource[], typeAttributes: attributesGaugeType): Observable { + if (!datasource.length) { + return EMPTY; + } + + const levelColorsSourcesSubscriptionOptions: WidgetSubscriptionOptions = { + datasources: datasource, + useDashboardTimewindow: false, + type: widgetType.latest, + callbacks: { + onDataUpdated: (subscription) => { + this.updateAttribute(subscription.data, typeAttributes); + } + } + }; + + return this.ctx.subscriptionApi.createSubscription(levelColorsSourcesSubscriptionOptions, true); + } + + updateAttribute(data: Array, typeAttributes: attributesGaugeType) { + for (const keyData of data) { + if (keyData && keyData.data && keyData.data[0]) { + const attrValue = keyData.data[0][1]; + if (isFinite(attrValue)) { + for (const setting of keyData.dataKey.settings) { + switch (typeAttributes) { + case 'levelColors': + this.localSettings.levelColors[setting.index] = { + value: attrValue, + color: setting.color + }; + break; + case 'ticks': + this.localSettings.ticks[setting] = attrValue; + break; + } + } + } + } + } + this.updateSetting(); + } + + updateSetting() { + (this.gauge.options as CanvasDigitalGaugeOptions).ticks = this.localSettings.ticks; + (this.gauge.options as CanvasDigitalGaugeOptions).levelColors = this.localSettings.levelColors; + this.gauge.options = CanvasDigitalGauge.configure(this.gauge.options); + this.gauge.update({} as CanvasDigitalGaugeOptions); + } + + update() { + if (this.ctx.data.length > 0) { + const cellData = this.ctx.data[0]; + if (cellData.data.length > 0) { + const tvPair = cellData.data[cellData.data.length - + 1]; + let timestamp; + if (this.localSettings.showTimestamp) { + timestamp = tvPair[0]; + const filter = this.ctx.$injector.get(DatePipe); + (this.gauge.options as CanvasDigitalGaugeOptions).label = + filter.transform(timestamp, this.localSettings.timestampFormat); + } + const value = tvPair[1]; + if (value !== this.gauge.value) { + if (!this.gauge.options.animation) { + this.gauge._value = value; + } + this.gauge.value = value; + } else if (this.localSettings.showTimestamp && this.gauge.timestamp !== timestamp) { + this.gauge.timestamp = timestamp; + } + } + } + } + + mobileModeChanged() { + const animation = this.ctx.settings.animation !== false && !this.ctx.isMobile; + this.gauge.update({animation, isMobile: this.ctx.isMobile} as CanvasDigitalGaugeOptions); + } + + resize() { + this.gauge.update({width: this.ctx.width, height: this.ctx.height} as GenericOptions); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.html new file mode 100644 index 0000000000..9502a35ede --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.html @@ -0,0 +1,24 @@ + +
+ + + {{ column.title }} + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.scss new file mode 100644 index 0000000000..c951de64ff --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.scss @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + width: 100%; + height: 100%; + min-width: 300px; + overflow: hidden; + background: #fff; + border-radius: 4px; + box-shadow: + 0 7px 8px -4px rgba(0, 0, 0, .2), + 0 13px 19px 2px rgba(0, 0, 0, .14), + 0 5px 24px 4px rgba(0, 0, 0, .12); + + .mat-content { + overflow: hidden; + background-color: #fff; + } + + .mat-padding { + padding: 16px; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.ts new file mode 100644 index 0000000000..d3fc0ae2dd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.ts @@ -0,0 +1,43 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, InjectionToken } from '@angular/core'; +import { DisplayColumn } from '@home/components/widget/lib/table-widget.models'; + +export const DISPLAY_COLUMNS_PANEL_DATA = new InjectionToken('DisplayColumnsPanelData'); + +export interface DisplayColumnsPanelData { + columns: DisplayColumn[]; + columnsUpdated: (columns: DisplayColumn[]) => void; +} + +@Component({ + selector: 'tb-display-columns-panel', + templateUrl: './display-columns-panel.component.html', + styleUrls: ['./display-columns-panel.component.scss'] +}) +export class DisplayColumnsPanelComponent { + + columns: DisplayColumn[]; + + constructor(@Inject(DISPLAY_COLUMNS_PANEL_DATA) public data: DisplayColumnsPanelData) { + this.columns = this.data.columns; + } + + public update() { + this.data.columnsUpdated(this.columns); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.component.html new file mode 100644 index 0000000000..3d000b55d4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.component.html @@ -0,0 +1,51 @@ + +
+
+ +
+ + +   + + + +
+
+
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.component.scss new file mode 100644 index 0000000000..abaa040a54 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.component.scss @@ -0,0 +1,121 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host-context(.tb-has-timewindow) { + .tb-entities-hierarchy { + mat-toolbar { + height: 60px; + max-height: 60px; + .mat-toolbar-tools { + height: 60px; + max-height: 60px; + } + } + } +} + +:host { + .tb-entities-hierarchy { + mat-toolbar.mat-table-toolbar:not([color="primary"]) { + background: transparent; + } + mat-toolbar { + min-height: 39px; + max-height: 39px; + .mat-toolbar-tools { + min-height: 39px; + max-height: 39px; + } + } + + .tb-entities-nav-tree-panel { + overflow-x: auto; + overflow-y: auto; + } + } +} + +:host ::ng-deep { + .tb-nav-tree-container { + &.jstree-proton { + .jstree-anchor { + div.node-icon { + display: inline-block; + width: 22px; + height: 22px; + margin-right: 2px; + margin-bottom: 2px; + background-color: transparent; + background-repeat: no-repeat; + background-attachment: scroll; + background-position: center center; + background-size: 18px 18px; + } + + mat-icon.node-icon { + width: 22px; + min-width: 22px; + height: 22px; + min-height: 22px; + margin-right: 2px; + margin-bottom: 2px; + color: inherit; + vertical-align: middle; + + &.material-icons { + font-size: 18px; + line-height: 22px; + text-align: center; + } + } + + &.jstree-hovered:not(.jstree-clicked), + &.jstree-disabled { + div.node-icon { + opacity: .5; + } + } + } + } + } + + @media (max-width: 768px) { + .tb-nav-tree-container { + &.jstree-proton-responsive { + .jstree-anchor { + div.node-icon { + width: 40px; + height: 40px; + margin: 0; + background-size: 24px 24px; + } + + mat-icon.node-icon { + width: 40px; + min-width: 40px; + height: 40px; + min-height: 40px; + margin: 0; + + &.material-icons { + font-size: 24px; + line-height: 40px; + } + } + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.component.ts new file mode 100644 index 0000000000..ebf9baf21e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.component.ts @@ -0,0 +1,477 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { WidgetAction, WidgetContext } from '@home/models/widget-component.models'; +import { DatasourceData, DatasourceType, WidgetConfig, widgetType } from '@shared/models/widget.models'; +import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; +import { UtilsService } from '@core/services/utils.service'; +import cssjs from '@core/css/css'; +import { fromEvent } from 'rxjs'; +import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; +import { constructTableCssString } from '@home/components/widget/lib/table-widget.models'; +import { Overlay } from '@angular/cdk/overlay'; +import { + LoadNodesCallback, + NavTreeEditCallbacks, + NodesCallback, + NodeSearchCallback, + NodeSelectedCallback, + NodesInsertedCallback +} from '@shared/components/nav-tree.component'; +import { EntityType } from '@shared/models/entity-type.models'; +import { deepClone, hashCode } from '@core/utils'; +import { + defaultNodeIconFunction, + defaultNodeOpenedFunction, + defaultNodeRelationQueryFunction, + defaultNodesSortFunction, + EntitiesHierarchyWidgetSettings, + HierarchyNavTreeNode, + HierarchyNodeContext, + HierarchyNodeDatasource, + iconUrlHtml, + loadNodeCtxFunction, + materialIconHtml, + NodeDisabledFunction, + NodeHasChildrenFunction, + NodeIconFunction, + NodeOpenedFunction, + NodeRelationQueryFunction, + NodesSortFunction, + NodeTextFunction +} from '@home/components/widget/lib/entities-hierarchy-widget.models'; +import { EntityRelationsQuery } from '@shared/models/relation.models'; +import { AliasFilterType, RelationsQueryFilter } from '@shared/models/alias.models'; +import { EntityFilter } from '@shared/models/query/query.models'; + +@Component({ + selector: 'tb-entities-hierarchy-widget', + templateUrl: './entities-hierarchy-widget.component.html', + styleUrls: ['./entities-hierarchy-widget.component.scss'] +}) +export class EntitiesHierarchyWidgetComponent extends PageComponent implements OnInit, AfterViewInit { + + @Input() + ctx: WidgetContext; + + @ViewChild('searchInput') searchInputField: ElementRef; + + public toastTargetId = 'entities-hierarchy-' + this.utils.guid(); + + public textSearchMode = false; + public textSearch = null; + + public nodeEditCallbacks: NavTreeEditCallbacks = {}; + + private settings: EntitiesHierarchyWidgetSettings; + private widgetConfig: WidgetConfig; + private subscription: IWidgetSubscription; + private datasources: Array; + private data: Array>; + + private nodesMap: {[nodeId: string]: HierarchyNavTreeNode} = {}; + private pendingUpdateNodeTasks: {[nodeId: string]: () => void} = {}; + private nodeIdCounter = 0; + + private nodeRelationQueryFunction: NodeRelationQueryFunction; + private nodeIconFunction: NodeIconFunction; + private nodeTextFunction: NodeTextFunction; + private nodeDisabledFunction: NodeDisabledFunction; + private nodeOpenedFunction: NodeOpenedFunction; + private nodeHasChildrenFunction: NodeHasChildrenFunction; + private nodesSortFunction: NodesSortFunction; + + private searchAction: WidgetAction = { + name: 'action.search', + show: true, + icon: 'search', + onAction: () => { + this.enterFilterMode(); + } + }; + + constructor(protected store: Store, + private elementRef: ElementRef, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, + private utils: UtilsService) { + super(store); + } + + ngOnInit(): void { + this.ctx.$scope.entitiesHierarchyWidget = this; + this.settings = this.ctx.settings; + this.widgetConfig = this.ctx.widgetConfig; + this.subscription = this.ctx.defaultSubscription; + this.datasources = this.subscription.datasources as Array; + this.data = this.subscription.dataPages[0].data; + this.initializeConfig(); + this.ctx.updateWidgetParams(); + } + + ngAfterViewInit(): void { + fromEvent(this.searchInputField.nativeElement, 'keyup') + .pipe( + debounceTime(150), + distinctUntilChanged(), + tap(() => { + this.updateSearchNodes(); + }) + ) + .subscribe(); + } + + public onDataUpdated() { + this.updateNodeData(this.subscription.data); + } + + private initializeConfig() { + this.ctx.widgetActions = [this.searchAction]; + + const testNodeCtx: HierarchyNodeContext = { + entity: { + id: { + entityType: EntityType.DEVICE, + id: '123' + }, + name: 'TEST DEV1' + }, + data: {}, + level: 2 + }; + const parentNodeCtx = deepClone(testNodeCtx); + parentNodeCtx.level = 1; + testNodeCtx.parentNodeCtx = parentNodeCtx; + + this.nodeRelationQueryFunction = loadNodeCtxFunction(this.settings.nodeRelationQueryFunction, 'nodeCtx', testNodeCtx); + this.nodeIconFunction = loadNodeCtxFunction(this.settings.nodeIconFunction, 'nodeCtx', testNodeCtx); + this.nodeTextFunction = loadNodeCtxFunction(this.settings.nodeTextFunction, 'nodeCtx', testNodeCtx); + this.nodeDisabledFunction = loadNodeCtxFunction(this.settings.nodeDisabledFunction, 'nodeCtx', testNodeCtx); + this.nodeOpenedFunction = loadNodeCtxFunction(this.settings.nodeOpenedFunction, 'nodeCtx', testNodeCtx); + this.nodeHasChildrenFunction = loadNodeCtxFunction(this.settings.nodeHasChildrenFunction, 'nodeCtx', testNodeCtx); + + const testNodeCtx2 = deepClone(testNodeCtx); + testNodeCtx2.entity.name = 'TEST DEV2'; + + this.nodesSortFunction = loadNodeCtxFunction(this.settings.nodesSortFunction, 'nodeCtx1,nodeCtx2', testNodeCtx, testNodeCtx2); + + this.nodeRelationQueryFunction = this.nodeRelationQueryFunction || defaultNodeRelationQueryFunction; + this.nodeIconFunction = this.nodeIconFunction || defaultNodeIconFunction; + this.nodeTextFunction = this.nodeTextFunction || ((nodeCtx) => nodeCtx.entity.name); + this.nodeDisabledFunction = this.nodeDisabledFunction || (() => false); + this.nodeOpenedFunction = this.nodeOpenedFunction || defaultNodeOpenedFunction; + this.nodeHasChildrenFunction = this.nodeHasChildrenFunction || (() => true); + this.nodesSortFunction = this.nodesSortFunction || defaultNodesSortFunction; + + const cssString = constructTableCssString(this.widgetConfig); + const cssParser = new cssjs(); + cssParser.testMode = false; + const namespace = 'entities-hierarchy-' + hashCode(cssString); + cssParser.cssPreviewNamespace = namespace; + cssParser.createStyleElement(namespace, cssString); + $(this.elementRef.nativeElement).addClass(namespace); + } + + private enterFilterMode() { + this.textSearchMode = true; + this.textSearch = ''; + this.ctx.hideTitlePanel = true; + this.ctx.detectChanges(true); + setTimeout(() => { + this.searchInputField.nativeElement.focus(); + this.searchInputField.nativeElement.setSelectionRange(0, 0); + }, 10); + } + + exitFilterMode() { + this.textSearchMode = false; + this.textSearch = null; + this.updateSearchNodes(); + this.ctx.hideTitlePanel = false; + this.ctx.detectChanges(true); + } + + private updateSearchNodes() { + if (this.textSearch != null) { + this.nodeEditCallbacks.search(this.textSearch); + } else { + this.nodeEditCallbacks.clearSearch(); + } + } + + private updateNodeData(subscriptionData: Array) { + const affectedNodes: string[] = []; + if (subscriptionData) { + subscriptionData.forEach((datasourceData) => { + const datasource = datasourceData.datasource as HierarchyNodeDatasource; + if (datasource.nodeId) { + const node = this.nodesMap[datasource.nodeId]; + const key = datasourceData.dataKey.label; + let value; + if (datasourceData.data && datasourceData.data.length) { + value = datasourceData.data[0][1]; + } + if (node.data.nodeCtx.data[key] !== value) { + if (affectedNodes.indexOf(datasource.nodeId) === -1) { + affectedNodes.push(datasource.nodeId); + } + node.data.nodeCtx.data[key] = value; + } + } + }); + } + affectedNodes.forEach((nodeId) => { + const node: HierarchyNavTreeNode = this.nodeEditCallbacks.getNode(nodeId); + if (node) { + this.updateNodeStyle(this.nodesMap[nodeId]); + } else { + this.pendingUpdateNodeTasks[nodeId] = () => { + this.updateNodeStyle(this.nodesMap[nodeId]); + }; + } + }); + } + + public loadNodes: LoadNodesCallback = (node, cb) => { + if (node.id === '#') { + const childNodes: HierarchyNavTreeNode[] = []; + this.datasources.forEach((childDatasource, index) => { + childNodes.push(this.datasourceToNode(childDatasource as HierarchyNodeDatasource, this.data[index])); + }); + cb(this.prepareNodes(childNodes)); + } else { + if (node.data && node.data.nodeCtx.entity && node.data.nodeCtx.entity.id && node.data.nodeCtx.entity.id.entityType !== 'function') { + this.loadChildren(node, node.data.datasource, cb); + /* (error) => { // TODO: + let errorText = 'Failed to get relations!'; + if (error && error.status === 400) { + errorText = 'Invalid relations query returned by \'Node relations query function\'! Please check widget configuration!'; + } + this.showError(errorText); + } + ); + */ + } else { + cb([]); + } + } + } + + public onNodeSelected: NodeSelectedCallback = (node, event) => { + let nodeId; + if (!node) { + nodeId = -1; + } else { + nodeId = node.id; + } + if (nodeId !== -1) { + const selectedNode = this.nodesMap[nodeId]; + if (selectedNode) { + const descriptors = this.ctx.actionsApi.getActionDescriptors('nodeSelected'); + if (descriptors.length) { + const entity = selectedNode.data.nodeCtx.entity; + this.ctx.actionsApi.handleWidgetAction(event, descriptors[0], entity.id, entity.name, { nodeCtx: selectedNode.data.nodeCtx }); + } + } + } + } + + public onNodesInserted: NodesInsertedCallback = (nodes) => { + if (nodes) { + nodes.forEach((nodeId) => { + const task = this.pendingUpdateNodeTasks[nodeId]; + if (task) { + task(); + delete this.pendingUpdateNodeTasks[nodeId]; + } + }); + } + } + + public searchCallback: NodeSearchCallback = (searchText, node) => { + const theNode = this.nodesMap[node.id]; + if (theNode && theNode.data.searchText) { + return theNode.data.searchText.includes(searchText.toLowerCase()); + } + return false; + } + + private updateNodeStyle(node: HierarchyNavTreeNode) { + const newText = this.prepareNodeText(node); + if (node.text !== newText) { + node.text = newText; + this.nodeEditCallbacks.updateNode(node.id, node.text); + } + const newDisabled = this.nodeDisabledFunction(node.data.nodeCtx); + if (node.state.disabled !== newDisabled) { + node.state.disabled = newDisabled; + if (node.state.disabled) { + this.nodeEditCallbacks.disableNode(node.id); + } else { + this.nodeEditCallbacks.enableNode(node.id); + } + } + const newHasChildren = this.nodeHasChildrenFunction(node.data.nodeCtx); + if (node.children !== newHasChildren) { + node.children = newHasChildren; + this.nodeEditCallbacks.setNodeHasChildren(node.id, node.children); + } + } + + private prepareNodes(nodes: HierarchyNavTreeNode[]): HierarchyNavTreeNode[] { + nodes = nodes.filter((node) => node !== null); + nodes.sort((node1, node2) => this.nodesSortFunction(node1.data.nodeCtx, node2.data.nodeCtx)); + return nodes; + } + + private prepareNodeText(node: HierarchyNavTreeNode): string { + const nodeIcon = this.prepareNodeIcon(node.data.nodeCtx); + const nodeText = this.nodeTextFunction(node.data.nodeCtx); + node.data.searchText = nodeText ? nodeText.replace(/<[^>]+>/g, '').toLowerCase() : ''; + return nodeIcon + nodeText; + } + + private prepareNodeIcon(nodeCtx: HierarchyNodeContext): string { + let iconInfo = this.nodeIconFunction(nodeCtx); + if (iconInfo) { + if (iconInfo === 'default') { + iconInfo = defaultNodeIconFunction(nodeCtx); + } + if (iconInfo && iconInfo !== 'default' && (iconInfo.iconUrl || iconInfo.materialIcon)) { + if (iconInfo.materialIcon) { + return materialIconHtml(iconInfo.materialIcon); + } else { + return iconUrlHtml(iconInfo.iconUrl); + } + } else { + return ''; + } + } else { + return ''; + } + } + + private datasourceToNode(datasource: HierarchyNodeDatasource, + data: DatasourceData[], + parentNodeCtx?: HierarchyNodeContext): HierarchyNavTreeNode { + const node: HierarchyNavTreeNode = { + id: (++this.nodeIdCounter) + '' + }; + this.nodesMap[node.id] = node; + datasource.nodeId = node.id; + node.icon = false; + const nodeCtx: HierarchyNodeContext = { + parentNodeCtx, + entity: { + id: { + id: datasource.entityId, + entityType: datasource.entityType + }, + name: datasource.entityName, + label: datasource.entityLabel ? datasource.entityLabel : datasource.entityName + }, + data: {} + }; + datasource.dataKeys.forEach((dataKey, index) => { + const keyData = data[index].data; + if (keyData && keyData.length && keyData[0].length > 1) { + nodeCtx.data[dataKey.label] = keyData[0][1]; + } else { + nodeCtx.data[dataKey.label] = ''; + } + }); + nodeCtx.level = parentNodeCtx ? parentNodeCtx.level + 1 : 1; + node.data = { + datasource, + nodeCtx + }; + node.state = { + disabled: this.nodeDisabledFunction(node.data.nodeCtx), + opened: this.nodeOpenedFunction(node.data.nodeCtx) + }; + node.text = this.prepareNodeText(node); + node.children = this.nodeHasChildrenFunction(node.data.nodeCtx); + return node; + } + + private loadChildren(parentNode: HierarchyNavTreeNode, datasource: HierarchyNodeDatasource, childrenNodesLoadCb: NodesCallback) { + const nodeCtx = parentNode.data.nodeCtx; + nodeCtx.childrenNodesLoaded = false; + const entityFilter = this.prepareNodeRelationsQueryFilter(nodeCtx); + const childrenDatasource = { + dataKeys: datasource.dataKeys, + type: DatasourceType.entity, + filterId: datasource.filterId, + entityFilter + } as HierarchyNodeDatasource; + const subscriptionOptions: WidgetSubscriptionOptions = { + type: widgetType.latest, + datasources: [childrenDatasource], + callbacks: { + onSubscriptionMessage: (subscription, message) => { + this.ctx.showToast(message.severity, message.message, undefined, + 'bottom', 'left', this.toastTargetId); + }, + onInitialPageDataChanged: (subscription) => { + this.ctx.subscriptionApi.removeSubscription(subscription.id); + this.nodeEditCallbacks.refreshNode(parentNode.id); + }, + onDataUpdated: subscription => { + if (nodeCtx.childrenNodesLoaded) { + this.updateNodeData(subscription.data); + } else { + const datasourcesPageData = subscription.datasourcePages[0]; + const dataPageData = subscription.dataPages[0]; + const childNodes: HierarchyNavTreeNode[] = []; + datasourcesPageData.data.forEach((childDatasource, index) => { + childNodes.push(this.datasourceToNode(childDatasource as HierarchyNodeDatasource, dataPageData.data[index])); + }); + nodeCtx.childrenNodesLoaded = true; + childrenNodesLoadCb(this.prepareNodes(childNodes)); + } + } + } + }; + this.ctx.subscriptionApi.createSubscription(subscriptionOptions, true); + } + + private prepareNodeRelationQuery(nodeCtx: HierarchyNodeContext): EntityRelationsQuery { + let relationQuery = this.nodeRelationQueryFunction(nodeCtx); + if (relationQuery && relationQuery === 'default') { + relationQuery = defaultNodeRelationQueryFunction(nodeCtx); + } + return relationQuery as EntityRelationsQuery; + } + + private prepareNodeRelationsQueryFilter(nodeCtx: HierarchyNodeContext): EntityFilter { + const relationQuery = this.prepareNodeRelationQuery(nodeCtx); + return { + rootEntity: { + id: relationQuery.parameters.rootId, + entityType: relationQuery.parameters.rootType + }, + direction: relationQuery.parameters.direction, + filters: relationQuery.filters, + maxLevel: relationQuery.parameters.maxLevel, + fetchLastLevelOnly: relationQuery.parameters.fetchLastLevelOnly, + type: AliasFilterType.relationsQuery + } as RelationsQueryFilter; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts new file mode 100644 index 0000000000..7315bd1286 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts @@ -0,0 +1,161 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { BaseData } from '@shared/models/base-data'; +import { EntityId } from '@shared/models/id/entity-id'; +import { NavTreeNode, NodesCallback } from '@shared/components/nav-tree.component'; +import { Datasource } from '@shared/models/widget.models'; +import { isDefined, isUndefined } from '@core/utils'; +import { EntityRelationsQuery, EntitySearchDirection, RelationTypeGroup } from '@shared/models/relation.models'; +import { EntityType } from '@shared/models/entity-type.models'; + +export interface EntitiesHierarchyWidgetSettings { + nodeRelationQueryFunction: string; + nodeHasChildrenFunction: string; + nodeOpenedFunction: string; + nodeDisabledFunction: string; + nodeIconFunction: string; + nodeTextFunction: string; + nodesSortFunction: string; +} + +export interface HierarchyNodeContext { + parentNodeCtx?: HierarchyNodeContext; + entity: BaseData; + childrenNodesLoaded?: boolean; + level?: number; + data: {[key: string]: any}; +} + +export interface HierarchyNavTreeNode extends NavTreeNode { + data?: { + datasource: HierarchyNodeDatasource; + nodeCtx: HierarchyNodeContext; + searchText?: string; + }; +} + +export interface HierarchyNodeDatasource extends Datasource { + nodeId: string; +} + +export interface HierarchyNodeIconInfo { + iconUrl?: string; + materialIcon?: string; +} + +export type NodeRelationQueryFunction = (nodeCtx: HierarchyNodeContext) => EntityRelationsQuery | 'default'; +export type NodeTextFunction = (nodeCtx: HierarchyNodeContext) => string; +export type NodeDisabledFunction = (nodeCtx: HierarchyNodeContext) => boolean; +export type NodeIconFunction = (nodeCtx: HierarchyNodeContext) => HierarchyNodeIconInfo | 'default'; +export type NodeOpenedFunction = (nodeCtx: HierarchyNodeContext) => boolean; +export type NodeHasChildrenFunction = (nodeCtx: HierarchyNodeContext) => boolean; +export type NodesSortFunction = (nodeCtx1: HierarchyNodeContext, nodeCtx2: HierarchyNodeContext) => number; + +export function loadNodeCtxFunction any>(functionBody: string, argNames: string, ...args: any[]): F { + let nodeCtxFunction: F = null; + if (isDefined(functionBody) && functionBody.length) { + try { + nodeCtxFunction = new Function(argNames, functionBody) as F; + const res = nodeCtxFunction.apply(null, args); + if (isUndefined(res)) { + nodeCtxFunction = null; + } + } catch (e) { + nodeCtxFunction = null; + } + } + return nodeCtxFunction; +} + +export function materialIconHtml(materialIcon: string): string { + return '' + materialIcon + ''; +} + +export function iconUrlHtml(iconUrl: string): string { + return '
 
'; +} + +export const defaultNodeRelationQueryFunction: NodeRelationQueryFunction = nodeCtx => { + const entity = nodeCtx.entity; + const query: EntityRelationsQuery = { + parameters: { + rootId: entity.id.id, + rootType: entity.id.entityType as EntityType, + direction: EntitySearchDirection.FROM, + relationTypeGroup: RelationTypeGroup.COMMON, + maxLevel: 1 + }, + filters: [ + { + relationType: 'Contains', + entityTypes: [] + } + ] + }; + return query; +}; + +export const defaultNodeIconFunction: NodeIconFunction = nodeCtx => { + let materialIcon = 'insert_drive_file'; + const entity = nodeCtx.entity; + if (entity && entity.id && entity.id.entityType) { + switch (entity.id.entityType as EntityType | string) { + case 'function': + materialIcon = 'functions'; + break; + case EntityType.DEVICE: + materialIcon = 'devices_other'; + break; + case EntityType.ASSET: + materialIcon = 'domain'; + break; + case EntityType.TENANT: + materialIcon = 'supervisor_account'; + break; + case EntityType.CUSTOMER: + materialIcon = 'supervisor_account'; + break; + case EntityType.USER: + materialIcon = 'account_circle'; + break; + case EntityType.DASHBOARD: + materialIcon = 'dashboards'; + break; + case EntityType.ALARM: + materialIcon = 'notifications_active'; + break; + case EntityType.ENTITY_VIEW: + materialIcon = 'view_quilt'; + break; + } + } + return { + materialIcon + }; +}; + +export const defaultNodeOpenedFunction: NodeOpenedFunction = nodeCtx => { + return nodeCtx.level <= 4; +}; + +export const defaultNodesSortFunction: NodesSortFunction = (nodeCtx1, nodeCtx2) => { + let result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType); + if (result === 0) { + result = nodeCtx1.entity.name.localeCompare(nodeCtx2.entity.name); + } + return result; +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html new file mode 100644 index 0000000000..2eb4b372b9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html @@ -0,0 +1,104 @@ + +
+
+ +
+ + +   + + + +
+
+
+ + + {{ column.title }} + + + + + + + +
+ +
+
+ + + + +
+
+
+ + +
+ entity.no-entities-prompt + {{ 'common.loading' | translate }} +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.scss new file mode 100644 index 0000000000..7fe357b7c2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.scss @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + width: 100%; + height: 100%; + .tb-table-widget { + .table-container { + position: relative; + } + .mat-table { + .mat-row { + &.invisible { + visibility: hidden; + } + } + } + span.no-data-found { + position: absolute; + top: 60px; + bottom: 0; + left: 0; + right: 0; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts new file mode 100644 index 0000000000..b9791b5ae9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts @@ -0,0 +1,720 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + Component, + ElementRef, + Input, + NgZone, + OnInit, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { WidgetAction, WidgetContext } from '@home/models/widget-component.models'; +import { + DataKey, + Datasource, + DatasourceData, + WidgetActionDescriptor, + WidgetConfig +} from '@shared/models/widget.models'; +import { IWidgetSubscription } from '@core/api/widget-api.models'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { createLabelFromDatasource, deepClone, hashCode, isDefined, isNumber } from '@core/utils'; +import cssjs from '@core/css/css'; +import { CollectionViewer, DataSource } from '@angular/cdk/collections'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { BehaviorSubject, fromEvent, merge, Observable } from 'rxjs'; +import { emptyPageData, PageData } from '@shared/models/page/page-data'; +import { EntityId } from '@shared/models/id/entity-id'; +import { entityTypeTranslations } from '@shared/models/entity-type.models'; +import { debounceTime, distinctUntilChanged, map, tap } from 'rxjs/operators'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort, SortDirection } from '@angular/material/sort'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import { + CellContentInfo, + CellStyleInfo, + constructTableCssString, + DisplayColumn, + EntityColumn, + EntityData, + entityDataSortOrderFromString, + findColumnByEntityKey, + findEntityKeyByColumnDef, + fromEntityColumnDef, + getCellContentInfo, + getCellStyleInfo, + getColumnWidth, + getEntityValue, + TableWidgetDataKeySettings, + TableWidgetSettings, + widthStyle +} from '@home/components/widget/lib/table-widget.models'; +import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; +import { + DISPLAY_COLUMNS_PANEL_DATA, + DisplayColumnsPanelComponent, + DisplayColumnsPanelData +} from '@home/components/widget/lib/display-columns-panel.component'; +import { + dataKeyToEntityKey, + Direction, + EntityDataPageLink, + entityDataPageLinkSortDirection, + EntityKeyType, + KeyFilter +} from '@shared/models/query/query.models'; +import { sortItems } from '@shared/models/page/page-link'; +import { entityFields } from '@shared/models/entity.models'; +import { DatePipe } from '@angular/common'; + +interface EntitiesTableWidgetSettings extends TableWidgetSettings { + entitiesTitle: string; + displayEntityName: boolean; + entityNameColumnTitle: string; + displayEntityLabel: boolean; + entityLabelColumnTitle: string; + displayEntityType: boolean; +} + +@Component({ + selector: 'tb-entities-table-widget', + templateUrl: './entities-table-widget.component.html', + styleUrls: ['./entities-table-widget.component.scss', './table-widget.scss'] +}) +export class EntitiesTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit { + + @Input() + ctx: WidgetContext; + + @ViewChild('searchInput') searchInputField: ElementRef; + @ViewChild(MatPaginator) paginator: MatPaginator; + @ViewChild(MatSort) sort: MatSort; + + public displayPagination = true; + public pageSizeOptions; + public pageLink: EntityDataPageLink; + public sortOrderProperty: string; + public textSearchMode = false; + public columns: Array = []; + public displayedColumns: string[] = []; + public actionCellDescriptors: WidgetActionDescriptor[]; + public entityDatasource: EntityDatasource; + + private settings: EntitiesTableWidgetSettings; + private widgetConfig: WidgetConfig; + private subscription: IWidgetSubscription; + + private entitiesTitlePattern: string; + + private defaultPageSize = 10; + private defaultSortOrder = 'entityName'; + + private contentsInfo: {[key: string]: CellContentInfo} = {}; + private stylesInfo: {[key: string]: CellStyleInfo} = {}; + private columnWidth: {[key: string]: string} = {}; + + private searchAction: WidgetAction = { + name: 'action.search', + show: true, + icon: 'search', + onAction: () => { + this.enterFilterMode(); + } + }; + + private columnDisplayAction: WidgetAction = { + name: 'entity.columns-to-display', + show: true, + icon: 'view_column', + onAction: ($event) => { + this.editColumnsToDisplay($event); + } + }; + + constructor(protected store: Store, + private elementRef: ElementRef, + private ngZone: NgZone, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, + private utils: UtilsService, + private datePipe: DatePipe, + private translate: TranslateService, + private domSanitizer: DomSanitizer) { + super(store); + this.pageLink = { + page: 0, + pageSize: this.defaultPageSize, + textSearch: null, + dynamic: true + }; + } + + ngOnInit(): void { + this.ctx.$scope.entitiesTableWidget = this; + this.settings = this.ctx.settings; + this.widgetConfig = this.ctx.widgetConfig; + this.subscription = this.ctx.defaultSubscription; + this.initializeConfig(); + this.updateDatasources(); + this.ctx.updateWidgetParams(); + } + + ngAfterViewInit(): void { + fromEvent(this.searchInputField.nativeElement, 'keyup') + .pipe( + debounceTime(150), + distinctUntilChanged(), + tap(() => { + if (this.displayPagination) { + this.paginator.pageIndex = 0; + } + this.updateData(); + }) + ) + .subscribe(); + + if (this.displayPagination) { + this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); + } + ((this.displayPagination ? merge(this.sort.sortChange, this.paginator.page) : this.sort.sortChange) as Observable) + .pipe( + tap(() => this.updateData()) + ) + .subscribe(); + this.updateData(); + } + + public onDataUpdated() { + this.updateTitle(true); + this.entityDatasource.dataUpdated(); + } + + public pageLinkSortDirection(): SortDirection { + return entityDataPageLinkSortDirection(this.pageLink); + } + + private initializeConfig() { + this.ctx.widgetActions = [this.searchAction, this.columnDisplayAction]; + + this.actionCellDescriptors = this.ctx.actionsApi.getActionDescriptors('actionCellButton'); + + if (this.settings.entitiesTitle && this.settings.entitiesTitle.length) { + this.entitiesTitlePattern = this.utils.customTranslation(this.settings.entitiesTitle, this.settings.entitiesTitle); + } else { + this.entitiesTitlePattern = this.translate.instant('entity.entities'); + } + + this.updateTitle(false); + + this.searchAction.show = isDefined(this.settings.enableSearch) ? this.settings.enableSearch : true; + this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true; + this.columnDisplayAction.show = isDefined(this.settings.enableSelectColumnDisplay) ? this.settings.enableSelectColumnDisplay : true; + + const pageSize = this.settings.defaultPageSize; + if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) { + this.defaultPageSize = pageSize; + } + this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3]; + this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : 1024; + + const cssString = constructTableCssString(this.widgetConfig); + const cssParser = new cssjs(); + cssParser.testMode = false; + const namespace = 'entities-table-' + hashCode(cssString); + cssParser.cssPreviewNamespace = namespace; + cssParser.createStyleElement(namespace, cssString); + $(this.elementRef.nativeElement).addClass(namespace); + } + + private updateTitle(updateWidgetParams = false) { + const newTitle = createLabelFromDatasource(this.subscription.datasources[0], this.entitiesTitlePattern); + if (this.ctx.widgetTitle !== newTitle) { + this.ctx.widgetTitle = newTitle; + if (updateWidgetParams) { + this.ctx.updateWidgetParams(); + } + } + } + + private updateDatasources() { + + const displayEntityName = isDefined(this.settings.displayEntityName) ? this.settings.displayEntityName : true; + const displayEntityLabel = isDefined(this.settings.displayEntityLabel) ? this.settings.displayEntityLabel : false; + let entityNameColumnTitle: string; + let entityLabelColumnTitle: string; + if (this.settings.entityNameColumnTitle && this.settings.entityNameColumnTitle.length) { + entityNameColumnTitle = this.utils.customTranslation(this.settings.entityNameColumnTitle, this.settings.entityNameColumnTitle); + } else { + entityNameColumnTitle = this.translate.instant('entity.entity-name'); + } + if (this.settings.entityLabelColumnTitle && this.settings.entityLabelColumnTitle.length) { + entityLabelColumnTitle = this.utils.customTranslation(this.settings.entityLabelColumnTitle, this.settings.entityLabelColumnTitle); + } else { + entityLabelColumnTitle = this.translate.instant('entity.entity-label'); + } + const displayEntityType = isDefined(this.settings.displayEntityType) ? this.settings.displayEntityType : true; + + if (displayEntityName) { + this.columns.push( + { + name: 'entityName', + label: 'entityName', + def: 'entityName', + title: entityNameColumnTitle, + entityKey: { + key: 'name', + type: EntityKeyType.ENTITY_FIELD + } + } as EntityColumn + ); + this.contentsInfo.entityName = { + useCellContentFunction: false + }; + this.stylesInfo.entityName = { + useCellStyleFunction: false + }; + this.columnWidth.entityName = '0px'; + } + if (displayEntityLabel) { + this.columns.push( + { + name: 'entityLabel', + label: 'entityLabel', + def: 'entityLabel', + title: entityLabelColumnTitle, + entityKey: { + key: 'label', + type: EntityKeyType.ENTITY_FIELD + } + } as EntityColumn + ); + this.contentsInfo.entityLabel = { + useCellContentFunction: false + }; + this.stylesInfo.entityLabel = { + useCellStyleFunction: false + }; + this.columnWidth.entityLabel = '0px'; + } + if (displayEntityType) { + this.columns.push( + { + name: 'entityType', + label: 'entityType', + def: 'entityType', + title: this.translate.instant('entity.entity-type'), + entityKey: { + key: 'entityType', + type: EntityKeyType.ENTITY_FIELD + } + } as EntityColumn + ); + this.contentsInfo.entityType = { + useCellContentFunction: false + }; + this.stylesInfo.entityType = { + useCellStyleFunction: false + }; + this.columnWidth.entityType = '0px'; + } + + const dataKeys: Array = []; + + const datasource = this.subscription.options.datasources ? this.subscription.options.datasources[0] : null; + + if (datasource && datasource.dataKeys) { + datasource.dataKeys.forEach((entityDataKey) => { + const dataKey: EntityColumn = deepClone(entityDataKey) as EntityColumn; + dataKey.entityKey = dataKeyToEntityKey(entityDataKey); + if (dataKey.type === DataKeyType.function) { + dataKey.name = dataKey.label; + } + dataKeys.push(dataKey); + + dataKey.label = this.utils.customTranslation(dataKey.label, dataKey.label); + dataKey.title = dataKey.label; + dataKey.def = 'def' + this.columns.length; + const keySettings: TableWidgetDataKeySettings = dataKey.settings; + if (dataKey.type === DataKeyType.entityField && + !isDefined(keySettings.columnWidth) || keySettings.columnWidth === '0px') { + const entityField = entityFields[dataKey.name]; + if (entityField && entityField.time) { + keySettings.columnWidth = '120px'; + } + } + + this.stylesInfo[dataKey.def] = getCellStyleInfo(keySettings); + this.contentsInfo[dataKey.def] = getCellContentInfo(keySettings, 'value, entity, ctx'); + this.contentsInfo[dataKey.def].units = dataKey.units; + this.contentsInfo[dataKey.def].decimals = dataKey.decimals; + this.columnWidth[dataKey.def] = getColumnWidth(keySettings); + this.columns.push(dataKey); + }); + this.displayedColumns.push(...this.columns.map(column => column.def)); + } + + if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) { + this.defaultSortOrder = this.utils.customTranslation(this.settings.defaultSortOrder, this.settings.defaultSortOrder); + } + + this.pageLink.sortOrder = entityDataSortOrderFromString(this.defaultSortOrder, this.columns); + let sortColumn: EntityColumn; + if (this.pageLink.sortOrder) { + sortColumn = findColumnByEntityKey(this.pageLink.sortOrder.key, this.columns); + } + this.sortOrderProperty = sortColumn ? sortColumn.def : null; + + if (this.actionCellDescriptors.length) { + this.displayedColumns.push('actions'); + } + this.entityDatasource = new EntityDatasource( + this.translate, dataKeys, this.subscription); + } + + private editColumnsToDisplay($event: Event) { + if ($event) { + $event.stopPropagation(); + } + const target = $event.target || $event.currentTarget; + const config = new OverlayConfig(); + config.backdropClass = 'cdk-overlay-transparent-backdrop'; + config.hasBackdrop = true; + const connectedPosition: ConnectedPosition = { + originX: 'end', + originY: 'bottom', + overlayX: 'end', + overlayY: 'top' + }; + config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) + .withPositions([connectedPosition]); + + const overlayRef = this.overlay.create(config); + overlayRef.backdropClick().subscribe(() => { + overlayRef.dispose(); + }); + + const columns: DisplayColumn[] = this.columns.map(column => { + return { + title: column.title, + def: column.def, + display: this.displayedColumns.indexOf(column.def) > -1 + }; + }); + + const injectionTokens = new WeakMap([ + [DISPLAY_COLUMNS_PANEL_DATA, { + columns, + columnsUpdated: (newColumns) => { + this.displayedColumns = newColumns.filter(column => column.display).map(column => column.def); + this.displayedColumns.push('actions'); + } + } as DisplayColumnsPanelData], + [OverlayRef, overlayRef] + ]); + const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens); + overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent, + this.viewContainerRef, injector)); + this.ctx.detectChanges(); + } + + private enterFilterMode() { + this.textSearchMode = true; + this.pageLink.textSearch = ''; + this.ctx.hideTitlePanel = true; + this.ctx.detectChanges(true); + setTimeout(() => { + this.searchInputField.nativeElement.focus(); + this.searchInputField.nativeElement.setSelectionRange(0, 0); + }, 10); + } + + exitFilterMode() { + this.textSearchMode = false; + this.pageLink.textSearch = null; + if (this.displayPagination) { + this.paginator.pageIndex = 0; + } + this.updateData(); + this.ctx.hideTitlePanel = false; + this.ctx.detectChanges(true); + } + + private updateData() { + if (this.displayPagination) { + this.pageLink.page = this.paginator.pageIndex; + this.pageLink.pageSize = this.paginator.pageSize; + } else { + this.pageLink.page = 0; + } + const key = findEntityKeyByColumnDef(this.sort.active, this.columns); + if (key) { + this.pageLink.sortOrder = { + key, + direction: Direction[this.sort.direction.toUpperCase()] + }; + } else { + this.pageLink.sortOrder = null; + } + const sortOrderLabel = fromEntityColumnDef(this.sort.active, this.columns); + const keyFilters: KeyFilter[] = null; // TODO: + this.entityDatasource.loadEntities(this.pageLink, sortOrderLabel, keyFilters); + this.ctx.detectChanges(); + } + + public trackByColumnDef(index, column: EntityColumn) { + return column.def; + } + + public trackByRowIndex(index: number) { + return index; + } + + public headerStyle(key: EntityColumn): any { + const columnWidth = this.columnWidth[key.def]; + return widthStyle(columnWidth); + } + + public cellStyle(entity: EntityData, key: EntityColumn): any { + let style: any = {}; + if (entity && key) { + const styleInfo = this.stylesInfo[key.def]; + const value = getEntityValue(entity, key); + if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { + try { + style = styleInfo.cellStyleFunction(value); + } catch (e) { + style = {}; + } + } else { + style = {}; + } + } + if (!style.width) { + const columnWidth = this.columnWidth[key.def]; + style = {...style, ...widthStyle(columnWidth)}; + } + return style; + } + + public cellContent(entity: EntityData, key: EntityColumn): SafeHtml { + if (entity && key) { + const contentInfo = this.contentsInfo[key.def]; + const value = getEntityValue(entity, key); + let content: string; + if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { + try { + content = contentInfo.cellContentFunction(value, entity, this.ctx); + } catch (e) { + content = '' + value; + } + } else { + content = this.defaultContent(key, contentInfo, value); + } + + if (!isDefined(content)) { + return ''; + + } else { + switch (typeof content) { + case 'string': + return this.domSanitizer.bypassSecurityTrustHtml(content); + default: + return content; + } + } + + } else { + return ''; + } + } + + private defaultContent(key: EntityColumn, contentInfo: CellContentInfo, value: any): any { + if (isDefined(value)) { + const entityField = entityFields[key.name]; + if (entityField) { + if (entityField.time) { + return this.datePipe.transform(value, 'yyyy-MM-dd HH:mm:ss'); + } + } + const decimals = (contentInfo.decimals || contentInfo.decimals === 0) ? contentInfo.decimals : this.ctx.widgetConfig.decimals; + const units = contentInfo.units || this.ctx.widgetConfig.units; + return this.ctx.utils.formatValue(value, decimals, units, true); + } else { + return ''; + } + } + + public onRowClick($event: Event, entity: EntityData, isDouble?: boolean) { + if ($event) { + $event.stopPropagation(); + } + this.entityDatasource.toggleCurrentEntity(entity); + const actionSourceId = isDouble ? 'rowDoubleClick' : 'rowClick'; + const descriptors = this.ctx.actionsApi.getActionDescriptors(actionSourceId); + if (descriptors.length) { + let entityId; + let entityName; + let entityLabel; + if (entity) { + entityId = entity.id; + entityName = entity.entityName; + entityLabel = entity.entityLabel; + } + this.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName, null, entityLabel); + } + } + + public onActionButtonClick($event: Event, entity: EntityData, actionDescriptor: WidgetActionDescriptor) { + if ($event) { + $event.stopPropagation(); + } + let entityId; + let entityName; + let entityLabel; + if (entity) { + entityId = entity.id; + entityName = entity.entityName; + entityLabel = entity.entityLabel; + } + this.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName, null, entityLabel); + } +} + +class EntityDatasource implements DataSource { + + private entitiesSubject = new BehaviorSubject([]); + private pageDataSubject = new BehaviorSubject>(emptyPageData()); + + private currentEntity: EntityData = null; + + public dataLoading = true; + + private appliedPageLink: EntityDataPageLink; + private appliedSortOrderLabel: string; + + constructor( + private translate: TranslateService, + private dataKeys: Array, + private subscription: IWidgetSubscription + ) { + } + + connect(collectionViewer: CollectionViewer): Observable> { + return this.entitiesSubject.asObservable(); + } + + disconnect(collectionViewer: CollectionViewer): void { + this.entitiesSubject.complete(); + this.pageDataSubject.complete(); + } + + loadEntities(pageLink: EntityDataPageLink, sortOrderLabel: string, keyFilters: KeyFilter[]) { + this.dataLoading = true; + // this.clear(); + this.appliedPageLink = pageLink; + this.appliedSortOrderLabel = sortOrderLabel; + this.subscription.subscribeForPaginatedData(0, pageLink, keyFilters); + } + + private clear() { + this.entitiesSubject.next([]); + this.pageDataSubject.next(emptyPageData()); + } + + dataUpdated() { + const datasourcesPageData = this.subscription.datasourcePages[0]; + const dataPageData = this.subscription.dataPages[0]; + let entities = new Array(); + datasourcesPageData.data.forEach((datasource, index) => { + entities.push(this.datasourceToEntityData(datasource, dataPageData.data[index])); + }); + if (this.appliedSortOrderLabel && this.appliedSortOrderLabel.length) { + const asc = this.appliedPageLink.sortOrder.direction === Direction.ASC; + entities = entities.sort((a, b) => sortItems(a, b, this.appliedSortOrderLabel, asc)); + } + const entitiesPageData: PageData = { + data: entities, + totalPages: datasourcesPageData.totalPages, + totalElements: datasourcesPageData.totalElements, + hasNext: datasourcesPageData.hasNext + }; + this.entitiesSubject.next(entities); + this.pageDataSubject.next(entitiesPageData); + this.dataLoading = false; + } + + private datasourceToEntityData(datasource: Datasource, data: DatasourceData[]): EntityData { + const entity: EntityData = { + id: {} as EntityId, + entityName: datasource.entityName, + entityLabel: datasource.entityLabel ? datasource.entityLabel : datasource.entityName + }; + if (datasource.entityId) { + entity.id.id = datasource.entityId; + } + if (datasource.entityType) { + entity.id.entityType = datasource.entityType; + entity.entityType = this.translate.instant(entityTypeTranslations.get(datasource.entityType).type); + } else { + entity.entityType = ''; + } + this.dataKeys.forEach((dataKey, index) => { + const keyData = data[index].data; + if (keyData && keyData.length && keyData[0].length > 1) { + if (data[index].dataKey.type !== DataKeyType.entityField || !entity.hasOwnProperty(dataKey.label)) { + entity[dataKey.label] = keyData[0][1]; + } + } else { + entity[dataKey.label] = ''; + } + }); + return entity; + } + + isEmpty(): Observable { + return this.entitiesSubject.pipe( + map((entities) => !entities.length) + ); + } + + total(): Observable { + return this.pageDataSubject.pipe( + map((pageData) => pageData.totalElements) + ); + } + + public toggleCurrentEntity(entity: EntityData): boolean { + if (this.currentEntity !== entity) { + this.currentEntity = entity; + return true; + } else { + return false; + } + } + + public isCurrentEntity(entity: EntityData): boolean { + return (this.currentEntity && entity && this.currentEntity.id && entity.id) && + (this.currentEntity.id.id === entity.id.id); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts new file mode 100644 index 0000000000..5a1c921f88 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts @@ -0,0 +1,1088 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +// tslint:disable-next-line:no-reference +/// + +import { DataKey, Datasource, DatasourceData, JsonSettingsSchema } from '@shared/models/widget.models'; +import * as moment_ from 'moment'; +import { DataKeyType } from "@shared/models/telemetry/telemetry.models"; + +export declare type ChartType = 'line' | 'pie' | 'bar' | 'state' | 'graph'; + +export declare type TbFlotSettings = TbFlotBaseSettings & TbFlotGraphSettings & TbFlotBarSettings & TbFlotPieSettings; + +export declare type TooltipValueFormatFunction = (value: any) => string; + +export declare type TbFlotTicksFormatterFunction = (t: number, a?: TbFlotPlotAxis) => string; + +export interface TbFlotSeries extends DatasourceData, JQueryPlotSeriesOptions { + dataKey: TbFlotDataKey; + xaxisIndex?: number; + yaxisIndex?: number; + yaxis?: number; +} + +export interface TbFlotDataKey extends DataKey { + settings?: TbFlotKeySettings; + tooltipValueFormatFunction?: TooltipValueFormatFunction; +} + +export interface TbFlotPlotAxis extends JQueryPlotAxis, TbFlotAxisOptions { + options: TbFlotAxisOptions; +} + +export interface TbFlotAxisOptions extends JQueryPlotAxisOptions { + tickUnits?: string; + hidden?: boolean; + keysInfo?: Array<{hidden: boolean}>; + ticksFormatterFunction?: TbFlotTicksFormatterFunction; +} + +export interface TbFlotPlotDataSeries extends JQueryPlotDataSeries { + datasource?: Datasource; + dataKey?: TbFlotDataKey; + percent?: number; +} + +export interface TbFlotPlotItem extends jquery.flot.item { + series: TbFlotPlotDataSeries; +} + +export interface TbFlotHoverInfo { + seriesHover: Array; + time?: any; +} + +export interface TbFlotSeriesHoverInfo { + hoverIndex: number; + units: string; + decimals: number; + label: string; + color: string; + index: number; + tooltipValueFormatFunction: TooltipValueFormatFunction; + value: any; + time: any; + distance: number; +} + +export interface TbFlotThresholdMarking { + lineWidth?: number; + color?: string; + [key: string]: any; +} + +export interface TbFlotThresholdKeySettings { + yaxis: number; + lineWidth: number; + color: string; +} + +export interface TbFlotGridSettings { + color: string; + backgroundColor: string; + tickColor: string; + outlineWidth: number; + verticalLines: boolean; + horizontalLines: boolean; + minBorderMargin: number; + margin: number; +} + +export interface TbFlotXAxisSettings { + showLabels: boolean; + title: string; + color: boolean; +} + +export interface TbFlotSecondXAxisSettings { + axisPosition: TbFlotXAxisPosition; + showLabels: boolean; + title: string; +} + +export interface TbFlotYAxisSettings { + min: number; + max: number; + showLabels: boolean; + title: string; + color: string; + ticksFormatter: string; + tickDecimals: number; + tickSize: number; +} + +export interface TbFlotBaseSettings { + stack: boolean; + shadowSize: number; + fontColor: string; + fontSize: number; + tooltipIndividual: boolean; + tooltipCumulative: boolean; + tooltipValueFormatter: string; + hideZeros: boolean; + grid: TbFlotGridSettings; + xaxis: TbFlotXAxisSettings; + yaxis: TbFlotYAxisSettings; +} + +export interface TbFlotComparisonSettings { + comparisonEnabled: boolean; + timeForComparison: moment_.unitOfTime.DurationConstructor; + xaxisSecond: TbFlotSecondXAxisSettings; +} + +export interface TbFlotThresholdsSettings { + thresholdsLineWidth: number; +} + +export interface TbFlotCustomLegendSettings { + customLegendEnabled: boolean; + dataKeysListForLabels: Array; +} + +export interface TbFlotLabelPatternSettings { + name: string; + type: DataKeyType; + settings?: any; +} + +export interface TbFlotGraphSettings extends TbFlotBaseSettings, TbFlotThresholdsSettings, TbFlotComparisonSettings, TbFlotCustomLegendSettings { + smoothLines: boolean; +} + +export declare type BarAlignment = 'left' | 'right' | 'center'; + +export interface TbFlotBarSettings extends TbFlotBaseSettings, TbFlotThresholdsSettings, TbFlotComparisonSettings, TbFlotCustomLegendSettings { + defaultBarWidth: number; + barAlignment: BarAlignment; +} + +export interface TbFlotPieSettings { + radius: number; + innerRadius: number; + tilt: number; + animatedPie: boolean; + stroke: { + color: string; + width: number; + }; + showLabels: boolean; + fontColor: string; + fontSize: number; +} + +export declare type TbFlotYAxisPosition = 'left' | 'right'; +export declare type TbFlotXAxisPosition = 'top' | 'bottom'; + +export declare type TbFlotThresholdValueSource = 'predefinedValue' | 'entityAttribute'; + +export interface TbFlotKeyThreshold { + thresholdValueSource: TbFlotThresholdValueSource; + thresholdEntityAlias: string; + thresholdAttribute: string; + thresholdValue: number; + lineWidth: number; + color: string; +} + +export interface TbFlotKeyComparisonSettings { + showValuesForComparison: boolean; + comparisonValuesLabel: string; + color: string; +} + +export interface TbFlotKeySettings { + excludeFromStacking: boolean; + hideDataByDefault: boolean; + disableDataHiding: boolean; + removeFromLegend: boolean; + showLines: boolean; + fillLines: boolean; + showPoints: boolean; + showPointShape: string; + pointShapeFormatter: string; + showPointsLineWidth: number; + showPointsRadius: number; + lineWidth: number; + tooltipValueFormatter: string; + showSeparateAxis: boolean; + axisMin: number; + axisMax: number; + axisTitle: string; + axisTickDecimals: number; + axisTickSize: number; + axisPosition: TbFlotYAxisPosition; + axisTicksFormatter: string; + thresholds: TbFlotKeyThreshold[]; + comparisonSettings: TbFlotKeyComparisonSettings; +} + +export function flotSettingsSchema(chartType: ChartType): JsonSettingsSchema { + + const schema: JsonSettingsSchema = { + schema: { + type: 'object', + title: 'Settings', + properties: { + } + } + }; + + const properties: any = schema.schema.properties; + properties.stack = { + title: 'Stacking', + type: 'boolean', + default: false + }; + if (chartType === 'graph') { + properties.smoothLines = { + title: 'Display smooth (curved) lines', + type: 'boolean', + default: false + }; + } + if (chartType === 'bar') { + properties.defaultBarWidth = { + title: 'Default bar width for non-aggregated data (milliseconds)', + type: 'number', + default: 600 + }; + properties.barAlignment = { + title: 'Bar alignment', + type: 'string', + default: 'left' + }; + } + if (chartType === 'graph' || chartType === 'bar') { + properties.thresholdsLineWidth = { + title: 'Default line width for all thresholds', + type: 'number' + }; + } + properties.shadowSize = { + title: 'Shadow size', + type: 'number', + default: 4 + }; + properties.fontColor = { + title: 'Font color', + type: 'string', + default: '#545454' + }; + properties.fontSize = { + title: 'Font size', + type: 'number', + default: 10 + }; + properties.tooltipIndividual = { + title: 'Hover individual points', + type: 'boolean', + default: false + }; + properties.tooltipCumulative = { + title: 'Show cumulative values in stacking mode', + type: 'boolean', + default: false + }; + properties.tooltipValueFormatter = { + title: 'Tooltip value format function, f(value)', + type: 'string', + default: '' + }; + properties.hideZeros = { + title: 'Hide zero/false values from tooltip', + type: 'boolean', + default: false + }; + + properties.grid = { + title: 'Grid settings', + type: 'object', + properties: { + color: { + title: 'Primary color', + type: 'string', + default: '#545454' + }, + backgroundColor: { + title: 'Background color', + type: 'string', + default: null + }, + tickColor: { + title: 'Ticks color', + type: 'string', + default: '#DDDDDD' + }, + outlineWidth: { + title: 'Grid outline/border width (px)', + type: 'number', + default: 1 + }, + verticalLines: { + title: 'Show vertical lines', + type: 'boolean', + default: true + }, + horizontalLines: { + title: 'Show horizontal lines', + type: 'boolean', + default: true + } + } + }; + + properties.xaxis = { + title: 'X axis settings', + type: 'object', + properties: { + showLabels: { + title: 'Show labels', + type: 'boolean', + default: true + }, + title: { + title: 'Axis title', + type: 'string', + default: null + }, + color: { + title: 'Ticks color', + type: 'string', + default: null + } + } + }; + + properties.yaxis = { + title: 'Y axis settings', + type: 'object', + properties: { + min: { + title: 'Minimum value on the scale', + type: 'number', + default: null + }, + max: { + title: 'Maximum value on the scale', + type: 'number', + default: null + }, + showLabels: { + title: 'Show labels', + type: 'boolean', + default: true + }, + title: { + title: 'Axis title', + type: 'string', + default: null + }, + color: { + title: 'Ticks color', + type: 'string', + default: null + }, + ticksFormatter: { + title: 'Ticks formatter function, f(value)', + type: 'string', + default: '' + }, + tickDecimals: { + title: 'The number of decimals to display', + type: 'number', + default: 0 + }, + tickSize: { + title: 'Step size between ticks', + type: 'number', + default: null + } + } + }; + + schema.schema.required = []; + schema.form = ['stack']; + if (chartType === 'graph') { + schema.form.push('smoothLines'); + } + if (chartType === 'bar') { + schema.form.push('defaultBarWidth'); + schema.form.push({ + key: 'barAlignment', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'left', + label: 'Left' + }, + { + value: 'right', + label: 'Right' + }, + { + value: 'center', + label: 'Center' + } + ] + }); + } + if (chartType === 'graph' || chartType === 'bar') { + schema.form.push('thresholdsLineWidth'); + } + schema.form.push('shadowSize'); + schema.form.push({ + key: 'fontColor', + type: 'color' + }); + schema.form.push('fontSize'); + schema.form.push('tooltipIndividual'); + schema.form.push('tooltipCumulative'); + schema.form.push({ + key: 'tooltipValueFormatter', + type: 'javascript' + }); + schema.form.push('hideZeros'); + schema.form.push({ + key: 'grid', + items: [ + { + key: 'grid.color', + type: 'color' + }, + { + key: 'grid.backgroundColor', + type: 'color' + }, + { + key: 'grid.tickColor', + type: 'color' + }, + 'grid.outlineWidth', + 'grid.verticalLines', + 'grid.horizontalLines' + ] + }); + schema.form.push({ + key: 'xaxis', + items: [ + 'xaxis.showLabels', + 'xaxis.title', + { + key: 'xaxis.color', + type: 'color' + } + ] + }); + schema.form.push({ + key: 'yaxis', + items: [ + 'yaxis.min', + 'yaxis.max', + 'yaxis.tickDecimals', + 'yaxis.tickSize', + 'yaxis.showLabels', + 'yaxis.title', + { + key: 'yaxis.color', + type: 'color' + }, + { + key: 'yaxis.ticksFormatter', + type: 'javascript' + } + ] + }); + if (chartType === 'graph' || chartType === 'bar') { + schema.groupInfoes = [{ + formIndex: 0, + GroupTitle: 'Common Settings' + }]; + schema.form = [schema.form]; + schema.schema.properties = {...schema.schema.properties, ...chartSettingsSchemaForComparison.schema.properties, ...chartSettingsSchemaForCustomLegend.schema.properties}; + schema.schema.required = schema.schema.required.concat(chartSettingsSchemaForComparison.schema.required, chartSettingsSchemaForCustomLegend.schema.required); + schema.form.push(chartSettingsSchemaForComparison.form, chartSettingsSchemaForCustomLegend.form); + schema.groupInfoes.push({ + formIndex: schema.groupInfoes.length, + GroupTitle:'Comparison Settings' + }); + schema.groupInfoes.push({ + formIndex: schema.groupInfoes.length, + GroupTitle:'Custom Legend Settings' + }); + } + return schema; +} + +const chartSettingsSchemaForComparison: JsonSettingsSchema = { + schema: { + title: 'Comparison Settings', + type: 'object', + properties: { + comparisonEnabled: { + title: 'Enable comparison', + type: 'boolean', + default: false + }, + timeForComparison: { + title: 'Time to show historical data', + type: 'string', + default: 'months' + }, + xaxisSecond: { + title: 'Second X axis', + type: 'object', + properties: { + axisPosition: { + title: 'Axis position', + type: 'string', + default: 'top' + }, + showLabels: { + title: 'Show labels', + type: 'boolean', + default: true + }, + title: { + title: 'Axis title', + type: 'string', + default: null + } + } + } + }, + required: [] + }, + form: [ + 'comparisonEnabled', + { + key: 'timeForComparison', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'days', + label: 'Day ago' + }, + { + value: 'weeks', + label: 'Week ago' + }, + { + value: 'months', + label: 'Month ago (default)' + }, + { + value: 'years', + label: 'Year ago' + } + ] + }, + { + key: 'xaxisSecond', + items: [ + { + key: 'xaxisSecond.axisPosition', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'top', + label: 'Top (default)' + }, + { + value: 'bottom', + label: 'Bottom' + } + ] + }, + 'xaxisSecond.showLabels', + 'xaxisSecond.title', + ] + } + ] +}; + +const chartSettingsSchemaForCustomLegend: JsonSettingsSchema = { + schema: { + title: 'Custom Legend Settings', + type: 'object', + properties: { + customLegendEnabled: { + title: 'Enable custom legend (this will allow you to use attribute/timeseries values in key labels)', + type: 'boolean', + default: false + }, + dataKeysListForLabels: { + title: 'Datakeys list to use in labels', + type: 'array', + items: { + type: 'object', + properties: { + name: { + title: 'Key name', + type: 'string' + }, + type: { + title: 'Key type', + type: 'string', + default: 'attribute' + } + }, + required: [ + 'name' + ] + } + } + }, + required: [] + }, + form: [ + 'customLegendEnabled', + { + key: 'dataKeysListForLabels', + condition: 'model.customLegendEnabled === true', + items: [ + { + key: 'dataKeysListForLabels[].type', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'attribute', + label: 'Attribute' + }, + { + value: 'timeseries', + label: 'Timeseries' + } + ] + }, + 'dataKeysListForLabels[].name' + ] + } + ] +}; + +export const flotPieSettingsSchema: JsonSettingsSchema = { + schema: { + type: 'object', + title: 'Settings', + properties: { + radius: { + title: 'Radius', + type: 'number', + default: 1 + }, + innerRadius: { + title: 'Inner radius', + type: 'number', + default: 0 + }, + tilt: { + title: 'Tilt', + type: 'number', + default: 1 + }, + animatedPie: { + title: 'Enable pie animation (experimental)', + type: 'boolean', + default: false + }, + stroke: { + title: 'Stroke', + type: 'object', + properties: { + color: { + title: 'Color', + type: 'string', + default: '' + }, + width: { + title: 'Width (pixels)', + type: 'number', + default: 0 + } + } + }, + showLabels: { + title: 'Show labels', + type: 'boolean', + default: false + }, + fontColor: { + title: 'Font color', + type: 'string', + default: '#545454' + }, + fontSize: { + title: 'Font size', + type: 'number', + default: 10 + } + }, + required: [] + }, + form: [ + 'radius', + 'innerRadius', + 'animatedPie', + 'tilt', + { + key: 'stroke', + items: [ + { + key: 'stroke.color', + type: 'color' + }, + 'stroke.width' + ] + }, + 'showLabels', + { + key: 'fontColor', + type: 'color' + }, + 'fontSize' + ] +}; + +export const flotPieDatakeySettingsSchema: JsonSettingsSchema = { + schema: { + type: 'object', + title: 'DataKeySettings', + properties: { + hideDataByDefault: { + title: 'Data is hidden by default', + type: 'boolean', + default: false + }, + disableDataHiding: { + title: 'Disable data hiding', + type: 'boolean', + default: false + }, + removeFromLegend: { + title: 'Remove datakey from legend', + type: 'boolean', + default: false + } + }, + required: [] + }, + form: [ + 'hideDataByDefault', + 'disableDataHiding', + 'removeFromLegend' + ] +}; + +export function flotDatakeySettingsSchema(defaultShowLines: boolean, chartType: ChartType): JsonSettingsSchema { + const schema: JsonSettingsSchema = { + schema: { + type: 'object', + title: 'DataKeySettings', + properties: { + excludeFromStacking: { + title: 'Exclude from stacking(available in "Stacking" mode)', + type: 'boolean', + default: false + }, + hideDataByDefault: { + title: 'Data is hidden by default', + type: 'boolean', + default: false + }, + disableDataHiding: { + title: 'Disable data hiding', + type: 'boolean', + default: false + }, + removeFromLegend: { + title: 'Remove datakey from legend', + type: 'boolean', + default: false + }, + showLines: { + title: 'Show lines', + type: 'boolean', + default: defaultShowLines + }, + fillLines: { + title: 'Fill lines', + type: 'boolean', + default: false + }, + showPoints: { + title: 'Show points', + type: 'boolean', + default: false + }, + showPointShape: { + title: 'Select point shape:', + type: 'string', + default: 'circle' + }, + pointShapeFormatter: { + title: 'Point shape format function, f(ctx, x, y, radius, shadow)', + type: 'string', + default: 'var size = radius * Math.sqrt(Math.PI) / 2;\n' + + 'ctx.moveTo(x - size, y - size);\n' + + 'ctx.lineTo(x + size, y + size);\n' + + 'ctx.moveTo(x - size, y + size);\n' + + 'ctx.lineTo(x + size, y - size);' + }, + showPointsLineWidth: { + title: 'Line width of points', + type: 'number', + default: 5 + }, + showPointsRadius: { + title: 'Radius of points', + type: 'number', + default: 3 + }, + tooltipValueFormatter: { + title: 'Tooltip value format function, f(value)', + type: 'string', + default: '' + }, + showSeparateAxis: { + title: 'Show separate axis', + type: 'boolean', + default: false + }, + axisMin: { + title: 'Minimum value on the axis scale', + type: 'number', + default: null + }, + axisMax: { + title: 'Maximum value on the axis scale', + type: 'number', + default: null + }, + axisTitle: { + title: 'Axis title', + type: 'string', + default: '' + }, + axisTickDecimals: { + title: 'Axis tick number of digits after floating point', + type: 'number', + default: null + }, + axisTickSize: { + title: 'Axis step size between ticks', + type: 'number', + default: null + }, + axisPosition: { + title: 'Axis position', + type: 'string', + default: 'left' + }, + axisTicksFormatter: { + title: 'Ticks formatter function, f(value)', + type: 'string', + default: '' + } + }, + required: ['showLines', 'fillLines', 'showPoints'] + }, + form: [ + 'hideDataByDefault', + 'disableDataHiding', + 'removeFromLegend', + 'excludeFromStacking', + 'showLines', + 'fillLines', + 'showPoints', + { + key: 'showPointShape', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'circle', + label: 'Circle' + }, + { + value: 'cross', + label: 'Cross' + }, + { + value: 'diamond', + label: 'Diamond' + }, + { + value: 'square', + label: 'Square' + }, + { + value: 'triangle', + label: 'Triangle' + }, + { + value: 'custom', + label: 'Custom function' + } + ] + }, + { + key: 'pointShapeFormatter', + type: 'javascript' + }, + 'showPointsLineWidth', + 'showPointsRadius', + { + key: 'tooltipValueFormatter', + type: 'javascript' + }, + 'showSeparateAxis', + 'axisMin', + 'axisMax', + 'axisTitle', + 'axisTickDecimals', + 'axisTickSize', + { + key: 'axisPosition', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'left', + label: 'Left' + }, + { + value: 'right', + label: 'Right' + } + ] + }, + { + key: 'axisTicksFormatter', + type: 'javascript' + } + ] + }; + + const properties = schema.schema.properties; + if (chartType === 'graph' || chartType === 'bar') { + properties.thresholds = { + title: 'Thresholds', + type: 'array', + items: { + title: 'Threshold', + type: 'object', + properties: { + thresholdValueSource: { + title: 'Threshold value source', + type: 'string', + default: 'predefinedValue' + }, + thresholdEntityAlias: { + title: 'Thresholds source entity alias', + type: 'string' + }, + thresholdAttribute: { + title: 'Threshold source entity attribute', + type: 'string' + }, + thresholdValue: { + title: 'Threshold value (if predefined value is selected)', + type: 'number' + }, + lineWidth: { + title: 'Line width', + type: 'number' + }, + color: { + title: 'Color', + type: 'string' + } + } + }, + required: [] + }; + schema.form.push({ + key: 'thresholds', + items: [ + { + key: 'thresholds[].thresholdValueSource', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'predefinedValue', + label: 'Predefined value (Default)' + }, + { + value: 'entityAttribute', + label: 'Value taken from entity attribute' + } + ] + }, + 'thresholds[].thresholdValue', + 'thresholds[].thresholdEntityAlias', + 'thresholds[].thresholdAttribute', + { + key: 'thresholds[].color', + type: 'color' + }, + 'thresholds[].lineWidth' + ] + }); + properties.comparisonSettings = { + title: 'Comparison Settings', + type: 'object', + properties: { + showValuesForComparison: { + title: 'Show historical values for comparison', + type: 'boolean', + default: true + }, + comparisonValuesLabel: { + title: 'Historical values label', + type: 'string', + default: '' + }, + color: { + title: 'Color', + type: 'string', + default: '' + } + }, + required: ['showValuesForComparison'] + }; + schema.form.push({ + key: 'comparisonSettings', + items: [ + 'comparisonSettings.showValuesForComparison', + 'comparisonSettings.comparisonValuesLabel', + { + key: 'comparisonSettings.color', + type: 'color' + } + ] + }); + } + + return schema; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts new file mode 100644 index 0000000000..b7962a098d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts @@ -0,0 +1,1467 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + + +import { WidgetContext } from '@home/models/widget-component.models'; +import { + createLabelFromDatasource, + deepClone, + insertVariable, + isDefined, + isDefinedAndNotNull, + isEqual, + isNumber, + isUndefined +} from '@app/core/utils'; +import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; +import { + DataKey, + Datasource, + DatasourceData, + DatasourceType, + JsonSettingsSchema, + widgetType +} from '@app/shared/models/widget.models'; +import { + ChartType, + flotDatakeySettingsSchema, + flotPieDatakeySettingsSchema, + flotPieSettingsSchema, + flotSettingsSchema, + TbFlotAxisOptions, + TbFlotHoverInfo, + TbFlotKeySettings, + TbFlotPlotAxis, + TbFlotPlotDataSeries, + TbFlotPlotItem, + TbFlotSeries, + TbFlotSeriesHoverInfo, + TbFlotSettings, + TbFlotThresholdKeySettings, + TbFlotThresholdMarking, + TbFlotTicksFormatterFunction, + TooltipValueFormatFunction +} from './flot-widget.models'; +import * as moment_ from 'moment'; +import * as tinycolor_ from 'tinycolor2'; +import { AggregationType } from '@shared/models/time/time.models'; +import { CancelAnimationFrame } from '@core/services/raf.service'; +import { UtilsService } from '@core/services/utils.service'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import Timeout = NodeJS.Timeout; + +const tinycolor = tinycolor_; +const moment = moment_; + +const flotPieSettingsSchemaValue = flotPieSettingsSchema; +const flotPieDatakeySettingsSchemaValue = flotPieDatakeySettingsSchema; + +export class TbFlot { + + private readonly utils: UtilsService; + + private settings: TbFlotSettings; + + private readonly tooltip: JQuery; + + private readonly yAxisTickFormatter: TbFlotTicksFormatterFunction; + private readonly yaxis: TbFlotAxisOptions; + private readonly xaxis: TbFlotAxisOptions; + private yaxes: Array; + + private readonly options: JQueryPlotOptions; + private subscription: IWidgetSubscription; + private $element: JQuery; + + private readonly trackUnits: string; + private readonly trackDecimals: number; + private readonly tooltipIndividual: boolean; + private readonly tooltipCumulative: boolean; + private readonly hideZeros: boolean; + + private readonly defaultBarWidth: number; + + private thresholdsSourcesSubscription: IWidgetSubscription; + private predefinedThresholds: TbFlotThresholdMarking[]; + + private labelPatternsSourcesSubscription: IWidgetSubscription; + private labelPatternsSourcesData: DatasourceData[]; + + private plotInited = false; + private plot: JQueryPlot; + + private createPlotTimeoutHandle: Timeout; + private updateTimeoutHandle: Timeout; + private resizeTimeoutHandle: Timeout; + + private mouseEventsEnabled: boolean; + private isMouseInteraction = false; + private flotHoverHandler = this.onFlotHover.bind(this); + private flotSelectHandler = this.onFlotSelect.bind(this); + private dblclickHandler = this.onFlotDblClick.bind(this); + private mousedownHandler = this.onFlotMouseDown.bind(this); + private mouseupHandler = this.onFlotMouseUp.bind(this); + private mouseleaveHandler = this.onFlotMouseLeave.bind(this); + private flotClickHandler = this.onFlotClick.bind(this); + + private readonly animatedPie: boolean; + private pieDataAnimationDuration: number; + private pieData: DatasourceData[]; + private pieRenderedData: any[]; + private pieTargetData: any[]; + private pieAnimationStartTime: number; + private pieAnimationLastTime: number; + private pieAnimationCaf: CancelAnimationFrame; + + static pieSettingsSchema(): JsonSettingsSchema { + return flotPieSettingsSchemaValue; + } + + static pieDatakeySettingsSchema(): JsonSettingsSchema { + return flotPieDatakeySettingsSchemaValue; + } + + static settingsSchema(chartType: ChartType): JsonSettingsSchema { + return flotSettingsSchema(chartType); + } + + static datakeySettingsSchema(defaultShowLines: boolean, chartType: ChartType): JsonSettingsSchema { + return flotDatakeySettingsSchema(defaultShowLines, chartType); + } + + constructor(private ctx: WidgetContext, private readonly chartType: ChartType) { + this.chartType = this.chartType || 'line'; + this.settings = ctx.settings as TbFlotSettings; + this.utils = this.ctx.$injector.get(UtilsService); + this.tooltip = $('#flot-series-tooltip'); + if (this.tooltip.length === 0) { + this.tooltip = this.createTooltipElement(); + } + + this.trackDecimals = ctx.decimals; + this.trackUnits = ctx.units; + this.tooltipIndividual = this.chartType === 'pie' || (isDefined(this.settings.tooltipIndividual) + ? this.settings.tooltipIndividual : false); + this.tooltipCumulative = isDefined(this.settings.tooltipCumulative) ? this.settings.tooltipCumulative : false; + this.hideZeros = isDefined(this.settings.hideZeros) ? this.settings.hideZeros : false; + + const font = { + color: this.settings.fontColor || '#545454', + size: this.settings.fontSize || 10, + family: 'Roboto' + }; + + this.options = { + title: null, + subtitile: null, + shadowSize: isDefined(this.settings.shadowSize) ? this.settings.shadowSize : 4, + HtmlText: false, + grid: { + hoverable: true, + mouseActiveRadius: 10, + autoHighlight: this.tooltipIndividual === true, + markings: [] + }, + selection : { mode : ctx.isMobile ? null : 'x' }, + legend : { + show: false + } + }; + + if (this.chartType === 'line' || this.chartType === 'bar' || this.chartType === 'state') { + this.options.xaxes = []; + this.xaxis = { + mode: 'time', + timezone: 'browser', + font: deepClone(font), + labelFont: deepClone(font) + }; + this.yaxis = { + font: deepClone(font), + labelFont: deepClone(font) + }; + if (this.settings.xaxis) { + this.xaxis.font.color = this.settings.xaxis.color || this.xaxis.font.color; + this.xaxis.label = this.utils.customTranslation(this.settings.xaxis.title, this.settings.xaxis.title) || null; + this.xaxis.labelFont.color = this.xaxis.font.color; + this.xaxis.labelFont.size = this.xaxis.font.size + 2; + this.xaxis.labelFont.weight = 'bold'; + } + + this.yAxisTickFormatter = this.formatYAxisTicks.bind(this); + + this.yaxis.tickFormatter = this.yAxisTickFormatter; + + if (this.settings.yaxis) { + this.yaxis.font.color = this.settings.yaxis.color || this.yaxis.font.color; + this.yaxis.min = isDefined(this.settings.yaxis.min) ? this.settings.yaxis.min : null; + this.yaxis.max = isDefined(this.settings.yaxis.max) ? this.settings.yaxis.max : null; + this.yaxis.label = this.utils.customTranslation(this.settings.yaxis.title, this.settings.yaxis.title) || null; + this.yaxis.labelFont.color = this.yaxis.font.color; + this.yaxis.labelFont.size = this.yaxis.font.size + 2; + this.yaxis.labelFont.weight = 'bold'; + if (isNumber(this.settings.yaxis.tickSize)) { + this.yaxis.tickSize = this.settings.yaxis.tickSize; + } else { + this.yaxis.tickSize = null; + } + if (isNumber(this.settings.yaxis.tickDecimals)) { + this.yaxis.tickDecimals = this.settings.yaxis.tickDecimals; + } else { + this.yaxis.tickDecimals = null; + } + if (this.settings.yaxis.ticksFormatter && this.settings.yaxis.ticksFormatter.length) { + try { + this.yaxis.ticksFormatterFunction = new Function('value', + this.settings.yaxis.ticksFormatter) as TbFlotTicksFormatterFunction; + } catch (e) { + this.yaxis.ticksFormatterFunction = null; + } + } + } + + this.options.grid.borderWidth = 1; + this.options.grid.color = this.settings.fontColor || '#545454'; + + if (this.settings.grid) { + this.options.grid.color = this.settings.grid.color || '#545454'; + this.options.grid.backgroundColor = this.settings.grid.backgroundColor || null; + this.options.grid.tickColor = this.settings.grid.tickColor || '#DDDDDD'; + this. options.grid.borderWidth = isDefined(this.settings.grid.outlineWidth) ? + this.settings.grid.outlineWidth : 1; + if (this.settings.grid.verticalLines === false) { + this.xaxis.tickLength = 0; + } + if (this.settings.grid.horizontalLines === false) { + this.yaxis.tickLength = 0; + } + if (isDefined(this.settings.grid.margin)) { + this.options.grid.margin = this.settings.grid.margin; + } + if (isDefined(this.settings.grid.minBorderMargin)) { + this.options.grid.minBorderMargin = this.settings.grid.minBorderMargin; + } + } + + this.options.xaxes[0] = deepClone(this.xaxis); + if (this.settings.xaxis && this.settings.xaxis.showLabels === false) { + this.options.xaxes[0].tickFormatter = () => { + return ''; + }; + } + + if (this.settings.comparisonEnabled) { + const xaxis = deepClone(this.xaxis); + xaxis.position = 'top'; + if (this.settings.xaxisSecond) { + if (this.settings.xaxisSecond.showLabels === false) { + xaxis.tickFormatter = () => { + return ''; + }; + } + xaxis.label = this.utils.customTranslation(this.settings.xaxisSecond.title, this.settings.xaxisSecond.title) || null; + xaxis.position = this.settings.xaxisSecond.axisPosition; + } + xaxis.tickLength = 0; + this.options.xaxes.push(xaxis); + + this.options.series = { + stack: false + }; + } else { + this.options.series = { + stack: this.settings.stack === true + }; + } + + this.options.crosshair = { + mode: 'x' + }; + + if (this.chartType === 'line' && this.settings.smoothLines) { + this.options.series.curvedLines = { + active: true, + monotonicFit: true + }; + } + + if (this.chartType === 'line' && isFinite(this.settings.thresholdsLineWidth)) { + this.options.grid.markingsLineWidth = this.settings.thresholdsLineWidth; + } + + if (this.chartType === 'bar') { + this.options.series.lines = { + show: false, + fill: false, + steps: false + }; + this.options.series.bars = { + show: true, + lineWidth: 0, + fill: 0.9, + align: this.settings.barAlignment || 'left' + }; + this.defaultBarWidth = this.settings.defaultBarWidth || 600; + } + + if (this.chartType === 'state') { + this.options.series.lines = { + steps: true, + show: true + }; + } + } else if (this.chartType === 'pie') { + this.options.series = { + pie: { + show: true, + label: { + show: this.settings.showLabels === true + }, + radius: this.settings.radius || 1, + innerRadius: this.settings.innerRadius || 0, + stroke: { + color: '#fff', + width: 0 + }, + tilt: this.settings.tilt || 1, + shadow: { + left: 5, + top: 15, + alpha: 0.02 + } + } + }; + + this.options.grid.clickable = true; + + if (this.settings.stroke) { + this.options.series.pie.stroke.color = this.settings.stroke.color || '#fff'; + this.options.series.pie.stroke.width = this.settings.stroke.width || 0; + } + + if (this.options.series.pie.label.show) { + this.options.series.pie.label.formatter = (label, series) => { + return `
${series.dataKey.label}
${Math.round(series.percent)}%
`; + }; + this.options.series.pie.label.radius = 3 / 4; + this.options.series.pie.label.background = { + opacity: 0.8 + }; + } + + // Experimental + this.animatedPie = this.settings.animatedPie === true; + + } + + if (this.ctx.defaultSubscription) { + this.init(this.ctx.$container, this.ctx.defaultSubscription); + } + } + + + private init($element: JQuery, subscription: IWidgetSubscription) { + this.subscription = subscription; + this.$element = $element; + const colors: string[] = []; + this.yaxes = []; + const yaxesMap: {[units: string]: TbFlotAxisOptions} = {}; + const predefinedThresholds: TbFlotThresholdMarking[] = []; + const thresholdsDatasources: Datasource[] = []; + if (this.settings.customLegendEnabled && this.settings.dataKeysListForLabels?.length) { + this.labelPatternsSourcesData = []; + const labelPatternsDatasources: Datasource[] = []; + this.settings.dataKeysListForLabels.forEach((item) => { + item.settings = {}; + }); + subscription.datasources.forEach((item) => { + let datasource: Datasource = { + type: item.type, + entityType: item.entityType, + entityId: item.entityId, + dataKeys: this.settings.dataKeysListForLabels + }; + labelPatternsDatasources.push(datasource); + }); + this.subscribeForLabelPatternsSources(labelPatternsDatasources); + } + + let tooltipValueFormatFunction: TooltipValueFormatFunction = null; + if (this.settings.tooltipValueFormatter && this.settings.tooltipValueFormatter.length) { + try { + tooltipValueFormatFunction = new Function('value', this.settings.tooltipValueFormatter) as TooltipValueFormatFunction; + } catch (e) { + tooltipValueFormatFunction = null; + } + } + + for (let i = 0; i < this.subscription.data.length; i++) { + const series = this.subscription.data[i] as TbFlotSeries; + colors.push(series.dataKey.color); + const keySettings = series.dataKey.settings; + series.dataKey.tooltipValueFormatFunction = tooltipValueFormatFunction; + if (keySettings.tooltipValueFormatter && keySettings.tooltipValueFormatter.length) { + try { + series.dataKey.tooltipValueFormatFunction = new Function('value', + keySettings.tooltipValueFormatter) as TooltipValueFormatFunction; + } catch (e) { + series.dataKey.tooltipValueFormatFunction = tooltipValueFormatFunction; + } + } + series.lines = { + fill: keySettings.fillLines === true + }; + + if (this.settings.stack && !this.settings.comparisonEnabled) { + series.stack = !keySettings.excludeFromStacking; + } else { + series.stack = false; + } + + if (this.chartType === 'line' || this.chartType === 'state') { + series.lines.show = keySettings.showLines !== false; + } else { + series.lines.show = keySettings.showLines === true; + } + if (isDefined(keySettings.lineWidth) && keySettings.lineWidth !== null) { + series.lines.lineWidth = keySettings.lineWidth; + } + series.points = { + show: false, + radius: 8 + }; + if (keySettings.showPoints === true) { + series.points.show = true; + series.points.lineWidth = isDefined(keySettings.showPointsLineWidth) ? keySettings.showPointsLineWidth : 5; + series.points.radius = isDefined(keySettings.showPointsRadius) ? keySettings.showPointsRadius : 3; + series.points.symbol = isDefined(keySettings.showPointShape) ? keySettings.showPointShape : 'circle'; + if (series.points.symbol === 'custom' && keySettings.pointShapeFormatter) { + try { + series.points.symbol = new Function('ctx, x, y, radius, shadow', keySettings.pointShapeFormatter); + } catch (e) { + series.points.symbol = 'circle'; + } + } + } + if (this.chartType === 'line' && this.settings.smoothLines && !series.points.show) { + series.curvedLines = { + apply: true + }; + } + + const lineColor = tinycolor(series.dataKey.color); + lineColor.setAlpha(.75); + + series.highlightColor = lineColor.toRgbString(); + + if (series.datasource.isAdditional) { + series.xaxisIndex = 1; + series.xaxis = 2; + } else { + series.xaxisIndex = 0; + series.xaxis = 1; + } + + if (this.yaxis) { + const units = series.dataKey.units && series.dataKey.units.length ? series.dataKey.units : this.trackUnits; + let yaxis: TbFlotAxisOptions; + if (keySettings.showSeparateAxis) { + yaxis = this.createYAxis(keySettings, units); + this.yaxes.push(yaxis); + } else { + yaxis = yaxesMap[units]; + if (!yaxis) { + yaxis = this.createYAxis(keySettings, units); + yaxesMap[units] = yaxis; + this.yaxes.push(yaxis); + } + } + series.yaxisIndex = this.yaxes.indexOf(yaxis); + series.yaxis = series.yaxisIndex + 1; + yaxis.keysInfo[i] = {hidden: false}; + yaxis.show = true; + + if (keySettings.thresholds && keySettings.thresholds.length) { + for (const threshold of keySettings.thresholds) { + if (threshold.thresholdValueSource === 'predefinedValue' && isFinite(threshold.thresholdValue)) { + const colorIndex = this.subscription.data.length + predefinedThresholds.length; + this.generateThreshold(predefinedThresholds, series.yaxis, threshold.lineWidth, + threshold.color, colorIndex, threshold.thresholdValue); + } else if (threshold.thresholdEntityAlias && threshold.thresholdAttribute) { + const entityAliasId = this.ctx.aliasController.getEntityAliasId(threshold.thresholdEntityAlias); + if (!entityAliasId) { + continue; + } + let datasource = thresholdsDatasources.filter((thresholdDatasource) => { + return thresholdDatasource.entityAliasId === entityAliasId; + })[0]; + const dataKey: DataKey = { + type: DataKeyType.attribute, + name: threshold.thresholdAttribute, + label: threshold.thresholdAttribute, + settings: { + yaxis: series.yaxis, + lineWidth: threshold.lineWidth, + color: threshold.color + } as TbFlotThresholdKeySettings, + _hash: Math.random() + }; + if (datasource) { + datasource.dataKeys.push(dataKey); + } else { + datasource = { + type: DatasourceType.entity, + name: threshold.thresholdEntityAlias, + aliasName: threshold.thresholdEntityAlias, + entityAliasId, + dataKeys: [ dataKey ] + }; + thresholdsDatasources.push(datasource); + } + } + } + } + } + if (this.labelPatternsSourcesData?.length) { + this.substituteLabelPatterns(series, i); + } + } + + this.subscribeForThresholdsAttributes(thresholdsDatasources); + this.options.grid.markings = predefinedThresholds; + this.predefinedThresholds = predefinedThresholds; + + this.options.colors = colors; + this.options.yaxes = deepClone(this.yaxes); + if (this.chartType === 'line' || this.chartType === 'bar' || this.chartType === 'state') { + if (this.chartType === 'bar') { + if (this.subscription.timeWindowConfig.aggregation && + this.subscription.timeWindowConfig.aggregation.type === AggregationType.NONE) { + this.options.series.bars.barWidth = this.defaultBarWidth; + } else { + this.options.series.bars.barWidth = this.subscription.timeWindow.interval * 0.6; + } + } + this.options.xaxes[0].min = this.subscription.timeWindow.minTime; + this.options.xaxes[0].max = this.subscription.timeWindow.maxTime; + if (this.settings.comparisonEnabled) { + this.options.xaxes[1].min = this.subscription.comparisonTimeWindow.minTime; + this.options.xaxes[1].max = this.subscription.comparisonTimeWindow.maxTime; + } + } + + this.checkMouseEvents(); + + if (this.plot) { + this.plot.destroy(); + } + if (this.chartType === 'pie' && this.animatedPie) { + this.pieDataAnimationDuration = 250; + this.pieData = deepClone(this.subscription.data); + this.pieRenderedData = []; + this.pieTargetData = []; + for (let i = 0; i < this.subscription.data.length; i++) { + this.pieTargetData[i] = (this.subscription.data[i].data && this.subscription.data[i].data[0]) + ? this.subscription.data[i].data[0][1] : 0; + } + this.pieDataRendered(); + } + this.plotInited = true; + this.createPlot(); + } + + public update() { + if (this.updateTimeoutHandle) { + clearTimeout(this.updateTimeoutHandle); + this.updateTimeoutHandle = null; + } + if (this.subscription) { + if (!this.isMouseInteraction && this.plot) { + if (this.chartType === 'line' || this.chartType === 'bar' || this.chartType === 'state') { + + let axisVisibilityChanged = false; + if (this.yaxis) { + for (let i = 0; i < this.subscription.data.length; i++) { + const series = this.subscription.data[i] as TbFlotSeries; + const yaxisIndex = series.yaxisIndex; + if (this.yaxes[yaxisIndex].keysInfo[i].hidden !== series.dataKey.hidden) { + this.yaxes[yaxisIndex].keysInfo[i].hidden = series.dataKey.hidden; + axisVisibilityChanged = true; + } + + if (this.labelPatternsSourcesData?.length) { + this.substituteLabelPatterns(series, i); + } + } + if (axisVisibilityChanged) { + this.options.yaxes.length = 0; + this.yaxes.forEach((yaxis) => { + let hidden = true; + yaxis.keysInfo.forEach((info) => { + if (info) { + hidden = hidden && info.hidden; + } + }); + yaxis.hidden = hidden; + let newIndex = 1; + if (!yaxis.hidden) { + this.options.yaxes.push(yaxis); + newIndex = this.options.yaxes.length; + } + for (let k = 0; k < yaxis.keysInfo.length; k++) { + if (yaxis.keysInfo[k]) { + (this.subscription.data[k] as TbFlotSeries).yaxis = newIndex; + } + } + + }); + this.options.yaxis = { + show: this.options.yaxes.length ? true : false + }; + } + } + + this.options.xaxes[0].min = this.subscription.timeWindow.minTime; + this.options.xaxes[0].max = this.subscription.timeWindow.maxTime; + if (this.settings.comparisonEnabled) { + this.options.xaxes[1].min = this.subscription.comparisonTimeWindow.minTime; + this.options.xaxes[1].max = this.subscription.comparisonTimeWindow.maxTime; + } + if (this.chartType === 'bar') { + if (this.subscription.timeWindowConfig.aggregation && + this.subscription.timeWindowConfig.aggregation.type === AggregationType.NONE) { + this.options.series.bars.barWidth = this.defaultBarWidth; + } else { + this.options.series.bars.barWidth = this.subscription.timeWindow.interval * 0.6; + } + } + + if (axisVisibilityChanged) { + this.redrawPlot(); + } else { + this.plot.getOptions().xaxes[0].min = this.subscription.timeWindow.minTime; + this.plot.getOptions().xaxes[0].max = this.subscription.timeWindow.maxTime; + if (this.settings.comparisonEnabled) { + this.plot.getOptions().xaxes[1].min = this.subscription.comparisonTimeWindow.minTime; + this.plot.getOptions().xaxes[1].max = this.subscription.comparisonTimeWindow.maxTime; + } + if (this.chartType === 'bar') { + if (this.subscription.timeWindowConfig.aggregation && + this.subscription.timeWindowConfig.aggregation.type === AggregationType.NONE) { + this.plot.getOptions().series.bars.barWidth = this.defaultBarWidth; + } else { + this.plot.getOptions().series.bars.barWidth = this.subscription.timeWindow.interval * 0.6; + } + } + this.updateData(); + } + } else if (this.chartType === 'pie') { + if (this.animatedPie) { + this.nextPieDataAnimation(true); + } else { + this.updateData(); + } + } + } else if (this.isMouseInteraction && this.plot) { + this.updateTimeoutHandle = setTimeout(this.update.bind(this), 30); + } + } + } + + public resize() { + if (this.resizeTimeoutHandle) { + clearTimeout(this.resizeTimeoutHandle); + this.resizeTimeoutHandle = null; + } + if (this.plot && this.plotInited) { + const width = this.$element.width(); + const height = this.$element.height(); + if (width && height) { + this.plot.resize(); + if (this.chartType !== 'pie') { + this.plot.setupGrid(); + } + this.plot.draw(); + } else { + this.resizeTimeoutHandle = setTimeout(this.resize.bind(this), 30); + } + } + } + + public checkMouseEvents() { + const enabled = !this.ctx.isMobile && !this.ctx.isEdit; + if (isUndefined(this.mouseEventsEnabled) || this.mouseEventsEnabled !== enabled) { + this.mouseEventsEnabled = enabled; + if (this.$element) { + if (enabled) { + this.enableMouseEvents(); + } else { + this.disableMouseEvents(); + } + this.redrawPlot(); + } + } + } + + public destroy() { + this.cleanup(); + if (this.plot) { + this.plot.destroy(); + this.plot = null; + this.plotInited = false; + } + } + + private cleanup() { + if (this.updateTimeoutHandle) { + clearTimeout(this.updateTimeoutHandle); + this.updateTimeoutHandle = null; + } + if (this.createPlotTimeoutHandle) { + clearTimeout(this.createPlotTimeoutHandle); + this.createPlotTimeoutHandle = null; + } + if (this.resizeTimeoutHandle) { + clearTimeout(this.resizeTimeoutHandle); + this.resizeTimeoutHandle = null; + } + } + + private createPlot() { + if (this.createPlotTimeoutHandle) { + clearTimeout(this.createPlotTimeoutHandle); + this.createPlotTimeoutHandle = null; + } + if (this.plotInited && !this.plot) { + const width = this.$element.width(); + const height = this.$element.height(); + if (width && height) { + if (this.chartType === 'pie' && this.animatedPie) { + this.plot = $.plot(this.$element, this.pieData, this.options) as JQueryPlot; + } else { + this.plot = $.plot(this.$element, this.subscription.data, this.options) as JQueryPlot; + } + } else { + this.createPlotTimeoutHandle = setTimeout(this.createPlot.bind(this), 30); + } + } + } + + private updateData() { + this.plot.setData(this.subscription.data); + if (this.chartType !== 'pie') { + this.plot.setupGrid(); + } + this.plot.draw(); + } + + private redrawPlot() { + if (this.plot && this.plotInited) { + this.plot.destroy(); + this.plot = null; + this.createPlot(); + } + } + + private createYAxis(keySettings: TbFlotKeySettings, units: string): TbFlotAxisOptions { + const yaxis = deepClone(this.yaxis); + let tickDecimals: number; + let tickSize: number; + const label = keySettings.axisTitle && keySettings.axisTitle.length ? keySettings.axisTitle : yaxis.label; + if (isNumber(keySettings.axisTickDecimals)) { + tickDecimals = keySettings.axisTickDecimals; + } else { + tickDecimals = yaxis.tickDecimals; + } + if (isNumber(keySettings.axisTickSize)) { + tickSize = keySettings.axisTickSize; + } else { + tickSize = yaxis.tickSize; + } + const position = keySettings.axisPosition && keySettings.axisPosition.length ? keySettings.axisPosition : 'left'; + const min = isDefined(keySettings.axisMin) ? keySettings.axisMin : yaxis.min; + const max = isDefined(keySettings.axisMax) ? keySettings.axisMax : yaxis.max; + yaxis.label = label; + yaxis.min = min; + yaxis.max = max; + yaxis.tickUnits = units; + yaxis.tickDecimals = tickDecimals; + yaxis.tickSize = tickSize; + if (position === 'right' && tickSize === null) { + yaxis.alignTicksWithAxis = 1; + } else { + yaxis.alignTicksWithAxis = null; + } + yaxis.position = position; + + yaxis.keysInfo = []; + + if (keySettings.axisTicksFormatter && keySettings.axisTicksFormatter.length) { + try { + yaxis.ticksFormatterFunction = new Function('value', keySettings.axisTicksFormatter) as TbFlotTicksFormatterFunction; + } catch (e) { + yaxis.ticksFormatterFunction = this.yaxis.ticksFormatterFunction; + } + } + return yaxis; + } + + private subscribeForThresholdsAttributes(datasources: Datasource[]) { + const thresholdsSourcesSubscriptionOptions: WidgetSubscriptionOptions = { + datasources, + useDashboardTimewindow: false, + type: widgetType.latest, + callbacks: { + onDataUpdated: (subscription) => {this.thresholdsSourcesDataUpdated(subscription.data)} + } + }; + this.ctx.subscriptionApi.createSubscription(thresholdsSourcesSubscriptionOptions, true).subscribe( + (subscription) => { + this.thresholdsSourcesSubscription = subscription; + } + ); + } + + private thresholdsSourcesDataUpdated(data: DatasourceData[]) { + const allThresholds = deepClone(this.predefinedThresholds); + data.forEach((keyData) => { + if (keyData && keyData.data && keyData.data[0]) { + const attrValue = keyData.data[0][1]; + if (isFinite(attrValue)) { + const settings: TbFlotThresholdKeySettings = keyData.dataKey.settings; + const colorIndex = this.subscription.data.length + allThresholds.length; + this.generateThreshold(allThresholds, settings.yaxis, settings.lineWidth, settings.color, colorIndex, attrValue); + } + } + }); + this.options.grid.markings = allThresholds; + this.redrawPlot(); + } + + private generateThreshold(existingThresholds: TbFlotThresholdMarking[], yaxis: number, lineWidth: number, + color: string, defaultColorIndex: number, thresholdValue: number) { + const marking: TbFlotThresholdMarking = {}; + let markingYAxis; + + if (yaxis !== 1) { + markingYAxis = 'y' + yaxis + 'axis'; + } else { + markingYAxis = 'yaxis'; + } + + if (isFinite(lineWidth)) { + marking.lineWidth = lineWidth; + } + + if (isDefined(color)) { + marking.color = color; + } else { + marking.color = this.utils.getMaterialColor(defaultColorIndex); + } + + marking[markingYAxis] = { + from: thresholdValue, + to: thresholdValue + }; + + const similarMarkings = existingThresholds.filter((existingMarking) => { + return isEqual(existingMarking[markingYAxis], marking[markingYAxis]); + }); + if (!similarMarkings.length) { + existingThresholds.push(marking); + } + } + + private subscribeForLabelPatternsSources(datasources: Datasource[]) { + const labelPatternsSourcesSubscriptionOptions: WidgetSubscriptionOptions = { + datasources, + useDashboardTimewindow: false, + type: widgetType.latest, + callbacks: { + onDataUpdated: (subscription) => { + this.labelPatternsParamsDataUpdated(subscription.data) + } + } + }; + this.ctx.subscriptionApi.createSubscription(labelPatternsSourcesSubscriptionOptions, true).subscribe( + (subscription) => { + this.labelPatternsSourcesSubscription = subscription; + } + ); + } + + private labelPatternsParamsDataUpdated(data: DatasourceData[]) { + this.labelPatternsSourcesData = data; + for (let i = 0; i < this.subscription.data.length; i++) { + const series = this.subscription.data[i] as TbFlotSeries; + this.substituteLabelPatterns(series, i); + } + this.updateData(); + this.ctx.detectChanges(); + } + + private substituteLabelPatterns(series: TbFlotSeries, seriesIndex: number) { + let seriesLabelPatternsSourcesData = this.labelPatternsSourcesData.filter((item) => { + return item.datasource.entityId === series.datasource.entityId; + }); + let label = createLabelFromDatasource(series.datasource, series.dataKey.pattern); + for (let i = 0; i < seriesLabelPatternsSourcesData.length; i++) { + let keyData = seriesLabelPatternsSourcesData[i]; + if (keyData && keyData.data && keyData.data[0]) { + let attrValue = keyData.data[0][1]; + let attrName = keyData.dataKey.name; + if (isDefined(attrValue) && (attrValue !== null)) { + label = insertVariable(label, attrName, attrValue); + } + } + } + if (isDefined(this.subscription.legendData)) { + let targetLegendKeyIndex = this.subscription.legendData.keys.findIndex((key) => { + return key.dataIndex === seriesIndex; + }); + if (targetLegendKeyIndex !== -1) { + this.subscription.legendData.keys[targetLegendKeyIndex].dataKey.label = label; + } + } + series.dataKey.label = label; + } + + private seriesInfoDiv(label: string, color: string, value: any, + units: string, trackDecimals: number, active: boolean, + percent: number, valueFormatFunction: TooltipValueFormatFunction): JQuery { + const divElement = $('
'); + divElement.css({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between' + }); + const lineSpan = $(''); + lineSpan.css({ + backgroundColor: color, + width: '20px', + height: '3px', + display: 'inline-block', + verticalAlign: 'middle', + marginRight: '5px' + }); + divElement.append(lineSpan); + const labelSpan = $(`${label}:`); + labelSpan.css({ + marginRight: '10px' + }); + if (active) { + labelSpan.css({ + color: '#FFF', + fontWeight: '700' + }); + } + divElement.append(labelSpan); + let valueContent: string; + if (valueFormatFunction) { + valueContent = valueFormatFunction(value); + } else { + valueContent = this.ctx.utils.formatValue(value, trackDecimals, units); + } + if (isNumber(percent)) { + valueContent += ' (' + Math.round(percent) + ' %)'; + } + const valueSpan = $(`${valueContent}`); + valueSpan.css({ + marginLeft: 'auto', + fontWeight: '700' + }); + if (active) { + valueSpan.css({ + color: '#FFF' + }); + } + divElement.append(valueSpan); + return divElement; + } + + private seriesInfoDivFromInfo(seriesHoverInfo: TbFlotSeriesHoverInfo, seriesIndex: number): string { + const units = seriesHoverInfo.units && seriesHoverInfo.units.length ? seriesHoverInfo.units : this.trackUnits; + const decimals = isDefinedAndNotNull(seriesHoverInfo.decimals) ? seriesHoverInfo.decimals : this.trackDecimals; + const divElement = this.seriesInfoDiv(seriesHoverInfo.label, seriesHoverInfo.color, + seriesHoverInfo.value, units, decimals, seriesHoverInfo.index === seriesIndex, null, seriesHoverInfo.tooltipValueFormatFunction); + return divElement.prop('outerHTML'); + } + + private createTooltipElement(): JQuery { + const tooltip = $('
'); + tooltip.css({ + fontSize: '12px', + fontFamily: 'Roboto', + fontWeight: '300', + lineHeight: '18px', + opacity: '1', + backgroundColor: 'rgba(0,0,0,0.7)', + color: '#D9DADB', + position: 'absolute', + display: 'none', + zIndex: '1100', + padding: '4px 10px', + borderRadius: '4px' + }).appendTo('body'); + return tooltip; + } + + private formatPieTooltip(item: TbFlotPlotItem): string { + const units = item.series.dataKey.units && item.series.dataKey.units.length ? item.series.dataKey.units : this.trackUnits; + const decimals = isDefinedAndNotNull(item.series.dataKey.decimals) ? item.series.dataKey.decimals : this.trackDecimals; + const divElement = this.seriesInfoDiv(item.series.dataKey.label, item.series.dataKey.color, + item.datapoint[1][0][1], units, decimals, true, item.series.percent, item.series.dataKey.tooltipValueFormatFunction); + return divElement.prop('outerHTML'); + } + + private formatChartTooltip(hoverInfo: TbFlotHoverInfo[], seriesIndex: number): string { + let content = ''; + if (this.tooltipIndividual) { + let seriesHoverArray: TbFlotSeriesHoverInfo[]; + if (hoverInfo[1] && hoverInfo[1].seriesHover.length) { + seriesHoverArray = hoverInfo[0].seriesHover.concat(hoverInfo[1].seriesHover); + } else { + seriesHoverArray = hoverInfo[0].seriesHover; + } + const found = seriesHoverArray.filter((seriesHover) => { + return seriesHover.index === seriesIndex; + }); + if (found && found.length) { + const timestamp = parseInt(found[0].time, 10); + const date = moment(timestamp).format('YYYY-MM-DD HH:mm:ss'); + const dateDiv = $('
' + date + '
'); + dateDiv.css({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: '4px', + fontWeight: '700' + }); + content += dateDiv.prop('outerHTML'); + content += this.seriesInfoDivFromInfo(found[0], seriesIndex); + } + } else { + let maxRows: number; + if (hoverInfo[1] && hoverInfo[1].seriesHover.length) { + maxRows = 5; + } else { + maxRows = 15; + } + let columns = 0; + if (hoverInfo[1] && (hoverInfo[1].seriesHover.length > hoverInfo[0].seriesHover.length)) { + columns = Math.ceil(hoverInfo[1].seriesHover.length / maxRows); + } else { + columns = Math.ceil(hoverInfo[0].seriesHover.length / maxRows); + } + hoverInfo.forEach((hoverData) => { + if (isNumber(hoverData.time)) { + let columnsContent = ''; + const timestamp = parseInt(hoverData.time, 10); + const date = moment(timestamp).format('YYYY-MM-DD HH:mm:ss'); + const dateDiv = $('
' + date + '
'); + dateDiv.css({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: '4px', + fontWeight: '700' + }); + content += dateDiv.prop('outerHTML'); + + const seriesDiv = $('
'); + seriesDiv.css({ + display: 'flex', + flexDirection: 'row' + }); + for (let c = 0; c < columns; c++) { + const columnDiv = $('
'); + columnDiv.css({ + display: 'flex', + flexDirection: 'column', + flex: '1' + }); + let columnContent = ''; + for (let i = c*maxRows; i < (c+1)*maxRows; i++) { + if (i >= hoverData.seriesHover.length) { + break; + } + const seriesHoverInfo = hoverData.seriesHover[i]; + columnContent += this.seriesInfoDivFromInfo(seriesHoverInfo, seriesIndex); + } + columnDiv.html(columnContent); + + if (columnContent) { + if (c > 0) { + columnsContent += ''; + } + columnsContent += columnDiv.prop('outerHTML'); + } + } + seriesDiv.html(columnsContent); + content += seriesDiv.prop('outerHTML'); + } + }); + } + return content; + } + + private formatYAxisTicks(value: number, axis?: TbFlotPlotAxis): string { + if (this.settings.yaxis && this.settings.yaxis.showLabels === false) { + return ''; + } + if (axis.options.ticksFormatterFunction) { + return axis.options.ticksFormatterFunction(value); + } + const factor = axis.options.tickDecimals ? Math.pow(10, axis.options.tickDecimals) : 1; + let formatted = '' + Math.round(value * factor) / factor; + if (isDefined(axis.options.tickDecimals) && axis.options.tickDecimals !== null) { + const decimal = formatted.indexOf('.'); + const precision = decimal === -1 ? 0 : formatted.length - decimal - 1; + if (precision < axis.options.tickDecimals) { + formatted = (precision ? formatted : formatted + '.') + ('' + factor).substr(1, axis.options.tickDecimals - precision); + } + } + if (axis.options.tickUnits) { + formatted += ' ' + axis.options.tickUnits; + } + return formatted; + } + + private enableMouseEvents() { + this.$element.css('pointer-events', ''); + this.$element.addClass('mouse-events'); + this.options.selection = { mode : 'x' }; + this.$element.bind('plothover', this.flotHoverHandler); + this.$element.bind('plotselected', this.flotSelectHandler); + this.$element.bind('dblclick', this.dblclickHandler); + this.$element.bind('mousedown', this.mousedownHandler); + this.$element.bind('mouseup', this.mouseupHandler); + this.$element.bind('mouseleave', this.mouseleaveHandler); + this.$element.bind('plotclick', this.flotClickHandler); + } + + private disableMouseEvents() { + this.$element.css('pointer-events', 'none'); + this.$element.removeClass('mouse-events'); + this.options.selection = { mode : null }; + this.$element.unbind('plothover', this.flotHoverHandler); + this.$element.unbind('plotselected', this.flotSelectHandler); + this.$element.unbind('dblclick', this.dblclickHandler); + this.$element.unbind('mousedown', this.mousedownHandler); + this.$element.unbind('mouseup', this.mouseupHandler); + this.$element.unbind('mouseleave', this.mouseleaveHandler); + this.$element.unbind('plotclick', this.flotClickHandler); + } + + private onFlotHover(e: any, pos: JQueryPlotPoint, item: TbFlotPlotItem) { + if (!this.plot) { + return; + } + if (!this.tooltipIndividual || item) { + const multipleModeTooltip = !this.tooltipIndividual; + if (multipleModeTooltip) { + this.plot.unhighlight(); + } + const pageX = pos.pageX; + const pageY = pos.pageY; + + let tooltipHtml; + let hoverInfo: TbFlotHoverInfo[]; + + if (this.chartType === 'pie') { + tooltipHtml = this.formatPieTooltip(item); + } else { + hoverInfo = this.getHoverInfo(this.plot.getData(), pos); + if (isNumber(hoverInfo[0].time) || (hoverInfo[1] && isNumber(hoverInfo[1].time))) { + hoverInfo[0].seriesHover.sort((a, b) => { + return b.value - a.value; + }); + if (hoverInfo[1] && hoverInfo[1].seriesHover.length) { + hoverInfo[1].seriesHover.sort((a, b) => { + return b.value - a.value; + }); + } + tooltipHtml = this.formatChartTooltip(hoverInfo, item ? item.seriesIndex : -1); + } + } + if (tooltipHtml) { + this.tooltip.html(tooltipHtml) + .css({top: 0, left: 0}) + .fadeIn(200); + + const windowWidth = $( window ).width(); + const windowHeight = $( window ).height(); + const tooltipWidth = this.tooltip.width(); + const tooltipHeight = this.tooltip.height(); + let left = pageX + 5; + let top = pageY + 5; + if (windowWidth - pageX < tooltipWidth + 50) { + left = pageX - tooltipWidth - 10; + } + if (windowHeight - pageY < tooltipHeight + 20) { + top = pageY - tooltipHeight - 10; + } + this.tooltip.css({ + top, + left + }); + if (multipleModeTooltip) { + hoverInfo.forEach((hoverData) => { + hoverData.seriesHover.forEach((seriesHoverInfo) => { + this.plot.highlight(seriesHoverInfo.index, seriesHoverInfo.hoverIndex); + }); + }); + } + } + } else { + this.tooltip.stop(true); + this.tooltip.hide(); + this.plot.unhighlight(); + } + } + + private onFlotSelect(e: any, ranges: JQueryPlotSelectionRanges) { + if (!this.plot) { + return; + } + this.plot.clearSelection(); + this.subscription.onUpdateTimewindow(ranges.xaxis.from, ranges.xaxis.to); + } + + private onFlotDblClick() { + this.subscription.onResetTimewindow(); + } + + private onFlotMouseDown() { + this.isMouseInteraction = true; + } + + private onFlotMouseUp() { + this.isMouseInteraction = false; + } + + private onFlotMouseLeave() { + if (!this.tooltip) { + return; + } + this.tooltip.stop(true); + this.tooltip.hide(); + if (this.plot) { + this.plot.unhighlight(); + } + this.isMouseInteraction = false; + } + + private onFlotClick(e: any, pos: JQueryPlotPoint, item: TbFlotPlotItem) { + if (!this.plot) { + return; + } + this.onPieSliceClick(e, item); + } + + private getHoverInfo(seriesList: TbFlotPlotDataSeries[], pos: JQueryPlotPoint): TbFlotHoverInfo[] { + let i: number; + let series: TbFlotPlotDataSeries; + let hoverIndex: number; + let hoverDistance: number; + let minDistance: number; + let pointTime: any; + let minTime: any; + let minTimeHistorical: any; + let hoverData: TbFlotSeriesHoverInfo; + let value: any; + let lastValue: any; + let minDistanceHistorical: number; + const results: TbFlotHoverInfo[] = [{ + seriesHover: [] + }]; + if (this.settings.comparisonEnabled) { + results.push({ + seriesHover: [] + }); + } + for (i = 0; i < seriesList.length; i++) { + series = seriesList[i]; + let posx: number; + if (series.datasource.isAdditional) { + posx = pos.x2; + } else { + posx = pos.x; + } + hoverIndex = this.findHoverIndexFromData(posx, series); + if (series.data[hoverIndex] && series.data[hoverIndex][0]) { + hoverDistance = posx - series.data[hoverIndex][0]; + pointTime = series.data[hoverIndex][0]; + + if (series.datasource.isAdditional) { + if (!minDistanceHistorical + || (hoverDistance >= 0 && (hoverDistance < minDistanceHistorical || minDistanceHistorical < 0)) + || (hoverDistance < 0 && hoverDistance > minDistanceHistorical)) { + minDistanceHistorical = hoverDistance; + minTimeHistorical = pointTime; + } + } else if (!minDistance + || (hoverDistance >= 0 && (hoverDistance < minDistance || minDistance < 0)) + || (hoverDistance < 0 && hoverDistance > minDistance)) { + minDistance = hoverDistance; + minTime = pointTime; + } + if (series.stack) { + if (this.tooltipIndividual || !this.tooltipCumulative) { + value = series.data[hoverIndex][1]; + } else { + lastValue += series.data[hoverIndex][1]; + value = lastValue; + } + } else { + value = series.data[hoverIndex][1]; + } + if (series.stack || (series.curvedLines && series.curvedLines.apply)) { + hoverIndex = this.findHoverIndexFromDataPoints(posx, series, hoverIndex); + } + if (!this.hideZeros || value) { + hoverData = { + value, + hoverIndex, + color: series.dataKey.color, + label: series.dataKey.label, + units: series.dataKey.units, + decimals: series.dataKey.decimals, + tooltipValueFormatFunction: series.dataKey.tooltipValueFormatFunction, + time: pointTime, + distance: hoverDistance, + index: i + }; + if (series.datasource.isAdditional) { + results[1].seriesHover.push(hoverData); + } else { + results[0].seriesHover.push(hoverData); + } + } + } + } + if (results[1] && results[1].seriesHover.length) { + results[1].time = minTimeHistorical; + } + results[0].time = minTime; + return results; + } + + private findHoverIndexFromData(posX: number, series: TbFlotPlotDataSeries): number { + let lower = 0; + let upper = series.data.length - 1; + let middle: number; + const index: number = null; + while (index === null) { + if (lower > upper) { + return Math.max(upper, 0); + } + middle = Math.floor((lower + upper) / 2); + if (series.data[middle][0] === posX) { + return middle; + } else if (series.data[middle][0] < posX) { + lower = middle + 1; + } else { + upper = middle - 1; + } + } + } + + private findHoverIndexFromDataPoints(posX: number, series: TbFlotPlotDataSeries, last: number): number { + const ps = series.datapoints.pointsize; + const initial = last * ps; + const len = series.datapoints.points.length; + let j: number; + for (j = initial; j < len; j += ps) { + if ((!series.lines.steps && series.datapoints.points[initial] != null && series.datapoints.points[j] == null) + || series.datapoints.points[j] > posX) { + return Math.max(j - ps, 0) / ps; + } + } + return j / ps - 1; + } + + pieDataRendered() { + for (let i = 0; i < this.pieTargetData.length; i++) { + const value = this.pieTargetData[i] ? this.pieTargetData[i] : 0; + this.pieRenderedData[i] = value; + if (!this.pieData[i].data[0]) { + this.pieData[i].data[0] = [0, 0]; + } + this.pieData[i].data[0][1] = value; + } + } + + nextPieDataAnimation(start) { + if (start) { + this.finishPieDataAnimation(); + this.pieAnimationStartTime = this.pieAnimationLastTime = Date.now(); + for (let i = 0; i < this.subscription.data.length; i++) { + this.pieTargetData[i] = (this.subscription.data[i].data && this.subscription.data[i].data[0]) + ? this.subscription.data[i].data[0][1] : 0; + } + } + if (this.pieAnimationCaf) { + this.pieAnimationCaf(); + this.pieAnimationCaf = null; + } + this.pieAnimationCaf = this.ctx.$scope.raf.raf(this.onPieDataAnimation.bind(this)); + } + + onPieDataAnimation() { + const time = Date.now(); + const elapsed = time - this.pieAnimationLastTime; // this.pieAnimationStartTime; + const progress = (time - this.pieAnimationStartTime) / this.pieDataAnimationDuration; + if (progress >= 1) { + this.finishPieDataAnimation(); + } else { + if (elapsed >= 40) { + for (let i = 0; i < this.pieTargetData.length; i++) { + const prevValue = this.pieRenderedData[i]; + const targetValue = this.pieTargetData[i]; + const value = prevValue + (targetValue - prevValue) * progress; + if (!this.pieData[i].data[0]) { + this.pieData[i].data[0] = [0, 0]; + } + this.pieData[i].data[0][1] = value; + } + this.plot.setData(this.pieData); + this.plot.draw(); + this.pieAnimationLastTime = time; + } + this.nextPieDataAnimation(false); + } + } + + private finishPieDataAnimation() { + this.pieDataRendered(); + this.plot.setData(this.pieData); + this.plot.draw(); + } + + private onPieSliceClick($event: any, item: TbFlotPlotItem) { + const descriptors = this.ctx.actionsApi.getActionDescriptors('sliceClick'); + if ($event && descriptors.length) { + $event.stopPropagation(); + const entityInfo = this.ctx.actionsApi.getActiveEntityInfo(); + const entityId = entityInfo ? entityInfo.entityId : null; + const entityName = entityInfo ? entityInfo.entityName : null; + const entityLabel = entityInfo ? entityInfo.entityLabel : null; + this.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName, item, entityLabel); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.html new file mode 100644 index 0000000000..8340857d8d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.html @@ -0,0 +1,266 @@ + +
+ + + + +
{{ 'gateway.thingsboard' | translate | uppercase }}
+
+
+ + +
+ + {{'gateway.security-type' | translate }} + + + {{ securityType.value.toString() | translate }} + + + +
+
+ + {{ 'gateway.thingsboard-host' | translate }} + + + gateway.thingsboard-host-required + + + + {{ 'gateway.thingsboard-port' | translate }} + + + gateway.thingsboard-port-required + + + gateway.thingsboard-port-min + + + gateway.thingsboard-port-max + + + gateway.thingsboard-port-pattern + + +
+ +
+ + {{ 'gateway.tls-path-ca-certificate' | translate }} + + + + {{ 'gateway.tls-path-private-key' | translate }} + + + + {{ 'gateway.tls-path-client-certificate' | translate }} + + +
+ + {{ 'gateway.remote' | translate }} + +
+ + {{'gateway.remote-logging-level' | translate }} + + + {{ logLevel }} + + + + + + {{ 'gateway.path-logs' | translate }} + + + gateway.path-logs-required + + +
+ +
+ + + + +
{{ 'gateway.storage' | translate | uppercase }}
+
+
+ +
+ + {{'gateway.storage-type' | translate }} + + + {{ storageType.value.toString() | translate}} + + + + +
+ + {{ 'gateway.storage-pack-size' | translate }} + + + gateway.storage-pack-size-required + + + gateway.storage-pack-size-min + + + gateway.storage-pack-size-pattern + + + + + + {{ (gatewayConfigurationGroup.get('storageType').value !== 'file' ? 'gateway.storage-max-records' : 'gateway.storage-max-file-records') | translate}} + + + + gateway.storage-max-records-required + + + gateway.storage-max-records-min + + + gateway.storage-max-records-pattern + + +
+ +
+ + {{ 'gateway.storage-max-files' | translate }} + + + gateway.storage-max-files-required + + + gateway.storage-max-files-min + + + gateway.storage-max-files-pattern + + + + + {{ 'gateway.storage-path' | translate }} + + + gateway.storage-path-required + + +
+
+
+ + + + +
{{ 'gateway.connectors' | translate | uppercase }}
+
+
+ +
+
+
+
+ +
+
+ + {{'gateway.connector-type' | translate }} + + + {{ connectorType }} + + + + gateway.connector-type-required + + + + + {{ 'gateway.connector-name' | translate }} + + + gateway.connector-name-required + + +
+
+ + +
+
+
+ {{'gateway.no-connectors' | translate}} +
+ +
+
+
+
+
+ + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.scss new file mode 100644 index 0000000000..5f21a32c51 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.scss @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host{ + .gateway-form { + height: 100%; + padding: 5px; + background-color: transparent; + overflow-y: auto; + overflow-x: hidden; + + .form-action-buttons{ + padding-top: 8px; + } + + .gateway-config { + .no-data-found { + position: relative; + display: flex; + height: 40px; + } + } + } + + ::ng-deep .mat-form-field-infix { + width: auto !important; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.ts new file mode 100644 index 0000000000..542c196775 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.ts @@ -0,0 +1,426 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, Inject, Input, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { AbstractControl, FormArray, FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { UtilsService } from '@core/services/utils.service'; +import { + CONFIGURATION_ATTRIBUTE, + CONFIGURATION_DRAFT_ATTRIBUTE, + ConnectorType, + createFormConfig, + CURRENT_CONFIGURATION_ATTRIBUTE, + DEFAULT_CONNECTOR, + gatewayConfigJSON, + GatewayFormConnectorModel, + GatewayFormModels, + GatewayLogLevel, + generateConnectorConfigFiles, + generateLogConfigFile, + generateYAMLConfigFile, + getDraftConnectorsJSON, + getEntityId, + REMOTE_LOGGING_LEVEL_ATTRIBUTE, + SecurityType, + SecurityTypeTranslationMap, + StorageType, + StorageTypeTranslationMap, + ValidateJSON, + WidgetSetting +} from './gateway-form.models'; +import { WINDOW } from '@core/services/window.service'; +import { MatDialog } from '@angular/material/dialog'; +import { + JsonObjectEditDialogComponent, + JsonObjectEditDialogData +} from '@shared/components/dialog/json-object-edit-dialog.component'; +import { TranslateService } from '@ngx-translate/core'; +import { DeviceService } from '@core/http/device.service'; +import { AttributeService } from '@core/http/attribute.service'; +import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models'; +import { forkJoin, Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { ImportExportService } from '@home/components/import-export/import-export.service'; +import { ResizeObserver } from '@juggle/resize-observer'; + +// @dynamic +@Component({ + selector: 'tb-gateway-form', + templateUrl: './gateway-form.component.html', + styleUrls: ['./gateway-form.component.scss'] +}) + +export class GatewayFormComponent extends PageComponent implements OnInit, OnDestroy { + constructor( + protected store: Store, + private elementRef: ElementRef, + private utils: UtilsService, + private ngZone: NgZone, + private fb: FormBuilder, + @Inject(WINDOW) private window: Window, + private dialog: MatDialog, + private translate: TranslateService, + private deviceService: DeviceService, + private attributeService: AttributeService, + private importExport: ImportExportService + ) { + super(store); + } + + get connectors(): FormArray { + return this.gatewayConfigurationGroup.get('connectors') as FormArray; + } + + @ViewChild('formContainer', {static: true}) formContainerRef: ElementRef; + @ViewChild('gatewayConfigurationForm', {static: true}) multipleInputForm: NgForm; + + private successfulSaved: string; + private gatewayNameExists: string; + private archiveFileName: string; + private formResize$: ResizeObserver; + private subscribeStorageType$: any; + private subscribeGateway$: any; + + alignment = 'row'; + layoutGap = '5px'; + gatewayType: string; + gatewayConfigurationGroup: FormGroup; + securityTypes = SecurityTypeTranslationMap; + gatewayLogLevels = Object.values(GatewayLogLevel); + connectorTypes = Object.keys(ConnectorType); + storageTypes = StorageTypeTranslationMap; + + toastTargetId = 'gateway-configuration-widget' + this.utils.guid(); + + @Input() + ctx: WidgetContext; + + @Input() + isStateForm: boolean; + + isReadOnlyForm = false; + deviceNameForm: string; + + ngOnInit(): void { + this.initWidgetSettings(this.ctx.settings); + if (this.ctx.datasources && this.ctx.datasources.length) { + this.deviceNameForm = this.ctx.datasources[0].name; + } + + this.buildForm(); + this.ctx.updateWidgetParams(); + this.formResize$ = new ResizeObserver(() => { + this.resize(); + }); + this.formResize$.observe(this.formContainerRef.nativeElement); + } + + ngOnDestroy(): void { + if (this.formResize$) { + this.formResize$.disconnect(); + } + this.subscribeGateway$.unsubscribe(); + this.subscribeStorageType$.unsubscribe(); + } + + private initWidgetSettings(settings: WidgetSetting): void { + let widgetTitle; + if (settings.gatewayTitle && settings.gatewayTitle.length) { + widgetTitle = this.utils.customTranslation(settings.gatewayTitle, settings.gatewayTitle); + } else { + widgetTitle = this.translate.instant('gateway.gateway'); + } + this.ctx.widgetTitle = widgetTitle; + this.isReadOnlyForm = (settings.readOnly) ? settings.readOnly : false; + + this.archiveFileName = settings.archiveFileName?.length ? settings.archiveFileName : 'gatewayConfiguration'; + this.gatewayType = settings.gatewayType?.length ? settings.gatewayType : 'Gateway'; + this.gatewayNameExists = this.utils.customTranslation(settings.gatewayNameExists, settings.gatewayNameExists) || this.translate.instant('gateway.gateway-exists'); + this.successfulSaved = this.utils.customTranslation(settings.successfulSave, settings.successfulSave) || this.translate.instant('gateway.gateway-saved'); + + this.updateWidgetDisplaying(); + } + + private resize(): void { + this.ngZone.run(() => { + this.updateWidgetDisplaying(); + this.ctx.detectChanges(); + }); + } + + private updateWidgetDisplaying(): void { + if(this.ctx.$container && this.ctx.$container[0].offsetWidth <= 425){ + this.layoutGap = '0'; + this.alignment = 'column'; + } else { + this.layoutGap = '5px'; + this.alignment = 'row'; + } + } + + private saveAttribute(attributeName: string, attributeValue: string, attributeScope: AttributeScope): Observable { + const gatewayId = this.gatewayConfigurationGroup.get('gateway').value; + const attributes: AttributeData = { + key: attributeName, + value: attributeValue + }; + return this.attributeService.saveEntityAttributes(getEntityId(gatewayId), attributeScope, [attributes]); + } + + private createConnector(setting: GatewayFormConnectorModel = DEFAULT_CONNECTOR): void { + this.connectors.push(this.fb.group({ + enabled: [setting.enabled], + configType: [setting.configType, [Validators.required]], + name: [setting.name, [Validators.required]], + config: [setting.config, [Validators.nullValidator, ValidateJSON]] + })); + } + + private getFormField(name: string): AbstractControl { + return this.gatewayConfigurationGroup.get(name); + } + + private buildForm(): void { + this.gatewayConfigurationGroup = this.fb.group({ + gateway: [null, []], + accessToken: [null, [Validators.required]], + securityType: [SecurityType.accessToken], + host: [this.window.location.hostname, [Validators.required]], + port: [1883, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]], + remoteConfiguration: [true], + caCertPath: ['/etc/thingsboard-gateway/ca.pem'], + privateKeyPath: ['/etc/thingsboard-gateway/privateKey.pem'], + certPath: ['/etc/thingsboard-gateway/certificate.pem'], + remoteLoggingLevel: [GatewayLogLevel.debug], + remoteLoggingPathToLogs: ['./logs/', [Validators.required]], + storageType: [StorageType.memory], + readRecordsCount: [100, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], + maxRecordsCount: [10000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], + maxFilesCount: [5, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], + dataFolderPath: ['./data/', [Validators.required]], + connectors: this.fb.array([]) + }); + + if (this.isReadOnlyForm) { + this.gatewayConfigurationGroup.disable({emitEvent: false}); + } + + this.subscribeStorageType$ = this.getFormField('storageType').valueChanges.subscribe((value: StorageType) => { + if (value === StorageType.memory) { + this.getFormField('maxFilesCount').disable(); + this.getFormField('dataFolderPath').disable(); + } else { + this.getFormField('maxFilesCount').enable(); + this.getFormField('dataFolderPath').enable(); + } + }); + + this.subscribeGateway$ = this.getFormField('gateway').valueChanges.subscribe((gatewayId: string) => { + if (gatewayId !== null) { + forkJoin([ + this.deviceService.getDeviceCredentials(gatewayId).pipe(tap(deviceCredential => { + this.getFormField('accessToken').patchValue(deviceCredential.credentialsId); + })), + ...this.getAttributes(gatewayId)]).subscribe(() => { + this.gatewayConfigurationGroup.markAsPristine(); + this.ctx.detectChanges(); + }); + } else { + this.getFormField('accessToken').patchValue(''); + } + }) + } + + gatewayExist(): void { + this.ctx.showErrorToast(this.gatewayNameExists, 'top', 'left', this.toastTargetId); + } + + exportConfig(): void { + const gatewayConfiguration: GatewayFormModels = this.gatewayConfigurationGroup.value; + const filesZip: any = {}; + filesZip['tb_gateway.yaml'] = generateYAMLConfigFile(gatewayConfiguration); + generateConnectorConfigFiles(filesZip, gatewayConfiguration.connectors); + generateLogConfigFile(filesZip, gatewayConfiguration.remoteLoggingLevel, gatewayConfiguration.remoteLoggingPathToLogs); + this.importExport.exportJSZip(filesZip, this.archiveFileName); + this.saveAttribute( + REMOTE_LOGGING_LEVEL_ATTRIBUTE, + this.gatewayConfigurationGroup.value.remoteLoggingLevel.toUpperCase(), + AttributeScope.SHARED_SCOPE); + } + + addNewConnector(): void { + this.createConnector(); + } + + removeConnector(index: number): void { + if (index > -1) { + this.connectors.removeAt(index); + this.connectors.markAsDirty(); + } + } + + openConfigDialog($event: Event, index: number, config: object, type: string): void { + if ($event) { + $event.stopPropagation(); + $event.preventDefault(); + } + this.dialog.open(JsonObjectEditDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + jsonValue: config, + title: this.translate.instant('gateway.title-connectors-json', {typeName: type}) + } + }).afterClosed().subscribe( + (res) => { + if (res) { + this.connectors.at(index).get('config').patchValue(res); + this.ctx.detectChanges(); + } + } + ); + } + + private createConnectorName(connectors: GatewayFormConnectorModel[], name: string, index: number = 0): string { + const newKeyName = index ? name + index : name; + const indexRes = connectors.findIndex((element) => element.name === newKeyName); + return indexRes === -1 ? newKeyName : this.createConnectorName(connectors, name, ++index); + } + + private validateConnectorName(connectors: GatewayFormConnectorModel[], name: string, connectorIndex: number, index = 0): string { + for (let i = 0; i < connectors.length; i++) { + const nameEq = (index === 0) ? name : name + index; + if (i !== connectorIndex && connectors[i].name === nameEq) { + this.validateConnectorName(connectors, name, connectorIndex, ++index); + } + } + return (index === 0) ? name : name + index; + } + + changeConnectorType(connector: AbstractControl): void { + if (!connector.get('name').value) { + const typeConnector = connector.get('configType').value; + const connectors = this.gatewayConfigurationGroup.value.connectors; + connector.get('name').patchValue(this.createConnectorName(connectors, ConnectorType[typeConnector])); + } + } + + changeConnectorName(connector: AbstractControl, index: number): void { + const connectors = this.gatewayConfigurationGroup.value.connectors; + connector.get('name').patchValue(this.validateConnectorName(connectors, connector.get('name').value, index)); + } + + save(): void { + const gatewayConfiguration: GatewayFormModels = this.gatewayConfigurationGroup.value; + forkJoin([ + this.saveAttribute( + CONFIGURATION_ATTRIBUTE, + window.btoa(JSON.stringify(gatewayConfigJSON(gatewayConfiguration))), + AttributeScope.SHARED_SCOPE), + this.saveAttribute( + CONFIGURATION_DRAFT_ATTRIBUTE, + window.btoa(JSON.stringify(getDraftConnectorsJSON(gatewayConfiguration.connectors))), + AttributeScope.SERVER_SCOPE), + this.saveAttribute( + REMOTE_LOGGING_LEVEL_ATTRIBUTE, + this.gatewayConfigurationGroup.value.remoteLoggingLevel.toUpperCase(), + AttributeScope.SHARED_SCOPE) + ]).subscribe(() =>{ + this.ctx.showSuccessToast(this.successfulSaved, + 2000, 'top', 'left', this.toastTargetId); + this.gatewayConfigurationGroup.markAsPristine(); + }) + } + + private getAttributes(gatewayId: string): Array>> { + const tasks = []; + tasks.push(forkJoin([this.getAttribute(CURRENT_CONFIGURATION_ATTRIBUTE, AttributeScope.CLIENT_SCOPE, gatewayId), + this.getAttribute(CONFIGURATION_DRAFT_ATTRIBUTE, AttributeScope.SERVER_SCOPE, gatewayId)]).pipe( + tap(([currentConfig, draftConfig]) => { + this.setFormGatewaySettings(currentConfig); + this.setFormConnectorsDraft(draftConfig); + if (this.isReadOnlyForm) { + this.gatewayConfigurationGroup.disable({emitEvent: false}); + } + }) + ) + ); + tasks.push(this.getAttribute(REMOTE_LOGGING_LEVEL_ATTRIBUTE, AttributeScope.SHARED_SCOPE, gatewayId).pipe( + tap(logsLevel => this.processLoggingLevel(logsLevel)) + )); + return tasks; + } + + private getAttribute(attributeName: string, attributeScope: AttributeScope, gatewayId: string): Observable> { + return this.attributeService.getEntityAttributes(getEntityId(gatewayId), attributeScope, [attributeName]); + } + + private setFormGatewaySettings(response: Array): void { + this.connectors.clear(); + if (response.length > 0) { + const attribute = JSON.parse(window.atob(response[0].value)); + for (const attributeKey of Object.keys(attribute)) { + const keyValue = attribute[attributeKey]; + if (attributeKey === 'thingsboard') { + if (keyValue !== null && Object.keys(keyValue).length > 0) { + this.gatewayConfigurationGroup.patchValue(createFormConfig(keyValue)); + } + } else { + for (const connector of Object.keys(keyValue)) { + let name = 'No name'; + if (Object.prototype.hasOwnProperty.call(keyValue[connector], 'name')) { + name = keyValue[connector].name; + } + const newConnector: GatewayFormConnectorModel = { + enabled: true, + configType: (attributeKey as ConnectorType), + config: keyValue[connector].config, + name + }; + this.createConnector(newConnector); + } + } + } + } + } + + private setFormConnectorsDraft(response: Array): void { + if (response.length > 0) { + const attribute = JSON.parse(window.atob(response[0].value)); + for (const connectorName of Object.keys(attribute)) { + const newConnector: GatewayFormConnectorModel = { + enabled: false, + configType: (attribute[connectorName].connector as ConnectorType), + config: attribute[connectorName].config, + name: connectorName + }; + this.createConnector(newConnector); + } + } + } + + private processLoggingLevel(value: Array): void { + let logsLevel = GatewayLogLevel.debug; + if (value.length > 0 && GatewayLogLevel[value[0].value.toLowerCase()]) { + logsLevel = GatewayLogLevel[value[0].value.toLowerCase()]; + } + this.getFormField('remoteLoggingLevel').patchValue(logsLevel); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.models.ts new file mode 100644 index 0000000000..6b49770e44 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.models.ts @@ -0,0 +1,380 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AbstractControl, ValidationErrors } from '@angular/forms'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export enum SecurityType { + tls = 'tls', + accessToken = 'accessToken' +} + +export interface WidgetSetting { + widgetTitle: string; + gatewayTitle?: string; + readOnly?: boolean; + archiveFileName: string; + gatewayType: string; + successfulSave: string; + gatewayNameExists: string; +} + +export const CURRENT_CONFIGURATION_ATTRIBUTE = 'current_configuration'; +export const CONFIGURATION_DRAFT_ATTRIBUTE = 'configuration_drafts'; +export const CONFIGURATION_ATTRIBUTE = 'configuration'; +export const REMOTE_LOGGING_LEVEL_ATTRIBUTE = 'RemoteLoggingLevel'; + +export const SecurityTypeTranslationMap = new Map( + [ + [SecurityType.tls, 'gateway.security-types.tls'], + [SecurityType.accessToken, 'gateway.security-types.access-token'] + ] +); + +export enum GatewayLogLevel { + none = 'NONE', + critical = 'CRITICAL', + error = 'ERROR', + warning = 'WARNING', + info = 'INFO', + debug = 'DEBUG' +} + +export enum StorageType { + memory = 'memory', + file = 'file' +} + +export const StorageTypeTranslationMap = new Map( + [ + [StorageType.memory, 'gateway.storage-types.memory-storage'], + [StorageType.file, 'gateway.storage-types.file-storage'] + ] +); + +export enum ConnectorType { + mqtt= 'MQTT', + modbus = 'Modbus', + opcua = 'OPC-UA', + ble = 'BLE', + request = 'Request', + can = 'CAN', + bacnet = 'BACnet', + custom = 'Custom' +} + +export interface GatewayFormModels { + gateway?: string; + accessToken?: string; + securityType?: SecurityType; + host?: string; + port?: number; + remoteConfiguration?: boolean; + caCertPath?: string; + privateKeyPath?: string; + certPath?: string; + remoteLoggingLevel?: GatewayLogLevel; + remoteLoggingPathToLogs?:string; + storageType?: StorageType; + readRecordsCount?: number; + maxRecordsCount?: number; + maxFilesCount?: number; + dataFolderPath?: number; + connectors?: Array; +} + +export interface GatewayFormConnectorModel { + config: object; + name: string; + configType: ConnectorType; + enabled: boolean; +} + +export const DEFAULT_CONNECTOR: GatewayFormConnectorModel = { + config: {}, + name: '', + configType: null, + enabled: false +}; + +type Connector = { + [key in ConnectorType]?: Array; +} + +interface GatewaySetting extends Connector{ + thingsboard: GatewayMainSetting; +} + +interface ConnectorConfig { + name: string; + config: object; +} + +interface GatewayMainSetting { + thingsboard: GatewayMainThingsboardSetting; + connectors: Array, + logs: string, + storage: GatewayStorage +} + +interface GatewayMainThingsboardSetting { + host: string, + remoteConfiguration: boolean, + port: number, + security: GatewaySecurity +} + +type GatewaySecurity = SecurityToken | SecurityCertificate; + +interface SecurityToken { + accessToken: string; +} + +interface SecurityCertificate { + caCert: string; + privateKey: string; + cert: string; +} + +type GatewayStorage = GatewayStorageMemory | GatewayStorageFile; + +interface GatewayStorageMemory { + type: string; + max_records_count: number; + read_records_count: number; +} + +interface GatewayStorageFile { + type: string; + data_folder_path: number; + max_file_count: number; + max_read_records_count: number; + max_records_per_file: number; +} + +interface GatewayMainConnector { + configuration: string; + name: string; + type: ConnectorType; +} + +export function ValidateJSON(control: AbstractControl): ValidationErrors | null { + if (JSON.stringify(control.value) === JSON.stringify({})) { + return { validJSON: true }; + } + return null; +} + +const TEMPLATE_LOGS_CONFIG = '[loggers]}}keys=root, service, connector, converter, tb_connection, storage, extension}}[handlers]}}keys=consoleHandler, serviceHandler, connectorHandler, converterHandler, tb_connectionHandler, storageHandler, extensionHandler}}[formatters]}}keys=LogFormatter}}[logger_root]}}level=ERROR}}handlers=consoleHandler}}[logger_connector]}}level={ERROR}}}handlers=connectorHandler}}formatter=LogFormatter}}qualname=connector}}[logger_storage]}}level={ERROR}}}handlers=storageHandler}}formatter=LogFormatter}}qualname=storage}}[logger_tb_connection]}}level={ERROR}}}handlers=tb_connectionHandler}}formatter=LogFormatter}}qualname=tb_connection}}[logger_service]}}level={ERROR}}}handlers=serviceHandler}}formatter=LogFormatter}}qualname=service}}[logger_converter]}}level={ERROR}}}handlers=converterHandler}}formatter=LogFormatter}}qualname=converter}}[logger_extension]}}level={ERROR}}}handlers=connectorHandler}}formatter=LogFormatter}}qualname=extension}}[handler_consoleHandler]}}class=StreamHandler}}level={ERROR}}}formatter=LogFormatter}}args=(sys.stdout,)}}[handler_connectorHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}connector.log", "d", 1, 7,)}}[handler_storageHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}storage.log", "d", 1, 7,)}}[handler_serviceHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}service.log", "d", 1, 7,)}}[handler_converterHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}converter.log", "d", 1, 3,)}}[handler_extensionHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}extension.log", "d", 1, 3,)}}[handler_tb_connectionHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}tb_connection.log", "d", 1, 3,)}}[formatter_LogFormatter]}}format="%(asctime)s - %(levelname)s - [%(filename)s] - %(module)s - %(lineno)d - %(message)s" }}datefmt="%Y-%m-%d %H:%M:%S"'; + +export function generateYAMLConfigFile(gatewaySetting: GatewayFormModels): string { + let config; + config = 'thingsboard:\n'; + config += ' host: ' + gatewaySetting.host + '\n'; + config += ' remoteConfiguration: ' + gatewaySetting.remoteConfiguration + '\n'; + config += ' port: ' + gatewaySetting.port + '\n'; + config += ' security:\n'; + if (gatewaySetting.securityType === SecurityType.accessToken) { + config += ' access-token: ' + gatewaySetting.accessToken + '\n'; + } else { + config += ' ca_cert: ' + gatewaySetting.caCertPath + '\n'; + config += ' privateKey: ' + gatewaySetting.privateKeyPath + '\n'; + config += ' cert: ' + gatewaySetting.certPath + '\n'; + } + config += 'storage:\n'; + if (gatewaySetting.storageType === StorageType.memory) { + config += ' type: memory\n'; + config += ' read_records_count: ' + gatewaySetting.readRecordsCount + '\n'; + config += ' max_records_count: ' + gatewaySetting.maxRecordsCount + '\n'; + } else { + config += ' type: file\n'; + config += ' data_folder_path: ' + gatewaySetting.dataFolderPath + '\n'; + config += ' max_file_count: ' + gatewaySetting.maxFilesCount + '\n'; + config += ' max_read_records_count: ' + gatewaySetting.readRecordsCount + '\n'; + config += ' max_records_per_file: ' + gatewaySetting.maxRecordsCount + '\n'; + } + config += 'connectors:\n'; + for(const connector of gatewaySetting.connectors){ + if (connector.enabled) { + config += ' -\n'; + config += ' name: ' + connector.name + '\n'; + config += ' type: ' + connector.configType + '\n'; + config += ' configuration: ' + generateFileName(connector.name) + '\n'; + } + } + return config; +} + +export function generateConnectorConfigFiles(fileZipAdd: object, connectors: Array): void { + for(const connector of connectors) { + if (connector.enabled) { + fileZipAdd[generateFileName(connector.name)] = JSON.stringify(connector.config); + } + } +} + +function generateFileName(fileName): string { + return fileName.replace('_', '') + .replace('-', '') + .replace(/^\s+|\s+/g, '') + .toLowerCase() + '.json'; +} + +export function generateLogConfigFile(fileZipAdd: object, logsLevel: string, logsPath: string): void { + fileZipAdd['logs.conf'] = getLogsConfig(logsLevel, logsPath); +} + +function getLogsConfig(logsLevel: string, logsPath: string): string { + return TEMPLATE_LOGS_CONFIG + .replace(/{ERROR}/g, logsLevel) + .replace(/{.\/logs\/}/g, logsPath); +} + +export function getEntityId(gatewayId: string): EntityId { + return { + id: gatewayId, + entityType: EntityType.DEVICE + } +} + +export function createFormConfig(keyValue: GatewayMainSetting): GatewayFormModels { + const formSetting: GatewayFormModels = {}; + if (Object.prototype.hasOwnProperty.call(keyValue, 'thingsboard')) { + formSetting.host = keyValue.thingsboard.host; + formSetting.port = keyValue.thingsboard.port; + formSetting.remoteConfiguration = keyValue.thingsboard.remoteConfiguration; + if (Object.prototype.hasOwnProperty.call(keyValue.thingsboard.security, SecurityType.accessToken)) { + formSetting.securityType = SecurityType.accessToken; + formSetting.accessToken = (keyValue.thingsboard.security as SecurityToken).accessToken; + } else { + formSetting.securityType = SecurityType.tls; + formSetting.caCertPath = (keyValue.thingsboard.security as SecurityCertificate).caCert; + formSetting.privateKeyPath = (keyValue.thingsboard.security as SecurityCertificate).privateKey; + formSetting.certPath = (keyValue.thingsboard.security as SecurityCertificate).cert; + } + } + + if (Object.prototype.hasOwnProperty.call(keyValue, 'storage') && Object.prototype.hasOwnProperty.call(keyValue.storage, 'type')) { + if (keyValue.storage.type === StorageType.memory) { + formSetting.storageType = StorageType.memory; + formSetting.readRecordsCount = (keyValue.storage as GatewayStorageMemory).read_records_count; + formSetting.maxRecordsCount = (keyValue.storage as GatewayStorageMemory).max_records_count; + } else if (keyValue.storage.type === StorageType.file) { + formSetting.storageType = StorageType.file; + formSetting.dataFolderPath = (keyValue.storage as GatewayStorageFile).data_folder_path; + formSetting.maxFilesCount = (keyValue.storage as GatewayStorageFile).max_file_count; + formSetting.readRecordsCount = (keyValue.storage as GatewayStorageMemory).read_records_count; + formSetting.maxRecordsCount = (keyValue.storage as GatewayStorageMemory).max_records_count; + } + } + return formSetting; +} + +export function getDraftConnectorsJSON(currentConnectors: Array) { + const draftConnectors = {}; + for(const connector of currentConnectors){ + if (!connector.enabled) { + draftConnectors[connector.name] = { + connector: connector.configType, + config: connector.config + }; + } + } + return draftConnectors; +} + +export function gatewayConfigJSON(gatewayConfiguration: GatewayFormModels): GatewaySetting { + const gatewayConfig = { + thingsboard: gatewayMainConfigJSON(gatewayConfiguration) + }; + gatewayConnectorJSON(gatewayConfig, gatewayConfiguration.connectors); + return gatewayConfig; +} + +function gatewayMainConfigJSON(gatewayConfiguration: GatewayFormModels): GatewayMainSetting { + let security: GatewaySecurity; + if (gatewayConfiguration.securityType === SecurityType.accessToken) { + security = { + accessToken: gatewayConfiguration.accessToken + } + } else { + security = { + caCert: gatewayConfiguration.caCertPath, + privateKey: gatewayConfiguration.privateKeyPath, + cert: gatewayConfiguration.certPath + } + } + const thingsboard: GatewayMainThingsboardSetting = { + host: gatewayConfiguration.host, + remoteConfiguration: gatewayConfiguration.remoteConfiguration, + port: gatewayConfiguration.port, + security + }; + + let storage: GatewayStorage; + if (gatewayConfiguration.storageType === StorageType.memory) { + storage = { + type: StorageType.memory, + read_records_count: gatewayConfiguration.readRecordsCount, + max_records_count: gatewayConfiguration.maxRecordsCount + }; + } else { + storage = { + type: StorageType.file, + data_folder_path: gatewayConfiguration.dataFolderPath, + max_file_count: gatewayConfiguration.maxFilesCount, + max_read_records_count: gatewayConfiguration.readRecordsCount, + max_records_per_file: gatewayConfiguration.maxRecordsCount + }; + } + + const connectors: Array = []; + for (const connector of gatewayConfiguration.connectors) { + if (connector.enabled) { + const connectorConfig: GatewayMainConnector = { + configuration: generateFileName(connector.name), + name: connector.name, + type: connector.configType + }; + connectors.push(connectorConfig); + } + } + + return { + thingsboard, + connectors, + storage, + logs: window.btoa(getLogsConfig(gatewayConfiguration.remoteLoggingLevel, gatewayConfiguration.remoteLoggingPathToLogs)) + } +} + +function gatewayConnectorJSON(gatewayConfiguration, currentConnectors: Array): void { + for (const connector of currentConnectors) { + if (connector.enabled) { + const typeConnector = connector.configType; + if (!Array.isArray(gatewayConfiguration[typeConnector])) { + gatewayConfiguration[typeConnector] = []; + } + + const connectorConfig: ConnectorConfig = { + name: connector.name, + config: connector.config + }; + gatewayConfiguration[typeConnector].push(connectorConfig); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts new file mode 100644 index 0000000000..5d6f39850e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts @@ -0,0 +1,744 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import L, { + FeatureGroup, + Icon, + LatLngBounds, + LatLngTuple, + markerClusterGroup, + MarkerClusterGroup, + MarkerClusterGroupOptions +} from 'leaflet'; +import tinycolor from 'tinycolor2'; +import 'leaflet-providers'; +import 'leaflet.markercluster/dist/leaflet.markercluster'; + +import { + defaultSettings, + FormattedData, + MapProviders, + MapSettings, + MarkerSettings, + PolygonSettings, + PolylineSettings, + ReplaceInfo, + UnitedMapSettings +} from './map-models'; +import { Marker } from './markers'; +import { Observable, of } from 'rxjs'; +import { Polyline } from './polyline'; +import { Polygon } from './polygon'; +import { + createLoadingDiv, + createTooltip, + parseArray, + parseData, + safeExecute +} from '@home/components/widget/lib/maps/maps-utils'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { deepClone, isDefinedAndNotNull, isEmptyStr, isString } from '@core/utils'; + +export default abstract class LeafletMap { + + markers: Map = new Map(); + polylines: Map = new Map(); + polygons: Map = new Map(); + map: L.Map; + options: UnitedMapSettings; + bounds: L.LatLngBounds; + datasources: FormattedData[]; + markersCluster: MarkerClusterGroup; + points: FeatureGroup; + markersData: FormattedData[] = []; + polygonsData: FormattedData[] = []; + defaultMarkerIconInfo: { size: number[], icon: Icon }; + loadingDiv: JQuery; + loading = false; + replaceInfoLabelMarker: Array = []; + markerLabelText: string; + replaceInfoTooltipMarker: Array = []; + markerTooltipText: string; + drawRoutes: boolean; + showPolygon: boolean; + updatePending = false; + addMarkers: L.Marker[] = []; + addPolygons: L.Polygon[] = []; + + protected constructor(public ctx: WidgetContext, + public $container: HTMLElement, + options: UnitedMapSettings) { + this.options = options; + } + + public initSettings(options: MapSettings) { + this.options.tinyColor = tinycolor(this.options.color || defaultSettings.color); + const { useClusterMarkers, + zoomOnClick, + showCoverageOnHover, + removeOutsideVisibleBounds, + animate, + chunkedLoading, + maxClusterRadius, + maxZoom }: MapSettings = options; + if (useClusterMarkers) { + const clusteringSettings: MarkerClusterGroupOptions = { + zoomToBoundsOnClick: zoomOnClick, + showCoverageOnHover, + removeOutsideVisibleBounds, + animate, + chunkedLoading + }; + if (maxClusterRadius && maxClusterRadius > 0) { + clusteringSettings.maxClusterRadius = Math.floor(maxClusterRadius); + } + if (maxZoom && maxZoom >= 0 && maxZoom < 19) { + clusteringSettings.disableClusteringAtZoom = Math.floor(maxZoom); + } + this.markersCluster = markerClusterGroup(clusteringSettings); + } + } + + addMarkerControl() { + if (this.options.draggableMarker) { + let mousePositionOnMap: L.LatLng; + let addMarker: L.Control; + this.map.on('mousemove', (e: L.LeafletMouseEvent) => { + mousePositionOnMap = e.latlng; + }); + const dragListener = (e: L.DragEndEvent) => { + if (e.type === 'dragend' && mousePositionOnMap) { + const icon = L.icon({ + iconRetinaUrl: 'marker-icon-2x.png', + iconUrl: 'marker-icon.png', + shadowUrl: 'marker-shadow.png', + iconSize: [25, 41], + iconAnchor: [12, 41], + popupAnchor: [1, -34], + tooltipAnchor: [16, -28], + shadowSize: [41, 41] + }); + const newMarker = L.marker(mousePositionOnMap, { icon }).addTo(this.map); + this.addMarkers.push(newMarker); + const datasourcesList = document.createElement('div'); + const customLatLng = this.convertToCustomFormat(mousePositionOnMap); + const header = document.createElement('p'); + header.appendChild(document.createTextNode('Select entity:')); + header.setAttribute('style', 'font-size: 14px; margin: 8px 0'); + datasourcesList.append(header); + this.datasources.forEach(ds => { + const dsItem = document.createElement('p'); + dsItem.appendChild(document.createTextNode(ds.entityName)); + dsItem.setAttribute('style', 'font-size: 14px; margin: 8px 0; cursor: pointer'); + dsItem.onclick = () => { + const updatedEnttity = { ...ds, ...customLatLng }; + this.saveMarkerLocation(updatedEnttity).subscribe(() => { + this.map.removeLayer(newMarker); + const markerIndex = this.addMarkers.indexOf(newMarker); + if (markerIndex > -1) { + this.addMarkers.splice(markerIndex, 1); + } + this.deleteMarker(ds.entityName); + this.createMarker(ds.entityName, updatedEnttity, this.datasources, this.options); + }); + }; + datasourcesList.append(dsItem); + }); + datasourcesList.append(document.createElement('br')); + const deleteBtn = document.createElement('a'); + deleteBtn.appendChild(document.createTextNode('Discard changes')); + deleteBtn.onclick = () => { + this.map.removeLayer(newMarker); + const markerIndex = this.addMarkers.indexOf(newMarker); + if (markerIndex > -1) { + this.addMarkers.splice(markerIndex, 1); + } + }; + datasourcesList.append(deleteBtn); + const popup = L.popup(); + popup.setContent(datasourcesList); + newMarker.bindPopup(popup).openPopup(); + } + addMarker.setPosition('topright'); + }; + L.Control.AddMarker = L.Control.extend({ + onAdd() { + const img = L.DomUtil.create('img') as any; + img.src = `assets/add_location.svg`; + img.style.width = '32px'; + img.style.height = '32px'; + img.title = 'Drag and drop to add marker'; + img.onclick = this.dragMarker; + img.draggable = true; + const draggableImg = new L.Draggable(img); + draggableImg.enable(); + draggableImg.on('dragend', dragListener); + return img; + }, + onRemove() { + }, + dragMarker: this.dragMarker + } as any); + L.control.addMarker = (opts) => { + return new L.Control.AddMarker(opts); + }; + addMarker = L.control.addMarker({ position: 'topright' }).addTo(this.map); + } + } + + addPolygonControl() { + if (this.options.showPolygon && this.options.editablePolygon) { + let mousePositionOnMap: L.LatLng[]; + let addPolygon: L.Control; + this.map.on('mousemove', (e: L.LeafletMouseEvent) => { + const polygonOffset = this.options.provider === MapProviders.image ? 10 : 0.01; + const latlng1 = e.latlng; + const latlng2 = L.latLng(e.latlng.lat, e.latlng.lng + polygonOffset); + const latlng3 = L.latLng(e.latlng.lat - polygonOffset, e.latlng.lng); + mousePositionOnMap = [latlng1, latlng2, latlng3]; + }); + const dragListener = (e: L.DragEndEvent) => { + if (e.type === 'dragend' && mousePositionOnMap) { + const newPolygon = L.polygon(mousePositionOnMap).addTo(this.map); + this.addPolygons.push(newPolygon); + const datasourcesList = document.createElement('div'); + const customLatLng = {[this.options.polygonKeyName]: this.convertToPolygonFormat(mousePositionOnMap)}; + const header = document.createElement('p'); + header.appendChild(document.createTextNode('Select entity:')); + header.setAttribute('style', 'font-size: 14px; margin: 8px 0'); + datasourcesList.append(header); + this.datasources.forEach(ds => { + const dsItem = document.createElement('p'); + dsItem.appendChild(document.createTextNode(ds.entityName)); + dsItem.setAttribute('style', 'font-size: 14px; margin: 8px 0; cursor: pointer'); + dsItem.onclick = () => { + const updatedEnttity = { ...ds, ...customLatLng }; + this.savePolygonLocation(updatedEnttity).subscribe(() => { + this.map.removeLayer(newPolygon); + const polygonIndex = this.addPolygons.indexOf(newPolygon); + if (polygonIndex > -1) { + this.addPolygons.splice(polygonIndex, 1); + } + this.deletePolygon(ds.entityName); + }); + }; + datasourcesList.append(dsItem); + }); + datasourcesList.append(document.createElement('br')); + const deleteBtn = document.createElement('a'); + deleteBtn.appendChild(document.createTextNode('Discard changes')); + deleteBtn.onclick = () => { + this.map.removeLayer(newPolygon); + const polygonIndex = this.addPolygons.indexOf(newPolygon); + if (polygonIndex > -1) { + this.addPolygons.splice(polygonIndex, 1); + } + }; + datasourcesList.append(deleteBtn); + const popup = L.popup(); + popup.setContent(datasourcesList); + newPolygon.bindPopup(popup).openPopup(); + } + addPolygon.setPosition('topright'); + }; + L.Control.AddPolygon = L.Control.extend({ + onAdd() { + const img = L.DomUtil.create('img') as any; + img.src = `assets/add_polygon.svg`; + img.style.width = '32px'; + img.style.height = '32px'; + img.title = 'Drag and drop to add Polygon'; + img.onclick = this.dragPolygonVertex; + img.draggable = true; + const draggableImg = new L.Draggable(img); + draggableImg.enable(); + draggableImg.on('dragend', dragListener); + return img; + }, + onRemove() { + }, + dragPolygonVertex: this.dragPolygonVertex + } as any); + L.control.addPolygon = (opts) => { + return new L.Control.AddPolygon(opts); + }; + addPolygon = L.control.addPolygon({ position: 'topright' }).addTo(this.map); + } + } + + public setLoading(loading: boolean) { + if (this.loading !== loading) { + this.loading = loading; + if (this.loading) { + if (!this.loadingDiv) { + this.loadingDiv = createLoadingDiv(this.ctx.translate.instant('common.loading')); + } + this.$container.append(this.loadingDiv[0]); + } else { + if (this.loadingDiv) { + this.loadingDiv.remove(); + } + } + } + } + + public setMap(map: L.Map) { + this.map = map; + if (this.options.useDefaultCenterPosition) { + this.map.panTo(this.options.defaultCenterPosition); + this.bounds = map.getBounds(); + } else { + this.bounds = new L.LatLngBounds(null, null); + } + if (this.options.disableScrollZooming) { + this.map.scrollWheelZoom.disable(); + } + if (this.options.draggableMarker) { + this.addMarkerControl(); + } + if (this.options.editablePolygon) { + this.addPolygonControl(); + } + if (this.options.useClusterMarkers) { + this.map.addLayer(this.markersCluster); + } + if (this.updatePending) { + this.updatePending = false; + this.updateData(this.drawRoutes, this.showPolygon); + } + } + + public saveMarkerLocation(datasource: FormattedData, lat?: number, lng?: number): Observable { + return of(null); + } + + public savePolygonLocation(datasource: FormattedData, coordinates?: Array<[number, number]>): Observable { + return of(null); + } + + createLatLng(lat: number, lng: number): L.LatLng { + return L.latLng(lat, lng); + } + + createBounds(): L.LatLngBounds { + return this.map.getBounds(); + } + + extendBounds(bounds: L.LatLngBounds, polyline: L.Polyline) { + if (polyline && polyline.getLatLngs() && polyline.getBounds()) { + bounds.extend(polyline.getBounds()); + } + } + + invalidateSize() { + this.map?.invalidateSize(true); + } + + onResize() { + + } + + getCenter() { + return this.map.getCenter(); + } + + fitBounds(bounds: LatLngBounds, padding?: LatLngTuple) { + if (bounds.isValid()) { + this.bounds = !!this.bounds ? this.bounds.extend(bounds) : bounds; + if (!this.options.fitMapBounds && this.options.defaultZoomLevel) { + this.map.setZoom(this.options.defaultZoomLevel, { animate: false }); + if (this.options.useDefaultCenterPosition) { + this.map.panTo(this.options.defaultCenterPosition, { animate: false }); + } + else { + this.map.panTo(this.bounds.getCenter()); + } + } else { + this.map.once('zoomend', () => { + let minZoom = this.options.minZoomLevel; + if (this.options.defaultZoomLevel) { + minZoom = Math.max(minZoom, this.options.defaultZoomLevel); + } + if (this.map.getZoom() > minZoom) { + this.map.setZoom(minZoom, { animate: false }); + } + }); + if (this.options.useDefaultCenterPosition) { + this.bounds = this.bounds.extend(this.options.defaultCenterPosition); + } + this.map.fitBounds(this.bounds, { padding: padding || [50, 50], animate: false }); + this.map.invalidateSize(); + } + } + } + + convertPosition(expression: object): L.LatLng { + if (!expression) { + return null; + } + const lat = expression[this.options.latKeyName]; + const lng = expression[this.options.lngKeyName]; + if (!isDefinedAndNotNull(lat) || isString(lat) || isNaN(lat) || !isDefinedAndNotNull(lng) || isString(lng) || isNaN(lng)) { + return null; + } + return L.latLng(lat, lng) as L.LatLng; + } + + convertPositionPolygon(expression: (LatLngTuple | LatLngTuple[] | LatLngTuple[][])[]) { + return (expression).map((el) => { + if (!Array.isArray(el[0]) && el.length === 2) { + return el; + } else if (Array.isArray(el) && el.length) { + return this.convertPositionPolygon(el as LatLngTuple[] | LatLngTuple[][]); + } else { + return null; + } + }).filter(el => !!el); + } + + convertToCustomFormat(position: L.LatLng): object { + return { + [this.options.latKeyName]: position.lat % 90, + [this.options.lngKeyName]: position.lng % 180 + }; + } + + convertToPolygonFormat(points: Array): Array { + if (points.length) { + return points.map(point => { + if (point.length) { + return this.convertToPolygonFormat(point); + } else { + return [point.lat, point.lng]; + } + }); + } else { + return []; + } + } + + convertPolygonToCustomFormat(expression: any[][]): object { + return { + [this.options.polygonKeyName] : this.convertToPolygonFormat(expression) + }; + } + + updateData(drawRoutes: boolean, showPolygon: boolean) { + this.drawRoutes = drawRoutes; + this.showPolygon = showPolygon; + if (this.map) { + const data = this.ctx.data; + const formattedData = parseData(this.ctx.data); + if (drawRoutes) { + this.updatePolylines(parseArray(data), false); + } + if (showPolygon) { + this.updatePolygons(formattedData, false); + } + this.updateMarkers(formattedData, false); + this.updateBoundsInternal(); + if (this.options.draggableMarker || this.options.editablePolygon) { + this.datasources = formattedData; + } + } else { + this.updatePending = true; + } + } + + private updateBoundsInternal() { + const bounds = new L.LatLngBounds(null, null); + if (this.drawRoutes) { + this.polylines.forEach((polyline) => { + bounds.extend(polyline.leafletPoly.getBounds()); + }); + } + if (this.showPolygon) { + this.polygons.forEach((polygon) => { + bounds.extend(polygon.leafletPoly.getBounds()); + }); + } + if ((this.options as MarkerSettings).useClusterMarkers && this.markersCluster.getBounds().isValid()) { + bounds.extend(this.markersCluster.getBounds()); + } else { + this.markers.forEach((marker) => { + bounds.extend(marker.leafletMarker.getLatLng()); + }); + } + + const mapBounds = this.map.getBounds(); + if (bounds.isValid() && (!this.bounds || !this.bounds.isValid() || !this.bounds.equals(bounds) && !mapBounds.contains(bounds))) { + this.bounds = bounds; + this.fitBounds(bounds); + } + } + + // Markers + updateMarkers(markersData: FormattedData[], updateBounds = true, callback?) { + const rawMarkers = markersData.filter(mdata => !!this.convertPosition(mdata)); + const toDelete = new Set(Array.from(this.markers.keys())); + const createdMarkers: Marker[] = []; + const updatedMarkers: Marker[] = []; + const deletedMarkers: Marker[] = []; + let m: Marker; + rawMarkers.forEach(data => { + if (data.rotationAngle || data.rotationAngle === 0) { + const currentImage = this.options.useMarkerImageFunction ? + safeExecute(this.options.markerImageFunction, + [data, this.options.markerImages, markersData, data.dsIndex]) : this.options.currentImage; + const style = currentImage ? 'background-image: url(' + currentImage.url + ');' : ''; + this.options.icon = L.divIcon({ + html: `
` + }); + } else { + this.options.icon = null; + } + if (this.markers.get(data.entityName)) { + m = this.updateMarker(data.entityName, data, markersData, this.options); + if (m) { + updatedMarkers.push(m); + } + } else { + m = this.createMarker(data.entityName, data, markersData, this.options as MarkerSettings, updateBounds, callback); + if (m) { + createdMarkers.push(m); + } + } + toDelete.delete(data.entityName); + }); + toDelete.forEach((key) => { + m = this.deleteMarker(key); + if (m) { + deletedMarkers.push(m); + } + }); + this.markersData = markersData; + if ((this.options as MarkerSettings).useClusterMarkers) { + if (createdMarkers.length) { + this.markersCluster.addLayers(createdMarkers.map(marker => marker.leafletMarker)); + } + if (updatedMarkers.length) { + this.markersCluster.refreshClusters(updatedMarkers.map(marker => marker.leafletMarker)); + } + if (deletedMarkers.length) { + this.markersCluster.removeLayers(deletedMarkers.map(marker => marker.leafletMarker)); + } + } + } + + dragMarker = (e, data = {} as FormattedData) => { + if (e.type !== 'dragend') { + return; + } + this.saveMarkerLocation({ ...data, ...this.convertToCustomFormat(e.target._latlng) }).subscribe(); + } + + private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings, + updateBounds = true, callback?): Marker { + const newMarker = new Marker(this, this.convertPosition(data), settings, data, dataSources, this.dragMarker); + if (callback) { + newMarker.leafletMarker.on('click', () => { + callback(data, true); + }); + } + if (this.bounds && updateBounds && !(this.options as MarkerSettings).useClusterMarkers) { + this.fitBounds(this.bounds.extend(newMarker.leafletMarker.getLatLng())); + } + this.markers.set(key, newMarker); + if (!this.options.useClusterMarkers) { + this.map.addLayer(newMarker.leafletMarker); + } + return newMarker; + } + + private updateMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings): Marker { + const marker: Marker = this.markers.get(key); + const location = this.convertPosition(data); + marker.updateMarkerPosition(location); + if (settings.showTooltip) { + marker.updateMarkerTooltip(data); + } + marker.setDataSources(data, dataSources); + marker.updateMarkerIcon(settings); + return marker; + } + + deleteMarker(key: string): Marker { + const marker = this.markers.get(key); + const leafletMarker = marker?.leafletMarker; + if (leafletMarker) { + if (!this.options.useClusterMarkers) { + this.map.removeLayer(leafletMarker); + } + this.markers.delete(key); + } + return marker; + } + + deletePolygon(key: string) { + const polygon = this.polygons.get(key)?.leafletPoly; + if (polygon) { + this.map.removeLayer(polygon); + this.polygons.delete(key); + } + return polygon; + } + + updatePoints(pointsData: FormattedData[], getTooltip: (point: FormattedData, setTooltip?: boolean) => string) { + if (this.points) { + this.map.removeLayer(this.points); + } + this.points = new FeatureGroup(); + pointsData.filter(pdata => !!this.convertPosition(pdata)).forEach(data => { + const point = L.circleMarker(this.convertPosition(data), { + color: this.options.pointColor, + radius: this.options.pointSize + }); + if (!this.options.pointTooltipOnRightPanel) { + point.on('click', () => getTooltip(data)); + } + else { + createTooltip(point, this.options, data.$datasource, getTooltip(data, false)); + } + this.points.addLayer(point); + }); + this.map.addLayer(this.points); + } + + // Polyline + + updatePolylines(polyData: FormattedData[][], updateBounds = true, activePolyline?: FormattedData) { + const keys: string[] = []; + polyData.forEach((dataSource: FormattedData[]) => { + const data = activePolyline || dataSource[0]; + if (dataSource.length && data.entityName === dataSource[0].entityName) { + if (this.polylines.get(data.entityName)) { + this.updatePolyline(data, dataSource, this.options, updateBounds); + } else { + this.createPolyline(data, dataSource, this.options, updateBounds); + } + keys.push(data.entityName); + } + }); + const toDelete: string[] = []; + this.polylines.forEach((v, mKey) => { + if (!keys.includes(mKey)) { + toDelete.push(mKey); + } + }); + toDelete.forEach((key) => { + this.removePolyline(key); + }); + } + + createPolyline(data: FormattedData, dataSources: FormattedData[], settings: PolylineSettings, updateBounds = true) { + const poly = new Polyline(this.map, + dataSources.map(el => this.convertPosition(el)).filter(el => !!el), data, dataSources, settings); + if (updateBounds) { + const bounds = poly.leafletPoly.getBounds(); + this.fitBounds(bounds); + } + this.polylines.set(data.entityName, poly); + } + + updatePolyline(data: FormattedData, dataSources: FormattedData[], settings: PolylineSettings, updateBounds = true) { + const poly = this.polylines.get(data.entityName); + const oldBounds = poly.leafletPoly.getBounds(); + poly.updatePolyline(dataSources.map(el => this.convertPosition(el)).filter(el => !!el), data, dataSources, settings); + const newBounds = poly.leafletPoly.getBounds(); + if (updateBounds && oldBounds.toBBoxString() !== newBounds.toBBoxString()) { + this.fitBounds(newBounds); + } + } + + removePolyline(name: string) { + const poly = this.polylines.get(name); + if (poly) { + this.map.removeLayer(poly.leafletPoly); + if (poly.polylineDecorator) { + this.map.removeLayer(poly.polylineDecorator); + } + this.polylines.delete(name); + } + } + + // Polygon + + updatePolygons(polyData: FormattedData[], updateBounds = true) { + const keys: string[] = []; + this.polygonsData = deepClone(polyData); + polyData.forEach((data: FormattedData) => { + if (data && isDefinedAndNotNull(data[this.options.polygonKeyName]) && !isEmptyStr(data[this.options.polygonKeyName])) { + if (isString((data[this.options.polygonKeyName]))) { + data[this.options.polygonKeyName] = JSON.parse(data[this.options.polygonKeyName]); + } + data[this.options.polygonKeyName] = this.convertPositionPolygon(data[this.options.polygonKeyName]); + + if (this.polygons.get(data.entityName)) { + this.updatePolygon(data, polyData, this.options, updateBounds); + } else { + this.createPolygon(data, polyData, this.options, updateBounds); + } + keys.push(data.entityName); + } + }); + const toDelete: string[] = []; + this.polygons.forEach((v, mKey) => { + if (!keys.includes(mKey)) { + toDelete.push(mKey); + } + }); + toDelete.forEach((key) => { + this.removePolygon(key); + }); + } + + dragPolygonVertex = (e?, data = {} as FormattedData) => { + if (e === undefined || (e.type !== 'editable:vertex:dragend' && e.type !== 'editable:vertex:deleted')) { + return; + } + this.savePolygonLocation({ ...data, ...this.convertPolygonToCustomFormat(e.layer._latlngs) }).subscribe(); + } + + createPolygon(polyData: FormattedData, dataSources: FormattedData[], settings: PolygonSettings, updateBounds = true) { + const polygon = new Polygon(this.map, polyData, dataSources, settings, this.dragPolygonVertex); + if (updateBounds) { + const bounds = polygon.leafletPoly.getBounds(); + this.fitBounds(bounds); + } + this.polygons.set(polyData.entityName, polygon); + } + + updatePolygon(polyData: FormattedData, dataSources: FormattedData[], settings: PolygonSettings, updateBounds = true) { + const poly = this.polygons.get(polyData.entityName); + const oldBounds = poly.leafletPoly.getBounds(); + poly.updatePolygon(polyData, dataSources, settings); + const newBounds = poly.leafletPoly.getBounds(); + if (updateBounds && oldBounds.toBBoxString() !== newBounds.toBBoxString()) { + this.fitBounds(newBounds); + } + } + + removePolygon(name: string) { + const poly = this.polygons.get(name); + if (poly) { + this.map.removeLayer(poly.leafletPoly); + this.polygons.delete(name); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts new file mode 100644 index 0000000000..63fe097b98 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts @@ -0,0 +1,248 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { LatLngTuple } from 'leaflet'; +import { Datasource } from '@app/shared/models/widget.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import tinycolor from 'tinycolor2'; + +export const DEFAULT_MAP_PAGE_SIZE = 16384; + +export type GenericFunction = (data: FormattedData, dsData: FormattedData[], dsIndex: number) => string; +export type MarkerImageFunction = (data: FormattedData, dsData: FormattedData[], dsIndex: number) => string; +export type PosFuncton = (origXPos, origYPos) => { x, y }; + +export type MapSettings = { + draggableMarker: boolean; + editablePolygon: boolean; + posFunction: PosFuncton; + defaultZoomLevel?: number; + disableScrollZooming?: boolean; + minZoomLevel?: number; + useClusterMarkers: boolean; + latKeyName?: string; + lngKeyName?: string; + xPosKeyName?: string; + yPosKeyName?: string; + imageEntityAlias: string; + imageUrlAttribute: string; + mapProvider: MapProviders; + mapProviderHere: string; + mapUrl?: string; + mapImageUrl?: string; + provider?: MapProviders; + credentials?: any; // declare credentials format + gmApiKey?: string; + defaultCenterPosition?: LatLngTuple; + markerClusteringSetting?; + useDefaultCenterPosition?: boolean; + gmDefaultMapType?: string; + useLabelFunction: boolean; + icon?: any; + zoomOnClick: boolean, + maxZoom: number, + showCoverageOnHover: boolean, + animate: boolean, + maxClusterRadius: number, + chunkedLoading: boolean, + removeOutsideVisibleBounds: boolean, + useCustomProvider: boolean, + customProviderTileUrl: string; + mapPageSize: number; +}; + +export enum MapProviders { + google = 'google-map', + openstreet = 'openstreet-map', + here = 'here', + image = 'image-map', + tencent = 'tencent-map' +} + +export type MarkerSettings = { + tooltipPattern?: any; + tooltipAction: { [name: string]: actionsHandler }; + icon?: any; + showLabel?: boolean; + label: string; + labelColor: string; + labelText: string; + useLabelFunction: boolean; + draggableMarker: boolean; + showTooltip?: boolean; + useTooltipFunction: boolean; + useColorFunction: boolean; + color?: string; + tinyColor?: tinycolor.Instance; + autocloseTooltip: boolean; + showTooltipAction: string; + useClusterMarkers: boolean; + currentImage?: string; + useMarkerImageFunction?: boolean; + markerImages?: string[]; + markerImageSize: number; + fitMapBounds: boolean; + markerImage: string; + markerClick: { [name: string]: actionsHandler }; + colorFunction: GenericFunction; + tooltipFunction: GenericFunction; + labelFunction: GenericFunction; + markerImageFunction?: MarkerImageFunction; + markerOffsetX: number; + markerOffsetY: number; +}; + +export interface FormattedData { + $datasource: Datasource; + entityName: string; + entityId: string; + entityType: EntityType; + dsIndex: number; + deviceType: string; + [key: string]: any; +} + +export interface ReplaceInfo { + variable: string; + valDec?: number; + dataKeyName: string; +} + +export type PolygonSettings = { + showPolygon: boolean; + polygonKeyName: string; + polKeyName: string; // deprecated + polygonStrokeOpacity: number; + polygonOpacity: number; + polygonStrokeWeight: number; + polygonStrokeColor: string; + polygonColor: string; + showPolygonTooltip: boolean; + autocloseTooltip: boolean; + showTooltipAction: string; + tooltipAction: { [name: string]: actionsHandler }; + polygonTooltipPattern: string; + usePolygonTooltipFunction: boolean; + polygonClick: { [name: string]: actionsHandler }; + usePolygonColorFunction: boolean; + polygonTooltipFunction: GenericFunction; + polygonColorFunction?: GenericFunction; + editablePolygon: boolean; +}; + +export type PolylineSettings = { + usePolylineDecorator: any; + autocloseTooltip: boolean; + showTooltipAction: string; + useColorFunction: any; + tooltipAction: { [name: string]: actionsHandler }; + color: string; + useStrokeOpacityFunction: any; + strokeOpacity: number; + useStrokeWeightFunction: any; + strokeWeight: number; + decoratorOffset: string | number; + endDecoratorOffset: string | number; + decoratorRepeat: string | number; + decoratorSymbol: any; + decoratorSymbolSize: any; + useDecoratorCustomColor: any; + decoratorCustomColor: any; + + + colorFunction: GenericFunction; + strokeOpacityFunction: GenericFunction; + strokeWeightFunction: GenericFunction; +}; + +export interface HistorySelectSettings { + buttonColor: string; +} + +export interface MapImage { + imageUrl: string; + aspect: number; + update?: boolean; +} + +export type TripAnimationSettings = { + showPoints: boolean; + pointColor: string; + pointSize: number; + pointTooltipOnRightPanel: boolean; + usePointAsAnchor: boolean; + normalizationStep: number; + showPolygon: boolean; + latKeyName: string; + lngKeyName: string; + rotationAngle: number; + label: string; + tooltipPattern: string; + useTooltipFunction: boolean; + useLabelFunction: boolean; + pointAsAnchorFunction: GenericFunction; + tooltipFunction: GenericFunction; + labelFunction: GenericFunction; +}; + +export type actionsHandler = ($event: Event, datasource: Datasource) => void; + +export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolylineSettings & TripAnimationSettings; + +export const defaultSettings: any = { + xPosKeyName: 'xPos', + yPosKeyName: 'yPos', + markerOffsetX: 0.5, + markerOffsetY: 1, + latKeyName: 'latitude', + lngKeyName: 'longitude', + polygonKeyName: 'coordinates', + showLabel: false, + label: '${entityName}', + showTooltip: false, + useDefaultCenterPosition: false, + showTooltipAction: 'click', + autocloseTooltip: false, + showPolygon: false, + labelColor: '#000000', + color: '#FE7569', + polygonColor: '#0000ff', + polygonStrokeColor: '#fe0001', + polygonOpacity: 0.5, + polygonStrokeOpacity: 1, + polygonStrokeWeight: 1, + useLabelFunction: false, + markerImages: [], + strokeWeight: 2, + strokeOpacity: 1.0, + initCallback: () => { }, + defaultZoomLevel: 8, + disableScrollZooming: false, + minZoomLevel: 16, + credentials: '', + markerClusteringSetting: null, + draggableMarker: false, + editablePolygon: false, + fitMapBounds: true, + mapPageSize: DEFAULT_MAP_PAGE_SIZE +}; + +export const hereProviders = [ + 'HERE.normalDay', + 'HERE.normalNight', + 'HERE.hybridDay', + 'HERE.terrainDay' +]; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.interface.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.interface.ts new file mode 100644 index 0000000000..d321e04953 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.interface.ts @@ -0,0 +1,32 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { JsonSettingsSchema } from '@shared/models/widget.models'; +import { MapProviders } from './map-models'; + +export interface MapWidgetInterface { + resize(); + update(); + onInit(); + onDestroy(); +} + +export interface MapWidgetStaticInterface { + settingsSchema(mapProvider?: MapProviders, drawRoutes?: boolean): JsonSettingsSchema; + getProvidersSchema(mapProvider?: MapProviders, ignoreImageMap?: boolean): JsonSettingsSchema; + dataKeySettingsSchema(): object; + actionSources(): object; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts new file mode 100644 index 0000000000..950e73e96d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts @@ -0,0 +1,350 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + defaultSettings, + FormattedData, + hereProviders, + MapProviders, + UnitedMapSettings +} from './map-models'; +import LeafletMap from './leaflet-map'; +import { + commonMapSettingsSchema, + mapPolygonSchema, + mapProviderSchema, + markerClusteringSettingsSchema, + markerClusteringSettingsSchemaLeaflet, + routeMapSettingsSchema +} from './schemes'; +import { MapWidgetInterface, MapWidgetStaticInterface } from './map-widget.interface'; +import { addCondition, addGroupInfo, addToSchema, initSchema, mergeSchemes } from '@core/schema-utils'; +import { WidgetContext } from '@app/modules/home/models/widget-component.models'; +import { getDefCenterPosition, parseFunction, parseWithTranslation } from './maps-utils'; +import { Datasource, DatasourceData, JsonSettingsSchema, WidgetActionDescriptor } from '@shared/models/widget.models'; +import { EntityId } from '@shared/models/id/entity-id'; +import { AttributeScope, DataKeyType, LatestTelemetry } from '@shared/models/telemetry/telemetry.models'; +import { AttributeService } from '@core/http/attribute.service'; +import { TranslateService } from '@ngx-translate/core'; +import { UtilsService } from '@core/services/utils.service'; +import _ from 'lodash'; +import { EntityDataPageLink } from '@shared/models/query/query.models'; +import { isDefined } from '@core/utils'; +import { forkJoin, Observable, of } from 'rxjs'; +import { providerSets } from '@home/components/widget/lib/maps/providers'; + +// @dynamic +export class MapWidgetController implements MapWidgetInterface { + + constructor( + public mapProvider: MapProviders, + private drawRoutes: boolean, + public ctx: WidgetContext, + $element: HTMLElement, + isEdit?: boolean + ) { + if (this.map) { + this.map.map.remove(); + delete this.map; + } + + this.data = ctx.data; + if (!$element) { + $element = ctx.$container[0]; + } + this.settings = this.initSettings(ctx.settings, isEdit); + this.settings.tooltipAction = this.getDescriptors('tooltipAction'); + this.settings.markerClick = this.getDescriptors('markerClick'); + this.settings.polygonClick = this.getDescriptors('polygonClick'); + + const MapClass = providerSets[this.provider]?.MapClass; + if (!MapClass) { + return; + } + parseWithTranslation.setTranslate(this.translate); + this.map = new MapClass(this.ctx, $element, this.settings); + (this.ctx as any).mapInstance = this.map; + this.map.saveMarkerLocation = this.setMarkerLocation; + this.map.savePolygonLocation = this.savePolygonLocation; + this.pageLink = { + page: 0, + pageSize: this.settings.mapPageSize, + textSearch: null, + dynamic: true + }; + this.map.setLoading(true); + this.ctx.defaultSubscription.subscribeAllForPaginatedData(this.pageLink, null); + } + + map: LeafletMap; + provider: MapProviders; + schema: JsonSettingsSchema; + data: DatasourceData[]; + settings: UnitedMapSettings; + pageLink: EntityDataPageLink; + + public static dataKeySettingsSchema(): object { + return {}; + } + + public static getProvidersSchema(mapProvider: MapProviders, ignoreImageMap = false) { + const providerSchema = _.cloneDeep(mapProviderSchema); + if (mapProvider) { + providerSchema.schema.properties.provider.default = mapProvider; + } + if (ignoreImageMap) { + providerSchema.form[0].items = providerSchema.form[0]?.items.filter(item => item.value !== 'image-map'); + } + return mergeSchemes([providerSchema, + ...Object.keys(providerSets)?.map( + (key: string) => { + const setting = providerSets[key]; + return addCondition(setting?.schema, `model.provider === '${setting.name}'`); + })]); + } + + public static settingsSchema(mapProvider: MapProviders, drawRoutes: boolean): JsonSettingsSchema { + const schema = initSchema(); + addToSchema(schema, this.getProvidersSchema(mapProvider)); + addGroupInfo(schema, 'Map Provider Settings'); + addToSchema(schema, commonMapSettingsSchema); + addGroupInfo(schema, 'Common Map Settings'); + addToSchema(schema, addCondition(mapPolygonSchema, 'model.showPolygon === true', ['showPolygon'])); + addGroupInfo(schema, 'Polygon Settings'); + if (drawRoutes) { + addToSchema(schema, routeMapSettingsSchema); + addGroupInfo(schema, 'Route Map Settings'); + } else { + const clusteringSchema = mergeSchemes([markerClusteringSettingsSchema, + addCondition(markerClusteringSettingsSchemaLeaflet, + `model.useClusterMarkers === true && model.provider !== "image-map"`)]); + addToSchema(schema, clusteringSchema); + addGroupInfo(schema, 'Markers Clustering Settings'); + } + return schema; + } + + public static actionSources(): object { + return { + markerClick: { + name: 'widget-action.marker-click', + multiple: false + }, + polygonClick: { + name: 'widget-action.polygon-click', + multiple: false + }, + tooltipAction: { + name: 'widget-action.tooltip-tag-action', + multiple: true + } + }; + } + + translate = (key: string, defaultTranslation?: string): string => { + if (key) { + return (this.ctx.$injector.get(UtilsService).customTranslation(key, defaultTranslation || key) + || this.ctx.$injector.get(TranslateService).instant(key)); + } + return ''; + } + + getDescriptors(name: string): { [name: string]: ($event: Event, datasource: Datasource) => void } { + const descriptors = this.ctx.actionsApi.getActionDescriptors(name); + const actions = {}; + descriptors.forEach(descriptor => { + actions[descriptor.name] = ($event: Event, datasource: Datasource) => this.onCustomAction(descriptor, $event, datasource); + }, actions); + return actions; + } + + onInit() { + } + + private onCustomAction(descriptor: WidgetActionDescriptor, $event: Event, entityInfo: Datasource) { + if ($event) { + $event.preventDefault(); + $event.stopPropagation(); + } + const { entityId, entityName, entityLabel, entityType } = entityInfo; + this.ctx.actionsApi.handleWidgetAction($event, descriptor, { + entityType, + id: entityId + }, entityName, null, entityLabel); + } + + setMarkerLocation = (e: FormattedData, lat?: number, lng?: number) => { + const attributeService = this.ctx.$injector.get(AttributeService); + + const entityId: EntityId = { + entityType: e.$datasource.entityType, + id: e.$datasource.entityId + }; + const attributes = []; + const timeseries = []; + + const latProperties = [this.settings.latKeyName, this.settings.xPosKeyName]; + const lngProperties = [this.settings.lngKeyName, this.settings.yPosKeyName]; + e.$datasource.dataKeys.forEach(key => { + let value; + if (latProperties.includes(key.name)) { + value = { + key: key.name, + value: isDefined(lat) ? lat : e[key.name] + }; + } else if (lngProperties.includes(key.name)) { + value = { + key: key.name, + value: isDefined(lng) ? lng : e[key.name] + }; + } + if (value) { + if (key.type === DataKeyType.attribute) { + attributes.push(value); + } + if (key.type === DataKeyType.timeseries) { + timeseries.push(value); + } + } + }); + const observables: Observable[] = []; + if (timeseries.length) { + observables.push(attributeService.saveEntityTimeseries( + entityId, + LatestTelemetry.LATEST_TELEMETRY, + timeseries + )); + } + if (attributes.length) { + observables.push(attributeService.saveEntityAttributes( + entityId, + AttributeScope.SERVER_SCOPE, + attributes + )); + } + if (observables.length) { + return forkJoin(observables); + } else { + return of(null); + } + } + + savePolygonLocation = (e: FormattedData, coordinates?: Array) => { + const attributeService = this.ctx.$injector.get(AttributeService); + + const entityId: EntityId = { + entityType: e.$datasource.entityType, + id: e.$datasource.entityId + }; + const attributes = []; + const timeseries = []; + + const coordinatesProperties = this.settings.polygonKeyName; + e.$datasource.dataKeys.forEach(key => { + let value; + if (coordinatesProperties === key.name) { + value = { + key: key.name, + value: isDefined(coordinates) ? coordinates : e[key.name] + }; + } + if (value) { + if (key.type === DataKeyType.attribute) { + attributes.push(value); + } + if (key.type === DataKeyType.timeseries) { + timeseries.push(value); + } + } + }); + const observables: Observable[] = []; + if (timeseries.length) { + observables.push(attributeService.saveEntityTimeseries( + entityId, + LatestTelemetry.LATEST_TELEMETRY, + timeseries + )); + } + if (attributes.length) { + observables.push(attributeService.saveEntityAttributes( + entityId, + AttributeScope.SERVER_SCOPE, + attributes + )); + } + if (observables.length) { + return forkJoin(observables); + } else { + return of(null); + } + } + + initSettings(settings: UnitedMapSettings, isEditMap?: boolean): UnitedMapSettings { + const functionParams = ['data', 'dsData', 'dsIndex']; + this.provider = settings.provider || this.mapProvider; + if (this.provider === MapProviders.here && !settings.mapProviderHere) { + if (settings.mapProvider && hereProviders.includes(settings.mapProvider)) { + settings.mapProviderHere = settings.mapProvider; + } else { + settings.mapProviderHere = hereProviders[0]; + } + } + const customOptions = { + provider: this.provider, + mapUrl: settings?.mapImageUrl, + labelFunction: parseFunction(settings.labelFunction, functionParams), + tooltipFunction: parseFunction(settings.tooltipFunction, functionParams), + colorFunction: parseFunction(settings.colorFunction, functionParams), + polygonColorFunction: parseFunction(settings.polygonColorFunction, functionParams), + polygonTooltipFunction: parseFunction(settings.polygonTooltipFunction, functionParams), + markerImageFunction: parseFunction(settings.markerImageFunction, ['data', 'images', 'dsData', 'dsIndex']), + labelColor: this.ctx.widgetConfig.color, + polygonKeyName: settings.polKeyName ? settings.polKeyName : settings.polygonKeyName, + tooltipPattern: settings.tooltipPattern || + '${entityName}

Latitude: ${' + + settings.latKeyName + ':7}
Longitude: ${' + settings.lngKeyName + ':7}', + defaultCenterPosition: getDefCenterPosition(settings?.defaultCenterPosition), + currentImage: (settings.markerImage?.length) ? { + url: settings.markerImage, + size: settings.markerImageSize || 34 + } : null + }; + if (isEditMap && !settings.hasOwnProperty('draggableMarker')) { + settings.draggableMarker = true; + } + if (isEditMap && !settings.hasOwnProperty('editablePolygon')) { + settings.editablePolygon = true; + } + return { ...defaultSettings, ...settings, ...customOptions, }; + } + + update() { + this.map.updateData(this.drawRoutes, this.settings.showPolygon); + this.map.setLoading(false); + } + + resize() { + this.map?.invalidateSize(); + this.map.onResize(); + } + + onDestroy() { + } +} + +export let TbMapWidgetV2: MapWidgetStaticInterface = MapWidgetController; + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts new file mode 100644 index 0000000000..e9f1f5dadd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts @@ -0,0 +1,467 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import L from 'leaflet'; +import { FormattedData, MarkerSettings, PolygonSettings, PolylineSettings, ReplaceInfo } from './map-models'; +import { Datasource, DatasourceData } from '@app/shared/models/widget.models'; +import _ from 'lodash'; +import { Observable, Observer, of } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { + createLabelFromDatasource, + hashCode, + isDefined, + isDefinedAndNotNull, isFunction, + isNumber, + isUndefined, + padValue +} from '@core/utils'; + +export function createTooltip(target: L.Layer, + settings: MarkerSettings | PolylineSettings | PolygonSettings, + datasource: Datasource, + content?: string | HTMLElement +): L.Popup { + const popup = L.popup(); + popup.setContent(content); + target.bindPopup(popup, { autoClose: settings.autocloseTooltip, closeOnClick: false }); + if (settings.showTooltipAction === 'hover') { + target.off('click'); + target.on('mouseover', () => { + target.openPopup(); + }); + target.on('mouseout', () => { + target.closePopup(); + }); + } + target.on('popupopen', () => { + bindPopupActions(popup, settings, datasource); + }); + return popup; +} + +export function bindPopupActions(popup: L.Popup, settings: MarkerSettings | PolylineSettings | PolygonSettings, + datasource: Datasource) { + const actions = popup.getElement().getElementsByClassName('tb-custom-action'); + Array.from(actions).forEach( + (element: HTMLElement) => { + const actionName = element.getAttribute('data-action-name'); + if (element && settings.tooltipAction[actionName]) { + element.onclick = ($event) => + { + settings.tooltipAction[actionName]($event, datasource); + return false; + }; + } + }); +} + +export function getRatio(firsMoment: number, secondMoment: number, intermediateMoment: number): number { + return (intermediateMoment - firsMoment) / (secondMoment - firsMoment); +} + +export function interpolateOnLineSegment( + pointA: FormattedData, + pointB: FormattedData, + latKeyName: string, + lngKeyName: string, + ratio: number +): { [key: string]: number } { + return { + [latKeyName]: (pointA[latKeyName] + (pointB[latKeyName] - pointA[latKeyName]) * ratio), + [lngKeyName]: (pointA[lngKeyName] + (pointB[lngKeyName] - pointA[lngKeyName]) * ratio) + }; +} + +export function findAngle(startPoint: FormattedData, endPoint: FormattedData, latKeyName: string, lngKeyName: string): number { + if (isUndefined(startPoint) || isUndefined(endPoint)) { + return 0; + } + let angle = -Math.atan2(endPoint[latKeyName] - startPoint[latKeyName], endPoint[lngKeyName] - startPoint[lngKeyName]); + angle = angle * 180 / Math.PI; + return parseInt(angle.toFixed(2), 10); +} + + +export function getDefCenterPosition(position) { + if (typeof (position) === 'string') { + return position.split(','); + } + if (typeof (position) === 'object') { + return position; + } + return [0, 0]; +} + + +const imageAspectMap = {}; + +function imageLoader(imageUrl: string): Observable { + return new Observable((observer: Observer) => { + const image = document.createElement('img'); // support IE + image.style.position = 'absolute'; + image.style.left = '-99999px'; + image.style.top = '-99999px'; + image.onload = () => { + observer.next(image); + document.body.removeChild(image); + observer.complete(); + }; + image.onerror = err => { + observer.error(err); + document.body.removeChild(image); + observer.complete(); + }; + document.body.appendChild(image); + image.src = imageUrl; + }); +} + +export function aspectCache(imageUrl: string): Observable { + if (imageUrl?.length) { + const hash = hashCode(imageUrl); + let aspect = imageAspectMap[hash]; + if (aspect) { + return of(aspect); + } + return imageLoader(imageUrl).pipe(map(image => { + aspect = image.width / image.height; + imageAspectMap[hash] = aspect; + return aspect; + })); + } +} + +export type TranslateFunc = (key: string, defaultTranslation?: string) => string; + +const varsRegex = /\${([^}]*)}/g; +const linkActionRegex = /([^<]*)<\/link-act>/g; +const buttonActionRegex = /([^<]*)<\/button-act>/g; + +function createLinkElement(actionName: string, actionText: string): string { + return `${actionText}`; +} + +function createButtonElement(actionName: string, actionText: string) { + return ``; +} + +function parseTemplate(template: string, data: { $datasource?: Datasource, [key: string]: any }, + translateFn?: TranslateFunc) { + let res = ''; + try { + if (translateFn) { + template = translateFn(template); + } + template = createLabelFromDatasource(data.$datasource, template); + + let match = /\${([^}]*)}/g.exec(template); + while (match !== null) { + const variable = match[0]; + let label = match[1]; + let valDec = 2; + const splitValues = label.split(':'); + if (splitValues.length > 1) { + label = splitValues[0]; + valDec = parseFloat(splitValues[1]); + } + + if (label.startsWith('#')) { + const keyIndexStr = label.substring(1); + const n = Math.floor(Number(keyIndexStr)); + if (String(n) === keyIndexStr && n >= 0) { + label = data.$datasource.dataKeys[n].label; + } + } + + const value = data[label] || ''; + let textValue: string; + if (isNumber(value)) { + textValue = padValue(value, valDec); + } else { + textValue = value; + } + template = template.replace(variable, textValue); + match = /\${([^}]*)}/g.exec(template); + } + + let actionTags: string; + let actionText: string; + let actionName: string; + let action: string; + + match = linkActionRegex.exec(template); + while (match !== null) { + [actionTags, actionName, actionText] = match; + action = createLinkElement(actionName, actionText); + template = template.replace(actionTags, action); + match = linkActionRegex.exec(template); + } + + match = buttonActionRegex.exec(template); + while (match !== null) { + [actionTags, actionName, actionText] = match; + action = createButtonElement(actionName, actionText); + template = template.replace(actionTags, action); + match = buttonActionRegex.exec(template); + } + + // const compiled = _.template(template); + // res = compiled(data); + res = template; + } catch (ex) { + console.log(ex, template); + } + return res; +} + +export function processPattern(template: string, data: { $datasource?: Datasource, [key: string]: any }): Array { + const replaceInfo = []; + try { + const reg = /\${([^}]*)}/g; + let match = reg.exec(template); + while (match !== null) { + const variableInfo: ReplaceInfo = { + dataKeyName: '', + valDec: 2, + variable: '' + }; + const variable = match[0]; + let label = match[1]; + let valDec = 2; + const splitValues = label.split(':'); + if (splitValues.length > 1) { + label = splitValues[0]; + valDec = parseFloat(splitValues[1]); + } + + variableInfo.variable = variable; + variableInfo.valDec = valDec; + + if (label.startsWith('#')) { + const keyIndexStr = label.substring(1); + const n = Math.floor(Number(keyIndexStr)); + if (String(n) === keyIndexStr && n >= 0) { + variableInfo.dataKeyName = data.$datasource.dataKeys[n].label; + } + } else { + variableInfo.dataKeyName = label; + } + replaceInfo.push(variableInfo); + + match = reg.exec(template); + } + } catch (ex) { + console.log(ex, template); + } + return replaceInfo; +} + +export function fillPattern(markerLabelText: string, replaceInfoLabelMarker: Array, data: FormattedData) { + let text = createLabelFromDatasource(data.$datasource, markerLabelText); + if (replaceInfoLabelMarker) { + for (const variableInfo of replaceInfoLabelMarker) { + let txtVal = ''; + if (variableInfo.dataKeyName && isDefinedAndNotNull(data[variableInfo.dataKeyName])) { + const varData = data[variableInfo.dataKeyName]; + if (isNumber(varData)) { + txtVal = padValue(varData, variableInfo.valDec); + } else { + txtVal = varData; + } + } + text = text.replace(variableInfo.variable, txtVal); + } + } + return text; +} + +function prepareProcessPattern(template: string, translateFn?: TranslateFunc): string { + if (translateFn) { + template = translateFn(template); + } + let actionTags: string; + let actionText: string; + let actionName: string; + let action: string; + + let match = linkActionRegex.exec(template); + while (match !== null) { + [actionTags, actionName, actionText] = match; + action = createLinkElement(actionName, actionText); + template = template.replace(actionTags, action); + match = linkActionRegex.exec(template); + } + + match = buttonActionRegex.exec(template); + while (match !== null) { + [actionTags, actionName, actionText] = match; + action = createButtonElement(actionName, actionText); + template = template.replace(actionTags, action); + match = buttonActionRegex.exec(template); + } + return template; +} + +export const parseWithTranslation = { + + translateFn: null, + + translate(key: string, defaultTranslation?: string): string { + if (this.translateFn) { + return this.translateFn(key, defaultTranslation); + } else { + throw console.error('Translate not assigned'); + } + }, + parseTemplate(template: string, data: object, forceTranslate = false): string { + return parseTemplate(forceTranslate ? this.translate(template) : template, data, this.translate.bind(this)); + }, + prepareProcessPattern(template: string, forceTranslate = false): string { + return prepareProcessPattern(forceTranslate ? this.translate(template) : template, this.translate.bind(this)); + }, + setTranslate(translateFn: TranslateFunc) { + this.translateFn = translateFn; + } +}; + +export function parseData(input: DatasourceData[]): FormattedData[] { + return _(input).groupBy(el => el?.datasource?.entityName) + .values().value().map((entityArray, i) => { + const obj: FormattedData = { + entityName: entityArray[0]?.datasource?.entityName, + entityId: entityArray[0]?.datasource?.entityId, + entityType: entityArray[0]?.datasource?.entityType, + $datasource: entityArray[0]?.datasource, + dsIndex: i, + deviceType: null + }; + entityArray.filter(el => el.data.length).forEach(el => { + const indexDate = el?.data?.length ? el.data.length - 1 : 0; + obj[el?.dataKey?.label] = el?.data[indexDate][1]; + obj[el?.dataKey?.label + '|ts'] = el?.data[indexDate][0]; + if (el?.dataKey?.label === 'type') { + obj.deviceType = el?.data[indexDate][1]; + } + }); + return obj; + }); +} + +export function parseArray(input: DatasourceData[]): FormattedData[][] { + return _(input).groupBy(el => el?.datasource?.entityName) + .values().value().map((entityArray) => + entityArray[0].data.map((el, i) => { + const obj: FormattedData = { + entityName: entityArray[0]?.datasource?.entityName, + entityId: entityArray[0]?.datasource?.entityId, + entityType: entityArray[0]?.datasource?.entityType, + $datasource: entityArray[0]?.datasource, + dsIndex: i, + time: el[0], + deviceType: null + }; + entityArray.filter(e => e.data.length && e.data[i]).forEach(entity => { + obj[entity?.dataKey?.label] = entity?.data[i][1]; + obj[entity?.dataKey?.label + '|ts'] = entity?.data[0][0]; + if (entity?.dataKey?.label === 'type') { + obj.deviceType = entity?.data[0][1]; + } + }); + return obj; + }) + ); +} + +export function parseFunction(source: any, params: string[] = ['def']): (...args: any[]) => any { + let res = null; + if (source?.length) { + try { + res = new Function(...params, source); + } + catch (err) { + res = null; + } + } + return res; +} + +export function safeExecute(func: (...args: any[]) => any, params = []) { + let res = null; + if (func && typeof (func) === 'function') { + try { + res = func(...params); + } + catch (err) { + console.log('error in external function:', err); + res = null; + } + } + return res; +} + +export function functionValueCalculator(useFunction: boolean, func: (...args: any[]) => any, params = [], defaultValue: any) { + let res; + if (useFunction && isDefined(func) && isFunction(func)) { + try { + res = func(...params); + if (!isDefinedAndNotNull(res) || res === '') { + res = defaultValue; + } + } catch (err) { + res = defaultValue; + console.log('error in external function:', err); + } + } else { + res = defaultValue; + } + return res; +} + +export function calculateNewPointCoordinate(coordinate: number, imageSize: number): number { + let pointCoordinate = coordinate / imageSize; + if (pointCoordinate < 0) { + pointCoordinate = 0; + } else if (pointCoordinate > 1) { + pointCoordinate = 1; + } + return pointCoordinate; +} + +export function createLoadingDiv(loadingText: string): JQuery { + return $(` +
+ ${loadingText} +
+ `); +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.scss new file mode 100644 index 0000000000..3ea11fa154 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.scss @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.arrow { + height: 30px; + width: 30px; + background-size: contain; + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC0AAAAtCAMAAAANxBKoAAACAVBMVEUAAAAAqv9AgP8ggP8udP8qgP8ndv8kbf8kgP8eeP8rcf8mc/8jdP8hev8me/8kdv8jcv8pcP8ic/8neP8leP8ofP8mef8pev8nef8oe/8nef8pef8qfv8qgf8qe/8mev8off8oev8oef8oe/8pev8qe/8oe/8off8pfP8oev8pev8qe/8qfP8pev8pe/8qfP8pe/8oev8pe/8qe/8qfP8qfP8qff8qff8qfP8qff8pe/8qff8qfP8qfP8pev8pev8JYf8KYP8UY/8UZf8WY/8WZv8YZP8bZP8fZv8fZ/8gZv8gZ/8hZP8iZ/8jaf8jav8kaP8kav8ka/8kbP8kbf8kb/8lbP8lbf8lbv8lb/8lcP8mb/8mcP8mcf8ncf8ncv8nc/8ndP8ndf8ocv4odP8odf8odv8od/8oeP8peP8pef8pev8qev8qe/8qfP8rbP8rfP8rff8tc/4vcP4xbv4zcf42dP49df4+gP4/fv5Rif5biP1ul/14mvx4nvyMrfuMrvuWsfuctvuguPuww/vD0fnH1vrI1frK2PrL2fnV3vnb4vnc5fnd5fnd5vne5fnj6vnl6/jm7Pno7fXo7fbo7ffo7fjp7fnp7vjq7vjs8fnt8Pjt8fjw8fXw8fbw8fjy8/jz9fn5+fj7+/f++/j/+/X/+/b/+/f/+/j//Pj//vhoUaHMAAAAQHRSTlMAAwQICwwNDg4REhQWFxscHTI1QkRGSktOU1RdYWFscXJzhYWKqre6wMHDw8PHx8rN09TW1uLi7e/x8vLz9fj5hfwW0QAAAnVJREFUSMeV1fdb00AYB/BzoQjurSjuvQcuFLcgWqTVtLVpL0GS2GuDuLeouPeAiLOuwOFf6WU0ucvq4/d52v6Qz/M+18u97wHgyfh5m5vUYlHt2rV8BojOqGUqonNhVgTegfxZE2JXoMAUFgXYsSoKy3YfnoIiUqxm8UwUnSoaT0WVUuPi6lCUz+ft/+pqNYwqsiwrlm8s45XBVpE7BQgFyeZLbB1ihVwmefjpDSjbi7HwziArCdlU/MArjDHfaestBh4dZs9/JBb/4kVbo5FEr/JaWRL4VPvZ39jMVV4q63qiCx7bIfDJtst/LYt/JnLldaNuACbS9JwiQj4Zu/3HlDr5nEyJivMcgPn0Nogwfaq1Z9igQ7pO9AuqNEJzQINTV5ZghjvywLFG6dYMVRptAF3uPvBc8xPsWH0Q44ccpEqjvaBoa0lI7/ts2kHdCsbDJ5jSSC2/SEU8856x+hDGd+JZujQquvp6ieT7Nyel0o/jCSgzTefq0y81I30f7PRr2i1fbWrd+98yXtMGfOt29wTyiebHpu83dZ+m3U969qSBeje5DHeod8Dx5PeYZ7/rPO8y0XLvk+1J8UcJyLzLScw5kTsgz8Xu2vXJV0uaOSfsGcznZZGcwdi1L5qV13HmDILV3tY1z3fbxa8Wv+Se74VEj/H3jmz0TvuVN4Z+x/ZORF8efU64s/CtUT0vCbk0d/DZTSgxPQ/WhswTSYTZrDNPllaaVQo1q/Y4k63mv+YgmFZxxk6gR/LsCngcO+6nR9lCrfcqqQ2/dxqD7rT1IXhxyB24O8BuCr9eq9axU7F7wYjoy3ty3TZVLaCC2rRxru/hPwVgtEdFK5vOAAAAAElFTkSuQmCC"); + background-position: center; + background-repeat: no-repeat; +} + +.tb-marker-label, +.tb-marker-label:before { + border: none; + background: none; + box-shadow: none; +} + +.leaflet-container{ + background-color: white; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts new file mode 100644 index 0000000000..4796956fa6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts @@ -0,0 +1,229 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import L, { Icon, LeafletMouseEvent } from 'leaflet'; +import { FormattedData, MarkerSettings } from './map-models'; +import { + aspectCache, + bindPopupActions, + createTooltip, + fillPattern, + parseWithTranslation, + processPattern, + safeExecute +} from './maps-utils'; +import tinycolor from 'tinycolor2'; +import { isDefined, isDefinedAndNotNull } from '@core/utils'; +import LeafletMap from './leaflet-map'; + +export class Marker { + leafletMarker: L.Marker; + tooltipOffset: L.LatLngTuple; + markerOffset: L.LatLngTuple; + tooltip: L.Popup; + data: FormattedData; + dataSources: FormattedData[]; + + constructor(private map: LeafletMap, private location: L.LatLng, public settings: MarkerSettings, + data?: FormattedData, dataSources?, onDragendListener?) { + this.setDataSources(data, dataSources); + this.leafletMarker = L.marker(location, { + draggable: settings.draggableMarker + }); + + this.markerOffset = [ + isDefined(settings.markerOffsetX) ? settings.markerOffsetX : 0.5, + isDefined(settings.markerOffsetY) ? settings.markerOffsetY : 1, + ]; + + this.createMarkerIcon((iconInfo) => { + this.leafletMarker.setIcon(iconInfo.icon); + this.tooltipOffset = [0, -iconInfo.size[1] * this.markerOffset[1] + 10]; + this.updateMarkerLabel(settings); + }); + + if (settings.showTooltip) { + this.tooltip = createTooltip(this.leafletMarker, settings, data.$datasource); + this.updateMarkerTooltip(data); + } + + if (this.settings.markerClick) { + this.leafletMarker.on('click', (event: LeafletMouseEvent) => { + for (const action in this.settings.markerClick) { + if (typeof (this.settings.markerClick[action]) === 'function') { + this.settings.markerClick[action](event.originalEvent, this.data.$datasource); + } + } + }); + } + + if (onDragendListener) { + this.leafletMarker.on('dragend', (e) => onDragendListener(e, this.data)); + } + } + + setDataSources(data: FormattedData, dataSources: FormattedData[]) { + this.data = data; + this.dataSources = dataSources; + } + + updateMarkerTooltip(data: FormattedData) { + if (!this.map.markerTooltipText || this.settings.useTooltipFunction) { + const pattern = this.settings.useTooltipFunction ? + safeExecute(this.settings.tooltipFunction, [this.data, this.dataSources, this.data.dsIndex]) : this.settings.tooltipPattern; + this.map.markerTooltipText = parseWithTranslation.prepareProcessPattern(pattern, true); + this.map.replaceInfoTooltipMarker = processPattern(this.map.markerTooltipText, data); + } + this.tooltip.setContent(fillPattern(this.map.markerTooltipText, this.map.replaceInfoTooltipMarker, data)); + if (this.tooltip.isOpen() && this.tooltip.getElement()) { + bindPopupActions(this.tooltip, this.settings, data.$datasource); + } + } + + updateMarkerPosition(position: L.LatLng) { + if (!this.location.equals(position)) { + this.location = position; + this.leafletMarker.setLatLng(position); + } + } + + updateMarkerLabel(settings: MarkerSettings) { + this.leafletMarker.unbindTooltip(); + if (settings.showLabel) { + if (!this.map.markerLabelText || settings.useLabelFunction) { + const pattern = settings.useLabelFunction ? + safeExecute(settings.labelFunction, [this.data, this.dataSources, this.data.dsIndex]) : settings.label; + this.map.markerLabelText = parseWithTranslation.prepareProcessPattern(pattern, true); + this.map.replaceInfoLabelMarker = processPattern(this.map.markerLabelText, this.data); + } + settings.labelText = fillPattern(this.map.markerLabelText, this.map.replaceInfoLabelMarker, this.data); + this.leafletMarker.bindTooltip(`
${settings.labelText}
`, + { className: 'tb-marker-label', permanent: true, direction: 'top', offset: this.tooltipOffset }); + } + } + + updateMarkerColor(color) { + this.createDefaultMarkerIcon(color, (iconInfo) => { + this.leafletMarker.setIcon(iconInfo.icon); + }); + } + + updateMarkerIcon(settings: MarkerSettings) { + this.createMarkerIcon((iconInfo) => { + this.leafletMarker.setIcon(iconInfo.icon); + this.tooltipOffset = [0, -iconInfo.size[1] * this.markerOffset[1] + 10]; + this.updateMarkerLabel(settings); + }); + } + + createMarkerIcon(onMarkerIconReady) { + if (this.settings.icon) { + onMarkerIconReady({ + size: [30, 30], + icon: this.settings.icon, + }); + return; + } + const currentImage = this.settings.useMarkerImageFunction ? + safeExecute(this.settings.markerImageFunction, + [this.data, this.settings.markerImages, this.dataSources, this.data.dsIndex]) : this.settings.currentImage; + let currentColor = this.settings.tinyColor; + if (this.settings.useColorFunction) { + const functionColor = safeExecute(this.settings.colorFunction, + [this.data, this.dataSources, this.data.dsIndex]); + if (isDefinedAndNotNull(functionColor)) { + currentColor = tinycolor(functionColor); + } + } + if (currentImage && currentImage.url) { + aspectCache(currentImage.url).subscribe( + (aspect) => { + if (aspect) { + let width; + let height; + if (aspect > 1) { + width = currentImage.size; + height = currentImage.size / aspect; + } else { + width = currentImage.size * aspect; + height = currentImage.size; + } + const icon = L.icon({ + iconUrl: currentImage.url, + iconSize: [width, height], + iconAnchor: [width * this.markerOffset[0], height * this.markerOffset[1]], + popupAnchor: [0, -height] + }); + const iconInfo = { + size: [width, height], + icon + }; + onMarkerIconReady(iconInfo); + } else { + this.createDefaultMarkerIcon(currentColor, onMarkerIconReady); + } + } + ); + } else { + this.createDefaultMarkerIcon(currentColor, onMarkerIconReady); + } + } + + createDefaultMarkerIcon(color: tinycolor.Instance, onMarkerIconReady) { + let icon: { size: number[], icon: Icon }; + if (!tinycolor.equals(color, this.settings.tinyColor)) { + icon = this.createColoredMarkerIcon(color); + } else { + if (!this.map.defaultMarkerIconInfo) { + this.map.defaultMarkerIconInfo = this.createColoredMarkerIcon(color); + } + icon = this.map.defaultMarkerIconInfo; + } + onMarkerIconReady(icon); + } + + createColoredMarkerIcon(color: tinycolor.Instance): { size: number[], icon: Icon } { + return { + size: [21, 34], + icon: L.icon({ + iconUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|' + color.toHex(), + iconSize: [21, 34], + iconAnchor: [21 * this.markerOffset[0], 34 * this.markerOffset[1]], + popupAnchor: [0, -34], + shadowUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_shadow', + shadowSize: [40, 37], + shadowAnchor: [12, 35] + }) + }; + } + + removeMarker() { + /* this.map$.subscribe(map => + this.leafletMarker.addTo(map))*/ + } + + extendBoundsWithMarker(bounds) { + bounds.extend(this.leafletMarker.getLatLng()); + } + + getMarkerPosition() { + return this.leafletMarker.getLatLng(); + } + + setMarkerPosition(latLng) { + this.leafletMarker.setLatLng(latLng); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts new file mode 100644 index 0000000000..dc2361a5fc --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts @@ -0,0 +1,118 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import L, { LatLngExpression, LeafletMouseEvent } from 'leaflet'; +import { createTooltip, functionValueCalculator, parseWithTranslation, safeExecute } from './maps-utils'; +import 'leaflet-editable/src/Leaflet.Editable'; +import { FormattedData, PolygonSettings } from './map-models'; + +export class Polygon { + + leafletPoly: L.Polygon; + tooltip: L.Popup; + data: FormattedData; + dataSources: FormattedData[]; + + constructor(public map, polyData: FormattedData, dataSources: FormattedData[], private settings: PolygonSettings, onDragendListener?) { + this.dataSources = dataSources; + this.data = polyData; + const polygonColor = this.getPolygonColor(settings); + this.leafletPoly = L.polygon(polyData[this.settings.polygonKeyName], { + fill: true, + fillColor: polygonColor, + color: settings.polygonStrokeColor, + weight: settings.polygonStrokeWeight, + fillOpacity: settings.polygonOpacity, + opacity: settings.polygonStrokeOpacity + }).addTo(this.map); + if (settings.editablePolygon) { + this.leafletPoly.enableEdit(this.map); + if (onDragendListener) { + this.leafletPoly.on('editable:vertex:dragend', e => onDragendListener(e, this.data)); + this.leafletPoly.on('editable:vertex:deleted', e => onDragendListener(e, this.data)); + } + } + + + if (settings.showPolygonTooltip) { + this.tooltip = createTooltip(this.leafletPoly, settings, polyData.$datasource); + this.updateTooltip(polyData); + } + if (settings.polygonClick) { + this.leafletPoly.on('click', (event: LeafletMouseEvent) => { + for (const action in this.settings.polygonClick) { + if (typeof (this.settings.polygonClick[action]) === 'function') { + this.settings.polygonClick[action](event.originalEvent, polyData.$datasource); + } + } + }); + } + } + + updateTooltip(data: FormattedData) { + const pattern = this.settings.usePolygonTooltipFunction ? + safeExecute(this.settings.polygonTooltipFunction, [this.data, this.dataSources, this.data.dsIndex]) : + this.settings.polygonTooltipPattern; + this.tooltip.setContent(parseWithTranslation.parseTemplate(pattern, data, true)); + } + + updatePolygon(data: FormattedData, dataSources: FormattedData[], settings: PolygonSettings) { + this.data = data; + this.dataSources = dataSources; + if (settings.editablePolygon) { + this.leafletPoly.disableEdit(); + } + this.leafletPoly.setLatLngs(data[this.settings.polygonKeyName]); + if (settings.editablePolygon) { + this.leafletPoly.enableEdit(this.map); + } + if (settings.showPolygonTooltip) { + this.updateTooltip(this.data); + } + this.updatePolygonColor(settings); + } + + removePolygon() { + this.map.removeLayer(this.leafletPoly); + } + + updatePolygonColor(settings: PolygonSettings) { + const polygonColor = this.getPolygonColor(settings); + const style: L.PathOptions = { + fill: true, + fillColor: polygonColor, + color: settings.polygonStrokeColor, + weight: settings.polygonStrokeWeight, + fillOpacity: settings.polygonOpacity, + opacity: settings.polygonStrokeOpacity + }; + this.leafletPoly.setStyle(style); + } + + getPolygonLatLngs() { + return this.leafletPoly.getLatLngs(); + } + + setPolygonLatLngs(latLngs: LatLngExpression[]) { + this.leafletPoly.setLatLngs(latLngs); + this.leafletPoly.redraw(); + } + + private getPolygonColor(settings: PolygonSettings): string | null { + return functionValueCalculator(settings.usePolygonColorFunction, settings.polygonColorFunction, + [this.data, this.dataSources, this.data.dsIndex], settings.polygonColor); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polyline.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/polyline.ts new file mode 100644 index 0000000000..8c4997933c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/polyline.ts @@ -0,0 +1,92 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import L, { PolylineDecoratorOptions } from 'leaflet'; +import 'leaflet-polylinedecorator'; + +import { FormattedData, PolylineSettings } from './map-models'; +import { functionValueCalculator, safeExecute } from '@home/components/widget/lib/maps/maps-utils'; + +export class Polyline { + + leafletPoly: L.Polyline; + polylineDecorator: L.PolylineDecorator; + dataSources: FormattedData[]; + data: FormattedData; + + constructor(private map: L.Map, locations: L.LatLng[], data: FormattedData, dataSources: FormattedData[], settings: PolylineSettings) { + this.dataSources = dataSources; + this.data = data; + + this.leafletPoly = L.polyline(locations, + this.getPolyStyle(settings) + ).addTo(this.map); + + if (settings.usePolylineDecorator) { + this.polylineDecorator = L.polylineDecorator(this.leafletPoly, this.getDecoratorSettings(settings)).addTo(this.map); + } + } + + getDecoratorSettings(settings: PolylineSettings): PolylineDecoratorOptions { + return { + patterns: [ + { + offset: settings.decoratorOffset, + endOffset: settings.endDecoratorOffset, + repeat: settings.decoratorRepeat, + symbol: L.Symbol[settings.decoratorSymbol]({ + pixelSize: settings.decoratorSymbolSize, + polygon: false, + pathOptions: { + color: settings.useDecoratorCustomColor ? settings.decoratorCustomColor : this.getPolyStyle(settings).color, + stroke: true + } + }) + } + ] + }; + } + + updatePolyline(locations: L.LatLng[], data: FormattedData, dataSources: FormattedData[], settings: PolylineSettings) { + this.data = data; + this.dataSources = dataSources; + this.leafletPoly.setLatLngs(locations); + this.leafletPoly.setStyle(this.getPolyStyle(settings)); + if (this.polylineDecorator) { + this.polylineDecorator.setPaths(this.leafletPoly); + } + } + + getPolyStyle(settings: PolylineSettings): L.PolylineOptions { + return { + interactive: false, + color: functionValueCalculator(settings.useColorFunction, settings.colorFunction, + [this.data, this.dataSources, this.data.dsIndex], settings.color), + opacity: functionValueCalculator(settings.useStrokeOpacityFunction, settings.strokeOpacityFunction, + [this.data, this.dataSources, this.data.dsIndex], settings.strokeOpacity), + weight: functionValueCalculator(settings.useStrokeWeightFunction, settings.strokeWeightFunction, + [this.data, this.dataSources, this.data.dsIndex], settings.strokeWeight) + }; + } + + removePolyline() { + this.map.removeLayer(this.leafletPoly); + } + + getPolylineLatLngs() { + return this.leafletPoly.getLatLngs(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/google-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/google-map.ts new file mode 100644 index 0000000000..94fa1a09ac --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/google-map.ts @@ -0,0 +1,66 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + + +import L from 'leaflet'; +import LeafletMap from '../leaflet-map'; +import { UnitedMapSettings } from '../map-models'; +import 'leaflet.gridlayer.googlemutant'; +import { ResourcesService } from '@core/services/resources.service'; +import { WidgetContext } from '@home/models/widget-component.models'; + +const gmGlobals: GmGlobal = {}; + +interface GmGlobal { + [key: string]: boolean; +} + +export class GoogleMap extends LeafletMap { + private resource: ResourcesService; + + constructor(ctx: WidgetContext, $container, options: UnitedMapSettings) { + super(ctx, $container, options); + this.resource = ctx.$injector.get(ResourcesService); + super.initSettings(options); + this.loadGoogle(() => { + const map = L.map($container, { + attributionControl: false, + editable: !!options.editablePolygon + }).setView(options?.defaultCenterPosition, options?.defaultZoomLevel); + (L.gridLayer as any).googleMutant({ + type: options?.gmDefaultMapType || 'roadmap' + }).addTo(map); + super.setMap(map); + }, options.gmApiKey); + } + + private loadGoogle(callback, apiKey = 'AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q') { + if (gmGlobals[apiKey]) { + callback(); + } else { + this.resource.loadResource(`https://maps.googleapis.com/maps/api/js?key=${apiKey}`).subscribe( + () => { + gmGlobals[apiKey] = true; + callback(); + }, + (error) => { + gmGlobals[apiKey] = false; + console.error(`Google map api load failed!`, error); + } + ); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/here-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/here-map.ts new file mode 100644 index 0000000000..a6418ee557 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/here-map.ts @@ -0,0 +1,33 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import L from 'leaflet'; +import LeafletMap from '../leaflet-map'; +import { UnitedMapSettings } from '../map-models'; +import { WidgetContext } from '@home/models/widget-component.models'; + +export class HEREMap extends LeafletMap { + constructor(ctx: WidgetContext, $container, options: UnitedMapSettings) { + super(ctx, $container, options); + const map = L.map($container, { + editable: !!options.editablePolygon + }).setView(options?.defaultCenterPosition, options?.defaultZoomLevel); + const tileLayer = (L.tileLayer as any).provider(options.mapProviderHere || 'HERE.normalDay', options.credentials); + tileLayer.addTo(map); + super.initSettings(options); + super.setMap(map); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts new file mode 100644 index 0000000000..1b20ddad01 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts @@ -0,0 +1,288 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import L, { LatLngBounds, LatLngLiteral, LatLngTuple } from 'leaflet'; +import LeafletMap from '../leaflet-map'; +import { MapImage, PosFuncton, UnitedMapSettings } from '../map-models'; +import { Observable, ReplaySubject } from 'rxjs'; +import { filter, map, mergeMap } from 'rxjs/operators'; +import { aspectCache, calculateNewPointCoordinate, parseFunction } from '@home/components/widget/lib/maps/maps-utils'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { DataSet, DatasourceType, widgetType } from '@shared/models/widget.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { WidgetSubscriptionOptions } from '@core/api/widget-api.models'; +import { isDefinedAndNotNull, isEmptyStr } from '@core/utils'; + +const maxZoom = 4; // ? + +export class ImageMap extends LeafletMap { + + imageOverlay: L.ImageOverlay; + aspect = 0; + width = 0; + height = 0; + imageUrl: string; + posFunction: PosFuncton; + + constructor(ctx: WidgetContext, $container: HTMLElement, options: UnitedMapSettings) { + super(ctx, $container, options); + this.posFunction = parseFunction(options.posFunction, ['origXPos', 'origYPos']) as PosFuncton; + this.mapImage(options).subscribe((mapImage) => { + this.imageUrl = mapImage.imageUrl; + this.aspect = mapImage.aspect; + if (mapImage.update) { + this.onResize(true); + } else { + this.onResize(); + super.initSettings(options); + super.setMap(this.map); + } + }); + } + + private mapImage(options: UnitedMapSettings): Observable { + const imageEntityAlias = options.imageEntityAlias; + const imageUrlAttribute = options.imageUrlAttribute; + if (!imageEntityAlias || !imageUrlAttribute) { + return this.imageFromUrl(options.mapUrl); + } + const entityAliasId = this.ctx.aliasController.getEntityAliasId(imageEntityAlias); + if (!entityAliasId) { + return this.imageFromUrl(options.mapUrl); + } + const datasources = [ + { + type: DatasourceType.entity, + name: imageEntityAlias, + aliasName: imageEntityAlias, + entityAliasId, + dataKeys: [ + { + type: DataKeyType.attribute, + name: imageUrlAttribute, + label: imageUrlAttribute, + settings: {}, + _hash: Math.random() + } + ] + } + ]; + const result = new ReplaySubject<[DataSet, boolean]>(); + let isUpdate = false; + const imageUrlSubscriptionOptions: WidgetSubscriptionOptions = { + datasources, + useDashboardTimewindow: false, + type: widgetType.latest, + callbacks: { + onDataUpdated: (subscription) => { + result.next([subscription.data[0]?.data, isUpdate]); + isUpdate = true; + } + } + }; + this.ctx.subscriptionApi.createSubscription(imageUrlSubscriptionOptions, true).subscribe(() => { }); + return this.imageFromAlias(result); + } + + private imageFromUrl(url: string): Observable { + return aspectCache(url).pipe( + map( aspect => { + const mapImage: MapImage = { + imageUrl: url, + aspect, + update: false + }; + return mapImage; + } + )); + } + + private imageFromAlias(alias: Observable<[DataSet, boolean]>): Observable { + return alias.pipe( + filter(result => result[0].length > 0), + mergeMap(res => { + const mapImage: MapImage = { + imageUrl: res[0][0][1], + aspect: null, + update: res[1] + }; + return aspectCache(mapImage.imageUrl).pipe( + map((aspect) => { + mapImage.aspect = aspect; + return mapImage; + } + )); + }) + ); + } + + updateBounds(updateImage?: boolean, lastCenterPos?) { + const w = this.width; + const h = this.height; + let southWest = this.pointToLatLng(0, h); + let northEast = this.pointToLatLng(w, 0); + const bounds = new L.LatLngBounds(southWest, northEast); + + if (updateImage && this.imageOverlay) { + this.imageOverlay.remove(); + this.imageOverlay = null; + } + + if (this.imageOverlay) { + this.imageOverlay.setBounds(bounds); + } else { + this.imageOverlay = L.imageOverlay(this.imageUrl, bounds).addTo(this.map); + } + const padding = 200 * maxZoom; + southWest = this.pointToLatLng(-padding, h + padding); + northEast = this.pointToLatLng(w + padding, -padding); + const maxBounds = new L.LatLngBounds(southWest, northEast); + this.map.setMaxBounds(maxBounds); + if (lastCenterPos) { + lastCenterPos.x *= w; + lastCenterPos.y *= h; + const center = this.pointToLatLng(lastCenterPos.x, lastCenterPos.y); + setTimeout(() => { + this.map.panTo(center, { animate: false }); + }, 0); + } + } + + onResize(updateImage?: boolean) { + let width = this.$container.clientWidth; + if (width > 0 && this.aspect) { + let height = width / this.aspect; + const imageMapHeight = this.$container.clientHeight; + if (imageMapHeight > 0 && height > imageMapHeight) { + height = imageMapHeight; + width = height * this.aspect; + } + width *= maxZoom; + const prevWidth = this.width; + const prevHeight = this.height; + if (this.width !== width || updateImage) { + this.width = width; + this.height = width / this.aspect; + if (!this.map) { + this.initMap(updateImage); + } else { + const lastCenterPos = this.latLngToPoint(this.map.getCenter()); + lastCenterPos.x /= prevWidth; + lastCenterPos.y /= prevHeight; + this.updateBounds(updateImage, lastCenterPos); + this.map.invalidateSize(true); + this.updateMarkers(this.markersData); + if (this.options.draggableMarker && this.addMarkers.length) { + this.addMarkers.forEach((marker) => { + const prevPoint = this.convertToCustomFormat(marker.getLatLng(), prevWidth, prevHeight); + marker.setLatLng(this.convertPosition(prevPoint)); + }); + } + this.updatePolygons(this.polygonsData); + if (this.options.showPolygon && this.options.editablePolygon && this.addPolygons.length) { + this.addPolygons.forEach((polygon) => { + const prevPolygonPoint = this.convertToPolygonFormat(polygon.getLatLngs(), prevWidth, prevHeight); + polygon.setLatLngs(this.convertPositionPolygon(prevPolygonPoint)); + }); + } + } + } + } + } + + fitBounds(bounds: LatLngBounds, padding?: LatLngTuple) { } + + initMap(updateImage?: boolean) { + if (!this.map && this.aspect > 0) { + const center = this.pointToLatLng(this.width / 2, this.height / 2); + this.map = L.map(this.$container, { + minZoom: 1, + maxZoom, + scrollWheelZoom: !this.options.disableScrollZooming, + center, + zoom: 1, + crs: L.CRS.Simple, + attributionControl: false, + editable: !!this.options.editablePolygon + }); + this.updateBounds(updateImage); + } + } + + convertPosition(expression): L.LatLng { + const xPos = expression[this.options.xPosKeyName]; + const yPos = expression[this.options.yPosKeyName]; + if (!isDefinedAndNotNull(xPos) || isEmptyStr(xPos) || isNaN(xPos) || !isDefinedAndNotNull(yPos) || isEmptyStr(yPos) || isNaN(yPos)) { + return null; + } + Object.assign(expression, this.posFunction(xPos, yPos)); + return this.pointToLatLng( + expression.x * this.width, + expression.y * this.height); + } + + convertPositionPolygon(expression: (LatLngTuple | LatLngTuple[] | LatLngTuple[][])[]){ + return (expression).map((el) => { + if (!Array.isArray(el[0]) && !Array.isArray(el[1]) && el.length === 2) { + return this.pointToLatLng( + el[0] * this.width, + el[1] * this.height + ); + } else if (Array.isArray(el) && el.length) { + return this.convertPositionPolygon(el as LatLngTuple[] | LatLngTuple[][]); + } else { + return null; + } + }).filter(el => !!el); + } + + pointToLatLng(x, y): L.LatLng { + return L.CRS.Simple.pointToLatLng({ x, y } as L.PointExpression, maxZoom - 1); + } + + latLngToPoint(latLng: LatLngLiteral) { + return L.CRS.Simple.latLngToPoint(latLng, maxZoom - 1); + } + + convertToCustomFormat(position: L.LatLng, width = this.width, height = this.height): object { + const point = this.latLngToPoint(position); + return { + [this.options.xPosKeyName]: calculateNewPointCoordinate(point.x, width), + [this.options.yPosKeyName]: calculateNewPointCoordinate(point.y, height) + }; + } + + convertToPolygonFormat(points: Array, width = this.width, height = this.height): Array { + if (points.length) { + return points.map(point => { + if (point.length) { + return this.convertToPolygonFormat(point, width, height); + } else { + const pos = this.latLngToPoint(point); + return [calculateNewPointCoordinate(pos.x, width), calculateNewPointCoordinate(pos.y, height)]; + } + }); + } else { + return []; + } + } + + convertPolygonToCustomFormat(expression: any[][]): object { + return { + [this.options.polygonKeyName] : this.convertToPolygonFormat(expression) + }; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/index.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/index.ts new file mode 100644 index 0000000000..9c6bb63ddf --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/index.ts @@ -0,0 +1,63 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + googleMapSettingsSchema, hereMapSettingsSchema, imageMapSettingsSchema, + openstreetMapSettingsSchema, + tencentMapSettingsSchema +} from '@home/components/widget/lib/maps/schemes'; +import { OpenStreetMap } from './openstreet-map'; +import { TencentMap } from './tencent-map'; +import { GoogleMap } from './google-map'; +import { HEREMap } from './here-map'; +import { ImageMap } from './image-map'; +import { Type } from '@angular/core'; +import LeafletMap from '@home/components/widget/lib/maps/leaflet-map'; +import { JsonSettingsSchema } from '@shared/models/widget.models'; + +interface IProvider { + MapClass: Type; + schema: JsonSettingsSchema; + name: string; +} + +export const providerSets: { [key: string]: IProvider } = { + 'openstreet-map': { + MapClass: OpenStreetMap, + schema: openstreetMapSettingsSchema, + name: 'openstreet-map' + }, + 'tencent-map': { + MapClass: TencentMap, + schema: tencentMapSettingsSchema, + name: 'tencent-map' + }, + 'google-map': { + MapClass: GoogleMap, + schema: googleMapSettingsSchema, + name: 'google-map' + }, + here: { + MapClass: HEREMap, + schema: hereMapSettingsSchema, + name: 'here' + }, + 'image-map': { + MapClass: ImageMap, + schema: imageMapSettingsSchema, + name: 'image-map' + } +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/openstreet-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/openstreet-map.ts new file mode 100644 index 0000000000..f4f10a0dac --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/openstreet-map.ts @@ -0,0 +1,38 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import L from 'leaflet'; +import LeafletMap from '../leaflet-map'; +import { UnitedMapSettings } from '../map-models'; +import { WidgetContext } from '@home/models/widget-component.models'; + +export class OpenStreetMap extends LeafletMap { + constructor(ctx: WidgetContext, $container, options: UnitedMapSettings) { + super(ctx, $container, options); + const map = L.map($container, { + editable: !!options.editablePolygon + }).setView(options?.defaultCenterPosition, options?.defaultZoomLevel); + let tileLayer; + if (options.useCustomProvider) { + tileLayer = L.tileLayer(options.customProviderTileUrl); + } else { + tileLayer = (L.tileLayer as any).provider(options.mapProvider || 'OpenStreetMap.Mapnik'); + } + tileLayer.addTo(map); + super.initSettings(options); + super.setMap(map); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/tencent-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/tencent-map.ts new file mode 100644 index 0000000000..498a6053a1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/tencent-map.ts @@ -0,0 +1,39 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + + +import L from 'leaflet'; +import LeafletMap from '../leaflet-map'; +import { UnitedMapSettings } from '../map-models'; +import { WidgetContext } from '@home/models/widget-component.models'; + +export class TencentMap extends LeafletMap { + constructor(ctx: WidgetContext, $container, options: UnitedMapSettings) { + super(ctx, $container, options); + const txUrl = 'http://rt{s}.map.gtimg.com/realtimerender?z={z}&x={x}&y={y}&type=vector&style=0'; + const map = L.map($container, { + editable: !!options.editablePolygon + }).setView(options?.defaultCenterPosition, options?.defaultZoomLevel); + const txLayer = L.tileLayer(txUrl, { + subdomains: '0123', + tms: true, + attribution: '©2020 Tencent - GS(2018)2236号- Data© NavInfo' + }).addTo(map); + txLayer.addTo(map); + super.initSettings(options); + super.setMap(map); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts new file mode 100644 index 0000000000..3506978c4a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts @@ -0,0 +1,1098 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export const googleMapSettingsSchema = +{ + schema: { + title: 'Google Map Configuration', + type: 'object', + properties: { + gmApiKey: { + title: 'Google Maps API Key', + type: 'string', + default: 'AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q' + }, + gmDefaultMapType: { + title: 'Default map type', + type: 'string', + default: 'roadmap' + } + }, + required: [] + }, + form: [ + 'gmApiKey', + { + key: 'gmDefaultMapType', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'roadmap', + label: 'Roadmap' + }, + { + value: 'satellite', + label: 'Satellite' + }, + { + value: 'hybrid', + label: 'Hybrid' + }, + { + value: 'terrain', + label: 'Terrain' + } + ] + } + ] +}; + +export const tencentMapSettingsSchema = +{ + schema: { + title: 'Tencent Map Configuration', + type: 'object', + properties: { + tmApiKey: { + title: 'Tencent Maps API Key', + type: 'string', + default: '84d6d83e0e51e481e50454ccbe8986b' + }, + tmDefaultMapType: { + title: 'Default map type', + type: 'string', + default: 'roadmap' + } + }, + required: [] + }, + form: [ + 'tmApiKey', + { + key: 'tmDefaultMapType', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'roadmap', + label: 'Roadmap' + }, + { + value: 'satellite', + label: 'Satellite' + }, + { + value: 'hybrid', + label: 'Hybrid' + }, + ] + } + ] +}; + +export const hereMapSettingsSchema = +{ + schema: { + title: 'HERE Map Configuration', + type: 'object', + properties: { + mapProviderHere: { + title: 'Map layer', + type: 'string', + default: 'HERE.normalDay' + }, + credentials: { + type: 'object', + title: 'Credentials', + properties: { + app_id: { + title: 'HERE app id', + type: 'string', + default: 'AhM6TzD9ThyK78CT3ptx' + }, + app_code: { + title: 'HERE app code', + type: 'string', + default: 'p6NPiITB3Vv0GMUFnkLOOg' + } + }, + required: ['app_id', 'app_code'] + } + }, + required: [] + }, + form: [ + { + key: 'mapProviderHere', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'HERE.normalDay', + label: 'HERE.normalDay (Default)' + }, + { + value: 'HERE.normalNight', + label: 'HERE.normalNight' + }, + { + value: 'HERE.hybridDay', + label: 'HERE.hybridDay' + }, + { + value: 'HERE.terrainDay', + label: 'HERE.terrainDay' + } + ] + }, + 'credentials' + ] +}; + +export const openstreetMapSettingsSchema = +{ + schema: { + title: 'Openstreet Map Configuration', + type: 'object', + properties: { + mapProvider: { + title: 'Map provider', + type: 'string', + default: 'OpenStreetMap.Mapnik' + }, + useCustomProvider: { + title: 'Use custom provider', + type: 'boolean', + default: false + }, + customProviderTileUrl: { + title: 'Custom provider tile URL', + type: 'string', + default: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' + } + }, + required: [] + }, + form: [ + { + key: 'mapProvider', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'OpenStreetMap.Mapnik', + label: 'OpenStreetMap.Mapnik (Default)' + }, + { + value: 'OpenStreetMap.HOT', + label: 'OpenStreetMap.HOT' + }, + { + value: 'Esri.WorldStreetMap', + label: 'Esri.WorldStreetMap' + }, + { + value: 'Esri.WorldTopoMap', + label: 'Esri.WorldTopoMap' + }, + { + value: 'CartoDB.Positron', + label: 'CartoDB.Positron' + }, + { + value: 'CartoDB.DarkMatter', + label: 'CartoDB.DarkMatter' + } + ] + }, + 'useCustomProvider', + { + key: 'customProviderTileUrl', + condition: 'model.useCustomProvider === true', + } + ] +}; + +export const commonMapSettingsSchema = +{ + schema: { + title: 'Map Configuration', + type: 'object', + properties: { + defaultZoomLevel: { + title: 'Default map zoom level (0 - 20)', + type: 'number' + }, + useDefaultCenterPosition: { + title: 'Use default map center position', + type: 'boolean', + default: false + }, + mapPageSize: { + title: 'Limit of entities to load', + type: 'number', + default: 16384 + }, + defaultCenterPosition: { + title: 'Default map center position (0,0)', + type: 'string', + default: '0,0' + }, + fitMapBounds: { + title: 'Fit map bounds to cover all markers', + type: 'boolean', + default: true + }, + draggableMarker: { + title: 'Draggable Marker', + type: 'boolean', + default: false + }, + disableScrollZooming: { + title: 'Disable scroll zooming', + type: 'boolean', + default: false + }, + latKeyName: { + title: 'Latitude key name', + type: 'string', + default: 'latitude' + }, + lngKeyName: { + title: 'Longitude key name', + type: 'string', + default: 'longitude' + }, + xPosKeyName: { + title: 'X position key name', + type: 'string', + default: 'xPos' + }, + yPosKeyName: { + title: 'Y position key name', + type: 'string', + default: 'yPos' + }, + showLabel: { + title: 'Show label', + type: 'boolean', + default: true + }, + label: { + title: 'Label (pattern examples: \'${entityName}\', \'${entityName}: (Text ${keyName} units.)\' )', + type: 'string', + default: '${entityName}' + }, + useLabelFunction: { + title: 'Use label function', + type: 'boolean', + default: false + }, + labelFunction: { + title: 'Label function: f(data, dsData, dsIndex)', + type: 'string' + }, + showTooltip: { + title: 'Show tooltip', + type: 'boolean', + default: true + }, + showTooltipAction: { + title: 'Action for displaying the tooltip', + type: 'string', + default: 'click' + }, + autocloseTooltip: { + title: 'Auto-close tooltips', + type: 'boolean', + default: true + }, + tooltipPattern: { + title: 'Tooltip (for ex. \'Text ${keyName} units.\' or Link text\')', + type: 'string', + default: '${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}' + }, + useTooltipFunction: { + title: 'Use tooltip function', + type: 'boolean', + default: false + }, + tooltipFunction: { + title: 'Tooltip function: f(data, dsData, dsIndex)', + type: 'string' + }, + posFunction: { + title: 'Position conversion function: f(origXPos, origYPos), should return x,y coordinates as double from 0 to 1 each', + type: 'string', + default: 'return {x: origXPos, y: origYPos};' + }, + markerOffsetX: { + title: 'Marker X offset relative to position', + type: 'number', + default: 0.5 + }, + markerOffsetY: { + title: 'Marker Y offset relative to position', + type: 'number', + default: 1 + }, + color: { + title: 'Color', + type: 'string' + }, + useColorFunction: { + title: 'Use color function', + type: 'boolean', + default: false + }, + colorFunction: { + title: 'Color function: f(data, dsData, dsIndex)', + type: 'string' + }, + markerImage: { + title: 'Custom marker image', + type: 'string' + }, + markerImageSize: { + title: 'Custom marker image size (px)', + type: 'number', + default: 34 + }, + useMarkerImageFunction: { + title: 'Use marker image function', + type: 'boolean', + default: false + }, + markerImageFunction: { + title: 'Marker image function: f(data, images, dsData, dsIndex)', + type: 'string' + }, + markerImages: { + title: 'Marker images', + type: 'array', + items: { + title: 'Marker image', + type: 'string' + } + } + }, + required: [] + }, + form: [ + { + key: 'defaultZoomLevel', + condition: 'model.provider !== "image-map"' + }, + { + key: 'useDefaultCenterPosition', + condition: 'model.provider !== "image-map"' + }, + { + key: 'defaultCenterPosition', + condition: 'model.provider !== "image-map"' + }, + { + key: 'fitMapBounds', + condition: 'model.provider !== "image-map"' + }, + 'mapPageSize', + 'draggableMarker', + { + key: 'disableScrollZooming', + condition: 'model.provider !== "image-map"' + }, + { + key: 'latKeyName', + condition: 'model.provider !== "image-map"' + }, + { + key: 'lngKeyName', + condition: 'model.provider !== "image-map"' + }, + { + key: 'xPosKeyName', + condition: 'model.provider === "image-map"' + }, + { + key: 'yPosKeyName', + condition: 'model.provider === "image-map"' + }, + 'showLabel', + 'label', + 'useLabelFunction', + { + key: 'labelFunction', + type: 'javascript' + }, + 'showTooltip', + { + key: 'showTooltipAction', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'click', + label: 'Show tooltip on click (Default)' + }, + { + value: 'hover', + label: 'Show tooltip on hover' + } + ] + }, + 'autocloseTooltip', + { + key: 'tooltipPattern', + type: 'textarea' + }, + 'useTooltipFunction', + { + key: 'tooltipFunction', + type: 'javascript' + }, + { + key: 'markerOffsetX', + condition: 'model.provider === "image-map"' + }, + { + key: 'markerOffsetY', + condition: 'model.provider === "image-map"' + }, + { + key: 'posFunction', + type: 'javascript', + condition: 'model.provider === "image-map"' + }, + { + key: 'color', + type: 'color' + }, + 'useColorFunction', + { + key: 'colorFunction', + type: 'javascript' + }, + { + key: 'markerImage', + type: 'image' + }, + 'markerImageSize', + 'useMarkerImageFunction', + { + key: 'markerImageFunction', + type: 'javascript' + }, + { + key: 'markerImages', + items: [ + { + key: 'markerImages[]', + type: 'image' + } + ] + } + ] +}; + +export const mapPolygonSchema = +{ + schema: { + title: 'Map Polygon Configuration', + type: 'object', + properties: { + showPolygon: { + title: 'Show polygon', + type: 'boolean', + default: false + }, + polygonKeyName: { + title: 'Polygon key name', + type: 'string', + default: 'coordinates' + }, + editablePolygon: { + title: 'Enable polygon edit', + type: 'boolean', + default: false + }, + polygonColor: { + title: 'Polygon color', + type: 'string' + }, + polygonOpacity: { + title: 'Polygon opacity', + type: 'number', + default: 0.5 + }, + polygonStrokeColor: { + title: 'Stroke color', + type: 'string' + }, + polygonStrokeOpacity: { + title: 'Stroke opacity', + type: 'number', + default: 1 + }, + polygonStrokeWeight: { + title: 'Stroke weight', + type: 'number', + default: 1 + }, + showPolygonTooltip: { + title: 'Show polygon tooltip', + type: 'boolean', + default: false + }, + polygonTooltipPattern: { + title: 'Tooltip (for ex. \'Text ${keyName} units.\' or Link text\')', + type: 'string', + default: '${entityName}

TimeStamp: ${ts:7}' + }, + usePolygonTooltipFunction: { + title: 'Use polygon tooltip function', + type: 'boolean', + default: false + }, + polygonTooltipFunction: { + title: 'Polygon tooltip function: f(data, dsData, dsIndex)', + type: 'string' + }, + usePolygonColorFunction: { + title: 'Use polygon color function', + type: 'boolean', + default: false + }, + polygonColorFunction: { + title: 'Polygon Color function: f(data, dsData, dsIndex)', + type: 'string' + }, + }, + required: [] + }, + form: [ + 'showPolygon', + 'polygonKeyName', + 'editablePolygon', + { + key: 'polygonColor', + type: 'color' + }, + 'polygonOpacity', + { + key: 'polygonStrokeColor', + type: 'color' + }, + 'polygonStrokeOpacity', 'polygonStrokeWeight', 'showPolygonTooltip', + { + key: 'polygonTooltipPattern', + type: 'textarea' + }, 'usePolygonTooltipFunction', { + key: 'polygonTooltipFunction', + type: 'javascript' + }, + 'usePolygonColorFunction', + { + key: 'polygonColorFunction', + type: 'javascript' + }, + ] +}; + +export const routeMapSettingsSchema = +{ + schema: { + title: 'Route Map Configuration', + type: 'object', + properties: { + strokeWeight: { + title: 'Stroke weight', + type: 'number', + default: 2 + }, + strokeOpacity: { + title: 'Stroke opacity', + type: 'number', + default: 1.0 + } + }, + required: [] + }, + form: [ + 'strokeWeight', + 'strokeOpacity' + ] +}; + +export const markerClusteringSettingsSchema = +{ + schema: { + title: 'Markers Clustering Configuration', + type: 'object', + properties: { + useClusterMarkers: { + title: 'Use map markers clustering', + type: 'boolean', + default: false + } + }, + required: [] + }, + form: [ + { + key: 'useClusterMarkers', + condition: 'model.provider !== "image-map"' + }, + ] +}; + +export const markerClusteringSettingsSchemaLeaflet = +{ + schema: { + title: 'Markers Clustering Configuration Leaflet', + type: 'object', + properties: { + zoomOnClick: { + title: 'Zoom when clicking on a cluster', + type: 'boolean', + default: true + }, + maxZoom: { + title: 'The maximum zoom level when a marker can be part of a cluster (0 - 18)', + type: 'number' + }, + showCoverageOnHover: { + title: 'Show the bounds of markers when mouse over a cluster', + type: 'boolean', + default: true + }, + animate: { + title: 'Show animation on markers when zooming', + type: 'boolean', + default: true + }, + maxClusterRadius: { + title: 'Maximum radius that a cluster will cover in pixels', + type: 'number', + default: 80 + }, + chunkedLoading: { + title: 'Use chunks for adding markers so that the page does not freeze', + type: 'boolean', + default: false + }, + removeOutsideVisibleBounds: { + title: 'Use lazy load for adding markers', + type: 'boolean', + default: true + } + }, + required: [] + }, + form: [ + 'zoomOnClick', + 'maxZoom', + 'showCoverageOnHover', + 'animate', + 'maxClusterRadius', + 'chunkedLoading', + 'removeOutsideVisibleBounds' + ] +}; + +export const imageMapSettingsSchema = +{ + schema: { + title: 'Image Map Configuration', + type: 'object', + properties: { + mapImageUrl: { + title: 'Image map background', + type: 'string', + default: 'data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==' + }, + imageEntityAlias: { + title: 'Image URL source entity alias', + type: 'string', + default: '' + }, + imageUrlAttribute: { + title: 'Image URL source entity attribute', + type: 'string', + default: '' + } + }, + required: [] + }, + form: [ + { + key: 'mapImageUrl', + type: 'image' + }, + 'imageEntityAlias', + 'imageUrlAttribute' + ] +}; + +export const pathSchema = +{ + schema: { + title: 'Trip Animation Path Configuration', + type: 'object', + properties: { + color: { + title: 'Path color', + type: 'string' + }, + strokeWeight: { + title: 'Stroke weight', + type: 'number', + default: 2 + }, + strokeOpacity: { + title: 'Stroke opacity', + type: 'number', + default: 1 + }, + useColorFunction: { + title: 'Use path color function', + type: 'boolean', + default: false + }, + colorFunction: { + title: 'Path color function: f(data, dsData, dsIndex)', + type: 'string' + }, + usePolylineDecorator: { + title: 'Use path decorator', + type: 'boolean', + default: false + }, + decoratorSymbol: { + title: 'Decorator symbol', + type: 'string', + default: 'arrowHead' + }, + decoratorSymbolSize: { + title: 'Decorator symbol size (px)', + type: 'number', + default: 10 + }, + useDecoratorCustomColor: { + title: 'Use path decorator custom color', + type: 'boolean', + default: false + }, + decoratorCustomColor: { + title: 'Decorator custom color', + type: 'string', + default: '#000' + }, + decoratorOffset: { + title: 'Decorator offset', + type: 'string', + default: '20px' + }, + endDecoratorOffset: { + title: 'End decorator offset', + type: 'string', + default: '20px' + }, + decoratorRepeat: { + title: 'Decorator repeat', + type: 'string', + default: '20px' + } + }, + required: [] + }, + form: [ + { + key: 'color', + type: 'color' + }, 'useColorFunction', { + key: 'colorFunction', + type: 'javascript' + }, 'strokeWeight', 'strokeOpacity', + 'usePolylineDecorator', { + key: 'decoratorSymbol', + type: 'rc-select', + multiple: false, + items: [{ + value: 'arrowHead', + label: 'Arrow' + }, { + value: 'dash', + label: 'Dash' + }] + }, 'decoratorSymbolSize', 'useDecoratorCustomColor', { + key: 'decoratorCustomColor', + type: 'color' + }, { + key: 'decoratorOffset', + type: 'textarea' + }, { + key: 'endDecoratorOffset', + type: 'textarea' + }, { + key: 'decoratorRepeat', + type: 'textarea' + } + ] +}; + +export const pointSchema = +{ + schema: { + title: 'Trip Animation Path Configuration', + type: 'object', + properties: { + showPoints: { + title: 'Show points', + type: 'boolean', + default: false + }, + pointColor: { + title: 'Point color', + type: 'string' + }, + pointSize: { + title: 'Point size (px)', + type: 'number', + default: 10 + }, + usePointAsAnchor: { + title: 'Use point as anchor', + type: 'boolean', + default: false + }, + pointAsAnchorFunction: { + title: 'Point as anchor function: f(data, dsData, dsIndex)', + type: 'string' + }, + pointTooltipOnRightPanel: { + title: 'Independant point tooltip', + type: 'boolean', + default: true + }, + }, + required: [] + }, + form: [ + 'showPoints', + { + key: 'pointColor', + type: 'color' + }, + 'pointSize', + 'usePointAsAnchor', + { + key: 'pointAsAnchorFunction', + type: 'javascript' + }, + 'pointTooltipOnRightPanel', + ] +}; + +export const mapProviderSchema = +{ + schema: { + title: 'Map Provider Configuration', + type: 'object', + properties: { + provider: { + title: 'Map Provider', + type: 'string', + default: 'openstreet-map' + } + }, + required: [] + }, + form: [ + { + key: 'provider', + type: 'rc-select', + multiple: false, + items: [ + { + value: 'google-map', + label: 'Google maps' + }, + { + value: 'openstreet-map', + label: 'Openstreet maps' + }, + { + value: 'here', + label: 'HERE maps' + }, + { + value: 'image-map', + label: 'Image map' + }, + { + value: 'tencent-map', + label: 'Tencent maps' + } + ] + } + ] +}; + +export const tripAnimationSchema = { + schema: { + title: 'Openstreet Map Configuration', + type: 'object', + properties: { + normalizationStep: { + title: 'Normalization data step (ms)', + type: 'number', + default: 1000 + }, + latKeyName: { + title: 'Latitude key name', + type: 'string', + default: 'latitude' + }, + lngKeyName: { + title: 'Longitude key name', + type: 'string', + default: 'longitude' + }, + showLabel: { + title: 'Show label', + type: 'boolean', + default: true + }, + label: { + title: 'Label (pattern examples: \'${entityName}\', \'${entityName}: (Text ${keyName} units.)\' )', + type: 'string', + default: '${entityName}' + }, + useLabelFunction: { + title: 'Use label function', + type: 'boolean', + default: false + }, + labelFunction: { + title: 'Label function: f(data, dsData, dsIndex)', + type: 'string' + }, + showTooltip: { + title: 'Show tooltip', + type: 'boolean', + default: true + }, + tooltipColor: { + title: 'Tooltip background color', + type: 'string', + default: '#fff' + }, + tooltipFontColor: { + title: 'Tooltip font color', + type: 'string', + default: '#000' + }, + tooltipOpacity: { + title: 'Tooltip opacity (0-1)', + type: 'number', + default: 1 + }, + tooltipPattern: { + title: 'Tooltip (for ex. \'Text ${keyName} units.\' or Link text\')', + type: 'string', + default: '${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}' + }, + useTooltipFunction: { + title: 'Use tooltip function', + type: 'boolean', + default: false + }, + tooltipFunction: { + title: 'Tooltip function: f(data, dsData, dsIndex)', + type: 'string' + }, + autocloseTooltip: { + title: 'Auto-close point popup', + type: 'boolean', + default: true + }, + markerImage: { + title: 'Custom marker image', + type: 'string' + }, + markerImageSize: { + title: 'Custom marker image size (px)', + type: 'number', + default: 34 + }, + rotationAngle: { + title: 'Set additional rotation angle for marker (deg)', + type: 'number', + default: 0 + }, + useMarkerImageFunction: { + title: 'Use marker image function', + type: 'boolean', + default: false + }, + markerImageFunction: { + title: 'Marker image function: f(data, images, dsData, dsIndex)', + type: 'string' + }, + markerImages: { + title: 'Marker images', + type: 'array', + items: { + title: 'Marker image', + type: 'string' + } + } + }, + required: [] + }, + form: ['normalizationStep', 'latKeyName', 'lngKeyName', 'showLabel', 'label', 'useLabelFunction', { + key: 'labelFunction', + type: 'javascript' + }, 'showTooltip', { + key: 'tooltipColor', + type: 'color' + }, { + key: 'tooltipFontColor', + type: 'color' + }, 'tooltipOpacity', { + key: 'tooltipPattern', + type: 'textarea' + }, 'useTooltipFunction', { + key: 'tooltipFunction', + type: 'javascript' + }, 'autocloseTooltip', { + key: 'markerImage', + type: 'image' + }, 'markerImageSize', 'rotationAngle', 'useMarkerImageFunction', + { + key: 'markerImageFunction', + type: 'javascript' + }, { + key: 'markerImages', + items: [ + { + key: 'markerImages[]', + type: 'image' + } + ] + }] +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html new file mode 100644 index 0000000000..6a964fc524 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html @@ -0,0 +1,134 @@ + +
+
+
+ {{ getGroupTitle(source.datasource) }} + +
+
+
+ + {{key.label}} + + {{key.settings.icon}} + + {{ getErrorMessageText(key.settings, 'required') }} + + +
+
+ + {{key.label}} + + {{key.settings.icon}} + + {{ getErrorMessageText(key.settings,'required') }} + + + {{ getErrorMessageText(key.settings,'min') }} + + + {{ getErrorMessageText(key.settings,'max') }} + + +
+
+ + {{key.label}} + +
+
+ + {{key.label}} + +
+
+ + {{key.label}} + + + + + {{ getErrorMessageText(key.settings, 'required') }} + + + {{ getErrorMessageText(key.settings, 'invalidDate') }} + + +
+
+
+
+
+ + +
+
+
+
+ {{ 'widgets.input-widgets.no-entity-selected' | translate }} +
+
+ {{ 'widgets.input-widgets.not-allowed-entity' | translate }} +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss new file mode 100644 index 0000000000..a5afcfabf3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss @@ -0,0 +1,67 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:host { + + .tb-multiple-input { + height: 100%; + overflow-x: hidden; + overflow-y: auto; + + .fields-group { + padding: 0 8px; + margin: 10px 0; + border: 1px groove rgba(0, 0, 0, .25); + + legend { + color: rgba(0, 0, 0, .7); + } + } + + .input-field { + padding-right: 10px; + + mat-form-field { + margin-bottom: 5px; + } + } + + mat-checkbox, + mat-slide-toggle { + display: block; + margin-top: 20px; + margin-bottom: 16px; + .label-wrapper { + white-space: normal; + } + } + + mat-slide-toggle { + height: auto; + } + + .date-time-input { + mat-form-field { + width: 100%; + margin: 2px 0; + } + } + + .vertical-alignment { + flex-direction: column; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts new file mode 100644 index 0000000000..0403f2844c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts @@ -0,0 +1,561 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, Input, NgZone, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Overlay } from '@angular/cdk/overlay'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { DataKey, Datasource, DatasourceData, DatasourceType, WidgetConfig } from '@shared/models/widget.models'; +import { IWidgetSubscription } from '@core/api/widget-api.models'; +import { isDefined, isEqual, isUndefined, createLabelFromDatasource, isDefinedAndNotNull } from '@core/utils'; +import { EntityType } from '@shared/models/entity-type.models'; +import * as _moment from 'moment'; +import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms'; +import { RequestConfig } from '@core/http/http-utils'; +import { AttributeService } from '@core/http/attribute.service'; +import { AttributeData, AttributeScope, LatestTelemetry } from '@shared/models/telemetry/telemetry.models'; +import { forkJoin, Observable } from 'rxjs'; +import { EntityId } from '@shared/models/id/entity-id'; +import { ResizeObserver } from '@juggle/resize-observer'; + +type FieldAlignment = 'row' | 'column'; + +type MultipleInputWidgetDataKeyType = 'server' | 'shared' | 'timeseries'; +type MultipleInputWidgetDataKeyValueType = 'string' | 'double' | 'integer' | + 'booleanCheckbox' | 'booleanSwitch' | + 'dateTime' | 'date' | 'time'; +type MultipleInputWidgetDataKeyEditableType = 'editable' | 'disabled' | 'readonly'; + +interface MultipleInputWidgetSettings { + widgetTitle: string; + showActionButtons: boolean; + updateAllValues: boolean; + saveButtonLabel: string; + resetButtonLabel: string; + showResultMessage: boolean; + showGroupTitle: boolean; + groupTitle: string; + fieldsAlignment: FieldAlignment; + fieldsInRow: number; + attributesShared?: boolean; +} + +interface MultipleInputWidgetDataKeySettings { + dataKeyType: MultipleInputWidgetDataKeyType; + dataKeyValueType: MultipleInputWidgetDataKeyValueType; + required: boolean; + isEditable: MultipleInputWidgetDataKeyEditableType; + disabledOnDataKey: string; + dataKeyHidden: boolean; + step?: number; + minValue?: number; + maxValue?: number; + requiredErrorMessage?: string; + invalidDateErrorMessage?: string; + minValueErrorMessage?: string; + maxValueErrorMessage?: string; + icon: string; + inputTypeNumber?: boolean; + readOnly?: boolean; + disabledOnCondition?: boolean; +} + +interface MultipleInputWidgetDataKey extends DataKey { + formId?: string; + settings: MultipleInputWidgetDataKeySettings; + isFocused: boolean; + value?: any; +} + +interface MultipleInputWidgetSource { + datasource: Datasource; + keys: MultipleInputWidgetDataKey[]; +} + +@Component({ + selector: 'tb-multiple-input-widget ', + templateUrl: './multiple-input-widget.component.html', + styleUrls: ['./multiple-input-widget.component.scss'] +}) +export class MultipleInputWidgetComponent extends PageComponent implements OnInit, OnDestroy { + + @ViewChild('formContainer', {static: true}) formContainerRef: ElementRef; + + @Input() + ctx: WidgetContext; + + private formResize$: ResizeObserver; + private settings: MultipleInputWidgetSettings; + private widgetConfig: WidgetConfig; + private subscription: IWidgetSubscription; + private datasources: Array; + private sources: Array = []; + + isVerticalAlignment: boolean; + inputWidthSettings: string; + changeAlignment: boolean; + saveButtonLabel: string; + resetButtonLabel: string; + + entityDetected = false; + isAllParametersValid = true; + + multipleInputFormGroup: FormGroup; + + toastTargetId = 'multiple-input-widget' + this.utils.guid(); + + constructor(protected store: Store, + private elementRef: ElementRef, + private ngZone: NgZone, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, + private utils: UtilsService, + private fb: FormBuilder, + private attributeService: AttributeService, + private translate: TranslateService) { + super(store); + } + + ngOnInit(): void { + this.ctx.$scope.multipleInputWidget = this; + this.settings = this.ctx.settings; + this.widgetConfig = this.ctx.widgetConfig; + this.subscription = this.ctx.defaultSubscription; + this.datasources = this.subscription.datasources; + this.initializeConfig(); + this.updateDatasources(); + this.buildForm(); + this.ctx.updateWidgetParams(); + this.formResize$ = new ResizeObserver(() => { + this.resize(); + }); + this.formResize$.observe(this.formContainerRef.nativeElement); + } + + ngOnDestroy(): void { + if (this.formResize$) { + this.formResize$.disconnect(); + } + } + + private initializeConfig() { + + if (this.settings.widgetTitle && this.settings.widgetTitle.length) { + this.ctx.widgetTitle = this.utils.customTranslation(this.settings.widgetTitle, this.settings.widgetTitle); + } else { + this.ctx.widgetTitle = this.ctx.widgetConfig.title; + } + + this.settings.groupTitle = this.settings.groupTitle || '${entityName}'; + + if (this.settings.saveButtonLabel && this.settings.saveButtonLabel.length) { + this.saveButtonLabel = this.utils.customTranslation(this.settings.saveButtonLabel, this.settings.saveButtonLabel); + } else { + this.saveButtonLabel = this.translate.instant('action.save'); + } + if (this.settings.resetButtonLabel && this.settings.resetButtonLabel.length) { + this.resetButtonLabel = this.utils.customTranslation(this.settings.resetButtonLabel, this.settings.resetButtonLabel); + } else { + this.resetButtonLabel = this.translate.instant('action.undo'); + } + + // For backward compatibility + if (isUndefined(this.settings.showActionButtons)) { + this.settings.showActionButtons = true; + } + if (isUndefined(this.settings.fieldsAlignment)) { + this.settings.fieldsAlignment = 'row'; + } + if (isUndefined(this.settings.fieldsInRow)) { + this.settings.fieldsInRow = 2; + } + // For backward compatibility + + this.isVerticalAlignment = !(this.settings.fieldsAlignment === 'row'); + + if (!this.isVerticalAlignment && this.settings.fieldsInRow) { + this.inputWidthSettings = 100 / this.settings.fieldsInRow + '%'; + } + + this.updateWidgetDisplaying(); + } + + private updateDatasources() { + if (this.datasources && this.datasources.length) { + this.entityDetected = true; + let keyIndex = 0; + this.datasources.forEach((datasource) => { + const source: MultipleInputWidgetSource = { + datasource, + keys: [] + }; + if (datasource.type === DatasourceType.entity) { + datasource.dataKeys.forEach((dataKey: MultipleInputWidgetDataKey) => { + if ((datasource.entityType !== EntityType.DEVICE) && (dataKey.settings.dataKeyType === 'shared')) { + this.isAllParametersValid = false; + } + if (dataKey.units) { + dataKey.label += ' (' + dataKey.units + ')'; + } + dataKey.formId = (++keyIndex) + ''; + dataKey.isFocused = false; + + // For backward compatibility + if (isUndefined(dataKey.settings.dataKeyType)) { + if (this.settings.attributesShared) { + dataKey.settings.dataKeyType = 'shared'; + } else { + dataKey.settings.dataKeyType = 'server'; + } + } + + if (isUndefined(dataKey.settings.dataKeyValueType)) { + if (dataKey.settings.inputTypeNumber) { + dataKey.settings.dataKeyValueType = 'double'; + } else { + dataKey.settings.dataKeyValueType = 'string'; + } + } + + if (isUndefined(dataKey.settings.isEditable)) { + if (dataKey.settings.readOnly) { + dataKey.settings.isEditable = 'readonly'; + } else { + dataKey.settings.isEditable = 'editable'; + } + } + // For backward compatibility + + source.keys.push(dataKey); + }); + } else { + this.entityDetected = false; + } + this.sources.push(source); + }); + } + } + + private buildForm() { + this.multipleInputFormGroup = this.fb.group({}); + this.sources.forEach((source) => { + for (const key of this.visibleKeys(source)) { + const validators: ValidatorFn[] = []; + if (key.settings.required) { + validators.push(Validators.required); + } + if (key.settings.dataKeyValueType === 'integer') { + validators.push(Validators.pattern(/^-?[0-9]+$/)); + } + if (key.settings.dataKeyValueType === 'integer' || key.settings.dataKeyValueType === 'double') { + if (isDefinedAndNotNull(key.settings.minValue) && isFinite(key.settings.minValue)) { + validators.push(Validators.min(key.settings.minValue)); + } + if (isDefinedAndNotNull(key.settings.maxValue) && isFinite(key.settings.maxValue)) { + validators.push(Validators.max(key.settings.maxValue)); + } + } + const formControl = this.fb.control( + { value: key.value, + disabled: key.settings.isEditable === 'disabled' || key.settings.disabledOnCondition}, + validators + ); + this.multipleInputFormGroup.addControl(key.formId, formControl); + } + }); + } + + private updateWidgetData(data: Array) { + let dataIndex = 0; + this.sources.forEach((source) => { + source.keys.forEach((key) => { + const keyData = data[dataIndex].data; + if (keyData && keyData.length) { + let value; + switch (key.settings.dataKeyValueType) { + case 'dateTime': + case 'date': + if (isDefinedAndNotNull(keyData[0][1]) && keyData[0][1] !== '') { + value = _moment(keyData[0][1]).toDate(); + } else { + value = null; + } + break; + case 'time': + value = _moment().startOf('day').add(keyData[0][1], 'ms').toDate(); + break; + case 'booleanCheckbox': + case 'booleanSwitch': + value = (keyData[0][1] === 'true'); + break; + default: + value = keyData[0][1]; + } + key.value = value; + } + + if (key.settings.isEditable === 'editable' && key.settings.disabledOnDataKey) { + const conditions = data.filter((item) => { + return source.datasource === item.datasource && item.dataKey.name === key.settings.disabledOnDataKey; + }); + if (conditions && conditions.length) { + if (conditions[0].data.length) { + if (conditions[0].data[0][1] === 'false') { + key.settings.disabledOnCondition = true; + } else { + key.settings.disabledOnCondition = !conditions[0].data[0][1]; + } + } + } + } + + if (!key.settings.dataKeyHidden) { + if (key.settings.isEditable === 'disabled' || key.settings.disabledOnCondition) { + this.multipleInputFormGroup.get(key.formId).disable({emitEvent: false}); + } else { + this.multipleInputFormGroup.get(key.formId).enable({emitEvent: false}); + } + const dirty = this.multipleInputFormGroup.get(key.formId).dirty; + if (!key.isFocused && !dirty) { + this.multipleInputFormGroup.get(key.formId).patchValue(key.value, {emitEvent: false}); + } + } + dataIndex++; + }); + }); + } + + private updateWidgetDisplaying() { + this.changeAlignment = (this.ctx.$container && this.ctx.$container[0].offsetWidth < 620); + } + + public onDataUpdated() { + this.updateWidgetData(this.subscription.data); + this.ctx.detectChanges(); + } + + private resize() { + this.updateWidgetDisplaying(); + this.ctx.detectChanges(); + } + + public getGroupTitle(datasource: Datasource): string { + const groupTitle = createLabelFromDatasource(datasource, this.settings.groupTitle); + return this.utils.customTranslation(groupTitle, groupTitle); + } + + public getErrorMessageText(keySettings: MultipleInputWidgetDataKeySettings, errorType: string): string { + let errorMessage; + let defaultMessage; + let messageValues; + switch (errorType) { + case 'required': + errorMessage = keySettings.requiredErrorMessage; + defaultMessage = ''; + break; + case 'min': + errorMessage = keySettings.minValueErrorMessage; + defaultMessage = 'widgets.input-widgets.min-value-error'; + messageValues = { + value: keySettings.minValue + }; + break; + case 'max': + errorMessage = keySettings.maxValueErrorMessage; + defaultMessage = 'widgets.input-widgets.max-value-error'; + messageValues = { + value: keySettings.maxValue + }; + break; + case 'invalidDate': + errorMessage = keySettings.invalidDateErrorMessage; + defaultMessage = 'widgets.input-widgets.invalid-date'; + break; + default: + return ''; + } + return this.getTranslatedErrorText(errorMessage, defaultMessage, messageValues); + } + + public getTranslatedErrorText(errorMessage: string, defaultMessage: string, messageValues?: object): string { + let messageText; + if (errorMessage && errorMessage.length) { + messageText = this.utils.customTranslation(errorMessage, errorMessage); + } else if (defaultMessage && defaultMessage.length) { + if (!messageValues) { + messageValues = {}; + } + messageText = this.translate.instant(defaultMessage, messageValues); + } else { + messageText = ''; + } + return messageText; + } + + public visibleKeys(source: MultipleInputWidgetSource): MultipleInputWidgetDataKey[] { + return source.keys.filter(key => !key.settings.dataKeyHidden); + } + + public datePickerType(keyType: MultipleInputWidgetDataKeyValueType): string { + switch (keyType) { + case 'dateTime': + return 'datetime'; + case 'date': + return 'date'; + case 'time': + return 'time'; + } + } + + public focusInputElement($event: Event) { + ($event.target as HTMLInputElement).select(); + } + + public inputChanged(source: MultipleInputWidgetSource, key: MultipleInputWidgetDataKey) { + if (!this.settings.showActionButtons) { + const currentValue = this.multipleInputFormGroup.get(key.formId).value; + if (!key.settings.required || (key.settings.required && isDefined(currentValue))) { + const dataToSave: MultipleInputWidgetSource = { + datasource: source.datasource, + keys: [key] + }; + this.save(dataToSave); + } + } + } + + public save(dataToSave?: MultipleInputWidgetSource) { + if (document && document.activeElement) { + (document.activeElement as HTMLElement).blur(); + } + const config: RequestConfig = { + ignoreLoading: !this.settings.showActionButtons + }; + let data: Array; + if (dataToSave) { + data = [dataToSave]; + } else { + data = this.sources; + } + const tasks: Observable[] = []; + data.forEach((toSave) => { + const serverAttributes: AttributeData[] = []; + const sharedAttributes: AttributeData[] = []; + const telemetry: AttributeData[] = []; + for (const key of this.visibleKeys(toSave)) { + const currentValue = this.multipleInputFormGroup.get(key.formId).value; + if (!isEqual(currentValue, key.value) || this.settings.updateAllValues) { + const attribute: AttributeData = { + key: key.name, + value: null + }; + if (currentValue) { + switch (key.settings.dataKeyValueType) { + case 'dateTime': + case 'date': + attribute.value = currentValue.getTime(); + break; + case 'time': + attribute.value = currentValue.getTime() - _moment().startOf('day').valueOf(); + break; + default: + attribute.value = currentValue; + } + } else { + if (currentValue === '') { + attribute.value = null; + } else { + attribute.value = currentValue; + } + } + + switch (key.settings.dataKeyType) { + case 'shared': + sharedAttributes.push(attribute); + break; + case 'timeseries': + telemetry.push(attribute); + break; + default: + serverAttributes.push(attribute); + } + } + } + const entityId: EntityId = { + entityType: toSave.datasource.entityType, + id: toSave.datasource.entityId + }; + if (serverAttributes.length) { + tasks.push(this.attributeService.saveEntityAttributes( + entityId, + AttributeScope.SERVER_SCOPE, + serverAttributes, + config + )); + } + if (sharedAttributes.length) { + tasks.push(this.attributeService.saveEntityAttributes( + entityId, + AttributeScope.SHARED_SCOPE, + sharedAttributes, + config + )); + } + if (telemetry.length) { + tasks.push(this.attributeService.saveEntityTimeseries( + entityId, + LatestTelemetry.LATEST_TELEMETRY, + telemetry, + config + )); + } + }); + if (tasks.length) { + forkJoin(tasks).subscribe( + () => { + this.multipleInputFormGroup.markAsPristine(); + this.ctx.detectChanges(); + if (this.settings.showResultMessage) { + this.ctx.showSuccessToast(this.translate.instant('widgets.input-widgets.update-successful'), + 1000, 'bottom', 'left', this.toastTargetId); + } + }, + () => { + if (this.settings.showResultMessage) { + this.ctx.showErrorToast(this.translate.instant('widgets.input-widgets.update-failed'), + 'bottom', 'left', this.toastTargetId); + } + }); + } else { + this.multipleInputFormGroup.markAsPristine(); + this.ctx.detectChanges(); + } + } + + public discardAll() { + this.multipleInputFormGroup.reset(undefined, {emitEvent: false}); + this.sources.forEach((source) => { + for (const key of this.visibleKeys(source)) { + this.multipleInputFormGroup.get(key.formId).patchValue(key.value, {emitEvent: false}); + } + }); + this.multipleInputFormGroup.markAsPristine(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.html new file mode 100644 index 0000000000..b3b457849b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.html @@ -0,0 +1,65 @@ + +
+
+
+
+ widgets.input-widgets.no-image + last photo +
+ +
+
+
+ +
+
+ + + +
+
+
+
+ preview photo + +
+
+ + +
+
+
+
+
+
+ {{ textMessage | translate }} +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.scss new file mode 100644 index 0000000000..ea364e17a1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.scss @@ -0,0 +1,82 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-web-camera { + height: 100%; + + &__last-photo { + width: 100%; + min-height: 0; + margin: 5px 0; + text-align: center; + border: solid 1px; + + &_text { + position: absolute; + top: 50%; + left: 50%; + margin-top: -.625em; + transform: translate(-50%, -50%); + } + + &_img { + width: 100%; + height: 100%; + object-fit: contain; + } + } + + .camera-container{ + height: 100%; + } + + .camera { + position: relative; + width: 100%; + height: 100%; + overflow: hidden; + + .camera-stream { + display: block; + width: 100%; + height: 100%; + object-fit: contain; + } + + .camera-controls { + position: absolute; + bottom: 0; + width: 100%; + padding: 0 5px 5px; + + .mat-button-base{ + margin: 6px 8px; + } + } + } + + .message-text { + font-size: 18px; + color: #a0a0a0; + text-align: center; + } + + .image-container{ + height: 100%; + min-height: 0; + width: 100%; + min-width: 100%; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.ts new file mode 100644 index 0000000000..aebc500faf --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.ts @@ -0,0 +1,320 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + Component, + ElementRef, + Inject, + Input, + NgZone, + OnDestroy, + OnInit, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Overlay } from '@angular/cdk/overlay'; +import { UtilsService } from '@core/services/utils.service'; +import { Datasource, DatasourceData, DatasourceType } from '@shared/models/widget.models'; +import { WINDOW } from '@core/services/window.service'; +import { AttributeService } from '@core/http/attribute.service'; +import { EntityId } from '@shared/models/id/entity-id'; +import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { Observable } from 'rxjs'; +import { isString } from '@core/utils'; +import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; + +interface PhotoCameraInputWidgetSettings { + widgetTitle: string; + imageQuality: number; + imageFormat: string; + maxWidth: number; + maxHeight: number; +} + +// @dynamic +@Component({ + selector: 'tb-photo-camera-widget', + templateUrl: './photo-camera-input.component.html', + styleUrls: ['./photo-camera-input.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class PhotoCameraInputWidgetComponent extends PageComponent implements OnInit, OnDestroy { + + constructor(@Inject(WINDOW) private window: Window, + protected store: Store, + private elementRef: ElementRef, + private ngZone: NgZone, + private overlay: Overlay, + private utils: UtilsService, + private attributeService: AttributeService, + private sanitizer: DomSanitizer + ) { + super(store); + } + + public get videoElement() { + return this.videoStreamRef.nativeElement; + } + + public get canvasElement() { + return this.canvasRef.nativeElement; + } + + public get videoWidth() { + const videoRatio = this.getVideoAspectRatio(); + return Math.min(this.width, this.height * videoRatio); + } + + public get videoHeight() { + const videoRatio = this.getVideoAspectRatio(); + return Math.min(this.height, this.width / videoRatio); + } + + private static DEFAULT_IMAGE_TYPE = 'image/jpeg'; + private static DEFAULT_IMAGE_QUALITY = 0.92; + + @Input() + ctx: WidgetContext; + + @ViewChild('videoStream', {static: false}) videoStreamRef: ElementRef; + @ViewChild('canvas', {static: false}) canvasRef: ElementRef; + + private videoInputsIndex = 0; + private settings: PhotoCameraInputWidgetSettings; + private datasource: Datasource; + private width = 640; + private height = 480; + private availableVideoInputs: MediaDeviceInfo[]; + private mediaStream: MediaStream; + + isEntityDetected = false; + dataKeyDetected = false; + isProtocolHttps = false; + isCameraSupport = false; + isDeviceDetect = false; + isShowCamera = false; + isPreviewPhoto = false; + isHavePermissionCamera = true; + isLoading = false; + singleDevice = true; + updatePhoto = false; + previewPhoto: SafeUrl; + lastPhoto: SafeUrl; + + private static hasGetUserMedia(): boolean { + return !!(window.navigator.mediaDevices && window.navigator.mediaDevices.getUserMedia); + } + + private static getAvailableVideoInputs(): Promise { + return new Promise((resolve, reject) => { + navigator.mediaDevices.enumerateDevices() + .then((devices: MediaDeviceInfo[]) => { + resolve(devices.filter((device: MediaDeviceInfo) => device.kind === 'videoinput')); + }) + .catch(err => { + reject(err.message || err); + }); + }); + } + + ngOnInit(): void { + this.ctx.$scope.photoCameraInputWidget = this; + this.isLoading = true; + this.settings = this.ctx.settings; + this.datasource = this.ctx.datasources[0]; + + if (this.settings.widgetTitle && this.settings.widgetTitle.length) { + this.ctx.widgetTitle = this.utils.customTranslation(this.settings.widgetTitle, this.settings.widgetTitle); + } else { + this.ctx.widgetTitle = this.ctx.widgetConfig.title; + } + + this.width = this.settings.maxWidth ? this.settings.maxWidth : 640; + this.height = this.settings.maxHeight ? this.settings.maxWidth : 480; + + if (this.datasource.type === DatasourceType.entity) { + if (this.datasource.entityType && this.datasource.entityId) { + this.isEntityDetected = true; + } + } + if (this.datasource.dataKeys.length) { + this.dataKeyDetected = true; + } + this.detectAvailableDevices(); + } + + ngOnDestroy(): void { + this.stopMediaTracks(); + } + + private updateWidgetData(data: Array) { + const keyData = data[0].data; + if (keyData?.length && isString(keyData[0][1]) && keyData[0][1].startsWith('data:image/')) { + this.lastPhoto = this.sanitizer.bypassSecurityTrustUrl(keyData[0][1]); + } + } + + public onDataUpdated() { + this.updateWidgetData(this.ctx.defaultSubscription.data); + this.ctx.detectChanges(); + } + + + private detectAvailableDevices(): void { + if (this.window.location.protocol === 'https:' || this.window.location.hostname === 'localhost') { + this.isProtocolHttps = true; + + if (PhotoCameraInputWidgetComponent.hasGetUserMedia()) { + this.isCameraSupport = true; + PhotoCameraInputWidgetComponent.getAvailableVideoInputs().then((devices) => { + this.isLoading = false; + this.isDeviceDetect = !!devices.length; + this.singleDevice = devices.length < 2; + this.availableVideoInputs = devices; + this.ctx.detectChanges(); + }, () => { + this.isLoading = false; + this.availableVideoInputs = []; + } + ); + } else { + this.isLoading = false; + } + } else { + this.isLoading = false; + } + } + + private getVideoAspectRatio(): number { + if (this.videoElement.videoWidth && this.videoElement.videoWidth > 0 && + this.videoElement.videoHeight && this.videoElement.videoHeight > 0) { + return this.videoElement.videoWidth / this.videoElement.videoHeight; + } + return this.width / this.height; + } + + private stopMediaTracks() { + if (this.mediaStream && this.mediaStream.getTracks) { + this.mediaStream.getTracks() + .forEach((track: MediaStreamTrack) => track.stop()); + } + } + + takePhoto() { + this.inititedVideoStream(this.availableVideoInputs[this.videoInputsIndex].deviceId, true); + } + + closeCamera() { + this.stopMediaTracks(); + this.videoElement.srcObject = null; + this.isShowCamera = false; + } + + cancelPhoto() { + this.isPreviewPhoto = false; + this.previewPhoto = ''; + } + + savePhoto() { + this.updatePhoto = true; + let task: Observable; + const entityId: EntityId = { + entityType: this.datasource.entityType, + id: this.datasource.entityId + }; + const saveData = [{ + key: this.datasource.dataKeys[0].name, + value: this.previewPhoto + }]; + if (this.datasource.dataKeys[0].type === DataKeyType.attribute) { + task = this.attributeService.saveEntityAttributes(entityId, AttributeScope.SERVER_SCOPE, saveData); + } else if (this.datasource.dataKeys[0].type === DataKeyType.timeseries) { + task = this.attributeService.saveEntityTimeseries(entityId, 'scope', saveData); + } + task.subscribe(() => { + this.isPreviewPhoto = false; + this.updatePhoto = false; + this.closeCamera(); + }, () => { + this.updatePhoto = false; + }); + } + + switchWebCamera() { + this.videoInputsIndex = (this.videoInputsIndex + 1) % this.availableVideoInputs.length; + this.stopMediaTracks(); + this.inititedVideoStream(this.availableVideoInputs[this.videoInputsIndex].deviceId); + } + + createPhoto() { + this.canvasElement.width = this.videoWidth; + this.canvasElement.height = this.videoHeight; + this.canvasElement.getContext('2d').drawImage(this.videoElement, 0, 0, this.videoWidth, this.videoHeight); + + const mimeType: string = this.settings.imageFormat ? this.settings.imageFormat : PhotoCameraInputWidgetComponent.DEFAULT_IMAGE_TYPE; + const quality: number = this.settings.imageQuality ? this.settings.imageQuality : PhotoCameraInputWidgetComponent.DEFAULT_IMAGE_QUALITY; + this.previewPhoto = this.canvasElement.toDataURL(mimeType, quality); + this.isPreviewPhoto = true; + } + + private inititedVideoStream(deviceId?: string, init = false) { + if (window.navigator.mediaDevices && window.navigator.mediaDevices.getUserMedia) { + const videoTrackConstraints = { + video: {deviceId: deviceId !== '' ? {exact: deviceId} : undefined} + }; + + window.navigator.mediaDevices.getUserMedia(videoTrackConstraints).then((stream: MediaStream) => { + if (init) { + this.isShowCamera = true; + } + this.mediaStream = stream; + this.videoElement.srcObject = stream; + this.ctx.detectChanges(); + }, () => { + this.isHavePermissionCamera = false; + }); + } + } + + get textMessage() { + if (this.isLoading) { + return ''; + } + if (!this.isProtocolHttps) { + return 'widgets.input-widgets.enable-https-use-widget'; + } + if (!this.isCameraSupport) { + return 'widgets.input-widgets.no-support-web-camera'; + } + if (!this.isEntityDetected) { + return 'widgets.input-widgets.no-entity-selected'; + } + if (!this.dataKeyDetected) { + return 'widgets.input-widgets.no-datakey-selected'; + } + if (!this.isDeviceDetect) { + return 'widgets.input-widgets.no-found-your-camera'; + } + if (!this.isHavePermissionCamera) { + return 'widgets.input-widgets.no-permission-camera'; + } + return null; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.html new file mode 100644 index 0000000000..38ac3f67c5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.html @@ -0,0 +1,45 @@ + +
+
+
+
+ {{ value }} +
+
+ +
+
+
+
+
+ +
+
+ {{ title }} +
+
+ min + + max +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.scss new file mode 100644 index 0000000000..af0bdf158b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.scss @@ -0,0 +1,165 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +$knob-img: url("./svg/knob.svg") !default; + +$bars-margin-pct: percentage(.033) !default; +$background-margin-pct: percentage(.05) !default; +$value-container-margin-pct: percentage(.35) !default; +$error-height: percentage(.05) !default; +$title-height: percentage(.066) !default; +$title-container-margin-pct: percentage(.2) !default; +$title-container-margin-bottom-pct: percentage(.05) !default; +$minmax-height: percentage(.04) !default; +$minmax-container-margin-pct: percentage(.18) !default; +$minmax-container-margin-bottom-pct: percentage(.12) !default; + +:host { + .tb-knob { + width: 100%; + height: 100%; + + .knob { + position: relative; + + &[draggable] { + user-select: none; + } + + #canvasBar { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 2; + } + + .canvas-background { + position: absolute; + top: $background-margin-pct; + right: $background-margin-pct; + bottom: $background-margin-pct; + left: $background-margin-pct; + z-index: 2; + background: #3f4346; + border-radius: 50%; + } + + .value-container { + position: absolute; + top: $value-container-margin-pct; + right: $value-container-margin-pct; + bottom: $value-container-margin-pct; + left: $value-container-margin-pct; + z-index: 4; + + .knob-value { + font-weight: 500; + color: #fff; + white-space: nowrap; + } + } + + .error-container { + position: absolute; + top: 1%; + right: 0; + left: 0; + z-index: 4; + height: $error-height; + + .knob-error { + color: #ff3315; + white-space: nowrap; + } + } + + .title-container { + position: absolute; + right: $title-container-margin-pct; + bottom: $title-container-margin-bottom-pct; + left: $title-container-margin-pct; + z-index: 4; + height: $title-height; + + .knob-title { + font-weight: 500; + color: #757575; + white-space: nowrap; + } + } + + .minmax-container { + position: absolute; + right: $minmax-container-margin-pct; + bottom: $minmax-container-margin-bottom-pct; + left: $minmax-container-margin-pct; + z-index: 4; + height: $minmax-height; + + .minmax-label { + font-weight: 500; + color: #757575; + white-space: nowrap; + } + } + + .top-pointer-container { + position: absolute; + top: $bars-margin-pct; + right: $bars-margin-pct; + bottom: $bars-margin-pct; + left: $bars-margin-pct; + z-index: 3; + cursor: pointer !important; + + .top-pointer { + position: absolute; + top: 50%; + left: 22%; + width: 5%; + height: 5%; + margin-top: -2.5%; + cursor: pointer !important; + content: ""; + background-color: #b5b5b5; + border-radius: 50%; + box-shadow: 1px 0 2px #040404; + } + } + + .top { + position: absolute; + top: $bars-margin-pct; + right: $bars-margin-pct; + bottom: $bars-margin-pct; + left: $bars-margin-pct; + z-index: 2; + cursor: pointer !important; + background: $knob-img no-repeat; + background-size: contain; + } + + #text-measure { + position: absolute; + width: auto; + height: auto; + white-space: nowrap; + visibility: hidden; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.ts new file mode 100644 index 0000000000..f5f48c86c1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.ts @@ -0,0 +1,446 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { UtilsService } from '@core/services/utils.service'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { isDefined, isNumber } from '@core/utils'; +import { CanvasDigitalGauge, CanvasDigitalGaugeOptions } from '@home/components/widget/lib/canvas-digital-gauge'; +import * as tinycolor_ from 'tinycolor2'; +import { ResizeObserver } from '@juggle/resize-observer'; +import GenericOptions = CanvasGauges.GenericOptions; + +const tinycolor = tinycolor_; + +interface KnobSettings { + minValue: number; + maxValue: number; + initialValue: number; + title: string; + getValueMethod: string; + setValueMethod: string; + requestTimeout: number; +} + +@Component({ + selector: 'tb-knob', + templateUrl: './knob.component.html', + styleUrls: ['./knob.component.scss'] +}) +export class KnobComponent extends PageComponent implements OnInit, OnDestroy { + + @ViewChild('knob', {static: true}) knobRef: ElementRef; + @ViewChild('knobContainer', {static: true}) knobContainerRef: ElementRef; + @ViewChild('knobTopPointerContainer', {static: true}) knobTopPointerContainerRef: ElementRef; + @ViewChild('knobTopPointer', {static: true}) knobTopPointerRef: ElementRef; + @ViewChild('knobValueContainer', {static: true}) knobValueContainerRef: ElementRef; + @ViewChild('knobValue', {static: true}) knobValueRef: ElementRef; + @ViewChild('knobErrorContainer', {static: true}) knobErrorContainerRef: ElementRef; + @ViewChild('knobError', {static: true}) knobErrorRef: ElementRef; + @ViewChild('knobTitleContainer', {static: true}) knobTitleContainerRef: ElementRef; + @ViewChild('knobTitle', {static: true}) knobTitleRef: ElementRef; + @ViewChild('knobMinmaxContainer', {static: true}) knobMinmaxContainerRef: ElementRef; + @ViewChild('textMeasure', {static: true}) textMeasureRef: ElementRef; + @ViewChild('canvasBar', {static: true}) canvasBarElementRef: ElementRef; + + @Input() + ctx: WidgetContext; + + value = '0'; + error = ''; + title = ''; + minValue: number; + maxValue: number; + newValue = 0; + + private startDeg = -1; + private currentDeg = 0; + private rotation = 0; + private lastDeg = 0; + private moving = false; + + private minDeg = -45; + private maxDeg = 225; + + private isSimulated: boolean; + private requestTimeout: number; + private getValueMethod: string; + private setValueMethod: string; + + private executingUpdateValue: boolean; + private scheduledValue: number; + private rpcValue: number; + + private knob: JQuery; + private knobContainer: JQuery; + private knobTopPointerContainer: JQuery; + private knobTopPointer: JQuery; + private knobValueContainer: JQuery; + private knobValue: JQuery; + private knobTitleContainer: JQuery; + private knobTitle: JQuery; + private knobErrorContainer: JQuery; + private knobError: JQuery; + private knobMinmaxContainer: JQuery; + private minmaxLabel: JQuery; + private textMeasure: JQuery; + private canvasBarElement: HTMLElement; + + private canvasBar: CanvasDigitalGauge; + + private knobResize$: ResizeObserver; + + constructor(private utils: UtilsService, + protected store: Store) { + super(store); + } + + ngOnInit(): void { + this.knob = $(this.knobRef.nativeElement); + this.knobContainer = $(this.knobContainerRef.nativeElement); + this.knobTopPointerContainer = $(this.knobTopPointerContainerRef.nativeElement); + this.knobTopPointer = $(this.knobTopPointerRef.nativeElement); + this.knobValueContainer = $(this.knobValueContainerRef.nativeElement); + this.knobValue = $(this.knobValueRef.nativeElement); + this.knobTitleContainer = $(this.knobTitleContainerRef.nativeElement); + this.knobTitle = $(this.knobTitleRef.nativeElement); + this.knobErrorContainer = $(this.knobErrorContainerRef.nativeElement); + this.knobError = $(this.knobErrorRef.nativeElement); + this.knobMinmaxContainer = $(this.knobMinmaxContainerRef.nativeElement); + this.minmaxLabel = this.knobMinmaxContainer.find('.minmax-label'); + this.textMeasure = $(this.textMeasureRef.nativeElement); + this.canvasBarElement = this.canvasBarElementRef.nativeElement; + + this.knobResize$ = new ResizeObserver(() => { + this.resize(); + }); + this.knobResize$.observe(this.knobContainerRef.nativeElement); + this.init(); + } + + ngOnDestroy(): void { + if (this.knobResize$) { + this.knobResize$.disconnect(); + } + } + + private init() { + const settings: KnobSettings = this.ctx.settings; + + this.minValue = isDefined(settings.minValue) ? settings.minValue : 0; + this.maxValue = isDefined(settings.maxValue) ? settings.maxValue : 100; + this.title = isDefined(settings.title) ? settings.title : ''; + + const levelColors = ['#19ff4b', '#ffff19', '#ff3232']; + + const canvasBarData: CanvasDigitalGaugeOptions = { + renderTo: this.canvasBarElement, + hideValue: true, + neonGlowBrightness: 0, + gaugeWidthScale: 0.4, + gaugeColor: 'rgba(0, 0, 0, 0)', + levelColors, + minValue: this.minValue, + maxValue: this.maxValue, + gaugeType: 'donut', + dashThickness: 2, + donutStartAngle: 3/4*Math.PI, + donutEndAngle: 9/4*Math.PI, + animation: false + }; + + this.canvasBar = new CanvasDigitalGauge(canvasBarData).draw(); + + this.knob.on('click', (e) => { + if (this.moving) { + this.moving = false; + return false; + } + e.preventDefault(); + + const offset = this.knob.offset(); + const center = { + y: offset.top + this.knob.height() / 2, + x: offset.left + this.knob.width() / 2 + }; + const rad2deg = 180 / Math.PI; + const t: Touch = ((e.originalEvent as any).touches) ? (e.originalEvent as any).touches[0] : e; + const a = center.y - t.pageY; + const b = center.x - t.pageX; + let deg = Math.atan2(a, b) * rad2deg; + if (deg < 0) { + deg = 360 + deg; + } + if (deg > this.maxDeg) { + if (deg - 360 > this.minDeg) { + deg = deg - 360; + } else { + return false; + } + } + this.currentDeg = deg; + this.lastDeg = deg; + this.knobTopPointerContainer.css('transform', 'rotate(' + (this.currentDeg) + 'deg)'); + this.turn(this.degreeToRatio(this.currentDeg)); + this.rotation = this.currentDeg; + this.startDeg = -1; + this.rpcUpdateValue(this.newValue); + }); + + + + this.knob.on('mousedown touchstart', (e) => { + this.moving = false; + e.preventDefault(); + const offset = this.knob.offset(); + const center = { + y : offset.top + this.knob.height()/2, + x: offset.left + this.knob.width()/2 + }; + const rad2deg = 180/Math.PI; + + $(document).on('mousemove.rem touchmove.rem', (ev) => { + this.moving = true; + const t: Touch = ((ev.originalEvent as any).touches) ? (ev.originalEvent as any).touches[0] : ev; + + const a = center.y - t.pageY; + const b = center.x - t.pageX; + let deg = Math.atan2(a,b)*rad2deg; + if(deg < 0){ + deg = 360 + deg; + } + + if(this.startDeg === -1){ + this.startDeg = deg; + } + + let tmp = Math.floor((deg-this.startDeg) + this.rotation); + + if(tmp < 0){ + tmp = 360 + tmp; + } + else if(tmp > 359){ + tmp = tmp % 360; + } + + if (tmp > this.maxDeg) { + if (tmp - 360 > this.minDeg) { + tmp = tmp - 360; + } else { + const deltaMax = Math.abs(this.maxDeg - this.lastDeg); + const deltaMin = Math.abs(this.minDeg - this.lastDeg); + if (deltaMax < deltaMin) { + tmp = this.maxDeg; + } else { + tmp = this.minDeg; + } + } + } + if(Math.abs(tmp - this.lastDeg) > 180){ + this.startDeg = deg; + this.rotation = this.currentDeg; + return false; + } + + this.currentDeg = tmp; + this.lastDeg = tmp; + + this.knobTopPointerContainer.css('transform','rotate('+(this.currentDeg)+'deg)'); + this.turn(this.degreeToRatio(this.currentDeg)); + }); + + $(document).on('mouseup.rem touchend.rem',() => { + if(this.newValue !== this.rpcValue && this.moving) { + this.rpcUpdateValue(this.newValue); + } + this.knob.off('.rem'); + $(document).off('.rem'); + this.rotation = this.currentDeg; + this.startDeg = -1; + }); + + }); + + const initialValue = isDefined(settings.initialValue) ? settings.initialValue : this.minValue; + this.setValue(initialValue); + + const subscription = this.ctx.defaultSubscription; + const rpcEnabled = subscription.rpcEnabled; + + this.isSimulated = this.utils.widgetEditMode; + + this.requestTimeout = 500; + if (settings.requestTimeout) { + this.requestTimeout = settings.requestTimeout; + } + this.getValueMethod = 'getValue'; + if (settings.getValueMethod && settings.getValueMethod.length) { + this.getValueMethod = settings.getValueMethod; + } + this.setValueMethod = 'setValue'; + if (settings.setValueMethod && settings.setValueMethod.length) { + this.setValueMethod = settings.setValueMethod; + } + if (!rpcEnabled) { + this.onError('Target device is not set!'); + } else { + if (!this.isSimulated) { + this.rpcRequestValue(); + } + } + } + + private degreeToRatio(degree: number): number { + return (degree-this.minDeg)/(this.maxDeg-this.minDeg); + } + + private ratioToDegree(ratio: number): number { + return this.minDeg + ratio*(this.maxDeg-this.minDeg); + } + + private turn(ratio: number) { + this.newValue = Number((this.minValue + (this.maxValue - this.minValue)*ratio).toFixed(this.ctx.decimals)); + if (this.canvasBar.value !== this.newValue) { + this.canvasBar.value = this.newValue; + } + this.updateColor(this.canvasBar.getValueColor()); + this.onValue(this.newValue); + } + + private resize() { + const width = this.knobContainer.width(); + const height = this.knobContainer.height(); + const size = Math.min(width, height); + this.knob.css({width: size, height: size}); + this.canvasBar.update({width: size, height: size} as GenericOptions); + this.setFontSize(this.knobTitle, this.title, this.knobTitleContainer.height(), this.knobTitleContainer.width()); + this.setFontSize(this.knobError, this.error, this.knobErrorContainer.height(), this.knobErrorContainer.width()); + const minmaxHeight = this.knobMinmaxContainer.height(); + this.minmaxLabel.css({fontSize: minmaxHeight+'px', lineHeight: minmaxHeight+'px'}); + this.checkValueSize(); + } + + private checkValueSize() { + const fontSize = this.knobValueContainer.height()/3.3; + const containerWidth = this.knobValueContainer.width(); + this.setFontSize(this.knobValue, this.value+'', fontSize, containerWidth); + } + + private setFontSize(element: JQuery, text: string, fontSize: number, maxWidth: number) { + let textWidth = this.measureTextWidth(text, fontSize); + while (textWidth > maxWidth) { + fontSize--; + if (fontSize < 0) { + break; + } + textWidth = this.measureTextWidth(text, fontSize); + } + element.css({fontSize: fontSize+'px', lineHeight: fontSize+'px'}); + } + + private measureTextWidth(text: string, fontSize: number): number { + this.textMeasure.css({fontSize: fontSize+'px', lineHeight: fontSize+'px'}); + this.textMeasure.html(text); + return this.textMeasure.width(); + } + + private setValue(value: number) { + const ratio = (value-this.minValue) / (this.maxValue - this.minValue); + this.rotation = this.lastDeg = this.currentDeg = this.ratioToDegree(ratio); + this.knobTopPointerContainer.css('transform','rotate('+(this.currentDeg)+'deg)'); + if (this.canvasBar.value !== value) { + this.canvasBar.value = value; + } + this.updateColor(this.canvasBar.getValueColor()); + this.value = this.formatValue(value); + this.checkValueSize(); + this.ctx.detectChanges(); + } + + private updateColor(color: string) { + const glowColor = tinycolor(color).brighten(30).toHexString(); + this.knobValue.css({color: glowColor}); + const textShadow = `${color} 1px 1px 10px, ${glowColor} 1px 1px 10px`; + this.knobValue.css({textShadow}); + this.knobTopPointer.css({backgroundColor: glowColor}); + const boxShadow = `inset 1px 0 2px #040404, 1px 1px 8px 2px ${glowColor}`; + this.knobTopPointer.css({boxShadow}); + } + + private onValue(value: number) { + this.value = this.formatValue(value); + this.checkValueSize(); + this.ctx.detectChanges(); + } + + private formatValue(value: any): string { + return this.ctx.utils.formatValue(value, this.ctx.decimals, this.ctx.units, true); + } + + private rpcRequestValue() { + this.error = ''; + this.ctx.controlApi.sendTwoWayCommand(this.getValueMethod, null, this.requestTimeout).subscribe( + (responseBody) => { + if (isNumber(responseBody)) { + const numValue = Number(Number(responseBody).toFixed(this.ctx.decimals)); + this.setValue(numValue); + } else { + const errorText = `Unable to parse response: ${responseBody}`; + this.onError(errorText); + } + }, + () => { + const errorText = this.ctx.defaultSubscription.rpcErrorText; + this.onError(errorText); + } + ); + } + + private rpcUpdateValue(value: number) { + if (this.executingUpdateValue) { + this.scheduledValue = value; + return; + } else { + this.scheduledValue = null; + this.rpcValue = value; + this.executingUpdateValue = true; + } + this.error = ''; + this.ctx.controlApi.sendOneWayCommand(this.setValueMethod, value, this.requestTimeout).subscribe( + () => { + this.executingUpdateValue = false; + if (this.scheduledValue != null && this.scheduledValue !== this.rpcValue) { + this.rpcUpdateValue(this.scheduledValue); + } + }, + () => { + this.executingUpdateValue = false; + const errorText = this.ctx.defaultSubscription.rpcErrorText; + this.onError(errorText); + } + ); + } + + private onError(error: string) { + this.error = error; + this.setFontSize(this.knobError, this.error, this.knobErrorContainer.height(), this.knobErrorContainer.width()); + this.ctx.detectChanges(); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.html new file mode 100644 index 0000000000..58f2337843 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.html @@ -0,0 +1,32 @@ + +
+
+ {{title}} +
+
+
+
+
+
+ {{ error }} +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.scss new file mode 100644 index 0000000000..bf484a847d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.scss @@ -0,0 +1,71 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "node_modules/compass-sass-mixins/lib/compass"; + +$error-height: 14px !default; + +:host { + .tb-led-indicator { + width: 100%; + height: 100%; + + .title-container { + .led-title { + font-weight: 500; + color: #757575; + white-space: nowrap; + } + } + + .error-container { + position: absolute; + top: 1%; + right: 0; + left: 0; + z-index: 4; + height: $error-height; + + .led-error { + color: #ff3315; + white-space: nowrap; + } + } + + #text-measure { + position: absolute; + width: auto; + height: auto; + white-space: nowrap; + visibility: hidden; + } + + #led-container { + padding: 10px; + + .led { + position: relative; + cursor: pointer; + background-image: radial-gradient(circle closest-corner at 50% 50%, transparent, rgba(0, 0, 0, .25)); + border-radius: 50%; + transition: background-color .5s, box-shadow .5s; + + &.disabled { + background-image: radial-gradient(circle closest-corner at 50% 50%, rgba(255, 255, 255, .5), rgba(0, 0, 0, .1)); + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts new file mode 100644 index 0000000000..efdf87bc6b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts @@ -0,0 +1,347 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { isDefined } from '@core/utils'; +import * as tinycolor_ from 'tinycolor2'; +import { UtilsService } from '@core/services/utils.service'; +import { IWidgetSubscription, SubscriptionInfo, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; +import { DatasourceType, widgetType } from '@shared/models/widget.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { ResizeObserver } from '@juggle/resize-observer'; +import Timeout = NodeJS.Timeout; + +const tinycolor = tinycolor_; + +const checkStatusPollingInterval = 10000; + +type RetrieveValueMethod = 'attribute' | 'timeseries'; + +interface LedIndicatorSettings { + initialValue: boolean; + title: string; + ledColor: string; + performCheckStatus: boolean; + checkStatusMethod: string; + retrieveValueMethod: RetrieveValueMethod; + valueAttribute: string; + parseValueFunction: string; + requestTimeout: number; +} + +@Component({ + selector: 'tb-led-indicator', + templateUrl: './led-indicator.component.html', + styleUrls: ['./led-indicator.component.scss'] +}) +export class LedIndicatorComponent extends PageComponent implements OnInit, OnDestroy { + + @ViewChild('led', {static: true}) ledRef: ElementRef; + @ViewChild('ledContainer', {static: true}) ledContainerRef: ElementRef; + @ViewChild('textMeasure', {static: true}) textMeasureRef: ElementRef; + @ViewChild('ledTitleContainer', {static: true}) ledTitleContainerRef: ElementRef; + @ViewChild('ledTitle', {static: true}) ledTitleRef: ElementRef; + @ViewChild('ledErrorContainer', {static: true}) ledErrorContainerRef: ElementRef; + @ViewChild('ledError', {static: true}) ledErrorRef: ElementRef; + + @Input() + ctx: WidgetContext; + + showTitle = false; + value = false; + error = ''; + title = ''; + + private valueAttribute: string; + private ledColor: string; + private ledMiddleColor: string; + private disabledColor: string; + private disabledMiddleColor: string; + + private isSimulated: boolean; + private requestTimeout: number; + private retrieveValueMethod: RetrieveValueMethod; + private parseValueFunction: (data: any) => boolean; + private performCheckStatus: boolean; + private checkStatusMethod: string; + + private destroyed = false; + private checkStatusTimeoutHandle: Timeout; + private subscription: IWidgetSubscription; + + private led: JQuery; + private ledContainer: JQuery; + private textMeasure: JQuery; + private ledTitleContainer: JQuery; + private ledTitle: JQuery; + private ledErrorContainer: JQuery; + private ledError: JQuery; + + private ledResize$: ResizeObserver; + + private subscriptionOptions: WidgetSubscriptionOptions = { + callbacks: { + onDataUpdated: (subscription, detectChanges) => this.ctx.ngZone.run(() => { + this.onDataUpdated(subscription, detectChanges); + }), + onDataUpdateError: (subscription, e) => this.ctx.ngZone.run(() => { + this.onDataUpdateError(subscription, e); + }), + dataLoading: () => {} + } + }; + + + constructor(private utils: UtilsService, + protected store: Store) { + super(store); + } + + ngOnInit(): void { + this.led = $(this.ledRef.nativeElement); + this.ledContainer = $(this.ledContainerRef.nativeElement); + this.textMeasure = $(this.textMeasureRef.nativeElement); + this.ledTitleContainer = $(this.ledTitleContainerRef.nativeElement); + this.ledTitle = $(this.ledTitleRef.nativeElement); + this.ledErrorContainer = $(this.ledErrorContainerRef.nativeElement); + this.ledError = $(this.ledErrorRef.nativeElement); + + this.ledResize$ = new ResizeObserver(() => { + this.resize(); + }); + this.ledResize$.observe(this.ledContainerRef.nativeElement); + this.init(); + } + + ngOnDestroy(): void { + this.destroyed = true; + if (this.checkStatusTimeoutHandle) { + clearTimeout(this.checkStatusTimeoutHandle); + } + if (this.subscription) { + this.ctx.subscriptionApi.removeSubscription(this.subscription.id); + } + if (this.ledResize$) { + this.ledResize$.disconnect(); + } + } + + private init() { + const settings: LedIndicatorSettings = this.ctx.settings; + this.title = isDefined(settings.title) ? settings.title : ''; + this.showTitle = !!(this.title && this.title.length); + + const origColor = isDefined(settings.ledColor) ? settings.ledColor : 'green'; + this.valueAttribute = isDefined(settings.valueAttribute) ? settings.valueAttribute : 'value'; + + this.ledColor = tinycolor(origColor).brighten(30).toHexString(); + this.ledMiddleColor = tinycolor(origColor).toHexString(); + this.disabledColor = tinycolor(origColor).darken(40).toHexString(); + this.disabledMiddleColor = tinycolor(origColor).darken(60).toHexString(); + + const initialValue = isDefined(settings.initialValue) ? settings.initialValue : false; + this.setValue(initialValue, true); + + const subscription = this.ctx.defaultSubscription; + const rpcEnabled = subscription.rpcEnabled; + + this.isSimulated = this.utils.widgetEditMode; + + this.requestTimeout = 500; + if (settings.requestTimeout) { + this.requestTimeout = settings.requestTimeout; + } + this.retrieveValueMethod = 'attribute'; + if (settings.retrieveValueMethod && settings.retrieveValueMethod.length) { + this.retrieveValueMethod = settings.retrieveValueMethod; + } + this.parseValueFunction = (data) => !!data; + if (settings.parseValueFunction && settings.parseValueFunction.length) { + try { + this.parseValueFunction = new Function('data', settings.parseValueFunction) as (data: any) => boolean; + } catch (e) { + this.parseValueFunction = (data) => !!data; + } + } + this.performCheckStatus = settings.performCheckStatus !== false; + if (this.performCheckStatus) { + this.checkStatusMethod = 'checkStatus'; + if (settings.checkStatusMethod && settings.checkStatusMethod.length) { + this.checkStatusMethod = settings.checkStatusMethod; + } + } + if (!rpcEnabled) { + this.onError('Target device is not set!'); + } else { + if (!this.isSimulated) { + if (this.performCheckStatus) { + this.rpcCheckStatus(); + } else { + this.subscribeForValue(); + } + } + } + } + + private resize() { + const width = this.ledContainer.width(); + const height = this.ledContainer.height(); + const size = Math.min(width, height); + + this.led.css({width: size, height: size}); + + if (this.showTitle) { + this.setFontSize(this.ledTitle, this.title, this.ledTitleContainer.height() * 2 / 3, this.ledTitleContainer.width()); + } + this.setFontSize(this.ledError, this.error, this.ledErrorContainer.height(), this.ledErrorContainer.width()); + } + + private setFontSize(element: JQuery, text: string, fontSize: number, maxWidth: number) { + let textWidth = this.measureTextWidth(text, fontSize); + while (textWidth > maxWidth) { + fontSize--; + textWidth = this.measureTextWidth(text, fontSize); + } + element.css({fontSize: fontSize+'px', lineHeight: fontSize+'px'}); + } + + private measureTextWidth(text: string, fontSize: number): number { + this.textMeasure.css({fontSize: fontSize+'px', lineHeight: fontSize+'px'}); + this.textMeasure.text(text); + return this.textMeasure.width(); + } + + private onError(error: string) { + this.error = error; + this.setFontSize(this.ledError, this.error, this.ledErrorContainer.height(), this.ledErrorContainer.width()); + this.ctx.detectChanges(); + } + + private setValue(value: boolean, forceUpdate?: boolean) { + if (this.value !== value || forceUpdate) { + this.value = value; + this.updateColor(); + } + } + + private updateColor() { + const color = this.value ? this.ledColor : this.disabledColor; + const middleColor = this.value ? this.ledMiddleColor : this.disabledMiddleColor; + const boxShadow = `#000 0 -1px 6px 1px, inset ${middleColor} 0 -1px 8px, ${color} 0 3px 11px`; + this.led.css({backgroundColor: color}); + this.led.css({boxShadow}); + if (this.value) { + this.led.removeClass( 'disabled' ); + } else { + this.led.addClass( 'disabled' ); + } + } + + private rpcCheckStatus() { + if (this.destroyed) { + return; + } + this.error = ''; + this.ctx.controlApi.sendTwoWayCommand(this.checkStatusMethod, null, this.requestTimeout).subscribe( + (responseBody) => { + const status = !!responseBody; + if (status) { + if (this.checkStatusTimeoutHandle) { + clearTimeout(this.checkStatusTimeoutHandle); + this.checkStatusTimeoutHandle = null; + } + this.subscribeForValue(); + } else { + const errorText = 'Unknown device status!'; + this.onError(errorText); + if (this.checkStatusTimeoutHandle) { + clearTimeout(this.checkStatusTimeoutHandle); + } + this.checkStatusTimeoutHandle = setTimeout(this.rpcCheckStatus.bind(this), checkStatusPollingInterval); + } + }, + () => { + const errorText = this.ctx.defaultSubscription.rpcErrorText; + this.onError(errorText); + if (this.checkStatusTimeoutHandle) { + clearTimeout(this.checkStatusTimeoutHandle); + } + this.checkStatusTimeoutHandle = setTimeout(this.rpcCheckStatus.bind(this), checkStatusPollingInterval); + } + ); + } + + private subscribeForValue() { + const subscriptionsInfo: SubscriptionInfo[] = [{ + type: DatasourceType.entity, + entityType: EntityType.DEVICE, + entityId: this.ctx.defaultSubscription.targetDeviceId + }]; + + if (this.retrieveValueMethod === 'attribute') { + subscriptionsInfo[0].attributes = [ + {name: this.valueAttribute} + ]; + } else { + subscriptionsInfo[0].timeseries = [ + {name: this.valueAttribute} + ]; + } + + this.ctx.subscriptionApi.createSubscriptionFromInfo ( + widgetType.latest, subscriptionsInfo, this.subscriptionOptions, false, true).subscribe( + (subscription) => { + this.subscription = subscription; + } + ); + } + + private onDataUpdated (subscription: IWidgetSubscription, detectChanges: boolean) { + let value = false; + const data = subscription.data; + if (data.length) { + const keyData = data[0]; + if (keyData && keyData.data && keyData.data[0]) { + const attrValue = keyData.data[0][1]; + if (isDefined(attrValue)) { + let parsed = null; + try { + parsed = this.parseValueFunction(JSON.parse(attrValue)); + } catch (e){/**/} + value = !!parsed; + } + } + } + this.setValue(value); + if (detectChanges) { + this.ctx.detectChanges(); + } + } + + private onDataUpdateError(subscription: IWidgetSubscription, e: any) { + const exceptionData = this.utils.parseException(e); + let errorText = exceptionData.name; + if (exceptionData.message) { + errorText += ': ' + exceptionData.message; + } + this.onError(errorText); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.html new file mode 100644 index 0000000000..692ca2379c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.html @@ -0,0 +1,38 @@ + +
+
+ {{title}} +
+
+
+ +
+ +
+
+
+
+ {{ error }} +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.scss new file mode 100644 index 0000000000..f083096b24 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.scss @@ -0,0 +1,195 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "node_modules/compass-sass-mixins/lib/compass"; + +$error-height: 14px !default; + +:host { + .tb-round-switch { + width: 100%; + height: 100%; + + .title-container { + .switch-title { + font-weight: 500; + color: #757575; + white-space: nowrap; + } + } + + .error-container { + position: absolute; + top: 1%; + right: 0; + left: 0; + z-index: 4; + height: $error-height; + + .switch-error { + color: #ff3315; + white-space: nowrap; + } + } + + #text-measure { + position: absolute; + width: auto; + height: auto; + white-space: nowrap; + visibility: hidden; + } + + #switch-container { + padding: 10px; + + .switch { + position: relative; + + box-sizing: border-box; + width: 260px; + min-width: 260px; + height: 260px; + min-height: 260px; + padding: 25px; + font-family: sans-serif; + font-size: 48px; + + color: #424242; + cursor: pointer; + background: #ddd; + background: linear-gradient(180deg, #bbb, #ddd); + border-radius: 130px; + + box-shadow: 0 0 0 8px rgba(0, 0, 0, .1), + 0 0 3px 1px rgba(0, 0, 0, .1), + inset 0 8px 3px -8px rgba(255, 255, 255, .4); + + input { + display: none; + } + + .on, + .off { + position: absolute; + width: 100%; + text-align: center; + + text-shadow: 1px 1px 4px #4a4a4a; + } + + .on { + top: 10px; + font-family: sans-serif; + color: #444; + + transition: all .1s; + } + + .off { + bottom: 5px; + + transition: all .1s; + + transform: scaleY(.85); + } + + .but { + position: relative; + display: block; + width: 200px; + height: 178px; + font-size: 48px; + cursor: pointer; + background-color: #d8d8d8; + border-bottom-width: 0; + border-radius: 400px 400px 400px 400px / 400px 400px 300px 300px; + + box-shadow: inset 8px 6px 5px -7px #a2a2a2, + inset -8px 6px 5px -7px #a2a2a2, + inset 0 -3px 2px -2px rgba(200, 200, 200, .5), + 0 3px 3px -2px #fff, + inset 0 -230px 60px -200px rgba(255, 255, 255, .2), + inset 0 220px 40px -200px rgba(0, 0, 0, .3); + + transition: all .2s; + } + + .back { + + box-sizing: border-box; + width: 210px; + height: 210px; + padding: 4px 4px; + cursor: pointer; + background-color: #888787; + background-image: linear-gradient(-90deg, transparent 30%, transparent 70%), linear-gradient(0deg, rgba(150, 150, 150, 0) 30%, rgba(150, 150, 150, .2) 50%, rgba(150, 150, 150, 0) 70%); + border-radius: 105px; + + box-shadow: 30px 30px 30px -20px rgba(58, 58, 58, .3), + -30px 30px 30px -20px rgba(58, 58, 58, .3), + 0 30px 30px 0 rgba(16, 16, 16, .3), + inset 0 -1px 0 0 #484848; + + transition: all .2s; + } + + + input:checked + .back .on, + input:checked + .back .off { + text-shadow: 1px 1px 4px #4a4a4a; + } + + input:checked + .back .on { + top: 10px; + color: #4c4c4c; + + transform: scaleY(.85); + } + + input:checked + .back .off { + bottom: 5px; + color: #444; + + transform: scaleY(1); + } + + input:checked + .back .but { + margin-top: 20px; + background: #dcdcdc radial-gradient(circle closest-corner at 50% 15%, rgba(0, 0, 0, .3), transparent); + border-radius: 400px 400px 400px 400px / 300px 300px 400px 400px; + + box-shadow: inset 8px -4px 5px -7px #a9a9a9, + inset -8px -4px 5px -7px #808080, + 0 -3px 8px -4px rgba(50, 50, 50, .4), + inset 0 3px 4px -2px #9c9c9c, + inset 0 280px 40px -200px rgba(0, 0, 0, .2), + inset 0 -200px 40px -200px rgba(180, 180, 180, .2); + } + + input:checked + .back { + padding: 2px 4px; + + background-image: linear-gradient(0deg, #868686 30%, transparent 70%), linear-gradient(90deg, rgba(115, 115, 115, 0) 0%, rgba(255, 255, 255, .74) 50%, rgba(105, 105, 105, 0) 100%); + + box-shadow: 30px 30px 30px -20px rgba(49, 49, 49, .1), + -30px 30px 30px -20px rgba(111, 111, 111, .1), + 0 30px 30px 0 rgba(0, 0, 0, .2), + inset 0 1px 2px 0 rgba(167, 167, 167, .6); + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.ts new file mode 100644 index 0000000000..a8ed46e529 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.ts @@ -0,0 +1,342 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { UtilsService } from '@core/services/utils.service'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { isDefined } from '@core/utils'; +import { IWidgetSubscription, SubscriptionInfo, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; +import { DatasourceType, widgetType } from '@shared/models/widget.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { ResizeObserver } from '@juggle/resize-observer'; + +type RetrieveValueMethod = 'rpc' | 'attribute' | 'timeseries'; + +interface RoundSwitchSettings { + initialValue: boolean; + title: string; + retrieveValueMethod: RetrieveValueMethod; + valueKey: string; + getValueMethod: string; + setValueMethod: string; + parseValueFunction: string; + convertValueFunction: string; + requestTimeout: number; +} + +@Component({ + selector: 'tb-round-switch', + templateUrl: './round-switch.component.html', + styleUrls: ['./round-switch.component.scss'] +}) +export class RoundSwitchComponent extends PageComponent implements OnInit, OnDestroy { + + @ViewChild('switch', {static: true}) switchElementRef: ElementRef; + @ViewChild('switchContainer', {static: true}) switchContainerRef: ElementRef; + @ViewChild('onoff', {static: true}) onoffRef: ElementRef; + @ViewChild('textMeasure', {static: true}) textMeasureRef: ElementRef; + @ViewChild('switchTitleContainer', {static: true}) switchTitleContainerRef: ElementRef; + @ViewChild('switchTitle', {static: true}) switchTitleRef: ElementRef; + @ViewChild('switchErrorContainer', {static: true}) switchErrorContainerRef: ElementRef; + @ViewChild('switchError', {static: true}) switchErrorRef: ElementRef; + + @Input() + ctx: WidgetContext; + + showTitle = false; + value = false; + error = ''; + title = ''; + checkboxId = 'onoff-' + this.utils.guid(); + + private isSimulated: boolean; + private requestTimeout: number; + private retrieveValueMethod: RetrieveValueMethod; + private valueKey: string; + private parseValueFunction: (data: any) => boolean; + private convertValueFunction: (value: any) => any; + private getValueMethod: string; + private setValueMethod: string; + + private valueSubscription: IWidgetSubscription; + + private executingUpdateValue: boolean; + private scheduledValue: boolean; + private rpcValue: boolean; + + private switchElement: JQuery; + private switchContainer: JQuery; + private onoff: JQuery; + private textMeasure: JQuery; + private switchTitleContainer: JQuery; + private switchTitle: JQuery; + private switchErrorContainer: JQuery; + private switchError: JQuery; + + private switchResize$: ResizeObserver; + + constructor(private utils: UtilsService, + protected store: Store) { + super(store); + } + + ngOnInit(): void { + this.switchElement = $(this.switchElementRef.nativeElement); + this.switchContainer = $(this.switchContainerRef.nativeElement); + this.onoff = $(this.onoffRef.nativeElement); + this.textMeasure = $(this.textMeasureRef.nativeElement); + this.switchTitleContainer = $(this.switchTitleContainerRef.nativeElement); + this.switchTitle = $(this.switchTitleRef.nativeElement); + this.switchErrorContainer = $(this.switchErrorContainerRef.nativeElement); + this.switchError = $(this.switchErrorRef.nativeElement); + + this.onoff.on('change', () => { + this.value = this.onoff.prop('checked') === false; + this.onValue(); + }); + + this.switchResize$ = new ResizeObserver(() => { + this.resize(); + }); + this.switchResize$.observe(this.switchContainerRef.nativeElement); + this.init(); + } + + ngOnDestroy(): void { + if (this.valueSubscription) { + this.ctx.subscriptionApi.removeSubscription(this.valueSubscription.id); + } + if (this.switchResize$) { + this.switchResize$.disconnect(); + } + } + + private init() { + const settings: RoundSwitchSettings = this.ctx.settings; + this.title = isDefined(settings.title) ? settings.title : ''; + this.showTitle = !!(this.title && this.title.length); + const initialValue = isDefined(settings.initialValue) ? settings.initialValue : false; + this.setValue(initialValue); + + const subscription = this.ctx.defaultSubscription; + const rpcEnabled = subscription.rpcEnabled; + + this.isSimulated = this.utils.widgetEditMode; + + this.requestTimeout = 500; + if (settings.requestTimeout) { + this.requestTimeout = settings.requestTimeout; + } + this.retrieveValueMethod = 'rpc'; + if (settings.retrieveValueMethod && settings.retrieveValueMethod.length) { + this.retrieveValueMethod = settings.retrieveValueMethod; + } + this.valueKey = 'value'; + if (settings.valueKey && settings.valueKey.length) { + this.valueKey = settings.valueKey; + } + this.parseValueFunction = (data) => !!data; + if (settings.parseValueFunction && settings.parseValueFunction.length) { + try { + this.parseValueFunction = new Function('data', settings.parseValueFunction) as (data: any) => boolean; + } catch (e) { + this.parseValueFunction = (data) => !!data; + } + } + this.convertValueFunction = (value) => value; + if (settings.convertValueFunction && settings.convertValueFunction.length) { + try { + this.convertValueFunction = new Function('value', settings.convertValueFunction) as (value: any) => any; + } catch (e) { + this.convertValueFunction = (value) => value; + } + } + this.getValueMethod = 'getValue'; + if (settings.getValueMethod && settings.getValueMethod.length) { + this.getValueMethod = settings.getValueMethod; + } + this.setValueMethod = 'setValue'; + if (settings.setValueMethod && settings.setValueMethod.length) { + this.setValueMethod = settings.setValueMethod; + } + if (!rpcEnabled) { + this.onError('Target device is not set!'); + } else { + if (!this.isSimulated) { + if (this.retrieveValueMethod === 'rpc') { + this.rpcRequestValue(); + } else if (this.retrieveValueMethod === 'attribute' || this.retrieveValueMethod === 'timeseries') { + this.subscribeForValue(); + } + } + } + + } + + private resize() { + const width = this.switchContainer.width(); + const height = this.switchContainer.height(); + const size = Math.min(width, height); + const scale = size/260; + this.switchElement.css({ + '-webkit-transform': `scale(${scale})`, + '-moz-transform': `scale(${scale})`, + '-ms-transform': `scale(${scale})`, + '-o-transform': `scale(${scale})`, + transform: `scale(${scale})` + }); + if (this.showTitle) { + this.setFontSize(this.switchTitle, this.title, this.switchTitleContainer.height() * 2 / 3, this.switchTitleContainer.width()); + } + this.setFontSize(this.switchError, this.error, this.switchErrorContainer.height(), this.switchErrorContainer.width()); + } + + private setFontSize(element: JQuery, text: string, fontSize: number, maxWidth: number) { + let textWidth = this.measureTextWidth(text, fontSize); + while (textWidth > maxWidth) { + fontSize--; + textWidth = this.measureTextWidth(text, fontSize); + } + element.css({fontSize: fontSize+'px', lineHeight: fontSize+'px'}); + } + + private measureTextWidth(text: string, fontSize: number): number { + this.textMeasure.css({fontSize: fontSize+'px', lineHeight: fontSize+'px'}); + this.textMeasure.text(text); + return this.textMeasure.width(); + } + + private onError(error: string) { + this.error = error; + this.setFontSize(this.switchError, this.error, this.switchErrorContainer.height(), this.switchErrorContainer.width()); + this.ctx.detectChanges(); + } + + private setValue(value: boolean) { + this.value = value ? true : false; + this.onoff.prop('checked', !this.value); + } + + private onValue() { + this.rpcUpdateValue(this.value); + } + + private rpcRequestValue() { + this.error = ''; + this.ctx.controlApi.sendTwoWayCommand(this.getValueMethod, null, this.requestTimeout).subscribe( + (responseBody) => { + this.setValue(this.parseValueFunction(responseBody)); + }, + () => { + const errorText = this.ctx.defaultSubscription.rpcErrorText; + this.onError(errorText); + } + ); + } + + private rpcUpdateValue(value) { + if (this.executingUpdateValue) { + this.scheduledValue = value; + return; + } else { + this.scheduledValue = null; + this.rpcValue = value; + this.executingUpdateValue = true; + } + this.error = ''; + this.ctx.controlApi.sendOneWayCommand(this.setValueMethod, this.convertValueFunction(value), this.requestTimeout).subscribe( + () => { + this.executingUpdateValue = false; + if (this.scheduledValue != null && this.scheduledValue !== this.rpcValue) { + this.rpcUpdateValue(this.scheduledValue); + } + }, + () => { + this.executingUpdateValue = false; + const errorText = this.ctx.defaultSubscription.rpcErrorText; + this.onError(errorText); + } + ); + } + + private subscribeForValue() { + const valueSubscriptionInfo: SubscriptionInfo[] = [{ + type: DatasourceType.entity, + entityType: EntityType.DEVICE, + entityId: this.ctx.defaultSubscription.targetDeviceId + }]; + if (this.retrieveValueMethod === 'attribute') { + valueSubscriptionInfo[0].attributes = [ + {name: this.valueKey} + ]; + } else { + valueSubscriptionInfo[0].timeseries = [ + {name: this.valueKey} + ]; + } + const subscriptionOptions: WidgetSubscriptionOptions = { + callbacks: { + onDataUpdated: (subscription, detectChanges) => this.ctx.ngZone.run(() => { + this.onDataUpdated(subscription, detectChanges); + }), + onDataUpdateError: (subscription, e) => this.ctx.ngZone.run(() => { + this.onDataUpdateError(subscription, e); + }) + } + }; + this.ctx.subscriptionApi.createSubscriptionFromInfo ( + widgetType.latest, valueSubscriptionInfo, subscriptionOptions, false, true).subscribe( + (subscription) => { + this.valueSubscription = subscription; + } + ); + } + + private onDataUpdated(subscription: IWidgetSubscription, detectChanges: boolean) { + let value = false; + const data = subscription.data; + if (data.length) { + const keyData = data[0]; + if (keyData && keyData.data && keyData.data[0]) { + const attrValue = keyData.data[0][1]; + if (isDefined(attrValue)) { + let parsed = null; + try { + parsed = this.parseValueFunction(JSON.parse(attrValue)); + } catch (e){/**/} + value = !!parsed; + } + } + } + this.setValue(value); + if (detectChanges) { + this.ctx.detectChanges(); + } + } + + private onDataUpdateError(subscription: IWidgetSubscription, e: any) { + const exceptionData = this.utils.parseException(e); + let errorText = exceptionData.name; + if (exceptionData.message) { + errorText += ': ' + exceptionData.message; + } + this.onError(errorText); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/rpc-widgets.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/rpc-widgets.module.ts new file mode 100644 index 0000000000..17f4ece4ef --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/rpc-widgets.module.ts @@ -0,0 +1,44 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@app/shared/shared.module'; +import { LedIndicatorComponent } from '@home/components/widget/lib/rpc/led-indicator.component'; +import { RoundSwitchComponent } from '@home/components/widget/lib/rpc/round-switch.component'; +import { SwitchComponent } from '@home/components/widget/lib/rpc/switch.component'; +import { KnobComponent } from '@home/components/widget/lib/rpc/knob.component'; + +@NgModule({ + declarations: + [ + LedIndicatorComponent, + RoundSwitchComponent, + SwitchComponent, + KnobComponent + ], + imports: [ + CommonModule, + SharedModule + ], + exports: [ + LedIndicatorComponent, + RoundSwitchComponent, + SwitchComponent, + KnobComponent + ] +}) +export class RpcWidgetsModule { } diff --git a/ui/src/app/widget/lib/rpc/svg/knob.svg b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/svg/knob.svg similarity index 100% rename from ui/src/app/widget/lib/rpc/svg/knob.svg rename to ui-ngx/src/app/modules/home/components/widget/lib/rpc/svg/knob.svg diff --git a/ui/src/app/widget/lib/rpc/svg/thumb-bar-checked.svg b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/svg/thumb-bar-checked.svg similarity index 100% rename from ui/src/app/widget/lib/rpc/svg/thumb-bar-checked.svg rename to ui-ngx/src/app/modules/home/components/widget/lib/rpc/svg/thumb-bar-checked.svg diff --git a/ui/src/app/widget/lib/rpc/svg/thumb-bar.svg b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/svg/thumb-bar.svg similarity index 100% rename from ui/src/app/widget/lib/rpc/svg/thumb-bar.svg rename to ui-ngx/src/app/modules/home/components/widget/lib/rpc/svg/thumb-bar.svg diff --git a/ui/src/app/widget/lib/rpc/svg/thumb-checked.svg b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/svg/thumb-checked.svg similarity index 100% rename from ui/src/app/widget/lib/rpc/svg/thumb-checked.svg rename to ui-ngx/src/app/modules/home/components/widget/lib/rpc/svg/thumb-checked.svg diff --git a/ui/src/app/widget/lib/rpc/svg/thumb.svg b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/svg/thumb.svg similarity index 100% rename from ui/src/app/widget/lib/rpc/svg/thumb.svg rename to ui-ngx/src/app/modules/home/components/widget/lib/rpc/svg/thumb.svg diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.html new file mode 100644 index 0000000000..bcba4a7c37 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.html @@ -0,0 +1,39 @@ + +
+
+ {{ error }} +
+
+ {{title}} +
+
+
+ + +
+
+ OFF + + ON + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.scss new file mode 100644 index 0000000000..68bea36407 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.scss @@ -0,0 +1,146 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +$thumb-img: url("./svg/thumb.svg") !default; +$thumb-checked-img: url("./svg/thumb-checked.svg") !default; +$thumb-bar-img: url("./svg/thumb-bar.svg") !default; +$thumb-bar-checked-img: url("./svg/thumb-bar-checked.svg") !default; + +$error-height: 14px !default; + +:host { + + .tb-switch { + width: 100%; + height: 100%; + + .error-container { + position: absolute; + top: 1%; + right: 0; + left: 0; + z-index: 4; + height: $error-height; + + .switch-error { + color: #ff3315; + white-space: nowrap; + } + } + + .onoff-container { + height: 100%; + font-weight: 500; + color: #757575; + white-space: nowrap; + + .off-label { + color: #b7b5b5; + } + + .on-label { + color: #ff7e57; + text-shadow: #ff6e4a 1px 1px 10px, #ffd1c3 1px 1px 10px; + } + } + + .title-container { + .switch-title { + font-weight: 500; + color: #757575; + white-space: nowrap; + } + } + + #switch-container { + padding-right: 10px; + padding-left: 10px; + } + + .switch { + position: relative; + } + + #text-measure { + position: absolute; + width: auto; + height: auto; + white-space: nowrap; + visibility: hidden; + } + } +} + +:host ::ng-deep { + .tb-switch { + .switch { + mat-slide-toggle { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + margin: 0; + + .mat-slide-toggle-bar { + top: 0; + left: 0; + width: 100%; + height: 100%; + background: $thumb-bar-img no-repeat; + background-size: contain; + border-radius: 0; + } + + .mat-slide-toggle-thumb-container { + top: 5%; + left: .25%; + width: 50%; + height: 90%; + } + + .mat-slide-toggle-label{ + height: 100%; + } + .mat-slide-toggle-thumb { + top: 0; + left: 0; + width: 100%; + height: 100%; + background: $thumb-img no-repeat; + background-size: contain; + border-radius: 0; + box-shadow: none; + } + + &.mat-checked { + .mat-slide-toggle-thumb-container { + transform: translate3d(100%,0,0); + } + .mat-slide-toggle-bar { + background: $thumb-bar-checked-img no-repeat; + background-size: contain; + } + + .mat-slide-toggle-thumb { + background: $thumb-checked-img no-repeat; + background-size: contain; + } + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts new file mode 100644 index 0000000000..f4a84d0326 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts @@ -0,0 +1,359 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { UtilsService } from '@core/services/utils.service'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { isDefined } from '@core/utils'; +import { IWidgetSubscription, SubscriptionInfo, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; +import { DatasourceType, widgetType } from '@shared/models/widget.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { MatSlideToggle } from '@angular/material/slide-toggle'; +import { ResizeObserver } from '@juggle/resize-observer'; + +const switchAspectRation = 2.7893; + +type RetrieveValueMethod = 'rpc' | 'attribute' | 'timeseries'; + +interface SwitchSettings { + initialValue: boolean; + title: string; + showOnOffLabels: boolean; + retrieveValueMethod: RetrieveValueMethod; + valueKey: string; + getValueMethod: string; + setValueMethod: string; + parseValueFunction: string; + convertValueFunction: string; + requestTimeout: number; +} + +@Component({ + selector: 'tb-switch', + templateUrl: './switch.component.html', + styleUrls: ['./switch.component.scss'] +}) +export class SwitchComponent extends PageComponent implements OnInit, OnDestroy { + + @ViewChild('switch', {static: true}) switchElementRef: ElementRef; + @ViewChild('switchContainer', {static: true}) switchContainerRef: ElementRef; + @ViewChild('matSlideToggle', {static: true}) matSlideToggleRef: MatSlideToggle; + @ViewChild('onoffContainer', {static: true}) onoffContainerRef: ElementRef; + @ViewChild('onLabel', {static: true}) onLabelRef: ElementRef; + @ViewChild('offLabel', {static: true}) offLabelRef: ElementRef; + @ViewChild('switchTitleContainer', {static: true}) switchTitleContainerRef: ElementRef; + @ViewChild('switchTitle', {static: true}) switchTitleRef: ElementRef; + @ViewChild('textMeasure', {static: true}) textMeasureRef: ElementRef; + @ViewChild('switchErrorContainer', {static: true}) switchErrorContainerRef: ElementRef; + @ViewChild('switchError', {static: true}) switchErrorRef: ElementRef; + + @Input() + ctx: WidgetContext; + + showTitle = false; + value = false; + error = ''; + title = ''; + showOnOffLabels = false; + + private isSimulated: boolean; + private requestTimeout: number; + private retrieveValueMethod: RetrieveValueMethod; + private valueKey: string; + private parseValueFunction: (data: any) => boolean; + private convertValueFunction: (value: any) => any; + private getValueMethod: string; + private setValueMethod: string; + + private valueSubscription: IWidgetSubscription; + + private executingUpdateValue: boolean; + private scheduledValue: boolean; + private rpcValue: boolean; + + private switchElement: JQuery; + private switchContainer: JQuery; + private matSlideToggle: JQuery; + private onoffContainer: JQuery; + private onLabel: JQuery; + private offLabel: JQuery; + private switchTitleContainer: JQuery; + private switchTitle: JQuery; + private textMeasure: JQuery; + private switchErrorContainer: JQuery; + private switchError: JQuery; + + private switchResize$: ResizeObserver; + + constructor(private utils: UtilsService, + protected store: Store) { + super(store); + } + + ngOnInit(): void { + this.switchElement = $(this.switchElementRef.nativeElement); + this.switchContainer = $(this.switchContainerRef.nativeElement); + this.matSlideToggle = $(this.matSlideToggleRef._elementRef.nativeElement); + this.onoffContainer = $(this.onoffContainerRef.nativeElement); + this.onLabel = $(this.onLabelRef.nativeElement); + this.offLabel = $(this.offLabelRef.nativeElement); + this.switchTitleContainer = $(this.switchTitleContainerRef.nativeElement); + this.switchTitle = $(this.switchTitleRef.nativeElement); + this.textMeasure = $(this.textMeasureRef.nativeElement); + this.switchErrorContainer = $(this.switchErrorContainerRef.nativeElement); + this.switchError = $(this.switchErrorRef.nativeElement); + + this.switchResize$ = new ResizeObserver(() => { + this.resize(); + }) + this.switchResize$.observe(this.switchContainerRef.nativeElement); + this.init(); + } + + ngOnDestroy(): void { + if (this.valueSubscription) { + this.ctx.subscriptionApi.removeSubscription(this.valueSubscription.id); + } + if (this.switchResize$) { + this.switchResize$.disconnect(); + } + } + + private init() { + const settings: SwitchSettings = this.ctx.settings; + this.title = isDefined(settings.title) ? settings.title : ''; + this.showTitle = !!(this.title && this.title.length); + this.showOnOffLabels = isDefined(settings.showOnOffLabels) ? settings.showOnOffLabels : true; + const initialValue = isDefined(settings.initialValue) ? settings.initialValue : false; + this.setValue(initialValue); + + const subscription = this.ctx.defaultSubscription; + const rpcEnabled = subscription.rpcEnabled; + + this.isSimulated = this.utils.widgetEditMode; + + this.requestTimeout = 500; + if (settings.requestTimeout) { + this.requestTimeout = settings.requestTimeout; + } + this.retrieveValueMethod = 'rpc'; + if (settings.retrieveValueMethod && settings.retrieveValueMethod.length) { + this.retrieveValueMethod = settings.retrieveValueMethod; + } + this.valueKey = 'value'; + if (settings.valueKey && settings.valueKey.length) { + this.valueKey = settings.valueKey; + } + this.parseValueFunction = (data) => !!data; + if (settings.parseValueFunction && settings.parseValueFunction.length) { + try { + this.parseValueFunction = new Function('data', settings.parseValueFunction) as (data: any) => boolean; + } catch (e) { + this.parseValueFunction = (data) => !!data; + } + } + this.convertValueFunction = (value) => value; + if (settings.convertValueFunction && settings.convertValueFunction.length) { + try { + this.convertValueFunction = new Function('value', settings.convertValueFunction) as (value: any) => any; + } catch (e) { + this.convertValueFunction = (value) => value; + } + } + this.getValueMethod = 'getValue'; + if (settings.getValueMethod && settings.getValueMethod.length) { + this.getValueMethod = settings.getValueMethod; + } + this.setValueMethod = 'setValue'; + if (settings.setValueMethod && settings.setValueMethod.length) { + this.setValueMethod = settings.setValueMethod; + } + if (!rpcEnabled) { + this.onError('Target device is not set!'); + } else { + if (!this.isSimulated) { + if (this.retrieveValueMethod === 'rpc') { + this.rpcRequestValue(); + } else if (this.retrieveValueMethod === 'attribute' || this.retrieveValueMethod === 'timeseries') { + this.subscribeForValue(); + } + } + } + + } + + private resize() { + let width = this.switchContainer.width(); + let height = this.switchContainer.height(); + if (this.showOnOffLabels) { + height = height*2/3; + } + const ratio = width/height; + if (ratio > switchAspectRation) { + width = height*switchAspectRation; + } else { + height = width/switchAspectRation; + } + this.switchElement.css({width, height}); + this.matSlideToggle.css({width, height, minWidth: width}); + + if (this.showTitle) { + this.setFontSize(this.switchTitle, this.title, this.switchTitleContainer.height() * 2 / 3, this.switchTitleContainer.width()); + } + + if (this.showOnOffLabels) { + this.onoffContainer.css({width, height: this.switchContainer.height() / 3}); + this.setFontSize(this.onLabel, 'OFF', this.onoffContainer.height(), this.onoffContainer.width() / 2); + this.setFontSize(this.offLabel, 'OFF', this.onoffContainer.height(), this.onoffContainer.width() / 2); + } + + this.setFontSize(this.switchError, this.error, this.switchErrorContainer.height(), this.switchErrorContainer.width()); + } + + private setFontSize(element: JQuery, text: string, fontSize: number, maxWidth: number) { + let textWidth = this.measureTextWidth(text, fontSize); + while (textWidth > maxWidth) { + fontSize--; + textWidth = this.measureTextWidth(text, fontSize); + } + element.css({fontSize: fontSize+'px', lineHeight: fontSize+'px'}); + } + + private measureTextWidth(text: string, fontSize: number): number { + this.textMeasure.css({fontSize: fontSize+'px', lineHeight: fontSize+'px'}); + this.textMeasure.text(text); + return this.textMeasure.width(); + } + + private onError(error: string) { + this.error = error; + this.setFontSize(this.switchError, this.error, this.switchErrorContainer.height(), this.switchErrorContainer.width()); + this.ctx.detectChanges(); + } + + private setValue(value: boolean) { + this.value = value ? true : false; + } + + public onValue() { + this.rpcUpdateValue(this.value); + } + + private rpcRequestValue() { + this.error = ''; + this.ctx.controlApi.sendTwoWayCommand(this.getValueMethod, null, this.requestTimeout).subscribe( + (responseBody) => { + this.setValue(this.parseValueFunction(responseBody)); + }, + () => { + const errorText = this.ctx.defaultSubscription.rpcErrorText; + this.onError(errorText); + } + ); + } + + private rpcUpdateValue(value) { + if (this.executingUpdateValue) { + this.scheduledValue = value; + return; + } else { + this.scheduledValue = null; + this.rpcValue = value; + this.executingUpdateValue = true; + } + this.error = ''; + this.ctx.controlApi.sendOneWayCommand(this.setValueMethod, this.convertValueFunction(value), this.requestTimeout).subscribe( + () => { + this.executingUpdateValue = false; + if (this.scheduledValue != null && this.scheduledValue !== this.rpcValue) { + this.rpcUpdateValue(this.scheduledValue); + } + }, + () => { + this.executingUpdateValue = false; + const errorText = this.ctx.defaultSubscription.rpcErrorText; + this.onError(errorText); + } + ); + } + + private subscribeForValue() { + const valueSubscriptionInfo: SubscriptionInfo[] = [{ + type: DatasourceType.entity, + entityType: EntityType.DEVICE, + entityId: this.ctx.defaultSubscription.targetDeviceId + }]; + if (this.retrieveValueMethod === 'attribute') { + valueSubscriptionInfo[0].attributes = [ + {name: this.valueKey} + ]; + } else { + valueSubscriptionInfo[0].timeseries = [ + {name: this.valueKey} + ]; + } + const subscriptionOptions: WidgetSubscriptionOptions = { + callbacks: { + onDataUpdated: (subscription, detectChanges) => this.ctx.ngZone.run(() => { + this.onDataUpdated(subscription, detectChanges); + }), + onDataUpdateError: (subscription, e) => this.ctx.ngZone.run(() => { + this.onDataUpdateError(subscription, e); + }) + } + }; + this.ctx.subscriptionApi.createSubscriptionFromInfo ( + widgetType.latest, valueSubscriptionInfo, subscriptionOptions, false, true).subscribe( + (subscription) => { + this.valueSubscription = subscription; + } + ); + } + + private onDataUpdated(subscription: IWidgetSubscription, detectChanges: boolean) { + let value = false; + const data = subscription.data; + if (data.length) { + const keyData = data[0]; + if (keyData && keyData.data && keyData.data[0]) { + const attrValue = keyData.data[0][1]; + if (isDefined(attrValue)) { + let parsed = null; + try { + parsed = this.parseValueFunction(JSON.parse(attrValue)); + } catch (e){/**/} + value = !!parsed; + } + } + } + this.setValue(value); + if (detectChanges) { + this.ctx.detectChanges(); + } + } + + private onDataUpdateError(subscription: IWidgetSubscription, e: any) { + const exceptionData = this.utils.parseException(e); + let errorText = exceptionData.name; + if (exceptionData.message) { + errorText += ': ' + exceptionData.message; + } + this.onError(errorText); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings.models.ts new file mode 100644 index 0000000000..9ba44a398d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings.models.ts @@ -0,0 +1,44 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + + +export type FontStyle = 'normal' | 'italic' | 'oblique'; +export type FontWeight = 'normal' | 'bold' | 'bolder' | 'lighter' + | '100' | '200' | '300' | '400' | '500' + | '600' | '700' | '800' | '900'; + +export interface FontSettings { + family?: string; + size?: number; + style?: FontStyle; + weight?: FontWeight; + color?: string; + shadowColor?: string; +} + +export function getFontFamily(fontSettings: FontSettings): string { + let family = fontSettings && fontSettings.family ? fontSettings.family : 'Roboto'; + if (family === 'RobotoDraft') { + family = 'Roboto'; + } + return family; +} + +export function prepareFontSettings(fontSettings: FontSettings, defaultFontSettings: FontSettings): FontSettings { + const result = {...defaultFontSettings, ...(fontSettings || {})}; + result.family = getFontFamily(result); + return result; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts new file mode 100644 index 0000000000..1ab9ea6fc4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts @@ -0,0 +1,364 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from '@shared/models/id/entity-id'; +import { DataKey, WidgetConfig } from '@shared/models/widget.models'; +import { getDescendantProp, isDefined } from '@core/utils'; +import { AlarmDataInfo, alarmFields } from '@shared/models/alarm.models'; +import * as tinycolor_ from 'tinycolor2'; +import { Direction, EntityDataSortOrder, EntityKey } from '@shared/models/query/query.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; + +const tinycolor = tinycolor_; + +export interface TableWidgetSettings { + enableSearch: boolean; + enableSelectColumnDisplay: boolean; + displayPagination: boolean; + defaultPageSize: number; + defaultSortOrder: string; +} + +export interface TableWidgetDataKeySettings { + columnWidth?: string; + useCellStyleFunction: boolean; + cellStyleFunction: string; + useCellContentFunction: boolean; + cellContentFunction: string; +} + +export interface EntityData { + id: EntityId; + entityName: string; + entityLabel?: string; + entityType?: string; + [key: string]: any; +} + +export interface EntityColumn extends DataKey { + def: string; + title: string; + entityKey?: EntityKey; +} + +export interface DisplayColumn { + title: string; + def: string; + display: boolean; +} + +export type CellContentFunction = (...args: any[]) => string; + +export interface CellContentInfo { + useCellContentFunction: boolean; + cellContentFunction?: CellContentFunction; + units?: string; + decimals?: number; +} + +export type CellStyleFunction = (value: any) => any; + +export interface CellStyleInfo { + useCellStyleFunction: boolean; + cellStyleFunction?: CellStyleFunction; +} + + +export function entityDataSortOrderFromString(strSortOrder: string, columns: EntityColumn[]): EntityDataSortOrder { + if (!strSortOrder && !strSortOrder.length) { + return null; + } + let property: string; + let direction = Direction.ASC; + if (strSortOrder.startsWith('-')) { + direction = Direction.DESC; + property = strSortOrder.substring(1); + } else { + if (strSortOrder.startsWith('+')) { + property = strSortOrder.substring(1); + } else { + property = strSortOrder; + } + } + if (!property && !property.length) { + return null; + } + let column = findColumnByLabel(property, columns); + if (!column) { + column = findColumnByName(property, columns); + } + if (column && column.entityKey) { + return {key: column.entityKey, direction}; + } + return null; +} + +export function findColumnByEntityKey(key: EntityKey, columns: EntityColumn[]): EntityColumn { + if (key) { + return columns.find(theColumn => theColumn.entityKey && + theColumn.entityKey.type === key.type && theColumn.entityKey.key === key.key); + } else { + return null; + } +} + +export function findEntityKeyByColumnDef(def: string, columns: EntityColumn[]): EntityKey { + if (def) { + const column = findColumnByDef(def, columns); + return column ? column.entityKey : null; + } else { + return null; + } +} + +export function findColumn(searchProperty: string, searchValue: string, columns: EntityColumn[]): EntityColumn { + return columns.find(theColumn => theColumn[searchProperty] === searchValue); +} + +export function findColumnByName(name: string, columns: EntityColumn[]): EntityColumn { + return findColumn('name', name, columns); +} + +export function findColumnByLabel(label: string, columns: EntityColumn[]): EntityColumn { + let column: EntityColumn; + const alarmColumns = columns.filter(c => c.type === DataKeyType.alarm); + if (alarmColumns.length) { + column = findColumn('name', label, alarmColumns); + } + if (!column) { + column = findColumn('label', label, columns); + } + return column; +} + +export function findColumnByDef(def: string, columns: EntityColumn[]): EntityColumn { + return findColumn('def', def, columns); +} + +export function findColumnProperty(searchProperty: string, searchValue: string, columnProperty: string, columns: EntityColumn[]): string { + let res = searchValue; + const column = columns.find(theColumn => theColumn[searchProperty] === searchValue); + if (column) { + res = column[columnProperty]; + } + return res; +} + +export function toEntityKey(def: string, columns: EntityColumn[]): string { + return findColumnProperty('def', def, 'label', columns); +} + +export function toEntityColumnDef(label: string, columns: EntityColumn[]): string { + return findColumnProperty('label', label, 'def', columns); +} + +export function fromEntityColumnDef(def: string, columns: EntityColumn[]): string { + return findColumnProperty('def', def, 'label', columns); +} + +export function toAlarmColumnDef(name: string, columns: EntityColumn[]): string { + return findColumnProperty('name', name, 'def', columns); +} + +export function fromAlarmColumnDef(def: string, columns: EntityColumn[]): string { + return findColumnProperty('def', def, 'name', columns); +} + +export function getEntityValue(entity: any, key: DataKey): any { + return getDescendantProp(entity, key.label); +} + +export function getAlarmValue(alarm: AlarmDataInfo, key: EntityColumn) { + let alarmField = null; + if (key.type === DataKeyType.alarm) { + alarmField = alarmFields[key.name]; + } + if (alarmField) { + return getDescendantProp(alarm, alarmField.value); + } else { + return getDescendantProp(alarm, key.label); + } +} + +export function getCellStyleInfo(keySettings: TableWidgetDataKeySettings): CellStyleInfo { + let cellStyleFunction: CellStyleFunction = null; + let useCellStyleFunction = false; + + if (keySettings.useCellStyleFunction === true) { + if (isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) { + try { + cellStyleFunction = new Function('value', keySettings.cellStyleFunction) as CellStyleFunction; + useCellStyleFunction = true; + } catch (e) { + cellStyleFunction = null; + useCellStyleFunction = false; + } + } + } + return { + useCellStyleFunction, + cellStyleFunction + }; +} + +export function getCellContentInfo(keySettings: TableWidgetDataKeySettings, ...args: string[]): CellContentInfo { + let cellContentFunction: CellContentFunction = null; + let useCellContentFunction = false; + + if (keySettings.useCellContentFunction === true) { + if (isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) { + try { + cellContentFunction = new Function(...args, keySettings.cellContentFunction) as CellContentFunction; + useCellContentFunction = true; + } catch (e) { + cellContentFunction = null; + useCellContentFunction = false; + } + } + } + return { + cellContentFunction, + useCellContentFunction + }; +} + +export function getColumnWidth(keySettings: TableWidgetDataKeySettings): string { + return isDefined(keySettings.columnWidth) ? keySettings.columnWidth : '0px'; +} + +export function widthStyle(width: string): any { + const widthStyleObj: any = {width}; + if (width !== '0px') { + widthStyleObj.minWidth = width; + widthStyleObj.maxWidth = width; + } + return widthStyleObj; +} + + + +export function constructTableCssString(widgetConfig: WidgetConfig): string { + const origColor = widgetConfig.color || 'rgba(0, 0, 0, 0.87)'; + const origBackgroundColor = widgetConfig.backgroundColor || 'rgb(255, 255, 255)'; + const currentEntityColor = 'rgba(221, 221, 221, 0.65)'; + const currentEntityStickyColor = tinycolor.mix(origBackgroundColor, + tinycolor(currentEntityColor).setAlpha(1), 65).toRgbString(); + const selectedColor = 'rgba(221, 221, 221, 0.5)'; + const selectedStickyColor = tinycolor.mix(origBackgroundColor, + tinycolor(selectedColor).setAlpha(1), 50).toRgbString(); + const hoverColor = 'rgba(221, 221, 221, 0.3)'; + const hoverStickyColor = tinycolor.mix(origBackgroundColor, + tinycolor(hoverColor).setAlpha(1), 30).toRgbString(); + const defaultColor = tinycolor(origColor); + const mdDark = defaultColor.setAlpha(0.87).toRgbString(); + const mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString(); + const mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString(); + const mdDarkDisabled2 = defaultColor.setAlpha(0.38).toRgbString(); + const mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString(); + + const cssString = + '.mat-input-element::placeholder {\n' + + ' color: ' + mdDarkSecondary + ';\n' + + '}\n' + + '.mat-input-element::-moz-placeholder {\n' + + ' color: ' + mdDarkSecondary + ';\n' + + '}\n' + + '.mat-input-element::-webkit-input-placeholder {\n' + + ' color: ' + mdDarkSecondary + ';\n' + + '}\n' + + '.mat-input-element:-ms-input-placeholder {\n' + + ' color: ' + mdDarkSecondary + ';\n' + + '}\n' + + 'mat-toolbar.mat-table-toolbar {\n' + + 'color: ' + mdDark + ';\n' + + '}\n' + + 'mat-toolbar.mat-table-toolbar:not([color="primary"]) button.mat-icon-button mat-icon {\n' + + 'color: ' + mdDarkSecondary + ';\n' + + '}\n' + + '.mat-tab-label {\n' + + 'color: ' + mdDark + ';\n' + + '}\n' + + '.mat-tab-header-pagination-chevron {\n' + + 'border-color: ' + mdDark + ';\n' + + '}\n' + + '.mat-tab-header-pagination-disabled .mat-tab-header-pagination-chevron {\n' + + 'border-color: ' + mdDarkDisabled2 + ';\n' + + '}\n' + + '.mat-table .mat-header-row {\n' + + 'background-color: ' + origBackgroundColor + ';\n' + + '}\n' + + '.mat-table .mat-header-cell {\n' + + 'color: ' + mdDarkSecondary + ';\n' + + '}\n' + + '.mat-table .mat-header-cell .mat-sort-header-arrow {\n' + + 'color: ' + mdDarkDisabled + ';\n' + + '}\n' + + '.mat-table .mat-cell, .mat-table .mat-header-cell {\n' + + 'border-bottom-color: ' + mdDarkDivider + ';\n' + + '}\n' + + '.mat-table .mat-cell .mat-checkbox-frame, .mat-table .mat-header-cell .mat-checkbox-frame {\n' + + 'border-color: ' + mdDarkSecondary + ';\n' + + '}\n' + + '.mat-table .mat-row .mat-cell.mat-table-sticky {\n' + + 'transition: background-color .2s;\n' + + '}\n' + + '.mat-table .mat-row.tb-current-entity {\n' + + 'background-color: ' + currentEntityColor + ';\n' + + '}\n' + + '.mat-table .mat-row.tb-current-entity .mat-cell.mat-table-sticky {\n' + + 'background-color: ' + currentEntityStickyColor + ';\n' + + '}\n' + + '.mat-table .mat-row:hover:not(.tb-current-entity) {\n' + + 'background-color: ' + hoverColor + ';\n' + + '}\n' + + '.mat-table .mat-row:hover:not(.tb-current-entity) .mat-cell.mat-table-sticky {\n' + + 'background-color: ' + hoverStickyColor + ';\n' + + '}\n' + + '.mat-table .mat-row.mat-row-select.mat-selected:not(.tb-current-entity) {\n' + + 'background-color: ' + selectedColor + ';\n' + + '}\n' + + '.mat-table .mat-row.mat-row-select.mat-selected:not(.tb-current-entity) .mat-cell.mat-table-sticky {\n' + + 'background-color: ' + selectedStickyColor + ';\n' + + '}\n' + + '.mat-table .mat-row .mat-cell.mat-table-sticky, .mat-table .mat-header-cell.mat-table-sticky {\n' + + 'background-color: ' + origBackgroundColor + ';\n' + + '}\n' + + '.mat-table .mat-cell {\n' + + 'color: ' + mdDark + ';\n' + + '}\n' + + '.mat-table .mat-cell button.mat-icon-button mat-icon {\n' + + 'color: ' + mdDarkSecondary + ';\n' + + '}\n' + + '.mat-table .mat-cell button.mat-icon-button[disabled][disabled] mat-icon {\n' + + 'color: ' + mdDarkDisabled + ';\n' + + '}\n' + + '.mat-divider {\n' + + 'border-top-color: ' + mdDarkDivider + ';\n' + + '}\n' + + '.mat-paginator {\n' + + 'color: ' + mdDarkSecondary + ';\n' + + '}\n' + + '.mat-paginator button.mat-icon-button {\n' + + 'color: ' + mdDarkSecondary + ';\n' + + '}\n' + + '.mat-paginator button.mat-icon-button[disabled][disabled] {\n' + + 'color: ' + mdDarkDisabled + ';\n' + + '}\n' + + '.mat-paginator .mat-select-value {\n' + + 'color: ' + mdDarkSecondary + ';\n' + + '}'; + return cssString; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.scss b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.scss new file mode 100644 index 0000000000..d20588e412 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.scss @@ -0,0 +1,61 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .tb-table-widget { + .mat-table, .mat-paginator, mat-toolbar.mat-table-toolbar:not([color="primary"]) { + background: transparent; + } + mat-toolbar { + height: 39px; + max-height: 39px; + .mat-toolbar-tools { + height: 39px; + max-height: 39px; + } + } + .table-container { + overflow: auto; + } + + .mat-row:not(.mat-row-select), .mat-header-row:not(.mat-row-select) { + mat-cell:nth-child(n+2):nth-last-child(n+2), mat-footer-cell:nth-child(n+2):nth-last-child(n+2), mat-header-cell:nth-child(n+2):nth-last-child(n+2) { + padding: 0 5px; + } + } + + .mat-row.mat-row-select, .mat-header-row.mat-row-select { + mat-cell:nth-child(2), mat-footer-cell:nth-child(2), mat-header-cell:nth-child(2) { + padding: 0 5px; + } + mat-cell:nth-child(n+3):nth-last-child(n+2), mat-footer-cell:nth-child(n+3):nth-last-child(n+2), mat-header-cell:nth-child(n+3):nth-last-child(n+2) { + padding: 0 5px; + } + } + } +} + +:host-context(.tb-has-timewindow) { + .tb-table-widget { + mat-toolbar { + height: 65px; + max-height: 65px; + .mat-toolbar-tools { + height: 65px; + max-height: 65px; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html new file mode 100644 index 0000000000..96335a3053 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html @@ -0,0 +1,111 @@ + +
+
+ +
+ + +   + + + +
+
+ + +
+ + + Timestamp + + + + + {{ h.dataKey.label }} + + + + + + + +
+ +
+
+ + + + +
+
+
+ + +
+ widget.no-data-found +
+ + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.scss new file mode 100644 index 0000000000..d1b28361f5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.scss @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + width: 100%; + height: 100%; + .tb-table-widget { + mat-footer-row, mat-row { + height: 38px; + } + mat-header-row { + height: 40px; + } + mat-toolbar { + z-index: 10; + } + span.no-data-found { + height: calc(100% - 44px); + } + } +} + +:host ::ng-deep { + .tb-table-widget { + .mat-tab-group { + height: 100%; + overflow: hidden; + } + .mat-tab-body-wrapper { + height: 100%; + } + .mat-tab-body-content { + display: flex; + flex-direction: column; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts new file mode 100644 index 0000000000..1b820ab722 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -0,0 +1,563 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + Component, + ElementRef, + Input, + NgZone, + OnInit, + QueryList, + ViewChild, + ViewChildren, + ViewContainerRef +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { WidgetAction, WidgetContext } from '@home/models/widget-component.models'; +import { + DataKey, + Datasource, + DatasourceData, + DatasourceType, + WidgetActionDescriptor, + WidgetConfig +} from '@shared/models/widget.models'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import {hashCode, isDefined, isDefinedAndNotNull, isNumber} from '@core/utils'; +import cssjs from '@core/css/css'; +import { PageLink } from '@shared/models/page/page-link'; +import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order'; +import { CollectionViewer, DataSource } from '@angular/cdk/collections'; +import { BehaviorSubject, fromEvent, merge, Observable, of } from 'rxjs'; +import { emptyPageData, PageData } from '@shared/models/page/page-data'; +import { catchError, debounceTime, distinctUntilChanged, map, tap } from 'rxjs/operators'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import { + CellContentInfo, + CellStyleInfo, + constructTableCssString, + getCellContentInfo, + getCellStyleInfo, + TableWidgetDataKeySettings +} from '@home/components/widget/lib/table-widget.models'; +import { Overlay } from '@angular/cdk/overlay'; +import { SubscriptionEntityInfo } from '@core/api/widget-api.models'; +import { DatePipe } from '@angular/common'; + +interface TimeseriesTableWidgetSettings { + showTimestamp: boolean; + showMilliseconds: boolean; + displayPagination: boolean; + defaultPageSize: number; + hideEmptyLines: boolean; +} + +interface TimeseriesRow { + [col: number]: any; + formattedTs: string; +} + +interface TimeseriesHeader { + index: number; + dataKey: DataKey; +} + +interface TimeseriesTableSource { + keyStartIndex: number; + keyEndIndex: number; + datasource: Datasource; + rawData: Array; + data: TimeseriesRow[]; + pageLink: PageLink; + displayedColumns: string[]; + timeseriesDatasource: TimeseriesDatasource; + header: TimeseriesHeader[]; + stylesInfo: CellStyleInfo[]; + contentsInfo: CellContentInfo[]; + rowDataTemplate: {[key: string]: any}; +} + +@Component({ + selector: 'tb-timeseries-table-widget', + templateUrl: './timeseries-table-widget.component.html', + styleUrls: ['./timeseries-table-widget.component.scss', './table-widget.scss'] +}) +export class TimeseriesTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit { + + @Input() + ctx: WidgetContext; + + @ViewChild('searchInput') searchInputField: ElementRef; + @ViewChildren(MatPaginator) paginators: QueryList; + @ViewChildren(MatSort) sorts: QueryList; + + public displayPagination = true; + public pageSizeOptions; + public textSearchMode = false; + public textSearch: string = null; + public actionCellDescriptors: WidgetActionDescriptor[]; + public sources: TimeseriesTableSource[]; + public sourceIndex: number; + + private settings: TimeseriesTableWidgetSettings; + private widgetConfig: WidgetConfig; + private data: Array; + private datasources: Array; + + private defaultPageSize = 10; + private defaultSortOrder = '-0'; + private hideEmptyLines = false; + private showTimestamp = true; + private dateFormatFilter: string; + + private searchAction: WidgetAction = { + name: 'action.search', + show: true, + icon: 'search', + onAction: () => { + this.enterFilterMode(); + } + }; + + constructor(protected store: Store, + private elementRef: ElementRef, + private ngZone: NgZone, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, + private utils: UtilsService, + private translate: TranslateService, + private domSanitizer: DomSanitizer, + private datePipe: DatePipe) { + super(store); + } + + ngOnInit(): void { + this.ctx.$scope.timeseriesTableWidget = this; + this.settings = this.ctx.settings; + this.widgetConfig = this.ctx.widgetConfig; + this.data = this.ctx.data; + this.datasources = this.ctx.datasources; + this.initialize(); + this.ctx.updateWidgetParams(); + } + + ngAfterViewInit(): void { + fromEvent(this.searchInputField.nativeElement, 'keyup') + .pipe( + debounceTime(150), + distinctUntilChanged(), + tap(() => { + if (this.displayPagination) { + this.paginators.forEach((paginator) => { + paginator.pageIndex = 0; + }); + } + this.sources.forEach((source) => { + source.pageLink.textSearch = this.textSearch; + }); + this.updateAllData(); + }) + ) + .subscribe(); + + if (this.displayPagination) { + this.sorts.forEach((sort, index) => { + sort.sortChange.subscribe(() => this.paginators.toArray()[index].pageIndex = 0); + }); + } + this.sorts.forEach((sort, index) => { + const paginator = this.displayPagination ? this.paginators.toArray()[index] : null; + sort.sortChange.subscribe(() => this.paginators.toArray()[index].pageIndex = 0); + ((this.displayPagination ? merge(sort.sortChange, paginator.page) : sort.sortChange) as Observable) + .pipe( + tap(() => this.updateData(sort, paginator, index)) + ) + .subscribe(); + }); + this.updateAllData(); + } + + public onDataUpdated() { + this.sources.forEach((source) => { + source.timeseriesDatasource.dataUpdated(this.data); + }); + } + + private initialize() { + this.ctx.widgetActions = [this.searchAction ]; + + this.actionCellDescriptors = this.ctx.actionsApi.getActionDescriptors('actionCellButton'); + + this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true; + this.hideEmptyLines = isDefined(this.settings.hideEmptyLines) ? this.settings.hideEmptyLines : false; + this.showTimestamp = this.settings.showTimestamp !== false; + this.dateFormatFilter = (this.settings.showMilliseconds !== true) ? 'yyyy-MM-dd HH:mm:ss' : 'yyyy-MM-dd HH:mm:ss.sss'; + + const pageSize = this.settings.defaultPageSize; + if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) { + this.defaultPageSize = pageSize; + } + this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3]; + + let cssString = constructTableCssString(this.widgetConfig); + + const origBackgroundColor = this.widgetConfig.backgroundColor || 'rgb(255, 255, 255)'; + cssString += '.tb-table-widget mat-toolbar.mat-table-toolbar:not([color=primary]) {\n' + + 'background-color: ' + origBackgroundColor + ' !important;\n' + + '}\n'; + + const cssParser = new cssjs(); + cssParser.testMode = false; + const namespace = 'ts-table-' + hashCode(cssString); + cssParser.cssPreviewNamespace = namespace; + cssParser.createStyleElement(namespace, cssString); + $(this.elementRef.nativeElement).addClass(namespace); + this.updateDatasources(); + } + + private updateDatasources() { + this.sources = []; + this.sourceIndex = 0; + let keyOffset = 0; + const pageSize = this.displayPagination ? this.defaultPageSize : Number.POSITIVE_INFINITY; + if (this.datasources) { + for (const datasource of this.datasources) { + const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder); + const source = {} as TimeseriesTableSource; + source.keyStartIndex = keyOffset; + keyOffset += datasource.dataKeys.length; + source.keyEndIndex = keyOffset; + source.datasource = datasource; + source.data = []; + source.rawData = []; + source.displayedColumns = []; + source.pageLink = new PageLink(pageSize, 0, null, sortOrder); + source.header = []; + source.stylesInfo = []; + source.contentsInfo = []; + source.rowDataTemplate = {}; + source.rowDataTemplate.Timestamp = null; + if (this.showTimestamp) { + source.displayedColumns.push('0'); + } + for (let a = 0; a < datasource.dataKeys.length; a++ ) { + const dataKey = datasource.dataKeys[a]; + const keySettings: TableWidgetDataKeySettings = dataKey.settings; + const index = a + 1; + source.header.push({ + index, + dataKey + }); + source.displayedColumns.push(index + ''); + source.rowDataTemplate[dataKey.label] = null; + source.stylesInfo.push(getCellStyleInfo(keySettings)); + const cellContentInfo = getCellContentInfo(keySettings, 'value, rowData, ctx'); + cellContentInfo.units = dataKey.units; + cellContentInfo.decimals = dataKey.decimals; + source.contentsInfo.push(cellContentInfo); + } + source.displayedColumns.push('actions'); + const tsDatasource = new TimeseriesDatasource(source, this.hideEmptyLines, this.dateFormatFilter, this.datePipe); + tsDatasource.dataUpdated(this.data); + this.sources.push(source); + } + } + this.updateActiveEntityInfo(); + } + + private updateActiveEntityInfo() { + const source = this.sources[this.sourceIndex]; + let activeEntityInfo: SubscriptionEntityInfo = null; + if (source) { + const datasource = source.datasource; + if (datasource.type === DatasourceType.entity && + datasource.entityType && datasource.entityId) { + activeEntityInfo = { + entityId: { + entityType: datasource.entityType, + id: datasource.entityId + }, + entityName: datasource.entityName, + entityLabel: datasource.entityLabel, + entityDescription: datasource.entityDescription + }; + } + } + this.ctx.activeEntityInfo = activeEntityInfo; + } + + onSourceIndexChanged() { + this.updateActiveEntityInfo(); + } + + private enterFilterMode() { + this.textSearchMode = true; + this.textSearch = ''; + this.sources.forEach((source) => { + source.pageLink.textSearch = this.textSearch; + }); + this.ctx.hideTitlePanel = true; + this.ctx.detectChanges(true); + setTimeout(() => { + this.searchInputField.nativeElement.focus(); + this.searchInputField.nativeElement.setSelectionRange(0, 0); + }, 10); + } + + exitFilterMode() { + this.textSearchMode = false; + this.textSearch = null; + this.sources.forEach((source, index) => { + source.pageLink.textSearch = this.textSearch; + const sort = this.sorts.toArray()[index]; + let paginator = null; + if (this.displayPagination) { + paginator = this.paginators.toArray()[index]; + paginator.pageIndex = 0; + } + this.updateData(sort, paginator, index); + }); + this.ctx.hideTitlePanel = false; + this.ctx.detectChanges(true); + } + + private updateAllData() { + this.sources.forEach((source, index) => { + const sort = this.sorts.toArray()[index]; + const paginator = this.displayPagination ? this.paginators.toArray()[index] : null; + this.updateData(sort, paginator, index); + }); + } + + private updateData(sort: MatSort, paginator: MatPaginator, index: number) { + const source = this.sources[index]; + if (this.displayPagination) { + source.pageLink.page = paginator.pageIndex; + source.pageLink.pageSize = paginator.pageSize; + } else { + source.pageLink.page = 0; + } + source.pageLink.sortOrder.property = sort.active; + source.pageLink.sortOrder.direction = Direction[sort.direction.toUpperCase()]; + source.timeseriesDatasource.loadRows(); + this.ctx.detectChanges(); + } + + public trackByColumnIndex(index, header: TimeseriesHeader) { + return header.index; + } + + public trackByRowIndex(index: number) { + return index; + } + + public cellStyle(source: TimeseriesTableSource, index: number, value: any): any { + let style: any = {}; + if (index > 0) { + const styleInfo = source.stylesInfo[index - 1]; + if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { + try { + style = styleInfo.cellStyleFunction(value); + } catch (e) { + style = {}; + } + } + } + return style; + } + + public cellContent(source: TimeseriesTableSource, index: number, row: TimeseriesRow, value: any): SafeHtml { + if (index === 0) { + return row.formattedTs; + } else { + let content; + const contentInfo = source.contentsInfo[index - 1]; + if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { + try { + const rowData = source.rowDataTemplate; + rowData.Timestamp = row[0]; + source.header.forEach((headerInfo) => { + rowData[headerInfo.dataKey.name] = row[headerInfo.index]; + }); + content = contentInfo.cellContentFunction(value, rowData, this.ctx); + } catch (e) { + content = '' + value; + } + } else { + const decimals = (contentInfo.decimals || contentInfo.decimals === 0) ? contentInfo.decimals : this.ctx.widgetConfig.decimals; + const units = contentInfo.units || this.ctx.widgetConfig.units; + content = this.ctx.utils.formatValue(value, decimals, units, true); + } + + if (!isDefined(content)) { + return ''; + + } else { + switch (typeof content) { + case 'string': + return this.domSanitizer.bypassSecurityTrustHtml(content); + default: + return content; + } + } + } + } + + public onRowClick($event: Event, row: TimeseriesRow) { + const descriptors = this.ctx.actionsApi.getActionDescriptors('rowClick'); + if (descriptors.length) { + if ($event) { + $event.stopPropagation(); + } + let entityId; + let entityName; + let entityLabel; + if (this.ctx.activeEntityInfo) { + entityId = this.ctx.activeEntityInfo.entityId; + entityName = this.ctx.activeEntityInfo.entityName; + entityLabel = this.ctx.activeEntityInfo.entityLabel; + } + this.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName, row, entityLabel); + } + } + + public onActionButtonClick($event: Event, row: TimeseriesRow, actionDescriptor: WidgetActionDescriptor) { + if ($event) { + $event.stopPropagation(); + } + let entityId; + let entityName; + let entityLabel; + if (this.ctx.activeEntityInfo) { + entityId = this.ctx.activeEntityInfo.entityId; + entityName = this.ctx.activeEntityInfo.entityName; + entityLabel = this.ctx.activeEntityInfo.entityLabel; + } + this.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName, row, entityLabel); + } +} + +class TimeseriesDatasource implements DataSource { + + private rowsSubject = new BehaviorSubject([]); + private pageDataSubject = new BehaviorSubject>(emptyPageData()); + + private allRowsSubject = new BehaviorSubject([]); + private allRows$: Observable> = this.allRowsSubject.asObservable(); + + constructor( + private source: TimeseriesTableSource, + private hideEmptyLines: boolean, + private dateFormatFilter: string, + private datePipe: DatePipe + ) { + this.source.timeseriesDatasource = this; + } + + connect(collectionViewer: CollectionViewer): Observable> { + return this.rowsSubject.asObservable(); + } + + disconnect(collectionViewer: CollectionViewer): void { + this.rowsSubject.complete(); + this.pageDataSubject.complete(); + } + + loadRows() { + this.fetchRows(this.source.pageLink).pipe( + catchError(() => of(emptyPageData())), + ).subscribe( + (pageData) => { + this.rowsSubject.next(pageData.data); + this.pageDataSubject.next(pageData); + } + ); + } + + dataUpdated(data: DatasourceData[]) { + this.source.rawData = data.slice(this.source.keyStartIndex, this.source.keyEndIndex); + this.updateSourceData(); + } + + private updateSourceData() { + this.source.data = this.convertData(this.source.rawData); + this.allRowsSubject.next(this.source.data); + } + + private convertData(data: DatasourceData[]): TimeseriesRow[] { + const rowsMap: {[timestamp: number]: TimeseriesRow} = {}; + for (let d = 0; d < data.length; d++) { + const columnData = data[d].data; + columnData.forEach((cellData) => { + const timestamp = cellData[0]; + let row = rowsMap[timestamp]; + if (!row) { + row = { + formattedTs: this.datePipe.transform(timestamp, this.dateFormatFilter) + }; + row[0] = timestamp; + for (let c = 0; c < data.length; c++) { + row[c + 1] = undefined; + } + rowsMap[timestamp] = row; + } + row[d + 1] = cellData[1]; + }); + } + + let rows: TimeseriesRow[] = []; + if (this.hideEmptyLines) { + for (const t of Object.keys(rowsMap)) { + let hideLine = true; + for (let c = 0; (c < data.length) && hideLine; c++) { + if (rowsMap[t][c + 1]) { + hideLine = false; + } + } + if (!hideLine) { + rows.push(rowsMap[t]); + } + } + } else { + rows = Object.values(rowsMap); + } + return rows; + } + + isEmpty(): Observable { + return this.rowsSubject.pipe( + map((rows) => !rows.length) + ); + } + + total(): Observable { + return this.pageDataSubject.pipe( + map((pageData) => pageData.totalElements) + ); + } + + private fetchRows(pageLink: PageLink): Observable> { + return this.allRows$.pipe( + map((data) => pageLink.filterData(data)) + ); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.html b/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.html new file mode 100644 index 0000000000..2ea80cf2af --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.html @@ -0,0 +1,43 @@ + +
+
+ {{label}} +
+
+
+
+ +
+
+
+
+ +
diff --git a/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.scss b/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.scss new file mode 100644 index 0000000000..cdb28f8a27 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.scss @@ -0,0 +1,89 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.trip-animation-widget { + position: relative; + width: 100%; + height: 100%; + font-size: 16px; + line-height: 24px; + display: flex; + flex-direction: column; + + .trip-animation-label-container { + height: 24px; + } + + .trip-animation-container { + position: relative; + z-index: 1; + flex: 1; + width: 100%; + + .map { + width: 100%; + height: 100%; + } + + .trip-animation-info-panel { + position: absolute; + top: 0; + right: 0; + pointer-events: none; + + .tooltip-button { + top: 0; + left: 0; + width: 32px; + min-width: 32px; + height: 32px; + min-height: 32px; + padding: 0 0 2px; + margin: 2px; + line-height: 24px; + z-index: 999; + + &::ng-deep .mat-button-wrapper { + padding: 0; + } + + mat-icon { + width: 24px; + height: 24px; + + svg { + width: inherit; + height: inherit; + } + } + } + } + + .trip-animation-tooltip { + position: absolute; + top: 30px; + right: 0; + z-index: 1000; + padding: 5px; + background-color: #fff; + transition: 0.3s ease-in-out; + + &-hidden { + transform: translateX(110%); + } + } + } +} \ No newline at end of file diff --git a/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts b/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts new file mode 100644 index 0000000000..99c399d3f2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts @@ -0,0 +1,238 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import _ from 'lodash'; +import tinycolor from 'tinycolor2'; + +import { + AfterViewInit, + ChangeDetectorRef, + Component, + Input, + OnDestroy, + OnInit, + SecurityContext, + ViewChild +} from '@angular/core'; +import { MapWidgetController, TbMapWidgetV2 } from '../lib/maps/map-widget2'; +import { FormattedData, MapProviders, TripAnimationSettings } from '../lib/maps/map-models'; +import { addCondition, addGroupInfo, addToSchema, initSchema } from '@app/core/schema-utils'; +import { mapPolygonSchema, pathSchema, pointSchema, tripAnimationSchema } from '../lib/maps/schemes'; +import { DomSanitizer } from '@angular/platform-browser'; +import { WidgetContext } from '@app/modules/home/models/widget-component.models'; +import { + findAngle, + getRatio, + interpolateOnLineSegment, + parseArray, + parseFunction, + parseWithTranslation, + safeExecute +} from '../lib/maps/maps-utils'; +import { JsonSettingsSchema, WidgetConfig } from '@shared/models/widget.models'; +import moment from 'moment'; +import { isUndefined } from '@core/utils'; +import { ResizeObserver } from '@juggle/resize-observer'; + + +@Component({ + // tslint:disable-next-line:component-selector + selector: 'trip-animation', + templateUrl: './trip-animation.component.html', + styleUrls: ['./trip-animation.component.scss'] +}) +export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy { + + private mapResize$: ResizeObserver; + + constructor(private cd: ChangeDetectorRef, private sanitizer: DomSanitizer) { } + + @Input() ctx: WidgetContext; + + @ViewChild('map') mapContainer; + + mapWidget: MapWidgetController; + historicalData: FormattedData[][]; + normalizationStep: number; + interpolatedTimeData = []; + widgetConfig: WidgetConfig; + settings: TripAnimationSettings; + mainTooltip = ''; + visibleTooltip = false; + activeTrip: FormattedData; + label: string; + minTime: number; + maxTime: number; + anchors: number[] = []; + useAnchors: boolean; + currentTime: number; + + static getSettingsSchema(): JsonSettingsSchema { + const schema = initSchema(); + addToSchema(schema, TbMapWidgetV2.getProvidersSchema(null, true)); + addGroupInfo(schema, 'Map Provider Settings'); + addToSchema(schema, tripAnimationSchema); + addGroupInfo(schema, 'Trip Animation Settings'); + addToSchema(schema, pathSchema); + addGroupInfo(schema, 'Path Settings'); + addToSchema(schema, addCondition(pointSchema, 'model.showPoints === true', ['showPoints'])); + addGroupInfo(schema, 'Path Points Settings'); + addToSchema(schema, addCondition(mapPolygonSchema, 'model.showPolygon === true', ['showPolygon'])); + addGroupInfo(schema, 'Polygon Settings'); + return schema; + } + + ngOnInit(): void { + this.widgetConfig = this.ctx.widgetConfig; + const settings = { + normalizationStep: 1000, + showLabel: false, + buttonColor: tinycolor(this.widgetConfig.color).setAlpha(0.54).toRgbString(), + disabledButtonColor: tinycolor(this.widgetConfig.color).setAlpha(0.3).toRgbString(), + rotationAngle: 0 + } + this.settings = { ...settings, ...this.ctx.settings }; + this.useAnchors = this.settings.showPoints && this.settings.usePointAsAnchor; + this.settings.pointAsAnchorFunction = parseFunction(this.settings.pointAsAnchorFunction, ['data', 'dsData', 'dsIndex']); + this.settings.tooltipFunction = parseFunction(this.settings.tooltipFunction, ['data', 'dsData', 'dsIndex']); + this.settings.labelFunction = parseFunction(this.settings.labelFunction, ['data', 'dsData', 'dsIndex']); + this.normalizationStep = this.settings.normalizationStep; + const subscription = this.ctx.defaultSubscription; + subscription.callbacks.onDataUpdated = () => { + this.historicalData = parseArray(this.ctx.data).filter(arr => arr.length); + if (this.historicalData.length) { + this.calculateIntervals(); + this.timeUpdated(this.currentTime && this.currentTime > this.minTime ? this.currentTime : this.minTime); + } + this.mapWidget.map.map?.invalidateSize(); + this.cd.detectChanges(); + } + } + + ngAfterViewInit() { + this.mapWidget = new MapWidgetController(MapProviders.openstreet, false, this.ctx, this.mapContainer.nativeElement); + this.mapResize$ = new ResizeObserver(() => { + this.mapWidget.resize(); + }); + this.mapResize$.observe(this.mapContainer.nativeElement); + } + + ngOnDestroy() { + if (this.mapResize$) { + this.mapResize$.disconnect(); + } + } + + timeUpdated(time: number) { + this.currentTime = time; + const currentPosition = this.interpolatedTimeData + .map(dataSource => dataSource[time]) + .filter(ds => ds); + if (isUndefined(currentPosition[0])) { + const timePoints = Object.keys(this.interpolatedTimeData[0]).map(item => parseInt(item, 10)); + for (let i = 1; i < timePoints.length; i++) { + if (timePoints[i - 1] < time && timePoints[i] > time) { + const beforePosition = this.interpolatedTimeData[0][timePoints[i - 1]]; + const afterPosition = this.interpolatedTimeData[0][timePoints[i]]; + const ratio = getRatio(timePoints[i - 1], timePoints[i], time); + currentPosition[0] = { + ...beforePosition, + time, + ...interpolateOnLineSegment(beforePosition, afterPosition, this.settings.latKeyName, this.settings.lngKeyName, ratio) + } + break; + } + } + } + this.calcLabel(); + this.calcTooltip(currentPosition.find(position => position.entityName === this.activeTrip.entityName)); + if (this.mapWidget && this.mapWidget.map && this.mapWidget.map.map) { + this.mapWidget.map.updatePolylines(this.interpolatedTimeData.map(ds => _.values(ds)), true, this.activeTrip); + if (this.settings.showPolygon) { + this.mapWidget.map.updatePolygons(this.interpolatedTimeData); + } + if (this.settings.showPoints) { + this.mapWidget.map.updatePoints(_.values(_.union(this.interpolatedTimeData)[0]), this.calcTooltip); + } + this.mapWidget.map.updateMarkers(currentPosition, true, (trip) => { + this.activeTrip = trip; + this.timeUpdated(this.currentTime) + }); + } + } + + setActiveTrip() { + } + + calculateIntervals() { + this.historicalData.forEach((dataSource, index) => { + this.minTime = dataSource[0]?.time || Infinity; + this.maxTime = dataSource[dataSource.length - 1]?.time || -Infinity; + this.interpolatedTimeData[index] = this.interpolateArray(dataSource); + }); + if(!this.activeTrip){ + this.activeTrip = this.interpolatedTimeData.map(dataSource => dataSource[this.minTime]).filter(ds => ds)[0]; + } + if (this.useAnchors) { + const anchorDate = Object.entries(_.union(this.interpolatedTimeData)[0]); + this.anchors = anchorDate + .filter((data: [string, FormattedData]) => safeExecute(this.settings.pointAsAnchorFunction, [data[1], anchorDate, data[1].dsIndex])) + .map(data => parseInt(data[0], 10)); + } + } + + calcTooltip = (point?: FormattedData): string => { + const data = point ? point : this.activeTrip; + const tooltipPattern: string = this.settings.useTooltipFunction ? + safeExecute(this.settings.tooltipFunction, [data, this.historicalData, point.dsIndex]) : this.settings.tooltipPattern; + const tooltipText = parseWithTranslation.parseTemplate(tooltipPattern, data, true); + this.mainTooltip = this.sanitizer.sanitize( + SecurityContext.HTML, tooltipText); + this.cd.detectChanges(); + this.activeTrip = point; + return tooltipText; + } + + calcLabel() { + const data = this.activeTrip; + const labelText: string = this.settings.useLabelFunction ? + safeExecute(this.settings.labelFunction, [data, this.historicalData, data.dsIndex]) : this.settings.label; + this.label = (parseWithTranslation.parseTemplate(labelText, data, true)); + } + + interpolateArray(originData: FormattedData[]) { + const result = {}; + const latKeyName = this.settings.latKeyName; + const lngKeyName = this.settings.lngKeyName; + for (const data of originData) { + const currentTime = data.time; + const normalizeTime = this.minTime + Math.ceil((currentTime - this.minTime) / this.normalizationStep) * this.normalizationStep; + result[normalizeTime] = { + ...data, + minTime: this.minTime !== Infinity ? moment(this.minTime).format('YYYY-MM-DD HH:mm:ss') : '', + maxTime: this.maxTime !== -Infinity ? moment(this.maxTime).format('YYYY-MM-DD HH:mm:ss') : '', + rotationAngle: this.settings.rotationAngle + }; + } + const timeStamp = Object.keys(result); + for (let i = 0; i < timeStamp.length - 1; i++) { + result[timeStamp[i]].rotationAngle += findAngle(result[timeStamp[i]], result[timeStamp[i + 1]], latKeyName, lngKeyName) + } + return result; + } +} + +export let TbTripAnimationWidget = TripAnimationComponent; diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts new file mode 100644 index 0000000000..04dc845450 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts @@ -0,0 +1,485 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Inject, Injectable, Optional, Type } from '@angular/core'; +import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service'; +import { WidgetService } from '@core/http/widget.service'; +import { forkJoin, Observable, of, ReplaySubject, Subject, throwError } from 'rxjs'; +import { + ErrorWidgetType, + MissingWidgetType, + toWidgetInfo, + toWidgetType, + WidgetInfo, + WidgetTypeInstance +} from '@home/models/widget-component.models'; +import cssjs from '@core/css/css'; +import { UtilsService } from '@core/services/utils.service'; +import { ResourcesService } from '@core/services/resources.service'; +import { Widget, widgetActionSources, WidgetControllerDescriptor, WidgetType } from '@shared/models/widget.models'; +import { catchError, map, mergeMap, switchMap } from 'rxjs/operators'; +import { isFunction, isUndefined } from '@core/utils'; +import { TranslateService } from '@ngx-translate/core'; +import { DynamicWidgetComponent } from '@home/components/widget/dynamic-widget.component'; +import { WidgetComponentsModule } from '@home/components/widget/widget-components.module'; +import { WINDOW } from '@core/services/window.service'; + +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { WidgetTypeId } from '@app/shared/models/id/widget-type-id'; +import { TenantId } from '@app/shared/models/id/tenant-id'; +import { SharedModule } from '@shared/shared.module'; +import { MODULES_MAP } from '@shared/public-api'; + +// @dynamic +@Injectable() +export class WidgetComponentService { + + private cssParser = new cssjs(); + + private widgetsInfoInMemoryCache = new Map(); + + private widgetsInfoFetchQueue = new Map>>(); + + private init$: Observable; + + private missingWidgetType: WidgetInfo; + private errorWidgetType: WidgetInfo; + private editingWidgetType: WidgetType; + + constructor(@Inject(WINDOW) private window: Window, + @Optional() @Inject(MODULES_MAP) private modulesMap: {[key: string]: any}, + private dynamicComponentFactoryService: DynamicComponentFactoryService, + private widgetService: WidgetService, + private utils: UtilsService, + private resources: ResourcesService, + private translate: TranslateService) { + + this.cssParser.testMode = false; + + this.widgetService.onWidgetTypeUpdated().subscribe((widgetType) => { + this.deleteWidgetInfoFromCache(widgetType.bundleAlias, widgetType.alias, widgetType.tenantId.id === NULL_UUID); + }); + + this.widgetService.onWidgetBundleDeleted().subscribe((widgetsBundle) => { + this.deleteWidgetsBundleFromCache(widgetsBundle.alias, widgetsBundle.tenantId.id === NULL_UUID); + }); + + this.init(); + } + + private init(): Observable { + if (this.init$) { + return this.init$; + } else { + this.missingWidgetType = {...MissingWidgetType}; + this.errorWidgetType = {...ErrorWidgetType}; + if (this.utils.widgetEditMode) { + this.editingWidgetType = toWidgetType( + { + widgetName: this.utils.editWidgetInfo.widgetName, + alias: 'customWidget', + type: this.utils.editWidgetInfo.type, + sizeX: this.utils.editWidgetInfo.sizeX, + sizeY: this.utils.editWidgetInfo.sizeY, + resources: this.utils.editWidgetInfo.resources, + templateHtml: this.utils.editWidgetInfo.templateHtml, + templateCss: this.utils.editWidgetInfo.templateCss, + controllerScript: this.utils.editWidgetInfo.controllerScript, + settingsSchema: this.utils.editWidgetInfo.settingsSchema, + dataKeySettingsSchema: this.utils.editWidgetInfo.dataKeySettingsSchema, + defaultConfig: this.utils.editWidgetInfo.defaultConfig + }, new WidgetTypeId('1'), new TenantId( NULL_UUID ), 'customWidgetBundle' + ); + } + const initSubject = new ReplaySubject(); + this.init$ = initSubject.asObservable(); + const loadDefaultWidgetInfoTasks = [ + this.loadWidgetResources(this.missingWidgetType, 'global-widget-missing-type', [SharedModule, WidgetComponentsModule]), + this.loadWidgetResources(this.errorWidgetType, 'global-widget-error-type', [SharedModule, WidgetComponentsModule]), + ]; + forkJoin(loadDefaultWidgetInfoTasks).subscribe( + () => { + initSubject.next(); + }, + (e) => { + let errorMessages = ['Failed to load default widget types!']; + if (e && e.length) { + errorMessages = errorMessages.concat(e); + } + console.error('Failed to load default widget types!'); + initSubject.error({ + widgetInfo: this.errorWidgetType, + errorMessages + }); + } + ); + return this.init$; + } + } + + public getInstantWidgetInfo(widget: Widget): WidgetInfo { + const widgetInfo = this.getWidgetInfoFromCache(widget.bundleAlias, widget.typeAlias, widget.isSystemType); + if (widgetInfo) { + return widgetInfo; + } else { + return {} as WidgetInfo; + } + } + + public getWidgetInfo(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): Observable { + return this.init().pipe( + mergeMap(() => this.getWidgetInfoInternal(bundleAlias, widgetTypeAlias, isSystem)) + ); + } + + private getWidgetInfoInternal(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): Observable { + const widgetInfoSubject = new ReplaySubject(); + const widgetInfo = this.getWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem); + if (widgetInfo) { + widgetInfoSubject.next(widgetInfo); + widgetInfoSubject.complete(); + } else { + if (this.utils.widgetEditMode) { + this.loadWidget(this.editingWidgetType, bundleAlias, isSystem, widgetInfoSubject); + } else { + const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); + let fetchQueue = this.widgetsInfoFetchQueue.get(key); + if (fetchQueue) { + fetchQueue.push(widgetInfoSubject); + } else { + fetchQueue = new Array>(); + this.widgetsInfoFetchQueue.set(key, fetchQueue); + this.widgetService.getWidgetType(bundleAlias, widgetTypeAlias, isSystem, {ignoreErrors: true}).subscribe( + (widgetType) => { + this.loadWidget(widgetType, bundleAlias, isSystem, widgetInfoSubject); + }, + () => { + widgetInfoSubject.next(this.missingWidgetType); + widgetInfoSubject.complete(); + this.resolveWidgetsInfoFetchQueue(key, this.missingWidgetType); + } + ); + } + } + } + return widgetInfoSubject.asObservable(); + } + + private loadWidget(widgetType: WidgetType, bundleAlias: string, isSystem: boolean, widgetInfoSubject: Subject) { + const widgetInfo = toWidgetInfo(widgetType); + const key = this.createWidgetInfoCacheKey(bundleAlias, widgetInfo.alias, isSystem); + let widgetControllerDescriptor: WidgetControllerDescriptor = null; + try { + widgetControllerDescriptor = this.createWidgetControllerDescriptor(widgetInfo, key); + } catch (e) { + const details = this.utils.parseException(e); + const errorMessage = `Failed to compile widget script. \n Error: ${details.message}`; + this.processWidgetLoadError([errorMessage], key, widgetInfoSubject); + } + if (widgetControllerDescriptor) { + const widgetNamespace = `widget-type-${(isSystem ? 'sys-' : '')}${bundleAlias}-${widgetInfo.alias}`; + this.loadWidgetResources(widgetInfo, widgetNamespace, [SharedModule, WidgetComponentsModule]).subscribe( + () => { + if (widgetControllerDescriptor.settingsSchema) { + widgetInfo.typeSettingsSchema = widgetControllerDescriptor.settingsSchema; + } + if (widgetControllerDescriptor.dataKeySettingsSchema) { + widgetInfo.typeDataKeySettingsSchema = widgetControllerDescriptor.dataKeySettingsSchema; + } + widgetInfo.typeParameters = widgetControllerDescriptor.typeParameters; + widgetInfo.actionSources = widgetControllerDescriptor.actionSources; + widgetInfo.widgetTypeFunction = widgetControllerDescriptor.widgetTypeFunction; + this.putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem); + if (widgetInfoSubject) { + widgetInfoSubject.next(widgetInfo); + widgetInfoSubject.complete(); + } + this.resolveWidgetsInfoFetchQueue(key, widgetInfo); + }, + (errorMessages: string[]) => { + this.processWidgetLoadError(errorMessages, key, widgetInfoSubject); + } + ); + } + } + + private loadWidgetResources(widgetInfo: WidgetInfo, widgetNamespace: string, modules?: Type[]): Observable { + this.cssParser.cssPreviewNamespace = widgetNamespace; + this.cssParser.createStyleElement(widgetNamespace, widgetInfo.templateCss); + const resourceTasks: Observable[] = []; + const modulesTasks: Observable[] | string>[] = []; + if (widgetInfo.resources.length > 0) { + widgetInfo.resources.filter(r => r.isModule).forEach( + (resource) => { + modulesTasks.push( + this.resources.loadModules(resource.url, this.modulesMap).pipe( + catchError((e: Error) => of(e?.message ? e.message : `Failed to load widget resource module: '${resource.url}'`)) + ) + ); + } + ); + } + widgetInfo.resources.filter(r => !r.isModule).forEach( + (resource) => { + resourceTasks.push( + this.resources.loadResource(resource.url).pipe( + catchError(e => of(`Failed to load widget resource: '${resource.url}'`)) + ) + ); + } + ); + + let modulesObservable: Observable[]>; + if (modulesTasks.length) { + modulesObservable = forkJoin(modulesTasks).pipe( + map(res => { + const msg = res.find(r => typeof r === 'string'); + if (msg) { + return msg as string; + } else { + let resModules = (res as Type[][]).flat(); + if (modules && modules.length) { + resModules = resModules.concat(modules); + } + return resModules; + } + }) + ); + } else { + modulesObservable = modules && modules.length ? of(modules) : of([]); + } + + resourceTasks.push( + modulesObservable.pipe( + mergeMap((resolvedModules) => { + if (typeof resolvedModules === 'string') { + return of(resolvedModules); + } else { + return this.dynamicComponentFactoryService.createDynamicComponentFactory( + class DynamicWidgetComponentInstance extends DynamicWidgetComponent {}, + widgetInfo.templateHtml, + resolvedModules + ).pipe( + map((factory) => { + widgetInfo.componentFactory = factory; + return null; + }), + catchError(e => { + const details = this.utils.parseException(e); + const errorMessage = `Failed to compile widget html. \n Error: ${details.message}`; + return of(errorMessage); + }) + ); + } + })) + ); + return forkJoin(resourceTasks).pipe( + switchMap(msgs => { + let errors: string[]; + if (msgs && msgs.length) { + errors = msgs.filter(msg => msg && msg.length > 0); + } + if (errors && errors.length) { + return throwError(errors); + } else { + return of(null); + } + } + )); + } + + private createWidgetControllerDescriptor(widgetInfo: WidgetInfo, name: string): WidgetControllerDescriptor { + let widgetTypeFunctionBody = `return function ${name} (ctx) {\n` + + ' var self = this;\n' + + ' self.ctx = ctx;\n\n'; /*+ + + ' self.onInit = function() {\n\n' + + + ' }\n\n' + + + ' self.onDataUpdated = function() {\n\n' + + + ' }\n\n' + + + ' self.useCustomDatasources = function() {\n\n' + + + ' }\n\n' + + + ' self.typeParameters = function() {\n\n' + + return { + useCustomDatasources: false, + maxDatasources: -1, //unlimited + maxDataKeys: -1, //unlimited + dataKeysOptional: false, + stateData: false + }; + ' }\n\n' + + + ' self.actionSources = function() {\n\n' + + return { + 'headerButton': { + name: 'Header button', + multiple: true + } + }; + }\n\n' + + ' self.onResize = function() {\n\n' + + + ' }\n\n' + + + ' self.onEditModeChanged = function() {\n\n' + + + ' }\n\n' + + + ' self.onMobileModeChanged = function() {\n\n' + + + ' }\n\n' + + + ' self.getSettingsSchema = function() {\n\n' + + + ' }\n\n' + + + ' self.getDataKeySettingsSchema = function() {\n\n' + + + ' }\n\n' + + + ' self.onDestroy = function() {\n\n' + + + ' }\n\n' + + '}';*/ + + widgetTypeFunctionBody += widgetInfo.controllerScript; + widgetTypeFunctionBody += '\n};\n'; + + try { + + const widgetTypeFunction = new Function(widgetTypeFunctionBody); + const widgetType = widgetTypeFunction.apply(this); + const widgetTypeInstance: WidgetTypeInstance = new widgetType(); + const result: WidgetControllerDescriptor = { + widgetTypeFunction: widgetType + }; + if (isFunction(widgetTypeInstance.getSettingsSchema)) { + result.settingsSchema = widgetTypeInstance.getSettingsSchema(); + } + if (isFunction(widgetTypeInstance.getDataKeySettingsSchema)) { + result.dataKeySettingsSchema = widgetTypeInstance.getDataKeySettingsSchema(); + } + if (isFunction(widgetTypeInstance.typeParameters)) { + result.typeParameters = widgetTypeInstance.typeParameters(); + } else { + result.typeParameters = {}; + } + if (isFunction(widgetTypeInstance.useCustomDatasources)) { + result.typeParameters.useCustomDatasources = widgetTypeInstance.useCustomDatasources(); + } else { + result.typeParameters.useCustomDatasources = false; + } + if (isUndefined(result.typeParameters.hasDataPageLink)) { + result.typeParameters.hasDataPageLink = false; + } + if (isUndefined(result.typeParameters.maxDatasources)) { + result.typeParameters.maxDatasources = -1; + } + if (isUndefined(result.typeParameters.maxDataKeys)) { + result.typeParameters.maxDataKeys = -1; + } + if (isUndefined(result.typeParameters.singleEntity)) { + result.typeParameters.singleEntity = false; + } + if (isUndefined(result.typeParameters.warnOnPageDataOverflow)) { + result.typeParameters.warnOnPageDataOverflow = true; + } + if (isUndefined(result.typeParameters.dataKeysOptional)) { + result.typeParameters.dataKeysOptional = false; + } + if (isUndefined(result.typeParameters.stateData)) { + result.typeParameters.stateData = false; + } + if (isFunction(widgetTypeInstance.actionSources)) { + result.actionSources = widgetTypeInstance.actionSources(); + } else { + result.actionSources = {}; + } + for (const actionSourceId of Object.keys(widgetActionSources)) { + result.actionSources[actionSourceId] = {...widgetActionSources[actionSourceId]}; + result.actionSources[actionSourceId].name = this.translate.instant(result.actionSources[actionSourceId].name); + } + return result; + } catch (e) { + this.utils.processWidgetException(e); + throw e; + } + } + + private processWidgetLoadError(errorMessages: string[], cacheKey: string, widgetInfoSubject: Subject) { + if (widgetInfoSubject) { + widgetInfoSubject.error({ + widgetInfo: this.errorWidgetType, + errorMessages + }); + } + this.resolveWidgetsInfoFetchQueue(cacheKey, this.errorWidgetType, errorMessages); + } + + private resolveWidgetsInfoFetchQueue(key: string, widgetInfo: WidgetInfo, errorMessages?: string[]) { + const fetchQueue = this.widgetsInfoFetchQueue.get(key); + if (fetchQueue) { + fetchQueue.forEach(subject => { + if (!errorMessages) { + subject.next(widgetInfo); + subject.complete(); + } else { + subject.error({ + widgetInfo, + errorMessages + }); + } + }); + this.widgetsInfoFetchQueue.delete(key); + } + } + + // Cache functions + + private createWidgetInfoCacheKey(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): string { + return `${isSystem ? 'sys_' : ''}${bundleAlias}_${widgetTypeAlias}`; + } + + private getWidgetInfoFromCache(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): WidgetInfo | undefined { + const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); + return this.widgetsInfoInMemoryCache.get(key); + } + + private putWidgetInfoToCache(widgetInfo: WidgetInfo, bundleAlias: string, widgetTypeAlias: string, isSystem: boolean) { + const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); + this.widgetsInfoInMemoryCache.set(key, widgetInfo); + } + + private deleteWidgetInfoFromCache(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean) { + const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); + this.widgetsInfoInMemoryCache.delete(key); + } + + private deleteWidgetsBundleFromCache(bundleAlias: string, isSystem: boolean) { + const key = (isSystem ? 'sys_' : '') + bundleAlias; + this.widgetsInfoInMemoryCache.forEach((widgetInfo, cacheKey) => { + if (cacheKey.startsWith(key)) { + this.widgetsInfoInMemoryCache.delete(cacheKey); + } + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts new file mode 100644 index 0000000000..c2a158fcdf --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -0,0 +1,78 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@app/shared/shared.module'; +import { EntitiesTableWidgetComponent } from '@home/components/widget/lib/entities-table-widget.component'; +import { DisplayColumnsPanelComponent } from '@home/components/widget/lib/display-columns-panel.component'; +import { AlarmsTableWidgetComponent } from '@home/components/widget/lib/alarms-table-widget.component'; +import { AlarmFilterPanelComponent } from '@home/components/widget/lib/alarm-filter-panel.component'; +import { SharedHomeComponentsModule } from '@home/components/shared-home-components.module'; +import { TimeseriesTableWidgetComponent } from '@home/components/widget/lib/timeseries-table-widget.component'; +import { EntitiesHierarchyWidgetComponent } from '@home/components/widget/lib/entities-hierarchy-widget.component'; +import { CustomDialogService } from '@home/components/widget/dialog/custom-dialog.service'; +import { RpcWidgetsModule } from '@home/components/widget/lib/rpc/rpc-widgets.module'; +import { + DateRangeNavigatorPanelComponent, + DateRangeNavigatorWidgetComponent +} from '@home/components/widget/lib/date-range-navigator/date-range-navigator.component'; +import { MultipleInputWidgetComponent } from './lib/multiple-input-widget.component'; +import { TripAnimationComponent } from './trip-animation/trip-animation.component'; +import { PhotoCameraInputWidgetComponent } from './lib/photo-camera-input.component'; +import { GatewayFormComponent } from './lib/gateway/gateway-form.component'; +import { ImportExportService } from '@home/components/import-export/import-export.service'; + +@NgModule({ + declarations: + [ + DisplayColumnsPanelComponent, + AlarmFilterPanelComponent, + EntitiesTableWidgetComponent, + AlarmsTableWidgetComponent, + TimeseriesTableWidgetComponent, + EntitiesHierarchyWidgetComponent, + DateRangeNavigatorWidgetComponent, + DateRangeNavigatorPanelComponent, + MultipleInputWidgetComponent, + TripAnimationComponent, + PhotoCameraInputWidgetComponent, + GatewayFormComponent + ], + imports: [ + CommonModule, + SharedModule, + RpcWidgetsModule, + SharedHomeComponentsModule + ], + exports: [ + EntitiesTableWidgetComponent, + AlarmsTableWidgetComponent, + TimeseriesTableWidgetComponent, + EntitiesHierarchyWidgetComponent, + RpcWidgetsModule, + DateRangeNavigatorWidgetComponent, + MultipleInputWidgetComponent, + TripAnimationComponent, + PhotoCameraInputWidgetComponent, + GatewayFormComponent + ], + providers: [ + CustomDialogService, + ImportExportService + ] +}) +export class WidgetComponentsModule { } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html new file mode 100644 index 0000000000..fc3fbccdda --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -0,0 +1,430 @@ + + + +
+
+
+ + {{ 'widget-config.use-dashboard-timewindow' | translate }} + + + {{ 'widget-config.display-timewindow' | translate }} + +
+
+ widget-config.timewindow + +
+
+
+
+ + alarm.alarm-status-list + + + {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} + + + + + alarm.alarm-severity-list + + + {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} + + + +
+
+ + alarm.alarm-type-list + + + {{type}} + cancel + + + + + + {{ 'alarm.search-propagated-alarms' | translate }} + +
+
+ + + +
widget-config.datasources
+
{{ 'widget-config.maximum-datasources' | translate:{count: modelValue?.typeParameters.maxDatasources} }}
+
+
+
+ datasource.add-datasource-prompt +
+ +
+ +
+ widget-config.datasource-type + widget-config.datasource-parameters + +
+
+
+
+ {{$index + 1}}. +
+
+ + + + {{ datasourceTypesTranslations.get(datasourceType) | translate }} + + + +
+ + + + + + + +
+ + + + +
+
+
+ + +
+ +
+
+
+
+
+ +
+
+ + + + {{ 'widget-config.target-device' | translate }} + + +
+ + +
+
+ + + + {{ 'widget-config.alarm-source' | translate }} + + +
+
+ + + + {{ datasourceTypesTranslations.get(datasourceType) | translate }} + + + +
+ + + + + + + + + + + + +
+ + +
+
+
+
+
+ +
+
+ widget-config.general-settings +
+
+ + widget-config.title + + + + widget-config.title-tooltip + + +
+
+ +
+
+
+
+ + {{ 'widget-config.display-icon' | translate }} + + + +
+
+ + + + widget-config.icon-size + + +
+
+
+
+ + {{ 'widget-config.display-title' | translate }} + + + {{ 'widget-config.drop-shadow' | translate }} + + + {{ 'widget-config.enable-fullscreen' | translate }} + +
+
+ +
+
+
+
+ + + + +
+
+ + widget-config.padding + + + + widget-config.margin + + +
+
+
+ + widget-config.units + + + + widget-config.decimals + + +
+
+ + {{ 'widget-config.display-legend' | translate }} + +
+ + +
+
+
+
+ widget-config.mobile-mode-settings +
+ + widget-config.order + + + + widget-config.height + + +
+
+
+
+ +
+ + +
+
+ + + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.models.ts new file mode 100644 index 0000000000..4335162656 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.models.ts @@ -0,0 +1,22 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityAliasSelectCallbacks } from '../alias/entity-alias-select.component.models'; +import { DataKeysCallbacks } from './data-keys.component.models'; +import { WidgetActionCallbacks } from './action/manage-widget-actions.component.models'; +import { FilterSelectCallbacks } from '@home/components/filter/filter-select.component.models'; + +export type WidgetConfigCallbacks = EntityAliasSelectCallbacks & FilterSelectCallbacks & DataKeysCallbacks & WidgetActionCallbacks; diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss new file mode 100644 index 0000000000..375f2f960c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss @@ -0,0 +1,122 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../scss/constants'; + +:host { + .tb-widget-config { + .tb-advanced-widget-config { + height: 100%; + } + .tb-advanced-widget-config { + height: 100%; + } + .tb-datasource-type { + min-width: 110px; + @media #{$mat-gt-sm} { + max-width: 110px; + } + } + .tb-datasource { + @media #{$mat-gt-sm} { + padding-left: 8px; + } + } + .tb-datasource-name { + @media #{$mat-gt-sm} { + width: 200px; + max-width: 200px; + } + } + .tb-data-keys { + @media #{$mat-gt-sm} { + padding-left: 8px; + } + } + } +} + +:host ::ng-deep { + .tb-widget-config { + .mat-tab-body-wrapper { + position: absolute; + top: 49px; + left: 0; + right: 0; + bottom: 0; + } + .tb-panel-hint { + font-size: 12px; + color: #808080; + } + //.mat-tab-body.mat-tab-body-active { + // .mat-tab-body-content > div { + // height: 100%; + // } + //} + .mat-checkbox-label { + white-space: normal; + } + .mat-expansion-panel { + &.tb-datasources{ + &.mat-expanded { + overflow: visible; + } + .mat-expansion-panel-body{ + padding: 0 12px 16px; + } + } + .mat-expansion-panel-content { + font: inherit; + } + } + .tb-datasource-type { + .mat-form-field-infix { + width: 100%; + } + .mat-select-value { + min-width: 92px; + } + } + .tb-datasource { + tb-entity-alias-select { + .mat-form-field { + margin-top: 20px; + } + @media #{$mat-gt-sm} { + .mat-form-field { + width: 200px; + max-width: 200px; + } + } + } + tb-filter-select { + @media #{$mat-gt-sm} { + .mat-form-field { + width: 200px; + max-width: 200px; + } + } + } + } + .tb-data-keys { + @media #{$mat-gt-sm} { + .mat-input-element { + min-height: 32px; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts new file mode 100644 index 0000000000..ce37c46326 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -0,0 +1,907 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + DataKey, + Datasource, + DatasourceType, + datasourceTypeTranslationMap, + defaultLegendConfig, + GroupInfo, + JsonSchema, + widgetType +} from '@shared/models/widget.models'; +import { + ControlValueAccessor, + FormArray, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { WidgetConfigComponentData } from '@home/models/widget-component.models'; +import { deepClone, isDefined, isObject } from '@app/core/utils'; +import { + alarmFields, + AlarmSearchStatus, + alarmSearchStatusTranslations, + AlarmSeverity, + alarmSeverityTranslations +} from '@shared/models/alarm.models'; +import { IAliasController } from '@core/api/widget-api.models'; +import { EntityAlias, EntityAliases } from '@shared/models/alias.models'; +import { UtilsService } from '@core/services/utils.service'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { TranslateService } from '@ngx-translate/core'; +import { EntityType } from '@shared/models/entity-type.models'; +import { forkJoin, Observable, of, Subscription } from 'rxjs'; +import { WidgetConfigCallbacks } from '@home/components/widget/widget-config.component.models'; +import { + EntityAliasDialogComponent, + EntityAliasDialogData +} from '@home/components/alias/entity-alias-dialog.component'; +import { catchError, map, mergeMap, tap } from 'rxjs/operators'; +import { MatDialog } from '@angular/material/dialog'; +import { EntityService } from '@core/http/entity.service'; +import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models'; +import { WidgetActionsData } from './action/manage-widget-actions.component.models'; +import { DashboardState } from '@shared/models/dashboard.models'; +import { entityFields } from '@shared/models/entity.models'; +import { Filter, Filters } from '@shared/models/query/query.models'; +import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component'; +import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; +import { MatChipInputEvent } from '@angular/material/chips'; + +const emptySettingsSchema: JsonSchema = { + type: 'object', + properties: {} +}; +const emptySettingsGroupInfoes: GroupInfo[] = []; +const defaultSettingsForm = [ + '*' +]; + +@Component({ + selector: 'tb-widget-config', + templateUrl: './widget-config.component.html', + styleUrls: ['./widget-config.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => WidgetConfigComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => WidgetConfigComponent), + multi: true, + } + ] +}) +export class WidgetConfigComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { + + readonly separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; + + widgetTypes = widgetType; + + entityTypes = EntityType; + + alarmSearchStatuses = [AlarmSearchStatus.ACTIVE, + AlarmSearchStatus.CLEARED, + AlarmSearchStatus.ACK, + AlarmSearchStatus.UNACK]; + + alarmSearchStatusTranslationMap = alarmSearchStatusTranslations; + + alarmSeverities = Object.keys(AlarmSeverity); + alarmSeverityEnum = AlarmSeverity; + + alarmSeverityTranslationMap = alarmSeverityTranslations; + + @Input() + forceExpandDatasources: boolean; + + @Input() + aliasController: IAliasController; + + @Input() + entityAliases: EntityAliases; + + @Input() + filters: Filters; + + @Input() + functionsOnly: boolean; + + @Input() + dashboardStates: {[id: string]: DashboardState }; + + @Input() disabled: boolean; + + widgetType: widgetType; + + datasourceType = DatasourceType; + datasourceTypes: Array; + datasourceTypesTranslations = datasourceTypeTranslationMap; + + widgetConfigCallbacks: WidgetConfigCallbacks = { + createEntityAlias: this.createEntityAlias.bind(this), + createFilter: this.createFilter.bind(this), + generateDataKey: this.generateDataKey.bind(this), + fetchEntityKeys: this.fetchEntityKeys.bind(this), + fetchDashboardStates: this.fetchDashboardStates.bind(this) + }; + + widgetEditMode = this.utils.widgetEditMode; + + selectedTab: number; + + modelValue: WidgetConfigComponentData; + + private propagateChange = null; + + public dataSettings: FormGroup; + public targetDeviceSettings: FormGroup; + public alarmSourceSettings: FormGroup; + public widgetSettings: FormGroup; + public layoutSettings: FormGroup; + public advancedSettings: FormGroup; + public actionsSettings: FormGroup; + + private dataSettingsChangesSubscription: Subscription; + private targetDeviceSettingsSubscription: Subscription; + private alarmSourceSettingsSubscription: Subscription; + private widgetSettingsSubscription: Subscription; + private layoutSettingsSubscription: Subscription; + private advancedSettingsSubscription: Subscription; + private actionsSettingsSubscription: Subscription; + + constructor(protected store: Store, + private utils: UtilsService, + private entityService: EntityService, + private dialog: MatDialog, + private translate: TranslateService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + if (this.functionsOnly) { + this.datasourceTypes = [DatasourceType.function]; + } else { + this.datasourceTypes = [DatasourceType.function, DatasourceType.entity]; + } + this.dataSettings = this.fb.group({}); + this.targetDeviceSettings = this.fb.group({}); + this.alarmSourceSettings = this.fb.group({}); + this.advancedSettings = this.fb.group({}); + this.widgetSettings = this.fb.group({ + title: [null, []], + showTitleIcon: [null, []], + titleIcon: [null, []], + iconColor: [null, []], + iconSize: [null, []], + titleTooltip: [null, []], + showTitle: [null, []], + dropShadow: [null, []], + enableFullscreen: [null, []], + backgroundColor: [null, []], + color: [null, []], + padding: [null, []], + margin: [null, []], + widgetStyle: [null, []], + titleStyle: [null, []], + units: [null, []], + decimals: [null, [Validators.min(0), Validators.max(15), Validators.pattern(/^\d*$/)]], + showLegend: [null, []], + legendConfig: [null, []] + }); + this.widgetSettings.get('showTitleIcon').valueChanges.subscribe((value: boolean) => { + if (value) { + this.widgetSettings.get('titleIcon').enable({emitEvent: false}); + } else { + this.widgetSettings.get('titleIcon').disable({emitEvent: false}); + } + }); + this.widgetSettings.get('showLegend').valueChanges.subscribe((value: boolean) => { + if (value) { + this.widgetSettings.get('legendConfig').enable({emitEvent: false}); + } else { + this.widgetSettings.get('legendConfig').disable({emitEvent: false}); + } + }); + this.layoutSettings = this.fb.group({ + mobileOrder: [null, [Validators.pattern(/^-?[0-9]+$/)]], + mobileHeight: [null, [Validators.min(1), Validators.max(10), Validators.pattern(/^\d*$/)]] + }); + this.actionsSettings = this.fb.group({ + actionsData: [null, []] + }); + } + + private removeChangeSubscriptions() { + if (this.dataSettingsChangesSubscription) { + this.dataSettingsChangesSubscription.unsubscribe(); + this.dataSettingsChangesSubscription = null; + } + if (this.targetDeviceSettingsSubscription) { + this.targetDeviceSettingsSubscription.unsubscribe(); + this.targetDeviceSettingsSubscription = null; + } + if (this.alarmSourceSettingsSubscription) { + this.alarmSourceSettingsSubscription.unsubscribe(); + this.alarmSourceSettingsSubscription = null; + } + if (this.widgetSettingsSubscription) { + this.widgetSettingsSubscription.unsubscribe(); + this.widgetSettingsSubscription = null; + } + if (this.layoutSettingsSubscription) { + this.layoutSettingsSubscription.unsubscribe(); + this.layoutSettingsSubscription = null; + } + if (this.advancedSettingsSubscription) { + this.advancedSettingsSubscription.unsubscribe(); + this.advancedSettingsSubscription = null; + } + if (this.actionsSettingsSubscription) { + this.actionsSettingsSubscription.unsubscribe(); + this.actionsSettingsSubscription = null; + } + } + + private createChangeSubscriptions() { + this.dataSettingsChangesSubscription = this.dataSettings.valueChanges.subscribe( + () => this.updateDataSettings() + ); + this.targetDeviceSettingsSubscription = this.targetDeviceSettings.valueChanges.subscribe( + () => this.updateTargetDeviceSettings() + ); + this.alarmSourceSettingsSubscription = this.alarmSourceSettings.valueChanges.subscribe( + () => this.updateAlarmSourceSettings() + ); + this.widgetSettingsSubscription = this.widgetSettings.valueChanges.subscribe( + () => this.updateWidgetSettings() + ); + this.layoutSettingsSubscription = this.layoutSettings.valueChanges.subscribe( + () => this.updateLayoutSettings() + ); + this.advancedSettingsSubscription = this.advancedSettings.valueChanges.subscribe( + () => this.updateAdvancedSettings() + ); + this.actionsSettingsSubscription = this.actionsSettings.valueChanges.subscribe( + () => this.updateActionSettings() + ); + } + + private buildForms() { + this.dataSettings = this.fb.group({}); + this.targetDeviceSettings = this.fb.group({}); + this.alarmSourceSettings = this.fb.group({}); + this.advancedSettings = this.fb.group({}); + if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) { + this.dataSettings.addControl('useDashboardTimewindow', this.fb.control(null)); + this.dataSettings.addControl('displayTimewindow', this.fb.control(null)); + this.dataSettings.addControl('timewindow', this.fb.control(null)); + this.dataSettings.get('useDashboardTimewindow').valueChanges.subscribe((value: boolean) => { + if (value) { + this.dataSettings.get('displayTimewindow').disable({emitEvent: false}); + this.dataSettings.get('timewindow').disable({emitEvent: false}); + } else { + this.dataSettings.get('displayTimewindow').enable({emitEvent: false}); + this.dataSettings.get('timewindow').enable({emitEvent: false}); + } + }); + if (this.widgetType === widgetType.alarm) { + this.dataSettings.addControl('alarmStatusList', this.fb.control(null)); + this.dataSettings.addControl('alarmSeverityList', this.fb.control(null)); + this.dataSettings.addControl('alarmTypeList', this.fb.control(null)); + this.dataSettings.addControl('searchPropagatedAlarms', this.fb.control(null)); + } + } + if (this.modelValue.isDataEnabled) { + if (this.widgetType !== widgetType.rpc && + this.widgetType !== widgetType.alarm && + this.widgetType !== widgetType.static) { + this.dataSettings.addControl('datasources', + this.fb.array([])); + } else if (this.widgetType === widgetType.rpc) { + this.targetDeviceSettings.addControl('targetDeviceAliasId', + this.fb.control(null, + this.widgetEditMode ? [] : [Validators.required])); + } else if (this.widgetType === widgetType.alarm) { + this.alarmSourceSettings = this.buildDatasourceForm(); + } + } + this.advancedSettings.addControl('settings', + this.fb.control(null, [])); + } + + datasourcesFormArray(): FormArray { + return this.dataSettings.get('datasources') as FormArray; + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: WidgetConfigComponentData): void { + this.modelValue = value; + this.removeChangeSubscriptions(); + if (this.modelValue) { + if (this.widgetType !== this.modelValue.widgetType) { + this.widgetType = this.modelValue.widgetType; + this.buildForms(); + } + const config = this.modelValue.config; + const layout = this.modelValue.layout; + if (config) { + this.selectedTab = 0; + this.widgetSettings.patchValue({ + title: config.title, + showTitleIcon: isDefined(config.showTitleIcon) ? config.showTitleIcon : false, + titleIcon: isDefined(config.titleIcon) ? config.titleIcon : '', + iconColor: isDefined(config.iconColor) ? config.iconColor : 'rgba(0, 0, 0, 0.87)', + iconSize: isDefined(config.iconSize) ? config.iconSize : '24px', + titleTooltip: isDefined(config.titleTooltip) ? config.titleTooltip : '', + showTitle: config.showTitle, + dropShadow: isDefined(config.dropShadow) ? config.dropShadow : true, + enableFullscreen: isDefined(config.enableFullscreen) ? config.enableFullscreen : true, + backgroundColor: config.backgroundColor, + color: config.color, + padding: config.padding, + margin: config.margin, + widgetStyle: isDefined(config.widgetStyle) ? config.widgetStyle : {}, + titleStyle: isDefined(config.titleStyle) ? config.titleStyle : { + fontSize: '16px', + fontWeight: 400 + }, + units: config.units, + decimals: config.decimals, + showLegend: isDefined(config.showLegend) ? config.showLegend : + this.widgetType === widgetType.timeseries, + legendConfig: config.legendConfig || defaultLegendConfig(this.widgetType) + }, + {emitEvent: false} + ); + const showTitleIcon: boolean = this.widgetSettings.get('showTitleIcon').value; + if (showTitleIcon) { + this.widgetSettings.get('titleIcon').enable({emitEvent: false}); + } else { + this.widgetSettings.get('titleIcon').disable({emitEvent: false}); + } + const showLegend: boolean = this.widgetSettings.get('showLegend').value; + if (showLegend) { + this.widgetSettings.get('legendConfig').enable({emitEvent: false}); + } else { + this.widgetSettings.get('legendConfig').disable({emitEvent: false}); + } + const actionsData: WidgetActionsData = { + actionsMap: config.actions || {}, + actionSources: this.modelValue.actionSources || {} + }; + this.actionsSettings.patchValue( + { + actionsData + }, + {emitEvent: false} + ); + if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) { + const useDashboardTimewindow = isDefined(config.useDashboardTimewindow) ? + config.useDashboardTimewindow : true; + this.dataSettings.patchValue( + { useDashboardTimewindow }, {emitEvent: false} + ); + if (useDashboardTimewindow) { + this.dataSettings.get('displayTimewindow').disable({emitEvent: false}); + this.dataSettings.get('timewindow').disable({emitEvent: false}); + } else { + this.dataSettings.get('displayTimewindow').enable({emitEvent: false}); + this.dataSettings.get('timewindow').enable({emitEvent: false}); + } + this.dataSettings.patchValue( + { displayTimewindow: isDefined(config.displayTimewindow) ? + config.displayTimewindow : true }, {emitEvent: false} + ); + this.dataSettings.patchValue( + { timewindow: config.timewindow }, {emitEvent: false} + ); + } + if (this.modelValue.isDataEnabled) { + if (this.widgetType !== widgetType.rpc && + this.widgetType !== widgetType.alarm && + this.widgetType !== widgetType.static) { + const datasourcesFormArray = this.dataSettings.get('datasources') as FormArray; + datasourcesFormArray.clear(); + if (config.datasources) { + config.datasources.forEach((datasource) => { + datasourcesFormArray.push(this.buildDatasourceForm(datasource)); + }); + } + } else if (this.widgetType === widgetType.rpc) { + let targetDeviceAliasId: string; + if (config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0) { + const aliasId = config.targetDeviceAliasIds[0]; + const entityAliases = this.aliasController.getEntityAliases(); + if (entityAliases[aliasId]) { + targetDeviceAliasId = entityAliases[aliasId].id; + } else { + targetDeviceAliasId = null; + } + } else { + targetDeviceAliasId = null; + } + this.targetDeviceSettings.patchValue({ + targetDeviceAliasId + }, {emitEvent: false}); + } else if (this.widgetType === widgetType.alarm) { + let alarmStatusList: AlarmSearchStatus[] = []; + if (isDefined(config.alarmStatusList) && config.alarmStatusList.length) { + alarmStatusList = config.alarmStatusList; + } else if (isDefined(config.alarmSearchStatus) && config.alarmSearchStatus !== AlarmSearchStatus.ANY) { + alarmStatusList = [config.alarmSearchStatus]; + } + this.dataSettings.patchValue( + { alarmStatusList }, {emitEvent: false} + ); + this.dataSettings.patchValue( + { alarmSeverityList: isDefined(config.alarmSeverityList) ? config.alarmSeverityList : [] }, {emitEvent: false} + ); + this.dataSettings.patchValue( + { alarmTypeList: isDefined(config.alarmTypeList) ? config.alarmTypeList : [] }, {emitEvent: false} + ); + this.dataSettings.patchValue( + { searchPropagatedAlarms: isDefined(config.searchPropagatedAlarms) ? + config.searchPropagatedAlarms : true }, {emitEvent: false} + ); + this.alarmSourceSettings.patchValue( + config.alarmSource, {emitEvent: false} + ); + const alarmSourceType: DatasourceType = this.alarmSourceSettings.get('type').value; + this.alarmSourceSettings.get('entityAliasId').setValidators( + alarmSourceType === DatasourceType.entity ? [Validators.required] : [] + ); + this.alarmSourceSettings.get('entityAliasId').updateValueAndValidity({emitEvent: false}); + } + } + + this.updateSchemaForm(config.settings); + + if (layout) { + this.layoutSettings.patchValue( + { + mobileOrder: layout.mobileOrder, + mobileHeight: layout.mobileHeight + }, + {emitEvent: false} + ); + } else { + this.layoutSettings.patchValue( + { + mobileOrder: null, + mobileHeight: null + }, + {emitEvent: false} + ); + } + } + this.createChangeSubscriptions(); + } + } + + private buildDatasourceForm(datasource?: Datasource): FormGroup { + const dataKeysRequired = !this.modelValue.typeParameters || !this.modelValue.typeParameters.dataKeysOptional; + const datasourceFormGroup = this.fb.group( + { + type: [datasource ? datasource.type : null, [Validators.required]], + name: [datasource ? datasource.name : null, []], + entityAliasId: [datasource ? datasource.entityAliasId : null, + datasource && datasource.type === DatasourceType.entity ? [Validators.required] : []], + filterId: [datasource ? datasource.filterId : null, []], + dataKeys: [datasource ? datasource.dataKeys : null, dataKeysRequired ? [Validators.required] : []] + } + ); + datasourceFormGroup.get('type').valueChanges.subscribe((type: DatasourceType) => { + datasourceFormGroup.get('entityAliasId').setValidators( + type === DatasourceType.entity ? [Validators.required] : [] + ); + datasourceFormGroup.get('entityAliasId').updateValueAndValidity(); + }); + return datasourceFormGroup; + } + + private updateSchemaForm(settings?: any) { + const widgetSettingsFormData: JsonFormComponentData = {}; + if (this.modelValue.settingsSchema && this.modelValue.settingsSchema.schema) { + widgetSettingsFormData.schema = this.modelValue.settingsSchema.schema; + widgetSettingsFormData.form = this.modelValue.settingsSchema.form || deepClone(defaultSettingsForm); + widgetSettingsFormData.groupInfoes = this.modelValue.settingsSchema.groupInfoes; + widgetSettingsFormData.model = settings; + } else { + widgetSettingsFormData.schema = deepClone(emptySettingsSchema); + widgetSettingsFormData.form = deepClone(defaultSettingsForm); + widgetSettingsFormData.groupInfoes = deepClone(emptySettingsGroupInfoes); + widgetSettingsFormData.model = {}; + } + this.advancedSettings.patchValue({ settings: widgetSettingsFormData }, {emitEvent: false}); + } + + private updateDataSettings() { + if (this.modelValue) { + if (this.modelValue.config) { + Object.assign(this.modelValue.config, this.dataSettings.value); + } + this.propagateChange(this.modelValue); + } + } + + private updateTargetDeviceSettings() { + if (this.modelValue) { + if (this.modelValue.config) { + const targetDeviceAliasId: string = this.targetDeviceSettings.get('targetDeviceAliasId').value; + if (targetDeviceAliasId) { + this.modelValue.config.targetDeviceAliasIds = [targetDeviceAliasId]; + } else { + this.modelValue.config.targetDeviceAliasIds = []; + } + } + this.propagateChange(this.modelValue); + } + } + + private updateAlarmSourceSettings() { + if (this.modelValue) { + if (this.modelValue.config) { + const alarmSource: Datasource = this.alarmSourceSettings.value; + this.modelValue.config.alarmSource = alarmSource; + } + this.propagateChange(this.modelValue); + } + } + + private updateWidgetSettings() { + if (this.modelValue) { + if (this.modelValue.config) { + Object.assign(this.modelValue.config, this.widgetSettings.value); + } + this.propagateChange(this.modelValue); + } + } + + private updateLayoutSettings() { + if (this.modelValue) { + if (this.modelValue.layout) { + Object.assign(this.modelValue.layout, this.layoutSettings.value); + } + this.propagateChange(this.modelValue); + } + } + + private updateAdvancedSettings() { + if (this.modelValue) { + if (this.modelValue.config) { + const settings = this.advancedSettings.get('settings').value.model; + this.modelValue.config.settings = settings; + } + this.propagateChange(this.modelValue); + } + } + + private updateActionSettings() { + if (this.modelValue) { + if (this.modelValue.config) { + const actions = (this.actionsSettings.get('actionsData').value as WidgetActionsData).actionsMap; + this.modelValue.config.actions = actions; + } + this.propagateChange(this.modelValue); + } + } + + public alarmTypeList(): string[] { + return this.dataSettings.get('alarmTypeList').value; + } + + public removeAlarmType(type: string): void { + const types: string[] = this.dataSettings.get('alarmTypeList').value; + const index = types.indexOf(type); + if (index >= 0) { + types.splice(index, 1); + this.dataSettings.get('alarmTypeList').setValue(types); + this.dataSettings.get('alarmTypeList').markAsDirty(); + } + } + + public addAlarmType(event: MatChipInputEvent): void { + const input = event.input; + const value = event.value; + + const types: string[] = this.dataSettings.get('alarmTypeList').value; + + if ((value || '').trim()) { + types.push(value.trim()); + this.dataSettings.get('alarmTypeList').setValue(types); + this.dataSettings.get('alarmTypeList').markAsDirty(); + } + + if (input) { + input.value = ''; + } + } + + public displayAdvanced(): boolean { + return !!this.modelValue && !!this.modelValue.settingsSchema && !!this.modelValue.settingsSchema.schema; + } + + public removeDatasource(index: number) { + (this.dataSettings.get('datasources') as FormArray).removeAt(index); + } + + public addDatasource() { + let newDatasource: Datasource; + if (this.functionsOnly) { + newDatasource = deepClone(this.utils.getDefaultDatasource(this.modelValue.dataKeySettingsSchema.schema)); + newDatasource.dataKeys = [this.generateDataKey('Sin', DataKeyType.function)]; + } else { + newDatasource = { type: DatasourceType.entity, + dataKeys: [] + }; + } + const datasourcesFormArray = this.dataSettings.get('datasources') as FormArray; + datasourcesFormArray.push(this.buildDatasourceForm(newDatasource)); + } + + public generateDataKey(chip: any, type: DataKeyType): DataKey { + if (isObject(chip)) { + (chip as DataKey)._hash = Math.random(); + return chip; + } else { + let label: string = chip; + if (type === DataKeyType.alarm || type === DataKeyType.entityField) { + const keyField = type === DataKeyType.alarm ? alarmFields[label] : entityFields[chip]; + if (keyField) { + label = this.translate.instant(keyField.name); + } + } + label = this.genNextLabel(label); + const result: DataKey = { + name: chip, + type, + label, + color: this.genNextColor(), + settings: {}, + _hash: Math.random() + }; + if (type === DataKeyType.function) { + result.name = 'f(x)'; + result.funcBody = this.utils.getPredefinedFunctionBody(chip); + if (!result.funcBody) { + result.funcBody = 'return prevValue + 1;'; + } + } + if (isDefined(this.modelValue.dataKeySettingsSchema.schema)) { + result.settings = this.utils.generateObjectFromJsonSchema(this.modelValue.dataKeySettingsSchema.schema); + } + return result; + } + } + + private genNextLabel(name: string): string { + let label = name; + let i = 1; + let matches = false; + const datasources = this.widgetType === widgetType.alarm ? [this.modelValue.config.alarmSource] : this.modelValue.config.datasources; + if (datasources) { + do { + matches = false; + datasources.forEach((datasource) => { + if (datasource && datasource.dataKeys) { + datasource.dataKeys.forEach((dataKey) => { + if (dataKey.label === label) { + i++; + label = name + ' ' + i; + matches = true; + } + }); + } + }); + } while (matches); + } + return label; + } + + private genNextColor(): string { + let i = 0; + const datasources = this.widgetType === widgetType.alarm ? [this.modelValue.config.alarmSource] : this.modelValue.config.datasources; + if (datasources) { + datasources.forEach((datasource) => { + if (datasource && datasource.dataKeys) { + i += datasource.dataKeys.length; + } + }); + } + return this.utils.getMaterialColor(i); + } + + private createEntityAlias(alias: string, allowedEntityTypes: Array): Observable { + const singleEntityAlias: EntityAlias = {id: null, alias, filter: {resolveMultiple: false}}; + return this.dialog.open(EntityAliasDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + isAdd: true, + allowedEntityTypes, + entityAliases: this.entityAliases, + alias: singleEntityAlias + } + }).afterClosed().pipe( + tap((entityAlias) => { + if (entityAlias) { + this.entityAliases[entityAlias.id] = entityAlias; + this.aliasController.updateEntityAliases(this.entityAliases); + } + }) + ); + } + + private createFilter(filter: string): Observable { + const singleFilter: Filter = {id: null, filter, keyFilters: [], editable: true}; + return this.dialog.open(FilterDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + isAdd: true, + filters: this.filters, + filter: singleFilter + } + }).afterClosed().pipe( + tap((result) => { + if (result) { + this.filters[result.id] = result; + this.aliasController.updateFilters(this.filters); + } + }) + ); + } + + private fetchEntityKeys(entityAliasId: string, query: string, dataKeyTypes: Array): Observable> { + return this.aliasController.resolveSingleEntityInfo(entityAliasId).pipe( + mergeMap((entity) => { + if (entity) { + const fetchEntityTasks: Array>> = []; + for (const dataKeyType of dataKeyTypes) { + fetchEntityTasks.push( + this.entityService.getEntityKeys( + {entityType: entity.entityType, id: entity.id}, + query, + dataKeyType, + {ignoreLoading: true, ignoreErrors: true} + ).pipe( + map((keys) => { + const dataKeys: Array = []; + for (const key of keys) { + dataKeys.push({name: key, type: dataKeyType}); + } + return dataKeys; + } + ), + catchError(val => of([])) + )); + } + return forkJoin(fetchEntityTasks).pipe( + map(arrayOfDataKeys => { + const result = new Array(); + arrayOfDataKeys.forEach((dataKeyArray) => { + result.push(...dataKeyArray); + }); + return result; + } + )); + } else { + return of([]); + } + }), + catchError(val => of([] as Array)) + ); + } + + private fetchDashboardStates(query: string): Array { + const stateIds = Object.keys(this.dashboardStates); + const result = query ? stateIds.filter(this.createFilterForDashboardState(query)) : stateIds; + if (result && result.length) { + return result; + } else { + return [query]; + } + } + + private createFilterForDashboardState(query: string): (stateId: string) => boolean { + const lowercaseQuery = query.toLowerCase(); + return stateId => stateId.toLowerCase().indexOf(lowercaseQuery) === 0; + } + + public validate(c: FormControl) { + if (!this.dataSettings.valid) { + return { + dataSettings: { + valid: false + } + }; + } else if (!this.widgetSettings.valid) { + return { + widgetSettings: { + valid: false + } + }; + } else if (!this.layoutSettings.valid) { + return { + widgetSettings: { + valid: false + } + }; + } else if (!this.advancedSettings.valid) { + return { + advancedSettings: { + valid: false + } + }; + } else if (this.modelValue) { + const config = this.modelValue.config; + if (this.widgetType === widgetType.rpc && this.modelValue.isDataEnabled) { + if (!config.targetDeviceAliasIds || !config.targetDeviceAliasIds.length) { + return { + targetDeviceAliasIds: { + valid: false + } + }; + } + } else if (this.widgetType === widgetType.alarm && this.modelValue.isDataEnabled) { + if (!this.alarmSourceSettings.valid || !config.alarmSource) { + return { + alarmSource: { + valid: false + } + }; + } + } else if (this.widgetType !== widgetType.static && this.modelValue.isDataEnabled) { + if (!config.datasources || !config.datasources.length) { + return { + datasources: { + valid: false + } + }; + } + } + } + return null; + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.html b/ui-ngx/src/app/modules/home/components/widget/widget.component.html new file mode 100644 index 0000000000..3d692305b1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.html @@ -0,0 +1,47 @@ + +
+ + +
+ +
+ + +
+
+ Widget Error: {{ widgetErrorData.name + ": " + widgetErrorData.message}} +
+
+ widget.no-data +
+
+ +
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget.component.scss new file mode 100644 index 0000000000..121b368ad9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.scss @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-widget { + .tb-widget-error { + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, .5); + + span { + color: #f00; + } + } + + .tb-widget-no-data { + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, .75); + + span { + color: #000; + text-align: center; + } + } + + .tb-widget-loading { + z-index: 3; + background: rgba(255, 255, 255, .15); + } + + .tb-widget-error-container { + position: absolute; + width: 100%; + height: 100%; + background-color: #fff; + } + + .tb-widget-error-msg { + padding: 5px; + font-size: 16px; + color: #f00; + word-wrap: break-word; + } + + #widget-container { + min-height: 0; + min-width: 0; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts new file mode 100644 index 0000000000..e3ac7f0a8f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -0,0 +1,1200 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ComponentFactoryResolver, + ComponentRef, + ElementRef, + Injector, + Input, + NgZone, + OnChanges, + OnDestroy, + OnInit, + SimpleChanges, + ViewChild, + ViewContainerRef, + ViewEncapsulation +} from '@angular/core'; +import { DashboardWidget } from '@home/models/dashboard-component.models'; +import { + defaultLegendConfig, + LegendConfig, + LegendData, + LegendPosition, + Widget, + WidgetActionDescriptor, + widgetActionSources, + WidgetActionType, + WidgetComparisonSettings, + WidgetResource, + widgetType, + WidgetTypeParameters +} from '@shared/models/widget.models'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { WidgetService } from '@core/http/widget.service'; +import { UtilsService } from '@core/services/utils.service'; +import { forkJoin, Observable, of, ReplaySubject, Subscription, throwError } from 'rxjs'; +import { deepClone, isDefined, objToBase64URI } from '@core/utils'; +import { + IDynamicWidgetComponent, + WidgetContext, + WidgetHeaderAction, + WidgetInfo, + WidgetTypeInstance +} from '@home/models/widget-component.models'; +import { + IWidgetSubscription, + StateObject, + StateParams, + SubscriptionEntityInfo, + SubscriptionInfo, + SubscriptionMessage, + WidgetSubscriptionContext, + WidgetSubscriptionOptions +} from '@core/api/widget-api.models'; +import { EntityId } from '@shared/models/id/entity-id'; +import { ActivatedRoute, Router } from '@angular/router'; +import cssjs from '@core/css/css'; +import { ResourcesService } from '@core/services/resources.service'; +import { catchError, switchMap } from 'rxjs/operators'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { TimeService } from '@core/services/time.service'; +import { DeviceService } from '@app/core/http/device.service'; +import { ExceptionData } from '@shared/models/error.models'; +import { WidgetComponentService } from './widget-component.service'; +import { Timewindow } from '@shared/models/time/time.models'; +import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; +import { DashboardService } from '@core/http/dashboard.service'; +import { WidgetSubscription } from '@core/api/widget-subscription'; +import { EntityService } from '@core/http/entity.service'; +import { ServicesMap } from '@home/models/services.map'; +import { ResizeObserver } from '@juggle/resize-observer'; +import { EntityDataService } from '@core/api/entity-data.service'; +import { TranslateService } from '@ngx-translate/core'; +import { NotificationType } from '@core/notification/notification.models'; +import { AlarmDataService } from '@core/api/alarm-data.service'; + +@Component({ + selector: 'tb-widget', + templateUrl: './widget.component.html', + styleUrls: ['./widget.component.scss'], + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class WidgetComponent extends PageComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy { + + @Input() + isEdit: boolean; + + @Input() + isMobile: boolean; + + @Input() + dashboardWidget: DashboardWidget; + + @ViewChild('widgetContent', {read: ViewContainerRef, static: true}) widgetContentContainer: ViewContainerRef; + + widget: Widget; + widgetInfo: WidgetInfo; + errorMessages: string[]; + widgetContext: WidgetContext; + widgetType: any; + typeParameters: WidgetTypeParameters; + widgetTypeInstance: WidgetTypeInstance; + widgetErrorData: ExceptionData; + loadingData: boolean; + displayNoData = false; + + displayLegend: boolean; + legendConfig: LegendConfig; + legendData: LegendData; + isLegendFirst: boolean; + legendContainerLayoutType: string; + legendStyle: {[klass: string]: any}; + + dynamicWidgetComponentRef: ComponentRef; + dynamicWidgetComponent: IDynamicWidgetComponent; + + subscriptionContext: WidgetSubscriptionContext; + + subscriptionInited = false; + destroyed = false; + widgetSizeDetected = false; + widgetInstanceInited = false; + dataUpdatePending = false; + pendingMessage: SubscriptionMessage; + + cafs: {[cafId: string]: CancelAnimationFrame} = {}; + + toastTargetId = 'widget-messages-' + this.utils.guid(); + + private widgetResize$: ResizeObserver; + + private cssParser = new cssjs(); + + private rxSubscriptions = new Array(); + + constructor(protected store: Store, + private route: ActivatedRoute, + private router: Router, + private widgetComponentService: WidgetComponentService, + private componentFactoryResolver: ComponentFactoryResolver, + private elementRef: ElementRef, + private injector: Injector, + private widgetService: WidgetService, + private resources: ResourcesService, + private timeService: TimeService, + private deviceService: DeviceService, + private entityService: EntityService, + private dashboardService: DashboardService, + private entityDataService: EntityDataService, + private alarmDataService: AlarmDataService, + private translate: TranslateService, + private utils: UtilsService, + private raf: RafService, + private ngZone: NgZone, + private cd: ChangeDetectorRef) { + super(store); + this.cssParser.testMode = false; + } + + ngOnInit(): void { + + this.loadingData = true; + + this.widget = this.dashboardWidget.widget; + + this.displayLegend = isDefined(this.widget.config.showLegend) ? this.widget.config.showLegend + : this.widget.type === widgetType.timeseries; + + this.legendContainerLayoutType = 'column'; + + if (this.displayLegend) { + this.legendConfig = this.widget.config.legendConfig || defaultLegendConfig(this.widget.type); + this.legendData = { + keys: [], + data: [] + }; + if (this.legendConfig.position === LegendPosition.top || + this.legendConfig.position === LegendPosition.bottom) { + this.legendContainerLayoutType = 'column'; + this.isLegendFirst = this.legendConfig.position === LegendPosition.top; + } else { + this.legendContainerLayoutType = 'row'; + this.isLegendFirst = this.legendConfig.position === LegendPosition.left; + } + switch (this.legendConfig.position) { + case LegendPosition.top: + this.legendStyle = { + paddingBottom: '8px', + maxHeight: '50%', + overflowY: 'auto' + }; + break; + case LegendPosition.bottom: + this.legendStyle = { + paddingTop: '8px', + maxHeight: '50%', + overflowY: 'auto' + }; + break; + case LegendPosition.left: + this.legendStyle = { + paddingRight: '0px', + maxWidth: '50%', + overflowY: 'auto' + }; + break; + case LegendPosition.right: + this.legendStyle = { + paddingLeft: '0px', + maxWidth: '50%', + overflowY: 'auto' + }; + break; + } + } + + const actionDescriptorsBySourceId: {[actionSourceId: string]: Array} = {}; + if (this.widget.config.actions) { + for (const actionSourceId of Object.keys(this.widget.config.actions)) { + const descriptors = this.widget.config.actions[actionSourceId]; + const actionDescriptors: Array = []; + descriptors.forEach((descriptor) => { + const actionDescriptor: WidgetActionDescriptor = deepClone(descriptor); + actionDescriptor.displayName = this.utils.customTranslation(descriptor.name, descriptor.name); + actionDescriptors.push(actionDescriptor); + }); + actionDescriptorsBySourceId[actionSourceId] = actionDescriptors; + } + } + + this.widgetContext = this.dashboardWidget.widgetContext; + this.widgetContext.changeDetector = this.cd; + this.widgetContext.ngZone = this.ngZone; + this.widgetContext.store = this.store; + this.widgetContext.servicesMap = ServicesMap; + this.widgetContext.isEdit = this.isEdit; + this.widgetContext.isMobile = this.isMobile; + + this.widgetContext.subscriptionApi = { + createSubscription: this.createSubscription.bind(this), + createSubscriptionFromInfo: this.createSubscriptionFromInfo.bind(this), + removeSubscription: (id) => { + const subscription = this.widgetContext.subscriptions[id]; + if (subscription) { + subscription.destroy(); + delete this.widgetContext.subscriptions[id]; + } + } + }; + + this.widgetContext.actionsApi = { + actionDescriptorsBySourceId, + getActionDescriptors: this.getActionDescriptors.bind(this), + handleWidgetAction: this.handleWidgetAction.bind(this), + elementClick: this.elementClick.bind(this), + getActiveEntityInfo: this.getActiveEntityInfo.bind(this) + }; + + this.widgetContext.customHeaderActions = []; + const headerActionsDescriptors = this.getActionDescriptors(widgetActionSources.headerButton.value); + headerActionsDescriptors.forEach((descriptor) => { + const headerAction: WidgetHeaderAction = { + name: descriptor.name, + displayName: descriptor.displayName, + icon: descriptor.icon, + descriptor, + onAction: $event => { + const entityInfo = this.getActiveEntityInfo(); + const entityId = entityInfo ? entityInfo.entityId : null; + const entityName = entityInfo ? entityInfo.entityName : null; + const entityLabel = entityInfo ? entityInfo.entityLabel : null; + this.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel); + } + }; + this.widgetContext.customHeaderActions.push(headerAction); + }); + + this.subscriptionContext = new WidgetSubscriptionContext(this.widgetContext.dashboard); + this.subscriptionContext.timeService = this.timeService; + this.subscriptionContext.deviceService = this.deviceService; + this.subscriptionContext.translate = this.translate; + this.subscriptionContext.entityDataService = this.entityDataService; + this.subscriptionContext.alarmDataService = this.alarmDataService; + this.subscriptionContext.utils = this.utils; + this.subscriptionContext.raf = this.raf; + this.subscriptionContext.widgetUtils = this.widgetContext.utils; + this.subscriptionContext.getServerTimeDiff = this.dashboardService.getServerTimeDiff.bind(this.dashboardService); + + this.widgetComponentService.getWidgetInfo(this.widget.bundleAlias, this.widget.typeAlias, this.widget.isSystemType).subscribe( + (widgetInfo) => { + this.widgetInfo = widgetInfo; + this.loadFromWidgetInfo(); + }, + (errorData) => { + this.widgetInfo = errorData.widgetInfo; + this.errorMessages = errorData.errorMessages; + this.loadFromWidgetInfo(); + } + ); + setTimeout(() => { + this.dashboardWidget.updateWidgetParams(); + }, 0); + } + + ngAfterViewInit(): void { + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'isEdit') { + this.onEditModeChanged(); + } else if (propName === 'isMobile') { + this.onMobileModeChanged(); + } + } + } + } + + ngOnDestroy(): void { + this.destroyed = true; + this.rxSubscriptions.forEach((subscription) => { + subscription.unsubscribe(); + }); + this.rxSubscriptions.length = 0; + this.onDestroy(); + } + + private displayWidgetInstance(): boolean { + if (this.widget.type !== widgetType.static) { + for (const id of Object.keys(this.widgetContext.subscriptions)) { + const subscription = this.widgetContext.subscriptions[id]; + if (subscription.isDataResolved()) { + return true; + } + } + return false; + } else { + return true; + } + } + + private onDestroy() { + if (this.widgetContext) { + const shouldDestroyWidgetInstance = this.displayWidgetInstance(); + for (const id of Object.keys(this.widgetContext.subscriptions)) { + const subscription = this.widgetContext.subscriptions[id]; + subscription.destroy(); + } + this.subscriptionInited = false; + this.dataUpdatePending = false; + this.pendingMessage = null; + this.widgetContext.subscriptions = {}; + if (this.widgetContext.inited) { + this.widgetContext.inited = false; + for (const cafId of Object.keys(this.cafs)) { + if (this.cafs[cafId]) { + this.cafs[cafId](); + this.cafs[cafId] = null; + } + } + try { + if (shouldDestroyWidgetInstance) { + this.widgetTypeInstance.onDestroy(); + this.widgetInstanceInited = false; + } + } catch (e) { + this.handleWidgetException(e); + } + } + this.widgetContext.destroyed = true; + this.destroyDynamicWidgetComponent(); + } + } + + public onTimewindowChanged(timewindow: Timewindow) { + for (const id of Object.keys(this.widgetContext.subscriptions)) { + const subscription = this.widgetContext.subscriptions[id]; + if (!subscription.useDashboardTimewindow) { + subscription.updateTimewindowConfig(timewindow); + } + } + } + + public onLegendKeyHiddenChange(index: number) { + for (const id of Object.keys(this.widgetContext.subscriptions)) { + const subscription = this.widgetContext.subscriptions[id]; + subscription.updateDataVisibility(index); + } + } + + private loadFromWidgetInfo() { + this.widgetContext.widgetNamespace = `widget-type-${(this.widget.isSystemType ? 'sys-' : '')}${this.widget.bundleAlias}-${this.widget.typeAlias}`; + const elem = this.elementRef.nativeElement; + elem.classList.add('tb-widget'); + elem.classList.add(this.widgetContext.widgetNamespace); + this.widgetType = this.widgetInfo.widgetTypeFunction; + this.typeParameters = this.widgetInfo.typeParameters; + + if (!this.widgetType) { + this.widgetTypeInstance = {}; + } else { + try { + this.widgetTypeInstance = new this.widgetType(this.widgetContext); + } catch (e) { + this.handleWidgetException(e); + this.widgetTypeInstance = {}; + } + } + if (!this.widgetTypeInstance.onInit) { + this.widgetTypeInstance.onInit = () => {}; + } + if (!this.widgetTypeInstance.onDataUpdated) { + this.widgetTypeInstance.onDataUpdated = () => {}; + } + if (!this.widgetTypeInstance.onResize) { + this.widgetTypeInstance.onResize = () => {}; + } + if (!this.widgetTypeInstance.onEditModeChanged) { + this.widgetTypeInstance.onEditModeChanged = () => {}; + } + if (!this.widgetTypeInstance.onMobileModeChanged) { + this.widgetTypeInstance.onMobileModeChanged = () => {}; + } + if (!this.widgetTypeInstance.onDestroy) { + this.widgetTypeInstance.onDestroy = () => {}; + } + + this.initialize().subscribe( + () => { + this.onInit(); + }, + (err) => { + // console.log(err); + } + ); + } + + private detectChanges() { + if (!this.destroyed) { + try { + this.cd.detectChanges(); + } catch (e) { + // console.log(e); + } + } + } + + private isReady(): boolean { + return this.subscriptionInited && this.widgetSizeDetected; + } + + private onInit(skipSizeCheck?: boolean) { + if (!this.widgetContext.$containerParent || this.destroyed) { + return; + } + if (!skipSizeCheck) { + this.checkSize(); + } + if (!this.widgetContext.inited && this.isReady()) { + this.widgetContext.inited = true; + if (this.cafs.init) { + this.cafs.init(); + this.cafs.init = null; + } + this.cafs.init = this.raf.raf(() => { + try { + if (this.displayWidgetInstance()) { + this.widgetTypeInstance.onInit(); + this.widgetInstanceInited = true; + if (this.dataUpdatePending) { + this.widgetTypeInstance.onDataUpdated(); + this.dataUpdatePending = false; + } + if (this.pendingMessage) { + this.displayMessage(this.pendingMessage.severity, this.pendingMessage.message); + this.pendingMessage = null; + } + } else { + this.loadingData = false; + this.displayNoData = true; + } + this.detectChanges(); + } catch (e) { + this.handleWidgetException(e); + } + }); + if (!this.typeParameters.useCustomDatasources && this.widgetContext.defaultSubscription) { + this.widgetContext.defaultSubscription.subscribe(); + } + } + } + + private onResize() { + if (this.checkSize()) { + if (this.widgetContext.inited) { + if (this.cafs.resize) { + this.cafs.resize(); + this.cafs.resize = null; + } + this.cafs.resize = this.raf.raf(() => { + try { + if (this.displayWidgetInstance()) { + this.widgetTypeInstance.onResize(); + } + } catch (e) { + this.handleWidgetException(e); + } + }); + } else { + this.onInit(true); + } + } + } + + private onEditModeChanged() { + if (this.widgetContext.isEdit !== this.isEdit) { + this.widgetContext.isEdit = this.isEdit; + if (this.widgetContext.inited) { + if (this.cafs.editMode) { + this.cafs.editMode(); + this.cafs.editMode = null; + } + this.cafs.editMode = this.raf.raf(() => { + try { + if (this.displayWidgetInstance()) { + this.widgetTypeInstance.onEditModeChanged(); + } + } catch (e) { + this.handleWidgetException(e); + } + }); + } + } + } + + private onMobileModeChanged() { + if (this.widgetContext.isMobile !== this.isMobile) { + this.widgetContext.isMobile = this.isMobile; + if (this.widgetContext.inited) { + if (this.cafs.mobileMode) { + this.cafs.mobileMode(); + this.cafs.mobileMode = null; + } + this.cafs.mobileMode = this.raf.raf(() => { + try { + if (this.displayWidgetInstance()) { + this.widgetTypeInstance.onMobileModeChanged(); + } + } catch (e) { + this.handleWidgetException(e); + } + }); + } + } + } + + private reInit() { + if (this.cafs.reinit) { + this.cafs.reinit(); + this.cafs.reinit = null; + } + this.cafs.reinit = this.raf.raf(() => { + this.ngZone.run(() => { + this.reInitImpl(); + }); + }); + } + + private reInitImpl() { + this.onDestroy(); + if (!this.typeParameters.useCustomDatasources) { + this.createDefaultSubscription().subscribe( + () => { + if (this.destroyed) { + this.onDestroy(); + } else { + this.widgetContext.reset(); + this.subscriptionInited = true; + this.configureDynamicWidgetComponent(); + this.onInit(); + } + }, + () => { + if (this.destroyed) { + this.onDestroy(); + } else { + this.widgetContext.reset(); + this.subscriptionInited = true; + this.onInit(); + } + } + ); + } else { + this.widgetContext.reset(); + this.subscriptionInited = true; + this.configureDynamicWidgetComponent(); + this.onInit(); + } + } + + private initialize(): Observable { + + const initSubject = new ReplaySubject(); + + this.rxSubscriptions.push(this.widgetContext.aliasController.entityAliasesChanged.subscribe( + (aliasIds) => { + let subscriptionChanged = false; + for (const id of Object.keys(this.widgetContext.subscriptions)) { + const subscription = this.widgetContext.subscriptions[id]; + subscriptionChanged = subscriptionChanged || subscription.onAliasesChanged(aliasIds); + } + if (subscriptionChanged && !this.typeParameters.useCustomDatasources) { + this.displayNoData = false; + this.reInit(); + } + } + )); + + this.rxSubscriptions.push(this.widgetContext.aliasController.filtersChanged.subscribe( + (filterIds) => { + let subscriptionChanged = false; + for (const id of Object.keys(this.widgetContext.subscriptions)) { + const subscription = this.widgetContext.subscriptions[id]; + subscriptionChanged = subscriptionChanged || subscription.onFiltersChanged(filterIds); + } + if (subscriptionChanged && !this.typeParameters.useCustomDatasources) { + this.displayNoData = false; + this.reInit(); + } + } + )); + + this.rxSubscriptions.push(this.widgetContext.dashboard.dashboardTimewindowChanged.subscribe( + (dashboardTimewindow) => { + for (const id of Object.keys(this.widgetContext.subscriptions)) { + const subscription = this.widgetContext.subscriptions[id]; + subscription.onDashboardTimewindowChanged(dashboardTimewindow); + } + } + )); + if (!this.typeParameters.useCustomDatasources) { + this.createDefaultSubscription().subscribe( + () => { + this.subscriptionInited = true; + this.configureDynamicWidgetComponent(); + initSubject.next(); + initSubject.complete(); + }, + (err) => { + this.subscriptionInited = true; + initSubject.error(err); + } + ); + } else { + this.loadingData = false; + this.subscriptionInited = true; + this.configureDynamicWidgetComponent(); + initSubject.next(); + initSubject.complete(); + } + return initSubject.asObservable(); + } + + private destroyDynamicWidgetComponent() { + if (this.widgetContext.$containerParent && this.widgetResize$) { + this.widgetResize$.disconnect(); + } + if (this.dynamicWidgetComponentRef) { + this.dynamicWidgetComponentRef.destroy(); + this.dynamicWidgetComponentRef = null; + } + } + + private handleWidgetException(e) { + console.error(e); + this.widgetErrorData = this.utils.processWidgetException(e); + this.detectChanges(); + } + + private displayMessage(type: NotificationType, message: string, duration?: number) { + this.widgetContext.showToast(type, message, duration, 'bottom', 'right', this.toastTargetId); + } + + private clearMessage() { + this.widgetContext.hideToast(this.toastTargetId); + } + + private configureDynamicWidgetComponent() { + this.widgetContentContainer.clear(); + const injector: Injector = Injector.create( + { + providers: [ + { + provide: 'widgetContext', + useValue: this.widgetContext + }, + { + provide: 'errorMessages', + useValue: this.errorMessages + } + ], + parent: this.injector + } + ); + + const containerElement = $(this.elementRef.nativeElement.querySelector('#widget-container')); + this.widgetContext.$containerParent = $(containerElement); + + try { + this.dynamicWidgetComponentRef = this.widgetContentContainer.createComponent(this.widgetInfo.componentFactory, 0, injector); + this.cd.detectChanges(); + } catch (e) { + console.error(e); + if (this.dynamicWidgetComponentRef) { + this.dynamicWidgetComponentRef.destroy(); + this.dynamicWidgetComponentRef = null; + } + this.widgetContentContainer.clear(); + } + + if (this.dynamicWidgetComponentRef) { + this.dynamicWidgetComponent = this.dynamicWidgetComponentRef.instance; + this.widgetContext.$container = $(this.dynamicWidgetComponentRef.location.nativeElement); + this.widgetContext.$container.css('display', 'block'); + this.widgetContext.$container.css('user-select', 'none'); + this.widgetContext.$container.attr('id', 'container'); + if (this.widgetSizeDetected) { + this.widgetContext.$container.css('height', this.widgetContext.height + 'px'); + this.widgetContext.$container.css('width', this.widgetContext.width + 'px'); + } + } + + this.widgetResize$ = new ResizeObserver(() => { + this.onResize(); + }); + this.widgetResize$.observe(this.widgetContext.$containerParent[0]); + } + + private createSubscription(options: WidgetSubscriptionOptions, subscribe?: boolean): Observable { + const createSubscriptionSubject = new ReplaySubject(); + options.dashboardTimewindow = this.widgetContext.dashboardTimewindow; + const subscription: IWidgetSubscription = new WidgetSubscription(this.subscriptionContext, options); + subscription.init$.subscribe( + () => { + this.widgetContext.subscriptions[subscription.id] = subscription; + if (subscribe) { + subscription.subscribe(); + } + createSubscriptionSubject.next(subscription); + createSubscriptionSubject.complete(); + }, + (err) => { + createSubscriptionSubject.error(err); + } + ); + return createSubscriptionSubject.asObservable(); + } + + private createSubscriptionFromInfo(type: widgetType, subscriptionsInfo: Array, + options: WidgetSubscriptionOptions, useDefaultComponents: boolean, + subscribe: boolean): Observable { + const createSubscriptionSubject = new ReplaySubject(); + options.type = type; + + if (useDefaultComponents) { + this.defaultComponentsOptions(options); + } else { + if (!options.timeWindowConfig) { + options.useDashboardTimewindow = true; + } + } + if (options.type === widgetType.alarm) { + options.alarmSource = this.entityService.createAlarmSourceFromSubscriptionInfo(subscriptionsInfo[0]); + } else { + options.datasources = this.entityService.createDatasourcesFromSubscriptionsInfo(subscriptionsInfo); + } + this.createSubscription(options, subscribe).subscribe( + (subscription) => { + if (useDefaultComponents) { + this.defaultSubscriptionOptions(subscription, options); + } + createSubscriptionSubject.next(subscription); + createSubscriptionSubject.complete(); + }, + (err) => { + createSubscriptionSubject.error(err); + } + ); + return createSubscriptionSubject.asObservable(); + } + + private defaultComponentsOptions(options: WidgetSubscriptionOptions) { + options.useDashboardTimewindow = isDefined(this.widget.config.useDashboardTimewindow) + ? this.widget.config.useDashboardTimewindow : true; + options.displayTimewindow = isDefined(this.widget.config.displayTimewindow) + ? this.widget.config.displayTimewindow : !options.useDashboardTimewindow; + options.timeWindowConfig = options.useDashboardTimewindow ? this.widgetContext.dashboardTimewindow : this.widget.config.timewindow; + options.legendConfig = null; + if (this.displayLegend) { + options.legendConfig = this.legendConfig; + } + options.decimals = this.widgetContext.decimals; + options.units = this.widgetContext.units; + options.callbacks = { + onDataUpdated: () => { + try { + if (this.displayWidgetInstance()) { + if (this.widgetInstanceInited) { + this.widgetTypeInstance.onDataUpdated(); + } else { + this.dataUpdatePending = true; + } + } + } catch (e){} + }, + onDataUpdateError: (subscription, e) => { + this.handleWidgetException(e); + }, + onSubscriptionMessage: (subscription, message) => { + if (this.displayWidgetInstance()) { + if (this.widgetInstanceInited) { + this.displayMessage(message.severity, message.message); + } else { + this.pendingMessage = message; + } + } + }, + onInitialPageDataChanged: (subscription, nextPageData) => { + this.reInit(); + }, + dataLoading: (subscription) => { + if (this.loadingData !== subscription.loadingData) { + this.loadingData = subscription.loadingData; + this.detectChanges(); + } + }, + legendDataUpdated: (subscription, detectChanges) => { + if (detectChanges) { + this.detectChanges(); + } + }, + timeWindowUpdated: (subscription, timeWindowConfig) => { + this.ngZone.run(() => { + this.widget.config.timewindow = timeWindowConfig; + this.detectChanges(); + }); + } + }; + + } + + private defaultSubscriptionOptions(subscription: IWidgetSubscription, options: WidgetSubscriptionOptions) { + if (this.displayLegend) { + this.legendData = subscription.legendData; + } + } + + private createDefaultSubscription(): Observable { + const createSubscriptionSubject = new ReplaySubject(); + let options: WidgetSubscriptionOptions; + if (this.widget.type !== widgetType.rpc && this.widget.type !== widgetType.static) { + const comparisonSettings: WidgetComparisonSettings = this.widgetContext.settings; + options = { + type: this.widget.type, + stateData: this.typeParameters.stateData, + hasDataPageLink: this.typeParameters.hasDataPageLink, + singleEntity: this.typeParameters.singleEntity, + warnOnPageDataOverflow: this.typeParameters.warnOnPageDataOverflow, + comparisonEnabled: comparisonSettings.comparisonEnabled, + timeForComparison: comparisonSettings.timeForComparison + }; + if (this.widget.type === widgetType.alarm) { + options.alarmSource = deepClone(this.widget.config.alarmSource); + } else { + options.datasources = deepClone(this.widget.config.datasources); + } + + this.defaultComponentsOptions(options); + + this.createSubscription(options).subscribe( + (subscription) => { + this.defaultSubscriptionOptions(subscription, options); + + // backward compatibility + this.widgetContext.datasources = subscription.datasources; + this.widgetContext.data = subscription.data; + this.widgetContext.hiddenData = subscription.hiddenData; + this.widgetContext.timeWindow = subscription.timeWindow; + this.widgetContext.defaultSubscription = subscription; + createSubscriptionSubject.next(); + createSubscriptionSubject.complete(); + }, + (err) => { + createSubscriptionSubject.error(err); + } + ); + } else if (this.widget.type === widgetType.rpc) { + this.loadingData = false; + options = { + type: this.widget.type, + targetDeviceAliasIds: this.widget.config.targetDeviceAliasIds + }; + options.callbacks = { + rpcStateChanged: (subscription) => { + if (this.dynamicWidgetComponent) { + this.dynamicWidgetComponent.rpcEnabled = subscription.rpcEnabled; + this.dynamicWidgetComponent.executingRpcRequest = subscription.executingRpcRequest; + this.detectChanges(); + } + }, + onRpcSuccess: (subscription) => { + if (this.dynamicWidgetComponent) { + this.dynamicWidgetComponent.executingRpcRequest = subscription.executingRpcRequest; + this.dynamicWidgetComponent.rpcErrorText = subscription.rpcErrorText; + this.dynamicWidgetComponent.rpcRejection = subscription.rpcRejection; + this.clearMessage(); + this.detectChanges(); + } + }, + onRpcFailed: (subscription) => { + if (this.dynamicWidgetComponent) { + this.dynamicWidgetComponent.executingRpcRequest = subscription.executingRpcRequest; + this.dynamicWidgetComponent.rpcErrorText = subscription.rpcErrorText; + this.dynamicWidgetComponent.rpcRejection = subscription.rpcRejection; + if (subscription.rpcErrorText) { + this.displayMessage('error', subscription.rpcErrorText); + } + this.detectChanges(); + } + }, + onRpcErrorCleared: (subscription) => { + if (this.dynamicWidgetComponent) { + this.dynamicWidgetComponent.rpcErrorText = null; + this.dynamicWidgetComponent.rpcRejection = null; + this.clearMessage(); + this.detectChanges(); + } + } + }; + this.createSubscription(options).subscribe( + (subscription) => { + this.widgetContext.defaultSubscription = subscription; + createSubscriptionSubject.next(); + createSubscriptionSubject.complete(); + }, + (err) => { + createSubscriptionSubject.error(err); + } + ); + this.detectChanges(); + } else if (this.widget.type === widgetType.static) { + this.loadingData = false; + createSubscriptionSubject.next(); + createSubscriptionSubject.complete(); + this.detectChanges(); + } else { + createSubscriptionSubject.next(); + createSubscriptionSubject.complete(); + this.detectChanges(); + } + return createSubscriptionSubject.asObservable(); + } + + private getActionDescriptors(actionSourceId: string): Array { + let result = this.widgetContext.actionsApi.actionDescriptorsBySourceId[actionSourceId]; + if (!result) { + result = []; + } + return result; + } + + private handleWidgetAction($event: Event, descriptor: WidgetActionDescriptor, + entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string): void { + const type = descriptor.type; + const targetEntityParamName = descriptor.stateEntityParamName; + let targetEntityId: EntityId; + if (descriptor.setEntityId) { + targetEntityId = entityId; + } + switch (type) { + case WidgetActionType.openDashboardState: + case WidgetActionType.updateDashboardState: + let targetDashboardStateId = descriptor.targetDashboardStateId; + const params = deepClone(this.widgetContext.stateController.getStateParams()); + this.updateEntityParams(params, targetEntityParamName, targetEntityId, entityName, entityLabel); + if (type === WidgetActionType.openDashboardState) { + this.widgetContext.stateController.openState(targetDashboardStateId, params, descriptor.openRightLayout); + } else { + this.widgetContext.stateController.updateState(targetDashboardStateId, params, descriptor.openRightLayout); + } + break; + case WidgetActionType.openDashboard: + const targetDashboardId = descriptor.targetDashboardId; + targetDashboardStateId = descriptor.targetDashboardStateId; + const stateObject: StateObject = {}; + stateObject.params = {}; + this.updateEntityParams(stateObject.params, targetEntityParamName, targetEntityId, entityName, entityLabel); + if (targetDashboardStateId) { + stateObject.id = targetDashboardStateId; + } + const state = objToBase64URI([ stateObject ]); + const isSinglePage = this.route.snapshot.data.singlePageMode; + let url; + if (isSinglePage) { + url = `/dashboard/${targetDashboardId}?state=${state}`; + } else { + url = `/dashboards/${targetDashboardId}?state=${state}`; + } + this.router.navigateByUrl(url); + break; + case WidgetActionType.custom: + const customFunction = descriptor.customFunction; + if (customFunction && customFunction.length > 0) { + try { + if (!additionalParams) { + additionalParams = {}; + } + const customActionFunction = new Function('$event', 'widgetContext', 'entityId', + 'entityName', 'additionalParams', 'entityLabel', customFunction); + customActionFunction($event, this.widgetContext, entityId, entityName, additionalParams, entityLabel); + } catch (e) { + console.error(e); + } + } + break; + case WidgetActionType.customPretty: + const customPrettyFunction = descriptor.customFunction; + const customHtml = descriptor.customHtml; + const customCss = descriptor.customCss; + const customResources = descriptor.customResources; + const actionNamespace = `custom-action-pretty-${descriptor.name.toLowerCase()}`; + let htmlTemplate = ''; + if (isDefined(customHtml) && customHtml.length > 0) { + htmlTemplate = customHtml; + } + this.loadCustomActionResources(actionNamespace, customCss, customResources).subscribe( + () => { + if (isDefined(customPrettyFunction) && customPrettyFunction.length > 0) { + try { + if (!additionalParams) { + additionalParams = {}; + } + const customActionPrettyFunction = new Function('$event', 'widgetContext', 'entityId', + 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel', customPrettyFunction); + customActionPrettyFunction($event, this.widgetContext, entityId, entityName, htmlTemplate, additionalParams, entityLabel); + } catch (e) { + console.error(e); + } + } + }, + (errorMessages: string[]) => { + this.processResourcesLoadErrors(errorMessages); + } + ); + break; + } + } + + private elementClick($event: Event) { + const e = ($event.target || $event.srcElement) as Element; + if (e.id) { + const descriptors = this.getActionDescriptors('elementClick'); + if (descriptors.length) { + descriptors.forEach((descriptor) => { + if (descriptor.name === e.id) { + $event.stopPropagation(); + const entityInfo = this.getActiveEntityInfo(); + const entityId = entityInfo ? entityInfo.entityId : null; + const entityName = entityInfo ? entityInfo.entityName : null; + const entityLabel = entityInfo && entityInfo.entityLabel ? entityInfo.entityLabel : null; + this.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel); + } + }); + } + } + } + + private updateEntityParams(params: StateParams, targetEntityParamName?: string, targetEntityId?: EntityId, + entityName?: string, entityLabel?: string) { + if (targetEntityId) { + let targetEntityParams: StateParams; + if (targetEntityParamName && targetEntityParamName.length) { + targetEntityParams = params[targetEntityParamName]; + if (!targetEntityParams) { + targetEntityParams = {}; + params[targetEntityParamName] = targetEntityParams; + params.targetEntityParamName = targetEntityParamName; + } + } else { + targetEntityParams = params; + } + targetEntityParams.entityId = targetEntityId; + if (entityName) { + targetEntityParams.entityName = entityName; + } + if (entityLabel) { + targetEntityParams.entityLabel = entityLabel; + } + } + } + + private loadCustomActionResources(actionNamespace: string, customCss: string, customResources: Array): Observable { + if (isDefined(customCss) && customCss.length > 0) { + this.cssParser.cssPreviewNamespace = actionNamespace; + this.cssParser.createStyleElement(actionNamespace, customCss, 'nonamespace'); + } + const resourceTasks: Observable[] = []; + if (isDefined(customResources) && customResources.length > 0) { + customResources.forEach((resource) => { + resourceTasks.push( + this.resources.loadResource(resource.url).pipe( + catchError(e => of(`Failed to load custom action resource: '${resource.url}'`)) + ) + ); + }); + return forkJoin(resourceTasks).pipe( + switchMap(msgs => { + let errors: string[]; + if (msgs && msgs.length) { + errors = msgs.filter(msg => msg && msg.length > 0); + } + if (errors && errors.length) { + return throwError(errors); + } else { + return of(null); + } + } + )); + } else { + return of(null); + } + } + + private processResourcesLoadErrors(errorMessages: string[]) { + let messageToShow = ''; + errorMessages.forEach(error => { + messageToShow += `
${error}
`; + }); + this.store.dispatch(new ActionNotificationShow({message: messageToShow, type: 'error'})); + } + + private getActiveEntityInfo(): SubscriptionEntityInfo { + let entityInfo = this.widgetContext.activeEntityInfo; + if (!entityInfo) { + for (const id of Object.keys(this.widgetContext.subscriptions)) { + const subscription = this.widgetContext.subscriptions[id]; + entityInfo = subscription.getFirstEntityInfo(); + if (entityInfo) { + break; + } + } + } + return entityInfo; + } + + private checkSize(): boolean { + const width = this.widgetContext.$containerParent.width(); + const height = this.widgetContext.$containerParent.height(); + let sizeChanged = false; + + if (!this.widgetContext.width || this.widgetContext.width !== width || + !this.widgetContext.height || this.widgetContext.height !== height) { + if (width > 0 && height > 0) { + if (this.widgetContext.$container) { + this.widgetContext.$container.css('height', height + 'px'); + this.widgetContext.$container.css('width', width + 'px'); + } + this.widgetContext.width = width; + this.widgetContext.height = height; + sizeChanged = true; + this.widgetSizeDetected = true; + } + } + return sizeChanged; + } + +} diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html new file mode 100644 index 0000000000..57bf9ed5cb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html @@ -0,0 +1,191 @@ + +
+ +

device.add-device-text

+ + +
+ + +
+
+ + + check + + +
+ {{ 'device.wizard.device-details' | translate}} +
+ + device.name + + + {{ 'device.name-required' | translate }} + + + + device.label + + + + device-profile.transport-type + + + {{deviceTransportTypeTranslations.get(type) | translate}} + + + + {{deviceTransportTypeHints.get(deviceWizardFormGroup.get('transportType').value) | translate}} + + + {{ 'device-profile.transport-type-required' | translate }} + + +
+ + + device.wizard.existing-device-profile + + + device.wizard.new-device-profile + + +
+ + + + device-profile.new-device-profile-name + + + {{ 'device-profile.new-device-profile-name-required' | translate }} + + +
+
+ + +
+
+ + +
+
+ + {{ 'device.is-gateway' | translate }} + + + device.description + + +
+
+
+ +
+ {{ 'device-profile.transport-configuration' | translate }} + + +
+
+ +
+ {{'device-profile.alarm-rules-with-count' | translate: + {count: alarmRulesFormGroup.get('alarms').value ? + alarmRulesFormGroup.get('alarms').value.length : 0} }} + + +
+
+ +
+ {{ 'device-profile.device-provisioning' | translate }} + + +
+
+ + {{ 'device.credentials' | translate }} +
+ {{ 'device.wizard.add-credential' | translate }} + + +
+
+ + {{ 'customer.customer' | translate }} +
+ + +
+
+
+
+
+ + + +
+ +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss new file mode 100644 index 0000000000..b84ff8105f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss @@ -0,0 +1,75 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import "../../../../../scss/constants"; + +:host-context(.tb-fullscreen-dialog .mat-dialog-container) { + @media #{$mat-lt-sm} { + .mat-dialog-content { + max-height: 75vh; + } + } + + .invisible{ + visibility: hidden; + } +} + +:host ::ng-deep { + .mat-dialog-content { + display: flex; + flex-direction: column; + height: 100%; + padding: 0 !important; + + .mat-stepper-horizontal { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + @media #{$mat-lt-sm} { + .mat-step-label { + white-space: normal; + overflow: visible; + .mat-step-text-label { + overflow: visible; + } + } + } + .mat-horizontal-content-container { + height: 530px; + max-height: 100%; + width: 100%;; + overflow-y: auto; + @media #{$mat-gt-sm} { + min-width: 800px; + } + } + .mat-horizontal-stepper-content[aria-expanded=true] { + height: 100%; + form { + height: 100%; + } + } + + tb-device-profile-autocomplete, tb-queue-type-list{ + .mat-form-field-wrapper{ + width: 180px !important; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts new file mode 100644 index 0000000000..6a7c05e0ac --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts @@ -0,0 +1,368 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnDestroy, SkipSelf, ViewChild } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; +import { + createDeviceProfileConfiguration, + createDeviceProfileTransportConfiguration, + DeviceProfile, + DeviceProfileType, + DeviceProvisionConfiguration, + DeviceProvisionType, + DeviceTransportType, + deviceTransportTypeConfigurationInfoMap, + deviceTransportTypeHintMap, + deviceTransportTypeTranslationMap +} from '@shared/models/device.models'; +import { MatHorizontalStepper } from '@angular/material/stepper'; +import { AddEntityDialogData } from '@home/models/entity/entity-component.models'; +import { BaseData, HasId } from '@shared/models/base-data'; +import { EntityType } from '@shared/models/entity-type.models'; +import { DeviceProfileService } from '@core/http/device-profile.service'; +import { EntityId } from '@shared/models/id/entity-id'; +import { Observable, of, Subscription } from 'rxjs'; +import { map, mergeMap, tap } from 'rxjs/operators'; +import { DeviceService } from '@core/http/device.service'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { StepperSelectionEvent } from '@angular/cdk/stepper'; +import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; +import { MediaBreakpoints } from '@shared/models/constants'; +import { RuleChainId } from '@shared/models/id/rule-chain-id'; +import { ServiceType } from '@shared/models/queue.models'; +import { deepTrim } from '@core/utils'; + +@Component({ + selector: 'tb-device-wizard', + templateUrl: './device-wizard-dialog.component.html', + providers: [], + styleUrls: ['./device-wizard-dialog.component.scss'] +}) +export class DeviceWizardDialogComponent extends + DialogComponent implements OnDestroy, ErrorStateMatcher { + + @ViewChild('addDeviceWizardStepper', {static: true}) addDeviceWizardStepper: MatHorizontalStepper; + + selectedIndex = 0; + + showNext = true; + + createProfile = false; + createTransportConfiguration = false; + + entityType = EntityType; + + deviceTransportTypes = Object.keys(DeviceTransportType); + + deviceTransportTypeTranslations = deviceTransportTypeTranslationMap; + + deviceTransportTypeHints = deviceTransportTypeHintMap; + + deviceWizardFormGroup: FormGroup; + + transportConfigFormGroup: FormGroup; + + alarmRulesFormGroup: FormGroup; + + provisionConfigFormGroup: FormGroup; + + credentialsFormGroup: FormGroup; + + customerFormGroup: FormGroup; + + labelPosition = 'end'; + + serviceType = ServiceType.TB_RULE_ENGINE; + + private subscriptions: Subscription[] = []; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AddEntityDialogData>, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private deviceProfileService: DeviceProfileService, + private deviceService: DeviceService, + private breakpointObserver: BreakpointObserver, + private fb: FormBuilder) { + super(store, router, dialogRef); + this.deviceWizardFormGroup = this.fb.group({ + name: ['', Validators.required], + label: [''], + gateway: [false], + transportType: [DeviceTransportType.DEFAULT, Validators.required], + addProfileType: [0], + deviceProfileId: [null, Validators.required], + newDeviceProfileTitle: [{value: null, disabled: true}], + defaultRuleChainId: [{value: null, disabled: true}], + defaultQueueName: [{value: null, disabled: true}], + description: [''] + } + ); + + this.subscriptions.push(this.deviceWizardFormGroup.get('addProfileType').valueChanges.subscribe( + (addProfileType: number) => { + if (addProfileType === 0) { + this.deviceWizardFormGroup.get('deviceProfileId').setValidators([Validators.required]); + this.deviceWizardFormGroup.get('deviceProfileId').enable(); + this.deviceWizardFormGroup.get('newDeviceProfileTitle').setValidators(null); + this.deviceWizardFormGroup.get('newDeviceProfileTitle').disable(); + this.deviceWizardFormGroup.get('defaultRuleChainId').disable(); + this.deviceWizardFormGroup.get('defaultQueueName').disable(); + this.deviceWizardFormGroup.updateValueAndValidity(); + this.createProfile = false; + this.createTransportConfiguration = false; + } else { + this.deviceWizardFormGroup.get('deviceProfileId').setValidators(null); + this.deviceWizardFormGroup.get('deviceProfileId').disable(); + this.deviceWizardFormGroup.get('newDeviceProfileTitle').setValidators([Validators.required]); + this.deviceWizardFormGroup.get('newDeviceProfileTitle').enable(); + this.deviceWizardFormGroup.get('defaultRuleChainId').enable(); + this.deviceWizardFormGroup.get('defaultQueueName').enable(); + + this.deviceWizardFormGroup.updateValueAndValidity(); + this.createProfile = true; + this.createTransportConfiguration = this.deviceWizardFormGroup.get('transportType').value && + deviceTransportTypeConfigurationInfoMap.get(this.deviceWizardFormGroup.get('transportType').value).hasProfileConfiguration; + } + } + )); + + this.transportConfigFormGroup = this.fb.group( + { + transportConfiguration: [createDeviceProfileTransportConfiguration(DeviceTransportType.DEFAULT), Validators.required] + } + ); + this.subscriptions.push(this.deviceWizardFormGroup.get('transportType').valueChanges.subscribe((transportType) => { + this.deviceProfileTransportTypeChanged(transportType); + })); + + this.alarmRulesFormGroup = this.fb.group({ + alarms: [null] + } + ); + + this.provisionConfigFormGroup = this.fb.group( + { + provisionConfiguration: [{ + type: DeviceProvisionType.DISABLED + } as DeviceProvisionConfiguration, [Validators.required]] + } + ); + + this.credentialsFormGroup = this.fb.group({ + setCredential: [false], + credential: [{value: null, disabled: true}] + } + ); + + this.subscriptions.push(this.credentialsFormGroup.get('setCredential').valueChanges.subscribe((value) => { + if (value) { + this.credentialsFormGroup.get('credential').enable(); + } else { + this.credentialsFormGroup.get('credential').disable(); + } + })); + + this.customerFormGroup = this.fb.group({ + customerId: [null] + } + ); + + this.labelPosition = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']) ? 'end' : 'bottom'; + + this.subscriptions.push(this.breakpointObserver + .observe(MediaBreakpoints['gt-sm']) + .subscribe((state: BreakpointState) => { + if (state.matches) { + this.labelPosition = 'end'; + } else { + this.labelPosition = 'bottom'; + } + } + )); + } + + ngOnDestroy() { + super.ngOnDestroy(); + this.subscriptions.forEach(s => s.unsubscribe()); + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + previousStep(): void { + this.addDeviceWizardStepper.previous(); + } + + nextStep(): void { + this.addDeviceWizardStepper.next(); + } + + getFormLabel(index: number): string { + if (index > 0) { + if (!this.createProfile) { + index += 3; + } else if (!this.createTransportConfiguration) { + index += 1; + } + } + switch (index) { + case 0: + return 'device.wizard.device-details'; + case 1: + return 'device-profile.transport-configuration'; + case 2: + return 'device-profile.alarm-rules'; + case 3: + return 'device-profile.device-provisioning'; + case 4: + return 'device.credentials'; + case 5: + return 'customer.customer'; + } + } + + get maxStepperIndex(): number { + return this.addDeviceWizardStepper?._steps?.length - 1; + } + + private deviceProfileTransportTypeChanged(deviceTransportType: DeviceTransportType): void { + this.transportConfigFormGroup.patchValue( + {transportConfiguration: createDeviceProfileTransportConfiguration(deviceTransportType)}); + this.createTransportConfiguration = this.createProfile && deviceTransportType && + deviceTransportTypeConfigurationInfoMap.get(deviceTransportType).hasProfileConfiguration; + } + + add(): void { + if (this.allValid()) { + this.createDeviceProfile().pipe( + mergeMap(profileId => this.createDevice(profileId)), + mergeMap(device => this.saveCredentials(device)) + ).subscribe( + (created) => { + this.dialogRef.close(created); + } + ); + } + } + + private createDeviceProfile(): Observable { + if (this.deviceWizardFormGroup.get('addProfileType').value) { + const deviceProvisionConfiguration: DeviceProvisionConfiguration = this.provisionConfigFormGroup.get('provisionConfiguration').value; + const provisionDeviceKey = deviceProvisionConfiguration.provisionDeviceKey; + delete deviceProvisionConfiguration.provisionDeviceKey; + const deviceProfile: DeviceProfile = { + name: this.deviceWizardFormGroup.get('newDeviceProfileTitle').value, + type: DeviceProfileType.DEFAULT, + transportType: this.deviceWizardFormGroup.get('transportType').value, + provisionType: deviceProvisionConfiguration.type, + provisionDeviceKey, + profileData: { + configuration: createDeviceProfileConfiguration(DeviceProfileType.DEFAULT), + transportConfiguration: this.transportConfigFormGroup.get('transportConfiguration').value, + alarms: this.alarmRulesFormGroup.get('alarms').value, + provisionConfiguration: deviceProvisionConfiguration + } + }; + if (this.deviceWizardFormGroup.get('defaultRuleChainId').value) { + deviceProfile.defaultRuleChainId = new RuleChainId(this.deviceWizardFormGroup.get('defaultRuleChainId').value); + } + return this.deviceProfileService.saveDeviceProfile(deepTrim(deviceProfile)).pipe( + map(profile => profile.id), + tap((profileId) => { + this.deviceWizardFormGroup.patchValue({ + deviceProfileId: profileId, + addProfileType: 0 + }); + }) + ); + } else { + return of(this.deviceWizardFormGroup.get('deviceProfileId').value); + } + } + + private createDevice(profileId): Observable> { + const device = { + name: this.deviceWizardFormGroup.get('name').value, + label: this.deviceWizardFormGroup.get('label').value, + deviceProfileId: profileId, + additionalInfo: { + gateway: this.deviceWizardFormGroup.get('gateway').value, + description: this.deviceWizardFormGroup.get('description').value + }, + customerId: null + }; + if (this.customerFormGroup.get('customerId').value) { + device.customerId = { + entityType: EntityType.CUSTOMER, + id: this.customerFormGroup.get('customerId').value + }; + } + return this.data.entitiesTableConfig.saveEntity(deepTrim(device)); + } + + private saveCredentials(device: BaseData): Observable { + if (this.credentialsFormGroup.get('setCredential').value) { + return this.deviceService.getDeviceCredentials(device.id.id).pipe( + mergeMap( + (deviceCredentials) => { + const deviceCredentialsValue = {...deviceCredentials, ...this.credentialsFormGroup.value.credential}; + return this.deviceService.saveDeviceCredentials(deviceCredentialsValue); + } + ), + map(() => true)); + } + return of(true); + } + + allValid(): boolean { + if (this.addDeviceWizardStepper.steps.find((item, index) => { + if (item.stepControl.invalid) { + item.interacted = true; + this.addDeviceWizardStepper.selectedIndex = index; + return true; + } else { + return false; + } + } )) { + return false; + } else { + return true; + } + } + + changeStep($event: StepperSelectionEvent): void { + this.selectedIndex = $event.selectedIndex; + if (this.selectedIndex === this.maxStepperIndex) { + this.showNext = false; + } else { + this.showNext = true; + } + } +} diff --git a/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.html b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.html new file mode 100644 index 0000000000..2fcc346477 --- /dev/null +++ b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.html @@ -0,0 +1,56 @@ + +
+ +

{{ assignToCustomerTitle | translate }}

+ + +
+ + +
+
+
+ {{ assignToCustomerText | translate }} + + +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.ts b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.ts new file mode 100644 index 0000000000..d7e03bc79f --- /dev/null +++ b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.ts @@ -0,0 +1,132 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { DeviceService } from '@core/http/device.service'; +import { EntityType } from '@shared/models/entity-type.models'; +import { forkJoin, Observable } from 'rxjs'; +import { AssetService } from '@core/http/asset.service'; +import { EntityViewService } from '@core/http/entity-view.service'; +import { DashboardService } from '@core/http/dashboard.service'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; + +export interface AddEntitiesToCustomerDialogData { + customerId: string; + entityType: EntityType; +} + +@Component({ + selector: 'tb-add-entities-to-customer-dialog', + templateUrl: './add-entities-to-customer-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: AddEntitiesToCustomerDialogComponent}], + styleUrls: [] +}) +export class AddEntitiesToCustomerDialogComponent extends + DialogComponent implements OnInit, ErrorStateMatcher { + + addEntitiesToCustomerFormGroup: FormGroup; + + submitted = false; + + entityType: EntityType; + + assignToCustomerTitle: string; + assignToCustomerText: string; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AddEntitiesToCustomerDialogData, + private deviceService: DeviceService, + private assetService: AssetService, + private entityViewService: EntityViewService, + private dashboardService: DashboardService, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + public fb: FormBuilder) { + super(store, router, dialogRef); + this.entityType = data.entityType; + } + + ngOnInit(): void { + this.addEntitiesToCustomerFormGroup = this.fb.group({ + entityIds: [null, [Validators.required]] + }); + switch (this.data.entityType) { + case EntityType.DEVICE: + this.assignToCustomerTitle = 'device.assign-device-to-customer'; + this.assignToCustomerText = 'device.assign-device-to-customer-text'; + break; + case EntityType.ASSET: + this.assignToCustomerTitle = 'asset.assign-asset-to-customer'; + this.assignToCustomerText = 'asset.assign-asset-to-customer-text'; + break; + case EntityType.ENTITY_VIEW: + this.assignToCustomerTitle = 'entity-view.assign-entity-view-to-customer'; + this.assignToCustomerText = 'entity-view.assign-entity-view-to-customer-text'; + break; + case EntityType.DASHBOARD: + this.assignToCustomerTitle = 'dashboard.assign-dashboard-to-customer'; + this.assignToCustomerText = 'dashboard.assign-dashboard-to-customer-text'; + break; + } + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(false); + } + + assign(): void { + this.submitted = true; + const entityIds: Array = this.addEntitiesToCustomerFormGroup.get('entityIds').value; + const tasks: Observable[] = []; + entityIds.forEach( + (entityId) => { + tasks.push(this.getAssignToCustomerTask(this.data.customerId, entityId)); + } + ); + forkJoin(tasks).subscribe( + () => { + this.dialogRef.close(true); + } + ); + } + + private getAssignToCustomerTask(customerId: string, entityId: string): Observable { + switch (this.data.entityType) { + case EntityType.DEVICE: + return this.deviceService.assignDeviceToCustomer(customerId, entityId); + case EntityType.ASSET: + return this.assetService.assignAssetToCustomer(customerId, entityId); + case EntityType.ENTITY_VIEW: + return this.entityViewService.assignEntityViewToCustomer(customerId, entityId); + case EntityType.DASHBOARD: + return this.dashboardService.assignDashboardToCustomer(customerId, entityId); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.html b/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.html new file mode 100644 index 0000000000..18d71968fd --- /dev/null +++ b/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.html @@ -0,0 +1,55 @@ + +
+ +

{{ assignToCustomerTitle | translate }}

+ + +
+ + +
+
+
+
{{ assignToCustomerText | translate }}
+ + +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.ts b/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.ts new file mode 100644 index 0000000000..97177378f6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.ts @@ -0,0 +1,124 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { DeviceService } from '@core/http/device.service'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; +import { forkJoin, Observable } from 'rxjs'; +import { AssetService } from '@core/http/asset.service'; +import { EntityViewService } from '@core/http/entity-view.service'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; + +export interface AssignToCustomerDialogData { + entityIds: Array; + entityType: EntityType; +} + +@Component({ + selector: 'tb-assign-to-customer-dialog', + templateUrl: './assign-to-customer-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: AssignToCustomerDialogComponent}], + styleUrls: [] +}) +export class AssignToCustomerDialogComponent extends + DialogComponent implements OnInit, ErrorStateMatcher { + + assignToCustomerFormGroup: FormGroup; + + submitted = false; + + entityType = EntityType; + + assignToCustomerTitle: string; + assignToCustomerText: string; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AssignToCustomerDialogData, + private deviceService: DeviceService, + private assetService: AssetService, + private entityViewService: EntityViewService, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + public fb: FormBuilder) { + super(store, router, dialogRef); + } + + ngOnInit(): void { + this.assignToCustomerFormGroup = this.fb.group({ + customerId: [null, [Validators.required]] + }); + switch (this.data.entityType) { + case EntityType.DEVICE: + this.assignToCustomerTitle = 'device.assign-device-to-customer'; + this.assignToCustomerText = 'device.assign-to-customer-text'; + break; + case EntityType.ASSET: + this.assignToCustomerTitle = 'asset.assign-asset-to-customer'; + this.assignToCustomerText = 'asset.assign-to-customer-text'; + break; + case EntityType.ENTITY_VIEW: + this.assignToCustomerTitle = 'entity-view.assign-entity-view-to-customer'; + this.assignToCustomerText = 'entity-view.assign-to-customer-text'; + break; + } + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(false); + } + + assign(): void { + this.submitted = true; + const customerId: string = this.assignToCustomerFormGroup.get('customerId').value; + const tasks: Observable[] = []; + this.data.entityIds.forEach( + (entityId) => { + tasks.push(this.getAssignToCustomerTask(customerId, entityId.id)); + } + ); + forkJoin(tasks).subscribe( + () => { + this.dialogRef.close(true); + } + ); + } + + private getAssignToCustomerTask(customerId: string, entityId: string): Observable { + switch (this.data.entityType) { + case EntityType.DEVICE: + return this.deviceService.assignDeviceToCustomer(customerId, entityId); + case EntityType.ASSET: + return this.assetService.assignAssetToCustomer(customerId, entityId); + case EntityType.ENTITY_VIEW: + return this.entityViewService.assignEntityViewToCustomer(customerId, entityId); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/dialogs/home-dialogs.module.ts b/ui-ngx/src/app/modules/home/dialogs/home-dialogs.module.ts new file mode 100644 index 0000000000..4a9ae93300 --- /dev/null +++ b/ui-ngx/src/app/modules/home/dialogs/home-dialogs.module.ts @@ -0,0 +1,42 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@app/shared/shared.module'; +import { AssignToCustomerDialogComponent } from '@modules/home/dialogs/assign-to-customer-dialog.component'; +import { AddEntitiesToCustomerDialogComponent } from '@modules/home/dialogs/add-entities-to-customer-dialog.component'; +import { HomeDialogsService } from './home-dialogs.service'; + +@NgModule({ + declarations: + [ + AssignToCustomerDialogComponent, + AddEntitiesToCustomerDialogComponent + ], + imports: [ + CommonModule, + SharedModule + ], + exports: [ + AssignToCustomerDialogComponent, + AddEntitiesToCustomerDialogComponent + ], + providers: [ + HomeDialogsService + ] +}) +export class HomeDialogsModule { } diff --git a/ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts b/ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts new file mode 100644 index 0000000000..e7524567b0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts @@ -0,0 +1,54 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { EntityType } from '@shared/models/entity-type.models'; +import { Observable } from 'rxjs'; +import { + ImportDialogCsvComponent, + ImportDialogCsvData +} from '@home/components/import-export/import-dialog-csv.component'; + +@Injectable() +export class HomeDialogsService { + constructor( + private dialog: MatDialog + ) { + } + + public importEntities(entityType: EntityType): Observable { + switch (entityType) { + case EntityType.DEVICE: + return this.openImportDialogCSV(entityType, 'device.import', 'device.device-file'); + case EntityType.ASSET: + return this.openImportDialogCSV(entityType, 'asset.import', 'asset.asset-file'); + } + } + + private openImportDialogCSV(entityType: EntityType, importTitle: string, importFileLabel: string): Observable { + return this.dialog.open(ImportDialogCsvComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + entityType, + importTitle, + importFileLabel + } + }).afterClosed(); + } +} diff --git a/ui-ngx/src/app/modules/home/home-routing.module.ts b/ui-ngx/src/app/modules/home/home-routing.module.ts new file mode 100644 index 0000000000..f73528db68 --- /dev/null +++ b/ui-ngx/src/app/modules/home/home-routing.module.ts @@ -0,0 +1,45 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { HomeComponent } from './home.component'; +import { AuthGuard } from '@core/guards/auth.guard'; +import { StoreModule } from '@ngrx/store'; + +const routes: Routes = [ + { path: '', + component: HomeComponent, + data: { + title: 'home.home', + breadcrumb: { + skip: true + } + }, + canActivate: [AuthGuard], + canActivateChild: [AuthGuard], + loadChildren: () => import('./pages/home-pages.module').then(m => m.HomePagesModule) + } +]; + +@NgModule({ + imports: [ + StoreModule, + RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class HomeRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/home.component.html b/ui-ngx/src/app/modules/home/home.component.html new file mode 100644 index 0000000000..b85569638a --- /dev/null +++ b/ui-ngx/src/app/modules/home/home.component.html @@ -0,0 +1,82 @@ + + + +
+ +
+ +
+
+
+ + + +
+ +
+ + + + + + +
+ + + + +
+ + + +
+ + +
+ +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/home.component.scss b/ui-ngx/src/app/modules/home/home.component.scss new file mode 100644 index 0000000000..6cb0adea27 --- /dev/null +++ b/ui-ngx/src/app/modules/home/home.component.scss @@ -0,0 +1,74 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + display: flex; + width: 100%; + height: 100%; + + .tb-invisible { + display: none !important; + } + + mat-sidenav-container { + flex: 1; + } + mat-sidenav.tb-site-sidenav { + width: 250px; + @media (max-width:456px) { + width: calc(100% - 56px); + } + .tb-nav-header { + z-index: 2; + flex-shrink: 0; + white-space: nowrap; + .tb-nav-header-toolbar { + min-height: 64px; + height: inherit; + padding: 0; + z-index: 2; + flex-shrink: 0; + white-space: nowrap; + border-bottom: 1px solid rgba(0, 0, 0, .12); + & > div { + height: 64px; + .tb-logo-title { + width: auto; + max-width: 100%; + height: 36px; + max-height: 100%; + margin: auto; + } + } + } + } + .tb-side-menu-toolbar { + overflow-y: auto; + height: inherit; + padding: 0; + } + } + .tb-primary-toolbar { + z-index: 2; + h1 { + font-size: 24px !important; + font-weight: 400 !important; + } + } + .tb-main-content { + overflow: auto; + position: relative; + } +} diff --git a/ui-ngx/src/app/modules/home/home.component.ts b/ui-ngx/src/app/modules/home/home.component.ts new file mode 100644 index 0000000000..9788241693 --- /dev/null +++ b/ui-ngx/src/app/modules/home/home.component.ts @@ -0,0 +1,176 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core'; +import { fromEvent, Observable } from 'rxjs'; +import { select, Store } from '@ngrx/store'; +import { debounceTime, distinctUntilChanged, map, tap } from 'rxjs/operators'; + +import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; +import { User } from '@shared/models/user.model'; +import { PageComponent } from '@shared/components/page.component'; +import { AppState } from '@core/core.state'; +import { getCurrentAuthState, selectAuthUser, selectUserDetails } from '@core/auth/auth.selectors'; +import { MediaBreakpoints } from '@shared/models/constants'; +import * as _screenfull from 'screenfull'; +import { MatSidenav } from '@angular/material/sidenav'; +import { AuthState } from '@core/auth/auth.models'; +import { WINDOW } from '@core/services/window.service'; +import { instanceOfSearchableComponent, ISearchableComponent } from '@home/models/searchable-component.models'; + +const screenfull = _screenfull as _screenfull.Screenfull; + +@Component({ + selector: 'tb-home', + templateUrl: './home.component.html', + styleUrls: ['./home.component.scss'] +}) +export class HomeComponent extends PageComponent implements AfterViewInit, OnInit { + + authState: AuthState = getCurrentAuthState(this.store); + + forceFullscreen = this.authState.forceFullscreen; + + activeComponent: any; + searchableComponent: ISearchableComponent; + + sidenavMode: 'over' | 'push' | 'side' = 'side'; + sidenavOpened = true; + + logo = 'assets/logo_title_white.svg'; + + @ViewChild('sidenav') + sidenav: MatSidenav; + + @ViewChild('searchInput') searchInputField: ElementRef; + + fullscreenEnabled = screenfull.isEnabled; + + authUser$: Observable; + userDetails$: Observable; + userDetailsString: Observable; + + searchEnabled = false; + showSearch = false; + searchText = ''; + + constructor(protected store: Store, + @Inject(WINDOW) private window: Window, + public breakpointObserver: BreakpointObserver) { + super(store); + } + + ngOnInit() { + + this.authUser$ = this.store.pipe(select(selectAuthUser)); + this.userDetails$ = this.store.pipe(select(selectUserDetails)); + this.userDetailsString = this.userDetails$.pipe(map((user: User) => { + return JSON.stringify(user); + })); + + const isGtSm = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']); + this.sidenavMode = isGtSm ? 'side' : 'over'; + this.sidenavOpened = isGtSm; + + this.breakpointObserver + .observe(MediaBreakpoints['gt-sm']) + .subscribe((state: BreakpointState) => { + if (state.matches) { + this.sidenavMode = 'side'; + this.sidenavOpened = true; + } else { + this.sidenavMode = 'over'; + this.sidenavOpened = false; + } + } + ); + } + + ngAfterViewInit() { + fromEvent(this.searchInputField.nativeElement, 'keyup') + .pipe( + debounceTime(150), + distinctUntilChanged(), + tap(() => { + this.searchTextUpdated(); + }) + ) + .subscribe(); + } + + sidenavClicked() { + if (this.sidenavMode === 'over') { + this.sidenav.toggle(); + } + } + + toggleFullscreen() { + if (screenfull.isEnabled) { + screenfull.toggle(); + } + } + + isFullscreen() { + return screenfull.isFullscreen; + } + + goBack() { + this.window.history.back(); + } + + activeComponentChanged(activeComponent: any) { + this.showSearch = false; + this.searchText = ''; + this.activeComponent = activeComponent; + if (this.activeComponent && instanceOfSearchableComponent(this.activeComponent)) { + this.searchEnabled = true; + this.searchableComponent = this.activeComponent; + } else { + this.searchEnabled = false; + this.searchableComponent = null; + } + } + + displaySearchMode(): boolean { + return this.searchEnabled && this.showSearch; + } + + openSearch() { + if (this.searchEnabled) { + this.showSearch = true; + setTimeout(() => { + this.searchInputField.nativeElement.focus(); + this.searchInputField.nativeElement.setSelectionRange(0, 0); + }, 10); + } + } + + closeSearch() { + if (this.searchEnabled) { + this.showSearch = false; + if (this.searchText.length) { + this.searchText = ''; + this.searchTextUpdated(); + } + } + } + + private searchTextUpdated() { + if (this.searchableComponent) { + this.searchableComponent.onSearchTextUpdated(this.searchText); + } + } +} diff --git a/ui-ngx/src/app/modules/home/home.module.ts b/ui-ngx/src/app/modules/home/home.module.ts new file mode 100644 index 0000000000..57364ccedf --- /dev/null +++ b/ui-ngx/src/app/modules/home/home.module.ts @@ -0,0 +1,41 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { HomeRoutingModule } from './home-routing.module'; +import { HomeComponent } from './home.component'; +import { SharedModule } from '@app/shared/shared.module'; +import { MenuLinkComponent } from '@modules/home/menu/menu-link.component'; +import { MenuToggleComponent } from '@modules/home/menu/menu-toggle.component'; +import { SideMenuComponent } from '@modules/home/menu/side-menu.component'; + +@NgModule({ + declarations: + [ + HomeComponent, + MenuLinkComponent, + MenuToggleComponent, + SideMenuComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeRoutingModule + ] +}) +export class HomeModule { } diff --git a/ui-ngx/src/app/modules/home/menu/menu-link.component.html b/ui-ngx/src/app/modules/home/menu/menu-link.component.html new file mode 100644 index 0000000000..034d92aa70 --- /dev/null +++ b/ui-ngx/src/app/modules/home/menu/menu-link.component.html @@ -0,0 +1,22 @@ + + + {{section.icon}} + + {{section.name | translate}} + diff --git a/ui-ngx/src/app/modules/home/menu/menu-link.component.scss b/ui-ngx/src/app/modules/home/menu/menu-link.component.scss new file mode 100644 index 0000000000..bb3718c2da --- /dev/null +++ b/ui-ngx/src/app/modules/home/menu/menu-link.component.scss @@ -0,0 +1,18 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + +} diff --git a/ui-ngx/src/app/modules/home/menu/menu-link.component.ts b/ui-ngx/src/app/modules/home/menu/menu-link.component.ts new file mode 100644 index 0000000000..6edef6a8af --- /dev/null +++ b/ui-ngx/src/app/modules/home/menu/menu-link.component.ts @@ -0,0 +1,35 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Input, OnInit } from '@angular/core'; +import { MenuSection } from '@core/services/menu.models'; + +@Component({ + selector: 'tb-menu-link', + templateUrl: './menu-link.component.html', + styleUrls: ['./menu-link.component.scss'] +}) +export class MenuLinkComponent implements OnInit { + + @Input() section: MenuSection; + + constructor() { + } + + ngOnInit() { + } + +} diff --git a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html new file mode 100644 index 0000000000..b8acb0f624 --- /dev/null +++ b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html @@ -0,0 +1,30 @@ + + + {{section.icon}} + + {{section.name | translate}} + + +
    +
  • + +
  • +
diff --git a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.scss b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.scss new file mode 100644 index 0000000000..8dbacbab9d --- /dev/null +++ b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.scss @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host ::ng-deep { + .tb-button-toggle { + .tb-toggle-icon { + display: inline-block; + width: 15px; + margin: auto 0 auto auto; + background-size: 100% auto; + transition: transform .3s ease-in-out; + &.tb-toggled { + transform: rotateZ(180deg); + } + } + } + + .tb-menu-toggle-list { + position: relative; + z-index: 1; + overflow: hidden; + + transition: .75s cubic-bezier(.35, 0, .25, 1); + + transition-property: height; + + a.mat-button { + padding: 0 16px 0 32px; + font-weight: 500; + text-transform: none !important; + text-rendering: optimizeLegibility; + } + } +} diff --git a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.ts b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.ts new file mode 100644 index 0000000000..da1b5d6a42 --- /dev/null +++ b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.ts @@ -0,0 +1,51 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Input, OnInit } from '@angular/core'; +import { MenuSection } from '@core/services/menu.models'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'tb-menu-toggle', + templateUrl: './menu-toggle.component.html', + styleUrls: ['./menu-toggle.component.scss'] +}) +export class MenuToggleComponent implements OnInit { + + @Input() section: MenuSection; + + constructor(private router: Router) { + } + + ngOnInit() { + } + + sectionActive(): boolean { + return this.router.isActive(this.section.path, false); + } + + sectionHeight(): string { + if (this.router.isActive(this.section.path, false)) { + return this.section.height; + } else { + return '0px'; + } + } + + trackBySectionPages(index: number, section: MenuSection){ + return section.id; + } +} diff --git a/ui-ngx/src/app/modules/home/menu/side-menu.component.html b/ui-ngx/src/app/modules/home/menu/side-menu.component.html new file mode 100644 index 0000000000..3819af5777 --- /dev/null +++ b/ui-ngx/src/app/modules/home/menu/side-menu.component.html @@ -0,0 +1,23 @@ + +
    +
  • + + +
  • +
diff --git a/ui-ngx/src/app/modules/home/menu/side-menu.component.scss b/ui-ngx/src/app/modules/home/menu/side-menu.component.scss new file mode 100644 index 0000000000..bd3bf999ca --- /dev/null +++ b/ui-ngx/src/app/modules/home/menu/side-menu.component.scss @@ -0,0 +1,70 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:host { + width: 100%; +} + +:host ::ng-deep { + + .tb-side-menu, + .tb-side-menu ul { + padding: 0; + margin-top: 0; + list-style: none; + } + + .tb-side-menu > li { + border-bottom: 1px solid rgba(0, 0, 0, .12); + } + + a.mat-button { + display: flex; + overflow: hidden; + line-height: 40px; + border-radius: 0; + &:hover { + border-bottom: none; + background-color: rgba(255,255,255,0.08); + } + &:focus { + border-bottom: none; + } + &.tb-active { + font-weight: 500; + background-color: rgba(255, 255, 255, .15); + } + + .mat-button-wrapper { + width: 100%; + display: inline-flex; + align-items: center; + mat-icon { + margin-right: 8px; + margin-left: 0; + } + span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + &.tb-toggle-icon { + padding-top: 12px; + padding-bottom: 12px; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/menu/side-menu.component.ts b/ui-ngx/src/app/modules/home/menu/side-menu.component.ts new file mode 100644 index 0000000000..92e80d38aa --- /dev/null +++ b/ui-ngx/src/app/modules/home/menu/side-menu.component.ts @@ -0,0 +1,40 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, OnInit } from '@angular/core'; +import { MenuService } from '@core/services/menu.service'; +import { MenuSection } from '@core/services/menu.models'; + +@Component({ + selector: 'tb-side-menu', + templateUrl: './side-menu.component.html', + styleUrls: ['./side-menu.component.scss'] +}) +export class SideMenuComponent implements OnInit { + + menuSections$ = this.menuService.menuSections(); + + constructor(private menuService: MenuService) { + } + + trackByMenuSection(index: number, section: MenuSection){ + return section.id; + } + + ngOnInit() { + } + +} diff --git a/ui-ngx/src/app/modules/home/models/contact.models.ts b/ui-ngx/src/app/modules/home/models/contact.models.ts new file mode 100644 index 0000000000..1ba1e6fb35 --- /dev/null +++ b/ui-ngx/src/app/modules/home/models/contact.models.ts @@ -0,0 +1,291 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export const COUNTRIES = [ + 'Afghanistan', + 'Åland Islands', + 'Albania', + 'Algeria', + 'American Samoa', + 'Andorra', + 'Angola', + 'Anguilla', + 'Antarctica', + 'Antigua and Barbuda', + 'Argentina', + 'Armenia', + 'Aruba', + 'Australia', + 'Austria', + 'Azerbaijan', + 'Bahamas', + 'Bahrain', + 'Bangladesh', + 'Barbados', + 'Belarus', + 'Belgium', + 'Belize', + 'Benin', + 'Bermuda', + 'Bhutan', + 'Bolivia', + 'Bonaire, Sint Eustatius and Saba', + 'Bosnia and Herzegovina', + 'Botswana', + 'Bouvet Island', + 'Brazil', + 'British Indian Ocean Territory', + 'Brunei Darussalam', + 'Bulgaria', + 'Burkina Faso', + 'Burundi', + 'Cambodia', + 'Cameroon', + 'Canada', + 'Cape Verde', + 'Cayman Islands', + 'Central African Republic', + 'Chad', + 'Chile', + 'China', + 'Christmas Island', + 'Cocos (Keeling) Islands', + 'Colombia', + 'Comoros', + 'Congo', + 'Congo, The Democratic Republic of the', + 'Cook Islands', + 'Costa Rica', + 'Côte d\'Ivoire', + 'Croatia', + 'Cuba', + 'Curaçao', + 'Cyprus', + 'Czech Republic', + 'Denmark', + 'Djibouti', + 'Dominica', + 'Dominican Republic', + 'Ecuador', + 'Egypt', + 'El Salvador', + 'Equatorial Guinea', + 'Eritrea', + 'Estonia', + 'Ethiopia', + 'Falkland Islands (Malvinas)', + 'Faroe Islands', + 'Fiji', + 'Finland', + 'France', + 'French Guiana', + 'French Polynesia', + 'French Southern Territories', + 'Gabon', + 'Gambia', + 'Georgia', + 'Germany', + 'Ghana', + 'Gibraltar', + 'Greece', + 'Greenland', + 'Grenada', + 'Guadeloupe', + 'Guam', + 'Guatemala', + 'Guernsey', + 'Guinea', + 'Guinea-Bissau', + 'Guyana', + 'Haiti', + 'Heard Island and McDonald Islands', + 'Holy See (Vatican City State)', + 'Honduras', + 'Hong Kong', + 'Hungary', + 'Iceland', + 'India', + 'Indonesia', + 'Iran, Islamic Republic of', + 'Iraq', + 'Ireland', + 'Isle of Man', + 'Israel', + 'Italy', + 'Jamaica', + 'Japan', + 'Jersey', + 'Jordan', + 'Kazakhstan', + 'Kenya', + 'Kiribati', + 'Korea, Democratic People\'s Republic of', + 'Korea, Republic of', + 'Kuwait', + 'Kyrgyzstan', + 'Lao People\'s Democratic Republic', + 'Latvia', + 'Lebanon', + 'Lesotho', + 'Liberia', + 'Libya', + 'Liechtenstein', + 'Lithuania', + 'Luxembourg', + 'Macao', + 'Macedonia, Republic Of', + 'Madagascar', + 'Malawi', + 'Malaysia', + 'Maldives', + 'Mali', + 'Malta', + 'Marshall Islands', + 'Martinique', + 'Mauritania', + 'Mauritius', + 'Mayotte', + 'Mexico', + 'Micronesia, Federated States of', + 'Moldova, Republic of', + 'Monaco', + 'Mongolia', + 'Montenegro', + 'Montserrat', + 'Morocco', + 'Mozambique', + 'Myanmar', + 'Namibia', + 'Nauru', + 'Nepal', + 'Netherlands', + 'New Caledonia', + 'New Zealand', + 'Nicaragua', + 'Niger', + 'Nigeria', + 'Niue', + 'Norfolk Island', + 'Northern Mariana Islands', + 'Norway', + 'Oman', + 'Pakistan', + 'Palau', + 'Palestinian Territory, Occupied', + 'Panama', + 'Papua New Guinea', + 'Paraguay', + 'Peru', + 'Philippines', + 'Pitcairn', + 'Poland', + 'Portugal', + 'Puerto Rico', + 'Qatar', + 'Reunion', + 'Romania', + 'Russian Federation', + 'Rwanda', + 'Saint Barthélemy', + 'Saint Helena, Ascension and Tristan da Cunha', + 'Saint Kitts and Nevis', + 'Saint Lucia', + 'Saint Martin (French Part)', + 'Saint Pierre and Miquelon', + 'Saint Vincent and the Grenadines', + 'Samoa', + 'San Marino', + 'Sao Tome and Principe', + 'Saudi Arabia', + 'Senegal', + 'Serbia', + 'Seychelles', + 'Sierra Leone', + 'Singapore', + 'Sint Maarten (Dutch Part)', + 'Slovakia', + 'Slovenia', + 'Solomon Islands', + 'Somalia', + 'South Africa', + 'South Georgia and the South Sandwich Islands', + 'South Sudan', + 'Spain', + 'Sri Lanka', + 'Sudan', + 'Suriname', + 'Svalbard and Jan Mayen', + 'Swaziland', + 'Sweden', + 'Switzerland', + 'Syrian Arab Republic', + 'Taiwan', + 'Tajikistan', + 'Tanzania, United Republic of', + 'Thailand', + 'Timor-Leste', + 'Togo', + 'Tokelau', + 'Tonga', + 'Trinidad and Tobago', + 'Tunisia', + 'Turkey', + 'Turkmenistan', + 'Turks and Caicos Islands', + 'Tuvalu', + 'Uganda', + 'Ukraine', + 'United Arab Emirates', + 'United Kingdom', + 'United States', + 'United States Minor Outlying Islands', + 'Uruguay', + 'Uzbekistan', + 'Vanuatu', + 'Venezuela', + 'Viet Nam', + 'Virgin Islands, British', + 'Virgin Islands, U.S.', + 'Wallis and Futuna', + 'Western Sahara', + 'Yemen', + 'Zambia', + 'Zimbabwe' +]; + +/* tslint:disable */ +export const POSTAL_CODE_PATTERNS = { + 'United States': '(\\d{5}([\\-]\\d{4})?)', + 'Australia': '[0-9]{4}', + 'Austria': '[0-9]{4}', + 'Belgium': '[0-9]{4}', + 'Brazil': '[0-9]{5}[\\-]?[0-9]{3}', + 'Canada': '^(?!.*[DFIOQU])[A-VXY][0-9][A-Z][ -]?[0-9][A-Z][0-9]$', + 'Denmark': '[0-9]{3,4}', + 'Faroe Islands': '[0-9]{3,4}', + 'Netherlands': '[1-9][0-9]{3}\\s?[a-zA-Z]{2}', + 'Germany': '[0-9]{5}', + 'Hungary': '[0-9]{4}', + 'Italy': '[0-9]{5}', + 'Japan': '\\d{3}-\\d{4}', + 'Luxembourg': '(L\\s*(-|—|–))\\s*?[\\d]{4}', + 'Poland': '[0-9]{2}\\-[0-9]{3}', + 'Spain': '((0[1-9]|5[0-2])|[1-4][0-9])[0-9]{3}', + 'Sweden': '\\d{3}\\s?\\d{2}', + 'United Kingdom': '[A-Za-z]{1,2}[0-9Rr][0-9A-Za-z]? [0-9][ABD-HJLNP-UW-Zabd-hjlnp-uw-z]{2}' +}; +/* tslint:enable */ + diff --git a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts new file mode 100644 index 0000000000..3709b08b28 --- /dev/null +++ b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts @@ -0,0 +1,519 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { GridsterComponent, GridsterConfig, GridsterItem, GridsterItemComponentInterface } from 'angular-gridster2'; +import { Widget, WidgetPosition, widgetType } from '@app/shared/models/widget.models'; +import { WidgetLayout, WidgetLayouts } from '@app/shared/models/dashboard.models'; +import { IDashboardWidget, WidgetAction, WidgetContext, WidgetHeaderAction } from './widget-component.models'; +import { Timewindow } from '@shared/models/time/time.models'; +import { Observable, of, Subject } from 'rxjs'; +import { guid, isDefined, isEqual, isUndefined } from '@app/core/utils'; +import { IterableDiffer, KeyValueDiffer } from '@angular/core'; +import { IAliasController, IStateController } from '@app/core/api/widget-api.models'; +import { enumerable } from '@shared/decorators/enumerable'; + +export interface WidgetsData { + widgets: Array; + widgetLayouts?: WidgetLayouts; +} + +export interface ContextMenuItem { + enabled: boolean; + shortcut?: string; + icon: string; + value: string; +} + +export interface DashboardContextMenuItem extends ContextMenuItem { + action: (contextMenuEvent: MouseEvent) => void; +} + +export interface WidgetContextMenuItem extends ContextMenuItem { + action: (contextMenuEvent: MouseEvent, widget: Widget) => void; +} + +export interface DashboardCallbacks { + onEditWidget?: ($event: Event, widget: Widget) => void; + onExportWidget?: ($event: Event, widget: Widget) => void; + onRemoveWidget?: ($event: Event, widget: Widget) => void; + onWidgetMouseDown?: ($event: Event, widget: Widget) => void; + onWidgetClicked?: ($event: Event, widget: Widget) => void; + prepareDashboardContextMenu?: ($event: Event) => Array; + prepareWidgetContextMenu?: ($event: Event, widget: Widget) => Array; +} + +export interface IDashboardComponent { + gridsterOpts: GridsterConfig; + gridster: GridsterComponent; + dashboardWidgets: DashboardWidgets; + mobileAutofillHeight: boolean; + isMobileSize: boolean; + autofillHeight: boolean; + dashboardTimewindow: Timewindow; + dashboardTimewindowChanged: Observable; + aliasController: IAliasController; + stateController: IStateController; + onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval?: number, persist?: boolean): void; + onResetTimewindow(): void; + resetHighlight(): void; + highlightWidget(widgetId: string, delay?: number); + selectWidget(widgetId: string, delay?: number); + getSelectedWidget(): Widget; + getEventGridPosition(event: Event): WidgetPosition; + notifyGridsterOptionsChanged(); + pauseChangeNotifications(); + resumeChangeNotifications(); + notifyLayoutUpdated(); +} + +declare type DashboardWidgetUpdateOperation = 'add' | 'remove' | 'update'; + +interface DashboardWidgetUpdateRecord { + widget?: Widget; + widgetLayout?: WidgetLayout; + widgetId: string; + operation: DashboardWidgetUpdateOperation; +} + +export class DashboardWidgets implements Iterable { + + highlightedMode = false; + + dashboardWidgets: Array = []; + widgets: Iterable; + widgetLayouts: WidgetLayouts; + + [Symbol.iterator](): Iterator { + return this.dashboardWidgets[Symbol.iterator](); + } + + constructor(private dashboard: IDashboardComponent, + private widgetsDiffer: IterableDiffer, + private widgetLayoutsDiffer: KeyValueDiffer) { + } + + doCheck() { + const widgetChange = this.widgetsDiffer.diff(this.widgets); + const widgetLayoutChange = this.widgetLayoutsDiffer.diff(this.widgetLayouts); + const updateRecords: Array = []; + + if (widgetChange !== null) { + widgetChange.forEachAddedItem((added) => { + updateRecords.push({ + widget: added.item, + widgetId: added.item.id, + widgetLayout: this.widgetLayouts ? this.widgetLayouts[added.item.id] : null, + operation: 'add' + }); + }); + widgetChange.forEachRemovedItem((removed) => { + let operation = updateRecords.find((record) => record.widgetId === removed.item.id); + if (operation) { + operation.operation = 'update'; + } else { + operation = { + widgetId: removed.item.id, + operation: 'remove' + }; + updateRecords.push(operation); + } + }); + } + if (widgetLayoutChange !== null) { + widgetLayoutChange.forEachChangedItem((changed) => { + const operation = updateRecords.find((record) => record.widgetId === changed.key); + if (!operation) { + const widget = this.dashboardWidgets.find((dashboardWidget) => dashboardWidget.widgetId === changed.key); + if (widget) { + updateRecords.push({ + widget: widget.widget, + widgetId: changed.key, + widgetLayout: changed.currentValue, + operation: 'update' + }); + } + } + }); + } + if (updateRecords.length) { + updateRecords.forEach((record) => { + switch (record.operation) { + case 'add': + this.dashboardWidgets.push( + new DashboardWidget(this.dashboard, record.widget, record.widgetLayout) + ); + break; + case 'remove': + let index = this.dashboardWidgets.findIndex((dashboardWidget) => dashboardWidget.widgetId === record.widgetId); + if (index > -1) { + this.dashboardWidgets.splice(index, 1); + } + break; + case 'update': + index = this.dashboardWidgets.findIndex((dashboardWidget) => dashboardWidget.widgetId === record.widgetId); + if (index > -1) { + const prevDashboardWidget = this.dashboardWidgets[index]; + if (!isEqual(prevDashboardWidget.widget, record.widget) || + !isEqual(prevDashboardWidget.widgetLayout, record.widgetLayout)) { + this.dashboardWidgets[index] = new DashboardWidget(this.dashboard, record.widget, record.widgetLayout); + this.dashboardWidgets[index].highlighted = prevDashboardWidget.highlighted; + this.dashboardWidgets[index].selected = prevDashboardWidget.selected; + } else { + this.dashboardWidgets[index].widget = record.widget; + this.dashboardWidgets[index].widgetLayout = record.widgetLayout; + } + } + break; + } + }); + this.updateRowsAndSort(); + } + } + + widgetLayoutsUpdated() { + for (const w of Object.keys(this.widgetLayouts)) { + const widgetLayout = this.widgetLayouts[w]; + const index = this.dashboardWidgets.findIndex((dashboardWidget) => dashboardWidget.widgetId === w); + if (index > -1) { + this.dashboardWidgets[index].widgetLayout = widgetLayout; + } + } + this.updateRowsAndSort(); + } + + setWidgets(widgets: Iterable, widgetLayouts: WidgetLayouts) { + this.highlightedMode = false; + this.widgets = widgets; + this.widgetLayouts = widgetLayouts; + } + + highlightWidget(widgetId: string): DashboardWidget { + const widget = this.findWidgetById(widgetId); + if (widget && (!this.highlightedMode || !widget.highlighted || this.highlightedMode && widget.highlighted)) { + this.highlightedMode = true; + widget.highlighted = true; + this.dashboardWidgets.forEach((dashboardWidget) => { + if (dashboardWidget !== widget) { + dashboardWidget.highlighted = false; + } + }); + return widget; + } else { + return null; + } + } + + selectWidget(widgetId: string): DashboardWidget { + const widget = this.findWidgetById(widgetId); + if (widget && (!widget.selected)) { + widget.selected = true; + this.dashboardWidgets.forEach((dashboardWidget) => { + if (dashboardWidget !== widget) { + dashboardWidget.selected = false; + } + }); + return widget; + } else { + return null; + } + } + + resetHighlight(): DashboardWidget { + const highlighted = this.dashboardWidgets.find((dashboardWidget) => dashboardWidget.highlighted); + this.highlightedMode = false; + this.dashboardWidgets.forEach((dashboardWidget) => { + dashboardWidget.highlighted = false; + dashboardWidget.selected = false; + }); + return highlighted; + } + + isHighlighted(widget: DashboardWidget): boolean { + return (this.highlightedMode && widget.highlighted) || (widget.selected); + } + + isNotHighlighted(widget: DashboardWidget): boolean { + return this.highlightedMode && !widget.highlighted; + } + + getSelectedWidget(): DashboardWidget { + return this.dashboardWidgets.find((dashboardWidget) => dashboardWidget.selected); + } + + private findWidgetById(widgetId: string): DashboardWidget { + return this.dashboardWidgets.find((dashboardWidget) => dashboardWidget.widgetId === widgetId); + } + + private updateRowsAndSort() { + let maxRows = this.dashboard.gridsterOpts.maxRows; + this.dashboardWidgets.forEach((dashboardWidget) => { + const bottom = dashboardWidget.y + dashboardWidget.rows; + maxRows = Math.max(maxRows, bottom); + }); + this.sortWidgets(); + this.dashboard.gridsterOpts.maxRows = maxRows; + this.dashboard.notifyGridsterOptionsChanged(); + } + + sortWidgets() { + this.dashboardWidgets.sort((widget1, widget2) => { + const row1 = widget1.widgetOrder; + const row2 = widget2.widgetOrder; + let res = row1 - row2; + if (res === 0) { + res = widget1.x - widget2.x; + } + return res; + }); + } + +} + +export class DashboardWidget implements GridsterItem, IDashboardWidget { + + highlighted = false; + selected = false; + + isFullscreen = false; + + color: string; + backgroundColor: string; + padding: string; + margin: string; + + title: string; + titleTooltip: string; + showTitle: boolean; + titleStyle: {[klass: string]: any}; + + titleIcon: string; + showTitleIcon: boolean; + titleIconStyle: {[klass: string]: any}; + + dropShadow: boolean; + enableFullscreen: boolean; + + hasTimewindow: boolean; + + hasAggregation: boolean; + + style: {[klass: string]: any}; + + showWidgetTitlePanel: boolean; + showWidgetActions: boolean; + + customHeaderActions: Array; + widgetActions: Array; + + widgetContext = new WidgetContext(this.dashboard, this, this.widget); + + widgetId: string; + + private gridsterItemComponentSubject = new Subject(); + private gridsterItemComponentValue: GridsterItemComponentInterface; + + set gridsterItemComponent(item: GridsterItemComponentInterface) { + this.gridsterItemComponentValue = item; + this.gridsterItemComponentSubject.next(this.gridsterItemComponentValue); + this.gridsterItemComponentSubject.complete(); + } + + constructor( + private dashboard: IDashboardComponent, + public widget: Widget, + public widgetLayout?: WidgetLayout) { + if (!widget.id) { + widget.id = guid(); + } + this.widgetId = widget.id; + this.updateWidgetParams(); + } + + gridsterItemComponent$(): Observable { + if (this.gridsterItemComponentValue) { + return of(this.gridsterItemComponentValue); + } else { + return this.gridsterItemComponentSubject.asObservable(); + } + } + + updateWidgetParams() { + this.color = this.widget.config.color || 'rgba(0, 0, 0, 0.87)'; + this.backgroundColor = this.widget.config.backgroundColor || '#fff'; + this.padding = this.widget.config.padding || '8px'; + this.margin = this.widget.config.margin || '0px'; + + this.title = isDefined(this.widgetContext.widgetTitle) + && this.widgetContext.widgetTitle.length ? this.widgetContext.widgetTitle : this.widget.config.title; + this.titleTooltip = isDefined(this.widgetContext.widgetTitleTooltip) + && this.widgetContext.widgetTitleTooltip.length ? this.widgetContext.widgetTitleTooltip : this.widget.config.titleTooltip; + this.showTitle = isDefined(this.widget.config.showTitle) ? this.widget.config.showTitle : true; + this.titleStyle = this.widget.config.titleStyle ? this.widget.config.titleStyle : {}; + + this.titleIcon = isDefined(this.widget.config.titleIcon) ? this.widget.config.titleIcon : ''; + this.showTitleIcon = isDefined(this.widget.config.showTitleIcon) ? this.widget.config.showTitleIcon : false; + this.titleIconStyle = {}; + if (this.widget.config.iconColor) { + this.titleIconStyle.color = this.widget.config.iconColor; + } + if (this.widget.config.iconSize) { + this.titleIconStyle.fontSize = this.widget.config.iconSize; + } + + this.dropShadow = isDefined(this.widget.config.dropShadow) ? this.widget.config.dropShadow : true; + this.enableFullscreen = isDefined(this.widget.config.enableFullscreen) ? this.widget.config.enableFullscreen : true; + + this.hasTimewindow = (this.widget.type === widgetType.timeseries || this.widget.type === widgetType.alarm) ? + (isDefined(this.widget.config.useDashboardTimewindow) ? + (!this.widget.config.useDashboardTimewindow && (isUndefined(this.widget.config.displayTimewindow) + || this.widget.config.displayTimewindow)) : false) + : false; + + this.hasAggregation = this.widget.type === widgetType.timeseries; + + this.style = {cursor: 'pointer', + color: this.color, + backgroundColor: this.backgroundColor, + padding: this.padding, + margin: this.margin}; + if (this.widget.config.widgetStyle) { + this.style = {...this.style, ...this.widget.config.widgetStyle}; + } + + this.showWidgetTitlePanel = this.widgetContext.hideTitlePanel ? false : + this.showTitle || this.hasTimewindow; + + this.showWidgetActions = this.widgetContext.hideTitlePanel ? false : true; + + this.customHeaderActions = this.widgetContext.customHeaderActions ? this.widgetContext.customHeaderActions : []; + this.widgetActions = this.widgetContext.widgetActions ? this.widgetContext.widgetActions : []; + } + + @enumerable(true) + get x(): number { + let res; + if (this.widgetLayout) { + res = this.widgetLayout.col; + } else { + res = this.widget.col; + } + return Math.floor(res); + } + + set x(x: number) { + if (!this.dashboard.isMobileSize) { + if (this.widgetLayout) { + this.widgetLayout.col = x; + } else { + this.widget.col = x; + } + } + } + + @enumerable(true) + get y(): number { + let res; + if (this.widgetLayout) { + res = this.widgetLayout.row; + } else { + res = this.widget.row; + } + return Math.floor(res); + } + + set y(y: number) { + if (!this.dashboard.isMobileSize) { + if (this.widgetLayout) { + this.widgetLayout.row = y; + } else { + this.widget.row = y; + } + } + } + + @enumerable(true) + get cols(): number { + let res; + if (this.widgetLayout) { + res = this.widgetLayout.sizeX; + } else { + res = this.widget.sizeX; + } + return Math.floor(res); + } + + set cols(cols: number) { + if (!this.dashboard.isMobileSize) { + if (this.widgetLayout) { + this.widgetLayout.sizeX = cols; + } else { + this.widget.sizeX = cols; + } + } + } + + @enumerable(true) + get rows(): number { + let res; + if (this.dashboard.isMobileSize && !this.dashboard.mobileAutofillHeight) { + let mobileHeight; + if (this.widgetLayout) { + mobileHeight = this.widgetLayout.mobileHeight; + } + if (!mobileHeight && this.widget.config.mobileHeight) { + mobileHeight = this.widget.config.mobileHeight; + } + if (mobileHeight) { + res = mobileHeight; + } else { + const sizeY = this.widgetLayout ? this.widgetLayout.sizeY : this.widget.sizeY; + res = sizeY * 24 / this.dashboard.gridsterOpts.minCols; + } + } else { + if (this.widgetLayout) { + res = this.widgetLayout.sizeY; + } else { + res = this.widget.sizeY; + } + } + return Math.floor(res); + } + + set rows(rows: number) { + if (!this.dashboard.isMobileSize && !this.dashboard.autofillHeight) { + if (this.widgetLayout) { + this.widgetLayout.sizeY = rows; + } else { + this.widget.sizeY = rows; + } + } + } + + @enumerable(true) + get widgetOrder(): number { + let order; + if (this.widgetLayout && isDefined(this.widgetLayout.mobileOrder) && this.widgetLayout.mobileOrder >= 0) { + order = this.widgetLayout.mobileOrder; + } else if (isDefined(this.widget.config.mobileOrder) && this.widget.config.mobileOrder >= 0) { + order = this.widget.config.mobileOrder; + } else if (this.widgetLayout) { + order = this.widgetLayout.row; + } else { + order = this.widget.row; + } + return order; + } +} diff --git a/ui-ngx/src/app/modules/home/models/datasource/attribute-datasource.ts b/ui-ngx/src/app/modules/home/models/datasource/attribute-datasource.ts new file mode 100644 index 0000000000..3d46a8a8cf --- /dev/null +++ b/ui-ngx/src/app/modules/home/models/datasource/attribute-datasource.ts @@ -0,0 +1,149 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { CollectionViewer, DataSource, SelectionModel } from '@angular/cdk/collections'; +import { BehaviorSubject, Observable, of, ReplaySubject } from 'rxjs'; +import { emptyPageData, PageData } from '@shared/models/page/page-data'; +import { PageLink } from '@shared/models/page/page-link'; +import { catchError, map, publishReplay, refCount, take, tap } from 'rxjs/operators'; +import { EntityId } from '@app/shared/models/id/entity-id'; +import { TranslateService } from '@ngx-translate/core'; +import { + AttributeData, + AttributeScope, + isClientSideTelemetryType, + TelemetrySubscriber, + TelemetryType +} from '@shared/models/telemetry/telemetry.models'; +import { AttributeService } from '@core/http/attribute.service'; +import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service'; +import { NgZone } from '@angular/core'; + +export class AttributeDatasource implements DataSource { + + private attributesSubject = new BehaviorSubject([]); + private pageDataSubject = new BehaviorSubject>(emptyPageData()); + + public pageData$ = this.pageDataSubject.asObservable(); + + public selection = new SelectionModel(true, []); + + private allAttributes: Observable>; + private telemetrySubscriber: TelemetrySubscriber; + + constructor(private attributeService: AttributeService, + private telemetryWsService: TelemetryWebsocketService, + private zone: NgZone, + private translate: TranslateService) {} + + connect(collectionViewer: CollectionViewer): Observable> { + return this.attributesSubject.asObservable(); + } + + disconnect(collectionViewer: CollectionViewer): void { + this.attributesSubject.complete(); + this.pageDataSubject.complete(); + if (this.telemetrySubscriber) { + this.telemetrySubscriber.unsubscribe(); + this.telemetrySubscriber = null; + } + } + + loadAttributes(entityId: EntityId, attributesScope: TelemetryType, + pageLink: PageLink, reload: boolean = false): Observable> { + if (reload) { + this.allAttributes = null; + if (this.telemetrySubscriber) { + this.telemetrySubscriber.unsubscribe(); + this.telemetrySubscriber = null; + } + } + this.selection.clear(); + const result = new ReplaySubject>(); + this.fetchAttributes(entityId, attributesScope, pageLink).pipe( + catchError(() => of(emptyPageData())), + ).subscribe( + (pageData) => { + this.attributesSubject.next(pageData.data); + this.pageDataSubject.next(pageData); + result.next(pageData); + } + ); + return result; + } + + fetchAttributes(entityId: EntityId, attributesScope: TelemetryType, + pageLink: PageLink): Observable> { + return this.getAllAttributes(entityId, attributesScope).pipe( + map((data) => pageLink.filterData(data)) + ); + } + + getAllAttributes(entityId: EntityId, attributesScope: TelemetryType): Observable> { + if (!this.allAttributes) { + let attributesObservable: Observable>; + if (isClientSideTelemetryType.get(attributesScope)) { + this.telemetrySubscriber = TelemetrySubscriber.createEntityAttributesSubscription( + this.telemetryWsService, entityId, attributesScope, this.zone); + this.telemetrySubscriber.subscribe(); + attributesObservable = this.telemetrySubscriber.attributeData$(); + } else { + attributesObservable = this.attributeService.getEntityAttributes(entityId, attributesScope as AttributeScope); + } + this.allAttributes = attributesObservable.pipe( + publishReplay(1), + refCount() + ); + } + return this.allAttributes; + } + + isAllSelected(): Observable { + const numSelected = this.selection.selected.length; + return this.attributesSubject.pipe( + map((attributes) => numSelected === attributes.length) + ); + } + + isEmpty(): Observable { + return this.attributesSubject.pipe( + map((attributes) => !attributes.length) + ); + } + + total(): Observable { + return this.pageDataSubject.pipe( + map((pageData) => pageData.totalElements) + ); + } + + masterToggle() { + this.attributesSubject.pipe( + tap((attributes) => { + const numSelected = this.selection.selected.length; + if (numSelected === attributes.length) { + this.selection.clear(); + } else { + attributes.forEach(row => { + this.selection.select(row); + }); + } + }), + take(1) + ).subscribe(); + } + +} diff --git a/ui-ngx/src/app/modules/home/models/datasource/entity-datasource.ts b/ui-ngx/src/app/modules/home/models/datasource/entity-datasource.ts new file mode 100644 index 0000000000..4593ed2822 --- /dev/null +++ b/ui-ngx/src/app/modules/home/models/datasource/entity-datasource.ts @@ -0,0 +1,141 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { PageLink } from '@shared/models/page/page-link'; +import { BehaviorSubject, Observable, of, ReplaySubject } from 'rxjs'; +import { emptyPageData, PageData } from '@shared/models/page/page-data'; +import { BaseData, HasId } from '@shared/models/base-data'; +import { CollectionViewer, DataSource, SelectionModel } from '@angular/cdk/collections'; +import { catchError, map, share, take, tap } from 'rxjs/operators'; +import { EntityBooleanFunction } from '@home/models/entity/entities-table-config.models'; + +export type EntitiesFetchFunction, P extends PageLink> = (pageLink: P) => Observable>; + +export class EntitiesDataSource, P extends PageLink = PageLink> implements DataSource { + + private entitiesSubject = new BehaviorSubject([]); + private pageDataSubject = new BehaviorSubject>(emptyPageData()); + + public pageData$ = this.pageDataSubject.asObservable(); + + public selection = new SelectionModel(true, []); + + public currentEntity: T = null; + + public dataLoading = true; + + constructor(private fetchFunction: EntitiesFetchFunction, + protected selectionEnabledFunction: EntityBooleanFunction, + protected dataLoadedFunction: (col?: number, row?: number) => void) {} + + connect(collectionViewer: CollectionViewer): Observable> { + return this.entitiesSubject.asObservable(); + } + + disconnect(collectionViewer: CollectionViewer): void { + this.entitiesSubject.complete(); + this.pageDataSubject.complete(); + } + + reset() { + const pageData = emptyPageData(); + this.onEntities(pageData.data); + this.pageDataSubject.next(pageData); + this.dataLoadedFunction(); + } + + loadEntities(pageLink: P): Observable> { + this.dataLoading = true; + const result = new ReplaySubject>(); + this.fetchFunction(pageLink).pipe( + tap(() => { + this.selection.clear(); + }), + catchError(() => of(emptyPageData())), + ).subscribe( + (pageData) => { + this.onEntities(pageData.data); + this.pageDataSubject.next(pageData); + result.next(pageData); + this.dataLoadedFunction(); + this.dataLoading = false; + } + ); + return result; + } + + protected onEntities(entities: T[]) { + this.entitiesSubject.next(entities); + } + + isAllSelected(): Observable { + const numSelected = this.selection.selected.length; + return this.entitiesSubject.pipe( + map((entities) => numSelected === this.selectableEntitiesCount(entities)), + share() + ); + } + + isEmpty(): Observable { + return this.entitiesSubject.pipe( + map((entities) => !entities.length), + share() + ); + } + + total(): Observable { + return this.pageDataSubject.pipe( + map((pageData) => pageData.totalElements), + share() + ); + } + + toggleCurrentEntity(entity: T): boolean { + if (this.currentEntity !== entity) { + this.currentEntity = entity; + return true; + } else { + return false; + } + } + + isCurrentEntity(entity: T): boolean { + return (this.currentEntity && entity && this.currentEntity.id && entity.id) && + (this.currentEntity.id.id === entity.id.id); + } + + masterToggle() { + this.entitiesSubject.pipe( + tap((entities) => { + const numSelected = this.selection.selected.length; + if (numSelected === this.selectableEntitiesCount(entities)) { + this.selection.clear(); + } else { + entities.forEach(row => { + if (this.selectionEnabledFunction(row)) { + this.selection.select(row); + } + }); + } + }), + take(1) + ).subscribe(); + } + + private selectableEntitiesCount(entities: Array): number { + return entities.filter((entity) => this.selectionEnabledFunction(entity)).length; + } +} diff --git a/ui-ngx/src/app/modules/home/models/datasource/relation-datasource.ts b/ui-ngx/src/app/modules/home/models/datasource/relation-datasource.ts new file mode 100644 index 0000000000..ebf029cae2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/models/datasource/relation-datasource.ts @@ -0,0 +1,142 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { CollectionViewer, DataSource, SelectionModel } from '@angular/cdk/collections'; +import { EntityRelationInfo, EntitySearchDirection } from '@shared/models/relation.models'; +import { BehaviorSubject, Observable, of, ReplaySubject } from 'rxjs'; +import { emptyPageData, PageData } from '@shared/models/page/page-data'; +import { EntityRelationService } from '@core/http/entity-relation.service'; +import { PageLink } from '@shared/models/page/page-link'; +import { catchError, map, publishReplay, refCount, take, tap } from 'rxjs/operators'; +import { EntityId } from '@app/shared/models/id/entity-id'; +import { TranslateService } from '@ngx-translate/core'; +import { entityTypeTranslations } from '@shared/models/entity-type.models'; + +export class RelationsDatasource implements DataSource { + + private relationsSubject = new BehaviorSubject([]); + private pageDataSubject = new BehaviorSubject>(emptyPageData()); + + public pageData$ = this.pageDataSubject.asObservable(); + + public selection = new SelectionModel(true, []); + + private allRelations: Observable>; + + constructor(private entityRelationService: EntityRelationService, + private translate: TranslateService) {} + + connect(collectionViewer: CollectionViewer): Observable> { + return this.relationsSubject.asObservable(); + } + + disconnect(collectionViewer: CollectionViewer): void { + this.relationsSubject.complete(); + this.pageDataSubject.complete(); + } + + loadRelations(direction: EntitySearchDirection, entityId: EntityId, + pageLink: PageLink, reload: boolean = false): Observable> { + if (reload) { + this.allRelations = null; + } + const result = new ReplaySubject>(); + this.fetchRelations(direction, entityId, pageLink).pipe( + tap(() => { + this.selection.clear(); + }), + catchError(() => of(emptyPageData())), + ).subscribe( + (pageData) => { + this.relationsSubject.next(pageData.data); + this.pageDataSubject.next(pageData); + result.next(pageData); + } + ); + return result; + } + + fetchRelations(direction: EntitySearchDirection, entityId: EntityId, + pageLink: PageLink): Observable> { + return this.getAllRelations(direction, entityId).pipe( + map((data) => pageLink.filterData(data)) + ); + } + + getAllRelations(direction: EntitySearchDirection, entityId: EntityId): Observable> { + if (!this.allRelations) { + let relationsObservable: Observable>; + switch (direction) { + case EntitySearchDirection.FROM: + relationsObservable = this.entityRelationService.findInfoByFrom(entityId); + break; + case EntitySearchDirection.TO: + relationsObservable = this.entityRelationService.findInfoByTo(entityId); + break; + } + this.allRelations = relationsObservable.pipe( + map(relations => { + relations.forEach(relation => { + if (direction === EntitySearchDirection.FROM) { + relation.toEntityTypeName = this.translate.instant(entityTypeTranslations.get(relation.to.entityType).type); + } else { + relation.fromEntityTypeName = this.translate.instant(entityTypeTranslations.get(relation.from.entityType).type); + } + }); + return relations; + }), + publishReplay(1), + refCount() + ); + } + return this.allRelations; + } + + isAllSelected(): Observable { + const numSelected = this.selection.selected.length; + return this.relationsSubject.pipe( + map((relations) => numSelected === relations.length) + ); + } + + isEmpty(): Observable { + return this.relationsSubject.pipe( + map((relations) => !relations.length) + ); + } + + total(): Observable { + return this.pageDataSubject.pipe( + map((pageData) => pageData.totalElements) + ); + } + + masterToggle() { + this.relationsSubject.pipe( + tap((relations) => { + const numSelected = this.selection.selected.length; + if (numSelected === relations.length) { + this.selection.clear(); + } else { + relations.forEach(row => { + this.selection.select(row); + }); + } + }), + take(1) + ).subscribe(); + } +} diff --git a/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts b/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts new file mode 100644 index 0000000000..4fbbd32900 --- /dev/null +++ b/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts @@ -0,0 +1,187 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { BaseData, HasId } from '@shared/models/base-data'; +import { EntitiesDataSource, EntitiesFetchFunction } from '@home/models/datasource/entity-datasource'; +import { Observable, of } from 'rxjs'; +import { emptyPageData } from '@shared/models/page/page-data'; +import { DatePipe } from '@angular/common'; +import { Direction, SortOrder } from '@shared/models/page/sort-order'; +import { EntityType, EntityTypeResource, EntityTypeTranslation } from '@shared/models/entity-type.models'; +import { EntityComponent } from '@home/components/entity/entity.component'; +import { Type } from '@angular/core'; +import { EntityAction } from './entity-component.models'; +import { HasUUID } from '@shared/models/id/has-uuid'; +import { PageLink } from '@shared/models/page/page-link'; +import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; +import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component'; +import { ActivatedRoute } from '@angular/router'; +import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; + +export type EntityBooleanFunction> = (entity: T) => boolean; +export type EntityStringFunction> = (entity: T) => string; +export type EntityVoidFunction> = (entity: T) => void; +export type EntityIdsVoidFunction> = (ids: HasUUID[]) => void; +export type EntityCountStringFunction = (count: number) => string; +export type EntityTwoWayOperation> = (entity: T) => Observable; +export type EntityByIdOperation> = (id: HasUUID) => Observable; +export type EntityIdOneWayOperation = (id: HasUUID) => Observable; +export type EntityActionFunction> = (action: EntityAction) => boolean; +export type CreateEntityOperation> = () => Observable; +export type EntityRowClickFunction> = (event: Event, entity: T) => boolean; + +export type CellContentFunction> = (entity: T, key: string) => string; +export type CellTooltipFunction> = (entity: T, key: string) => string | undefined; +export type HeaderCellStyleFunction> = (key: string) => object; +export type CellStyleFunction> = (entity: T, key: string) => object; + +export interface CellActionDescriptor> { + name: string; + nameFunction?: (entity: T) => string; + icon?: string; + mdiIcon?: string; + style?: any; + isEnabled: (entity: T) => boolean; + onAction: ($event: MouseEvent, entity: T) => void; +} + +export interface GroupActionDescriptor> { + name: string; + icon: string; + isEnabled: boolean; + onAction: ($event: MouseEvent, entities: T[]) => void; +} + +export interface HeaderActionDescriptor { + name: string; + icon: string; + isEnabled: () => boolean; + onAction: ($event: MouseEvent) => void; +} + +export type EntityTableColumnType = 'content' | 'action'; + +export class BaseEntityTableColumn> { + constructor(public type: EntityTableColumnType, + public key: string, + public title: string, + public width: string = '0px', + public sortable: boolean = true, + public ignoreTranslate: boolean = false, + public mobileHide: boolean = false) { + } +} + +export class EntityTableColumn> extends BaseEntityTableColumn { + constructor(public key: string, + public title: string, + public width: string = '0px', + public cellContentFunction: CellContentFunction = (entity, property) => entity[property] ? entity[property] : '', + public cellStyleFunction: CellStyleFunction = () => ({}), + public sortable: boolean = true, + public headerCellStyleFunction: HeaderCellStyleFunction = () => ({}), + public cellTooltipFunction: CellTooltipFunction = () => undefined, + public isNumberColumn: boolean = false) { + super('content', key, title, width, sortable); + } +} + +export class EntityActionTableColumn> extends BaseEntityTableColumn { + constructor(public key: string, + public title: string, + public actionDescriptor: CellActionDescriptor, + public width: string = '0px') { + super('action', key, title, width, false); + } +} + +export class DateEntityTableColumn> extends EntityTableColumn { + constructor(key: string, + title: string, + datePipe: DatePipe, + width: string = '0px', + dateFormat: string = 'yyyy-MM-dd HH:mm:ss', + cellStyleFunction: CellStyleFunction = () => ({})) { + super(key, + title, + width, + (entity, property) => datePipe.transform(entity[property], dateFormat), + cellStyleFunction); + } +} + +export type EntityColumn> = EntityTableColumn | EntityActionTableColumn; + +export class EntityTableConfig, P extends PageLink = PageLink, L extends BaseData = T> { + + constructor() {} + + componentsData: any = null; + + loadDataOnInit = true; + onLoadAction: (route: ActivatedRoute) => void = null; + table: EntitiesTableComponent = null; + useTimePageLink = false; + entityType: EntityType = null; + tableTitle = ''; + selectionEnabled = true; + searchEnabled = true; + addEnabled = true; + entitiesDeleteEnabled = true; + detailsPanelEnabled = true; + hideDetailsTabsOnEdit = true; + actionsColumnTitle = null; + entityTranslations: EntityTypeTranslation; + entityResources: EntityTypeResource; + entityComponent: Type>; + entityTabsComponent: Type>; + addDialogStyle = {}; + defaultSortOrder: SortOrder = {property: 'createdTime', direction: Direction.DESC}; + displayPagination = true; + defaultPageSize = 10; + columns: Array> = []; + cellActionDescriptors: Array> = []; + groupActionDescriptors: Array> = []; + headerActionDescriptors: Array = []; + addActionDescriptors: Array = []; + headerComponent: Type>; + addEntity: CreateEntityOperation = null; + dataSource: (dataLoadedFunction: (col?: number, row?: number) => void) + => EntitiesDataSource = (dataLoadedFunction: (col?: number, row?: number) => void) => { + return new EntitiesDataSource(this.entitiesFetchFunction, this.entitySelectionEnabled, dataLoadedFunction); + }; + detailsReadonly: EntityBooleanFunction = () => false; + entitySelectionEnabled: EntityBooleanFunction = () => true; + deleteEnabled: EntityBooleanFunction = () => true; + deleteEntityTitle: EntityStringFunction = () => ''; + deleteEntityContent: EntityStringFunction = () => ''; + deleteEntitiesTitle: EntityCountStringFunction = () => ''; + deleteEntitiesContent: EntityCountStringFunction = () => ''; + loadEntity: EntityByIdOperation = () => of(); + saveEntity: EntityTwoWayOperation = (entity) => of(entity); + deleteEntity: EntityIdOneWayOperation = () => of(); + entitiesFetchFunction: EntitiesFetchFunction = () => of(emptyPageData()); + onEntityAction: EntityActionFunction = () => false; + handleRowClick: EntityRowClickFunction = () => false; + entityTitle: EntityStringFunction = (entity) => entity?.name; + entityAdded: EntityVoidFunction = () => {}; + entityUpdated: EntityVoidFunction = () => {}; + entitiesDeleted: EntityIdsVoidFunction = () => {}; +} + +export function checkBoxCell(value: boolean): string { + return `${value ? 'check_box' : 'check_box_outline_blank'}`; +} diff --git a/ui-ngx/src/app/modules/home/models/entity/entity-component.models.ts b/ui-ngx/src/app/modules/home/models/entity/entity-component.models.ts new file mode 100644 index 0000000000..9b081431ef --- /dev/null +++ b/ui-ngx/src/app/modules/home/models/entity/entity-component.models.ts @@ -0,0 +1,28 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { BaseData, HasId } from '@shared/models/base-data'; +import { EntityTableConfig } from './entities-table-config.models'; + +export interface AddEntityDialogData> { + entitiesTableConfig: EntityTableConfig; +} + +export interface EntityAction> { + event: Event; + action: string; + entity: T; +} diff --git a/ui-ngx/src/app/modules/home/models/searchable-component.models.ts b/ui-ngx/src/app/modules/home/models/searchable-component.models.ts new file mode 100644 index 0000000000..3542aa4cb4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/models/searchable-component.models.ts @@ -0,0 +1,23 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export interface ISearchableComponent { + onSearchTextUpdated(searchText: string); +} + +export function instanceOfSearchableComponent(object: any): object is ISearchableComponent { + return 'onSearchTextUpdated' in object; +} diff --git a/ui-ngx/src/app/modules/home/models/services.map.ts b/ui-ngx/src/app/modules/home/models/services.map.ts new file mode 100644 index 0000000000..8a8fbbce4e --- /dev/null +++ b/ui-ngx/src/app/modules/home/models/services.map.ts @@ -0,0 +1,56 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Type } from '@angular/core'; +import { DeviceService } from '@core/http/device.service'; +import { AssetService } from '@core/http/asset.service'; +import { AttributeService } from '@core/http/attribute.service'; +import { EntityRelationService } from '@core/http/entity-relation.service'; +import { EntityService } from '@core/http/entity.service'; +import { DialogService } from '@core/services/dialog.service'; +import { CustomDialogService } from '@home/components/widget/dialog/custom-dialog.service'; +import { DatePipe } from '@angular/common'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { HttpClient } from '@angular/common/http'; +import { EntityViewService } from '@core/http/entity-view.service'; +import { CustomerService } from '@core/http/customer.service'; +import { DashboardService } from '@core/http/dashboard.service'; +import { UserService } from '@core/http/user.service'; +import { AlarmService } from '@core/http/alarm.service'; +import { Router } from '@angular/router'; + +export const ServicesMap = new Map>( + [ + ['deviceService', DeviceService], + ['alarmService', AlarmService], + ['assetService', AssetService], + ['entityViewService', EntityViewService], + ['customerService', CustomerService], + ['dashboardService', DashboardService], + ['userService', UserService], + ['attributeService', AttributeService], + ['entityRelationService', EntityRelationService], + ['entityService', EntityService], + ['dialogs', DialogService], + ['customDialog', CustomDialogService], + ['date', DatePipe], + ['utils', UtilsService], + ['translate', TranslateService], + ['http', HttpClient], + ['router', Router] + ] +); diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts new file mode 100644 index 0000000000..3b434d805a --- /dev/null +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -0,0 +1,462 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { IDashboardComponent } from '@home/models/dashboard-component.models'; +import { + DataSet, + Datasource, + DatasourceData, + JsonSettingsSchema, + Widget, + WidgetActionDescriptor, + WidgetActionSource, + WidgetConfig, + WidgetControllerDescriptor, + WidgetType, + widgetType, + WidgetTypeDescriptor, + WidgetTypeParameters +} from '@shared/models/widget.models'; +import { Timewindow, WidgetTimewindow } from '@shared/models/time/time.models'; +import { + IAliasController, + IStateController, + IWidgetSubscription, + IWidgetUtils, + RpcApi, + SubscriptionEntityInfo, + TimewindowFunctions, + WidgetActionsApi, + WidgetSubscriptionApi +} from '@core/api/widget-api.models'; +import { ChangeDetectorRef, ComponentFactory, Injector, NgZone, Type } from '@angular/core'; +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { RafService } from '@core/services/raf.service'; +import { WidgetTypeId } from '@shared/models/id/widget-type-id'; +import { TenantId } from '@shared/models/id/tenant-id'; +import { WidgetLayout } from '@shared/models/dashboard.models'; +import { formatValue, isDefined } from '@core/utils'; +import { forkJoin, of } from 'rxjs'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + NotificationHorizontalPosition, + NotificationType, + NotificationVerticalPosition +} from '@core/notification/notification.models'; +import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions'; +import { AuthUser } from '@shared/models/user.model'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { DeviceService } from '@core/http/device.service'; +import { AssetService } from '@core/http/asset.service'; +import { EntityViewService } from '@core/http/entity-view.service'; +import { CustomerService } from '@core/http/customer.service'; +import { DashboardService } from '@core/http/dashboard.service'; +import { UserService } from '@core/http/user.service'; +import { AttributeService } from '@core/http/attribute.service'; +import { EntityRelationService } from '@core/http/entity-relation.service'; +import { EntityService } from '@core/http/entity.service'; +import { DialogService } from '@core/services/dialog.service'; +import { CustomDialogService } from '@home/components/widget/dialog/custom-dialog.service'; +import { DatePipe } from '@angular/common'; +import { TranslateService } from '@ngx-translate/core'; +import { PageLink } from '@shared/models/page/page-link'; +import { SortOrder } from '@shared/models/page/sort-order'; +import { DomSanitizer } from '@angular/platform-browser'; +import { Router } from '@angular/router'; + +export interface IWidgetAction { + name: string; + icon: string; + onAction: ($event: Event) => void; +} + +export interface WidgetHeaderAction extends IWidgetAction { + displayName: string; + descriptor: WidgetActionDescriptor; +} + +export interface WidgetAction extends IWidgetAction { + show: boolean; +} + +export interface IDashboardWidget { + updateWidgetParams(); +} + +export class WidgetContext { + + constructor(public dashboard: IDashboardComponent, + private dashboardWidget: IDashboardWidget, + private widget: Widget) {} + + get stateController(): IStateController { + return this.dashboard.stateController; + } + + get aliasController(): IAliasController { + return this.dashboard.aliasController; + } + + get dashboardTimewindow(): Timewindow { + return this.dashboard.dashboardTimewindow; + } + + get widgetConfig(): WidgetConfig { + return this.widget.config; + } + + get settings(): any { + return this.widget.config.settings; + } + + get units(): string { + return this.widget.config.units || ''; + } + + get decimals(): number { + return isDefined(this.widget.config.decimals) ? this.widget.config.decimals : 2; + } + + set changeDetector(cd: ChangeDetectorRef) { + this.changeDetectorValue = cd; + } + + get currentUser(): AuthUser { + if (this.store) { + return getCurrentAuthUser(this.store); + } else { + return null; + } + } + + deviceService: DeviceService; + assetService: AssetService; + entityViewService: EntityViewService; + customerService: CustomerService; + dashboardService: DashboardService; + userService: UserService; + attributeService: AttributeService; + entityRelationService: EntityRelationService; + entityService: EntityService; + dialogs: DialogService; + customDialog: CustomDialogService; + date: DatePipe; + translate: TranslateService; + http: HttpClient; + sanitizer: DomSanitizer; + router: Router; + + private changeDetectorValue: ChangeDetectorRef; + + inited = false; + destroyed = false; + + subscriptions: {[id: string]: IWidgetSubscription} = {}; + defaultSubscription: IWidgetSubscription = null; + + timewindowFunctions: TimewindowFunctions = { + onUpdateTimewindow: (startTimeMs, endTimeMs, interval) => { + if (this.defaultSubscription) { + this.defaultSubscription.onUpdateTimewindow(startTimeMs, endTimeMs, interval); + } + }, + onResetTimewindow: () => { + if (this.defaultSubscription) { + this.defaultSubscription.onResetTimewindow(); + } + } + }; + + controlApi: RpcApi = { + sendOneWayCommand: (method, params, timeout) => { + if (this.defaultSubscription) { + return this.defaultSubscription.sendOneWayCommand(method, params, timeout); + } else { + return of(null); + } + }, + sendTwoWayCommand: (method, params, timeout) => { + if (this.defaultSubscription) { + return this.defaultSubscription.sendTwoWayCommand(method, params, timeout); + } else { + return of(null); + } + } + }; + + utils: IWidgetUtils = { + formatValue + }; + + $container: JQuery; + $containerParent: JQuery; + width: number; + height: number; + $scope: IDynamicWidgetComponent; + isEdit: boolean; + isMobile: boolean; + + widgetNamespace?: string; + subscriptionApi?: WidgetSubscriptionApi; + + actionsApi?: WidgetActionsApi; + activeEntityInfo?: SubscriptionEntityInfo; + + datasources?: Array; + data?: Array; + hiddenData?: Array<{data: DataSet}>; + timeWindow?: WidgetTimewindow; + + hideTitlePanel = false; + + widgetTitle?: string; + widgetTitleTooltip?: string; + customHeaderActions?: Array; + widgetActions?: Array; + + servicesMap?: Map>; + + $injector?: Injector; + + ngZone?: NgZone; + + store?: Store; + + rxjs = { + forkJoin, + of + }; + + showSuccessToast(message: string, duration: number = 1000, + verticalPosition: NotificationVerticalPosition = 'bottom', + horizontalPosition: NotificationHorizontalPosition = 'left', + target?: string) { + this.showToast('success', message, duration, verticalPosition, horizontalPosition, target); + } + + showInfoToast(message: string, + verticalPosition: NotificationVerticalPosition = 'bottom', + horizontalPosition: NotificationHorizontalPosition = 'left', + target?: string) { + this.showToast('info', message, undefined, verticalPosition, horizontalPosition, target); + } + + showWarnToast(message: string, + verticalPosition: NotificationVerticalPosition = 'bottom', + horizontalPosition: NotificationHorizontalPosition = 'left', + target?: string) { + this.showToast('warn', message, undefined, verticalPosition, horizontalPosition, target); + } + + showErrorToast(message: string, + verticalPosition: NotificationVerticalPosition = 'bottom', + horizontalPosition: NotificationHorizontalPosition = 'left', + target?: string) { + this.showToast('error', message, undefined, verticalPosition, horizontalPosition, target); + } + + showToast(type: NotificationType, message: string, duration: number, + verticalPosition: NotificationVerticalPosition = 'bottom', + horizontalPosition: NotificationHorizontalPosition = 'left', + target?: string) { + this.store.dispatch(new ActionNotificationShow( + { + message, + type, + duration, + verticalPosition, + horizontalPosition, + target, + panelClass: this.widgetNamespace, + forceDismiss: true + })); + } + + hideToast(target?: string) { + this.store.dispatch(new ActionNotificationHide( + { + target, + })); + } + + detectChanges(updateWidgetParams: boolean = false) { + if (!this.destroyed) { + if (updateWidgetParams) { + this.dashboardWidget.updateWidgetParams(); + } + try { + this.changeDetectorValue.detectChanges(); + } catch (e) { + // console.log(e); + } + } + } + + updateWidgetParams() { + if (!this.destroyed) { + setTimeout(() => { + this.dashboardWidget.updateWidgetParams(); + }, 0); + } + } + + updateAliases(aliasIds?: Array) { + this.aliasController.updateAliases(aliasIds); + } + + reset() { + this.destroyed = false; + this.hideTitlePanel = false; + this.widgetTitle = undefined; + this.widgetActions = undefined; + } + + pageLink(pageSize: number, page: number = 0, textSearch: string = null, sortOrder: SortOrder = null): PageLink { + return new PageLink(pageSize, page, textSearch, sortOrder); + } +} + +export interface IDynamicWidgetComponent { + readonly ctx: WidgetContext; + readonly errorMessages: string[]; + readonly $injector: Injector; + executingRpcRequest: boolean; + rpcEnabled: boolean; + rpcErrorText: string; + rpcRejection: HttpErrorResponse; + raf: RafService; + [key: string]: any; +} + +export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescriptor { + widgetName: string; + alias: string; + typeSettingsSchema?: string | any; + typeDataKeySettingsSchema?: string | any; + componentFactory?: ComponentFactory; +} + +export interface WidgetConfigComponentData { + config: WidgetConfig; + layout: WidgetLayout; + widgetType: widgetType; + typeParameters: WidgetTypeParameters; + actionSources: {[actionSourceId: string]: WidgetActionSource}; + isDataEnabled: boolean; + settingsSchema: JsonSettingsSchema; + dataKeySettingsSchema: JsonSettingsSchema; +} + +export const MissingWidgetType: WidgetInfo = { + type: widgetType.latest, + widgetName: 'Widget type not found', + alias: 'undefined', + sizeX: 8, + sizeY: 6, + resources: [], + templateHtml: '
' + + '
' + + '
', + templateCss: '', + controllerScript: 'self.onInit = function() {}', + settingsSchema: '{}\n', + dataKeySettingsSchema: '{}\n', + defaultConfig: '{\n' + + '"title": "Widget type not found",\n' + + '"datasources": [],\n' + + '"settings": {}\n' + + '}\n', + typeParameters: {} +}; + +export const ErrorWidgetType: WidgetInfo = { + type: widgetType.latest, + widgetName: 'Error loading widget', + alias: 'error', + sizeX: 8, + sizeY: 6, + resources: [], + templateHtml: '
' + + '
widget.widget-type-load-error
' + + '
{{ error }}
' + + '
', + templateCss: '', + controllerScript: 'self.onInit = function() {}', + settingsSchema: '{}\n', + dataKeySettingsSchema: '{}\n', + defaultConfig: '{\n' + + '"title": "Widget failed to load",\n' + + '"datasources": [],\n' + + '"settings": {}\n' + + '}\n', + typeParameters: {} +}; + +export interface WidgetTypeInstance { + getSettingsSchema?: () => string; + getDataKeySettingsSchema?: () => string; + typeParameters?: () => WidgetTypeParameters; + useCustomDatasources?: () => boolean; + actionSources?: () => {[actionSourceId: string]: WidgetActionSource}; + + onInit?: () => void; + onDataUpdated?: () => void; + onResize?: () => void; + onEditModeChanged?: () => void; + onMobileModeChanged?: () => void; + onDestroy?: () => void; +} + +export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo { + return { + widgetName: widgetTypeEntity.name, + alias: widgetTypeEntity.alias, + type: widgetTypeEntity.descriptor.type, + sizeX: widgetTypeEntity.descriptor.sizeX, + sizeY: widgetTypeEntity.descriptor.sizeY, + resources: widgetTypeEntity.descriptor.resources, + templateHtml: widgetTypeEntity.descriptor.templateHtml, + templateCss: widgetTypeEntity.descriptor.templateCss, + controllerScript: widgetTypeEntity.descriptor.controllerScript, + settingsSchema: widgetTypeEntity.descriptor.settingsSchema, + dataKeySettingsSchema: widgetTypeEntity.descriptor.dataKeySettingsSchema, + defaultConfig: widgetTypeEntity.descriptor.defaultConfig + }; +} + +export function toWidgetType(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId, bundleAlias: string): WidgetType { + const descriptor: WidgetTypeDescriptor = { + type: widgetInfo.type, + sizeX: widgetInfo.sizeX, + sizeY: widgetInfo.sizeY, + resources: widgetInfo.resources, + templateHtml: widgetInfo.templateHtml, + templateCss: widgetInfo.templateCss, + controllerScript: widgetInfo.controllerScript, + settingsSchema: widgetInfo.settingsSchema, + dataKeySettingsSchema: widgetInfo.dataKeySettingsSchema, + defaultConfig: widgetInfo.defaultConfig + }; + return { + id, + tenantId, + bundleAlias, + alias: widgetInfo.alias, + name: widgetInfo.widgetName, + descriptor + }; +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts new file mode 100644 index 0000000000..7fefbfb521 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts @@ -0,0 +1,128 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable, NgModule } from '@angular/core'; +import { Resolve, RouterModule, Routes } from '@angular/router'; + +import { MailServerComponent } from '@modules/home/pages/admin/mail-server.component'; +import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; +import { Authority } from '@shared/models/authority.enum'; +import { GeneralSettingsComponent } from '@modules/home/pages/admin/general-settings.component'; +import { SecuritySettingsComponent } from '@modules/home/pages/admin/security-settings.component'; +import { OAuth2SettingsComponent } from '@home/pages/admin/oauth2-settings.component'; +import { User } from '@shared/models/user.model'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { UserService } from '@core/http/user.service'; +import { Observable } from 'rxjs'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { OAuth2Service } from '@core/http/oauth2.service'; +import { UserProfileResolver } from '@home/pages/profile/profile-routing.module'; + +@Injectable() +export class OAuth2LoginProcessingUrlResolver implements Resolve { + + constructor(private oauth2Service: OAuth2Service) { + } + + resolve(): Observable { + return this.oauth2Service.getLoginProcessingUrl(); + } +} + +const routes: Routes = [ + { + path: 'settings', + data: { + auth: [Authority.SYS_ADMIN], + breadcrumb: { + label: 'admin.system-settings', + icon: 'settings' + } + }, + children: [ + { + path: '', + redirectTo: 'general', + pathMatch: 'full' + }, + { + path: 'general', + component: GeneralSettingsComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + auth: [Authority.SYS_ADMIN], + title: 'admin.general-settings', + breadcrumb: { + label: 'admin.general', + icon: 'settings_applications' + } + } + }, + { + path: 'outgoing-mail', + component: MailServerComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + auth: [Authority.SYS_ADMIN], + title: 'admin.outgoing-mail-settings', + breadcrumb: { + label: 'admin.outgoing-mail', + icon: 'mail' + } + } + }, + { + path: 'security-settings', + component: SecuritySettingsComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + auth: [Authority.SYS_ADMIN], + title: 'admin.security-settings', + breadcrumb: { + label: 'admin.security-settings', + icon: 'security' + } + } + }, + { + path: 'oauth2', + component: OAuth2SettingsComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + auth: [Authority.SYS_ADMIN], + title: 'admin.oauth2.oauth2', + breadcrumb: { + label: 'admin.oauth2.oauth2', + icon: 'security' + } + }, + resolve: { + loginProcessingUrl: OAuth2LoginProcessingUrlResolver + } + } + ] + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [ + OAuth2LoginProcessingUrlResolver + ] +}) +export class AdminRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts new file mode 100644 index 0000000000..73fef6a32f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts @@ -0,0 +1,43 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { AdminRoutingModule } from './admin-routing.module'; +import { SharedModule } from '@app/shared/shared.module'; +import { MailServerComponent } from '@modules/home/pages/admin/mail-server.component'; +import { GeneralSettingsComponent } from '@modules/home/pages/admin/general-settings.component'; +import { SecuritySettingsComponent } from '@modules/home/pages/admin/security-settings.component'; +import { HomeComponentsModule } from '@modules/home/components/home-components.module'; +import { OAuth2SettingsComponent } from '@modules/home/pages/admin/oauth2-settings.component'; + +@NgModule({ + declarations: + [ + GeneralSettingsComponent, + MailServerComponent, + SecuritySettingsComponent, + OAuth2SettingsComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + AdminRoutingModule + ] +}) +export class AdminModule { } diff --git a/ui-ngx/src/app/modules/home/pages/admin/general-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/general-settings.component.html new file mode 100644 index 0000000000..1a5c4da887 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/general-settings.component.html @@ -0,0 +1,51 @@ + +
+ + +
+ admin.general-settings +
+
+ + +
+ +
+
+ + admin.base-url + + + {{ 'admin.base-url-required' | translate }} + + + + {{ 'admin.prohibit-different-url' | translate }} + +
admin.prohibit-different-url-hint
+
+ +
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/admin/general-settings.component.scss b/ui-ngx/src/app/modules/home/pages/admin/general-settings.component.scss new file mode 100644 index 0000000000..bb3718c2da --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/general-settings.component.scss @@ -0,0 +1,18 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/general-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/general-settings.component.ts new file mode 100644 index 0000000000..90cdecff30 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/general-settings.component.ts @@ -0,0 +1,75 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, OnInit } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { PageComponent } from '@shared/components/page.component'; +import { Router } from '@angular/router'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { AdminSettings, GeneralSettings } from '@shared/models/settings.models'; +import { AdminService } from '@core/http/admin.service'; +import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; + +@Component({ + selector: 'tb-general-settings', + templateUrl: './general-settings.component.html', + styleUrls: ['./general-settings.component.scss', './settings-card.scss'] +}) +export class GeneralSettingsComponent extends PageComponent implements OnInit, HasConfirmForm { + + generalSettings: FormGroup; + adminSettings: AdminSettings; + + constructor(protected store: Store, + private router: Router, + private adminService: AdminService, + public fb: FormBuilder) { + super(store); + } + + ngOnInit() { + this.buildGeneralServerSettingsForm(); + this.adminService.getAdminSettings('general').subscribe( + (adminSettings) => { + this.adminSettings = adminSettings; + this.generalSettings.reset(this.adminSettings.jsonValue); + } + ); + } + + buildGeneralServerSettingsForm() { + this.generalSettings = this.fb.group({ + baseUrl: ['', [Validators.required]], + prohibitDifferentUrl: ['',[]] + }); + } + + save(): void { + this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.generalSettings.value}; + this.adminService.saveAdminSettings(this.adminSettings).subscribe( + (adminSettings) => { + this.adminSettings = adminSettings; + this.generalSettings.reset(this.adminSettings.jsonValue); + } + ); + } + + confirmForm(): FormGroup { + return this.generalSettings; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.html b/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.html new file mode 100644 index 0000000000..88424b72eb --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.html @@ -0,0 +1,147 @@ + +
+ + +
+ admin.outgoing-mail-settings + +
+
+
+ + +
+ +
+
+ + admin.mail-from + + + {{ 'admin.mail-from-required' | translate }} + + + + admin.smtp-protocol + + + {{protocol.toUpperCase()}} + + + +
+ + admin.smtp-host + + + {{ 'admin.smtp-host-required' | translate }} + + + + admin.smtp-port + + {{smtpPortInput.value?.length || 0}}/5 + + {{ 'admin.smtp-port-required' | translate }} + + + {{ 'admin.smtp-port-invalid' | translate }} + + +
+ + admin.timeout-msec + + {{timeoutInput.value?.length || 0}}/6 + + {{ 'admin.timeout-required' | translate }} + + + {{ 'admin.timeout-invalid' | translate }} + + + + {{ 'admin.enable-tls' | translate }} + + + admin.tls-version + + + {{ tlsVersion }} + + + + + {{ 'admin.enable-proxy' | translate }} + +
+
+ + admin.proxy-host + + + {{ 'admin.proxy-host-required' | translate }} + + + + admin.proxy-port + + + {{ 'admin.proxy-port-required' | translate }} + + + {{ 'admin.proxy-port-range' | translate }} + + +
+ + admin.proxy-user + + + + admin.proxy-password + + +
+ + common.username + + + + common.password + + +
+ + +
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.scss b/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.scss new file mode 100644 index 0000000000..bb3718c2da --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.scss @@ -0,0 +1,18 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.ts b/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.ts new file mode 100644 index 0000000000..4fa8e16612 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.ts @@ -0,0 +1,129 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, OnInit } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { PageComponent } from '@shared/components/page.component'; +import { Router } from '@angular/router'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { AdminSettings, MailServerSettings, smtpPortPattern } from '@shared/models/settings.models'; +import { AdminService } from '@core/http/admin.service'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { TranslateService } from '@ngx-translate/core'; +import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; +import { isString } from '@core/utils'; + +@Component({ + selector: 'tb-mail-server', + templateUrl: './mail-server.component.html', + styleUrls: ['./mail-server.component.scss', './settings-card.scss'] +}) +export class MailServerComponent extends PageComponent implements OnInit, HasConfirmForm { + + mailSettings: FormGroup; + adminSettings: AdminSettings; + smtpProtocols = ['smtp', 'smtps']; + + tlsVersions = ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3']; + + constructor(protected store: Store, + private router: Router, + private adminService: AdminService, + private translate: TranslateService, + public fb: FormBuilder) { + super(store); + } + + ngOnInit() { + this.buildMailServerSettingsForm(); + this.adminService.getAdminSettings('mail').subscribe( + (adminSettings) => { + this.adminSettings = adminSettings; + if (this.adminSettings.jsonValue && isString(this.adminSettings.jsonValue.enableTls)) { + this.adminSettings.jsonValue.enableTls = (this.adminSettings.jsonValue.enableTls as any) === 'true'; + } + this.mailSettings.reset(this.adminSettings.jsonValue); + this.enableProxyChanged(); + } + ); + } + + buildMailServerSettingsForm() { + this.mailSettings = this.fb.group({ + mailFrom: ['', [Validators.required]], + smtpProtocol: ['smtp'], + smtpHost: ['localhost', [Validators.required]], + smtpPort: ['25', [Validators.required, + Validators.pattern(smtpPortPattern), + Validators.maxLength(5)]], + timeout: ['10000', [Validators.required, + Validators.pattern(/^[0-9]{1,6}$/), + Validators.maxLength(6)]], + enableTls: [false], + tlsVersion: [], + enableProxy: [false, []], + proxyHost: ['', [Validators.required]], + proxyPort: ['', [Validators.required, Validators.min(1), Validators.max(65535)]], + proxyUser: [''], + proxyPassword: [''], + username: [''], + password: [''] + }); + this.registerDisableOnLoadFormControl(this.mailSettings.get('smtpProtocol')); + this.registerDisableOnLoadFormControl(this.mailSettings.get('enableTls')); + this.registerDisableOnLoadFormControl(this.mailSettings.get('enableProxy')); + this.mailSettings.get('enableProxy').valueChanges.subscribe(() => { + this.enableProxyChanged(); + }); + } + + enableProxyChanged(): void { + const enableProxy: boolean = this.mailSettings.get('enableProxy').value; + if (enableProxy) { + this.mailSettings.get('proxyHost').enable(); + this.mailSettings.get('proxyPort').enable(); + } else { + this.mailSettings.get('proxyHost').disable(); + this.mailSettings.get('proxyPort').disable(); + } + } + + sendTestMail(): void { + this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettings.value}; + this.adminService.sendTestMail(this.adminSettings).subscribe( + () => { + this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('admin.test-mail-sent'), + type: 'success' })); + } + ); + } + + save(): void { + this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettings.value}; + this.adminService.saveAdminSettings(this.adminSettings).subscribe( + (adminSettings) => { + this.adminSettings = adminSettings; + this.mailSettings.reset(this.adminSettings.jsonValue); + } + ); + } + + confirmForm(): FormGroup { + return this.mailSettings; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html new file mode 100644 index 0000000000..e9d3030019 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html @@ -0,0 +1,483 @@ + +
+ + +
+ admin.oauth2.oauth2 + +
+
+
+ + +
+ +
+
+ + {{ 'admin.oauth2.enable' | translate }} + +
+ +
+ + + + + + {{ domainListTittle(domain) }} + + + + + + + + +
+
+
+
+
+ + admin.oauth2.protocol + + + {{ domainSchemaTranslations.get(protocol) | translate | uppercase }} + + + + + admin.domain-name + + + {{ 'admin.error-verification-url' | translate }} + + +
+ + {{ 'admin.domain-name-unique' | translate }} + +
+ +
+ + admin.oauth2.redirect-uri-template + + + + + + + + + +
+
+ +
+ +
+
+
+
+ +
+
+ + +
+ + + + {{ getProviderName(registration) }} + + + + + + + +
+
+ + admin.oauth2.login-provider + + + {{ provider }} + + + +
+
+ +
+ + admin.oauth2.client-id + + + {{ 'admin.oauth2.client-id-required' | translate }} + + + + + admin.oauth2.client-secret + + + {{ 'admin.oauth2.client-secret-required' | translate }} + + +
+ + + + + {{ 'admin.oauth2.custom-setting' | translate }} + + + + + +
+ + admin.oauth2.access-token-uri + + + + {{ 'admin.oauth2.access-token-uri-required' | translate }} + + + {{ 'admin.oauth2.uri-pattern-error' | translate }} + + + + + admin.oauth2.authorization-uri + + + + {{ 'admin.oauth2.authorization-uri-required' | translate }} + + + {{ 'admin.oauth2.uri-pattern-error' | translate }} + + +
+ +
+ + admin.oauth2.jwk-set-uri + + + + {{ 'admin.oauth2.uri-pattern-error' | translate }} + + + + + admin.oauth2.user-info-uri + + + + {{ 'admin.oauth2.user-info-uri-required' | translate }} + + + {{ 'admin.oauth2.uri-pattern-error' | translate }} + + +
+ + + admin.oauth2.client-authentication-method + + + {{ clientAuthenticationMethod | uppercase }} + + + + +
+ + admin.oauth2.login-button-label + + + {{ 'admin.oauth2.login-button-label-required' | translate }} + + + + + admin.oauth2.login-button-icon + + +
+ +
+
+ + {{ 'admin.oauth2.allow-user-creation' | translate }} + + + {{ 'admin.oauth2.activate-user' | translate }} + +
+
+ + + admin.oauth2.scope + + + {{scope}} + cancel + + + + + {{ 'admin.oauth2.scope-required' | translate }} + + + + + +
+ + + admin.oauth2.user-name-attribute-name + + + {{ 'admin.oauth2.user-name-attribute-name-required' | translate }} + + + +
+ + admin.oauth2.type + + + {{ mapperConfigType }} + + + + +
+ + admin.oauth2.email-attribute-key + + + {{ 'admin.oauth2.email-attribute-key-required' | translate }} + + + +
+ + admin.oauth2.first-name-attribute-key + + + + + admin.oauth2.last-name-attribute-key + + +
+ +
+ + admin.oauth2.tenant-name-strategy + + + {{ tenantNameStrategy }} + + + + + + admin.oauth2.tenant-name-pattern + + + {{ 'admin.oauth2.tenant-name-pattern-required' | translate }} + + +
+ + + admin.oauth2.customer-name-pattern + + + +
+ + admin.oauth2.default-dashboard-name + + + + + {{ 'admin.oauth2.always-fullscreen' | translate}} + +
+
+ +
+ + admin.oauth2.url + + + {{ 'admin.oauth2.url-required' | translate }} + + + {{ 'admin.oauth2.url-pattern' | translate }} + + + +
+ + common.username + + + + + common.password + + +
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+ +
+
+
+
+
+
+
+
+ + +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.scss b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.scss new file mode 100644 index 0000000000..0358badb1c --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.scss @@ -0,0 +1,71 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host{ + .checkbox-row { + margin-top: 1em; + } + + .registration-card{ + margin-bottom: 0.5em; + border: 1px solid rgba(0, 0, 0, 0.2); + + .custom-settings{ + font-size: 16px; + .mat-expansion-panel-header{ + padding: 0 2px; + } + } + } + + .container{ + margin-bottom: 1em; + + .tb-highlight{ + margin: 0; + } + + .tb-hint{ + padding-bottom: 0; + } + } + + .mat-expansion-panel { + .mat-expansion-panel-header { + &.mat-expanded { + height: 48px; + } + } + } +} + +:host ::ng-deep{ + .registration-card{ + .custom-settings{ + .mat-expansion-panel-body{ + padding: 0 2px 1em; + } + .mat-form-field-suffix .mat-icon-button .mat-icon{ + font-size: 18px; + } + } + } + .domains-list{ + margin-bottom: 1.5em; + .mat-form-field-suffix .mat-icon-button .mat-icon{ + font-size: 24px; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts new file mode 100644 index 0000000000..ac26797899 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts @@ -0,0 +1,520 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; +import { AbstractControl, FormArray, FormBuilder, FormGroup, ValidationErrors, Validators } from '@angular/forms'; +import { + ClientAuthenticationMethod, + ClientRegistration, + DomainInfo, + DomainSchema, + domainSchemaTranslations, + MapperConfig, + MapperConfigBasic, + MapperConfigCustom, + MapperConfigType, + OAuth2ClientRegistrationTemplate, + OAuth2ClientsDomainParams, + OAuth2ClientsParams, + TenantNameStrategy +} from '@shared/models/oauth2.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { PageComponent } from '@shared/components/page.component'; +import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; +import { COMMA, ENTER } from '@angular/cdk/keycodes'; +import { MatChipInputEvent } from '@angular/material/chips'; +import { WINDOW } from '@core/services/window.service'; +import { forkJoin, Subscription } from 'rxjs'; +import { DialogService } from '@core/services/dialog.service'; +import { TranslateService } from '@ngx-translate/core'; +import { isDefined, isDefinedAndNotNull } from '@core/utils'; +import { OAuth2Service } from '@core/http/oauth2.service'; +import { ActivatedRoute } from '@angular/router'; + +@Component({ + selector: 'tb-oauth2-settings', + templateUrl: './oauth2-settings.component.html', + styleUrls: ['./oauth2-settings.component.scss', './settings-card.scss'] +}) +export class OAuth2SettingsComponent extends PageComponent implements OnInit, HasConfirmForm, OnDestroy { + + private URL_REGEXP = /^[A-Za-z][A-Za-z\d.+-]*:\/*(?:\w+(?::\w+)?@)?[^\s/]+(?::\d+)?(?:\/[\w#!:.,?+=&%@\-/]*)?$/; + private DOMAIN_AND_PORT_REGEXP = /^(?:\w+(?::\w+)?@)?[^\s/]+(?::\d+)?$/; + private subscriptions: Subscription[] = []; + private templates = new Map(); + private defaultProvider = { + additionalInfo: { + providerName: 'Custom' + }, + clientAuthenticationMethod: ClientAuthenticationMethod.POST, + userNameAttributeName: 'email', + mapperConfig: { + allowUserCreation: true, + activateUser: false, + type: MapperConfigType.BASIC, + basic: { + emailAttributeKey: 'email', + tenantNameStrategy: TenantNameStrategy.DOMAIN, + alwaysFullScreen: false + } + } + }; + + readonly separatorKeysCodes: number[] = [ENTER, COMMA]; + + oauth2SettingsForm: FormGroup; + auth2ClientsParams: OAuth2ClientsParams; + + clientAuthenticationMethods = Object.keys(ClientAuthenticationMethod); + mapperConfigType = MapperConfigType; + mapperConfigTypes = Object.keys(MapperConfigType); + tenantNameStrategies = Object.keys(TenantNameStrategy); + protocols = Object.keys(DomainSchema); + domainSchemaTranslations = domainSchemaTranslations; + + templateProvider = ['Custom']; + + private loginProcessingUrl: string = this.route.snapshot.data.loginProcessingUrl; + + constructor(protected store: Store, + private route: ActivatedRoute, + private oauth2Service: OAuth2Service, + private fb: FormBuilder, + private dialogService: DialogService, + private translate: TranslateService, + @Inject(WINDOW) private window: Window) { + super(store); + } + + ngOnInit(): void { + this.buildOAuth2SettingsForm(); + forkJoin([ + this.oauth2Service.getOAuth2Template(), + this.oauth2Service.getOAuth2Settings() + ]).subscribe( + ([templates, auth2ClientsParams]) => { + this.initTemplates(templates); + this.auth2ClientsParams = auth2ClientsParams; + this.initOAuth2Settings(this.auth2ClientsParams); + } + ); + } + + ngOnDestroy() { + super.ngOnDestroy(); + this.subscriptions.forEach((subscription) => { + subscription.unsubscribe(); + }); + } + + private initTemplates(templates: OAuth2ClientRegistrationTemplate[]): void { + templates.map(provider => { + delete provider.additionalInfo; + this.templates.set(provider.name, provider); + }); + this.templateProvider.push(...Array.from(this.templates.keys())); + this.templateProvider.sort(); + } + + get domainsParams(): FormArray { + return this.oauth2SettingsForm.get('domainsParams') as FormArray; + } + + private formBasicGroup(type: MapperConfigType, mapperConfigBasic?: MapperConfigBasic): FormGroup { + let tenantNamePattern; + if (mapperConfigBasic?.tenantNamePattern) { + tenantNamePattern = mapperConfigBasic.tenantNamePattern; + } else { + tenantNamePattern = {value: null, disabled: true}; + } + const basicGroup = this.fb.group({ + emailAttributeKey: [mapperConfigBasic?.emailAttributeKey ? mapperConfigBasic.emailAttributeKey : 'email', Validators.required], + firstNameAttributeKey: [mapperConfigBasic?.firstNameAttributeKey ? mapperConfigBasic.firstNameAttributeKey : ''], + lastNameAttributeKey: [mapperConfigBasic?.lastNameAttributeKey ? mapperConfigBasic.lastNameAttributeKey : ''], + tenantNameStrategy: [mapperConfigBasic?.tenantNameStrategy ? mapperConfigBasic.tenantNameStrategy : TenantNameStrategy.DOMAIN], + tenantNamePattern: [tenantNamePattern, Validators.required], + customerNamePattern: [mapperConfigBasic?.customerNamePattern ? mapperConfigBasic.customerNamePattern : null], + defaultDashboardName: [mapperConfigBasic?.defaultDashboardName ? mapperConfigBasic.defaultDashboardName : null], + alwaysFullScreen: [isDefinedAndNotNull(mapperConfigBasic?.alwaysFullScreen) ? mapperConfigBasic.alwaysFullScreen : false] + }); + + this.subscriptions.push(basicGroup.get('tenantNameStrategy').valueChanges.subscribe((domain) => { + if (domain === 'CUSTOM') { + basicGroup.get('tenantNamePattern').enable(); + } else { + basicGroup.get('tenantNamePattern').disable(); + } + })); + + return basicGroup; + } + + private formCustomGroup(mapperConfigCustom?: MapperConfigCustom): FormGroup { + return this.fb.group({ + url: [mapperConfigCustom?.url ? mapperConfigCustom.url : null, [Validators.required, Validators.pattern(this.URL_REGEXP)]], + username: [mapperConfigCustom?.username ? mapperConfigCustom.username : null], + password: [mapperConfigCustom?.password ? mapperConfigCustom.password : null] + }); + } + + private buildOAuth2SettingsForm(): void { + this.oauth2SettingsForm = this.fb.group({ + domainsParams: this.fb.array([]), + enabled: [false] + }); + } + + private initOAuth2Settings(auth2ClientsParams: OAuth2ClientsParams): void { + if (auth2ClientsParams) { + this.oauth2SettingsForm.patchValue({enabled: auth2ClientsParams.enabled}, {emitEvent: false}); + auth2ClientsParams.domainsParams.forEach((domain) => { + this.domainsParams.push(this.buildDomainsForm(domain)); + }); + } + } + + private uniqueDomainValidator(control: FormGroup): { [key: string]: boolean } | null { + if (control.parent?.value) { + const domain = control.value.name; + const listProtocols = control.parent.getRawValue() + .filter((domainInfo) => domainInfo.name === domain) + .map((domainInfo) => domainInfo.scheme); + if (listProtocols.length > 1 && listProtocols.indexOf(DomainSchema.MIXED) > -1 || + new Set(listProtocols).size !== listProtocols.length) { + return {unique: true}; + } + } + return null; + } + + public domainListTittle(control: AbstractControl): string { + const domainInfos = control.get('domainInfos').value as DomainInfo[]; + if (domainInfos.length) { + const domainList = new Set(); + domainInfos.forEach((domain) => { + domainList.add(domain.name); + }); + return Array.from(domainList).join(', '); + } + return this.translate.instant('admin.oauth2.new-domain'); + } + + private buildDomainsForm(auth2ClientsDomainParams?: OAuth2ClientsDomainParams): FormGroup { + const formDomain = this.fb.group({ + domainInfos: this.fb.array([], Validators.required), + clientRegistrations: this.fb.array([], Validators.required) + }); + + if (auth2ClientsDomainParams) { + auth2ClientsDomainParams.domainInfos.forEach((domain) => { + this.clientDomainInfos(formDomain).push(this.buildDomainForm(domain)); + }); + auth2ClientsDomainParams.clientRegistrations.forEach((registration) => { + this.clientDomainProviders(formDomain).push(this.buildProviderForm(registration)); + }); + } else { + this.clientDomainProviders(formDomain).push(this.buildProviderForm()); + this.clientDomainInfos(formDomain).push(this.buildDomainForm()); + } + + return formDomain; + } + + private buildDomainForm(domainInfo?: DomainInfo): FormGroup { + const domain = this.fb.group({ + name: [domainInfo ? domainInfo.name : this.window.location.hostname, [ + Validators.required, + Validators.pattern(this.DOMAIN_AND_PORT_REGEXP)]], + scheme: [domainInfo?.scheme ? domainInfo.scheme : DomainSchema.HTTPS, Validators.required] + }, {validators: this.uniqueDomainValidator}); + return domain; + } + + private buildProviderForm(clientRegistration?: ClientRegistration): FormGroup { + let additionalInfo = null; + if (isDefinedAndNotNull(clientRegistration?.additionalInfo)) { + additionalInfo = clientRegistration.additionalInfo; + if (this.templateProvider.indexOf(additionalInfo.providerName) === -1) { + additionalInfo.providerName = 'Custom'; + } + } + let defaultProviderName = 'Custom'; + if (this.templateProvider.indexOf('Google')) { + defaultProviderName = 'Google'; + } + + const clientRegistrationFormGroup = this.fb.group({ + additionalInfo: this.fb.group({ + providerName: [additionalInfo?.providerName ? additionalInfo?.providerName : defaultProviderName, Validators.required] + }), + loginButtonLabel: [clientRegistration?.loginButtonLabel ? clientRegistration.loginButtonLabel : null, Validators.required], + loginButtonIcon: [clientRegistration?.loginButtonIcon ? clientRegistration.loginButtonIcon : null], + clientId: [clientRegistration?.clientId ? clientRegistration.clientId : '', Validators.required], + clientSecret: [clientRegistration?.clientSecret ? clientRegistration.clientSecret : '', Validators.required], + accessTokenUri: [clientRegistration?.accessTokenUri ? clientRegistration.accessTokenUri : '', + [Validators.required, + Validators.pattern(this.URL_REGEXP)]], + authorizationUri: [clientRegistration?.authorizationUri ? clientRegistration.authorizationUri : '', + [Validators.required, + Validators.pattern(this.URL_REGEXP)]], + scope: this.fb.array(clientRegistration?.scope ? clientRegistration.scope : [], this.validateScope), + jwkSetUri: [clientRegistration?.jwkSetUri ? clientRegistration.jwkSetUri : '', Validators.pattern(this.URL_REGEXP)], + userInfoUri: [clientRegistration?.userInfoUri ? clientRegistration.userInfoUri : '', + [Validators.required, + Validators.pattern(this.URL_REGEXP)]], + clientAuthenticationMethod: [ + clientRegistration?.clientAuthenticationMethod ? clientRegistration.clientAuthenticationMethod : ClientAuthenticationMethod.POST, + Validators.required], + userNameAttributeName: [ + clientRegistration?.userNameAttributeName ? clientRegistration.userNameAttributeName : 'email', Validators.required], + mapperConfig: this.fb.group({ + allowUserCreation: [ + isDefinedAndNotNull(clientRegistration?.mapperConfig?.allowUserCreation) ? + clientRegistration.mapperConfig.allowUserCreation : true + ], + activateUser: [ + isDefinedAndNotNull(clientRegistration?.mapperConfig?.activateUser) ? clientRegistration.mapperConfig.activateUser : false + ], + type: [ + clientRegistration?.mapperConfig?.type ? clientRegistration.mapperConfig.type : MapperConfigType.BASIC, Validators.required + ] + } + ) + }); + + if (clientRegistration) { + this.changeMapperConfigType(clientRegistrationFormGroup, clientRegistration.mapperConfig.type, clientRegistration.mapperConfig); + } else { + this.changeMapperConfigType(clientRegistrationFormGroup, MapperConfigType.BASIC); + this.setProviderDefaultValue(defaultProviderName, clientRegistrationFormGroup); + } + + this.subscriptions.push(clientRegistrationFormGroup.get('mapperConfig.type').valueChanges.subscribe((value) => { + this.changeMapperConfigType(clientRegistrationFormGroup, value); + })); + + this.subscriptions.push(clientRegistrationFormGroup.get('additionalInfo.providerName').valueChanges.subscribe((provider) => { + (clientRegistrationFormGroup.get('scope') as FormArray).clear(); + this.setProviderDefaultValue(provider, clientRegistrationFormGroup); + })); + + return clientRegistrationFormGroup; + } + + private validateScope(control: AbstractControl): ValidationErrors | null { + const scope: string[] = control.value; + if (!scope || !scope.length) { + return { + required: true + }; + } + return null; + } + + private setProviderDefaultValue(provider: string, clientRegistration: FormGroup) { + if (provider === 'Custom') { + clientRegistration.reset(this.defaultProvider, {emitEvent: false}); + clientRegistration.get('accessTokenUri').enable(); + clientRegistration.get('authorizationUri').enable(); + clientRegistration.get('jwkSetUri').enable(); + clientRegistration.get('userInfoUri').enable(); + } else { + const template = this.templates.get(provider); + template.clientId = ''; + template.clientSecret = ''; + template.scope.forEach(() => { + (clientRegistration.get('scope') as FormArray).push(this.fb.control('')); + }); + clientRegistration.get('accessTokenUri').disable(); + clientRegistration.get('authorizationUri').disable(); + clientRegistration.get('jwkSetUri').disable(); + clientRegistration.get('userInfoUri').disable(); + clientRegistration.patchValue(template); + } + } + + private changeMapperConfigType(control: AbstractControl, type: MapperConfigType, predefinedValue?: MapperConfig) { + const mapperConfig = control.get('mapperConfig') as FormGroup; + if (type === MapperConfigType.CUSTOM) { + mapperConfig.removeControl('basic'); + mapperConfig.addControl('custom', this.formCustomGroup(predefinedValue?.custom)); + } else { + mapperConfig.removeControl('custom'); + if (!mapperConfig.get('basic')) { + mapperConfig.addControl('basic', this.formBasicGroup(type, predefinedValue?.basic)); + } + if (type === MapperConfigType.GITHUB) { + mapperConfig.get('basic.emailAttributeKey').disable(); + mapperConfig.get('basic.emailAttributeKey').patchValue(null, {emitEvent: false}); + } else { + mapperConfig.get('basic.emailAttributeKey').enable(); + } + } + } + + save(): void { + const setting = this.oauth2SettingsForm.getRawValue(); + this.oauth2Service.saveOAuth2Settings(setting).subscribe( + (oauth2Settings) => { + this.auth2ClientsParams = oauth2Settings; + this.oauth2SettingsForm.patchValue(this.oauth2SettingsForm, {emitEvent: false}); + this.oauth2SettingsForm.markAsUntouched(); + this.oauth2SettingsForm.markAsPristine(); + } + ); + } + + confirmForm(): FormGroup { + return this.oauth2SettingsForm; + } + + addScope(event: MatChipInputEvent, control: AbstractControl): void { + const input = event.input; + const value = event.value; + const controller = control.get('scope') as FormArray; + if ((value.trim() !== '')) { + controller.push(this.fb.control(value.trim())); + controller.markAsDirty(); + } + + if (input) { + input.value = ''; + } + } + + removeScope(i: number, control: AbstractControl): void { + const controller = control.get('scope') as FormArray; + controller.removeAt(i); + controller.markAsTouched(); + controller.markAsDirty(); + } + + addDomain(): void { + this.domainsParams.push(this.buildDomainsForm()); + } + + deleteDomain($event: Event, index: number): void { + if ($event) { + $event.stopPropagation(); + $event.preventDefault(); + } + + const domainName = this.domainListTittle(this.domainsParams.at(index)); + this.dialogService.confirm( + this.translate.instant('admin.oauth2.delete-domain-title', {domainName: domainName || ''}), + this.translate.instant('admin.oauth2.delete-domain-text'), null, + this.translate.instant('action.delete') + ).subscribe((data) => { + if (data) { + this.domainsParams.removeAt(index); + this.domainsParams.markAsTouched(); + this.domainsParams.markAsDirty(); + } + }); + } + + clientDomainProviders(control: AbstractControl): FormArray { + return control.get('clientRegistrations') as FormArray; + } + + clientDomainInfos(control: AbstractControl): FormArray { + return control.get('domainInfos') as FormArray; + } + + addProvider(control: AbstractControl): void { + this.clientDomainProviders(control).push(this.buildProviderForm()); + } + + deleteProvider($event: Event, control: AbstractControl, index: number): void { + if ($event) { + $event.stopPropagation(); + $event.preventDefault(); + } + + const providerName = this.clientDomainProviders(control).at(index).get('additionalInfo.providerName').value; + this.dialogService.confirm( + this.translate.instant('admin.oauth2.delete-registration-title', {name: providerName || ''}), + this.translate.instant('admin.oauth2.delete-registration-text'), null, + this.translate.instant('action.delete') + ).subscribe((data) => { + if (data) { + this.clientDomainProviders(control).removeAt(index); + this.clientDomainProviders(control).markAsTouched(); + this.clientDomainProviders(control).markAsDirty(); + } + }); + } + + toggleEditMode(control: AbstractControl, path: string) { + control.get(path).disabled ? control.get(path).enable() : control.get(path).disable(); + } + + getProviderName(controller: AbstractControl): string { + return controller.get('additionalInfo.providerName').value; + } + + isCustomProvider(controller: AbstractControl): boolean { + return this.getProviderName(controller) === 'Custom'; + } + + getHelpLink(controller: AbstractControl): string { + const provider = controller.get('additionalInfo.providerName').value; + if (provider === null || provider === 'Custom') { + return ''; + } + return this.templates.get(provider).helpLink; + } + + addDomainInfo(control: AbstractControl): void { + this.clientDomainInfos(control).push(this.buildDomainForm({ + name: '', + scheme: DomainSchema.HTTPS + })); + } + + removeDomain($event: Event, control: AbstractControl, index: number): void { + if ($event) { + $event.stopPropagation(); + $event.preventDefault(); + } + this.clientDomainInfos(control).removeAt(index); + this.clientDomainInfos(control).markAsTouched(); + this.clientDomainInfos(control).markAsDirty(); + } + + redirectURI(control: AbstractControl, schema?: DomainSchema): string { + const domainInfo = control.value as DomainInfo; + if (domainInfo.name !== '') { + let protocol; + if (isDefined(schema)) { + protocol = schema.toLowerCase(); + } else { + protocol = domainInfo.scheme === DomainSchema.MIXED ? DomainSchema.HTTPS.toLowerCase() : domainInfo.scheme.toLowerCase(); + } + return `${protocol}://${domainInfo.name}${this.loginProcessingUrl}`; + } + return ''; + } + + redirectURIMixed(control: AbstractControl): string { + return this.redirectURI(control, DomainSchema.HTTP); + } + + trackByParams(index: number): number { + return index; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html new file mode 100644 index 0000000000..e23e3e02ce --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html @@ -0,0 +1,155 @@ + +
+ + +
+ admin.security-settings + +
+
+
+ + +
+ +
+
+
+ + + + +
admin.general-policy
+
+
+ + admin.max-failed-login-attempts + + + {{ 'admin.minimum-max-failed-login-attempts-range' | translate }} + + + + admin.user-lockout-notification-email + + +
+ + + +
admin.password-policy
+
+
+
+ + admin.minimum-password-length + + + {{ 'admin.minimum-password-length-required' | translate }} + + + {{ 'admin.minimum-password-length-range' | translate }} + + + {{ 'admin.minimum-password-length-range' | translate }} + + + + admin.minimum-uppercase-letters + + + {{ 'admin.minimum-uppercase-letters-range' | translate }} + + + + admin.minimum-lowercase-letters + + + {{ 'admin.minimum-lowercase-letters-range' | translate }} + + + + admin.minimum-digits + + + {{ 'admin.minimum-digits-range' | translate }} + + + + admin.minimum-special-characters + + + {{ 'admin.minimum-special-characters-range' | translate }} + + + + admin.password-expiration-period-days + + + {{ 'admin.password-expiration-period-days-range' | translate }} + + + + admin.password-reuse-frequency-days + + + {{ 'admin.password-reuse-frequency-days-range' | translate }} + + +
+
+
+
+
+ +
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.scss b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.scss new file mode 100644 index 0000000000..9356738028 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.scss @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .mat-accordion-container { + margin-bottom: 16px; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts new file mode 100644 index 0000000000..52e6c64b6b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts @@ -0,0 +1,86 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, OnInit } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { PageComponent } from '@shared/components/page.component'; +import { Router } from '@angular/router'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { SecuritySettings } from '@shared/models/settings.models'; +import { AdminService } from '@core/http/admin.service'; +import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; + +@Component({ + selector: 'tb-security-settings', + templateUrl: './security-settings.component.html', + styleUrls: ['./security-settings.component.scss', './settings-card.scss'] +}) +export class SecuritySettingsComponent extends PageComponent implements OnInit, HasConfirmForm { + + securitySettingsFormGroup: FormGroup; + securitySettings: SecuritySettings; + + constructor(protected store: Store, + private router: Router, + private adminService: AdminService, + public fb: FormBuilder) { + super(store); + } + + ngOnInit() { + this.buildSecuritySettingsForm(); + this.adminService.getSecuritySettings().subscribe( + (securitySettings) => { + this.securitySettings = securitySettings; + this.securitySettingsFormGroup.reset(this.securitySettings); + } + ); + } + + buildSecuritySettingsForm() { + this.securitySettingsFormGroup = this.fb.group({ + maxFailedLoginAttempts: [null, [Validators.min(0)]], + userLockoutNotificationEmail: ['', []], + passwordPolicy: this.fb.group( + { + minimumLength: [null, [Validators.required, Validators.min(5), Validators.max(50)]], + minimumUppercaseLetters: [null, Validators.min(0)], + minimumLowercaseLetters: [null, Validators.min(0)], + minimumDigits: [null, Validators.min(0)], + minimumSpecialCharacters: [null, Validators.min(0)], + passwordExpirationPeriodDays: [null, Validators.min(0)], + passwordReuseFrequencyDays: [null, Validators.min(0)] + } + ) + }); + } + + save(): void { + this.securitySettings = {...this.securitySettings, ...this.securitySettingsFormGroup.value}; + this.adminService.saveSecuritySettings(this.securitySettings).subscribe( + (securitySettings) => { + this.securitySettings = securitySettings; + this.securitySettingsFormGroup.reset(this.securitySettings); + } + ); + } + + confirmForm(): FormGroup { + return this.securitySettingsFormGroup; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/settings-card.scss b/ui-ngx/src/app/modules/home/pages/admin/settings-card.scss new file mode 100644 index 0000000000..6e7c08532d --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/settings-card.scss @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../../../scss/constants"; + +:host { + mat-card.settings-card { + margin: 8px; + @media #{$mat-gt-sm} { + width: 60%; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/asset/asset-routing.module.ts b/ui-ngx/src/app/modules/home/pages/asset/asset-routing.module.ts new file mode 100644 index 0000000000..b882dff5e0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/asset/asset-routing.module.ts @@ -0,0 +1,50 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { EntitiesTableComponent } from '../../components/entity/entities-table.component'; +import { Authority } from '@shared/models/authority.enum'; +import { AssetsTableConfigResolver } from './assets-table-config.resolver'; + +const routes: Routes = [ + { + path: 'assets', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + title: 'asset.assets', + assetsType: 'tenant', + breadcrumb: { + label: 'asset.assets', + icon: 'domain' + } + }, + resolve: { + entitiesTableConfig: AssetsTableConfigResolver + } + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [ + AssetsTableConfigResolver + ] +}) +export class AssetRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/asset/asset-table-header.component.html b/ui-ngx/src/app/modules/home/pages/asset/asset-table-header.component.html new file mode 100644 index 0000000000..c68da42088 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/asset/asset-table-header.component.html @@ -0,0 +1,23 @@ + + + diff --git a/ui-ngx/src/app/modules/home/pages/asset/asset-table-header.component.scss b/ui-ngx/src/app/modules/home/pages/asset/asset-table-header.component.scss new file mode 100644 index 0000000000..bd3bc86b1c --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/asset/asset-table-header.component.scss @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../scss/constants'; + +:host { + flex: 1; + display: flex; + justify-content: flex-start; + min-width: 150px; +} + +:host ::ng-deep { + tb-entity-subtype-select { + width: 100%; + + mat-form-field { + font-size: 16px; + + .mat-form-field-wrapper { + padding-bottom: 0; + } + + .mat-form-field-underline { + bottom: 0; + } + + @media #{$mat-xs} { + width: 100%; + + .mat-form-field-infix { + width: auto !important; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/asset/asset-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/asset/asset-table-header.component.ts new file mode 100644 index 0000000000..3c879e2eac --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/asset/asset-table-header.component.ts @@ -0,0 +1,42 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTableHeaderComponent } from '../../components/entity/entity-table-header.component'; +import { EntityType } from '@shared/models/entity-type.models'; +import { AssetInfo } from '@shared/models/asset.models'; + +@Component({ + selector: 'tb-asset-table-header', + templateUrl: './asset-table-header.component.html', + styleUrls: ['./asset-table-header.component.scss'] +}) +export class AssetTableHeaderComponent extends EntityTableHeaderComponent { + + entityType = EntityType; + + constructor(protected store: Store) { + super(store); + } + + assetTypeChanged(assetType: string) { + this.entitiesTableConfig.componentsData.assetType = assetType; + this.entitiesTableConfig.table.resetSortAndFilter(true); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.html b/ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.html new file mode 100644 index 0000000000..dcb3f49bef --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.html @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.ts new file mode 100644 index 0000000000..c9cfb4b900 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.ts @@ -0,0 +1,38 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; +import { AssetInfo } from '@app/shared/models/asset.models'; + +@Component({ + selector: 'tb-asset-tabs', + templateUrl: './asset-tabs.component.html', + styleUrls: [] +}) +export class AssetTabsComponent extends EntityTabsComponent { + + constructor(protected store: Store) { + super(store); + } + + ngOnInit() { + super.ngOnInit(); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/asset/asset.component.html b/ui-ngx/src/app/modules/home/pages/asset/asset.component.html new file mode 100644 index 0000000000..a78d55ef93 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/asset/asset.component.html @@ -0,0 +1,92 @@ + +
+ + + + +
+ +
+
+
+ + asset.assignedToCustomer + + +
+ {{ 'asset.asset-public' | translate }} +
+
+
+ + asset.name + + + {{ 'asset.name-required' | translate }} + + + + + + asset.label + + +
+ + asset.description + + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/asset/asset.component.scss b/ui-ngx/src/app/modules/home/pages/asset/asset.component.scss new file mode 100644 index 0000000000..bb3718c2da --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/asset/asset.component.scss @@ -0,0 +1,18 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + +} diff --git a/ui-ngx/src/app/modules/home/pages/asset/asset.component.ts b/ui-ngx/src/app/modules/home/pages/asset/asset.component.ts new file mode 100644 index 0000000000..5c3fd44a8e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/asset/asset.component.ts @@ -0,0 +1,98 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityComponent } from '../../components/entity/entity.component'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { EntityType } from '@shared/models/entity-type.models'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { TranslateService } from '@ngx-translate/core'; +import { AssetInfo } from '@app/shared/models/asset.models'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; + +@Component({ + selector: 'tb-asset', + templateUrl: './asset.component.html', + styleUrls: ['./asset.component.scss'] +}) +export class AssetComponent extends EntityComponent { + + entityType = EntityType; + + assetScope: 'tenant' | 'customer' | 'customer_user'; + + constructor(protected store: Store, + protected translate: TranslateService, + @Inject('entity') protected entityValue: AssetInfo, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + public fb: FormBuilder) { + super(store, fb, entityValue, entitiesTableConfigValue); + } + + ngOnInit() { + this.assetScope = this.entitiesTableConfig.componentsData.assetScope; + super.ngOnInit(); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + isAssignedToCustomer(entity: AssetInfo): boolean { + return entity && entity.customerId && entity.customerId.id !== NULL_UUID; + } + + buildForm(entity: AssetInfo): FormGroup { + return this.fb.group( + { + name: [entity ? entity.name : '', [Validators.required]], + type: [entity ? entity.type : null, [Validators.required]], + label: [entity ? entity.label : ''], + additionalInfo: this.fb.group( + { + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''], + } + ) + } + ); + } + + updateForm(entity: AssetInfo) { + this.entityForm.patchValue({name: entity.name}); + this.entityForm.patchValue({type: entity.type}); + this.entityForm.patchValue({label: entity.label}); + this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}}); + } + + + onAssetIdCopied($event) { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('asset.idCopiedMessage'), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/asset/asset.module.ts b/ui-ngx/src/app/modules/home/pages/asset/asset.module.ts new file mode 100644 index 0000000000..4bcb2404ab --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/asset/asset.module.ts @@ -0,0 +1,41 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { HomeDialogsModule } from '../../dialogs/home-dialogs.module'; +import { AssetComponent } from './asset.component'; +import { AssetTableHeaderComponent } from './asset-table-header.component'; +import { AssetRoutingModule } from './asset-routing.module'; +import { HomeComponentsModule } from '@modules/home/components/home-components.module'; +import { AssetTabsComponent } from '@home/pages/asset/asset-tabs.component'; + +@NgModule({ + declarations: [ + AssetComponent, + AssetTabsComponent, + AssetTableHeaderComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + HomeDialogsModule, + AssetRoutingModule + ] +}) +export class AssetModule { } diff --git a/ui-ngx/src/app/modules/home/pages/asset/assets-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/asset/assets-table-config.resolver.ts new file mode 100644 index 0000000000..d199af2542 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/asset/assets-table-config.resolver.ts @@ -0,0 +1,429 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; + +import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router'; +import { + CellActionDescriptor, + checkBoxCell, + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig, + GroupActionDescriptor, + HeaderActionDescriptor +} from '@home/models/entity/entities-table-config.models'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { forkJoin, Observable, of } from 'rxjs'; +import { select, Store } from '@ngrx/store'; +import { selectAuthUser } from '@core/auth/auth.selectors'; +import { map, mergeMap, take, tap } from 'rxjs/operators'; +import { AppState } from '@core/core.state'; +import { Authority } from '@app/shared/models/authority.enum'; +import { CustomerService } from '@core/http/customer.service'; +import { Customer } from '@app/shared/models/customer.model'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { BroadcastService } from '@core/services/broadcast.service'; +import { MatDialog } from '@angular/material/dialog'; +import { DialogService } from '@core/services/dialog.service'; +import { + AssignToCustomerDialogComponent, + AssignToCustomerDialogData +} from '@modules/home/dialogs/assign-to-customer-dialog.component'; +import { + AddEntitiesToCustomerDialogComponent, + AddEntitiesToCustomerDialogData +} from '../../dialogs/add-entities-to-customer-dialog.component'; +import { Asset, AssetInfo } from '@app/shared/models/asset.models'; +import { AssetService } from '@app/core/http/asset.service'; +import { AssetComponent } from '@modules/home/pages/asset/asset.component'; +import { AssetTableHeaderComponent } from '@modules/home/pages/asset/asset-table-header.component'; +import { AssetId } from '@app/shared/models/id/asset-id'; +import { AssetTabsComponent } from '@home/pages/asset/asset-tabs.component'; +import { HomeDialogsService } from '@home/dialogs/home-dialogs.service'; +import { DeviceInfo } from '@shared/models/device.models'; + +@Injectable() +export class AssetsTableConfigResolver implements Resolve> { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + private customerId: string; + + constructor(private store: Store, + private broadcast: BroadcastService, + private assetService: AssetService, + private customerService: CustomerService, + private dialogService: DialogService, + private homeDialogs: HomeDialogsService, + private translate: TranslateService, + private datePipe: DatePipe, + private router: Router, + private dialog: MatDialog) { + + this.config.entityType = EntityType.ASSET; + this.config.entityComponent = AssetComponent; + this.config.entityTabsComponent = AssetTabsComponent; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.ASSET); + this.config.entityResources = entityTypeResources.get(EntityType.ASSET); + + this.config.deleteEntityTitle = asset => this.translate.instant('asset.delete-asset-title', { assetName: asset.name }); + this.config.deleteEntityContent = () => this.translate.instant('asset.delete-asset-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('asset.delete-assets-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('asset.delete-assets-text'); + + this.config.loadEntity = id => this.assetService.getAssetInfo(id.id); + this.config.saveEntity = asset => { + return this.assetService.saveAsset(asset).pipe( + tap(() => { + this.broadcast.broadcast('assetSaved'); + }), + mergeMap((savedAsset) => this.assetService.getAssetInfo(savedAsset.id.id) + )); + }; + this.config.onEntityAction = action => this.onAssetAction(action); + this.config.detailsReadonly = () => this.config.componentsData.assetScope === 'customer_user'; + + this.config.headerComponent = AssetTableHeaderComponent; + + } + + resolve(route: ActivatedRouteSnapshot): Observable> { + const routeParams = route.params; + this.config.componentsData = { + assetScope: route.data.assetsType, + assetType: '' + }; + this.customerId = routeParams.customerId; + return this.store.pipe(select(selectAuthUser), take(1)).pipe( + tap((authUser) => { + if (authUser.authority === Authority.CUSTOMER_USER) { + this.config.componentsData.assetScope = 'customer_user'; + this.customerId = authUser.customerId; + } + }), + mergeMap(() => + this.customerId ? this.customerService.getCustomer(this.customerId) : of(null as Customer) + ), + map((parentCustomer) => { + if (parentCustomer) { + if (parentCustomer.additionalInfo && parentCustomer.additionalInfo.isPublic) { + this.config.tableTitle = this.translate.instant('customer.public-assets'); + } else { + this.config.tableTitle = parentCustomer.title + ': ' + this.translate.instant('asset.assets'); + } + } else { + this.config.tableTitle = this.translate.instant('asset.assets'); + } + this.config.columns = this.configureColumns(this.config.componentsData.assetScope); + this.configureEntityFunctions(this.config.componentsData.assetScope); + this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.assetScope); + this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.assetScope); + this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.assetScope); + this.config.addEnabled = this.config.componentsData.assetScope !== 'customer_user'; + this.config.entitiesDeleteEnabled = this.config.componentsData.assetScope === 'tenant'; + this.config.deleteEnabled = () => this.config.componentsData.assetScope === 'tenant'; + return this.config; + }) + ); + } + + configureColumns(assetScope: string): Array> { + const columns: Array> = [ + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), + new EntityTableColumn('name', 'asset.name', '25%'), + new EntityTableColumn('type', 'asset.asset-type', '25%'), + new EntityTableColumn('label', 'asset.label', '25%'), + ]; + if (assetScope === 'tenant') { + columns.push( + new EntityTableColumn('customerTitle', 'customer.customer', '25%'), + new EntityTableColumn('customerIsPublic', 'asset.public', '60px', + entity => { + return checkBoxCell(entity.customerIsPublic); + }, () => ({}), false), + ); + } + return columns; + } + + configureEntityFunctions(assetScope: string): void { + if (assetScope === 'tenant') { + this.config.entitiesFetchFunction = pageLink => + this.assetService.getTenantAssetInfos(pageLink, this.config.componentsData.assetType); + this.config.deleteEntity = id => this.assetService.deleteAsset(id.id); + } else { + this.config.entitiesFetchFunction = pageLink => + this.assetService.getCustomerAssetInfos(this.customerId, pageLink, this.config.componentsData.assetType); + this.config.deleteEntity = id => this.assetService.unassignAssetFromCustomer(id.id); + } + } + + configureCellActions(assetScope: string): Array> { + const actions: Array> = []; + if (assetScope === 'tenant') { + actions.push( + { + name: this.translate.instant('asset.make-public'), + icon: 'share', + isEnabled: (entity) => (!entity.customerId || entity.customerId.id === NULL_UUID), + onAction: ($event, entity) => this.makePublic($event, entity) + }, + { + name: this.translate.instant('asset.assign-to-customer'), + icon: 'assignment_ind', + isEnabled: (entity) => (!entity.customerId || entity.customerId.id === NULL_UUID), + onAction: ($event, entity) => this.assignToCustomer($event, [entity.id]) + }, + { + name: this.translate.instant('asset.unassign-from-customer'), + icon: 'assignment_return', + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && !entity.customerIsPublic), + onAction: ($event, entity) => this.unassignFromCustomer($event, entity) + }, + { + name: this.translate.instant('asset.make-private'), + icon: 'reply', + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && entity.customerIsPublic), + onAction: ($event, entity) => this.unassignFromCustomer($event, entity) + } + ); + } + if (assetScope === 'customer') { + actions.push( + { + name: this.translate.instant('asset.unassign-from-customer'), + icon: 'assignment_return', + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && !entity.customerIsPublic), + onAction: ($event, entity) => this.unassignFromCustomer($event, entity) + }, + { + name: this.translate.instant('asset.make-private'), + icon: 'reply', + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && entity.customerIsPublic), + onAction: ($event, entity) => this.unassignFromCustomer($event, entity) + } + ); + } + return actions; + } + + configureGroupActions(assetScope: string): Array> { + const actions: Array> = []; + if (assetScope === 'tenant') { + actions.push( + { + name: this.translate.instant('asset.assign-assets'), + icon: 'assignment_ind', + isEnabled: true, + onAction: ($event, entities) => this.assignToCustomer($event, entities.map((entity) => entity.id)) + } + ); + } + if (assetScope === 'customer') { + actions.push( + { + name: this.translate.instant('asset.unassign-assets'), + icon: 'assignment_return', + isEnabled: true, + onAction: ($event, entities) => this.unassignAssetsFromCustomer($event, entities) + } + ); + } + return actions; + } + + configureAddActions(assetScope: string): Array { + const actions: Array = []; + if (assetScope === 'tenant') { + actions.push( + { + name: this.translate.instant('asset.add-asset-text'), + icon: 'insert_drive_file', + isEnabled: () => true, + onAction: ($event) => this.config.table.addEntity($event) + }, + { + name: this.translate.instant('asset.import'), + icon: 'file_upload', + isEnabled: () => true, + onAction: ($event) => this.importAssets($event) + } + ); + } + if (assetScope === 'customer') { + actions.push( + { + name: this.translate.instant('asset.assign-new-asset'), + icon: 'add', + isEnabled: () => true, + onAction: ($event) => this.addAssetsToCustomer($event) + } + ); + } + return actions; + } + + importAssets($event: Event) { + this.homeDialogs.importEntities(EntityType.ASSET).subscribe((res) => { + if (res) { + this.broadcast.broadcast('assetSaved'); + this.config.table.updateData(); + } + }); + } + + addAssetsToCustomer($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(AddEntitiesToCustomerDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + customerId: this.customerId, + entityType: EntityType.ASSET + } + }).afterClosed() + .subscribe((res) => { + if (res) { + this.config.table.updateData(); + } + }); + } + + makePublic($event: Event, asset: Asset) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('asset.make-public-asset-title', {assetName: asset.name}), + this.translate.instant('asset.make-public-asset-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + this.assetService.makeAssetPublic(asset.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + assignToCustomer($event: Event, assetIds: Array) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(AssignToCustomerDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + entityIds: assetIds, + entityType: EntityType.ASSET + } + }).afterClosed() + .subscribe((res) => { + if (res) { + this.config.table.updateData(); + } + }); + } + + unassignFromCustomer($event: Event, asset: AssetInfo) { + if ($event) { + $event.stopPropagation(); + } + const isPublic = asset.customerIsPublic; + let title; + let content; + if (isPublic) { + title = this.translate.instant('asset.make-private-asset-title', {assetName: asset.name}); + content = this.translate.instant('asset.make-private-asset-text'); + } else { + title = this.translate.instant('asset.unassign-asset-title', {assetName: asset.name}); + content = this.translate.instant('asset.unassign-asset-text'); + } + this.dialogService.confirm( + title, + content, + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + this.assetService.unassignAssetFromCustomer(asset.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + unassignAssetsFromCustomer($event: Event, assets: Array) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('asset.unassign-assets-title', {count: assets.length}), + this.translate.instant('asset.unassign-assets-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + const tasks: Observable[] = []; + assets.forEach( + (asset) => { + tasks.push(this.assetService.unassignAssetFromCustomer(asset.id.id)); + } + ); + forkJoin(tasks).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + onAssetAction(action: EntityAction): boolean { + switch (action.action) { + case 'makePublic': + this.makePublic(action.event, action.entity); + return true; + case 'assignToCustomer': + this.assignToCustomer(action.event, [action.entity.id]); + return true; + case 'unassignFromCustomer': + this.unassignFromCustomer(action.event, action.entity); + return true; + } + return false; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/audit-log/audit-log-routing.module.ts b/ui-ngx/src/app/modules/home/pages/audit-log/audit-log-routing.module.ts new file mode 100644 index 0000000000..468dc5184d --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/audit-log/audit-log-routing.module.ts @@ -0,0 +1,41 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { Authority } from '@shared/models/authority.enum'; +import { AuditLogTableComponent } from '@home/components/audit-log/audit-log-table.component'; + +const routes: Routes = [ + { + path: 'auditLogs', + component: AuditLogTableComponent, + data: { + auth: [Authority.TENANT_ADMIN], + title: 'audit-log.audit-logs', + breadcrumb: { + label: 'audit-log.audit-logs', + icon: 'track_changes' + } + } + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class AuditLogRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/audit-log/audit-log.module.ts b/ui-ngx/src/app/modules/home/pages/audit-log/audit-log.module.ts new file mode 100644 index 0000000000..5c48ebda3f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/audit-log/audit-log.module.ts @@ -0,0 +1,31 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { AuditLogRoutingModule } from '@modules/home/pages/audit-log/audit-log-routing.module'; + +@NgModule({ + declarations: [ + ], + imports: [ + CommonModule, + SharedModule, + AuditLogRoutingModule + ] +}) +export class AuditLogModule { } diff --git a/ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts b/ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts new file mode 100644 index 0000000000..f3ec436a1e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts @@ -0,0 +1,149 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { EntitiesTableComponent } from '../../components/entity/entities-table.component'; +import { Authority } from '@shared/models/authority.enum'; +import { UsersTableConfigResolver } from '../user/users-table-config.resolver'; +import { CustomersTableConfigResolver } from './customers-table-config.resolver'; +import { DevicesTableConfigResolver } from '@modules/home/pages/device/devices-table-config.resolver'; +import { AssetsTableConfigResolver } from '../asset/assets-table-config.resolver'; +import { DashboardsTableConfigResolver } from '@modules/home/pages/dashboard/dashboards-table-config.resolver'; +import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.component'; +import { BreadCrumbConfig } from '@shared/components/breadcrumb'; +import { dashboardBreadcumbLabelFunction, DashboardResolver } from '@home/pages/dashboard/dashboard-routing.module'; + +const routes: Routes = [ + { + path: 'customers', + data: { + breadcrumb: { + label: 'customer.customers', + icon: 'supervisor_account' + } + }, + children: [ + { + path: '', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN], + title: 'customer.customers' + }, + resolve: { + entitiesTableConfig: CustomersTableConfigResolver + } + }, + { + path: ':customerId/users', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN], + title: 'user.customer-users', + breadcrumb: { + label: 'user.customer-users', + icon: 'account_circle' + } + }, + resolve: { + entitiesTableConfig: UsersTableConfigResolver + } + }, + { + path: ':customerId/devices', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN], + title: 'customer.devices', + devicesType: 'customer', + breadcrumb: { + label: 'customer.devices', + icon: 'devices_other' + } + }, + resolve: { + entitiesTableConfig: DevicesTableConfigResolver + } + }, + { + path: ':customerId/assets', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN], + title: 'customer.assets', + assetsType: 'customer', + breadcrumb: { + label: 'customer.assets', + icon: 'domain' + } + }, + resolve: { + entitiesTableConfig: AssetsTableConfigResolver + } + }, + { + path: ':customerId/dashboards', + data: { + breadcrumb: { + label: 'customer.dashboards', + icon: 'dashboard' + } + }, + children: [ + { + path: '', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN], + title: 'customer.dashboards', + dashboardsType: 'customer' + }, + resolve: { + entitiesTableConfig: DashboardsTableConfigResolver + } + }, + { + path: ':dashboardId', + component: DashboardPageComponent, + data: { + breadcrumb: { + labelFunction: dashboardBreadcumbLabelFunction, + icon: 'dashboard' + } as BreadCrumbConfig, + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + title: 'customer.dashboard', + widgetEditMode: false + }, + resolve: { + dashboard: DashboardResolver + } + } + ] + } + ] + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [ + CustomersTableConfigResolver + ] +}) +export class CustomerRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/customer/customer-tabs.component.html b/ui-ngx/src/app/modules/home/pages/customer/customer-tabs.component.html new file mode 100644 index 0000000000..caa1d929cc --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/customer/customer-tabs.component.html @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/app/modules/home/pages/customer/customer-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/customer/customer-tabs.component.ts new file mode 100644 index 0000000000..bb6e481eae --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/customer/customer-tabs.component.ts @@ -0,0 +1,38 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; +import { Customer } from '@shared/models/customer.model'; + +@Component({ + selector: 'tb-customer-tabs', + templateUrl: './customer-tabs.component.html', + styleUrls: [] +}) +export class CustomerTabsComponent extends EntityTabsComponent { + + constructor(protected store: Store) { + super(store); + } + + ngOnInit() { + super.ngOnInit(); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/customer/customer.component.html b/ui-ngx/src/app/modules/home/pages/customer/customer.component.html new file mode 100644 index 0000000000..bb75d81fbd --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/customer/customer.component.html @@ -0,0 +1,79 @@ + +
+ + + + + +
+ +
+
+
+
+
+ + customer.title + + + {{ 'customer.title-required' | translate }} + + +
+ + customer.description + + +
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/customer/customer.component.ts b/ui-ngx/src/app/modules/home/pages/customer/customer.component.ts new file mode 100644 index 0000000000..5d58d2be2c --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/customer/customer.component.ts @@ -0,0 +1,81 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Customer } from '@shared/models/customer.model'; +import { ActionNotificationShow } from '@app/core/notification/notification.actions'; +import { TranslateService } from '@ngx-translate/core'; +import { ContactBasedComponent } from '../../components/entity/contact-based.component'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; + +@Component({ + selector: 'tb-customer', + templateUrl: './customer.component.html' +}) +export class CustomerComponent extends ContactBasedComponent { + + isPublic = false; + + constructor(protected store: Store, + protected translate: TranslateService, + @Inject('entity') protected entityValue: Customer, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + protected fb: FormBuilder) { + super(store, fb, entityValue, entitiesTableConfigValue); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + buildEntityForm(entity: Customer): FormGroup { + return this.fb.group( + { + title: [entity ? entity.title : '', [Validators.required]], + additionalInfo: this.fb.group( + { + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''] + } + ) + } + ); + } + + updateEntityForm(entity: Customer) { + this.isPublic = entity.additionalInfo && entity.additionalInfo.isPublic; + this.entityForm.patchValue({title: entity.title}); + this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}}); + } + + onCustomerIdCopied(event) { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('customer.idCopiedMessage'), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/customer/customer.module.ts b/ui-ngx/src/app/modules/home/pages/customer/customer.module.ts new file mode 100644 index 0000000000..2cdbbcf7c1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/customer/customer.module.ts @@ -0,0 +1,37 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { CustomerComponent } from '@modules/home/pages/customer/customer.component'; +import { CustomerRoutingModule } from './customer-routing.module'; +import { HomeComponentsModule } from '@modules/home/components/home-components.module'; +import { CustomerTabsComponent } from '@home/pages/customer/customer-tabs.component'; + +@NgModule({ + declarations: [ + CustomerComponent, + CustomerTabsComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + CustomerRoutingModule + ] +}) +export class CustomerModule { } diff --git a/ui-ngx/src/app/modules/home/pages/customer/customers-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/customer/customers-table-config.resolver.ts new file mode 100644 index 0000000000..2e0b0abe14 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/customer/customers-table-config.resolver.ts @@ -0,0 +1,168 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; + +import { Resolve, Router } from '@angular/router'; + +import { + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { Customer } from '@app/shared/models/customer.model'; +import { CustomerService } from '@app/core/http/customer.service'; +import { CustomerComponent } from '@modules/home/pages/customer/customer.component'; +import { CustomerTabsComponent } from '@home/pages/customer/customer-tabs.component'; + +@Injectable() +export class CustomersTableConfigResolver implements Resolve> { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor(private customerService: CustomerService, + private translate: TranslateService, + private datePipe: DatePipe, + private router: Router) { + + this.config.entityType = EntityType.CUSTOMER; + this.config.entityComponent = CustomerComponent; + this.config.entityTabsComponent = CustomerTabsComponent; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.CUSTOMER); + this.config.entityResources = entityTypeResources.get(EntityType.CUSTOMER); + + this.config.columns.push( + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), + new EntityTableColumn('title', 'customer.title', '25%'), + new EntityTableColumn('email', 'contact.email', '25%'), + new EntityTableColumn('country', 'contact.country', '25%'), + new EntityTableColumn('city', 'contact.city', '25%') + ); + + this.config.cellActionDescriptors.push( + { + name: this.translate.instant('customer.manage-customer-users'), + icon: 'account_circle', + isEnabled: (customer) => !customer.additionalInfo || !customer.additionalInfo.isPublic, + onAction: ($event, entity) => this.manageCustomerUsers($event, entity) + }, + { + name: this.translate.instant('customer.manage-customer-assets'), + nameFunction: (customer) => { + return customer.additionalInfo && customer.additionalInfo.isPublic + ? this.translate.instant('customer.manage-public-assets') + : this.translate.instant('customer.manage-customer-assets'); + }, + icon: 'domain', + isEnabled: (customer) => true, + onAction: ($event, entity) => this.manageCustomerAssets($event, entity) + }, + { + name: this.translate.instant('customer.manage-customer-devices'), + nameFunction: (customer) => { + return customer.additionalInfo && customer.additionalInfo.isPublic + ? this.translate.instant('customer.manage-public-devices') + : this.translate.instant('customer.manage-customer-devices'); + }, + icon: 'devices_other', + isEnabled: (customer) => true, + onAction: ($event, entity) => this.manageCustomerDevices($event, entity) + }, + { + name: this.translate.instant('customer.manage-customer-dashboards'), + nameFunction: (customer) => { + return customer.additionalInfo && customer.additionalInfo.isPublic + ? this.translate.instant('customer.manage-public-dashboards') + : this.translate.instant('customer.manage-customer-dashboards'); + }, + icon: 'dashboard', + isEnabled: (customer) => true, + onAction: ($event, entity) => this.manageCustomerDashboards($event, entity) + } + ); + + this.config.deleteEntityTitle = customer => this.translate.instant('customer.delete-customer-title', { customerTitle: customer.title }); + this.config.deleteEntityContent = () => this.translate.instant('customer.delete-customer-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('customer.delete-customers-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('customer.delete-customers-text'); + + this.config.entitiesFetchFunction = pageLink => this.customerService.getCustomers(pageLink); + this.config.loadEntity = id => this.customerService.getCustomer(id.id); + this.config.saveEntity = customer => this.customerService.saveCustomer(customer); + this.config.deleteEntity = id => this.customerService.deleteCustomer(id.id); + this.config.onEntityAction = action => this.onCustomerAction(action); + this.config.deleteEnabled = (customer) => customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic); + this.config.entitySelectionEnabled = (customer) => customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic); + this.config.detailsReadonly = (customer) => customer && customer.additionalInfo && customer.additionalInfo.isPublic; + } + + resolve(): EntityTableConfig { + this.config.tableTitle = this.translate.instant('customer.customers'); + + return this.config; + } + + manageCustomerUsers($event: Event, customer: Customer) { + if ($event) { + $event.stopPropagation(); + } + this.router.navigateByUrl(`customers/${customer.id.id}/users`); + } + + manageCustomerAssets($event: Event, customer: Customer) { + if ($event) { + $event.stopPropagation(); + } + this.router.navigateByUrl(`customers/${customer.id.id}/assets`); + } + + manageCustomerDevices($event: Event, customer: Customer) { + if ($event) { + $event.stopPropagation(); + } + this.router.navigateByUrl(`customers/${customer.id.id}/devices`); + } + + manageCustomerDashboards($event: Event, customer: Customer) { + if ($event) { + $event.stopPropagation(); + } + this.router.navigateByUrl(`customers/${customer.id.id}/dashboards`); + } + + onCustomerAction(action: EntityAction): boolean { + switch (action.action) { + case 'manageUsers': + this.manageCustomerUsers(action.event, action.entity); + return true; + case 'manageAssets': + this.manageCustomerAssets(action.event, action.entity); + return true; + case 'manageDevices': + this.manageCustomerDevices(action.event, action.entity); + return true; + case 'manageDashboards': + this.manageCustomerDashboards(action.event, action.entity); + return true; + } + return false; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/add-widget-dialog.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/add-widget-dialog.component.html new file mode 100644 index 0000000000..fb0d17e73a --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/add-widget-dialog.component.html @@ -0,0 +1,57 @@ + +
+ +

widget.add

+ +
+ +
+ + +
+
+ + +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/add-widget-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/add-widget-dialog.component.ts new file mode 100644 index 0000000000..fb0319a359 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/add-widget-dialog.component.ts @@ -0,0 +1,132 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { Widget, widgetTypesData } from '@shared/models/widget.models'; +import { Dashboard } from '@app/shared/models/dashboard.models'; +import { IAliasController } from '@core/api/widget-api.models'; +import { WidgetConfigComponentData, WidgetInfo } from '@home/models/widget-component.models'; +import { isDefined, isString } from '@core/utils'; + +export interface AddWidgetDialogData { + dashboard: Dashboard; + aliasController: IAliasController; + widget: Widget; + widgetInfo: WidgetInfo; +} + +@Component({ + selector: 'tb-add-widget-dialog', + templateUrl: './add-widget-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: AddWidgetDialogComponent}], + styleUrls: [] +}) +export class AddWidgetDialogComponent extends DialogComponent + implements OnInit, ErrorStateMatcher { + + widgetFormGroup: FormGroup; + + dashboard: Dashboard; + aliasController: IAliasController; + widget: Widget; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AddWidgetDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private fb: FormBuilder) { + super(store, router, dialogRef); + + this.dashboard = this.data.dashboard; + this.aliasController = this.data.aliasController; + this.widget = this.data.widget; + + const widgetInfo = this.data.widgetInfo; + + const rawSettingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema; + const rawDataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema; + const typeParameters = widgetInfo.typeParameters; + const actionSources = widgetInfo.actionSources; + const isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true; + let settingsSchema; + if (!rawSettingsSchema || rawSettingsSchema === '') { + settingsSchema = {}; + } else { + settingsSchema = isString(rawSettingsSchema) ? JSON.parse(rawSettingsSchema) : rawSettingsSchema; + } + let dataKeySettingsSchema; + if (!rawDataKeySettingsSchema || rawDataKeySettingsSchema === '') { + dataKeySettingsSchema = {}; + } else { + dataKeySettingsSchema = isString(rawDataKeySettingsSchema) ? JSON.parse(rawDataKeySettingsSchema) : rawDataKeySettingsSchema; + } + const widgetConfig: WidgetConfigComponentData = { + config: this.widget.config, + layout: {}, + widgetType: this.widget.type, + typeParameters, + actionSources, + isDataEnabled, + settingsSchema, + dataKeySettingsSchema + }; + + this.widgetFormGroup = this.fb.group({ + widgetConfig: [widgetConfig, []] + } + ); + } + + ngOnInit(): void { + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + helpLinkIdForWidgetType(): string { + let link = 'widgetsConfig'; + if (this.widget && this.widget.type) { + link = widgetTypesData.get(this.widget.type).configHelpLinkId; + } + return link; + } + + cancel(): void { + this.dialogRef.close(null); + } + + add(): void { + this.submitted = true; + const widgetConfig: WidgetConfigComponentData = this.widgetFormGroup.get('widgetConfig').value; + this.widget.config = widgetConfig.config; + this.widget.config.mobileOrder = widgetConfig.layout.mobileOrder; + this.widget.config.mobileHeight = widgetConfig.layout.mobileHeight; + this.dialogRef.close(this.widget); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html new file mode 100644 index 0000000000..4c8376e218 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html @@ -0,0 +1,110 @@ + +
+ + + + + + + +
+
+ + dashboard.assignedToCustomers + + +
+ + +
+ + dashboard.public-link + + + +
+
+
+
+ + dashboard.title + + + {{ 'dashboard.title-required' | translate }} + + +
+ + dashboard.description + + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.scss b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.scss new file mode 100644 index 0000000000..bb3718c2da --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.scss @@ -0,0 +1,18 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts new file mode 100644 index 0000000000..ee72b33453 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts @@ -0,0 +1,121 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityComponent } from '../../components/entity/entity.component'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { TranslateService } from '@ngx-translate/core'; +import { + Dashboard, + getDashboardAssignedCustomersText, + isCurrentPublicDashboardCustomer, + isPublicDashboard +} from '@shared/models/dashboard.models'; +import { DashboardService } from '@core/http/dashboard.service'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import { isEqual } from '@core/utils'; + +@Component({ + selector: 'tb-dashboard-form', + templateUrl: './dashboard-form.component.html', + styleUrls: ['./dashboard-form.component.scss'] +}) +export class DashboardFormComponent extends EntityComponent { + + dashboardScope: 'tenant' | 'customer' | 'customer_user'; + customerId: string; + + publicLink: string; + assignedCustomersText: string; + + constructor(protected store: Store, + protected translate: TranslateService, + private dashboardService: DashboardService, + @Inject('entity') protected entityValue: Dashboard, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + public fb: FormBuilder) { + super(store, fb, entityValue, entitiesTableConfigValue); + } + + ngOnInit() { + this.dashboardScope = this.entitiesTableConfig.componentsData.dashboardScope; + this.customerId = this.entitiesTableConfig.componentsData.customerId; + super.ngOnInit(); + } + + isPublic(entity: Dashboard): boolean { + return isPublicDashboard(entity); + } + + isCurrentPublicCustomer(entity: Dashboard): boolean { + return isCurrentPublicDashboardCustomer(entity, this.customerId); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + buildForm(entity: Dashboard): FormGroup { + this.updateFields(entity); + return this.fb.group( + { + title: [entity ? entity.title : '', [Validators.required]], + configuration: this.fb.group( + { + description: [entity && entity.configuration ? entity.configuration.description : ''], + } + ) + } + ); + } + + updateForm(entity: Dashboard) { + this.updateFields(entity); + this.entityForm.patchValue({title: entity.title}); + this.entityForm.patchValue({configuration: {description: entity.configuration ? entity.configuration.description : ''}}); + } + + prepareFormValue(formValue: any): any { + const preparedValue = super.prepareFormValue(formValue); + preparedValue.configuration = {...(this.entity.configuration || {}), ...(preparedValue.configuration || {})}; + return preparedValue; + } + + onPublicLinkCopied($event) { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('dashboard.public-link-copied-message'), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } + + private updateFields(entity: Dashboard): void { + if (entity && !isEqual(entity, {})) { + this.assignedCustomersText = getDashboardAssignedCustomersText(entity); + this.publicLink = this.dashboardService.getPublicDashboardLink(entity); + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html new file mode 100644 index 0000000000..9f46f6b219 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html @@ -0,0 +1,268 @@ + +
+ +
+ +
+
+ + +
+ + +
+ + +
+
+ + + + + + + + + + + + + + + + + +
+
+
+
+
+ + +
+

{{ dashboard.title }}

+ + dashboard.title + + +
+ + + + + + + + + + + + +
+ + +
+
+
+ + +
+ +
+
+ {{ 'widgets-bundle.current' | translate }} + + +
+
+ + +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.scss b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.scss new file mode 100644 index 0000000000..19c35876f1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.scss @@ -0,0 +1,141 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../../../scss/constants"; + +$toolbar-height: 50px !default; +$fullscreen-toolbar-height: 64px !default; +$mobile-toolbar-height: 84px !default; + +tb-dashboard-page { + display: flex; + width: 100%; + height: 100%; +} + +div.tb-dashboard-page { + &.mat-content { + overflow: hidden; + background-color: #eee; + } + section.tb-dashboard-title { + position: relative; + padding-left: 20px; + max-height: 60px; + mat-form-field { + .mat-form-field-infix { + width: 100%; + } + } + input.tb-dashboard-title { + height: 38px; + font-size: 2rem; + font-weight: 500; + letter-spacing: .005em; + } + } + + section.tb-dashboard-toolbar { + position: absolute; + top: 0; + left: 0; + z-index: 13; + pointer-events: none; + + &.tb-dashboard-toolbar-opened { + right: 0; + // transition: right .3s cubic-bezier(.55, 0, .55, .2); + } + + &.tb-dashboard-toolbar-closed { + right: 18px; + transition: right .3s cubic-bezier(.55, 0, .55, .2) .2s; + } + } + + .tb-dashboard-container { + &.tb-dashboard-toolbar-opened { + &.is-fullscreen { + margin-top: $mobile-toolbar-height; + + @media #{$mat-gt-sm} { + margin-top: $fullscreen-toolbar-height; + } + } + + &:not(.is-fullscreen) { + margin-top: $mobile-toolbar-height; + + @media #{$mat-gt-sm} { + margin-top: $toolbar-height; + } + &.tb-dashboard-toolbar-animated { + transition: margin-top .3s cubic-bezier(.55, 0, .55, .2); + } + } + } + + &.tb-dashboard-toolbar-closed { + margin-top: 0; + + &.tb-dashboard-toolbar-animated { + transition: margin-top .3s cubic-bezier(.55, 0, .55, .2) .2s; + } + } + } + + mat-drawer-container.tb-dashboard-drawer-container { + mat-drawer-container.tb-dashboard-layouts { + width: 100%; + &.tb-shrinked { + width: 40%; + } + } + + mat-drawer.tb-details-drawer { + @media #{$mat-gt-sm} { + width: 85% !important; + } + + @media #{$mat-gt-md} { + width: 75% !important; + } + + @media #{$mat-gt-xl} { + width: 60% !important; + } + } + } + + section.tb-powered-by-footer { + position: absolute; + right: 25px; + bottom: 5px; + z-index: 30; + pointer-events: none; + + span { + font-size: 12px; + + a { + font-weight: 700; + text-decoration: none; + pointer-events: all; + border: none; + } + } + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts new file mode 100644 index 0000000000..4658100be1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts @@ -0,0 +1,1083 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, Inject, NgZone, OnDestroy, ViewChild, ViewEncapsulation } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ActivatedRoute, Router } from '@angular/router'; +import { UtilsService } from '@core/services/utils.service'; +import { AuthService } from '@core/auth/auth.service'; +import { + Dashboard, + DashboardConfiguration, + DashboardLayoutId, + DashboardLayoutInfo, + DashboardLayoutsInfo, + DashboardState, + DashboardStateLayouts, + GridSettings, + WidgetLayout +} from '@app/shared/models/dashboard.models'; +import { WINDOW } from '@core/services/window.service'; +import { WindowMessage } from '@shared/models/window-message.model'; +import { deepClone, isDefined } from '@app/core/utils'; +import { + DashboardContext, + DashboardPageLayout, + DashboardPageLayoutContext, + DashboardPageLayouts, + DashboardPageScope, + IDashboardController, + LayoutWidgetsArray +} from './dashboard-page.models'; +import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; +import { MediaBreakpoints } from '@shared/models/constants'; +import { AuthUser } from '@shared/models/user.model'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { Widget, WidgetConfig, WidgetPosition, widgetTypesData } from '@app/shared/models/widget.models'; +import { environment as env } from '@env/environment'; +import { Authority } from '@shared/models/authority.enum'; +import { DialogService } from '@core/services/dialog.service'; +import { EntityService } from '@core/http/entity.service'; +import { AliasController } from '@core/api/alias-controller'; +import { Observable, of, Subscription } from 'rxjs'; +import { FooterFabButtons } from '@shared/components/footer-fab-buttons.component'; +import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; +import { DashboardService } from '@core/http/dashboard.service'; +import { DashboardContextMenuItem, WidgetContextMenuItem } from '../../models/dashboard-component.models'; +import { WidgetComponentService } from '../../components/widget/widget-component.service'; +import { FormBuilder } from '@angular/forms'; +import { ItemBufferService } from '@core/services/item-buffer.service'; +import { MatDialog } from '@angular/material/dialog'; +import { + EntityAliasesDialogComponent, + EntityAliasesDialogData +} from '@home/components/alias/entity-aliases-dialog.component'; +import { EntityAliases } from '@app/shared/models/alias.models'; +import { EditWidgetComponent } from '@home/pages/dashboard/edit-widget.component'; +import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; +import { AddWidgetDialogComponent, AddWidgetDialogData } from '@home/pages/dashboard/add-widget-dialog.component'; +import { TranslateService } from '@ngx-translate/core'; +import { + ManageDashboardLayoutsDialogComponent, + ManageDashboardLayoutsDialogData +} from '@home/pages/dashboard/layout/manage-dashboard-layouts-dialog.component'; +import { SelectTargetLayoutDialogComponent } from '@home/components/dashboard/select-target-layout-dialog.component'; +import { + DashboardSettingsDialogComponent, + DashboardSettingsDialogData +} from '@home/pages/dashboard/dashboard-settings-dialog.component'; +import { + ManageDashboardStatesDialogComponent, + ManageDashboardStatesDialogData +} from '@home/pages/dashboard/states/manage-dashboard-states-dialog.component'; +import { ImportExportService } from '@home/components/import-export/import-export.service'; +import { AuthState } from '@app/core/auth/auth.models'; +import { FiltersDialogComponent, FiltersDialogData } from '@home/components/filter/filters-dialog.component'; +import { Filters } from '@shared/models/query/query.models'; + +// @dynamic +@Component({ + selector: 'tb-dashboard-page', + templateUrl: './dashboard-page.component.html', + styleUrls: ['./dashboard-page.component.scss'], + encapsulation: ViewEncapsulation.None, + // changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DashboardPageComponent extends PageComponent implements IDashboardController, OnDestroy { + + authState: AuthState = getCurrentAuthState(this.store); + + authUser: AuthUser = this.authState.authUser; + + dashboard: Dashboard; + dashboardConfiguration: DashboardConfiguration; + + prevDashboard: Dashboard; + + iframeMode = this.utils.iframeMode; + widgetEditMode: boolean; + singlePageMode: boolean; + forceFullscreen = this.authState.forceFullscreen; + + isFullscreen = false; + isEdit = false; + isEditingWidget = false; + isEditingWidgetClosed = true; + isMobile = !this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']); + forceDashboardMobileMode = false; + isAddingWidget = false; + isAddingWidgetClosed = true; + widgetsBundle: WidgetsBundle = null; + + isToolbarOpened = false; + isToolbarOpenedAnimate = false; + isRightLayoutOpened = false; + + editingWidget: Widget = null; + editingWidgetLayout: WidgetLayout = null; + editingWidgetOriginal: Widget = null; + editingWidgetLayoutOriginal: WidgetLayout = null; + editingWidgetSubtitle: string = null; + editingLayoutCtx: DashboardPageLayoutContext = null; + + thingsboardVersion: string = env.tbVersion; + + currentDashboardId: string; + currentCustomerId: string; + currentDashboardScope: DashboardPageScope; + + addingLayoutCtx: DashboardPageLayoutContext; + + + dashboardCtx: DashboardContext = { + getDashboard: () => this.dashboard, + dashboardTimewindow: null, + state: null, + stateController: null, + aliasController: null, + runChangeDetection: this.runChangeDetection.bind(this) + }; + + layouts: DashboardPageLayouts = { + main: { + show: false, + layoutCtx: { + id: 'main', + widgets: null, + widgetLayouts: {}, + gridSettings: {}, + ignoreLoading: true, + ctrl: null, + dashboardCtrl: this + } + }, + right: { + show: false, + layoutCtx: { + id: 'right', + widgets: null, + widgetLayouts: {}, + gridSettings: {}, + ignoreLoading: true, + ctrl: null, + dashboardCtrl: this + } + } + }; + + addWidgetFabButtons: FooterFabButtons = { + fabTogglerName: 'dashboard.add-widget', + fabTogglerIcon: 'add', + buttons: [ + { + name: 'dashboard.create-new-widget', + icon: 'insert_drive_file', + onAction: ($event) => { + this.addWidget($event); + } + }, + { + name: 'dashboard.import-widget', + icon: 'file_upload', + onAction: ($event) => { + this.importWidget($event); + } + } + ] + }; + + private rxSubscriptions = new Array(); + + get toolbarOpened(): boolean { + return !this.widgetEditMode && + (this.toolbarAlwaysOpen() || this.isToolbarOpened || this.isEdit || this.showRightLayoutSwitch()); + } + set toolbarOpened(toolbarOpened: boolean) { + } + + get rightLayoutOpened(): boolean { + return !this.isMobile || this.isRightLayoutOpened; + } + set rightLayoutOpened(rightLayoutOpened: boolean) { + } + + @ViewChild('tbEditWidget') editWidgetComponent: EditWidgetComponent; + + constructor(protected store: Store, + @Inject(WINDOW) private window: Window, + private breakpointObserver: BreakpointObserver, + private route: ActivatedRoute, + private router: Router, + private utils: UtilsService, + private dashboardUtils: DashboardUtilsService, + private authService: AuthService, + private entityService: EntityService, + private dialogService: DialogService, + private widgetComponentService: WidgetComponentService, + private dashboardService: DashboardService, + private itembuffer: ItemBufferService, + private importExport: ImportExportService, + private fb: FormBuilder, + private dialog: MatDialog, + private translate: TranslateService, + private ngZone: NgZone, + private cd: ChangeDetectorRef) { + super(store); + + this.rxSubscriptions.push(this.route.data.subscribe( + (data) => { + this.init(data); + this.runChangeDetection(); + } + )); + + this.rxSubscriptions.push(this.breakpointObserver + .observe(MediaBreakpoints['gt-sm']) + .subscribe((state: BreakpointState) => { + this.isMobile = !state.matches; + } + )); + } + + private init(data: any) { + + this.reset(); + + this.currentDashboardId = this.route.snapshot.params.dashboardId; + + if (this.route.snapshot.params.customerId) { + this.currentCustomerId = this.route.snapshot.params.customerId; + this.currentDashboardScope = 'customer'; + } else { + this.currentDashboardScope = this.authUser.authority === Authority.TENANT_ADMIN ? 'tenant' : 'customer'; + this.currentCustomerId = this.authUser.customerId; + } + + this.dashboard = data.dashboard; + this.dashboardConfiguration = this.dashboard.configuration; + this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow; + this.layouts.main.layoutCtx.widgets = new LayoutWidgetsArray(this.dashboardCtx); + this.layouts.right.layoutCtx.widgets = new LayoutWidgetsArray(this.dashboardCtx); + this.widgetEditMode = data.widgetEditMode; + this.singlePageMode = data.singlePageMode; + + this.dashboardCtx.aliasController = new AliasController(this.utils, + this.entityService, + () => this.dashboardCtx.stateController, + this.dashboardConfiguration.entityAliases, + this.dashboardConfiguration.filters); + + if (this.widgetEditMode) { + const message: WindowMessage = { + type: 'widgetEditModeInited' + }; + this.window.parent.postMessage(JSON.stringify(message), '*'); + } + } + + private reset() { + this.dashboard = null; + this.dashboardConfiguration = null; + this.prevDashboard = null; + + this.widgetEditMode = false; + this.singlePageMode = false; + + this.isFullscreen = false; + this.isEdit = false; + this.isEditingWidget = false; + this.isEditingWidgetClosed = true; + this.forceDashboardMobileMode = false; + this.isAddingWidget = false; + this.isAddingWidgetClosed = true; + this.widgetsBundle = null; + + this.isToolbarOpened = false; + this.isToolbarOpenedAnimate = false; + this.isRightLayoutOpened = false; + + this.editingWidget = null; + this.editingWidgetLayout = null; + this.editingWidgetOriginal = null; + this.editingWidgetLayoutOriginal = null; + this.editingWidgetSubtitle = null; + this.editingLayoutCtx = null; + + this.currentDashboardId = null; + this.currentCustomerId = null; + this.currentDashboardScope = null; + + this.dashboardCtx.state = null; + } + + ngOnDestroy(): void { + this.rxSubscriptions.forEach((subscription) => { + subscription.unsubscribe(); + }); + this.rxSubscriptions.length = 0; + } + + public runChangeDetection() { + /*setTimeout(() => { + this.cd.detectChanges(); + });*/ + } + + public openToolbar() { + this.isToolbarOpenedAnimate = true; + this.isToolbarOpened = true; + } + + public closeToolbar() { + this.isToolbarOpenedAnimate = true; + this.isToolbarOpened = false; + } + + public showCloseToolbar() { + return !this.toolbarAlwaysOpen() && !this.isEdit && !this.showRightLayoutSwitch(); + } + + public hideFullscreenButton(): boolean { + return this.widgetEditMode || this.iframeMode || this.forceFullscreen || this.singlePageMode; + } + + public toolbarAlwaysOpen(): boolean { + if (this.dashboard.configuration.settings && + isDefined(this.dashboard.configuration.settings.toolbarAlwaysOpen)) { + return this.dashboard.configuration.settings.toolbarAlwaysOpen; + } else { + return true; + } + } + + public displayTitle(): boolean { + if (this.dashboard.configuration.settings && + isDefined(this.dashboard.configuration.settings.showTitle)) { + return this.dashboard.configuration.settings.showTitle; + } else { + return false; + } + } + + public displayExport(): boolean { + if (this.dashboard.configuration.settings && + isDefined(this.dashboard.configuration.settings.showDashboardExport)) { + return this.dashboard.configuration.settings.showDashboardExport; + } else { + return true; + } + } + + public displayDashboardTimewindow(): boolean { + if (this.dashboard.configuration.settings && + isDefined(this.dashboard.configuration.settings.showDashboardTimewindow)) { + return this.dashboard.configuration.settings.showDashboardTimewindow; + } else { + return true; + } + } + + public displayDashboardsSelect(): boolean { + if (this.dashboard.configuration.settings && + isDefined(this.dashboard.configuration.settings.showDashboardsSelect)) { + return this.dashboard.configuration.settings.showDashboardsSelect; + } else { + return true; + } + } + + public displayEntitiesSelect(): boolean { + if (this.dashboard.configuration.settings && + isDefined(this.dashboard.configuration.settings.showEntitiesSelect)) { + return this.dashboard.configuration.settings.showEntitiesSelect; + } else { + return true; + } + } + + public displayFilters(): boolean { + if (this.dashboard.configuration.settings && + isDefined(this.dashboard.configuration.settings.showFilters)) { + return this.dashboard.configuration.settings.showFilters; + } else { + return true; + } + } + + public showRightLayoutSwitch(): boolean { + return this.isMobile && this.layouts.right.show; + } + + public toggleLayouts() { + this.isRightLayoutOpened = !this.isRightLayoutOpened; + } + + public openRightLayout() { + this.isRightLayoutOpened = true; + } + + public mainLayoutWidth(): string { + if (this.isEditingWidget && this.editingLayoutCtx.id === 'main') { + return '100%'; + } else { + return this.layouts.right.show && !this.isMobile ? '50%' : '100%'; + } + } + + public mainLayoutHeight(): string { + if (!this.isEditingWidget || this.editingLayoutCtx.id === 'main') { + return '100%'; + } else { + return '0px'; + } + } + + public rightLayoutWidth(): string { + if (this.isEditingWidget && this.editingLayoutCtx.id === 'right') { + return '100%'; + } else { + return this.isMobile ? '100%' : '50%'; + } + } + + public rightLayoutHeight(): string { + if (!this.isEditingWidget || this.editingLayoutCtx.id === 'right') { + return '100%'; + } else { + return '0px'; + } + } + + public isPublicUser(): boolean { + return this.authUser.isPublic; + } + + public isTenantAdmin(): boolean { + return this.authUser.authority === Authority.TENANT_ADMIN; + } + + public isSystemAdmin(): boolean { + return this.authUser.authority === Authority.SYS_ADMIN; + } + + public exportDashboard($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.importExport.exportDashboard(this.currentDashboardId); + } + + public openEntityAliases($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(EntityAliasesDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + entityAliases: deepClone(this.dashboard.configuration.entityAliases), + widgets: this.dashboardUtils.getWidgetsArray(this.dashboard), + isSingleEntityAlias: false + } + }).afterClosed().subscribe((entityAliases) => { + if (entityAliases) { + this.dashboard.configuration.entityAliases = entityAliases; + this.entityAliasesUpdated(); + } + }); + } + + public openFilters($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(FiltersDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + filters: deepClone(this.dashboard.configuration.filters), + widgets: this.dashboardUtils.getWidgetsArray(this.dashboard), + isSingleFilter: false + } + }).afterClosed().subscribe((filters) => { + if (filters) { + this.dashboard.configuration.filters = filters; + this.filtersUpdated(); + } + }); + } + + public openDashboardSettings($event: Event) { + if ($event) { + $event.stopPropagation(); + } + let gridSettings: GridSettings = null; + const layoutKeys = this.dashboardUtils.isSingleLayoutDashboard(this.dashboard); + if (layoutKeys) { + gridSettings = deepClone(this.dashboard.configuration.states[layoutKeys.state].layouts[layoutKeys.layout].gridSettings); + } + this.dialog.open(DashboardSettingsDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + settings: deepClone(this.dashboard.configuration.settings), + gridSettings + } + }).afterClosed().subscribe((data) => { + if (data) { + this.dashboard.configuration.settings = data.settings; + const newGridSettings = data.gridSettings; + if (newGridSettings) { + const layout = this.dashboard.configuration.states[layoutKeys.state].layouts[layoutKeys.layout]; + this.dashboardUtils.updateLayoutSettings(layout, newGridSettings); + this.updateLayouts(); + } + } + }); + } + + public manageDashboardStates($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(ManageDashboardStatesDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + states: deepClone(this.dashboard.configuration.states) + } + }).afterClosed().subscribe((states) => { + if (states) { + this.updateStates(states); + } + }); + } + + public manageDashboardLayouts($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(ManageDashboardLayoutsDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + layouts: deepClone(this.dashboard.configuration.states[this.dashboardCtx.state].layouts) + } + }).afterClosed().subscribe((layouts) => { + if (layouts) { + this.updateDashboardLayouts(layouts); + } + }); + } + + private updateDashboardLayouts(newLayouts: DashboardStateLayouts) { + this.dashboardUtils.setLayouts(this.dashboard, this.dashboardCtx.state, newLayouts); + this.updateLayouts(); + } + + private updateStates(states: {[id: string]: DashboardState }) { + this.dashboard.configuration.states = states; + this.dashboardUtils.removeUnusedWidgets(this.dashboard); + let targetState = this.dashboardCtx.state; + if (!this.dashboard.configuration.states[targetState]) { + targetState = this.dashboardUtils.getRootStateId(this.dashboardConfiguration.states); + } + this.openDashboardState(targetState); + } + + private importWidget($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.importExport.importWidget(this.dashboard, this.dashboardCtx.state, + this.selectTargetLayout.bind(this), this.entityAliasesUpdated.bind(this), this.filtersUpdated.bind(this)).subscribe( + (importData) => { + if (importData) { + const widget = importData.widget; + const layoutId = importData.layoutId; + this.layouts[layoutId].layoutCtx.widgets.addWidgetId(widget.id); + } + } + ); + } + + public currentDashboardIdChanged(dashboardId: string) { + if (!this.widgetEditMode) { + this.dashboardCtx.stateController.cleanupPreservedStates(); + if (this.currentDashboardScope === 'customer' && this.authUser.authority === Authority.TENANT_ADMIN) { + this.router.navigateByUrl(`customers/${this.currentCustomerId}/dashboards/${dashboardId}`); + } else { + if (this.singlePageMode) { + this.router.navigateByUrl(`dashboard/${dashboardId}`); + } else { + this.router.navigateByUrl(`dashboards/${dashboardId}`); + } + } + } + } + + public toggleDashboardEditMode() { + this.setEditMode(!this.isEdit, true); + } + + public saveDashboard() { + this.setEditMode(false, false); + this.notifyDashboardUpdated(); + } + + public openDashboardState(state: string, openRightLayout?: boolean) { + const layoutsData = this.dashboardUtils.getStateLayoutsData(this.dashboard, state); + if (layoutsData) { + this.dashboardCtx.state = state; + this.dashboardCtx.aliasController.dashboardStateChanged(); + this.isRightLayoutOpened = openRightLayout ? true : false; + this.updateLayouts(layoutsData); + } + } + + private updateLayouts(layoutsData?: DashboardLayoutsInfo) { + if (!layoutsData) { + layoutsData = this.dashboardUtils.getStateLayoutsData(this.dashboard, this.dashboardCtx.state); + } + for (const l of Object.keys(this.layouts)) { + const layout: DashboardPageLayout = this.layouts[l]; + if (layoutsData[l]) { + layout.show = true; + const layoutInfo: DashboardLayoutInfo = layoutsData[l]; + this.updateLayout(layout, layoutInfo); + } else { + layout.show = false; + this.updateLayout(layout, {widgetIds: [], widgetLayouts: {}, gridSettings: null}); + } + } + } + + private updateLayout(layout: DashboardPageLayout, layoutInfo: DashboardLayoutInfo) { + if (layoutInfo.gridSettings) { + layout.layoutCtx.gridSettings = layoutInfo.gridSettings; + } + layout.layoutCtx.widgets.setWidgetIds(layoutInfo.widgetIds); + layout.layoutCtx.widgetLayouts = layoutInfo.widgetLayouts; + if (layout.show && layout.layoutCtx.ctrl) { + layout.layoutCtx.ctrl.reload(); + } + layout.layoutCtx.ignoreLoading = true; + } + + private setEditMode(isEdit: boolean, revert: boolean) { + this.isEdit = isEdit; + if (this.isEdit) { + this.dashboardCtx.stateController.preserveState(); + this.prevDashboard = deepClone(this.dashboard); + } else { + if (this.widgetEditMode) { + if (revert) { + this.dashboard = this.prevDashboard; + } + } else { + this.resetHighlight(); + if (revert) { + this.dashboard = this.prevDashboard; + this.dashboardConfiguration = this.dashboard.configuration; + this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow; + this.entityAliasesUpdated(); + this.filtersUpdated(); + this.updateLayouts(); + } else { + this.dashboard.configuration.timewindow = this.dashboardCtx.dashboardTimewindow; + } + } + } + } + + private resetHighlight() { + for (const l of Object.keys(this.layouts)) { + if (this.layouts[l].layoutCtx) { + if (this.layouts[l].layoutCtx.ctrl) { + this.layouts[l].layoutCtx.ctrl.resetHighlight(); + } + } + } + } + + private entityAliasesUpdated() { + this.dashboardCtx.aliasController.updateEntityAliases(this.dashboard.configuration.entityAliases); + } + + private filtersUpdated() { + this.dashboardCtx.aliasController.updateFilters(this.dashboard.configuration.filters); + } + + private notifyDashboardUpdated() { + if (this.widgetEditMode) { + const widget = this.layouts.main.layoutCtx.widgets.widgetByIndex(0); + const layout = this.layouts.main.layoutCtx.widgetLayouts[widget.id]; + widget.sizeX = layout.sizeX; + widget.sizeY = layout.sizeY; + const message: WindowMessage = { + type: 'widgetEditUpdated', + data: widget + }; + this.window.parent.postMessage(JSON.stringify(message), '*'); + } else { + this.dashboardService.saveDashboard(this.dashboard).subscribe(); + } + } + + helpLinkIdForWidgetType(): string { + let link = 'widgetsConfig'; + if (this.editingWidget && this.editingWidget.type) { + link = widgetTypesData.get(this.editingWidget.type).configHelpLinkId; + } + return link; + } + + addWidget($event: Event, layoutCtx?: DashboardPageLayoutContext) { + if ($event) { + $event.stopPropagation(); + } + this.isAddingWidget = true; + this.addingLayoutCtx = layoutCtx; + } + + onAddWidgetClosed() { + this.isAddingWidget = false; + } + + detailsDrawerOpenedStart() { + if (this.isEditingWidget) { + this.isEditingWidgetClosed = false; + } else if (this.isAddingWidget) { + this.isAddingWidgetClosed = false; + } + setTimeout(() => { + this.cd.detach(); + }, 0); + } + + detailsDrawerOpened() { + this.cd.reattach(); + } + + detailsDrawerClosedStart() { + this.cd.detach(); + } + + detailsDrawerClosed() { + this.isEditingWidgetClosed = true; + this.isAddingWidgetClosed = true; + this.cd.reattach(); + } + + private addWidgetToLayout(widget: Widget, layoutId: DashboardLayoutId) { + this.dashboardUtils.addWidgetToLayout(this.dashboard, this.dashboardCtx.state, layoutId, widget); + this.layouts[layoutId].layoutCtx.widgets.addWidgetId(widget.id); + } + + private selectTargetLayout(): Observable { + const layouts = this.dashboardConfiguration.states[this.dashboardCtx.state].layouts; + const layoutIds = Object.keys(layouts); + if (layoutIds.length > 1) { + return this.dialog.open(SelectTargetLayoutDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] + }).afterClosed(); + } else { + return of(layoutIds[0] as DashboardLayoutId); + } + } + + private addWidgetToDashboard(widget: Widget) { + if (this.addingLayoutCtx) { + this.addWidgetToLayout(widget, this.addingLayoutCtx.id); + this.addingLayoutCtx = null; + } else { + this.selectTargetLayout().subscribe((layoutId) => { + if (layoutId) { + this.addWidgetToLayout(widget, layoutId); + } + }); + } + } + + addWidgetFromType(widget: Widget) { + this.onAddWidgetClosed(); + this.widgetComponentService.getWidgetInfo(widget.bundleAlias, widget.typeAlias, widget.isSystemType).subscribe( + (widgetTypeInfo) => { + const config: WidgetConfig = JSON.parse(widgetTypeInfo.defaultConfig); + config.title = 'New ' + widgetTypeInfo.widgetName; + config.datasources = []; + const newWidget: Widget = { + isSystemType: widget.isSystemType, + bundleAlias: widget.bundleAlias, + typeAlias: widgetTypeInfo.alias, + type: widgetTypeInfo.type, + title: 'New widget', + sizeX: widgetTypeInfo.sizeX, + sizeY: widgetTypeInfo.sizeY, + config, + row: 0, + col: 0 + }; + if (widgetTypeInfo.typeParameters.useCustomDatasources) { + this.addWidgetToDashboard(newWidget); + } else { + this.dialog.open(AddWidgetDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + dashboard: this.dashboard, + aliasController: this.dashboardCtx.aliasController, + widget: newWidget, + widgetInfo: widgetTypeInfo + } + }).afterClosed().subscribe((addedWidget) => { + if (addedWidget) { + this.addWidgetToDashboard(addedWidget); + } + }); + } + } + ); + } + + onRevertWidgetEdit() { + if (this.editWidgetComponent.widgetFormGroup.dirty) { + this.editWidgetComponent.widgetFormGroup.markAsPristine(); + this.editingWidget = deepClone(this.editingWidgetOriginal); + this.editingWidgetLayout = deepClone(this.editingWidgetLayoutOriginal); + } + } + + saveWidget() { + this.editWidgetComponent.widgetFormGroup.markAsPristine(); + const widget = deepClone(this.editingWidget); + const widgetLayout = deepClone(this.editingWidgetLayout); + const id = this.editingWidgetOriginal.id; + this.dashboardConfiguration.widgets[id] = widget; + this.editingWidgetOriginal = widget; + this.editingWidgetLayoutOriginal = widgetLayout; + this.editingLayoutCtx.widgetLayouts[widget.id] = widgetLayout; + setTimeout(() => { + this.editingLayoutCtx.ctrl.highlightWidget(widget.id, 0); + }, 0); + } + + onEditWidgetClosed() { + this.editingWidgetOriginal = null; + this.editingWidget = null; + this.editingWidgetLayoutOriginal = null; + this.editingWidgetLayout = null; + this.editingLayoutCtx = null; + this.editingWidgetSubtitle = null; + this.isEditingWidget = false; + this.resetHighlight(); + this.forceDashboardMobileMode = false; + } + + editWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { + $event.stopPropagation(); + if (this.editingWidgetOriginal === widget) { + this.onEditWidgetClosed(); + } else { + const transition = !this.forceDashboardMobileMode; + this.editingWidgetOriginal = widget; + this.editingWidgetLayoutOriginal = layoutCtx.widgetLayouts[widget.id]; + this.editingWidget = deepClone(this.editingWidgetOriginal); + this.editingWidgetLayout = deepClone(this.editingWidgetLayoutOriginal); + this.editingLayoutCtx = layoutCtx; + this.editingWidgetSubtitle = this.widgetComponentService.getInstantWidgetInfo(this.editingWidget).widgetName; + this.forceDashboardMobileMode = true; + this.isEditingWidget = true; + if (layoutCtx) { + const delayOffset = transition ? 350 : 0; + const delay = transition ? 400 : 300; + setTimeout(() => { + layoutCtx.ctrl.highlightWidget(widget.id, delay); + }, delayOffset); + } + } + } + + copyWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { + this.itembuffer.copyWidget(this.dashboard, + this.dashboardCtx.state, layoutCtx.id, widget); + } + + copyWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { + this.itembuffer.copyWidgetReference(this.dashboard, + this.dashboardCtx.state, layoutCtx.id, widget); + } + + pasteWidget($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition) { + this.itembuffer.pasteWidget(this.dashboard, this.dashboardCtx.state, layoutCtx.id, + pos, this.entityAliasesUpdated.bind(this), this.filtersUpdated.bind(this)).subscribe( + (widget) => { + layoutCtx.widgets.addWidgetId(widget.id); + }); + } + + pasteWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition) { + this.itembuffer.pasteWidgetReference(this.dashboard, this.dashboardCtx.state, layoutCtx.id, + pos).subscribe( + (widget) => { + layoutCtx.widgets.addWidgetId(widget.id); + }); + } + + removeWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { + let title = widget.config.title; + if (!title || title.length === 0) { + title = this.widgetComponentService.getInstantWidgetInfo(widget).widgetName; + } + const confirmTitle = this.translate.instant('widget.remove-widget-title', {widgetTitle: title}); + const confirmContent = this.translate.instant('widget.remove-widget-text'); + this.dialogService.confirm(confirmTitle, + confirmContent, + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + ).subscribe((res) => { + if (res) { + if (layoutCtx.widgets.removeWidgetId(widget.id)) { + this.dashboardUtils.removeWidgetFromLayout(this.dashboard, this.dashboardCtx.state, layoutCtx.id, widget.id); + } + } + }); + } + + exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { + $event.stopPropagation(); + this.importExport.exportWidget(this.dashboard, this.dashboardCtx.state, layoutCtx.id, widget); + } + + widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { + if (this.isEditingWidget) { + this.editWidget($event, layoutCtx, widget); + } + } + + widgetMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { + if (this.isEdit && !this.isEditingWidget) { + layoutCtx.ctrl.selectWidget(widget.id, 0); + } + } + + prepareDashboardContextMenu(layoutCtx: DashboardPageLayoutContext): Array { + const dashboardContextActions: Array = []; + if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { + dashboardContextActions.push( + { + action: this.openDashboardSettings.bind(this), + enabled: true, + value: 'dashboard.settings', + icon: 'settings' + } + ); + dashboardContextActions.push( + { + action: this.openEntityAliases.bind(this), + enabled: true, + value: 'entity.aliases', + icon: 'devices_other' + } + ); + dashboardContextActions.push( + { + action: ($event) => { + layoutCtx.ctrl.pasteWidget($event); + }, + enabled: this.itembuffer.hasWidget(), + value: 'action.paste', + icon: 'content_paste', + shortcut: 'M-V' + } + ); + dashboardContextActions.push( + { + action: ($event) => { + layoutCtx.ctrl.pasteWidgetReference($event); + }, + enabled: this.itembuffer.canPasteWidgetReference(this.dashboard, this.dashboardCtx.state, layoutCtx.id), + value: 'action.paste-reference', + icon: 'content_paste', + shortcut: 'M-I' + } + ); + } + return dashboardContextActions; + } + + prepareWidgetContextMenu(layoutCtx: DashboardPageLayoutContext, widget: Widget): Array { + const widgetContextActions: Array = []; + if (this.isEdit && !this.isEditingWidget) { + widgetContextActions.push( + { + action: (event, currentWidget) => { + this.editWidget(event, layoutCtx, currentWidget); + }, + enabled: true, + value: 'action.edit', + icon: 'edit' + } + ); + if (!this.widgetEditMode) { + widgetContextActions.push( + { + action: (event, currentWidget) => { + this.copyWidget(event, layoutCtx, currentWidget); + }, + enabled: true, + value: 'action.copy', + icon: 'content_copy', + shortcut: 'M-C' + } + ); + widgetContextActions.push( + { + action: (event, currentWidget) => { + this.copyWidgetReference(event, layoutCtx, currentWidget); + }, + enabled: true, + value: 'action.copy-reference', + icon: 'content_copy', + shortcut: 'M-R' + } + ); + widgetContextActions.push( + { + action: (event, currentWidget) => { + this.removeWidget(event, layoutCtx, currentWidget); + }, + enabled: true, + value: 'action.delete', + icon: 'clear', + shortcut: 'M-X' + } + ); + } + } + return widgetContextActions; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.models.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.models.ts new file mode 100644 index 0000000000..010c64b444 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.models.ts @@ -0,0 +1,145 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Dashboard, DashboardLayoutId, GridSettings, WidgetLayouts } from '@app/shared/models/dashboard.models'; +import { Widget, WidgetPosition } from '@app/shared/models/widget.models'; +import { Timewindow } from '@shared/models/time/time.models'; +import { IAliasController, IStateController } from '@core/api/widget-api.models'; +import { ILayoutController } from './layout/layout.models'; +import { DashboardContextMenuItem, WidgetContextMenuItem } from '@home/models/dashboard-component.models'; + +export declare type DashboardPageScope = 'tenant' | 'customer'; + +export interface DashboardContext { + state: string; + getDashboard: () => Dashboard; + dashboardTimewindow: Timewindow; + aliasController: IAliasController; + stateController: IStateController; + runChangeDetection: () => void; +} + +export interface IDashboardController { + dashboardCtx: DashboardContext; + openRightLayout(); + openDashboardState(stateId: string, openRightLayout: boolean); + addWidget($event: Event, layoutCtx: DashboardPageLayoutContext); + editWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); + exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); + removeWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); + widgetMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); + widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); + prepareDashboardContextMenu(layoutCtx: DashboardPageLayoutContext): Array; + prepareWidgetContextMenu(layoutCtx: DashboardPageLayoutContext, widget: Widget): Array; + copyWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); + copyWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); + pasteWidget($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition); + pasteWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition); +} + +export interface DashboardPageLayoutContext { + id: DashboardLayoutId; + widgets: LayoutWidgetsArray; + widgetLayouts: WidgetLayouts; + gridSettings: GridSettings; + ctrl: ILayoutController; + dashboardCtrl: IDashboardController; + ignoreLoading: boolean; +} + +export interface DashboardPageLayout { + show: boolean; + layoutCtx: DashboardPageLayoutContext; +} + +export declare type DashboardPageLayouts = {[key in DashboardLayoutId]: DashboardPageLayout}; + +export class LayoutWidgetsArray implements Iterable { + + private widgetIds: string[] = []; + + private loaded = false; + + constructor(private dashboardCtx: DashboardContext) { + } + + size() { + return this.widgetIds.length; + } + + isLoading() { + return !this.loaded; + } + + isEmpty() { + return this.loaded && this.widgetIds.length === 0; + } + + setWidgetIds(widgetIds: string[]) { + this.widgetIds = widgetIds; + this.loaded = true; + } + + addWidgetId(widgetId: string) { + this.widgetIds.push(widgetId); + } + + removeWidgetId(widgetId: string): boolean { + const index = this.widgetIds.indexOf(widgetId); + if (index > -1) { + this.widgetIds.splice(index, 1); + return true; + } + return false; + } + + [Symbol.iterator](): Iterator { + let pointer = 0; + const widgetIds = this.widgetIds; + const dashboard = this.dashboardCtx.getDashboard(); + return { + next(value?: any): IteratorResult { + if (pointer < widgetIds.length) { + const widgetId = widgetIds[pointer++]; + const widget = dashboard.configuration.widgets[widgetId]; + return { + done: false, + value: widget + }; + } else { + return { + done: true, + value: null + }; + } + } + }; + } + + public widgetByIndex(index: number): Widget { + const widgetId = this.widgetIds[index]; + if (widgetId) { + return this.widgetById(widgetId); + } else { + return null; + } + } + + private widgetById(widgetId: string): Widget { + return this.dashboardCtx.getDashboard().configuration.widgets[widgetId]; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts new file mode 100644 index 0000000000..ebb916d39a --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts @@ -0,0 +1,100 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable, NgModule } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve, RouterModule, Routes } from '@angular/router'; + +import { EntitiesTableComponent } from '../../components/entity/entities-table.component'; +import { Authority } from '@shared/models/authority.enum'; +import { DashboardsTableConfigResolver } from './dashboards-table-config.resolver'; +import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.component'; +import { BreadCrumbConfig, BreadCrumbLabelFunction } from '@shared/components/breadcrumb'; +import { Observable } from 'rxjs'; +import { Dashboard } from '@app/shared/models/dashboard.models'; +import { DashboardService } from '@core/http/dashboard.service'; +import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; +import { map } from 'rxjs/operators'; + +@Injectable() +export class DashboardResolver implements Resolve { + + constructor(private dashboardService: DashboardService, + private dashboardUtils: DashboardUtilsService) { + } + + resolve(route: ActivatedRouteSnapshot): Observable { + const dashboardId = route.params.dashboardId; + return this.dashboardService.getDashboard(dashboardId).pipe( + map((dashboard) => this.dashboardUtils.validateAndUpdateDashboard(dashboard)) + ); + } +} + +export const dashboardBreadcumbLabelFunction: BreadCrumbLabelFunction + = ((route, translate, component) => component.dashboard.title); + +const routes: Routes = [ + { + path: 'dashboards', + data: { + breadcrumb: { + label: 'dashboard.dashboards', + icon: 'dashboard' + } + }, + children: [ + { + path: '', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + title: 'dashboard.dashboards', + dashboardsType: 'tenant' + }, + resolve: { + entitiesTableConfig: DashboardsTableConfigResolver + } + }, + { + path: ':dashboardId', + component: DashboardPageComponent, + data: { + breadcrumb: { + labelFunction: dashboardBreadcumbLabelFunction, + icon: 'dashboard' + } as BreadCrumbConfig, + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + title: 'dashboard.dashboard', + widgetEditMode: false + }, + resolve: { + dashboard: DashboardResolver + } + } + ] + } +]; + +// @dynamic +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [ + DashboardsTableConfigResolver, + DashboardResolver + ] +}) +export class DashboardRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-settings-dialog.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-settings-dialog.component.html new file mode 100644 index 0000000000..80d5412226 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-settings-dialog.component.html @@ -0,0 +1,168 @@ + +
+ +

{{settings ? 'dashboard.settings' : 'layout.settings'}}

+ + +
+ + +
+
+
+ + dashboard.state-controller + + + {{stateControllerId}} + + + +
+ + {{ 'dashboard.toolbar-always-open' | translate }} + + + {{ 'dashboard.display-title' | translate }} + + + +
+
+ + {{ 'dashboard.display-dashboards-selection' | translate }} + + + {{ 'dashboard.display-entities-selection' | translate }} + + + {{ 'dashboard.display-filters' | translate }} + + + {{ 'dashboard.display-dashboard-timewindow' | translate }} + + + {{ 'dashboard.display-dashboard-export' | translate }} + +
+
+
+ + + + dashboard.columns-count + + + {{ 'dashboard.columns-count-required' | translate }} + + + {{ 'dashboard.min-columns-count-message' | translate }} + + + {{ 'dashboard.max-columns-count-message' | translate }} + + + + dashboard.widgets-margins + + + {{ 'dashboard.margin-required' | translate }} + + + {{ 'dashboard.min-margin-message' | translate }} + + + {{ 'dashboard.max-margin-message' | translate }} + + + + {{ 'dashboard.autofill-height' | translate }} + + + + + + + dashboard.background-size-mode + + Fit width + Fit height + Cover + Contain + Original size + + + dashboard.mobile-layout +
+ + {{ 'dashboard.autofill-height' | translate }} + + + dashboard.mobile-row-height + + + {{ 'dashboard.mobile-row-height-required' | translate }} + + + {{ 'dashboard.min-mobile-row-height-message' | translate }} + + + {{ 'dashboard.max-mobile-row-height-message' | translate }} + + +
+
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-settings-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-settings-dialog.component.scss new file mode 100644 index 0000000000..5bc0574210 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-settings-dialog.component.scss @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + small { + white-space: normal; + } +} + +:host ::ng-deep { + .mat-checkbox-label { + white-space: normal; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-settings-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-settings-dialog.component.ts new file mode 100644 index 0000000000..39a19a4bba --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-settings-dialog.component.ts @@ -0,0 +1,140 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { DashboardSettings, GridSettings, StateControllerId } from '@app/shared/models/dashboard.models'; +import { isDefined, isUndefined } from '@core/utils'; +import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; +import { StatesControllerService } from './states/states-controller.service'; + +export interface DashboardSettingsDialogData { + settings?: DashboardSettings; + gridSettings?: GridSettings; +} + +@Component({ + selector: 'tb-dashboard-settings-dialog', + templateUrl: './dashboard-settings-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: DashboardSettingsDialogComponent}], + styleUrls: ['./dashboard-settings-dialog.component.scss'] +}) +export class DashboardSettingsDialogComponent extends DialogComponent + implements OnInit, ErrorStateMatcher { + + settings: DashboardSettings; + gridSettings: GridSettings; + + settingsFormGroup: FormGroup; + gridSettingsFormGroup: FormGroup; + + stateControllerIds: string[]; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: DashboardSettingsDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + private utils: UtilsService, + private dashboardUtils: DashboardUtilsService, + private translate: TranslateService, + private statesControllerService: StatesControllerService) { + super(store, router, dialogRef); + + this.stateControllerIds = Object.keys(this.statesControllerService.getStateControllers()); + + this.settings = this.data.settings; + this.gridSettings = this.data.gridSettings; + + if (this.settings) { + this.settingsFormGroup = this.fb.group({ + stateControllerId: [isUndefined(this.settings.stateControllerId) ? 'entity' : this.settings.stateControllerId, []], + toolbarAlwaysOpen: [isUndefined(this.settings.toolbarAlwaysOpen) ? true : this.settings.toolbarAlwaysOpen, []], + showTitle: [isUndefined(this.settings.showTitle) ? true : this.settings.showTitle, []], + titleColor: [isUndefined(this.settings.titleColor) ? 'rgba(0,0,0,0.870588)' : this.settings.titleColor, []], + showDashboardsSelect: [isUndefined(this.settings.showDashboardsSelect) ? true : this.settings.showDashboardsSelect, []], + showEntitiesSelect: [isUndefined(this.settings.showEntitiesSelect) ? true : this.settings.showEntitiesSelect, []], + showFilters: [isUndefined(this.settings.showFilters) ? true : this.settings.showFilters, []], + showDashboardTimewindow: [isUndefined(this.settings.showDashboardTimewindow) ? true : this.settings.showDashboardTimewindow, []], + showDashboardExport: [isUndefined(this.settings.showDashboardExport) ? true : this.settings.showDashboardExport, []] + }); + this.settingsFormGroup.get('stateControllerId').valueChanges.subscribe( + (stateControllerId: StateControllerId) => { + if (stateControllerId !== 'default') { + this.settingsFormGroup.get('toolbarAlwaysOpen').setValue(true); + } + } + ); + } else { + this.settingsFormGroup = this.fb.group({}); + } + + if (this.gridSettings) { + this.gridSettingsFormGroup = this.fb.group({ + color: [this.gridSettings.color || 'rgba(0,0,0,0.870588)', []], + columns: [this.gridSettings.columns || 24, [Validators.required, Validators.min(10), Validators.max(1000)]], + margin: [isDefined(this.gridSettings.margin) ? this.gridSettings.margin : 10, + [Validators.required, Validators.min(0), Validators.max(50)]], + autoFillHeight: [isUndefined(this.gridSettings.autoFillHeight) ? false : this.gridSettings.autoFillHeight, []], + backgroundColor: [this.gridSettings.backgroundColor || 'rgba(0,0,0,0)', []], + backgroundImageUrl: [this.gridSettings.backgroundImageUrl, []], + backgroundSizeMode: [this.gridSettings.backgroundSizeMode || '100%', []], + mobileAutoFillHeight: [isUndefined(this.gridSettings.mobileAutoFillHeight) ? false : this.gridSettings.mobileAutoFillHeight, []], + mobileRowHeight: [isUndefined(this.gridSettings.mobileRowHeight) ? 70 : this.gridSettings.mobileRowHeight, + [Validators.required, Validators.min(5), Validators.max(200)]] + }); + } else { + this.gridSettingsFormGroup = this.fb.group({}); + } + } + + ngOnInit(): void { + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + let settings: DashboardSettings = null; + let gridSettings: GridSettings = null; + if (this.settings) { + settings = {...this.settings, ...this.settingsFormGroup.value}; + } + if (this.gridSettings) { + gridSettings = {...this.gridSettings, ...this.gridSettingsFormGroup.value}; + } + this.dialogRef.close({settings, gridSettings}); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-tabs.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-tabs.component.html new file mode 100644 index 0000000000..21c53edc3d --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-tabs.component.html @@ -0,0 +1,21 @@ + + + + diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-tabs.component.ts new file mode 100644 index 0000000000..dd3e075b78 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-tabs.component.ts @@ -0,0 +1,38 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; +import { Dashboard } from '@shared/models/dashboard.models'; + +@Component({ + selector: 'tb-dashboard-tabs', + templateUrl: './dashboard-tabs.component.html', + styleUrls: [] +}) +export class DashboardTabsComponent extends EntityTabsComponent { + + constructor(protected store: Store) { + super(store); + } + + ngOnInit() { + super.ngOnInit(); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-toolbar.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-toolbar.component.html new file mode 100644 index 0000000000..46c0e9ef5a --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-toolbar.component.html @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-toolbar.component.scss b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-toolbar.component.scss new file mode 100644 index 0000000000..98d67d2a03 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-toolbar.component.scss @@ -0,0 +1,168 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../../../scss/constants"; + +$toolbar-height: 50px !default; +$fullscreen-toolbar-height: 64px !default; +$mobile-toolbar-height: 80px !default; +$half-mobile-toolbar-height: 40px !default; +$mobile-toolbar-height-total: 84px !default; + +tb-dashboard-toolbar { + mat-fab-toolbar { + mat-fab-trigger { + .mat-fab { + width: 36px; + height: 36px; + margin: 4px 0 0 4px; + .mat-button-wrapper { + padding: 0; + } + } + } + + &.is-fullscreen { + + .mat-fab-toolbar-wrapper { + height: $mobile-toolbar-height-total; + + @media #{$mat-gt-sm} { + height: $fullscreen-toolbar-height; + } + + mat-toolbar { + height: $mobile-toolbar-height; + min-height: $mobile-toolbar-height; + .mat-toolbar-tools { + height: $mobile-toolbar-height; + min-height: $mobile-toolbar-height; + } + @media #{$mat-gt-sm} { + height: $fullscreen-toolbar-height; + min-height: $fullscreen-toolbar-height; + .mat-toolbar-tools { + height: $fullscreen-toolbar-height; + min-height: $fullscreen-toolbar-height; + } + } + } + } + } + + .mat-fab-toolbar-wrapper { + height: $mobile-toolbar-height-total; + + @media #{$mat-gt-sm} { + height: $toolbar-height; + } + + mat-toolbar { + height: $mobile-toolbar-height; + min-height: $mobile-toolbar-height; + .mat-toolbar-tools { + height: $mobile-toolbar-height; + min-height: $mobile-toolbar-height; + } + + @media #{$mat-gt-sm} { + height: $toolbar-height; + min-height: $toolbar-height; + .mat-toolbar-tools { + height: $toolbar-height; + min-height: $toolbar-height; + } + } + + mat-fab-actions { + margin-top: 0; + font-size: 16px; + + @media #{$mat-lt-md} { + height: $mobile-toolbar-height; + max-height: $mobile-toolbar-height; + } + + .close-action { + margin-right: -18px; + } + + .mat-fab-action-item { + width: 100%; + height: $mobile-toolbar-height; + + @media #{$mat-gt-sm} { + height: 46px; + } + + .tb-dashboard-action-panels { + height: $mobile-toolbar-height; + + @media #{$mat-gt-sm} { + height: 46px; + } + + .tb-dashboard-action-panel { + min-width: 0; + height: $half-mobile-toolbar-height; + + @media #{$mat-lt-md} { + mat-menu{ + margin: 0; + } + } + + @media #{$mat-gt-sm} { + height: 46px; + } + + > div { + height: $half-mobile-toolbar-height; + + @media #{$mat-gt-sm} { + height: 46px; + } + } + + mat-select { + margin: 0; + pointer-events: all; + } + + tb-states-component { + pointer-events: all; + } + + button.mat-icon-button:not(.tb-mat-32) { + min-width: 40px; + + @media #{$mat-lt-md} { + min-width: 28px; + padding: 2px; + margin: 0; + .mat-button-wrapper { + display: block; + line-height: 24px; + } + } + } + } + } + } + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-toolbar.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-toolbar.component.ts new file mode 100644 index 0000000000..1812cd7f03 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-toolbar.component.ts @@ -0,0 +1,46 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; + +@Component({ + selector: 'tb-dashboard-toolbar', + templateUrl: './dashboard-toolbar.component.html', + styleUrls: ['./dashboard-toolbar.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class DashboardToolbarComponent implements OnInit { + + @Input() + toolbarOpened: boolean; + + @Input() + forceFullscreen: boolean; + + @Output() + triggerClick = new EventEmitter(); + + constructor() { + } + + ngOnInit(): void { + } + + onTriggerClick() { + this.triggerClick.emit(); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.html new file mode 100644 index 0000000000..dec6eedd0c --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.html @@ -0,0 +1,84 @@ + +
+ + + + + + + + + + + + + + + + + + widgets-bundle.empty + widget.select-widgets-bundle +
diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.scss b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.scss new file mode 100644 index 0000000000..06bedaabf4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.scss @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host ::ng-deep { + .mat-tab-group { + .mat-tab-body-wrapper { + height: 100%; + } + } +} + diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.ts new file mode 100644 index 0000000000..0c90a9bdda --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.ts @@ -0,0 +1,133 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; +import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; +import { IAliasController } from '@core/api/widget-api.models'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { WidgetService } from '@core/http/widget.service'; +import { Widget, widgetType } from '@shared/models/widget.models'; +import { toWidgetInfo } from '@home/models/widget-component.models'; +import { DashboardCallbacks } from '../../models/dashboard-component.models'; + +@Component({ + selector: 'tb-dashboard-widget-select', + templateUrl: './dashboard-widget-select.component.html', + styleUrls: ['./dashboard-widget-select.component.scss'] +}) +export class DashboardWidgetSelectComponent implements OnInit, OnChanges { + + @Input() + widgetsBundle: WidgetsBundle; + + @Input() + aliasController: IAliasController; + + @Output() + widgetSelected: EventEmitter = new EventEmitter(); + + timeseriesWidgetTypes: Array = []; + latestWidgetTypes: Array = []; + rpcWidgetTypes: Array = []; + alarmWidgetTypes: Array = []; + staticWidgetTypes: Array = []; + + callbacks: DashboardCallbacks = { + onWidgetClicked: this.onWidgetClicked.bind(this) + }; + + constructor(private widgetsService: WidgetService) { + } + + ngOnInit(): void { + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (change.currentValue !== change.previousValue && change.currentValue) { + if (propName === 'widgetsBundle') { + this.loadLibrary(); + } + } + } + } + + private loadLibrary() { + this.timeseriesWidgetTypes.length = 0; + this.latestWidgetTypes.length = 0; + this.rpcWidgetTypes.length = 0; + this.alarmWidgetTypes.length = 0; + this.staticWidgetTypes.length = 0; + const bundleAlias = this.widgetsBundle.alias; + const isSystem = this.widgetsBundle.tenantId.id === NULL_UUID; + this.widgetsService.getBundleWidgetTypes(bundleAlias, + isSystem).subscribe( + (types) => { + types = types.sort((a, b) => b.createdTime - a.createdTime); + let top = 0; + types.forEach((type) => { + const widgetTypeInfo = toWidgetInfo(type); + const widget: Widget = { + typeId: type.id, + isSystemType: isSystem, + bundleAlias, + typeAlias: widgetTypeInfo.alias, + type: widgetTypeInfo.type, + title: widgetTypeInfo.widgetName, + sizeX: widgetTypeInfo.sizeX, + sizeY: widgetTypeInfo.sizeY, + row: top, + col: 0, + config: JSON.parse(widgetTypeInfo.defaultConfig) + }; + widget.config.title = widgetTypeInfo.widgetName; + switch (widgetTypeInfo.type) { + case widgetType.timeseries: + this.timeseriesWidgetTypes.push(widget); + break; + case widgetType.latest: + this.latestWidgetTypes.push(widget); + break; + case widgetType.rpc: + this.rpcWidgetTypes.push(widget); + break; + case widgetType.alarm: + this.alarmWidgetTypes.push(widget); + break; + case widgetType.static: + this.staticWidgetTypes.push(widget); + break; + } + top += widget.sizeY; + }); + } + ); + } + + hasWidgetTypes() { + return this.timeseriesWidgetTypes.length > 0 || + this.latestWidgetTypes.length > 0 || + this.rpcWidgetTypes.length > 0 || + this.alarmWidgetTypes.length > 0 || + this.staticWidgetTypes.length > 0; + } + + private onWidgetClicked($event: Event, widget: Widget, index: number): void { + this.widgetSelected.emit(widget); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts new file mode 100644 index 0000000000..6b8729fbdd --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts @@ -0,0 +1,65 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { HomeDialogsModule } from '../../dialogs/home-dialogs.module'; +import { DashboardFormComponent } from '@modules/home/pages/dashboard/dashboard-form.component'; +import { ManageDashboardCustomersDialogComponent } from '@modules/home/pages/dashboard/manage-dashboard-customers-dialog.component'; +import { DashboardRoutingModule } from './dashboard-routing.module'; +import { MakeDashboardPublicDialogComponent } from '@modules/home/pages/dashboard/make-dashboard-public-dialog.component'; +import { HomeComponentsModule } from '@modules/home/components/home-components.module'; +import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.component'; +import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.component'; +import { DashboardToolbarComponent } from './dashboard-toolbar.component'; +import { StatesControllerModule } from '@home/pages/dashboard/states/states-controller.module'; +import { DashboardLayoutComponent } from './layout/dashboard-layout.component'; +import { EditWidgetComponent } from './edit-widget.component'; +import { DashboardWidgetSelectComponent } from './dashboard-widget-select.component'; +import { AddWidgetDialogComponent } from './add-widget-dialog.component'; +import { ManageDashboardLayoutsDialogComponent } from './layout/manage-dashboard-layouts-dialog.component'; +import { DashboardSettingsDialogComponent } from './dashboard-settings-dialog.component'; +import { ManageDashboardStatesDialogComponent } from './states/manage-dashboard-states-dialog.component'; +import { DashboardStateDialogComponent } from './states/dashboard-state-dialog.component'; + +@NgModule({ + declarations: [ + DashboardFormComponent, + DashboardTabsComponent, + ManageDashboardCustomersDialogComponent, + MakeDashboardPublicDialogComponent, + DashboardToolbarComponent, + DashboardPageComponent, + DashboardLayoutComponent, + EditWidgetComponent, + DashboardWidgetSelectComponent, + AddWidgetDialogComponent, + ManageDashboardLayoutsDialogComponent, + DashboardSettingsDialogComponent, + ManageDashboardStatesDialogComponent, + DashboardStateDialogComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + HomeDialogsModule, + StatesControllerModule, + DashboardRoutingModule + ] +}) +export class DashboardModule { } diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts new file mode 100644 index 0000000000..4d176a765d --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts @@ -0,0 +1,506 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; + +import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router'; +import { + CellActionDescriptor, + checkBoxCell, + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig, + GroupActionDescriptor, + HeaderActionDescriptor +} from '@home/models/entity/entities-table-config.models'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { forkJoin, Observable, of } from 'rxjs'; +import { select, Store } from '@ngrx/store'; +import { selectAuthUser } from '@core/auth/auth.selectors'; +import { map, mergeMap, take, tap } from 'rxjs/operators'; +import { AppState } from '@core/core.state'; +import { Authority } from '@app/shared/models/authority.enum'; +import { CustomerService } from '@core/http/customer.service'; +import { Customer } from '@app/shared/models/customer.model'; +import { MatDialog } from '@angular/material/dialog'; +import { DialogService } from '@core/services/dialog.service'; +import { + AddEntitiesToCustomerDialogComponent, + AddEntitiesToCustomerDialogData +} from '../../dialogs/add-entities-to-customer-dialog.component'; +import { + Dashboard, + DashboardInfo, + getDashboardAssignedCustomersText, + isCurrentPublicDashboardCustomer, + isPublicDashboard +} from '@app/shared/models/dashboard.models'; +import { DashboardService } from '@app/core/http/dashboard.service'; +import { DashboardFormComponent } from '@modules/home/pages/dashboard/dashboard-form.component'; +import { + ManageDashboardCustomersActionType, + ManageDashboardCustomersDialogComponent, + ManageDashboardCustomersDialogData +} from './manage-dashboard-customers-dialog.component'; +import { + MakeDashboardPublicDialogComponent, + MakeDashboardPublicDialogData +} from '@modules/home/pages/dashboard/make-dashboard-public-dialog.component'; +import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.component'; +import { ImportExportService } from '@home/components/import-export/import-export.service'; + +@Injectable() +export class DashboardsTableConfigResolver implements Resolve> { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor(private store: Store, + private dashboardService: DashboardService, + private customerService: CustomerService, + private dialogService: DialogService, + private importExport: ImportExportService, + private translate: TranslateService, + private datePipe: DatePipe, + private router: Router, + private dialog: MatDialog) { + + this.config.entityType = EntityType.DASHBOARD; + this.config.entityComponent = DashboardFormComponent; + this.config.entityTabsComponent = DashboardTabsComponent; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.DASHBOARD); + this.config.entityResources = entityTypeResources.get(EntityType.DASHBOARD); + + this.config.deleteEntityTitle = dashboard => + this.translate.instant('dashboard.delete-dashboard-title', { dashboardTitle: dashboard.title }); + this.config.deleteEntityContent = () => this.translate.instant('dashboard.delete-dashboard-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('dashboard.delete-dashboards-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('dashboard.delete-dashboards-text'); + + this.config.loadEntity = id => this.dashboardService.getDashboard(id.id); + this.config.saveEntity = dashboard => { + return this.dashboardService.saveDashboard(dashboard as Dashboard); + }; + this.config.onEntityAction = action => this.onDashboardAction(action); + this.config.detailsReadonly = () => this.config.componentsData.dashboardScope === 'customer_user'; + } + + resolve(route: ActivatedRouteSnapshot): Observable> { + const routeParams = route.params; + this.config.componentsData = { + dashboardScope: route.data.dashboardsType, + customerId: routeParams.customerId + }; + return this.store.pipe(select(selectAuthUser), take(1)).pipe( + tap((authUser) => { + if (authUser.authority === Authority.CUSTOMER_USER) { + this.config.componentsData.dashboardScope = 'customer_user'; + this.config.componentsData.customerId = authUser.customerId; + } + }), + mergeMap(() => + this.config.componentsData.customerId ? + this.customerService.getCustomer(this.config.componentsData.customerId) : of(null as Customer) + ), + map((parentCustomer) => { + if (parentCustomer) { + if (parentCustomer.additionalInfo && parentCustomer.additionalInfo.isPublic) { + this.config.tableTitle = this.translate.instant('customer.public-dashboards'); + } else { + this.config.tableTitle = parentCustomer.title + ': ' + this.translate.instant('dashboard.dashboards'); + } + } else { + this.config.tableTitle = this.translate.instant('dashboard.dashboards'); + } + this.config.columns = this.configureColumns(this.config.componentsData.dashboardScope); + this.configureEntityFunctions(this.config.componentsData.dashboardScope); + this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.dashboardScope); + this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.dashboardScope); + this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.dashboardScope); + this.config.addEnabled = this.config.componentsData.dashboardScope !== 'customer_user'; + this.config.entitiesDeleteEnabled = this.config.componentsData.dashboardScope === 'tenant'; + this.config.deleteEnabled = () => this.config.componentsData.dashboardScope === 'tenant'; + return this.config; + }) + ); + } + + configureColumns(dashboardScope: string): Array> { + const columns: Array> = [ + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), + new EntityTableColumn('title', 'dashboard.title', '50%') + ]; + if (dashboardScope === 'tenant') { + columns.push( + new EntityTableColumn('customersTitle', 'dashboard.assignedToCustomers', + '50%', entity => { + return getDashboardAssignedCustomersText(entity); + }, () => ({}), false), + new EntityTableColumn('dashboardIsPublic', 'dashboard.public', '60px', + entity => { + return checkBoxCell(isPublicDashboard(entity)); + }, () => ({}), false), + ); + } + return columns; + } + + configureEntityFunctions(dashboardScope: string): void { + if (dashboardScope === 'tenant') { + this.config.entitiesFetchFunction = pageLink => + this.dashboardService.getTenantDashboards(pageLink); + this.config.deleteEntity = id => this.dashboardService.deleteDashboard(id.id); + } else { + this.config.entitiesFetchFunction = pageLink => + this.dashboardService.getCustomerDashboards(this.config.componentsData.customerId, pageLink); + this.config.deleteEntity = id => + this.dashboardService.unassignDashboardFromCustomer(this.config.componentsData.customerId, id.id); + } + } + + configureCellActions(dashboardScope: string): Array> { + const actions: Array> = []; + actions.push( + { + name: this.translate.instant('dashboard.open-dashboard'), + icon: 'dashboard', + isEnabled: () => true, + onAction: ($event, entity) => this.openDashboard($event, entity) + } + ); + if (dashboardScope === 'tenant') { + actions.push( + { + name: this.translate.instant('dashboard.export'), + icon: 'file_download', + isEnabled: () => true, + onAction: ($event, entity) => this.exportDashboard($event, entity) + }, + { + name: this.translate.instant('dashboard.make-public'), + icon: 'share', + isEnabled: (entity) => !isPublicDashboard(entity), + onAction: ($event, entity) => this.makePublic($event, entity) + }, + { + name: this.translate.instant('dashboard.make-private'), + icon: 'reply', + isEnabled: (entity) => isPublicDashboard(entity), + onAction: ($event, entity) => this.makePrivate($event, entity) + }, + { + name: this.translate.instant('dashboard.manage-assigned-customers'), + icon: 'assignment_ind', + isEnabled: () => true, + onAction: ($event, entity) => this.manageAssignedCustomers($event, entity) + } + ); + } + if (dashboardScope === 'customer') { + actions.push( + { + name: this.translate.instant('dashboard.export'), + icon: 'file_download', + isEnabled: () => true, + onAction: ($event, entity) => this.exportDashboard($event, entity) + }, + { + name: this.translate.instant('dashboard.make-private'), + icon: 'reply', + isEnabled: (entity) => isCurrentPublicDashboardCustomer(entity, this.config.componentsData.customerId), + onAction: ($event, entity) => this.makePrivate($event, entity) + }, + { + name: this.translate.instant('dashboard.unassign-from-customer'), + icon: 'assignment_return', + isEnabled: (entity) => !isCurrentPublicDashboardCustomer(entity, this.config.componentsData.customerId), + onAction: ($event, entity) => this.unassignFromCustomer($event, entity, this.config.componentsData.customerId) + } + ); + } + return actions; + } + + configureGroupActions(dashboardScope: string): Array> { + const actions: Array> = []; + if (dashboardScope === 'tenant') { + actions.push( + { + name: this.translate.instant('dashboard.assign-dashboards'), + icon: 'assignment_ind', + isEnabled: true, + onAction: ($event, entities) => this.assignDashboardsToCustomers($event, entities.map((entity) => entity.id.id)) + } + ); + actions.push( + { + name: this.translate.instant('dashboard.unassign-dashboards'), + icon: 'assignment_return', + isEnabled: true, + onAction: ($event, entities) => this.unassignDashboardsFromCustomers($event, entities.map((entity) => entity.id.id)) + } + ); + } + if (dashboardScope === 'customer') { + actions.push( + { + name: this.translate.instant('dashboard.unassign-dashboards'), + icon: 'assignment_return', + isEnabled: true, + onAction: ($event, entities) => + this.unassignDashboardsFromCustomer($event, entities.map((entity) => entity.id.id), this.config.componentsData.customerId) + } + ); + } + return actions; + } + + configureAddActions(dashboardScope: string): Array { + const actions: Array = []; + if (dashboardScope === 'tenant') { + actions.push( + { + name: this.translate.instant('dashboard.create-new-dashboard'), + icon: 'insert_drive_file', + isEnabled: () => true, + onAction: ($event) => this.config.table.addEntity($event) + }, + { + name: this.translate.instant('dashboard.import'), + icon: 'file_upload', + isEnabled: () => true, + onAction: ($event) => this.importDashboard($event) + } + ); + } + if (dashboardScope === 'customer') { + actions.push( + { + name: this.translate.instant('dashboard.assign-new-dashboard'), + icon: 'add', + isEnabled: () => true, + onAction: ($event) => this.addDashboardsToCustomer($event) + } + ); + } + return actions; + } + + openDashboard($event: Event, dashboard: DashboardInfo) { + if ($event) { + $event.stopPropagation(); + } + if (this.config.componentsData.dashboardScope === 'customer') { + this.router.navigateByUrl(`customers/${this.config.componentsData.customerId}/dashboards/${dashboard.id.id}`); + } else { + this.router.navigateByUrl(`dashboards/${dashboard.id.id}`); + } + } + + importDashboard($event: Event) { + this.importExport.importDashboard().subscribe( + (dashboard) => { + if (dashboard) { + this.config.table.updateData(); + } + } + ); + } + + exportDashboard($event: Event, dashboard: DashboardInfo) { + if ($event) { + $event.stopPropagation(); + } + this.importExport.exportDashboard(dashboard.id.id); + } + + addDashboardsToCustomer($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(AddEntitiesToCustomerDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + customerId: this.config.componentsData.customerId, + entityType: EntityType.DASHBOARD + } + }).afterClosed() + .subscribe((res) => { + if (res) { + this.config.table.updateData(); + } + }); + } + + makePublic($event: Event, dashboard: DashboardInfo) { + if ($event) { + $event.stopPropagation(); + } + this.dashboardService.makeDashboardPublic(dashboard.id.id).subscribe( + (publicDashboard) => { + this.dialog.open + (MakeDashboardPublicDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + dashboard: publicDashboard + } + }).afterClosed() + .subscribe(() => { + this.config.table.updateData(); + }); + } + ); + } + + makePrivate($event: Event, dashboard: DashboardInfo) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('dashboard.make-private-dashboard-title', {dashboardTitle: dashboard.title}), + this.translate.instant('dashboard.make-private-dashboard-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + this.dashboardService.makeDashboardPrivate(dashboard.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + manageAssignedCustomers($event: Event, dashboard: DashboardInfo) { + const assignedCustomersIds = dashboard.assignedCustomers ? + dashboard.assignedCustomers.map(customerInfo => customerInfo.customerId.id) : []; + this.showManageAssignedCustomersDialog($event, [dashboard.id.id], 'manage', assignedCustomersIds); + } + + assignDashboardsToCustomers($event: Event, dashboardIds: Array) { + this.showManageAssignedCustomersDialog($event, dashboardIds, 'assign'); + } + + unassignDashboardsFromCustomers($event: Event, dashboardIds: Array) { + this.showManageAssignedCustomersDialog($event, dashboardIds, 'unassign'); + } + + showManageAssignedCustomersDialog($event: Event, dashboardIds: Array, + actionType: ManageDashboardCustomersActionType, + assignedCustomersIds?: Array) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(ManageDashboardCustomersDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + dashboardIds, + actionType, + assignedCustomersIds + } + }).afterClosed() + .subscribe((res) => { + if (res) { + this.config.table.updateData(); + } + }); + } + + unassignFromCustomer($event: Event, dashboard: DashboardInfo, customerId: string) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('dashboard.unassign-dashboard-title', {dashboardTitle: dashboard.title}), + this.translate.instant('dashboard.unassign-dashboard-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + this.dashboardService.unassignDashboardFromCustomer(customerId, dashboard.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + unassignDashboardsFromCustomer($event: Event, dashboardIds: Array, customerId: string) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('dashboard.unassign-dashboards-title', {count: dashboardIds.length}), + this.translate.instant('dashboard.unassign-dashboards-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + const tasks: Observable[] = []; + dashboardIds.forEach( + (dashboardId) => { + tasks.push(this.dashboardService.unassignDashboardFromCustomer(customerId, dashboardId)); + } + ); + forkJoin(tasks).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + onDashboardAction(action: EntityAction): boolean { + switch (action.action) { + case 'open': + this.openDashboard(action.event, action.entity); + return true; + case 'export': + this.exportDashboard(action.event, action.entity); + return true; + case 'makePublic': + this.makePublic(action.event, action.entity); + return true; + case 'makePrivate': + this.makePrivate(action.event, action.entity); + return true; + case 'manageAssignedCustomers': + this.manageAssignedCustomers(action.event, action.entity); + return true; + case 'unassignFromCustomer': + this.unassignFromCustomer(action.event, action.entity, this.config.componentsData.customerId); + return true; + } + return false; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/edit-widget.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/edit-widget.component.html new file mode 100644 index 0000000000..9dc430596a --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/edit-widget.component.html @@ -0,0 +1,29 @@ + +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/edit-widget.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/edit-widget.component.ts new file mode 100644 index 0000000000..8b4c740693 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/edit-widget.component.ts @@ -0,0 +1,119 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { MatDialog } from '@angular/material/dialog'; +import { Dashboard, WidgetLayout } from '@shared/models/dashboard.models'; +import { IAliasController } from '@core/api/widget-api.models'; +import { Widget } from '@shared/models/widget.models'; +import { WidgetComponentService } from '@home/components/widget/widget-component.service'; +import { WidgetConfigComponentData } from '../../models/widget-component.models'; +import { isDefined, isString } from '@core/utils'; +import { FormBuilder, FormGroup } from '@angular/forms'; + +@Component({ + selector: 'tb-edit-widget', + templateUrl: './edit-widget.component.html', + styleUrls: [] +}) +export class EditWidgetComponent extends PageComponent implements OnInit, OnChanges { + + @Input() + dashboard: Dashboard; + + @Input() + aliasController: IAliasController; + + @Input() + widgetEditMode: boolean; + + @Input() + widget: Widget; + + @Input() + widgetLayout: WidgetLayout; + + widgetFormGroup: FormGroup; + + widgetConfig: WidgetConfigComponentData; + + constructor(protected store: Store, + private dialog: MatDialog, + private fb: FormBuilder, + private widgetComponentService: WidgetComponentService) { + super(store); + this.widgetFormGroup = this.fb.group({ + widgetConfig: [null] + }); + } + + ngOnInit(): void { + this.loadWidgetConfig(); + } + + ngOnChanges(changes: SimpleChanges): void { + let reloadConfig = false; + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (['widget', 'widgetLayout'].includes(propName)) { + reloadConfig = true; + } + } + } + if (reloadConfig) { + this.loadWidgetConfig(); + } + } + + private loadWidgetConfig() { + if (!this.widget) { + return; + } + const widgetInfo = this.widgetComponentService.getInstantWidgetInfo(this.widget); + const rawSettingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema; + const rawDataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema; + const typeParameters = widgetInfo.typeParameters; + const actionSources = widgetInfo.actionSources; + const isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true; + let settingsSchema; + if (!rawSettingsSchema || rawSettingsSchema === '') { + settingsSchema = {}; + } else { + settingsSchema = isString(rawSettingsSchema) ? JSON.parse(rawSettingsSchema) : rawSettingsSchema; + } + let dataKeySettingsSchema; + if (!rawDataKeySettingsSchema || rawDataKeySettingsSchema === '') { + dataKeySettingsSchema = {}; + } else { + dataKeySettingsSchema = isString(rawDataKeySettingsSchema) ? JSON.parse(rawDataKeySettingsSchema) : rawDataKeySettingsSchema; + } + this.widgetConfig = { + config: this.widget.config, + layout: this.widgetLayout, + widgetType: this.widget.type, + typeParameters, + actionSources, + isDataEnabled, + settingsSchema, + dataKeySettingsSchema + }; + this.widgetFormGroup.reset({widgetConfig: this.widgetConfig}); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.html new file mode 100644 index 0000000000..52734e6b4b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.html @@ -0,0 +1,62 @@ + +
+ + +
+
+
+ + {{'dashboard.no-widgets' | translate}} + + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.scss b/ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.scss new file mode 100644 index 0000000000..49b78bc9bd --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.scss @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + button.tb-add-new-widget { + padding-right: 12px; + font-size: 24px; + border-style: dashed; + border-width: 2px; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.ts new file mode 100644 index 0000000000..f6f9ab12ab --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.ts @@ -0,0 +1,257 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { ILayoutController } from '@home/pages/dashboard/layout/layout.models'; +import { DashboardContext, DashboardPageLayoutContext } from '@home/pages/dashboard/dashboard-page.models'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Widget } from '@shared/models/widget.models'; +import { + DashboardCallbacks, + DashboardContextMenuItem, + IDashboardComponent, + WidgetContextMenuItem +} from '@home/models/dashboard-component.models'; +import { Subscription } from 'rxjs'; +import { Hotkey } from 'angular2-hotkeys'; +import { TranslateService } from '@ngx-translate/core'; +import { ItemBufferService } from '@app/core/services/item-buffer.service'; +import { DomSanitizer, SafeStyle } from '@angular/platform-browser'; +import { TbCheatSheetComponent } from '@shared/components/cheatsheet.component'; + +@Component({ + selector: 'tb-dashboard-layout', + templateUrl: './dashboard-layout.component.html', + styleUrls: ['./dashboard-layout.component.scss'] +}) +export class DashboardLayoutComponent extends PageComponent implements ILayoutController, DashboardCallbacks, OnInit, OnDestroy { + + layoutCtxValue: DashboardPageLayoutContext; + dashboardStyle: {[klass: string]: any} = null; + backgroundImage: SafeStyle | string; + + hotKeys: Hotkey[] = []; + + @Input() dashboardCheatSheet: TbCheatSheetComponent; + + @Input() + set layoutCtx(val: DashboardPageLayoutContext) { + this.layoutCtxValue = val; + if (this.layoutCtxValue) { + this.layoutCtxValue.ctrl = this; + if (this.dashboardStyle == null) { + this.loadDashboardStyle(); + } + } + } + get layoutCtx(): DashboardPageLayoutContext { + return this.layoutCtxValue; + } + + @Input() + dashboardCtx: DashboardContext; + + @Input() + isEdit: boolean; + + @Input() + isEditingWidget: boolean; + + @Input() + isMobile: boolean; + + @Input() + widgetEditMode: boolean; + + @ViewChild('dashboard', {static: true}) dashboard: IDashboardComponent; + + private rxSubscriptions = new Array(); + + constructor(protected store: Store, + private translate: TranslateService, + private itembuffer: ItemBufferService, + private sanitizer: DomSanitizer) { + super(store); + this.initHotKeys(); + } + + ngOnInit(): void { + this.rxSubscriptions.push(this.dashboard.dashboardTimewindowChanged.subscribe( + (dashboardTimewindow) => { + this.dashboardCtx.dashboardTimewindow = dashboardTimewindow; + this.dashboardCtx.runChangeDetection(); + }) + ); + } + + ngOnDestroy(): void { + this.rxSubscriptions.forEach((subscription) => { + subscription.unsubscribe(); + }); + this.rxSubscriptions.length = 0; + } + + private initHotKeys(): void { + this.hotKeys.push( + new Hotkey('ctrl+c', (event: KeyboardEvent) => { + if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { + const widget = this.dashboard.getSelectedWidget(); + if (widget) { + event.preventDefault(); + this.copyWidget(event, widget); + } + } + return false; + }, null, + this.translate.instant('action.copy')) + ); + this.hotKeys.push( + new Hotkey('ctrl+r', (event: KeyboardEvent) => { + if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { + const widget = this.dashboard.getSelectedWidget(); + if (widget) { + event.preventDefault(); + this.copyWidgetReference(event, widget); + } + } + return false; + }, null, + this.translate.instant('action.copy-reference')) + ); + this.hotKeys.push( + new Hotkey('ctrl+v', (event: KeyboardEvent) => { + if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { + if (this.itembuffer.hasWidget()) { + event.preventDefault(); + this.pasteWidget(event); + } + } + return false; + }, null, + this.translate.instant('action.paste')) + ); + this.hotKeys.push( + new Hotkey('ctrl+i', (event: KeyboardEvent) => { + if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { + if (this.itembuffer.canPasteWidgetReference(this.dashboardCtx.getDashboard(), + this.dashboardCtx.state, this.layoutCtx.id)) { + event.preventDefault(); + this.pasteWidgetReference(event); + } + } + return false; + }, null, + this.translate.instant('action.paste-reference')) + ); + this.hotKeys.push( + new Hotkey('ctrl+x', (event: KeyboardEvent) => { + if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { + const widget = this.dashboard.getSelectedWidget(); + if (widget) { + event.preventDefault(); + this.layoutCtx.dashboardCtrl.removeWidget(event, this.layoutCtx, widget); + } + } + return false; + }, null, + this.translate.instant('action.delete')) + ); + } + + private loadDashboardStyle() { + this.dashboardStyle = {'background-color': this.layoutCtx.gridSettings.backgroundColor, + 'background-repeat': 'no-repeat', + 'background-attachment': 'scroll', + 'background-size': this.layoutCtx.gridSettings.backgroundSizeMode || '100%', + 'background-position': '0% 0%'}; + this.backgroundImage = this.layoutCtx.gridSettings.backgroundImageUrl ? + this.sanitizer.bypassSecurityTrustStyle('url(' + this.layoutCtx.gridSettings.backgroundImageUrl + ')') : 'none'; + } + + reload() { + this.loadDashboardStyle(); + this.dashboard.pauseChangeNotifications(); + setTimeout(() => { + this.dashboard.resumeChangeNotifications(); + this.dashboard.notifyLayoutUpdated(); + }, 0); + } + + resetHighlight() { + this.dashboard.resetHighlight(); + } + + highlightWidget(widgetId: string, delay?: number) { + this.dashboard.highlightWidget(widgetId, delay); + } + + selectWidget(widgetId: string, delay?: number) { + this.dashboard.selectWidget(widgetId, delay); + } + + addWidget($event: Event) { + this.layoutCtx.dashboardCtrl.addWidget($event, this.layoutCtx); + } + + onEditWidget($event: Event, widget: Widget): void { + this.layoutCtx.dashboardCtrl.editWidget($event, this.layoutCtx, widget); + } + + onExportWidget($event: Event, widget: Widget): void { + this.layoutCtx.dashboardCtrl.exportWidget($event, this.layoutCtx, widget); + } + + onRemoveWidget($event: Event, widget: Widget): void { + return this.layoutCtx.dashboardCtrl.removeWidget($event, this.layoutCtx, widget); + } + + onWidgetMouseDown($event: Event, widget: Widget): void { + this.layoutCtx.dashboardCtrl.widgetMouseDown($event, this.layoutCtx, widget); + } + + onWidgetClicked($event: Event, widget: Widget): void { + this.layoutCtx.dashboardCtrl.widgetClicked($event, this.layoutCtx, widget); + } + + prepareDashboardContextMenu($event: Event): Array { + return this.layoutCtx.dashboardCtrl.prepareDashboardContextMenu(this.layoutCtx); + } + + prepareWidgetContextMenu($event: Event, widget: Widget): Array { + return this.layoutCtx.dashboardCtrl.prepareWidgetContextMenu(this.layoutCtx, widget); + } + + copyWidget($event: Event, widget: Widget) { + this.layoutCtx.dashboardCtrl.copyWidget($event, this.layoutCtx, widget); + } + + copyWidgetReference($event: Event, widget: Widget) { + this.layoutCtx.dashboardCtrl.copyWidgetReference($event, this.layoutCtx, widget); + } + + pasteWidget($event: Event) { + const pos = this.dashboard.getEventGridPosition($event); + this.layoutCtx.dashboardCtrl.pasteWidget($event, this.layoutCtx, pos); + } + + pasteWidgetReference($event: Event) { + const pos = this.dashboard.getEventGridPosition($event); + this.layoutCtx.dashboardCtrl.pasteWidgetReference($event, this.layoutCtx, pos); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/layout/layout.models.ts b/ui-ngx/src/app/modules/home/pages/dashboard/layout/layout.models.ts new file mode 100644 index 0000000000..4d6602c178 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/layout/layout.models.ts @@ -0,0 +1,24 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export interface ILayoutController { + reload(); + resetHighlight(); + highlightWidget(widgetId: string, delay?: number); + selectWidget(widgetId: string, delay?: number); + pasteWidget($event: MouseEvent); + pasteWidgetReference($event: MouseEvent); +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/layout/manage-dashboard-layouts-dialog.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/layout/manage-dashboard-layouts-dialog.component.html new file mode 100644 index 0000000000..0014fcb0cb --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/layout/manage-dashboard-layouts-dialog.component.html @@ -0,0 +1,70 @@ + +
+ +

layout.manage

+ + +
+ + +
+
+
+ + {{ 'layout.main' | translate }} + + + {{ 'layout.right' | translate }} + +
+
+ + +
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/layout/manage-dashboard-layouts-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/layout/manage-dashboard-layouts-dialog.component.ts new file mode 100644 index 0000000000..3e6e40ffaa --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/layout/manage-dashboard-layouts-dialog.component.ts @@ -0,0 +1,121 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { DashboardLayoutId, DashboardStateLayouts } from '@app/shared/models/dashboard.models'; +import { deepClone, isDefined } from '@core/utils'; +import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; +import { + DashboardSettingsDialogComponent, + DashboardSettingsDialogData +} from '@home/pages/dashboard/dashboard-settings-dialog.component'; + +export interface ManageDashboardLayoutsDialogData { + layouts: DashboardStateLayouts; +} + +@Component({ + selector: 'tb-manage-dashboard-layouts-dialog', + templateUrl: './manage-dashboard-layouts-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: ManageDashboardLayoutsDialogComponent}], + styleUrls: ['../../../components/dashboard/layout-button.scss'] +}) +export class ManageDashboardLayoutsDialogComponent extends DialogComponent + implements OnInit, ErrorStateMatcher { + + layoutsFormGroup: FormGroup; + + layouts: DashboardStateLayouts; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: ManageDashboardLayoutsDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + private utils: UtilsService, + private dashboardUtils: DashboardUtilsService, + private translate: TranslateService, + private dialog: MatDialog) { + super(store, router, dialogRef); + + this.layouts = this.data.layouts; + this.layoutsFormGroup = this.fb.group({ + main: [{value: isDefined(this.layouts.main), disabled: true}, []], + right: [isDefined(this.layouts.right), []], + } + ); + for (const l of Object.keys(this.layoutsFormGroup.controls)) { + const control = this.layoutsFormGroup.controls[l]; + if (!this.layouts[l]) { + this.layouts[l] = this.dashboardUtils.createDefaultLayoutData(); + } + } + } + + ngOnInit(): void { + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + openLayoutSettings(layoutId: DashboardLayoutId) { + const gridSettings = deepClone(this.layouts[layoutId].gridSettings); + this.dialog.open(DashboardSettingsDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + settings: null, + gridSettings + } + }).afterClosed().subscribe((data) => { + if (data && data.gridSettings) { + this.dashboardUtils.updateLayoutSettings(this.layouts[layoutId], data.gridSettings); + this.layoutsFormGroup.markAsDirty(); + } + }); + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + for (const l of Object.keys(this.layoutsFormGroup.controls)) { + const control = this.layoutsFormGroup.controls[l]; + if (!control.value) { + delete this.layouts[l]; + } + } + this.dialogRef.close(this.layouts); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/make-dashboard-public-dialog.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/make-dashboard-public-dialog.component.html new file mode 100644 index 0000000000..3097e0e303 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/make-dashboard-public-dialog.component.html @@ -0,0 +1,63 @@ + +
+ +

{{ 'dashboard.public-dashboard-title' | translate }}

+ + +
+ + +
+
+ + +
+
{{ publicLink }}
+ +
+
+ + +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/make-dashboard-public-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/make-dashboard-public-dialog.component.ts new file mode 100644 index 0000000000..caceb2023b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/make-dashboard-public-dialog.component.ts @@ -0,0 +1,77 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder } from '@angular/forms'; +import { DashboardService } from '@core/http/dashboard.service'; +import { DashboardInfo } from '@app/shared/models/dashboard.models'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { TranslateService } from '@ngx-translate/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; + +export interface MakeDashboardPublicDialogData { + dashboard: DashboardInfo; +} + +@Component({ + selector: 'tb-make-dashboard-public-dialog', + templateUrl: './make-dashboard-public-dialog.component.html', + styleUrls: [] +}) +export class MakeDashboardPublicDialogComponent extends DialogComponent implements OnInit { + + dashboard: DashboardInfo; + + publicLink: string; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: MakeDashboardPublicDialogData, + public translate: TranslateService, + private dashboardService: DashboardService, + public dialogRef: MatDialogRef, + public fb: FormBuilder) { + super(store, router, dialogRef); + + this.dashboard = data.dashboard; + this.publicLink = dashboardService.getPublicDashboardLink(this.dashboard); + } + + ngOnInit(): void { + } + + close(): void { + this.dialogRef.close(); + } + + + onPublicLinkCopied($event) { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('dashboard.public-link-copied-message'), + type: 'success', + target: 'makeDashboardPublicDialogContent', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'left' + })); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/manage-dashboard-customers-dialog.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/manage-dashboard-customers-dialog.component.html new file mode 100644 index 0000000000..86369282c7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/manage-dashboard-customers-dialog.component.html @@ -0,0 +1,57 @@ + +
+ +

{{ titleText | translate }}

+ + +
+ + +
+
+
+ {{ labelText | translate }} + + +
+
+
+ + + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/manage-dashboard-customers-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/manage-dashboard-customers-dialog.component.ts new file mode 100644 index 0000000000..b3ea169238 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/manage-dashboard-customers-dialog.component.ts @@ -0,0 +1,130 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms'; +import { EntityType } from '@shared/models/entity-type.models'; +import { DashboardService } from '@core/http/dashboard.service'; +import { forkJoin, Observable } from 'rxjs'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; + +export type ManageDashboardCustomersActionType = 'assign' | 'manage' | 'unassign'; + +export interface ManageDashboardCustomersDialogData { + actionType: ManageDashboardCustomersActionType; + dashboardIds: Array; + assignedCustomersIds?: Array; +} + +@Component({ + selector: 'tb-manage-dashboard-customers-dialog', + templateUrl: './manage-dashboard-customers-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: ManageDashboardCustomersDialogComponent}], + styleUrls: [] +}) +export class ManageDashboardCustomersDialogComponent extends + DialogComponent implements OnInit, ErrorStateMatcher { + + dashboardCustomersFormGroup: FormGroup; + + submitted = false; + + entityType = EntityType; + + titleText: string; + labelText: string; + actionName: string; + + assignedCustomersIds: string[]; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: ManageDashboardCustomersDialogData, + private dashboardService: DashboardService, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + public fb: FormBuilder) { + super(store, router, dialogRef); + + this.assignedCustomersIds = data.assignedCustomersIds || []; + switch (data.actionType) { + case 'assign': + this.titleText = 'dashboard.assign-to-customers'; + this.labelText = 'dashboard.assign-to-customers-text'; + this.actionName = 'action.assign'; + break; + case 'manage': + this.titleText = 'dashboard.manage-assigned-customers'; + this.labelText = 'dashboard.assigned-customers'; + this.actionName = 'action.update'; + break; + case 'unassign': + this.titleText = 'dashboard.unassign-from-customers'; + this.labelText = 'dashboard.unassign-from-customers-text'; + this.actionName = 'action.unassign'; + break; + } + } + + ngOnInit(): void { + this.dashboardCustomersFormGroup = this.fb.group({ + assignedCustomerIds: [[...this.assignedCustomersIds]] + }); + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(false); + } + + submit(): void { + this.submitted = true; + const customerIds: Array = this.dashboardCustomersFormGroup.get('assignedCustomerIds').value; + const tasks: Observable[] = []; + + this.data.dashboardIds.forEach( + (dashboardId) => { + tasks.push(this.getManageDashboardCustomersTask(dashboardId, customerIds)); + } + ); + forkJoin(tasks).subscribe( + () => { + this.dialogRef.close(true); + } + ); + } + + private getManageDashboardCustomersTask(dashboardId: string, customerIds: Array): Observable { + switch (this.data.actionType) { + case 'assign': + return this.dashboardService.addDashboardCustomers(dashboardId, customerIds); + case 'manage': + return this.dashboardService.updateDashboardCustomers(dashboardId, customerIds); + case 'unassign': + return this.dashboardService.removeDashboardCustomers(dashboardId, customerIds); + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/dashboard-state-dialog.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/states/dashboard-state-dialog.component.html new file mode 100644 index 0000000000..52788ca0e8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/dashboard-state-dialog.component.html @@ -0,0 +1,67 @@ + +
+ +

{{ isAdd ? 'dashboard.add-state' : 'dashboard.edit-state' }}

+ + +
+ + +
+
+ + dashboard.state-name + + + {{ 'dashboard.state-name-required' | translate }} + + + + dashboard.state-id + + + {{ 'dashboard.state-id-required' | translate }} + + + {{ 'dashboard.state-id-exists' | translate }} + + + + {{ 'dashboard.is-root-state' | translate }} + +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/dashboard-state-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/states/dashboard-state-dialog.component.ts new file mode 100644 index 0000000000..4835e50656 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/dashboard-state-dialog.component.ts @@ -0,0 +1,144 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + FormBuilder, + FormControl, + FormGroup, + FormGroupDirective, + NgForm, + ValidatorFn, + Validators +} from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { DashboardState } from '@app/shared/models/dashboard.models'; +import { DashboardStateInfo } from '@home/pages/dashboard/states/manage-dashboard-states-dialog.component.models'; +import { TranslateService } from '@ngx-translate/core'; +import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; + +export interface DashboardStateDialogData { + states: {[id: string]: DashboardState }; + state: DashboardStateInfo; + isAdd: boolean; +} + +@Component({ + selector: 'tb-dashboard-state-dialog', + templateUrl: './dashboard-state-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: DashboardStateDialogComponent}], + styleUrls: [] +}) +export class DashboardStateDialogComponent extends + DialogComponent + implements OnInit, ErrorStateMatcher { + + stateFormGroup: FormGroup; + + states: {[id: string]: DashboardState }; + state: DashboardStateInfo; + prevStateId: string; + + stateIdTouched: boolean; + + isAdd: boolean; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: DashboardStateDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + private translate: TranslateService, + private dashboardUtils: DashboardUtilsService) { + super(store, router, dialogRef); + + this.states = this.data.states; + this.isAdd = this.data.isAdd; + if (this.isAdd) { + this.state = {id: '', ...this.dashboardUtils.createDefaultState('', false)}; + this.prevStateId = ''; + } else { + this.state = this.data.state; + this.prevStateId = this.state.id; + } + + this.stateFormGroup = this.fb.group({ + name: [this.state.name, [Validators.required]], + id: [this.state.id, [Validators.required, this.validateDuplicateStateId()]], + root: [this.state.root, []], + }); + + this.stateFormGroup.get('name').valueChanges.subscribe((name: string) => { + this.checkStateName(name); + }); + + this.stateFormGroup.get('id').valueChanges.subscribe((id: string) => { + this.stateIdTouched = true; + }); + } + + private checkStateName(name: string) { + if (name && !this.stateIdTouched && this.isAdd) { + this.stateFormGroup.get('id').setValue( + name.toLowerCase().replace(/\W/g, '_'), + { emitEvent: false } + ); + } + } + + private validateDuplicateStateId(): ValidatorFn { + return (c: FormControl) => { + const newStateId: string = c.value; + if (newStateId) { + const existing = this.states[newStateId]; + if (existing && newStateId !== this.prevStateId) { + return { + stateExists: true + }; + } + } + return null; + }; + } + + ngOnInit(): void { + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + this.state = {...this.state, ...this.stateFormGroup.value}; + this.state.id = this.state.id.trim(); + this.dialogRef.close(this.state); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/default-state-controller.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/states/default-state-controller.component.html new file mode 100644 index 0000000000..c6ed997d17 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/default-state-controller.component.html @@ -0,0 +1,23 @@ + + + + {{getStateName(stateKv.key, stateKv.value)}} + + diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/default-state-controller.component.scss b/ui-ngx/src/app/modules/home/pages/dashboard/states/default-state-controller.component.scss new file mode 100644 index 0000000000..919fbbf16f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/default-state-controller.component.scss @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + width: min-content; //for Safari + + mat-select.default-state-controller { + margin: 0; + } +} + +:host ::ng-deep { + mat-select.default-state-controller { + .mat-select-value { + max-width: 200px; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/default-state-controller.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/states/default-state-controller.component.ts new file mode 100644 index 0000000000..00863cd18e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/default-state-controller.component.ts @@ -0,0 +1,248 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, NgZone, OnDestroy, OnInit } from '@angular/core'; +import { StateObject, StateParams } from '@core/api/widget-api.models'; +import { ActivatedRoute, Router } from '@angular/router'; +import { DashboardState } from '@shared/models/dashboard.models'; +import { StateControllerState } from './state-controller.models'; +import { StateControllerComponent } from './state-controller.component'; +import { StatesControllerService } from '@home/pages/dashboard/states/states-controller.service'; +import { EntityId } from '@app/shared/models/id/entity-id'; +import { UtilsService } from '@core/services/utils.service'; +import { base64toObj, objToBase64URI } from '@app/core/utils'; +import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; +import { EntityService } from '@core/http/entity.service'; + +@Component({ + selector: 'tb-default-state-controller', + templateUrl: './default-state-controller.component.html', + styleUrls: ['./default-state-controller.component.scss'] +}) +export class DefaultStateControllerComponent extends StateControllerComponent implements OnInit, OnDestroy { + + constructor(protected router: Router, + protected route: ActivatedRoute, + protected ngZone: NgZone, + protected statesControllerService: StatesControllerService, + private utils: UtilsService, + private entityService: EntityService, + private dashboardUtils: DashboardUtilsService) { + super(router, route, ngZone, statesControllerService); + } + + ngOnInit(): void { + super.ngOnInit(); + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + } + + protected init() { + if (this.preservedState) { + this.stateObject = this.preservedState; + setTimeout(() => { + this.gotoState(this.stateObject[0].id, true); + }, 1); + } else { + const initialState = this.currentState; + this.stateObject = this.parseState(initialState); + setTimeout(() => { + this.gotoState(this.stateObject[0].id, false); + }, 1); + } + } + + protected onMobileChanged() { + } + + protected onStateIdChanged() { + } + + protected onStatesChanged() { + } + + protected onStateChanged() { + this.stateObject = this.parseState(this.currentState); + this.gotoState(this.stateObject[0].id, false); + } + + protected stateControllerId(): string { + return 'default'; + } + + public getStateParams(): StateParams { + if (this.stateObject && this.stateObject.length) { + return this.stateObject[this.stateObject.length - 1].params; + } else { + return {}; + } + } + + public openState(id: string, params?: StateParams, openRightLayout?: boolean): void { + if (this.states && this.states[id]) { + if (!params) { + params = {}; + } + const newState: StateObject = { + id, + params + }; + this.stateObject[0] = newState; + this.gotoState(this.stateObject[0].id, true, openRightLayout); + } + } + + public pushAndOpenState(states: Array, openRightLayout?: boolean): void { + const state = states[states.length - 1]; + this.openState(state.id, state.params, openRightLayout); + } + + public updateState(id: string, params?: StateParams, openRightLayout?: boolean): void { + if (!id) { + id = this.getStateId(); + } + if (this.states && this.states[id]) { + if (!params) { + params = {}; + } + const newState: StateObject = { + id, + params + }; + this.stateObject[0] = newState; + this.gotoState(this.stateObject[0].id, true, openRightLayout); + } + } + + public getEntityId(entityParamName: string): EntityId { + return null; + } + + public getStateId(): string { + if (this.stateObject && this.stateObject.length) { + return this.stateObject[this.stateObject.length - 1].id; + } else { + return ''; + } + } + + public getStateIdAtIndex(index: number): string { + if (this.stateObject && this.stateObject[index]) { + return this.stateObject[index].id; + } else { + return ''; + } + } + + public getStateIndex(): number { + if (this.stateObject && this.stateObject.length) { + return this.stateObject.length - 1; + } else { + return -1; + } + } + + public getStateParamsByStateId(stateId: string): StateParams { + const stateObj = this.getStateObjById(stateId); + if (stateObj) { + return stateObj.params; + } else { + return null; + } + } + + public navigatePrevState(index: number): void { + if (index < this.stateObject.length - 1) { + this.stateObject.splice(index + 1, this.stateObject.length - index - 1); + this.gotoState(this.stateObject[this.stateObject.length - 1].id, true); + } + } + + public resetState(): void { + const rootStateId = this.dashboardUtils.getRootStateId(this.states); + this.stateObject = [ { id: rootStateId, params: {} } ]; + this.gotoState(rootStateId, true); + } + + public getStateName(id: string, state: DashboardState): string { + return this.utils.customTranslation(state.name, id); + } + + public displayStateSelection(): boolean { + return this.states && Object.keys(this.states).length > 1; + } + + public selectedStateIdChanged() { + this.gotoState(this.stateObject[0].id, true); + } + + private parseState(stateBase64: string): StateControllerState { + let result: StateControllerState; + if (stateBase64) { + try { + result = base64toObj(stateBase64); + } catch (e) { + result = [ { id: null, params: {} } ]; + } + } + if (!result) { + result = []; + } + if (!result.length) { + result[0] = { id: null, params: {} }; + } else if (result.length > 1) { + const newResult = []; + newResult.push(result[result.length - 1]); + result = newResult; + } + const rootStateId = this.dashboardUtils.getRootStateId(this.states); + if (!result[0].id) { + result[0].id = rootStateId; + } + if (!this.states[result[0].id]) { + result[0].id = rootStateId; + } + let i = result.length; + while (i--) { + if (!result[i].id || !this.states[result[i].id]) { + result.splice(i, 1); + } + } + return result; + } + + private gotoState(stateId: string, update: boolean, openRightLayout?: boolean) { + if (this.dashboardCtrl.dashboardCtx.state !== stateId) { + this.dashboardCtrl.openDashboardState(stateId, openRightLayout); + if (update) { + this.updateLocation(); + } + } + } + + private updateLocation() { + if (this.stateObject[0].id) { + const newState = objToBase64URI(this.stateObject); + this.updateStateParam(newState); + } + } + + private getStateObjById(id: string): StateObject { + return this.stateObject.find((stateObj) => stateObj.id === id); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.html new file mode 100644 index 0000000000..5d03bcd107 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.html @@ -0,0 +1,35 @@ + +
+
+ + {{getStateName(i)}} + > + +
+ + + {{getStateName(i)}} + + +
+ + + diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.scss b/ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.scss new file mode 100644 index 0000000000..6aa314c46d --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.scss @@ -0,0 +1,53 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + overflow: hidden; + + .entity-state-controller { + .state-divider { + padding: 0 15px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + pointer-events: none; + } + + .state-entry { + pointer-events: all; + overflow: hidden; + font-size: 18px; + text-overflow: ellipsis; + white-space: nowrap; + outline: none; + } + + mat-select { + margin: 0; + } + } +} + +:host ::ng-deep { + mat-select { + .mat-select-value { + max-width: 200px; + .mat-select-value-text { + font-size: 18px; + font-weight: 700; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.ts new file mode 100644 index 0000000000..295dc57730 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.ts @@ -0,0 +1,330 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, NgZone, OnDestroy, OnInit } from '@angular/core'; +import { StateObject, StateParams } from '@core/api/widget-api.models'; +import { ActivatedRoute, Router } from '@angular/router'; +import { forkJoin, Observable, of } from 'rxjs'; +import { StateControllerState } from './state-controller.models'; +import { StateControllerComponent } from './state-controller.component'; +import { StatesControllerService } from '@home/pages/dashboard/states/states-controller.service'; +import { EntityId } from '@app/shared/models/id/entity-id'; +import { UtilsService } from '@core/services/utils.service'; +import { base64toObj, insertVariable, isEmpty, objToBase64URI } from '@app/core/utils'; +import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; +import { EntityService } from '@core/http/entity.service'; +import { EntityType } from '@shared/models/entity-type.models'; +import { map, tap } from 'rxjs/operators'; + +@Component({ + selector: 'tb-entity-state-controller', + templateUrl: './entity-state-controller.component.html', + styleUrls: ['./entity-state-controller.component.scss'] +}) +export class EntityStateControllerComponent extends StateControllerComponent implements OnInit, OnDestroy { + + selectedStateIndex = -1; + + constructor(protected router: Router, + protected route: ActivatedRoute, + protected ngZone: NgZone, + protected statesControllerService: StatesControllerService, + private utils: UtilsService, + private entityService: EntityService, + private dashboardUtils: DashboardUtilsService) { + super(router, route, ngZone, statesControllerService); + } + + ngOnInit(): void { + super.ngOnInit(); + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + } + + protected init() { + if (this.preservedState) { + this.stateObject = this.preservedState; + this.selectedStateIndex = this.stateObject.length - 1; + setTimeout(() => { + this.gotoState(this.stateObject[this.stateObject.length - 1].id, true); + }, 1); + } else { + const initialState = this.currentState; + this.stateObject = this.parseState(initialState); + this.selectedStateIndex = this.stateObject.length - 1; + setTimeout(() => { + this.gotoState(this.stateObject[this.stateObject.length - 1].id, false); + }, 1); + } + } + + protected onMobileChanged() { + } + + protected onStateIdChanged() { + } + + protected onStatesChanged() { + } + + protected onStateChanged() { + this.stateObject = this.parseState(this.currentState); + this.selectedStateIndex = this.stateObject.length - 1; + this.gotoState(this.stateObject[this.stateObject.length - 1].id, false); + } + + protected stateControllerId(): string { + return 'entity'; + } + + public getStateParams(): StateParams { + if (this.stateObject && this.stateObject.length) { + return this.stateObject[this.stateObject.length - 1].params; + } else { + return {}; + } + } + + public openState(id: string, params?: StateParams, openRightLayout?: boolean): void { + if (this.states && this.states[id]) { + this.resolveEntity(params).subscribe( + () => { + const newState: StateObject = { + id, + params + }; + this.stateObject.push(newState); + this.selectedStateIndex = this.stateObject.length - 1; + this.gotoState(this.stateObject[this.stateObject.length - 1].id, true, openRightLayout); + } + ); + } + } + + public pushAndOpenState(states: Array, openRightLayout?: boolean): void { + if (this.states) { + for (const state of states) { + if (!this.states[state.id]) { + return; + } + } + forkJoin(states.map(state => this.resolveEntity(state.params))).subscribe( + () => { + this.stateObject.push(...states); + this.selectedStateIndex = this.stateObject.length - 1; + this.gotoState(this.stateObject[this.stateObject.length - 1].id, true, openRightLayout); + } + ); + } + } + + public updateState(id: string, params?: StateParams, openRightLayout?: boolean): void { + if (!id) { + id = this.getStateId(); + } + if (this.states && this.states[id]) { + this.resolveEntity(params).subscribe( + () => { + this.stateObject[this.stateObject.length - 1] = { + id, + params + }; + this.gotoState(this.stateObject[this.stateObject.length - 1].id, true, openRightLayout); + } + ); + } + } + + public getEntityId(entityParamName: string): EntityId { + const stateParams = this.getStateParams(); + if (!entityParamName || !entityParamName.length) { + return stateParams.entityId; + } else if (stateParams[entityParamName]) { + return stateParams[entityParamName].entityId; + } + return null; + } + + public getStateId(): string { + if (this.stateObject && this.stateObject.length) { + return this.stateObject[this.stateObject.length - 1].id; + } else { + return ''; + } + } + + public getStateIdAtIndex(index: number): string { + if (this.stateObject && this.stateObject[index]) { + return this.stateObject[index].id; + } else { + return ''; + } + } + + public getStateIndex(): number { + if (this.stateObject && this.stateObject.length) { + return this.stateObject.length - 1; + } else { + return -1; + } + } + + public getStateParamsByStateId(stateId: string): StateParams { + const stateObj = this.getStateObjById(stateId); + if (stateObj) { + return stateObj.params; + } else { + return null; + } + } + + public navigatePrevState(index: number): void { + if (index < this.stateObject.length - 1) { + this.stateObject.splice(index + 1, this.stateObject.length - index - 1); + this.selectedStateIndex = this.stateObject.length - 1; + this.gotoState(this.stateObject[this.stateObject.length - 1].id, true); + } + } + + public resetState(): void { + const rootStateId = this.dashboardUtils.getRootStateId(this.states); + this.stateObject = [ { id: rootStateId, params: {} } ]; + this.gotoState(rootStateId, true); + } + + public getStateName(index: number): string { + let result = ''; + const state = this.stateObject[index]; + if (state) { + const dashboardState = this.states[state.id]; + if (dashboardState) { + let stateName = dashboardState.name; + stateName = this.utils.customTranslation(stateName, stateName); + const params = this.stateObject[index].params; + const entityName = params && params.entityName ? params.entityName : ''; + const entityLabel = params && params.entityLabel ? params.entityLabel : ''; + result = insertVariable(stateName, 'entityName', entityName); + result = insertVariable(result, 'entityLabel', entityLabel); + for (const prop of Object.keys(params)) { + if (params[prop] && params[prop].entityName) { + result = insertVariable(result, prop + ':entityName', params[prop].entityName); + } + if (params[prop] && params[prop].entityLabel) { + result = insertVariable(result, prop + ':entityLabel', params[prop].entityLabel); + } + } + } + } + return result; + } + + public selectedStateIndexChanged() { + this.navigatePrevState(this.selectedStateIndex); + } + + private parseState(stateBase64: string): StateControllerState { + let result: StateControllerState; + if (stateBase64) { + try { + result = base64toObj(stateBase64); + } catch (e) { + result = [ { id: null, params: {} } ]; + } + } + if (!result) { + result = []; + } + if (!result.length) { + result[0] = { id: null, params: {} }; + } + const rootStateId = this.dashboardUtils.getRootStateId(this.states); + if (!result[0].id) { + result[0].id = rootStateId; + } + if (!this.states[result[0].id]) { + result[0].id = rootStateId; + } + let i = result.length; + while (i--) { + if (!result[i].id || !this.states[result[i].id]) { + result.splice(i, 1); + } + } + return result; + } + + private gotoState(stateId: string, update: boolean, openRightLayout?: boolean) { + this.dashboardCtrl.openDashboardState(stateId, openRightLayout); + if (update) { + this.updateLocation(); + } + } + + private updateLocation() { + if (this.stateObject[this.stateObject.length - 1].id) { + let newState; + if (this.isDefaultState()) { + newState = null; + } else { + newState = objToBase64URI(this.stateObject); + } + this.updateStateParam(newState); + } + } + + private isDefaultState(): boolean { + if (this.stateObject.length === 1) { + const state = this.stateObject[0]; + const rootStateId = this.dashboardUtils.getRootStateId(this.states); + if (state.id === rootStateId && (!state.params || isEmpty(state.params))) { + return true; + } + } + return false; + } + + private resolveEntity(params: StateParams): Observable { + if (params && params.targetEntityParamName) { + params = params[params.targetEntityParamName]; + } + if (params && params.entityId && params.entityId.id && params.entityId.entityType) { + if (this.isEntityResolved(params)) { + return of(null); + } else { + return this.entityService.getEntity(params.entityId.entityType as EntityType, + params.entityId.id, {ignoreLoading: true, ignoreErrors: true}).pipe( + tap((entity) => { + params.entityName = entity.name; + params.entityLabel = entity.label; + }), + map(() => null) + ); + } + } else { + return of(null); + } + } + + private isEntityResolved(params: StateParams): boolean { + return !(!params.entityName || !params.entityName.length); + } + + private getStateObjById(id: string): StateObject { + return this.stateObject.find((stateObj) => stateObj.id === id); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.html new file mode 100644 index 0000000000..7f4f486278 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.html @@ -0,0 +1,151 @@ + +
+ +

dashboard.manage-states

+ + +
+ + +
+
+
+
+
+ +
+ dashboard.states + + + +
+
+ +
+ + +   + + + +
+
+
+ + + {{ 'dashboard.state-name' | translate }} + + {{ state.name }} + + + + {{ 'dashboard.state-id' | translate }} + + {{ state.id }} + + + + {{ 'dashboard.is-root-state' | translate }} + + {{state.root ? 'check_box' : 'check_box_outline_blank'}} + + + + + + +
+ + +
+
+
+ + +
+ {{ 'dashboard.no-states-text' }} +
+ + +
+
+
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.models.ts b/ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.models.ts new file mode 100644 index 0000000000..f72d6fbc42 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.models.ts @@ -0,0 +1,99 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { DashboardState } from '@shared/models/dashboard.models'; +import { CollectionViewer, DataSource } from '@angular/cdk/collections'; +import { BehaviorSubject, Observable, of, ReplaySubject } from 'rxjs'; +import { emptyPageData, PageData } from '@shared/models/page/page-data'; +import { PageLink } from '@shared/models/page/page-link'; +import { catchError, map, publishReplay, refCount } from 'rxjs/operators'; + +export interface DashboardStateInfo extends DashboardState { + id: string; +} + +export class DashboardStatesDatasource implements DataSource { + + private statesSubject = new BehaviorSubject([]); + private pageDataSubject = new BehaviorSubject>(emptyPageData()); + + public pageData$ = this.pageDataSubject.asObservable(); + + private allStates: Observable>; + + constructor(private states: {[id: string]: DashboardState }) { + } + + connect(collectionViewer: CollectionViewer): Observable> { + return this.statesSubject.asObservable(); + } + + disconnect(collectionViewer: CollectionViewer): void { + this.statesSubject.complete(); + this.pageDataSubject.complete(); + } + + loadStates(pageLink: PageLink, reload: boolean = false): Observable> { + if (reload) { + this.allStates = null; + } + const result = new ReplaySubject>(); + this.fetchStates(pageLink).pipe( + catchError(() => of(emptyPageData())), + ).subscribe( + (pageData) => { + this.statesSubject.next(pageData.data); + this.pageDataSubject.next(pageData); + result.next(pageData); + } + ); + return result; + } + + fetchStates(pageLink: PageLink): Observable> { + return this.getAllStates().pipe( + map((data) => pageLink.filterData(data)) + ); + } + + getAllStates(): Observable> { + if (!this.allStates) { + const states: DashboardStateInfo[] = []; + for (const id of Object.keys(this.states)) { + const state = this.states[id]; + states.push({id, ...state}); + } + this.allStates = of(states).pipe( + publishReplay(1), + refCount() + ); + } + return this.allStates; + } + + isEmpty(): Observable { + return this.statesSubject.pipe( + map((states) => !states.length) + ); + } + + total(): Observable { + return this.pageDataSubject.pipe( + map((pageData) => pageData.totalElements) + ); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.scss new file mode 100644 index 0000000000..739726b9a9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.scss @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .manage-dashboard-states { + .tb-entity-table { + .tb-entity-table-content { + width: 100%; + height: 100%; + background: #fff; + + .tb-entity-table-title { + padding-right: 20px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .table-container { + overflow: auto; + } + } + } + } +} + +:host ::ng-deep { + .manage-dashboard-states { + .mat-sort-header-sorted .mat-sort-header-arrow { + opacity: 1 !important; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.ts new file mode 100644 index 0000000000..c09292765b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.ts @@ -0,0 +1,250 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, Component, ElementRef, Inject, OnInit, SkipSelf, ViewChild } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { DashboardState } from '@app/shared/models/dashboard.models'; +import { PageLink } from '@shared/models/page/page-link'; +import { + DashboardStateInfo, + DashboardStatesDatasource +} from '@home/pages/dashboard/states/manage-dashboard-states-dialog.component.models'; +import { Direction, SortOrder } from '@shared/models/page/sort-order'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; +import { fromEvent, merge } from 'rxjs'; +import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; +import { DialogService } from '@core/services/dialog.service'; +import { deepClone, isUndefined } from '@core/utils'; +import { + DashboardStateDialogComponent, + DashboardStateDialogData +} from '@home/pages/dashboard/states/dashboard-state-dialog.component'; + +export interface ManageDashboardStatesDialogData { + states: {[id: string]: DashboardState }; +} + +@Component({ + selector: 'tb-manage-dashboard-states-dialog', + templateUrl: './manage-dashboard-states-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: ManageDashboardStatesDialogComponent}], + styleUrls: ['./manage-dashboard-states-dialog.component.scss'] +}) +export class ManageDashboardStatesDialogComponent extends + DialogComponent + implements OnInit, ErrorStateMatcher, AfterViewInit { + + statesFormGroup: FormGroup; + + states: {[id: string]: DashboardState }; + + displayedColumns: string[]; + pageLink: PageLink; + textSearchMode = false; + dataSource: DashboardStatesDatasource; + + submitted = false; + + @ViewChild('searchInput') searchInputField: ElementRef; + + @ViewChild(MatPaginator) paginator: MatPaginator; + @ViewChild(MatSort) sort: MatSort; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: ManageDashboardStatesDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + private translate: TranslateService, + private dialogs: DialogService, + private dialog: MatDialog) { + super(store, router, dialogRef); + + this.states = this.data.states; + this.statesFormGroup = this.fb.group({}); + + const sortOrder: SortOrder = { property: 'name', direction: Direction.ASC }; + this.pageLink = new PageLink(5, 0, null, sortOrder); + this.displayedColumns = ['name', 'id', 'root', 'actions']; + this.dataSource = new DashboardStatesDatasource(this.states); + } + + ngOnInit(): void { + this.dataSource.loadStates(this.pageLink); + } + + ngAfterViewInit() { + fromEvent(this.searchInputField.nativeElement, 'keyup') + .pipe( + debounceTime(150), + distinctUntilChanged(), + tap(() => { + this.paginator.pageIndex = 0; + this.updateData(); + }) + ) + .subscribe(); + + this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); + + merge(this.sort.sortChange, this.paginator.page) + .pipe( + tap(() => this.updateData()) + ) + .subscribe(); + } + + updateData(reload: boolean = false) { + this.pageLink.page = this.paginator.pageIndex; + this.pageLink.pageSize = this.paginator.pageSize; + this.pageLink.sortOrder.property = this.sort.active; + this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; + this.dataSource.loadStates(this.pageLink, reload); + } + + addState($event: Event) { + this.openStateDialog($event); + } + + editState($event: Event, state: DashboardStateInfo) { + this.openStateDialog($event, state); + } + + deleteState($event: Event, state: DashboardStateInfo) { + if ($event) { + $event.stopPropagation(); + } + const title = this.translate.instant('dashboard.delete-state-title'); + const content = this.translate.instant('dashboard.delete-state-text', {stateName: state.name}); + this.dialogs.confirm(title, content, this.translate.instant('action.no'), + this.translate.instant('action.yes')).subscribe( + (res) => { + if (res) { + delete this.states[state.id]; + this.onStatesUpdated(); + } + } + ); + } + + enterFilterMode() { + this.textSearchMode = true; + this.pageLink.textSearch = ''; + setTimeout(() => { + this.searchInputField.nativeElement.focus(); + this.searchInputField.nativeElement.setSelectionRange(0, 0); + }, 10); + } + + exitFilterMode() { + this.textSearchMode = false; + this.pageLink.textSearch = null; + this.paginator.pageIndex = 0; + this.updateData(); + } + + openStateDialog($event: Event, state: DashboardStateInfo = null) { + if ($event) { + $event.stopPropagation(); + } + const isAdd = state === null; + let prevStateId = null; + if (!isAdd) { + prevStateId = state.id; + } + this.dialog.open(DashboardStateDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + isAdd, + states: this.states, + state: deepClone(state) + } + }).afterClosed().subscribe( + (res) => { + if (res) { + this.saveState(res, prevStateId); + } + } + ); + } + + saveState(state: DashboardStateInfo, prevStateId: string) { + const newState: DashboardState = { + name: state.name, + root: state.root, + layouts: state.layouts + }; + if (prevStateId && prevStateId !== state.id) { + delete this.states[prevStateId]; + this.states[state.id] = newState; + } else { + this.states[state.id] = newState; + } + if (state.root) { + for (const id of Object.keys(this.states)) { + const otherState = this.states[id]; + if (id !== state.id) { + otherState.root = false; + } + } + } else { + let rootFound = false; + for (const id of Object.keys(this.states)) { + const otherState = this.states[id]; + if (otherState.root) { + rootFound = true; + break; + } + } + if (!rootFound) { + const firstStateId = Object.keys(this.states)[0]; + this.states[firstStateId].root = true; + } + } + this.onStatesUpdated(); + } + + private onStatesUpdated() { + this.statesFormGroup.markAsDirty(); + this.updateData(true); + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + this.dialogRef.close(this.states); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/state-controller.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/states/state-controller.component.ts new file mode 100644 index 0000000000..bd552f409f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/state-controller.component.ts @@ -0,0 +1,193 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { IStateControllerComponent, StateControllerState } from '@home/pages/dashboard/states/state-controller.models'; +import { IDashboardController } from '../dashboard-page.models'; +import { DashboardState } from '@app/shared/models/dashboard.models'; +import { Subscription } from 'rxjs'; +import { NgZone, OnDestroy, OnInit, Directive } from '@angular/core'; +import { ActivatedRoute, Params, Router } from '@angular/router'; +import { StatesControllerService } from '@home/pages/dashboard/states/states-controller.service'; +import { EntityId } from '@app/shared/models/id/entity-id'; +import { StateObject, StateParams } from '@app/core/api/widget-api.models'; + +@Directive() +export abstract class StateControllerComponent implements IStateControllerComponent, OnInit, OnDestroy { + + stateObject: StateControllerState = []; + dashboardCtrl: IDashboardController; + preservedState: any; + + isMobileValue: boolean; + set isMobile(val: boolean) { + if (this.isMobileValue !== val) { + this.isMobileValue = val; + if (this.inited) { + this.onMobileChanged(); + } + } + } + get isMobile(): boolean { + return this.isMobileValue; + } + + stateValue: string; + set state(val: string) { + if (this.stateValue !== val) { + this.stateValue = val; + if (this.inited) { + this.onStateIdChanged(); + } + } + } + get state(): string { + return this.stateValue; + } + + dashboardIdValue: string; + set dashboardId(val: string) { + if (this.dashboardIdValue !== val) { + this.dashboardIdValue = val; +/* if (this.inited) { + this.currentState = this.route.snapshot.queryParamMap.get('state'); + this.init(); + }*/ + } + } + get dashboardId(): string { + return this.dashboardIdValue; + } + + statesValue: { [id: string]: DashboardState }; + set states(val: { [id: string]: DashboardState }) { + if (this.statesValue !== val) { + this.statesValue = val; + if (this.inited) { + this.onStatesChanged(); + } + } + } + get states(): { [id: string]: DashboardState } { + return this.statesValue; + } + + currentState: string; + + private rxSubscriptions = new Array(); + + private inited = false; + + protected constructor(protected router: Router, + protected route: ActivatedRoute, + protected ngZone: NgZone, + protected statesControllerService: StatesControllerService) { + } + + ngOnInit(): void { + this.rxSubscriptions.push(this.route.queryParamMap.subscribe((paramMap) => { + const dashboardId = this.route.snapshot.params.dashboardId; + if (this.dashboardId === dashboardId) { + const newState = this.decodeStateParam(paramMap.get('state')); + if (this.currentState !== newState) { + this.currentState = newState; + if (this.inited) { + this.onStateChanged(); + } + } + } + })); + this.init(); + this.inited = true; + } + + ngOnDestroy(): void { + this.rxSubscriptions.forEach((subscription) => { + subscription.unsubscribe(); + }); + this.rxSubscriptions.length = 0; + } + + protected updateStateParam(newState: string) { + this.currentState = newState; + const queryParams: Params = { state: this.currentState }; + this.ngZone.run(() => { + this.router.navigate( + [], + { + relativeTo: this.route, + queryParams, + queryParamsHandling: 'merge', + }); + }); + } + + public openRightLayout(): void { + this.dashboardCtrl.openRightLayout(); + } + + public preserveState() { + this.statesControllerService.preserveStateControllerState(this.stateControllerId(), this.stateObject); + } + + public cleanupPreservedStates() { + this.statesControllerService.cleanupPreservedStates(); + } + + public reInit() { + this.preservedState = null; + this.currentState = this.decodeStateParam(this.route.snapshot.queryParamMap.get('state')); + this.init(); + } + + private decodeStateParam(stateURI: string): string{ + return stateURI !== null ? decodeURIComponent(stateURI) : null; + } + + protected abstract init(); + + protected abstract onMobileChanged(); + + protected abstract onStateIdChanged(); + + protected abstract onStatesChanged(); + + protected abstract onStateChanged(); + + protected abstract stateControllerId(): string; + + public abstract getEntityId(entityParamName: string): EntityId; + + public abstract getStateId(): string; + + public abstract getStateIdAtIndex(index: number): string; + + public abstract getStateIndex(): number; + + public abstract getStateParams(): StateParams; + + public abstract getStateParamsByStateId(stateId: string): StateParams; + + public abstract navigatePrevState(index: number): void; + + public abstract openState(id: string, params?: StateParams, openRightLayout?: boolean): void; + + public abstract pushAndOpenState(states: Array, openRightLayout?: boolean): void; + + public abstract resetState(): void; + + public abstract updateState(id?: string, params?: StateParams, openRightLayout?: boolean): void; + +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/state-controller.models.ts b/ui-ngx/src/app/modules/home/pages/dashboard/states/state-controller.models.ts new file mode 100644 index 0000000000..8607b7d1f4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/state-controller.models.ts @@ -0,0 +1,31 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { IStateController, StateObject } from '@core/api/widget-api.models'; +import { IDashboardController } from '@home/pages/dashboard/dashboard-page.models'; +import { DashboardState } from '@shared/models/dashboard.models'; + +export declare type StateControllerState = StateObject[]; + +export interface IStateControllerComponent extends IStateController { + state: string; + isMobile: boolean; + dashboardCtrl: IDashboardController; + states: {[id: string]: DashboardState }; + dashboardId: string; + preservedState: any; + reInit(): void; +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/states-component.directive.ts b/ui-ngx/src/app/modules/home/pages/dashboard/states/states-component.directive.ts new file mode 100644 index 0000000000..41b2194f4e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/states-component.directive.ts @@ -0,0 +1,124 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ComponentRef, + Directive, + Input, + OnChanges, + OnDestroy, + OnInit, + SimpleChanges, + ViewContainerRef +} from '@angular/core'; +import { DashboardState } from '@shared/models/dashboard.models'; +import { IDashboardController } from '@home/pages/dashboard/dashboard-page.models'; +import { StatesControllerService } from '@home/pages/dashboard/states/states-controller.service'; +import { IStateControllerComponent } from '@home/pages/dashboard/states/state-controller.models'; + +@Directive({ + // tslint:disable-next-line:directive-selector + selector: 'tb-states-component' +}) +export class StatesComponentDirective implements OnInit, OnDestroy, OnChanges { + + @Input() + statesControllerId: string; + + @Input() + dashboardCtrl: IDashboardController; + + @Input() + dashboardId: string; + + @Input() + states: {[id: string]: DashboardState }; + + @Input() + state: string; + + @Input() + isMobile: boolean; + + stateControllerComponentRef: ComponentRef; + stateControllerComponent: IStateControllerComponent; + + constructor(private viewContainerRef: ViewContainerRef, + private statesControllerService: StatesControllerService) { + } + + ngOnInit(): void { + this.init(); + } + + ngOnDestroy(): void { + this.destroy(); + } + + ngOnChanges(changes: SimpleChanges): void { + let reInitController = false; + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'statesControllerId') { + this.reInit(); + } else if (propName === 'states') { + this.stateControllerComponent.states = this.states; + } else if (propName === 'dashboardId') { + this.stateControllerComponent.dashboardId = this.dashboardId; + reInitController = true; + } else if (propName === 'isMobile') { + this.stateControllerComponent.isMobile = this.isMobile; + } else if (propName === 'state') { + this.stateControllerComponent.state = this.state; + } + } + } + if (reInitController) { + this.stateControllerComponent.reInit(); + } + } + + private reInit() { + this.destroy(); + this.init(); + } + + private init() { + this.viewContainerRef.clear(); + let stateControllerData = this.statesControllerService.getStateController(this.statesControllerId); + if (!stateControllerData) { + stateControllerData = this.statesControllerService.getStateController('default'); + } + const preservedState = this.statesControllerService.withdrawStateControllerState(this.statesControllerId); + const stateControllerFactory = stateControllerData.factory; + this.stateControllerComponentRef = this.viewContainerRef.createComponent(stateControllerFactory); + this.stateControllerComponent = this.stateControllerComponentRef.instance; + this.dashboardCtrl.dashboardCtx.stateController = this.stateControllerComponent; + this.stateControllerComponent.preservedState = preservedState; + this.stateControllerComponent.dashboardCtrl = this.dashboardCtrl; + this.stateControllerComponent.state = this.state; + this.stateControllerComponent.isMobile = this.isMobile; + this.stateControllerComponent.states = this.states; + this.stateControllerComponent.dashboardId = this.dashboardId; + } + + private destroy() { + if (this.stateControllerComponentRef) { + this.stateControllerComponentRef.destroy(); + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/states-controller.module.ts b/ui-ngx/src/app/modules/home/pages/dashboard/states/states-controller.module.ts new file mode 100644 index 0000000000..73b94d31d1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/states-controller.module.ts @@ -0,0 +1,52 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { HomeComponentsModule } from '@modules/home/components/home-components.module'; +import { StatesControllerService } from './states-controller.service'; +import { EntityStateControllerComponent } from './entity-state-controller.component'; +import { StatesComponentDirective } from './states-component.directive'; +import { HomeDialogsModule } from '@app/modules/home/dialogs/home-dialogs.module'; +import { DefaultStateControllerComponent } from '@home/pages/dashboard/states/default-state-controller.component'; + +@NgModule({ + declarations: [ + StatesComponentDirective, + DefaultStateControllerComponent, + EntityStateControllerComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + HomeDialogsModule + ], + exports: [ + StatesComponentDirective + ], + providers: [ + StatesControllerService + ] +}) +export class StatesControllerModule { + + constructor(private statesControllerService: StatesControllerService) { + this.statesControllerService.registerStatesController('default', DefaultStateControllerComponent); + this.statesControllerService.registerStatesController('entity', EntityStateControllerComponent); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/states-controller.service.ts b/ui-ngx/src/app/modules/home/pages/dashboard/states/states-controller.service.ts new file mode 100644 index 0000000000..83b20762bb --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/states-controller.service.ts @@ -0,0 +1,64 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ComponentFactory, ComponentFactoryResolver, Injectable, Type } from '@angular/core'; +import { deepClone } from '@core/utils'; +import { IStateControllerComponent } from '@home/pages/dashboard/states/state-controller.models'; + +export interface StateControllerData { + factory: ComponentFactory; + state?: any; +} + +@Injectable() +export class StatesControllerService { + + statesControllers: {[stateControllerId: string]: StateControllerData} = {}; + + constructor(private componentFactoryResolver: ComponentFactoryResolver) { + } + + public registerStatesController(stateControllerId: string, stateControllerComponent: Type): void { + const componentFactory = this.componentFactoryResolver.resolveComponentFactory(stateControllerComponent); + this.statesControllers[stateControllerId] = { + factory: componentFactory + }; + } + + public getStateControllers(): {[stateControllerId: string]: StateControllerData} { + return this.statesControllers; + } + + public getStateController(stateControllerId: string): StateControllerData { + return this.statesControllers[stateControllerId]; + } + + public preserveStateControllerState(stateControllerId: string, state: any) { + this.statesControllers[stateControllerId].state = deepClone(state); + } + + public withdrawStateControllerState(stateControllerId: string): any { + const state = this.statesControllers[stateControllerId].state; + this.statesControllers[stateControllerId].state = null; + return state; + } + + public cleanupPreservedStates() { + for (const stateControllerId of Object.keys(this.statesControllers)) { + this.statesControllers[stateControllerId].state = null; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-routing.module.ts b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-routing.module.ts new file mode 100644 index 0000000000..f0ffcd2c4d --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-routing.module.ts @@ -0,0 +1,56 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { EntitiesTableComponent } from '../../components/entity/entities-table.component'; +import { Authority } from '@shared/models/authority.enum'; +import { DeviceProfilesTableConfigResolver } from './device-profiles-table-config.resolver'; + +const routes: Routes = [ + { + path: 'deviceProfiles', + data: { + breadcrumb: { + label: 'device-profile.device-profiles', + icon: 'mdi:alpha-d-box' + } + }, + children: [ + { + path: '', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN], + title: 'device-profile.device-profiles' + }, + resolve: { + entitiesTableConfig: DeviceProfilesTableConfigResolver + } + } + ] + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [ + DeviceProfilesTableConfigResolver + ] +}) +export class DeviceProfileRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html new file mode 100644 index 0000000000..a803d35b1d --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html @@ -0,0 +1,76 @@ + + +
+ + device-profile.transport-type + + + {{deviceTransportTypeTranslations.get(type) | translate}} + + + + {{deviceTransportTypeHints.get(detailsForm.get('transportType').value) | translate}} + + + {{ 'device-profile.transport-type-required' | translate }} + + +
+ + +
+
+
+ +
+
+ +
+
+
+ +
+
+ + +
+
+
+ +
+
+ + +
+
+
+ + + diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts new file mode 100644 index 0000000000..cba34b4987 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts @@ -0,0 +1,49 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; +import { + DeviceProfile, + DeviceTransportType, + deviceTransportTypeHintMap, + deviceTransportTypeTranslationMap +} from '@shared/models/device.models'; + +@Component({ + selector: 'tb-device-profile-tabs', + templateUrl: './device-profile-tabs.component.html', + styleUrls: [] +}) +export class DeviceProfileTabsComponent extends EntityTabsComponent { + + deviceTransportTypes = Object.keys(DeviceTransportType); + + deviceTransportTypeTranslations = deviceTransportTypeTranslationMap; + + deviceTransportTypeHints = deviceTransportTypeHintMap; + + constructor(protected store: Store) { + super(store); + } + + ngOnInit() { + super.ngOnInit(); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile.module.ts b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile.module.ts new file mode 100644 index 0000000000..09207057ac --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile.module.ts @@ -0,0 +1,35 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { HomeComponentsModule } from '@modules/home/components/home-components.module'; +import { DeviceProfileTabsComponent } from './device-profile-tabs.component'; +import { DeviceProfileRoutingModule } from './device-profile-routing.module'; + +@NgModule({ + declarations: [ + DeviceProfileTabsComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + DeviceProfileRoutingModule + ] +}) +export class DeviceProfileModule { } diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts new file mode 100644 index 0000000000..60cb15c199 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts @@ -0,0 +1,156 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { Resolve } from '@angular/router'; +import { + checkBoxCell, + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { DialogService } from '@core/services/dialog.service'; +import { + DeviceProfile, + deviceProfileTypeTranslationMap, + deviceTransportTypeTranslationMap +} from '@shared/models/device.models'; +import { DeviceProfileService } from '@core/http/device-profile.service'; +import { DeviceProfileComponent } from '../../components/profile/device-profile.component'; +import { DeviceProfileTabsComponent } from './device-profile-tabs.component'; +import { Observable } from 'rxjs'; +import { MatDialog } from '@angular/material/dialog'; +import { + AddDeviceProfileDialogComponent, + AddDeviceProfileDialogData +} from '../../components/profile/add-device-profile-dialog.component'; + +@Injectable() +export class DeviceProfilesTableConfigResolver implements Resolve> { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor(private deviceProfileService: DeviceProfileService, + private translate: TranslateService, + private datePipe: DatePipe, + private dialogService: DialogService, + private dialog: MatDialog) { + + this.config.entityType = EntityType.DEVICE_PROFILE; + this.config.entityComponent = DeviceProfileComponent; + this.config.entityTabsComponent = DeviceProfileTabsComponent; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.DEVICE_PROFILE); + this.config.entityResources = entityTypeResources.get(EntityType.DEVICE_PROFILE); + + this.config.hideDetailsTabsOnEdit = false; + + this.config.addDialogStyle = {width: '1000px'}; + + this.config.columns.push( + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), + new EntityTableColumn('name', 'device-profile.name', '20%'), + new EntityTableColumn('type', 'device-profile.type', '20%', (deviceProfile) => { + return this.translate.instant(deviceProfileTypeTranslationMap.get(deviceProfile.type)); + }), + new EntityTableColumn('transportType', 'device-profile.transport-type', '20%', (deviceProfile) => { + return this.translate.instant(deviceTransportTypeTranslationMap.get(deviceProfile.transportType)); + }), + new EntityTableColumn('description', 'device-profile.description', '40%'), + new EntityTableColumn('isDefault', 'device-profile.default', '60px', + entity => { + return checkBoxCell(entity.default); + }) + ); + + this.config.cellActionDescriptors.push( + { + name: this.translate.instant('device-profile.set-default'), + icon: 'flag', + isEnabled: (deviceProfile) => !deviceProfile.default, + onAction: ($event, entity) => this.setDefaultDeviceProfile($event, entity) + } + ); + + this.config.deleteEntityTitle = deviceProfile => this.translate.instant('device-profile.delete-device-profile-title', + { deviceProfileName: deviceProfile.name }); + this.config.deleteEntityContent = () => this.translate.instant('device-profile.delete-device-profile-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('device-profile.delete-device-profiles-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('device-profile.delete-device-profiles-text'); + + this.config.entitiesFetchFunction = pageLink => this.deviceProfileService.getDeviceProfiles(pageLink); + this.config.loadEntity = id => this.deviceProfileService.getDeviceProfile(id.id); + this.config.saveEntity = deviceProfile => this.deviceProfileService.saveDeviceProfile(deviceProfile); + this.config.deleteEntity = id => this.deviceProfileService.deleteDeviceProfile(id.id); + this.config.onEntityAction = action => this.onDeviceProfileAction(action); + this.config.deleteEnabled = (deviceProfile) => deviceProfile && !deviceProfile.default; + this.config.entitySelectionEnabled = (deviceProfile) => deviceProfile && !deviceProfile.default; + this.config.addEntity = () => this.addDeviceProfile(); + } + + resolve(): EntityTableConfig { + this.config.tableTitle = this.translate.instant('device-profile.device-profiles'); + + return this.config; + } + + addDeviceProfile(): Observable { + return this.dialog.open(AddDeviceProfileDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + deviceProfileName: null, + transportType: null + } + }).afterClosed(); + } + + setDefaultDeviceProfile($event: Event, deviceProfile: DeviceProfile) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('device-profile.set-default-device-profile-title', {deviceProfileName: deviceProfile.name}), + this.translate.instant('device-profile.set-default-device-profile-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + this.deviceProfileService.setDefaultDeviceProfile(deviceProfile.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + onDeviceProfileAction(action: EntityAction): boolean { + switch (action.action) { + case 'setDefault': + this.setDefaultDeviceProfile(action.event, action.entity); + return true; + } + return false; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/device/data/default-device-configuration.component.html b/ui-ngx/src/app/modules/home/pages/device/data/default-device-configuration.component.html new file mode 100644 index 0000000000..001cc16801 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/default-device-configuration.component.html @@ -0,0 +1,24 @@ + +
+ +
diff --git a/ui-ngx/src/app/modules/home/pages/device/data/default-device-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/default-device-configuration.component.ts new file mode 100644 index 0000000000..07d8d33b79 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/default-device-configuration.component.ts @@ -0,0 +1,97 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { + DefaultDeviceConfiguration, + DeviceConfiguration, + DeviceProfileType +} from '@shared/models/device.models'; + +@Component({ + selector: 'tb-default-device-configuration', + templateUrl: './default-device-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DefaultDeviceConfigurationComponent), + multi: true + }] +}) +export class DefaultDeviceConfigurationComponent implements ControlValueAccessor, OnInit { + + defaultDeviceConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.defaultDeviceConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.defaultDeviceConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.defaultDeviceConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.defaultDeviceConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DefaultDeviceConfiguration | null): void { + this.defaultDeviceConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceConfiguration = null; + if (this.defaultDeviceConfigurationFormGroup.valid) { + configuration = this.defaultDeviceConfigurationFormGroup.getRawValue().configuration; + configuration.type = DeviceProfileType.DEFAULT; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device/data/default-device-transport-configuration.component.html b/ui-ngx/src/app/modules/home/pages/device/data/default-device-transport-configuration.component.html new file mode 100644 index 0000000000..68189fbb84 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/default-device-transport-configuration.component.html @@ -0,0 +1,24 @@ + +
+ +
diff --git a/ui-ngx/src/app/modules/home/pages/device/data/default-device-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/default-device-transport-configuration.component.ts new file mode 100644 index 0000000000..111c632a74 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/default-device-transport-configuration.component.ts @@ -0,0 +1,97 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { + DefaultDeviceTransportConfiguration, + DeviceTransportConfiguration, + DeviceTransportType +} from '@shared/models/device.models'; + +@Component({ + selector: 'tb-default-device-transport-configuration', + templateUrl: './default-device-transport-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DefaultDeviceTransportConfigurationComponent), + multi: true + }] +}) +export class DefaultDeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit { + + defaultDeviceTransportConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.defaultDeviceTransportConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.defaultDeviceTransportConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.defaultDeviceTransportConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.defaultDeviceTransportConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DefaultDeviceTransportConfiguration | null): void { + this.defaultDeviceTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceTransportConfiguration = null; + if (this.defaultDeviceTransportConfigurationFormGroup.valid) { + configuration = this.defaultDeviceTransportConfigurationFormGroup.getRawValue().configuration; + configuration.type = DeviceTransportType.DEFAULT; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device/data/device-configuration.component.html b/ui-ngx/src/app/modules/home/pages/device/data/device-configuration.component.html new file mode 100644 index 0000000000..ffda7bac44 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/device-configuration.component.html @@ -0,0 +1,27 @@ + +
+
+ + + + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/device/data/device-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/device-configuration.component.ts new file mode 100644 index 0000000000..3e6abc1fd6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/device-configuration.component.ts @@ -0,0 +1,103 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { DeviceConfiguration, DeviceProfileType } from '@shared/models/device.models'; +import { deepClone } from '@core/utils'; + +@Component({ + selector: 'tb-device-configuration', + templateUrl: './device-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceConfigurationComponent), + multi: true + }] +}) +export class DeviceConfigurationComponent implements ControlValueAccessor, OnInit { + + deviceProfileType = DeviceProfileType; + + deviceConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + type: DeviceProfileType; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.deviceConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.deviceConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.deviceConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.deviceConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DeviceConfiguration | null): void { + this.type = value?.type; + const configuration = deepClone(value); + if (configuration) { + delete configuration.type; + } + this.deviceConfigurationFormGroup.patchValue({configuration}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceConfiguration = null; + if (this.deviceConfigurationFormGroup.valid) { + configuration = this.deviceConfigurationFormGroup.getRawValue().configuration; + configuration.type = this.type; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device/data/device-data.component.html b/ui-ngx/src/app/modules/home/pages/device/data/device-data.component.html new file mode 100644 index 0000000000..0cff6b3693 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/device-data.component.html @@ -0,0 +1,43 @@ + +
+ + + + +
device.device-configuration
+
+
+ + +
+ + + +
device.transport-configuration
+
+
+ + +
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/device/data/device-data.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/device-data.component.ts new file mode 100644 index 0000000000..db9297f275 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/device-data.component.ts @@ -0,0 +1,107 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { + DeviceData, + deviceProfileTypeConfigurationInfoMap, + deviceTransportTypeConfigurationInfoMap +} from '@shared/models/device.models'; + +@Component({ + selector: 'tb-device-data', + templateUrl: './device-data.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceDataComponent), + multi: true + }] +}) +export class DeviceDataComponent implements ControlValueAccessor, OnInit { + + deviceDataFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + displayDeviceConfiguration: boolean; + displayTransportConfiguration: boolean; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.deviceDataFormGroup = this.fb.group({ + configuration: [null, Validators.required], + transportConfiguration: [null, Validators.required] + }); + this.deviceDataFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.deviceDataFormGroup.disable({emitEvent: false}); + } else { + this.deviceDataFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DeviceData | null): void { + const deviceProfileType = value?.configuration?.type; + this.displayDeviceConfiguration = deviceProfileType && + deviceProfileTypeConfigurationInfoMap.get(deviceProfileType).hasDeviceConfiguration; + const deviceTransportType = value?.transportConfiguration?.type; + this.displayTransportConfiguration = deviceTransportType && + deviceTransportTypeConfigurationInfoMap.get(deviceTransportType).hasDeviceConfiguration; + this.deviceDataFormGroup.patchValue({configuration: value?.configuration}, {emitEvent: false}); + this.deviceDataFormGroup.patchValue({transportConfiguration: value?.transportConfiguration}, {emitEvent: false}); + } + + private updateModel() { + let deviceData: DeviceData = null; + if (this.deviceDataFormGroup.valid) { + deviceData = this.deviceDataFormGroup.getRawValue(); + } + this.propagateChange(deviceData); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.html b/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.html new file mode 100644 index 0000000000..6762202f06 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.html @@ -0,0 +1,39 @@ + +
+
+ + + + + + + + + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.ts new file mode 100644 index 0000000000..505f494551 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.ts @@ -0,0 +1,106 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { + DeviceTransportConfiguration, + DeviceTransportType +} from '@shared/models/device.models'; +import { deepClone } from '@core/utils'; + +@Component({ + selector: 'tb-device-transport-configuration', + templateUrl: './device-transport-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceTransportConfigurationComponent), + multi: true + }] +}) +export class DeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit { + + deviceTransportType = DeviceTransportType; + + deviceTransportConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + transportType: DeviceTransportType; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.deviceTransportConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.deviceTransportConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.deviceTransportConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.deviceTransportConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DeviceTransportConfiguration | null): void { + this.transportType = value?.type; + const configuration = deepClone(value); + if (configuration) { + delete configuration.type; + } + this.deviceTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceTransportConfiguration = null; + if (this.deviceTransportConfigurationFormGroup.valid) { + configuration = this.deviceTransportConfigurationFormGroup.getRawValue().configuration; + configuration.type = this.transportType; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.html b/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.html new file mode 100644 index 0000000000..4a90cf6ecf --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.html @@ -0,0 +1,24 @@ + +
+ +
diff --git a/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.ts new file mode 100644 index 0000000000..f20f481824 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.ts @@ -0,0 +1,96 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { + DeviceTransportConfiguration, + DeviceTransportType, Lwm2mDeviceTransportConfiguration +} from '@shared/models/device.models'; + +@Component({ + selector: 'tb-lwm2m-device-transport-configuration', + templateUrl: './lwm2m-device-transport-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => Lwm2mDeviceTransportConfigurationComponent), + multi: true + }] +}) +export class Lwm2mDeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit { + + lwm2mDeviceTransportConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.lwm2mDeviceTransportConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.lwm2mDeviceTransportConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.lwm2mDeviceTransportConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.lwm2mDeviceTransportConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: Lwm2mDeviceTransportConfiguration | null): void { + this.lwm2mDeviceTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceTransportConfiguration = null; + if (this.lwm2mDeviceTransportConfigurationFormGroup.valid) { + configuration = this.lwm2mDeviceTransportConfigurationFormGroup.getRawValue().configuration; + // configuration.type = DeviceTransportType.LWM2M; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device/data/mqtt-device-transport-configuration.component.html b/ui-ngx/src/app/modules/home/pages/device/data/mqtt-device-transport-configuration.component.html new file mode 100644 index 0000000000..9e7390e8d7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/mqtt-device-transport-configuration.component.html @@ -0,0 +1,24 @@ + +
+ +
diff --git a/ui-ngx/src/app/modules/home/pages/device/data/mqtt-device-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/mqtt-device-transport-configuration.component.ts new file mode 100644 index 0000000000..68348b8017 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/mqtt-device-transport-configuration.component.ts @@ -0,0 +1,96 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { + DeviceTransportConfiguration, + DeviceTransportType, MqttDeviceTransportConfiguration +} from '@shared/models/device.models'; + +@Component({ + selector: 'tb-mqtt-device-transport-configuration', + templateUrl: './mqtt-device-transport-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MqttDeviceTransportConfigurationComponent), + multi: true + }] +}) +export class MqttDeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit { + + mqttDeviceTransportConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.mqttDeviceTransportConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.mqttDeviceTransportConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.mqttDeviceTransportConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.mqttDeviceTransportConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: MqttDeviceTransportConfiguration | null): void { + this.mqttDeviceTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceTransportConfiguration = null; + if (this.mqttDeviceTransportConfigurationFormGroup.valid) { + configuration = this.mqttDeviceTransportConfigurationFormGroup.getRawValue().configuration; + configuration.type = DeviceTransportType.MQTT; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html new file mode 100644 index 0000000000..2936b60911 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html @@ -0,0 +1,52 @@ + +
+ +

device.device-credentials

+ + +
+ + +
+
+
+ + +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts new file mode 100644 index 0000000000..94083cc10b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts @@ -0,0 +1,111 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms'; +import { DeviceService } from '@core/http/device.service'; +import { credentialTypeNames, DeviceCredentials, DeviceCredentialsType } from '@shared/models/device.models'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; + +export interface DeviceCredentialsDialogData { + isReadOnly: boolean; + deviceId: string; +} + +@Component({ + selector: 'tb-device-credentials-dialog', + templateUrl: './device-credentials-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: DeviceCredentialsDialogComponent}], + styleUrls: [] +}) +export class DeviceCredentialsDialogComponent extends + DialogComponent implements OnInit, ErrorStateMatcher { + + deviceCredentialsFormGroup: FormGroup; + + isReadOnly: boolean; + + deviceCredentials: DeviceCredentials; + + submitted = false; + + deviceCredentialsType = DeviceCredentialsType; + + credentialsTypes = Object.keys(DeviceCredentialsType); + + credentialTypeNamesMap = credentialTypeNames; + + hidePassword = true; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: DeviceCredentialsDialogData, + private deviceService: DeviceService, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + public fb: FormBuilder) { + super(store, router, dialogRef); + + this.isReadOnly = data.isReadOnly; + } + + ngOnInit(): void { + this.deviceCredentialsFormGroup = this.fb.group({ + credential: [null] + }); + if (this.isReadOnly) { + this.deviceCredentialsFormGroup.disable({emitEvent: false}); + } + this.loadDeviceCredentials(); + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + loadDeviceCredentials() { + this.deviceService.getDeviceCredentials(this.data.deviceId).subscribe( + (deviceCredentials) => { + this.deviceCredentials = deviceCredentials; + this.deviceCredentialsFormGroup.patchValue({ + credential: deviceCredentials + }, {emitEvent: false}); + } + ); + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + const deviceCredentialsValue = this.deviceCredentialsFormGroup.value.credential; + this.deviceCredentials = {...this.deviceCredentials, ...deviceCredentialsValue}; + this.deviceService.saveDeviceCredentials(this.deviceCredentials).subscribe( + (deviceCredentials) => { + this.dialogRef.close(deviceCredentials); + } + ); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device/device-routing.module.ts b/ui-ngx/src/app/modules/home/pages/device/device-routing.module.ts new file mode 100644 index 0000000000..a44c38cdc5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/device-routing.module.ts @@ -0,0 +1,50 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { EntitiesTableComponent } from '../../components/entity/entities-table.component'; +import { Authority } from '@shared/models/authority.enum'; +import { DevicesTableConfigResolver } from '@modules/home/pages/device/devices-table-config.resolver'; + +const routes: Routes = [ + { + path: 'devices', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + title: 'device.devices', + devicesType: 'tenant', + breadcrumb: { + label: 'device.devices', + icon: 'devices_other' + } + }, + resolve: { + entitiesTableConfig: DevicesTableConfigResolver + } + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [ + DevicesTableConfigResolver + ] +}) +export class DeviceRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.html b/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.html new file mode 100644 index 0000000000..21a7469850 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.html @@ -0,0 +1,23 @@ + + + diff --git a/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.scss b/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.scss new file mode 100644 index 0000000000..7a61453421 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.scss @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../scss/constants'; + +:host { + flex: 1; + display: flex; + justify-content: flex-start; + min-width: 150px; +} + +:host ::ng-deep { + tb-device-profile-autocomplete { + width: 100%; + + mat-form-field { + font-size: 16px; + + .mat-form-field-wrapper { + padding-bottom: 0; + } + + .mat-form-field-underline { + bottom: 0; + } + + @media #{$mat-xs} { + width: 100%; + + .mat-form-field-infix { + width: auto !important; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.ts new file mode 100644 index 0000000000..e78a5f73db --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.ts @@ -0,0 +1,43 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTableHeaderComponent } from '../../components/entity/entity-table-header.component'; +import { DeviceInfo } from '@app/shared/models/device.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { DeviceProfileId } from '../../../../shared/models/id/device-profile-id'; + +@Component({ + selector: 'tb-device-table-header', + templateUrl: './device-table-header.component.html', + styleUrls: ['./device-table-header.component.scss'] +}) +export class DeviceTableHeaderComponent extends EntityTableHeaderComponent { + + entityType = EntityType; + + constructor(protected store: Store) { + super(store); + } + + deviceProfileChanged(deviceProfileId: DeviceProfileId) { + this.entitiesTableConfig.componentsData.deviceProfileId = deviceProfileId; + this.entitiesTableConfig.table.resetSortAndFilter(true); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/device/device-tabs.component.html b/ui-ngx/src/app/modules/home/pages/device/device-tabs.component.html new file mode 100644 index 0000000000..18a635c492 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/device-tabs.component.html @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/app/modules/home/pages/device/device-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/device/device-tabs.component.ts new file mode 100644 index 0000000000..d76c01fd70 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/device-tabs.component.ts @@ -0,0 +1,38 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { DeviceInfo } from '@shared/models/device.models'; +import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; + +@Component({ + selector: 'tb-device-tabs', + templateUrl: './device-tabs.component.html', + styleUrls: [] +}) +export class DeviceTabsComponent extends EntityTabsComponent { + + constructor(protected store: Store) { + super(store); + } + + ngOnInit() { + super.ngOnInit(); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/device/device.component.html b/ui-ngx/src/app/modules/home/pages/device/device.component.html new file mode 100644 index 0000000000..e9371b4708 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/device.component.html @@ -0,0 +1,112 @@ + +
+ + + + + +
+ + +
+
+
+ + device.assignedToCustomer + + +
+ {{ 'device.device-public' | translate }} +
+
+
+ + device.name + + + {{ 'device.name-required' | translate }} + + + + + + device.label + + + + +
+ + {{ 'device.is-gateway' | translate }} + + + device.description + + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/device/device.component.scss b/ui-ngx/src/app/modules/home/pages/device/device.component.scss new file mode 100644 index 0000000000..bb3718c2da --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/device.component.scss @@ -0,0 +1,18 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + +} diff --git a/ui-ngx/src/app/modules/home/pages/device/device.component.ts b/ui-ngx/src/app/modules/home/pages/device/device.component.ts new file mode 100644 index 0000000000..833919eb78 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/device.component.ts @@ -0,0 +1,170 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityComponent } from '../../components/entity/entity.component'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { + createDeviceConfiguration, + createDeviceProfileConfiguration, createDeviceTransportConfiguration, + DeviceData, + DeviceInfo, + DeviceProfileData, + DeviceProfileInfo, + DeviceProfileType, + DeviceTransportType +} from '@shared/models/device.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { TranslateService } from '@ngx-translate/core'; +import { DeviceService } from '@core/http/device.service'; +import { ClipboardService } from 'ngx-clipboard'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; + +@Component({ + selector: 'tb-device', + templateUrl: './device.component.html', + styleUrls: ['./device.component.scss'] +}) +export class DeviceComponent extends EntityComponent { + + entityType = EntityType; + + deviceScope: 'tenant' | 'customer' | 'customer_user'; + + constructor(protected store: Store, + protected translate: TranslateService, + private deviceService: DeviceService, + private clipboardService: ClipboardService, + @Inject('entity') protected entityValue: DeviceInfo, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + public fb: FormBuilder) { + super(store, fb, entityValue, entitiesTableConfigValue); + } + + ngOnInit() { + this.deviceScope = this.entitiesTableConfig.componentsData.deviceScope; + super.ngOnInit(); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + isAssignedToCustomer(entity: DeviceInfo): boolean { + return entity && entity.customerId && entity.customerId.id !== NULL_UUID; + } + + buildForm(entity: DeviceInfo): FormGroup { + return this.fb.group( + { + name: [entity ? entity.name : '', [Validators.required]], + deviceProfileId: [entity ? entity.deviceProfileId : null, [Validators.required]], + label: [entity ? entity.label : ''], + deviceData: [entity ? entity.deviceData : null, [Validators.required]], + additionalInfo: this.fb.group( + { + gateway: [entity && entity.additionalInfo ? entity.additionalInfo.gateway : false], + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''], + } + ) + } + ); + } + + updateForm(entity: DeviceInfo) { + this.entityForm.patchValue({name: entity.name}); + this.entityForm.patchValue({deviceProfileId: entity.deviceProfileId}); + this.entityForm.patchValue({label: entity.label}); + this.entityForm.patchValue({deviceData: entity.deviceData}); + this.entityForm.patchValue({additionalInfo: + {gateway: entity.additionalInfo ? entity.additionalInfo.gateway : false}}); + this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}}); + } + + + onDeviceIdCopied($event) { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('device.idCopiedMessage'), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } + + copyAccessToken($event) { + if (this.entity.id) { + this.deviceService.getDeviceCredentials(this.entity.id.id, true).subscribe( + (deviceCredentials) => { + const credentialsId = deviceCredentials.credentialsId; + if (this.clipboardService.copyFromContent(credentialsId)) { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('device.accessTokenCopiedMessage'), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } + } + ); + } + } + + onDeviceProfileUpdated() { + this.entitiesTableConfig.table.updateData(false); + } + + onDeviceProfileChanged(deviceProfile: DeviceProfileInfo) { + if (deviceProfile && this.isEdit) { + const deviceProfileType: DeviceProfileType = deviceProfile.type; + const deviceTransportType: DeviceTransportType = deviceProfile.transportType; + let deviceData: DeviceData = this.entityForm.getRawValue().deviceData; + if (!deviceData) { + deviceData = { + configuration: createDeviceConfiguration(deviceProfileType), + transportConfiguration: createDeviceTransportConfiguration(deviceTransportType) + }; + this.entityForm.patchValue({deviceData}); + this.entityForm.markAsDirty(); + } else { + let changed = false; + if (deviceData.configuration.type !== deviceProfileType) { + deviceData.configuration = createDeviceConfiguration(deviceProfileType); + changed = true; + } + if (deviceData.transportConfiguration.type !== deviceTransportType) { + deviceData.transportConfiguration = createDeviceTransportConfiguration(deviceTransportType); + changed = true; + } + if (changed) { + this.entityForm.patchValue({deviceData}); + this.entityForm.markAsDirty(); + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device/device.module.ts b/ui-ngx/src/app/modules/home/pages/device/device.module.ts new file mode 100644 index 0000000000..53ee34d570 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/device.module.ts @@ -0,0 +1,57 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { DeviceComponent } from '@modules/home/pages/device/device.component'; +import { DeviceRoutingModule } from './device-routing.module'; +import { DeviceTableHeaderComponent } from '@modules/home/pages/device/device-table-header.component'; +import { DeviceCredentialsDialogComponent } from '@modules/home/pages/device/device-credentials-dialog.component'; +import { HomeDialogsModule } from '../../dialogs/home-dialogs.module'; +import { HomeComponentsModule } from '@modules/home/components/home-components.module'; +import { DeviceTabsComponent } from '@home/pages/device/device-tabs.component'; +import { DefaultDeviceConfigurationComponent } from './data/default-device-configuration.component'; +import { DeviceConfigurationComponent } from './data/device-configuration.component'; +import { DeviceDataComponent } from './data/device-data.component'; +import { DefaultDeviceTransportConfigurationComponent } from './data/default-device-transport-configuration.component'; +import { DeviceTransportConfigurationComponent } from './data/device-transport-configuration.component'; +import { MqttDeviceTransportConfigurationComponent } from './data/mqtt-device-transport-configuration.component'; +import { Lwm2mDeviceTransportConfigurationComponent } from './data/lwm2m-device-transport-configuration.component'; + +@NgModule({ + declarations: [ + DefaultDeviceConfigurationComponent, + DeviceConfigurationComponent, + DefaultDeviceTransportConfigurationComponent, + MqttDeviceTransportConfigurationComponent, + Lwm2mDeviceTransportConfigurationComponent, + DeviceTransportConfigurationComponent, + DeviceDataComponent, + DeviceComponent, + DeviceTabsComponent, + DeviceTableHeaderComponent, + DeviceCredentialsDialogComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + HomeDialogsModule, + DeviceRoutingModule + ] +}) +export class DeviceModule { } diff --git a/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts new file mode 100644 index 0000000000..0aaf8e62e7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts @@ -0,0 +1,502 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; + +import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router'; +import { + CellActionDescriptor, + checkBoxCell, + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig, + GroupActionDescriptor, + HeaderActionDescriptor +} from '@home/models/entity/entities-table-config.models'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models'; +import { Device, DeviceCredentials, DeviceInfo } from '@app/shared/models/device.models'; +import { DeviceComponent } from '@modules/home/pages/device/device.component'; +import { forkJoin, Observable, of } from 'rxjs'; +import { select, Store } from '@ngrx/store'; +import { selectAuthUser } from '@core/auth/auth.selectors'; +import { map, mergeMap, take, tap } from 'rxjs/operators'; +import { AppState } from '@core/core.state'; +import { DeviceService } from '@app/core/http/device.service'; +import { Authority } from '@app/shared/models/authority.enum'; +import { CustomerService } from '@core/http/customer.service'; +import { Customer } from '@app/shared/models/customer.model'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { BroadcastService } from '@core/services/broadcast.service'; +import { DeviceTableHeaderComponent } from '@modules/home/pages/device/device-table-header.component'; +import { MatDialog } from '@angular/material/dialog'; +import { + DeviceCredentialsDialogComponent, + DeviceCredentialsDialogData +} from '@modules/home/pages/device/device-credentials-dialog.component'; +import { DialogService } from '@core/services/dialog.service'; +import { + AssignToCustomerDialogComponent, + AssignToCustomerDialogData +} from '@modules/home/dialogs/assign-to-customer-dialog.component'; +import { DeviceId } from '@app/shared/models/id/device-id'; +import { + AddEntitiesToCustomerDialogComponent, + AddEntitiesToCustomerDialogData +} from '../../dialogs/add-entities-to-customer-dialog.component'; +import { DeviceTabsComponent } from '@home/pages/device/device-tabs.component'; +import { HomeDialogsService } from '@home/dialogs/home-dialogs.service'; +import { DeviceWizardDialogComponent } from '@home/components/wizard/device-wizard-dialog.component'; +import { BaseData, HasId } from '@shared/models/base-data'; + +@Injectable() +export class DevicesTableConfigResolver implements Resolve> { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + private customerId: string; + + constructor(private store: Store, + private broadcast: BroadcastService, + private deviceService: DeviceService, + private customerService: CustomerService, + private dialogService: DialogService, + private homeDialogs: HomeDialogsService, + private translate: TranslateService, + private datePipe: DatePipe, + private router: Router, + private dialog: MatDialog) { + + this.config.entityType = EntityType.DEVICE; + this.config.entityComponent = DeviceComponent; + this.config.entityTabsComponent = DeviceTabsComponent; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.DEVICE); + this.config.entityResources = entityTypeResources.get(EntityType.DEVICE); + + this.config.addDialogStyle = {width: '600px'}; + + this.config.deleteEntityTitle = device => this.translate.instant('device.delete-device-title', { deviceName: device.name }); + this.config.deleteEntityContent = () => this.translate.instant('device.delete-device-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('device.delete-devices-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('device.delete-devices-text'); + + this.config.loadEntity = id => this.deviceService.getDeviceInfo(id.id); + this.config.saveEntity = device => { + return this.deviceService.saveDevice(device).pipe( + tap(() => { + this.broadcast.broadcast('deviceSaved'); + }), + mergeMap((savedDevice) => this.deviceService.getDeviceInfo(savedDevice.id.id) + )); + }; + this.config.onEntityAction = action => this.onDeviceAction(action); + this.config.detailsReadonly = () => this.config.componentsData.deviceScope === 'customer_user'; + + this.config.headerComponent = DeviceTableHeaderComponent; + + } + + resolve(route: ActivatedRouteSnapshot): Observable> { + const routeParams = route.params; + this.config.componentsData = { + deviceScope: route.data.devicesType, + deviceProfileId: null + }; + this.customerId = routeParams.customerId; + return this.store.pipe(select(selectAuthUser), take(1)).pipe( + tap((authUser) => { + if (authUser.authority === Authority.CUSTOMER_USER) { + this.config.componentsData.deviceScope = 'customer_user'; + this.customerId = authUser.customerId; + } + }), + mergeMap(() => + this.customerId ? this.customerService.getCustomer(this.customerId) : of(null as Customer) + ), + map((parentCustomer) => { + if (parentCustomer) { + if (parentCustomer.additionalInfo && parentCustomer.additionalInfo.isPublic) { + this.config.tableTitle = this.translate.instant('customer.public-devices'); + } else { + this.config.tableTitle = parentCustomer.title + ': ' + this.translate.instant('device.devices'); + } + } else { + this.config.tableTitle = this.translate.instant('device.devices'); + } + this.config.columns = this.configureColumns(this.config.componentsData.deviceScope); + this.configureEntityFunctions(this.config.componentsData.deviceScope); + this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.deviceScope); + this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.deviceScope); + this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.deviceScope); + this.config.addEnabled = this.config.componentsData.deviceScope !== 'customer_user'; + this.config.entitiesDeleteEnabled = this.config.componentsData.deviceScope === 'tenant'; + this.config.deleteEnabled = () => this.config.componentsData.deviceScope === 'tenant'; + return this.config; + }) + ); + } + + configureColumns(deviceScope: string): Array> { + const columns: Array> = [ + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), + new EntityTableColumn('name', 'device.name', '25%'), + new EntityTableColumn('deviceProfileName', 'device-profile.device-profile', '25%'), + new EntityTableColumn('label', 'device.label', '25%') + ]; + if (deviceScope === 'tenant') { + columns.push( + new EntityTableColumn('customerTitle', 'customer.customer', '25%'), + new EntityTableColumn('customerIsPublic', 'device.public', '60px', + entity => { + return checkBoxCell(entity.customerIsPublic); + }, () => ({}), false), + ); + } + columns.push( + new EntityTableColumn('gateway', 'device.is-gateway', '60px', + entity => { + return checkBoxCell(entity.additionalInfo && entity.additionalInfo.gateway); + }, () => ({}), false) + ); + return columns; + } + + configureEntityFunctions(deviceScope: string): void { + if (deviceScope === 'tenant') { + this.config.entitiesFetchFunction = pageLink => + this.deviceService.getTenantDeviceInfosByDeviceProfileId(pageLink, + this.config.componentsData.deviceProfileId !== null ? + this.config.componentsData.deviceProfileId.id : ''); + this.config.deleteEntity = id => this.deviceService.deleteDevice(id.id); + } else { + this.config.entitiesFetchFunction = pageLink => + this.deviceService.getCustomerDeviceInfosByDeviceProfileId(this.customerId, pageLink, + this.config.componentsData.deviceProfileId !== null ? + this.config.componentsData.deviceProfileId.id : ''); + this.config.deleteEntity = id => this.deviceService.unassignDeviceFromCustomer(id.id); + } + } + + configureCellActions(deviceScope: string): Array> { + const actions: Array> = []; + if (deviceScope === 'tenant') { + actions.push( + { + name: this.translate.instant('device.make-public'), + icon: 'share', + isEnabled: (entity) => (!entity.customerId || entity.customerId.id === NULL_UUID), + onAction: ($event, entity) => this.makePublic($event, entity) + }, + { + name: this.translate.instant('device.assign-to-customer'), + icon: 'assignment_ind', + isEnabled: (entity) => (!entity.customerId || entity.customerId.id === NULL_UUID), + onAction: ($event, entity) => this.assignToCustomer($event, [entity.id]) + }, + { + name: this.translate.instant('device.unassign-from-customer'), + icon: 'assignment_return', + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && !entity.customerIsPublic), + onAction: ($event, entity) => this.unassignFromCustomer($event, entity) + }, + { + name: this.translate.instant('device.make-private'), + icon: 'reply', + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && entity.customerIsPublic), + onAction: ($event, entity) => this.unassignFromCustomer($event, entity) + }, + { + name: this.translate.instant('device.manage-credentials'), + icon: 'security', + isEnabled: () => true, + onAction: ($event, entity) => this.manageCredentials($event, entity) + } + ); + } + if (deviceScope === 'customer') { + actions.push( + { + name: this.translate.instant('device.unassign-from-customer'), + icon: 'assignment_return', + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && !entity.customerIsPublic), + onAction: ($event, entity) => this.unassignFromCustomer($event, entity) + }, + { + name: this.translate.instant('device.make-private'), + icon: 'reply', + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && entity.customerIsPublic), + onAction: ($event, entity) => this.unassignFromCustomer($event, entity) + }, + { + name: this.translate.instant('device.manage-credentials'), + icon: 'security', + isEnabled: () => true, + onAction: ($event, entity) => this.manageCredentials($event, entity) + } + ); + } + if (deviceScope === 'customer_user') { + actions.push( + { + name: this.translate.instant('device.view-credentials'), + icon: 'security', + isEnabled: () => true, + onAction: ($event, entity) => this.manageCredentials($event, entity) + } + ); + } + return actions; + } + + configureGroupActions(deviceScope: string): Array> { + const actions: Array> = []; + if (deviceScope === 'tenant') { + actions.push( + { + name: this.translate.instant('device.assign-devices'), + icon: 'assignment_ind', + isEnabled: true, + onAction: ($event, entities) => this.assignToCustomer($event, entities.map((entity) => entity.id)) + } + ); + } + if (deviceScope === 'customer') { + actions.push( + { + name: this.translate.instant('device.unassign-devices'), + icon: 'assignment_return', + isEnabled: true, + onAction: ($event, entities) => this.unassignDevicesFromCustomer($event, entities) + } + ); + } + return actions; + } + + configureAddActions(deviceScope: string): Array { + const actions: Array = []; + if (deviceScope === 'tenant') { + actions.push( + { + name: this.translate.instant('device.add-device-text'), + icon: 'insert_drive_file', + isEnabled: () => true, + onAction: ($event) => this.deviceWizard($event) + }, + { + name: this.translate.instant('device.import'), + icon: 'file_upload', + isEnabled: () => true, + onAction: ($event) => this.importDevices($event) + }, + ); + } + if (deviceScope === 'customer') { + actions.push( + { + name: this.translate.instant('device.assign-new-device'), + icon: 'add', + isEnabled: () => true, + onAction: ($event) => this.addDevicesToCustomer($event) + } + ); + } + return actions; + } + + importDevices($event: Event) { + this.homeDialogs.importEntities(EntityType.DEVICE).subscribe((res) => { + if (res) { + this.broadcast.broadcast('deviceSaved'); + this.config.table.updateData(); + } + }); + } + + deviceWizard($event: Event) { + this.dialog.open>, + boolean>(DeviceWizardDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + entitiesTableConfig: this.config.table.entitiesTableConfig + } + }).afterClosed().subscribe( + (res) => { + if (res) { + this.config.table.updateData(); + } + } + ); + } + + addDevicesToCustomer($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(AddEntitiesToCustomerDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + customerId: this.customerId, + entityType: EntityType.DEVICE + } + }).afterClosed() + .subscribe((res) => { + if (res) { + this.config.table.updateData(); + } + }); + } + + makePublic($event: Event, device: Device) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('device.make-public-device-title', {deviceName: device.name}), + this.translate.instant('device.make-public-device-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + this.deviceService.makeDevicePublic(device.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + assignToCustomer($event: Event, deviceIds: Array) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(AssignToCustomerDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + entityIds: deviceIds, + entityType: EntityType.DEVICE + } + }).afterClosed() + .subscribe((res) => { + if (res) { + this.config.table.updateData(); + } + }); + } + + unassignFromCustomer($event: Event, device: DeviceInfo) { + if ($event) { + $event.stopPropagation(); + } + const isPublic = device.customerIsPublic; + let title; + let content; + if (isPublic) { + title = this.translate.instant('device.make-private-device-title', {deviceName: device.name}); + content = this.translate.instant('device.make-private-device-text'); + } else { + title = this.translate.instant('device.unassign-device-title', {deviceName: device.name}); + content = this.translate.instant('device.unassign-device-text'); + } + this.dialogService.confirm( + title, + content, + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + this.deviceService.unassignDeviceFromCustomer(device.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + unassignDevicesFromCustomer($event: Event, devices: Array) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('device.unassign-devices-title', {count: devices.length}), + this.translate.instant('device.unassign-devices-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + const tasks: Observable[] = []; + devices.forEach( + (device) => { + tasks.push(this.deviceService.unassignDeviceFromCustomer(device.id.id)); + } + ); + forkJoin(tasks).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + manageCredentials($event: Event, device: Device) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(DeviceCredentialsDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + deviceId: device.id.id, + isReadOnly: this.config.componentsData.deviceScope === 'customer_user' + } + }); + } + + onDeviceAction(action: EntityAction): boolean { + switch (action.action) { + case 'makePublic': + this.makePublic(action.event, action.entity); + return true; + case 'assignToCustomer': + this.assignToCustomer(action.event, [action.entity.id]); + return true; + case 'unassignFromCustomer': + this.unassignFromCustomer(action.event, action.entity); + return true; + case 'manageCredentials': + this.manageCredentials(action.event, action.entity); + return true; + } + return false; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-routing.module.ts b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-routing.module.ts new file mode 100644 index 0000000000..58b3416eb2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-routing.module.ts @@ -0,0 +1,50 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { EntitiesTableComponent } from '../../components/entity/entities-table.component'; +import { Authority } from '@shared/models/authority.enum'; +import { EntityViewsTableConfigResolver } from '@modules/home/pages/entity-view/entity-views-table-config.resolver'; + +const routes: Routes = [ + { + path: 'entityViews', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + title: 'entity-view.entity-views', + entityViewsType: 'tenant', + breadcrumb: { + label: 'entity-view.entity-views', + icon: 'view_quilt' + } + }, + resolve: { + entitiesTableConfig: EntityViewsTableConfigResolver + } + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [ + EntityViewsTableConfigResolver + ] +}) +export class EntityViewRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-table-header.component.html b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-table-header.component.html new file mode 100644 index 0000000000..c41fe495db --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-table-header.component.html @@ -0,0 +1,23 @@ + + + diff --git a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-table-header.component.scss b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-table-header.component.scss new file mode 100644 index 0000000000..bd3bc86b1c --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-table-header.component.scss @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../scss/constants'; + +:host { + flex: 1; + display: flex; + justify-content: flex-start; + min-width: 150px; +} + +:host ::ng-deep { + tb-entity-subtype-select { + width: 100%; + + mat-form-field { + font-size: 16px; + + .mat-form-field-wrapper { + padding-bottom: 0; + } + + .mat-form-field-underline { + bottom: 0; + } + + @media #{$mat-xs} { + width: 100%; + + .mat-form-field-infix { + width: auto !important; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-table-header.component.ts new file mode 100644 index 0000000000..104009ac4d --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-table-header.component.ts @@ -0,0 +1,42 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTableHeaderComponent } from '../../components/entity/entity-table-header.component'; +import { EntityType } from '@shared/models/entity-type.models'; +import { EntityViewInfo } from '@app/shared/models/entity-view.models'; + +@Component({ + selector: 'tb-entity-view-table-header', + templateUrl: './entity-view-table-header.component.html', + styleUrls: ['./entity-view-table-header.component.scss'] +}) +export class EntityViewTableHeaderComponent extends EntityTableHeaderComponent { + + entityType = EntityType; + + constructor(protected store: Store) { + super(store); + } + + entityViewTypeChanged(entityViewType: string) { + this.entitiesTableConfig.componentsData.entityViewType = entityViewType; + this.entitiesTableConfig.table.resetSortAndFilter(true); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-tabs.component.html b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-tabs.component.html new file mode 100644 index 0000000000..c0a4c76826 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-tabs.component.html @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-tabs.component.ts new file mode 100644 index 0000000000..5c7b330063 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-tabs.component.ts @@ -0,0 +1,38 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; +import { EntityViewInfo } from '@app/shared/models/entity-view.models'; + +@Component({ + selector: 'tb-entity-view-tabs', + templateUrl: './entity-view-tabs.component.html', + styleUrls: [] +}) +export class EntityViewTabsComponent extends EntityTabsComponent { + + constructor(protected store: Store) { + super(store); + } + + ngOnInit() { + super.ngOnInit(); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html new file mode 100644 index 0000000000..538c09feea --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html @@ -0,0 +1,156 @@ + +
+ + + + +
+ +
+
+
+ + entity-view.assignedToCustomer + + +
+ {{ 'entity-view.entity-view-public' | translate }} +
+
+
+ + entity-view.name + + + {{ 'entity-view.name-required' | translate }} + + + + +
+ + + +
+
+ + + + +
entity-view.attributes-propagation
+
+
+
entity-view.attributes-propagation-hint
+ + + + + + + + + +
+ + + +
entity-view.timeseries-data
+
+
+
entity-view.timeseries-data-hint
+ + + +
+
+
+ + +
+ + entity-view.description + + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.scss b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.scss new file mode 100644 index 0000000000..9356738028 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.scss @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .mat-accordion-container { + margin-bottom: 16px; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.ts b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.ts new file mode 100644 index 0000000000..1a1895dada --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.ts @@ -0,0 +1,140 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityComponent } from '../../components/entity/entity.component'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { EntityType } from '@shared/models/entity-type.models'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { TranslateService } from '@ngx-translate/core'; +import { EntityViewInfo } from '@app/shared/models/entity-view.models'; +import { Observable } from 'rxjs'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { EntityId } from '@app/shared/models/id/entity-id'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; + +@Component({ + selector: 'tb-entity-view', + templateUrl: './entity-view.component.html', + styleUrls: ['./entity-view.component.scss'] +}) +export class EntityViewComponent extends EntityComponent { + + entityType = EntityType; + + dataKeyType = DataKeyType; + + entityViewScope: 'tenant' | 'customer' | 'customer_user'; + + allowedEntityTypes = [EntityType.DEVICE, EntityType.ASSET]; + + maxStartTimeMs: Observable; + minEndTimeMs: Observable; + + selectedEntityId: Observable; + + constructor(protected store: Store, + protected translate: TranslateService, + @Inject('entity') protected entityValue: EntityViewInfo, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + public fb: FormBuilder) { + super(store, fb, entityValue, entitiesTableConfigValue); + } + + ngOnInit() { + this.entityViewScope = this.entitiesTableConfig.componentsData.entityViewScope; + super.ngOnInit(); + this.maxStartTimeMs = this.entityForm.get('endTimeMs').valueChanges; + this.minEndTimeMs = this.entityForm.get('startTimeMs').valueChanges; + this.selectedEntityId = this.entityForm.get('entityId').valueChanges; + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + isAssignedToCustomer(entity: EntityViewInfo): boolean { + return entity && entity.customerId && entity.customerId.id !== NULL_UUID; + } + + buildForm(entity: EntityViewInfo): FormGroup { + return this.fb.group( + { + name: [entity ? entity.name : '', [Validators.required]], + type: [entity ? entity.type : null, [Validators.required]], + entityId: [entity ? entity.entityId : null, [Validators.required]], + startTimeMs: [entity ? entity.startTimeMs : null], + endTimeMs: [entity ? entity.endTimeMs : null], + keys: this.fb.group( + { + attributes: this.fb.group( + { + cs: [entity && entity.keys && entity.keys.attributes ? entity.keys.attributes.cs : null], + sh: [entity && entity.keys && entity.keys.attributes ? entity.keys.attributes.sh : null], + ss: [entity && entity.keys && entity.keys.attributes ? entity.keys.attributes.ss : null], + } + ), + timeseries: [entity && entity.keys && entity.keys.timeseries ? entity.keys.timeseries : null] + } + ), + additionalInfo: this.fb.group( + { + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''], + } + ) + } + ); + } + + updateForm(entity: EntityViewInfo) { + this.entityForm.patchValue({name: entity.name}); + this.entityForm.patchValue({type: entity.type}); + this.entityForm.patchValue({entityId: entity.entityId}); + this.entityForm.patchValue({startTimeMs: entity.startTimeMs}); + this.entityForm.patchValue({endTimeMs: entity.endTimeMs}); + this.entityForm.patchValue({ + keys: + { + attributes: { + cs: entity.keys && entity.keys.attributes ? entity.keys.attributes.cs : null, + sh: entity.keys && entity.keys.attributes ? entity.keys.attributes.sh : null, + ss: entity.keys && entity.keys.attributes ? entity.keys.attributes.ss : null, + }, + timeseries: entity.keys && entity.keys.timeseries ? entity.keys.timeseries : null + } + }); + this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}}); + } + + + onEntityViewIdCopied($event) { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('entity-view.idCopiedMessage'), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.module.ts b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.module.ts new file mode 100644 index 0000000000..494ca571ea --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.module.ts @@ -0,0 +1,41 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { HomeDialogsModule } from '../../dialogs/home-dialogs.module'; +import { EntityViewComponent } from '@modules/home/pages/entity-view/entity-view.component'; +import { EntityViewTableHeaderComponent } from './entity-view-table-header.component'; +import { EntityViewRoutingModule } from './entity-view-routing.module'; +import { HomeComponentsModule } from '@modules/home/components/home-components.module'; +import { EntityViewTabsComponent } from '@home/pages/entity-view/entity-view-tabs.component'; + +@NgModule({ + declarations: [ + EntityViewComponent, + EntityViewTabsComponent, + EntityViewTableHeaderComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + HomeDialogsModule, + EntityViewRoutingModule + ] +}) +export class EntityViewModule { } diff --git a/ui-ngx/src/app/modules/home/pages/entity-view/entity-views-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/entity-view/entity-views-table-config.resolver.ts new file mode 100644 index 0000000000..755d5b37d9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/entity-view/entity-views-table-config.resolver.ts @@ -0,0 +1,403 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; + +import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router'; +import { + CellActionDescriptor, + checkBoxCell, + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig, + GroupActionDescriptor, + HeaderActionDescriptor +} from '@home/models/entity/entities-table-config.models'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { forkJoin, Observable, of } from 'rxjs'; +import { select, Store } from '@ngrx/store'; +import { selectAuthUser } from '@core/auth/auth.selectors'; +import { map, mergeMap, take, tap } from 'rxjs/operators'; +import { AppState } from '@core/core.state'; +import { Authority } from '@app/shared/models/authority.enum'; +import { CustomerService } from '@core/http/customer.service'; +import { Customer } from '@app/shared/models/customer.model'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { BroadcastService } from '@core/services/broadcast.service'; +import { MatDialog } from '@angular/material/dialog'; +import { DialogService } from '@core/services/dialog.service'; +import { + AssignToCustomerDialogComponent, + AssignToCustomerDialogData +} from '@modules/home/dialogs/assign-to-customer-dialog.component'; +import { + AddEntitiesToCustomerDialogComponent, + AddEntitiesToCustomerDialogData +} from '../../dialogs/add-entities-to-customer-dialog.component'; +import { EntityView, EntityViewInfo } from '@app/shared/models/entity-view.models'; +import { EntityViewService } from '@core/http/entity-view.service'; +import { EntityViewComponent } from '@modules/home/pages/entity-view/entity-view.component'; +import { EntityViewTableHeaderComponent } from '@modules/home/pages/entity-view/entity-view-table-header.component'; +import { EntityViewId } from '@shared/models/id/entity-view-id'; +import { EntityViewTabsComponent } from '@home/pages/entity-view/entity-view-tabs.component'; + +@Injectable() +export class EntityViewsTableConfigResolver implements Resolve> { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + private customerId: string; + + constructor(private store: Store, + private broadcast: BroadcastService, + private entityViewService: EntityViewService, + private customerService: CustomerService, + private dialogService: DialogService, + private translate: TranslateService, + private datePipe: DatePipe, + private router: Router, + private dialog: MatDialog) { + + this.config.entityType = EntityType.ENTITY_VIEW; + this.config.entityComponent = EntityViewComponent; + this.config.entityTabsComponent = EntityViewTabsComponent; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.ENTITY_VIEW); + this.config.entityResources = entityTypeResources.get(EntityType.ENTITY_VIEW); + + this.config.addDialogStyle = {maxWidth: '800px'}; + + this.config.deleteEntityTitle = entityView => + this.translate.instant('entity-view.delete-entity-view-title', { entityViewName: entityView.name }); + this.config.deleteEntityContent = () => this.translate.instant('entity-view.delete-entity-view-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('entity-view.delete-entity-views-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('entity-view.delete-entity-views-text'); + + this.config.loadEntity = id => this.entityViewService.getEntityViewInfo(id.id); + this.config.saveEntity = entityView => { + return this.entityViewService.saveEntityView(entityView).pipe( + tap(() => { + this.broadcast.broadcast('entityViewSaved'); + }), + mergeMap((savedEntityView) => this.entityViewService.getEntityViewInfo(savedEntityView.id.id) + )); + }; + this.config.onEntityAction = action => this.onEntityViewAction(action); + this.config.detailsReadonly = () => this.config.componentsData.entityViewScope === 'customer_user'; + + this.config.headerComponent = EntityViewTableHeaderComponent; + + } + + resolve(route: ActivatedRouteSnapshot): Observable> { + const routeParams = route.params; + this.config.componentsData = { + entityViewScope: route.data.entityViewsType, + entityViewType: '' + }; + this.customerId = routeParams.customerId; + return this.store.pipe(select(selectAuthUser), take(1)).pipe( + tap((authUser) => { + if (authUser.authority === Authority.CUSTOMER_USER) { + this.config.componentsData.entityViewScope = 'customer_user'; + this.customerId = authUser.customerId; + } + }), + mergeMap(() => + this.customerId ? this.customerService.getCustomer(this.customerId) : of(null as Customer) + ), + map((parentCustomer) => { + if (parentCustomer) { + if (parentCustomer.additionalInfo && parentCustomer.additionalInfo.isPublic) { + this.config.tableTitle = this.translate.instant('customer.public-entity-views'); + } else { + this.config.tableTitle = parentCustomer.title + ': ' + this.translate.instant('entity-view.entity-views'); + } + } else { + this.config.tableTitle = this.translate.instant('entity-view.entity-views'); + } + this.config.columns = this.configureColumns(this.config.componentsData.entityViewScope); + this.configureEntityFunctions(this.config.componentsData.entityViewScope); + this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.entityViewScope); + this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.entityViewScope); + this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.entityViewScope); + this.config.addEnabled = this.config.componentsData.entityViewScope !== 'customer_user'; + this.config.entitiesDeleteEnabled = this.config.componentsData.entityViewScope === 'tenant'; + this.config.deleteEnabled = () => this.config.componentsData.entityViewScope === 'tenant'; + return this.config; + }) + ); + } + + configureColumns(entityViewScope: string): Array> { + const columns: Array> = [ + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), + new EntityTableColumn('name', 'entity-view.name', '33%'), + new EntityTableColumn('type', 'entity-view.entity-view-type', '33%'), + ]; + if (entityViewScope === 'tenant') { + columns.push( + new EntityTableColumn('customerTitle', 'customer.customer', '33%'), + new EntityTableColumn('customerIsPublic', 'entity-view.public', '60px', + entity => { + return checkBoxCell(entity.customerIsPublic); + }, () => ({}), false), + ); + } + return columns; + } + + configureEntityFunctions(entityViewScope: string): void { + if (entityViewScope === 'tenant') { + this.config.entitiesFetchFunction = pageLink => + this.entityViewService.getTenantEntityViewInfos(pageLink, this.config.componentsData.entityViewType); + this.config.deleteEntity = id => this.entityViewService.deleteEntityView(id.id); + } else { + this.config.entitiesFetchFunction = pageLink => + this.entityViewService.getCustomerEntityViewInfos(this.customerId, pageLink, this.config.componentsData.entityViewType); + this.config.deleteEntity = id => this.entityViewService.unassignEntityViewFromCustomer(id.id); + } + } + + configureCellActions(entityViewScope: string): Array> { + const actions: Array> = []; + if (entityViewScope === 'tenant') { + actions.push( + { + name: this.translate.instant('entity-view.make-public'), + icon: 'share', + isEnabled: (entity) => (!entity.customerId || entity.customerId.id === NULL_UUID), + onAction: ($event, entity) => this.makePublic($event, entity) + }, + { + name: this.translate.instant('entity-view.assign-to-customer'), + icon: 'assignment_ind', + isEnabled: (entity) => (!entity.customerId || entity.customerId.id === NULL_UUID), + onAction: ($event, entity) => this.assignToCustomer($event, [entity.id]) + }, + { + name: this.translate.instant('entity-view.unassign-from-customer'), + icon: 'assignment_return', + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && !entity.customerIsPublic), + onAction: ($event, entity) => this.unassignFromCustomer($event, entity) + }, + { + name: this.translate.instant('entity-view.make-private'), + icon: 'reply', + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && entity.customerIsPublic), + onAction: ($event, entity) => this.unassignFromCustomer($event, entity) + } + ); + } + if (entityViewScope === 'customer') { + actions.push( + { + name: this.translate.instant('entity-view.unassign-from-customer'), + icon: 'assignment_return', + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && !entity.customerIsPublic), + onAction: ($event, entity) => this.unassignFromCustomer($event, entity) + }, + { + name: this.translate.instant('entity-view.make-private'), + icon: 'reply', + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && entity.customerIsPublic), + onAction: ($event, entity) => this.unassignFromCustomer($event, entity) + } + ); + } + return actions; + } + + configureGroupActions(entityViewScope: string): Array> { + const actions: Array> = []; + if (entityViewScope === 'tenant') { + actions.push( + { + name: this.translate.instant('entity-view.assign-entity-views'), + icon: 'assignment_ind', + isEnabled: true, + onAction: ($event, entities) => this.assignToCustomer($event, entities.map((entity) => entity.id)) + } + ); + } + if (entityViewScope === 'customer') { + actions.push( + { + name: this.translate.instant('entity-view.unassign-entity-views'), + icon: 'assignment_return', + isEnabled: true, + onAction: ($event, entities) => this.unassignEntityViewsFromCustomer($event, entities) + } + ); + } + return actions; + } + + configureAddActions(entityViewScope: string): Array { + const actions: Array = []; + if (entityViewScope === 'customer') { + actions.push( + { + name: this.translate.instant('entity-view.assign-new-entity-view'), + icon: 'add', + isEnabled: () => true, + onAction: ($event) => this.addEntityViewsToCustomer($event) + } + ); + } + return actions; + } + + addEntityViewsToCustomer($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(AddEntitiesToCustomerDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + customerId: this.customerId, + entityType: EntityType.ENTITY_VIEW + } + }).afterClosed() + .subscribe((res) => { + if (res) { + this.config.table.updateData(); + } + }); + } + + makePublic($event: Event, entityView: EntityView) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('entity-view.make-public-entity-view-title', {entityViewName: entityView.name}), + this.translate.instant('entity-view.make-public-entity-view-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + this.entityViewService.makeEntityViewPublic(entityView.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + assignToCustomer($event: Event, entityViewIds: Array) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(AssignToCustomerDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + entityIds: entityViewIds, + entityType: EntityType.ENTITY_VIEW + } + }).afterClosed() + .subscribe((res) => { + if (res) { + this.config.table.updateData(); + } + }); + } + + unassignFromCustomer($event: Event, entityView: EntityViewInfo) { + if ($event) { + $event.stopPropagation(); + } + const isPublic = entityView.customerIsPublic; + let title; + let content; + if (isPublic) { + title = this.translate.instant('entity-view.make-private-entity-view-title', {entityViewName: entityView.name}); + content = this.translate.instant('entity-view.make-private-entity-view-text'); + } else { + title = this.translate.instant('entity-view.unassign-entity-view-title', {entityViewName: entityView.name}); + content = this.translate.instant('entity-view.unassign-entity-view-text'); + } + this.dialogService.confirm( + title, + content, + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + this.entityViewService.unassignEntityViewFromCustomer(entityView.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + unassignEntityViewsFromCustomer($event: Event, entityViews: Array) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('entity-view.unassign-entity-views-title', {count: entityViews.length}), + this.translate.instant('entity-view.unassign-entity-views-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + const tasks: Observable[] = []; + entityViews.forEach( + (entityView) => { + tasks.push(this.entityViewService.unassignEntityViewFromCustomer(entityView.id.id)); + } + ); + forkJoin(tasks).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + onEntityViewAction(action: EntityAction): boolean { + switch (action.action) { + case 'makePublic': + this.makePublic(action.event, action.entity); + return true; + case 'assignToCustomer': + this.assignToCustomer(action.event, [action.entity.id]); + return true; + case 'unassignFromCustomer': + this.unassignFromCustomer(action.event, action.entity); + return true; + } + return false; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts b/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts new file mode 100644 index 0000000000..94fca64ac2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts @@ -0,0 +1,42 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { HomeLinksComponent } from './home-links.component'; +import { Authority } from '@shared/models/authority.enum'; + +const routes: Routes = [ + { + path: 'home', + component: HomeLinksComponent, + data: { + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + title: 'home.home', + breadcrumb: { + label: 'home.home', + icon: 'home' + } + } + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class HomeLinksRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.html b/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.html new file mode 100644 index 0000000000..f2d07de7fb --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.html @@ -0,0 +1,37 @@ + + + + + + {{section.name}} + + + + + + {{place.icon}} + + {{place.name}} + + + + + + + diff --git a/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.scss b/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.scss new file mode 100644 index 0000000000..83bdb6c118 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.scss @@ -0,0 +1,80 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../scss/constants'; + +:host ::ng-deep { + .tb-home-links { + .mat-headline { + font-size: 20px; + @media #{$mat-gt-xmd} { + font-size: 24px; + } + } + mat-card { + padding: 0; + margin: 8px; + mat-card-title { + margin: 0; + padding: 24px 16px 16px; + } + mat-card-title+mat-card-content { + padding-top: 0; + } + mat-card-content { + padding: 16px; + } + } + .tb-card-button { + width: 100%; + height: 100%; + max-width: 240px; + &:hover { + border-bottom: none; + } + &:focus { + border-bottom: none; + } + .mat-button-wrapper { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + mat-icon { + margin: auto; + } + span { + height: 18px; + min-height: 36px; + max-height: 36px; + padding: 0 0 20px 0; + margin: auto; + font-size: 18px; + font-weight: 400; + line-height: 18px; + white-space: normal; + } + } + &.mat-raised-button.mat-primary { + .mat-ripple-element { + opacity: 0.3; + background-color: rgba(255, 255, 255, 0.3); + } + } + } + } +} + diff --git a/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.ts b/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.ts new file mode 100644 index 0000000000..2c43b4d702 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.ts @@ -0,0 +1,66 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, OnInit } from '@angular/core'; +import { MenuService } from '@core/services/menu.service'; +import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; +import { MediaBreakpoints } from '@shared/models/constants'; +import { HomeSection } from '@core/services/menu.models'; + +@Component({ + selector: 'tb-home-links', + templateUrl: './home-links.component.html', + styleUrls: ['./home-links.component.scss'] +}) +export class HomeLinksComponent implements OnInit { + + homeSections$ = this.menuService.homeSections(); + + cols = 2; + + constructor(private menuService: MenuService, + public breakpointObserver: BreakpointObserver) { + } + + ngOnInit() { + this.updateColumnCount(); + this.breakpointObserver + .observe([MediaBreakpoints.lg, MediaBreakpoints['gt-lg']]) + .subscribe((state: BreakpointState) => this.updateColumnCount()); + } + + private updateColumnCount() { + this.cols = 2; + if (this.breakpointObserver.isMatched(MediaBreakpoints.lg)) { + this.cols = 3; + } + if (this.breakpointObserver.isMatched(MediaBreakpoints['gt-lg'])) { + this.cols = 4; + } + } + + sectionColspan(section: HomeSection): number { + if (this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm'])) { + let colspan = this.cols; + if (section && section.places && section.places.length <= colspan) { + colspan = section.places.length; + } + return colspan; + } else { + return 2; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/home-links/home-links.module.ts b/ui-ngx/src/app/modules/home/pages/home-links/home-links.module.ts new file mode 100644 index 0000000000..af2f2bf6be --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/home-links/home-links.module.ts @@ -0,0 +1,35 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { HomeLinksRoutingModule } from './home-links-routing.module'; +import { HomeLinksComponent } from './home-links.component'; +import { SharedModule } from '@app/shared/shared.module'; + +@NgModule({ + declarations: + [ + HomeLinksComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeLinksRoutingModule + ] +}) +export class HomeLinksModule { } diff --git a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts new file mode 100644 index 0000000000..497c6231f3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts @@ -0,0 +1,62 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; + +import { AdminModule } from './admin/admin.module'; +import { HomeLinksModule } from './home-links/home-links.module'; +import { ProfileModule } from './profile/profile.module'; +import { TenantModule } from '@modules/home/pages/tenant/tenant.module'; +import { CustomerModule } from '@modules/home/pages/customer/customer.module'; +import { AuditLogModule } from '@modules/home/pages/audit-log/audit-log.module'; +import { UserModule } from '@modules/home/pages/user/user.module'; +import { DeviceModule } from '@modules/home/pages/device/device.module'; +import { AssetModule } from '@modules/home/pages/asset/asset.module'; +import { EntityViewModule } from '@modules/home/pages/entity-view/entity-view.module'; +import { RuleChainModule } from '@modules/home/pages/rulechain/rulechain.module'; +import { WidgetLibraryModule } from '@modules/home/pages/widget/widget-library.module'; +import { DashboardModule } from '@modules/home/pages/dashboard/dashboard.module'; +import { TenantProfileModule } from './tenant-profile/tenant-profile.module'; +import { MODULES_MAP } from '@shared/public-api'; +import { modulesMap } from '../../common/modules-map'; +import { DeviceProfileModule } from './device-profile/device-profile.module'; + +@NgModule({ + exports: [ + AdminModule, + HomeLinksModule, + ProfileModule, + TenantProfileModule, + TenantModule, + DeviceProfileModule, + DeviceModule, + AssetModule, + EntityViewModule, + CustomerModule, + RuleChainModule, + WidgetLibraryModule, + DashboardModule, + AuditLogModule, + UserModule + ], + providers: [ + { + provide: MODULES_MAP, + useValue: modulesMap + } + ] +}) +export class HomePagesModule { } diff --git a/ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.html b/ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.html new file mode 100644 index 0000000000..926c4d7bcc --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.html @@ -0,0 +1,61 @@ + +
+ +

profile.change-password

+ + +
+ + +
+
+ + profile.current-password + + lock + + + login.new-password + + lock + + + login.new-password-again + + lock + +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.scss new file mode 100644 index 0000000000..655a7ced9b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.scss @@ -0,0 +1,17 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { +} diff --git a/ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.ts new file mode 100644 index 0000000000..d439ed5dbf --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.ts @@ -0,0 +1,70 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, OnInit } from '@angular/core'; +import { MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { TranslateService } from '@ngx-translate/core'; +import { AuthService } from '@core/auth/auth.service'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'tb-change-password-dialog', + templateUrl: './change-password-dialog.component.html', + styleUrls: ['./change-password-dialog.component.scss'] +}) +export class ChangePasswordDialogComponent extends DialogComponent implements OnInit { + + changePassword: FormGroup; + + constructor(protected store: Store, + protected router: Router, + private translate: TranslateService, + private authService: AuthService, + public dialogRef: MatDialogRef, + public fb: FormBuilder) { + super(store, router, dialogRef); + } + + ngOnInit(): void { + this.buildChangePasswordForm(); + } + + buildChangePasswordForm() { + this.changePassword = this.fb.group({ + currentPassword: [''], + newPassword: [''], + newPassword2: [''] + }); + } + + onChangePassword(): void { + if (this.changePassword.get('newPassword').value !== this.changePassword.get('newPassword2').value) { + this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('login.passwords-mismatch-error'), + type: 'error' })); + } else { + this.authService.changePassword( + this.changePassword.get('currentPassword').value, + this.changePassword.get('newPassword').value).subscribe(() => { + this.dialogRef.close(true); + }); + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile-routing.module.ts b/ui-ngx/src/app/modules/home/pages/profile/profile-routing.module.ts new file mode 100644 index 0000000000..0a6f02eca7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/profile/profile-routing.module.ts @@ -0,0 +1,69 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable, NgModule } from '@angular/core'; +import { Resolve, RouterModule, Routes } from '@angular/router'; + +import { ProfileComponent } from './profile.component'; +import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; +import { Authority } from '@shared/models/authority.enum'; +import { User } from '@shared/models/user.model'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { UserService } from '@core/http/user.service'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { Observable } from 'rxjs'; + +@Injectable() +export class UserProfileResolver implements Resolve { + + constructor(private store: Store, + private userService: UserService) { + } + + resolve(): Observable { + const userId = getCurrentAuthUser(this.store).userId; + return this.userService.getUser(userId); + } +} + +const routes: Routes = [ + { + path: 'profile', + component: ProfileComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + title: 'profile.profile', + breadcrumb: { + label: 'profile.profile', + icon: 'account_circle' + } + }, + resolve: { + user: UserProfileResolver + } + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [ + UserProfileResolver + ] +}) +export class ProfileRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile.component.html b/ui-ngx/src/app/modules/home/pages/profile/profile.component.html new file mode 100644 index 0000000000..afb5eba3c0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/profile/profile.component.html @@ -0,0 +1,85 @@ + +
+ + +
+
+ profile.profile + {{ profile ? profile.get('email').value : '' }} +
+
+ profile.last-login-time + +
+
+
+ + + + +
+ +
+
+ + user.email + + + {{ 'user.email-required' | translate }} + + + {{ 'user.invalid-email-format' | translate }} + + + + user.first-name + + + + user.last-name + + + + language.language + + + {{ lang ? ('language.locales.' + lang | translate) : ''}} + + + +
+ +
+
+ + +
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile.component.scss b/ui-ngx/src/app/modules/home/pages/profile/profile.component.scss new file mode 100644 index 0000000000..0fd9ca02e6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/profile/profile.component.scss @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../../../scss/constants"; + +:host { + mat-card.profile-card { + margin: 8px; + @media #{$mat-gt-sm} { + width: 60%; + } + .mat-headline { + margin: 0; + } + .profile-email { + font-size: 16px; + font-weight: 400; + } + .mat-subheader { + line-height: 24px; + color: rgba(0,0,0,0.54); + font-size: 14px; + font-weight: 400; + } + .profile-last-login-ts { + font-size: 16px; + font-weight: 400; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts b/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts new file mode 100644 index 0000000000..1fe2dd4f8d --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts @@ -0,0 +1,121 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, OnInit } from '@angular/core'; +import { UserService } from '@core/http/user.service'; +import { User } from '@shared/models/user.model'; +import { Authority } from '@shared/models/authority.enum'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; +import { ActionAuthUpdateUserDetails } from '@core/auth/auth.actions'; +import { environment as env } from '@env/environment'; +import { TranslateService } from '@ngx-translate/core'; +import { ActionSettingsChangeLanguage } from '@core/settings/settings.actions'; +import { ChangePasswordDialogComponent } from '@modules/home/pages/profile/change-password-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; +import { DialogService } from '@core/services/dialog.service'; +import { AuthService } from '@core/auth/auth.service'; +import { ActivatedRoute } from '@angular/router'; + +@Component({ + selector: 'tb-profile', + templateUrl: './profile.component.html', + styleUrls: ['./profile.component.scss'] +}) +export class ProfileComponent extends PageComponent implements OnInit, HasConfirmForm { + + authorities = Authority; + profile: FormGroup; + user: User; + languageList = env.supportedLangs; + + constructor(protected store: Store, + private route: ActivatedRoute, + private userService: UserService, + private authService: AuthService, + private translate: TranslateService, + public dialog: MatDialog, + public dialogService: DialogService, + public fb: FormBuilder) { + super(store); + } + + ngOnInit() { + this.buildProfileForm(); + this.userLoaded(this.route.snapshot.data.user); + } + + buildProfileForm() { + this.profile = this.fb.group({ + email: ['', [Validators.required, Validators.email]], + firstName: [''], + lastName: [''], + language: [''] + }); + } + + save(): void { + this.user = {...this.user, ...this.profile.value}; + if (!this.user.additionalInfo) { + this.user.additionalInfo = {}; + } + this.user.additionalInfo.lang = this.profile.get('language').value; + this.userService.saveUser(this.user).subscribe( + (user) => { + this.userLoaded(user); + this.store.dispatch(new ActionAuthUpdateUserDetails({ userDetails: { + additionalInfo: {...user.additionalInfo}, + authority: user.authority, + createdTime: user.createdTime, + tenantId: user.tenantId, + customerId: user.customerId, + email: user.email, + firstName: user.firstName, + id: user.id, + lastName: user.lastName, + } })); + this.store.dispatch(new ActionSettingsChangeLanguage({ userLang: user.additionalInfo.lang })); + } + ); + } + + changePassword(): void { + this.dialog.open(ChangePasswordDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] + }); + } + + userLoaded(user: User) { + this.user = user; + this.profile.reset(user); + let lang; + if (user.additionalInfo && user.additionalInfo.lang) { + lang = user.additionalInfo.lang; + } else { + lang = this.translate.currentLang; + } + this.profile.get('language').setValue(lang); + } + + confirmForm(): FormGroup { + return this.profile; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile.module.ts b/ui-ngx/src/app/modules/home/pages/profile/profile.module.ts new file mode 100644 index 0000000000..528fac1782 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/profile/profile.module.ts @@ -0,0 +1,35 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ProfileComponent } from './profile.component'; +import { SharedModule } from '@shared/shared.module'; +import { ProfileRoutingModule } from './profile-routing.module'; +import { ChangePasswordDialogComponent } from '@modules/home/pages/profile/change-password-dialog.component'; + +@NgModule({ + declarations: [ + ProfileComponent, + ChangePasswordDialogComponent + ], + imports: [ + CommonModule, + SharedModule, + ProfileRoutingModule + ] +}) +export class ProfileModule { } diff --git a/ui-ngx/src/app/modules/home/pages/public-api.ts b/ui-ngx/src/app/modules/home/pages/public-api.ts new file mode 100644 index 0000000000..407ed090b2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/public-api.ts @@ -0,0 +1,17 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export * from './home-pages.module'; diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-dialog.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-dialog.component.html new file mode 100644 index 0000000000..75ccbb764e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-dialog.component.html @@ -0,0 +1,55 @@ + +
+ +

rulenode.add

+ +
+ +
+ + +
+
+ + +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-link-dialog.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-link-dialog.component.html new file mode 100644 index 0000000000..12de2706f5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-link-dialog.component.html @@ -0,0 +1,54 @@ + +
+ +

rulenode.add-link

+ +
+ +
+ + +
+
+ + +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-link-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-link-dialog.component.scss new file mode 100644 index 0000000000..67986c991a --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-link-dialog.component.scss @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host ::ng-deep { + tb-rule-node-link { + form { + overflow: hidden !important; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/link-labels.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/link-labels.component.html new file mode 100644 index 0000000000..d227e7ac03 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/link-labels.component.html @@ -0,0 +1,66 @@ + + + rulenode.link-labels + + + {{label.name}} + close + + + + + + + + +
+
+ rulenode.no-link-labels-found +
+ + + {{ translate.get('rulenode.no-link-label-matching', + {label: truncate.transform(searchText, true, 6, '...')}) | async }} + + + + rulenode.create-new-link-label + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/link-labels.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/link-labels.component.ts new file mode 100644 index 0000000000..87f7a65067 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/link-labels.component.ts @@ -0,0 +1,259 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { LinkLabel } from '@shared/models/rule-node.models'; +import { Observable, of } from 'rxjs'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { deepClone } from '@core/utils'; +import { TruncatePipe } from '@shared/pipe/truncate.pipe'; +import { MatAutocomplete, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { MatChipInputEvent, MatChipList } from '@angular/material/chips'; +import { TranslateService } from '@ngx-translate/core'; +import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; +import { map, mergeMap, share, startWith } from 'rxjs/operators'; + +@Component({ + selector: 'tb-link-labels', + templateUrl: './link-labels.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => LinkLabelsComponent), + multi: true + }] +}) +export class LinkLabelsComponent implements ControlValueAccessor, OnInit, OnChanges { + + @ViewChild('chipList') chipList: MatChipList; + @ViewChild('labelAutocomplete') matAutocomplete: MatAutocomplete; + @ViewChild('labelInput') labelInput: ElementRef; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private allowCustomValue: boolean; + get allowCustom(): boolean { + return this.allowCustomValue; + } + @Input() + set allowCustom(value: boolean) { + this.allowCustomValue = coerceBooleanProperty(value); + this.separatorKeysCodes = this.allowCustomValue ? [ENTER, COMMA, SEMICOLON] : []; + } + + @Input() + allowedLabels: {[label: string]: LinkLabel}; + + linksFormGroup: FormGroup; + + modelValue: Array; + + private labelsList: Array = []; + + separatorKeysCodes: number[] = []; + + filteredLabels: Observable>; + + labels: Array = []; + + searchText = ''; + + private propagateChange = (v: any) => { }; + + constructor(private fb: FormBuilder, + public truncate: TruncatePipe, + public translate: TranslateService) { + this.linksFormGroup = this.fb.group({ + label: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + + ngOnInit(): void { + this.filteredLabels = this.linksFormGroup.get('label').valueChanges + .pipe( + startWith(''), + map((value) => value ? value : ''), + mergeMap(name => this.fetchLabels(name) ), + share() + ); + } + + ngOnChanges(changes: SimpleChanges): void { + let reloadLabels = false; + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (change.currentValue !== change.previousValue) { + if (['allowCustom', 'allowedLabels'].includes(propName)) { + reloadLabels = true; + } + } + } + if (reloadLabels) { + this.linksFormGroup.get('label').patchValue('', {emitEvent: true}); + } + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.linksFormGroup.disable({emitEvent: false}); + } else { + this.linksFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: string[]): void { + this.searchText = ''; + this.labelsList.length = 0; + this.labels.length = 0; + this.modelValue = value; + for (const label of Object.keys(this.allowedLabels)) { + this.labelsList.push({name: this.allowedLabels[label].name, value: this.allowedLabels[label].value}); + } + if (value) { + value.forEach((label) => { + if (this.allowedLabels[label]) { + this.labels.push(deepClone(this.allowedLabels[label])); + } else { + this.labels.push({ + name: label, + value: label + }); + } + }); + } + if (this.chipList && this.required) { + this.chipList.errorState = this.labels.length ? false : true; + } + this.linksFormGroup.get('label').patchValue('', {emitEvent: true}); + } + + displayLabelFn(label?: LinkLabel): string | undefined { + return label ? label.name : undefined; + } + + textIsNotEmpty(text: string): boolean { + return (text && text != null && text.length > 0) ? true : false; + } + + createLinkLabel($event: Event, value: string) { + $event.preventDefault(); + this.transformLinkLabel(value); + } + + add(event: MatChipInputEvent): void { + if (!this.matAutocomplete.isOpen || this.allowCustom) { + this.transformLinkLabel(event.value); + } + } + + private fetchLabels(searchText?: string): Observable> { + this.searchText = searchText; + if (this.searchText && this.searchText.length) { + const search = this.searchText.toUpperCase(); + return of(this.labelsList.filter(label => label.name.toUpperCase().includes(search))); + } else { + return of(this.labelsList); + } + } + + private transformLinkLabel(value: string) { + if ((value || '').trim()) { + let newLabel: LinkLabel = null; + const labelName = value.trim(); + const existingLabel = this.labelsList.find(label => label.name === labelName); + if (existingLabel) { + newLabel = deepClone(existingLabel); + } else if (this.allowCustom) { + newLabel = { + name: labelName, + value: labelName + }; + } + if (newLabel) { + this.addLabel(newLabel); + } + } + this.clear(''); + } + + remove(label: LinkLabel) { + const index = this.labels.indexOf(label); + if (index >= 0) { + this.labels.splice(index, 1); + if (!this.labels.length) { + if (this.required) { + this.chipList.errorState = true; + } + } + this.updateModel(); + } + } + + selected(event: MatAutocompleteSelectedEvent): void { + this.addLabel(event.option.value); + this.clear(''); + } + + addLabel(label: LinkLabel): void { + const index = this.labels.findIndex(existinglabel => existinglabel.value === label.value); + if (index === -1) { + this.labels.push(label); + if (this.required) { + this.chipList.errorState = false; + } + this.updateModel(); + } + } + + onFocus() { + this.linksFormGroup.get('label').updateValueAndValidity({onlySelf: true, emitEvent: true}); + } + + clear(value: string = '') { + this.labelInput.nativeElement.value = value; + this.linksFormGroup.get('label').patchValue(null, {emitEvent: true}); + setTimeout(() => { + this.labelInput.nativeElement.blur(); + this.labelInput.nativeElement.focus(); + }, 0); + } + + private updateModel() { + const labels = this.labels.map((label => label.value)); + this.propagateChange(labels); + } +} + diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-colors.scss b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-colors.scss new file mode 100644 index 0000000000..7417507452 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-colors.scss @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@mixin rule-node-colors { + &.tb-filter-type { + background-color: #f1e861; + } + + &.tb-enrichment-type { + background-color: #cdf14e; + } + + &.tb-transformation-type { + background-color: #79cef1; + } + + &.tb-action-type { + background-color: #f1928f; + } + + &.tb-external-type { + background-color: #fbc766; + } + + &.tb-rule-chain-type { + background-color: #d6c4f1; + } + + &.tb-unknown-type { + background-color: #f16c29; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.html new file mode 100644 index 0000000000..8b8ed7f0ca --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.html @@ -0,0 +1,28 @@ + +
+ +
{{definedDirectiveError}}
+ + +
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.scss b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.scss new file mode 100644 index 0000000000..6d2ac3d585 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.scss @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + tb-json-object-edit.tb-rule-node-configuration-json { + display: block; + height: 300px; + } + + .tb-rulenode-directive-error { + font-size: 13px; + font-weight: 400; + color: rgb(221, 44, 0); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts new file mode 100644 index 0000000000..8d4ef31a71 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts @@ -0,0 +1,201 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + Component, + ComponentRef, + forwardRef, + Input, + OnDestroy, + OnInit, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { + IRuleNodeConfigurationComponent, + RuleNodeConfiguration, + RuleNodeDefinition +} from '@shared/models/rule-node.models'; +import { Subscription } from 'rxjs'; +import { RuleChainService } from '@core/http/rule-chain.service'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { TranslateService } from '@ngx-translate/core'; +import { JsonObjectEditComponent } from '@shared/components/json-object-edit.component'; +import { deepClone } from '@core/utils'; + +@Component({ + selector: 'tb-rule-node-config', + templateUrl: './rule-node-config.component.html', + styleUrls: ['./rule-node-config.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RuleNodeConfigComponent), + multi: true + }] +}) +export class RuleNodeConfigComponent implements ControlValueAccessor, OnInit, OnDestroy, AfterViewInit { + + @ViewChild('definedConfigContent', {read: ViewContainerRef, static: true}) definedConfigContainer: ViewContainerRef; + + @ViewChild('jsonObjectEditComponent') jsonObjectEditComponent: JsonObjectEditComponent; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @Input() + ruleNodeId: string; + + nodeDefinitionValue: RuleNodeDefinition; + + @Input() + set nodeDefinition(nodeDefinition: RuleNodeDefinition) { + if (this.nodeDefinitionValue !== nodeDefinition) { + this.nodeDefinitionValue = nodeDefinition; + if (this.nodeDefinitionValue) { + this.validateDefinedDirective(); + } + } + } + + get nodeDefinition(): RuleNodeDefinition { + return this.nodeDefinitionValue; + } + + definedDirectiveError: string; + + ruleNodeConfigFormGroup: FormGroup; + + changeSubscription: Subscription; + + private definedConfigComponentRef: ComponentRef; + private definedConfigComponent: IRuleNodeConfigurationComponent; + + private configuration: RuleNodeConfiguration; + + private propagateChange = (v: any) => { }; + + constructor(private translate: TranslateService, + private ruleChainService: RuleChainService, + private fb: FormBuilder) { + this.ruleNodeConfigFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit(): void { + } + + ngOnDestroy(): void { + if (this.definedConfigComponentRef) { + this.definedConfigComponentRef.destroy(); + } + } + + ngAfterViewInit(): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.ruleNodeConfigFormGroup.disable({emitEvent: false}); + } else { + this.ruleNodeConfigFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: RuleNodeConfiguration): void { + this.configuration = deepClone(value); + if (this.changeSubscription) { + this.changeSubscription.unsubscribe(); + this.changeSubscription = null; + } + if (this.definedConfigComponent) { + this.definedConfigComponent.configuration = this.configuration; + this.changeSubscription = this.definedConfigComponent.configurationChanged.subscribe((configuration) => { + this.updateModel(configuration); + }); + } else { + this.ruleNodeConfigFormGroup.get('configuration').patchValue(this.configuration, {emitEvent: false}); + this.changeSubscription = this.ruleNodeConfigFormGroup.get('configuration').valueChanges.subscribe( + (configuration: RuleNodeConfiguration) => { + this.updateModel(configuration); + } + ); + } + } + + useDefinedDirective(): boolean { + return this.nodeDefinition && + (this.nodeDefinition.configDirective && + this.nodeDefinition.configDirective.length) && !this.definedDirectiveError; + } + + private updateModel(configuration: RuleNodeConfiguration) { + if (this.definedConfigComponent || this.ruleNodeConfigFormGroup.valid) { + this.propagateChange(configuration); + } else { + this.propagateChange(this.required ? null : configuration); + } + } + + private validateDefinedDirective() { + if (this.definedConfigComponentRef) { + this.definedConfigComponentRef.destroy(); + this.definedConfigComponentRef = null; + } + if (this.nodeDefinition.uiResourceLoadError && this.nodeDefinition.uiResourceLoadError.length) { + this.definedDirectiveError = this.nodeDefinition.uiResourceLoadError; + } else if (this.nodeDefinition.configDirective && this.nodeDefinition.configDirective.length) { + if (this.changeSubscription) { + this.changeSubscription.unsubscribe(); + this.changeSubscription = null; + } + this.definedConfigContainer.clear(); + const factory = this.ruleChainService.getRuleNodeConfigFactory(this.nodeDefinition.configDirective); + this.definedConfigComponentRef = this.definedConfigContainer.createComponent(factory); + this.definedConfigComponent = this.definedConfigComponentRef.instance; + this.definedConfigComponent.ruleNodeId = this.ruleNodeId; + this.definedConfigComponent.configuration = this.configuration; + this.changeSubscription = this.definedConfigComponent.configurationChanged.subscribe((configuration) => { + this.updateModel(configuration); + }); + } + } + + validate() { + if (this.useDefinedDirective()) { + this.definedConfigComponent.validate(); + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html new file mode 100644 index 0000000000..fd4efddf5a --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html @@ -0,0 +1,68 @@ + +
+ +
+
+
+
+
+
+ + rulenode.name + + + {{ 'rulenode.name-required' | translate }} + + + + {{ 'rulenode.debug-mode' | translate }} + +
+ + +
+ + rulenode.description + + +
+
+
+ + +
+ + rulenode.description + + +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.scss b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.scss new file mode 100644 index 0000000000..b597effd5c --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.scss @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + form { + overflow-x: hidden !important; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts new file mode 100644 index 0000000000..b5630b52f8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts @@ -0,0 +1,153 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { FcRuleNode, RuleNodeType } from '@shared/models/rule-node.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { Subscription } from 'rxjs'; +import { RuleChainService } from '@core/http/rule-chain.service'; +import { RuleNodeConfigComponent } from './rule-node-config.component'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'tb-rule-node', + templateUrl: './rule-node-details.component.html', + styleUrls: ['./rule-node-details.component.scss'] +}) +export class RuleNodeDetailsComponent extends PageComponent implements OnInit, OnChanges { + + @ViewChild('ruleNodeConfigComponent') ruleNodeConfigComponent: RuleNodeConfigComponent; + + @Input() + ruleNode: FcRuleNode; + + @Input() + ruleChainId: string; + + @Input() + isEdit: boolean; + + @Input() + isReadOnly: boolean; + + @Input() + isAdd = false; + + ruleNodeType = RuleNodeType; + entityType = EntityType; + + ruleNodeFormGroup: FormGroup; + + private ruleNodeFormSubscription: Subscription; + + constructor(protected store: Store, + private fb: FormBuilder, + private ruleChainService: RuleChainService, + private router: Router) { + super(store); + this.ruleNodeFormGroup = this.fb.group({}); + } + + private buildForm() { + if (this.ruleNodeFormSubscription) { + this.ruleNodeFormSubscription.unsubscribe(); + this.ruleNodeFormSubscription = null; + } + if (this.ruleNode) { + if (this.ruleNode.component.type !== RuleNodeType.RULE_CHAIN) { + + this.ruleNodeFormGroup = this.fb.group({ + name: [this.ruleNode.name, [Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*')]], + debugMode: [this.ruleNode.debugMode, []], + configuration: [this.ruleNode.configuration, [Validators.required]], + additionalInfo: this.fb.group( + { + description: [this.ruleNode.additionalInfo ? this.ruleNode.additionalInfo.description : ''], + } + ) + }); + } else { + this.ruleNodeFormGroup = this.fb.group({ + targetRuleChainId: [this.ruleNode.targetRuleChainId, [Validators.required]], + additionalInfo: this.fb.group( + { + description: [this.ruleNode.additionalInfo ? this.ruleNode.additionalInfo.description : ''], + } + ) + }); + } + this.ruleNodeFormSubscription = this.ruleNodeFormGroup.valueChanges.subscribe(() => { + this.updateRuleNode(); + }); + } else { + this.ruleNodeFormGroup = this.fb.group({}); + } + } + + private updateRuleNode() { + const formValue = this.ruleNodeFormGroup.value || {}; + + if (this.ruleNode.component.type === RuleNodeType.RULE_CHAIN) { + const targetRuleChainId: string = formValue.targetRuleChainId; + if (this.ruleNode.targetRuleChainId !== targetRuleChainId && targetRuleChainId) { + this.ruleChainService.getRuleChain(targetRuleChainId).subscribe( + (ruleChain) => { + this.ruleNode.name = ruleChain.name; + Object.assign(this.ruleNode, formValue); + } + ); + } else { + Object.assign(this.ruleNode, formValue); + } + } else { + formValue.name = formValue.name.trim(); + Object.assign(this.ruleNode, formValue); + } + } + + ngOnInit(): void { + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (change.currentValue !== change.previousValue) { + if (propName === 'ruleNode') { + this.buildForm(); + } + } + } + } + + validate() { + if (this.ruleNode.component.type !== RuleNodeType.RULE_CHAIN) { + this.ruleNodeConfigComponent.validate(); + } + } + + openRuleChain($event: Event) { + if ($event) { + $event.stopPropagation(); + } + if (this.ruleNode.targetRuleChainId) { + this.router.navigateByUrl(`/ruleChains/${this.ruleNode.targetRuleChainId}`); + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-link.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-link.component.html new file mode 100644 index 0000000000..112e95ca45 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-link.component.html @@ -0,0 +1,27 @@ + +
+
+ +
+
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-link.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-link.component.ts new file mode 100644 index 0000000000..7d92636d1b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-link.component.ts @@ -0,0 +1,111 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { FcRuleEdge, LinkLabel } from '@shared/models/rule-node.models'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { TruncatePipe } from '@shared/pipe/truncate.pipe'; +import { TranslateService } from '@ngx-translate/core'; + +@Component({ + selector: 'tb-rule-node-link', + templateUrl: './rule-node-link.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RuleNodeLinkComponent), + multi: true + }] +}) +export class RuleNodeLinkComponent implements ControlValueAccessor, OnInit { + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private allowCustomValue: boolean; + get allowCustom(): boolean { + return this.allowCustomValue; + } + @Input() + set allowCustom(value: boolean) { + this.allowCustomValue = coerceBooleanProperty(value); + } + + @Input() + allowedLabels: {[label: string]: LinkLabel}; + + ruleNodeLinkFormGroup: FormGroup; + + modelValue: FcRuleEdge; + + private propagateChange = (v: any) => { }; + + constructor(private fb: FormBuilder, + public truncate: TruncatePipe, + public translate: TranslateService) { + this.ruleNodeLinkFormGroup = this.fb.group({ + labels: [[], Validators.required] + }); + this.ruleNodeLinkFormGroup.get('labels').valueChanges.subscribe( + (labels: string[]) => this.updateModel(labels) + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit(): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.ruleNodeLinkFormGroup.disable({emitEvent: false}); + } else { + this.ruleNodeLinkFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: FcRuleEdge): void { + this.modelValue = value; + const labels = this.modelValue && this.modelValue.labels ? this.modelValue.labels : []; + this.ruleNodeLinkFormGroup.get('labels').patchValue(labels, {emitEvent: false}); + } + + private updateModel(labels: string[]) { + if (labels && labels.length) { + this.modelValue.labels = labels; + this.modelValue.label = labels.join(' / '); + this.propagateChange(this.modelValue); + } else { + this.propagateChange(this.required ? null : this.modelValue); + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html new file mode 100644 index 0000000000..793b8d4c60 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html @@ -0,0 +1,254 @@ + +
+ +
+
+
+ +
+ + + +
+ + + + + + + +
+
+
+ + + + {{ ruleNodeTypeDescriptorsMap.get(ruleNodeType).icon }} +
{{ ruleNodeTypeDescriptorsMap.get(ruleNodeType).name }}
+
+
+ + +
+
+
+ + +
+
+
+ + + + + + + + + +
+
+
{{editingRuleNode.component.name}}
+
 
+
{{editingRuleNode.component.configurationDescriptor.nodeDefinition.description}}
+
 
+
+
+
+
+
+
+ +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ {{contextInfo.icon}} + +
+
{{contextInfo.title}}
+
{{contextInfo.subtitle}}
+
+
+
+ + +
+
+
+
+ + +
+ +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.scss b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.scss new file mode 100644 index 0000000000..b4a9a85a08 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.scss @@ -0,0 +1,382 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import './rule-node-colors'; + +.tb-rulechain { + width: 100%; + height: 100%; + + .mat-tab-group.tb-rulenode-details { + > .mat-tab-body-wrapper { + position: absolute; + top: 49px; + left: 0; + right: 0; + bottom: 0; + } + > .mat-tab-header { + .mat-tab-label { + min-width: 40px; + } + } + } + + button.mat-button.mat-icon-button.tb-fullscreen-button { + position: absolute; + top: 10px; + right: 10px; + background: #ccc; + color: #666; + opacity: .85; + margin: 0 6px; + z-index: 2; + } + section.tb-header-buttons.tb-library-open { + position: absolute; + top: 0; + left: 0; + z-index: 2; + pointer-events: none; + + .mat-fab { + .mat-button-wrapper { + padding: 0; + } + } + + .mat-button.tb-btn-open-library { + top: 0; + left: 0; + width: 36px; + height: 36px; + margin: 4px 0 0 4px; + line-height: 36px; + opacity: .5; + } + } + + .tb-rulechain-library { + z-index: 1; + width: 250px; + min-width: 250px; + + .mat-toolbar { + height: 48px; + min-height: 48px; + padding: 0; + + .mat-toolbar-tools { + height: 48px; + padding: 0 6px; + font-size: 14px; + + .mat-form-field { + .mat-form-field-infix { + width: auto; + } + } + + .mat-button.mat-icon-button { + margin: 0; + + &.tb-small { + width: 32px; + height: 32px; + min-height: 32px; + padding: 6px; + line-height: 20px; + mat-icon { + width: 20px; + min-width: 20px; + height: 20px; + min-height: 20px; + font-size: 20px; + line-height: 20px; + } + } + } + } + } + .tb-rulechain-library-panel-group { + overflow-x: hidden; + overflow-y: auto; + .mat-expansion-panel { + border-radius: 0; + &:last-child { + margin-bottom: 5px; + } + .mat-expansion-panel-header { + background: #e6e6e6; + &:hover { + background: #dadada; + } + .mat-expansion-panel-header-title { + line-height: 48px; + height: 48px; + .mat-icon { + min-width: 24px; + margin: auto 8px auto auto; + } + .tb-panel-title { + min-width: 140px; + user-select: none; + } + } + } + &.mat-expanded { + .mat-expansion-panel-header { + border-bottom: 1px solid; + border-color: #909090; + } + } + } + .mat-expansion-panel-body { + padding: 0; + } + .fc-canvas { + background: #f9f9f9; + } + } + } + .mat-drawer-content.tb-rulechain-graph-content { + overflow: hidden; + .tb-rulechain-graph { + z-index: 0; + overflow: auto; + } + } + .fc-canvas { + .fc-node { + border-radius: 8px; + &.fc-selected { + &:not(.fc-edit) { + margin: -3px; + border: solid 3px #f00; + } + } + } + + .fc-edit { + .fc-nodeedit, + .fc-nodedelete { + box-sizing: content-box; + border: solid 2px #fff; + background: #f83e05; + outline: none; + } + } + + .fc-nodeopen{ + display: block; + position: absolute; + top: 11px; + right: -12px; + border: 1px solid #FFFFFF; + border-radius: 4px; + line-height: 18px; + height: 22px; + width: 22px; + background: #886CB1; + color: #fff; + text-align: center; + cursor: pointer; + box-sizing: border-box; + + mat-icon{ + width: 16px; + min-width: 16px; + height: 16px; + min-height: 16px; + font-size: 16px; + } + + &:hover{ + background-color: #4E2D7E; + } + } + + .fc-arrow-marker { + polygon { + fill: #808080; + stroke: #808080; + } + } + + .fc-arrow-marker-selected { + polygon { + fill: #f00; + stroke: #f00; + } + } + + .fc-edge { + outline: none; + stroke: #808080; + + &.fc-selected { + stroke: #f00; + } + + &.fc-hover { + stroke: #808080; + } + } + + .edge-endpoint { + fill: #808080; + } + + .fc-edge-label { + opacity: 1 !important; + &:focus { + outline: 0; + } + + &.fc-selected { + .fc-edge-label-text { + span { + color: #fff !important; + background-color: #f00 !important; + border: solid #f00 !important; + } + } + } + + .fc-edge-label-text { + font-size: 14px !important; + font-weight: 600 !important; + + span { + background-color: #fff !important; + color: #003a79 !important; + border: solid 2px #003a79 !important; + } + } + } + } +} + +.tb-rule-node-tooltip, +.tb-rule-node-help { + color: #333; +} + +.tb-rule-node-tooltip { + max-width: 300px; + font-size: 14px; + + &.tb-lib-tooltip { + width: 300px; + } +} + +.tb-rule-node-help { + font-size: 16px; +} + +.tb-rule-node-error-tooltip { + font-size: 16px; + color: #ea0d0d; +} + +.tb-rule-node-tooltip, +.tb-rule-node-error-tooltip, +.tb-rule-node-help { + #tb-node-content { + .tb-node-title { + font-weight: 600; + } + + .tb-node-description { + font-style: italic; + color: #555; + } + + .tb-node-details { + padding-top: 10px; + padding-bottom: 10px; + } + + code { + padding: 0 3px 2px 3px; + margin: 1px; + font-size: 12px; + color: #ad1625; + white-space: nowrap; + background-color: #f7f7f9; + border: 1px solid #e1e1e8; + border-radius: 2px; + } + } +} + +.tb-rule-chain-context-menu { + min-width: 256px; + max-height: 404px; + border-radius: 8px; + margin-left: -20px; + + &.mat-menu-below { + margin-top: -60px; + } + + .mat-menu-content { + padding: 0; + display: flex; + flex-direction: column; + .tb-rule-chain-context-menu-container { + pointer-events: auto; + padding: 0 0 8px; + display: flex; + flex-direction: column; + overflow-y: auto; + } + } + + .tb-context-menu-header { + display: flex; + box-sizing: content-box; + flex-direction: row; + height: 36px; + min-height: 36px; + padding: 8px 5px 5px; + font-size: 14px; + + @include rule-node-colors(); + + &.tb-rulechain-header { + background-color: #aac7e4; + } + + &.tb-link-header { + background-color: #aac7e4; + } + + .mat-icon { + padding-right: 10px; + padding-left: 2px; + margin: auto; + } + + .tb-context-menu-title { + font-weight: 500; + } + + .tb-context-menu-subtitle { + font-size: 12px; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts new file mode 100644 index 0000000000..7096045aed --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts @@ -0,0 +1,1559 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + Component, + ElementRef, + HostBinding, + Inject, + OnDestroy, + OnInit, + QueryList, + SkipSelf, + ViewChild, + ViewChildren, + ViewEncapsulation +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard'; +import { TranslateService } from '@ngx-translate/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { MatExpansionPanel } from '@angular/material/expansion'; +import { DialogService } from '@core/services/dialog.service'; +import { AuthService } from '@core/auth/auth.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { + inputNodeComponent, + NodeConnectionInfo, + ResolvedRuleChainMetaData, + RuleChain, + RuleChainConnectionInfo, + RuleChainImport, + RuleChainMetaData, + ruleChainNodeComponent +} from '@shared/models/rule-chain.models'; +import { FcItemInfo, FlowchartConstants, NgxFlowchartComponent, UserCallbacks } from 'ngx-flowchart/dist/ngx-flowchart'; +import { + FcRuleEdge, + FcRuleNode, + FcRuleNodeType, + getRuleNodeHelpLink, + LinkLabel, + RuleNode, + RuleNodeComponentDescriptor, + RuleNodeType, + ruleNodeTypeDescriptors, + ruleNodeTypesLibrary +} from '@shared/models/rule-node.models'; +import { FcRuleNodeModel, FcRuleNodeTypeModel, RuleChainMenuContextInfo } from './rulechain-page.models'; +import { RuleChainService } from '@core/http/rule-chain.service'; +import { fromEvent, NEVER, Observable, of, Subscription } from 'rxjs'; +import { debounceTime, distinctUntilChanged, mergeMap, tap } from 'rxjs/operators'; +import { ISearchableComponent } from '../../models/searchable-component.models'; +import { deepClone } from '@core/utils'; +import { RuleNodeDetailsComponent } from '@home/pages/rulechain/rule-node-details.component'; +import { RuleNodeLinkComponent } from './rule-node-link.component'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { MatMenuTrigger } from '@angular/material/menu'; +import { ItemBufferService, RuleNodeConnection } from '@core/services/item-buffer.service'; +import { Hotkey } from 'angular2-hotkeys'; +import { EntityType } from '@shared/models/entity-type.models'; +import { DebugEventType, EventType } from '@shared/models/event.models'; +import Timeout = NodeJS.Timeout; + +@Component({ + selector: 'tb-rulechain-page', + templateUrl: './rulechain-page.component.html', + styleUrls: ['./rulechain-page.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class RuleChainPageComponent extends PageComponent + implements AfterViewInit, OnInit, OnDestroy, HasDirtyFlag, ISearchableComponent { + + get isDirty(): boolean { + return this.isDirtyValue || this.isImport; + } + + @HostBinding('style.width') width = '100%'; + @HostBinding('style.height') height = '100%'; + + @ViewChild('ruleNodeSearchInput') ruleNodeSearchInputField: ElementRef; + + @ViewChild('ruleChainCanvas', {static: true}) ruleChainCanvas: NgxFlowchartComponent; + + @ViewChildren('ruleNodeTypeExpansionPanels', + {read: MatExpansionPanel}) expansionPanels: QueryList; + + @ViewChild('ruleChainMenuTrigger', {static: true}) ruleChainMenuTrigger: MatMenuTrigger; + + eventTypes = EventType; + + debugEventTypes = DebugEventType; + + ruleChainMenuPosition = { x: '0px', y: '0px' }; + + contextMenuEvent: MouseEvent; + + ruleNodeTypeDescriptorsMap = ruleNodeTypeDescriptors; + ruleNodeTypesLibraryArray = ruleNodeTypesLibrary; + + isImport: boolean; + isDirtyValue: boolean; + isInvalid = false; + + errorTooltips: {[nodeId: string]: JQueryTooltipster.ITooltipsterInstance} = {}; + isFullscreen = false; + + selectedRuleNodeTabIndex = 0; + editingRuleNode: FcRuleNode = null; + isEditingRuleNode = false; + editingRuleNodeIndex = -1; + editingRuleNodeAllowCustomLabels = false; + editingRuleNodeLinkLabels: {[label: string]: LinkLabel}; + + @ViewChild('tbRuleNode') ruleNodeComponent: RuleNodeDetailsComponent; + @ViewChild('tbRuleNodeLink') ruleNodeLinkComponent: RuleNodeLinkComponent; + + editingRuleNodeLink: FcRuleEdge = null; + isEditingRuleNodeLink = false; + editingRuleNodeLinkIndex = -1; + + hotKeys: Hotkey[] = []; + + enableHotKeys = true; + isLibraryOpen = true; + + ruleNodeSearch = ''; + ruleNodeTypeSearch = ''; + + ruleChain: RuleChain; + ruleChainMetaData: ResolvedRuleChainMetaData; + + ruleChainModel: FcRuleNodeModel = { + nodes: [], + edges: [] + }; + selectedObjects = []; + + editCallbacks: UserCallbacks = { + edgeDoubleClick: (event, edge) => { + this.openLinkDetails(edge); + }, + edgeEdit: (event, edge) => { + this.openLinkDetails(edge); + }, + nodeCallbacks: { + doubleClick: (event, node: FcRuleNode) => { + this.openNodeDetails(node); + }, + nodeEdit: (event, node: FcRuleNode) => { + this.openNodeDetails(node); + }, + mouseEnter: this.displayNodeDescriptionTooltip.bind(this), + mouseLeave: this.destroyTooltips.bind(this), + mouseDown: this.destroyTooltips.bind(this) + }, + isValidEdge: (source, destination) => { + return source.type === FlowchartConstants.rightConnectorType && destination.type === FlowchartConstants.leftConnectorType; + }, + createEdge: (event, edge: FcRuleEdge) => { + const sourceNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.source) as FcRuleNode; + if (sourceNode.component.type === RuleNodeType.INPUT) { + const destNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.destination) as FcRuleNode; + if (destNode.component.type === RuleNodeType.RULE_CHAIN) { + return NEVER; + } else { + const found = this.ruleChainModel.edges.find(theEdge => theEdge.source === (this.inputConnectorId + '')); + if (found) { + this.ruleChainCanvas.modelService.edges.delete(found); + } + return of(edge); + } + } else { + if (edge.label) { + if (!edge.labels) { + edge.labels = edge.label.split(' / '); + } + return of(edge); + } else { + const labels = this.ruleChainService.getRuleNodeSupportedLinks(sourceNode.component); + const allowCustomLabels = this.ruleChainService.ruleNodeAllowCustomLinks(sourceNode.component); + this.enableHotKeys = false; + return this.addRuleNodeLink(edge, labels, allowCustomLabels).pipe( + tap(() => { + this.enableHotKeys = true; + }), + mergeMap((res) => { + if (res) { + return of(res); + } else { + return NEVER; + } + }) + ); + } + } + }, + dropNode: (event, node: FcRuleNode) => { + this.addRuleNode(node); + } + }; + + nextNodeID: number; + nextConnectorID: number; + inputConnectorId: number; + + ruleNodeTypesModel: {[type: string]: {model: FcRuleNodeTypeModel, selectedObjects: any[]}} = {}; + + nodeLibCallbacks: UserCallbacks = { + nodeCallbacks: { + mouseEnter: this.displayLibNodeDescriptionTooltip.bind(this), + mouseLeave: this.destroyTooltips.bind(this), + mouseDown: this.destroyTooltips.bind(this) + } + }; + + ruleNodeComponents: Array; + + flowchartConstants = FlowchartConstants; + + private rxSubscription: Subscription; + + private tooltipTimeout: Timeout; + + constructor(protected store: Store, + private route: ActivatedRoute, + private router: Router, + private ruleChainService: RuleChainService, + private authService: AuthService, + private translate: TranslateService, + private itembuffer: ItemBufferService, + public dialog: MatDialog, + public dialogService: DialogService, + public fb: FormBuilder) { + super(store); + + this.rxSubscription = this.route.data.subscribe( + () => { + this.reset(); + this.init(); + } + ); + } + + ngOnInit() { + } + + ngAfterViewInit() { + fromEvent(this.ruleNodeSearchInputField.nativeElement, 'keyup') + .pipe( + debounceTime(150), + distinctUntilChanged(), + tap(() => { + this.updateRuleChainLibrary(); + }) + ) + .subscribe(); + this.ruleChainCanvas.adjustCanvasSize(true); + } + + ngOnDestroy() { + super.ngOnDestroy(); + this.rxSubscription.unsubscribe(); + } + + onSearchTextUpdated(searchText: string) { + this.ruleNodeSearch = searchText; + this.updateRuleNodesHighlight(); + } + + private init() { + this.initHotKeys(); + this.isImport = this.route.snapshot.data.import; + if (this.isImport) { + const ruleChainImport: RuleChainImport = this.itembuffer.getRuleChainImport(); + this.ruleChain = ruleChainImport.ruleChain; + this.ruleChainMetaData = ruleChainImport.resolvedMetadata; + } else { + this.ruleChain = this.route.snapshot.data.ruleChain; + this.ruleChainMetaData = this.route.snapshot.data.ruleChainMetaData; + } + this.ruleNodeComponents = this.route.snapshot.data.ruleNodeComponents; + for (const type of ruleNodeTypesLibrary) { + const desc = ruleNodeTypeDescriptors.get(type); + if (!desc.special) { + this.ruleNodeTypesModel[type] = { + model: { + nodes: [], + edges: [] + }, + selectedObjects: [] + }; + } + } + this.updateRuleChainLibrary(); + this.createRuleChainModel(); + } + + private reset(): void { + this.selectedObjects = []; + this.ruleChainModel.nodes = []; + this.ruleChainModel.edges = []; + this.ruleNodeTypesModel = {}; + if (this.ruleChainCanvas) { + this.ruleChainCanvas.adjustCanvasSize(true); + } + this.isEditingRuleNode = false; + this.isEditingRuleNodeLink = false; + this.updateRuleNodesHighlight(); + } + + private initHotKeys(): void { + if (!this.hotKeys.length) { + this.hotKeys.push( + new Hotkey('ctrl+a', (event: KeyboardEvent) => { + if (this.enableHotKeys) { + event.preventDefault(); + this.ruleChainCanvas.modelService.selectAll(); + return false; + } + return true; + }, ['INPUT', 'SELECT', 'TEXTAREA'], + this.translate.instant('rulenode.select-all-objects')) + ); + this.hotKeys.push( + new Hotkey('ctrl+c', (event: KeyboardEvent) => { + if (this.enableHotKeys) { + event.preventDefault(); + this.copyRuleNodes(); + return false; + } + return true; + }, ['INPUT', 'SELECT', 'TEXTAREA'], + this.translate.instant('rulenode.copy-selected')) + ); + this.hotKeys.push( + new Hotkey('ctrl+v', (event: KeyboardEvent) => { + if (this.enableHotKeys) { + event.preventDefault(); + if (this.itembuffer.hasRuleNodes()) { + this.pasteRuleNodes(); + } + return false; + } + return true; + }, ['INPUT', 'SELECT', 'TEXTAREA'], + this.translate.instant('action.paste')) + ); + this.hotKeys.push( + new Hotkey('esc', (event: KeyboardEvent) => { + if (this.enableHotKeys) { + event.preventDefault(); + event.stopPropagation(); + this.ruleChainCanvas.modelService.deselectAll(); + return false; + } + return true; + }, ['INPUT', 'SELECT', 'TEXTAREA'], + this.translate.instant('rulenode.deselect-all-objects')) + ); + this.hotKeys.push( + new Hotkey('ctrl+s', (event: KeyboardEvent) => { + if (this.enableHotKeys) { + event.preventDefault(); + this.saveRuleChain(); + return false; + } + return true; + }, ['INPUT', 'SELECT', 'TEXTAREA'], + this.translate.instant('action.apply')) + ); + this.hotKeys.push( + new Hotkey('ctrl+z', (event: KeyboardEvent) => { + if (this.enableHotKeys) { + event.preventDefault(); + this.revertRuleChain(); + return false; + } + return true; + }, ['INPUT', 'SELECT', 'TEXTAREA'], + this.translate.instant('action.decline-changes')) + ); + this.hotKeys.push( + new Hotkey('del', (event: KeyboardEvent) => { + if (this.enableHotKeys) { + event.preventDefault(); + this.ruleChainCanvas.modelService.deleteSelected(); + return false; + } + return true; + }, ['INPUT', 'SELECT', 'TEXTAREA'], + this.translate.instant('rulenode.delete-selected-objects')) + ); + } + } + + updateRuleChainLibrary() { + const search = this.ruleNodeTypeSearch.toUpperCase(); + const res = this.ruleNodeComponents.filter( + (ruleNodeComponent) => ruleNodeComponent.name.toUpperCase().includes(search)); + this.loadRuleChainLibrary(res); + } + + private loadRuleChainLibrary(ruleNodeComponents: Array) { + for (const componentType of Object.keys(this.ruleNodeTypesModel)) { + this.ruleNodeTypesModel[componentType].model.nodes.length = 0; + } + ruleNodeComponents.forEach((ruleNodeComponent) => { + const componentType = ruleNodeComponent.type; + const model = this.ruleNodeTypesModel[componentType].model; + const desc = ruleNodeTypeDescriptors.get(RuleNodeType[componentType]); + let icon = desc.icon; + let iconUrl = null; + if (ruleNodeComponent.configurationDescriptor.nodeDefinition.icon) { + icon = ruleNodeComponent.configurationDescriptor.nodeDefinition.icon; + } + if (ruleNodeComponent.configurationDescriptor.nodeDefinition.iconUrl) { + iconUrl = ruleNodeComponent.configurationDescriptor.nodeDefinition.iconUrl; + } + const node: FcRuleNodeType = { + id: 'node-lib-' + componentType + '-' + model.nodes.length, + component: ruleNodeComponent, + name: '', + nodeClass: desc.nodeClass, + icon, + iconUrl, + x: 30, + y: 10 + 50 * model.nodes.length, + connectors: [] + }; + if (ruleNodeComponent.configurationDescriptor.nodeDefinition.inEnabled) { + node.connectors.push( + { + type: FlowchartConstants.leftConnectorType, + id: (model.nodes.length * 2) + '' + } + ); + } + if (ruleNodeComponent.configurationDescriptor.nodeDefinition.outEnabled) { + node.connectors.push( + { + type: FlowchartConstants.rightConnectorType, + id: (model.nodes.length * 2 + 1) + '' + } + ); + } + model.nodes.push(node); + }); + if (this.expansionPanels) { + for (let i = 0; i < ruleNodeTypesLibrary.length; i++) { + const panel = this.expansionPanels.find((item, index) => { + return index === i; + }); + if (panel) { + const type = ruleNodeTypesLibrary[i]; + if (!this.ruleNodeTypesModel[type].model.nodes.length) { + panel.close(); + } else { + panel.open(); + } + } + } + } + } + + private createRuleChainModel() { + this.nextNodeID = 1; + this.nextConnectorID = 1; + + this.selectedObjects = []; + this.ruleChainModel.nodes = []; + this.ruleChainModel.edges = []; + + this.inputConnectorId = this.nextConnectorID++; + this.ruleChainModel.nodes.push( + { + id: 'rule-chain-node-' + this.nextNodeID++, + component: inputNodeComponent, + name: '', + nodeClass: ruleNodeTypeDescriptors.get(RuleNodeType.INPUT).nodeClass, + icon: ruleNodeTypeDescriptors.get(RuleNodeType.INPUT).icon, + readonly: true, + x: 50, + y: 150, + connectors: [ + { + type: FlowchartConstants.rightConnectorType, + id: this.inputConnectorId + '' + }, + ] + + } + ); + const nodes: FcRuleNode[] = []; + this.ruleChainMetaData.nodes.forEach((ruleNode) => { + const component = this.ruleChainService.getRuleNodeComponentByClazz(ruleNode.type); + const descriptor = ruleNodeTypeDescriptors.get(component.type); + let icon = descriptor.icon; + let iconUrl = null; + if (component.configurationDescriptor.nodeDefinition.icon) { + icon = component.configurationDescriptor.nodeDefinition.icon; + } + if (component.configurationDescriptor.nodeDefinition.iconUrl) { + iconUrl = component.configurationDescriptor.nodeDefinition.iconUrl; + } + const node: FcRuleNode = { + id: 'rule-chain-node-' + this.nextNodeID++, + ruleNodeId: ruleNode.id, + additionalInfo: ruleNode.additionalInfo, + configuration: ruleNode.configuration, + debugMode: ruleNode.debugMode, + x: Math.round(ruleNode.additionalInfo.layoutX), + y: Math.round(ruleNode.additionalInfo.layoutY), + component, + name: ruleNode.name, + nodeClass: descriptor.nodeClass, + icon, + iconUrl, + connectors: [] + }; + if (component.configurationDescriptor.nodeDefinition.inEnabled) { + node.connectors.push( + { + type: FlowchartConstants.leftConnectorType, + id: (this.nextConnectorID++) + '' + } + ); + } + if (component.configurationDescriptor.nodeDefinition.outEnabled) { + node.connectors.push( + { + type: FlowchartConstants.rightConnectorType, + id: (this.nextConnectorID++) + '' + } + ); + } + nodes.push(node); + this.ruleChainModel.nodes.push(node); + }); + if (this.ruleChainMetaData.firstNodeIndex > -1) { + const destNode = nodes[this.ruleChainMetaData.firstNodeIndex]; + if (destNode) { + const connectors = destNode.connectors.filter(connector => connector.type === FlowchartConstants.leftConnectorType); + if (connectors && connectors.length) { + const edge: FcRuleEdge = { + source: this.inputConnectorId + '', + destination: connectors[0].id + }; + this.ruleChainModel.edges.push(edge); + } + } + } + if (this.ruleChainMetaData.connections) { + const edgeMap: {[edgeKey: string]: FcRuleEdge} = {}; + this.ruleChainMetaData.connections.forEach((connection) => { + const sourceNode = nodes[connection.fromIndex]; + const destNode = nodes[connection.toIndex]; + if (sourceNode && destNode) { + const sourceConnectors = sourceNode.connectors.filter(connector => connector.type === FlowchartConstants.rightConnectorType); + const destConnectors = destNode.connectors.filter(connector => connector.type === FlowchartConstants.leftConnectorType); + if (sourceConnectors && sourceConnectors.length && destConnectors && destConnectors.length) { + const sourceId = sourceConnectors[0].id; + const destId = destConnectors[0].id; + const edgeKey = sourceId + '_' + destId; + let edge = edgeMap[edgeKey]; + if (!edge) { + edge = { + source: sourceId, + destination: destId, + label: connection.type, + labels: [connection.type] + }; + edgeMap[edgeKey] = edge; + this.ruleChainModel.edges.push(edge); + } else { + edge.label += ' / ' + connection.type; + edge.labels.push(connection.type); + } + } + } + }); + } + if (this.ruleChainMetaData.ruleChainConnections) { + const ruleChainsMap = this.ruleChainMetaData.targetRuleChainsMap; + const ruleChainNodesMap: {[ruleChainNodeId: string]: FcRuleNode} = {}; + const ruleChainEdgeMap: {[edgeKey: string]: FcRuleEdge} = {}; + this.ruleChainMetaData.ruleChainConnections.forEach((ruleChainConnection) => { + const ruleChain = ruleChainsMap[ruleChainConnection.targetRuleChainId.id]; + if (ruleChainConnection.additionalInfo && ruleChainConnection.additionalInfo.ruleChainNodeId) { + let ruleChainNode = ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId]; + if (!ruleChainNode) { + ruleChainNode = { + id: 'rule-chain-node-' + this.nextNodeID++, + name: ruleChain.name ? ruleChain.name : 'Unresolved', + targetRuleChainId: ruleChain.name ? ruleChainConnection.targetRuleChainId.id : null, + error: ruleChain.name ? undefined : this.translate.instant('rulenode.invalid-target-rulechain'), + additionalInfo: ruleChainConnection.additionalInfo, + x: Math.round(ruleChainConnection.additionalInfo.layoutX), + y: Math.round(ruleChainConnection.additionalInfo.layoutY), + component: ruleChainNodeComponent, + nodeClass: ruleNodeTypeDescriptors.get(RuleNodeType.RULE_CHAIN).nodeClass, + icon: ruleNodeTypeDescriptors.get(RuleNodeType.RULE_CHAIN).icon, + connectors: [ + { + type: FlowchartConstants.leftConnectorType, + id: (this.nextConnectorID++) + '' + } + ] + }; + ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId] = ruleChainNode; + this.ruleChainModel.nodes.push(ruleChainNode); + } + const sourceNode = nodes[ruleChainConnection.fromIndex]; + if (sourceNode) { + const connectors = sourceNode.connectors.filter(connector => connector.type === FlowchartConstants.rightConnectorType); + if (connectors && connectors.length) { + const sourceId = connectors[0].id; + const destId = ruleChainNode.connectors[0].id; + const edgeKey = sourceId + '_' + destId; + let ruleChainEdge = ruleChainEdgeMap[edgeKey]; + if (!ruleChainEdge) { + ruleChainEdge = { + source: sourceId, + destination: destId, + label: ruleChainConnection.type, + labels: [ruleChainConnection.type] + }; + ruleChainEdgeMap[edgeKey] = ruleChainEdge; + this.ruleChainModel.edges.push(ruleChainEdge); + } else { + ruleChainEdge.label += ' / ' + ruleChainConnection.type; + ruleChainEdge.labels.push(ruleChainConnection.type); + } + } + } + } + }); + } + if (this.ruleChainCanvas) { + this.ruleChainCanvas.adjustCanvasSize(true); + } + this.isDirtyValue = false; + this.updateRuleNodesHighlight(); + this.validate(); + } + + openRuleChainContextMenu($event: MouseEvent) { + if (this.ruleChainCanvas.modelService && !$event.ctrlKey && !$event.metaKey) { + const x = $event.clientX; + const y = $event.clientY; + const item = this.ruleChainCanvas.modelService.getItemInfoAtPoint(x, y); + const contextInfo = this.prepareContextMenu(item); + if (contextInfo.menuItems && contextInfo.menuItems.length > 0) { + $event.preventDefault(); + $event.stopPropagation(); + this.contextMenuEvent = $event; + this.ruleChainMenuPosition.x = x + 'px'; + this.ruleChainMenuPosition.y = y + 'px'; + this.ruleChainMenuTrigger.menuData = { contextInfo }; + this.ruleChainMenuTrigger.openMenu(); + } + } + } + + onRuleChainContextMenuMouseLeave() { + this.ruleChainMenuTrigger.closeMenu(); + } + + private prepareContextMenu(item: FcItemInfo): RuleChainMenuContextInfo { + if (this.objectsSelected() || (!item.node && !item.edge)) { + return this.prepareRuleChainContextMenu(); + } else if (item.node) { + return this.prepareRuleNodeContextMenu(item.node); + } else if (item.edge) { + return this.prepareEdgeContextMenu(item.edge); + } + } + + private prepareRuleChainContextMenu(): RuleChainMenuContextInfo { + const contextInfo: RuleChainMenuContextInfo = { + headerClass: 'tb-rulechain-header', + icon: 'settings_ethernet', + title: this.ruleChain.name, + subtitle: this.translate.instant('rulechain.rulechain'), + menuItems: [] + }; + if (this.ruleChainCanvas.modelService.nodes.getSelectedNodes().length) { + contextInfo.menuItems.push( + { + action: () => { + this.copyRuleNodes(); + }, + enabled: true, + value: 'rulenode.copy-selected', + icon: 'content_copy', + shortcut: 'M-C' + } + ); + } + contextInfo.menuItems.push( + { + action: ($event) => { + this.pasteRuleNodes($event); + }, + enabled: this.itembuffer.hasRuleNodes(), + value: 'action.paste', + icon: 'content_paste', + shortcut: 'M-V' + } + ); + contextInfo.menuItems.push( + { + divider: true + } + ); + if (this.objectsSelected()) { + contextInfo.menuItems.push( + { + action: () => { + this.ruleChainCanvas.modelService.deselectAll(); + }, + enabled: true, + value: 'rulenode.deselect-all', + icon: 'tab_unselected', + shortcut: 'Esc' + } + ); + contextInfo.menuItems.push( + { + action: () => { + this.ruleChainCanvas.modelService.deleteSelected(); + }, + enabled: true, + value: 'rulenode.delete-selected', + icon: 'clear', + shortcut: 'Del' + } + ); + } else { + contextInfo.menuItems.push( + { + action: () => { + this.ruleChainCanvas.modelService.selectAll(); + }, + enabled: true, + value: 'rulenode.select-all', + icon: 'select_all', + shortcut: 'M-A' + } + ); + } + contextInfo.menuItems.push( + { + divider: true + } + ); + contextInfo.menuItems.push( + { + action: () => { + this.saveRuleChain(); + }, + enabled: !(this.isInvalid || (!this.isDirty && !this.isImport)), + value: 'action.apply-changes', + icon: 'done', + shortcut: 'M-S' + } + ); + contextInfo.menuItems.push( + { + action: () => { + this.revertRuleChain(); + }, + enabled: this.isDirty, + value: 'action.decline-changes', + icon: 'close', + shortcut: 'M-Z' + } + ); + return contextInfo; + } + + private prepareRuleNodeContextMenu(node: FcRuleNode): RuleChainMenuContextInfo { + const contextInfo: RuleChainMenuContextInfo = { + headerClass: node.nodeClass, + icon: node.icon, + iconUrl: node.iconUrl, + title: node.name, + subtitle: node.component.name, + menuItems: [] + }; + if (!node.readonly) { + contextInfo.menuItems.push( + { + action: () => { + this.openNodeDetails(node); + }, + enabled: true, + value: 'rulenode.details', + icon: 'menu' + } + ); + contextInfo.menuItems.push( + { + action: () => { + this.copyNode(node); + }, + enabled: true, + value: 'action.copy', + icon: 'content_copy' + } + ); + contextInfo.menuItems.push( + { + action: () => { + this.ruleChainCanvas.modelService.nodes.delete(node); + }, + enabled: true, + value: 'action.delete', + icon: 'clear', + shortcut: 'M-X' + } + ); + } + return contextInfo; + } + + private prepareEdgeContextMenu(edge: FcRuleEdge): RuleChainMenuContextInfo { + const contextInfo: RuleChainMenuContextInfo = { + headerClass: 'tb-link-header', + icon: 'trending_flat', + title: edge.label, + subtitle: this.translate.instant('rulenode.link'), + menuItems: [] + }; + const sourceNode: FcRuleNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.source); + if (sourceNode.component.type !== RuleNodeType.INPUT) { + contextInfo.menuItems.push( + { + action: () => { + this.openLinkDetails(edge); + }, + enabled: true, + value: 'rulenode.details', + icon: 'menu' + } + ); + } + contextInfo.menuItems.push( + { + action: () => { + this.ruleChainCanvas.modelService.edges.delete(edge); + }, + enabled: true, + value: 'action.delete', + icon: 'clear', + shortcut: 'M-X' + } + ); + return contextInfo; + } + + onModelChanged() { + this.isDirtyValue = true; + this.validate(); + } + + helpLinkIdForRuleNodeType(): string { + let component: RuleNodeComponentDescriptor = null; + if (this.editingRuleNode) { + component = this.editingRuleNode.component; + } + return getRuleNodeHelpLink(component); + } + + openNodeDetails(node: FcRuleNode) { + if (node.component.type !== RuleNodeType.INPUT) { + this.enableHotKeys = false; + this.updateErrorTooltips(true); + this.isEditingRuleNodeLink = false; + this.editingRuleNodeLink = null; + this.isEditingRuleNode = true; + this.editingRuleNodeIndex = this.ruleChainModel.nodes.indexOf(node); + this.editingRuleNode = deepClone(node, ['component']); + setTimeout(() => { + this.ruleNodeComponent.ruleNodeFormGroup.markAsPristine(); + }, 0); + } + } + + openLinkDetails(edge: FcRuleEdge) { + const sourceNode: FcRuleNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.source) as FcRuleNode; + if (sourceNode.component.type !== RuleNodeType.INPUT) { + this.enableHotKeys = false; + this.updateErrorTooltips(true); + this.isEditingRuleNode = false; + this.editingRuleNode = null; + this.editingRuleNodeLinkLabels = this.ruleChainService.getRuleNodeSupportedLinks(sourceNode.component); + this.editingRuleNodeAllowCustomLabels = this.ruleChainService.ruleNodeAllowCustomLinks(sourceNode.component); + this.isEditingRuleNodeLink = true; + this.editingRuleNodeLinkIndex = this.ruleChainModel.edges.indexOf(edge); + this.editingRuleNodeLink = deepClone(edge); + setTimeout(() => { + this.ruleNodeLinkComponent.ruleNodeLinkFormGroup.markAsPristine(); + }, 0); + } + } + + private copyNode(node: FcRuleNode) { + this.itembuffer.copyRuleNodes([node], []); + } + + private copyRuleNodes() { + const nodes: FcRuleNode[] = this.ruleChainCanvas.modelService.nodes.getSelectedNodes(); + const edges: FcRuleEdge[] = this.ruleChainCanvas.modelService.edges.getSelectedEdges(); + const connections: RuleNodeConnection[] = []; + edges.forEach((edge) => { + const sourceNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.source); + const destNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.destination); + const isInputSource = sourceNode.component.type === RuleNodeType.INPUT; + const fromIndex = nodes.indexOf(sourceNode); + const toIndex = nodes.indexOf(destNode); + if ( (isInputSource || fromIndex > -1) && toIndex > -1 ) { + const connection: RuleNodeConnection = { + isInputSource, + fromIndex, + toIndex, + label: edge.label, + labels: edge.labels + }; + connections.push(connection); + } + }); + this.itembuffer.copyRuleNodes(nodes, connections); + } + + private pasteRuleNodes(event?: MouseEvent) { + const canvas = $(this.ruleChainCanvas.modelService.canvasHtmlElement); + let x: number; + let y: number; + if (event) { + const offset = canvas.offset(); + x = Math.round(event.clientX - offset.left); + y = Math.round(event.clientY - offset.top); + } else { + const scrollParent = canvas.parent(); + const scrollTop = scrollParent.scrollTop(); + const scrollLeft = scrollParent.scrollLeft(); + x = scrollLeft + scrollParent.width() / 2; + y = scrollTop + scrollParent.height() / 2; + } + const ruleNodes = this.itembuffer.pasteRuleNodes(x, y); + if (ruleNodes) { + this.ruleChainCanvas.modelService.deselectAll(); + const nodes: FcRuleNode[] = []; + ruleNodes.nodes.forEach((node) => { + node.id = 'rule-chain-node-' + this.nextNodeID++; + const component = node.component; + if (component.configurationDescriptor.nodeDefinition.inEnabled) { + node.connectors.push( + { + type: FlowchartConstants.leftConnectorType, + id: (this.nextConnectorID++) + '' + } + ); + } + if (component.configurationDescriptor.nodeDefinition.outEnabled) { + node.connectors.push( + { + type: FlowchartConstants.rightConnectorType, + id: (this.nextConnectorID++) + '' + } + ); + } + nodes.push(node); + this.ruleChainModel.nodes.push(node); + this.ruleChainCanvas.modelService.nodes.select(node); + }); + ruleNodes.connections.forEach((connection) => { + const sourceNode = nodes[connection.fromIndex]; + const destNode = nodes[connection.toIndex]; + if ( (connection.isInputSource || sourceNode) && destNode ) { + let source: string; + let destination: string; + if (connection.isInputSource) { + source = this.inputConnectorId + ''; + const found = this.ruleChainModel.edges.find(theEdge => theEdge.source === (this.inputConnectorId + '')); + if (found) { + this.ruleChainCanvas.modelService.edges.delete(found); + } + } else { + const sourceConnectors = this.ruleChainCanvas.modelService.nodes + .getConnectorsByType(sourceNode, FlowchartConstants.rightConnectorType); + if (sourceConnectors && sourceConnectors.length) { + source = sourceConnectors[0].id; + } + } + const destConnectors = this.ruleChainCanvas.modelService.nodes + .getConnectorsByType(destNode, FlowchartConstants.leftConnectorType); + if (destConnectors && destConnectors.length) { + destination = destConnectors[0].id; + } + if (source && destination) { + const edge: FcRuleEdge = { + source, + destination, + label: connection.label, + labels: connection.labels + }; + this.ruleChainModel.edges.push(edge); + this.ruleChainCanvas.modelService.edges.select(edge); + } + } + }); + this.updateRuleNodesHighlight(); + this.validate(); + this.onModelChanged(); + } + } + + onDetailsDrawerClosed() { + this.onEditRuleNodeClosed(); + this.onEditRuleNodeLinkClosed(); + this.enableHotKeys = true; + this.updateErrorTooltips(false); + } + + onEditRuleNodeClosed() { + this.editingRuleNode = null; + this.isEditingRuleNode = false; + } + + onEditRuleNodeLinkClosed() { + this.editingRuleNodeLink = null; + this.isEditingRuleNodeLink = false; + } + + onRevertRuleNodeEdit() { + this.ruleNodeComponent.ruleNodeFormGroup.markAsPristine(); + const node = this.ruleChainModel.nodes[this.editingRuleNodeIndex]; + this.editingRuleNode = deepClone(node, ['component']); + } + + onRevertRuleNodeLinkEdit() { + this.ruleNodeLinkComponent.ruleNodeLinkFormGroup.markAsPristine(); + const edge = this.ruleChainModel.edges[this.editingRuleNodeLinkIndex]; + this.editingRuleNodeLink = deepClone(edge); + } + + saveRuleNode() { + this.ruleNodeComponent.validate(); + if (this.ruleNodeComponent.ruleNodeFormGroup.valid) { + this.ruleNodeComponent.ruleNodeFormGroup.markAsPristine(); + if (this.editingRuleNode.error) { + delete this.editingRuleNode.error; + } + this.ruleChainModel.nodes[this.editingRuleNodeIndex] = this.editingRuleNode; + this.editingRuleNode = deepClone(this.editingRuleNode, ['component']); + this.onModelChanged(); + this.updateRuleNodesHighlight(); + } + } + + saveRuleNodeLink() { + this.ruleNodeLinkComponent.ruleNodeLinkFormGroup.markAsPristine(); + this.ruleChainModel.edges[this.editingRuleNodeLinkIndex] = this.editingRuleNodeLink; + this.editingRuleNodeLink = deepClone(this.editingRuleNodeLink); + this.onModelChanged(); + } + + typeHeaderMouseEnter(event: MouseEvent, ruleNodeType: RuleNodeType) { + const type = ruleNodeTypeDescriptors.get(ruleNodeType); + this.displayTooltip(event, + '
' + + '
' + + '
' + this.translate.instant(type.name) + '
' + + '
' + this.translate.instant(type.details) + '
' + + '
' + + '
' + ); + } + + displayLibNodeDescriptionTooltip(event: MouseEvent, node: FcRuleNodeType) { + this.displayTooltip(event, + '
' + + '
' + + '
' + node.component.name + '
' + + '
' + node.component.configurationDescriptor.nodeDefinition.description + '
' + + '
' + node.component.configurationDescriptor.nodeDefinition.details + '
' + + '
' + + '
' + ); + } + + displayNodeDescriptionTooltip(event: MouseEvent, node: FcRuleNode) { + if (!this.errorTooltips[node.id]) { + let name: string; + let desc: string; + let details: string; + if (node.component.type === RuleNodeType.INPUT) { + name = this.translate.instant(ruleNodeTypeDescriptors.get(RuleNodeType.INPUT).name); + desc = this.translate.instant(ruleNodeTypeDescriptors.get(RuleNodeType.INPUT).details); + } else { + name = node.name; + desc = this.translate.instant(ruleNodeTypeDescriptors.get(node.component.type).name) + ' - ' + node.component.name; + if (node.additionalInfo) { + details = node.additionalInfo.description; + } + } + let tooltipContent = '
' + + '
' + + '
' + name + '
' + + '
' + desc + '
'; + if (details) { + tooltipContent += '
' + details + '
'; + } + tooltipContent += '
' + + '
'; + this.displayTooltip(event, tooltipContent); + } + } + + destroyTooltips() { + if (this.tooltipTimeout) { + clearTimeout(this.tooltipTimeout); + this.tooltipTimeout = null; + } + const instances = $.tooltipster.instances(); + instances.forEach((instance) => { + if (!instance.isErrorTooltip) { + instance.destroy(); + } + }); + } + + private updateRuleNodesHighlight() { + for (const ruleNode of this.ruleChainModel.nodes) { + ruleNode.highlighted = false; + } + if (this.ruleNodeSearch) { + const search = this.ruleNodeSearch.toUpperCase(); + const res = this.ruleChainModel.nodes.filter(node => node.name.toUpperCase().includes(search)); + if (res) { + for (const ruleNode of res) { + ruleNode.highlighted = true; + } + } + } + if (this.ruleChainCanvas) { + this.ruleChainCanvas.modelService.detectChanges(); + } + } + + objectsSelected(): boolean { + return this.ruleChainCanvas.modelService.nodes.getSelectedNodes().length > 0 || + this.ruleChainCanvas.modelService.edges.getSelectedEdges().length > 0; + } + + deleteSelected() { + this.ruleChainCanvas.modelService.deleteSelected(); + } + + isDebugModeEnabled(): boolean { + const res = this.ruleChainModel.nodes.find((node) => node.debugMode); + return typeof res !== 'undefined'; + } + + resetDebugModeInAllNodes() { + let changed = false; + this.ruleChainModel.nodes.forEach((node) => { + if (node.component.type !== RuleNodeType.INPUT && node.component.type !== RuleNodeType.RULE_CHAIN) { + changed = changed || node.debugMode; + node.debugMode = false; + } + }); + if (changed) { + this.onModelChanged(); + } + } + + validate() { + setTimeout(() => { + this.isInvalid = false; + this.ruleChainModel.nodes.forEach((node) => { + if (node.error) { + this.isInvalid = true; + } + this.updateNodeErrorTooltip(node); + }); + }, 0); + } + + saveRuleChain() { + let saveRuleChainObservable: Observable; + if (this.isImport) { + saveRuleChainObservable = this.ruleChainService.saveRuleChain(this.ruleChain); + } else { + saveRuleChainObservable = of(this.ruleChain); + } + saveRuleChainObservable.subscribe((ruleChain) => { + this.ruleChain = ruleChain; + const ruleChainMetaData: RuleChainMetaData = { + ruleChainId: this.ruleChain.id, + nodes: [], + connections: [], + ruleChainConnections: [] + }; + const nodes: FcRuleNode[] = []; + this.ruleChainModel.nodes.forEach((node) => { + if (node.component.type !== RuleNodeType.INPUT && node.component.type !== RuleNodeType.RULE_CHAIN) { + const ruleNode: RuleNode = { + id: node.ruleNodeId, + type: node.component.clazz, + name: node.name, + configuration: node.configuration, + additionalInfo: node.additionalInfo ? node.additionalInfo : {}, + debugMode: node.debugMode + }; + ruleNode.additionalInfo.layoutX = Math.round(node.x); + ruleNode.additionalInfo.layoutY = Math.round(node.y); + ruleChainMetaData.nodes.push(ruleNode); + nodes.push(node); + } + }); + const firstNodeEdge = this.ruleChainModel.edges.find((edge) => edge.source === this.inputConnectorId + ''); + if (firstNodeEdge) { + const firstNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(firstNodeEdge.destination); + ruleChainMetaData.firstNodeIndex = nodes.indexOf(firstNode); + } + this.ruleChainModel.edges.forEach((edge) => { + const sourceNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.source); + const destNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.destination); + if (sourceNode.component.type !== RuleNodeType.INPUT) { + const fromIndex = nodes.indexOf(sourceNode); + if (destNode.component.type === RuleNodeType.RULE_CHAIN) { + const ruleChainConnection = { + fromIndex, + targetRuleChainId: {entityType: EntityType.RULE_CHAIN, id: destNode.targetRuleChainId}, + additionalInfo: destNode.additionalInfo ? destNode.additionalInfo : {} + } as RuleChainConnectionInfo; + ruleChainConnection.additionalInfo.layoutX = Math.round(destNode.x); + ruleChainConnection.additionalInfo.layoutY = Math.round(destNode.y); + ruleChainConnection.additionalInfo.ruleChainNodeId = destNode.id; + edge.labels.forEach((label) => { + const newRuleChainConnection = deepClone(ruleChainConnection); + newRuleChainConnection.type = label; + ruleChainMetaData.ruleChainConnections.push(newRuleChainConnection); + }); + } else { + const toIndex = nodes.indexOf(destNode); + const nodeConnection = { + fromIndex, + toIndex + } as NodeConnectionInfo; + edge.labels.forEach((label) => { + const newNodeConnection = deepClone(nodeConnection); + newNodeConnection.type = label; + ruleChainMetaData.connections.push(newNodeConnection); + }); + } + } + }); + this.ruleChainService.saveAndGetResolvedRuleChainMetadata(ruleChainMetaData).subscribe((savedRuleChainMetaData) => { + this.ruleChainMetaData = savedRuleChainMetaData; + if (this.isImport) { + this.isDirtyValue = false; + this.isImport = false; + this.router.navigateByUrl(`ruleChains/${this.ruleChain.id.id}`); + } else { + this.createRuleChainModel(); + } + }); + }); + } + + revertRuleChain() { + this.createRuleChainModel(); + } + + addRuleNode(ruleNode: FcRuleNode) { + ruleNode.configuration = deepClone(ruleNode.component.configurationDescriptor.nodeDefinition.defaultConfiguration); + const ruleChainId = this.ruleChain.id ? this.ruleChain.id.id : null; + this.enableHotKeys = false; + this.dialog.open(AddRuleNodeDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + ruleNode, + ruleChainId + } + }).afterClosed().subscribe( + (addedRuleNode) => { + if (addedRuleNode) { + addedRuleNode.id = 'rule-chain-node-' + this.nextNodeID++; + addedRuleNode.connectors = []; + if (addedRuleNode.component.configurationDescriptor.nodeDefinition.inEnabled) { + addedRuleNode.connectors.push( + { + id: (this.nextConnectorID++) + '', + type: FlowchartConstants.leftConnectorType + } + ); + } + if (addedRuleNode.component.configurationDescriptor.nodeDefinition.outEnabled) { + addedRuleNode.connectors.push( + { + id: (this.nextConnectorID++) + '', + type: FlowchartConstants.rightConnectorType + } + ); + } + this.ruleChainModel.nodes.push(addedRuleNode); + this.onModelChanged(); + this.updateRuleNodesHighlight(); + } + this.enableHotKeys = true; + } + ); + } + + addRuleNodeLink(link: FcRuleEdge, labels: {[label: string]: LinkLabel}, allowCustomLabels: boolean): Observable { + return this.dialog.open(AddRuleNodeLinkDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + link, + labels, + allowCustomLabels + } + }).afterClosed(); + } + + private updateNodeErrorTooltip(node: FcRuleNode) { + if (node.error) { + const element = $('#' + node.id); + let tooltip = this.errorTooltips[node.id]; + if (!tooltip || !element.hasClass('tooltipstered')) { + element.tooltipster( + { + theme: 'tooltipster-shadow', + delay: 0, + animationDuration: 0, + trigger: 'custom', + triggerOpen: { + click: false, + tap: false + }, + triggerClose: { + click: false, + tap: false, + scroll: false + }, + side: 'top', + trackOrigin: true + } + ); + const content = '
' + + '
' + + '
' + node.error + '
' + + '
' + + '
'; + const contentElement = $(content); + tooltip = element.tooltipster('instance'); + tooltip.isErrorTooltip = true; + tooltip.content(contentElement); + this.errorTooltips[node.id] = tooltip; + } + setTimeout(() => { + tooltip.open(); + }, 0); + } else { + if (this.errorTooltips[node.id]) { + const tooltip = this.errorTooltips[node.id]; + tooltip.destroy(); + delete this.errorTooltips[node.id]; + } + } + } + + private updateErrorTooltips(hide: boolean) { + for (const nodeId of Object.keys(this.errorTooltips)) { + const tooltip = this.errorTooltips[nodeId]; + if (hide) { + tooltip.close(); + } else { + tooltip.open(); + } + } + } + + private displayTooltip(event: MouseEvent, content: string) { + this.destroyTooltips(); + this.tooltipTimeout = setTimeout(() => { + const element = $(event.target); + element.tooltipster( + { + theme: 'tooltipster-shadow', + delay: 100, + trigger: 'custom', + triggerOpen: { + click: false, + tap: false + }, + triggerClose: { + click: true, + tap: true, + scroll: true + }, + side: 'right', + distance: 12, + trackOrigin: true + } + ); + const contentElement = $(content); + const tooltip = element.tooltipster('instance'); + tooltip.content(contentElement); + tooltip.open(); + }, 500); + } +} + +export interface AddRuleNodeLinkDialogData { + link: FcRuleEdge; + labels: {[label: string]: LinkLabel}; + allowCustomLabels: boolean; +} + +@Component({ + selector: 'tb-add-rule-node-link-dialog', + templateUrl: './add-rule-node-link-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: AddRuleNodeLinkDialogComponent}], + styleUrls: ['./add-rule-node-link-dialog.component.scss'] +}) +export class AddRuleNodeLinkDialogComponent extends DialogComponent + implements OnInit, ErrorStateMatcher { + + ruleNodeLinkFormGroup: FormGroup; + + link: FcRuleEdge; + labels: {[label: string]: LinkLabel}; + allowCustomLabels: boolean; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AddRuleNodeLinkDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private fb: FormBuilder) { + super(store, router, dialogRef); + + this.link = this.data.link; + this.labels = this.data.labels; + this.allowCustomLabels = this.data.allowCustomLabels; + + this.ruleNodeLinkFormGroup = this.fb.group({ + link: [deepClone(this.link), [Validators.required]] + } + ); + } + + ngOnInit(): void { + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + add(): void { + this.submitted = true; + const link: FcRuleEdge = this.ruleNodeLinkFormGroup.get('link').value; + this.link = {...this.link, ...link}; + this.dialogRef.close(this.link); + } +} + +export interface AddRuleNodeDialogData { + ruleNode: FcRuleNode; + ruleChainId: string; +} + +@Component({ + selector: 'tb-add-rule-node-dialog', + templateUrl: './add-rule-node-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: AddRuleNodeDialogComponent}], + styleUrls: [] +}) +export class AddRuleNodeDialogComponent extends DialogComponent + implements OnInit, ErrorStateMatcher { + + @ViewChild('tbRuleNode', {static: true}) ruleNodeDetailsComponent: RuleNodeDetailsComponent; + + ruleNode: FcRuleNode; + ruleChainId: string; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AddRuleNodeDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef) { + super(store, router, dialogRef); + + this.ruleNode = this.data.ruleNode; + this.ruleChainId = this.data.ruleChainId; + } + + ngOnInit(): void { + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + helpLinkIdForRuleNodeType(): string { + return getRuleNodeHelpLink(this.ruleNode.component); + } + + cancel(): void { + this.dialogRef.close(null); + } + + add(): void { + this.submitted = true; + + this.ruleNodeDetailsComponent.validate(); + if (this.ruleNodeDetailsComponent.ruleNodeFormGroup.valid) { + this.dialogRef.close(this.ruleNode); + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.models.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.models.ts new file mode 100644 index 0000000000..f0f3401ba9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.models.ts @@ -0,0 +1,45 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { FcModel } from 'ngx-flowchart/dist/ngx-flowchart'; +import { FcRuleEdge, FcRuleNode, FcRuleNodeType } from '@shared/models/rule-node.models'; + +export interface FcRuleNodeTypeModel extends FcModel { + nodes: Array; +} + +export interface FcRuleNodeModel extends FcModel { + nodes: Array; + edges: Array; +} + +export interface RuleChainMenuItem { + action?: ($event: MouseEvent) => void; + enabled?: boolean; + value?: string; + icon?: string; + shortcut?: string; + divider?: boolean; +} + +export interface RuleChainMenuContextInfo { + headerClass: string; + icon: string; + iconUrl?: string; + title: string; + subtitle: string; + menuItems: RuleChainMenuItem[]; +} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-routing.module.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-routing.module.ts new file mode 100644 index 0000000000..0431be2e14 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-routing.module.ts @@ -0,0 +1,184 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Inject, Injectable, NgModule, Optional } from '@angular/core'; +import { + ActivatedRouteSnapshot, + CanActivate, + Resolve, + Router, + RouterModule, + RouterStateSnapshot, + Routes, + UrlTree +} from '@angular/router'; + +import { EntitiesTableComponent } from '../../components/entity/entities-table.component'; +import { Authority } from '@shared/models/authority.enum'; +import { RuleChainsTableConfigResolver } from '@modules/home/pages/rulechain/rulechains-table-config.resolver'; +import { Observable } from 'rxjs'; +import { BreadCrumbConfig, BreadCrumbLabelFunction } from '@shared/components/breadcrumb'; +import { ResolvedRuleChainMetaData, RuleChain } from '@shared/models/rule-chain.models'; +import { RuleChainService } from '@core/http/rule-chain.service'; +import { RuleChainPageComponent } from '@home/pages/rulechain/rulechain-page.component'; +import { RuleNodeComponentDescriptor } from '@shared/models/rule-node.models'; +import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; +import { ItemBufferService } from '@core/public-api'; +import { MODULES_MAP } from '@shared/public-api'; + +@Injectable() +export class RuleChainResolver implements Resolve { + + constructor(private ruleChainService: RuleChainService) { + } + + resolve(route: ActivatedRouteSnapshot): Observable { + const ruleChainId = route.params.ruleChainId; + return this.ruleChainService.getRuleChain(ruleChainId); + } +} + +@Injectable() +export class ResolvedRuleChainMetaDataResolver implements Resolve { + + constructor(private ruleChainService: RuleChainService) { + } + + resolve(route: ActivatedRouteSnapshot): Observable { + const ruleChainId = route.params.ruleChainId; + return this.ruleChainService.getResolvedRuleChainMetadata(ruleChainId); + } +} + +@Injectable() +export class RuleNodeComponentsResolver implements Resolve> { + + constructor(private ruleChainService: RuleChainService, + @Optional() @Inject(MODULES_MAP) private modulesMap: {[key: string]: any}) { + } + + resolve(route: ActivatedRouteSnapshot): Observable> { + return this.ruleChainService.getRuleNodeComponents(this.modulesMap); + } +} + +@Injectable() +export class RuleChainImportGuard implements CanActivate { + + constructor(private itembuffer: ItemBufferService, + private router: Router) { + } + + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): + Observable | Promise | boolean | UrlTree { + if (this.itembuffer.hasRuleChainImport()) { + return true; + } else { + return this.router.parseUrl('ruleChains'); + } + } + +} + +export const ruleChainBreadcumbLabelFunction: BreadCrumbLabelFunction + = ((route, translate, component) => { + let label: string = component.ruleChain.name; + if (component.ruleChain.root) { + label += ` (${translate.instant('rulechain.root')})`; + } + return label; +}); + +export const importRuleChainBreadcumbLabelFunction: BreadCrumbLabelFunction = + ((route, translate, component) => { + return `${translate.instant('rulechain.import')}: ${component.ruleChain.name}`; +}); + +const routes: Routes = [ + { + path: 'ruleChains', + data: { + breadcrumb: { + label: 'rulechain.rulechains', + icon: 'settings_ethernet' + } + }, + children: [ + { + path: '', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN], + title: 'rulechain.rulechains' + }, + resolve: { + entitiesTableConfig: RuleChainsTableConfigResolver + } + }, + { + path: ':ruleChainId', + component: RuleChainPageComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + breadcrumb: { + labelFunction: ruleChainBreadcumbLabelFunction, + icon: 'settings_ethernet' + } as BreadCrumbConfig, + auth: [Authority.TENANT_ADMIN], + title: 'rulechain.rulechain', + import: false + }, + resolve: { + ruleChain: RuleChainResolver, + ruleChainMetaData: ResolvedRuleChainMetaDataResolver, + ruleNodeComponents: RuleNodeComponentsResolver + } + }, + { + path: 'ruleChain/import', + component: RuleChainPageComponent, + canActivate: [RuleChainImportGuard], + canDeactivate: [ConfirmOnExitGuard], + data: { + breadcrumb: { + labelFunction: importRuleChainBreadcumbLabelFunction, + icon: 'settings_ethernet' + } as BreadCrumbConfig, + auth: [Authority.TENANT_ADMIN], + title: 'rulechain.rulechain', + import: true + }, + resolve: { + ruleNodeComponents: RuleNodeComponentsResolver + } + } + ] + } +]; + +// @dynamic +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [ + RuleChainsTableConfigResolver, + RuleChainResolver, + ResolvedRuleChainMetaDataResolver, + RuleNodeComponentsResolver, + RuleChainImportGuard + ] +}) +export class RuleChainRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-tabs.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-tabs.component.html new file mode 100644 index 0000000000..e9e08893de --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-tabs.component.html @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-tabs.component.ts new file mode 100644 index 0000000000..e658ed0d7e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-tabs.component.ts @@ -0,0 +1,38 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; +import { RuleChain } from '@shared/models/rule-chain.models'; + +@Component({ + selector: 'tb-rulechain-tabs', + templateUrl: './rulechain-tabs.component.html', + styleUrls: [] +}) +export class RuleChainTabsComponent extends EntityTabsComponent { + + constructor(protected store: Store) { + super(store); + } + + ngOnInit() { + super.ngOnInit(); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.html new file mode 100644 index 0000000000..29c832e576 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.html @@ -0,0 +1,75 @@ + +
+ + + + +
+ +
+
+
+
+
+ + rulechain.name + + + {{ 'rulechain.name-required' | translate }} + + + + {{ 'rulechain.debug-mode' | translate }} + +
+ + rulechain.description + + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.scss b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.scss new file mode 100644 index 0000000000..bb3718c2da --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.scss @@ -0,0 +1,18 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + +} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.ts new file mode 100644 index 0000000000..8cd88e548f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.ts @@ -0,0 +1,81 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityComponent } from '../../components/entity/entity.component'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { TranslateService } from '@ngx-translate/core'; +import { RuleChain } from '@shared/models/rule-chain.models'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; + +@Component({ + selector: 'tb-rulechain', + templateUrl: './rulechain.component.html', + styleUrls: ['./rulechain.component.scss'] +}) +export class RuleChainComponent extends EntityComponent { + + constructor(protected store: Store, + protected translate: TranslateService, + @Inject('entity') protected entityValue: RuleChain, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + public fb: FormBuilder) { + super(store, fb, entityValue, entitiesTableConfigValue); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + buildForm(entity: RuleChain): FormGroup { + return this.fb.group( + { + name: [entity ? entity.name : '', [Validators.required]], + debugMode: [entity ? entity.debugMode : false], + additionalInfo: this.fb.group( + { + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''], + } + ) + } + ); + } + + updateForm(entity: RuleChain) { + this.entityForm.patchValue({name: entity.name}); + this.entityForm.patchValue({debugMode: entity.debugMode}); + this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}}); + } + + + onRuleChainIdCopied($event) { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('rulechain.idCopiedMessage'), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.module.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.module.ts new file mode 100644 index 0000000000..bcc18f4714 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.module.ts @@ -0,0 +1,64 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { RuleChainComponent } from '@modules/home/pages/rulechain/rulechain.component'; +import { RuleChainRoutingModule } from '@modules/home/pages/rulechain/rulechain-routing.module'; +import { HomeComponentsModule } from '@modules/home/components/home-components.module'; +import { RuleChainTabsComponent } from '@home/pages/rulechain/rulechain-tabs.component'; +import { + AddRuleNodeDialogComponent, + AddRuleNodeLinkDialogComponent, + RuleChainPageComponent +} from './rulechain-page.component'; +import { RuleNodeComponent } from '@home/pages/rulechain/rulenode.component'; +import { FC_NODE_COMPONENT_CONFIG } from 'ngx-flowchart/dist/ngx-flowchart'; +import { RuleNodeDetailsComponent } from './rule-node-details.component'; +import { RuleNodeLinkComponent } from './rule-node-link.component'; +import { LinkLabelsComponent } from '@home/pages/rulechain/link-labels.component'; +import { RuleNodeConfigComponent } from './rule-node-config.component'; + +@NgModule({ + declarations: [ + RuleChainComponent, + RuleChainTabsComponent, + RuleChainPageComponent, + RuleNodeComponent, + RuleNodeDetailsComponent, + RuleNodeConfigComponent, + LinkLabelsComponent, + RuleNodeLinkComponent, + AddRuleNodeLinkDialogComponent, + AddRuleNodeDialogComponent + ], + providers: [ + { + provide: FC_NODE_COMPONENT_CONFIG, + useValue: { + nodeComponentType: RuleNodeComponent + } + } + ], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + RuleChainRoutingModule + ] +}) +export class RuleChainModule { } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts new file mode 100644 index 0000000000..3dff54f6f5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts @@ -0,0 +1,186 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; + +import { Resolve, Router } from '@angular/router'; +import { + checkBoxCell, + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { RuleChain } from '@shared/models/rule-chain.models'; +import { RuleChainService } from '@core/http/rule-chain.service'; +import { RuleChainComponent } from '@modules/home/pages/rulechain/rulechain.component'; +import { DialogService } from '@core/services/dialog.service'; +import { RuleChainTabsComponent } from '@home/pages/rulechain/rulechain-tabs.component'; +import { ImportExportService } from '@home/components/import-export/import-export.service'; +import { ItemBufferService } from '@core/services/item-buffer.service'; + +@Injectable() +export class RuleChainsTableConfigResolver implements Resolve> { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor(private ruleChainService: RuleChainService, + private dialogService: DialogService, + private importExport: ImportExportService, + private itembuffer: ItemBufferService, + private translate: TranslateService, + private datePipe: DatePipe, + private router: Router) { + + this.config.entityType = EntityType.RULE_CHAIN; + this.config.entityComponent = RuleChainComponent; + this.config.entityTabsComponent = RuleChainTabsComponent; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.RULE_CHAIN); + this.config.entityResources = entityTypeResources.get(EntityType.RULE_CHAIN); + + this.config.columns.push( + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), + new EntityTableColumn('name', 'rulechain.name', '100%'), + new EntityTableColumn('root', 'rulechain.root', '60px', + entity => { + return checkBoxCell(entity.root); + }), + ); + + this.config.addActionDescriptors.push( + { + name: this.translate.instant('rulechain.create-new-rulechain'), + icon: 'insert_drive_file', + isEnabled: () => true, + onAction: ($event) => this.config.table.addEntity($event) + }, + { + name: this.translate.instant('rulechain.import'), + icon: 'file_upload', + isEnabled: () => true, + onAction: ($event) => this.importRuleChain($event) + } + ); + + this.config.cellActionDescriptors.push( + { + name: this.translate.instant('rulechain.open-rulechain'), + icon: 'settings_ethernet', + isEnabled: () => true, + onAction: ($event, entity) => this.openRuleChain($event, entity) + }, + { + name: this.translate.instant('rulechain.export'), + icon: 'file_download', + isEnabled: () => true, + onAction: ($event, entity) => this.exportRuleChain($event, entity) + }, + { + name: this.translate.instant('rulechain.set-root'), + icon: 'flag', + isEnabled: (ruleChain) => !ruleChain.root, + onAction: ($event, entity) => this.setRootRuleChain($event, entity) + } + ); + + this.config.deleteEntityTitle = ruleChain => this.translate.instant('rulechain.delete-rulechain-title', + { ruleChainName: ruleChain.name }); + this.config.deleteEntityContent = () => this.translate.instant('rulechain.delete-rulechain-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('rulechain.delete-rulechains-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('rulechain.delete-rulechains-text'); + + this.config.entitiesFetchFunction = pageLink => this.ruleChainService.getRuleChains(pageLink); + this.config.loadEntity = id => this.ruleChainService.getRuleChain(id.id); + this.config.saveEntity = ruleChain => this.ruleChainService.saveRuleChain(ruleChain); + this.config.deleteEntity = id => this.ruleChainService.deleteRuleChain(id.id); + this.config.onEntityAction = action => this.onRuleChainAction(action); + this.config.deleteEnabled = (ruleChain) => ruleChain && !ruleChain.root; + this.config.entitySelectionEnabled = (ruleChain) => ruleChain && !ruleChain.root; + } + + resolve(): EntityTableConfig { + this.config.tableTitle = this.translate.instant('rulechain.rulechains'); + + return this.config; + } + + importRuleChain($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.importExport.importRuleChain().subscribe((ruleChainImport) => { + if (ruleChainImport) { + this.itembuffer.storeRuleChainImport(ruleChainImport); + this.router.navigateByUrl(`ruleChains/ruleChain/import`); + } + }); + } + + openRuleChain($event: Event, ruleChain: RuleChain) { + if ($event) { + $event.stopPropagation(); + } + this.router.navigateByUrl(`ruleChains/${ruleChain.id.id}`); + } + + exportRuleChain($event: Event, ruleChain: RuleChain) { + if ($event) { + $event.stopPropagation(); + } + this.importExport.exportRuleChain(ruleChain.id.id); + } + + setRootRuleChain($event: Event, ruleChain: RuleChain) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('rulechain.set-root-rulechain-title', {ruleChainName: ruleChain.name}), + this.translate.instant('rulechain.set-root-rulechain-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + this.ruleChainService.setRootRuleChain(ruleChain.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + onRuleChainAction(action: EntityAction): boolean { + switch (action.action) { + case 'open': + this.openRuleChain(action.event, action.entity); + return true; + case 'export': + this.exportRuleChain(action.event, action.entity); + return true; + case 'setRoot': + this.setRootRuleChain(action.event, action.entity); + return true; + } + return false; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.html new file mode 100644 index 0000000000..bc4632f817 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.html @@ -0,0 +1,67 @@ + +
+
+
+ {{node.icon}} + +
+ {{ node.component.name }} + {{ node.name }} +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ × +
+
+
+
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.scss b/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.scss new file mode 100644 index 0000000000..f95da40ebc --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.scss @@ -0,0 +1,154 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import './rule-node-colors'; + +:host { + + .fc-node-overlay { + position: absolute; + pointer-events: none; + left: 0; + top: 0; + right: 0; + bottom: 0; + background-color: #000; + border-radius: 5px; + opacity: 0; + } + + :host-context(.fc-hover) .fc-node-overlay { + opacity: 0.25; + transition: opacity .2s; + } + + :host-context(.fc-selected) .fc-node-overlay { + opacity: 0.25; + } + + :host-context(.fc-edit) { + .fc-nodeedit, + .fc-nodedelete { + box-sizing: content-box; + border: solid 2px #fff; + background: #f83e05; + outline: none; + } + } + + .tb-rule-node { + box-sizing: content-box; + display: flex; + flex-direction: row; + min-width: 150px; + max-width: 150px; + height: 32px; + min-height: 32px; + max-height: 32px; + padding: 5px 10px; + font-size: 12px; + line-height: 16px; + color: #333; + pointer-events: none; + background-color: #f15b26; + border: solid 1px #777; + border-radius: 5px; + + @include rule-node-colors(); + + &.tb-rule-node-highlighted:not(.tb-rule-node-invalid) { + box-shadow: 0 0 10px 6px #51cbee; + + .tb-node-title { + font-weight: 700; + text-decoration: underline; + } + } + + &.tb-rule-node-invalid { + box-shadow: 0 0 10px 6px #ff5c50; + } + + &.tb-input-type { + user-select: none; + background-color: #a3eaa9; + } + + mat-icon, img { + margin: auto; + width: 20px; + min-width: 20px; + height: 20px; + min-height: 20px; + padding-right: 4px; + font-size: 20px; + } + + .tb-node-title { + font-weight: 500; + } + + .tb-node-type, + .tb-node-title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .fc-leftConnectors, + .fc-rightConnectors { + position: absolute; + top: 0; + + z-index: 0; + + display: flex; + flex-direction: column; + height: 100%; + + .fc-magnet { + align-items: center; + } + } + + .fc-leftConnectors { + left: -20px; + } + + .fc-rightConnectors { + right: -20px; + } + + .fc-magnet { + display: flex; + flex-grow: 1; + justify-content: center; + height: 60px; + } + + .fc-connector { + width: 14px; + height: 14px; + margin: 10px; + pointer-events: all; + background-color: #ccc; + border: 1px solid #333; + border-radius: 5px; + } + + .fc-connector.fc-hover { + background-color: #000; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.ts new file mode 100644 index 0000000000..2d057565e2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.ts @@ -0,0 +1,54 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; +import { Component, OnInit } from '@angular/core'; +import { FcNodeComponent } from 'ngx-flowchart/dist/ngx-flowchart'; +import { FcRuleNode, RuleNodeType } from '@shared/models/rule-node.models'; +import { Router } from '@angular/router'; + +@Component({ + // tslint:disable-next-line:component-selector + selector: 'rule-node', + templateUrl: './rulenode.component.html', + styleUrls: ['./rulenode.component.scss'] +}) +export class RuleNodeComponent extends FcNodeComponent implements OnInit { + + iconUrl: SafeResourceUrl; + RuleNodeType = RuleNodeType; + + constructor(private sanitizer: DomSanitizer, + private router: Router) { + super(); + } + + ngOnInit(): void { + super.ngOnInit(); + if (this.node.iconUrl) { + this.iconUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.node.iconUrl); + } + } + + openRuleChain($event: Event, node: FcRuleNode) { + if ($event) { + $event.stopPropagation(); + } + if (node.targetRuleChainId) { + this.router.navigateByUrl(`/ruleChains/${node.targetRuleChainId}`); + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-routing.module.ts b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-routing.module.ts new file mode 100644 index 0000000000..c10efb76f8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-routing.module.ts @@ -0,0 +1,56 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { EntitiesTableComponent } from '../../components/entity/entities-table.component'; +import { Authority } from '@shared/models/authority.enum'; +import { TenantProfilesTableConfigResolver } from './tenant-profiles-table-config.resolver'; + +const routes: Routes = [ + { + path: 'tenantProfiles', + data: { + breadcrumb: { + label: 'tenant-profile.tenant-profiles', + icon: 'mdi:alpha-t-box' + } + }, + children: [ + { + path: '', + component: EntitiesTableComponent, + data: { + auth: [Authority.SYS_ADMIN], + title: 'tenant-profile.tenant-profiles' + }, + resolve: { + entitiesTableConfig: TenantProfilesTableConfigResolver + } + } + ] + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [ + TenantProfilesTableConfigResolver + ] +}) +export class TenantProfileRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.html b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.html new file mode 100644 index 0000000000..40a9f55aa5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.ts new file mode 100644 index 0000000000..cab08974f3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.ts @@ -0,0 +1,38 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; +import { TenantProfile } from '@shared/models/tenant.model'; + +@Component({ + selector: 'tb-tenant-profile-tabs', + templateUrl: './tenant-profile-tabs.component.html', + styleUrls: [] +}) +export class TenantProfileTabsComponent extends EntityTabsComponent { + + constructor(protected store: Store) { + super(store); + } + + ngOnInit() { + super.ngOnInit(); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile.module.ts b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile.module.ts new file mode 100644 index 0000000000..66d55d37ed --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile.module.ts @@ -0,0 +1,35 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { HomeComponentsModule } from '@modules/home/components/home-components.module'; +import { TenantProfileRoutingModule } from './tenant-profile-routing.module'; +import { TenantProfileTabsComponent } from './tenant-profile-tabs.component'; + +@NgModule({ + declarations: [ + TenantProfileTabsComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + TenantProfileRoutingModule + ] +}) +export class TenantProfileModule { } diff --git a/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profiles-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profiles-table-config.resolver.ts new file mode 100644 index 0000000000..a56fbf60da --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profiles-table-config.resolver.ts @@ -0,0 +1,122 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { Resolve } from '@angular/router'; +import { TenantProfile } from '@shared/models/tenant.model'; +import { + checkBoxCell, + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { TenantProfileService } from '@core/http/tenant-profile.service'; +import { TenantProfileComponent } from '../../components/profile/tenant-profile.component'; +import { TenantProfileTabsComponent } from './tenant-profile-tabs.component'; +import { DialogService } from '@core/services/dialog.service'; + +@Injectable() +export class TenantProfilesTableConfigResolver implements Resolve> { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor(private tenantProfileService: TenantProfileService, + private translate: TranslateService, + private datePipe: DatePipe, + private dialogService: DialogService) { + + this.config.entityType = EntityType.TENANT_PROFILE; + this.config.entityComponent = TenantProfileComponent; + this.config.entityTabsComponent = TenantProfileTabsComponent; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.TENANT_PROFILE); + this.config.entityResources = entityTypeResources.get(EntityType.TENANT_PROFILE); + + this.config.columns.push( + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), + new EntityTableColumn('name', 'tenant-profile.name', '40%'), + new EntityTableColumn('description', 'tenant-profile.description', '60%'), + new EntityTableColumn('isDefault', 'tenant-profile.default', '60px', + entity => { + return checkBoxCell(entity.default); + }) + ); + + this.config.cellActionDescriptors.push( + { + name: this.translate.instant('tenant-profile.set-default'), + icon: 'flag', + isEnabled: (tenantProfile) => !tenantProfile.default, + onAction: ($event, entity) => this.setDefaultTenantProfile($event, entity) + } + ); + + this.config.deleteEntityTitle = tenantProfile => this.translate.instant('tenant-profile.delete-tenant-profile-title', + { tenantProfileName: tenantProfile.name }); + this.config.deleteEntityContent = () => this.translate.instant('tenant-profile.delete-tenant-profile-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('tenant-profile.delete-tenant-profiles-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('tenant-profile.delete-tenant-profiles-text'); + + this.config.entitiesFetchFunction = pageLink => this.tenantProfileService.getTenantProfiles(pageLink); + this.config.loadEntity = id => this.tenantProfileService.getTenantProfile(id.id); + this.config.saveEntity = tenantProfile => this.tenantProfileService.saveTenantProfile(tenantProfile); + this.config.deleteEntity = id => this.tenantProfileService.deleteTenantProfile(id.id); + this.config.onEntityAction = action => this.onTenantProfileAction(action); + this.config.deleteEnabled = (tenantProfile) => tenantProfile && !tenantProfile.default; + this.config.entitySelectionEnabled = (tenantProfile) => tenantProfile && !tenantProfile.default; + } + + resolve(): EntityTableConfig { + this.config.tableTitle = this.translate.instant('tenant-profile.tenant-profiles'); + + return this.config; + } + + setDefaultTenantProfile($event: Event, tenantProfile: TenantProfile) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('tenant-profile.set-default-tenant-profile-title', {tenantProfileName: tenantProfile.name}), + this.translate.instant('tenant-profile.set-default-tenant-profile-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + this.tenantProfileService.setDefaultTenantProfile(tenantProfile.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + onTenantProfileAction(action: EntityAction): boolean { + switch (action.action) { + case 'setDefault': + this.setDefaultTenantProfile(action.event, action.entity); + return true; + } + return false; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/tenant/tenant-routing.module.ts b/ui-ngx/src/app/modules/home/pages/tenant/tenant-routing.module.ts new file mode 100644 index 0000000000..c2406e8826 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/tenant/tenant-routing.module.ts @@ -0,0 +1,72 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { EntitiesTableComponent } from '../../components/entity/entities-table.component'; +import { Authority } from '@shared/models/authority.enum'; +import { TenantsTableConfigResolver } from '@modules/home/pages/tenant/tenants-table-config.resolver'; +import { UsersTableConfigResolver } from '../user/users-table-config.resolver'; + +const routes: Routes = [ + { + path: 'tenants', + data: { + breadcrumb: { + label: 'tenant.tenants', + icon: 'supervisor_account' + } + }, + children: [ + { + path: '', + component: EntitiesTableComponent, + data: { + auth: [Authority.SYS_ADMIN], + title: 'tenant.tenants' + }, + resolve: { + entitiesTableConfig: TenantsTableConfigResolver + } + }, + { + path: ':tenantId/users', + component: EntitiesTableComponent, + data: { + auth: [Authority.SYS_ADMIN], + title: 'user.tenant-admins', + breadcrumb: { + label: 'user.tenant-admins', + icon: 'account_circle' + } + }, + resolve: { + entitiesTableConfig: UsersTableConfigResolver + } + } + ] + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [ + TenantsTableConfigResolver + ] +}) +export class TenantRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/tenant/tenant-tabs.component.html b/ui-ngx/src/app/modules/home/pages/tenant/tenant-tabs.component.html new file mode 100644 index 0000000000..7cf26dc73f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/tenant/tenant-tabs.component.html @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/app/modules/home/pages/tenant/tenant-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/tenant/tenant-tabs.component.ts new file mode 100644 index 0000000000..388de81fda --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/tenant/tenant-tabs.component.ts @@ -0,0 +1,38 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; +import { TenantInfo } from '@shared/models/tenant.model'; + +@Component({ + selector: 'tb-tenant-tabs', + templateUrl: './tenant-tabs.component.html', + styleUrls: [] +}) +export class TenantTabsComponent extends EntityTabsComponent { + + constructor(protected store: Store) { + super(store); + } + + ngOnInit() { + super.ngOnInit(); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.html b/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.html new file mode 100644 index 0000000000..dd603d4a91 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.html @@ -0,0 +1,67 @@ + +
+ + +
+ +
+
+
+
+
+ + tenant.title + + + {{ 'tenant.title-required' | translate }} + + + + +
+ + tenant.description + + +
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts b/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts new file mode 100644 index 0000000000..f139deb1b5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts @@ -0,0 +1,94 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Tenant, TenantInfo } from '@app/shared/models/tenant.model'; +import { ActionNotificationShow } from '@app/core/notification/notification.actions'; +import { TranslateService } from '@ngx-translate/core'; +import { ContactBasedComponent } from '../../components/entity/contact-based.component'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; + +@Component({ + selector: 'tb-tenant', + templateUrl: './tenant.component.html', + styleUrls: [] +}) +export class TenantComponent extends ContactBasedComponent { + + constructor(protected store: Store, + protected translate: TranslateService, + @Inject('entity') protected entityValue: TenantInfo, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + protected fb: FormBuilder) { + super(store, fb, entityValue, entitiesTableConfigValue); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + buildEntityForm(entity: TenantInfo): FormGroup { + return this.fb.group( + { + title: [entity ? entity.title : '', [Validators.required]], + tenantProfileId: [entity ? entity.tenantProfileId : null, [Validators.required]], + additionalInfo: this.fb.group( + { + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''] + } + ) + } + ); + } + + updateEntityForm(entity: Tenant) { + this.entityForm.patchValue({title: entity.title}); + this.entityForm.patchValue({tenantProfileId: entity.tenantProfileId}); + this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}}); + } + + updateFormState() { + if (this.entityForm) { + if (this.isEditValue) { + this.entityForm.enable({emitEvent: false}); + } else { + this.entityForm.disable({emitEvent: false}); + } + } + } + + onTenantIdCopied(event) { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('tenant.idCopiedMessage'), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } + + onTenantProfileUpdated() { + this.entitiesTableConfig.table.updateData(false); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/tenant/tenant.module.ts b/ui-ngx/src/app/modules/home/pages/tenant/tenant.module.ts new file mode 100644 index 0000000000..4997342ad5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/tenant/tenant.module.ts @@ -0,0 +1,37 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { TenantComponent } from '@modules/home/pages/tenant/tenant.component'; +import { TenantRoutingModule } from '@modules/home/pages/tenant/tenant-routing.module'; +import { HomeComponentsModule } from '@modules/home/components/home-components.module'; +import { TenantTabsComponent } from '@home/pages/tenant/tenant-tabs.component'; + +@NgModule({ + declarations: [ + TenantComponent, + TenantTabsComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + TenantRoutingModule + ] +}) +export class TenantModule { } diff --git a/ui-ngx/src/app/modules/home/pages/tenant/tenants-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/tenant/tenants-table-config.resolver.ts new file mode 100644 index 0000000000..c3c0c14e88 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/tenant/tenants-table-config.resolver.ts @@ -0,0 +1,106 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; + +import { Resolve, Router } from '@angular/router'; + +import { TenantInfo } from '@shared/models/tenant.model'; +import { + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { TenantService } from '@core/http/tenant.service'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { TenantComponent } from '@modules/home/pages/tenant/tenant.component'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { TenantTabsComponent } from '@home/pages/tenant/tenant-tabs.component'; +import { mergeMap } from 'rxjs/operators'; + +@Injectable() +export class TenantsTableConfigResolver implements Resolve> { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor(private tenantService: TenantService, + private translate: TranslateService, + private datePipe: DatePipe, + private router: Router) { + + this.config.entityType = EntityType.TENANT; + this.config.entityComponent = TenantComponent; + this.config.entityTabsComponent = TenantTabsComponent; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.TENANT); + this.config.entityResources = entityTypeResources.get(EntityType.TENANT); + + this.config.columns.push( + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), + new EntityTableColumn('title', 'tenant.title', '20%'), + new EntityTableColumn('tenantProfileName', 'tenant-profile.tenant-profile', '20%'), + new EntityTableColumn('email', 'contact.email', '20%'), + new EntityTableColumn('country', 'contact.country', '20%'), + new EntityTableColumn('city', 'contact.city', '20%') + ); + + this.config.cellActionDescriptors.push( + { + name: this.translate.instant('tenant.manage-tenant-admins'), + icon: 'account_circle', + isEnabled: () => true, + onAction: ($event, entity) => this.manageTenantAdmins($event, entity) + } + ); + + this.config.deleteEntityTitle = tenant => this.translate.instant('tenant.delete-tenant-title', { tenantTitle: tenant.title }); + this.config.deleteEntityContent = () => this.translate.instant('tenant.delete-tenant-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('tenant.delete-tenants-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('tenant.delete-tenants-text'); + + this.config.entitiesFetchFunction = pageLink => this.tenantService.getTenantInfos(pageLink); + this.config.loadEntity = id => this.tenantService.getTenantInfo(id.id); + this.config.saveEntity = tenant => this.tenantService.saveTenant(tenant).pipe( + mergeMap((savedTenant) => this.tenantService.getTenantInfo(savedTenant.id.id)) + ); + this.config.deleteEntity = id => this.tenantService.deleteTenant(id.id); + this.config.onEntityAction = action => this.onTenantAction(action); + } + + resolve(): EntityTableConfig { + this.config.tableTitle = this.translate.instant('tenant.tenants'); + + return this.config; + } + + manageTenantAdmins($event: Event, tenant: TenantInfo) { + if ($event) { + $event.stopPropagation(); + } + this.router.navigateByUrl(`tenants/${tenant.id.id}/users`); + } + + onTenantAction(action: EntityAction): boolean { + switch (action.action) { + case 'manageTenantAdmins': + this.manageTenantAdmins(action.event, action.entity); + return true; + } + return false; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/user/activation-link-dialog.component.html b/ui-ngx/src/app/modules/home/pages/user/activation-link-dialog.component.html new file mode 100644 index 0000000000..52d221c55f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/user/activation-link-dialog.component.html @@ -0,0 +1,57 @@ + +
+ +

user.activation-link

+ + +
+ + +
+
+
+ +
+
{{ activationLink }}
+ +
+
+
+
+ +
+
diff --git a/ui-ngx/src/app/modules/home/pages/user/activation-link-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/user/activation-link-dialog.component.ts new file mode 100644 index 0000000000..54bbc9c7fa --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/user/activation-link-dialog.component.ts @@ -0,0 +1,66 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; + +export interface ActivationLinkDialogData { + activationLink: string; +} + +@Component({ + selector: 'tb-activation-link-dialog', + templateUrl: './activation-link-dialog.component.html' +}) +export class ActivationLinkDialogComponent extends DialogComponent implements OnInit { + + activationLink: string; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: ActivationLinkDialogData, + public dialogRef: MatDialogRef, + private translate: TranslateService) { + super(store, router, dialogRef); + this.activationLink = this.data.activationLink; + } + + ngOnInit(): void { + } + + close(): void { + this.dialogRef.close(); + } + + onActivationLinkCopied() { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('user.activation-link-copied-message'), + type: 'success', + target: 'activationLinkDialogContent', + duration: 1200, + verticalPosition: 'bottom', + horizontalPosition: 'left' + })); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.html b/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.html new file mode 100644 index 0000000000..a66a3ab995 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.html @@ -0,0 +1,57 @@ + +
+ +

user.add

+ +
+ +
+ + +
+
+ + + user.activation-method + + + {{ activationMethodTranslations.get(activationMethodEnum[activationMethod]) | translate }} + + + +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.scss new file mode 100644 index 0000000000..a49b3e8fc1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.scss @@ -0,0 +1,19 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:host ::ng-deep .mat-padding{ + padding: 0; +} diff --git a/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.ts new file mode 100644 index 0000000000..cc50bea358 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.ts @@ -0,0 +1,118 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, ViewChild } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormGroup } from '@angular/forms'; +import { UserComponent } from '@modules/home/pages/user/user.component'; +import { Authority } from '@shared/models/authority.enum'; +import { ActivationMethod, activationMethodTranslations, User } from '@shared/models/user.model'; +import { CustomerId } from '@shared/models/id/customer-id'; +import { UserService } from '@core/http/user.service'; +import { Observable } from 'rxjs'; +import { + ActivationLinkDialogComponent, + ActivationLinkDialogData +} from '@modules/home/pages/user/activation-link-dialog.component'; +import { TenantId } from '@app/shared/models/id/tenant-id'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; + +export interface AddUserDialogData { + tenantId: string; + customerId: string; + authority: Authority; +} + +@Component({ + selector: 'tb-add-user-dialog', + templateUrl: './add-user-dialog.component.html', + styleUrls: ['./add-user-dialog.component.scss'] +}) +export class AddUserDialogComponent extends DialogComponent implements OnInit { + + detailsForm: FormGroup; + user: User; + + activationMethods = Object.keys(ActivationMethod); + activationMethodEnum = ActivationMethod; + + activationMethodTranslations = activationMethodTranslations; + + activationMethod = ActivationMethod.DISPLAY_ACTIVATION_LINK; + + @ViewChild(UserComponent, {static: true}) userComponent: UserComponent; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AddUserDialogData, + public dialogRef: MatDialogRef, + private userService: UserService, + private dialog: MatDialog) { + super(store, router, dialogRef); + } + + ngOnInit(): void { + this.user = {} as User; + this.userComponent.isEdit = true; + this.userComponent.entity = this.user; + this.detailsForm = this.userComponent.entityForm; + } + + cancel(): void { + this.dialogRef.close(null); + } + + add(): void { + if (this.detailsForm.valid) { + this.user = {...this.user, ...this.userComponent.entityForm.value}; + this.user.authority = this.data.authority; + this.user.tenantId = new TenantId(this.data.tenantId); + this.user.customerId = new CustomerId(this.data.customerId); + const sendActivationEmail = this.activationMethod === ActivationMethod.SEND_ACTIVATION_MAIL; + this.userService.saveUser(this.user, sendActivationEmail).subscribe( + (user) => { + if (this.activationMethod === ActivationMethod.DISPLAY_ACTIVATION_LINK) { + this.userService.getActivationLink(user.id.id).subscribe( + (activationLink) => { + this.displayActivationLink(activationLink).subscribe( + () => { + this.dialogRef.close(user); + } + ); + } + ); + } else { + this.dialogRef.close(user); + } + } + ); + } + } + + displayActivationLink(activationLink: string): Observable { + return this.dialog.open(ActivationLinkDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + activationLink + } + }).afterClosed(); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/user/user-routing.module.ts b/ui-ngx/src/app/modules/home/pages/user/user-routing.module.ts new file mode 100644 index 0000000000..380548f2bd --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/user/user-routing.module.ts @@ -0,0 +1,28 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { UsersTableConfigResolver } from '@modules/home/pages/user/users-table-config.resolver'; + +@NgModule({ + imports: [], + exports: [RouterModule], + providers: [ + UsersTableConfigResolver + ] +}) +export class UserRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/user/user-tabs.component.html b/ui-ngx/src/app/modules/home/pages/user/user-tabs.component.html new file mode 100644 index 0000000000..9d5533c2f6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/user/user-tabs.component.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + diff --git a/ui-ngx/src/app/modules/home/pages/user/user-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/user/user-tabs.component.ts new file mode 100644 index 0000000000..f8266d2309 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/user/user-tabs.component.ts @@ -0,0 +1,38 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; +import { User } from '@app/shared/models/user.model'; + +@Component({ + selector: 'tb-user-tabs', + templateUrl: './user-tabs.component.html', + styleUrls: [] +}) +export class UserTabsComponent extends EntityTabsComponent { + + constructor(protected store: Store) { + super(store); + } + + ngOnInit() { + super.ngOnInit(); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/user/user.component.html b/ui-ngx/src/app/modules/home/pages/user/user.component.html new file mode 100644 index 0000000000..2b8917596e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/user/user.component.html @@ -0,0 +1,102 @@ + +
+ + + + + + +
+
+
+
+ + user.email + + + {{ 'user.invalid-email-format' | translate }} + + + {{ 'user.email-required' | translate }} + + + + user.first-name + + + + user.last-name + + +
+ + user.description + + +
+
+ + + {{ 'user.always-fullscreen' | translate }} + +
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/user/user.component.scss b/ui-ngx/src/app/modules/home/pages/user/user.component.scss new file mode 100644 index 0000000000..4f11e9a63e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/user/user.component.scss @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../../../scss/constants"; + +:host { + .tb-default-dashboard { + tb-dashboard-autocomplete { + @media #{$mat-gt-sm} { + padding-right: 12px; + } + + @media #{$mat-lt-md} { + padding-bottom: 12px; + } + } + mat-checkbox { + @media #{$mat-gt-sm} { + margin-top: 16px; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/user/user.component.ts b/ui-ngx/src/app/modules/home/pages/user/user.component.ts new file mode 100644 index 0000000000..07567267db --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/user/user.component.ts @@ -0,0 +1,94 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, Optional } from '@angular/core'; +import { select, Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityComponent } from '../../components/entity/entity.component'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { User } from '@shared/models/user.model'; +import { selectAuth } from '@core/auth/auth.selectors'; +import { map } from 'rxjs/operators'; +import { Authority } from '@shared/models/authority.enum'; +import { isUndefined } from '@core/utils'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; + +@Component({ + selector: 'tb-user', + templateUrl: './user.component.html', + styleUrls: ['./user.component.scss'] +}) +export class UserComponent extends EntityComponent { + + authority = Authority; + + loginAsUserEnabled$ = this.store.pipe( + select(selectAuth), + map((auth) => auth.userTokenAccessEnabled) + ); + + constructor(protected store: Store, + @Optional() @Inject('entity') protected entityValue: User, + @Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + public fb: FormBuilder) { + super(store, fb, entityValue, entitiesTableConfigValue); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + isUserCredentialsEnabled(): boolean { + if (!this.entity || !this.entity.additionalInfo || isUndefined(this.entity.additionalInfo.userCredentialsEnabled)) { + return true; + } else { + return this.entity.additionalInfo.userCredentialsEnabled === true; + } + } + + buildForm(entity: User): FormGroup { + return this.fb.group( + { + email: [entity ? entity.email : '', [Validators.required, Validators.email]], + firstName: [entity ? entity.firstName : ''], + lastName: [entity ? entity.lastName : ''], + additionalInfo: this.fb.group( + { + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''], + defaultDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null], + defaultDashboardFullscreen: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardFullscreen : false], + } + ) + } + ); + } + + updateForm(entity: User) { + this.entityForm.patchValue({email: entity.email}); + this.entityForm.patchValue({firstName: entity.firstName}); + this.entityForm.patchValue({lastName: entity.lastName}); + this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}}); + this.entityForm.patchValue({additionalInfo: + {defaultDashboardId: entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null}}); + this.entityForm.patchValue({additionalInfo: + {defaultDashboardFullscreen: entity.additionalInfo ? entity.additionalInfo.defaultDashboardFullscreen : false}}); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/user/user.module.ts b/ui-ngx/src/app/modules/home/pages/user/user.module.ts new file mode 100644 index 0000000000..5e7678ec1d --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/user/user.module.ts @@ -0,0 +1,41 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { UserComponent } from '@modules/home/pages/user/user.component'; +import { UserRoutingModule } from '@modules/home/pages/user/user-routing.module'; +import { AddUserDialogComponent } from '@modules/home/pages/user/add-user-dialog.component'; +import { ActivationLinkDialogComponent } from '@modules/home/pages/user/activation-link-dialog.component'; +import { HomeComponentsModule } from '@modules/home/components/home-components.module'; +import { UserTabsComponent } from '@home/pages/user/user-tabs.component'; + +@NgModule({ + declarations: [ + UserComponent, + UserTabsComponent, + AddUserDialogComponent, + ActivationLinkDialogComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + UserRoutingModule + ] +}) +export class UserModule { } diff --git a/ui-ngx/src/app/modules/home/pages/user/users-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/user/users-table-config.resolver.ts new file mode 100644 index 0000000000..3a20827e30 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/user/users-table-config.resolver.ts @@ -0,0 +1,246 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; + +import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; +import { + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { User } from '@shared/models/user.model'; +import { UserService } from '@core/http/user.service'; +import { UserComponent } from '@modules/home/pages/user/user.component'; +import { CustomerService } from '@core/http/customer.service'; +import { map, mergeMap, take, tap } from 'rxjs/operators'; +import { Observable } from 'rxjs'; +import { Authority } from '@shared/models/authority.enum'; +import { CustomerId } from '@shared/models/id/customer-id'; +import { MatDialog } from '@angular/material/dialog'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { AddUserDialogComponent, AddUserDialogData } from '@modules/home/pages/user/add-user-dialog.component'; +import { AuthState } from '@core/auth/auth.models'; +import { select, Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { selectAuth } from '@core/auth/auth.selectors'; +import { AuthService } from '@core/auth/auth.service'; +import { + ActivationLinkDialogComponent, + ActivationLinkDialogData +} from '@modules/home/pages/user/activation-link-dialog.component'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { TenantService } from '@app/core/http/tenant.service'; +import { TenantId } from '@app/shared/models/id/tenant-id'; +import { UserTabsComponent } from '@home/pages/user/user-tabs.component'; + +export interface UsersTableRouteData { + authority: Authority; +} + +@Injectable() +export class UsersTableConfigResolver implements Resolve> { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + private tenantId: string; + private customerId: string; + private authority: Authority; + private authUser: User; + + constructor(private store: Store, + private userService: UserService, + private authService: AuthService, + private tenantService: TenantService, + private customerService: CustomerService, + private translate: TranslateService, + private datePipe: DatePipe, + private dialog: MatDialog) { + + this.config.entityType = EntityType.USER; + this.config.entityComponent = UserComponent; + this.config.entityTabsComponent = UserTabsComponent; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.USER); + this.config.entityResources = entityTypeResources.get(EntityType.USER); + + this.config.columns.push( + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), + new EntityTableColumn('firstName', 'user.first-name', '33%'), + new EntityTableColumn('lastName', 'user.last-name', '33%'), + new EntityTableColumn('email', 'user.email', '33%') + ); + + this.config.deleteEnabled = user => user && user.id && user.id.id !== this.authUser.id.id; + this.config.deleteEntityTitle = user => this.translate.instant('user.delete-user-title', { userEmail: user.email }); + this.config.deleteEntityContent = () => this.translate.instant('user.delete-user-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('user.delete-users-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('user.delete-users-text'); + + this.config.loadEntity = id => this.userService.getUser(id.id); + this.config.saveEntity = user => this.saveUser(user); + this.config.deleteEntity = id => this.userService.deleteUser(id.id); + this.config.onEntityAction = action => this.onUserAction(action); + this.config.addEntity = () => this.addUser(); + } + + resolve(route: ActivatedRouteSnapshot): Observable> { + const routeParams = route.params; + return this.store.pipe(select(selectAuth), take(1)).pipe( + tap((auth) => { + this.authUser = auth.userDetails; + this.authority = routeParams.tenantId ? Authority.TENANT_ADMIN : Authority.CUSTOMER_USER; + if (this.authority === Authority.TENANT_ADMIN) { + this.tenantId = routeParams.tenantId; + this.customerId = NULL_UUID; + this.config.entitiesFetchFunction = pageLink => this.userService.getTenantAdmins(this.tenantId, pageLink); + } else { + this.tenantId = this.authUser.tenantId.id; + this.customerId = routeParams.customerId; + this.config.entitiesFetchFunction = pageLink => this.userService.getCustomerUsers(this.customerId, pageLink); + } + this.updateActionCellDescriptors(auth); + }), + mergeMap(() => this.authority === Authority.TENANT_ADMIN ? + this.tenantService.getTenant(this.tenantId) : + this.customerService.getCustomer(this.customerId)), + map((parentEntity) => { + if (this.authority === Authority.TENANT_ADMIN) { + this.config.tableTitle = parentEntity.title + ': ' + this.translate.instant('user.tenant-admins'); + } else { + this.config.tableTitle = parentEntity.title + ': ' + this.translate.instant('user.customer-users'); + } + return this.config; + }) + ); + } + + updateActionCellDescriptors(auth: AuthState) { + this.config.cellActionDescriptors.splice(0); + if (auth.userTokenAccessEnabled) { + this.config.cellActionDescriptors.push( + { + name: this.authority === Authority.TENANT_ADMIN ? + this.translate.instant('user.login-as-tenant-admin') : + this.translate.instant('user.login-as-customer-user'), + mdiIcon: 'mdi:login', + isEnabled: () => true, + onAction: ($event, entity) => this.loginAsUser($event, entity) + } + ); + } + } + + saveUser(user: User): Observable { + user.tenantId = new TenantId(this.tenantId); + user.customerId = new CustomerId(this.customerId); + user.authority = this.authority; + return this.userService.saveUser(user); + } + + addUser(): Observable { + return this.dialog.open(AddUserDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + tenantId: this.tenantId, + customerId: this.customerId, + authority: this.authority + } + }).afterClosed(); + } + + loginAsUser($event: Event, user: User) { + if ($event) { + $event.stopPropagation(); + } + this.authService.loginAsUser(user.id.id).subscribe(); + } + + displayActivationLink($event: Event, user: User) { + if ($event) { + $event.stopPropagation(); + } + this.userService.getActivationLink(user.id.id).subscribe( + (activationLink) => { + this.dialog.open(ActivationLinkDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + activationLink + } + }); + } + ); + } + + resendActivation($event: Event, user: User) { + if ($event) { + $event.stopPropagation(); + } + this.userService.sendActivationEmail(user.email).subscribe(() => { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('user.activation-email-sent-message'), + type: 'success' + })); + }); + } + + setUserCredentialsEnabled($event: Event, user: User, userCredentialsEnabled: boolean) { + if ($event) { + $event.stopPropagation(); + } + this.userService.setUserCredentialsEnabled(user.id.id, userCredentialsEnabled).subscribe(() => { + if (!user.additionalInfo) { + user.additionalInfo = {}; + } + user.additionalInfo.userCredentialsEnabled = userCredentialsEnabled; + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant(userCredentialsEnabled ? 'user.enable-account-message' : 'user.disable-account-message'), + type: 'success' + })); + }); + } + + onUserAction(action: EntityAction): boolean { + switch (action.action) { + case 'loginAsUser': + this.loginAsUser(action.event, action.entity); + return true; + case 'displayActivationLink': + this.displayActivationLink(action.event, action.entity); + return true; + case 'resendActivation': + this.resendActivation(action.event, action.entity); + return true; + case 'disableAccount': + this.setUserCredentialsEnabled(action.event, action.entity, false); + return true; + case 'enableAccount': + this.setUserCredentialsEnabled(action.event, action.entity, true); + return true; + } + return false; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.html b/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.html new file mode 100644 index 0000000000..0ae1e24a07 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.html @@ -0,0 +1,62 @@ + +
+ +

widget.save-widget-type-as

+ + +
+ + +
+
+
+ widget.save-widget-type-as-text + + widget.title + + + {{ 'widget.title-required' | translate }} + + + + +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.ts new file mode 100644 index 0000000000..595e372e95 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.ts @@ -0,0 +1,81 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, OnInit } from '@angular/core'; +import { MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; +import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { Authority } from '@shared/models/authority.enum'; + +export interface SaveWidgetTypeAsDialogResult { + widgetName: string; + bundleId: string; + bundleAlias: string; +} + +@Component({ + selector: 'tb-save-widget-type-as-dialog', + templateUrl: './save-widget-type-as-dialog.component.html', + styleUrls: [] +}) +export class SaveWidgetTypeAsDialogComponent extends + DialogComponent implements OnInit { + + saveWidgetTypeAsFormGroup: FormGroup; + + bundlesScope: string; + + constructor(protected store: Store, + protected router: Router, + public dialogRef: MatDialogRef, + public fb: FormBuilder) { + super(store, router, dialogRef); + + const authUser = getCurrentAuthUser(store); + if (authUser.authority === Authority.TENANT_ADMIN) { + this.bundlesScope = 'tenant'; + } else { + this.bundlesScope = 'system'; + } + } + + ngOnInit(): void { + this.saveWidgetTypeAsFormGroup = this.fb.group({ + title: [null, [Validators.required]], + widgetsBundle: [null, [Validators.required]] + }); + } + + cancel(): void { + this.dialogRef.close(null); + } + + saveAs(): void { + const widgetName: string = this.saveWidgetTypeAsFormGroup.get('title').value; + const widgetsBundle: WidgetsBundle = this.saveWidgetTypeAsFormGroup.get('widgetsBundle').value; + const result: SaveWidgetTypeAsDialogResult = { + widgetName, + bundleId: widgetsBundle.id.id, + bundleAlias: widgetsBundle.alias + }; + this.dialogRef.close(result); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.html b/ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.html new file mode 100644 index 0000000000..233011d400 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.html @@ -0,0 +1,57 @@ + +
+ +

widget.select-widget-type

+ + +
+ + +
+
+
+
+ +
+
+
+
+ +
+
diff --git a/ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.scss new file mode 100644 index 0000000000..5ead955c81 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.scss @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host ::ng-deep { + button.tb-card-button { + width: 100%; + height: 100%; + max-width: 240px; + .mat-button-wrapper { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + mat-icon { + margin: auto; + } + span { + height: 18px; + min-height: 18px; + max-height: 18px; + margin: 0 0 20px 0; + font-size: 18px; + font-weight: 400; + line-height: 18px; + white-space: normal; + } + } + &.mat-raised-button.mat-primary { + .mat-ripple-element { + opacity: 0.3; + background-color: rgba(255, 255, 255, 0.3); + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.ts new file mode 100644 index 0000000000..92222f2ac8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.ts @@ -0,0 +1,52 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; +import { widgetType, widgetTypesData } from '@shared/models/widget.models'; + +@Component({ + selector: 'tb-select-widget-type-dialog', + templateUrl: './select-widget-type-dialog.component.html', + styleUrls: ['./select-widget-type-dialog.component.scss'] +}) +export class SelectWidgetTypeDialogComponent extends + DialogComponent { + + widgetTypes = widgetType; + + allWidgetTypes = Object.keys(widgetType); + + widgetTypesDataMap = widgetTypesData; + + constructor(protected store: Store, + protected router: Router, + public dialogRef: MatDialogRef) { + super(store, router, dialogRef); + } + + cancel(): void { + this.dialogRef.close(null); + } + + typeSelected(type: widgetType) { + this.dialogRef.close(type); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html new file mode 100644 index 0000000000..a5a43a2ecc --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html @@ -0,0 +1,267 @@ + +
+
+ + + + + + + + + + {{ widgetTypesDataMap.get(widgetTypes[type]).name | translate }} + + + + + + + + + + + + + + + + + + +
+
+
+
+ + +
+
+
+ + + + + {{ 'widget.resource-is-module' | translate }} + + +
+
+ +
+
+
+
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+
+
+
+
+
+
+ + +
+
+ + +
+
+
+
+ +
+
+ + +
+
+
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+ +
+
+ +
+ +
+
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.scss b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.scss new file mode 100644 index 0000000000..6ae687c691 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.scss @@ -0,0 +1,197 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../scss/constants'; + +$edit-toolbar-height: 40px !default; + +tb-widget-editor { + flex: 1; + display: flex; + flex-direction: column; +} + + +.tb-editor { + .tb-split { + box-sizing: border-box; + overflow-x: hidden; + overflow-y: auto; + } + + mat-form-field.resource-field { + max-height: 40px; + margin: 10px 0 0; + .mat-form-field-wrapper { + padding-bottom: 0; + .mat-form-field-flex { + max-height: 40px; + .mat-form-field-infix { + border: 0; + } + } + .mat-form-field-underline { + bottom: 0; + } + } + } + + .ace_editor { + font-size: 14px !important; + } + + .tb-content { + border: 1px solid #c0c0c0; + } + + .gutter { + background-color: transparent; + + background-repeat: no-repeat; + background-position: 50%; + + &.gutter-horizontal { + cursor: col-resize; + background-image: url("../../../../../assets/split.js/grips/vertical.png"); + } + + &.gutter-vertical { + cursor: row-resize; + background-image: url("../../../../../assets/split.js/grips/horizontal.png"); + } + } + + .tb-split.tb-split-horizontal, + .gutter.gutter-horizontal { + float: left; + height: 100%; + } + + .tb-split.tb-split-vertical { + display: flex; + + .tb-split.tb-content { + height: 100%; + } + } + + .container{ + width: 100%; + height: 100%; + } +} + +.tb-split-vertical { + mat-tab-group { + .mat-tab-body-wrapper { + height: calc(100% - 49px); + + mat-tab-body { + height: 100%; + + & > div { + height: 100%; + } + } + } + } +} + +div.tb-editor-area-title-panel { + position: absolute; + top: 5px; + right: 20px; + z-index: 5; + font-size: .8rem; + font-weight: 500; + + label { + padding: 4px; + color: #00acc1; + background: rgba(220, 220, 220, .35); + border-radius: 5px; + &:not(:last-child) { + margin-right: 4px; + } + } + + button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 { + align-items: center; + vertical-align: middle; + min-width: 32px; + min-height: 15px; + padding: 4px; + margin: 0; + font-size: .8rem; + line-height: 15px; + color: #7b7b7b; + background: rgba(220, 220, 220, .35); + &:not(:last-child) { + margin-right: 4px; + } + } +} + +.tb-resize-container { + position: relative; + width: 100%; + height: 100%; + overflow-y: auto; + + .ace_editor { + height: 100%; + } +} + +mat-toolbar.tb-edit-toolbar { + + min-height: $edit-toolbar-height !important; + max-height: $edit-toolbar-height !important; + + button.mat-button-base:not(.mat-icon-button) { + font-size: 12px; + line-height: 28px; + padding: 0 8px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + mat-icon { + height: 20px; + width: 20px; + font-size: 20px; + } + } + + mat-form-field { + input, mat-select { + font-size: 1.1rem; + font-weight: 400; + letter-spacing: .005em; + } + div.mat-form-field-infix { + padding-bottom: 5px; + } + &.tb-widget-title { + min-width: 250px; + } + } + + @media #{$mat-lt-lg} { + mat-form-field.tb-widget-title { + min-width: 0; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts new file mode 100644 index 0000000000..905efe2adb --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts @@ -0,0 +1,691 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { PageComponent } from '@shared/components/page.component'; +import { Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; +import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { WidgetService } from '@core/http/widget.service'; +import { toWidgetInfo, WidgetInfo } from '@home/models/widget-component.models'; +import { Widget, WidgetConfig, WidgetType, widgetType, widgetTypesData } from '@shared/models/widget.models'; +import { ActivatedRoute, Router } from '@angular/router'; +import { deepClone } from '@core/utils'; +import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard'; +import { AuthUser } from '@shared/models/user.model'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { Authority } from '@shared/models/authority.enum'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { Hotkey } from 'angular2-hotkeys'; +import { TranslateService } from '@ngx-translate/core'; +import { getCurrentIsLoading } from '@app/core/interceptors/load.selectors'; +import * as ace from 'ace-builds'; +import { css_beautify, html_beautify } from 'js-beautify'; +import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; +import { WINDOW } from '@core/services/window.service'; +import { WindowMessage } from '@shared/models/window-message.model'; +import { ExceptionData } from '@shared/models/error.models'; +import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions'; +import { MatDialog } from '@angular/material/dialog'; +import { + SaveWidgetTypeAsDialogComponent, + SaveWidgetTypeAsDialogResult +} from '@home/pages/widget/save-widget-type-as-dialog.component'; +import { Subscription } from 'rxjs'; +import { ResizeObserver } from '@juggle/resize-observer'; +import Timeout = NodeJS.Timeout; +import { widgetEditorCompleter } from '@home/pages/widget/widget-editor.models'; + +// @dynamic +@Component({ + selector: 'tb-widget-editor', + templateUrl: './widget-editor.component.html', + styleUrls: ['./widget-editor.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class WidgetEditorComponent extends PageComponent implements OnInit, OnDestroy, HasDirtyFlag { + + @ViewChild('topPanel', {static: true}) + topPanelElmRef: ElementRef; + + @ViewChild('topLeftPanel', {static: true}) + topLeftPanelElmRef: ElementRef; + + @ViewChild('topRightPanel', {static: true}) + topRightPanelElmRef: ElementRef; + + @ViewChild('bottomPanel', {static: true}) + bottomPanelElmRef: ElementRef; + + @ViewChild('javascriptPanel', {static: true}) + javascriptPanelElmRef: ElementRef; + + @ViewChild('framePanel', {static: true}) + framePanelElmRef: ElementRef; + + @ViewChild('htmlInput', {static: true}) + htmlInputElmRef: ElementRef; + + @ViewChild('cssInput', {static: true}) + cssInputElmRef: ElementRef; + + @ViewChild('settingsJsonInput', {static: true}) + settingsJsonInputElmRef: ElementRef; + + @ViewChild('dataKeySettingsJsonInput', {static: true}) + dataKeySettingsJsonInputElmRef: ElementRef; + + @ViewChild('javascriptInput', {static: true}) + javascriptInputElmRef: ElementRef; + + @ViewChild('widgetIFrame', {static: true}) + widgetIFrameElmRef: ElementRef; + + iframe: JQuery; + + widgetTypes = widgetType; + allWidgetTypes = Object.keys(widgetType); + widgetTypesDataMap = widgetTypesData; + + authUser: AuthUser; + + isReadOnly: boolean; + + widgetsBundle: WidgetsBundle; + widgetType: WidgetType; + widget: WidgetInfo; + origWidget: WidgetInfo; + + isDirty = false; + + fullscreen = false; + htmlFullscreen = false; + cssFullscreen = false; + jsonSettingsFullscreen = false; + jsonDataKeySettingsFullscreen = false; + javascriptFullscreen = false; + iFrameFullscreen = false; + + aceEditors: ace.Ace.Editor[] = []; + editorsResizeCafs: {[editorId: string]: CancelAnimationFrame} = {}; + htmlEditor: ace.Ace.Editor; + cssEditor: ace.Ace.Editor; + jsonSettingsEditor: ace.Ace.Editor; + dataKeyJsonSettingsEditor: ace.Ace.Editor; + jsEditor: ace.Ace.Editor; + aceResize$: ResizeObserver; + + onWindowMessageListener = this.onWindowMessage.bind(this); + + iframeWidgetEditModeInited = false; + saveWidgetPending = false; + saveWidgetAsPending = false; + + gotError = false; + errorMarkers: number[] = []; + errorAnnotationId = -1; + + saveWidgetTimeout: Timeout; + + hotKeys: Hotkey[] = []; + + private rxSubscriptions = new Array(); + + constructor(protected store: Store, + @Inject(WINDOW) private window: Window, + private route: ActivatedRoute, + private router: Router, + private widgetService: WidgetService, + private translate: TranslateService, + private raf: RafService, + private dialog: MatDialog) { + super(store); + + this.authUser = getCurrentAuthUser(store); + + this.rxSubscriptions.push(this.route.data.subscribe( + (data) => { + this.init(data); + } + )); + + this.initHotKeys(); + } + + private init(data: any) { + this.widgetsBundle = data.widgetsBundle; + if (this.authUser.authority === Authority.TENANT_ADMIN) { + this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID; + } else { + this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN; + } + this.widgetType = data.widgetEditorData.widgetType; + this.widget = data.widgetEditorData.widget; + if (this.widgetType) { + const config = JSON.parse(this.widget.defaultConfig); + this.widget.defaultConfig = JSON.stringify(config); + } + this.origWidget = deepClone(this.widget); + if (!this.widgetType) { + this.isDirty = true; + } + } + + ngOnInit(): void { + this.initSplitLayout(); + this.initAceEditors(); + this.iframe = $(this.widgetIFrameElmRef.nativeElement); + this.window.addEventListener('message', this.onWindowMessageListener); + this.iframe.attr('data-widget', JSON.stringify(this.widget)); + this.iframe.attr('src', '/widget-editor'); + } + + ngOnDestroy(): void { + this.window.removeEventListener('message', this.onWindowMessageListener); + this.aceResize$.disconnect(); + this.rxSubscriptions.forEach((subscription) => { + subscription.unsubscribe(); + }); + this.rxSubscriptions.length = 0; + } + + private initHotKeys(): void { + this.hotKeys.push( + new Hotkey('ctrl+q', (event: KeyboardEvent) => { + if (!getCurrentIsLoading(this.store) && !this.undoDisabled()) { + event.preventDefault(); + this.undoWidget(); + } + return false; + }, ['INPUT', 'SELECT', 'TEXTAREA'], + this.translate.instant('widget.undo')) + ); + this.hotKeys.push( + new Hotkey('ctrl+s', (event: KeyboardEvent) => { + if (!getCurrentIsLoading(this.store) && !this.saveDisabled()) { + event.preventDefault(); + this.saveWidget(); + } + return false; + }, ['INPUT', 'SELECT', 'TEXTAREA'], + this.translate.instant('widget.save')) + ); + this.hotKeys.push( + new Hotkey('shift+ctrl+s', (event: KeyboardEvent) => { + if (!getCurrentIsLoading(this.store) && !this.saveAsDisabled()) { + event.preventDefault(); + this.saveWidgetAs(); + } + return false; + }, ['INPUT', 'SELECT', 'TEXTAREA'], + this.translate.instant('widget.saveAs')) + ); + this.hotKeys.push( + new Hotkey('shift+ctrl+f', (event: KeyboardEvent) => { + event.preventDefault(); + this.fullscreen = !this.fullscreen; + return false; + }, ['INPUT', 'SELECT', 'TEXTAREA'], + this.translate.instant('widget.toggle-fullscreen')) + ); + this.hotKeys.push( + new Hotkey('ctrl+enter', (event: KeyboardEvent) => { + event.preventDefault(); + this.applyWidgetScript(); + return false; + }, ['INPUT', 'SELECT', 'TEXTAREA'], + this.translate.instant('widget.run')) + ); + } + + private initSplitLayout() { + Split([this.topPanelElmRef.nativeElement, this.bottomPanelElmRef.nativeElement], { + sizes: [35, 65], + gutterSize: 8, + cursor: 'row-resize', + direction: 'vertical' + }); + Split([this.topLeftPanelElmRef.nativeElement, this.topRightPanelElmRef.nativeElement], { + sizes: [50, 50], + gutterSize: 8, + cursor: 'col-resize' + }); + Split([this.javascriptPanelElmRef.nativeElement, this.framePanelElmRef.nativeElement], { + sizes: [50, 50], + gutterSize: 8, + cursor: 'col-resize' + }); + } + + private initAceEditors() { + this.aceResize$ = new ResizeObserver((entries) => { + entries.forEach((entry) => { + const editor = this.aceEditors.find(aceEditor => aceEditor.container === entry.target); + this.onAceEditorResize(editor); + }); + }); + this.htmlEditor = this.createAceEditor(this.htmlInputElmRef, 'html'); + this.htmlEditor.on('input', () => { + const editorValue = this.htmlEditor.getValue(); + if (this.widget.templateHtml !== editorValue) { + this.widget.templateHtml = editorValue; + this.isDirty = true; + } + }); + this.cssEditor = this.createAceEditor(this.cssInputElmRef, 'css'); + this.cssEditor.on('input', () => { + const editorValue = this.cssEditor.getValue(); + if (this.widget.templateCss !== editorValue) { + this.widget.templateCss = editorValue; + this.isDirty = true; + } + }); + this.jsonSettingsEditor = this.createAceEditor(this.settingsJsonInputElmRef, 'json'); + this.jsonSettingsEditor.on('input', () => { + const editorValue = this.jsonSettingsEditor.getValue(); + if (this.widget.settingsSchema !== editorValue) { + this.widget.settingsSchema = editorValue; + this.isDirty = true; + } + }); + this.dataKeyJsonSettingsEditor = this.createAceEditor(this.dataKeySettingsJsonInputElmRef, 'json'); + this.dataKeyJsonSettingsEditor.on('input', () => { + const editorValue = this.dataKeyJsonSettingsEditor.getValue(); + if (this.widget.dataKeySettingsSchema !== editorValue) { + this.widget.dataKeySettingsSchema = editorValue; + this.isDirty = true; + } + }); + this.jsEditor = this.createAceEditor(this.javascriptInputElmRef, 'javascript'); + this.jsEditor.on('input', () => { + const editorValue = this.jsEditor.getValue(); + if (this.widget.controllerScript !== editorValue) { + this.widget.controllerScript = editorValue; + this.isDirty = true; + } + }); + this.jsEditor.on('change', () => { + this.cleanupJsErrors(); + }); + this.jsEditor.completers = [widgetEditorCompleter, ...(this.jsEditor.completers || [])]; + this.setAceEditorValues(); + } + + private setAceEditorValues() { + this.htmlEditor.setValue(this.widget.templateHtml ? this.widget.templateHtml : '', -1); + this.cssEditor.setValue(this.widget.templateCss ? this.widget.templateCss : '', -1); + this.jsonSettingsEditor.setValue(this.widget.settingsSchema ? this.widget.settingsSchema : '', -1); + this.dataKeyJsonSettingsEditor.setValue(this.widget.dataKeySettingsSchema ? this.widget.dataKeySettingsSchema : '', -1); + this.jsEditor.setValue(this.widget.controllerScript ? this.widget.controllerScript : '', -1); + } + + private createAceEditor(editorElementRef: ElementRef, mode: string): ace.Ace.Editor { + const editorElement = editorElementRef.nativeElement; + let editorOptions: Partial = { + mode: `ace/mode/${mode}`, + showGutter: true, + showPrintMargin: true + }; + const advancedOptions = { + enableSnippets: true, + enableBasicAutocompletion: true, + enableLiveAutocompletion: true + }; + editorOptions = {...editorOptions, ...advancedOptions}; + const aceEditor = ace.edit(editorElement, editorOptions); + aceEditor.session.setUseWrapMode(true); + this.aceEditors.push(aceEditor); + this.aceResize$.observe(editorElement); + return aceEditor; + } + + private onAceEditorResize(aceEditor: ace.Ace.Editor) { + if (this.editorsResizeCafs[aceEditor.id]) { + this.editorsResizeCafs[aceEditor.id](); + delete this.editorsResizeCafs[aceEditor.id]; + } + this.editorsResizeCafs[aceEditor.id] = this.raf.raf(() => { + aceEditor.resize(); + aceEditor.renderer.updateFull(); + }); + } + + private onWindowMessage(event: MessageEvent) { + let message: WindowMessage; + if (event.data) { + try { + message = JSON.parse(event.data); + } catch (e) {} + } + if (message) { + switch (message.type) { + case 'widgetException': + this.onWidgetException(message.data); + break; + case 'widgetEditModeInited': + this.onWidgetEditModeInited(); + break; + case 'widgetEditUpdated': + this.onWidgetEditUpdated(message.data); + break; + } + } + } + + private onWidgetEditModeInited() { + this.iframeWidgetEditModeInited = true; + if (this.saveWidgetPending || this.saveWidgetAsPending) { + if (!this.saveWidgetTimeout) { + this.saveWidgetTimeout = setTimeout(() => { + if (!this.gotError) { + if (this.saveWidgetPending) { + this.commitSaveWidget(); + } else if (this.saveWidgetAsPending) { + this.commitSaveWidgetAs(); + } + } else { + this.store.dispatch(new ActionNotificationShow( + {message: this.translate.instant('widget.unable-to-save-widget-error'), type: 'error'})); + this.saveWidgetPending = false; + this.saveWidgetAsPending = false; + } + this.saveWidgetTimeout = undefined; + }, 1500); + } + } + } + + private onWidgetEditUpdated(widget: Widget) { + this.widget.sizeX = widget.sizeX / 2; + this.widget.sizeY = widget.sizeY / 2; + this.widget.defaultConfig = JSON.stringify(widget.config); + this.iframe.attr('data-widget', JSON.stringify(this.widget)); + this.isDirty = true; + } + + private onWidgetException(details: ExceptionData) { + if (!this.gotError) { + this.gotError = true; + let errorInfo = 'Error:'; + if (details.name) { + errorInfo += ' ' + details.name + ':'; + } + if (details.message) { + errorInfo += ' ' + details.message; + } + if (details.lineNumber) { + errorInfo += '
Line ' + details.lineNumber; + if (details.columnNumber) { + errorInfo += ' column ' + details.columnNumber; + } + errorInfo += ' of script.'; + } + if (!this.saveWidgetPending && !this.saveWidgetAsPending) { + this.store.dispatch(new ActionNotificationShow( + {message: errorInfo, type: 'error', target: 'javascriptPanel'})); + } + if (details.lineNumber) { + const line = details.lineNumber - 1; + let column = 0; + if (details.columnNumber) { + column = details.columnNumber; + } + const errorMarkerId = this.jsEditor.session.addMarker(new ace.Range(line, 0, line, Infinity), + 'ace_active-line', 'screenLine'); + this.errorMarkers.push(errorMarkerId); + const annotations = this.jsEditor.session.getAnnotations(); + const errorAnnotation: ace.Ace.Annotation = { + row: line, + column, + text: details.message, + type: 'error' + }; + this.errorAnnotationId = annotations.push(errorAnnotation) - 1; + this.jsEditor.session.setAnnotations(annotations); + } + } + } + + private cleanupJsErrors() { + this.store.dispatch(new ActionNotificationHide({})); + this.errorMarkers.forEach((errorMarker) => { + this.jsEditor.session.removeMarker(errorMarker); + }); + this.errorMarkers.length = 0; + if (this.errorAnnotationId > -1) { + const annotations = this.jsEditor.session.getAnnotations(); + annotations.splice(this.errorAnnotationId, 1); + this.jsEditor.session.setAnnotations(annotations); + this.errorAnnotationId = -1; + } + } + + private commitSaveWidget() { + const id = (this.widgetType && this.widgetType.id) ? this.widgetType.id : undefined; + this.widgetService.saveWidgetType(this.widget, id, this.widgetsBundle.alias).subscribe( + (widgetTypeInstance) => { + this.setWidgetType(widgetTypeInstance); + this.saveWidgetPending = false; + this.store.dispatch(new ActionNotificationShow( + {message: this.translate.instant('widget.widget-saved'), type: 'success', duration: 500})); + }, + () => { + this.saveWidgetPending = false; + } + ); + } + + private commitSaveWidgetAs() { + this.dialog.open(SaveWidgetTypeAsDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] + }).afterClosed().subscribe( + (saveWidgetAsData) => { + if (saveWidgetAsData) { + this.widget.widgetName = saveWidgetAsData.widgetName; + this.widget.alias = undefined; + const config = JSON.parse(this.widget.defaultConfig); + config.title = this.widget.widgetName; + this.widget.defaultConfig = JSON.stringify(config); + this.isDirty = false; + this.widgetService.saveWidgetType(this.widget, undefined, saveWidgetAsData.bundleAlias).subscribe( + (widgetTypeInstance) => { + this.router.navigateByUrl(`/widgets-bundles/${saveWidgetAsData.bundleId}/widgetTypes/${widgetTypeInstance.id.id}`); + } + ); + } + this.saveWidgetAsPending = false; + } + ); + } + + private setWidgetType(widgetTypeInstance: WidgetType) { + this.widgetType = widgetTypeInstance; + this.widget = toWidgetInfo(this.widgetType); + const config = JSON.parse(this.widget.defaultConfig); + this.widget.defaultConfig = JSON.stringify(config); + this.origWidget = deepClone(this.widget); + this.isDirty = false; + } + + applyWidgetScript(): void { + this.cleanupJsErrors(); + this.gotError = false; + this.iframeWidgetEditModeInited = false; + const config: WidgetConfig = JSON.parse(this.widget.defaultConfig); + config.title = this.widget.widgetName; + this.widget.defaultConfig = JSON.stringify(config); + this.iframe.attr('data-widget', JSON.stringify(this.widget)); + this.iframe[0].contentWindow.location.reload(true); + } + + undoWidget(): void { + this.widget = deepClone(this.origWidget); + this.setAceEditorValues(); + this.isDirty = false; + this.applyWidgetScript(); + } + + saveWidget(): void { + if (!this.widget.widgetName) { + this.store.dispatch(new ActionNotificationShow( + {message: this.translate.instant('widget.missing-widget-title-error'), type: 'error'})); + } else { + this.saveWidgetPending = true; + this.applyWidgetScript(); + } + } + + saveWidgetAs(): void { + this.saveWidgetAsPending = true; + this.applyWidgetScript(); + } + + undoDisabled(): boolean { + return !this.isDirty + || !this.iframeWidgetEditModeInited + || this.saveWidgetPending + || this.saveWidgetAsPending; + } + + saveDisabled(): boolean { + return this.isReadOnly + || !this.isDirty + || !this.iframeWidgetEditModeInited + || this.saveWidgetPending + || this.saveWidgetAsPending; + } + + saveAsDisabled(): boolean { + return !this.iframeWidgetEditModeInited + || this.saveWidgetPending + || this.saveWidgetAsPending; + } + + beautifyCss(): void { + const res = css_beautify(this.widget.templateCss, {indent_size: 4}); + if (this.widget.templateCss !== res) { + this.isDirty = true; + this.widget.templateCss = res; + this.cssEditor.setValue(this.widget.templateCss ? this.widget.templateCss : '', -1); + } + } + + beautifyHtml(): void { + const res = html_beautify(this.widget.templateHtml, {indent_size: 4, wrap_line_length: 60}); + if (this.widget.templateHtml !== res) { + this.isDirty = true; + this.widget.templateHtml = res; + this.htmlEditor.setValue(this.widget.templateHtml ? this.widget.templateHtml : '', -1); + } + } + + beautifyJson(): void { + const res = js_beautify(this.widget.settingsSchema, {indent_size: 4}); + if (this.widget.settingsSchema !== res) { + this.isDirty = true; + this.widget.settingsSchema = res; + this.jsonSettingsEditor.setValue(this.widget.settingsSchema ? this.widget.settingsSchema : '', -1); + } + } + + beautifyDataKeyJson(): void { + const res = js_beautify(this.widget.dataKeySettingsSchema, {indent_size: 4}); + if (this.widget.dataKeySettingsSchema !== res) { + this.isDirty = true; + this.widget.dataKeySettingsSchema = res; + this.dataKeyJsonSettingsEditor.setValue(this.widget.dataKeySettingsSchema ? this.widget.dataKeySettingsSchema : '', -1); + } + } + + beautifyJs(): void { + const res = js_beautify(this.widget.controllerScript, {indent_size: 4, wrap_line_length: 60}); + if (this.widget.controllerScript !== res) { + this.isDirty = true; + this.widget.controllerScript = res; + this.jsEditor.setValue(this.widget.controllerScript ? this.widget.controllerScript : '', -1); + } + } + + removeResource(index: number) { + if (index > -1) { + if (this.widget.resources.splice(index, 1).length > 0) { + this.isDirty = true; + } + } + } + + addResource() { + this.widget.resources.push({url: ''}); + this.isDirty = true; + } + + widetTypeChanged() { + const config: WidgetConfig = JSON.parse(this.widget.defaultConfig); + if (this.widget.type !== widgetType.rpc && + this.widget.type !== widgetType.alarm) { + if (config.targetDeviceAliases) { + delete config.targetDeviceAliases; + } + if (config.alarmSource) { + delete config.alarmSource; + } + if (!config.datasources) { + config.datasources = []; + } + if (!config.timewindow) { + config.timewindow = { + realtime: { + timewindowMs: 60000 + } + }; + } + } else if (this.widget.type === widgetType.rpc) { + if (config.datasources) { + delete config.datasources; + } + if (config.alarmSource) { + delete config.alarmSource; + } + if (config.timewindow) { + delete config.timewindow; + } + if (!config.targetDeviceAliases) { + config.targetDeviceAliases = []; + } + } else { // alarm + if (config.datasources) { + delete config.datasources; + } + if (config.targetDeviceAliases) { + delete config.targetDeviceAliases; + } + if (!config.alarmSource) { + config.alarmSource = {}; + } + if (!config.timewindow) { + config.timewindow = { + realtime: { + timewindowMs: 24 * 60 * 60 * 1000 + } + }; + } + } + this.widget.defaultConfig = JSON.stringify(config); + this.isDirty = true; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.models.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.models.ts new file mode 100644 index 0000000000..913e566f34 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.models.ts @@ -0,0 +1,93 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { TbEditorCompleter, TbEditorCompletions } from '@shared/models/ace/completion.models'; +import { widgetContextCompletions } from '@shared/models/ace/widget-completion.models'; +import { serviceCompletions } from '@shared/models/ace/service-completion.models'; + +const widgetEditorCompletions: TbEditorCompletions = { + ... {self: { + description: 'Built-in variable self that is a reference to the widget instance', + type: 'WidgetTypeInstance', + meta: 'object', + children: { + ...{ + onInit: { + description: 'The first function which is called when widget is ready for initialization.
Should be used to prepare widget DOM, process widget settings and initial subscription information.', + meta: 'function' + }, + onDataUpdated: { + description: 'Called when the new data is available from the widget subscription.
Latest data can be accessed from ' + + 'the defaultSubscription property of widget context (ctx).', + meta: 'function' + }, + onResize: { + description: 'Called when widget container is resized. Latest width and height can be obtained from widget context (ctx).', + meta: 'function' + }, + onEditModeChanged: { + description: 'Called when dashboard editing mode is changed. Latest mode is handled by isEdit property of widget context (ctx).', + meta: 'function' + }, + onMobileModeChanged: { + description: 'Called when dashboard view width crosses mobile breakpoint. Latest state is handled by isMobile property of widget context (ctx).', + meta: 'function' + }, + onDestroy: { + description: 'Called when widget element is destroyed. Should be used to cleanup all resources if necessary.', + meta: 'function' + }, + getSettingsSchema: { + description: 'Optional function returning widget settings schema json as alternative to Settings tab of Settings schema section.', + meta: 'function', + return: { + description: 'An widget settings schema json', + type: 'object' + } + }, + getDataKeySettingsSchema: { + description: 'Optional function returning particular data key settings schema json as alternative to Data key settings schema of Settings schema section.', + meta: 'function', + return: { + description: 'A particular data key settings schema json', + type: 'object' + } + }, + typeParameters: { + description: 'Returns object describing widget datasource parameters.', + meta: 'function', + return: { + description: 'An object describing widget datasource parameters.', + type: 'WidgetTypeParameters' + } + }, + actionSources: { + description: 'Returns map describing available widget action sources used to define user actions.', + meta: 'function', + return: { + description: 'A map of action sources by action source id.', + type: '{[actionSourceId: string]: WidgetActionSource}' + } + } + }, + ...widgetContextCompletions + } + }}, + ...widgetContextCompletions, + ...serviceCompletions +}; + +export const widgetEditorCompleter = new TbEditorCompleter(widgetEditorCompletions); diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts new file mode 100644 index 0000000000..503df91e46 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts @@ -0,0 +1,217 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable, NgModule } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve, RouterModule, Routes } from '@angular/router'; + +import { EntitiesTableComponent } from '../../components/entity/entities-table.component'; +import { Authority } from '@shared/models/authority.enum'; +import { WidgetsBundlesTableConfigResolver } from '@modules/home/pages/widget/widgets-bundles-table-config.resolver'; +import { WidgetLibraryComponent } from '@home/pages/widget/widget-library.component'; +import { BreadCrumbConfig, BreadCrumbLabelFunction } from '@shared/components/breadcrumb'; +import { Observable } from 'rxjs'; +import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; +import { WidgetService } from '@core/http/widget.service'; +import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component'; +import { map } from 'rxjs/operators'; +import { toWidgetInfo, WidgetInfo } from '@home/models/widget-component.models'; +import { widgetType, WidgetType } from '@app/shared/models/widget.models'; +import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; +import { WidgetsData } from '@home/models/dashboard-component.models'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; + +export interface WidgetEditorData { + widgetType: WidgetType; + widget: WidgetInfo; +} + +@Injectable() +export class WidgetsBundleResolver implements Resolve { + + constructor(private widgetsService: WidgetService) { + } + + resolve(route: ActivatedRouteSnapshot): Observable { + let widgetsBundleId = route.params.widgetsBundleId; + if (!widgetsBundleId) { + widgetsBundleId = route.parent.params.widgetsBundleId; + } + return this.widgetsService.getWidgetsBundle(widgetsBundleId); + } +} + +@Injectable() +export class WidgetsTypesDataResolver implements Resolve { + + constructor(private widgetsService: WidgetService) { + } + + resolve(route: ActivatedRouteSnapshot): Observable { + const widgetsBundle: WidgetsBundle = route.parent.data.widgetsBundle; + const bundleAlias = widgetsBundle.alias; + const isSystem = widgetsBundle.tenantId.id === NULL_UUID; + return this.widgetsService.loadBundleLibraryWidgets(bundleAlias, + isSystem).pipe( + map((widgets) => { + return { widgets }; + } + )); + } +} + +@Injectable() +export class WidgetEditorDataResolver implements Resolve { + + constructor(private widgetsService: WidgetService) { + } + + resolve(route: ActivatedRouteSnapshot): Observable { + const widgetTypeId = route.params.widgetTypeId; + return this.widgetsService.getWidgetTypeById(widgetTypeId).pipe( + map((result) => { + return { + widgetType: result, + widget: toWidgetInfo(result) + }; + }) + ); + } +} + +@Injectable() +export class WidgetEditorAddDataResolver implements Resolve { + + constructor(private widgetsService: WidgetService) { + } + + resolve(route: ActivatedRouteSnapshot): Observable { + let widgetTypeParam = route.params.widgetType as widgetType; + if (!widgetTypeParam) { + widgetTypeParam = widgetType.timeseries; + } + return this.widgetsService.getWidgetTemplate(widgetTypeParam).pipe( + map((widget) => { + widget.widgetName = null; + return { + widgetType: null, + widget + }; + }) + ); + } +} + +export const widgetTypesBreadcumbLabelFunction: BreadCrumbLabelFunction = ((route, translate) => + route.data.widgetsBundle.title); + +export const widgetEditorBreadcumbLabelFunction: BreadCrumbLabelFunction = + ((route, translate, component) => component ? component.widget.widgetName : ''); + +export const routes: Routes = [ + { + path: 'widgets-bundles', + data: { + breadcrumb: { + label: 'widgets-bundle.widgets-bundles', + icon: 'now_widgets' + } + }, + children: [ + { + path: '', + component: EntitiesTableComponent, + data: { + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], + title: 'widgets-bundle.widgets-bundles' + }, + resolve: { + entitiesTableConfig: WidgetsBundlesTableConfigResolver + } + }, + { + path: ':widgetsBundleId/widgetTypes', + data: { + breadcrumb: { + labelFunction: widgetTypesBreadcumbLabelFunction, + icon: 'now_widgets' + } as BreadCrumbConfig + }, + resolve: { + widgetsBundle: WidgetsBundleResolver + }, + children: [ + { + path: '', + component: WidgetLibraryComponent, + data: { + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], + title: 'widget.widget-library' + }, + resolve: { + widgetsData: WidgetsTypesDataResolver + } + }, + { + path: ':widgetTypeId', + component: WidgetEditorComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], + title: 'widget.editor', + breadcrumb: { + labelFunction: widgetEditorBreadcumbLabelFunction, + icon: 'insert_chart' + } as BreadCrumbConfig + }, + resolve: { + widgetEditorData: WidgetEditorDataResolver + } + }, + { + path: 'add/:widgetType', + component: WidgetEditorComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], + title: 'widget.editor', + breadcrumb: { + labelFunction: widgetEditorBreadcumbLabelFunction, + icon: 'insert_chart' + } as BreadCrumbConfig + }, + resolve: { + widgetEditorData: WidgetEditorAddDataResolver + } + } + ] + } + ] + } +]; + +// @dynamic +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [ + WidgetsBundlesTableConfigResolver, + WidgetsBundleResolver, + WidgetsTypesDataResolver, + WidgetEditorDataResolver, + WidgetEditorAddDataResolver + ] +}) +export class WidgetLibraryRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html new file mode 100644 index 0000000000..e1df8b4335 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html @@ -0,0 +1,40 @@ + +
+ + widgets-bundle.empty +
+ + + diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.scss b/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.scss new file mode 100644 index 0000000000..49b78bc9bd --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.scss @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + button.tb-add-new-widget { + padding-right: 12px; + font-size: 24px; + border-style: dashed; + border-width: 2px; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts new file mode 100644 index 0000000000..bcc433cb63 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts @@ -0,0 +1,206 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, OnInit, ViewChild } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { PageComponent } from '@shared/components/page.component'; +import { AuthUser } from '@shared/models/user.model'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Authority } from '@shared/models/authority.enum'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { of } from 'rxjs'; +import { Widget, widgetType } from '@app/shared/models/widget.models'; +import { WidgetService } from '@core/http/widget.service'; +import { map, mergeMap } from 'rxjs/operators'; +import { DialogService } from '@core/services/dialog.service'; +import { FooterFabButtons } from '@app/shared/components/footer-fab-buttons.component'; +import { DashboardCallbacks, IDashboardComponent, WidgetsData } from '@home/models/dashboard-component.models'; +import { IAliasController, IStateController, StateParams } from '@app/core/api/widget-api.models'; +import { AliasController } from '@core/api/alias-controller'; +import { MatDialog } from '@angular/material/dialog'; +import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component'; +import { TranslateService } from '@ngx-translate/core'; +import { ImportExportService } from '@home/components/import-export/import-export.service'; +import { UtilsService } from '@core/services/utils.service'; +import { EntityService } from '@core/http/entity.service'; + +@Component({ + selector: 'tb-widget-library', + templateUrl: './widget-library.component.html', + styleUrls: ['./widget-library.component.scss'] +}) +export class WidgetLibraryComponent extends PageComponent implements OnInit { + + authUser: AuthUser; + + isReadOnly: boolean; + + widgetsBundle: WidgetsBundle; + + widgetsData: WidgetsData; + + footerFabButtons: FooterFabButtons = { + fabTogglerName: 'widget.add-widget-type', + fabTogglerIcon: 'add', + buttons: [ + { + name: 'widget-type.create-new-widget-type', + icon: 'insert_drive_file', + onAction: ($event) => { + this.addWidgetType($event); + } + }, + { + name: 'widget-type.import', + icon: 'file_upload', + onAction: ($event) => { + this.importWidgetType($event); + } + } + ] + }; + + dashboardCallbacks: DashboardCallbacks = { + onEditWidget: this.openWidgetType.bind(this), + onExportWidget: this.exportWidgetType.bind(this), + onRemoveWidget: this.removeWidgetType.bind(this) + }; + + aliasController: IAliasController = new AliasController(this.utils, + this.entityService, + () => { return { + getStateParams(): StateParams { + return {}; + } + } as IStateController; + }, + {}, + {}); + + @ViewChild('dashboard', {static: true}) dashboard: IDashboardComponent; + + constructor(protected store: Store, + private route: ActivatedRoute, + private router: Router, + private widgetService: WidgetService, + private dialogService: DialogService, + private importExport: ImportExportService, + private dialog: MatDialog, + private translate: TranslateService, + private utils: UtilsService, + private entityService: EntityService) { + super(store); + + this.authUser = getCurrentAuthUser(this.store); + this.widgetsBundle = this.route.snapshot.data.widgetsBundle; + this.widgetsData = this.route.snapshot.data.widgetsData; + if (this.authUser.authority === Authority.TENANT_ADMIN) { + this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID; + } else { + this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN; + } + } + + ngOnInit(): void { + } + + addWidgetType($event: Event): void { + this.openWidgetType($event); + } + + importWidgetType($event: Event): void { + if ($event) { + $event.stopPropagation(); + } + this.importExport.importWidgetType(this.widgetsBundle.alias).subscribe( + (widgetTypeInstance) => { + if (widgetTypeInstance) { + this.reload(); + } + } + ); + } + + private reload() { + const bundleAlias = this.widgetsBundle.alias; + const isSystem = this.widgetsBundle.tenantId.id === NULL_UUID; + this.widgetService.loadBundleLibraryWidgets(bundleAlias, isSystem).subscribe( + (widgets) => { + this.widgetsData = {widgets}; + } + ); + } + + openWidgetType($event: Event, widget?: Widget): void { + if ($event) { + $event.stopPropagation(); + } + if (widget) { + this.router.navigate([widget.typeId.id], {relativeTo: this.route}); + } else { + this.dialog.open(SelectWidgetTypeDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] + }).afterClosed().subscribe( + (type) => { + if (type) { + this.router.navigate(['add', type], {relativeTo: this.route}); + } + } + ); + } + } + + exportWidgetType($event: Event, widget: Widget): void { + if ($event) { + $event.stopPropagation(); + } + this.importExport.exportWidgetType(widget.typeId.id); + } + + removeWidgetType($event: Event, widget: Widget): void { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('widget.remove-widget-type-title', {widgetName: widget.config.title}), + this.translate.instant('widget.remove-widget-type-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + ).pipe( + mergeMap((result) => { + if (result) { + return this.widgetService.deleteWidgetType(widget.bundleAlias, widget.typeAlias, widget.isSystemType); + } else { + return of(false); + } + }), + map((result) => { + if (result !== false) { + this.reload(); + return true; + } else { + return false; + } + } + )).subscribe(); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library.module.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-library.module.ts new file mode 100644 index 0000000000..04fe7fdad7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-library.module.ts @@ -0,0 +1,43 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { WidgetsBundleComponent } from '@modules/home/pages/widget/widgets-bundle.component'; +import { WidgetLibraryRoutingModule } from '@modules/home/pages/widget/widget-library-routing.module'; +import { HomeComponentsModule } from '@modules/home/components/home-components.module'; +import { WidgetLibraryComponent } from './widget-library.component'; +import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component'; +import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component'; +import { SaveWidgetTypeAsDialogComponent } from './save-widget-type-as-dialog.component'; + +@NgModule({ + declarations: [ + WidgetsBundleComponent, + WidgetLibraryComponent, + WidgetEditorComponent, + SelectWidgetTypeDialogComponent, + SaveWidgetTypeAsDialogComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + WidgetLibraryRoutingModule + ] +}) +export class WidgetLibraryModule { } diff --git a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.html b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.html new file mode 100644 index 0000000000..a63e2353d2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.html @@ -0,0 +1,50 @@ + +
+ + + +
+
+
+
+ + widgets-bundle.title + + + {{ 'widgets-bundle.title-required' | translate }} + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.scss b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.scss new file mode 100644 index 0000000000..bb3718c2da --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.scss @@ -0,0 +1,18 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + +} diff --git a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.ts new file mode 100644 index 0000000000..26db26b4bf --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.ts @@ -0,0 +1,58 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityComponent } from '../../components/entity/entity.component'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; + +@Component({ + selector: 'tb-widgets-bundle', + templateUrl: './widgets-bundle.component.html', + styleUrls: ['./widgets-bundle.component.scss'] +}) +export class WidgetsBundleComponent extends EntityComponent { + + constructor(protected store: Store, + @Inject('entity') protected entityValue: WidgetsBundle, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + public fb: FormBuilder) { + super(store, fb, entityValue, entitiesTableConfigValue); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + buildForm(entity: WidgetsBundle): FormGroup { + return this.fb.group( + { + title: [entity ? entity.title : '', [Validators.required]] + } + ); + } + + updateForm(entity: WidgetsBundle) { + this.entityForm.patchValue({title: entity.title}); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts new file mode 100644 index 0000000000..c59dd6cc63 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts @@ -0,0 +1,169 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; + +import { Resolve, Router } from '@angular/router'; +import { + checkBoxCell, + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; +import { WidgetService } from '@app/core/http/widget.service'; +import { WidgetsBundleComponent } from '@modules/home/pages/widget/widgets-bundle.component'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { getCurrentAuthUser } from '@app/core/auth/auth.selectors'; +import { Authority } from '@shared/models/authority.enum'; +import { DialogService } from '@core/services/dialog.service'; +import { ImportExportService } from '@home/components/import-export/import-export.service'; +import { Direction } from "@shared/models/page/sort-order"; + +@Injectable() +export class WidgetsBundlesTableConfigResolver implements Resolve> { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor(private store: Store, + private dialogService: DialogService, + private widgetsService: WidgetService, + private translate: TranslateService, + private importExport: ImportExportService, + private datePipe: DatePipe, + private router: Router) { + + this.config.entityType = EntityType.WIDGETS_BUNDLE; + this.config.entityComponent = WidgetsBundleComponent; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.WIDGETS_BUNDLE); + this.config.entityResources = entityTypeResources.get(EntityType.WIDGETS_BUNDLE); + this.config.defaultSortOrder = {property: 'title', direction: Direction.ASC}; + + this.config.entityTitle = (widgetsBundle) => widgetsBundle ? + widgetsBundle.title : ''; + + this.config.columns.push( + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), + new EntityTableColumn('title', 'widgets-bundle.title', '100%'), + new EntityTableColumn('tenantId', 'widgets-bundle.system', '60px', + entity => { + return checkBoxCell(entity.tenantId.id === NULL_UUID); + }), + ); + + this.config.addActionDescriptors.push( + { + name: this.translate.instant('widgets-bundle.create-new-widgets-bundle'), + icon: 'insert_drive_file', + isEnabled: () => true, + onAction: ($event) => this.config.table.addEntity($event) + }, + { + name: this.translate.instant('widgets-bundle.import'), + icon: 'file_upload', + isEnabled: () => true, + onAction: ($event) => this.importWidgetsBundle($event) + } + ); + + this.config.cellActionDescriptors.push( + { + name: this.translate.instant('widgets-bundle.open-widgets-bundle'), + icon: 'now_widgets', + isEnabled: () => true, + onAction: ($event, entity) => this.openWidgetsBundle($event, entity) + }, + { + name: this.translate.instant('widgets-bundle.export'), + icon: 'file_download', + isEnabled: () => true, + onAction: ($event, entity) => this.exportWidgetsBundle($event, entity) + } + ); + + this.config.deleteEntityTitle = widgetsBundle => this.translate.instant('widgets-bundle.delete-widgets-bundle-title', + { widgetsBundleTitle: widgetsBundle.title }); + this.config.deleteEntityContent = () => this.translate.instant('widgets-bundle.delete-widgets-bundle-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('widgets-bundle.delete-widgets-bundles-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('widgets-bundle.delete-widgets-bundles-text'); + + this.config.entitiesFetchFunction = pageLink => this.widgetsService.getWidgetBundles(pageLink); + this.config.loadEntity = id => this.widgetsService.getWidgetsBundle(id.id); + this.config.saveEntity = widgetsBundle => this.widgetsService.saveWidgetsBundle(widgetsBundle); + this.config.deleteEntity = id => this.widgetsService.deleteWidgetsBundle(id.id); + this.config.onEntityAction = action => this.onWidgetsBundleAction(action); + } + + resolve(): EntityTableConfig { + this.config.tableTitle = this.translate.instant('widgets-bundle.widgets-bundles'); + const authUser = getCurrentAuthUser(this.store); + this.config.deleteEnabled = (widgetsBundle) => this.isWidgetsBundleEditable(widgetsBundle, authUser.authority); + this.config.entitySelectionEnabled = (widgetsBundle) => this.isWidgetsBundleEditable(widgetsBundle, authUser.authority); + this.config.detailsReadonly = (widgetsBundle) => !this.isWidgetsBundleEditable(widgetsBundle, authUser.authority); + return this.config; + } + + isWidgetsBundleEditable(widgetsBundle: WidgetsBundle, authority: Authority): boolean { + if (authority === Authority.TENANT_ADMIN) { + return widgetsBundle && widgetsBundle.tenantId && widgetsBundle.tenantId.id !== NULL_UUID; + } else { + return authority === Authority.SYS_ADMIN; + } + } + + importWidgetsBundle($event: Event) { + this.importExport.importWidgetsBundle().subscribe( + (widgetsBundle) => { + if (widgetsBundle) { + this.config.table.updateData(); + } + } + ); + } + + openWidgetsBundle($event: Event, widgetsBundle: WidgetsBundle) { + if ($event) { + $event.stopPropagation(); + } + this.router.navigateByUrl(`widgets-bundles/${widgetsBundle.id.id}/widgetTypes`); + } + + exportWidgetsBundle($event: Event, widgetsBundle: WidgetsBundle) { + if ($event) { + $event.stopPropagation(); + } + this.importExport.exportWidgetsBundle(widgetsBundle.id.id); + } + + onWidgetsBundleAction(action: EntityAction): boolean { + switch (action.action) { + case 'open': + this.openWidgetsBundle(action.event, action.entity); + return true; + case 'export': + this.exportWidgetsBundle(action.event, action.entity); + return true; + } + return false; + } + +} diff --git a/ui-ngx/src/app/modules/home/public-api.ts b/ui-ngx/src/app/modules/home/public-api.ts new file mode 100644 index 0000000000..c0c122c69d --- /dev/null +++ b/ui-ngx/src/app/modules/home/public-api.ts @@ -0,0 +1,17 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export * from './home.module'; diff --git a/ui-ngx/src/app/modules/login/login-routing.module.ts b/ui-ngx/src/app/modules/login/login-routing.module.ts new file mode 100644 index 0000000000..a2cf21ced9 --- /dev/null +++ b/ui-ngx/src/app/modules/login/login-routing.module.ts @@ -0,0 +1,79 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { LoginComponent } from './pages/login/login.component'; +import { AuthGuard } from '@core/guards/auth.guard'; +import { ResetPasswordRequestComponent } from '@modules/login/pages/login/reset-password-request.component'; +import { ResetPasswordComponent } from '@modules/login/pages/login/reset-password.component'; +import { CreatePasswordComponent } from '@modules/login/pages/login/create-password.component'; + +const routes: Routes = [ + { + path: 'login', + component: LoginComponent, + data: { + title: 'login.login', + module: 'public' + }, + canActivate: [AuthGuard] + }, + { + path: 'login/resetPasswordRequest', + component: ResetPasswordRequestComponent, + data: { + title: 'login.request-password-reset', + module: 'public' + }, + canActivate: [AuthGuard] + }, + { + path: 'login/resetPassword', + component: ResetPasswordComponent, + data: { + title: 'login.reset-password', + module: 'public' + }, + canActivate: [AuthGuard] + }, + { + path: 'login/resetExpiredPassword', + component: ResetPasswordComponent, + data: { + title: 'login.reset-password', + module: 'public', + expiredPassword: true + }, + canActivate: [AuthGuard] + }, + { + path: 'login/createPassword', + component: CreatePasswordComponent, + data: { + title: 'login.create-password', + module: 'public' + }, + canActivate: [AuthGuard] + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class LoginRoutingModule { } diff --git a/ui-ngx/src/app/modules/login/login.module.ts b/ui-ngx/src/app/modules/login/login.module.ts new file mode 100644 index 0000000000..03fb9a83da --- /dev/null +++ b/ui-ngx/src/app/modules/login/login.module.ts @@ -0,0 +1,40 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { LoginRoutingModule } from './login-routing.module'; +import { LoginComponent } from './pages/login/login.component'; +import { SharedModule } from '@app/shared/shared.module'; +import { ResetPasswordRequestComponent } from '@modules/login/pages/login/reset-password-request.component'; +import { ResetPasswordComponent } from '@modules/login/pages/login/reset-password.component'; +import { CreatePasswordComponent } from '@modules/login/pages/login/create-password.component'; + +@NgModule({ + declarations: [ + LoginComponent, + ResetPasswordRequestComponent, + ResetPasswordComponent, + CreatePasswordComponent + ], + imports: [ + CommonModule, + SharedModule, + LoginRoutingModule + ] +}) +export class LoginModule { } diff --git a/ui-ngx/src/app/modules/login/pages/login/create-password.component.html b/ui-ngx/src/app/modules/login/pages/login/create-password.component.html new file mode 100644 index 0000000000..ae412afaf4 --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/create-password.component.html @@ -0,0 +1,56 @@ + +
+ + + login.create-password + + + + + +
+
+
+ + + common.password + + lock + + + login.password-again + + lock + +
+ + +
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/login/pages/login/create-password.component.scss b/ui-ngx/src/app/modules/login/pages/login/create-password.component.scss new file mode 100644 index 0000000000..96cd5016a1 --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/create-password.component.scss @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../scss/constants'; + +:host { + display: flex; + flex: 1 1 0; + .tb-create-password-content { + background-color: #eee; + .tb-create-password-card { + @media #{$mat-gt-xs} { + width: 450px !important; + } + } + } +} diff --git a/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts b/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts new file mode 100644 index 0000000000..47e36c5ce4 --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts @@ -0,0 +1,74 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { AuthService } from '@core/auth/auth.service'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { PageComponent } from '@shared/components/page.component'; +import { FormBuilder } from '@angular/forms'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { TranslateService } from '@ngx-translate/core'; +import { ActivatedRoute } from '@angular/router'; +import { Subscription } from 'rxjs'; + +@Component({ + selector: 'tb-create-password', + templateUrl: './create-password.component.html', + styleUrls: ['./create-password.component.scss'] +}) +export class CreatePasswordComponent extends PageComponent implements OnInit, OnDestroy { + + activateToken = ''; + sub: Subscription; + + createPassword = this.fb.group({ + password: [''], + password2: [''] + }); + + constructor(protected store: Store, + private route: ActivatedRoute, + private authService: AuthService, + private translate: TranslateService, + public fb: FormBuilder) { + super(store); + } + + ngOnInit() { + this.sub = this.route + .queryParams + .subscribe(params => { + this.activateToken = params.activateToken || ''; + }); + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + this.sub.unsubscribe(); + } + + onCreatePassword() { + if (this.createPassword.get('password').value !== this.createPassword.get('password2').value) { + this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('login.passwords-mismatch-error'), + type: 'error' })); + } else { + this.authService.activate( + this.activateToken, + this.createPassword.get('password').value, true).subscribe(); + } + } +} diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.html b/ui-ngx/src/app/modules/login/pages/login/login.component.html new file mode 100644 index 0000000000..c0d4d5ed23 --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.html @@ -0,0 +1,70 @@ + + diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.scss b/ui-ngx/src/app/modules/login/pages/login/login.component.scss new file mode 100644 index 0000000000..b8fa052e75 --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.scss @@ -0,0 +1,93 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../scss/constants'; + +:host { + display: flex; + flex: 1 1 0; + .tb-login-content { + margin-top: 36px; + margin-bottom: 76px; + background-color: rgb(250, 250, 250); + .tb-login-form { + @media #{$mat-gt-xs} { + width: 550px !important; + } + + .forgot-password { + padding: 0 0.5em 1em; + .tb-reset-password { + padding: 0 6px; + } + } + + .tb-action-button{ + padding: 20px 0 16px; + } + } + + .oauth-container{ + padding: 0; + + .container-divider { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + width: 100%; + + .line { + flex: 1; + } + + .mat-divider-horizontal{ + position: relative; + } + + .text { + padding-right: 10px; + padding-left: 10px; + } + } + + .material-icons{ + width: 20px; + min-width: 20px; + } + + a.login-with-button { + color: rgba(black, 0.87); + + &:hover { + border-bottom: 0; + } + + .icon{ + height: 20px; + width: 20px; + vertical-align: sub; + } + } + + .centered ::ng-deep .mat-button-wrapper { + display: flex; + justify-content: center; + align-items: center; + } + } + } +} + diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.ts b/ui-ngx/src/app/modules/login/pages/login/login.component.ts new file mode 100644 index 0000000000..30ecafe0b2 --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.ts @@ -0,0 +1,72 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, OnInit } from '@angular/core'; +import { AuthService } from '@core/auth/auth.service'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { PageComponent } from '@shared/components/page.component'; +import { FormBuilder } from '@angular/forms'; +import { HttpErrorResponse } from '@angular/common/http'; +import { Constants } from '@shared/models/constants'; +import { Router } from '@angular/router'; +import { OAuth2ClientInfo } from '@shared/models/oauth2.models'; + +@Component({ + selector: 'tb-login', + templateUrl: './login.component.html', + styleUrls: ['./login.component.scss'] +}) +export class LoginComponent extends PageComponent implements OnInit { + + loginFormGroup = this.fb.group({ + username: '', + password: '' + }); + oauth2Clients: Array = null; + + constructor(protected store: Store, + private authService: AuthService, + public fb: FormBuilder, + private router: Router) { + super(store); + } + + ngOnInit() { + this.oauth2Clients = this.authService.oauth2Clients; + } + + login(): void { + if (this.loginFormGroup.valid) { + this.authService.login(this.loginFormGroup.value).subscribe( + () => {}, + (error: HttpErrorResponse) => { + if (error && error.error && error.error.errorCode) { + if (error.error.errorCode === Constants.serverErrorCode.credentialsExpired) { + this.router.navigateByUrl(`login/resetExpiredPassword?resetToken=${error.error.resetToken}`); + } + } + } + ); + } else { + Object.keys(this.loginFormGroup.controls).forEach(field => { + const control = this.loginFormGroup.get(field); + control.markAsTouched({onlySelf: true}); + }); + } + } + +} diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.html b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.html new file mode 100644 index 0000000000..b4a64c9a93 --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.html @@ -0,0 +1,54 @@ + +
+ + + login.request-password-reset + + + + + +
+
+
+ + + login.email + + email + + {{ 'user.invalid-email-format' | translate }} + + +
+ + +
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.scss b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.scss new file mode 100644 index 0000000000..06fd1646ef --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.scss @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../scss/constants'; + +:host { + display: flex; + flex: 1 1 0; + .tb-request-password-reset-content { + background-color: #eee; + .tb-request-password-reset-card { + @media #{$mat-gt-xs} { + width: 450px !important; + } + } + } +} diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.ts b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.ts new file mode 100644 index 0000000000..a2356439c2 --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.ts @@ -0,0 +1,56 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, OnInit } from '@angular/core'; +import { AuthService } from '@core/auth/auth.service'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { PageComponent } from '@shared/components/page.component'; +import { FormBuilder } from '@angular/forms'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { TranslateService } from '@ngx-translate/core'; + +@Component({ + selector: 'tb-reset-password-request', + templateUrl: './reset-password-request.component.html', + styleUrls: ['./reset-password-request.component.scss'] +}) +export class ResetPasswordRequestComponent extends PageComponent implements OnInit { + + requestPasswordRequest = this.fb.group({ + email: [''] + }); + + constructor(protected store: Store, + private authService: AuthService, + private translate: TranslateService, + public fb: FormBuilder) { + super(store); + } + + ngOnInit() { + } + + sendResetPasswordLink() { + this.authService.sendResetPasswordLink(this.requestPasswordRequest.get('email').value).subscribe( + () => { + this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('login.password-link-sent-message'), + type: 'success' })); + } + ); + } + +} diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html new file mode 100644 index 0000000000..6c712f5e0a --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html @@ -0,0 +1,59 @@ + +
+ + + login.password-reset + + +
login.expired-password-reset-message
+
+ + + + +
+
+
+ + + login.new-password + + lock + + + login.new-password-again + + lock + +
+ + +
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.scss b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.scss new file mode 100644 index 0000000000..ec8986748c --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.scss @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../scss/constants'; + +:host { + display: flex; + flex: 1 1 0; + .tb-reset-password-content { + background-color: #eee; + .tb-reset-password-card { + @media #{$mat-gt-sm} { + width: 450px !important; + } + } + + .tb-card-title{ + padding-top: 0; + padding-bottom: 0; + } + } +} diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts new file mode 100644 index 0000000000..5b62f11438 --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts @@ -0,0 +1,77 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { AuthService } from '@core/auth/auth.service'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { PageComponent } from '@shared/components/page.component'; +import { FormBuilder } from '@angular/forms'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { TranslateService } from '@ngx-translate/core'; +import { ActivatedRoute } from '@angular/router'; +import { Subscription } from 'rxjs'; + +@Component({ + selector: 'tb-reset-password', + templateUrl: './reset-password.component.html', + styleUrls: ['./reset-password.component.scss'] +}) +export class ResetPasswordComponent extends PageComponent implements OnInit, OnDestroy { + + isExpiredPassword: boolean; + + resetToken = ''; + sub: Subscription; + + resetPassword = this.fb.group({ + newPassword: [''], + newPassword2: [''] + }); + + constructor(protected store: Store, + private route: ActivatedRoute, + private authService: AuthService, + private translate: TranslateService, + public fb: FormBuilder) { + super(store); + } + + ngOnInit() { + this.isExpiredPassword = this.route.snapshot.data.expiredPassword; + this.sub = this.route + .queryParams + .subscribe(params => { + this.resetToken = params.resetToken || ''; + }); + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + this.sub.unsubscribe(); + } + + onResetPassword() { + if (this.resetPassword.get('newPassword').value !== this.resetPassword.get('newPassword2').value) { + this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('login.passwords-mismatch-error'), + type: 'error' })); + } else { + this.authService.resetPassword( + this.resetToken, + this.resetPassword.get('newPassword').value).subscribe(); + } + } +} diff --git a/ui-ngx/src/app/shared/animations/speed-dial-fab.animations.ts b/ui-ngx/src/app/shared/animations/speed-dial-fab.animations.ts new file mode 100644 index 0000000000..d3c3d0fcb6 --- /dev/null +++ b/ui-ngx/src/app/shared/animations/speed-dial-fab.animations.ts @@ -0,0 +1,58 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { animate, keyframes, query, stagger, state, style, transition, trigger } from '@angular/animations'; + +export const speedDialFabAnimations = [ + trigger('fabToggler', [ + state('inactive', style({ + transform: 'rotate(0deg)' + })), + state('active', style({ + transform: 'rotate(225deg)' + })), + transition('* <=> *', animate('200ms cubic-bezier(0.4, 0.0, 0.2, 1)')), + ]), + trigger('speedDialStagger', [ + transition('* => *', [ + + query(':enter', style({ opacity: 0 }), {optional: true}), + + query(':enter', stagger('40ms', + [ + animate('200ms cubic-bezier(0.4, 0.0, 0.2, 1)', + keyframes( + [ + style({opacity: 0, transform: 'translateY(10px)'}), + style({opacity: 1, transform: 'translateY(0)'}), + ] + ) + ) + ] + ), {optional: true}), + + query(':leave', + animate('200ms cubic-bezier(0.4, 0.0, 0.2, 1)', + keyframes([ + style({opacity: 1}), + style({opacity: 0}), + ]) + ), {optional: true} + ) + + ]) + ]) +]; diff --git a/ui-ngx/src/app/shared/components/breadcrumb.component.html b/ui-ngx/src/app/shared/components/breadcrumb.component.html new file mode 100644 index 0000000000..d7d4438566 --- /dev/null +++ b/ui-ngx/src/app/shared/components/breadcrumb.component.html @@ -0,0 +1,41 @@ + +
+

+ {{ breadcrumb.ignoreTranslate ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : breadcrumb.label) : (breadcrumb.label | translate) }} +

+ + + + + + {{ breadcrumb.icon }} + + {{ breadcrumb.ignoreTranslate ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : breadcrumb.label) : (breadcrumb.label | translate) }} + + + + + + {{ breadcrumb.icon }} + + {{ breadcrumb.ignoreTranslate ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : breadcrumb.label) : (breadcrumb.label | translate) }} + + > + +
diff --git a/ui-ngx/src/app/shared/components/breadcrumb.component.scss b/ui-ngx/src/app/shared/components/breadcrumb.component.scss new file mode 100644 index 0000000000..a36f9b3bb9 --- /dev/null +++ b/ui-ngx/src/app/shared/components/breadcrumb.component.scss @@ -0,0 +1,57 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + min-width: 0; + + .tb-breadcrumb { + font-size: 18px; + font-weight: 400; + overflow: hidden; + + h1, + a, + span:not(.divider) { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + h1 { + font-size: 24px; + font-weight: 400; + } + + a { + border: none; + opacity: .75; + transition: opacity .35s; + color: inherit; + text-decoration: none; + outline: none; + } + + a:hover, + a:focus { + text-decoration: none !important; + border: none; + opacity: 1; + } + + .divider { + padding: 0 20px; + } + } +} diff --git a/ui-ngx/src/app/shared/components/breadcrumb.component.ts b/ui-ngx/src/app/shared/components/breadcrumb.component.ts new file mode 100644 index 0000000000..401d72859b --- /dev/null +++ b/ui-ngx/src/app/shared/components/breadcrumb.component.ts @@ -0,0 +1,119 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { BehaviorSubject, Subject } from 'rxjs'; +import { BreadCrumb, BreadCrumbConfig } from './breadcrumb'; +import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router'; +import { distinctUntilChanged, filter, map } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; +import { guid } from '@core/utils'; + +@Component({ + selector: 'tb-breadcrumb', + templateUrl: './breadcrumb.component.html', + styleUrls: ['./breadcrumb.component.scss'] +}) +export class BreadcrumbComponent implements OnInit, OnDestroy { + + activeComponentValue: any; + + @Input() + set activeComponent(activeComponent: any) { + this.activeComponentValue = activeComponent; + } + + breadcrumbs$: Subject> = new BehaviorSubject>(this.buildBreadCrumbs(this.activatedRoute.snapshot)); + + routerEventsSubscription = this.router.events.pipe( + filter((event) => event instanceof NavigationEnd ), + distinctUntilChanged(), + map( () => this.buildBreadCrumbs(this.activatedRoute.snapshot) ) + ).subscribe(breadcrumns => this.breadcrumbs$.next(breadcrumns) ); + + lastBreadcrumb$ = this.breadcrumbs$.pipe( + map( breadcrumbs => breadcrumbs[breadcrumbs.length - 1]) + ); + + constructor(private router: Router, + private activatedRoute: ActivatedRoute, + private translate: TranslateService) { + } + + ngOnInit(): void { + } + + ngOnDestroy(): void { + if (this.routerEventsSubscription) { + this.routerEventsSubscription.unsubscribe(); + } + } + + private lastChild(route: ActivatedRouteSnapshot) { + let child = route; + while (child.firstChild !== null) { + child = child.firstChild; + } + return child; + } + + buildBreadCrumbs(route: ActivatedRouteSnapshot, breadcrumbs: Array = [], + lastChild?: ActivatedRouteSnapshot): Array { + if (!lastChild) { + lastChild = this.lastChild(route); + } + let newBreadcrumbs = breadcrumbs; + if (route.routeConfig && route.routeConfig.data) { + const breadcrumbConfig = route.routeConfig.data.breadcrumb as BreadCrumbConfig; + if (breadcrumbConfig && !breadcrumbConfig.skip) { + let label; + let labelFunction; + let ignoreTranslate; + if (breadcrumbConfig.labelFunction) { + labelFunction = () => { + return breadcrumbConfig.labelFunction(route, this.translate, this.activeComponentValue, lastChild.data); + }; + ignoreTranslate = true; + } else { + label = breadcrumbConfig.label || 'home.home'; + ignoreTranslate = false; + } + const icon = breadcrumbConfig.icon || 'home'; + const isMdiIcon = icon.startsWith('mdi:'); + const link = [ route.pathFromRoot.map(v => v.url.map(segment => segment.toString()).join('/')).join('/') ]; + const breadcrumb = { + id: guid(), + label, + labelFunction, + ignoreTranslate, + icon, + isMdiIcon, + link, + queryParams: null + }; + newBreadcrumbs = [...breadcrumbs, breadcrumb]; + } + } + if (route.firstChild) { + return this.buildBreadCrumbs(route.firstChild, newBreadcrumbs, lastChild); + } + return newBreadcrumbs; + } + + trackByBreadcrumbs(index: number, breadcrumb: BreadCrumb){ + return breadcrumb.id; + } +} diff --git a/ui-ngx/src/app/shared/components/breadcrumb.ts b/ui-ngx/src/app/shared/components/breadcrumb.ts new file mode 100644 index 0000000000..bf43f8ef5b --- /dev/null +++ b/ui-ngx/src/app/shared/components/breadcrumb.ts @@ -0,0 +1,38 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ActivatedRouteSnapshot, Params } from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; +import { HasUUID } from '@shared/models/id/has-uuid'; + +export interface BreadCrumb extends HasUUID{ + label: string; + labelFunction?: () => string; + ignoreTranslate: boolean; + icon: string; + isMdiIcon: boolean; + link: any[]; + queryParams: Params; +} + +export type BreadCrumbLabelFunction = (route: ActivatedRouteSnapshot, translate: TranslateService, component: C, data?: any) => string; + +export interface BreadCrumbConfig { + labelFunction: BreadCrumbLabelFunction; + label: string; + icon: string; + skip: boolean; +} diff --git a/ui-ngx/src/app/shared/components/cheatsheet.component.ts b/ui-ngx/src/app/shared/components/cheatsheet.component.ts new file mode 100644 index 0000000000..52129db790 --- /dev/null +++ b/ui-ngx/src/app/shared/components/cheatsheet.component.ts @@ -0,0 +1,165 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, Input, OnDestroy, OnInit } from '@angular/core'; +import { Hotkey, HotkeysService } from 'angular2-hotkeys'; + +@Component({ + selector : 'tb-hotkeys-cheatsheet', + styles : [` +.tb-hotkeys-container { + display: table !important; + position: fixed; + width: 100%; + height: 100%; + top: 0; + left: 0; + color: #333; + font-size: 1em; + background-color: rgba(255,255,255,0.9); + outline: 0; +} +.tb-hotkeys-container.fade { + z-index: -1024; + visibility: hidden; + opacity: 0; + -webkit-transition: opacity 0.15s linear; + -moz-transition: opacity 0.15s linear; + -o-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; +} +.tb-hotkeys-container.fade.in { + z-index: 10002; + visibility: visible; + opacity: 1; +} +.tb-hotkeys-title { + font-weight: bold; + text-align: center; + font-size: 1.2em; +} +.tb-hotkeys { + width: 100%; + height: 100%; + display: table-cell; + vertical-align: middle; +} +.tb-hotkeys table { + margin: auto; + color: #333; +} +.tb-content { + display: table-cell; + vertical-align: middle; +} +.tb-hotkeys-keys { + padding: 5px; + text-align: right; +} +.tb-hotkeys-key { + display: inline-block; + color: #fff; + background-color: #333; + border: 1px solid #333; + border-radius: 5px; + text-align: center; + margin-right: 5px; + box-shadow: inset 0 1px 0 #666, 0 1px 0 #bbb; + padding: 5px 9px; + font-size: 1em; +} +.tb-hotkeys-text { + padding-left: 10px; + font-size: 1em; +} +.tb-hotkeys-close { + position: fixed; + top: 20px; + right: 20px; + font-size: 2em; + font-weight: bold; + padding: 5px 10px; + border: 1px solid #ddd; + border-radius: 5px; + min-height: 45px; + min-width: 45px; + text-align: center; +} +.tb-hotkeys-close:hover { + background-color: #fff; + cursor: pointer; +} +@media all and (max-width: 500px) { + .tb-hotkeys { + font-size: 0.8em; + } +} +@media all and (min-width: 750px) { + .tb-hotkeys { + font-size: 1.2em; + } +} `], + template : ``, +}) +export class TbCheatSheetComponent implements OnInit, OnDestroy { + + helpVisible = false; + @Input() title = 'Keyboard Shortcuts:'; + + @Input() + hotkeys: Hotkey[]; + + hotkeysList: Hotkey[]; + + private mousetrap: MousetrapInstance; + + constructor(private elementRef: ElementRef, + private hotkeysService: HotkeysService) { + this.mousetrap = new Mousetrap(this.elementRef.nativeElement); + this.mousetrap.bind('?', (event: KeyboardEvent, combo: string) => { + this.toggleCheatSheet(); + }); + } + + public ngOnInit(): void { + if (this.hotkeys) { + this.hotkeysList = this.hotkeys.filter(hotkey => hotkey.description); + } + } + + public setHotKeys(hotkeys: Hotkey[]) { + this.hotkeysList = hotkeys.filter(hotkey => hotkey.description); + } + + public toggleCheatSheet(): void { + this.helpVisible = !this.helpVisible; + } + + ngOnDestroy() { + this.mousetrap.unbind('?'); + } +} diff --git a/ui-ngx/src/app/shared/components/circular-progress.directive.ts b/ui-ngx/src/app/shared/components/circular-progress.directive.ts new file mode 100644 index 0000000000..a7b0b7b690 --- /dev/null +++ b/ui-ngx/src/app/shared/components/circular-progress.directive.ts @@ -0,0 +1,82 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ComponentFactoryResolver, ComponentRef, Directive, ElementRef, Input, ViewContainerRef } from '@angular/core'; +import { MatSpinner } from '@angular/material/progress-spinner'; + +@Directive({ + selector: '[tb-circular-progress]' +}) +export class CircularProgressDirective { + + showProgressValue = false; + + children: JQuery; + + cssWidth: any; + + @Input('tb-circular-progress') + set showProgress(showProgress: boolean) { + if (this.showProgressValue !== showProgress) { + const element = this.elementRef.nativeElement; + this.showProgressValue = showProgress; + this.spinnerRef.instance._elementRef.nativeElement.style.display = showProgress ? 'block' : 'none'; + if (showProgress) { + this.cssWidth = $(element).prop('style').width; + if (!this.cssWidth) { + $(element).css('width', ''); + const width = $(element).prop('offsetWidth'); + $(element).css('width', width + 'px'); + } + this.children = $(element).children(); + $(element).empty(); + $(element).append($(this.spinnerRef.instance._elementRef.nativeElement)); + } else { + $(element).empty(); + $(element).append(this.children); + if (this.cssWidth) { + $(element).css('width', this.cssWidth); + } else { + $(element).css('width', ''); + } + } + } + } + + spinnerRef: ComponentRef; + + constructor(private elementRef: ElementRef, + private componentFactoryResolver: ComponentFactoryResolver, + private viewContainerRef: ViewContainerRef) { + this.createCircularProgress(); + } + + createCircularProgress() { + this.elementRef.nativeElement.style.position = 'relative'; + const factory = this.componentFactoryResolver.resolveComponentFactory(MatSpinner); + this.spinnerRef = this.viewContainerRef.createComponent(factory, 0); + this.spinnerRef.instance.mode = 'indeterminate'; + this.spinnerRef.instance.diameter = 20; + const el = this.spinnerRef.instance._elementRef.nativeElement; + el.style.margin = 'auto'; + el.style.position = 'absolute'; + el.style.left = '0'; + el.style.right = '0'; + el.style.top = '0'; + el.style.bottom = '0'; + el.style.display = 'none'; + } +} diff --git a/ui-ngx/src/app/shared/components/color-input.component.html b/ui-ngx/src/app/shared/components/color-input.component.html new file mode 100644 index 0000000000..003b2dc88c --- /dev/null +++ b/ui-ngx/src/app/shared/components/color-input.component.html @@ -0,0 +1,36 @@ + + + + {{icon}} + {{label}} + +
+
+
+ + + + {{ requiredText }} + +
diff --git a/ui-ngx/src/app/shared/components/color-input.component.scss b/ui-ngx/src/app/shared/components/color-input.component.scss new file mode 100644 index 0000000000..530eb5c9f6 --- /dev/null +++ b/ui-ngx/src/app/shared/components/color-input.component.scss @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .mat-form-field { + width: 100%; + } +} + +:host ::ng-deep { + .mat-form-field-infix{ + width: 150px; + } +} diff --git a/ui-ngx/src/app/shared/components/color-input.component.ts b/ui-ngx/src/app/shared/components/color-input.component.ts new file mode 100644 index 0000000000..1c5bb6a357 --- /dev/null +++ b/ui-ngx/src/app/shared/components/color-input.component.ts @@ -0,0 +1,165 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { TranslateService } from '@ngx-translate/core'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { DialogService } from '@core/services/dialog.service'; + +@Component({ + selector: 'tb-color-input', + templateUrl: './color-input.component.html', + styleUrls: ['./color-input.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ColorInputComponent), + multi: true + } + ] +}) +export class ColorInputComponent extends PageComponent implements OnInit, ControlValueAccessor { + + @Input() + icon: string; + + @Input() + label: string; + + @Input() + requiredText: string; + + private colorClearButtonValue: boolean; + get colorClearButton(): boolean { + return this.colorClearButtonValue; + } + @Input() + set colorClearButton(value: boolean) { + const newVal = coerceBooleanProperty(value); + if (this.colorClearButtonValue !== newVal) { + this.colorClearButtonValue = newVal; + } + } + + private openOnInputValue: boolean; + get openOnInput(): boolean { + return this.openOnInputValue; + } + @Input() + set openOnInput(value: boolean) { + const newVal = coerceBooleanProperty(value); + if (this.openOnInputValue !== newVal) { + this.openOnInputValue = newVal; + } + } + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + const newVal = coerceBooleanProperty(value); + if (this.requiredValue !== newVal) { + this.requiredValue = newVal; + this.updateValidators(); + } + } + + @Input() + disabled: boolean; + + private modelValue: string; + + private propagateChange = null; + + public colorFormGroup: FormGroup; + + constructor(protected store: Store, + private dialogs: DialogService, + private translate: TranslateService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.colorFormGroup = this.fb.group({ + color: [null, this.required ? [Validators.required] : []] + }); + + this.colorFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + updateValidators() { + if (this.colorFormGroup) { + this.colorFormGroup.get('color').setValidators(this.required ? [Validators.required] : []); + this.colorFormGroup.get('color').updateValueAndValidity(); + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.colorFormGroup.disable({emitEvent: false}); + } else { + this.colorFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: string): void { + this.modelValue = value; + this.colorFormGroup.patchValue( + { color: this.modelValue }, {emitEvent: false} + ); + } + + private updateModel() { + const color: string = this.colorFormGroup.get('color').value; + if (this.modelValue !== color) { + this.modelValue = color; + this.propagateChange(this.modelValue); + } + } + + showColorPicker() { + this.dialogs.colorPicker(this.colorFormGroup.get('color').value).subscribe( + (color) => { + if (color) { + this.colorFormGroup.patchValue( + {color}, {emitEvent: true} + ); + } + } + ); + } + + clear() { + this.colorFormGroup.get('color').patchValue(null, {emitEvent: true}); + } +} diff --git a/ui-ngx/src/app/shared/components/contact.component.html b/ui-ngx/src/app/shared/components/contact.component.html new file mode 100644 index 0000000000..3e5493e17c --- /dev/null +++ b/ui-ngx/src/app/shared/components/contact.component.html @@ -0,0 +1,63 @@ + +
+ + contact.country + + + {{ country }} + + + +
+ + contact.city + + + + contact.state + + + + contact.postal-code + + + {{ 'contact.postal-code-invalid' | translate }} + + +
+ + contact.address + + + + contact.address2 + + + + contact.phone + + + + contact.email + + + {{ 'user.invalid-email-format' | translate }} + + +
diff --git a/ui-ngx/src/app/shared/components/contact.component.ts b/ui-ngx/src/app/shared/components/contact.component.ts new file mode 100644 index 0000000000..7b6d5b778d --- /dev/null +++ b/ui-ngx/src/app/shared/components/contact.component.ts @@ -0,0 +1,34 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Input } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { COUNTRIES } from '@home/models/contact.models'; + +@Component({ + selector: 'tb-contact', + templateUrl: './contact.component.html' +}) +export class ContactComponent { + + @Input() + parentForm: FormGroup; + + @Input() isEdit: boolean; + + countries = COUNTRIES; + +} diff --git a/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html b/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html new file mode 100644 index 0000000000..8993c9b3a4 --- /dev/null +++ b/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html @@ -0,0 +1,49 @@ + + + + + + + + + + + {{ translate.get('dashboard.no-dashboards-matching', {entity: searchText}) | async }} + + + + + + + + + + diff --git a/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts b/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts new file mode 100644 index 0000000000..830f91ec2e --- /dev/null +++ b/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts @@ -0,0 +1,223 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Observable, of } from 'rxjs'; +import { PageLink } from '@shared/models/page/page-link'; +import { Direction } from '@shared/models/page/sort-order'; +import { map, mergeMap, startWith, tap } from 'rxjs/operators'; +import { emptyPageData, PageData } from '@shared/models/page/page-data'; +import { DashboardInfo } from '@app/shared/models/dashboard.models'; +import { DashboardService } from '@core/http/dashboard.service'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { getCurrentAuthUser } from '@app/core/auth/auth.selectors'; +import { Authority } from '@shared/models/authority.enum'; +import { TranslateService } from '@ngx-translate/core'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; + +@Component({ + selector: 'tb-dashboard-autocomplete', + templateUrl: './dashboard-autocomplete.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DashboardAutocompleteComponent), + multi: true + }] +}) +export class DashboardAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit { + + selectDashboardFormGroup: FormGroup; + + modelValue: DashboardInfo | string | null; + + @Input() + useIdValue = true; + + @Input() + selectFirstDashboard = false; + + @Input() + placeholder: string; + + @Input() + dashboardsScope: 'customer' | 'tenant'; + + @Input() + tenantId: string; + + @Input() + customerId: string; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @ViewChild('dashboardInput', {static: true}) dashboardInput: ElementRef; + + filteredDashboards: Observable>; + + searchText = ''; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + public translate: TranslateService, + private dashboardService: DashboardService, + private fb: FormBuilder) { + this.selectDashboardFormGroup = this.fb.group({ + dashboard: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.filteredDashboards = this.selectDashboardFormGroup.get('dashboard').valueChanges + .pipe( + tap(value => { + let modelValue; + if (typeof value === 'string' || !value) { + modelValue = null; + } else { + modelValue = this.useIdValue ? value.id.id : value; + } + this.updateView(modelValue); + }), + startWith(''), + map(value => value ? (typeof value === 'string' ? value : value.name) : ''), + mergeMap(name => this.fetchDashboards(name) ) + ); + } + + ngAfterViewInit(): void { + // this.selectFirstDashboardIfNeeded(); + } + + selectFirstDashboardIfNeeded(): void { + if (this.selectFirstDashboard && !this.modelValue) { + this.getDashboards(new PageLink(1)).subscribe( + (data) => { + if (data.data.length) { + const dashboard = data.data[0]; + this.modelValue = this.useIdValue ? dashboard.id.id : dashboard; + this.selectDashboardFormGroup.get('dashboard').patchValue(dashboard, {emitEvent: false}); + this.propagateChange(this.modelValue); + } + } + ); + } + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: DashboardInfo | string | null): void { + this.searchText = ''; + if (value != null) { + if (typeof value === 'string') { + this.dashboardService.getDashboardInfo(value).subscribe( + (dashboard) => { + this.modelValue = this.useIdValue ? dashboard.id.id : dashboard; + this.selectDashboardFormGroup.get('dashboard').patchValue(dashboard, {emitEvent: true}); + } + ); + } else { + this.modelValue = this.useIdValue ? value.id.id : value; + this.selectDashboardFormGroup.get('dashboard').patchValue(value, {emitEvent: true}); + } + } else { + this.modelValue = null; + this.selectDashboardFormGroup.get('dashboard').patchValue(null, {emitEvent: true}); + this.selectFirstDashboardIfNeeded(); + } + } + + updateView(value: DashboardInfo | string | null) { + if (this.modelValue !== value) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + displayDashboardFn(dashboard?: DashboardInfo): string | undefined { + return dashboard ? dashboard.title : undefined; + } + + fetchDashboards(searchText?: string): Observable> { + this.searchText = searchText; + const pageLink = new PageLink(10, 0, searchText, { + property: 'title', + direction: Direction.ASC + }); + return this.getDashboards(pageLink).pipe( + map(pageData => { + return pageData.data; + }) + ); + } + + getDashboards(pageLink: PageLink): Observable> { + let dashboardsObservable: Observable>; + const authUser = getCurrentAuthUser(this.store); + if (this.dashboardsScope === 'customer' || authUser.authority === Authority.CUSTOMER_USER) { + if (this.customerId) { + dashboardsObservable = this.dashboardService.getCustomerDashboards(this.customerId, pageLink, + {ignoreLoading: true}); + } else { + dashboardsObservable = of(emptyPageData()); + } + } else { + if (authUser.authority === Authority.SYS_ADMIN) { + if (this.tenantId) { + dashboardsObservable = this.dashboardService.getTenantDashboardsByTenantId(this.tenantId, pageLink, + {ignoreLoading: true}); + } else { + dashboardsObservable = of(emptyPageData()); + } + } else { + dashboardsObservable = this.dashboardService.getTenantDashboards(pageLink, + {ignoreLoading: true}); + } + } + return dashboardsObservable; + } + + clear() { + this.selectDashboardFormGroup.get('dashboard').patchValue(null, {emitEvent: true}); + setTimeout(() => { + this.dashboardInput.nativeElement.blur(); + this.dashboardInput.nativeElement.focus(); + }, 0); + } + +} diff --git a/ui-ngx/src/app/shared/components/dashboard-select-panel.component.html b/ui-ngx/src/app/shared/components/dashboard-select-panel.component.html new file mode 100644 index 0000000000..625fcd6800 --- /dev/null +++ b/ui-ngx/src/app/shared/components/dashboard-select-panel.component.html @@ -0,0 +1,28 @@ + +
+ + {{'dashboard.select-dashboard' | translate}} + + + {{dashboard.title}} + + + +
diff --git a/ui-ngx/src/app/shared/components/dashboard-select-panel.component.scss b/ui-ngx/src/app/shared/components/dashboard-select-panel.component.scss new file mode 100644 index 0000000000..99c27975e6 --- /dev/null +++ b/ui-ngx/src/app/shared/components/dashboard-select-panel.component.scss @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../scss/constants"; + +:host { + min-width: 300px; + max-width: 320px; + max-height: 150px; + overflow-x: hidden; + overflow-y: auto; + background: #fff; + border-radius: 4px; + box-shadow: + 0 7px 8px -4px rgba(0, 0, 0, .2), + 0 13px 19px 2px rgba(0, 0, 0, .14), + 0 5px 24px 4px rgba(0, 0, 0, .12); + + @media (min-height: 350px) { + max-height: 250px; + } + + .mat-content { + background-color: #fff; + } +} + +:host ::ng-deep { + mat-form-field { + .mat-form-field-infix { + width: 100%; + mat-select { + .mat-select-value { + max-width: 100%; + } + } + } + } +} diff --git a/ui-ngx/src/app/shared/components/dashboard-select-panel.component.ts b/ui-ngx/src/app/shared/components/dashboard-select-panel.component.ts new file mode 100644 index 0000000000..e63a881272 --- /dev/null +++ b/ui-ngx/src/app/shared/components/dashboard-select-panel.component.ts @@ -0,0 +1,48 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, InjectionToken } from '@angular/core'; +import { Observable } from 'rxjs'; +import { DashboardInfo } from '../models/dashboard.models'; + +export const DASHBOARD_SELECT_PANEL_DATA = new InjectionToken('DashboardSelectPanelData'); + +export interface DashboardSelectPanelData { + dashboards$: Observable>; + dashboardId: string; + onDashboardSelected: (dashboardId: string) => void; +} + +@Component({ + selector: 'tb-dashboard-select-panel', + templateUrl: './dashboard-select-panel.component.html', + styleUrls: ['./dashboard-select-panel.component.scss'] +}) +export class DashboardSelectPanelComponent { + + dashboards$: Observable>; + dashboardId: string; + + constructor(@Inject(DASHBOARD_SELECT_PANEL_DATA) + private data: DashboardSelectPanelData) { + this.dashboards$ = this.data.dashboards$; + this.dashboardId = this.data.dashboardId; + } + + public dashboardSelected(dashboardId: string) { + this.data.onDashboardSelected(dashboardId); + } +} diff --git a/ui-ngx/src/app/shared/components/dashboard-select.component.html b/ui-ngx/src/app/shared/components/dashboard-select.component.html new file mode 100644 index 0000000000..d516a07918 --- /dev/null +++ b/ui-ngx/src/app/shared/components/dashboard-select.component.html @@ -0,0 +1,35 @@ + + + + {{dashboard.title}} + + +
+ +
diff --git a/ui-ngx/src/app/shared/components/dashboard-select.component.scss b/ui-ngx/src/app/shared/components/dashboard-select.component.scss new file mode 100644 index 0000000000..fb220c41e6 --- /dev/null +++ b/ui-ngx/src/app/shared/components/dashboard-select.component.scss @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + width: min-content; //for Safari + min-width: 52px; + + mat-select { + max-width: 300px; + pointer-events: all; + } + + .tb-dashboard-select { + min-height: 32px; + + span { + pointer-events: all; + cursor: pointer; + } + } +} + +:host ::ng-deep { + mat-select { + .mat-select-value { + max-width: 282px; + } + } +} diff --git a/ui-ngx/src/app/shared/components/dashboard-select.component.ts b/ui-ngx/src/app/shared/components/dashboard-select.component.ts new file mode 100644 index 0000000000..1e1bb3ce0f --- /dev/null +++ b/ui-ngx/src/app/shared/components/dashboard-select.component.ts @@ -0,0 +1,217 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Inject, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Observable, of } from 'rxjs'; +import { PageLink } from '@shared/models/page/page-link'; +import { map, share } from 'rxjs/operators'; +import { emptyPageData, PageData } from '@shared/models/page/page-data'; +import { DashboardInfo } from '@app/shared/models/dashboard.models'; +import { DashboardService } from '@core/http/dashboard.service'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { getCurrentAuthUser } from '@app/core/auth/auth.selectors'; +import { Authority } from '@shared/models/authority.enum'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { TooltipPosition } from '@angular/material/tooltip'; +import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { BreakpointObserver } from '@angular/cdk/layout'; +import { DOCUMENT } from '@angular/common'; +import { WINDOW } from '@core/services/window.service'; +import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; +import { + DASHBOARD_SELECT_PANEL_DATA, + DashboardSelectPanelComponent, + DashboardSelectPanelData +} from './dashboard-select-panel.component'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; + +// @dynamic +@Component({ + selector: 'tb-dashboard-select', + templateUrl: './dashboard-select.component.html', + styleUrls: ['./dashboard-select.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DashboardSelectComponent), + multi: true + }] +}) +export class DashboardSelectComponent implements ControlValueAccessor, OnInit { + + @Input() + dashboardsScope: 'customer' | 'tenant'; + + @Input() + customerId: string; + + @Input() + tooltipPosition: TooltipPosition = 'above'; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + dashboards$: Observable>; + + dashboardId: string | null; + + @ViewChild('dashboardSelectPanelOrigin') dashboardSelectPanelOrigin: CdkOverlayOrigin; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private dashboardService: DashboardService, + private overlay: Overlay, + private breakpointObserver: BreakpointObserver, + private viewContainerRef: ViewContainerRef, + @Inject(DOCUMENT) private document: Document, + @Inject(WINDOW) private window: Window) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + + const pageLink = new PageLink(100); + + this.dashboards$ = this.getDashboards(pageLink).pipe( + map((pageData) => pageData.data), + share() + ); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: string | null): void { + this.dashboardId = value; + } + + dashboardIdChanged() { + this.updateView(); + } + + openDashboardSelectPanel() { + if (this.disabled) { + return; + } + const panelHeight = this.breakpointObserver.isMatched('min-height: 350px') ? 250 : 150; + const panelWidth = 300; + const position = this.overlay.position(); + const config = new OverlayConfig({ + panelClass: 'tb-dashboard-select-panel', + backdropClass: 'cdk-overlay-transparent-backdrop', + hasBackdrop: true, + }); + const el = this.dashboardSelectPanelOrigin.elementRef.nativeElement; + const offset = el.getBoundingClientRect(); + const scrollTop = this.window.pageYOffset || this.document.documentElement.scrollTop || this.document.body.scrollTop || 0; + const scrollLeft = this.window.pageXOffset || this.document.documentElement.scrollLeft || this.document.body.scrollLeft || 0; + const bottomY = offset.bottom - scrollTop; + const leftX = offset.left - scrollLeft; + let originX; + let originY; + let overlayX; + let overlayY; + const wHeight = this.document.documentElement.clientHeight; + const wWidth = this.document.documentElement.clientWidth; + if (bottomY + panelHeight > wHeight) { + originY = 'top'; + overlayY = 'bottom'; + } else { + originY = 'bottom'; + overlayY = 'top'; + } + if (leftX + panelWidth > wWidth) { + originX = 'end'; + overlayX = 'end'; + } else { + originX = 'start'; + overlayX = 'start'; + } + const connectedPosition: ConnectedPosition = { + originX, + originY, + overlayX, + overlayY + }; + config.positionStrategy = position.flexibleConnectedTo(this.dashboardSelectPanelOrigin.elementRef) + .withPositions([connectedPosition]); + const overlayRef = this.overlay.create(config); + overlayRef.backdropClick().subscribe(() => { + overlayRef.dispose(); + }); + + const injector = this._createDashboardSelectPanelInjector( + overlayRef, + { + dashboards$: this.dashboards$, + dashboardId: this.dashboardId, + onDashboardSelected: (dashboardId) => { + overlayRef.dispose(); + this.dashboardId = dashboardId; + this.updateView(); + } + } + ); + overlayRef.attach(new ComponentPortal(DashboardSelectPanelComponent, this.viewContainerRef, injector)); + } + + private _createDashboardSelectPanelInjector(overlayRef: OverlayRef, data: DashboardSelectPanelData): PortalInjector { + const injectionTokens = new WeakMap([ + [DASHBOARD_SELECT_PANEL_DATA, data], + [OverlayRef, overlayRef] + ]); + return new PortalInjector(this.viewContainerRef.injector, injectionTokens); + } + + private updateView() { + this.propagateChange(this.dashboardId); + } + + private getDashboards(pageLink: PageLink): Observable> { + let dashboardsObservable: Observable>; + const authUser = getCurrentAuthUser(this.store); + if (this.dashboardsScope === 'customer' || authUser.authority === Authority.CUSTOMER_USER) { + if (this.customerId && this.customerId !== NULL_UUID) { + dashboardsObservable = this.dashboardService.getCustomerDashboards(this.customerId, pageLink, + {ignoreLoading: true}); + } else { + dashboardsObservable = of(emptyPageData()); + } + } else { + dashboardsObservable = this.dashboardService.getTenantDashboards(pageLink, {ignoreLoading: true}); + } + return dashboardsObservable; + } + +} diff --git a/ui-ngx/src/app/shared/components/dialog.component.ts b/ui-ngx/src/app/shared/components/dialog.component.ts new file mode 100644 index 0000000000..9ab803748a --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog.component.ts @@ -0,0 +1,51 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Directive, OnDestroy } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { MatDialogRef } from '@angular/material/dialog'; +import { NavigationStart, Router, RouterEvent } from '@angular/router'; +import { Subscription } from 'rxjs'; +import { filter } from 'rxjs/operators'; + +@Directive() +export abstract class DialogComponent extends PageComponent implements OnDestroy { + + routerSubscription: Subscription; + + protected constructor(protected store: Store, + protected router: Router, + protected dialogRef: MatDialogRef) { + super(store); + this.routerSubscription = this.router.events + .pipe( + filter((event: RouterEvent) => event instanceof NavigationStart), + filter(() => !!this.dialogRef) + ) + .subscribe(() => { + this.dialogRef.close(); + }); + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + if (this.routerSubscription) { + this.routerSubscription.unsubscribe(); + } + } +} diff --git a/ui-ngx/src/app/shared/components/dialog/alert-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/alert-dialog.component.html new file mode 100644 index 0000000000..cbcbb262be --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/alert-dialog.component.html @@ -0,0 +1,22 @@ + +

{{data.title}}

+
+
+ +
diff --git a/ui-ngx/src/app/shared/components/dialog/alert-dialog.component.scss b/ui-ngx/src/app/shared/components/dialog/alert-dialog.component.scss new file mode 100644 index 0000000000..ffca1bcf7e --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/alert-dialog.component.scss @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .mat-dialog-content { + padding: 0 24px 24px; + } +} diff --git a/ui-ngx/src/app/shared/components/dialog/alert-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/alert-dialog.component.ts new file mode 100644 index 0000000000..56de36cdc7 --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/alert-dialog.component.ts @@ -0,0 +1,34 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; + +export interface AlertDialogData { + title: string; + message: string; + ok: string; +} + +@Component({ + selector: 'tb-alert-dialog', + templateUrl: './alert-dialog.component.html', + styleUrls: ['./alert-dialog.component.scss'] +}) +export class AlertDialogComponent { + constructor(public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: AlertDialogData) {} +} diff --git a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html new file mode 100644 index 0000000000..4de07b1f80 --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html @@ -0,0 +1,41 @@ + +
+
+ + +
+
+ + + +
+
diff --git a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts new file mode 100644 index 0000000000..2acf97111e --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts @@ -0,0 +1,78 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@shared/components/dialog.component'; + +export interface ColorPickerDialogData { + color: string; +} + +@Component({ + selector: 'tb-color-picker-dialog', + templateUrl: './color-picker-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: ColorPickerDialogComponent}], + styleUrls: [] +}) +export class ColorPickerDialogComponent extends DialogComponent + implements OnInit, ErrorStateMatcher { + + colorPickerFormGroup: FormGroup; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: ColorPickerDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + public fb: FormBuilder) { + super(store, router, dialogRef); + } + + ngOnInit(): void { + this.colorPickerFormGroup = this.fb.group({ + color: [this.data.color, [Validators.required]] + }); + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + onColorChange(color: string) { + this.colorPickerFormGroup.get('color').setValue(color); + this.colorPickerFormGroup.markAsDirty(); + } + + cancel(): void { + this.dialogRef.close(null); + } + + select(): void { + this.submitted = true; + const color: string = this.colorPickerFormGroup.get('color').value; + this.dialogRef.close(color); + } +} diff --git a/ui-ngx/src/app/shared/components/dialog/confirm-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/confirm-dialog.component.html new file mode 100644 index 0000000000..aeb705d631 --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/confirm-dialog.component.html @@ -0,0 +1,23 @@ + +

{{data.title}}

+
+
+ + +
diff --git a/ui-ngx/src/app/shared/components/dialog/confirm-dialog.component.scss b/ui-ngx/src/app/shared/components/dialog/confirm-dialog.component.scss new file mode 100644 index 0000000000..ffca1bcf7e --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/confirm-dialog.component.scss @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .mat-dialog-content { + padding: 0 24px 24px; + } +} diff --git a/ui-ngx/src/app/shared/components/dialog/confirm-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/confirm-dialog.component.ts new file mode 100644 index 0000000000..7c5d729eb4 --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/confirm-dialog.component.ts @@ -0,0 +1,36 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; + +export interface ConfirmDialogData { + title: string; + message: string; + cancel: string; + ok: string; +} + +// @dynamic +@Component({ + selector: 'tb-confirm-dialog', + templateUrl: './confirm-dialog.component.html', + styleUrls: ['./confirm-dialog.component.scss'] +}) +export class ConfirmDialogComponent { + constructor(public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: ConfirmDialogData) {} +} diff --git a/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html new file mode 100644 index 0000000000..9d5ee6c6ef --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html @@ -0,0 +1,56 @@ + +
+ +

{{ title }}

+ + +
+ + +
+
+
+ + +
+
+
+ + + +
+
diff --git a/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts new file mode 100644 index 0000000000..0e702c3ffd --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts @@ -0,0 +1,66 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { TranslateService } from '@ngx-translate/core'; + +export interface JsonObjectEditDialogData { + jsonValue: object; + title?: string; +} + +@Component({ + selector: 'tb-object-edit-dialog', + templateUrl: './json-object-edit-dialog.component.html', + styleUrls: [] +}) +export class JsonObjectEditDialogComponent extends DialogComponent implements OnInit { + + jsonFormGroup: FormGroup; + title: string; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: JsonObjectEditDialogData, + public dialogRef: MatDialogRef, + public fb: FormBuilder, + private translate: TranslateService) { + super(store, router, dialogRef); + } + + ngOnInit(): void { + this.title = this.data.title ? this.data.title : this.translate.instant('details.edit-json'); + this.jsonFormGroup = this.fb.group({ + json: [this.data.jsonValue, []] + }); + } + + cancel(): void { + this.dialogRef.close(undefined); + } + + add(): void { + this.dialogRef.close(this.jsonFormGroup.get('json').value); + } +} diff --git a/ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.html new file mode 100644 index 0000000000..9d668b1b2d --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.html @@ -0,0 +1,78 @@ + +
+ +

{{ 'icon.select-icon' | translate }}

+ +
+ + + +
+ +
+ + +
+
+ +
+
+
+
+ + + + + + +
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.scss b/ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.scss new file mode 100644 index 0000000000..5c0520d6ab --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.scss @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .tb-material-icons-dialog { + position: relative; + } + .tb-icons-load { + top: 64px; + z-index: 3; + background: rgba(255, 255, 255, .75); + } +} + +:host ::ng-deep { + .tb-material-icons-dialog { + button.mat-icon-button.tb-select-icon-button { + width: 56px; + height: 56px; + padding: 16px; + margin: 10px; + border: solid 1px #ffa500; + border-radius: 0; + line-height: 0; + } + } +} diff --git a/ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.ts new file mode 100644 index 0000000000..000cd9ea3a --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.ts @@ -0,0 +1,106 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, Component, Inject, OnInit, QueryList, ViewChildren } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { UtilsService } from '@core/services/utils.service'; +import { FormControl } from '@angular/forms'; +import { merge, Observable, of } from 'rxjs'; +import { delay, map, mapTo, mergeMap, share, startWith, tap } from 'rxjs/operators'; + +export interface MaterialIconsDialogData { + icon: string; +} + +@Component({ + selector: 'tb-material-icons-dialog', + templateUrl: './material-icons-dialog.component.html', + providers: [], + styleUrls: ['./material-icons-dialog.component.scss'] +}) +export class MaterialIconsDialogComponent extends DialogComponent + implements OnInit, AfterViewInit { + + @ViewChildren('iconButtons') iconButtons: QueryList; + + selectedIcon: string; + icons$: Observable>; + loadingIcons$: Observable; + + showAllControl: FormControl; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: MaterialIconsDialogData, + private utils: UtilsService, + public dialogRef: MatDialogRef) { + super(store, router, dialogRef); + this.selectedIcon = data.icon; + this.showAllControl = new FormControl(false); + } + + ngOnInit(): void { + this.icons$ = this.showAllControl.valueChanges.pipe( + map((showAll) => { + return {firstTime: false, showAll}; + }), + startWith<{firstTime: boolean, showAll: boolean}>({firstTime: true, showAll: false}), + mergeMap((data) => { + if (data.showAll) { + return this.utils.getMaterialIcons().pipe(delay(100)); + } else { + const res = of(this.utils.getCommonMaterialIcons()); + return data.firstTime ? res : res.pipe(delay(50)); + } + }), + share() + ); + } + + ngAfterViewInit(): void { + this.loadingIcons$ = merge( + this.showAllControl.valueChanges.pipe( + mapTo(true), + ), + this.iconButtons.changes.pipe( + delay(100), + mapTo( false), + ) + ).pipe( + tap((loadingIcons) => { + if (loadingIcons) { + this.showAllControl.disable({emitEvent: false}); + } else { + this.showAllControl.enable({emitEvent: false}); + } + }), + share() + ); + } + + selectIcon(icon: string) { + this.dialogRef.close(icon); + } + + cancel(): void { + this.dialogRef.close(null); + } + +} diff --git a/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.html new file mode 100644 index 0000000000..365445ed02 --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.html @@ -0,0 +1,125 @@ + +
+ +

{{ 'rulenode.test-script-function' | translate }}

+ + +
+
+
+
+
+
+
+ +
+
+
+ + +
+ + +
+
+
+
+
+
+ +
+ + +
+
+
+
+
+
+
+ +
+ + +
+
+
+
+
+ +
+ + +
+
+
+
+
+
+ + + + +
+
diff --git a/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.scss b/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.scss new file mode 100644 index 0000000000..2681932b3f --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.scss @@ -0,0 +1,106 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-node-script-test-dialog { + width: 100%; + height: 100%; + + .tb-split { + box-sizing: border-box; + overflow-x: hidden; + overflow-y: auto; + } + + .ace_editor { + font-size: 14px !important; + } + + .tb-content { + padding-top: 5px; + padding-left: 5px; + border: 1px solid #c0c0c0; + } + .gutter { + background-color: #eee; + background-repeat: no-repeat; + background-position: 50%; + } + + .gutter.gutter-horizontal { + cursor: col-resize; + background-image: url("../../../../assets/split.js/grips/vertical.png"); + } + + .gutter.gutter-vertical { + cursor: row-resize; + background-image: url("../../../../assets/split.js/grips/horizontal.png"); + } + + .tb-split.tb-split-horizontal, + .gutter.gutter-horizontal { + float: left; + height: 100%; + } + + .tb-split.tb-split-vertical { + display: flex; + + .tb-split.tb-content { + height: 100%; + } + } + + div.tb-editor-area-title-panel { + position: absolute; + top: 13px; + right: 40px; + z-index: 5; + font-size: .8rem; + font-weight: 500; + + &.tb-js-function { + right: 80px; + } + + label { + padding: 4px; + color: #00acc1; + background: rgba(220, 220, 220, .35); + border-radius: 5px; + } + + .mat-button { + min-width: 32px; + min-height: 15px; + padding: 4px; + margin: 0; + font-size: .8rem; + line-height: 15px; + color: #7b7b7b; + background: rgba(220, 220, 220, .35); + } + } + + .tb-resize-container { + position: relative; + width: 100%; + height: 100%; + overflow-y: auto; + + .ace_editor { + height: 100%; + } + } +} diff --git a/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.ts new file mode 100644 index 0000000000..007a96dbad --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.ts @@ -0,0 +1,217 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + Component, + ElementRef, + HostBinding, + Inject, + OnInit, + QueryList, + SkipSelf, + ViewChild, + ViewChildren, + ViewEncapsulation +} from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { NEVER, Observable, of } from 'rxjs'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { ContentType } from '@shared/models/constants'; +import { JsonContentComponent } from '@shared/components/json-content.component'; +import { TestScriptInputParams } from '@shared/models/rule-node.models'; +import { RuleChainService } from '@core/http/rule-chain.service'; +import { mergeMap } from 'rxjs/operators'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; + +export interface NodeScriptTestDialogData { + script: string; + scriptType: string; + functionTitle: string; + functionName: string; + argNames: string[]; + msg?: any; + metadata?: {[key: string]: string}; + msgType?: string; +} + +// @dynamic +@Component({ + selector: 'tb-node-script-test-dialog', + templateUrl: './node-script-test-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: NodeScriptTestDialogComponent}], + styleUrls: ['./node-script-test-dialog.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class NodeScriptTestDialogComponent extends DialogComponent implements OnInit, AfterViewInit, ErrorStateMatcher { + + @HostBinding('style.width') width = '100%'; + @HostBinding('style.height') height = '100%'; + + @ViewChildren('topPanel') + topPanelElmRef: QueryList>; + + @ViewChildren('topLeftPanel') + topLeftPanelElmRef: QueryList>; + + @ViewChildren('topRightPanel') + topRightPanelElmRef: QueryList>; + + @ViewChildren('bottomPanel') + bottomPanelElmRef: QueryList>; + + @ViewChildren('bottomLeftPanel') + bottomLeftPanelElmRef: QueryList>; + + @ViewChildren('bottomRightPanel') + bottomRightPanelElmRef: QueryList>; + + @ViewChild('payloadContent', {static: true}) payloadContent: JsonContentComponent; + + nodeScriptTestFormGroup: FormGroup; + + functionTitle: string; + + submitted = false; + + contentTypes = ContentType; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: NodeScriptTestDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + public fb: FormBuilder, + private ruleChainService: RuleChainService) { + super(store, router, dialogRef); + this.functionTitle = this.data.functionTitle; + } + + ngOnInit(): void { + this.nodeScriptTestFormGroup = this.fb.group({ + payload: this.fb.group({ + msgType: [this.data.msgType, [Validators.required]], + msg: [js_beautify(JSON.stringify(this.data.msg), {indent_size: 4}), []], + }), + metadata: [this.data.metadata, [Validators.required]], + script: [this.data.script, []], + output: ['', []] + }); + } + + ngAfterViewInit(): void { + this.initSplitLayout(this.topPanelElmRef.first.nativeElement, + this.topLeftPanelElmRef.first.nativeElement, + this.topRightPanelElmRef.first.nativeElement, + this.bottomPanelElmRef.first.nativeElement, + this.bottomLeftPanelElmRef.first.nativeElement, + this.bottomRightPanelElmRef.first.nativeElement); + } + + private initSplitLayout(topPanel: any, + topLeftPanel: any, + topRightPanel: any, + bottomPanel: any, + bottomLeftPanel: any, + bottomRightPanel: any) { + + Split([topPanel, bottomPanel], { + sizes: [35, 65], + gutterSize: 8, + cursor: 'row-resize', + direction: 'vertical' + }); + + Split([topLeftPanel, topRightPanel], { + sizes: [50, 50], + gutterSize: 8, + cursor: 'col-resize' + }); + + Split([bottomLeftPanel, bottomRightPanel], { + sizes: [50, 50], + gutterSize: 8, + cursor: 'col-resize' + }); + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + test(): void { + this.testNodeScript().subscribe((output) => { + this.nodeScriptTestFormGroup.get('output').setValue(js_beautify(output, {indent_size: 4})); + }); + } + + private testNodeScript(): Observable { + if (this.checkInputParamErrors()) { + const inputParams: TestScriptInputParams = { + argNames: this.data.argNames, + scriptType: this.data.scriptType, + msgType: this.nodeScriptTestFormGroup.get('payload.msgType').value, + msg: this.nodeScriptTestFormGroup.get('payload.msg').value, + metadata: this.nodeScriptTestFormGroup.get('metadata').value, + script: this.nodeScriptTestFormGroup.get('script').value + }; + return this.ruleChainService.testScript(inputParams).pipe( + mergeMap((result) => { + if (result.error) { + this.store.dispatch(new ActionNotificationShow( + { + message: result.error, + type: 'error' + })); + return NEVER; + } else { + return of(result.output); + } + }) + ); + } else { + return NEVER; + } + } + + private checkInputParamErrors(): boolean { + this.payloadContent.validateOnSubmit(); + if (!this.nodeScriptTestFormGroup.get('payload').valid) { + return false; + } + return true; + } + + save(): void { + this.submitted = true; + this.testNodeScript().subscribe(() => { + this.nodeScriptTestFormGroup.get('script').markAsPristine(); + this.dialogRef.close(this.nodeScriptTestFormGroup.get('script').value); + }); + } +} diff --git a/ui-ngx/src/app/shared/components/dialog/todo-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/todo-dialog.component.html new file mode 100644 index 0000000000..80c502e2f8 --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/todo-dialog.component.html @@ -0,0 +1,24 @@ + +

Coming soon!

+
+

COMING SOON!

+
+
+ +
diff --git a/ui-ngx/src/app/shared/components/dialog/todo-dialog.component.scss b/ui-ngx/src/app/shared/components/dialog/todo-dialog.component.scss new file mode 100644 index 0000000000..07439f34ff --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/todo-dialog.component.scss @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .mat-dialog-content { + padding: 0 24px 24px; + img { + max-width: 500px; + } + } +} diff --git a/ui-ngx/src/app/shared/components/dialog/todo-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/todo-dialog.component.ts new file mode 100644 index 0000000000..ff8fbd5d0c --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/todo-dialog.component.ts @@ -0,0 +1,28 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { MatDialogRef } from '@angular/material/dialog'; + +@Component({ + selector: 'tb-todo-dialog', + templateUrl: './todo-dialog.component.html', + styleUrls: ['./todo-dialog.component.scss'] +}) +export class TodoDialogComponent { + constructor(public dialogRef: MatDialogRef) { + } +} diff --git a/ui-ngx/src/app/shared/components/directives/tb-json-to-string.directive.ts b/ui-ngx/src/app/shared/components/directives/tb-json-to-string.directive.ts new file mode 100644 index 0000000000..bcd10135c0 --- /dev/null +++ b/ui-ngx/src/app/shared/components/directives/tb-json-to-string.directive.ts @@ -0,0 +1,98 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Directive, ElementRef, forwardRef, HostListener, Renderer2, SkipSelf } from '@angular/core'; +import { + ControlValueAccessor, + FormControl, + FormGroupDirective, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + NgForm, + ValidationErrors, + Validator +} from '@angular/forms'; +import { ErrorStateMatcher } from '@angular/material/core'; + +@Directive({ + selector: '[tb-json-to-string]', + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TbJsonToStringDirective), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TbJsonToStringDirective), + multi: true, + }, + { + provide: ErrorStateMatcher, + useExisting: TbJsonToStringDirective + }] +}) + +export class TbJsonToStringDirective implements ControlValueAccessor, Validator, ErrorStateMatcher { + private propagateChange = null; + private parseError: boolean; + private data: any; + + @HostListener('input', ['$event.target.value']) input(newValue: any): void { + try { + this.data = JSON.parse(newValue); + this.parseError = false; + } catch (e) { + this.parseError = true; + } + + this.propagateChange(this.data); + } + + constructor(private render: Renderer2, + private element: ElementRef, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher) { + + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.parseError); + return originalErrorState || customErrorState; + } + + validate(c: FormControl): ValidationErrors { + return (!this.parseError) ? null : { + invalidJSON: { + valid: false + } + }; + } + + writeValue(obj: any): void { + if (obj) { + this.data = obj; + this.parseError = false; + this.render.setProperty(this.element.nativeElement, 'value', JSON.stringify(obj)); + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } +} diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html new file mode 100644 index 0000000000..fa74c6cc03 --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html @@ -0,0 +1,46 @@ + + + + + + + + + + + {{ translate.get(noEntitiesMatchingText, {entity: searchText}) | async }} + + + + + {{ entityRequiredText | translate }} + + diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.scss b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.scss new file mode 100644 index 0000000000..a4b3fb932d --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.scss @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host ::ng-deep { + .mat-form-field-infix { + width: auto; + min-width: 100px; + } +} diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts new file mode 100644 index 0000000000..27c156790b --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts @@ -0,0 +1,367 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Observable } from 'rxjs'; +import { map, mergeMap, share, tap } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; +import { BaseData } from '@shared/models/base-data'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityService } from '@core/http/entity.service'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { Authority } from '@shared/models/authority.enum'; + +@Component({ + selector: 'tb-entity-autocomplete', + templateUrl: './entity-autocomplete.component.html', + styleUrls: ['./entity-autocomplete.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntityAutocompleteComponent), + multi: true + }] +}) +export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit { + + selectEntityFormGroup: FormGroup; + + modelValue: string | null; + + entityTypeValue: EntityType | AliasEntityType; + + entitySubtypeValue: string; + + @Input() + set entityType(entityType: EntityType) { + if (this.entityTypeValue !== entityType) { + this.entityTypeValue = entityType; + this.load(); + this.reset(); + this.dirty = true; + } + } + + @Input() + set entitySubtype(entitySubtype: string) { + if (this.entitySubtypeValue !== entitySubtype) { + this.entitySubtypeValue = entitySubtype; + const currentEntity = this.getCurrentEntity(); + if (currentEntity) { + if ((currentEntity as any).type !== this.entitySubtypeValue) { + this.reset(); + this.dirty = true; + } + } + this.selectEntityFormGroup.get('entity').updateValueAndValidity(); + } + } + + @Input() + excludeEntityIds: Array; + + @Input() + labelText: string; + + @Input() + requiredText: string; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @ViewChild('entityInput', {static: true}) entityInput: ElementRef; + + entityText: string; + noEntitiesMatchingText: string; + entityRequiredText: string; + + filteredEntities: Observable>>; + + searchText = ''; + + private dirty = false; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + public translate: TranslateService, + private entityService: EntityService, + private fb: FormBuilder) { + this.selectEntityFormGroup = this.fb.group({ + entity: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.filteredEntities = this.selectEntityFormGroup.get('entity').valueChanges + .pipe( + tap(value => { + let modelValue; + if (typeof value === 'string' || !value) { + modelValue = null; + } else { + modelValue = value.id.id; + } + this.updateView(modelValue); + if (value === null) { + this.clear(); + } + }), + // startWith>(''), + map(value => value ? (typeof value === 'string' ? value : value.name) : ''), + mergeMap(name => this.fetchEntities(name) ), + share() + ); + } + + ngAfterViewInit(): void {} + + load(): void { + if (this.entityTypeValue) { + switch (this.entityTypeValue) { + case EntityType.ASSET: + this.entityText = 'asset.asset'; + this.noEntitiesMatchingText = 'asset.no-assets-matching'; + this.entityRequiredText = 'asset.asset-required'; + break; + case EntityType.DEVICE: + this.entityText = 'device.device'; + this.noEntitiesMatchingText = 'device.no-devices-matching'; + this.entityRequiredText = 'device.device-required'; + break; + case EntityType.ENTITY_VIEW: + this.entityText = 'entity-view.entity-view'; + this.noEntitiesMatchingText = 'entity-view.no-entity-views-matching'; + this.entityRequiredText = 'entity-view.entity-view-required'; + break; + case EntityType.RULE_CHAIN: + this.entityText = 'rulechain.rulechain'; + this.noEntitiesMatchingText = 'rulechain.no-rulechains-matching'; + this.entityRequiredText = 'rulechain.rulechain-required'; + break; + case EntityType.TENANT: + case AliasEntityType.CURRENT_TENANT: + this.entityText = 'tenant.tenant'; + this.noEntitiesMatchingText = 'tenant.no-tenants-matching'; + this.entityRequiredText = 'tenant.tenant-required'; + break; + case EntityType.CUSTOMER: + this.entityText = 'customer.customer'; + this.noEntitiesMatchingText = 'customer.no-customers-matching'; + this.entityRequiredText = 'customer.customer-required'; + break; + case EntityType.USER: + case AliasEntityType.CURRENT_USER: + this.entityText = 'user.user'; + this.noEntitiesMatchingText = 'user.no-users-matching'; + this.entityRequiredText = 'user.user-required'; + break; + case EntityType.DASHBOARD: + this.entityText = 'dashboard.dashboard'; + this.noEntitiesMatchingText = 'dashboard.no-dashboards-matching'; + this.entityRequiredText = 'dashboard.dashboard-required'; + break; + case EntityType.ALARM: + this.entityText = 'alarm.alarm'; + this.noEntitiesMatchingText = 'alarm.no-alarms-matching'; + this.entityRequiredText = 'alarm.alarm-required'; + break; + case AliasEntityType.CURRENT_CUSTOMER: + this.entityText = 'customer.default-customer'; + this.noEntitiesMatchingText = 'customer.no-customers-matching'; + this.entityRequiredText = 'customer.default-customer-required'; + break; + case AliasEntityType.CURRENT_USER_OWNER: + const authUser = getCurrentAuthUser(this.store); + if (authUser.authority === Authority.TENANT_ADMIN) { + this.entityText = 'tenant.tenant'; + this.noEntitiesMatchingText = 'tenant.no-tenants-matching'; + this.entityRequiredText = 'tenant.tenant-required'; + } else { + this.entityText = 'customer.customer'; + this.noEntitiesMatchingText = 'customer.no-customers-matching'; + this.entityRequiredText = 'customer.customer-required'; + } + break; + } + } + if (this.labelText && this.labelText.length) { + this.entityText = this.labelText; + } + if (this.requiredText && this.requiredText.length) { + this.entityRequiredText = this.requiredText; + } + const currentEntity = this.getCurrentEntity(); + if (currentEntity) { + const currentEntityType = currentEntity.id.entityType; + if (this.entityTypeValue && currentEntityType !== this.entityTypeValue) { + this.reset(); + } + } + } + + getCurrentEntity(): BaseData | null { + const currentEntity = this.selectEntityFormGroup.get('entity').value; + if (currentEntity && typeof currentEntity !== 'string') { + return currentEntity as BaseData; + } else { + return null; + } + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.selectEntityFormGroup.disable({emitEvent: false}); + } else { + this.selectEntityFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: string | EntityId | null): void { + this.searchText = ''; + if (value != null) { + if (typeof value === 'string') { + const targetEntityType = this.checkEntityType(this.entityTypeValue); + this.entityService.getEntity(targetEntityType, value, {ignoreLoading: true, ignoreErrors: true}).subscribe( + (entity) => { + this.modelValue = entity.id.id; + this.selectEntityFormGroup.get('entity').patchValue(entity, {emitEvent: false}); + }, + () => { + this.modelValue = null; + this.selectEntityFormGroup.get('entity').patchValue('', {emitEvent: false}); + if (value !== null) { + this.propagateChange(this.modelValue); + } + } + ); + } else { + const targetEntityType = this.checkEntityType(value.entityType); + this.entityService.getEntity(targetEntityType, value.id, {ignoreLoading: true, ignoreErrors: true}).subscribe( + (entity) => { + this.modelValue = entity.id.id; + this.selectEntityFormGroup.get('entity').patchValue(entity, {emitEvent: false}); + }, + () => { + this.modelValue = null; + this.selectEntityFormGroup.get('entity').patchValue('', {emitEvent: false}); + if (value !== null) { + this.propagateChange(this.modelValue); + } + } + ); + } + } else { + this.modelValue = null; + this.selectEntityFormGroup.get('entity').patchValue('', {emitEvent: false}); + } + this.dirty = true; + } + + onFocus() { + if (this.dirty) { + this.selectEntityFormGroup.get('entity').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + reset() { + this.selectEntityFormGroup.get('entity').patchValue('', {emitEvent: false}); + } + + updateView(value: string | null) { + if (this.modelValue !== value) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + displayEntityFn(entity?: BaseData): string | undefined { + return entity ? entity.name : undefined; + } + + fetchEntities(searchText?: string): Observable>> { + this.searchText = searchText; + const targetEntityType = this.checkEntityType(this.entityTypeValue); + return this.entityService.getEntitiesByNameFilter(targetEntityType, searchText, + 50, this.entitySubtypeValue, {ignoreLoading: true}).pipe( + map((data) => { + if (data) { + if (this.excludeEntityIds && this.excludeEntityIds.length) { + const entities: Array> = []; + data.forEach((entity) => { + if (this.excludeEntityIds.indexOf(entity.id.id) === -1) { + entities.push(entity); + } + }); + return entities; + } else { + return data; + } + } else { + return []; + } + } + )); + } + + clear() { + this.selectEntityFormGroup.get('entity').patchValue('', {emitEvent: true}); + setTimeout(() => { + this.entityInput.nativeElement.blur(); + this.entityInput.nativeElement.focus(); + }, 0); + } + + checkEntityType(entityType: EntityType | AliasEntityType): EntityType { + if (entityType === AliasEntityType.CURRENT_CUSTOMER) { + return EntityType.CUSTOMER; + } else if (entityType === AliasEntityType.CURRENT_TENANT) { + return EntityType.TENANT; + } else if (entityType === AliasEntityType.CURRENT_USER) { + return EntityType.USER; + } else if (entityType === AliasEntityType.CURRENT_USER_OWNER) { + const authUser = getCurrentAuthUser(this.store); + if (authUser.authority === Authority.TENANT_ADMIN) { + return EntityType.TENANT; + } else { + return EntityType.CUSTOMER; + } + } + return entityType; + } +} diff --git a/ui-ngx/src/app/shared/components/entity/entity-gateway-select.component.html b/ui-ngx/src/app/shared/components/entity/entity-gateway-select.component.html new file mode 100644 index 0000000000..4d0ea838a4 --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-gateway-select.component.html @@ -0,0 +1,56 @@ + + + + + + + + + +
+
+ gateway.no-gateway-found +
+ + + gateway.no-gateway-matching + + + + gateway.create-new-gateway + +
+
+
+ + {{ 'gateway.gateway-name-required' | translate }} + +
diff --git a/ui-ngx/src/app/shared/components/entity/entity-gateway-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-gateway-select.component.ts new file mode 100644 index 0000000000..4dcfdc7540 --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-gateway-select.component.ts @@ -0,0 +1,249 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, EventEmitter, forwardRef, Input, OnInit, Output, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { ENTER } from '@angular/cdk/keycodes'; +import { Observable, of } from 'rxjs'; +import { filter, map, mergeMap, reduce, share, switchMap, tap } from 'rxjs/operators'; +import { EntityService } from '@core/http/entity.service'; +import { EntityType } from '@shared/models/entity-type.models'; +import { Device } from '@shared/models/device.models'; +import { DialogService } from '@core/services/dialog.service'; +import { TranslateService } from '@ngx-translate/core'; +import { DeviceService } from '@core/http/device.service'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { Authority } from '@shared/models/authority.enum'; + +@Component({ + selector: 'tb-entity-gateway-select', + templateUrl: './entity-gateway-select.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntityGatewaySelectComponent), + multi: true + }] +}) + +export class EntityGatewaySelectComponent implements ControlValueAccessor, OnInit { + get required(): boolean { + return this.requiredValue; + } + + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + set newGatewayType(value: string){ + this.gatewayType = value; + } + + @Input() + deviceName: string; + + @Input() + isStateForm: boolean; + + @Output() + private gatewayNameExist = new EventEmitter(); + + constructor(private store: Store, + private entityService: EntityService, + private dialogService: DialogService, + private deviceService: DeviceService, + private translate: TranslateService, + private fb: FormBuilder) { + } + + private gatewayType = 'Gateway'; + private dirty: boolean; + private requiredValue: boolean; + private gatewayList: Array; + + searchText = ''; + filteredGateways: Observable>; + selectDeviceGatewayFormGroup: FormGroup; + modelValue: string | null; + + @ViewChild('deviceGatewayInput', {static: true}) deviceGatewayInput: ElementRef; + private propagateChange = (v: any) => { }; + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.selectDeviceGatewayFormGroup = this.fb.group({ + gateway: this.fb.control({value: null, disabled: this.isStateForm}) + }); + this.loadGatewayList(); + this.filteredGateways = this.selectDeviceGatewayFormGroup.get('gateway').valueChanges + .pipe( + tap(value => { + let modelValue; + if (typeof value === 'string' || !value) { + modelValue = null; + } else { + modelValue = value.id.id; + } + this.updateView(modelValue); + if (value === null) { + this.clear(); + } + }), + map(value => value ? (typeof value === 'string' ? value : value.name) : ''), + mergeMap(name => this.fetchGateway(name) ), + share() + ); + } + + fetchGateway(searchText?: string): Observable> { + this.searchText = searchText; + let result = []; + if (searchText && searchText.length) { + result = this.gatewayList.filter((gateway) => gateway.name.toLowerCase().includes(searchText.toLowerCase())); + } else { + result = this.gatewayList; + } + return of(result); + } + + onFocus() { + if (this.dirty) { + this.selectDeviceGatewayFormGroup.get('gateway').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + displayGatewayFn(gateway?: Device): string | undefined { + return gateway ? gateway.name : undefined; + } + + setDisabledState(isDisabled: boolean): void { + } + + writeValue(value: string | null): void { + if(value === null){ + this.searchText = ''; + this.selectDeviceGatewayFormGroup.get('gateway').patchValue('', {emitEvent: false}); + this.dirty = true; + } + } + + clear(value: string = '', hideList?: boolean) { + this.searchText = value; + this.selectDeviceGatewayFormGroup.get('gateway').patchValue(value, {emitEvent: true}); + if(!hideList) { + setTimeout(() => { + this.deviceGatewayInput.nativeElement.blur(); + this.deviceGatewayInput.nativeElement.focus(); + }, 0); + } + } + + textIsNotEmpty(text: string): boolean { + return !!text && text.length > 0; + } + + gatewayNameEnter($event: KeyboardEvent) { + if ($event.keyCode === ENTER) { + if (!this.modelValue) { + this.createGateway($event, this.searchText); + } + } + } + + createGateway($event: Event, gatewayName: string) { + $event.preventDefault(); + $event.stopPropagation(); + const title = this.translate.instant('gateway.create-new-gateway'); + const content = this.translate.instant('gateway.create-new-gateway-text', {gatewayName}); + this.dialogService.confirm(title, content, null, null, true).subscribe(value => { + if(value){ + this.createDeviceGateway(gatewayName); + } else { + this.clear('', true); + } + }); + } + + private createDeviceGateway(gatewayName: string){ + this.deviceService.findByName(gatewayName, {ignoreErrors: true}).subscribe(value => { + this.gatewayNameExist.emit(gatewayName) + }, () => { + const newGateway: Device = { + name: gatewayName, + label: null, + type: this.gatewayType, + additionalInfo: { + gateway: true + } + }; + + this.deviceService.saveDevice(newGateway).subscribe( + (device) => { + this.searchText = ''; + this.gatewayList.push(device); + this.selectDeviceGatewayFormGroup.get('gateway').patchValue(device, {emitEvent: true}); + } + ); + }) + } + + private loadGatewayList(): void { + let listObservable: Observable; + if (getCurrentAuthUser(this.store).authority === Authority.SYS_ADMIN) { + listObservable = of([]); + } else { + const entityNameFilter = this.isStateForm && this.deviceName ? this.deviceName : ''; + listObservable = this.entityService.getEntitiesByNameFilter(EntityType.DEVICE, entityNameFilter, + -1, '', {ignoreLoading: true}); + } + listObservable.pipe( + map((devices) => devices ? devices.filter((device) => + (device as Device)?.additionalInfo?.gateway): []), + ).subscribe((devices) => { + this.gatewayList = devices; + if (!this.searchText) { + if (this.gatewayList.length) { + let foundGateway: Device = null; + if (this.deviceName) { + foundGateway = this.gatewayList.find((gateway) => gateway.name === this.deviceName); + } + if (!foundGateway) { + foundGateway = this.gatewayList[0]; + } + if (foundGateway) { + this.selectDeviceGatewayFormGroup.get('gateway').patchValue(foundGateway, {emitEvent: true}); + } + } + } + }) + } + + private updateView(modelValue: any) { + this.propagateChange(modelValue); + } +} diff --git a/ui-ngx/src/app/shared/components/entity/entity-keys-list.component.html b/ui-ngx/src/app/shared/components/entity/entity-keys-list.component.html new file mode 100644 index 0000000000..abfff93acb --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-keys-list.component.html @@ -0,0 +1,49 @@ + + + + + {{key}} + close + + + + + + + + + diff --git a/ui-ngx/src/app/shared/components/entity/entity-keys-list.component.scss b/ui-ngx/src/app/shared/components/entity/entity-keys-list.component.scss new file mode 100644 index 0000000000..4f2bb50c1d --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-keys-list.component.scss @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + +} + +:host ::ng-deep { + .mat-form-field-flex { + padding-top: 0; + .mat-form-field-infix { + border-top: 0; + } + } +} diff --git a/ui-ngx/src/app/shared/components/entity/entity-keys-list.component.ts b/ui-ngx/src/app/shared/components/entity/entity-keys-list.component.ts new file mode 100644 index 0000000000..21d6a6572d --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-keys-list.component.ts @@ -0,0 +1,206 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; +import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Observable, of } from 'rxjs'; +import { map, mergeMap, share } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityService } from '@core/http/entity.service'; +import { MatAutocomplete, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { MatChipInputEvent, MatChipList } from '@angular/material/chips'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { isEqual } from '@core/utils'; + +@Component({ + selector: 'tb-entity-keys-list', + templateUrl: './entity-keys-list.component.html', + styleUrls: ['./entity-keys-list.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntityKeysListComponent), + multi: true + } + ] +}) +export class EntityKeysListComponent implements ControlValueAccessor, OnInit, AfterViewInit { + + keysListFormGroup: FormGroup; + + modelValue: Array | null; + + entityIdValue: EntityId; + + @Input() + set entityId(entityId: EntityId) { + if (!isEqual(this.entityIdValue, entityId)) { + this.entityIdValue = entityId; + this.dirty = true; + } + } + + @Input() + keysText: string; + + @Input() + dataKeyType: DataKeyType; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @ViewChild('keyInput') keyInput: ElementRef; + @ViewChild('keyAutocomplete') matAutocomplete: MatAutocomplete; + @ViewChild('chipList') chipList: MatChipList; + + filteredKeys: Observable>; + + separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; + + searchText = ''; + + private dirty = false; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + public translate: TranslateService, + private entityService: EntityService, + private fb: FormBuilder) { + this.keysListFormGroup = this.fb.group({ + key: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.filteredKeys = this.keysListFormGroup.get('key').valueChanges + .pipe( + map((value) => value ? value : ''), + mergeMap(name => this.fetchKeys(name) ), + share() + ); + } + + ngAfterViewInit(): void {} + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.keysListFormGroup.disable({emitEvent: false}); + } else { + this.keysListFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: Array | null): void { + this.searchText = ''; + if (value != null) { + this.modelValue = [...value]; + } else { + this.modelValue = []; + } + } + + onFocus() { + if (this.dirty) { + this.keysListFormGroup.get('key').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + addKey(key: string): void { + if (!this.modelValue || this.modelValue.indexOf(key) === -1) { + if (!this.modelValue) { + this.modelValue = []; + } + this.modelValue.push(key); + if (this.required) { + this.chipList.errorState = false; + } + } + this.propagateChange(this.modelValue); + } + + add(event: MatChipInputEvent): void { + if (!this.matAutocomplete.isOpen) { + const value = event.value; + if ((value || '').trim()) { + this.addKey(value.trim()); + } + this.clear(''); + } + } + + remove(key: string) { + const index = this.modelValue.indexOf(key); + if (index >= 0) { + this.modelValue.splice(index, 1); + if (!this.modelValue.length) { + if (this.required) { + this.chipList.errorState = true; + } + } + this.propagateChange(this.modelValue.length ? this.modelValue : null); + } + } + + selected(event: MatAutocompleteSelectedEvent): void { + this.addKey(event.option.viewValue); + this.clear(''); + } + + displayKeyFn(key?: string): string | undefined { + return key ? key : undefined; + } + + fetchKeys(searchText?: string): Observable> { + this.searchText = searchText; + return this.entityIdValue ? this.entityService.getEntityKeys(this.entityIdValue, searchText, + this.dataKeyType, {ignoreLoading: true}).pipe( + map((data) => data ? data : [])) : of([]); + } + + clear(value: string = '') { + this.keyInput.nativeElement.value = value; + this.keysListFormGroup.get('key').patchValue(null, {emitEvent: true}); + setTimeout(() => { + this.keyInput.nativeElement.blur(); + this.keyInput.nativeElement.focus(); + }, 0); + } + +} diff --git a/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html new file mode 100644 index 0000000000..04adcc134d --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html @@ -0,0 +1,36 @@ + +
+ + + + +
diff --git a/ui-ngx/src/app/shared/components/entity/entity-list-select.component.scss b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.scss new file mode 100644 index 0000000000..9aa6c98ee9 --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.scss @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { +} + +:host ::ng-deep { + tb-entity-list { + &.tb-not-empty { + .mat-form-field-flex { + padding-top: 0; + } + } + } +} diff --git a/ui-ngx/src/app/shared/components/entity/entity-list-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.ts new file mode 100644 index 0000000000..94c52bbcb1 --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.ts @@ -0,0 +1,170 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; +import { EntityService } from '@core/http/entity.service'; +import { EntityId } from '@shared/models/id/entity-id'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; + +interface EntityListSelectModel { + entityType: EntityType | AliasEntityType; + ids: Array; +} + +@Component({ + selector: 'tb-entity-list-select', + templateUrl: './entity-list-select.component.html', + styleUrls: ['./entity-list-select.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntityListSelectComponent), + multi: true + }] +}) + +export class EntityListSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit { + + entityListSelectFormGroup: FormGroup; + + modelValue: EntityListSelectModel = {entityType: null, ids: []}; + + @Input() + allowedEntityTypes: Array; + + @Input() + useAliasEntityTypes: boolean; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + displayEntityTypeSelect: boolean; + + private readonly defaultEntityType: EntityType | AliasEntityType = null; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private entityService: EntityService, + public translate: TranslateService, + private fb: FormBuilder) { + + const entityTypes = this.entityService.prepareAllowedEntityTypesList(this.allowedEntityTypes, + this.useAliasEntityTypes); + if (entityTypes.length === 1) { + this.displayEntityTypeSelect = false; + this.defaultEntityType = entityTypes[0]; + } else { + this.displayEntityTypeSelect = true; + } + + this.entityListSelectFormGroup = this.fb.group({ + entityType: [this.defaultEntityType], + entityIds: [[]] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.entityListSelectFormGroup.get('entityType').valueChanges.subscribe( + (value) => { + this.updateView(value, this.modelValue.ids); + } + ); + this.entityListSelectFormGroup.get('entityIds').valueChanges.subscribe( + (values) => { + this.updateView(this.modelValue.entityType, values); + } + ); + } + + ngAfterViewInit(): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.entityListSelectFormGroup.disable(); + } else { + this.entityListSelectFormGroup.enable(); + } + } + + writeValue(value: Array | null): void { + if (value != null && value.length > 0) { + const id = value[0]; + this.modelValue = { + entityType: id.entityType, + ids: value.map(val => val.id) + }; + } else { + this.modelValue = { + entityType: this.defaultEntityType, + ids: [] + }; + } + this.entityListSelectFormGroup.get('entityType').patchValue(this.modelValue.entityType, {emitEvent: true}); + this.entityListSelectFormGroup.get('entityIds').patchValue([...this.modelValue.ids], {emitEvent: true}); + } + + updateView(entityType: EntityType | AliasEntityType | null, entityIds: Array | null) { + if (this.modelValue.entityType !== entityType || + !this.compareIds(this.modelValue.ids, entityIds)) { + this.modelValue = { + entityType, + ids: this.modelValue.entityType !== entityType || !entityIds ? [] : [...entityIds] + }; + this.propagateChange(this.toEntityIds(this.modelValue)); + } + } + + compareIds(ids1: Array | null, ids2: Array | null): boolean { + if (ids1 !== null && ids2 !== null) { + return JSON.stringify(ids1) === JSON.stringify(ids2); + } else { + return ids1 === ids2; + } + } + + toEntityIds(modelValue: EntityListSelectModel): Array { + if (modelValue !== null && modelValue.entityType && modelValue.ids && modelValue.ids.length > 0) { + const entityType = modelValue.entityType; + return modelValue.ids.map(id => ({entityType, id})); + } else { + return null; + } + } + +} diff --git a/ui-ngx/src/app/shared/components/entity/entity-list.component.html b/ui-ngx/src/app/shared/components/entity/entity-list.component.html new file mode 100644 index 0000000000..308ccdfc3c --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-list.component.html @@ -0,0 +1,54 @@ + + + + + {{entity.name}} + close + + + + + + + + + + {{ translate.get('entity.no-entities-matching', {entity: searchText}) | async }} + + + + + {{ 'entity.entity-list-empty' | translate }} + + diff --git a/ui-ngx/src/app/shared/components/entity/entity-list.component.scss b/ui-ngx/src/app/shared/components/entity/entity-list.component.scss new file mode 100644 index 0000000000..0557349be7 --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-list.component.scss @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host ::ng-deep { + .mat-form-field { + .mat-form-field-infix { + border-top: none; + } + } +} diff --git a/ui-ngx/src/app/shared/components/entity/entity-list.component.ts b/ui-ngx/src/app/shared/components/entity/entity-list.component.ts new file mode 100644 index 0000000000..5595708b96 --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-list.component.ts @@ -0,0 +1,240 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + Component, + ElementRef, + forwardRef, + Input, + OnChanges, + OnInit, + SimpleChanges, + ViewChild +} from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Observable } from 'rxjs'; +import { filter, map, mergeMap, share, tap } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { EntityType } from '@shared/models/entity-type.models'; +import { BaseData } from '@shared/models/base-data'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityService } from '@core/http/entity.service'; +import { MatAutocomplete } from '@angular/material/autocomplete'; +import { MatChipList } from '@angular/material/chips'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; + +@Component({ + selector: 'tb-entity-list', + templateUrl: './entity-list.component.html', + styleUrls: ['./entity-list.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntityListComponent), + multi: true + } + ] +}) +export class EntityListComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnChanges { + + entityListFormGroup: FormGroup; + + modelValue: Array | null; + + @Input() + entityType: EntityType; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + const newVal = coerceBooleanProperty(value); + if (this.requiredValue !== newVal) { + this.requiredValue = newVal; + this.updateValidators(); + } + } + + @Input() + disabled: boolean; + + @ViewChild('entityInput') entityInput: ElementRef; + @ViewChild('entityAutocomplete') matAutocomplete: MatAutocomplete; + @ViewChild('chipList', {static: true}) chipList: MatChipList; + + entities: Array> = []; + filteredEntities: Observable>>; + + searchText = ''; + + private dirty = false; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + public translate: TranslateService, + private entityService: EntityService, + private fb: FormBuilder) { + this.entityListFormGroup = this.fb.group({ + entities: [this.entities, this.required ? [Validators.required] : []], + entity: [null] + }); + } + + updateValidators() { + this.entityListFormGroup.get('entities').setValidators(this.required ? [Validators.required] : []); + this.entityListFormGroup.get('entities').updateValueAndValidity(); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.filteredEntities = this.entityListFormGroup.get('entity').valueChanges + .pipe( + // startWith>(''), + tap((value) => { + if (value && typeof value !== 'string') { + this.add(value); + } else if (value === null) { + this.clear(this.entityInput.nativeElement.value); + } + }), + filter((value) => typeof value === 'string'), + map((value) => value ? (typeof value === 'string' ? value : value.name) : ''), + mergeMap(name => this.fetchEntities(name) ), + share() + ); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'entityType') { + this.reset(); + } + } + } + } + + ngAfterViewInit(): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.entityListFormGroup.disable({emitEvent: false}); + } else { + this.entityListFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: Array | null): void { + this.searchText = ''; + if (value != null && value.length > 0) { + this.modelValue = [...value]; + this.entityService.getEntities(this.entityType, value).subscribe( + (entities) => { + this.entities = entities; + this.entityListFormGroup.get('entities').setValue(this.entities); + } + ); + } else { + this.entities = []; + this.entityListFormGroup.get('entities').setValue(this.entities); + this.modelValue = null; + } + this.dirty = true; + } + + reset() { + this.entities = []; + this.entityListFormGroup.get('entities').setValue(this.entities); + this.modelValue = null; + if (this.entityInput) { + this.entityInput.nativeElement.value = ''; + } + this.entityListFormGroup.get('entity').patchValue('', {emitEvent: false}); + this.propagateChange(this.modelValue); + this.dirty = true; + } + + add(entity: BaseData): void { + if (!this.modelValue || this.modelValue.indexOf(entity.id.id) === -1) { + if (!this.modelValue) { + this.modelValue = []; + } + this.modelValue.push(entity.id.id); + this.entities.push(entity); + this.entityListFormGroup.get('entities').setValue(this.entities); + } + this.propagateChange(this.modelValue); + this.clear(); + } + + remove(entity: BaseData) { + let index = this.entities.indexOf(entity); + if (index >= 0) { + this.entities.splice(index, 1); + this.entityListFormGroup.get('entities').setValue(this.entities); + index = this.modelValue.indexOf(entity.id.id); + this.modelValue.splice(index, 1); + if (!this.modelValue.length) { + this.modelValue = null; + } + this.propagateChange(this.modelValue); + this.clear(); + } + } + + displayEntityFn(entity?: BaseData): string | undefined { + return entity ? entity.name : undefined; + } + + fetchEntities(searchText?: string): Observable>> { + this.searchText = searchText; + return this.entityService.getEntitiesByNameFilter(this.entityType, searchText, + 50, '', {ignoreLoading: true}).pipe( + map((data) => data ? data : [])); + } + + onFocus() { + if (this.dirty) { + this.entityListFormGroup.get('entity').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + clear(value: string = '') { + this.entityInput.nativeElement.value = value; + this.entityListFormGroup.get('entity').patchValue(value, {emitEvent: true}); + setTimeout(() => { + this.entityInput.nativeElement.blur(); + this.entityInput.nativeElement.focus(); + }, 0); + } + +} diff --git a/ui-ngx/src/app/shared/components/entity/entity-select.component.html b/ui-ngx/src/app/shared/components/entity/entity-select.component.html new file mode 100644 index 0000000000..957633cd7f --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-select.component.html @@ -0,0 +1,37 @@ + +
+ + + + +
diff --git a/ui-ngx/src/app/shared/components/entity/entity-select.component.scss b/ui-ngx/src/app/shared/components/entity/entity-select.component.scss new file mode 100644 index 0000000000..655a7ced9b --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-select.component.scss @@ -0,0 +1,17 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { +} diff --git a/ui-ngx/src/app/shared/components/entity/entity-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-select.component.ts new file mode 100644 index 0000000000..0fc3d95086 --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-select.component.ts @@ -0,0 +1,158 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; +import { EntityService } from '@core/http/entity.service'; +import { EntityId } from '@shared/models/id/entity-id'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; + +@Component({ + selector: 'tb-entity-select', + templateUrl: './entity-select.component.html', + styleUrls: ['./entity-select.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntitySelectComponent), + multi: true + }] +}) +export class EntitySelectComponent implements ControlValueAccessor, OnInit, AfterViewInit { + + entitySelectFormGroup: FormGroup; + + modelValue: EntityId = {entityType: null, id: null}; + + @Input() + allowedEntityTypes: Array; + + @Input() + useAliasEntityTypes: boolean; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + displayEntityTypeSelect: boolean; + + AliasEntityType = AliasEntityType; + + private readonly defaultEntityType: EntityType | AliasEntityType = null; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private entityService: EntityService, + public translate: TranslateService, + private fb: FormBuilder) { + + const entityTypes = this.entityService.prepareAllowedEntityTypesList(this.allowedEntityTypes, + this.useAliasEntityTypes); + if (entityTypes.length === 1) { + this.displayEntityTypeSelect = false; + this.defaultEntityType = entityTypes[0]; + } else { + this.displayEntityTypeSelect = true; + } + + this.entitySelectFormGroup = this.fb.group({ + entityType: [this.defaultEntityType], + entityId: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.entitySelectFormGroup.get('entityType').valueChanges.subscribe( + (value) => { + this.updateView(value, this.modelValue.id); + } + ); + this.entitySelectFormGroup.get('entityId').valueChanges.subscribe( + (value) => { + const id = value ? (typeof value === 'string' ? value : value.id) : null; + this.updateView(this.modelValue.entityType, id); + } + ); + } + + ngAfterViewInit(): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.entitySelectFormGroup.disable(); + } else { + this.entitySelectFormGroup.enable(); + } + } + + writeValue(value: EntityId | null): void { + if (value != null) { + this.modelValue = value; + this.entitySelectFormGroup.get('entityType').patchValue(value.entityType, {emitEvent: true}); + this.entitySelectFormGroup.get('entityId').patchValue(value, {emitEvent: true}); + } else { + this.modelValue = { + entityType: this.defaultEntityType, + id: null + }; + this.entitySelectFormGroup.get('entityType').patchValue(this.defaultEntityType, {emitEvent: true}); + this.entitySelectFormGroup.get('entityId').patchValue(null, {emitEvent: true}); + } + } + + updateView(entityType: EntityType | AliasEntityType | null, entityId: string | null) { + if (this.modelValue.entityType !== entityType || this.modelValue.id !== entityId) { + this.modelValue = { + entityType, + id: this.modelValue.entityType !== entityType ? null : entityId + }; + + if (this.modelValue.entityType === AliasEntityType.CURRENT_TENANT + || this.modelValue.entityType === AliasEntityType.CURRENT_USER + || this.modelValue.entityType === AliasEntityType.CURRENT_USER_OWNER) { + this.modelValue.id = NULL_UUID; + } + + if (this.modelValue.entityType && this.modelValue.id) { + this.propagateChange(this.modelValue); + } else { + this.propagateChange(null); + } + } + } +} diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.html b/ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.html new file mode 100644 index 0000000000..d683213f54 --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.html @@ -0,0 +1,43 @@ + + + {{ entitySubtypeText | translate }} + + + + + + + + + {{ entitySubtypeRequiredText | translate }} + + diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.ts new file mode 100644 index 0000000000..59d7920106 --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.ts @@ -0,0 +1,230 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Observable, Subscription, throwError } from 'rxjs'; +import { map, mergeMap, publishReplay, refCount, tap } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { DeviceService } from '@core/http/device.service'; +import { EntitySubtype, EntityType } from '@app/shared/models/entity-type.models'; +import { BroadcastService } from '@app/core/services/broadcast.service'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { AssetService } from '@core/http/asset.service'; +import { EntityViewService } from '@core/http/entity-view.service'; + +@Component({ + selector: 'tb-entity-subtype-autocomplete', + templateUrl: './entity-subtype-autocomplete.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntitySubTypeAutocompleteComponent), + multi: true + }] +}) +export class EntitySubTypeAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy { + + subTypeFormGroup: FormGroup; + + modelValue: string | null; + + @Input() + entityType: EntityType; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @ViewChild('subTypeInput', {static: true}) subTypeInput: ElementRef; + + selectEntitySubtypeText: string; + entitySubtypeText: string; + entitySubtypeRequiredText: string; + + filteredSubTypes: Observable>; + + subTypes: Observable>; + + private broadcastSubscription: Subscription; + + searchText = ''; + + private dirty = false; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private broadcast: BroadcastService, + public translate: TranslateService, + private deviceService: DeviceService, + private assetService: AssetService, + private entityViewService: EntityViewService, + private fb: FormBuilder) { + this.subTypeFormGroup = this.fb.group({ + subType: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + + switch (this.entityType) { + case EntityType.ASSET: + this.selectEntitySubtypeText = 'asset.select-asset-type'; + this.entitySubtypeText = 'asset.asset-type'; + this.entitySubtypeRequiredText = 'asset.asset-type-required'; + this.broadcastSubscription = this.broadcast.on('assetSaved', () => { + this.subTypes = null; + }); + break; + case EntityType.DEVICE: + this.selectEntitySubtypeText = 'device.select-device-type'; + this.entitySubtypeText = 'device.device-type'; + this.entitySubtypeRequiredText = 'device.device-type-required'; + this.broadcastSubscription = this.broadcast.on('deviceSaved', () => { + this.subTypes = null; + }); + break; + case EntityType.ENTITY_VIEW: + this.selectEntitySubtypeText = 'entity-view.select-entity-view-type'; + this.entitySubtypeText = 'entity-view.entity-view-type'; + this.entitySubtypeRequiredText = 'entity-view.entity-view-type-required'; + this.broadcastSubscription = this.broadcast.on('entityViewSaved', () => { + this.subTypes = null; + }); + break; + } + + this.filteredSubTypes = this.subTypeFormGroup.get('subType').valueChanges + .pipe( + tap(value => { + this.updateView(value); + }), + // startWith(''), + map(value => value ? value : ''), + mergeMap(type => this.fetchSubTypes(type) ) + ); + } + + ngAfterViewInit(): void { + } + + ngOnDestroy(): void { + if (this.broadcastSubscription) { + this.broadcastSubscription.unsubscribe(); + } + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.subTypeFormGroup.disable({emitEvent: false}); + } else { + this.subTypeFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: string | null): void { + this.searchText = ''; + this.modelValue = value; + this.subTypeFormGroup.get('subType').patchValue(value, {emitEvent: false}); + this.dirty = true; + } + + onFocus() { + if (this.dirty) { + this.subTypeFormGroup.get('subType').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + updateView(value: string | null) { + if (this.modelValue !== value) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + displaySubTypeFn(subType?: string): string | undefined { + return subType ? subType : undefined; + } + + fetchSubTypes(searchText?: string, strictMatch: boolean = false): Observable> { + this.searchText = searchText; + return this.getSubTypes().pipe( + map(subTypes => subTypes.filter( subType => { + if (strictMatch) { + return searchText ? subType === searchText : false; + } else { + return searchText ? subType.toUpperCase().startsWith(searchText.toUpperCase()) : true; + } + })) + ); + } + + getSubTypes(): Observable> { + if (!this.subTypes) { + let subTypesObservable: Observable>; + switch (this.entityType) { + case EntityType.ASSET: + subTypesObservable = this.assetService.getAssetTypes({ignoreLoading: true}); + break; + case EntityType.DEVICE: + subTypesObservable = this.deviceService.getDeviceTypes({ignoreLoading: true}); + break; + case EntityType.ENTITY_VIEW: + subTypesObservable = this.entityViewService.getEntityViewTypes({ignoreLoading: true}); + break; + } + if (subTypesObservable) { + this.subTypes = subTypesObservable.pipe( + map(subTypes => subTypes.map(subType => subType.type)), + publishReplay(1), + refCount() + ); + } else { + return throwError(null); + } + } + return this.subTypes; + } + + clear() { + this.subTypeFormGroup.get('subType').patchValue(null, {emitEvent: true}); + setTimeout(() => { + this.subTypeInput.nativeElement.blur(); + this.subTypeInput.nativeElement.focus(); + }, 0); + } + +} diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html b/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html new file mode 100644 index 0000000000..6bbe3058a0 --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html @@ -0,0 +1,57 @@ + + + + + {{entitySubtype}} + close + + + + + + + + + + {{ translate.get(noSubtypesMathingText, {entitySubtype: searchText}) | async }} + + + + + {{ subtypeListEmptyText | translate }} + + diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.scss b/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.scss new file mode 100644 index 0000000000..0557349be7 --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.scss @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host ::ng-deep { + .mat-form-field { + .mat-form-field-infix { + border-top: none; + } + } +} diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts b/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts new file mode 100644 index 0000000000..8ccd81edf5 --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts @@ -0,0 +1,296 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Observable, Subscription, throwError } from 'rxjs'; +import { map, mergeMap, publishReplay, refCount, share } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { EntitySubtype, EntityType } from '@shared/models/entity-type.models'; +import { MatAutocomplete, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { MatChipInputEvent, MatChipList } from '@angular/material/chips'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { AssetService } from '@core/http/asset.service'; +import { DeviceService } from '@core/http/device.service'; +import { EntityViewService } from '@core/http/entity-view.service'; +import { BroadcastService } from '@core/services/broadcast.service'; +import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; + +@Component({ + selector: 'tb-entity-subtype-list', + templateUrl: './entity-subtype-list.component.html', + styleUrls: ['./entity-subtype-list.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntitySubTypeListComponent), + multi: true + } + ] +}) +export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy { + + entitySubtypeListFormGroup: FormGroup; + + modelValue: Array | null; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + const newVal = coerceBooleanProperty(value); + if (this.requiredValue !== newVal) { + this.requiredValue = newVal; + this.updateValidators(); + } + } + + @Input() + disabled: boolean; + + @Input() + entityType: EntityType; + + @ViewChild('entitySubtypeInput') entitySubtypeInput: ElementRef; + @ViewChild('entitySubtypeAutocomplete') entitySubtypeAutocomplete: MatAutocomplete; + @ViewChild('chipList', {static: true}) chipList: MatChipList; + + entitySubtypeList: Array = []; + filteredEntitySubtypeList: Observable>; + entitySubtypes: Observable>; + + private broadcastSubscription: Subscription; + + placeholder: string; + secondaryPlaceholder: string; + noSubtypesMathingText: string; + subtypeListEmptyText: string; + + separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; + + searchText = ''; + + private dirty = false; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private broadcast: BroadcastService, + public translate: TranslateService, + private assetService: AssetService, + private deviceService: DeviceService, + private entityViewService: EntityViewService, + private fb: FormBuilder) { + this.entitySubtypeListFormGroup = this.fb.group({ + entitySubtypeList: [this.entitySubtypeList, this.required ? [Validators.required] : []], + entitySubtype: [null] + }); + } + + updateValidators() { + this.entitySubtypeListFormGroup.get('entitySubtypeList').setValidators(this.required ? [Validators.required] : []); + this.entitySubtypeListFormGroup.get('entitySubtypeList').updateValueAndValidity(); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + + switch (this.entityType) { + case EntityType.ASSET: + this.placeholder = this.required ? this.translate.instant('asset.enter-asset-type') + : this.translate.instant('asset.any-asset'); + this.secondaryPlaceholder = '+' + this.translate.instant('asset.asset-type'); + this.noSubtypesMathingText = 'asset.no-asset-types-matching'; + this.subtypeListEmptyText = 'asset.asset-type-list-empty'; + this.broadcastSubscription = this.broadcast.on('assetSaved', () => { + this.entitySubtypes = null; + }); + break; + case EntityType.DEVICE: + this.placeholder = this.required ? this.translate.instant('device.enter-device-type') + : this.translate.instant('device.any-device'); + this.secondaryPlaceholder = '+' + this.translate.instant('device.device-type'); + this.noSubtypesMathingText = 'device.no-device-types-matching'; + this.subtypeListEmptyText = 'device.device-type-list-empty'; + this.broadcastSubscription = this.broadcast.on('deviceSaved', () => { + this.entitySubtypes = null; + }); + break; + case EntityType.ENTITY_VIEW: + this.placeholder = this.required ? this.translate.instant('entity-view.enter-entity-view-type') + : this.translate.instant('entity-view.any-entity-view'); + this.secondaryPlaceholder = '+' + this.translate.instant('entity-view.entity-view-type'); + this.noSubtypesMathingText = 'entity-view.no-entity-view-types-matching'; + this.subtypeListEmptyText = 'entity-view.entity-view-type-list-empty'; + this.broadcastSubscription = this.broadcast.on('entityViewSaved', () => { + this.entitySubtypes = null; + }); + break; + } + + this.filteredEntitySubtypeList = this.entitySubtypeListFormGroup.get('entitySubtype').valueChanges + .pipe( + map(value => value ? value : ''), + mergeMap(name => this.fetchEntitySubtypes(name) ), + share() + ); + } + + ngAfterViewInit(): void { + } + + ngOnDestroy(): void { + if (this.broadcastSubscription) { + this.broadcastSubscription.unsubscribe(); + } + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.entitySubtypeListFormGroup.disable({emitEvent: false}); + } else { + this.entitySubtypeListFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: Array | null): void { + this.searchText = ''; + if (value != null && value.length > 0) { + this.modelValue = [...value]; + this.entitySubtypeList = [...value]; + this.entitySubtypeListFormGroup.get('entitySubtypeList').setValue(this.entitySubtypeList); + } else { + this.entitySubtypeList = []; + this.entitySubtypeListFormGroup.get('entitySubtypeList').setValue(this.entitySubtypeList); + this.modelValue = null; + } + this.dirty = true; + } + + add(entitySubtype: string): void { + if (!this.modelValue || this.modelValue.indexOf(entitySubtype) === -1) { + if (!this.modelValue) { + this.modelValue = []; + } + this.modelValue.push(entitySubtype); + this.entitySubtypeList.push(entitySubtype); + this.entitySubtypeListFormGroup.get('entitySubtypeList').setValue(this.entitySubtypeList); + } + this.propagateChange(this.modelValue); + this.clear(); + } + + chipAdd(event: MatChipInputEvent): void { + const value = event.value; + if ((value || '').trim()) { + this.add(value.trim()); + } + this.clear(''); + } + + remove(entitySubtype: string) { + const index = this.entitySubtypeList.indexOf(entitySubtype); + if (index >= 0) { + this.entitySubtypeList.splice(index, 1); + this.entitySubtypeListFormGroup.get('entitySubtypeList').setValue(this.entitySubtypeList); + this.modelValue.splice(index, 1); + if (!this.modelValue.length) { + this.modelValue = null; + } + this.propagateChange(this.modelValue); + this.clear(); + } + } + + selected(event: MatAutocompleteSelectedEvent): void { + this.add(event.option.viewValue); + this.clear(''); + } + + displayEntitySubtypeFn(entitySubtype?: string): string | undefined { + return entitySubtype ? entitySubtype : undefined; + } + + fetchEntitySubtypes(searchText?: string): Observable> { + this.searchText = searchText; + return this.getEntitySubtypes().pipe( + map(subTypes => { + let result = subTypes.filter( subType => { + return searchText ? subType.toUpperCase().startsWith(searchText.toUpperCase()) : true; + }); + if (!result.length) { + result = [searchText]; + } + return result; + }) + ); + } + + getEntitySubtypes(): Observable> { + if (!this.entitySubtypes) { + let subTypesObservable: Observable>; + switch (this.entityType) { + case EntityType.ASSET: + subTypesObservable = this.assetService.getAssetTypes({ignoreLoading: true}); + break; + case EntityType.DEVICE: + subTypesObservable = this.deviceService.getDeviceTypes({ignoreLoading: true}); + break; + case EntityType.ENTITY_VIEW: + subTypesObservable = this.entityViewService.getEntityViewTypes({ignoreLoading: true}); + break; + } + if (subTypesObservable) { + this.entitySubtypes = subTypesObservable.pipe( + map(subTypes => subTypes.map(subType => subType.type)), + publishReplay(1), + refCount() + ); + } else { + return throwError(null); + } + } + return this.entitySubtypes; + } + + onFocus() { + if (this.dirty) { + this.entitySubtypeListFormGroup.get('entitySubtype').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + clear(value: string = '') { + this.entitySubtypeInput.nativeElement.value = value; + this.entitySubtypeListFormGroup.get('entitySubtype').patchValue(value, {emitEvent: true}); + setTimeout(() => { + this.entitySubtypeInput.nativeElement.blur(); + this.entitySubtypeInput.nativeElement.focus(); + }, 0); + } + +} diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.html b/ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.html new file mode 100644 index 0000000000..92b43d490f --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.html @@ -0,0 +1,28 @@ + + + {{ entitySubtypeTitle | translate }} + + + {{ displaySubTypeFn(subType) }} + + + + {{ entitySubtypeRequiredText | translate }} + + diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.scss b/ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.scss new file mode 100644 index 0000000000..bb3718c2da --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.scss @@ -0,0 +1,18 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + +} diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.ts new file mode 100644 index 0000000000..72540ad4c8 --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.ts @@ -0,0 +1,245 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Observable, Subject, Subscription, throwError } from 'rxjs'; +import { map, mergeMap, publishReplay, refCount, startWith, tap } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { DeviceService } from '@core/http/device.service'; +import { EntitySubtype, EntityType } from '@app/shared/models/entity-type.models'; +import { BroadcastService } from '@app/core/services/broadcast.service'; +import { AssetService } from '@core/http/asset.service'; +import { EntityViewService } from '@core/http/entity-view.service'; + +@Component({ + selector: 'tb-entity-subtype-select', + templateUrl: './entity-subtype-select.component.html', + styleUrls: ['./entity-subtype-select.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntitySubTypeSelectComponent), + multi: true + }] +}) +export class EntitySubTypeSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy { + + subTypeFormGroup: FormGroup; + + modelValue: string | null = ''; + + @Input() + entityType: EntityType; + + @Input() + showLabel: boolean; + + @Input() + required: boolean; + + @Input() + disabled: boolean; + + @Input() + typeTranslatePrefix: string; + + entitySubtypeTitle: string; + entitySubtypeRequiredText: string; + + subTypesOptions: Observable>; + + private subTypesOptionsSubject: Subject = new Subject(); + + subTypes: Observable>; + + subTypesLoaded = false; + + private broadcastSubscription: Subscription; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private broadcast: BroadcastService, + public translate: TranslateService, + private deviceService: DeviceService, + private assetService: AssetService, + private entityViewService: EntityViewService, + private fb: FormBuilder) { + this.subTypeFormGroup = this.fb.group({ + subType: [''] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + + switch (this.entityType) { + case EntityType.ASSET: + this.entitySubtypeTitle = 'asset.asset-type'; + this.entitySubtypeRequiredText = 'asset.asset-type-required'; + this.broadcastSubscription = this.broadcast.on('assetSaved', () => { + this.subTypes = null; + this.subTypesOptionsSubject.next(''); + }); + break; + case EntityType.DEVICE: + this.entitySubtypeTitle = 'device.device-type'; + this.entitySubtypeRequiredText = 'device.device-type-required'; + this.broadcastSubscription = this.broadcast.on('deviceSaved', () => { + this.subTypes = null; + this.subTypesOptionsSubject.next(''); + }); + break; + case EntityType.ENTITY_VIEW: + this.entitySubtypeTitle = 'entity-view.entity-view-type'; + this.entitySubtypeRequiredText = 'entity-view.entity-view-type-required'; + this.broadcastSubscription = this.broadcast.on('entityViewSaved', () => { + this.subTypes = null; + this.subTypesOptionsSubject.next(''); + }); + break; + } + + this.subTypesOptions = this.subTypesOptionsSubject.asObservable().pipe( + startWith(''), + mergeMap(() => this.getSubTypes()) + ); + + this.subTypeFormGroup.get('subType').valueChanges.subscribe( + (value) => { + let modelValue; + if (!value || value === '') { + modelValue = ''; + } else { + modelValue = value.type; + } + this.updateView(modelValue); + } + ); + } + + ngAfterViewInit(): void { + } + + ngOnDestroy(): void { + if (this.broadcastSubscription) { + this.broadcastSubscription.unsubscribe(); + } + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.subTypeFormGroup.disable(); + } else { + this.subTypeFormGroup.enable(); + } + } + + writeValue(value: string | null): void { + if (value != null && value !== '') { + this.modelValue = value; + this.findSubTypes(value).subscribe( + (subTypes) => { + const subType = subTypes && subTypes.length === 1 ? subTypes[0] : ''; + this.subTypeFormGroup.get('subType').patchValue(subType, {emitEvent: true}); + } + ); + } else { + this.modelValue = ''; + this.subTypeFormGroup.get('subType').patchValue('', {emitEvent: true}); + } + } + + updateView(value: string | null) { + if (this.modelValue !== value) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + displaySubTypeFn(subType?: EntitySubtype | string): string | undefined { + if (subType && typeof subType !== 'string') { + if (this.typeTranslatePrefix) { + return this.translate.instant(this.typeTranslatePrefix + '.' + subType.type); + } else { + return subType.type; + } + } else { + return this.translate.instant('entity.all-subtypes'); + } + } + + findSubTypes(searchText?: string): Observable> { + return this.getSubTypes().pipe( + map(subTypes => subTypes.filter( subType => { + return searchText ? (typeof subType === 'string' ? false : subType.type === searchText) : false; + })) + ); + } + + getSubTypes(): Observable> { + if (!this.subTypes) { + switch (this.entityType) { + case EntityType.ASSET: + this.subTypes = this.assetService.getAssetTypes({ignoreLoading: true}); + break; + case EntityType.DEVICE: + this.subTypes = this.deviceService.getDeviceTypes({ignoreLoading: true}); + break; + case EntityType.ENTITY_VIEW: + this.subTypes = this.entityViewService.getEntityViewTypes({ignoreLoading: true}); + break; + } + if (this.subTypes) { + this.subTypes = this.subTypes.pipe( + map((allSubtypes) => { + allSubtypes.unshift(''); + this.subTypesLoaded = true; + return allSubtypes; + }), + tap((subTypes) => { + const type: EntitySubtype | string = this.subTypeFormGroup.get('subType').value; + const strType = typeof type === 'string' ? type : type.type; + const found = subTypes.find((subType) => { + if (typeof subType === 'string') { + return subType === type; + } else { + return subType.type === strType; + } + }); + if (found) { + this.subTypeFormGroup.get('subType').patchValue(found); + } + }), + publishReplay(1), + refCount() + ); + } else { + return throwError(null); + } + } + return this.subTypes; + } +} diff --git a/ui-ngx/src/app/shared/components/entity/entity-type-list.component.html b/ui-ngx/src/app/shared/components/entity/entity-type-list.component.html new file mode 100644 index 0000000000..81f428e3e6 --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-type-list.component.html @@ -0,0 +1,54 @@ + + + + + {{entityType.name}} + close + + + + + + + + + + {{ translate.get('entity.no-entity-types-matching', {entityType: searchText}) | async }} + + + + + {{ 'entity.entity-type-list-empty' | translate }} + + diff --git a/ui-ngx/src/app/shared/components/entity/entity-type-list.component.ts b/ui-ngx/src/app/shared/components/entity/entity-type-list.component.ts new file mode 100644 index 0000000000..ca2c97909f --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-type-list.component.ts @@ -0,0 +1,243 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Observable, of } from 'rxjs'; +import { filter, map, mergeMap, share, tap } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { AliasEntityType, EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { EntityService } from '@core/http/entity.service'; +import { MatAutocomplete } from '@angular/material/autocomplete'; +import { MatChipList } from '@angular/material/chips'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; + +interface EntityTypeInfo { + name: string; + value: EntityType; +} + +@Component({ + selector: 'tb-entity-type-list', + templateUrl: './entity-type-list.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntityTypeListComponent), + multi: true + } + ] +}) +export class EntityTypeListComponent implements ControlValueAccessor, OnInit, AfterViewInit { + + entityTypeListFormGroup: FormGroup; + + modelValue: Array | null; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + const newVal = coerceBooleanProperty(value); + if (this.requiredValue !== newVal) { + this.requiredValue = newVal; + this.updateValidators(); + } + } + + @Input() + disabled: boolean; + + @Input() + allowedEntityTypes: Array; + + @Input() + ignoreAuthorityFilter: boolean; + + @ViewChild('entityTypeInput') entityTypeInput: ElementRef; + @ViewChild('entityTypeAutocomplete') entityTypeAutocomplete: MatAutocomplete; + @ViewChild('chipList', {static: true}) chipList: MatChipList; + + allEntityTypeList: Array = []; + entityTypeList: Array = []; + filteredEntityTypeList: Observable>; + + placeholder: string; + secondaryPlaceholder: string; + + searchText = ''; + + private dirty = false; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + public translate: TranslateService, + private entityService: EntityService, + private fb: FormBuilder) { + this.entityTypeListFormGroup = this.fb.group({ + entityTypeList: [this.entityTypeList, this.required ? [Validators.required] : []], + entityType: [null] + }); + } + + updateValidators() { + this.entityTypeListFormGroup.get('entityTypeList').setValidators(this.required ? [Validators.required] : []); + this.entityTypeListFormGroup.get('entityTypeList').updateValueAndValidity(); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + + this.placeholder = this.required ? this.translate.instant('entity.enter-entity-type') + : this.translate.instant('entity.any-entity'); + this.secondaryPlaceholder = '+' + this.translate.instant('entity.entity-type'); + + let entityTypes: Array; + if (this.ignoreAuthorityFilter && this.allowedEntityTypes + && this.allowedEntityTypes.length) { + entityTypes = []; + this.allowedEntityTypes.forEach((entityTypeValue) => { + entityTypes.push(entityTypeValue); + }); + } else { + entityTypes = this.entityService.prepareAllowedEntityTypesList(this.allowedEntityTypes) as Array; + } + + entityTypes.forEach((entityType) => { + this.allEntityTypeList.push({ + name: this.translate.instant(entityTypeTranslations.get(entityType).type), + value: entityType as EntityType + }); + }); + + this.filteredEntityTypeList = this.entityTypeListFormGroup.get('entityType').valueChanges + .pipe( + // startWith>(''), + tap((value) => { + if (value && typeof value !== 'string') { + this.add(value); + } else if (value === null) { + this.clear(this.entityTypeInput.nativeElement.value); + } + }), + filter((value) => typeof value === 'string'), + map((value) => value ? (typeof value === 'string' ? value : value.name) : ''), + mergeMap(name => this.fetchEntityTypes(name) ), + share() + ); + } + + ngAfterViewInit(): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.entityTypeListFormGroup.disable({emitEvent: false}); + } else { + this.entityTypeListFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: Array | null): void { + this.searchText = ''; + if (value != null && value.length > 0) { + this.modelValue = [...value]; + this.entityTypeList = []; + value.forEach((entityType) => { + this.entityTypeList.push({ + name: entityTypeTranslations.has(entityType) ? this.translate.instant(entityTypeTranslations.get(entityType).type) : 'Unknown', + value: entityType + }); + }); + this.entityTypeListFormGroup.get('entityTypeList').setValue(this.entityTypeList); + } else { + this.entityTypeList = []; + this.entityTypeListFormGroup.get('entityTypeList').setValue(this.entityTypeList); + this.modelValue = null; + } + this.dirty = true; + } + + add(entityType: EntityTypeInfo): void { + if (!this.modelValue || this.modelValue.indexOf(entityType.value) === -1) { + if (!this.modelValue) { + this.modelValue = []; + } + this.modelValue.push(entityType.value); + this.entityTypeList.push(entityType); + this.entityTypeListFormGroup.get('entityTypeList').setValue(this.entityTypeList); + } + this.propagateChange(this.modelValue); + this.clear(); + } + + remove(entityType: EntityTypeInfo) { + const index = this.entityTypeList.indexOf(entityType); + if (index >= 0) { + this.entityTypeList.splice(index, 1); + this.entityTypeListFormGroup.get('entityTypeList').setValue(this.entityTypeList); + this.modelValue.splice(index, 1); + if (!this.modelValue.length) { + this.modelValue = null; + } + this.propagateChange(this.modelValue); + this.clear(); + } + } + + displayEntityTypeFn(entityType?: EntityTypeInfo): string | undefined { + return entityType ? entityType.name : undefined; + } + + fetchEntityTypes(searchText?: string): Observable> { + this.searchText = searchText; + let result = this.allEntityTypeList; + if (searchText && searchText.length) { + result = this.allEntityTypeList.filter((entityTypeInfo) => entityTypeInfo.name.toLowerCase().includes(searchText.toLowerCase())); + } + return of(result); + } + + onFocus() { + if (this.dirty) { + this.entityTypeListFormGroup.get('entityType').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + clear(value: string = '') { + this.entityTypeInput.nativeElement.value = value; + this.entityTypeListFormGroup.get('entityType').patchValue(value, {emitEvent: true}); + setTimeout(() => { + this.entityTypeInput.nativeElement.blur(); + this.entityTypeInput.nativeElement.focus(); + }, 0); + } + +} diff --git a/ui-ngx/src/app/shared/components/entity/entity-type-select.component.html b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.html new file mode 100644 index 0000000000..0fcf0f8948 --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.html @@ -0,0 +1,29 @@ + + + {{ 'entity.type' | translate }} + + + {{ displayEntityTypeFn(type) }} + + + + {{ 'entity.type-required' | translate }} + + diff --git a/ui-ngx/src/app/shared/components/entity/entity-type-select.component.scss b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.scss new file mode 100644 index 0000000000..ed875e81f5 --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.scss @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { +} + +:host ::ng-deep { + .mat-form-field-infix { + width: 100%; + } + .mat-select-value { + max-width: 200px; + min-width: 100px; + } +} diff --git a/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts new file mode 100644 index 0000000000..2695867f96 --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts @@ -0,0 +1,140 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { AliasEntityType, EntityType, entityTypeTranslations } from '@app/shared/models/entity-type.models'; +import { EntityService } from '@core/http/entity.service'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; + +@Component({ + selector: 'tb-entity-type-select', + templateUrl: './entity-type-select.component.html', + styleUrls: ['./entity-type-select.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntityTypeSelectComponent), + multi: true + }] +}) +export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit { + + entityTypeFormGroup: FormGroup; + + modelValue: EntityType | AliasEntityType | null; + + @Input() + allowedEntityTypes: Array; + + @Input() + useAliasEntityTypes: boolean; + + private showLabelValue: boolean; + get showLabel(): boolean { + return this.showLabelValue; + } + @Input() + set showLabel(value: boolean) { + this.showLabelValue = coerceBooleanProperty(value); + } + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + entityTypes: Array; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private entityService: EntityService, + public translate: TranslateService, + private fb: FormBuilder) { + this.entityTypeFormGroup = this.fb.group({ + entityType: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.entityTypes = this.entityService.prepareAllowedEntityTypesList(this.allowedEntityTypes, this.useAliasEntityTypes); + this.entityTypeFormGroup.get('entityType').valueChanges.subscribe( + (value) => { + let modelValue; + if (!value || value === '') { + modelValue = null; + } else { + modelValue = value; + } + this.updateView(modelValue); + } + ); + } + + ngAfterViewInit(): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.entityTypeFormGroup.disable(); + } else { + this.entityTypeFormGroup.enable(); + } + } + + writeValue(value: EntityType | AliasEntityType | null): void { + if (value != null) { + this.modelValue = value; + this.entityTypeFormGroup.get('entityType').patchValue(value, {emitEvent: true}); + } else { + this.modelValue = null; + this.entityTypeFormGroup.get('entityType').patchValue(null, {emitEvent: true}); + } + } + + updateView(value: EntityType | AliasEntityType | null) { + if (this.modelValue !== value) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + displayEntityTypeFn(entityType?: EntityType | AliasEntityType | null): string | undefined { + if (entityType) { + return this.translate.instant(entityTypeTranslations.get(entityType as EntityType).type); + } else { + return ''; + } + } +} diff --git a/ui-ngx/src/app/shared/components/fab-toolbar.component.html b/ui-ngx/src/app/shared/components/fab-toolbar.component.html new file mode 100644 index 0000000000..67d4cb6536 --- /dev/null +++ b/ui-ngx/src/app/shared/components/fab-toolbar.component.html @@ -0,0 +1,22 @@ + +
+
+ +
+
diff --git a/ui-ngx/src/app/shared/components/fab-toolbar.component.scss b/ui-ngx/src/app/shared/components/fab-toolbar.component.scss new file mode 100644 index 0000000000..e4b5732884 --- /dev/null +++ b/ui-ngx/src/app/shared/components/fab-toolbar.component.scss @@ -0,0 +1,187 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +$font-size: 10px !default; +@function rem($multiplier) { + @return $multiplier * $font-size; +} + +$button-fab-width: rem(5.600) !default; +$button-fab-height: rem(5.600) !default; +$button-fab-padding: rem(1.60) !default; +$icon-button-margin: rem(0.600) !default; +$z-index-fab: 20 !default; + +$swift-ease-in-duration: 0.3s !default; +$swift-ease-in-timing-function: cubic-bezier(0.55, 0, 0.55, 0.2) !default; +$swift-ease-in: all $swift-ease-in-duration $swift-ease-in-timing-function !default; + +@mixin rtl-prop($ltr-prop, $rtl-prop, $value, $reset-value) { + #{$ltr-prop}: $value; + [dir=rtl] & { + #{$ltr-prop}: $reset-value; + #{$rtl-prop}: $value; + } +} + +@mixin fab-position($spot, $top: auto, $right: auto, $bottom: auto, $left: auto) { + &.mat-fab-#{$spot} { + top: $top; + right: $right; + bottom: $bottom; + left: $left; + position: absolute; + } +} + +@mixin fab-all-positions() { + @include fab-position(bottom-right, auto, ($button-fab-width - $button-fab-padding)/2, ($button-fab-height - $button-fab-padding)/2, auto); + @include fab-position(bottom-left, auto, auto, ($button-fab-height - $button-fab-padding)/2, ($button-fab-width - $button-fab-padding)/2); + @include fab-position(top-right, ($button-fab-height - $button-fab-padding)/2, ($button-fab-width - $button-fab-padding)/2, auto, auto); + @include fab-position(top-left, ($button-fab-height - $button-fab-padding)/2, auto, auto, ($button-fab-width - $button-fab-padding)/2); +} + +mat-fab-toolbar { + $icon-delay: 200ms; + @include fab-all-positions(); + display: block; + + .mat-fab-toolbar-wrapper { + display: block; + position: relative; + overflow: hidden; + + height: $button-fab-width + ($icon-button-margin * 2); + } + + mat-fab-trigger { + position: absolute; + z-index: $z-index-fab; + + button { + overflow: visible !important; + opacity: .5; + } + + .mat-fab-toolbar-background { + display: block; + position: absolute; + z-index: $z-index-fab + 1; + opacity: 1; + } + + mat-icon { + position: relative; + z-index: $z-index-fab + 2; + + opacity: 1; + + } + + } + + &.mat-left { + mat-fab-trigger { + @include rtl-prop(right, left, 0, auto); + } + + .mat-toolbar-tools { + flex-direction: row-reverse; + + > .mat-button:first-child { + @include rtl-prop(margin-right, margin-left, 0.6rem, auto) + } + + > .mat-button:first-child { + @include rtl-prop(margin-left, margin-right, -0.8rem, auto); + } + + + > .mat-button:last-child { + @include rtl-prop(margin-right, margin-left, 8px, auto); + } + + } + } + + &.mat-right { + mat-fab-trigger { + @include rtl-prop(left, right, 0, auto); + } + + .mat-toolbar-tools { + flex-direction: row; + } + } + + mat-toolbar { + padding: 0 !important; + background-color: transparent !important; + pointer-events: none; + position: relative; + z-index: $z-index-fab + 3; + + .mat-toolbar-tools { + padding: 0 20px !important; + margin-top: 3px; + } + + .mat-fab-action-item { + opacity: 0; + transform: scale(0); + } + } + + &.mat-is-open { + mat-fab-trigger > button { + box-shadow: none; + opacity: 1; + + mat-icon { + opacity: 0; + } + } + + .mat-fab-action-item { + opacity: 1; + transform: scale(1); + } + } + + &.mat-animation { + mat-fab-trigger { + button { + transition: opacity .3s cubic-bezier(.55, 0, .55, .2) .2s; + } + .mat-fab-toolbar-background { + transition: $swift-ease-in; + } + mat-icon { + transition: all $icon-delay ease-in; + } + } + mat-toolbar { + .mat-fab-action-item { + transition: $swift-ease-in; + transition-duration: $swift-ease-in-duration / 2; + } + } + &.mat-is-open { + mat-fab-trigger > button { + transition: opacity .3s cubic-bezier(.55, 0, .55, .2); + } + } + } +} diff --git a/ui-ngx/src/app/shared/components/fab-toolbar.component.ts b/ui-ngx/src/app/shared/components/fab-toolbar.component.ts new file mode 100644 index 0000000000..a40efb9447 --- /dev/null +++ b/ui-ngx/src/app/shared/components/fab-toolbar.component.ts @@ -0,0 +1,194 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + Component, + Directive, + ElementRef, + Inject, + Input, + OnChanges, + OnDestroy, + OnInit, + SimpleChanges, + ViewEncapsulation +} from '@angular/core'; +import { WINDOW } from '@core/services/window.service'; +import { CanColorCtor, mixinColor } from '@angular/material/core'; +import { ResizeObserver } from '@juggle/resize-observer'; + +export declare type FabToolbarDirection = 'left' | 'right'; + +class MatFabToolbarBase { + // tslint:disable-next-line:variable-name + constructor(public _elementRef: ElementRef) {} +} +const MatFabToolbarMixinBase: CanColorCtor & typeof MatFabToolbarBase = mixinColor(MatFabToolbarBase); + +@Directive({ + // tslint:disable-next-line:directive-selector + selector: 'mat-fab-trigger' +}) +export class FabTriggerDirective { + + constructor(private el: ElementRef) { + } + +} + +@Directive({ + // tslint:disable-next-line:directive-selector + selector: 'mat-fab-actions' +}) +export class FabActionsDirective implements OnInit { + + constructor(private el: ElementRef) { + } + + ngOnInit(): void { + const element = $(this.el.nativeElement); + const children = element.children(); + children.wrap('
'); + } + +} + +// @dynamic +@Component({ + // tslint:disable-next-line:component-selector + selector: 'mat-fab-toolbar', + templateUrl: './fab-toolbar.component.html', + styleUrls: ['./fab-toolbar.component.scss'], + inputs: ['color'], + encapsulation: ViewEncapsulation.None +}) +export class FabToolbarComponent extends MatFabToolbarMixinBase implements OnInit, OnDestroy, AfterViewInit, OnChanges { + + private fabToolbarResize$: ResizeObserver; + + @Input() + isOpen: boolean; + + @Input() + direction: FabToolbarDirection; + + constructor(private el: ElementRef, + @Inject(WINDOW) private window: Window) { + super(el); + } + + ngOnInit(): void { + const element = $(this.el.nativeElement); + element.addClass('mat-fab-toolbar'); + element.find('mat-fab-trigger').find('button') + .prepend('
'); + element.addClass(`mat-${this.direction}`); + this.fabToolbarResize$ = new ResizeObserver(() => { + this.onFabToolbarResize(); + }); + this.fabToolbarResize$.observe(this.el.nativeElement); + } + + ngOnDestroy(): void { + this.fabToolbarResize$.disconnect(); + } + + ngAfterViewInit(): void { + this.triggerOpenClose(true); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'isOpen') { + this.triggerOpenClose(); + } + } + } + } + + private onFabToolbarResize() { + if (this.isOpen) { + this.triggerOpenClose(true); + } + } + + private triggerOpenClose(disableAnimation?: boolean): void { + const el = this.el.nativeElement; + const element = $(this.el.nativeElement); + if (disableAnimation) { + element.removeClass('mat-animation'); + } else { + element.addClass('mat-animation'); + } + const backgroundElement: HTMLElement = el.querySelector('.mat-fab-toolbar-background'); + const triggerElement: HTMLElement = el.querySelector('mat-fab-trigger button'); + const toolbarElement: HTMLElement = el.querySelector('mat-toolbar'); + const iconElement: HTMLElement = el.querySelector('mat-fab-trigger button mat-icon'); + const actions = element.find('mat-fab-actions').children(); + if (triggerElement && backgroundElement) { + const width = el.offsetWidth; + const scale = 2 * (width / triggerElement.offsetWidth); + + backgroundElement.style.borderRadius = width + 'px'; + + if (this.isOpen) { + element.addClass('mat-is-open'); + toolbarElement.style.pointerEvents = 'inherit'; + + backgroundElement.style.width = triggerElement.offsetWidth + 'px'; + backgroundElement.style.height = triggerElement.offsetHeight + 'px'; + backgroundElement.style.transform = 'scale(' + scale + ')'; + + backgroundElement.style.transitionDelay = '0ms'; + if (iconElement) { + iconElement.style.transitionDelay = disableAnimation ? '0ms' : '.3s'; + } + + actions.each((index, action) => { + action.style.transitionDelay = disableAnimation ? '0ms' : ((actions.length - index) * 25 + 'ms'); + }); + + } else { + element.removeClass('mat-is-open'); + toolbarElement.style.pointerEvents = 'none'; + + backgroundElement.style.transform = 'scale(1)'; + + backgroundElement.style.top = '0'; + + if (element.hasClass('mat-right')) { + backgroundElement.style.left = '0'; + backgroundElement.style.right = null; + } + + if (element.hasClass('mat-left')) { + backgroundElement.style.right = '0'; + backgroundElement.style.left = null; + } + + backgroundElement.style.transitionDelay = disableAnimation ? '0ms' : '200ms'; + + actions.each((index, action) => { + action.style.transitionDelay = (disableAnimation ? 0 : 200) + (index * 25) + 'ms'; + }); + } + } + } + +} diff --git a/ui-ngx/src/app/shared/components/file-input.component.html b/ui-ngx/src/app/shared/components/file-input.component.html new file mode 100644 index 0000000000..52a4a38562 --- /dev/null +++ b/ui-ngx/src/app/shared/components/file-input.component.html @@ -0,0 +1,46 @@ + +
+ + +
+
+ +
+
+ + +
+
+
+
+
+ +
{{ noFileText }}
+
{{ fileName }}
+
diff --git a/ui-ngx/src/app/shared/components/file-input.component.scss b/ui-ngx/src/app/shared/components/file-input.component.scss new file mode 100644 index 0000000000..f4085a87fc --- /dev/null +++ b/ui-ngx/src/app/shared/components/file-input.component.scss @@ -0,0 +1,80 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../scss/constants"; + +$previewSize: 100px !default; + +:host { + + .tb-container { + margin-top: 0; + label.tb-title { + display: block; + padding-bottom: 8px; + } + } + + .tb-file-select-container { + position: relative; + width: 100%; + height: $previewSize; + } + + .tb-file-preview { + width: auto; + max-width: $previewSize; + height: auto; + max-height: $previewSize; + } + + .tb-file-clear-container { + position: relative; + float: right; + width: 48px; + height: $previewSize; + } + + .tb-file-clear-btn { + position: absolute !important; + top: 50%; + transform: translate(0%, -50%) !important; + } + + .file-input { + display: none; + } + + .tb-flow-drop { + position: relative; + height: $previewSize; + overflow: hidden; + border: dashed 2px; + + label { + display: flex; + flex-direction: column; + justify-content: center; + width: 100%; + height: 100%; + font-size: 16px; + text-align: center; + + @media #{$mat-gt-sm} { + font-size: 24px; + } + } + } +} diff --git a/ui-ngx/src/app/shared/components/file-input.component.ts b/ui-ngx/src/app/shared/components/file-input.component.ts new file mode 100644 index 0000000000..56f6be1dd1 --- /dev/null +++ b/ui-ngx/src/app/shared/components/file-input.component.ts @@ -0,0 +1,199 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + Component, + EventEmitter, + forwardRef, + Input, + OnChanges, + OnDestroy, + Output, + SimpleChanges, + ViewChild +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Subscription } from 'rxjs'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { FlowDirective } from '@flowjs/ngx-flow'; +import { TranslateService } from '@ngx-translate/core'; + +@Component({ + selector: 'tb-file-input', + templateUrl: './file-input.component.html', + styleUrls: ['./file-input.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FileInputComponent), + multi: true + } + ] +}) +export class FileInputComponent extends PageComponent implements AfterViewInit, OnDestroy, ControlValueAccessor, OnChanges { + + @Input() + label: string; + + @Input() + accept = '*/*'; + + @Input() + noFileText = 'import.no-file'; + + @Input() + inputId = 'select'; + + @Input() + allowedExtensions: string; + + @Input() + dropLabel: string; + + @Input() + contentConvertFunction: (content: string) => any; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + const newVal = coerceBooleanProperty(value); + if (this.requiredValue !== newVal) { + this.requiredValue = newVal; + } + } + + private requiredAsErrorValue: boolean; + get requiredAsError(): boolean { + return this.requiredAsErrorValue; + } + @Input() + set requiredAsError(value: boolean) { + const newVal = coerceBooleanProperty(value); + if (this.requiredAsErrorValue !== newVal) { + this.requiredAsErrorValue = newVal; + } + } + + @Input() + disabled: boolean; + + @Input() + existingFileName: string; + + @Output() + fileNameChanged = new EventEmitter(); + + fileName: string; + fileContent: any; + + @ViewChild('flow', {static: true}) + flow: FlowDirective; + + autoUploadSubscription: Subscription; + + private propagateChange = null; + + constructor(protected store: Store, + public translate: TranslateService) { + super(store); + } + + ngAfterViewInit() { + this.autoUploadSubscription = this.flow.events$.subscribe(event => { + if (event.type === 'fileAdded') { + const file = event.event[0] as flowjs.FlowFile; + if (this.filterFile(file)) { + const reader = new FileReader(); + reader.onload = (loadEvent) => { + if (typeof reader.result === 'string') { + const fileContent = reader.result; + if (fileContent && fileContent.length > 0) { + if (this.contentConvertFunction) { + this.fileContent = this.contentConvertFunction(fileContent); + } else { + this.fileContent = fileContent; + } + if (this.fileContent) { + this.fileName = file.name; + } else { + this.fileName = null; + } + this.updateModel(); + } + } + }; + reader.readAsText(file.file); + } + } + }); + } + + private filterFile(file: flowjs.FlowFile): boolean { + if (this.allowedExtensions) { + return this.allowedExtensions.split(',').indexOf(file.getExtension()) > -1; + } else { + return true; + } + } + + ngOnDestroy() { + this.autoUploadSubscription.unsubscribe(); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: any): void { + this.fileName = this.existingFileName || null; + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (change.currentValue !== change.previousValue) { + if (propName === 'existingFileName') { + this.fileName = this.existingFileName || null; + } + } + } + } + + private updateModel() { + this.propagateChange(this.fileContent); + this.fileNameChanged.emit(this.fileName); + } + + clearFile() { + this.fileName = null; + this.fileContent = null; + this.updateModel(); + } +} diff --git a/ui-ngx/src/app/shared/components/footer-fab-buttons.component.html b/ui-ngx/src/app/shared/components/footer-fab-buttons.component.html new file mode 100644 index 0000000000..34872229c7 --- /dev/null +++ b/ui-ngx/src/app/shared/components/footer-fab-buttons.component.html @@ -0,0 +1,39 @@ + + diff --git a/ui-ngx/src/app/shared/components/footer-fab-buttons.component.scss b/ui-ngx/src/app/shared/components/footer-fab-buttons.component.scss new file mode 100644 index 0000000000..106692602b --- /dev/null +++ b/ui-ngx/src/app/shared/components/footer-fab-buttons.component.scss @@ -0,0 +1,54 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + section.tb-footer-fab-buttons { + &:not(.relative-buttons) { + position: fixed; + right: 20px; + bottom: 20px; + z-index: 30; + pointer-events: none; + } + + .fab-container { + display: flex; + flex-direction: column-reverse; + align-items: center; + > div { + display: flex; + flex-direction: column-reverse; + align-items: center; + + button { + margin-bottom: 17px; + } + } + } + + .tb-btn-footer { + &.fab-toggler { + margin-top: 0; + } + position: relative !important; + display: inline-block !important; + animation: tbMoveFromBottomFade .3s ease both; + + &.tb-hide { + animation: tbMoveToBottomFade .3s ease both; + } + } + } +} diff --git a/ui-ngx/src/app/shared/components/footer-fab-buttons.component.ts b/ui-ngx/src/app/shared/components/footer-fab-buttons.component.ts new file mode 100644 index 0000000000..c67ef019ab --- /dev/null +++ b/ui-ngx/src/app/shared/components/footer-fab-buttons.component.ts @@ -0,0 +1,95 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, HostListener, Input } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { speedDialFabAnimations } from '@shared/animations/speed-dial-fab.animations'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; + +export interface FooterFabButton { + name: string; + icon: string; + onAction: ($event: Event) => void; +} + +export interface FooterFabButtons { + fabTogglerName: string; + fabTogglerIcon: string; + buttons: Array; +} + +@Component({ + selector: 'tb-footer-fab-buttons', + templateUrl: './footer-fab-buttons.component.html', + styleUrls: ['./footer-fab-buttons.component.scss'], + animations: speedDialFabAnimations +}) +export class FooterFabButtonsComponent extends PageComponent { + + @Input() + footerFabButtons: FooterFabButtons; + + private relativeValue: boolean; + get relative(): boolean { + return this.relativeValue; + } + @Input() + set relative(value: boolean) { + this.relativeValue = coerceBooleanProperty(value); + } + + buttons: Array = []; + fabTogglerState = 'inactive'; + + closeTimeout = null; + + @HostListener('focusout', ['$event']) + onFocusOut($event) { + if (!this.closeTimeout) { + this.closeTimeout = setTimeout(() => { + this.hideItems(); + }, 100); + } + } + + @HostListener('focusin', ['$event']) + onFocusIn($event) { + if (this.closeTimeout) { + clearTimeout(this.closeTimeout); + this.closeTimeout = null; + } + } + + constructor(protected store: Store) { + super(store); + } + + showItems() { + this.fabTogglerState = 'active'; + this.buttons = this.footerFabButtons.buttons; + } + + hideItems() { + this.fabTogglerState = 'inactive'; + this.buttons = []; + } + + onToggleFab() { + this.buttons.length ? this.hideItems() : this.showItems(); + } +} diff --git a/ui-ngx/src/app/shared/components/footer.component.html b/ui-ngx/src/app/shared/components/footer.component.html new file mode 100644 index 0000000000..e26f858a5d --- /dev/null +++ b/ui-ngx/src/app/shared/components/footer.component.html @@ -0,0 +1,20 @@ + + diff --git a/ui-ngx/src/app/shared/components/footer.component.scss b/ui-ngx/src/app/shared/components/footer.component.scss new file mode 100644 index 0000000000..9bb2f2cbbe --- /dev/null +++ b/ui-ngx/src/app/shared/components/footer.component.scss @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.footer-text { + position: absolute; + width: 100%; + bottom: 20px; + margin: 0; + left: 0; + line-height: 20px; + text-align: center; + small { + font-size: 14px; + color: #98a6ad; + } +} diff --git a/ui-ngx/src/app/shared/components/footer.component.ts b/ui-ngx/src/app/shared/components/footer.component.ts new file mode 100644 index 0000000000..5fdf70e91e --- /dev/null +++ b/ui-ngx/src/app/shared/components/footer.component.ts @@ -0,0 +1,28 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; + +@Component({ + selector: 'tb-footer', + templateUrl: './footer.component.html', + styleUrls: ['./footer.component.scss'] +}) +export class FooterComponent { + + year = new Date().getFullYear(); + +} diff --git a/ui-ngx/src/app/shared/components/fullscreen.directive.ts b/ui-ngx/src/app/shared/components/fullscreen.directive.ts new file mode 100644 index 0000000000..20558c7c75 --- /dev/null +++ b/ui-ngx/src/app/shared/components/fullscreen.directive.ts @@ -0,0 +1,160 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + Directive, + ElementRef, + EventEmitter, + Input, + OnChanges, + OnDestroy, + Output, + Renderer2, + SecurityContext, + SimpleChanges, + ViewContainerRef +} from '@angular/core'; +import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { ComponentPortal } from '@angular/cdk/portal'; +import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; +import { DomSanitizer, SafeStyle } from '@angular/platform-browser'; + +@Directive({ + selector: '[tb-fullscreen]' +}) +export class FullscreenDirective implements OnChanges, OnDestroy { + + fullscreenValue = false; + + private overlayRef: OverlayRef; + private parentElement: HTMLElement; + + @Input() + fullscreen: boolean; + + @Input() + fullscreenElement: HTMLElement; + + @Input() + fullscreenBackgroundStyle: {[klass: string]: any}; + + @Input() + fullscreenBackgroundImage: SafeStyle | string; + + @Output() + fullscreenChanged = new EventEmitter(); + + constructor(public elementRef: ElementRef, + private renderer: Renderer2, + private sanitizer: DomSanitizer, + private viewContainerRef: ViewContainerRef, + private overlay: Overlay) { + } + + ngOnChanges(changes: SimpleChanges): void { + let updateFullscreen = false; + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'fullscreen') { + updateFullscreen = true; + } + } + } + if (updateFullscreen) { + if (this.fullscreen) { + this.enterFullscreen(); + } else { + this.exitFullscreen(); + } + } + } + + ngOnDestroy(): void { + if (this.fullscreen) { + this.exitFullscreen(); + } + } + + enterFullscreen() { + const targetElement: HTMLElement = this.fullscreenElement || this.elementRef.nativeElement; + this.parentElement = targetElement.parentElement; + this.parentElement.removeChild(targetElement); + targetElement.classList.add('tb-fullscreen'); + const position = this.overlay.position(); + const config = new OverlayConfig({ + hasBackdrop: false, + panelClass: 'tb-fullscreen-parent' + }); + config.minWidth = '100%'; + config.minHeight = '100%'; + config.positionStrategy = position.global().top('0%').left('0%') + .right('0%').bottom('0%'); + + this.overlayRef = this.overlay.create(config); + this.overlayRef.attach(new EmptyPortal()); + if (this.fullscreenBackgroundStyle) { + for (const key of Object.keys(this.fullscreenBackgroundStyle)) { + this.setStyle(this.overlayRef.overlayElement, key, this.fullscreenBackgroundStyle[key]); + } + } + if (this.fullscreenBackgroundImage) { + this.setStyle(this.overlayRef.overlayElement, 'backgroundImage', this.fullscreenBackgroundImage); + } + this.overlayRef.overlayElement.appendChild( targetElement ); + this.fullscreenChanged.emit(true); + } + + private setStyle(el: any, nameAndUnit: string, value: any): void { + const [name, unit] = nameAndUnit.split('.'); + let renderValue: string|null = + this.sanitizer.sanitize(SecurityContext.STYLE, value as{} | string); + if (renderValue != null) { + renderValue = renderValue.toString(); + } + renderValue = renderValue != null && unit ? `${renderValue}${unit}` : renderValue; + if (renderValue != null) { + this.renderer.setStyle(this.overlayRef.overlayElement, name, renderValue); + } else { + this.renderer.removeStyle(this.overlayRef.overlayElement, name); + } + } + + exitFullscreen() { + const targetElement: HTMLElement = this.fullscreenElement || this.elementRef.nativeElement; + if (this.parentElement) { + this.overlayRef.overlayElement.removeChild( targetElement ); + this.parentElement.appendChild(targetElement); + this.parentElement = null; + } + targetElement.classList.remove('tb-fullscreen'); + if (this.elementRef) { + this.elementRef.nativeElement.classList.remove('tb-fullscreen'); + } + if (this.overlayRef) { + this.overlayRef.dispose(); + } + this.fullscreenChanged.emit(false); + } +} + +class EmptyPortal extends ComponentPortal { + + constructor() { + super(TbAnchorComponent); + } + +} diff --git a/ui-ngx/src/app/shared/components/help.component.html b/ui-ngx/src/app/shared/components/help.component.html new file mode 100644 index 0000000000..15dacc7761 --- /dev/null +++ b/ui-ngx/src/app/shared/components/help.component.html @@ -0,0 +1,24 @@ + + diff --git a/ui-ngx/src/app/shared/components/help.component.ts b/ui-ngx/src/app/shared/components/help.component.ts new file mode 100644 index 0000000000..6321563767 --- /dev/null +++ b/ui-ngx/src/app/shared/components/help.component.ts @@ -0,0 +1,41 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Input } from '@angular/core'; +import { HelpLinks } from '@shared/models/constants'; + +@Component({ + // tslint:disable-next-line:component-selector + selector: '[tb-help]', + templateUrl: './help.component.html' +}) +export class HelpComponent { + + // tslint:disable-next-line:no-input-rename + @Input('tb-help') helpLinkId: string; + + gotoHelpPage(): void { + let helpUrl = HelpLinks.linksMap[this.helpLinkId]; + if (!helpUrl && this.helpLinkId && + (this.helpLinkId.startsWith('http://') || this.helpLinkId.startsWith('https://'))) { + helpUrl = this.helpLinkId; + } + if (helpUrl) { + window.open(helpUrl, '_blank'); + } + } + +} diff --git a/ui-ngx/src/app/shared/components/hotkeys.directive.ts b/ui-ngx/src/app/shared/components/hotkeys.directive.ts new file mode 100644 index 0000000000..744a0765e6 --- /dev/null +++ b/ui-ngx/src/app/shared/components/hotkeys.directive.ts @@ -0,0 +1,87 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Directive, ElementRef, Input, OnDestroy, OnInit } from '@angular/core'; +import { Hotkey } from 'angular2-hotkeys'; +import 'mousetrap'; +import { TbCheatSheetComponent } from '@shared/components/cheatsheet.component'; + +@Directive({ + selector : '[tb-hotkeys]' +}) +export class TbHotkeysDirective implements OnInit, OnDestroy { + @Input() hotkeys: Hotkey[] = []; + @Input() cheatSheet: TbCheatSheetComponent; + + private mousetrap: MousetrapInstance; + private hotkeysList: Hotkey[] = []; + + private preventIn = ['INPUT', 'SELECT', 'TEXTAREA']; + + constructor(private elementRef: ElementRef) { + this.mousetrap = new Mousetrap(this.elementRef.nativeElement); + (this.elementRef.nativeElement as HTMLElement).tabIndex = -1; + (this.elementRef.nativeElement as HTMLElement).style.outline = '0'; + } + + ngOnInit() { + for (const hotkey of this.hotkeys) { + this.hotkeysList.push(hotkey); + this.bindEvent(hotkey); + } + if (this.cheatSheet) { + const hotkeyObj: Hotkey = new Hotkey( + '?', + (event: KeyboardEvent) => { + this.cheatSheet.toggleCheatSheet(); + return false; + }, + [], + 'Show / hide this help menu', + ); + this.hotkeysList.unshift(hotkeyObj); + this.bindEvent(hotkeyObj); + this.cheatSheet.setHotKeys(this.hotkeysList); + } + } + + private bindEvent(hotkey: Hotkey): void { + this.mousetrap.bind((hotkey as Hotkey).combo, (event: KeyboardEvent, combo: string) => { + let shouldExecute = true; + if (event) { + const target: HTMLElement = (event.target || event.srcElement) as HTMLElement; + const nodeName: string = target.nodeName.toUpperCase(); + if ((' ' + target.className + ' ').indexOf(' mousetrap ') > -1) { + shouldExecute = true; + } else if (this.preventIn.indexOf(nodeName) > -1 && (hotkey as Hotkey). + allowIn.map(allow => allow.toUpperCase()).indexOf(nodeName) === -1) { + shouldExecute = false; + } + } + + if (shouldExecute) { + return (hotkey as Hotkey).callback.apply(this, [event, combo]); + } + }); + } + + ngOnDestroy() { + for (const hotkey of this.hotkeysList) { + this.mousetrap.unbind(hotkey.combo); + } + } + +} diff --git a/ui-ngx/src/app/shared/components/image-input.component.html b/ui-ngx/src/app/shared/components/image-input.component.html new file mode 100644 index 0000000000..bd8e24983d --- /dev/null +++ b/ui-ngx/src/app/shared/components/image-input.component.html @@ -0,0 +1,45 @@ + +
+ + +
+
+
dashboard.no-image
+ +
+
+ +
+
+ + +
+
+
+
diff --git a/ui-ngx/src/app/shared/components/image-input.component.scss b/ui-ngx/src/app/shared/components/image-input.component.scss new file mode 100644 index 0000000000..986c1253b3 --- /dev/null +++ b/ui-ngx/src/app/shared/components/image-input.component.scss @@ -0,0 +1,104 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../scss/constants"; + +$previewSize: 100px !default; + +:host { + + .tb-container { + margin-top: 0; + label.tb-title { + display: block; + padding-bottom: 8px; + } + } + + .tb-image-select-container { + position: relative; + width: 100%; + height: $previewSize; + } + + .tb-image-preview { + width: auto; + max-width: $previewSize - 2; + height: auto; + max-height: $previewSize - 2; + } + + .tb-image-preview-container { + position: relative; + float: left; + width: $previewSize; + height: $previewSize; + margin-right: 12px; + vertical-align: top; + border: solid 1px; + + div { + width: 100%; + font-size: 18px; + text-align: center; + } + + div, + .tb-image-preview { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + } + + .tb-image-clear-container { + position: relative; + float: right; + width: 48px; + height: $previewSize; + } + + .tb-image-clear-btn { + position: absolute !important; + top: 50%; + transform: translate(0%, -50%) !important; + } + + .file-input { + display: none; + } + + .tb-flow-drop { + position: relative; + height: $previewSize; + overflow: hidden; + border: dashed 2px; + + label { + display: flex; + flex-direction: column; + justify-content: center; + width: 100%; + height: 100%; + font-size: 16px; + text-align: center; + + @media #{$mat-gt-sm} { + font-size: 24px; + } + } + } +} diff --git a/ui-ngx/src/app/shared/components/image-input.component.ts b/ui-ngx/src/app/shared/components/image-input.component.ts new file mode 100644 index 0000000000..a9e73323e3 --- /dev/null +++ b/ui-ngx/src/app/shared/components/image-input.component.ts @@ -0,0 +1,137 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, Component, forwardRef, Input, OnDestroy, ViewChild } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, } from '@angular/forms'; +import { Subscription } from 'rxjs'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { FlowDirective } from '@flowjs/ngx-flow'; +import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; +import { UtilsService } from '@core/services/utils.service'; + +@Component({ + selector: 'tb-image-input', + templateUrl: './image-input.component.html', + styleUrls: ['./image-input.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ImageInputComponent), + multi: true + } + ] +}) +export class ImageInputComponent extends PageComponent implements AfterViewInit, OnDestroy, ControlValueAccessor { + + @Input() + label: string; + + private requiredValue: boolean; + + get required(): boolean { + return this.requiredValue; + } + + @Input() + set required(value: boolean) { + const newVal = coerceBooleanProperty(value); + if (this.requiredValue !== newVal) { + this.requiredValue = newVal; + } + } + + @Input() + disabled: boolean; + + @Input() + showClearButton = true; + + @Input() + showPreview = true; + + @Input() + inputId = this.utils.guid(); + + imageUrl: string; + safeImageUrl: SafeUrl; + + @ViewChild('flow', {static: true}) + flow: FlowDirective; + + autoUploadSubscription: Subscription; + + private propagateChange = null; + + constructor(protected store: Store, + private utils: UtilsService, + private sanitizer: DomSanitizer) { + super(store); + } + + ngAfterViewInit() { + this.autoUploadSubscription = this.flow.events$.subscribe(event => { + if (event.type === 'fileAdded') { + const file = (event.event[0] as flowjs.FlowFile).file; + const reader = new FileReader(); + reader.onload = (loadEvent) => { + if (typeof reader.result === 'string' && reader.result.startsWith('data:image/')) { + this.imageUrl = reader.result; + this.safeImageUrl = this.sanitizer.bypassSecurityTrustUrl(this.imageUrl); + this.updateModel(); + } + }; + reader.readAsDataURL(file); + } + }); + } + + ngOnDestroy() { + this.autoUploadSubscription.unsubscribe(); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: string): void { + this.imageUrl = value; + if (this.imageUrl) { + this.safeImageUrl = this.sanitizer.bypassSecurityTrustUrl(this.imageUrl); + } else { + this.safeImageUrl = null; + } + } + + private updateModel() { + this.propagateChange(this.imageUrl); + } + + clearImage() { + this.imageUrl = null; + this.safeImageUrl = null; + this.updateModel(); + } +} diff --git a/ui-ngx/src/app/shared/components/js-func.component.html b/ui-ngx/src/app/shared/components/js-func.component.html new file mode 100644 index 0000000000..fb38aa212d --- /dev/null +++ b/ui-ngx/src/app/shared/components/js-func.component.html @@ -0,0 +1,40 @@ + +
+
+ + + + +
+
+
+
+
+ +
+
diff --git a/ui-ngx/src/app/shared/components/js-func.component.scss b/ui-ngx/src/app/shared/components/js-func.component.scss new file mode 100644 index 0000000000..88136167b2 --- /dev/null +++ b/ui-ngx/src/app/shared/components/js-func.component.scss @@ -0,0 +1,60 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-js-func { + position: relative; + + &.tb-disabled { + color: rgba(0, 0, 0, .38); + } + + &.fill-height { + height: 100%; + } + + .tb-js-func-panel { + height: calc(100% - 80px); + margin-left: 15px; + border: 1px solid #c0c0c0; + + #tb-javascript-input { + width: 100%; + min-width: 200px; + height: 100%; + + &:not(.fill-height) { + min-height: 200px; + } + } + } + + .tb-js-func-toolbar { + button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 { + align-items: center; + vertical-align: middle; + min-width: 32px; + min-height: 15px; + padding: 4px; + margin: 0; + font-size: .8rem; + line-height: 15px; + color: #7b7b7b; + background: rgba(220, 220, 220, .35); + &:not(:last-child) { + margin-right: 4px; + } + } + } +} diff --git a/ui-ngx/src/app/shared/components/js-func.component.ts b/ui-ngx/src/app/shared/components/js-func.component.ts new file mode 100644 index 0000000000..5515ba055f --- /dev/null +++ b/ui-ngx/src/app/shared/components/js-func.component.ts @@ -0,0 +1,352 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + Component, + ElementRef, + forwardRef, + Input, + OnDestroy, + OnInit, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'; +import * as ace from 'ace-builds'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { UtilsService } from '@core/services/utils.service'; +import { guid, isUndefined } from '@app/core/utils'; +import { TranslateService } from '@ngx-translate/core'; +import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; +import { ResizeObserver } from '@juggle/resize-observer'; +import { TbEditorCompleter } from '@shared/models/ace/completion.models'; + +@Component({ + selector: 'tb-js-func', + templateUrl: './js-func.component.html', + styleUrls: ['./js-func.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => JsFuncComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => JsFuncComponent), + multi: true, + } + ], + encapsulation: ViewEncapsulation.None +}) +export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator { + + @ViewChild('javascriptEditor', {static: true}) + javascriptEditorElmRef: ElementRef; + + private jsEditor: ace.Ace.Editor; + private editorsResizeCaf: CancelAnimationFrame; + private editorResize$: ResizeObserver; + private ignoreChange = false; + + toastTargetId = `jsFuncEditor-${guid()}`; + + @Input() functionName: string; + + @Input() functionArgs: Array; + + @Input() validationArgs: Array; + + @Input() resultType: string; + + @Input() disabled: boolean; + + @Input() fillHeight: boolean; + + @Input() editorCompleter: TbEditorCompleter; + + private noValidateValue: boolean; + get noValidate(): boolean { + return this.noValidateValue; + } + @Input() + set noValidate(value: boolean) { + this.noValidateValue = coerceBooleanProperty(value); + } + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + functionArgsString = ''; + + fullscreen = false; + + modelValue: string; + + functionValid = true; + + validationError: string; + + errorShowed = false; + + errorMarkers: number[] = []; + errorAnnotationId = -1; + + private propagateChange = null; + + constructor(public elementRef: ElementRef, + private utils: UtilsService, + private translate: TranslateService, + protected store: Store, + private raf: RafService) { + } + + ngOnInit(): void { + if (!this.resultType || this.resultType.length === 0) { + this.resultType = 'nocheck'; + } + if (this.functionArgs) { + this.functionArgs.forEach((functionArg) => { + if (this.functionArgsString.length > 0) { + this.functionArgsString += ', '; + } + this.functionArgsString += functionArg; + }); + } + const editorElement = this.javascriptEditorElmRef.nativeElement; + let editorOptions: Partial = { + mode: 'ace/mode/javascript', + showGutter: true, + showPrintMargin: true, + readOnly: this.disabled + }; + + const advancedOptions = { + enableSnippets: true, + enableBasicAutocompletion: true, + enableLiveAutocompletion: true + }; + + editorOptions = {...editorOptions, ...advancedOptions}; + this.jsEditor = ace.edit(editorElement, editorOptions); + this.jsEditor.session.setUseWrapMode(true); + this.jsEditor.setValue(this.modelValue ? this.modelValue : '', -1); + this.jsEditor.on('change', () => { + if (!this.ignoreChange) { + this.cleanupJsErrors(); + this.updateView(); + } + }); + if (this.editorCompleter) { + this.jsEditor.completers = [this.editorCompleter, ...(this.jsEditor.completers || [])]; + } + this.editorResize$ = new ResizeObserver(() => { + this.onAceEditorResize(); + }); + this.editorResize$.observe(editorElement); + } + + ngOnDestroy(): void { + if (this.editorResize$) { + this.editorResize$.disconnect(); + } + } + + private onAceEditorResize() { + if (this.editorsResizeCaf) { + this.editorsResizeCaf(); + this.editorsResizeCaf = null; + } + this.editorsResizeCaf = this.raf.raf(() => { + this.jsEditor.resize(); + this.jsEditor.renderer.updateFull(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.jsEditor) { + this.jsEditor.setReadOnly(this.disabled); + } + } + + public validate(c: FormControl) { + return (this.functionValid) ? null : { + jsFunc: { + valid: false, + }, + }; + } + + beautifyJs() { + const res = js_beautify(this.modelValue, {indent_size: 4, wrap_line_length: 60}); + this.jsEditor.setValue(res ? res : '', -1); + this.updateView(); + } + + validateOnSubmit(): void { + if (!this.disabled) { + this.cleanupJsErrors(); + this.functionValid = this.validateJsFunc(); + if (!this.functionValid) { + this.propagateChange(this.modelValue); + this.store.dispatch(new ActionNotificationShow( + { + message: this.validationError, + type: 'error', + target: this.toastTargetId, + verticalPosition: 'bottom', + horizontalPosition: 'left' + })); + this.errorShowed = true; + } + } + } + + private validateJsFunc(): boolean { + try { + const toValidate = new Function(this.functionArgsString, this.modelValue); + if (this.noValidate) { + return true; + } + if (this.validationArgs) { + let res: any; + let validationError: any; + for (const validationArg of this.validationArgs) { + try { + res = toValidate.apply(this, validationArg); + validationError = null; + break; + } catch (e) { + validationError = e; + } + } + if (validationError) { + throw validationError; + } + if (this.resultType !== 'nocheck') { + if (this.resultType === 'any') { + if (isUndefined(res)) { + this.validationError = this.translate.instant('js-func.no-return-error'); + return false; + } + } else { + const resType = typeof res; + if (resType !== this.resultType) { + this.validationError = this.translate.instant('js-func.return-type-mismatch', {type: this.resultType}); + return false; + } + } + } + return true; + } else { + return true; + } + } catch (e) { + const details = this.utils.parseException(e); + let errorInfo = 'Error:'; + if (details.name) { + errorInfo += ' ' + details.name + ':'; + } + if (details.message) { + errorInfo += ' ' + details.message; + } + if (details.lineNumber) { + errorInfo += '
Line ' + details.lineNumber; + if (details.columnNumber) { + errorInfo += ' column ' + details.columnNumber; + } + errorInfo += ' of script.'; + } + this.validationError = errorInfo; + if (details.lineNumber) { + const line = details.lineNumber - 1; + let column = 0; + if (details.columnNumber) { + column = details.columnNumber; + } + const errorMarkerId = this.jsEditor.session.addMarker(new ace.Range(line, 0, line, Infinity), + 'ace_active-line', 'screenLine'); + this.errorMarkers.push(errorMarkerId); + const annotations = this.jsEditor.session.getAnnotations(); + const errorAnnotation: ace.Ace.Annotation = { + row: line, + column, + text: details.message, + type: 'error' + }; + this.errorAnnotationId = annotations.push(errorAnnotation) - 1; + this.jsEditor.session.setAnnotations(annotations); + } + return false; + } + } + + private cleanupJsErrors(): void { + if (this.errorShowed) { + this.store.dispatch(new ActionNotificationHide( + { + target: this.toastTargetId + })); + this.errorShowed = false; + } + this.errorMarkers.forEach((errorMarker) => { + this.jsEditor.session.removeMarker(errorMarker); + }); + this.errorMarkers.length = 0; + if (this.errorAnnotationId > -1) { + const annotations = this.jsEditor.session.getAnnotations(); + annotations.splice(this.errorAnnotationId, 1); + this.jsEditor.session.setAnnotations(annotations); + this.errorAnnotationId = -1; + } + } + + writeValue(value: string): void { + this.modelValue = value; + if (this.jsEditor) { + this.ignoreChange = true; + this.jsEditor.setValue(this.modelValue ? this.modelValue : '', -1); + this.ignoreChange = false; + } + } + + updateView() { + const editorValue = this.jsEditor.getValue(); + if (this.modelValue !== editorValue) { + this.modelValue = editorValue; + this.functionValid = true; + this.propagateChange(this.modelValue); + } + } + +} diff --git a/ui-ngx/src/app/shared/components/json-content.component.html b/ui-ngx/src/app/shared/components/json-content.component.html new file mode 100644 index 0000000000..9d9ab5f2a1 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-content.component.html @@ -0,0 +1,43 @@ + +
+
+ + + + + +
+
+
+
+
diff --git a/ui-ngx/src/app/shared/components/json-content.component.scss b/ui-ngx/src/app/shared/components/json-content.component.scss new file mode 100644 index 0000000000..61146e32e2 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-content.component.scss @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + position: relative; + + .fill-height { + height: 100%; + } +} + +.tb-json-content-toolbar { + button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 { + align-items: center; + vertical-align: middle; + min-width: 32px; + min-height: 15px; + padding: 4px; + margin: 0; + font-size: .8rem; + line-height: 15px; + color: #7b7b7b; + background: rgba(220, 220, 220, .35); + &:not(:last-child) { + margin-right: 4px; + } + } +} + +.tb-json-content-panel { + height: 100%; + margin-left: 15px; + border: 1px solid #c0c0c0; + + #tb-json-input { + width: 100%; + min-width: 200px; + height: 100%; + + &:not(.fill-height) { + min-height: 200px; + } + } +} diff --git a/ui-ngx/src/app/shared/components/json-content.component.ts b/ui-ngx/src/app/shared/components/json-content.component.ts new file mode 100644 index 0000000000..3339863cd2 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-content.component.ts @@ -0,0 +1,302 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + Component, + ElementRef, + forwardRef, + Input, + OnChanges, + OnDestroy, + OnInit, + SimpleChanges, + ViewChild +} from '@angular/core'; +import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'; +import * as ace from 'ace-builds'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ContentType, contentTypesMap } from '@shared/models/constants'; +import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; +import { guid } from '@core/utils'; +import { ResizeObserver } from '@juggle/resize-observer'; + +@Component({ + selector: 'tb-json-content', + templateUrl: './json-content.component.html', + styleUrls: ['./json-content.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => JsonContentComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => JsonContentComponent), + multi: true, + } + ] +}) +export class JsonContentComponent implements OnInit, ControlValueAccessor, Validator, OnChanges, OnDestroy { + + @ViewChild('jsonEditor', {static: true}) + jsonEditorElmRef: ElementRef; + + private jsonEditor: ace.Ace.Editor; + private editorsResizeCaf: CancelAnimationFrame; + private editorResize$: ResizeObserver; + private ignoreChange = false; + + toastTargetId = `jsonContentEditor-${guid()}`; + + @Input() label: string; + + @Input() contentType: ContentType; + + @Input() disabled: boolean; + + @Input() fillHeight: boolean; + + @Input() editorStyle: {[klass: string]: any}; + + private readonlyValue: boolean; + get readonly(): boolean { + return this.readonlyValue; + } + @Input() + set readonly(value: boolean) { + this.readonlyValue = coerceBooleanProperty(value); + } + + private validateContentValue: boolean; + get validateContent(): boolean { + return this.validateContentValue; + } + @Input() + set validateContent(value: boolean) { + this.validateContentValue = coerceBooleanProperty(value); + } + + private validateOnChangeValue: boolean; + get validateOnChange(): boolean { + return this.validateOnChangeValue; + } + @Input() + set validateOnChange(value: boolean) { + this.validateOnChangeValue = coerceBooleanProperty(value); + } + + fullscreen = false; + + contentBody: string; + + contentValid: boolean; + + errorShowed = false; + + private propagateChange = null; + + constructor(public elementRef: ElementRef, + protected store: Store, + private raf: RafService) { + } + + ngOnInit(): void { + const editorElement = this.jsonEditorElmRef.nativeElement; + let mode = 'text'; + if (this.contentType) { + mode = contentTypesMap.get(this.contentType).code; + } + let editorOptions: Partial = { + mode: `ace/mode/${mode}`, + showGutter: true, + showPrintMargin: false, + readOnly: this.disabled || this.readonly + }; + + const advancedOptions = { + enableSnippets: true, + enableBasicAutocompletion: true, + enableLiveAutocompletion: true + }; + + editorOptions = {...editorOptions, ...advancedOptions}; + this.jsonEditor = ace.edit(editorElement, editorOptions); + this.jsonEditor.session.setUseWrapMode(true); + this.jsonEditor.setValue(this.contentBody ? this.contentBody : '', -1); + this.jsonEditor.on('change', () => { + if (!this.ignoreChange) { + this.cleanupJsonErrors(); + this.updateView(); + } + }); + this.jsonEditor.on('blur', () => { + this.contentValid = !this.validateContent || this.doValidate(true); + }); + this.editorResize$ = new ResizeObserver(() => { + this.onAceEditorResize(); + }); + this.editorResize$.observe(editorElement); + } + + ngOnDestroy(): void { + if (this.editorResize$) { + this.editorResize$.disconnect(); + } + } + + private onAceEditorResize() { + if (this.editorsResizeCaf) { + this.editorsResizeCaf(); + this.editorsResizeCaf = null; + } + this.editorsResizeCaf = this.raf.raf(() => { + this.jsonEditor.resize(); + this.jsonEditor.renderer.updateFull(); + }); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'contentType') { + if (this.jsonEditor) { + let mode = 'text'; + if (this.contentType) { + mode = contentTypesMap.get(this.contentType).code; + } + this.jsonEditor.session.setMode(`ace/mode/${mode}`); + } + } + } + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.jsonEditor) { + this.jsonEditor.setReadOnly(this.disabled || this.readonly); + } + } + + public validate(c: FormControl) { + return (this.contentValid) ? null : { + contentBody: { + valid: false, + }, + }; + } + + validateOnSubmit(): void { + if (!this.disabled && !this.readonly) { + this.cleanupJsonErrors(); + this.contentValid = true; + this.propagateChange(this.contentBody); + this.contentValid = this.doValidate(true); + this.propagateChange(this.contentBody); + } + } + + private doValidate(showErrorToast = false): boolean { + try { + if (this.validateContent && this.contentType === ContentType.JSON) { + JSON.parse(this.contentBody); + } + return true; + } catch (ex) { + if (showErrorToast) { + let errorInfo = 'Error:'; + if (ex.name) { + errorInfo += ' ' + ex.name + ':'; + } + if (ex.message) { + errorInfo += ' ' + ex.message; + } + this.store.dispatch(new ActionNotificationShow( + { + message: errorInfo, + type: 'error', + target: this.toastTargetId, + verticalPosition: 'bottom', + horizontalPosition: 'left' + })); + this.errorShowed = true; + } + return false; + } + } + + cleanupJsonErrors(): void { + if (this.errorShowed) { + this.store.dispatch(new ActionNotificationHide( + { + target: this.toastTargetId + })); + this.errorShowed = false; + } + } + + writeValue(value: string): void { + this.contentBody = value; + this.contentValid = true; + if (this.jsonEditor) { + this.ignoreChange = true; + this.jsonEditor.setValue(this.contentBody ? this.contentBody : '', -1); + this.ignoreChange = false; + } + } + + updateView() { + const editorValue = this.jsonEditor.getValue(); + if (this.contentBody !== editorValue) { + this.contentBody = editorValue; + this.contentValid = !this.validateOnChange || this.doValidate(); + this.propagateChange(this.contentBody); + } + } + + beautifyJSON() { + const res = js_beautify(this.contentBody, {indent_size: 4, wrap_line_length: 60}); + this.jsonEditor.setValue(res ? res : '', -1); + this.updateView(); + } + + minifyJSON() { + const res = JSON.stringify(this.contentBody); + this.jsonEditor.setValue(res ? res : '', -1); + this.updateView(); + } + + onFullscreen() { + if (this.jsonEditor) { + setTimeout(() => { + this.jsonEditor.resize(); + }, 0); + } + } + +} diff --git a/ui-ngx/src/app/shared/components/json-form/json-form-component.models.ts b/ui-ngx/src/app/shared/components/json-form/json-form-component.models.ts new file mode 100644 index 0000000000..2f54b91cb2 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-form/json-form-component.models.ts @@ -0,0 +1,22 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + + +import { JsonSettingsSchema } from '@shared/models/widget.models'; + +export interface JsonFormComponentData extends JsonSettingsSchema { + model?: any; +} diff --git a/ui-ngx/src/app/shared/components/json-form/json-form.component.html b/ui-ngx/src/app/shared/components/json-form/json-form.component.html new file mode 100644 index 0000000000..a469a650db --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-form/json-form.component.html @@ -0,0 +1,22 @@ + +
+
+
diff --git a/ui-ngx/src/app/shared/components/json-form/json-form.component.scss b/ui-ngx/src/app/shared/components/json-form/json-form.component.scss new file mode 100644 index 0000000000..b373862dc3 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-form/json-form.component.scss @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-json-form { + padding: 12px; + padding-bottom: 14px !important; + overflow: auto; +} diff --git a/ui-ngx/src/app/shared/components/json-form/json-form.component.ts b/ui-ngx/src/app/shared/components/json-form/json-form.component.ts new file mode 100644 index 0000000000..ecd249af60 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-form/json-form.component.ts @@ -0,0 +1,269 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + Component, + ElementRef, + forwardRef, + Input, + OnChanges, + OnDestroy, + OnInit, + SimpleChanges, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { deepClone, isString } from '@app/core/utils'; +import { TranslateService } from '@ngx-translate/core'; +import { JsonFormProps } from './react/json-form.models'; +import inspector from 'schema-inspector'; +import * as tinycolor_ from 'tinycolor2'; +import { DialogService } from '@app/core/services/dialog.service'; +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import ReactSchemaForm from './react/json-form-react'; +import JsonFormUtils from './react/json-form-utils'; +import { JsonFormComponentData } from './json-form-component.models'; +import { GroupInfo } from '@shared/models/widget.models'; + +const tinycolor = tinycolor_; + +@Component({ + selector: 'tb-json-form', + templateUrl: './json-form.component.html', + styleUrls: ['./json-form.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => JsonFormComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => JsonFormComponent), + multi: true, + } + ], + encapsulation: ViewEncapsulation.None +}) +export class JsonFormComponent implements OnInit, ControlValueAccessor, Validator, OnChanges, OnDestroy { + + @ViewChild('reactRoot', {static: true}) + reactRootElmRef: ElementRef; + + private readonlyValue: boolean; + get readonly(): boolean { + return this.readonlyValue; + } + @Input() + set required(value: boolean) { + this.readonlyValue = coerceBooleanProperty(value); + } + + formProps: JsonFormProps = { + isFullscreen: false, + option: { + formDefaults: { + startEmpty: true + } + }, + onModelChange: this.onModelChange.bind(this), + onColorClick: this.onColorClick.bind(this), + onIconClick: this.onIconClick.bind(this), + onToggleFullscreen: this.onToggleFullscreen.bind(this) + }; + + data: JsonFormComponentData; + + model: any; + schema: any; + form: any; + groupInfoes: GroupInfo[]; + + isModelValid = true; + + isFullscreen = false; + targetFullscreenElement: HTMLElement; + fullscreenFinishFn: () => void; + + private propagateChange = null; + private propagateChangePending = false; + private writingValue = false; + private updateViewPending = false; + + constructor(public elementRef: ElementRef, + private translate: TranslateService, + private dialogs: DialogService, + protected store: Store) { + } + + ngOnInit(): void { + } + + ngOnDestroy(): void { + this.destroyReactSchemaForm(); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + if (this.propagateChangePending) { + this.propagateChangePending = false; + setTimeout(() => { + this.propagateChange(this.data); + }, 0); + } + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + } + + public validate(c: FormControl) { + return this.isModelValid ? null : { + modelValid: false + }; + } + + writeValue(data: JsonFormComponentData): void { + this.writingValue = true; + this.data = data; + this.schema = this.data && this.data.schema ? deepClone(this.data.schema) : { + type: 'object' + }; + this.schema.strict = true; + this.form = this.data && this.data.form ? deepClone(this.data.form) : [ '*' ]; + this.groupInfoes = this.data && this.data.groupInfoes ? deepClone(this.data.groupInfoes) : []; + this.model = this.data && this.data.model || {}; + this.model = inspector.sanitize(this.schema, this.model).data; + this.updateAndRender(); + this.isModelValid = this.validateModel(); + this.writingValue = false; + if (!this.isModelValid || this.updateViewPending) { + this.updateView(); + } +} + + updateView() { + if (!this.writingValue) { + this.updateViewPending = false; + if (this.data) { + this.data.model = this.model; + if (this.propagateChange) { + try { + this.propagateChange(this.data); + } catch (e) { + this.propagateChangePending = true; + } + } else { + this.propagateChangePending = true; + } + } + } else { + this.updateViewPending = true; + } + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'readonly') { + this.updateAndRender(); + } + } + } + } + + private onModelChange(key: (string | number)[], val: any, forceUpdate = false) { + if (isString(val) && val === '') { + val = undefined; + } + if (JsonFormUtils.updateValue(key, this.model, val) || forceUpdate) { + this.isModelValid = this.validateModel(); + this.updateView(); + } + } + + private onColorClick(key: (string | number)[], + val: tinycolor.ColorFormats.RGBA, + colorSelectedFn: (color: tinycolor.ColorFormats.RGBA) => void) { + this.dialogs.colorPicker(tinycolor(val).toRgbString()).subscribe((color) => { + if (colorSelectedFn) { + colorSelectedFn(tinycolor(color).toRgb()); + } + }); + } + + private onIconClick(key: (string | number)[], + val: string, + iconSelectedFn: (icon: string) => void) { + this.dialogs.materialIconPicker(val).subscribe((icon) => { + if (icon && iconSelectedFn) { + iconSelectedFn(icon); + } + }); + } + + private onToggleFullscreen(element: HTMLElement, fullscreenFinishFn?: () => void) { + this.targetFullscreenElement = element; + this.isFullscreen = !this.isFullscreen; + this.fullscreenFinishFn = fullscreenFinishFn; + } + + onFullscreenChanged(fullscreen: boolean) { + this.formProps.isFullscreen = fullscreen; + this.renderReactSchemaForm(false); + if (this.fullscreenFinishFn) { + this.fullscreenFinishFn(); + this.fullscreenFinishFn = null; + } + } + + private updateAndRender() { + + this.formProps.option.formDefaults.readonly = this.readonly; + this.formProps.schema = this.schema; + this.formProps.form = this.form; + this.formProps.groupInfoes = this.groupInfoes; + this.formProps.model = this.model; + this.renderReactSchemaForm(); + } + + private renderReactSchemaForm(destroy: boolean = true) { + if (destroy) { + this.destroyReactSchemaForm(); + } + ReactDOM.render(React.createElement(ReactSchemaForm, this.formProps), this.reactRootElmRef.nativeElement); + } + + private destroyReactSchemaForm() { + ReactDOM.unmountComponentAtNode(this.reactRootElmRef.nativeElement); + } + + private validateModel(): boolean { + if (this.schema && this.model) { + return JsonFormUtils.validateBySchema(this.schema, this.model).valid; + } + return true; + } +} + diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-ace-editor.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-ace-editor.tsx new file mode 100644 index 0000000000..7a631623a0 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-ace-editor.tsx @@ -0,0 +1,182 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as React from 'react'; +import ThingsboardBaseComponent from './json-form-base-component'; +import reactCSS from 'reactcss'; +import ReactAce from 'react-ace'; +import Button from '@material-ui/core/Button'; +import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; +import { IEditorProps } from 'react-ace/src/types'; + +interface ThingsboardAceEditorProps extends JsonFormFieldProps { + mode: string; + onTidy: (value: string) => string; +} + +interface ThingsboardAceEditorState extends JsonFormFieldState { + isFull: boolean; + focused: boolean; +} + +class ThingsboardAceEditor extends React.Component { + + hostElement: HTMLElement; + private aceEditor: IEditorProps; + + constructor(props) { + super(props); + this.onValueChanged = this.onValueChanged.bind(this); + this.onBlur = this.onBlur.bind(this); + this.onFocus = this.onFocus.bind(this); + this.onTidy = this.onTidy.bind(this); + this.onLoad = this.onLoad.bind(this); + this.onToggleFull = this.onToggleFull.bind(this); + const value = props.value ? props.value + '' : ''; + this.state = { + isFull: false, + value, + focused: false + }; + } + + onValueChanged(value) { + this.setState({ + value + }); + this.props.onChangeValidate({ + target: { + value + } + }); + } + + onBlur() { + this.setState({ focused: false }); + } + + onFocus() { + this.setState({ focused: true }); + } + + onTidy() { + if (!this.props.form.readonly) { + let value = this.state.value; + value = this.props.onTidy(value); + this.setState({ + value + }); + this.props.onChangeValidate({ + target: { + value + } + }); + } + } + + onLoad(editor: IEditorProps) { + this.aceEditor = editor; + } + + onToggleFull() { + this.setState({ isFull: !this.state.isFull }); + this.props.onToggleFullscreen(this.hostElement, () => { + if (this.aceEditor) { + this.aceEditor.resize(); + this.aceEditor.renderer.updateFull(); + } + }); + } + + componentDidUpdate() { + } + + render() { + + const styles = reactCSS({ + default: { + tidyButtonStyle: { + color: '#7B7B7B', + minWidth: '32px', + minHeight: '15px', + lineHeight: '15px', + fontSize: '0.800rem', + margin: '0', + padding: '4px', + height: '23px', + borderRadius: '5px', + marginLeft: '5px' + } + } + }); + + let labelClass = 'tb-label'; + if (this.props.form.required) { + labelClass += ' tb-required'; + } + if (this.props.form.readonly) { + labelClass += ' tb-readonly'; + } + if (this.state.focused) { + labelClass += ' tb-focused'; + } + let containerClass = 'tb-container'; + const style = this.props.form.style || {width: '100%'}; + if (this.state.isFull) { + containerClass += ' fullscreen-form-field'; + } + return ( +
+
(this.hostElement = c)}> +
+ +
+
+ + + +
+ +
+
{this.props.error}
+
+
+
+ ); + } +} + +export default ThingsboardBaseComponent(ThingsboardAceEditor); diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-array.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-array.tsx new file mode 100644 index 0000000000..02cbf43787 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-array.tsx @@ -0,0 +1,178 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as React from 'react'; +import JsonFormUtils from './json-form-utils'; +import ThingsboardBaseComponent from './json-form-base-component'; +import Button from '@material-ui/core/Button'; +import _ from 'lodash'; +import IconButton from '@material-ui/core/IconButton'; +import Clear from '@material-ui/icons/Clear'; +import Add from '@material-ui/icons/Add'; +import Tooltip from '@material-ui/core/Tooltip'; +import { + JsonFormData, + JsonFormFieldProps, + JsonFormFieldState +} from '@shared/components/json-form/react/json-form.models'; + +interface ThingsboardArrayState extends JsonFormFieldState { + model: any[]; + keys: number[]; +} + +class ThingsboardArray extends React.Component { + + constructor(props) { + super(props); + this.onAppend = this.onAppend.bind(this); + this.onDelete = this.onDelete.bind(this); + const model = JsonFormUtils.selectOrSet(this.props.form.key, this.props.model) || []; + const keys: number[] = []; + for (let i = 0; i < model.length; i++) { + keys.push(i); + } + this.state = { + model, + keys + }; + } + + componentDidMount() { + if (this.props.form.startEmpty !== true && this.state.model.length === 0) { + this.onAppend(); + } + } + + onAppend() { + let empty; + if (this.props.form && this.props.form.schema && this.props.form.schema.items) { + const items = this.props.form.schema.items; + if (items.type && items.type.indexOf('object') !== -1) { + empty = {}; + if (!this.props.options || this.props.options.setSchemaDefaults !== false) { + empty = typeof items.default !== 'undefined' ? items.default : empty; + if (empty) { + JsonFormUtils.traverseSchema(items, (prop, path) => { + if (typeof prop.default !== 'undefined') { + JsonFormUtils.selectOrSet(path, empty, prop.default); + } + }); + } + } + } else if (items.type && items.type.indexOf('array') !== -1) { + empty = []; + if (!this.props.options || this.props.options.setSchemaDefaults !== false) { + empty = items.default || empty; + } + } else { + if (!this.props.options || this.props.options.setSchemaDefaults !== false) { + empty = items.default || empty; + } + } + } + const newModel = this.state.model; + newModel.push(empty); + const newKeys = this.state.keys; + let key = 0; + if (newKeys.length > 0) { + key = newKeys[newKeys.length - 1] + 1; + } + newKeys.push(key); + this.setState({ + model: newModel, + keys: newKeys + } + ); + this.props.onChangeValidate(this.state.model, true); + } + + onDelete(index: number) { + const newModel = this.state.model; + newModel.splice(index, 1); + const newKeys = this.state.keys; + newKeys.splice(index, 1); + this.setState( + { + model: newModel, + keys: newKeys + } + ); + this.props.onChangeValidate(this.state.model, true); + } + + setIndex(index: number) { + return (form: JsonFormData) => { + if (form.key) { + form.key[form.key.indexOf('')] = index; + } + }; + } + + copyWithIndex(form: JsonFormData, index: number): JsonFormData { + const copy: JsonFormData = _.cloneDeep(form); + copy.arrayIndex = index; + JsonFormUtils.traverseForm(copy, this.setIndex(index)); + return copy; + } + + render() { + const arrays = []; + const fields = []; + const model = this.state.model; + const keys = this.state.keys; + const items = this.props.form.items; + for (let i = 0; i < model.length; i++ ) { + let removeButton: JSX.Element = null; + if (!this.props.form.readonly) { + const boundOnDelete = this.onDelete.bind(this, i); + removeButton = ; + } + const forms = (this.props.form.items as JsonFormData[]).map((form, index) => { + const copy = this.copyWithIndex(form, i); + return this.props.builder(copy, this.props.model, index, this.props.onChange, + this.props.onColorClick, this.props.onIconClick, this.props.onToggleFullscreen, this.props.mapper); + }); + arrays.push( +
  • + {removeButton} + {forms} +
  • + ); + } + let addButton: JSX.Element = null; + if (!this.props.form.readonly) { + addButton = ; + } + + return ( +
    +
    +
    {this.props.form.title}
    +
      + {arrays} +
    +
    + {addButton} +
    + ); + } +} + +export default ThingsboardBaseComponent(ThingsboardArray); diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-base-component.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-base-component.tsx new file mode 100644 index 0000000000..677544a764 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-base-component.tsx @@ -0,0 +1,119 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as React from 'react'; +import JsonFormUtils from './json-form-utils'; +import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; + +export default ThingsboardBaseComponent => class

    + extends React.Component { + + constructor(props) { + super(props); + this.onChangeValidate = this.onChangeValidate.bind(this); + const value = this.defaultValue(); + const validationResult = JsonFormUtils.validate(this.props.form, value); + this.state = { + value, + valid: !!(validationResult.valid || !value), + error: !validationResult.valid && value ? validationResult.error.message : null + }; + } + + componentDidMount() { + if (typeof this.state.value !== 'undefined') { + this.props.onChange(this.props.form.key, this.state.value); + } + } + + onChangeValidate(e, forceUpdate?: boolean) { + let value = null; + if (this.props.form.schema.type === 'integer' || this.props.form.schema.type === 'number') { + if (e.target.value === null || e.target.value === '') { + value = undefined; + } else if (e.target.value.indexOf('.') === -1) { + value = parseInt(e.target.value, 10); + } else { + value = parseFloat(e.target.value); + } + } else if (this.props.form.schema.type === 'boolean') { + value = e.target.checked; + } else if (this.props.form.schema.type === 'date' || this.props.form.schema.type === 'array') { + value = e; + } else { // string + value = e.target.value; + } + const validationResult = JsonFormUtils.validate(this.props.form, value); + this.setState({ + value, + valid: validationResult.valid, + error: validationResult.valid ? null : validationResult.error.message + }); + this.props.onChange(this.props.form.key, value, forceUpdate); + } + + defaultValue() { + let value = JsonFormUtils.selectOrSet(this.props.form.key, this.props.model); + if (this.props.form.schema.type === 'boolean') { + if (typeof value !== 'boolean' && this.props.form.default) { + value = this.props.form.default; + } + if (typeof value !== 'boolean' && this.props.form.schema && this.props.form.schema.default) { + value = this.props.form.schema.default; + } + if (typeof value !== 'boolean' && + this.props.form.schema && + this.props.form.required) { + value = false; + } + } else if (this.props.form.schema.type === 'integer' || this.props.form.schema.type === 'number') { + if (typeof value !== 'number' && this.props.form.default) { + value = this.props.form.default; + } + if (typeof value !== 'number' && this.props.form.schema && this.props.form.schema.default) { + value = this.props.form.schema.default; + } + if (typeof value !== 'number' && this.props.form.titleMap && this.props.form.titleMap[0].value) { + value = this.props.form.titleMap[0].value; + } + if (value && typeof value === 'string') { + if (value.indexOf('.') === -1) { + value = parseInt(value, 10); + } else { + value = parseFloat(value); + } + } + } else { + if (!value && this.props.form.default) { + value = this.props.form.default; + } + if (!value && this.props.form.schema && this.props.form.schema.default) { + value = this.props.form.schema.default; + } + if (!value && this.props.form.titleMap && this.props.form.titleMap[0].value) { + value = this.props.form.titleMap[0].value; + } + } + return value; + } + + render() { + if (this.props.form && this.props.form.schema) { + return ; + } else { + return

    ; + } + } +}; diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-checkbox.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-checkbox.tsx new file mode 100644 index 0000000000..1ff412b573 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-checkbox.tsx @@ -0,0 +1,45 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as React from 'react'; +import ThingsboardBaseComponent from './json-form-base-component'; +import Checkbox from '@material-ui/core/Checkbox'; +import { JsonFormFieldProps, JsonFormFieldState } from './json-form.models.js'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; + +class ThingsboardCheckbox extends React.Component { + render() { + return ( +
    + { + this.props.onChangeValidate(e); + }} + /> + } + label={this.props.form.title} + /> +
    + ); + } +} + +export default ThingsboardBaseComponent(ThingsboardCheckbox); diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-color.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-color.tsx new file mode 100644 index 0000000000..a3be69b6a5 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-color.tsx @@ -0,0 +1,186 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import ThingsboardBaseComponent from './json-form-base-component'; +import reactCSS from 'reactcss'; +import * as tinycolor_ from 'tinycolor2'; +import TextField from '@material-ui/core/TextField'; +import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; +import IconButton from '@material-ui/core/IconButton'; +import Clear from '@material-ui/icons/Clear'; +import Tooltip from '@material-ui/core/Tooltip'; + +const tinycolor = tinycolor_; + +interface ThingsboardColorState extends JsonFormFieldState { + color: tinycolor.ColorFormats.RGBA | null; + focused: boolean; +} + +class ThingsboardColor extends React.Component { + + constructor(props) { + super(props); + this.onBlur = this.onBlur.bind(this); + this.onFocus = this.onFocus.bind(this); + this.onValueChanged = this.onValueChanged.bind(this); + this.onSwatchClick = this.onSwatchClick.bind(this); + this.onClear = this.onClear.bind(this); + const value = props.value ? props.value + '' : null; + const color = value != null ? tinycolor(value).toRgb() : null; + this.state = { + color, + focused: false + }; + } + + onBlur() { + this.setState({focused: false}); + } + + onFocus() { + this.setState({focused: true}); + } + + componentDidMount() { + const node = ReactDOM.findDOMNode(this); + const colContainer = $(node).children('#color-container'); + colContainer.click((event) => { + if (!this.props.form.readonly) { + this.onSwatchClick(event); + } + }); + } + + componentWillUnmount() { + const node = ReactDOM.findDOMNode(this); + const colContainer = $(node).children('#color-container'); + colContainer.off( 'click' ); + } + + onValueChanged(value: tinycolor.ColorFormats.RGBA | null) { + let color: tinycolor.Instance = null; + if (value != null) { + color = tinycolor(value); + } + this.setState({ + color: value + }); + let colorValue = ''; + if (color != null && color.getAlpha() !== 1) { + colorValue = color.toRgbString(); + } else if (color != null) { + colorValue = color.toHexString(); + } + this.props.onChangeValidate({ + target: { + value: colorValue + } + }); + } + + onSwatchClick(event) { + this.props.onColorClick(this.props.form.key, this.state.color, + (color) => { + this.onValueChanged(color); + } + ); + } + + onClear(event) { + if (event) { + event.stopPropagation(); + } + this.onValueChanged(null); + } + + render() { + + let background = 'rgba(0,0,0,0)'; + if (this.state.color != null) { + background = `rgba(${ this.state.color.r }, ${ this.state.color.g }, ${ this.state.color.b }, ${ this.state.color.a })`; + } + + const styles = reactCSS({ + default: { + color: { + background: `${ background }` + }, + swatch: { + display: 'inline-block', + marginRight: '10px', + marginTop: 'auto', + marginBottom: 'auto', + cursor: 'pointer', + opacity: `${ this.props.form.readonly ? '0.6' : '1' }` + }, + swatchText: { + width: '100%' + }, + container: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center' + }, + colorContainer: { + display: 'flex', + width: '100%' + } + }, + }); + + let fieldClass = 'tb-field'; + if (this.props.form.required) { + fieldClass += ' tb-required'; + } + if (this.props.form.readonly) { + fieldClass += ' tb-readonly'; + } + if (this.state.focused) { + fieldClass += ' tb-focused'; + } + + let stringColor = ''; + if (this.state.color != null) { + const color = tinycolor(this.state.color); + stringColor = color.toRgbString(); + } + + return ( +
    +
    +
    +
    +
    + +
    + +
    + ); + } +} + +export default ThingsboardBaseComponent(ThingsboardColor); diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-css.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-css.tsx new file mode 100644 index 0000000000..a5107ad4db --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-css.tsx @@ -0,0 +1,39 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as React from 'react'; +import ThingsboardAceEditor from './json-form-ace-editor'; +import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; +import { css_beautify } from 'js-beautify'; + +class ThingsboardCss extends React.Component { + + constructor(props) { + super(props); + this.onTidyCss = this.onTidyCss.bind(this); + } + + onTidyCss(css: string): string { + return css_beautify(css, {indent_size: 4}); + } + + render() { + return ( + + ); + } +} + +export default ThingsboardCss; diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-date.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-date.tsx new file mode 100644 index 0000000000..b33829ca11 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-date.tsx @@ -0,0 +1,79 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as React from 'react'; +import ThingsboardBaseComponent from './json-form-base-component'; +import DateFnsUtils from '@date-io/date-fns'; +import { KeyboardDatePicker, MuiPickersUtilsProvider } from '@material-ui/pickers'; +import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; + +interface ThingsboardDateState extends JsonFormFieldState { + currentValue: Date | null; +} + +class ThingsboardDate extends React.Component { + + constructor(props) { + super(props); + this.onDatePicked = this.onDatePicked.bind(this); + let value: Date | null = null; + if (this.props.value && typeof this.props.value === 'number') { + value = new Date(this.props.value); + } + this.state = { + currentValue: value + }; + } + + + onDatePicked(date: Date | null) { + this.setState({ + currentValue: date + }); + this.props.onChangeValidate(date ? date.getTime() : null); + } + + render() { + + let fieldClass = 'tb-date-field'; + if (this.props.form.required) { + fieldClass += ' tb-required'; + } + if (this.props.form.readonly) { + fieldClass += ' tb-readonly'; + } + + return ( + +
    + + +
    +
    + ); + } +} + +export default ThingsboardBaseComponent(ThingsboardDate); diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-fieldset.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-fieldset.tsx new file mode 100644 index 0000000000..ab175dbeb3 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-fieldset.tsx @@ -0,0 +1,44 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as React from 'react'; +import { + JsonFormData, + JsonFormFieldProps, + JsonFormFieldState +} from '@shared/components/json-form/react/json-form.models'; + +class ThingsboardFieldSet extends React.Component { + + render() { + const forms = (this.props.form.items as JsonFormData[]).map((form: JsonFormData, index) => { + return this.props.builder(form, this.props.model, index, this.props.onChange, + this.props.onColorClick, this.props.onIconClick, this.props.onToggleFullscreen, this.props.mapper); + }); + + return ( +
    +
    + {this.props.form.title} +
    +
    + {forms} +
    +
    + ); + } +} + +export default ThingsboardFieldSet; diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-help.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-help.tsx new file mode 100644 index 0000000000..8b09878fd3 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-help.tsx @@ -0,0 +1,27 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as React from 'react'; +import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; + +class ThingsboardHelp extends React.Component { + render() { + return ( +
    + ); + } +} + +export default ThingsboardHelp; diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-html.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-html.tsx new file mode 100644 index 0000000000..4d7d0c956f --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-html.tsx @@ -0,0 +1,39 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as React from 'react'; +import ThingsboardAceEditor from './json-form-ace-editor'; +import { html_beautify } from 'js-beautify'; +import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; + +class ThingsboardHtml extends React.Component { + + constructor(props) { + super(props); + this.onTidyHtml = this.onTidyHtml.bind(this); + } + + onTidyHtml(html: string): string { + return html_beautify(html, {indent_size: 4}); + } + + render() { + return ( + + ); + } +} + +export default ThingsboardHtml; diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-icon.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-icon.tsx new file mode 100644 index 0000000000..e71b63ce2f --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-icon.tsx @@ -0,0 +1,159 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import ThingsboardBaseComponent from './json-form-base-component'; +import reactCSS from 'reactcss'; +import TextField from '@material-ui/core/TextField'; +import IconButton from '@material-ui/core/IconButton'; +import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; +import Clear from '@material-ui/icons/Clear'; +import Icon from '@material-ui/core/Icon'; +import Tooltip from '@material-ui/core/Tooltip'; + +interface ThingsboardIconState extends JsonFormFieldState { + icon: string | null; + focused: boolean; +} + +class ThingsboardIcon extends React.Component { + + constructor(props) { + super(props); + this.onBlur = this.onBlur.bind(this); + this.onFocus = this.onFocus.bind(this); + this.onValueChanged = this.onValueChanged.bind(this); + this.onIconClick = this.onIconClick.bind(this); + this.onClear = this.onClear.bind(this); + const icon = props.value ? props.value : ''; + this.state = { + icon, + focused: false + }; + } + + onBlur() { + this.setState({focused: false}); + } + + onFocus() { + this.setState({focused: true}); + } + + componentDidMount() { + const node = ReactDOM.findDOMNode(this); + const iconContainer = $(node).children('#icon-container'); + iconContainer.click((event) => { + if (!this.props.form.readonly) { + this.onIconClick(event); + } + }); + } + + componentWillUnmount() { + const node = ReactDOM.findDOMNode(this); + const iconContainer = $(node).children('#icon-container'); + iconContainer.off( 'click' ); + } + + onValueChanged(value: string | null) { + const icon = value; + this.setState({ + icon: value + }); + this.props.onChange(this.props.form.key, value); + } + + onIconClick(event) { + this.props.onIconClick(this.props.form.key, this.state.icon, + (color) => { + this.onValueChanged(color); + } + ); + } + + onClear(event) { + if (event) { + event.stopPropagation(); + } + this.onValueChanged(''); + } + + render() { + + const styles = reactCSS({ + default: { + container: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center' + }, + icon: { + marginRight: '10px', + marginBottom: 'auto', + cursor: 'pointer', + border: 'solid 1px rgba(0, 0, 0, .27)', + borderRadius: '0' + }, + iconContainer: { + display: 'flex', + width: '100%' + }, + iconText: { + width: '100%' + }, + }, + }); + + let fieldClass = 'tb-field'; + if (this.props.form.required) { + fieldClass += ' tb-required'; + } + if (this.state.focused) { + fieldClass += ' tb-focused'; + } + + let pickedIcon = 'more_horiz'; + let icon = ''; + if (this.state.icon !== '') { + pickedIcon = this.state.icon; + icon = this.state.icon; + } + + return ( +
    +
    + + {pickedIcon} + + +
    + +
    + ); + } +} + +export default ThingsboardBaseComponent(ThingsboardIcon); diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-image.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-image.tsx new file mode 100644 index 0000000000..0f71e60a7f --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-image.tsx @@ -0,0 +1,108 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import Dropzone from 'react-dropzone'; +import ThingsboardBaseComponent from './json-form-base-component'; +import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; +import IconButton from '@material-ui/core/IconButton'; +import Clear from '@material-ui/icons/Clear'; +import Tooltip from '@material-ui/core/Tooltip'; + +interface ThingsboardImageState extends JsonFormFieldState { + imageUrl: string; +} + +class ThingsboardImage extends React.Component { + + constructor(props) { + super(props); + this.onDrop = this.onDrop.bind(this); + this.onClear = this.onClear.bind(this); + const value = props.value ? props.value + '' : null; + this.state = { + imageUrl: value + }; + } + + onDrop(acceptedFiles: File[]) { + const reader = new FileReader(); + reader.onload = () => { + this.onValueChanged(reader.result); + }; + reader.readAsDataURL(acceptedFiles[0]); + } + + onValueChanged(value) { + this.setState({ + imageUrl: value + }); + this.props.onChangeValidate({ + target: { + value + } + }); + } + + onClear(event) { + if (event) { + event.stopPropagation(); + } + this.onValueChanged(''); + } + + render() { + + let labelClass = 'tb-label'; + if (this.props.form.required) { + labelClass += ' tb-required'; + } + if (this.props.form.readonly) { + labelClass += ' tb-readonly'; + } + + let previewComponent; + if (this.state.imageUrl) { + previewComponent = ; + } else { + previewComponent =
    No image selected
    ; + } + + return ( +
    + +
    +
    {previewComponent}
    +
    + + + +
    + + {({getRootProps, getInputProps}) => ( +
    +
    Drop an image or click to select a file to upload.
    + +
    + )} +
    +
    +
    + ); + } +} + +export default ThingsboardBaseComponent(ThingsboardImage); diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-javascript.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-javascript.tsx new file mode 100644 index 0000000000..4530434b03 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-javascript.tsx @@ -0,0 +1,38 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as React from 'react'; +import ThingsboardAceEditor from './json-form-ace-editor'; +import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; + +class ThingsboardJavaScript extends React.Component { + + constructor(props) { + super(props); + this.onTidyJavascript = this.onTidyJavascript.bind(this); + } + + onTidyJavascript(javascript: string): string { + return js_beautify(javascript, {indent_size: 4, wrap_line_length: 60}); + } + + render() { + return ( + + ); + } +} + +export default ThingsboardJavaScript; diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-json.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-json.tsx new file mode 100644 index 0000000000..5fc5f5d98f --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-json.tsx @@ -0,0 +1,38 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as React from 'react'; +import ThingsboardAceEditor from './json-form-ace-editor'; +import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; + +class ThingsboardJson extends React.Component { + + constructor(props) { + super(props); + this.onTidyJson = this.onTidyJson.bind(this); + } + + onTidyJson(json: string): string { + return js_beautify(json, {indent_size: 4}); + } + + render() { + return ( + + ); + } +} + +export default ThingsboardJson; diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-number.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-number.tsx new file mode 100644 index 0000000000..a9c7ea5320 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-number.tsx @@ -0,0 +1,97 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as React from 'react'; +import ThingsboardBaseComponent from './json-form-base-component'; +import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; +import { TextField } from '@material-ui/core'; + +interface ThingsboardNumberState extends JsonFormFieldState { + focused: boolean; + lastSuccessfulValue: number; +} + +class ThingsboardNumber extends React.Component { + + constructor(props) { + super(props); + this.preValidationCheck = this.preValidationCheck.bind(this); + this.onBlur = this.onBlur.bind(this); + this.onFocus = this.onFocus.bind(this); + this.state = { + lastSuccessfulValue: this.props.value, + focused: false + }; + } + + isNumeric(n) { + return n === null || n === '' || !isNaN(n) && isFinite(n); + } + + onBlur() { + this.setState({focused: false}); + } + + onFocus() { + this.setState({focused: true}); + } + + preValidationCheck(e) { + if (this.isNumeric(e.target.value)) { + this.setState({ + lastSuccessfulValue: e.target.value + }); + this.props.onChangeValidate(e); + } + } + + render() { + + let fieldClass = 'tb-field'; + if (this.props.form.required) { + fieldClass += ' tb-required'; + } + if (this.props.form.readonly) { + fieldClass += ' tb-readonly'; + } + if (this.state.focused) { + fieldClass += ' tb-focused'; + } + let value = this.state.lastSuccessfulValue; + if (typeof value !== 'undefined') { + value = Number(value); + } else { + value = null; + } + return ( +
    + +
    + ); + } +} + +export default ThingsboardBaseComponent(ThingsboardNumber); diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-radios.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-radios.tsx new file mode 100644 index 0000000000..8aa63aa4e4 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-radios.tsx @@ -0,0 +1,46 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as React from 'react'; +import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import { FormLabel, Radio, RadioGroup } from '@material-ui/core'; +import FormControl from '@material-ui/core/FormControl'; +import ThingsboardBaseComponent from '@shared/components/json-form/react/json-form-base-component'; + +class ThingsboardRadios extends React.Component { + render() { + const items = this.props.form.titleMap.map((item, index) => { + return ( + } label={item.name} key={index} /> + ); + }); + + return ( + + {this.props.form.title} + { + this.props.onChangeValidate(e); + }}> + {items} + + + ); + } +} + +export default ThingsboardBaseComponent(ThingsboardRadios); diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-rc-select.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-rc-select.tsx new file mode 100644 index 0000000000..a6473c7967 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-rc-select.tsx @@ -0,0 +1,196 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as React from 'react'; +import ThingsboardBaseComponent from './json-form-base-component'; +import Select, { Option } from 'rc-select'; +import { + JsonFormFieldProps, + JsonFormFieldState, + KeyLabelItem +} from '@shared/components/json-form/react/json-form.models'; +import { Mode } from 'rc-select/lib/interface'; + +interface ThingsboardRcSelectState extends JsonFormFieldState { + currentValue: KeyLabelItem | KeyLabelItem[]; + items: Array; + focused: boolean; +} + +class ThingsboardRcSelect extends React.Component { + + constructor(props) { + super(props); + this.onSelect = this.onSelect.bind(this); + this.onDeselect = this.onDeselect.bind(this); + this.onBlur = this.onBlur.bind(this); + this.onFocus = this.onFocus.bind(this); + this.state = { + currentValue: this.keyToCurrentValue(this.props.value, this.props.form.schema.type === 'array'), + items: this.props.form.items as KeyLabelItem[], + focused: false + }; + } + + keyToCurrentValue(key: string | string[], isArray: boolean): KeyLabelItem | KeyLabelItem[] { + let currentValue: KeyLabelItem | KeyLabelItem[] = isArray ? [] : null; + if (isArray) { + const keys = key; + if (keys) { + (keys as string[]).forEach((keyVal) => { + (currentValue as KeyLabelItem[]).push({key: keyVal, label: this.labelFromKey(keyVal)}); + }); + } + } else { + currentValue = {key: key as string, label: this.labelFromKey(key as string)}; + } + return currentValue; + } + + labelFromKey(key: string): string { + let label = key || ''; + if (key) { + for (const item of this.props.form.items) { + if (item.value === key) { + label = item.label; + break; + } + } + } + return label; + } + + arrayValues(items: KeyLabelItem[]): string[] { + const v: string[] = []; + if (items) { + items.forEach(item => { + v.push(item.key); + }); + } + return v; + } + + keyIndex(values: KeyLabelItem[], key: string): number { + let index = -1; + if (values) { + for (let i = 0; i < values.length; i++) { + if (values[i].key === key) { + index = i; + break; + } + } + } + return index; + } + + onSelect(value: KeyLabelItem, option) { + if (this.props.form.schema.type === 'array') { + const v = this.state.currentValue as KeyLabelItem[]; + v.push(this.keyToCurrentValue(value.key, false) as KeyLabelItem); + this.setState({ + currentValue: v + }); + this.props.onChangeValidate(this.arrayValues(v)); + } else { + this.setState({currentValue: this.keyToCurrentValue(value.key, false)}); + this.props.onChangeValidate({target: {value: value.key}}); + } + } + + onDeselect(value: KeyLabelItem, option) { + if (this.props.form.schema.type === 'array') { + const v = this.state.currentValue as KeyLabelItem[]; + const index = this.keyIndex(v, value.key); + if (index > -1) { + v.splice(index, 1); + } + this.setState({ + currentValue: v + }); + this.props.onChangeValidate(this.arrayValues(v)); + } + } + + onBlur() { + this.setState({ focused: false }); + } + + onFocus() { + this.setState({ focused: true }); + } + + render() { + + let options: JSX.Element[] = []; + if (this.state.items && this.state.items.length > 0) { + options = this.state.items.map((item, idx) => ( + + )); + } + + let labelClass = 'tb-label'; + if (this.props.form.required) { + labelClass += ' tb-required'; + } + if (this.props.form.readonly) { + labelClass += ' tb-readonly'; + } + if (this.state.focused) { + labelClass += ' tb-focused'; + } + let mode: Mode; + if (this.props.form.tags) { + mode = 'tags'; + } else if (this.props.form.multiple) { + mode = 'multiple'; + } + + const dropdownStyle = {...this.props.form.dropdownStyle, ...{zIndex: 100001}}; + let dropdownClassName = 'tb-rc-select-dropdown'; + if (this.props.form.dropdownClassName) { + dropdownClassName += ' ' + this.props.form.dropdownClassName; + } + + return ( +
    + + +
    {this.props.error}
    +
    + ); + } +} + +export default ThingsboardBaseComponent(ThingsboardRcSelect); diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-react.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-react.tsx new file mode 100644 index 0000000000..cd03f83581 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-react.tsx @@ -0,0 +1,53 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as React from 'react'; +import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles'; +import thingsboardTheme from './styles/thingsboardTheme'; +import ThingsboardSchemaForm from './json-form-schema-form'; +import { JsonFormProps } from './json-form.models'; + +const tbTheme = createMuiTheme(thingsboardTheme); + +class ReactSchemaForm extends React.Component { + + static defaultProps: JsonFormProps; + + constructor(props) { + super(props); + } + + render() { + if (this.props.form.length > 0) { + return ; + } else { + return
    ; + } + } +} + +ReactSchemaForm.defaultProps = { + isFullscreen: false, + schema: {}, + form: ['*'], + groupInfoes: [], + option: { + formDefaults: { + startEmpty: true + } + } +}; + +export default ReactSchemaForm; diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-schema-form.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-schema-form.tsx new file mode 100644 index 0000000000..50d48565a0 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-schema-form.tsx @@ -0,0 +1,199 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as React from 'react'; +import JsonFormUtils from './json-form-utils'; + +import ThingsboardArray from './json-form-array'; +import ThingsboardJavaScript from './json-form-javascript'; +import ThingsboardJson from './json-form-json'; +import ThingsboardHtml from './json-form-html'; +import ThingsboardCss from './json-form-css'; +import ThingsboardColor from './json-form-color'; +import ThingsboardRcSelect from './json-form-rc-select'; +import ThingsboardNumber from './json-form-number'; +import ThingsboardText from './json-form-text'; +import ThingsboardSelect from './json-form-select'; +import ThingsboardRadios from './json-form-radios'; +import ThingsboardDate from './json-form-date'; +import ThingsboardImage from './json-form-image'; +import ThingsboardCheckbox from './json-form-checkbox'; +import ThingsboardHelp from './json-form-help'; +import ThingsboardFieldSet from './json-form-fieldset'; +import ThingsboardIcon from './json-form-icon'; +import { JsonFormData, JsonFormProps, onChangeFn, OnColorClickFn, OnIconClickFn } from './json-form.models'; + +import _ from 'lodash'; +import * as tinycolor_ from 'tinycolor2'; +import { GroupInfo } from '@shared/models/widget.models'; + +const tinycolor = tinycolor_; + +class ThingsboardSchemaForm extends React.Component { + + private hasConditions: boolean; + private readonly mapper: {[type: string]: any}; + + constructor(props) { + super(props); + + this.mapper = { + number: ThingsboardNumber, + text: ThingsboardText, + password: ThingsboardText, + textarea: ThingsboardText, + select: ThingsboardSelect, + radios: ThingsboardRadios, + date: ThingsboardDate, + image: ThingsboardImage, + checkbox: ThingsboardCheckbox, + help: ThingsboardHelp, + array: ThingsboardArray, + javascript: ThingsboardJavaScript, + json: ThingsboardJson, + html: ThingsboardHtml, + css: ThingsboardCss, + color: ThingsboardColor, + 'rc-select': ThingsboardRcSelect, + fieldset: ThingsboardFieldSet, + icon: ThingsboardIcon + }; + + this.onChange = this.onChange.bind(this); + this.onColorClick = this.onColorClick.bind(this); + this.onIconClick = this.onIconClick.bind(this); + this.onToggleFullscreen = this.onToggleFullscreen.bind(this); + this.hasConditions = false; + } + + onChange(key: (string | number)[], val: any, forceUpdate?: boolean) { + this.props.onModelChange(key, val, forceUpdate); + if (this.hasConditions) { + this.forceUpdate(); + } + } + + onColorClick(key: (string | number)[], val: tinycolor.ColorFormats.RGBA, + colorSelectedFn: (color: tinycolor.ColorFormats.RGBA) => void) { + this.props.onColorClick(key, val, colorSelectedFn); + } + + onIconClick(key: (string | number)[], val: string, + iconSelectedFn: (icon: string) => void) { + this.props.onIconClick(key, val, iconSelectedFn); + } + + onToggleFullscreen(element: HTMLElement, fullscreenFinishFn?: () => void) { + this.props.onToggleFullscreen(element, fullscreenFinishFn); + } + + + builder(form: JsonFormData, + model: any, + index: number, + onChange: onChangeFn, + onColorClick: OnColorClickFn, + onIconClick: OnIconClickFn, + onToggleFullscreen: () => void, + mapper: {[type: string]: any}): JSX.Element { + const type = form.type; + const Field = this.mapper[type]; + if (!Field) { + console.log('Invalid field: \"' + form.key[0] + '\"!'); + return null; + } + if (form.condition) { + this.hasConditions = true; + // tslint:disable-next-line:no-eval + if (eval(form.condition) === false) { + return null; + } + } + return ; + } + + createSchema(theForm: any[]): JSX.Element { + const merged = JsonFormUtils.merge(this.props.schema, theForm, this.props.ignore, this.props.option); + let mapper = this.mapper; + if (this.props.mapper) { + mapper = _.merge(this.mapper, this.props.mapper); + } + const forms = merged.map(function(form, index) { + return this.builder(form, this.props.model, index, this.onChange, this.onColorClick, + this.onIconClick, this.onToggleFullscreen, mapper); + }.bind(this)); + + let formClass = 'SchemaForm'; + if (this.props.isFullscreen) { + formClass += ' SchemaFormFullscreen'; + } + + return ( +
    {forms}
    + ); + } + + render() { + if (this.props.groupInfoes && this.props.groupInfoes.length > 0) { + const content: JSX.Element[] = []; + for (const info of this.props.groupInfoes) { + const forms = this.createSchema(this.props.form[info.formIndex]); + const item = ; + content.push(item); + } + return (
    {content}
    ); + } else { + return this.createSchema(this.props.form); + } + } +} +export default ThingsboardSchemaForm; + +interface ThingsboardSchemaGroupProps { + info: GroupInfo; + forms: JSX.Element; +} + +interface ThingsboardSchemaGroupState { + showGroup: boolean; +} + +class ThingsboardSchemaGroup extends React.Component { + constructor(props) { + super(props); + this.state = { + showGroup: true + }; + } + + toogleGroup(index) { + this.setState({ + showGroup: !this.state.showGroup + }); + } + + render() { + const theCla = 'pull-right fa fa-chevron-down tb-toggle-icon' + (this.state.showGroup ? '' : ' tb-toggled'); + return (
    +
    {this.props.info.GroupTitle}
    +
    {this.props.forms}
    +
    ); + } +} diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-select.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-select.tsx new file mode 100644 index 0000000000..0653209e6a --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-select.tsx @@ -0,0 +1,86 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as React from 'react'; +import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; +import MenuItem from '@material-ui/core/MenuItem'; +import FormControl from '@material-ui/core/FormControl'; +import InputLabel from '@material-ui/core/InputLabel'; +import Select from '@material-ui/core/Select'; +import ThingsboardBaseComponent from '@shared/components/json-form/react/json-form-base-component'; + +interface ThingsboardSelectState extends JsonFormFieldState { + currentValue: any; +} + +class ThingsboardSelect extends React.Component { + + constructor(props) { + super(props); + this.onSelected = this.onSelected.bind(this); + const possibleValue = this.getModelKey(this.props.model, this.props.form.key); + this.state = { + currentValue: this.props.model !== undefined && possibleValue ? possibleValue : this.props.form.titleMap != null ? + this.props.form.titleMap[0].value : '' + }; + } + + componentWillReceiveProps(nextProps) { + if (nextProps.model && nextProps.form.key) { + this.setState({ + currentValue: this.getModelKey(nextProps.model, nextProps.form.key) + || (nextProps.form.titleMap != null ? nextProps.form.titleMap[0].value : '') + }); + } + } + + getModelKey(model, key) { + if (Array.isArray(key)) { + return key.reduce((cur, nxt) => (cur[nxt] || {}), model); + } else { + return model[key]; + } + } + + onSelected(event: React.ChangeEvent<{ name?: string; value: any }>) { + + this.setState({ + currentValue: event.target.value + }); + this.props.onChangeValidate(event); + } + + render() { + const menuItems = this.props.form.titleMap.map((item, idx) => ( + {item.name} + )); + + return ( + + {this.props.form.title} + + + ); + } +} + +export default ThingsboardBaseComponent(ThingsboardSelect); diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-text.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-text.tsx new file mode 100644 index 0000000000..0b690f8181 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-text.tsx @@ -0,0 +1,91 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as React from 'react'; +import ThingsboardBaseComponent from './json-form-base-component'; +import TextField from '@material-ui/core/TextField'; +import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; + +interface ThingsboardTextState extends JsonFormFieldState { + focused: boolean; +} + +class ThingsboardText extends React.Component { + + constructor(props) { + super(props); + this.onBlur = this.onBlur.bind(this); + this.onFocus = this.onFocus.bind(this); + this.state = { + focused: false + }; + } + + onBlur() { + this.setState({focused: false}); + } + + onFocus() { + this.setState({focused: true}); + } + + render() { + + let fieldClass = 'tb-field'; + if (this.props.form.required) { + fieldClass += ' tb-required'; + } + if (this.props.form.readonly) { + fieldClass += ' tb-readonly'; + } + if (this.state.focused) { + fieldClass += ' tb-focused'; + } + + const multiline = this.props.form.type === 'textarea'; + let rows = 1; + let rowsMax = 1; + let minHeight = 48; + if (multiline) { + rows = this.props.form.rows || 2; + rowsMax = this.props.form.rowsMax; + minHeight = 19 * rows + 48; + } + + return ( +
    + { + this.props.onChangeValidate(e); + }} + defaultValue={this.props.value} + disabled={this.props.form.readonly} + rows={rows} + rowsMax={rowsMax} + onFocus={this.onFocus} + onBlur={this.onBlur} + style={this.props.form.style || {width: '100%', minHeight: minHeight + 'px'}}/> +
    + ); + } +} + +export default ThingsboardBaseComponent(ThingsboardText); diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-utils.ts b/ui-ngx/src/app/shared/components/json-form/react/json-form-utils.ts new file mode 100644 index 0000000000..6937a4acb8 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-utils.ts @@ -0,0 +1,592 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import * as tv from 'tv4'; +import ObjectPath from 'objectpath'; +import _ from 'lodash'; +import { + DefaultsFormOptions, + FormOption, + JsonFormData, + JsonSchemaData, + SchemaValidationResult +} from './json-form.models'; +import { isDefined, isEqual, isString, isUndefined } from '@core/utils'; + +function validateBySchema(schema: any, value: any): SchemaValidationResult { + return tv.validateResult(value, schema); +} + +function validate(form: any, value: any): SchemaValidationResult { + + if (!form) { + return {valid: true}; + } + const schema = form.schema; + + if (!schema) { + return {valid: true}; + } + + if (value === '') { + value = undefined; + } + + // Numbers fields will give a null value, which also means empty field + if (form.type === 'number' && value === null) { + value = undefined; + } + + if (form.type === 'number' && isNaN(parseFloat(value))) { + value = undefined; + } + const wrap: any = {type: 'object', properties: {}}; + const propName = form.key[form.key.length - 1]; + wrap.properties[propName] = schema; + + if (form.required) { + wrap.required = [propName]; + } + const valueWrap = {}; + if (typeof value !== 'undefined') { + valueWrap[propName] = value; + } + + const tv4Result: SchemaValidationResult = tv.validateResult(valueWrap, wrap); + if (tv4Result != null && !tv4Result.valid && form.validationMessage != null && typeof value !== 'undefined') { + tv4Result.error.message = form.validationMessage; + } + return tv4Result; +} + +function stripNullType(type: any): string { + if (Array.isArray(type) && type.length === 2) { + if (type[0] === 'null') { + return type[1]; + } + if (type[1] === 'null') { + return type[0]; + } + } + return type; +} + +const enumToTitleMap = (enm: string[]): { name: string, value: string }[] => { + const titleMap: { name: string, value: string }[] = []; + enm.forEach((name) => { + titleMap.push({name, value: name}); + }); + return titleMap; +}; + +const canonicalTitleMap = (titleMap: any, originalEnum?: string[]): { name: string, value: string }[] => { + if (!_.isArray(titleMap)) { + const canonical: { name: string, value: string }[] = []; + if (originalEnum) { + originalEnum.forEach((value) => { + canonical.push({name: titleMap[value], value}); + }); + } else { + for (const k of Object.keys(titleMap)) { + if (titleMap.hasOwnProperty(k)) { + canonical.push({name: k, value: titleMap[k]}); + } + } + } + return canonical; + } + return titleMap; +}; + +const stdFormObj = (name: string, schema: any, options: DefaultsFormOptions): any => { + options = options || {}; + const f: any = options.global && options.global.formDefaults ? _.cloneDeep(options.global.formDefaults) : {}; + if (options.global && options.global.supressPropertyTitles === true) { + f.title = schema.title; + } else { + f.title = schema.title || name; + } + + if (schema.description) { + f.description = schema.description; + } + if (options.required === true || schema.required === true) { + f.required = true; + } + if (schema.maxLength) { + f.maxlength = schema.maxLength; + } + if (schema.minLength) { + f.minlength = schema.minLength; + } + if (schema.readOnly || schema.readonly) { + f.readonly = true; + } + if (schema.minimum) { + f.minimum = schema.minimum + (schema.exclusiveMinimum ? 1 : 0); + } + if (schema.maximum) { + f.maximum = schema.maximum - (schema.exclusiveMaximum ? 1 : 0); + } + + // Non standard attributes (DONT USE DEPRECATED) + // If you must set stuff like this in the schema use the x-schema-form attribute + if (schema.validationMessage) { + f.validationMessage = schema.validationMessage; + } + if (schema.enumNames) { + f.titleMap = canonicalTitleMap(schema.enumNames, schema.enum); + } + f.schema = schema; + + // Ng model options doesn't play nice with undefined, might be defined + // globally though + f.ngModelOptions = f.ngModelOptions || {}; + + return f; +}; + +const text = (name: string, schema: any, options: DefaultsFormOptions): any => { + if (stripNullType(schema.type) === 'string' && !schema.enum) { + const f = stdFormObj(name, schema, options); + f.key = options.path; + f.type = 'text'; + options.lookup[ObjectPath.stringify(options.path)] = f; + return f; + } +}; + +const numberType = (name: string, schema: any, options: DefaultsFormOptions): any => { + if (stripNullType(schema.type) === 'number') { + const f = stdFormObj(name, schema, options); + f.key = options.path; + f.type = 'number'; + options.lookup[ObjectPath.stringify(options.path)] = f; + return f; + } +}; + +const integer = (name: string, schema: any, options: DefaultsFormOptions): any => { + if (stripNullType(schema.type) === 'integer') { + const f = stdFormObj(name, schema, options); + f.key = options.path; + f.type = 'number'; + options.lookup[ObjectPath.stringify(options.path)] = f; + return f; + } +}; + +const date = (name: string, schema: any, options: DefaultsFormOptions): any => { + if (stripNullType(schema.type) === 'date') { + const f = stdFormObj(name, schema, options); + f.key = options.path; + f.type = 'date'; + options.lookup[ObjectPath.stringify(options.path)] = f; + return f; + } +}; + +const checkbox = (name: string, schema: any, options: DefaultsFormOptions): any => { + if (stripNullType(schema.type) === 'boolean') { + const f = stdFormObj(name, schema, options); + f.key = options.path; + f.type = 'checkbox'; + options.lookup[ObjectPath.stringify(options.path)] = f; + return f; + } +}; + +const select = (name: string, schema: any, options: DefaultsFormOptions): any => { + if (stripNullType(schema.type) === 'string' && schema.enum) { + const f = stdFormObj(name, schema, options); + f.key = options.path; + f.type = 'select'; + if (!f.titleMap) { + f.titleMap = enumToTitleMap(schema.enum); + } + options.lookup[ObjectPath.stringify(options.path)] = f; + return f; + } +}; + +const checkboxes = (name: string, schema: any, options: DefaultsFormOptions): any => { + if (stripNullType(schema.type) === 'array' && schema.items && schema.items.enum) { + const f = stdFormObj(name, schema, options); + f.key = options.path; + f.type = 'checkboxes'; + if (!f.titleMap) { + f.titleMap = enumToTitleMap(schema.items.enum); + } + options.lookup[ObjectPath.stringify(options.path)] = f; + return f; + } +}; + +const fieldset = (name: string, schema: any, options: DefaultsFormOptions): any => { + if (stripNullType(schema.type) === 'object') { + const f = stdFormObj(name, schema, options); + f.type = 'fieldset'; + f.items = []; + options.lookup[ObjectPath.stringify(options.path)] = f; + + // recurse down into properties + for (const k of Object.keys(schema.properties)) { + if (schema.properties.hasOwnProperty(k)) { + const path = options.path.slice(); + path.push(k); + if (options.ignore[ObjectPath.stringify(path)] !== true) { + const required = schema.required && schema.required.indexOf(k) !== -1; + + const def = defaultFormDefinition(k, schema.properties[k], { + path, + required: required || false, + lookup: options.lookup, + ignore: options.ignore, + global: options.global + }); + if (def) { + f.items.push(def); + } + } + } + } + return f; + } +}; + +const array = (name: string, schema: any, options: DefaultsFormOptions): any => { + + if (stripNullType(schema.type) === 'array') { + const f = stdFormObj(name, schema, options); + f.type = 'array'; + f.key = options.path; + options.lookup[ObjectPath.stringify(options.path)] = f; + + // don't do anything if items is not defined. + if (typeof schema.items !== 'undefined') { + const required = schema.required && schema.required.indexOf(options.path[options.path.length - 1]) !== -1; + + const arrPath = options.path.slice(); + arrPath.push(''); + const def = defaultFormDefinition(name, schema.items, { + path: arrPath, + required: required || false, + lookup: options.lookup, + ignore: options.ignore, + global: options.global + }); + if (def) { + f.items = [def]; + } else { + // This is the case that item only contains key value pair for rc-select multipel + f.items = schema.items; + } + } + return f; + } +}; + +const defaults: { [key: string]: ((name: string, schema: any, options: DefaultsFormOptions) => any)[] } = { + string: [select, text], + object: [fieldset], + number: [numberType], + integer: [integer], + boolean: [checkbox], + array: [checkboxes, array], + date: [date] +}; + +function defaultFormDefinition(name: string, schema: any, options: DefaultsFormOptions): any { + const rules = defaults[stripNullType(schema.type)]; + if (rules) { + let def; + for (const rule of rules) { + def = rule(name, schema, options); + if (def) { + + // Do we have form defaults in the schema under the x-schema-form-attribute? + if (def.schema['x-schema-form'] && _.isObject(def.schema['x-schema-form'])) { + def = _.extend(def, def.schema['x-schema-form']); + } + return def; + } + } + } +} + +interface DefaultsFormData { + form: any[]; + lookup: { [key: string]: any }; +} + +function getDefaults(schema: any, ignore: { [key: string]: boolean }, globalOptions: FormOption): DefaultsFormData { + const form = []; + const lookup: { [key: string]: any } = {}; + ignore = ignore || {}; + globalOptions = globalOptions || {}; + if (stripNullType(schema.type) === 'object') { + for (const k of Object.keys(schema.properties)) { + if (schema.properties.hasOwnProperty(k)) { + if (ignore[k] !== true) { + const required = schema.required && schema.required.indexOf(k) !== -1; + const def = defaultFormDefinition(k, schema.properties[k], { + path: [k], // Path to this property in bracket notation. + lookup, // Extra map to register with. Optimization for merger. + ignore, // The ignore list of paths (sans root level name) + required, // Is it required? (v4 json schema style) + global: globalOptions // Global options, including form defaults + }); + if (def) { + form.push(def); + } + } + } + } + } else { + throw new Error('Not implemented. Only type "object" allowed at root level of schema.'); + } + return {form, lookup}; +} + +const postProcessFn = (form: any[]): any[] => { + return form; +}; + +function merge(schema: any, form: any[], ignore: { [key: string]: boolean }, options: FormOption, isReadonly?: boolean): any[] { + form = form || ['*']; + options = options || {}; + isReadonly = isReadonly || schema.readonly || schema.readOnly; + const stdForm = getDefaults(schema, ignore, options); + const idx = form.indexOf('*'); + if (idx !== -1) { + form = form.slice(0, idx).concat(stdForm.form).concat(form.slice(idx + 1)); + } + const lookup = stdForm.lookup; + return postProcessFn(form.map((obj) => { + + if (typeof obj === 'string') { + obj = {key: obj}; + } + + if (obj.key) { + if (typeof obj.key === 'string') { + obj.key = ObjectPath.parse(obj.key); + } + } + + if (obj.titleMap) { + obj.titleMap = canonicalTitleMap(obj.titleMap); + } + + if (obj.itemForm) { + obj.items = []; + const str: string = ObjectPath.stringify(obj.key); + const lookupForm = lookup[str]; + lookupForm.items.forEach((item) => { + const o = _.cloneDeep(obj.itemForm); + o.key = item.key; + obj.items.push(o); + }); + } + + if (obj.key) { + const strid: string = ObjectPath.stringify(obj.key); + if (lookup[strid]) { + const schemaDefaults = lookup[strid]; + for (const k of Object.keys(schemaDefaults)) { + if (schemaDefaults.hasOwnProperty(k)) { + if (obj[k] === undefined) { + obj[k] = schemaDefaults[k]; + } + } + } + } + } + + if (isReadonly === true) { + obj.readonly = true; + } + + if (obj.items && obj.items.length > 0) { + obj.items = merge(schema, obj.items, ignore, options, obj.readonly); + } + + if (obj.tabs) { + obj.tabs.forEach((tab) => { + tab.items = merge(schema, tab.items, ignore, options, obj.readonly); + }); + } + + if (obj.type === 'checkbox' && _.isUndefined(obj.schema.default)) { + obj.schema.default = false; + } + + return obj; + })); +} + +function selectOrSet(projection: string | (string | number)[], obj: any, valueToSet?: any): any { + const numRe = /^\d+$/; + + if (!obj) { + obj = this; + } + const parts = typeof projection === 'string' ? ObjectPath.parse(projection) : projection; + + if (typeof valueToSet !== 'undefined' && parts.length === 1) { + obj[parts[0]] = valueToSet; + return obj; + } + + if (typeof valueToSet !== 'undefined' && typeof obj[parts[0]] === 'undefined') { + obj[parts[0]] = parts.length > 2 && numRe.test(parts[1]) ? [] : {}; + } + + let value = obj[parts[0]]; + for (let i = 1; i < parts.length; i++) { + if (parts[i] === '') { + return undefined; + } + if (typeof valueToSet !== 'undefined') { + if (i === parts.length - 1) { + value[parts[i]] = valueToSet; + return valueToSet; + } else { + let tmp = value[parts[i]]; + if (typeof tmp === 'undefined' || tmp === null) { + tmp = numRe.test(parts[i + 1]) ? [] : {}; + value[parts[i]] = tmp; + } + value = tmp; + } + } else if (value) { + value = value[parts[i]]; + } + } + return value; +} + +function updateValue(projection: string | (string | number)[], obj: any, valueToSet: any): boolean { + const numRe = /^\d+$/; + + if (!obj) { + obj = this; + } + + if (!obj) { + return false; + } + + const parts: string[] = isString(projection) ? ObjectPath.parse(projection) : projection; + + if (parts.length === 1) { + return setValue(obj, parts[0], valueToSet); + } + + if (isUndefined(obj[parts[0]])) { + obj[parts[0]] = parts.length > 2 && numRe.test(parts[1]) ? [] : {}; + } + + let value = obj[parts[0]]; + for (let i = 1; i < parts.length; i++) { + if (parts[i] === '') { + return false; + } + if (i === parts.length - 1) { + return setValue(value, parts[i], valueToSet); + } else { + let tmp = value[parts[i]]; + if (isUndefined(tmp) || tmp === null) { + tmp = numRe.test(parts[i + 1]) ? [] : {}; + value[parts[i]] = tmp; + } + value = tmp; + } + } + return value; +} + + +function setValue(obj: any, key: string, val: any): boolean { + let changed = false; + if (obj) { + if (isUndefined(val)) { + if (isDefined(obj[key])) { + delete obj[key]; + changed = true; + } + } else { + changed = !isEqual(obj[key], val); + obj[key] = val; + } + } + return changed; +} + +function traverseSchema(schema: JsonSchemaData, fn: (prop: any, path: string[]) => any, path?: string[], ignoreArrays?: boolean) { + ignoreArrays = typeof ignoreArrays !== 'undefined' ? ignoreArrays : true; + + path = path || []; + + const traverse = ($schema: JsonSchemaData, $fn: (prop: any, path: string[]) => any, $path: string[]) => { + $fn($schema, $path); + if ($schema.properties) { + for (const k of Object.keys($schema.properties)) { + if ($schema.properties.hasOwnProperty(k)) { + const currentPath = $path.slice(); + currentPath.push(k); + traverse($schema.properties[k], $fn, currentPath); + } + } + } + if (!ignoreArrays && $schema.items) { + const arrPath = $path.slice(); + arrPath.push(''); + traverse($schema.items, $fn, arrPath); + } + }; + + traverse(schema, fn, path || []); +} + +function traverseForm(form: JsonFormData, fn: (form: JsonFormData) => any) { + fn(form); + if (form.items) { + form.items.forEach((f) => { + traverseForm(f, fn); + }); + } + + if (form.tabs) { + form.tabs.forEach((tab) => { + tab.items.forEach((f) => { + traverseForm(f, fn); + }); + }); + } +} + + +const utils = { + validateBySchema, + validate, + merge, + updateValue, + selectOrSet, + traverseSchema, + traverseForm +}; +export default utils; diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form.models.ts b/ui-ngx/src/app/shared/components/json-form/react/json-form.models.ts new file mode 100644 index 0000000000..29d21b9e5f --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form.models.ts @@ -0,0 +1,135 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import * as tinycolor_ from 'tinycolor2'; +import { GroupInfo } from '@shared/models/widget.models'; + +const tinycolor = tinycolor_; + +export interface SchemaValidationResult { + valid: boolean; + error?: { + message?: string; + }; +} + +export interface FormOption { + formDefaults?: { + startEmpty?: boolean; + readonly?: boolean; + }; + supressPropertyTitles?: boolean; +} + +export interface DefaultsFormOptions { + global?: FormOption; + required?: boolean; + path?: string[]; + lookup?: {[key: string]: any}; + ignore?: {[key: string]: boolean}; +} + +export type onChangeFn = (key: (string | number)[], val: any, forceUpdate?: boolean) => void; +export type OnColorClickFn = (key: (string | number)[], val: tinycolor.ColorFormats.RGBA, + colorSelectedFn: (color: tinycolor.ColorFormats.RGBA) => void) => void; +export type OnIconClickFn = (key: (string | number)[], val: string, + iconSelectedFn: (icon: string) => void) => void; +export type onToggleFullscreenFn = (element: HTMLElement, fullscreenFinishFn?: () => void) => void; + +export interface JsonFormProps { + model?: any; + schema?: any; + form?: any; + groupInfoes?: GroupInfo[]; + isFullscreen: boolean; + ignore?: {[key: string]: boolean}; + option: FormOption; + onModelChange?: onChangeFn; + onColorClick?: OnColorClickFn; + onIconClick?: OnIconClickFn; + onToggleFullscreen?: onToggleFullscreenFn; + mapper?: {[type: string]: any}; +} + +export interface KeyLabelItem { + key: string; + label: string; + value?: string; +} + +export interface JsonSchemaData { + type: string; + default: any; + items?: JsonSchemaData; + properties?: any; +} + +export interface JsonFormData { + type: string; + key: (string | number)[]; + title: string; + readonly: boolean; + required: boolean; + default?: any; + condition?: string; + style?: any; + rows?: number; + rowsMax?: number; + placeholder?: string; + schema: JsonSchemaData; + titleMap: { + value: any; + name: string; + }[]; + items?: Array | Array; + tabs?: Array; + tags?: any; + startEmpty?: boolean; + [key: string]: any; +} + +export type ComponentBuilderFn = (form: JsonFormData, + model: any, + index: number, + onChange: onChangeFn, + onColorClick: OnColorClickFn, + onIconClick: OnIconClickFn, + onToggleFullscreen: onToggleFullscreenFn, + mapper: {[type: string]: any}) => JSX.Element; + +export interface JsonFormFieldProps { + value: any; + model: any; + form: JsonFormData; + builder: ComponentBuilderFn; + mapper?: {[type: string]: any}; + onChange?: onChangeFn; + onColorClick?: OnColorClickFn; + onIconClick?: OnIconClickFn; + onChangeValidate?: (e: any, forceUpdate?: boolean) => void; + onToggleFullscreen?: onToggleFullscreenFn; + valid?: boolean; + error?: string; + options?: { + setSchemaDefaults?: boolean; + }; +} + +export interface JsonFormFieldState { + value?: any; + valid?: boolean; + error?: string; +} diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form.scss b/ui-ngx/src/app/shared/components/json-form/react/json-form.scss new file mode 100644 index 0000000000..8b59ccd29a --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form.scss @@ -0,0 +1,351 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "~compass-sass-mixins/lib/compass"; + +$swift-ease-out-duration: .4s !default; +$swift-ease-out-timing-function: cubic-bezier(.25, .8, .25, 1) !default; + +$input-label-float-offset: 6px !default; +$input-label-float-scale: .75 !default; + +$previewSize: 100px !default; + +.tb-json-form { + + &.tb-fullscreen { + background: #fff; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + > div.fullscreen-form-field { + position: relative; + width: 100%; + height: 100%; + } + } + + .json-form-error { + position: relative; + bottom: -5px; + font-size: 12px; + line-height: 12px; + color: rgb(244, 67, 54); + + transition: all 450ms cubic-bezier(.23, 1, .32, 1) 0ms; + } + + .tb-container { + position: relative; + box-sizing: border-box; + padding: 10px 0; + margin-top: 32px; + } + + .tb-field { + padding-bottom: 18px; + + .MuiInputBase-multiline { + flex: 1; + flex-direction: column; + .MuiInputBase-inputMultiline { + flex: 1; + } + } + + &.tb-required { + label::after { + font-size: 13px; + color: rgba(0, 0, 0, .54); + vertical-align: top; + content: " *"; + } + } + + &.tb-focused:not(.tb-readonly) { + label::after { + color: rgb(221, 44, 0); + } + } + } + + .tb-date-field { + &.tb-required { + div > div:first-child::after { + font-size: 13px; + color: rgba(0, 0, 0, .54); + vertical-align: top; + content: " *"; + } + } + + &.tb-focused:not(.tb-readonly) { + div > div:first-child::after { + color: rgb(221, 44, 0); + } + } + } + + label.tb-label { + position: absolute; + right: auto; + bottom: 100%; + left: 0; + color: rgba(0, 0, 0, .54); + + transition: transform $swift-ease-out-timing-function $swift-ease-out-duration, width $swift-ease-out-timing-function $swift-ease-out-duration; + + transform: translate3d(0, $input-label-float-offset, 0) scale($input-label-float-scale); + transform-origin: left top; + -webkit-font-smoothing: antialiased; + + &.tb-focused { + color: rgb(96, 125, 139); + } + + &.tb-required::after { + font-size: 13px; + color: rgba(0, 0, 0, .54); + vertical-align: top; + content: " *"; + } + + &.tb-focused:not(.tb-readonly)::after { + color: rgb(221, 44, 0); + } + } + + .tb-head-label { + color: rgba(0, 0, 0, .54); + padding-bottom: 15px; + } + + .SchemaGroupname { + padding: 10px 20px; + background-color: #f1f1f1; + } + + .invisible { + display: none; + } + + .tb-button-toggle .tb-toggle-icon { + display: inline-block; + width: 15px; + margin: auto 0 auto auto; + background-size: 100% auto; + + transition: transform .3s, ease-in-out; + } + + .tb-button-toggle .tb-toggle-icon.tb-toggled { + transform: rotateZ(180deg); + } + + .fullscreen-form-field { + .json-form-ace-editor { + height: calc(100% - 60px); + } + } + + .json-form-ace-editor { + position: relative; + height: 100%; + border: 1px solid #c0c0c0; + + .title-panel { + position: absolute; + top: 10px; + right: 20px; + z-index: 5; + font-size: .8rem; + font-weight: 500; + + label { + padding: 4px; + color: #00acc1; + background: rgba(220, 220, 220, .35); + border-radius: 5px; + } + + button.tidy-button { + background: rgba(220, 220, 220, .35) !important; + + span { + padding: 0 !important; + font-size: 12px !important; + } + } + } + } + + .tb-image-select-container { + position: relative; + width: 100%; + height: $previewSize; + } + + .tb-image-preview { + width: auto; + max-width: $previewSize; + height: auto; + max-height: $previewSize; + } + + .tb-image-preview-container { + position: relative; + float: left; + width: $previewSize; + height: $previewSize; + margin-right: 12px; + vertical-align: top; + border: solid 1px; + + div { + width: 100%; + font-size: 18px; + text-align: center; + } + + div, .tb-image-preview { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + } + + .tb-dropzone { + outline: none; + position: relative; + height: $previewSize; + padding: 0 8px; + overflow: hidden; + vertical-align: top; + border: dashed 2px; + + div { + position: absolute; + top: 50%; + left: 50%; + width: 100%; + font-size: 24px; + text-align: center; + transform: translate(-50%, -50%); + } + } + + .tb-image-clear-container { + position: relative; + float: right; + width: 48px; + height: $previewSize; + } + + .tb-image-clear-btn { + position: absolute !important; + top: 50%; + transform: translate(0%, -50%) !important; + } + + .MuiButton-root { + text-transform: none; + } + +} + +.rc-select { + box-sizing: border-box; + display: inline-block; + position: relative; + vertical-align: middle; + color: #666; + line-height: 28px; + font-size: inherit !important; + .rc-select-selector { + outline: none; + user-select: none; + box-sizing: border-box; + display: block; + background-color: #fff; + border-radius: 6px; + } + &.rc-select-single { + &:not(.rc-select-customize-input) { + .rc-select-selector { + height: 28px; + line-height: 28px; + position: relative; + border: 1px solid #d9d9d9; + &:hover { + border-color: #23c0fa; + box-shadow: 0 0 2px rgba(45, 183, 245, 0.8); + } + .rc-select-selection-search { + .rc-select-selection-search-input { + cursor: pointer; + background: transparent; + margin-left: 10px; + } + } + .rc-select-selection-item, .rc-select-selection-placeholder { + top: 0; + left: 10px; + } + } + &.rc-select-focused { + .rc-select-selector { + border-color: #23c0fa !important; + box-shadow: 0 0 2px rgba(45, 183, 245, 0.8) !important; + } + } + } + } +} + +.rc-select-dropdown { + &.tb-rc-select-dropdown { + z-index: 100001; + background-color: white; + border: 1px solid #d9d9d9; + box-shadow: 0 0 4px #d9d9d9; + border-radius: 4px; + box-sizing: border-box; + outline: none; + + .rc-select-item { + &.rc-select-item-option { + margin: 0; + position: relative; + display: block; + padding: 7px 10px; + font-weight: normal; + color: #666; + white-space: nowrap; + &.rc-select-item-option-selected { + color: #666; + background-color: #ddd; + } + &.rc-select-item-option-active { + background-color: #5897fb; + color: white; + cursor: pointer; + } + } + } + } +} diff --git a/ui-ngx/src/app/shared/components/json-form/react/styles/thingsboardTheme.ts b/ui-ngx/src/app/shared/components/json-form/react/styles/thingsboardTheme.ts new file mode 100644 index 0000000000..866cae7332 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-form/react/styles/thingsboardTheme.ts @@ -0,0 +1,62 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +/* + * Copyright © 2016-2019 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import indigo from '@material-ui/core/colors/indigo'; +import deeepOrange from '@material-ui/core/colors/deepOrange'; +import { ThemeOptions } from '@material-ui/core/styles/createMuiTheme'; +import { PaletteOptions } from '@material-ui/core/styles/createPalette'; +import { mergeDeep } from '@core/utils'; + +const PRIMARY_COLOR = '#305680'; +const SECONDARY_COLOR = '#527dad'; +const HUE3_COLOR = '#a7c1de'; + +const tbIndigo = mergeDeep({}, indigo, { + 500: PRIMARY_COLOR, + 600: SECONDARY_COLOR, + 700: PRIMARY_COLOR, + A100: HUE3_COLOR +}); + +const thingsboardPalette: PaletteOptions = { + primary: tbIndigo, + secondary: deeepOrange, + background: { + default: '#eee' + } +}; + +export default { + typography: { + fontFamily: 'Roboto, \'Helvetica Neue\', sans-serif' + }, + palette: thingsboardPalette, +} as ThemeOptions; diff --git a/ui-ngx/src/app/shared/components/json-object-edit.component.html b/ui-ngx/src/app/shared/components/json-object-edit.component.html new file mode 100644 index 0000000000..aaacf1f857 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-object-edit.component.html @@ -0,0 +1,45 @@ + +
    +
    + + + + + +
    +
    +
    +
    +
    diff --git a/ui-ngx/src/app/shared/components/json-object-edit.component.scss b/ui-ngx/src/app/shared/components/json-object-edit.component.scss new file mode 100644 index 0000000000..0323300f4d --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-object-edit.component.scss @@ -0,0 +1,57 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + position: relative; + + .fill-height { + height: 100%; + } +} + +.tb-json-object-toolbar { + button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 { + align-items: center; + vertical-align: middle; + min-width: 32px; + min-height: 15px; + padding: 4px; + margin: 0; + font-size: .8rem; + line-height: 15px; + color: #7b7b7b; + background: rgba(220, 220, 220, .35); + + &:not(:last-child) { + margin-right: 4px; + } + } +} + +.tb-json-object-panel { + height: 100%; + margin-left: 15px; + border: 1px solid #c0c0c0; + + #tb-json-input { + width: 100%; + min-width: 200px; + height: 100%; + + &:not(.fill-height) { + min-height: 200px; + } + } +} diff --git a/ui-ngx/src/app/shared/components/json-object-edit.component.ts b/ui-ngx/src/app/shared/components/json-object-edit.component.ts new file mode 100644 index 0000000000..503b12c1a8 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-object-edit.component.ts @@ -0,0 +1,276 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'; +import * as ace from 'ace-builds'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; +import { guid } from '@core/utils'; +import { ResizeObserver } from '@juggle/resize-observer'; + +@Component({ + selector: 'tb-json-object-edit', + templateUrl: './json-object-edit.component.html', + styleUrls: ['./json-object-edit.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => JsonObjectEditComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => JsonObjectEditComponent), + multi: true, + } + ] +}) +export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Validator, OnDestroy { + + @ViewChild('jsonEditor', {static: true}) + jsonEditorElmRef: ElementRef; + + private jsonEditor: ace.Ace.Editor; + private editorsResizeCaf: CancelAnimationFrame; + private editorResize$: ResizeObserver; + + toastTargetId = `jsonObjectEditor-${guid()}`; + + @Input() label: string; + + @Input() disabled: boolean; + + @Input() fillHeight: boolean; + + @Input() editorStyle: {[klass: string]: any}; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + private readonlyValue: boolean; + get readonly(): boolean { + return this.readonlyValue; + } + @Input() + set readonly(value: boolean) { + this.readonlyValue = coerceBooleanProperty(value); + } + + fullscreen = false; + + modelValue: any; + + contentValue: string; + + objectValid: boolean; + + validationError: string; + + errorShowed = false; + + ignoreChange = false; + + private propagateChange = null; + + constructor(public elementRef: ElementRef, + protected store: Store, + private raf: RafService) { + } + + ngOnInit(): void { + const editorElement = this.jsonEditorElmRef.nativeElement; + let editorOptions: Partial = { + mode: 'ace/mode/json', + showGutter: true, + showPrintMargin: false, + readOnly: this.disabled || this.readonly + }; + + const advancedOptions = { + enableSnippets: true, + enableBasicAutocompletion: true, + enableLiveAutocompletion: true + }; + + editorOptions = {...editorOptions, ...advancedOptions}; + this.jsonEditor = ace.edit(editorElement, editorOptions); + this.jsonEditor.session.setUseWrapMode(false); + this.jsonEditor.setValue(this.contentValue ? this.contentValue : '', -1); + this.jsonEditor.on('change', () => { + if (!this.ignoreChange) { + this.cleanupJsonErrors(); + this.updateView(); + } + }); + this.editorResize$ = new ResizeObserver(() => { + this.onAceEditorResize(); + }); + this.editorResize$.observe(editorElement); + } + + ngOnDestroy(): void { + if (this.editorResize$) { + this.editorResize$.disconnect(); + } + } + + private onAceEditorResize() { + if (this.editorsResizeCaf) { + this.editorsResizeCaf(); + this.editorsResizeCaf = null; + } + this.editorsResizeCaf = this.raf.raf(() => { + this.jsonEditor.resize(); + this.jsonEditor.renderer.updateFull(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.jsonEditor) { + this.jsonEditor.setReadOnly(this.disabled || this.readonly); + } + } + + public validate(c: FormControl) { + return (this.objectValid) ? null : { + jsonParseError: { + valid: false, + }, + }; + } + + validateOnSubmit(): void { + if (!this.disabled && !this.readonly) { + this.cleanupJsonErrors(); + if (!this.objectValid) { + this.store.dispatch(new ActionNotificationShow( + { + message: this.validationError, + type: 'error', + target: this.toastTargetId, + verticalPosition: 'bottom', + horizontalPosition: 'left' + })); + this.errorShowed = true; + } + } + } + + cleanupJsonErrors(): void { + if (this.errorShowed) { + this.store.dispatch(new ActionNotificationHide( + { + target: this.toastTargetId + })); + this.errorShowed = false; + } + } + + beautifyJSON() { + if (this.jsonEditor && this.objectValid) { + const res = JSON.stringify(this.modelValue, null, 2); + this.jsonEditor.setValue(res ? res : '', -1); + this.updateView(); + } + } + + minifyJSON() { + if (this.jsonEditor && this.objectValid) { + const res = JSON.stringify(this.modelValue); + this.jsonEditor.setValue(res ? res : '', -1); + this.updateView(); + } + } + + writeValue(value: any): void { + this.modelValue = value; + this.contentValue = ''; + this.objectValid = false; + try { + if (this.modelValue) { + this.contentValue = JSON.stringify(this.modelValue, undefined, 2); + this.objectValid = true; + } else { + this.objectValid = !this.required; + this.validationError = 'Json object is required.'; + } + } catch (e) { + // + } + if (this.jsonEditor) { + this.ignoreChange = true; + this.jsonEditor.setValue(this.contentValue ? this.contentValue : '', -1); + this.ignoreChange = false; + } + } + + updateView() { + const editorValue = this.jsonEditor.getValue(); + if (this.contentValue !== editorValue) { + this.contentValue = editorValue; + let data = null; + this.objectValid = false; + if (this.contentValue && this.contentValue.length > 0) { + try { + data = JSON.parse(this.contentValue); + this.objectValid = true; + this.validationError = ''; + } catch (ex) { + let errorInfo = 'Error:'; + if (ex.name) { + errorInfo += ' ' + ex.name + ':'; + } + if (ex.message) { + errorInfo += ' ' + ex.message; + } + this.validationError = errorInfo; + } + } else { + this.objectValid = !this.required; + this.validationError = this.required ? 'Json object is required.' : ''; + } + this.modelValue = data; + this.propagateChange(data); + } + } + + onFullscreen() { + if (this.jsonEditor) { + setTimeout(() => { + this.jsonEditor.resize(); + }, 0); + } + } + +} diff --git a/ui-ngx/src/app/shared/components/kv-map.component.html b/ui-ngx/src/app/shared/components/kv-map.component.html new file mode 100644 index 0000000000..77ee8e5b9f --- /dev/null +++ b/ui-ngx/src/app/shared/components/kv-map.component.html @@ -0,0 +1,59 @@ + +
    + +
    + + + + + + + + + +
    + {{noDataText ? noDataText : 'key-val.no-data'}} +
    + +
    +
    diff --git a/ui-ngx/src/app/shared/components/kv-map.component.scss b/ui-ngx/src/app/shared/components/kv-map.component.scss new file mode 100644 index 0000000000..203f234c7c --- /dev/null +++ b/ui-ngx/src/app/shared/components/kv-map.component.scss @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .tb-kv-map { + span.no-data-found { + position: relative; + display: flex; + height: 40px; + + &.disabled { + color: rgba(0, 0, 0, .38); + } + } + } +} + +:host ::ng-deep { + .mat-form-field-wrapper { + padding-bottom: 0; + } + .mat-form-field-infix { + border-top: 0; + } + .mat-form-field-underline { + bottom: 0; + } +} diff --git a/ui-ngx/src/app/shared/components/kv-map.component.ts b/ui-ngx/src/app/shared/components/kv-map.component.ts new file mode 100644 index 0000000000..ee98adf837 --- /dev/null +++ b/ui-ngx/src/app/shared/components/kv-map.component.ts @@ -0,0 +1,163 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Subscription } from 'rxjs'; + +@Component({ + selector: 'tb-key-val-map', + templateUrl: './kv-map.component.html', + styleUrls: ['./kv-map.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => KeyValMapComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => KeyValMapComponent), + multi: true, + } + ] +}) +export class KeyValMapComponent extends PageComponent implements ControlValueAccessor, OnInit, Validator { + + @Input() disabled: boolean; + + @Input() titleText: string; + + @Input() keyPlaceholderText: string; + + @Input() valuePlaceholderText: string; + + @Input() noDataText: string; + + kvListFormGroup: FormGroup; + + private propagateChange = null; + + private valueChangeSubscription: Subscription = null; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.kvListFormGroup = this.fb.group({}); + this.kvListFormGroup.addControl('keyVals', + this.fb.array([])); + } + + keyValsFormArray(): FormArray { + return this.kvListFormGroup.get('keyVals') as FormArray; + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState?(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.kvListFormGroup.disable({emitEvent: false}); + } else { + this.kvListFormGroup.enable({emitEvent: false}); + } + } + + writeValue(keyValMap: {[key: string]: string}): void { + if (this.valueChangeSubscription) { + this.valueChangeSubscription.unsubscribe(); + } + const keyValsControls: Array = []; + if (keyValMap) { + for (const property of Object.keys(keyValMap)) { + if (Object.prototype.hasOwnProperty.call(keyValMap, property)) { + keyValsControls.push(this.fb.group({ + key: [property, [Validators.required]], + value: [keyValMap[property], [Validators.required]] + })); + } + } + } + this.kvListFormGroup.setControl('keyVals', this.fb.array(keyValsControls)); + this.valueChangeSubscription = this.kvListFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + if (this.disabled) { + this.kvListFormGroup.disable({emitEvent: false}); + } else { + this.kvListFormGroup.enable({emitEvent: false}); + } + } + + public removeKeyVal(index: number) { + (this.kvListFormGroup.get('keyVals') as FormArray).removeAt(index); + } + + public addKeyVal() { + const keyValsFormArray = this.kvListFormGroup.get('keyVals') as FormArray; + keyValsFormArray.push(this.fb.group({ + key: ['', [Validators.required]], + value: ['', [Validators.required]] + })); + } + + public validate(c: FormControl) { + const kvList: {key: string; value: string}[] = this.kvListFormGroup.get('keyVals').value; + let valid = true; + for (const entry of kvList) { + if (!entry.key || !entry.value) { + valid = false; + break; + } + } + return (valid) ? null : { + keyVals: { + valid: false, + }, + }; + } + + private updateModel() { + const kvList: {key: string; value: string}[] = this.kvListFormGroup.get('keyVals').value; + const keyValMap: {[key: string]: string} = {}; + kvList.forEach((entry) => { + keyValMap[entry.key] = entry.value; + }); + this.propagateChange(keyValMap); + } +} diff --git a/ui-ngx/src/app/shared/components/led-light.component.html b/ui-ngx/src/app/shared/components/led-light.component.html new file mode 100644 index 0000000000..e7e3201338 --- /dev/null +++ b/ui-ngx/src/app/shared/components/led-light.component.html @@ -0,0 +1,18 @@ + +
    diff --git a/ui-ngx/src/app/shared/components/led-light.component.ts b/ui-ngx/src/app/shared/components/led-light.component.ts new file mode 100644 index 0000000000..aa2c373dfa --- /dev/null +++ b/ui-ngx/src/app/shared/components/led-light.component.ts @@ -0,0 +1,124 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, Component, ElementRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import Raphael, { RaphaelElement, RaphaelPaper, RaphaelSet } from 'raphael'; +import * as tinycolor_ from 'tinycolor2'; + +const tinycolor = tinycolor_; + +interface CircleElement extends RaphaelElement { + theGlow?: RaphaelSet; +} + +@Component({ + selector: 'tb-led-light', + templateUrl: './led-light.component.html', + styleUrls: [] +}) +export class LedLightComponent implements OnInit, AfterViewInit, OnChanges { + + @Input() size: number; + + @Input() colorOn: string; + + @Input() colorOff: string; + + @Input() offOpacity: number; + + private enabledValue: boolean; + get enabled(): boolean { + return this.enabledValue; + } + @Input() + set enabled(value: boolean) { + this.enabledValue = coerceBooleanProperty(value); + } + + private canvasSize: number; + private radius: number; + private glowSize: number; + private glowColor: string; + + private paper: RaphaelPaper; + private circleElement: CircleElement; + + constructor(private elementRef: ElementRef) { + } + + ngOnInit(): void { + this.offOpacity = this.offOpacity || 0.4; + this.glowColor = tinycolor(this.colorOn).lighten().toHexString(); + } + + ngAfterViewInit(): void { + this.update(); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'enabled') { + this.draw(); + } else if (propName === 'size') { + this.update(); + } + } + } + } + + private update() { + this.size = this.size || 50; + this.canvasSize = this.size; + this.radius = this.canvasSize / 4; + this.glowSize = this.radius / 5; + if (this.paper) { + this.paper.remove(); + } + this.paper = Raphael($('#canvas_container', this.elementRef.nativeElement)[0], this.canvasSize, this.canvasSize); + const center = this.canvasSize / 2; + this.circleElement = this.paper.circle(center, center, this.radius); + this.draw(); + } + + private draw() { + if (this.enabled) { + this.circleElement.attr('fill', this.colorOn); + this.circleElement.attr('stroke', this.colorOn); + this.circleElement.attr('opacity', 1); + if (this.circleElement.theGlow) { + this.circleElement.theGlow.remove(); + } + this.circleElement.theGlow = this.circleElement.glow( + { + color: this.glowColor, + width: this.radius + this.glowSize, + opacity: 0.8, + fill: true + }); + } else { + if (this.circleElement.theGlow) { + this.circleElement.theGlow.remove(); + } + this.circleElement.attr('fill', this.colorOff); + this.circleElement.attr('stroke', this.colorOff); + this.circleElement.attr('opacity', this.offOpacity); + } + } + +} diff --git a/ui-ngx/src/app/shared/components/logo.component.html b/ui-ngx/src/app/shared/components/logo.component.html new file mode 100644 index 0000000000..699f188061 --- /dev/null +++ b/ui-ngx/src/app/shared/components/logo.component.html @@ -0,0 +1,19 @@ + + diff --git a/ui-ngx/src/app/shared/components/logo.component.scss b/ui-ngx/src/app/shared/components/logo.component.scss new file mode 100644 index 0000000000..2c81be0ef8 --- /dev/null +++ b/ui-ngx/src/app/shared/components/logo.component.scss @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host-context(.login-logo) { + img.tb-logo-title { + width: 280px; + height: 60px; + text-decoration: none; + cursor: pointer; + border: none; + transform: none; + + &:focus { + outline: 0; + } + } +} diff --git a/ui-ngx/src/app/shared/components/logo.component.ts b/ui-ngx/src/app/shared/components/logo.component.ts new file mode 100644 index 0000000000..2069778e4b --- /dev/null +++ b/ui-ngx/src/app/shared/components/logo.component.ts @@ -0,0 +1,32 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; + +@Component({ + selector: 'tb-logo', + templateUrl: './logo.component.html', + styleUrls: ['./logo.component.scss'] +}) +export class LogoComponent { + + logo = 'assets/logo_title_white.svg'; + + gotoThingsboard(): void { + window.open('https://thingsboard.io', '_blank'); + } + +} diff --git a/ui-ngx/src/app/shared/components/mat-chip-draggable.directive.ts b/ui-ngx/src/app/shared/components/mat-chip-draggable.directive.ts new file mode 100644 index 0000000000..dd9d6eaba2 --- /dev/null +++ b/ui-ngx/src/app/shared/components/mat-chip-draggable.directive.ts @@ -0,0 +1,253 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, Directive, ElementRef, EventEmitter, HostListener, Output } from '@angular/core'; +import { MatChip, MatChipList } from '@angular/material/chips'; +import Timeout = NodeJS.Timeout; + +export interface MatChipDropEvent { + from: number; + to: number; +} + +@Directive({ + selector: 'mat-chip-list[tb-chip-draggable]', +}) +export class MatChipDraggableDirective implements AfterViewInit { + + @Output() + chipDrop = new EventEmitter(); + + private draggableChips: Array = []; + + constructor(private chipsList: MatChipList, + private elementRef: ElementRef) { + } + + @HostListener('document:mouseup') + onDocumentMouseUp() { + this.draggableChips.forEach((draggableChip) => { + draggableChip.preventDrag = false; + }); + } + + ngAfterViewInit(): void { + this.configureDraggableChipList(); + this.chipsList.chips.changes.subscribe(() => { + this.configureDraggableChipList(); + }); + } + + private configureDraggableChipList() { + const toRemove: Array = []; + this.chipsList.chips.forEach((chip) => { + const found = this.draggableChips.find((draggableChip) => draggableChip.chip === chip); + if (!found) { + this.draggableChips.push(new DraggableChip(chip, + this.chipsList, + this.elementRef.nativeElement, + this.chipDrop)); + } + } + ); + this.draggableChips.forEach((draggableChip) => { + const found = this.chipsList.chips.find((chip) => chip === draggableChip.chip); + if (!found) { + toRemove.push(draggableChip); + } + }); + toRemove.forEach((draggableChip) => { + const index = this.draggableChips.indexOf(draggableChip); + this.draggableChips.splice(index, 1); + }); + } +} + +const draggingClassName = 'dragging'; +const droppingClassName = 'dropping'; +const droppingBeforeClassName = 'dropping-before'; +const droppingAfterClassName = 'dropping-after'; + +let globalDraggingChipListId = null; + +class DraggableChip { + + private chipElement: HTMLElement; + private readonly handle: HTMLElement; + + private dragging = false; + private counter = 0; + + private dropPosition: 'after' | 'before'; + + private dropTimeout: Timeout; + + public preventDrag = false; + + private dropHandler = this.onDrop.bind(this); + private dragOverHandler = this.onDragOver.bind(this); + + constructor(public chip: MatChip, + private chipsList: MatChipList, + private chipListElement: HTMLElement, + private chipDrop: EventEmitter) { + this.chipElement = chip._elementRef.nativeElement; + this.chipElement.setAttribute('draggable', 'true'); + this.handle = this.chipElement.getElementsByClassName('tb-chip-drag-handle')[0] as HTMLElement; + this.chipElement.addEventListener('mousedown', this.onMouseDown.bind(this)); + this.chipElement.addEventListener('dragstart', this.onDragStart.bind(this)); + this.chipElement.addEventListener('dragend', this.onDragEnd.bind(this)); + this.chipElement.addEventListener('dragenter', this.onDragEnter.bind(this)); + this.chipElement.addEventListener('dragleave', this.onDragLeave.bind(this)); + } + + private onMouseDown(event: MouseEvent) { + if (event.target !== this.handle) { + this.preventDrag = true; + } + } + + private onDragStart(event: Event | any) { + if (this.preventDrag) { + event.preventDefault(); + } else { + this.dragging = true; + globalDraggingChipListId = this.chipListElement.id; + this.chipListElement.classList.add(draggingClassName); + this.chipElement.classList.add(draggingClassName); + event = (event as any).originalEvent || event; + const dataTransfer = event.dataTransfer; + dataTransfer.effectAllowed = 'copyMove'; + dataTransfer.dropEffect = 'move'; + dataTransfer.setData('text', this.index() + ''); + } + } + + private onDragEnter(event: Event | any) { + this.counter++; + if (this.dragging) { + return; + } + this.chipElement.removeEventListener('dragover', this.dragOverHandler); + this.chipElement.removeEventListener('drop', this.dropHandler); + + this.chipElement.addEventListener('dragover', this.dragOverHandler); + this.chipElement.addEventListener('drop', this.dropHandler); + } + + private onDragLeave(event: Event | any) { + this.counter--; + if (this.counter <= 0) { + this.counter = 0; + this.chipElement.classList.remove(droppingClassName); + this.chipElement.classList.remove(droppingAfterClassName); + this.chipElement.classList.remove(droppingBeforeClassName); + } + } + + private onDragEnd(event: Event | any) { + this.dragging = false; + globalDraggingChipListId = null; + this.chipListElement.classList.remove(draggingClassName); + this.chipElement.classList.remove(draggingClassName); + } + + private onDragOver(event: Event | any) { + if (this.dragging) { + return; + } + event.preventDefault(); + if (globalDraggingChipListId !== this.chipListElement.id) { + return; + } + const bounds = this.chipElement.getBoundingClientRect(); + event = (event as any).originalEvent || event; + const props = { + width: bounds.right - bounds.left, + height: bounds.bottom - bounds.top, + x: event.clientX - bounds.left, + y: event.clientY - bounds.top, + }; + + const horizontalOffset = props.x; + const horizontalMidPoint = props.width / 2; + + const verticalOffset = props.y; + const verticalMidPoint = props.height / 2; + + this.chipElement.classList.add(droppingClassName); + + this.chipElement.classList.remove(droppingAfterClassName); + this.chipElement.classList.remove(droppingBeforeClassName); + + if (horizontalOffset >= horizontalMidPoint || verticalOffset >= verticalMidPoint) { + this.dropPosition = 'after'; + this.chipElement.classList.add(droppingAfterClassName); + } else { + this.dropPosition = 'before'; + this.chipElement.classList.add(droppingBeforeClassName); + } + + } + + private onDrop(event: Event | any) { + this.counter = 0; + event.preventDefault(); + if (globalDraggingChipListId !== this.chipListElement.id) { + return; + } + event = (event as any).originalEvent || event; + const droppedItemIndex = parseInt(event.dataTransfer.getData('text'), 10); + const currentIndex = this.index(); + let newIndex; + if (this.dropPosition === 'before') { + if (droppedItemIndex < currentIndex) { + newIndex = currentIndex - 1; + } else { + newIndex = currentIndex; + } + } else { + if (droppedItemIndex < currentIndex) { + newIndex = currentIndex; + } else { + newIndex = currentIndex + 1; + } + } + if (this.dropTimeout) { + clearTimeout(this.dropTimeout); + } + this.dropTimeout = setTimeout(() => { + this.dropPosition = null; + + this.chipElement.classList.remove(droppingClassName); + this.chipElement.classList.remove(droppingAfterClassName); + this.chipElement.classList.remove(droppingBeforeClassName); + + this.chipElement.removeEventListener('drop', this.dropHandler); + + const dropEvent: MatChipDropEvent = { + from: droppedItemIndex, + to: newIndex + }; + this.chipDrop.emit(dropEvent); + }, 1000 / 16); + } + + private index(): number { + return this.chipsList.chips.toArray().indexOf(this.chip); + } + +} diff --git a/ui-ngx/src/app/shared/components/material-icon-select.component.html b/ui-ngx/src/app/shared/components/material-icon-select.component.html new file mode 100644 index 0000000000..b5f7a45b03 --- /dev/null +++ b/ui-ngx/src/app/shared/components/material-icon-select.component.html @@ -0,0 +1,24 @@ + +
    + {{materialIconFormGroup.get('icon').value}} + + icon.icon + + +
    diff --git a/ui-ngx/src/app/shared/components/material-icon-select.component.scss b/ui-ngx/src/app/shared/components/material-icon-select.component.scss new file mode 100644 index 0000000000..c48d07b106 --- /dev/null +++ b/ui-ngx/src/app/shared/components/material-icon-select.component.scss @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .mat-icon { + padding: 4px; + margin: 8px 4px 4px; + cursor: pointer; + border: solid 1px rgba(0, 0, 0, .27); + } +} + +:host ::ng-deep { + .mat-form-field-infix{ + width: 146px; + } +} diff --git a/ui-ngx/src/app/shared/components/material-icon-select.component.ts b/ui-ngx/src/app/shared/components/material-icon-select.component.ts new file mode 100644 index 0000000000..71d5679b73 --- /dev/null +++ b/ui-ngx/src/app/shared/components/material-icon-select.component.ts @@ -0,0 +1,105 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { DialogService } from '@core/services/dialog.service'; + +@Component({ + selector: 'tb-material-icon-select', + templateUrl: './material-icon-select.component.html', + styleUrls: ['./material-icon-select.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MaterialIconSelectComponent), + multi: true + } + ] +}) +export class MaterialIconSelectComponent extends PageComponent implements OnInit, ControlValueAccessor { + + @Input() + disabled: boolean; + + private modelValue: string; + + private propagateChange = null; + + public materialIconFormGroup: FormGroup; + + constructor(protected store: Store, + private dialogs: DialogService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.materialIconFormGroup = this.fb.group({ + icon: [null, []] + }); + + this.materialIconFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.materialIconFormGroup.disable({emitEvent: false}); + } else { + this.materialIconFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: string): void { + this.modelValue = value; + this.materialIconFormGroup.patchValue( + { icon: this.modelValue }, {emitEvent: false} + ); + } + + private updateModel() { + const icon: string = this.materialIconFormGroup.get('icon').value; + if (this.modelValue !== icon) { + this.modelValue = icon; + this.propagateChange(this.modelValue); + } + } + + openIconDialog() { + this.dialogs.materialIconPicker(this.materialIconFormGroup.get('icon').value).subscribe( + (icon) => { + if (icon) { + this.materialIconFormGroup.patchValue( + {icon}, {emitEvent: true} + ); + } + } + ); + } +} diff --git a/ui-ngx/src/app/shared/components/message-type-autocomplete.component.html b/ui-ngx/src/app/shared/components/message-type-autocomplete.component.html new file mode 100644 index 0000000000..1f1a229a96 --- /dev/null +++ b/ui-ngx/src/app/shared/components/message-type-autocomplete.component.html @@ -0,0 +1,43 @@ + + + {{ 'rulenode.message-type' | translate }} + + + + + + + + + {{ 'rulenode.message-type-required' | translate }} + + diff --git a/ui-ngx/src/app/shared/components/message-type-autocomplete.component.ts b/ui-ngx/src/app/shared/components/message-type-autocomplete.component.ts new file mode 100644 index 0000000000..f5663bc065 --- /dev/null +++ b/ui-ngx/src/app/shared/components/message-type-autocomplete.component.ts @@ -0,0 +1,179 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Observable, of } from 'rxjs'; +import { map, mergeMap, startWith, tap } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { MessageType, messageTypeNames } from '@shared/models/rule-node.models'; +import { objectValues } from '@core/utils'; + +@Component({ + selector: 'tb-message-type-autocomplete', + templateUrl: './message-type-autocomplete.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MessageTypeAutocompleteComponent), + multi: true + }] +}) +export class MessageTypeAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy { + + messageTypeFormGroup: FormGroup; + + modelValue: string | null; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @ViewChild('messageTypeInput', {static: true}) messageTypeInput: ElementRef; + + filteredMessageTypes: Observable>; + + searchText = ''; + + private dirty = false; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + public translate: TranslateService, + private fb: FormBuilder) { + this.messageTypeFormGroup = this.fb.group({ + messageType: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.filteredMessageTypes = this.messageTypeFormGroup.get('messageType').valueChanges + .pipe( + tap(value => { + this.updateView(value); + }), + startWith(''), + map(value => value ? value : ''), + mergeMap(messageType => this.fetchMessageTypes(messageType) ) + ); + } + + ngAfterViewInit(): void { + } + + ngOnDestroy(): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.messageTypeFormGroup.disable({emitEvent: false}); + } else { + this.messageTypeFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: string | null): void { + this.searchText = ''; + this.modelValue = value; + let res: MessageType | string = null; + if (value) { + if (objectValues(MessageType).includes(value)) { + res = MessageType[value]; + } else { + res = value; + } + } + this.messageTypeFormGroup.get('messageType').patchValue(res, {emitEvent: false}); + this.dirty = true; + } + + onFocus() { + if (this.dirty) { + this.messageTypeFormGroup.get('messageType').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + updateView(value: MessageType | string | null) { + let res: string = null; + if (value) { + if (objectValues(MessageType).includes(value)) { + res = MessageType[value]; + } else { + res = value; + } + } + if (this.modelValue !== res) { + this.modelValue = res; + this.propagateChange(this.modelValue); + } + } + + displayMessageTypeFn(messageType?: MessageType | string): string | undefined { + if (messageType) { + if (objectValues(MessageType).includes(messageType)) { + return messageTypeNames.get(MessageType[messageType]); + } else { + return messageType; + } + } + return undefined; + } + + fetchMessageTypes(searchText?: string): Observable> { + this.searchText = searchText; + const result: Array = []; + messageTypeNames.forEach((value, key) => { + if (value.toUpperCase().includes(searchText.toUpperCase())) { + result.push(key); + } + }); + if (result.length) { + return of(result); + } else { + return of([searchText]); + } + } + + clear() { + this.messageTypeFormGroup.get('messageType').patchValue(null, {emitEvent: true}); + setTimeout(() => { + this.messageTypeInput.nativeElement.blur(); + this.messageTypeInput.nativeElement.focus(); + }, 0); + } + +} diff --git a/ui/src/app/components/nav-tree.tpl.html b/ui-ngx/src/app/shared/components/nav-tree.component.html similarity index 100% rename from ui/src/app/components/nav-tree.tpl.html rename to ui-ngx/src/app/shared/components/nav-tree.component.html diff --git a/ui-ngx/src/app/shared/components/nav-tree.component.scss b/ui-ngx/src/app/shared/components/nav-tree.component.scss new file mode 100644 index 0000000000..ae318587e5 --- /dev/null +++ b/ui-ngx/src/app/shared/components/nav-tree.component.scss @@ -0,0 +1,391 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-nav-tree-container { + padding: 15px; + font-family: Roboto, "Helvetica Neue", sans-serif; + + &.jstree-proton, &.jstree-proton-small, &.jstree-proton-large { + .jstree-node, + .jstree-icon { + background-image: url("../../../assets/jstree/tb32px.png"); + } + + .jstree-last { + background: transparent; + } + + .jstree-themeicon-custom { + background-image: none; + } + } + + &.jstree-proton { + + .jstree-themeicon-custom { + &.material-icons { + font-size: 18px; + } + } + + .jstree-anchor { + font-size: 16px; + } + + .jstree-file { + background: url("../../../assets/jstree/tb32px.png") -101px -69px no-repeat; + } + + .jstree-folder { + background: url("../../../assets/jstree/tb32px.png") -261px -5px no-repeat; + } + } + + &.jstree-proton-small { + + .jstree-themeicon-custom { + &.material-icons { + font-size: 14px; + } + } + + .jstree-anchor { + font-size: 14px; + } + + .jstree-file { + background: url("../../../assets/jstree/tb32px.png") -103px -71px no-repeat; + } + + .jstree-folder { + background: url("../../../assets/jstree/tb32px.png") -263px -7px no-repeat; + } + } + + &.jstree-proton-large { + + .jstree-themeicon-custom { + &.material-icons { + font-size: 24px; + } + } + + .jstree-anchor { + font-size: 20px; + } + + .jstree-file { + background: url("../../../assets/jstree/tb32px.png") -96px -64px no-repeat; + } + + .jstree-folder { + background: url("../../../assets/jstree/tb32px.png") -256px 0px no-repeat; + } + } + + a { + border-bottom: none; + + i.jstree-themeicon-custom { + &.tb-user-group { + &::before { + content: "account_circle"; + } + } + + &.tb-customer-group { + &::before { + content: "supervisor_account"; + } + } + + &.tb-asset-group { + &::before { + content: "domain"; + } + } + + &.tb-device-group { + &::before { + content: "devices_other"; + } + } + + &.tb-entity-view-group { + &::before { + content: "view_quilt"; + } + } + + &.tb-dashboard-group { + &::before { + content: "dashboard"; + } + } + + &.tb-customer { + &::before { + content: "supervisor_account"; + } + } + } + } +} + +#jstree-dnd { + &.jstree-proton, &.jstree-proton-small, &.jstree-proton-large { + .jstree-ok, + .jstree-er { + background-image: url("../../../assets/jstree/tb32px.png"); + } + } +} + +@media (max-width: 768px) { + .tb-nav-tree-container { + &.jstree-proton-responsive { + .jstree-node, + .jstree-icon, + .jstree-node > .jstree-ocl, + .jstree-themeicon, + .jstree-checkbox { + background-image: url("../../../assets/jstree/tb40px.png"); + background-size: 120px 240px; + } + + .jstree-container-ul { + overflow: visible; + } + + .jstree-themeicon-custom { + background-color: transparent; + background-image: none; + background-position: 0 0; + + &.material-icons { + margin: 0; + font-size: 24px; + } + } + + .jstree-node, + .jstree-leaf > .jstree-ocl { + background: 0 0; + } + + .jstree-node { + min-width: 40px; + min-height: 40px; + margin-left: 40px; + line-height: 40px; + white-space: nowrap; + background-repeat: repeat-y; + background-position: -80px 0; + } + + .jstree-last { + background: 0 0; + } + + .jstree-anchor { + height: 40px; + font-size: 1.1em; + font-weight: 700; + line-height: 40px; + text-shadow: 1px 1px #fff; + } + + .jstree-icon, + .jstree-icon:empty { + width: 40px; + height: 40px; + line-height: 40px; + } + + > { + .jstree-container-ul > .jstree-node { + margin-right: 0; + margin-left: 0; + } + } + + .jstree-ocl, + .jstree-themeicon, + .jstree-checkbox { + background-size: 120px 240px; + } + + .jstree-leaf > .jstree-ocl { + background-position: -40px -120px; + } + + .jstree-last > .jstree-ocl { + background-position: -40px -160px; + } + + .jstree-open > .jstree-ocl { + background-position: 0 0 !important; + } + + .jstree-closed > .jstree-ocl { + background-position: 0 -40px !important; + } + + .jstree-themeicon { + background-position: -40px -40px; + } + + .jstree-file { + background: url("../../../assets/jstree/tb40px.png") 0 -160px no-repeat; + background-size: 120px 240px; + } + .jstree-folder { + background: url("../../../assets/jstree/tb40px.png") -40px -40px no-repeat; + background-size: 120px 240px; + } + + .jstree-checkbox, + .jstree-checkbox:hover { + background-position: -40px -80px; + } + + &.jstree-checkbox-selection { + .jstree-clicked > .jstree-checkbox, + .jstree-clicked > .jstree-checkbox:hover { + background-position: 0 -80px; + } + } + + .jstree-checked > .jstree-checkbox, + .jstree-checked > .jstree-checkbox:hover { + background-position: 0 -80px; + } + + .jstree-anchor > .jstree-undetermined, + .jstree-anchor > .jstree-undetermined:hover { + background-position: 0 -120px; + } + + .jstree-striped { + background: 0 0; + } + + .jstree-wholerow { + height: 40px; + background: #ebebeb; + border-top: 1px solid rgba(255, 255, 255, .7); + border-bottom: 1px solid rgba(64, 64, 64, .2); + } + + .jstree-wholerow-hovered { + background: #e7f4f9; + } + + .jstree-wholerow-clicked { + background: #beebff; + } + + .jstree-children { + .jstree-last > .jstree-wholerow { + box-shadow: inset 0 -6px 3px -5px #666; + } + + .jstree-open > .jstree-wholerow { + border-top: 0; + box-shadow: inset 0 6px 3px -5px #666; + } + + .jstree-open + .jstree-open { + box-shadow: none; + } + } + + &.jstree-rtl { + .jstree-node { + margin-right: 40px; + margin-left: 0; + } + + .jstree-container-ul > .jstree-node { + margin-right: 0; + } + + .jstree-closed > .jstree-ocl { + background-position: -40px 0 !important; + } + } + } + } +} + +@media (max-width: 768px) { + #jstree-dnd { + &.jstree-dnd-responsive { + .jstree-ok, + .jstree-er { + background-image: url("../../../assets/jstree/tb40px.png"); + background-size: 120px 240px; + } + .jstree-ok { + background-position: 0 -200px; + } + .jstree-er { + background-position: -40px -200px; + } + } + } +} + +.tb-nav-tree .mat-button.tb-active { + font-weight: 500; + background-color: rgba(255, 255, 255, .15); +} + +.tb-nav-tree, +.tb-nav-tree ul { + margin-top: 0; + list-style: none; + + &:first-child { + padding: 0; + } + + li { + .mat-button { + width: 100%; + max-height: 40px; + padding: 0 16px; + margin: 0; + overflow: hidden; + line-height: 40px; + color: inherit; + text-align: left; + text-decoration: none; + text-overflow: ellipsis; + text-transform: none; + text-rendering: optimizeLegibility; + white-space: nowrap; + cursor: pointer; + border-radius: 0; + + span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + } +} + diff --git a/ui-ngx/src/app/shared/components/nav-tree.component.ts b/ui-ngx/src/app/shared/components/nav-tree.component.ts new file mode 100644 index 0000000000..a06a33d5c5 --- /dev/null +++ b/ui-ngx/src/app/shared/components/nav-tree.component.ts @@ -0,0 +1,272 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, Input, NgZone, OnInit, ViewEncapsulation } from '@angular/core'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { deepClone } from '@core/utils'; + +export interface NavTreeNodeState { + disabled?: boolean; + opened?: boolean; + loaded?: boolean; +} + +export interface NavTreeNode { + id: string; + icon?: boolean; + text?: string; + state?: NavTreeNodeState; + children?: NavTreeNode[] | boolean; + data?: any; +} + +export interface NavTreeEditCallbacks { + selectNode?: (id: string) => void; + deselectAll?: () => void; + getNode?: (id: string) => NavTreeNode; + getParentNodeId?: (id: string) => string; + openNode?: (id: string, cb?: () => void) => void; + nodeIsOpen?: (id: string) => boolean; + nodeIsLoaded?: (id: string) => boolean; + refreshNode?: (id: string) => void; + updateNode?: (id: string, newName: string) => void; + createNode?: (parentId: string, node: NavTreeNode, pos: number | string) => void; + deleteNode?: (id: string) => void; + disableNode?: (id: string) => void; + enableNode?: (id: string) => void; + setNodeHasChildren?: (id: string, hasChildren: boolean) => void; + search?: (searchText: string) => void; + clearSearch?: () => void; +} + +export type NodesCallback = (nodes: NavTreeNode[]) => void; +export type LoadNodesCallback = (node: NavTreeNode, cb: NodesCallback) => void; +export type NodeSearchCallback = (searchText: string, node: NavTreeNode) => boolean; +export type NodeSelectedCallback = (node: NavTreeNode, event: Event) => void; +export type NodesInsertedCallback = (nodes: string[], parent: string) => void; + +@Component({ + selector: 'tb-nav-tree', + templateUrl: './nav-tree.component.html', + styleUrls: ['./nav-tree.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class NavTreeComponent implements OnInit { + + private enableSearchValue: boolean; + get enableSearch(): boolean { + return this.enableSearchValue; + } + @Input() + set enableSearch(value: boolean) { + this.enableSearchValue = coerceBooleanProperty(value); + } + + @Input() + private loadNodes: LoadNodesCallback; + + @Input() + private searchCallback: NodeSearchCallback; + + @Input() + private onNodeSelected: NodeSelectedCallback; + + @Input() + private onNodesInserted: NodesInsertedCallback; + + @Input() + private editCallbacks: NavTreeEditCallbacks; + + private treeElement: JSTree; + + constructor(private elementRef: ElementRef, + private ngZone: NgZone) { + } + + ngOnInit(): void { + this.initTree(); + } + + private initTree() { + + const loadNodes: LoadNodesCallback = (node, cb) => { + const outCb = (nodes: NavTreeNode[]) => { + const copied: NavTreeNode[] = []; + if (nodes) { + nodes.forEach((n) => { + copied.push(deepClone(n, ['data'])); + }); + } + cb(copied); + }; + this.ngZone.runOutsideAngular(() => { + this.loadNodes(node, outCb); + }); + }; + + const config: JSTreeStaticDefaults = { + core: { + worker: false, + multiple: false, + check_callback: true, + themes: { name: 'proton', responsive: true }, + data: loadNodes, + error: () => { + console.error('Unexpected jstree error!'); + } + }, + plugins: [] + }; + + if (this.enableSearch) { + config.plugins.push('search'); + config.search = { + ajax: false, + fuzzy: false, + close_opened_onclear: true, + case_sensitive: false, + show_only_matches: true, + show_only_matches_children: false, + search_leaves_only: false, + search_callback: this.searchCallback + }; + } + + this.treeElement = $('.tb-nav-tree-container', this.elementRef.nativeElement).jstree(config); + + this.treeElement.on('changed.jstree', (e: any, data) => { + const node: NavTreeNode = data.instance.get_selected(true)[0]; + if (this.onNodeSelected) { + this.ngZone.run(() => this.onNodeSelected(node, e as Event)); + } + }); + + this.treeElement.on('model.jstree', (e: any, data) => { + if (this.onNodesInserted) { + this.ngZone.run(() => this.onNodesInserted(data.nodes, data.parent)); + } + }); + + if (this.editCallbacks) { + this.editCallbacks.selectNode = id => { + const node: NavTreeNode = this.treeElement.jstree('get_node', id); + if (node) { + this.treeElement.jstree('deselect_all', true); + this.treeElement.jstree('select_node', node); + } + }; + this.editCallbacks.deselectAll = () => { + this.treeElement.jstree('deselect_all'); + }; + this.editCallbacks.getNode = (id) => { + const node: NavTreeNode = this.treeElement.jstree('get_node', id); + return node; + }; + this.editCallbacks.getParentNodeId = (id) => { + const node: NavTreeNode = this.treeElement.jstree('get_node', id); + if (node) { + return this.treeElement.jstree('get_parent', node); + } + }; + this.editCallbacks.openNode = (id, cb) => { + const node: NavTreeNode = this.treeElement.jstree('get_node', id); + if (node) { + this.treeElement.jstree('open_node', node, cb); + } + }; + this.editCallbacks.nodeIsOpen = (id) => { + const node: NavTreeNode = this.treeElement.jstree('get_node', id); + if (node) { + return this.treeElement.jstree('is_open', node); + } else { + return true; + } + }; + this.editCallbacks.nodeIsLoaded = (id) => { + const node: NavTreeNode = this.treeElement.jstree('get_node', id); + if (node) { + return this.treeElement.jstree('is_loaded', node); + } else { + return true; + } + }; + this.editCallbacks.refreshNode = (id) => { + if (id === '#') { + this.treeElement.jstree('refresh'); + this.treeElement.jstree('redraw'); + } else { + const node: NavTreeNode = this.treeElement.jstree('get_node', id); + if (node) { + const opened = this.treeElement.jstree('is_open', node); + this.treeElement.jstree('refresh_node', node); + this.treeElement.jstree('redraw'); + if (node.children && opened/* && !node.children.length*/) { + this.treeElement.jstree('open_node', node); + } + } + } + }; + this.editCallbacks.updateNode = (id, newName) => { + const node: NavTreeNode = this.treeElement.jstree('get_node', id); + if (node) { + this.treeElement.jstree('rename_node', node, newName); + } + }; + this.editCallbacks.createNode = (parentId, node, pos) => { + const parentNode: NavTreeNode = this.treeElement.jstree('get_node', parentId); + if (parentNode) { + this.treeElement.jstree('create_node', parentNode, node, pos); + } + }; + this.editCallbacks.deleteNode = (id) => { + const node: NavTreeNode = this.treeElement.jstree('get_node', id); + if (node) { + this.treeElement.jstree('delete_node', node); + } + }; + this.editCallbacks.disableNode = (id) => { + const node: NavTreeNode = this.treeElement.jstree('get_node', id); + if (node) { + this.treeElement.jstree('disable_node', node); + } + }; + this.editCallbacks.enableNode = (id) => { + const node: NavTreeNode = this.treeElement.jstree('get_node', id); + if (node) { + this.treeElement.jstree('enable_node', node); + } + }; + this.editCallbacks.setNodeHasChildren = (id, hasChildren) => { + const node: NavTreeNode = this.treeElement.jstree('get_node', id); + if (node) { + if (!node.children || (Array.isArray(node.children) && !node.children.length)) { + node.children = hasChildren; + node.state.loaded = !hasChildren; + node.state.opened = false; + this.treeElement.jstree('_node_changed', node.id); + this.treeElement.jstree('redraw'); + } + } + }; + this.editCallbacks.search = (searchText) => { + this.treeElement.jstree('search', searchText); + }; + this.editCallbacks.clearSearch = () => { + this.treeElement.jstree('clear_search'); + }; + } + } +} diff --git a/ui-ngx/src/app/shared/components/page.component.ts b/ui-ngx/src/app/shared/components/page.component.ts new file mode 100644 index 0000000000..6827b30e3a --- /dev/null +++ b/ui-ngx/src/app/shared/components/page.component.ts @@ -0,0 +1,57 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Directive, OnDestroy } from '@angular/core'; +import { select, Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Observable, Subscription } from 'rxjs'; +import { selectIsLoading } from '@core/interceptors/load.selectors'; +import { delay, share } from 'rxjs/operators'; +import { AbstractControl } from '@angular/forms'; + +@Directive() +export abstract class PageComponent implements OnDestroy { + + isLoading$: Observable; + loadingSubscription: Subscription; + disabledOnLoadFormControls: Array = []; + + protected constructor(protected store: Store) { + this.isLoading$ = this.store.pipe(delay(0), select(selectIsLoading), share()); + } + + protected registerDisableOnLoadFormControl(control: AbstractControl) { + this.disabledOnLoadFormControls.push(control); + if (!this.loadingSubscription) { + this.loadingSubscription = this.isLoading$.subscribe((isLoading) => { + for (const formControl of this.disabledOnLoadFormControls) { + if (isLoading) { + formControl.disable({emitEvent: false}); + } else { + formControl.enable({emitEvent: false}); + } + } + }); + } + } + + ngOnDestroy(): void { + if (this.loadingSubscription) { + this.loadingSubscription.unsubscribe(); + } + } + +} diff --git a/ui-ngx/src/app/shared/components/public-api.ts b/ui-ngx/src/app/shared/components/public-api.ts new file mode 100644 index 0000000000..5e39116cec --- /dev/null +++ b/ui-ngx/src/app/shared/components/public-api.ts @@ -0,0 +1,21 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export * from './dialog/color-picker-dialog.component'; +export * from './dialog/material-icons-dialog.component'; +export * from './page.component'; +export * from './dialog.component'; +export * from './js-func.component'; diff --git a/ui-ngx/src/app/shared/components/queue/queue-type-list.component.html b/ui-ngx/src/app/shared/components/queue/queue-type-list.component.html new file mode 100644 index 0000000000..1380e7e28b --- /dev/null +++ b/ui-ngx/src/app/shared/components/queue/queue-type-list.component.html @@ -0,0 +1,44 @@ + + + {{ 'queue.name' | translate }} + + + + + + + + + {{ 'queue.name_required' | translate }} + + device-profile.select-queue-hint + diff --git a/ui-ngx/src/app/shared/components/queue/queue-type-list.component.ts b/ui-ngx/src/app/shared/components/queue/queue-type-list.component.ts new file mode 100644 index 0000000000..7e65bfbd91 --- /dev/null +++ b/ui-ngx/src/app/shared/components/queue/queue-type-list.component.ts @@ -0,0 +1,177 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Observable } from 'rxjs'; +import { map, mergeMap, publishReplay, refCount, tap } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { QueueService } from '@core/http/queue.service'; +import { ServiceType } from '@shared/models/queue.models'; + +@Component({ + selector: 'tb-queue-type-list', + templateUrl: './queue-type-list.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => QueueTypeListComponent), + multi: true + }] +}) +export class QueueTypeListComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy { + + queueFormGroup: FormGroup; + + modelValue: string | null; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @Input() + queueType: ServiceType; + + @ViewChild('queueInput', {static: true}) queueInput: ElementRef; + + filteredQueues: Observable>; + + queues: Observable>; + + searchText = ''; + + private dirty = false; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + public translate: TranslateService, + private queueService: QueueService, + private fb: FormBuilder) { + this.queueFormGroup = this.fb.group({ + queue: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.filteredQueues = this.queueFormGroup.get('queue').valueChanges + .pipe( + tap(value => { + this.updateView(value); + }), + map(value => value ? value : ''), + mergeMap(queue => this.fetchQueues(queue) ) + ); + } + + ngAfterViewInit(): void { + } + + ngOnDestroy(): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.queueFormGroup.disable({emitEvent: false}); + } else { + this.queueFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: string | null): void { + this.searchText = ''; + this.modelValue = value; + this.queueFormGroup.get('queue').patchValue(value, {emitEvent: false}); + this.dirty = true; + } + + onFocus() { + if (this.dirty) { + this.queueFormGroup.get('queue').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + updateView(value: string | null) { + if (this.modelValue !== value) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + displayQueueFn(queue?: string): string | undefined { + return queue ? queue : undefined; + } + + fetchQueues(searchText?: string): Observable> { + this.searchText = searchText; + return this.getQueues().pipe( + map(queues => { + const result = queues.filter( queue => { + return searchText ? queue.toUpperCase().startsWith(searchText.toUpperCase()) : true; + }); + if (result.length) { + if (searchText && searchText.length && result.indexOf(searchText) === -1) { + result.push(searchText); + } + result.sort(); + } else if (searchText && searchText.length) { + result.push(searchText); + } + return result; + }) + ); + } + + getQueues(): Observable> { + if (!this.queues) { + this.queues = this.queueService. + getTenantQueuesByServiceType(this.queueType, {ignoreLoading: true}).pipe( + publishReplay(1), + refCount() + ); + } + return this.queues; + } + + clear() { + this.queueFormGroup.get('queue').patchValue(null, {emitEvent: true}); + setTimeout(() => { + this.queueInput.nativeElement.blur(); + this.queueInput.nativeElement.focus(); + }, 0); + } + +} diff --git a/ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.html b/ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.html new file mode 100644 index 0000000000..c4f28ee796 --- /dev/null +++ b/ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.html @@ -0,0 +1,42 @@ + + + + + + + + + + + {{ 'relation.relation-type-required' | translate }} + + diff --git a/ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.ts b/ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.ts new file mode 100644 index 0000000000..13026b2084 --- /dev/null +++ b/ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.ts @@ -0,0 +1,156 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Observable, of } from 'rxjs'; +import { map, mergeMap, tap } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { BroadcastService } from '@app/core/services/broadcast.service'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { RelationTypes } from '@app/shared/models/relation.models'; + +@Component({ + selector: 'tb-relation-type-autocomplete', + templateUrl: './relation-type-autocomplete.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RelationTypeAutocompleteComponent), + multi: true + }] +}) +export class RelationTypeAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy { + + relationTypeFormGroup: FormGroup; + + modelValue: string | null; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @ViewChild('relationTypeInput', {static: true}) relationTypeInput: ElementRef; + + filteredRelationTypes: Observable>; + + searchText = ''; + + private dirty = false; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private broadcast: BroadcastService, + public translate: TranslateService, + private fb: FormBuilder) { + this.relationTypeFormGroup = this.fb.group({ + relationType: [null, this.required ? [Validators.required] : []] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + + this.filteredRelationTypes = this.relationTypeFormGroup.get('relationType').valueChanges + .pipe( + tap(value => { + this.updateView(value); + }), + // startWith(''), + map(value => value ? value : ''), + mergeMap(type => this.fetchRelationTypes(type) ) + ); + } + + ngAfterViewInit(): void { + } + + ngOnDestroy(): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.relationTypeFormGroup.disable({emitEvent: false}); + } else { + this.relationTypeFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: string | null): void { + this.searchText = ''; + this.modelValue = value; + this.relationTypeFormGroup.get('relationType').patchValue(value, {emitEvent: false}); + this.dirty = true; + } + + onFocus() { + if (this.dirty) { + this.relationTypeFormGroup.get('relationType').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + updateView(value: string | null) { + if (this.modelValue !== value) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + displayRelationTypeFn(relationType?: string): string | undefined { + return relationType ? relationType : undefined; + } + + fetchRelationTypes(searchText?: string, strictMatch: boolean = false): Observable> { + this.searchText = searchText; + return of(RelationTypes).pipe( + map(relationTypes => relationTypes.filter( relationType => { + if (strictMatch) { + return searchText ? relationType === searchText : false; + } else { + return searchText ? relationType.toUpperCase().startsWith(searchText.toUpperCase()) : true; + } + })) + ); + } + + clear() { + this.relationTypeFormGroup.get('relationType').patchValue(null, {emitEvent: true}); + setTimeout(() => { + this.relationTypeInput.nativeElement.blur(); + this.relationTypeInput.nativeElement.focus(); + }, 0); + } + +} diff --git a/ui-ngx/src/app/shared/components/snack-bar-component.html b/ui-ngx/src/app/shared/components/snack-bar-component.html new file mode 100644 index 0000000000..573166fd1e --- /dev/null +++ b/ui-ngx/src/app/shared/components/snack-bar-component.html @@ -0,0 +1,29 @@ + +
    +
    + +
    diff --git a/ui-ngx/src/app/shared/components/snack-bar-component.scss b/ui-ngx/src/app/shared/components/snack-bar-component.scss new file mode 100644 index 0000000000..8b211bffce --- /dev/null +++ b/ui-ngx/src/app/shared/components/snack-bar-component.scss @@ -0,0 +1,73 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + display: inline-block; + pointer-events: all; + &.toast-panel { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: 1000; + display: flex; + &.left { + justify-content: flex-start; + } + &.right { + justify-content: flex-end; + } + &.top { + align-items: flex-start; + } + &.bottom { + align-items: flex-end; + } + &.h-center { + justify-content: center; + } + &.v-center { + align-items: center; + } + } + .tb-toast { + box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12); + color: #fff; + font-size: 18px; + border-radius: 4px; + padding: 0 18px; + margin: 8px; + .toast-text { + padding: 0 6px; + width: 100%; + } + button { + margin: 6px 0 6px 12px; + } + &.info-toast { + background: #323232; + } + &.warn-toast { + background: #dc6d1b; + } + &.error-toast { + background: #800000; + } + &.success-toast { + background: #008000; + } + } +} diff --git a/ui-ngx/src/app/shared/components/socialshare-panel.component.html b/ui-ngx/src/app/shared/components/socialshare-panel.component.html new file mode 100644 index 0000000000..d283e5a3ae --- /dev/null +++ b/ui-ngx/src/app/shared/components/socialshare-panel.component.html @@ -0,0 +1,53 @@ + +
    + + + + +
    diff --git a/ui-ngx/src/app/shared/components/socialshare-panel.component.scss b/ui-ngx/src/app/shared/components/socialshare-panel.component.scss new file mode 100644 index 0000000000..9ad5c09ade --- /dev/null +++ b/ui-ngx/src/app/shared/components/socialshare-panel.component.scss @@ -0,0 +1,18 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + display: block; +} diff --git a/ui-ngx/src/app/shared/components/socialshare-panel.component.ts b/ui-ngx/src/app/shared/components/socialshare-panel.component.ts new file mode 100644 index 0000000000..bfde800a0e --- /dev/null +++ b/ui-ngx/src/app/shared/components/socialshare-panel.component.ts @@ -0,0 +1,53 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Input, OnInit } from '@angular/core'; +import { isLocalUrl } from '@core/utils'; + +@Component({ + selector: 'tb-social-share-panel', + templateUrl: './socialshare-panel.component.html', + styleUrls: ['./socialshare-panel.component.scss'] +}) +export class SocialSharePanelComponent implements OnInit { + + @Input() + shareTitle: string; + + @Input() + shareText: string; + + @Input() + shareLink: string; + + @Input() + shareHashTags: string; + + constructor() { + } + + ngOnInit(): void { + } + + isShareLinkLocal(): boolean { + if (this.shareLink && this.shareLink.length > 0) { + return isLocalUrl(this.shareLink); + } else { + return true; + } + } + +} diff --git a/ui-ngx/src/app/shared/components/tb-anchor.component.ts b/ui-ngx/src/app/shared/components/tb-anchor.component.ts new file mode 100644 index 0000000000..e93040a4a5 --- /dev/null +++ b/ui-ngx/src/app/shared/components/tb-anchor.component.ts @@ -0,0 +1,25 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ViewContainerRef } from '@angular/core'; + +@Component({ + selector: 'tb-anchor', + template: '' +}) +export class TbAnchorComponent { + constructor(public viewContainerRef: ViewContainerRef) { } +} diff --git a/ui-ngx/src/app/shared/components/tb-checkbox.component.html b/ui-ngx/src/app/shared/components/tb-checkbox.component.html new file mode 100644 index 0000000000..02a18633e1 --- /dev/null +++ b/ui-ngx/src/app/shared/components/tb-checkbox.component.html @@ -0,0 +1,24 @@ + + + + diff --git a/ui-ngx/src/app/shared/components/tb-checkbox.component.ts b/ui-ngx/src/app/shared/components/tb-checkbox.component.ts new file mode 100644 index 0000000000..d063718af2 --- /dev/null +++ b/ui-ngx/src/app/shared/components/tb-checkbox.component.ts @@ -0,0 +1,74 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, forwardRef, Input, Output } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; + +@Component({ + selector: 'tb-checkbox', + templateUrl: './tb-checkbox.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TbCheckboxComponent), + multi: true + } + ] +}) +export class TbCheckboxComponent implements ControlValueAccessor { + + innerValue: boolean; + + @Input() disabled: boolean; + @Input() trueValue: any = true; + @Input() falseValue: any = false; + @Output() valueChange = new EventEmitter(); + + private propagateChange = (_: any) => {}; + + onHostChange(ev) { + this.propagateChange(ev.checked ? this.trueValue : this.falseValue); + } + + modelChange($event) { + if ($event) { + this.innerValue = true; + this.valueChange.emit(this.trueValue); + } else { + this.innerValue = false; + this.valueChange.emit(this.falseValue); + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(obj: any): void { + if (obj === this.trueValue) { + this.innerValue = true; + } else { + this.innerValue = false; + } + } +} diff --git a/ui-ngx/src/app/shared/components/tb-error.component.ts b/ui-ngx/src/app/shared/components/tb-error.component.ts new file mode 100644 index 0000000000..2a19f6cb0a --- /dev/null +++ b/ui-ngx/src/app/shared/components/tb-error.component.ts @@ -0,0 +1,67 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Input } from '@angular/core'; +import { animate, state, style, transition, trigger } from '@angular/animations'; + +@Component({ + selector: 'tb-error', + template: ` +
    + + {{message}} + +
    + `, + styles: [` + :host { + height: 24px; + } + `], + animations: [ + trigger('animation', [ + state('show', style({ + opacity: 1, + })), + state('hide', style({ + opacity: 0, + transform: 'translateY(-1rem)' + })), + transition('show => hide', animate('200ms ease-out')), + transition('* => show', animate('200ms ease-in')) + + ]), + ] +}) +export class TbErrorComponent { + errorValue: any; + state: any; + message; + + @Input() + set error(value) { + if (value && !this.message) { + this.message = value; + this.state = 'hide'; + setTimeout(() => { + this.state = 'show'; + }); + } else { + this.errorValue = value; + this.state = value ? 'show' : 'hide'; + } + } +} diff --git a/ui-ngx/src/app/shared/components/time/datetime-period.component.html b/ui-ngx/src/app/shared/components/time/datetime-period.component.html new file mode 100644 index 0000000000..1ed050194f --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/datetime-period.component.html @@ -0,0 +1,47 @@ + +
    +
    + + datetime.date-from + + + + + + datetime.time-from + + + + +
    +
    + + datetime.date-to + + + + + + datetime.time-to + + + + +
    +
    diff --git a/ui-ngx/src/app/shared/components/time/datetime-period.component.scss b/ui-ngx/src/app/shared/components/time/datetime-period.component.scss new file mode 100644 index 0000000000..5e5a9c0ede --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/datetime-period.component.scss @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../scss/constants'; + +:host ::ng-deep { + .mat-form-field-wrapper { + padding-bottom: 8px; + } + .mat-form-field-underline { + bottom: 8px; + } + .mat-form-field-infix { + width: 150px; + + @media #{$mat-xs} { + width: 100%; + } + } +} diff --git a/ui-ngx/src/app/shared/components/time/datetime-period.component.ts b/ui-ngx/src/app/shared/components/time/datetime-period.component.ts new file mode 100644 index 0000000000..006c2559bb --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/datetime-period.component.ts @@ -0,0 +1,138 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { FixedWindow } from '@shared/models/time/time.models'; + +@Component({ + selector: 'tb-datetime-period', + templateUrl: './datetime-period.component.html', + styleUrls: ['./datetime-period.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DatetimePeriodComponent), + multi: true + } + ] +}) +export class DatetimePeriodComponent implements OnInit, ControlValueAccessor { + + @Input() disabled: boolean; + + modelValue: FixedWindow; + + startDate: Date; + endDate: Date; + + endTime: any; + + maxStartDate: Date; + minEndDate: Date; + maxEndDate: Date; + + changePending = false; + + private propagateChange = null; + + constructor() { + } + + ngOnInit(): void { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + if (this.changePending && this.propagateChange) { + this.changePending = false; + this.propagateChange(this.modelValue); + } + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(datePeriod: FixedWindow): void { + this.modelValue = datePeriod; + if (this.modelValue) { + this.startDate = new Date(this.modelValue.startTimeMs); + this.endDate = new Date(this.modelValue.endTimeMs); + } else { + const date = new Date(); + this.startDate = new Date( + date.getFullYear(), + date.getMonth(), + date.getDate() - 1, + date.getHours(), + date.getMinutes(), + date.getSeconds(), + date.getMilliseconds()); + this.endDate = date; + this.updateView(); + } + this.updateMinMaxDates(); + } + + updateView() { + let value: FixedWindow = null; + if (this.startDate && this.endDate) { + value = { + startTimeMs: this.startDate.getTime(), + endTimeMs: this.endDate.getTime() + }; + } + this.modelValue = value; + if (!this.propagateChange) { + this.changePending = true; + } else { + this.propagateChange(this.modelValue); + } + } + + updateMinMaxDates() { + this.maxStartDate = new Date(this.endDate.getTime() - 1000); + this.minEndDate = new Date(this.startDate.getTime() + 1000); + this.maxEndDate = new Date(); + } + + onStartDateChange() { + if (this.startDate) { + if (this.startDate.getTime() > this.maxStartDate.getTime()) { + this.startDate = new Date(this.maxStartDate.getTime()); + } + this.updateMinMaxDates(); + } + this.updateView(); + } + + onEndDateChange() { + if (this.endDate) { + if (this.endDate.getTime() < this.minEndDate.getTime()) { + this.endDate = new Date(this.minEndDate.getTime()); + } else if (this.endDate.getTime() > this.maxEndDate.getTime()) { + this.endDate = new Date(this.maxEndDate.getTime()); + } + this.updateMinMaxDates(); + } + this.updateView(); + } + +} diff --git a/ui-ngx/src/app/shared/components/time/datetime.component.html b/ui-ngx/src/app/shared/components/time/datetime.component.html new file mode 100644 index 0000000000..d2262a7e91 --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/datetime.component.html @@ -0,0 +1,40 @@ + +
    + + {{ dateText | translate }} + + + + + + {{ timeText | translate }} + + + + +
    diff --git a/ui-ngx/src/app/shared/components/time/datetime.component.scss b/ui-ngx/src/app/shared/components/time/datetime.component.scss new file mode 100644 index 0000000000..ad297c6c02 --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/datetime.component.scss @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host ::ng-deep { + .mat-form-field-wrapper { + padding-bottom: 8px; + } + .mat-form-field-underline { + bottom: 8px; + } + .mat-form-field-infix { + width: auto; + min-width: 100px; + } + mat-form-field { + &.no-label { + .mat-form-field-infix { + border-top-width: 0.2em; + } + } + } +} diff --git a/ui-ngx/src/app/shared/components/time/datetime.component.ts b/ui-ngx/src/app/shared/components/time/datetime.component.ts new file mode 100644 index 0000000000..0152d26984 --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/datetime.component.ts @@ -0,0 +1,114 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; + +@Component({ + selector: 'tb-datetime', + templateUrl: './datetime.component.html', + styleUrls: ['./datetime.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DatetimeComponent), + multi: true + } + ] +}) +export class DatetimeComponent implements OnInit, ControlValueAccessor { + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @Input() + dateText: string; + + @Input() + timeText: string; + + @Input() + showLabel = true; + + minDateValue: Date | null; + + @Input() + set minDate(minDate: number | null) { + this.minDateValue = minDate ? new Date(minDate) : null; + } + + maxDateValue: Date | null; + + @Input() + set maxDate(maxDate: number | null) { + this.maxDateValue = maxDate ? new Date(maxDate) : null; + } + + modelValue: number; + + date: Date; + + private propagateChange = (v: any) => { }; + + constructor() { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + ngOnInit(): void { + } + + writeValue(datetime: number | null): void { + this.modelValue = datetime; + if (this.modelValue) { + this.date = new Date(this.modelValue); + } else { + this.date = null; + } + } + + updateView(value: number | null) { + if (this.modelValue !== value) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + onDateChange() { + const value = this.date ? this.date.getTime() : null; + this.updateView(value); + } + +} diff --git a/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.html b/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.html new file mode 100644 index 0000000000..2fb3bb1db5 --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.html @@ -0,0 +1,54 @@ + +
    +
    + + +
    + + +
    + {{ this.currentTime | date:'medium'}} + {{ "widget.no-data-found" | translate}} +
    +
    + + + + + + {{speedValue}} + +
    +
    \ No newline at end of file diff --git a/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.scss b/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.scss new file mode 100644 index 0000000000..aa152e9e7a --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.scss @@ -0,0 +1,132 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.trip-animation-label-container { + height: 24px; +} + +.trip-animation-container { + position: relative; + z-index: 1; + flex: 1; + width: 100%; + + #trip-animation-map { + z-index: 1; + width: 100%; + height: 100%; + + .pointsLayerMarkerIcon { + border-radius: 50%; + } + } + + .trip-animation-info-panel { + position: absolute; + top: 0; + right: 0; + z-index: 2; + pointer-events: none; + + .mat-button { + top: 0; + left: 0; + width: 32px; + min-width: 32px; + height: 32px; + min-height: 32px; + padding: 0 0 2px; + margin: 2px; + line-height: 24px; + + mat-icon { + width: 24px; + height: 24px; + + svg { + width: inherit; + height: inherit; + } + } + } + } + + .trip-animation-tooltip { + position: absolute; + top: 38px; + right: 0; + z-index: 2; + padding: 10px; + background-color: #fff; + transition: 0.3s ease-in-out; + + &-hidden { + transform: translateX(110%); + } + } +} + +.trip-animation-control-panel { + position: relative; + box-sizing: border-box; + width: 100%; + padding-bottom: 16px; + padding-left: 10px; + + mat-slider-container { + mat-slider { + min-width: 80px; + } + } + + .mat-icon-button { + width: 44px; + min-width: 44px; + height: 48px; + min-height: 48px; + margin: 0; + line-height: 28px; + + mat-icon { + width: 24px; + height: 24px; + font-size: 24px; + + svg { + width: inherit; + height: inherit; + } + } + + mat-select { + margin: 0; + } + } + + .panel-timer { + max-width: none; + margin-top: -20px; + font-size: 12px; + font-weight: 500; + text-align: center; + } +} + +.speed-select { + width: 70px; + margin-left: 10px; + margin-top: 10px; +} diff --git a/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.ts b/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.ts new file mode 100644 index 0000000000..6c5f4d9576 --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.ts @@ -0,0 +1,145 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; +import { interval } from 'rxjs'; +import { filter } from 'rxjs/operators'; +import { HistorySelectSettings } from '@app/modules/home/components/widget/lib/maps/map-models'; + +@Component({ + selector: 'tb-history-selector', + templateUrl: './history-selector.component.html', + styleUrls: ['./history-selector.component.scss'] +}) +export class HistorySelectorComponent implements OnInit, OnChanges { + + @Input() settings: HistorySelectSettings + @Input() minTime: number; + @Input() maxTime: number; + @Input() step = 1000; + @Input() anchors = []; + @Input() useAnchors = false; + + @Output() timeUpdated: EventEmitter = new EventEmitter(); + + minTimeIndex = 0; + maxTimeIndex = 0; + speed = 1; + index = 0; + playing = false; + interval; + speeds = [1, 5, 10, 25]; + currentTime = null; + + + constructor(private cd: ChangeDetectorRef) { } + + ngOnInit(): void { + } + + ngOnChanges() { + this.maxTimeIndex = Math.ceil((this.maxTime - this.minTime) / this.step); + this.currentTime = this.minTime === Infinity ? null : this.minTime; + } + + play() { + this.playing = true; + if (!this.interval) + this.interval = interval(1000 / this.speed) + .pipe( + filter(() => this.playing)).subscribe(() => { + this.index++; + this.currentTime = this.minTime + this.index * this.step; + if (this.index <= this.maxTimeIndex) { + this.cd.detectChanges(); + this.timeUpdated.emit(this.currentTime); + } + else { + this.interval.complete(); + } + }, err => { + console.error(err); + }, () => { + this.currentTime = this.index = this.minTimeIndex; + this.playing = false; + this.interval = null; + this.cd.detectChanges(); + }); + } + + reeneble() { + if (this.playing) { + const position = this.index; + this.interval.complete(); + this.index = position; + this.play(); + } + } + + pause() { + this.playing = false; + this.currentTime = this.minTime + this.index * this.step; + this.cd.detectChanges(); + this.timeUpdated.emit(this.currentTime); + } + + moveNext() { + if (this.index < this.maxTimeIndex) { + if (this.useAnchors) { + const anchorIndex = this.findIndex(this.currentTime, this.anchors) + 1; + this.index = Math.floor((this.anchors[anchorIndex] - this.minTime) / this.step); + } else { + this.index++; + } + } + this.pause(); + } + + movePrev() { + if (this.index > this.minTimeIndex) { + if (this.useAnchors) { + const anchorIndex = this.findIndex(this.currentTime, this.anchors) - 1; + this.index = Math.floor((this.anchors[anchorIndex] - this.minTime) / this.step); + } else { + this.index--; + } + } + this.pause(); + } + + findIndex(value: number, array: number[]): number { + let i = 0; + while (array[i] < value) { + i++; + } + return i; + } + + moveStart() { + this.index = this.minTimeIndex; + this.pause(); + } + + moveEnd() { + this.index = this.maxTimeIndex; + this.pause(); + } + + changeIndex() { + this.currentTime = this.minTime + this.index * this.step; + this.timeUpdated.emit(this.currentTime); + } +} diff --git a/ui-ngx/src/app/shared/components/time/timeinterval.component.html b/ui-ngx/src/app/shared/components/time/timeinterval.component.html new file mode 100644 index 0000000000..4de184141d --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/timeinterval.component.html @@ -0,0 +1,58 @@ + +
    +
    + + +
    +
    + +
    + + timeinterval.days + + + + timeinterval.hours + + + + timeinterval.minutes + + + + timeinterval.seconds + + +
    +
    +
    + + {{ predefinedName }} + + + {{ interval.name | translate:interval.translateParams }} + + + +
    +
    + + +
    +
    diff --git a/ui-ngx/src/app/shared/components/time/timeinterval.component.scss b/ui-ngx/src/app/shared/components/time/timeinterval.component.scss new file mode 100644 index 0000000000..6453b024b9 --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/timeinterval.component.scss @@ -0,0 +1,54 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../scss/constants'; + +:host { + min-width: 355px; + + .advanced-switch { + margin-bottom: 16px; + } + + .advanced-label { + margin: 5px 0; + } + + .hide-label { + margin-bottom: 5px; + margin-right: 5px; + } + + .interval-section { + min-height: 66px; + .interval-label { + margin-bottom: 7px; + margin-top: -1px; + } + } + + @media #{$mat-xs} { + min-width: 0; + width: 100%; + } +} + +:host ::ng-deep { + .number-input { + .mat-form-field-infix { + width: 70px; + } + } +} diff --git a/ui-ngx/src/app/shared/components/time/timeinterval.component.ts b/ui-ngx/src/app/shared/components/time/timeinterval.component.ts new file mode 100644 index 0000000000..ff2eeb16bf --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/timeinterval.component.ts @@ -0,0 +1,302 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { TimeInterval, TimeService } from '@core/services/time.service'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; + +@Component({ + selector: 'tb-timeinterval', + templateUrl: './timeinterval.component.html', + styleUrls: ['./timeinterval.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimeintervalComponent), + multi: true + } + ] +}) +export class TimeintervalComponent implements OnInit, ControlValueAccessor { + + minValue: number; + maxValue: number; + + @Input() + set min(min: number) { + if (typeof min !== 'undefined' && min !== this.minValue) { + this.minValue = min; + this.maxValue = Math.max(this.maxValue, this.minValue); + this.updateView(); + } + } + + @Input() + set max(max: number) { + if (typeof max !== 'undefined' && max !== this.maxValue) { + this.maxValue = max; + this.minValue = Math.min(this.minValue, this.maxValue); + this.updateView(); + } + } + + @Input() predefinedName: string; + + isEditValue = false; + + @Input() + set isEdit(val) { + this.isEditValue = coerceBooleanProperty(val); + } + + get isEdit() { + return this.isEditValue; + } + + hideFlagValue = false; + + @Input() + get hideFlag() { + return this.hideFlagValue; + } + + set hideFlag(val) { + this.hideFlagValue = val; + } + + @Output() hideFlagChange = new EventEmitter(); + + @Input() disabled: boolean; + + days = 0; + hours = 0; + mins = 1; + secs = 0; + + intervalMs = 0; + modelValue: number; + + advanced = false; + rendered = false; + + intervals: Array; + + private propagateChange = (_: any) => {}; + + constructor(private timeService: TimeService) { + } + + ngOnInit(): void { + this.boundInterval(); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(intervalMs: number): void { + this.modelValue = intervalMs; + this.rendered = true; + if (typeof this.modelValue !== 'undefined') { + const min = this.timeService.boundMinInterval(this.minValue); + const max = this.timeService.boundMaxInterval(this.maxValue); + if (this.modelValue >= min && this.modelValue <= max) { + this.advanced = !this.timeService.matchesExistingInterval(this.minValue, this.maxValue, this.modelValue); + this.setIntervalMs(this.modelValue); + } else { + this.boundInterval(); + } + } + } + + setIntervalMs(intervalMs: number) { + if (!this.advanced) { + this.intervalMs = intervalMs; + } + const intervalSeconds = Math.floor(intervalMs / 1000); + this.days = Math.floor(intervalSeconds / 86400); + this.hours = Math.floor((intervalSeconds % 86400) / 3600); + this.mins = Math.floor(((intervalSeconds % 86400) % 3600) / 60); + this.secs = intervalSeconds % 60; + } + + boundInterval() { + const min = this.timeService.boundMinInterval(this.minValue); + const max = this.timeService.boundMaxInterval(this.maxValue); + this.intervals = this.timeService.getIntervals(this.minValue, this.maxValue); + if (this.rendered) { + let newIntervalMs = this.modelValue; + if (newIntervalMs < min) { + newIntervalMs = min; + } else if (newIntervalMs > max) { + newIntervalMs = max; + } + if (!this.advanced) { + newIntervalMs = this.timeService.boundToPredefinedInterval(min, max, newIntervalMs); + } + if (newIntervalMs !== this.modelValue) { + this.setIntervalMs(newIntervalMs); + this.updateView(); + } + } + } + + updateView() { + if (!this.rendered) { + return; + } + let value = null; + let intervalMs; + if (!this.advanced) { + intervalMs = this.intervalMs; + if (!intervalMs || isNaN(intervalMs)) { + intervalMs = this.calculateIntervalMs(); + } + } else { + intervalMs = this.calculateIntervalMs(); + } + if (!isNaN(intervalMs) && intervalMs > 0) { + value = intervalMs; + } + this.modelValue = value; + this.propagateChange(this.modelValue); + this.boundInterval(); + } + + calculateIntervalMs(): number { + return (this.days * 86400 + + this.hours * 3600 + + this.mins * 60 + + this.secs) * 1000; + } + + onIntervalMsChange() { + this.updateView(); + } + + onAdvancedChange() { + if (!this.advanced) { + this.intervalMs = this.calculateIntervalMs(); + } else { + let intervalMs = this.intervalMs; + if (!intervalMs || isNaN(intervalMs)) { + intervalMs = this.calculateIntervalMs(); + } + this.setIntervalMs(intervalMs); + } + this.updateView(); + } + + onHideFlagChange() { + this.hideFlagChange.emit(this.hideFlagValue); + } + + onTimeInputChange(type: string) { + switch (type) { + case 'secs': + setTimeout(() => this.onSecsChange(), 0); + break; + case 'mins': + setTimeout(() => this.onMinsChange(), 0); + break; + case 'hours': + setTimeout(() => this.onHoursChange(), 0); + break; + case 'days': + setTimeout(() => this.onDaysChange(), 0); + break; + } + } + + onSecsChange() { + if (typeof this.secs === 'undefined') { + return; + } + if (this.secs < 0) { + if ((this.days + this.hours + this.mins) > 0) { + this.secs = this.secs + 60; + this.mins--; + this.onMinsChange(); + } else { + this.secs = 0; + } + } else if (this.secs >= 60) { + this.secs = this.secs - 60; + this.mins++; + this.onMinsChange(); + } + this.updateView(); + } + + onMinsChange() { + if (typeof this.mins === 'undefined') { + return; + } + if (this.mins < 0) { + if ((this.days + this.hours) > 0) { + this.mins = this.mins + 60; + this.hours--; + this.onHoursChange(); + } else { + this.mins = 0; + } + } else if (this.mins >= 60) { + this.mins = this.mins - 60; + this.hours++; + this.onHoursChange(); + } + this.updateView(); + } + + onHoursChange() { + if (typeof this.hours === 'undefined') { + return; + } + if (this.hours < 0) { + if (this.days > 0) { + this.hours = this.hours + 24; + this.days--; + this.onDaysChange(); + } else { + this.hours = 0; + } + } else if (this.hours >= 24) { + this.hours = this.hours - 24; + this.days++; + this.onDaysChange(); + } + this.updateView(); + } + + onDaysChange() { + if (typeof this.days === 'undefined') { + return; + } + if (this.days < 0) { + this.days = 0; + } + this.updateView(); + } + +} diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html new file mode 100644 index 0000000000..398d5dd9d1 --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html @@ -0,0 +1,158 @@ + +
    +
    +
    + + +
    + +
    +
    + +
    +
    + + +
    +
    +
    + + +
    + timewindow.last + +
    +
    + +
    + timewindow.time-period + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    + + aggregation.function + + + {{ aggregationTypesTranslations.get(aggregationTypes[aggregation]) | translate }} + + + +
    +
    +
    +
    + + +
    +
    +
    + aggregation.limit + + + + + +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss new file mode 100644 index 0000000000..9bbcc75d75 --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss @@ -0,0 +1,72 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + width: 100%; + height: 100%; + form, + fieldset { + height: 100%; + } + + .mat-content { + overflow: hidden; + background-color: #fff; + } + + .mat-padding { + padding: 0 16px; + } + + .hide-label { + margin-bottom: 5px; + margin-right: 5px; + } + + tb-timeinterval[ng-reflect-fx-show="true"] { + margin-bottom: -16px; + } + + .limit-slider-container { + >:first-child { + margin-right: 16px; + } + >:last-child { + margin-left: 16px; + } + >:first-child, >:last-child { + min-width: 25px; + max-width: 42px; + } + mat-form-field input[type=number] { + text-align: center; + } + } + +} + +:host ::ng-deep { + mat-radio-button { + display: block; + margin-bottom: 16px; + .mat-radio-label { + width: 100%; + align-items: start; + .mat-radio-label-content { + width: 100%; + } + } + } +} diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts new file mode 100644 index 0000000000..cf5df4a4b5 --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts @@ -0,0 +1,235 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, InjectionToken, OnInit, ViewContainerRef } from '@angular/core'; +import { + aggregationTranslations, + AggregationType, + DAY, + HistoryWindowType, + Timewindow, + TimewindowType +} from '@shared/models/time/time.models'; +import { OverlayRef } from '@angular/cdk/overlay'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { TimeService } from '@core/services/time.service'; + +export const TIMEWINDOW_PANEL_DATA = new InjectionToken('TimewindowPanelData'); + +export interface TimewindowPanelData { + historyOnly: boolean; + timewindow: Timewindow; + aggregation: boolean; + isEdit: boolean; +} + +@Component({ + selector: 'tb-timewindow-panel', + templateUrl: './timewindow-panel.component.html', + styleUrls: ['./timewindow-panel.component.scss'] +}) +export class TimewindowPanelComponent extends PageComponent implements OnInit { + + historyOnly = false; + + aggregation = false; + + isEdit = false; + + timewindow: Timewindow; + + result: Timewindow; + + timewindowForm: FormGroup; + + historyTypes = HistoryWindowType; + + timewindowTypes = TimewindowType; + + aggregationTypes = AggregationType; + + aggregations = Object.keys(AggregationType); + + aggregationTypesTranslations = aggregationTranslations; + + constructor(@Inject(TIMEWINDOW_PANEL_DATA) public data: TimewindowPanelData, + public overlayRef: OverlayRef, + protected store: Store, + public fb: FormBuilder, + private timeService: TimeService, + public viewContainerRef: ViewContainerRef) { + super(store); + this.historyOnly = data.historyOnly; + this.timewindow = data.timewindow; + this.aggregation = data.aggregation; + this.isEdit = data.isEdit; + } + + ngOnInit(): void { + const hideInterval = this.timewindow.hideInterval || false; + const hideAggregation = this.timewindow.hideAggregation || false; + const hideAggInterval = this.timewindow.hideAggInterval || false; + + this.timewindowForm = this.fb.group({ + realtime: this.fb.group( + { + timewindowMs: [ + this.timewindow.realtime && typeof this.timewindow.realtime.timewindowMs !== 'undefined' + ? this.timewindow.realtime.timewindowMs : null + ], + interval: [ + this.timewindow.realtime && typeof this.timewindow.realtime.interval !== 'undefined' + ? this.timewindow.realtime.interval : null + ] + } + ), + history: this.fb.group( + { + historyType: this.fb.control({ + value: this.timewindow.history && typeof this.timewindow.history.historyType !== 'undefined' + ? this.timewindow.history.historyType : HistoryWindowType.LAST_INTERVAL, + disabled: hideInterval + }), + timewindowMs: this.fb.control({ + value: this.timewindow.history && typeof this.timewindow.history.timewindowMs !== 'undefined' + ? this.timewindow.history.timewindowMs : null, + disabled: hideInterval + }), + interval: [ + this.timewindow.history && typeof this.timewindow.history.interval !== 'undefined' + ? this.timewindow.history.interval : null + ], + fixedTimewindow: this.fb.control({ + value: this.timewindow.history && typeof this.timewindow.history.fixedTimewindow !== 'undefined' + ? this.timewindow.history.fixedTimewindow : null, + disabled: hideInterval + }) + } + ), + aggregation: this.fb.group( + { + type: this.fb.control({ + value: this.timewindow.aggregation && typeof this.timewindow.aggregation.type !== 'undefined' + ? this.timewindow.aggregation.type : null, + disabled: hideAggregation + }), + limit: this.fb.control({ + value: this.timewindow.aggregation && typeof this.timewindow.aggregation.limit !== 'undefined' + ? this.timewindow.aggregation.limit : null, + disabled: hideAggInterval + }, [Validators.min(this.minDatapointsLimit()), Validators.max(this.maxDatapointsLimit())]) + } + ) + }); + } + + update() { + const timewindowFormValue = this.timewindowForm.getRawValue(); + this.timewindow.realtime = { + timewindowMs: timewindowFormValue.realtime.timewindowMs, + interval: timewindowFormValue.realtime.interval + }; + this.timewindow.history = { + historyType: timewindowFormValue.history.historyType, + timewindowMs: timewindowFormValue.history.timewindowMs, + interval: timewindowFormValue.history.interval, + fixedTimewindow: timewindowFormValue.history.fixedTimewindow + }; + if (this.aggregation) { + this.timewindow.aggregation = { + type: timewindowFormValue.aggregation.type, + limit: timewindowFormValue.aggregation.limit + }; + } + this.result = this.timewindow; + this.overlayRef.dispose(); + } + + cancel() { + this.overlayRef.dispose(); + } + + minDatapointsLimit() { + return this.timeService.getMinDatapointsLimit(); + } + + maxDatapointsLimit() { + return this.timeService.getMaxDatapointsLimit(); + } + + minRealtimeAggInterval() { + return this.timeService.minIntervalLimit(this.timewindowForm.get('realtime.timewindowMs').value); + } + + maxRealtimeAggInterval() { + return this.timeService.maxIntervalLimit(this.timewindowForm.get('realtime.timewindowMs').value); + } + + minHistoryAggInterval() { + return this.timeService.minIntervalLimit(this.currentHistoryTimewindow()); + } + + maxHistoryAggInterval() { + return this.timeService.maxIntervalLimit(this.currentHistoryTimewindow()); + } + + currentHistoryTimewindow() { + const timewindowFormValue = this.timewindowForm.getRawValue(); + if (timewindowFormValue.history.historyType === HistoryWindowType.LAST_INTERVAL) { + return timewindowFormValue.history.timewindowMs; + } else if (timewindowFormValue.history.fixedTimewindow) { + return timewindowFormValue.history.fixedTimewindow.endTimeMs - + timewindowFormValue.history.fixedTimewindow.startTimeMs; + } else { + return DAY; + } + } + + onHideIntervalChanged() { + if (this.timewindow.hideInterval) { + this.timewindowForm.get('history.historyType').disable({emitEvent: false}); + this.timewindowForm.get('history.timewindowMs').disable({emitEvent: false}); + this.timewindowForm.get('history.fixedTimewindow').disable({emitEvent: false}); + } else { + this.timewindowForm.get('history.historyType').enable({emitEvent: false}); + this.timewindowForm.get('history.timewindowMs').enable({emitEvent: false}); + this.timewindowForm.get('history.fixedTimewindow').enable({emitEvent: false}); + } + this.timewindowForm.markAsDirty(); + } + + onHideAggregationChanged() { + if (this.timewindow.hideAggregation) { + this.timewindowForm.get('aggregation.type').disable({emitEvent: false}); + } else { + this.timewindowForm.get('aggregation.type').enable({emitEvent: false}); + } + this.timewindowForm.markAsDirty(); + } + + onHideAggIntervalChanged() { + if (this.timewindow.hideAggInterval) { + this.timewindowForm.get('aggregation.limit').disable({emitEvent: false}); + } else { + this.timewindowForm.get('aggregation.limit').enable({emitEvent: false}); + } + this.timewindowForm.markAsDirty(); + } + +} diff --git a/ui-ngx/src/app/shared/components/time/timewindow.component.html b/ui-ngx/src/app/shared/components/time/timewindow.component.html new file mode 100644 index 0000000000..cce90ea208 --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/timewindow.component.html @@ -0,0 +1,46 @@ + + +
    + + + {{innerValue?.displayValue}} + + +
    diff --git a/ui-ngx/src/app/shared/components/time/timewindow.component.scss b/ui-ngx/src/app/shared/components/time/timewindow.component.scss new file mode 100644 index 0000000000..a030c7e5ee --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/timewindow.component.scss @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + min-width: 52px; + margin: 8px 0; + section.tb-timewindow { + min-height: 32px; + padding: 0 6px; + + span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + pointer-events: all; + cursor: pointer; + } + } +} diff --git a/ui-ngx/src/app/shared/components/time/timewindow.component.ts b/ui-ngx/src/app/shared/components/time/timewindow.component.ts new file mode 100644 index 0000000000..649d48343e --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/timewindow.component.ts @@ -0,0 +1,290 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Inject, Input, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { TranslateService } from '@ngx-translate/core'; +import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; +import { + cloneSelectedTimewindow, + HistoryWindowType, + initModelFromDefaultTimewindow, + Timewindow, + TimewindowType +} from '@shared/models/time/time.models'; +import { DatePipe, DOCUMENT } from '@angular/common'; +import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { + TIMEWINDOW_PANEL_DATA, + TimewindowPanelComponent, + TimewindowPanelData +} from '@shared/components/time/timewindow-panel.component'; +import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; +import { MediaBreakpoints } from '@shared/models/constants'; +import { BreakpointObserver } from '@angular/cdk/layout'; +import { WINDOW } from '@core/services/window.service'; +import { TimeService } from '@core/services/time.service'; +import { TooltipPosition } from '@angular/material/tooltip'; +import { deepClone } from '@core/utils'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; + +// @dynamic +@Component({ + selector: 'tb-timewindow', + templateUrl: './timewindow.component.html', + styleUrls: ['./timewindow.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimewindowComponent), + multi: true + } + ] +}) +export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAccessor { + + historyOnlyValue = false; + + @Input() + set historyOnly(val) { + this.historyOnlyValue = coerceBooleanProperty(val); + } + + get historyOnly() { + return this.historyOnlyValue; + } + + aggregationValue = false; + + @Input() + set aggregation(val) { + this.aggregationValue = coerceBooleanProperty(val); + } + + get aggregation() { + return this.aggregationValue; + } + + isToolbarValue = false; + + @Input() + set isToolbar(val) { + this.isToolbarValue = coerceBooleanProperty(val); + } + + get isToolbar() { + return this.isToolbarValue; + } + + asButtonValue = false; + + @Input() + set asButton(val) { + this.asButtonValue = coerceBooleanProperty(val); + } + + get asButton() { + return this.asButtonValue; + } + + isEditValue = false; + + @Input() + set isEdit(val) { + this.isEditValue = coerceBooleanProperty(val); + this.timewindowDisabled = this.isTimewindowDisabled(); + } + + get isEdit() { + return this.isEditValue; + } + + @Input() + direction: 'left' | 'right' = 'left'; + + @Input() + tooltipPosition: TooltipPosition = 'above'; + + @Input() disabled: boolean; + + @ViewChild('timewindowPanelOrigin') timewindowPanelOrigin: CdkOverlayOrigin; + + innerValue: Timewindow; + + timewindowDisabled: boolean; + + private propagateChange = (_: any) => {}; + + constructor(private translate: TranslateService, + private timeService: TimeService, + private millisecondsToTimeStringPipe: MillisecondsToTimeStringPipe, + private datePipe: DatePipe, + private overlay: Overlay, + public viewContainerRef: ViewContainerRef, + public breakpointObserver: BreakpointObserver, + @Inject(DOCUMENT) private document: Document, + @Inject(WINDOW) private window: Window) { + } + + ngOnInit(): void { + } + + ngOnDestroy(): void { + } + + openEditMode() { + if (this.timewindowDisabled) { + return; + } + const isGtXs = this.breakpointObserver.isMatched(MediaBreakpoints['gt-xs']); + const position = this.overlay.position(); + const config = new OverlayConfig({ + panelClass: 'tb-timewindow-panel', + backdropClass: 'cdk-overlay-transparent-backdrop', + hasBackdrop: isGtXs, + }); + if (isGtXs) { + config.minWidth = '417px'; + config.maxHeight = '440px'; + const panelHeight = 375; + const panelWidth = 417; + const el = this.timewindowPanelOrigin.elementRef.nativeElement; + const offset = el.getBoundingClientRect(); + const scrollTop = this.window.pageYOffset || this.document.documentElement.scrollTop || this.document.body.scrollTop || 0; + const scrollLeft = this.window.pageXOffset || this.document.documentElement.scrollLeft || this.document.body.scrollLeft || 0; + const bottomY = offset.bottom - scrollTop; + const leftX = offset.left - scrollLeft; + let originX; + let originY; + let overlayX; + let overlayY; + const wHeight = this.document.documentElement.clientHeight; + const wWidth = this.document.documentElement.clientWidth; + if (bottomY + panelHeight > wHeight) { + originY = 'top'; + overlayY = 'bottom'; + } else { + originY = 'bottom'; + overlayY = 'top'; + } + if (leftX + panelWidth > wWidth) { + originX = 'end'; + overlayX = 'end'; + } else { + originX = 'start'; + overlayX = 'start'; + } + const connectedPosition: ConnectedPosition = { + originX, + originY, + overlayX, + overlayY + }; + config.positionStrategy = position.flexibleConnectedTo(this.timewindowPanelOrigin.elementRef) + .withPositions([connectedPosition]); + } else { + config.minWidth = '100%'; + config.minHeight = '100%'; + config.positionStrategy = position.global().top('0%').left('0%') + .right('0%').bottom('0%'); + } + + const overlayRef = this.overlay.create(config); + + overlayRef.backdropClick().subscribe(() => { + overlayRef.dispose(); + }); + + const injector = this._createTimewindowPanelInjector( + overlayRef, + { + timewindow: deepClone(this.innerValue), + historyOnly: this.historyOnly, + aggregation: this.aggregation, + isEdit: this.isEdit + } + ); + + const componentRef = overlayRef.attach(new ComponentPortal(TimewindowPanelComponent, this.viewContainerRef, injector)); + componentRef.onDestroy(() => { + if (componentRef.instance.result) { + this.innerValue = componentRef.instance.result; + this.timewindowDisabled = this.isTimewindowDisabled(); + this.updateDisplayValue(); + this.notifyChanged(); + } + }); + } + + private _createTimewindowPanelInjector(overlayRef: OverlayRef, data: TimewindowPanelData): PortalInjector { + const injectionTokens = new WeakMap([ + [TIMEWINDOW_PANEL_DATA, data], + [OverlayRef, overlayRef] + ]); + return new PortalInjector(this.viewContainerRef.injector, injectionTokens); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + this.timewindowDisabled = this.isTimewindowDisabled(); + } + + writeValue(obj: Timewindow): void { + this.innerValue = initModelFromDefaultTimewindow(obj, this.timeService); + this.timewindowDisabled = this.isTimewindowDisabled(); + this.updateDisplayValue(); + } + + notifyChanged() { + this.propagateChange(cloneSelectedTimewindow(this.innerValue)); + } + + updateDisplayValue() { + if (this.innerValue.selectedTab === TimewindowType.REALTIME && !this.historyOnly) { + this.innerValue.displayValue = this.translate.instant('timewindow.realtime') + ' - ' + + this.translate.instant('timewindow.last-prefix') + ' ' + + this.millisecondsToTimeStringPipe.transform(this.innerValue.realtime.timewindowMs); + } else { + this.innerValue.displayValue = !this.historyOnly ? (this.translate.instant('timewindow.history') + ' - ') : ''; + if (this.innerValue.history.historyType === HistoryWindowType.LAST_INTERVAL) { + this.innerValue.displayValue += this.translate.instant('timewindow.last-prefix') + ' ' + + this.millisecondsToTimeStringPipe.transform(this.innerValue.history.timewindowMs); + } else { + const startString = this.datePipe.transform(this.innerValue.history.fixedTimewindow.startTimeMs, 'yyyy-MM-dd HH:mm:ss'); + const endString = this.datePipe.transform(this.innerValue.history.fixedTimewindow.endTimeMs, 'yyyy-MM-dd HH:mm:ss'); + this.innerValue.displayValue += this.translate.instant('timewindow.period', {startTime: startString, endTime: endString}); + } + } + } + + hideLabel() { + return this.isToolbar && !this.breakpointObserver.isMatched(MediaBreakpoints['gt-md']); + } + + private isTimewindowDisabled(): boolean { + return this.disabled || + (!this.isEdit && (!this.innerValue || this.innerValue.hideInterval && + (!this.aggregation || this.innerValue.hideAggregation && this.innerValue.hideAggInterval))); + } + +} diff --git a/ui-ngx/src/app/shared/components/time/timezone-select.component.html b/ui-ngx/src/app/shared/components/time/timezone-select.component.html new file mode 100644 index 0000000000..b5e063ac72 --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/timezone-select.component.html @@ -0,0 +1,50 @@ + + + timezone.timezone + + + + + + + + + {{ translate.get('timezone.no-timezones-matching', {timezone: searchText}) | async }} + + + + + {{ 'timezone.timezone-required' | translate }} + + diff --git a/ui-ngx/src/app/shared/components/time/timezone-select.component.ts b/ui-ngx/src/app/shared/components/time/timezone-select.component.ts new file mode 100644 index 0000000000..7dc89df7da --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/timezone-select.component.ts @@ -0,0 +1,221 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, Component, forwardRef, Input, NgZone, OnInit, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Observable, of } from 'rxjs'; +import { map, mergeMap, share, tap } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import * as _moment from 'moment-timezone'; +import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; + +interface TimezoneInfo { + id: string; + name: string; + offset: string; + nOffset: number; +} + +@Component({ + selector: 'tb-timezone-select', + templateUrl: './timezone-select.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimezoneSelectComponent), + multi: true + }] +}) +export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit { + + selectTimezoneFormGroup: FormGroup; + + modelValue: string | null; + + defaultTimezoneId: string = null; + + defaultTimezoneInfo: TimezoneInfo = null; + + timezones: TimezoneInfo[] = _moment.tz.names().map((zoneName) => { + const tz = _moment.tz(zoneName); + return { + id: zoneName, + name: zoneName.replace(/_/g, ' '), + offset: `UTC${tz.format('Z')}`, + nOffset: tz.utcOffset() + } + }); + + @Input() + set defaultTimezone(timezone: string) { + if (this.defaultTimezoneId !== timezone) { + this.defaultTimezoneId = timezone; + if (this.defaultTimezoneId) { + this.defaultTimezoneInfo = + this.timezones.find((timezoneInfo) => timezoneInfo.id === this.defaultTimezoneId); + } else { + this.defaultTimezoneInfo = null; + } + } + } + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @ViewChild('timezoneInput', {static: true, read: MatAutocompleteTrigger}) timezoneInputTrigger: MatAutocompleteTrigger; + + filteredTimezones: Observable>; + + searchText = ''; + + ignoreClosePanel = false; + + private dirty = false; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + public translate: TranslateService, + private ngZone: NgZone, + private fb: FormBuilder) { + this.selectTimezoneFormGroup = this.fb.group({ + timezone: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.filteredTimezones = this.selectTimezoneFormGroup.get('timezone').valueChanges + .pipe( + tap(value => { + let modelValue; + if (typeof value === 'string' || !value) { + modelValue = null; + } else { + modelValue = value.id; + } + this.updateView(modelValue); + if (value === null) { + this.clear(); + } + }), + map(value => value ? (typeof value === 'string' ? value : value.name) : ''), + mergeMap(name => this.fetchTimezones(name) ), + share() + ); + } + + ngAfterViewInit(): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.selectTimezoneFormGroup.disable({emitEvent: false}); + } else { + this.selectTimezoneFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: string | null): void { + this.searchText = ''; + let foundTimezone: TimezoneInfo = null; + if (value !== null) { + foundTimezone = this.timezones.find(timezoneInfo => timezoneInfo.id === value); + } + if (foundTimezone !== null) { + this.modelValue = value; + this.selectTimezoneFormGroup.get('timezone').patchValue(foundTimezone, {emitEvent: false}); + } else { + if (this.defaultTimezoneInfo) { + this.selectTimezoneFormGroup.get('timezone').patchValue(this.defaultTimezoneInfo, {emitEvent: false}); + setTimeout(() => { + this.updateView(this.defaultTimezoneInfo.id); + }, 0); + } else { + this.modelValue = null; + this.selectTimezoneFormGroup.get('timezone').patchValue('', {emitEvent: false}); + } + } + this.dirty = true; + } + + onFocus() { + if (this.dirty) { + this.selectTimezoneFormGroup.get('timezone').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + onPanelClosed() { + if (this.ignoreClosePanel) { + this.ignoreClosePanel = false; + } else { + if (!this.modelValue && this.defaultTimezoneInfo) { + this.ngZone.run(() => { + this.selectTimezoneFormGroup.get('timezone').reset(this.defaultTimezoneInfo, {emitEvent: true}); + }); + } + } + } + + updateView(value: string | null) { + if (this.modelValue !== value) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + displayTimezoneFn(timezone?: TimezoneInfo): string | undefined { + return timezone ? `${timezone.name} (${timezone.offset})` : undefined; + } + + fetchTimezones(searchText?: string): Observable> { + this.searchText = searchText; + let result = this.timezones; + if (searchText && searchText.length) { + result = this.timezones.filter((timezoneInfo) => + timezoneInfo.name.toLowerCase().includes(searchText.toLowerCase())); + } + return of(result); + } + + clear() { + this.selectTimezoneFormGroup.get('timezone').patchValue('', {emitEvent: true}); + setTimeout(() => { + this.timezoneInputTrigger.openPanel(); + }, 0); + } + +} diff --git a/ui-ngx/src/app/shared/components/toast.directive.ts b/ui-ngx/src/app/shared/components/toast.directive.ts new file mode 100644 index 0000000000..fb704c74db --- /dev/null +++ b/ui-ngx/src/app/shared/components/toast.directive.ts @@ -0,0 +1,374 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, ChangeDetectorRef, + Component, ComponentFactoryResolver, ComponentRef, + Directive, + ElementRef, HostBinding, + Inject, + Input, + NgZone, + OnDestroy, Optional, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import { MAT_SNACK_BAR_DATA, MatSnackBar, MatSnackBarConfig, MatSnackBarRef } from '@angular/material/snack-bar'; +import { NotificationMessage } from '@app/core/notification/notification.models'; +import { Subscription } from 'rxjs'; +import { NotificationService } from '@app/core/services/notification.service'; +import { BreakpointObserver } from '@angular/cdk/layout'; +import { MediaBreakpoints } from '@shared/models/constants'; +import { MatButton } from '@angular/material/button'; +import Timeout = NodeJS.Timeout; +import { PortalInjector } from '@angular/cdk/portal'; + +@Directive({ + selector: '[tb-toast]' +}) +export class ToastDirective implements AfterViewInit, OnDestroy { + + @Input() + toastTarget = 'root'; + + private notificationSubscription: Subscription = null; + private hideNotificationSubscription: Subscription = null; + + private snackBarRef: MatSnackBarRef = null; + private toastComponentRef: ComponentRef; + private currentMessage: NotificationMessage = null; + + private dismissTimeout: Timeout = null; + + constructor(private elementRef: ElementRef, + private viewContainerRef: ViewContainerRef, + private notificationService: NotificationService, + private componentFactoryResolver: ComponentFactoryResolver, + private snackBar: MatSnackBar, + private ngZone: NgZone, + private breakpointObserver: BreakpointObserver, + private cd: ChangeDetectorRef) { + } + + ngAfterViewInit(): void { + this.notificationSubscription = this.notificationService.getNotification().subscribe( + (notificationMessage) => { + if (this.shouldDisplayMessage(notificationMessage)) { + this.currentMessage = notificationMessage; + const isGtSm = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']); + if (isGtSm && this.toastTarget !== 'root') { + this.showToastPanel(notificationMessage); + } else { + this.showSnackBar(notificationMessage, isGtSm); + } + } + } + ); + + this.hideNotificationSubscription = this.notificationService.getHideNotification().subscribe( + (hideNotification) => { + if (hideNotification) { + const target = hideNotification.target || 'root'; + if (this.toastTarget === target) { + this.ngZone.run(() => { + if (this.snackBarRef) { + this.snackBarRef.dismiss(); + } + if (this.toastComponentRef) { + this.toastComponentRef.instance.actionButton._elementRef.nativeElement.click(); + } + }); + } + } + } + ); + } + + private showToastPanel(notificationMessage: NotificationMessage) { + this.ngZone.run(() => { + if (this.snackBarRef) { + this.snackBarRef.dismiss(); + } + if (this.toastComponentRef) { + this.viewContainerRef.detach(0); + this.toastComponentRef.destroy(); + } + let panelClass = ['tb-toast-panel', 'toast-panel']; + if (notificationMessage.panelClass) { + if (typeof notificationMessage.panelClass === 'string') { + panelClass.push(notificationMessage.panelClass); + } else if (notificationMessage.panelClass.length) { + panelClass = panelClass.concat(notificationMessage.panelClass); + } + } + const horizontalPosition = notificationMessage.horizontalPosition || 'left'; + const verticalPosition = notificationMessage.verticalPosition || 'top'; + if (horizontalPosition === 'start' || horizontalPosition === 'left') { + panelClass.push('left'); + } else if (horizontalPosition === 'end' || horizontalPosition === 'right') { + panelClass.push('right'); + } else { + panelClass.push('h-center'); + } + if (verticalPosition === 'top') { + panelClass.push('top'); + } else { + panelClass.push('bottom'); + } + + const componentFactory = this.componentFactoryResolver.resolveComponentFactory(TbSnackBarComponent); + const data: ToastPanelData = { + notification: notificationMessage, + panelClass, + destroyToastComponent: () => { + this.viewContainerRef.detach(0); + this.toastComponentRef.destroy(); + } + }; + const injectionTokens = new WeakMap([ + [MAT_SNACK_BAR_DATA, data] + ]); + const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens); + this.toastComponentRef = this.viewContainerRef.createComponent(componentFactory, 0, injector); + this.cd.detectChanges(); + + if (notificationMessage.duration && notificationMessage.duration > 0) { + if (this.dismissTimeout !== null) { + clearTimeout(this.dismissTimeout); + this.dismissTimeout = null; + } + this.dismissTimeout = setTimeout(() => { + if (this.toastComponentRef) { + this.toastComponentRef.instance.actionButton._elementRef.nativeElement.click(); + } + this.dismissTimeout = null; + }, notificationMessage.duration + 500); + } + this.toastComponentRef.onDestroy(() => { + if (this.dismissTimeout !== null) { + clearTimeout(this.dismissTimeout); + this.dismissTimeout = null; + } + this.toastComponentRef = null; + this.currentMessage = null; + }); + }); + } + + private showSnackBar(notificationMessage: NotificationMessage, isGtSm: boolean) { + this.ngZone.run(() => { + if (this.snackBarRef) { + this.snackBarRef.dismiss(); + } + const data: ToastPanelData = { + notification: notificationMessage, + parent: this.elementRef, + panelClass: [], + destroyToastComponent: () => {} + }; + const config: MatSnackBarConfig = { + horizontalPosition: notificationMessage.horizontalPosition || 'left', + verticalPosition: !isGtSm ? 'bottom' : (notificationMessage.verticalPosition || 'top'), + viewContainerRef: this.viewContainerRef, + duration: notificationMessage.duration, + panelClass: notificationMessage.panelClass, + data + }; + this.snackBarRef = this.snackBar.openFromComponent(TbSnackBarComponent, config); + if (notificationMessage.duration && notificationMessage.duration > 0 && notificationMessage.forceDismiss) { + if (this.dismissTimeout !== null) { + clearTimeout(this.dismissTimeout); + this.dismissTimeout = null; + } + this.dismissTimeout = setTimeout(() => { + if (this.snackBarRef) { + this.snackBarRef.instance.actionButton._elementRef.nativeElement.click(); + } + this.dismissTimeout = null; + }, notificationMessage.duration); + } + this.snackBarRef.afterDismissed().subscribe(() => { + if (this.dismissTimeout !== null) { + clearTimeout(this.dismissTimeout); + this.dismissTimeout = null; + } + this.snackBarRef = null; + this.currentMessage = null; + }); + }); + } + + private shouldDisplayMessage(notificationMessage: NotificationMessage): boolean { + if (notificationMessage && notificationMessage.message) { + const target = notificationMessage.target || 'root'; + if (this.toastTarget === target) { + if (!this.currentMessage || this.currentMessage.message !== notificationMessage.message + || this.currentMessage.type !== notificationMessage.type) { + return true; + } + } + } + return false; + } + + ngOnDestroy(): void { + if (this.toastComponentRef) { + this.viewContainerRef.detach(0); + this.toastComponentRef.destroy(); + } + if (this.notificationSubscription) { + this.notificationSubscription.unsubscribe(); + } + if (this.hideNotificationSubscription) { + this.hideNotificationSubscription.unsubscribe(); + } + } +} + +interface ToastPanelData { + notification: NotificationMessage; + parent?: ElementRef; + panelClass: string[]; + destroyToastComponent: () => void; +} + +import { + AnimationTriggerMetadata, + AnimationEvent, + trigger, + state, + transition, + style, + animate, +} from '@angular/animations'; +import { onParentScrollOrWindowResize } from '@core/utils'; + +export const toastAnimations: { + readonly showHideToast: AnimationTriggerMetadata; +} = { + showHideToast: trigger('showHideAnimation', [ + state('in', style({ transform: 'scale(1)', opacity: 1 })), + transition('void => opened', [style({ transform: 'scale(0)', opacity: 0 }), animate('{{ open }}ms')]), + transition( + 'opened => closing', + animate('{{ close }}ms', style({ transform: 'scale(0)', opacity: 0 })), + ), + ]), +}; + +export type ToastAnimationState = 'default' | 'opened' | 'closing'; + +@Component({ + selector: 'tb-snack-bar-component', + templateUrl: 'snack-bar-component.html', + styleUrls: ['snack-bar-component.scss'], + animations: [toastAnimations.showHideToast] +}) +export class TbSnackBarComponent implements AfterViewInit, OnDestroy { + + @ViewChild('actionButton', {static: true}) actionButton: MatButton; + + @HostBinding('class') + get panelClass(): string[] { + return this.data.panelClass; + } + + private parentEl: HTMLElement; + private snackBarContainerEl: HTMLElement; + private parentScrollSubscription: Subscription = null; + + public notification: NotificationMessage; + + animationState: ToastAnimationState; + + animationParams = { + open: 100, + close: 100 + }; + + constructor(@Inject(MAT_SNACK_BAR_DATA) + private data: ToastPanelData, + private elementRef: ElementRef, + @Optional() + private snackBarRef: MatSnackBarRef) { + this.animationState = !!this.snackBarRef ? 'default' : 'opened'; + this.notification = data.notification; + } + + ngAfterViewInit() { + if (this.snackBarRef) { + this.parentEl = this.data.parent.nativeElement; + this.snackBarContainerEl = this.elementRef.nativeElement.parentNode; + this.snackBarContainerEl.style.position = 'absolute'; + this.updateContainerRect(); + this.updatePosition(this.snackBarRef.containerInstance.snackBarConfig); + this.parentScrollSubscription = onParentScrollOrWindowResize(this.parentEl).subscribe(() => { + this.updateContainerRect(); + }); + } + } + + private updatePosition(config: MatSnackBarConfig) { + const isRtl = config.direction === 'rtl'; + const isLeft = (config.horizontalPosition === 'left' || + (config.horizontalPosition === 'start' && !isRtl) || + (config.horizontalPosition === 'end' && isRtl)); + const isRight = !isLeft && config.horizontalPosition !== 'center'; + if (isLeft) { + this.snackBarContainerEl.style.justifyContent = 'flex-start'; + } else if (isRight) { + this.snackBarContainerEl.style.justifyContent = 'flex-end'; + } else { + this.snackBarContainerEl.style.justifyContent = 'center'; + } + if (config.verticalPosition === 'top') { + this.snackBarContainerEl.style.alignItems = 'flex-start'; + } else { + this.snackBarContainerEl.style.alignItems = 'flex-end'; + } + } + + private updateContainerRect() { + const viewportOffset = this.parentEl.getBoundingClientRect(); + this.snackBarContainerEl.style.top = viewportOffset.top + 'px'; + this.snackBarContainerEl.style.left = viewportOffset.left + 'px'; + this.snackBarContainerEl.style.width = viewportOffset.width + 'px'; + this.snackBarContainerEl.style.height = viewportOffset.height + 'px'; + } + + ngOnDestroy() { + if (this.parentScrollSubscription) { + this.parentScrollSubscription.unsubscribe(); + } + } + + action(): void { + if (this.snackBarRef) { + this.snackBarRef.dismissWithAction(); + } else { + this.animationState = 'closing'; + } + } + + onHideFinished(event: AnimationEvent) { + const { toState } = event; + const isFadeOut = (toState as ToastAnimationState) === 'closing'; + const itFinished = this.animationState === 'closing'; + if (isFadeOut && itFinished) { + this.data.destroyToastComponent(); + } + } +} diff --git a/ui-ngx/src/app/shared/components/user-menu.component.html b/ui-ngx/src/app/shared/components/user-menu.component.html new file mode 100644 index 0000000000..1a9a5d5e11 --- /dev/null +++ b/ui-ngx/src/app/shared/components/user-menu.component.html @@ -0,0 +1,41 @@ + +
    + + + +
    + + +
    +
    +
    diff --git a/ui-ngx/src/app/shared/components/user-menu.component.scss b/ui-ngx/src/app/shared/components/user-menu.component.scss new file mode 100644 index 0000000000..b600bd9fab --- /dev/null +++ b/ui-ngx/src/app/shared/components/user-menu.component.scss @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + div.tb-user-info { + line-height: 1.5; + + span { + text-transform: none; + } + + span.tb-user-display-name { + font-size: .8rem; + font-weight: 300; + letter-spacing: .008em; + } + + span.tb-user-authority { + font-size: .8rem; + font-weight: 300; + letter-spacing: .005em; + opacity: .8; + } + + } + + mat-icon.tb-mini-avatar { + width: 36px; + height: 36px; + margin: auto 8px; + font-size: 36px; + cursor: default; + } +} + +.tb-user-menu-items { + min-width: 256px; +} diff --git a/ui-ngx/src/app/shared/components/user-menu.component.ts b/ui-ngx/src/app/shared/components/user-menu.component.ts new file mode 100644 index 0000000000..ca8ebd48ce --- /dev/null +++ b/ui-ngx/src/app/shared/components/user-menu.component.ts @@ -0,0 +1,112 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { User } from '@shared/models/user.model'; +import { Authority } from '@shared/models/authority.enum'; +import { select, Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { selectAuthUser, selectUserDetails } from '@core/auth/auth.selectors'; +import { map } from 'rxjs/operators'; +import { AuthService } from '@core/auth/auth.service'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'tb-user-menu', + templateUrl: './user-menu.component.html', + styleUrls: ['./user-menu.component.scss'] +}) +export class UserMenuComponent implements OnInit, OnDestroy { + + @Input() displayUserInfo: boolean; + + authorities = Authority; + + authority$ = this.store.pipe( + select(selectAuthUser), + map((authUser) => authUser ? authUser.authority : Authority.ANONYMOUS) + ); + + authorityName$ = this.store.pipe( + select(selectUserDetails), + map((user) => this.getAuthorityName(user)) + ); + + userDisplayName$ = this.store.pipe( + select(selectUserDetails), + map((user) => this.getUserDisplayName(user)) + ); + + constructor(private store: Store, + private router: Router, + private authService: AuthService) { + } + + ngOnInit(): void { + } + + ngOnDestroy(): void { + } + + getAuthorityName(user: User): string { + let name = null; + if (user) { + const authority = user.authority; + switch (authority) { + case Authority.SYS_ADMIN: + name = 'user.sys-admin'; + break; + case Authority.TENANT_ADMIN: + name = 'user.tenant-admin'; + break; + case Authority.CUSTOMER_USER: + name = 'user.customer'; + break; + } + } + return name; + } + + getUserDisplayName(user: User): string { + let name = ''; + if (user) { + if ((user.firstName && user.firstName.length > 0) || + (user.lastName && user.lastName.length > 0)) { + if (user.firstName) { + name += user.firstName; + } + if (user.lastName) { + if (name.length > 0) { + name += ' '; + } + name += user.lastName; + } + } else { + name = user.email; + } + } + return name; + } + + openProfile(): void { + this.router.navigate(['profile']); + } + + logout(): void { + this.authService.logout(); + } + +} diff --git a/ui-ngx/src/app/shared/components/value-input.component.html b/ui-ngx/src/app/shared/components/value-input.component.html new file mode 100644 index 0000000000..b189d8f4b6 --- /dev/null +++ b/ui-ngx/src/app/shared/components/value-input.component.html @@ -0,0 +1,79 @@ + +
    +
    + + value.type + + + + {{ valueTypes.get(valueType).name | translate }} + + + + {{ valueTypes.get(valueTypeEnum[valueType]).name | translate }} + + + + + value.string-value + + + {{ (requiredText ? requiredText : 'value.string-value-required') | translate }} + + + + value.integer-value + + + {{ (requiredText ? requiredText : 'value.integer-value-required') | translate }} + + + {{ 'value.invalid-integer-value' | translate }} + + + + value.double-value + + + {{ (requiredText ? requiredText : 'value.double-value-required') | translate }} + + +
    + + {{ (modelValue ? 'value.true' : 'value.false') | translate }} + +
    +
    + + value.json-value + + + + {{ (requiredText ? requiredText : 'value.json-value-required') | translate }} + + + {{ 'value.json-value-invalid' | translate }} + + +
    +
    +
    diff --git a/ui-ngx/src/app/shared/components/value-input.component.scss b/ui-ngx/src/app/shared/components/value-input.component.scss new file mode 100644 index 0000000000..df5a1e13c5 --- /dev/null +++ b/ui-ngx/src/app/shared/components/value-input.component.scss @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host ::ng-deep { + mat-form-field.tb-value-type { + .mat-form-field-infix { + padding-bottom: 1px; + } + mat-select-trigger { + .mat-icon { + vertical-align: middle; + margin-right: 16px; + } + } + } +} diff --git a/ui-ngx/src/app/shared/components/value-input.component.ts b/ui-ngx/src/app/shared/components/value-input.component.ts new file mode 100644 index 0000000000..1cddd25292 --- /dev/null +++ b/ui-ngx/src/app/shared/components/value-input.component.ts @@ -0,0 +1,139 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, NgForm } from '@angular/forms'; +import { ValueType, valueTypesMap } from '@shared/models/constants'; +import { isObject } from '@core/utils'; +import { MatDialog } from '@angular/material/dialog'; +import { + JsonObjectEditDialogComponent, + JsonObjectEditDialogData +} from '@shared/components/dialog/json-object-edit-dialog.component'; + +@Component({ + selector: 'tb-value-input', + templateUrl: './value-input.component.html', + styleUrls: ['./value-input.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ValueInputComponent), + multi: true + } + ] +}) +export class ValueInputComponent implements OnInit, ControlValueAccessor { + + @Input() disabled: boolean; + + @Input() requiredText: string; + + @ViewChild('inputForm', {static: true}) inputForm: NgForm; + + modelValue: any; + + valueType: ValueType; + + public valueTypeEnum = ValueType; + + valueTypeKeys = Object.keys(ValueType); + + valueTypes = valueTypesMap; + + private propagateChange = null; + + constructor( + public dialog: MatDialog, + ) { + + } + + ngOnInit(): void { + } + + openEditJSONDialog($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(JsonObjectEditDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + jsonValue: this.modelValue + } + }).afterClosed().subscribe( + (res) => { + if (res) { + this.modelValue = res; + this.inputForm.control.patchValue({value: this.modelValue}); + } + } + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: any): void { + this.modelValue = value; + if (this.modelValue === true || this.modelValue === false) { + this.valueType = ValueType.BOOLEAN; + } else if (typeof this.modelValue === 'number') { + if (this.modelValue.toString().indexOf('.') === -1) { + this.valueType = ValueType.INTEGER; + } else { + this.valueType = ValueType.DOUBLE; + } + } else if (isObject(this.modelValue)) { + this.valueType = ValueType.JSON; + } else { + this.valueType = ValueType.STRING; + } + } + + updateView() { + if (this.inputForm.valid || this.valueType === ValueType.BOOLEAN) { + this.propagateChange(this.modelValue); + } else { + this.propagateChange(null); + } + } + + onValueTypeChanged() { + if (this.valueType === ValueType.BOOLEAN) { + this.modelValue = false; + } if (this.valueType === ValueType.JSON) { + this.modelValue = {}; + } else { + this.modelValue = null; + } + this.updateView(); + } + + onValueChanged() { + this.updateView(); + } + +} diff --git a/ui-ngx/src/app/shared/components/widgets-bundle-select.component.html b/ui-ngx/src/app/shared/components/widgets-bundle-select.component.html new file mode 100644 index 0000000000..6d3261e85f --- /dev/null +++ b/ui-ngx/src/app/shared/components/widgets-bundle-select.component.html @@ -0,0 +1,40 @@ + + + + + +
    + {{widgetsBundle?.title}} + widgets-bundle.system +
    +
    + +
    + {{widgetsBundle.title}} + widgets-bundle.system +
    +
    +
    +
    diff --git a/ui-ngx/src/app/shared/components/widgets-bundle-select.component.scss b/ui-ngx/src/app/shared/components/widgets-bundle-select.component.scss new file mode 100644 index 0000000000..4af250b0bb --- /dev/null +++ b/ui-ngx/src/app/shared/components/widgets-bundle-select.component.scss @@ -0,0 +1,111 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../scss/constants'; + +tb-widgets-bundle-select { + mat-select { + margin: 0; + } + + .tb-bundle-item { + height: 26px; + line-height: 26px; + } +} + +.tb-widgets-bundle-select { + .tb-bundle-item { + height: 48px; + line-height: 48px; + } +} + +tb-widgets-bundle-select, +.tb-widgets-bundle-select { + .mat-select-value-text { + display: block; + width: 100%; + } + + .tb-bundle-item { + display: inline-block; + width: 100%; + + span { + display: inline-block; + vertical-align: middle; + } + + .tb-bundle-system { + float: right; + font-size: .8rem; + opacity: .8; + } + } + + mat-option { + height: auto !important; + white-space: normal !important; + } +} + +mat-toolbar { + tb-widgets-bundle-select { + .mat-form-field-wrapper { + padding-bottom: 0 !important; + .mat-form-field-infix { + background: rgba(255, 255, 255, .2); + padding: 5px 20px !important; + border: none; + + mat-select { + .mat-select-value-text { + font-size: 1.2rem; + color: #fff; + + span:first-child::after { + color: #fff; + } + } + + .mat-select-value { + vertical-align: middle; + min-height: 30px; + height: 30px; + padding: 2px 2px 1px; + .mat-select-placeholder { + color: #fff; + opacity: .8; + } + } + } + + .mat-select.mat-select-invalid { + .mat-select-arrow { + color: #fff !important; + } + } + + @media #{$mat-xs} { + width: auto; + } + } + .mat-form-field-underline { + display: none; + } + } + } +} diff --git a/ui-ngx/src/app/shared/components/widgets-bundle-select.component.ts b/ui-ngx/src/app/shared/components/widgets-bundle-select.component.ts new file mode 100644 index 0000000000..90952c1ddb --- /dev/null +++ b/ui-ngx/src/app/shared/components/widgets-bundle-select.component.ts @@ -0,0 +1,156 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Observable } from 'rxjs'; +import { share, tap } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; +import { WidgetService } from '@core/http/widget.service'; +import { isDefined } from '@core/utils'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; + +@Component({ + selector: 'tb-widgets-bundle-select', + templateUrl: './widgets-bundle-select.component.html', + styleUrls: ['./widgets-bundle-select.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => WidgetsBundleSelectComponent), + multi: true + }], + encapsulation: ViewEncapsulation.None +}) +export class WidgetsBundleSelectComponent implements ControlValueAccessor, OnInit, OnChanges { + + @Input() + bundlesScope: 'system' | 'tenant'; + + @Input() + selectFirstBundle: boolean; + + @Input() + selectBundleAlias: string; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + widgetsBundles$: Observable>; + + widgetsBundles: Array; + + widgetsBundle: WidgetsBundle | null; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private widgetService: WidgetService) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.widgetsBundles$ = this.getWidgetsBundles().pipe( + tap((widgetsBundles) => { + this.widgetsBundles = widgetsBundles; + if (this.selectFirstBundle) { + if (widgetsBundles.length > 0) { + if (this.widgetsBundle !== widgetsBundles[0]) { + this.widgetsBundle = widgetsBundles[0]; + this.updateView(); + } else if (isDefined(this.selectBundleAlias)) { + this.selectWidgetsBundleByAlias(this.selectBundleAlias); + } + } + } + }), + share() + ); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'selectBundleAlias') { + this.selectWidgetsBundleByAlias(this.selectBundleAlias); + } + } + } + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: WidgetsBundle | null): void { + this.widgetsBundle = value; + } + + widgetsBundleChanged() { + this.updateView(); + } + + isSystem(item: WidgetsBundle) { + return item && item.tenantId.id === NULL_UUID; + } + + private selectWidgetsBundleByAlias(alias: string) { + if (this.widgetsBundles && alias) { + const found = this.widgetsBundles.find((widgetsBundle) => widgetsBundle.alias === alias); + if (found && this.widgetsBundle !== found) { + this.widgetsBundle = found; + this.updateView(); + } + } + } + + private updateView() { + this.propagateChange(this.widgetsBundle); + } + + private getWidgetsBundles(): Observable> { + let widgetsBundlesObservable: Observable>; + if (this.bundlesScope) { + if (this.bundlesScope === 'system') { + widgetsBundlesObservable = this.widgetService.getSystemWidgetsBundles(); + } else if (this.bundlesScope === 'tenant') { + widgetsBundlesObservable = this.widgetService.getTenantWidgetsBundles(); + } + } else { + widgetsBundlesObservable = this.widgetService.getAllWidgetsBundles(); + } + return widgetsBundlesObservable; + } + +} diff --git a/ui-ngx/src/app/shared/decorators/enumerable.ts b/ui-ngx/src/app/shared/decorators/enumerable.ts new file mode 100644 index 0000000000..c1a37ed7d0 --- /dev/null +++ b/ui-ngx/src/app/shared/decorators/enumerable.ts @@ -0,0 +1,25 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export function enumerable(value: boolean) { + return ( + target: any, + propertyKey: string, + descriptor: PropertyDescriptor + ) => { + descriptor.enumerable = value; + }; +} diff --git a/ui-ngx/src/app/shared/models/ace/completion.models.ts b/ui-ngx/src/app/shared/models/ace/completion.models.ts new file mode 100644 index 0000000000..f2810781bb --- /dev/null +++ b/ui-ngx/src/app/shared/models/ace/completion.models.ts @@ -0,0 +1,215 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import * as ace from 'ace-builds'; + +export type tbMetaType = 'object' | 'function' | 'service' | 'property' | 'argument'; + +export type TbEditorCompletions = {[name: string]: TbEditorCompletion}; + +export interface FunctionArgType { + type?: string; + description?: string; +} + +export interface FunctionArg extends FunctionArgType { + name: string; + type?: string; + description?: string; + optional?: boolean; +} + +export interface TbEditorCompletion { + meta: tbMetaType; + description?: string; + type?: string; + args?: FunctionArg[]; + return?: FunctionArgType; + children?: TbEditorCompletions; +} + +interface TbEditorAceCompletion extends ace.Ace.Completion { + isTbEditorAceCompletion: true; + snippet: string; + description?: string; + type?: string; + args?: FunctionArg[]; + return?: FunctionArgType; +} + +export class TbEditorCompleter implements ace.Ace.Completer { + + identifierRegexps: RegExp[] = [ + /[a-zA-Z_0-9\$\-\u00A2-\u2000\u2070-\uFFFF.]/ + ]; + + constructor(private editorCompletions: TbEditorCompletions) { + } + + getCompletions(editor: ace.Ace.Editor, session: ace.Ace.EditSession, + position: ace.Ace.Point, prefix: string, callback: ace.Ace.CompleterCallback): void { + const result = this.prepareCompletions(prefix); + if (result) { + callback(null, result); + } + } + + private resolvePath(prefix: string): string[] { + if (!prefix || !prefix.length) { + return []; + } + let parts = prefix.split('.'); + if (parts.length) { + parts.pop(); + } else { + parts = []; + } + let currentCompletions = this.editorCompletions; + for (const part of parts) { + if (currentCompletions[part]) { + currentCompletions = currentCompletions[part].children; + if (!currentCompletions) { + return null; + } + } else { + return null; + } + } + return parts; + } + + private prepareCompletions(prefix: string): ace.Ace.Completion[] { + const path = this.resolvePath(prefix); + if (path !== null) { + return this.toAceCompletionsList(this.editorCompletions, path); + } else { + return []; + } + } + + private toAceCompletionsList(completions: TbEditorCompletions, parentPath: string[]): ace.Ace.Completion[] { + const result: ace.Ace.Completion[] = []; + let targetCompletions = completions; + let parentPrefix = ''; + if (parentPath.length) { + parentPrefix = parentPath.join('.') + '.'; + for (const path of parentPath) { + targetCompletions = targetCompletions[path].children; + } + } + for (const key of Object.keys(targetCompletions)) { + result.push(this.toAceCompletion(key, targetCompletions[key], parentPrefix)); + } + return result; + } + + private toAceCompletion(name: string, completion: TbEditorCompletion, parentPrefix: string): ace.Ace.Completion { + const aceCompletion: TbEditorAceCompletion = { + isTbEditorAceCompletion: true, + snippet: parentPrefix + name, + name, + caption: parentPrefix + name, + score: 100000, + value: parentPrefix + name, + meta: completion.meta, + type: completion.type, + description: completion.description, + args: completion.args, + return: completion.return + }; + return aceCompletion; + } + + getDocTooltip(completion: TbEditorAceCompletion) { + if (completion && completion.isTbEditorAceCompletion) { + return { + docHTML: this.createDocHTML(completion) + }; + } + } + + private createDocHTML(completion: TbEditorAceCompletion): string { + let title = `${completion.name}`; + if (completion.meta === 'function') { + title += '('; + if (completion.args) { + const strArgs: string[] = []; + for (const arg of completion.args) { + let strArg = `${arg.name}`; + if (arg.optional) { + strArg += '?'; + } + if (arg.type) { + strArg += `: ${arg.type}`; + } + strArgs.push(strArg); + } + title += strArgs.join(', '); + } + title += '): '; + if (completion.return) { + title += completion.return.type; + } else { + title += 'void'; + } + } else { + title += `: ${completion.type ? completion.type : completion.meta}`; + } + let html = `
    ${title}`; + if (completion.description) { + html += `
    ${completion.description}
    `; + } + if (completion.args || completion.return) { + let functionInfoBlock = '
    '; + if (completion.args) { + functionInfoBlock += '
    Parameters
    ' + let argsTable = ''; + const strArgs: string[] = []; + for (const arg of completion.args) { + let strArg = `'; + strArgs.push(strArg); + } + argsTable += strArgs.join('') + '
    ${arg.name}`; + if (arg.optional) { + strArg += ' (optional)'; + } + strArg += ''; + if (arg.type) { + strArg += `${arg.type}` + } + strArg += ''; + if (arg.description) { + strArg += `${arg.description}`; + } + strArg += '
    '; + functionInfoBlock += argsTable; + } + if (completion.return) { + let returnStr = '
    Returns
    '; + returnStr += `
    ${completion.return.type}`; + if (completion.return.description) { + returnStr += `: ${completion.return.description}`; + } + returnStr += '
    '; + functionInfoBlock += returnStr; + } + functionInfoBlock += '
    '; + html += functionInfoBlock; + } + html += '
    '; + return html; + } +} diff --git a/ui-ngx/src/app/shared/models/ace/service-completion.models.ts b/ui-ngx/src/app/shared/models/ace/service-completion.models.ts new file mode 100644 index 0000000000..f9b42f12f5 --- /dev/null +++ b/ui-ngx/src/app/shared/models/ace/service-completion.models.ts @@ -0,0 +1,1389 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { FunctionArg, FunctionArgType, TbEditorCompletions } from '@shared/models/ace/completion.models'; + +export const entityIdHref = 'EntityId'; + +export const baseDataHref = 'Base data'; + +export const alarmDataHref = 'Alarm data'; + +export const alarmDataQueryHref = 'Alarm data query'; + +export const attributeScopeHref = 'Attribute scope'; + +export const entityTypeHref = 'EntityType'; + +export const pageDataHref = 'PageData'; + +export const deviceInfoHref = 'DeviceInfo'; + +export const assetInfoHref = 'AssetInfo'; + +export const entityViewInfoHref = 'EntityViewInfo'; + +export const entityRelationsQueryHref = 'EntityRelationsQuery'; + +export const entityRelationInfoHref = 'EntityRelationInfo'; + +export const dashboardInfoHref = 'DashboardInfo'; + +export const deviceHref = 'Device'; + +export const assetHref = 'Asset'; + +export const entityViewHref = 'entityView'; + +export const entityRelationHref = 'Entity relation'; + +export const dashboardHref = 'Dashboard'; + +export const customerHref = 'Customer'; + +export const attributeDataHref = 'Attribute Data'; + +export const userHref = 'User'; + +export const entityDataHref = 'Entity data'; + +export const entityDataQueryHref = 'Entity Data Query'; + +export const deviceCredentialsHref = 'DeviceCredentials'; + +export const entityFilterHref = 'Entity filter'; + +export const entityInfoHref = 'Entity info'; + +export const aliasEntityTypeHref = 'Alias Entity Type'; + +export const aliasFilterTypeHref = 'Alias filter type'; + +export const entityAliasHref = 'Entity alias'; + +export const dataKeyTypeHref = 'Data key type'; + +export const subscriptionInfoHref = 'Subscription info'; + +export const dataSourceHref = 'Datasource'; + +export const stateParamsHref = 'State params'; + +export const aliasInfoHref = 'Alias info'; + +export const entityAliasFilterHref = 'Entity alias filter'; + +export const entityAliasFilterResultHref = 'Entity alias filter result'; + +export const importEntityDataHref = 'Import entity data'; + +export const importEntitiesResultInfoHref = 'Import entities result info'; + +export const customDialogComponentHref = 'CustomDialogComponent'; + +export const pageLinkArg: FunctionArg = { + name: 'pageLink', + type: 'PageLink', + description: 'Page link object used to perform paginated request.' +}; + +export const requestConfigArg: FunctionArg = { + name: 'config', + type: 'RequestConfig', + description: 'HTTP request configuration.', + optional: true +}; + +export function observableReturnType(objectType: string): FunctionArgType { + return { + type: `Observable<${objectType}>`, + description: `An Observable of ${objectType} object.` + }; +} + +export function observableReturnTypeVariable(variableType: string): FunctionArgType { + return { + type: `Observable<${variableType}>`, + description: `An Observable of ${variableType} variable.` + }; +} + +export function observableVoid(): FunctionArgType { + return { + type: `Observable<void>`, + description: `An Observable.` + }; +} + +export function observableArrayReturnType(objectType: string): FunctionArgType { + return { + type: `Observable<Array<${objectType}>>`, + description: `An Observable of array of ${objectType} objects.` + }; +} + +export function observableBaseDataReturnType(): FunctionArgType { + return { + type: `Observable<${baseDataHref}<${entityIdHref}>>`, + description: `An Observable of ${baseDataHref} object.` + }; +} + +export function observableArrayBaseDataReturnType(): FunctionArgType { + return { + type: `Observable<Array<${baseDataHref}<${entityIdHref}>>>`, + description: `An Observable of array of ${baseDataHref} objects.` + }; +} + +export function observablePageDataReturnType(objectType: string): FunctionArgType { + return { + type: `Observable<${pageDataHref}<${objectType}>>`, + description: `An Observable of page result as a ${pageDataHref} holding array of ${objectType} objects.` + }; +} + +export const serviceCompletions: TbEditorCompletions = { + deviceService: { + description: 'Device Service API
    ' + + 'See DeviceService for API reference.', + meta: 'service', + type: 'DeviceService', + children: { + getTenantDeviceInfos: { + description: 'Get tenant devices', + meta: 'function', + args: [ + pageLinkArg, + { name: 'type', type: 'string', optional: true, description: 'Device type'}, + requestConfigArg + ], + return: observablePageDataReturnType(deviceInfoHref) + }, + getCustomerDeviceInfos: { + description: 'Get customer devices', + meta: 'function', + args: [ + { name: 'customerId', type: 'string', description: 'Id of the customer'}, + pageLinkArg, + { name: 'type', type: 'string', optional: true, description: 'Device type'}, + requestConfigArg + ], + return: observablePageDataReturnType(deviceInfoHref) + }, + getDevice: { + description: 'Get device by id', + meta: 'function', + args: [ + { name: 'deviceId', type: 'string', description: 'Id of the device'}, + requestConfigArg + ], + return: observableReturnType(deviceHref) + }, + getDevices: { + description: 'Get devices by ids', + meta: 'function', + args: [ + { name: 'deviceIds', type: `Array<string>`, description: 'List of device ids'}, + requestConfigArg + ], + return: observableArrayReturnType(deviceHref) + }, + getDeviceInfo: { + description: 'Get device info by id', + meta: 'function', + args: [ + { name: 'deviceId', type: 'string', description: 'Id of the device'}, + requestConfigArg + ], + return: observableReturnType(deviceInfoHref) + }, + saveDevice: { + description: 'Save device', + meta: 'function', + args: [ + { name: 'device', type: deviceHref, description: 'Device object to save'}, + requestConfigArg + ], + return: observableReturnType(deviceHref) + }, + deleteDevice: { + description: 'Delete device by id', + meta: 'function', + args: [ + { name: 'deviceId', type: 'string', description: 'Id of the device'}, + requestConfigArg + ], + return: observableVoid() + }, + getDeviceTypes: { + description: 'Get all available devices types', + meta: 'function', + args: [ + requestConfigArg + ], + return: observableArrayReturnType('EntitySubtype') + }, + getDeviceCredentials: { + description: 'Get device credentials by device id', + meta: 'function', + args: [ + { name: 'deviceId', type: 'string', description: 'Id of the device'}, + { name: 'sync', type: 'boolean', description: 'Whether to execute HTTP request synchronously (false by default)', optional: true}, + requestConfigArg + ], + return: observableReturnType(deviceCredentialsHref) + }, + saveDeviceCredentials: { + description: 'Save device credentials', + meta: 'function', + args: [ + { name: 'deviceCredentials', type: deviceCredentialsHref, description: 'Device credentials object to save'}, + requestConfigArg + ], + return: observableReturnType(deviceCredentialsHref) + }, + makeDevicePublic: { + description: 'Make device public (available from public dashboard)', + meta: 'function', + args: [ + { name: 'deviceId', type: 'string', description: 'Id of the device'}, + requestConfigArg + ], + return: observableReturnType(deviceHref) + }, + assignDeviceToCustomer: { + description: 'Assign device to specific customer', + meta: 'function', + args: [ + { name: 'customerId', type: 'string', description: 'Id of the customer'}, + { name: 'deviceId', type: 'string', description: 'Id of the device'}, + requestConfigArg + ], + return: observableReturnType(deviceHref) + }, + unassignDeviceFromCustomer: { + description: 'Unassign device from any customer', + meta: 'function', + args: [ + { name: 'deviceId', type: 'string', description: 'Id of the device'}, + requestConfigArg + ], + return: observableVoid() + }, + sendOneWayRpcCommand: { + description: 'Send one way (without response) RPC command to the device.', + meta: 'function', + args: [ + { name: 'deviceId', type: 'string', description: 'Id of the device'}, + { name: 'requestBody', type: 'object', description: 'Request body to be sent to device'}, + requestConfigArg + ], + return: { + type: `Observable<any>`, + description: `A command execution Observable.` + } + }, + sendTwoWayRpcCommand: { + description: 'Sends two way (with response) RPC command to the device.', + meta: 'function', + args: [ + { name: 'deviceId', type: 'string', description: 'Id of the device'}, + { name: 'requestBody', type: 'object', description: 'Request body to be sent to device'}, + requestConfigArg + ], + return: { + type: `Observable<any>`, + description: `A command execution Observable of response body.` + } + }, + findByQuery: { + description: 'Find devices by search query', + meta: 'function', + args: [ + { name: 'query', type: 'DeviceSearchQuery', + description: 'Device search query object'}, + requestConfigArg + ], + return: observableArrayReturnType(deviceHref) + }, + findByName: { + description: 'Find device by name', + meta: 'function', + args: [ + { name: 'deviceName', type: 'string', + description: 'Search device name'}, + requestConfigArg + ], + return: observableReturnType(deviceHref) + }, + claimDevice: { + description: 'Send claim device request', + meta: 'function', + args: [ + { name: 'deviceName', type: 'string', + description: 'Claiming device name'}, + requestConfigArg + ], + return: observableReturnType('ClaimResult') + }, + unclaimDevice: { + description: 'Send un-claim device request', + meta: 'function', + args: [ + { name: 'deviceName', type: 'string', + description: 'Device name to un-claim'}, + requestConfigArg + ], + return: observableVoid() + } + } + }, + assetService: { + description: 'Asset Service API
    ' + + 'See AssetService for API reference.', + meta: 'service', + type: 'AssetService', + children: { + getTenantAssetInfos: { + description: 'Get tenant assets', + meta: 'function', + args: [ + pageLinkArg, + {name: 'type', type: 'string', optional: true, description: 'Asset type'}, + requestConfigArg + ], + return: observablePageDataReturnType(assetInfoHref) + }, + getCustomerAssetInfos: { + description: 'Get customer assets', + meta: 'function', + args: [ + {name: 'customerId', type: 'string', description: 'Id of the customer'}, + pageLinkArg, + {name: 'type', type: 'string', optional: true, description: 'Asset type'}, + requestConfigArg + ], + return: observablePageDataReturnType(assetInfoHref) + }, + getAsset: { + description: 'Get asset by id', + meta: 'function', + args: [ + {name: 'assetId', type: 'string', description: 'Id of the asset'}, + requestConfigArg + ], + return: observableReturnType(assetHref) + }, + getAssets: { + description: 'Get assets by ids', + meta: 'function', + args: [ + {name: 'assetIds', type: `Array<string>`, description: 'Ids of the assets'}, + requestConfigArg + ], + return: observableArrayReturnType(assetHref) + }, + getAssetInfo: { + description: 'Get asset info by id', + meta: 'function', + args: [ + {name: 'assetId', type: 'string', description: 'Id of the assets'}, + requestConfigArg + ], + return: observableReturnType(assetInfoHref) + }, + saveAsset: { + description: 'Save asset', + meta: 'function', + args: [ + {name: 'asset', type: assetHref, description: 'Asset object to save'}, + requestConfigArg + ], + return: observableReturnType(assetHref) + }, + deleteAsset: { + description: 'Delete asset by id', + meta: 'function', + args: [ + {name: 'assetId', type: 'string', description: 'Id of the asset'}, + requestConfigArg + ], + return: observableVoid() + }, + getAssetTypes: { + description: 'Get all available assets types', + meta: 'function', + args: [ + requestConfigArg + ], + return: observableArrayReturnType('EntitySubtype') + }, + makeAssetPublic: { + description: 'Make asset public (available from public dashboard)', + meta: 'function', + args: [ + {name: 'assetId', type: 'string', description: 'Id of the asset'}, + requestConfigArg + ], + return: observableReturnType(assetHref) + }, + assignAssetToCustomer: { + description: 'Assign asset to specific customer', + meta: 'function', + args: [ + {name: 'customerId', type: 'string', description: 'Id of the customer'}, + {name: 'assetId', type: 'string', description: 'Id of the asset'}, + requestConfigArg + ], + return: observableReturnType(assetHref) + }, + unassignAssetFromCustomer: { + description: 'Unassign asset from any customer', + meta: 'function', + args: [ + {name: 'assetId', type: 'string', description: 'Id of the asset'}, + requestConfigArg + ], + return: observableVoid() + }, + findByQuery: { + description: 'Find assets by search query', + meta: 'function', + args: [ + { + name: 'query', + type: 'AssetSearchQuery', + description: 'Asset search query object' + }, + requestConfigArg + ], + return: observableArrayReturnType(assetHref) + }, + findByName: { + description: 'Find asset by name', + meta: 'function', + args: [ + { + name: 'assetName', type: 'string', + description: 'Search asset name' + }, + requestConfigArg + ], + return: observableReturnType(assetHref) + }, + }, + }, + entityViewService: { + description: 'EntityView Service API
    ' + + 'See EntityViewService for API reference.', + meta: 'service', + type: 'EntityViewService', + children: { + getTenantEntityViewInfos: { + description: 'Get tenant entity view infos', + meta: 'function', + args: [ + pageLinkArg, + {name: 'type', type: 'string', optional: true, description: 'Entity view type'}, + requestConfigArg + ], + return: observablePageDataReturnType(entityViewInfoHref) + }, + getCustomerEntityViewInfos: { + description: 'Get customer entities view infos by id', + meta: 'function', + args: [ + { name: 'customerId', type: 'string', description: 'Id of the customer'}, + pageLinkArg, + { name: 'type', type: 'string', optional: true, description: 'Entity view type'}, + requestConfigArg + ], + return: observablePageDataReturnType(entityViewInfoHref) + }, + getEntityView: { + description: 'Get entity view by id', + meta: 'function', + args: [ + { name: 'entityViewId', type: 'string', description: 'Id of the entity view'}, + requestConfigArg + ], + return: observableReturnType(entityViewHref) + }, + getEntityViewInfo: { + description: 'Get entity view info by id', + meta: 'function', + args: [ + {name: 'entityViewId', type: 'string', description: 'Id of the entity view'}, + requestConfigArg + ], + return: observableReturnType(entityViewInfoHref) + }, + saveEntityView: { + description: 'Save entity view', + meta: 'function', + args: [ + {name: 'entityView', type: entityViewHref, description: 'Entity view object to save'}, + requestConfigArg + ], + return: observableReturnType(entityViewHref) + }, + deleteEntityView: { + description: 'Delete entity view by id', + meta: 'function', + args: [ + {name: 'entityViewId', type: 'string', description: 'Id of the entity view'}, + requestConfigArg + ], + return: observableVoid() + }, + getEntityViewTypes: { + description: 'Get all available entity view types', + meta: 'function', + args: [ + requestConfigArg + ], + return: observableArrayReturnType('EntitySubtype') + }, + makeEntityViewPublic: { + description: 'Make entity view public (available from public dashboard)', + meta: 'function', + args: [ + {name: 'entityViewId', type: 'string', description: 'Id of the entity view'}, + requestConfigArg + ], + return: observableReturnType(entityViewHref) + }, + assignEntityViewToCustomer: { + description: 'Assign entity view to specific customer', + meta: 'function', + args: [ + {name: 'customerId', type: 'string', description: 'Id of the customer'}, + {name: 'entityViewId', type: 'string', description: 'Id of the entity view'}, + requestConfigArg + ], + return: observableReturnType(entityViewHref) + }, + unassignEntityViewFromCustomer: { + description: 'Unassign entity view from any customer', + meta: 'function', + args: [ + {name: 'entityViewId', type: 'string', description: 'Id of the entity view'}, + requestConfigArg + ], + return: observableVoid() + }, + findByQuery: { + description: 'Find entities view by search query', + meta: 'function', + args: [ + { + name: 'query', + type: 'AssetSearchQuery', + description: 'Entity view search query object' + }, + requestConfigArg + ], + return: observableArrayReturnType(entityViewHref) + }, + } + }, + customerService: { + description: 'Customer Service API
    ' + + 'See CustomerService for API reference.', + meta: 'service', + type: 'CustomerService', + children: { + getCustomer: { + description: 'Get customer by id', + meta: 'function', + args: [ + {name: 'customerId', type: 'string', description: 'Id of the customer'}, + requestConfigArg + ], + return: observableReturnType(customerHref) + }, + getCustomers: { + description: 'Get customers by ids', + meta: 'function', + args: [ + pageLinkArg, + requestConfigArg + ], + return: observablePageDataReturnType(customerHref) + }, + saveCustomer: { + description: 'Save customer', + meta: 'function', + args: [ + {name: 'customer', type: customerHref, description: 'Customer object to save'}, + requestConfigArg + ], + return: observableReturnType(customerHref) + }, + deleteCustomer: { + description: 'Delete customer by id', + meta: 'function', + args: [ + {name: 'customerId', type: 'string', description: 'Id of the customer'}, + requestConfigArg + ], + return: observableVoid() + }, + } + }, + dashboardService: { + description: 'Dashboard Service API
    ' + + 'See DashboardService for API reference.', + meta: 'service', + type: 'DashboardService', + children: { + getTenantDashboards: { + description: 'Get tenant dashboards', + meta: 'function', + args: [ + pageLinkArg, + requestConfigArg + ], + return: observablePageDataReturnType(dashboardInfoHref) + }, + getTenantDashboardsByTenantId: { + description: 'Get dashboards by tenant id', + meta: 'function', + args: [ + {name: 'tenantId', type: 'string', description: 'Id of the tenant'}, + pageLinkArg, + requestConfigArg + ], + return: observablePageDataReturnType(dashboardInfoHref) + }, + getCustomerDashboards: { + description: 'Get dashboards by customer id', + meta: 'function', + args: [ + {name: 'customerId', type: 'string', description: 'Id of the customer'}, + pageLinkArg, + requestConfigArg + ], + return: observablePageDataReturnType(dashboardInfoHref) + }, + getDashboard: { + description: 'Get dashboard by id', + meta: 'function', + args: [ + {name: 'dashboardId', type: 'string', description: 'Id of the dashboard'}, + requestConfigArg + ], + return: observableReturnType(dashboardHref) + }, + getDashboardInfo: { + description: 'Get dashboard info by id', + meta: 'function', + args: [ + {name: 'dashboardId', type: 'string', description: 'Id of the dashboard'}, + requestConfigArg + ], + return: observableReturnType(dashboardInfoHref) + }, + saveDashboard: { + description: 'Save dashboard', + meta: 'function', + args: [ + {name: 'dashboard', type: dashboardHref, description: 'Dashboard object to save'}, + requestConfigArg + ], + return: observableReturnType(dashboardHref) + }, + deleteDashboard: { + description: 'Delete dashboard by id', + meta: 'function', + args: [ + {name: 'dashboardId', type: 'string', description: 'Id of the dashboard'}, + requestConfigArg + ], + return: observableVoid() + }, + assignDashboardToCustomer: { + description: 'Assign dashboard to specific customer', + meta: 'function', + args: [ + {name: 'customerId', type: 'string', description: 'Id of the customer'}, + {name: 'dashboardId', type: 'string', description: 'Id of the dashboard'}, + requestConfigArg + ], + return: observableReturnType(dashboardHref) + }, + unassignDashboardFromCustomer: { + description: 'Unassign dashboard from specific customer', + meta: 'function', + args: [ + {name: 'customerId', type: 'string', description: 'Id of the customer'}, + {name: 'dashboardId', type: 'string', description: 'Id of the dashboard'}, + requestConfigArg + ], + return: observableVoid() + }, + makeDashboardPublic: { + description: 'Make dashboard public by id', + meta: 'function', + args: [ + {name: 'dashboardId', type: 'string', description: 'Id of the dashboard'}, + requestConfigArg + ], + return: observableReturnType(dashboardHref) + }, + makeDashboardPrivate: { + description: 'Make dashboard private by id', + meta: 'function', + args: [ + {name: 'dashboardId', type: 'string', description: 'Id of the dashboard'}, + requestConfigArg + ], + return: observableReturnType(dashboardHref) + }, + updateDashboardCustomers: { + description: 'Update customers assigned to dashboard by ids', + meta: 'function', + args: [ + {name: 'dashboardId', type: 'string', description: 'Id of the dashboard'}, + {name: 'customerIds', type: `Array<string>`, description: 'Ids of the customers'}, + requestConfigArg + ], + return: observableReturnType(dashboardHref) + }, + addDashboardCustomers: { + description: 'Assign (Add) customers to dashboard by ids', + meta: 'function', + args: [ + {name: 'dashboardId', type: 'string', description: 'Id of the dashboard'}, + {name: 'customerIds', type: `Array<string>`, description: 'Ids of the customers'}, + requestConfigArg + ], + return: observableReturnType(dashboardHref) + }, + removeDashboardCustomers: { + description: 'Unassign (Remove) customers from dashboard by ids', + meta: 'function', + args: [ + {name: 'dashboardId', type: 'string', description: 'Id of the dashboard'}, + {name: 'customerIds', type: `Array<string>`, description: 'Id of the customers'}, + requestConfigArg + ], + return: observableReturnType(dashboardHref) + }, + getPublicDashboardLink: { + description: 'Get public dashboard link', + meta: 'function', + args: [ + {name: 'dashboard', type: dashboardInfoHref, description: 'dashboard info'}, + ], + return: { + type: `string|null`, + description: `Returns dashboard url` + } + }, + getServerTimeDiff: { + description: 'Get time difference', + meta: 'function', + args: [ + ], + return: observableReturnTypeVariable('number') + }, + } + }, + userService: { + description: 'User Service API
    ' + + 'See UserService for API reference.', + meta: 'service', + type: 'UserService', + children: { + getUsers: { + description: 'Get users', + meta: 'function', + args: [ + pageLinkArg, + requestConfigArg + ], + return: observablePageDataReturnType(userHref) + }, + getTenantAdmins: { + description: 'Get tenant admins by id', + meta: 'function', + args: [ + {name: 'tenantId', type: 'string', description: 'Id of the tenant'}, + pageLinkArg, + requestConfigArg + ], + return: observablePageDataReturnType(userHref) + }, + getCustomerUsers: { + description: 'Get customer users by id', + meta: 'function', + args: [ + {name: 'customerId', type: 'string', description: 'Id of the customer'}, + pageLinkArg, + requestConfigArg + ], + return: observablePageDataReturnType(userHref) + }, + getUser: { + description: 'Get user by id', + meta: 'function', + args: [ + {name: 'userId', type: 'string', description: 'Id of the user'}, + requestConfigArg + ], + return: observableReturnType(userHref) + }, + saveUser: { + description: 'Save user', + meta: 'function', + args: [ + {name: 'user', type: userHref, description: 'User object to save'}, + {name: 'sendActivationMail', type: 'boolean', description: 'Send activation email', optional: true}, + requestConfigArg + ], + return: observableReturnType(userHref) + }, + deleteUser: { + description: 'Delete user by id', + meta: 'function', + args: [ + {name: 'userId', type: 'string', description: 'Id of the user'}, + requestConfigArg + ], + return: observableVoid() + }, + setUserCredentialsEnabled: { + description: 'Set user credentials enabled by id', + meta: 'function', + args: [ + {name: 'userId', type: 'string', description: 'Id of the user'}, + {name: 'userCredentialsEnabled', type: 'boolean', description: 'User credentials enabled'}, + requestConfigArg + ], + return: observableReturnTypeVariable('any') + }, + getActivationLink: { + description: 'Get activation link by id', + meta: 'function', + args: [ + {name: 'userId', type: 'string', description: 'Id of the user'}, + requestConfigArg + ], + return: observableReturnTypeVariable('string') + }, + sendActivationEmail: { + description: 'Send activation email', + meta: 'function', + args: [ + {name: 'email', type: 'string', description: 'Email of the user'}, + requestConfigArg + ], + return: observableVoid() + }, + } + }, + entityRelationService: { + description: 'Entity Relation Service API
    ' + + 'See EntityRelationService for API reference.', + meta: 'service', + type: 'EntityRelationService', + children: { + saveRelation: { + description: 'Save relation', + meta: 'function', + args: [ + {name: 'relation', type: entityRelationHref, description: 'Relation object to save'}, + requestConfigArg + ], + return: observableReturnType(entityRelationHref) + }, + deleteRelation: { + description: 'Delete relation by ids', + meta: 'function', + args: [ + {name: 'fromId', type: entityIdHref, description: 'From-id'}, + {name: 'relationType', type: 'string', description: 'Relation type'}, + {name: 'toId', type: entityIdHref, description: 'To-id'}, + requestConfigArg + ], + return: observableVoid() + }, + deleteRelations: { + description: 'Delete relations by entity id', + meta: 'function', + args: [ + {name: 'entityId', type: entityIdHref, description: 'Entity Id'}, + requestConfigArg + ], + return: observableVoid() + }, + getRelation: { + description: 'Get relation by ids', + meta: 'function', + args: [ + {name: 'fromId', type: entityIdHref, description: 'From-id'}, + {name: 'relationType', type: 'string', description: 'Relation type'}, + {name: 'toId', type: entityIdHref, description: 'To-id'}, + requestConfigArg + ], + return: observableReturnType(entityRelationHref) + }, + findByFrom: { + description: 'Find by from-id', + meta: 'function', + args: [ + {name: 'fromId', type: entityIdHref, description: 'From-id'}, + requestConfigArg + ], + return: observableArrayReturnType(entityRelationHref) + }, + findInfoByFrom: { + description: 'Find info by from-id', + meta: 'function', + args: [ + {name: 'fromId', type: entityIdHref, description: 'From-id'}, + requestConfigArg + ], + return: observableArrayReturnType(entityRelationInfoHref) + }, + findByFromAndType: { + description: 'Find by from-id and relation type', + meta: 'function', + args: [ + {name: 'fromId', type: entityIdHref, description: 'From-id'}, + {name: 'relationType', type: 'string', description: 'Relation type'}, + requestConfigArg + ], + return: observableArrayReturnType(entityRelationHref) + }, + findByTo: { + description: 'Find by to-id', + meta: 'function', + args: [ + {name: 'toId', type: entityIdHref, description: 'To-id'}, + requestConfigArg + ], + return: observableArrayReturnType(entityRelationHref) + }, + findInfoByTo: { + description: 'Find info by to-id', + meta: 'function', + args: [ + {name: 'toId', type: entityIdHref, description: 'To-id'}, + requestConfigArg + ], + return: observableArrayReturnType(entityRelationInfoHref) + }, + findByToAndType: { + description: 'Find by to-id and relation type', + meta: 'function', + args: [ + {name: 'toId', type: entityIdHref, description: 'To-id'}, + {name: 'relationType', type: 'string', description: 'Relation type'}, + requestConfigArg + ], + return: observableArrayReturnType(entityRelationHref) + }, + findByQuery: { + description: 'Find by query', + meta: 'function', + args: [ + {name: 'query', type: entityRelationsQueryHref, description: 'Entity relations query'}, + requestConfigArg + ], + return: observableArrayReturnType(entityRelationHref) + }, + findInfoByQuery: { + description: 'Find info by query', + meta: 'function', + args: [ + {name: 'query', type: entityRelationsQueryHref, description: 'Entity relations query'}, + requestConfigArg + ], + return: observableArrayReturnType(entityRelationInfoHref) + }, + } + }, + attributeService: { + description: 'Attribute Service API
    ' + + 'See AttributeService for API reference.', + meta: 'service', + type: 'AttributeService', + children: { + getEntityAttributes: { + description: 'Get entity attributes by id', + meta: 'function', + args: [ + {name: 'entityId', type: entityIdHref, description: 'Id of the entity'}, + {name: 'attributeScope', type: attributeScopeHref, description: 'Attribute scope'}, + {name: 'keys', type: `Array<string>`, description: 'Array of the keys'}, + requestConfigArg + ], + return: observableArrayReturnType(attributeDataHref) + }, + deleteEntityAttributes: { + description: 'Delete entity attributes by id', + meta: 'function', + args: [ + {name: 'entityId', type: entityIdHref, description: 'Id of the entity'}, + {name: 'attributeScope', type: attributeScopeHref, description: 'Attribute scope'}, + {name: 'attributes', type: `array<${attributeDataHref}>`, description: 'Array of the attributes data'}, + requestConfigArg + ], + return: observableReturnTypeVariable('any') + }, + deleteEntityTimeseries: { + description: 'Delete entity timeseries by id', + meta: 'function', + args: [ + {name: 'entityId', type: entityIdHref, description: 'Id of the entity'}, + {name: 'timeseries', type: `Array<${attributeDataHref}>>`, description: 'Array of the timeseries data'}, + {name: 'deleteAllDataForKeys', type: 'boolean', optional: true, description: 'Delete all data for keys'}, + requestConfigArg + ], + return: observableReturnTypeVariable('any') + }, + saveEntityAttributes: { + description: 'Save entity attributes', + meta: 'function', + args: [ + {name: 'entityId', type: entityIdHref, description: 'Id of the entity'}, + {name: 'attributeScope', type: attributeScopeHref, description: 'Attribute scope'}, + {name: 'attributes', type: 'Array<${attributeDataHref}>>', description: 'Array of the attributes data'}, + requestConfigArg + ], + return: observableReturnTypeVariable('any') + }, + saveEntityTimeseries: { + description: 'Save entity timeseries', + meta: 'function', + args: [ + {name: 'entityId', type: entityIdHref, description: 'Id of the entity'}, + {name: 'timeseriesScope', type: 'string', description: 'Timeseries scope'}, + {name: 'timeseries', type: `Array<attributeDataHref>`, description: 'Array of the timeseries data'}, + requestConfigArg + ], + return: observableReturnTypeVariable('any') + }, + } + }, + entityService: { + description: 'Entity Service API
    ' + + 'See EntityService for API reference.', + meta: 'service', + type: 'EntityService', + children: { + getEntity: { + description: 'Get entity by id', + meta: 'function', + args: [ + {name: 'entityType', type: entityTypeHref, description: 'Entity type'}, + {name: 'entityId', type: 'string', description: 'Id of the entity'}, + requestConfigArg + ], + return: observableBaseDataReturnType() + }, + getEntities: { + description: 'Get entities by ids', + meta: 'function', + args: [ + {name: 'entityType', type: entityTypeHref, description: 'Entity type'}, + {name: 'entityIds', type: `Array<string>`, description: 'Ids of the entities'}, + requestConfigArg + ], + return: observableArrayBaseDataReturnType() + }, + getEntitiesByNameFilter: { + description: 'Get entities by name filter', + meta: 'function', + args: [ + {name: 'entityType', type: entityTypeHref, description: 'Entity type'}, + {name: 'entityNameFilter', type: 'string', description: 'Name filter for the entity'}, + {name: 'pageSize', type: 'number', description: 'Size of the page'}, + {name: 'subType', type: 'string', optional: true, description: 'Subtype'}, + requestConfigArg + ], + return: observableArrayBaseDataReturnType() + }, + findEntityDataByQuery: { + description: 'Find entity data by query', + meta: 'function', + args: [ + {name: 'query', type: entityDataQueryHref, description: 'Entity data query'}, + requestConfigArg + ], + return: observablePageDataReturnType(entityDataHref) + }, + findAlarmDataByQuery: { + description: 'Find alarm data by query', + meta: 'function', + args: [ + {name: 'query', type: alarmDataQueryHref, description: 'Alarm data query'}, + requestConfigArg + ], + return: observablePageDataReturnType(alarmDataHref) + }, + findEntityInfosByFilterAndName: { + description: 'Find entity infos by filter and name', + meta: 'function', + args: [ + {name: 'filter', type: entityFilterHref, description: 'Filter for the entities'}, + {name: 'searchText', type: 'string', description: 'Search text'}, + requestConfigArg + ], + return: observablePageDataReturnType(entityInfoHref) + }, + findSingleEntityInfoByEntityFilter: { + description: 'Find single entity infos by filter', + meta: 'function', + args: [ + {name: 'filter', type: entityFilterHref, description: 'Filter for the entity'}, + requestConfigArg + ], + return: observableReturnType(entityInfoHref) + }, + getAliasFilterTypesByEntityTypes: { + description: 'Get alias filter types by entity types', + meta: 'function', + args: [ + {name: 'entityTypes', type: `Array<${entityTypeHref}|${aliasEntityTypeHref}>`, description: 'Entity types'} + ], + return: { + type: `Array<${aliasFilterTypeHref}$gt;`, + description: `Array of ${aliasFilterTypeHref} objects` + } + }, + filterAliasByEntityTypes: { + description: 'Filter alias by entity types', + meta: 'function', + args: [ + {name: 'entityAlias', type: entityAliasHref, description: 'Alias of the entity'}, + {name: 'entityTypes', type: `Array<${entityTypeHref}|${aliasEntityTypeHref}>`, description: 'Entity types'} + ], + return: { + type: 'boolean', + description: `Returns boolean variable based on the filter` + } + }, + prepareAllowedEntityTypesList: { + description: 'Prepare allowed entity types list', + meta: 'function', + args: [ + {name: 'allowedEntityTypes', type: `Array<${entityTypeHref}|${aliasEntityTypeHref}>`, description: 'Entity types'}, + {name: 'useAliasEntityTypes', type: 'boolean', description: 'Use alias entity types'}, + ], + return: { + type: `Array<${entityTypeHref}|${aliasEntityTypeHref}>`, + description: `Returns entity types array` + } + }, + getEntityKeys: { + description: 'Get entity keys by id', + meta: 'function', + args: [ + {name: 'entityId', type: entityIdHref, description: 'Id of the entity'}, + {name: 'query', type: 'string', description: 'Key name starts with'}, + {name: 'type', type: dataKeyTypeHref, description: 'Datakey type'}, + requestConfigArg + ], + return: { + type: `Observable<Array<string>>`, + description: `An Observable of array of string variables.` + } + }, + createDatasourcesFromSubscriptionsInfo: { + description: 'Create datasources from subscriptions info', + meta: 'function', + args: [ + {name: 'subscriptionsInfo', type: 'array', description: 'Subscriptions info'} + ], + return: { + type: `Array<${dataSourceHref}>`, + description: `Array of ${dataSourceHref} objects` + } + }, + createAlarmSourceFromSubscriptionInfo: { + description: 'Create alarm source from subscriptions info', + meta: 'function', + args: [ + {name: 'subscriptionInfo', type: subscriptionInfoHref, description: 'Subscription info'} + ], + return: { + type: `${dataSourceHref}`, + description: `${dataSourceHref} object` + } + }, + resolveAlias: { + description: 'Resolve alias', + meta: 'function', + args: [ + {name: 'entityAlias', type: entityAliasHref, description: 'Entity alias'}, + {name: 'stateParams', type: stateParamsHref, description: 'State params'}, + ], + return: observableReturnType(aliasInfoHref) + }, + resolveAliasFilter: { + description: 'Resolve alias filter', + meta: 'function', + args: [ + {name: 'filter', type: entityAliasFilterHref, description: 'Entity alias filter'}, + {name: 'stateParams', type: stateParamsHref, description: 'State params'}, + ], + return: observableReturnType(entityAliasFilterResultHref) + }, + checkEntityAlias: { + description: 'Check entity alias', + meta: 'function', + args: [ + {name: 'entityAlias', type: entityAliasHref, description: 'Entity alias'}, + ], + return: observableReturnTypeVariable('boolean') + }, + saveEntityParameters: { + description: 'Save entity parameters', + meta: 'function', + args: [ + {name: 'entityType', type: entityTypeHref, description: 'Entity type'}, + {name: 'entityData', type: importEntityDataHref, description: 'Entity data'}, + {name: 'update', type: 'boolean', description: 'Update'}, + requestConfigArg + ], + return: observableReturnType(importEntitiesResultInfoHref) + }, + saveEntityData: { + description: 'Save entity data', + meta: 'function', + args: [ + {name: 'entityId', type: entityIdHref, description: 'Id of the entity'}, + {name: 'entityData', type: importEntityDataHref, description: 'Entity data'}, + requestConfigArg + ], + return: observableReturnTypeVariable('any') + }, + } + }, + dialogs: { + description: 'Dialogs Service API
    ' + + 'See DialogService for API reference.', + meta: 'service', + type: 'DialogService', + children: { + confirm: { + description: 'Confirm', + meta: 'function', + args: [ + {name: 'title', type: 'string', description: 'Title'}, + {name: 'message', type: 'string', description: 'Message'}, + {name: 'cancel', type: 'string', optional: true, description: 'Cancel'}, + {name: 'ok', type: 'string', optional: true, description: 'Ok'}, + {name: 'fullscreen', type: 'boolean', optional: true, description: 'Fullscreen'}, + ], + return: observableReturnTypeVariable('boolean') + }, + alert: { + description: 'Alert', + meta: 'function', + args: [ + {name: 'title', type: 'string', description: 'Title'}, + {name: 'message', type: 'string', description: 'Message'}, + {name: 'ok', type: 'string', optional: true, description: 'Ok'}, + {name: 'fullscreen', type: 'boolean', optional: true, description: 'Fullscreen'}, + ], + return: observableReturnTypeVariable('boolean') + }, + colorPicker: { + description: 'Color picker', + meta: 'function', + args: [ + {name: 'color', type: 'string', description: 'Сolor'}, + ], + return: observableReturnTypeVariable('string') + }, + materialIconPicker: { + description: 'Material icon picker', + meta: 'function', + args: [ + {name: 'icon', type: 'string', description: 'Icon'}, + ], + return: observableReturnTypeVariable('string') + }, + forbidden: { + description: 'Forbidden', + meta: 'function', + args: [ + ], + return: observableReturnTypeVariable('boolean') + }, + todo: { + description: 'To do', + meta: 'function', + args: [ + ], + return: observableReturnTypeVariable('any') + }, + } + }, + customDialog: { + description: 'Custom Dialog Service API
    ' + + 'See CustomDialogService for API reference.', + meta: 'service', + type: 'CustomDialogService', + children: { + customDialog: { + description: 'Custom Dialog', + meta: 'function', + args: [ + {name: 'template', type: 'string', description: 'Template'}, + {name: 'controller', type: customDialogComponentHref, description: 'Controller'}, + {name: 'data', type: 'any', description: 'Data', optional: true}, + ], + return: observableReturnTypeVariable('any') + }, + } + }, + date: { + description: 'Date Pipe
    Formats a date value according to locale rules.
    ' + + 'See DatePipe for API reference.', + meta: 'service', + type: 'DatePipe' + }, + translate: { + description: 'Translate Service API
    ' + + 'See TranslateService for API reference.', + meta: 'service', + type: 'TranslateService' + }, + http: { + description: 'HTTP Client Service
    ' + + 'See HttpClient for API reference.', + meta: 'service', + type: 'HttpClient' + }, + sanitizer: { + description: 'DomSanitizer Service
    ' + + 'See DomSanitizer for API reference.', + meta: 'service', + type: 'DomSanitizer' + }, + router: { + description: 'Router Service
    ' + + 'See Router for API reference.', + meta: 'service', + type: 'Router' + } +}; diff --git a/ui-ngx/src/app/shared/models/ace/widget-completion.models.ts b/ui-ngx/src/app/shared/models/ace/widget-completion.models.ts new file mode 100644 index 0000000000..acdcc080ba --- /dev/null +++ b/ui-ngx/src/app/shared/models/ace/widget-completion.models.ts @@ -0,0 +1,660 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { TbEditorCompletion, TbEditorCompletions } from '@shared/models/ace/completion.models'; +import { entityIdHref, serviceCompletions } from '@shared/models/ace/service-completion.models'; + +export const timewindowCompletion: TbEditorCompletion = { + description: 'Timewindow configuration object', + meta: 'property', + type: 'Timewindow', + children: { + displayValue: { + description: 'Current timewindow display value.', + meta: 'property', + type: 'string' + }, + hideInterval: { + description: 'Whether to hide interval selection in timewindow panel.', + meta: 'property', + type: 'boolean' + }, + hideAggregation: { + description: 'Whether to hide aggregation selection in timewindow panel.', + meta: 'property', + type: 'boolean' + }, + hideAggInterval: { + description: 'Whether to hide aggregation interval selection in timewindow panel.', + meta: 'property', + type: 'boolean' + }, + selectedTab: { + description: 'Current selected timewindow type (0 - realtime, 1 - history).', + meta: 'property', + type: 'number' + }, + realtime: { + description: 'Realtime timewindow configuration object.', + meta: 'property', + type: 'IntervalWindow', + children: { + interval: { + description: 'Timewindow aggregation interval in milliseconds', + meta: 'property', + type: 'number' + }, + timewindowMs: { + description: 'Timewindow interval in milliseconds', + meta: 'property', + type: 'number' + } + } + }, + history: { + description: 'History timewindow configuration object.', + meta: 'property', + type: 'HistoryWindow', + children: { + historyType: { + description: 'History timewindow type (0 - last interval, 1 - fixed)', + meta: 'property', + type: 'number' + }, + interval: { + description: 'Timewindow aggregation interval in milliseconds', + meta: 'property', + type: 'number' + }, + timewindowMs: { + description: 'Timewindow interval in milliseconds', + meta: 'property', + type: 'number' + }, + fixedTimewindow: { + description: 'Fixed history timewindow configuration object', + meta: 'property', + type: 'FixedWindow', + children: { + startTimeMs: { + description: 'Timewindow start time in UTC milliseconds', + meta: 'property', + type: 'number' + }, + endTimeMs: { + description: 'Timewindow end time in UTC milliseconds', + meta: 'property', + type: 'number' + } + } + } + } + }, + aggregation: { + description: 'Timewindow aggregation configuration object.', + meta: 'property', + type: 'Aggregation', + children: { + interval: { + description: 'Aggregation interval in milliseconds', + meta: 'property', + type: 'number' + }, + type: { + description: 'Aggregation type', + meta: 'property', + type: 'AggregationType' + }, + limit: { + description: 'Maximum allowed datapoints when aggregation is disabled (AggregationType == \'NONE\')', + meta: 'property', + type: 'number' + } + } + } + } +} + +export const widgetContextCompletions: TbEditorCompletions = { + ctx: { + description: 'A reference to widget context that has all necessary API
    and data used by widget instance.', + meta: 'object', + type: 'WidgetContext', + children: { + ...{ + $container: { + description: 'Container element of the widget.
    Can be used to dynamically access or modify widget DOM using jQuery API.', + meta: 'property', + type: 'jQuery Object' + }, + $scope: { + description: 'Reference to the current widget component.
    Can be used to access/modify component properties when widget is built using Angular approach.', + meta: 'property', + type: 'IDynamicWidgetComponent' + }, + width: { + description: 'Current width of widget container in pixels.', + meta: 'property', + type: 'number' + }, + height: { + description: 'Current height of widget container in pixels.', + meta: 'property', + type: 'number' + }, + isEdit: { + description: 'Indicates whether the dashboard is in in the view or editing state.', + meta: 'property', + type: 'boolean' + }, + isMobile: { + description: 'Indicates whether the dashboard view is less then 960px width (default mobile breakpoint).', + meta: 'property', + type: 'boolean' + }, + widgetConfig: { + description: 'Common widget configuration containing properties such as color (text color), backgroundColor (widget background color), etc.', + meta: 'property', + type: 'WidgetConfig', + children: { + title: { + description: 'Widget title.', + meta: 'property', + type: 'string' + }, + titleIcon: { + description: 'Widget title icon.', + meta: 'property', + type: 'string' + }, + showTitle: { + description: 'Whether to show widget title.', + meta: 'property', + type: 'boolean' + }, + showTitleIcon: { + description: 'Whether to show widget title icon.', + meta: 'property', + type: 'boolean' + }, + iconColor: { + description: 'Widget title icon color.', + meta: 'property', + type: 'string' + }, + iconSize: { + description: 'Widget title icon size.', + meta: 'property', + type: 'string' + }, + titleTooltip: { + description: 'Widget title tooltip content.', + meta: 'property', + type: 'string' + }, + dropShadow: { + description: 'Enable/disable widget card shadow.', + meta: 'property', + type: 'boolean' + }, + enableFullscreen: { + description: 'Whether to enable fullscreen button on widget.', + meta: 'property', + type: 'boolean' + }, + useDashboardTimewindow: { + description: 'Whether to use dashboard timewindow (applicable for timeseries widgets).', + meta: 'property', + type: 'boolean' + }, + displayTimewindow: { + description: 'Whether to display timewindow (applicable for timeseries widgets).', + meta: 'property', + type: 'boolean' + }, + showLegend: { + description: 'Whether to show legend.', + meta: 'property', + type: 'boolean' + }, + legendConfig: { + description: 'Legend configuration.', + meta: 'property', + type: 'LegendConfig', + children: { + position: { + description: 'Legend position. Possible values: \'top\', \'bottom\', \'left\', \'right\'', + meta: 'property', + type: 'LegendPosition', + }, + direction: { + description: 'Legend direction. Possible values: \'column\', \'row\'', + meta: 'property', + type: 'LegendDirection', + }, + showMin: { + description: 'Whether to display aggregated min values.', + meta: 'property', + type: 'boolean', + }, + showMax: { + description: 'Whether to display aggregated max values.', + meta: 'property', + type: 'boolean', + }, + showAvg: { + description: 'Whether to display aggregated average values.', + meta: 'property', + type: 'boolean', + }, + showTotal: { + description: 'Whether to display aggregated total values.', + meta: 'property', + type: 'boolean', + } + } + }, + timewindow: timewindowCompletion, + mobileHeight: { + description: 'Widget height in mobile mode.', + meta: 'property', + type: 'number' + }, + mobileOrder: { + description: 'Widget order in mobile mode.', + meta: 'property', + type: 'number' + }, + color: { + description: 'Widget text color.', + meta: 'property', + type: 'string' + }, + backgroundColor: { + description: 'Widget background color.', + meta: 'property', + type: 'string' + }, + padding: { + description: 'Widget card padding.', + meta: 'property', + type: 'string' + }, + margin: { + description: 'Widget card margin.', + meta: 'property', + type: 'string' + }, + widgetStyle: { + description: 'Widget element style object.', + meta: 'property', + type: 'object' + }, + titleStyle: { + description: 'Widget title element style object.', + meta: 'property', + type: 'object' + }, + units: { + description: 'Optional property defining units text of values displayed by widget. Useful for simple widgets like cards or gauges.', + meta: 'property', + type: 'string' + }, + decimals: { + description: 'Optional property defining how many positions should be used to display decimal part of the value number.', + meta: 'property', + type: 'number' + }, + actions: { + description: 'Map of configured widget actions.', + meta: 'property', + type: 'object' + }, + settings: { + description: 'Object holding widget settings according to widget type.', + meta: 'property', + type: 'object' + }, + alarmSource: { + description: 'Configured alarm source for alarm widget type.', + meta: 'property', + type: 'Datasource' + }, + alarmSearchStatus: { + description: 'Configured default alarm search status for alarm widget type.', + meta: 'property', + type: 'AlarmSearchStatus' + }, + alarmsPollingInterval: { + description: 'Configured alarms polling interval for alarm widget type.', + meta: 'property', + type: 'number' + }, + alarmsMaxCountLoad: { + description: 'Configured maximum alarms to load for alarm widget type.', + meta: 'property', + type: 'number' + }, + alarmsFetchSize: { + description: 'Configured alarms page size used to load alarms.', + meta: 'property', + type: 'number' + }, + datasources: { + description: 'Array of configured widget datasources.', + meta: 'property', + type: 'Array<Datasource>' + } + } + }, + settings: { + description: 'Widget settings containing widget specific properties according to the defined settings json schema', + meta: 'property', + type: 'object' + }, + datasources: { + description: 'Array of resolved widget datasources.', + meta: 'property', + type: 'Array<Datasource>' + }, + data: { + description: 'Array of latest datasources data.', + meta: 'property', + type: 'Array<DatasourceData>' + }, + timeWindow: { + description: 'Current widget timewindow (applicable for timeseries widgets).', + meta: 'property', + type: 'WidgetTimewindow' + }, + units: { + description: 'Optional property defining units text of values displayed by widget. Useful for simple widgets like cards or gauges.', + meta: 'property', + type: 'string' + }, + decimals: { + description: 'Optional property defining how many positions should be used to display decimal part of the value number.', + meta: 'property', + type: 'number' + }, + hideTitlePanel: { + description: 'Manages visibility of widget title panel. Useful for widget with custom title panels or different states. updateWidgetParams() function must be called after this property change.', + meta: 'property', + type: 'boolean' + }, + widgetTitle: { + description: 'If set, will override configured widget title text. updateWidgetParams() function must be called after this property change.', + meta: 'property', + type: 'string' + }, + detectChanges: { + description: 'Trigger change detection for current widget. Must be invoked when widget HTML template bindings should be updated due to widget data changes.', + meta: 'function' + }, + updateWidgetParams: { + description: 'Updates widget with runtime set properties such as widgetTitle, hideTitlePanel, etc. Must be invoked in order these properties changes take effect.', + meta: 'function' + }, + defaultSubscription: { + description: 'Default widget subscription object contains all subscription information,
    including current data, according to the widget type.', + meta: 'property', + type: 'IWidgetSubscription' + }, + timewindowFunctions: { + description: 'Object with timewindow functions used to manage widget data time frame. Can by used by Time-series or Alarm widgets.', + meta: 'property', + type: 'TimewindowFunctions', + children: { + onUpdateTimewindow: { + description: 'This function can be used to update current subscription time frame
    to historical one identified by startTimeMs and endTimeMs arguments.', + meta: 'function', + args: [ + { + name: 'startTimeMs', + description: 'Timewindow start time in UTC milliseconds', + type: 'number' + }, + { + name: 'endTimeMs', + description: 'Timewindow end time in UTC milliseconds', + type: 'number' + } + ] + }, + onResetTimewindow: { + description: 'Resets subscription time frame to default defined by widget timewindow component
    or dashboard timewindow depending on widget settings.', + meta: 'function' + } + } + }, + controlApi: { + description: 'Object that provides API functions for RPC (Control) widgets.', + meta: 'property', + type: 'RpcApi', + children: { + sendOneWayCommand: { + description: 'Sends one way (without response) RPC command to the device.', + meta: 'function', + args: [ + { + name: 'method', + description: 'RPC method name', + type: 'string' + }, + { + name: 'params', + description: 'RPC method params, custom json object', + type: 'object', + optional: true + }, + { + name: 'timeout', + description: 'Maximum delay in milliseconds to wait until response/acknowledgement is received.', + type: 'number', + optional: true + } + ], + return: { + description: 'A command execution Observable.', + type: 'Observable<any>' + } + }, + sendTwoWayCommand: { + description: 'Sends two way (with response) RPC command to the device.', + meta: 'function', + args: [ + { + name: 'method', + description: 'RPC method name', + type: 'string' + }, + { + name: 'params', + description: 'RPC method params, custom json object', + type: 'object', + optional: true + }, + { + name: 'timeout', + description: 'Maximum delay in milliseconds to wait until response/acknowledgement is received.', + type: 'number', + optional: true + } + ], + return: { + description: 'A command execution Observable of response body.', + type: 'Observable<any>' + } + } + } + }, + actionsApi: { + description: 'Set of API functions to work with user defined actions.', + meta: 'property', + type: 'WidgetActionsApi', + children: { + getActionDescriptors: { + description: 'Get list of action descriptors for provided actionSourceId.', + meta: 'function', + args: [ + { + name: 'actionSourceId', + description: 'Id of widget action source', + type: 'string' + } + ], + return: { + description: 'The list of action descriptors', + type: 'Array<WidgetActionDescriptor>' + } + }, + handleWidgetAction: { + description: 'Handle action produced by particular action source.', + meta: 'function', + args: [ + { + name: '$event', + description: 'DOM event object associated with action.', + type: 'Event' + }, + { + name: 'descriptor', + description: 'An action descriptor.', + type: 'WidgetActionDescriptor' + }, + { + name: 'entityId', + description: 'Current entity id provided by action source if available.', + type: entityIdHref, + optional: true + }, + { + name: 'entityName', + description: 'Current entity name provided by action source if available.', + type: 'string', + optional: true + } + ] + } + } + }, + stateController: { + description: 'Reference to Dashboard state controller, providing API to manage current dashboard state.', + meta: 'property', + type: 'IStateController', + children: { + openState: { + description: 'Navigate to new dashboard state.', + meta: 'function', + args: [ + { + name: 'id', + description: 'An id of the target dashboard state.', + type: 'string' + }, + { + name: 'params', + description: 'An object with state parameters to use by the new state.', + type: 'StateParams', + optional: true + }, + { + name: 'openRightLayout', + description: 'An optional boolean argument to force open right dashboard layout if present in mobile view mode.', + type: 'boolean', + optional: true + } + ] + }, + pushAndOpenState: { + description: 'Navigate to new dashboard state and adding intermediate states.', + meta: 'function', + args: [ + { + name: 'id', + description: 'An array state object of the target dashboard state.', + type: 'Array StateObject', + }, + { + name: 'openRightLayout', + description: 'An optional boolean argument to force open right dashboard layout if present in mobile view mode.', + type: 'boolean', + optional: true + } + ] + }, + updateState: { + description: 'Updates current dashboard state.', + meta: 'function', + args: [ + { + name: 'id', + description: 'An optional id of the target dashboard state to replace current state id.', + type: 'string', + optional: true + }, + { + name: 'params', + description: 'An object with state parameters to update current state parameters.', + type: 'StateParams', + optional: true + }, + { + name: 'openRightLayout', + description: 'An optional boolean argument to force open right dashboard layout if present in mobile view mode.', + type: 'boolean', + optional: true + } + ] + }, + getStateId: { + description: 'Get current dashboard state id.', + meta: 'function', + return: { + description: 'current dashboard state id.', + type: 'string' + } + }, + getStateParams: { + description: 'Get current dashboard state parameters.', + meta: 'function', + return: { + description: 'current dashboard state parameters.', + type: 'StateParams' + } + }, + getStateParamsByStateId: { + description: 'Get state parameters for particular dashboard state identified by id.', + meta: 'function', + args: [ + { + name: 'id', + description: 'An id of the target dashboard state.', + type: 'string' + } + ], + return: { + description: 'current dashboard state parameters.', + type: 'StateParams' + } + } + } + } + }, + ...serviceCompletions + } + } +} diff --git a/ui-ngx/src/app/shared/models/alarm.models.ts b/ui-ngx/src/app/shared/models/alarm.models.ts new file mode 100644 index 0000000000..8f0741206d --- /dev/null +++ b/ui-ngx/src/app/shared/models/alarm.models.ts @@ -0,0 +1,234 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { BaseData } from '@shared/models/base-data'; +import { TenantId } from '@shared/models/id/tenant-id'; +import { AlarmId } from '@shared/models/id/alarm-id'; +import { EntityId } from '@shared/models/id/entity-id'; +import { TimePageLink } from '@shared/models/page/page-link'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { EntityType } from '@shared/models/entity-type.models'; +import { isString } from '@core/utils'; + +export enum AlarmSeverity { + CRITICAL = 'CRITICAL', + MAJOR = 'MAJOR', + MINOR = 'MINOR', + WARNING = 'WARNING', + INDETERMINATE = 'INDETERMINATE' +} + +export enum AlarmStatus { + ACTIVE_UNACK = 'ACTIVE_UNACK', + ACTIVE_ACK = 'ACTIVE_ACK', + CLEARED_UNACK = 'CLEARED_UNACK', + CLEARED_ACK = 'CLEARED_ACK' +} + +export enum AlarmSearchStatus { + ANY = 'ANY', + ACTIVE = 'ACTIVE', + CLEARED = 'CLEARED', + ACK = 'ACK', + UNACK = 'UNACK' +} + +export const alarmSeverityTranslations = new Map( + [ + [AlarmSeverity.CRITICAL, 'alarm.severity-critical'], + [AlarmSeverity.MAJOR, 'alarm.severity-major'], + [AlarmSeverity.MINOR, 'alarm.severity-minor'], + [AlarmSeverity.WARNING, 'alarm.severity-warning'], + [AlarmSeverity.INDETERMINATE, 'alarm.severity-indeterminate'] + ] +); + +export const alarmStatusTranslations = new Map( + [ + [AlarmStatus.ACTIVE_UNACK, 'alarm.display-status.ACTIVE_UNACK'], + [AlarmStatus.ACTIVE_ACK, 'alarm.display-status.ACTIVE_ACK'], + [AlarmStatus.CLEARED_UNACK, 'alarm.display-status.CLEARED_UNACK'], + [AlarmStatus.CLEARED_ACK, 'alarm.display-status.CLEARED_ACK'], + ] +); + +export const alarmSearchStatusTranslations = new Map( + [ + [AlarmSearchStatus.ANY, 'alarm.search-status.ANY'], + [AlarmSearchStatus.ACTIVE, 'alarm.search-status.ACTIVE'], + [AlarmSearchStatus.CLEARED, 'alarm.search-status.CLEARED'], + [AlarmSearchStatus.ACK, 'alarm.search-status.ACK'], + [AlarmSearchStatus.UNACK, 'alarm.search-status.UNACK'] + ] +); + +export const alarmSeverityColors = new Map( + [ + [AlarmSeverity.CRITICAL, 'red'], + [AlarmSeverity.MAJOR, 'orange'], + [AlarmSeverity.MINOR, '#ffca3d'], + [AlarmSeverity.WARNING, '#abab00'], + [AlarmSeverity.INDETERMINATE, 'green'] + ] +); + +export interface Alarm extends BaseData { + tenantId: TenantId; + type: string; + originator: EntityId; + severity: AlarmSeverity; + status: AlarmStatus; + startTs: number; + endTs: number; + ackTs: number; + clearTs: number; + propagate: boolean; + details?: any; +} + +export interface AlarmInfo extends Alarm { + originatorName: string; +} + +export interface AlarmDataInfo extends AlarmInfo { + [key: string]: any; +} + +export const simulatedAlarm: AlarmInfo = { + id: new AlarmId(NULL_UUID), + tenantId: new TenantId(NULL_UUID), + createdTime: new Date().getTime(), + startTs: new Date().getTime(), + endTs: 0, + ackTs: 0, + clearTs: 0, + originatorName: 'Simulated', + originator: { + entityType: EntityType.DEVICE, + id: '1' + }, + type: 'TEMPERATURE', + severity: AlarmSeverity.MAJOR, + status: AlarmStatus.ACTIVE_UNACK, + details: { + message: 'Temperature is high!' + }, + propagate: false +}; + +export interface AlarmField { + keyName: string; + value: string; + name: string; + time?: boolean; +} + +export const alarmFields: {[fieldName: string]: AlarmField} = { + createdTime: { + keyName: 'createdTime', + value: 'createdTime', + name: 'alarm.created-time', + time: true + }, + startTime: { + keyName: 'startTime', + value: 'startTs', + name: 'alarm.start-time', + time: true + }, + endTime: { + keyName: 'endTime', + value: 'endTs', + name: 'alarm.end-time', + time: true + }, + ackTime: { + keyName: 'ackTime', + value: 'ackTs', + name: 'alarm.ack-time', + time: true + }, + clearTime: { + keyName: 'clearTime', + value: 'clearTs', + name: 'alarm.clear-time', + time: true + }, + originator: { + keyName: 'originator', + value: 'originatorName', + name: 'alarm.originator' + }, + originatorType: { + keyName: 'originatorType', + value: 'originator.entityType', + name: 'alarm.originator-type' + }, + type: { + keyName: 'type', + value: 'type', + name: 'alarm.type' + }, + severity: { + keyName: 'severity', + value: 'severity', + name: 'alarm.severity' + }, + status: { + keyName: 'status', + value: 'status', + name: 'alarm.status' + } +}; + +export class AlarmQuery { + + affectedEntityId: EntityId; + pageLink: TimePageLink; + searchStatus: AlarmSearchStatus; + status: AlarmStatus; + fetchOriginator: boolean; + offset: string; + + constructor(entityId: EntityId, pageLink: TimePageLink, + searchStatus: AlarmSearchStatus, status: AlarmStatus, + fetchOriginator: boolean, offset: string) { + this.affectedEntityId = entityId; + this.pageLink = pageLink; + this.searchStatus = searchStatus; + this.status = status; + this.fetchOriginator = fetchOriginator; + this.offset = offset; + } + + public toQuery(): string { + let query = `/${this.affectedEntityId.entityType}/${this.affectedEntityId.id}`; + query += this.pageLink.toQuery(); + if (this.searchStatus) { + query += `&searchStatus=${this.searchStatus}`; + } else if (this.status) { + query += `&status=${this.status}`; + } + if (typeof this.fetchOriginator !== 'undefined' && this.fetchOriginator !== null) { + query += `&fetchOriginator=${this.fetchOriginator}`; + } + if (isString(this.offset) && this.offset.length) { + query += `&offset=${this.offset}`; + } + return query; + } + +} diff --git a/ui-ngx/src/app/shared/models/alias.models.ts b/ui-ngx/src/app/shared/models/alias.models.ts new file mode 100644 index 0000000000..f437931f27 --- /dev/null +++ b/ui-ngx/src/app/shared/models/alias.models.ts @@ -0,0 +1,169 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityType } from '@shared/models/entity-type.models'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntitySearchDirection, EntityTypeFilter } from '@shared/models/relation.models'; +import { EntityInfo } from './entity.models'; +import { EntityFilter } from '@shared/models/query/query.models'; + +export enum AliasFilterType { + singleEntity = 'singleEntity', + entityList = 'entityList', + entityName = 'entityName', + stateEntity = 'stateEntity', + assetType = 'assetType', + deviceType = 'deviceType', + entityViewType = 'entityViewType', + apiUsageState = 'apiUsageState', + relationsQuery = 'relationsQuery', + assetSearchQuery = 'assetSearchQuery', + deviceSearchQuery = 'deviceSearchQuery', + entityViewSearchQuery = 'entityViewSearchQuery' +} + +export const aliasFilterTypeTranslationMap = new Map( + [ + [ AliasFilterType.singleEntity, 'alias.filter-type-single-entity' ], + [ AliasFilterType.entityList, 'alias.filter-type-entity-list' ], + [ AliasFilterType.entityName, 'alias.filter-type-entity-name' ], + [ AliasFilterType.stateEntity, 'alias.filter-type-state-entity' ], + [ AliasFilterType.assetType, 'alias.filter-type-asset-type' ], + [ AliasFilterType.deviceType, 'alias.filter-type-device-type' ], + [ AliasFilterType.entityViewType, 'alias.filter-type-entity-view-type' ], + [ AliasFilterType.apiUsageState, 'alias.filter-type-apiUsageState' ], + [ AliasFilterType.relationsQuery, 'alias.filter-type-relations-query' ], + [ AliasFilterType.assetSearchQuery, 'alias.filter-type-asset-search-query' ], + [ AliasFilterType.deviceSearchQuery, 'alias.filter-type-device-search-query' ], + [ AliasFilterType.entityViewSearchQuery, 'alias.filter-type-entity-view-search-query' ] + ] +); + +export interface SingleEntityFilter { + singleEntity?: EntityId; +} + +export interface EntityListFilter { + entityType?: EntityType; + entityList?: string[]; +} + +export interface EntityNameFilter { + entityType?: EntityType; + entityNameFilter?: string; +} + +export interface StateEntityFilter { + stateEntityParamName?: string; + defaultStateEntity?: EntityId; +} + +export interface AssetTypeFilter { + assetType?: string; + assetNameFilter?: string; +} + +export interface DeviceTypeFilter { + deviceType?: string; + deviceNameFilter?: string; +} + +export interface EntityViewFilter { + entityViewType?: string; + entityViewNameFilter?: string; +} + +export interface RelationsQueryFilter { + rootStateEntity?: boolean; + stateEntityParamName?: string; + defaultStateEntity?: EntityId; + rootEntity?: EntityId; + direction?: EntitySearchDirection; + filters?: Array; + maxLevel?: number; + fetchLastLevelOnly?: boolean; +} + +export interface EntitySearchQueryFilter { + rootStateEntity?: boolean; + stateEntityParamName?: string; + defaultStateEntity?: EntityId; + rootEntity?: EntityId; + relationType?: string; + direction?: EntitySearchDirection; + maxLevel?: number; + fetchLastLevelOnly?: boolean; +} + +export interface ApiUsageStateFilter { + +} + +export interface AssetSearchQueryFilter extends EntitySearchQueryFilter { + assetTypes?: string[]; +} + +export interface DeviceSearchQueryFilter extends EntitySearchQueryFilter { + deviceTypes?: string[]; +} + +export interface EntityViewSearchQueryFilter extends EntitySearchQueryFilter { + entityViewTypes?: string[]; +} + +export type EntityFilters = + SingleEntityFilter & + EntityListFilter & + EntityNameFilter & + StateEntityFilter & + AssetTypeFilter & + DeviceTypeFilter & + EntityViewFilter & + RelationsQueryFilter & + AssetSearchQueryFilter & + DeviceSearchQueryFilter & + EntityViewSearchQueryFilter & + EntitySearchQueryFilter; + +export interface EntityAliasFilter extends EntityFilters { + type?: AliasFilterType; + resolveMultiple?: boolean; +} + +export interface EntityAliasInfo { + alias: string; + filter: EntityAliasFilter; + [key: string]: any; +} + +export interface AliasesInfo { + datasourceAliases: {[datasourceIndex: number]: EntityAliasInfo}; + targetDeviceAliases: {[targetDeviceAliasIndex: number]: EntityAliasInfo}; +} + +export interface EntityAlias extends EntityAliasInfo { + id: string; +} + +export interface EntityAliases { + [id: string]: EntityAlias; +} + +export interface EntityAliasFilterResult { + stateEntity: boolean; + entityFilter: EntityFilter; + entityParamName?: string; +} diff --git a/ui-ngx/src/app/shared/models/asset.models.ts b/ui-ngx/src/app/shared/models/asset.models.ts new file mode 100644 index 0000000000..ab952d26a5 --- /dev/null +++ b/ui-ngx/src/app/shared/models/asset.models.ts @@ -0,0 +1,39 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { BaseData } from '@shared/models/base-data'; +import { AssetId } from './id/asset-id'; +import { TenantId } from '@shared/models/id/tenant-id'; +import { CustomerId } from '@shared/models/id/customer-id'; +import { EntitySearchQuery } from '@shared/models/relation.models'; + +export interface Asset extends BaseData { + tenantId?: TenantId; + customerId?: CustomerId; + name: string; + type: string; + label: string; + additionalInfo?: any; +} + +export interface AssetInfo extends Asset { + customerTitle: string; + customerIsPublic: boolean; +} + +export interface AssetSearchQuery extends EntitySearchQuery { + assetTypes: Array; +} diff --git a/ui-ngx/src/app/shared/models/audit-log.models.ts b/ui-ngx/src/app/shared/models/audit-log.models.ts new file mode 100644 index 0000000000..1b450d5568 --- /dev/null +++ b/ui-ngx/src/app/shared/models/audit-log.models.ts @@ -0,0 +1,108 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { BaseData } from './base-data'; +import { AuditLogId } from './id/audit-log-id'; +import { CustomerId } from './id/customer-id'; +import { EntityId } from './id/entity-id'; +import { UserId } from './id/user-id'; +import { TenantId } from './id/tenant-id'; + +export enum AuditLogMode { + TENANT, + ENTITY, + USER, + CUSTOMER +} + +export enum ActionType { + ADDED = 'ADDED', + DELETED = 'DELETED', + UPDATED = 'UPDATED', + ATTRIBUTES_UPDATED = 'ATTRIBUTES_UPDATED', + ATTRIBUTES_DELETED = 'ATTRIBUTES_DELETED', + RPC_CALL = 'RPC_CALL', + CREDENTIALS_UPDATED = 'CREDENTIALS_UPDATED', + ASSIGNED_TO_CUSTOMER = 'ASSIGNED_TO_CUSTOMER', + UNASSIGNED_FROM_CUSTOMER = 'UNASSIGNED_FROM_CUSTOMER', + ACTIVATED = 'ACTIVATED', + SUSPENDED = 'SUSPENDED', + CREDENTIALS_READ = 'CREDENTIALS_READ', + ATTRIBUTES_READ = 'ATTRIBUTES_READ', + RELATION_ADD_OR_UPDATE = 'RELATION_ADD_OR_UPDATE', + RELATION_DELETED = 'RELATION_DELETED', + RELATIONS_DELETED = 'RELATIONS_DELETED', + ALARM_ACK = 'ALARM_ACK', + ALARM_CLEAR = 'ALARM_CLEAR', + LOGIN = 'LOGIN', + LOGOUT = 'LOGOUT', + LOCKOUT = 'LOCKOUT', + ASSIGNED_FROM_TENANT = 'ASSIGNED_FROM_TENANT', + ASSIGNED_TO_TENANT = 'ASSIGNED_TO_TENANT' +} + +export enum ActionStatus { + SUCCESS = 'SUCCESS', + FAILURE = 'FAILURE' +} + +export const actionTypeTranslations = new Map( + [ + [ActionType.ADDED, 'audit-log.type-added'], + [ActionType.DELETED, 'audit-log.type-deleted'], + [ActionType.UPDATED, 'audit-log.type-updated'], + [ActionType.ATTRIBUTES_UPDATED, 'audit-log.type-attributes-updated'], + [ActionType.ATTRIBUTES_DELETED, 'audit-log.type-attributes-deleted'], + [ActionType.RPC_CALL, 'audit-log.type-rpc-call'], + [ActionType.CREDENTIALS_UPDATED, 'audit-log.type-credentials-updated'], + [ActionType.ASSIGNED_TO_CUSTOMER, 'audit-log.type-assigned-to-customer'], + [ActionType.UNASSIGNED_FROM_CUSTOMER, 'audit-log.type-unassigned-from-customer'], + [ActionType.ACTIVATED, 'audit-log.type-activated'], + [ActionType.SUSPENDED, 'audit-log.type-suspended'], + [ActionType.CREDENTIALS_READ, 'audit-log.type-credentials-read'], + [ActionType.ATTRIBUTES_READ, 'audit-log.type-attributes-read'], + [ActionType.RELATION_ADD_OR_UPDATE, 'audit-log.type-relation-add-or-update'], + [ActionType.RELATION_DELETED, 'audit-log.type-relation-delete'], + [ActionType.RELATIONS_DELETED, 'audit-log.type-relations-delete'], + [ActionType.ALARM_ACK, 'audit-log.type-alarm-ack'], + [ActionType.ALARM_CLEAR, 'audit-log.type-alarm-clear'], + [ActionType.LOGIN, 'audit-log.type-login'], + [ActionType.LOGOUT, 'audit-log.type-logout'], + [ActionType.LOCKOUT, 'audit-log.type-lockout'], + [ActionType.ASSIGNED_FROM_TENANT, 'audit-log.type-assigned-from-tenant'], + [ActionType.ASSIGNED_TO_TENANT, 'audit-log.type-assigned-to-tenant'] + ] +); + +export const actionStatusTranslations = new Map( + [ + [ActionStatus.SUCCESS, 'audit-log.status-success'], + [ActionStatus.FAILURE, 'audit-log.status-failure'], + ] +); + +export interface AuditLog extends BaseData { + tenantId: TenantId; + customerId: CustomerId; + entityId: EntityId; + entityName: string; + userId: UserId; + userName: string; + actionType: ActionType; + actionData: any; + actionStatus: ActionStatus; + actionFailureDetails: string; +} diff --git a/ui-ngx/src/app/shared/models/authority.enum.ts b/ui-ngx/src/app/shared/models/authority.enum.ts new file mode 100644 index 0000000000..0a6302a50b --- /dev/null +++ b/ui-ngx/src/app/shared/models/authority.enum.ts @@ -0,0 +1,23 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export enum Authority { + SYS_ADMIN = 'SYS_ADMIN', + TENANT_ADMIN = 'TENANT_ADMIN', + CUSTOMER_USER = 'CUSTOMER_USER', + REFRESH_TOKEN = 'REFRESH_TOKEN', + ANONYMOUS = 'ANONYMOUS' +} diff --git a/ui-ngx/src/app/shared/models/base-data.ts b/ui-ngx/src/app/shared/models/base-data.ts new file mode 100644 index 0000000000..8fb76e1584 --- /dev/null +++ b/ui-ngx/src/app/shared/models/base-data.ts @@ -0,0 +1,27 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from '@shared/models/id/entity-id'; +import { HasUUID } from '@shared/models/id/has-uuid'; + +export declare type HasId = EntityId | HasUUID; + +export interface BaseData { + createdTime?: number; + id?: T; + name?: string; + label?: string; +} diff --git a/ui-ngx/src/app/shared/models/component-descriptor.models.ts b/ui-ngx/src/app/shared/models/component-descriptor.models.ts new file mode 100644 index 0000000000..13627f2264 --- /dev/null +++ b/ui-ngx/src/app/shared/models/component-descriptor.models.ts @@ -0,0 +1,39 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { RuleNodeType } from '@shared/models/rule-node.models'; + +export enum ComponentType { + ENRICHMENT = 'ENRICHMENT', + FILTER = 'FILTER', + TRANSFORMATION = 'TRANSFORMATION', + ACTION = 'ACTION', + EXTERNAL = 'EXTERNAL' +} + +export enum ComponentScope { + SYSTEM = 'SYSTEM', + TENANT = 'TENANT' +} + +export interface ComponentDescriptor { + type: ComponentType | RuleNodeType; + scope?: ComponentScope; + name: string; + clazz: string; + configurationDescriptor?: any; + actions?: string; +} diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts new file mode 100644 index 0000000000..71dcb16a37 --- /dev/null +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -0,0 +1,223 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { InjectionToken } from '@angular/core'; + +export const Constants = { + serverErrorCode: { + general: 2, + authentication: 10, + jwtTokenExpired: 11, + tenantTrialExpired: 12, + credentialsExpired: 15, + permissionDenied: 20, + invalidArguments: 30, + badRequestParams: 31, + itemNotFound: 32, + tooManyRequests: 33, + tooManyUpdates: 34 + }, + entryPoints: { + login: '/api/auth/login', + tokenRefresh: '/api/auth/token', + nonTokenBased: '/api/noauth' + } +}; + +export const MediaBreakpoints = { + xs: 'screen and (max-width: 599px)', + sm: 'screen and (min-width: 600px) and (max-width: 959px)', + md: 'screen and (min-width: 960px) and (max-width: 1279px)', + lg: 'screen and (min-width: 1280px) and (max-width: 1919px)', + xl: 'screen and (min-width: 1920px) and (max-width: 5000px)', + 'lt-sm': 'screen and (max-width: 599px)', + 'lt-md': 'screen and (max-width: 959px)', + 'lt-lg': 'screen and (max-width: 1279px)', + 'lt-xl': 'screen and (max-width: 1919px)', + 'gt-xs': 'screen and (min-width: 600px)', + 'gt-sm': 'screen and (min-width: 960px)', + 'gt-md': 'screen and (min-width: 1280px)', + 'gt-lg': 'screen and (min-width: 1920px)', + 'gt-xl': 'screen and (min-width: 5001px)' +}; + +const helpBaseUrl = 'https://thingsboard.io'; + +export const HelpLinks = { + linksMap: { + outgoingMailSettings: helpBaseUrl + '/docs/user-guide/ui/mail-settings', + securitySettings: helpBaseUrl + '/docs/user-guide/ui/security-settings', + oauth2Settings: helpBaseUrl + '/docs/user-guide/oauth-2-support/', + ruleEngine: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/overview/', + ruleNodeCheckRelation: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#check-relation-filter-node', + ruleNodeCheckExistenceFields: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#check-existence-fields-node', + ruleNodeGpsGeofencingFilter: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#gps-geofencing-filter-node', + ruleNodeJsFilter: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#script-filter-node', + ruleNodeJsSwitch: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#switch-node', + ruleNodeMessageTypeFilter: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#message-type-filter-node', + ruleNodeMessageTypeSwitch: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#message-type-switch-node', + ruleNodeOriginatorTypeFilter: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#originator-type-filter-node', + ruleNodeOriginatorTypeSwitch: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#originator-type-switch-node', + ruleNodeOriginatorAttributes: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/enrichment-nodes/#originator-attributes', + ruleNodeOriginatorFields: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/enrichment-nodes/#originator-fields', + ruleNodeOriginatorTelemetry: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/enrichment-nodes/#originator-telemetry', + ruleNodeCustomerAttributes: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/enrichment-nodes/#customer-attributes', + ruleNodeCustomerDetails: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/enrichment-nodes/#customer-details', + ruleNodeDeviceAttributes: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/enrichment-nodes/#device-attributes', + ruleNodeRelatedAttributes: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/enrichment-nodes/#related-attributes', + ruleNodeTenantAttributes: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/enrichment-nodes/#tenant-attributes', + ruleNodeTenantDetails: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/enrichment-nodes/#tenant-details', + ruleNodeChangeOriginator: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/transformation-nodes/#change-originator', + ruleNodeTransformMsg: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/transformation-nodes/#script-transformation-node', + ruleNodeMsgToEmail: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/transformation-nodes/#to-email-node', + ruleNodeAssignToCustomer: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/transformation-nodes/#assign-to-customer-node', + ruleNodeUnassignFromCustomer: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/transformation-nodes/#unassign-from-customer-node', + ruleNodeClearAlarm: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#clear-alarm-node', + ruleNodeCreateAlarm: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#create-alarm-node', + ruleNodeCreateRelation: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#create-relation-node', + ruleNodeDeleteRelation: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#delete-relation-node', + ruleNodeMsgDelay: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#delay-node', + ruleNodeMsgGenerator: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#generator-node', + ruleNodeGpsGeofencingEvents: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#gps-geofencing-events-node', + ruleNodeLog: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#log-node', + ruleNodeRpcCallReply: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#rpc-call-reply-node', + ruleNodeRpcCallRequest: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#rpc-call-request-node', + ruleNodeSaveAttributes: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#save-attributes-node', + ruleNodeSaveTimeseries: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#save-timeseries-node', + ruleNodeSaveToCustomTable: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#save-to-custom-table', + ruleNodeRuleChain: helpBaseUrl + '/docs/user-guide/ui/rule-chains/', + ruleNodeAwsSns: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/external-nodes/#aws-sns-node', + ruleNodeAwsSqs: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/external-nodes/#aws-sqs-node', + ruleNodeKafka: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/external-nodes/#kafka-node', + ruleNodeMqtt: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/external-nodes/#mqtt-node', + ruleNodeAzureIotHub: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/external-nodes/#azure-iot-hub-node', + ruleNodeRabbitMq: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/external-nodes/#rabbitmq-node', + ruleNodeRestApiCall: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/external-nodes/#rest-api-call-node', + ruleNodeSendEmail: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/external-nodes/#send-email-node', + tenants: helpBaseUrl + '/docs/user-guide/ui/tenants', + tenantProfiles: helpBaseUrl + '/docs/user-guide/ui/tenant-profiles', + customers: helpBaseUrl + '/docs/user-guide/ui/customers', + users: helpBaseUrl + '/docs/user-guide/ui/users', + devices: helpBaseUrl + '/docs/user-guide/ui/devices', + deviceProfiles: helpBaseUrl + '/docs/user-guide/ui/device-profiles', + assets: helpBaseUrl + '/docs/user-guide/ui/assets', + entityViews: helpBaseUrl + '/docs/user-guide/ui/entity-views', + entitiesImport: helpBaseUrl + '/docs/user-guide/bulk-provisioning', + rulechains: helpBaseUrl + '/docs/user-guide/ui/rule-chains', + dashboards: helpBaseUrl + '/docs/user-guide/ui/dashboards', + widgetsBundles: helpBaseUrl + '/docs/user-guide/ui/widget-library#bundles', + widgetsConfig: helpBaseUrl + '/docs/user-guide/ui/dashboards#widget-configuration', + widgetsConfigTimeseries: helpBaseUrl + '/docs/user-guide/ui/dashboards#timeseries', + widgetsConfigLatest: helpBaseUrl + '/docs/user-guide/ui/dashboards#latest', + widgetsConfigRpc: helpBaseUrl + '/docs/user-guide/ui/dashboards#rpc', + widgetsConfigAlarm: helpBaseUrl + '/docs/user-guide/ui/dashboards#alarm', + widgetsConfigStatic: helpBaseUrl + '/docs/user-guide/ui/dashboards#static', + } +}; + +export interface ValueTypeData { + name: string; + icon: string; +} + +export enum ValueType { + STRING = 'STRING', + INTEGER = 'INTEGER', + DOUBLE = 'DOUBLE', + BOOLEAN = 'BOOLEAN', + JSON = 'JSON' +} + +export const valueTypesMap = new Map( + [ + [ + ValueType.STRING, + { + name: 'value.string', + icon: 'mdi:format-text' + } + ], + [ + ValueType.INTEGER, + { + name: 'value.integer', + icon: 'mdi:numeric' + } + ], + [ + ValueType.DOUBLE, + { + name: 'value.double', + icon: 'mdi:numeric' + } + ], + [ + ValueType.BOOLEAN, + { + name: 'value.boolean', + icon: 'mdi:checkbox-marked-outline' + } + ], + [ + ValueType.JSON, + { + name: 'value.json', + icon: 'mdi:json' + } + ] + ] +); + +export interface ContentTypeData { + name: string; + code: string; +} + +export enum ContentType { + JSON = 'JSON', + TEXT = 'TEXT', + BINARY = 'BINARY' +} + +export const contentTypesMap = new Map( + [ + [ + ContentType.JSON, + { + name: 'content-type.json', + code: 'json' + } + ], + [ + ContentType.TEXT, + { + name: 'content-type.text', + code: 'text' + } + ], + [ + ContentType.BINARY, + { + name: 'content-type.binary', + code: 'text' + } + ] + ] +); + +export const customTranslationsPrefix = 'custom.'; + +export const MODULES_MAP = new InjectionToken<{[key: string]: any}>('ModulesMap'); diff --git a/ui-ngx/src/app/shared/models/contact-based.model.ts b/ui-ngx/src/app/shared/models/contact-based.model.ts new file mode 100644 index 0000000000..e9f4038a3d --- /dev/null +++ b/ui-ngx/src/app/shared/models/contact-based.model.ts @@ -0,0 +1,28 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { BaseData, HasId } from './base-data'; + +export interface ContactBased extends BaseData { + country: string; + state: string; + city: string; + address: string; + address2: string; + zip: string; + phone: string; + email: string; +} diff --git a/ui-ngx/src/app/shared/models/customer.model.ts b/ui-ngx/src/app/shared/models/customer.model.ts new file mode 100644 index 0000000000..f33ea0ff3b --- /dev/null +++ b/ui-ngx/src/app/shared/models/customer.model.ts @@ -0,0 +1,31 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { CustomerId } from '@shared/models/id/customer-id'; +import { ContactBased } from '@shared/models/contact-based.model'; +import { TenantId } from './id/tenant-id'; + +export interface Customer extends ContactBased { + tenantId: TenantId; + title: string; + additionalInfo?: any; +} + +export interface ShortCustomerInfo { + customerId: CustomerId; + title: string; + public: boolean; +} diff --git a/ui-ngx/src/app/shared/models/dashboard.models.ts b/ui-ngx/src/app/shared/models/dashboard.models.ts new file mode 100644 index 0000000000..7bdf229c07 --- /dev/null +++ b/ui-ngx/src/app/shared/models/dashboard.models.ts @@ -0,0 +1,137 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { BaseData } from '@shared/models/base-data'; +import { DashboardId } from '@shared/models/id/dashboard-id'; +import { TenantId } from '@shared/models/id/tenant-id'; +import { ShortCustomerInfo } from '@shared/models/customer.model'; +import { Widget } from './widget.models'; +import { Timewindow } from '@shared/models/time/time.models'; +import { EntityAliases } from './alias.models'; +import { Filters } from '@shared/models/query/query.models'; + +export interface DashboardInfo extends BaseData { + tenantId?: TenantId; + title?: string; + assignedCustomers?: Array; +} + +export interface WidgetLayout { + sizeX?: number; + sizeY?: number; + mobileHeight?: number; + mobileOrder?: number; + col?: number; + row?: number; +} + +export interface WidgetLayouts { + [id: string]: WidgetLayout; +} + +export interface GridSettings { + backgroundColor?: string; + color?: string; + columns?: number; + margin?: number; + backgroundSizeMode?: string; + backgroundImageUrl?: string; + autoFillHeight?: boolean; + mobileAutoFillHeight?: boolean; + mobileRowHeight?: number; + [key: string]: any; +} + +export interface DashboardLayout { + widgets: WidgetLayouts; + gridSettings: GridSettings; +} + +export interface DashboardLayoutInfo { + widgetIds?: string[]; + widgetLayouts?: WidgetLayouts; + gridSettings?: GridSettings; +} + +export declare type DashboardLayoutId = 'main' | 'right'; + +export declare type DashboardStateLayouts = {[key in DashboardLayoutId]?: DashboardLayout}; + +export declare type DashboardLayoutsInfo = {[key in DashboardLayoutId]?: DashboardLayoutInfo}; + +export interface DashboardState { + name: string; + root: boolean; + layouts: DashboardStateLayouts; +} + +export declare type StateControllerId = 'entity' | 'default' | string; + +export interface DashboardSettings { + stateControllerId?: StateControllerId; + showTitle?: boolean; + showDashboardsSelect?: boolean; + showEntitiesSelect?: boolean; + showFilters?: boolean; + showDashboardTimewindow?: boolean; + showDashboardExport?: boolean; + toolbarAlwaysOpen?: boolean; + titleColor?: string; +} + +export interface DashboardConfiguration { + timewindow?: Timewindow; + settings?: DashboardSettings; + widgets?: {[id: string]: Widget } | Widget[]; + states?: {[id: string]: DashboardState }; + entityAliases?: EntityAliases; + filters?: Filters; + [key: string]: any; +} + +export interface Dashboard extends DashboardInfo { + configuration?: DashboardConfiguration; +} + +export function isPublicDashboard(dashboard: DashboardInfo): boolean { + if (dashboard && dashboard.assignedCustomers) { + return dashboard.assignedCustomers + .filter(customerInfo => customerInfo.public).length > 0; + } else { + return false; + } +} + +export function getDashboardAssignedCustomersText(dashboard: DashboardInfo): string { + if (dashboard && dashboard.assignedCustomers && dashboard.assignedCustomers.length > 0) { + return dashboard.assignedCustomers + .filter(customerInfo => !customerInfo.public) + .map(customerInfo => customerInfo.title) + .join(', '); + } else { + return ''; + } +} + +export function isCurrentPublicDashboardCustomer(dashboard: DashboardInfo, customerId: string): boolean { + if (customerId && dashboard && dashboard.assignedCustomers) { + return dashboard.assignedCustomers.filter(customerInfo => { + return customerInfo.public && customerId === customerInfo.customerId.id; + }).length > 0; + } else { + return false; + } +} diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts new file mode 100644 index 0000000000..e6d96346cd --- /dev/null +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -0,0 +1,536 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { BaseData } from '@shared/models/base-data'; +import { DeviceId } from './id/device-id'; +import { TenantId } from '@shared/models/id/tenant-id'; +import { CustomerId } from '@shared/models/id/customer-id'; +import { DeviceCredentialsId } from '@shared/models/id/device-credentials-id'; +import { EntitySearchQuery } from '@shared/models/relation.models'; +import { DeviceProfileId } from '@shared/models/id/device-profile-id'; +import { RuleChainId } from '@shared/models/id/rule-chain-id'; +import { EntityInfoData } from '@shared/models/entity.models'; +import { KeyFilter } from '@shared/models/query/query.models'; +import { TimeUnit } from '@shared/models/time/time.models'; +import * as _moment from 'moment-timezone'; +import { AbstractControl, ValidationErrors } from '@angular/forms'; + +export enum DeviceProfileType { + DEFAULT = 'DEFAULT' +} + +export enum DeviceTransportType { + DEFAULT = 'DEFAULT', + MQTT = 'MQTT', + // LWM2M = 'LWM2M' +} + +export enum MqttTransportPayloadType { + JSON = 'JSON', + PROTOBUF = 'PROTOBUF' +} + +export enum DeviceProvisionType { + DISABLED = 'DISABLED', + ALLOW_CREATE_NEW_DEVICES = 'ALLOW_CREATE_NEW_DEVICES', + CHECK_PRE_PROVISIONED_DEVICES = 'CHECK_PRE_PROVISIONED_DEVICES' +} + +export interface DeviceConfigurationFormInfo { + hasProfileConfiguration: boolean; + hasDeviceConfiguration: boolean; +} + +export const deviceProfileTypeTranslationMap = new Map( + [ + [DeviceProfileType.DEFAULT, 'device-profile.type-default'] + ] +); + +export const deviceProfileTypeConfigurationInfoMap = new Map( + [ + [ + DeviceProfileType.DEFAULT, + { + hasProfileConfiguration: false, + hasDeviceConfiguration: false, + } + ] + ] +); + +export const deviceTransportTypeTranslationMap = new Map( + [ + [DeviceTransportType.DEFAULT, 'device-profile.transport-type-default'], + [DeviceTransportType.MQTT, 'device-profile.transport-type-mqtt'], + // [DeviceTransportType.LWM2M, 'device-profile.transport-type-lwm2m'] + ] +); + + +export const deviceProvisionTypeTranslationMap = new Map( + [ + [DeviceProvisionType.DISABLED, 'device-profile.provision-strategy-disabled'], + [DeviceProvisionType.ALLOW_CREATE_NEW_DEVICES, 'device-profile.provision-strategy-created-new'], + [DeviceProvisionType.CHECK_PRE_PROVISIONED_DEVICES, 'device-profile.provision-strategy-check-pre-provisioned'] + ] +); + +export const deviceTransportTypeHintMap = new Map( + [ + [DeviceTransportType.DEFAULT, 'device-profile.transport-type-default-hint'], + [DeviceTransportType.MQTT, 'device-profile.transport-type-mqtt-hint'], + // [DeviceTransportType.LWM2M, 'device-profile.transport-type-lwm2m-hint'] + ] +); + +export const mqttTransportPayloadTypeTranslationMap = new Map( + [ + [MqttTransportPayloadType.JSON, 'device-profile.mqtt-device-payload-type-json'], + [MqttTransportPayloadType.PROTOBUF, 'device-profile.mqtt-device-payload-type-proto'] + ] +); + + +export const deviceTransportTypeConfigurationInfoMap = new Map( + [ + [ + DeviceTransportType.DEFAULT, + { + hasProfileConfiguration: false, + hasDeviceConfiguration: false, + } + ], + [ + DeviceTransportType.MQTT, + { + hasProfileConfiguration: true, + hasDeviceConfiguration: false, + } + ], + /*[ + DeviceTransportType.LWM2M, + { + hasProfileConfiguration: true, + hasDeviceConfiguration: false, + } + ]*/ + ] +); + +export interface DefaultDeviceProfileConfiguration { + [key: string]: any; +} + +export type DeviceProfileConfigurations = DefaultDeviceProfileConfiguration; + +export interface DeviceProfileConfiguration extends DeviceProfileConfigurations { + type: DeviceProfileType; +} + +export interface DefaultDeviceProfileTransportConfiguration { + [key: string]: any; +} + +export interface MqttDeviceProfileTransportConfiguration { + deviceTelemetryTopic?: string; + deviceAttributesTopic?: string; + [key: string]: any; +} + +export interface Lwm2mDeviceProfileTransportConfiguration { + [key: string]: any; +} + +export type DeviceProfileTransportConfigurations = DefaultDeviceProfileTransportConfiguration & + MqttDeviceProfileTransportConfiguration & + Lwm2mDeviceProfileTransportConfiguration; + +export interface DeviceProfileTransportConfiguration extends DeviceProfileTransportConfigurations { + type: DeviceTransportType; +} + +export interface DeviceProvisionConfiguration { + type: DeviceProvisionType; + provisionDeviceSecret?: string; + provisionDeviceKey?: string; +} + +export function createDeviceProfileConfiguration(type: DeviceProfileType): DeviceProfileConfiguration { + let configuration: DeviceProfileConfiguration = null; + if (type) { + switch (type) { + case DeviceProfileType.DEFAULT: + const defaultConfiguration: DefaultDeviceProfileConfiguration = {}; + configuration = {...defaultConfiguration, type: DeviceProfileType.DEFAULT}; + break; + } + } + return configuration; +} + +export function createDeviceConfiguration(type: DeviceProfileType): DeviceConfiguration { + let configuration: DeviceConfiguration = null; + if (type) { + switch (type) { + case DeviceProfileType.DEFAULT: + const defaultConfiguration: DefaultDeviceConfiguration = {}; + configuration = {...defaultConfiguration, type: DeviceProfileType.DEFAULT}; + break; + } + } + return configuration; +} + +export function createDeviceProfileTransportConfiguration(type: DeviceTransportType): DeviceProfileTransportConfiguration { + let transportConfiguration: DeviceProfileTransportConfiguration = null; + if (type) { + switch (type) { + case DeviceTransportType.DEFAULT: + const defaultTransportConfiguration: DefaultDeviceProfileTransportConfiguration = {}; + transportConfiguration = {...defaultTransportConfiguration, type: DeviceTransportType.DEFAULT}; + break; + case DeviceTransportType.MQTT: + const mqttTransportConfiguration: MqttDeviceProfileTransportConfiguration = { + deviceTelemetryTopic: 'v1/devices/me/telemetry', + deviceAttributesTopic: 'v1/devices/me/attributes', + transportPayloadType: MqttTransportPayloadType.JSON + }; + transportConfiguration = {...mqttTransportConfiguration, type: DeviceTransportType.MQTT}; + break; + /*case DeviceTransportType.LWM2M: + const lwm2mTransportConfiguration: Lwm2mDeviceProfileTransportConfiguration = {}; + transportConfiguration = {...lwm2mTransportConfiguration, type: DeviceTransportType.LWM2M}; + break;*/ + } + } + return transportConfiguration; +} + +export function createDeviceTransportConfiguration(type: DeviceTransportType): DeviceTransportConfiguration { + let transportConfiguration: DeviceTransportConfiguration = null; + if (type) { + switch (type) { + case DeviceTransportType.DEFAULT: + const defaultTransportConfiguration: DefaultDeviceTransportConfiguration = {}; + transportConfiguration = {...defaultTransportConfiguration, type: DeviceTransportType.DEFAULT}; + break; + case DeviceTransportType.MQTT: + const mqttTransportConfiguration: MqttDeviceTransportConfiguration = {}; + transportConfiguration = {...mqttTransportConfiguration, type: DeviceTransportType.MQTT}; + break; + /* case DeviceTransportType.LWM2M: + const lwm2mTransportConfiguration: Lwm2mDeviceTransportConfiguration = {}; + transportConfiguration = {...lwm2mTransportConfiguration, type: DeviceTransportType.LWM2M}; + break;*/ + } + } + return transportConfiguration; +} + +export enum AlarmConditionType { + SIMPLE = 'SIMPLE', + DURATION = 'DURATION', + REPEATING = 'REPEATING' +} + +export const AlarmConditionTypeTranslationMap = new Map( + [ + [AlarmConditionType.SIMPLE, 'device-profile.condition-type-simple'], + [AlarmConditionType.DURATION, 'device-profile.condition-type-duration'], + [AlarmConditionType.REPEATING, 'device-profile.condition-type-repeating'] + ] +); + +export interface AlarmConditionSpec{ + type?: AlarmConditionType; + unit?: TimeUnit; + value?: number; + count?: number; +} + +export interface AlarmCondition { + condition: Array; + spec?: AlarmConditionSpec; +} + +export enum AlarmScheduleType { + ANY_TIME = 'ANY_TIME', + SPECIFIC_TIME = 'SPECIFIC_TIME', + CUSTOM = 'CUSTOM' +} + +export const AlarmScheduleTypeTranslationMap = new Map( + [ + [AlarmScheduleType.ANY_TIME, 'device-profile.schedule-any-time'], + [AlarmScheduleType.SPECIFIC_TIME, 'device-profile.schedule-specific-time'], + [AlarmScheduleType.CUSTOM, 'device-profile.schedule-custom'] + ] +); + +export interface AlarmSchedule{ + type: AlarmScheduleType; + timezone?: string; + daysOfWeek?: number[]; + startsOn?: number; + endsOn?: number; + items?: CustomTimeSchedulerItem[]; +} + +export interface CustomTimeSchedulerItem{ + enabled: boolean; + dayOfWeek: number; + startsOn: number; + endsOn: number; +} + +export interface AlarmRule { + condition: AlarmCondition; + alarmDetails?: string; + schedule?: AlarmSchedule; +} + +export function alarmRuleValidator(control: AbstractControl): ValidationErrors | null { + const alarmRule: AlarmRule = control.value; + return alarmRuleValid(alarmRule) ? null : {alarmRule: true}; +} + +function alarmRuleValid(alarmRule: AlarmRule): boolean { + if (!alarmRule || !alarmRule.condition || !alarmRule.condition.condition || !alarmRule.condition.condition.length) { + return false; + } + return true; +} + +export interface DeviceProfileAlarm { + id: string; + alarmType: string; + createRules: {[severity: string]: AlarmRule}; + clearRule?: AlarmRule; + propagate?: boolean; + propagateRelationTypes?: Array; +} + +export function deviceProfileAlarmValidator(control: AbstractControl): ValidationErrors | null { + const deviceProfileAlarm: DeviceProfileAlarm = control.value; + if (deviceProfileAlarm && deviceProfileAlarm.id && deviceProfileAlarm.alarmType && + deviceProfileAlarm.createRules) { + const severities = Object.keys(deviceProfileAlarm.createRules); + if (severities.length) { + let alarmRulesValid = true; + for (const severity of severities) { + const alarmRule = deviceProfileAlarm.createRules[severity]; + if (!alarmRuleValid(alarmRule)) { + alarmRulesValid = false; + break; + } + } + if (alarmRulesValid) { + if (deviceProfileAlarm.clearRule && !alarmRuleValid(deviceProfileAlarm.clearRule)) { + alarmRulesValid = false; + } + } + if (alarmRulesValid) { + return null; + } + } + } + return {deviceProfileAlarm: true}; +} + + +export interface DeviceProfileData { + configuration: DeviceProfileConfiguration; + transportConfiguration: DeviceProfileTransportConfiguration; + alarms?: Array; + provisionConfiguration?: DeviceProvisionConfiguration; +} + +export interface DeviceProfile extends BaseData { + tenantId?: TenantId; + name: string; + description?: string; + default?: boolean; + type: DeviceProfileType; + transportType: DeviceTransportType; + provisionType: DeviceProvisionType; + provisionDeviceKey?: string; + defaultRuleChainId?: RuleChainId; + defaultQueueName?: string; + profileData: DeviceProfileData; +} + +export interface DeviceProfileInfo extends EntityInfoData { + type: DeviceProfileType; + transportType: DeviceTransportType; +} + +export interface DefaultDeviceConfiguration { + [key: string]: any; +} + +export type DeviceConfigurations = DefaultDeviceConfiguration; + +export interface DeviceConfiguration extends DeviceConfigurations { + type: DeviceProfileType; +} + +export interface DefaultDeviceTransportConfiguration { + [key: string]: any; +} + +export interface MqttDeviceTransportConfiguration { + [key: string]: any; +} + +export interface Lwm2mDeviceTransportConfiguration { + [key: string]: any; +} + +export type DeviceTransportConfigurations = DefaultDeviceTransportConfiguration & + MqttDeviceTransportConfiguration & + Lwm2mDeviceTransportConfiguration; + +export interface DeviceTransportConfiguration extends DeviceTransportConfigurations { + type: DeviceTransportType; +} + +export interface DeviceData { + configuration: DeviceConfiguration; + transportConfiguration: DeviceTransportConfiguration; +} + +export interface Device extends BaseData { + tenantId?: TenantId; + customerId?: CustomerId; + name: string; + type: string; + label: string; + deviceProfileId?: DeviceProfileId; + deviceData?: DeviceData; + additionalInfo?: any; +} + +export interface DeviceInfo extends Device { + customerTitle: string; + customerIsPublic: boolean; + deviceProfileName: string; +} + +export enum DeviceCredentialsType { + ACCESS_TOKEN = 'ACCESS_TOKEN', + X509_CERTIFICATE = 'X509_CERTIFICATE', + MQTT_BASIC = 'MQTT_BASIC' +} + +export const credentialTypeNames = new Map( + [ + [DeviceCredentialsType.ACCESS_TOKEN, 'Access token'], + [DeviceCredentialsType.X509_CERTIFICATE, 'MQTT X.509'], + [DeviceCredentialsType.MQTT_BASIC, 'MQTT Basic'] + ] +); + +export interface DeviceCredentials extends BaseData { + deviceId: DeviceId; + credentialsType: DeviceCredentialsType; + credentialsId: string; + credentialsValue: string; +} + +export interface DeviceCredentialMQTTBasic { + clientId: string; + userName: string; + password: string; +} + +export interface DeviceSearchQuery extends EntitySearchQuery { + deviceTypes: Array; +} + +export interface ClaimRequest { + secretKey: string; +} + +export enum ClaimResponse { + SUCCESS = 'SUCCESS', + FAILURE = 'FAILURE', + CLAIMED = 'CLAIMED' +} + +export interface ClaimResult { + device: Device; + response: ClaimResponse; +} + +export const dayOfWeekTranslations = new Array( + 'device-profile.schedule-day.monday', + 'device-profile.schedule-day.tuesday', + 'device-profile.schedule-day.wednesday', + 'device-profile.schedule-day.thursday', + 'device-profile.schedule-day.friday', + 'device-profile.schedule-day.saturday', + 'device-profile.schedule-day.sunday' +); + +export function getDayString(day: number): string { + switch (day) { + case 0: + return 'device-profile.schedule-day.monday'; + case 1: + return this.translate.instant('device-profile.schedule-day.tuesday'); + case 2: + return this.translate.instant('device-profile.schedule-day.wednesday'); + case 3: + return this.translate.instant('device-profile.schedule-day.thursday'); + case 4: + return this.translate.instant('device-profile.schedule-day.friday'); + case 5: + return this.translate.instant('device-profile.schedule-day.saturday'); + case 6: + return this.translate.instant('device-profile.schedule-day.sunday'); + } +} + +export function timeOfDayToUTCTimestamp(date: Date | number): number { + if (typeof date === 'number' || date === null) { + return 0; + } + return _moment.utc([1970, 0, 1, date.getHours(), date.getMinutes(), date.getSeconds(), 0]).valueOf(); +} + +export function utcTimestampToTimeOfDay(time = 0): Date { + return new Date(time + new Date(time).getTimezoneOffset() * 60 * 1000); +} + +function timeOfDayToMoment(date: Date | number): _moment.Moment { + if (typeof date === 'number' || date === null) { + return _moment([1970, 0, 1, 0, 0, 0, 0]); + } + return _moment([1970, 0, 1, date.getHours(), date.getMinutes(), 0, 0]); +} + +export function getAlarmScheduleRangeText(startsOn: Date | number, endsOn: Date | number): string { + const start = timeOfDayToMoment(startsOn); + const end = timeOfDayToMoment(endsOn); + if (start < end) { + return `${start.format('hh:mm A')}${end.format('hh:mm A')}`; + } else if (start.valueOf() === 0 && end.valueOf() === 0 || start.isSame(_moment([1970, 0])) && end.isSame(_moment([1970, 0]))) { + return '12:00 AM12:00 PM'; + } + return `12:00 AM${end.format('hh:mm A')}` + + ` and ${start.format('hh:mm A')}12:00 PM`; +} diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts new file mode 100644 index 0000000000..c6a94ad8b5 --- /dev/null +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -0,0 +1,357 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { TenantId } from './id/tenant-id'; +import { BaseData, HasId } from '@shared/models/base-data'; + +/// +/// Copyright © 2016-2019 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export enum EntityType { + TENANT = 'TENANT', + TENANT_PROFILE = 'TENANT_PROFILE', + CUSTOMER = 'CUSTOMER', + USER = 'USER', + DASHBOARD = 'DASHBOARD', + ASSET = 'ASSET', + DEVICE = 'DEVICE', + DEVICE_PROFILE = 'DEVICE_PROFILE', + ALARM = 'ALARM', + RULE_CHAIN = 'RULE_CHAIN', + RULE_NODE = 'RULE_NODE', + ENTITY_VIEW = 'ENTITY_VIEW', + WIDGETS_BUNDLE = 'WIDGETS_BUNDLE', + WIDGET_TYPE = 'WIDGET_TYPE' +} + +export enum AliasEntityType { + CURRENT_CUSTOMER = 'CURRENT_CUSTOMER', + CURRENT_TENANT = 'CURRENT_TENANT', + CURRENT_USER = 'CURRENT_USER', + CURRENT_USER_OWNER = 'CURRENT_USER_OWNER' +} + +export interface EntityTypeTranslation { + type?: string; + typePlural?: string; + list?: string; + nameStartsWith?: string; + details?: string; + add?: string; + noEntities?: string; + selectedEntities?: string; + search?: string; +} + +export interface EntityTypeResource { + helpLinkId: string; + helpLinkIdForEntity?(entity: T): string; +} + +export const entityTypeTranslations = new Map( + [ + [ + EntityType.TENANT, + { + type: 'entity.type-tenant', + typePlural: 'entity.type-tenants', + list: 'entity.list-of-tenants', + nameStartsWith: 'entity.tenant-name-starts-with', + details: 'tenant.tenant-details', + add: 'tenant.add', + noEntities: 'tenant.no-tenants-text', + search: 'tenant.search', + selectedEntities: 'tenant.selected-tenants' + } + ], + [ + EntityType.TENANT_PROFILE, + { + type: 'entity.type-tenant-profile', + typePlural: 'entity.type-tenant-profiles', + list: 'entity.list-of-tenant-profiles', + nameStartsWith: 'entity.tenant-profile-name-starts-with', + details: 'tenant-profile.tenant-profile-details', + add: 'tenant-profile.add', + noEntities: 'tenant-profile.no-tenant-profiles-text', + search: 'tenant-profile.search', + selectedEntities: 'tenant-profile.selected-tenant-profiles' + } + ], + [ + EntityType.CUSTOMER, + { + type: 'entity.type-customer', + typePlural: 'entity.type-customers', + list: 'entity.list-of-customers', + nameStartsWith: 'entity.customer-name-starts-with', + details: 'customer.customer-details', + add: 'customer.add', + noEntities: 'customer.no-customers-text', + search: 'customer.search', + selectedEntities: 'customer.selected-customers' + } + ], + [ + EntityType.USER, + { + type: 'entity.type-user', + typePlural: 'entity.type-users', + list: 'entity.list-of-users', + nameStartsWith: 'entity.user-name-starts-with', + details: 'user.user-details', + add: 'user.add', + noEntities: 'user.no-users-text', + search: 'user.search', + selectedEntities: 'user.selected-users' + } + ], + [ + EntityType.DEVICE, + { + type: 'entity.type-device', + typePlural: 'entity.type-devices', + list: 'entity.list-of-devices', + nameStartsWith: 'entity.device-name-starts-with', + details: 'device.device-details', + add: 'device.add', + noEntities: 'device.no-devices-text', + search: 'device.search', + selectedEntities: 'device.selected-devices' + } + ], + [ + EntityType.DEVICE_PROFILE, + { + type: 'entity.type-device-profile', + typePlural: 'entity.type-device-profiles', + list: 'entity.list-of-device-profiles', + nameStartsWith: 'entity.device-profile-name-starts-with', + details: 'device-profile.device-profile-details', + add: 'device-profile.add', + noEntities: 'device-profile.no-device-profiles-text', + search: 'device-profile.search', + selectedEntities: 'device-profile.selected-device-profiles' + } + ], + [ + EntityType.ASSET, + { + type: 'entity.type-asset', + typePlural: 'entity.type-assets', + list: 'entity.list-of-assets', + nameStartsWith: 'entity.asset-name-starts-with', + details: 'asset.asset-details', + add: 'asset.add', + noEntities: 'asset.no-assets-text', + search: 'asset.search', + selectedEntities: 'asset.selected-assets' + } + ], + [ + EntityType.ENTITY_VIEW, + { + type: 'entity.type-entity-view', + typePlural: 'entity.type-entity-views', + list: 'entity.list-of-entity-views', + nameStartsWith: 'entity.entity-view-name-starts-with', + details: 'entity-view.entity-view-details', + add: 'entity-view.add', + noEntities: 'entity-view.no-entity-views-text', + search: 'entity-view.search', + selectedEntities: 'entity-view.selected-entity-views' + } + ], + [ + EntityType.RULE_CHAIN, + { + type: 'entity.type-rulechain', + typePlural: 'entity.type-rulechains', + list: 'entity.list-of-rulechains', + nameStartsWith: 'entity.rulechain-name-starts-with', + details: 'rulechain.rulechain-details', + add: 'rulechain.add', + noEntities: 'rulechain.no-rulechains-text', + search: 'rulechain.search', + selectedEntities: 'rulechain.selected-rulechains' + } + ], + [ + EntityType.RULE_NODE, + { + type: 'entity.type-rulenode', + typePlural: 'entity.type-rulenodes', + list: 'entity.list-of-rulenodes', + nameStartsWith: 'entity.rulenode-name-starts-with' + } + ], + [ + EntityType.DASHBOARD, + { + type: 'entity.type-dashboard', + typePlural: 'entity.type-dashboards', + list: 'entity.list-of-dashboards', + nameStartsWith: 'entity.dashboard-name-starts-with', + details: 'dashboard.dashboard-details', + add: 'dashboard.add', + noEntities: 'dashboard.no-dashboards-text', + search: 'dashboard.search', + selectedEntities: 'dashboard.selected-dashboards' + } + ], + [ + EntityType.ALARM, + { + type: 'entity.type-alarm', + typePlural: 'entity.type-alarms', + list: 'entity.list-of-alarms', + nameStartsWith: 'entity.alarm-name-starts-with', + details: 'dashboard.dashboard-details', + noEntities: 'alarm.no-alarms-prompt', + search: 'alarm.search', + selectedEntities: 'alarm.selected-alarms' + } + ], + [ + EntityType.WIDGETS_BUNDLE, + { + details: 'widgets-bundle.widgets-bundle-details', + add: 'widgets-bundle.add', + noEntities: 'widgets-bundle.no-widgets-bundles-text', + search: 'widgets-bundle.search', + selectedEntities: 'widgets-bundle.selected-widgets-bundles' + } + ], + [ + AliasEntityType.CURRENT_CUSTOMER, + { + type: 'entity.type-current-customer', + list: 'entity.type-current-customer' + } + ], + [ + AliasEntityType.CURRENT_TENANT, + { + type: 'entity.type-current-tenant', + list: 'entity.type-current-tenant' + } + ], + [ + AliasEntityType.CURRENT_USER, + { + type: 'entity.type-current-user', + list: 'entity.type-current-user' + } + ], + [ + AliasEntityType.CURRENT_USER_OWNER, + { + type: 'entity.type-current-user-owner', + list: 'entity.type-current-user-owner' + } + ] + ] +); + +export const entityTypeResources = new Map>>( + [ + [ + EntityType.TENANT, + { + helpLinkId: 'tenants' + } + ], + [ + EntityType.TENANT_PROFILE, + { + helpLinkId: 'tenantProfiles' + } + ], + [ + EntityType.CUSTOMER, + { + helpLinkId: 'customers' + } + ], + [ + EntityType.USER, + { + helpLinkId: 'users' + } + ], + [ + EntityType.DEVICE, + { + helpLinkId: 'devices' + } + ], + [ + EntityType.DEVICE_PROFILE, + { + helpLinkId: 'deviceProfiles' + } + ], + [ + EntityType.ASSET, + { + helpLinkId: 'assets' + } + ], + [ + EntityType.ENTITY_VIEW, + { + helpLinkId: 'entityViews' + } + ], + [ + EntityType.RULE_CHAIN, + { + helpLinkId: 'rulechains' + } + ], + [ + EntityType.DASHBOARD, + { + helpLinkId: 'dashboards' + } + ], + [ + EntityType.WIDGETS_BUNDLE, + { + helpLinkId: 'widgetsBundles' + } + ] + ] +); + +export interface EntitySubtype { + tenantId: TenantId; + entityType: EntityType; + type: string; +} diff --git a/ui-ngx/src/app/shared/models/entity-view.models.ts b/ui-ngx/src/app/shared/models/entity-view.models.ts new file mode 100644 index 0000000000..758916fe4b --- /dev/null +++ b/ui-ngx/src/app/shared/models/entity-view.models.ts @@ -0,0 +1,54 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { BaseData } from '@shared/models/base-data'; +import { TenantId } from '@shared/models/id/tenant-id'; +import { CustomerId } from '@shared/models/id/customer-id'; +import { EntityViewId } from '@shared/models/id/entity-view-id'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntitySearchQuery } from '@shared/models/relation.models'; + +export interface AttributesEntityView { + cs: Array; + ss: Array; + sh: Array; +} + +export interface TelemetryEntityView { + timeseries: Array; + attributes: AttributesEntityView; +} + +export interface EntityView extends BaseData { + tenantId: TenantId; + customerId: CustomerId; + entityId: EntityId; + name: string; + type: string; + keys: TelemetryEntityView; + startTimeMs: number; + endTimeMs: number; + additionalInfo?: any; +} + +export interface EntityViewInfo extends EntityView { + customerTitle: string; + customerIsPublic: boolean; +} + +export interface EntityViewSearchQuery extends EntitySearchQuery { + entityViewTypes: Array; +} diff --git a/ui-ngx/src/app/shared/models/entity.models.ts b/ui-ngx/src/app/shared/models/entity.models.ts new file mode 100644 index 0000000000..8f1d4f417e --- /dev/null +++ b/ui-ngx/src/app/shared/models/entity.models.ts @@ -0,0 +1,144 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityType } from '@shared/models/entity-type.models'; +import { AttributeData } from './telemetry/telemetry.models'; +import { EntityId } from '@shared/models/id/entity-id'; + +export interface EntityInfo { + name?: string; + label?: string; + entityType?: EntityType; + id?: string; + entityDescription?: string; +} + +export interface EntityInfoData { + id: EntityId; + name: string; +} + +export interface ImportEntityData { + name: string; + type: string; + label: string; + gateway: boolean; + description: string; + accessToken: string; + attributes: { + server: AttributeData[], + shared: AttributeData[] + }; + timeseries: AttributeData[]; +} + +export interface ImportEntitiesResultInfo { + create?: { + entity: number; + }; + update?: { + entity: number; + }; + error?: { + entity: number; + }; +} + +export interface EntityField { + keyName: string; + value: string; + name: string; + time?: boolean; +} + +export const entityFields: {[fieldName: string]: EntityField} = { + createdTime: { + keyName: 'createdTime', + name: 'entity-field.created-time', + value: 'createdTime', + time: true + }, + name: { + keyName: 'name', + name: 'entity-field.name', + value: 'name' + }, + type: { + keyName: 'type', + name: 'entity-field.type', + value: 'type' + }, + firstName: { + keyName: 'firstName', + name: 'entity-field.first-name', + value: 'firstName' + }, + lastName: { + keyName: 'lastName', + name: 'entity-field.last-name', + value: 'lastName' + }, + email: { + keyName: 'email', + name: 'entity-field.email', + value: 'email' + }, + title: { + keyName: 'title', + name: 'entity-field.title', + value: 'title' + }, + country: { + keyName: 'country', + name: 'entity-field.country', + value: 'country' + }, + state: { + keyName: 'state', + name: 'entity-field.state', + value: 'state' + }, + city: { + keyName: 'city', + name: 'entity-field.city', + value: 'city' + }, + address: { + keyName: 'address', + name: 'entity-field.address', + value: 'address' + }, + address2: { + keyName: 'address2', + name: 'entity-field.address2', + value: 'address2' + }, + zip: { + keyName: 'zip', + name: 'entity-field.zip', + value: 'zip' + }, + phone: { + keyName: 'phone', + name: 'entity-field.phone', + value: 'phone' + }, + label: { + keyName: 'label', + name: 'entity-field.label', + value: 'label' + } +}; diff --git a/ui-ngx/src/app/shared/models/error.models.ts b/ui-ngx/src/app/shared/models/error.models.ts new file mode 100644 index 0000000000..d36fcf58b4 --- /dev/null +++ b/ui-ngx/src/app/shared/models/error.models.ts @@ -0,0 +1,23 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + + +export interface ExceptionData { + message?: string; + name?: string; + lineNumber?: number; + columnNumber?: number; +} diff --git a/ui-ngx/src/app/shared/models/event.models.ts b/ui-ngx/src/app/shared/models/event.models.ts new file mode 100644 index 0000000000..68b1eefd39 --- /dev/null +++ b/ui-ngx/src/app/shared/models/event.models.ts @@ -0,0 +1,85 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { BaseData } from '@shared/models/base-data'; +import { TenantId } from '@shared/models/id/tenant-id'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EventId } from './id/event-id'; +import { ContentType } from '@shared/models/constants'; + +export enum EventType { + ERROR = 'ERROR', + LC_EVENT = 'LC_EVENT', + STATS = 'STATS' +} + +export enum DebugEventType { + DEBUG_RULE_NODE = 'DEBUG_RULE_NODE', + DEBUG_RULE_CHAIN = 'DEBUG_RULE_CHAIN' +} + +export const eventTypeTranslations = new Map( + [ + [EventType.ERROR, 'event.type-error'], + [EventType.LC_EVENT, 'event.type-lc-event'], + [EventType.STATS, 'event.type-stats'], + [DebugEventType.DEBUG_RULE_NODE, 'event.type-debug-rule-node'], + [DebugEventType.DEBUG_RULE_CHAIN, 'event.type-debug-rule-chain'], + ] +); + +export interface BaseEventBody { + server: string; +} + +export interface ErrorEventBody extends BaseEventBody { + method: string; + error: string; +} + +export interface LcEventEventBody extends BaseEventBody { + event: string; + success: boolean; + error: string; +} + +export interface StatsEventBody extends BaseEventBody { + messagesProcessed: number; + errorsOccurred: number; +} + +export interface DebugRuleNodeEventBody extends BaseEventBody { + type: string; + entityId: string; + entityName: string; + msgId: string; + msgType: string; + relationType: string; + dataType: ContentType; + data: string; + metadata: string; + error: string; +} + +export type EventBody = ErrorEventBody & LcEventEventBody & StatsEventBody & DebugRuleNodeEventBody; + +export interface Event extends BaseData { + tenantId: TenantId; + entityId: EntityId; + type: string; + uid: string; + body: EventBody; +} diff --git a/ui-ngx/src/app/shared/models/id/alarm-id.ts b/ui-ngx/src/app/shared/models/id/alarm-id.ts new file mode 100644 index 0000000000..a99ef94335 --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/alarm-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from './entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class AlarmId implements EntityId { + entityType = EntityType.ALARM; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/asset-id.ts b/ui-ngx/src/app/shared/models/id/asset-id.ts new file mode 100644 index 0000000000..46fff680f8 --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/asset-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from './entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class AssetId implements EntityId { + entityType = EntityType.ASSET; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/audit-log-id.ts b/ui-ngx/src/app/shared/models/id/audit-log-id.ts new file mode 100644 index 0000000000..98d5d9c169 --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/audit-log-id.ts @@ -0,0 +1,24 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { HasUUID } from '@shared/models/id/has-uuid'; + +export class AuditLogId implements HasUUID { + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/customer-id.ts b/ui-ngx/src/app/shared/models/id/customer-id.ts new file mode 100644 index 0000000000..5c458aa4e0 --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/customer-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from './entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class CustomerId implements EntityId { + entityType = EntityType.CUSTOMER; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/dashboard-id.ts b/ui-ngx/src/app/shared/models/id/dashboard-id.ts new file mode 100644 index 0000000000..3dd9842442 --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/dashboard-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from './entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class DashboardId implements EntityId { + entityType = EntityType.DASHBOARD; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/device-credentials-id.ts b/ui-ngx/src/app/shared/models/id/device-credentials-id.ts new file mode 100644 index 0000000000..e5e2b05ea6 --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/device-credentials-id.ts @@ -0,0 +1,24 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { HasUUID } from '@shared/models/id/has-uuid'; + +export class DeviceCredentialsId implements HasUUID { + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/device-id.ts b/ui-ngx/src/app/shared/models/id/device-id.ts new file mode 100644 index 0000000000..d70801b4cc --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/device-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from './entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class DeviceId implements EntityId { + entityType = EntityType.DEVICE; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/device-profile-id.ts b/ui-ngx/src/app/shared/models/id/device-profile-id.ts new file mode 100644 index 0000000000..6bb43fe415 --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/device-profile-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from './entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class DeviceProfileId implements EntityId { + entityType = EntityType.DEVICE_PROFILE; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/entity-id.ts b/ui-ngx/src/app/shared/models/id/entity-id.ts new file mode 100644 index 0000000000..1596af127b --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/entity-id.ts @@ -0,0 +1,31 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; +import { HasUUID } from '@shared/models/id/has-uuid'; +import { isDefinedAndNotNull } from '@core/utils'; + +export interface EntityId extends HasUUID { + entityType: EntityType | AliasEntityType; +} + +export function entityIdEquals(entityId1: EntityId, entityId2: EntityId): boolean { + if (isDefinedAndNotNull(entityId1) && isDefinedAndNotNull(entityId2)) { + return entityId1.id === entityId2.id && entityId1.entityType === entityId2.entityType; + } else { + return entityId1 === entityId2; + } +} diff --git a/ui-ngx/src/app/shared/models/id/entity-view-id.ts b/ui-ngx/src/app/shared/models/id/entity-view-id.ts new file mode 100644 index 0000000000..feaf8799fb --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/entity-view-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from './entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class EntityViewId implements EntityId { + entityType = EntityType.ENTITY_VIEW; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/event-id.ts b/ui-ngx/src/app/shared/models/id/event-id.ts new file mode 100644 index 0000000000..0eb2ae77ad --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/event-id.ts @@ -0,0 +1,24 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { HasUUID } from '@shared/models/id/has-uuid'; + +export class EventId implements HasUUID { + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/has-uuid.ts b/ui-ngx/src/app/shared/models/id/has-uuid.ts new file mode 100644 index 0000000000..de203bf67f --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/has-uuid.ts @@ -0,0 +1,21 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export const NULL_UUID = '13814000-1dd2-11b2-8080-808080808080'; + +export interface HasUUID { + id: string; +} diff --git a/ui-ngx/src/app/shared/models/id/public-api.ts b/ui-ngx/src/app/shared/models/id/public-api.ts new file mode 100644 index 0000000000..57287b1a16 --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/public-api.ts @@ -0,0 +1,35 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export * from './alarm-id'; +export * from './asset-id'; +export * from './audit-log-id'; +export * from './customer-id'; +export * from './dashboard-id'; +export * from './device-credentials-id'; +export * from './device-id'; +export * from './device-profile-id'; +export * from './entity-id'; +export * from './entity-view-id'; +export * from './event-id'; +export * from './has-uuid'; +export * from './rule-chain-id'; +export * from './rule-node-id'; +export * from './tenant-id'; +export * from './tenant-profile-id'; +export * from './user-id'; +export * from './widget-type-id'; +export * from './widgets-bundle-id'; diff --git a/ui-ngx/src/app/shared/models/id/rule-chain-id.ts b/ui-ngx/src/app/shared/models/id/rule-chain-id.ts new file mode 100644 index 0000000000..4c069af595 --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/rule-chain-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from './entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class RuleChainId implements EntityId { + entityType = EntityType.RULE_CHAIN; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/rule-node-id.ts b/ui-ngx/src/app/shared/models/id/rule-node-id.ts new file mode 100644 index 0000000000..2c23c51dd3 --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/rule-node-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from './entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class RuleNodeId implements EntityId { + entityType = EntityType.RULE_NODE; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/tenant-id.ts b/ui-ngx/src/app/shared/models/id/tenant-id.ts new file mode 100644 index 0000000000..73b03c3582 --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/tenant-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from './entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class TenantId implements EntityId { + entityType = EntityType.TENANT; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/tenant-profile-id.ts b/ui-ngx/src/app/shared/models/id/tenant-profile-id.ts new file mode 100644 index 0000000000..6ac46bec62 --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/tenant-profile-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from './entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class TenantProfileId implements EntityId { + entityType = EntityType.TENANT_PROFILE; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/user-id.ts b/ui-ngx/src/app/shared/models/id/user-id.ts new file mode 100644 index 0000000000..18069bc3db --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/user-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from './entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class UserId implements EntityId { + entityType = EntityType.USER; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/widget-type-id.ts b/ui-ngx/src/app/shared/models/id/widget-type-id.ts new file mode 100644 index 0000000000..625c3dc631 --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/widget-type-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from './entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class WidgetTypeId implements EntityId { + entityType = EntityType.WIDGET_TYPE; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/widgets-bundle-id.ts b/ui-ngx/src/app/shared/models/id/widgets-bundle-id.ts new file mode 100644 index 0000000000..6788a1a08d --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/widgets-bundle-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from './entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class WidgetsBundleId implements EntityId { + entityType = EntityType.WIDGETS_BUNDLE; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/login.models.ts b/ui-ngx/src/app/shared/models/login.models.ts new file mode 100644 index 0000000000..19b40fd50e --- /dev/null +++ b/ui-ngx/src/app/shared/models/login.models.ts @@ -0,0 +1,29 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export interface LoginRequest { + username: string; + password: string; +} + +export interface PublicLoginRequest { + publicId: string; +} + +export interface LoginResponse { + token: string; + refreshToken: string; +} diff --git a/ui-ngx/src/app/shared/models/material.models.ts b/ui-ngx/src/app/shared/models/material.models.ts new file mode 100644 index 0000000000..7016898edf --- /dev/null +++ b/ui-ngx/src/app/shared/models/material.models.ts @@ -0,0 +1,369 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import * as tinycolor_ from 'tinycolor2'; + +const tinycolor = tinycolor_; + +export interface MaterialColorItem { + value: string; + group: string; + label: string; + isDark: boolean; +} + +export const materialColorPalette: {[palette: string]: {[spectrum: string]: string}} = { + red: { + 50: '#ffebee', + 100: '#ffcdd2', + 200: '#ef9a9a', + 300: '#e57373', + 400: '#ef5350', + 500: '#f44336', + 600: '#e53935', + 700: '#d32f2f', + 800: '#c62828', + 900: '#b71c1c', + A100: '#ff8a80', + A200: '#ff5252', + A400: '#ff1744', + A700: '#d50000' + }, + pink: { + 50: '#fce4ec', + 100: '#f8bbd0', + 200: '#f48fb1', + 300: '#f06292', + 400: '#ec407a', + 500: '#e91e63', + 600: '#d81b60', + 700: '#c2185b', + 800: '#ad1457', + 900: '#880e4f', + A100: '#ff80ab', + A200: '#ff4081', + A400: '#f50057', + A700: '#c51162' + }, + purple: { + 50: '#f3e5f5', + 100: '#e1bee7', + 200: '#ce93d8', + 300: '#ba68c8', + 400: '#ab47bc', + 500: '#9c27b0', + 600: '#8e24aa', + 700: '#7b1fa2', + 800: '#6a1b9a', + 900: '#4a148c', + A100: '#ea80fc', + A200: '#e040fb', + A400: '#d500f9', + A700: '#aa00ff' + }, + 'deep-purple': { + 50: '#ede7f6', + 100: '#d1c4e9', + 200: '#b39ddb', + 300: '#9575cd', + 400: '#7e57c2', + 500: '#673ab7', + 600: '#5e35b1', + 700: '#512da8', + 800: '#4527a0', + 900: '#311b92', + A100: '#b388ff', + A200: '#7c4dff', + A400: '#651fff', + A700: '#6200ea' + }, + indigo: { + 50: '#e8eaf6', + 100: '#c5cae9', + 200: '#9fa8da', + 300: '#7986cb', + 400: '#5c6bc0', + 500: '#3f51b5', + 600: '#3949ab', + 700: '#303f9f', + 800: '#283593', + 900: '#1a237e', + A100: '#8c9eff', + A200: '#536dfe', + A400: '#3d5afe', + A700: '#304ffe' + }, + blue: { + 50: '#e3f2fd', + 100: '#bbdefb', + 200: '#90caf9', + 300: '#64b5f6', + 400: '#42a5f5', + 500: '#2196f3', + 600: '#1e88e5', + 700: '#1976d2', + 800: '#1565c0', + 900: '#0d47a1', + A100: '#82b1ff', + A200: '#448aff', + A400: '#2979ff', + A700: '#2962ff' + }, + 'light-blue': { + 50: '#e1f5fe', + 100: '#b3e5fc', + 200: '#81d4fa', + 300: '#4fc3f7', + 400: '#29b6f6', + 500: '#03a9f4', + 600: '#039be5', + 700: '#0288d1', + 800: '#0277bd', + 900: '#01579b', + A100: '#80d8ff', + A200: '#40c4ff', + A400: '#00b0ff', + A700: '#0091ea' + }, + cyan: { + 50: '#e0f7fa', + 100: '#b2ebf2', + 200: '#80deea', + 300: '#4dd0e1', + 400: '#26c6da', + 500: '#00bcd4', + 600: '#00acc1', + 700: '#0097a7', + 800: '#00838f', + 900: '#006064', + A100: '#84ffff', + A200: '#18ffff', + A400: '#00e5ff', + A700: '#00b8d4' + }, + teal: { + 50: '#e0f2f1', + 100: '#b2dfdb', + 200: '#80cbc4', + 300: '#4db6ac', + 400: '#26a69a', + 500: '#009688', + 600: '#00897b', + 700: '#00796b', + 800: '#00695c', + 900: '#004d40', + A100: '#a7ffeb', + A200: '#64ffda', + A400: '#1de9b6', + A700: '#00bfa5' + }, + green: { + 50: '#e8f5e9', + 100: '#c8e6c9', + 200: '#a5d6a7', + 300: '#81c784', + 400: '#66bb6a', + 500: '#4caf50', + 600: '#43a047', + 700: '#388e3c', + 800: '#2e7d32', + 900: '#1b5e20', + A100: '#b9f6ca', + A200: '#69f0ae', + A400: '#00e676', + A700: '#00c853' + }, + 'light-green': { + 50: '#f1f8e9', + 100: '#dcedc8', + 200: '#c5e1a5', + 300: '#aed581', + 400: '#9ccc65', + 500: '#8bc34a', + 600: '#7cb342', + 700: '#689f38', + 800: '#558b2f', + 900: '#33691e', + A100: '#ccff90', + A200: '#b2ff59', + A400: '#76ff03', + A700: '#64dd17' + }, + lime: { + 50: '#f9fbe7', + 100: '#f0f4c3', + 200: '#e6ee9c', + 300: '#dce775', + 400: '#d4e157', + 500: '#cddc39', + 600: '#c0ca33', + 700: '#afb42b', + 800: '#9e9d24', + 900: '#827717', + A100: '#f4ff81', + A200: '#eeff41', + A400: '#c6ff00', + A700: '#aeea00' + }, + yellow: { + 50: '#fffde7', + 100: '#fff9c4', + 200: '#fff59d', + 300: '#fff176', + 400: '#ffee58', + 500: '#ffeb3b', + 600: '#fdd835', + 700: '#fbc02d', + 800: '#f9a825', + 900: '#f57f17', + A100: '#ffff8d', + A200: '#ffff00', + A400: '#ffea00', + A700: '#ffd600' + }, + amber: { + 50: '#fff8e1', + 100: '#ffecb3', + 200: '#ffe082', + 300: '#ffd54f', + 400: '#ffca28', + 500: '#ffc107', + 600: '#ffb300', + 700: '#ffa000', + 800: '#ff8f00', + 900: '#ff6f00', + A100: '#ffe57f', + A200: '#ffd740', + A400: '#ffc400', + A700: '#ffab00' + }, + orange: { + 50: '#fff3e0', + 100: '#ffe0b2', + 200: '#ffcc80', + 300: '#ffb74d', + 400: '#ffa726', + 500: '#ff9800', + 600: '#fb8c00', + 700: '#f57c00', + 800: '#ef6c00', + 900: '#e65100', + A100: '#ffd180', + A200: '#ffab40', + A400: '#ff9100', + A700: '#ff6d00' + }, + 'deep-orange': { + 50: '#fbe9e7', + 100: '#ffccbc', + 200: '#ffab91', + 300: '#ff8a65', + 400: '#ff7043', + 500: '#ff5722', + 600: '#f4511e', + 700: '#e64a19', + 800: '#d84315', + 900: '#bf360c', + A100: '#ff9e80', + A200: '#ff6e40', + A400: '#ff3d00', + A700: '#dd2c00' + }, + brown: { + 50: '#efebe9', + 100: '#d7ccc8', + 200: '#bcaaa4', + 300: '#a1887f', + 400: '#8d6e63', + 500: '#795548', + 600: '#6d4c41', + 700: '#5d4037', + 800: '#4e342e', + 900: '#3e2723', + A100: '#d7ccc8', + A200: '#bcaaa4', + A400: '#8d6e63', + A700: '#5d4037' + }, + grey: { + 50: '#fafafa', + 100: '#f5f5f5', + 200: '#eeeeee', + 300: '#e0e0e0', + 400: '#bdbdbd', + 500: '#9e9e9e', + 600: '#757575', + 700: '#616161', + 800: '#424242', + 900: '#212121', + A100: '#ffffff', + A200: '#000000', + A400: '#303030', + A700: '#616161' + }, + 'blue-grey': { + 50: '#eceff1', + 100: '#cfd8dc', + 200: '#b0bec5', + 300: '#90a4ae', + 400: '#78909c', + 500: '#607d8b', + 600: '#546e7a', + 700: '#455a64', + 800: '#37474f', + 900: '#263238', + A100: '#cfd8dc', + A200: '#b0bec5', + A400: '#78909c', + A700: '#455a64' + } +}; + +export const materialColors = new Array(); + +const colorPalettes = ['blue', 'green', 'red', 'amber', 'blue-grey', 'purple', 'light-green', + 'indigo', 'pink', 'yellow', 'light-blue', 'orange', 'deep-purple', 'lime', 'teal', 'brown', 'cyan', 'deep-orange', 'grey']; +const colorSpectrum = ['500', 'A700', '600', '700', '800', '900', '300', '400', 'A200', 'A400']; + +for (const key of Object.keys(materialColorPalette)) { + const value = materialColorPalette[key]; + for (const label of Object.keys(value)) { + if (colorSpectrum.indexOf(label) > -1) { + const colorValue = value[label]; + const color = tinycolor(colorValue); + const isDark = color.isDark(); + const colorItem = { + value: color.toHexString(), + group: key, + label, + isDark + }; + materialColors.push(colorItem); + } + } +} + +materialColors.sort((colorItem1, colorItem2) => { + const spectrumIndex1 = colorSpectrum.indexOf(colorItem1.label); + const spectrumIndex2 = colorSpectrum.indexOf(colorItem2.label); + let result = spectrumIndex1 - spectrumIndex2; + if (result === 0) { + const paletteIndex1 = colorPalettes.indexOf(colorItem1.group); + const paletteIndex2 = colorPalettes.indexOf(colorItem2.group); + result = paletteIndex1 - paletteIndex2; + } + return result; +}); diff --git a/ui-ngx/src/app/shared/models/oauth2.models.ts b/ui-ngx/src/app/shared/models/oauth2.models.ts new file mode 100644 index 0000000000..54d4d3c8d7 --- /dev/null +++ b/ui-ngx/src/app/shared/models/oauth2.models.ts @@ -0,0 +1,119 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { HasUUID } from '@shared/models/id/has-uuid'; + +export interface OAuth2ClientsParams { + enabled: boolean; + domainsParams: OAuth2ClientsDomainParams[]; +} + +export interface OAuth2ClientsDomainParams { + clientRegistrations: ClientRegistration[]; + domainInfos: DomainInfo[]; +} + +export interface DomainInfo { + name: string; + scheme: DomainSchema; +} + +export enum DomainSchema{ + HTTP = 'HTTP', + HTTPS = 'HTTPS', + MIXED = 'MIXED' +} + +export const domainSchemaTranslations = new Map( + [ + [DomainSchema.HTTP, 'admin.oauth2.domain-schema-http'], + [DomainSchema.HTTPS, 'admin.oauth2.domain-schema-https'], + [DomainSchema.MIXED, 'admin.oauth2.domain-schema-mixed'] + ] +); + +export enum MapperConfigType{ + BASIC = 'BASIC', + CUSTOM = 'CUSTOM', + GITHUB = 'GITHUB' +} + +export enum TenantNameStrategy{ + DOMAIN = 'DOMAIN', + EMAIL = 'EMAIL', + CUSTOM = 'CUSTOM' +} + +export interface OAuth2ClientRegistrationTemplate extends ClientRegistration{ + comment: string; + createdTime: number; + helpLink: string; + name: string; + providerId: string; + id: HasUUID; +} + +export interface ClientRegistration { + loginButtonLabel: string; + loginButtonIcon: string; + clientId: string; + clientSecret: string; + accessTokenUri: string; + authorizationUri: string; + scope: string[]; + jwkSetUri?: string; + userInfoUri: string; + clientAuthenticationMethod: ClientAuthenticationMethod; + userNameAttributeName: string; + mapperConfig: MapperConfig; + additionalInfo: string; +} + +export enum ClientAuthenticationMethod { + BASIC = 'BASIC', + POST = 'POST' +} + +export interface MapperConfig { + allowUserCreation: boolean; + activateUser: boolean; + type: MapperConfigType; + basic?: MapperConfigBasic; + custom?: MapperConfigCustom; +} + +export interface MapperConfigBasic { + emailAttributeKey: string; + firstNameAttributeKey?: string; + lastNameAttributeKey?: string; + tenantNameStrategy: TenantNameStrategy; + tenantNamePattern?: string; + customerNamePattern?: string; + defaultDashboardName?: string; + alwaysFullScreen?: boolean; +} + +export interface MapperConfigCustom { + url: string; + username?: string; + password?: string; +} + +export interface OAuth2ClientInfo { + name: string; + icon?: string; + url: string; +} diff --git a/ui-ngx/src/app/shared/models/page/page-data.ts b/ui-ngx/src/app/shared/models/page/page-data.ts new file mode 100644 index 0000000000..02662c4348 --- /dev/null +++ b/ui-ngx/src/app/shared/models/page/page-data.ts @@ -0,0 +1,31 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export interface PageData { + data: Array; + totalPages: number; + totalElements: number; + hasNext: boolean; +} + +export function emptyPageData(): PageData { + return { + data: [], + totalPages: 0, + totalElements: 0, + hasNext: false + } as PageData; +} diff --git a/ui-ngx/src/app/shared/models/page/page-link.ts b/ui-ngx/src/app/shared/models/page/page-link.ts new file mode 100644 index 0000000000..dba4ce097c --- /dev/null +++ b/ui-ngx/src/app/shared/models/page/page-link.ts @@ -0,0 +1,186 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Direction, SortOrder } from '@shared/models/page/sort-order'; +import { emptyPageData, PageData } from '@shared/models/page/page-data'; +import { getDescendantProp, isObject } from '@core/utils'; +import { SortDirection } from '@angular/material/sort'; + +export const MAX_SAFE_PAGE_SIZE = 2147483647; + +export type PageLinkSearchFunction = (entity: T, textSearch: string, searchProperty?: string) => boolean; + +export function defaultPageLinkSearchFunction(searchProperty?: string): PageLinkSearchFunction { + return (entity, textSearch) => defaultPageLinkSearch(entity, textSearch, searchProperty); +} + +const defaultPageLinkSearch: PageLinkSearchFunction = + (entity: any, textSearch: string, searchProperty?: string) => { + if (textSearch === null || !textSearch.length) { + return true; + } + const expected = ('' + textSearch).toLowerCase(); + if (searchProperty && searchProperty.length) { + if (Object.prototype.hasOwnProperty.call(entity, searchProperty)) { + const val = entity[searchProperty]; + if (val !== null) { + if (val !== Object(val)) { + const actual = ('' + val).toLowerCase(); + if (actual.indexOf(expected) !== -1) { + return true; + } + } + } + } + } else { + for (const key of Object.keys(entity)) { + const val = entity[key]; + if (val !== null) { + if (val !== Object(val)) { + const actual = ('' + val).toLowerCase(); + if (actual.indexOf(expected) !== -1) { + return true; + } + } else if (isObject(val)) { + if (defaultPageLinkSearch(val, textSearch)) { + return true; + } + } + } + } + } + return false; + }; + +export function sortItems(item1: any, item2: any, property: string, asc: boolean): number { + const item1Value = getDescendantProp(item1, property); + const item2Value = getDescendantProp(item2, property); + let result = 0; + if (item1Value !== item2Value) { + const item1Type = typeof item1Value; + const item2Type = typeof item2Value; + if (item1Type === 'number' && item2Type === 'number') { + result = item1Value - item2Value; + } else if (item1Type === 'string' && item2Type === 'string') { + result = item1Value.localeCompare(item2Value); + } else if ((item1Type === 'boolean' && item2Type === 'boolean') || (item1Type !== item2Type)) { + if (item1Value && !item2Value) { + result = 1; + } else if (!item1Value && item2Value) { + result = -1; + } + } + } + return asc ? result : result * -1; +} + +export class PageLink { + + textSearch: string; + pageSize: number; + page: number; + sortOrder: SortOrder; + + constructor(pageSize: number, page: number = 0, textSearch: string = null, sortOrder: SortOrder = null) { + this.textSearch = textSearch; + this.pageSize = pageSize; + this.page = page; + this.sortOrder = sortOrder; + } + + public nextPageLink(): PageLink { + return new PageLink(this.pageSize, this.page + 1, this.textSearch, this.sortOrder); + } + + public toQuery(): string { + let query = `?pageSize=${this.pageSize}&page=${this.page}`; + if (this.textSearch && this.textSearch.length) { + query += `&textSearch=${this.textSearch}`; + } + if (this.sortOrder) { + query += `&sortProperty=${this.sortOrder.property}&sortOrder=${this.sortOrder.direction}`; + } + return query; + } + + public sort(item1: any, item2: any): number { + if (this.sortOrder) { + const sortProperty = this.sortOrder.property; + const asc = this.sortOrder.direction === Direction.ASC; + return sortItems(item1, item2, sortProperty, asc); + } + return 0; + } + + public filterData(data: Array, + searchFunction: PageLinkSearchFunction = defaultPageLinkSearchFunction()): PageData { + const pageData = emptyPageData(); + pageData.data = [...data]; + if (this.textSearch && this.textSearch.length) { + pageData.data = pageData.data.filter((entity) => searchFunction(entity, this.textSearch)); + } + pageData.totalElements = pageData.data.length; + pageData.totalPages = this.pageSize === Number.POSITIVE_INFINITY ? 1 : Math.ceil(pageData.totalElements / this.pageSize); + if (this.sortOrder) { + const sortProperty = this.sortOrder.property; + const asc = this.sortOrder.direction === Direction.ASC; + pageData.data = pageData.data.sort((a, b) => sortItems(a, b, sortProperty, asc)); + } + if (this.pageSize !== Number.POSITIVE_INFINITY) { + const startIndex = this.pageSize * this.page; + pageData.data = pageData.data.slice(startIndex, startIndex + this.pageSize); + pageData.hasNext = pageData.totalElements > startIndex + pageData.data.length; + } + return pageData; + } + + public sortDirection(): SortDirection { + if (this.sortOrder) { + return (this.sortOrder.direction + '').toLowerCase() as SortDirection; + } else { + return '' as SortDirection; + } + } + +} + +export class TimePageLink extends PageLink { + + startTime: number; + endTime: number; + + constructor(pageSize: number, page: number = 0, textSearch: string = null, sortOrder: SortOrder = null, + startTime: number = null, endTime: number = null) { + super(pageSize, page, textSearch, sortOrder); + this.startTime = startTime; + this.endTime = endTime; + } + + public nextPageLink(): TimePageLink { + return new TimePageLink(this.pageSize, this.page + 1, this.textSearch, this.sortOrder, this.startTime, this.endTime); + } + + public toQuery(): string { + let query = super.toQuery(); + if (this.startTime) { + query += `&startTime=${this.startTime}`; + } + if (this.endTime) { + query += `&endTime=${this.endTime}`; + } + return query; + } +} diff --git a/ui-ngx/src/app/shared/models/page/public-api.ts b/ui-ngx/src/app/shared/models/page/public-api.ts new file mode 100644 index 0000000000..021d87cc05 --- /dev/null +++ b/ui-ngx/src/app/shared/models/page/public-api.ts @@ -0,0 +1,19 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export * from './page-data'; +export * from './page-link'; +export * from './sort-order'; diff --git a/ui-ngx/src/app/shared/models/page/sort-order.ts b/ui-ngx/src/app/shared/models/page/sort-order.ts new file mode 100644 index 0000000000..8c3bf99aff --- /dev/null +++ b/ui-ngx/src/app/shared/models/page/sort-order.ts @@ -0,0 +1,42 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + + +export interface SortOrder { + property: string; + direction: Direction; +} + +export enum Direction { + ASC = 'ASC', + DESC = 'DESC' +} + +export function sortOrderFromString(strSortOrder: string): SortOrder { + let property: string; + let direction = Direction.ASC; + if (strSortOrder.startsWith('-')) { + direction = Direction.DESC; + property = strSortOrder.substring(1); + } else { + if (strSortOrder.startsWith('+')) { + property = strSortOrder.substring(1); + } else { + property = strSortOrder; + } + } + return {property, direction}; +} diff --git a/ui-ngx/src/app/shared/models/public-api.ts b/ui-ngx/src/app/shared/models/public-api.ts new file mode 100644 index 0000000000..c2130236c1 --- /dev/null +++ b/ui-ngx/src/app/shared/models/public-api.ts @@ -0,0 +1,49 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export * from './id/public-api'; +export * from './page/public-api'; +export * from './telemetry/telemetry.models'; +export * from './time/time.models'; +export * from './alarm.models'; +export * from './alias.models'; +export * from './audit-log.models'; +export * from './authority.enum'; +export * from './base-data'; +export * from './component-descriptor.models'; +export * from './constants'; +export * from './contact-based.model'; +export * from './customer.model'; +export * from './dashboard.models'; +export * from './device.models'; +export * from './entity.models'; +export * from './entity-type.models'; +export * from './entity-view.models'; +export * from './error.models'; +export * from './event.models'; +export * from './login.models'; +export * from './material.models'; +export * from './oauth2.models'; +export * from './queue.models'; +export * from './relation.models'; +export * from './rule-chain.models'; +export * from './rule-node.models'; +export * from './settings.models'; +export * from './tenant.model'; +export * from './user.model'; +export * from './widget.models'; +export * from './widgets-bundle.model'; +export * from './window-message.model'; diff --git a/ui-ngx/src/app/shared/models/query/query.models.ts b/ui-ngx/src/app/shared/models/query/query.models.ts new file mode 100644 index 0000000000..ba86bce85b --- /dev/null +++ b/ui-ngx/src/app/shared/models/query/query.models.ts @@ -0,0 +1,823 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AliasFilterType, EntityFilters } from '@shared/models/alias.models'; +import { EntityId } from '@shared/models/id/entity-id'; +import { SortDirection } from '@angular/material/sort'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { EntityInfo } from '@shared/models/entity.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { DataKey, Datasource, DatasourceType } from '@shared/models/widget.models'; +import { PageData } from '@shared/models/page/page-data'; +import { isDefined, isEqual } from '@core/utils'; +import { TranslateService } from '@ngx-translate/core'; +import { AlarmInfo, AlarmSearchStatus, AlarmSeverity } from '../alarm.models'; +import { Filter } from '@material-ui/icons'; +import { DatePipe } from '@angular/common'; + +export enum EntityKeyType { + ATTRIBUTE = 'ATTRIBUTE', + CLIENT_ATTRIBUTE = 'CLIENT_ATTRIBUTE', + SHARED_ATTRIBUTE = 'SHARED_ATTRIBUTE', + SERVER_ATTRIBUTE = 'SERVER_ATTRIBUTE', + TIME_SERIES = 'TIME_SERIES', + ENTITY_FIELD = 'ENTITY_FIELD', + ALARM_FIELD = 'ALARM_FIELD' +} + +export const entityKeyTypeTranslationMap = new Map( + [ + [EntityKeyType.ATTRIBUTE, 'filter.key-type.attribute'], + [EntityKeyType.TIME_SERIES, 'filter.key-type.timeseries'], + [EntityKeyType.ENTITY_FIELD, 'filter.key-type.entity-field'] + ] +); + +export function entityKeyTypeToDataKeyType(entityKeyType: EntityKeyType): DataKeyType { + switch (entityKeyType) { + case EntityKeyType.ATTRIBUTE: + case EntityKeyType.CLIENT_ATTRIBUTE: + case EntityKeyType.SHARED_ATTRIBUTE: + case EntityKeyType.SERVER_ATTRIBUTE: + return DataKeyType.attribute; + case EntityKeyType.TIME_SERIES: + return DataKeyType.timeseries; + case EntityKeyType.ENTITY_FIELD: + return DataKeyType.entityField; + case EntityKeyType.ALARM_FIELD: + return DataKeyType.alarm; + } +} + +export function dataKeyTypeToEntityKeyType(dataKeyType: DataKeyType): EntityKeyType { + switch (dataKeyType) { + case DataKeyType.timeseries: + return EntityKeyType.TIME_SERIES; + case DataKeyType.attribute: + return EntityKeyType.ATTRIBUTE; + case DataKeyType.function: + return EntityKeyType.ENTITY_FIELD; + case DataKeyType.alarm: + return EntityKeyType.ALARM_FIELD; + case DataKeyType.entityField: + return EntityKeyType.ENTITY_FIELD; + } +} + +export interface EntityKey { + type: EntityKeyType; + key: string; +} + +export function dataKeyToEntityKey(dataKey: DataKey): EntityKey { + return { + key: dataKey.name, + type: dataKeyTypeToEntityKeyType(dataKey.type) + }; +} + +export enum EntityKeyValueType { + STRING = 'STRING', + NUMERIC = 'NUMERIC', + BOOLEAN = 'BOOLEAN', + DATE_TIME = 'DATE_TIME' +} + +export interface EntityKeyValueTypeData { + name: string; + icon: string; +} + +export const entityKeyValueTypesMap = new Map( + [ + [ + EntityKeyValueType.STRING, + { + name: 'filter.value-type.string', + icon: 'mdi:format-text' + } + ], + [ + EntityKeyValueType.NUMERIC, + { + name: 'filter.value-type.numeric', + icon: 'mdi:numeric' + } + ], + [ + EntityKeyValueType.BOOLEAN, + { + name: 'filter.value-type.boolean', + icon: 'mdi:checkbox-marked-outline' + } + ], + [ + EntityKeyValueType.DATE_TIME, + { + name: 'filter.value-type.date-time', + icon: 'mdi:calendar-clock' + } + ] + ] +); + +export function entityKeyValueTypeToFilterPredicateType(valueType: EntityKeyValueType): FilterPredicateType { + switch (valueType) { + case EntityKeyValueType.STRING: + return FilterPredicateType.STRING; + case EntityKeyValueType.NUMERIC: + case EntityKeyValueType.DATE_TIME: + return FilterPredicateType.NUMERIC; + case EntityKeyValueType.BOOLEAN: + return FilterPredicateType.BOOLEAN; + } +} + +export function createDefaultFilterPredicateInfo(valueType: EntityKeyValueType, complex: boolean): KeyFilterPredicateInfo { + const predicate = createDefaultFilterPredicate(valueType, complex); + return { + keyFilterPredicate: predicate, + userInfo: createDefaultFilterPredicateUserInfo() + }; +} + +export function createDefaultFilterPredicateUserInfo(): KeyFilterPredicateUserInfo { + return { + editable: true, + label: '', + autogeneratedLabel: true, + order: 0 + }; +} + +export function createDefaultFilterPredicate(valueType: EntityKeyValueType, complex: boolean): KeyFilterPredicate { + const predicate = { + type: complex ? FilterPredicateType.COMPLEX : entityKeyValueTypeToFilterPredicateType(valueType) + } as KeyFilterPredicate; + switch (predicate.type) { + case FilterPredicateType.STRING: + predicate.operation = StringOperation.STARTS_WITH; + predicate.value = { + defaultValue: '' + }; + predicate.ignoreCase = false; + break; + case FilterPredicateType.NUMERIC: + predicate.operation = NumericOperation.EQUAL; + predicate.value = { + defaultValue: valueType === EntityKeyValueType.DATE_TIME ? Date.now() : 0 + }; + break; + case FilterPredicateType.BOOLEAN: + predicate.operation = BooleanOperation.EQUAL; + predicate.value = { + defaultValue: false + }; + break; + case FilterPredicateType.COMPLEX: + predicate.operation = ComplexOperation.AND; + predicate.predicates = []; + break; + } + return predicate; +} + +export enum FilterPredicateType { + STRING = 'STRING', + NUMERIC = 'NUMERIC', + BOOLEAN = 'BOOLEAN', + COMPLEX = 'COMPLEX' +} + +export enum StringOperation { + EQUAL = 'EQUAL', + NOT_EQUAL = 'NOT_EQUAL', + STARTS_WITH = 'STARTS_WITH', + ENDS_WITH = 'ENDS_WITH', + CONTAINS = 'CONTAINS', + NOT_CONTAINS = 'NOT_CONTAINS' +} + +export const stringOperationTranslationMap = new Map( + [ + [StringOperation.EQUAL, 'filter.operation.equal'], + [StringOperation.NOT_EQUAL, 'filter.operation.not-equal'], + [StringOperation.STARTS_WITH, 'filter.operation.starts-with'], + [StringOperation.ENDS_WITH, 'filter.operation.ends-with'], + [StringOperation.CONTAINS, 'filter.operation.contains'], + [StringOperation.NOT_CONTAINS, 'filter.operation.not-contains'] + ] +); + +export enum NumericOperation { + EQUAL = 'EQUAL', + NOT_EQUAL = 'NOT_EQUAL', + GREATER = 'GREATER', + LESS = 'LESS', + GREATER_OR_EQUAL = 'GREATER_OR_EQUAL', + LESS_OR_EQUAL = 'LESS_OR_EQUAL' +} + +export const numericOperationTranslationMap = new Map( + [ + [NumericOperation.EQUAL, 'filter.operation.equal'], + [NumericOperation.NOT_EQUAL, 'filter.operation.not-equal'], + [NumericOperation.GREATER, 'filter.operation.greater'], + [NumericOperation.LESS, 'filter.operation.less'], + [NumericOperation.GREATER_OR_EQUAL, 'filter.operation.greater-or-equal'], + [NumericOperation.LESS_OR_EQUAL, 'filter.operation.less-or-equal'] + ] +); + +export enum BooleanOperation { + EQUAL = 'EQUAL', + NOT_EQUAL = 'NOT_EQUAL' +} + +export const booleanOperationTranslationMap = new Map( + [ + [BooleanOperation.EQUAL, 'filter.operation.equal'], + [BooleanOperation.NOT_EQUAL, 'filter.operation.not-equal'] + ] +); + +export enum ComplexOperation { + AND = 'AND', + OR = 'OR' +} + +export const complexOperationTranslationMap = new Map( + [ + [ComplexOperation.AND, 'filter.operation.and'], + [ComplexOperation.OR, 'filter.operation.or'] + ] +); + +export enum DynamicValueSourceType { + CURRENT_TENANT = 'CURRENT_TENANT', + CURRENT_CUSTOMER = 'CURRENT_CUSTOMER', + CURRENT_USER = 'CURRENT_USER', + CURRENT_DEVICE = 'CURRENT_DEVICE' +} + +export const dynamicValueSourceTypeTranslationMap = new Map( + [ + [DynamicValueSourceType.CURRENT_TENANT, 'filter.current-tenant'], + [DynamicValueSourceType.CURRENT_CUSTOMER, 'filter.current-customer'], + [DynamicValueSourceType.CURRENT_USER, 'filter.current-user'], + [DynamicValueSourceType.CURRENT_DEVICE, 'filter.current-device'] + ] +); + +export interface DynamicValue { + sourceType: DynamicValueSourceType; + sourceAttribute: string; +} + +export interface FilterPredicateValue { + defaultValue: T; + userValue?: T; + dynamicValue?: DynamicValue; +} + +export interface StringFilterPredicate { + type: FilterPredicateType.STRING; + operation: StringOperation; + value: FilterPredicateValue; + ignoreCase: boolean; +} + +export interface NumericFilterPredicate { + type: FilterPredicateType.NUMERIC; + operation: NumericOperation; + value: FilterPredicateValue; +} + +export interface BooleanFilterPredicate { + type: FilterPredicateType.BOOLEAN; + operation: BooleanOperation; + value: FilterPredicateValue; +} + +export interface BaseComplexFilterPredicate { + type: FilterPredicateType.COMPLEX; + operation: ComplexOperation; + predicates: Array; +} + +export type ComplexFilterPredicate = BaseComplexFilterPredicate; + +export type ComplexFilterPredicateInfo = BaseComplexFilterPredicate; + +export type KeyFilterPredicate = StringFilterPredicate | + NumericFilterPredicate | + BooleanFilterPredicate | + ComplexFilterPredicate | + ComplexFilterPredicateInfo; + +export interface KeyFilterPredicateUserInfo { + editable: boolean; + label: string; + autogeneratedLabel: boolean; + order?: number; +} + +export interface KeyFilterPredicateInfo { + keyFilterPredicate: KeyFilterPredicate; + userInfo: KeyFilterPredicateUserInfo; +} + +export interface KeyFilter { + key: EntityKey; + valueType: EntityKeyValueType; + predicate: KeyFilterPredicate; +} + +export interface KeyFilterInfo { + key: EntityKey; + valueType: EntityKeyValueType; + predicates: Array; +} + +export interface FilterInfo { + filter: string; + editable: boolean; + keyFilters: Array; +} + +export interface FiltersInfo { + datasourceFilters: {[datasourceIndex: number]: FilterInfo}; +} + +export function keyFiltersToText(translate: TranslateService, datePipe: DatePipe, keyFilters: Array): string { + const filtersText = keyFilters.map(keyFilter => + keyFilterToText(translate, datePipe, keyFilter, + keyFilters.length > 1 ? ComplexOperation.AND : undefined)); + let result: string; + if (filtersText.length > 1) { + const andText = translate.instant('filter.operation.and'); + result = filtersText.join(' ' + andText + ' '); + } else { + result = filtersText[0]; + } + return result; +} + +export function keyFilterToText(translate: TranslateService, datePipe: DatePipe, keyFilter: KeyFilter, + parentComplexOperation?: ComplexOperation): string { + const keyFilterPredicate = keyFilter.predicate; + return keyFilterPredicateToText(translate, datePipe, keyFilter, keyFilterPredicate, parentComplexOperation); +} + +export function keyFilterPredicateToText(translate: TranslateService, + datePipe: DatePipe, + keyFilter: KeyFilter, + keyFilterPredicate: KeyFilterPredicate, + parentComplexOperation?: ComplexOperation): string { + if (keyFilterPredicate.type === FilterPredicateType.COMPLEX) { + const complexPredicate = keyFilterPredicate as ComplexFilterPredicate; + const complexOperation = complexPredicate.operation; + const complexPredicatesText = + complexPredicate.predicates.map(predicate => keyFilterPredicateToText(translate, datePipe, keyFilter, predicate, complexOperation)); + if (complexPredicatesText.length > 1) { + const operationText = translate.instant(complexOperationTranslationMap.get(complexOperation)); + let result = complexPredicatesText.join(' ' + operationText + ' '); + if (complexOperation === ComplexOperation.OR && parentComplexOperation && ComplexOperation.OR !== parentComplexOperation) { + result = `(${result})`; + } + return result; + } else { + return complexPredicatesText[0]; + } + } else { + return simpleKeyFilterPredicateToText(translate, datePipe, keyFilter, keyFilterPredicate); + } +} + +function simpleKeyFilterPredicateToText(translate: TranslateService, + datePipe: DatePipe, + keyFilter: KeyFilter, + keyFilterPredicate: StringFilterPredicate | + NumericFilterPredicate | + BooleanFilterPredicate): string { + const key = keyFilter.key.key; + let operation: string; + let value: string; + const val = keyFilterPredicate.value; + const dynamicValue = !!val.dynamicValue && !!val.dynamicValue.sourceType; + if (dynamicValue) { + value = '' + + translate.instant(dynamicValueSourceTypeTranslationMap.get(val.dynamicValue.sourceType)) + ''; + value += '.' + val.dynamicValue.sourceAttribute + ''; + } + switch (keyFilterPredicate.type) { + case FilterPredicateType.STRING: + operation = translate.instant(stringOperationTranslationMap.get(keyFilterPredicate.operation)); + if (keyFilterPredicate.ignoreCase) { + operation += ' ' + translate.instant('filter.ignore-case'); + } + if (!dynamicValue) { + value = `'${keyFilterPredicate.value.defaultValue}'`; + } + break; + case FilterPredicateType.NUMERIC: + operation = translate.instant(numericOperationTranslationMap.get(keyFilterPredicate.operation)); + if (!dynamicValue) { + if (keyFilter.valueType === EntityKeyValueType.DATE_TIME) { + value = datePipe.transform(keyFilterPredicate.value.defaultValue, 'yyyy-MM-dd HH:mm'); + } else { + value = keyFilterPredicate.value.defaultValue + ''; + } + } + break; + case FilterPredicateType.BOOLEAN: + operation = translate.instant(booleanOperationTranslationMap.get(keyFilterPredicate.operation)); + value = translate.instant(keyFilterPredicate.value.defaultValue ? 'value.true' : 'value.false'); + break; + } + if (!dynamicValue) { + value = `${value}`; + } + return `${key} ${operation} ${value}`; +} + +export function keyFilterInfosToKeyFilters(keyFilterInfos: Array): Array { + if (!keyFilterInfos) { + return []; + } + const keyFilters: Array = []; + for (const keyFilterInfo of keyFilterInfos) { + const key = keyFilterInfo.key; + for (const predicate of keyFilterInfo.predicates) { + const keyFilter: KeyFilter = { + key, + valueType: keyFilterInfo.valueType, + predicate: keyFilterPredicateInfoToKeyFilterPredicate(predicate) + }; + keyFilters.push(keyFilter); + } + } + return keyFilters; +} + +export function keyFiltersToKeyFilterInfos(keyFilters: Array): Array { + const keyFilterInfos: Array = []; + const keyFilterInfoMap: {[infoKey: string]: KeyFilterInfo} = {}; + if (keyFilters) { + for (const keyFilter of keyFilters) { + const key = keyFilter.key; + const infoKey = key.key + key.type + keyFilter.valueType; + let keyFilterInfo = keyFilterInfoMap[infoKey]; + if (!keyFilterInfo) { + keyFilterInfo = { + key, + valueType: keyFilter.valueType, + predicates: [] + }; + keyFilterInfoMap[infoKey] = keyFilterInfo; + keyFilterInfos.push(keyFilterInfo); + } + if (keyFilter.predicate) { + keyFilterInfo.predicates.push(keyFilterPredicateToKeyFilterPredicateInfo(keyFilter.predicate)); + } + } + } + return keyFilterInfos; +} + +export function filterInfoToKeyFilters(filter: FilterInfo): Array { + const keyFilterInfos = filter.keyFilters; + const keyFilters: Array = []; + for (const keyFilterInfo of keyFilterInfos) { + const key = keyFilterInfo.key; + for (const predicate of keyFilterInfo.predicates) { + const keyFilter: KeyFilter = { + key, + valueType: keyFilterInfo.valueType, + predicate: keyFilterPredicateInfoToKeyFilterPredicate(predicate) + }; + keyFilters.push(keyFilter); + } + } + return keyFilters; +} + +export function keyFilterPredicateInfoToKeyFilterPredicate(keyFilterPredicateInfo: KeyFilterPredicateInfo): KeyFilterPredicate { + let keyFilterPredicate = keyFilterPredicateInfo.keyFilterPredicate; + if (keyFilterPredicate.type === FilterPredicateType.COMPLEX) { + const complexInfo = keyFilterPredicate as ComplexFilterPredicateInfo; + const predicates = complexInfo.predicates.map((predicateInfo => keyFilterPredicateInfoToKeyFilterPredicate(predicateInfo))); + keyFilterPredicate = { + type: FilterPredicateType.COMPLEX, + operation: complexInfo.operation, + predicates + } as ComplexFilterPredicate; + } + return keyFilterPredicate; +} + +export function keyFilterPredicateToKeyFilterPredicateInfo(keyFilterPredicate: KeyFilterPredicate): KeyFilterPredicateInfo { + const keyFilterPredicateInfo: KeyFilterPredicateInfo = { + keyFilterPredicate: null, + userInfo: null + }; + if (keyFilterPredicate.type === FilterPredicateType.COMPLEX) { + const complexPredicate = keyFilterPredicate as ComplexFilterPredicate; + const predicateInfos = complexPredicate.predicates.map( + predicate => keyFilterPredicateToKeyFilterPredicateInfo(predicate)); + keyFilterPredicateInfo.keyFilterPredicate = { + predicates: predicateInfos, + operation: complexPredicate.operation, + type: FilterPredicateType.COMPLEX + } as ComplexFilterPredicateInfo; + } else { + keyFilterPredicateInfo.keyFilterPredicate = keyFilterPredicate; + } + return keyFilterPredicateInfo; +} + +export function isFilterEditable(filter: FilterInfo): boolean { + if (filter.editable) { + return filter.keyFilters.some(value => isKeyFilterInfoEditable(value)); + } else { + return false; + } +} + +export function isKeyFilterInfoEditable(keyFilterInfo: KeyFilterInfo): boolean { + return keyFilterInfo.predicates.some(value => isPredicateInfoEditable(value)); +} + +export function isPredicateInfoEditable(predicateInfo: KeyFilterPredicateInfo): boolean { + if (predicateInfo.keyFilterPredicate.type === FilterPredicateType.COMPLEX) { + const complexFilterPredicateInfo: ComplexFilterPredicateInfo = predicateInfo.keyFilterPredicate as ComplexFilterPredicateInfo; + return complexFilterPredicateInfo.predicates.some(value => isPredicateInfoEditable(value)); + } else { + return predicateInfo.userInfo.editable; + } +} + +export interface UserFilterInputInfo { + label: string; + valueType: EntityKeyValueType; + info: KeyFilterPredicateInfo; +} + +export function filterToUserFilterInfoList(filter: Filter, translate: TranslateService): Array { + const result = filter.keyFilters.map((keyFilterInfo => keyFilterInfoToUserFilterInfoList(keyFilterInfo, translate))); + let userInputs: Array = [].concat.apply([], result); + userInputs = userInputs.sort((input1, input2) => { + const order1 = isDefined(input1.info.userInfo.order) ? input1.info.userInfo.order : 0; + const order2 = isDefined(input2.info.userInfo.order) ? input2.info.userInfo.order : 0; + return order1 - order2; + }); + return userInputs; +} + +export function keyFilterInfoToUserFilterInfoList(keyFilterInfo: KeyFilterInfo, translate: TranslateService): Array { + const result = keyFilterInfo.predicates.map((predicateInfo => predicateInfoToUserFilterInfoList(keyFilterInfo.key, + keyFilterInfo.valueType, predicateInfo, translate))); + return [].concat.apply([], result); +} + +export function predicateInfoToUserFilterInfoList(key: EntityKey, + valueType: EntityKeyValueType, + predicateInfo: KeyFilterPredicateInfo, + translate: TranslateService): Array { + if (predicateInfo.keyFilterPredicate.type === FilterPredicateType.COMPLEX) { + const complexFilterPredicateInfo: ComplexFilterPredicateInfo = predicateInfo.keyFilterPredicate as ComplexFilterPredicateInfo; + const result = complexFilterPredicateInfo.predicates.map((predicateInfo1 => + predicateInfoToUserFilterInfoList(key, valueType, predicateInfo1, translate))); + return [].concat.apply([], result); + } else { + if (predicateInfo.userInfo.editable) { + const userInput: UserFilterInputInfo = { + info: predicateInfo, + label: predicateInfo.userInfo.label, + valueType + }; + if (predicateInfo.userInfo.autogeneratedLabel) { + userInput.label = generateUserFilterValueLabel(key.key, valueType, + predicateInfo.keyFilterPredicate.operation, translate); + } + return [userInput]; + } else { + return []; + } + } +} + +export function generateUserFilterValueLabel(key: string, valueType: EntityKeyValueType, + operation: StringOperation | BooleanOperation | NumericOperation, + translate: TranslateService) { + let label = key; + let operationTranslationKey: string; + switch (valueType) { + case EntityKeyValueType.STRING: + operationTranslationKey = stringOperationTranslationMap.get(operation as StringOperation); + break; + case EntityKeyValueType.NUMERIC: + case EntityKeyValueType.DATE_TIME: + operationTranslationKey = numericOperationTranslationMap.get(operation as NumericOperation); + break; + case EntityKeyValueType.BOOLEAN: + operationTranslationKey = booleanOperationTranslationMap.get(operation as BooleanOperation); + break; + } + label += ' ' + translate.instant(operationTranslationKey); + return label; +} + +export interface Filter extends FilterInfo { + id: string; +} + +export interface Filters { + [id: string]: Filter; +} + +export interface EntityFilter extends EntityFilters { + type?: AliasFilterType; +} + +export enum Direction { + ASC = 'ASC', + DESC = 'DESC' +} + +export interface EntityDataSortOrder { + key: EntityKey; + direction: Direction; +} + +export interface EntityDataPageLink { + pageSize: number; + page: number; + textSearch?: string; + sortOrder?: EntityDataSortOrder; + dynamic?: boolean; +} + +export interface AlarmDataPageLink extends EntityDataPageLink { + startTs?: number; + endTs?: number; + timeWindow?: number; + typeList?: Array; + statusList?: Array; + severityList?: Array; + searchPropagatedAlarms?: boolean; +} + +export function entityDataPageLinkSortDirection(pageLink: EntityDataPageLink): SortDirection { + if (pageLink.sortOrder) { + return (pageLink.sortOrder.direction + '').toLowerCase() as SortDirection; + } else { + return '' as SortDirection; + } +} + +export function createDefaultEntityDataPageLink(pageSize: number): EntityDataPageLink { + return { + pageSize, + page: 0, + sortOrder: { + key: { + type: EntityKeyType.ENTITY_FIELD, + key: 'createdTime' + }, + direction: Direction.DESC + } + }; +} + +export const singleEntityDataPageLink: EntityDataPageLink = createDefaultEntityDataPageLink(1); +export const defaultEntityDataPageLink: EntityDataPageLink = createDefaultEntityDataPageLink(1024); + +export interface EntityCountQuery { + entityFilter: EntityFilter; +} + +export interface AbstractDataQuery extends EntityCountQuery { + pageLink: T; + entityFields?: Array; + latestValues?: Array; + keyFilters?: Array; +} + +export interface EntityDataQuery extends AbstractDataQuery { +} + +export interface AlarmDataQuery extends AbstractDataQuery { + alarmFields?: Array; +} + +export interface TsValue { + ts: number; + value: string; +} + +export interface EntityData { + entityId: EntityId; + latest: {[entityKeyType: string]: {[key: string]: TsValue}}; + timeseries: {[key: string]: Array}; +} + +export interface AlarmData extends AlarmInfo { + entityId: string; + latest: {[entityKeyType: string]: {[key: string]: TsValue}}; +} + +export function entityPageDataChanged(prevPageData: PageData, nextPageData: PageData): boolean { + const prevIds = prevPageData.data.map((entityData) => entityData.entityId.id); + const nextIds = nextPageData.data.map((entityData) => entityData.entityId.id); + return !isEqual(prevIds, nextIds); +} + +export const entityInfoFields: EntityKey[] = [ + { + type: EntityKeyType.ENTITY_FIELD, + key: 'name' + }, + { + type: EntityKeyType.ENTITY_FIELD, + key: 'label' + }, + { + type: EntityKeyType.ENTITY_FIELD, + key: 'additionalInfo' + } +]; + +export function entityDataToEntityInfo(entityData: EntityData): EntityInfo { + const entityInfo: EntityInfo = { + id: entityData.entityId.id, + entityType: entityData.entityId.entityType as EntityType + }; + if (entityData.latest && entityData.latest[EntityKeyType.ENTITY_FIELD]) { + const fields = entityData.latest[EntityKeyType.ENTITY_FIELD]; + if (fields.name) { + entityInfo.name = fields.name.value; + } else { + entityInfo.name = ''; + } + if (fields.label) { + entityInfo.label = fields.label.value; + } else { + entityInfo.label = ''; + } + entityInfo.entityDescription = ''; + if (fields.additionalInfo) { + const additionalInfo = fields.additionalInfo.value; + if (additionalInfo && additionalInfo.length) { + try { + const additionalInfoJson = JSON.parse(additionalInfo); + if (additionalInfoJson && additionalInfoJson.description) { + entityInfo.entityDescription = additionalInfoJson.description; + } + } catch (e) {} + } + } + } + return entityInfo; +} + +export function updateDatasourceFromEntityInfo(datasource: Datasource, entity: EntityInfo, createFilter = false) { + datasource.entity = { + id: { + entityType: entity.entityType, + id: entity.id + } + }; + datasource.entityId = entity.id; + datasource.entityType = entity.entityType; + if (datasource.type === DatasourceType.entity) { + datasource.entityName = entity.name; + datasource.entityLabel = entity.label; + datasource.name = entity.name; + datasource.entityDescription = entity.entityDescription; + datasource.entity.label = entity.label; + datasource.entity.name = entity.name; + if (createFilter) { + datasource.entityFilter = { + type: AliasFilterType.singleEntity, + singleEntity: { + id: entity.id, + entityType: entity.entityType + } + }; + } + } +} diff --git a/ui-ngx/src/app/shared/models/queue.models.ts b/ui-ngx/src/app/shared/models/queue.models.ts new file mode 100644 index 0000000000..ff8f386dbe --- /dev/null +++ b/ui-ngx/src/app/shared/models/queue.models.ts @@ -0,0 +1,22 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export enum ServiceType { + TB_CORE = 'TB_CORE', + TB_RULE_ENGINE = 'TB_RULE_ENGINE', + TB_TRANSPORT = 'TB_TRANSPORT', + JS_EXECUTOR = 'JS_EXECUTOR' +} diff --git a/ui-ngx/src/app/shared/models/relation.models.ts b/ui-ngx/src/app/shared/models/relation.models.ts new file mode 100644 index 0000000000..dfb0b1d671 --- /dev/null +++ b/ui-ngx/src/app/shared/models/relation.models.ts @@ -0,0 +1,92 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export const CONTAINS_TYPE = 'Contains'; +export const MANAGES_TYPE = 'Manages'; + +export const RelationTypes = [ + CONTAINS_TYPE, + MANAGES_TYPE +]; + +export enum RelationTypeGroup { + COMMON = 'COMMON', + ALARM = 'ALARM', + DASHBOARD = 'DASHBOARD', + RULE_CHAIN = 'RULE_CHAIN', + RULE_NODE = 'RULE_NODE', +} + +export enum EntitySearchDirection { + FROM = 'FROM', + TO = 'TO' +} + +export const entitySearchDirectionTranslations = new Map( + [ + [EntitySearchDirection.FROM, 'relation.search-direction.FROM'], + [EntitySearchDirection.TO, 'relation.search-direction.TO'], + ] +); + +export const directionTypeTranslations = new Map( + [ + [EntitySearchDirection.FROM, 'relation.direction-type.FROM'], + [EntitySearchDirection.TO, 'relation.direction-type.TO'], + ] +); + +export interface EntityTypeFilter { + relationType: string; + entityTypes: Array; +} + +export interface RelationsSearchParameters { + rootId: string; + rootType: EntityType; + direction: EntitySearchDirection; + relationTypeGroup?: RelationTypeGroup; + maxLevel?: number; + fetchLastLevelOnly?: boolean; +} + +export interface EntityRelationsQuery { + parameters: RelationsSearchParameters; + filters: Array; +} + +export interface EntitySearchQuery { + parameters: RelationsSearchParameters; + relationType: string; +} + +export interface EntityRelation { + from: EntityId; + to: EntityId; + type: string; + typeGroup: RelationTypeGroup; + additionalInfo?: any; +} + +export interface EntityRelationInfo extends EntityRelation { + fromName: string; + toEntityTypeName?: string; + toName: string; + fromEntityTypeName?: string; +} diff --git a/ui-ngx/src/app/shared/models/rule-chain.models.ts b/ui-ngx/src/app/shared/models/rule-chain.models.ts new file mode 100644 index 0000000000..6e145a6b5b --- /dev/null +++ b/ui-ngx/src/app/shared/models/rule-chain.models.ts @@ -0,0 +1,112 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { BaseData } from '@shared/models/base-data'; +import { TenantId } from '@shared/models/id/tenant-id'; +import { RuleChainId } from '@shared/models/id/rule-chain-id'; +import { RuleNodeId } from '@shared/models/id/rule-node-id'; +import { RuleNode, RuleNodeComponentDescriptor, RuleNodeType } from '@shared/models/rule-node.models'; +import { ComponentType } from '@shared/models/component-descriptor.models'; + +export interface RuleChain extends BaseData { + tenantId: TenantId; + name: string; + firstRuleNodeId: RuleNodeId; + root: boolean; + debugMode: boolean; + configuration?: any; + additionalInfo?: any; +} + +export interface RuleChainMetaData { + ruleChainId: RuleChainId; + firstNodeIndex?: number; + nodes: Array; + connections: Array; + ruleChainConnections: Array; +} + +export interface ResolvedRuleChainMetaData extends RuleChainMetaData { + targetRuleChainsMap: {[ruleChainId: string]: RuleChain}; +} + +export interface RuleChainImport { + ruleChain: RuleChain; + metadata: RuleChainMetaData; + resolvedMetadata?: ResolvedRuleChainMetaData; +} + +export interface NodeConnectionInfo { + fromIndex: number; + toIndex: number; + type: string; +} + +export interface RuleChainConnectionInfo { + fromIndex: number; + targetRuleChainId: RuleChainId; + additionalInfo: any; + type: string; +} + +export const ruleNodeTypeComponentTypes: ComponentType[] = + [ + ComponentType.FILTER, + ComponentType.ENRICHMENT, + ComponentType.TRANSFORMATION, + ComponentType.ACTION, + ComponentType.EXTERNAL + ]; + +export const ruleChainNodeComponent: RuleNodeComponentDescriptor = { + type: RuleNodeType.RULE_CHAIN, + name: 'rule chain', + clazz: 'tb.internal.RuleChain', + configurationDescriptor: { + nodeDefinition: { + description: '', + details: 'Forwards incoming messages to specified Rule Chain', + inEnabled: true, + outEnabled: false, + relationTypes: [], + customRelations: false, + defaultConfiguration: {} + } + } +}; + +export const unknownNodeComponent: RuleNodeComponentDescriptor = { + type: RuleNodeType.UNKNOWN, + name: 'unknown', + clazz: 'tb.internal.Unknown', + configurationDescriptor: { + nodeDefinition: { + description: '', + details: '', + inEnabled: true, + outEnabled: true, + relationTypes: [], + customRelations: false, + defaultConfiguration: {} + } + } +}; + +export const inputNodeComponent: RuleNodeComponentDescriptor = { + type: RuleNodeType.INPUT, + name: 'Input', + clazz: 'tb.internal.Input' +}; diff --git a/ui-ngx/src/app/shared/models/rule-node.models.ts b/ui-ngx/src/app/shared/models/rule-node.models.ts new file mode 100644 index 0000000000..b9494fb1c4 --- /dev/null +++ b/ui-ngx/src/app/shared/models/rule-node.models.ts @@ -0,0 +1,435 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { BaseData } from '@shared/models/base-data'; +import { RuleChainId } from '@shared/models/id/rule-chain-id'; +import { RuleNodeId } from '@shared/models/id/rule-node-id'; +import { ComponentDescriptor } from '@shared/models/component-descriptor.models'; +import { FcEdge, FcNode } from 'ngx-flowchart/dist/ngx-flowchart'; +import { Observable } from 'rxjs'; +import { PageComponent } from '@shared/components/page.component'; +import { AfterViewInit, EventEmitter, Inject, OnInit, Directive } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { AbstractControl, FormGroup } from '@angular/forms'; + +export interface RuleNodeConfiguration { + [key: string]: any; +} + +export interface RuleNode extends BaseData { + ruleChainId?: RuleChainId; + type: string; + name: string; + debugMode: boolean; + configuration: RuleNodeConfiguration; + additionalInfo?: any; +} + +export interface LinkLabel { + name: string; + value: string; +} + +export interface RuleNodeDefinition { + description: string; + details: string; + inEnabled: boolean; + outEnabled: boolean; + relationTypes: string[]; + customRelations: boolean; + defaultConfiguration: RuleNodeConfiguration; + icon?: string; + iconUrl?: string; + docUrl?: string; + uiResources?: string[]; + uiResourceLoadError?: string; + configDirective?: string; +} + +export interface RuleNodeConfigurationDescriptor { + nodeDefinition: RuleNodeDefinition; +} + +export interface IRuleNodeConfigurationComponent { + ruleNodeId: string; + configuration: RuleNodeConfiguration; + configurationChanged: Observable; + validate(); + [key: string]: any; +} + +@Directive() +export abstract class RuleNodeConfigurationComponent extends PageComponent implements + IRuleNodeConfigurationComponent, OnInit, AfterViewInit { + + ruleNodeId: string; + + configurationValue: RuleNodeConfiguration; + + private configurationSet = false; + + set configuration(value: RuleNodeConfiguration) { + this.configurationValue = value; + if (!this.configurationSet) { + this.configurationSet = true; + this.setupConfiguration(value); + } else { + this.updateConfiguration(value); + } + } + + get configuration(): RuleNodeConfiguration { + return this.configurationValue; + } + + configurationChangedEmiter = new EventEmitter(); + configurationChanged = this.configurationChangedEmiter.asObservable(); + + protected constructor(@Inject(Store) protected store: Store) { + super(store); + } + + ngOnInit() {} + + ngAfterViewInit(): void { + setTimeout(() => { + if (!this.validateConfig()) { + this.configurationChangedEmiter.emit(null); + } + }, 0); + } + + validate() { + this.onValidate(); + } + + protected setupConfiguration(configuration: RuleNodeConfiguration) { + this.onConfigurationSet(this.prepareInputConfig(configuration)); + this.updateValidators(false); + for (const trigger of this.validatorTriggers()) { + const path = trigger.split('.'); + let control: AbstractControl = this.configForm(); + for (const part of path) { + control = control.get(part); + } + control.valueChanges.subscribe(() => { + this.updateValidators(true, trigger); + }); + } + this.configForm().valueChanges.subscribe((updated: RuleNodeConfiguration) => { + this.onConfigurationChanged(updated); + }); + } + + protected updateConfiguration(configuration: RuleNodeConfiguration) { + this.configForm().reset(this.prepareInputConfig(configuration), {emitEvent: false}); + this.updateValidators(false); + } + + protected updateValidators(emitEvent: boolean, trigger?: string) { + } + + protected validatorTriggers(): string[] { + return []; + } + + protected onConfigurationChanged(updated: RuleNodeConfiguration) { + this.configurationValue = updated; + if (this.validateConfig()) { + this.configurationChangedEmiter.emit(this.prepareOutputConfig(updated)); + } else { + this.configurationChangedEmiter.emit(null); + } + } + + protected prepareInputConfig(configuration: RuleNodeConfiguration): RuleNodeConfiguration { + return configuration; + } + + protected prepareOutputConfig(configuration: RuleNodeConfiguration): RuleNodeConfiguration { + return configuration; + } + + protected validateConfig(): boolean { + return this.configForm().valid; + } + + protected onValidate() {} + + protected abstract configForm(): FormGroup; + + protected abstract onConfigurationSet(configuration: RuleNodeConfiguration); + +} + + +export enum RuleNodeType { + FILTER = 'FILTER', + ENRICHMENT = 'ENRICHMENT', + TRANSFORMATION = 'TRANSFORMATION', + ACTION = 'ACTION', + EXTERNAL = 'EXTERNAL', + RULE_CHAIN = 'RULE_CHAIN', + UNKNOWN = 'UNKNOWN', + INPUT = 'INPUT' +} + +export const ruleNodeTypesLibrary = [ + RuleNodeType.FILTER, + RuleNodeType.ENRICHMENT, + RuleNodeType.TRANSFORMATION, + RuleNodeType.ACTION, + RuleNodeType.EXTERNAL, + RuleNodeType.RULE_CHAIN, +]; + +export interface RuleNodeTypeDescriptor { + value: RuleNodeType; + name: string; + details: string; + nodeClass: string; + icon: string; + special?: boolean; +} + +export const ruleNodeTypeDescriptors = new Map( + [ + [ + RuleNodeType.FILTER, + { + value: RuleNodeType.FILTER, + name: 'rulenode.type-filter', + details: 'rulenode.type-filter-details', + nodeClass: 'tb-filter-type', + icon: 'filter_list' + } + ], + [ + RuleNodeType.ENRICHMENT, + { + value: RuleNodeType.ENRICHMENT, + name: 'rulenode.type-enrichment', + details: 'rulenode.type-enrichment-details', + nodeClass: 'tb-enrichment-type', + icon: 'playlist_add' + } + ], + [ + RuleNodeType.TRANSFORMATION, + { + value: RuleNodeType.TRANSFORMATION, + name: 'rulenode.type-transformation', + details: 'rulenode.type-transformation-details', + nodeClass: 'tb-transformation-type', + icon: 'transform' + } + ], + [ + RuleNodeType.ACTION, + { + value: RuleNodeType.ACTION, + name: 'rulenode.type-action', + details: 'rulenode.type-action-details', + nodeClass: 'tb-action-type', + icon: 'flash_on' + } + ], + [ + RuleNodeType.EXTERNAL, + { + value: RuleNodeType.EXTERNAL, + name: 'rulenode.type-external', + details: 'rulenode.type-external-details', + nodeClass: 'tb-external-type', + icon: 'cloud_upload' + } + ], + [ + RuleNodeType.RULE_CHAIN, + { + value: RuleNodeType.RULE_CHAIN, + name: 'rulenode.type-rule-chain', + details: 'rulenode.type-rule-chain-details', + nodeClass: 'tb-rule-chain-type', + icon: 'settings_ethernet' + } + ], + [ + RuleNodeType.INPUT, + { + value: RuleNodeType.INPUT, + name: 'rulenode.type-input', + details: 'rulenode.type-input-details', + nodeClass: 'tb-input-type', + icon: 'input', + special: true + } + ], + [ + RuleNodeType.UNKNOWN, + { + value: RuleNodeType.UNKNOWN, + name: 'rulenode.type-unknown', + details: 'rulenode.type-unknown-details', + nodeClass: 'tb-unknown-type', + icon: 'help_outline' + } + ] + ] +); + +export interface RuleNodeComponentDescriptor extends ComponentDescriptor { + type: RuleNodeType; + configurationDescriptor?: RuleNodeConfigurationDescriptor; +} + +export interface FcRuleNodeType extends FcNode { + component?: RuleNodeComponentDescriptor; + nodeClass?: string; + icon?: string; + iconUrl?: string; +} + +export interface FcRuleNode extends FcRuleNodeType { + ruleNodeId?: RuleNodeId; + additionalInfo?: any; + configuration?: RuleNodeConfiguration; + debugMode?: boolean; + targetRuleChainId?: string; + error?: string; + highlighted?: boolean; + componentClazz?: string; +} + +export interface FcRuleEdge extends FcEdge { + labels?: string[]; +} + +export interface TestScriptInputParams { + script: string; + scriptType: string; + argNames: string[]; + msg: string; + metadata: {[key: string]: string}; + msgType: string; +} + +export interface TestScriptResult { + output: string; + error: string; +} + +export enum MessageType { + POST_ATTRIBUTES_REQUEST = 'POST_ATTRIBUTES_REQUEST', + POST_TELEMETRY_REQUEST = 'POST_TELEMETRY_REQUEST', + TO_SERVER_RPC_REQUEST = 'TO_SERVER_RPC_REQUEST', + RPC_CALL_FROM_SERVER_TO_DEVICE = 'RPC_CALL_FROM_SERVER_TO_DEVICE', + ACTIVITY_EVENT = 'ACTIVITY_EVENT', + INACTIVITY_EVENT = 'INACTIVITY_EVENT', + CONNECT_EVENT = 'CONNECT_EVENT', + DISCONNECT_EVENT = 'DISCONNECT_EVENT', + ENTITY_CREATED = 'ENTITY_CREATED', + ENTITY_UPDATED = 'ENTITY_UPDATED', + ENTITY_DELETED = 'ENTITY_DELETED', + ENTITY_ASSIGNED = 'ENTITY_ASSIGNED', + ENTITY_UNASSIGNED = 'ENTITY_UNASSIGNED', + ATTRIBUTES_UPDATED = 'ATTRIBUTES_UPDATED', + ATTRIBUTES_DELETED = 'ATTRIBUTES_DELETED' +} + +export const messageTypeNames = new Map( + [ + [MessageType.POST_ATTRIBUTES_REQUEST, 'Post attributes'], + [MessageType.POST_TELEMETRY_REQUEST, 'Post telemetry'], + [MessageType.TO_SERVER_RPC_REQUEST, 'RPC Request from Device'], + [MessageType.RPC_CALL_FROM_SERVER_TO_DEVICE, 'RPC Request to Device'], + [MessageType.ACTIVITY_EVENT, 'Activity Event'], + [MessageType.INACTIVITY_EVENT, 'Inactivity Event'], + [MessageType.CONNECT_EVENT, 'Connect Event'], + [MessageType.DISCONNECT_EVENT, 'Disconnect Event'], + [MessageType.ENTITY_CREATED, 'Entity Created'], + [MessageType.ENTITY_UPDATED, 'Entity Updated'], + [MessageType.ENTITY_DELETED, 'Entity Deleted'], + [MessageType.ENTITY_ASSIGNED, 'Entity Assigned'], + [MessageType.ENTITY_UNASSIGNED, 'Entity Unassigned'], + [MessageType.ATTRIBUTES_UPDATED, 'Attributes Updated'], + [MessageType.ATTRIBUTES_DELETED, 'Attributes Deleted'] + ] +); + +const ruleNodeClazzHelpLinkMap = { + 'org.thingsboard.rule.engine.filter.TbCheckRelationNode': 'ruleNodeCheckRelation', + 'org.thingsboard.rule.engine.filter.TbCheckMessageNode': 'ruleNodeCheckExistenceFields', + 'org.thingsboard.rule.engine.geo.TbGpsGeofencingFilterNode': 'ruleNodeGpsGeofencingFilter', + 'org.thingsboard.rule.engine.filter.TbJsFilterNode': 'ruleNodeJsFilter', + 'org.thingsboard.rule.engine.filter.TbJsSwitchNode': 'ruleNodeJsSwitch', + 'org.thingsboard.rule.engine.filter.TbMsgTypeFilterNode': 'ruleNodeMessageTypeFilter', + 'org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode': 'ruleNodeMessageTypeSwitch', + 'org.thingsboard.rule.engine.filter.TbOriginatorTypeFilterNode': 'ruleNodeOriginatorTypeFilter', + 'org.thingsboard.rule.engine.filter.TbOriginatorTypeSwitchNode': 'ruleNodeOriginatorTypeSwitch', + 'org.thingsboard.rule.engine.metadata.TbGetAttributesNode': 'ruleNodeOriginatorAttributes', + 'org.thingsboard.rule.engine.metadata.TbGetOriginatorFieldsNode': 'ruleNodeOriginatorFields', + 'org.thingsboard.rule.engine.metadata.TbGetTelemetryNode': 'ruleNodeOriginatorTelemetry', + 'org.thingsboard.rule.engine.metadata.TbGetCustomerAttributeNode': 'ruleNodeCustomerAttributes', + 'org.thingsboard.rule.engine.metadata.TbGetCustomerDetailsNode': 'ruleNodeCustomerDetails', + 'org.thingsboard.rule.engine.metadata.TbGetDeviceAttrNode': 'ruleNodeDeviceAttributes', + 'org.thingsboard.rule.engine.metadata.TbGetRelatedAttributeNode': 'ruleNodeRelatedAttributes', + 'org.thingsboard.rule.engine.metadata.TbGetTenantAttributeNode': 'ruleNodeTenantAttributes', + 'org.thingsboard.rule.engine.metadata.TbGetTenantDetailsNode': 'ruleNodeTenantDetails', + 'org.thingsboard.rule.engine.transform.TbChangeOriginatorNode': 'ruleNodeChangeOriginator', + 'org.thingsboard.rule.engine.transform.TbTransformMsgNode': 'ruleNodeTransformMsg', + 'org.thingsboard.rule.engine.mail.TbMsgToEmailNode': 'ruleNodeMsgToEmail', + 'org.thingsboard.rule.engine.action.TbAssignToCustomerNode': 'ruleNodeAssignToCustomer', + 'org.thingsboard.rule.engine.action.TbUnassignFromCustomerNode': 'ruleNodeUnassignFromCustomer', + 'org.thingsboard.rule.engine.action.TbClearAlarmNode': 'ruleNodeClearAlarm', + 'org.thingsboard.rule.engine.action.TbCreateAlarmNode': 'ruleNodeCreateAlarm', + 'org.thingsboard.rule.engine.action.TbCreateRelationNode': 'ruleNodeCreateRelation', + 'org.thingsboard.rule.engine.action.TbDeleteRelationNode': 'ruleNodeDeleteRelation', + 'org.thingsboard.rule.engine.delay.TbMsgDelayNode': 'ruleNodeMsgDelay', + 'org.thingsboard.rule.engine.debug.TbMsgGeneratorNode': 'ruleNodeMsgGenerator', + 'org.thingsboard.rule.engine.geo.TbGpsGeofencingActionNode': 'ruleNodeGpsGeofencingEvents', + 'org.thingsboard.rule.engine.action.TbLogNode': 'ruleNodeLog', + 'org.thingsboard.rule.engine.rpc.TbSendRPCReplyNode': 'ruleNodeRpcCallReply', + 'org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode': 'ruleNodeRpcCallRequest', + 'org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode': 'ruleNodeSaveAttributes', + 'org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode': 'ruleNodeSaveTimeseries', + 'org.thingsboard.rule.engine.action.TbSaveToCustomCassandraTableNode': 'ruleNodeSaveToCustomTable', + 'tb.internal.RuleChain': 'ruleNodeRuleChain', + 'org.thingsboard.rule.engine.aws.sns.TbSnsNode': 'ruleNodeAwsSns', + 'org.thingsboard.rule.engine.aws.sqs.TbSqsNode': 'ruleNodeAwsSqs', + 'org.thingsboard.rule.engine.kafka.TbKafkaNode': 'ruleNodeKafka', + 'org.thingsboard.rule.engine.mqtt.TbMqttNode': 'ruleNodeMqtt', + 'org.thingsboard.rule.engine.mqtt.azure.TbAzureIotHubNode': 'ruleNodeAzureIotHub', + 'org.thingsboard.rule.engine.rabbitmq.TbRabbitMqNode': 'ruleNodeRabbitMq', + 'org.thingsboard.rule.engine.rest.TbRestApiCallNode': 'ruleNodeRestApiCall', + 'org.thingsboard.rule.engine.mail.TbSendEmailNode': 'ruleNodeSendEmail' +}; + +export function getRuleNodeHelpLink(component: RuleNodeComponentDescriptor): string { + if (component) { + if (component.configurationDescriptor && + component.configurationDescriptor.nodeDefinition && + component.configurationDescriptor.nodeDefinition.docUrl) { + return component.configurationDescriptor.nodeDefinition.docUrl; + } else if (component.clazz) { + if (ruleNodeClazzHelpLinkMap[component.clazz]) { + return ruleNodeClazzHelpLinkMap[component.clazz]; + } + } + } + return 'ruleEngine'; +} diff --git a/ui-ngx/src/app/shared/models/settings.models.ts b/ui-ngx/src/app/shared/models/settings.models.ts new file mode 100644 index 0000000000..f71ac1bb2e --- /dev/null +++ b/ui-ngx/src/app/shared/models/settings.models.ts @@ -0,0 +1,62 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export const smtpPortPattern: RegExp = /^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/; + +export interface AdminSettings { + key: string; + jsonValue: T; +} + +export declare type SmtpProtocol = 'smtp' | 'smtps'; + +export interface MailServerSettings { + mailFrom: string; + smtpProtocol: SmtpProtocol; + smtpHost: string; + smtpPort: number; + timeout: number; + enableTls: boolean; + username: string; + password: string; + enableProxy: boolean; + proxyHost: string; + proxyPort: number; + proxyUser: string; + proxyPassword: string; +} + +export interface GeneralSettings { + baseUrl: string; +} + +export interface UserPasswordPolicy { + minimumLength: number; + minimumUppercaseLetters: number; + minimumLowercaseLetters: number; + minimumDigits: number; + minimumSpecialCharacters: number; + passwordExpirationPeriodDays: number; +} + +export interface SecuritySettings { + passwordPolicy: UserPasswordPolicy; +} + +export interface UpdateMessage { + message: string; + updateAvailable: boolean; +} diff --git a/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts b/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts new file mode 100644 index 0000000000..aa4fb51fd1 --- /dev/null +++ b/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts @@ -0,0 +1,526 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + + +import { EntityType } from '@shared/models/entity-type.models'; +import { AggregationType } from '../time/time.models'; +import { Observable, ReplaySubject, Subject } from 'rxjs'; +import { EntityId } from '@shared/models/id/entity-id'; +import { map } from 'rxjs/operators'; +import { NgZone } from '@angular/core'; +import { + AlarmData, + AlarmDataQuery, + EntityData, + EntityDataQuery, + EntityKey, + TsValue +} from '@shared/models/query/query.models'; +import { PageData } from '@shared/models/page/page-data'; + +export enum DataKeyType { + timeseries = 'timeseries', + attribute = 'attribute', + function = 'function', + alarm = 'alarm', + entityField = 'entityField' +} + +export enum LatestTelemetry { + LATEST_TELEMETRY = 'LATEST_TELEMETRY' +} + +export enum AttributeScope { + CLIENT_SCOPE = 'CLIENT_SCOPE', + SERVER_SCOPE = 'SERVER_SCOPE', + SHARED_SCOPE = 'SHARED_SCOPE' +} + +export enum TelemetryFeature { + ATTRIBUTES = 'ATTRIBUTES', + TIMESERIES = 'TIMESERIES' +} + +export type TelemetryType = LatestTelemetry | AttributeScope; + +export function toTelemetryType(val: string): TelemetryType { + if (LatestTelemetry[val]) { + return LatestTelemetry[val]; + } else { + return AttributeScope[val]; + } +} + +export const telemetryTypeTranslations = new Map( + [ + [LatestTelemetry.LATEST_TELEMETRY, 'attribute.scope-latest-telemetry'], + [AttributeScope.CLIENT_SCOPE, 'attribute.scope-client'], + [AttributeScope.SERVER_SCOPE, 'attribute.scope-server'], + [AttributeScope.SHARED_SCOPE, 'attribute.scope-shared'] + ] +); + +export const isClientSideTelemetryType = new Map( + [ + [LatestTelemetry.LATEST_TELEMETRY, true], + [AttributeScope.CLIENT_SCOPE, true], + [AttributeScope.SERVER_SCOPE, false], + [AttributeScope.SHARED_SCOPE, false] + ] +); + +export interface AttributeData { + lastUpdateTs?: number; + key: string; + value: any; +} + +export interface TimeseriesData { + [key: string]: Array; +} + +export enum DataSortOrder { + ASC = 'ASC', + DESC = 'DESC' +} + +export interface WebsocketCmd { + cmdId: number; +} + +export interface TelemetryPluginCmd extends WebsocketCmd { + keys: string; +} + +export abstract class SubscriptionCmd implements TelemetryPluginCmd { + cmdId: number; + keys: string; + entityType: EntityType; + entityId: string; + scope?: AttributeScope; + unsubscribe: boolean; + abstract getType(): TelemetryFeature; +} + +export class AttributesSubscriptionCmd extends SubscriptionCmd { + getType() { + return TelemetryFeature.ATTRIBUTES; + } +} + +export class TimeseriesSubscriptionCmd extends SubscriptionCmd { + startTs: number; + timeWindow: number; + interval: number; + limit: number; + agg: AggregationType; + + getType() { + return TelemetryFeature.TIMESERIES; + } +} + +export class GetHistoryCmd implements TelemetryPluginCmd { + cmdId: number; + keys: string; + entityType: EntityType; + entityId: string; + startTs: number; + endTs: number; + interval: number; + limit: number; + agg: AggregationType; +} + +export interface EntityHistoryCmd { + keys: Array; + startTs: number; + endTs: number; + interval: number; + limit: number; + agg: AggregationType; + fetchLatestPreviousPoint?: boolean; +} + +export interface LatestValueCmd { + keys: Array; +} + +export interface TimeSeriesCmd { + keys: Array; + startTs: number; + timeWindow: number; + interval: number; + limit: number; + agg: AggregationType; + fetchLatestPreviousPoint?: boolean; +} + +export class EntityDataCmd implements WebsocketCmd { + cmdId: number; + query?: EntityDataQuery; + historyCmd?: EntityHistoryCmd; + latestCmd?: LatestValueCmd; + tsCmd?: TimeSeriesCmd; + + public isEmpty(): boolean { + return !this.query && !this.historyCmd && !this.latestCmd && !this.tsCmd; + } +} + +export class AlarmDataCmd implements WebsocketCmd { + cmdId: number; + query?: AlarmDataQuery; + + public isEmpty(): boolean { + return !this.query; + } +} + +export class EntityDataUnsubscribeCmd implements WebsocketCmd { + cmdId: number; +} + +export class AlarmDataUnsubscribeCmd implements WebsocketCmd { + cmdId: number; +} + +export class TelemetryPluginCmdsWrapper { + attrSubCmds: Array; + tsSubCmds: Array; + historyCmds: Array; + entityDataCmds: Array; + entityDataUnsubscribeCmds: Array; + alarmDataCmds: Array; + alarmDataUnsubscribeCmds: Array; + + constructor() { + this.attrSubCmds = []; + this.tsSubCmds = []; + this.historyCmds = []; + this.entityDataCmds = []; + this.entityDataUnsubscribeCmds = []; + this.alarmDataCmds = []; + this.alarmDataUnsubscribeCmds = []; + } + + public hasCommands(): boolean { + return this.tsSubCmds.length > 0 || + this.historyCmds.length > 0 || + this.attrSubCmds.length > 0 || + this.entityDataCmds.length > 0 || + this.entityDataUnsubscribeCmds.length > 0 || + this.alarmDataCmds.length > 0 || + this.alarmDataUnsubscribeCmds.length > 0; + } + + public clear() { + this.attrSubCmds.length = 0; + this.tsSubCmds.length = 0; + this.historyCmds.length = 0; + this.entityDataCmds.length = 0; + this.entityDataUnsubscribeCmds.length = 0; + this.alarmDataCmds.length = 0; + this.alarmDataUnsubscribeCmds.length = 0; + } + + public preparePublishCommands(maxCommands: number): TelemetryPluginCmdsWrapper { + const preparedWrapper = new TelemetryPluginCmdsWrapper(); + let leftCount = maxCommands; + preparedWrapper.tsSubCmds = this.popCmds(this.tsSubCmds, leftCount); + leftCount -= preparedWrapper.tsSubCmds.length; + preparedWrapper.historyCmds = this.popCmds(this.historyCmds, leftCount); + leftCount -= preparedWrapper.historyCmds.length; + preparedWrapper.attrSubCmds = this.popCmds(this.attrSubCmds, leftCount); + leftCount -= preparedWrapper.attrSubCmds.length; + preparedWrapper.entityDataCmds = this.popCmds(this.entityDataCmds, leftCount); + leftCount -= preparedWrapper.entityDataCmds.length; + preparedWrapper.entityDataUnsubscribeCmds = this.popCmds(this.entityDataUnsubscribeCmds, leftCount); + leftCount -= preparedWrapper.entityDataUnsubscribeCmds.length; + preparedWrapper.alarmDataCmds = this.popCmds(this.alarmDataCmds, leftCount); + leftCount -= preparedWrapper.alarmDataCmds.length; + preparedWrapper.alarmDataUnsubscribeCmds = this.popCmds(this.alarmDataUnsubscribeCmds, leftCount); + return preparedWrapper; + } + + private popCmds(cmds: Array, leftCount: number): Array { + const toPublish = Math.min(cmds.length, leftCount); + if (toPublish > 0) { + return cmds.splice(0, toPublish); + } else { + return []; + } + } +} + +export interface SubscriptionData { + [key: string]: [number, any][]; +} + +export interface SubscriptionDataHolder { + data: SubscriptionData; +} + +export interface SubscriptionUpdateMsg extends SubscriptionDataHolder { + subscriptionId: number; + errorCode: number; + errorMsg: string; +} + +export enum DataUpdateType { + ENTITY_DATA = 'ENTITY_DATA', + ALARM_DATA = 'ALARM_DATA' +} + +export interface DataUpdateMsg { + cmdId: number; + data?: PageData; + update?: Array; + errorCode: number; + errorMsg: string; + dataUpdateType: DataUpdateType; +} + +export interface EntityDataUpdateMsg extends DataUpdateMsg { + dataUpdateType: DataUpdateType.ENTITY_DATA; +} + +export interface AlarmDataUpdateMsg extends DataUpdateMsg { + dataUpdateType: DataUpdateType.ALARM_DATA; + allowedEntities: number; + totalEntities: number; +} + +export type WebsocketDataMsg = AlarmDataUpdateMsg | EntityDataUpdateMsg | SubscriptionUpdateMsg; + +export function isEntityDataUpdateMsg(message: WebsocketDataMsg): message is EntityDataUpdateMsg { + const updateMsg = (message as DataUpdateMsg); + return updateMsg.cmdId !== undefined && updateMsg.dataUpdateType === DataUpdateType.ENTITY_DATA; +} + +export function isAlarmDataUpdateMsg(message: WebsocketDataMsg): message is AlarmDataUpdateMsg { + const updateMsg = (message as DataUpdateMsg); + return updateMsg.cmdId !== undefined && updateMsg.dataUpdateType === DataUpdateType.ALARM_DATA; +} + +export class SubscriptionUpdate implements SubscriptionUpdateMsg { + subscriptionId: number; + errorCode: number; + errorMsg: string; + data: SubscriptionData; + + constructor(msg: SubscriptionUpdateMsg) { + this.subscriptionId = msg.subscriptionId; + this.errorCode = msg.errorCode; + this.errorMsg = msg.errorMsg; + this.data = msg.data; + } + + public prepareData(keys: string[]) { + if (!this.data) { + this.data = {}; + } + if (keys) { + keys.forEach((key) => { + if (!this.data[key]) { + this.data[key] = []; + } + }); + } + } + + public updateAttributeData(origData: Array): Array { + for (const key of Object.keys(this.data)) { + const keyData = this.data[key]; + if (keyData.length) { + const existing = origData.find((data) => data.key === key); + if (existing) { + existing.lastUpdateTs = keyData[0][0]; + existing.value = keyData[0][1]; + } else { + origData.push( + { + key, + lastUpdateTs: keyData[0][0], + value: keyData[0][1] + } + ); + } + } + } + return origData; + } +} + +export class DataUpdate implements DataUpdateMsg { + cmdId: number; + errorCode: number; + errorMsg: string; + data?: PageData; + update?: Array; + dataUpdateType: DataUpdateType; + + constructor(msg: DataUpdateMsg) { + this.cmdId = msg.cmdId; + this.errorCode = msg.errorCode; + this.errorMsg = msg.errorMsg; + this.data = msg.data; + this.update = msg.update; + this.dataUpdateType = msg.dataUpdateType; + } +} + +export class EntityDataUpdate extends DataUpdate { + constructor(msg: EntityDataUpdateMsg) { + super(msg); + } +} + +export class AlarmDataUpdate extends DataUpdate { + allowedEntities: number; + totalEntities: number; + + constructor(msg: AlarmDataUpdateMsg) { + super(msg); + this.allowedEntities = msg.allowedEntities; + this.totalEntities = msg.totalEntities; + } +} + +export interface TelemetryService { + subscribe(subscriber: TelemetrySubscriber); + update(subscriber: TelemetrySubscriber); + unsubscribe(subscriber: TelemetrySubscriber); +} + +export class TelemetrySubscriber { + + private dataSubject = new ReplaySubject(1); + private entityDataSubject = new ReplaySubject(1); + private alarmDataSubject = new ReplaySubject(1); + private reconnectSubject = new Subject(); + + private zone: NgZone; + + public subscriptionCommands: Array; + + public data$ = this.dataSubject.asObservable(); + public entityData$ = this.entityDataSubject.asObservable(); + public alarmData$ = this.alarmDataSubject.asObservable(); + public reconnect$ = this.reconnectSubject.asObservable(); + + public static createEntityAttributesSubscription(telemetryService: TelemetryService, + entityId: EntityId, attributeScope: TelemetryType, + zone: NgZone, keys: string[] = null): TelemetrySubscriber { + let subscriptionCommand: SubscriptionCmd; + if (attributeScope === LatestTelemetry.LATEST_TELEMETRY) { + subscriptionCommand = new TimeseriesSubscriptionCmd(); + } else { + subscriptionCommand = new AttributesSubscriptionCmd(); + } + subscriptionCommand.entityType = entityId.entityType as EntityType; + subscriptionCommand.entityId = entityId.id; + subscriptionCommand.scope = attributeScope as AttributeScope; + if (keys) { + subscriptionCommand.keys = keys.join(','); + } + const subscriber = new TelemetrySubscriber(telemetryService); + subscriber.zone = zone; + subscriber.subscriptionCommands.push(subscriptionCommand); + return subscriber; + } + + constructor(private telemetryService: TelemetryService) { + this.subscriptionCommands = []; + } + + public subscribe() { + this.telemetryService.subscribe(this); + } + + public update() { + this.telemetryService.update(this); + } + + public unsubscribe() { + this.telemetryService.unsubscribe(this); + this.complete(); + } + + public complete() { + this.dataSubject.complete(); + this.entityDataSubject.complete(); + this.alarmDataSubject.complete(); + this.reconnectSubject.complete(); + } + + public onData(message: SubscriptionUpdate) { + const cmdId = message.subscriptionId; + let keys: string[]; + const cmd = this.subscriptionCommands.find((command) => command.cmdId === cmdId); + if (cmd) { + const telemetryPluginCmd = cmd as TelemetryPluginCmd; + if (telemetryPluginCmd.keys && telemetryPluginCmd.keys.length) { + keys = telemetryPluginCmd.keys.split(','); + } + } + message.prepareData(keys); + if (this.zone) { + this.zone.run( + () => { + this.dataSubject.next(message); + } + ); + } else { + this.dataSubject.next(message); + } + } + + public onEntityData(message: EntityDataUpdate) { + if (this.zone) { + this.zone.run( + () => { + this.entityDataSubject.next(message); + } + ); + } else { + this.entityDataSubject.next(message); + } + } + + public onAlarmData(message: AlarmDataUpdate) { + if (this.zone) { + this.zone.run( + () => { + this.alarmDataSubject.next(message); + } + ); + } else { + this.alarmDataSubject.next(message); + } + } + + public onReconnected() { + this.reconnectSubject.next(); + } + + public attributeData$(): Observable> { + const attributeData = new Array(); + return this.data$.pipe( + map((message) => message.updateAttributeData(attributeData)) + ); + } +} diff --git a/ui-ngx/src/app/shared/models/tenant.model.ts b/ui-ngx/src/app/shared/models/tenant.model.ts new file mode 100644 index 0000000000..ad946efdb7 --- /dev/null +++ b/ui-ngx/src/app/shared/models/tenant.model.ts @@ -0,0 +1,95 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ContactBased } from '@shared/models/contact-based.model'; +import { TenantId } from './id/tenant-id'; +import { TenantProfileId } from '@shared/models/id/tenant-profile-id'; +import { BaseData } from '@shared/models/base-data'; + +export enum TenantProfileType { + DEFAULT = 'DEFAULT' +} + +export interface DefaultTenantProfileConfiguration { + maxDevices: number; + maxAssets: number; + + transportTenantMsgRateLimit?: string; + transportTenantTelemetryMsgRateLimit?: string; + transportTenantTelemetryDataPointsRateLimit?: string; + transportDeviceMsgRateLimit?: string; + transportDeviceTelemetryMsgRateLimit?: string; + transportDeviceTelemetryDataPointsRateLimit?: string; + + maxTransportMessages: number; + maxTransportDataPoints: number; + maxREExecutions: number; + maxJSExecutions: number; + maxDPStorageDays: number; + maxRuleNodeExecutionsPerMessage: number; +} + +export type TenantProfileConfigurations = DefaultTenantProfileConfiguration; + +export interface TenantProfileConfiguration extends TenantProfileConfigurations { + type: TenantProfileType; +} + +export function createTenantProfileConfiguration(type: TenantProfileType): TenantProfileConfiguration { + let configuration: TenantProfileConfiguration = null; + if (type) { + switch (type) { + case TenantProfileType.DEFAULT: + const defaultConfiguration: DefaultTenantProfileConfiguration = { + maxDevices: 0, + maxAssets: 0, + maxTransportMessages: 0, + maxTransportDataPoints: 0, + maxREExecutions: 0, + maxJSExecutions: 0, + maxDPStorageDays: 0, + maxRuleNodeExecutionsPerMessage: 0 + }; + configuration = {...defaultConfiguration, type: TenantProfileType.DEFAULT}; + break; + } + } + return configuration; +} + +export interface TenantProfileData { + configuration: TenantProfileConfiguration; +} + +export interface TenantProfile extends BaseData { + name: string; + description?: string; + default?: boolean; + isolatedTbCore?: boolean; + isolatedTbRuleEngine?: boolean; + profileData?: TenantProfileData; +} + +export interface Tenant extends ContactBased { + title: string; + region: string; + tenantProfileId: TenantProfileId; + additionalInfo?: any; +} + +export interface TenantInfo extends Tenant { + tenantProfileName: string; +} diff --git a/ui-ngx/src/app/shared/models/time/time.models.ts b/ui-ngx/src/app/shared/models/time/time.models.ts new file mode 100644 index 0000000000..b81952dbde --- /dev/null +++ b/ui-ngx/src/app/shared/models/time/time.models.ts @@ -0,0 +1,483 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { TimeService } from '@core/services/time.service'; +import { deepClone, isDefined, isUndefined } from '@app/core/utils'; +import * as moment_ from 'moment'; + +const moment = moment_; + +export const SECOND = 1000; +export const MINUTE = 60 * SECOND; +export const HOUR = 60 * MINUTE; +export const DAY = 24 * HOUR; +export const YEAR = DAY * 365; + +export enum TimewindowType { + REALTIME, + HISTORY +} + +export enum HistoryWindowType { + LAST_INTERVAL, + FIXED +} + +export interface IntervalWindow { + interval?: number; + timewindowMs?: number; +} + +export interface FixedWindow { + startTimeMs: number; + endTimeMs: number; +} + +export interface HistoryWindow extends IntervalWindow { + historyType?: HistoryWindowType; + fixedTimewindow?: FixedWindow; +} + +export enum AggregationType { + MIN = 'MIN', + MAX = 'MAX', + AVG = 'AVG', + SUM = 'SUM', + COUNT = 'COUNT', + NONE = 'NONE' +} + +export const aggregationTranslations = new Map( + [ + [AggregationType.MIN, 'aggregation.min'], + [AggregationType.MAX, 'aggregation.max'], + [AggregationType.AVG, 'aggregation.avg'], + [AggregationType.SUM, 'aggregation.sum'], + [AggregationType.COUNT, 'aggregation.count'], + [AggregationType.NONE, 'aggregation.none'], + ] +); + +export interface Aggregation { + interval?: number; + type: AggregationType; + limit: number; +} + +export interface Timewindow { + displayValue?: string; + hideInterval?: boolean; + hideAggregation?: boolean; + hideAggInterval?: boolean; + selectedTab?: TimewindowType; + realtime?: IntervalWindow; + history?: HistoryWindow; + aggregation?: Aggregation; +} + +export interface SubscriptionAggregation extends Aggregation { + interval?: number; + timeWindow?: number; + stateData?: boolean; +} + +export interface SubscriptionTimewindow { + startTs?: number; + realtimeWindowMs?: number; + fixedWindow?: FixedWindow; + aggregation?: SubscriptionAggregation; +} + +export interface WidgetTimewindow { + minTime?: number; + maxTime?: number; + interval?: number; + stDiff?: number; +} + +export function historyInterval(timewindowMs: number): Timewindow { + const timewindow: Timewindow = { + selectedTab: TimewindowType.HISTORY, + history: { + historyType: HistoryWindowType.LAST_INTERVAL, + timewindowMs + } + }; + return timewindow; +} + +export function defaultTimewindow(timeService: TimeService): Timewindow { + const currentTime = moment().valueOf(); + const timewindow: Timewindow = { + displayValue: '', + hideInterval: false, + hideAggregation: false, + hideAggInterval: false, + selectedTab: TimewindowType.REALTIME, + realtime: { + interval: SECOND, + timewindowMs: MINUTE + }, + history: { + historyType: HistoryWindowType.LAST_INTERVAL, + interval: SECOND, + timewindowMs: MINUTE, + fixedTimewindow: { + startTimeMs: currentTime - DAY, + endTimeMs: currentTime + } + }, + aggregation: { + type: AggregationType.AVG, + limit: Math.floor(timeService.getMaxDatapointsLimit() / 2) + } + }; + return timewindow; +} + +export function initModelFromDefaultTimewindow(value: Timewindow, timeService: TimeService): Timewindow { + const model = defaultTimewindow(timeService); + if (value) { + model.hideInterval = value.hideInterval; + model.hideAggregation = value.hideAggregation; + model.hideAggInterval = value.hideAggInterval; + if (isUndefined(value.selectedTab)) { + if (value.realtime) { + model.selectedTab = TimewindowType.REALTIME; + } else { + model.selectedTab = TimewindowType.HISTORY; + } + } else { + model.selectedTab = value.selectedTab; + } + if (model.selectedTab === TimewindowType.REALTIME) { + if (isDefined(value.realtime.interval)) { + model.realtime.interval = value.realtime.interval; + } + model.realtime.timewindowMs = value.realtime.timewindowMs; + } else { + if (isDefined(value.history.interval)) { + model.history.interval = value.history.interval; + } + if (isUndefined(value.history.historyType)) { + if (isDefined(value.history.timewindowMs)) { + model.history.historyType = HistoryWindowType.LAST_INTERVAL; + } else { + model.history.historyType = HistoryWindowType.FIXED; + } + } else { + model.history.historyType = value.history.historyType; + } + if (model.history.historyType === HistoryWindowType.LAST_INTERVAL) { + model.history.timewindowMs = value.history.timewindowMs; + } else { + model.history.fixedTimewindow.startTimeMs = value.history.fixedTimewindow.startTimeMs; + model.history.fixedTimewindow.endTimeMs = value.history.fixedTimewindow.endTimeMs; + } + } + if (value.aggregation) { + if (value.aggregation.type) { + model.aggregation.type = value.aggregation.type; + } + model.aggregation.limit = value.aggregation.limit || Math.floor(timeService.getMaxDatapointsLimit() / 2); + } + } + return model; +} + +export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number, endTimeMs: number, + interval: number, timeService: TimeService): Timewindow { + if (timewindow.history) { + interval = isDefined(interval) ? interval : timewindow.history.interval; + } else if (timewindow.realtime) { + interval = timewindow.realtime.interval; + } else { + interval = 0; + } + let aggType: AggregationType; + let limit: number; + if (timewindow.aggregation) { + aggType = timewindow.aggregation.type || AggregationType.AVG; + limit = timewindow.aggregation.limit || timeService.getMaxDatapointsLimit(); + } else { + aggType = AggregationType.AVG; + limit = timeService.getMaxDatapointsLimit(); + } + const historyTimewindow: Timewindow = { + hideInterval: timewindow.hideInterval || false, + hideAggregation: timewindow.hideAggregation || false, + hideAggInterval: timewindow.hideAggInterval || false, + selectedTab: TimewindowType.HISTORY, + history: { + historyType: HistoryWindowType.FIXED, + fixedTimewindow: { + startTimeMs, + endTimeMs + }, + interval: timeService.boundIntervalToTimewindow(endTimeMs - startTimeMs, interval, AggregationType.AVG) + }, + aggregation: { + type: aggType, + limit + } + }; + return historyTimewindow; +} + +export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: number, stateData: boolean, + timeService: TimeService): SubscriptionTimewindow { + const subscriptionTimewindow: SubscriptionTimewindow = { + fixedWindow: null, + realtimeWindowMs: null, + aggregation: { + interval: SECOND, + limit: timeService.getMaxDatapointsLimit(), + type: AggregationType.AVG + } + }; + let aggTimewindow = 0; + if (stateData) { + subscriptionTimewindow.aggregation.type = AggregationType.NONE; + subscriptionTimewindow.aggregation.stateData = true; + } + if (isDefined(timewindow.aggregation) && !stateData) { + subscriptionTimewindow.aggregation = { + type: timewindow.aggregation.type || AggregationType.AVG, + limit: timewindow.aggregation.limit || timeService.getMaxDatapointsLimit() + }; + } + let selectedTab = timewindow.selectedTab; + if (isUndefined(selectedTab)) { + selectedTab = isDefined(timewindow.realtime) ? TimewindowType.REALTIME : TimewindowType.HISTORY; + } + if (selectedTab === TimewindowType.REALTIME) { + subscriptionTimewindow.realtimeWindowMs = timewindow.realtime.timewindowMs; + subscriptionTimewindow.aggregation.interval = + timeService.boundIntervalToTimewindow(subscriptionTimewindow.realtimeWindowMs, timewindow.realtime.interval, + subscriptionTimewindow.aggregation.type); + subscriptionTimewindow.startTs = Date.now() + stDiff - subscriptionTimewindow.realtimeWindowMs; + const startDiff = subscriptionTimewindow.startTs % subscriptionTimewindow.aggregation.interval; + aggTimewindow = subscriptionTimewindow.realtimeWindowMs; + if (startDiff) { + subscriptionTimewindow.startTs -= startDiff; + aggTimewindow += subscriptionTimewindow.aggregation.interval; + } + } else { + let historyType = timewindow.history.historyType; + if (isUndefined(historyType)) { + historyType = isDefined(timewindow.history.timewindowMs) ? HistoryWindowType.LAST_INTERVAL : HistoryWindowType.FIXED; + } + if (historyType === HistoryWindowType.LAST_INTERVAL) { + const currentTime = Date.now(); + subscriptionTimewindow.fixedWindow = { + startTimeMs: currentTime - timewindow.history.timewindowMs, + endTimeMs: currentTime + }; + aggTimewindow = timewindow.history.timewindowMs; + } else { + subscriptionTimewindow.fixedWindow = { + startTimeMs: timewindow.history.fixedTimewindow.startTimeMs, + endTimeMs: timewindow.history.fixedTimewindow.endTimeMs + }; + aggTimewindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs; + } + subscriptionTimewindow.startTs = subscriptionTimewindow.fixedWindow.startTimeMs; + subscriptionTimewindow.aggregation.interval = + timeService.boundIntervalToTimewindow(aggTimewindow, timewindow.history.interval, subscriptionTimewindow.aggregation.type); + } + const aggregation = subscriptionTimewindow.aggregation; + aggregation.timeWindow = aggTimewindow; + if (aggregation.type !== AggregationType.NONE) { + aggregation.limit = Math.ceil(aggTimewindow / subscriptionTimewindow.aggregation.interval); + } + return subscriptionTimewindow; +} + +export function createTimewindowForComparison(subscriptionTimewindow: SubscriptionTimewindow, + timeUnit: moment_.unitOfTime.DurationConstructor): SubscriptionTimewindow { + const timewindowForComparison: SubscriptionTimewindow = { + fixedWindow: null, + realtimeWindowMs: null, + aggregation: subscriptionTimewindow.aggregation + }; + + if (subscriptionTimewindow.realtimeWindowMs) { + timewindowForComparison.startTs = moment(subscriptionTimewindow.startTs).subtract(1, timeUnit).valueOf(); + timewindowForComparison.realtimeWindowMs = subscriptionTimewindow.realtimeWindowMs; + } else if (subscriptionTimewindow.fixedWindow) { + const timeInterval = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs; + const endTimeMs = moment(subscriptionTimewindow.fixedWindow.endTimeMs).subtract(1, timeUnit).valueOf(); + + timewindowForComparison.startTs = endTimeMs - timeInterval; + timewindowForComparison.fixedWindow = { + startTimeMs: timewindowForComparison.startTs, + endTimeMs + }; + } + + return timewindowForComparison; +} + +export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow { + const cloned: Timewindow = {}; + cloned.hideInterval = timewindow.hideInterval || false; + cloned.hideAggregation = timewindow.hideAggregation || false; + cloned.hideAggInterval = timewindow.hideAggInterval || false; + if (isDefined(timewindow.selectedTab)) { + cloned.selectedTab = timewindow.selectedTab; + if (timewindow.selectedTab === TimewindowType.REALTIME) { + cloned.realtime = deepClone(timewindow.realtime); + } else if (timewindow.selectedTab === TimewindowType.HISTORY) { + cloned.history = deepClone(timewindow.history); + } + } + cloned.aggregation = deepClone(timewindow.aggregation); + return cloned; +} + +export function cloneSelectedHistoryTimewindow(historyWindow: HistoryWindow): HistoryWindow { + const cloned: HistoryWindow = {}; + if (isDefined(historyWindow.historyType)) { + cloned.historyType = historyWindow.historyType; + cloned.interval = historyWindow.interval; + if (historyWindow.historyType === HistoryWindowType.LAST_INTERVAL) { + cloned.timewindowMs = historyWindow.timewindowMs; + } else if (historyWindow.historyType === HistoryWindowType.FIXED) { + cloned.fixedTimewindow = deepClone(historyWindow.fixedTimewindow); + } + } + return cloned; +} + +export interface TimeInterval { + name: string; + translateParams: {[key: string]: any}; + value: number; +} + +export const defaultTimeIntervals = new Array( + { + name: 'timeinterval.seconds-interval', + translateParams: {seconds: 1}, + value: 1 * SECOND + }, + { + name: 'timeinterval.seconds-interval', + translateParams: {seconds: 5}, + value: 5 * SECOND + }, + { + name: 'timeinterval.seconds-interval', + translateParams: {seconds: 10}, + value: 10 * SECOND + }, + { + name: 'timeinterval.seconds-interval', + translateParams: {seconds: 15}, + value: 15 * SECOND + }, + { + name: 'timeinterval.seconds-interval', + translateParams: {seconds: 30}, + value: 30 * SECOND + }, + { + name: 'timeinterval.minutes-interval', + translateParams: {minutes: 1}, + value: 1 * MINUTE + }, + { + name: 'timeinterval.minutes-interval', + translateParams: {minutes: 2}, + value: 2 * MINUTE + }, + { + name: 'timeinterval.minutes-interval', + translateParams: {minutes: 5}, + value: 5 * MINUTE + }, + { + name: 'timeinterval.minutes-interval', + translateParams: {minutes: 10}, + value: 10 * MINUTE + }, + { + name: 'timeinterval.minutes-interval', + translateParams: {minutes: 15}, + value: 15 * MINUTE + }, + { + name: 'timeinterval.minutes-interval', + translateParams: {minutes: 30}, + value: 30 * MINUTE + }, + { + name: 'timeinterval.hours-interval', + translateParams: {hours: 1}, + value: 1 * HOUR + }, + { + name: 'timeinterval.hours-interval', + translateParams: {hours: 2}, + value: 2 * HOUR + }, + { + name: 'timeinterval.hours-interval', + translateParams: {hours: 5}, + value: 5 * HOUR + }, + { + name: 'timeinterval.hours-interval', + translateParams: {hours: 10}, + value: 10 * HOUR + }, + { + name: 'timeinterval.hours-interval', + translateParams: {hours: 12}, + value: 12 * HOUR + }, + { + name: 'timeinterval.days-interval', + translateParams: {days: 1}, + value: 1 * DAY + }, + { + name: 'timeinterval.days-interval', + translateParams: {days: 7}, + value: 7 * DAY + }, + { + name: 'timeinterval.days-interval', + translateParams: {days: 30}, + value: 30 * DAY + } +); + +export enum TimeUnit { + SECONDS = 'SECONDS', + MINUTES = 'MINUTES', + HOURS = 'HOURS', + DAYS = 'DAYS' +} + +export const timeUnitTranslationMap = new Map( + [ + [TimeUnit.SECONDS, 'timeunit.seconds'], + [TimeUnit.MINUTES, 'timeunit.minutes'], + [TimeUnit.HOURS, 'timeunit.hours'], + [TimeUnit.DAYS, 'timeunit.days'] + ] +); diff --git a/ui-ngx/src/app/shared/models/user.model.ts b/ui-ngx/src/app/shared/models/user.model.ts new file mode 100644 index 0000000000..9a64242702 --- /dev/null +++ b/ui-ngx/src/app/shared/models/user.model.ts @@ -0,0 +1,56 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { BaseData } from './base-data'; +import { UserId } from './id/user-id'; +import { CustomerId } from './id/customer-id'; +import { Authority } from './authority.enum'; +import { TenantId } from './id/tenant-id'; + +export interface User extends BaseData { + tenantId: TenantId; + customerId: CustomerId; + email: string; + authority: Authority; + firstName: string; + lastName: string; + additionalInfo: any; +} + +export enum ActivationMethod { + DISPLAY_ACTIVATION_LINK = 'DISPLAY_ACTIVATION_LINK', + SEND_ACTIVATION_MAIL = 'SEND_ACTIVATION_MAIL' +} + +export const activationMethodTranslations = new Map( + [ + [ActivationMethod.DISPLAY_ACTIVATION_LINK, 'user.display-activation-link'], + [ActivationMethod.SEND_ACTIVATION_MAIL, 'user.send-activation-mail'] + ] +); + +export interface AuthUser { + sub: string; + scopes: string[]; + userId: string; + firstName: string; + lastName: string; + enabled: boolean; + tenantId: string; + customerId: string; + isPublic: boolean; + authority: Authority; +} diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts new file mode 100644 index 0000000000..a97a66a1ef --- /dev/null +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -0,0 +1,434 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { BaseData } from '@shared/models/base-data'; +import { TenantId } from '@shared/models/id/tenant-id'; +import { WidgetTypeId } from '@shared/models/id/widget-type-id'; +import { Timewindow } from '@shared/models/time/time.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { AlarmSearchStatus, AlarmSeverity } from '@shared/models/alarm.models'; +import { DataKeyType } from './telemetry/telemetry.models'; +import { EntityId } from '@shared/models/id/entity-id'; +import * as moment_ from 'moment'; +import { EntityDataPageLink, EntityFilter, KeyFilter } from '@shared/models/query/query.models'; + +export enum widgetType { + timeseries = 'timeseries', + latest = 'latest', + rpc = 'rpc', + alarm = 'alarm', + static = 'static' +} + +export interface WidgetTypeTemplate { + bundleAlias: string; + alias: string; +} + +export interface WidgetTypeData { + name: string; + icon: string; + isMdiIcon?: boolean; + configHelpLinkId: string; + template: WidgetTypeTemplate; +} + +export const widgetTypesData = new Map( + [ + [ + widgetType.timeseries, + { + name: 'widget.timeseries', + icon: 'timeline', + configHelpLinkId: 'widgetsConfigTimeseries', + template: { + bundleAlias: 'charts', + alias: 'basic_timeseries' + } + } + ], + [ + widgetType.latest, + { + name: 'widget.latest-values', + icon: 'track_changes', + configHelpLinkId: 'widgetsConfigLatest', + template: { + bundleAlias: 'cards', + alias: 'attributes_card' + } + } + ], + [ + widgetType.rpc, + { + name: 'widget.rpc', + icon: 'mdi:developer-board', + configHelpLinkId: 'widgetsConfigRpc', + isMdiIcon: true, + template: { + bundleAlias: 'gpio_widgets', + alias: 'basic_gpio_control' + } + } + ], + [ + widgetType.alarm, + { + name: 'widget.alarm', + icon: 'error', + configHelpLinkId: 'widgetsConfigAlarm', + template: { + bundleAlias: 'alarm_widgets', + alias: 'alarms_table' + } + } + ], + [ + widgetType.static, + { + name: 'widget.static', + icon: 'font_download', + configHelpLinkId: 'widgetsConfigStatic', + template: { + bundleAlias: 'cards', + alias: 'html_card' + } + } + ] + ] +); + +export interface WidgetResource { + url: string; + isModule?: boolean; +} + +export interface WidgetActionSource { + name: string; + value: string; + multiple: boolean; +} + +export const widgetActionSources: {[acionSourceId: string]: WidgetActionSource} = { + headerButton: + { + name: 'widget-action.header-button', + value: 'headerButton', + multiple: true, + } +}; + +export interface WidgetTypeDescriptor { + type: widgetType; + resources: Array; + templateHtml: string; + templateCss: string; + controllerScript: string; + settingsSchema?: string | any; + dataKeySettingsSchema?: string | any; + defaultConfig: string; + sizeX: number; + sizeY: number; +} + +export interface WidgetTypeParameters { + useCustomDatasources?: boolean; + maxDatasources?: number; + maxDataKeys?: number; + dataKeysOptional?: boolean; + stateData?: boolean; + hasDataPageLink?: boolean; + singleEntity?: boolean; + warnOnPageDataOverflow?: boolean; +} + +export interface WidgetControllerDescriptor { + widgetTypeFunction?: any; + settingsSchema?: string | any; + dataKeySettingsSchema?: string | any; + typeParameters?: WidgetTypeParameters; + actionSources?: {[actionSourceId: string]: WidgetActionSource}; +} + +export interface WidgetType extends BaseData { + tenantId: TenantId; + bundleAlias: string; + alias: string; + name: string; + descriptor: WidgetTypeDescriptor; +} + +export enum LegendDirection { + column = 'column', + row = 'row' +} + +export const legendDirectionTranslationMap = new Map( + [ + [ LegendDirection.column, 'direction.column' ], + [ LegendDirection.row, 'direction.row' ] + ] +); + +export enum LegendPosition { + top = 'top', + bottom = 'bottom', + left = 'left', + right = 'right' +} + +export const legendPositionTranslationMap = new Map( + [ + [ LegendPosition.top, 'position.top' ], + [ LegendPosition.bottom, 'position.bottom' ], + [ LegendPosition.left, 'position.left' ], + [ LegendPosition.right, 'position.right' ] + ] +); + +export interface LegendConfig { + position: LegendPosition; + direction?: LegendDirection; + sortDataKeys: boolean; + showMin: boolean; + showMax: boolean; + showAvg: boolean; + showTotal: boolean; +} + +export function defaultLegendConfig(wType: widgetType): LegendConfig { + return { + direction: LegendDirection.column, + position: LegendPosition.bottom, + sortDataKeys: false, + showMin: false, + showMax: false, + showAvg: wType === widgetType.timeseries, + showTotal: false + }; +} + +export interface KeyInfo { + name: string; + label?: string; + color?: string; + funcBody?: string; + postFuncBody?: string; + units?: string; + decimals?: number; +} + +export interface DataKey extends KeyInfo { + type: DataKeyType; + pattern?: string; + settings?: any; + usePostProcessing?: boolean; + hidden?: boolean; + inLegend?: boolean; + isAdditional?: boolean; + origDataKeyIndex?: number; + _hash?: number; +} + +export enum DatasourceType { + function = 'function', + entity = 'entity' +} + +export const datasourceTypeTranslationMap = new Map( + [ + [ DatasourceType.function, 'function.function' ], + [ DatasourceType.entity, 'entity.entity' ] + ] +); + +export interface Datasource { + type?: DatasourceType | any; + name?: string; + aliasName?: string; + dataKeys?: Array; + entityType?: EntityType; + entityId?: string; + entityName?: string; + entityAliasId?: string; + filterId?: string; + unresolvedStateEntity?: boolean; + dataReceived?: boolean; + entity?: BaseData; + entityLabel?: string; + entityDescription?: string; + generated?: boolean; + isAdditional?: boolean; + origDatasourceIndex?: number; + pageLink?: EntityDataPageLink; + keyFilters?: Array; + entityFilter?: EntityFilter; + dataKeyStartIndex?: number; + [key: string]: any; +} + +export type DataSet = [number, any][]; + +export interface DataSetHolder { + data: DataSet; +} + +export interface DatasourceData extends DataSetHolder { + datasource: Datasource; + dataKey: DataKey; +} + +export interface LegendKey { + dataKey: DataKey; + dataIndex: number; +} + +export interface LegendKeyData { + min: string; + max: string; + avg: string; + total: string; + hidden: boolean; +} + +export interface LegendData { + keys: Array; + data: Array; +} + +export enum WidgetActionType { + openDashboardState = 'openDashboardState', + updateDashboardState = 'updateDashboardState', + openDashboard = 'openDashboard', + custom = 'custom', + customPretty = 'customPretty' +} + +export const widgetActionTypeTranslationMap = new Map( + [ + [ WidgetActionType.openDashboardState, 'widget-action.open-dashboard-state' ], + [ WidgetActionType.updateDashboardState, 'widget-action.update-dashboard-state' ], + [ WidgetActionType.openDashboard, 'widget-action.open-dashboard' ], + [ WidgetActionType.custom, 'widget-action.custom' ], + [ WidgetActionType.customPretty, 'widget-action.custom-pretty' ] + ] +); + +export interface CustomActionDescriptor { + customFunction?: string; + customResources?: Array; + customHtml?: string; + customCss?: string; +} + +export interface WidgetActionDescriptor extends CustomActionDescriptor { + id: string; + name: string; + icon: string; + displayName?: string; + type: WidgetActionType; + targetDashboardId?: string; + targetDashboardStateId?: string; + openRightLayout?: boolean; + setEntityId?: boolean; + stateEntityParamName?: string; +} + +export interface WidgetComparisonSettings { + comparisonEnabled?: boolean; + timeForComparison?: moment_.unitOfTime.DurationConstructor; +} + +export interface WidgetConfig { + title?: string; + titleIcon?: string; + showTitle?: boolean; + showTitleIcon?: boolean; + iconColor?: string; + iconSize?: string; + titleTooltip?: string; + dropShadow?: boolean; + enableFullscreen?: boolean; + useDashboardTimewindow?: boolean; + displayTimewindow?: boolean; + showLegend?: boolean; + legendConfig?: LegendConfig; + timewindow?: Timewindow; + mobileHeight?: number; + mobileOrder?: number; + color?: string; + backgroundColor?: string; + padding?: string; + margin?: string; + widgetStyle?: {[klass: string]: any}; + titleStyle?: {[klass: string]: any}; + units?: string; + decimals?: number; + actions?: {[actionSourceId: string]: Array}; + settings?: any; + alarmSource?: Datasource; + alarmStatusList?: AlarmSearchStatus[]; + alarmSeverityList?: AlarmSeverity[]; + alarmTypeList?: string[]; + searchPropagatedAlarms?: boolean; + datasources?: Array; + targetDeviceAliasIds?: Array; + [key: string]: any; +} + +export interface Widget { + id?: string; + typeId?: WidgetTypeId; + isSystemType: boolean; + bundleAlias: string; + typeAlias: string; + type: widgetType; + title: string; + sizeX: number; + sizeY: number; + row: number; + col: number; + config: WidgetConfig; +} + +export interface GroupInfo { + formIndex: number; + GroupTitle: string; +} + +export interface JsonSchema { + type: string; + title?: string; + properties: {[key: string]: any}; + required?: string[]; +} + +export interface JsonSettingsSchema { + schema?: JsonSchema; + form?: any[]; + groupInfoes?: GroupInfo[] +} + +export interface WidgetPosition { + row: number; + column: number; +} + +export interface WidgetSize { + sizeX: number; + sizeY: number; +} diff --git a/ui-ngx/src/app/shared/models/widgets-bundle.model.ts b/ui-ngx/src/app/shared/models/widgets-bundle.model.ts new file mode 100644 index 0000000000..0d91340ed3 --- /dev/null +++ b/ui-ngx/src/app/shared/models/widgets-bundle.model.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { BaseData } from '@shared/models/base-data'; +import { TenantId } from '@shared/models/id/tenant-id'; +import { WidgetsBundleId } from '@shared/models/id/widgets-bundle-id'; + +export interface WidgetsBundle extends BaseData { + tenantId: TenantId; + alias: string; + title: string; + image: string; +} diff --git a/ui-ngx/src/app/shared/models/window-message.model.ts b/ui-ngx/src/app/shared/models/window-message.model.ts new file mode 100644 index 0000000000..ac5305c565 --- /dev/null +++ b/ui-ngx/src/app/shared/models/window-message.model.ts @@ -0,0 +1,22 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export type WindowMessageType = 'widgetException' | 'widgetEditModeInited' | 'widgetEditUpdated'; + +export interface WindowMessage { + type: WindowMessageType; + data?: any; +} diff --git a/ui-ngx/src/app/shared/pipe/enum-to-array.pipe.ts b/ui-ngx/src/app/shared/pipe/enum-to-array.pipe.ts new file mode 100644 index 0000000000..7717ecbf19 --- /dev/null +++ b/ui-ngx/src/app/shared/pipe/enum-to-array.pipe.ts @@ -0,0 +1,27 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'enumToArray' +}) +export class EnumToArrayPipe implements PipeTransform { + transform(data: object): string[] { + const keys = Object.keys(data); + return keys.slice(keys.length / 2); + } +} diff --git a/ui-ngx/src/app/shared/pipe/highlight.pipe.ts b/ui-ngx/src/app/shared/pipe/highlight.pipe.ts new file mode 100644 index 0000000000..bcd158d146 --- /dev/null +++ b/ui-ngx/src/app/shared/pipe/highlight.pipe.ts @@ -0,0 +1,28 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ name: 'highlight' }) +export class HighlightPipe implements PipeTransform { + transform(text: string, search): string { + const pattern = search + .replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); + const regex = new RegExp('^' + pattern, 'i'); + + return search ? text.replace(regex, match => `${match}`) : text; + } +} diff --git a/ui-ngx/src/app/shared/pipe/keyboard-shortcut.pipe.ts b/ui-ngx/src/app/shared/pipe/keyboard-shortcut.pipe.ts new file mode 100644 index 0000000000..7f029c7d94 --- /dev/null +++ b/ui-ngx/src/app/shared/pipe/keyboard-shortcut.pipe.ts @@ -0,0 +1,49 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Inject, Pipe, PipeTransform } from '@angular/core'; +import { WINDOW } from '@core/services/window.service'; + +// @dynamic +@Pipe({ + name: 'keyboardShortcut' +}) +export class KeyboardShortcutPipe implements PipeTransform { + + constructor(@Inject(WINDOW) private window: Window) {} + + transform(value: string): string { + if (!value) { + return; + } + const keys = value.split('-'); + const isOSX = /Mac OS X/.test(this.window.navigator.userAgent); + + const seperator = (!isOSX || keys.length > 2) ? '+' : ''; + + const abbreviations = { + M: isOSX ? '⌘' : 'Ctrl', + A: isOSX ? 'Option' : 'Alt', + S: 'Shift' + }; + + return keys.map((key, index) => { + const last = index === keys.length - 1; + return last ? key : abbreviations[key]; + }).join(seperator); + } + +} diff --git a/ui-ngx/src/app/shared/pipe/milliseconds-to-time-string.pipe.ts b/ui-ngx/src/app/shared/pipe/milliseconds-to-time-string.pipe.ts new file mode 100644 index 0000000000..ba5f048f93 --- /dev/null +++ b/ui-ngx/src/app/shared/pipe/milliseconds-to-time-string.pipe.ts @@ -0,0 +1,58 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Pipe, PipeTransform } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; + +@Pipe({ + name: 'milliSecondsToTimeString' +}) +export class MillisecondsToTimeStringPipe implements PipeTransform { + + constructor(private translate: TranslateService) { + } + + transform(millseconds: number, args?: any): string { + let seconds = Math.floor(millseconds / 1000); + const days = Math.floor(seconds / 86400); + let hours = Math.floor((seconds % 86400) / 3600); + let minutes = Math.floor(((seconds % 86400) % 3600) / 60); + seconds = seconds % 60; + let timeString = ''; + if (days > 0) { + timeString += this.translate.instant('timewindow.days', {days}); + } + if (hours > 0) { + if (timeString.length === 0 && hours === 1) { + hours = 0; + } + timeString += this.translate.instant('timewindow.hours', {hours}); + } + if (minutes > 0) { + if (timeString.length === 0 && minutes === 1) { + minutes = 0; + } + timeString += this.translate.instant('timewindow.minutes', {minutes}); + } + if (seconds > 0) { + if (timeString.length === 0 && seconds === 1) { + seconds = 0; + } + timeString += this.translate.instant('timewindow.seconds', {seconds}); + } + return timeString; + } +} diff --git a/ui-ngx/src/app/shared/pipe/nospace.pipe.ts b/ui-ngx/src/app/shared/pipe/nospace.pipe.ts new file mode 100644 index 0000000000..a0bcd58391 --- /dev/null +++ b/ui-ngx/src/app/shared/pipe/nospace.pipe.ts @@ -0,0 +1,28 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'nospace' +}) +export class NospacePipe implements PipeTransform { + + transform(value: string, args?: any): string { + return (!value) ? '' : value.replace(/ /g, ''); + } + +} diff --git a/ui-ngx/src/app/shared/pipe/public-api.ts b/ui-ngx/src/app/shared/pipe/public-api.ts new file mode 100644 index 0000000000..4f801b7d51 --- /dev/null +++ b/ui-ngx/src/app/shared/pipe/public-api.ts @@ -0,0 +1,22 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export * from './enum-to-array.pipe'; +export * from './highlight.pipe'; +export * from './keyboard-shortcut.pipe'; +export * from './milliseconds-to-time-string.pipe'; +export * from './nospace.pipe'; +export * from './truncate.pipe'; diff --git a/ui-ngx/src/app/shared/pipe/tbJson.pipe.ts b/ui-ngx/src/app/shared/pipe/tbJson.pipe.ts new file mode 100644 index 0000000000..42d3f5a74c --- /dev/null +++ b/ui-ngx/src/app/shared/pipe/tbJson.pipe.ts @@ -0,0 +1,30 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Pipe, PipeTransform } from '@angular/core'; +import { isNumber, isObject } from '@core/utils'; + +@Pipe({name: 'tbJson'}) +export class TbJsonPipe implements PipeTransform { + transform(value: any): string { + if (isObject(value)) { + return JSON.stringify(value); + } else if (isNumber(value)) { + return value.toString(); + } + return value; + } +} diff --git a/ui-ngx/src/app/shared/pipe/truncate.pipe.ts b/ui-ngx/src/app/shared/pipe/truncate.pipe.ts new file mode 100644 index 0000000000..2830b6ce7f --- /dev/null +++ b/ui-ngx/src/app/shared/pipe/truncate.pipe.ts @@ -0,0 +1,42 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Pipe, PipeTransform } from '@angular/core'; +import { isString } from '@core/utils'; + +@Pipe({ name: 'truncate' }) +export class TruncatePipe implements PipeTransform { + transform(text: string, wordwise: boolean, max: any, tail: string): string { + if (!text) { return ''; } + if (isString(max)) { + max = parseInt(max, 10); + } + if (!max) { return text; } + if (text.length <= max) { return text; } + + text = text.substr(0, max); + if (wordwise) { + let lastspace = text.lastIndexOf(' '); + if (lastspace !== -1) { + if (text.charAt(lastspace - 1) === '.' || text.charAt(lastspace - 1) === ',') { + lastspace = lastspace - 1; + } + text = text.substr(0, lastspace); + } + } + return text + (tail || ' …'); + } +} diff --git a/ui-ngx/src/app/shared/public-api.ts b/ui-ngx/src/app/shared/public-api.ts new file mode 100644 index 0000000000..d514dfd31c --- /dev/null +++ b/ui-ngx/src/app/shared/public-api.ts @@ -0,0 +1,20 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export * from './components/public-api'; +export * from './models/public-api'; +export * from './pipe/public-api'; +export * from './shared.module'; diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts new file mode 100644 index 0000000000..d071869808 --- /dev/null +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -0,0 +1,391 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule, DatePipe } from '@angular/common'; +import { FooterComponent } from '@shared/components/footer.component'; +import { LogoComponent } from '@shared/components/logo.component'; +import { TbSnackBarComponent, ToastDirective } from '@shared/components/toast.directive'; +import { BreadcrumbComponent } from '@shared/components/breadcrumb.component'; +import { FlowInjectionToken, NgxFlowModule } from '@flowjs/ngx-flow'; +import { NgxFlowchartModule } from 'ngx-flowchart/dist/ngx-flowchart'; +import Flow from '@flowjs/flow.js'; + +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatChipsModule } from '@angular/material/chips'; +import { MatDatepickerModule } from '@angular/material/datepicker'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatDividerModule } from '@angular/material/divider'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { MatGridListModule } from '@angular/material/grid-list'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatPaginatorModule } from '@angular/material/paginator'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatRadioModule } from '@angular/material/radio'; +import { MatSelectModule } from '@angular/material/select'; +import { MatSidenavModule } from '@angular/material/sidenav'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; +import { MatSliderModule } from '@angular/material/slider'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { MatSortModule } from '@angular/material/sort'; +import { MatStepperModule } from '@angular/material/stepper'; +import { MatTableModule } from '@angular/material/table'; +import { MatTabsModule } from '@angular/material/tabs'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimepicker/core'; +import { NgxDaterangepickerMd } from 'ngx-daterangepicker-material'; +import { GridsterModule } from 'angular-gridster2'; +import { FlexLayoutModule } from '@angular/flex-layout'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; +import { ShareModule as ShareButtonsModule } from 'ngx-sharebuttons'; +import { HotkeyModule } from 'angular2-hotkeys'; +import { ColorPickerModule } from 'ngx-color-picker'; +import { NgxHmCarouselModule } from 'ngx-hm-carousel'; +import { UserMenuComponent } from '@shared/components/user-menu.component'; +import { NospacePipe } from '@shared/pipe/nospace.pipe'; +import { TranslateModule } from '@ngx-translate/core'; +import { TbCheckboxComponent } from '@shared/components/tb-checkbox.component'; +import { HelpComponent } from '@shared/components/help.component'; +import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; +import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; +import { TimewindowComponent } from '@shared/components/time/timewindow.component'; +import { OverlayModule } from '@angular/cdk/overlay'; +import { TimewindowPanelComponent } from '@shared/components/time/timewindow-panel.component'; +import { TimeintervalComponent } from '@shared/components/time/timeinterval.component'; +import { DatetimePeriodComponent } from '@shared/components/time/datetime-period.component'; +import { EnumToArrayPipe } from '@shared/pipe/enum-to-array.pipe'; +import { ClipboardModule } from 'ngx-clipboard'; +import { ValueInputComponent } from '@shared/components/value-input.component'; +import { FullscreenDirective } from '@shared/components/fullscreen.directive'; +import { HighlightPipe } from '@shared/pipe/highlight.pipe'; +import { DashboardAutocompleteComponent } from '@shared/components/dashboard-autocomplete.component'; +import { EntitySubTypeAutocompleteComponent } from '@shared/components/entity/entity-subtype-autocomplete.component'; +import { EntitySubTypeSelectComponent } from '@shared/components/entity/entity-subtype-select.component'; +import { EntityAutocompleteComponent } from '@shared/components/entity/entity-autocomplete.component'; +import { EntityListComponent } from '@shared/components/entity/entity-list.component'; +import { EntityTypeSelectComponent } from '@shared/components/entity/entity-type-select.component'; +import { EntitySelectComponent } from '@shared/components/entity/entity-select.component'; +import { DatetimeComponent } from '@shared/components/time/datetime.component'; +import { EntityKeysListComponent } from '@shared/components/entity/entity-keys-list.component'; +import { SocialSharePanelComponent } from '@shared/components/socialshare-panel.component'; +import { RelationTypeAutocompleteComponent } from '@shared/components/relation/relation-type-autocomplete.component'; +import { EntityListSelectComponent } from '@shared/components/entity/entity-list-select.component'; +import { JsonObjectEditComponent } from '@shared/components/json-object-edit.component'; +import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons.component'; +import { CircularProgressDirective } from '@shared/components/circular-progress.directive'; +import { + FabActionsDirective, + FabToolbarComponent, + FabTriggerDirective +} from '@shared/components/fab-toolbar.component'; +import { DashboardSelectPanelComponent } from '@shared/components/dashboard-select-panel.component'; +import { DashboardSelectComponent } from '@shared/components/dashboard-select.component'; +import { WidgetsBundleSelectComponent } from '@shared/components/widgets-bundle-select.component'; +import { KeyboardShortcutPipe } from '@shared/pipe/keyboard-shortcut.pipe'; +import { TbErrorComponent } from '@shared/components/tb-error.component'; +import { EntityTypeListComponent } from '@shared/components/entity/entity-type-list.component'; +import { EntitySubTypeListComponent } from '@shared/components/entity/entity-subtype-list.component'; +import { TruncatePipe } from '@shared/pipe/truncate.pipe'; +import { TbJsonPipe } from '@shared/pipe/tbJson.pipe'; +import { ColorPickerDialogComponent } from '@shared/components/dialog/color-picker-dialog.component'; +import { MatChipDraggableDirective } from '@shared/components/mat-chip-draggable.directive'; +import { ColorInputComponent } from '@shared/components/color-input.component'; +import { JsFuncComponent } from '@shared/components/js-func.component'; +import { JsonFormComponent } from '@shared/components/json-form/json-form.component'; +import { ConfirmDialogComponent } from '@shared/components/dialog/confirm-dialog.component'; +import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component'; +import { TodoDialogComponent } from '@shared/components/dialog/todo-dialog.component'; +import { MaterialIconsDialogComponent } from '@shared/components/dialog/material-icons-dialog.component'; +import { MaterialIconSelectComponent } from '@shared/components/material-icon-select.component'; +import { ImageInputComponent } from '@shared/components/image-input.component'; +import { FileInputComponent } from '@shared/components/file-input.component'; +import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-script-test-dialog.component'; +import { MessageTypeAutocompleteComponent } from '@shared/components/message-type-autocomplete.component'; +import { JsonContentComponent } from '@shared/components/json-content.component'; +import { KeyValMapComponent } from '@shared/components/kv-map.component'; +import { TbCheatSheetComponent } from '@shared/components/cheatsheet.component'; +import { TbHotkeysDirective } from '@shared/components/hotkeys.directive'; +import { NavTreeComponent } from '@shared/components/nav-tree.component'; +import { LedLightComponent } from '@shared/components/led-light.component'; +import { TbJsonToStringDirective } from '@shared/components/directives/tb-json-to-string.directive'; +import { JsonObjectEditDialogComponent } from '@shared/components/dialog/json-object-edit-dialog.component'; +import { HistorySelectorComponent } from './components/time/history-selector/history-selector.component'; +import { EntityGatewaySelectComponent } from '@shared/components/entity/entity-gateway-select.component'; +import { QueueTypeListComponent } from '@shared/components/queue/queue-type-list.component'; +import { ContactComponent } from '@shared/components/contact.component'; +import { TimezoneSelectComponent } from '@shared/components/time/timezone-select.component'; + +@NgModule({ + providers: [ + DatePipe, + MillisecondsToTimeStringPipe, + EnumToArrayPipe, + HighlightPipe, + TruncatePipe, + TbJsonPipe, + { + provide: FlowInjectionToken, + useValue: Flow + } + ], + declarations: [ + FooterComponent, + LogoComponent, + FooterFabButtonsComponent, + ToastDirective, + FullscreenDirective, + CircularProgressDirective, + MatChipDraggableDirective, + TbHotkeysDirective, + TbAnchorComponent, + HelpComponent, + TbCheckboxComponent, + TbSnackBarComponent, + TbErrorComponent, + TbCheatSheetComponent, + BreadcrumbComponent, + UserMenuComponent, + TimewindowComponent, + TimewindowPanelComponent, + TimeintervalComponent, + DashboardSelectComponent, + DashboardSelectPanelComponent, + DatetimePeriodComponent, + DatetimeComponent, + TimezoneSelectComponent, + ValueInputComponent, + DashboardAutocompleteComponent, + EntitySubTypeAutocompleteComponent, + EntitySubTypeSelectComponent, + EntitySubTypeListComponent, + EntityAutocompleteComponent, + EntityListComponent, + EntityTypeSelectComponent, + EntitySelectComponent, + EntityKeysListComponent, + EntityListSelectComponent, + EntityTypeListComponent, + QueueTypeListComponent, + RelationTypeAutocompleteComponent, + SocialSharePanelComponent, + JsonObjectEditComponent, + JsonContentComponent, + JsFuncComponent, + FabTriggerDirective, + FabActionsDirective, + FabToolbarComponent, + WidgetsBundleSelectComponent, + ConfirmDialogComponent, + AlertDialogComponent, + TodoDialogComponent, + ColorPickerDialogComponent, + MaterialIconsDialogComponent, + ColorInputComponent, + MaterialIconSelectComponent, + NodeScriptTestDialogComponent, + JsonFormComponent, + ImageInputComponent, + FileInputComponent, + MessageTypeAutocompleteComponent, + KeyValMapComponent, + NavTreeComponent, + LedLightComponent, + NospacePipe, + MillisecondsToTimeStringPipe, + EnumToArrayPipe, + HighlightPipe, + TruncatePipe, + TbJsonPipe, + KeyboardShortcutPipe, + TbJsonToStringDirective, + JsonObjectEditDialogComponent, + HistorySelectorComponent, + EntityGatewaySelectComponent, + ContactComponent + ], + imports: [ + CommonModule, + RouterModule, + TranslateModule, + MatButtonModule, + MatCheckboxModule, + MatIconModule, + MatCardModule, + MatProgressBarModule, + MatInputModule, + MatSnackBarModule, + MatSidenavModule, + MatToolbarModule, + MatMenuModule, + MatGridListModule, + MatDialogModule, + MatSelectModule, + MatTooltipModule, + MatTableModule, + MatPaginatorModule, + MatSortModule, + MatProgressSpinnerModule, + MatDividerModule, + MatTabsModule, + MatRadioModule, + MatSlideToggleModule, + MatDatepickerModule, + MatNativeDatetimeModule, + MatDatetimepickerModule, + NgxDaterangepickerMd.forRoot(), + MatSliderModule, + MatExpansionModule, + MatStepperModule, + MatAutocompleteModule, + MatChipsModule, + GridsterModule, + ClipboardModule, + FlexLayoutModule.withConfig({addFlexToParent: false}), + FormsModule, + ReactiveFormsModule, + OverlayModule, + ShareButtonsModule, + HotkeyModule, + ColorPickerModule, + NgxHmCarouselModule, + NgxFlowModule, + NgxFlowchartModule + ], + exports: [ + FooterComponent, + LogoComponent, + FooterFabButtonsComponent, + ToastDirective, + FullscreenDirective, + CircularProgressDirective, + MatChipDraggableDirective, + TbHotkeysDirective, + TbAnchorComponent, + HelpComponent, + TbCheckboxComponent, + TbErrorComponent, + TbCheatSheetComponent, + BreadcrumbComponent, + UserMenuComponent, + TimewindowComponent, + TimewindowPanelComponent, + TimeintervalComponent, + DashboardSelectComponent, + DatetimePeriodComponent, + DatetimeComponent, + TimezoneSelectComponent, + DashboardAutocompleteComponent, + EntitySubTypeAutocompleteComponent, + EntitySubTypeSelectComponent, + EntitySubTypeListComponent, + EntityAutocompleteComponent, + EntityListComponent, + EntityTypeSelectComponent, + EntitySelectComponent, + EntityKeysListComponent, + EntityListSelectComponent, + EntityTypeListComponent, + QueueTypeListComponent, + RelationTypeAutocompleteComponent, + SocialSharePanelComponent, + JsonObjectEditComponent, + JsonContentComponent, + JsFuncComponent, + FabTriggerDirective, + FabActionsDirective, + FabToolbarComponent, + WidgetsBundleSelectComponent, + ValueInputComponent, + MatButtonModule, + MatCheckboxModule, + MatIconModule, + MatCardModule, + MatProgressBarModule, + MatInputModule, + MatSnackBarModule, + MatSidenavModule, + MatToolbarModule, + MatMenuModule, + MatGridListModule, + MatDialogModule, + MatSelectModule, + MatTooltipModule, + MatTableModule, + MatPaginatorModule, + MatSortModule, + MatProgressSpinnerModule, + MatDividerModule, + MatTabsModule, + MatRadioModule, + MatSlideToggleModule, + MatDatepickerModule, + MatNativeDatetimeModule, + MatDatetimepickerModule, + NgxDaterangepickerMd, + MatSliderModule, + MatExpansionModule, + MatStepperModule, + MatAutocompleteModule, + MatChipsModule, + GridsterModule, + ClipboardModule, + FlexLayoutModule, + FormsModule, + ReactiveFormsModule, + OverlayModule, + ShareButtonsModule, + HotkeyModule, + ColorPickerModule, + NgxHmCarouselModule, + NgxFlowchartModule, + ConfirmDialogComponent, + AlertDialogComponent, + TodoDialogComponent, + ColorPickerDialogComponent, + MaterialIconsDialogComponent, + ColorInputComponent, + MaterialIconSelectComponent, + NodeScriptTestDialogComponent, + JsonFormComponent, + ImageInputComponent, + FileInputComponent, + MessageTypeAutocompleteComponent, + KeyValMapComponent, + NavTreeComponent, + LedLightComponent, + NospacePipe, + MillisecondsToTimeStringPipe, + EnumToArrayPipe, + HighlightPipe, + TruncatePipe, + TbJsonPipe, + KeyboardShortcutPipe, + TranslateModule, + JsonObjectEditDialogComponent, + HistorySelectorComponent, + EntityGatewaySelectComponent, + ContactComponent + ] +}) +export class SharedModule { } diff --git a/ui-ngx/src/assets/add_location.svg b/ui-ngx/src/assets/add_location.svg new file mode 100644 index 0000000000..e3150115b7 --- /dev/null +++ b/ui-ngx/src/assets/add_location.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui-ngx/src/assets/add_polygon.svg b/ui-ngx/src/assets/add_polygon.svg new file mode 100644 index 0000000000..098f02c128 --- /dev/null +++ b/ui-ngx/src/assets/add_polygon.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/src/png/jstree/32px.png b/ui-ngx/src/assets/jstree/tb32px.png similarity index 100% rename from ui/src/png/jstree/32px.png rename to ui-ngx/src/assets/jstree/tb32px.png diff --git a/ui/src/png/jstree/40px.png b/ui-ngx/src/assets/jstree/tb40px.png similarity index 100% rename from ui/src/png/jstree/40px.png rename to ui-ngx/src/assets/jstree/tb40px.png diff --git a/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json b/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json new file mode 100644 index 0000000000..e1b06f543b --- /dev/null +++ b/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json @@ -0,0 +1,1961 @@ +{ + "access": { + "unauthorized": "Neautorizováno", + "unauthorized-access": "Neautorizovaný přístup", + "unauthorized-access-text": "Musíte se zaregistrovat, abyste měli přístup k tomuto zdroji!", + "access-forbidden": "Přístup zakázán", + "access-forbidden-text": "Pro přístup do tohoto umístění nemáte přístupová oprávnění!
    Pokud si stále přejete získat přístup do tohoto umístění, zkuste se zaregistrovat pod jiným uživatelem.", + "refresh-token-expired": "Relace vypršela", + "refresh-token-failed": "Nemohu aktualizovat relaci", + "permission-denied": "Oprávnění zamítnuto", + "permission-denied-text": "Pro provedení této operace nemáte oprávnění!" + }, + "action": { + "activate": "Aktivovat", + "suspend": "Deaktivovat", + "save": "Uložit", + "saveAs": "Uložit jako", + "cancel": "Storno", + "ok": "OK", + "delete": "Smazat", + "add": "Přidat", + "yes": "Ano", + "no": "Ne", + "update": "Aktualizovat", + "remove": "Odstranit", + "select": "Vybrat", + "search": "Vyhledat", + "clear-search": "Zrušit hledání", + "assign": "Přiřadit", + "unassign": "Odebrat", + "share": "Sdílet", + "make-private": "Učinit soukromým", + "apply": "Použít", + "apply-changes": "Uložit změny", + "edit-mode": "Režim editace", + "enter-edit-mode": "Vstoupit do režimu editace", + "decline-changes": "Zahodit změny", + "close": "Zavřít", + "back": "Zpět", + "run": "Spustit", + "sign-in": "Zaregistrovat!", + "edit": "Editovat", + "view": "Zobrazit", + "create": "Vytvořit", + "drag": "Táhnout", + "refresh": "Obnovit", + "undo": "Vrátit", + "copy": "Kopírovat", + "paste": "Vložit", + "copy-reference": "Kopírovat referenci", + "paste-reference": "Vložit referenci", + "import": "Importovat", + "export": "Exportovat", + "share-via": "Sdílet přes {{provider}}", + "continue": "Pokračovat", + "discard-changes": "Zahodit změny", + "download": "Stáhnout" + }, + "aggregation": { + "aggregation": "Agregace", + "function": "Funkce pro agregaci dat", + "limit": "Maximální hodnoty", + "group-interval": "Interval seskupení", + "min": "Min", + "max": "Max", + "avg": "Průměr", + "sum": "Suma", + "count": "Počet", + "none": "Žádná" + }, + "admin": { + "general": "Obecné", + "general-settings": "Obecná nastavení", + "outgoing-mail": "Odchozí email", + "outgoing-mail-settings": "Nastavení odchozího emailu", + "system-settings": "Systémová nastavení", + "test-mail-sent": "Testovací zpráva byla úspěšně odeslána!", + "base-url": "Základní URL", + "base-url-required": "Hodnota Základní URL je povinná.", + "mail-from": "Email od", + "mail-from-required": "Hodnota Email od je povinná.", + "smtp-protocol": "SMTP protokol", + "smtp-host": "SMTP host", + "smtp-host-required": "Hodnota SMTP host je povinná.", + "smtp-port": "SMTP port", + "smtp-port-required": "Musíte zadat smtp port.", + "smtp-port-invalid": "Tohle nevypadá jako platný smtp port.", + "timeout-msec": "Časový limit (msec)", + "timeout-required": "Hodnota Časový limit je povinná.", + "timeout-invalid": "Tohle nevypadá jako platný časový limit.", + "enable-tls": "Povolit TLS", + "tls-version": "Verze TLS", + "enable-proxy": "Povolit proxy", + "proxy-host": "Host proxy", + "proxy-host-required": "Host proxy je povinný.", + "proxy-port": "Port proxy", + "proxy-port-required": "Port proxy je povinný.", + "proxy-port-range": "Port proxy by měl být v rozsahu od 1 do 65535.", + "proxy-user": "Uživatel proxy", + "proxy-password": "Heslo proxy", + "send-test-mail": "Odeslat testovací zprávu", + "security-settings": "Bezpečnostní nastavení", + "password-policy": "Politika hesel", + "minimum-password-length": "Minimální délka hesla", + "minimum-password-length-required": "Minimální délka hesla je povinná", + "minimum-password-length-range": "Minimální délka hesla by měla být v rozsahu od 5 do 50", + "minimum-uppercase-letters": "Minimální počet velkých písmen", + "minimum-uppercase-letters-range": "Minimální počet velkých písmen nemůže být záporný", + "minimum-lowercase-letters": "Minimální počet malých písmen", + "minimum-lowercase-letters-range": "Minimální počet malých písmen nemůže být záporný", + "minimum-digits": "Minimální počet číslic", + "minimum-digits-range": "Minimální počet číslic nemůže být záporný ", + "minimum-special-characters": "Minimální počet speciálních znaků", + "minimum-special-characters-range": "Minimální počet speciálních znaků nemůže být záporný", + "password-expiration-period-days": "Lhůta expirace hesla ve dnech", + "password-expiration-period-days-range": "Lhůta expirace hesla ve dnech nemůže být záporná", + "password-reuse-frequency-days": "Frekvence opětovného použití hesla ve dnech", + "password-reuse-frequency-days-range": "Frekvence opětovného použití hesla ve dnech nemůže být záporná", + "general-policy": "Obecná politika", + "max-failed-login-attempts": "Maximální počet neúspěšných pokusů o přihlášení před zablokováním účtu", + "minimum-max-failed-login-attempts-range": "Maximální počet neúspěšných pokusů o přihlášení před zablokováním účtu nemůže být záporný", + "user-lockout-notification-email": "V případě zablokování uživatelského účtu odeslat upozornění na email" + }, + "alarm": { + "alarm": "Alarm", + "alarms": "Alarmy", + "select-alarm": "Vybrat alarm", + "no-alarms-matching": "Žádné alarmy odpovídající '{{entity}}' nebyly nalezeny.", + "alarm-required": "Alarm je povinný", + "alarm-status": "Stav alarmu", + "search-status": { + "ANY": "Všechny", + "ACTIVE": "Aktivní", + "CLEARED": "Odstraněné", + "ACK": "Přijaté", + "UNACK": "Nepřijaté" + }, + "display-status": { + "ACTIVE_UNACK": "Aktivní nepřijaté", + "ACTIVE_ACK": "Aktivní přijaté", + "CLEARED_UNACK": "Odstraněné nepřijaté", + "CLEARED_ACK": "Odstraněné přijaté" + }, + "no-alarms-prompt": "Žádné alarmy nebyly nalezeny", + "created-time": "Datum vytvoření", + "type": "Typ", + "severity": "Závažnost", + "originator": "Původce", + "originator-type": "Typ původce", + "details": "Detail", + "status": "Stav", + "alarm-details": "Detail alarmu", + "start-time": "Datum zahájení", + "end-time": "Datum ukončení", + "ack-time": "Datum přijetí", + "clear-time": "Datum vyřešení", + "severity-critical": "Kritická", + "severity-major": "Vysoká", + "severity-minor": "Nízká", + "severity-warning": "Varování", + "severity-indeterminate": "Střední", + "acknowledge": "Přijmout", + "clear": "Vyřešit", + "search": "Vyhledat alarmy", + "selected-alarms": "Vybráno { count, plural, 1 {1 alarmů} other {# alarmů} }", + "no-data": "Nejsou zde žádná data", + "polling-interval": "Interval frekvence příjmu alarmů (vteřin)", + "polling-interval-required": "Interval frekvence příjmu alarmů je povinný.", + "min-polling-interval-message": "Minimální povolený interval frekvence příjmu alarmů je 1 vteřina.", + "aknowledge-alarms-title": "Přijmout { count, plural, 1 {1 alarm} other {# alarmů} }", + "aknowledge-alarms-text": "Jste si jisti že chcete přijmout { count, plural, 1 {1 alarm} other {# alarmů} }?", + "aknowledge-alarm-title": "Přijmout alarm", + "aknowledge-alarm-text": "Jste si jisti, že chcete přijmout alarm?", + "clear-alarms-title": "Odstranit { count, plural, 1 {1 alarm} other {# alarmů} }", + "clear-alarms-text": "Jste si jisti, že chcete odstranit { count, plural, 1 {1 alarm} other {# alarmů} }?", + "clear-alarm-title": "Odstranit alarm", + "clear-alarm-text": "Jste si jisti, že chcete alarm odstranit?", + "alarm-status-filter": "Filtr stavu alarmu", + "max-count-load": "Maximální počet nahraných alarmů (0 - neomezeně)", + "max-count-load-required": "Maximální počet nahraných alarmů je povinný.", + "max-count-load-error-min": "Minimální hodnota je 0.", + "fetch-size": "Velikost dávky", + "fetch-size-required": "Velikost dávky je povinná.", + "fetch-size-error-min": "Minimální hodnota je 10." + }, + "alias": { + "add": "Přidat alias", + "edit": "Editovat alias", + "name": "Název aliasu", + "name-required": "Název aliasu je povinný", + "duplicate-alias": "Alias s identickým názvem již existuje.", + "filter-type-single-entity": "Jedna entita", + "filter-type-entity-list": "Seznam entit", + "filter-type-entity-name": "Název entity", + "filter-type-state-entity": "Entita ze stavu dashboardu", + "filter-type-state-entity-description": "Entita převzata z parametrů stavu dashboardu", + "filter-type-asset-type": "Typ aktiva", + "filter-type-asset-type-description": "Aktiva typu '{{assetType}}'", + "filter-type-asset-type-and-name-description": "Aktiva typu '{{assetType}}' s názvem začínajícím '{{prefix}}'", + "filter-type-device-type": "Typ zařízení", + "filter-type-device-type-description": "Zařízení typu '{{deviceType}}'", + "filter-type-device-type-and-name-description": "Zařízení typu '{{deviceType}}' s názvem začínajícím '{{prefix}}'", + "filter-type-entity-view-type": "Typ entitního pohledu", + "filter-type-entity-view-type-description": "Entitní pohledy typu '{{entityView}}'", + "filter-type-entity-view-type-and-name-description": "Entitní pohledy typu '{{entityView}}' s názvem začínajícím '{{prefix}}'", + "filter-type-relations-query": "Dotaz na vztahy", + "filter-type-relations-query-description": "{{entities}} se {{relationType}} vztahem {{direction}} {{rootEntity}}", + "filter-type-asset-search-query": "Dotaz na vyhledání aktiva", + "filter-type-asset-search-query-description": "Aktiva typů {{assetTypes}} se {{relationType}} vztahem {{direction}} {{rootEntity}}", + "filter-type-device-search-query": "Dotaz na vyhledání zařízení", + "filter-type-device-search-query-description": "Zařízení typů {{deviceTypes}} se {{relationType}} vztahem {{direction}} {{rootEntity}}", + "filter-type-entity-view-search-query": "Dotaz na vyhledání zobrazení entity", + "filter-type-entity-view-search-query-description": "Entitní pohledy typů {{entityViewTypes}} se {{relationType}} vztahem {{direction}} {{rootEntity}}", + "entity-filter": "Filtr entity", + "resolve-multiple": "Použít jako více entit", + "filter-type": "Typ filtru", + "filter-type-required": "Typ filtru je povinný.", + "entity-filter-no-entity-matched": "Žádné entity odpovídající specifikovanému filtru nebyly nalezeny.", + "no-entity-filter-specified": "Nebyl specifikován žádný filtr entit", + "root-state-entity": "Použít stav entity dashboard jako kořenovou", + "last-level-relation": "Nahrát pouze vztah poslední úrovně", + "root-entity": "Kořenová entita", + "state-entity-parameter-name": "Název parametru stavu entity", + "default-state-entity": "Defaultní stav entity", + "default-entity-parameter-name": "Defaultně", + "max-relation-level": "Maximální úroveň vazeb", + "unlimited-level": "Neomezená úroveň", + "state-entity": "Dashboard stav entity", + "all-entities": "Všechny entity", + "any-relation": "všechny" + }, + "asset": { + "asset": "Aktivum", + "assets": "Aktiva", + "management": "Správa aktiv", + "view-assets": "Zobrazit aktiva", + "add": "Přidat aktivum", + "assign-to-customer": "Přiřadit zákazníkovi", + "assign-asset-to-customer": "Přiřadit aktiva zákazníkovi", + "assign-asset-to-customer-text": "Prosím vyberte aktiva, která mají být přiřazena zákazníkovi", + "no-assets-text": "Žádná aktiva nebyla nalezena", + "assign-to-customer-text": "Prosím vyberte zákazníka, kterému mají být aktiva přiřazena", + "public": "Veřejné", + "assignedToCustomer": "Přiřazeno zákazníkovi", + "make-public": "Zveřejnit aktivum", + "make-private": "Učinit aktivum neveřejným", + "unassign-from-customer": "Odebrat aktivum zákazníkovi", + "delete": "Smazat aktivum", + "asset-public": "Aktivum je veřejné", + "asset-type": "Typ aktiva", + "asset-type-required": "Typ aktiva je povinný.", + "select-asset-type": "Vyberte typ aktiva", + "enter-asset-type": "Zadejte typ aktiva", + "any-asset": "Všechna aktiva", + "no-asset-types-matching": "Žádné typy aktiv odpovídající '{{entitySubtype}}' nebyly nalezeny.", + "asset-type-list-empty": "Žádné typy aktiv nebyly vybrány.", + "asset-types": "Typy aktiv", + "name": "Název", + "name-required": "Název je povinný.", + "description": "Popis", + "type": "Typ", + "type-required": "Typ je povinný.", + "details": "Detail", + "events": "Události", + "add-asset-text": "Přidat nové aktivum", + "asset-details": "Detail aktiva", + "assign-assets": "Přiřadit aktiva", + "assign-assets-text": "Přiřadit { count, plural, 1 {1 aktivum} other {# aktiva} } zákazníkovi", + "delete-assets": "Smazat aktiva", + "unassign-assets": "Odebrat aktiva", + "unassign-assets-action-title": "Odebrat { count, plural, 1 {1 aktivum} other {# aktiva} } zákazníkovi", + "assign-new-asset": "Přiřadit nové aktivum", + "delete-asset-title": "Jste si jisti, že chcete smazat aktivum '{{assetName}}'?", + "delete-asset-text": "Buďte opatrní, protože po potvrzení nebude možné aktivum ani žádná související data obnovit.", + "delete-assets-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 aktivum} other {# aktiva} }?", + "delete-assets-action-title": "Smazat { count, plural, 1 {1 aktivum} other {# aktiva} }", + "delete-assets-text": "Buďte opatrní, protože po potvrzení budou všechna vybraná aktiva odstraněna a žádná související data nebude možné obnovit.", + "make-public-asset-title": "Jste si jisti, že chcete aktivum '{{assetName}}' zveřejnit?", + "make-public-asset-text": "Po potvrzení se aktivum a všechna související data stanou veřejnými a dostupnými pro ostatní.", + "make-private-asset-title": "Jste si jisti, že chcete aktivum '{{assetName}}' učinit neveřejným?", + "make-private-asset-text": "Po potvrzení se aktivum a všechna související data stanou neveřejnými a nedostupnými pro ostatní.", + "unassign-asset-title": "Jste si jisti, že chcete odebrat aktivum '{{assetName}}'?", + "unassign-asset-text": "Po potvrzení bude aktivum odebráno a nebude pro zákazníka dostupné.", + "unassign-asset": "Odebrat aktivum", + "unassign-assets-title": "Jste si jisti, že chcete odebrat { count, plural, 1 {1 aktivum} other {# aktiva} }?", + "unassign-assets-text": "Po potvrzení budou všechna vybraná aktiva odebrána a nebudou pro zákazníka dostupná.", + "copyId": "Kopírovat Id aktiva", + "idCopiedMessage": "Id aktiva bylo zkopírováno do schránky", + "select-asset": "Vybrat aktivum", + "no-assets-matching": "Žádná aktiva odpovídající '{{entity}}' nebyla nalezena.", + "asset-required": "Aktivum je povinné", + "name-starts-with": "Název aktiva začíná", + "import": "Importovat aktiva", + "asset-file": "Soubor aktiva", + "search": "Vyhledat aktiva", + "selected-assets": "Vybráno { count, plural, 1 {1 aktiv} other {# aktiv} }", + "label": "Označení" + }, + "attribute": { + "attributes": "Atributy", + "latest-telemetry": "Poslední telemetrie", + "attributes-scope": "Rozsah atributů entity", + "scope-latest-telemetry": "Poslední telemetrie", + "scope-client": "Atributy klienta", + "scope-server": "Atributy serveru", + "scope-shared": "Sdílené atributy", + "add": "Přidat atribut", + "key": "Klíč", + "last-update-time": "Čas poslední aktualizace", + "key-required": "atribut klíč je povinný.", + "value": "Hodnota", + "value-required": "Atribut hodnota je povinný.", + "delete-attributes-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 atribut} other {# atributů} }?", + "delete-attributes-text": "Buďte opatrní, protože po potvrzení budou všechny vybrané atributy odstraněny.", + "delete-attributes": "Smazat atributy", + "enter-attribute-value": "Zadejte hodnotu atributu", + "show-on-widget": "Zobrazit ve widgetu", + "widget-mode": "Režim widgetu", + "next-widget": "Další widget", + "prev-widget": "Předchozí widget", + "add-to-dashboard": "Přidat na dashboard", + "add-widget-to-dashboard": "Přidat widget na dashboard", + "selected-attributes": "Vybráno { count, plural, 1 {1 atributů} other {# atributů} }", + "selected-telemetry": "Vybráno { count, plural, 1 {1 jednotek telemetrie} other {# jednotek telemetrie} }", + "no-attributes-text": "Žádné atributy nebyly nalezeny", + "no-telemetry-text": "Žádná telemetrie nebyla nalezena" + }, + "audit-log": { + "audit": "Audit", + "audit-logs": "Záznamy auditu", + "timestamp": "Časová značka", + "entity-type": "Typ entity", + "entity-name": "Název entity", + "user": "Uživatel", + "type": "Typ", + "status": "Stav", + "details": "Detail", + "type-added": "Přidáno", + "type-deleted": "Smazáno", + "type-updated": "Aktualizováno", + "type-attributes-updated": "Atributy aktualizovány", + "type-attributes-deleted": "Atributy smazány", + "type-rpc-call": "RPC volání", + "type-credentials-updated": "Přístupové údaje aktualizovány", + "type-assigned-to-customer": "Přiřazeno zákazníkovi", + "type-unassigned-from-customer": "Odebráno zákazníkovi", + "type-activated": "Aktivováno", + "type-suspended": "Deaktivováno", + "type-credentials-read": "Zobrazení přístupových údajů", + "type-attributes-read": "Zobrazení atributů", + "type-relation-add-or-update": "Vztah aktualizován", + "type-relation-delete": "Vztah smazán", + "type-relations-delete": "Všechny vztahy smazány", + "type-alarm-ack": "Přijato", + "type-alarm-clear": "Odstraněno", + "type-login": "Přihlášení", + "type-logout": "Odhlášení", + "type-lockout": "Zablokování", + "status-success": "Úspěch", + "status-failure": "Chyba", + "audit-log-details": "Detail záznamu auditu", + "no-audit-logs-prompt": "Žádné záznamy nebyly nalezeny", + "action-data": "Data akce", + "failure-details": "Detail chyby", + "search": "Prohledat záznamy auditu", + "clear-search": "Vymazat vyhledávání" + }, + "confirm-on-exit": { + "message": "Některé změny nebyly uloženy. Jste si jisti, že chcete tuto stránku opustit?", + "html-message": "Některé změny nebyly uloženy.
    Jste si jisti, že chcete tuto stránku opustit?", + "title": "Neuložené změny" + }, + "contact": { + "country": "Stát", + "city": "Město", + "state": "Region", + "postal-code": "PSČ", + "postal-code-invalid": "Formát PSČ neplatný.", + "address": "Adresa", + "address2": "Adresa 2", + "phone": "Telefon", + "email": "Email", + "no-address": "Žádná adresa" + }, + "common": { + "username": "Uživatelské jméno", + "password": "Heslo", + "enter-username": "Zadejte uživatelské jméno", + "enter-password": "Zadejte heslo", + "enter-search": "Zadejte hledaný řetězec", + "created-time": "Datum vytvoření", + "loading": "Načítám..." + }, + "content-type": { + "json": "JSON", + "text": "Text", + "binary": "Binární (Base64)" + }, + "customer": { + "customer": "Zákazník", + "customers": "Zákazníci", + "management": "Správa zákazníků", + "dashboard": "Dashboard zákazníka", + "dashboards": "Dashboardy zákazníka", + "devices": "Zařízení zákazníka", + "entity-views": "Entitní pohledy zákazníka", + "assets": "Aktiva zákazníka", + "public-dashboards": "Veřejné dashboardy", + "public-devices": "Veřejná zařízení", + "public-assets": "Veřejná aktiva", + "public-entity-views": "Veřejné entitní pohledy", + "add": "Přidat zákazníka", + "delete": "Smazat zákazníka", + "manage-customer-users": "Spravovat uživatele zákazníka", + "manage-customer-devices": "Spravovat zařízení zákazníka", + "manage-customer-dashboards": "Spravovat dashboardy zákazníka", + "manage-public-devices": "Spravovat veřejná zařízení", + "manage-public-dashboards": "Spravovat veřejné dashboardy", + "manage-customer-assets": "Spravovat aktiva zákazníka", + "manage-public-assets": "Spravovat veřejná aktiva", + "add-customer-text": "Přidat nového zákazníka", + "no-customers-text": "Žádní zákazníci nebyli nalezeni", + "customer-details": "Detail zákazníka", + "delete-customer-title": "Jste si jisti, že chcete smazat zákazníka '{{customerTitle}}'?", + "delete-customer-text": "Buďte opatrní, protože po potvrzení nebude možné zákazníka ani žádná související data obnovit.", + "delete-customers-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 zákazníka} other {# zákazníků} }?", + "delete-customers-action-title": "Smazat { count, plural, 1 {1 zákazníka} other {# zákazníků} }", + "delete-customers-text": "Buďte opatrní, protože po potvrzení budou všichni vybraní zákazníci odstraněni a žádná související data nebude možné obnovit.", + "manage-users": "Spravovat uživatele", + "manage-assets": "Spravovat aktiva", + "manage-devices": "Spravovat zařízení", + "manage-dashboards": "Spravovat dashboardy", + "title": "Název", + "title-required": "Název je povinný.", + "description": "Popis", + "details": "Detail", + "events": "Události", + "copyId": "Kopírovat Id zákazníka", + "idCopiedMessage": "Id zákazníka bylo zkopírováno do schránky", + "select-customer": "Vybrat zákazníka", + "no-customers-matching": "Žádní zákazníci odpovídající '{{entity}}' nebyli nalezeni.", + "customer-required": "Zákazník je povinný", + "select-default-customer": "Vybrat defaultního zákazníka", + "default-customer": "Defaultní zákazník", + "default-customer-required": "Aby bylo možné ladit dashboard na úrovni tenanta, je nutné zadat defaultního zákazníka.", + "search": "Vyhledat zákazníky", + "selected-customers": "Vybráno { count, plural, 1 {1 zákazníků} other {# zákazníků} }" + }, + "datetime": { + "date-from": "Datum od", + "time-from": "Čas od", + "date-to": "Datum do", + "time-to": "Čas do" + }, + "dashboard": { + "dashboard": "Dashboard", + "dashboards": "Dashboardy", + "management": "Správa dashboardů", + "view-dashboards": "Zobrazit dashboardy", + "add": "Přidat dashboard", + "assign-dashboard-to-customer": "Přiřadit dashboard(y) zákazníkovi", + "assign-dashboard-to-customer-text": "Prosím vyberte dashboardy, které mají být přiřazeny zákazníkovi", + "assign-to-customer-text": "Prosím vyberete zákazníka, kterému má být dashboard(y) přiřazen", + "assign-to-customer": "Přiřadit zákazníkovi", + "unassign-from-customer": "Odebrat zákazníkovi", + "make-public": "Zveřejnit dashboard", + "make-private": "Učinit dashboard neveřejným", + "manage-assigned-customers": "Spravovat přiřazené zákazníky", + "assigned-customers": "Přiřazení zákazníci", + "assign-to-customers": "Přiřadit dashboard(y) zákazníkovi", + "assign-to-customers-text": "Prosím vyberte zákazníky, kterým má být dashboard(y) přiřazen(y)", + "unassign-from-customers": "Odebrat dashboard(y) zákazníkům", + "unassign-from-customers-text": "Prosím vyberte zákazníky, kterým má být dashboard(y) odebrán(y)", + "no-dashboards-text": "Žádné dashboardy nebyly nalezeny", + "no-widgets": "Nejsou nastaveny žádné widgety", + "add-widget": "Přidat nový widget", + "title": "Název", + "select-widget-title": "Vybrat widget", + "select-widget-subtitle": "Seznam dostupných typů widgetu", + "delete": "Smazat dashboard", + "title-required": "Název je povinný.", + "description": "Popis", + "details": "Detail", + "dashboard-details": "Detail dashboardu", + "add-dashboard-text": "Přidat nový dashboard", + "assign-dashboards": "Přiřadit dashboardy", + "assign-new-dashboard": "Přiřadit nový dashboard", + "assign-dashboards-text": "Přiřadit { count, plural, 1 {1 dashboard} other {# dashboardů} } zákazníkům", + "unassign-dashboards-action-text": "Odebrat { count, plural, 1 {1 dashboard} other {# dashboardů} } zákazníkům", + "delete-dashboards": "Smazat dashboardy", + "unassign-dashboards": "Odebrat dashboardy", + "unassign-dashboards-action-title": "Odebrat { count, plural, 1 {1 dashboard} other {# dashboardů} } zákazníkovi", + "delete-dashboard-title": "Jste si jisti, že chcete odstranit dashboard '{{dashboardTitle}}'?", + "delete-dashboard-text": "Buďte opatrní, protože po potvrzení nebude možné dashboard ani žádná související data obnovit.", + "delete-dashboards-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 dashboard} other {# dashboardů} }?", + "delete-dashboards-action-title": "Smazat { count, plural, 1 {1 dashboard} other {# dashboardů} }", + "delete-dashboards-text": "Buďte opatrní, protože po potvrzení budou všechny vybrané dashboardy smazány a žádná související data nebude možné obnovit.", + "unassign-dashboard-title": "Jste si jistí, že chcete odebrat dashboard '{{dashboardTitle}}'?", + "unassign-dashboard-text": "Po potvrzení bude dashboard odebrán a nebude pro zákazníka dostupný.", + "unassign-dashboard": "Odebrat dashboard", + "unassign-dashboards-title": "Jste si jisti, že chcete odebrat { count, plural, 1 {1 dashboard} other {# dashboardů} }?", + "unassign-dashboards-text": "Po potvrzení budou všechny vybrané dashboardy odebrány a nebudou pro zákazníka dostupné.", + "public-dashboard-title": "Dashboard je nyní veřejný", + "public-dashboard-text": "Váš dashboard {{dashboardTitle}} je nyní veřejný a dostupný prostřednictvím následujícího veřejného odkazu:", + "public-dashboard-notice": "Poznámka: Nezapomeňte zveřejnit také příslušná zařízení, aby bylo možné přistupovat k jejich datům.", + "make-private-dashboard-title": "Jste si jisti, že chcete dashboard '{{dashboardTitle}}' zneveřejnit?", + "make-private-dashboard-text": "Po potvrzení bude dashboard neveřejný a nebude pro ostatní dostupný.", + "make-private-dashboard": "Učinit dashboard neveřejným", + "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard", + "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard", + "select-dashboard": "Vybrat dashboard", + "no-dashboards-matching": "Žádné dashboardy odpovídající '{{entity}}' nebyly nalezeny.", + "dashboard-required": "Dashboard je povinný.", + "select-existing": "Vybrat existující dashboard", + "create-new": "Vytvořit nový dashboard", + "new-dashboard-title": "Název nového dashboardu", + "open-dashboard": "Otevřít dashboard", + "set-background": "Nastavit pozadí", + "background-color": "Barva pozadí", + "background-image": "Obrázek pozadí", + "background-size-mode": "Režim velikosti pozadí", + "no-image": "Žádný obrázek nebyl vybrán", + "drop-image": "Přetáhněte sem obrázek nebo klikněte pro výběr souboru pro nahrání.", + "settings": "Nastavení", + "columns-count": "Počet sloupců", + "columns-count-required": "Počet sloupců je povinný.", + "min-columns-count-message": "Minimální povolený počet sloupců je 10.", + "max-columns-count-message": "Maximální povolený počet sloupců je 1000.", + "widgets-margins": "Okraj mezi widgety", + "margin-required": "Hodnota okraje je povinná.", + "min-margin-message": "Pouze 0 je povolena jako minimální hodnota okraje.", + "max-margin-message": "Pouze 50 je povoleno jako maximální hodnota okraje.", + "horizontal-margin": "Horizontální okraj", + "horizontal-margin-required": "Hodnota horizontálního okraje je povinná.", + "min-horizontal-margin-message": "Minimální povolená hodnota horizontálního okraje je 0.", + "max-horizontal-margin-message": "Maximální povolená hodnota horizontálního okraje je 50.", + "vertical-margin": "Vertikální okraj", + "vertical-margin-required": "Hodnota vertikálního okraje je povinná.", + "min-vertical-margin-message": "Minimální povolená hodnota vertikálního okraje je 0.", + "max-vertical-margin-message": "Maximální povolená hodnota vertikálního okraje je 50.", + "autofill-height": "Automaticky vyplnit na výšku rozmístění", + "mobile-layout": "Nastavení rozmístění pro mobilní zařízení", + "mobile-row-height": "Výška řádku pro mobilní zařízení, px", + "mobile-row-height-required": "Hodnota výšku řádku pro mobilní zařízení je povinná.", + "min-mobile-row-height-message": "Minimální povolená hodnota výšku řádku pro mobilní zařízení je 5 pixelů.", + "max-mobile-row-height-message": "Maximální povolená hodnota výšku řádku pro mobilní zařízení je 200 pixelů.", + "display-title": "Zobrazit název dashboardu", + "toolbar-always-open": "Ponechat nástrojovou lištu otevřenou", + "title-color": "Barva názvu", + "display-dashboards-selection": "Zobrazit výběr dashboardů", + "display-entities-selection": "Zobrazit výběr entit", + "display-dashboard-timewindow": "Zobrazit časové okno", + "display-dashboard-export": "Zobrazit export", + "import": "Importovat dashboard", + "export": "Exportovat dashboard", + "export-failed-error": "Dashboard nebylo možné exportovat: {{error}}", + "create-new-dashboard": "Vytvořit nový dashboard", + "dashboard-file": "Soubor dashboardu", + "invalid-dashboard-file-error": "Dashboard nebylo možné importovat: Neplatná datová struktura dashboardu.", + "dashboard-import-missing-aliases-title": "Konfigurovat aliasy používané importovaným dashboardem", + "create-new-widget": "Přidat nový widget", + "import-widget": "Importovat widget", + "widget-file": "Soubor widgetu", + "invalid-widget-file-error": "Widget nebylo možné importovat: Neplatná datová struktura widgetu.", + "widget-import-missing-aliases-title": "Konfigurovat aliasy používané importovaným widgetem", + "open-toolbar": "Otevřít nástrojovou lištu dashboardu", + "close-toolbar": "Zavřít nástrojovou lištu", + "configuration-error": "Chyba konfigurace", + "alias-resolution-error-title": "Chyba konfigurace aliasů dashboardu", + "invalid-aliases-config": "Nebylo možné nalézt žádná zařízení odpovídající některému z aliasů ve filtru.
    Pro vyřešení tohoto problému prosím kontaktujte vašeho administrátora.", + "select-devices": "Vybrat zařízení", + "assignedToCustomer": "Přiřazeno zákazníkovi", + "assignedToCustomers": "Přiřazeno zákazníkům", + "public": "Veřejné", + "public-link": "Veřejný odkaz", + "copy-public-link": "Kopírovat veřejný odkaz", + "public-link-copied-message": "Veřejný odkaz na dashboard byl zkopírován do schránky", + "manage-states": "Spravovat stavy dashboardu", + "states": "Stavy dashboardu", + "search-states": "Vyhledat stavy dashboardu", + "selected-states": "Vybráno { count, plural, 1 {1 stavů dashboardu} other {# stavů dashboardu} }", + "edit-state": "Editovat stav dashboardu", + "delete-state": "Smazat stav dashboardu", + "add-state": "Přidat stav dashboardu", + "no-states-text": "Žádné No states found", + "state": "Stav dashboardu", + "state-name": "Název", + "state-name-required": "Název stavu dashboardu je povinný.", + "state-id": "Id stavu", + "state-id-required": "Id stavu dashboardu je povinné.", + "state-id-exists": "Stav dashboardu s identickým Id již existuje.", + "is-root-state": "Základní stav", + "delete-state-title": "Smazat stav dashboardu", + "delete-state-text": "Jste si jisti, že chcete odstranit stav dashboardu s názvem '{{stateName}}'?", + "show-details": "Zobrazit detaily", + "hide-details": "Skrýt detaily", + "select-state": "Vybrat cílový stav", + "state-controller": "Kontrolér stavu", + "search": "Vyhledat dashboardy", + "selected-dashboards": "Vybráno { count, plural, 1 {1 dashboardů} other {# dashboardů} }" + }, + "datakey": { + "settings": "Nastavení", + "advanced": "Rozšířené", + "label": "Označení", + "color": "Barva", + "units": "Speciální symbol, který bude zobrazen vedle hodnoty", + "decimals": "Počet číslic za desetinnou čárkou", + "data-generation-func": "Funkce pro generování dat", + "use-data-post-processing-func": "Použít funkci pro následné zpracování", + "configuration": "Konfigurace datového klíče", + "timeseries": "Časové řady", + "attributes": "Atributy", + "entity-field": "Pole entity", + "alarm": "Pole alarmu", + "timeseries-required": "Časové řady entity jsou povinné.", + "timeseries-or-attributes-required": "Časové řady / atributy entity jsou povinné.", + "maximum-timeseries-or-attributes": "Maximálně { count, plural, 1 {1 časová řada/atribut je povolena.} other {# časových řad/atributů je povoleno} }", + "alarm-fields-required": "Pole alarmu jsou povinná.", + "function-types": "Typy funkcí", + "function-types-required": "Typy funkcí jsou povinné.", + "maximum-function-types": "Maximálně { count, plural, 1 {1 typ funkce je povolen.} other {# typů funkce je povoleno} }", + "time-description": "Časová značka aktuální hodnoty;", + "value-description": "Aktuální hodnota;", + "prev-value-description": "Výsledek předchozího volání funkce;", + "time-prev-description": "Časová značka předchozí hodnoty;", + "prev-orig-value-description": "Původní předchozí hodnota;" + }, + "datasource": { + "type": "Typ datového zdroje", + "name": "Název", + "add-datasource-prompt": "Přidejte prosím datový zdroj" + }, + "details": { + "details": "Detail", + "edit-mode": "Režim editace", + "edit-json": "Editovat JSON", + "toggle-edit-mode": "Přepnout do režimu editace" + }, + "device": { + "device": "Zařízení", + "device-required": "Zařízení je povinné.", + "devices": "Zařízení", + "management": "Správa zařízení", + "view-devices": "Zobrazit zařízení", + "device-alias": "Alias zařízení", + "aliases": "Aliasy zařízení", + "no-alias-matching": "'{{alias}}' nenalezen.", + "no-aliases-found": "Žádné aliasy nebyly nalezeny.", + "no-key-matching": "'{{key}}' nenalezen.", + "no-keys-found": "Žádné klíče nebyly nalezeny.", + "create-new-alias": "Vytvořit nový!", + "create-new-key": "Vytvořit nový!", + "duplicate-alias-error": "Byl nalezen duplicitní alias '{{alias}}'.
    Aliasy zařízení musí být v rámci dashboardu unikátní.", + "configure-alias": "Konfigurovat '{{alias}}' alias", + "no-devices-matching": "Žádná zařízení odpovídající '{{entity}}' nebyla nalezena.", + "alias": "Alias", + "alias-required": "Alias zařízení je povinný.", + "remove-alias": "Odebrat alias zařízení", + "add-alias": "Přidat alias zařízení", + "name-starts-with": "Název zařízení začíná", + "device-list": "Seznam zařízení", + "use-device-name-filter": "Použít filtr", + "device-list-empty": "Nebyla vybrána žádná zařízení.", + "device-name-filter-required": "Název filtru zařízení je povinný.", + "device-name-filter-no-device-matched": "Žádná zařízení začínající '{{device}}' nebyla nalezena.", + "add": "Přidat zařízení", + "assign-to-customer": "Přiřadit zákazníkovi", + "assign-device-to-customer": "Přiřadit zařízení zákazníkovi", + "assign-device-to-customer-text": "Vyberte prosím zařízení, která mají být přiřazena zákazníkovi", + "make-public": "Zveřejnit zařízení", + "make-private": "Učinit zařízení neveřejným", + "no-devices-text": "Žádná zařízení nebyla nalezena", + "assign-to-customer-text": "Vyberte prosím zákazníka, který má být přiřazen zařízení(m)", + "device-details": "Detail zařízení", + "add-device-text": "Přidat nové zařízení", + "credentials": "Přístupové údaje", + "manage-credentials": "Spravovat přístupové údaje", + "delete": "Smazat zařízení", + "assign-devices": "Přiřadit zařízení", + "assign-devices-text": "Přiřadit { count, plural, 1 {1 zařízení} other {# zařízení} } zákazníkovi", + "delete-devices": "Smazat zařízení", + "unassign-from-customer": "Odebrat zákazníkovi", + "unassign-devices": "Odebrat zařízení", + "unassign-devices-action-title": "Odebrat { count, plural, 1 {1 zařízení} other {# zařízení} } zákazníkovi", + "assign-new-device": "Přiřadit nové zařízení", + "make-public-device-title": "Jste si jisti, že chcete zařízení '{{deviceName}}' zveřejnit?", + "make-public-device-text": "Po potvrzení bude zařízení a všechna jeho data veřejná a dostupná pro ostatní.", + "make-private-device-title": "Jste si jisti, že chcete zařízení '{{deviceName}}' učinit neveřejným?", + "make-private-device-text": "Po potvrzení budou zařízení a všechna jeho data neveřejné a nedostupné pro ostatní.", + "view-credentials": "Zobrazit přístupové údaje", + "delete-device-title": "Jste si jisti, že chcete smazat zařízení '{{deviceName}}'?", + "delete-device-text": "Buďte opatrní, protože po potvrzení nebude možné zařízení ani žádná související data obnovit.", + "delete-devices-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 zařízení} other {# zařízení} }?", + "delete-devices-action-title": "Smazat { count, plural, 1 {1 zařízení} other {# zařízení} }", + "delete-devices-text": "Buďte opatrní, protože po potvrzení budou vybraná zařízení odstraněna a žádná související data nebude možné obnovit.", + "unassign-device-title": "Jste si jisti, že chcete odebrat zařízení '{{deviceName}}'?", + "unassign-device-text": "Po potvrzení bude zařízení odebráno a nebude pro zákazníka dostupné.", + "unassign-device": "Odebrat zařízení", + "unassign-devices-title": "Jste si jisti, že chcete odebrat { count, plural, 1 {1 zařízení} other {# zařízení} }?", + "unassign-devices-text": "Po potvrzení budou všechna vybraná zařízení odebrána a nebudou pro zákazníka dostupná.", + "device-credentials": "Přístupové údaje zařízení", + "credentials-type": "Typ přístupových údajů", + "access-token": "Přístupový token", + "access-token-required": "Přístupový token je povinný.", + "access-token-invalid": "Délka přístupového tokenu musí být od 1 do 20 znaků.", + "rsa-key": "RSA veřejný klíč", + "rsa-key-required": "RSA veřejný klíč je povinný.", + "secret": "Heslo", + "secret-required": "Heslo je povinné.", + "device-type": "Typ zařízení", + "device-type-required": "Typ zařízení je povinný.", + "select-device-type": "Vybrat typ zařízení", + "enter-device-type": "Zadejte typ zařízení", + "any-device": "Všechna zařízení", + "no-device-types-matching": "Žádné typy zařízení odpovídající '{{entitySubtype}}' nebyly nalezeny.", + "device-type-list-empty": "Nebyl vybrán typ zařízení.", + "device-types": "Typy zařízení", + "name": "Název", + "name-required": "Název je povinný.", + "description": "Popis", + "label": "Označení", + "events": "Události", + "details": "Detail", + "copyId": "Kopírovat Id zařízení", + "copyAccessToken": "Kopírovat přístupový token", + "idCopiedMessage": "Id zařízení bylo zkopírováno do schránky", + "accessTokenCopiedMessage": "Přístupový token zařízení byl zkopírován do schránky", + "assignedToCustomer": "Přiřazeno zákazníkovi", + "unable-delete-device-alias-title": "Nebylo možné smazat alias zařízení", + "unable-delete-device-alias-text": "Alias zařízení '{{deviceAlias}}' nelze smazat, protože je používán následujícími widgety:
    {{widgetsList}}", + "is-gateway": "Je bránou", + "public": "Veřejné", + "device-public": "Zařízení je veřejné", + "select-device": "Vybrat zařízení", + "import": "Importovat zařízení", + "device-file": "Soubor zařízení", + "search": "Vyhledat zařízení", + "selected-devices": "Vybráno { count, plural, 1 {1 zařízení} other {# zařízení} }" + }, + "dialog": { + "close": "Zavřít dialog" + }, + "direction": { + "column": "Sloupec", + "row": "Řádek" + }, + "error": { + "unable-to-connect": "Nebylo možné se připojit k serveru! Zkontrolujte internetové připojení.", + "unhandled-error-code": "Neošetřený chybový kód: {{errorCode}}", + "unknown-error": "Neznámá chyba" + }, + "entity": { + "entity": "Entita", + "entities": "Entity", + "aliases": "Entitní aliasy", + "entity-alias": "Alias entity", + "unable-delete-entity-alias-title": "Alias entity nebylo možné smazat", + "unable-delete-entity-alias-text": "Alias entity '{{entityAlias}}' nelze smazat, protože je používán následujícími widgety:
    {{widgetsList}}", + "duplicate-alias-error": "Nalezen dupliticní alias '{{alias}}'.
    Aliasy entit musí být v rámci dashboardu unikátní.", + "missing-entity-filter-error": "Ve filtru chybí alias '{{alias}}'.", + "configure-alias": "Konfigurovat '{{alias}}' alias", + "alias": "Alias", + "alias-required": "Alias entity je povinný", + "remove-alias": "Odebrat alias entity", + "add-alias": "Přidat alias entity", + "entity-list": "Seznam entit", + "entity-type": "Typ entity", + "entity-types": "Typy entit", + "entity-type-list": "Seznam typů entit", + "any-entity": "Všechny entity", + "enter-entity-type": "Zadat typ entity", + "no-entities-matching": "Žádné entity odpovídající '{{entity}}' nebyly nalezeny.", + "no-entity-types-matching": "Žádné entity odpovídající '{{entityType}}' nebyly nalezeny.", + "name-starts-with": "Název začíná", + "use-entity-name-filter": "Použít filtr", + "entity-list-empty": "Žádné entity nebyly nalezeny.", + "entity-type-list-empty": "Nebyl vybrán žádný typ entity.", + "entity-name-filter-required": "Filtr názvu entity je povinný.", + "entity-name-filter-no-entity-matched": "Žádné entity začínající '{{entity}}' nebyly nalezeny.", + "all-subtypes": "Vše", + "select-entities": "Vybrat entity", + "no-aliases-found": "Žádné aliasy nebyly nalezeny.", + "no-alias-matching": "'{{alias}}' nebyl nalezen.", + "create-new-alias": "Vytvořit nový!", + "key": "Klíč", + "key-name": "Název klíče", + "no-keys-found": "Nebyly nalezeny žádné klíče.", + "no-key-matching": "'{{key}}' nebyl nalezen.", + "create-new-key": "Vytvořit nový!", + "type": "Typ", + "type-required": "Typ entity je povinný.", + "type-device": "Zařízení", + "type-devices": "Zařízení", + "list-of-devices": "{ count, plural, 1 {Jedno zařízení} other {Seznam # zařízení} }", + "device-name-starts-with": "Zařízení, jejichž název začíná '{{prefix}}'", + "type-asset": "Aktivum", + "type-assets": "Aktiva", + "list-of-assets": "{ count, plural, 1 {Jedno aktivum} other {Seznam # aktiv} }", + "asset-name-starts-with": "Aktiva, jejichž název začíná '{{prefix}}'", + "type-entity-view": "Entitní pohled", + "type-entity-views": "Entitní pohledy", + "list-of-entity-views": "{ count, plural, 1 {Jeden entitní pohled} other {Seznam # entitních pohledů} }", + "entity-view-name-starts-with": "Entitní pohledy, jejichž název začíná '{{prefix}}'", + "type-rule": "Pravidlo", + "type-rules": "Pravidla", + "list-of-rules": "{ count, plural, 1 {Jedno pravidlo} other {Seznam # pravidel} }", + "rule-name-starts-with": "Pravidla, jejichž název začíná '{{prefix}}'", + "type-plugin": "Zásuvný modul", + "type-plugins": "Zásuvné moduly", + "list-of-plugins": "{ count, plural, 1 {Jeden zásuvný modul} other {Seznam # zásuvných modulů} }", + "plugin-name-starts-with": "Zásuvné moduly, jejichž název začíná '{{prefix}}'", + "type-tenant": "Tenant", + "type-tenants": "Tenanti", + "list-of-tenants": "{ count, plural, 1 {Jeden tenant} other {Seznam # tenantů} }", + "tenant-name-starts-with": "Tenanti, jejichž název začíná '{{prefix}}'", + "type-customer": "Zákazník", + "type-customers": "Zákazníci", + "list-of-customers": "{ count, plural, 1 {Jeden zákazník} other {Seznam # zákazníků} }", + "customer-name-starts-with": "Zákazníci, jejichž název začíná '{{prefix}}'", + "type-user": "Uživatel", + "type-users": "Uživatelé", + "list-of-users": "{ count, plural, 1 {Jeden uživatel} other {Seznam # uživatelů} }", + "user-name-starts-with": "Uživatelé, jejichž název začíná '{{prefix}}'", + "type-dashboard": "Dashboard", + "type-dashboards": "Dashboardy", + "list-of-dashboards": "{ count, plural, 1 {Jeden dashboard} other {Seznam # dashboardů} }", + "dashboard-name-starts-with": "Dashboardy, jejichž název začíná '{{prefix}}'", + "type-alarm": "Alarm", + "type-alarms": "Alarmy", + "list-of-alarms": "{ count, plural, 1 {Jeden alarm} other {Seznam # alarmů} }", + "alarm-name-starts-with": "Alarmy, jejichž název začíná '{{prefix}}'", + "type-rulechain": "Řetěz pravidel", + "type-rulechains": "Řetězy pravidel", + "list-of-rulechains": "{ count, plural, 1 {Jeden řetěz pravidel} other {Seznam # řetězů pravidel} }", + "rulechain-name-starts-with": "Řetězy pravidel, jejichž název začíná '{{prefix}}'", + "type-rulenode": "Uzel pravidla", + "type-rulenodes": "Uzly pravidel", + "list-of-rulenodes": "{ count, plural, 1 {Jeden uzel pravidla} other {Seznam # uzlů pravidel} }", + "rulenode-name-starts-with": "Uzly pravidel, jejichž název začíná '{{prefix}}'", + "type-current-customer": "Stávající zákazník", + "type-current-tenant": "Stávající tenant", + "search": "Vyhledat entity", + "selected-entities": "{ count, plural, 1 {1 entita} other {# entit} } zvoleno", + "entity-name": "Název entity", + "entity-label": "Označení entity", + "details": "Detail entity", + "no-entities-prompt": "Žádné entity nebyly nalezeny", + "no-data": "Nelze zobrazit žádná data", + "columns-to-display": "Zobrazit sloupce" + }, + "entity-field": { + "created-time": "Datum vytvoření", + "name": "Název", + "type": "Typ", + "first-name": "Jméno", + "last-name": "Příjmení", + "email": "Email", + "title": "Titul", + "country": "Stát", + "state": "Region", + "city": "Město", + "address": "Adresa", + "address2": "Adresa 2", + "zip": "PSČ", + "phone": "Telefon", + "label": "Označení" + }, + "entity-view": { + "entity-view": "Entitní pohled", + "entity-view-required": "Entitní pohled je povinný.", + "entity-views": "Entitní pohledy", + "management": "Správa entitních pohledů", + "view-entity-views": "Zobrazit entitní pohledy", + "entity-view-alias": "Alias entitního pohledu", + "aliases": "Aliasy entitního pohledu", + "no-alias-matching": "'{{alias}}' nenalezen.", + "no-aliases-found": "Žádné aliasy nebyly nalezeny.", + "no-key-matching": "'{{key}}' nenalezen.", + "no-keys-found": "Žádné klíče nebyly nalezeny.", + "create-new-alias": "Vytvořit nový!", + "create-new-key": "Vytvořit nový!", + "duplicate-alias-error": "Byl nalezen duplicitní alias '{{alias}}'.
    Aliasy entitních pohledů musí být v rámci dashboardu unikátní.", + "configure-alias": "Konfigurovat '{{alias}}' alias", + "no-entity-views-matching": "Žádné entitní pohledy odpovídající '{{entity}}' nebyly nalezeny.", + "public": "Veřejný", + "alias": "Alias", + "alias-required": "Alias entitního pohledu je povinný.", + "remove-alias": "Odebrat alias entitního pohledu", + "add-alias": "Přidat alias entitního pohledu", + "name-starts-with": "Název entitního pohledu začíná", + "entity-view-list": "Seznam entitních pohledů", + "use-entity-view-name-filter": "Použít filtr", + "entity-view-list-empty": "Nebyly vybrány žádné entitní pohledy.", + "entity-view-name-filter-required": "Filtr názvu entitního pohledu je povinný.", + "entity-view-name-filter-no-entity-view-matched": "Žádné entitní pohledy začínající '{{entityView}}' nebyly nalezeny.", + "add": "Přidat entitní pohled", + "entity-view-public": "Entitní pohled je veřejný", + "assign-to-customer": "Přiřadit zákazníkovi", + "assign-entity-view-to-customer": "Přiřadit entitní pohled(y) zákazníkovi", + "assign-entity-view-to-customer-text": "Vyberte prosím entitní pohledy, které mají být přiřazeny zákazníkovi", + "no-entity-views-text": "Žádné entitní pohledy nebyly nalezeny", + "assign-to-customer-text": "Vyberte prosím zákazníka, kterému má být entitní pohled(y) přiřazen(y)", + "entity-view-details": "Detail entitního pohledu", + "add-entity-view-text": "Přidat nový entitní pohled", + "delete": "Smazat entitní pohled", + "assign-entity-views": "Přiřadit entitní pohledy", + "assign-entity-views-text": "Přiřadit { count, plural, 1 {1 entitní pohled} other {# entitních pohledů} } zákazníkovi", + "delete-entity-views": "Smazat entitní pohledy", + "unassign-from-customer": "Odebrat zákazníkovi", + "unassign-entity-views": "Odebrat entitní pohledy", + "unassign-entity-views-action-title": "Odebrat { count, plural, 1 {1 entitní pohled} other {# entitních pohledů} } zákazníkovi", + "assign-new-entity-view": "Přiřadit nový entitní pohled", + "delete-entity-view-title": "Jste si jisti, že chcete smazat entitní pohled '{{entityViewName}}'?", + "delete-entity-view-text": "Buďte opatrní, protože po potvrzení nebude možné entitní pohled ani žádná související data obnovit.", + "delete-entity-views-title": "Jste si jisti, že chcete odstranit entitní pohled { count, plural, 1 {1 entitní pohled} other {# entitních pohledů} }?", + "delete-entity-views-action-title": "Smazat { count, plural, 1 {1 entitní pohled} other {# entitních pohledů} }", + "delete-entity-views-text": "Buďte opatrní, protože po potvrzení budou všechny vybrané entitní pohledy smazány a žádná související data nebude možné obnovit.", + "unassign-entity-view-title": "Jste si jisti, že chcete odebrat entitní pohled '{{entityViewName}}'?", + "unassign-entity-view-text": "Po potvrzení bude entitní pohled odebrán a nebude pro zákazníka dostupný.", + "unassign-entity-view": "Odebrat entitní pohled", + "unassign-entity-views-title": "Jste si jisti, že chcete odebrat { count, plural, 1 {1 entitní pohled} other {# entitních pohledů} }?", + "unassign-entity-views-text": "Po potvrzení budou všechny vybrané entitní pohledy odebrány a nebudou pro zákazníka dostupné.", + "entity-view-type": "Typ entitního pohledu", + "entity-view-type-required": "Typ entitního pohledu je povinný.", + "select-entity-view-type": "Vybrat typ entitního pohledu", + "enter-entity-view-type": "Zadat typ entitního pohledu", + "any-entity-view": "Všechny entitní pohledy", + "no-entity-view-types-matching": "Žádné typy entitních pohledů odpovídající '{{entitySubtype}}' nebyly nalezeny.", + "entity-view-type-list-empty": "Žádné typy entitních pohledů nebyly nalezeny.", + "entity-view-types": "Typy entitních pohledů", + "created-time": "Datum vytvoření", + "name": "Název", + "name-required": "Název je povinný.", + "description": "Popis", + "events": "Události", + "details": "Detail", + "copyId": "Kopírovat Id entitního pohledu", + "idCopiedMessage": "Id entitního pohledu bylo zkopírováno do schránky", + "assignedToCustomer": "Přiřazeno zákazníkovi", + "unable-entity-view-device-alias-title": "Alias entitního typu nebylo možné smazat", + "unable-entity-view-device-alias-text": "Alias zařízení '{{entityViewAlias}}' nelze smazat, protože je používán následujícími widgety:
    {{widgetsList}}", + "select-entity-view": "Vybrat entitní pohled", + "make-public": "Zveřejnit entitní pohled", + "make-private": "Učinit entitní pohled neveřejným", + "start-date": "Datum zahájení", + "start-ts": "Čas zahájení", + "end-date": "Datum ukončení", + "end-ts": "Čas ukončení", + "date-limits": "Omezení data", + "client-attributes": "Klientské atributy", + "shared-attributes": "Sdílené atributy", + "server-attributes": "Serverové atributy", + "timeseries": "Časové řady", + "client-attributes-placeholder": "Klientské atributy", + "shared-attributes-placeholder": "Sdílené atributy", + "server-attributes-placeholder": "Serverové atributy", + "timeseries-placeholder": "Časové řady", + "target-entity": "Cílová entita", + "attributes-propagation": "Propagace atributů", + "attributes-propagation-hint": "Entitní pohled bude automaticky kopírovat specifikované atributy z cílové entity vždy, když uložíte nebo aktualizujete tento entitní pohled. Z výkonnostních důvodů nejsou atributy cílové entity propagovány při každé změně atributu. Automatickou propagaci můžete povolit konfigurací \"copy to view\" uzlu pravidla v rámci vašeho řetězu pravidel a provázáním \"Post attributes\" a \"Attributes Updated\" zpráv na nový uzel pravidla.", + "timeseries-data": "Data časových řad", + "timeseries-data-hint": "Nakonfigurujte klíče dat časových řad cílové entity, která budou dostupná pro entitní pohled. Tato data časových řad jsou pouze pro čtení.", + "make-public-entity-view-title": "Jste si jisti, že chcete entitní pohled '{{entityViewName}}' zveřejnit?", + "make-public-entity-view-text": "Po potvrzení bude entitní pohled a všechna jeho data veřejné a dostupné pro ostatní.", + "make-private-entity-view-title": "Jste si jisti, že chcete entitní pohled '{{entityViewName}}' učinit neveřejným?", + "make-private-entity-view-text": "Po potvrzení bude entitní pohled a všechna jeho data neveřejné a nebudou dostupné pro ostatní.", + "search": "Vyhledat entitní pohledy", + "selected-entity-views": "Vybráno { count, plural, 1 {1 entitních pohledů} other {# entitních pohledů} }" + }, + "event": { + "event-type": "Typ události", + "type-error": "Chyba", + "type-lc-event": "Událost životního cyklu", + "type-stats": "Statistika", + "type-debug-rule-node": "Ladění", + "type-debug-rule-chain": "Ladění", + "no-events-prompt": "Žádné události nebyly nalezeny", + "error": "Chyba", + "alarm": "Alarm", + "event-time": "Čas události", + "server": "Server", + "body": "Tělo", + "method": "Metoda", + "type": "Typ", + "entity": "Entita", + "message-id": "Id zprávy", + "message-type": "Typ zprávy", + "data-type": "Typ dat", + "relation-type": "Typ vztahu", + "metadata": "Metadata", + "data": "Data", + "event": "Událost", + "status": "Stav", + "success": "Úspěch", + "failed": "Neúspěch", + "messages-processed": "Zpracované zprávy", + "errors-occurred": "Vyskytly se chyby" + }, + "extension": { + "extensions": "Rozšíření", + "selected-extensions": "Vybráno { count, plural, 1 {1 rozšíření} other {# rozšíření} }", + "type": "Typ", + "key": "Klíč", + "value": "Hodnota", + "id": "Id", + "extension-id": "Id rozšíření", + "extension-type": "Typ rozšíření", + "transformer-json": "JSON *", + "unique-id-required": "Id stávajícího rozšíření již existuje.", + "delete": "Smazat rozšíření", + "add": "Přidat rozšíření", + "edit": "Editovat rozšíření", + "delete-extension-title": "Jste si jisti, že chcete smazat rozšíření '{{extensionId}}'?", + "delete-extension-text": "Buďte opatrní, protože po potvrzení nebude možné rozšíření ani související data obnovit.", + "delete-extensions-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 rozšíření} other {# rozšíření} }?", + "delete-extensions-text": "Buďte opatrní, protože po potvrzení budou všechna vybraná rozšíření odstraněna.", + "converters": "Převodník", + "converter-id": "Id převodníku", + "configuration": "Konfigurace", + "converter-configurations": "Konfigurace převodníku", + "token": "Bezpečnostní token", + "add-converter": "Přidat převodník", + "add-config": "Přidat konfiguraci převodníku", + "device-name-expression": "Výraz názvu zařízení", + "device-type-expression": "Výraz typu zařízení", + "custom": "Vlastní", + "to-double": "Zdvojnásobit", + "transformer": "Transformátor", + "json-required": "JSON transformátoru je povinný.", + "json-parse": "Nebylo možné parsovat JSON transformátoru.", + "attributes": "Atributy", + "add-attribute": "Přidat atributy", + "add-map": "Přidat mapovací prvek", + "timeseries": "Časové řady", + "add-timeseries": "Přidat časové řady", + "field-required": "Pole je povinné", + "brokers": "Message brokers", + "add-broker": "Přidat message brokera", + "host": "Host", + "port": "Port", + "port-range": "Port by měl být v rozsahu mezi 1 a 65535.", + "ssl": "Ssl", + "credentials": "Přístupové údaje", + "username": "Uživatelské jméno", + "password": "Heslo", + "retry-interval": "Interval pro další pokus v milisekundách", + "anonymous": "Anonymní", + "basic": "Základní", + "pem": "PEM", + "ca-cert": "soubor CA certifikátu *", + "private-key": "Soubor privátního klíče *", + "cert": "Soubor certifikátu *", + "no-file": "Žádný soubor nebyl vybrán.", + "drop-file": "Přetáhněte sem soubor nebo klikněte pro výběr souboru pro nahrání.", + "mapping": "Mapování", + "topic-filter": "Filtr MQ fronty", + "converter-type": "Typ převodníku", + "converter-json": "JSON", + "json-name-expression": "JSON výraz názvu zařízení", + "topic-name-expression": "Výraz názvu MQ fronty", + "json-type-expression": "JSON Výraz typu zařízení", + "topic-type-expression": "Výraz MQ fronty typu zařízení", + "attribute-key-expression": "Výraz klíče atributu", + "attr-json-key-expression": "JSON výraz klíče atributu", + "attr-topic-key-expression": "Výraz MQ fronty klíče atributu", + "request-id-expression": "Výraz Id požadavku", + "request-id-json-expression": "JSON výraz Id požadavku", + "request-id-topic-expression": "Výraz MQ fronty ID požadavku", + "response-topic-expression": "Výraz fronty odpovědi", + "value-expression": "Výraz hodnoty", + "topic": " MQ fronta", + "timeout": "Timeout v milisekundách", + "converter-json-required": "JSON převodníku je povinný.", + "converter-json-parse": "JSON převodníku nebylo možné parsovat.", + "filter-expression": "Výraz filtru", + "connect-requests": "Požadavky na spojení", + "add-connect-request": "Přidat požadavek na spojení", + "disconnect-requests": "Požadavky na odpojení", + "add-disconnect-request": "Přidat požadavek na odpojení", + "attribute-requests": "Požadavky na atribut", + "add-attribute-request": "Přidat požadavek na atribut", + "attribute-updates": "Aktualizace atributu", + "add-attribute-update": "Přidat aktualizaci atributu", + "server-side-rpc": "RPC na straně serveru", + "add-server-side-rpc-request": "Přidat požadavek na RPC na straně serveru", + "device-name-filter": "Filtr názvu zařízení", + "attribute-filter": "Filtr atributu", + "method-filter": "Filtr metody", + "request-topic-expression": "Výraz požadavku na MQ frontu", + "response-timeout": "Timeout odpovědi v milisekundách", + "topic-expression": "Výraz MQ fronty", + "client-scope": "Scope klienta", + "add-device": "Přidat zařízení", + "opc-server": "Servery", + "opc-add-server": "Přidat server", + "opc-add-server-prompt": "Prosím přidejte server", + "opc-application-name": "Název aplikace", + "opc-application-uri": "URI aplikace", + "opc-scan-period-in-seconds": "Interval skenování ve vteřinách", + "opc-security": "Bezpečnost", + "opc-identity": "Identita", + "opc-keystore": "Úložiště klíčů", + "opc-type": "Typ", + "opc-keystore-type": "Typ", + "opc-keystore-location": "Umístění *", + "opc-keystore-password": "Heslo", + "opc-keystore-alias": "Alias", + "opc-keystore-key-password": "Heslo klíče", + "opc-device-node-pattern": "Vzor uzlu zařízení", + "opc-device-name-pattern": "Vzor názvu zařízení", + "modbus-server": "Servery/slaves", + "modbus-add-server": "Přidat server/slave", + "modbus-add-server-prompt": "Prosím přidejte server/slave", + "modbus-transport": "Transport", + "modbus-tcp-reconnect": "Automaticky znovu připojit", + "modbus-rtu-over-tcp": "RTU přes TCP", + "modbus-port-name": "Název sériového portu", + "modbus-encoding": "Šifrování", + "modbus-parity": "Parita", + "modbus-baudrate": "Baud rate", + "modbus-databits": "Data bits", + "modbus-stopbits": "Stop bits", + "modbus-databits-range": "Data bits by měly být v rozsahu od 7 do 8.", + "modbus-stopbits-range": "Stop bits by měly být v rozsahu od 1 do 2.", + "modbus-unit-id": "ID jednotky", + "modbus-unit-id-range": "ID jednotky by mělo být v rozsahu od 1 do 247.", + "modbus-device-name": "Název zařízení", + "modbus-poll-period": "Interval kontroly (ms)", + "modbus-attributes-poll-period": "Interval kontroly atributů (ms)", + "modbus-timeseries-poll-period": "Interval kontroly časových řad (ms)", + "modbus-poll-period-range": "Interval kontroly by měl mít kladnou hodnotu.", + "modbus-tag": "Štítek", + "modbus-function": "Funkce", + "modbus-register-address": "Adresa registrace", + "modbus-register-address-range": "Adresa registrace by měla být v rozsahu od 0 do 65535.", + "modbus-register-bit-index": "Bit index", + "modbus-register-bit-index-range": "Bit index by měl být v rozsahu od 0 do 15.", + "modbus-register-count": "Počet registrací", + "modbus-register-count-range": "Počet registrací by měl mít kladnou hodnotu.", + "modbus-byte-order": "Byte order", + "sync": { + "status": "Stav", + "sync": "Synchronizováno", + "not-sync": "Nesynchronizováno", + "last-sync-time": "Čas poslední synchronizace", + "not-available": "Nedostupné" + }, + "export-extensions-configuration": "Exportovat konfiguraci rozšíření", + "import-extensions-configuration": "Importovat konfiguraci rozšíření", + "import-extensions": "Importovat rozšíření", + "import-extension": "Importovat rozšíření", + "export-extension": "Exportovat rozšíření", + "file": "Soubor rozšíření", + "invalid-file-error": "Neplatný soubor rozšíření" + }, + "fullscreen": { + "expand": "Rozšířit do režimu celé obrazovky", + "exit": "Ukončit režim celé obrazovky", + "toggle": "Přepnout do režimu celé obrazovky", + "fullscreen": "Celá obrazovka" + }, + "function": { + "function": "Funkce" + }, + "gateway": { + "add-entry": "Přidat konfiguraci", + "connector-add": "Přidat nový konektor", + "connector-enabled": "Povolit konektor", + "connector-name": "Název konektoru", + "connector-name-required": "Název konektoru je povinný.", + "connector-type": "Typ konektoru", + "connector-type-required": "Typ konektoru je povinný.", + "connectors": "Konfigurace konektoru", + "create-new-gateway": "Vytvořit novou bránu", + "create-new-gateway-text": "Jste si jisti, že chcete vytvořit novou bránu s názvem: '{{gatewayName}}'?", + "delete": "Smazat konfiguraci", + "download-tip": "Stáhnout soubor konfigurace", + "gateway": "Brána", + "gateway-exists": "Zařízení se shodným názvem již existuje.", + "gateway-name": "Název brány", + "gateway-name-required": "Název brány je povinný.", + "gateway-saved": "Konfigurace brány byla úspěšně uložena.", + "json-parse": "Neplatný JSON.", + "json-required": "Pole nemůže být prázdné.", + "no-connectors": "Žádné konektory", + "no-data": "Žádné konfigurace", + "no-gateway-found": "Žádné brány nebyly nalezeny.", + "no-gateway-matching": " '{{item}}' nenalezena.", + "path-logs": "Cesta k souborům logu", + "path-logs-required": "Cesta je povinná.", + "remote": "Vzdálená konfigurace", + "remote-logging-level": "Úroveň logování", + "remove-entry": "Odstranit konfiguraci", + "save-tip": "Uložit soubor konfigurace", + "security-type": "Typ zabezpečení", + "security-types": { + "access-token": "Přístupový token", + "tls": "TLS" + }, + "storage": "Úložiště", + "storage-max-file-records": "Maximální počet záznamů v souboru", + "storage-max-files": "Maximální počet souborů", + "storage-max-files-min": "Minimální počet je 1.", + "storage-max-files-pattern": "Počet není platný.", + "storage-max-files-required": "Počet je povinný.", + "storage-max-records": "Maximální počet záznamů v úložišti", + "storage-max-records-min": "Minimální počet záznamů je 1.", + "storage-max-records-pattern": "Počet není platný.", + "storage-max-records-required": "Maximální počet záznamů je povinný.", + "storage-pack-size": "Maximální velikost souboru událostí", + "storage-pack-size-min": "Minimální počet je 1.", + "storage-pack-size-pattern": "Počet není platný.", + "storage-pack-size-required": "Maximální velikost souboru událostí je povinná.", + "storage-path": "Cesta k úložišti", + "storage-path-required": "Cesta k úložišti je povinná.", + "storage-type": "Typ úložiště", + "storage-types": { + "file-storage": "Soubor", + "memory-storage": "Paměť" + }, + "thingsboard": "ThingsBoard", + "thingsboard-host": "Host ThingsBoard", + "thingsboard-host-required": "Host je povinný.", + "thingsboard-port": "Port ThingsBoard", + "thingsboard-port-max": "Maximální číslo portu je 65535.", + "thingsboard-port-min": "Minimální číslo portu je 1.", + "thingsboard-port-pattern": "Port není platný.", + "thingsboard-port-required": "Port je povinný.", + "tidy": "Uspořádat", + "tidy-tip": "Uspořádat JSON konfiguraci", + "title-connectors-json": "Konfigurace {{typeName}} konektoru", + "tls-path-ca-certificate": "Cesta k certifikátu CA brány", + "tls-path-client-certificate": "Cesta k certifikátu klienta brány", + "tls-path-private-key": "Cesta k privátnímu klíči brány", + "toggle-fullscreen": "Přepnout do režimu celé obrazovky", + "transformer-json-config": "JSON* konfigurace", + "update-config": "Přidat/editovat JSON konfiguraci" + }, + "grid": { + "delete-item-title": "Jste si jisti, že chcete smazat tuto položku?", + "delete-item-text": "Buďte opatrní, protože po potvrzení nebude možné tuto položku ani žádná související data obnovit.", + "delete-items-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 položku} other {# položek} }?", + "delete-items-action-title": "Smazat { count, plural, 1 {1 položku} other {# položek} }", + "delete-items-text": "Buďte opatrní, protože po potvrzení budou všechny vybrané položky odstraněny a žádná související data nebude možné obnovit.", + "add-item-text": "Přidat novou položku", + "no-items-text": "Žádné položky nebyly nalezeny", + "item-details": "Detail položky", + "delete-item": "Smazat položku", + "delete-items": "Smazat položky", + "scroll-to-top": "Nahoru" + }, + "help": { + "goto-help-page": "Jít na stránku nápovědy" + }, + "home": { + "home": "Domů", + "profile": "Profil", + "logout": "Odhlásit", + "menu": "Menu", + "avatar": "Avatar", + "open-user-menu": "Otevřít uživatelské menu" + }, + "import": { + "no-file": "Nebyl vybrán žádný soubor", + "drop-file": "Přetáhněte sem JSON soubor nebo klikněte pro výběr souboru pro nahrání.", + "drop-file-csv": "Přetáhněte sem CSV soubor nebo klikněte pro výběr souboru pro nahrání.", + "column-value": "Hodnota", + "column-title": "Název", + "column-example": "Příklad hodnoty", + "column-key": "Klíč atributu/telemetrie", + "csv-delimiter": "Oddělovač CSV", + "csv-first-line-header": "První řádek obsahuje názvy sloupců", + "csv-update-data": "Editovat atributy/telemetrii", + "import-csv-number-columns-error": "Soubor by měl obsahovat minimálně dva sloupce", + "import-csv-invalid-format-error": "Neplatný formát souboru. Řádek: '{{line}}'", + "column-type": { + "name": "Název", + "type": "Typ", + "label": "Označení", + "column-type": "Typ sloupce", + "client-attribute": "Atribut klienta", + "shared-attribute": "Sdílený atribut", + "server-attribute": "Atribut serveru", + "timeseries": "Časové řady", + "entity-field": "Pole entity", + "access-token": "Přístupový token", + "isgateway": "Je bránou", + "description": "Popis" + }, + "stepper-text":{ + "select-file": "Vybrat soubor", + "configuration": "Importovat konfiguraci", + "column-type": "Vybrat typ sloupců", + "creat-entities": "Vytvořím nové entity", + "done": "Hotovo" + }, + "message": { + "create-entities": "{{count}} nových entit bylo úspěšně vytvořeno.", + "update-entities": "{{count}} entit bylo úspěšně aktualizováno.", + "error-entities": "Při vytvoření {{count}} entit došlo k chybě." + } + }, + "item": { + "selected": "Vybráno" + }, + "js-func": { + "no-return-error": "Funkce musí vrátit hodnotu!", + "return-type-mismatch": "Funkce musí vrátit hodnotu '{{type}}' typu!", + "tidy": "Tidy", + "mini": "Mini" + }, + "key-val": { + "key": "Klíč", + "value": "Hodnota", + "remove-entry": "Odstranit záznam", + "add-entry": "Přidat záznam", + "no-data": "Žádné záznamy" + }, + "layout": { + "layout": "Rozmístění", + "manage": "Spravovat rozmístění", + "settings": "Nastavení rozmístění", + "color": "Barva", + "main": "Hlavní", + "right": "Vpravo", + "select": "Vybrat cílové rozmístění" + }, + "legend": { + "direction": "Směr legendy", + "position": "Pozice legendy", + "show-max": "Zobrazit max hodnotu", + "show-min": "Zobrazit min hodnotu", + "show-avg": "Zobrazit průměrnou hodnotu", + "show-total": "Zobrazit celkovou hodnotu", + "settings": "Nastavení legendy", + "min": "min", + "max": "max", + "avg": "průměr", + "total": "celkem", + "comparison-time-ago": { + "days": "(před dny)", + "weeks": "(před týdny)", + "months": "(před měsíci)", + "years": "(před roky)" + } + }, + "login": { + "login": "Přihlásit se", + "request-password-reset": "Vyžádat reset hesla", + "reset-password": "Reset hesla", + "create-password": "Vytvořit heslo", + "passwords-mismatch-error": "Zadaná hesla se musí shodovat!", + "password-again": "Heslo znovu", + "sign-in": "Prosím zaregistrujte se", + "username": "Uživatelské jméno (email)", + "remember-me": "Zapamatovat si mě", + "forgot-password": "Zapomněli jste heslo?", + "password-reset": "Reset hesla", + "expired-password-reset-message": "Platnost vašich přístupových údajů vypršela! Nastavte si prosím nové heslo.", + "new-password": "Nové heslo", + "new-password-again": "Nové heslo znovu", + "password-link-sent-message": "Odkaz pro reset hesla byl úspěšně odeslán!", + "email": "Email", + "login-with": "Přihlásit se přes {{name}}", + "or": "nebo", + "error": "Chyba přihlášení" + }, + "position": { + "top": "Nahoře", + "bottom": "Dole", + "left": "Vlevo", + "right": "Vpravo" + }, + "profile": { + "profile": "Profil", + "last-login-time": "Poslední přihlášení", + "change-password": "Změnit heslo", + "current-password": "Stávající heslo" + }, + "relation": { + "relations": "Vztahy", + "direction": "Směr", + "search-direction": { + "FROM": "Od", + "TO": "K" + }, + "direction-type": { + "FROM": "od", + "TO": "k" + }, + "from-relations": "Odchozí vztahy", + "to-relations": "Příchozí vztahy", + "selected-relations": "Vybráno { count, plural, 1 {1 vztahů} other {# vztahů} }", + "type": "Typ", + "to-entity-type": "K typ entity", + "to-entity-name": "K název entity", + "from-entity-type": "Z typ entity", + "from-entity-name": "Z název entity", + "to-entity": "K entitě", + "from-entity": "Od entity", + "delete": "Smazat vztah", + "relation-type": "Typ vztahu", + "relation-type-required": "Typ vztahu je povinný.", + "any-relation-type": "Všechny typy", + "add": "Přidat vztah", + "edit": "Editovat vztah", + "delete-to-relation-title": "Jste si jisti, že chcete smazat vztah k entitě '{{entityName}}'?", + "delete-to-relation-text": "Buďte opatrní, protože po potvrzení bude vtah entity '{{entityName}}' k aktuální entitě zrušen.", + "delete-to-relations-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 vztah} other {# vztahů} }?", + "delete-to-relations-text": "Buďte opatrní, protože po potvrzení budou všechny vybrané vztahy odstraněny a vztah odpovídajících entit k aktuální entitě bude zrušen.", + "delete-from-relation-title": "Jste si jisti, že chcete smazat vztah z entity '{{entityName}}'?", + "delete-from-relation-text": "Buďte opatrní, protože po potvrzení bude zrušen vztah aktuální entity k entitě '{{entityName}}'.", + "delete-from-relations-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 vztah} other {# vztahů} }?", + "delete-from-relations-text": "Buďte opatrní, protože po potvrzení budou všechny vybrané vztahy odstraněny a bude zrušen vztah aktuální entity k odpovídajícím entitám.", + "remove-relation-filter": "Odebrat filtr vztahů", + "add-relation-filter": "Přidat filtr vztahu", + "any-relation": "Všechny vztahy", + "relation-filters": "Filtry vztahů", + "additional-info": "Další info (JSON)", + "invalid-additional-info": "Další informace v JSON nebylo možné parsovat.", + "no-relations-text": "Žádné vztahy nebyly nalezeny" + }, + "rulechain": { + "rulechain": "Řetěz pravidel", + "rulechains": "Řetězy pravidel", + "root": "Základní", + "delete": "Smazat řetěz pravidel", + "name": "Název", + "name-required": "Název je povinný.", + "description": "Popis", + "add": "Přidat řetěz pravidel", + "set-root": "Učinit řetěz pravidel základním", + "set-root-rulechain-title": "Jste si jisti, že chcete učinit řetěz pravidel '{{ruleChainName}}' základním?", + "set-root-rulechain-text": "Po potvrzení se stane řetěz pravidel základním a bude zajišťovat zpracování všech příchozích transportních zpráv.", + "delete-rulechain-title": "Jste si jisti, že chcete smazat řetěz pravidel '{{ruleChainName}}'?", + "delete-rulechain-text": "Buďte opatrní, protože po potvrzení nebude možné řetěz pravidel ani žádná související data obnovit.", + "delete-rulechains-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 řetěz pravidel} other {# řetězů pravidel} }?", + "delete-rulechains-action-title": "Smazat { count, plural, 1 {1 řetěz pravidel} other {# řetězy pravidel} }", + "delete-rulechains-text": "Buďte opatrní, protože po potvrzení budou všechny vybrané řetězy pravidel odstraněny a žádná související data nebude možné obnovit.", + "add-rulechain-text": "Přidat nový řetěz pravidel", + "no-rulechains-text": "Žádné řetězy pravidel nebyly nalezeny", + "rulechain-details": "Detail řetězu pravidel", + "details": "Detail", + "events": "Události", + "system": "Systém", + "import": "Importovat řetěz pravidel", + "export": "Exportovat řetěz pravidel", + "export-failed-error": "Řetěz pravidel nebylo možné smazat: {{error}}", + "create-new-rulechain": "Vytvořit nový řetěz pravidel", + "rulechain-file": "Soubor řetězu pravidel", + "invalid-rulechain-file-error": "Řetěz pravidel nebylo možné importovat: neplatná datová struktura řetězu pravidel.", + "copyId": "Kopírovat Id řetězu pravidel", + "idCopiedMessage": "Id řetězu pravidel bylo zkopírováno do schránky", + "select-rulechain": "Vybrat řetěz pravidel", + "no-rulechains-matching": "Žádné řetězy pravidel odpovídající '{{entity}}' nebyly nalezeny.", + "rulechain-required": "Řetěz pravidel je povinný", + "management": "Správa pravidel", + "debug-mode": "Režim ladění", + "search": "Vyhledat řetězy pravidel", + "selected-rulechains": "Vybráno { count, plural, 1 {1 řetězů pravidel} other {# řetězů pravidel} }", + "open-rulechain": "Otevřít řetěz pravidel" + }, + "rulenode": { + "details": "Detail", + "events": "Události", + "search": "Vyhledat uzly", + "open-node-library": "Otevřít knihovnu uzlů", + "add": "Přidat uzel pravidla", + "name": "Název", + "name-required": "Název je povinný.", + "type": "Typ", + "description": "Popis", + "delete": "Smazat uzel pravidla", + "select-all-objects": "Vybrat všechny uzly a spojení", + "deselect-all-objects": "Zrušit výběr všech uzlů a spojení", + "delete-selected-objects": "Smazat vybrané uzly a spojení", + "delete-selected": "Smazat vybrané", + "select-all": "Vybrat vše", + "copy-selected": "Kopírovat vybrané", + "deselect-all": "Zrušit výběr všech", + "rulenode-details": "Detail uzlu pravidla", + "debug-mode": "Režim ladění", + "configuration": "Konfigurace", + "link": "Odkaz", + "link-details": "Detail odkazu uzlu pravidla", + "add-link": "Přidat odkaz", + "link-label": "Název odkazu", + "link-label-required": "Název odkazu je povinný.", + "custom-link-label": "Vlastní název odkazu", + "custom-link-label-required": "Název vlastního odkazu je povinný.", + "link-labels": "Názvy odkazu", + "link-labels-required": "Názvy odkazu jsou povinné.", + "no-link-labels-found": "Žádné názvy odkazů nebyly nalezeny", + "no-link-label-matching": "'{{label}}' nenalezen.", + "create-new-link-label": "Vytvořit nový!", + "type-filter": "Filtr", + "type-filter-details": "Filtruje příchozí zprávy na základě definovaných podmínek", + "type-enrichment": "Obohacení", + "type-enrichment-details": "Přidá doplňující informace do metadat zprávy", + "type-transformation": "Transformace", + "type-transformation-details": "Změní zprávu a metadata", + "type-action": "Akce", + "type-action-details": "Provede speciální akci", + "type-external": "Externí", + "type-external-details": "Interaguje s externím systémem", + "type-rule-chain": "Řetěz pravidel", + "type-rule-chain-details": "Předá příchozí zprávy specifikovanému řetězu pravidel", + "type-input": "Vstup", + "type-input-details": "Logický vstup řetězu pravidel, předává příchozí zprávy dalšímu navazujícímu uzlu pravidla", + "type-unknown": "Neznámý", + "type-unknown-details": "Nevyřešený uzel pravidla", + "directive-is-not-loaded": "Definovaná konfigurační direktiva '{{directiveName}}' není dostupná.", + "ui-resources-load-error": "Nepodařilo se nahrát konfigurační ui zdroje.", + "invalid-target-rulechain": "Není možné interagovat s cílovým řetězem pravidel!", + "test-script-function": "Testovat funkci skriptu", + "message": "Zpráva", + "message-type": "Typ zprávy", + "select-message-type": "Vybrat typ zprávy", + "message-type-required": "Typ zprávy je povinný", + "metadata": "Metadata", + "metadata-required": "Záznam metadat nemůže být prázdný.", + "output": "Výstup", + "test": "Test", + "help": "Nápověda", + "reset-debug-mode": "Resetovat režim ladění na všech uzlech" + }, + "queue": { + "select_name": "Vybrat název fronty", + "name": "Název fronty", + "name_required": "Název fronty je povinný!" + }, + "tenant": { + "tenant": "Tenant", + "tenants": "Tenanti", + "management": "Správa tenantů", + "add": "Přidat tenanta", + "admins": "Administrátoři", + "manage-tenant-admins": "Spravovat administrátory tenanta", + "delete": "Smazat tenanta", + "add-tenant-text": "Přidat nového tenanta", + "no-tenants-text": "Žádní tenanti nebyli nalezeni", + "tenant-details": "Detail tenanta", + "delete-tenant-title": "Jste si jisti, že chcete smazat tenanta '{{tenantTitle}}'?", + "delete-tenant-text": "Buďte opatrní, protože po potvrzení nebude možné tenanta ani žádná související data obnovit.", + "delete-tenants-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 tenanta} other {# tenantů} }?", + "delete-tenants-action-title": "Smazat { count, plural, 1 {1 tenanta} other {# tenantů} }", + "delete-tenants-text": "Buďte opatrní, protože po potvrzení budou všichni vybraní tenanti odstraněni a žádná související data nebude možné obnovit.", + "title": "Název", + "title-required": "Název je povinný.", + "description": "Popis", + "details": "Detail", + "events": "Události", + "copyId": "Kopírovat Id tenanta", + "idCopiedMessage": "Id tenanta bylo zkopírováno do schránky", + "select-tenant": "Vybrat tenanta", + "no-tenants-matching": "Žádní tenanti odpovídající '{{entity}}' nebyli nalezeni.", + "tenant-required": "Tenant je povinný", + "search": "Vyhledat tenanty", + "selected-tenants": "Vybráno { count, plural, 1 {1 tenantů} other {# tenantů} }", + "isolated-tb-core": "Zpracování v izolovaném kontejneru ThingsBoard Core", + "isolated-tb-rule-engine": "Zpracování v izolovaném kontejneru ThingsBoard Rule Engine", + "isolated-tb-core-details": "Vyžaduje samostatnou mikroslužbu(y) pro každého izolovaného tenanta", + "isolated-tb-rule-engine-details": "Vyžaduje samostatnou mikroslužbu(y) pro každého izolovaného tenanta" + }, + "timeinterval": { + "seconds-interval": "{ seconds, plural, 1 {1 vteřina} other {# vteřin} }", + "minutes-interval": "{ minutes, plural, 1 {1 minuta} other {# minut} }", + "hours-interval": "{ hours, plural, 1 {1 hodina} other {# hodin} }", + "days-interval": "{ days, plural, 1 {1 den} other {# dnů} }", + "days": "Dny", + "hours": "Hodiny", + "minutes": "Minuty", + "seconds": "Vteřiny", + "advanced": "Rozšířené" + }, + "timewindow": { + "days": "{ days, plural, 1 { den } other {# days } }", + "hours": "{ hours, plural, 0 { hodina } 1 {1 hodina } other {# hodin } }", + "minutes": "{ minutes, plural, 0 { minuta } 1 {1 minuta } other {# minut } }", + "seconds": "{ seconds, plural, 0 { vteřina } 1 {1 vteřina } other {# vteřin } }", + "realtime": "V reálném čase", + "history": "Historie", + "last-prefix": "poslední", + "period": "od {{ startTime }} do {{ endTime }}", + "edit": "Editovat časové okno", + "date-range": "Rozsah data", + "last": "Poslední", + "time-period": "Časový interval", + "hide": "Skrýt" + }, + "user": { + "user": "Uživatel", + "users": "Uživatelé", + "customer-users": "Uživatelé zákazníka", + "tenant-admins": "Administrátoři tenanta", + "sys-admin": "Systémový administrátor", + "tenant-admin": "Administrátor tenanta", + "customer": "Zákazník", + "anonymous": "Anonymní", + "add": "Přidat uživatele", + "delete": "Smazat uživatele", + "add-user-text": "Přidat nového uživatele", + "no-users-text": "Žádní uživatelé nebyli nalezeni", + "user-details": "Detail uživatele", + "delete-user-title": "Jste si jisti, že chcete smazat uživatele '{{userEmail}}'?", + "delete-user-text": "Buďte opatrní, protože po potvrzení nebude možné uživatele ani žádná související data obnovit.", + "delete-users-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 uživatele} other {# uživatele} }?", + "delete-users-action-title": "Smazat { count, plural, 1 {1 uživatele} other {# uživatele} }", + "delete-users-text": "Buďte opatrní, protože po potvrzení budou všichni vybraní uživatelé odstraněni a žádná související data nebude možné obnovit.", + "activation-email-sent-message": "Aktivační email byl úspěšně odeslán!", + "resend-activation": "Znovu poslat aktivační email", + "email": "Email", + "email-required": "Email je povinný.", + "invalid-email-format": "Neplatný formát emailu.", + "first-name": "Jméno", + "last-name": "Příjmení", + "description": "Popis", + "default-dashboard": "Defaultní dashboard", + "always-fullscreen": "Zobrazení vždy na celé obrazovce", + "select-user": "Vybrat uživatele", + "no-users-matching": "Žádní uživatelé odpovídající '{{entity}}' nebyli nalezeni.", + "user-required": "Uživatel je povinný", + "activation-method": "Metoda aktivace", + "display-activation-link": "Zobrazit aktivační odkaz", + "send-activation-mail": "Odeslat aktivační email", + "activation-link": "Aktivační odkaz uživatele", + "activation-link-text": "Pro aktivaci uživatele použijte následující aktivační odkaz :", + "copy-activation-link": "Kopírovat aktivační odkaz", + "activation-link-copied-message": "Aktivační odkaz uživatele byl zkopírován do schránky", + "details": "Detail", + "login-as-tenant-admin": "Přihlásit se jako administrátor tenanta", + "login-as-customer-user": "Přihlásit se jako uživatel zákazníka", + "search": "Vyhledat uživatele", + "selected-users": "Vybráno { count, plural, 1 {1 uživatelů} other {# uživatelů} }", + "disable-account": "Zakázat uživatelský účet", + "enable-account": "Povolit uživatelský účet", + "enable-account-message": "Uživatelský účet byl úspěšně povolen!", + "disable-account-message": "Uživatelský účet byl úspěšně zakázán!" + }, + "value": { + "type": "Typ hodnoty", + "string": "Řetězec", + "string-value": "Hodnota řetězce", + "string-value-required": "Hodnota řetězce je povinná", + "integer": "Celé číslo", + "integer-value": "Hodnota celého čísla", + "integer-value-required": "Hodnota celého čísla je povinná", + "invalid-integer-value": "Neplatná hodnota celého čísla", + "double": "Číslo s plovoucí řádovou s dvojitou přesností", + "double-value": "Hodnota čísla s plovoucí řádovou řádkou", + "double-value-required": "Hodnota čísla s plovoucí řádovou čárkou je povinná", + "boolean": "Pravdivostní hodnota", + "boolean-value": "Hodnota pravdivostní hodnoty", + "false": "Nepravda", + "true": "Pravda", + "long": "Vysoké celé číslo", + "json": "JSON", + "json-value": "Hodnota JSON", + "json-value-invalid": "Hodnota JSON má neplatný formát", + "json-value-required": "Hodnota JSON je povinná." + }, + "widget": { + "widget-library": "Knihovna widgetů", + "widget-bundle": "Kategorie widgetů", + "select-widgets-bundle": "Vybrat kategorii widgetů", + "management": "Správa widgetů", + "editor": "Editor widgetů", + "widget-type-not-found": "Problém s nahráním konfigurace widgetu.
    Pravděpodobně byl asociovaný\n typ widgetu odstraněn.", + "widget-type-load-error": "Widget nebyl nahrán z důvodu následujících chyb:", + "remove": "Odebrat widget", + "edit": "Editovat widget", + "remove-widget-title": "Jste si jisti, že chcete odebrat widget '{{widgetTitle}}'?", + "remove-widget-text": "Po potvrzení nebude možné widget ani žádná související data obnovit.", + "timeseries": "Časové řady", + "search-data": "Vyhledat data", + "no-data-found": "Žádná data nebyla nalezena", + "latest-values": "Poslední hodnoty", + "rpc": "Ovládací widget", + "alarm": "Widgety alarmu", + "static": "Statické widgety", + "select-widget-type": "Vybrat typ widgetu", + "missing-widget-title-error": "Název widgetu musí být specifikován!", + "widget-saved": "Widget uložen", + "unable-to-save-widget-error": "Widget nebylo možné uložit! Widget obsahuje chyby!", + "save": "Uložit widget", + "saveAs": "Uložit widget jako", + "save-widget-type-as": "Uložit typ widgetu jako", + "save-widget-type-as-text": "Zadejte prosím název nového widgetu a/nebo vyberte cílovou kategorii widgetů", + "toggle-fullscreen": "Přepnout na celou obrazovku", + "run": "Spustit widget", + "title": "Název widgetu", + "title-required": "Název widgetu je povinný.", + "type": "Typ widgetu", + "resources": "Zdroje", + "resource-url": "JavaScript/CSS URL", + "remove-resource": "Odebrat zdroj", + "add-resource": "Přidat zdroj", + "html": "HTML", + "tidy": "Tidy", + "css": "CSS", + "settings-schema": "Schéma nastavení", + "datakey-settings-schema": "Schéma nastavení datového klíče", + "javascript": "Javascript", + "js": "JS", + "remove-widget-type-title": "Jste si jisti, že chcete odebrat typ widgetu '{{widgetName}}'?", + "remove-widget-type-text": "Po potvrzení nebude možné typ widgetu ani žádná související data obnovit.", + "remove-widget-type": "Odebrat typ widgetu", + "add-widget-type": "Přidat nový typ widgetu", + "widget-type-load-failed-error": "Nahrání typu widgetu selhalo!", + "widget-template-load-failed-error": "Nahrání šablony widgetu selhalo!", + "add": "Přidat widget", + "undo": "Vrátit změny widgetu", + "export": "Exportovat widget" + }, + "widget-action": { + "header-button": "Tlačítko hlavičky widgetu", + "open-dashboard-state": "Přejít k novému stavu dashboardu", + "update-dashboard-state": "Aktualizovat stávající stav dashboardu", + "open-dashboard": "Přejít k jinému dashboardu", + "custom": "Vlastní akce", + "custom-pretty": "Vlastní akce (s HTML šablonou)", + "target-dashboard-state": "Cílový stav dashboardu", + "target-dashboard-state-required": "Cílový stav dashboardu je povinný", + "set-entity-from-widget": "Nastavit entitu z widgetu", + "target-dashboard": "Cílový dashboard", + "open-right-layout": "Otevřít rozmístění dashboardu vpravo (mobilní zobrazení)" + }, + "widgets-bundle": { + "current": "Vybraná kategorie", + "widgets-bundles": "Kategorie widgetů", + "add": "Přidat kategorii widgetů", + "delete": "Smazat kategorii widgetů", + "title": "Název", + "title-required": "Název je povinný.", + "add-widgets-bundle-text": "Přidat novou kategorii widgetů", + "no-widgets-bundles-text": "Žádné kategorie widgetů nebyly nalezeny", + "empty": "Kategorie widgetů je prázdná", + "details": "Detail", + "widgets-bundle-details": "Detail kategorie widgetů", + "delete-widgets-bundle-title": "Jste si jisti, že chcete smazat kategorii widgetů '{{widgetsBundleTitle}}'?", + "delete-widgets-bundle-text": "Buďte opatrní, po potvrzení nebude možné kategorii widgetů ani žádná související data obnovit.", + "delete-widgets-bundles-title": "Jste si jisti, že chcete odstranit { count, plural, 1 {1 kategorii widgetů} other {# kategorií widgetů} }?", + "delete-widgets-bundles-action-title": "Smazat { count, plural, 1 {1 kategorii widgetů} other {# kategorií widgetů} }", + "delete-widgets-bundles-text": "Buďte opatrní, po potvrzení budou všechny vybrané kategorie widgetů odstraněny a žádná související data nebude možné obnovit.", + "no-widgets-bundles-matching": "Žádné kategorie widgetů odpovídající '{{widgetsBundle}}' nebyly nalezeny.", + "widgets-bundle-required": "Kategorie widgetů je povinná.", + "system": "Systém", + "import": "Importovat kategorii widgetů", + "export": "Exportovat kategorii widgetů", + "export-failed-error": "Kategorii widgetů nebylo možné exportovat: {{error}}", + "create-new-widgets-bundle": "Vytvořit novou kategorii widgetů", + "widgets-bundle-file": "Soubor kategorie widgetů", + "invalid-widgets-bundle-file-error": "Kategorii widgetů nebylo možné importovat: Neplatná datová struktura kategorie widgetů.", + "search": "Vyhledat kategorie widgetů", + "selected-widgets-bundles": "Vybráno { count, plural, 1 {1 kategorií widgetů} other {# kategorií widgetů} }", + "open-widgets-bundle": "Otevřít kategorii widgetů" + }, + "widget-config": { + "data": "Data", + "settings": "Nastavení", + "advanced": "Rozšířené", + "title": "Název", + "title-tooltip": "Popisek názvu", + "general-settings": "Obecná nastavení", + "display-title": "Zobrazovaný název", + "drop-shadow": "Stín", + "enable-fullscreen": "Povolit zobrazení na celé obrazovce", + "background-color": "Barva pozadí", + "text-color": "Barva textu", + "padding": "Šířka vnitřního okraje", + "margin": "Okraj", + "widget-style": "Styl widgetu", + "title-style": "Název stylu", + "mobile-mode-settings": "Nastavení mobilního režimu", + "order": "Pořadí", + "height": "Výška", + "units": "Speciální symbol zobrazovaný za hodnotou", + "decimals": "Počet číslic za desetinnou čárkou", + "timewindow": "Časové okno", + "use-dashboard-timewindow": "Použít časové okno dashboardu", + "display-timewindow": "Zobrazit časové okno", + "display-legend": "Zobrazit legendu", + "datasources": "Datové zdroje", + "maximum-datasources": "Maximum { count, plural, 1 {1 datový zdroj je povolen.} other {# datových zdrojů je povoleno} }", + "datasource-type": "Typ", + "datasource-parameters": "Parametry", + "remove-datasource": "Odebrat datový zdroj", + "add-datasource": "Přidat datový zdroj", + "target-device": "Cílové zařízení", + "alarm-source": "Zdroj alarmu", + "actions": "Akce", + "action": "Akce", + "add-action": "Přidat akci", + "search-actions": "Vyhledat akce", + "no-actions-text": "Žádné akce nebyly nalezeny", + "action-source": "Zdroj akce", + "action-source-required": "Zdroj akce je povinný.", + "action-name": "Název", + "action-name-required": "Název akce je povinný.", + "action-name-not-unique": "Jiná akce s identickým názvem již existuje.
    Název akce by měl být v rámci zdroje akce unikátní.", + "action-icon": "Ikona", + "action-type": "Typ", + "action-type-required": "Typ akce je povinný.", + "edit-action": "Editovat akci", + "delete-action": "Smazat akci", + "delete-action-title": "Smazat akci widgetu", + "delete-action-text": "Jste si jisti, že chcete smazat akci widgetu s názvem '{{actionName}}'?", + "display-icon": "Zobrazit název ikony", + "icon-color": "Barva ikony", + "icon-size": "Velikost ikony" + }, + "widget-type": { + "import": "Importovat typ widgetu", + "export": "Exportovat typ widgetu", + "export-failed-error": "Typ widgetu nebylo možné exportovat: {{error}}", + "create-new-widget-type": "Vytvořit nový typ widgetu", + "widget-type-file": "Soubor typu widgetu", + "invalid-widget-type-file-error": "Typ widgetu nebylo možné importovat: Neplatná datová struktura typu widgetu." + }, + "widgets": { + "date-range-navigator": { + "localizationMap": { + "Sun": "Ne", + "Mon": "Po", + "Tue": "Út", + "Wed": "St", + "Thu": "Čt", + "Fri": "Pá", + "Sat": "So", + "Jan": "Led", + "Feb": "Úno", + "Mar": "Bře", + "Apr": "Dub", + "May": "Květen", + "Jun": "Čvn", + "Jul": "Čvc", + "Aug": "Srp", + "Sep": "Zář", + "Oct": "Říj", + "Nov": "Lis", + "Dec": "Pro", + "January": "Leden", + "February": "Únor", + "March": "Březen", + "April": "Duben", + "June": "Červen", + "July": "Červenec", + "August": "Srpen", + "September": "Září", + "October": "Říjen", + "November": "Listopad", + "December": "Prosinec", + "Custom Date Range": "Vlastní rozsah data", + "Date Range Template": "Šablona rozsahu data", + "Today": "Dnes", + "Yesterday": "Včera", + "This Week": "Tento týden", + "Last Week": "Minulý týden", + "This Month": "Tento měsíc", + "Last Month": "Minulý měsíc", + "Year": "Rok", + "This Year": "Tento rok", + "Last Year": "Minulý rok", + "Date picker": "Výběr data", + "Hour": "Hodina", + "Day": "Den", + "Week": "Týden", + "2 weeks": "2 týdny", + "Month": "Měsíc", + "3 months": "3 měsíce", + "6 months": "6 měsíců", + "Custom interval": "Vlastní interval", + "Interval": "Interval", + "Step size": "Velikost kroku", + "Ok": "Ok" + } + }, + "input-widgets": { + "attribute-not-allowed": "Parametr atributu nelze v tomto widgetu použít", + "blocked-location": "Geolokace je ve vašem prohlížeči zakázána", + "claim-device": "Získat zařízení", + "claim-failed": "Nepodařilo se získat zařízení!", + "claim-not-found": "Zařízení nenalezeno!", + "claim-successful": "Zařízení bylo úspěšně získáno!", + "date": "Datum", + "device-name": "Název zařízení", + "device-name-required": "Název zařízení je povinný", + "discard-changes": "Zahodit změny", + "entity-attribute-required": "Atribut entity je povinný", + "entity-coordinate-required": "Obě pole, zeměpisná šířka i zeměpisná délka, jsou povinná", + "entity-timeseries-required": "Časové řady entity jsou povinné", + "get-location": "Získat aktuální polohu", + "latitude": "Zeměpisná šířka", + "longitude": "Zeměpisná délka", + "not-allowed-entity": "Vybraná entita nemůže mít sdílené atributy", + "no-attribute-selected": "Není vybrán žádný atribut", + "no-datakey-selected": "Není vybrán žádný datový klíč", + "no-coordinate-specified": "Datový klíč pro zeměpisnou šířku/délku nebyl specifikován", + "no-entity-selected": "Není vybrána žádná entita", + "no-image": "Žádný obrázek", + "no-support-geolocation": "Váš prohlížeč nepodporuje geolokaci", + "no-support-web-camera": "Žádná podporovaná webová kamera", + "no-timeseries-selected": "Nejsou vybrány žádné časové řady", + "secret-key": "Tajný klíč", + "secret-key-required": "Tajný klíč je povinný", + "switch-attribute-value": "Přepnout hodnotu atributu entity", + "switch-camera": "Přepnout kameru", + "switch-timeseries-value": "Přepnout hodnotu časové řady entity", + "take-photo": "Vyfotit", + "time": "Čas", + "timeseries-not-allowed": "Časové řady nelze v tomto widgetu použít", + "update-failed": "Aktualizace selhala", + "update-successful": "Aktualizace úspěšná", + "update-attribute": "Editovat atribut", + "update-timeseries": "Editovat časové řady", + "value": "Hodnota" + } + }, + "icon": { + "icon": "Ikona", + "select-icon": "Vybrat ikonu", + "material-icons": "Ikony Material", + "show-all": "Zobrazit všechny ikony" + }, + "custom": { + "widget-action": { + "action-cell-button": "Tlačítko buňky akce", + "row-click": "Při kliknutí na řádek", + "polygon-click": "Při kliknutí na polygon", + "marker-click": "Při kliknutí na značku", + "tooltip-tag-action": "Akce štítku nápovědy", + "node-selected": "Při výběru uzlu", + "element-click": "Při kliknutí na prvek HTML", + "pie-slice-click": "Při kliknutí na oblast grafu", + "row-double-click": "Při dvoj kliknutí na řádek" + } + }, + "language": { + "language": "Jazyk", + "locales": { + "de_DE": "Deutsch", + "fr_FR": "Français", + "zh_CN": "简体中文", + "zh_TW": "繁體中文", + "en_US": "English", + "it_IT": "Italiano", + "ko_KR": "한글", + "ru_RU": "Русский", + "es_ES": "Español", + "ja_JA": "日本語", + "tr_TR": "Türkçe", + "fa_IR": "فارسي", + "uk_UA": "Українська", + "cs_CZ": "Česky", + "el_GR": "Ελληνικά", + "ro_RO": "Română", + "lv_LV": "Latviešu" + } + } +} diff --git a/ui-ngx/src/assets/locale/locale.constant-de_DE.json b/ui-ngx/src/assets/locale/locale.constant-de_DE.json new file mode 100644 index 0000000000..d7b1869bfd --- /dev/null +++ b/ui-ngx/src/assets/locale/locale.constant-de_DE.json @@ -0,0 +1,1694 @@ +{ + "access": { + "unauthorized": "Nicht autorisiert", + "unauthorized-access": "Unautorisierter Zugriff", + "unauthorized-access-text": "Sie sollten sich anmelden, um Zugriff auf diese Daten zu erhalten!", + "access-forbidden": "Keine Zugangsberechtigung", + "access-forbidden-text": "Sie haben keine Zugangsberechtigung für diesen Bereich!
    Versuchen Sie sich mit einem anderen Benutzer anzumelden um Zugriff auf diesen Bereich zu bekommen.", + "refresh-token-expired": "Sitzung ist abgelaufen", + "refresh-token-failed": "Sitzung kann nicht aktualisiert werden" + }, + "action": { + "activate": "Aktivieren", + "suspend": "Unterbrechen", + "save": "Speichern", + "saveAs": "Speichern unter", + "cancel": "Abbrechen", + "ok": "OK", + "delete": "Löschen", + "add": "Hinzufügen", + "yes": "Ja", + "no": "Nein", + "update": "Aktualisieren", + "remove": "Löschen", + "search": "Suche", + "clear-search": "Suchanfrage löschen", + "assign": "Zuordnen", + "unassign": "Zuordnung aufheben", + "share": "Teilen", + "make-private": "Privat machen", + "apply": "Anwenden", + "apply-changes": "Änderungen übernehmen", + "edit-mode": "Bearbeitungsmodus", + "enter-edit-mode": "Zum Bearbeitungsmodus wechseln", + "decline-changes": "Änderungen nicht übernehmen", + "close": "Schließen", + "back": "Zurück", + "run": "Ausführen", + "sign-in": "Anmelden!", + "edit": "Bearbeiten", + "view": "Ansicht", + "create": "Erstellen", + "drag": "Ziehen", + "refresh": "Aktualisieren", + "undo": "Rückgängig machen", + "copy": "Kopieren", + "paste": "Einfügen", + "copy-reference": "Zeichen kopieren", + "paste-reference": "Zeichen einfügen", + "import": "Importieren", + "export": "Exportieren", + "share-via": "Teilen mit {{provider}}" + }, + "aggregation": { + "aggregation": "Aggregation", + "function": "Datenaggregationsfunktion", + "limit": "Höchstwerte", + "group-interval": "Gruppierungsintervall", + "min": "Minimal", + "max": "Maximal", + "avg": "Durchschnitt", + "sum": "Summe", + "count": "Anzahl", + "none": "kein Wert" + }, + "admin": { + "general": "Allgemein", + "general-settings": "Allgemeine Einstellungen", + "outgoing-mail": "E-Mail Versand", + "outgoing-mail-settings": "Konfiguration des Postausgangsservers", + "system-settings": "Systemeinstellungen", + "test-mail-sent": "Test E-Mail wurde erfolgreich versendet!", + "base-url": "Basis-URL", + "base-url-required": "Basis-URL ist erforderlich.", + "mail-from": "E-Mail von", + "mail-from-required": "E-Mail von ist erforderlich.", + "smtp-protocol": "SMTP Protokoll", + "smtp-host": "SMTP Host", + "smtp-host-required": "SMTP Host ist erforderlich.", + "smtp-port": "SMTP Port", + "smtp-port-required": "Sie müssen einen SMTP Port angeben.", + "smtp-port-invalid": "Das ist kein gültiger SMTP Port.", + "timeout-msec": "Wartezeit (msec)", + "timeout-required": "Wartezeit ist erforderlich.", + "timeout-invalid": "Das ist keine gültige Wartezeit.", + "enable-tls": "TLS aktivieren", + "tls-version" : "TLS-Version", + "send-test-mail": "Test E-Mail senden", + "security-settings": "Sicherheitseinstellungen", + "password-policy": "Kennwortrichtlinie", + "minimum-password-length": "Minimale Passwortlänge", + "minimum-password-length-required": "Minimale Passwortlänge ist erforderlich", + "minimum-password-length-range": "Die Mindestlänge des Passworts sollte zwischen 5 und 50 liegen", + "minimum-uppercase-letters": "Mindestanzahl von Großbuchstaben", + "minimum-uppercase-letters-range": "Die Mindestanzahl von Großbuchstaben kann nicht negativ sein", + "minimum-lowercase-letters": "Mindestanzahl von Kleinbuchstaben", + "minimum-lowercase-letters-range": "Die Mindestanzahl von Kleinbuchstaben kann nicht negativ sein", + "minimum-digits": "Mindestanzahl von Ziffern", + "minimum-digits-range": "Die Mindestanzahl von Ziffern kann nicht negativ sein", + "minimum-special-characters": "Mindestanzahl von Sonderzeichen", + "minimum-special-characters-range": "Die Mindestanzahl von Sonderzeichen kann nicht negativ sein", + "password-expiration-period-days": "Gültigkeitsdauer des Passworts in Tagen", + "password-expiration-period-days-range": "Die Gültigkeitsdauer des Passworts in Tagen kann nicht negativ sein", + "password-reuse-frequency-days": "Häufigkeit der Kennwortwiederverwendung in Tagen", + "password-reuse-frequency-days-range": "Die Häufigkeit der Kennwortwiederverwendung in Tagen kann nicht negativ sein", + "general-policy": "Allgemeine Politik", + "max-failed-login-attempts": "Maximale Anzahl fehlgeschlagener Anmeldeversuche, bevor das Konto gesperrt wird", + "minimum-max-failed-login-attempts-range": "Die maximale Anzahl fehlgeschlagener Anmeldeversuche kann nicht negativ sein", + "user-lockout-notification-email": "Wenn das Benutzerkonto gesperrt ist, senden Sie eine Benachrichtigung per E-Mail" + }, + "alarm": { + "alarm": "Alarm", + "alarms": "Alarme", + "select-alarm": "Alarm auswählen", + "no-alarms-matching": "Keine passenden Alarme zu '{{entity}}' wurden gefunden.", + "alarm-required": "Alarm ist erforderlich", + "alarm-status": "Alarm Status", + "search-status": { + "ANY": "Jeder", + "ACTIVE": "Aktiv", + "CLEARED": "Gelöscht", + "ACK": "Bestätigt", + "UNACK": "Nicht bestätigt" + }, + "display-status": { + "ACTIVE_UNACK": "Nicht bestätigt aktiv", + "ACTIVE_ACK": "Bestätigt aktiv", + "CLEARED_UNACK": "Nicht bestätigt", + "CLEARED_ACK": "Bestätigung gelöscht" + }, + "no-alarms-prompt": "Keine Alarme gefunden", + "created-time": "Erstellungszeit", + "type": "Typ", + "severity": "Schwere", + "originator": "Urheber", + "originator-type": "Urheber-Typ", + "details": "Details", + "status": "Status", + "alarm-details": "Alarm-Details", + "start-time": "Startzeit", + "end-time": "Endzeit", + "ack-time": "Bestätigungszeit", + "clear-time": "Zeit gelöscht", + "severity-critical": "Kritisch", + "severity-major": "Groß", + "severity-minor": "Klein", + "severity-warning": "Warnung", + "severity-indeterminate": "Unbestimmt", + "acknowledge": "Bestätigen", + "clear": "Löschen", + "search": "Alarme Suchen", + "selected-alarms": "{ count, plural, 1 {1 Alarm} other {# Alarme} } ausgewählt", + "no-data": "Keine Daten zum Anzeigen", + "polling-interval": "Alarmabfrageintervall (sec)", + "polling-interval-required": "Alarmabfrageintervall ist erforderlich.", + "min-polling-interval-message": "Mindestens 1 sec Abrufintervall ist zulässig.", + "aknowledge-alarms-title": "{ count, plural, 1 {1 Alarm} other {# Alarme} } bestätigen", + "aknowledge-alarms-text": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Alarm} other {# Alarme} } bestätigen möchten?", + "aknowledge-alarm-title": "Alarm bestätigen", + "aknowledge-alarm-text": "Möchten Sie den Alarm wirklich bestätigen?", + "clear-alarms-title": "{ count, plural, 1 {1 Alarm} other {# Alarme} } löschen", + "clear-alarms-text": "Möchten Sie wirklich { count, plural, 1 {1 Alarm} other {# Alarme} } löschen?", + "clear-alarm-title": "Alarm löschen", + "clear-alarm-text": "Möchten Sie den Alarm wirklich löschen?", + "alarm-status-filter": "Alarm Status Filter" + }, + "alias": { + "add": "Alias hinzufügen", + "edit": "Alias bearbeiten", + "name": "Aliasname", + "name-required": "Aliasname ist erforderlich", + "duplicate-alias": "Ein Alias mit demselben Namen ist bereits vorhanden.", + "filter-type-single-entity": "Einzelne Entität", + "filter-type-entity-list": "Entitätsliste", + "filter-type-entity-name": "Entitätsname", + "filter-type-state-entity": "Entität aus dem Dashboard Status", + "filter-type-state-entity-description": "Entität aus den Dashboard Status Parametern", + "filter-type-asset-type": "Objekttyp", + "filter-type-asset-type-description": "Objekte vom Typ '{{assetType}}'", + "filter-type-asset-type-and-name-description": "Objekte vom Typ '{{assetType}}' und Name beginnend mit '{{prefix}}'", + "filter-type-device-type": "Gerätetyp", + "filter-type-device-type-description": "Geräte vom Typ '{{deviceType}}'", + "filter-type-device-type-and-name-description": "Geräte vom Typ '{{deviceType}}' und Name beginnend mit '{{prefix}}'", + "filter-type-entity-view-type": "Entitätsansichtstyp", + "filter-type-entity-view-type-description": "Entitätsansichten vom Typ '{{entityView}}'", + "filter-type-entity-view-type-and-name-description": "Entitätsansichten vom Typ '{{entityView}}' und Name beginnend mit '{{prefix}}'", + "filter-type-relations-query": "Beziehungsabfrage", + "filter-type-relations-query-description": "{{entities}} mit {{relationType}} Beziehung {{direction}} {{rootEntity}}", + "filter-type-asset-search-query": "Objektabfrage", + "filter-type-asset-search-query-description": "Objekte vom Typ {{assetTypes}} mit {{relationType}} Beziehung {{direction}} {{rootEntity}}", + "filter-type-device-search-query": "Geräteabfrage", + "filter-type-device-search-query-description": "Geräte vom Typ {{deviceTypes}} mit {{relationType}} Beziehung {{direction}} {{rootEntity}}", + "filter-type-entity-view-search-query": "Entitätsansichtsabfrage", + "filter-type-entity-view-search-query-description": "Entitätsansichten vom Typ {{entityViewTypes}} mit {{relationType}} Beziehung {{direction}} {{rootEntity}}", + "entity-filter": "Entitätsfilter", + "resolve-multiple": "Als mehrere Entitäten auflösen", + "filter-type": "Filtertyp", + "filter-type-required": "Filtertyp ist erforderlich.", + "entity-filter-no-entity-matched": "Es wurden keine Entitäten gefunden, die dem angegebenen Filter entsprechen.", + "no-entity-filter-specified": "Es wurde kein Entitätsfilter angegeben", + "root-state-entity": "Dashboard Status Entität als Wurzel verwenden", + "root-entity": "Wurzelentität", + "state-entity-parameter-name": "Parameter-Name der Statusentität", + "default-state-entity": "Standard Statusentität", + "default-entity-parameter-name": "Standardmäßig", + "max-relation-level": "Maximale Beziehungstiefe", + "unlimited-level": "Unbegrenzte Ebenen", + "state-entity": "Dashboard Status Entität", + "all-entities": "Alle Entitäten", + "any-relation": "Jede Beziehung" + }, + "asset": { + "asset": "Objekt", + "assets": "Objekte", + "management": "Objektverwaltung", + "view-assets": "Objekte anzeigen", + "add": "Objekt hinzufügen", + "assign-to-customer": "Einem Kunden zuordnen", + "assign-asset-to-customer": "Objekte dem Kunden zuordnen", + "assign-asset-to-customer-text": "Bitte wählen Sie die Objekte aus, die dem Kunden zugeordnet werden sollen", + "no-assets-text": "Keine Objekte gefunden", + "assign-to-customer-text": "Bitte wählen Sie den Kunden aus, dem die Objekte zugeordnet werden sollen", + "public": "Öffentlich", + "assignedToCustomer": "Kunden zugeordnet", + "make-public": "Objekt öffentlich machen", + "make-private": "Objekt privat machen", + "unassign-from-customer": "Kundenzuordnung aufheben", + "delete": "Objekt löschen", + "asset-public": "Objekt ist öffentlich", + "asset-type": "Objekttyp", + "asset-type-required": "Objekttyp ist erforderlich.", + "select-asset-type": "Objekttyp auswählen", + "enter-asset-type": "Objekttyp bestätigen", + "any-asset": "Jedes Objekt", + "no-asset-types-matching": "Es wurden keine zu '{{entitySubtype}}' passenden Objekte gefunden.", + "asset-type-list-empty": "Keine Objekttypen ausgewählt.", + "asset-types": "Objekttypen", + "name": "Name", + "name-required": "Name ist erforderlich.", + "description": "Beschreibung", + "type": "Typ", + "type-required": "Typ ist erforderlich.", + "details": "Details", + "events": "Ereignisse", + "add-asset-text": "Neues Objekt hinzufügen", + "asset-details": "Objektdetails", + "assign-assets": "Objekte zuordnen", + "assign-assets-text": "Kunden { count, plural, 1 {1 Objekt} other {# Objekte} } zuordnen", + "delete-assets": "Objekte löschen", + "unassign-assets": "Objektzuordnungen aufheben", + "unassign-assets-action-title": "Kunden { count, plural, 1 {1 Objektzuordnung} other {# Objektzuordnungen} } aufheben", + "assign-new-asset": "Neues Objekt zuordnen", + "delete-asset-title": "Sind Sie sicher, dass Sie das Objekt '{{assetName}}' löschen möchten?", + "delete-asset-text": "Vorsicht, nach Bestätigung wird das Objekt und alle zugehörigen Daten nicht wiederherstellbar gelöscht.", + "delete-assets-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Objekt} other {# Objekte} } löschen möchten?", + "delete-assets-action-title": "{ count, plural, 1 {1 Objekt} other {# Objekte} } löschen", + "delete-assets-text": "Vorsicht, nach Bestätigung werden die ausgewählten Objekte und alle zugehörigen Daten nicht wiederherstellbar gelöscht", + "make-public-asset-title": "Sind Sie sicher, dass Sie das Objekt '{{assetName}}' öffentlich machen möchten?", + "make-public-asset-text": "Nach Bestätigung wird das Objekt und alle zugehörigen Daten anderen zugänglich gemacht.", + "make-private-asset-title": "Sind Sie sicher, dass Sie das Objekt '{{assetName}}' privat machen möchten?", + "make-private-asset-text": "Nach Bestätigung wird das Objekt und alle zugehörigen Daten privat und ist für andere nicht mehr zugänglich.", + "unassign-asset-title": "Sind Sie sicher, dass Sie die Zuordnung für das Objekt '{{assetName}}' aufheben möchten?", + "unassign-asset-text": "Nach Bestätigung wird die Zuordnung des Objekts aufgehoben und es ist für den Kunden nicht mehr zugänglich.", + "unassign-asset": "Zuordnung des Objekts aufheben", + "unassign-assets-title": "Möchten Sie die Zuordnung von { count, plural, 1 {1 Objekt} other {# Objekte} } aufheben?", + "unassign-assets-text": "Nach Bestätigung wird die Zuordnung der ausgewählten Objekte aufgehoben und sie sind für den Kunden nicht mehr zugänglich.", + "copyId": "Objekt-ID kopieren", + "idCopiedMessage": "Objekt-ID wurde in die Zwischenablage kopiert", + "select-asset": "Objekt auswählen", + "no-assets-matching": "Es wurden keine zu '{{entity}}' passenden Objekte gefunden.", + "asset-required": "Objekt ist erforderlich", + "name-starts-with": "Name des Objekts beginnt mit", + "label": "Bezeichnung" + }, + "attribute": { + "attributes": "Eigenschaften", + "latest-telemetry": "Neueste Telemetrie", + "attributes-scope": "Entitätseigenschaftsbereich", + "scope-latest-telemetry": "Neueste Telemetrie", + "scope-client": "Client Eigenschaften", + "scope-server": "Server Eigenschaften", + "scope-shared": "Gemeinsame Eigenschaften", + "add": "Eigenschaft hinzufügen", + "key": "Schlüssel", + "last-update-time": "Datum der letzten Aktualisierung", + "key-required": "Eigenschaftsschlüssel ist erforderlich.", + "value": "Wert", + "value-required": "Eigenschaftswert ist erforderlich.", + "delete-attributes-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Eigenschaft} other {# Eigenschaften} } löschen möchten?", + "delete-attributes-text": "Seien Sie vorsichtig, nach der Bestätigung werden alle ausgewählten Eigenschaften entfernt.", + "delete-attributes": "Eigenschaften löschen", + "enter-attribute-value": "Geben Sie den Eigenschaftswert ein", + "show-on-widget": "Im Widget anzeigen", + "widget-mode": "Widget-Modus", + "next-widget": "Nächstes Widget", + "prev-widget": "Vorheriges Widget", + "add-to-dashboard": "Zum Dashboard hinzufügen", + "add-widget-to-dashboard": "Widget zum Dashboard hinzufügen", + "selected-attributes": "{ count, plural, 1 {1 Eigenschaft} other {# Eigenschaften} } ausgewählt", + "selected-telemetry": "{ count, plural, 1 {1 Telemetrieeinheit } other {# Telemetrieeinheiten} } ausgewählt" + }, + "audit-log": { + "audit": "Audit", + "audit-logs": "Audit-Protokolle", + "timestamp": "Zeitstempel", + "entity-type": "Entitätstype", + "entity-name": "Entitätsname", + "user": "User", + "type": "Typ", + "status": "Status", + "details": "Details", + "type-added": "Hinzugefügt", + "type-deleted": "Gelöscht", + "type-updated": "Aktualisiert", + "type-attributes-updated": "Eigenschaften aktualisiert", + "type-attributes-deleted": "Eigenschaften gelöscht", + "type-rpc-call": "RPC Aufruf", + "type-credentials-updated": "Anmeldeinformationen wurden aktualisiert", + "type-assigned-to-customer": "Kunden Zuordnung", + "type-unassigned-from-customer": "Kunden Zuordnung aufgehoben", + "type-activated": "Aktiviert", + "type-suspended": "Ausgesetzt", + "type-credentials-read": "Anmeldeinformationen gelesen", + "type-attributes-read": "Eigenschaften gelesen", + "type-relation-add-or-update": "Beziehung aktualisiert", + "type-relation-delete": "Beziehung gelöscht", + "type-relations-delete": "Alle Beziehungen gelöscht", + "type-alarm-ack": "Bestätigt", + "type-alarm-clear": "Gelöscht", + "type-login": "Anmeldung", + "type-logout": "Ausloggen", + "type-lockout": "Aussperrung", + "status-success": "Erfolg", + "status-failure": "Fehler", + "audit-log-details": "Audit-Protokoll Details", + "no-audit-logs-prompt": "Keine Protokolle gefunden", + "action-data": "Aktionsdaten", + "failure-details": "Fehlerdetails", + "search": "Audit-Protokolle durchsuchen", + "clear-search": "Suche leeren" + }, + "confirm-on-exit": { + "message": "Sie haben nicht gespeicherte Änderungen. Möchten Sie diese Seite wirklich verlassen?", + "html-message": "Sie haben nicht gespeicherte Änderungen.
    Möchten Sie diese Seite wirklich verlassen?", + "title": "Nicht gespeicherte Änderungen" + }, + "contact": { + "country": "Land", + "city": "Stadt", + "state": "Bundesland", + "postal-code": "Postleitzahl", + "postal-code-invalid": "Ungültiges Format der Postleitzahl.", + "address": "Adresse", + "address2": "Adresse 2", + "phone": "Telefon", + "email": "E-Mail", + "no-address": "Keine Adresse" + }, + "common": { + "username": "Benutzername", + "password": "Passwort", + "enter-username": "Benutzername eingeben", + "enter-password": "Passwort eingeben", + "enter-search": "Suche eingeben", + "created-time": "Erstellungszeit" + }, + "content-type": { + "json": "Json", + "text": "Text", + "binary": "Binär (Base64)" + }, + "customer": { + "customer": "Kunde", + "customers": "Kunden", + "management": "Kundenverwaltung", + "dashboard": "Kunden Dashboard", + "dashboards": "Kunden Dashboards", + "devices": "Kundengeräte", + "entity-views": "Kunden Entitätsansichten", + "assets": "Kundenobjekte", + "public-dashboards": "Öffentliche Dashboards", + "public-devices": "Öffentliche Geräte", + "public-assets": "Öffentliche Objekte", + "public-entity-views": "Öffentliche Entitätsansichten", + "add": "Kunde hinzufügen", + "delete": "Kunde löschen", + "manage-customer-users": "Kundenbenutzer verwalten", + "manage-customer-devices": "Kundengeräte verwalten", + "manage-customer-dashboards": "Kunden-Dashboards verwalten", + "manage-public-devices": "Öffentliche Geräte verwalten", + "manage-public-dashboards": "Öffentliche Dashboards verwalten", + "manage-customer-assets": "Kundenobjekte verwalten", + "manage-public-assets": "Öffentliche Objekte verwalten", + "add-customer-text": "Neuen Kunden hinzufügen", + "no-customers-text": "Keine Kunden gefunden", + "customer-details": "Kundendetails", + "delete-customer-title": "Sind Sie sicher, dass der Kunde '{{customerTitle}}' gelöscht werden soll?", + "delete-customer-text": "Vorsicht, nach Bestätigung wird der Kunde und alle zugehörigen Daten nicht wiederherstellbar gelöscht.", + "delete-customers-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Kunde} other {# Kunden} } löschen möchten?", + "delete-customers-action-title": "{ count, plural, 1 {1 Kunde} other {# Kunden} } löschen", + "delete-customers-text": "Seien Sie vorsichtig, nach der Bestätigung werden alle ausgewählten Kunden und alle zugehörigen Daten nicht wiederherstellbar gelöscht.", + "manage-users": "User verwalten", + "manage-assets": "Objekte verwalten", + "manage-devices": "Geräte verwalten", + "manage-dashboards": "Dashboards verwalten", + "title": "Titel", + "title-required": "Titel ist erforderlich.", + "description": "Beschreibung", + "details": "Details", + "events": "Ereignisse", + "copyId": "Kunden-ID kopieren", + "idCopiedMessage": "Kunden-ID wurde in die Zwischenablage kopiert", + "select-customer": "Kunden auswählen", + "no-customers-matching": "Keine Kunden für '{{entity}}' gefunden.", + "customer-required": "Kunde ist erforderlich", + "select-default-customer": "Wählen Sie den Standardkunden aus.", + "default-customer": "Standardkunde", + "default-customer-required": "Ein Standardkunde ist erforderlich, um das Dashboard auf Mandantenebene zu testen." + }, + "datetime": { + "date-from": "Datum von", + "time-from": "Zeit von", + "date-to": "Datum bis", + "time-to": "Zeit bis" + }, + "dashboard": { + "dashboard": "Dashboard", + "dashboards": "Dashboards", + "management": "Dashboardverwaltung", + "view-dashboards": "Dashboards anzeigen", + "add": "Dashboard hinzufügen", + "assign-dashboard-to-customer": "Dashboard(s) dem Kunden zuordnen", + "assign-dashboard-to-customer-text": "Bitte wählen Sie die Dashboards aus, die Sie dem Kunden zuordnen möchten", + "assign-to-customer-text": "Bitte wählen Sie den Kunden aus, dem die Dashboards zugeordnet werden sollen", + "assign-to-customer": "Kunden zuordnen", + "unassign-from-customer": "Zuordnung zum Kunden aufheben", + "make-public": "Dashboard öffentlich machen", + "make-private": "Dashboard privat machen", + "manage-assigned-customers": "Zugeordnete Kunden verwalten", + "assigned-customers": "Zugeordnete Kunden", + "assign-to-customers": "Dashboard(s) zu Kunden zuweisen", + "assign-to-customers-text": "Bitte wählen Sie den Kunden aus, dem Sie das Dashboard(s) zuweisen möchten", + "unassign-from-customers": "Zuordnung von Dashboard(s) zu Kunden aufheben", + "unassign-from-customers-text": "Bitte wählen Sie die Kunden aus, für die die Zuordnung von Dashboard(s) aufgehoben werden soll", + "no-dashboards-text": "Keine Dashboard(s) gefunden", + "no-widgets": "Keine Widgets konfiguriert", + "add-widget": "Neues Widget hinzufügen", + "title": "Titel", + "select-widget-title": "Widget auswählen", + "select-widget-subtitle": "Liste der verfügbaren Widget-Typen", + "delete": "Dashboard löschen", + "title-required": "Titel ist erforderlich.", + "description": "Beschreibung", + "details": "Details", + "dashboard-details": "Dashboard-Details", + "add-dashboard-text": "Neues Dashboard hinzufügen", + "assign-dashboards": "Dashboards zuweisen", + "assign-new-dashboard": "Neues Dashboard zuweisen", + "assign-dashboards-text": "Zuordnen { count, plural, 1 {1 Dashboard} other {# Dashboards} } zu Kunden", + "unassign-dashboards-action-text": "Zuordnung { count, plural, 1 {1 Dashboard} other {# Dashboards} } vom Kunden aufheben", + "delete-dashboards": "Dashboards löschen", + "unassign-dashboards": "Zuordnung von Dashboards aufheben", + "unassign-dashboards-action-title": "Zuordnung { count, plural, 1 {1 Dashboard} other {# Dashboards} } vom Kunden aufheben", + "delete-dashboard-title": "Sind Sie sicher, dass Sie das Dashboard '{{dashboardTitle}}' löschen möchten?", + "delete-dashboard-text": "Vorsicht, nach Bestätigung werden das Dashboard und alle zugehörigen Daten nicht mehr wiederhergestellt.", + "delete-dashboards-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Dashboard} other {# Dashboards} } löschen möchten?", + "delete-dashboards-action-title": "Löschen { count, plural, 1 {1 Dashboard} other {# Dashboards} }", + "delete-dashboards-text": "Vorsicht, nach Bestätigung werden alle ausgewählten Dashboards entfernt und alle zugehörigen Daten nicht mehr wiederhergestellt.", + "unassign-dashboard-title": "Sind Sie sicher, dass Sie die Zuordnung zum Dashboard '{{dashboardTitle}}' aufheben möchten?", + "unassign-dashboard-text": "Nach der Bestätigung wird die Zuordnung des Dashboards aufgehoben und es ist für den Kunden nicht mehr zugänglich.", + "unassign-dashboard": "Zuordnung zum Kunden aufheben", + "unassign-dashboards-title": "Sind Sie sicher, dass Sie die Zuordug aufheben möchten { count, plural, 1 {1 Dashboard} other {# Dashboards} }?", + "unassign-dashboards-text": "Nach der Bestätigung wird die Zuordnung aller ausgewählten Dashboards aufgehoben und sie sind für den Kunden nicht mehr zugänglich.", + "public-dashboard-title": "Dashboard wurde veröffentlicht", + "public-dashboard-text": "Ihr Dashboard {{dashboardTitle}} ist jetzt öffentlich und über nächste Öffentlichkeit zugänglich link:", + "public-dashboard-notice": "Note: Vergessen Sie nicht, verwandte Geräte öffentlich zu machen, um auf Ihre Daten zugreifen zu können.", + "make-private-dashboard-title": "Sind Sie sicher, dass Sie das Dashboard '{{dashboardTitle}}' privatisieren möchten?", + "make-private-dashboard-text": "Nach der Bestätigung wird das Dashboard privatisiert und ist für andere nicht zugänglich.", + "make-private-dashboard": "Dashboard privatisieren", + "socialshare-text": "'{{dashboardTitle}}' Bereitgestellt vom ThingsBoard", + "socialshare-title": "'{{dashboardTitle}}' Bereitgestellt vom ThingsBoard", + "select-dashboard": "Dashboard auswählen", + "no-dashboards-matching": "Es wurden keine passenden Dashboards '{{entity}}' gefunden.", + "dashboard-required": "Dashboard ist erforderlich.", + "select-existing": "Existierendes Dashboard auswählen", + "create-new": "Neues Dashboard erstellen", + "new-dashboard-title": "Neuer Dashboard Titel", + "open-dashboard": "Dashboard öffnen", + "set-background": "Hintergrund einstellen", + "background-color": "Hintergrundfarbe", + "background-image": "Hintergrundbild", + "background-size-mode": "Hintergrundgrößenmodus", + "no-image": "Kein Bild ausgewählt", + "drop-image": "Legen Sie ein Bild ab oder klicken Sie, um eine hochzuladende Datei auszuwählen.", + "settings": "Einstellungen", + "columns-count": "Spalten zählen", + "columns-count-required": "Die Anzahl der Spalten ist erforderlich.", + "min-columns-count-message": "Es müssen mindestens 10 Spalten vorhanden sein.", + "max-columns-count-message": "Es sind maximal 100 Spalten zulässig.", + "widgets-margins": "Abstand zwischen den Widgets", + "horizontal-margin": "Horizontaler Abstand", + "horizontal-margin-required": "Horizontaler Abstandswert ist erforderlich.", + "min-horizontal-margin-message": "Der horizontale Abstandswert muss mindestens 0 betragen.", + "max-horizontal-margin-message": "Der horizontale Abstandswert beträgt maximal 50.", + "vertical-margin": "Vertikaler Abstand", + "vertical-margin-required": "Vertikaler Abstandswert ist erforderlich.", + "min-vertical-margin-message": "Der vertikale Abstandswert muss mindestens 0 betragen.", + "max-vertical-margin-message": "Der vertikale Abstandswert beträgt maximal 50.", + "autofill-height": "Layouthöhe automatisch füllen", + "mobile-layout": "Mobile Layouteinstellungen", + "mobile-row-height": "Mobile Zeilenhöhe, px", + "mobile-row-height-required": "Ein mobiler Zeilenhöhenwert ist erforderlich.", + "min-mobile-row-height-message": "Der Mindestwert für die mobile Zeilenhöhe beträgt 5 Pixel.", + "max-mobile-row-height-message": "Der Höchstwert für die mobile Zeilenhöhe beträgt 200 Pixel.", + "display-title": "Display Dashboard Titel", + "toolbar-always-open": "Werkzeugleiste geöffnet lassen", + "title-color": "Titelfarbe ", + "display-dashboards-selection": "Auswahl der Dashboards anzeigen", + "display-entities-selection": "Auswahl der Einheiten zulassen", + "display-dashboard-timewindow": "Zeitfenster anzeigen", + "display-dashboard-export": "Export anzeigen", + "import": "Dashboard importieren", + "export": "Dashboard exportieren", + "export-failed-error": "Dashboard kann nicht exportiert werden: {{error}}", + "create-new-dashboard": "Neues Dashboard erstellen", + "dashboard-file": "Dashboard-Datei", + "invalid-dashboard-file-error": "Dashboard kann nicht importiert werden: Ungültige Dashboard-Datenstruktur.", + "dashboard-import-missing-aliases-title": "Konfigurieren Sie die von importierten Dashboards verwendeten Aliasnamen", + "create-new-widget": "Neues Widget erstellen", + "import-widget": "Widget importieren", + "widget-file": "Widget-Datei", + "invalid-widget-file-error": "Widget kann nicht importiert werden: Ungültige Widget-Datenstruktur.", + "widget-import-missing-aliases-title": "Konfigurieren Sie die von importierten Widgets verwendeten Aliase", + "open-toolbar": "Dashboard-Werkzeugleiste öffnen", + "close-toolbar": "Werkzeugleiste schließen", + "configuration-error": "Konfigurationsfehler", + "alias-resolution-error-title": "Konfigurationsfehler für Dashboard-Aliasnamen", + "invalid-aliases-config": "Es konnten keine Geräte gefunden werden, die mit dem Aliase-Filter übereinstimmen.
    Bitte wenden Sie sich an Ihren Administrator, um dieses problem zu beheben.", + "select-devices": "Geräte auswählen", + "assignedToCustomer": "Dem Kunden zugewiesen", + "assignedToCustomers": "Kunden zugwiesen", + "public": "Öffentlich", + "public-link": "Öffentlicher Link", + "copy-public-link": "Öffentlichen Link kopieren", + "public-link-copied-message": "Der öffentliche Link des Dashboards wurde in die Zwischenablage kopiert", + "manage-states": "Dashboard-Status verwalten", + "states": "Dashboard-Status", + "search-states": "Dashboard-Status suchen", + "selected-states": "{ count, plural, 1 {1 dashboard state} other {# dashboard states} } ausgewählt", + "edit-state": "Dashboard-Status bearbeiten", + "delete-state": "Dashboard-Status löschen", + "add-state": "Dashboard-Status hinzufügen", + "state": "Dashboard-Status", + "state-name": "Name", + "state-name-required": "Name des Dashboard-Status ist erforderlich.", + "state-id": "Status-Id", + "state-id-required": "Dashboard-Status-ID ist erforderlich.", + "state-id-exists": "Dashboard-Status mit derselben ID ist bereits vorhanden .", + "is-root-state": "Grundzustand", + "delete-state-title": "Dashboard-Status löschen", + "delete-state-text": "Sind Sie sicher, dass Sie den Dashboard-Status '{{stateName}}' löschen möchten?", + "show-details": "Details anzeigen", + "hide-details": "Details ausblenden", + "select-state": "Soll-Zustand auswählen", + "state-controller": "Zustandssteuerung" + }, + "datakey": { + "settings": "Einstellungen", + "advanced": "Erweitert", + "label": "Bezeichnung", + "color": "Farbe", + "units": "Maßeinheit die neben dem Wert angezeigt wird", + "decimals": "Anzahl der Stellen nach dem Komma", + "data-generation-func": "Daten generieren", + "use-data-post-processing-func": "Datenverarbeitungsfunktion verwenden", + "configuration": "Datenschlüsselkonfiguration", + "timeseries": "Zeitreihe", + "attributes": "Eigenschaften", + "alarm": "Alarmfelder", + "timeseries-required": "Entity-Zeitreihen sind erforderlich.", + "timeseries-or-attributes-required": "Entity-Zeitreihen/Eigenschaften sind erforderlich.", + "maximum-timeseries-or-attributes": "Maximum { count, plural, 1 {1 Zeitreihe/Eigenschaft ist erlaubt} other {# Zeitreihen/Eigenschaften sind erlaubt} }.", + "alarm-fields-required": "Alarmfelder sind erforderlich.", + "function-types": "Funktionsarten", + "function-types-required": "Funktionstypen sind erforderlich.", + "maximum-function-types": "Maximal { count, plural, 1 {1 Funktionstyp ist erlaubt} other {# Funktionstypen sind erlaubt} }.", + "time-description": "Zeitstempel des aktuellen Wertes;", + "value-description": "Der aktuelle Wert;", + "prev-value-description": "Ergebnis des vorherigen Funktionsaufrufs;", + "time-prev-description": "Zeitmarke des vorherigen Wertes;", + "prev-orig-value-description": "Ursprünglicher vorheriger Wert;" + }, + "datasource": { + "type": "Datenquellentyp", + "name": "Name", + "add-datasource-prompt": "Bitte Datenquelle hinzufügen" + }, + "details": { + "edit-mode": "Bearbeitungsmodus", + "toggle-edit-mode": "Bearbeitungsmodus umschalten" + }, + "device": { + "device": "Gerät", + "device-required": "Gerät ist erforderlich.", + "devices": "Geräte", + "management": "Geräte verwalten", + "view-devices": "Geräte anzeigen", + "device-alias": "Geräte-Alias", + "aliases": "Gerätealiasnamen", + "no-alias-matching": "'{{alias}}' nicht gefunden.", + "no-aliases-found": "Keine Aliase gefunden.", + "no-key-matching": "'{{key}}' nicht gefunden.", + "no-keys-found": "Keine Schlüssel gefunden.", + "create-new-alias": "Neues Alias erstellen!", + "create-new-key": "Neuen Schlüssel erstellen!", + "duplicate-alias-error": "Doppelter Alias gefunden '{{alias}}'.
    Gerätealiasnamen müssen innerhalb des Dashboards eindeutig sein.", + "configure-alias": "Alias '{{alias}}' konfigurieren", + "no-devices-matching": "Keine passenden Geräte '{{entity}}' gefunden.", + "alias": "Alias", + "alias-required": "Geräte-Alias ist erforderlich.", + "remove-alias": "Geräte-Alias entfernen", + "add-alias": "Geräte-Alias hinzufügen", + "name-starts-with": "Gerätename beginnt mit", + "device-list": "Geräteliste", + "use-device-name-filter": "Filter verwenden", + "device-list-empty": "Keine Geräte ausgewählt.", + "device-name-filter-required": "Der Gerätenamefilter ist erforderlich.", + "device-name-filter-no-device-matched": "Keine Geräte beginnend mit '{{device}}' gefunden.", + "add": "Gerät hinzufügen", + "assign-to-customer": "Kunden zuordnen", + "assign-device-to-customer": "Gerät(e) dem Kunden zuordnen", + "assign-device-to-customer-text": "Bitte wählen Sie die Geräte aus, die Sie dem Kunden zuordnen möchten", + "make-public": "Gerät veröffentlichen", + "make-private": "Gerät privatisieren", + "no-devices-text": "Keine Geräte gefunden", + "assign-to-customer-text": "Bitte wählen Sie einen Kunden aus, dem die Geräte zugeordnet werden sollen", + "device-details": "Gerätedetails", + "add-device-text": "Neues Gerät hinzufügen", + "credentials": "Zugangsdaten", + "manage-credentials": "Zugangsdaten verwalten", + "delete": "Gerät löschen", + "assign-devices": "Gerät zuordnen", + "assign-devices-text": "{ count, plural, 1 {1 Gerät} other {# Geräte} } dem Kunden zuordnen", + "delete-devices": "Geräte löschen", + "unassign-from-customer": "Zuordnung zum Kunden aufheben", + "unassign-devices": "Nicht zugeordnete Geräte", + "unassign-devices-action-title": "Zuordnung von { count, plural, 1 {1 Gerät} other {# Geräte} } zum Kunden aufheben", + "assign-new-device": "Neues Gerät zuordnen", + "make-public-device-title": "Sind Sie sicher, dass Sie das Gerät '{{deviceName}}' öffentlich machen möchten?", + "make-public-device-text": "Nach der Bestätigung werden das Gerät und dessen Daten öffentlich und für andere zugänglich.", + "make-private-device-title": "Sind Sie sicher, dass Sie das Gerät '{{deviceName}}' privat machen möchten?", + "make-private-device-text": "Nach der Bestätigung werden das Gerät und dessen Daten privat und sind für andere nicht mehr zugänglich.", + "view-credentials": "Zugangsdaten anzeigen", + "delete-device-title": "Möchten Sie das Gerät '{{deviceName}}' wirklich löschen?", + "delete-device-text": "Vorsicht, nach Bestätigung werden das Gerät und alle zugehörigen Daten nicht mehr wiederhergestellt.", + "delete-devices-title": "Sind Sie sicher, dass Sie löschen möchten { count, plural, 1 {1 Gerät} other {# Geräte} }?", + "delete-devices-action-title": "Löschen { count, plural, 1 {1 Gerät} other {# Geräte} }", + "delete-devices-text": "Vorsicht, nach Bestätigung werden alle ausgewählten Geräte entfernt und alle zugehörigen Daten werden nicht mehr wiederhergestellt.", + "unassign-device-title": "Sind Sie sicher, dass Sie die Zuordnung zum Gerät '{{deviceName}}' wirklich aufheben möchten?", + "unassign-device-text": "Nach der Bestätigung ist das Gerät nicht zugeordnet und für den Kunden nicht zugänglich.", + "unassign-device": "Nicht zugeordnete Geräte", + "unassign-devices-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Gerät} other {# Geräte} } nicht mehr zuordnen möchten?", + "unassign-devices-text": "Nach der Bestätigung werden alle ausgewählten Geräte nicht zugewiesen und sind für den Kunden nicht zugänglich.", + "device-credentials": "Geräte Zugangsdaten", + "credentials-type": "Art der Zugangsdaten", + "access-token": "Zugangs-Token", + "access-token-required": "Zugangs-Token ist erforderlich.", + "access-token-invalid": "Die Länge des Zugangs-Tokens muss zwischen 1 und 20 Zeichen betragen.", + "rsa-key": "RSA öffentlicher Schlüssel", + "rsa-key-required": "RSA öffentlicher Schlüssel ist erforderlich.", + "secret": "Geheimnis", + "secret-required": "Geheimnis ist erforderlich.", + "device-type": "Gerätetyp", + "device-type-required": "Gerätetyp ist erforderlich.", + "select-device-type": "Gerätetyp auswählen", + "enter-device-type": "Gerätetyp eingeben", + "any-device": "Jedes Gerät", + "no-device-types-matching": "Keine passenden Gerätetypen '{{entitySubtype}}' gefunden.", + "device-type-list-empty": "Kein Gerätetyp ausgewählt.", + "device-types": "Gerätetypen", + "name": "Name", + "name-required": "Name ist erforderlich.", + "description": "Beschreibung", + "events": "Ereignisse", + "details": "Details", + "copyId": "Geräte-ID kopieren", + "copyAccessToken": "Zugangs-Token kopieren", + "idCopiedMessage": "Geräte-ID wurde in die Zwischenablage kopiert", + "accessTokenCopiedMessage": "Geräte-Zugangs-Token wurde in die Zwischenablage kopiert", + "assignedToCustomer": "Dem Kunden zuordnen", + "unable-delete-device-alias-title": "Geräte-Alias kann nicht gelöscht werden", + "unable-delete-device-alias-text": "Geräte-Alias '{{deviceAlias}}' kann nicht gelöscht werden, da er von den folgenden Widgets verwendet wird:
    {{widgetsList}}", + "is-gateway": "Ist ein Gateway", + "public": "Öffentlich", + "device-public": "Gerät ist öffentlich", + "select-device": "Gerät auswählen" + }, + "dialog": { + "close": "Dialog schließen" + }, + "error": { + "unable-to-connect": "Es konnte keine Verbindung zum Server hergestellt werden! Bitte überprüfen Sie Ihre Internetverbindung.", + "unhandled-error-code": "Unbehandelter Fehlercode: {{errorCode}}", + "unknown-error": "Unbekannter Fehler" + }, + "entity": { + "entity": "Entität", + "entities": "Entitäten", + "aliases": "Entitäts-Aliasnamen", + "entity-alias": "Entitätsalias", + "unable-delete-entity-alias-title": "Alias der Entität kann nicht gelöscht werden", + "unable-delete-entity-alias-text": "Alias der Entität '{{entityAlias}}' kann nicht gelöscht werden, da es von den folgenden Widget(s) verwendet wird:
    {{widgetsList}}", + "duplicate-alias-error": "Doppelte Alias gefunden '{{alias}}'.
    Die Aliase der Entität müssen innerhalb des Dashboards eindeutig sein.", + "missing-entity-filter-error": "Fehlender Filter für Alias '{{alias}}'.", + "configure-alias": "Alias '{{alias}}' konfigurieren", + "alias": "Alias", + "alias-required": "Alias der Entität ist erforderlich.", + "remove-alias": "Alias der Entität entfernen", + "add-alias": "Alias der Entität erforderlich", + "entity-list": "Entitätsliste", + "entity-type": "Entitätstyp", + "entity-types": "Entitätstypen", + "entity-type-list": "Liste der Entitätstyp", + "any-entity": "Jede Entität", + "enter-entity-type": "Entitätstyp eingeben", + "no-entities-matching": "Keine passenden Entitäten für '{{entity}}' gefunden.", + "no-entity-types-matching": "Keine passende Entitätstypen für '{{entityType}}' gefunden.", + "name-starts-with": "Name beginnt mit", + "use-entity-name-filter": "Filter verwenden", + "entity-list-empty": "Keine Entitäten ausgewählt.", + "entity-type-list-empty": "Keine Entitättypen ausgewählt.", + "entity-name-filter-required": "Entitätsnamenfilter ist erforderlich.", + "entity-name-filter-no-entity-matched": "Keine Entitäten beginnend mit '{{entity}}' gefunden.", + "all-subtypes": "Alle", + "select-entities": "Entitäten auswählen", + "no-aliases-found": "Keine Aliase gefunden.", + "no-alias-matching": "'{{alias}}' nicht gefunden.", + "create-new-alias": "Erstellen Sie einen neuen Alias!", + "key": "Schlüssel", + "key-name": "Name des Schlüssels", + "no-keys-found": "Kein Schlüssel gefunden.", + "no-key-matching": "'{{key}}' nicht gefunden.", + "create-new-key": "Erstellen Sie einen neuen Schlüssel!", + "type": "Typ", + "type-required": "Typ der Entität ist erforderlich.", + "type-device": "Gerät", + "type-devices": "Geräte", + "list-of-devices": "{ count, plural, 1 {Ein Gerät} other {Liste von # Geräten} }", + "device-name-starts-with": "Geräte beginnend mit '{{prefix}}'", + "type-asset": "Objekt", + "type-assets": "Objekte", + "list-of-assets": "{ count, plural, 1 {Ein Objekt} other {Liste von # Objekten} }", + "asset-name-starts-with": "Objekte beginnend mit '{{prefix}}'", + "type-entity-view": "Entitätsansicht", + "type-entity-views": "Entitätsansichten", + "list-of-entity-views": "{ count, plural, 1 {Eine Entitätsansicht} other {Liste von # Entitätsansichten} }", + "entity-view-name-starts-with": "Entitätsansichten beginnend mit'{{prefix}}'", + "type-rule": "Regel", + "type-rules": "Regeln", + "list-of-rules": "{ count, plural, 1 {Eine Regel} other {Liste von # Regeln} }", + "rule-name-starts-with": "Regeln beginnend mit '{{prefix}}'", + "type-plugin": "Plugin", + "type-plugins": "Plugins", + "list-of-plugins": "{ count, plural, 1 {Ein Plugin} other {Liste von # Plugins} }", + "plugin-name-starts-with": "Plugins beginnend mit '{{prefix}}'", + "type-tenant": "Mandant", + "type-tenants": "Mandanten", + "list-of-tenants": "{ count, plural, 1 {Ein Mandant} other {Liste von # Mandanten} }", + "tenant-name-starts-with": "Mandanten beginnend mit '{{prefix}}'", + "type-customer": "Kunde", + "type-customers": "Kunden", + "list-of-customers": "{ count, plural, 1 {Ein Kunde} other {Liste von # Kunden} }", + "customer-name-starts-with": "Kunden beginnend mit '{{prefix}}'", + "type-user": "Benutzer", + "type-users": "Benutzer", + "list-of-users": "{ count, plural, 1 {Ein Benutzer} other {Liste von # Benutzern} }", + "user-name-starts-with": "Benutzer beginnend mit '{{prefix}}'", + "type-dashboard": "Dashboard", + "type-dashboards": "Dashboards", + "list-of-dashboards": "{ count, plural, 1 {Ein Dashboard} other {Liste von # Dashboards} }", + "dashboard-name-starts-with": "Dashboards beginnend mit '{{prefix}}'", + "type-alarm": "Alarm", + "type-alarms": "Alarme", + "list-of-alarms": "{ count, plural, 1 {Ein Alarm} other {Liste von # Alarmen} }", + "alarm-name-starts-with": "Alarme, beginnend mit '{{prefix}}'", + "type-rulechain": "Regelkette", + "type-rulechains": "Regelketten", + "list-of-rulechains": "{ count, plural, 1 {Eine Regelkette} other {Liste von # Regelketten} }", + "rulechain-name-starts-with": "Regelketten beginnend mit '{{prefix}}'", + "type-rulenode": "Regelknoten", + "type-rulenodes": "Regelknoten", + "list-of-rulenodes": "{ count, plural, 1 {Ein Regelknoten} other {Liste von # Regelknoten} }", + "rulenode-name-starts-with": "Regelknoten beginnend mit '{{prefix}}'", + "type-current-customer": "Aktueller Kunde", + "search": "Entitäten suchen", + "selected-entities": "{ count, plural, 1 {Entität} other {# Entitäten} } ausgewählt", + "entity-name": "Entitätsname", + "details": "Entitätsdetails", + "no-entities-prompt": "Keine Entitäten gefunden", + "no-data": "Keine Daten zum Anzeigen", + "columns-to-display": "Anzuzeigende Spalten" + }, + "entity-view": { + "entity-view": "Entitätsansicht", + "entity-view-required": "Entitätsansicht ist erforderlich.", + "entity-views": "Entitätsansichten", + "management": "Entitätsansichten verwalten", + "view-entity-views": "Entitätsansichten anzeigen", + "entity-view-alias": "Entitätsansichtsalias", + "aliases": "Entitätsansichten-Aliase", + "no-alias-matching": "'{{alias}}' nicht gefunden.", + "no-aliases-found": "Keine Aliase gefunden.", + "no-key-matching": "'{{key}}' nicht gefunden.", + "no-keys-found": "Keine Schlüssel gefunden.", + "create-new-alias": "Neuen Alias erstellen!", + "create-new-key": "Neuen Schlüssel erstellen!", + "duplicate-alias-error": "Doppelter Alias gefunden '{{alias}}'.
    Aliase der Entitätsansicht müssen innerhalb des Dashboards eindeutig sein.", + "configure-alias": "Alias '{{alias}}' konfigurieren", + "no-entity-views-matching": "Keine passenden Entitätsansichten für '{{entity}}' gefunden.", + "alias": "Alias", + "alias-required": "Alias der Entitätsansicht erforderlich.", + "remove-alias": "Alias der Entitätsansicht entfernen", + "add-alias": "Alias für die Entitätsansicht hinzufügen", + "name-starts-with": "Entitätsansichtsname beginnend mit", + "entity-view-list": "Liste der Entitätsansichten", + "use-entity-view-name-filter": "Filter anwenden", + "entity-view-list-empty": "Keine der Entitätsansichten ausgewählt.", + "entity-view-name-filter-required": "Filterung nach Entitätsansichtenname erforderlich.", + "entity-view-name-filter-no-entity-view-matched": "Keine Entitätsansichten beginnend mit '{{entityView}}' wurden gefunden.", + "add": "Entitätsansicht hinzufügen", + "assign-to-customer": "Einem Kunden zuordnen", + "assign-entity-view-to-customer": "Entitätsansichten dem Kunden zuordnen", + "assign-entity-view-to-customer-text": "Bitte wählen Sie die Entitätsansichten aus, die dem Kunden zugeordnet werden sollen", + "no-entity-views-text": "Keine Entitätsansichten gefunden", + "assign-to-customer-text": "Bitte wählen Sie den Kunden aus, dem die Entitätsansichten zugeordnet werden sollen", + "entity-view-details": "Details der Entitätsansicht", + "add-entity-view-text": "Neue Entitätsansicht hinzufügen", + "delete": "Entitätsansicht löschen", + "assign-entity-views": "Entitätsansicht zuordnen", + "assign-entity-views-text": "Dem Kunden { count, plural, 1 {1 Entitätsansicht} other {# Entitätsansichten} } zuordnen", + "delete-entity-views": "Entitätsansichten löschen", + "unassign-from-customer": "Zuordnung zum Kunden aufheben", + "unassign-entity-views": "Zuordnung der Entitätsansichten aufheben", + "unassign-entity-views-action-title": "Die Zuordnung { count, plural, 1 {1 Entitätsansicht} other {# Entitätsansichten} } zum Kunden aufheben", + "assign-new-entity-view": "Neue Entitätsansicht zuordnen", + "delete-entity-view-title": "Möchten Sie die Entitätsansicht wirklich löschen '{{entityViewName}}'?", + "delete-entity-view-text": "Seien Sie vorsichtig, nach der Bestätigung werden die Entitätsansicht und alle zugehörigen Daten nicht wiederhergestellt.", + "delete-entity-views-title": "Sind Sie sicher, dass Sie die Entitätsansichten löschen möchten { count, plural, 1 {1 Entitätsansicht} other {# Entitätsansichten} }?", + "delete-entity-views-action-title": "Löschen { count, plural, 1 {1 Entitätsansicht} other {# Entitätsansichten} }", + "delete-entity-views-text": "Vorsicht, nach Bestätigung werden alle ausgewählten Entitätsansichten entfernt und alle zugehörigen Daten werden nicht wiederhergestellt.", + "unassign-entity-view-title": "Möchten Sie die Zuordnung der Entitätsansicht '{{entityViewName}}' wirklich aufheben?", + "unassign-entity-view-text": "Nach der Bestätigung wird die Zuordnung der Entitätsansicht aufgehoben und ist für den Kunden nicht mehr zugänglich.", + "unassign-entity-view": "Zuordnung der Entitätsansicht aufheben", + "unassign-entity-views-title": "Sind Sie sicher, dass Sie die Zuordnung aufheben möchten { count, plural, 1 {1 Entitätsansicht} other {# Entitätsansichten} }?", + "unassign-entity-views-text": "Nach der Bestätigung werden die Zuordnungen der ausgewählten Entitätsansichten aufgehoben und sind für den Kunden nicht mehr zugänglich.", + "entity-view-type": "Entitätsansichtstyp", + "entity-view-type-required": "Entitätsansichtstyp ist erforderlich.", + "select-entity-view-type": "Entitätsansichtstyp auswählen", + "enter-entity-view-type": "Entitätsansichtstyp eingeben", + "any-entity-view": "Jede Entitätsansicht", + "no-entity-view-types-matching": "Es wurden keine passenden Entitätsansichtstypen für '{{entitySubtype}}' gefunden.", + "entity-view-type-list-empty": "Keine Entitätsansichtstypen ausgewählt.", + "entity-view-types": "Entitätsansichtstypen", + "name": "Name", + "name-required": "Name ist erforderlich.", + "description": "Beschreibung", + "events": "Ereignisse", + "details": "Details", + "copyId": "Entitätsansichts-ID kopieren", + "assignedToCustomer": "Dem Kunden zuordnen", + "unable-entity-view-device-alias-title": "Alias der Entitätsansicht kann nicht gelöscht werden", + "unable-entity-view-device-alias-text": "Geräte-Alias '{{entityViewAlias}}' kann nicht gelöscht werden, da es von den folgenden widget(s):
    {{widgetsList}} verwendet wird", + "select-entity-view": "Entitätsansicht auswählen", + "make-public": "Entitätsansicht öffentlich machen", + "start-date": "Start-Datum", + "start-ts": "Start-Zeit", + "end-date": "Ende-Datum", + "end-ts": "Ende-Zeit", + "date-limits": "Datumslimits", + "client-attributes": "Client Eigenschaften", + "shared-attributes": "Gemeinsame Eigenschaften", + "server-attributes": "Server Eigenschaften", + "timeseries": "Zeitreihe", + "client-attributes-placeholder": "Client Eigenschaften", + "shared-attributes-placeholder": "Gemeinsame Eigenschaften", + "server-attributes-placeholder": "Server Eigenschaften", + "timeseries-placeholder": "Zeitreihe", + "target-entity": "Zielentität", + "attributes-propagation": "Eigenschaftsübertragung", + "attributes-propagation-hint": "Die Entitätsansicht kopiert automatisch die angegebenen Eigenschaften der Ziel-Entität, wenn Sie diese Entitätsansicht speichern oder aktualisieren. Aus Performance-Gründen werden die Attribute der Ziel-Entität nicht bei jeder Eigenschaftsänderung in die Entitätsansicht übertragen. Sie können die automatische Weitergabe aktivieren, indem Sie den Regelknoten \"copy to view\" in Ihrer Regelkette konfigurieren und die Nachrichten \"Post attributes\" und \"Attributes updated\" mit dem neuen Regelknoten verknüpfen.", + "timeseries-data": "Zeitreihendaten", + "timeseries-data-hint": "Konfigurieren Sie die Datensatzschlüssel der Zeitreihe der Zielentität, auf die die Entitätsansicht zugreifen kann. Die Daten dieser Zeitreihe sind schreibgeschützt." + }, + "event": { + "event-type": "Ereignistyp", + "type-error": "Fehler", + "type-lc-event": "Lebenszyklusereignis", + "type-stats": "Statistiken", + "type-debug-rule-node": "Fehlersuche", + "type-debug-rule-chain": "Fehlersuche", + "no-events-prompt": "Keine Ereignisse gefunden", + "error": "Fehler", + "alarm": "Alarm", + "event-time": "Ereigniszeit", + "server": "Server", + "body": "Inhalt", + "method": "Methode", + "type": "Typ", + "entity": "Entität", + "message-id": "Nachrichten-Id", + "message-type": "Nachrichten-Typ", + "data-type": "Datentyp", + "relation-type": "Beziehungstyp", + "metadata": "Meta-Daten", + "data": "Daten", + "event": "Ereignis", + "status": "Status", + "success": "Erfolg", + "failed": "Fehlgeschlagen", + "messages-processed": "Nachrichten verarbeitet", + "errors-occurred": "Fehler aufgetreten" + }, + "extension": { + "extensions": "Erweiterungen", + "selected-extensions": "{ count, plural, 1 {Erweiterung} other {# extensions} } ausgewählt", + "type": "Typ", + "key": "Schlüssel", + "value": "Wert", + "id": "ID", + "extension-id": "Erweiterungs-ID", + "extension-type": "Erweiterungstyp", + "transformer-json": "JSON *", + "unique-id-required": "Die aktuelle Erweiterungs-ID ist bereits vorhanden.", + "delete": "Erweiterung löschen", + "add": "Erweiterung hinzufügen", + "edit": "Erweiterung bearbeiten", + "delete-extension-title": "Möchten Sie die Erweiterung '{{extensionId}}' wirklich löschen?", + "delete-extension-text": "Vorsicht, nach Bestätigung werden die Erweiterung und alle zugehörigen Daten nicht wiederhergestellt.", + "delete-extensions-title": "Möchten Sie wirklich löschen? { count, plural, 1 {1 extension} other {# extensions} }?", + "delete-extensions-text": "Vorsicht, nach der Bestätigung werden alle ausgewählten Erweiterungen entfernt.", + "converters": "Konverter", + "converter-id": "Konverter-ID", + "configuration": "Konfiguration", + "converter-configurations": "Konvertierte Konfigurationen", + "token": "Sicherheitszeichen", + "add-converter": "Konverter hinzufügen", + "add-config": "Konvertierte Konfigurationen hinzufügen", + "device-name-expression": "Angabe des Gerätenamens", + "device-type-expression": "Angabe des Gerätetyps", + "custom": "Regel", + "to-double": "Duplizieren", + "transformer": "Transformator", + "json-required": "Transformer json ist erforderlich.", + "json-parse": "Transformer json kann nicht analysiert werden.", + "attributes": "Eigenschaften", + "add-attribute": "Eigenschaften hinzufügen", + "add-map": "Mapping-Element hinzufügen", + "timeseries": "Zeitreihe", + "add-timeseries": "Zeitreihe hinzufügen", + "field-required": "Feld ist erforderlich", + "brokers": "Vermittler", + "add-broker": "Vermittler hinzufügen", + "host": "Host", + "port": "Port", + "port-range": "Der Port sollte im Bereich von 1 bis 65535 liegen.", + "ssl": "Ssl", + "credentials": "Zugangsdaten", + "username": "Benutzername", + "password": "Passwort", + "retry-interval": "Wiederholungsintervall in Millisekunden", + "anonymous": "Anonym", + "basic": "Basic", + "pem": "PEM", + "ca-cert": "CA-Zertifikatsdatei *", + "private-key": "Privatschlüsseldatei *", + "cert": "Zertifikatsdatei *", + "no-file": "Keine Datei ausgewählt.", + "drop-file": "Legen Sie eine Datei ab oder wählen Sie eine Datei aus um diese hochzuladen.", + "mapping": "Mapping", + "topic-filter": "Themenfilter", + "converter-type": "Konvertierungstyp", + "converter-json": "Json", + "json-name-expression": "Angabe des Gerätenamens json", + "topic-name-expression": "Themenangabe des Gerätenamens", + "json-type-expression": "Angabe des Gerätenamens json", + "topic-type-expression": "Themenangabe des Gerätetyps", + "attribute-key-expression": "Angabe des Eigenschaftenschlüssels", + "attr-json-key-expression": "Angabe des Eigenschaftenschlüssels", + "attr-topic-key-expression": "Themenangabe des Eigenschaftenschlüssels", + "request-id-expression": "ID-Angabe anfordern", + "request-id-json-expression": "ID-Angabe anforern json", + "request-id-topic-expression": "Themenangabe der ID anfordern", + "response-topic-expression": "Antwort Themenangabe", + "value-expression": "Wertangabe", + "topic": "Thema", + "timeout": "Unterbrechung in Millisekunden", + "converter-json-required": "Konvertierte json ist erforderlich.", + "converter-json-parse": "Konvertierte json konnte nicht analysiert werden.", + "filter-expression": "Filterangabe", + "connect-requests": "Abfragen verbinden", + "add-connect-request": "Verbindungsabfrage hinzufügen", + "disconnect-requests": "Abfrage trennen", + "add-disconnect-request": "Trennung der Abfrage hinzufügen", + "attribute-requests": "Abfrage der Eigenschaften", + "add-attribute-request": "Abfrage der Eigenschaften hinzufügen", + "attribute-updates": "Aktualisierungen der Eigenschaften", + "add-attribute-update": "Aktualisierung der Eigenschaften hinzufügen", + "server-side-rpc": "Serverseite RPC", + "add-server-side-rpc-request": "Abfrage der Serverseite RPC hinzufügen", + "device-name-filter": "Gerätenamefilter", + "attribute-filter": "Eigenschaftenfilter", + "method-filter": "Methodenfilter", + "request-topic-expression": "Themenabgabe anfordern", + "response-timeout": "Antwortzeit in Millisekunden", + "topic-expression": "Themenangabe", + "client-scope": "Kundenumfrage", + "add-device": "Gerät hinzufügen", + "opc-server": "Servers", + "opc-add-server": "Server hinzufügen", + "opc-add-server-prompt": "Bitte einen Server hinzufügen", + "opc-application-name": "Anwendungsname", + "opc-application-uri": "Anwendung uri", + "opc-scan-period-in-seconds": "Scanzeitraum in Sekunden", + "opc-security": "Sicherheit", + "opc-identity": "Identifizierung", + "opc-keystore": "Schlüsselspeicher", + "opc-type": "Typ", + "opc-keystore-type": "Typ", + "opc-keystore-location": "Standort *", + "opc-keystore-password": "Passwort", + "opc-keystore-alias": "Alias", + "opc-keystore-key-password": "Schlüsselpasswort", + "opc-device-node-pattern": "Geräteknotenmuster", + "opc-device-name-pattern": "Gerätenamensmuster", + "modbus-server": "Servers/Folgegerät", + "modbus-add-server": "Server/Folgegerät hinzufügen", + "modbus-add-server-prompt": "Bitte Server/Folgegerät hinzufügen", + "modbus-transport": "Transport", + "modbus-tcp-reconnect": "Verbindung automatisch wiederherstellen", + "modbus-rtu-over-tcp": "RTU über TCP", + "modbus-port-name": "Name des Seriellen Anschlusses", + "modbus-encoding": "Verschlüsselung", + "modbus-parity": "Übereinstimmung", + "modbus-baudrate": "Datenübertragungsgeschwindigkeit", + "modbus-databits": "Daten Bits", + "modbus-stopbits": "Stopp-Bits", + "modbus-databits-range": "Datenbits sollten im Bereich von 7 bis 8 liegen.", + "modbus-stopbits-range": "Stoppbits sollten im Bereich von 1 bis 2 liegen.", + "modbus-unit-id": "ID der Einheit", + "modbus-unit-id-range": "Die Einheiten-ID sollte im Bereich von 1 bis 247 liegen.", + "modbus-device-name": "Gerätename", + "modbus-poll-period": "Abfragezeitraum in Millisekunden", + "modbus-attributes-poll-period": "Abfrageintervall der Eigenschaften in Millisekunden", + "modbus-timeseries-poll-period": "Abfrageintervall der Zeitreihen in Millisekunden", + "modbus-poll-period-range": "Das Abfrageintervall sollte einen positiven Wert haben.", + "modbus-tag": "Kennzeichnung", + "modbus-function": "Funktion", + "modbus-register-address": "Registeradresse", + "modbus-register-address-range": "Die Registeradresse sollte im Bereich zwischen 0 und 65535 liegen.", + "modbus-register-bit-index": "Bitindex", + "modbus-register-bit-index-range": "Der Bitindex sollte im Bereich von 0 bis 15 liegen.", + "modbus-register-count": "Registeranzahl", + "modbus-register-count-range": "Die Registeranzahl sollten einen positiven Wert haben.", + "modbus-byte-order": "Byte-Reihenfolge", + "sync": { + "status": "Status", + "sync": "Synchronisiert", + "not-sync": "Nicht synchronisiert", + "last-sync-time": "Zeit der letzten Synchronisierung", + "not-available": "Nicht verfügbar" + }, + "export-extensions-configuration": "Erweiterungskonfiguration exportieren", + "import-extensions-configuration": "Erweiterungskonfiguration importieren", + "import-extensions": "Erweiterungen importieren", + "import-extension": "Erweiterung importieren", + "export-extension": "Erweiterung exportieren", + "file": "Erweiterungsdatei", + "invalid-file-error": "Ungültige Erweiterungsdatei" + }, + "fullscreen": { + "expand": "Auf Vollbildmodus erweitern", + "exit": "Vollbildmodus verlassen", + "toggle": "Vollbildmodus umschalten", + "fullscreen": "Vollbild" + }, + "function": { + "function": "Funktion" + }, + "grid": { + "delete-item-title": "Möchten Sie dieses Element wirklich löschen?", + "delete-item-text": "Vorsicht, nach Bestätigung wird das Element und alle zugehörigen Daten nicht wiederhergestellt.", + "delete-items-title": "Sind Sie sicher, dass Sie löschen möchten { count, plural, 1 {Symbol} other {Symbole} }?", + "delete-items-action-title": "Löschen { count, plural, 1 {Symbol} other {# Symbole} }", + "delete-items-text": "Vorsicht, nach Bestätigung werden alle ausgewählten Elemente entfernt und alle zugehörigen Daten nicht wiederhergestellt.", + "add-item-text": "Neues Element hinzufügen", + "no-items-text": "Keine Elemente gefunden", + "item-details": "Elementdetails", + "delete-item": "Element löschen", + "delete-items": "Elemente löschen", + "scroll-to-top": "zum Seitenanfang" + }, + "help": { + "goto-help-page": "Gehen Sie zur Hilfeseite" + }, + "home": { + "home": "Startseite", + "profile": "Profil", + "logout": "Abmelden", + "menu": "Menü", + "avatar": "Benutzerbild", + "open-user-menu": "Benutzermenü öffnen" + }, + "import": { + "no-file": "Keine Datei ausgewählt", + "drop-file": "Legen Sie eine JSON-Datei ab oder wählen Sie eine Datei zum hochladen aus." + }, + "item": { + "selected": "Ausgewählt" + }, + "js-func": { + "no-return-error": "Funktion muss einen Wert zurückgeben!", + "return-type-mismatch": "Funktion muss einen Wert vom Typ '{{type}}' zurückgeben!", + "tidy": "Aufräumen" + }, + "key-val": { + "key": "Schlüssel", + "value": "Wert", + "remove-entry": "Eintrag entfernen", + "add-entry": "Eintag hinzufügen", + "no-data": "Keine Einträge" + }, + "layout": { + "layout": "Layout", + "manage": "Layouts verwalten", + "settings": "Layout-Einstellungen", + "color": "Farbe", + "main": "Hauptbereich", + "right": "Recht", + "select": "Wählen Sie das Ziellayout aus" + }, + "legend": { + "position": "Legendenposition", + "show-max": "Maximalwert anzeigen", + "show-min": "Minimalwert anzeigen", + "show-avg": "Durchschnittswert anzeigen", + "show-total": "Gesamtwert anzeigen", + "settings": "Legendeneinstellungen", + "min": "min.", + "max": "max.", + "avg": "mittelw.", + "total": "Gesamt" + }, + "login": { + "login": "Anmelden", + "request-password-reset": "Passwortzurücksetzung anfordern", + "reset-password": "Passwort zurücksetzen", + "create-password": "Passwort erstellen", + "passwords-mismatch-error": "Eingegebene Passwörter müssen identisch sein!", + "password-again": "Passwort wiederholen", + "sign-in": "Bitte anmelden", + "username": "Benutzername (E-Mail)", + "remember-me": "Login speichern", + "forgot-password": "Passwort vergessen?", + "password-reset": "Passwort zurücksetzen", + "new-password": "Neues Passwort", + "new-password-again": "Neues Passwort wiederholen", + "password-link-sent-message": "Der Link zum Zurücksetzen des Passworts wurde erfolgreich versendet!", + "email": "E-Mail", + "login-with": "Mit {{name}} anmelden", + "or": "oder" + }, + "position": { + "top": "Oben", + "bottom": "Unten", + "left": "Links", + "right": "Rechts" + }, + "profile": { + "profile": "Profil", + "last-login-time": "Letzte Anmeldung", + "change-password": "Passwort ändern", + "current-password": "Aktuelles Passwort" + }, + "relation": { + "relations": "Beziehungen", + "direction": "Richtung", + "search-direction": { + "FROM": "Von", + "TO": "Zu" + }, + "direction-type": { + "FROM": "von", + "TO": "zu" + }, + "from-relations": "Ausgehende Verbindungen", + "to-relations": "Eingehende Verbindungen", + "selected-relations": "{ count, plural, 1 {1 Beziehung} other {# Beziehungen} } ausgewählt", + "type": "Typ", + "to-entity-type": "Zum Entitätstyp", + "to-entity-name": "Zum Entitätsnamen", + "from-entity-type": "Vom Entitätstyp", + "from-entity-name": "Vom Entitätsnamen", + "to-entity": "Zur Entität", + "from-entity": "Von Entität", + "delete": "Beziehung löschen", + "relation-type": "Art der Beziehung", + "relation-type-required": "Art der Beziehung erforderlich.", + "any-relation-type": "Jede Art", + "add": "Beziehung hinzufügen", + "edit": "Beziehung bearbeiten", + "delete-to-relation-title": "Möchten Sie die Beziehung zur Einheit'{{entityName}}' wirklich löschen?", + "delete-to-relation-text": "Vorsicht, nach Bestätigung ist die Entität '{{entityName}}' nicht mehr mit der aktuellen Entität verbunden.", + "delete-to-relations-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Beziehung} other {# Beziehungen} } wirklich löschen?", + "delete-to-relations-text": "Vorsicht, nach Bestätigung werden alle ausgewählten Beziehungen entfernt und die entsprechenden Entitäten sind nicht mehr mit der aktuellen Entität verbunden.", + "delete-from-relation-title": "Sind Sie sicher, dass Sie die Verbindung aus der Entität '{{entityName}}' löschen möchten?", + "delete-from-relation-text": "Vorsicht, nach Bestätigung wird die aktuelle Entität '{{entityName}}' von der Entität unabhängig sein.", + "delete-from-relations-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Beziehung} other {# Beziehungen} } löschen möchten?", + "delete-from-relations-text": "Vorsicht, nach Bestätigung werden alle ausgewählten Beziehungen entfernt und die aktuellen Entität wird nicht mehr mit den entsprechenden Entitäten verknüpft sein.", + "remove-relation-filter": "Beziehungsfilter entfernen", + "add-relation-filter": "Beziehungsfilter hinzufügen", + "any-relation": "Jede Beziehung", + "relation-filters": "Beziehungsfilter", + "additional-info": "Zusätzliche Information (JSON)", + "invalid-additional-info": "Json der Zusätzlichen Informationen konnte nicht gelesen werden." + }, + "rulechain": { + "rulechain": "Regelkette", + "rulechains": "Regelketten", + "root": "Wurzel", + "delete": "Regelkette löschen", + "name": "Name", + "name-required": "Name ist erforderlich.", + "description": "Beschreibung", + "add": "Regelkette hinzufügen", + "set-root": "Regelkette zur Wurzel machen", + "set-root-rulechain-title": "Sind Sie sicher, dass Sie die Regelkette '{{ruleChainName}}' zur Wurzel machen möchten?", + "set-root-rulechain-text": "Nach der Bestätigung wird die Regelkette zur Wurzel und bearbeitet alle eingehenden Transportnachrichten.", + "delete-rulechain-title": "Sind Sie sicher, dass Sie die Regelkette '{{ruleChainName}}' löschen möchten?", + "delete-rulechain-text": "Vorsichtig, nach Bestätigung werden die Regelkette und alle zugehörigen Daten gelöscht.", + "delete-rulechains-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Regelkette} other {# Regelketten} } löschen möchten?", + "delete-rulechains-action-title": "{ count, plural, 1 {1 Regelkette} other {# Regelketten} } löschen", + "delete-rulechains-text": "Vorsichtig, nach Bestätigung werden alle ausgewählten Regelketten entfernt und alle zugehörigen Daten werden gelöscht.", + "add-rulechain-text": "Neue Regelkette hinzufügen", + "no-rulechains-text": "Keine Regelkette gefunden", + "rulechain-details": "Regelketten-Details", + "details": "Details", + "events": "Ereignisse", + "system": "System", + "import": "Regelkette importieren", + "export": "Regelkette exportieren", + "export-failed-error": "Regelkette konnte nicht exportiert werden: {{error}}", + "create-new-rulechain": "Neue Regelkette erstellen", + "rulechain-file": "Regelkettendatei", + "invalid-rulechain-file-error": "Regelkette konnte nicht importiert werden: Ungültige Regelkettendatenstruktur.", + "copyId": "Regelketten-ID kopieren", + "idCopiedMessage": "Regelketten-ID wurde in die Zwischenablage kopiert", + "select-rulechain": "Regelkette auswählen", + "no-rulechains-matching": "Es wurden keine passenden Regelketten für '{{entity}}' gefunden.", + "rulechain-required": "Regelkette ist erforderlich", + "management": "Regelverwaltung", + "debug-mode": "Modus zur Fehlersuche" + }, + "rulenode": { + "details": "Details", + "events": "Ereignisse", + "search": "Knoten suchen", + "open-node-library": "Knotenbibliothek öffnen", + "add": "Neuen Regelknoten hinzufügen", + "name": "Name", + "name-required": "Name ist erforderlich.", + "type": "Typ", + "description": "Beschreibung", + "delete": "Regelknoten löschen", + "select-all-objects": "Alle Knoten und Verbindungen auswählen", + "deselect-all-objects": "Auswahl aller Knoten und Verbindungen aufheben", + "delete-selected-objects": "Ausgewählte Knoten und Verbindungen löschen", + "delete-selected": "Auswahl löschen", + "select-all": "Alle auswählen", + "copy-selected": "Auswahl kopieren", + "deselect-all": "Nichts auswählen", + "rulenode-details": "Details der Regelknoten", + "debug-mode": "Modus zur Fehlersuche", + "configuration": "Konfiguration", + "link": "Verbindung", + "link-details": "Verbindungsdetails der Regelknoten", + "add-link": "Verbindung hinzufügen", + "link-label": "Verbindungsbeschriftung", + "link-label-required": "Verbindungsbeschriftung ist erforderlich.", + "custom-link-label": "Benutzerdefinierte Verbindungsbeschriftung", + "custom-link-label-required": "Benutzerdefinierte Verbindungsbeschriftung ist erforderlich.", + "link-labels": "Verbindungsbeschriftungen", + "link-labels-required": "Verbindungsbeschriftungen sind erforderlich.", + "no-link-labels-found": "Keine Verbindungsbeschriftungen gefunden", + "no-link-label-matching": "'{{label}}' nicht gefunden.", + "create-new-link-label": "Bitte erstellen Sie eine neue Verbindungsbeschriftung!", + "type-filter": "Filter", + "type-filter-details": "Eingehende Nachrichten mit konfigurierten Bedingungen filtern", + "type-enrichment": "Anreicherung", + "type-enrichment-details": "Fügen Sie zusätzliche Informationen zu den Nachrichtenmetadaten hinzu", + "type-transformation": "Transformation", + "type-transformation-details": "Ändern Sie die Nutzerdaten und Metadaten der Nachricht", + "type-action": "Aktion", + "type-action-details": "Besondere Aktion ausführen", + "type-external": "Extern", + "type-external-details": "Interagiert mit externem System", + "type-rule-chain": "Regelkette", + "type-rule-chain-details": "Leitet eingehende Nachrichten an die angegebene Regelkette weiter", + "type-input": "Input", + "type-input-details": "Logische Eingabe der Regelkette, leitet eingehende Nachrichten an die nächste zugehörige Regelkette weiter", + "type-unknown": "Unbekannt", + "type-unknown-details": "Nicht aufgelöster Regelknoten", + "directive-is-not-loaded": "Definierte Konfigurationsanweisung '{{directiveName}}' ist nicht verfügbar.", + "ui-resources-load-error": "Fehler beim Laden der Konfigurations-UI-Ressourcen.", + "invalid-target-rulechain": "Zielregelkette kann nicht aufgelöst werden!", + "test-script-function": "Skriptfunktion testen", + "message": "Nachricht", + "message-type": "Nachrichtentyp", + "select-message-type": "Nachrichtentyp auswählen", + "message-type-required": "Nachrichtentyp ist erforderlich", + "metadata": "Metadaten", + "metadata-required": "Metadateneinträge dürfen nicht leer sein.", + "output": "Ausgabe", + "test": "Test", + "help": "Hilfe" + }, + "tenant": { + "tenant": "Mandant", + "tenants": "Mandanten", + "management": "Mandantenverwaltung", + "add": "Mandant hinzufügen", + "admins": "Administratoren", + "manage-tenant-admins": "Mandantenadministratoren verwalten", + "delete": "Mandant löschen", + "add-tenant-text": "Neuen Mandanten hinzufügen", + "no-tenants-text": "Keine Mandanten gefunden", + "tenant-details": "Mandantendetails", + "delete-tenant-title": "Möchten Sie den Mandanten '{{tenantTitle}}' wirklich löschen?", + "delete-tenant-text": "Vorsicht, nach Bestätigung werden der Mandant und alle zugehörigen Daten gelöscht.", + "delete-tenants-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Mandant} other {# Mandanten} } löschen möchten?", + "delete-tenants-action-title": "{ count, plural, 1 {1 Mandant} other {# Mandanten} } löschen", + "delete-tenants-text": "Vorsicht, nach Bestätigung werden alle ausgewählten Mandanten entfernt und alle zugehörigen Daten werden gelöscht.", + "title": "Titel", + "title-required": "Titel ist erforderlich.", + "description": "Beschreibung", + "details": "Details", + "events": "Ereignisse", + "copyId": "Mandanten-ID kopieren", + "idCopiedMessage": "Mandanten-ID wurde in die Zwischenablage kopiert", + "select-tenant": "Mandant auswählen", + "no-tenants-matching": "Es wurden keine passenden Mandanten für '{{entity}}' gefunden.", + "tenant-required": "Mandant ist erforderlich" + }, + "timeinterval": { + "seconds-interval": "{ seconds, plural, 1 {1 Sekunde} other {# Sekunden} }", + "minutes-interval": "{ minutes, plural, 1 {1 Minute} other {# Minuten} }", + "hours-interval": "{ hours, plural, 1 {1 Stunde} other {# Stunden} }", + "days-interval": "{ days, plural, 1 {1 Tag} other {# Tage} }", + "days": "Tage", + "hours": "Stunden", + "minutes": "Minuten", + "seconds": "Sekunden", + "advanced": "Erweitert" + }, + "timewindow": { + "days": "{ days, plural, 1 { Tag } other {# Tage } }", + "hours": "{ hours, plural, 0 { Stunde } 1 {1 Stunde } other {# Stunden } }", + "minutes": "{ minutes, plural, 0 { Minute } 1 {1 Minute } other {# Minuten } }", + "seconds": "{ seconds, plural, 0 { Sekunde } 1 {1 Sekunde } other {# Sekunden } }", + "realtime": "Echtzeit", + "history": "Historie", + "last-prefix": "letzte", + "period": "von {{ startTime }} bis {{ endTime }}", + "edit": "Zeitfenster bearbeiten", + "date-range": "Datumsbereich", + "last": "Letzte", + "time-period": "Zeitfenster" + }, + "user": { + "user": "User", + "users": "Users", + "customer-users": "Kunden Users", + "tenant-admins": "Mandanten-Administratoren", + "sys-admin": "System-Administrator", + "tenant-admin": "Mandanten-Administrator", + "customer": "Kunde", + "anonymous": "Anonym", + "add": "Benutzer hinzufügen", + "delete": "Benutzer löschen", + "add-user-text": "Neuen Benutzer hinzufügen", + "no-users-text": "Keine Benutzer gefunden", + "user-details": "Benutzer-Details", + "delete-user-title": "Möchten Sie den Benutzer '{{userEmail}}' wirklich löschen?", + "delete-user-text": "Vorsicht, nach Bestätigung werden der Benutzer und alle zugehörigen Daten gelöscht.", + "delete-users-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Benutzer} other {# Benutzer} } löschen möchten??", + "delete-users-action-title": "{ count, plural, 1 {1 Benutzer} other {# Benutzer} } löschen", + "delete-users-text": "Vorsicht, nach Bestätigung werden alle ausgewählten Benutzer entfernt und alle zugehörigen Daten werden gelöscht.", + "activation-email-sent-message": "Aktivierungs E-Mail wurde erfolgreich gesendet!", + "resend-activation": "Aktivierung erneut senden", + "email": "E-Mail", + "email-required": "E-Mail ist erforderlich.", + "invalid-email-format": "Ungültiges E-Mail Format.", + "first-name": "Vorname", + "last-name": "Nachname", + "description": "Beschreibung", + "default-dashboard": "Standard-Dashboard", + "always-fullscreen": "Immer Vollbild", + "select-user": "Benutzer auswählen", + "no-users-matching": "Keine passenden Benutzer für '{{entity}}' gefunden.", + "user-required": "Benutzer ist erforderlich", + "activation-method": "Aktivierungsmethode", + "display-activation-link": "Aktivierungslink anzeigen", + "send-activation-mail": "Aktivierungs E-Mail senden", + "activation-link": "Link zur Benutzer-Aktivierung", + "activation-link-text": "Um den Benutzer zu aktivieren, verwenden Sie bitte folgenden Aktivierungslink:", + "copy-activation-link": "Aktivierungslink kopieren", + "activation-link-copied-message": "Der Link zur Benutzer-Aktivierung wurde in die Zwischenablage kopiert ", + "details": "Details", + "login-as-tenant-admin": "Als Mandanten-Administrator anmelden", + "login-as-customer-user": "Als Kunden-Benutzer anmelden", + "disable-account": "Benutzerkonto deaktivieren", + "enable-account": "Benutzerkonto aktivieren", + "enable-account-message": "Benutzerkonto wurde erfolgreich aktiviert!", + "disable-account-message": "Benutzerkonto wurde erfolgreich deaktiviert!" + }, + "value": { + "type": "Wertetyp", + "string": "Text", + "string-value": "Textwert", + "integer": "Ganzzahlig", + "integer-value": "Ganzzahliger Wert", + "invalid-integer-value": "Ungültiger ganzzahliger Wert", + "double": "Gleitkommazahl", + "double-value": "Gleitkomma Wert", + "boolean": "Binär", + "boolean-value": "Binärwert", + "false": "Falsch", + "true": "Wahr", + "long": "Lang" + }, + "widget": { + "widget-library": "Widget-Bibliothek", + "widget-bundle": "Widget-Paket", + "select-widgets-bundle": "Widget-Paket auswählen", + "management": "Widget Verwaltung", + "editor": "Widget Editor", + "widget-type-not-found": "Problem beim Laden der Widget-Konfiguration.
    Zugeordneter Widget-Typ wurde entfernt.", + "widget-type-load-error": "Widget wurde aufgrund der folgenden Fehler nicht geladen:", + "remove": "Widget entfernen", + "edit": "Widget bearbeiten", + "remove-widget-title": "Möchten Sie das Widget '{{widgetTitle}}' wirklich entfernen?", + "remove-widget-text": "Nach der Bestätigung werden das Widget und alle zugehörigen Daten nicht wiederhergestellt.", + "timeseries": "Zeitreihe", + "search-data": "Daten suchen", + "no-data-found": "Keine Daten gefunden", + "latest-values": "Neueste Werte", + "rpc": "Steuerungswidget", + "alarm": "Alarm-Widget", + "static": "Statisches Widget", + "select-widget-type": "Widget-Typ auswählen", + "missing-widget-title-error": "Widget-Titel muss angegeben werden!", + "widget-saved": "Widget gespeichert", + "unable-to-save-widget-error": "Das Widget kann nicht gespeichert werden! Fehlermeldung!", + "save": "Widget speichern", + "saveAs": "Widget speichern unter", + "save-widget-type-as": "Widget-Typ speichern unter", + "save-widget-type-as-text": "Bitte geben Sie den neuen Widget-Titel ein und/oder wählen Sie das Ziel-Widget-Paket aus", + "toggle-fullscreen": "Vollbild umschalten", + "run": "Widget ausführen", + "title": "Widget-Titel", + "title-required": "Widget-Titel ist erforderlich.", + "type": "Widget-Typ", + "resources": "Ressourcen", + "resource-url": "JavaScript/CSS URL", + "remove-resource": "Ressource entfernen", + "add-resource": "Ressource hinzufügen", + "html": "HTML", + "tidy": "Aufgeräumt", + "css": "CSS", + "settings-schema": "Einstellungsschema", + "datakey-settings-schema": "Datenschlüssel-Einstellungsschema", + "javascript": "Javascript", + "remove-widget-type-title": "Möchten Sie den Widget-Typ '{{widgetName}}' wirklich entfernen?", + "remove-widget-type-text": "Nach der Bestätigung werden der Widget-Typ und alle zugehörigen Daten nicht wiederhergestellt.", + "remove-widget-type": "Widget-Typ entfernen", + "add-widget-type": "Neuen Widget-Typ hinzufügen", + "widget-type-load-failed-error": "Widget-Typ konnte nicht geladen werden!", + "widget-template-load-failed-error": "Widget-Vorlage konnte nicht geladen werden!", + "add": "Widget hinzufügen", + "undo": "Widget-Änderungen widerrufen ", + "export": "Widget exportieren" + }, + "widget-action": { + "header-button": "Widget-Header-Schaltfläche", + "open-dashboard-state": "Zum neuen Dashboard-Status navigieren", + "update-dashboard-state": "Aktuellen Dashboard-Status aktualisieren", + "open-dashboard": "Zu einem anderen Dashboard navigieren", + "custom": "Benutzerdefinierte Aktion", + "target-dashboard-state": "Zielstatus des Dashboards", + "target-dashboard-state-required": "Der Zielstatus ist erforderlich", + "set-entity-from-widget": "Widget-Entität festlegen", + "target-dashboard": "Ziel-Dashboard", + "open-right-layout": "Das rechte Dashboard-Layout öffnen (mobile Ansicht)" + }, + "widgets-bundle": { + "current": "Aktuelles Paket", + "widgets-bundles": "Widget-Pakete", + "add": "Widget-Pakete hinzufügen", + "delete": "Widget-Pakete löschen", + "title": "Titel", + "title-required": "Titel ist erforderlich.", + "add-widgets-bundle-text": "Neues Widget-Paket hinzufügen", + "no-widgets-bundles-text": "Keine Widget-Pakete gefunden", + "empty": "Widget-Paket ist leer ", + "details": "Details", + "widgets-bundle-details": "Widget-Paket-Details", + "delete-widgets-bundle-title": "Möchten Sie das Widget-Paket '{{widgetsBundleTitle}}' wirklich löschen? ", + "delete-widgets-bundle-text": "Seien Sie vorsichtig, nach der Bestätigung werden das Widget-Paket und alle zugehörigen Daten gelöscht.", + "delete-widgets-bundles-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Widget-Paket} other {# Widget-Pakete} } löschen möchten?", + "delete-widgets-bundles-action-title": "{ count, plural, 1 {1 Widgets-Paket} other {# Widget-Pakete} } löschen", + "delete-widgets-bundles-text": "Vorsicht, nach Bestätigung werden alle ausgewählten Widget-Pakete entfernt und alle zugehörigen Daten werden gelöscht.", + "no-widgets-bundles-matching": "Keine passenden Widget-Pakete '{{widgetsBundle}}' gefunden.", + "widgets-bundle-required": "Widget-Paket ist erforderlich.", + "system": "System", + "import": "Widget-Paket importieren", + "export": "Widget-Paket exportieren", + "export-failed-error": "Widget-Paket kann nicht exportiert werden: {{error}}", + "create-new-widgets-bundle": "Neues Widget-Paket erstellen", + "widgets-bundle-file": "Widget-Paket-Datei", + "invalid-widgets-bundle-file-error": "Widget-Paket kann nicht importiert werden: Ungültige Widget-Paket-Datenstruktur." + }, + "widget-config": { + "data": "Daten", + "settings": "Einstellungen", + "advanced": "Erweitert ", + "title": "Titel", + "title-tooltip": "Titel Tooltip", + "general-settings": "Allgemeine Einstellungen", + "display-title": "Titel anzeigen", + "drop-shadow": "Schlagschatten", + "enable-fullscreen": "Vollbild aktivieren", + "background-color": "Hintergrundfarbe", + "text-color": "Textfarbe", + "padding": "Pufferung", + "margin": "Rand", + "widget-style": "Widget-Stil", + "title-style": "Titel-Stil", + "mobile-mode-settings": "Einstellungen für den mobilen Modus", + "order": "Reihenfolge", + "height": "Größe", + "units": "Spezielles Symbol, das neben dem Wert angezeigt wird", + "decimals": "Anzahl der Stellen nach dem Fließkomma", + "timewindow": "Zeitfenster", + "use-dashboard-timewindow": "Dashboard-Zeitfenster verwenden", + "display-timewindow": "Zeitfenster anzeigen", + "display-legend": "Legende anzeigen", + "datasources": "Datenquellen", + "maximum-datasources": "Maximal { count, plural, 1 {1 Datenquelle ist erlaubt} other {# Datenquellen sind erlaubt} }.", + "datasource-type": "Typ", + "datasource-parameters": "Parameter", + "remove-datasource": "Datenquelle entfernen", + "add-datasource": "Datenquelle hinzufügen ", + "target-device": "Zielgerät", + "alarm-source": "Alarmquelle", + "actions": "Aktionen", + "action": "Aktion", + "add-action": "Aktion hinzufügen", + "search-actions": "Aktion suchen", + "action-source": "Aktionsquelle", + "action-source-required": "Aktionsquelle ist erforderlich.", + "action-name": "Name", + "action-name-required": "Aktionsname ist erforderlich.", + "action-name-not-unique": "Eine andere Aktion mit demselben Namen ist bereits vorhanden.
    Der Aktionsname sollte innerhalb derselben Aktionsquelle eindeutig sein.", + "action-icon": "Symbol ", + "action-type": "Art", + "action-type-required": "Aktionsart ist erforderlich.", + "edit-action": "Aktion bearbeiten", + "delete-action": "Aktion löschen", + "delete-action-title": "Widget-Aktion löschen", + "delete-action-text": "Möchten Sie die Widget-Aktion mit Namen '{{actionName}}' wirklich löschen?" + }, + "widget-type": { + "import": "Widget-Typ importieren", + "export": "Widget-Typ exportieren", + "export-failed-error": "Widget-Typ kann nicht exportiert werden: {{error}}", + "create-new-widget-type": "Neuen Widget-Typ erstellen", + "widget-type-file": "Widget-Typdatei", + "invalid-widget-type-file-error": "Widget-Typ kann nicht importiert werden: Ungültige Datenstruktur des Widget-Typs." + }, + "widgets": { + "date-range-navigator": { + "localizationMap": { + "Sun": "So.", + "Mon": "Mo.", + "Tue": "Di.", + "Wed": "Mi.", + "Thu": "Do.", + "Fri": "Fr.", + "Sat": "Sa.", + "Jan": "Jan.", + "Feb": "Feb.", + "Mar": "März", + "Apr": "Apr.", + "May": "Mai", + "Jun": "Juni", + "Jul": "Juli", + "Aug": "Aug.", + "Sep": "Sep.", + "Oct": "Okt.", + "Nov": "Nov.", + "Dec": "Dez.", + "January": "Januar", + "February": "Februar", + "March": "März", + "April": "April", + "June": "Juni", + "July": "Juli", + "August": "August", + "September": "September", + "October": "Oktober", + "November": "November", + "December": "Dezember", + "Custom Date Range": "Benutzerdefinierter Datumsbereich", + "Date Range Template": "Datumsbereichsvorlage", + "Today": "Heute", + "Yesterday": "Gestern", + "This Week": "Diese Woche", + "Last Week": "Letzte Woche", + "This Month": "Diesen Monat", + "Last Month": "Im vergangenen Monat", + "Year": "Jahr", + "This Year": "Dieses Jahr", + "Last Year": "Vergangenes Jahr", + "Date picker": "Datumsauswahl", + "Hour": "Stunde", + "Day": "Tag", + "Week": "Woche", + "2 weeks": "2 Wochen", + "Month": "Monat", + "3 months": "3 Monate", + "6 months": "6 Monate", + "Custom interval": "Benutzerdefiniertes Intervall", + "Interval": "Intervall", + "Step size": "Schrittlänge", + "Ok": "Ok" + } + }, + "input-widgets": { + "attribute-not-allowed": "Attributparameter können in diesem Widget nicht verwendet werden", + "date": "Datum", + "discard-changes": "Änderungen verwerfen", + "entity-attribute-required": "Entitätsattribut ist erforderlich", + "entity-timeseries-required": "Zeitreihen für Entitäten sind erforderlich", + "not-allowed-entity": "Die ausgewählte Entität kann keine gemeinsamen Attribute haben", + "no-attribute-selected": "Es ist kein Attribut ausgewählt", + "no-datakey-selected": "Es ist kein Datenschlüssel ausgewählt", + "no-entity-selected": "Keine Entität ausgewählt", + "no-image": "Kein Bild", + "no-support-web-camera": "Keine unterstützte Webcam", + "no-timeseries-selected": "Keine Zeitreihen ausgewählt", + "switch-attribute-value": "Entitätsattributwert wechseln", + "switch-camera": "Kamera wechseln", + "switch-timeseries-value": "Wert für Zeitreihen von Entitäten wechseln", + "take-photo": "Foto machen", + "time": "Zeit", + "timeseries-not-allowed": "Der Timeseries-Parameter kann in diesem Widget nicht verwendet werden", + "update-failed": "Aktualisierung fehlgeschlagen", + "update-successful": "Aktualisierung erfolgreich", + "update-attribute": "Attribut aktualisieren", + "update-timeseries": "Zeitreihen aktualisieren", + "value": "Wert" + } + }, + "icon": { + "icon": "Symbol", + "select-icon": "Symbol auswählen", + "material-icons": "Material-Symbole", + "show-all": "Alle Symbole anzeigen" + }, + "custom": { + "widget-action": { + "action-cell-button": "Aktionszellenschaltfläche", + "row-click": "Klick auf Zeile", + "polygon-click": "Klick auf Polygon", + "marker-click": "Klick auf Marker", + "tooltip-tag-action": "Tooltip-Tag-Aktion", + "node-selected": "Klick auf Node", + "element-click": "Klick auf HTML element", + "pie-slice-click": "Klicken auf Slice", + "row-double-click": "Doppelklicken auf Zeile" + } + }, + "language": { + "language": "Sprache" + } +} diff --git a/ui-ngx/src/assets/locale/locale.constant-el_GR.json b/ui-ngx/src/assets/locale/locale.constant-el_GR.json new file mode 100644 index 0000000000..2df1635fd2 --- /dev/null +++ b/ui-ngx/src/assets/locale/locale.constant-el_GR.json @@ -0,0 +1,2609 @@ +{ + "access": { + "unauthorized": "Χωρίς δικαιώματα πρόσβασης", + "unauthorized-access": "Μη εξουσιοδοτημένη πρόσβαση", + "unauthorized-access-text": "Θα πρέπει να συνδεθείτε για να έχετε πρόσβαση σε αυτόν τον πόρο!", + "access-forbidden": "Απαγορεύεται η πρόσβαση!", + "access-forbidden-text": "Δεν έχετε δικαιώματα πρόσβασης σε αυτήν την τοποθεσία!
    Συνδεθείτε με διαφορετικό όνομα χρήστη, αν εξακολουθείτε να θέλετε να έχετε πρόσβαση σε αυτήν την τοποθεσία.", + "refresh-token-expired": "Η περίοδος χρήσης έχει λήξει", + "refresh-token-failed": "Δεν είναι δυνατή η ανανέωση της περιόδου χρήσης", + "permission-denied": "Άρνηση πρόσβασης!", + "permission-denied-text": "Δεν έχετε δικαίωμα εκτέλεσης αυτής της λειτουργίας!" + }, + "action": { + "activate": "Ενεργοποίηση", + "suspend": "Αναστολή", + "save": "Αποθήκευση", + "saveAs": "Αποθήκευση ως", + "cancel": "Ακύρωση", + "ok": "OK", + "delete": "Διαγραφή", + "add": "Προσθήκη", + "yes": "Ναι", + "no": "Όχι", + "update": "Ενημέρωση", + "remove": "Διαγραφή", + "search": "Αναζήτηση", + "clear-search": "Καθαρισμός Αναζήτησης", + "assign": "Ανάθεση", + "unassign": "Ακύρωση Ανάθεσης", + "share": "Διαμοιρασμός", + "make-private": "Ιδιωτικό", + "apply": "Εφαρμογή", + "apply-changes": "Εφααρμογή Αλλαγών", + "edit-mode": "Λειτουργία Επεξεργασίας", + "enter-edit-mode": "Έναρξη Επεξεργασίας", + "decline-changes": "Απόρριψη Αλλαγών", + "close": "Κλείσιμο", + "back": "Πίσω", + "run": "Εκτέλεση", + "sign-in": "Σύνδεση!", + "edit": "Επεξεργασία", + "view": "Επισκόπηση", + "create": "Δημιουργία", + "drag": "Drag", + "refresh": "Ανανέωση", + "undo": "Αναίρεση", + "copy": "Αντιγραφή", + "paste": "Επικόλληση", + "copy-reference": "Αντιγραφή παραπομπής", + "paste-reference": "Επικόλληση παραπομπής", + "import": "Εισαγωγή", + "export": "Εξαγωγή", + "share-via": "Διαμοίραση μέσω {{provider}}", + "move": "Μετακίνηση", + "select": "Επιλογή", + "continue": "Συνέχεια" + }, + "aggregation": { + "aggregation": "Συνάθροιση", + "function": "Συνάρτηση συνάθροισης δεδομένων", + "limit": "Μέγιστες τιμές", + "group-interval": "Διάστημα ομαδοποίησης", + "min": "Min", + "max": "Max", + "avg": "Μέσος Όρος", + "sum": "Άθροισμα", + "count": "Καταμέτρηση", + "none": "Κανένα" + }, + "admin": { + "general": "Γενικά", + "general-settings": "Γενικές Ρυθμίσεις", + "outgoing-mail": "Διακομιστής Αλληλογραφίας", + "outgoing-mail-settings": "Διακομιστής Εξερχόμενης Αλληλογραφίας", + "system-settings": "Ρυθμίσεις Συστήματος", + "test-mail-sent": "Το δοκιμαστικό μήνυμα στάλθηκε με επιτυχία!", + "base-url": "Βασική διεύθυνση URL", + "base-url-required": "Απαιτείται ορισμός Base URL.", + "mail-from": "Αποστολέας", + "mail-from-required": "Απαιτείται βασική διεύθυνση URL.", + "smtp-protocol": "Πρωτόκολλο SMTP", + "smtp-host": "SMTP host", + "smtp-host-required": "Απαιτείται ορισμός SMTP host.", + "smtp-port": "Θύρα SMTP", + "smtp-port-required": "Πρέπει να εισάγετε SMTP port.", + "smtp-port-invalid": "Αυτή δε φαίνεται να είναι μια έγκυρη SMTP port.", + "timeout-msec": "Timeout (msec)", + "timeout-required": "Απαιτείται τιμή Timeout.", + "timeout-invalid": "Αυτή δε φαίνεται να είναι μια έγκυρη τιμή timeout.", + "enable-tls": "Ενεργοποίηση TLS", + "tls-version": "Έκδοση TLS", + "send-test-mail": "Αποστολή δοκιμαστικού μηνύματος", + "use-system-mail-settings": "Χρήση των ρυθμίσεων διακομιστή αλληλογραφίας συστήματος", + "mail-templates": "Πρότυπα αλληλογραφίας", + "mail-template-settings": "Ρυθμίσεις προτύπων αλληλογραφίας", + "use-system-mail-template-settings": "Χρήση προτύπων ηλεκτρονικού ταχυδρομείου συστήματος", + "mail-template": { + "mail-template": "Πρότυπο αλληλογραφίας", + "test": "Δοκιμαστικό μήνυμα ηλεκτρονικού ταχυδρομείου", + "activation": "Μήνυμα ενεργοποίησης λογαριασμού", + "account-activated": "Μήνυμα επιτυχούς ενεργοποίησης λογαριασμού", + "reset-password": "Μήνυμα επαναφοράς κωδικού πρόσβασης", + "password-was-reset": "Μήνυμα επαναφοράς κωδικού πρόσβασης", + "user-activated": "Μήνυμα ενεργοποίησης χρήστη", + "user-registered": "Μήνυμα καταχώρησης χρήστη" + }, + "mail-subject": "Θέμα μηνύματος (subject)", + "mail-body": "Κυρίως μήνυμα (body)" + }, + "alarm": { + "alarm": "Alarm", + "alarms": "Alarms", + "select-alarm": "Επιλογή alarm", + "no-alarms-matching": "Δεν βρέθηκαν alarms σχετικά με '{{entity}}'.", + "alarm-required": "Απαιτείται Alarm", + "alarm-status": "Κατάσταση Alarm", + "search-status": { + "ANY": "'Ολες", + "ACTIVE": "Ενεργό", + "CLEARED": "Εκκαθαρίστηκε", + "ACK": "Επιβεβαιώθηκε", + "UNACK": "Χωρίς επιβεβαίωση" + }, + "display-status": { + "ACTIVE_UNACK": "Ενεργό χωρίς επιβεβαίωση", + "ACTIVE_ACK": "Ενεργό επιβεβαιωμένο", + "CLEARED_UNACK": "Εκκαθαρίστηκε χωρίς επιβεβαίωση", + "CLEARED_ACK": "Εκκαθαρίστηκε επιβεβαιωμένο" + }, + "no-alarms-prompt": "Δεν βρέθηκαν alarm", + "created-time": "Ώρα δημιουργίας", + "type": "Τύπος", + "severity": "Βαρύτητα", + "originator": "Δημιουργός", + "originator-type": "Τύπος δημιουργού", + "details": "Λεπτομέρειες", + "status": "Κατάσταση", + "alarm-details": "Λεπτομέρειες Alarm", + "start-time": "Ώρα έναρξης", + "end-time": "Ώρα λήξης", + "ack-time": "Ώρα επιβεβαίωσης", + "clear-time": "Ώρα εκκαθάρισης", + "severity-critical": "Critical", + "severity-major": "Κρίσιμο", + "severity-minor": "Ασήμαντο", + "severity-warning": "Προειδοποίηση", + "severity-indeterminate": "Απροσδιόριστο", + "acknowledge": "Επιβεβαίωση", + "clear": "Εκκαθάριση", + "search": "Αναζήτηση alarm", + "selected-alarms": "{ count, plural, 1 {1 alarm} other {# alarms} } επιλέχθηκαν", + "no-data": "Δεν υπάρχουν δεδομένα για εμφάνιση", + "polling-interval": "Διάστημα δειγματοληψίας alarm(sec)", + "polling-interval-required": "Απαιτείται ορισμός διαστήματος δειγματοληψίας alarm.", + "min-polling-interval-message": "Η ελάχιστη επιτρεπόμενη τιμή διαστήματος δειγματοληψίας alarm είναι 1 sec.", + "aknowledge-alarms-title": "Επιβεβαίωση { count, plural, 1 {1 alarm} other {# alarms} }", + "aknowledge-alarms-text": "Είστε σίγουρος ότι θέλετε να επιβεβαιώσετε { count, plural, 1 {1 alarm} other {# alarms} };", + "aknowledge-alarm-title": "Επιβεβαίωση Alarm", + "aknowledge-alarm-text": "Είστε σίγουρος ότι θέλετε να επιβεβαιώσετε το Alarm?", + "clear-alarms-title": "Εκκαθάριση { count, plural, 1 {1 alarm} other {# alarms} }", + "clear-alarms-text": "Είστε σίγουρος ότι θέλετε να εκκαθαρίσετε { count, plural, 1 {1 alarm} other {# alarms} }?", + "clear-alarm-title": "Εκκαθάριση Alarm", + "clear-alarm-text": "Είστε σίγουρος ότι θέλετε να εκκαθαρίσετε το Alarm?", + "alarm-status-filter": "Φίλτρο κατάστασης Alarm" + }, + "alias": { + "add": "Προσθήκη ψευδωνύμου", + "edit": "Επεξεργασία ψευδωνύμου", + "name": "Ψευδώνυμο", + "name-required": "Απαιτείται Ψευδώνυμο", + "duplicate-alias": "Το ψευδώνυμο υπάρχει ήδη.", + "filter-type-single-entity": "Απλή Οντότητα", + "filter-type-entity-group": "Ομάδα Οντοτήτων", + "filter-type-entity-list": "Λίστα Οντοτήτων", + "filter-type-entity-name": "Όνομα Οντότητας", + "filter-type-entity-group-list": "Λίστα ομάδας Οντοτήτων", + "filter-type-entity-group-name": "Όνομα ομάδας Οντοτήτων", + "filter-type-state-entity": "Οντότητα από την κατάσταση του dashboard", + "filter-type-state-entity-description": "Οντότητα που λαμβάνεται από τις παραμέτρους κατάστασης του dashboard", + "filter-type-asset-type": "Τύπος Asset", + "filter-type-asset-type-description": "Assets του τύπου '{{assetType}}'", + "filter-type-asset-type-and-name-description": "Assets του τύπου '{{assetType}}' με όνομα που αρχίζει από '{{prefix}}'", + "filter-type-device-type": "Τύπος Συσκευής", + "filter-type-device-type-description": "Συσκευές του τύπου '{{deviceType}}'", + "filter-type-device-type-and-name-description": "Συσκευές του τύπου '{{deviceType}}' με όνομα που αρχίζει από '{{prefix}}'", + "filter-type-entity-view-type": "Τύπος προβολής Οντοτήτων", + "filter-type-entity-view-type-description": "Τύπος προβολής Οντοτήτων '{{entityView}}'", + "filter-type-entity-view-type-and-name-description": "Τύπος προβολής Οντοτήτων '{{entityView}}' με όνομα που αρχίζει από '{{prefix}}'", + "filter-type-relations-query": "Ερώτημα Σχέσεων", + "filter-type-relations-query-description": "{{entities}} που έχουν σχέση {{relationType}} {{direction}} {{rootEntity}}", + "filter-type-asset-search-query": "Ερώτημα αναζήτησης Asset", + "filter-type-asset-search-query-description": "Asset με τύπο {{assetTypes}} που έχουν σχέση {{relationType}} {{direction}} {{rootEntity}}", + "filter-type-device-search-query": "Ερώτημα αναζήτησης Συσκευών", + "filter-type-device-search-query-description": "Συσκευές με τύπο {{deviceTypes}} που έχουν σχέση {{relationType}} {{direction}} {{rootEntity}}", + "filter-type-entity-view-search-query": "Ερώτημα αναζήτησης Όψεων Οντοτήτων", + "filter-type-entity-view-search-query-description": "Όψεις Οντοτήτων με τύπο {{entityViewTypes}} που έχουν σχέση {{relationType}} {{direction}} {{rootEntity}}", + "entity-filter": "Φίλτρο Οντοτήτων", + "resolve-multiple": "Διακανονισμός ως πολλαπλές οντότητες", + "filter-type": "Είδος Φίλτρου", + "filter-type-required": "Απαιτείται καθορισμός του Είδους Φίλτρου.", + "entity-filter-no-entity-matched": "Δεν βρέθηκαν οντότητες που ταιριάζουν με το συγκεκριμένο φίλτρο.", + "no-entity-filter-specified": "Δεν έχει οριστεί Φίλτρο Οντοτήτων", + "root-state-entity": "Χρήση της οντότητας κατάστασης του dashboard ως ρίζα", + "group-state-entity": "Χρήση της οντότητας κατάστασης του dashboard ως ομάδας οντοτήτων", + "root-entity": "Οντότητα ρίζας", + "state-entity-parameter-name": "Όνομα παραμέτρου οντότητας κατάστασης", + "default-state-entity": "Προεπιλεγμένη οντότητα κατάστασης", + "default-state-entity-group": "Προεπιλεγμένη ομάδα καταστάσεων οντοτήτων", + "default-entity-parameter-name": "Εξ ορισμού", + "max-relation-level": "Μέγιστος βαθμός συσχέτισης", + "unlimited-level": "Απεριόριστος βαθμός", + "state-entity": "Οντότητα κατάστασης του Dashboard", + "entities-of-group-state-entity": "Οντότητες από την ομάδα οντοτήτων κατάστασης του dashboard", + "all-entities": "Όλες οι Οντότητες", + "any-relation": "Οποιαδήποτε" + }, + "asset": { + "asset": "Asset", + "assets": "Assets", + "management": "Διαχείριση Asset", + "view-assets": "Προβολή Asset", + "add": "Προσθήκη Asset", + "assign-to-customer": "Ανάθεση σε Πελάτη", + "assign-asset-to-customer": "Ανάθεση Asset(s) σε Πελάτη", + "assign-asset-to-customer-text": "Επιλέξτε τα Asset που θα αναθέσετε στον πελάτη", + "no-assets-text": "Δεν βρέθηκαν Asset", + "assign-to-customer-text": "Παρακαλώ επιλέξτε πελάτη για να αναθέσετε το Asset(s)", + "public": "Δημόσιο", + "assignedToCustomer": "Συνδεδεμένο με πελάτη", + "make-public": "Κάνε το Asset δημόσιο", + "make-private": "Κάνε το Asset ιδιωτικό", + "unassign-from-customer": "Αποσύνδεση από τον πελάτη", + "delete": "Διαγραφή Αsset", + "asset-public": "Το Asset είναι δημόσιο", + "asset-type": "Τύπος Asset", + "asset-type-required": "Απαιτείται ορισμός είδους Asset.", + "select-asset-type": "Επιλογή τύπου Αsset", + "enter-asset-type": "Εισαγωγή τύπου Αsset", + "any-asset": "Οποιοδήποτε Αsset", + "no-asset-types-matching": "Δεν βρέθηκαν τύποι Asset που να τιαιριάζουν με '{{entitySubtype}}'.", + "asset-type-list-empty": "Δεν επιλέχθηκε τύπος Asset.", + "asset-types": "Τύποι Asset", + "name": "Όνομα", + "name-required": "Απαιτείται Όνομα.", + "description": "Περιγραφή", + "type": "Τύπος", + "type-required": "Απαιτείται Τύπος.", + "details": "Λεπτομέρειες", + "events": "Γεγονότα", + "add-asset-text": "Προσθήκη νέου Asset", + "asset-details": "Λεπτομέρειες Asset", + "assign-assets": "Ανάθεση Assets", + "assign-assets-text": "Ανάθεση { count, plural, 1 {1 asset} other {# assets} } σε πελάτη", + "delete-assets": "Διαγραφή Assets", + "unassign-assets": "Αποσύνδεση Assets", + "unassign-assets-action-title": "Αποσύνδεση { count, plural, 1 {1 asset} other {# assets} } από πελάτη", + "assign-new-asset": "Ανάθεση νέου Asset", + "delete-asset-title": "Είστε βέβαιοι ότι θέλετε να διαγράψετε το Asset '{{assetName}}'?", + "delete-asset-text": "Προσοχή!, Μετά την επιβεβαίωση, το Asset και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-assets-title": "Είστε βέβαιοι ότι θέλετε να διαγράψετε { count, plural, 1 {1 asset} other {# assets} };", + "delete-assets-action-title": "Διαγραφή { count, plural, 1 {1 asset} other {# assets} }", + "delete-assets-text": "Προσοχή! Μετά την επιβεβαίωση, όλα τα επιλεγμένα Asset και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "make-public-asset-title": "Είστε βέβαιοι ότι θέλετε να κάνετε το Asset '{{assetName}}' δημόσιο;", + "make-public-asset-text": "Μετά την επιβεβαίωση, το Asset και όλα τα στοιχεία του θα είναι προσβάσιμα από άλλους.", + "make-private-asset-title": "Είστε βέβαιοι ότι θέλετε να κάνετε το Asset '{{assetName}}' ιδιωτικό;", + "make-private-asset-text": "Μετά την επιβεβαίωση, το Asset και όλα τα στοιχεία του θα είναι ιδιωτικά και μη προσβάσιμα από άλλους.", + "unassign-asset-title": "Είστε βέβαιοι ότι θέλετε να αποσυνδέσετε το Asset '{{assetName}}';", + "unassign-asset-text": "Μετά την επιβεβαίωση, το Asset θα αποσυνδεθεί και δεν θα είναι προσβάσιμο από τον πελάτη.", + "unassign-asset": "Αποσύνδεση Asset", + "unassign-assets-title": "Είστε βέβαιοι ότι θέλετε να αποσυνδέσετε { count, plural, 1 {1 asset} other {# assets} };", + "unassign-assets-text": "Μετά την επιβεβαίωση, όλα τα επιλεγμένα Asset θα αποσυνδεθούν και δεν θα είναι προσβάσιμα από τον πελάτη.", + "copyId": "Αντιγραφή Asset Id", + "idCopiedMessage": "Το Asset Id αντιγράφηκε στο πρόχειρο", + "select-asset": "Επιλογή Asset", + "no-assets-matching": "Δεν βρέθηκαν Asset που να ταιριάζουν με '{{entity}}'.", + "asset-required": "Απαιτείται Asset", + "name-starts-with": "Όνομα Asset που ξεκινάει με", + "selected-assets": "{ count, plural, 1 {1 asset} other {# assets} } επιλέχθηκαν", + "search": "Αναζήτηση Asset", + "select-group-to-add": "Επιλέξτε ομάδα στόχο για να προσθέσετε επιλεγμένα Asset", + "select-group-to-move": "Επιλέξτε ομάδα στόχο για να μετακινήσετε επιλεγμένα assets", + "remove-assets-from-group": "Είστε βέβαιοι ότι θέλετε να καταργήσετε { count, plural, 1 {1 asset} other {# assets} } από την ομάδα '{entityGroup}';", + "group": "Ομάδα Asset", + "list-of-groups": "{ count, plural, 1 {One asset group} other {List of # asset groups} }", + "group-name-starts-with": "Ομάδες Asset των οποίων τα ονόματα ξεκινούν με '{{prefix}}'", + "import": "Εισαγωγή Asset", + "asset-file": "Αρχείο Asset" + }, + "attribute": { + "attributes": "Χαρακτηριστικά", + "latest-telemetry": "Τελευταία τηλεμετρία", + "attributes-scope": "Πεδίο εφαρμογής Χαρακτηριστικών Οντότητας", + "scope-latest-telemetry": "Τελευταία τηλεμετρία", + "scope-client": "Χαρακτηριστικά Client", + "scope-server": "Χαρακτηριστικά Server", + "scope-shared": "Κοινόχρηστα Χαρακτηριστικά", + "add": "Προσθήκη Χαρακτηριστικού", + "add-attribute-prompt": "Προσθέστε Χαρακτηριστικό", + "key": "Όνομα", + "last-update-time": "Ώρα τελευταίας ενημέρωσης", + "key-required": "Απαιτείται μεταβλητή χαρακτηριστικού.", + "value": "Τιμή", + "value-required": "Απαιτείται ορισμός τιμής χαρακτηριστικού.", + "delete-attributes-title": "Είστε βέβαιοι ότι θέλετε να διαγράψετε { count, plural, 1 {1 attribute} other {# attributes} };", + "delete-attributes-text": "Προσοχή! Μετά την επιβεβαίωση θα αφαιρεθούν όλα τα επιλεγμένα χαρακτηριστικά.", + "delete-attributes": "Διαγραφή χαρακτηριστικών", + "enter-attribute-value": "Καταχωρίστε την τιμή του χαρακτηριστικού", + "show-on-widget": "Εμφάνιση σε widget", + "widget-mode": "Είδος λειτουργίας widget", + "next-widget": "Επόμενο widget", + "prev-widget": "Προηγούμενο widget", + "add-to-dashboard": "Προσθήκη σε dashboard", + "add-widget-to-dashboard": "Προσθήκη widget στο dashboard", + "selected-attributes": "{ count, plural, 1 {1 attribute} other {# attributes} } επιλέχθηκαν", + "selected-telemetry": "{ count, plural, 1 {1 telemetry unit} other {# telemetry units} } επιλέχθηκαν" + }, + "audit-log": { + "audit": "Καταγραφή", + "audit-logs": "Ημερολόγια Καταγραφής", + "timestamp": "Ώρα", + "entity-type": "Είδος Οντότητας", + "entity-name": "Όνομα Οντότητας", + "user": "Χρήστης", + "type": "Τύπος", + "status": "Κατάσταση", + "details": "Λεπτομέρειες", + "type-added": "Προστέθηκε", + "type-deleted": "Διαγράφηκε", + "type-updated": "Ενημερώθηκε", + "type-attributes-updated": "Τα χαρακτηριστικά ενημερώθηκαν", + "type-attributes-deleted": "Τα χαρακτηριστικά διαγράφηκαν", + "type-rpc-call": "Κλήση RPC", + "type-credentials-updated": "Τα διαπιστευτήρια ενημερώθηκαν", + "type-assigned-to-customer": "Ανατέθηκε σε Πελάτη", + "type-unassigned-from-customer": "Αποσυνδέθηκε από Πελάτη", + "type-activated": "Ενεργοποιήθηκε", + "type-suspended": "Μπήκε σε αναστολή", + "type-credentials-read": "Τα διαπιστευτήρια διαβάστηκαν", + "type-attributes-read": "Τα χαρακτηριστικά διαβάστηκαν", + "type-added-to-entity-group": "Προστέθηκε στην ομάδα", + "type-removed-from-entity-group": "Αφαιρέθηκε από την ομάδα", + "type-relation-add-or-update": "Relation updated", + "type-relation-delete": "Η συσχέτιση ενημερώθηκε", + "type-relations-delete": "Όλες οι συσχετίσεις διαγράφηκαν", + "type-alarm-ack": "Επιβεβαιώθηκε", + "type-alarm-clear": "Εκκαθαρίστηκε", + "type-rest-api-rule-engine-call": "Κλήση Rule engine REST API", + "type-made-public": "Έγινε δημόσιο", + "type-made-private": "Έγινε ιδιωτικό", + "status-success": "Επιτυχία", + "status-failure": "Αποτυχία", + "audit-log-details": "΄Λεπτομέρειες καταγραφής", + "no-audit-logs-prompt": "Δεν βρέθηκαν αρχεία καταγραφής", + "action-data": "Δεδομένα ενεργειών", + "failure-details": "Λεπτομέρειες αποτυχίας", + "search": "Αναζήτηση αρχείων καταγραφής", + "clear-search": "΄Καθαρισμός αναζήτησης" + }, + "confirm-on-exit": { + "message": "Έχετε μη αποθηκευμένες αλλαγές. Είστε βέβαιοι ότι θέλετε να φύγετε από τη σελίδα;", + "html-message": "Έχετε μη αποθηκευμένες αλλαγές.
    Είστε βέβαιοι ότι θέλετε να φύγετε από τη σελίδα;", + "title": "Μη αποθηκευμένες αλλαγές" + }, + "contact": { + "country": "Χώρα", + "city": "Πόλη", + "state": "Περιφέρεια", + "postal-code": "Ταχυδρομικός Κώδικας", + "postal-code-invalid": "Μη έγκυρη μορφή ταχυδρομικού κώδικα.", + "address": "Διεύθυνση (γραμμή 1)", + "address2": "Διεύθυνση (γραμμή 2)", + "phone": "Τηλέφωνο", + "email": "Email", + "no-address": "Χωρίς διεύθυνση" + }, + "common": { + "username": "Όνομα χρήστη", + "password": "Κωδικός πρόσβασης", + "enter-username": "Εισάγετε Όνομα χρήστη", + "enter-password": "Εισάγετε Κωδικό πρόσβασης", + "enter-search": "Αναζήτηση", + "created-time": "Δημιουργήθηκε" + }, + "converter": { + "converter": "Μετατροπέας δεδομένων", + "converters": "Μετατροπείς δεδομένων", + "select-converter": "Επιλογή μετατροπέα δεδομένων", + "no-converters-matching": "Δεν βρέθηκαν μετατροπείς δεδομένων για '{{entity}}'.", + "converter-required": "Απαιτείται ορισμός μετατροπέα δεδομένων", + "delete": "Διαγραφή μετατροπέα", + "management": "Διαχείριση μετατροπέων δεδομένων", + "add-converter-text": "Προσθήκη νέου μετατροπέα δεδομένων", + "no-converters-text": "Δεν βρέθηκαν μετατροπείς δεδομένων", + "selected-converters": "{ count, plural, 1 {1 data converter} other {# data converters} } επιλέχθηκαν", + "delete-converter-title": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τον μετατροπέα δεδομένων '{{converterName}}'?", + "delete-converter-text": "Προσέξτε, μετά την επιβεβαίωση, ο μετατροπέας δεδομένων και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-converters-title": "Είστε βέβαιοι ότι θέλετε να διαγράψετε { count, plural, 1 {1 data converter} other {# data converters} };", + "delete-converters-action-title": "Διαγραφή { count, plural, 1 {1 data converter} other {# data converters} }", + "delete-converters-text": "Προσοχή! Μετά την επιβεβαίωση, όλοι οι επιλεγμένοι μετατροπείς δεδομένων και όλα τα σχετικά δεδομένα θα καταστούν μη ανακτήσιμα.", + "events": "Γεγονότα", + "add": "Προσθήκη μετατροπέα δεδομένων", + "converter-details": "Λεπτομέρειες μετατροπέα δεδομένων", + "details": "Λεπτομέρειες", + "copyId": "Αντιγραφή ID μετατροπέα δεδομένων", + "idCopiedMessage": "Το ID του μετατροπέα δεδομένων αντιγράφηκε στο πρόχειρο.", + "debug-mode": "Λειτουργία εντοπισμού σφαλμάτων", + "name": "Όνομα", + "name-required": "Απαιτείται Όνομα.", + "description": "Περιγραφή", + "decoder": "Αποκωδικοποιητής", + "encoder": "Kωδικοποιητής", + "test-decoder-fuction": "Δοκιμή λειτουργίας αποκωδικοποιητή", + "test-encoder-fuction": "Δοκιμή λειτουργίας κωδικοποιητή", + "decoder-input-params": "Είσοδο παραμέτρων αποκωδικοποιητή", + "encoder-input-params": "Είσοδο παραμέτρων κωδικοποιητή", + "payload": "Φορτίο", + "payload-content-type": "Τύπος περιεχόμενου φορτίου", + "payload-content": "Περιεχόμενο φορτίο", + "message": "Μήνυμα", + "message-type": "Τύπος μηνύματος", + "message-type-required": "Απαιτείται τύπος μηνύματος", + "test": "Δοκιμή", + "metadata": "Μεταδεδομένα", + "metadata-required": "Οι καταχωρίσεις μεταδεδομένων δεν μπορούν να είναι κενές.", + "integration-metadata": "Μεταδεδομένα ενσωμάτωσης", + "integration-metadata-required": "Οι καταχωρήσεις μεταδεδομένων ενσωμάτωσης δεν μπορούν να είναι κενές.", + "output": "Έξοδος", + "import": "Εισαγωγή μετατροπέα", + "export": "Εξαγωγή μετατροπέα", + "export-failed-error": "Δεν είναι δυνατή η εξαγωγή του μετατροπέα: {{error}}", + "create-new-converter": "Δημιουργία νέου μετατροπέα", + "converter-file": "Αρχείο μετατροπέα", + "invalid-converter-file-error": "Δεν είναι δυνατή η εισαγωγή του μετατροπέα: Μη έγκυρη δομή δεδομένων μετατροπέα.", + "type": "Τύπος", + "type-required": "Απαιτείται τύπος.", + "type-uplink": "Uplink", + "type-downlink": "Downlink" + }, + "content-type": { + "json": "Json", + "text": "Text", + "binary": "Binary (Base64)" + }, + "customer": { + "customer": "Πελάτης", + "customers": "Πελάτες", + "management": "Διαχείριση Πελατών", + "dashboard": "Dashboard Πελάτη", + "dashboards": "Dashboards Πελάτη", + "devices": "Συσκευές Πελάτη", + "entity-views": "Προβολές Οντοτήτων ΠελάτηCustomer Entity Views", + "assets": "Assets Πελάτη", + "public-dashboards": "Δημόσια Dashboards", + "public-devices": "Δημόσιες Devices", + "public-assets": "Δημόσια Assets", + "public-entity-views": "Δημόσιες Προβολές Οντοτήτων", + "add": "Προσθήκη Πελάτη", + "delete": "Διαγραφή πελάτη", + "manage-customer-user-groups": "Διαχείριση ομάδων χρηστών πελατών", + "manage-customer-groups": "Διαχείριση ομάδων πελατών", + "manage-customer-device-groups": "Διαχείριση ομάδων συσκευών πελατών", + "manage-customer-asset-groups": "Διαχείριση ομάδων Asset πελατών", + "manage-customer-entity-view-groups": "Διαχείριση ομάδων προβολής οντότητας πελατών", + "manage-customer-dashboard-groups": "Διαχείριση ομάδων dashboard πελατών", + "manage-customer-users": "Διαχείριση χρηστών πελατών", + "manage-customers": "Διαχείριση πελατών", + "manage-customer-devices": "Διαχείρηση συσκευών πελατών", + "manage-customer-entity-views": "Διαχείριση προβολής οντότητας πελατών", + "manage-customer-dashboards": "Διαχείριση dashboards πελατών", + "manage-public-devices": "Διαχείριση δημόσιων συσκευών", + "manage-public-dashboards": "Διαχείριση δημόσιων συσκευών", + "manage-customer-assets": "Διαχείριση οντοτήτων πελατών", + "manage-public-assets": "Διαχείριση δημόσιων οντοτήτων", + "add-customer-text": "Προσθήκη νέου πελάτη", + "no-customers-text": "Δεν βρέθηκαν πελάτες", + "customer-details": "Λεπτομέρειες πελάτη", + "delete-customer-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε τον πελάτη '{{customerTitle}}'?", + "delete-customer-text": "Προσέξτε, μετά την επιβεβαίωση, ο πελάτης και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-customers-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 customer} other {# customers} }?", + "delete-customers-action-title": "Διαγραφή { count, plural, 1 {1 customer} other {# customers} }", + "delete-customers-text": "Προσέξτε, μετά την επιβεβαίωση, οι πελάτες και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "manage-user-groups": "Διαχείρηση ομαδών χρηστών", + "manage-asset-groups": "Διαχείρηση ομαδών οντοτήτων", + "manage-device-groups": "Διαχείρηση ομαδών συσκευών", + "manage-dashboard-groups": "Διαχείρηση ομαδών dashboards", + "manage-entity-view-groups": "Διαχείρηση ομαδών προβολής οντοτήτων", + "manage-users": "Διαχείρηση χρηστών", + "manage-assets": "Διαχείρηση οντοτήτων", + "manage-devices": "Διαχείρηση συσκευών", + "manage-dashboards": "Διαχείρηση dashboards", + "title": "Τίτλος", + "title-required": "Απαιτείται ένας τίτλος.", + "description": "Περιγραφή", + "details": "Λεπτομέρειες", + "events": "Γεγονότα", + "copyId": "Αντιγραφή ID πελάτη", + "idCopiedMessage": "Το ID του πελάτη έχει αντιγραφεί στο πρόχειρο", + "select-customer": "Επιλέξτε πελάτη", + "no-customers-matching": "Δεν βρέθηκαν πελάτες που να αντιστοιχούν σε '{{entity}}'.", + "customer-required": "Απαιτείται πελάτης", + "selected-customers": "{ count, plural, 1 {1 customer} other {# customers} } επιλέχθηκαν", + "search": "Αναζήτηση πελατών", + "select-group-to-add": "Επιλέξτε ομάδα για να προσθέσετε τους επιλεγμένους πελάτες", + "select-group-to-move": "Επιλέξτε ομάδα για να μετακινήσετε τους επιλεγμένους πελάτες", + "remove-customers-from-group": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε { count, plural, 1 {1 customer} other {# customers} } από την ομάδα '{entityGroup}'?", + "group": "Ομάδα πελατών", + "list-of-groups": "{ count, plural, 1 {One customer group} other {List of # customer groups} }", + "group-name-starts-with": "Πελάτες των οποίων το όνομα αρχίζει από '{{prefix}}'", + "select-default-customer": "Επιλογή προεπιλεγμένου πελάτη", + "default-customer": "Προεπιλεγμένος πελάτης", + "default-customer-required": "Ο προεπιλεγμένος πελάτης είναι υποχρεωτικός για να είναι δυνατή η απασφαλμάτωση του dashboard από τον Tenant", + "allow-white-labeling": "Επιτρέψτε το White Labeling" + }, + "customers-hierarchy": { + "customers-hierarchy": "Ιεραρχία Πελατών", + "open-nav-tree": "Άνοιγμα διακλάδωσης", + "return-to-top-level": "Επιστροφή στο αρχικό επίπεδο" + }, + "custom-menu": { + "custom-menu": "Προσαρμοσμένο Μενού", + "custom-menu-hint": "Ορίστε το προσαρμοσμένο μενού (JSON) παρακάτω. Αυτό το JSON περιέχει μια λίστα με προσαρμοσμένα στοιχεία μενού." + }, + "custom-translation": { + "custom-translation": "Μεταφράσεις", + "translation-map": "Χάρτης μετάφρασης", + "key": "Κλειδί", + "import": "Εισαγωγή μετάφρασης", + "export": "Εξαγωγή μετάφρασης", + "export-data": "Εξαγωγή δεδομένων μετάφρασης", + "import-data": "Εισαγωγή δεδομένων μετάφρασης", + "translation-file": "Αρχείο μετάφρασης", + "invalid-translation-file-error": "Δεν είναι δυνατή η εισαγωγή αρχείου μετάφρασης: Μη έγκυρη δομή δεδομένων μετάφρασης.", + "custom-translation-hint": "Καθορίστε την προσαρμοσμένη μετάφραση (JSON) παρακάτω. Αυτό το JSON θα αντικαταστήσει την προεπιλεγμένη μετάφραση. Κάνετε Λήψη αρχείου γλώσσας για να λάβετε την υπάρχουσα μετάφραση. Μπορείτε επίσης να χρησιμοποιήσετε το ληφθέν αρχείο ως αναφορά στα διαθέσιμα ζεύγη κλειδιών-τιμών μετάφρασης.", + "download-locale-file": "Λήψη αρχείου γλώσσας" + }, + "datetime": { + "date-from": "Ημ/νία από", + "time-from": "Ώρα από", + "date-to": "Ημ/νία έως", + "time-to": "Ώρα έως" + }, + "dashboard": { + "dashboard": "Dashboard", + "dashboards": "Dashboards", + "management": "Διαχείριση Dashboard", + "view-dashboards": "Προβολή Dashboards", + "add": "Προσθήκη Dashboard", + "assign-dashboard-to-customer": "Ανάθεση Dashboard(s) Σε Πελάτη", + "assign-dashboard-to-customer-text": "Παρακαλώ επιλέξτε τα dashboards που θέλετε να αναθέσετε σε πελάτη", + "assign-to-customer-text": "Παρακαλώ επιλέξτε τον πελάτη στον οποίο θέλετε να αναθέσετε το/τα dashboard(s)", + "assign-to-customer": "Ανάθεση σε πελάτη", + "unassign-from-customer": "Αφαίρεση από πελάτη", + "make-public": "Δημοσιοποιήση Dashboard", + "make-private": "Ιδιωτικοποίηση Dashboard", + "manage-assigned-customers": "Διαχείρηση ανατεθειμένων πελατών", + "assigned-customers": "Ανατεθειμένοι πελάτες", + "assign-to-customers": "Ανάθεση Dashboard(s) Σε Πελάτες", + "assign-to-customers-text": "Παρακαλώ επιλέξτε τους πελάτες στους οποίους θέλετε να αναθέσετε το/τα dashboard(s)", + "unassign-from-customers": "Μη Ανατεθειμένα Dashboard(s) Από Πελάτες", + "unassign-from-customers-text": "Παρακαλώ επιλέξτε τους πελάτες τους οποίους θέλετε να αφαιρέσετε από το/τα dashboard(s)", + "no-dashboards-text": "Δεν βρέθηκαν dashboards", + "no-widgets": "Δεν έχουν ρυθμιστεί widgets", + "add-widget": "Προσθέστε νέο widget", + "title": "Τίτλος", + "select-widget-title": "Επιλογή widget", + "select-widget-subtitle": "Λίστα με τους διαθέσιμους τύπους widget", + "delete": "Διαγραφή dashboard", + "title-required": "Απαιτείται ένας τίτλος.", + "description": "Περιγραφή", + "details": "Λεπτομέρειες", + "dashboard-details": "Λεπτομέρειες Dashboard", + "add-dashboard-text": "Προσθέστε νέο dashboard", + "assign-dashboards": "Αναθέστε dashboards", + "assign-new-dashboard": "Ανάθεση νέου dashboard", + "assign-dashboards-text": "Ανάθεση { count, plural, 1 {1 dashboard} other {# dashboards} } σε πελάτες", + "unassign-dashboards-action-text": "Μη ανατεθειμένο/α { count, plural, 1 {1 dashboard} other {# dashboards} } από πελάτες", + "delete-dashboards": "Διαγραφή dashboards", + "unassign-dashboards": "Αφαίρεση dashboards", + "unassign-dashboards-action-title": "Αφαίρεση { count, plural, 1 {1 dashboard} other {# dashboards} } από πελάτη", + "delete-dashboard-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε το dashboard '{{dashboardTitle}}'?", + "delete-dashboard-text": "Προσοχή, μετά την επιβεβαίωση, το dashboard και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-dashboards-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 dashboard} other {# dashboards} }?", + "delete-dashboards-action-title": "Διααγραφή { count, plural, 1 {1 dashboard} other {# dashboards} }", + "delete-dashboards-text": "Προσοχή, μετά την επιβεβαίωση, όλα τα επιλεγμένα dashboards και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "unassign-dashboard-title": "Είστε σίγουρου ότι θέλετε να αφαιρέσετε το dashboard '{{dashboardTitle}}'?", + "unassign-dashboard-text": "Μετά την επιβεβαίωση το dashboard θα αφαιρεθεί και δεν θα είναι διαθέσιμο στον πελάτη.", + "unassign-dashboard": "Αφαίρεση dashboard", + "unassign-dashboards-title": "Είστε σίγουρου ότι θέλετε να αφαιρέσετε { count, plural, 1 {1 dashboard} other {# dashboards} }?", + "unassign-dashboards-text": "Μετά την επιβεβαίωση όλα τα επιλεγμένα dashboards θα αφαιρεθούν και δεν θα είναι διαθέσιμα στον πελάτη.", + "public-dashboard-title": "Το dashboard είναι δημόσιο", + "public-dashboard-text": "Το dashboard {{dashboardTitle}} είναι δημόσιο και διαθέσιμο μέσω του παρακάτω συνδέσμου:", + "public-dashboard-notice": "Σημείωση: Μην ξεχνάτε να κάνετε δημόσιες τις συσκευές που συσχετίζονται ώστε να είναι δυνατή η πρόσβαση στα δεδομένα τους.", + "public-dashboard-link": "Σύνδεσμος δημόσιου dashboard", + "public-dashboard-link-text": "Το δημόσιο dashboard {{dashboardTitle}} είναι πλέον διαθέσιμο μέσω μέσω του παρακάτω συνδέσμου:", + "public-dashboard-link-notice": "Σημείωση: Μην ξεχνάτε να κάνετε δημόσιες τις συσκευές, τα assets και την προβολή οντοτήτων που συσχετίζονται ώστε να είναι δυνατή η πρόσβαση στα δεδομένα τους.", + "make-private-dashboard-title": "Είστε σίγουροι ότι θέλετε να κάνετε το dashboard '{{dashboardTitle}}' ιδιωτικό", + "make-private-dashboard-text": "Μετά την επιβεβαίωση το dashboard θα γίνουν ιδιωτικά και δεν θα είναι διαθέσιμα από τρίτους.", + "make-private-dashboard": "Κάνε το dashboard ιδιωτικό", + "socialshare-text": "'{{dashboardTitle}}' powered by EyeTech", + "socialshare-title": "'{{dashboardTitle}}' powered by EyeTech", + "select-dashboard": "Επιλογή dashboard", + "no-dashboards-matching": "Δεν βρέθηκαν dashboards που να αντιστοιχούν σε '{{entity}}'.", + "dashboard-required": "Απαιτείται dashboard.", + "select-existing": "Επιλέξτε ένα υπάρχον dashboard", + "create-new": "Δημιουργία νέου dashboard", + "new-dashboard-title": "Τίτλος νέου dashboard", + "open-dashboard": "Άνοιγμα dashboard", + "set-background": "Ορίστε φόντο", + "background-color": "χρώμα φόντου", + "background-image": "εικόνα φόντου", + "background-size-mode": "μέγεθος φόντου", + "no-image": "Δεν επιλέχθηκε εικόνα", + "drop-image": "Ρίξτε εδώ μια εικόνα ή κάνετε κλικ για να επιλέξετε ένα αρχείο για να ανεβεί.", + "settings": "Ρυθμίσεις", + "columns-count": "Αρίθμιση στηλών", + "columns-count-required": "Απαιτείται η αρίθμιση στηλών.", + "min-columns-count-message": "Ελάχιστος αριθμός στηλών είναι 10.", + "max-columns-count-message": "Μέγιστος αριθμός στηλών είναι 1000.", + "widgets-margins": "Περιθώριο μεταξύ widgets", + "horizontal-margin": "Οριζόντιο περιθώριο", + "horizontal-margin-required": "Απαιτείται τιμή για το οριζόντιο περιθώριο.", + "min-horizontal-margin-message": "Ελάχιστη τιμή οριζόντιου περιθωρίου είναι 0.", + "max-horizontal-margin-message": "Μέγιστη τιμή οριζόντιου περιθωρίου είναι 50.", + "vertical-margin": "Κάθετο περιθώριο", + "vertical-margin-required": "Απαιτείται τιμή για το κάθετο περιθώριο", + "min-vertical-margin-message": "Ελάχιστη τιμή καθέτου περιθωρίου είναι 0.", + "max-vertical-margin-message": "Μέγιστη τιμή καθέτου περιθωρίου είναι 50.", + "autofill-height": "Αυτόματη συμπλήρωση ύψους διάταξης", + "mobile-layout": "Ρυθμίσεις διάταξης mobile", + "mobile-row-height": "ύψος γραμμής mobile, σε px", + "mobile-row-height-required": "Απαιτείται ύψος γραμμής mobile.", + "min-mobile-row-height-message": "Ελάχιστη τιμή ύψους γραμμής mobile είναι 5.", + "max-mobile-row-height-message": "Μέγιστη τιμή ύψους γραμμής mobile είναι 200.", + "display-title": "Τίτλος εμφάνισης dashboard", + "toolbar-always-open": "Γραμμή εργαλείων πάντα ανοιχτή", + "title-color": "Χρώμα τίτλου", + "display-dashboards-selection": "Εμφάνηση επιλογής dashboards", + "display-entities-selection": "Εμφάνιση επιλογής οντοτήτων", + "display-dashboard-timewindow": "Εμφάνιση χρονικού πλαισίου", + "display-dashboard-export": "Εμφάνιση εξαγωγής", + "import": "Εισαγωγή dashboard", + "export": "Εξαγωγή dashboard", + "export-failed-error": "Δεν ήταν δυνατή η εξαγωγή dashboard: {{error}}", + "export-pdf": "Εξαγωγή ως PDF", + "export-png": "Εξαγωγή ως PNG", + "export-jpg": "Εξαγωγή ως JPEG", + "export-json-config": "Εξαγωγή ρυθμίσεις JSON", + "download-dashboard-progress": "Δημιουργία dashboard {{reportType}} ...", + "create-new-dashboard": "Δημιουργία νέου dashboard", + "dashboard-file": "Αρχείο dashboard", + "invalid-dashboard-file-error": "Δεν ήταν δυνατή η εισαγωγή dashboard: Μη έγκυρη δομή δεδομένων dashboard.", + "dashboard-import-missing-aliases-title": "Διαμορφώστε τα ψευδώνυμα που χρησιμοποιούνται από το dashboard που εισαγάγατε", + "create-new-widget": "Δημιουργία νέου widget", + "import-widget": "Εισαγωγή widget", + "widget-file": "Αρχείο widget", + "invalid-widget-file-error": "Δεν ήταν δυνατή η εισαγωγή widget: Μη έγκυρη δομή δεδομένων dashboard.", + "widget-import-missing-aliases-title": "Διαμορφώστε τα ψευδώνυμα που χρησιμοποιούνται από το widget που εισαγάγατε", + "open-toolbar": "Άνοιγμα γραμμής εργαλείων dashboard", + "close-toolbar": "Κλείσιμο γραμμής εργαλείων", + "configuration-error": "Σφάλμα ρυθμίσεων", + "alias-resolution-error-title": "Σφάλμα ρυθμίσεων ψευδωνύμων dashboard", + "invalid-aliases-config": "Δεν βρέθηκε καμία συσκευή που να ταιριάζει με κάποιο από τα φίλτρα.
    Παρακαλούμε επικοινωνήστε με τον Διαχειριστή για την επίλυση του ζητήματος.", + "select-devices": "Επιλογή συσκευών", + "assignedToCustomer": "Ανατεθειμένο σε πελάτη", + "assignedToCustomers": "Ανατεθειμένο σε πελάτες", + "public": "Δημόσιο", + "public-link": "Δημόσιος σύνδεσμος", + "copy-public-link": "Αντιγραφή δημόσιου συνδέσμου", + "public-link-copied-message": "Ο δημόσιος σύνδεσμος έχει αντιγραφεί στο πρόχειρο", + "manage-states": "Διαχείρηση κατάστασης dashboard", + "states": "Κατάσταση dashboard", + "search-states": "Αναζήτηση σε κατάσταση dashboard", + "selected-states": "{ count, plural, 1 {1 dashboard state} other {# dashboard states} } επιλεγμένα", + "edit-state": "Επεξεργασία κατάστασης dashboard", + "delete-state": "Διαγραφή κατάστασης dashboard", + "add-state": "Προσθήκη κατάστασης dashboard", + "state": "Κατάσταση Dashboard", + "state-name": "Όνομα", + "state-name-required": "Απαιτείται όνομα κατάστασης dashboard.", + "state-id": "ID Κατάστασης", + "state-id-required": "Απαιτείται ID κατάστασης dashboard.", + "state-id-exists": "Υπάρχει ήδη κατάσταση dashboard με το ίδιο ID.", + "is-root-state": "Root state", + "delete-state-title": "Διαγραφή κατάστασης dashboard", + "delete-state-text": "Είστε σίγουροι ότι θέλετε να διαγράψετε την κατάσταση dashboard με όνομα '{{stateName}}'?", + "show-details": "Εμφάνιση λεπτομερειών", + "hide-details": "Απόκρυψη λεπτομερειών", + "select-state": "Επιλογή κατάστασης", + "state-controller": "Έλεγχος κατάστασης", + "selected-dashboards": "{ count, plural, 1 {1 dashboard} other {# dashboards} } επιλεγμένα", + "search": "Αναζήτηση dashboards", + "select-group-to-add": "Επιλογή ομάδας για να προστεθεί στα επιλεγμένα dashboards", + "select-group-to-move": "Επιλογή ομάδας για να μετακινηθεί στα επιλεγμένα dashboards", + "remove-dashboards-from-group": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε { count, plural, 1 {1 dashboard} other {# dashboards} } from group '{entityGroup}'?", + "group": "Ομάδα dashboards", + "list-of-groups": "{ count, plural, 1 {One dashboard group} other {List of # dashboard groups} }", + "group-name-starts-with": "Ομάδες dashboard των οποίων το όνομα αρχίζει από '{{prefix}}'" + }, + "datakey": { + "settings": "Ρυθμίσεις", + "advanced": "Προχωρημένες", + "label": "Ετικέτα", + "color": "Χρώμα", + "units": "Ειδικό σύμβολο που εμφανίζεται δίπλα από την τιμή", + "decimals": "Αριθμός ψηφίων μετά την υποδιαστολή", + "data-generation-func": "Λειτουργία δημιουργίας δεδομένων", + "use-data-post-processing-func": "Χρησιμοποιήστε τη λειτουργία μετα-επεξεργασίας δεδομένων", + "configuration": "Ρυθμίσεις ονομάτων δεδομένων", + "timeseries": "Χρονική σειρά", + "attributes": "Ιδιώτητες", + "alarm": "Πεδία alarm", + "timeseries-required": "Απαιτείται χρονική σειρά οντοτήτων.", + "timeseries-or-attributes-required": "Απαιτείται χρονική σειρά/ιδιώτητες οντοτήτων.", + "maximum-timeseries-or-attributes": "Μέγιστο { count, plural, 1 {1 timeseries/attribute is allowed.} other {# timeseries/attributes are allowed} }", + "alarm-fields-required": "Απαιτούνται πεδία alarm.", + "function-types": "Τύποι λειτουργιών", + "function-types-required": "Απαιτούνται τύποι λειτουργιών.", + "maximum-function-types": "Μέγιστο { count, plural, 1 {1 function type is allowed.} other {# function types are allowed} }", + "time-description": "χρονικό σημείο της συγκεκριμένης τιμής", + "value-description": "η συγκεκριμένη τιμή", + "prev-value-description": "αποτέλεσμα της προηγούμενης λειτουργίας", + "time-prev-description": "χρονσικό σημείο της προηγούμενης τιμής", + "prev-orig-value-description": "αρχική προηγούμενη τιμή" + }, + "datasource": { + "type": "Τύπος πηγής δεδομένων", + "name": "Όνομα", + "add-datasource-prompt": "Παρακαλούμε προσθέστε πηγή δεδομένων" + }, + "details": { + "details": "Λεπτομέρειες", + "edit-mode": "Λειτουργία Επεξεργασίας", + "toggle-edit-mode": "Εναλλαγή λειτουργίας επεξεργασίας" + }, + "device": { + "device": "Συσκευή", + "device-required": "Απαιτείται ορισμός συσκευής.", + "devices": "Συσκευές", + "management": "Διαχείριση Συσκευών", + "view-devices": "Προβολή Συσκευών", + "device-alias": "Ψευδώνυμο συσκευής", + "aliases": "Ψευδώνυμα συσκευής", + "no-alias-matching": "Δεν βρέθηκε '{{alias}}'.", + "no-aliases-found": "Δεν βρέθηκαν ψευδώνυμα.", + "no-key-matching": "Δεν βρέθηκε '{{key}}'.", + "no-keys-found": "Δεν βρέθηκαν ονόματα", + "create-new-alias": "Δημιουργήστε ένα νέο ψευδώνυμο!", + "create-new-key": "Δημιουργήστε ένα νέο!", + "duplicate-alias-error": "Βρέθηκε διπλότυπο το ψευδώνυμο '{{alias}}'.
    Τα ψευδώνυμα συσκευών πρέπει να είναι μοναδικά σε ένα dashboard.", + "configure-alias": "Ρύθμιση ψευδώνυμου '{{alias}}'", + "no-devices-matching": "Δεν βρέθηκε συσκευή η οποία να αντιστοιχεί σε '{{entity}}'.", + "alias": "Ψευδώνυμο", + "alias-required": "Απαιτείται ψευδώνυμο συσκευής.", + "remove-alias": "Αφαίρεση ψευδώνυμου συσκευής", + "add-alias": "Προσθήκη ψευδώνυμου συσκευής", + "name-starts-with": "Το όνομα συσκευής αρχίζει με", + "device-list": "Λίστα συσκευών", + "use-device-name-filter": "Χρήση φίλτρου", + "device-list-empty": "Δεν επιλέχθηκαν συσκευές.", + "device-name-filter-required": "Απαιτείται φίλτρο ονόματος συσκυεής.", + "device-name-filter-no-device-matched": "Δεν βρέθηκαν συσκευές οι οποίες να αρχίζουν από '{{device}}'.", + "add": "Προσθήκη συσκευής", + "assign-to-customer": "Ανάθεση σε πελάτη", + "assign-device-to-customer": "Ανάθεση Συσκευών Σε Πελάτη", + "assign-device-to-customer-text": "Παρακαλούμε επιλέξτε συσκευές προς ανάθεση σε πελάτη", + "make-public": "Δημοσιοποίηση συσκευών", + "make-private": "Ιδιωτικοποίηση συσκευών", + "no-devices-text": "Δεν βρέθηκαν συσκευές", + "assign-to-customer-text": "Παρακαλώ επιλέξτε πελάτη για να αναθέσετε τις συσκευές", + "device-details": "Λεπτομέρειες συσκευές", + "add-device-text": "Προσθήκη νέας συσκευής", + "credentials": "Διαπιστευτήρια", + "manage-credentials": "Διαχείρηση διαπιστευτηρίων", + "delete": "Διαγραφή συσκευής", + "assign-devices": "Ανάθεση συσκευών", + "assign-devices-text": "Ανάθεση { count, plural, 1 {1 device} other {# devices} } σε πελάτη", + "delete-devices": "Διαγραφή συσκευών", + "unassign-from-customer": "Αφαίρεση από πελάτη", + "unassign-devices": "Αφαίρεση συσκευών", + "unassign-devices-action-title": "Αφαίρεση { count, plural, 1 {1 device} other {# devices} } από πελάτη", + "assign-new-device": "Ανάθεση νέων συσκευών", + "make-public-device-title": "Είστε σίγουροι ότι θέλετε να κάνετε τη συσκευή '{{deviceName}}' δημόσια;", + "make-public-device-text": "Μετά από την επιβεβαίωσή σας η συσκευή και όλα τα δεδομένα της θα είναι δημόσια και διαθέσιμα σε τρίτους.", + "make-private-device-title": "Είστε σίγουροι ότι θέλετε να κάνετε τη συσκευή '{{deviceName}}' ιδιωτική;", + "make-private-device-text": "Μετά από την επιβεβαίωσή σας η συσκευή και όλα τα δεδομένα της θα είναι ιδιωτικά και δεν θα είναι διαθέσιμα σε τριτους.", + "view-credentials": "Προβολή διαπιστευτηρίων", + "delete-device-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε την συσκευή '{{deviceName}}';", + "delete-device-text": "Προσοχή, μετά την επιβεβαίωση, η συσκευή και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-devices-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 device} other {# devices} };", + "delete-devices-action-title": "Διαγραφή { count, plural, 1 {1 device} other {# devices} }", + "delete-devices-text": "Προσοχή, μετά την επιβεβαίωση, όλες οι επιλεγμένες συσκευές θα αφαιρεθούν και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "unassign-device-title": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε την συσκευή '{{deviceName}}'?", + "unassign-device-text": "Μετά την επιβεβαίωση, η συσκευή θα αφαιρεθεί και δεν θα είναι προσβάσιμες από τον πελάτη.", + "unassign-device": "Αφαίρεση συσκευής", + "unassign-devices-title": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε { count, plural, 1 {1 device} other {# devices} }?", + "unassign-devices-text": "Μετά την επιβεβαίωση, όλες οι επιλεγμένες συσκευές θα καταργηθούν και δεν θα είναι προσβάσιμες από τον πελάτη.", + "device-credentials": "Πιστοποιητικά συσκευής", + "credentials-type": "Τύπος διαπιστευτηρίων", + "access-token": "Διακριτικό πρόσβασης", + "access-token-required": "Απαιτείται διακριτικό πρόσβασης.", + "access-token-invalid": "Access token length must be from 1 to 20 characters.", + "rsa-key": "RSA public key", + "rsa-key-required": "Απαιτείται RSA public key.", + "secret": "Secret", + "secret-required": "Απαιτείται secret.", + "device-type": "Τύπος συσκευής", + "device-type-required": "Απαιτείται τύπος συσκεύης.", + "select-device-type": "Επιλογή τύπου συσκευής", + "enter-device-type": "Είσοδο τύπου συσκευής", + "any-device": "Οποιαδήποτε συσκευή", + "no-device-types-matching": "Δεν βρέθηκε συσκευή η οποία να αντιστοιχεί σε '{{entitySubtype}}'.", + "device-type-list-empty": "Δεν έχουν επιλεχθεί τύποι συσκευής.", + "device-types": "Τύποι συσκευής", + "name": "Όνομα", + "name-required": "Απαιτείται όνομα.", + "description": "Περιγραφή", + "label": "Ετικέτα", + "events": "Γεγονότα", + "details": "Λεπτομέρειες", + "copyId": "Αντιγραφή ID συσκευής", + "copyAccessToken": "Αντιγραφή διακριτικού διαπιστευτηρίου", + "idCopiedMessage": "Το ID της συσκευής έχει αντιγραφεί στο πρόχειρο", + "accessTokenCopiedMessage": "Το διακριτικό διαπιστευτήριο έχει αντιγραφεί στο πρόχειρο", + "assignedToCustomer": "Ανάθεση σε πελάτη", + "unable-delete-device-alias-title": "Αδύνατο να διαγραφεί το ψευδώνυμο συσκευής", + "unable-delete-device-alias-text": "Το ψευδώνυμο συσκευής '{{deviceAlias}}' δεν μπορεί να διαγραφεί όσο εξακωλουθεί να χρησιμοποιείται από τα ακόλουθα widget(s):
    {{widgetsList}}", + "is-gateway": "είναι gateway", + "public": "Δημόσιο", + "device-public": "Η συσκευή είναι δημόσια", + "select-device": "Επιλογή συσκευής", + "selected-devices": "{ count, plural, 1 {1 device} other {# devices} } επιλέχθηκαν", + "search": "Αναζήτηση συσκευών", + "select-group-to-add": "Επιλέξτε την ομάδα στην οποία θα προστεθουν οι επιλεγμένες συσκευές", + "select-group-to-move": "Επιλέξτε την ομάδα στην οποία θα μετακινηθούν οι επιλεγμένες συσκευές", + "remove-devices-from-group": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε { count, plural, 1 {1 device} other {# devices} } from group '{entityGroup}'?", + "group": "Ομάδα Συσκευών", + "list-of-groups": "{ count, plural, 1 {One device group} other {List of # device groups} }", + "group-name-starts-with": "Ομάδες συσκευών των οποίων το όνομα αρχίζει από '{{prefix}}'", + "import": "Εισαγωγή συσκευής", + "device-file": "Αρχείο συσκευής" + }, + "dialog": { + "close": "Κλείσιμο διαλόγου" + }, + "direction": { + "column": "Στήλη", + "row": "Γραμμή" + }, + "error": { + "unable-to-connect": "Αδύνατον να συνδεθεί στον διακομιστή! Παρακαλούμε ελέγξτε την σύνδεσή σας στο ίντερνετ.", + "unhandled-error-code": "Μη διορθωμένος κωδικός σφάλματος: {{errorCode}}", + "unknown-error": "Άγνωστο σφάλμα" + }, + "entity": { + "entity": "Οντότητα", + "entities": "Οντότητες", + "aliases": "Ψευδώνυμα οντότητας", + "entity-alias": "Ψευδώνυμο οντότητας", + "unable-delete-entity-alias-title": "Αδύνατο να διαγραφεί το ψευδώνυμο οντότητας", + "unable-delete-entity-alias-text": "Το ψευδώνυμο οντότητας '{{entityAlias}}' δεν μπορεί να διαγραφεί όσο εξακολουθεί να χρησιμοποιείται από τα παρακάτω widget(s):
    {{widgetsList}}", + "duplicate-alias-error": "Βρέθηκε διπλότυπο το ψευδώνυμο '{{alias}}'.
    Τα ψευδώνυμα οντοτήτων πρέπει να είναι μοναδικά σε ένα dashboard.", + "missing-entity-filter-error": "Λείπει φίλτρο από το ψευδώνυμο '{{alias}}'.", + "configure-alias": "Ρύθμιση ψευδώνυμου '{{alias}}'", + "alias": "Ψευδώνυμο", + "alias-required": "Απαιτείται ψευδώνυμο οντότητας.", + "remove-alias": "Αφαίρεση ψευδώνυμου οντότητας", + "add-alias": "Προσθήκη ψευδώνυμου οντότητας", + "entity-list": "Λίστα οντοτήτων", + "entity-type": "Τύπος οντοτήτων", + "entity-types": "Τύποι οντοτήτων", + "entity-type-list": "Λίστα τύπων οντοτήτων", + "any-entity": "Οποιαδήποτε οντότητα", + "enter-entity-type": "Εισαγωγή τύπου οντότητας", + "no-entities-matching": "Δεν βρέθηκαν οντότητες ο οποίες να σχετίζονται με '{{entity}}'.", + "no-entity-types-matching": "Δεν βρέθηκαν τύποι οντότητας ο οποίοι να σχετίζονται με '{{entityType}}'.", + "name-starts-with": "το όνομα αρχίζει από", + "use-entity-name-filter": "χρήση φίλτρου", + "entity-list-empty": "Δεν έχουν επιλεγεί οντότητες.", + "entity-type-list-empty": "Δεν έχουν επιλεγεί τύποι οντότητας.", + "entity-name-filter-required": "Απαιτείται φίλτρο ονόματος οντότητας.", + "entity-name-filter-no-entity-matched": "Δεν βρέθηκαν οντότητες οι οποίες αρχίζουν από '{{entity}}'.", + "all-subtypes": "Όλοι", + "select-entities": "Επιλογή οντοτήτων", + "no-aliases-found": "Δεν βρέθηκαν ψευδώνυμα.", + "no-alias-matching": "Δεν βρέθηκε '{{alias}}'.", + "create-new-alias": "Δημιουργείστε ένα νέο!", + "key": "Κλειδί", + "key-name": "όνομα κλειδιού", + "no-keys-found": "δεν βρέθηκαν κλειδιά.", + "no-key-matching": "Δεν βρέθηκε '{{key}}'.", + "create-new-key": "Δημιουργείστε ένα νέο!", + "type": "Τύπος", + "type-required": "Απαιτείται τύπος οντότητας.", + "type-device": "Συσκευή", + "type-devices": "Συσκευές", + "list-of-devices": "{ count, plural, 1 {One device} other {List of # devices} }", + "device-name-starts-with": "Συσκευές των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-asset": "Asset", + "type-assets": "Assets", + "list-of-assets": "{ count, plural, 1 {One asset} other {List of # assets} }", + "asset-name-starts-with": "Assets των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-entity-view": "Προβολή οντότητας", + "type-entity-views": "Προβολές οντότητας", + "list-of-entity-views": "{ count, plural, 1 {One entity view} other {List of # entity views} }", + "entity-view-name-starts-with": "Προβολές οντότητας των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-rule": "Κανόνας", + "type-rules": "Κανόνες", + "list-of-rules": "{ count, plural, 1 {One rule} other {List of # rules} }", + "rule-name-starts-with": "Κανόνες των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-plugin": "Πρόσθετο", + "type-plugins": "Προσθετα", + "list-of-plugins": "{ count, plural, 1 {One plugin} other {List of # plugins} }", + "plugin-name-starts-with": "Πρόσθετα των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-tenant": "Μισθωτής", + "type-tenants": "Μισθωτές", + "list-of-tenants": "{ count, plural, 1 {One tenant} other {List of # tenants} }", + "tenant-name-starts-with": "Μισθωτές των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-customer": "Πελάτης", + "type-customers": "Πελάτες", + "list-of-customers": "{ count, plural, 1 {One customer} other {List of # customers} }", + "customer-name-starts-with": "Πελάτες των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-user": "Χρήστης", + "type-users": "Χρήστες", + "list-of-users": "{ count, plural, 1 {One user} other {List of # users} }", + "user-name-starts-with": "Χρήστες των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-dashboard": "Dashboard", + "type-dashboards": "Dashboards", + "list-of-dashboards": "{ count, plural, 1 {One dashboard} other {List of # dashboards} }", + "dashboard-name-starts-with": "Dashboards των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-alarm": "Alarm", + "type-alarms": "Alarms", + "list-of-alarms": "{ count, plural, 1 {One alarms} other {List of # alarms} }", + "alarm-name-starts-with": "Alarms των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-rulechain": "Αλυσίδα Κανόνων", + "type-rulechains": "Αλυσίδες Κανόνων", + "list-of-rulechains": "{ count, plural, 1 {One rule chain} other {List of # rule chains} }", + "rulechain-name-starts-with": "Αλυσίδες Κανόνων των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-scheduler-event": "Προγραμματιστής", + "type-scheduler-events": "Προγραμματιστής", + "list-of-scheduler-events": "{ count, plural, 1 {One scheduler event} other {List of # scheduler events} }", + "scheduler-event-name-starts-with": "Προγραμματιστές γεγονότων των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-blob-entity": "Ογκώδη οντότητα", + "type-blob-entities": "Ογκώδεις οντότητες", + "list-of-blob-entities": "{ count, plural, 1 {One blob entity} other {List of # blob entities} }", + "blob-entity-name-starts-with": "Ογκώδεις οντότητες των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-rulenode": "Κόμβος κανόνα", + "type-rulenodes": "Κόμβοι κανόνων", + "list-of-rulenodes": "{ count, plural, 1 {One rule node} other {List of # rule nodes} }", + "rulenode-name-starts-with": "Κόμβοι κανόνων των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-current-customer": "Τρέχον Πελάτης", + "search": "Αναζήτηση Οντότητες", + "selected-entities": "{ count, plural, 1 {1 entity} other {# entities} } επιλεγμένα", + "entity-name": "Όνομα οντότητας", + "details": "Λεπτομέρειες οντότητας", + "no-entities-prompt": "Δεν βρέθηκαν οντότητες", + "no-data": "Δεν υπάρχουν δεδομένα προς προβολή", + "columns-to-display": "στήλες που προβάλονται", + "type-entity-group": "Ομάδα Οντότητας", + "type-converter": "Μετατροπέας Δεδομένων", + "type-converters": "Μετατροπείς Δεδομένων", + "list-of-converters": "{ count, plural, 1 {One data converter} other {List of # data converters} }", + "converter-name-starts-with": "Μετατροπείς δεδομένων των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-integration": "Ενσωμάτωση", + "type-integrations": "Ενσωματώσεις", + "list-of-integrations": "{ count, plural, 1 {One integration} other {List of # integrations} }", + "integration-name-starts-with": "Ενσωματώσεις των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-role": "Ρόλος", + "type-roles": "Ρόλοι", + "list-of-roles": "{ count, plural, 1 {One role} other {List of # roles} }", + "role-name-starts-with": "Ρόλοι των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-group-permission": "Άδεια Ομάδας" + }, + "entity-group": { + "entity-group": "Ομάδα Οντότητας", + "details": "Λεπτομέρειες", + "columns": "Στήλες", + "add-column": "Προσθήκη στήλης", + "column-value": "Τιμή", + "column-value-required": "Απαιτείται τιμή στήλης.", + "column-title": "Τίτλος", + "default-sort-order": "Προεπιλογή ταξινόμισης", + "default-sort-order-required": "Απαιτείται προεπιλογή ταξινόμισης στήλης.", + "hide-in-mobile-view": "κρυφό σε mobile", + "use-cell-style-function": "Λειτουργία χρήσης στυλ κελιού", + "use-cell-content-function": "Λειτουργία χρήσης περιεχομένου κελιού", + "edit-column": "Επεξεργασία στήλης", + "column-details": "Λεπτομέρειες στήλης", + "actions": "Ενέργιες", + "settings": "Ρυθμίσεις", + "delete": "Διαγραφή ομάδας οντοτήτων", + "name": "Όνομα", + "name-required": "Απαιτείται όνομα.", + "description": "Περιγραφή", + "add": "Προσθήκη ομάδας οντοτήτων", + "add-entity-group-text": "Προσθήκη νέας ομάδας οντοτήτων", + "no-entity-groups-text": "Δεν βρέθηκαν ομάδες οντοτήτων", + "entity-group-details": "Λεπτομέρειες ομαδών οντοτήτων", + "delete-entity-groups": "Διαγραφή ομαδών οντοτήτων", + "delete-entity-group-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε την ομάδα οντοτήτων '{{entityGroupName}}'?", + "delete-entity-group-text": "Προσοχή, μετά την επιβεβαίωση, η ομάδα οντοτήτων και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-entity-groups-title": "Είστε σίγουροι ότι θέλετε να διαγραφούν { count, plural, 1 {1 entity group} other {# entity groups} }?", + "delete-entity-groups-action-title": "Διαγραφή { count, plural, 1 {1 entity group} other {# entity groups} }", + "delete-entity-groups-text": "Προσοχή, αφού ολοκληρωθεί η επιβεβαίωση, όλες οι επιλεγμένες ομάδες οντοτήτων θα αφαιρεθούν και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "device-groups": "Ομάδες Συσκευών", + "asset-groups": "Ομάδες Asset", + "customer-groups": "Ομάδες Πελατών", + "device-group": "Ομάδα Πελατών", + "asset-group": "Ομάδα Asset", + "user-group": "Ομάδα Χρηστών", + "user-groups": "Ομάδες Χρηστών", + "customer-group": "Ομάδα Χρηστών", + "entity-view-groups": "Ομάδες Όψεων", + "entity-view-group": "Προβολή ομαδών οντοτήτων", + "dashboard-groups": "Ομάδες Dashboard", + "dashboard-group": "Ομάδα Dashboard", + "fetch-more": "Περισσότερα", + "column-type": { + "column-type": "Τύπος στήλης", + "client-attribute": "Χαρακτηριστικά Client", + "shared-attribute": "Κοινόχρηστα Χαρακτηριστικά", + "server-attribute": "Χαρακτηριστικό Server", + "timeseries": "Χρονική σειρά", + "entity-field": "Πεδίο οντότητας" + }, + "column-type-required": "Απαιτείται τύπος στήλης.", + "entity-field": { + "created-time": "Δημιουργήθηκε", + "name": "Όνομα", + "type": "Τύπος", + "assigned_customer": "Ανατεθειμένος πελάτης", + "authority": "Εξουσιοδότηση", + "first_name": "Όνομα", + "last_name": "Επίθετο", + "email": "Email", + "title": "Τίτλος", + "country": "Χώρα", + "state": "Νομός", + "city": "Πόλη", + "address": "Διεύθυνση", + "address2": "Διεύθυνση 2", + "zip": "Τ.Κ.", + "phone": "Τηλέφωνο" + }, + "sort-order": { + "asc": "Αύξουσα", + "desc": "Φθίνουσα", + "none": "Καμία" + }, + "details-mode": { + "on-row-click": "στο κλικ στη γραμμη", + "on-action-button-click": "στο κλικ στο κουμπί λεπτομέρειες", + "disabled": "μη διαθέσιμο" + }, + "change-owner": "Αλλαγή ιδιοκτήτη", + "select-target-owner": "Επιλογή ιδιοκτήτη", + "no-owners-matching": "Δεν βρέθηκε ιδιοκτήτης που να αντιστοιχεί σε '{{owner}}'.", + "target-owner-required": "Απαιτείται επιλεγμένος ιδιοκτήτης.", + "confirm-change-owner-title": "Είστε σίγουροι ότι θέλετε να αλλάξετε ιδιοκτήτη για { count, plural, 1 {1 selected entity} other {# selected entities} }?", + "confirm-change-owner-text": "Προσοχή, μετά την επιβεβαίωση, όλες οι επιλεγμένες οντότητες θα αφαιρεθούν από τον τρέχοντα ιδιοκτήτη και θα τοποθετηθούν στην ομάδα 'Όλα' του επιλεγμένου ιδιοκτήτη.", + "add-to-group": "Προσθήκη σε ομάδα", + "move-to-group": "Μετακίνηση σε ομάδα", + "select-entity-group": "Επιλογή ομάδας οντοτήτων", + "no-entity-groups-matching": "Δεν βρέθηκαν ομάδες οντοτήτων που να αντιστοιχούν με '{{entityGroup}}'.", + "target-entity-group-required": "Απαιτείται επιλεγμένη ομάδα οντοτήτων.", + "select-user-group": "Επιλογή ομάδας χρηστών", + "no-user-groups-matching": "Δεν βρέθηκαν ομάδες χρηστών οι οποίες να αντιστοιχούν σε '{{entityGroup}}'.", + "target-user-group-required": "Απαιτείται επιλεγμένη ομάδα χρηστών.", + "remove-from-group": "Αφαίρεση από ομάδα", + "group-table-title": "Τίτλος ομάδας πίνακα", + "enable-search": "Ενεργοποίηση αναζήτησης οντοτήτων", + "enable-add": "Ενεργοποίηση προσθήκης οντοτήτων", + "enable-delete": "Ενεργοποίηση διαγραφής οντοτήτων", + "enable-selection": "Ενεργοποίηση επιλογής οντοτήτων", + "enable-group-transfer": "Ενεργοποίηση μεταφοράς ομάδων", + "display-pagination": "Προβολή σελιδοποίησης", + "default-page-size": "Προεπιλεγμένο μέγεθος σελίδας", + "enable-assignment-actions": "Ενεργοποίηση ανάθεσης", + "enable-credentials-management": "Ενεργοποίηση διαχείρησης διαπιστευτηρίων", + "enable-login-as-user": "Ενεργοποίηση εισόδου ως χρήστη", + "enable-users-management": "Ενεργοποίηση διαχείρισης χρήστών", + "enable-customers-management": "Ενεργοποίηση διαχείρησης πελατών", + "enable-assets-management": "Ενεργοποίηση διαχείρησης assets", + "enable-devices-management": "Ενεργοποίηση διαχείρησης συσκευών", + "enable-entity-views-management": "Ενεργοποίηση διαχείρησης προβολής οντοτήτων", + "enable-dashboards-management": "Ενεργοποίηση διαχείρησης dashboard", + "open-details-on": "Άνοιγμα λεπτομερειών οντότητας σε", + "select-existing": "Ε[ιλογή υπάρχον ομάδας οντοτήτων", + "create-new": "Δημιουργία νέας ομάδας οντοτήτων", + "new-entity-group-name": "Νέο όνομα ομάδας οντοτήτων", + "entity-group-list": "Λίστα ομάδων οντοτήτων", + "entity-group-list-empty": "Δεν έχουν επιλεχθεί ομάδες οντοτήτων.", + "name-starts-with": "Το όνομα της ομάδας οντοτήτων αρχίζει από", + "entity-group-name-filter-required": "Απαιτείται φίλτρο ονόματος ομάδας οντοτήτων.", + "roles": "Ρόλοι", + "permissions": "Δικαιώματα", + "public": "Δημόσιο", + "entity-group-public": "Η ομάδα οντοτήτων είναι δημόσια", + "make-public": "Δημοσιοποίηση ομάδας οντοτήτων", + "make-private": "Ιδιωτικοποίηση ομάδας οντοτήτων", + "make-public-entity-group-title": "Είστε σίγουροι ότι θέλετε να κάνετε την ομάδα οντοτήτων '{{entityGroupName}}' δημόσια;", + "make-public-entity-group-text": "Μετά την επιβεβαίωση, η ομάδα οντοτήτων και όλες οι οντότητές της θα δημοσιοποιηθούν και θα είναι προσβάσιμες από τρίτους.", + "make-private-entity-group-title": "Είστε σίγουροι ότι θέλετε να κάνετε την ομάδα οντοτήτων '{{entityGroupName}}' ιδιωτική?", + "make-private-entity-group-text": "Μετά την επιβεβαίωση, η ομάδα οντοτήτων και όλες οι οντότητές της θα γίνουν ιδιωτικές και δεν θα είναι προσβάσιμες από τρίτους.", + "copyId": "Αντιγραφή ID ομάδας οντοτήτων", + "idCopiedMessage": "Το ID της ομάδας οντοτήτων έχει αντιγραφεί στο πρόχειρο" + }, + "entity-field": { + "created-time": "Δημιουργήθηκε", + "name": "Όνομα", + "type": "Τύπος", + "first-name": "Όνομα", + "last-name": "Επίθετο", + "email": "Email", + "title": "Τίτλος", + "country": "Χώρα", + "state": "Νομός", + "city": "Πόλη", + "address": "Διεύθυνση", + "address2": "Διεύθυνση 2", + "zip": "Τ.Κ.", + "phone": "Τηλέφωνο" + }, + "entity-view": { + "entity-view": "Όψη Οντότητας", + "entity-view-required": "Απαιτείται προβολή οντότητας.", + "entity-views": "Όψεις Οντοτήτων", + "management": "Διαχείριση Όψεων Οντοτήτων", + "view-entity-views": "Προβολή οντοτήτων", + "entity-view-alias": "Ψευδώνυμο προβολής οντότητας", + "aliases": "Ψευδώνυμα προβολής οντότητας", + "no-alias-matching": "'Δεν βρέθηκαν {{alias}}'.", + "no-aliases-found": "Δεν βρέθηκαν ψευδώνυμα.", + "no-key-matching": "'Δεν βρέθηκαν {{key}}'.", + "no-keys-found": "Δεν βρέθηκαν ονόματα.", + "create-new-alias": "Δημιουργήστε ένα νέο!", + "create-new-key": "Δημιουργήστε ένα νέο!", + "duplicate-alias-error": "Βρέθηκε διπλότυπο '{{alias}}'.
    Τα ψευδώνυμα προβολής οντότητας πρέπει να είναι μοναδικά εντός του dashboard.", + "configure-alias": "Ρύθμιση ψευδώνυμου '{{alias}}'", + "no-entity-views-matching": "Δεν βρέθηκαν προβολές οντοτήτων οι οποίες να αντιστοιχούν σε '{{entity}}'.", + "alias": "Ψευδώνυμο", + "alias-required": "Απαιτείται ψευδώνυμο προβολής οντοτήτων.", + "remove-alias": "Αφαίρεση ψευδωνύμου προβολής οντοτήτων", + "add-alias": "Προβολή ψευδωνύμου προβολής οντοτήτων", + "name-starts-with": "Το όνομα προβολής οντότητας αρχίζει από", + "entity-view-list": "Λίστα προβολής οντότητας", + "use-entity-view-name-filter": "Χρήση φίλτρου", + "entity-view-list-empty": "Δεν έχουν επιλεχθεί προβολές οντότητας.", + "entity-view-name-filter-required": "Απαιτείται φίλτρο ονόματος προβολής οντότητας.", + "entity-view-name-filter-no-entity-view-matched": "Δεν βρέθηκαν προβολές οντότητας οι οποίες να αρχίζουν από '{{entityView}}'.", + "add": "Προσθήκη Προβολής Οντότητας", + "assign-to-customer": "Ανάθεση σε πελάτη", + "assign-entity-view-to-customer": "Ανάθεση προβολής/ών οντότητας σε πελάτη", + "assign-entity-view-to-customer-text": "Παρακαλώ επιλέξτε τις προβολές οντότητας για να αναθέσετε σε πελάτη", + "no-entity-views-text": "Δεν βρέθηκαν προβολές οντότητας", + "assign-to-customer-text": "Παρακαλώ επιλέξτε πελάτη για να ανάθεση προβολών οντότητας", + "entity-view-details": "Λεπτομέρειες προβολής οντότητας", + "add-entity-view-text": "Προσθήκη νέας προβολής οντότητας", + "delete": "Διαγραφή προβολής οντότητας", + "assign-entity-views": "Ανάθεση προβολών οντότητας", + "assign-entity-views-text": "Ανάθεση { count, plural, 1 {1 entityView} other {# entityViews} } σε πελάτη", + "delete-entity-views": "Διαγραφή προβολών οντότητας", + "make-public": "Δημοσιοποίηση προβολής οντότητας", + "make-private": "Ιδιωτικοποίηση προβολής οντότητας", + "unassign-from-customer": "Αφαίρεση από πελάτη", + "unassign-entity-views": "Αφαίρεση προβολών οντότητας", + "unassign-entity-views-action-title": "Αφαίρεση { count, plural, 1 {1 entityView} other {# entityViews} } από πελάτη", + "assign-new-entity-view": "Ανάθεση νέας προβολής οντότητας", + "delete-entity-view-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε την προβολή οντότητας '{{entityViewName}}'?", + "delete-entity-view-text": "Προσοχή, μετά την επιβεβαίωση, η προβολή της οντότητας και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-entity-views-title": "Είστε σίγουροι ότι θέλετε να προβάλετε την οντότητα { count, plural, 1 {1 entityView} other {# entityViews} };", + "delete-entity-views-action-title": "Διαγραφή { count, plural, 1 {1 entityView} other {# entityViews} }", + "delete-entity-views-text": "Προσοχή, μετά την επιβεβαίωση, όλες οι επιλεγμένες προβολές της οντότητας και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "make-public-entity-view-title": "Είστε σίγουροι ότι θέλετε να κάνετε την προβολή οντότητας '{{entityViewName}}' δημόσια?", + "make-public-entity-view-text": "Μετά την επιβεβαίωση, η προβολή της οντότητας και όλα τα δεδομένα της θα δημοσιοποιηθούν και θα είναι προσβάσιμα από τρίτους.", + "make-private-entity-view-title": "Είστε σίγουροι ότι θέλετε να κάνετε την προβολή οντότητας '{{entityViewName}}' ιδιωτική;", + "make-private-entity-view-text": "Μετά την επιβεβαίωση, η προβολή της οντότητας και όλα τα δεδομένα της θα γίνουν ιδιωτικά και θα δεν είναι προσβάσιμα από τρίτους.", + "unassign-entity-view-title": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε την προβολη οντότητας '{{entityViewName}}';", + "unassign-entity-view-text": "Μετά την επιβεβαίωση, η προβολή της οντότητας θα καταργηθεί και δεν θα είναι προσβάσιμη από τον πελάτη.", + "unassign-entity-view": "Αφαίρεση προβολής οντότητας", + "unassign-entity-views-title": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε { count, plural, 1 {1 entityView} other {# entityViews} };", + "unassign-entity-views-text": "Μετά την επιβεβαίωση, όλες οι επιλεγμένες προβολές οντοτήτων θα αφαιρεθούν και δεν θα είναι προσβάσιμες από τον πελάτη.", + "entity-view-type": "Τύπος Προβολής Οντότητας", + "entity-view-type-required": "Απαιτείται τύπος προβολής οντότητας.", + "select-entity-view-type": "Επιλογή τύπου προβολής οντότητας", + "enter-entity-view-type": "Εισαγωγή τύπου προβολής οντοτήτων", + "any-entity-view": "Οποιαδήποτε προβολή οντότητας", + "no-entity-view-types-matching": "Δεν βρέθηκαν τύποι προβολής οντότητας οι οποίοι να αντιστοιχούν με '{{entitySubtype}}'.", + "entity-view-type-list-empty": "Δεν έχουν επιλεχθεί τύποι προβολής οντότητας.", + "entity-view-types": "Τύποι Προβολής Οντότητας", + "name": "Όνομα", + "name-required": "Απαιτείται όνομα.", + "description": "Περιγραφή", + "events": "Γεγονότα", + "details": "Λεπτομέρειες", + "copyId": "Αντιγραφή ID προβολής οντότητας", + "idCopiedMessage": "Το ID της προβολής οντότητας έχει αντιγραφεί στο πρόχειρο", + "assignedToCustomer": "Αναθέση σε πελάτη", + "unable-entity-view-device-alias-title": "Αδύνατον να διαγραφεί το ψευδώνυμο προβολής οντότητας", + "unable-entity-view-device-alias-text": "Το ψευδώνυμο συσκευής '{{entityViewAlias}}' δεν μπορεί να διαγραφεί όσο εξακωλουθεί να χρησιμοποιείτε από τα παρακάτω widget(s):
    {{widgetsList}}", + "select-entity-view": "Επιλογή προβολής οντότητας", + "start-date": "Ημερομηνία έναρξης", + "start-ts": "Ώρα έναρξης", + "end-date": "Ημερομηνία λήξης", + "end-ts": "Ώρα λήξης", + "date-limits": "Όρια ημερομηνίας", + "client-attributes": "Χαρακτηριστικά Client", + "shared-attributes": "Κοινόχρηστα Χαρακτηριστικά", + "server-attributes": "Χαρακτηριστικά Server", + "timeseries": "Χρονική σειρά", + "client-attributes-placeholder": "Χαρακτηριστικά Client", + "shared-attributes-placeholder": "Κοινόχρηστα Χαρακτηριστικά", + "server-attributes-placeholder": "Χαρακτηριστικά Server", + "timeseries-placeholder": "Χρονική σειρά", + "target-entity": "Στοχευμένη οντότητα", + "attributes-propagation": "Διάδοση χαρακτηριστικών", + "attributes-propagation-hint": "Η προβολή οντοτήτων θα αντιγράφει αυτόματα καθορισμένα χαρακτηριστικά από την στοχευμένη οντότητα κάθε φορά που αποθηκεύετε ή ενημερώνετε αυτήν την προβολή οντότητας. Για λόγους απόδοσης, τα χαρακτηριστικά της στοχευμένης οντότητας δεν μεταδίδονται στην προβολή οντότητας με κάθε αλλαγή χαρακτηριστικών. Μπορείτε να ενεργοποιήσετε την αυτόματη διάδοση ρυθμίζοντας τον κόμβο \"αντιγραφή για προβολή \" στην αλυσίδα κανόνων σας και συνδέοντας τα μηνύματα \"Χαρακτηριστικά Post \" και \"Ενημερωμένα Χαρακτηριστικά \" στον νέο κόμβο.", + "timeseries-data": "Δεδομένα χρονικής σειράς", + "timeseries-data-hint": "Ρυθμίστε τα δεδομένα της χρονικής σειράς της στοχευμένης οντότητας που θα είναι διαθέσιμα στην προβολή οντοτητας. Αυτά τα δεδομένα χρονικής σειράς είναι μόνο για ανάγνωση.", + "selected-entity-views": "{ count, plural, 1 {1 entity view} other {# entity views} } επιλεγμένα", + "search": "Αναζήτηση προβολών οντότητας", + "select-group-to-add": "Επιλογή ομάδας για προσθήκη των επιλεγμένων προβολών οντότητας", + "select-group-to-move": "Επιλογή ομάδας για μετακίνηση των επιλεγμένων προβολών οντότητας", + "remove-entity-views-from-group": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε { count, plural, 1 {1 entity view} other {# entity views} } from group '{entityGroup}'?", + "group": "Ομάδα προβολών οντότητας", + "list-of-groups": "{ count, plural, 1 {One entity view group} other {List of # entity view groups} }", + "group-name-starts-with": "Ομάδες προβολής οντότητας των οποίων το όνομα αρχίζει από '{{prefix}}'" + }, + "event": { + "events": "Γεγονότα", + "event-type": "Τύπος Γεγονότος", + "type-error": "Σφάλμα", + "type-lc-event": "Γεγονός κύκλου ζωής", + "type-stats": "Στατιστική", + "type-debug-converter": "Αποσφαλμάτωση", + "type-debug-integration": "Αποσφαλμάτωση", + "type-debug-rule-node": "Αποσφαλμάτωση", + "type-debug-rule-chain": "Αποσφαλμάτωση", + "no-events-prompt": "Δεν βρέθηκαν γεγονότα", + "error": "Σφάλμα", + "alarm": "Alarm", + "event-time": "Ώρα Γεγονότος", + "server": "Διακομιστής", + "body": "Σώμα (body)", + "method": "Μέθοδος", + "type": "Τύπος", + "in": "Είσοδος", + "out": "Έξοδος", + "metadata": "Μεταδεδομένα", + "message": "Μήνυμα", + "entity": "Οντότητα", + "message-id": "ID Μηνύματος", + "message-type": "Τύπος Μηνύματος", + "data-type": "Τύπος Δεδομένων", + "relation-type": "Τύπος Σχέσης", + "data": "Δεδομέναα", + "event": "Γεγονός", + "status": "Κατάσταση", + "success": "Επιτυχία", + "failed": "Απέτυχε", + "messages-processed": "Επεξεργασμένα μηνύματα", + "errors-occurred": "Παρουσιάστηκαν σφάλματα" + }, + "extension": { + "extensions": "Επεκτάσεις", + "selected-extensions": "{ count, plural, 1 {1 extension} other {# extensions} } επιλέχθηκαν", + "type": "Τύπος", + "key": "Κλειδί", + "value": "Τιμή", + "id": "ID", + "extension-id": "ID επέκτασης", + "extension-type": "Τύπος επέκτασης", + "transformer-json": "JSON *", + "unique-id-required": "Το τρέχον ID επέκτασης υπάρχει ήδη.", + "delete": "Διαγραφή επέκτασης", + "add": "Προσθήκη επέκτασης", + "edit": "Επεξεργασία επέκτασης", + "view": "Προβολή επέκτασης", + "delete-extension-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε την επέκταση '{{extensionId}}';", + "delete-extension-text": "Προσοχή, μετά την επιβεβαίωση, η επέκταση και όλα τα σχετικά δεδομένα θα διαγραφούν μόνιμα.", + "delete-extensions-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 extension} other {# extensions} }?", + "delete-extensions-text": "Προσέξτε, μετά την επιβεβαίωση θα αφαιρεθούν όλες οι επιλεγμένες επεκτάσεις.", + "converters": "Μετατροπείς", + "converter-id": "ID Μετατροπέα", + "configuration": "Διαμόρφωση", + "converter-configurations": "Ρυθμίσεις μετατροπέα", + "token": "Ετικέτα ασφαλείας", + "add-converter": "Προσθήκη μετατροπέα", + "add-config": "Προσθήκη ρυθμήσεων μετατροπέα", + "device-name-expression": "Έκφραση ονόματος συσκευής", + "device-type-expression": "Έκφραση τύπου συσκευής", + "custom": "Προσαρμοσμένο", + "to-double": "Διπλασιασμός", + "transformer": "Μετατροπέας", + "json-required": "Απαιτείται μετατροπέας JSON.", + "json-parse": "Αδύνατον να γίνει αναλύση του μετατροπέα JSON.", + "attributes": "Χαρακτηριστικά", + "add-attribute": "Προσθήκη χαρακτηριστικού", + "add-map": "Προσθήκη στοιχείου χαρτογράφισης", + "timeseries": "Χρονική σειρά", + "add-timeseries": "Προσθήκη χρονικής σειράς", + "field-required": "Απαιτείται το πεδίο", + "brokers": "Brokers", + "add-broker": "Προσθήκη broker", + "host": "Host", + "port": "Port", + "port-range": "Η Port πρέπει να είναι από 1 ως 65535.", + "ssl": "Ssl", + "credentials": "Διαπιστευτήρια", + "username": "Όνομα Χρήστη", + "password": "Κωδικός", + "retry-interval": "Διάστημα επανάληψης σε χιλιοστά του δευτερολέπτου", + "anonymous": "Ανώνυμα", + "basic": "Βασικά", + "pem": "PEM", + "ca-cert": "Αρχείο πιστοποιητικού CA *", + "private-key": "Αρχείο ιδιωτικού κλειδιού *", + "cert": "Αρχείο πιστοποιητικού *", + "no-file": "Δεν έχει επιλεχθεί αρχείο.", + "drop-file": "Αποθέστε ένα αρχείο ή κάνετε κλικ για να επιλέξετε ένα αρχείο για ανέβασμα.", + "mapping": "χαρτογράφιση", + "topic-filter": "Φίλτρο θέματος", + "converter-type": "Τύπος μετατροπέα", + "converter-json": "JSON", + "json-name-expression": "Έκφραση json ονόματος συσκευής", + "topic-name-expression": "Έκφραση θέματος ονόματος συσκευής", + "json-type-expression": "Έκφραση json τύπου συσκευής", + "topic-type-expression": "Έκφραση θέματος τύπου συσκευής", + "attribute-key-expression": "Έκφραση χαρακτηριστικού κλειδιού", + "attr-json-key-expression": "Έκφραση JSON χαρακτηριστικού κλειδιού", + "attr-topic-key-expression": "Έκφραση θέματος χαρακτηριστικού κλειδιού", + "request-id-expression": "Αίτημα έκφρασης ID", + "request-id-json-expression": "Αίτημα ID έκφρασης JSON", + "request-id-topic-expression": "Αίτημα ID έκφρασης θέματος", + "response-topic-expression": "Απάντηση έκφρασης θέματος", + "value-expression": "Έκφραση τιμής", + "topic": "Θέμα", + "timeout": "Λήξη σε χιλιοστά του δευτερολέπτου", + "converter-json-required": "Απαιτείται μετατροπέας JSON.", + "converter-json-parse": "Αδύνατον να αναλυθεί ο μετατροπέας JSON.", + "filter-expression": "Έκφραση φίλτρου", + "connect-requests": "Αιτήματα σύνδεσης", + "add-connect-request": "Προσθήκη αιτήματος σύνδεσης", + "disconnect-requests": "Αιτήματα αποσύνδεσης", + "add-disconnect-request": "Προσθήκη αιτήματος αποσύνδεσης", + "attribute-requests": "Χαρακτιριστικό αιτημάτων", + "add-attribute-request": "Προσθήκη χαρακτηριστικού αιτήματος", + "attribute-updates": "Ανανεώσεις χαρακτηριστικού", + "add-attribute-update": "Προσθήκη ανανέωσης χαρακτηριστικού", + "server-side-rpc": "Server side RPC", + "add-server-side-rpc-request": "Προσθήκη αιτήματος server-side RPC", + "device-name-filter": "Φίλτρο ονόματος συσκευής", + "attribute-filter": "ίλτρο χαρακτηριστικού", + "method-filter": "Φίλτρο μεθόδου", + "request-topic-expression": "Αίτημα έκφρασης θέματος", + "response-timeout": "Λήξη απάντησης σε χιλιοστά του δευτερολέπτου", + "topic-expression": "Έκφρασου θέματος", + "client-scope": "Πεδίο εφαρμογής πελάτη", + "add-device": "Προσθήκη συσκευής", + "opc-server": "Servers", + "opc-add-server": "Προσθήκη server", + "opc-add-server-prompt": "Παρακαλούμε προσθέστε server", + "opc-application-name": "Όνομα εφαρμογής", + "opc-application-uri": "URI Εφαρμογής", + "opc-scan-period-in-seconds": "Περίοδος σάρωσης σε δευτερόλεπτα", + "opc-security": "Ασφάλεια", + "opc-identity": "Ταυτότητα", + "opc-keystore": "Keystore", + "opc-type": "Τύπος", + "opc-keystore-type": "Τύπος", + "opc-keystore-location": "τοποθεσία *", + "opc-keystore-password": "Κωδικός", + "opc-keystore-alias": "Ψευδώνυμο", + "opc-keystore-key-password": "Κωδικός Κλειδί", + "opc-device-node-pattern": "Μοτίβο κόμβου συσκευής", + "opc-device-name-pattern": "Μοτίβο ονόματος συσκευής", + "modbus-server": "Servers/slaves", + "modbus-add-server": "Προσθήκη server/slave", + "modbus-add-server-prompt": "Παρακαλούμε προσθέστε server/slave", + "modbus-transport": "Μεταφορά", + "modbus-tcp-reconnect": "Αυτόματη επανασύνδεση", + "modbus-rtu-over-tcp": "RTU over TCP", + "modbus-port-name": "Όνομα serial port", + "modbus-encoding": "Encoding", + "modbus-parity": "Parity", + "modbus-baudrate": "Baud rate", + "modbus-databits": "Data bits", + "modbus-stopbits": "Stop bits", + "modbus-databits-range": "Τα Data bits πρέπει να κυμαίνονται από 7 εώς 8.", + "modbus-stopbits-range": "Τα Stop bits πρέπει να κυμαίνονται από 1 εώς 2.", + "modbus-unit-id": "ID Μονάδας", + "modbus-unit-id-range": "Το ID Μονάδας πρέπει να κυμαίνεται από 1 εώς 247.", + "modbus-device-name": "Όνομα συσκευής", + "modbus-poll-period": "Poll period (ms)", + "modbus-attributes-poll-period": "Χαρακτηριστικά poll period (ms)", + "modbus-timeseries-poll-period": "Χρονική σειρά poll period (ms)", + "modbus-poll-period-range": "Το Poll period θα πρέπει να έχει θετική τιμή.", + "modbus-tag": "Ετικέτα", + "modbus-function": "Λειτουργία", + "modbus-register-address": "Καταχώριση διεύθυνσης", + "modbus-register-address-range": "η καταχωριμένη διεύθυνση πρεπει να κυμαίνεται από 0 ως 65535.", + "modbus-register-bit-index": "Bit index", + "modbus-register-bit-index-range": "Το Bit index πρέπει να κυμαίνεται από 0 ως 15.", + "modbus-register-count": "Μετρητής καταχώρησης", + "modbus-register-count-range": "Ο μετρητής καταχώρησης πρεέπει να έχει θετική τιμή.", + "modbus-byte-order": "Byte order", + "sync": { + "status": "Κατάσταση", + "sync": "Σε Συγχρονισμό", + "not-sync": "Δεν Συγχρονίζεται", + "last-sync-time": "Τελευταία φορά συγχρονισμού", + "not-available": "Μη διαθέσιμο" + }, + "export-extensions-configuration": "Εξαγωγή διαμόρφωσης επεκτάσεων", + "import-extensions-configuration": "Εισαγωγή διαμόρφωσης επεκτάσεων", + "import-extensions": "Εισαγωγή επεκτάσεων", + "import-extension": "Εισαγωγή επέκτασης", + "export-extension": "Εξαγωγή επέκτασης", + "file": "Επεκτάσεις αρχείου", + "invalid-file-error": "Μη έγκυρη επέκταση αρχείου" + }, + "fullscreen": { + "expand": "Ανάπτυξη σε πλήρη οθόνη", + "exit": "Έξοδος από πλήρη οθόνη", + "toggle": "Εναλλαγή σε πλήρη οθόνη", + "fullscreen": "Πλήρης οθόνη" + }, + "function": { + "function": "Λειτουργία" + }, + "grid": { + "delete-item-title": "Είστε σίγουροι ότι θέλετε να διαγραφεί αυτό το αντικείμενο?", + "delete-item-text": "Προσοχή, μετά την επιβεβαίωση, αυτό το στοιχείο και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-items-title": "Είστε βέβαιοι ότι θέλετε να διαγράψετε { count, plural, 1 {1 item} other {# items} }?", + "delete-items-action-title": "Διαγραφή { count, plural, 1 {1 item} other {# items} }", + "delete-items-text": "Προσοχή, μετά την επιβεβαίωση, όλα τα επιλεγμένα στοιχεία θα καταργηθούν και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "add-item-text": "Προσθήκη νέου αντικειμένου", + "no-items-text": "Δεν βρέθηκαν αντικείμενα", + "item-details": "λεπτομέρειες αντικείμενου", + "delete-item": "Διαγραφή Αντικειμένου", + "delete-items": "Διαγραφή Αντικειμένων", + "scroll-to-top": "Κύλιση προς τα πάνω" + }, + "help": { + "goto-help-page": "Πηγαίνετε στη σελίδα" + }, + "home": { + "home": "Αρχικη", + "profile": "Προφίλ", + "logout": "Αποσυνδέση", + "menu": "Μενού", + "avatar": "Avatar", + "open-user-menu": "Άνοιγμα μενού χρήστη" + }, + "import": { + "no-file": "Δεν έχει επιλεχθεί αρχείο", + "drop-file": "Αποθέστε ένα αρχείο JSON ή κάνετε κλικ για να επιλέξετε ένα αρχείο για ανέβασμα.", + "drop-csv-file": "Αποθέστε ένα αρχείο CVS ή κάνετε κλικ για να επιλέξετε ένα αρχείο για ανέβασμα.", + "drop-file-csv": "Αποθέστε ένα αρχείο CSV ή κάνετε κλικ για να επιλέξετε ένα αρχείο για ανέβασμα.", + "column-value": "Τιμή", + "column-title": "Τίτλος", + "column-example": "Παράδειγμα δεδομένων", + "column-key": "Κλειδί χαρακτηριστικού/τηλεμετρίας", + "csv-delimiter": "Οριοθέτηση CSV", + "csv-first-line-header": "Η πρώτη σειρά περιέχει ονόματα στηλών", + "csv-update-data": "Ανανέωση χαρακτηριστικών/τηλεμετρίας", + "import-csv-number-columns-error": "Ένα αρχείο θα πρέπει να περιέχει τουλάχιστον δυο στήλες", + "import-csv-invalid-format-error": "Μη έγκυρη μορφή αρχείου. Σειρά: '{{line}}'", + "column-type": { + "name": "Όνομα", + "type": "Τύπος", + "column-type": "Τύπος στήλης", + "client-attribute": "Χαρακτηριστικό Client", + "shared-attribute": "Χαρακτηριστικό Shared", + "server-attribute": "Χαρακτηριστικό Server", + "timeseries": "Χρονική σειρά", + "entity-field": "Πεδίο Οντότητας", + "access-token": "Διακριτικό πρόσβασης" + }, + "stepper-text": { + "select-file": "Επιλογή αρχείου", + "configuration": "Εισαγωγή ρύθμισης", + "column-type": "Επιλογή τύπου στήλης", + "creat-entities": "Δημιουργία νέων οντοτήτων", + "done": "Ολοκληρώθηκε" + }, + "message": { + "create-entities": "{{count}} νέες οντότητες δημιουργήθηκαν με επιτυχία.", + "update-entities": "{{count}} οντότητες ενημερώθηκαν με επιτυχία.", + "error-entities": "Υπήρξε κάποιο σφάλμα κατά τη δημιουργία {{count}} οντοτήτων." + } + }, + "integration": { + "integration": "Ενσωμάτωση", + "integrations": "Ενσωματώσεις", + "select-integration": "Επιλογή ενσωμάτωσης", + "no-integrations-matching": "Δεν βρέθηκαν ενσωματώσεις πο να αντιστοιχούν σε '{{entity}}'.", + "integration-required": "Απαιτείται ενσωμάτωση", + "delete": "Διαγραφή ενσωμάτωσης", + "management": "Διαχείριση Ενσωματώσεων", + "add-integration-text": "Προσθήκη νέας ενσωμάτωσης", + "no-integrations-text": "Δεν βρέθηκαν ενσωματώσεις", + "selected-integrations": "{ count, plural, 1 {1 integration} other {# integrations} } επιλέχθηκαν", + "delete-integration-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε την ενσωμάτωση '{{integrationName}}';", + "delete-integration-text": "Προσοχή, μετά την επιβεβαίωση, η ενσωμάτωση και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-integrations-title": "Είστε σίγουροι ότι θέλετε { count, plural, 1 {1 integration} other {# integrations} };", + "delete-integrations-action-title": "Διαγραφή { count, plural, 1 {1 integration} other {# integrations} }", + "delete-integrations-text": "Προσοχή, αφού ολοκληρωθεί η επιβεβαίωση, όλες οι επιλεγμένες ενσωματώσεις θα αφαιρεθούν και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "events": "Γεγονότα", + "add": "Προσθήκη Ενσωμάτωσης", + "integration-details": "Λεπτομέρειες Ενσωμάτωσης", + "details": "Λεπτομέρειες", + "copyId": "Αντιγραη ID ενσωμάτωσης", + "idCopiedMessage": "Το ID της ενσωμάτωσης έχει αντιγραφεί στο πρόχειρο.", + "debug-mode": "Λειτουργία απασφαλμάτωσης", + "enable-security": "Ενεργοποίηση ασφάλειας", + "headers-filter": "Φίλτρο Headers", + "header": "Header", + "no-headers-filter": "Όχι φίλτρο headers", + "downlink-url": "URL Λήψης", + "application-uri": "URI Εφαρμογής", + "as-id": "AS ID", + "as-id-required": "Απαιτείται AS ID.", + "as-key": "AS Key", + "as-key-required": "Απαιτείται AS Key.", + "max-time-diff-in-seconds": "Μέγιστη χρονική διαφορά (δευτερόλεπτα)", + "max-time-diff-in-seconds-required": "Απαιτείται μέγιστη χρονική διαφορά.", + "name": "Όνομα", + "name-required": "Απαιτείται όνομα.", + "description": "Περιγραφή", + "base-url": "Base URL", + "base-url-required": "Απαιτείται Base URL", + "security-key": "Κλειδί ασφάλειας", + "http-endpoint": "HTTP endpoint URL", + "copy-http-endpoint-url": "Αντιγραφή HTTP endpoint URL", + "http-endpoint-url-copied-message": "Το HTTP endpoint URL έχει αντιγραφεί στο πρόχειρο", + "host": "Host", + "host-required": "Απαιτείται Host.", + "host-type": "Τύπος Host", + "host-type-required": "Απαιτείται τύπος Host.", + "custom-host": "Προσαρμοσμένο host", + "custom-host-required": "Απαιτείται προσαρμοσμένο host.", + "port": "Port", + "port-required": "Απαιτείται Port.", + "port-range": "Η Port πρέπει να κυμαίνεται από 1 ως 65535.", + "connect-timeout": "Λήξη σύνδεσης (δευτερόλεπτα)", + "connect-timeout-required": "Απαιτείται λήξη σύνδεσης.", + "connect-timeout-range": "Η λήξη σύνδεσης πρέπει να κυμαίνεται από 1 ως 200.", + "client-id": "Client ID", + "clean-session": "Καθαρή συνεδρία session", + "enable-ssl": "Ενεργοποίηση SSL", + "credentials": "Διαπιστευτήρια", + "credentials-type": "Τύπος διαπιστευτηρίων", + "credentials-type-required": "Απαιτείται τύπος διαπιστευτηρίων.", + "username": "Όνομα Χρήστη", + "username-required": "Απαιτείται όνομα χρήστη.", + "password": "Κωδικός", + "password-required": "Απαιτείται κωδικός.", + "ca-cert": "Αρχείο πιστοποιητικού CA *", + "private-key": "Αρχείο ιδιωτικού κλειδιού *", + "private-key-password": "κωδικός ιδιωτικού κλειδιού", + "cert": "Αρχείο πιστοποιητικού *", + "no-file": "Δεν έχει επιλεχθεί αρχείο.", + "drop-file": "Αποθέστε αρχείο ή κάνετε κλικ για να επιλέξετε ένα αρχείο προς ανέβασμα.", + "topic-filters": "Φίτρο θέματος", + "remove-topic-filter": "Αφαίρεση φίλτρου θέματος", + "add-topic-filter": "προσθήκη φίλτρου θέματος", + "add-topic-filter-prompt": "παρακαλούμε προσθέστε φίλτρο θέματος", + "topic": "Θέμα", + "mqtt-qos": "QoS", + "mqtt-qos-at-most-once": "Το πολύ μια", + "mqtt-qos-at-least-once": "Τουλάχιστον μια", + "mqtt-qos-exactly-once": "Ακριβώς μια", + "downlink-topic-pattern": "Μοτίβο θέματος σύνδεσης", + "downlink-topic-pattern-required": "Απαιτείται μοτίβο θέματος σύνδεσης.", + "aws-iot-endpoint": "AWS IoT Endpoint", + "aws-iot-endpoint-required": "Απαιτείται AWS IoT Endpoint.", + "aws-iot-credentials": "AWS IoT Διαπιστευτήρια", + "application-credentials": "Διαπιστευτήρια Εφαρμογής", + "api-key": "API Key", + "api-key-required": "Απαιτείται API Key.", + "auth-token": "Τεκμίριο Ταυτοποίησης", + "auth-token-required": "Απαιτείται τεκμίριο ταυτοποίησης", + "region": "Περιοχή", + "region-required": "Απαιτείται περιοχή.", + "application-id": "ID Εφαρμογής", + "application-id-required": "Απαιτείται ID εφαρμογής.", + "access-key": "Κλειδί πρόσβασης", + "access-key-required": "Απαιτείται κλειδί πρόσβασης.", + "connection-parameters": "Παράμετροι σύνδεσης", + "service-bus-namespace-name": "Service Bus Namespace Name", + "service-bus-namespace-name-required": "Απαιτείται Service Bus Namespace Name.", + "event-hub-name": "Όνομα Event Hub", + "event-hub-name-required": "Απαιτείται όνομα Event Hub.", + "sas-key-name": "Όνομα SAS Key", + "sas-key-name-required": "Απαιτείται όνομα SAS Key.", + "sas-key": "SAS Key", + "sas-key-required": "Απαιτείται SAS Key.", + "iot-hub-name": "IoT Hub Name (απαιτείται για downlink)", + "metadata": "Μεταδεδομένα", + "type": "Τύπος", + "type-required": "Απαιτείται τύπος.", + "uplink-converter": "Μετατροπέας δεδομένων uplink", + "uplink-converter-required": "Απαιτείται μετατροπέας δεδομένων uplink.", + "downlink-converter": "Μετατροπέας δεδομένων downlink", + "type-http": "HTTP", + "type-ocean-connect": "OceanConnect", + "type-sigfox": "SigFox", + "type-thingpark": "ThingPark", + "type-tmobile-iot-cdp": "T-Mobile – IoT CDP", + "type-mqtt": "MQTT", + "type-aws-iot": "AWS IoT", + "type-ibm-watson-iot": "IBM Watson IoT", + "type-ttn": "TheThingsNetwork", + "type-azure-event-hub": "Azure Event Hub", + "type-opc-ua": "OPC-UA", + "opc-ua-application-name": "Όνομα εφαρμογής", + "opc-ua-application-uri": "URI Εφαρμογής", + "opc-ua-scan-period-in-seconds": "Περίοδος σάρωσης σε δευτερόλεπτα", + "opc-ua-scan-period-in-seconds-required": "Απαιτείται περίοδος σάρωσης", + "opc-ua-timeout": "Λήξη χρόνου σε χιλιοστά του δευτερολέπτου", + "opc-ua-timeout-required": "Απαιτείται λήξη χρόνου", + "opc-ua-security": "Ασφάλεια", + "opc-ua-security-required": "Απαιτειται ασφάλεια", + "opc-ua-identity": "Ταυτότητα", + "opc-ua-identity-required": "Απαιτείται ταυτότητα", + "opc-ua-keystore": "Keystore", + "add-opc-ua-keystore-prompt": "Παρακαλούμε προσθέστε αρχείο keystore", + "opc-ua-keystore-required": "Απαιτείται keystore", + "opc-ua-type": "Τύπος", + "opc-ua-keystore-type": "Τύπος", + "opc-ua-keystore-type-required": "Απαιτέιται τύπος", + "opc-ua-keystore-location": "Τοποθεσία *", + "opc-ua-keystore-password": "Κωδικός", + "opc-ua-keystore-password-required": "Απαιτείται κωδικός", + "opc-ua-keystore-alias": "Ψευδώνυμο", + "opc-ua-keystore-alias-required": "Απαιτείται ψευδώνυμο", + "opc-ua-keystore-key-password": "Κλειδί κωδικού", + "opc-ua-keystore-key-password-required": "Απαιτείται κλειδί κωδικού", + "opc-ua-mapping": "χαρτογράφιση", + "add-opc-ua-mapping-prompt": "Παρακαλούμε προσθέστε χαρτογράφιση", + "opc-ua-mapping-type": "Τύπος χαρτογράφισης", + "opc-ua-mapping-type-required": "Απαιτείται τύπος χαρτογράφισης", + "opc-ua-device-node-pattern": "Μοτίβο Κόμβου Συσκευής", + "opc-ua-device-node-pattern-required": "Απαιτείται Μοτίβο Κόμβου Συσκευής", + "opc-ua-namespace": "Namespace", + "opc-ua-add-map": "Προσθήκη στοιχείου χαρτογράφισης", + "subscription-tags": "Ετικέτες εγγραφής", + "remove-subscription-tag": "Αφαίρεση ετικέτας εγγραφής", + "add-subscription-tag": "Προσθήκη ετικέτας εγγραφής", + "add-subscription-tag-prompt": "Παρακαλούμε προσθέστε ετικέτα εγγραφής", + "key": "Κλειδί", + "path": "Μονοπάτι", + "required": "Απαιτείται" + }, + "item": { + "selected": "Επιλέχθηκε" + }, + "js-func": { + "no-return-error": "Η λειτουργία πρέπει να επιστρέφει τιμή!", + "return-type-mismatch": "Η λειτουργία πρέπει να επιστρέφει τιμή από '{{type}}' τύπο!", + "tidy": "Tidy" + }, + "key-val": { + "key": "Όνομα", + "value": "Τιμή", + "remove-entry": "Αφαίρεση καταχώρησης", + "add-entry": "Προσθήκη καταχωρησης", + "no-data": "Καμία καταχώρηση" + }, + "layout": { + "layout": "Διάταξη", + "manage": "Διαχείριση Διατάξεων", + "settings": "Ρυθμίσεις Διάταξης", + "color": "Χρώμα", + "main": "Κεντρικά", + "right": "Δεξιά", + "select": "Επιλογή διάταξης" + }, + "legend": { + "direction": "Κατεύθυνση Λεζάντας", + "position": "Θέση λεζάντας", + "show-max": "Προβολή μέγιστης τιμής", + "show-min": "Προβολή ελάχιστης τιμής", + "show-avg": "Προβολή μέσης τιμής", + "show-total": "Προβολή συνολικής τιμής", + "settings": "Ρυθμίσεις λεζάντας", + "min": "min", + "max": "max", + "avg": "Μ.Ο.", + "total": "Σύνολο" + }, + "login": { + "login": "Σύνδεση", + "request-password-reset": "Αίτημα επαναφοράς κωδικού πρόσβασης", + "reset-password": "Επαναφορά κωδικού πρόσβασης", + "create-password": "Δημιουργία κωδικού πρόσβασης", + "passwords-mismatch-error": "Οι καταχωρημένοι κωδικοί πρόσβασης πρέπει να είναι ίδιοι!", + "password-again": "Επανάληψη κωδικού πρόσβασης", + "sign-in": "Παρακαλώ, συνδεθείτε", + "username": "Όνομα Χρήστη (email)", + "remember-me": "Να με Θυμάσαι", + "forgot-password": "Ξεχάσατε τον κωδικό πρόσβασης;", + "password-reset": "Επαναφορά κωδικού πρόσβασης", + "new-password": "Νέος κωδικός πρόσβασης", + "new-password-again": "Επανάληψη νέου κωδικού πρόσβασης", + "password-link-sent-message": "Ο σύνδεσμος επαναφοράς κωδικού πρόσβασης στάλθηκε με επιτυχία!", + "email": "Email", + "no-account": "Δεν έχετε λογαριασμό;", + "create-account": "Δημιουργία λογαριασμού", + "login-with": "Σύνδεση μέσω {{name}}", + "or": "ή" + }, + "signup": { + "firstname": "Όνομα", + "lastname": "Επίθετο", + "email": "Email", + "signup": "Εγγραφή", + "create-password": "Δημιουργία κωδικού", + "repeat-password": "Επαναλάβετε τον κωδικό", + "have-account": "Έχετε ήδη έναν λογαριασμό;", + "signin": "Είσοδος", + "no-captcha-message": "Πρέπει να επιβεβαιώσετε ότι δεν είστε ρομπότ", + "password-length-message": "Ο κωδικός σας πρεπει να περιλαμβάνει τουλάχιστον 6 χαρακτήρες", + "email-verification": "Επιβεβαίωση email", + "email-verification-message": "Έχει σταλεί ένα email επιβεβαίωσης στην διεύθυνση email που καθορίσατε.
    Παρακαλούμε ακολουθήστε τις οδηγίες που συμπεριλαμβανονται στο email ώστε να ολοκληρωθεί η εγγραφή σας.
    Σημείωση: Αν δεν έχει εμφανιστεί σχετικά άμεσα το email, παρακαλούμε ελέγξτε στον φάκελο με την 'Ανεπιθύμητη Αλληλογραφία' (spam) ή ξαναπροσπαθείστε να στείλετε το email κάνοντας κλικ στο κουμπί 'Αποστολή εκ νέου'", + "account-activation-title": "Ενεργοποίηση λογαριασμού", + "account-activated": "Ο λογαριασμός ενεργοποιήθηκε με επιτυχία!", + "account-activated-text": "Συγχαρητήρια!
    Ο λογαριασμός σας έχει ενεργοποιηθεί.
    Τώρα μπορείται να κάνετε είσοδο στην πλατφόρμα.", + "resend": "Αποστολή εκ νέου", + "inactive-user-exists-title": "Ο ανενεργός χρήστης υπάρχει ήδη", + "inactive-user-exists-text": "Υπάρχει ήδη εγγεγραμμένος χρήστης με μη επιβεβαιωμένη διεύθυνση email.
    Click Πατήστε το κουμπί 'Επαναποστολή', αν θέλετε να στείλετε ξανά email επιβεβαίωσης.", + "activating-account": "Ενεργοποίηση λογαριασμού...", + "activating-account-text": "Ο λογαριασμός σας ενεργοποιείται αυτήν τη στιγμή. Παρακαλούμε περιμένετε...", + "accept-privacy-policy": "Αποδοχή Πολιτικής Απορρήτου", + "accept": "Αποδοχή", + "privacy-policy": "Πολιτική Απορρήτου" + }, + "position": { + "top": "Κορυφή", + "bottom": "Κάτω μέρος", + "left": "Αριστερά", + "right": "Δεξιά" + }, + "profile": { + "profile": "Προφίλ", + "change-password": "Αλλαγή κωδικού", + "current-password": "Τρέχων κωδικός" + }, + "relation": { + "relations": "Σχέσεις", + "direction": "Κατεύθυνση", + "search-direction": { + "FROM": "Από", + "TO": "Σε" + }, + "direction-type": { + "FROM": "από", + "TO": "σε" + }, + "from-relations": "Εξωτερικές σχέσεις", + "to-relations": "Εσωτερικές σχέσεις", + "selected-relations": "{ count, plural, 1 {1 relation} other {# relations} } επιλέχθηκαν", + "type": "Τύπος", + "to-entity-type": "Στον τύπο οντότητας", + "to-entity-name": "Στο όνομα οντότητας", + "from-entity-type": "Από τύπο οντότητας", + "from-entity-name": "Από όνομα οντότητας", + "to-entity": "Σε οντότητα", + "from-entity": "Από οντότητα", + "delete": "Διαγραφή σχέσης", + "relation-type": "Relation type", + "relation-type-required": "Απαιτείται τύπος σχέσης.", + "any-relation-type": "Οποιοσδήποτε τύπος", + "add": "Προσθήκη σχέσης", + "edit": "Επεξεργασία σχέσης", + "delete-to-relation-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε τη σχέση με την οντότητα '{{entityName}}'?", + "delete-to-relation-text": "Προσοχή, μετά την επιβεβαίωση η οντότητα '{{entityName}}' δεν θα σχετίζεται με την τρέχουσα οντότητα.", + "delete-to-relations-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 relation} other {# relations} };", + "delete-to-relations-text": "Προσοχή, μετά την επιβεβαίωση όλες οι επιλεγμένες σχέσεις θα αφαιρεθούν και οι αντίστοιχες οντότητες δεν θα σχετίζονται με την τρέχουσα οντότητα.", + "delete-from-relation-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε σχέση από την οντότητα '{{entityName}}';", + "delete-from-relation-text": "Προσοχή, μετά την επιβεβαίωση η τρέχουσα οντότητα δεν θα σχετίζεται με την οντότητα '{{entityName}}'.", + "delete-from-relations-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 relation} other {# relations} };", + "delete-from-relations-text": "Προσοχή, μετά την επιβεβαίωση όλες οι επιλεγμένες σχέσεις θα αφαιρεθούν και η τρέχουσα οντότητα δεν θα σχετίζεται με τις αντίστοιχες οντότητες.", + "remove-relation-filter": "Αφαίρεση φίλτρου σχέσης", + "add-relation-filter": "Προσθήκη φίλτρου σχέσης", + "any-relation": "Οποιαδήποτε σχέση", + "relation-filters": "Φίλτρα σχέσεων", + "additional-info": "Συμπληρωματικές πληροφορίες (JSON)", + "invalid-additional-info": "Δεν είναι δυνατή η ανάλυση των πρόσθετων πληροφοριών json." + }, + "rulechain": { + "rulechain": "Αλυσίδα Κανόνων", + "rulechains": "Κανόνες", + "root": "Ρίζα", + "delete": "Διαγραφή Αλυσίδας Κανόνων", + "name": "Όνομα", + "name-required": "Απαιτείται όνομα.", + "description": "Περιγραφή", + "add": "Προσθήκη Αλυσίδας Κανόνων", + "set-root": "Δημιουργία ριζικής Αλυσίδας Κανόνων", + "set-root-rulechain-title": "Είστε σίγουροι ότι θέλετε να δημιουργήσετε τη ριζική Αλυσίδα Κανόνων '{{ruleChainName}}' ;", + "set-root-rulechain-text": "Μετά την επιβεβαίωση, η Αλυσίδα Κανόνων θα γίνει ριζική και θα χειριστεί όλα τα εισερχόμενα μηνύματα μεταφοράς.", + "delete-rulechain-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε την Αλυσίδα Κανόνων '{{ruleChainName}}';", + "delete-rulechain-text": "Προσοχή, μετά την επιβεβαίωση η Αλυσίδα Κανόνων θα αφαιρεθεί και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-rulechains-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 rule chain} other {# rule chains} };", + "delete-rulechains-action-title": "Διαγραφή { count, plural, 1 {1 rule chain} other {# rule chains} }", + "delete-rulechains-text": "Προσοχή, μετά την επιβεβαίωση όλες οι επιλεγμένες αλυσίδες κανόνων θα καταργηθούν και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "add-rulechain-text": "Προσθήκη νέας Αλυσίδας Κανόνων", + "no-rulechains-text": "Δεν βρέθηκαν Αλυσίδες Κανόνων", + "rulechain-details": "Λεπτομέρειες Αλυσίδας Κανόνων", + "details": "Λεπομέρειες", + "events": "Γεγονότα", + "system": "Σύστημα", + "import": "Εισαγωγή Αλυσίδας Κανόνων", + "export": "Εξαγωγή Αλυσίδας Κανόνων", + "export-failed-error": "Δεν είναι δυνατή η εξαγωγή Αλυσίδας Κανόνων: {{error}}", + "create-new-rulechain": "Δημιουργία νέας Αλυσίδας Κανόνων", + "rulechain-file": "Αρχείο Αλυσίδας Κανόνων", + "invalid-rulechain-file-error": "Δεν είναι δυνατή η εισαγωγή Αλυσίδας Κανόνων: Μη έγκυρη δομή δεδομένων Αλυσίδας Κανόνων.", + "copyId": "Αντιγραφή ταυτότητας Αλυσίδας Κανόνων", + "idCopiedMessage": "Η ταυτότητα της Αλυσίδας Κανόνων έχει αντιγραφεί στο πρόχειρο", + "select-rulechain": "Επιλογή Αλυσίδας Κανόνων", + "no-rulechains-matching": "Δεν βρέθηκαν Αλυσίδες Κανόνων που να ταιριάζουν '{{entity}}'.", + "rulechain-required": "Απαιτείται Αλυσίδα Κανόνων", + "management": "Διαχείριση κανόνων", + "debug-mode": "Λειτουργία Εκσφαλμάτωσης" + }, + "rulenode": { + "details": "Λεπτομέρειες", + "events": "Γεγονότα", + "search": "Αναζήτηση κόμβων", + "open-node-library": "Άνοιγμα βιβλιοθήκης κόμβων", + "add": "Προσθήκη κανόνα κόμβου", + "name": "Όνομα", + "name-required": "Απαιτείται όνομα.", + "type": "Τύπος", + "description": "Περιγραφή", + "delete": "Διαγραφή κανόνα κόμβου", + "select-all-objects": "Επιλογή όλων των κόμβων και συνδέσεων", + "deselect-all-objects": "Αποεπιλογή όλων των κόμβων και συνδέσεων", + "delete-selected-objects": "Διαγραφή επιλεγμένων κόμβων και συνδέσεων", + "delete-selected": "Delete selected", + "select-all": "Select all", + "copy-selected": "Επιλογή αντιγραφής", + "deselect-all": "Αποεπιλογή όλων", + "rulenode-details": "Λεπτομέρειες κανόνα κόμβου", + "debug-mode": "Λειτουργία εντοπισμού σφαλμάτων", + "configuration": "Διαμόρφωση", + "link": "Σύνδεσμος", + "link-details": "Λεπτομέρειες συνδέσμου κανόνα κόμβου", + "add-link": "Προσθήκη συνδέσμου", + "link-label": "Ετικέτα συνδέσμου", + "link-label-required": "Απαιτείται ετικετα συνδέσμου.", + "custom-link-label": "Ετικέτα προσαρμοσμένου συνδέσμου", + "custom-link-label-required": "Απαιτείται ετικέτα προσαρμοσμένου συνδέσμου.", + "link-labels": "Ετικέτες συνδέσμου", + "link-labels-required": "Απαιτούνται ετικέτες συνδέσμου.", + "no-link-labels-found": "Δεν βρέθηκαν ετικέτες συνδέσμου", + "no-link-label-matching": "Δεν βρέθηκαν '{{label}}'.", + "create-new-link-label": "Δημιουργήστε μια νέα!", + "type-filter": "Φίλτρο", + "type-filter-details": "Φιλτράρετε εισερχόμενα μηνύματα με διαμορφωμένες συνθήκες", + "type-enrichment": "Εμπλουτισμός", + "type-enrichment-details": "Προσθέστε επιπλέον πληροφορίες στα Μεταδεδομένα του μηνύματος", + "type-transformation": "Μεταμόρφωση", + "type-transformation-details": "Αλλαγή ωφέλιμου φορτίου και μεταδεδομένων μηνύματος", + "type-action": "Ενέργεια", + "type-action-details": "Εκτέλεση ειδικής ενέργειας", + "type-analytics": "Analytics", + "type-analytics-details": "Εκτελέστε ανάλυση δεδομένων που διαβιβάζονται με ροή ή συνεχίζονται", + "type-external": "Εξωτερικός", + "type-external-details": "Αλληλεπίδραση με εξωτερικό σύστημα", + "type-rule-chain": "Αλυσίδα κανόνων", + "type-rule-chain-details": "Προωθεί τα εισερχόμενα μηνύματα σε συγκεκριμένη αλυσίδα κανόνων", + "type-input": "Εισαγωγήγή", + "type-input-details": "Λογική εισαγωγή της αλυσίδας κανόνων, προωθεί τα εισερχόμενα μηνύματα στον επόμενο σχετικό κανόνα κόμβου", + "type-unknown": "Άγνωστο", + "type-unknown-details": "Ανεπεξέργαστος κανόνας κόμβου", + "directive-is-not-loaded": "Δεν είναι διαθέσιμη καθορισμένη οδηγία διαμόρφωσης '{{directiveName}}'.", + "ui-resources-load-error": "Αποτυχία φόρτωσης διαμόρφωσης πόρων ui.", + "invalid-target-rulechain": "Δεν είναι δυνατή η επίλυση της αλυσίδας κανόνα στόχου!", + "test-script-function": "Λειτουργία σεναρίου δοκιμής", + "message": "Μήνυμα", + "message-type": "Τύπος μηνύματος", + "select-message-type": "Επιλογή τύπου μηνύματος", + "message-type-required": "Απαιτείται τύπος μηνύματος", + "metadata": "Μεταδεδομένα", + "metadata-required": "Οι καταχωρίσεις μεταδεδομένων δεν μπορούν να είναι κενές.", + "output": "Απόδοση", + "test": "Τεστ", + "help": "Βοήθεια", + "reset-debug-mode": "Επαναφορά λειτουργίας εντοπισμού σφαλμάτων σε όλους τους κόμβους" + }, + "role": { + "role": "Ρόλος", + "roles": "Ρόλοι", + "management": "Διαχείριση ρόλων", + "view-roles": "Προβολή ρόλων", + "no-roles-matching": "Δεν βρέθηκαν ρόλοι που να ταιριάζουν '{{entity}}'.", + "role-list": "Λίστα ρόλων", + "add": "Προσθήκη ρόλου", + "view": "Προβολή ρόλου", + "no-roles-text": "Δεν βρέθηκαν ρόλοι", + "role-details": "Λεπτομέρειες ρόλου", + "add-role-text": "Προσθήκη νέου ρόλου", + "delete": "Διαγραφή ρόλου", + "delete-roles": "Διαγραφή ρόλων", + "delete-role-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε το ρόλο '{{roleName}}';", + "delete-role-text": "Προσοχή, μετά την επιβεβαίωση ο ρόλος και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-roles-title": "Είστε σίγουροι ότι θέλετε να παίξετε { count, plural, 1 {1 role} other {# roles} };", + "delete-roles-action-title": "Διαγραφή { count, plural, 1 {1 role} other {# roles} }", + "delete-roles-text": "Προσοχή, μετά την επιβεβαίωση όλοι οι επιλεγμένοι ρόλοι θα καταργηθούν και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "role-type": "΄Τύπος ρόλου", + "role-type-required": "Απαιτείται τύπος ρόλου.", + "select-role-type": "Επιλογή τύπου ρόλου", + "enter-role-type": "Εισαγωγή τύπου ρόλου", + "any-role": "Οποιοσδήποτε ρόλος", + "no-role-types-matching": "Δεν βρέθηκαν τύποι ρόλων που να ταιριάζουν '{{entitySubtype}}'.", + "role-type-list-empty": "Δεν έγινε επιλογή τύπου ρόλου.", + "role-types": "Τύποι ρόλου", + "name": "Όνομα", + "name-required": "Απαιτείται όνομα.", + "description": "Περιγραφή", + "events": "Γεγονότα", + "details": "Λεπτομέρειες", + "copyId": "Αντιγραφή ταυτότητας ρόλου", + "idCopiedMessage": "Η ταυτότητα ρόλου έχει αντιγραφεί στο πρόχειρο", + "permissions": "Άδειες", + "role-required": "Απαιτείται ρόλος", + "display-type": { + "GENERIC": "Γενικός", + "GROUP": "Ομάδα" + } + }, + "group-permission": { + "user-group-roles": "Ρόλοι ομάδας χρηστών", + "entity-group-permissions": "Άδειες ομάδας Οντοτήτων", + "role-type": "Τύπος ρόλου", + "role-name": "Όνομα ρόλου", + "group-type": "Τύπος ομάδας", + "group-name": "Όνομα ομάδας", + "group-owner": "Κάτοχος ομάδας", + "user-group-name": "Όνομα χρήστη ομάδας", + "user-group-owner": "Κάτοχος χρήστη ομάδας", + "edit": "Επεξεργασία Αδειών", + "delete": "Διαγραφή αδειών", + "selected-group-permissions": "{ count, plural, 1 {1 group permission} other {# group permissions} } επιλέχθηκαν", + "delete-group-permission-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε την άδεια ομάδας '{{roleName}}';", + "delete-group-permission-text": "Προσοχή, μετά την επιβεβαίωση η άδεια ομάδας και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-group-permission": "Διαγραφή άδειας ομάδας", + "delete-group-permissions-title": "Έίστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 group permission} other {# group permission} };", + "delete-group-permissions-text": "Προσοχή, μετά την επιβεβαίωση όλες οι επιλεγμένες άδειες ομάδας θα καταργηθούν και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-group-permissions": "Διαγραφή αδειών ομάδας", + "add-group-permission": "Προσθήκη ομαδικής άδειας", + "edit-group-permission": "Επεξεργασία άδειας ομάδας", + "entity-group": "Ομάδα οντοτήτων", + "user-group": "Χρήστης ομάδας", + "no-owners-matching": "Δεν βρέθηκαν κάτοχοι που να ταιριάζουν '{{owner}}'.", + "target-owner-required": " Απαιτείται κάτοχος της ομάδας οντότητας.", + "target-user-group-owner-required": "Απαιτείται κάτοχος χρήστη ομάδας." + }, + "permission": { + "permissions-required": "Τουλάχιστον μία εγγραφή άδειας πρέπει να οριστεί.", + "remove-permission": "Καταργήστε την καταχώριση δικαιωμάτων", + "add-permission": "Προσθήκη καταχώρησης άδειας", + "resource": { + "resource": "Πηγή", + "select-resource": "Επιλογή πηγής", + "resource-required": "Απαιτείται πηγή", + "no-resources-matching": "Δεν βρέθηκαν πηγές που να ταιριάζουν '{{resource}}'.", + "display-type": { + "ALL": "Όλες", + "PROFILE": "Προφίλ", + "ADMIN_SETTINGS": "Ρυθμίσεις Διαχειριστή", + "ALARM": "Alarm", + "DEVICE": "Συσκευή", + "ASSET": "Asset", + "CUSTOMER": "Πελάτης", + "DASHBOARD": "Dashboard", + "ENTITY_VIEW": "Προβολή οντοτήτων", + "TENANT": "Μισθωτής", + "RULE_CHAIN": "Αλυσίδα Κανόνα", + "USER": "Χρήστης", + "WIDGETS_BUNDLE": "Δέσμη Widgets", + "WIDGET_TYPE": "Τύπος Widget", + "CONVERTER": "Μετατροπέας", + "INTEGRATION": "Ενσωμάτωση", + "SCHEDULER_EVENT": "Πρόγραμμα εκδηλώσεων", + "BLOB_ENTITY": "Δέσμη Οντότητας", + "CUSTOMER_GROUP": "Ομάδα Πελατών", + "DEVICE_GROUP": "Ομάδα Συσκευών", + "ASSET_GROUP": "Ομάδες Asset", + "USER_GROUP": "Ομάδα Χρηστών", + "ENTITY_VIEW_GROUP": "Ομάδα Οντοτήτων", + "DASHBOARD_GROUP": "Ομάδα Dashboard", + "ROLE": "Ρόλος", + "GROUP_PERMISSION": "Ομαδική Άδεια", + "WHITE_LABELING": "Εμφάνιση", + "AUDIT_LOG": "Αρχείο Ελέγχου" + } + }, + "operation": { + "operation": "Εργασία", + "operations": "Εργασίες", + "operations-required": "Τουλάχιστον μία εργασία πρέπει να καθοριστεί.", + "enter-operation": "Εισαγωγή εργασίας", + "no-operations-matching": "Δεν βρέθηκαν εργασίες που να ταιριάζουν '{{operation}}'.", + "display-type": { + "ALL": "'Ολες", + "CREATE": "Δημιουργία", + "READ": "Ανάγνωση", + "WRITE": "Εγγραφή", + "DELETE": "Διαγραφή", + "ASSIGN_TO_CUSTOMER": "Ανάθεση σε Πελάτη", + "UNASSIGN_FROM_CUSTOMER": "Αποσύνδεση από Πελάτη", + "RPC_CALL": "Κλήση RPC", + "READ_CREDENTIALS": "Ανάγνωση Διαπιστευτηρίων", + "WRITE_CREDENTIALS": "Γράψτε Διαπιστευτήρια", + "READ_ATTRIBUTES": "Ανάγνωση Χαρακτηριστικών", + "WRITE_ATTRIBUTES": "Γράψτε Χαρακτηριστικά", + "READ_TELEMETRY": "Ανάγνωση Τηλεμετρίας", + "WRITE_TELEMETRY": "Γράψτε Τηλεμετρία", + "CLAIM_DEVICES": "Αιτήματα Συσκευών", + "IMPERSONATE": "Impersonate", + "CHANGE_OWNER": "Αλλαγή Κατόχου", + "ADD_TO_GROUP": "Προσθήκη στην Ομάδα", + "REMOVE_FROM_GROUP": "Αφαίρεση από την Ομάδα" + } + } + }, + "scheduler": { + "scheduler": "Προγραμματιστής", + "scheduler-event": "Προγραμματισμένο γεγονός", + "select-scheduler-event": "Επιλέξτε προγραμματισμένα γεγονότα που να ταιριάζουν '{{entity}}'", + "scheduler-event-required": "Απαιτείται προγραμματισμένο γεγονός", + "management": "Διαχείριση Προγράμματος", + "scheduler-events": "Προγραμματισμένα γεγονότα", + "add-scheduler-event": "Προσθήκη προγραμματισμένου γεγονότος", + "search-scheduler-events": "Αναζήτηση προγραμματισμένων γεγονότων", + "created-time": "Χρόνος που δημιουργήθηκε", + "name": "Όνομα", + "type": "Τύπος", + "created_customer": "Πελάτης που δημιουργήθηκε", + "edit-scheduler-event": "Επεξεργασία προγραμματισμένου γεγονότος", + "view-scheduler-event": "Προβολή προγραμματισμένου γεγονότος", + "delete-scheduler-event": "Διαγραφή προγραμματισμένου γεγονότος", + "no-scheduler-events": "Δεν βρέθηκαν προγραμματισμένα γεγονότα", + "selected-scheduler-events": "{ count, plural, 1 {1 scheduler event} other {# scheduler events} } επιλέχθηκαν", + "delete-scheduler-event-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε το προγραμματισμένο γεγονός '{{schedulerEventName}}';", + "delete-scheduler-event-text": "Προσοχή, μετά την επιβεβαίωση το προγραμματισμένο γεγονός και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-scheduler-events-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 scheduler event} other {# scheduler events} };", + "delete-scheduler-events-text": "Προσοχή, μετά την επιβεβαίωση όλα τα επιλεγμένα προγραμματισμένα γεγονότα θα καταργηθούν και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "create": "Δημιουργία προγραμματισμένου γεγονότος", + "edit": "Επεξεργασία προγραμματισμένου γεγονότος", + "view": "Προβολή προγραμματισμένου γεγονότος", + "name-required": "Απαιτείται Όνομα", + "configuration": "Διαμόρφωση", + "schedule": "Πρόγραμμα", + "start": "Έναρξη", + "date": "Ημερομηνία", + "time": "Ώρα", + "repeat": "Επανάληψη", + "repeats": "Επαναλήψεις", + "daily": "Καθημερινά", + "weekly": "Εβδομαδιαία", + "timer": "Χρονομετρητής", + "repeats-required": "Απαιτούνται επαναλήψεις.", + "repeat-on": "Επανάληψη σε", + "repeat-every": "Επανάληψη κάθε", + "ends-on": "Τελειώνει σε", + "sunday-label": "K", + "monday-label": "Δ", + "tuesday-label": "T", + "wednesday-label": "Τ", + "thursday-label": "Π", + "friday-label": "Π", + "saturday-label": "Σ", + "repeat-on-sunday": "Επανάληψη την Κυριακή", + "repeat-on-monday": "Επανάληψη τη Δευτέρα", + "repeat-on-tuesday": "Επανάληψη την Τρίτη", + "repeat-on-wednesday": "Επανάληψη την Τετάρτη", + "repeat-on-thursday": "Επανάληψη την Πέμπτη", + "repeat-on-friday": "Επανάληψη την Παρασκευή", + "repeat-on-saturday": "Επανάληψη το Σάββατο", + "event-type": "Τύπος Γεγονότος", + "select-event-type": "Επιλογή Τύπου Γεγονότος", + "event-type-required": "Απαιτείται Τύπος Γεγονότος.", + "list-mode": "Προβολή Λίστας", + "calendar-mode": "Προβολή Ημερολογίου", + "calendar-view-type": "Προβολή τύπου ημερολογίου", + "month": "Μήνας", + "week": "Εβδομάδα", + "day": "Ημέρα", + "agenda-week": "Εβδομαδιαία Ατζέντα", + "agenda-day": "Ημερήσια Ατζέντα", + "list-year": "Λίστα Έτους", + "list-month": "Λίστα Μήνα", + "list-week": "Λίστα Εβδομάδος", + "list-day": "Λίστα Ημέρας", + "today": "Σήμερα", + "navigate-before": "Πλοηγηθείτε Πριν", + "navigate-next": "Πλοηγηθείτε Μετά", + "starting-from": "Έναρξη Από", + "until": "μέχρι", + "on": "σε", + "sunday": "Κυριακή", + "monday": "Δευτέρα", + "tuesday": "Τρίτη", + "wednesday": "Τετάρτη", + "thursday": "Πέμπτη", + "friday": "Παρασκευή", + "saturday": "Σάββατο", + "originator": "Δημιουργός" , + "single-entity": "Ενιαία Οντότητα", + "group-of-entities": "Ομάδα Οντοτήτων", + "single-device": "Ενιαία Συσκευή", + "group-of-devices": "Ομάδα Συσκευών", + "message-body": "Σώμα μηνυμάτων", + "target": "Στόχος", + "rpc-method": "Μέθοδος", + "rpc-method-required": "Απαιτείται μέθοδος", + "rpc-params": "Παράμετροι", + "select-dashboard-state": "Επιλογή κατάστασης πίνακα ελέγχου", + "hours": "Ώρες", + "minutes": "Λεπτά", + "seconds": "Δευτερόλεπτα", + "time-interval-required": "Απαιτείται χρονικό διάστημα", + "time-unit-required": "Απαιτείται μονάδα ώρας" + }, + "report": { + "report-config": "Διαμόρφωση αναφοράς", + "email-config": "Διαμόρφωση email", + "dashboard-state-param": "Τιμή παραμέτρου κατάστασης πίνακα", + "base-url": "Βάση URL", + "base-url-required": "Απαιτείται Βάση URL.", + "use-dashboard-timewindow": "Χρησιμοποιήστε το χρονικό πλαίσιο του πίνακα ελέγχου", + "timewindow": "Χρονικό πλαίσιο", + "name-pattern": "Αναφορά πρότυπου ονόματος", + "name-pattern-required": "Απαιτείται αναφορά πρότυπου ονόματος", + "type": "Τύπος αναφοράς", + "use-current-user-credentials": "Χρησιμοποιήστε τα τρέχοντα διαπιστευτήρια του χρήστη", + "customer-user-credentials": "Διαπιστευτήρια του χρήστη του πελάτη", + "customer-user-credentials-required": "Απαιτούνται διαπιστευτήρια του χρήστη του πελάτη", + "generate-test-report": "Δημιουργία αναφοράς δοκιμής", + "send-email": "Αποστολή email", + "from": "Από", + "from-required": "Απαιτείται Από.", + "to": "Σε", + "to-required": "Απαιτείται Σε.", + "cc": "Cc", + "bcc": "Bcc", + "subject": "Θέμα", + "subject-required": "Απαιτείται Θέμα.", + "body": "Σώμα", + "body-required": "Απαιτείται Σώμα." + }, + "blob-entity": { + "blob-entity": "Ογκώδης οντότητα", + "select-blob-entity": "Επιλογή ογκώδους οντότητας", + "no-blob-entities-matching": "Δεν βρέθηκαν ογκώδεις οντότητες που να ταιριάζουν '{{entity}}'.", + "blob-entity-required": "Απαιτείται ογκώδης οντότητα", + "files": "Αρχεία", + "search": "Αναζήτηση αρχείων", + "clear-search": "Εκκαθάριση αναζήτησης", + "no-blob-entities-prompt": "Δεν βρέθηκαν αρχεία", + "report": "Αναφορά", + "created-time": "Χρόνος που δημιουργήθηκε", + "name": "Όνομα", + "type": "Τύπος", + "created_customer": "Δημιουργήθηκε από Πελάτη", + "download-blob-entity": "Λήψη αρχείου", + "delete-blob-entity": "Διαγραφή αρχείου", + "delete-blob-entity-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε το αρχείο '{{blobEntityName}}';", + "delete-blob-entity-text": "Προσοχή, μετά την επιβεβαίωση τα δεδομένα αρχείου θα διαγραφούν οριστικά." + }, + "timezone": { + "timezone": "Ζώνη Ώρας", + "select-timezone": "Επιλογή Ζώνης Ώρας", + "no-timezones-matching": "Δεν βρέθηκαν Ζώνες ώρας που να ταιριάζουν'{{timezone}}' .", + "timezone-required": "Απαιτείται Ζώνη Ώρας." + }, + "tenant": { + "tenant": "Μισθωτής", + "tenants": "Μισθωτές", + "management": "Διαχείριση Μισθωτών", + "add": "Πρόσθεση Μισθωτή", + "admins": "Διαχειριστές", + "manage-tenant-admins": "Επεξεργασία των διαχειριστών του Μισθωτή", + "delete": "Διαγραφή Μισθωτή", + "add-tenant-text": "Πρόσθεση νέου Μισθωτή", + "no-tenants-text": "Δεν βρέθηκαν Μισθωτές", + "tenant-details": "Λεπτομέρειες Μισθωτή", + "delete-tenant-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε τον Μισθωτή '{{tenantTitle}}';", + "delete-tenant-text": "Προσοχή, μετά την επιβεβαίωση ο Μισθωτής και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-tenants-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 tenant} other {# tenants} };", + "delete-tenants-action-title": "Διαγραφή { count, plural, 1 {1 tenant} other {# tenants} }", + "delete-tenants-text": "Προσοχή, μετά την επιβεβαίωση όλοι οι επιλεγμένοι Μισθωτές θα αφαιρεθούν και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "title": "Τίτλος", + "title-required": "Απαιτείται Τίτλος.", + "description": "Περιγραφή", + "details": "Λεπτομέρειες", + "events": "Γεγονότα", + "copyId": "Αντιγραφή Ταυτότητας του Μισθωτή", + "idCopiedMessage": "Η Ταυτότητα του Μισθωτή έχει αντιγραφεί στο πρόχειρο", + "select-tenant": "Επιλογή Μισθωτή", + "no-tenants-matching": "Δεν βρέθηκαν Μισθωτές που να ταιριάζουν '{{entity}}'.", + "tenant-required": "Απαιτείται Μισθωτής", + "selected-tenants": "{ count, plural, 1 {1 tenant} other {# tenants} } επιλέχθηκαν", + "search": "Αναζήτηση Μισθωτών", + "allow-white-labeling": "Επιτρέπεται Προσαρμογή Εμφάνισης", + "allow-customer-white-labeling": "Επιτρέπεται Προσαρμογή Εμφάνισης Πελάτη" + }, + "timeinterval": { + "seconds-interval": "{ seconds, plural, 1 {1 second} other {# seconds} }", + "minutes-interval": "{ minutes, plural, 1 {1 minute} other {# minutes} }", + "hours-interval": "{ hours, plural, 1 {1 hour} other {# hours} }", + "days-interval": "{ days, plural, 1 {1 day} other {# days} }", + "days": "Ημέρες", + "hours": "Ώρες", + "minutes": "Λεπτά", + "seconds": "Δευτερόλεπτα", + "advanced": "Προηγμένος" + }, + "timewindow": { + "days": "{ days, plural, 1 { day } other {# days } }", + "hours": "{ hours, plural, 0 { hour } 1 {1 hour } other {# hours } }", + "minutes": "{ minutes, plural, 0 { minute } 1 {1 minute } other {# minutes } }", + "seconds": "{ seconds, plural, 0 { second } 1 {1 second } other {# seconds } }", + "realtime": "Πραγματικός Χρόνος", + "history": "Ιστορικό", + "last-prefix": "Τελευταίος", + "period": "από {{ startTime }} σε {{ endTime }}", + "edit": "Επεξεργασία Χρονικού Πλαισίου", + "date-range": "Εύρος ημερομηνιών", + "last": "Τελευταίος", + "time-period": "Χρονική Περίοδος" + }, + "user": { + "user": "Χρήστης", + "users": "Χρήστες", + "management": "Διαχείριση Χρηστών", + "customer-users": "Χρήστες του Πελάτη", + "tenant-admins": "Διαχειριστές Μισθωτή", + "sys-admin": "Διαχειριστής Συστήματος", + "tenant-admin": "Διαχειριστής Μισθωτή", + "customer": "Πελάτης", + "anonymous": "Ανώνυμος", + "add": "Προσθήκη χρήστη", + "delete": "Διαγραφή χρήστη", + "add-user-text": "Προσθήκη νέου Χρήστη", + "no-users-text": "Δεν βρέθηκαν Χρήστες", + "user-details": "Λεπτομέρειες Χρήστη", + "delete-users": "Διαγραφή Χρηστών", + "delete-user-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε το Χρήστη '{{userEmail}}'?", + "delete-user-text": "Προσοχή, μετά την επιβεβαίωση ο Χρήστης και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-users-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 user} other {# users} };", + "delete-users-action-title": "Διαγραφή { count, plural, 1 {1 user} other {# users} }", + "delete-users-text": "Προσοχή, μετά την επιβεβαίωση όλοι οι επιλεγμένοι Χρήστες θα αφαιρεθούν και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "activation-email-sent-message": "Το email ενεργοποίησης στάλθηκε με επιτυχία!", + "resend-activation": "Επανάληψη ενεργοποίησης", + "email": "Email", + "email-required": "Απαιτείται email.", + "invalid-email-format": "Μη έγκυρη μορφή email.", + "first-name": "Όνομα", + "last-name": "Επίθετο", + "description": "Περιγραφή", + "default-dashboard": "Προκαθορισμένος πίνακας ελέγχου", + "always-fullscreen": "Πάντα με πλήρη οθόνη", + "select-user": "Επιλογή Χρήστη", + "no-users-matching": "Δεν βρέθηκαν χρήστες που να ταιριάζουν '{{entity}}'.", + "user-required": "Απαιτείται Χρήστης", + "activation-method": "Μέθοδος ενεργοποίησης", + "display-activation-link": "Εμφάνιση συνδέσμου ενεργοποίησης", + "send-activation-mail": "Αποστολή mail ενεργοποίησης", + "activation-link": "Σύνδεσμος ενεργοποίησης Χρήστη", + "activation-link-text": "΄Προκειμένου να ενεργοποιήσετε το Χρήστη, χρησιμοποιήστε το εξής activation link :", + "copy-activation-link": "Αντιγραφή συνδέσμου ενεργοποίησης", + "activation-link-copied-message": "Ο σύνδεσμος ενεργοποίησης χρήστη έχει αντιγραφεί στο πρόχειρο", + "selected-users": "{ count, plural, 1 {1 user} other {# users} } επιλέχθηκαν", + "search": "Αναζήτηση Χρηστών", + "details": "Λεπτομέρειες", + "login-as-tenant-admin": "Συνδεθείτε ως Διαχειριστής Μισθωτή", + "login-as-customer-user": "Συνδεθείτε ως Χρήστης του Πελάτη", + "select-group-to-add": "Επιλέξτε την ομάδα προορισμού για να προσθέσετε επιλεγμένους Χρήστες", + "select-group-to-move": "Επιλέξτε την ομάδα προορισμού για να μετακινήσετε επιλεγμένους χρήστες", + "remove-users-from-group": "Είστε σίγουροι ότι θέλετε να καταργήσετε { count, plural, 1 {1 user} other {# users} } από την ομάδα '{entityGroup}';", + "group": "Ομάδα από Χρήστες", + "list-of-groups": "{ count, plural, 1 {One user group} other {List of # user groups} }", + "group-name-starts-with": "Ομάδες Χρηστών των οποίων τα ονόματα ξεκινούν με'{{prefix}}'" + }, + "value": { + "type": "Είδος τιμής", + "string": "Συμβολοσειρά", + "string-value": "Τιμή συμβολοσειράς", + "integer": "Ακέραιος", + "integer-value": "Ακέραια τιμή", + "invalid-integer-value": "Μη έγκυρη ακέραια τιμή", + "double": "Πραγματικός", + "double-value": "Πραγματική τιμή", + "boolean": "Λογικό", + "boolean-value": "Λογική τιμή", + "false": "Εσφαλμένος", + "true": "Αληθής", + "long": "Μακρύς" + }, + "widget": { + "widget-library": "Βιβλιοθήκη Widget", + "widget-bundle": "Δέσμη Widget", + "select-widgets-bundle": "Επιλογή δέσμης Widgets", + "management": "Διαχείριση Widget", + "editor": "Συντάκτης Widget", + "widget-type-not-found": "Πρόβλημα φόρτωσης διαμόρφωσης Widget.
    Probably associated\n widget type was removed.", + "widget-type-load-error": "Το Widget δεν φορτώθηκε λόγω των παρακάτω σφαλμάτων:", + "remove": "Αφαίρεση Widget", + "edit": "Επεξεργασία Widget", + "remove-widget-title": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε το Widget '{{widgetTitle}}'?", + "remove-widget-text": "Προσοχή, μετά την επιβεβαίωση το widget και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "timeseries": "Χρονική σειρά", + "search-data": "Αναζήτηση δεδομένων", + "no-data-found": "Δεν βρέθηκαν δεδομένα", + "latest-values": "Τελευταίες αξίες", + "rpc": "Έλεγχος Widget", + "alarm": "Alarm widget", + "static": "Στατικό widget", + "select-widget-type": "Επιλογή τύπου Widget", + "missing-widget-title-error": "Ο τίτλος Widget πρέπει να καθοριστεί!", + "widget-saved": "Το Widget αποθηκεύτηκε", + "unable-to-save-widget-error": "Δεν είναι δυνατή η αποθήκευση του Widget! Το Widget έχει σφάλματα!", + "save": "Αποθήκευση widget", + "saveAs": "Αποθήκευση widget ως", + "save-widget-type-as": "Αποθήκευση τύπου Widget type ως", + "save-widget-type-as-text": "Παρακαλούμε εισάγετε νέο τίτλο Widget και/ή επιλέξετε στοχευμένη δέσμη widgets", + "toggle-fullscreen": "Λειτουργεία πλήρους οθόνης", + "run": "Εκτέλεση Widget", + "title": "Τίτλος Widget", + "title-required": "Απαιτείται τίτλος Widget.", + "type": "Τύπος Widget", + "resources": "Πόροι", + "resource-url": "JavaScript/CSS URL", + "remove-resource": "Αφαίρεση πηγής", + "add-resource": "Προσθήκη πηγής", + "html": "HTML", + "tidy": "Τακτοποιημένος", + "css": "CSS", + "settings-schema": "Ρυθμίσεις σχήματος", + "datakey-settings-schema": "Πλήκτρο δεδομένων σχήματος ρυθμίσεων", + "javascript": "Javascript", + "remove-widget-type-title": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε τον τύπο Widget '{{widgetName}}'?", + "remove-widget-type-text": "Προσοχή, μετά την επιβεβαίωση ο τύπος Widget και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "remove-widget-type": "Αφαίρεση τύπου Widget", + "add-widget-type": "Προσθήκη νέου τύπου Widget", + "widget-type-load-failed-error": "Αποτυχία φόρτωσης τύπου Widget!", + "widget-template-load-failed-error": "Αποτυχία φόρτωσης προτύπου Widget!", + "add": "Προσθήκη Widget", + "undo": "Αναίρεση αλλαγών Widget", + "export": "Εξαγωγή Widget", + "export-data": "Εξαγωγή δεδομένων Widget", + "export-to-csv": "Εξαγωγή δεδομένων σε CSV...", + "export-to-excel": "Εξαγωγή δεδομένων σε XLS...", + "no-data": "Δεν υπάρχουν δεδομένα για εμφάνιση στο Widget" + }, + "widget-action": { + "header-button": "Κουμπί κεφαλίδας στη νέα κατάσταση του πίνακα ελέγχου", + "update-dashboard-state": "Ενημέρωση της τρέχουσας κατάστασης του dashboard", + "open-dashboard": "Πλοήγηση σε άλλο dashboard", + "custom": "Προσαρμοσμένη ενέργεια", + "target-dashboard-state": "Κατάσταση προορισμού dashboard", + "target-dashboard-state-required": "Απαιτείται κατάσταση προορισμού dashboard", + "set-entity-from-widget": "Ορισμός οντότητας από Widget", + "target-dashboard": "dashboard προορισμού", + "open-right-layout": "Ανοίξτε τη δεξιά διάταξη του dashboard (προβολή κινητού)" + }, + "widgets-bundle": { + "current": "Τρέχουσα δέσμη", + "widgets-bundles": "Δέσμες Widgets", + "add": "Προσθήκη δέσμης Widgets", + "delete": "Διαγραφή δέσμης Widgets", + "title": "Τίτλος", + "title-required": "Απαιτείται τίτλος.", + "add-widgets-bundle-text": "Προσθήκη νέας δέσμης widgets", + "no-widgets-bundles-text": "Δεν βρέθηκαν δέσμες widgets", + "empty": "Η δέσμη Widgets είναι κενή", + "details": "Λεπτομέρειες", + "widgets-bundle-details": "Λεπτομέρειες δέσμης Widgets", + "delete-widgets-bundle-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε τη δέσμη Widgets '{{widgetsBundleTitle}}';", + "delete-widgets-bundle-text": "Προσοχή, μετά την επιβεβαίωση η δέσμη Widget και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-widgets-bundles-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 widgets bundle} other {# widgets bundles} };", + "delete-widgets-bundles-action-title": "Διαγραφή { count, plural, 1 {1 widgets bundle} other {# widgets bundles} }", + "delete-widgets-bundles-text": "Προσοχή, μετά την επιβεβαίωση όλες οι επιλεγμένες δέσμες Widget και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "no-widgets-bundles-matching": "Δεν βρέθηκαν δέσμες Widgets που να ταιριάζουν'{{widgetsBundle}}'.", + "widgets-bundle-required": "Απαιτείται δέσμη Widgets.", + "system": "Σύστημα", + "import": "Εισαγωγή δέσμης Widgets", + "export": "Εξαγωγή δέσμης Widgets", + "export-failed-error": "Δεν είναι δυνατή η εξαγωγή δέσμης Widgets: {{error}}", + "create-new-widgets-bundle": "Δημιουργία νέας δέσμης Widgets", + "widgets-bundle-file": "Αρχείο δέσμης Widgets", + "invalid-widgets-bundle-file-error": "Δεν είναι δυνατή η εισαγωγή δέσμης Widgets: Μη έγκυρη δομή δεδομένων Widgets." + }, + "widget-config": { + "data": "Δεδομένα", + "settings": "Ρυθμίσεις", + "advanced": "Προηγμένος", + "title": "Τίτλος", + "general-settings": "Γενικές ρυθμίσεις", + "display-title": "Εμφάνιση τίτλου", + "drop-shadow": "Σκίαση", + "enable-fullscreen": "Ενεργοποίηση πλήρους οθόνης", + "enable-data-export": "Ενεργοποίηση εξαγωγής δεδομένων", + "background-color": "Χρώμα φόντου", + "text-color": "Χρώμα κειμένου", + "padding": "Εσωτερικό περιθώριο", + "margin": "Περιθώριο", + "widget-style": "Στυλ Widget", + "title-style": "Στυλ τίτλου", + "mobile-mode-settings": "Ρυθμίσεις λειτουργίας κινητού", + "order": "Εντολή", + "height": "Ύψος", + "units": "Ειδικό σύμβολο για εμφάνιση δίπλα στην αξία", + "decimals": "Αριθμός ψηφίων μετά το κυμαινόμενο σημείο", + "timewindow": "Timewindow", + "use-dashboard-timewindow": "Χρήση dashboard timewindow", + "display-timewindow": "Απεικόνιση timewindow", + "display-legend": "Απεικόνιση λεζάντας", + "datasources": "Πηγές δεδομένων", + "maximum-datasources": "Το μέγιστο { count, plural, 1 {1 datasource is allowed.} other {# datasources are allowed} }", + "datasource-type": "Τύπος", + "datasource-parameters": "Παράμετροι", + "remove-datasource": "Κατάργηση της πηγής δεδομένων", + "add-datasource": "Προσθήκη πηγής δεδομένων", + "target-device": "Target device", + "alarm-source": "Πηγή Alarm", + "actions": "Ενέργειες", + "action": "ενέργεια", + "add-action": "Προσθήκη ενέργειας", + "search-actions": "Αναζήτηση ενεργειών", + "action-source": "Πηγή ενέργειας", + "action-source-required": "Απαιτείται πηγή ενέργειας.", + "action-name": "Όνομα", + "action-name-required": "Απαιτείται όνομα ενέργειας.", + "action-name-not-unique": "Μια άλλη ενέργεια με το ίδιο όνομα υπάρχει ήδη.
    Το όνομα ενέργειας πρέπει να είναι μοναδικό μέσα στην ίδια πηγή ενέργειας.", + "action-icon": "Εικονίδιο", + "action-type": "Τύπος", + "action-type-required": "Απαιτείται τύπος ενέργειας.", + "edit-action": "Επεξεργασία ενέργειας", + "delete-action": "Διαγραφή ενέργειας", + "delete-action-title": "Διαγραφή ενέργειας Widget", + "delete-action-text": "Είστε σίγουροι ότι θέλετε να διαγράψετε δράση widget με όνομα '{{actionName}}';" + }, + "widget-type": { + "import": "Εισαγωγή τύπου Widget", + "export": "Εξαγωγή τύπου Widget", + "export-failed-error": "Δεν είναι δυνατή η εξαγωγή τύπου Widget: {{error}}", + "create-new-widget-type": "Δημιουργία νέου τύπου Widget", + "widget-type-file": "Αρχείο τύπου Widget", + "invalid-widget-type-file-error": "Δεν είναι δυνατή η εισαγωγή τύπου Widget: Μη έγκυρη δομή δεδομένων τύπου Widget." + }, + "self-registration": { + "self-registration": "Αυτόματη εγγραφή", + "self-registration-url": "Αυτόματη εγγραφή URL", + "captcha-site-key": "reCAPTCHA κλειδί ιστότοπου", + "captcha-secret-key": "reCAPTCHA μυστικό κλειδί", + "notification-email": "Εmail γνωστοποίησης", + "privacy-policy-text": "Κείμενο πολιτικής απορρήτου", + "text-message-page": "Μήνυμα κειμένου για τη σελίδα εγγραφής" + }, + "white-labeling": { + "white-labeling": "Εμφάνιση", + "login-white-labeling": "Εμφάνιση Σύδεσης", + "preview": "Προεπισκόπηση", + "app-title": "Τίτλος εφαρμογής", + "favicon": "Εικονίδιο Ιστότοπου", + "favicon-description": "Εικόνα *.ico, *.gif or *.png με μέγιστο μέγεθος {{kbSize}} KBytes.", + "favicon-size-error": "Η εικόνα ιστότοπου είναι πολύ μεγάλη. Μέγιστο επιτρεπόμενο μέγεθος {{kbSize}} KBytes.", + "favicon-type-error": "Μη έγκυρη μορφή αρχείου εικόνας ιστότοπου. Μόνο εικόνες ICO, GIF ή PNG γίνονται αποδεκτές.", + "drop-favicon-image": "Σύρετε ένα εικονίδιο ιστότοπου ή κάντε κλικ για να επιλέξετε ένα αρχείο για μεταφόρτωση.", + "no-favicon-image": "Δεν έχει επιλεχθεί εικονίδιο", + "logo": "Logo", + "logo-description": "Οποιαδήποτε εικόνα με μέγιστο μέγεθος {{kbSize}} KBytes.", + "logo-size-error": "Η εικόνα του λογότυπου είναι πολύ μεγάλη. Μέγιστο επιτρεπόμενο μέγεθος {{kbSize}} KBytes.", + "logo-type-error": "Μη έγκυρη μορφή αρχείου λογότυπου. Μόνο εικόνες είναι αποδεκτές.", + "drop-logo-image": "Σείρετε μια εικόνα λογότυπου ή κάντε κλικ για να επιλέξετε ένα αρχείο για μεταφόρτωση.", + "no-logo-image": "Δεν έχει επιλεγεί λογότυπο", + "logo-height": "Ύψος λογότυπου, px", + "primary-palette": "Κύρια παλέτα", + "accent-palette": "Παλέτα τονισμών", + "customize-palette": "Προσαρμογή", + "edit-palette": "Επεξεργασία παλέτας", + "save-palette": "Αποθήκευση παλέτας", + "primary-background": "Κύριο χρώμα υποβάθρου", + "secondary-background": "Δευτερεύον χρώμα υποβάθρου", + "hue1": "HUE 1", + "hue2": "HUE 2", + "hue3": "HUE 3", + "page-background-color": "Χρώμα υποβάθρου σελίδας", + "dark-foreground": "Σκοτεινό χρώμα προσκηνίου", + "domain-name": "Όνομα Domain", + "help-link-base-url": "Base url για συνδέσμους βοηθείας", + "enable-help-links": "Ενεργοποίηση συνδέσμων βοηθείας", + "error-verification-url": "Ένα όνομα domain δεν πρέπει να περιέχει σύμβολα '/' και ':'. Παράδειγμα: gprs.cloud", + "show-platform-name-version": "Εμφάνιση ονόματος και έκδοσης πλατφόρμας", + "platform-name": "Όνομα πλατφόρμας", + "platform-version": "Έκδοση πλατφόρμας", + "version-mask": "{{name}} v.{{verion}}", + "position": { + "label": "Όνομα πλατφόρμας και θέση έκδοσης", + "under-logo": "Κάτω από το λογότυπο", + "bottom": "Στο κάτω μέρος της φόρμας σύνδεσης" + } + }, + "widgets": { + "date-range-navigator": { + "localizationMap": { + "Sun": "Κυρ", + "Mon": "Δευ", + "Tue": "Τρι", + "Wed": "Τετ", + "Thu": "Πεμ", + "Fri": "Παρ", + "Sat": "Σαβ", + "Jan": "Ιαν", + "Feb": "Φεβ", + "Mar": "Μαρ", + "Apr": "Απρ", + "May": "Μάιος", + "Jun": "Ιουν", + "Jul": "Ιουλ", + "Aug": "Αυγ", + "Sep": "Σεπ", + "Oct": "Οκτ", + "Nov": "Νοε", + "Dec": "Δεκ", + "January": "Ιανουάριος", + "February": "Φεβρουάριος", + "March": "Μάρτιος", + "April": "Απρίλιος", + "June": "Ιούνιος", + "July": "Ιούλιος", + "August": "Αύγουστος", + "September": "Σεπτέμβριος", + "October": "Οκτώβριος", + "November": "Νοέμβριος", + "December": "Δεκέμβριος", + "Custom Date Range": "Προσαρμοσμένο εύρος ημερομηνιών", + "Date Range Template": "Πρότυπο εύρους ημερομηνιών", + "Today": "Σήμερα", + "Yesterday": "Χθες", + "This Week": "Αυτή την εβοδομάδα", + "Last Week": "Την προηγούμενη εβδομάδα", + "This Month": "Αυτόν τον μήνα", + "Last Month": "Τον προηγούμενο μήνα", + "Year": "Έτος", + "This Year": "Αυτό το χρόνο", + "Last Year": "Τον προηγούμενο χρόνο", + "Date picker": "Επιλογέας ημερομηνίας", + "Hour": "Ώρα", + "Day": "Ημέρα", + "Week": "Εβδομάδα", + "2 weeks": "2 Εβδομάδες", + "Month": "Μήνας", + "3 months": "3 Μήνες", + "6 months": "6 Μήνες", + "Custom interval": "Προσαρμοσμένο διάστημα", + "Interval": "Διάστημα", + "Step size": "Μέγεθος βήματος", + "Ok": "Ok" + } + } + }, + "icon": { + "icon": "Εικονίδιο", + "select-icon": "Επιλογή εικονιδίου ", + "material-icons": "Υλικά εικονίδια", + "show-all": "Προβολή όλων των εικονιδίων" + }, + "subscription": { + "entity-limit-text": "Ωστόσο, μπορείτε να αναβαθμίσετε το πρόγραμμα εγγραφής σας για να αυξήσετε τα όριά σας.", + "upgrade-your-plan": "Αναβάθμιση του σχεδίου συνδρομής", + "white-labeling-feature": "Εμφάνιση χαρακτηριστικού", + "white-labeling-text-full": "Αλλάξτε το διακριτικό τίτλο της πλατφόρμας με το λογότυπο της εταιρείας ή του προϊόντος σας και το συνδυασμό χρωμάτων σε 2 λεπτά.

    Αφαιρέστε το \"Powered By\" στο κάτω μέρος του dashboard.
    Δεν απαιτείται κώδικας ή επανεκκίνηση υπηρεσίας. Επιτρέψτε και στους πελάτες σας να αλλάξουν το περιβάλλον τους.", + "enable-white-labeling": "Ενεργοποίηση εμφάνισης χαρακτηριστικού τώρα αναβαθμίζοντας το σχέδιο εγγραφής σας!", + "read-more": "Διαβάστε περισσότερα", + "white-labeling-video-text": "Δείτε το εκπαιδευτικό βίντεο παρακάτω για να δείτε πώς λειτουργεί αυτό το χαρακτηριστικό!" + }, + "subscription-error": { + "title": "Παραβίαση συνδρομής", + "warning-title": "Προειδοποίηση συνδρομής", + "limit-reached": { + "device-count": "Έχετε φτάσει τις μέγιστες συσκευές ({{value}}) που επιτρέπονται από το πρόγραμμα εγγραφής σας!", + "asset-count": "Έχετε φτάσει τα μέγιστα assets ({{value}}) που επιτρέπονται από το πρόγραμμα εγγραφής σας!" + }, + "feature-disabled": { + "white-labeling": "Η εμφάνιση χαρακτηριστικού δεν επιτρέπεται από το σχέδιο εγγραφής σας!" + } + }, + "custom": { + "widget-action": { + "action-cell-button": "Κουμπί ενέργειας κελιών", + "row-click": "Κλικ στη σειρά", + "polygon-click": "Κλικ στο πολύγωνο", + "marker-click": "Κλικ στο δείκτη", + "tooltip-tag-action": "Ετικέτα εργαλείου ενέργειας", + "node-selected": "Στον επιλεγμένο κόμβο", + "element-click": "Κλικ στο στοιχείο HTML" + } + }, + "language": { + } +} diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json new file mode 100644 index 0000000000..8e57dd64ad --- /dev/null +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -0,0 +1,2385 @@ +{ + "access": { + "unauthorized": "Unauthorized", + "unauthorized-access": "Unauthorized Access", + "unauthorized-access-text": "You should sign in to have access to this resource!", + "access-forbidden": "Access Forbidden", + "access-forbidden-text": "You haven't access rights to this location!
    Try to sign in with different user if you still wish to gain access to this location.", + "refresh-token-expired": "Session has expired", + "refresh-token-failed": "Unable to refresh session", + "permission-denied": "Permission Denied", + "permission-denied-text": "You don't have permission to perform this operation!" + }, + "action": { + "activate": "Activate", + "suspend": "Suspend", + "save": "Save", + "saveAs": "Save as", + "cancel": "Cancel", + "ok": "OK", + "delete": "Delete", + "add": "Add", + "yes": "Yes", + "no": "No", + "update": "Update", + "remove": "Remove", + "select": "Select", + "search": "Search", + "clear-search": "Clear search", + "assign": "Assign", + "unassign": "Unassign", + "share": "Share", + "make-private": "Make private", + "apply": "Apply", + "apply-changes": "Apply changes", + "edit-mode": "Edit mode", + "enter-edit-mode": "Enter edit mode", + "decline-changes": "Decline changes", + "close": "Close", + "back": "Back", + "run": "Run", + "sign-in": "Sign in!", + "edit": "Edit", + "view": "View", + "create": "Create", + "drag": "Drag", + "refresh": "Refresh", + "undo": "Undo", + "copy": "Copy", + "paste": "Paste", + "copy-reference": "Copy reference", + "paste-reference": "Paste reference", + "import": "Import", + "export": "Export", + "share-via": "Share via {{provider}}", + "continue": "Continue", + "discard-changes": "Discard Changes", + "download": "Download", + "next-with-label": "Next: {{label}}", + "read-more": "Read more", + "hide": "Hide" + }, + "aggregation": { + "aggregation": "Aggregation", + "function": "Data aggregation function", + "limit": "Max values", + "group-interval": "Grouping interval", + "min": "Min", + "max": "Max", + "avg": "Average", + "sum": "Sum", + "count": "Count", + "none": "None" + }, + "admin": { + "general": "General", + "general-settings": "General Settings", + "outgoing-mail": "Mail Server", + "outgoing-mail-settings": "Outgoing Mail Server Settings", + "system-settings": "System Settings", + "test-mail-sent": "Test mail was successfully sent!", + "base-url": "Base URL", + "base-url-required": "Base URL is required.", + "prohibit-different-url": "Prohibit to use hostname from the client request headers", + "prohibit-different-url-hint": "This setting should be enabled for production environments. May cause security issues when disabled", + "mail-from": "Mail From", + "mail-from-required": "Mail From is required.", + "smtp-protocol": "SMTP protocol", + "smtp-host": "SMTP host", + "smtp-host-required": "SMTP host is required.", + "smtp-port": "SMTP port", + "smtp-port-required": "You must supply a smtp port.", + "smtp-port-invalid": "That doesn't look like a valid smtp port.", + "timeout-msec": "Timeout (msec)", + "timeout-required": "Timeout is required.", + "timeout-invalid": "That doesn't look like a valid timeout.", + "enable-tls": "Enable TLS", + "tls-version": "TLS version", + "enable-proxy": "Enable proxy", + "proxy-host": "Proxy host", + "proxy-host-required": "Proxy host is required.", + "proxy-port": "Proxy port", + "proxy-port-required": "Proxy port is required.", + "proxy-port-range": "Proxy port should be in a range from 1 to 65535.", + "proxy-user": "Proxy user", + "proxy-password": "Proxy password", + "send-test-mail": "Send test mail", + "security-settings": "Security settings", + "password-policy": "Password policy", + "minimum-password-length": "Minimum password length", + "minimum-password-length-required": "Minimum password length is required", + "minimum-password-length-range": "Minimum password length should be in a range from 5 to 50", + "minimum-uppercase-letters": "Minimum number of uppercase letters", + "minimum-uppercase-letters-range": "Minimum number of uppercase letters can't be negative", + "minimum-lowercase-letters": "Minimum number of lowercase letters", + "minimum-lowercase-letters-range": "Minimum number of lowercase letters can't be negative", + "minimum-digits": "Minimum number of digits", + "minimum-digits-range": "Minimum number of digits can't be negative", + "minimum-special-characters": "Minimum number of special characters", + "minimum-special-characters-range": "Minimum number of special characters can't be negative", + "password-expiration-period-days": "Password expiration period in days", + "password-expiration-period-days-range": "Password expiration period in days can't be negative", + "password-reuse-frequency-days": "Password reuse frequency in days", + "password-reuse-frequency-days-range": "Password reuse frequency in days can't be negative", + "general-policy": "General policy", + "max-failed-login-attempts": "Maximum number of failed login attempts, before account is locked", + "minimum-max-failed-login-attempts-range": "Maximum number of failed login attempts can't be negative", + "user-lockout-notification-email": "In case user account lockout, send notification to email", + "domain-name": "Domain name", + "domain-name-unique": "Domain name and protocol need to unique.", + "error-verification-url": "A domain name shouldn't contain symbols '/' and ':'. Example: thingsboard.io", + "oauth2": { + "access-token-uri": "Access token URI", + "access-token-uri-required": "Access token URI is required.", + "activate-user": "Activate user", + "add-domain": "Add domain", + "delete-domain": "Delete domain", + "add-provider": "Add provider", + "delete-provider": "Delete provider", + "allow-user-creation": "Allow user creation", + "always-fullscreen": "Always fullscreen", + "authorization-uri": "Authorization URI", + "authorization-uri-required": "Authorization URI is required.", + "client-authentication-method": "Client authentication method", + "client-id": "Client ID", + "client-id-required": "Client ID is required.", + "client-secret": "Client secret", + "client-secret-required": "Client secret is required.", + "custom-setting": "Custom settings", + "customer-name-pattern": "Customer name pattern", + "default-dashboard-name": "Default dashboard name", + "delete-domain-text": "Be careful, after the confirmation a domain and all provider data will be unavailable.", + "delete-domain-title": "Are you sure you want to delete settings the domain '{{domainName}}'?", + "delete-registration-text": "Be careful, after the confirmation a provider data will be unavailable.", + "delete-registration-title": "Are you sure you want to delete the provider '{{name}}'?", + "email-attribute-key": "Email attribute key", + "email-attribute-key-required": "Email attribute key is required.", + "first-name-attribute-key": "First name attribute key", + "general": "General", + "jwk-set-uri": "JSON Web Key URI", + "last-name-attribute-key": "Last name attribute key", + "login-button-icon": "Login button icon", + "login-button-label": "Provider label", + "login-button-label-placeholder": "Login with $(Provider label)", + "login-button-label-required": "Label is required.", + "login-provider": "Login provider", + "mapper": "Mapper", + "new-domain": "New domain", + "oauth2": "OAuth2", + "redirect-uri-template": "Redirect URI template", + "copy-redirect-uri": "Copy redirect URI", + "registration-id": "Registration ID", + "registration-id-required": "Registration ID is required.", + "registration-id-unique": "Registration ID need to unique for the system.", + "scope": "Scope", + "scope-required": "Scope is required.", + "tenant-name-pattern": "Tenant name pattern", + "tenant-name-pattern-required": "Tenant name pattern is required.", + "tenant-name-strategy": "Tenant name strategy", + "type": "Mapper type", + "uri-pattern-error": "Invalid URI format.", + "url": "URL", + "url-pattern": "Invalid URL format.", + "url-required": "URL is required.", + "user-info-uri": "User info URI", + "user-info-uri-required": "User info URI is required.", + "user-name-attribute-name": "User name attribute key", + "user-name-attribute-name-required": "User name attribute key is required", + "protocol": "Protocol", + "domain-schema-http": "HTTP", + "domain-schema-https": "HTTPS", + "domain-schema-mixed": "HTTP+HTTPS", + "enable": "Enable OAuth2 settings" + } + }, + "alarm": { + "alarm": "Alarm", + "alarms": "Alarms", + "select-alarm": "Select alarm", + "no-alarms-matching": "No alarms matching '{{entity}}' were found.", + "alarm-required": "Alarm is required", + "alarm-status": "Alarm status", + "alarm-status-list": "Alarm status list", + "any-status": "Any status", + "search-status": { + "ANY": "Any", + "ACTIVE": "Active", + "CLEARED": "Cleared", + "ACK": "Acknowledged", + "UNACK": "Unacknowledged" + }, + "display-status": { + "ACTIVE_UNACK": "Active Unacknowledged", + "ACTIVE_ACK": "Active Acknowledged", + "CLEARED_UNACK": "Cleared Unacknowledged", + "CLEARED_ACK": "Cleared Acknowledged" + }, + "no-alarms-prompt": "No alarms found", + "created-time": "Created time", + "type": "Type", + "severity": "Severity", + "originator": "Originator", + "originator-type": "Originator type", + "details": "Details", + "status": "Status", + "alarm-details": "Alarm details", + "start-time": "Start time", + "end-time": "End time", + "ack-time": "Acknowledged time", + "clear-time": "Cleared time", + "alarm-severity-list": "Alarm severity list", + "any-severity": "Any severity", + "severity-critical": "Critical", + "severity-major": "Major", + "severity-minor": "Minor", + "severity-warning": "Warning", + "severity-indeterminate": "Indeterminate", + "acknowledge": "Acknowledge", + "clear": "Clear", + "search": "Search alarms", + "selected-alarms": "{ count, plural, 1 {1 alarm} other {# alarms} } selected", + "no-data": "No data to display", + "polling-interval": "Alarms polling interval (sec)", + "polling-interval-required": "Alarms polling interval is required.", + "min-polling-interval-message": "At least 1 sec polling interval is allowed.", + "aknowledge-alarms-title": "Acknowledge { count, plural, 1 {1 alarm} other {# alarms} }", + "aknowledge-alarms-text": "Are you sure you want to acknowledge { count, plural, 1 {1 alarm} other {# alarms} }?", + "aknowledge-alarm-title": "Acknowledge Alarm", + "aknowledge-alarm-text": "Are you sure you want to acknowledge Alarm?", + "clear-alarms-title": "Clear { count, plural, 1 {1 alarm} other {# alarms} }", + "clear-alarms-text": "Are you sure you want to clear { count, plural, 1 {1 alarm} other {# alarms} }?", + "clear-alarm-title": "Clear Alarm", + "clear-alarm-text": "Are you sure you want to clear Alarm?", + "alarm-status-filter": "Alarm Status Filter", + "alarm-filter": "Alarm Filter", + "max-count-load": "Maximum number of alarms to load (0 - unlimited)", + "max-count-load-required": "Maximum number of alarms to load is required.", + "max-count-load-error-min": "Minimum value is 0.", + "fetch-size": "Fetch size", + "fetch-size-required": "Fetch size is required.", + "fetch-size-error-min": "Minimum value is 10.", + "alarm-type-list": "Alarm type list", + "any-type": "Any type", + "search-propagated-alarms": "Search propagated alarms" + }, + "alias": { + "add": "Add alias", + "edit": "Edit alias", + "name": "Alias name", + "name-required": "Alias name is required", + "duplicate-alias": "Alias with same name is already exists.", + "filter-type-single-entity": "Single entity", + "filter-type-entity-list": "Entity list", + "filter-type-entity-name": "Entity name", + "filter-type-state-entity": "Entity from dashboard state", + "filter-type-state-entity-description": "Entity taken from dashboard state parameters", + "filter-type-asset-type": "Asset type", + "filter-type-asset-type-description": "Assets of type '{{assetType}}'", + "filter-type-asset-type-and-name-description": "Assets of type '{{assetType}}' and with name starting with '{{prefix}}'", + "filter-type-device-type": "Device type", + "filter-type-device-type-description": "Devices of type '{{deviceType}}'", + "filter-type-device-type-and-name-description": "Devices of type '{{deviceType}}' and with name starting with '{{prefix}}'", + "filter-type-entity-view-type": "Entity View type", + "filter-type-entity-view-type-description": "Entity Views of type '{{entityView}}'", + "filter-type-entity-view-type-and-name-description": "Entity Views of type '{{entityView}}' and with name starting with '{{prefix}}'", + "filter-type-relations-query": "Relations query", + "filter-type-relations-query-description": "{{entities}} that have {{relationType}} relation {{direction}} {{rootEntity}}", + "filter-type-asset-search-query": "Asset search query", + "filter-type-asset-search-query-description": "Assets with types {{assetTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}", + "filter-type-device-search-query": "Device search query", + "filter-type-device-search-query-description": "Devices with types {{deviceTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}", + "filter-type-entity-view-search-query": "Entity view search query", + "filter-type-entity-view-search-query-description": "Entity views with types {{entityViewTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}", + "filter-type-apiUsageState": "Api Usage State", + "entity-filter": "Entity filter", + "resolve-multiple": "Resolve as multiple entities", + "filter-type": "Filter type", + "filter-type-required": "Filter type is required.", + "entity-filter-no-entity-matched": "No entities matching specified filter were found.", + "no-entity-filter-specified": "No entity filter specified", + "root-state-entity": "Use dashboard state entity as root", + "last-level-relation": "Fetch last level relation only", + "root-entity": "Root entity", + "state-entity-parameter-name": "State entity parameter name", + "default-state-entity": "Default state entity", + "default-entity-parameter-name": "By default", + "max-relation-level": "Max relation level", + "unlimited-level": "Unlimited level", + "state-entity": "Dashboard state entity", + "all-entities": "All entities", + "any-relation": "any" + }, + "asset": { + "asset": "Asset", + "assets": "Assets", + "management": "Asset management", + "view-assets": "View Assets", + "add": "Add Asset", + "assign-to-customer": "Assign to customer", + "assign-asset-to-customer": "Assign Asset(s) To Customer", + "assign-asset-to-customer-text": "Please select the assets to assign to the customer", + "no-assets-text": "No assets found", + "assign-to-customer-text": "Please select the customer to assign the asset(s)", + "public": "Public", + "assignedToCustomer": "Assigned to customer", + "make-public": "Make asset public", + "make-private": "Make asset private", + "unassign-from-customer": "Unassign from customer", + "delete": "Delete asset", + "asset-public": "Asset is public", + "asset-type": "Asset type", + "asset-type-required": "Asset type is required.", + "select-asset-type": "Select asset type", + "enter-asset-type": "Enter asset type", + "any-asset": "Any asset", + "no-asset-types-matching": "No asset types matching '{{entitySubtype}}' were found.", + "asset-type-list-empty": "No asset types selected.", + "asset-types": "Asset types", + "name": "Name", + "name-required": "Name is required.", + "description": "Description", + "type": "Type", + "type-required": "Type is required.", + "details": "Details", + "events": "Events", + "add-asset-text": "Add new asset", + "asset-details": "Asset details", + "assign-assets": "Assign assets", + "assign-assets-text": "Assign { count, plural, 1 {1 asset} other {# assets} } to customer", + "delete-assets": "Delete assets", + "unassign-assets": "Unassign assets", + "unassign-assets-action-title": "Unassign { count, plural, 1 {1 asset} other {# assets} } from customer", + "assign-new-asset": "Assign new asset", + "delete-asset-title": "Are you sure you want to delete the asset '{{assetName}}'?", + "delete-asset-text": "Be careful, after the confirmation the asset and all related data will become unrecoverable.", + "delete-assets-title": "Are you sure you want to delete { count, plural, 1 {1 asset} other {# assets} }?", + "delete-assets-action-title": "Delete { count, plural, 1 {1 asset} other {# assets} }", + "delete-assets-text": "Be careful, after the confirmation all selected assets will be removed and all related data will become unrecoverable.", + "make-public-asset-title": "Are you sure you want to make the asset '{{assetName}}' public?", + "make-public-asset-text": "After the confirmation the asset and all its data will be made public and accessible by others.", + "make-private-asset-title": "Are you sure you want to make the asset '{{assetName}}' private?", + "make-private-asset-text": "After the confirmation the asset and all its data will be made private and won't be accessible by others.", + "unassign-asset-title": "Are you sure you want to unassign the asset '{{assetName}}'?", + "unassign-asset-text": "After the confirmation the asset will be unassigned and won't be accessible by the customer.", + "unassign-asset": "Unassign asset", + "unassign-assets-title": "Are you sure you want to unassign { count, plural, 1 {1 asset} other {# assets} }?", + "unassign-assets-text": "After the confirmation all selected assets will be unassigned and won't be accessible by the customer.", + "copyId": "Copy asset Id", + "idCopiedMessage": "Asset Id has been copied to clipboard", + "select-asset": "Select asset", + "no-assets-matching": "No assets matching '{{entity}}' were found.", + "asset-required": "Asset is required", + "name-starts-with": "Asset name starts with", + "import": "Import assets", + "asset-file": "Asset file", + "search": "Search assets", + "selected-assets": "{ count, plural, 1 {1 asset} other {# assets} } selected", + "label": "Label" + }, + "attribute": { + "attributes": "Attributes", + "latest-telemetry": "Latest telemetry", + "attributes-scope": "Entity attributes scope", + "scope-latest-telemetry": "Latest telemetry", + "scope-client": "Client attributes", + "scope-server": "Server attributes", + "scope-shared": "Shared attributes", + "add": "Add attribute", + "key": "Key", + "last-update-time": "Last update time", + "key-required": "Attribute key is required.", + "value": "Value", + "value-required": "Attribute value is required.", + "delete-attributes-title": "Are you sure you want to delete { count, plural, 1 {1 attribute} other {# attributes} }?", + "delete-attributes-text": "Be careful, after the confirmation all selected attributes will be removed.", + "delete-attributes": "Delete attributes", + "enter-attribute-value": "Enter attribute value", + "show-on-widget": "Show on widget", + "widget-mode": "Widget mode", + "next-widget": "Next widget", + "prev-widget": "Previous widget", + "add-to-dashboard": "Add to dashboard", + "add-widget-to-dashboard": "Add widget to dashboard", + "selected-attributes": "{ count, plural, 1 {1 attribute} other {# attributes} } selected", + "selected-telemetry": "{ count, plural, 1 {1 telemetry unit} other {# telemetry units} } selected", + "no-attributes-text": "No attributes found", + "no-telemetry-text": "No telemetry found" + }, + "audit-log": { + "audit": "Audit", + "audit-logs": "Audit Logs", + "timestamp": "Timestamp", + "entity-type": "Entity Type", + "entity-name": "Entity Name", + "user": "User", + "type": "Type", + "status": "Status", + "details": "Details", + "type-added": "Added", + "type-deleted": "Deleted", + "type-updated": "Updated", + "type-attributes-updated": "Attributes updated", + "type-attributes-deleted": "Attributes deleted", + "type-rpc-call": "RPC call", + "type-credentials-updated": "Credentials updated", + "type-assigned-to-customer": "Assigned to Customer", + "type-unassigned-from-customer": "Unassigned from Customer", + "type-activated": "Activated", + "type-suspended": "Suspended", + "type-credentials-read": "Credentials read", + "type-attributes-read": "Attributes read", + "type-relation-add-or-update": "Relation updated", + "type-relation-delete": "Relation deleted", + "type-relations-delete": "All relation deleted", + "type-alarm-ack": "Acknowledged", + "type-alarm-clear": "Cleared", + "type-login": "Login", + "type-logout": "Logout", + "type-lockout": "Lockout", + "status-success": "Success", + "status-failure": "Failure", + "audit-log-details": "Audit log details", + "no-audit-logs-prompt": "No logs found", + "action-data": "Action data", + "failure-details": "Failure details", + "search": "Search audit logs", + "clear-search": "Clear search", + "type-assigned-from-tenant": "Assigned from Tenant", + "type-assigned-to-tenant": "Assigned to Tenant" + }, + "confirm-on-exit": { + "message": "You have unsaved changes. Are you sure you want to leave this page?", + "html-message": "You have unsaved changes.
    Are you sure you want to leave this page?", + "title": "Unsaved changes" + }, + "contact": { + "country": "Country", + "city": "City", + "state": "State / Province", + "postal-code": "Zip / Postal Code", + "postal-code-invalid": "Invalid Zip / Postal Code format.", + "address": "Address", + "address2": "Address 2", + "phone": "Phone", + "email": "Email", + "no-address": "No address" + }, + "common": { + "username": "Username", + "password": "Password", + "enter-username": "Enter username", + "enter-password": "Enter password", + "enter-search": "Enter search", + "created-time": "Created time", + "loading": "Loading..." + }, + "content-type": { + "json": "Json", + "text": "Text", + "binary": "Binary (Base64)" + }, + "customer": { + "customer": "Customer", + "customers": "Customers", + "management": "Customer management", + "dashboard": "Customer Dashboard", + "dashboards": "Customer Dashboards", + "devices": "Customer Devices", + "entity-views": "Customer Entity Views", + "assets": "Customer Assets", + "public-dashboards": "Public Dashboards", + "public-devices": "Public Devices", + "public-assets": "Public Assets", + "public-entity-views": "Public Entity Views", + "add": "Add Customer", + "delete": "Delete customer", + "manage-customer-users": "Manage customer users", + "manage-customer-devices": "Manage customer devices", + "manage-customer-dashboards": "Manage customer dashboards", + "manage-public-devices": "Manage public devices", + "manage-public-dashboards": "Manage public dashboards", + "manage-customer-assets": "Manage customer assets", + "manage-public-assets": "Manage public assets", + "add-customer-text": "Add new customer", + "no-customers-text": "No customers found", + "customer-details": "Customer details", + "delete-customer-title": "Are you sure you want to delete the customer '{{customerTitle}}'?", + "delete-customer-text": "Be careful, after the confirmation the customer and all related data will become unrecoverable.", + "delete-customers-title": "Are you sure you want to delete { count, plural, 1 {1 customer} other {# customers} }?", + "delete-customers-action-title": "Delete { count, plural, 1 {1 customer} other {# customers} }", + "delete-customers-text": "Be careful, after the confirmation all selected customers will be removed and all related data will become unrecoverable.", + "manage-users": "Manage users", + "manage-assets": "Manage assets", + "manage-devices": "Manage devices", + "manage-dashboards": "Manage dashboards", + "title": "Title", + "title-required": "Title is required.", + "description": "Description", + "details": "Details", + "events": "Events", + "copyId": "Copy customer Id", + "idCopiedMessage": "Customer Id has been copied to clipboard", + "select-customer": "Select customer", + "no-customers-matching": "No customers matching '{{entity}}' were found.", + "customer-required": "Customer is required", + "select-default-customer": "Select default customer", + "default-customer": "Default customer", + "default-customer-required": "Default customer is required in order to debug dashboard on Tenant level", + "search": "Search customers", + "selected-customers": "{ count, plural, 1 {1 customer} other {# customers} } selected" + }, + "datetime": { + "date-from": "Date from", + "time-from": "Time from", + "date-to": "Date to", + "time-to": "Time to" + }, + "dashboard": { + "dashboard": "Dashboard", + "dashboards": "Dashboards", + "management": "Dashboard management", + "view-dashboards": "View Dashboards", + "add": "Add Dashboard", + "assign-dashboard-to-customer": "Assign Dashboard(s) To Customer", + "assign-dashboard-to-customer-text": "Please select the dashboards to assign to the customer", + "assign-to-customer-text": "Please select the customer to assign the dashboard(s)", + "assign-to-customer": "Assign to customer", + "unassign-from-customer": "Unassign from customer", + "make-public": "Make dashboard public", + "make-private": "Make dashboard private", + "manage-assigned-customers": "Manage assigned customers", + "assigned-customers": "Assigned customers", + "assign-to-customers": "Assign Dashboard(s) To Customers", + "assign-to-customers-text": "Please select the customers to assign the dashboard(s)", + "unassign-from-customers": "Unassign Dashboard(s) From Customers", + "unassign-from-customers-text": "Please select the customers to unassign from the dashboard(s)", + "no-dashboards-text": "No dashboards found", + "no-widgets": "No widgets configured", + "add-widget": "Add new widget", + "title": "Title", + "select-widget-title": "Select widget", + "select-widget-subtitle": "List of available widget types", + "delete": "Delete dashboard", + "title-required": "Title is required.", + "description": "Description", + "details": "Details", + "dashboard-details": "Dashboard details", + "add-dashboard-text": "Add new dashboard", + "assign-dashboards": "Assign dashboards", + "assign-new-dashboard": "Assign new dashboard", + "assign-dashboards-text": "Assign { count, plural, 1 {1 dashboard} other {# dashboards} } to customers", + "unassign-dashboards-action-text": "Unassign { count, plural, 1 {1 dashboard} other {# dashboards} } from customers", + "delete-dashboards": "Delete dashboards", + "unassign-dashboards": "Unassign dashboards", + "unassign-dashboards-action-title": "Unassign { count, plural, 1 {1 dashboard} other {# dashboards} } from customer", + "delete-dashboard-title": "Are you sure you want to delete the dashboard '{{dashboardTitle}}'?", + "delete-dashboard-text": "Be careful, after the confirmation the dashboard and all related data will become unrecoverable.", + "delete-dashboards-title": "Are you sure you want to delete { count, plural, 1 {1 dashboard} other {# dashboards} }?", + "delete-dashboards-action-title": "Delete { count, plural, 1 {1 dashboard} other {# dashboards} }", + "delete-dashboards-text": "Be careful, after the confirmation all selected dashboards will be removed and all related data will become unrecoverable.", + "unassign-dashboard-title": "Are you sure you want to unassign the dashboard '{{dashboardTitle}}'?", + "unassign-dashboard-text": "After the confirmation the dashboard will be unassigned and won't be accessible by the customer.", + "unassign-dashboard": "Unassign dashboard", + "unassign-dashboards-title": "Are you sure you want to unassign { count, plural, 1 {1 dashboard} other {# dashboards} }?", + "unassign-dashboards-text": "After the confirmation all selected dashboards will be unassigned and won't be accessible by the customer.", + "public-dashboard-title": "Dashboard is now public", + "public-dashboard-text": "Your dashboard {{dashboardTitle}} is now public and accessible via next public link:", + "public-dashboard-notice": "Note: Do not forget to make related devices public in order to access their data.", + "make-private-dashboard-title": "Are you sure you want to make the dashboard '{{dashboardTitle}}' private?", + "make-private-dashboard-text": "After the confirmation the dashboard will be made private and won't be accessible by others.", + "make-private-dashboard": "Make dashboard private", + "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard", + "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard", + "select-dashboard": "Select dashboard", + "no-dashboards-matching": "No dashboards matching '{{entity}}' were found.", + "dashboard-required": "Dashboard is required.", + "select-existing": "Select existing dashboard", + "create-new": "Create new dashboard", + "new-dashboard-title": "New dashboard title", + "open-dashboard": "Open dashboard", + "set-background": "Set background", + "background-color": "Background color", + "background-image": "Background image", + "background-size-mode": "Background size mode", + "no-image": "No image selected", + "drop-image": "Drop an image or click to select a file to upload.", + "settings": "Settings", + "columns-count": "Columns count", + "columns-count-required": "Columns count is required.", + "min-columns-count-message": "Only 10 minimum column count is allowed.", + "max-columns-count-message": "Only 1000 maximum column count is allowed.", + "widgets-margins": "Margin between widgets", + "margin-required": "Margin value is required.", + "min-margin-message": "Only 0 is allowed as minimum margin value.", + "max-margin-message": "Only 50 is allowed as maximum margin value.", + "horizontal-margin": "Horizontal margin", + "horizontal-margin-required": "Horizontal margin value is required.", + "min-horizontal-margin-message": "Only 0 is allowed as minimum horizontal margin value.", + "max-horizontal-margin-message": "Only 50 is allowed as maximum horizontal margin value.", + "vertical-margin": "Vertical margin", + "vertical-margin-required": "Vertical margin value is required.", + "min-vertical-margin-message": "Only 0 is allowed as minimum vertical margin value.", + "max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value.", + "autofill-height": "Auto fill layout height", + "mobile-layout": "Mobile layout settings", + "mobile-row-height": "Mobile row height, px", + "mobile-row-height-required": "Mobile row height value is required.", + "min-mobile-row-height-message": "Only 5 pixels is allowed as minimum mobile row height value.", + "max-mobile-row-height-message": "Only 200 pixels is allowed as maximum mobile row height value.", + "display-title": "Display dashboard title", + "toolbar-always-open": "Keep toolbar opened", + "title-color": "Title color", + "display-dashboards-selection": "Display dashboards selection", + "display-entities-selection": "Display entities selection", + "display-filters": "Display filters", + "display-dashboard-timewindow": "Display timewindow", + "display-dashboard-export": "Display export", + "import": "Import dashboard", + "export": "Export dashboard", + "export-failed-error": "Unable to export dashboard: {{error}}", + "create-new-dashboard": "Create new dashboard", + "dashboard-file": "Dashboard file", + "invalid-dashboard-file-error": "Unable to import dashboard: Invalid dashboard data structure.", + "dashboard-import-missing-aliases-title": "Configure aliases used by imported dashboard", + "create-new-widget": "Create new widget", + "import-widget": "Import widget", + "widget-file": "Widget file", + "invalid-widget-file-error": "Unable to import widget: Invalid widget data structure.", + "widget-import-missing-aliases-title": "Configure aliases used by imported widget", + "open-toolbar": "Open dashboard toolbar", + "close-toolbar": "Close toolbar", + "configuration-error": "Configuration error", + "alias-resolution-error-title": "Dashboard aliases configuration error", + "invalid-aliases-config": "Unable to find any devices matching to some of the aliases filter.
    Please contact your administrator in order to resolve this issue.", + "select-devices": "Select devices", + "assignedToCustomer": "Assigned to customer", + "assignedToCustomers": "Assigned to customers", + "public": "Public", + "public-link": "Public link", + "copy-public-link": "Copy public link", + "public-link-copied-message": "Dashboard public link has been copied to clipboard", + "manage-states": "Manage dashboard states", + "states": "Dashboard states", + "search-states": "Search dashboard states", + "selected-states": "{ count, plural, 1 {1 dashboard state} other {# dashboard states} } selected", + "edit-state": "Edit dashboard state", + "delete-state": "Delete dashboard state", + "add-state": "Add dashboard state", + "no-states-text": "No states found", + "state": "Dashboard state", + "state-name": "Name", + "state-name-required": "Dashboard state name is required.", + "state-id": "State Id", + "state-id-required": "Dashboard state id is required.", + "state-id-exists": "Dashboard state with the same id is already exists.", + "is-root-state": "Root state", + "delete-state-title": "Delete dashboard state", + "delete-state-text": "Are you sure you want delete dashboard state with name '{{stateName}}'?", + "show-details": "Show details", + "hide-details": "Hide details", + "select-state": "Select target state", + "state-controller": "State controller", + "search": "Search dashboards", + "selected-dashboards": "{ count, plural, 1 {1 dashboard} other {# dashboards} } selected" + }, + "datakey": { + "settings": "Settings", + "advanced": "Advanced", + "label": "Label", + "color": "Color", + "units": "Special symbol to show next to value", + "decimals": "Number of digits after floating point", + "data-generation-func": "Data generation function", + "use-data-post-processing-func": "Use data post-processing function", + "configuration": "Data key configuration", + "timeseries": "Timeseries", + "attributes": "Attributes", + "entity-field": "Entity field", + "alarm": "Alarm fields", + "timeseries-required": "Entity timeseries are required.", + "timeseries-or-attributes-required": "Entity timeseries/attributes are required.", + "alarm-fields-timeseries-or-attributes-required": "Alarm fields or entity timeseries/attributes are required.", + "maximum-timeseries-or-attributes": "Maximum { count, plural, 1 {1 timeseries/attribute is allowed.} other {# timeseries/attributes are allowed} }", + "alarm-fields-required": "Alarm fields are required.", + "function-types": "Function types", + "function-types-required": "Function types are required.", + "maximum-function-types": "Maximum { count, plural, 1 {1 function type is allowed.} other {# function types are allowed} }", + "time-description": "timestamp of the current value;", + "value-description": "the current value;", + "prev-value-description": "result of the previous function call;", + "time-prev-description": "timestamp of the previous value;", + "prev-orig-value-description": "original previous value;" + }, + "datasource": { + "type": "Datasource type", + "name": "Name", + "add-datasource-prompt": "Please add datasource" + }, + "details": { + "details": "Details", + "edit-mode": "Edit mode", + "edit-json": "Edit JSON", + "toggle-edit-mode": "Toggle edit mode" + }, + "device": { + "device": "Device", + "device-required": "Device is required.", + "devices": "Devices", + "management": "Device management", + "view-devices": "View Devices", + "device-alias": "Device alias", + "aliases": "Device aliases", + "no-alias-matching": "'{{alias}}' not found.", + "no-aliases-found": "No aliases found.", + "no-key-matching": "'{{key}}' not found.", + "no-keys-found": "No keys found.", + "create-new-alias": "Create a new one!", + "create-new-key": "Create a new one!", + "duplicate-alias-error": "Duplicate alias found '{{alias}}'.
    Device aliases must be unique whithin the dashboard.", + "configure-alias": "Configure '{{alias}}' alias", + "no-devices-matching": "No devices matching '{{entity}}' were found.", + "alias": "Alias", + "alias-required": "Device alias is required.", + "remove-alias": "Remove device alias", + "add-alias": "Add device alias", + "name-starts-with": "Device name starts with", + "device-list": "Device list", + "use-device-name-filter": "Use filter", + "device-list-empty": "No devices selected.", + "device-name-filter-required": "Device name filter is required.", + "device-name-filter-no-device-matched": "No devices starting with '{{device}}' were found.", + "add": "Add Device", + "assign-to-customer": "Assign to customer", + "assign-device-to-customer": "Assign Device(s) To Customer", + "assign-device-to-customer-text": "Please select the devices to assign to the customer", + "make-public": "Make device public", + "make-private": "Make device private", + "no-devices-text": "No devices found", + "assign-to-customer-text": "Please select the customer to assign the device(s)", + "device-details": "Device details", + "add-device-text": "Add new device", + "credentials": "Credentials", + "manage-credentials": "Manage credentials", + "delete": "Delete device", + "assign-devices": "Assign devices", + "assign-devices-text": "Assign { count, plural, 1 {1 device} other {# devices} } to customer", + "delete-devices": "Delete devices", + "unassign-from-customer": "Unassign from customer", + "unassign-devices": "Unassign devices", + "unassign-devices-action-title": "Unassign { count, plural, 1 {1 device} other {# devices} } from customer", + "assign-new-device": "Assign new device", + "make-public-device-title": "Are you sure you want to make the device '{{deviceName}}' public?", + "make-public-device-text": "After the confirmation the device and all its data will be made public and accessible by others.", + "make-private-device-title": "Are you sure you want to make the device '{{deviceName}}' private?", + "make-private-device-text": "After the confirmation the device and all its data will be made private and won't be accessible by others.", + "view-credentials": "View credentials", + "delete-device-title": "Are you sure you want to delete the device '{{deviceName}}'?", + "delete-device-text": "Be careful, after the confirmation the device and all related data will become unrecoverable.", + "delete-devices-title": "Are you sure you want to delete { count, plural, 1 {1 device} other {# devices} }?", + "delete-devices-action-title": "Delete { count, plural, 1 {1 device} other {# devices} }", + "delete-devices-text": "Be careful, after the confirmation all selected devices will be removed and all related data will become unrecoverable.", + "unassign-device-title": "Are you sure you want to unassign the device '{{deviceName}}'?", + "unassign-device-text": "After the confirmation the device will be unassigned and won't be accessible by the customer.", + "unassign-device": "Unassign device", + "unassign-devices-title": "Are you sure you want to unassign { count, plural, 1 {1 device} other {# devices} }?", + "unassign-devices-text": "After the confirmation all selected devices will be unassigned and won't be accessible by the customer.", + "device-credentials": "Device Credentials", + "credentials-type": "Credentials type", + "access-token": "Access token", + "access-token-required": "Access token is required.", + "access-token-invalid": "Access token length must be from 1 to 20 characters.", + "rsa-key": "RSA public key", + "rsa-key-required": "RSA public key is required.", + "client-id": "Client ID", + "client-id-pattern": "Contains invalid character.", + "user-name": "User Name", + "user-name-required": "User Name is required.", + "client-id-or-user-name-necessary": "Client ID and/or User Name are necessary", + "password": "Password", + "secret": "Secret", + "secret-required": "Secret is required.", + "device-type": "Device type", + "device-type-required": "Device type is required.", + "select-device-type": "Select device type", + "enter-device-type": "Enter device type", + "any-device": "Any device", + "no-device-types-matching": "No device types matching '{{entitySubtype}}' were found.", + "device-type-list-empty": "No device types selected.", + "device-types": "Device types", + "name": "Name", + "name-required": "Name is required.", + "description": "Description", + "label": "Label", + "events": "Events", + "details": "Details", + "copyId": "Copy device Id", + "copyAccessToken": "Copy access token", + "idCopiedMessage": "Device Id has been copied to clipboard", + "accessTokenCopiedMessage": "Device access token has been copied to clipboard", + "assignedToCustomer": "Assigned to customer", + "unable-delete-device-alias-title": "Unable to delete device alias", + "unable-delete-device-alias-text": "Device alias '{{deviceAlias}}' can't be deleted as it used by the following widget(s):
    {{widgetsList}}", + "is-gateway": "Is gateway", + "public": "Public", + "device-public": "Device is public", + "select-device": "Select device", + "import": "Import device", + "device-file": "Device file", + "search": "Search devices", + "selected-devices": "{ count, plural, 1 {1 device} other {# devices} } selected", + "device-configuration": "Device configuration", + "transport-configuration": "Transport configuration", + "wizard": { + "device-wizard": "Device Wizard", + "device-details": "Device details", + "new-device-profile": "Create new device profile", + "existing-device-profile": "Select existing device profile", + "specific-configuration": "Specific configuration", + "customer-to-assign-device": "Customer to assign the device", + "add-credential": "Add credential" + } + }, + "device-profile": { + "device-profile": "Device profile", + "device-profiles": "Device profiles", + "all-device-profiles": "All", + "add": "Add device profile", + "edit": "Edit device profile", + "device-profile-details": "Device profile details", + "no-device-profiles-text": "No device profiles found", + "search": "Search device profiles", + "selected-device-profiles": "{ count, plural, 1 {1 device profile} other {# device profiles} } selected", + "no-device-profiles-matching": "No device profile matching '{{entity}}' were found.", + "device-profile-required": "Device profile is required", + "idCopiedMessage": "Device profile Id has been copied to clipboard", + "set-default": "Make device profile default", + "delete": "Delete device profile", + "copyId": "Copy device profile Id", + "new-device-profile-name": "Device profile name", + "new-device-profile-name-required": "Device profile name is required.", + "name": "Name", + "name-required": "Name is required.", + "type": "Profile type", + "type-required": "Profile type is required.", + "type-default": "Default", + "transport-type": "Transport type", + "transport-type-required": "Transport type is required.", + "transport-type-default": "Default", + "transport-type-default-hint": "Supports basic MQTT, HTTP and CoAP transport", + "transport-type-mqtt": "MQTT", + "transport-type-mqtt-hint": "Enables advanced MQTT transport settings", + "transport-type-lwm2m": "LWM2M", + "transport-type-lwm2m-hint": "LWM2M transport type", + "description": "Description", + "default": "Default", + "profile-configuration": "Profile configuration", + "transport-configuration": "Transport configuration", + "default-rule-chain": "Default rule chain", + "select-queue-hint": "Select from a drop-down list or add a custom name.", + "delete-device-profile-title": "Are you sure you want to delete the device profile '{{deviceProfileName}}'?", + "delete-device-profile-text": "Be careful, after the confirmation the device profile and all related data will become unrecoverable.", + "delete-device-profiles-title": "Are you sure you want to delete { count, plural, 1 {1 device profile} other {# device profiles} }?", + "delete-device-profiles-text": "Be careful, after the confirmation all selected device profiles will be removed and all related data will become unrecoverable.", + "set-default-device-profile-title": "Are you sure you want to make the device profile '{{deviceProfileName}}' default?", + "set-default-device-profile-text": "After the confirmation the device profile will be marked as default and will be used for new devices with no profile specified.", + "no-device-profiles-found": "No device profiles found.", + "create-new-device-profile": "Create a new one!", + "mqtt-device-topic-filters": "MQTT device topic filters", + "mqtt-device-payload-type": "MQTT device payload", + "mqtt-device-payload-type-json": "JSON", + "mqtt-device-payload-type-proto": "Protobuf", + "mqtt-payload-type-required": "Payload type is required.", + "support-level-wildcards": "Single [+] and multi-level [#] wildcards supported.", + "telemetry-topic-filter": "Telemetry topic filter", + "telemetry-topic-filter-required": "Telemetry topic filter is required.", + "attributes-topic-filter": "Attributes topic filter", + "attributes-topic-filter-required": "Attributes topic filter is required.", + "rpc-response-topic-filter": "RPC response topic filter", + "rpc-response-topic-filter-required": "RPC response topic filter is required.", + "not-valid-pattern-topic-filter": "Not valid pattern topic filter", + "not-valid-single-character": "Invalid use of a single-level wildcard character", + "not-valid-multi-character": "Invalid use of a multi-level wildcard character", + "single-level-wildcards-hint": "[+] is suitable for any topic filter level. Ex.: v1/devices/+/telemetry or +/devices/+/attributes.", + "multi-level-wildcards-hint": "[#] can replace the topic filter itself and must be the last symbol of the topic. Ex.: # or v1/devices/me/#.", + "alarm-rules": "Alarm rules", + "alarm-rules-with-count": "Alarm rules ({{count}})", + "no-alarm-rules": "No alarm rules configured", + "add-alarm-rule": "Add alarm rule", + "edit-alarm-rule": "Edit alarm rule", + "alarm-type": "Alarm type", + "alarm-type-required": "Alarm type is required.", + "alarm-type-pattern-hint": "Alarm type pattern, use ${metaKeyName} to substitute variables from metadata", + "create-alarm-pattern": "Create {{alarmType}} alarm", + "create-alarm-rules": "Create alarm rules", + "no-create-alarm-rules": "No create conditions configured", + "add-create-alarm-rule-prompt": "Please add create alarm rule", + "clear-alarm-rule": "Clear alarm rule", + "no-clear-alarm-rule": "No clear condition configured", + "add-create-alarm-rule": "Add create condition", + "add-clear-alarm-rule": "Add clear condition", + "select-alarm-severity": "Select alarm severity", + "alarm-severity-required": "Alarm severity is required.", + "condition-duration": "Condition duration", + "condition-duration-value": "Duration value", + "condition-duration-time-unit": "Time unit", + "condition-duration-value-range": "Duration value should be in a range from 1 to 2147483647.", + "condition-duration-value-pattern": "Duration value should be integers.", + "condition-duration-value-required": "Duration value is required.", + "condition-duration-time-unit-required": "Time unit is required.", + "advanced-settings": "Advanced settings", + "alarm-rule-details": "Details", + "add-alarm-rule-details": "Add details", + "propagate-alarm": "Propagate alarm", + "alarm-rule-relation-types-list": "Relation types to propagate", + "alarm-rule-relation-types-list-hint": "If Propagate relation types are not selected, alarms will be propagated without filtering by relation type.", + "alarm-details": "Alarm details", + "alarm-rule-condition": "Alarm rule condition", + "enter-alarm-rule-condition-prompt": "Please add alarm rule condition", + "edit-alarm-rule-condition": "Edit alarm rule condition", + "device-provisioning": "Device provisioning", + "provision-strategy": "Provision strategy", + "provision-strategy-required": "Provision strategy is required.", + "provision-strategy-disabled": "Disabled", + "provision-strategy-created-new": "Allow create new devices", + "provision-strategy-check-pre-provisioned": "Check pre provisioned devices", + "provision-device-key": "Provision device key", + "provision-device-key-required": "Provision device key is required.", + "provision-device-secret": "Provision device secret", + "provision-device-secret-required": "Provision device secret is required.", + "condition": "Condition", + "condition-type": "Condition type", + "condition-type-simple": "Simple", + "condition-type-duration": "Duration", + "condition-during": "During {{during}}", + "condition-type-repeating": "Repeating", + "condition-type-required": "Condition type is required.", + "condition-repeating-value": "Count of events", + "condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.", + "condition-repeating-value-pattern": "Count of events should be integers.", + "condition-repeating-value-required": "Count of events is required.", + "condition-repeat-times": "Repeats { count, plural, 1 {1 time} other {# times} }", + "schedule-type": "Scheduler type", + "schedule-type-required": "Scheduler type is required.", + "schedule": "Schedule", + "edit-schedule": "Edit alarm schedule", + "schedule-any-time": "Active all the time", + "schedule-specific-time": "Active at a specific time", + "schedule-custom": "Custom", + "schedule-day": { + "monday": "Monday", + "tuesday": "Tuesday", + "wednesday": "Wednesday", + "thursday": "Thursday", + "friday": "Friday", + "saturday": "Saturday", + "sunday": "Sunday" + }, + "schedule-days": "Days", + "schedule-time": "Time", + "schedule-time-from": "From", + "schedule-time-to": "To", + "schedule-days-of-week-required": "At least one day of week should be selected." + }, + "dialog": { + "close": "Close dialog" + }, + "direction": { + "column": "Column", + "row": "Row" + }, + "error": { + "unable-to-connect": "Unable to connect to the server! Please check your internet connection.", + "unhandled-error-code": "Unhandled error code: {{errorCode}}", + "unknown-error": "Unknown error" + }, + "entity": { + "entity": "Entity", + "entities": "Entities", + "aliases": "Entity aliases", + "entity-alias": "Entity alias", + "unable-delete-entity-alias-title": "Unable to delete entity alias", + "unable-delete-entity-alias-text": "Entity alias '{{entityAlias}}' can't be deleted as it used by the following widget(s):
    {{widgetsList}}", + "duplicate-alias-error": "Duplicate alias found '{{alias}}'.
    Entity aliases must be unique whithin the dashboard.", + "missing-entity-filter-error": "Filter is missing for alias '{{alias}}'.", + "configure-alias": "Configure '{{alias}}' alias", + "alias": "Alias", + "alias-required": "Entity alias is required.", + "remove-alias": "Remove entity alias", + "add-alias": "Add entity alias", + "entity-list": "Entity list", + "entity-type": "Entity type", + "entity-types": "Entity types", + "entity-type-list": "Entity type list", + "any-entity": "Any entity", + "enter-entity-type": "Enter entity type", + "no-entities-matching": "No entities matching '{{entity}}' were found.", + "no-entity-types-matching": "No entity types matching '{{entityType}}' were found.", + "name-starts-with": "Name starts with", + "use-entity-name-filter": "Use filter", + "entity-list-empty": "No entities selected.", + "entity-type-list-empty": "No entity types selected.", + "entity-name-filter-required": "Entity name filter is required.", + "entity-name-filter-no-entity-matched": "No entities starting with '{{entity}}' were found.", + "all-subtypes": "All", + "select-entities": "Select entities", + "no-aliases-found": "No aliases found.", + "no-alias-matching": "'{{alias}}' not found.", + "create-new-alias": "Create a new one!", + "key": "Key", + "key-name": "Key name", + "no-keys-found": "No keys found.", + "no-key-matching": "'{{key}}' not found.", + "create-new-key": "Create a new one!", + "type": "Type", + "type-required": "Entity type is required.", + "type-device": "Device", + "type-devices": "Devices", + "list-of-devices": "{ count, plural, 1 {One device} other {List of # devices} }", + "device-name-starts-with": "Devices whose names start with '{{prefix}}'", + "type-device-profile": "Device profile", + "type-device-profiles": "Device profiles", + "list-of-device-profiles": "{ count, plural, 1 {One device profile} other {List of # device profiles} }", + "device-profile-name-starts-with": "Device profiles whose names start with '{{prefix}}'", + "type-asset": "Asset", + "type-assets": "Assets", + "list-of-assets": "{ count, plural, 1 {One asset} other {List of # assets} }", + "asset-name-starts-with": "Assets whose names start with '{{prefix}}'", + "type-entity-view": "Entity View", + "type-entity-views": "Entity Views", + "list-of-entity-views": "{ count, plural, 1 {One entity view} other {List of # entity views} }", + "entity-view-name-starts-with": "Entity Views whose names start with '{{prefix}}'", + "type-rule": "Rule", + "type-rules": "Rules", + "list-of-rules": "{ count, plural, 1 {One rule} other {List of # rules} }", + "rule-name-starts-with": "Rules whose names start with '{{prefix}}'", + "type-plugin": "Plugin", + "type-plugins": "Plugins", + "list-of-plugins": "{ count, plural, 1 {One plugin} other {List of # plugins} }", + "plugin-name-starts-with": "Plugins whose names start with '{{prefix}}'", + "type-tenant": "Tenant", + "type-tenants": "Tenants", + "list-of-tenants": "{ count, plural, 1 {One tenant} other {List of # tenants} }", + "tenant-name-starts-with": "Tenants whose names start with '{{prefix}}'", + "type-tenant-profile": "Tenant profile", + "type-tenant-profiles": "Tenant profiles", + "list-of-tenant-profiles": "{ count, plural, 1 {One tenant profile} other {List of # tenant profiles} }", + "tenant-profile-name-starts-with": "Tenant profiles whose names start with '{{prefix}}'", + "type-customer": "Customer", + "type-customers": "Customers", + "list-of-customers": "{ count, plural, 1 {One customer} other {List of # customers} }", + "customer-name-starts-with": "Customers whose names start with '{{prefix}}'", + "type-user": "User", + "type-users": "Users", + "list-of-users": "{ count, plural, 1 {One user} other {List of # users} }", + "user-name-starts-with": "Users whose names start with '{{prefix}}'", + "type-dashboard": "Dashboard", + "type-dashboards": "Dashboards", + "list-of-dashboards": "{ count, plural, 1 {One dashboard} other {List of # dashboards} }", + "dashboard-name-starts-with": "Dashboards whose names start with '{{prefix}}'", + "type-alarm": "Alarm", + "type-alarms": "Alarms", + "list-of-alarms": "{ count, plural, 1 {One alarms} other {List of # alarms} }", + "alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'", + "type-rulechain": "Rule chain", + "type-rulechains": "Rule chains", + "list-of-rulechains": "{ count, plural, 1 {One rule chain} other {List of # rule chains} }", + "rulechain-name-starts-with": "Rule chains whose names start with '{{prefix}}'", + "type-rulenode": "Rule node", + "type-rulenodes": "Rule nodes", + "list-of-rulenodes": "{ count, plural, 1 {One rule node} other {List of # rule nodes} }", + "rulenode-name-starts-with": "Rule nodes whose names start with '{{prefix}}'", + "type-current-customer": "Current Customer", + "type-current-tenant": "Current Tenant", + "type-current-user": "Current User", + "type-current-user-owner": "Current User Owner", + "search": "Search entities", + "selected-entities": "{ count, plural, 1 {1 entity} other {# entities} } selected", + "entity-name": "Entity name", + "entity-label": "Entity label", + "details": "Entity details", + "no-entities-prompt": "No entities found", + "no-data": "No data to display", + "columns-to-display": "Columns to Display" + }, + "entity-field": { + "created-time": "Created time", + "name": "Name", + "type": "Type", + "first-name": "First name", + "last-name": "Last name", + "email": "Email", + "title": "Title", + "country": "Country", + "state": "State", + "city": "City", + "address": "Address", + "address2": "Address 2", + "zip": "Zip", + "phone": "Phone", + "label": "Label" + }, + "entity-view": { + "entity-view": "Entity View", + "entity-view-required": "Entity view is required.", + "entity-views": "Entity Views", + "management": "Entity View management", + "view-entity-views": "View Entity Views", + "entity-view-alias": "Entity View alias", + "aliases": "Entity View aliases", + "no-alias-matching": "'{{alias}}' not found.", + "no-aliases-found": "No aliases found.", + "no-key-matching": "'{{key}}' not found.", + "no-keys-found": "No keys found.", + "create-new-alias": "Create a new one!", + "create-new-key": "Create a new one!", + "duplicate-alias-error": "Duplicate alias found '{{alias}}'.
    Entity View aliases must be unique within the dashboard.", + "configure-alias": "Configure '{{alias}}' alias", + "no-entity-views-matching": "No entity views matching '{{entity}}' were found.", + "public": "Public", + "alias": "Alias", + "alias-required": "Entity View alias is required.", + "remove-alias": "Remove entity view alias", + "add-alias": "Add entity view alias", + "name-starts-with": "Entity View name starts with", + "entity-view-list": "Entity View list", + "use-entity-view-name-filter": "Use filter", + "entity-view-list-empty": "No entity views selected.", + "entity-view-name-filter-required": "Entity view name filter is required.", + "entity-view-name-filter-no-entity-view-matched": "No entity views starting with '{{entityView}}' were found.", + "add": "Add Entity View", + "entity-view-public": "Entity view is public", + "assign-to-customer": "Assign to customer", + "assign-entity-view-to-customer": "Assign Entity View(s) To Customer", + "assign-entity-view-to-customer-text": "Please select the entity views to assign to the customer", + "no-entity-views-text": "No entity views found", + "assign-to-customer-text": "Please select the customer to assign the entity view(s)", + "entity-view-details": "Entity view details", + "add-entity-view-text": "Add new entity view", + "delete": "Delete entity view", + "assign-entity-views": "Assign entity views", + "assign-entity-views-text": "Assign { count, plural, 1 {1 entity view} other {# entity views} } to customer", + "delete-entity-views": "Delete entity views", + "unassign-from-customer": "Unassign from customer", + "unassign-entity-views": "Unassign entity views", + "unassign-entity-views-action-title": "Unassign { count, plural, 1 {1 entity view} other {# entity views} } from customer", + "assign-new-entity-view": "Assign new entity view", + "delete-entity-view-title": "Are you sure you want to delete the entity view '{{entityViewName}}'?", + "delete-entity-view-text": "Be careful, after the confirmation the entity view and all related data will become unrecoverable.", + "delete-entity-views-title": "Are you sure you want to delete { count, plural, 1 {1 entity view} other {# entity views} }?", + "delete-entity-views-action-title": "Delete { count, plural, 1 {1 entity view} other {# entity views} }", + "delete-entity-views-text": "Be careful, after the confirmation all selected entity views will be removed and all related data will become unrecoverable.", + "unassign-entity-view-title": "Are you sure you want to unassign the entity view '{{entityViewName}}'?", + "unassign-entity-view-text": "After the confirmation the entity view will be unassigned and won't be accessible by the customer.", + "unassign-entity-view": "Unassign entity view", + "unassign-entity-views-title": "Are you sure you want to unassign { count, plural, 1 {1 entity view} other {# entity views} }?", + "unassign-entity-views-text": "After the confirmation all selected entity views will be unassigned and won't be accessible by the customer.", + "entity-view-type": "Entity View type", + "entity-view-type-required": "Entity View type is required.", + "select-entity-view-type": "Select entity view type", + "enter-entity-view-type": "Enter entity view type", + "any-entity-view": "Any entity view", + "no-entity-view-types-matching": "No entity view types matching '{{entitySubtype}}' were found.", + "entity-view-type-list-empty": "No entity view types selected.", + "entity-view-types": "Entity View types", + "created-time": "Created time", + "name": "Name", + "name-required": "Name is required.", + "description": "Description", + "events": "Events", + "details": "Details", + "copyId": "Copy entity view Id", + "idCopiedMessage": "Entity view Id has been copied to clipboard", + "assignedToCustomer": "Assigned to customer", + "unable-entity-view-device-alias-title": "Unable to delete entity view alias", + "unable-entity-view-device-alias-text": "Device alias '{{entityViewAlias}}' can't be deleted as it used by the following widget(s):
    {{widgetsList}}", + "select-entity-view": "Select entity view", + "make-public": "Make entity view public", + "make-private": "Make entity view private", + "start-date": "Start date", + "start-ts": "Start time", + "end-date": "End date", + "end-ts": "End time", + "date-limits": "Date limits", + "client-attributes": "Client attributes", + "shared-attributes": "Shared attributes", + "server-attributes": "Server attributes", + "timeseries": "Timeseries", + "client-attributes-placeholder": "Client attributes", + "shared-attributes-placeholder": "Shared attributes", + "server-attributes-placeholder": "Server attributes", + "timeseries-placeholder": "Timeseries", + "target-entity": "Target entity", + "attributes-propagation": "Attributes propagation", + "attributes-propagation-hint": "Entity View will automatically copy specified attributes from Target Entity each time you save or update this entity view. For performance reasons target entity attributes are not propagated to entity view on each attribute change. You can enable automatic propagation by configuring \"copy to view\" rule node in your rule chain and linking \"Post attributes\" and \"Attributes Updated\" messages to the new rule node.", + "timeseries-data": "Timeseries data", + "timeseries-data-hint": "Configure timeseries data keys of the target entity that will be accessible to the entity view. This timeseries data is read-only.", + "make-public-entity-view-title": "Are you sure you want to make the entity view '{{entityViewName}}' public?", + "make-public-entity-view-text": "After the confirmation the entity view and all its data will be made public and accessible by others.", + "make-private-entity-view-title": "Are you sure you want to make the entity view '{{entityViewName}}' private?", + "make-private-entity-view-text": "After the confirmation the entity view and all its data will be made private and won't be accessible by others.", + "search": "Search entity views", + "selected-entity-views": "{ count, plural, 1 {1 entity view} other {# entity views} } selected" + }, + "event": { + "event-type": "Event type", + "type-error": "Error", + "type-lc-event": "Lifecycle event", + "type-stats": "Statistics", + "type-debug-rule-node": "Debug", + "type-debug-rule-chain": "Debug", + "no-events-prompt": "No events found", + "error": "Error", + "alarm": "Alarm", + "event-time": "Event time", + "server": "Server", + "body": "Body", + "method": "Method", + "type": "Type", + "entity": "Entity", + "message-id": "Message Id", + "message-type": "Message Type", + "data-type": "Data Type", + "relation-type": "Relation Type", + "metadata": "Metadata", + "data": "Data", + "event": "Event", + "status": "Status", + "success": "Success", + "failed": "Failed", + "messages-processed": "Messages processed", + "errors-occurred": "Errors occurred" + }, + "extension": { + "extensions": "Extensions", + "selected-extensions": "{ count, plural, 1 {1 extension} other {# extensions} } selected", + "type": "Type", + "key": "Key", + "value": "Value", + "id": "Id", + "extension-id": "Extension id", + "extension-type": "Extension type", + "transformer-json": "JSON *", + "unique-id-required": "Current extension id already exists.", + "delete": "Delete extension", + "add": "Add extension", + "edit": "Edit extension", + "delete-extension-title": "Are you sure you want to delete the extension '{{extensionId}}'?", + "delete-extension-text": "Be careful, after the confirmation the extension and all related data will become unrecoverable.", + "delete-extensions-title": "Are you sure you want to delete { count, plural, 1 {1 extension} other {# extensions} }?", + "delete-extensions-text": "Be careful, after the confirmation all selected extensions will be removed.", + "converters": "Converters", + "converter-id": "Converter id", + "configuration": "Configuration", + "converter-configurations": "Converter configurations", + "token": "Security token", + "add-converter": "Add converter", + "add-config": "Add converter configuration", + "device-name-expression": "Device name expression", + "device-type-expression": "Device type expression", + "custom": "Custom", + "to-double": "To Double", + "transformer": "Transformer", + "json-required": "Transformer json is required.", + "json-parse": "Unable to parse transformer json.", + "attributes": "Attributes", + "add-attribute": "Add attribute", + "add-map": "Add mapping element", + "timeseries": "Timeseries", + "add-timeseries": "Add timeseries", + "field-required": "Field is required", + "brokers": "Brokers", + "add-broker": "Add broker", + "host": "Host", + "port": "Port", + "port-range": "Port should be in a range from 1 to 65535.", + "ssl": "Ssl", + "credentials": "Credentials", + "username": "Username", + "password": "Password", + "retry-interval": "Retry interval in milliseconds", + "anonymous": "Anonymous", + "basic": "Basic", + "pem": "PEM", + "ca-cert": "CA certificate file *", + "private-key": "Private key file *", + "cert": "Certificate file *", + "no-file": "No file selected.", + "drop-file": "Drop a file or click to select a file to upload.", + "mapping": "Mapping", + "topic-filter": "Topic filter", + "converter-type": "Converter type", + "converter-json": "Json", + "json-name-expression": "Device name json expression", + "topic-name-expression": "Device name topic expression", + "json-type-expression": "Device type json expression", + "topic-type-expression": "Device type topic expression", + "attribute-key-expression": "Attribute key expression", + "attr-json-key-expression": "Attribute key json expression", + "attr-topic-key-expression": "Attribute key topic expression", + "request-id-expression": "Request id expression", + "request-id-json-expression": "Request id json expression", + "request-id-topic-expression": "Request id topic expression", + "response-topic-expression": "Response topic expression", + "value-expression": "Value expression", + "topic": "Topic", + "timeout": "Timeout in milliseconds", + "converter-json-required": "Converter json is required.", + "converter-json-parse": "Unable to parse converter json.", + "filter-expression": "Filter expression", + "connect-requests": "Connect requests", + "add-connect-request": "Add connect request", + "disconnect-requests": "Disconnect requests", + "add-disconnect-request": "Add disconnect request", + "attribute-requests": "Attribute requests", + "add-attribute-request": "Add attribute request", + "attribute-updates": "Attribute updates", + "add-attribute-update": "Add attribute update", + "server-side-rpc": "Server side RPC", + "add-server-side-rpc-request": "Add server-side RPC request", + "device-name-filter": "Device name filter", + "attribute-filter": "Attribute filter", + "method-filter": "Method filter", + "request-topic-expression": "Request topic expression", + "response-timeout": "Response timeout in milliseconds", + "topic-expression": "Topic expression", + "client-scope": "Client scope", + "add-device": "Add device", + "opc-server": "Servers", + "opc-add-server": "Add server", + "opc-add-server-prompt": "Please add server", + "opc-application-name": "Application name", + "opc-application-uri": "Application uri", + "opc-scan-period-in-seconds": "Scan period in seconds", + "opc-security": "Security", + "opc-identity": "Identity", + "opc-keystore": "Keystore", + "opc-type": "Type", + "opc-keystore-type": "Type", + "opc-keystore-location": "Location *", + "opc-keystore-password": "Password", + "opc-keystore-alias": "Alias", + "opc-keystore-key-password": "Key password", + "opc-device-node-pattern": "Device node pattern", + "opc-device-name-pattern": "Device name pattern", + "modbus-server": "Servers/slaves", + "modbus-add-server": "Add server/slave", + "modbus-add-server-prompt": "Please add server/slave", + "modbus-transport": "Transport", + "modbus-tcp-reconnect": "Automatically reconnect", + "modbus-rtu-over-tcp": "RTU over TCP", + "modbus-port-name": "Serial port name", + "modbus-encoding": "Encoding", + "modbus-parity": "Parity", + "modbus-baudrate": "Baud rate", + "modbus-databits": "Data bits", + "modbus-stopbits": "Stop bits", + "modbus-databits-range": "Data bits should be in a range from 7 to 8.", + "modbus-stopbits-range": "Stop bits should be in a range from 1 to 2.", + "modbus-unit-id": "Unit ID", + "modbus-unit-id-range": "Unit ID should be in a range from 1 to 247.", + "modbus-device-name": "Device name", + "modbus-poll-period": "Poll period (ms)", + "modbus-attributes-poll-period": "Attributes poll period (ms)", + "modbus-timeseries-poll-period": "Timeseries poll period (ms)", + "modbus-poll-period-range": "Poll period should be positive value.", + "modbus-tag": "Tag", + "modbus-function": "Function", + "modbus-register-address": "Register address", + "modbus-register-address-range": "Register address should be in a range from 0 to 65535.", + "modbus-register-bit-index": "Bit index", + "modbus-register-bit-index-range": "Bit index should be in a range from 0 to 15.", + "modbus-register-count": "Register count", + "modbus-register-count-range": "Register count should be a positive value.", + "modbus-byte-order": "Byte order", + "sync": { + "status": "Status", + "sync": "Sync", + "not-sync": "Not sync", + "last-sync-time": "Last sync time", + "not-available": "Not available" + }, + "export-extensions-configuration": "Export extensions configuration", + "import-extensions-configuration": "Import extensions configuration", + "import-extensions": "Import extensions", + "import-extension": "Import extension", + "export-extension": "Export extension", + "file": "Extensions file", + "invalid-file-error": "Invalid extension file" + }, + "filter": { + "add": "Add filter", + "edit": "Edit filter", + "name": "Filter name", + "name-required": "Filter name is required.", + "duplicate-filter": "Filter with same name is already exists.", + "filters": "Filters", + "unable-delete-filter-title": "Unable to delete filter", + "unable-delete-filter-text": "Filter '{{filter}}' can't be deleted as it used by the following widget(s):
    {{widgetsList}}", + "duplicate-filter-error": "Duplicate filter found '{{filter}}'.
    Filters must be unique within the dashboard.", + "missing-key-filters-error": "Key filters is missing for filter '{{filter}}'.", + "filter": "Filter", + "editable": "Editable", + "no-filters-found": "No filters found.", + "no-filter-text": "No filter specified", + "add-filter-prompt": "Please add filter", + "no-filter-matching": "'{{filter}}' not found.", + "create-new-filter": "Create a new one!", + "filter-required": "Filter is required.", + "operation": { + "operation": "Operation", + "equal": "equal", + "not-equal": "not equal", + "starts-with": "starts with", + "ends-with": "ends with", + "contains": "contains", + "not-contains": "not contains", + "greater": "greater than", + "less": "less than", + "greater-or-equal": "greater or equal", + "less-or-equal": "less or equal", + "and": "and", + "or": "or" + }, + "ignore-case": "ignore case", + "value": "Value", + "remove-filter": "Remove filter", + "preview": "Filter preview", + "no-filters": "No filters configured", + "add-filter": "Add filter", + "add-complex-filter": "Add complex filter", + "add-complex": "Add complex", + "complex-filter": "Complex filter", + "edit-complex-filter": "Edit complex filter", + "edit-filter-user-params": "Edit filter predicate user parameters", + "filter-user-params": "Filter predicate user parameters", + "user-parameters": "User parameters", + "display-label": "Label to display", + "autogenerated-label": "Auto generate label", + "order-priority": "Field order priority", + "key-filter": "Key filter", + "key-filters": "Key filters", + "key-name": "Key name", + "key-name-required": "Key name is required.", + "key-type": { + "key-type": "Key type", + "attribute": "Attribute", + "timeseries": "Timeseries", + "entity-field": "Entity field" + }, + "value-type": { + "value-type": "Value type", + "string": "String", + "numeric": "Numeric", + "boolean": "Boolean", + "date-time": "Datetime" + }, + "value-type-required": "Key value type is required.", + "key-value-type-change-title": "Are you sure you want to change key value type?", + "key-value-type-change-message": "If you confirm new value type all entered key filters will be removed.", + "no-key-filters": "No key filters configured", + "add-key-filter": "Add key filter", + "remove-key-filter": "Remove key filter", + "edit-key-filter": "Edit key filter", + "date": "Date", + "time": "Time", + "current-tenant": "Current tenant", + "current-customer": "Current customer", + "current-user": "Current user", + "current-device": "Current device", + "default-value": "Default value", + "dynamic-source-type": "Dynamic source type", + "no-dynamic-value": "No dynamic value", + "source-attribute": "Source attribute", + "switch-to-dynamic-value": "Switch to dynamic value", + "switch-to-default-value": "Switch to default value" + }, + "fullscreen": { + "expand": "Expand to fullscreen", + "exit": "Exit fullscreen", + "toggle": "Toggle fullscreen mode", + "fullscreen": "Fullscreen" + }, + "function": { + "function": "Function" + }, + "gateway": { + "add-entry": "Add configuration", + "connector-add": "Add new connector", + "connector-enabled": "Enable connector", + "connector-name": "Connector name", + "connector-name-required": "Connector name is required.", + "connector-type": "Connector type", + "connector-type-required": "Connector type is required.", + "connectors": "Connectors configuration", + "create-new-gateway": "Create a new gateway", + "create-new-gateway-text": "Are you sure you want create a new gateway with name: '{{gatewayName}}'?", + "delete": "Delete configuration", + "download-tip": "Download configuration file", + "gateway": "Gateway", + "gateway-exists": "Device with same name is already exists.", + "gateway-name": "Gateway name", + "gateway-name-required": "Gateway name is required.", + "gateway-saved": "Gateway configuration successfully saved.", + "json-parse": "Not valid JSON.", + "json-required": "Field cannot be empty.", + "no-connectors": "No connectors", + "no-data": "No configurations", + "no-gateway-found": "No gateway found.", + "no-gateway-matching": " '{{item}}' not found.", + "path-logs": "Path to log files", + "path-logs-required": "Path is required.", + "remote": "Remote configuration", + "remote-logging-level": "Logging level", + "remove-entry": "Remove configuration", + "save-tip": "Save configuration file", + "security-type": "Security type", + "security-types": { + "access-token": "Access Token", + "tls": "TLS" + }, + "storage": "Storage", + "storage-max-file-records": "Maximum records in file", + "storage-max-files": "Maximum number of files", + "storage-max-files-min": "Minimum number is 1.", + "storage-max-files-pattern": "Number is not valid.", + "storage-max-files-required": "Number is required.", + "storage-max-records": "Maximum records in storage", + "storage-max-records-min": "Minimum number of records is 1.", + "storage-max-records-pattern": "Number is not valid.", + "storage-max-records-required": "Maximum records is required.", + "storage-pack-size": "Maximum event pack size", + "storage-pack-size-min": "Minimum number is 1.", + "storage-pack-size-pattern": "Number is not valid.", + "storage-pack-size-required": "Maximum event pack size is required.", + "storage-path": "Storage path", + "storage-path-required": "Storage path is required.", + "storage-type": "Storage type", + "storage-types": { + "file-storage": "File storage", + "memory-storage": "Memory storage" + }, + "thingsboard": "ThingsBoard", + "thingsboard-host": "ThingsBoard host", + "thingsboard-host-required": "Host is required.", + "thingsboard-port": "ThingsBoard port", + "thingsboard-port-max": "Maximum port number is 65535.", + "thingsboard-port-min": "Minimum port number is 1.", + "thingsboard-port-pattern": "Port is not valid.", + "thingsboard-port-required": "Port is required.", + "tidy": "Tidy", + "tidy-tip": "Tidy config JSON", + "title-connectors-json": "Connector {{typeName}} configuration", + "tls-path-ca-certificate": "Path to CA certificate on gateway", + "tls-path-client-certificate": "Path to client certificate on gateway", + "tls-path-private-key": "Path to private key on gateway", + "toggle-fullscreen": "Toggle fullscreen", + "transformer-json-config": "Configuration JSON*", + "update-config": "Add/update configuration JSON" + }, + "grid": { + "delete-item-title": "Are you sure you want to delete this item?", + "delete-item-text": "Be careful, after the confirmation this item and all related data will become unrecoverable.", + "delete-items-title": "Are you sure you want to delete { count, plural, 1 {1 item} other {# items} }?", + "delete-items-action-title": "Delete { count, plural, 1 {1 item} other {# items} }", + "delete-items-text": "Be careful, after the confirmation all selected items will be removed and all related data will become unrecoverable.", + "add-item-text": "Add new item", + "no-items-text": "No items found", + "item-details": "Item details", + "delete-item": "Delete Item", + "delete-items": "Delete Items", + "scroll-to-top": "Scroll to top" + }, + "help": { + "goto-help-page": "Go to help page" + }, + "home": { + "home": "Home", + "profile": "Profile", + "logout": "Logout", + "menu": "Menu", + "avatar": "Avatar", + "open-user-menu": "Open user menu" + }, + "import": { + "no-file": "No file selected", + "drop-file": "Drop a JSON file or click to select a file to upload.", + "drop-file-csv": "Drop a CSV file or click to select a file to upload.", + "column-value": "Value", + "column-title": "Title", + "column-example": "Example value data", + "column-key": "Attribute/telemetry key", + "csv-delimiter": "CSV delimiter", + "csv-first-line-header": "First line contains column names", + "csv-update-data": "Update attributes/telemetry", + "import-csv-number-columns-error": "A file should contain at least two columns", + "import-csv-invalid-format-error": "Invalid file format. Line: '{{line}}'", + "column-type": { + "name": "Name", + "type": "Type", + "label": "Label", + "column-type": "Column type", + "client-attribute": "Client attribute", + "shared-attribute": "Shared attribute", + "server-attribute": "Server attribute", + "timeseries": "Timeseries", + "entity-field": "Entity field", + "access-token": "Access token", + "isgateway": "Is Gateway", + "description": "Description" + }, + "stepper-text":{ + "select-file": "Select a file", + "configuration": "Import configuration", + "column-type": "Select columns type", + "creat-entities": "Creating new entities", + "done": "Done" + }, + "message": { + "create-entities": "{{count}} new entities were successfully created.", + "update-entities": "{{count}} entities were successfully updated.", + "error-entities": "There was an error creating {{count}} entities." + } + }, + "item": { + "selected": "Selected" + }, + "js-func": { + "no-return-error": "Function must return value!", + "return-type-mismatch": "Function must return value of '{{type}}' type!", + "tidy": "Tidy", + "mini": "Mini" + }, + "key-val": { + "key": "Key", + "value": "Value", + "remove-entry": "Remove entry", + "add-entry": "Add entry", + "no-data": "No entries" + }, + "layout": { + "layout": "Layout", + "manage": "Manage layouts", + "settings": "Layout settings", + "color": "Color", + "main": "Main", + "right": "Right", + "select": "Select target layout" + }, + "legend": { + "direction": "Legend direction", + "position": "Legend position", + "sort-legend": "Sort datakeys in legend", + "show-max": "Show max value", + "show-min": "Show min value", + "show-avg": "Show average value", + "show-total": "Show total value", + "settings": "Legend settings", + "min": "min", + "max": "max", + "avg": "avg", + "total": "total", + "comparison-time-ago": { + "days": "(day ago)", + "weeks": "(week ago)", + "months": "(month ago)", + "years": "(year ago)" + } + }, + "login": { + "login": "Login", + "request-password-reset": "Request Password Reset", + "reset-password": "Reset Password", + "create-password": "Create Password", + "passwords-mismatch-error": "Entered passwords must be same!", + "password-again": "Password again", + "sign-in": "Please sign in", + "username": "Username (email)", + "remember-me": "Remember me", + "forgot-password": "Forgot Password?", + "password-reset": "Password reset", + "expired-password-reset-message": "Your credentials has been expired! Please create new password.", + "new-password": "New password", + "new-password-again": "New password again", + "password-link-sent-message": "Password reset link was successfully sent!", + "email": "Email", + "login-with": "Login with {{name}}", + "or": "or", + "error": "Login error" + }, + "position": { + "top": "Top", + "bottom": "Bottom", + "left": "Left", + "right": "Right" + }, + "profile": { + "profile": "Profile", + "last-login-time": "Last Login", + "change-password": "Change Password", + "current-password": "Current password" + }, + "relation": { + "relations": "Relations", + "direction": "Direction", + "search-direction": { + "FROM": "From", + "TO": "To" + }, + "direction-type": { + "FROM": "from", + "TO": "to" + }, + "from-relations": "Outbound relations", + "to-relations": "Inbound relations", + "selected-relations": "{ count, plural, 1 {1 relation} other {# relations} } selected", + "type": "Type", + "to-entity-type": "To entity type", + "to-entity-name": "To entity name", + "from-entity-type": "From entity type", + "from-entity-name": "From entity name", + "to-entity": "To entity", + "from-entity": "From entity", + "delete": "Delete relation", + "relation-type": "Relation type", + "relation-type-required": "Relation type is required.", + "any-relation-type": "Any type", + "add": "Add relation", + "edit": "Edit relation", + "delete-to-relation-title": "Are you sure you want to delete relation to the entity '{{entityName}}'?", + "delete-to-relation-text": "Be careful, after the confirmation the entity '{{entityName}}' will be unrelated from the current entity.", + "delete-to-relations-title": "Are you sure you want to delete { count, plural, 1 {1 relation} other {# relations} }?", + "delete-to-relations-text": "Be careful, after the confirmation all selected relations will be removed and corresponding entities will be unrelated from the current entity.", + "delete-from-relation-title": "Are you sure you want to delete relation from the entity '{{entityName}}'?", + "delete-from-relation-text": "Be careful, after the confirmation current entity will be unrelated from the entity '{{entityName}}'.", + "delete-from-relations-title": "Are you sure you want to delete { count, plural, 1 {1 relation} other {# relations} }?", + "delete-from-relations-text": "Be careful, after the confirmation all selected relations will be removed and current entity will be unrelated from the corresponding entities.", + "remove-relation-filter": "Remove relation filter", + "add-relation-filter": "Add relation filter", + "any-relation": "Any relation", + "relation-filters": "Relation filters", + "additional-info": "Additional info (JSON)", + "invalid-additional-info": "Unable to parse additional info json.", + "no-relations-text": "No relations found" + }, + "rulechain": { + "rulechain": "Rule chain", + "rulechains": "Rule chains", + "root": "Root", + "delete": "Delete rule chain", + "name": "Name", + "name-required": "Name is required.", + "description": "Description", + "add": "Add Rule Chain", + "set-root": "Make rule chain root", + "set-root-rulechain-title": "Are you sure you want to make the rule chain '{{ruleChainName}}' root?", + "set-root-rulechain-text": "After the confirmation the rule chain will become root and will handle all incoming transport messages.", + "delete-rulechain-title": "Are you sure you want to delete the rule chain '{{ruleChainName}}'?", + "delete-rulechain-text": "Be careful, after the confirmation the rule chain and all related data will become unrecoverable.", + "delete-rulechains-title": "Are you sure you want to delete { count, plural, 1 {1 rule chain} other {# rule chains} }?", + "delete-rulechains-action-title": "Delete { count, plural, 1 {1 rule chain} other {# rule chains} }", + "delete-rulechains-text": "Be careful, after the confirmation all selected rule chains will be removed and all related data will become unrecoverable.", + "add-rulechain-text": "Add new rule chain", + "no-rulechains-text": "No rule chains found", + "rulechain-details": "Rule chain details", + "details": "Details", + "events": "Events", + "system": "System", + "import": "Import rule chain", + "export": "Export rule chain", + "export-failed-error": "Unable to export rule chain: {{error}}", + "create-new-rulechain": "Create new rule chain", + "rulechain-file": "Rule chain file", + "invalid-rulechain-file-error": "Unable to import rule chain: Invalid rule chain data structure.", + "copyId": "Copy rule chain Id", + "idCopiedMessage": "Rule chain Id has been copied to clipboard", + "select-rulechain": "Select rule chain", + "no-rulechains-matching": "No rule chains matching '{{entity}}' were found.", + "rulechain-required": "Rule chain is required", + "management": "Rules management", + "debug-mode": "Debug mode", + "search": "Search rule chains", + "selected-rulechains": "{ count, plural, 1 {1 rule chain} other {# rule chains} } selected", + "open-rulechain": "Open rule chain" + }, + "rulenode": { + "details": "Details", + "events": "Events", + "search": "Search nodes", + "open-node-library": "Open node library", + "add": "Add rule node", + "name": "Name", + "name-required": "Name is required.", + "type": "Type", + "description": "Description", + "delete": "Delete rule node", + "select-all-objects": "Select all nodes and connections", + "deselect-all-objects": "Deselect all nodes and connections", + "delete-selected-objects": "Delete selected nodes and connections", + "delete-selected": "Delete selected", + "select-all": "Select all", + "copy-selected": "Copy selected", + "deselect-all": "Deselect all", + "rulenode-details": "Rule node details", + "debug-mode": "Debug mode", + "configuration": "Configuration", + "link": "Link", + "link-details": "Rule node link details", + "add-link": "Add link", + "link-label": "Link label", + "link-label-required": "Link label is required.", + "custom-link-label": "Custom link label", + "custom-link-label-required": "Custom link label is required.", + "link-labels": "Link labels", + "link-labels-required": "Link labels is required.", + "no-link-labels-found": "No link labels found", + "no-link-label-matching": "'{{label}}' not found.", + "create-new-link-label": "Create a new one!", + "type-filter": "Filter", + "type-filter-details": "Filter incoming messages with configured conditions", + "type-enrichment": "Enrichment", + "type-enrichment-details": "Add additional information into Message Metadata", + "type-transformation": "Transformation", + "type-transformation-details": "Change Message payload and Metadata", + "type-action": "Action", + "type-action-details": "Perform special action", + "type-external": "External", + "type-external-details": "Interacts with external system", + "type-rule-chain": "Rule Chain", + "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain", + "type-input": "Input", + "type-input-details": "Logical input of Rule Chain, forwards incoming messages to next related Rule Node", + "type-unknown": "Unknown", + "type-unknown-details": "Unresolved Rule Node", + "directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.", + "ui-resources-load-error": "Failed to load configuration ui resources.", + "invalid-target-rulechain": "Unable to resolve target rule chain!", + "test-script-function": "Test script function", + "message": "Message", + "message-type": "Message type", + "select-message-type": "Select message type", + "message-type-required": "Message type is required", + "metadata": "Metadata", + "metadata-required": "Metadata entries can't be empty.", + "output": "Output", + "test": "Test", + "help": "Help", + "reset-debug-mode": "Reset debug mode in all nodes" + }, + "timezone": { + "timezone": "Timezone", + "select-timezone": "Select timezone", + "no-timezones-matching": "No timezones matching '{{timezone}}' were found.", + "timezone-required": "Timezone is required." + }, + "queue": { + "select_name": "Select queue name", + "name": "Queue Name", + "name_required": "Queue name is required!" + }, + "tenant": { + "tenant": "Tenant", + "tenants": "Tenants", + "management": "Tenant management", + "add": "Add Tenant", + "admins": "Admins", + "manage-tenant-admins": "Manage tenant admins", + "delete": "Delete tenant", + "add-tenant-text": "Add new tenant", + "no-tenants-text": "No tenants found", + "tenant-details": "Tenant details", + "delete-tenant-title": "Are you sure you want to delete the tenant '{{tenantTitle}}'?", + "delete-tenant-text": "Be careful, after the confirmation the tenant and all related data will become unrecoverable.", + "delete-tenants-title": "Are you sure you want to delete { count, plural, 1 {1 tenant} other {# tenants} }?", + "delete-tenants-action-title": "Delete { count, plural, 1 {1 tenant} other {# tenants} }", + "delete-tenants-text": "Be careful, after the confirmation all selected tenants will be removed and all related data will become unrecoverable.", + "title": "Title", + "title-required": "Title is required.", + "description": "Description", + "details": "Details", + "events": "Events", + "copyId": "Copy tenant Id", + "idCopiedMessage": "Tenant Id has been copied to clipboard", + "select-tenant": "Select tenant", + "no-tenants-matching": "No tenants matching '{{entity}}' were found.", + "tenant-required": "Tenant is required", + "search": "Search tenants", + "selected-tenants": "{ count, plural, 1 {1 tenant} other {# tenants} } selected", + "isolated-tb-core": "Processing in isolated ThingsBoard Core container", + "isolated-tb-rule-engine": "Processing in isolated ThingsBoard Rule Engine container", + "isolated-tb-core-details": "Requires separate microservice(s) per isolated Tenant", + "isolated-tb-rule-engine-details": "Requires separate microservice(s) per isolated Tenant" + }, + "tenant-profile": { + "tenant-profile": "Tenant profile", + "tenant-profiles": "Tenant profiles", + "add": "Add tenant profile", + "edit": "Edit tenant profile", + "tenant-profile-details": "Tenant profile details", + "no-tenant-profiles-text": "No tenant profiles found", + "search": "Search tenant profiles", + "selected-tenant-profiles": "{ count, plural, 1 {1 tenant profile} other {# tenant profiles} } selected", + "no-tenant-profiles-matching": "No tenant profile matching '{{entity}}' were found.", + "tenant-profile-required": "Tenant profile is required", + "idCopiedMessage": "Tenant profile Id has been copied to clipboard", + "set-default": "Make tenant profile default", + "delete": "Delete tenant profile", + "copyId": "Copy tenant profile Id", + "name": "Name", + "name-required": "Name is required.", + "data": "Profile data", + "profile-configuration": "Profile configuration", + "description": "Description", + "default": "Default", + "delete-tenant-profile-title": "Are you sure you want to delete the tenant profile '{{tenantProfileName}}'?", + "delete-tenant-profile-text": "Be careful, after the confirmation the tenant profile and all related data will become unrecoverable.", + "delete-tenant-profiles-title": "Are you sure you want to delete { count, plural, 1 {1 tenant profile} other {# tenant profiles} }?", + "delete-tenant-profiles-text": "Be careful, after the confirmation all selected tenant profiles will be removed and all related data will become unrecoverable.", + "set-default-tenant-profile-title": "Are you sure you want to make the tenant profile '{{tenantProfileName}}' default?", + "set-default-tenant-profile-text": "After the confirmation the tenant profile will be marked as default and will be used for new tenants with no profile specified.", + "no-tenant-profiles-found": "No tenant profiles found.", + "create-new-tenant-profile": "Create a new one!", + "maximum-devices": "Maximum number of devices (0 - unlimited)", + "maximum-devices-required": "Maximum number of devices is required.", + "maximum-devices-range": "Minimum number of devices can't be negative", + "maximum-assets": "Maximum number of assets (0 - unlimited)", + "maximum-assets-required": "Maximum number of assets is required.", + "maximum-assets-range": "Maximum number of assets can't be negative", + "transport-tenant-msg-rate-limit": "Transport tenant messages rate limit.", + "transport-tenant-telemetry-msg-rate-limit": "Transport tenant telemetry messages rate limit.", + "transport-tenant-telemetry-data-points-rate-limit": "Transport tenant telemetry data points rate limit.", + "transport-device-msg-rate-limit": "Transport device messages rate limit.", + "transport-device-telemetry-msg-rate-limit": "Transport device telemetry messages rate limit.", + "transport-device-telemetry-data-points-rate-limit": "Transport device telemetry data points rate limit.", + "max-transport-messages": "Maximum number of transport messages (0 - unlimited)", + "max-transport-messages-required": "Maximum number of transport messages is required.", + "max-transport-messages-range": "Maximum number of transport messages can't be negative", + "max-transport-data-points": "Maximum number of transport data points (0 - unlimited)", + "max-transport-data-points-required": "Maximum number of transport data points is required.", + "max-transport-data-points-range": "Minimum number of transport data points can't be negative", + "max-r-e-executions": "Maximum number of Rule Engine executions (0 - unlimited)", + "max-r-e-executions-required": "Maximum number of Rule Engine executions is required.", + "max-r-e-executions-range": "Minimum number of Rule Engine executions can't be negative", + "max-j-s-executions": "Maximum number of JavaScript executions (0 - unlimited)", + "max-j-s-executions-required": "Maximum number of JavaScript executions is required.", + "max-j-s-executions-range": "Minimum number of JavaScript executions can't be negative", + "max-d-p-storage-days": "Maximum number of data points storage days (0 - unlimited)", + "max-d-p-storage-days-required": "Maximum number of data points storage days is required.", + "max-d-p-storage-days-range": "Minimum number of data points storage days can't be negative", + "max-rule-node-executions-per-message": "Maximum number of rule node executions per message (0 - unlimited)", + "max-rule-node-executions-per-message-required": "Maximum number of rule node executions per message is required.", + "max-rule-node-executions-per-message-range": "Minimum number of rule node executions per message can't be negative" + }, + "timeinterval": { + "seconds-interval": "{ seconds, plural, 1 {1 second} other {# seconds} }", + "minutes-interval": "{ minutes, plural, 1 {1 minute} other {# minutes} }", + "hours-interval": "{ hours, plural, 1 {1 hour} other {# hours} }", + "days-interval": "{ days, plural, 1 {1 day} other {# days} }", + "days": "Days", + "hours": "Hours", + "minutes": "Minutes", + "seconds": "Seconds", + "advanced": "Advanced" + }, + "timeunit": { + "seconds": "Seconds", + "minutes": "Minutes", + "hours": "Hours", + "days": "Days" + }, + "timewindow": { + "days": "{ days, plural, 1 { day } other {# days } }", + "hours": "{ hours, plural, 0 { hour } 1 {1 hour } other {# hours } }", + "minutes": "{ minutes, plural, 0 { minute } 1 {1 minute } other {# minutes } }", + "seconds": "{ seconds, plural, 0 { second } 1 {1 second } other {# seconds } }", + "realtime": "Realtime", + "history": "History", + "last-prefix": "last", + "period": "from {{ startTime }} to {{ endTime }}", + "edit": "Edit timewindow", + "date-range": "Date range", + "last": "Last", + "time-period": "Time period", + "hide": "Hide" + }, + "user": { + "user": "User", + "users": "Users", + "customer-users": "Customer Users", + "tenant-admins": "Tenant Admins", + "sys-admin": "System administrator", + "tenant-admin": "Tenant administrator", + "customer": "Customer", + "anonymous": "Anonymous", + "add": "Add User", + "delete": "Delete user", + "add-user-text": "Add new user", + "no-users-text": "No users found", + "user-details": "User details", + "delete-user-title": "Are you sure you want to delete the user '{{userEmail}}'?", + "delete-user-text": "Be careful, after the confirmation the user and all related data will become unrecoverable.", + "delete-users-title": "Are you sure you want to delete { count, plural, 1 {1 user} other {# users} }?", + "delete-users-action-title": "Delete { count, plural, 1 {1 user} other {# users} }", + "delete-users-text": "Be careful, after the confirmation all selected users will be removed and all related data will become unrecoverable.", + "activation-email-sent-message": "Activation email was successfully sent!", + "resend-activation": "Resend activation", + "email": "Email", + "email-required": "Email is required.", + "invalid-email-format": "Invalid email format.", + "first-name": "First Name", + "last-name": "Last Name", + "description": "Description", + "default-dashboard": "Default dashboard", + "always-fullscreen": "Always fullscreen", + "select-user": "Select user", + "no-users-matching": "No users matching '{{entity}}' were found.", + "user-required": "User is required", + "activation-method": "Activation method", + "display-activation-link": "Display activation link", + "send-activation-mail": "Send activation mail", + "activation-link": "User activation link", + "activation-link-text": "In order to activate user use the following activation link :", + "copy-activation-link": "Copy activation link", + "activation-link-copied-message": "User activation link has been copied to clipboard", + "details": "Details", + "login-as-tenant-admin": "Login as Tenant Admin", + "login-as-customer-user": "Login as Customer User", + "search": "Search users", + "selected-users": "{ count, plural, 1 {1 user} other {# users} } selected", + "disable-account": "Disable User Account", + "enable-account": "Enable User Account", + "enable-account-message": "User account was successfully enabled!", + "disable-account-message": "User account was successfully disabled!" + }, + "value": { + "type": "Value type", + "string": "String", + "string-value": "String value", + "string-value-required": "String value is required", + "integer": "Integer", + "integer-value": "Integer value", + "integer-value-required": "Integer value is required", + "invalid-integer-value": "Invalid integer value", + "double": "Double", + "double-value": "Double value", + "double-value-required": "Double value is required", + "boolean": "Boolean", + "boolean-value": "Boolean value", + "false": "False", + "true": "True", + "long": "Long", + "json": "JSON", + "json-value": "JSON value", + "json-value-invalid": "JSON value has an invalid format", + "json-value-required": "JSON value is required." + }, + "widget": { + "widget-library": "Widgets Library", + "widget-bundle": "Widgets Bundle", + "select-widgets-bundle": "Select widgets bundle", + "management": "Widget management", + "editor": "Widget Editor", + "widget-type-not-found": "Problem loading widget configuration.
    Probably associated\n widget type was removed.", + "widget-type-load-error": "Widget wasn't loaded due to the following errors:", + "remove": "Remove widget", + "edit": "Edit widget", + "remove-widget-title": "Are you sure you want to remove the widget '{{widgetTitle}}'?", + "remove-widget-text": "After the confirmation the widget and all related data will become unrecoverable.", + "timeseries": "Time series", + "search-data": "Search data", + "no-data-found": "No data found", + "latest-values": "Latest values", + "rpc": "Control widget", + "alarm": "Alarm widget", + "static": "Static widget", + "select-widget-type": "Select widget type", + "missing-widget-title-error": "Widget title must be specified!", + "widget-saved": "Widget saved", + "unable-to-save-widget-error": "Unable to save widget! Widget has errors!", + "save": "Save widget", + "saveAs": "Save widget as", + "save-widget-type-as": "Save widget type as", + "save-widget-type-as-text": "Please enter new widget title and/or select target widgets bundle", + "toggle-fullscreen": "Toggle fullscreen", + "run": "Run widget", + "title": "Widget title", + "title-required": "Widget title is required.", + "type": "Widget type", + "resources": "Resources", + "resource-url": "JavaScript/CSS URL", + "resource-is-module": "Is module", + "remove-resource": "Remove resource", + "add-resource": "Add resource", + "html": "HTML", + "tidy": "Tidy", + "css": "CSS", + "settings-schema": "Settings schema", + "datakey-settings-schema": "Data key settings schema", + "javascript": "Javascript", + "js": "JS", + "remove-widget-type-title": "Are you sure you want to remove the widget type '{{widgetName}}'?", + "remove-widget-type-text": "After the confirmation the widget type and all related data will become unrecoverable.", + "remove-widget-type": "Remove widget type", + "add-widget-type": "Add new widget type", + "widget-type-load-failed-error": "Failed to load widget type!", + "widget-template-load-failed-error": "Failed to load widget template!", + "add": "Add Widget", + "undo": "Undo widget changes", + "export": "Export widget", + "no-data": "No data to display on widget", + "data-overflow": "Widget displays {{count}} out of {{total}} entities", + "alarm-data-overflow": "Widget displays alarms for {{allowedEntities}} (maximum allowed) entities out of {{totalEntities}} entities" + }, + "widget-action": { + "header-button": "Widget header button", + "open-dashboard-state": "Navigate to new dashboard state", + "update-dashboard-state": "Update current dashboard state", + "open-dashboard": "Navigate to other dashboard", + "custom": "Custom action", + "custom-pretty": "Custom action (with HTML template)", + "target-dashboard-state": "Target dashboard state", + "target-dashboard-state-required": "Target dashboard state is required", + "set-entity-from-widget": "Set entity from widget", + "target-dashboard": "Target dashboard", + "open-right-layout": "Open right dashboard layout (mobile view)" + }, + "widgets-bundle": { + "current": "Current bundle", + "widgets-bundles": "Widgets Bundles", + "add": "Add Widgets Bundle", + "delete": "Delete widgets bundle", + "title": "Title", + "title-required": "Title is required.", + "add-widgets-bundle-text": "Add new widgets bundle", + "no-widgets-bundles-text": "No widgets bundles found", + "empty": "Widgets bundle is empty", + "details": "Details", + "widgets-bundle-details": "Widgets bundle details", + "delete-widgets-bundle-title": "Are you sure you want to delete the widgets bundle '{{widgetsBundleTitle}}'?", + "delete-widgets-bundle-text": "Be careful, after the confirmation the widgets bundle and all related data will become unrecoverable.", + "delete-widgets-bundles-title": "Are you sure you want to delete { count, plural, 1 {1 widgets bundle} other {# widgets bundles} }?", + "delete-widgets-bundles-action-title": "Delete { count, plural, 1 {1 widgets bundle} other {# widgets bundles} }", + "delete-widgets-bundles-text": "Be careful, after the confirmation all selected widgets bundles will be removed and all related data will become unrecoverable.", + "no-widgets-bundles-matching": "No widgets bundles matching '{{widgetsBundle}}' were found.", + "widgets-bundle-required": "Widgets bundle is required.", + "system": "System", + "import": "Import widgets bundle", + "export": "Export widgets bundle", + "export-failed-error": "Unable to export widgets bundle: {{error}}", + "create-new-widgets-bundle": "Create new widgets bundle", + "widgets-bundle-file": "Widgets bundle file", + "invalid-widgets-bundle-file-error": "Unable to import widgets bundle: Invalid widgets bundle data structure.", + "search": "Search widget bundles", + "selected-widgets-bundles": "{ count, plural, 1 {1 widgets bundle} other {# widgets bundles} } selected", + "open-widgets-bundle": "Open widgets bundle" + }, + "widget-config": { + "data": "Data", + "settings": "Settings", + "advanced": "Advanced", + "title": "Title", + "title-tooltip": "Title Tooltip", + "general-settings": "General settings", + "display-title": "Display title", + "drop-shadow": "Drop shadow", + "enable-fullscreen": "Enable fullscreen", + "background-color": "Background color", + "text-color": "Text color", + "padding": "Padding", + "margin": "Margin", + "widget-style": "Widget style", + "title-style": "Title style", + "mobile-mode-settings": "Mobile mode settings", + "order": "Order", + "height": "Height", + "units": "Special symbol to show next to value", + "decimals": "Number of digits after floating point", + "timewindow": "Timewindow", + "use-dashboard-timewindow": "Use dashboard timewindow", + "display-timewindow": "Display timewindow", + "display-legend": "Display legend", + "datasources": "Datasources", + "maximum-datasources": "Maximum { count, plural, 1 {1 datasource is allowed.} other {# datasources are allowed} }", + "datasource-type": "Type", + "datasource-parameters": "Parameters", + "remove-datasource": "Remove datasource", + "add-datasource": "Add datasource", + "target-device": "Target device", + "alarm-source": "Alarm source", + "actions": "Actions", + "action": "Action", + "add-action": "Add action", + "search-actions": "Search actions", + "no-actions-text": "No actions found", + "action-source": "Action source", + "action-source-required": "Action source is required.", + "action-name": "Name", + "action-name-required": "Action name is required.", + "action-name-not-unique": "Another action with the same name already exists.
    Action name should be unique within the same action source.", + "action-icon": "Icon", + "action-type": "Type", + "action-type-required": "Action type is required.", + "edit-action": "Edit action", + "delete-action": "Delete action", + "delete-action-title": "Delete widget action", + "delete-action-text": "Are you sure you want delete widget action with name '{{actionName}}'?", + "display-icon": "Display title icon", + "icon-color": "Icon color", + "icon-size": "Icon size" + }, + "widget-type": { + "import": "Import widget type", + "export": "Export widget type", + "export-failed-error": "Unable to export widget type: {{error}}", + "create-new-widget-type": "Create new widget type", + "widget-type-file": "Widget type file", + "invalid-widget-type-file-error": "Unable to import widget type: Invalid widget type data structure." + }, + "widgets": { + "date-range-navigator": { + "localizationMap": { + "Sun": "Sun", + "Mon": "Mon", + "Tue": "Tue", + "Wed": "Wed", + "Thu": "Thu", + "Fri": "Fri", + "Sat": "Sat", + "Jan": "Jan", + "Feb": "Feb", + "Mar": "Mar", + "Apr": "Apr", + "May": "May", + "Jun": "Jun", + "Jul": "Jul", + "Aug": "Aug", + "Sep": "Sep", + "Oct": "Oct", + "Nov": "Nov", + "Dec": "Dec", + "January": "January", + "February": "February", + "March": "March", + "April": "April", + "June": "June", + "July": "July", + "August": "August", + "September": "September", + "October": "October", + "November": "November", + "December": "December", + "Custom Date Range": "Custom Date Range", + "Date Range Template": "Date Range Template", + "Today": "Today", + "Yesterday": "Yesterday", + "This Week": "This Week", + "Last Week": "Last Week", + "This Month": "This Month", + "Last Month": "Last Month", + "Year": "Year", + "This Year": "This Year", + "Last Year": "Last Year", + "Date picker": "Date picker", + "Hour": "Hour", + "Day": "Day", + "Week": "Week", + "2 weeks": "2 Weeks", + "Month": "Month", + "3 months": "3 Months", + "6 months": "6 Months", + "Custom interval": "Custom interval", + "Interval": "Interval", + "Step size": "Step size", + "Ok": "Ok" + } + }, + "input-widgets": { + "attribute-not-allowed": "Attribute parameter cannot be used in this widget", + "blocked-location": "Geolocation is blocked in your browser", + "claim-device": "Claim device", + "claim-failed": "Failed to claim the device!", + "claim-not-found": "Device not found!", + "claim-successful": "Device was successfully claimed!", + "date": "Date", + "device-name": "Device name", + "device-name-required": "Device name is required", + "discard-changes": "Discard changes", + "entity-attribute-required": "Entity attribute is required", + "entity-coordinate-required": "Both fields, latitude and longitude, are required", + "entity-timeseries-required": "Entity timeseries is required", + "get-location": "Get current location", + "invalid-date": "Invalid Date", + "latitude": "Latitude", + "longitude": "Longitude", + "min-value-error": "Min value is {{value}}", + "max-value-error": "Max value is {{value}}", + "not-allowed-entity": "Selected entity cannot have shared attributes", + "no-attribute-selected": "No attribute is selected", + "no-datakey-selected": "No datakey is selected", + "no-coordinate-specified": "Datakey for latitude/longitude doesn't specified", + "no-entity-selected": "No entity selected", + "no-image": "No image", + "no-support-geolocation": "Your browser doesn't support geolocation", + "no-support-web-camera": "Your browser does not support cameras", + "enable-https-use-widget": "Please enable HTTPS to use this widget", + "no-found-your-camera": "Can't find your camera", + "no-permission-camera": "Permission was denied by the user / This site doesn't have permission to use the camera", + "no-timeseries-selected": "No timeseries selected", + "secret-key": "Secret key", + "secret-key-required": "Secret key is required", + "switch-attribute-value": "Switch entity attribute value", + "switch-camera": "Switch camera", + "switch-timeseries-value": "Switch entity timeseries value", + "take-photo": "Take photo", + "time": "Time", + "timeseries-not-allowed": "Timeseries parameter cannot be used in this widget", + "update-failed": "Update failed", + "update-successful": "Update successful", + "update-attribute": "Update attribute", + "update-timeseries": "Update timeseries", + "value": "Value" + } + }, + "icon": { + "icon": "Icon", + "select-icon": "Select icon", + "material-icons": "Material icons", + "show-all": "Show all icons" + }, + "custom": { + "widget-action": { + "action-cell-button": "Action cell button", + "row-click": "On row click", + "polygon-click": "On polygon click", + "marker-click": "On marker click", + "tooltip-tag-action": "Tooltip tag action", + "node-selected": "On node selected", + "element-click": "On HTML element click", + "pie-slice-click": "On slice click", + "row-double-click": "On row double click" + } + }, + "language": { + "language": "Language", + "locales": { + "de_DE": "Deutsch", + "fr_FR": "Français", + "zh_CN": "简体中文", + "zh_TW": "繁體中文", + "en_US": "English", + "it_IT": "Italiano", + "ko_KR": "한글", + "ru_RU": "Русский", + "es_ES": "Español", + "ja_JA": "日本語", + "tr_TR": "Türkçe", + "fa_IR": "فارسي", + "uk_UA": "Українська", + "cs_CZ": "Česky", + "el_GR": "Ελληνικά", + "ro_RO": "Română", + "lv_LV": "Latviešu", + "ka_GE": "ქართული", + "pt_BR": "Português do Brasil" + } + } +} diff --git a/ui-ngx/src/assets/locale/locale.constant-es_ES.json b/ui-ngx/src/assets/locale/locale.constant-es_ES.json new file mode 100644 index 0000000000..91286c0c3f --- /dev/null +++ b/ui-ngx/src/assets/locale/locale.constant-es_ES.json @@ -0,0 +1,1897 @@ +{ + "access": { + "unauthorized": "No autorizado", + "unauthorized-access": "Acceso no autorizado", + "unauthorized-access-text": "Debes iniciar sesión para tener acceso a este recurso!", + "access-forbidden": "Acceso Prohibido", + "access-forbidden-text": "No tienes derechos para acceder a esta ubicación!
    Intenta iniciar sesión con otro usuario si todavía quieres acceder a esta ubicación.", + "refresh-token-expired": "La sesión ha expirado", + "refresh-token-failed": "No se puede actualizar la sesión" + }, + "action": { + "activate": "Activar", + "suspend": "Suspender", + "save": "Guardar", + "saveAs": "Guardar como", + "cancel": "Cancelar", + "ok": "OK", + "delete": "Borrar", + "add": "Agregar", + "yes": "Si", + "no": "No", + "update": "Actualizar", + "remove": "Eliminar", + "search": "Buscar", + "clear-search": "Borrar búsqueda", + "assign": "Asignar", + "unassign": "Anular asignación", + "share": "Compartir", + "make-private": "Hacer privado", + "apply": "Aplicar", + "apply-changes": "Aplicar cambios", + "edit-mode": "Modo Edición", + "enter-edit-mode": "Modo Edición", + "decline-changes": "Descartar cambios", + "close": "Cerrar", + "back": "Atrás", + "run": "Ejecutar", + "sign-in": "Entrar!", + "edit": "Editar", + "view": "Ver", + "create": "Crear", + "drag": "Arrastrar", + "refresh": "Actualizar", + "undo": "Deshacer", + "copy": "Copiar", + "paste": "Pegar", + "copy-reference": "Copiar referencia", + "paste-reference": "Pegar referencia", + "import": "Importar", + "export": "Exportar", + "share-via": "Compartir vía {{provider}}", + "discard-changes": "Cancelar los cambios", + "continue": "Continuar", + "download": "Descargar" + }, + "aggregation": { + "aggregation": "Agregación", + "function": "Función de Agregación", + "limit": "Valores Max", + "group-interval": "Intervalo de agrupación", + "min": "Min", + "max": "Max", + "avg": "Promedio", + "sum": "Suma", + "count": "Contar", + "none": "Ninguno" + }, + "admin": { + "general": "General", + "general-settings": "Configuración general", + "outgoing-mail": "Servidor de correo", + "outgoing-mail-settings": "Configuración del servidor de correo de salida", + "system-settings": "Sistema", + "test-mail-sent": "Mail de prueba enviado correctamente!", + "base-url": "URL Base", + "base-url-required": "URL Base requerida.", + "mail-from": "Mail Desde", + "mail-from-required": "Mail Desde requerido.", + "smtp-protocol": "Protocolo SMTP", + "smtp-host": "Host SMTP", + "smtp-host-required": "Host SMTP requerido.", + "smtp-port": "Puerto SMTP", + "smtp-port-required": "Debe ingresar un Puerto SMTP.", + "smtp-port-invalid": "No parece un Puerto SMTP valido.", + "timeout-msec": "Timeout (ms)", + "timeout-required": "Timeout requerido.", + "timeout-invalid": "No parece un Timeout valido.", + "enable-tls": "Habilitar TLS", + "tls-version": "Versión TLS", + "send-test-mail": "Enviar correo de prueba", + "password-policy": "Política de contraseñas", + "security-settings": "Configuraciones de seguridad", + "minimum-password-length": "Longitud mínima de contraseña", + "minimum-password-length-required": "Se requiere una longitud mínima de contraseña", + "minimum-password-length-range": "La longitud mínima de la contraseña debe estar en un rango de 5 a 50", + "minimum-uppercase-letters": "Número mínimo de letras mayúsculas", + "minimum-uppercase-letters-range": "El número mínimo de letras mayúsculas no puede ser negativo", + "minimum-lowercase-letters": "Número mínimo de letras minúsculas", + "minimum-lowercase-letters-range": "El número mínimo de letras minúsculas no puede ser negativo", + "minimum-digits": "Número mínimo de dígitos", + "minimum-digits-range": "El número mínimo de dígitos no puede ser negativo", + "minimum-special-characters": "Número mínimo de caracteres especiales.", + "minimum-special-characters-range": "El número mínimo de caracteres especiales no puede ser negativo.", + "password-expiration-period-days": "Periodo de caducidad de contraseña en días", + "password-expiration-period-days-range": "El período de caducidad de la contraseña en días no puede ser negativo", + "password-reuse-frequency-days": "Frecuencia de reutilización de contraseña en días", + "password-reuse-frequency-days-range": "La frecuencia de reutilización de contraseña en días no puede ser negativa", + "general-policy": "Política general", + "max-failed-login-attempts": "Número máximo de intentos fallidos de inicio de sesión, antes de que la cuenta esté bloqueada", + "minimum-max-failed-login-attempts-range": "El número máximo de intentos fallidos de inicio de sesión no puede ser negativo", + "user-lockout-notification-email": "En caso de bloqueo de la cuenta del usuario, envíe una notificación por correo electrónico" + }, + "alarm": { + "alarm": "Alarma", + "alarms": "Alarmas", + "select-alarm": "Seleccionar alarma", + "no-alarms-matching": "No se han encontrado alarmas coincidentes con '{{entity}}' .", + "alarm-required": "Alarma requerida", + "alarm-status": "Estado de Alarma", + "search-status": { + "ANY": "Todas", + "ACTIVE": "Activas", + "CLEARED": "Borradas", + "ACK": "Reconocidas", + "UNACK": "Ignoradas" + }, + "display-status": { + "ACTIVE_UNACK": "Activa No reconocida", + "ACTIVE_ACK": "Activa Reconocida", + "CLEARED_UNACK": "Normalizada no reconocida", + "CLEARED_ACK": "Normalizada reconocida" + }, + "no-alarms-prompt": "No se encontraron alarmas", + "created-time": "Hora de creación", + "type": "Tipo", + "severity": "Gravedad", + "originator": "Origen", + "originator-type": "Tipo de origen", + "details": "Detalles", + "status": "Estado", + "alarm-details": "Detalles Alarma", + "start-time": "Hora de inicio", + "end-time": "Hora fin", + "ack-time": "Hora de reconocimiento", + "clear-time": "Hora de normalización", + "severity-critical": "Crítica", + "severity-major": "Mayor", + "severity-minor": "Menor", + "severity-warning": "Peligro", + "severity-indeterminate": "Indeterminada", + "acknowledge": "Reconocer", + "clear": "Normalizar", + "search": "Buscar alarmas", + "selected-alarms": "{ count, plural, 1 {1 alarma} other {# alarmas} } seleccionadas", + "no-data": "Sin datos que mostrar", + "polling-interval": "Ciclo de refresco de alarmas (seg)", + "polling-interval-required": "Se requiere un ciclo de refresco válido.", + "min-polling-interval-message": "El ciclo debe ser por lo menos de 1 segundo.", + "aknowledge-alarms-title": "Reconocer { count, plural, 1 {1 alarma} other {# alarmas} }", + "aknowledge-alarms-text": "Estas seguro de reconocer { count, plural, 1 {1 alarma} other {# alarmas} }?", + "aknowledge-alarm-title": "Recononcer Alarma", + "aknowledge-alarm-text": "Estas seguro de reconocer Alarma?", + "clear-alarms-title": "Normalizar { count, plural, 1 {1 alarma} other {# alarmas} }", + "clear-alarms-text": "Estás seguro de limpiar { count, plural, 1 {1 alarma} other {# alarmas} }?", + "clear-alarm-title": "Limpiar Alarma", + "clear-alarm-text": "Estás seguro de limpiar Alarma?", + "alarm-status-filter": "Filtro de Alarmas", + "max-count-load": "Número máximo de alarmas a cargar (0 - ilimitado)", + "max-count-load-required": "Se requiere número máximo de alarmas.", + "max-count-load-error-min": "El valor mínimo es 0.", + "fetch-size": "Tamaño de búsqueda (Fetch)", + "fetch-size-required": "Se requiere tamaño de búsqueda.", + "fetch-size-error-min": "El valor mínimo es 10." + }, + "alias": { + "add": "Añadir alias", + "edit": "Editar alias", + "name": "Nombre de Alias", + "name-required": "Se requiere nombre de alias", + "duplicate-alias": "Ya existe ese nombre de alias.", + "filter-type-single-entity": "Única entidad", + "filter-type-entity-list": "Lista de entidades", + "filter-type-entity-name": "Nombre de entidad", + "filter-type-state-entity": "Entidad desde estado de panel", + "filter-type-state-entity-description": "Entidad tomada de los parámetros de panel", + "filter-type-asset-type": "Tipo de activo", + "filter-type-asset-type-description": "Activos del tipo '{{assetType}}'", + "filter-type-asset-type-and-name-description": "Activos del tipo '{{assetType}}' y cuyo nombre comience por '{{prefix}}'", + "filter-type-device-type": "Tipo de dispositivo", + "filter-type-device-type-description": "Dispositivos de tipo '{{deviceType}}'", + "filter-type-device-type-and-name-description": "Dispositivos de tipo '{{deviceType}}' y cuyo nombre comience por '{{prefix}}'", + "filter-type-entity-view-type": "Tipo de vista de entidad", + "filter-type-entity-view-type-description": "Vistas de entidad del tipo '{{entityView}}'", + "filter-type-entity-view-type-and-name-description": "Vistas de entidad del tipo '{{entityView}}' y cuyo nombre comience por '{{prefix}}'", + "filter-type-relations-query": "Consulta de relaciones", + "filter-type-relations-query-description": "{{entities}} que tienen {{relationType}} relación {{direction}} {{rootEntity}}", + "filter-type-asset-search-query": "Búsqueda de activos", + "filter-type-asset-search-query-description": "Activos con tipos {{assetTypes}} que tienen {{relationType}} relación {{direction}} {{rootEntity}}", + "filter-type-device-search-query": "Búsqueda de dispositivos", + "filter-type-device-search-query-description": "Dispositivos con tipos {{deviceTypes}} que tienen {{relationType}} relación {{direction}} {{rootEntity}}", + "filter-type-entity-view-search-query": "Consulta de búsqueda de vista de entidad", + "filter-type-entity-view-search-query-description": "Vistas de entidad con tipos {{entityViewTypes}} que tienen tipo de relación {{relationType}} con dirección {{direction}} {{rootEntity}}", + "entity-filter": "Filtro por entidad", + "resolve-multiple": "Tomar como múltiples entidades", + "filter-type": "Filtro por tipo", + "filter-type-required": "Se requiere filtro por tipo.", + "entity-filter-no-entity-matched": "No se han encontrado entidades con el filtro especificado.", + "no-entity-filter-specified": "No hay filtro de entidades especificado", + "root-state-entity": "Usar estado de panel como raíz", + "last-level-relation": "Buscar sólo la relación de último nivel", + "root-entity": "Entidad raiz", + "state-entity-parameter-name": "Nombre de parámetro de entidad de estado", + "default-state-entity": "Entidad de estado predeterminada", + "default-entity-parameter-name": "Por defecto", + "max-relation-level": "Máx nivel de relación", + "unlimited-level": "Nivel ilimitado", + "state-entity": "Entidad estado del panel", + "all-entities": "Todas las entidades", + "any-relation": "cualquiera" + }, + "asset": { + "asset": "Activo", + "assets": "Activos", + "management": "Gestión de Activos", + "view-assets": "Ver Activos", + "add": "Añadir Activo", + "assign-to-customer": "Asignar a cliente", + "assign-asset-to-customer": "Asignar Activo(s) A Cliente", + "assign-asset-to-customer-text": "Selecciona los activos a asignar al cliente", + "no-assets-text": "No se encontraron activos", + "assign-to-customer-text": "Selecciona el cliente al que asignar los activos", + "public": "Público", + "assignedToCustomer": "Asignado a cliente", + "make-public": "Hacer activo público", + "make-private": "Hacer activo privado", + "unassign-from-customer": "Cancelar la asignación de activo del cliente", + "delete": "Borrar activo", + "asset-public": "El activo es público", + "asset-type": "Tipo de activo", + "asset-type-required": "Se requiere tipo de activo.", + "select-asset-type": "Selecciona tipo de activo", + "enter-asset-type": "Entrar tipo de activo", + "any-asset": "Cualquier activo", + "no-asset-types-matching": "No se han encontrado activos coincidiendo con '{{entitySubtype}}' .", + "asset-type-list-empty": "No hay ningun tipo de activo seleccionado.", + "asset-types": "Tipos de activo", + "name": "Nombre", + "name-required": "Nombre requerido.", + "description": "Descripción", + "type": "Tipo", + "type-required": "Tipo requerido.", + "details": "Detalles", + "events": "Eventos", + "add-asset-text": "Añadir nuevo activo", + "asset-details": "Detalles de activo", + "assign-assets": "Asignar activos", + "assign-assets-text": "Asignar { count, plural, 1 {1 activo} other {# activos} } a cliente", + "delete-assets": "Borrar activos", + "unassign-assets": "Cancelar asignación de activo", + "unassign-assets-action-title": "Cancelar asignación de { count, plural, 1 {1 activo} other {# activos} } del cliente", + "assign-new-asset": "Asignar nuevo activo", + "delete-asset-title": "Estás seguro de borrar el activo '{{assetName}}'?", + "delete-asset-text": "Atención, tras la confirmación el activo y sus datos serán borrados e irrecuperables.", + "delete-assets-title": "Estás seguro de borrar los activos { count, plural, 1 {1 activo} other {# activos} }?", + "delete-assets-action-title": "Borrar { count, plural, 1 {1 activo} other {# activos} }", + "delete-assets-text": "Atención, tras la confirmación todos los activos seleccionados y sus datos serán borrados e irrecuperables.", + "make-public-asset-title": "Estás seguro de hacer el activo '{{assetName}}' público?", + "make-public-asset-text": "Tras la confirmación, el activo y sus datos se harán públicos y accesibles por otros.", + "make-private-asset-title": "Estás seguro de hacer el activo '{{assetName}}' privado?", + "make-private-asset-text": "Tras la confirmación, el activo y sus datos se harán privados y no serán accesibles por otros.", + "unassign-asset-title": "Estás seguro de cancelar la asignación del activo '{{assetName}}'?", + "unassign-asset-text": "Tras la confirmación, el activo será desasignado y no será accesible por el cliente.", + "unassign-asset": "Cancelar asignación de activo", + "unassign-assets-title": "Estás seguro de cancelar las asignaciones { count, plural, 1 {1 activo} other {# activos} }?", + "unassign-assets-text": "Tras la confirmación todos los activos seleccionados serán desasignados y no serán accesibles por el cliente.", + "copyId": "Copiar ID de activo", + "idCopiedMessage": "El ID ha sido copiado al portapapeles", + "select-asset": "Seleccionar activo", + "no-assets-matching": "No se han encontrado activos que coincidan con '{{entity}}' .", + "asset-required": "Nombre de activo requerido", + "name-starts-with": "El nombre de activo comienza con", + "import": "Importar activos", + "asset-file": "Archivo del activo", + "label": "Etiqueta" + }, + "attribute": { + "attributes": "Atributos", + "latest-telemetry": "Última telemetría", + "attributes-scope": "Alcance de los atributos del dispositivo", + "scope-latest-telemetry": "Última telemetría", + "scope-client": "Atributos de Cliente", + "scope-server": "Atributos de Servidor", + "scope-shared": "Atributos Compartidos", + "add": "Agregar atributo", + "key": "Clave", + "last-update-time": "Hora de última actualización", + "key-required": "Clave del atributo requerida.", + "value": "Valor", + "value-required": "Valor del atributo requerido.", + "delete-attributes-title": "¿Estás seguro que quieres eliminar { count, plural, 1 {1 atributo} other {# atributos} }?", + "delete-attributes-text": "Atención, tras la confirmación el atributo será eliminado, y la información relacionada será irrecuperable.", + "delete-attributes": "Borrar atributo", + "enter-attribute-value": "Ingresar valor del atributo", + "show-on-widget": "Mostrar en Widget", + "widget-mode": "Widget", + "next-widget": "Widget siguiente", + "prev-widget": "Widget anterior", + "add-to-dashboard": "Agregar al Panel", + "add-widget-to-dashboard": "Agregar widget al Panel", + "selected-attributes": "{ count, plural, 1 {1 atributo} other {# atributos} } seleccionados", + "selected-telemetry": "{ count, plural, 1 {1 telemetría} other {# telemetrías} } seleccionadas" + }, + "audit-log": { + "audit": "Auditoría", + "audit-logs": "Registro Auditoría", + "timestamp": "Timestamp", + "entity-type": "Tipo Entidad", + "entity-name": "Nombre Entidad", + "user": "Usuario", + "type": "Tipo", + "status": "Estado", + "details": "Detalles", + "type-added": "Añadido", + "type-deleted": "Borrado", + "type-updated": "Actualizado", + "type-attributes-updated": "Atributos actualizados", + "type-attributes-deleted": "Atributos borrados", + "type-rpc-call": "Llamada RPC", + "type-credentials-updated": "Credenciales actualizados", + "type-assigned-to-customer": "Asignado a Cliente", + "type-unassigned-from-customer": "Deasignado a Cliente", + "type-activated": "Activado", + "type-suspended": "Suspendido", + "type-credentials-read": "Lectura de credenciales", + "type-attributes-read": "Lectura de atributos", + "type-relation-add-or-update": "Relación actualizada", + "type-relation-delete": "Relación borrada", + "type-relations-delete": "Borradas todas las relaciones", + "type-alarm-ack": "Alarma Acusada", + "type-alarm-clear": "Alarma Limpiada", + "type-login": "Inicio de sesión", + "type-logout": "Cierre de sesión", + "type-lockout": "Cierre por bloqueo", + "status-success": "Éxito", + "status-failure": "Fallo", + "audit-log-details": "Detalle del registro de auditoría", + "no-audit-logs-prompt": "No se encontraron registros", + "action-data": "Datos de acción", + "failure-details": "Detalles del error", + "search": "Buscar registros de auditoría", + "clear-search": "Borrar búsqueda" + }, + "confirm-on-exit": { + "message": "Tienes cambios sin guardar. ¿Estás seguro que quieres abandonar la página?", + "html-message": "Tienes cambios sin guardar.
    ¿Estás seguro que quieres abandonar la página?", + "title": "Cambios sin guardar" + }, + "contact": { + "country": "País", + "city": "Ciudad", + "state": "Estado / Provincia", + "postal-code": "Código Postal", + "postal-code-invalid": "Formato de código postal inválido.", + "address": "Dirección", + "address2": "Dirección 2", + "phone": "Teléfono", + "email": "Email", + "no-address": "Sin Dirección" + }, + "common": { + "username": "Usuario", + "password": "Contraseña", + "enter-username": "Introduce el nombre de usuario.", + "enter-password": "Introduce la contraseña", + "enter-search": "Introduce búsqueda" + }, + "content-type": { + "json": "Json", + "text": "Texto", + "binary": "Binario (Base64)" + }, + "customer": { + "customer": "Cliente", + "customers": "Clientes", + "management": "Gestión de Clientes", + "dashboard": "Panel del Cliente", + "dashboards": "Paneles del Cliente", + "devices": "Dispositivos del cliente", + "entity-views": "Vistas de Entidad del cliente", + "assets": "Activos de Cliente", + "public-dashboards": "Paneles Públicos", + "public-devices": "Dispositivos Públicos", + "public-assets": "Activos Públicos", + "public-entity-views": "Vistas de Entidad Públicas", + "add": "Agregar cliente", + "delete": "Borrar cliente", + "manage-customer-users": "Gestionar usuarios del cliente", + "manage-customer-devices": "Gestionar dispositivos del cliente", + "manage-customer-dashboards": "Gestionar paneles del cliente", + "manage-public-devices": "Gestionar dispositivos públicos", + "manage-public-dashboards": "Gestionar paneles públicos", + "manage-customer-assets": "Gestionar activos del cliente", + "manage-public-assets": "Gestionar activos públicos", + "add-customer-text": "Agregar nuevo cliente", + "no-customers-text": "No se encontraron clientes", + "customer-details": "Detalles del cliente", + "delete-customer-title": "¿Estás seguro que quieres eliminar el cliente '{{customerTitle}}'?", + "delete-customer-text": "Atención, tras la confirmación el cliente será eliminado y toda la información relacionada será irrecuperable.", + "delete-customers-title": "¿Estás seguro que quieres eliminar { count, plural, 1 {1 cliente} other {# clientes} }?", + "delete-customers-action-title": "Borrar { count, plural, 1 {1 cliente} other {# clientes} }", + "delete-customers-text": "Atención, tras la confirmación todos los clientes seleccionados serán eliminados y su información relacionada será irrecuperable.", + "manage-users": "Gestionar usuarios", + "manage-assets": "Gestionar activos", + "manage-devices": "Gestionar dispositivos", + "manage-dashboards": "Gestionar paneles", + "title": "Título", + "title-required": "Título requerido.", + "description": "Descripción", + "details": "Detalles", + "events": "Eventos", + "copyId": "Copiar ID de cliente", + "idCopiedMessage": "El ID de cliente se ha copiado al portapapeles", + "select-customer": "Seleccionar Cliente", + "no-customers-matching": "No se han encontrado clientes que coincidan con '{{entity}}' .", + "customer-required": "Cliente requerido", + "select-default-customer": "Seleccionar cliente por defecto", + "default-customer": "Cliente por defecto", + "default-customer-required": "Se requiere cliente por defecto para realizar debug a nivel de propietario" + }, + "datetime": { + "date-from": "Fecha desde", + "time-from": "Hora desde", + "date-to": "Fecha hasta", + "time-to": "Hora hasta" + }, + "dashboard": { + "dashboard": "Panel", + "dashboards": "Paneles", + "management": "Gestión de Paneles", + "view-dashboards": "Ver Paneles", + "add": "Agregar Panel", + "assign-dashboard-to-customer": "Asignar panel(es) a cliente", + "assign-dashboard-to-customer-text": "Por favor, seleccione algún panel para asignar al Cliente.", + "assign-to-customer-text": "Por favor, seleccione algún cliente para asignar al(los) panel(es).", + "assign-to-customer": "Asignar a cliente", + "unassign-from-customer": "Desasignar del cliente", + "make-public": "Hacer panel público", + "make-private": "Hacer panel privado", + "manage-assigned-customers": "Administrar clientes asignados", + "assigned-customers": "Clientes asignados", + "assign-to-customers": "Asignar Panel / Paneles a Clientes", + "assign-to-customers-text": "Selecciona los clientes para asignar los paneles", + "unassign-from-customers": "Desasignar Panel / Paneles de clientes", + "unassign-from-customers-text": "Selecciona los clientes para desasignar los paneles", + "no-dashboards-text": "Ningún panel encontrado", + "no-widgets": "Ningún widget configurado", + "add-widget": "Agregar nuevo widget", + "title": "Título", + "select-widget-title": "Seleccionar widget", + "select-widget-subtitle": "Lista de tipos de widgets disponibles", + "delete": "Eliminar panel", + "title-required": "Título requerido.", + "description": "Descripción", + "details": "Detalles", + "dashboard-details": "Detalles del panel", + "add-dashboard-text": "Agregar nuevo panel", + "assign-dashboards": "Asignar paneles", + "assign-new-dashboard": "Asignar nuevo panel", + "assign-dashboards-text": "Asignar { count, plural, 1 {1 panel} other {# paneles} } al cliente", + "unassign-dashboards-action-text": "Desasignar { count, plural, 1 {1 panel} other {# paneles} } a los clientes", + "delete-dashboards": "Eliminar paneles", + "unassign-dashboards": "Desasignar paneles", + "unassign-dashboards-action-title": "Desasignar { count, plural, 1 {1 paneles} other {# paneles} } del cliente", + "delete-dashboard-title": "¿Estás seguro que quieres eliminar el panel '{{dashboardTitle}}'?", + "delete-dashboard-text": "Atención, el panel seleccionado será eliminado y la información relacionada sera irrecuperable.", + "delete-dashboards-title": "¿Estás seguro que quieres eliminar { count, plural, 1 {1 panel} other {# paneles} }?", + "delete-dashboards-action-title": "Eliminar { count, plural, 1 {1 panel} other {# paneles} }", + "delete-dashboards-text": "Atención, los paneles seleccionados serán eliminados y la información relacionada será irrecuperable.", + "unassign-dashboard-title": "¿Estás seguro que quieres desasignar el panel '{{dashboardTitle}}'?", + "unassign-dashboard-text": "Tras la confirmación, el panel será desasignado y no podrá ser accesible por el cliente.", + "unassign-dashboard": "Desasignar panel", + "unassign-dashboards-title": "¿Estás seguro que quieres desasignar { count, plural, 1 {1 panel} other {# paneles} }?", + "unassign-dashboards-text": "Atención, tras la confirmación los paneles seleccionados serán desasignados y no podrán ser accesibles por el cliente.", + "public-dashboard-title": "El panel ahora es público", + "public-dashboard-text": "Tu panel {{dashboardTitle}} es ahora público y podrá ser accedido desde: aquí:", + "public-dashboard-notice": "Nota: No olvides hacer públicos los dispositivos relacionados para acceder a sus datos.", + "make-private-dashboard-title": "¿Estás seguro que quieres hacer el panel '{{dashboardTitle}}' privado?", + "make-private-dashboard-text": "Tras la confirmación, el panel será privado y no podrá ser accesible por otros.", + "make-private-dashboard": "Hacer panel privado", + "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard", + "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard", + "select-dashboard": "Seleccionar panel", + "no-dashboards-matching": "Panel '{{entity}}' no encontrado.", + "dashboard-required": "Panel requerido.", + "select-existing": "Seleccionar paneles existentes", + "create-new": "Crear nuevo panel", + "new-dashboard-title": "Nuevo título", + "open-dashboard": "Abrir panel", + "set-background": "Definir fondo", + "background-color": "Color de fondo", + "background-image": "Imagen de fondo", + "background-size-mode": "Modo tamaño de fondo", + "no-image": "No se ha seleccionado ningúna imagen", + "drop-image": "Suelte una imagen o haga clic para seleccionar un archivo para cargar.", + "settings": "Ajustes", + "columns-count": "Número de columnas", + "columns-count-required": "Número de columnas requerido.", + "min-columns-count-message": "Solo se permite un número mínimo de 10 columnas.", + "max-columns-count-message": "Solo se permite un número máximo de 1000 columnas.", + "widgets-margins": "Margen entre widgets", + "horizontal-margin": "Margen horizontal", + "horizontal-margin-required": "Margen horizontal requerido.", + "min-horizontal-margin-message": "Solo se permite margen horizontal mínimo de 0.", + "max-horizontal-margin-message": "Solo se permite margen horizontal máximo de 50.", + "vertical-margin": "Margen vertical", + "vertical-margin-required": "Margen vertical requerido.", + "min-vertical-margin-message": "Solo se permite margen vertical mínimo de 0.", + "max-vertical-margin-message": "Solo se permite margen vertical máximo de 50.", + "autofill-height": "Altura diseño auto relleno", + "mobile-layout": "Ajustes del diseño móvil", + "mobile-row-height": "Altura de fila para móvil, px", + "mobile-row-height-required": "Altura de fila requerida.", + "min-mobile-row-height-message": "Sólo se permiten 5 píxeles como altura mínima de fila (móvil).", + "max-mobile-row-height-message": "Sólo se permiten 200 píxeles como altura máxima de fila (móvil).", + "display-title": "Mostrar título del panel", + "toolbar-always-open": "Mantener la barra de herramientas abierta", + "title-color": "Color del título", + "display-dashboards-selection": "Mostrar selección de paneles", + "display-entities-selection": "Mostrar selección de entidades", + "display-dashboard-timewindow": "Mostrar ventana de tiempo", + "display-dashboard-export": "Mostrar exportar", + "import": "Importar panel", + "export": "Exportar panel", + "export-failed-error": "Imposible exportar panel: {{error}}", + "create-new-dashboard": "Crear nuevo panel", + "dashboard-file": "Archivar panel", + "invalid-dashboard-file-error": "Imposible importar panel: Estructura de datos inválida.", + "dashboard-import-missing-aliases-title": "Configurar alias utilizados por el panel importado", + "create-new-widget": "Crear nuevo widget", + "import-widget": "Importar widget", + "widget-file": "Archivo de widget", + "invalid-widget-file-error": "Imposible importar widget: Estructura de datos inválida.", + "widget-import-missing-aliases-title": "Configurar alias utilizados por el widget", + "open-toolbar": "Abrir toolbar del panel", + "close-toolbar": "Cerrar toolbar", + "configuration-error": "Error de configuración", + "alias-resolution-error-title": "Error de configuración de alias del panel", + "invalid-aliases-config": "No se puede encontrar ningún dispositivo que coincida con algunos de los alias de filtro.
    Póngase en contacto con su administrador para resolver este problema.", + "select-devices": "Seleccionar dispositivos", + "assignedToCustomer": "Asignado al cliente", + "assignedToCustomers": "Asignado a los clientes", + "public": "Público", + "public-link": "Link público", + "copy-public-link": "Copiar link público", + "public-link-copied-message": "El link público del panel se ha copiado al portapapeles", + "manage-states": "Administrar estados de paneles", + "states": "Estados de paneles", + "search-states": "Buscar estados de paneles", + "selected-states": "{ count, plural, 1 {1 estado panel} other {# estado paneles} } seleccionados", + "edit-state": "Editar estado panel", + "delete-state": "Borrar estado panel", + "add-state": "Añadir estado panel", + "state": "Estado de panel", + "state-name": "Nombre", + "state-name-required": "Se requiere nombre del estado.", + "state-id": "ID Estado", + "state-id-required": "Se requiere el ID de estado.", + "state-id-exists": "Ya existe un ID de estado.", + "is-root-state": "Estado raiz(Root)", + "delete-state-title": "Borrar estado de panel", + "delete-state-text": "Estás seguro de eliminar el estado de panel con nombre: '{{stateName}}'?", + "show-details": "Mostrar detalles", + "hide-details": "Ocultar detalles", + "select-state": "Seleccionar estado destino (target state)", + "state-controller": "Controlador de estados" + }, + "datakey": { + "settings": "Ajustes", + "advanced": "Avanzado", + "label": "Etiqueta", + "color": "Color", + "units": "Símbolo especial para mostrar junto con el valor", + "decimals": "Número de dígitos después de la coma", + "data-generation-func": "Función de generación de datos", + "use-data-post-processing-func": "Usar funcíon de post-procesamiendo de datos", + "configuration": "Ajustes de clave de datos", + "timeseries": "Serie de tiempos", + "attributes": "Atributos", + "entity-field" : "Campo de entidad", + "alarm": "Campos de alarma", + "timeseries-required": "Series de tiempo del dispositivo requerido.", + "timeseries-or-attributes-required": "Series de tiempo/Atributos requeridos.", + "maximum-timeseries-or-attributes": "Máximo { count, plural, 1 {1 timeseries/atributo es permitido.} other {# timeseries/atributos son permitidos} }", + "alarm-fields-required": "Campos de alarma requeridos.", + "function-types": "Tipos de funciones", + "function-types-required": "Tipos de funciones requerido.", + "maximum-function-types": "Máximo { count, plural, 1 {1 tipo de función está permitida.} other {# tipos de funciones están permitidos} }", + "time-description": "hora del valor actual", + "value-description": "el valor actual", + "prev-value-description": "resultado de la llamada anterior de la función", + "time-prev-description": "hora del valor previo", + "prev-orig-value-description": "valor original previo" + }, + "datasource": { + "type": "Típo de fuente de datos", + "name": "Nombre", + "add-datasource-prompt": "Por favor, agrega una fuente de datos" + }, + "details": { + "edit-mode": "Modo Edición", + "edit-json": "Editar JSON", + "toggle-edit-mode": "Ir a Modo Edición" + }, + "device": { + "device": "Dispositivo", + "device-required": "Dispositivo requerido.", + "devices": "Dispositivos", + "management": "Gestión de Dispositivos", + "view-devices": "Ver Dispositivos", + "device-alias": "Alias de dispositivo", + "aliases": "Alias de dispositivos", + "no-alias-matching": "'{{alias}}' no encontrado.", + "no-aliases-found": "Ningún alias encontrado.", + "no-key-matching": "'{{key}}' no encontrado.", + "no-keys-found": "Ninguna clave encontrada.", + "create-new-alias": "Crear nuevo alias!", + "create-new-key": "Crear nueva clave!", + "duplicate-alias-error": "Alias duplicado '{{alias}}'.
    El alias de los dispositivos deben ser únicos dentro del panel.", + "configure-alias": "Configurar alias '{{alias}}'", + "no-devices-matching": "No se encontró dispositivo '{{entity}}'", + "alias": "Alias", + "alias-required": "Alias de dispositivo requerido.", + "remove-alias": "Eliminar alias", + "add-alias": "Agregar alias", + "name-starts-with": "Nombre empieza con", + "device-list": "Lista de dispositivos", + "use-device-name-filter": "Usar filtro", + "device-list-empty": "Ningún dispositivo seleccionado.", + "device-name-filter-required": "Nombre de filtro requerido.", + "device-name-filter-no-device-matched": "Ningún dispositivo encontrado que comience con '{{device}}'.", + "add": "Agregar dispositivo", + "assign-to-customer": "Asignar a cliente", + "assign-device-to-customer": "Asignar dispositivo(s) a Cliente", + "assign-device-to-customer-text": "Por favor, seleccione los dispositivos que serán asignados al cliente", + "make-public": "Hacer dispositivo público", + "make-private": "Hacer dispositivo privado", + "no-devices-text": "Ningún dispositivo encontrado", + "assign-to-customer-text": "Por favor, seleccione el cliente para asignar el(los) dispositivo(s)", + "device-details": "Detalles del dispositivo", + "add-device-text": "Agregar nuevo dispositivo", + "credentials": "Credenciales", + "manage-credentials": "Gestionar credenciales", + "delete": "Eliminar dispositivo", + "assign-devices": "Asignar dispositivo", + "assign-devices-text": "Asignar { count, plural, 1 {1 dispositivo} other {# dispositivos} } al cliente", + "delete-devices": "Eliminar dispositivo", + "unassign-from-customer": "Desasignar del cliente", + "unassign-devices": "Desasignar dispositivos", + "unassign-devices-action-title": "Desasignar { count, plural, 1 {1 dispositivo} other {# dispositivos} } del cliente", + "assign-new-device": "Asignar nuevo dispositivo", + "make-public-device-title": "¿Estás seguro que quieres hacer el dispositivo '{{deviceName}}' público?", + "make-public-device-text": "Tras la confirmación, el dispositivo y la información relacionada serán públicos y podrá ser accesible por otros.", + "make-private-device-title": "¿Estás seguro que quieres hacer el dispositivo '{{deviceName}}' privado?", + "make-private-device-text": "Tras la confirmación, el dispositivo y la información relacionada serán privados y no podrá ser accesible por otros.", + "view-credentials": "Ver credenciales", + "delete-device-title": "¿Estás seguro que quieres eliminar el dispositivo '{{deviceName}}'?", + "delete-device-text": "Atención, tras la confirmación los dispositivos serán eliminados y la información relacionada será irrecuperable.", + "delete-devices-title": "¿Estás seguro que quieres eliminar { count, plural, 1 {1 dispositivo} other {# dispositivos} }?", + "delete-devices-action-title": "Eliminar { count, plural, 1 {1 dispositivo} other {# dispositivos} }", + "delete-devices-text": "Atención, tras la confirmación los dispositivos seleccionados serán eliminados y la información relacionada será irrecuperable.", + "unassign-device-title": "¿Estás seguro que quieres desasignar el dispositivo '{{deviceName}}'?", + "unassign-device-text": "Tras la confirmación, el dispositivo será desasignado y no podrá ser accesible por el cliente.", + "unassign-device": "Desasignar dispositivo", + "unassign-devices-title": "¿Estás seguro que quieres desasignar { count, plural, 1 {1 dispositivo} other {# dispositivos} }?", + "unassign-devices-text": "Tras la confirmación, los dispositivos seleccionados serán desasignados y no podrán ser accedidos por el cliente.", + "device-credentials": "Credenciales del dispositivo", + "credentials-type": "Tipo de credencial", + "access-token": "Tóken de acceso", + "access-token-required": "Access token requerido.", + "access-token-invalid": "Access token debe tener entre 1 a 20 caracteres.", + "rsa-key": "Clave pública RSA", + "rsa-key-required": "Clave pública RSA requerida.", + "secret": "Secreta", + "secret-required": "Secreta requerida.", + "device-type": "Tipo de dispositivo", + "device-type-required": "Tipo de dispositivo requerido.", + "select-device-type": "Seleccionar tipo de dispositivo", + "enter-device-type": "Entrar tipo de dispositivo", + "any-device": "Cualquier dispositivo", + "no-device-types-matching": "No hay tipos de dispositivo que coincidan con '{{entitySubtype}}' .", + "device-type-list-empty": "No hay tipos de dispositivo seleccionados.", + "device-types": "Tipos de dispositivo", + "name": "Nombre", + "name-required": "El nombre es requerido.", + "label": "Etiqueta", + "description": "Descripción", + "events": "Eventos", + "details": "Detalles", + "copyId": "Copiar ID", + "copyAccessToken": "Copiar access token", + "idCopiedMessage": "Id del dispositivo copiado al portapapeles", + "accessTokenCopiedMessage": "Access token del dispositivo copiado al portapapeles", + "assignedToCustomer": "Asignado al cliente", + "unable-delete-device-alias-title": "Imposible eliminar alias del dispositivo", + "unable-delete-device-alias-text": "Alias '{{deviceAlias}}' no puede ser eliminado. Esta siendo usado por el(los) widget(s):
    {{widgetsList}}", + "is-gateway": "Es gateway", + "public": "Público", + "device-public": "El dispositivo es público", + "select-device": "Seleccionar dispositivo", + "device-file": "Archivo de dispositivo", + "import": "Importar dispositivo" + }, + "dialog": { + "close": "Cerrar diálogo" + }, + "direction": { + "column": "Columna", + "row": "Fila" + }, + "error": { + "unable-to-connect": "Imposible conectar con el servidor! Por favor, revise su conexión a internet.", + "unhandled-error-code": "Código de error no controlado: {{errorCode}}", + "unknown-error": "Error desconocido" + }, + "entity": { + "entity": "Entidad", + "entities": "Entidades", + "aliases": "Alias de entidad", + "entity-alias": "Alias de entidad", + "unable-delete-entity-alias-title": "No ha sido posible eliminar el alias de entidad", + "unable-delete-entity-alias-text": "El alias de entidad '{{entityAlias}}' no puede ser eliminado ya que se esta usando por los siguientes widgets:
    {{widgetsList}}", + "duplicate-alias-error": "Encontrado un alias duplicado '{{alias}}'.
    Loas alias de entidad tienen que ser únicos para cada panel.", + "missing-entity-filter-error": "Falta el filtro para el alias '{{alias}}'.", + "configure-alias": "Configurar alias '{{alias}}' ", + "alias": "Alias", + "alias-required": "Alias de entidad requerido.", + "remove-alias": "Eliminar alias de entidad", + "add-alias": "Añadir alias de entidad", + "entity-list": "Lista de entidades", + "entity-type": "Tipo de entidad", + "entity-types": "Tipos de entidades", + "entity-type-list": "Lista de tipos de entidad", + "any-entity": "Cualquier entdad", + "enter-entity-type": "Introducir tipo de entidad", + "no-entities-matching": "No se han encontrado entidades que coincidan con '{{entity}}' .", + "no-entity-types-matching": "No se han encontrado tipos de entidad que coincidan con '{{entityType}}' .", + "name-starts-with": "Nombre empieza con", + "use-entity-name-filter": "Usar filtro", + "entity-list-empty": "No hay entidades seleccionadas.", + "entity-type-list-empty": "No hay tipos de entidad seleccionados.", + "entity-name-filter-required": "Filtro de nombre de entidad requerido.", + "entity-name-filter-no-entity-matched": "No hay entidades que comiencen con '{{entity}}' .", + "all-subtypes": "Todos", + "select-entities": "Seleccionar entidades", + "no-aliases-found": "No se han encontrado alias.", + "no-alias-matching": "'{{alias}}' no encontrado.", + "create-new-alias": "Crear nuevo alias!", + "key": "Clave", + "key-name": "Nombre de clave", + "no-keys-found": "No se han encontrado claves.", + "no-key-matching": "'{{key}}' no encontrada.", + "create-new-key": "Crear nueva clave!", + "type": "Tipo", + "type-required": "Tipo de entidad requerido.", + "type-device": "Dispositivo", + "type-devices": "Dispositivos", + "list-of-devices": "{ count, plural, 1 {Un dispositivo} other {Lista de # Dispositivos} }", + "device-name-starts-with": "Dispositivos cuyos nombres comiencen por '{{prefix}}'", + "type-asset": "Activo", + "type-assets": "Activos", + "list-of-assets": "{ count, plural, 1 {Un activo} other {Lista de # activos} }", + "asset-name-starts-with": "Activos cuyos nombres comiencen por '{{prefix}}'", + "type-entity-view": "Vista Entidad", + "type-entity-views": "Vistas Entidades", + "list-of-entity-views": "{ count, plural, 1 {Una vista de entidad} other {Lista de # Vistas de Entidades} }", + "entity-view-name-starts-with": "Vistas de Entidades cuyo nombre comiencen por '{{prefix}}'", + "type-rule": "Regla", + "type-rules": "Reglas", + "list-of-rules": "{ count, plural, 1 {Una regla} other {Lista de # reglas} }", + "rule-name-starts-with": "Reglas cuyos nombres comiencen por '{{prefix}}'", + "type-plugin": "Plugin", + "type-plugins": "Plugins", + "list-of-plugins": "{ count, plural, 1 {Un plugin} other {Lista de # plugins} }", + "plugin-name-starts-with": "Plugins cuyos nombres comiencen por '{{prefix}}'", + "type-tenant": "Propietario", + "type-tenants": "Propietarios", + "list-of-tenants": "{ count, plural, 1 {Un propietario} other {Lista de # propietarios} }", + "tenant-name-starts-with": "Tenants cuyos nombres comiencen por '{{prefix}}'", + "type-customer": "Cliente", + "type-customers": "Clientes", + "list-of-customers": "{ count, plural, 1 {Un cliente} other {Lista de # clientes} }", + "customer-name-starts-with": "Clientes cuyos nombres comiencen por '{{prefix}}'", + "type-user": "Usuario", + "type-users": "Usuarios", + "list-of-users": "{ count, plural, 1 {Un usuario} other {Lista de # usuarios} }", + "user-name-starts-with": "Usuarios cuyos nombres comiencen por '{{prefix}}'", + "type-dashboard": "Panel", + "type-dashboards": "Paneles", + "list-of-dashboards": "{ count, plural, 1 {Un panel} other {Lista de # paneles} }", + "dashboard-name-starts-with": "Paneles cuyos nombres comiencen por '{{prefix}}'", + "type-alarm": "Alarma", + "type-alarms": "Alarmas", + "list-of-alarms": "{ count, plural, 1 {Una alarma} other {Lista de # alarmas} }", + "alarm-name-starts-with": "Alarmas cuyos nombres comienzan con '{{prefix}}'", + "type-rulechain": "Cadena de reglas", + "type-rulechains": "Cadenas de reglas", + "list-of-rulechains": "{ count, plural, 1 {Una cadena de reglas} other {Lista de # cadenas de reglas} }", + "rulechain-name-starts-with": "Cadenas de reglas cuyos nombres comienzan con '{{prefix}}'", + "type-rulenode": "Nodo de reglas", + "type-rulenodes": "Nodos de reglas", + "list-of-rulenodes": "{ count, plural, 1 {Un nodo de reglas} other {Lista de # nodos de reglas} }", + "rulenode-name-starts-with": "Nodos de reglas cuyos nombres comienzan con '{{prefix}}'", + "type-current-customer": "Cliente Actual", + "type-current-tenant": "Propietario Actual", + "search": "Buscar entidades", + "selected-entities": "{ count, plural, 1 {1 entidad} other {# entidades} } seleccionadas", + "entity-label": "Etiqueta de entidad", + "entity-name": "Nombre de entidad", + "details": "Detalles de entidad", + "no-entities-prompt": "No se han encontrado entidades", + "no-data": "No hay datos que mostrar", + "columns-to-display": "Columnas a Mostrar" + }, + "entity-field": { + "created-time": "Tiempo de creación", + "name": "Nombre", + "type": "Tipo", + "first-name": "Nombre", + "last-name": "Apellido", + "email": "Correo electrónico", + "title": "Título", + "country": "País", + "state": "Estado", + "city": "Ciudad", + "address": "Dirección", + "address2": "Dirección 2", + "zip": "Código postal", + "phone": "Teléfono", + "label": "Etiqueta" + }, + "entity-view": { + "entity-view": "Vista de entidad", + "entity-view-required": "Vista de entidad es requerido.", + "entity-views": "Vistas de entidad", + "management": "Gestión de vistas de entidad", + "view-entity-views": "Ver vista de entidad", + "entity-view-alias": "Alias de vista de entidad", + "aliases": "Alias de vista de entidad", + "no-alias-matching": "'{{alias}}' no encontrado.", + "no-aliases-found": "No se encontraron alias.", + "no-key-matching": "'{{key}}' no encontrada.", + "no-keys-found": "No se encontraron claves.", + "create-new-alias": "¡Crear un nuevo!", + "create-new-key": "¡Crear una nueva!", + "duplicate-alias-error": "Alias duplicado'{{alias}}'.
    Los alias de Entity View deben ser únicos en el panel.", + "configure-alias": "Configurar alias '{{alias}}'", + "no-entity-views-matching": "No se encontraron vistas que coincidan con '{{entity}}'.", + "alias": "Alias", + "alias-required": "Alias de vista de entidad es requerido.", + "remove-alias": "Borrar alias de la vista de entidad", + "add-alias": "Añadir alias a la vista de entidad", + "name-starts-with": "Nombre de vista de entidad comienza con", + "entity-view-list": "Lista de vistas de entidad", + "use-entity-view-name-filter": "Usar el filtro", + "entity-view-list-empty": "No hay vistas de entidad seleccionadas.", + "entity-view-name-filter-required": "Nombre del filtro de vista de entidad es requerido.", + "entity-view-name-filter-no-entity-view-matched": "No se encontraron vistas de entidad que comiencen con '{{entityView}}'.", + "add": "Añadir vista de entidad", + "assign-to-customer": "Asignar a cliente", + "assign-entity-view-to-customer": "Asignar vista de entidad a cliente", + "assign-entity-view-to-customer-text": "Por favor, seleccione las vistas de entidad para asignar al cliente", + "no-entity-views-text": "No se encontraron vistas de entidad", + "assign-to-customer-text": "Por favor, seleccione el cliente para asignar la vista de entidad", + "entity-view-details": "Detalles de la vista de entidad", + "add-entity-view-text": "Añadir nueva vista de entidad", + "delete": "Borrar vista de entidad", + "assign-entity-views": "Asignar vistas de entidad", + "assign-entity-views-text": "Asignar { count, plural, 1 {1 vista de entidad} other {# vistas de entidad} } a cliente", + "delete-entity-views": "Borrar vistas de entidad", + "unassign-from-customer": "Anular asignación a cliente", + "unassign-entity-views": "Anular asignación de vistas de entidad", + "unassign-entity-views-action-title": "Anular asignación { count, plural, 1 {1 vista de entidad} other {# vistas de entidad} } al cliente", + "assign-new-entity-view": "Asignar nueva vista de entidad", + "delete-entity-view-title": "¿Está seguro que quiere borrar la vista de entidad '{{entityViewName}}'?", + "delete-entity-view-text": "¡Cuidado! Después de la confirmación, la vista de la entidad y todos los datos relacionados serán irrecuperables.", + "delete-entity-views-title": "¿Está seguro que quiere borrar las vistas de entidad { count, plural, 1 {1 entityView} other {# entityViews} }?", + "delete-entity-views-action-title": "Borrar { count, plural, 1 {1 vista de entidad} other {# vistas de entidad} }", + "delete-entity-views-text": "¡Cuidado! Después de la confirmación, todas las vistas de entidades seleccionadas se eliminarán y todos los datos relacionados serán irrecuperables.", + "unassign-entity-view-title": "¿Está seguro que quiere anular la asignación de la vista de entidad '{{entityViewName}}'?", + "unassign-entity-view-text": "Después de la confirmación, la vista de la entidad quedará sin asignar y el cliente no podrá acceder a ella.", + "unassign-entity-view": "Anular asignación de la vista de entidad", + "unassign-entity-views-title": "¿Está seguro que quiere anular la asignación de { count, plural, 1 {1 vista de entidad} other {# vistas de entidad} }?", + "unassign-entity-views-text": "Después de la confirmación, todas las vistas de entidades seleccionadas quedarán sin asignar y el cliente no podrá acceder a ellas.", + "entity-view-type": "Tipo de vista de entidad", + "entity-view-type-required": "Tipo de vista de entidad es requerido.", + "select-entity-view-type": "Seleccione el tipo de vista de entidad", + "enter-entity-view-type": "Teclee el tipo de vista de entidad", + "any-entity-view": "Cualquier vista de entidad", + "no-entity-view-types-matching": "No se encontraron tipos de vista de entidad que coincidan con '{{entitySubtype}}'.", + "entity-view-type-list-empty": "No hay tipos de vista de entidad seleccionados.", + "entity-view-types": "Tipos de vista de entidad", + "name": "Nombre", + "name-required": "Nombre Requerido.", + "description": "Descripción", + "events": "Eventos", + "details": "Detalles", + "copyId": "Copiar el Id de la vista de entidad", + "assignedToCustomer": "Asignado a cliente", + "unable-entity-view-device-alias-title": "No se puede eliminar el alias de vista de entidad", + "unable-entity-view-device-alias-text": "El alias del dispositivo '{{entityViewAlias}}' no se puede borrar porque está siendo usado por el widget(s):
    {{widgetsList}}", + "select-entity-view": "Seleccionar vista de entidad", + "make-public": "Hacer pública la vista de entidad", + "make-private": "Hacer que la vista de entidad sea privada", + "start-date": "Fecha de inicio", + "start-ts": "Tiempo de inicio", + "end-date": "Fecha de finalización", + "end-ts": "Tiempo de finalización", + "date-limits": "Limites de fecha", + "client-attributes": "Atributos de cliente", + "shared-attributes": "Atributos compartidos", + "server-attributes": "Atributos de servidor", + "timeseries": "Series temporales", + "client-attributes-placeholder": "Atributos de cliente", + "shared-attributes-placeholder": "Atributos compartidos", + "server-attributes-placeholder": "Atributos de servidor", + "timeseries-placeholder": "Series temporales", + "target-entity": "Entidad objetivo", + "attributes-propagation": "Propagación de atributos", + "attributes-propagation-hint": "La vista de entidad copiará automáticamente los atributos especificados de la entidad de destino cada vez que guarde o actualice esta vista de entidad. Por razones de rendimiento, los atributos de entidad objetivo no se propagan a la vista de entidad en cada cambio de atributo. Puede habilitar la propagación automática configurando el nodo de la regla \"copiar a la vista\" en su cadena de reglas y vincular los mensajes \"Atributos de la publicación\" y \"Atributos actualizados\" al nuevo nodo de la regla.", + "timeseries-data": "Datos de series temporales", + "timeseries-data-hint": "Configure las claves de los datos de las series temporales de la entidad de destino que serán accesibles para la vista de la entidad. Los datos de esta serie temporal son de solo lectura.", + "make-public-entity-view-title": "¿Está seguro de que desea que la vista de entidad '{{entityViewName}}' sea pública?", + "make-public-entity-view-text": "Después de la confirmación, la vista de la entidad y todos sus datos se harán públicos y accesibles para otros.", + "make-private-entity-view-title": "¿Está seguro de que desea que la vista de entidad '{{entityViewName}}' sea privada?", + "make-private-entity-view-text": "Después de la confirmación, la vista de la entidad y todos sus datos se harán privados y no serán accesibles para otros." + }, + "event": { + "event-type": "Tipo de evento", + "type-error": "Error", + "type-lc-event": "Ciclo de vida del evento", + "type-stats": "Estadísticas", + "type-debug-rule-node": "Debug", + "type-debug-rule-chain": "Debug", + "no-events-prompt": "Ningún evento encontrado.", + "error": "Error", + "alarm": "Alarma", + "event-time": "Hora del evento", + "server": "Servidor", + "body": "Cuerpo", + "method": "Método", + "type": "Tipo", + "entity": "Entidad", + "message-id": "Id Mensaje", + "message-type": "Tipo Mensaje", + "data-type": "Tipo de Datos", + "relation-type": "Tipo de relación", + "metadata": "Metadatos", + "data": "Datos", + "event": "Evento", + "status": "Estado", + "success": "Éxito", + "failed": "Fallo", + "messages-processed": "Mensajes procesados", + "errors-occurred": "Ocurrieron errores" + }, + "extension": { + "extensions": "Extensiones", + "selected-extensions": "{ count, plural, 1 {1 extension} other {# extensions} } seleccionadas", + "type": "Tipo", + "key": "Clave", + "value": "Valor", + "id": "ID", + "extension-id": "ID de extensión", + "extension-type": "Tipo de extensión", + "transformer-json": "JSON *", + "unique-id-required": "El id de extensión ya existe.", + "delete": "Borrar Extensión", + "add": "Añadir Extensión", + "edit": "Editar Extensión", + "delete-extension-title": "Estás seguro de borrar la extensión '{{extensionId}}'?", + "delete-extension-text": "Atención, tras la confirmación la extensión y sus datos serán borrados e irrecuperables.", + "delete-extensions-title": "Estás seguro de borrar las extensiones { count, plural, 1 {1 extensión} other {# extensiones} }?", + "delete-extensions-text": "Atención, tras la confirmación todas las extensiones seleccionadas y sus datos serán borrados e irrecuperables.", + "converters": "Convertidores", + "converter-id": "Id de convertidor", + "configuration": "Configuración", + "converter-configurations": "Ajustes de convertidor", + "token": "Tóken de seguridad", + "add-converter": "Añadir convertidor", + "add-config": "Añadir configuración de convertidor", + "device-name-expression": "Expresión del nombre de dispositivo", + "device-type-expression": "Expresión del tipo de dispositivo", + "custom": "Personalizado", + "to-double": "Para duplicar", + "transformer": "Transformador", + "json-required": "Se requiere el JSON del transformador.", + "json-parse": "No ha sido posible analizar el JSON del transformador.", + "attributes": "Atributos", + "add-attribute": "Añadir Atributo", + "add-map": "Agregar elemento de mapeado", + "timeseries": "Series de tiempo", + "add-timeseries": "Añadir series de tiempo", + "field-required": "Campo requerido", + "brokers": "Brokers", + "add-broker": "Añadir broker", + "host": "Host", + "port": "Puerto", + "port-range": "El puerto debe estar en un rango de 1 a 65535.", + "ssl": "SSL", + "credentials": "Credenciales", + "username": "Usuario", + "password": "Contraseña", + "retry-interval": "Intervalo de reintento en milisegundos", + "anonymous": "Anónimo", + "basic": "Básico", + "pem": "PEM", + "ca-cert": "Archivo de certificado CA *", + "private-key": "Archivo de clave privado *", + "cert": "Archivo de certificado *", + "no-file": "Ningún archivo seleccionado.", + "drop-file": "Colocar un archivo o hacer clic para seleccionar un archivo para cargar.", + "mapping": "Mapeo", + "topic-filter": "Filtro de tema", + "converter-type": "Tipo de conversor", + "converter-json": "Json", + "json-name-expression": "Expresión json para nombre del dispositivo", + "topic-name-expression": "Expresión temática para nombre del dispositivo", + "json-type-expression": "Expresión json para tipo de dispositivo", + "topic-type-expression": "Expresión temática para tipo de dispositivo", + "attribute-key-expression": "Expresión para clave de atributo", + "attr-json-key-expression": "Expresión json para clave de atributo", + "attr-topic-key-expression": "Expresión temática para clave de atributo", + "request-id-expression": "Expresión para solicitud de ID", + "request-id-json-expression": "Expresión json para solicitud de ID", + "request-id-topic-expression": "Expresión temática para solicitud de ID", + "response-topic-expression": "Expresión temática para respuesta", + "value-expression": "Expresión para valor", + "topic": "Tema", + "timeout": "Tiempo de espera en milisegundos", + "converter-json-required": "Conversor json es requerido.", + "converter-json-parse": "No se puede analizar el conversor json.", + "filter-expression": "Expresión para filtro", + "connect-requests": "Solicitudes de conexión", + "add-connect-request": "Agregar solicitudes de conexión", + "disconnect-requests": "Solicitudes de desconexión", + "add-disconnect-request": "Agregar solicitud de desconexión", + "attribute-requests": "Solicitudes de atributo", + "add-attribute-request": "Agregar solicitudes de atributo", + "attribute-updates": "Actualizaciones de atributo", + "add-attribute-update": "Agregar actualizaciones de atributo", + "server-side-rpc": "RPC lado servidor", + "add-server-side-rpc-request": "Agregar solicitud RPC lado servidor", + "device-name-filter": "Filtro de nombre de dispositivo", + "attribute-filter": "Filtro de atributo", + "method-filter": "Filtro de método", + "request-topic-expression": "Expresión temática para solicitud", + "response-timeout": "Tiempo de espera de respuesta en milisegundos", + "topic-expression": "Expresión temática", + "client-scope": "Alcance del cliente", + "add-device": "Agregar dispositivo", + "opc-server": "Servidores", + "opc-add-server": "Agregar servidor", + "opc-add-server-prompt": "Por favor agregar servidor", + "opc-application-name": "Nombre de aplicación", + "opc-application-uri": "Aplicación URI", + "opc-scan-period-in-seconds": "Período de exploración en segundos", + "opc-security": "Seguridad", + "opc-identity": "Identidad", + "opc-keystore": "Almacén de claves", + "opc-type": "Tipo", + "opc-keystore-type": "Tipo", + "opc-keystore-location": "Ubicación *", + "opc-keystore-password": "Contraseña", + "opc-keystore-alias": "Alias", + "opc-keystore-key-password": "Clave de contraseña", + "opc-device-node-pattern": "Patrón de nodo de dispositivo", + "opc-device-name-pattern": "Patrón de nombre de dispositivo", + "modbus-server": "Servidores/esclavos", + "modbus-add-server": "Agregar servidor/esclavo", + "modbus-add-server-prompt": "Por favor agregar servidor/esclavo", + "modbus-transport": "Transporte", + "modbus-tcp-reconnect": "Reconexión automática", + "modbus-rtu-over-tcp": "RTU sobre TCP", + "modbus-port-name": "Nombre del puerto serial", + "modbus-encoding": "Codificación", + "modbus-parity": "Paridad", + "modbus-baudrate": "Tasa de baudios", + "modbus-databits": "Bits de datos", + "modbus-stopbits": "Bits de parada", + "modbus-databits-range": "Bits de datos deben estar en un rango entre 7 y 8.", + "modbus-stopbits-range": "Bits de parada deben estar en un rango entre 1 a 2.", + "modbus-unit-id": "ID de unidad", + "modbus-unit-id-range": "ID de unidad debe estar en un rango entre 1 a 247.", + "modbus-device-name": "Nombre del dispositivo", + "modbus-poll-period": "Período de sondeo (ms)", + "modbus-attributes-poll-period": "Atributos del período de sondeo (ms)", + "modbus-timeseries-poll-period": "Período de sondeo de las series temporales (ms)", + "modbus-poll-period-range": "El período de sondeo debe ser una valor positivo.", + "modbus-tag": "Etiqueta", + "modbus-function": "Función", + "modbus-register-address": "Dirección del registro", + "modbus-register-address-range": "Dirección del registro debe estar en un rango entre 0 y 65535.", + "modbus-register-bit-index": "Índice de bit", + "modbus-register-bit-index-range": "Índice de bit debe estar en un rango entre 0 y 15.", + "modbus-register-count": "Contador del registro", + "modbus-register-count-range": "Contador del registro debe ser un valor positivo.", + "modbus-byte-order": "Orden del byte", + "sync": { + "status": "Estado", + "sync": "Sincronizado", + "not-sync": "No Sincronizado", + "last-sync-time": "Hora de última sincronización", + "not-available": "No disponible" + }, + "export-extensions-configuration": "Exportar configuración de extensiones", + "import-extensions-configuration": "Importar configuración de extensiones", + "import-extensions": "Importar extensiones", + "import-extension": "Importar extensión", + "export-extension": "Exportar extensión", + "file": "Fichero de extensiones", + "invalid-file-error": "Fichero de extensiones inválido" + }, + "fullscreen": { + "expand": "Expandir a Pantalla Completa", + "exit": "Salir de Pantalla Completa", + "toggle": "Cambiar el modo de Pantalla Completa", + "fullscreen": "Pantalla Completa" + }, + "function": { + "function": "Función" + }, + "gateway": { + "add-entry": "Añadir configuración", + "connector-add": "Añadir conector", + "connector-enabled": "Activar conector", + "connector-name": "Nombre conector", + "connector-name-required": "Se requiere nombre conector.", + "connector-type": "Tipo conector", + "connector-type-required": "Se requiere tipo conector.", + "connectors": "Configuración de conectores", + "create-new-gateway": "Crear un gateway nuevo", + "create-new-gateway-text": "Estás seguro de crear un nuevo gateway con el nombre: '{{gatewayName}}'?", + "delete": "Borrar configuración", + "download-tip": "Descargar fichero de configuración", + "gateway": "Gateway", + "gateway-exists": "Ya existe un dispositivo con el mismo nombre.", + "gateway-name": "Nombre de Gateway", + "gateway-name-required": "Se requiere un nombre de gateway.", + "gateway-saved": "Configuración de gateway grabada satisfactoriamente.", + "json-parse": "JSON no válido.", + "json-required": "El campo no puede estar vacío.", + "no-connectors": "No hay conectores", + "no-data": "No hay configuraciones", + "no-gateway-found": "No se ha encontrado ningún gateway.", + "no-gateway-matching": " '{{item}}' no encontrado.", + "path-logs": "Ruta a los archivos de log", + "path-logs-required": "Ruta requerida.", + "remote": "Configuración remota", + "remote-logging-level": "Nivel de logging", + "remove-entry": "Borrar configuración", + "save-tip": "Grabar fichero de configuración", + "security-type": "Tipo de seguridad", + "security-types": { + "access-token": "Token de acceso", + "tls": "TLS" + }, + "storage": "Grabación", + "storage-max-file-records": "Número máximo de registros en fichero", + "storage-max-files": "Número máximo de ficheros", + "storage-max-files-min": "El número mínimo es 1.", + "storage-max-files-pattern": "Número no válido.", + "storage-max-files-required": "Se requiere número.", + "storage-max-records": "Máximo de registros en el almacén", + "storage-max-records-min": "El número mínimo es 1.", + "storage-max-records-pattern": "Número no válido.", + "storage-max-records-required": "Se requiere número.", + "storage-pack-size": "Tamaño máximo de eventos", + "storage-pack-size-min": "El número mínimo es 1.", + "storage-pack-size-pattern": "Número no válido.", + "storage-pack-size-required": "Se requiere número.", + "storage-path": "Ruta de almacén", + "storage-path-required": "Se requiere ruta de almacén.", + "storage-type": "Tipo de almacén", + "storage-types": { + "file-storage": "Almacén fichero", + "memory-storage": "Almacén en memoria" + }, + "thingsboard": "ThingsBoard", + "thingsboard-host": "Host ThingsBoard", + "thingsboard-host-required": "Se requiere Host.", + "thingsboard-port": "Puerto ThingsBoard", + "thingsboard-port-max": "El puerto máximo es 65535.", + "thingsboard-port-min": "El puerto mínimo es 1.", + "thingsboard-port-pattern": "Puerto no válido.", + "thingsboard-port-required": "Se requiere puerto.", + "tidy": "Tidy", + "tidy-tip": "Tidy JSON", + "title-connectors-json": "Configuración conector {{typeName}}", + "tls-path-ca-certificate": "Ruta al certificado CA en el gateway", + "tls-path-client-certificate": "Ruta al certificado cliente en el gateway", + "tls-path-private-key": "Ruta a la clave privada en el gateway", + "toggle-fullscreen": "Pantalla completa fullscreen", + "transformer-json-config": "Configuración JSON*", + "update-config": "Añadir/actualizar configuración JSON", + "state-title": "Estado gateway", + "show-config-tip": "Mostrar configuración gateway", + "title-show-config": "Mostrar configuración gateway", + "read-only": "Solo lectura", + "read-write": "" + }, + "grid": { + "delete-item-title": "¿Estás seguro que quieres eliminar este item?", + "delete-item-text": "Atención, tras la confirmación el item será eliminado y la información relacionada será irrecuperable.", + "delete-items-title": "¿Estás seguro que quieres eliminar { count, plural, 1 {1 item} other {# items} }?", + "delete-items-action-title": "Eliminar { count, plural, 1 {1 item} other {# items} }", + "delete-items-text": "Atención, tras la confirmación los items seleccionados serán eliminados y la información relacionada será irrecuperable.", + "add-item-text": "Agregar nuevo item", + "no-items-text": "Ningún item encontrado", + "item-details": "Detalles del item", + "delete-item": "Borrar Item", + "delete-items": "Borrar Items", + "scroll-to-top": "Ir hacia arriba" + }, + "help": { + "goto-help-page": "Ir a la página de ayuda" + }, + "home": { + "home": "Principal", + "profile": "Perfil", + "logout": "Salir", + "menu": "Menu", + "avatar": "Avatar", + "open-user-menu": "Abrir menú de usuario" + }, + "import": { + "no-file": "Ningún archivo seleccionado", + "drop-file": "Suelte un archivo JSON o haga clic para seleccionar un archivo para cargar.", + "column-value": "Valor", + "column-title": "Título", + "column-example": "Datos de ejemplo", + "drop-file-csv": "Suelte un archivo CSV o haga clic para seleccionar un archivo para cargar.", + "column-key": "Clave de atributo/telemetría", + "csv-delimiter": "Delimitador CSV", + "csv-first-line-header": "La primera línea contiene nombres de columna.", + "csv-update-data": "Actualizar atributos/telemetría", + "import-csv-number-columns-error": "Un archivo debe contener al menos dos columnas", + "import-csv-invalid-format-error": "Formato de archivo inválido. Línea: '{{line}}'", + "column-type": { + "name": "Nombre", + "type": "Tipo", + "label": "Etiqueta", + "column-type": "Tipo de columna", + "client-attribute": "Atributo de cliente", + "shared-attribute": "Atributo compartido", + "server-attribute": "Atributo de servidor", + "timeseries": "Series de tiempo", + "entity-field": "Campo de entidad", + "access-token": "Token de acceso", + "isgateway": "Es Gateway", + "description": "Descripción" + }, + "stepper-text": { + "select-file": "Seleccione un archivo", + "configuration": "Importar configuración", + "column-type": "Seleccionar tipo de columnas", + "creat-entities": "Creando nuevas entidades", + "done": "Hecho" + }, + "message": { + "create-entities": "Se crearon{{count}} nuevas entidades correctamente.", + "update-entities": "{{count}} entidades se actualizaron correctamente.", + "error-entities": "Se produjo un error al crear {{count}} entidades." + } + }, + "item": { + "selected": "Seleccionado" + }, + "js-func": { + "no-return-error": "La función debe retornar un valor!", + "return-type-mismatch": "La función debe retornar un valor de tipo: '{{type}}'!", + "tidy": "Tidy", + "mini": "Mini" + }, + "key-val": { + "key": "Clave", + "value": "Valor", + "remove-entry": "Borrar entrada", + "add-entry": "Añadir entrada", + "no-data": "Sin datos" + }, + "layout": { + "layout": "Diseño", + "manage": "Administrar diseños", + "settings": "Ajustes de diseño", + "color": "Color", + "main": "Principal", + "right": "Derecho", + "select": "Seleccionar diseño de destino" + }, + "legend": { + "direction": "Dirección de la leyenda", + "position": "Posición de la leyenda", + "show-max": "Mostrar valor máximo", + "show-min": "Mostrar valor mínimo", + "show-avg": "Mostrar valor promedio", + "show-total": "Mostrar valor total", + "settings": "Configuración de la leyenda", + "min": "mínimo", + "max": "máximo", + "avg": "promedio", + "total": "total", + "comparison-time-ago": { + "days": "(hace un día)", + "weeks": "(hace una semana)", + "months": "(hace un mes)", + "years": "(hace un año)" + } + }, + "login": { + "login": "Entrar", + "request-password-reset": "Solicitar restablecer contraseña", + "reset-password": "Restablecer contraseña", + "create-password": "Crear contraseña", + "passwords-mismatch-error": "¡Las contraseñas introducidas deben ser iguales!", + "password-again": "Repita la contraseña de nuevo", + "sign-in": "Por favor, inicie sesión", + "username": "Nombre de usuario (correo electrónico)", + "remember-me": "Recordarme", + "forgot-password": "¿Olvidó la contraseña?", + "password-reset": "Restablecer contraseña", + "expired-password-reset-message": "Tus credenciales han expirado! Por favor, crea una nueva contraseña.", + "new-password": "Nueva contraseña", + "new-password-again": "Repita la nueva contraseña", + "password-link-sent-message": "Se ha enviado el enlace de restablecimiento de contraseña con éxito!", + "email": "Email", + "login-with": "Iniciar sesión con {{name}}", + "or": "o", + "error": "Error de login" + }, + "position": { + "top": "Superior", + "bottom": "Inferior", + "left": "Izquierda", + "right": "Derecha" + }, + "profile": { + "profile": "Perfil", + "last-login-time": "Último acceso", + "change-password": "Cambiar contraseña", + "current-password": "Contraseña actual" + }, + "relation": { + "relations": "Relaciones", + "direction": "Dirección", + "search-direction": { + "FROM": "Desde", + "TO": "Hacia" + }, + "direction-type": { + "FROM": "desde", + "TO": "hacia" + }, + "from-relations": "Relaciones salientes (outbound)", + "to-relations": "Relaciones entrantes (inbound)", + "selected-relations": "{ count, plural, 1 {1 relación} other {# relaciones} } seleccionadas", + "type": "Tipo", + "to-entity-type": "Hacia tipo de entidad", + "to-entity-name": "Hacia nombre de entidad", + "from-entity-type": "Desde tipo de entidad", + "from-entity-name": "Desde nombre de entidad", + "to-entity": "Hacia entidad", + "from-entity": "Desde entidad", + "delete": "Borrar relación", + "relation-type": "Tipo de relación", + "relation-type-required": "Tipo de relación requerido.", + "any-relation-type": "Cualquier tipo", + "add": "Añadir relación", + "edit": "Editar relación", + "delete-to-relation-title": "¿Estás seguro que quieres eliminar la relación con la entidad '{{entityName}}'?", + "delete-to-relation-text": "Atención, tras la confirmación la entidad '{{entityName}}' no estará relacionada con la entidad actual.", + "delete-to-relations-title": "¿Estás seguro que quieres eliminar { count, plural, 1 {1 relación} other {# relaciones} }?", + "delete-to-relations-text": "Atención, tras la confirmación todas las relaciones seleccionadas se eliminarán y sus entidades correspondientes no estarán relacionadas con la entidad actual.", + "delete-from-relation-title": "¿Estás seguro que quieres eliminar la relación con la entidad '{{entityName}}'?", + "delete-from-relation-text": "Atención, tras la confirmación la entidad actual no estará relacionada con la entidad '{{entityName}}'.", + "delete-from-relations-title": "¿Estás seguro que quieres eliminar { count, plural, 1 {1 relación} other {# relaciones} }?", + "delete-from-relations-text": "Atención, tras la confirmación todas las relaciones seleccionadas se eliminarán y sus entidades correspondientes no estarán relacionadas con sus entidades correspondientes.", + "remove-relation-filter": "Quitar filtro de relación", + "add-relation-filter": "Añadir filtro de relación", + "any-relation": "Cualquier relación", + "relation-filters": "Filtro de relación", + "additional-info": "Información adicional (JSON)", + "invalid-additional-info": "Error al analizar el fichero JSON de información adicional." + }, + "rulechain": { + "rulechain": "Cadena de Regla", + "rulechains": "Cadenas de Reglas", + "root": "Raíz", + "delete": "Borrar cadena de reglas", + "name": "Nombre", + "name-required": "Nombre requerido.", + "description": "Descripción", + "add": "Añadir Cadena", + "set-root": "Hacer la cadena de reglas Raíz", + "set-root-rulechain-title": "¿Estás seguro de que desea hacer la cadena de reglas '{{ruleChainName}}' de tipo raíz?", + "set-root-rulechain-text": "Tras la confirmación, la cadena de reglas se volverá raíz y manejará todos los mensajes de transporte entrantes.", + "delete-rulechain-title": "¿Estás seguro que quieres eliminar la cadena de reglas '{{ruleChainName}}'?", + "delete-rulechain-text": "Atención, tras la confirmación la cadena de reglas y todos los datos serán irrecuperables.", + "delete-rulechains-title": "¿Está seguro que quieres eliminar { count, plural, 1 {1 cadena de reglas} other {# cadenas de reglas} }?", + "delete-rulechains-action-title": "Eliminar { count, plural, 1 {1 cadena de reglas} other {# cadenas de reglas} }", + "delete-rulechains-text": "Atención, tras la confirmación todas las cadena de reglas seleccionadas y todos sus datos serán irrecuperables.", + "add-rulechain-text": "Añadir nueva cadena de reglas", + "no-rulechains-text": "Cadenas de reglas no encontradas", + "rulechain-details": "Detalles de la cadena de reglas", + "details": "Detalles", + "events": "Eventos", + "system": "Sistema", + "import": "Importar cadena de reglas", + "export": "Exportar cadena de reglas", + "export-failed-error": "No se puede exportar la cadena de reglas: {{error}}", + "create-new-rulechain": "Crear nueva cadena de reglas", + "rulechain-file": "Fichero de cadena de reglas", + "invalid-rulechain-file-error": "No se puede importar la cadena de reglas: Estructura de datos de la cadena de reglas inválida.", + "copyId": "Copiar ID de la cadena de reglas", + "idCopiedMessage": "ID de la cadena de reglas ha sido copiada al portapapeles", + "select-rulechain": "Seleccionar cadena de reglas", + "no-rulechains-matching": "No se encontraron cadenas de reglas que coincidan con '{{entity}}' .", + "rulechain-required": "Cadena de reglas requerida", + "management": "Gestión de reglas", + "debug-mode": "Modo Debug" + }, + "rulenode": { + "details": "Detalles", + "events": "Eventos", + "search": "Buscar nodos", + "open-node-library": "Abrir librería de nodos", + "add": "Añadir nodo de reglas", + "name": "Nombre", + "name-required": "El nombre es requerido.", + "type": "Tipo", + "description": "Descripción", + "delete": "Eliminar nodo de reglas", + "select-all-objects": "Seleccionar todos los nodos y conexiones", + "deselect-all-objects": "Deshacer selección de todos los nodos y conexiones", + "delete-selected-objects": "Eliminar nodos y conexiones seleccionados", + "delete-selected": "Eliminar seleccionado", + "select-all": "Seleccionar todos", + "copy-selected": "Copiar seleccionado", + "deselect-all": "Deshacer selección de todos", + "rulenode-details": "Detalles del nodo de reglas", + "debug-mode": "Modo Debug", + "configuration": "Configuración", + "link": "Enlace", + "link-details": "Detalles del enlace del nodo de reglas", + "add-link": "Agregar enlace", + "link-label": "Etiqueta del enlace", + "link-label-required": "Etiqueta del enlace es requerida.", + "custom-link-label": "Etiqueta del enlace personalizada", + "custom-link-label-required": "Etiqueta del enlace personalizado es requerida.", + "link-labels": "Etiquetas del enlace", + "link-labels-required": "Etiquetas del enlace son requeridas.", + "no-link-labels-found": "Etiquetas de enlaces no encontradas", + "no-link-label-matching": "'{{label}}' no encontrada.", + "create-new-link-label": "Crear una nueva!", + "type-filter": "Filtro", + "type-filter-details": "Filtrar mensajes entrantes con las condiciones configuradas", + "type-enrichment": "Enriquecimiento", + "type-enrichment-details": "Agregar información adicional en mensajes de metadatos", + "type-transformation": "Transformación", + "type-transformation-details": "Cambiar carga útil del Mensaje y Metadatos", + "type-action": "Acción", + "type-action-details": "Ejecutar acción especial", + "type-external": "Externo", + "type-external-details": "Interactuar con sistemas externos", + "type-rule-chain": "Cadena de reglas", + "type-rule-chain-details": "Reenvíar los mensajes entrantes a la cadena de reglas especificada", + "type-input": "Entrada", + "type-input-details": "Entrada lógica de la Cadena de Reglas, reenvíar los mensajes entrantes al siguiente nodo de regla relacionado.", + "type-unknown": "Desconocido", + "type-unknown-details": "Regla de nodo no resuelta", + "directive-is-not-loaded": "La directiva de configuración definida '{{directiveName}}' no está disponible.", + "ui-resources-load-error": "Error al cargar los recursos de configuración ui.", + "invalid-target-rulechain": "No se puede resolver la cadena de reglas objetivo!", + "test-script-function": "Probar Script Función", + "message": "Mensaje", + "message-type": "Tipo de mensaje", + "select-message-type": "Seleccionar tipo de mensaje", + "message-type-required": "Tipo de mensaje es requerido", + "metadata": "Metadatos", + "metadata-required": "La entradas de metadatos no pueden estar vacías.", + "output": "Salida", + "test": "Test", + "help": "Ayuda", + "reset-debug-mode": "Restablecer el modo de depuración en todos los nodos" + }, + "queue": { + "select_name": "Selecciona el nombre de la cola", + "name": "Nombre Cola", + "name_required": "Necesario especificar el nombre de cola" + + }, + "tenant": { + "tenant": "Propietario", + "tenants": "Propietarios", + "management": "Gestión de Propietarios", + "add": "Agregar propietario", + "admins": "Admins", + "manage-tenant-admins": "Gestionar administradores de propietario", + "delete": "Eliminar propietario", + "add-tenant-text": "Agregar nuevo propietario", + "no-tenants-text": "Ningún propietario encontrado", + "tenant-details": "Detalles del propietario", + "delete-tenant-title": "¿Estás seguro que quieres eliminar el propietario '{{tenantTitle}}'?", + "delete-tenant-text": "Atención, tras la confirmación el propietario será eliminado y la información relacionada será irrecuperable.", + "delete-tenants-title": "¿Estás seguro que quieres eliminar { count, plural, 1 {1 propietario} other {# propietarios} }?", + "delete-tenants-action-title": "Eliminar { count, plural, 1 {1 propietario} other {# propietarios} }", + "delete-tenants-text": "Atención, tras la confirmación los propietarios seleccionados serán eliminados y la información relacionada será irrecuperable.", + "title": "Título", + "title-required": "Título requerido.", + "description": "Descripción", + "details": "Detalles", + "events": "Eventos", + "copyId": "Copiar ID de propietario", + "idCopiedMessage": "El ID de propietario se ha copiado al portapapeles", + "select-tenant": "Seleccionar propietario", + "no-tenants-matching": "No hay propietarios que coincidan con '{{entity}}' .", + "tenant-required": "Propietario requerido", + "isolated-tb-core": "Procesando en contenedor aislado", + "isolated-tb-rule-engine": "Procesando en contenedor Motor de Reglas aislado", + "isolated-tb-core-details": "Requiere microservicios separados por propietario aislado", + "isolated-tb-rule-engine-details": "Requiere microservicios separados por propietario aislado" + }, + "timeinterval": { + "seconds-interval": "{ seconds, plural, 1 {1 segundo} other {# segundos} }", + "minutes-interval": "{ minutes, plural, 1 {1 minuto} other {# minutos} }", + "hours-interval": "{ hours, plural, 1 {1 hora} other {# horas} }", + "days-interval": "{ days, plural, 1 {1 día} other {# días} }", + "days": "Días", + "hours": "Horas", + "minutes": "Minutos", + "seconds": "Segundos", + "advanced": "Avanzado" + }, + "timewindow": { + "days": "{ days, plural, 1 { día } other {# días } }", + "hours": "{ hours, plural, 0 { horas } 1 {1 hora } other {# horas } }", + "minutes": "{ minutes, plural, 0 { minutos } 1 {1 minuto } other {# minutos } }", + "seconds": "{ seconds, plural, 0 { segundos } 1 {1 segundo } other {# segundos } }", + "realtime": "Tiempo-real", + "history": "Histórico", + "last-prefix": "último(s)", + "period": "desde {{ startTime }} hasta {{ endTime }}", + "edit": "Editar ventana de tiempo", + "date-range": "Rango de fechas", + "last": "Últimos(s)", + "time-period": "Período de tiempo", + "hide": "Ocultar" + }, + "user": { + "user": "Usuario", + "users": "Usuarios", + "customer-users": "Usuarios del Cliente", + "tenant-admins": "Admins propietarios", + "sys-admin": "Administrador del Sistema", + "tenant-admin": "Administrador Propietario", + "customer": "Cliente", + "anonymous": "Anónimo", + "add": "Agregar usuario", + "delete": "Eliminar usuario", + "add-user-text": "Agregar nuevo usuario", + "no-users-text": "Ningún usuario encontrado", + "user-details": "Detalles del usuario", + "delete-user-title": "¿Estás seguro que quieres eliminar el usuario '{{userEmail}}'?", + "delete-user-text": "Atención, tras la confirmación el usuario seleccionado será eliminado y la información relacionada será irrecuperable.", + "delete-users-title": "¿Estás seguro que quieres eliminar { count, plural, 1 {1 usuario} other {# usuarios} }?", + "delete-users-action-title": "Borrar { count, plural, 1 {1 usuario} other {# usuarios} }", + "delete-users-text": "Atención, tras la confirmación los usuarios seleccionados serán eliminados y la información relacionada será irrecuperable.", + "activation-email-sent-message": "Mail de activación enviado con éxito!", + "resend-activation": "Reenviar activación", + "email": "Email", + "email-required": "Email requerido.", + "invalid-email-format": "Formato de email no válido.", + "first-name": "Nombre", + "last-name": "Apellido", + "description": "Descripción", + "default-dashboard": "Panel por defecto", + "always-fullscreen": "Siempre en pantalla completa", + "select-user": "Seleccionar usuario", + "no-users-matching": "No se han encontrado usuarios coindiendo con '{{entity}}' .", + "user-required": "Usuario requerido", + "activation-method": "Método de activación", + "display-activation-link": "Mostrar enlace de activación", + "send-activation-mail": "Enviar mail de activación", + "activation-link": "Enlace de activacion de usuario", + "activation-link-text": "Para activar el usuario, usa el siguiente enlace: Activar Usuario :", + "copy-activation-link": "Copiar enlace de activación", + "activation-link-copied-message": "El enlace de activación se ha copiado al portapapeles", + "details": "Detalles", + "login-as-tenant-admin": "Iniciar sesión como Administrador Propietario", + "login-as-customer-user": "Iniciar sesión como Usuario Cliente", + "disable-account": "Deshabilitar cuenta de usuario", + "enable-account": "Habilitar cuenta de usuario", + "enable-account-message": "¡La cuenta de usuario se ha habilitado correctamente!", + "disable-account-message": "¡La cuenta de usuario se deshabilitó correctamente!" + }, + "value": { + "type": "Tipo de valor", + "string": "Cadena de texto", + "string-value": "Valor de cadena de texto", + "integer": "Nro entero", + "integer-value": "Valor de nro entero", + "invalid-integer-value": "Valor inválido", + "double": "Nro decimal", + "double-value": "Valor nro decimal", + "boolean": "Booleano", + "boolean-value": "Valor booleano", + "false": "Falso", + "true": "Verdadero", + "long": "Nro Largo", + "json": "JSON", + "json-value": "Valor JSON" + }, + "widget": { + "widget-library": "Bibloteca de Widgets", + "widget-bundle": "Paquetes de Widgets", + "select-widgets-bundle": "Seleccionar paquete de widgets", + "management": "Gestión de Widgets", + "editor": "Editor de widgets", + "widget-type-not-found": "Problema al cargar la configuración del widget.
    Probablemente asociado\n El tipo de widget fue eliminado.", + "widget-type-load-error": "El widget no pudo ser cargado debido a estos errores:", + "remove": "Eliminar widget", + "edit": "Editar widget", + "remove-widget-title": "¿Estás seguro que quieres eliminar el widget '{{widgetTitle}}'?", + "remove-widget-text": "Atención, tras la confirmación el widget será eliminado y toda la información relacionada será irrecuperable..", + "timeseries": "Series de tiempo", + "search-data": "Buscar datos", + "no-data-found": "No se han encontrado datos", + "latest-values": "Últimos valores", + "rpc": "Widget de control", + "alarm": "Widget de Alarma", + "static": "Widget estático", + "select-widget-type": "Seleccionar tipo de widget", + "missing-widget-title-error": "El titulo del widget debe ser especificado!", + "widget-saved": "Widget guardado", + "unable-to-save-widget-error": "Imposible guardar widget! Tiene errores!", + "save": "Guardar widget", + "saveAs": "Guardar widget como", + "save-widget-type-as": "Guardar tipo de widget como", + "save-widget-type-as-text": "Por favor, ingrese un nuevo titulo y/o seleccione un paquete de destino.", + "toggle-fullscreen": "Cambiar a pantalla completa", + "run": "Ejecutar widget", + "title": "Título", + "title-required": "Título requerido.", + "type": "Tipo", + "resources": "Recursos", + "resource-url": "URL JavaScript/CSS", + "remove-resource": "Eliminar recurso", + "add-resource": "Agregar recurso", + "html": "HTML", + "tidy": "Tidy", + "css": "CSS", + "settings-schema": "Esquema de configuración", + "datakey-settings-schema": "Esquema de configuración de clave de datos", + "javascript": "Javascript", + "js": "JS", + "remove-widget-type-title": "¿Estás seguro que quieres eliminar el tipo del widget '{{widgetName}}'?", + "remove-widget-type-text": "Atención, tras la confirmación el tipo será eliminado y la información relacionada será irrecuperable.", + "remove-widget-type": "Eliminar tipo de widget.", + "add-widget-type": "Agregar nuevo tipo de widget", + "widget-type-load-failed-error": "Error al cargar el tipo de widget!", + "widget-template-load-failed-error": "Error al cargar la plantilla del widget!", + "add": "Agregar Widget", + "undo": "Deshacer cambios", + "export": "Exportar widget" + }, + "widget-action": { + "header-button": "Botón de encabezado widget", + "open-dashboard-state": "Navegar a un nuevo estado de panel", + "update-dashboard-state": "Actualizar el estado del panel actual", + "open-dashboard": "Navegar hacia otro panel", + "custom": "Acción personalizada", + "custom-pretty": "Acción personalizada (con plantilla HTML)", + "target-dashboard-state": "Estado de panel de destino", + "target-dashboard-state-required": "Se requiere estado de panel de destino", + "set-entity-from-widget": "Establecer entidad desde widget", + "target-dashboard": "Panel de destino", + "open-right-layout": "Abrir diseño de panel (derecho)(vista móvil)" + }, + "widgets-bundle": { + "current": "Paquete actual", + "widgets-bundles": "Paquete de Widgets", + "add": "Agregar paquete de widgets", + "delete": "Eliminar paquete de widgets", + "title": "Título", + "title-required": "Título requerido.", + "add-widgets-bundle-text": "Agregar nuevo paquete de widgets", + "no-widgets-bundles-text": "Ningún paquete de widgets encontrado", + "empty": "Paquete de widgets vacío.", + "details": "Detalles", + "widgets-bundle-details": "Detalles del paquete de Widgets", + "delete-widgets-bundle-title": "¿Estás seguro que quieres eliminar el paquete de widgets '{{widgetsBundleTitle}}'?", + "delete-widgets-bundle-text": "Atención, tras la confirmación todos los paquetes seleccionados serán eliminados y su información relacionada será irrecuperable.", + "delete-widgets-bundles-title": "¿Estás seguro que deseas eliminar { count, plural, 1 {1 paquete de widgets} other {# paquetes de widgets} }?", + "delete-widgets-bundles-action-title": "Eliminar { count, plural, 1 {1 paquete de widgets} other {# paquetes de widgets} }", + "delete-widgets-bundles-text": "Atención, tras la confirmación todos los paquetes seleccionados serán eliminados y la información relacionada será irrecuperable.", + "no-widgets-bundles-matching": "Ningún paquete '{{widgetsBundle}}' encontrado.", + "widgets-bundle-required": "Paquete de widget requerido.", + "system": "Widget de Sistema", + "import": "Importar paquete de widgets", + "export": "Exportar paquete de widgets", + "export-failed-error": "Imposible exportar paquete de widgets: {{error}}", + "create-new-widgets-bundle": "Crear nuevo paquete de widgets", + "widgets-bundle-file": "Archivo de paquete de widgets", + "invalid-widgets-bundle-file-error": "Imposible importar paquete de widgets: Estructura de datos inválida." + }, + "widget-config": { + "data": "Datos", + "settings": "Ajustes", + "advanced": "Avanzado", + "title": "Titulo", + "title-tooltip": "Tooltip Título", + "general-settings": "Ajustes generales", + "display-title": "Mostrar titulo", + "drop-shadow": "Sombra", + "enable-fullscreen": "Habilitar pantalla completa", + "background-color": "Color de fondo", + "text-color": "Color del texto", + "padding": "Relleno", + "margin": "Margen", + "widget-style": "Estilo de widget", + "title-style": "Estilo de título", + "mobile-mode-settings": "Ajustes móvil.", + "order": "Orden", + "height": "Altura", + "units": "Caracter especial a mostrar en el siguiente valor", + "decimals": "Números de dígitos después de la coma", + "timewindow": "Ventana de tiempo", + "use-dashboard-timewindow": "Usar ventana de tiempo del Panel", + "display-timewindow": "Mostrar ventana de tiempo", + "display-legend": "Mostrar leyenda", + "datasources": "Set de datos", + "maximum-datasources": "Un máximo de { count, plural, 1 {1 set de datos es permitido.} other {# set de datos son permitidos} }", + "datasource-type": "Tipo", + "datasource-parameters": "Parámetros", + "remove-datasource": "Eliminar set de datos", + "add-datasource": "Agregar set de datos", + "target-device": "Dispositivo destino", + "alarm-source": "Origen de alarmas", + "actions": "Acciones", + "action": "Acción", + "add-action": "Añadir acción", + "search-actions": "Buscar acciones", + "action-source": "Origen de acción", + "action-source-required": "Origen de acción requerido.", + "action-name": "Nombre", + "action-name-required": "Nombre de accion requerido.", + "action-name-not-unique": "Existe una acción con el mismo nombre.
    El nombre de acción debe ser único dentro de la misma fuente de acción (origen).", + "action-icon": "Icono", + "action-type": "Tipo", + "action-type-required": "Tipo de acción requerido.", + "edit-action": "Editar acción", + "delete-action": "Borrar acción", + "delete-action-title": "Borrar acción de widget", + "delete-action-text": "Estás seguro de borrar la acción de widget con el nombre '{{actionName}}'?", + "display-icon": "Mostrar icono del título", + "icon-color": "Color del icono", + "icon-size": "Tamaño del icono" + }, + "widget-type": { + "import": "Importar tipo de widget", + "export": "Exportar tipo de widget", + "export-failed-error": "Imposible exportar tipo de widget: {{error}}", + "create-new-widget-type": "Crear nuevo tipo de widget", + "widget-type-file": "Archivo de tipo de widget", + "invalid-widget-type-file-error": "No se puede importar tipo de widget: Estructura de datos del tipo de widget es inválida." + }, + "widgets": { + "date-range-navigator": { + "localizationMap": { + "Sun": "Dom.", + "Mon": "Lun.", + "Tue": "Mar.", + "Wed": "Mié", + "Thu": "Jue.", + "Fri": "Vie.", + "Sat": "Sáb.", + "Jan": "Ene.", + "Feb": "Feb.", + "Mar": "Mar.", + "Apr": "Abr.", + "May": "May.", + "Jun": "Jun.", + "Jul": "Jul.", + "Aug": "Ago.", + "Sep": "Sept.", + "Oct": "Oct.", + "Nov": "Nov.", + "Dec": "Dic.", + "January": "Enero", + "February": "Febrero", + "March": "Marzo", + "April": "Abril", + "June": "Junio", + "July": "Julio", + "August": "Agosto", + "September": "Septiembre", + "October": "Octubre", + "November": "Noviembre", + "December": "Diciembre", + "Custom Date Range": "Intervalo de fechas personalizado", + "Date Range Template": "Plantilla de rango de fechas", + "Today": "Hoy", + "Yesterday": "Ayer", + "This Week": "Esta semana", + "Last Week": "La semana pasada", + "This Month": "Este mes", + "Last Month": "El mes pasado", + "Year": "Año", + "This Year": "Este año", + "Last Year": "Último", + "Date picker": "Selector de fecha", + "Hour": "Hora", + "Day": "Día", + "Week": "Semana", + "2 weeks": "2 Semanas", + "Month": "Mes", + "3 months": "3 Meses", + "6 months": "6 Meses", + "Custom interval": "Intervalo personalizado", + "Interval": "Intervalo", + "Step size": "Número de pasos", + "Ok": "De acuerdo" + } + }, + "input-widgets": { + "attribute-not-allowed": "El parámetro de atributo no se puede usar en este widget", + "blocked-location": "La función de geolocalización está bloqueada en tu navegador", + "claim-device": "Reclamar dispositivo", + "claim-failed": "Error al reclamar dispositivo!", + "claim-not-found": "Dispositivo no encontrado!", + "claim-successful": "Dispositivo reclamado correctamente!", + "date": "Fecha", + "device-name": "Nombre del dispositivo", + "device-name-required": "Se requere nombre de dispositivo", + "discard-changes": "Descartar los cambios", + "entity-attribute-required": "Se requiere atributo de entidad", + "entity-coordinate-required": "Se requieren ambos campos (latitud y longitud)", + "entity-timeseries-required": "Se requiere la serie de tiempo de la entidad", + "get-location": "Obtener localización actual", + "latitude": "Latitud", + "longitude": "Longitud", + "not-allowed-entity": "La entidad seleccionada no puede tener atributos compartidos", + "no-attribute-selected": "No se seleccionó ningún atributo", + "no-datakey-selected": "No se seleccionó ninguna clave de datos", + "no-coordinate-specified": "No se ha especificado la clave para latitud/longitud", + "no-entity-selected": "Ninguna entidad seleccionada", + "no-image": "Sin imagen", + "no-support-geolocation": "Tu navegador no soporta geolocalización", + "no-support-web-camera": "No hay cámara web compatible", + "no-timeseries-selected": "No hay series de tiempo seleccionadas", + "secret-key": "Clave", + "secret-key-required": "Clave requerida", + "switch-attribute-value": "Cambiar el valor del atributo de entidad", + "switch-camera": "Cambiar de cámara", + "switch-timeseries-value": "Cambiar el valor de la serie de tiempo de la entidad", + "take-photo": "Tomar foto", + "time": "Tiempo", + "timeseries-not-allowed": "El parámetro Timeseries no se puede usar en este widget", + "update-failed": "Actualización fallida", + "update-successful": "Actualización exitosa", + "update-attribute": "Actualizar atributo", + "update-timeseries": "Actualizar series de tiempo", + "value": "Valor" + } + }, + "icon": { + "icon": "Icono", + "select-icon": "Seleccionar iconos", + "material-icons": "Iconos material-design", + "show-all": "Mostrar todos los iconos" + }, + "custom": { + "widget-action": { + "action-cell-button": "Acción botón de celda", + "row-click": "En click de fila", + "polygon-click": "Clic en polígono", + "marker-click": "En click de marcador", + "tooltip-tag-action": "Acción de la etiqueta Tooltip", + "node-selected": "Clic en el nodo seleccionado", + "element-click": "Clic en el elemento HTML", + "pie-slice-click": "Clic en la rebanada", + "row-double-click": "Doble clic en la fila" + } + }, + "language": { + "language": "Lenguaje" + } +} diff --git a/ui-ngx/src/assets/locale/locale.constant-fa_IR.json b/ui-ngx/src/assets/locale/locale.constant-fa_IR.json new file mode 100644 index 0000000000..05e6aeb2f6 --- /dev/null +++ b/ui-ngx/src/assets/locale/locale.constant-fa_IR.json @@ -0,0 +1,1631 @@ +{ + "access": { + "unauthorized": "غير مجاز", + "unauthorized-access": "دسترسي غير مجاز", + "unauthorized-access-text": "!شما بايد وارد شويد تا به اين منبع دسترسي پيدا کنيد", + "access-forbidden": "دسترسي ممنوع", + "access-forbidden-text": "!اگر هنوز تمايل داريد به اينجا دسترسي پيدا کنيد، تلاش کنيد با نام کاربري ديگري وارد شويد
    .شما حق دسترسي به اينجا را نداريد", + "refresh-token-expired": "اين بخش، منقضي شده است", + "refresh-token-failed": "بازيابي اين بخش ممکن نيست" + }, + "action": { + "activate": "فعال سازي", + "suspend": "معلّق", + "save": "ذخيره سازي", + "saveAs": "ذخيره سازي در", + "cancel": "لغو", + "ok": "قبول", + "delete": "حذف", + "add": "اضافه", + "yes": "بله", + "no": "خير", + "update": "به روز کردن", + "remove": "حذف", + "search": "جستجو", + "clear-search": "پاک کردن جستجو", + "assign": "تخصيص", + "unassign": "لغو تخصيص", + "share": "به اشتراک گذاري", + "make-private": "شخصي سازي", + "apply": "اعمال", + "apply-changes": "اعمال تغييرات", + "edit-mode": "حالت ويرايش", + "enter-edit-mode": "ورود به حالت ويرايش", + "decline-changes": "عدم پذيرش تغييرات", + "close": "بستن", + "back": "بازگشت", + "run": "اجرا", + "sign-in": "!ورود", + "edit": "ويرايش", + "view": "نمايش", + "create": "ايجاد", + "drag": "کشيدن", + "refresh": "بازيابي", + "undo": "برگرداندن آخرين عمل", + "copy": "رونوشت", + "paste": "الصاق رونوشت", + "copy-reference": "رونوشت مرجع", + "paste-reference": "رونوشت مرجع", + "import": "وارد کردن", + "export": "صدور", + "share-via": "{{provider}} اشتراک گذاري از طريق" + }, + "aggregation": { + "aggregation": "تجميع", + "function": "تابع تجميع داده ها", + "limit": "بيشترين مقادير", + "group-interval": "فاصله گروه بندي", + "min": "کمترين", + "max": "بيشترين", + "avg": "ميانگين", + "sum": "جمع", + "count": "شمارش", + "none": "هيچکدام" + }, + "admin": { + "general": "عمومي", + "general-settings": "تنظيمات عمومي", + "outgoing-mail": "پيام خروجي", + "outgoing-mail-settings": "تنظيمات پيام خروجي", + "system-settings": "تنظيمات سيستم", + "test-mail-sent": "!ارسال پيام آزمايشي موفقيت آميز بود", + "base-url": "مبنا URL", + "base-url-required": ".مبنا مورد نياز است URL", + "mail-from": "... پيام از", + "mail-from-required": ".پيام از ... مورد نياز است", + "smtp-protocol": "SMTP قرارداد", + "smtp-host": "SMTP ميزبان", + "smtp-host-required": ".مورد نياز است SMTP ميزبان", + "smtp-port": "SMTP درگاه", + "smtp-port-required": ".فراهم کنيد SMTP شما بايد يک درگاه", + "smtp-port-invalid": ".معتبر باشد SMTP به نظر نمي آيد يک درگاه", + "timeout-msec": "مهلت (msec)", + "timeout-required": ".مهلت مورد نياز است", + "timeout-invalid": ".مهلت، به نظر نمي آيد معتبر باشد", + "enable-tls": "TLS فعال سازي", + "tls-version": "نسخه TLS", + "send-test-mail": "ارسال پيام آزمايشي" + }, + "alarm": { + "alarm": "هشدار", + "alarms": "هشدارها", + "select-alarm": "انتخاب هشدار", + "no-alarms-matching": ".يافت نشد '{{entity}}' هيچ هشداري مطابق", + "alarm-required": "هشدار مورد نياز است", + "alarm-status": "وضعيت هشدار", + "search-status": { + "ANY": "هر", + "ACTIVE": "فعال", + "CLEARED": "پاک شده", + "ACK": "تصديق شده", + "UNACK": "تصديق نشده" + }, + "display-status": { + "ACTIVE_UNACK": "تصديق نشده فعال", + "ACTIVE_ACK": "تصديق شده فعال", + "CLEARED_UNACK": "تصديق نشده پاک شده", + "CLEARED_ACK": "تصديق شده پاک شده" + }, + "no-alarms-prompt": "هيچ هشداري يافت نشد", + "created-time": "زمان ايجاد", + "type": "نوع", + "severity": "شدت", + "originator": "مبدأ", + "originator-type": "نوع مبدأ", + "details": "جزئيات", + "status": "وضعيت", + "alarm-details": "جزئيات هشدار", + "start-time": "زمان شروع", + "end-time": "زمان پايان", + "ack-time": "زمان تصديق", + "clear-time": "زمان پاک شدن", + "severity-critical": "بحراني", + "severity-major": "مهم", + "severity-minor": "جزئي", + "severity-warning": "اخطار", + "severity-indeterminate": "نامشخص", + "acknowledge": "تصديق", + "clear": "پاک کردن", + "search": "جستجوي هشدارها", + "selected-alarms": "اننخاب شده { count, plural, 1 {1 هشدار} other {# هشدارها} }", + "no-data": "هيچ داده اي براي نمايش نيست", + "polling-interval": "هشدار دهنده فاصله نمونه برداري (sec)", + "polling-interval-required": ".هشدار دهنده فاصله نمونه برداري مورد نياز است", + "min-polling-interval-message": ".حداقل فاصله مجاز نمونه برداري، 1 ثانيه است", + "aknowledge-alarms-title": "{ count, plural, 1 {1 هشدار} other {# هشدارها} } تصديق", + "aknowledge-alarms-text": "اطمينان داريد؟ { count, plural, 1 {1 هشدار} other {# هشدارها} } آيا شما از تصديق", + "aknowledge-alarm-title": "تصديق هشدار", + "aknowledge-alarm-text": "آيا شما از تصديق هشدار اطمينان داريد؟", + "clear-alarms-title": "{ count, plural, 1 {1 هشدار} other {# هشدارها} } پاک کردن", + "clear-alarms-text": "اطمينان داريد؟ { count, plural, 1 {1 هشدار} other {# هشدارها} } آيا شما از پاک کردن", + "clear-alarm-title": "پاک کردن هشدار", + "clear-alarm-text": "آيا شما از پاک کردن هشدار اطمينان داريد؟", + "alarm-status-filter": "فيلتر وضعيت هشدار" + }, + "alias": { + "add": "افزودن نام مستعار", + "edit": "ويرايش نام مستعار", + "name": "نام مستعار", + "name-required": "نام مستعار مورد نياز است", + "duplicate-alias": ".در حال حاضر نام مستعار مشابهي وجود دارد", + "filter-type-single-entity": "موجودي تکي", + "filter-type-entity-list": "ليست موجودي", + "filter-type-entity-name": "نام موجودي", + "filter-type-state-entity": "موجودي از وضعيت داشبورد", + "filter-type-state-entity-description": "پارامترهاي موجودي گرفته شده از وضعيت داشبورد", + "filter-type-asset-type": "نوع دارايي", + "filter-type-asset-type-description": "'{{assetType}}' دارايي هاي نوع", + "filter-type-asset-type-and-name-description": ".شروع مي شود '{{prefix}}' که نامشان با '{{assetType}}' دارايي هاي نوع", + "filter-type-device-type": "نوع دستگاه", + "filter-type-device-type-description": "'{{deviceType}}' دستگاه هاي نوع", + "filter-type-device-type-and-name-description": ".شروع مي شود '{{prefix}}' که نامشان با '{{deviceType}}' دستگاه هاي نوع", + "filter-type-entity-view-type": "نوع نمايش موجودي", + "filter-type-entity-view-type-description": "'{{entityView}}' نمايش هاي موجودي نوع ", + "filter-type-entity-view-type-and-name-description": ".شروع مي شود '{{prefix}}' که نامشان با '{{entityView}}' نمايش هاي موجودي نوع", + "filter-type-relations-query": "پرس و جو درمورد ارتباطات", + "filter-type-relations-query-description": ". دارند {{direction}} {{rootEntity}} را {{relationType}} که ارتباط {{entities}}", + "filter-type-asset-search-query": "پرس و جو درمورد جستجوي دارايي", + "filter-type-asset-search-query-description": ".دارند {{direction}} {{rootEntity}} را {{relationType}} که ارتباط{{assetTypes}} دارايي ها از انواع", + "filter-type-device-search-query": "پرس و چو درمورد جستجوي دستگاه", + "filter-type-device-search-query-description": ".دارند {{direction}} {{rootEntity}} را {{relationType}} که ارتباط{{deviceTypes}} دستگاه ها از انواع", + "filter-type-entity-view-search-query": "پرس و جو درمورد جستجوي نمايش موجودي", + "filter-type-entity-view-search-query-description": ".دارند {{direction}} {{rootEntity}} را {{relationType}} که ارتباط{{entityViewTypes}} نمايش هاي موجودي از انواع", + "entity-filter": "فيلتر موجودي", + "resolve-multiple": "تصميم با توجه به موجودي هاي متعدد", + "filter-type": "نوع فيلتر", + "filter-type-required": ".نوع فيلتر مورد نياز است", + "entity-filter-no-entity-matched": ".هيچ موجودي منطبق بر فيلتر مشخص شده يافت نشد", + "no-entity-filter-specified": ".هيچ فيلتر موجودي اي تعيين نشده است", + "root-state-entity": "موجودي وضعيت داشبورد به عنوان پايه استفاده شود", + "root-entity": "موجودي پايه", + "state-entity-parameter-name": "نام پارامتر موجودي وضعيت", + "default-state-entity": "موجودي وضعيت پيش فرض", + "default-entity-parameter-name": "به صورت پيش فرض", + "max-relation-level": "بالاترين سطح ارتباط", + "unlimited-level": "سطح نامحدود", + "state-entity": "موجودي وضعيت داشبورد", + "all-entities": "تمام موجودي ها", + "any-relation": "هر" + }, + "asset": { + "asset": "دارايي", + "assets": "دارايي ها", + "management": "مديريت دارايي", + "view-assets": "نمايش دارايي ها", + "add": "افزودن دارايي", + "assign-to-customer": "تخصيص به مشتري", + "assign-asset-to-customer": "تخصيص دارايي(ها) به مشتري", + "assign-asset-to-customer-text": "لطفا دارايي ها را انتخاب کنيد تا به مشتري تخصيص يابد", + "no-assets-text": "هيچ دارايي اي يافت نشد", + "assign-to-customer-text": "لطفا مشتري را انتخاب کنيد تا دارايي(ها) تخصيص يابد", + "public": "عمومي", + "assignedToCustomer": "تخصيص يافته به مشتري", + "make-public": "عمومي سازي دارايي", + "make-private": "شخصي سازي دارايي", + "unassign-from-customer": "لغو تخصيص از مشتري", + "delete": "حذف دارايي", + "asset-public": "دارايي عمومي است", + "asset-type": "نوع دارايي", + "asset-type-required": ".نوع دارايي مورد نياز است", + "select-asset-type": "انتخاب کردن نوع دارايي", + "enter-asset-type": "وارد کردن نوع دارايي", + "any-asset": "هر دارايي", + "no-asset-types-matching": ".يافت نشد '{{entitySubtype}}' هيچ دارايي منطبق بر", + "asset-type-list-empty": ".هيچيک از انواع دارايي انتخاب نشد", + "asset-types": "انواع دارايي", + "name": "نام", + "name-required": ".نام مورد نياز است", + "description": "توصيف", + "type": "نوع", + "type-required": ".نوع مورد نياز است", + "details": "جزئيات", + "events": "رويدادها", + "add-asset-text": "افزودن دارايي جديد", + "asset-details": "جزئيات دارايي", + "assign-assets": "تخصيص دارايي ها", + "assign-assets-text": "به مشتري { count, plural, 1 {1 دارايي} other {# دارايي} } تخصيص", + "delete-assets": "حذف دارايي ها", + "unassign-assets": "لغو تخصيص دارايي ها", + "unassign-assets-action-title": "از مشتري { count, plural, 1 {1 دارايي} other {# دارايي} } لغو تخصيص", + "assign-new-asset": "تخصيص دارايي جديد", + "delete-asset-title": "مطمئنيد؟ '{{assetName}}' آيا از حذف دارايي", + "delete-asset-text": ".مراقب باشيد، پس از تأييد، دارايي و تمام داده هاي مربوطه غير قابل بازيابي مي شوند", + "delete-assets-title": "مطمئنيد؟ { count, plural, 1 {1 دارايي} other {# دارايي} } آيا از حذف", + "delete-assets-action-title": "{ count, plural, 1 {1 دارايي} other {# دارايي} } حذف", + "delete-assets-text": ".مراقب باشيد، پس از تأييد، تمام دارايي هاي انتخاب شده حذف، و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", + "make-public-asset-title": "مطمئنيد؟ '{{assetName}}' آيا از عمومي سازي", + "make-public-asset-text": ".پس از تأييد، دارايي و تمامي داده هايش عمومي و قابل دسترسي براي ديگران مي شود", + "make-private-asset-title": "مطمئنيد؟ '{{assetName}}' آيا از شخصي سازي دارايي", + "make-private-asset-text": ".پس از تأييد، دارايي و تمامي داده هايش شخصي و خارج از دسترس ديگران مي شوند", + "unassign-asset-title": "مطمئنيد؟ '{{assetName}}' آيا از لغو تخصيص دارايي", + "unassign-asset-text": ".پس از تأييد، دارايي، لغو تخصيص و خارج از دسترس مشتري مي شود", + "unassign-asset": "لغو تخصيص دارايي", + "unassign-assets-title": "مطمئنيد؟ { count, plural, 1 {1 دارايي} other {# دارايي} } آيا از لغو تخصيص", + "unassign-assets-text": ".پس از تأييد، تمام دارايي هاي انتخاب شده، لغو تخصيص و خارج از دسترس مشتري مي شوند", + "copyId": "دارايي ID رونوشت از", + "idCopiedMessage": "دارايي در حافظه موقت رونوشت شد ID", + "select-asset": "انتخاب دارايي", + "no-assets-matching": ".يافت نشد '{{entity}}' هيچ دارايي منطبق بر", + "asset-required": "دارايي مود نياز است", + "name-starts-with": "نام دارايي شروع مي شود با", + "label": "برچسب" + }, + "attribute": { + "attributes": "ويژگي ها", + "latest-telemetry": "آخرين سنجش", + "attributes-scope": "حوزه ويژگي هاي موجودي", + "scope-latest-telemetry": "آخرين سنجش", + "scope-client": "ويژگي هاي مشتري", + "scope-server": "ويژگي هاي سِروِر", + "scope-shared": "ويژگي هاي مشترک", + "add": "افزودن ويژگي ها", + "key": "کليد", + "last-update-time": "آخرين زمان به روز رساني", + "key-required": ".کليد ويژگي مورد نياز است", + "value": "مقدار", + "value-required": ".مقدار ويژگي مورد نياز است", + "delete-attributes-title": "مطمئنيد؟ { count, plural, 1 {1 ويژگي} other {# ويژگي} } آيا از حذف", + "delete-attributes-text": ".مراقب باشيد، پس از تأييد، تمام ويژگي هاي انتخاب شده حذف مي گردند", + "delete-attributes": "حذف ويژگي ها", + "enter-attribute-value": "وارد کردن مقدار ويژگي", + "show-on-widget": "نمايش بر ويجت", + "widget-mode": "حالت ويجت", + "next-widget": "ويجت بعد", + "prev-widget": "ويجت قبل", + "add-to-dashboard": "افزودن به داشبورد", + "add-widget-to-dashboard": "افزودن ويجت به داشبورد", + "selected-attributes": "انتخاب شدند { count, plural, 1 {1 ويژگي} other {# ويژگي} }", + "selected-telemetry": "انتخاب شد { count, plural, 1 {1 واحد سنجش} other {# واحد سنجش} }" + }, + "audit-log": { + "audit": "بازبيني", + "audit-logs": "داده هاي ثبت شده از بازبيني", + "timestamp": "برچسب زمان", + "entity-type": "نوع موحودي", + "entity-name": "نام موجودي", + "user": "کاربر", + "type": "نوع", + "status": "وضعيت", + "details": "جزئيات", + "type-added": "اضافه شده", + "type-deleted": "حذف شده", + "type-updated": "به روز", + "type-attributes-updated": "ويژگي ها به روز شد", + "type-attributes-deleted": "ويژگي ها حذف شد", + "type-rpc-call": "RPC فراخواني", + "type-credentials-updated": "اعتبارنامه ها به روز شد", + "type-assigned-to-customer": "به مشتري تخصيص يافت", + "type-unassigned-from-customer": "از مشتري لغو تخصيص شد", + "type-activated": "فعال شد", + "type-suspended": "معلق", + "type-credentials-read": "اعتبارنامه ها خوانده شد", + "type-attributes-read": "ويژگي ها خوانده شد", + "type-relation-add-or-update": "ارتباط به روز شد", + "type-relation-delete": "ارتباط حذف شد", + "type-relations-delete": "تمام ارتباطات حذف شد", + "type-alarm-ack": "تصديق شده", + "type-alarm-clear": "پاک شده", + "status-success": "موفقيت", + "status-failure": "عدم موفقيت", + "audit-log-details": "بازبيني جزئيات ثبت داده ها", + "no-audit-logs-prompt": "هيچ داده ثبت شده اي يافت نشد", + "action-data": "داده هاي اقدام", + "failure-details": "جزئيات عدم موفقيت", + "search": "جستجوي داده هاي ثبت شده از بازبيني", + "clear-search": "پاک کردن جستجو" + }, + "confirm-on-exit": { + "message": "شما تغييراتي ذخيره نشده داريد. از ترک اين صفحه مطمئنيد؟", + "html-message": "از ترک اين صفحه مطمئنيد؟
    .شما تغييراتي ذخيره نشده داريد", + "title": "تغييرات ذخيره نشده " + }, + "contact": { + "country": "کشور", + "city": "شهر", + "state": "استان / ايالت", + "postal-code": "کد پستي", + "postal-code-invalid": ".قالب کد پستي نامعتبر است", + "address": "نشاني", + "address2": "2 نشاني", + "phone": "تلفن", + "email": "پست الکترونيک", + "no-address": "بدون آدرس" + }, + "common": { + "username": "نام کاربري", + "password": "رمز عبور", + "enter-username": "وارد کردن نام کاربري", + "enter-password": "وارد کردن رمز عبور", + "enter-search": "وارد کردن جستجو", + "created-time": "زمان ايجاد" + }, + "content-type": { + "json": "JSON", + "text": "Text", + "binary": "Binary (Base64)" + }, + "customer": { + "customer": "مشتري", + "customers": "مشتريان", + "management": "مديريت مشتري", + "dashboard": "داشبورد مشتري", + "dashboards": "داشبوردهاي مشتري", + "devices": "دستگاه هاي مشتري", + "entity-views": "نمايش موجودي مشتري", + "assets": "دارايي هاي مشتري", + "public-dashboards": "داشبوردهاي عمومي", + "public-devices": "دستگاه هاي عمومي", + "public-assets": "دارايي هاي عمومي", + "public-entity-views": "نمايش موجودي عمومي", + "add": "افزودن مشتري", + "delete": "حذف مشتري", + "manage-customer-users": "مديريت کاربرهاي مشتري", + "manage-customer-devices": "مديريت دستگاه هاي مشتري", + "manage-customer-dashboards": "مديريت داشبوردهاي مشتري", + "manage-public-devices": "مديريت دستگاه هاي عمومي", + "manage-public-dashboards": "مديريت داشبوردهاي عمومي", + "manage-customer-assets": "مديريت دارايي هاي مشتري", + "manage-public-assets": "مديريت دارايي هاي عمومي", + "add-customer-text": "افزودن مشتري جديد", + "no-customers-text": "هيچ مشتري اي يافت نشد", + "customer-details": "جزئيات اطلاعات مشتري", + "delete-customer-title": "مطمئنيد؟ '{{customerTitle}}' از حذف مشتري", + "delete-customer-text": ".مراقب باشيد، پس از تأييد، مشتري و تمامي داده هاي مربوطه، غير قابل بازيابي مي شوند", + "delete-customers-title": "مطمئنيد؟ { count, plural, 1 {1 مشتري} other {# مشتري} } از حذف", + "delete-customers-action-title": "{ count, plural, 1 {1 مشتري} other {# مشتري} } حذف", + "delete-customers-text": ".مراقب باشيد، پس از تأييد، تمام مشتريانِ انتخاب شده حذف، و تمامي داده هاي مربوطه غير قابل دسترسي مي شوند", + "manage-users": "مديريت کاربرها", + "manage-assets": "مديريت دارايي ها", + "manage-devices": "مديريت دستگاه ها", + "manage-dashboards": "مديريت داشبوردها", + "title": "عنوان", + "title-required": ".عنوان مورد نياز است", + "description": "توصيف", + "details": "جزئيات", + "events": "رويدادها", + "copyId": "مشتري ID رونوشت از", + "idCopiedMessage": "مشتري در حافظه موقت رونوشت شد ID", + "select-customer": "انتخاب مشتري", + "no-customers-matching": ".يافت نشد '{{entity}}' هيچ مشتري منطبق بر", + "customer-required": "مشتري مورد نياز است", + "select-default-customer": "انتخاب مشتري پيش فرض", + "default-customer": "مشتري پيش فرض", + "default-customer-required": "جهت عيب يابي داشبورد در سطح کاربر مياني، مشتري پيش فرض مورد نياز است" + }, + "datetime": { + "date-from": "تاريخ از", + "time-from": "زمان از", + "date-to": "تاريخ تا", + "time-to": "زمان تا" + }, + "dashboard": { + "dashboard": "داشبورد", + "dashboards": "داشبوردها", + "management": "مديريت داشبورد", + "view-dashboards": "نمايش داشبوردها", + "add": "افزودن داشبورد", + "assign-dashboard-to-customer": "تخصيص داشبورد(ها) به مشتري", + "assign-dashboard-to-customer-text": "لطفا داشبوردها را، جهت تخصيص به مشتري، انتخاب کنيد", + "assign-to-customer-text": "لطفا مشتري را، جهت تخصيص داشبورد(ها)، انتخاب کنيد", + "assign-to-customer": "تخصيص به مشتري", + "unassign-from-customer": "لغو تخصيص از مشتري", + "make-public": "عمومي سازي مشتري", + "make-private": "شخصي سازي داشبورد", + "manage-assigned-customers": "مديريت مشتريان تخصيص داده شده", + "assigned-customers": "مشتريان تخصيص داده شده", + "assign-to-customers": "تخصيص داشبورد(ها) به مشتريان", + "assign-to-customers-text": "لطفا مشتريان را، جهت تخصيص داشبورد(ها)، انتخاب کنيد", + "unassign-from-customers": "لغو تخصيص داشبوردها از مشتريان", + "unassign-from-customers-text": "لطفا مشتريان را، جهت لغو تخصيص از داشبورد(ها)، انتخاب کنيد", + "no-dashboards-text": "هيچ داشبوردي يافت نشد", + "no-widgets": "هيچ ويجتي پيکربندي نشده است", + "add-widget": "افزودن ويجت جديد", + "title": "عنوان", + "select-widget-title": "انتخاب ويجت", + "select-widget-subtitle": "ليست انواع ويجت هاي در دسترس", + "delete": "حذف داشبورد", + "title-required": ".عنوان مورد نياز است", + "description": "توصيف", + "details": "جزئيات", + "dashboard-details": "جزئيات داشبورد", + "add-dashboard-text": "افزودن داشبورد جديد", + "assign-dashboards": "تخصيص داشبوردها", + "assign-new-dashboard": "تخصيص داشبورد جديد", + "assign-dashboards-text": "به مشتريان { count, plural, 1 {1 داشبورد} other {# داشبورد} } تخصيص", + "unassign-dashboards-action-text": "از مشتريان { count, plural, 1 {1 داشبورد} other {# داشبورد} } لغو تخصيص", + "delete-dashboards": "حذف داشبوردها", + "unassign-dashboards": "لغو تخصيص داشبوردها", + "unassign-dashboards-action-title": "از مشتري { count, plural, 1 {1 داشبورد} other {# داشبورد} } لغو تخصيص", + "delete-dashboard-title": "مطمئنيد؟ '{{dashboardTitle}}' از حذف", + "delete-dashboard-text": ".مراقب باشيد، پس از تأييد، داشبورد و تمامي داده هاي مربوطه، غير قابل بازيابي مي شوند", + "delete-dashboards-title": "مطمئنيد؟ { count, plural, 1 {1 داشبورد} other {# داشبورد} } از حذف", + "delete-dashboards-action-title": "{ count, plural, 1 {1 داشبورد} other {# داشبورد} } حذف", + "delete-dashboards-text": ".مراقب باشيد، پس از تأييد، تمام داشبوردهاي انتخاب شده حذف، و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند ", + "unassign-dashboard-title": "مطمئنيد؟ '{{dashboardTitle}}' از لغو تخصيص داشبورد", + "unassign-dashboard-text": ".پس از تأييد، داشبورد، لغو تخصيص و خارج از دسترس مشتري مي شود", + "unassign-dashboard": "لغو تخصيص داشبورد", + "unassign-dashboards-title": "مطمئنيد؟ { count, plural, 1 {1 داشبورد} other {# داشبورد} } از لغو تخصيص", + "unassign-dashboards-text": ".پس از تأييد، تمام داشبوردهاي انتخاب شده، لغو تخصيص و خارج از دسترس مشتري مي شوند", + "public-dashboard-title": "داشبورد اکنون عمومي است", + "public-dashboard-text": ":قابل دسترسي است اکنون عمومي بوده و از طريق پيوند عمومي ديگر ، {{dashboardTitle}} ،داشبورد شما", + "public-dashboard-notice": ".فراموش نکنيد براي دسترسي به داده هاي دستگاه هاي مربوطه، آنها را عمومي نماييد :توجه", + "make-private-dashboard-title": "مطمئنيد؟ '{{dashboardTitle}}' از شخصي سازي داشبورد", + "make-private-dashboard-text": ".پس از تأييد، داشبورد، شخصي و خارج از دسترس ديگران مي شود", + "make-private-dashboard": "شخصي سازي داشبورد", + "socialshare-text": "ThingsBoard طراحي شده توسط '{{dashboardTitle}}'", + "socialshare-title": "ThingsBoard طراحي شده توسط '{{dashboardTitle}}'", + "select-dashboard": "انتخاب داشبورد", + "no-dashboards-matching": ".يافت نشد '{{entity}}' هيچ داشبوردي منطبق بر", + "dashboard-required": ".داشبورد مورد نياز است", + "select-existing": "انتخاب داشبورد موجود", + "create-new": "ايجاد داشبورد جديد", + "new-dashboard-title": "عنوان داشبورد جديد", + "open-dashboard": "باز کردن داشبورد", + "set-background": "تنظيم پس زمينه", + "background-color": "رنگ پس زمينه", + "background-image": "تصوير پس زمينه", + "background-size-mode": "حالت اندازه پس زمينه", + "no-image": "هيچ تصويري انتخاب نشد", + "drop-image": ".جهت بارگذاري يک تصوير، آن را با موس کِشيده و رها کنيد، و يا روي آن کليک نماييد", + "settings": "تنظيمات", + "columns-count": "شمارش ستون ها", + "columns-count-required": ".شمارش ستون ها مورد نياز است", + "min-columns-count-message": ".کمترين تعداد مجاز ستون ها 10 عدد است", + "max-columns-count-message": ".بيشترين تعداد مجاز ستون ها 1000 عدد است", + "widgets-margins": "حاشيه بين ويجت ها", + "horizontal-margin": "حاشيه افقي", + "horizontal-margin-required": ".مقدار حاشيه افقي مورد نياز است", + "min-horizontal-margin-message": ".کمترين مقدار مجاز حاشيه افقي 0 است", + "max-horizontal-margin-message": ".بيشترين مقدار مجاز حاشيه افقي 50 است", + "vertical-margin": "حاشيه عمودي", + "vertical-margin-required": ".حاشيه عمودي مورد نياز است", + "min-vertical-margin-message": ".کمترين مقدار مجاز حاشيه عمودي 0 است", + "max-vertical-margin-message": ".بيشترين مقدار مجاز حاشيه افقي 50 است", + "autofill-height": "تنظيم خودکار ارتفاع چيدمان طرح", + "mobile-layout": "تنظيمات چيدمان طرح در تلفن همراه", + "mobile-row-height": "(px) ارتفاع رديف در تلفن همراه", + "mobile-row-height-required": ".مقدار ارتفاع ردبف در تلفن همراه مورد نياز است", + "min-mobile-row-height-message": ".کمترين مقدار مجاز ارتفاع رديف در تلفن همراه 5 پيکسل است", + "max-mobile-row-height-message": ".بيشترين مقدار مجاز ارتفاع رديف در تلفن همراه 200 پيکسل است", + "display-title": "نمايش عنوان داشبورد", + "toolbar-always-open": "باز نگه داشتن نوار ابزار", + "title-color": "رنگ عنوان", + "display-dashboards-selection": "نمايش انتخاب داشبوردها", + "display-entities-selection": "نمايش انتخاب موجودي ها", + "display-dashboard-timewindow": "نمايش پنجره زمان", + "display-dashboard-export": "نمايش صدور", + "import": "وارد کردن داشبورد", + "export": "صادر کردن داشبورد", + "export-failed-error": "{{error}} :صدور داشبورد ممکن نيست", + "create-new-dashboard": "ايجاد داشبورد جديد", + "dashboard-file": "پرونده داشبورد", + "invalid-dashboard-file-error": ".وارد کردن داشبورد ممکن نيست: ساختار داده داشبورد نامعتبر است", + "dashboard-import-missing-aliases-title": "پيکربندي نامهاي مستعار استفاده شده توسط داشبوردِ وارده", + "create-new-widget": "ايجاد ويجت جديد", + "import-widget": "وارد کردن ويجت", + "widget-file": "پرونده ويجت", + "invalid-widget-file-error": ".وارد کردن ويجت ممکن نيست: ساختار داده ويجت نامعتبر است", + "widget-import-missing-aliases-title": "پيکربندي نامهاي مستعار استفاده شده توسط ويجتِ وارده", + "open-toolbar": "باز کردن نوار ابزار داشبورد", + "close-toolbar": "بستن نوار ابزار", + "configuration-error": "خطاي پيکربندي", + "alias-resolution-error-title": "خطاي پيکربندي نامهاي مستعار داشبورد", + "invalid-aliases-config": ".لطفا جهت حل اين موضوع با مسئول مربوط به خود تماس بگيريد
    .يافتن دستگاهي منطبق بر فبلتر بعضي نامهاي مستعار ممکن نيست", + "select-devices": "انتخاب دستگاه ها", + "assignedToCustomer": "تخصيص يافته به مشتري", + "assignedToCustomers": "تخصيص يافته به مشتريان", + "public": "عمومي", + "public-link": "پيوند عمومي", + "copy-public-link": "رونوشت از پيوند عمومي", + "public-link-copied-message": "پيوند عمومي داشبورد در حافظه موقت رونوشت شد", + "manage-states": "مديريت وضعيت هاي داشبورد", + "states": "وضعيت هاي داشبورد", + "search-states": "جستجوي وضعيت هاي داشبورد", + "selected-states": "انتخاب شدند { count, plural, 1 {1 وضعيت داشبورد} other {# وضعيت داشبورد} }", + "edit-state": "ويرايش وضعيت داشبورد", + "delete-state": "حذف وضعيت داشبورد", + "add-state": "افزودن وضعيت داشبورد", + "state": "وضعيت داشبورد", + "state-name": "نام", + "state-name-required": ".نام وضعيت داشبورد مورد نياز است", + "state-id": "وضعيت ID", + "state-id-required": ".وضعيت داشبورد مورد نياز است ID", + "state-id-exists": ".مشابه موجود است ID در حال حاضر وضعيت داشبوردي با", + "is-root-state": "وضعيت پايه", + "delete-state-title": "حذف وضعيت داشبورد", + "delete-state-text": "مطمئنيد؟ '{{stateName}}' از حذف وضعيت داشبورد با نام", + "show-details": "نمايش جزئيات", + "hide-details": "پنهان کردن جزئيات", + "select-state": "انتخاب وضعيت هدف", + "state-controller": "کنترل کننده وضعيت" + }, + "datakey": { + "settings": "تنظيمات", + "advanced": "پيشرفته", + "label": "برچسب", + "color": "رنگ", + "units": "کارکتر خاص براي نمايش بعد از مقدار تعين شده", + "decimals": "تعداد ارقام بعد از مميّز شناور", + "data-generation-func": "تابع توليد داده", + "use-data-post-processing-func": "استفاده از تابع پس پردازش داده", + "configuration": "پيکربندي کليد داده", + "timeseries": "سري هاي زماني", + "attributes": "ويژگي ها", + "alarm": "حوزه هاي هشدار", + "timeseries-required": ".سري هاي زماني موجودي مورد نياز است", + "timeseries-or-attributes-required": ".سري هاي زماني / ويژگي هاي موجودي مورد نياز است", + "maximum-timeseries-or-attributes": "{ count, plural, 1 {.1 سري زماني / ويژگي مجاز است} other {# سري زماني / ويژگي مجازند} } بيشترين", + "alarm-fields-required": ".حوزه هاي هشدار مورد نياز است", + "function-types": "نوع توابع", + "function-types-required": ".نوع تابع مورد نياز است", + "maximum-function-types": "{ count, plural, 1 {.1 نوع تابع مجاز است} other {# نوع تابع مجازند} } بيشترين", + "time-description": "برچسب زماني مقدار فعلي؛", + "value-description": "مقدار فعلي؛", + "prev-value-description": "نتيجه ي فراخوانيِ تابع قبلي؛", + "time-prev-description": "برچسب زماني مقدار قبلي؛", + "prev-orig-value-description": "ممقدار اصلي قبلي" + }, + "datasource": { + "type": "نوع منبع داده", + "name": "نام", + "add-datasource-prompt": "لطفا منبع داده را اضافه کنيد" + }, + "details": { + "edit-mode": "حالت ويرايش", + "toggle-edit-mode": "حالت ويرايش را تغيير دهيد" + }, + "device": { + "device": "دستگاه", + "device-required": ".دستگاه مورد نياز است", + "devices": "دستگاه ها", + "management": "مديريت دستگاه", + "view-devices": "نمايش دستگاه ها", + "device-alias": "نام مستعار دستگاه", + "aliases": "نامهاي مستعار دستگاه", + "no-alias-matching": ".يافت نشد'{{alias}}'", + "no-aliases-found": ".هيچ نام مستعاري يافت نشد", + "no-key-matching": ".يافت نشد'{{key}}'", + "no-keys-found": ".هيچ کليدي يافت نشد", + "create-new-alias": "!ايجاد يک نام مستعار جديد", + "create-new-key": "!ايجاد يک کليد جديد", + "duplicate-alias-error": ".نام مستعار در داشبورد بايد يکتا باشد
    '{{alias}}'نام مستعار مشابه يافت شد", + "configure-alias": "نام مستعار '{{alias}}' پيکربندي", + "no-devices-matching": "مطابقت داشته باشد وجود ندارد '{{entity}}' هيچ دستگاهي که با ", + "alias": "نام مستعار", + "alias-required": ".نام مستعار مورد نياز است", + "remove-alias": "حذف نام مستعار دستگاه", + "add-alias": "افزودن نام مستعار دستگاه", + "name-starts-with": "اسم دستگاه شروع مي شود با", + "device-list": "ليست دستگاه ها", + "use-device-name-filter": "از فيلتر استفاده کنيد", + "device-list-empty": ".هيچ دستگاهي انتخاب نشده است", + "device-name-filter-required": ".فيلتر نام دستگاه مورد نياز است", + "device-name-filter-no-device-matched": ".شروع شود يافت نشد '{{device}}' هيچ دستگاهي که با", + "add": "افزودن دستگاه", + "assign-to-customer": "تخصيص به مشتري", + "assign-device-to-customer": "تخصيص دستگاه (ها) به مشتري", + "assign-device-to-customer-text": "لطفا دستگاه ها را انتخاب کنيد تا به مشتري تخصيص يابد", + "make-public": "عمومي سازي دستگاه", + "make-private": "شخصي سازي دستگاه", + "no-devices-text": "هيچ دستگاهي يافت نشد", + "assign-to-customer-text": "لطفا مشتري را انتخاب کنيد تا دستگاه(ها) تخصيص يابد", + "device-details": "جزئيات دستگاه", + "add-device-text": "افزودن دستگاه جديد", + "credentials": "اعتبارنامه ها", + "manage-credentials": "مديريت اعتبارنامه ها", + "delete": "حذف دستگاه", + "assign-devices": "تخصيص دستگاه ها", + "assign-devices-text": "به مشتري { count, plural, 1 {1 دستگاه} other {# دستگاه} } تخصيص", + "delete-devices": "حذف دستگاه ها", + "unassign-from-customer": "لغو تخصيص از مشتري", + "unassign-devices": "لغو تخصيص دستگاه ها", + "unassign-devices-action-title": "از مشتري { count, plural, 1 {1 دستگاه} other {# دستگاه} } لغو تخصيص", + "assign-new-device": "تخصيص دستگاه جديد", + "make-public-device-title": "مطمئنيد؟ '{{deviceName}}' از عمومي سازي دستگاه", + "make-public-device-text": ".پس از تأييد، دستگاه و تمامي داده هايش عمومي و قابل دسترسي براي ديگران مي شود", + "make-private-device-title": "مطمئنيد؟ '{{deviceName}}' از شخصي سازي دستگاه", + "make-private-device-text": ".پس از تأييد، دستگاه و تمامي داده هايش شخصي و خارج از دسترس ديگران مي شوند", + "view-credentials": "نمايش اعتبارنامه ها", + "delete-device-title": "مطمئنيد؟ '{{deviceName}}' از حذف", + "delete-device-text": ".مراقب باشيد، پس از تأييد، دستگاه و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", + "delete-devices-title": "مطمئنيد؟ { count, plural, 1 {1 دستگاه} other {# دستگاه} } از حذف", + "delete-devices-action-title": "{ count, plural, 1 {1 دستگاه} other {# دستگاه} } حذف", + "delete-devices-text": ".مراقب باشيد، پس از تأييد، تمام دستگاه هاي انتخاب شده، حذف، و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", + "unassign-device-title": "مطمئنيد؟ '{{deviceName}}' از لغو تخصيص", + "unassign-device-text": ".پس از تأييد، دستگاه، لغو تخصيص و خارج از دسترس مشتري مي شود", + "unassign-device": "لغو تخصيص دستگاه", + "unassign-devices-title": "مطمئنيد؟ { count, plural, 1 {1 دستگاه} other {# دستگاه} } از لغو تخصيص", + "unassign-devices-text": ".پس از تأييد، تمام دستگاه هاي انتخاب شده، لغو تخصيص و خارج از دسترس مشتري مي شوند", + "device-credentials": "اعتبارنامه هاي دستگاه", + "credentials-type": "نوع اعتبارنامه ها", + "access-token": "شناسه دسترسي", + "access-token-required": ".شناسه دسترسي مورد نياز است", + "access-token-invalid": ".طول شناسه دسترسي بايد از 1 تا 20 حرف باشد", + "rsa-key": "RSA کليد عمومي", + "rsa-key-required": ".مورد نياز است RSA کليد عمومي", + "secret": "محرمانه", + "secret-required": ".شناسه محرمانه مورد نياز است", + "device-type": "نوع دستگاه", + "device-type-required": ".نوع دستگاه مورد نياز است", + "select-device-type": "انتخاب نوع دستگاه", + "enter-device-type": "وارد کردن نوع دستگاه", + "any-device": "هر دستگاهي", + "no-device-types-matching": ".يافت نشد '{{entitySubtype}}' هيچ نوع دستگاهي منطبق بر", + "device-type-list-empty": ".هيچ نوع دستگاهي انتخاب نشد", + "device-types": "انواع دستگاه", + "name": "نام", + "name-required": ".نام مورد نياز است", + "description": "توصيف", + "events": "رويدادها", + "details": "جزئيات", + "copyId": "دستگاه ID رونوشت از", + "copyAccessToken": "رونوشت از شناسه دسترسي", + "idCopiedMessage": ".دستگاه در حافظه موقت رونوشت شد ID", + "accessTokenCopiedMessage": ".شناسه دسترسي دستگاه در حافظه موقت رونوشت شد", + "assignedToCustomer": "تخصيص يافته به مشتري", + "unable-delete-device-alias-title": "حذف نام مستعار دستگاه ممکن نيست", + "unable-delete-device-alias-text": "
    {{widgetsList}} :را تا زمان استفاده توسط ويجت(هاي) زير نمي توان حذف کرد ، '{{deviceAlias}}' ،نام مستعار دستگاه", + "is-gateway": "درگاه است", + "public": "عمومي", + "device-public": "دستگاه عمومي است", + "select-device": "انتخاب دستگاه" + }, + "dialog": { + "close": "بستن گفتگو" + }, + "error": { + "unable-to-connect": ".اتصال به سِروِر ممکن نيست! لطفا اتصال اينترنت خود را بررسي کنيد", + "unhandled-error-code": "{{errorCode}} :کد خطاي رسيدگي نشده", + "unknown-error": "خطاي ناشناخته" + }, + "entity": { + "entity": "موجودي", + "entities": "موجودي ها", + "aliases": "نامهاي مستعار موجودي", + "entity-alias": "نام مستعار موجودي", + "unable-delete-entity-alias-title": "حذف نام مستعار موجودي ممکن نيست", + "unable-delete-entity-alias-text": "
    {{widgetsList}} :را تا زمان استفاده توسط ويجت(هاي) زير نمي توان حذف کرد ، '{{entityAlias}}' ،نام مستعار موجودي", + "duplicate-alias-error": ".نامهاي مستعار موجودي بايد در داشبورد، منحصر بفرد باشند
    .يافت شد '{{alias}}' نام مستعار تکراري", + "missing-entity-filter-error": ".مفقود است '{{alias}}' فيلتر براي نام مستعار", + "configure-alias": "'{{alias}}' پيکربندي نام مستعار", + "alias": "نام مستعار", + "alias-required": ".نام مستعار موجودي مورد نياز است", + "remove-alias": "حذف نام مستعار موجودي", + "add-alias": "افزودن نام مستعار موجودي", + "entity-list": "ليست موجودي", + "entity-type": "نوع موجودي", + "entity-types": "انواع موجودي", + "entity-type-list": "ليست نوع موجودي", + "any-entity": "هر موجودي", + "enter-entity-type": "وارد کردن نوع موجودي", + "no-entities-matching": ".يافت نشد '{{entity}}' هيچ موجودي منطبق بر", + "no-entity-types-matching": ".يافت نشد '{{entityType}}' هيچ نوع موجودي منطبق بر", + "name-starts-with": "نام شروع مي شود با", + "use-entity-name-filter": "استفاده از فيلتر", + "entity-list-empty": ".هيچ موجودي اي انتخاب نشده است", + "entity-type-list-empty": ".هيچ نوع موجودي انتخاب نشده است", + "entity-name-filter-required": ".فيلتر نام موجودي مورد نياز است", + "entity-name-filter-no-entity-matched": ".شروع شود يافت نشد '{{entity}}' هيچ موجودي که با", + "all-subtypes": "همه", + "select-entities": "انتخاب موجودي ها", + "no-aliases-found": ".هيچ نام مستعاري يافت نشد", + "no-alias-matching": ".يافت نشد '{{alias}}'", + "create-new-alias": "!ايجاد يک نام مستعار جديد", + "key": "کليد", + "key-name": "نام کليد", + "no-keys-found": ".هيچ کليدي يافت نشد", + "no-key-matching": "'.يافت نشد {{key}}'", + "create-new-key": "!ايجاد يک کليد جديد", + "type": "نوع", + "type-required": ".نوع موجودي مورد نياز است", + "type-device": "دستگاه", + "type-devices": "دستگاه ها", + "list-of-devices": "{ count, plural, 1 {يک دستگاه} other {ليست # دستگاه} }", + "device-name-starts-with": "شروع مي شود '{{prefix}}' دستگاه هايي که نامشان با", + "type-asset": "دارايي", + "type-assets": "دارايي ها", + "list-of-assets": "{ count, plural, 1 {يک دارايي} other {ليست # دارايي} }", + "asset-name-starts-with": "شروع مي شود '{{prefix}}' دارايي هايي که نامشان با", + "type-entity-view": "نمايش موجودي", + "type-entity-views": "نمايش هاي موجودي", + "list-of-entity-views": "{ count, plural, 1 {يک نمايش موجودي} other {ليست # نمايش موجودي} }", + "entity-view-name-starts-with": "شروع مي شود '{{prefix}}' نمايش هاي موجودي که نامشان با", + "type-rule": "قاعده", + "type-rules": "قواعد", + "list-of-rules": "{ count, plural, 1 {يک قاعده} other {ليست # قاعده} }", + "rule-name-starts-with": "شروع مي شود '{{prefix}}' قواعدي که نامشان با", + "type-plugin": "ابزار جانبي", + "type-plugins": "ابزارهاي جانبي", + "list-of-plugins": "{ count, plural, 1 {يک ابزار جانبي} other {ليست # ابزار جانبي} }", + "plugin-name-starts-with": "شروع مي شود '{{prefix}}' ابزارهاي جانبي که نامشان با", + "type-tenant": "کاربر", + "type-tenants": "کاربران", + "list-of-tenants": "{ count, plural, 1 {يک کاربر} other {ليست # کاربر} }", + "tenant-name-starts-with": "شروع مي شود '{{prefix}}' کاربرهايي که نامشان با", + "type-customer": "مشتري", + "type-customers": "مشتريان", + "list-of-customers": "{ count, plural, 1 {يک مشتري} other {ليست # مشتري} }", + "customer-name-starts-with": "شروع مي شود '{{prefix}}' مشترياني که نامشان با", + "type-user": "کاربر", + "type-users": "کاربران", + "list-of-users": "{ count, plural, 1 {يک کاربر} other {ليست # کاربر} }", + "user-name-starts-with": "شروع مي شود '{{prefix}}' کاربرهايي که نامشان با", + "type-dashboard": "داشبورد", + "type-dashboards": "داشبوردها", + "list-of-dashboards": "{ count, plural, 1 {يک داشبورد} other {ليست # داشبورد} }", + "dashboard-name-starts-with": "شروع مي شود '{{prefix}}' داشبوردهايي که نامشان با", + "type-alarm": "هشدار", + "type-alarms": "هشدارها", + "list-of-alarms": "{ count, plural, 1 {يک هشدار} other {ليست # هشدار} }", + "alarm-name-starts-with": "شروع مي شود '{{prefix}}' هشدارهايي که نامشان با", + "type-rulechain": "زنجيره قواعد", + "type-rulechains": "زنجيره هاي قواعد", + "list-of-rulechains": "{ count, plural, 1 {يک زنجيره قواعد} other {ليست # زنجيره قواعد} }", + "rulechain-name-starts-with": "شروع مي شود '{{prefix}}' زنجيره هاي قواعدي که نامشان با", + "type-rulenode": "گره قواعد", + "type-rulenodes": "گره هاي قواعد", + "list-of-rulenodes": "{ count, plural, 1 {يک گره قواعد} other {ليست # گره قواعد} }", + "rulenode-name-starts-with": "شروع مي شود '{{prefix}}' گره هاي قواعدي که نامشان با", + "type-current-customer": "مشتري فعلي", + "search": "جستجوي موجودي ها", + "selected-entities": "انتخاب شدند { count, plural, 1 {1 موجودي} other {# موجودي} }", + "entity-name": "نام موجودي", + "details": "جزئيات موجودي", + "no-entities-prompt": "هيچ موجودي اي يافت نشد", + "no-data": "هيچ داده اي براي نمايش نيست", + "columns-to-display": "ستون ها براي نمايش" + }, + "entity-view": { + "entity-view": "نمايش موجودي", + "entity-view-required": ".نمايش موجودي مورد نياز است", + "entity-views": "نمايش هاي موجودي", + "management": "مديريت نمايش موجودي", + "view-entity-views": "نمايش نمايش هاي موجودي", + "entity-view-alias": "نام مستعار نمايش موجودي", + "aliases": "نامهاي مستعار نمايش موجودي", + "no-alias-matching": ".يافت نشد '{{alias}}'", + "no-aliases-found": ".هيچ نام مستعاري يافت نشد", + "no-key-matching": ".يافت نشد '{{key}}'", + "no-keys-found": ".هيچ کليدي يافت نشد", + "create-new-alias": "!ايجاد نام مستعار جديد", + "create-new-key": "!ايجاد کليد جديد", + "duplicate-alias-error": ".نامهاي مستعار نمايش موجودي بايد در داشبورد، منحصر بفرد باشند
    .يافت شد '{{alias}}' نام مستعار تکراري", + "configure-alias": "'{{alias}}' پيکربندي نام مستعار", + "no-entity-views-matching": ".يافت نشد '{{entity}}' هيچ موجودي منطبق بر", + "alias": "نام مستعار", + "alias-required": ".نام مستعار نمايش موجودي مورد نياز است", + "remove-alias": "حذف نام مستعار نمايش موجودي", + "add-alias": "افزودن نام مستعار نمايش موجودي", + "name-starts-with": "نام نمايش موجودي شروع مي شود با", + "entity-view-list": "ليست نمايش موجودي", + "use-entity-view-name-filter": "استفاده از فيلتر", + "entity-view-list-empty": ".هيچ نمايش موجودي انتخاب نشد", + "entity-view-name-filter-required": ".فيلتر نام نمايش موجودي مورد نياز است", + "entity-view-name-filter-no-entity-view-matched": ".شروع شود يافت نشد '{{entityView}}' هيچ نمايش موجودي که با", + "add": "افزودن نمايش موجودي", + "assign-to-customer": "تخصيص به مشتري", + "assign-entity-view-to-customer": "تخصيص نمايش(هاي) موجودي به مشتري", + "assign-entity-view-to-customer-text": ".لطفا نمايش هاي موجودي را انتخاب کنيد تا به مشتري تخصيص يابند", + "no-entity-views-text": "هيچ نمايش موجودي يافت نشد", + "assign-to-customer-text": ".لطفا مشتري را انتخاب کنيد تا نمايش(هاي) موجودي تخصيص يابد", + "entity-view-details": "جزئيات نمايش موجودي", + "add-entity-view-text": "افزودن نمايش موجودي جديد", + "delete": "حذف نمايش موجودي", + "assign-entity-views": "تخصيص نمايش هاي موجودي", + "assign-entity-views-text": "به مشتري { count, plural, 1 {1 نمايش موجودي} other {# نمايش موجودي} } تخصيص", + "delete-entity-views": "حذف نمايش هاي موجودي", + "unassign-from-customer": "لغو تخصيص از مشتري", + "unassign-entity-views": "لغو تخصيص نمايش هاي موجودي", + "unassign-entity-views-action-title": "از مشتري { count, plural, 1 {1 نمايش موجودي} other {# نمايش موجودي} } لغو تخصيص", + "assign-new-entity-view": "تخصيص نمايش موجودي جديد", + "delete-entity-view-title": "مطمئنيد؟ '{{entityViewName}}' از حذف نمايش موجودي", + "delete-entity-view-text": ".مراقب باشيد، پس از تأييد، نمايش موجودي و تمامي داده هاي مربوطه، غير قابل بازيابي مي شوند", + "delete-entity-views-title": "مطمئنيد؟ { count, plural, 1 {1 نمايش موجودي} other {# نمايش موجودي} } از حذف نمايش موجودي", + "delete-entity-views-action-title": "{ count, plural, 1 {1 نمايش موجودي} other {# نمايش موجودي} } حذف", + "delete-entity-views-text": ".مراقب باشيد، پس از تأييد، تمام نمايش هاي موجوديِ انتخاب شده حذف، و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", + "unassign-entity-view-title": "مطمئنيد؟ '{{entityViewName}}' از لغو تخصيص نمايش موجودي", + "unassign-entity-view-text": ".پس از تأييد، نمايش موجودي، لغو تخصيص و خارج از دسترس مشتري مي شود", + "unassign-entity-view": "لغو تخصيص نمايش موجودي", + "unassign-entity-views-title": "مطمئنيد؟ { count, plural, 1 {1 نمايش موجودي} other {# نمايش موجودي} } از لغو تخصيص", + "unassign-entity-views-text": ".پس از تأييد، تمام نمايش هاي موجوديِ انتخاب شده، لغو تخصيص و خارج از دسترس مشتري مي شوند", + "entity-view-type": "نوع نمايش موجودي", + "entity-view-type-required": ".نوع نمايش موجودي مورد نياز است", + "select-entity-view-type": "انتخاب نوع نمايش موجودي", + "enter-entity-view-type": "وارد کردن نوع نمايش موجودي", + "any-entity-view": "هر نمايش موجودي", + "no-entity-view-types-matching": ".يافت نشد '{{entitySubtype}}' هيچ نوع نمايش موجودي منطبق بر", + "entity-view-type-list-empty": ".هيچ نوع نمايش موجودي انتخاب نشد", + "entity-view-types": "انواع نمايش موجودي", + "name": "نام", + "name-required": ".نام مورد نياز است", + "description": "توصيف", + "events": "رويدادها", + "details": "جزئيات", + "copyId": "نمايش موجودي ID رونوشت از", + "assignedToCustomer": "تخصيص يافته به مشتري", + "unable-entity-view-device-alias-title": ".حذف نام مستعار نمايش موجودي ممکن نيست", + "unable-entity-view-device-alias-text": "
    {{widgetsList}} :را تا زمان استفاده توسط ويجت(هاي) زير نمي توان حذف کرد ، '{{entityViewAlias}}' ،نام مستعار دستگاه", + "select-entity-view": "انتخاب نمايش موجودي", + "make-public": "عمومي سازي نمايش موجودي", + "start-date": "تاريخ شروع", + "start-ts": "زمان شروع", + "end-date": "تاريخ پايان", + "end-ts": "زمان پايان", + "date-limits": "محدوده تاريخ", + "client-attributes": "ويژگي هاي مشتري", + "shared-attributes": "ويژگي هاي مشترک", + "server-attributes": "ويژگي هاي سِروِر", + "timeseries": "سري هاي زماني", + "client-attributes-placeholder": "ويژگي هاي مشتري", + "shared-attributes-placeholder": "ويژگي هاي مشترک", + "server-attributes-placeholder": "ويژگي هاي سِروِر", + "timeseries-placeholder": "سري هاي زماني", + "target-entity": "موجودي هدف", + "attributes-propagation": "انتشار ويژگي ها", + "attributes-propagation-hint": "هر بار که شما نمايش موجودي را بروز رساني يا ذخيره مي کنيد، نمايش موجودي بصور خودکار ويژگي هاي تعيين شده را از موجودي هدف کپي مي کند و به دلايل عملکردي، ويژگي هاي موجودي هدف، با هر بار تغيير ويژگي، در نمايش موجودي انتشار نمي يابند. مي توانيد با پيکربندي گره قواعد در زنجيره قواعد خود و پيوند دهي \"Post attributes\" و \"Attributes Updated\" آن به گره قواعد جديد، انتشار خودکار \"copy to view\" را ممکن سازيد ." , + "timeseries-data": "داده ي سري هاي زماني", + "timeseries-data-hint": "کليدهاي داده ي سري هاي زمانيِ موجوديِ هدف را پيکربندي کنيد تا در دسترسِ نمايش موجودي باشند. اين سري هاي زماني، فقط خواندني است" + }, + "event": { + "event-type": "نوع رويداد", + "type-error": "خطا", + "type-lc-event": "رويداد چرخه عمر", + "type-stats": "آمار", + "type-debug-rule-node": "اشکال زدايي", + "type-debug-rule-chain": "اشکال زدايي", + "no-events-prompt": "هيچ رويدادي يافت نشد", + "error": "خطا", + "alarm": "هشدار", + "event-time": "زمان رويداد", + "server": "سِروِر", + "body": "بدنه", + "method": "روش", + "type": "نوع", + "entity": "موجودي", + "message-id": "پيام ID", + "message-type": "نوع پيام", + "data-type": "نوع داده", + "relation-type": "نوع ارتباط", + "metadata": "فرا داده", + "data": "داده", + "event": "رويداد", + "status": "وضعيت", + "success": "موفقيت", + "failed": "عدم موفقيت", + "messages-processed": "پيام پردازش شد", + "errors-occurred": "خطاها رخ دادند" + }, + "extension": { + "extensions": "دنباله ها", + "selected-extensions": "انتخاب شدند { count, plural, 1 {1 افزونه} other {افزونه ها #} }", + "type": "نوع", + "key": "کليد", + "value": "مقدار", + "id": "ID", + "extension-id": " افزونه ID", + "extension-type": "نوع افزونه", + "transformer-json": "JSON *", + "unique-id-required": ".افزونه فعلي موجود است ID در حال حاضر", + "delete": "حذف دنباله", + "add": "افزودن دنباله", + "edit": "ويرايش دنباله", + "delete-extension-title": "مطمئنيد؟ '{{extensionId}}' از حذف افزونه", + "delete-extension-text": ".مراقب باشيد، پس از تأييد، افزونه و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", + "delete-extensions-title": "مطمئنيد؟ { count, plural, 1 {1 افزونه} other {# افزونه} } از حذف", + "delete-extensions-text": ".مراقب باشيد، پس از تأييد، تمام افزونه هاي انتخاب شده حذف مي گردند", + "converters": "مبدّل ها", + "converter-id": "مبدّل ID", + "configuration": "پيکربندي", + "converter-configurations": "پيکربندي هاي مبدّل", + "token": "نشانه امنيت", + "add-converter": "افزودن مبدّل", + "add-config": "افزودن پيکربندي مبدّل", + "device-name-expression": "عبارت نام دستگاه", + "device-type-expression": "عبارت نوع دستگاه", + "custom": "متداول", + "to-double": "دو برابر شدن", + "transformer": "مبدّل", + "json-required": ".مبدّل مورد نياز است JSON", + "json-parse": ".مبدّل ممکن نيست JSON تجزيه", + "attributes": "ويژگي ها", + "add-attribute": "افزودن ويژگي", + "add-map": "افزودن جزء نگاشت", + "timeseries": "سري هاي زماني", + "add-timeseries": "افزودن سري هاي زماني", + "field-required": "دامنه مورد نياز است", + "brokers": "واسطه ها", + "add-broker": "افزودن واسطه", + "host": "ميزبان", + "port": "درگاه", + "port-range": ".درگاه بايد در بازه اي بين 1 تا 65535 باشد", + "ssl": "Ssl", + "credentials": "اعتبارنامه ها", + "username": "نام کاربري", + "password": "رمز عبور", + "retry-interval": "بازخواني فاصله در ميلي ثانيه", + "anonymous": "بي نام", + "basic": "پايه", + "pem": "PEM", + "ca-cert": "CA پرونده گواهينامه *", + "private-key": "پرونده کليد شخصي *", + "cert": "پرونده گواهينامه *", + "no-file": ".هيچ پرونده اي انتخاب نشد", + "drop-file": ".جهت بارگذاري يک پرونده، آن را با موس کِشيده و رها کنيد، و يا روي آن کليک نماييد", + "mapping": "نگاشت", + "topic-filter": "فيلتر عنوان", + "converter-type": "نوع مبدّل", + "converter-json": "JSON", + "json-name-expression": "نام دستگاه JSON عبارت", + "topic-name-expression": "عبارت عنوان نام دستگاه", + "json-type-expression": "نوع دستگاه JSON عبارت", + "topic-type-expression": "عبارت عنوان نوع دستگاه", + "attribute-key-expression": "عبارت کليد ويژگي", + "attr-json-key-expression": "کليد ويژگي JSON عبارت", + "attr-topic-key-expression": "عبارت عنوان کليد ويژگي", + "request-id-expression": "ID درخواست عبارت", + "request-id-json-expression": "ID JSON درخواست عبارت", + "request-id-topic-expression": "ID درخواست عبارت عنوان", + "response-topic-expression": "عبارت عنوان پاسخ", + "value-expression": "عبارت مقدار", + "topic": "عنوان", + "timeout": "وقفه در ميلي ثانيه", + "converter-json-required": ".مبدّل مورد نياز است JSON", + "converter-json-parse": ".مبدّل ممکن نيست JSON تجزيه", + "filter-expression": "عبارت فيلتر", + "connect-requests": "درخواست هاي اتصال", + "add-connect-request": "افزودن درخواست اتصال", + "disconnect-requests": "درخواست هاي قطع اتصال", + "add-disconnect-request": "افزودن درخواست قطع اتصال", + "attribute-requests": "درخواست هاي ويژگي", + "add-attribute-request": "افزودن درخواست ويژگي", + "attribute-updates": "به روز رساني هاي ويژگي ", + "add-attribute-update": "افزودن به روز رساني ويژگي ", + "server-side-rpc": "سَمت سِروِر RPC", + "add-server-side-rpc-request": "سَمت سِروِر RPC افزودن درخواست", + "device-name-filter": "فيلتر نام دستگاه", + "attribute-filter": "فيلتر ويژگي ", + "method-filter": "فيلتر روش", + "request-topic-expression": "عبارت عنوان درخواست", + "response-timeout": "وقفه پاسخ در ميلي ثانيه", + "topic-expression": "بيان موضوع", + "client-scope": "حوزه مشتري", + "add-device": "افزودن دستگاه", + "opc-server": "سِروِرها", + "opc-add-server": "افزودن سِروِر", + "opc-add-server-prompt": "لطفا سِروِر را اضافه کنيد", + "opc-application-name": "نام برنامه کاربردي", + "opc-application-uri": "برنامه کاربردي URI", + "opc-scan-period-in-seconds": "دوره پويش در ثانيه", + "opc-security": "امنيت", + "opc-identity": "هويت", + "opc-keystore": "کي استور", + "opc-type": "نوع", + "opc-keystore-type": "نوع", + "opc-keystore-location": "* موقعيت مکاني", + "opc-keystore-password": "رمز عبور", + "opc-keystore-alias": "نام مستعار", + "opc-keystore-key-password": "کليد رمز عبور", + "opc-device-node-pattern": "الگوي گره دستگاه", + "opc-device-name-pattern": "الگوي نام دستگاه", + "modbus-server": "سِروِرها/جايگزين آماده به کار", + "modbus-add-server": "افزودن سِروِر/ جايگزين آماده به کار ", + "modbus-add-server-prompt": "لطفا سِروِرها/جايگزين آماده به کار را اضافه کنيد", + "modbus-transport": "انتقال", + "modbus-port-name": "نام در گاه سريال", + "modbus-encoding": "رمز گذاري", + "modbus-parity": "توازن", + "modbus-baudrate": "نرخ علامت در ثانيه", + "modbus-databits": "بيت هاي داده", + "modbus-stopbits": "بيت هاي توقف", + "modbus-databits-range": ".بيت هاي داده بايد در بازه اي بين 7 تا 8 باشند", + "modbus-stopbits-range": ".بيت هاي توقف بايد در بازه اي بين 1 تا 2 باشند", + "modbus-unit-id": "واحد ID", + "modbus-unit-id-range": ".واحد بايد در بازه اي بين 1 تا 247 باشد ID", + "modbus-device-name": "نام دستگاه", + "modbus-poll-period": "(ms) دوره نمونه برداري", + "modbus-attributes-poll-period": "(ms) دوره نمونه برداري ويژگي ها", + "modbus-timeseries-poll-period": "(ms) دوره نمونه برداري سري هاي زماني", + "modbus-poll-period-range": ".دوره نمونه برداري بايد مقداري مثبت باشد", + "modbus-tag": "برچسب", + "modbus-function": "تابع", + "modbus-register-address": "ثبت نام نشاني", + "modbus-register-address-range": ".نشاني ثبت بايد در بازه اي بين 0 تا 65535 باشد", + "modbus-register-bit-index": "شاخص بيت", + "modbus-register-bit-index-range": ".شاخص بيت بايد در بازه اي بين 0 تا 15 باشد", + "modbus-register-count": "شمارش ثبت", + "modbus-register-count-range": ".شمارش ثبت بايد مقداري مثبت باشد", + "modbus-byte-order": "ترتيب بايت", + + "sync": { + "status": "وضعيت", + "sync": "همگام", + "not-sync": "غير همگام", + "last-sync-time": "آخرين زمان همگام سازي", + "not-available": "خارج از دسترس" + }, + + "export-extensions-configuration": "صدور پيکربندي افزونه ها", + "import-extensions-configuration": "وارد کردن پيکربندي افزونه ها", + "import-extensions": "وارد کردن افزونه ها", + "import-extension": "وارد کردن افزونه", + "export-extension": "صدور افزونه", + "file": "پرونده افزونه ها", + "invalid-file-error": "پرونده افزونه نامعتبر است" + }, + "fullscreen": { + "expand": "بسط به حالت تمام صفحه", + "exit": "خروج از حالت تمام صفحه", + "toggle": "تغيير حالت تمام صفحه", + "fullscreen": "حالت تمام صفحه" + }, + "function": { + "function": "تابع" + }, + "grid": { + "delete-item-title": "از حذف اين مورد مطمئنيد؟", + "delete-item-text": ".مراقب باشيد، پس از تأييد، اين مورد و تمامي داده هاي مربوطه غيرقابل بازيابي مي شوند", + "delete-items-title": "مطمئنيد؟ { count, plural, 1 {1 مورد} other {# مورد} } از حذف", + "delete-items-action-title": "{ count, plural, 1 {1 مورد} other {# مورد} } حذف", + "delete-items-text": ".مراقب باشيد، پس از تأييد، تمام مواردِ انتخاب شده حذف، و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", + "add-item-text": "افزودن مورد جديد", + "no-items-text": "هيچ موردي يافت نشد", + "item-details": "جزئيات مورد", + "delete-item": "حذف مورد", + "delete-items": "حذف موارد", + "scroll-to-top": "پيمايش به بالا" + }, + "help": { + "goto-help-page": "رفتن به صفحه کمک" + }, + "home": { + "home": "خانه", + "profile": "پرونده شخصي", + "logout": "خروج", + "menu": "فهرست انتخاب", + "avatar": "آواتار", + "open-user-menu": "بازکردن فهرست انتخاب کاربر" + }, + "import": { + "no-file": "هيچ پرونده اي انتخاب نشد", + "drop-file": ".آن را با موس کِشيده و رها کنيد، و يا روي آن کليک نماييد ،JSON جهت بارگذاري يک پرونده" + }, + "item": { + "selected": "انتخاب شده" + }, + "js-func": { + "no-return-error": "!تابع بايد مقدار را برگرداند", + "return-type-mismatch": "!را برگرداند '{{type}}' تابع بايد مقدار نوع", + "tidy": "مرتب" + }, + "key-val": { + "key": "کليد", + "value": "مقدار", + "remove-entry": "حذف ورودي", + "add-entry": "افزودن ورودي", + "no-data": "هيچ ورودي وجود ندارد" + }, + "layout": { + "layout": "طرح بندي", + "manage": "مديريت طرح بندي ها", + "settings": "تنظيمات طرح بندي", + "color": "رنگ", + "main": "اصلي", + "right": "راست", + "select": "انتخاب طرح بندي هدف" + }, + "legend": { + "position": "محل فهرست علائم", + "show-max": "نمايش بيشترين مقدار", + "show-min": "نمايش کمترين مقدار", + "show-avg": "نمايش مقدار ميانگين", + "show-total": "نمايش مقدار مجموع", + "settings": "تنظيمات فهرست علائم", + "min": "کمترين", + "max": "بيشترين", + "avg": "ميانگين", + "total": "مجموع" + }, + "login": { + "login": "ورود", + "request-password-reset": "درخواست بازنشاني رمز عبور", + "reset-password": "بازنشاني رمز عبور", + "create-password": "ايجاد رمز عبور", + "passwords-mismatch-error": "!رمزهاي عبور وارد شده بايد مشابه باشند", + "password-again": "رمز عبور دوباره", + "sign-in": "لطفا وارد شويد", + "username": "(نام کاربري (پست الکترونيک", + "remember-me": "مرا به خاطر داشته باش", + "forgot-password": "رمز عبور را فراموش کرده ايد؟", + "password-reset": "بازنشاني رمز عبور", + "new-password": "رمز عبور جديد", + "new-password-again": "رمز عبور جديد دوباره", + "password-link-sent-message": "!پيوند بازنشاني رمز عبور با موفقيت ارسال شد", + "email": "پست الکترونيک" + }, + "position": { + "top": "بالا", + "bottom": "پايين", + "left": "چپ", + "right": "راست" + }, + "profile": { + "profile": "پرونده شخصي", + "change-password": "تغيير رمز عبور", + "current-password": "رمز عبور فعلي" + }, + "relation": { + "relations": "ارتباطات", + "direction": "جهت", + "search-direction": { + "FROM": "از", + "TO": "به" + }, + "direction-type": { + "FROM": "از", + "TO": "به" + }, + "from-relations": "ارتباطات خارج از محدوده", + "to-relations": "ارتباطات داخل محدوده", + "selected-relations": "انتخاب شدند { count, plural, 1 {1 ارتباط} other {ارتباط #} }", + "type": "نوع", + "to-entity-type": "به نوع موجودي", + "to-entity-name": "به نام موجودي", + "from-entity-type": "از نوع موجودي", + "from-entity-name": "از نام موجودي", + "to-entity": "به موجودي", + "from-entity": "از موجودي", + "delete": "حذف ارتباط", + "relation-type": "نوع ارتباط", + "relation-type-required": ".نوع ارتباط مورد نياز است", + "any-relation-type": "هر نوع", + "add": "افزودن ارتباط", + "edit": "ويرايش ارتباط", + "delete-to-relation-title": "مطمئنيد؟ '{{entityName}}' از حذف ارتباط با موجودي", + "delete-to-relation-text": ".غيرمرتبط با موجودي فعلي مي شود '{{entityName}}' مراقب باشيد، پس از تأييد، موجودي", + "delete-to-relations-title": "مطمئنيد؟ { count, plural, 1 {1 ارتباط} other {# ارتباط} } از حذف", + "delete-to-relations-text": ".مراقب باشيد، پس از تأييد، تمام روابطِ انتخاب شده حذف، و موجودي هاي مربوطه، غيرمرتبط با موجودي فعلي مي شوند", + "delete-from-relation-title": "مطمئنيد؟ '{{entityName}}' از حذف ارتباط از موجودي", + "delete-from-relation-text": ".مي شود '{{entityName}}' مراقب باشيد، پس از تأييد، موجودي فعلي، غيرمرتبط از جانب موجودي", + "delete-from-relations-title": "مطمئنيد؟ { count, plural, 1 {1 ارتباط} other {# ارتباط} } از حذف", + "delete-from-relations-text": ".مراقب باشيد، پس از تأييد، تمام روابطِ انتخاب شده حذف، و موجودي فعلي، غيرمرتبط از جانب موجودي هاي مربوطه مي شود", + "remove-relation-filter": "حذف فيلتر ارتباط", + "add-relation-filter": "افزودن فيلتر ارتباط", + "any-relation": "هر ارتباط", + "relation-filters": "فيلترهاي ارتباط", + "additional-info": "(JSON) اطلاعات تکميلي", + "invalid-additional-info": "اطلاعات تکميلي ممکن نيست JSON تجزيه" + }, + "rulechain": { + "rulechain": "زنجيره قواعد", + "rulechains": "زنجيره هاي قواعد", + "root": "پايه", + "delete": "حذف زنجيره قواعد", + "name": "نام", + "name-required": ".نام مورد نياز است", + "description": "توصيف", + "add": "افزودن زنجيره قواعد", + "set-root": "ريشه زنجيره قواعد را ايجاد کنيد", + "set-root-rulechain-title": "به عنوان ريشه مطمئنيد؟ '{{ruleChainName}}' از قرار دادن زنجيره قواعد", + "set-root-rulechain-text": ".پس از تأييد، زنجيره قواعد، به عنوان ريشه تعيين شده و به تمام پيامهاي انتقالي رسيدگي مي کند", + "delete-rulechain-title": "مطمئنيد؟ '{{ruleChainName}}' از حذف زنجيره قواعد", + "delete-rulechain-text": ".مراقب باشيد، پس از تأييد، زنجيره قواعد و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", + "delete-rulechains-title": "مطمئنيد؟ { count, plural, 1 {1 زنجيره قواعد} other {# زنجيره قواعد} } از حذف", + "delete-rulechains-action-title": "{ count, plural, 1 {1 زنجيره قواعد} other {# زنجيره قواعد} } حذف", + "delete-rulechains-text": ".مراقب باشيد، پس از تأييد، تمام زنجيره هاي قواعدِ انتخاب شده حذف، و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", + "add-rulechain-text": "افزودن زنجيره قواعد جديد", + "no-rulechains-text": "هيچ زنجيره قواعدي يافت نشد", + "rulechain-details": "جزئيات زنجيره قواعد", + "details": "جزئيات", + "events": "رويدادها", + "system": "سيستم", + "import": "وارد کردن زنجيره قواعد", + "export": "صدور زنجيره قواعد", + "export-failed-error": "{{error}} :صدور زنجيره قواعد ممکن نيست", + "create-new-rulechain": "ايجاد زنجيره قواعد جديد", + "rulechain-file": "پرونده زنجيره قواعد", + "invalid-rulechain-file-error": ".وارد کردن زنجيره قواعد ممکن نيست: ساختار داده زنجيره قواعد نامعتبر است", + "copyId": "زنجيره قواعد ID رونوشت", + "idCopiedMessage": "زنجيره قواعد در حافظه موقت رونوشت شد ID", + "select-rulechain": "انتخاب زنجيره قواعد", + "no-rulechains-matching": ".يافت نشد '{{entity}}' هيچ زنجيره قواعدي منطبق بر", + "rulechain-required": "زنجيره قواعد مورد نياز است", + "management": "مديريت قواعد", + "debug-mode": "حالت اشکال زدايي" + }, + "rulenode": { + "details": "جزئيات", + "events": "رويدادها", + "search": "جستجوي گره ها", + "open-node-library": "باز کردن کتابخانه گره ها", + "add": "افزودن گره قواعد", + "name": "نام", + "name-required": ".نام مورد نياز است", + "type": "نوع", + "description": "توصيف", + "delete": "حذف گره قواعد", + "select-all-objects": "انتخاب تمام گره ها و اتصالات", + "deselect-all-objects": "لغو انتخاب تمام گره ها و اتصالات", + "delete-selected-objects": "حذف گره ها و اتصالاتِ انتخاب شده", + "delete-selected": "حذفِ انتخاب شده", + "select-all": "انتخاب همه", + "copy-selected": "رونوشتِ انتخاب شده", + "deselect-all": "لغو انتخاب همه", + "rulenode-details": "جزئيات گره قواعد", + "debug-mode": "حالت اشکال زدايي", + "configuration": "پيکربندي", + "link": "پيوند", + "link-details": "جزئيات پيوند گره قواعد", + "add-link": "افزودن پيوند", + "link-label": "برچسب پيوند", + "link-label-required": ".برچسب پيوند مورد نياز است", + "custom-link-label": "برچسب پيوند متداول", + "custom-link-label-required": ".برچسب پيوند متداول مورد نياز است", + "link-labels": "برچسب هاي پيوند", + "link-labels-required": ".برچسب هاي پيوند مورد نيازند", + "no-link-labels-found": "هيچ برچسب پيوندي يافت نشد", + "no-link-label-matching": ".پيدا نشد'{{label}}'", + "create-new-link-label": "!برچسب پيوند ايجاد کنيد", + "type-filter": "فيلتر", + "type-filter-details": "پيام هاي ورودي را با شرايط پيکربندي فيلتر کنيد", + "type-enrichment": "افزودن", + "type-enrichment-details": "افزودن اطلاعات تکميلي به فرا داده ي پيام", + "type-transformation": "تبديل", + "type-transformation-details": "تغيير بازده و فرا داده ي پيام", + "type-action": "اقدام", + "type-action-details": "انجام اقدام ويژه", + "type-external": "خارجي", + "type-external-details": "ارتباط متقابل با سيستم خارجي", + "type-rule-chain": "زنجيره قواعد", + "type-rule-chain-details": "ارسال پيامهاي وارده به زنجيره قواعدي مشخص", + "type-input": "ورودي", + "type-input-details": "ورودي منطقي زنجيره قواعد، پيامهاي ورودي را به گره قواعد مرتبط بعدي ارسال مي کند", + "type-unknown": "ناشناخته", + "type-unknown-details": "گره قواعدِ حل نشده", + "directive-is-not-loaded": ".در دسترس نيست '{{directiveName}}' دستورالعمل پيکربنديِ مشخص شده", + "ui-resources-load-error": ".پيکربندي UI عدم موفقيت در بارگذاري منابع", + "invalid-target-rulechain": "!حلّ زنجيره قواعد هدف ممکن نيست", + "test-script-function": "آزمايش تابع اسکريپت", + "message": "پيام", + "message-type": "نوع پيام", + "select-message-type": "انتخاب نوع پيام", + "message-type-required": "نوع پيام مورد نياز است", + "metadata": "فوق داده", + "metadata-required": ".ورودي هاي فرا داده نمي تواند خالي باشد", + "output": "خروجي", + "test": "آزمايش", + "help": "کمک" + }, + "tenant": { + "tenant": "کاربر", + "tenants": "کاربران", + "management": "مديريت کاربران", + "add": "افزودن کاربر", + "admins": "سرپرستان", + "manage-tenant-admins": "مديريت مديران کاربر", + "delete": "حذف کاربر", + "add-tenant-text": "افزودن کاربر جديد", + "no-tenants-text": "هيچ کاربري يافت نشد", + "tenant-details": "جزئيات کاربر", + "delete-tenant-title": "مطمئنيد؟ '{{tenantTitle}}' از حذف کاربر", + "delete-tenant-text": ".مراقب باشيد، پس از تأييد، کاربر و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", + "delete-tenants-title": "مطمئنيد؟ { count, plural, 1 {1 کاربر} other {# کاربر} } از حذف", + "delete-tenants-action-title": "{ count, plural, 1 {1 کاربر} other {# کاربر} } حذف", + "delete-tenants-text": ".مراقب باشيد، پس از تأييد، تمام کاربران حذف، و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", + "title": "عنوان", + "title-required": ".عنوان مورد نياز است", + "description": "توصيف", + "details": "جزئيات", + "events": "رويدادها", + "copyId": "متصرّف ID رونوشت", + "idCopiedMessage": "کاربر در حافظه موقت رونوشت شد ID", + "select-tenant": "انتخاب کاربر", + "no-tenants-matching": ".يافت نشد '{{entity}}' هيچ کاربري منطبق بر", + "tenant-required": "کاربر مورد نياز است" + }, + "timeinterval": { + "seconds-interval": "{ seconds, plural, 1 {1 ثانيه} other {ثانيه #} }", + "minutes-interval": "{ minutes, plural, 1 {1 دقيقه} other {دقيقه #} }", + "hours-interval": "{ hours, plural, 1 {1 ساعت} other {ساعت #} }", + "days-interval": "{ days, plural, 1 {1 روز} other {روز #} }", + "days": "روزها", + "hours": "ساعات", + "minutes": "دقايق", + "seconds": "ثانيه ها", + "advanced": "پيشرفته" + }, + "timewindow": { + "days": "{ days, plural, 1 { روز } other {روز #} }", + "hours": "{ hours, plural, 0 { 1ساعت } 1 { ساعت } other {# ساعت } }", + "minutes": "{ minutes, plural, 0 { 1دقيقه } 1 { دقيقه } other {دقيقه # } }", + "seconds": "{ seconds, plural, 0 { 1ثانيه } 1 { ثانيه } other {ثانيه # } }", + "realtime": "بي درنگ", + "history": "تاريخچه", + "last-prefix": "آخرين", + "period": "{{ endTime }} تا {{ startTime }} از", + "edit": "ويرايش پنجره زماني", + "date-range": "بازه داده", + "last": "آخرين", + "time-period": "دوره زماني" + }, + "user": { + "user": "کاربر", + "users": "کاربرها", + "customer-users": "کاربرهاي مشتري", + "tenant-admins": "مديران کاربر", + "sys-admin": "مدير سيستم", + "tenant-admin": "کاربر مدير", + "customer": "مشتري", + "anonymous": "بي نام", + "add": "افزودن کاربر", + "delete": "حذف کاربر", + "add-user-text": "افزودن کاربر جديد", + "no-users-text": "هيچ کاربري يافت نشد", + "user-details": "جزئيات کاربر", + "delete-user-title": "مطمئنيد؟ '{{userEmail}}' از حذف کاربر", + "delete-user-text": ".مراقب باشيد، پس از تأييد، کاربر و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", + "delete-users-title": "مطمئنيد؟ { count, plural, 1 {1 کاربر} other {# کاربر} } از حذف", + "delete-users-action-title": "{ count, plural, 1 {1 کاربر} other {کاربر #} } حذف", + "delete-users-text": ".مراقب باشيد، پس از تأييد، تمام کاربرهاي انتخاب شده حذف، و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", + "activation-email-sent-message": "!پست الکترونيک فعال سازي با موفقيت ارسال شد", + "resend-activation": "ارسال مجدد فعال سازي", + "email": "پست الکترونيک", + "email-required": ".پست الکترونيک مورد نياز است", + "invalid-email-format": ".قالب نامعتبر پست الکترونيک", + "first-name": "نام", + "last-name": "نام خانوادگي", + "description": "توصيف", + "default-dashboard": "داشبورد پيش فرض", + "always-fullscreen": "همواره در حالت تمام صفحه", + "select-user": "انتخاب کاربر", + "no-users-matching": ".يافت نشد '{{entity}}' هيچ کاربري منطبق بر", + "user-required": "کاربر مورد نياز است", + "activation-method": "روش فعال سازي", + "display-activation-link": "نمايش پيوند فعال سازي", + "send-activation-mail": "ارسال پست الکترونيک فعال سازي", + "activation-link": "پيوند فعال سازي کاربر", + "activation-link-text": ":
    استفاده کنيد جهت فعال سازي کاربر، از پيوند فعال سازي زير", + "copy-activation-link": "رونوشت پيوند فعال سازي", + "activation-link-copied-message": "پيوند فعال سازي کاربر در حافظه موقت رونوشت شد", + "details": "جزئيات", + "login-as-tenant-admin": "ورود به عنوان کاربر مدير", + "login-as-customer-user": "ورود به عنوان کاربر مشتري" + }, + "value": { + "type": "نوع مقدار", + "string": "رشته", + "string-value": "مقدار رشته", + "integer": "عدد صحيح", + "integer-value": "مقدار عدد صحيح", + "invalid-integer-value": "عدد صحيح نامعتبر", + "double": "دو برابر", + "double-value": "مقدار دو برابر", + "boolean": "بولين", + "boolean-value": "مقدار بولين", + "false": "نادرست", + "true": "صحيح", + "long": "بلند" + }, + "widget": { + "widget-library": "کتابخانه ويجت ها", + "widget-bundle": "بسته ويجت", + "select-widgets-bundle": "انتخاب بسته ويجت", + "management": "مديريت ويجت", + "editor": "ويرايشگر ويجت", + "widget-type-not-found": ".نوع ويجت حذف شد \n احتمالا مرتبط است
    .مشکل بارگذاري پيکربندي ويجت", + "widget-type-load-error": ":ويجت، به علت خطاهاي زير، بارگذاري نشد", + "remove": "حذف ويجت", + "edit": "ويرايش ويجت", + "remove-widget-title": "مطمئنيد؟ '{{widgetTitle}}' از حذف ويجت", + "remove-widget-text": ".پس از تأييد، ويجت و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", + "timeseries": "سري هاي زماني", + "search-data": "جستجوي داده", + "no-data-found": "هيچ داده اي يافت نشد", + "latest-values": "آخرين مقادير", + "rpc": "ويجت کنترل", + "alarm": "ويجت هشدار", + "static": "ويجت ايستا", + "select-widget-type": "انتخاب نوع ويجت", + "missing-widget-title-error": "!عنوان ويجت بايد مشخص شود", + "widget-saved": "ويجت ذخيره شد", + "unable-to-save-widget-error": "!ذخيره سازي ويجت ممکن نيست! ويجت خطاهايي دارد", + "save": "ذخيره سازي ويجت", + "saveAs": "ذخيره سازي ويجت به عنوان", + "save-widget-type-as": "ذخيره سازي نوع ويجت به عنوان", + "save-widget-type-as-text": "لطفا عنوان ويجت جديد را وارد کنيد و/يا بسته ويجت هدف را انتخاب نماييد", + "toggle-fullscreen": "حالت تمام صفحه را تغيير دهيد", + "run": "اجراي ويجت", + "title": "عنوان ويجت", + "title-required": ".عنوان ويجت مورد نياز است", + "type": "نوع ويجت", + "resources": "منابع", + "resource-url": "JavaScript/CSS URL", + "remove-resource": "حذف منبع", + "add-resource": "افزودن منبع", + "html": "HTML", + "tidy": "مرتب", + "css": "CSS", + "settings-schema": "طرح تنظيمات", + "datakey-settings-schema": "طرح تنظيمات کليد داده", + "javascript": "Javascript", + "remove-widget-type-title": "مطمئنيد؟ '{{widgetName}}' از حذف ويجت نوع", + "remove-widget-type-text": ".پس از تأييد، نوع ويجت و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", + "remove-widget-type": "حذف نوع ويجت", + "add-widget-type": "افزودن نوع ويجت جديد", + "widget-type-load-failed-error": "!عدم موفقيت در بارگذاري نوع ويجت", + "widget-template-load-failed-error": "!عدم موفقيت در بارگذاري قالب ويجت", + "add": "افزودن ويجت", + "undo": "برگرداندن تغييرات ويجت", + "export": "صدور ويجت" + }, + "widget-action": { + "header-button": "دکمه هدر ويجت", + "open-dashboard-state": "هدايت به وضعيت داشبورد جديد", + "update-dashboard-state": "به روز رساني وضعيت داشبورد فعلي", + "open-dashboard": "هدايت به داشبورد ديگر", + "custom": "اقدام متداول", + "target-dashboard-state": "وضعيت داشبورد هدف", + "target-dashboard-state-required": "وضعيت داشبورد هدف مورد نياز است", + "set-entity-from-widget": "تنظيم موجودي از ويجت", + "target-dashboard": "داشبورد هدف", + "open-right-layout": "(طرح داشبورد سمت راست را باز کنيد (نماي تلفن همراه" + }, + "widgets-bundle": { + "current": "بسته فعلي", + "widgets-bundles": "بسته هاي ويجت", + "add": "افزودن بسته ويجت", + "delete": "حذف بسته ويجت", + "title": "عنوان", + "title-required": ".عنوان مورد نياز است", + "add-widgets-bundle-text": "افزودن بسته ويجت جديد", + "no-widgets-bundles-text": "هيچ بسته ويجتي يافت نشد", + "empty": "بسته ويجت خالي است", + "details": "جزئيات", + "widgets-bundle-details": "جزئيات بسته ويجت", + "delete-widgets-bundle-title": "مطمئنيد؟ '{{widgetsBundleTitle}}' از حذف بسته ويجت", + "delete-widgets-bundle-text": ".مراقب باشيد، پس از تأييد، بسته ويجت و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", + "delete-widgets-bundles-title": "مطمئنيد؟ { count, plural, 1 {1 بسته ويجت} other {# بسته ويجت} } از حذف", + "delete-widgets-bundles-action-title": "{ count, plural, 1 {1 بسته ويجت} other {# بسته ويجت} } حذف", + "delete-widgets-bundles-text": ".مراقب باشيد، پس از تأييد، تمام بسته هاي ويجتِ انتخاب شده حذف، و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", + "no-widgets-bundles-matching": "يافت نشد '{{widgetsBundle}}' هيچ بسته ويجتي منطبق بر", + "widgets-bundle-required": ".بسته ويجت مورد نياز است", + "system": "سيستم", + "import": "وارد کردن بسته ويجت", + "export": "صدور بسته ويجت", + "export-failed-error": "{{error}} :صدور بسته ويجت ممکن نيست", + "create-new-widgets-bundle": "ايجاد بسته ويجت جديد", + "widgets-bundle-file": "پرونده بسته ويجت", + "invalid-widgets-bundle-file-error": ".وارد کردن بسته ويجت ممکن نيست: ساختار داده بسته ويجت نامعتبر است" + }, + "widget-config": { + "data": "داده", + "settings": "تنظيمات", + "advanced": "پيشرفته", + "title": "عنوان", + "general-settings": "تنظيمات عمومي", + "display-title": "نمايش عنوان", + "drop-shadow": "سايه افتادن", + "enable-fullscreen": "فعال سازي حالت تمام صفحه", + "background-color": "رنگ پس زمينه", + "text-color": "رنگ متن", + "padding": "حاشيه داخلي", + "margin": "حاشيه", + "widget-style": "سبک ويجت", + "title-style": "سبک عنوان", + "mobile-mode-settings": "تنظيمات حالت تلفن همراه", + "order": "ترتيب", + "height": "ارتفاع", + "units": "کارکتر خاص براي نمايش بعد از مقدار تعين شده", + "decimals": "تعداد ارقام بعد از مميّز شناور", + "timewindow": "پنجره زمان", + "use-dashboard-timewindow": "استفاده از پنجره زمان داشبورد", + "display-legend": "نمايش فهرست علائم", + "datasources": "منابع داده", + "maximum-datasources": "{ count, plural, 1 {.1 منبع داده مجاز است} other {# منبع داده مجازند.} } بيشترين", + "datasource-type": "نوع", + "datasource-parameters": "پارامترها", + "remove-datasource": "حذف منبع داده", + "add-datasource": "افزودن منبع داده", + "target-device": "دستگاه هدف", + "alarm-source": "منشأ هشدار", + "actions": "اقدامات", + "action": "اقدام", + "add-action": "افزودن اقدام", + "search-actions": "جستجوي اقدامات", + "action-source": "منشأ اقدام", + "action-source-required": ".منشأ اقدام مورد نياز است", + "action-name": "نام", + "action-name-required": ".نام اقدام مورد نياز است", + "action-name-not-unique": ".در حيطه يک منشأ اقدام، نام اقدام بايد منحصر بفرد باشد
    .در حال حاضر اقدامي ديگر با نام مشابه موجود است", + "action-icon": "شمايل", + "action-type": "نوع", + "action-type-required": ".نوع اقدام مورد نياز است", + "edit-action": "ويرايش اقدام", + "delete-action": "حذف اقدام", + "delete-action-title": "حذف اقدام ويجت", + "delete-action-text": "مطمئنيد؟ '{{actionName}}' از حذف اقدام ويجت با نام" + }, + "widget-type": { + "import": "وارد کردن نوع ويجت", + "export": "صدور نوع ويجت", + "export-failed-error": "{{error}} :صدور نوع ويجت ممکن نيست", + "create-new-widget-type": "ايجاد نوع جديد ويجت", + "widget-type-file": "پرونده نوع ويجت", + "invalid-widget-type-file-error": ".وارد کردن نوع ويجت ممکن نيست: ساختار داده نوع ويجت نامعتبر است" + }, + "widgets": { + "date-range-navigator": { + "localizationMap": { + "Sun": "یکشنبه", + "Mon": "دوشنبه", + "Tue": "سه‌شنبه", + "Wed": "چهارشنبه", + "Thu": "پنجشنبه", + "Fri": "جمعه", + "Sat": "شنبه", + "Jan": "ژانویهٔ", + "Feb": "فوریهٔ", + "Mar": "مارس", + "Apr": "آوریل", + "May": "مهٔ", + "Jun": "ژوئن", + "Jul": "ژوئیهٔ", + "Aug": "اوت", + "Sep": "سپتامبر", + "Oct": "اکتبر", + "Nov": "نوامبر", + "Dec": "دسامبر", + "January": "January", + "February": "February", + "March": "March", + "April": "April", + "June": "June", + "July": "July", + "August": "August", + "September": "September", + "October": "October", + "November": "November", + "December": "December", + "Custom Date Range": "Custom Date Range", + "Date Range Template": "Date Range Template", + "Today": "Today", + "Yesterday": "Yesterday", + "This Week": "This Week", + "Last Week": "Last Week", + "This Month": "This Month", + "Last Month": "Last Month", + "Year": "Year", + "This Year": "This Year", + "Last Year": "Last Year", + "Date picker": "Date picker", + "Hour": "Hour", + "Day": "Day", + "Week": "Week", + "2 weeks": "2 weeks", + "Month": "Month", + "3 months": "3 months", + "6 months": "6 months", + "Custom interval": "Custom interval", + "Interval": "Interval", + "Step size": "Step size", + "Ok": "Ok" + } + } + }, + "icon": { + "icon": "آيکون", + "select-icon": "انتخاب آيکون", + "material-icons": "آيکونهاي اجسام", + "show-all": "نمايش تمام آيکونها" + }, + "custom": { + "widget-action": { + "action-cell-button": "دکمه سلول عملياتي", + "row-click": "در رديف کليک کنيد", + "marker-click": "روي نشانگر کليک کنيد", + "tooltip-tag-action": "اقدام برچسب راهنماي ابزار" + } + }, + "language": { + "language": "زبان" + } +} diff --git a/ui-ngx/src/assets/locale/locale.constant-fr_FR.json b/ui-ngx/src/assets/locale/locale.constant-fr_FR.json new file mode 100644 index 0000000000..a5e16b23a0 --- /dev/null +++ b/ui-ngx/src/assets/locale/locale.constant-fr_FR.json @@ -0,0 +1,1721 @@ +{ + "access": { + "access-forbidden": "Accès interdit", + "access-forbidden-text": "Vous n'avez pas accès à cet emplacement!
    Essayez de vous connecter avec un autre utilisateur si vous souhaitez toujours accéder à cet emplacement.", + "refresh-token-expired": "La session a expiré", + "refresh-token-failed": "Impossible de rafraîchir la session", + "unauthorized": "non autorisé", + "unauthorized-access": "accès non autorisé", + "unauthorized-access-text": "Vous devez vous connecter pour avoir accès à cette ressource!" + }, + "action": { + "activate": "Activer", + "add": "Ajouter", + "apply": "Appliquer", + "apply-changes": "Appliquer les modifications", + "assign": "Attribuer", + "back": "retour", + "cancel": "Annuler", + "clear-search": "Effacer la recherche", + "close": "Fermer", + "continue": "Continue", + "copy": "Copier", + "copy-reference": "Copier la référence", + "create": "Créer", + "decline-changes": "Refuser les modifications", + "delete": "Supprimer", + "discard-changes": "Annuler les modifications", + "drag": "Drag", + "edit": "Modifier", + "edit-mode": "Mode édition", + "enter-edit-mode": "Entrer en mode édition", + "export": "Exporter", + "import": "Importer", + "make-private": "Rendre privé", + "no": "Non", + "ok": "OK", + "paste": "coller", + "paste-reference": "Coller référence", + "refresh": "Rafraîchir", + "remove": "Supprimer", + "run": "Exécuter", + "save": "Enregistrer", + "saveAs": "Enregistrer sous", + "search": "Rechercher", + "share": "Partager", + "share-via": "Partager via {{provider}}", + "sign-in": "Connectez-vous!", + "suspend": "Suspendre", + "unassign": "Retirer", + "undo": "Annuler", + "update": "mise à jour", + "view": "Afficher", + "yes": "Oui" + }, + "admin": { + "base-url": "URL de base", + "base-url-required": "L'URL de base est requise.", + "enable-tls": "Activer TLS", + "tls-version": "Version TLS", + "general": "Général", + "general-settings": "Paramètres généraux", + "mail-from": "Mail de", + "mail-from-required": "Mail de est requis.", + "outgoing-mail": "courrier sortant", + "outgoing-mail-settings": "Paramètres de courrier sortant", + "send-test-mail": "Envoyer un mail de test", + "smtp-host": "Hôte SMTP", + "smtp-host-required": "L'hôte SMTP est requis.", + "smtp-port": "Port SMTP", + "smtp-port-invalid": "Cela ne ressemble pas à un port smtp valide.", + "smtp-port-required": "Vous devez fournir un port smtp.", + "smtp-protocol": "Protocole SMTP", + "system-settings": "Paramètres système", + "test-mail-sent": "Le courrier de test a été envoyé avec succés!", + "timeout-invalid": "Cela ne ressemble pas à un délai d'expiration valide.", + "timeout-msec": "Délai (msec)", + "timeout-required": "Le délai est requis.", + "security-settings": "Les paramètres de sécurité", + "password-policy": "Politique de mot de passe", + "minimum-password-length": "Longueur minimale du mot de passe", + "minimum-password-length-required": "La longueur minimale du mot de passe est requise", + "minimum-password-length-range": "La longueur minimale du mot de passe doit être comprise entre 5 et 50.", + "minimum-uppercase-letters": "Nombre minimum de lettres majuscules", + "minimum-uppercase-letters-range": "Le nombre minimum de lettres majuscules ne peut pas être négatif", + "minimum-lowercase-letters": "Nombre minimum de lettres minuscules", + "minimum-lowercase-letters-range": "Le nombre minimum de lettres minuscules ne peut pas être négatif", + "minimum-digits": "Nombre minimum de chiffres", + "minimum-digits-range": "Le nombre minimum de chiffres ne peut pas être négatif", + "minimum-special-characters": "Nombre minimum de caractères spéciaux", + "minimum-special-characters-range": "Le nombre minimum de caractères spéciaux ne peut pas être négatif", + "password-expiration-period-days": "Délai d'expiration du mot de passe en jours", + "password-expiration-period-days-range": "La période d'expiration du mot de passe en jours ne peut pas être négative", + "password-reuse-frequency-days": "Fréquence de réutilisation du mot de passe en jours", + "password-reuse-frequency-days-range": "La fréquence de réutilisation du mot de passe en jours ne peut être négative", + "general-policy": "Politique générale", + "max-failed-login-attempts": "Nombre maximal de tentatives de connexion infructueuses avant que le compte ne soit verrouillé", + "minimum-max-failed-login-attempts-range": "Le nombre maximal de tentatives de connexion ayant échoué ne peut pas être négatif", + "user-lockout-notification-email": "En cas de verrouillage du compte d'utilisateur, envoyez une notification par courrier électronique." + }, + "aggregation": { + "aggregation": "agrégation", + "avg": "Moyenne", + "count": "Compte", + "function": "Fonction d'agrégation de données", + "group-interval": "Intervalle de regroupement", + "limit": "Valeurs maximales", + "max": "Max", + "min": "Min", + "none": "Aucune", + "sum": "Somme" + }, + "alarm": { + "ack-time": "Heure d'acquittement", + "acknowledge": "Acquitter", + "aknowledge-alarm-text": "Êtes-vous sûr de vouloir reconnaître l'alarme?", + "aknowledge-alarm-title": "Reconnaître l'alarme", + "aknowledge-alarms-text": "Êtes-vous sûr de vouloir acquitter { count, plural, 1 {1 alarme} other {# alarmes} }?", + "aknowledge-alarms-title": "Acquitter { count, plural, 1 {1 alarme} other {# alarmes} }", + "alarm": "Alarme", + "alarm-details": "Détails de l'alarme", + "alarm-required": "Une alarme est requise", + "alarm-status": "État d'alarme", + "alarm-status-filter": "Filtre d'état d'alarme", + "alarms": "Alarmes", + "clear": "Effacer", + "clear-alarm-text": "Êtes-vous sûr de vouloir effacer l'alarme?", + "clear-alarm-title": "Effacer l'alarme", + "clear-alarms-text": "Êtes-vous sûr de vouloir effacer {count, plural, 1 {1 alarme} other {# alarmes} }?", + "clear-alarms-title": "Effacer {count, plural, 1 {1 alarme} other {# alarmes} }", + "clear-time": "Heure d'éffacement", + "created-time": "Heure de création", + "details": "Détails", + "display-status": { + "ACTIVE_ACK": "Active acquittée", + "ACTIVE_UNACK": "Active non acquittée", + "CLEARED_ACK": "effacée acquittée", + "CLEARED_UNACK": "effacée non acquittée" + }, + "end-time": "Heure de fin", + "min-polling-interval-message": "Un intervalle d'interrogation d'au moins 1 seconde est autorisé.", + "no-alarms-matching": "Aucune alarme correspondant à {{entity}} n'a été trouvée. ", + "no-alarms-prompt": "Aucune alarme", + "no-data": "Aucune donnée à afficher", + "originator": "Source", + "originator-type": "Type de Source", + "polling-interval": "Intervalle d'interrogation des alarmes (sec)", + "polling-interval-required": "L'intervalle d'interrogation des alarmes est requis.", + "search": "Rechercher des alarmes", + "search-status": { + "ACK": "acquitté", + "ACTIVE": "active", + "ANY": "Toutes", + "CLEARED": "effacée", + "UNACK": "non acquittée" + }, + "select-alarm": "Sélectionnez une alarme", + "selected-alarms": "{count, plural, 1 {1 alarme} other {# alarmes} } sélectionnées", + "severity": "Gravité", + "severity-critical": "Critique", + "severity-indeterminate": "indéterminée", + "severity-major": "Majeure", + "severity-minor": "mineure", + "severity-warning": "Avertissement", + "start-time": "Heure de début", + "status": "État", + "type": "Type" + }, + "alias": { + "add": "Ajouter un alias", + "all-entities": "Toutes les entités", + "any-relation": "toutes", + "default-entity-parameter-name": "Par défaut", + "default-state-entity": "Entité d'état par défaut", + "duplicate-alias": "Un alias portant le même nom existe déjà.", + "edit": "Modifier l'alias", + "entity-filter": "Filtre d'entité", + "entity-filter-no-entity-matched": "Aucune entité correspondant au filtre spécifié n'a été trouvée.", + "filter-type": "Type de filtre", + "filter-type-asset-search-query": "requête de recherche d'actifs", + "filter-type-asset-search-query-description": "Actifs de types {{assetTypes}} ayant {{relationType}} relation {{direction}} {{rootEntity}}", + "filter-type-asset-type": "type d'actif", + "filter-type-asset-type-and-name-description": "Actifs de type '{{assetType}}' et dont le nom commence par '{{prefix}}'", + "filter-type-asset-type-description": "Actifs de type '{{assetType}}'", + "filter-type-device-search-query": "Requête de recherche de dispositif", + "filter-type-device-search-query-description": "Dispositifs de types {{deviceTypes}} ayant {{relationType}} relation {{direction}} {{rootEntity}}", + "filter-type-device-type": "Type de dispositif", + "filter-type-device-type-and-name-description": "Dispositifs de type '{{deviceType}}' et dont le nom commence par '{{prefix}}'", + "filter-type-device-type-description": "Dispositifs de type '{{deviceType}}'", + "filter-type-entity-list": "Liste d'entités", + "filter-type-entity-name": "Nom d'entité", + "filter-type-entity-view-search-query": "Requête de recherche vue d'entité", + "filter-type-entity-view-search-query-description": "Vues d'entité avec les types {{entityViewTypes}} ayant {{relationType}} relation {{direction}} {{rootEntity}}", + "filter-type-entity-view-type": "Type de vue d'entité", + "filter-type-entity-view-type-and-name-description": "Vues d'entité de type '{{entityView}}' et dont le nom commence par '{{prefix}}'", + "filter-type-entity-view-type-description": "Vues d'entité de type '{{entityView}}'", + "filter-type-relations-query": "Interrogation des relations", + "filter-type-relations-query-description": "{{entities}} ayant {{relationType}} relation {{direction}} {{rootEntity}}", + "filter-type-required": "Le type de filtre est requis.", + "filter-type-single-entity": "Entité unique", + "filter-type-state-entity": "Entité de l'état du tableau de bord", + "filter-type-state-entity-description": "Entité extraite des paramétres d'état du tableau de bord", + "max-relation-level": "Niveau de relation maximum", + "name": "Nom de l'alias", + "name-required": "Le nom d'alias est requis", + "no-entity-filter-specified": "Aucun filtre d'entité spécifié", + "resolve-multiple": "Résoudre en plusieurs entités", + "root-entity": "Entité racine", + "root-state-entity": "Utiliser l'entité d'état du tableau de bord en tant que racine", + "state-entity": "Entité d'état du tableau de bord", + "state-entity-parameter-name": "Nom du paramétre d'entité d'état", + "unlimited-level": "niveau illimité" + }, + "asset": { + "add": "Ajouter un actif", + "add-asset-text": "Ajouter un nouvel actif", + "any-asset": "Tout actif", + "asset": "Actif", + "asset-details": "Détails de l'actif", + "asset-file": "Actif file", + "asset-public": "L'actif est public", + "asset-required": "Actif requis", + "asset-type": "Type d'actif", + "asset-type-list-empty": "Aucun type d'actif sélectionné.", + "asset-type-required": "Le type d'actif est requis.", + "asset-types": "Types d'actif", + "assets": "Actifs", + "assign-asset-to-customer": "Attribuer des actifs au client", + "assign-asset-to-customer-text": "Veuillez sélectionner les actifs à attribuer au client", + "assign-assets": "Attribuer des actifs", + "assign-assets-text": "Attribuer {count, plural, 1 {1 asset} other {# assets} } au client", + "assign-new-asset": "Attribuer un nouvel Asset", + "assign-to-customer": "Attribuer au client", + "assign-to-customer-text": "Veuillez sélectionner le client pour attribuer le ou les actifs", + "assignedToCustomer": "attribué au client", + "copyId": "Copier l'Id de l'actif", + "delete": "Supprimer un actif", + "delete-asset-text": "Faites attention, après la confirmation, l'actif et toutes les données associées deviendront irrécupérables.", + "delete-asset-title": "Êtes-vous sûr de vouloir supprimer l'actif '{{assetName}}'?", + "delete-assets": "Supprimer des actifs", + "delete-assets-action-title": "Supprimer {count, plural, 1 {1 asset} other {# assets} }", + "delete-assets-text": "Attention, après la confirmation, tous les actifs sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", + "delete-assets-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 asset} other {# assets} }?", + "description": "Description", + "details": "Détails", + "enter-asset-type": "Entrez le type d'actif", + "events": "Evénements", + "idCopiedMessage": "L'Id d'asset a été copié dans le presse-papier", + "import": "Import actifs", + "make-private": "Rendre l'actif privé", + "make-private-asset-text": "Après la confirmation, l'actif et toutes ses données seront rendus privés et ne seront pas accessibles par d'autres.", + "make-private-asset-title": "Êtes-vous sûr de vouloir rendre l'actif '{{assetName}}' privé '?", + "make-public": "Rendre l'actif public", + "make-public-asset-text": "Après la confirmation, l'asset et toutes ses données seront rendus publics et accessibles aux autres.", + "make-public-asset-title": "Êtes-vous sûr de vouloir rendre l'actif '{{assetName}}' public '?", + "management": "Gestion d'actifs", + "name": "Nom", + "name-required": "Nom est requis.", + "name-starts-with": "Le nom de l'actif commence par", + "no-asset-types-matching": "Aucun type d'actif correspondant à {{entitySubtype}} n'a été trouvé. ", + "no-assets-matching": "Aucun actif correspondant à {{entity}} n'a été trouvé. ", + "no-assets-text": "Aucun actif trouvé", + "public": "Public", + "select-asset": "Sélectionner un actif", + "select-asset-type": "Sélectionner le type d'actif", + "type": "Type", + "type-required": "Le type est requis.", + "unassign-asset": "Retirer l'actif", + "unassign-asset-text": "Après la confirmation, l'actif sera non attribué et ne sera pas accessible au client.", + "unassign-asset-title": "Êtes-vous sûr de vouloir retirer l'attribution de l'actif '{{assetName}}'?", + "unassign-assets": "Retirer les actifs", + "unassign-assets-action-title": "Retirer {count, plural, 1 {1 asset} other {# assets} } du client", + "unassign-assets-text": "Après la confirmation, tous les actifs sélectionnés ne seront pas attribués et ne seront pas accessibles au client.", + "unassign-assets-title": "Êtes-vous sûr de vouloir retirer l'attribution de {count, plural, 1 {1 asset} other {# assets} }?", + "unassign-from-customer": "Retirer du client", + "view-assets": "Afficher les actifs", + "label": "Label" + }, + "attribute": { + "add": "Ajouter un attribut", + "add-to-dashboard": "Ajouter au tableau de bord", + "add-widget-to-dashboard": "Ajouter un widget au tableau de bord", + "attributes": "Attributs", + "attributes-scope": "Étendue des attributs d'entité", + "delete-attributes": "Supprimer les attributs", + "delete-attributes-text": "Attention, après la confirmation, tous les attributs sélectionnés seront supprimés.", + "delete-attributes-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 attribut} other {# attributs} }?", + "enter-attribute-value": "Entrez la valeur de l'attribut", + "key": "Clé", + "key-required": "La Clé d'attribut est requise.", + "last-update-time": "Dernière mise à jour", + "latest-telemetry": "Dernière télémétrie", + "next-widget": "Widget suivant", + "prev-widget": "Widget précédent", + "scope-client": "Attributs du client", + "scope-latest-telemetry": "Dernière télémétrie", + "scope-server": "Attributs du serveur", + "scope-shared": "Attributs partagés", + "selected-attributes": "{count, plural, 1 {1 attribut} other {# attributs} } sélectionnés", + "selected-telemetry": "{count, plural, 1 {1 unité de télémétrie} other {# unités de télémétrie} } sélectionnées", + "show-on-widget": "Afficher sur le widget", + "value": "Valeur", + "value-required": "La valeur d'attribut est obligatoire.", + "widget-mode": "Mode du widget" + }, + "audit-log": { + "action-data": "Action data", + "audit": "Audit", + "audit-log-details": "Détails du journal d'audit", + "audit-logs": "Journaux d'audit", + "clear-search": "Effacer la recherche", + "details": "Détails", + "entity-name": "Nom de l'entité", + "entity-type": "Type d'entité", + "failure-details": "Détails de l'échec", + "no-audit-logs-prompt": "Aucun journal trouvé", + "search": "Rechercher les journaux d'audit", + "status": "État", + "status-failure": "Échec", + "status-success": "Succès", + "timestamp": "Horodatage", + "type": "Type", + "type-activated": "Activé", + "type-added": "Ajouté", + "type-alarm-ack": "Acquitté", + "type-alarm-clear": "Effacé", + "type-assigned-to-customer": "Attribué au client", + "type-attributes-deleted": "Attributs supprimés", + "type-attributes-read": "Attributs lus", + "type-attributes-updated": "Attributs mis à jour", + "type-credentials-read": "Lecture des informations d'identification", + "type-credentials-updated": "Informations d'identification actualisées", + "type-deleted": "Supprimé", + "type-login": "Login", + "type-logout": "Connectez - Out", + "type-lockout": "Verrouillage", + "type-relation-add-or-update": "Relation mise à jour", + "type-relation-delete": "Relation supprimée", + "type-relations-delete": "Toutes les relations ont été supprimées", + "type-rpc-call": "Appel RPC", + "type-suspended": "Suspendu", + "type-unassigned-from-customer": "Non attribué du client", + "type-updated": "Mise à jour", + "user": "Utilisateur" + }, + "common": { + "enter-password": "Entrez le mot de passe", + "enter-search": "Entrez la recherche", + "enter-username": "Entrez le nom d'utilisateur", + "password": "Mot de passe", + "username": "Nom d'utilisateur", + "created-time": "Heure de création" + }, + "confirm-on-exit": { + "html-message": "Vous avez des modifications non enregistrées.
    Êtes-vous sûr de vouloir quitter cette page?", + "message": "Vous avez des modifications non enregistrées. Êtes-vous sûr de vouloir quitter cette page?", + "title": "Modifications non enregistrées" + }, + "contact": { + "address": "Adresse", + "address2": "adresse 2", + "city": "Ville", + "country": "Pays", + "email": "Email", + "no-address": "Pas d'adresse", + "phone": "Téléphone", + "postal-code": "Code postal", + "postal-code-invalid": "Format de code postal / code postal invalide", + "state": "Province" + }, + "content-type": { + "binary": "Binaire (Base64)", + "json": "Json", + "text": "Texte" + }, + "custom": { + "widget-action": { + "action-cell-button": "Bouton de cellule d'action", + "marker-click": "Sur le marqueur cliquez", + "row-click": "Au rang, cliquez", + "polygon-click": "Cliquez sur le polygone", + "tooltip-tag-action": "Action de balise d'info-bulle", + "node-selected": "Sur le noeud sélectionné", + "element-click": "Sur l'élément HTML, cliquez sur", + "pie-slice-click": "Sur tranche cliquez", + "row-double-click": "Sur la ligne double clic" + } + }, + "customer": { + "add": "Ajouter un client", + "add-customer-text": "Ajouter un nouveau client", + "assets": "Actifs du client", + "copyId": "Copier l'id du client", + "customer": "Client", + "customer-details": "Détails du client", + "customer-required": "Le client est requis", + "customers": "Clients", + "dashboard": "Tableau de bord du client", + "dashboards": "tableaux de bord du client", + "default-customer": "Client par défaut", + "default-customer-required": "Le client par défaut est requis pour déboguer le tableau de bord au niveau du Tenant", + "delete": "Supprimer le client", + "delete-customer-text": "Faites attention, après la confirmation, le client et toutes les données associées deviendront irrécupérables.", + "delete-customer-title": "Êtes-vous sûr de vouloir supprimer le client '{{customerTitle}}'?", + "delete-customers-action-title": "Supprimer {count, plural, 1 {1 customer} other {# customers} }", + "delete-customers-text": "Faites attention, après la confirmation, tous les clients sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", + "delete-customers-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 customer} other {# customers} }?", + "description": "Description", + "details": "Détails", + "devices": "Dispositifs du client", + "entity-views": "Vues de l'entité client", + "events": "Événements", + "idCopiedMessage": "L'Id du client a été copié dans le presse-papier", + "manage-assets": "Gérer les actifs", + "manage-customer-assets": "Gérer les actifs du client", + "manage-customer-dashboards": "Gérer les tableaux de bord du client", + "manage-customer-devices": "Gérer les dispositifs du client", + "manage-customer-users": "Gérer les utilisateurs du client", + "manage-dashboards": "Gérer les tableaux de bord", + "manage-devices": "Gérer les dispositifs", + "manage-public-assets": "Gérer les actifs publics", + "manage-public-dashboards": "Gérer les tableaux de bord publics", + "manage-public-devices": "Gérer les dispositifs publics", + "manage-users": "Gérer les utilisateurs", + "management": "Gestion des clients", + "no-customers-matching": "Aucun client correspondant à '{{entity}} n'a été trouvé.", + "no-customers-text": "Aucun client trouvé", + "public-assets": "Actifs publics", + "public-dashboards": "Tableaux de bord publics", + "public-devices": "Dispositifs publics", + "public-entity-views": "Vues d'entités publiques", + "select-customer": "Sélectionner un client", + "select-default-customer": "Sélectionnez le client par défaut", + "title": "Titre", + "title-required": "Le titre est requis." + }, + "dashboard": { + "add": "Ajouter un tableau de bord", + "add-dashboard-text": "Ajouter un nouveau tableau de bord", + "add-state": "Ajouter un état du tableau de bord", + "add-widget": "Ajouter un nouveau widget", + "alias-resolution-error-title": "Erreur de configuration des alias de tableau de bord", + "assign-dashboard-to-customer": "Attribuer des tableaux de bord au client", + "assign-dashboard-to-customer-text": "Veuillez sélectionner les tableaux de bord à affecter au client", + "assign-dashboards": "Attribuer des tableaux de bord", + "assign-dashboards-text": "Attribuer {count, plural, 1 {1 tableau de bord} other {# tableaux de bord} } aux clients", + "assign-new-dashboard": "Attribuer un nouveau tableau de bord", + "assign-to-customer": "Attribuer au client", + "assign-to-customer-text": "Veuillez sélectionner le client pour attribuer le ou les tableaux de bord", + "assign-to-customers": "Attribuer des tableaux de bord aux clients", + "assign-to-customers-text": "Veuillez sélectionner les clients pour attribuer les tableaux de bord", + "assigned-customers": "clients affectés", + "assignedToCustomer": "Attribué au client", + "assignedToCustomers": "attribué aux clients", + "autofill-height": "Hauteur de remplissage automatique", + "background-color": "Couleur de fond", + "background-image": "Image d'arriére-plan", + "background-size-mode": "Mode de taille d'arriére-plan", + "close-toolbar": "Fermer la barre d'outils", + "columns-count": "Nombre de colonnes", + "columns-count-required": "Le nombre de colonnes est requis.", + "configuration-error": "Erreur de configuration", + "copy-public-link": "Copier le lien public", + "create-new": "Créer un nouveau tableau de bord", + "create-new-dashboard": "Créer un nouveau tableau de bord", + "create-new-widget": "Créer un nouveau widget", + "dashboard": "Tableau de bord", + "dashboard-details": "Détails du tableau de bord", + "dashboard-file": "Fichier du tableau de bord", + "dashboard-import-missing-aliases-title": "Configurer les alias utilisés par le tableau de bord importé", + "dashboard-required": "Le tableau de bord est requis.", + "dashboards": "Tableaux de bord", + "delete": "Supprimer le tableau de bord", + "delete-dashboard-text": "Faites attention, après la confirmation, le tableau de bord et toutes les données associées deviendront irrécupérables.", + "delete-dashboard-title": "Êtes-vous sûr de vouloir supprimer le tableau de bord '{{dashboardTitle}}'?", + "delete-dashboards": "Supprimer les tableaux de bord", + "delete-dashboards-action-title": "Supprimer {count, plural, 1 {1 tableau de bord} other {# tableaux de bord} }", + "delete-dashboards-text": "Attention, après la confirmation, tous les tableaux de bord sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", + "delete-dashboards-title": "Voulez-vous vraiment supprimer {count, plural, 1 {1 tableau de bord} other {# tableaux de bord} }?", + "delete-state": "Supprimer l'état du tableau de bord", + "delete-state-text": "Etes-vous sûr de vouloir supprimer l'état du tableau de bord avec le nom '{{stateName}}'?", + "delete-state-title": "Supprimer l'état du tableau de bord", + "description": "Description", + "details": "Détails", + "display-dashboard-export": "Afficher l'exportation", + "display-dashboard-timewindow": "Afficher fenêtre de temps", + "display-dashboards-selection": "Afficher la sélection des tableaux de bord", + "display-entities-selection": "Afficher la sélection des entités", + "display-title": "Afficher le titre du tableau de bord", + "drop-image": "Déposer une image ou cliquez pour sélectionner un fichier à télécharger.", + "edit-state": "Modifier l'état du tableau de bord", + "export": "Exporter le tableau de bord", + "export-failed-error": "Impossible d'exporter le tableau de bord: {{error}}", + "hide-details": "Masquer les détails", + "horizontal-margin": "Marge horizontale", + "horizontal-margin-required": "Une valeur de marge horizontale est requise.", + "import": "Importer le tableau de bord", + "import-widget": "Importer un widget", + "invalid-aliases-config": "Impossible de trouver des dispositifs correspondant à certains filtres d'alias.
    Veuillez contacter votre administrateur pour résoudre ce problème.", + "invalid-dashboard-file-error": "Impossible d'importer le tableau de bord: structure de données du tableau de bord non valide", + "invalid-widget-file-error": "Impossible d'importer le widget: structure de données de widget invalide.", + "is-root-state": "État racine", + "make-private": "Rendre privé le tableau de bord", + "make-private-dashboard": "Rendre privé le tableau de bord", + "make-private-dashboard-text": "Après la confirmation, le tableau de bord sera rendu privé et ne sera plus accessible aux autres.", + "make-private-dashboard-title": "Êtes-vous sûr de vouloir rendre le tableau de bord '{{dashboardTitle}}' privé?", + "make-public": "Rendre public le tableau de bord", + "manage-assigned-customers": "Gérer les clients affectés", + "manage-states": "Gérer les états du tableau de bord", + "management": "Gestion du tableau de bord", + "max-columns-count-message": "Seulement 1000 colonnes maximum sont autorisées.", + "max-horizontal-margin-message": "Seulement 50 sont autorisés en tant que valeur de marge horizontale maximale.", + "max-mobile-row-height-message": "Seuls 200 pixels sont autorisés en tant que valeur maximale de hauteur de ligne mobile.", + "max-vertical-margin-message": "Seulement 50 sont autorisés en tant que valeur de marge verticale maximale.", + "min-columns-count-message": "Seul un nombre minimum de 10 colonnes est autorisé.", + "min-horizontal-margin-message": "Seul 0 est autorisé comme valeur de marge horizontale minimale.", + "min-mobile-row-height-message": "Seuls 5 pixels sont autorisés en tant que valeur minimale de hauteur de ligne mobile.", + "min-vertical-margin-message": "Seul 0 est autorisé comme valeur de marge verticale minimale.", + "mobile-layout": "Paramètres de mise en page mobiles", + "mobile-row-height": "Hauteur de ligne mobile, px", + "mobile-row-height-required": "Une valeur de hauteur de ligne mobile est requise.", + "new-dashboard-title": "Nouveau titre du tableau de bord", + "no-dashboards-matching": "Aucun tableau de bord correspondant à {{entity}} n'a été trouvé. ", + "no-dashboards-text": "Aucun tableau de bord trouvé", + "no-image": "Aucune image sélectionnée", + "no-widgets": "Aucun widget configuré", + "open-dashboard": "Ouvrir le tableau de bord", + "open-toolbar": "Ouvrir la barre d'outils du tableau de bord", + "public": "Public", + "public-dashboard-notice": " Remarque: N'oubliez pas de rendre publics les dispositifs associés pour accéder à leurs données.", + "public-dashboard-text": "Votre tableau de bord {{dashboardTitle}} est maintenant public et accessible via le lien public
    : ", + "public-dashboard-title": "Le tableau de bord est maintenant public", + "public-link": "Lien public", + "public-link-copied-message": "Le lien public du tableau de bord a été copié dans le presse-papier", + "search-states": "Recherche des états du tableau de bord", + "select-dashboard": "Sélectionner le tableau de bord", + "select-devices": "Selectionner les dispositifs", + "select-existing": "Sélectionnez un tableau de bord existant", + "select-state": "Sélectionnez l'état cible", + "select-widget-subtitle": "Liste des types de widgets disponibles", + "select-widget-title": "Sélectionner un widget", + "selected-states": "{count, plural, 1 {1 état du tableau de bord} other {# états du tableau de bord} } sélectionnés", + "set-background": "Définir l'arrière-plan", + "settings": "Paramètres", + "show-details": "Afficher les détails", + "socialshare-text": "'{{dashboardTitle}}' propulsé par ThingsBoard", + "socialshare-title": "'{{dashboardTitle}}' propulsé par ThingsBoard", + "state": "État du tableau de bord", + "state-controller": "Contrôleur d'état", + "state-id": "ID d'état", + "state-id-exists": "L'état du tableau de bord avec le même Id existe déjà.", + "state-id-required": "L'Id d'état du tableau de bord est requis.", + "state-name": "Nom", + "state-name-required": "Le nom de l'état du tableau de bord est requis", + "states": "États du tableau de bord", + "title": "Titre", + "title-color": "Couleur du titre", + "title-required": "Le titre est requis.", + "toolbar-always-open": "Garder la barre d'outils ouverte", + "unassign-dashboard": "Retirer le tableau de bord", + "unassign-dashboard-text": "Après la confirmation, le tableau de bord ne sera pas attribué et ne sera pas accessible au client.", + "unassign-dashboard-title": "Êtes-vous sûr de vouloir annuler l'affectation du tableau de bord '{{dashboardTitle}}'?", + "unassign-dashboards": "Retirer les tableaux de bord", + "unassign-dashboards-action-text": "Annuler l'affectation {count, plural, 1 {1 tableau de bord} other {# tableaux de bord} } des clients", + "unassign-dashboards-action-title": "Annuler l'affectation {count, plural, 1 {1 tableau de bord} other {# tableaux de bord} } du client", + "unassign-dashboards-text": "Après la confirmation, tous les tableaux de bord sélectionnés ne seront pas attribués et ne seront pas accessibles au client.", + "unassign-dashboards-title": "Etes-vous sûr de vouloir annuler l'affectation {count, plural, 1 {1 tableau de bord} other {# tableaux de bord} }?", + "unassign-from-customer": "Retirer du client", + "unassign-from-customers": "Retirer les tableaux de bord des clients", + "unassign-from-customers-text": "Veuillez sélectionner les clients à annuler l'affectation du ou des tableaux de bord", + "vertical-margin": "Marge verticale", + "vertical-margin-required": "Une valeur de marge verticale est requise", + "view-dashboards": "Afficher les tableaux de bord", + "widget-file": "Fichier du Widget", + "widget-import-missing-aliases-title": "Configurer les alias utilisés par le widget importé", + "widgets-margins": "Marge entre les widgets" + }, + "datakey": { + "advanced": "Avancé", + "alarm": "Champs d'alarme", + "alarm-fields-required": "Les champs d'alarme sont obligatoires.", + "attributes": "Attributs", + "color": "Couleur", + "configuration": "Configuration de la clé de données", + "data-generation-func": "Fonction de génération de données", + "decimals": "Nombre de chiffres après virgule flottante", + "function-types": "Types de fonctions", + "function-types-required": "Les types de fonctions sont obligatoires", + "label": "Label", + "maximum-function-types": "Maximum {count, plural, 1 {1 type de fonction est autorisé.} other {# types de fonctions sont autorisés} }", + "maximum-timeseries-or-attributes": "Maximum {count, plural, 1 {1 timeseries / attribut est autorisé.} other {# timeseries / attributs sont autorisés} }", + "prev-orig-value-description": "valeur précédente d'origine;", + "prev-value-description": "résultat de l'appel de fonction précédent;", + "settings": "Paramètres", + "time-description": "horodatage de la valeur actuelle;", + "time-prev-description": "horodatage de la valeur précédente;", + "timeseries": "Timeseries", + "timeseries-or-attributes-required": "Les timeseries / attributs d'entité sont obligatoires.", + "timeseries-required": "Les Timeseries de l'entité sont obligatoires.", + "units": "Symbole spécial à afficher à côté de la valeur", + "use-data-post-processing-func": "Utiliser la fonction de post-traitement des données", + "value-description": "la valeur actuelle;" + }, + "datasource": { + "add-datasource-prompt": "Veuillez ajouter une source de données", + "name": "Nom", + "type": "Type de source de données" + }, + "datetime": { + "date-from": "Date de", + "date-to": "Date à", + "time-from": "Heure de", + "time-to": "Heure à" + }, + "details": { + "edit-mode": "Mode édition", + "toggle-edit-mode": "Activer le mode édition" + }, + "device": { + "access-token": "Jeton d'accès", + "access-token-invalid": "La longueur du jeton d'accès doit être comprise entre 1 et 20 caractéres.", + "access-token-required": "Le jeton d'accès est requis.", + "accessTokenCopiedMessage": "Le jeton d'accès au dispositif a été copié dans le presse-papier", + "add": "Ajouter un dispositif", + "add-alias": "Ajouter un alias de dispositif", + "add-device-text": "Ajouter un nouveau dispositif", + "alias": "Alias", + "alias-required": "Un alias du dispositif est requis.", + "aliases": "Alias des dispositifs", + "any-device": "N'importe quel dispositif", + "assign-device-to-customer": "Affecter des dispositifs au client", + "assign-device-to-customer-text": "Veuillez sélectionner les dispositif à affecter au client", + "assign-devices": "Attribuer des dispositifs", + "assign-devices-text": "Attribuer {count, plural, 1 {1 dispositif} other {# dispositifs} } au client", + "assign-new-device": "Attribuer un nouveau dispositif", + "assign-to-customer": "Attribuer au client", + "assign-to-customer-text": "Veuillez sélectionner le client pour attribuer le ou les dispositifs", + "assignedToCustomer": "Attribué au client", + "configure-alias": "Configurer '{{alias}}' alias", + "copyAccessToken": "Copier le jeton d'accès", + "copyId": "Copier l'Id du dispositif", + "create-new-alias": "Créez un nouveau!", + "create-new-key": "Créez un nouveau!", + "credentials": "Informations d'identification", + "credentials-type": "Type d'identification", + "delete": "Supprimer le dispositif", + "delete-device-text": "Faites attention, après la confirmation, le dispositif et toutes les données associées deviendront irrécupérables.", + "delete-device-title": "Êtes-vous sûr de vouloir supprimer le dispositif '{{deviceName}}'?", + "delete-devices": "Supprimer les dispositifs", + "delete-devices-action-title": "Supprimer {count, plural, 1 {1 device} other {# devices} }", + "delete-devices-text": "Faites attention, après la confirmation, tous les dispositifs sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", + "delete-devices-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 device} other {# devices} }?", + "description": "Description", + "details": "Détails", + "device": "Dispositif", + "device-alias": "Alias ​​du dispositif", + "device-credentials": "Informations d'identification du dispositif", + "device-details": "Détails du dispositif", + "device-list": "Liste des dispositifs", + "device-list-empty": "Aucun dispositif sélectionné.", + "device-name-filter-no-device-matched": "Aucun dispositif commençant par '{{device}} n'a été trouvé.", + "device-name-filter-required": "Le filtre de nom de dispositif est requis.", + "device-public": "Le dispositif est public", + "device-required": "Le dispositif est requis.", + "device-type": "Type de dispositif", + "device-type-list-empty": "Aucun type de dispositif sélectionné.", + "device-type-required": "Le type de dispositif est requis.", + "device-types": "Types de dispositif", + "devices": "Dispositifs", + "duplicate-alias-error": "Alias ??en double trouvé '{{alias}}'.
    Les alias de dispositifs doivent être uniques dans le tableau de bord.", + "enter-device-type": "Entrez le type de dispositif", + "events": "Événements", + "idCopiedMessage": "l'Id du dispositif a été copié dans le presse-papiers", + "is-gateway": "Est une passerelle", + "label": "Label", + "make-private": "Rendre le dispositif privé", + "make-private-device-text": "Après la confirmation, le dispositif et toutes ses données seront rendues privées et ne seront pas accessibles par d'autres.", + "make-private-device-title": "Êtes-vous sûr de vouloir rendre le dispositif {{deviceName}} privé?", + "make-public": "Rendre le dispositif public", + "make-public-device-text": "Après la confirmation, le dispositif et toutes ses données seront rendus publics et accessibles par d'autres.", + "make-public-device-title": "Êtes-vous sûr de vouloir rendre le dispositif {{deviceName}} 'public?", + "manage-credentials": "Gérer les informations d'identification", + "management": "Gestion des dispositifs", + "name": "Nom", + "name-required": "Le nom est requis.", + "name-starts-with": "Le nom du dispositif commence par", + "no-alias-matching": "'{{alias}}' introuvable.", + "no-aliases-found": "Aucun alias trouvé.", + "no-device-types-matching": "Aucun type de dispositif correspondant à {{entitySubtype}} n'a été trouvé.", + "no-devices-matching": "Aucun dispositif correspondant à '{{entity}} n'a été trouvé.", + "no-devices-text": "Aucun dispositif trouvé", + "no-key-matching": "'{{key}}' introuvable.", + "no-keys-found": "Aucune clé trouvée", + "public": "Public", + "remove-alias": "Supprimer l'alias du dispositif", + "rsa-key": "Clé publique RSA", + "rsa-key-required": "La clé publique RSA est requise.", + "secret": "Secret", + "secret-required": "Code secret est requis.", + "select-device": "Selectionner un dispositif", + "select-device-type": "Sélectionner le type d'appareil", + "unable-delete-device-alias-text": "L'alias du dispositif '{{deviceAlias}}' ne peut pas être supprimé car il est utilisé par les widgets suivants:
    {{widgetsList}}", + "unable-delete-device-alias-title": "Impossible de supprimer l'alias du dispositif", + "unassign-device": "Annuler l'affectation du dispositif", + "unassign-device-text": "Après la confirmation, le dispositif ne sera pas attribué et ne sera pas accessible au client.", + "unassign-device-title": "Êtes-vous sûr de vouloir annuler l'affection du dispositif {{deviceName}} '?", + "unassign-devices": "Annuler l'affectation des dispositifs", + "unassign-devices-action-title": "Annuler l'affectation de {count, plural, 1 {1 device} other {#devices} } du client", + "unassign-devices-text": "Après la confirmation, tous les dispositifs sélectionnés ne seront pas attribues et ne seront pas accessibles par le client.", + "unassign-devices-title": "Voulez-vous vraiment annuler l'affectation de {count, plural, 1 {1 device} other {# devices} }?", + "unassign-from-customer": "Retirer du client", + "use-device-name-filter": "Utiliser le filtre", + "view-credentials": "Afficher les informations d'identification", + "view-devices": "Afficher les dispositifs" + }, + "dialog": { + "close": "Fermer le dialogue" + }, + "entity": { + "add-alias": "Ajouter un alias d'entité", + "alarm-name-starts-with": "Les actifs dont le nom commence par '{{prefix}}'", + "alias": "Alias", + "alias-required": "Un alias d'entité est requis.", + "aliases": "alias d'entité", + "all-subtypes": "Tout", + "any-entity": "Toute entité", + "asset-name-starts-with": "Les Assets dont le nom commence par '{{prefix}}'", + "columns-to-display": "Colonnes à afficher", + "configure-alias": "Configurer '{{alias}}' alias", + "create-new-alias": "Créez un nouveau!", + "create-new-key": "Créez un nouveau!", + "customer-name-starts-with": "Les clients dont les noms commencent par '{{prefix}}'", + "dashboard-name-starts-with": "Les tableaux de bord dont les noms commencent par '{{prefix}}'", + "details": "Détails de l'entité", + "device-name-starts-with": "Dispositifs dont le nom commence par '{{prefix}}'", + "duplicate-alias-error": "Alias ​​en double trouvé '{{alias}}'.
    Les alias d'entité doivent être uniques dans le tableau de bord.", + "enter-entity-type": "Entrez le type d'entité", + "entities": "Entités", + "entity": "Entité", + "entity-alias": "Alias de l'entité", + "entity-list": "Liste d'entités", + "entity-list-empty": "Aucune entité sélectionnée.", + "entity-name": "Nom de l'entité", + "entity-name-filter-no-entity-matched": "Aucune entité commençant par '{{entity}}' n'a été trouvée.", + "entity-name-filter-required": "Le filtre de nom d'entité est requis.", + "entity-type": "Type d'entité", + "entity-type-list": "Liste de types d'entités", + "entity-type-list-empty": "Aucun type d'entité sélectionné.", + "entity-types": "Types d'entité", + "entity-view-name-starts-with": "Les vues d'entité dont le nom commence par '{{prefix}}'", + "key": "Clé", + "key-name": "Nom de la clé", + "list-of-alarms": "{count, plural, 1 {Une alarme} other {Liste de # alarmes} }", + "list-of-assets": "{count, plural, 1 {Un Asset} other {Liste de # Assets} }", + "list-of-customers": "{count, plural, 1 {Un client} other {Liste de # clients} }", + "list-of-dashboards": "{count, plural, 1 {Un tableau de bord} other {Liste de # tableaux de bord} }", + "list-of-devices": "{count, plural, 1 {Un dispositif} other {Liste de # dispositifs} }", + "list-of-plugins": "{count, plural, 1 {Un plugin} other {Liste de # plugins} }", + "list-of-rulechains": "{count, plural, 1 {Une chaîne de règles} other {Liste de # chaînes de règles} }", + "list-of-rulenodes": "{count, plural, 1 {Un noeud de règles} other {Liste de # noeuds de règles} }", + "list-of-rules": "{count, plural, 1 {Une règle} other {Liste de # règles} }", + "list-of-tenants": "{count, plural, 1 {Un tenant} other {Liste de # tenants} }", + "list-of-users": "{count, plural, 1 {Un utilisateur} other {Liste de # utilisateurs} }", + "missing-entity-filter-error": "Le filtre est manquant pour l'alias '{{alias}}'.", + "name-starts-with": "Nom commence par", + "no-alias-matching": "'{{alias}}' introuvable.", + "no-aliases-found": "Aucun alias trouvé.", + "no-data": "Aucune donnée à afficher", + "no-entities-matching": "Aucune entité correspondant à '{{entity}}' n'a été trouvée.", + "no-entities-prompt": "Aucune entité trouvée", + "no-entity-types-matching": "Aucun type d'entité correspondant à {{entityType}} n'a été trouvé. ", + "no-key-matching": "'{{key}}' introuvable.", + "no-keys-found": "Aucune clé trouvée", + "plugin-name-starts-with": "Plugins dont les noms commencent par '{{prefix}}'", + "remove-alias": "Supprimer l'alias d'entité", + "rule-name-starts-with": "Régles dont les noms commencent par '{{prefix}}'", + "rulechain-name-starts-with": "Chaînes de régles dont les noms commencent par '{{prefix}}'", + "rulenode-name-starts-with": "Les noeuds de régles dont le nom commence par '{{prefix}}'", + "search": "Recherche d'entités", + "select-entities": "Sélectionner des entités", + "selected-entities": "{count, plural, 1 {1 entité} other {# entités} } sélectionnées", + "tenant-name-starts-with": "Les Tenant dont le nom commence par '{{prefix}}'", + "type": "Type", + "type-alarm": "Alarme", + "type-alarms": "Alarmes", + "type-asset": "Actif", + "type-assets": "Actifs", + "type-current-customer": "Client actuel", + "type-customer": "Client", + "type-customers": "Clients", + "type-dashboard": "Tableau de bord", + "type-dashboards": "Tableaux de bord", + "type-device": "Dispositif", + "type-devices": "Dispositifs", + "type-entity-view": "Vue d'entité", + "type-entity-views": "Vues d'entités", + "type-plugin": "Plugin", + "type-plugins": "Plugins", + "type-required": "Le type d'entité est obligatoire.", + "type-rule": "Régle", + "type-rulechain": "Chaîne de régles", + "type-rulechains": "Chaînes de régles", + "type-rulenode": "Noeud de régle", + "type-rulenodes": "Noeuds de régle", + "type-rules": "Régles", + "type-tenant": "Tenant", + "type-tenants": "Tenants", + "type-user": "Utilisateur", + "type-users": "Utilisateurs", + "unable-delete-entity-alias-text": "L'alias d'entité '{{entityAlias}}' ne peut pas être supprimé car il est utilisé par les widgets suivants:
    {{widgetsList}}", + "unable-delete-entity-alias-title": "Impossible de supprimer l'alias d'entité", + "use-entity-name-filter": "Utiliser un filtre", + "user-name-starts-with": "Utilisateurs dont les noms commencent par '{{prefix}}'" + }, + "entity-field": { + "address": "Adresse", + "address2": "Adresse 2", + "city": "Ville", + "country": "Pays", + "created-time": "Heure de création", + "email": "Email", + "first-name": "Prénom", + "last-name": "Nom de famille", + "name": "Nom", + "phone": "Téléphone", + "state": "Prov", + "title": "Titre", + "type": "Type", + "zip": "Code postal" + }, + "entity-view": { + "add": "Ajouter une vue d'entité", + "add-alias": "Ajouter un alias de vue d'entité", + "add-entity-view-text": "Ajouter une nouvelle vue d'entité", + "alias": "Alias", + "alias-required": "Un alias de vue d'entité est requis.", + "aliases": "Alias de vue d'entité", + "any-entity-view": "Toute vue d'entité", + "assign-entity-view-to-customer": "Attribuer une (des) vue (s) d'entité à un client", + "assign-entity-view-to-customer-text": "Veuillez sélectionner les vues d'entité à affecter au client", + "assign-entity-views": "Attribuer des vues d'entité", + "assign-entity-views-text": "Attribuer { count, plural, 1 {1 entityView} other {# entityViews} } au client", + "assign-new-entity-view": "Attribuer une nouvelle vue d'entité", + "assign-to-customer": "Attribuer au client", + "assign-to-customer-text": "Veuillez sélectionner le client auquel attribuer la ou les vues d'entité.", + "assignedToCustomer": "Assigné au client", + "attributes-propagation": "Propagation des attributs", + "attributes-propagation-hint": "La vue d'entité copiera automatiquement les attributs spécifiés de l'entité cible chaque fois que vous enregistrez ou mettez à jour cette vue d'entité. Pour des raisons de performances, les attributs d'entité cible ne sont pas propagés à la vue d'entité à chaque changement d'attribut. Vous pouvez activer la propagation automatique en configurant le noeud de règle \" copier pour afficher \" dans votre chaîne de règles et en liant les messages \"Post attributs \" et \"attributs mis à jour \" au nouveau noeud de règle.", + "client-attributes": "Attributs du client", + "client-attributes-placeholder": "Attributs du client", + "configure-alias": "Configurez l'alias '{{alias}}'", + "copyId": "Copier l'ID de la vue d'entité", + "create-new-alias": "Créer un nouveau!", + "create-new-key": "Créer un nouveau!", + "date-limits": "Limites de date", + "delete": "Supprimer la vue d'entité", + "delete-entity-view-text": "Attention, après la confirmation, la vue de l'entité et toutes les données associées deviendront irrécupérables.", + "delete-entity-view-title": "Êtes-vous sûr de vouloir supprimer la vue de l'entité '{{entityViewName}}'?", + "delete-entity-views": "Supprimer les vues d'entité", + "delete-entity-views-action-title": "Supprimer { count, plural, 1 {1 entityView} other {# entityViews} }", + "delete-entity-views-text": "Attention, après la confirmation, toutes les vues d'entité sélectionnées seront supprimées et toutes les données associées deviendront irrécupérables.", + "delete-entity-views-title": "Êtes-vous sûr de vouloir voir l'entité { count, plural, 1 {1 entityView} other {# entityViews} }?", + "description": "Description", + "details": "Détails", + "duplicate-alias-error": "Alias '{{alias}}' existe déjà.
    Les alias de vue d'entité doivent être uniques dans le tableau de bord.", + "end-date": "Date de fin", + "end-ts": "Heure de fin", + "enter-entity-view-type": "Entrer le type de vue d'entité", + "entity-view": "Vue d'entité", + "entity-view-alias": "Alias de vue d'entité", + "entity-view-details": "Détails de la vue d'entité", + "entity-view-list": "Liste de vues d'entités", + "entity-view-list-empty": "Aucune vue d'entité sélectionnée.", + "entity-view-name-filter-no-entity-view-matched": "Aucune vue d'entité commençant par '{{entityView}}' n'a été trouvée.", + "entity-view-name-filter-required": "Un filtre de nom de vue d'entité est requis.", + "entity-view-required": "Une vue d'entité est requise.", + "entity-view-type": "Type de vue d'entité", + "entity-view-type-list-empty": "Aucun type de vue d'entité sélectionné.", + "entity-view-type-required": "Le type d'entité est requis.", + "entity-view-types": "Types de vues d'entité", + "entity-views": "Vues d'entité", + "events": "Événements", + "make-private": "Rendre la vue d'entité privée", + "make-private-entity-view-text": "Après la confirmation, la vue de l'entité et toutes ses données seront rendues privées et ne seront pas accessibles par d'autres", + "make-private-entity-view-title": "Êtes-vous sûr de vouloir rendre la vue d'entité '{{entityViewName}}' privée?", + "make-public": "Rendre la vue d'entité publique", + "make-public-entity-view-text": "Après la confirmation, la vue de l'entité et toutes ses données seront rendues publiques et accessibles à d'autres", + "make-public-entity-view-title": "Voulez-vous vraiment que la vue de l'entité '{{entityViewName}}' soit publique?", + "management": "Gestion de vue d'entité", + "name": "Nom", + "name-required": "Un nom est requis.", + "name-starts-with": "Le nom de la vue d'entité commence par", + "no-alias-matching": "'{{alias}}' non trouvé.", + "no-aliases-found": "Aucun alias trouvé.", + "no-entity-view-types-matching": "Aucun type de vue d'entité correspondant à '{{entitySubtype}}' n'a été trouvé.", + "no-entity-views-matching": "Aucune vue d'entité correspondant à '{{entity}}' n'a été trouvée.", + "no-entity-views-text": "Aucune vue d'entité trouvée.", + "no-key-matching": "'{{key}}' non trouvé.", + "no-keys-found": "Aucune clé trouvée.", + "remove-alias": "Supprimer un alias de vue d'entité", + "select-entity-view": "Sélectionner une vue d'entité", + "select-entity-view-type": "Sélectionner le type de vue d'entité", + "server-attributes": "Attributs du serveur", + "server-attributes-placeholder": "Attributs du serveur", + "shared-attributes": "Attributs partagés", + "shared-attributes-placeholder": "Attributs partagés", + "start-date": "Date de début", + "start-ts": "Heure de début", + "target-entity": "Entité cible", + "timeseries": "Séries chronologiques", + "timeseries-data": "Données de séries chronologiques", + "timeseries-data-hint": "Configurez les clés de données de séries chronologiques de l'entité cible qui seront accessibles à la vue de l'entité. Ces données temporelles sont en lecture seule.", + "timeseries-placeholder": "Séries chronologiques", + "unable-entity-view-device-alias-text": "L'alias de dispositif '{{entityViewAlias}}' ne peut pas être supprimé car il est utilisé par les widgets suivants:
    {{widgetsList}}", + "unable-entity-view-device-alias-title": "Impossible de supprimer l'alias de la vue d'entité.", + "unassign-entity-view": "Annuler l'affectation de la vue d'entité", + "unassign-entity-view-text": "Après la confirmation, la vue de l'entité sera non attribuée et ne sera pas accessible par le client.", + "unassign-entity-view-title": "Voulez-vous vraiment annuler l'attribution de la vue d'entité '{{entityViewName}}'?", + "unassign-entity-views": "Annuler l'attribution des vues d'entité", + "unassign-entity-views-action-title": "Annuler l'attribution { count, plural, 1 {1 entityView} other {# entityViews} } du client", + "unassign-entity-views-text": "Après la confirmation, toutes les vues des entités sélectionnées seront non attribuées et ne seront pas accessibles par le client.", + "unassign-entity-views-title": "Êtes-vous sûr de vouloir annuler l'attribution { count, plural, 1 {1 entityView} other {# entityViews} }?", + "unassign-from-customer": "Annuler l'attribution au client", + "use-entity-view-name-filter": "Use filter", + "view-entity-views": "Voir les vues d'entité" + }, + "error": { + "unable-to-connect": "Impossible de se connecter au serveur! Veuillez vérifier votre connexion Internet.", + "unhandled-error-code": "Code d'erreur non géré: {{errorCode}}", + "unknown-error": "Erreur inconnue" + }, + "event": { + "alarm": "Alarme", + "body": "Corps", + "data": "Données", + "data-type": "Type de données", + "entity": "Entité", + "error": "erreur", + "errors-occurred": "Des erreurs sont survenues", + "event": "événement", + "event-time": "Heure de l'événement", + "event-type": "Type d'événement", + "failed": "Échec", + "message-id": "Message Id", + "message-type": "Type de message", + "messages-processed": "Messages traités", + "metadata": "Métadonnées", + "method": "Méthode", + "no-events-prompt": "Aucun événement trouvé", + "relation-type": "Type de relation", + "server": "Serveur", + "status": "État", + "success": "Succès", + "type": "Type", + "type-debug-rule-chain": "Debug", + "type-debug-rule-node": "Debug", + "type-error": "Erreur", + "type-lc-event": "Evénement du cycle de vie", + "type-stats": "Statistiques" + }, + "extension": { + "add": "Ajouter une extension", + "add-attribute": "Ajouter un attribut", + "add-attribute-request": "Ajouter une demande d'attribut", + "add-attribute-update": "Ajouter une mise à jour d'attribut", + "add-broker": "Ajouter un Broker", + "add-config": "Ajouter une configuration de convertisseur", + "add-connect-request": "Ajouter une demande de connexion", + "add-converter": "Ajouter un convertisseur", + "add-device": "Ajouter un dispositif", + "add-disconnect-request": "Ajouter une demande de déconnexion", + "add-map": "Ajouter un élément de mappage", + "add-server-side-rpc-request": "Ajouter une requête RPC côté serveur", + "add-timeseries": "Ajouter des timeseries", + "anonymous": "Anonyme", + "attr-json-key-expression": "Expression json de la clé d'attribut", + "attr-topic-key-expression": "Expression du topic de la clé d'attribut", + "attribute-filter": "Filtre d'attribut", + "attribute-key-expression": "Expression de clé d'attribut", + "attribute-requests": "Demandes d'attributs", + "attribute-updates": "Mises à jour des attributs", + "attributes": "Attributs", + "basic": "Basic", + "brokers": "Brokers", + "ca-cert": "Fichier de certificat CA", + "cert": "Fichier de certificat *", + "client-scope": "Portée client", + "configuration": "Configuration", + "connect-requests": "Demandes de connexion", + "converter-configurations": "Configurations du convertisseur", + "converter-id": "ID du convertisseur", + "converter-json": "Json", + "converter-json-parse": "Impossible d'analyser le convertisseur json.", + "converter-json-required": "Le convertisseur json est requis.", + "converter-type": "Type de convertisseur", + "converters": "Convertisseurs", + "credentials": "Informations d'identification", + "custom": "Sur mesure", + "delete": "Supprimer l'extension", + "delete-extension-text": "Attention, après la confirmation, l'extension et toutes les données associées deviendront irrécupérables.", + "delete-extension-title": "Êtes-vous sûr de vouloir supprimer l'extension '{{extensionId}}'?", + "delete-extensions-text": "Attention, après la confirmation, toutes les extensions sélectionnées seront supprimées.", + "delete-extensions-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 extension} other {# extensions} }?", + "device-name-expression": "expression du nom du dispositif", + "device-name-filter": "Filtre de nom de dispositif", + "device-type-expression": "expression de type de dispositif", + "disconnect-requests": "Demandes de déconnection", + "drop-file": "Déposez un fichier ou cliquez pour sélectionner un fichier à télécharger.", + "edit": "Modifier l'extension", + "export-extension": "Exporter l'extension", + "export-extensions-configuration": "Exporter la configuration des extensions", + "extension-id": "Id de l'extension", + "extension-type": "Type d'extension", + "extensions": "Extensions", + "field-required": "Le champ est obligatoire", + "file": "Fichier d'extensions", + "filter-expression": "Expression du filtre", + "host": "Hôte", + "id": "Id", + "import-extension": "Importer une extension", + "import-extensions": "Importer des extensions", + "import-extensions-configuration": "Importer la configuration des extensions", + "invalid-file-error": "Fichier d'extension non valide", + "json-name-expression": "Expression json du nom du dispositif", + "json-parse": "Impossible d'analyser json transformer.", + "json-required": "Transformer json est requis.", + "json-type-expression": "Expression json du type de dispositif", + "key": "Clé", + "mapping": "Mappage", + "method-filter": "Filtre de méthode", + "modbus-add-server": "Ajouter serveur/esclave", + "modbus-add-server-prompt": "Veuillez ajouter serveur/esclave", + "modbus-attributes-poll-period": "Période d'interrogation des attributs (ms)", + "modbus-baudrate": "Débit en bauds", + "modbus-byte-order": "Ordre des octets", + "modbus-databits": "Bits de données", + "modbus-databits-range": "Les bits de données doivent être compris entre 7 et 8.", + "modbus-device-name": "Nom du dispositif", + "modbus-encoding": "Encodage", + "modbus-function": "Fonction", + "modbus-parity": "parité", + "modbus-poll-period": "Période d'interrogation (ms)", + "modbus-poll-period-range": "La période d'interrogation doit être une valeur positive.", + "modbus-port-name": "Nom du port série", + "modbus-register-address": "Adresse du registre", + "modbus-register-address-range": "L'adresse du registre doit être comprise entre 0 et 65535.", + "modbus-register-bit-index": "Bit index", + "modbus-register-bit-index-range": "L'index de bit doit être compris entre 0 et 15.", + "modbus-register-count": "Nombre de registre", + "modbus-register-count-range": "Le nombre de registres doit être une valeur positive.", + "modbus-server": "Serveurs / esclaves", + "modbus-stopbits": "Bits d'arrêt", + "modbus-stopbits-range": "Les bits d'arrêt doivent être compris entre 1 et 2.", + "modbus-tag": "Tag", + "modbus-timeseries-poll-period": "Période d'interrogation des Timeseries (ms)", + "modbus-transport": "Transport", + "modbus-unit-id": "Id de l'unité", + "modbus-unit-id-range": "L'ID de l'unité doit être compris entre 1 et 247.", + "no-file": "Aucun fichier sélectionné.", + "opc-add-server": "Ajouter un serveur", + "opc-add-server-prompt": "Veuillez ajouter un serveur", + "opc-application-name": "Nom de l'application", + "opc-application-uri": "Uri de l'application", + "opc-device-name-pattern": "modèle de nom du dispositif", + "opc-device-node-pattern": "modèle de noeud de dispositif", + "opc-identity": "Identité", + "opc-keystore": "Magasin de clés", + "opc-keystore-alias": "Alias", + "opc-keystore-key-password": "Mot de passe de la clé", + "opc-keystore-location": "Emplacement *", + "opc-keystore-password": "Mot de passe", + "opc-keystore-type": "Type", + "opc-scan-period-in-seconds": "Période d'analyse en secondes", + "opc-security": "Sécurité", + "opc-server": "Serveurs", + "opc-type": "Type", + "password": "Mot de passe", + "pem": "PEM", + "port": "Port", + "port-range": "Le port doit être compris entre 1 et 65535.", + "private-key": "Fichier de clé privée *", + "request-id-expression": "Expression de demande d'id", + "request-id-json-expression": "Expression json de la demande d'id", + "request-id-topic-expression": "Expression de la demande d'id du topic", + "request-topic-expression": "Expression de la demande du topic", + "response-timeout": "Délai de réponse en millisecondes", + "response-topic-expression": "Expression du topic de la réponse", + "retry-interval": "Intervalle de nouvelle tentative en millisecondes", + "selected-extensions": "{count, plural, 1 {1 extension} other {# extensions} } sélectionné", + "server-side-rpc": "RPC côté serveur", + "ssl": "Ssl", + "sync": { + "last-sync-time": "Dernière heure de synchronisation", + "not-available": "Non disponible", + "not-sync": "Non sync", + "status": "Status", + "sync": "Sync" + }, + "timeout": "Délai d'attente en millisecondes", + "timeseries": "Timeseries", + "to-double": "Au double", + "token": "Jeton de sécurité", + "topic": "Topic", + "topic-expression": "Expression du topic", + "topic-filter": "Filtre du topic", + "topic-name-expression": "Expression du nom du dispositif (topic)", + "topic-type-expression": "Expression de type de dispositif (topic)", + "transformer": "Transformer", + "transformer-json": "JSON *", + "type": "Type", + "unique-id-required": "L'identifiant d'extension actuel existe déjà.", + "username": "Nom d'utilisateur", + "value": "Valeur", + "value-expression": "Expression de la valeur" + }, + "fullscreen": { + "exit": "Quitter le plein écran", + "expand": "Afficher en plein écran", + "fullscreen": "Plein écran", + "toggle": "Activer le mode plein écran" + }, + "function": { + "function": "Fonction" + }, + "grid": { + "add-item-text": "Ajouter un nouvel élément", + "delete-item": "Supprimer l'élément", + "delete-item-text": "Faites attention, après la confirmation, cet élément et toutes les données associées deviendront irrécupérables.", + "delete-item-title": "Êtes-vous sûr de vouloir supprimer cet élément?", + "delete-items": "Supprimer les éléments", + "delete-items-action-title": "Supprimer {count, plural, 1 {1 élément} other {# éléments} }", + "delete-items-text": "Attention, après la confirmation, tous les éléments sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", + "delete-items-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 élément} other {# éléments} }?", + "item-details": "Détails de l'élément", + "no-items-text": "Aucun élément trouvé", + "scroll-to-top": "Défiler vers le haut" + }, + "help": { + "goto-help-page": "Aller à la page d'aide" + }, + "home": { + "avatar": "Avatar", + "home": "Accueil", + "logout": "Déconnexion", + "menu": "Menu", + "open-user-menu": "Ouvrir le menu utilisateur", + "profile": "Profile" + }, + "icon": { + "icon": "Icône", + "material-icons": "Icônes matérielles", + "select-icon": "Sélectionner l'icône", + "show-all": "Afficher toutes les icônes" + }, + "import": { + "drop-file": "Déposez un fichier JSON ou cliquez pour sélectionner un fichier à télécharger.", + "no-file": "Aucun fichier sélectionné" + }, + "item": { + "selected": "Sélectionné" + }, + "js-func": { + "no-return-error": "La fonction doit renvoyer une valeur!", + "return-type-mismatch": "La fonction doit renvoyer une valeur de type '{{type}}' !", + "tidy": "Nettoyer" + }, + "key-val": { + "add-entry": "Ajouter une entrée", + "key": "Clé", + "no-data": "Aucune entrée", + "remove-entry": "Supprimer l'entrée", + "value": "Valeur" + }, + "language": { + "language": "Language" + }, + "layout": { + "color": "Couleur", + "layout": "Mise en page", + "main": "Principal", + "manage": "Gérer les mises en page", + "right": "Droite", + "select": "Sélectionner la mise en page cible", + "settings": "Paramètres de mise en page" + }, + "legend": { + "avg": "moy", + "max": "max", + "min": "min", + "position": "Position de la légende", + "settings": "Paramètres de la légende", + "show-avg": "Afficher la valeur moyenne", + "show-max": "Afficher la valeur maximale", + "show-min": "Afficher la valeur min", + "show-total": "Afficher la valeur totale", + "total": "total" + }, + "login": { + "create-password": "Créer un mot de passe", + "email": "Email", + "forgot-password": "Mot de passe oublié?", + "login": "Connexion", + "new-password": "Nouveau mot de passe", + "new-password-again": "nouveau mot de passe", + "password-again": "Mot de passe à nouveau", + "password-link-sent-message": "Le lien de réinitialisation du mot de passe a été envoyé avec succès!", + "password-reset": "Mot de passe réinitialisé", + "passwords-mismatch-error": "Les mots de passe saisis doivent être identiques!", + "remember-me": "Se souvenir de moi", + "request-password-reset": "Demander la réinitialisation du mot de passe", + "reset-password": "Réinitialiser le mot de passe", + "sign-in": "Veuillez vous connecter", + "username": "Nom d'utilisateur (courriel)", + "login-with": "Se connecter avec {{name}}", + "or": "ou" + }, + "position": { + "bottom": "Bas", + "left": "Gauche", + "right": "Droite", + "top": "Haut" + }, + "profile": { + "change-password": "Modifier le mot de passe", + "current-password": "Mot de passe actuel", + "last-login-time": "Dernière connexion", + "profile": "Profile" + }, + "relation": { + "add": "Ajouter une relation", + "add-relation-filter": "Ajouter un filtre de relation", + "additional-info": "Informations supplémentaires (JSON)", + "any-relation": "toute relation", + "any-relation-type": "N'importe quel type", + "delete": "Supprimer la relation", + "delete-from-relation-text": "Attention, après la confirmation, l'entité actuelle ne sera pas liée à l'entité '{{entityName}}'.", + "delete-from-relation-title": "Êtes-vous sûr de vouloir supprimer la relation de l'entité '{{entityName}}'?", + "delete-from-relations-text": "Attention, après la confirmation, toutes les relations sélectionnées seront supprimées et l'entité actuelle ne sera pas liée aux entités correspondantes.", + "delete-from-relations-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 relation} other {# relations} }?", + "delete-to-relation-text": "Attention, après la confirmation, l'entité '{{entityName}} ne sera plus liée à l'entité actuelle.", + "delete-to-relation-title": "Êtes-vous sûr de vouloir supprimer la relation avec l'entité '{{entityName}}'?", + "delete-to-relations-text": "Attention, après la confirmation, toutes les relations sélectionnées seront supprimées et les entités correspondantes ne seront pas liées à l'entité en cours.", + "delete-to-relations-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 relation} other {# relations} }?", + "direction": "Sens", + "direction-type": { + "FROM": "de", + "TO": "à" + }, + "edit": "Modifier la relation", + "from-entity": "De l'entité", + "from-entity-name": "Du nom d'entité", + "from-entity-type": "Du type d'entité", + "from-relations": "Relations sortantes", + "invalid-additional-info": "Impossible d'analyser les informations supplémentaires json.", + "relation-filters": "Filtres de relation", + "relation-type": "Type de relation", + "relation-type-required": "Le type de relation est requis.", + "relations": "Relations", + "remove-relation-filter": "Supprimer le filtre de relation", + "search-direction": { + "FROM": "De", + "TO": "Vers" + }, + "selected-relations": "{count, plural, 1 {1 relation} other {# relations} } sélectionné", + "to-entity": "Vers l'entité", + "to-entity-name": "vers le nom de l'entité", + "to-entity-type": "Vers le type d'entité", + "to-relations": "Relations entrantes", + "type": "Type" + }, + "rulechain": { + "add": "Ajouter une chaîne de règles", + "add-rulechain-text": "Ajouter une nouvelle chaîne de règles", + "copyId": "Copier l'identifiant de la chaîne de règles", + "create-new-rulechain": "Créer une nouvelle chaîne de règles", + "debug-mode": "Mode de débogage", + "delete": "Supprimer la chaîne de règles", + "delete-rulechain-text": "Attention, après la confirmation, la chaîne de règles et toutes les données associées deviendront irrécupérables.", + "delete-rulechain-title": "Voulez-vous vraiment supprimer la chaîne de règles '{{ruleChainName}}'?", + "delete-rulechains-action-title": "Supprimer {count, plural, 1 {1 chaîne de règles} other {# chaînes de règles} }", + "delete-rulechains-text": "Attention, après la confirmation, toutes les chaînes de règles sélectionnées seront supprimées et toutes les données associées deviendront irrécupérables.", + "delete-rulechains-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 chaîne de règles} other {# chaînes de règles} }?", + "description": "Description", + "details": "Détails", + "events": "Evénements", + "export": "Exporter la chaîne de règles", + "export-failed-error": "Impossible d'exporter la chaîne de règles: {{error}}", + "idCopiedMessage": "L'ID de la chaîne de règles a été copié dans le presse-papier", + "import": "Importer la chaîne de règles", + "invalid-rulechain-file-error": "Impossible d'importer la chaîne de règles: structure de données de la chaîne de règles non valide", + "management": "Gestion des règles", + "name": "Nom", + "name-required": "Le nom est requis.", + "no-rulechains-matching": "Aucune chaîne de règles correspondant à {{entity}} n'a été trouvée.", + "no-rulechains-text": "Aucune chaîne de règles trouvée", + "root": "Racine", + "rulechain": "Chaîne de règles", + "rulechain-details": "Détails de la chaîne de règles", + "rulechain-file": "Fichier de chaîne de règles", + "rulechain-required": "Chaîne de règles requise", + "rulechains": "Chaînes de règles", + "select-rulechain": "Sélectionner la chaîne de règles", + "set-root": "Rend la chaîne de règles racine (root) ", + "set-root-rulechain-text": "Après la confirmation, la chaîne de règles deviendra racine (root) et gérera tous les messages de transport entrants.", + "set-root-rulechain-title": "Voulez-vous vraiment que la chaîne de règles '{{ruleChainName}} soit racine (root) ?", + "system": "Système" + }, + "rulenode": { + "add": "Ajouter un noeud de règle", + "add-link": "Ajouter un lien", + "configuration": "Configuration", + "copy-selected": "Copier les éléments sélectionnés", + "create-new-link-label": "Créez un nouveau!", + "custom-link-label": "Etiquette de lien personnalisée", + "custom-link-label-required": "Une étiquette de lien personnalisée est requise", + "debug-mode": "Mode de débogage", + "delete": "Supprimer le noeud de règle", + "delete-selected": "Supprimer les éléments sélectionnés", + "delete-selected-objects": "Supprimer les nœuds et les connexions sélectionnés", + "description": "Description", + "deselect-all": "Désélectionner tout", + "deselect-all-objects": "Désélectionnez tous les nœuds et toutes les connexions", + "details": "Détails", + "directive-is-not-loaded": "La directive de configuration définie '{{directiveName}} n'est pas disponible.", + "events": "Événements", + "help": "Aide", + "invalid-target-rulechain": "Impossible de résoudre la chaîne de règles cible!", + "link": "Lien", + "link-details": "Détails du lien du noeud de la règle", + "link-label": "Étiquette du lien", + "link-label-required": "L'étiquette du lien est obligatoire", + "link-labels": "Étiquettes de lien", + "link-labels-required": "Les étiquettes de lien sont obligatoires", + "message": "Message", + "message-type": "Type de message", + "message-type-required": "Le type de message est obligatoire", + "metadata": "Métadonnées", + "metadata-required": "Les entrées de métadonnées ne peuvent pas être vides.", + "name": "Nom", + "name-required": "Le nom est requis.", + "no-link-label-matching": "'{{label}}' introuvable.", + "no-link-labels-found": "Aucune étiquette de lien trouvée", + "open-node-library": "Ouvrir la bibliothèque de noeud", + "output": "Output", + "rulenode-details": "Détails du noeud de la régle", + "search": "Recherche de noeuds", + "select-all": "Tout sélectionner", + "select-all-objects": "Sélectionnez tous les noeuds et connexions", + "select-message-type": "Sélectionner le type de message", + "test": "Test", + "test-script-function": "Tester le script", + "type": "Type", + "type-action": "Action", + "type-action-details": "Effectuer une action spéciale", + "type-enrichment": "Enrichissement", + "type-enrichment-details": "Ajouter des informations supplémentaires dans les métadonnées de message", + "type-external": "Externe", + "type-external-details": "Interagit avec le systéme externe", + "type-filter": "Filtre", + "type-filter-details": "Filtrer les messages entrants avec des conditions configurées", + "type-input": "Input", + "type-input-details": "Entrée logique de la chaîne de règles, transmet les messages entrants au prochain nœud de règle associé", + "type-rule-chain": "Chaîne de régles", + "type-rule-chain-details": "Transmet les messages entrants à la chaîne de régles spécifiée", + "type-transformation": "Transformation", + "type-transformation-details": "Modifier le payload du message et les métadonnées ", + "type-unknown": "Inconnu", + "type-unknown-details": "Noeud de règle non résolu", + "ui-resources-load-error": "Impossible de charger les ressources de configuration de l'interface utilisateur." + }, + "tenant": { + "add": "Ajouter un Tenant", + "add-tenant-text": "Ajouter un nouveau Tenant", + "admins": "Admins", + "copyId": "Copier l'Id du Tenant", + "delete": "Supprimer le Tenant", + "delete-tenant-text": "Attention, après la confirmation, le Tenant et toutes les données associées deviendront irrécupérables.", + "delete-tenant-title": "Êtes-vous sûr de vouloir supprimer le tenant '{{tenantTitle}}'?", + "delete-tenants-action-title": "Supprimer {count, plural, 1 {1 tenant} other {# tenants} }", + "delete-tenants-text": "Attention, après la confirmation, tous les Tenants sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", + "delete-tenants-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 tenant} other {# tenants} }?", + "description": "Description", + "details": "Détails", + "events": "Événements", + "idCopiedMessage": "L'Id du Tenant a été copié dans le Presse-papiers", + "manage-tenant-admins": "Gérer les administrateurs du Tenant", + "management": "Gestion des Tenants", + "no-tenants-matching": "Aucun Tenant correspondant à {{entity}} n'a été trouvé. ", + "no-tenants-text": "Aucun Tenant trouvé", + "select-tenant": "Sélectionner un Tenant", + "tenant": "Tenant", + "tenant-details": "Détails du Tenant", + "tenant-required": "Tenant requis", + "tenants": "Tenants", + "title": "Titre", + "title-required": "Le titre est requis." + }, + "timeinterval": { + "advanced": "Avancé", + "days": "Jours", + "days-interval": "{days, plural, 1 {1 jour} other {# jours} }", + "hours": "Heures", + "hours-interval": "{hours, plural, 1 {1 heure} other {# heures} }", + "minutes": "Minutes", + "minutes-interval": "{minutes, plural, 1 {1 minute} other {# minutes} }", + "seconds": "Secondes", + "seconds-interval": "{seconds, plural, 1 {1 seconde} other {# secondes} }" + }, + "timewindow": { + "date-range": "Plage de dates", + "days": "{days, plural, 1 {jour} other {# jours} }", + "edit": "Modifier timewindow", + "history": "Historique", + "hours": "{hours, plural, 0 {heure} 1 {1 heure} other {# heures} }", + "last": "Dernier", + "last-prefix": "dernier", + "minutes": "{minutes, plural, 0 {minute} 1 {1 minute} other {# minutes} }", + "period": "de {{startTime}} à {{endTime}}", + "realtime": "Temps réel", + "seconds": "{seconds, plural, 0 {second} 1 {1 second} other {# seconds} }", + "time-period": "Période", + "hide": "Masquer" + }, + "user": { + "activation-email-sent-message": "Le courriel d'activation a été envoyé avec succès!", + "activation-link": "Lien d'activation utilisateur", + "activation-link-copied-message": "le lien d'activation de l'utilisateur a été copié dans le presse-papier", + "activation-link-text": "Pour activer l'utilisateur, utilisez le lien d'activation suivant: ", + "activation-method": "Méthode d'activation", + "add": "Ajouter un utilisateur", + "add-user-text": "Ajouter un nouvel utilisateur", + "always-fullscreen": "Toujours en plein écran", + "anonymous": "Anonyme", + "copy-activation-link": "Copier le lien d'activation", + "customer": "Client", + "customer-users": "Utilisateurs du client", + "default-dashboard": "Tableau de bord par défaut", + "delete": "Supprimer l'utilisateur", + "delete-user-text": "Attention, après la confirmation, l'utilisateur et toutes les données associées deviendront irrécupérables.", + "delete-user-title": "Êtes-vous sûr de vouloir supprimer l'utilisateur '{{userEmail}}'?", + "delete-users-action-title": "Supprimer {count, plural, 1 {1 utilisateur} other {# utilisateurs} }", + "delete-users-text": "Attention, après la confirmation, tous les utilisateurs sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", + "delete-users-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 utilisateur} other {# utilisateurs} }?", + "description": "Description", + "details": "Détails", + "disable-account": "Désactiver le compte d'utilisateur", + "disable-account-message": "Le compte d'utilisateur a été désactivé avec succès!", + "display-activation-link": "Afficher le lien d'activation", + "email": "Email", + "email-required": "Email est requis.", + "enable-account": "Activer le compte d'utilisateur", + "enable-account-message": "Le compte d'utilisateur a été activé avec succès!", + "first-name": "Prénom", + "invalid-email-format": "Format de courrier électronique non valide", + "last-name": "Nom de famille", + "login-as-customer-user": "Se connecter en tant qu'utilisateur client", + "login-as-tenant-admin": "Connectez-vous en tant qu'administrateur Tenant", + "no-users-matching": "Aucun utilisateur correspondant à '{{entity}}' n'a été trouvé.", + "no-users-text": "Aucun utilisateur trouvé", + "resend-activation": "Renvoyer l'activation", + "select-user": "Sélectionner l'utilisateur", + "send-activation-mail": "Envoyer un mail d'activation", + "sys-admin": "Administrateur du système", + "tenant-admin": "Administrateur du Tenant", + "tenant-admins": "administrateurs du Tenant", + "user": "utilisateur", + "user-details": "Détails de l'utilisateur", + "user-required": "L'utilisateur est requis", + "users": "Utilisateurs" + }, + "value": { + "boolean": "booléen", + "boolean-value": "Valeur booléenne", + "double": "Double", + "double-value": "Valeur double", + "false": "Faux", + "integer": "Entier", + "integer-value": "Valeur entière", + "invalid-integer-value": "Valeur entière invalide", + "long": "Long", + "string": "String", + "string-value": "Valeur String", + "true": "Vrai", + "type": "Type de valeur" + }, + "widget": { + "add": "Ajouter un widget", + "add-resource": "Ajouter une ressource", + "add-widget-type": "Ajouter un nouveau type de widget", + "alarm": "Widget d'alarme", + "css": "CSS", + "datakey-settings-schema": "Schéma des paramètres de Data key", + "edit": "Modifier le widget", + "editor": " Editeur de widget", + "export": "Exporter widget", + "html": "HTML", + "javascript": "Javascript", + "latest-values": "Dernières valeurs", + "management": "Gestion des widgets", + "missing-widget-title-error": "Le titre du widget doit être spécifié!", + "no-data-found": "Aucune donnée trouvée", + "remove": "Supprimer le widget", + "remove-resource": "Supprimer une ressource", + "remove-widget-text": "Après la confirmation, le widget et toutes les données associées deviendront irrécupérables.", + "remove-widget-title": "Êtes-vous sûr de vouloir supprimer le widget '{{widgetTitle}}'?", + "remove-widget-type": "Supprimer le type de widget", + "remove-widget-type-text": "Après la confirmation, le type de widget et toutes les données associées deviendront irrécupérables.", + "remove-widget-type-title": "Êtes-vous sûr de vouloir supprimer le type de widget '{{widgetName}}'?", + "resource-url": "URL JavaScript / CSS", + "resources": "Ressources", + "rpc": "Widget de contrôle", + "run": "Exécuter un widget", + "save": "Enregistrer le widget", + "save-widget-type-as": "Enregistrer le type de widget sous", + "save-widget-type-as-text": "Veuillez saisir un nouveau titre de widget et / ou sélectionner un ensemble de widgets cibles", + "saveAs": "Enregistrer le widget sous", + "search-data": "Rechercher des données", + "select-widget-type": "Sélectionnez le type de widget", + "select-widgets-bundle": "Sélectionner un ensemble de widgets", + "settings-schema": "Schéma des paramétres", + "static": "Widget statique", + "tidy": "Nettoyer", + "timeseries": "Séries chronologiques", + "title": "Titre du widget", + "title-required": "Le titre du widget est requis.", + "toggle-fullscreen": "Basculer le mode plein écran", + "type": "Type de widget", + "unable-to-save-widget-error": "Impossible de sauvegarder le widget! Le widget a des erreurs!", + "undo": "Annuler les modifications du widget", + "widget-bundle": "Ensemble de widget", + "widget-library": "Bibliothèque de widgets", + "widget-saved": "Widget enregistré", + "widget-template-load-failed-error": "Impossible de charger le modéle de widget!", + "widget-type-load-error": "Le widget n'a pas été chargé à cause des erreurs suivantes:", + "widget-type-load-failed-error": "Impossible de charger le type de widget!", + "widget-type-not-found": "Problème de chargement de la configuration du widget.
    Le type de widget associé a probablement été supprimé." + }, + "widget-action": { + "custom": "Action personnalisée", + "header-button": "Bouton d'en-tête de widget", + "open-dashboard": "Naviguer vers un autre tableau de bord", + "open-dashboard-state": "Naviguer vers un nouvel état du tableau de bord", + "open-right-layout": "Ouvrir la disposition du tableau de bord droite (vue mobile)", + "set-entity-from-widget": "Définir l'entité à partir du widget", + "target-dashboard": "Tableau de bord cible", + "target-dashboard-state": "État du tableau de bord cible", + "target-dashboard-state-required": "L'état du tableau de bord cible est requis", + "update-dashboard-state": "Mettre à jour l'état actuel du tableau de bord" + }, + "widget-config": { + "action": "Action", + "action-icon": "Icône", + "action-name": "Nom", + "action-name-not-unique": "Une autre action portant le même nom existe déjà.
    Le nom de l'action doit être unique dans la même source d'action.", + "action-name-required": "Le nom de l'action est requis", + "action-source": "Source de l'action", + "action-source-required": "Une source d'action est requise.", + "action-type": "Type", + "action-type-required": "Le type d'action est requis.", + "actions": "Actions", + "add-action": "Ajouter une action", + "add-datasource": "Ajouter une source de données", + "advanced": "Avancé", + "alarm-source": "Source d'alarme", + "background-color": "couleur de fond", + "data": "Données", + "datasource-parameters": "Paramètres", + "datasource-type": "Type", + "datasources": "Sources de données", + "decimals": "Nombre de chiffres après virgule flottante", + "delete-action": "Supprimer l'action", + "delete-action-text": "Êtes-vous sûr de vouloir supprimer l'action du widget nommé '{{actionName}}'?", + "delete-action-title": "Supprimer l'action du widget", + "display-timewindow": "Afficher fenêtre de temps", + "display-legend": "Afficher la légende", + "display-title": "Afficher le titre", + "drop-shadow": "Ombre portée", + "edit-action": "Modifier l'action", + "enable-fullscreen": "Activer le plein écran", + "general-settings": "Paramètres généraux", + "height": "Hauteur", + "margin": "Marge", + "maximum-datasources": "Maximum {count, plural, 1 {1 datasource est autorisé.} other {# datasources sont autorisés} }", + "mobile-mode-settings": "Paramètres du mode mobile", + "order": "Ordre", + "padding": "Padding", + "remove-datasource": "Supprimer la source de données", + "search-actions": "Recherche d'actions", + "settings": "Paramètres", + "target-device": "Dispositif cible", + "text-color": "Couleur du texte", + "timewindow": "Fenêtre de temps", + "title": "Titre", + "title-style": "Style de titre", + "title-tooltip": "Tooltip de titre", + "units": "Symbole spécial à afficher à côté de la valeur", + "use-dashboard-timewindow": "Utiliser la fenêtre de temps du tableau de bord", + "widget-style": "Style du widget", + "display-icon": "Afficher l'icône du titre", + "icon-color": "Couleur de l'icône", + "icon-size": "Taille de l'icône" + }, + "widget-type": { + "create-new-widget-type": "Créer un nouveau type de widget", + "export": "Exporter le type de widget", + "export-failed-error": "Impossible d'exporter le type de widget: {{error}}", + "import": "Importer le type de widget", + "invalid-widget-type-file-error": "Impossible d'importer le type de widget: structure de données de type widget invalide.", + "widget-type-file": "Fichier de type Widget" + }, + "widgets": { + "date-range-navigator": { + "localizationMap": { + "Sun": "Dim.", + "Mon": "Lun.", + "Tue": "Mar.", + "Wed": "Mer.", + "Thu": "Jeu.", + "Fri": "Ven.", + "Sat": "Sam.", + "Jan": "Janv.", + "Feb": "Févr.", + "Mar": "Mars", + "Apr": "Avr.", + "May": "Mai", + "Jun": "Juin", + "Jul": "Juil.", + "Aug": "Août", + "Sep": "Sept.", + "Oct": "Oct.", + "Nov": "Nov.", + "Dec": "Déc.", + "January": "Janvier", + "February": "Février", + "March": "Mars", + "April": "Avril", + "June": "Juin", + "July": "Juillet", + "August": "Août", + "September": "Septembre", + "October": "Octobre", + "November": "Novembre", + "December": "Décembre", + "Custom Date Range": "Plage de dates personnalisée", + "Date Range Template": "Modèle de plage de dates", + "Today": "Aujourd'hui", + "Yesterday": "Hier", + "This Week": "Cette semaine", + "Last Week": "La semaine dernière", + "This Month": "Ce mois-ci", + "Last Month": "Le mois dernier", + "Year": "Année", + "This Year": "Cette année", + "Last Year": "L'année dernière", + "Date picker": "Sélecteur de date", + "Hour": "Heure", + "Day": "Journée", + "Week": "La semaine", + "2 weeks": "2 Semaines", + "Month": "Mois", + "3 months": "3 Mois", + "6 months": "6 Mois", + "Custom interval": "Intervalle personnalisé", + "Interval": "Intervalle", + "Step size": "Taille de pas", + "Ok": "Ok" + } + }, + "input-widgets": { + "attribute-not-allowed": "Le paramètre d'attribut ne peut pas être utilisé dans ce widget", + "date": "Date", + "discard-changes": "Annuler les modifications", + "entity-attribute-required": "L'attribut d'entité est requis", + "entity-timeseries-required": "Entité timeseries est requis", + "not-allowed-entity": "L'entité sélectionnée ne peut pas avoir d'attributs partagés", + "no-attribute-selected": "Aucun attribut n'est sélectionné", + "no-datakey-selected": "Aucune date n'est sélectionnée", + "no-entity-selected": "Aucune entité sélectionnée", + "no-image": "Pas d'image", + "no-support-web-camera": "Pas de webcam supportée", + "no-timeseries-selected": "Aucune série temporelle sélectionnée", + "switch-attribute-value": "Changer la valeur de l'attribut d'entité", + "switch-camera": "Changer de caméra", + "switch-timeseries-value": "Changer la valeur de l'entité série temporelle", + "take-photo": "Prendre une photo", + "time": "Temps", + "timeseries-not-allowed": "Le paramètre série temporelle ne peut pas être utilisé dans ce widget", + "update-failed": "Mise à jour a échoué", + "update-successful": "Mise à jour réussie", + "update-attribute": "Attribut de mise à jour", + "update-timeseries": "Mise à jour de la série temporelle", + "value": "Valeur" + } + }, + "widgets-bundle": { + "add": "Ajouter un groupe de widgets", + "add-widgets-bundle-text": "Ajouter un nouveau groupe de widgets", + "create-new-widgets-bundle": "Créer un nouveau groupe de widgets", + "current": "Groupe actuel", + "delete": "Supprimer le groupe de widgets", + "delete-widgets-bundle-text": "Attention, après la confirmation, le groupe de widgets et toutes les données associées deviendront irrécupérables.", + "delete-widgets-bundle-title": "Êtes-vous sûr de vouloir supprimer le groupe de widgets '{{widgetsBundleTitle}}'?", + "delete-widgets-bundles-action-title": "Supprimer {count, plural, 1 {1 groupe de widgets} other {# groupes de widgets} }", + "delete-widgets-bundles-text": "Attention, après la confirmation, tous les groupes de widgets sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", + "delete-widgets-bundles-title": "Voulez-vous vraiment supprimer {count, plural, 1 {1 groupe de widgets} other {# groupes de widgets} }?", + "details": "Détails", + "empty": "Le groupe de widgets est vide", + "export": "Exporter le groupe de widgets", + "export-failed-error": "Impossible d'exporter le groupe de widgets: {{error}}", + "import": "Importer un groupe de widgets", + "invalid-widgets-bundle-file-error": "Impossible d'importer un groupe de widgets: structure de données du groupe de widgets non valides.", + "no-widgets-bundles-matching": "Aucun groupe de widgets correspondant à {{widgetsBundle}} n'a été trouvé.", + "no-widgets-bundles-text": "Aucun groupe de widgets trouvé", + "system": "Système", + "title": "Titre", + "title-required": "Le titre est requis.", + "widgets-bundle-details": "Détails des groupes de widgets", + "widgets-bundle-file": "Fichier de groupe de widgets", + "widgets-bundle-required": "Un groupe de widgets est requis.", + "widgets-bundles": "Groupes de widgets" + } +} diff --git a/ui-ngx/src/assets/locale/locale.constant-it_IT.json b/ui-ngx/src/assets/locale/locale.constant-it_IT.json new file mode 100644 index 0000000000..9313471fa5 --- /dev/null +++ b/ui-ngx/src/assets/locale/locale.constant-it_IT.json @@ -0,0 +1,1709 @@ +{ + "access": { + "unauthorized": "Non autorizzato", + "unauthorized-access": "Accesso non autorizzato", + "unauthorized-access-text": "Devi effettuare il login per accedere a questa risorsa!", + "access-forbidden": "Accesso Vietato", + "access-forbidden-text": "Non hai i diritti di accesso a questa posizione!
    Prova ad effettuare il login con un diverso account.", + "refresh-token-expired": "Sessione scaduta", + "refresh-token-failed": "Impossibile aggiornare la sessione" + }, + "action": { + "activate": "Attiva", + "suspend": "Sospendi", + "save": "Salva", + "saveAs": "Salva come", + "cancel": "Cancella", + "ok": "OK", + "delete": "Elimina", + "add": "Aggiungi", + "yes": "Sì", + "no": "No", + "update": "Aggiorna", + "remove": "Rimuovi", + "search": "Cerca", + "clear-search": "Cancella ricerca", + "assign": "Assegna", + "unassign": "Annulla assegnazione", + "share": "Condividi", + "make-private": "Rendi privato", + "apply": "Applica", + "apply-changes": "Applica modifiche", + "edit-mode": "Modalità modifica", + "enter-edit-mode": "Attiva modalità di modifica", + "decline-changes": "Annulla modifiche", + "close": "Chiudi", + "back": "Indietro", + "run": "Esegui", + "sign-in": "Registrati!", + "edit": "Modifica", + "view": "Visualizza", + "create": "Crea", + "drag": "Trascina", + "refresh": "Aggiorna", + "undo": "Annulla", + "copy": "Copia", + "paste": "Incolla", + "copy-reference": "Copia riferimento", + "paste-reference": "Incolla riferimento", + "import": "Importa", + "export": "Esporta", + "share-via": "Condividi con {{provider}}", + "discard-changes": "Annulla le modifiche" + }, + "aggregation": { + "aggregation": "Aggregazione", + "function": "Funzione di aggregazione dati", + "limit": "Valori max", + "group-interval": "Intervallo di raggruppamento", + "min": "Min", + "max": "Max", + "avg": "Media", + "sum": "Somma", + "count": "Conteggio", + "none": "Nessuna" + }, + "admin": { + "general": "Generale", + "general-settings": "Impostazioni Generali", + "outgoing-mail": "Posta in uscita", + "outgoing-mail-settings": "Impostazioni Posta in uscita", + "system-settings": "Impostazioni di sistema", + "test-mail-sent": "Mail di test inviata con successo!", + "base-url": "URL di base", + "base-url-required": "URL di base obbligatoria.", + "mail-from": "Mittente", + "mail-from-required": "Mittente obbligatorio.", + "smtp-protocol": "Protocollo SMTP", + "smtp-host": "Host SMTP", + "smtp-host-required": "Host SMTP obbligatorio.", + "smtp-port": "Porta SMTP", + "smtp-port-required": "Porta SMTP obbligatoria.", + "smtp-port-invalid": "Numero di porta SMTP non valido.", + "timeout-msec": "Timeout (msec)", + "timeout-required": "Timeout obbligatorio.", + "timeout-invalid": "Timeout non valido.", + "enable-tls": "Abilita TLS", + "tls-version" : "Versione TLS", + "send-test-mail": "Invia mail di test", + "security-settings": "Settaggi di sicurezza", + "password-policy": "Politica password", + "minimum-password-length": "Lunghezza minima password", + "minimum-password-length-required": "È richiesta una lunghezza minima della password", + "minimum-password-length-range": "La lunghezza minima della password deve essere compresa tra 5 e 50", + "minimum-uppercase-letters": "Numero minimo di lettere maiuscole", + "minimum-uppercase-letters-range": "Il numero minimo di lettere maiuscole non può essere negativo", + "minimum-lowercase-letters": "Numero minimo di lettere minuscole", + "minimum-lowercase-letters-range": "Il numero minimo di lettere minuscole non può essere negativo", + "minimum-digits": "Numero minimo di cifre", + "minimum-digits-range": "Il numero minimo di cifre non può essere negativo", + "minimum-special-characters": "Numero minimo di caratteri speciali", + "minimum-special-characters-range": "Il numero minimo di caratteri speciali non può essere negativo", + "password-expiration-period-days": "Periodo di scadenza della password in giorni", + "password-expiration-period-days-range": "Il periodo di scadenza della password in giorni non può essere negativo", + "password-reuse-frequency-days": "Frequenza di riutilizzo della password in giorni", + "password-reuse-frequency-days-range": "La frequenza di riutilizzo della password in giorni non può essere negativa", + "general-policy": "Politica generale", + "max-failed-login-attempts": "Numero massimo di tentativi di accesso non riusciti, prima che l'account sia bloccato", + "minimum-max-failed-login-attempts-range": "Il numero massimo di tentativi di accesso non riusciti non può essere negativo", + "user-lockout-notification-email": "In caso di blocco dell'account utente, inviare una notifica via e-mail" + }, + "alarm": { + "alarm": "Allarme", + "alarms": "Allarmi", + "select-alarm": "Seleziona un allarme", + "no-alarms-matching": "Nessun allarme corrispondente a '{{entity}}' è stato trovato.", + "alarm-required": "Allarme richiesto", + "alarm-status": "Stato Allarme", + "search-status": { + "ANY": "Qualsiasi", + "ACTIVE": "Attivo", + "CLEARED": "Cancellato", + "ACK": "Riconosciuto", + "UNACK": "Non riconosciuto" + }, + "display-status": { + "ACTIVE_UNACK": "Active Unacknowledged", + "ACTIVE_ACK": "Active Acknowledged", + "CLEARED_UNACK": "Cleared Unacknowledged", + "CLEARED_ACK": "Cleared Acknowledged" + }, + "no-alarms-prompt": "Nessun allarme trovato", + "created-time": "Orario di creazione", + "type": "Tipo", + "severity": "Livello di gravità", + "originator": "Origine", + "originator-type": "Tipo origine", + "details": "Dettagli", + "status": "Stato", + "alarm-details": "Dettagli allarme", + "start-time": "Ora inizio", + "end-time": "Ora fine", + "ack-time": "Ora conferma", + "clear-time": "Ora cancellazione", + "severity-critical": "Critico", + "severity-major": "Maggiore", + "severity-minor": "Minore", + "severity-warning": "Avviso", + "severity-indeterminate": "Indeterminato", + "acknowledge": "Conferma", + "clear": "Cancella", + "search": "Cerca allarmi", + "selected-alarms": "{ count, plural, 1 {1 allarme selezionato} other {# allarmi selezionati} }", + "no-data": "Nessun dato da visualizzare", + "polling-interval": "Intervallo di polling (sec) Allarmi", + "polling-interval-required": "Intervallo di polling Allarmi richiesto.", + "min-polling-interval-message": "L'intervallo di polling deve essere di almeno 1 sec.", + "aknowledge-alarms-title": "Conferma { count, plural, 1 {1 allarme} other {# allarmi} }", + "aknowledge-alarms-text": "Sei sicuro di voler confermare { count, plural, 1 {1 allarme} other {# allarmi} }?", + "aknowledge-alarm-title": "Conferma allarme", + "aknowledge-alarm-text": "Sei sicuro di voler confermare l'allarme?", + "clear-alarms-title": "Elimina { count, plural, 1 {1 allarme} other {# allarmi} }", + "clear-alarms-text": "Sei sicuro di voler eliminare { count, plural, 1 {1 allarme} other {# allarmi} }?", + "clear-alarm-title": "Elimina allarme", + "clear-alarm-text": "Sei sicuro di voler eliminare l'allarme?", + "alarm-status-filter": "Filtro stato allarme" + }, + "alias": { + "add": "Aggiungi alias", + "edit": "Modifica alias", + "name": "Nome Alias", + "name-required": "Nome Alias obbligatorio", + "duplicate-alias": "Un Alias con lo stesso nome è già presente.", + "filter-type-single-entity": "Singola entità", + "filter-type-entity-list": "Lista Entità", + "filter-type-entity-name": "Nome Entità", + "filter-type-state-entity": "Entità dallo stato della dashboard", + "filter-type-state-entity-description": "Entità prelevata dai parametri di stato della dashboard", + "filter-type-asset-type": "Tipo di Asset", + "filter-type-asset-type-description": "Asset di tipo '{{assetType}}'", + "filter-type-asset-type-and-name-description": "Asset di tipo '{{assetType}}' e con un nome che inizia per '{{prefix}}'", + "filter-type-device-type": "Tipo di dispositivo", + "filter-type-device-type-description": "Dispositivi di tipo '{{deviceType}}'", + "filter-type-device-type-and-name-description": "Dispositivi di tipo '{{deviceType}}' e con un nome che inizia per '{{prefix}}'", + "filter-type-entity-view-type": "Tipo vista entità", + "filter-type-entity-view-type-description": "Viste entità di tipo '{{entityView}}'", + "filter-type-entity-view-type-and-name-description": "Viste entità di tipo '{{entityView}}' e con un nome che inizia per '{{prefix}}'", + "filter-type-relations-query": "Query relazioni", + "filter-type-relations-query-description": "{{entities}} che hanno una relazione {{relationType}} {{direction}} {{rootEntity}}", + "filter-type-asset-search-query": "Query ricerca asset", + "filter-type-asset-search-query-description": "Asset di tipo {{assetTypes}} che hanno una relazione {{relationType}} {{direction}} {{rootEntity}}", + "filter-type-device-search-query": "Query ricerca dispositivo", + "filter-type-device-search-query-description": "Dispositivi di tipo {{deviceTypes}} che hanno una relazione {{relationType}} {{direction}} {{rootEntity}}", + "filter-type-entity-view-search-query": "Query ricerca Vista entità", + "filter-type-entity-view-search-query-description": "Viste entità di tipo {{entityViewTypes}} che hanno una relazione {{relationType}} {{direction}} {{rootEntity}}", + "entity-filter": "Filtro entità", + "resolve-multiple": "Risolvi come entità multiple", + "filter-type": "Tipo di filtro", + "filter-type-required": "Tipo di filtro richiesto.", + "entity-filter-no-entity-matched": "Nessuna entità corrispondente al filtro specificato è stata trovata.", + "no-entity-filter-specified": "Nessun filtro di entità specificato", + "root-state-entity": "Usa l'entità di stato della dashboard come radice", + "root-entity": "Entità radice", + "state-entity-parameter-name": "Nome parametro entità di stato", + "default-state-entity": "Entità di stato predefinita", + "default-entity-parameter-name": "Predefinito", + "max-relation-level": "Massimo livello relazione", + "unlimited-level": "Illimitato", + "state-entity": "Entità di stato della dashboard", + "all-entities": "Tutte le entità", + "any-relation": "qualsiasi" + }, + "asset": { + "asset": "Asset", + "assets": "Asset", + "management": "Gestione Asset", + "view-assets": "Visualizza Asset", + "add": "Aggiungi Asset", + "assign-to-customer": "Assegna a cliente", + "assign-asset-to-customer": "Assegna Asset al Cliente", + "assign-asset-to-customer-text": "Seleziona gli asset da assegnare al cliente", + "no-assets-text": "Nessun asset trovato", + "assign-to-customer-text": "Seleziona il cliente a cui assegnare l'asset / gli asset", + "public": "Pubblico", + "assignedToCustomer": "Assegnato al cliente", + "make-public": "Rendi pubblico l'asset", + "make-private": "Rendi privato l'asset", + "unassign-from-customer": "Assegnazione annullata dal cliente", + "delete": "Cancella asset", + "asset-public": "L'Asset è pubblico", + "asset-type": "Tipo di Asset", + "asset-type-required": "Tipo di Asset richiesto.", + "select-asset-type": "Seleziona tipo di asset", + "enter-asset-type": "Inserisci tipo di asset", + "any-asset": "Qualsiasi asset", + "no-asset-types-matching": "Nessun asset corrispondente al tipo '{{entitySubtype}}' è stato trovato.", + "asset-type-list-empty": "Nessun tipo di asset selezionato.", + "asset-types": "Tipi di Asset", + "name": "Nome", + "name-required": "Nome obbligatorio.", + "description": "Descrizione", + "type": "Tipo", + "type-required": "Tipo obbligatorio.", + "details": "Dettagli", + "events": "Eventi", + "add-asset-text": "Aggiungi un nuovo asset", + "asset-details": "Dettagli Asset", + "assign-assets": "Assegna asset", + "assign-assets-text": "Assegna { count, plural, 1 {1 asset} other {# asset} } al cliente", + "delete-assets": "Cancella asset", + "unassign-assets": "Annulla assegnazione asset", + "unassign-assets-action-title": "Annulla assegnazione { count, plural, 1 {1 asset} other {# asset} } al cliente", + "assign-new-asset": "Assegna un nuovo asset", + "delete-asset-title": "Sei sicuro di voler cancellare l'asset '{{assetName}}'?", + "delete-asset-text": "Attenzione, dopo la conferma l'asset e tutti i relativi dati non saranno più recuperabili.", + "delete-assets-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 asset} other {# asset} }?", + "delete-assets-action-title": "Elimina { count, plural, 1 {1 asset} other {# asset} }", + "delete-assets-text": "Attenzione, dopo la modifica tutti gli asset selezionati saranno rimossi e tutti i relativi dati non saranno più recuperabili.", + "make-public-asset-title": "Sei sicuro di voler rendere pubblico l'asset '{{assetName}}'?", + "make-public-asset-text": "Dopo la conferma l'asset e tutti i suoi dati saranno resi pubblici e accessibili dagli altri.", + "make-private-asset-title": "Sei sicuro di voler rendere privato l'asset '{{assetName}}'?", + "make-private-asset-text": "Dopo la conferma l'asset e tutti i suoi dati saranno resi privati e non accessibili dagli altri.", + "unassign-asset-title": "Sei sicuro di voler annullare l'assegnazione dell'asset '{{assetName}}'?", + "unassign-asset-text": "Dopo la conferma l'assegnazione dell'asset sarà annullata e l'asset non sarà più accessibile dal cliente.", + "unassign-asset": "Annulla assegnazione asset", + "unassign-assets-title": "Sei sicuro di voler annullare l'assegnazione di { count, plural, 1 {1 asset} other {# asset} }?", + "unassign-assets-text": "Dopo la conferma sarà annullata l'assegnazione di tutti gli asset selezionati e questi non saranno più accessibili dal cliente.", + "copyId": "Copia Id asset", + "idCopiedMessage": "Id Asset copiato negli Appunti", + "select-asset": "Seleziona asset", + "no-assets-matching": "Nessun asset corrispondente a '{{entity}}' è stato trovato.", + "asset-required": "Asset obbligatorio", + "name-starts-with": "Asset con nome che inizia per", + "label": "Etichetta" + }, + "attribute": { + "attributes": "Attributi", + "latest-telemetry": "Ultima telemetria", + "attributes-scope": "Visibilità attributi entità", + "scope-latest-telemetry": "Ultima telemetria", + "scope-client": "Attributi client", + "scope-server": "Attributi server", + "scope-shared": "Attributi condivisi", + "add": "Aggiungi attributo", + "key": "Chiave", + "last-update-time": "Ultimo aggiornamento", + "key-required": "Attributo chiave richiesto.", + "value": "Valore", + "value-required": "Attributo valore richiesto.", + "delete-attributes-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 attributo} other {# attributi} }?", + "delete-attributes-text": "Attenzione, dopo la conferma tutti gli attributi selezionati saranno rimossi.", + "delete-attributes": "Elimina attributi", + "enter-attribute-value": "Inserisci il valore dell'attributo", + "show-on-widget": "Mostra sul widget", + "widget-mode": "Modalità Widget", + "next-widget": "Widget successivo", + "prev-widget": "Widget precedente", + "add-to-dashboard": "Aggiungi alla dashboard", + "add-widget-to-dashboard": "Aggiungi widget alla dashboard", + "selected-attributes": "{ count, plural, 1 {1 attributo selezionato} other {# attributi selezionati} }", + "selected-telemetry": "{ count, plural, 1 {1 unità di telemetria selezionata} other {# unità di telemetria selezionate} }" + }, + "audit-log": { + "audit": "Audit", + "audit-logs": "Log Audit", + "timestamp": "Timestamp", + "entity-type": "Tipo Entità", + "entity-name": "Nome Entità", + "user": "Utente", + "type": "Tipo", + "status": "Stato", + "details": "Dettagli", + "type-added": "Aggiunto", + "type-deleted": "Eliminato", + "type-updated": "Aggiornato", + "type-attributes-updated": "Attributi aggiornati", + "type-attributes-deleted": "Attributi eliminati", + "type-rpc-call": "Chiamata RPC", + "type-credentials-updated": "Credenziali aggiornate", + "type-assigned-to-customer": "Assegnato al Cliente", + "type-unassigned-from-customer": "Assegnazione annullata dal Cliente", + "type-activated": "Attivato", + "type-suspended": "Sospeso", + "type-credentials-read": "Credenziali lette", + "type-attributes-read": "Attributi letti", + "type-relation-add-or-update": "Relazione aggiornata", + "type-relation-delete": "Relazione eliminata", + "type-relations-delete": "Eliminate tutte le relazioni", + "type-alarm-ack": "Confermato", + "type-alarm-clear": "Eliminato", + "type-login": "Accesso", + "type-logout": "Disconnettersi", + "type-lockout": "Bloccato", + "status-success": "Successo", + "status-failure": "Fallito", + "audit-log-details": "Dettaglio log audit", + "no-audit-logs-prompt": "Log non trovati", + "action-data": "Action data", + "failure-details": "Dettagli fallimento", + "search": "Cerca log audit", + "clear-search": "Cancella ricerca" + }, + "confirm-on-exit": { + "message": "Alcune modifiche non sono state salvate. Sei sicuro di voler abbandonare questa pagina?", + "html-message": "Alcune modifiche non sono state salvate.
    Sei sicuro di voler abbandonare questa pagina?", + "title": "Modifiche non salvate" + }, + "contact": { + "country": "Nazione", + "city": "Città", + "state": "Stato / Provincia", + "postal-code": "CAP", + "postal-code-invalid": "Formato CAP non valido.", + "address": "Indirizzo", + "address2": "Indirizzo 2", + "phone": "Telefono", + "email": "Email", + "no-address": "Nessun indirizzo" + }, + "common": { + "username": "Nome utente", + "password": "Password", + "enter-username": "Inserisci nome utente", + "enter-password": "Inserisci password", + "enter-search": "Cerca ...", + "created-time": "Ora di creazione" + }, + "content-type": { + "json": "Json", + "text": "Testo", + "binary": "Binario (Base64)" + }, + "customer": { + "customer": "Cliente", + "customers": "Clienti", + "management": "Gestione cliente", + "dashboard": "Dashboard cliente", + "dashboards": "Dashboard cliente", + "devices": "Dispositivi cliente", + "entity-views": "Viste entità cliente", + "assets": "Asset cliente", + "public-dashboards": "Dashboard pubbliche", + "public-devices": "Dispositivi pubblici", + "public-assets": "Asset pubblici", + "public-entity-views": "Viste entità pubbliche", + "add": "Aggiungi cliente", + "delete": "Elimina cliente", + "manage-customer-users": "Gestisci utenti cliente", + "manage-customer-devices": "Gestisci dispositivi cliente", + "manage-customer-dashboards": "Gestisci dashboard cliente", + "manage-public-devices": "Gestisci dispositivi pubblici", + "manage-public-dashboards": "Gestisci dashboard pubbliche", + "manage-customer-assets": "Gestisci asset cliente", + "manage-public-assets": "Gestisci asset pubblici", + "add-customer-text": "Aggiungi nuovo cliente", + "no-customers-text": "Nessun cliente trovato", + "customer-details": "Dettagli cliente", + "delete-customer-title": "Sei sicuro di voler eliminare il cliente '{{customerTitle}}'?", + "delete-customer-text": "Attenzione, dopo la conferma il cliente e tutti i suoi dati non saranno più recuperabili.", + "delete-customers-title": "Sei sicuro di voler cancellare { count, plural, 1 {1 cliente} other {# clienti} }?", + "delete-customers-action-title": "Elimina { count, plural, 1 {1 cliente} other {# clienti} }", + "delete-customers-text": "Attenzione, dopo la conferma tutti i clienti selezionati saranno rimossi e i loro dati non saranno più recuperabili.", + "manage-users": "Gestisci utenti", + "manage-assets": "Gestisci asset", + "manage-devices": "Gestisci dispositivi", + "manage-dashboards": "Gestisci dashboard", + "title": "Titolo", + "title-required": "Titolo obbligatorio.", + "description": "Descrizione", + "details": "Dettagli", + "events": "Eventi", + "copyId": "Copia Id cliente", + "idCopiedMessage": "Id cliente copiato negli appunti", + "select-customer": "Seleziona cliente", + "no-customers-matching": "Nessun cliente corrispondente a '{{entity}}' è stato trovato.", + "customer-required": "Cliente obbligatorio", + "select-default-customer": "Seleziona cliente di default", + "default-customer": "Cliente di default", + "default-customer-required": "Il cliente di default è obbligatorio per il debug della dashboard a livello di Tenant" + }, + "datetime": { + "date-from": "Data da", + "time-from": "Ora da", + "date-to": "Data a", + "time-to": "Ora a" + }, + "dashboard": { + "dashboard": "Dashboard", + "dashboards": "Dashboard", + "management": "Gestione Dashboard", + "view-dashboards": "Mostra Dashboard", + "add": "Aggiungi Dashboard", + "assign-dashboard-to-customer": "Assegna Dashboard al cliente", + "assign-dashboard-to-customer-text": "Seleziona le dashboard da assegnare al client", + "assign-to-customer-text": "Seleziona il cliente a cui assegnare la/le dashboard", + "assign-to-customer": "Assegna al cliente", + "unassign-from-customer": "Annulla assegnazione al cliente", + "make-public": "Rendi pubblica la dashboard", + "make-private": "Rendi privata la dashboard", + "manage-assigned-customers": "Gestisci clienti assegnati", + "assigned-customers": "Clienti assegnati", + "assign-to-customers": "Assegna Dashboard ai Clienti", + "assign-to-customers-text": "Seleziona i clienti da assegnare alla/alle dashboard", + "unassign-from-customers": "Annulla assegnazione Dashboard ai Clienti", + "unassign-from-customers-text": "Seleziona i clienti di cui annullare l'assegnazione alla/alle dashboard", + "no-dashboards-text": "Nessuna dashboard trovata", + "no-widgets": "Nessun widget configurato", + "add-widget": "Aggiungi nuovo widget", + "title": "Titolo", + "select-widget-title": "Seleziona widget", + "select-widget-subtitle": "Elenco tipi di widget disponibili", + "delete": "Elimina dashboard", + "title-required": "Titolo obbligatorio.", + "description": "Descrizione", + "details": "Dettagli", + "dashboard-details": "Dettagli dashboard", + "add-dashboard-text": "Aggiungi nuova dashboard", + "assign-dashboards": "Assegna dashboard", + "assign-new-dashboard": "Assegna nuova dashboard", + "assign-dashboards-text": "Assegna { count, plural, 1 {1 dashboard} other {# dashboard} } ai clienti", + "unassign-dashboards-action-text": "Annulla assegnazione { count, plural, 1 {1 dashboard} other {# dashboard} } ai clienti", + "delete-dashboards": "Elimina dashboard", + "unassign-dashboards": "Annulla assegnazione dashboard", + "unassign-dashboards-action-title": "Annulla assegnazione { count, plural, 1 {1 dashboard} other {# dashboard} } al cliente", + "delete-dashboard-title": "Sei sicuro di voler cancellare la dashboard '{{dashboardTitle}}'?", + "delete-dashboard-text": "Attenzione, dopo la conferma la dashboard e tutti i suoi dati non saranno più recuperabili.", + "delete-dashboards-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 dashboard} other {# dashboard} }?", + "delete-dashboards-action-title": "Cancella { count, plural, 1 {1 dashboard} other {# dashboard} }", + "delete-dashboards-text": "Attenzione, dopo la conferma tutte le dashboard selezionate saranno eliminate e tutti i loro dati non saranno più recuperabili.", + "unassign-dashboard-title": "Sei sicuro di voler annullare l'assegnazione della dashboard '{{dashboardTitle}}'?", + "unassign-dashboard-text": "Dopo la conferma sarà annullata l'assegnazione della dashboard e questa non sarà più accessibile dal cliente.", + "unassign-dashboard": "Annulla assegnazione dashboard", + "unassign-dashboards-title": "Sei sicuro di voler annullare l'assegnazione di { count, plural, 1 {1 dashboard} other {# dashboard} }?", + "unassign-dashboards-text": "Dopo la conferma sarà annullata l'assegnazione di tutte le dashboard selezionate e queste non saranno più accessibili dal cliente.", + "public-dashboard-title": "La Dashboard è ora pubblica", + "public-dashboard-text": "La dashboard {{dashboardTitle}} è ora pubblica e accessibile al link:", + "public-dashboard-notice": "Nota: Ricorda di rendere pubblici i relativi dispositivi per accedere ai loro dati.", + "make-private-dashboard-title": "Sei sicuro di voler rendere privata la dashboard '{{dashboardTitle}}'?", + "make-private-dashboard-text": "Dopo la conferma la dashboard sarà resa privata e non più accessibile dagli altri.", + "make-private-dashboard": "Rendi privata la dashboard", + "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard", + "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard", + "select-dashboard": "Seleziona dashboard", + "no-dashboards-matching": "Nessuna dashboard corrispondente a '{{entity}}' è stata trovata.", + "dashboard-required": "Dashboard obbligatoria.", + "select-existing": "Seleziona una dashboard esistente", + "create-new": "Crea nuova dashboard", + "new-dashboard-title": "Titolo nuova dashboard", + "open-dashboard": "Apri dashboard", + "set-background": "Imposta sfondo", + "background-color": "Colore sfondo", + "background-image": "Immagine sfondo", + "background-size-mode": "Modalità dimensione sfondo", + "no-image": "Nessuna immagine selezionata", + "drop-image": "Trascina un'immagine o fai clic per selezionare un file da caricare.", + "settings": "Impostazioni", + "columns-count": "Numero colonne", + "columns-count-required": "Numero colonne obbligatorio.", + "min-columns-count-message": "Ammesso un numero minimo di colonne pari a 10.", + "max-columns-count-message": "Ammesso un numero massimo di colonne pari a 1000.", + "widgets-margins": "Margine tra i widget", + "horizontal-margin": "Margine orizzontale", + "horizontal-margin-required": "Margine orizzontale obbligatorio.", + "min-horizontal-margin-message": "Ammesso un margine orizzontale minimo pari a 0.", + "max-horizontal-margin-message": "Ammesso un margine orizzontale massimo pari a 50.", + "vertical-margin": "Margine verticale", + "vertical-margin-required": "Margine verticale obbligatorio.", + "min-vertical-margin-message": "Ammesso un margine verticale minimo pari a 0.", + "max-vertical-margin-message": "Ammesso un margine verticale massimo pari a 50.", + "autofill-height": "Riempi automaticamente altezza layout", + "mobile-layout": "Impostazioni layout mobile", + "mobile-row-height": "Altezza riga mobile (px)", + "mobile-row-height-required": "Altezza riga mobile è richiesta.", + "min-mobile-row-height-message": "5 pixel è il minimo concesso al valore altezza riga mobile.", + "max-mobile-row-height-message": "200 pixel è il massimo concesso al valore altezza riga mobile.", + "display-title": "Mostra titolo dashboard", + "toolbar-always-open": "Mantieni aperta la barra degli strumenti", + "title-color": "Colore titolo", + "display-dashboards-selection": "Mostra selezione dashboard", + "display-entities-selection": "Mostra selezione entità", + "display-dashboard-timewindow": "Mostra intervallo temporale", + "display-dashboard-export": "Mostra esportazione", + "import": "Importa dashboard", + "export": "Esporta dashboard", + "export-failed-error": "Impossibile esportare la dashboard: {{error}}", + "create-new-dashboard": "Crea nuova dashboard", + "dashboard-file": "File dashboard", + "invalid-dashboard-file-error": "Impossibile importare la dashboard: struttura dati della dashboard non valida.", + "dashboard-import-missing-aliases-title": "Configura alias utilizzati dalla dashboard importata", + "create-new-widget": "Crea nuovo widget", + "import-widget": "Importa widget", + "widget-file": "Widget file", + "invalid-widget-file-error": "Impossibile importare il widget: struttura dati del widget non valida.", + "widget-import-missing-aliases-title": "Configura gli alias utilizzati dai widget importati", + "open-toolbar": "Apri barra degli strumenti", + "close-toolbar": "Chiudi barra degli strumenti", + "configuration-error": "Errore di configurazione", + "alias-resolution-error-title": "Errore di configurazione degli alias della dashboard", + "invalid-aliases-config": "Impossibile trovare un dispositivo corrispondente ad un qualche filtro degli alias.
    Contatta l'amministratore per risolvere il problema.", + "select-devices": "Seleziona dispositivi", + "assignedToCustomer": "Assegnato al cliente", + "assignedToCustomers": "Assegnato ai clienti", + "public": "Pubblico", + "public-link": "Link pubblico", + "copy-public-link": "Copia link pubblico", + "public-link-copied-message": "Link pubblico della dashboard copiato negli appunti", + "manage-states": "Gestisci stati dashboard", + "states": "Stati dashboard", + "search-states": "Ricerca stati dashboard", + "selected-states": "{ count, plural, 1 {1 stato dashboard selezionato} other {# stati dashboard selezionati} }", + "edit-state": "Modifica stato dashboard", + "delete-state": "Elimina stato dashboard", + "add-state": "Aggiungi stato dashboard", + "state": "Stato dashboard", + "state-name": "Nome", + "state-name-required": "Nome stato dashboard obbligatorio.", + "state-id": "Id stato", + "state-id-required": "Id stato dashboard obbligatorio.", + "state-id-exists": "Uno stato della dashboard con lo stesso id è già presente.", + "is-root-state": "Stato radice", + "delete-state-title": "Elimina stato dashboard", + "delete-state-text": "Sei sicuro di voler eliminare lo stato della dashboard di nome '{{stateName}}'?", + "show-details": "Mostra dettagli", + "hide-details": "Nascondi dettagli", + "select-state": "Seleziona stato target", + "state-controller": "Stato controller" + }, + "datakey": { + "settings": "Impostazioni", + "advanced": "Avanzate", + "label": "Etichetta", + "color": "Colore", + "units": "Simbolo speciale da mostrare accanto al valore", + "decimals": "Numero cifre decimali", + "data-generation-func": "Funzione generazione dati", + "use-data-post-processing-func": "Usa funzione dopo il processamento dei dati", + "configuration": "Configurazione data key", + "timeseries": "Serie temporali", + "attributes": "Attributi", + "alarm": "Campi allarme", + "timeseries-required": "Le serie temporali dell'entità sono richieste.", + "timeseries-or-attributes-required": "Le serie temporali o gli attributi dell'entità sono richiesti.", + "maximum-timeseries-or-attributes": "Massimo { count, plural, 1 {1 serie temporale/attributo consentito.} other {# serie temporali/attributi consentiti.} }", + "alarm-fields-required": "Campi allarme obbligatori.", + "function-types": "Tipi funzione", + "function-types-required": "Tipi funzione obbligatorio.", + "maximum-function-types": "Massimo { count, plural, 1 {1 tipo di funzione consentito.} other {# tipi di funzione consentiti} }", + "time-description": "timestamp del valore corrente;", + "value-description": "il valore corrente;", + "prev-value-description": "risultato della precedente chiamata alla funzione;", + "time-prev-description": "timestamp del valore precedente;", + "prev-orig-value-description": "valore precedente originale;" + }, + "datasource": { + "type": "Tipo sorgente dati", + "name": "Nome", + "add-datasource-prompt": "Aggiungi una sorgente dati" + }, + "details": { + "edit-mode": "Modalità modifica", + "toggle-edit-mode": "Attiva/disattiva modalità di modifica" + }, + "device": { + "device": "Dispositivo", + "device-required": "Dispositivo richiesto.", + "devices": "Dispositivi", + "management": "Gestione dispositivo", + "view-devices": "Visualizza Dispositivi", + "device-alias": "Alias dispositivo", + "aliases": "Alias dispositivo", + "no-alias-matching": "'{{alias}}' non trovato.", + "no-aliases-found": "Nessun alias trovato.", + "no-key-matching": "'{{key}}' non trovata.", + "no-keys-found": "Nessuna chiave trovata.", + "create-new-alias": "Creane uno nuovo!", + "create-new-key": "Creane una nuova!", + "duplicate-alias-error": "Sono stati trovati dei duplicati dell'alias '{{alias}}'.
    Gli alias di un dispositivo devono essere univoci all'interno della dashboard.", + "configure-alias": "Configura alias '{{alias}}'", + "no-devices-matching": "Nessun dispositivo corrispondente a '{{entity}}' è stato trovato.", + "alias": "Alias", + "alias-required": "Alias dispositivo richiesto.", + "remove-alias": "Rimuovi alias dispositivo", + "add-alias": "Aggiungi alias dispositivo", + "name-starts-with": "Dispositivo il cui nome inizia per", + "device-list": "Lista dispositivi", + "use-device-name-filter": "Usa filtro", + "device-list-empty": "Nessun dispositivo selezionato.", + "device-name-filter-required": "Filtro nome dispositivo obbligatorio.", + "device-name-filter-no-device-matched": "Nessun dispositivo il cui nome inizia per '{{device}}' è stato trovato.", + "add": "Aggiungi Dispositivo", + "assign-to-customer": "Assegna al cliente", + "assign-device-to-customer": "Assegna dispositivo/dispositivi al Cliente", + "assign-device-to-customer-text": "Seleziona i dispositivi da assegnare al cliente", + "make-public": "Rendi pubblico il dispositivo", + "make-private": "Rendi privato il dispositivo", + "no-devices-text": "Nessun dispositivo trovato", + "assign-to-customer-text": "Seleziona il cliente a cui assegnare il dispositivo/i dispositivi", + "device-details": "Dettagli dispositivo", + "add-device-text": "Aggiungi nuovo dispositivo", + "credentials": "Credenziali", + "manage-credentials": "Gestisci credenziali", + "delete": "Elimina dispositivo", + "assign-devices": "Assegna dispositivi", + "assign-devices-text": "Assegna { count, plural, 1 {1 dispositivo} other {# dispositivi} } al cliente", + "delete-devices": "Elimina dispositivi", + "unassign-from-customer": "Annulla assegnazione al cliente", + "unassign-devices": "Annulla assegnazione dispositivi", + "unassign-devices-action-title": "Annulla assegnazione { count, plural, 1 {1 dispositivo} other {# dispositivi} } al cliente", + "assign-new-device": "Assegna nuovo dispositivo", + "make-public-device-title": "Sei sicuro di voler rendere pubblico il dispositivo '{{deviceName}}'?", + "make-public-device-text": "Dopo la conferma il dispositivo e tutti i suoi dati saranno resi pubblici e accessibili dagli altri.", + "make-private-device-title": "Sei sicuro di voler rendere privato il dispositivo '{{deviceName}}'?", + "make-private-device-text": "Dopo la conferma il dispositivo e tutti i suoi dati saranno resi privati e non più accessibili da altri utenti.", + "view-credentials": "Visualizza credenziali", + "delete-device-title": "Sei sicuro di voler eliminare il dispositivo '{{deviceName}}'?", + "delete-device-text": "Attenzione, dopo la conferma il dispositivo e tutti i suoi dati non saranno più recuperabili.", + "delete-devices-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 dispositivo} other {# dispositivi} }?", + "delete-devices-action-title": "Elimina { count, plural, 1 {1 dispositivo} other {# dispositivi} }", + "delete-devices-text": "Attenzione, dopo la conferma tutti i dispositivi selezionati saranno eliminati e i relativi dati non saranno più recuperabili.", + "unassign-device-title": "Sei sicuro di voler annullare l'assegnazione del dispositivo '{{deviceName}}'?", + "unassign-device-text": "Dopo la conferma sarà annullata l'assegnazione del dispositivo e questo non sarà più accessibile dal cliente.", + "unassign-device": "Annulla assegnazione dispositivo", + "unassign-devices-title": "Sei sicuro di voler annullare l'assegnazione di { count, plural, 1 {1 dispositivo} other {# dispositivi} }?", + "unassign-devices-text": "Dopo la conferma sarà annullata l'assegnazione di tutti i dispositivi selezionati e questi non saranno più accessibili dal cliente.", + "device-credentials": "Credenziali Dispositivo", + "credentials-type": "Tipo credenziali", + "access-token": "Token di accesso", + "access-token-required": "Token di accesso obbligatorio.", + "access-token-invalid": "Il token di accesso deve avere una lunghezza compresa tra 1 e 20 caratteri.", + "rsa-key": "Chiave pubblica RSA", + "rsa-key-required": "Chiave pubblica RSA obbligatoria.", + "secret": "Secret", + "secret-required": "Secret obbligatorio.", + "device-type": "Tipo dispositivo", + "device-type-required": "Tipo dispositivo obbligatorio.", + "select-device-type": "Seleziona tipo dispositivo", + "enter-device-type": "Inserisci typo dispositivo", + "any-device": "Qualsiasi dispositivo", + "no-device-types-matching": "Nessun dispositivo corrispondente a '{{entitySubtype}}' è stato trovato.", + "device-type-list-empty": "Nessun tipo di dispositivo selezionato.", + "device-types": "Tipi dispositivo", + "name": "Nome", + "name-required": "Nome obbligatorio.", + "description": "Descrizione", + "events": "Eventi", + "details": "Dettagli", + "copyId": "Copia Id dispositivo", + "copyAccessToken": "Copia token di accesso", + "idCopiedMessage": "Id dispositivo copiato negli Appunti", + "accessTokenCopiedMessage": "Token di accesso del dispositivo copiato negli Appunti", + "assignedToCustomer": "Assegnato al cliente", + "unable-delete-device-alias-title": "Impossibile rimuovere l'alias del dispositivo", + "unable-delete-device-alias-text": "L'alias del dispositivo '{{deviceAlias}}' non può essere eliminato perché utilizzato dai seguenti widget:
    {{widgetsList}}", + "is-gateway": "È un gateway", + "public": "Pubblico", + "device-public": "Il dispositivo è pubblico", + "select-device": "Seleziona dispositivo" + }, + "dialog": { + "close": "Close dialog" + }, + "direction": { + "column": "Colonna", + "row": "Riga" + }, + "error": { + "unable-to-connect": "Impossibile connettersi al server! Controlla la connessione ad Internet.", + "unhandled-error-code": "Codice errore non gestito: {{errorCode}}", + "unknown-error": "Errore sconosciuto" + }, + "entity": { + "entity": "Entità", + "entities": "Entità", + "aliases": "Alias entità", + "entity-alias": "Alias entità", + "unable-delete-entity-alias-title": "Impossibile eliminare alias entità", + "unable-delete-entity-alias-text": "L'alias dell'entità '{{entityAlias}}' non può essere eliminato perché utilizzato dai seguenti widget:
    {{widgetsList}}", + "duplicate-alias-error": "Trovato un duplicato dell'alias '{{alias}}'.
    Gli alias dell'entità devono essere univoci all'interno della dashboard.", + "missing-entity-filter-error": "Manca il filtro per l'alias '{{alias}}'.", + "configure-alias": "Configura '{{alias}}' alias", + "alias": "Alias", + "alias-required": "Alias entità obbligatorio.", + "remove-alias": "Rimuovi alias entità", + "add-alias": "Aggiungi alias entità", + "entity-list": "Lista entità", + "entity-type": "Tipo entità", + "entity-types": "Tipi entità", + "entity-type-list": "Lista tipo entità", + "any-entity": "Qualsiasi entità", + "enter-entity-type": "Inserisci tipo entità", + "no-entities-matching": "Nessuna entità corrispondente a '{{entity}}' è stata trovata.", + "no-entity-types-matching": "Nessun tipo di entità corrispondente a '{{entityType}}' è stato trovato.", + "name-starts-with": "Nome inizia per", + "use-entity-name-filter": "Usa filtro", + "entity-list-empty": "Nessuna entità selezionata.", + "entity-type-list-empty": "Nessun tipo di entità selezionato.", + "entity-name-filter-required": "Filtro nome entità obbligatorio.", + "entity-name-filter-no-entity-matched": "Nessuna entità che inizia per '{{entity}}' è stata trovata.", + "all-subtypes": "Tutte", + "select-entities": "Seleziona entità", + "no-aliases-found": "Nessun alias trovato.", + "no-alias-matching": "'{{alias}}' non trovato.", + "create-new-alias": "Creane uno nuovo!", + "key": "Chiave", + "key-name": "Nome chiave", + "no-keys-found": "Nessuna chiave trovata.", + "no-key-matching": "'{{key}}' non trovata.", + "create-new-key": "Creane una nuova!", + "type": "Tipo", + "type-required": "Tipo entità obbligatorio.", + "type-device": "Dispositivo", + "type-devices": "Dispositivi", + "list-of-devices": "{ count, plural, 1 {Un dispositivo} other {Lista di # dispositivi} }", + "device-name-starts-with": "Dispositivi i cui nomi iniziano per '{{prefix}}'", + "type-asset": "Asset", + "type-assets": "Asset", + "list-of-assets": "{ count, plural, 1 {Un asset} other {Lista di # asset} }", + "asset-name-starts-with": "Asset i cui nomi iniziano per '{{prefix}}'", + "type-entity-view": "Vista entità", + "type-entity-views": "Viste entità", + "list-of-entity-views": "{ count, plural, 1 {Una vista entità} other {Lista di # viste entità} }", + "entity-view-name-starts-with": "Viste entità i cui nomi iniziano per '{{prefix}}'", + "type-rule": "Regola", + "type-rules": "Regole", + "list-of-rules": "{ count, plural, 1 {Una regola} other {Lista di # regole} }", + "rule-name-starts-with": "Regole i cui nomi iniziano per '{{prefix}}'", + "type-plugin": "Plugin", + "type-plugins": "Plugin", + "list-of-plugins": "{ count, plural, 1 {Un plugin} other {Lista di # plugin} }", + "plugin-name-starts-with": "Plugin i cui nomi iniziano per '{{prefix}}'", + "type-tenant": "Tenant", + "type-tenants": "Tenants", + "list-of-tenants": "{ count, plural, 1 {One tenant} other {Lista di # tenants} }", + "tenant-name-starts-with": "Tenants whose names start with '{{prefix}}'", + "type-customer": "Cliente", + "type-customers": "Clienti", + "list-of-customers": "{ count, plural, 1 {Un cliente} other {Lista di # clienti} }", + "customer-name-starts-with": "Clienti i cui nomi iniziano per '{{prefix}}'", + "type-user": "Utente", + "type-users": "Utenti", + "list-of-users": "{ count, plural, 1 {Un utente} other {Lista di # utenti} }", + "user-name-starts-with": "Utenti i cui nomi iniziano per '{{prefix}}'", + "type-dashboard": "Dashboard", + "type-dashboards": "Dashboard", + "list-of-dashboards": "{ count, plural, 1 {Una dashboard} other {Lista di # dashboard} }", + "dashboard-name-starts-with": "Dashboard i cui nomi iniziano per '{{prefix}}'", + "type-alarm": "Allarme", + "type-alarms": "Allarmi", + "list-of-alarms": "{ count, plural, 1 {Un allarme} other {Lista di # allarmi} }", + "alarm-name-starts-with": "Allarmi i cui nomi iniziano per '{{prefix}}'", + "type-rulechain": "Rule chain", + "type-rulechains": "Rule chain", + "list-of-rulechains": "{ count, plural, 1 {Una rule chain} other {Lista di # catene di regole} }", + "rulechain-name-starts-with": "Catene di regole i cui nomi iniziano per '{{prefix}}'", + "type-rulenode": "Nodo regola", + "type-rulenodes": "Nodi regola", + "list-of-rulenodes": "{ count, plural, 1 {Un nodo regola} other {Lista di # nodi regola} }", + "rulenode-name-starts-with": "Nodi regola i cui nomi iniziano per '{{prefix}}'", + "type-current-customer": "Cliente attuale", + "search": "Ricerca entità", + "selected-entities": "{ count, plural, 1 {1 entità selezionata} other {# entità selezionate} }", + "entity-name": "Nome entità", + "details": "Dettagli entità", + "no-entities-prompt": "Nessuna entità trovata", + "no-data": "Nessun dato da mostrare", + "columns-to-display": "Colonne da mostrare" + }, + "entity-view": { + "entity-view": "Vista entità", + "entity-view-required": "Vista entità richiesta.", + "entity-views": "Viste entità", + "management": "Gestione viste entità", + "view-entity-views": "Visualizza Viste entità", + "entity-view-alias": "Alias vista entità", + "aliases": "Alias vista entità", + "no-alias-matching": "'{{alias}}' non trovato.", + "no-aliases-found": "Nessun alias trovato.", + "no-key-matching": "'{{key}}' non trovata.", + "no-keys-found": "Nessuna chiave trovata.", + "create-new-alias": "Creane uno nuovo!", + "create-new-key": "Creane una nuova!", + "duplicate-alias-error": "Sono stati trovati dei duplicati dell'alias '{{alias}}'.
    Gli alias di una vista entità devono essere univoci all'interno della dashboard.", + "configure-alias": "Configura alias '{{alias}}'", + "no-entity-views-matching": "Nessuna vista entità corrispondente a '{{entity}}' è stata trovato.", + "alias": "Alias", + "alias-required": "Alias vista entità richiesto.", + "remove-alias": "Rimuovi alias vista entità", + "add-alias": "Aggiungi alias vista entità", + "name-starts-with": "Vista entità il cui nome inizia per", + "entity-view-list": "Lista viste entità", + "use-entity-view-name-filter": "Usa filtro", + "entity-view-list-empty": "Nessuna vista entità selezionata.", + "entity-view-name-filter-required": "Filtro nome vista entità obbligatorio.", + "entity-view-name-filter-no-entity-view-matched": "Nessuna vista entità il cui nome inizia per '{{entity-view}}' è stata trovata.", + "add": "Aggiungi Vista entità", + "assign-to-customer": "Assegna al cliente", + "assign-entity-view-to-customer": "Assegna vista entità/viste entità al Cliente", + "assign-entity-view-to-customer-text": "Seleziona la vista entità da assegnare al cliente", + "no-entity-views-text": "Nessuna vista entità trovata", + "assign-to-customer-text": "Seleziona il cliente a cui assegnare la vista entità/le vista entità", + "entity-view-details": "Dettagli vista entità", + "add-entity-view-text": "Aggiungi nuova vista entità", + "delete": "Elimina vista entità", + "assign-entity-views": "Assegna viste entità", + "assign-entity-views-text": "Assegna { count, plural, 1 {1 vista entità} other {# viste entità} } al cliente", + "delete-entity-views": "Elimina viste entità", + "unassign-from-customer": "Annulla assegnazione al cliente", + "unassign-entity-views": "Annulla assegnazione viste entità", + "unassign-entity-views-action-title": "Annulla assegnazione { count, plural, 1 {1 vista entità} other {# viste entità} } al cliente", + "assign-new-entity-view": "Assegna nuova vista entità", + "delete-entity-view-title": "Sei sicuro di voler eliminare la vista entità '{{entity-viewName}}'?", + "delete-entity-view-text": "Attenzione, dopo la conferma la vista entità e tutti i suoi dati non saranno più recuperabili.", + "delete-entity-views-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 vista entità} other {# viste entità} }?", + "delete-entity-views-action-title": "Elimina { count, plural, 1 {1 vista entità} other {# viste entità} }", + "delete-entity-views-text": "Attenzione, dopo la conferma tutte le vista entità selezionati saranno eliminate e i relativi dati non saranno più recuperabili.", + "unassign-entity-view-title": "Sei sicuro di voler annullare l'assegnazione della vista entità '{{entity-viewName}}'?", + "unassign-entity-view-text": "Dopo la conferma sarà annullata l'assegnazione della vista entità e questa non sarà più accessibile dal cliente.", + "unassign-entity-view": "Annulla assegnazione vista entità", + "unassign-entity-views-title": "Sei sicuro di voler annullare l'assegnazione di { count, plural, 1 {1 vista entità} other {# viste entità} }?", + "unassign-entity-views-text": "Dopo la conferma sarà annullata l'assegnazione di tutte le vista entità selezionate e queste non saranno più accessibili dal cliente.", + "entity-view-type": "Tipo vista entità", + "entity-view-type-required": "Tipo vista entità obbligatorio.", + "select-entity-view-type": "Seleziona tipo vista entità", + "enter-entity-view-type": "Inserisci tipo vista entità", + "any-entity-view": "Qualsiasi vista entità", + "no-entity-view-types-matching": "Nessuna vista entità corrispondente a '{{entitySubtype}}' è stata trovata.", + "entity-view-type-list-empty": "Nessun tipo di vista entità selezionato.", + "entity-view-types": "Tipi vista entità", + "name": "Nome", + "name-required": "Nome obbligatorio.", + "description": "Descrizione", + "events": "Eventi", + "details": "Dettagli", + "copyId": "Copia Id vista entità", + "assignedToCustomer": "Assegnata al cliente", + "unable-entity-view-device-alias-title": "Impossibile rimuovere l'alias del vista entità", + "unable-entity-view-device-alias-text": "L'alias del vista entità '{{entity-viewAlias}}' non può essere eliminato perché utilizzato dai seguenti widget:
    {{widgetsList}}", + "select-entity-view": "Seleziona vista entità", + "make-public": "Rendi pubblica la vista entità", + "make-private": "Rendi privata la vista entità", + "start-date": "Data inizio", + "start-ts": "Ora inizio", + "end-date": "Data fine", + "end-ts": "Ora fine", + "date-limits": "Limiti temporali", + "client-attributes": "Attributi cliente", + "shared-attributes": "Attributi condivisi", + "server-attributes": "Attributi server", + "timeseries": "Serie temporali", + "client-attributes-placeholder": "Attributi cliente", + "shared-attributes-placeholder": "Attributi condivisi", + "server-attributes-placeholder": "Attributi server", + "timeseries-placeholder": "Serie temporali", + "target-entity": "Entità target", + "attributes-propagation": "Propagazione degli attributi", + "attributes-propagation-hint": "La vista entità copierà automaticamente gli attributi specificati dall'entità target ogni volta che questa vista entità sarà salvata e aggiornata. Per ragioni di performance, gli attributi dell'entità target non sono propagati alle viste entità ogni cambiamento di attributo. È possibile abilitare la propagazione automatica configurando il nodo regola \"Copia alla vista\" nella rule chain e collegando i messaggi \"Post attributes\" a \"Attributes Updated\" al nuovo nodo regola.", + "timeseries-data": "Dati delle serie temporali", + "timeseries-data-hint": "Imposta le chiavi delle serie temporali dell'entità target che saranno accessibili alla vista entità. Questi dati sono di sola lettura.", + "make-public-entity-view-title": "Sei sicuro di voler rendere pubblica la vista entità '{{entity-viewName}}'?", + "make-public-entity-view-text": "Dopo la conferma la vista entità e tutti i suoi dati saranno resi pubblici e accessibili dagli altri.", + "make-private-entity-view-title": "Sei sicuro di voler rendere privata la vista entità '{{entity-viewName}}'?", + "make-private-entity-view-text": "Dopo la conferma la vista entità e tutti i suoi dati saranno resi privati e non più accessibili da altri utenti." + }, + "event": { + "event-type": "Tipo evento", + "type-error": "Errore", + "type-lc-event": "Ciclo di vita evento", + "type-stats": "Statistiche", + "type-debug-rule-node": "Debug", + "type-debug-rule-chain": "Debug", + "no-events-prompt": "Nessun evento trovato", + "error": "Errore", + "alarm": "Allarme", + "event-time": "Orario evento", + "server": "Server", + "body": "Body", + "method": "Metodo", + "type": "Tipo", + "entity": "Entità", + "message-id": "Id Messaggio", + "message-type": "Tipo Messaggio", + "data-type": "Tipo di dato", + "relation-type": "Tipo di relazione", + "metadata": "Metadati", + "data": "Dati", + "event": "Evento", + "status": "Stato", + "success": "Success", + "failed": "Failed", + "messages-processed": "Messaggi elaborati", + "errors-occurred": "Si sono verificati degli errori" + }, + "extension": { + "extensions": "Estensioni", + "selected-extensions": "{ count, plural, 1 {1 estensione selezionata} other {# estensioni selezionate} }", + "type": "Tipo", + "key": "Chiave", + "value": "Valore", + "id": "Id", + "extension-id": "Id Estensione", + "extension-type": "Tipo Estensione", + "transformer-json": "JSON *", + "unique-id-required": "Id estensione corrente già esistente.", + "delete": "Elimina estensione", + "add": "Aggiungi estensione", + "edit": "Modifica estensione", + "delete-extension-title": "Sei sicuro di voler eliminare l'estensione '{{extensionId}}'?", + "delete-extension-text": "Attenzione, dopo la conferma l'estensione e tutti i suoi data non saranno più recuperabili.", + "delete-extensions-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 estensione} other {# estensioni} }?", + "delete-extensions-text": "Attenzione, dopo la conferma tutte le estensioni selezionate saranno eliminate.", + "converters": "Convertitori", + "converter-id": "Id convertitore", + "configuration": "Configurazione", + "converter-configurations": "Configurazioni convertitore", + "token": "Token di sicurezza", + "add-converter": "Aggiungi convertitore", + "add-config": "Aggiungi configurazione convertitore", + "device-name-expression": "Espressione nome dispositivo", + "device-type-expression": "Espressione tipo dispositivo", + "custom": "Custom", + "to-double": "To Double", + "transformer": "Transformer", + "json-required": "Transformer json is required.", + "json-parse": "Unable to parse transformer json.", + "attributes": "Attributi", + "add-attribute": "Aggiungi attributo", + "add-map": "Add mapping element", + "timeseries": "Serie temporali", + "add-timeseries": "Add timeseries", + "field-required": "Campo obbligatorio", + "brokers": "Broker", + "add-broker": "Aggiungi broker", + "host": "Host", + "port": "Porta", + "port-range": "Il numero di porta deve essere compreso tra 1 e 65535.", + "ssl": "Ssl", + "credentials": "Credenziali", + "username": "Nome utente", + "password": "Password", + "retry-interval": "Intervallo di ripetizione in millisecondi", + "anonymous": "Anonimo", + "basic": "Basic", + "pem": "PEM", + "ca-cert": "File certificato CA *", + "private-key": "File chiave privata *", + "cert": "File certificato *", + "no-file": "Nessun file selezionato.", + "drop-file": "Trascina un file o fai clic per selezionare un file da caricare.", + "mapping": "Mapping", + "topic-filter": "Filtro topic", + "converter-type": "Tipo convertitore", + "converter-json": "Json", + "json-name-expression": "Device name json expression", + "topic-name-expression": "Device name topic expression", + "json-type-expression": "Device type json expression", + "topic-type-expression": "Device type topic expression", + "attribute-key-expression": "Attribute key expression", + "attr-json-key-expression": "Attribute key json expression", + "attr-topic-key-expression": "Attribute key topic expression", + "request-id-expression": "Request id expression", + "request-id-json-expression": "Request id json expression", + "request-id-topic-expression": "Request id topic expression", + "response-topic-expression": "Response topic expression", + "value-expression": "Value expression", + "topic": "Topic", + "timeout": "Timeout in millisecondi", + "converter-json-required": "Convertitore json obbligatorio.", + "converter-json-parse": "Unable to parse converter json.", + "filter-expression": "Filter expression", + "connect-requests": "Richieste di connessione", + "add-connect-request": "Aggiungi richiesta di connessione", + "disconnect-requests": "Richieste di disconnessione", + "add-disconnect-request": "Aggiungi richiesta di disconnessione", + "attribute-requests": "Attribute requests", + "add-attribute-request": "Add attribute request", + "attribute-updates": "Attribute updates", + "add-attribute-update": "Add attribute update", + "server-side-rpc": "RPC lato server", + "add-server-side-rpc-request": "Aggiungi richiesta RPC server-side", + "device-name-filter": "Filtro nome dispositivo", + "attribute-filter": "Filtro attributo", + "method-filter": "Filtro metodo", + "request-topic-expression": "Request topic expression", + "response-timeout": "Timeout risposta in millisecondi", + "topic-expression": "Topic expression", + "client-scope": "Visibilità client", + "add-device": "Aggiungi dispositivo", + "opc-server": "Server", + "opc-add-server": "Aggiungi server", + "opc-add-server-prompt": "Aggiungi server", + "opc-application-name": "Nome applicazione", + "opc-application-uri": "Uri applicazione", + "opc-scan-period-in-seconds": "Intervallo di scansione in secondi", + "opc-security": "Sicurezza", + "opc-identity": "Identità", + "opc-keystore": "Keystore", + "opc-type": "Tipo", + "opc-keystore-type": "Tipo", + "opc-keystore-location": "Location *", + "opc-keystore-password": "Password", + "opc-keystore-alias": "Alias", + "opc-keystore-key-password": "Chiave password", + "opc-device-node-pattern": "Device node pattern", + "opc-device-name-pattern": "Device name pattern", + "modbus-server": "Server/slave", + "modbus-add-server": "Aggiungi server/slave", + "modbus-add-server-prompt": "Aggiungi server/slave", + "modbus-transport": "Transport", + "modbus-tcp-reconnect": "Riconnessione automatica", + "modbus-rtu-over-tcp": "RTU over TCP", + "modbus-port-name": "Nome porta seriale", + "modbus-encoding": "Codifica", + "modbus-parity": "Parità", + "modbus-baudrate": "Baud rate", + "modbus-databits": "Data bits", + "modbus-stopbits": "Stop bits", + "modbus-databits-range": "Data bits deve essere compreso nell'intervallo 7-8.", + "modbus-stopbits-range": "Stop bits deve essere compreso nell'intervallo 1-2.", + "modbus-unit-id": "ID unità", + "modbus-unit-id-range": "ID unità deve essere compreso nell'intervallo 1-247.", + "modbus-device-name": "Nome dispositivo", + "modbus-poll-period": "Intervallo di polling (ms)", + "modbus-attributes-poll-period": "Intervallo di polling degli attributi (ms)", + "modbus-timeseries-poll-period": "Intervallo di polling delle serie temporali (ms)", + "modbus-poll-period-range": "L'intervallo di polling deve essere un valore positivo.", + "modbus-tag": "Tag", + "modbus-function": "Funzione", + "modbus-register-address": "Indirizzo registro", + "modbus-register-address-range": "Indirizzo registro deve essere compreso tra 0 e 65535.", + "modbus-register-bit-index": "Bit index", + "modbus-register-bit-index-range": "Bit index should be in a range from 0 to 15.", + "modbus-register-count": "Register count", + "modbus-register-count-range": "Register count should be a positive value.", + "modbus-byte-order": "Byte order", + "sync": { + "status": "Stato", + "sync": "Sincronizzato", + "not-sync": "Non sincronizzato", + "last-sync-time": "Ultima sincronizzazione", + "not-available": "Non disponibile" + }, + "export-extensions-configuration": "Esporta configurazione estensioni", + "import-extensions-configuration": "Importa configurazione estensioni", + "import-extensions": "Importa estensione", + "import-extension": "Importa estensione", + "export-extension": "Esporta estensione", + "file": "File estensione", + "invalid-file-error": "File estensione non valido" + }, + "fullscreen": { + "expand": "Espandi a tutto schermo", + "exit": "Esci da schermo intero", + "toggle": "Commuta modalità schermo intero", + "fullscreen": "Schermo intero" + }, + "function": { + "function": "Funzione" + }, + "grid": { + "delete-item-title": "Sei sicuro di voler eliminare questo elemento?", + "delete-item-text": "Attenzione, dopo la conferma questo elemento e tutti i suoi dati non saranno più recuperabili.", + "delete-items-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 elemento} other {# elementi} }?", + "delete-items-action-title": "Elimina { count, plural, 1 {1 elemento} other {# elementi} }", + "delete-items-text": "Attenzione, dopo la conferma tutti gli elementi selezionati saranno rimossi e i relativi dati non saranno più recuperabili.", + "add-item-text": "Aggiungi nuovo elemento", + "no-items-text": "Nessun elemento trovato", + "item-details": "Dettagli elemento", + "delete-item": "Elimina elemento", + "delete-items": "Elimina elementi", + "scroll-to-top": "Scorri verso l'alto" + }, + "help": { + "goto-help-page": "Vai all'help" + }, + "home": { + "home": "Home", + "profile": "Profilo", + "logout": "Logout", + "menu": "Menu", + "avatar": "Avatar", + "open-user-menu": "Apri menu utente" + }, + "import": { + "no-file": "Nessun file selezionato", + "drop-file": "Trascina un file JSON o fai clic per selezionare un file da caricare." + }, + "item": { + "selected": "Selezionata" + }, + "js-func": { + "no-return-error": "La funzione deve restituire un valore!", + "return-type-mismatch": "La funzione deve restituire un valore di tipo '{{type}}'!", + "tidy": "Tidy" + }, + "key-val": { + "key": "Chiave", + "value": "Valore", + "remove-entry": "Rimuovi voce", + "add-entry": "Aggiungi voce", + "no-data": "Nessuna voce" + }, + "layout": { + "layout": "Layout", + "manage": "Gestisci layout", + "settings": "Impostazioni layout", + "color": "Colore", + "main": "Main", + "right": "Destra", + "select": "Select target layout" + }, + "legend": { + "direction": "Direzione", + "position": "Posizione Legenda", + "show-max": "Mostra valore max", + "show-min": "Mostra valore min", + "show-avg": "Mostra valore medio", + "show-total": "Mostra valore totale", + "settings": "Impostazioni legenda", + "min": "min", + "max": "max", + "avg": "avg", + "total": "totale" + }, + "login": { + "login": "Accedi", + "request-password-reset": "Richiesta reset password", + "reset-password": "Reset Password", + "create-password": "Crea Password", + "passwords-mismatch-error": "Le password inserite devono corrispondere!", + "password-again": "Ripeti Password", + "sign-in": "Please sign in", + "username": "Nome utente (email)", + "remember-me": "Ricordami", + "forgot-password": "Password dimenticata?", + "password-reset": "Password reset", + "new-password": "Nuova password", + "new-password-again": "Ripeti nuova password", + "password-link-sent-message": "Link reset password inviato con successo!", + "email": "Email", + "login-with": "Accedi con {{name}}", + "or": "o" + }, + "position": { + "top": "Alto", + "bottom": "Basso", + "left": "Sinistra", + "right": "Destra" + }, + "profile": { + "profile": "Profilo", + "last-login-time": "Ultimo accesso", + "change-password": "Modifica Password", + "current-password": "Password attuale" + }, + "relation": { + "relations": "Relazioni", + "direction": "Direzione", + "search-direction": { + "FROM": "Da", + "TO": "A" + }, + "direction-type": { + "FROM": "da", + "TO": "a" + }, + "from-relations": "Relazioni in uscita", + "to-relations": "Relazioni in ingresso", + "selected-relations": "{ count, plural, 1 {1 relazione selezionata} other {# relazioni selezionate} }", + "type": "Tipo", + "to-entity-type": "A tipo entità", + "to-entity-name": "A nome entità", + "from-entity-type": "Da tipo entità", + "from-entity-name": "Da nome entità", + "to-entity": "A entità", + "from-entity": "Da entità", + "delete": "Elimina relazione", + "relation-type": "Tipo di relazione", + "relation-type-required": "Tipo di relazione obbligatorio.", + "any-relation-type": "Ogni tipo", + "add": "Aggiungi relazione", + "edit": "Modifica relazione", + "delete-to-relation-title": "Sei sicuro di voler eliminare la relazione con l'entità '{{entityName}}'?", + "delete-to-relation-text": "Attenzione, dopo la conferma l'entità '{{entityName}}' sarà scollegata dall'entità corrente.", + "delete-to-relations-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 relazione} other {# relazioni} }?", + "delete-to-relations-text": "Attenzione, dopo la conferma tutte le relazioni selezionate saranno rimosse e le corrispondenti entità scollegate da quella corrente.", + "delete-from-relation-title": "Sei sicuro di voler eliminare la relazione dall'entità '{{entityName}}'?", + "delete-from-relation-text": "Attenzione, dopo la conferma l'entità corrente sarà scollegata dall'entità '{{entityName}}'.", + "delete-from-relations-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 relazione} other {# relazioni} }?", + "delete-from-relations-text": "Attenzione, dopo la conferma tutte le relazioni selezionate saranno rimosse e l'entità corrente scollegata dalle corrispondenti entità.", + "remove-relation-filter": "Rimuovi filtro relazioni", + "add-relation-filter": "Aggiungi filtro relazioni", + "any-relation": "Qualsiasi relazione", + "relation-filters": "Filtri relazioni", + "additional-info": "Informazioni aggiuntive (JSON)", + "invalid-additional-info": "Impossibile analizzare le informazioni aggiuntive in JSON." + }, + "rulechain": { + "rulechain": "Rule chain", + "rulechains": "Rule chain", + "root": "Root", + "delete": "Cancella rule chain", + "name": "Nome", + "name-required": "Nome obbligatorio.", + "description": "Descrizione", + "add": "Aggiungi Rule Chain", + "set-root": "Imposta la rule chain come root", + "set-root-rulechain-title": "Sei sicuro di voler impostare la rule chain '{{ruleChainName}}' come root?", + "set-root-rulechain-text": "Dopo la conferma la rule chain diverrà root a gestirà tutti i messaggi in arrivo.", + "delete-rulechain-title": "Sei sicuro di voler eliminare la rule chain '{{ruleChainName}}'?", + "delete-rulechain-text": "Attenzione, dopo la conferma la rule chain e tutti i dati relativi non saranno più recuperabili.", + "delete-rulechains-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 rule chain} other {# rule chain} }?", + "delete-rulechains-action-title": "Elimina { count, plural, 1 {1 rule chain} other {# rule chain} }", + "delete-rulechains-text": "Attenzione, dopo la conferma tutte le rule chain selezionate saranno rimosse e tutti i relativi dati non saranno più recuperabili.", + "add-rulechain-text": "Aggiungi nuova rule chain", + "no-rulechains-text": "Nessuna rule chain trovata", + "rulechain-details": "Dettagli rule chain", + "details": "Dettagli", + "events": "Eventi", + "system": "Sistema", + "import": "Importa rule chain", + "export": "Esporta rule chain", + "export-failed-error": "Impossibile esportare rule chain: {{error}}", + "create-new-rulechain": "Crea nuova rule chain", + "rulechain-file": "File rule chain", + "invalid-rulechain-file-error": "Impossibile importare rule chain: struttura dati rule chain non valida.", + "copyId": "Copia Id rule chain", + "idCopiedMessage": "Id rule chain copiato negli appunti", + "select-rulechain": "Seleziona rule chain", + "no-rulechains-matching": "Nessuna rule chain corrispondente a '{{entity}}' è stata trovata.", + "rulechain-required": "Rule chain obbligatoria", + "management": "Gestione regole", + "debug-mode": "Modalità debug" + }, + "rulenode": { + "details": "Dettagli", + "events": "Eventi", + "search": "Ricerca nodi", + "open-node-library": "Apri libreria nodi", + "add": "Aggiungi nodo regola", + "name": "Nome", + "name-required": "Nome obbligatorio.", + "type": "Tipo", + "description": "Descrizione", + "delete": "Elimina nodo regola", + "select-all-objects": "Seleziona tutti i nodi e le connessioni", + "deselect-all-objects": "Deseleziona tutti i nodi e le connessioni", + "delete-selected-objects": "Cancella nodi e connessioni selezionate", + "delete-selected": "Elimina selezionati", + "select-all": "Seleziona tutto", + "copy-selected": "Copia selezionata", + "deselect-all": "Deseleziona tutto", + "rulenode-details": "Dettagli nodo regola", + "debug-mode": "Modalità debug", + "configuration": "Configurazione", + "link": "Link", + "link-details": "Dettagli link nodo regola", + "add-link": "Aggiungi link", + "link-label": "Etichetta link", + "link-label-required": "Etichetta link obbligatoria.", + "custom-link-label": "Etichetta link personalizzata", + "custom-link-label-required": "Etichetta link personalizzata obbligatoria.", + "link-labels": "Etichette link", + "link-labels-required": "Etichette link richieste.", + "no-link-labels-found": "Nessuna etichetta link trovata.", + "no-link-label-matching": "'{{label}}' non trovata.", + "create-new-link-label": "Creane una nuova!", + "type-filter": "Filtro", + "type-filter-details": "Filtra i messaggi in arrivo con le condizioni configurate", + "type-enrichment": "Enrichment", + "type-enrichment-details": "Aggiungi informazioni addizionali nei metadati del messaggio", + "type-transformation": "Trasformazione", + "type-transformation-details": "Change Message payload and Metadata", + "type-action": "Azioni", + "type-action-details": "Perform special action", + "type-external": "External", + "type-external-details": "Interagisci con un sistema esterno", + "type-rule-chain": "Rule Chain", + "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain", + "type-input": "Input", + "type-input-details": "Logical input of Rule Chain, forwards incoming messages to next related Rule Node", + "type-unknown": "Sconosciuto", + "type-unknown-details": "Nodo regola non trovato", + "directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.", + "ui-resources-load-error": "Failed to load configuration ui resources.", + "invalid-target-rulechain": "Unable to resolve target rule chain!", + "test-script-function": "Test script function", + "message": "Messaggio", + "message-type": "Tipo messaggio", + "select-message-type": "Seleziona tipo messaggio", + "message-type-required": "Tipo messaggio obbligatorio", + "metadata": "Metadata", + "metadata-required": "Metadata entries can't be empty.", + "output": "Output", + "test": "Test", + "help": "Aiuto" + }, + "tenant": { + "tenant": "Tenant", + "tenants": "Tenant", + "management": "Gestione Tenant", + "add": "Aggiungi Tenant", + "admins": "Amministratori", + "manage-tenant-admins": "Gestisci amministratori tenant", + "delete": "Cancella tenant", + "add-tenant-text": "Aggiungi nuovo tenant", + "no-tenants-text": "Nessun tenant trovato", + "tenant-details": "Dettagli tenant", + "delete-tenant-title": "Sei sicuro di voler eliminare il tenant '{{tenantTitle}}'?", + "delete-tenant-text": "Attenzione, dopo la conferma il tenant e tutti i suoi dati non saranno più recuperabili.", + "delete-tenants-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 tenant} other {# tenant} }?", + "delete-tenants-action-title": "Elimina { count, plural, 1 {1 tenant} other {# tenant} }", + "delete-tenants-text": "Attenzione, dopo la conferma tutti i tenant selezionati saranno eliminati e tutti i loro dati non saranno più recuperabili.", + "title": "Titolo", + "title-required": "Titolo obbligatorio.", + "description": "Descrizione", + "details": "Dettagli", + "events": "Eventi", + "copyId": "Copia Id tenant", + "idCopiedMessage": "Id tenant copiato negli appunti", + "select-tenant": "Seleziona tenant", + "no-tenants-matching": "Nessun tenant corrispondente a '{{entity}}' è stato trovato.", + "tenant-required": "Tenant obbligatorio" + }, + "timeinterval": { + "seconds-interval": "{ seconds, plural, 1 {1 secondo} other {# secondi} }", + "minutes-interval": "{ minutes, plural, 1 {1 minuto} other {# minuti} }", + "hours-interval": "{ hours, plural, 1 {1 ora} other {# ore} }", + "days-interval": "{ days, plural, 1 {1 giorno} other {# giorni} }", + "days": "Giorni", + "hours": "Ore", + "minutes": "Minuti", + "seconds": "Secondi", + "advanced": "Avanzate" + }, + "timewindow": { + "days": "{ days, plural, 1 { giorno } other {# giorni } }", + "hours": "{ hours, plural, 0 { ora } 1 {1 ora } other {# ore } }", + "minutes": "{ minutes, plural, 0 { minuto } 1 {1 minuto } other {# minuti } }", + "seconds": "{ seconds, plural, 0 { secondo } 1 {1 secondo } other {# secondi } }", + "realtime": "Realtime", + "history": "Cronologia", + "last-prefix": "ultimo", + "period": "da {{ startTime }} a {{ endTime }}", + "edit": "Modifica intervallo temporale", + "date-range": "Intervallo date", + "last": "Ultimo", + "time-period": "Intervallo temporale", + "hide": "Nascondi" + }, + "user": { + "user": "Utente", + "users": "Utenti", + "customer-users": "Utente cliente", + "tenant-admins": "Amministratori Tenant", + "sys-admin": "Amministratore di sistema", + "tenant-admin": "Amministratore tenant", + "customer": "Cliente", + "anonymous": "Anonimo", + "add": "Aggiungi Utente", + "delete": "Elimina utente", + "add-user-text": "Aggiungi nuovo utente", + "no-users-text": "Nessun utente trovato", + "user-details": "Dettagli utente", + "delete-user-title": "Sei sicuro di voler eliminare l'utente '{{userEmail}}'?", + "delete-user-text": "Attenzione, dopo la conferma l'utente e tutti i suoi dati non saranno più recuperabili.", + "delete-users-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 utente} other {# utenti} }?", + "delete-users-action-title": "Elimina { count, plural, 1 {1 utente} other {# utenti} }", + "delete-users-text": "Attenzione, dopo la conferma tutti gli utenti selezionati saranno eliminati e tutti i relativi dati non saranno più recuperabili.", + "activation-email-sent-message": "Email di attivazione inviata con successo!", + "resend-activation": "Invia di nuovo attivazione", + "email": "Email", + "email-required": "Email obbligatoria.", + "invalid-email-format": "Formato email non valido.", + "first-name": "Nome", + "last-name": "Cognome", + "description": "Descrizione", + "default-dashboard": "Dashboard di default", + "always-fullscreen": "Sempre a schermo intero", + "select-user": "Seleziona utente", + "no-users-matching": "Nessun utente corrispondente a '{{entity}}' è stato trovato.", + "user-required": "Utente obbligatorio", + "activation-method": "Metodo di attivazione", + "display-activation-link": "Mostra link di attivazione", + "send-activation-mail": "Invia email di attivazione", + "activation-link": "Link attivazione utente", + "activation-link-text": "Per attivare l'utente utilizza il seguente link di attivazione :", + "copy-activation-link": "Copia link di attivazione", + "activation-link-copied-message": "Link di attivazione utente copiato negli appunti", + "details": "Dettagli", + "login-as-tenant-admin": "Accedi come Amministratore tenant", + "login-as-customer-user": "Accedi come Utente cliente", + "disable-account": "Disabilita account utente", + "enable-account": "Abilita account utente", + "enable-account-message": "L'account utente è stato abilitato correttamente!", + "disable-account-message": "L'account utente è stato disabilitato correttamente!" + }, + "value": { + "type": "Tipo valore", + "string": "String", + "string-value": "Valore string", + "integer": "Integer", + "integer-value": "Valore integer", + "invalid-integer-value": "Valore integer non valido", + "double": "Double", + "double-value": "Valore double", + "boolean": "Boolean", + "boolean-value": "Valore boolean", + "false": "Falso", + "true": "Vero", + "long": "Long" + }, + "widget": { + "widget-library": "Libreria Widget", + "widget-bundle": "Bundle widget", + "select-widgets-bundle": "Seleziona bundle widget", + "management": "Gestione widget", + "editor": "Editor Widget", + "widget-type-not-found": "Problem loading widget configuration.
    Probably associated\n widget type was removed.", + "widget-type-load-error": "Widget non caricato a causa dei seguenti errori:", + "remove": "Elimina widget", + "edit": "Modifica widget", + "remove-widget-title": "sei sicuro di voler eliminare il widget '{{widgetTitle}}'?", + "remove-widget-text": "Dopo la conferma il widget e tutti i suoi dati non saranno più recuperabili.", + "timeseries": "Time series", + "search-data": "Cerca dati", + "no-data-found": "Nessun dato trovato", + "latest-values": "Ultimi valori", + "rpc": "Control widget", + "alarm": "Alarm widget", + "static": "Static widget", + "select-widget-type": "Seleziona tipo widget", + "missing-widget-title-error": "Il tiolo del widget deve essere specificato!", + "widget-saved": "Widget salvato", + "unable-to-save-widget-error": "Impossibile salvare il widget! Sono presenti degli errori!", + "save": "Salva widget", + "saveAs": "Salva widget come", + "save-widget-type-as": "Salva tipo widget come", + "save-widget-type-as-text": "Please enter new widget title and/or select target widgets bundle", + "toggle-fullscreen": "Commuta modalità schermo intero", + "run": "Esegui widget", + "title": "Titolo widget", + "title-required": "Titolo widget obbligatorio.", + "type": "Tipo widget", + "resources": "Risorse", + "resource-url": "JavaScript/CSS URL", + "remove-resource": "Rimuovi risorsa", + "add-resource": "Aggiungi risorsa", + "html": "HTML", + "tidy": "Tidy", + "css": "CSS", + "settings-schema": "Impostazioni schema", + "datakey-settings-schema": "Impostazioni Data key schema", + "javascript": "Javascript", + "remove-widget-type-title": "Sei sicuro di voler rimuovere il tipo di widget '{{widgetName}}'?", + "remove-widget-type-text": "Dopo la conferma il tipo di widget e tutti i suoi dati non saranno più recuperabili.", + "remove-widget-type": "Rimuovi tipo widget", + "add-widget-type": "Aggiungi nuovo tipo widget", + "widget-type-load-failed-error": "Caricamento tipo widget fallito!", + "widget-template-load-failed-error": "Caricamento template widget fallito!", + "add": "Aggiungi Widget", + "undo": "Annulla modifiche widget", + "export": "Esporta widget" + }, + "widget-action": { + "header-button": "Widget header button", + "open-dashboard-state": "Navigate to new dashboard state", + "update-dashboard-state": "Update current dashboard state", + "open-dashboard": "Navigate to other dashboard", + "custom": "Custom action", + "target-dashboard-state": "Target dashboard state", + "target-dashboard-state-required": "Target dashboard state is required", + "set-entity-from-widget": "Set entity from widget", + "target-dashboard": "Target dashboard", + "open-right-layout": "Open right dashboard layout (mobile view)" + }, + "widgets-bundle": { + "current": "Bundle corrente", + "widgets-bundles": "Bundle Widget", + "add": "Aggiungi Bundle Widget", + "delete": "Cancella bundle widget", + "title": "Titolo", + "title-required": "Titolo obbligatorio.", + "add-widgets-bundle-text": "Aggiungi nuovo bundle widget", + "no-widgets-bundles-text": "Nessun bundle widget trovato", + "empty": "Bundle widget vuoto", + "details": "Dettagli", + "widgets-bundle-details": "Dettagli bundle widget", + "delete-widgets-bundle-title": "Sei sicuro di voler eliminare il bundle widget '{{widgetsBundleTitle}}'?", + "delete-widgets-bundle-text": "Attenzione, dopo la conferma il bundle widget e tutti i suoi dati non saranno più recuperabili.", + "delete-widgets-bundles-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 bundle widget} other {# bundle widget} }?", + "delete-widgets-bundles-action-title": "Elimina { count, plural, 1 {1 bundle widget} other {# bundle widget} }", + "delete-widgets-bundles-text": "Attenzione, dopo la conferma tutti i bundle widget selezionati saranno rimossi e tutti i loro dati non saranno più recuperabili.", + "no-widgets-bundles-matching": "Nessun bundle widget corrispondente a '{{widgetsBundle}}' è stato trovato.", + "widgets-bundle-required": "Bundle widget obbligatorio.", + "system": "Sistema", + "import": "Importa bundle widget", + "export": "Esporta bundle widget", + "export-failed-error": "Impossibile esportare bundle widget: {{error}}", + "create-new-widgets-bundle": "Crea nuovo bundle widget", + "widgets-bundle-file": "File bundle widget", + "invalid-widgets-bundle-file-error": "Impossibile importare bundle widget: struttura dati non valida." + }, + "widget-config": { + "data": "Dati", + "settings": "Impostazioni", + "advanced": "Avanzate", + "title": "Titolo", + "title-tooltip": "Tooltip titolo", + "general-settings": "Impostazioni generali", + "display-title": "Mostra titolo", + "drop-shadow": "Drop shadow", + "enable-fullscreen": "Abilita schermo intero", + "background-color": "Colore sfondo", + "text-color": "Colore testo", + "padding": "Padding", + "margin": "Margin", + "widget-style": "Stile Widget", + "title-style": "Stile titolo", + "mobile-mode-settings": "Impostazioni modalità mobile", + "order": "Ordinamento", + "height": "Altezza", + "units": "Simbolo speciale da mostrare accanto al valore", + "decimals": "Numero di cifre decimali", + "timewindow": "Intervallo temporale", + "use-dashboard-timewindow": "Usa intervallo temporale dashboard", + "display-timewindow": "Mostra intervallo temporale", + "display-legend": "Mostra legenda", + "datasources": "Sorgenti dei dati", + "maximum-datasources": "Massimo { count, plural, 1 {1 sorgente dati consentita.} other {# sorgenti dati consentite} }", + "datasource-type": "Tipo", + "datasource-parameters": "Parametri", + "remove-datasource": "Rimuovi sorgente dati", + "add-datasource": "Aggiungi sorgente dati", + "target-device": "Dispositivo Target", + "alarm-source": "Sorgente Allarme", + "actions": "Azioni", + "action": "Azione", + "add-action": "Aggiungi azione", + "search-actions": "Ricerca azioni", + "action-source": "Sorgente azione", + "action-source-required": "Sorgente azione obbligatoria.", + "action-name": "Nome", + "action-name-required": "Nome azione obbligatorio.", + "action-name-not-unique": "Un'altra azione con lo stesso nome è già presente.
    Il nome di una azione dovrebbe essere univoco all'interno della stessa sorgente.", + "action-icon": "Icona", + "action-type": "Tipo", + "action-type-required": "Tipo azione obbligatorio.", + "edit-action": "Modifica azione", + "delete-action": "Cancella azione", + "delete-action-title": "Cancella azione del widget", + "delete-action-text": "Sei sicuro di voler cancellare l'azione del widget '{{actionName}}'?", + "display-icon": "Mostra icona titolo", + "icon-color": "Colore dell'icona", + "icon-size": "Dimensione dell'icona" + }, + "widget-type": { + "import": "Importa un tipo di widget", + "export": "Esporta un tipo di widget", + "export-failed-error": "Impossibile esportare il tipo di widget: {{error}}", + "create-new-widget-type": "Crea un nuovo tipo di widget", + "widget-type-file": "File tipo di widget", + "invalid-widget-type-file-error": "Impossibile importare un tipo di widget: struttura dati del widget non valida." + }, + "widgets": { + "date-range-navigator": { + "localizationMap": { + "Sun": "Dom", + "Mon": "Lun", + "Tue": "Mar", + "Wed": "Mer", + "Thu": "Gio", + "Fri": "Ven", + "Sat": "Sab", + "Jan": "Gen", + "Feb": "Feb", + "Mar": "Mar", + "Apr": "Apr", + "May": "Mag", + "Jun": "Giu", + "Jul": "Lug", + "Aug": "Ago", + "Sep": "Set", + "Oct": "Ott", + "Nov": "Nov", + "Dec": "Dic", + "January": "Gennaio", + "February": "Febbraio", + "March": "Marzo", + "April": "Aprile", + "June": "Giugno", + "July": "Luglio", + "August": "Agosto", + "September": "Settembre", + "October": "Ottobre", + "November": "Novembre", + "December": "Dicembre", + "Custom Date Range": "Intervallo date personalizzato", + "Date Range Template": "Modello intervallo date", + "Today": "Oggi", + "Yesterday": "Ieri", + "This Week": "Questa settimana", + "Last Week": "La settimana scorsa", + "This Month": "Questo mese", + "Last Month": "Lo scorso mese", + "Year": "Anno", + "This Year": "Quest'anno", + "Last Year": "L'anno scorso", + "Date picker": "Date picker", + "Hour": "Ora", + "Day": "Giorno", + "Week": "Settimana", + "2 weeks": "2 Settimane", + "Month": "Mese", + "3 months": "3 Mesi", + "6 months": "6 Mesi", + "Custom interval": "Intervallo personalizzato", + "Interval": "Intervallo", + "Step size": "Dimensione del passo", + "Ok": "Ok" + } + }, + "input-widgets": { + "attribute-not-allowed": "Questo widget non può usare un parametro di tipo attributo", + "date": "Data", + "discard-changes": "Annulla modifiche", + "entity-attribute-required": "E' richiesta un'entità di tipo attributo", + "entity-timeseries-required": "E' richiesta un'entità di tipo serie temporale", + "not-allowed-entity": "L'entità selezionata non può avere attributi condivisi", + "no-attribute-selected": "Nessun attributo selezionato", + "no-datakey-selected": "Nessuna datakey selezionata", + "no-entity-selected": "Nessuna entità selezionata", + "no-image": "Nessuna immagine", + "no-support-web-camera": "Web camera non supportata", + "no-timeseries-selected": "Nessuna serie temporale selezionata", + "switch-attribute-value": "Cambia il valore dell'attributo", + "switch-camera": "Cambia camera", + "switch-timeseries-value": "Cambia il valore della serie temporale", + "take-photo": "Fai una foto", + "time": "Tempo", + "timeseries-not-allowed": "Questo widget non può usare un parametro di tipo serie temporale", + "update-failed": "Aggiornamento fallito", + "update-successful": "Aggiornamento eseguito con successo", + "update-attribute": "Aggiorna attributo", + "update-timeseries": "Aggiorna serie temporale", + "value": "Valore" + } + }, + "icon": { + "icon": "Icona", + "select-icon": "Seleziona icona", + "material-icons": "Icone Material", + "show-all": "Mostra tutte le icone" + }, + "custom": { + "widget-action": { + "action-cell-button": "Pulsante azione cella", + "row-click": "Click sulla riga", + "polygon-click": "Click sul poligono", + "marker-click": "Click sul marker", + "tooltip-tag-action": "Azione tooltip", + "node-selected": "Click su nodo selezionato", + "element-click": "Click su elemento HTML", + "pie-slice-click": "Click sulla fetta", + "row-double-click": "Doppio click sulla riga" + } + }, + "language": { + "language": "Lingua" + } +} diff --git a/ui-ngx/src/assets/locale/locale.constant-ja_JA.json b/ui-ngx/src/assets/locale/locale.constant-ja_JA.json new file mode 100644 index 0000000000..980daadaa6 --- /dev/null +++ b/ui-ngx/src/assets/locale/locale.constant-ja_JA.json @@ -0,0 +1,1517 @@ +{ + "access": { + "unauthorized": "無許可", + "unauthorized-access": "不正アクセス", + "unauthorized-access-text": "このリソースにアクセスするにはサインインする必要があります。", + "access-forbidden": "アクセス禁止", + "access-forbidden-text": "あなたはこの場所へのアクセス権を持っていません!この場所にアクセスしたい場合は、別のユーザーとサインインしてみてください。", + "refresh-token-expired": "セッションが終了しました", + "refresh-token-failed": "セッションをリフレッシュできません" + }, + "action": { + "activate": "アクティブ化する", + "suspend": "サスペンド", + "save": "セーブ", + "saveAs": "名前を付けて保存", + "cancel": "キャンセル", + "ok": "[OK]", + "delete": "削除", + "add": "追加", + "yes": "はい", + "no": "いいえ", + "update": "更新", + "remove": "削除する", + "search": "サーチ", + "clear-search": "検索をクリアする", + "assign": "割り当てます", + "unassign": "割り当て解除", + "share": "シェア", + "make-private": "プライベートにする", + "apply": "適用", + "apply-changes": "変更を適用する", + "edit-mode": "編集モード", + "enter-edit-mode": "編集モードに入る", + "decline-changes": "変更を拒否する", + "close": "閉じる", + "back": "バック", + "run": "走る", + "sign-in": "サインイン!", + "edit": "編集", + "view": "ビュー", + "create": "作成する", + "drag": "ドラッグ", + "refresh": "リフレッシュ", + "undo": "元に戻す", + "copy": "コピー", + "paste": "ペースト", + "copy-reference": "コピーリファレンス", + "paste-reference": "参照貼り付け", + "import": "インポート", + "export": "輸出する", + "share-via": "{{provider}}" + }, + "aggregation": { + "aggregation": "集約", + "function": "データ集約機能", + "limit": "最大値", + "group-interval": "グループ化の間隔", + "min": "分", + "max": "最大", + "avg": "平均", + "sum": "和", + "count": "カウント", + "none": "なし" + }, + "admin": { + "general": "一般", + "general-settings": "一般設定", + "outgoing-mail": "送信メール", + "outgoing-mail-settings": "送信メールの設定", + "system-settings": "システム設定", + "test-mail-sent": "テストメールが正常に送信されました!", + "base-url": "ベースURL", + "base-url-required": "ベースURLは必須です。", + "mail-from": "メール", + "mail-from-required": "メールの送信元が必要です。", + "smtp-protocol": "SMTPプロトコル", + "smtp-host": "SMTPホスト", + "smtp-host-required": "SMTPホストが必要です。", + "smtp-port": "SMTPポート", + "smtp-port-required": "smtpポートを指定する必要があります。", + "smtp-port-invalid": "それは有効なsmtpポートのようには見えません。", + "timeout-msec": "タイムアウト(ミリ秒)", + "timeout-required": "タイムアウトが必要です。", + "timeout-invalid": "それは有効なタイムアウトのようには見えません。", + "enable-tls": "TLSを有効にする", + "tls-version": "TLSバージョン", + "send-test-mail": "テストメールを送信する" + }, + "alarm": { + "alarm": "警報", + "alarms": "アラーム", + "select-alarm": "アラームを選択", + "no-alarms-matching": "'{{entity}}'発見されました。", + "alarm-required": "アラームが必要です", + "alarm-status": "アラーム状態", + "search-status": { + "ANY": "どれか", + "ACTIVE": "アクティブ", + "CLEARED": "クリアされた", + "ACK": "承認された", + "UNACK": "未確認の" + }, + "display-status": { + "ACTIVE_UNACK": "アクティブ未確認", + "ACTIVE_ACK": "Active Acknowledged", + "CLEARED_UNACK": "クリアされた未確認のメッセージ", + "CLEARED_ACK": "承認された承認済み" + }, + "no-alarms-prompt": "アラームが見つかりません", + "created-time": "作成時刻", + "type": "タイプ", + "severity": "重大度", + "originator": "創始者", + "originator-type": "発信者タイプ", + "details": "詳細", + "status": "状態", + "alarm-details": "アラームの詳細", + "start-time": "始まる時間", + "end-time": "終了時間", + "ack-time": "確認された時間", + "clear-time": "クリアされた時間", + "severity-critical": "クリティカル", + "severity-major": "メジャー", + "severity-minor": "マイナー", + "severity-warning": "警告", + "severity-indeterminate": "不確定", + "acknowledge": "認める", + "clear": "クリア", + "search": "アラームの検索", + "selected-alarms": "{ count, plural, 1 {1 alarm} other {# alarms} }選択された", + "no-data": "表示するデータがありません", + "polling-interval": "アラームポーリング間隔(秒)", + "polling-interval-required": "アラームのポーリング間隔が必要です。", + "min-polling-interval-message": "少なくとも1秒間のポーリング間隔が許可されます。", + "aknowledge-alarms-title": "{ count, plural, 1 {1 alarm} other {# alarms} }", + "aknowledge-alarms-text": "{ count, plural, 1 {1 alarm} other {# alarms} }?", + "clear-alarms-title": "{ count, plural, 1 {1 alarm} other {# alarms} }", + "clear-alarms-text": "{ count, plural, 1 {1 alarm} other {# alarms} }?" + }, + "alias": { + "add": "エイリアスを追加する", + "edit": "エイリアスを編集する", + "name": "エイリアス名", + "name-required": "エイリアス名は必須です", + "duplicate-alias": "同じ名前のエイリアスは既に存在します。", + "filter-type-single-entity": "単一のエンティティ", + "filter-type-entity-list": "エンティティリスト", + "filter-type-entity-name": "エンティティ名", + "filter-type-state-entity": "ダッシュボード状態からのエンティティ", + "filter-type-state-entity-description": "ダッシュボードの状態パラメータから取得されたエンティティ", + "filter-type-asset-type": "資産の種類", + "filter-type-asset-type-description": "'{{assetType}}'", + "filter-type-asset-type-and-name-description": "'{{assetType}}''{{prefix}}'", + "filter-type-device-type": "デバイスタイプ", + "filter-type-device-type-description": "'{{deviceType}}'", + "filter-type-device-type-and-name-description": "'{{deviceType}}''{{prefix}}'", + "filter-type-relations-query": "関係クエリ", + "filter-type-relations-query-description": "{{entities}}{{relationType}}{{direction}}{{rootEntity}}", + "filter-type-asset-search-query": "資産検索クエリ", + "filter-type-asset-search-query-description": "{{assetTypes}}{{relationType}}{{direction}}{{rootEntity}}", + "filter-type-device-search-query": "デバイス検索クエリ", + "filter-type-device-search-query-description": "{{deviceTypes}}{{relationType}}{{direction}}{{rootEntity}}", + "entity-filter": "エンティティフィルタ", + "resolve-multiple": "複数のエンティティとして解決する", + "filter-type": "フィルタタイプ", + "filter-type-required": "フィルタタイプが必要です。", + "entity-filter-no-entity-matched": "指定されたフィルタに一致するエンティティは見つかりませんでした。", + "no-entity-filter-specified": "エンティティフィルタが指定されていない", + "root-state-entity": "ルートとしてダッシュボードの状態エンティティを使用する", + "root-entity": "ルートエンティティ", + "state-entity-parameter-name": "状態エンティティのパラメータ名", + "default-state-entity": "デフォルト状態エンティティ", + "default-entity-parameter-name": "デフォルトでは", + "max-relation-level": "最大関連レベル", + "unlimited-level": "無制限レベル", + "state-entity": "ダッシュボードの状態エンティティ", + "all-entities": "すべてのエンティティ", + "any-relation": "どれか" + }, + "asset": { + "asset": "資産", + "assets": "資産", + "management": "資産運用管理", + "view-assets": "アセットの表示", + "add": "アセットを追加", + "assign-to-customer": "顧客に割り当てる", + "assign-asset-to-customer": "顧客に資産を割り当てる", + "assign-asset-to-customer-text": "顧客に割り当てる資産を選択してください", + "no-assets-text": "アセットが見つかりません", + "assign-to-customer-text": "資産を割り当てる顧客を選択してください", + "public": "パブリック", + "assignedToCustomer": "顧客に割り当てられた", + "make-public": "アセットを公開する", + "make-private": "アセットをプライベートにする", + "unassign-from-customer": "顧客からの割り当て解除", + "delete": "アセットを削除", + "asset-public": "資産は公開されています", + "asset-type": "資産の種類", + "asset-type-required": "資産の種類が必要です。", + "select-asset-type": "アセットタイプを選択", + "enter-asset-type": "アセットタイプを入力", + "any-asset": "すべてのアセット", + "no-asset-types-matching": "'{{entitySubtype}}'発見されました。", + "asset-type-list-empty": "選択されたアセットタイプはありません。", + "asset-types": "資産タイプ", + "name": "名", + "name-required": "名前は必須です。", + "description": "説明", + "type": "タイプ", + "type-required": "タイプが必要です。", + "details": "詳細", + "events": "イベント", + "add-asset-text": "新しいアセットを追加する", + "asset-details": "資産の詳細", + "assign-assets": "アセットの割り当て", + "assign-assets-text": "{ count, plural, 1 {1 asset} other {# assets} }顧客に", + "delete-assets": "アセットを削除する", + "unassign-assets": "アセットの割り当てを解除する", + "unassign-assets-action-title": "{ count, plural, 1 {1 asset} other {# assets} }顧客から", + "assign-new-asset": "新しいアセットを割り当てる", + "delete-asset-title": "'{{assetName}}'?", + "delete-asset-text": "確認後、資産と関連するすべてのデータが回復不能になることに注意してください。", + "delete-assets-title": "{ count, plural, 1 {1 asset} other {# assets} }?", + "delete-assets-action-title": "{ count, plural, 1 {1 asset} other {# assets} }", + "delete-assets-text": "確認後、選択したすべての資産が削除され、関連するすべてのデータは回復不能になりますので注意してください。", + "make-public-asset-title": "'{{assetName}}'パブリック?", + "make-public-asset-text": "確認後、資産とそのすべてのデータは公開され、他の人がアクセスできるようになります。", + "make-private-asset-title": "'{{assetName}}'プライベート?", + "make-private-asset-text": "確認後、資産とそのすべてのデータは非公開にされ、他の人がアクセスすることはできません。", + "unassign-asset-title": "'{{assetName}}'?", + "unassign-asset-text": "確認後、資産は割り当て解除され、顧客はアクセスできなくなります。", + "unassign-asset": "アセットの割り当てを解除する", + "unassign-assets-title": "{ count, plural, 1 {1 asset} other {# assets} }?", + "unassign-assets-text": "確認後、選択されたすべての資産が割り当て解除され、顧客がアクセスできなくなります。", + "copyId": "アセットIDをコピーする", + "idCopiedMessage": "アセットIDがクリップボードにコピーされました", + "select-asset": "アセットを選択", + "no-assets-matching": "'{{entity}}'発見されました。", + "asset-required": "資産が必要です", + "name-starts-with": "アセット名はで始まります", + "label": "ラベル" + }, + "attribute": { + "attributes": "属性", + "latest-telemetry": "最新テレメトリ", + "attributes-scope": "エンティティ属性のスコープ", + "scope-latest-telemetry": "最新テレメトリ", + "scope-client": "クライアントの属性", + "scope-server": "サーバーの属性", + "scope-shared": "共有属性", + "add": "属性を追加する", + "key": "キー", + "last-update-time": "最終更新時間", + "key-required": "属性キーは必須です。", + "value": "値", + "value-required": "属性値は必須です。", + "delete-attributes-title": "{ count, plural, 1 {1 attribute} other {# attributes} }?", + "delete-attributes-text": "注意してください。確認後、選択したすべての属性が削除されます。", + "delete-attributes": "属性を削除する", + "enter-attribute-value": "属性値を入力", + "show-on-widget": "ウィジェットで表示", + "widget-mode": "ウィジェットモード", + "next-widget": "次のウィジェット", + "prev-widget": "前のウィジェット", + "add-to-dashboard": "ダッシュボードに追加", + "add-widget-to-dashboard": "ウィジェットをダッシュ​​ボードに追加する", + "selected-attributes": "{ count, plural, 1 {1 attribute} other {# attributes} }選択された", + "selected-telemetry": "{ count, plural, 1 {1 telemetry unit} other {# telemetry units} }選択された" + }, + "audit-log": { + "audit": "監査", + "audit-logs": "監査ログ", + "timestamp": "タイムスタンプ", + "entity-type": "エンティティタイプ", + "entity-name": "エンティティ名", + "user": "ユーザー", + "type": "タイプ", + "status": "状態", + "details": "詳細", + "type-added": "追加された", + "type-deleted": "削除済み", + "type-updated": "更新しました", + "type-attributes-updated": "属性が更新されました", + "type-attributes-deleted": "属性が削除されました", + "type-rpc-call": "RPC呼び出し", + "type-credentials-updated": "資格が更新されました", + "type-assigned-to-customer": "顧客に割り当てられた", + "type-unassigned-from-customer": "顧客から割り当てられていない", + "type-activated": "活性化", + "type-suspended": "一時停止中", + "type-credentials-read": "信用証明書を読む", + "type-attributes-read": "読み取られた属性", + "type-relation-add-or-update": "関係が更新されました", + "type-relation-delete": "関係が削除されました", + "type-relations-delete": "すべてのリレーションを削除", + "type-alarm-ack": "承認された", + "type-alarm-clear": "クリアされた", + "status-success": "成功", + "status-failure": "失敗", + "audit-log-details": "監査ログの詳細", + "no-audit-logs-prompt": "ログが見つかりません", + "action-data": "行動データ", + "failure-details": "失敗の詳細", + "search": "監査ログの検索", + "clear-search": "検索をクリアする" + }, + "confirm-on-exit": { + "message": "保存されていない変更があります。あなたは本当にこのページを出るのですか?", + "html-message": "保存していない変更があります。
    このページを終了してもよろしいですか?", + "title": "保存されていない変更" + }, + "contact": { + "country": "国", + "city": "シティ", + "state": "州/県", + "postal-code": "郵便番号", + "postal-code-invalid": "無効な郵便番号形式です。", + "address": "住所", + "address2": "アドレス2", + "phone": "電話", + "email": "Eメール", + "no-address": "住所がありません" + }, + "common": { + "username": "ユーザー名", + "password": "パスワード", + "enter-username": "ユーザーネームを入力してください", + "enter-password": "パスワードを入力する", + "enter-search": "検索を入力", + "created-time": "作成時刻" + }, + "content-type": { + "json": "Json", + "text": "テキスト", + "binary": "バイナリ(Base64)" + }, + "customer": { + "customer": "顧客", + "customers": "顧客", + "management": "顧客管理", + "dashboard": "カスタマーダッシュボード", + "dashboards": "カスタマーダッシュボード", + "devices": "顧客デバイス", + "assets": "顧客資産", + "public-dashboards": "パブリックダッシュボード", + "public-devices": "パブリックデバイス", + "public-assets": "公的資産", + "add": "顧客を追加", + "delete": "顧客を削除する", + "manage-customer-users": "顧客ユーザーを管理する", + "manage-customer-devices": "顧客のデバイスを管理する", + "manage-customer-dashboards": "顧客ダッシュボードの管理", + "manage-public-devices": "パブリックデバイスを管理する", + "manage-public-dashboards": "公開ダッシュボードの管理", + "manage-customer-assets": "顧客資産の管理", + "manage-public-assets": "公的資産を管理する", + "add-customer-text": "新規顧客を追加", + "no-customers-text": "顧客が見つかりません", + "customer-details": "お客様情報", + "delete-customer-title": "'{{customerTitle}}'?", + "delete-customer-text": "確認後、お客様および関連するすべてのデータが回復不能になるので注意してください。", + "delete-customers-title": "{ count, plural, 1 {1 customer} other {# customers} }?", + "delete-customers-action-title": "{ count, plural, 1 {1 customer} other {# customers} }", + "delete-customers-text": "確認後、選択したすべての顧客は削除され、関連するすべてのデータは回復不能になります。", + "manage-users": "ユーザーを管理する", + "manage-assets": "アセットを管理する", + "manage-devices": "デバイスを管理する", + "manage-dashboards": "ダッシュボードの管理", + "title": "タイトル", + "title-required": "タイトルは必須です。", + "description": "説明", + "details": "詳細", + "events": "イベント", + "copyId": "顧客IDをコピー", + "idCopiedMessage": "顧客IDがクリップボードにコピーされました", + "select-customer": "顧客を選択", + "no-customers-matching": "'{{entity}}'発見されました。", + "customer-required": "顧客は必須です", + "select-default-customer": "デフォルトの顧客を選択", + "default-customer": "デフォルトの顧客", + "default-customer-required": "テナントレベルのダッシュボードをデバッグするには、デフォルトの顧客が必要です" + }, + "datetime": { + "date-from": "デートから", + "time-from": "からの時間", + "date-to": "日付", + "time-to": "の時間" + }, + "dashboard": { + "dashboard": "ダッシュボード", + "dashboards": "ダッシュボード", + "management": "ダッシュボード管理", + "view-dashboards": "ダッシュボードを表示する", + "add": "ダッシュボードを追加", + "assign-dashboard-to-customer": "顧客にダッシュボードを割り当てる", + "assign-dashboard-to-customer-text": "顧客に割り当てるダッシュボードを選択してください", + "assign-to-customer-text": "ダッシュボードを割り当てる顧客を選択してください", + "assign-to-customer": "顧客に割り当てる", + "unassign-from-customer": "顧客からの割り当て解除", + "make-public": "ダッシュボードを公開する", + "make-private": "ダッシュボードを非公開にする", + "manage-assigned-customers": "割り当てられた顧客を管理する", + "assigned-customers": "割り当てられた顧客", + "assign-to-customers": "顧客にダッシュボードを割り当てる", + "assign-to-customers-text": "ダッシュボードを割り当てる顧客を選択してください", + "unassign-from-customers": "顧客からのダッシュボードの割り当て解除", + "unassign-from-customers-text": "ダッシュボードから割り当て解除する顧客を選択してください", + "no-dashboards-text": "ダッシュボードが見つかりません", + "no-widgets": "ウィジェットは設定されていません", + "add-widget": "新しいウィジェットを追加", + "title": "タイトル", + "select-widget-title": "ウィジェットを選択", + "select-widget-subtitle": "利用可能なウィジェットタイプのリスト", + "delete": "ダッシュボードの削除", + "title-required": "タイトルは必須です。", + "description": "説明", + "details": "詳細", + "dashboard-details": "ダッシュボードの詳細", + "add-dashboard-text": "新しいダッシュボードを追加する", + "assign-dashboards": "ダッシュボードの割り当て", + "assign-new-dashboard": "新しいダッシュボードを割り当てる", + "assign-dashboards-text": "{ count, plural, 1 {1 dashboard} other {# dashboards} }顧客に", + "unassign-dashboards-action-text": "{ count, plural, 1 {1 dashboard} other {# dashboards} }顧客から", + "delete-dashboards": "ダッシュボードの削除", + "unassign-dashboards": "ダッシュボードの割り当てを解除する", + "unassign-dashboards-action-title": "{ count, plural, 1 {1 dashboard} other {# dashboards} }顧客から", + "delete-dashboard-title": "'{{dashboardTitle}}'?", + "delete-dashboard-text": "確認後、ダッシュボードとすべての関連データが回復不能になるので注意してください。", + "delete-dashboards-title": "{ count, plural, 1 {1 dashboard} other {# dashboards} }?", + "delete-dashboards-action-title": "{ count, plural, 1 {1 dashboard} other {# dashboards} }", + "delete-dashboards-text": "注意してください。確認後、選択したダッシュボードはすべて削除され、関連するすべてのデータは回復不能になります。", + "unassign-dashboard-title": "'{{dashboardTitle}}'?", + "unassign-dashboard-text": "確認後、ダッシュボードは割り当てられなくなり、顧客はアクセスできなくなります。", + "unassign-dashboard": "ダッシュボードの割り当てを解除する", + "unassign-dashboards-title": "{ count, plural, 1 {1 dashboard} other {# dashboards} }?", + "unassign-dashboards-text": "確認の後、選択したすべてのダッシュボードは割り当てられなくなり、顧客はアクセスできなくなります。", + "public-dashboard-title": "ダッシュボードは公開されました", + "public-dashboard-text": "{{dashboardTitle}} is now public and accessible via next public link:", + "public-dashboard-notice": "注:データにアクセスするために、関連するデバイスを公開することを忘れないでください。", + "make-private-dashboard-title": "'{{dashboardTitle}}'プライベート?", + "make-private-dashboard-text": "確認の後、ダッシュボードはプライベートにされ、他の人がアクセスすることはできません。", + "make-private-dashboard": "ダッシュボードを非公開にする", + "socialshare-text": "'{{dashboardTitle}}'ThingsBoardを搭載", + "socialshare-title": "'{{dashboardTitle}}'ThingsBoardを搭載", + "select-dashboard": "ダッシュボードを選択", + "no-dashboards-matching": "'{{entity}}'発見されました。", + "dashboard-required": "ダッシュボードが必要です。", + "select-existing": "既存のダッシュボードを選択", + "create-new": "新しいダッシュボードを作成する", + "new-dashboard-title": "新しいダッシュボードのタイトル", + "open-dashboard": "ダッシュボードを開く", + "set-background": "背景を設定する", + "background-color": "背景色", + "background-image": "背景画像", + "background-size-mode": "背景サイズモード", + "no-image": "選択した画像がありません", + "drop-image": "画像をドロップするか、クリックしてアップロードするファイルを選択します。", + "settings": "設定", + "columns-count": "列数", + "columns-count-required": "列数が必要です。", + "min-columns-count-message": "わずか10の最小列数が許可されます。", + "max-columns-count-message": "最大1000の列カウントのみが許可されます。", + "widgets-margins": "ウィジェット間のマージン", + "horizontal-margin": "水平マージン", + "horizontal-margin-required": "水平余白値が必要です。", + "min-horizontal-margin-message": "最小水平マージン値としては0だけが許容されます。", + "max-horizontal-margin-message": "最大水平マージン値は50だけです。", + "vertical-margin": "垂直マージン", + "vertical-margin-required": "垂直マージン値が必要です。", + "min-vertical-margin-message": "最小の垂直マージン値として0のみが許可されます。", + "max-vertical-margin-message": "最大垂直マージン値は50のみです。", + "autofill-height": "自動レイアウトの高さ", + "mobile-layout": "モバイルレイアウトの設定", + "mobile-row-height": "モバイル行の高さ、px", + "mobile-row-height-required": "モバイル行の高さ値が必要です。", + "min-mobile-row-height-message": "最小の行の高さの値として、5ピクセルしか許可されません。", + "max-mobile-row-height-message": "移動可能な行の高さの最大値として許可されるのは200ピクセルだけです。", + "display-title": "ダッシュボードのタイトルを表示する", + "toolbar-always-open": "ツールバーを開いたままにする", + "title-color": "タイトルカラー", + "display-dashboards-selection": "ダッシュボードの選択を表示する", + "display-entities-selection": "エンティティの選択を表示する", + "display-dashboard-timewindow": "タイムウィンドウを表示する", + "display-dashboard-export": "エクスポートの表示", + "import": "インポートダッシュボード", + "export": "エクスポートダッシュボード", + "export-failed-error": "{{error}}", + "create-new-dashboard": "新しいダッシュボードを作成する", + "dashboard-file": "ダッシュボードファイル", + "invalid-dashboard-file-error": "ダッシュボードをインポートできません:ダッシュボードのデータ構造が無効です。", + "dashboard-import-missing-aliases-title": "インポートされたダッシュボードで使用されるエイリアスを設定する", + "create-new-widget": "新しいウィジェットを作成する", + "import-widget": "インポートウィジェット", + "widget-file": "ウィジェットファイル", + "invalid-widget-file-error": "ウィジェットをインポートできません:ウィジェットのデータ構造が無効です。", + "widget-import-missing-aliases-title": "インポートされたウィジェットで使用されるエイリアスを設定する", + "open-toolbar": "ダッシュボードツールバーを開く", + "close-toolbar": "ツールバーを閉じる", + "configuration-error": "設定エラー", + "alias-resolution-error-title": "ダッシュボードエイリアス設定エラー", + "invalid-aliases-config": "エイリアスフィルタの一部に一致するデバイスを見つけることができません。
    この問題を解決するには、管理者に連絡してください。", + "select-devices": "デバイスの選択", + "assignedToCustomer": "顧客に割り当てられた", + "assignedToCustomers": "顧客に割り当てられた", + "public": "パブリック", + "public-link": "パブリックリンク", + "copy-public-link": "パブリックリンクをコピーする", + "public-link-copied-message": "ダッシュボードのパブリックリンクがクリップボードにコピーされました", + "manage-states": "ダッシュボードの状態を管理する", + "states": "ダッシュボードの状態", + "search-states": "検索ダッシュボードの状態", + "selected-states": "{ count, plural, 1 {1 dashboard state} other {# dashboard states} }選択された", + "edit-state": "ダッシュボードの状態を編集する", + "delete-state": "ダッシュボードの状態を削除する", + "add-state": "ダッシュボードの状態を追加する", + "state": "ダッシュボードの状態", + "state-name": "名", + "state-name-required": "ダッシュボードの状態名は必須です。", + "state-id": "状態ID", + "state-id-required": "ダッシュボードの状態IDは必須です。", + "state-id-exists": "同じIDを持つダッシュボードの状態は既に存在します。", + "is-root-state": "ルート状態", + "delete-state-title": "ダッシュボードの状態を削除する", + "delete-state-text": "'{{stateName}}'?", + "show-details": "詳細を表示", + "hide-details": "詳細を隠す", + "select-state": "ターゲット状態を選択する", + "state-controller": "状態コントローラ" + }, + "datakey": { + "settings": "設定", + "advanced": "上級", + "label": "ラベル", + "color": "色", + "units": "値の隣に表示する特別なシンボル", + "decimals": "浮動小数点の後の桁数", + "data-generation-func": "データ生成関数", + "use-data-post-processing-func": "データ後処理機能を使用する", + "configuration": "データキー設定", + "timeseries": "タイムズ", + "attributes": "属性", + "alarm": "アラームフィールド", + "timeseries-required": "エンティティの時系列データが必要です。", + "timeseries-or-attributes-required": "エンティティのtimeseries /属性は必須です。", + "maximum-timeseries-or-attributes": "{ count, plural, 1 {1 timeseries/attribute is allowed.} other {# timeseries/attributes are allowed} }", + "alarm-fields-required": "アラームフィールドが必要です。", + "function-types": "関数型", + "function-types-required": "関数型が必要です。", + "maximum-function-types": "{ count, plural, 1 {1 function type is allowed.} other {# function types are allowed} }" + }, + "datasource": { + "type": "データソースタイプ", + "name": "名", + "add-datasource-prompt": "データソースを追加してください" + }, + "details": { + "edit-mode": "編集モード", + "toggle-edit-mode": "編集モードを切り替える" + }, + "device": { + "device": "デバイス", + "device-required": "デバイスが必要です。", + "devices": "デバイス", + "management": "端末管理", + "view-devices": "デバイスの表示", + "device-alias": "デバイスエイリアス", + "aliases": "デバイスエイリアス", + "no-alias-matching": "'{{alias}}'見つかりません。", + "no-aliases-found": "別名は見つかりませんでした。", + "no-key-matching": "'{{key}}'見つかりません。", + "no-keys-found": "キーが見つかりません。", + "create-new-alias": "新しいものを作成してください!", + "create-new-key": "新しいものを作成してください!", + "duplicate-alias-error": "'{{alias}}'
    デバイスエイリアスは、ダッシュボード内で一意である必要があります。", + "configure-alias": "'{{alias}}'エイリアス", + "no-devices-matching": "'{{entity}}'発見されました。", + "alias": "エイリアス", + "alias-required": "デバイスエイリアスが必要です。", + "remove-alias": "デバイスエイリアスを削除する", + "add-alias": "デバイスエイリアスを追加する", + "name-starts-with": "デバイス名はで始まります", + "device-list": "デバイスリスト", + "use-device-name-filter": "フィルタを使用する", + "device-list-empty": "デバイスが選択されていません。", + "device-name-filter-required": "デバイス名フィルタが必要です。", + "device-name-filter-no-device-matched": "'{{device}}'発見されました。", + "add": "デバイスを追加", + "assign-to-customer": "顧客に割り当てる", + "assign-device-to-customer": "顧客にデバイスを割り当てる", + "assign-device-to-customer-text": "顧客に割り当てるデバイスを選択してください", + "make-public": "端末を公開する", + "make-private": "デバイスを非公開にする", + "no-devices-text": "デバイスが見つかりません", + "assign-to-customer-text": "デバイスを割り当てる顧客を選択してください", + "device-details": "デバイスの詳細", + "add-device-text": "新しいデバイスを追加する", + "credentials": "資格情報", + "manage-credentials": "資格情報を管理する", + "delete": "デバイスを削除する", + "assign-devices": "デバイスを割り当てる", + "assign-devices-text": "{ count, plural, 1 {1 device} other {# devices} }顧客に", + "delete-devices": "デバイスを削除する", + "unassign-from-customer": "顧客からの割り当て解除", + "unassign-devices": "デバイスの割り当てを解除する", + "unassign-devices-action-title": "{ count, plural, 1 {1 device} other {# devices} }顧客から", + "assign-new-device": "新しいデバイスを割り当てる", + "make-public-device-title": "'{{deviceName}}'パブリック?", + "make-public-device-text": "確認後、デバイスとそのすべてのデータは公開され、他のユーザーがアクセスできるようになります。", + "make-private-device-title": "'{{deviceName}}'プライベート?", + "make-private-device-text": "確認後、デバイスとそのすべてのデータは非公開になり、他人がアクセスできなくなります。", + "view-credentials": "資格情報を表示する", + "delete-device-title": "'{{deviceName}}'?", + "delete-device-text": "確認後、デバイスと関連するすべてのデータが回復不能になるので注意してください。", + "delete-devices-title": "{ count, plural, 1 {1 device} other {# devices} }?", + "delete-devices-action-title": "{ count, plural, 1 {1 device} other {# devices} }", + "delete-devices-text": "注意してください。確認後、選択したすべてのデバイスが削除され、関連するすべてのデータは回復不能になります。", + "unassign-device-title": "'{{deviceName}}'?", + "unassign-device-text": "確認の後、デバイスは割り当てが解除され、顧客がアクセスできなくなります。", + "unassign-device": "デバイスの割り当てを解除する", + "unassign-devices-title": "{ count, plural, 1 {1 device} other {# devices} }?", + "unassign-devices-text": "確認の後、選択されたすべてのデバイスが割り当て解除され、顧客がアクセスできなくなります。", + "device-credentials": "デバイス資格情報", + "credentials-type": "資格情報タイプ", + "access-token": "アクセストークン", + "access-token-required": "アクセストークンが必要です。", + "access-token-invalid": "アクセストークンの長さは、1〜20文字でなければなりません。", + "rsa-key": "RSA公開鍵", + "rsa-key-required": "RSA公開鍵が必要です。", + "secret": "秘密", + "secret-required": "秘密が必要です。", + "device-type": "デバイスタイプ", + "device-type-required": "デバイスタイプが必要です。", + "select-device-type": "デバイスタイプを選択", + "enter-device-type": "デバイスタイプを入力", + "any-device": "すべてのデバイス", + "no-device-types-matching": "'{{entitySubtype}}'発見されました。", + "device-type-list-empty": "選択されたデバイスタイプはありません。", + "device-types": "デバイスの種類", + "name": "名", + "name-required": "名前は必須です。", + "description": "説明", + "events": "イベント", + "details": "詳細", + "copyId": "デバイスIDをコピーする", + "copyAccessToken": "コピーアクセストークン", + "idCopiedMessage": "デバイスIDがクリップボードにコピーされました", + "accessTokenCopiedMessage": "デバイスアクセストークンがクリップボードにコピーされました", + "assignedToCustomer": "顧客に割り当てられた", + "unable-delete-device-alias-title": "デバイスエイリアスを削除できません", + "unable-delete-device-alias-text": "'{{deviceAlias}}'{{widgetsList}}", + "is-gateway": "ゲートウェイです", + "public": "パブリック", + "device-public": "デバイスは公開されています", + "select-device": "デバイスの選択" + }, + "dialog": { + "close": "ダイアログを閉じる" + }, + "error": { + "unable-to-connect": "サーバーに接続できません!インターネット接続を確認してください。", + "unhandled-error-code": "{{errorCode}}", + "unknown-error": "不明なエラー" + }, + "entity": { + "entity": "エンティティ", + "entities": "エンティティ", + "aliases": "エンティティエイリアス", + "entity-alias": "エンティティエイリアス", + "unable-delete-entity-alias-title": "エンティティエイリアスを削除できません", + "unable-delete-entity-alias-text": "'{{entityAlias}}'{{widgetsList}}", + "duplicate-alias-error": "'{{alias}}'
    エンティティのエイリアスは、ダッシュボード内で一意である必要があります。", + "missing-entity-filter-error": "'{{alias}}'.", + "configure-alias": "'{{alias}}'エイリアス", + "alias": "エイリアス", + "alias-required": "エンティティエイリアスが必要です。", + "remove-alias": "エンティティエイリアスを削除する", + "add-alias": "エンティティエイリアスを追加する", + "entity-list": "エンティティリスト", + "entity-type": "エンティティタイプ", + "entity-types": "エンティティタイプ", + "entity-type-list": "エンティティタイプリスト", + "any-entity": "任意のエンティティ", + "enter-entity-type": "エンティティタイプを入力", + "no-entities-matching": "'{{entity}}'発見されました。", + "no-entity-types-matching": "'{{entityType}}'発見されました。", + "name-starts-with": "名前はで始まる", + "use-entity-name-filter": "フィルタを使用する", + "entity-list-empty": "選択されたエンティティはありません", + "entity-type-list-empty": "エンティティタイプは選択されていません。", + "entity-name-filter-required": "エンティティ名フィルタが必要です。", + "entity-name-filter-no-entity-matched": "'{{entity}}'発見されました。", + "all-subtypes": "すべて", + "select-entities": "エンティティの選択", + "no-aliases-found": "別名は見つかりませんでした。", + "no-alias-matching": "'{{alias}}'見つかりません。", + "create-new-alias": "新しいものを作成してください!", + "key": "キー", + "key-name": "キー名", + "no-keys-found": "キーが見つかりません。", + "no-key-matching": "'{{key}}'見つかりません。", + "create-new-key": "新しいものを作成してください!", + "type": "タイプ", + "type-required": "エンティティタイプが必要です。", + "type-device": "デバイス", + "type-devices": "デバイス", + "list-of-devices": "{ count, plural, 1 {One device} other {List of # devices} }", + "device-name-starts-with": "'{{prefix}}'", + "type-asset": "資産", + "type-assets": "資産", + "list-of-assets": "{ count, plural, 1 {One asset} other {List of # assets} }", + "asset-name-starts-with": "'{{prefix}}'", + "type-rule": "ルール", + "type-rules": "ルール", + "list-of-rules": "{ count, plural, 1 {One rule} other {List of # rules} }", + "rule-name-starts-with": "'{{prefix}}'", + "type-plugin": "プラグイン", + "type-plugins": "プラグイン", + "list-of-plugins": "{ count, plural, 1 {One plugin} other {List of # plugins} }", + "plugin-name-starts-with": "'{{prefix}}'", + "type-tenant": "テナント", + "type-tenants": "テナント", + "list-of-tenants": "{ count, plural, 1 {One tenant} other {List of # tenants} }", + "tenant-name-starts-with": "'{{prefix}}'", + "type-customer": "顧客", + "type-customers": "顧客", + "list-of-customers": "{ count, plural, 1 {One customer} other {List of # customers} }", + "customer-name-starts-with": "'{{prefix}}'", + "type-user": "ユーザー", + "type-users": "ユーザー", + "list-of-users": "{ count, plural, 1 {One user} other {List of # users} }", + "user-name-starts-with": "'{{prefix}}'", + "type-dashboard": "ダッシュボード", + "type-dashboards": "ダッシュボード", + "list-of-dashboards": "{ count, plural, 1 {One dashboard} other {List of # dashboards} }", + "dashboard-name-starts-with": "'{{prefix}}'", + "type-alarm": "警報", + "type-alarms": "アラーム", + "list-of-alarms": "{ count, plural, 1 {One alarms} other {List of # alarms} }", + "alarm-name-starts-with": "'{{prefix}}'", + "type-rulechain": "ルールチェーン", + "type-rulechains": "ルールチェーン", + "list-of-rulechains": "{ count, plural, 1 {One rule chain} other {List of # rule chains} }", + "rulechain-name-starts-with": "'{{prefix}}'", + "type-rulenode": "ルールノード", + "type-rulenodes": "ルールノード", + "list-of-rulenodes": "{ count, plural, 1 {One rule node} other {List of # rule nodes} }", + "rulenode-name-starts-with": "'{{prefix}}'", + "type-current-customer": "現在の顧客", + "search": "検索エンティティ", + "selected-entities": "{ count, plural, 1 {1 entity} other {# entities} }選択された", + "entity-name": "エンティティ名", + "details": "エンティティの詳細", + "no-entities-prompt": "エンティティが見つかりません", + "no-data": "表示するデータがありません" + }, + "event": { + "event-type": "イベントタイプ", + "type-error": "エラー", + "type-lc-event": "ライフサイクルイベント", + "type-stats": "統計", + "type-debug-rule-node": "デバッグ", + "type-debug-rule-chain": "デバッグ", + "no-events-prompt": "イベントは見つかりませんでした", + "error": "エラー", + "alarm": "警報", + "event-time": "イベント時間", + "server": "サーバ", + "body": "体", + "method": "方法", + "type": "タイプ", + "entity": "エンティティ", + "message-id": "メッセージID", + "message-type": "メッセージタイプ", + "data-type": "データ・タイプ", + "relation-type": "関係タイプ", + "metadata": "メタデータ", + "data": "データ", + "event": "イベント", + "status": "状態", + "success": "成功", + "failed": "失敗", + "messages-processed": "処理されたメッセージ", + "errors-occurred": "エラーが発生しました" + }, + "extension": { + "extensions": "拡張機能", + "selected-extensions": "{ count, plural, 1 {1 extension} other {# extensions} }選択された", + "type": "タイプ", + "key": "キー", + "value": "値", + "id": "イド", + "extension-id": "内線番号", + "extension-type": "拡張タイプ", + "transformer-json": "JSON *", + "unique-id-required": "現在の拡張IDは既に存在します。", + "delete": "拡張子を削除", + "add": "内線番号を追加", + "edit": "拡張機能を編集する", + "delete-extension-title": "'{{extensionId}}'?", + "delete-extension-text": "確認後、拡張子と関連するすべてのデータが回復不能になることに注意してください。", + "delete-extensions-title": "{ count, plural, 1 {1 extension} other {# extensions} }?", + "delete-extensions-text": "注意してください。確認後、選択したすべての内線番号が削除されます。", + "converters": "コンバーター", + "converter-id": "コンバーターID", + "configuration": "構成", + "converter-configurations": "コンバータ構成", + "token": "セキュリティトークン", + "add-converter": "コンバータを追加する", + "add-config": "コンバータ設定を追加する", + "device-name-expression": "デバイス名式", + "device-type-expression": "デバイスタイプの式", + "custom": "カスタム", + "to-double": "ダブル", + "transformer": "トランス", + "json-required": "トランスフォーマーjsonが必要です。", + "json-parse": "変圧器jsonを解析できません。", + "attributes": "属性", + "add-attribute": "属性を追加する", + "add-map": "マッピング要素を追加する", + "timeseries": "タイムズ", + "add-timeseries": "時系列を追加する", + "field-required": "フィールドは必須項目です", + "brokers": "ブローカー", + "add-broker": "ブローカーを追加", + "host": "ホスト", + "port": "ポート", + "port-range": "ポートは1〜65535の範囲内にある必要があります。", + "ssl": "SSL", + "credentials": "資格情報", + "username": "ユーザー名", + "password": "パスワード", + "retry-interval": "ミリ秒単位の再試行間隔", + "anonymous": "匿名", + "basic": "ベーシック", + "pem": "PEM", + "ca-cert": "CA証明書ファイル*", + "private-key": "秘密鍵ファイル*", + "cert": "証明書ファイル*", + "no-file": "ファイルが選択されていません。", + "drop-file": "ファイルをドロップするか、クリックしてアップロードするファイルを選択します。", + "mapping": "マッピング", + "topic-filter": "トピックフィルタ", + "converter-type": "コンバータタイプ", + "converter-json": "Json", + "json-name-expression": "デバイス名json式", + "topic-name-expression": "デバイス名トピック表現", + "json-type-expression": "デバイスタイプjson式", + "topic-type-expression": "デバイスタイプトピック表現", + "attribute-key-expression": "属性キー式", + "attr-json-key-expression": "属性キーjson式", + "attr-topic-key-expression": "属性キートピック式", + "request-id-expression": "要求ID式", + "request-id-json-expression": "リクエストID json式", + "request-id-topic-expression": "リクエストIDトピック表現", + "response-topic-expression": "応答トピック表現", + "value-expression": "値式", + "topic": "トピック", + "timeout": "タイムアウト(ミリ秒)", + "converter-json-required": "コンバータjsonが必要です。", + "converter-json-parse": "コンバータjsonを解析できません。", + "filter-expression": "フィルタ式", + "connect-requests": "接続要求", + "add-connect-request": "接続要求を追加", + "disconnect-requests": "切断要求", + "add-disconnect-request": "切断リクエストを追加する", + "attribute-requests": "属性要求", + "add-attribute-request": "属性要求を追加する", + "attribute-updates": "属性の更新", + "add-attribute-update": "属性の更新を追加する", + "server-side-rpc": "サーバー側RPC", + "add-server-side-rpc-request": "サーバー側RPC要求を追加する", + "device-name-filter": "デバイス名フィルタ", + "attribute-filter": "属性フィルタ", + "method-filter": "方法フィルター", + "request-topic-expression": "トピック表現を要求する", + "response-timeout": "応答タイムアウト(ミリ秒)", + "topic-expression": "トピック表現", + "client-scope": "クライアントスコープ", + "add-device": "デバイスを追加", + "opc-server": "サーバー", + "opc-add-server": "サーバーを追加", + "opc-add-server-prompt": "サーバーを追加してください", + "opc-application-name": "アプリケーション名", + "opc-application-uri": "アプリケーションURI", + "opc-scan-period-in-seconds": "スキャン時間(秒)", + "opc-security": "セキュリティ", + "opc-identity": "身元", + "opc-keystore": "キーストア", + "opc-type": "タイプ", + "opc-keystore-type": "タイプ", + "opc-keystore-location": "ロケーション*", + "opc-keystore-password": "パスワード", + "opc-keystore-alias": "エイリアス", + "opc-keystore-key-password": "キーのパスワード", + "opc-device-node-pattern": "デバイスノードパターン", + "opc-device-name-pattern": "デバイス名パターン", + "modbus-server": "サーバー/スレーブ", + "modbus-add-server": "サーバー/スレーブを追加する", + "modbus-add-server-prompt": "サーバー/スレーブを追加してください", + "modbus-transport": "輸送", + "modbus-port-name": "シリアルポート名", + "modbus-encoding": "エンコーディング", + "modbus-parity": "パリティ", + "modbus-baudrate": "ボーレート", + "modbus-databits": "データビット", + "modbus-stopbits": "ストップビット", + "modbus-databits-range": "データビットは7〜8の範囲内にある必要があります。", + "modbus-stopbits-range": "ストップビットは1〜2の範囲内でなければなりません。", + "modbus-unit-id": "ユニットID", + "modbus-unit-id-range": "ユニットIDは1〜247の範囲で指定してください。", + "modbus-device-name": "装置名", + "modbus-poll-period": "投票期間(ミリ秒)", + "modbus-attributes-poll-period": "属性のポーリング期間(ミリ秒)", + "modbus-timeseries-poll-period": "時系列ポーリング期間(ミリ秒)", + "modbus-poll-period-range": "投票期間は正の値でなければなりません。", + "modbus-tag": "タグ", + "modbus-function": "関数", + "modbus-register-address": "登録アドレス", + "modbus-register-address-range": "レジスタのアドレスは0〜65535の範囲内である必要があります。", + "modbus-register-bit-index": "ビットインデックス", + "modbus-register-bit-index-range": "ビットインデックスは0〜15の範囲内である必要があります。", + "modbus-register-count": "レジスタ数", + "modbus-register-count-range": "レジスタ数は正の値でなければなりません。", + "modbus-byte-order": "バイト順", + "sync": { + "status": "状態", + "sync": "同期", + "not-sync": "同期しない", + "last-sync-time": "前回の同期時間", + "not-available": "利用不可" + }, + "export-extensions-configuration": "エクステンション設定のエクスポート", + "import-extensions-configuration": "エクステンション設定のインポート", + "import-extensions": "拡張機能のインポート", + "import-extension": "インポート拡張", + "export-extension": "輸出延長", + "file": "拡張機能ファイル", + "invalid-file-error": "無効な拡張ファイル" + }, + "fullscreen": { + "expand": "フルスクリーンに拡大", + "exit": "全画面表示を終了", + "toggle": "フルスクリーンモードを切り替える", + "fullscreen": "全画面表示" + }, + "function": { + "function": "関数" + }, + "grid": { + "delete-item-title": "このアイテムを削除してもよろしいですか?", + "delete-item-text": "注意してください。確認後、この項目と関連するすべてのデータは回復不能になります。", + "delete-items-title": "{ count, plural, 1 {1 item} other {# items} }?", + "delete-items-action-title": "{ count, plural, 1 {1 item} other {# items} }", + "delete-items-text": "注意してください。確認後、選択したすべてのアイテムが削除され、関連するすべてのデータは回復不能になります。", + "add-item-text": "新しいアイテムを追加", + "no-items-text": "項目は見つかりませんでした", + "item-details": "商品詳細", + "delete-item": "アイテムを削除", + "delete-items": "アイテムを削除する", + "scroll-to-top": "トップにスクロールします" + }, + "help": { + "goto-help-page": "ヘルプページに行く" + }, + "home": { + "home": "ホーム", + "profile": "プロフィール", + "logout": "ログアウト", + "menu": "メニュー", + "avatar": "アバター", + "open-user-menu": "ユーザーメニューを開く" + }, + "import": { + "no-file": "ファイルが選択されていません", + "drop-file": "JSONファイルをドロップするか、アップロードするファイルをクリックして選択します。" + }, + "item": { + "selected": "選択された" + }, + "js-func": { + "no-return-error": "関数は値を返す必要があります!", + "return-type-mismatch": "'{{type}}'タイプ!", + "tidy": "きちんとした" + }, + "key-val": { + "key": "キー", + "value": "値", + "remove-entry": "エントリを削除", + "add-entry": "エントリを追加", + "no-data": "エントリなし" + }, + "layout": { + "layout": "レイアウト", + "manage": "レイアウトの管理", + "settings": "レイアウト設定", + "color": "色", + "main": "メイン", + "right": "右", + "select": "ターゲットレイアウトを選択" + }, + "legend": { + "position": "伝説の位置", + "show-max": "最大値を表示", + "show-min": "最小値を表示する", + "show-avg": "平均値を表示", + "show-total": "合計値を表示", + "settings": "凡例の設定", + "min": "分", + "max": "最大", + "avg": "平均", + "total": "合計" + }, + "login": { + "login": "ログイン", + "request-password-reset": "リクエストパスワードのリセット", + "reset-password": "パスワードを再設定する", + "create-password": "パスワードの作成", + "passwords-mismatch-error": "入力されたパスワードは同じでなければなりません!", + "password-again": "パスワードをもう一度", + "sign-in": "サインインしてください", + "username": "ユーザー名(電子メール)", + "remember-me": "私を覚えてますか", + "forgot-password": "パスワードをお忘れですか?", + "password-reset": "パスワードのリセット", + "new-password": "新しいパスワード", + "new-password-again": "新しいパスワードを再入力", + "password-link-sent-message": "パスワードリセットリンクが正常に送信されました!", + "email": "Eメール", + "login-with": "{{name}}でログイン", + "or": "または" + }, + "position": { + "top": "上", + "bottom": "ボトム", + "left": "左", + "right": "右" + }, + "profile": { + "profile": "プロフィール", + "change-password": "パスワードを変更する", + "current-password": "現在のパスワード" + }, + "relation": { + "relations": "関係", + "direction": "方向", + "search-direction": { + "FROM": "から", + "TO": "に" + }, + "direction-type": { + "FROM": "から", + "TO": "に" + }, + "from-relations": "アウトバウンド関係", + "to-relations": "インバウンド関係", + "selected-relations": "{ count, plural, 1 {1 relation} other {# relations} }選択された", + "type": "タイプ", + "to-entity-type": "エンティティタイプへ", + "to-entity-name": "エンティティ名に", + "from-entity-type": "エンティティタイプから", + "from-entity-name": "エンティティ名から", + "to-entity": "実体へ", + "from-entity": "エンティティから", + "delete": "関係を削除する", + "relation-type": "関係タイプ", + "relation-type-required": "関係タイプが必要です。", + "any-relation-type": "いかなるタイプ", + "add": "関係を追加する", + "edit": "関係を編集する", + "delete-to-relation-title": "'{{entityName}}'?", + "delete-to-relation-text": "'{{entityName}}'現在のエンティティとは無関係です。", + "delete-to-relations-title": "{ count, plural, 1 {1 relation} other {# relations} }?", + "delete-to-relations-text": "注意してください。確認後、選択されたリレーションはすべて削除され、対応するエンティティは現在のエンティティとは無関係になります。", + "delete-from-relation-title": "'{{entityName}}'?", + "delete-from-relation-text": "'{{entityName}}'.", + "delete-from-relations-title": "{ count, plural, 1 {1 relation} other {# relations} }?", + "delete-from-relations-text": "注意してください。確認後、選択されたリレーションはすべて削除され、現在のエンティティは対応するエンティティとは無関係になります。", + "remove-relation-filter": "関係フィルタを削除する", + "add-relation-filter": "関係フィルタを追加する", + "any-relation": "関係", + "relation-filters": "関係フィルタ", + "additional-info": "追加情報(JSON)", + "invalid-additional-info": "追加情報jsonを解析できません。" + }, + "rulechain": { + "rulechain": "ルールチェーン", + "rulechains": "ルールチェーン", + "root": "ルート", + "delete": "ルールチェーンの削除", + "name": "名", + "name-required": "名前は必須です。", + "description": "説明", + "add": "ルールチェーンを追加する", + "set-root": "ルールチェーンのルートを作る", + "set-root-rulechain-title": "'{{ruleChainName}}'ルート?", + "set-root-rulechain-text": "確認後、ルールチェーンはルートになり、すべての受信トランスポートメッセージを処理します。", + "delete-rulechain-title": "'{{ruleChainName}}'?", + "delete-rulechain-text": "確認後、ルールチェーンと関連するすべてのデータが回復不能になるので注意してください。", + "delete-rulechains-title": "{ count, plural, 1 {1 rule chain} other {# rule chains} }?", + "delete-rulechains-action-title": "{ count, plural, 1 {1 rule chain} other {# rule chains} }", + "delete-rulechains-text": "確認後、選択したすべてのルールチェーンが削除され、関連するすべてのデータが回復不能になるので注意してください。", + "add-rulechain-text": "新しいルールチェーンを追加する", + "no-rulechains-text": "ルールチェーンが見つかりません", + "rulechain-details": "ルールチェーンの詳細", + "details": "詳細", + "events": "イベント", + "system": "システム", + "import": "ルールチェーンのインポート", + "export": "ルールチェーンのエクスポート", + "export-failed-error": "{{error}}", + "create-new-rulechain": "新しいルールチェーンを作成する", + "rulechain-file": "ルールチェーンファイル", + "invalid-rulechain-file-error": "ルールチェーンをインポートできません:ルールチェーンのデータ構造が無効です。", + "copyId": "ルールチェーンIDのコピー", + "idCopiedMessage": "ルールチェーンIDがクリップボードにコピーされました", + "select-rulechain": "ルールチェーンの選択", + "no-rulechains-matching": "'{{entity}}'発見されました。", + "rulechain-required": "ルールチェーンが必要です", + "management": "ルール管理", + "debug-mode": "デバッグモード" + }, + "rulenode": { + "details": "詳細", + "events": "イベント", + "search": "検索ノード", + "open-node-library": "オープンノードライブラリ", + "add": "ルールノードを追加する", + "name": "名", + "name-required": "名前は必須です。", + "type": "タイプ", + "description": "説明", + "delete": "ルールノードを削除", + "select-all-objects": "すべてのノードと接続を選択する", + "deselect-all-objects": "すべてのノードと接続の選択を解除する", + "delete-selected-objects": "選択したノードと接続を削除する", + "delete-selected": "選択を削除します", + "select-all": "すべて選択", + "copy-selected": "選択したコピー", + "deselect-all": "すべての選択を解除", + "rulenode-details": "ルールノードの詳細", + "debug-mode": "デバッグモード", + "configuration": "構成", + "link": "リンク", + "link-details": "ルールノードのリンクの詳細", + "add-link": "リンクを追加", + "link-label": "リンクラベル", + "link-label-required": "リンクラベルが必要です。", + "custom-link-label": "カスタムリンクラベル", + "custom-link-label-required": "カスタムリンクラベルが必要です。", + "link-labels": "リンクラベル", + "link-labels-required": "リンクラベルが必要です。", + "no-link-labels-found": "リンクラベルが見つかりません", + "no-link-label-matching": "'{{label}}'見つかりません。", + "create-new-link-label": "新しいものを作成してください!", + "type-filter": "フィルタ", + "type-filter-details": "設定された条件で着信メッセージをフィルタリングする", + "type-enrichment": "豊かな", + "type-enrichment-details": "メッセージメタデータに追加情報を追加する", + "type-transformation": "変換", + "type-transformation-details": "メッセージペイロードとメタデータの変更", + "type-action": "アクション", + "type-action-details": "特別なアクションを実行する", + "type-external": "外部", + "type-external-details": "外部システムとの相互作用", + "type-rule-chain": "ルールチェーン", + "type-rule-chain-details": "受信したメッセージを指定したルールチェーンに転送する", + "type-input": "入力", + "type-input-details": "ルールチェーンの論理入力、次の関連ルールノードへの着信メッセージの転送", + "type-unknown": "未知の", + "type-unknown-details": "未解決のルールノード", + "directive-is-not-loaded": "'{{directiveName}}'利用できません。", + "ui-resources-load-error": "構成UIリソースをロードできませんでした。", + "invalid-target-rulechain": "ターゲットルールチェーンを解決できません!", + "test-script-function": "テストスクリプト機能", + "message": "メッセージ", + "message-type": "メッセージタイプ", + "select-message-type": "メッセージタイプを選択", + "message-type-required": "メッセージタイプは必須です", + "metadata": "メタデータ", + "metadata-required": "メタデータのエントリを空にすることはできません。", + "output": "出力", + "test": "テスト", + "help": "助けて" + }, + "tenant": { + "tenant": "テナント", + "tenants": "テナント", + "management": "テナント管理", + "add": "テナントを追加", + "admins": "管理者", + "manage-tenant-admins": "テナント管理者の管理", + "delete": "テナントの削除", + "add-tenant-text": "新しいテナントを追加する", + "no-tenants-text": "テナントは見つかりませんでした", + "tenant-details": "テナントの詳細", + "delete-tenant-title": "'{{tenantTitle}}'?", + "delete-tenant-text": "確認後、テナントと関連するすべてのデータが回復不能になるので注意してください。", + "delete-tenants-title": "{ count, plural, 1 {1 tenant} other {# tenants} }?", + "delete-tenants-action-title": "{ count, plural, 1 {1 tenant} other {# tenants} }", + "delete-tenants-text": "注意してください。確認後、選択されたすべてのテナントが削除され、関連するすべてのデータは回復不能になります。", + "title": "タイトル", + "title-required": "タイトルは必須です。", + "description": "説明", + "details": "詳細", + "events": "イベント", + "copyId": "テナントIDをコピーする", + "idCopiedMessage": "テナントIDがクリップボードにコピーされました", + "select-tenant": "テナントを選択", + "no-tenants-matching": "'{{entity}}'発見されました。", + "tenant-required": "テナントが必要です" + }, + "timeinterval": { + "seconds-interval": "{ seconds, plural, 1 {1 second} other {# seconds} }", + "minutes-interval": "{ minutes, plural, 1 {1 minute} other {# minutes} }", + "hours-interval": "{ hours, plural, 1 {1 hour} other {# hours} }", + "days-interval": "{ days, plural, 1 {1 day} other {# days} }", + "days": "日々", + "hours": "時間", + "minutes": "分", + "seconds": "秒", + "advanced": "上級" + }, + "timewindow": { + "days": "{ days, plural, 1 { day } other {# days } }", + "hours": "{ hours, plural, 0 { hour } 1 {1 hour } other {# hours } }", + "minutes": "{ minutes, plural, 0 { minute } 1 {1 minute } other {# minutes } }", + "seconds": "{ seconds, plural, 0 { second } 1 {1 second } other {# seconds } }", + "realtime": "リアルタイム", + "history": "歴史", + "last-prefix": "最終", + "period": "{{ startTime }}{{ endTime }}", + "edit": "タイムウィンドウを編集", + "date-range": "期間", + "last": "最終", + "time-period": "期間" + }, + "user": { + "user": "ユーザー", + "users": "ユーザー", + "customer-users": "顧客ユーザー", + "tenant-admins": "テナント管理者", + "sys-admin": "システム管理者", + "tenant-admin": "テナント管理者", + "customer": "顧客", + "anonymous": "匿名", + "add": "ユーザーを追加する", + "delete": "ユーザーを削除", + "add-user-text": "新しいユーザーを追加", + "no-users-text": "ユーザが見つかりませんでした", + "user-details": "ユーザーの詳細", + "delete-user-title": "'{{userEmail}}'?", + "delete-user-text": "確認後、ユーザーと関連するすべてのデータが回復不能になるので注意してください。", + "delete-users-title": "{ count, plural, 1 {1 user} other {# users} }?", + "delete-users-action-title": "{ count, plural, 1 {1 user} other {# users} }", + "delete-users-text": "注意してください。確認後、選択したすべてのユーザーが削除され、関連するすべてのデータは回復不能になります。", + "activation-email-sent-message": "アクティベーション電子メールが正常に送信されました!", + "resend-activation": "アクティブ化を再送", + "email": "Eメール", + "email-required": "電子メールが必要です。", + "invalid-email-format": "メールフォーマットが無効です。", + "first-name": "ファーストネーム", + "last-name": "苗字", + "description": "説明", + "default-dashboard": "デフォルトのダッシュボード", + "always-fullscreen": "常に全画面表示", + "select-user": "ユーザーを選択", + "no-users-matching": "'{{entity}}'発見されました。", + "user-required": "ユーザーは必須です", + "activation-method": "起動方法", + "display-activation-link": "アクティブ化リンクを表示する", + "send-activation-mail": "アクティベーションメールを送信する", + "activation-link": "ユーザーアクティベーションリンク", + "activation-link-text": "activation link :", + "copy-activation-link": "アクティブ化リンクをコピーする", + "activation-link-copied-message": "ユーザーのアクティベーションリンクがクリップボードにコピーされました", + "details": "詳細" + }, + "value": { + "type": "値のタイプ", + "string": "文字列", + "string-value": "文字列値", + "integer": "整数", + "integer-value": "整数値", + "invalid-integer-value": "整数値が無効です", + "double": "ダブル", + "double-value": "二重価値", + "boolean": "ブール", + "boolean-value": "ブール値", + "false": "偽", + "true": "真", + "long": "長いです" + }, + "widget": { + "widget-library": "ウィジェットライブラリ", + "widget-bundle": "ウィジェットバンドル", + "select-widgets-bundle": "ウィジェットのバンドルを選択", + "management": "ウィジェット管理", + "editor": "ウィジェットエディタ", + "widget-type-not-found": "ウィジェットの設定を読み込む際に問題が発生しました。
    おそらく関連付けられているウィジェットのタイプが削除されています。", + "widget-type-load-error": "次のエラーのためにウィジェットが読み込まれませんでした:", + "remove": "ウィジェットを削除", + "edit": "ウィジェットの編集", + "remove-widget-title": "'{{widgetTitle}}'?", + "remove-widget-text": "確認後、ウィジェットと関連するすべてのデータは回復不能になります。", + "timeseries": "時系列", + "search-data": "検索データ", + "no-data-found": "何もデータが見つかりませんでした", + "latest-values": "最新の値", + "rpc": "コントロールウィジェット", + "alarm": "アラームウィジェット", + "static": "静的ウィジェット", + "select-widget-type": "ウィジェットタイプを選択", + "missing-widget-title-error": "ウィジェットのタイトルを指定する必要があります!", + "widget-saved": "ウィジェットが保存されました", + "unable-to-save-widget-error": "ウィジェットを保存できません!ウィジェットにエラーがあります!", + "save": "ウィジェットを保存", + "saveAs": "ウィジェットを次のように保存する", + "save-widget-type-as": "ウィジェットタイプを次のように保存します", + "save-widget-type-as-text": "新しいウィジェットのタイトルを入力したり、ターゲットウィジェットのバンドルを選択してください", + "toggle-fullscreen": "フルスクリーン切り替え", + "run": "ウィジェットを実行する", + "title": "ウィジェットのタイトル", + "title-required": "ウィジェットのタイトルが必要です。", + "type": "ウィジェットタイプ", + "resources": "リソース", + "resource-url": "JavaScript / CSS URL", + "remove-resource": "リソースを削除する", + "add-resource": "リソースを追加", + "html": "HTML", + "tidy": "きちんとした", + "css": "CSS", + "settings-schema": "設定スキーマ", + "datakey-settings-schema": "データキー設定のスキーマ", + "javascript": "Javascript", + "remove-widget-type-title": "'{{widgetName}}'?", + "remove-widget-type-text": "確認後、ウィジェットのタイプと関連するすべてのデータは回復不能になります。", + "remove-widget-type": "ウィジェットタイプを削除", + "add-widget-type": "新しいウィジェットタイプを追加する", + "widget-type-load-failed-error": "ウィジェットタイプの読み込みに失敗しました!", + "widget-template-load-failed-error": "ウィジェットテンプレートを読み込めませんでした!", + "add": "ウィジェットを追加", + "undo": "ウィジェットの変更を元に戻す", + "export": "ウィジェットの書き出し" + }, + "widget-action": { + "header-button": "ウィジェットのヘッダーボタン", + "open-dashboard-state": "新しいダッシュボードの状態に移動する", + "update-dashboard-state": "現在のダッシュボードの状態を更新する", + "open-dashboard": "他のダッシュボードに移動する", + "custom": "カスタムアクション", + "target-dashboard-state": "ターゲットダッシュボードの状態", + "target-dashboard-state-required": "ターゲットダッシュボードの状態が必要です", + "set-entity-from-widget": "エンティティをウィジェットから設定する", + "target-dashboard": "ターゲットダッシュボード", + "open-right-layout": "右ダッシュボードレイアウトを開く(モバイルビュー)" + }, + "widgets-bundle": { + "current": "現在のバンドル", + "widgets-bundles": "ウィジェットバンドル", + "add": "ウィジェットのバンドルを追加", + "delete": "ウィジェットのバンドルを削除する", + "title": "タイトル", + "title-required": "タイトルは必須です。", + "add-widgets-bundle-text": "新しいウィジェットのバンドルを追加する", + "no-widgets-bundles-text": "ウィジェットバンドルが見つかりません", + "empty": "ウィジェットのバンドルが空です", + "details": "詳細", + "widgets-bundle-details": "ウィジェットのバンドルの詳細", + "delete-widgets-bundle-title": "'{{widgetsBundleTitle}}'?", + "delete-widgets-bundle-text": "確認後、ウィジェットはバンドルされ、関連するすべてのデータは回復不能になります。", + "delete-widgets-bundles-title": "{ count, plural, 1 {1 widgets bundle} other {# widgets bundles} }?", + "delete-widgets-bundles-action-title": "{ count, plural, 1 {1 widgets bundle} other {# widgets bundles} }", + "delete-widgets-bundles-text": "確認後、選択したすべてのウィジェットバンドルは削除され、関連するすべてのデータは回復不能になります。", + "no-widgets-bundles-matching": "'{{widgetsBundle}}'発見されました。", + "widgets-bundle-required": "ウィジェットバンドルが必要です。", + "system": "システム", + "import": "インポートウィジェットバンドル", + "export": "ウィジェットのエクスポートバンドル", + "export-failed-error": "{{error}}", + "create-new-widgets-bundle": "新しいウィジェットバンドルを作成する", + "widgets-bundle-file": "ウィジェットのバンドルファイル", + "invalid-widgets-bundle-file-error": "ウィジェットをインポートできません。bundle:データ構造が無効です。" + }, + "widget-config": { + "data": "データ", + "settings": "設定", + "advanced": "上級", + "title": "タイトル", + "general-settings": "一般設定", + "display-title": "タイトルを表示", + "drop-shadow": "影を落とす", + "enable-fullscreen": "フルスクリーンを有効にする", + "background-color": "背景色", + "text-color": "テキストの色", + "padding": "パディング", + "margin": "マージン", + "widget-style": "ウィジェットスタイル", + "title-style": "タイトルスタイル", + "mobile-mode-settings": "モバイルモードの設定", + "order": "注文", + "height": "高さ", + "units": "値の隣に表示する特別なシンボル", + "decimals": "浮動小数点の後の桁数", + "timewindow": "タイムウィンドウ", + "use-dashboard-timewindow": "ダッシュボードのタイムウィンドウを使用する", + "display-legend": "伝説を表示", + "datasources": "データソース", + "maximum-datasources": "{ count, plural, 1 {1 datasource is allowed.} other {# datasources are allowed} }", + "datasource-type": "タイプ", + "datasource-parameters": "パラメーター", + "remove-datasource": "データソースを削除", + "add-datasource": "データソースを追加", + "target-device": "ターゲットデバイス", + "alarm-source": "アラームソース", + "actions": "行動", + "action": "アクション", + "add-action": "アクションを追加", + "search-actions": "検索アクション", + "action-source": "アクションソース", + "action-source-required": "アクションソースが必要です。", + "action-name": "名", + "action-name-required": "アクション名は必須です。", + "action-name-not-unique": "同じ名前の別のアクションがすでに存在します。
    アクション名は、同じアクションソース内で一意である必要があります。", + "action-icon": "アイコン", + "action-type": "タイプ", + "action-type-required": "アクションタイプが必要です。", + "edit-action": "アクションの編集", + "delete-action": "アクションの削除", + "delete-action-title": "ウィジェットアクションを削除する", + "delete-action-text": "'{{actionName}}'?" + }, + "widget-type": { + "import": "インポートウィジェットタイプ", + "export": "ウィジェットのタイプをエクスポートする", + "export-failed-error": "{{error}}", + "create-new-widget-type": "新しいウィジェットタイプを作成する", + "widget-type-file": "ウィジェットタイプファイル", + "invalid-widget-type-file-error": "ウィジェットタイプをインポートできません:ウィジェットタイプのデータ構造が無効です。" + }, + "widgets": { + "date-range-navigator": { + "localizationMap": { + "Sun": "日", + "Mon": "月", + "Tue": "火", + "Wed": "水", + "Thu": "木", + "Fri": "金", + "Sat": "土", + "Jan": "1月", + "Feb": "2月", + "Mar": "3月", + "Apr": "4月", + "May": "5月", + "Jun": "6月", + "Jul": "7月", + "Aug": "8月", + "Sep": "9月", + "Oct": "10月", + "Nov": "11月", + "Dec": "12月", + "January": "1月", + "February": "2月", + "March": "行進", + "April": "4月", + "June": "六月", + "July": "7月", + "August": "8月", + "September": "9月", + "October": "10月", + "November": "11月", + "December": "12月", + "Custom Date Range": "カスタム期間", + "Date Range Template": "日付範囲テンプレート", + "Today": "今日", + "Yesterday": "昨日", + "This Week": "今週", + "Last Week": "先週", + "This Month": "今月", + "Last Month": "先月", + "Year": "年", + "This Year": "今年", + "Last Year": "昨年", + "Date picker": "日付ピッカー", + "Hour": "時", + "Day": "日", + "Week": "週間", + "2 weeks": "2週間", + "Month": "月", + "3 months": "3ヶ月", + "6 months": "6ヵ月", + "Custom interval": "カスタム間隔", + "Interval": "間隔", + "Step size": "刻み幅", + "Ok": "Ok" + } + } + }, + "icon": { + "icon": "アイコン", + "select-icon": "選択アイコン", + "material-icons": "マテリアルアイコン", + "show-all": "すべてのアイコンを表示する" + }, + "custom": { + "widget-action": { + "action-cell-button": "アクションセルボタン", + "row-click": "行のクリック", + "polygon-click": "ポリゴンクリック", + "marker-click": "マーカークリック", + "tooltip-tag-action": "ツールチップのタグアクション" + } + }, + "language": { + "language": "言語" + } +} diff --git a/ui-ngx/src/assets/locale/locale.constant-ka_GE.json b/ui-ngx/src/assets/locale/locale.constant-ka_GE.json new file mode 100644 index 0000000000..d9713d3419 --- /dev/null +++ b/ui-ngx/src/assets/locale/locale.constant-ka_GE.json @@ -0,0 +1,1814 @@ +{ + "access": { + "unauthorized": "არა ავტორიზირებული", + "unauthorized-access": "უნებართვო წვდომა", + "unauthorized-access-text": "უნებართვო წვდომის ტექსტი", + "access-forbidden": "შესვლა აკრძალულია", + "access-forbidden-text": "თქვენ არ გაქვთ ამ რესურსზე წვდომის უფლებები!
    წვდომის მისაღებად, შეეცადეთ შეხვიდეთ როგორც სხვა მომხმარებელი.", + "refresh-token-expired": "სესია ამოიწურა", + "refresh-token-failed": "სესიის განახლება ვერ მოხერხდა", + "permission-denied": "წვდომა აკრძალულია", + "permission-denied-text": "თქვენ არ გაქვთ უფლება შეასრულოთ აღნიშნული ოპერაცია" + }, + "action": { + "activate": "გააქტიურება", + "suspend": "შეაჩერე", + "save": "შენახვა", + "saveAs": "შეინახე როგორც", + "cancel": "გაუქმება", + "ok": "კარგი", + "delete": "წაშლა", + "add": "დამატება", + "yes": "დიახ", + "no": "არა", + "update": "განახლება", + "remove": "წაშლა", + "select": "შერჩევა", + "search": "ძებნა", + "clear-search": "გასუფთავება", + "assign": "მინიჭება", + "unassign": "მოხსნა", + "share": "გაზიარება", + "make-private": "გახადე პრივატული", + "apply": "დამახსოვრება", + "apply-changes": "ცვლილების დამახსოვრება", + "edit-mode": "რედაქტირების რეჟიმში", + "enter-edit-mode": "რედაქტირების რეჟიმში შესვლა", + "decline-changes": "ცვლილებების გაუქმება", + "close": "დახურვა", + "back": "უკან", + "run": "გაშვება", + "sign-in": "შესვლა", + "edit": "რედაქტირება", + "view": "ხედი", + "create": "შექმნა", + "drag": "გადაათრიეთ", + "refresh": "განახლება", + "undo": "დაბრუნება", + "copy": "კოპირება", + "paste": "ჩასმა", + "copy-reference": "მისმართის კომპირება", + "paste-reference": "მისამართის ჩასმა", + "import": "იმპორტი", + "export": "ექსპორტი", + "share-via": "გაზიარება როგორც {{provider}}", + "continue": "გაგრძელება", + "discard-changes": "ცვლილებების გაუქმება" + }, + "aggregation": { + "aggregation": "აგრეგაცია", + "function": "ფუნქცია", + "limit": "ზღვარი", + "group-interval": "ჯგუფური ინტერვალი", + "min": "წთ", + "max": "მაქ", + "avg": "საშუალო", + "sum": "ჯამი", + "count": "რაოდენობა", + "none": "არცერთი" + }, + "admin": { + "general": "ზოგადი", + "general-settings": "ძირითადი პარამეტრები", + "outgoing-mail": "გამავალი მეილი", + "outgoing-mail-settings": "გამავალი ფოსტის-პარამეტრები", + "system-settings": "სისტემის პარამეტრები", + "test-mail-sent": "სატესტო მეილი გაგზავნილია", + "base-url": "ბაზა-url", + "base-url-required": "ბაზა-url-აუცილებელია", + "mail-from": "გამგზავნი", + "mail-from-required": "გამგზავნის ველი აუცილებელია", + "smtp-protocol": "smtp- პროტოკოლი", + "smtp-host": "smtp- ჰოსთი", + "smtp-host-required": "smtp- ჰოსთი აუციელებელია", + "smtp-port": "smtp- პორტი", + "smtp-port-required": "smtp- პორტი აუციელებელია", + "smtp-port-invalid": "smtp-პორტი არასწორია", + "timeout-msec": "ტაიმაუტი (მ/წ)", + "timeout-required": "ტაიმაუტი აუცილებელია", + "timeout-invalid": "არასწორი ტაიმაუტის დრო", + "enable-tls": "TLS-ის ჩართვა", + "send-test-mail": "სატესტო წერილის გაგზავნა", + "security-settings": "უსაფრთხოების პარამეტრები", + "password-policy": "პაროლის პოლიტიკა", + "minimum-password-length": "მინიმალური პაროლის სიგრძე", + "minimum-password-length-required": "პაროლის მინიმალური ზომა", + "minimum-password-length-range": "პაროლის სიგრძის მინიმალური დიაპაზონი", + "minimum-uppercase-letters": "მინიმალური-დიდი ასოები", + "minimum-uppercase-letters-range": "გამოიყენეთ მინიმუმ 1 დიდი ასო", + "minimum-lowercase-letters": "მინიმალური-მცირე ასოები", + "minimum-lowercase-letters-range": "მინიმალური-მცირე ასოების დიაპაზონი", + "minimum-digits": "ციფრების მინიმალური რაოდენობა", + "minimum-digits-range": "ციფრების მინიმალური დიაპაზონი", + "minimum-special-characters": "სიმბოლოების მინიმალური რაოდენობა", + "minimum-special-characters-range": "სიმბოლოების რაოდენობა არ შეიძლება იყოს ნეგატიური", + "password-expiration-period-days": "პაროლის ვადა", + "password-expiration-period-days-range": "პაროლის ვადა არ შეიძლება იყოს უარყოფითი", + "password-reuse-frequency-days": "პაროლის განმეორებით გამოყენების სიხშირე (დღე)", + "password-reuse-frequency-days-range": "პაროლის განმეორებით გამოყენების სიხშირე არ შეიძლება იყოს ნეგატიური", + "general-policy": "ზოგადი პოლიტიკა", + "max-failed-login-attempts": "მაქსიმალური შესვლის მცდელობები", + "minimum-max-failed-login-attempts-range": "მაქსიმალური შესვლის მცდელობების რაოდენობა არ შეიძლება იყოს ნეგატიური", + "user-lockout-notification-email": "თუ დაგებლოკათ ანგარიში გააგზავნეთ ნოთიფიკაცია მეილზე" + }, + "alarm": { + "alarm": "განგაში", + "alarms": "განგაშები", + "select-alarm": "აირჩიე განგაში", + "no-alarms-matching": "შესატყვისი განგაში '{{entity}}' ვერ მოიძებნა.", + "alarm-required": "საჭიროა განგაში", + "alarm-status": "განგაშის სტატუსი", + "search-status": { + "ANY": "ნებისმიერი", + "ACTIVE": "აქტიური", + "CLEARED": "გასუფთავებული", + "ACK": "დასტური", + "UNACK": "დაუდასტურებელი" + }, + "display-status": { + "ACTIVE_UNACK": "აქტიური დაუდასტურებელი", + "ACTIVE_ACK": "აქტიური დადასტურებული", + "CLEARED_UNACK": "გასუფთავება დაუდასტურებელი", + "CLEARED_ACK": "გასუფთავება_დადასტურებული" + }, + "no-alarms-prompt": "განგაში არ არსებობს", + "created-time": "შექმნის დრო", + "type": "ტიპი", + "severity": "დონე", + "originator": "ინიციატორი", + "originator-type": "ინიციატორის ტიპი", + "details": "დეტალები", + "status": "სტატუსი", + "alarm-details": "განგაშის დეტალები", + "start-time": "დაწყების დრო", + "end-time": "დასრულების დრო", + "ack-time": "დადასტურების დრო", + "clear-time": "გასუფთავების დრო", + "severity-critical": "სიმძიმე-კრიტიკული", + "severity-major": "სიმძიმე-დიდი", + "severity-minor": "სიმძიმე-მცირე", + "severity-warning": "სიმძიმის გაფრთხილება", + "severity-indeterminate": "სიმძიმე-განუსაზღვრელი", + "acknowledge": "დადასტურება", + "clear": "გასუფთავება", + "search": "ძიება", + "selected-alarms": "შერჩეული განგაშები", + "no-data": "მონაცემები არ არის", + "polling-interval": "კითხვის ინტერვალი", + "polling-interval-required": "კითხვის ინტერვალი აუცილებელია", + "min-polling-interval-message": "გამოთხვისი მინიმალური ინტერვალი", + "aknowledge-alarms-title": "გაფრთხილების დასტური სათაური", + "aknowledge-alarms-text": "დარწმუნებული ხართ რომ გინდათ დაადასტუროთ { count, plural, 1 {1 alarm} other {# alarms} }?", + "aknowledge-alarm-title": "დაადასტურე გაფრთხილება", + "aknowledge-alarm-text": "გაფრთხილების დადასტურება", + "clear-alarms-title": "წაშლა { count, plural, 1 {1 alarm} other {# alarms} }", + "clear-alarms-text": "დარწმუნებული ხართ რომ გინდათ წაშალოთ { count, plural, 1 {1 alarm} other {# alarms} }?", + "clear-alarm-title": "გართხილების სათაურის წაშლა", + "clear-alarm-text": "ნამდვილად გინდათ გაფრთხილების წაშლა", + "alarm-status-filter": "განგაშის სტატუსის ფილტრი", + "max-count-load": "გაფრთხილების მაქსიმალური რაოდენობა (0-შეუზღუდავი)", + "max-count-load-required": "გაფრთხილების მაქსიმალური რაოდენობა აუცილებელია", + "max-count-load-error-min": "მინიმალური მნიშვნელობა 0", + "fetch-size": "ჩასატვირთი პაკეტის ზომა", + "fetch-size-required": "მიუთითეთ ჩასატვირთი პაკეტის ზომა", + "fetch-size-error-min": "ჩასატვირთი პაკეტის -შეცდომა-წთ" + }, + "alias": { + "add": "დამატება", + "edit": "რედაქტირება", + "name": "სახელი", + "name-required": "სახელი აუცილებელია", + "duplicate-alias": "ასეთი ჩანაწერი არსებობს", + "filter-type-single-entity": "ფილტრის ტიპის ერთეული", + "filter-type-entity-list": "ფილტრის ტიპის სია", + "filter-type-entity-name": "ფილტრის ტიპის სახელი", + "filter-type-state-entity": "ფილტრის ტიპის სუბიექტი", + "filter-type-state-entity-description": "ფილტრის ტიპის სუბიექტის აღწერა", + "filter-type-asset-type": "აქტივის ტიპი", + "filter-type-asset-type-description": "აქტივის ტიპის აღწერა '{{assetType}}'", + "filter-type-asset-type-and-name-description": "ტიპის ასეტები '{{assetType}}' და სახელები რომლები იწყება '{{prefix}}'", + "filter-type-device-type": "მოწყობილობის ტიპი", + "filter-type-device-type-description": "ტიპის მოწყობილობები '{{deviceType}}'", + "filter-type-device-type-and-name-description": "ტიპის მოწყობილობები '{{deviceType}}' რომლების სახელებიც იწყება '{{prefix}}'", + "filter-type-entity-view-type": "ობიექტის მოწყობილობის ტიპი", + "filter-type-entity-view-type-description": "ობიექტის ტიპის ვარიანტები '{{entityView}}'", + "filter-type-entity-view-type-and-name-description": "ობიექტის ტიპის განსაზღვრება '{{entityView}}' და დასახელება რომელიც იწყება '{{prefix}}'", + "filter-type-relations-query": "მოთხოვნა რელაციის ტიპის მიხედვით", + "filter-type-relations-query-description": "{{entities}}, არსებული რელაციის ტიპი {{relationType}} {{direction}} {{rootEntity}}", + "filter-type-asset-search-query": "ძიება აქტივების მიხედვით", + "filter-type-asset-search-query-description": "აქტივების ტიპი {{assetTypes}}, არსებული აქტივები {{relationType}} {{direction}} {{rootEntity}}", + "filter-type-device-search-query": "ძიება მოწყობილობების მიხედვით", + "filter-type-device-search-query-description": "მოწყობილობის ტიპი {{deviceTypes}}, არსებული ტიპები {{relationType}} {{direction}} {{rootEntity}}", + "filter-type-entity-view-search-query": "ძიება ობიეტქების მიხევით", + "filter-type-entity-view-search-query-description": "ობიექტის ტიპის განსაზღვრება {{entityViewTypes}}, არსებული რელაციები {{relationType}} {{direction}} {{rootEntity}}", + "entity-filter": "ობიექტების ფილტრი", + "resolve-multiple": "როგორც მრავალი ობიექტი", + "filter-type": "ფილტრის ტიპი", + "filter-type-required": "სავალდებულო ფილტრის ტიპი", + "entity-filter-no-entity-matched": "ფილტრის შესაბამისი ობიექტები არ იძებნება", + "no-entity-filter-specified": "ობიექტის ფილტრი არ არ არის მითითებული", + "root-state-entity": "ობიექტის გამოყენება დეშბორდიდან როგორც ძირეული", + "root-entity": "ძირეული ობიექტი", + "state-entity-parameter-name": "სტატუსის ერთეულის პარამეტრის სახელი", + "default-state-entity": "ობიექტის ნაგულისხმევი სტატუსი", + "default-entity-parameter-name": "ობიექტის ნაგულისხმევი სახელი", + "max-relation-level": "მაქს. რელაციის დონე", + "unlimited-level": "ულიმიტო დონე", + "state-entity": "ობიექტის სტატუსი", + "all-entities": "ყველა ერთეული", + "any-relation": "ნებისმიერი რელაცია" + }, + "asset": { + "asset": "აქტივი", + "assets": "აქტივები", + "management": "მენეჯმენტი", + "view-assets": "აქტივების ნახვა", + "add": "დამატება", + "assign-to-customer": "მომხმარებელზე მინიჭება", + "assign-asset-to-customer": "აქტივის მინიჭება კლინტზე", + "assign-asset-to-customer-text": "შეარჩიეთ კატივი კილენტზე მისანიჭებლად", + "no-assets-text": "აქტივი არ იძებნება", + "assign-to-customer-text": "აირჩიე კლიენტი აქტივის მისანიჭებლად", + "public": "საჯარო", + "assignedToCustomer": "კლიენტზე მიბმა", + "make-public": "გასაჯაროება", + "make-private": "გასაჯარეობის გათიშვა", + "unassign-from-customer": "კლიენტისგან მოხსნა", + "delete": "წაშლა", + "asset-public": "საჯარო აქტივი", + "asset-type": "აქტივის ტიპი", + "asset-type-required": "აქტივის ტიპი სავალდებულოა", + "select-asset-type": "აირჩიეთ აქტივის ტიპი", + "enter-asset-type": "შეიყვანეთ აქტივის ტიპი", + "any-asset": "ნებისმიერი აქტივი", + "no-asset-types-matching": "აქტივის ტიპი '{{entitySubtype}}' ვერ მოიძებნა.", + "asset-type-list-empty": "აქტივის ტიპი ცარიელია", + "asset-types": "აქტივების ტიპები", + "name": "სახელი", + "name-required": "სახელი სავალდებულოა.", + "description": "აღწერა", + "type": "ტიპი", + "type-required": "ტიპი სავალდებულოა.", + "details": "დეტალები", + "events": "ივენთი", + "add-asset-text": "აქტივის დამატება", + "asset-details": "აქტივების დეტალები", + "assign-assets": "აქტივის მინიჭება", + "assign-assets-text": "აქტივის { count, plural, 1 {1 asset} other {# assets} } მინიჭება კლიენტზე", + "delete-assets": "აქტივების წაშლა", + "unassign-assets": "აქტივების მოშორება", + "unassign-assets-action-title": "გამოხმობა { count, plural, 1 {1 asset} other {# assets} } კლიენტისგან", + "assign-new-asset": "ახალი აქტივის მინიჭება", + "delete-asset-title": "დარწმუნებული ხართ რომ წავშალო '{{assetName}}' აქტივი?", + "delete-asset-text": "ფრთხილად, დადასტურების შემდეგ ყველა აქტივი წაიშლება და მონაცემები ვეღარ აღდგება.", + "delete-assets-title": "დარწმუნებული ხართ რომ წაიშალოს { count, plural, 1 {1 asset} other {# assets} }?", + "delete-assets-action-title": " { count, plural, 1 {1 asset} other {# assets} } წაშლა", + "delete-assets-text": "ფრთხილად, დადასტურების შემდეგ ყველა მონიშნული აქტივი წაიშლება და მონაცემები ვეღარ აღდგება.", + "make-public-asset-title": "დარწმუნებული ხართ რომ გნებავთ აქტივის '{{assetName}}' გასაჯაროება?", + "make-public-asset-text": "დადასტურების შედეგად აქტივი და მისი მონაცემები გახდება საჯარო და ხელმისაწვდომი ყველასთვის.", + "make-private-asset-title": "დარწმუნებული ხართ რომ გნებავთ აქტივის '{{assetName}}' დამალვა?", + "make-private-asset-text": "დადასტურების შედეგად აქტივი და მისი მონაცემები გახდება პრივატული და ხელმიუწვდომელი ყველასთვის.", + "unassign-asset-title": "დატწმუნებული ხართ რომ გნებავთ აქტივის '{{assetName}}' გამოხმობა?", + "unassign-asset-text": "დადასტურების შედეგად აქტივი გახდება კლიენტისთვის ხელმიუწვდომელი.", + "unassign-asset": "აქტივის გამოხმობა", + "unassign-assets-title": "დარწმუნებული ხართ რომ გნებავთ გამოიხმოთ { count, plural, 1 {1 asset} other {# assets} }?", + "unassign-assets-text": "დადასტურების შედეგად ყველა მონიშნული აქტივი გახდება კლიენტისთვის ხელმიუწვდომელი.", + "copyId": "აქტივის ID-ის დაკოპირება", + "idCopiedMessage": "აქტივი დაკოპირებულია კლიპბორდში", + "select-asset": "აქტივის მონიშვნა", + "no-assets-matching": "შესაბამისი აქტივი '{{entity}}' არ მოიძებნა.", + "asset-required": "აქტივი აუცილებელია", + "name-starts-with": "აქტივის სახელი იწყება", + "import": "აქტივების იმპორტი", + "asset-file": "აქტივის ფაილი", + "search": "აქტივების ძიება", + "selected-assets": "{ count, plural, 1 {1 asset} other {# assets} } მონიშნულია", + "label": "ნიშნული" + }, + "attribute": { + "attributes": "ატრიბუტები", + "latest-telemetry": "უახლესი ტელემეტრია", + "attributes-scope": "ობიექტის ატრიბუტების ფარგლები", + "scope-latest-telemetry": "უახლესი ტელემეტრია", + "scope-client": "კლიენტის ატრიბუტები", + "scope-server": "სერვერის ატრიბუტები", + "scope-shared": "ატრიბუტების გაზიარება", + "add": "ატრიბუტის დამატება", + "key": "გასაღები", + "last-update-time": "ბოლო განახლების დრო", + "key-required": "ატრიბუტის გასაღები სავალდებულოა.", + "value": "მნიშვნელობა", + "value-required": "ატრიბუტის მნიშვნელობა საჭიროა.", + "delete-attributes-title": "დარწმუნებული ხართ რომ გსურთ წაშალოთ { count, plural, 1 {1 attribute} other {# attributes} }?", + "delete-attributes-text": "ფრთხილად, დადასტურების შემდეგ ყველა მონიშნული ატრიბუტი წაიშლება.", + "delete-attributes": "ატრიბუტების წაშკა", + "enter-attribute-value": "შეიყვანეთ ატრიბუტის მნიშვნელობა", + "show-on-widget": "ვიჯეტზე გამოტანა", + "widget-mode": "ვიჯეტის რეჟიმი", + "next-widget": "შემდეგი ვიჯეტი", + "prev-widget": "წინა ვიჯეტი", + "add-to-dashboard": "დეშბორდზე დამატება", + "add-widget-to-dashboard": "ვიჯეტის დეშბორდზე დამატება", + "selected-attributes": "{ count, plural, 1 {1 attribute} other {# attributes} } მონიშნულია", + "selected-telemetry": "{ count, plural, 1 {1 telemetry unit} other {# telemetry units} } მონიშნულია" + }, + "audit-log": { + "audit": "აუდიტი", + "audit-logs": "აუდიტორული ჟურნალი", + "timestamp": "თაიმსტემპი", + "entity-type": "ობიექტის ტიპი", + "entity-name": "ობიექტის სახელი", + "user": "მომხმარებელი", + "type": "ტიპი", + "status": "სტატუსი", + "details": "დეტალები", + "type-added": "დამატებული", + "type-deleted": "წაშლილი", + "type-updated": "განახლებული", + "type-attributes-updated": "განახლებული ატრიბუტები", + "type-attributes-deleted": "წაშლილი ატრიბუტები", + "type-rpc-call": "RPC გამოძახება", + "type-credentials-updated": "მომხმარებლის ჩანაწერი განახლებულია", + "type-assigned-to-customer": "კლიენტზე მიმაგრებული", + "type-unassigned-from-customer": "კლიენტისგან მოხსნილი", + "type-activated": "გააქტიურებული", + "type-suspended": "შეჩერებული", + "type-credentials-read": "მომხმარებლის ჩანაწრის წაკითხვა", + "type-attributes-read": "ატრიბუტების წაკითხვა", + "type-relation-add-or-update": "რელაცია განახლებულია", + "type-relation-delete": "რელაცია წაშლილია", + "type-relations-delete": "ყველა რელაციის წაშლა", + "type-alarm-ack": "დადასტურებულია", + "type-alarm-clear": "გასუფთავებულია", + "type-login": "შესვლა", + "type-logout": "გამოსვლა", + "type-lockout": "ჩაკეტვა", + "status-success": "წარმატება", + "status-failure": "წარუმატებელი", + "audit-log-details": "აუდიტის ჟურნალის დეტალები", + "no-audit-logs-prompt": "ლოგები არ მოიძებნა", + "action-data": "მოქმედების მონაცემები", + "failure-details": "პრობლემის დეტალები", + "search": "აუდიტის ლოგებში ძიება", + "clear-search": "ძებნის გასუფთავება" + }, + "confirm-on-exit": { + "message": "თქვენ გაქვთ დაუმახსოვრებელი ცვლილებები. დარწმუნებული ხართ რომ გინდათ ამ გვერდიდან გადასვლა?", + "html-message": "თქვენ გაქვთ დაუმახსოვრებელი ცვლილებები.
    დარწმუნებული ხართ რომ გინდათ ამ გვერდიდან გადასვლა?", + "title": "დაუმახსოვრებელი ცვლილებები" + }, + "contact": { + "country": "ქვეყანა", + "city": "ქალაქი", + "state": "შტატი/პროვინცია", + "postal-code": "საფოსტო ინდექსი", + "postal-code-invalid": "საფოსტო კოდი არასწორია", + "address": "მისამართი", + "address2": "მისამართი 2", + "phone": "ტელეფონი", + "email": "ელ.ფოსტა", + "no-address": "მისამართის გარეშე" + }, + "common": { + "username": "მომხმარებლის სახელი", + "password": "პაროლი", + "enter-username": "შეიყვანეთ მომხმარებლის სახელი", + "enter-password": "შეიყვანეთ პაროლი", + "enter-search": "შეიყვანეთ საძიებო სიტყვა" + }, + "content-type": { + "json": "ჯეისონი", + "text": "ტექსტი", + "binary": "ორობითი (base64)" + }, + "customer": { + "customer": "კლიენტი", + "customers": "კლიენტები", + "management": "კლიენტების მართვა", + "dashboard": "მომხმარებლის დაშბორდი", + "dashboards": "მომხმარებლის დაშბორდები", + "devices": "მომხმარებლის მოწყობილობები", + "entity-views": "მომხმარებლის ობიექტები", + "assets": "კლიენტების აქტივები", + "public-dashboards": "საჯარო დეშბორდები", + "public-devices": "საჯარო მოწყობილობები", + "public-assets": "საჯარო აქტივები", + "public-entity-views": "ობიექტის საკჯარო წარმოდგენა", + "add": "მომხმარებლის დამატება", + "delete": "მომხმარებლის წაშლა", + "manage-customer-users": "კლიენტის ჯგუფის მართვა", + "manage-customer-devices": "მომხმარებელთა მოწყობილობების მართვა", + "manage-customer-dashboards": "კლიენტის დეშბორდების მართვა", + "manage-public-devices": "საჯარო მოწყობილობების მართვა", + "manage-public-dashboards": "საჯარო დეშბორდების მართვა", + "manage-customer-assets": "კლიენტების აქტივების მართვა", + "manage-public-assets": "საჯარო აქტივების მართვა", + "add-customer-text": "ახალი მომხმარებლის დამატება", + "no-customers-text": "მომხმარებელი არ იძებნება", + "customer-details": "მომხმარებლის დეტალები", + "delete-customer-title": "დარწმუნებული ხართ რომ გსურთ წაშალოთ მომხმარებელი '{{customerTitle}}'?", + "delete-customer-text": "ყურადღებით დადასტურების შემდეგ ყველა მონიშნული მომხმარებელი და მასთან დაკავშირებული მონაცემები წაიშლება.", + "delete-customers-title": "დაწრმუნებული ხართ რომ გსურთ წაშალოთ { count, plural, 1 {1 customer} other {# customers} }?", + "delete-customers-action-title": "წაშლა { count, plural, 1 {1 customer} other {# customers} }", + "delete-customers-text": "ყურადღებით დადასტურების შემდეგ ყველა მონიშნული მომხმარებელი და მასთან დაკავშირებული მონაცემები წაიშლება.", + "manage-users": "მომხმარებლების მართვა", + "manage-assets": "აქტივების მართვა", + "manage-devices": "მოწყობილობების მართვა", + "manage-dashboards": "დეშბორდების მართვა", + "title": "სათაური", + "title-required": "სათაური აუცილებელია", + "description": "აღწერილობა", + "details": "დეტალები", + "events": "ივენთები", + "copyId": "მომხმარებლის ID ის დაკოპირება", + "idCopiedMessage": "კლიენიტს ID-ის დაკოპირებულია კლიპბორდში", + "select-customer": "აირჩიე მომხმარებელი", + "no-customers-matching": "მომხმარებელი '{{entity}}' არ მოიძებნა.", + "customer-required": "მომხმარებელი სავალდებულოა", + "select-default-customer": "აირჩიეთ ნაგულისხმევი კლიენტი", + "default-customer": "ნაგულისხმევი კლიენტი", + "default-customer-required": "ნაგულისხმევი კლიენტი სავალდებულოა რომ მოხერხდეს დეშბორდის ანალიზი ტენანტის დონეზე", + "search": "მომხმარებლების ძიება", + "selected-customers": "შერჩეული მომხმარებლები" + }, + "datetime": { + "date-from": "თარიღიდან", + "time-from": "დრო-დან", + "date-to": "თარიღამდე", + "time-to": "დრომდე" + }, + "dashboard": { + "dashboard": "დეშბორდი", + "dashboards": "დეშბორდები", + "management": "დეშბორდების მენეჯმენტი", + "view-dashboards": "შეხედე დეშბორდებს", + "add": "დაამატე დეშბორდი", + "assign-dashboard-to-customer": "მიამაგრე დეშბორდი მომხმარებელს", + "assign-dashboard-to-customer-text": "გთხოვთ აირჩიოთ მოხმარებელზე მისამაგრებელი დეშბორდი", + "assign-to-customer-text": "გთხოვთ აირჩიოთ მოხმარებელი რომ მიამაგროთ დეშბორდს", + "assign-to-customer": "მომხმარებელზე მიმაგრება", + "unassign-from-customer": "მომხმარებლისგან მოხსნა", + "make-public": "გახადე დეშბორდი საჯარო", + "make-private": "გახადე დეშბორდი პრივატული", + "manage-assigned-customers": "მიმაგრებული მომხმარებლების მართვა", + "assigned-customers": "მინიჭებული მოხმარებლები", + "assign-to-customers": "დეშბორდის მომხმარებეზე მიბმა", + "assign-to-customers-text": "გთხოვთ აირჩიოთ მოხმარებლები რათა მიამაგროს დეშბორდი", + "unassign-from-customers": "მომხარებლებისგან დეშბორიდს მოხსნა", + "unassign-from-customers-text": "გთხოვთ აირჩიოთ მოხმარებლები რათა მოეხსნათ დეშბორდი", + "no-dashboards-text": "დეშბორდი არ იძებნა", + "no-widgets": "ვიჯეტები არ არის დაკომფიგირებული", + "add-widget": "ვიჯეტის დამატება", + "title": "სათაური", + "select-widget-title": "აირჩიეთ ვიჯეტი", + "select-widget-subtitle": "ხელმისაწვდომი ვიჯეტების სია", + "delete": "დეშბორდის წაშლა", + "title-required": "სათაური აუცილებელია", + "description": "აღწერილობა", + "details": "დეტალები", + "dashboard-details": "დეშბორდის დეტალები", + "add-dashboard-text": "დაამატე ახალი დეშბორდი", + "assign-dashboards": "მიამაგრე დეშბორდები", + "assign-new-dashboard": "მიამაგრე ახალი დეშბორდი", + "assign-dashboards-text": "მიამაგრე { count, plural, 1 {1 dashboard} other {# dashboards} } მომხმარებელს", + "unassign-dashboards-action-text": "მოხსენი { count, plural, 1 {1 dashboard} other {# dashboards} } მომხმარებელს", + "delete-dashboards": "დეშბორდების წაშლა", + "unassign-dashboards": "დეშბორდების მოხსნა", + "unassign-dashboards-action-title": "მოხსენი { count, plural, 1 {1 dashboard} other {# dashboards} } მომხმარებელს", + "delete-dashboard-title": "დარწმუნებული ხართ რომ გინდათ დეშბორდის წაშლა '{{dashboardTitle}}'?", + "delete-dashboard-text": "ყურადღებით, დადასტურების შემდეგ დეშბორდი ყველა მონიშნული მონაცემები გახდება ხელმიუწვდომელი", + "delete-dashboards-title": "დარწმუნებული ხართ რომ გინდათ წაშლა { count, plural, 1 {1 dashboard} other {# dashboards} }?", + "delete-dashboards-action-title": "წაშლა { count, plural, 1 {1 dashboard} other {# dashboards} }", + "delete-dashboards-text": "ყურადღებით, დადასტურების შემდეგ დეშბორდი ყველა მონიშნული მონაცემები გახდება ხელმიუწვდომელი", + "unassign-dashboard-title": "ნამდვილად გსურთ დეშბორდის გამოხმობა '{{dashboardTitle}}'?", + "unassign-dashboard-text": "დადასტურების შემდგომ დეშბორდი არ იქნება ხელმისაწვდომი კლიენტისთვის", + "unassign-dashboard": "დეშბორდის გამოხმობა", + "unassign-dashboards-title": "ნამდვილად გსურთ დეშბორდის გამოხმობა { count, plural, 1 {1 dashboard} other {# dashboards} }?", + "unassign-dashboards-text": "დადასტურების შემდგომ დეშბორდი არ იქნება ხელმისაწვდომი კლიენტისთვის", + "public-dashboard-title": "დეშბორდი ხელმისაწვდომია", + "public-dashboard-text": "დეშბორდი ხელმისაწვდომია {{dashboardTitle}} is now public and accessible via next public link:", + "public-dashboard-notice": "შენიშვნა: მოწყობილობის მონაცემებზე წვდომისთვის, თქვენ უნდა გახსთათ წვდომა ამ მოწყობილობებთან", + "make-private-dashboard-title": "დარწმუნებული ხართ რომ გინდათ გახადოთ დეშბორდი '{{dashboardTitle}}' პრივატული?", + "make-private-dashboard-text": "დადასტურების შემდეგ დეშბორდი გახდება პრივატული და აღარ იქნება ხელმისაწვდომი სხვა მომხმარებლებისთვის", + "make-private-dashboard": "აქციე დეშბორდი პრივატულად", + "socialshare-text": "სოციალური ტექსტი", + "socialshare-title": "'{{dashboardTitle}}' Powered by Giot", + "select-dashboard": "აირჩიე დეშბორდი", + "no-dashboards-matching": "'{{entity}}'-ს მზგავსი დეშბორდი არ იქნა ნაპოვნი.", + "dashboard-required": "დეშბორდი აუცილებელია", + "select-existing": "აირჩიე არსებული დეშბორდი", + "create-new": "შექმენი ახალი დეშბორდი", + "new-dashboard-title": "ახალი დეშბორდის სათაური", + "open-dashboard": "ღია დეშბორდი", + "set-background": "დააყენე ფონი", + "background-color": "ფონის ფერი", + "background-image": "ფონის სურათი", + "background-size-mode": "ფონის ზომის მოუდი", + "no-image": "აირჩიეთ სურათი", + "drop-image": "ჩააგდეთ სურათი ან დააჭირეთ სურათის ასატვირთად", + "settings": "პარამეტრები", + "columns-count": "სვეტების დათვლა", + "columns-count-required": "სვეტების დათვლა სავალდებულოა", + "min-columns-count-message": "მინიმალური სვეტების დათვლის რაოდენობაა 10", + "max-columns-count-message": "მაქსიმალური სვეტების დათვლის რაოდენობაა 1000", + "widgets-margins": "ვიჯეტებს შორის ზღვარი", + "horizontal-margin": "ჰორიზონტალური ზღვარი", + "horizontal-margin-required": "'ჰორიზონტალური ზღვარი' მნიშვნელობა სავალდებულოა", + "min-horizontal-margin-message": "მინიმალური ჰორიზონტალური ზღვარი არის 0", + "max-horizontal-margin-message": "მაქსიმალრუი ჰორიზონტალური ზღვარი არის 50", + "vertical-margin": "ვერტიკალური ზღვარი", + "vertical-margin-required": "ვერტიკალური ზღვარი მნიშვნელობა სავალდებულოა", + "min-vertical-margin-message": "მინიმალური ვერტიკალური ზღვარი არის 0", + "max-vertical-margin-message": "მაქსიმალური ვერტიკალური ზღვარი არის 50", + "autofill-height": "ინტერფეისის სიმაღლის ავტომატური შერჩევა", + "mobile-layout": "მობილური წყობის პარამეტრები", + "mobile-row-height": "მობილური რიგის სიმაღლე PX", + "mobile-row-height-required": "მობილური რიგის სიმაღლე სავალდებულოა", + "min-mobile-row-height-message": "მობილური რიგის მინიმალური სიმაღლე არის 5 პიქსელი", + "max-mobile-row-height-message": "მობილური რიგის მაქსიმალური სიმაღლე არის 200 პიქსელი", + "display-title": "აჩვენე დეშბორდის სათაური", + "toolbar-always-open": "დატოვეთ ინსტრუმენტების პანელი ღია", + "title-color": "სათაურის ფერი", + "display-dashboards-selection": "დეშბორდების არჩევანის ჩვენება", + "display-entities-selection": "ობიექტის არჩევანის ჩვენება", + "display-dashboard-timewindow": "დროის მონაკვეტის ჩვენება", + "display-dashboard-export": "ექსპორტის ჩვენება", + "import": "დეშბორდის იმპორტი", + "export": "დეშბორდის ექსპორტი", + "export-failed-error": "დეშბორდის ექსპორტი შეუძლებელია: {{error}}", + "create-new-dashboard": "ახალი დეშბორდის შექმნა", + "dashboard-file": "დეშბორდის ფაილი", + "invalid-dashboard-file-error": "დეშბორდის იმპორტი შეუძლებელია: დეშბორდის სტრუქტურა დარღვეულია", + "dashboard-import-missing-aliases-title": "დააყენეთ იმპორტირებული დეშბორდების ზედმეტსახელები", + "create-new-widget": "შექმენი ახალი ვიჯეტი", + "import-widget": "ვიჯეტის იმპორტი", + "widget-file": "ვიჯეტ ფაილი", + "invalid-widget-file-error": "ვიჯეტის იმპორტი შეუძლებელია: ვიჯეტის სტრუქტურა დარღვეულია", + "widget-import-missing-aliases-title": "დააყენე იმპორტირებული ვიჯეტის ზედმეტსახელი", + "open-toolbar": "დეშბორდის პანელის გახსნა", + "close-toolbar": "პანელის დახურვა", + "configuration-error": "კონფიგურაციის შეცდომა", + "alias-resolution-error-title": "დეშბორდის ზედმეტსახელის კონფიგურაციის შეცდომა", + "invalid-aliases-config": "ზედმეტსახელის ფილტრს არ ემთხვევა არცერთი მოწყობილობა.
    გთხოვთ მიმართეთ თქვენს ადმინისტრატორს.", + "select-devices": "აირჩიეთ მოწყობილობები", + "assignedToCustomer": "მიმაგრებული მომხმარებელზე", + "assignedToCustomers": "მიმაგრებული მომხმარებლელბზე", + "public": "საჯარო", + "public-link": "საჯარო ბმული", + "copy-public-link": "დააკოპირე საჯარო ბმული", + "public-link-copied-message": "დეშბორდის საჯარო ბმული დაკოპირებულია", + "manage-states": "გააკონტროლე დეშბორდის მდგომარეობა", + "states": "დეშბორდის მდგომარეობა", + "search-states": "მოძებნე დეშბორდის მდგომარეობა", + "selected-states": "{ count, plural, 1 {1 dashboard state} other {# dashboard states} } არჩეულია", + "edit-state": "დეშბორდის მდგომარეობის რედაქტირება", + "delete-state": "დეშბორდის მდგომარეობის წაშლა", + "add-state": "დაამატე დეშბორდის მდგომარეობა", + "state": "დეშბორდის მდგომარეობა", + "state-name": "მდგომარეობა", + "state-name-required": "დეშბორდის მოცემულობის დასახელება სავალდებულოა", + "state-id": "ID მოცემულობა", + "state-id-required": "დეშბორდის ID მოცემულობა სავალდებულოა", + "state-id-exists": "არსებული სახელით დეშბორდი უკვე არსებობს", + "is-root-state": "არის ძირეული მდგომარეობა", + "delete-state-title": "დეშბორდის მოცემულობის წაშლა", + "delete-state-text": "დარწმუნებული ხართ რომ გსურთ წაშალოთ დეშბორდი '{{stateName}}'?", + "show-details": "დეტალების გამოტანა", + "hide-details": "დეტალების დაფარვა", + "select-state": "მდგომარების არჩევა", + "state-controller": "მდგომარების კონტროლერი", + "search": "მოძებნე დეშბორდი", + "selected-dashboards": "{ count, plural, 1 {1 dashboard} other {# dashboards} } არჩეულია" + }, + "datakey": { + "settings": "პარამეტრები", + "advanced": "დამატებითი", + "label": "ნიშნული", + "color": "ფერი", + "units": "მიუთითეთ სიმბოლოები რომლებიც უნდა გამოიყენოთ დანაყოფების შემდგომ", + "decimals": "წილადები", + "data-generation-func": "მონაცემთა გენერირების ფუნქცია", + "use-data-post-processing-func": "მონაცემების დამუშავებათა შემდგომი ფუნქციის გამოყენება", + "configuration": "მონაცემთა გასაღების კონფიგურაცია", + "timeseries": "ტელემეტრია", + "attributes": "ატრიბუტები", + "entity-field": "ერთეული ობიექტის ველი", + "alarm": "განგაში", + "timeseries-required": "ტელემეტრია ობიექტის სავალდებულოა", + "timeseries-or-attributes-required": "ტელემეტრია/ატრიბუტები სავალდებულოა", + "maximum-timeseries-or-attributes": "მაქსიმალური { count, plural, 1 {1 timeseries/attribute is allowed.} other {# timeseries/attributes are allowed} }", + "alarm-fields-required": "განგაშის ველები სავალდებულოა", + "function-types": "ფუნქციის ტიპები", + "function-types-required": "ფუნქციის ტიპები სავალდებულოა", + "maximum-function-types": "მაქსიუმალური { count, plural, 1 {1 function type is allowed.} other {# function types are allowed} }", + "time-description": "დრო არსებული მნიშნველობისთვის", + "value-description": "არსებული ღირებულება", + "prev-value-description": "წინა ფუნქციის რეზულტატი", + "time-prev-description": "წინა მნიშვნელობის დრო", + "prev-orig-value-description": "ორიგინალი წინა მნიშვნელობა" + }, + "datasource": { + "type": "ინფორმაციის წყაროს ტიპი", + "name": "სახელი", + "add-datasource-prompt": "დაამატეთ მონაცემთა წყარო" + }, + "details": { + "details": "დეტალები", + "edit-mode": "რედაქტირების რეჟიმში", + "toggle-edit-mode": "რედაქტირების რეჟიმში ჩართვა/გამორთვა" + }, + "device": { + "device": "მოწყობილობა", + "device-required": "მოწყობილობა სავალდებულოა", + "devices": "მოწყობილობები", + "management": "მოწყობილობების მენეჯმენტი", + "view-devices": "ნახე მოწყობილობები", + "device-alias": "მოწყობილობის ზედმეტსახელი", + "aliases": "მოწყობილობების ზედმეტსახელები", + "no-alias-matching": "'{{alias}}' არ მოიძებნა", + "no-aliases-found": "ზედმეტსახელი არ მოიძებნა", + "no-key-matching": "'{{key}}' არ მოიძებნა", + "no-keys-found": "გასაღებები არ მოიძებნა", + "create-new-alias": "შექმენი ახალი!", + "create-new-key": "შექმენი ახალი!", + "duplicate-alias-error": "ნაპოვნია დუბლიკატი ზედმეტსახელი '{{alias}}'.
    მოწყობილობის ზედმესახელი უნდა იყოს უნიკალური დაშბორდის მასშტაბით.", + "configure-alias": "დააყენე '{{alias}}' ზედმეტსახელი", + "no-devices-matching": "მოწყობილობები რომლებიც ემთხვევა '{{entity}}' არ იქნა აღმოჩენილი", + "alias": "ზედმეტსახელი", + "alias-required": "მოწყობილობის ზედმეტსახელი სავალდებულოა", + "remove-alias": "წაშალე მოწყობილობის ზედმესახელი", + "add-alias": "დაამატე მოწყობილობის ზედმესახელი", + "name-starts-with": "მოწყობილობის სახელი იწყება", + "device-list": "მოწყობილობის სია", + "use-device-name-filter": "ფილტრის გამოყენება", + "device-list-empty": "არ არის არცეული მოწყობილობა", + "device-name-filter-required": "მოწყობილობის სახელის ფილტრი სავალდებულოა", + "device-name-filter-no-device-matched": "მოწყობილობები რომლებიც ემთხვევა '{{device}}' არ იქნა აღმოჩენილი", + "add": "მოწყობილობის დამატება", + "assign-to-customer": "კლიენტზე მიბმა", + "assign-device-to-customer": "მოწყობილობების კლიენტზე მიბმა", + "assign-device-to-customer-text": "გთხოვთ აირჩიოთ მოწყობილობები კლიენტზე მისამაგრებლად", + "make-public": "გახადე მოწყობილობა საჯარო", + "make-private": "გახადე მოწყობილობა პრივატული", + "no-devices-text": "მოწყობილობები არ იქნა აღმოჩენილი", + "assign-to-customer-text": "გთხოვთ აირჩიოთ კლიენტი რომ მიამაგროთ მოწყობილობები", + "device-details": "მოწყობილობის დეტალები", + "add-device-text": "ახალი მოწყობილობის დამატება", + "credentials": "მოხმარებლის ჩანაწერი", + "manage-credentials": "მომხმარებლის ცანაწერის მართვა", + "delete": "მოწყობილობის წაშლა", + "assign-devices": "მოწყობილობის მინიჭება", + "assign-devices-text": "მიანიჭე { count, plural, 1 {1 device} other {# devices} } კლიენტს", + "delete-devices": "წაშალე მოწყობილობები", + "unassign-from-customer": "მოხსენი მოხმარებელს", + "unassign-devices": "მოხსენი მოწყობილოებები", + "unassign-devices-action-title": "მოხსენი { count, plural, 1 {1 device} other {# devices} } მოხმარებელს", + "assign-new-device": "მიამაგრე ახალი მოწყობილობა", + "make-public-device-title": "დარწმუნებული ხართ რომ გინდათ გახადოთ '{{deviceName}}' საჯარო?", + "make-public-device-text": "დადასტურების შემდგომ , მოწყობილობა და ყველა მასთან დაკავშირებული მონაცემები იქნება საჯარო და ხელმისაწვდომი", + "make-private-device-title": "დარწმუნებული ხართ რომ გინდათ შეზღუდოთ წვდომა '{{deviceName}}' თან", + "make-private-device-text": "ყურადღებით , დადასტურების შემდგომ , მოწყობილობა და ყველა მასთან დაკავშირებული მონაცემები არ იქნება ხელმისაწვდომი", + "view-credentials": "მონაცემთა ბაზის ნახვა", + "delete-device-title": "დარწმუნებული ხართ რო გინდათ წაშალოთ მოწყობილობა '{{deviceName}}'?", + "delete-device-text": "ყურადღებით , დადასტურების შემდგომ , მოწყობილობა და ყველა მასთან დაკავშირებული ჩანაწერი არ იქნება ხელმისაწვდომი", + "delete-devices-title": "დარწმუნებული ხართ რო გინდათ წაშალოთ { count, plural, 1 {1 device} other {# devices} }?", + "delete-devices-action-title": "წაშლა { count, plural, 1 {1 device} other {# devices} }", + "delete-devices-text": "ყურადღებით , დადასტურების შემდგომ , მოწყობილობა და ყველა მასთან დაკავშირებული ჩანაწერი არ იქნება ხელმისაწვდომი", + "unassign-device-title": "დარწმუნებული ხართ რო გინდათ გამოიხმოთ '{{deviceName}}'?", + "unassign-device-text": "დადასტურების შემდგომ , მოწყობილობა არ იქნება მომხმარებლისთვის ხელმისაწვდომი", + "unassign-device": "მოწყობილობის გამოხმობა", + "unassign-devices-title": "დარწმუნებული ხართ რო გინდათ გამოიხმოთ { count, plural, 1 {1 device} other {# devices} }?", + "unassign-devices-text": "დადასტურების შემდგომ , მოწყობილობა არ იქნება მომხმარებლისთვის ხელმისაწვდომი", + "device-credentials": "მოწყობილობის სარეგისტრაციო მონაცემები", + "credentials-type": "მომხმარებლის ჩანაწერი", + "access-token": "ტოკენი", + "access-token-required": "ტოკენი სავალდებულოა", + "access-token-invalid": "ტოკენის სიგრძე უნდა იყოს 1 დან 20 სიმბოლომდე", + "rsa-key": "ღია გასაღები RSA", + "rsa-key-required": "ღია გასაღები RSA აუცილებელია", + "secret": "საიდუმლო", + "secret-required": "საიდუმლო აუცილებელია", + "device-type": "მოწყობილობის ტიპი", + "device-type-required": "მოწყობილობის ტიპის სავალდებულოა", + "select-device-type": "აირჩიეთ-მოწყობილობის ტიპი", + "enter-device-type": "შეიყვანეთ მოწყობილობის ტიპი", + "any-device": "ნებისმიერი მოწყობილობა", + "no-device-types-matching": "მოწყობილობის ტიპი რომელიც შეესაბამება '{{entitySubtype}}', არ იძებნება", + "device-type-list-empty": "მოწყობილობის ტიპი არ არის მითითებული", + "device-types": "მოწყობილობის ტიპები", + "name": "სახელი", + "name-required": "სახელი სავალდებულოა", + "description": "აღწერა", + "label": "ნიშნული", + "events": "ივენთი", + "details": "დეტალები", + "copyId": "დააკოპირე მოწყობილობის ID", + "copyAccessToken": "დააკოპირე წვდომის ტოკენი", + "idCopiedMessage": "მოწყობილობის ID დაკოპირებულია", + "accessTokenCopiedMessage": "მოწყობილობის წვდომის ტოკენი დაკოპირებულია", + "assignedToCustomer": "მიმაგრებულია მოხმარებელს", + "unable-delete-device-alias-title": "შეუძლებელია მოწყობილობის ზედმესახელის წაშლა", + "unable-delete-device-alias-text": "მოწყობილობა ზედმესახელით '{{deviceAlias}}' ვერ იშლება რადგან ის გამოიყენება მომდევნო ვიჯეტში
    {{widgetsList}}", + "is-gateway": "არის კარიბჭე", + "public": "საჯარო", + "device-public": "მოწყობილობა საჯაროა", + "select-device": "აირჩიეთ მოწყობილობა", + "import": "იმპორტირებული მოწყობილობა", + "device-file": "მოწყობილობის ფაილი", + "search": "მოწყობილობების ძიება", + "selected-devices": "{ count, plural, 1 {1 device} other {# devices} } არჩეულია" + }, + "dialog": { + "close": "დახუურე დიალოგი" + }, + "direction": { + "column": "სვეტი", + "row": "რიგი" + }, + "error": { + "unable-to-connect": "ვერ ვუკავშირდები სერვერს! გთხოვთ შეამოწმოთ ინტერნეტ კავშირი.", + "unhandled-error-code": "მოსაგვარებელი პრობლემის კოდი: {{errorCode}}", + "unknown-error": "უცნობი შეცდომა" + }, + "entity": { + "entity": "სუბიექტი", + "entities": "სუბიექტები", + "aliases": "სუბიექტების ზედმესახელები", + "entity-alias": "სუბიექტის ზედმეტსახელი", + "unable-delete-entity-alias-title": "ვერ მოხერხდა სუბიექტის ზედმესახელის წაშლა", + "unable-delete-entity-alias-text": "სუბიექტის ზედმესახელი '{{entityAlias}}' ვერ წაიშლება რადგან მას იყენებს მომდევნო ვიჯეტი:
    {{widgetsList}}", + "duplicate-alias-error": "ნაპოვნია დუბლიკატი ზედმესახელი '{{alias}}'.
    ობიქტის ზედმესახელი უნდა იყოს უნიკალური დეშბორდის მასშტაბით.", + "missing-entity-filter-error": "ფილტრი აკლია ზედმესახელისთვის: '{{alias}}'.", + "configure-alias": "დააყენე '{{alias}}' ზედმესახელი", + "alias": "ზედმესახელი", + "alias-required": "ობიექტის ზედმესახელი სავალდებულოა", + "remove-alias": "წაშალე ობიექტის ზედმესახელი", + "add-alias": "დაამატე ობიექტის ზედმესახელი", + "entity-list": "ობიექტის სია", + "entity-type": "ობიექტის ტიპი", + "entity-types": "ობიექტის ტიპები", + "entity-type-list": "ობიექტის ტიპის სია", + "any-entity": "ნებისმიერი ობიექტი", + "enter-entity-type": "შეიყვანეთ ობიექტის ტიპი", + "no-entities-matching": "შემდგომი ობიექტი '{{entity}}' არ მოიძებნა.", + "no-entity-types-matching": "შემდგომი ობიექტის ტიპი '{{entityType}}' არ მოიძებნა.", + "name-starts-with": "სახელი იწყება", + "use-entity-name-filter": "გამოიყენე ფილტრი", + "entity-list-empty": "ობიექტი არ არის არჩეული", + "entity-type-list-empty": "ობიექტის ტიპი არ არის არჩეული", + "entity-name-filter-required": "ობიექტის სახელის ფილტრი სავალდებულოა", + "entity-name-filter-no-entity-matched": "ობიექტები რომბლებიც იწყება '{{entity}}' -ით არ იქნა ნაპოვნი", + "all-subtypes": "ყველა", + "select-entities": "აირჩიე ობიექტები", + "no-aliases-found": "ზედმეტსახელები არ არის ნაპოვნი", + "no-alias-matching": "'{{alias}}' არ არის ნაპოვნი", + "create-new-alias": "შექმენი ახალი!", + "key": "გასაღები", + "key-name": "გასაღების სახელი", + "no-keys-found": "გასარები არ არის ნაპოვნი", + "no-key-matching": "'{{key}}' არ არის ნაპოვნი.", + "create-new-key": "შექმნა ახალი გასაღები", + "type": "ტიპი", + "type-required": "ობიექტის ტიპი სავალდებულოა", + "type-device": "მოწყობილობა", + "type-devices": "მოწყობილობები", + "list-of-devices": "{ count, plural, 1 {One device} other {List of # devices} }", + "device-name-starts-with": "მოწყობილობები რომლების სახელიც იწყება '{{prefix}}' -ით", + "type-asset": "აქტივი", + "type-assets": "აქტივები", + "list-of-assets": "{ count, plural, 1 {ერთი აქტიობა} other {სია შემდეგი აქტიობები} }", + "asset-name-starts-with": "აქტიობები, რომელიც იწყება '{{prefix}}' ით", + "type-entity-view": "წარმოდგენილი ობიექტი", + "type-entity-views": "წარმოდგენილი ობიექტები", + "list-of-entity-views": "{ count, plural, 1 {ერთი ობიექტი} other {სია შემდეგი ობიექტებიდან} }", + "entity-view-name-starts-with": "ობიექტი, რომელიც იწყება '{{prefix}}' ით", + "type-rule": " წესი", + "type-rules": " წესები", + "list-of-rules": "{ count, plural, 1 {ერთი წესი} other {სია შემდეგი წესებისგან} }", + "rule-name-starts-with": "წესი, ვისი სახელიც იწყება '{{prefix}}' ით", + "type-plugin": "პლაგინი", + "type-plugins": "პლაგინები", + "list-of-plugins": "{ count, plural, 1 {ერთი პლაგინი} other {სია შემდეგი პლაგინებისგან} }", + "plugin-name-starts-with": "პლაგინი ვისი სახელიც იწყება '{{prefix}}' ით", + "type-tenant": "მეპატრონე", + "type-tenants": "მეპატრონეები", + "list-of-tenants": "{ count, plural, 1 {ერთი მომხმარებელი} other {სია შემდეგი მომხმარებლებისგან} }", + "tenant-name-starts-with": "მომხმარებლები ვისი სახელიც იწყება '{{prefix}}' ით", + "type-customer": "მომხმარებელი", + "type-customers": "მომხმარებლები", + "list-of-customers": " { count, plural, 1 {ერთი მომხმარებელი} other {სია შემდეგი მომხმარებლებისგან} }", + "customer-name-starts-with": "მომხმარებლები ვისი სახელიც იწყება '{{prefix}}' ით", + "type-user": "მომხმარებელი", + "type-users": "მომხმარებლები", + "list-of-users": "მომხმარებელთა სია { count, plural, 1 {ერთი მომხმარებელი} other {სია შემდეგი მომხმარებლებისგან} }", + "user-name-starts-with": "მომხმარებლების ჯგუფი რომლის სახელებიც იწყება '{{prefix}}' ით", + "type-dashboard": "დეშბორდი", + "type-dashboards": "დეშბორდები", + "list-of-dashboards": "{ count, plural, 1 {დეშბორდი} other {დეშბორდების სია} }", + "dashboard-name-starts-with": "დეშბორდები რომლების სახელებიც იწყება '{{prefix}}' -ით", + "type-alarm": "განგაში", + "type-alarms": "განგაშები", + "list-of-alarms": "{ count, plural, 1 {ერთი განგაში} other {განგაშის სია} }", + "alarm-name-starts-with": "განგაშები რომლების სახელებიც იწყება '{{prefix}}' -ით", + "type-rulechain": "წესების ჯაჭვი", + "type-rulechains": "წესების ჯაჭვები", + "list-of-rulechains": "{ count, plural, 1 {ერთი წესების ჯაჭვი} other {წესების ჯაჭვის სია} }", + "rulechain-name-starts-with": "წესების ჯაჭვები რომელთა სახელებიც იწყება '{{prefix}}' -ით", + "type-rulenode": "წესების ნოუდი", + "type-rulenodes": "წესების ნოუდები", + "list-of-rulenodes": "{ თვლა, plural, 1 {ერთი წესების ნოუდი} other {წესების ნოდების სია} }", + "rulenode-name-starts-with": "წესების ნოუდები რომლების სახელებიც იწყება '{{prefix}}' -ით", + "type-current-customer": "არსებული მომხმარებელი", + "search": "ობიექტის ძიება", + "selected-entities": "{ count, plural, 1 {1 ობიექტი} other {# ობიექტები} } არჩეულია", + "entity-name": "ობიექტის სახელი", + "entity-label": "ობიექტის ეტიკეტი", + "details": "ობიექტის დეტალები", + "no-entities-prompt": "ობიექტები არ მოიძებნა", + "no-data": "მონაცემები არ არის", + "columns-to-display": "სვეტების ჩვენება" + }, + "entity-field": { + "created-time": "შექმნის დრო", + "name": "სახელი", + "type": "ტიპი", + "first-name": "სახელი", + "last-name": "გვარი", + "email": "ელ.ფოსტა", + "title": "დასახელება", + "country": "ქვეყანა", + "state": "დაბა/რეგიონი", + "city": "ქალაქი", + "address": "მისამართი", + "address2": "მისამართი 2", + "zip": "ზიპ კოდი", + "phone": "ტელეფონი", + "label": "ნიშნული" + }, + "entity-view": { + "entity-view": "წარმოდგენილი ობიექტი", + "entity-view-required": "წარმოდგენილი ობიექტი სავალდებულოა", + "entity-views": "წარმოდგენილი ობიექტი", + "management": "წარმოდგენილი ობიექტის მართვა", + "view-entity-views": "წარმოდგენილი ობიექტის ნახვა", + "entity-view-alias": "ობიექტის წამოქმნის ზედმეტსახელი ", + "aliases": "ობიექტის წამოქმნის ზედმეტსახელი არ იძებნება", + "no-alias-matching": "ზედმეტსახელი '{{alias}}' არ იძებნება", + "no-aliases-found": "ზედმეტსახელი არ იძებნება", + "no-key-matching": "გასაღები '{{key}}' არ მოიძებნა", + "no-keys-found": "გასაღები არ მოიძებნა", + "create-new-alias": "ახალი სინონიმის შექმნა", + "create-new-key": "ახალი გასაღების შექმნა", + "duplicate-alias-error": "ნაპოვნია ზედმესახელის დუბლიკატი '{{alias}}'.
    ერთეული დაშბორდის პირობებში , წარმოდგენილი ობიექტის ზედმესახელი უნდა იყოს უნიკალური", + "configure-alias": "დააყენე ზედმეტსახელი '{{alias}}'", + "no-entity-views-matching": "ზწდმეტსახელის შესაბამისი ობიექტია '{{alias}}' არ იძებნება", + "alias": "ზედმეტსახელი", + "alias-required": "ობიექტის აღმნიშვნელი ზედმეტსახელი სავალდებულოა", + "remove-alias": "ობიექტის აღმნიშვნელი ზედმეტსახელის მოშორება", + "add-alias": "ობიექტის აღმნიშვნელი ზედმეტსახელის დამატება", + "name-starts-with": "ობიექტის აღმნიშვნელი ზედმეტსახელი რომლიც იწყება ", + "entity-view-list": "წარმოდგენილი ობიექტების სია", + "use-entity-view-name-filter": "ფილტრის გამოყენება", + "entity-view-list-empty": "წარმოდგენილი ობიექტების ცარიელი სია", + "entity-view-name-filter-required": "წარმოდგენილი ობიექტების ფილტრი დასახელებით სავალდებულოა", + "entity-view-name-filter-no-entity-view-matched": "წარმოდგენილი ობიექტები რომელთა დასახელება იწყება '{{entityView}}' ით არ იძებნება", + "add": "წარმოდგენილი ობიექტები", + "assign-to-customer": "დავალება მომხმარებელს", + "assign-entity-view-to-customer": "დავალებათა ერთობა მომხმარებლისთვის", + "assign-entity-view-to-customer-text": "აირჩიეთ წარმოდგენილი ობიექტები, რომლებიც კლიენტებისთვისაა", + "no-entity-views-text": "წარმოდგენილი ობიექტები არ იძებნება", + "assign-to-customer-text": "აირჩიეთ მომხმარებელი რომლისთვისაც საჭიროა წარმოდგენილი ობიექტები, ", + "entity-view-details": " წარმოდგენილი ობიექტების დეტალები", + "add-entity-view-text": " წარმოდგენილი ობიექტების დამატება", + "delete": "წარმოდგენილი ობიექტების წაშლა", + "assign-entity-views": "წარმოდგენილი ობიექტების დამატება", + "assign-entity-views-text": "მომხმარებლის { count, plural, 1 {1 წარმოდგენილი ობიექტები} other {# წარმოდგენილი ობიექტები} }", + "delete-entity-views": "წარმოდგენილი ობიექტების წაშლა", + "unassign-from-customer": "მომხმარებლისდან გამოხმობა", + "unassign-entity-views": "წარმოდგენილი ობიექტების გამოხმობა", + "unassign-entity-views-action-title": "გამოხმობა მომხმარებლის { count, plural, 1 {1 წარმოდგენილი ობიექტები} other {# წარმოდგენილი ობიექტები} }", + "assign-new-entity-view": "ახალი წარმოდგენილი ობიექტები", + "delete-entity-view-title": "ნამდვილათ გინდათ წარმოდგენილი ობიექტების წაშლა '{{entityViewName}}'?", + "delete-entity-view-text": "ყურადღება! თანხმობის შემთხვევაში ობიექტის ჯგუფი და ყველა შესაბამისი ინფორმაცია არ დაექვემდებარება აღდგენას", + "delete-entity-views-title": "დარწმუნებული ხართ რომ გინდათ წაშალოთ { count, plural, 1 {1 წარმოდგენილი ობიექტები} other {# წარმოდგენილი ობიექტები} }?", + "delete-entity-views-action-title": " წაშალა { count, plural, 1 {1 წარმოდგენილი ობიექტები} other {# წარმოდგენილი ობიექტები} }?", + "delete-entity-views-text": "ყურადღება! თანხმობის შემთხვევაში ობიექტის ჯგუფი და ყველა შესაბამისი ინფორმაცია არ დაექვემდებარება აღდგენას", + "unassign-entity-view-title": "დარწმუნებული ხართ რომ გინდათ გამოიხმოთ წარმოდგენილი ობიექტები '{{entityViewName}}'?", + "unassign-entity-view-text": "ყურადღება! თანხმობის შემთხვევაში ობიექტის ჯგუფი და ყველა შესაბამისი ინფორმაცია მომხმარებლისთვის იქნება მიუწვდომელი", + "unassign-entity-view": "წარმოდგენილი ობიექტების გამოხმობა", + "unassign-entity-views-title": "დარწმუნებული ხართ რომ გინდათ გამოიხმოთ { count, plural, 1 {1 წარმოდგენილი ობიექტები} other {# წარმოდგენილი ობიექტები} }?", + "unassign-entity-views-text": "თანხმობის შემთხვევაში ობიექტის ჯგუფი და ყველა შესაბამისი ინფორმაცია მომხმარებლისთვის იქნება მიუწვდომელი", + "entity-view-type": "წარმოდგენილი ობიექტების ტიპი", + "entity-view-type-required": "წარმოდგენილი ობიექტების ტიპი აუცილებელია", + "select-entity-view-type": "აირჩიეთ წარმოდგენილი ობიექტების ტიპი", + "enter-entity-view-type": "შეიყვანეთ წარმოდგენილი ობიექტების ტიპი", + "any-entity-view": "ნებისმიერი წარმოდგენილი ობიექტები", + "no-entity-view-types-matching": "წარმოდგენილი ობიექტების ტიპი '{{entitySubtype}}' არ იძებნება", + "entity-view-type-list-empty": "წარმოდგენილი ობიექტების ტიპი არ იძებნება", + "entity-view-types": "წარმოდგენილი ობიექტების ტიპი ", + "name": "სახელი", + "name-required": "სახელი სავალდებულოა", + "description": "აღწერა", + "events": "ივენთი", + "details": "დეტალები", + "copyId": "წარმოდგენილი ობიექტების ID ის კოპირება", + "idCopiedMessage": "idCopiedMessage", + "assignedToCustomer": "დანიშნულება მომხმარებლის ", + "unable-entity-view-device-alias-title": "ვერ ხერხდება წარმოდგენილი ობიექტების ზედმეტსახელის წაშლა", + "unable-entity-view-device-alias-text": "ვერ ხერხდება წარმოდგენილი ობიექტების ზედმეტსახელის წაშლა '{{entityViewAlias}}', რადგანაც გამოიყენება შემდეგი ვიჯეტებით
    {{widgetsList}}", + "select-entity-view": "წარმოდგენილი ობიექტების შერჩევა", + "make-public": "წარმოდგენილი ობიექტების ღია წვდომა", + "make-private": "ერთეულის ხედის დამალვა", + "start-date": "დაწყების თარიღი", + "start-ts": "დაწების დრო", + "end-date": "დასრულების თარიღი", + "end-ts": "დასრულების დრო", + "date-limits": "დროის ლიმიტი", + "client-attributes": "კლიენტ-ატრიბუტები", + "shared-attributes": "საერთო ატრიბუტები", + "server-attributes": "სერვერის ატრიბუტები", + "timeseries": "ტელემეტრია", + "client-attributes-placeholder": "მომხმარებლის ატრიბუტები", + "shared-attributes-placeholder": "ზოგადი ატრიბუტები", + "server-attributes-placeholder": "სერვერის ატრიბუტები", + "timeseries-placeholder": "ტელემეტრია", + "target-entity": "მიზნობრივი ობიექტი", + "attributes-propagation": "ატრიბუტები-გამრავლების", + "attributes-propagation-hint": "ერთეულის ხედი აუტომატურად დააკოპირებს მითითებულ ატრიბუტებს სამიზნე ატრიბუტებიდან ყოველთვის როდესაც განაახლებთ ან შეინახავთ ამ ერთეულის ხედს. წარმოდობის მიზნებიდან გამომდინარე სამიზნე ერთეულის ხედი არ გავრცელდება სათითაო ატრიბუტებზე ყოველი ცვლილების დროს. თქვენ შეძლებთ ჩართოთ ავტომატური გავრცელება \"ხედის დაკოპირება\"-ს წესის კონფიგურირებით თქვენს წესების ჯაჭვში \"ატრიბუტების დაპოსტვის\" და \"ატრიბუტების განახლება\" შეტყობინებების ახალი წესების კვანძში.", + "timeseries-data": "ტელემეტრიის მონაცემები", + "timeseries-data-hint": "მიზნობრივი ობიექტის ტელემეტრიის მონაცემების გასაღების დაყენება, რომელიც ხელმისაწვდომია აღნიშნული ობიექტის , საკითხავი ინფორმაცია", + "make-public-entity-view-title": "make-public-ერთეულის ხედი-სათაური", + "make-public-entity-view-text": "make-public- ერთეულის ხედვის ტექსტი", + "make-private-entity-view-title": "make-private- პირი-ხედი-სათაური", + "make-private-entity-view-text": "make-private- პირი-ხედვა-ტექსტი", + "search": "ძებნა", + "selected-entity-views": "შერჩეული ერთეულის შეხედულებები" + }, + "event": { + "event-type": "ღონისძიების ტიპი", + "type-error": "შეცდომა", + "type-lc-event": "საციცოცხლო ციკლის მოვლენა", + "type-stats": "სტატისტიკა", + "type-debug-rule-node": "დებაგი", + "type-debug-rule-chain": "დებაგები", + "no-events-prompt": "მოვლენა არ იძებნება", + "error": "შეცდომა", + "alarm": "განგაში", + "event-time": "ივენთის დრო", + "server": "სერვერი", + "body": "სხეული", + "method": "მეთოდი", + "type": "ტიპი", + "entity": "ობიექტი", + "message-id": "მესიჯის-ID", + "message-type": "მესიჯის ტიპი", + "data-type": "მონაცემთა ტიპი", + "relation-type": "ურთიერთობის ტიპი", + "metadata": "მეტამონაცემები", + "data": "მონაცემები", + "event": "ივენთი", + "status": "სტატუსი", + "success": "წარმატება", + "failed": "ვერ მოხერხდა", + "messages-processed": "შეტყობინებების დამუშავება", + "errors-occurred": "შეცდომები მოხდა" + }, + "extension": { + "extensions": "დამატებითი აპი", + "selected-extensions": "{ count, plural, 1 {1 დამატებითი აპი} other {# დამატებითი აპიები} } selected", + "type": "ტიპი", + "key": "გასაღები", + "value": "მნიშვნელობა", + "id": "ID", + "extension-id": "დამატებიტი-აპი-ID", + "extension-type": "დამატებითი აპის ტიპი", + "transformer-json": "JSON", + "unique-id-required": "ეს დამატებითი აპის ID უკვე არსებობს", + "delete": "დამატებითი აპის წაშლა", + "add": "დამატებითი აპის დამატება", + "edit": "დამატებითი აპის რედაქტირება", + "delete-extension-title": "დარწმუნებული ხართ რომ გინდათ წაშალოთ დამატებითი აპი '{{extensionId}}'?", + "delete-extension-text": "ყურადღება, თანხმობი შემდეგ დამატებითი აპი და ყველა მასთან დაკავშირებული ინფორმაცია სამუდამოდ წაიშლება", + "delete-extensions-title": "დარწმუნებული ხართ რომ გინდათ წაშალოთ { count, plural, 1 {1 დამატებითი აპი} other {# დამატებითი აპები} }?", + "delete-extensions-text": "ყურადღებით, თანხმობის შემდეგ ყველა დამატებითი აპი წაიშლება", + "converters": "გადამყვანები", + "converter-id": "გადამყვანის-ID", + "configuration": "კონფიგურაცია", + "converter-configurations": "გადამყვანის-კონფიგურაცია", + "token": "უსაფღთხოების ტოკენი", + "add-converter": "კონვერტორის დამატება", + "add-config": "კონვერტორის კონფიგურაციის დამატება", + "device-name-expression": "მოწყობილობა-სახელი-გამოხატვა", + "device-type-expression": "მოწყობილობის ტიპის გამოხატვა", + "custom": "დაკვეთით", + "to-double": "გაორმაგდება", + "transformer": "ტრანსფორმატორი", + "json-required": "ტრანსფორმატორის json– სავალდებულოა", + "json-parse": "ტრანსფორმატორის json -ის წაკითხვა შუძლებელია", + "attributes": "ატრიბუტები", + "add-attribute": "ატრიბუტის დამატება", + "add-map": "დაამატე მაპინგ ელემენტი", + "timeseries": "დროის სერიები", + "add-timeseries": "დროის სერიების დამატება", + "field-required": "ველი სავალდებულოა", + "brokers": "ბროკერები", + "add-broker": "ბროკერის დამატება", + "host": "მასპინძელი", + "port": "პორტი", + "port-range": "პროტი უნდა იყოს მომდევნო დიაპაზონში 1 დან 65535", + "ssl": "SSL", + "credentials": "ავტორიზაციის ინფორმაცია", + "username": "მომხმარებლის სახელი", + "password": "პაროლი", + "retry-interval": "ცდა-ინტერვალი მილიწამებში", + "anonymous": "ანონიმური", + "basic": "ძირითადი", + "pem": "PEM", + "ca-cert": "ca-cert", + "private-key": "Private key ფაილი", + "cert": "სერტიფიკატ ფაილი", + "no-file": "ფაილი არ არის არჩეული", + "drop-file": "ჩააგდეთ ფაილი ან დააჭირეთ ფაილის ასატვირთად", + "mapping": "მაპინგი", + "topic-filter": "თემის ფილტრი", + "converter-type": "გადამყვანის ტიპის", + "converter-json": "JSON", + "json-name-expression": "მოწყობილობის სახელი json გამოხატულება", + "topic-name-expression": "მოწყობილობის სახელი topic გამოხატულება", + "json-type-expression": "მოწყობილობის ტიპი json გამოხატულება", + "topic-type-expression": "მოწყობილობის ტიპი json გამოხატულება", + "attribute-key-expression": "ატრიბუტ გასღების გამოხატვა", + "attr-json-key-expression": "ატრიბუტ გასაღები json გამოხატვა", + "attr-topic-key-expression": "ატრიბუტ გასაღები topic გამოხატვა", + "request-id-expression": "მოთხოვნის ID გამოხატვა", + "request-id-json-expression": "მოთხოვნის ID JSON გამოხატვა", + "request-id-topic-expression": "მოთხოვნის ID TOPIC გამოხატვა", + "response-topic-expression": "პასუხი TOPIC გამოხატვა", + "value-expression": "მნიშვნელობის გამოხატვა", + "topic": "თემა", + "timeout": "დროის ამოწურვა მილიწამებში", + "converter-json-required": "გადამყვანი-json სავალდებულოა", + "converter-json-parse": "კონვერტორი json -ის წაკითხვა შუძლებელია", + "filter-expression": "ფილტრაცია", + "connect-requests": "დაკავშირების მოთხოვნა", + "add-connect-request": "მოწყობილობის გათიშვის მოთხოვნის დამატება", + "disconnect-requests": "მოწყობილობის გათიშვის მოთხოვნა", + "add-disconnect-request": "მოწყობილობის გათიშვის მოთხოვნა დამატება", + "attribute-requests": "მოთხოვნები ატრიბუტებისთვი", + "add-attribute-request": "ატრიბუტების მოთხოვნის დამატება", + "attribute-updates": "ატრიბუტების განახლება", + "add-attribute-update": "ატრიბუტების დამატების განახლება", + "server-side-rpc": "სერვერი RPC", + "add-server-side-rpc-request": "rpc სერვერის დამატება", + "device-name-filter": "მოწყობილობის სახელის ფილტრი", + "attribute-filter": "ფილტრი ატრიბუტებისათვის ", + "method-filter": " პროცესების ფილტრი", + "request-topic-expression": "მოთხოვნა-თემის გამოხატვა", + "response-timeout": "პასუხების დაყოვნების დრო მილიწამებში", + "topic-expression": "თემების დამომხატველი დასახელება", + "client-scope": "კლიენტის მასშტაბი", + "add-device": "მოწყობილობის დამატება", + "opc-server": "opc სერვერი", + "opc-add-server": "დაამატეთ სერვერი", + "opc-add-server-prompt": "გთხოვთ დაამატეთ სერვერი", + "opc-application-name": "opc- აპლიკაციის დასახელება", + "opc-application-uri": "uri აპლიკაცია", + "opc-scan-period-in-seconds": "სკანირების სიხშირე წამში", + "opc-security": "opc- უსაფრთხოება", + "opc-identity": "opc- ინდენთიფიკაცია", + "opc-keystore": "გასაღებების საცავი", + "opc-type": "opc ტიპი", + "opc-keystore-type": "opc-keystore ტიპი", + "opc-keystore-location": "opc-keystore-ადგილმდებარეობა", + "opc-keystore-password": "opc-keystore-პაროლი", + "opc-keystore-alias": "დამატებითი დასახელება", + "opc-keystore-key-password": "opc-keystore-key-პაროლი", + "opc-device-node-pattern": "opc- მოწყობილობა-კვანძი", + "opc-device-name-pattern": "მოწყობილობის პატერრ დასახელება", + "modbus-server": "სერვერი / მოწყობილობა", + "modbus-add-server": "სერვერის დამატება/მოწყობილობა", + "modbus-add-server-prompt": "სერვერის დამატება/მოწყობილობა", + "modbus-transport": "modbus-transport", + "modbus-tcp-reconnect": "კავშირის ავტომატური აღდგენა", + "modbus-rtu-over-tcp": "modbus-rtu-over-tcp", + "modbus-port-name": "თანმემდევრული პორტის დასახელება", + "modbus-encoding": "სიმბოლოების კოდირება", + "modbus-parity": " პარიტეტი", + "modbus-baudrate": "გადაცემის სიჩქარე", + "modbus-databits": "ბიტების ბაზა", + "modbus-stopbits": "modbus-stopbits", + "modbus-databits-range": "პარამეტრი databits იყენებს მნიშვნელს 7 ან 8", + "modbus-stopbits-range": "პარამეტრი stopbits იყენებს მნიშვნელს 1 ან 2", + "modbus-unit-id": "ID მოწყობილობა", + "modbus-unit-id-range": "ID მოწყობილობის დიაპაზონი 1 დან 247 მდე", + "modbus-device-name": "მოწყობილობის სახელი", + "modbus-poll-period": "გამოკითხვის სიხშირე (მილიწამებში)", + "modbus-attributes-poll-period": "ატრიბუტების გამოკითხვის სიხშირე (მილიწამები)", + "modbus-timeseries-poll-period": "ტელემეტრიის გამოკითხვის სიხშირე ( მილიწამები)", + "modbus-poll-period-range": "ტელემეტრიისთვის სავალდებულო გამოკითხვის სიხშირე უნდა იყოს ნულზე მეტი", + "modbus-tag": "modbus-tag", + "modbus-function": "modbus- ფუნქცია", + "modbus-register-address": "რეგისტრაციის მისამართი", + "modbus-register-address-range": "რეგისტრაციის მისამართი უნდა იყოს 0 დან 65535", + "modbus-register-bit-index": "ბიტის ნომერი", + "modbus-register-bit-index-range": "modbus-Register-bit-index-range", + "modbus-register-count": "რეგისტრების რაოდენობა", + "modbus-register-count-range": "დარეგისტრირებულების რაოდენობა ნოლზე მეტი", + "modbus-byte-order": "ბაიტების წყობა", + "sync": { + "status": "სტატუსი", + "sync": "დასინქრონილდა", + "not-sync": "არ დასინქრონილდა", + "last-sync-time": "ბოლო სინქრონიზაციის დრო", + "not-available": "მიუწვდომელია" + }, + "export-extensions-configuration": " გაფართოებების კონფიგურაციის ექსპორტი", + "import-extensions-configuration": "გაფართოებების კონფიგურაციის იმპორტი", + "import-extensions": "გაფართოების იმპორტი", + "import-extension": "გაფართოების იმპორტი", + "export-extension": " გაფართოების ექსპორტი", + "file": "ფაილი", + "invalid-file-error": " ფაილის არასწორი ფორმატი" + }, + "fullscreen": { + "expand": "გაფართოება ეკრანზე", + "exit": "გასასვლელი", + "toggle": "გადართეთ მთლიან ეკრანზე", + "fullscreen": "სრულ ეკრანზე" + }, + "function": { + "function": "ფუნქცია" + }, + "grid": { + "delete-item-title": "დარწმუნებული ხართ რომ გინდათ წაშალოთ წარმოდგენილი ობიექტები", + "delete-item-text": "ყურადღება! თანხმობის შემთხვევაში ობიექტის ჯგუფი და ყველა შესაბამისი ინფორმაცია არ დაექვემდებარება აღდგენას", + "delete-items-title": "დარწმუნებული ხართ რომ გინდათ წაშალოთ { count, plural, 1 {1 წარმოდგენილი ობიექტები} other {# წარმოდგენილი ობიექტები} }?", + "delete-items-action-title": " წაშალა { count, plural, 1 {1 წარმოდგენილი ობიექტები} other {# წარმოდგენილი ობიექტები} }?", + "delete-items-text": "ყურადღება! თანხმობის შემთხვევაში ობიექტის ჯგუფი და ყველა შესაბამისი ინფორმაცია არ დაექვემდებარება აღდგენას", + "add-item-text": "ახალი ობიექტის დამატება", + "no-items-text": "ობიექტი არ იძებნება", + "item-details": "ობიექტის დეტალები", + "delete-item": "ობიექტის წაშლა", + "delete-items": "ობიექტების წაშლა", + "scroll-to-top": "ზემოთ დაბრუნება" + }, + "help": { + "goto-help-page": "დახმარების გვერდზე გადასვლა" + }, + "home": { + "home": "მთავარი", + "profile": "პროფილი", + "logout": "გამოსვლა", + "menu": "მენიუ", + "avatar": "ავატარი", + "open-user-menu": "გახსენი მომხმარებლის მენიუ" + }, + "import": { + "no-file": "აირჩიეთ ფაილი", + "drop-file": "ჩააგდეთ JSON ფაილი ან დააჭირეთ ფაილის ასატვირთად", + "drop-file-csv": "ჩააგდეთ CSV ფაილი ან დააჭირეთ ფაილის ასატვირთად", + "column-value": "მნიშვნელობა", + "column-title": "სათაური", + "column-example": "მაგალითი ინფორმაციის მნიშნველობის", + "column-key": "ატრიბუტ/ტელემენტარი გასარები", + "csv-delimiter": "CSV დელიმიტერი", + "csv-first-line-header": "პირველი ხაზი შეიცავს სვეტის სახელებს", + "csv-update-data": "განაახლე ატრიბუტ/ტელემენტარი", + "import-csv-number-columns-error": "ფაილი უნდა შეიცავდეს მინიმუმ 2 სვეტს", + "import-csv-invalid-format-error": "არასწორი ფაილის ფორმატი. ხაზი: '{{line}}'", + "column-type": { + "name": "სახელი", + "type": "ტიპი", + "label": "იარლიყი", + "column-type": "სვეტის ტიპი", + "client-attribute": "კლიენტ-ატრიბუტი", + "shared-attribute": "საერთო-ატრიბუტი", + "server-attribute": "სერვერის ატრიბუტი", + "timeseries": "დროის სერიები", + "entity-field": "ობიექტის ველი", + "access-token": "წვდომის ტოკენი" + }, + "stepper-text": { + "select-file": "აირჩიე ფაილი", + "configuration": "დააიმპორტე კონფიგურაცია", + "column-type": "აირჩიე სვეტის ტიპი", + "creat-entities": "იქმნება ახალი ობიექტები", + "done": "შესრულებულია" + }, + "message": { + "create-entities": "{{count}} ახალი ობიექტები წარმატებით შეიქმნა.", + "update-entities": "{{count}} ობიექტები წარმატებით განახლდა.", + "error-entities": "მოხდა შეცდომა {{count}} ობიექტების შექმნისას" + } + }, + "item": { + "selected": "შერჩეული" + }, + "js-func": { + "no-return-error": "ფუნქცია უნდა აბრუნებდეს მნიშნველობას!", + "return-type-mismatch": "'{{type}}' ის დაბრუნების ფუნქცია", + "tidy": "დალაგება" + }, + "key-val": { + "key": "გასაღები", + "value": "მნიშვნელობა", + "remove-entry": "ამოღება", + "add-entry": "დამატება", + "no-data": "მონაცემები არ არის" + }, + "layout": { + "layout": "განლაგება", + "manage": "მართვა", + "settings": "პარამეტრები", + "color": "ფერი", + "main": "მთავარი", + "right": "მარჯვენა", + "select": "შერჩევა" + }, + "legend": { + "direction": "მიმართულება", + "position": "პოზიცია", + "show-max": "მაქსიმალური მნიშვნელის ჩვენება", + "show-min": "მინიმალური მნიშვნელის ჩვენება", + "show-avg": "საშუალო მნიშვნელის ჩვენება", + "show-total": "ფასის ჩვენება", + "settings": "პარამეტრების დაყენება", + "min": "მინ", + "max": "მაქ", + "avg": "საშუალო", + "total": "სულ", + "comparison-time-ago": { + "days": "დღის წინ", + "weeks": "კვირის წინ", + "months": "თვის წინ", + "years": "1 წლის წინ" + } + }, + "login": { + "login": "შესვლა", + "request-password-reset": "პაროლის გადატვირთვის მოთხოვნა", + "reset-password": "პაროლის გადატვირთვა", + "create-password": "პაროლის შექმნა", + "passwords-mismatch-error": "პაროლები არ ემთხვევა", + "password-again": "შეიყვანეთ პაროლი თავიდა", + "sign-in": "სისტემაში შესვლა", + "username": "მომხმარებლის სახელი", + "remember-me": "დამახსოვრება", + "forgot-password": " დაგავიწყდა პაროლი?", + "password-reset": "პაროლის აღდგენა", + "expired-password-reset-message": "პაროლს ვადა გაუვიდა, გთხოვთ შეიყვანოთ ახალი", + "new-password": "ახალი პაროლი", + "new-password-again": "გაიმეორეთ ახალი პაროლი", + "password-link-sent-message": "პაროლის შეცვლის მოთხოვნა გაიგზავნა", + "email": "ელ.ფოსტა" + }, + "position": { + "top": "ზევით", + "bottom": "ქვედა", + "left": "დარჩა", + "right": "მარჯვნივ" + }, + "profile": { + "profile": "პროფილი", + "last-login-time": "ბოლო შესვლის დრო", + "change-password": "პაროლის შეცვლა", + "current-password": "მიმდინარე პაროლი" + }, + "relation": { + "relations": "ურთიერთობს", + "direction": "მიმართულება", + "search-direction": { + "FROM": "დან", + "TO": "კენ" + }, + "direction-type": { + "FROM": "დან", + "TO": "კენ" + }, + "from-relations": "ურთიერთობებიდან", + "to-relations": "მოთხოვნა", + "selected-relations": "შერჩეული { count, plural, 1 {1 ურთიერთობები } other {#ურთიერთობები} }", + "type": "ტიპი", + "to-entity-type": "ერთეული ობიექტის ტიპისთვის", + "to-entity-name": "პირის სახელი", + "from-entity-type": "ერთეულის ტიპისგან", + "from-entity-name": "ობიექტიდან გამომდინარე", + "to-entity": "ობიექტისადმი", + "from-entity": "ობიექტიდან გამომდინარე", + "delete": "წაშლა", + "relation-type": "ურთიერთობის ტიპი", + "relation-type-required": "აუცილებელი ურთიერთობის ტიპი", + "any-relation-type": "ნებისმიერი ურთიერთობის ტიპის", + "add": "დამატება", + "edit": "რედაქტირება", + "delete-to-relation-title": "ნამდვილათ გსურთ წაშალოთ '{{entityName}}'?", + "delete-to-relation-text": "ყურედღება ! დადასტურების შემდეგ '{{entityName}}' იქნება უკან გამოხმობილი", + "delete-to-relations-title": "ნამდვილათ გსურთ წაშალოთ { count, plural, 1 {1 ქმედება} other {# ქმედება} }?", + "delete-to-relations-text": "ყურედღება ! დადასტურების შემდეგ შერჩეული ობიექტი იქნება უკან გამოხმობილი", + "delete-from-relation-title": "დარწმუნებული ხართ რო გინდათ '{{entityName}}' იდან ობიექტის წაშლა?", + "delete-from-relation-text": "ყურედღება ! დადასტურების შემდეგ '{{entityName}}' იქნება უკან გამოხმობილი", + "delete-from-relations-title": "ნამდვილათ გსურთ წაშალოთ { count, plural, 1 {1 ქმედება} other {# ქმედება} }?", + "delete-from-relations-text": "ყურედღება ! დადასტურების შემდეგ არჩეული ობიექტები იქნება უკან გამოხმობილი არსებული ობიექტებიდან", + "remove-relation-filter": "რელაციის ფილტრის მოშორება", + "add-relation-filter": "რელაციის ფილტრის დამატება", + "any-relation": "ნებისმიერი რელაცია", + "relation-filters": "რელაციის ფილტრები", + "additional-info": "დამატებითი ინფორმაცია (JSON)", + "invalid-additional-info": "ვერ ხერხდება დამატებითი ინფორმაციის (JSON) იდან წაკითხვა" + }, + "rulechain": { + "rulechain": "წესების წყობა", + "rulechains": "წესების წყობა", + "root": "ძირეული", + "delete": "წესების წყობის წაშლა", + "name": "სახელი", + "name-required": "სახელი (აუცილებელია", + "description": "აღწერა", + "add": "წესების წყობის დამატება", + "set-root": "წესების წყობის ძირეულად გადაკეთება", + "set-root-rulechain-title": "ნამდვილათ გინდათ '{{ruleChainName}}' წესების წყობის ძირეულად გადაკეთება", + "set-root-rulechain-text": "ყურედღება ! დადასტურების შემდეგ არჩეული წესების წყობა იქნება ძირეული და დაამუშავებს ყველა შემოსულ იმფოს", + "delete-rulechain-title": "ნამდვილათ გსურთ წაშალოთ წესების წყობა '{{ruleChainName}}'?", + "delete-rulechain-text": "ყურედღება ! დადასტურების შემდეგ არჩეული წესების წყობა და მასთან დაკავშირებული ყველა ინფო იქნება წაშლილი", + "delete-rulechains-title": "ნამდვილათ გსურთ წაშალოთ { count, plural, 1 {1 წესების წყობა} other {# წესების წყობა} }?", + "delete-rulechains-action-title": " წაშალა { count, plural, 1 {1 წესების წყობა} other {# წესების წყობა} }?", + "delete-rulechains-text": "ყურედღება ! დადასტურების შემდეგ არჩეული წესების წყობა და მასთან დაკავშირებული ყველა ინფო იქნება წაშლილი", + "add-rulechain-text": "ახალი წესების წყობის ფუნქციონალის დამატება", + "no-rulechains-text": " წესების წყობის ფუნქციონალი არ იძებნება", + "rulechain-details": " წესების წყობის ფუნქციონალის დეტალები", + "details": "დეტალები", + "events": "ივენთი", + "system": "სისტემური", + "import": " წესების წყობის ფუნქციონალის იმპორტი", + "export": " წესების წყობის ფუნქციონალის ექსპორტი", + "export-failed-error": "ვერ ხერხდება წესების წყობის ექსპორტირება {{error}}", + "create-new-rulechain": " ახალი წესების ჯაჭვის შექმნა", + "rulechain-file": "წესების ჯაჭვის ფაილი", + "invalid-rulechain-file-error": "ვერ ხერხდება წესების ჯაჭვის იმპორტირება, არასწორი ფორმატი ", + "copyId": "წესების ჯაჭვის ID მისამართის კოპირება", + "idCopiedMessage": "წესების ჯაჭვის ID მისამართის კოპირება გაცვლით ბუფერში", + "select-rulechain": "წესების ჯაჭვის არჩევა", + "no-rulechains-matching": "წესების ჯაჭვის შესატყვისი '{{entity}}' არ იძებნება", + "rulechain-required": "წესების ჯაჭვი აუცილებელია", + "management": "წესების ჯაჭვის მართვა", + "debug-mode": "გამართვის რეჟიმი" + }, + "rulenode": { + "details": "დეტალები", + "events": "ივენთი", + "search": " წესების ძებნა", + "open-node-library": "წესების ბიბლიოთეკის გახსნა", + "add": "წესების დამატება", + "name": "სახელი", + "name-required": "სახელი (აუცილებელია", + "type": "ტიპი", + "description": "აღწერა", + "delete": "წაშლა", + "select-all-objects": "გამოყავით ყველა წესი და კავშირი", + "deselect-all-objects": "გამოყავით ყველა წესი და კავშირი / გაუქმება", + "delete-selected-objects": "წაშლა/ ყველა წესი და კავშირი", + "delete-selected": "მონიშნულის წაშლა", + "select-all": "მონიშნე ყველა", + "copy-selected": "მონიშნულის კოპირება", + "deselect-all": "გააუქმეთ მონიშვნა", + "rulenode-details": "დეტალები წესებისათვის", + "debug-mode": "გამართვის რეჟიმი", + "configuration": "კონფიგურაცია", + "link": "ბმული", + "link-details": "ბმულთან დაკავშირებული დეტალები", + "add-link": "კავშირის დამატება", + "link-label": "ბმულის იარლიყი", + "link-label-required": "ბმულის იარლიყი/აუცილებელია", + "custom-link-label": "სამომხმარებლო ბმულის იარლიყი", + "custom-link-label-required": "სამომხმარებლო ბმულის იარლიყი აუცილებელია", + "link-labels": "ბმულის იარლიყი", + "link-labels-required": "ბმულის იარლიყი/აუცილებელია", + "no-link-labels-found": "ბმულის იარლიყი არ იძებნება", + "no-link-label-matching": "ბმულის იარლიყი '{{label}}' არ იძებნება", + "create-new-link-label": "ახალი ბმულის იარლიყის შექმნა", + "type-filter": " ფილტრი", + "type-filter-details": "შემოსული იმფოს ფილტრი", + "type-enrichment": "დაშენება", + "type-enrichment-details": "იმფოს დამატება მეტადატაში", + "type-transformation": "ტრანსფორმაცია", + "type-transformation-details": "მეტადატის იმფოს შეცვლა", + "type-action": "ქმედება", + "type-action-details": "დავალების შესრულება", + "type-external": "გარეგანი", + "type-external-details": "გარე სისტემებთან ურთიერრთობა", + "type-rule-chain": "წესების ჯაჭვი", + "type-rule-chain-details": "შემოსული იმფოს გადამისამართება სხვა წესების ჟაჭვით", + "type-input": " შეყვანა", + "type-input-details": "გონივრული შემომავალი წესების ჯაჭვის გადამისამართება შემდეგ დისჩიპლინებათ", + "type-unknown": "უცნობია", + "type-unknown-details": "უცნობი-დეტალები", + "directive-is-not-loaded": "კონფიგის დირექტივა '{{directiveName}}' არ იძებნება", + "ui-resources-load-error": "ui- რესურსების ჩატვირთვის შეცდომა", + "invalid-target-rulechain": "სამიზნე წესების ჯაჭვის გადაწყვეტა შეუძლებელია", + "test-script-function": "სატესტო სკრიფტ ფუნქცია", + "message": "მესიჯი", + "message-type": "მესიჯის ტიპი", + "select-message-type": "აირჩიეთ მესიჯის ტიპი", + "message-type-required": "მესიჯის ტიპი სავალდებულოა", + "metadata": "მეტამონაცემები", + "metadata-required": "მეტამონაცემები ვერ იქნება ცარიელი", + "output": "რეზულტატი", + "test": "ტესტი", + "help": "დახმარება", + "reset-debug-mode": "Debug რეჟიმის გათიშვა ყველა ნოდისთვის" + }, + "tenant": { + "tenant": "ტენანტი", + "tenants": "ტენანტი", + "management": "ტენანტების მართვა", + "add": "ტენანტის დამატება", + "admins": "ადმინისტრატორები", + "manage-tenant-admins": "ტენატ ადმინების მართვა", + "delete": "ტენანტის წაშლა", + "add-tenant-text": "ახალი ტენანტის დამატება", + "no-tenants-text": "ტენანტი ვერ მოიძებნა", + "tenant-details": "ტენანტის დეტალები", + "delete-tenant-title": "დარწმუნებული ხართ რომ გსურთ '{{tenantTitle}}'-ის წაშლა ?", + "delete-tenant-text": "ფრთხილად, დადასტურების შემდეგ ტენანტი და მასთან ასოცირებული მონაცემები იქნება დაკარგული.", + "delete-tenants-title": "დარწმუნებული ხართ რომ გსურთ წაშალოთ { count, plural, 1 {1 ტენანტი} other {# ტენანტი} }?", + "delete-tenants-action-title": "{ count, plural, 1 {1 ტენანტი} other {# ტენანტი} } წაშლა", + "delete-tenants-text": "ფრთხილად, დადასტურების შემდეგ ყველა მონიშნული ტენანტი და მასთან ასოცირებული მონაცემები იქნება დაკარგული.", + "title": "სათაური", + "title-required": "სათაური საჭიროა", + "description": "აღწერა", + "details": "დეტალები", + "events": "ივენთები", + "copyId": "ტენანტის ID-ის კოპირება", + "idCopiedMessage": "ტენანტის ID-ი დაკოპირებული აკლიპბორდში", + "select-tenant": "ტენანტის არჩევა", + "no-tenants-matching": "ტენანტი '{{entity}}' ვერ მოიძებნა.", + "tenant-required": "ტენანტი სავალდებულოა", + "search": "ტენანტის ძებნა", + "selected-tenants": "{ count, plural, 1 {1 ტენანტი} other {# ტენანტი} } მონიშნულია" + }, + "timeinterval": { + "seconds-interval": "{ seconds, plural, 1 {1 წამი} other {# წამი} }", + "minutes-interval": "{ minutes, plural, 1 {1 წუთი} other {# წუთი} }", + "hours-interval": "{ hours, plural, 1 {1 ს} other {# საათი} }", + "days-interval": "{ days, plural, 1 {1 დღე} other {# დღე} }", + "days": "დღე", + "hours": "საათი", + "minutes": "წუთი", + "seconds": "წამი", + "advanced": "დამატებითი" + }, + "timewindow": { + "days": "{ days, plural, 1 { დღე } other {# დღე } }", + "hours": "{ hours, plural, 0 { საათი } 1 {1 საათი } other {# საათი } }", + "minutes": "{ minutes, plural, 0 { წუთი } 1 {1 წუთი } other {# წუთი } }", + "seconds": "{ seconds, plural, 0 { წამი } 1 {1 წამი } other {# წამი } }", + "realtime": "რეალური დრო", + "history": "ისტორია", + "last-prefix": "ბოლო", + "period": "დან {{ startTime }} {{ endTime }} მდე", + "edit": "დროის ფანჯრის რედაქტირება", + "date-range": "თარიღის დიაპაზონი", + "last": "ბოლო", + "time-period": "დროის მონაკვეთი", + "hide": "დამალვა" + }, + "user": { + "user": "მომხმარებელი", + "users": "მომხმარებლები", + "customer-users": "კლიენტის მომხმარებლები", + "tenant-admins": "ტენანტ ადმინები", + "sys-admin": "სისტემური ადმინისტრატორი", + "tenant-admin": "ტენანტ ადმინისტრატორი", + "customer": "მომხმარებელი", + "anonymous": "ანონიმური", + "add": "მომხმარებლის დამატება", + "delete": "მომხმარებლის წაშლა", + "add-user-text": "ახალი მომხმარებლის დამატება", + "no-users-text": "მომხმარებლების ვერ მოიძებნა", + "user-details": "მომხმარებლის დეტალები", + "delete-user-title": "დარწმუნებული ხართ რომ გინდათ '{{userEmail}}' -ის წაშლა?", + "delete-user-text": "ფრთხილად, დადასტურების შემდეგ მომხმარებელი და მასთან ასოცირებული მონაცემები იქნება დაკარგული.", + "delete-users-title": "დარწმუნებული ხათ რომ გსურთ წაშალოთ { count, plural, 1 {1 მომხმარებელი} other {# მომხმარებლები} }?", + "delete-users-action-title": "{ count, plural, 1 {1 მომხმარებელი} other {# მომხმარებლები} } წაშლა", + "delete-users-text": "ფრთხილად, დადასტურების შემდეგ ყველა მონიშნული მომხმარებელი და მასთან ასოცირებული მონაცემები იქნება დაკარგული.", + "activation-email-sent-message": "აქტივაციი სელფოსტა წარმატებით გაიგზავნა!", + "resend-activation": "აქტივაციის გადაგზავნა", + "email": "ელ.ფოსტა", + "email-required": "ელ.ფოსტა საჭირო", + "invalid-email-format": "არასწორი ელ.ფოსტის ფორმატი", + "first-name": "სახელი", + "last-name": "გვარი", + "description": "აღწერა", + "default-dashboard": "ნაგულისხმევი დეშბორდი", + "always-fullscreen": "ყოველთვის მთელს ეკრანზე", + "select-user": "მომხმარებლის არჩევა", + "no-users-matching": "მომხმარებელი '{{entity}}' ვერ მოიძებნა.", + "user-required": "მომხმარებლი სავალდებულოა", + "activation-method": "აქტივაციის მეთოდი", + "display-activation-link": "აქტვივაციის ბმულის ჩვენება", + "send-activation-mail": "აქტივაციის ელფოსტის გაგზავნა", + "activation-link": "მომხმარებლის აქტივაციის ბმული", + "activation-link-text": "იმისთვის რომ გააქტიუროთ მომხმარებეკი გამოიყენეთ შემდეგი აქტივაციის ბმული :", + "copy-activation-link": "აქტივაციის ბმული დაკოპირება", + "activation-link-copied-message": "აქტივაციის ბმული დაკოპირებულია კლიპბორდში", + "details": "დეტალები", + "login-as-tenant-admin": "შესვლა როგორც ტენანტ ადმინი", + "login-as-customer-user": "შესვლა, როგორც კლიენტის მომხმარებელი", + "search": "მომხმარებლების ძებნა", + "selected-users": "{ count, plural, 1 {1 მომხმარებელი} other {# მომხმარებლები} } მონიშნულია", + "disable-account": "მომხმარებლის ანგარიშის გამორთვა", + "enable-account": "მომხმარებლის ანგარიშის ჩართვა", + "enable-account-message": "მომხმარებლის ანგარიშის წარმატებით ჩაირთო!", + "disable-account-message": "მომხმარებლის ანგარიშის წარმატებით გამოირთო!" + }, + "value": { + "type": "მნიშვნელობის ტიპი", + "string": "სტრინგი", + "string-value": "სტრინგის მნიშვნელობა", + "integer": "მთელი რიცხვი", + "integer-value": "მთელი რიცხვის მნიშვნელობა", + "invalid-integer-value": "არასწორი მთელი რიცხვის მნიშვნელობა", + "double": "ორმაგი", + "double-value": "ორმაგი მნიშვნელობა", + "boolean": "ლოგიკური", + "boolean-value": "ლოგიკური მნიშვნელობა", + "false": "ცრუ", + "true": "ჭეშმარიტი", + "long": "გრძელი" + }, + "widget": { + "widget-library": "ვიჯეტების ბიბლიოთეკა", + "widget-bundle": "ვიჯეტების ნაკრები", + "select-widgets-bundle": "ვიჯედების ნაკრების არჩევა", + "management": "ვიჯეტების მენეჯმენტი", + "editor": "ვიჯეტების რედაქტორი", + "widget-type-not-found": "შეცდომა ვიჯეტის კონფიგურაციის ჩატვირთვისას.
    სავარაუდოთ დაკავშირებულია ვიჯეტის ტიოის წაშლასთან.", + "widget-type-load-error": "ვიჯეტი ვერ ჩაიტვირთა შემდეგი შეცდომის გამო:", + "remove": "ვიჯეტის წაშლა", + "edit": "ვიჯეტის რედაქტირება", + "remove-widget-title": "დარწმუნებული ხართ რომ გსურთ წაშალოთ ვიჯეტი '{{widgetTitle}}'?", + "remove-widget-text": "დადასტურების შემდეგ ვიჯეტი და მასთან ასიცირებული მონაცემები იქნება დაკარგული.", + "timeseries": "მონაცემთა სერია", + "search-data": "საძიებო მონაცემები", + "no-data-found": "მონაცემი ვერ მოიძებნა", + "latest-values": "უახლესი მნიშვნელობები", + "rpc": "ვიჯეტის კონტროლი", + "alarm": "ვიჯეტის განგაში", + "static": "სტატიკური ვიჯეტი", + "select-widget-type": "აირჩიეთ ვიჯეტის ტიპი", + "missing-widget-title-error": "ვიჯეტის სატაური უნდა იყოს მითითებული", + "widget-saved": "ვიჯეტი შენახულია", + "unable-to-save-widget-error": "შენახვა შეუძლებელია ვიჯეტის შეცდომა", + "save": "ვიჯეტის შენახვა", + "saveAs": "შეინახე ვიჯეტი როგორც", + "save-widget-type-as": "შეინახე ვიჯეტის ტიპი როგორც", + "save-widget-type-as-text": "შეიყვანეთ ახალი ვიჯეტის სატაური ან/და აირჩიეთ სამიზნე ვიჯეტიბის ნაკრებიდან.", + "toggle-fullscreen": "მთელს ეკრანზე გაშლა", + "run": "ვიჯეტის გაშვება", + "title": "ვიჯეტის სათაური", + "title-required": "ვიჯეტის სატაური საჭიროა.", + "type": "ვიჯეტის ტიპი", + "resources": "რესურსები", + "resource-url": "JavaScript/CSS URL", + "remove-resource": "რესურსის წაშლა", + "add-resource": "რესურსის დამატება", + "html": "HTML", + "tidy": "აკურატული", + "css": "CSS", + "settings-schema": "პარამეტრების სქემა", + "datakey-settings-schema": "მონაცემტა გასაღები პარამეტრების სქემა", + "javascript": "javascript", + "js": "JS", + "remove-widget-type-title": "დარწმუნებული ხართ რომ გსურთ ვიჯეტის ტიპი '{{widgetName}}'?", + "remove-widget-type-text": "დასტურის შემთხვევაში ვიჯეტი და მასთან ასოცირებული მონაცემები დაიკარგება.", + "remove-widget-type": "ვიჯეტის ტიპის ამოღება", + "add-widget-type": "ახალი ვიჯეტის ტიპი", + "widget-type-load-failed-error": "ვიჯეტის ტიპის ჩატვირთვა ვერ მოხერხდა!", + "widget-template-load-failed-error": "ვიჯეტის შაბლონი ჩატვირთვა ვერ მოხერხდა!", + "add": "ვიჯეტის დამატება", + "undo": "ვიჯეტის ცვლილების გაუქმება", + "export": "ვიჯეტის ექსპორტი" + }, + "widget-action": { + "header-button": "ვიჯეტის თავსართის ღილაკი", + "open-dashboard-state": "ახლი დეშბორდის მდგომარეობაზე გადასვლა", + "update-dashboard-state": "მიმდინარე დეშბორდის მდგომარეობის განახლება", + "open-dashboard": "სხვა დეშბორდზე გადასვლა", + "custom": "სხვა მოქმედება", + "custom-pretty": "სხვა მოქმედება (HTML შაბლონით)", + "target-dashboard-state": "სამიზნე დეშბორდის მდგომარეობა", + "target-dashboard-state-required": "სამიზნე დეშბორდის მდგომარეობა საჭიროა", + "set-entity-from-widget": "ობიექტის მიბიჭება ვიჯეტიდან", + "target-dashboard": "სამიზნე დეშბორდი", + "open-right-layout": "დეშბორდის მარჯვენა განლაგების გახსნა (მობილურის ხედით)" + }, + "widgets-bundle": { + "current": "მიმდინარე ნაკრები", + "widgets-bundles": "ვიჯეტების ნაკრები", + "add": "ვიჯეტების ნაკრების დამატება", + "delete": "ვიჯეტების ნაკრების წაშლა", + "title": "სათაური", + "title-required": "სათაური საჭიროა.", + "add-widgets-bundle-text": "ახალი ვიჯეტების ნაკრების დამატება", + "no-widgets-bundles-text": "ვიჯეტების ნაკრები ვერ მოიძებნა", + "empty": "ვიჯეტების ნაკრები ცარიელია", + "details": "დეტალები", + "widgets-bundle-details": "ვიჯეტების ნაკრების დეტალები", + "delete-widgets-bundle-title": "დარწმუნებული ხართ რომ გსურთ ვიჯეტების ნაკრების წაშლა '{{widgetsBundleTitle}}'?", + "delete-widgets-bundle-text": "ფრთხილად, დადასტურების შემდეგ ვიჯეტების ნაკრები და მასთან ასოცირებული მონაცემები იქნება დაკარგული.", + "delete-widgets-bundles-title": "დარწმუნებული ხარ რომ გსურს წაშალო { count, plural, 1 {1 ვიჯეტის ნაკრები} other {# ვიჯეტების ნაკრები} }?", + "delete-widgets-bundles-action-title": "{ count, plural, 1 {1 ვიჯეტის ნაკრები} other {# ვიჯეტის ნაკრებები} } წაშლა", + "delete-widgets-bundles-text": "ფრთხილად, დადასტურების შემდეგ ყველა მონიშნული ვიჯეტების ნაკრები და მასთან ასოცირებული მონაცემები იქნება დაკარგული.", + "no-widgets-bundles-matching": "ვიჯეტის ნაკრები '{{widgetsBundle}}' ვერ მოიძებნა.", + "widgets-bundle-required": "ვიჯეტის ნაკრები სავალდებულოა.", + "system": "სისტემა", + "import": "ვიჯეტის ნაკრების იმპორტი", + "export": "ვიჯეტის ნაკრების ექსპორტი", + "export-failed-error": "ვიჯეტის ნაკრების ექსპორტი ვერ განხორციელებული შეცდომა: {{error}}", + "create-new-widgets-bundle": "ახალი ვიჯეტის ნაკრების შექმნა", + "widgets-bundle-file": "ვიჯეტებებსი ნაკრების ფაილი", + "invalid-widgets-bundle-file-error": "ვიჯეტის ნაკრების იმპორტი ვერ განხორციელდა: არასწორი სტრუქტურა." + }, + "widget-config": { + "data": "მონაცემები", + "settings": "პარამეტრები", + "advanced": "დამატებითი", + "title": "სათაური", + "title-tooltip": "სათაურის განმარტება", + "general-settings": "ძირითადი პარამეტრები", + "display-title": "სათაურის ჩვენება", + "drop-shadow": "ჩრდილი", + "enable-fullscreen": "ჩართვა მთელს ეკრანზე", + "background-color": "ფონის ფერი", + "text-color": "ტექსტის ფერი", + "padding": "დაშორება", + "margin": "ზღვარი", + "widget-style": "ვიჯეტის სტილი", + "title-style": "სათაურის სტილი", + "mobile-mode-settings": "მობილური რეჟიმის პარამეტრები", + "order": "განლაგება", + "height": "სიმაღლე", + "units": "სპეციალური სიმბოლო შემდეგი მნიშვნელობისთვის", + "decimals": "ციფრების რაოდენობა წერტილის შემდეგ", + "timewindow": "ქრონომეტრაჟი", + "use-dashboard-timewindow": "დეშბორდის ქრონომეტრაჟის გამოყენება", + "display-timewindow": "ქრონომეტრაჟის ჩვენება", + "display-legend": "ლეგენდის ჩვენება", + "datasources": "მონაცემთა წყაროები", + "maximum-datasources": "მაქს. { count, plural, 1 {1 მონცემთა წყარო დაშვებულია.} other {# მონაცემტა წყაროები დაშვებულია} }", + "datasource-type": "ტიპი", + "datasource-parameters": "პარამეტრები", + "remove-datasource": "მონაცემთა წყაროს წაშლა", + "add-datasource": "დაამატეთ მონაცემთა წყარო", + "target-device": "სამიზნე მოწყობილობა", + "alarm-source": "განგაშის წყარო", + "actions": "მოქმედებები", + "action": "მოქმედება", + "add-action": "მოქმედების დამატება", + "search-actions": "საძიებო მოქმედებები", + "action-source": "მოქმედების წყარო", + "action-source-required": "მოქმედების წყარო საჭიროა.", + "action-name": "სახელი", + "action-name-required": "მოქმედების სახელი საჭიროა.", + "action-name-not-unique": "სხვა მოქმედება იგივე სახელით უკვე არსებობს.
    მოქმედების სახელი უნდა იყოს უნიკალური ერთი და იგივე მონაცემთა წყაროსთვის.", + "action-icon": "ხატულა", + "action-type": "ტიპი", + "action-type-required": "მოქმედების ტიპი საჭიროა.", + "edit-action": "მოქმედების რედაქტირება", + "delete-action": "მოქმედების წაშლა", + "delete-action-title": "მოქმედების ვიჯეტის წაშლა", + "delete-action-text": "დარწმუნებული ხართ რომ გსურთ წაშალოთ მოწმდებების ვიჯეტი სახელად '{{actionName}}'?", + "display-icon": "სათაურის ხატულას ჩვენება", + "icon-color": "ხატულას ფერი", + "icon-size": "ხატულას ზომა" + }, + "widget-type": { + "import": "ვიჯეტის ტიპის იმპორტი", + "export": "ვიჯეტის ტიპის ექსპორტი", + "export-failed-error": "ვიჯეტის ტიპის ექსპორტი ვერ განხორციელდა: {{error}}", + "create-new-widget-type": "ახალი ვიჯეტისტიპის შექმნა", + "widget-type-file": "ვიჯეტის ტიპის ფაილი", + "invalid-widget-type-file-error": "ვიჯეტის ტიპის იმპორტი ვერ განხორციელდ: არასწირი მონაცემტა სტრუქტურა." + }, + "widgets": { + "date-range-navigator": { + "localizationMap": { + "Sun": "მზე", + "Mon": "ორშ", + "Tue": "სამ", + "Wed": "ოთხ", + "Thu": "ხუთ", + "Fri": "პარ", + "Sat": "შაბ", + "Jan": "იან", + "Feb": "თებ", + "Mar": "მარ", + "Apr": "აპრილი", + "May": "მაისი", + "Jun": "ივნ", + "Jul": "იული", + "Aug": "აგვისტო", + "Sep": "სექტ", + "Oct": "ოქტომბერი", + "Nov": "ნოემბერი", + "Dec": "დეკ", + "January": "იანვარი", + "February": "თებერვალი", + "March": "მარტი", + "April": "აპრილი", + "June": "ივნისი", + "July": "ივლისი", + "August": "აგვისტო", + "September": "სექტემბერი", + "October": "ოქტომბერი", + "November": "ნოემბერი", + "December": "დეკემბერი", + "Custom Date Range": "თარიღის მორგებული ზომა", + "Date Range Template": "თარიღი დიაპაზონის შაბლონი", + "Today": "დღეს", + "Yesterday": "გუშინ", + "This Week": "მიმდინარე კვირა", + "Last Week": "წინა კვირა", + "This Month": "მიმდინარე თვე", + "Last Month": "გასული თვე", + "Year": "წელი", + "This Year": "მიმდინარე წელი", + "Last Year": "გასული წელი", + "Date picker": "თარიღის ამომრჩევი", + "Hour": "საათი", + "Day": "დღე", + "Week": "კვირა", + "2 weeks": "2 კვირა", + "Month": "თვე", + "3 months": "3 თვე", + "6 months": "6 თვე", + "Custom interval": "არჩევითი ინტერვალი", + "Interval": "ინტერვალი", + "Step size": "ნაბიჯის ზომა", + "Ok": "კარგი" + } + }, + "input-widgets": { + "attribute-not-allowed": "პარამეტრის ატრიბუტის ვერ გამოიყენებთ ამ ვიჯეტისთვის", + "blocked-location": "გეოლოკაცია ადაბლოკილია თქვენს ბროუზერში", + "claim-device": "მოწყობილობის გააქტიურება", + "claim-failed": "მოწყობილობის გააქტიურება ვერ მოხერხდა!", + "claim-not-found": "მოწყობილობა ვერ მოიძებნა!", + "claim-successful": "მოწყობილობა გააქტიურდა წარმატებით!", + "date": "თარიღი", + "device-name": "მოწყობილობის სახელი", + "device-name-required": "მოწყობილობის სახელი სავალდებულუა", + "discard-changes": "ცვლილებების გაუქმება", + "entity-attribute-required": "ობიექტის ატრიბუტი სავალდებულოა", + "entity-coordinate-required": "ორივე ველი: გრძედი და განედი აუცილებელია", + "entity-timeseries-required": "ობიექტის თაიმსერია აუცილებელია", + "get-location": "მიმდინაე ადგილმდებარეობის გაგება", + "latitude": "განედი", + "longitude": "გრძედი", + "not-allowed-entity": "მონიშნულ ობიექტს არ გააჩნია გაზიარებადი ატრიბუტები", + "no-attribute-selected": "ატრიბუტებაი არ არის მონიშნული", + "no-datakey-selected": "მონაცემთა გასაღები არ არის მონიშნული", + "no-coordinate-specified": "მონაცემთა გასაღები გრძედ/განედისთვის არ არის მითითებული", + "no-entity-selected": "ობიექტი არ არის მონიშნული", + "no-image": "სურათი არ არის", + "no-support-geolocation": "თქვენ ბროუზერს არ გააჩნია გეოლოკაციის მხარდაჭერა", + "no-support-web-camera": "ვებ-კამერის მიუწვდომელია", + "no-timeseries-selected": "მონაცემტასერია არ არის მონიშნული", + "secret-key": "საიდუმლო გასაღები", + "secret-key-required": "საიდუმლო გასაღები საჭიროა", + "switch-attribute-value": "ობიექტის ატრიბუტის მნიშვნელობის გადართვა", + "switch-camera": "კამერის გადართვა", + "switch-timeseries-value": "ობიექტის მონაცემთა სერიის მნიშვნელობის გართვა", + "take-photo": "ფოტოს გადაღება", + "time": "დრო", + "timeseries-not-allowed": "მონაცემთა სერიის პარამეტრი ვერ გააქტიურდება ამ ვიჯეტისთვის", + "update-failed": "განახლება ვერ მოხერხდა", + "update-successful": "წარმატებით განახლდა", + "update-attribute": "ატრიბუტის განახლება", + "update-timeseries": "მონაცემთა სერიის განახლება", + "value": "მნიშვნელობა" + } + }, + "icon": { + "icon": "ხატულა", + "select-icon": "აირჩიეთ ხატულა", + "material-icons": "მასალის ხატულები", + "show-all": "მაჩვენე ყველა ხატულა" + }, + "custom": { + "widget-action": { + "action-cell-button": "მოქმედების უჯრედ-ღილაკი", + "row-click": "რიგზე დაჭერით", + "polygon-click": "პოლიგონზე დაჭერით", + "marker-click": "მარკერის დაჭერით", + "tooltip-tag-action": "ინსტრუმენტის სახელმძღვანელო", + "node-selected": "მონიშნულ კვანძზე", + "element-click": "HTML ელემენტზე დაჭერით", + "pie-slice-click": "სლაისზე დაჭერით", + "row-double-click": "რიგზე ორჯერ დაკლიკებით" + } + }, + "language": { + "language": "ენა" + } +} diff --git a/ui-ngx/src/assets/locale/locale.constant-ko_KR.json b/ui-ngx/src/assets/locale/locale.constant-ko_KR.json new file mode 100644 index 0000000000..c255440ba7 --- /dev/null +++ b/ui-ngx/src/assets/locale/locale.constant-ko_KR.json @@ -0,0 +1,1396 @@ +{ + "access": { + "unauthorized": "승인되지 않음", + "unauthorized-access": "승인되지 않은 접근", + "unauthorized-access-text": "이 리소스에 접근하려면 로그인해야 합니다!", + "access-forbidden": "접근 금지", + "access-forbidden-text": "접근 권한이 없습니다!
    만일 이 페이지에 계속 접근하려면 다른 사용자로 로그인 하세요.", + "refresh-token-expired": "세션이 만료되었습니다", + "refresh-token-failed": "세션을 새로 고칠 수 없습니다.", + "permission-denied": "권한이 없습니다", + "permission-denied-text": "이 작업을 수행할 권한이 없습니다!" + }, + "action": { + "activate": "활설화", + "suspend": "비활성화", + "save": "저장", + "saveAs": "다른 이름으로 저장", + "cancel": "취소", + "ok": "확인", + "delete": "삭제", + "add": "추가", + "yes": "네", + "no": "아니오", + "update": "업데이트", + "remove": "제거", + "search": "검색", + "clear-search": "검색 초기화", + "assign": "할당", + "unassign": "비할당", + "share": "Share", + "make-private": "비공개로 설정", + "apply": "적용", + "apply-changes": "변경사항 적용", + "edit-mode": "수정 모드", + "enter-edit-mode": "수정 모드 진입", + "decline-changes": "변경사항 포기", + "close": "닫기", + "back": "뒤로", + "run": "실행", + "sign-in": "로그인!", + "edit": "수정", + "view": "보기", + "create": "만들기", + "drag": "끌기", + "refresh": "새로고침", + "undo": "취소", + "copy": "복사", + "paste": "붙여넣기", + "copy-reference": "참조 복사", + "paste-reference": "참조 붙여넣기", + "import": "가져오기", + "export": "내보내기", + "share-via": "Share via {{provider}}" + }, + "aggregation": { + "aggregation": "집합", + "function": "데이터 집합 함수", + "limit": "최대 값", + "group-interval": "그룹 간격", + "min": "최소", + "max": "최대", + "avg": "평균", + "sum": "합계", + "count": "숫자", + "none": "없음" + }, + "admin": { + "general": "일반", + "general-settings": "일반 설정", + "outgoing-mail": "메일 전송", + "outgoing-mail-settings": "메일 전송 설정", + "system-settings": "시스템 설정", + "test-mail-sent": "테스트 메일이 성공적으로 전송되었습니다!", + "base-url": "기본 URL", + "base-url-required": "기본 URL을 입력해야 합니다.", + "mail-from": "보내는 사람", + "mail-from-required": "보내는 사람을 입력해야 합니다.", + "smtp-protocol": "SMTP 프로토콜", + "smtp-host": "SMTP 호스트", + "smtp-host-required": "SMTP 호스트를 입력해야 합니다.", + "smtp-port": "SMTP 포트", + "smtp-port-required": "SMTP 포트를 입력해야 합니다.", + "smtp-port-invalid": "올바른 SMTP 포트가 아닙니다.", + "timeout-msec": "제한시간 (ms)", + "timeout-required": "제한시이 입력되지 않았습니다.", + "timeout-invalid": "제한시간이 올바르게 입력되지 않았습니다.", + "enable-tls": "TLS 사용", + "tls-version" : "TLS 버전", + "send-test-mail": "테스트 메일 보내기" + }, + "alarm": { + "alarm": "알람", + "alarms": "알람", + "select-alarm": "알람 선택", + "no-alarms-matching": "'{{entity}}'에 대한 알람이 존재하지 않습니다.", + "alarm-required": "알람이 필요합니다", + "alarm-status": "알람 상태", + "search-status": { + "ANY": "Any", + "ACTIVE": "활성", + "CLEARED": "Cleared", + "ACK": "수용", + "UNACK": "불수용" + }, + "display-status": { + "ACTIVE_UNACK": "Active Unacknowledged", + "ACTIVE_ACK": "Active Acknowledged", + "CLEARED_UNACK": "Cleared Unacknowledged", + "CLEARED_ACK": "Cleared Acknowledged" + }, + "no-alarms-prompt": "No alarms found", + "created-time": "생성된 시간", + "type": "종류", + "severity": "심각도", + "originator": "창시자", + "originator-type": "창시자 종류", + "details": "자세히", + "status": "상태", + "alarm-details": "Alarm details", + "start-time": "시작 시각", + "end-time": "마지막 시각", + "ack-time": "Acknowledged time", + "clear-time": "Cleared time", + "severity-critical": "심각한", + "severity-major": "주요한", + "severity-minor": "작은", + "severity-warning": "경고", + "severity-indeterminate": "중간", + "acknowledge": "수용", + "clear": "지우기", + "search": "알람 검색", + "selected-alarms": "{ count, plural, 1 {1 alarm} other {# alarms} } 선택됨", + "no-data": "표시할 데이터가 없습니다", + "polling-interval": "Alarms polling interval (sec)", + "polling-interval-required": "Alarms polling interval is required.", + "min-polling-interval-message": "At least 1 sec polling interval is allowed.", + "aknowledge-alarms-title": "Acknowledge { count, plural, 1 {1 alarm} other {# alarms} }", + "aknowledge-alarms-text": "Are you sure you want to acknowledge { count, plural, 1 {1 alarm} other {# alarms} }?", + "clear-alarms-title": "Clear { count, plural, 1 {1 alarm} other {# alarms} }", + "clear-alarms-text": "Are you sure you want to clear { count, plural, 1 {1 alarm} other {# alarms} }?" + }, + "alias": { + "add": "Add alias", + "edit": "Edit alias", + "name": "Alias name", + "name-required": "Alias name is required", + "duplicate-alias": "Alias with same name is already exists.", + "filter-type-single-entity": "Single entity", + "filter-type-entity-list": "Entity list", + "filter-type-entity-name": "Entity name", + "filter-type-state-entity": "Entity from dashboard state", + "filter-type-state-entity-description": "Entity taken from dashboard state parameters", + "filter-type-asset-type": "Asset type", + "filter-type-asset-type-description": "Assets of type '{{assetType}}'", + "filter-type-asset-type-and-name-description": "Assets of type '{{assetType}}' and with name starting with '{{prefix}}'", + "filter-type-device-type": "Device type", + "filter-type-device-type-description": "Devices of type '{{deviceType}}'", + "filter-type-device-type-and-name-description": "Devices of type '{{deviceType}}' and with name starting with '{{prefix}}'", + "filter-type-relations-query": "Relations query", + "filter-type-relations-query-description": "{{entities}} that have {{relationType}} relation {{direction}} {{rootEntity}}", + "filter-type-asset-search-query": "Asset search query", + "filter-type-asset-search-query-description": "Assets with types {{assetTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}", + "filter-type-device-search-query": "Device search query", + "filter-type-device-search-query-description": "Devices with types {{deviceTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}", + "entity-filter": "Entity filter", + "resolve-multiple": "Resolve as multiple entities", + "filter-type": "Filter type", + "filter-type-required": "Filter type is required.", + "entity-filter-no-entity-matched": "No entities matching specified filter were found.", + "no-entity-filter-specified": "No entity filter specified", + "root-state-entity": "Use dashboard state entity as root", + "root-entity": "Root entity", + "state-entity-parameter-name": "State entity parameter name", + "default-state-entity": "Default state entity", + "default-entity-parameter-name": "By default", + "max-relation-level": "Max relation level", + "unlimited-level": "Unlimited level", + "state-entity": "Dashboard state entity", + "all-entities": "All entities", + "any-relation": "any" + }, + "asset": { + "asset": "자산", + "assets": "자산", + "management": "자산 관리", + "view-assets": "자산 보기", + "add": "자산 추가", + "assign-to-customer": "고객에게 자산 지정", + "assign-asset-to-customer": "자산을 고객에게 지정", + "assign-asset-to-customer-text": "고객에게 지정할 자산을 선택하세요", + "no-assets-text": "아무 자산도 없습니다", + "assign-to-customer-text": "자산에 지정될 고객을 선택하세요", + "public": "공개", + "assignedToCustomer": "지정된 고객", + "make-public": "자산을 공개로 설정", + "make-private": "자산을 비공개로 설정", + "unassign-from-customer": "고객 지정 해제", + "delete": "자산 삭제", + "asset-public": "공개된 자산", + "asset-type": "자산 종류", + "asset-type-required": "자산 종류를 선택하세요.", + "select-asset-type": "자산 종류 선택", + "enter-asset-type": "자산 종류 입력", + "any-asset": "모든 자산", + "no-asset-types-matching": "'{{entitySubtype}}'과 일치하는 자산 종류가 없습니다.", + "asset-type-list-empty": "아무 자산 종류도 선택되지 않았습니다.", + "asset-types": "자산 종류", + "name": "이름", + "name-required": "이름을 입력하세요.", + "description": "설명", + "type": "종류", + "type-required": "종류를 입력하세요.", + "details": "자세히", + "events": "이벤트", + "add-asset-text": "새로운 자산 추가", + "asset-details": "자산 자세히", + "assign-assets": "자산 지정", + "assign-assets-text": "자산 { count, plural, 1 {1 asset} other {# assets} }을 고객에게 지정", + "delete-assets": "자산 삭제", + "unassign-assets": "자산 지정 해제", + "unassign-assets-action-title": "Unassign { count, plural, 1 {1 asset} other {# assets} } from customer", + "assign-new-asset": "새로운 자산 지정", + "delete-asset-title": "Are you sure you want to delete the asset '{{assetName}}'?", + "delete-asset-text": "Be careful, after the confirmation the asset and all related data will become unrecoverable.", + "delete-assets-title": "Are you sure you want to delete { count, plural, 1 {1 asset} other {# assets} }?", + "delete-assets-action-title": "Delete { count, plural, 1 {1 asset} other {# assets} }", + "delete-assets-text": "Be careful, after the confirmation all selected assets will be removed and all related data will become unrecoverable.", + "make-public-asset-title": "Are you sure you want to make the asset '{{assetName}}' public?", + "make-public-asset-text": "After the confirmation the asset and all its data will be made public and accessible by others.", + "make-private-asset-title": "Are you sure you want to make the asset '{{assetName}}' private?", + "make-private-asset-text": "After the confirmation the asset and all its data will be made private and won't be accessible by others.", + "unassign-asset-title": "Are you sure you want to unassign the asset '{{assetName}}'?", + "unassign-asset-text": "After the confirmation the asset will be unassigned and won't be accessible by the customer.", + "unassign-asset": "Unassign asset", + "unassign-assets-title": "Are you sure you want to unassign { count, plural, 1 {1 asset} other {# assets} }?", + "unassign-assets-text": "After the confirmation all selected assets will be unassigned and won't be accessible by the customer.", + "copyId": "Copy asset Id", + "idCopiedMessage": "Asset Id has been copied to clipboard", + "select-asset": "Select asset", + "no-assets-matching": "No assets matching '{{entity}}' were found.", + "asset-required": "Asset is required", + "name-starts-with": "Asset name starts with" + }, + "attribute": { + "attributes": "속성", + "latest-telemetry": "최근 데이터", + "attributes-scope": "디바이스 속성 범위", + "scope-latest-telemetry": "최근 데이터", + "scope-client": "클라이언트 속성", + "scope-server": "서버 속성", + "scope-shared": "공유 속성", + "add": "속성 추가", + "key": "키", + "last-update-time": "마지막 수정된 시간", + "key-required": "속성 키를 입력하세요.", + "value": "Value", + "value-required": "속성 값을 입력하세요.", + "delete-attributes-title": "{ count, plural, 1 {속성} other {여러 속성들을} } 삭제하시겠습니까??", + "delete-attributes-text": "모든 선택된 속성들이 제거 될 것이므로 주의하십시오.", + "delete-attributes": "속성 삭제", + "enter-attribute-value": "속성 값 입력", + "show-on-widget": "위젯 보기", + "widget-mode": "위젯 모드", + "next-widget": "다음 위젯", + "prev-widget": "이전 위젯", + "add-to-dashboard": "대시보드에 추가", + "add-widget-to-dashboard": "대시보드에 위젯 추가", + "selected-attributes": "{ count, plural, 1 {속성 1개} other {속성 #개} } 선택됨", + "selected-telemetry": "{ count, plural, 1 {최근 데이터 1개} other {최근 데이터 #개} } 선택됨" + "no-attributes-text": "아무 속성도 찾을 수 없습니다", + "no-telemetry-text": "아무 텔레메트리도 찾을 수 없습니다." + }, + "audit-log": { + "audit": "감사", + "audit-logs": "감사 로그", + "timestamp": "타임스탬프", + "entity-type": "기체 종류", + "entity-name": "개체 이름", + "user": "사용자", + "type": "종류", + "status": "상태", + "details": "자세히", + "type-added": "추가됨", + "type-deleted": "삭제됨", + "type-updated": "수정됨", + "type-attributes-updated": "속성이 수정되었습니다", + "type-attributes-deleted": "속성이 삭제되었습니다", + "type-rpc-call": "RPC call", + "type-credentials-updated": "자격 증명이 갱신되었습니다", + "type-assigned-to-customer": "고객에게 지정", + "type-unassigned-from-customer": "지정된 고객 해제", + "type-activated": "활성", + "type-suspended": "일시 중지", + "type-credentials-read": "자격 증명 읽기", + "type-attributes-read": "속성 읽기", + "status-success": "성공", + "status-failure": "실패", + "audit-log-details": "감사 로그 세부 사항", + "no-audit-logs-prompt": "아무 로그도 없습니다.", + "action-data": "액션 데이터", + "failure-details": "실패 세부 사항", + "search": "감사 로그 검색", + "clear-search": "검색 초기화" + }, + "confirm-on-exit": { + "message": "변경 사항을 저장하지 않았습니다. 이 페이지를 나가시겠습니까?", + "html-message": "변경 사항을 저장하지 않았습니다.
    이 페이지를 나가시겠습니까?", + "title": "저장되지 않은 변경사항" + }, + "contact": { + "country": "국가", + "city": "시", + "state": "도", + "postal-code": "우편 번호", + "postal-code-invalid": "숫자만 입력하세요.", + "address": "주소", + "address2": "상세주소", + "phone": "전화번호", + "email": "Email", + "no-address": "주소 정보 없음" + }, + "common": { + "username": "사용자명", + "password": "비밀번호", + "enter-username": "사용자명을 입력하세요.", + "enter-password": "비밀번호를 입력하세요.", + "enter-search": "검색어 입력" + }, + "content-type": { + "json": "Json", + "text": "텍스트", + "binary": "바이너리 (Base64)" + }, + "customer": { + "customers": "커스터머", + "management": "커스터머 관리", + "dashboard": "커스터머 대시보드", + "dashboards": "커스터머 대시보드", + "devices": "커스터머 디바이스", + "add": "커스터머 추가", + "delete": "커스터머 삭제", + "manage-customer-users": "커스터머 사용자 관리", + "manage-customer-devices": "커스터머 디바이스 관리", + "manage-customer-dashboards": "커스터머 대시보드 관리", + "manage-public-devices": "공개된 디바이스 관리", + "manage-public-dashboards": "공개된 대시보드 관리", + "manage-customer-assets": "고객 자산 관리", + "manage-public-assets": "공개된 자산 관리", + "add-customer-text": "커스터머 추가", + "no-customers-text": "커스터머가 없습니다.", + "customer-details": "커스터머 상세정보", + "delete-customer-title": "'{{customerTitle}}' 커스터머를 삭제하시겠습니까?", + "delete-customer-text": "커스터머 및 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", + "delete-customers-title": "{ count, plural, 1 {커스터머 1개} other {커스터머 #개} }를 삭제하시겠습니까?", + "delete-customers-action-title": "{ count, plural, 1 {커스터머 1개} other {커스터머 #개} } 삭제", + "delete-customers-text": "선택된 커스터머는 삭제되고 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", + "manage-users": "사용자 관리", + "manage-devices": "디바이스 관리", + "manage-dashboards": "대시보드 관리", + "title": "타이틀", + "title-required": "타이틀을 입력하세요.", + "description": "설명", + "details": "자세히", + "events": "이벤트", + "copyId": "고객 ID 복사", + "idCopiedMessage": "고객 ID가 클립 보드에 복사되었습니다.", + "select-customer": "선택된 고객", + "no-customers-matching": "'{{entity}}'에 해당하는 고객을 찾을 수 없습니다.", + "customer-required": "고객을 입력하세요.", + "select-default-customer": "기본 고객 선택", + "default-customer": "기본 고객", + "default-customer-required": "테넌트 수준에서 대시보드를 디버그 하기 위해서는 기본 고객이 필요합니다." + }, + "datetime": { + "date-from": "시작 날짜", + "time-from": "시작 시간", + "date-to": "종료 날짜", + "time-to": "종료 시간" + }, + "dashboard": { + "dashboard": "대시보드", + "dashboards": "대시보드", + "management": "대시보드 관리", + "view-dashboards": "대시보드 보기", + "add": "대시보드 추가", + "assign-dashboard-to-customer": "대시보드 커스터머 선택", + "assign-dashboard-to-customer-text": "대시보드 커스터머를 선택하세요.", + "assign-to-customer-text": "대시보드 커스터머를 선택하세요.", + "assign-to-customer": "커스터머 선택", + "unassign-from-customer": "커스터머 해제", + "no-dashboards-text": "대시보드가 없습니다", + "no-widgets": "설정된 위젯 없음", + "add-widget": "위젯 추가", + "title": "타이틀", + "select-widget-title": "위젯 선택", + "select-widget-subtitle": "사용가능한 위젯 타입 목록", + "delete": "대시보드 삭제", + "title-required": "타이틀을 입력하세요.", + "description": "설명", + "details": "상세", + "dashboard-details": "대시보드 상세정보", + "add-dashboard-text": "대시보드 추가", + "assign-dashboards": "대시보드 지정", + "assign-new-dashboard": "새 대시보드 할당", + "assign-dashboards-text": "{ count, plural, 1 {대시보드 1개} other {대시보드 #개} }를 커스터머 할당", + "delete-dashboards": "대시보드 삭제", + "unassign-dashboards": "대시보드 할당 취소", + "unassign-dashboards-action-title": "{ count, plural, 1 {대시보드 1개} other {대시보드 #개} }를 커스터머 할당 취소", + "delete-dashboard-title": "'{{dashboardTitle}}' 대시보드를 삭제하시겠습니까?", + "delete-dashboard-text": "대시보드 및 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", + "delete-dashboards-title": "{ count, plural, 1 {대시보드 1개} other {대시보드 #개} }를 삭제하시겠습니까?", + "delete-dashboards-action-title": "{ count, plural, 1 {대시보드 1개} other {대시보드 #개} } 삭제", + "delete-dashboards-text": "선택된 대시보드가 삭제되고 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", + "unassign-dashboard-title": "'{{dashboardTitle}}' 대시보드 할당을 해제하시겠습니까?", + "unassign-dashboard-text": "대시보드가 할당 해제되고 커스터머는 액세스 할 수 없게됩니다.", + "unassign-dashboard": "대시보드 할달 취소", + "unassign-dashboards-title": "{ count, plural, 1 {대시보드 1개} other {대시보드 #개} }의 할당을 취소하시겠습니까?", + "unassign-dashboards-text": "선택된 대시보드가 할당 해제되고 커스터머는 액세스 할 수 없게됩니다.", + "select-dashboard": "대시보드 선택", + "no-dashboards-matching": "'{{entity}}'와 일치하는 대시보드가 없습니다.", + "dashboard-required": "대시보드를 입력하세요.", + "select-existing": "기존 대시보드 선택", + "create-new": "대시보드 생성", + "new-dashboard-title": "새로운 대시보드 타이틀", + "open-dashboard": "대시보드 열기", + "set-background": "대시보드 설정", + "background-color": "배경색", + "background-image": "배경 이미지", + "background-size-mode": "배경 사이즈 모드", + "no-image": "이미지 없음", + "drop-image": "이곳에 이미지를 끌어다놓거나 이곳을 클릭하여 파일을 선택하고 업로드하세요.", + "settings": "설정", + "columns-count": "열 개수", + "columns-count-required": "열 개수를 입력하세요.", + "min-columns-count-message": "열 개수를 최소 10 이상 입력하세요.", + "max-columns-count-message": "열 개수를 최대 100 이하로 입력하세요.", + "widgets-margins": "위젯 사이 여백 크기", + "horizontal-margin": "세로 여백", + "horizontal-margin-required": "세로 여백 값을 입력하세요.", + "min-horizontal-margin-message": "세로 여백 값을 최소 0 이상 입력하세요.", + "max-horizontal-margin-message": "세로 여백 값을 최대 50 이하로 입력하세요.", + "vertical-margin": "가로 여백", + "vertical-margin-required": "가로 여백 값을 입력하세요.", + "min-vertical-margin-message": "가로 여백 값을 최소 0 이상 입력하세요.", + "max-vertical-margin-message": "가로 여백 값을 최대 50 이하로 입력하세요.", + "display-title": "대시보드 타이틀 표시", + "title-color": "타이틀 색상", + "import": "대시보드 가져오기", + "export": "대시보드 내보내기", + "export-failed-error": "대시보드 내보내기를 할 수 없습니다.: {error}", + "create-new-dashboard": "대시보드 생성", + "dashboard-file": "대시보드 파일", + "invalid-dashboard-file-error": "대시보드 가져오기를 할 수 없습니다.: 대시보드 데이터 구조가 잘못되었습니다.", + "dashboard-import-missing-aliases-title": "대시보드 앨리어스를 위해 누락 된 디바이스 선택", + "create-new-widget": "새로운 위젯 생성", + "import-widget": "위젯 가져오기", + "widget-file": "위젯 파일", + "invalid-widget-file-error": "위젯 가져오기를 할 수 없습니다: 위젯 데이터 구조가 잘못되었습니다.", + "widget-import-missing-aliases-title": "위젯에서 사용하는 누락 된 디바이스 선택", + "open-toolbar": "대시보드 툴바 열기", + "close-toolbar": "툴바 닫기", + "configuration-error": "구성 오류", + "alias-resolution-error-title": "대시보드 앨리어스 구성 오류", + "invalid-aliases-config": "일부 앨리어스 필터와 일치하는 디바이스를 찾을 수 없습니다.
    이 문제를 해결하려면 관리자에게 문의하십시오.", + "select-devices": "디바이스 선택", + "assignedToCustomer": "커스터머에 할당됨" + }, + "datakey": { + "settings": "설정", + "advanced": "고급", + "label": "Label", + "color": "색상", + "data-generation-func": "데이터 생성 기능", + "use-data-post-processing-func": "데이터 후처리 기능 사용", + "configuration": "데이터 key 구성", + "timeseries": "Timeseries", + "attributes": "Attributes", + "timeseries-required": "디바이스 timeseries 를 입력하세요.", + "timeseries-or-attributes-required": "디바이스 timeseries/attributes 를 입력하세요.", + "maximum-timeseries-or-attributes": "Maximum { count, plural, 1 {1 timeseries/attribute is allowed.} other {# timeseries/attributes are allowed} }", + "alarm-fields-required": "Alarm fields are required.", + "function-types": "함수 유형", + "function-types-required": "함수 유형을 입력하세요.", + "maximum-function-types": "Maximum { count, plural, 1 {1 function type is allowed.} other {# function types are allowed} }" + }, + "datasource": { + "type": "데이터소스 유형", + "name": "Name", + "add-datasource-prompt": "데이터소스를 추가하세요." + }, + "details": { + "edit-mode": "편집 모드", + "toggle-edit-mode": "편집 모드 전환" + }, + "device": { + "device": "디바이스", + "device-required": "디바이스를 입력하세요.", + "devices": "디바이스", + "management": "디바이스 관리", + "view-devices": "디바이스 보기", + "device-alias": "디바이스 앨리어스", + "aliases": "디바이스 앨리어스", + "no-alias-matching": "'{{alias}}' 를 찾을 수 없습니다.", + "no-aliases-found": "앨리어스가 없습니다.", + "no-key-matching": "'{{key}}' 를 찾을 수 없습니다.", + "no-keys-found": "Key가 없습니다.", + "create-new-alias": "새로 만들기!", + "create-new-key": "새로 만들기!", + "duplicate-alias-error": "중복된 '{{alias}}' 앨리어스가 있습니다.
    디바이스 앨리어스는 대시보드 내에서 고유해야 합니다.", + "configure-alias": "'{{alias}}' 앨리어스 구성", + "no-devices-matching": "'{{entity}}'와 일치하는 디바이스를 찾을 수 없습니다.", + "alias": "앨리어스", + "alias-required": "디바이스 앨리어스를 입력하세요.", + "remove-alias": "디바이스 앨리어스 삭제", + "add-alias": "디바이스 앨리어스 추가", + "name-starts-with": "시작되는 이름", + "device-list": "디바이스 리스트", + "use-device-name-filter": "필터 사용", + "device-list-empty": "선택된 디바이스가 없습니다.", + "device-name-filter-required": "디바이스 필터 이름을 입력하세요.", + "device-name-filter-no-device-matched": "'{{device}}' 로 시작되는 디바이스를 찾을 수 없습니다.", + "add": "디바이스 추가", + "assign-to-customer": "커스터머에게 할당", + "assign-device-to-customer": "디바이스를 커스터머에게 할당", + "assign-device-to-customer-text": "고객에게 할당할 디바이스를 선택하십시오.", + "no-devices-text": "디바이스 없음", + "assign-to-customer-text": "디바이스를 할당할 커스터머를 선택하세요.", + "device-details": "디바이스 상세정보", + "add-device-text": "디바이스 추가", + "credentials": "자격 증명", + "manage-credentials": "자격 증명 관리", + "delete": "디바이스 삭제", + "assign-devices": "디바이스 할당", + "assign-devices-text": "{ count, plural, 1 {디바이스 1개} other {디바이스 #개} }를 커서터머에 할당", + "delete-devices": "디바이스 삭제", + "unassign-from-customer": "커스터머 할당 해제", + "unassign-devices": "디바이스 할당 취소", + "unassign-devices-action-title": "{ count, plural, 1 {디바이스 1개} other {디바이스 #개} }를 커스터머에게서 할당 해제", + "assign-new-device": "새로운 디바이스 할당", + "view-credentials": "크리덴셜 보기", + "delete-device-title": "'{{deviceName}}' 디바이스를 삭제하시겠습니까?", + "delete-device-text": "디바이스 및 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", + "delete-devices-title": "{ count, plural, 1 {디바이스 1개} other {디바이스 #개} }를 삭제하시겠습니까?", + "delete-devices-action-title": "{ count, plural, 1 {디바이스 1개} other {디바이스 #개} } 삭제", + "delete-devices-text": "선택된 디바이스가 삭제되고 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", + "unassign-device-title": "'{{deviceName}}' 디바이스 할당을 해제하시겠습니까?", + "unassign-device-text": "디바이스가 할당 해제되고 커스터머는 액세스 할 수 없게됩니다.", + "unassign-device": "디바이스 할당 취소", + "unassign-devices-title": "{ count, plural, 1 {디바이스 1개} other {디바이스 #개} }의 할당을 해제하시겠습니까??", + "unassign-devices-text": "선택된 디바이스가 할당 해제되고 커스터머는 액세스 할 수 없게됩니다.", + "device-credentials": "디바이스 크리덴셜", + "credentials-type": "크리덴셜 타입", + "access-token": "억세스 토큰", + "access-token-required": "액세스 토큰을 입력하세요.", + "access-token-invalid": "액세스 토큰 길이는 1 - 20 자 여야합니다.", + "rsa-key": "RSA public key", + "rsa-key-required": "RSA public key 를 입력하세요.", + "secret": "시크릿", + "secret-required": "시크릿을 입력하세요.", + "name": "이름", + "name-required": "이름을 입력하세요.", + "description": "설명", + "events": "이벤트", + "details": "상세", + "copyId": "디바이스 아이디 복사", + "copyAccessToken": "억세스 토큰 복사", + "idCopiedMessage": "디바이스 아이디가 클립보드에 복사되었습니다.", + "accessTokenCopiedMessage": "디바이스 억세스 토큰이 클립보드에 복사되었습니다.", + "assignedToCustomer": "커스터머에 할당됨", + "unable-delete-device-alias-title": "디바이스 앨리어스를 삭제할 수 없습니다.", + "unable-delete-device-alias-text": "'{{deviceAlias}}' 디바이스 앨리어스를 삭제할 수 없습니다. 다음 위젯에서 사용하고 있습니다.
    {{widgetsList}}", + "is-gateway": "게이트웨이 여부" + }, + "dialog": { + "close": "다이얼로그 닫기" + }, + "error": { + "unable-to-connect": "서버에 연결할 수 없습니다! 인터넷 연결을 확인하십시오.", + "unhandled-error-code": "처리되지 않은 오류 코드: {{errorCode}}", + "unknown-error": "알 수 없는 오류" + }, + "entity": { + "entity": "개체", + "entities": "개체", + "aliases": "Entity aliases", + "entity-alias": "Entity alias", + "unable-delete-entity-alias-title": "Unable to delete entity alias", + "unable-delete-entity-alias-text": "Entity alias '{{entityAlias}}' can't be deleted as it used by the following widget(s):
    {{widgetsList}}", + "duplicate-alias-error": "Duplicate alias found '{{alias}}'.
    Entity aliases must be unique whithin the dashboard.", + "missing-entity-filter-error": "Filter is missing for alias '{{alias}}'.", + "configure-alias": "Configure '{{alias}}' alias", + "alias": "Alias", + "alias-required": "Entity alias is required.", + "remove-alias": "Remove entity alias", + "add-alias": "Add entity alias", + "entity-list": "개체 목록", + "entity-type": "개체 종류", + "entity-types": "개체 종류", + "entity-type-list": "개체 종류 목록", + "any-entity": "모든 개체", + "enter-entity-type": "개체 종류 입력", + "no-entities-matching": "No entities matching '{{entity}}' were found.", + "no-entity-types-matching": "No entity types matching '{{entityType}}' were found.", + "name-starts-with": "다음으로 시작하는 이름", + "use-entity-name-filter": "Use filter", + "entity-list-empty": "아무 개체도 선택되지 않았습니다.", + "entity-type-list-empty": "개체 종류가 선택되지 않았습니다.", + "entity-name-filter-required": "Entity name filter is required.", + "entity-name-filter-no-entity-matched": "No entities starting with '{{entity}}' were found.", + "all-subtypes": "모두", + "select-entities": "선택된 개체", + "no-aliases-found": "No aliases found.", + "no-alias-matching": "'{{alias}}' not found.", + "create-new-alias": "생성 완료!", + "key": "키", + "key-name": "키 이름", + "no-keys-found": "아무 키도 찾을 수 없습니다..", + "no-key-matching": "'{{key}}'를 찾을 수 없습니다.", + "create-new-key": "생성 완료!", + "type": "종류", + "type-required": "개체의 종류를 입력하세요.", + "type-device": "장치", + "type-devices": "장치", + "list-of-devices": "{ count, plural, 1 {One device} other {List of # devices} }", + "device-name-starts-with": "Devices whose names start with '{{prefix}}'", + "type-asset": "자산", + "type-assets": "자산", + "list-of-assets": "{ count, plural, 1 {One asset} other {List of # assets} }", + "asset-name-starts-with": "Assets whose names start with '{{prefix}}'", + "type-rule": "규칙", + "type-rules": "규칙", + "list-of-rules": "{ count, plural, 1 {One rule} other {List of # rules} }", + "rule-name-starts-with": "Rules whose names start with '{{prefix}}'", + "type-plugin": "플러그인", + "type-plugins": "플러그인", + "list-of-plugins": "{ count, plural, 1 {One plugin} other {List of # plugins} }", + "plugin-name-starts-with": "Plugins whose names start with '{{prefix}}'", + "type-tenant": "테넌트", + "type-tenants": "테넌트", + "list-of-tenants": "{ count, plural, 1 {One tenant} other {List of # tenants} }", + "tenant-name-starts-with": "Tenants whose names start with '{{prefix}}'", + "type-customer": "고객", + "type-customers": "고객", + "list-of-customers": "{ count, plural, 1 {One customer} other {List of # customers} }", + "customer-name-starts-with": "Customers whose names start with '{{prefix}}'", + "type-user": "사용자", + "type-users": "사용자", + "list-of-users": "{ count, plural, 1 {One user} other {List of # users} }", + "user-name-starts-with": "Users whose names start with '{{prefix}}'", + "type-dashboard": "대시보드", + "type-dashboards": "대시보드", + "list-of-dashboards": "{ count, plural, 1 {One dashboard} other {List of # dashboards} }", + "dashboard-name-starts-with": "Dashboards whose names start with '{{prefix}}'", + "type-alarm": "알람", + "type-alarms": "알람", + "list-of-alarms": "{ count, plural, 1 {One alarms} other {List of # alarms} }", + "alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'", + "type-rulechain": "규칙 사슬", + "type-rulechains": "규칙 사슬", + "list-of-rulechains": "{ count, plural, 1 {One rule chain} other {List of # rule chains} }", + "rulechain-name-starts-with": "Rule chains whose names start with '{{prefix}}'", + "type-current-customer": "Current Customer", + "search": "Search entities", + "selected-entities": "{ count, plural, 1 {1 entity} other {# entities} } selected", + "entity-name": "Entity name", + "details": "Entity details", + "no-entities-prompt": "No entities found", + "no-data": "No data to display" + }, + "event": { + "event-type": "이벤트 타입", + "type-error": "에러", + "type-lc-event": "주기적 이벤트", + "type-stats": "통계", + "type-debug-rule-node": "디버그", + "type-debug-rule-chain": "디버그", + "no-events-prompt": "이벤트 없음", + "error": "에러", + "alarm": "알람", + "event-time": "이벤트 발생 시간", + "server": "서버", + "body": "Body", + "method": "방법", + "type": "종류", + "entity": "개체", + "message-id": "메시지 ID", + "message-type": "메시지 종류", + "data-type": "데이터 종류", + "relation-type": "관계 종류", + "metadata": "메타데이터", + "data": "데이터", + "event": "이벤트", + "status": "상태", + "success": "성공", + "failed": "실패", + "messages-processed": "처리된 메시지", + "errors-occurred": "오류가 발생했습니다" + }, + "extension": { + "extensions": "확장", + "selected-extensions": "{ count, plural, 1 {1 extension} other {# extensions} } selected", + "type": "종류", + "key": "키", + "value": "값", + "id": "Id", + "extension-id": "Extension id", + "extension-type": "Extension type", + "transformer-json": "JSON *", + "unique-id-required": "Current extension id already exists.", + "delete": "Delete extension", + "add": "Add extension", + "edit": "Edit extension", + "delete-extension-title": "Are you sure you want to delete the extension '{{extensionId}}'?", + "delete-extension-text": "Be careful, after the confirmation the extension and all related data will become unrecoverable.", + "delete-extensions-title": "Are you sure you want to delete { count, plural, 1 {1 extension} other {# extensions} }?", + "delete-extensions-text": "Be careful, after the confirmation all selected extensions will be removed.", + "converters": "Converters", + "converter-id": "Converter id", + "configuration": "Configuration", + "converter-configurations": "Converter configurations", + "token": "Security token", + "add-converter": "Add converter", + "add-config": "Add converter configuration", + "device-name-expression": "Device name expression", + "device-type-expression": "Device type expression", + "custom": "Custom", + "to-double": "To Double", + "transformer": "Transformer", + "json-required": "Transformer json is required.", + "json-parse": "Unable to parse transformer json.", + "attributes": "Attributes", + "add-attribute": "Add attribute", + "add-map": "Add mapping element", + "timeseries": "Timeseries", + "add-timeseries": "Add timeseries", + "field-required": "Field is required", + "brokers": "Brokers", + "add-broker": "Add broker", + "host": "Host", + "port": "Port", + "port-range": "Port should be in a range from 1 to 65535.", + "ssl": "Ssl", + "credentials": "Credentials", + "username": "Username", + "password": "Password", + "retry-interval": "Retry interval in milliseconds", + "anonymous": "Anonymous", + "basic": "Basic", + "pem": "PEM", + "ca-cert": "CA certificate file *", + "private-key": "Private key file *", + "cert": "Certificate file *", + "no-file": "No file selected.", + "drop-file": "Drop a file or click to select a file to upload.", + "mapping": "Mapping", + "topic-filter": "Topic filter", + "converter-type": "Converter type", + "converter-json": "Json", + "json-name-expression": "Device name json expression", + "topic-name-expression": "Device name topic expression", + "json-type-expression": "Device type json expression", + "topic-type-expression": "Device type topic expression", + "attribute-key-expression": "Attribute key expression", + "attr-json-key-expression": "Attribute key json expression", + "attr-topic-key-expression": "Attribute key topic expression", + "request-id-expression": "Request id expression", + "request-id-json-expression": "Request id json expression", + "request-id-topic-expression": "Request id topic expression", + "response-topic-expression": "Response topic expression", + "value-expression": "Value expression", + "topic": "Topic", + "timeout": "Timeout in milliseconds", + "converter-json-required": "Converter json is required.", + "converter-json-parse": "Unable to parse converter json.", + "filter-expression": "Filter expression", + "connect-requests": "Connect requests", + "add-connect-request": "Add connect request", + "disconnect-requests": "Disconnect requests", + "add-disconnect-request": "Add disconnect request", + "attribute-requests": "Attribute requests", + "add-attribute-request": "Add attribute request", + "attribute-updates": "Attribute updates", + "add-attribute-update": "Add attribute update", + "server-side-rpc": "Server side RPC", + "add-server-side-rpc-request": "Add server-side RPC request", + "device-name-filter": "Device name filter", + "attribute-filter": "Attribute filter", + "method-filter": "Method filter", + "request-topic-expression": "Request topic expression", + "response-timeout": "Response timeout in milliseconds", + "topic-expression": "Topic expression", + "client-scope": "Client scope", + "add-device": "Add device", + "opc-server": "Servers", + "opc-add-server": "Add server", + "opc-add-server-prompt": "Please add server", + "opc-application-name": "Application name", + "opc-application-uri": "Application uri", + "opc-scan-period-in-seconds": "Scan period in seconds", + "opc-security": "Security", + "opc-identity": "Identity", + "opc-keystore": "Keystore", + "opc-type": "Type", + "opc-keystore-type": "Type", + "opc-keystore-location": "Location *", + "opc-keystore-password": "Password", + "opc-keystore-alias": "Alias", + "opc-keystore-key-password": "Key password", + "opc-device-node-pattern": "Device node pattern", + "opc-device-name-pattern": "Device name pattern", + "modbus-server": "Servers/slaves", + "modbus-add-server": "Add server/slave", + "modbus-add-server-prompt": "Please add server/slave", + "modbus-transport": "Transport", + "modbus-port-name": "Serial port name", + "modbus-encoding": "Encoding", + "modbus-parity": "Parity", + "modbus-baudrate": "Baud rate", + "modbus-databits": "Data bits", + "modbus-stopbits": "Stop bits", + "modbus-databits-range": "Data bits should be in a range from 7 to 8.", + "modbus-stopbits-range": "Stop bits should be in a range from 1 to 2.", + "modbus-unit-id": "Unit ID", + "modbus-unit-id-range": "Unit ID should be in a range from 1 to 247.", + "modbus-device-name": "Device name", + "modbus-poll-period": "Poll period (ms)", + "modbus-attributes-poll-period": "Attributes poll period (ms)", + "modbus-timeseries-poll-period": "Timeseries poll period (ms)", + "modbus-poll-period-range": "Poll period should be positive value.", + "modbus-tag": "Tag", + "modbus-function": "Function", + "modbus-register-address": "Register address", + "modbus-register-address-range": "Register address should be in a range from 0 to 65535.", + "modbus-register-bit-index": "Bit index", + "modbus-register-bit-index-range": "Bit index should be in a range from 0 to 15.", + "modbus-register-count": "Register count", + "modbus-register-count-range": "Register count should be a positive value.", + "modbus-byte-order": "Byte order", + "sync": { + "status": "Status", + "sync": "Sync", + "not-sync": "Not sync", + "last-sync-time": "Last sync time", + "not-available": "Not available" + }, + "export-extensions-configuration": "Export extensions configuration", + "import-extensions-configuration": "Import extensions configuration", + "import-extensions": "Import extensions", + "import-extension": "Import extension", + "export-extension": "Export extension", + "file": "Extensions file", + "invalid-file-error": "Invalid extension file" + }, + "fullscreen": { + "expand": "전체화면으로 확장", + "exit": "전체화면 종료", + "toggle": "전체화면 모드 전환", + "fullscreen": "전체화면" + }, + "function": { + "function": "기능" + }, + "grid": { + "delete-item-title": "이 항목을 삭제 하시겠습니까?", + "delete-item-text": "항목과 모든 관련 데이터를 복구 할 수 없으므로 주의하십시오.", + "delete-items-title": "{ count, plural, 1 {아이템 1개} other {아이템 #개} }를 삭제하시겠습니까?", + "delete-items-action-title": "{ count, plural, 1 {아이템 1개} other {아이템 #개} } 삭제", + "delete-items-text": "선택한 모든 아이템이 제거되고 관련된 모든 데이터는 복구 할 수 없으므로 주의하십시오.", + "add-item-text": "새로운 아이템 추가", + "no-items-text": "아이템이 없습니다.", + "item-details": "아이템 상세", + "delete-item": "아이템 삭제", + "delete-items": "아이템 삭제", + "scroll-to-top": "스크롤 맨 위로" + }, + "help": { + "goto-help-page": "도움" + }, + "home": { + "home": "홈", + "profile": "프로파일", + "logout": "로그아웃", + "menu": "메뉴", + "avatar": "Avatar", + "open-user-menu": "사용자 메뉴 열기" + }, + "import": { + "no-file": "선택된 파일이 없습니다.", + "drop-file": "JSON 파일을 끌어다 놓거나 클릭하여 업로드 할 파일을 선택하십시오." + }, + "item": { + "selected": "선택됨" + }, + "js-func": { + "no-return-error": "함수는 값을 반환해야 합니다!", + "return-type-mismatch": "함수는 '{{type}}' 유형의 값을 반환해야 합니다!", + "tidy": "Tidy" + }, + "key-val": { + "key": "Key", + "value": "Value", + "remove-entry": "Remove entry", + "add-entry": "Add entry", + "no-data": "No entries" + }, + "layout": { + "layout": "Layout", + "manage": "Manage layouts", + "settings": "Layout settings", + "color": "Color", + "main": "Main", + "right": "Right", + "select": "Select target layout" + }, + "legend": { + "position": "범례 위치", + "show-max": "최대값 표시", + "show-min": "최소값 표시", + "show-avg": "평균값 표시", + "show-total": "총합 표시", + "settings": "범례 설정", + "min": "최소", + "max": "최대", + "avg": "평균", + "total": "합계" + }, + "login": { + "login": "로그인", + "request-password-reset": "비밀번호 재설정", + "reset-password": "비밀번호 재설정", + "create-password": "비밀번호 생성", + "passwords-mismatch-error": "입력된 비밀번호는 같아야 합니다!", + "password-again": "비밀번호 확인", + "sign-in": "로그인", + "username": "사용자명 (이메일)", + "remember-me": "아이디 저장", + "forgot-password": "비밀번호찾기", + "password-reset": "비밀번호 재설정", + "new-password": "새 비밀번호", + "new-password-again": "새 비밀번호 확인", + "password-link-sent-message": "비밀번호 재설정 링크가 성공적으로 전송되었습니다!", + "email": "이메일", + "login-with": "{{name}}으로 로그인", + "or": "또는" + }, + "position": { + "top": "상단", + "bottom": "하단", + "left": "왼쪽", + "right": "오른쪽" + }, + "profile": { + "profile": "프로파일", + "change-password": "비밀번호 변경", + "current-password": "현재 비밀번호" + }, + "relation": { + "relations": "Relations", + "direction": "Direction", + "search-direction": { + "FROM": "From", + "TO": "To" + }, + "direction-type": { + "FROM": "from", + "TO": "to" + }, + "from-relations": "Outbound relations", + "to-relations": "Inbound relations", + "selected-relations": "{ count, plural, 1 {1 relation} other {# relations} } selected", + "type": "Type", + "to-entity-type": "To entity type", + "to-entity-name": "To entity name", + "from-entity-type": "From entity type", + "from-entity-name": "From entity name", + "to-entity": "To entity", + "from-entity": "From entity", + "delete": "Delete relation", + "relation-type": "Relation type", + "relation-type-required": "Relation type is required.", + "any-relation-type": "Any type", + "add": "Add relation", + "edit": "Edit relation", + "delete-to-relation-title": "Are you sure you want to delete relation to the entity '{{entityName}}'?", + "delete-to-relation-text": "Be careful, after the confirmation the entity '{{entityName}}' will be unrelated from the current entity.", + "delete-to-relations-title": "Are you sure you want to delete { count, plural, 1 {1 relation} other {# relations} }?", + "delete-to-relations-text": "Be careful, after the confirmation all selected relations will be removed and corresponding entities will be unrelated from the current entity.", + "delete-from-relation-title": "Are you sure you want to delete relation from the entity '{{entityName}}'?", + "delete-from-relation-text": "Be careful, after the confirmation current entity will be unrelated from the entity '{{entityName}}'.", + "delete-from-relations-title": "Are you sure you want to delete { count, plural, 1 {1 relation} other {# relations} }?", + "delete-from-relations-text": "Be careful, after the confirmation all selected relations will be removed and current entity will be unrelated from the corresponding entities.", + "remove-relation-filter": "Remove relation filter", + "add-relation-filter": "Add relation filter", + "any-relation": "Any relation", + "relation-filters": "Relation filters", + "additional-info": "Additional info (JSON)", + "invalid-additional-info": "Unable to parse additional info json." + }, + "rulechain": { + "rulechain": "규칙 사슬", + "rulechains": "규칙 사슬", + "root": "Root", + "delete": "Delete rule chain", + "name": "이름", + "name-required": "이름을 입력하세요.", + "description": "설명", + "add": "규칙 사슬 추가", + "set-root": "Make rule chain root", + "set-root-rulechain-title": "Are you sure you want to make the rule chain '{{ruleChainName}}' root?", + "set-root-rulechain-text": "After the confirmation the rule chain will become root and will handle all incoming transport messages.", + "delete-rulechain-title": "Are you sure you want to delete the rule chain '{{ruleChainName}}'?", + "delete-rulechain-text": "Be careful, after the confirmation the rule chain and all related data will become unrecoverable.", + "delete-rulechains-title": "Are you sure you want to delete { count, plural, 1 {1 rule chain} other {# rule chains} }?", + "delete-rulechains-action-title": "Delete { count, plural, 1 {1 rule chain} other {# rule chains} }", + "delete-rulechains-text": "Be careful, after the confirmation all selected rule chains will be removed and all related data will become unrecoverable.", + "add-rulechain-text": "새로운 규칙 사슬 추가", + "no-rulechains-text": "아무 규칙 사슬도 없습니다.", + "rulechain-details": "규칙 사슬 상세 정보", + "details": "자세히", + "events": "이벤트", + "system": "시스템", + "import": "규칙 사슬 불러오기", + "export": "규칙 사슬 내보내기", + "export-failed-error": "Unable to export rule chain: {{error}}", + "create-new-rulechain": "Create new rule chain", + "rulechain-file": "Rule chain file", + "invalid-rulechain-file-error": "Unable to import rule chain: Invalid rule chain data structure.", + "copyId": "Copy rule chain Id", + "idCopiedMessage": "Rule chain Id has been copied to clipboard", + "select-rulechain": "Select rule chain", + "no-rulechains-matching": "No rule chains matching '{{entity}}' were found.", + "rulechain-required": "Rule chain is required", + "management": "Rules management", + "debug-mode": "Debug mode" + }, + "rulenode": { + "details": "자세히", + "events": "이벤트", + "search": "노드 검색", + "open-node-library": "노드 라이브러리 열기", + "add": "규칙 노드 추가", + "name": "이름", + "name-required": "이름을 입력하세요.", + "type": "종류", + "description": "설명", + "delete": "규칙 노드 삭제", + "select-all-objects": "모든 노드와 연결을 선택", + "deselect-all-objects": "모든 노드와 연결을 선택 해제", + "delete-selected-objects": "선택된 노드와 연결을 삭제", + "delete-selected": "선택 삭제", + "select-all": "모두 선택", + "copy-selected": "선택 복사", + "deselect-all": "선택 해제", + "rulenode-details": "규칙 노드 상세 정보", + "debug-mode": "디버그 모드", + "configuration": "설정", + "link": "링크", + "link-details": "규칙 노드 링크 상세 정보", + "add-link": "링크 추가", + "link-label": "링크 라벨", + "link-label-required": "링크 라벨을 입력하세요.", + "custom-link-label": "링크 라벨 사용자 정의", + "custom-link-label-required": "링크 라벨 사용자 정의를 입력하세요.", + "type-filter": "필터", + "type-filter-details": "Filter incoming messages with configured conditions", + "type-enrichment": "Enrichment", + "type-enrichment-details": "Add additional information into Message Metadata", + "type-transformation": "Transformation", + "type-transformation-details": "Change Message payload and Metadata", + "type-action": "", + "type-action-details": "Perform special action", + "type-external": "외부", + "type-external-details": "Interacts with external system", + "type-rule-chain": "Rule Chain", + "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain", + "type-input": "입력", + "type-input-details": "Logical input of Rule Chain, forwards incoming messages to next related Rule Node", + "directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.", + "ui-resources-load-error": "Failed to load configuration ui resources.", + "invalid-target-rulechain": "Unable to resolve target rule chain!", + "test-script-function": "Test script function", + "message": "메시지", + "message-type": "메시지 종류", + "message-type-required": "메시지 종류를 입력하세요.", + "metadata": "메타데이터", + "metadata-required": "메타데이터 엔트리를 입력하세요.", + "output": "출력", + "test": "테스트", + "help": "도움말" + }, + "tenant": { + "tenants": "테넌트", + "management": "테넌트 관리", + "add": "테넌트 추가", + "admins": "관리자", + "manage-tenant-admins": "테넌트 관리자 관리", + "delete": "테넌트 삭제", + "add-tenant-text": "테넌트 추가", + "no-tenants-text": "테넌트가 없습니다.", + "tenant-details": "테넌트 상세 정보", + "delete-tenant-title": "'{{tenantTitle}}' 테넌트를 삭제하시겠습니까?", + "delete-tenant-text": "테넌트와 관련된 모든 정보를 복구할 수 없으므로 주의하십시오.", + "delete-tenants-title": "{ count, plural, 1 {테넌트 1개} other {테넌트 #개} }를 삭제하시겠습니까?", + "delete-tenants-action-title": "{ count, plural, 1 {테넌트 1개} other {테넌트 #개} } 삭제", + "delete-tenants-text": "선택된 테넌트가 삭제되고 관련된 모든 정보를 복구할 수 없으므로 주의하십시오.", + "title": "타이틀", + "title-required": "타이틀을 입력하세요.", + "description": "설명", + "details": "자세히", + "events": "이벤트", + "copyId": "테넌트 ID 복사", + "idCopiedMessage": "테넌트 ID를 클립보드로 복사", + "select-tenant": "테넌트 선택", + "no-tenants-matching": "No tenants matching '{{entity}}' were found.", + "tenant-required": "테넌트가 필요합니다." + }, + "timeinterval": { + "seconds-interval": "{ seconds, plural, 1 {1 second} other {# seconds} }", + "minutes-interval": "{ minutes, plural, 1 {1 minute} other {# minutes} }", + "hours-interval": "{ hours, plural, 1 {1 hour} other {# hours} }", + "days-interval": "{ days, plural, 1 {1 day} other {# days} }", + "days": "일", + "hours": "시간", + "minutes": "분", + "seconds": "초", + "advanced": "고급" + }, + "timewindow": { + "days": "{ days, plural, 1 { day } other {# days } }", + "hours": "{ hours, plural, 0 { hour } 1 {1 hour } other {# hours } }", + "minutes": "{ minutes, plural, 0 { minute } 1 {1 minute } other {# minutes } }", + "seconds": "{ seconds, plural, 0 { second } 1 {1 second } other {# seconds } }", + "realtime": "실시간", + "history": "기록", + "last-prefix": "과거", + "period": "{{ startTime }}부터 {{ endTime }}까지", + "edit": "타임윈도우 편집", + "date-range": "날짜 범위", + "last": "과거", + "time-period": "기간" + }, + "user": { + "users": "사용자", + "customer-users": "커스터머 사용자", + "tenant-admins": "테넌트 관리자", + "sys-admin": "시스템 관리자", + "tenant-admin": "테넌트 관리자", + "customer": "커스터머", + "anonymous": "Anonymous", + "add": "사용자 추가", + "delete": "사용자 삭제", + "add-user-text": "새로운 사용자 추가", + "no-users-text": "사용자가 없습니다.", + "user-details": "사용자 상세 정보", + "delete-user-title": "'{{userEmail}}' 사용자를 삭제하시겠습니까?", + "delete-user-text": "사용자와 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", + "delete-users-title": "{ count, plural, 1 {사용자 1명} other {사용자 #명} }을 삭제하시겠니까?", + "delete-users-action-title": "{ count, plural, 1 {사용자 1명} other {사용자 #명} } 삭제", + "delete-users-text": "선택된 사용자가 삭제된고 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", + "activation-email-sent-message": "활성화 이메일을 보냈습니다!", + "resend-activation": "활성화 재전송", + "email": "Email", + "email-required": "Email을 입력하세요.", + "first-name": "이름", + "last-name": "성", + "description": "설명", + "default-dashboard": "기본 대시보드", + "always-fullscreen": "항상 전체화면", + "select-user": "Select user", + "no-users-matching": "No users matching '{{entity}}' were found.", + "user-required": "User is required", + "activation-method": "Activation method", + "display-activation-link": "Display activation link", + "send-activation-mail": "Send activation mail", + "activation-link": "User activation link", + "activation-link-text": "In order to activate user use the following activation link :", + "copy-activation-link": "Copy activation link", + "activation-link-copied-message": "User activation link has been copied to clipboard", + "details": "Details" + }, + "value": { + "type": "Value type", + "string": "String", + "string-value": "String value", + "integer": "Integer", + "integer-value": "Integer value", + "invalid-integer-value": "Invalid integer value", + "double": "Double", + "double-value": "Double value", + "boolean": "Boolean", + "boolean-value": "Boolean value", + "false": "False", + "true": "True" + }, + "widget": { + "widget-library": "위젯 저장소", + "widget-bundle": "위젯 번들", + "select-widgets-bundle": "위젯 번들 선택", + "management": "위젯 관리", + "editor": "위젯 편집기", + "widget-type-not-found": "위젯 구성을 로드하는 중 문제가 발생했습니다.
    연결된 위젯 타입이 삭제되었습니다.", + "widget-type-load-error": "다음과 같은 오류로 인해 위젯이로드되지 않았습니다:", + "remove": "위젯 삭제", + "edit": "위젯 수정", + "remove-widget-title": "'{{widgetTitle}}' 위젯을 삭제하시겠습니까?", + "remove-widget-text": "위젯과 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", + "timeseries": "Time series", + "latest-values": "Latest values", + "rpc": "Control 위젯", + "static": "Static 위젯", + "select-widget-type": "위젯 타입 선택", + "missing-widget-title-error": "위젯 타이틀을 입력하세요!", + "widget-saved": "위젯이 저장되었습니다.", + "unable-to-save-widget-error": "위젯을 저장할 수 없습니다! 위젯에 오류가 있습니다!", + "save": "위젯 저장", + "saveAs": "다른 이름으로 위젯 저장", + "save-widget-type-as": "다른 이름으로 위젯 타입 저장", + "save-widget-type-as-text": "새로운 위젯 이름과 위젯 번들을 선택하세요.", + "toggle-fullscreen": "전체화면 전환", + "run": "위젯 실행", + "title": "위젯 타이틀", + "title-required": "위젯 타이틀을 입력하세요.", + "type": "위젯 타입", + "resources": "리소스", + "resource-url": "JavaScript/CSS URI", + "remove-resource": "리소스 삭제", + "add-resource": "리소스 추가", + "html": "HTML", + "tidy": "Tidy", + "css": "CSS", + "settings-schema": "스키마 설정", + "datakey-settings-schema": "데이터 키 설정 스키마", + "javascript": "Javascript", + "remove-widget-type-title": "'{{widgetName}}' 위젯 타입을 삭제하시겠습니까?", + "remove-widget-type-text": "위젯 타입과 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", + "remove-widget-type": "위젯 타입 삭제", + "add-widget-type": "새로운 위젯 타입 추가", + "widget-type-load-failed-error": "위젯 타입을 로드하지 못했습니다!", + "widget-template-load-failed-error": "위젯 템플릿을 로드하지 못했습니다!", + "add": "위젯 추가", + "undo": "위젯 변경사항 취소", + "export": "위젯 내보내기" + }, + "widget-action": { + "header-button": "Widget header button", + "open-dashboard-state": "Navigate to new dashboard state", + "update-dashboard-state": "Update current dashboard state", + "open-dashboard": "Navigate to other dashboard", + "custom": "Custom action", + "target-dashboard-state": "대상 대시보드 상태", + "target-dashboard-state-required": "대상 대시보드 상태가 필요합니다.", + "set-entity-from-widget": "위젯으로 부터 객체 설정", + "target-dashboard": "대상 대시보드", + "open-right-layout": "Open right dashboard layout (mobile view)" + }, + "widgets-bundle": { + "current": "현재 번들", + "widgets-bundles": "위젯 번들", + "add": "위젯 번들 추가", + "delete": "위젯 번들 삭제", + "title": "제목", + "title-required": "제목을 입력하세요.", + "add-widgets-bundle-text": "위젯 번들 추가", + "no-widgets-bundles-text": "위젯 번들이 없습니다.", + "empty": "위젯 번들이 비어있습니다.", + "details": "상세", + "widgets-bundle-details": "위젯 번들 상세 정보", + "delete-widgets-bundle-title": "'{{widgetsBundleTitle}}' 위젯 번들을 삭제하시겠습니까?", + "delete-widgets-bundle-text": "위젯 번들과 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", + "delete-widgets-bundles-title": "{ count, plural, 1 {위젯 번들 1개} other {위젯 번들 #개} }를 삭제하시겠습니까?", + "delete-widgets-bundles-action-title": "{ count, plural, 1 {위젯 번들 1개} other {위젯 번들 #개} } 삭제", + "delete-widgets-bundles-text": "선택된 위젯 번들이 삭제되고 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", + "no-widgets-bundles-matching": "'{{widgetsBundle}}' 와(과) 일치하는 위젯 번들을 찾을 수 없습니다.", + "widgets-bundle-required": "위젯 번들을 입력하세요.", + "system": "시스템", + "import": "위젯 번들 가져오기", + "export": "위젯 번들 내보내기", + "export-failed-error": "위젯 번들을 내보내기 할 수 없습니다.: {{error}}", + "create-new-widgets-bundle": "새로운 위젯 번들 생성", + "widgets-bundle-file": "위젯 번들 파일", + "invalid-widgets-bundle-file-error": "위젯 번들을 가져오기 할 수 없습니다.: 잘못된 위젯 번들 데이터 구조입니다." + }, + "widget-config": { + "data": "데이터", + "settings": "설정", + "advanced": "고급", + "title": "제목", + "general-settings": "일반 설정", + "display-title": "제목 표시", + "drop-shadow": "그림자", + "enable-fullscreen": "전체화면 사용 ", + "background-color": "배경 색", + "text-color": "글자 색", + "padding": "패딩", + "title-style": "제목 스타일", + "mobile-mode-settings": "모바일 모드 설정", + "order": "순서", + "height": "높이", + "units": "값 옆에 표시할 특수 기호", + "decimals": "소수점 이하 자릿수", + "timewindow": "타임윈도우", + "use-dashboard-timewindow": "대시보드 타임윈도우", + "display-legend": "범례 표시", + "datasources": "데이터소스", + "datasource-type": "유형", + "datasource-parameters": "파라미터", + "remove-datasource": "데이터소스 삭제", + "add-datasource": "데이터소스 추가", + "target-device": "대상 디바이스" + }, + "widget-type": { + "import": "위젯 타입 가져오기", + "export": "위젯 타입 내보내기", + "export-failed-error": "위젯 타입을 내보내기 할 수 없습니다.: {{error}}", + "create-new-widget-type": "새로운 위젯 타입 생성", + "widget-type-file": "위젯 타입 파일", + "invalid-widget-type-file-error": "위젯 타입을 가져오기 할 수 없습니다.: 잘못된 위젯 타입 데이터 구조입니다." + }, + "widgets": { + "date-range-navigator": { + "localizationMap": { + "Sun": "일", + "Mon": "월", + "Tue": "화", + "Wed": "수", + "Thu": "목", + "Fri": "금", + "Sat": "토", + "Jan": "1월", + "Feb": "2월", + "Mar": "3월", + "Apr": "4월", + "May": "5월", + "Jun": "6월", + "Jul": "7월", + "Aug": "8월", + "Sep": "9월", + "Oct": "10월", + "Nov": "11월", + "Dec": "12월", + "January": "1월", + "February": "2월", + "March": "3월", + "April": "4월", + "June": "6월", + "July": "7월", + "August": "8월", + "September": "9월", + "October": "10월", + "November": "11월", + "December": "12월", + "Custom Date Range": "임의 기간 범위", + "Date Range Template": "기간 템플릿", + "Today": "오늘", + "Yesterday": "어제", + "This Week": "이번 주", + "Last Week": "지난주", + "This Month": "이번 달", + "Last Month": "지난 달", + "Year": "년", + "This Year": "올해", + "Last Year": "작년", + "Date picker": "날짜 선택기", + "Hour": "시간", + "Day": "일", + "Week": "주", + "2 weeks": "2 주", + "Month": "달", + "3 months": "3 개월", + "6 months": "6 개월", + "Custom interval": "사용자 지정 간격", + "Interval": "간격", + "Step size": "단계 크기", + "Ok": "확인" + } + } + }, + "icon": { + "icon": "아이콘", + "select-icon": "선택된 아이콘", + "material-icons": "Material icons", + "show-all": "모든 아이콘 보기" + }, + "custom": { + "widget-action": { + "action-cell-button": "Action cell button", + "row-click": "On row click", + "polygon-click": "On polygon click", + "marker-click": "On marker click", + "tooltip-tag-action": "Tooltip tag action" + } + }, + "language": { + "language": "언어" + } +} diff --git a/ui-ngx/src/assets/locale/locale.constant-lv_LV.json b/ui-ngx/src/assets/locale/locale.constant-lv_LV.json new file mode 100644 index 0000000000..4cefaf52fa --- /dev/null +++ b/ui-ngx/src/assets/locale/locale.constant-lv_LV.json @@ -0,0 +1,1683 @@ +{ + "access": { + "unauthorized": "Neatļauta", + "unauthorized-access": "Neatļauta piekļuve", + "unauthorized-access-text": "Lai piekļūtu šim resursam, jums jāpierakstās!", + "access-forbidden": "Piekļuve aizliegta", + "access-forbidden-text": "Jums nav piekļuves tiesību!
    Mēģiniet pierakstīties ar citu lietotājvārdu.", + "refresh-token-expired": "Sesija ir beigusies", + "refresh-token-failed": "Nevar atjaunot sesiju" + }, + "action": { + "activate": "Aktivizēt", + "suspend": "Apturēt", + "save": "Saglabāt", + "saveAs": "Saglabāt kā", + "cancel": "Atcelt", + "ok": "OK", + "delete": "Dzēst", + "add": "Pievienot", + "yes": "Jā", + "no": "Nē", + "update": "Atjaunināt", + "remove": "Noņemt", + "search": "Meklēt", + "clear-search": "Notīrīt meklēšanu", + "assign": "Piešķirt", + "unassign": "Noņemt", + "share": "Dalīties", + "make-private": "Padarīt privātu", + "apply": "Pielietot", + "apply-changes": "Pielietot izmaiņas", + "edit-mode": "Rediģēšanas režīms", + "enter-edit-mode": "Ievadiet rediģēšanas režīmu", + "decline-changes": "Noraidīt izmaiņas", + "close": "Aizvērt", + "back": "Atpakaļ", + "run": "Uz priekšu", + "sign-in": "Pierakstīties!", + "edit": "Rediģēt", + "view": "Skatīt", + "create": "Radīt", + "drag": "Velciet", + "refresh": "Atjaunot", + "undo": "Atsaukt", + "copy": "Kopēt", + "paste": "Ielīmēt", + "copy-reference": "Kopija atsauce", + "paste-reference": "Ielīmēt atsauce", + "import": "Importēt", + "export": "Eksportēt", + "share-via": "Dalīties caur {{provider}}", + "continue": "Turpināt" + }, + "aggregation": { + "aggregation": "Sakopojums", + "function": "Datu sakopojuma funkcija", + "limit": "Limits", + "group-interval": "Grupas intervāls", + "min": "Min", + "max": "Max", + "avg": "Vidējais", + "sum": "Sum", + "count": "Skaits", + "none": "Neviena" + }, + "admin": { + "general": "Vispārīgi", + "general-settings": "Vispārīgie iestatījumi", + "outgoing-mail": "Pasta serveris", + "outgoing-mail-settings": "Izejošā pasta servera iestatījumi", + "system-settings": "Sistēmas iestatījumi", + "test-mail-sent": "Testa pasts sekmīgi nosūtīts!", + "base-url": "pamata URL", + "base-url-required": "Pamata URL ir nepieciešams.", + "mail-from": "Pasts no", + "mail-from-required": "Pasts no ir nepieciešams.", + "smtp-protocol": "SMTP protokols", + "smtp-host": "SMTP saimnieks", + "smtp-host-required": "SMTP saimnieks ir nepieciešams.", + "smtp-port": "SMTP ports", + "smtp-port-required": "Jums vajag nodrošināt SMTP portu.", + "smtp-port-invalid": "Tas neizskatās pēc atļauta SMTP porta.", + "timeout-msec": "Pārtraukums (msec)", + "timeout-required": "Pārtraukums ir nepieciešams.", + "timeout-invalid": "Tas neizskatās pēc atļauta pārtraukuma.", + "enable-tls": "Iespējot TLS", + "send-test-mail": "Nosūtīt testa pastu" + }, + "alarm": { + "alarm": "Trauksme", + "alarms": "Trauksmes", + "select-alarm": "Atlasīt trauksmi", + "no-alarms-matching": "Nav atbilstošu trauksmju '{{entity}}' .", + "alarm-required": "Trauksme ir nepieciešama", + "alarm-status": "Trauksmes statuss", + "search-status": { + "ANY": "Jebkura", + "ACTIVE": "Aktīvs", + "CLEARED": "Dzēsts", + "ACK": "Apstiprināts", + "UNACK": "Neapstiprināts" + }, + "display-status": { + "ACTIVE_UNACK": "Aktīvs Neapstiprināts", + "ACTIVE_ACK": "Aktīvs Apstiprināts", + "CLEARED_UNACK": "Dzēsts Neapstiprināts", + "CLEARED_ACK": "Dzēsts Apstiprināts" + }, + "no-alarms-prompt": "Trauksmes nav atrastas", + "created-time": "Izveidošanas laiks", + "type": "Tips", + "severity": "Smaguma pakāpe", + "originator": "Iniciātors", + "originator-type": "Iniciātora tips", + "details": "Detaļas", + "status": "Statuss", + "alarm-details": "Trauksmes detaļas", + "start-time": "Sākuma laiks", + "end-time": "Beigu laiks", + "ack-time": "Apstiprinājuma laiks", + "clear-time": "Notīrīšanas laiks", + "severity-critical": "Smaguma pakāpe - kritiska", + "severity-major": "Būtiska", + "severity-minor": "Minora", + "severity-warning": "Brīdinājums", + "severity-indeterminate": "Nenoteikts", + "acknowledge": "Apstiprināt", + "clear": "Notīrīt", + "search": "Meklēt trauksmes", + "selected-alarms": "{ count, plural, 1 {1 alarm} other {# trauksmes} } selected", + "no-data": "Nav datu ko attēlot", + "polling-interval": "Trauksmju pārbaužu intervāls (sec)", + "polling-interval-required": "Trauksmju pārbaužu intervāls ir nepieciešams.", + "min-polling-interval-message": "Vismaz 1 sekundes pārbaužu intervāls ir atļauts.", + "aknowledge-alarms-title": "Apstiprināt { count, plural, 1 {1 alarm} other {# trauksmes} }", + "aknowledge-alarms-text": "Vai Jūs tiešām vēlaties apstirpināt { count, plural, 1 {1 alarm} other {# trauksmes} }?", + "aknowledge-alarm-title": "Apstiprināt trauksmi", + "aknowledge-alarm-text": "Vai Jūs tiešām vēlaties apstiprināt trauksmi?", + "clear-alarms-title": "Dzēst { count, plural, 1 {1 alarm} other {# trauksmes} }", + "clear-alarms-text": "Vai Jūs tiešām vēlaties dzēst { count, plural, 1 {1 alarm} other {# trauksmes} }?", + "clear-alarm-title": "Dzēst trauksmi", + "clear-alarm-text": "Vai Jūs tiešām vēlaties dzēst trauksmi?", + "alarm-status-filter": "Trauksmes statusa filtrs" + }, + "alias": { + "add": "Pievienot segvārdu", + "edit": "Rediģēt segvārdu", + "name": "Segvārda nosaukums", + "name-required": "Segvārda nosaukums vārds ir nepieciešams", + "duplicate-alias": "Segvārds ar tādu pašu nosaukumu jau eksistē.", + "filter-type-single-entity": "Viena vienība", + "filter-type-entity-list": "Vienību saraksts", + "filter-type-entity-name": "Vienības vārds", + "filter-type-state-entity": "Vienība no paneļa stāvokļa", + "filter-type-state-entity-description": "Vienība ņemta no paneļa stāvokļa parametriem", + "filter-type-asset-type": "Aktīvu tips", + "filter-type-asset-type-description": "Aktīvu tipi '{{assetType}}'", + "filter-type-asset-type-and-name-description": "Aktīvu tips '{{assetType}}' un ar vārdu sākot ar '{{prefix}}'", + "filter-type-device-type": "Iekārtas tips", + "filter-type-device-type-description": "Iekārtas tipi '{{deviceType}}'", + "filter-type-device-type-and-name-description": "Iekārtas tipi '{{deviceType}}' un ar vārdu sākot ar '{{prefix}}'", + "filter-type-entity-view-type": "Vienības skata tips", + "filter-type-entity-view-type-description": "Vienības skats tipam '{{entityView}}'", + "filter-type-entity-view-type-and-name-description": "Vienības skats tipam '{{entityView}}' un ar vārdu sākot ar '{{prefix}}'", + "filter-type-relations-query": "Attiecību vaicājums", + "filter-type-relations-query-description": "{{entities}} kam ir {{relationType}} attiecība {{direction}} {{rootEntity}}", + "filter-type-asset-search-query": "Aktīvu meklēšanas vaicājums", + "filter-type-asset-search-query-description": "Aktīvi ar tipu {{assetTypes}} kam ir {{relationType}} attiecība {{direction}} {{rootEntity}}", + "filter-type-device-search-query": "Iekārtu meklēšanas vaicājums", + "filter-type-device-search-query-description": "Iekārtas ar tipu {{deviceTypes}} kam ir {{relationType}} attiecība {{direction}} {{rootEntity}}", + "filter-type-entity-view-search-query": "Vienības skata meklēšanas vaicājuma", + "filter-type-entity-view-search-query-description": "Vienību skats ar tipu {{entityViewTypes}} kam ir {{relationType}} attiecība {{direction}} {{rootEntity}}", + "entity-filter": "Vienību filtrs", + "resolve-multiple": "Atrisināt kā daudzas vienības", + "filter-type": "Filtra tips", + "filter-type-required": "Filtra tips ir nepieciešams.", + "entity-filter-no-entity-matched": "Nav atrastas vienības kam atbilst filtru iestatījumi.", + "no-entity-filter-specified": "Nav vienību filtrs specificēts", + "root-state-entity": "Lieto paneļa statusa vienību kā sakni ", + "root-entity": "Saknes vienības", + "state-entity-parameter-name": "Statusa vienības parametra vārds", + "default-state-entity": "Noklusējuma statusa vienība", + "default-entity-parameter-name": "Pēc noklusējuma", + "max-relation-level": "Maksimālais attiecību līmenis", + "unlimited-level": "Nelimitēts līmenis", + "state-entity": "Paneļa statusa vienība", + "all-entities": "Visas vienības", + "any-relation": "Jebkura" + }, + "asset": { + "asset": "Aktīvs", + "assets": "Aktīvi", + "management": "Aktīvu pārvaldība", + "view-assets": "Skatīt aktīvus", + "add": "Pievienot aktīvu", + "assign-to-customer": "Pieškirt klientam", + "assign-asset-to-customer": "Piešķirt aktīvu klientam", + "assign-asset-to-customer-text": "Lūdzu izvēlēties aktīvu lai pieškirtu klientam", + "no-assets-text": "Aktīvi nav atrasti", + "assign-to-customer-text": "Lūdzu izvēlēties klientu lai pieškirtu aktīvu", + "public": "Publisks", + "assignedToCustomer": "Pieškirts klientam", + "make-public": "Veidot aktīvu publisku", + "make-private": "Veidot aktīvu privātu", + "unassign-from-customer": "Noņemt klientam", + "delete": "Dzēst aktīvu", + "asset-public": "Aktīvs ir publisks", + "asset-type": "Aktīva tips", + "asset-type-required": "Aktīva tips ir nepieciešams.", + "select-asset-type": "Izvēlies aktīva tipu", + "enter-asset-type": "Ievadi aktīva tipu", + "any-asset": "Jebkurš aktīvs", + "no-asset-types-matching": "Nav atbilstošs aktīvu tips '{{entitySubtype}}' atrasts.", + "asset-type-list-empty": "Nav aktīvu tipi izvēlēti.", + "asset-types": "Aktīvu tipi", + "name": "Vārds", + "name-required": "Vārds ir nepieciešams.", + "description": "Apraksts", + "type": "Tips", + "type-required": "Tips ir nepieciešams.", + "details": "Detaļas", + "events": "Notikumi", + "add-asset-text": "Pievieno jaunu aktīvu", + "asset-details": "Aktīvu detaļas", + "assign-assets": "Piešķirt aktīvus", + "assign-assets-text": "Piešķirt { count, plural, 1 {1 asset} other {# aktīvus} } klientam", + "delete-assets": "Dzēst aktīvus", + "unassign-assets": "Noņemt aktīvus", + "unassign-assets-action-title": "Noņemt { count, plural, 1 {1 asset} other {# aktīvus} } no klienta", + "assign-new-asset": "Pieškirt jaunu aktīvu", + "delete-asset-title": "Vai esat pārliecināts,ka vēlaties dzēst aktīvu '{{assetName}}'?", + "delete-asset-text": "Esiet uzmanīgs, pēc apstiprināšanas aktīvs un saistītie dati nebūs atjaunojami.", + "delete-assets-title": "Vai esat pārliecināts ka vēlaties dzēst { count, plural, 1 {1 asset} other {# aktīvus} }?", + "delete-assets-action-title": "Dzēst { count, plural, 1 {1 asset} other {# aktīvus} }", + "delete-assets-text": "Esiet uzmanīgs, pēc apstiprinājuma visi izvēlētie aktīvi tiks dzēsti un saistītā informācija nebūs atjaunojama.", + "make-public-asset-title": "Vai esat pārliecināts ka vēlaties aktīvu '{{assetName}}' veidot publisku?", + "make-public-asset-text": "Pēc apstiprinājuma aktīvs un tā dati tiks publiski pieejami.", + "make-private-asset-title": "Vai esat pārliecināts ka vēlaties aktīvu '{{assetName}}' veidot privātu?", + "make-private-asset-text": "Pēc apstiprinājums aktīvs un tā saistītie dati būs privāti un nebūs pieejami citiem.", + "unassign-asset-title": "Vai esat pārliecināts ka vēlaties noņemt aktīvu '{{assetName}}'?", + "unassign-asset-text": "Pēc apstiprināšanas aktīvs tiks noņemts un nebūs pieejams klientiem.", + "unassign-asset": "Noņemt aktīvu", + "unassign-assets-title": "Vai esat pārliecināts ka vēlaties noņemt { count, plural, 1 {1 asset} other {# aktīvus} }?", + "unassign-assets-text": "Pēc apstiprināšanas visi izvēlētie aktīvi būs noņemti un nebūs pieejami klientiem.", + "copyId": "Kopēt aktīva Id", + "idCopiedMessage": "Aktīva Id ir kopēts uz starpliktuvi", + "select-asset": "Atlasīt aktīvu", + "no-assets-matching": "Nav atbilstošs aktīvs '{{entity}}' atrasts.", + "asset-required": "Aktīvs ir nepieciešams", + "name-starts-with": "Aktīva vārds sākas ar", + "import": "Importēt aktīvus", + "asset-file": "Aktīvu fails" + }, + "attribute": { + "attributes": "Attribūti", + "latest-telemetry": "Jaunākā telemetrija", + "attributes-scope": "Vienības atribūtu darbības joma", + "scope-latest-telemetry": "Jaunākā telemetrija", + "scope-client": "Klientu atribūti", + "scope-server": "Servera atribūti", + "scope-shared": "Dalītie atribūti", + "add": "Pievieno atribūtu", + "key": "Atslēga", + "last-update-time": "Pēdēja atjaunojuma laiks", + "key-required": "Atribūta atslēga ir nepieciešama.", + "value": "Vērtība", + "value-required": "Atribūta vērtība ir nepieciešama.", + "delete-attributes-title": "Vai esat pārliecināts ka vēlaties dzēst { count, plural, 1 {1 attribute} other {# attribūtus} }?", + "delete-attributes-text": "Esiet uzmanīgs, pēc apstiprinājuma visi izvēlētie atribūti tiks dzēsti.", + "delete-attributes": "Dzēst atribūtu", + "enter-attribute-value": "Ievadiet atribūta vērtību", + "show-on-widget": "Parādīt logrīkā", + "widget-mode": "Logrīka režīms", + "next-widget": "Nākamais logrīks", + "prev-widget": "Iepriekšējais logrīks", + "add-to-dashboard": "Pievienot panelim", + "add-widget-to-dashboard": "Pievienot logrīku panelim", + "selected-attributes": "{ count, plural, 1 {1 attribute} other {# atribūtus} } izvēlētajam", + "selected-telemetry": "{ count, plural, 1 {1 telemetry unit} other {# telemetrijas vienības} } izvēlētas" + }, + "audit-log": { + "audit": "Audits", + "audit-logs": "Audita logs", + "timestamp": "Laika zīmogs", + "entity-type": "Vienības tips", + "entity-name": "Vienības vārds", + "user": "Lietotājs", + "type": "Tips", + "status": "Statuss", + "details": "Detaļas", + "type-added": "Pievienots", + "type-deleted": "Dzēsts", + "type-updated": "Atjaunots", + "type-attributes-updated": "Atribūti atjaunoti", + "type-attributes-deleted": "Atribūti dzēsti", + "type-rpc-call": "RPC izsaukumi", + "type-credentials-updated": "Akreditācijas dati atjaunoti", + "type-assigned-to-customer": "Pieškirts klientam", + "type-unassigned-from-customer": "Noņemts no klienta", + "type-activated": "Aktivizēts", + "type-suspended": "Apturēts", + "type-credentials-read": "Akreditācijas datu nolasījums", + "type-attributes-read": "Atribūtu nolasījums", + "type-relation-add-or-update": "Attiecība atjaunota", + "type-relation-delete": "Atiecība dzēsta", + "type-relations-delete": "Visas attiecības dzēstas", + "type-alarm-ack": "Apstiprinājums", + "type-alarm-clear": "Notīrīts", + "status-success": "Sekmīgi", + "status-failure": "Neveiksme", + "audit-log-details": "Audita loga detaļas", + "no-audit-logs-prompt": "Nav logu atrastu", + "action-data": "Aktivitāšu dati", + "failure-details": "Neveiksmju detaļas", + "search": "Meklēt audita logus", + "clear-search": "Notīrīt meklēšanu" + }, + "confirm-on-exit": { + "message": "Jums ir nesaglabātas izmaiņas. Vai tiešām vēlaties pamest šo lapu?", + "html-message": "Jums ir nesaglabātas izmaiņas.
    Vai tiešām vēlaties pamest šo lapu?", + "title": "Nesaglabātas izmaiņas" + }, + "contact": { + "country": "Valsts", + "city": "Pilsēta", + "state": "Štats/Province", + "postal-code": "Zip / Pasta kods", + "postal-code-invalid": "Invalīds Zip / Pasta koda formāts.", + "address": "Adrese", + "address2": "Adrese 2", + "phone": "Telefons", + "email": "Email", + "no-address": "Nav adreses" + }, + "common": { + "username": "Lietotājvārdse", + "password": "Parole", + "enter-username": "Ievadiet lietotājvārdu", + "enter-password": "Ievadiet paroli", + "enter-search": "Ievadiet meklēt", + "created-time": "Izveidošanas laiks" + }, + "content-type": { + "json": "Json", + "text": "Teksts", + "binary": "Bināri (Base64)" + }, + "customer": { + "customer": "Klients", + "customers": "Klienti", + "management": "Klientu pārvaldība", + "dashboard": "Klientu panelis", + "dashboards": "Klientu paneļi", + "devices": "Klienta iekārtas", + "entity-views": "Klienta vienību skati", + "assets": "Klienta aktīvi", + "public-dashboards": "Publiskie paneļi", + "public-devices": "Publiskās iekārtas", + "public-assets": "Publiskie aktīvi", + "public-entity-views": "Publisko vienību skati", + "add": "Pievienot klientu", + "delete": "Dzēst klientu", + "manage-customer-users": "Pārvaldīt klienta lietotājus", + "manage-customer-devices": "Pārvaldīt klienta iekārtas", + "manage-customer-dashboards": "Pārvaldīt klienta paneļus", + "manage-public-devices": "Pārvaldīt publiskās iekārtas", + "manage-public-dashboards": "Pārvaldīt publiskos paneļus", + "manage-customer-assets": "Pārvaldīt klienta aktīvus", + "manage-public-assets": "Pārvaldīt publiskos aktīvus", + "add-customer-text": "Pievienot jaunu klientu", + "no-customers-text": "Nav klienti atrasti", + "customer-details": "Klienta detaļas", + "delete-customer-title": "Vai esat pārliecināts, ka vēlaties dzēst klientu '{{customerTitle}}'?", + "delete-customer-text": "Esiet uzmanīgs, pēc apstiprinājuma klients un tā saistītie dati nebūs atjaunojami.", + "delete-customers-title": "Vai esat pārliecināts, ka vēlaties dzēst { count, plural, 1 {1 customer} other {# klientus} }?", + "delete-customers-action-title": "Dzēst { count, plural, 1 {1 customer} other {# klientus} }", + "delete-customers-text": "Esiet uzmanīgs, pēc apstiprinājuma visi izvēlētie klienti tisk dzēsti un to saistītie dati nebūs atjaunojami.", + "manage-users": "Pārvaldīt lietotājus", + "manage-assets": "Pārvaldīt aktīvus", + "manage-devices": "Pārvaldīt iekārtas", + "manage-dashboards": "Pārvaldīt paneļus", + "title": "Virsraksts", + "title-required": "Virsraksts ir nepieciešams.", + "description": "Apraksts", + "details": "Detaļas", + "events": "Notikumi", + "copyId": "Kopēt klienta Id", + "idCopiedMessage": "Klienta Id ir kopēts uz starpliktuvi", + "select-customer": "Atlasīt klientu", + "no-customers-matching": "Nav atbilstoši klienti '{{entity}}' atrasti.", + "customer-required": "Klients ir nepieciešams", + "select-default-customer": "Atlasīt pamata klientu", + "default-customer": "Pamata klients", + "default-customer-required": "Pamata klients ir nepieciešams lai atkļūdotu paneli īrnieka līmenī" + }, + "datetime": { + "date-from": "Datums no", + "time-from": "Laiks no", + "date-to": "Datums līdz", + "time-to": "Laiks līdz" + }, + "dashboard": { + "dashboard": "Panelis", + "dashboards": "Paneļi", + "management": "Paneļu pārvaldība", + "view-dashboards": "Skatīt paneļus", + "add": "Pievienot paneļus", + "assign-dashboard-to-customer": "Piešķirt paneļus klientam", + "assign-dashboard-to-customer-text": "Lūdzu izvēlēties paneļus lai piešķirtu tos klientam", + "assign-to-customer-text": "Lūdzu izvēlēties klientu, kuram piešķirt paneļus", + "assign-to-customer": "Piešķirt klientam", + "unassign-from-customer": "Noņemt no klienta", + "make-public": "Veidot paneli publisku", + "make-private": "Veidot paneli privātu", + "manage-assigned-customers": "Pārvaldīt piešķirtos klientus", + "assigned-customers": "Piešķirtie klienti", + "assign-to-customers": "Piešķirt paneļus klientiem", + "assign-to-customers-text": "Lūdzu atlasīt klientus lai pieškirtu paneļus", + "unassign-from-customers": "Noņemt no klientiem paneļus", + "unassign-from-customers-text": "Lūdzu atlasīt klientus kuriem noņemt paneļus", + "no-dashboards-text": "Nav paneļi atrasti", + "no-widgets": "Nav logrīki konfigurēti", + "add-widget": "Pievienot jaunu logrīku", + "title": "Virsraksts", + "select-widget-title": "Atlasīt logrīku", + "select-widget-subtitle": "Pieejamo logrīku tipu saraksts", + "delete": "Dzēst paneli", + "title-required": "Virsraksts ir nepieciešams.", + "description": "Apraksts", + "details": "Detaļas", + "dashboard-details": "Paneļa detaļas", + "add-dashboard-text": "Pievienot jaunu paneli", + "assign-dashboards": "Pieškirt paneļus", + "assign-new-dashboard": "Pieškirt jaunu paneli", + "assign-dashboards-text": "Pieškirt { count, plural, 1 {1 dashboard} other {# paneļus} } klientiem", + "unassign-dashboards-action-text": "Noņemt { count, plural, 1 {1 dashboard} other {# paneļus} } no klientiem", + "delete-dashboards": "Dzēst paneļus", + "unassign-dashboards": "Noņemt paneļus", + "unassign-dashboards-action-title": "Noņemt { count, plural, 1 {1 dashboard} other {# paneļus} } no klienta", + "delete-dashboard-title": "Vai esat pārliecināts ka vēlaties dzēst paneli '{{dashboardTitle}}'?", + "delete-dashboard-text": "Esiet uzmanīgs, pēc apstiprinājuma panelis un visi tā saistītie dati nebūs atjaunojami.", + "delete-dashboards-title": "Vai esat pārliecināts ka vēlaties dzēst { count, plural, 1 {1 dashboard} other {# paneļus} }?", + "delete-dashboards-action-title": "Dzēst { count, plural, 1 {1 dashboard} other {# paneļus} }", + "delete-dashboards-text": "Esiet uzmanīgs, pēc apstiprinājuma visi izvēlētie paneļi būs noņemti un visi saistitie dati nebūs atjaunojami.", + "unassign-dashboard-title": "Vai esat pārliecināts, ka vēlaties noņemt paneli '{{dashboardTitle}}'?", + "unassign-dashboard-text": "Pēc apstiprinājuma panelis tiks noņemts un nebūs pieejams klientam.", + "unassign-dashboard": "Noņemt paneli", + "unassign-dashboards-title": "Vai esat pārliecināts ka vēlaties noņemt { count, plural, 1 {1 dashboard} other {# paneļus} }?", + "unassign-dashboards-text": "Pēc apstiprinājuma visi izvēlētie paneļi būs noņemti un nebūs pieejami klientam.", + "public-dashboard-title": "Panelis tagad ir publisks", + "public-dashboard-text": "Jūsu panelis {{dashboardTitle}} tagad ir publisks un pieejams pēc saites link:", + "public-dashboard-notice": "Note: Neaizmirstie veidot attiecīgās iekārtas publiski pieejamas lai piekļutu to datiem.", + "make-private-dashboard-title": "Vai esat pārliecināts, ka vēlaties veidot paneli '{{dashboardTitle}}' privātu?", + "make-private-dashboard-text": "Pēc apstiprinājuma panelis būs privāts un nebūs pieejams citiem.", + "make-private-dashboard": "Veidot paneli privātu", + "socialshare-text": "'{{dashboardTitle}}' atbalsts no TeT", + "socialshare-title": "'{{dashboardTitle}}' atbalsts no TeT", + "select-dashboard": "Atlasīt paneli", + "no-dashboards-matching": "Nav atbilstoši paneļi '{{entity}}' atrasti.", + "dashboard-required": "Penelis ir nepieciešams.", + "select-existing": "Atlasīt paneli", + "create-new": "Radīt jaunu paneli", + "new-dashboard-title": "Jauns paneļa Virsraksts", + "open-dashboard": "Atvērt paneli", + "set-background": "Iestatīt fonu", + "background-color": "Fona krāsa", + "background-image": "Fona attēls", + "background-size-mode": "Fona lieluma mode", + "no-image": "Nav izvēlēts attēls", + "drop-image": "Nomest attēlu vai noklikšķiniet lai atlasītu failu augšupielādei.", + "settings": "Iestatījumi", + "columns-count": "Kolonu skaitīšana", + "columns-count-required": "Kolonu skaitīšana ir nepieciešams.", + "min-columns-count-message": "Tikai minimums 10 kolonu skaitīšana ir atļauta.", + "max-columns-count-message": "Tikai maksimums 100 kolonu skaitīšana ir atļauta.", + "widgets-margins": "Robeža starp logrīkiem", + "horizontal-margin": "Horizontālā robeža", + "horizontal-margin-required": "Horizontālās robežas vērtība ir nepieciešama.", + "min-horizontal-margin-message": "Tikai 0 ir atļauta kā minimālā horizontālās robežas vērtība.", + "max-horizontal-margin-message": "Tikai 50 ir atļauta kā maksimālā horizontālās robežas vērtība.", + "vertical-margin": "Vertikālā robeža", + "vertical-margin-required": "Vertikālās robežas vērtība ir nepieciešama.", + "min-vertical-margin-message": "Tikai 0 ir atļauta kā minimālā vertikālās robežas vērtība.", + "max-vertical-margin-message": "Tikai 50 ir atļauta kā maksimālā vertikālās robežas vērtība.", + "autofill-height": "Automātiskās aizpildīšanas izkārtojuma augstums", + "mobile-layout": "Mobilā izkārtojuma iestatījumi", + "mobile-row-height": "Mobilās rindas augstums, px", + "mobile-row-height-required": "Mobile row height value is required.", + "min-mobile-row-height-message": "Tikai 5 pikseļi ir atļauti kā minimālās mobilās rindas augstuma vērtības.", + "max-mobile-row-height-message": "Tikai 200 pikseļi ir atļauti kā maksimālās mobilās rindas augstuma vērtības.", + "display-title": "Parādīt paneļa virsrakstu", + "toolbar-always-open": "Turēt rīkjoslu atvērtu", + "title-color": "Virsraksta krāsa", + "display-dashboards-selection": "Parādīt paneļa izvēli", + "display-entities-selection": "Parādīt vienību izvēli", + "display-dashboard-timewindow": "Parādīt laika logu", + "display-dashboard-export": "Parādīt eksportu", + "import": "Importēt paneli", + "export": "Eksportēt panelis", + "export-failed-error": "Nav iespējams eksportēt paneli: {{error}}", + "create-new-dashboard": "Radīt jaunu paneli", + "dashboard-file": "Paneļa fails", + "invalid-dashboard-file-error": "Nav iespējams importēt paneli: Invalīda paneļa datu struktūra.", + "dashboard-import-missing-aliases-title": "Jānokonfigurē segvārdi kas lietoti importētajā panelī", + "create-new-widget": "Radīt jaunu logrīku", + "import-widget": "Importēt logrīku", + "widget-file": "Logrīka fails", + "invalid-widget-file-error": "Nav iespējams importēt logrīku: Invalīda logrīka datu struktūra.", + "widget-import-missing-aliases-title": "Jānokonfigurē segvārdi kas lietoti importētajā logrīkā", + "open-toolbar": "Atvērt paneļa rīkjoslu", + "close-toolbar": "Aizvērt rīkjoslu", + "configuration-error": "Konfigurācijas kļūda", + "alias-resolution-error-title": "Paneļa segvārdu konfigurācijas kļūda", + "invalid-aliases-config": "Nav iespējams atrast nevienu iekārtu kam atbilst kāds no segvārdu filtriem.
    Lūdzu sazinieties ar savu administrātoru.", + "select-devices": "Atlasīt iekārtas", + "assignedToCustomer": "Pišķirtas klientam", + "assignedToCustomers": "Piešķirtas klientiem", + "public": "Publisks", + "public-link": "Publiska saite", + "copy-public-link": "Kopēt publisku saiti", + "public-link-copied-message": "Paneļa publiskā saite ir kopēta starpliktuvē", + "manage-states": "Pārvaldīt paneļa stāvokļus", + "states": "Paneļa stāvokļi", + "search-states": "Meklēt paneļa stāvokļus", + "selected-states": "{ count, plural, 1 {1 dashboard state} other {# paneļu statusus} } atlasītos", + "edit-state": "Rediģēt paneļa stāvokli", + "delete-state": "Dzēst paneļa stāvokli", + "add-state": "Pievienot paneļa stāvokli", + "state": "Paneļa stāvoklis", + "state-name": "Nosaukums", + "state-name-required": "Paneļa stāvokļa nosaukums ir nepieciešams.", + "state-id": "Stāvokļa Id", + "state-id-required": "Paneļa stāvokļa Id ir nepieciešams.", + "state-id-exists": "Paneļa stāvoklis ar šādu Id jau eksistē.", + "is-root-state": "Saknes stāvoklis", + "delete-state-title": "Dzēst paneļa stāvokli", + "delete-state-text": "Vai esat pārliecināts ka vēlaties dzēst paneļa stāvokli ar nosaukumu '{{stateName}}'?", + "show-details": "Rādīt detaļas", + "hide-details": "Noslēpt detaļas", + "select-state": "Atlasīt mērķa stāvokli", + "state-controller": "Stavokļa kontrolieris" + }, + "datakey": { + "settings": "Iestatījumi", + "advanced": "Pieredzējis lietotājs", + "label": "Etiķete", + "color": "Krāsa", + "units": "Speciāls simbols, ko parādīt pēc vērtība", + "decimals": "Ciparu skaits aiz komata", + "data-generation-func": "Datu ģenerācijas funkcija", + "use-data-post-processing-func": "Lietot datu pēcapstrādes funkciju", + "configuration": "Datu atslēgu konfigurācija", + "timeseries": "Laika periodi", + "attributes": "Atribūti", + "alarm": "Trauksme", + "timeseries-required": "Vienības laika periodi ir nepieciešami.", + "timeseries-or-attributes-required": "Vienības laika periodi/atribūti ir nepieciešami.", + "maximum-timeseries-or-attributes": "Maksimums { count, plural, 1 {1 timeseries/attribute is allowed.} other {# laika sērijas/atribūti ir atļauti} }", + "alarm-fields-required": "Trauksmes lauki ir nepieciešami.", + "function-types": "Funkciju tipi", + "function-types-required": "Funkciju tipi ir nepieciešami.", + "maximum-function-types": "Maksimums { count, plural, 1 {1 function type is allowed.} other {# funkciju tipi ir atļauti} }", + "time-description": "Laika zīmogs patreizējai vērtībai;", + "value-description": "patreizējā vērtība;", + "prev-value-description": "rezultāts no iepriekšējā funkciju pieprasījuma;", + "time-prev-description": "Laika zīmogs no iepriekšējās vērtības;", + "prev-orig-value-description": "Oriģinālā iepriekšējā vērtība;" + }, + "datasource": { + "type": "Datu avota tips", + "name": "Nosaukums", + "add-datasource-prompt": "Lūdzu pievienot datu avotu" + }, + "details": { + "edit-mode": "Rediģēšanas mode", + "toggle-edit-mode": "Pārslēgt rediģēšanas modi" + }, + "device": { + "device": "Iekārta", + "device-required": "Iekārta ir nepieciešama.", + "devices": "Iekārtas", + "management": "Iekārtu pārvaldība", + "view-devices": "Skatīt iekārtas", + "device-alias": "Iekārtu segvārdi", + "aliases": "Iekārtas segvārdi", + "no-alias-matching": "'{{alias}}' nav atrasti.", + "no-aliases-found": "Nav segvārdi atrasti.", + "no-key-matching": "'{{key}}' nav atrasti.", + "no-keys-found": "Nav atslēgas atrastas.", + "create-new-alias": "Radīt jaunu!", + "create-new-key": "Radīt jaunu!", + "duplicate-alias-error": "Dublēti segvārdi atrasti '{{alias}}'.
    Iekārtas segvārdiem ir jābūt unikāliem panelī.", + "configure-alias": "Konfigurēt '{{alias}}' segvārdus", + "no-devices-matching": "Nav iekārtu atbilstības '{{entity}}' atrastas.", + "alias": "Segvārdi", + "alias-required": "Iekārtu segvārdi ir nepieciešami.", + "remove-alias": "Noņemt iekārtas segvārdus", + "add-alias": "Pievienot iekārtas segvārdus", + "name-starts-with": "Iekārtas nosaukums sākas ar", + "device-list": "Iekārtu saraksts", + "use-device-name-filter": "Lietot filtru", + "device-list-empty": "Nav iekārtas atlasītas.", + "device-name-filter-required": "Iekārtas nosaukuma filtrs ir nepieciešams.", + "device-name-filter-no-device-matched": "Nav iekārtas kas sākas ar '{{device}}' atrastas.", + "add": "Pievienot iekārtu", + "assign-to-customer": "Piešķirt klientam", + "assign-device-to-customer": "Piešķirt iekārtas klientam", + "assign-device-to-customer-text": "Lūdzu atlasīt iekārtas lai pieškirtu klientam", + "make-public": "Veidot iekārtu publisku", + "make-private": "Veidot iekārtu privātu", + "no-devices-text": "Nav iekārtas atrastas", + "assign-to-customer-text": "Lūdzu atlasīt klientu lai pieškirtu iekārtas", + "device-details": "iekārtas detaļas", + "add-device-text": "Pievienot jaunu iekārtu", + "credentials": "Akreditācijas dati", + "manage-credentials": "Pārvaldīt akreditācijas datus", + "delete": "Dzēst iekārtu", + "assign-devices": "Pieškirt iekārtas", + "assign-devices-text": "Piešķirt { count, plural, 1 {1 device} other {# iekārtas} } klientam", + "delete-devices": "Dzēst iekārtas", + "unassign-from-customer": "Noņemt no klienta", + "unassign-devices": "Noņemt iekārtas", + "unassign-devices-action-title": "Noņemt { count, plural, 1 {1 device} other {# iekārtas} } no klienta", + "assign-new-device": "Pieškirt jaunu iekārtu", + "make-public-device-title": "Vai esat pārliecināts ka vēlaties veidot iekārtu '{{deviceName}}' publisku?", + "make-public-device-text": "Pēc apstiprinājuma iekārta un tās saistītie dati būs pieejami publiski un pieejami citiem.", + "make-private-device-title": "vai esat pārliecināts ka vēlaties veidot iekārtu '{{deviceName}}' privāti?", + "make-private-device-text": "Pēc apstiprinājuma iekārta un tās saistītie dati būs pieejami privāti un nebūs pieejami citiem.", + "view-credentials": "Skatīt akreditācijas datus", + "delete-device-title": "Vai esat pārliecināts, ka vēlaties dzēst iekārtu '{{deviceName}}'?", + "delete-device-text": "Esat uzmanīgs, pēc apstiprinājuma iekārta un tās saistītie dati nebūs atjaunojami.", + "delete-devices-title": "Vai esat pārliecināts, ka vēlaties dzēst { count, plural, 1 {1 device} other {# iekārtas} }?", + "delete-devices-action-title": "Dzēst { count, plural, 1 {1 device} other {# iekārtas} }", + "delete-devices-text": "Esat uzmanīgs, pēc apstiprinājuma iekārtas un to saistītie dati tiks noņemti un nebūs atjaunojami.", + "unassign-device-title": "Vai esat pārliecināts, ka vēlaties noņemt iekārtu '{{deviceName}}'?", + "unassign-device-text": "Pēc apstiprinājuma iekārta tiks noņemta un nebūs pieejama klientam.", + "unassign-device": "Noņemt iekārtu", + "unassign-devices-title": "Vai esat pārliecināts, ka vēlaties noņemt { count, plural, 1 {1 device} other {# iekārtas} }?", + "unassign-devices-text": "Pēc apstipinājuma visas atlasītās iekārtas būs noņemtas un nebūs pieejamas klientam.", + "device-credentials": "iekārtas akreditācijas dati", + "credentials-type": "Akreditācijas datu tips", + "access-token": "Piekļuves tokens", + "access-token-required": "Piekļuves tokens ir nepieciešams.", + "access-token-invalid": "Piekļuves tokena garumam ir jābūt no 1 līdz 20 rakstzīmēm.", + "rsa-key": "RSA publiskā atslēga", + "rsa-key-required": "RSA publiskā atslēga ir nepieciešama.", + "secret": "Noslēpums", + "secret-required": "Noslēpums ir nepieciešams.", + "device-type": "Iekārtas tips", + "device-type-required": "Iekārtas tips ir nepieciešams.", + "select-device-type": "Atlasīt iekārtas tipu", + "enter-device-type": "Ievadīt iekārtas tipu", + "any-device": "Jebkura iekārta", + "no-device-types-matching": "Nav iekārtas tipa saderības '{{entitySubtype}}' atrastas.", + "device-type-list-empty": "Nav iekārtas tipi izvēlēti.", + "device-types": "Iekārtas tipi", + "name": "Nosaukums", + "name-required": "Nosaukums ir nepieciešams.", + "description": "Apraksts", + "label": "Etiķete", + "events": "Notikumi", + "details": "Detaļas", + "copyId": "Kopēt iekārtas Id", + "copyAccessToken": "Kopēt piekļuves tokenu", + "idCopiedMessage": "iekārtas Id ir kopēts uz starpliktuvi", + "accessTokenCopiedMessage": "Iekārtas piekļuves tokens ir kopēts uz starpliktuvi", + "assignedToCustomer": "Piešķirts klientam", + "unable-delete-device-alias-title": "Nav iespējas dzēst iekārtas segvārdus", + "unable-delete-device-alias-text": "Iekārtas segvārdi '{{deviceAlias}}' nevar būt dzēsti, jo tie lietoti sekojošajos logrīkos:
    {{widgetsList}}", + "is-gateway": "Tā ir vārteja", + "public": "Publisks", + "device-public": "Iekārta ir publiska", + "select-device": "Atlasīt iekārtu", + "import": "Importēt iekārtu", + "device-file": "Iekārtas fails" + }, + "dialog": { + "close": "Aizvērt dialogu" + }, + "direction": { + "column": "Kolona", + "row": "Rinda" + }, + "error": { + "unable-to-connect": "Nav iespējams pievienoties serverim! Lūdzu pārbaudīt interneta savienojumu.", + "unhandled-error-code": "Neapstrādāta kļūda: {{errorCode}}", + "unknown-error": "Nezināma kļūda" + }, + "entity": { + "entity": "Vienība", + "entities": "Vienības", + "aliases": "Vienību segvārdi", + "entity-alias": "Vienību segvārdi", + "unable-delete-entity-alias-title": "Nav iespējams dzēst vienību segvārdus", + "unable-delete-entity-alias-text": "Vienību segvārdi '{{entityAlias}}' nevar tikt dzēsti, jo tos izmanto sekojošie logrīki:
    {{widgetsList}}", + "duplicate-alias-error": "Dublikāti segvārdi atrasti '{{alias}}'.
    Vienību segvārdiem ir jābūt unikāliem paneļos.", + "missing-entity-filter-error": "Filtrs trūkst priekš segvārda '{{alias}}'.", + "configure-alias": "Konfigurēt '{{alias}}' segvārdus", + "alias": "Segvārds", + "alias-required": "Vienību segvārds ir nepieciešams.", + "remove-alias": "Noņemt vienību segvārdu", + "add-alias": "Pievienot vienību segvārdu", + "entity-list": "Vienību saraksts", + "entity-type": "Vienības tips", + "entity-types": "Vienības tipi", + "entity-type-list": "Vienības tipu saraksts", + "any-entity": "Jebkura vienība", + "enter-entity-type": "Ievadīt vienības tipu", + "no-entities-matching": "Nav vienības saderības '{{entity}}' atrastas.", + "no-entity-types-matching": "Nav vienības tipu saderības '{{entityType}}' atrastas.", + "name-starts-with": "Nosaukums sākas ar", + "use-entity-name-filter": "Lietot filtru", + "entity-list-empty": "Nav vienības atlasītas.", + "entity-type-list-empty": "Nav vienības tipi atlasīti.", + "entity-name-filter-required": "Vienību nosaukuma filtri ir vajadzīgi.", + "entity-name-filter-no-entity-matched": "Nav vienības kas sākas ar '{{entity}}' atrastas.", + "all-subtypes": "Visi", + "select-entities": "Atlasīt vienības", + "no-aliases-found": "Nav segvārdi atrasti.", + "no-alias-matching": "'{{alias}}' nav atrasts.", + "create-new-alias": "Radīt jaunu!", + "key": "Atslēga", + "key-name": "Atslēgas nosaukums", + "no-keys-found": "Nav atslēgas atrastas.", + "no-key-matching": "'{{key}}' nav atrasta.", + "create-new-key": "Radīt jaunu!", + "type": "Tips", + "type-required": "Vienības tips ir nepieciešams.", + "type-device": "Iekārta", + "type-devices": "Iekārtas", + "list-of-devices": "{ count, plural, 1 {One device} other {List of # iekārtas} }", + "device-name-starts-with": "Iekārtas, kuras nosaukumi sākas ar '{{prefix}}'", + "type-asset": "Aktīvs", + "type-assets": "Aktīvi", + "list-of-assets": "{ count, plural, 1 {One asset} other {List of # aktīvi} }", + "asset-name-starts-with": "Aktīvi, kuru nosaukumi sākas ar '{{prefix}}'", + "type-entity-view": "Vienības skats View", + "type-entity-views": "Vienības skati", + "list-of-entity-views": "{ count, plural, 1 {One entity view} other {List of # vienību skati} }", + "entity-view-name-starts-with": "Vienibas skati, kuru nosaukumi sākas ar '{{prefix}}'", + "type-rule": "Noteikums", + "type-rules": "Noteikumi", + "list-of-rules": "{ count, plural, 1 {One rule} other {List of # noteikumi} }", + "rule-name-starts-with": "Noteikumi, kuru nosaukumi sākas ar '{{prefix}}'", + "type-plugin": "Spraudnis", + "type-plugins": "Spraudņi", + "list-of-plugins": "{ count, plural, 1 {One plugin} other {List of # spraudņi} }", + "plugin-name-starts-with": "Spraudņi, kuru vārds sākas ar '{{prefix}}'", + "type-tenant": "Īrnieks", + "type-tenants": "Īrnieki", + "list-of-tenants": "{ count, plural, 1 {One tenant} other {List of # īrnieki} }", + "tenant-name-starts-with": "Īrnieki, kuru nosaukumi sākas ar '{{prefix}}'", + "type-customer": "Klients", + "type-customers": "Klienti", + "list-of-customers": "{ count, plural, 1 {One customer} other {List of # klienti} }", + "customer-name-starts-with": "Klienti, kuru nosaukumi sākas ar '{{prefix}}'", + "type-user": "Lietotājs", + "type-users": "Lietotāji", + "list-of-users": "{ count, plural, 1 {One user} other {List of # lietotāji} }", + "user-name-starts-with": "Lietotāji, kuru nosaukums sākas ar '{{prefix}}'", + "type-dashboard": "Panelis", + "type-dashboards": "Paneļi", + "list-of-dashboards": "{ count, plural, 1 {One dashboard} other {List of # paneļi} }", + "dashboard-name-starts-with": "Paneļi, kuru nosaukums sākas ar '{{prefix}}'", + "type-alarm": "Trauksme", + "type-alarms": "Trauksmes", + "list-of-alarms": "{ count, plural, 1 {One alarms} other {List of # trauksmes} }", + "alarm-name-starts-with": "Trauksmes, kuru nosaukumi sākas ar '{{prefix}}'", + "type-rulechain": "Noteikumu ķēde", + "type-rulechains": "Noteikumu ķēdes", + "list-of-rulechains": "{ count, plural, 1 {One rule chain} other {List of # noteikumu ķēdes} }", + "rulechain-name-starts-with": "Noteikumu ķēdes, kuru nosaukumi sākas ar '{{prefix}}'", + "type-rulenode": "Noteikumu node", + "type-rulenodes": "Noteikumu nodes", + "list-of-rulenodes": "{ count, plural, 1 {One rule node} other {List of # noteikumu nodes} }", + "rulenode-name-starts-with": "Noteikumu nodes, juru nosaukumi sākas ar '{{prefix}}'", + "type-current-customer": "Pašreizējais klients", + "search": "Meklēšanas vienības", + "selected-entities": "{ count, plural, 1 {1 entity} other {# vienības} } atlasītas", + "entity-name": "Vienības nosaukums", + "details": "Vienības detaļas", + "no-entities-prompt": "Nav vienības atrastas", + "no-data": "Nav datu ko attēlot", + "columns-to-display": "Kolonas ko attēlot" + }, + "entity-view": { + "entity-view": "Vienības skats", + "entity-view-required": "Vienības skats ir nepieciešams.", + "entity-views": "Vienības skati", + "management": "Vienības skatu pārvaldība", + "view-entity-views": "Skatīt vienību skatus", + "entity-view-alias": "Vienību skatu segvārdi", + "aliases": "Vienību skatu segvārdi", + "no-alias-matching": "'{{alias}}' nav atrasts.", + "no-aliases-found": "Nav segvārds atrasts.", + "no-key-matching": "'{{key}}' nav atrasts.", + "no-keys-found": "Nav atslēgas atrastas.", + "create-new-alias": "Radīt jaunu!", + "create-new-key": "Radīt jaunu!", + "duplicate-alias-error": "Dublēti segvārdi atrasti '{{alias}}'.
    Vienību skatu segvārdiem ir jābūt unikāliem paneļa ietvaros.", + "configure-alias": "Konfigurēt '{{alias}}' segvārdu", + "no-entity-views-matching": "Nav vienību skata atbilstības '{{entity}}' atrastas.", + "alias": "Segvārds", + "alias-required": "Vienību skatu segvārdi ir nepieciešami.", + "remove-alias": "Noņemt vienību skatu segvārdu", + "add-alias": "Pievienot vienību skatu segvārdu", + "name-starts-with": "Vienību skata nosaukums sākas ar", + "entity-view-list": "Vienību skata saraksts", + "use-entity-view-name-filter": "Lietot filtru", + "entity-view-list-empty": "Nav vienību skati atlasīti.", + "entity-view-name-filter-required": "Vienību skatu nosaukumu filtri ir nepieciešami.", + "entity-view-name-filter-no-entity-view-matched": "Nav vienību skati kas sākas ar '{{entityView}}' atrasti.", + "add": "Pievienot vienību skatu", + "assign-to-customer": "Pieškirt klientam", + "assign-entity-view-to-customer": "Piešķirt vienību skatus klientam", + "assign-entity-view-to-customer-text": "Lūdzu izvēlēties vienību skatus ko pieškirt klientam", + "no-entity-views-text": "Nav vienību skati atrasti", + "assign-to-customer-text": "Lūdzu izvēlēties klientu lai pieškirtu vienības skatus", + "entity-view-details": "Vienību skata detaļas", + "add-entity-view-text": "Pievienot jaunu vienību skatu", + "delete": "Dzēsts vienību skatu", + "assign-entity-views": "Piešķirt vienību skatus", + "assign-entity-views-text": "Piešķirt { count, plural, 1 {1 entityView} other {# vienību skati} } klientam", + "delete-entity-views": "Dzēst vienību skatus", + "unassign-from-customer": "Noņemt no klienta", + "unassign-entity-views": "Noņemt vienību skatus", + "unassign-entity-views-action-title": "Noņemt { count, plural, 1 {1 entityView} other {# vienību skati} } no klienta", + "assign-new-entity-view": "Piešķirt jaunu vienību skatu", + "delete-entity-view-title": "Vai esat pārliecināts,ka vēlaties dzēst vienību skatu '{{entityViewName}}'?", + "delete-entity-view-text": "Esiet uzmanīgs, pēc apstiprinājuma vienību skats un tā sasitītie dati nebūs atjaunojami.", + "delete-entity-views-title": "Vai esat pārliecināts, ka vēlaties vienību skatu { count, plural, 1 {1 entityView} other {# vienību skati} }?", + "delete-entity-views-action-title": "Dzēst { count, plural, 1 {1 entityView} other {# vienību skati} }", + "delete-entity-views-text": "Esiet uzmanīgs, pēc apstiprinājuma visir atlasītie vienības skati tiks noņemti un to saistītie dati nebūs atjaunojami.", + "unassign-entity-view-title": "Vai esat pārliecināts, ka vēlaties atspējot vienību skatu '{{entityViewName}}'?", + "unassign-entity-view-text": "Pēc apstiprinājuma vienību skats tiks atspējots un nebūs pieejams klientam.", + "unassign-entity-view": "Atspējot vienību skatu", + "unassign-entity-views-title": "Vai esat pārliecināts, ka vēlaties atspējot { count, plural, 1 {1 entityView} other {# vienību skatus} }?", + "unassign-entity-views-text": "Pēc apstiprinājuma visi atlasītie vienību skati būs atspējoti un nebūs pieejami klientiem.", + "entity-view-type": "Vienību skata tips", + "entity-view-type-required": "Vienību skata tips ir nepieciešams.", + "select-entity-view-type": "Atlasīt vienību skata tipu", + "enter-entity-view-type": "Ievadīt vienību skata tipu", + "any-entity-view": "Jebkurš vienību skats", + "no-entity-view-types-matching": "Nav vienību skata atbilstības '{{entitySubtype}}' atrastas.", + "entity-view-type-list-empty": "Nav vienību skatu tipi atlasīti.", + "entity-view-types": "Vienību skatu tipi", + "name": "Nosaukums", + "name-required": "Nosaukums ir nepieciešams.", + "description": "Apraksts", + "events": "Notikumi", + "details": "Detaļas", + "copyId": "Kopēt vienību skata Id", + "assignedToCustomer": "Piešķirta klientam", + "unable-entity-view-device-alias-title": "Nav iespējas dzēst vienību skata segvārdu", + "unable-entity-view-device-alias-text": "Iekārtas segvārds '{{entityViewAlias}}' nevar tikt dzēsts, jo to izmanto sekojošs logrīks:
    {{widgetsList}}", + "select-entity-view": "Atlasīt vienību skatu", + "make-public": "Veidot vienību skatu publisku", + "make-private": "Veidot vienību skatu privātu", + "start-date": "Starta datums", + "start-ts": "Starta laiks", + "end-date": "Beigu datums", + "end-ts": "Beigu laiks", + "date-limits": "Datuma limits", + "client-attributes": "Klienta atribūti", + "shared-attributes": "Dalītie atribūti", + "server-attributes": "Servera atribūti", + "timeseries": "Laika sērijas", + "client-attributes-placeholder": "Klienta atribūti", + "shared-attributes-placeholder": "Dalītie atribūti", + "server-attributes-placeholder": "Servera atribūti", + "timeseries-placeholder": "Laika sērijas", + "target-entity": "Mērķa vienība", + "attributes-propagation": "Atribūtu izplatīšana", + "attributes-propagation-hint": "Vienību skats automātiski kopē specificētos atribūtus no mērķa vienības katru reizi kad jūs saglabājat vai atjaunojat vienību skatu.", + "timeseries-data": "Laika sērijas dati", + "timeseries-data-hint": "Configure timeseries data keys of the target entity that will be accessible to the entity view. This timeseries data is read-only.", + "make-public-entity-view-title": "Vai esat pārliecināts, ka vēlaties veidot vienību skatu '{{entityViewName}}' publisku?", + "make-public-entity-view-text": "Pēc apstiprinājuma vienību skats un tā saistītie dati būs publiski un pieejami citiem.", + "make-private-entity-view-title": "Vai esat pārliecināts, ka vēlaties veidot vienību skatu '{{entityViewName}}' privātu?", + "make-private-entity-view-text": "Pēc apstiprinājuma vienību skats un tā saistītie dati būs privāti un nebūs pieejami citiem." + }, + "event": { + "event-type": "Notikuma tips", + "type-error": "Kļūda", + "type-lc-event": "Dzīves cikla notikums", + "type-stats": "Statistika", + "type-debug-rule-node": "Atkļūdot", + "type-debug-rule-chain": "Atkļūdot", + "no-events-prompt": "Nav notikumi atrasti", + "error": "Kļūda", + "alarm": "Trauksme", + "event-time": "Notikuma laiks", + "server": "Serveris", + "body": "Galvenā daļa", + "method": "Metode", + "type": "Tips", + "entity": "Vienība", + "message-id": "Ziņojuma Id", + "message-type": "Ziņojuma tips", + "data-type": "Datu tips", + "relation-type": "Attiecību tips", + "metadata": "Metadata", + "data": "Dati", + "event": "Notikumi", + "status": "Statuss", + "success": "Sekmīgi", + "failed": "Kļūda", + "messages-processed": "Ziņojumi apstrādāti", + "errors-occurred": "Kļūdas konstatētas" + }, + "extension": { + "extensions": "Paplašinājumi", + "selected-extensions": "{ count, plural, 1 {1 extension} other {# paplašinājumi} } atlasītie", + "type": "Tips", + "key": "Atslēga", + "value": "Vērtība", + "id": "Id", + "extension-id": "Paplašinājuma id", + "extension-type": "Paplašinājuma tips", + "transformer-json": "JSON *", + "unique-id-required": "Patreizējā paplašinājuma id jau eksistē.", + "delete": "Dzēst paplašinājumu", + "add": "Pievienot paplašinājumu", + "edit": "Rediģēt paplašinājumu", + "delete-extension-title": "Vai esat pārliecināts, ka vēlaties dzēst paplašinājumu '{{extensionId}}'?", + "delete-extension-text": "Esiet uzmanīgs, pēc apstiprinājuma paplašinājums un visi tā saistītie dati nebūs atjaunojami.", + "delete-extensions-title": "Vai esat pārliecināts, ka vēlaties dzēst { count, plural, 1 {1 extension} other {# paplašinājumus} }?", + "delete-extensions-text": "Esiet uzmanīgs, pēc apstiprinājuma visi atlasītie paplašinājumi tiks dzēsti.", + "converters": "Pārveidotāji", + "converter-id": "Pārveidotāja id", + "configuration": "Konfigurācija", + "converter-configurations": "Pārveidotāja konfigurācija", + "token": "Drošības tokens", + "add-converter": "Pievienot pārveidotāju", + "add-config": "Pievienot pārveidotāja konfigurāciju", + "device-name-expression": "Iekārtas nosaukuma izteiksme", + "device-type-expression": "Iekārtas tipa izteiksme", + "custom": "Pielāgot", + "to-double": "Dubutot", + "transformer": "Pārveidotājs", + "json-required": "json pārveidotājs ir nepieciešams.", + "json-parse": "Nav iespējams parsēt json pārveidotāju.", + "attributes": "Atribūti", + "add-attribute": "Pievienot atribūtus", + "add-map": "Pievienot kartēšanas elementu", + "timeseries": "Laika sērijas", + "add-timeseries": "Pievienot laika sērijas", + "field-required": "Lauks ir nepieciešams", + "brokers": "Brokeris", + "add-broker": "Pievienot brokeri", + "host": "Saimnieks", + "port": "Ports", + "port-range": "Portam jābūt robežās no 1 līdz 65535.", + "ssl": "Ssl", + "credentials": "Akreditācijas dati", + "username": "Lietotājvārds", + "password": "Parole", + "retry-interval": "Mēģināt vēlreiz intervāls milisekundēs", + "anonymous": "Anonīmi", + "basic": "Pamata", + "pem": "PEM", + "ca-cert": "CA sertifikācijas fails *", + "private-key": "Privātās atslēgas fails *", + "cert": "Srtifikāta fails *", + "no-file": "Nav fails izvēlēts.", + "drop-file": "Nosviest failu vai klikšķināt izvēlēto failu augšupielādei.", + "mapping": "Kartēšana", + "topic-filter": "Temata filtrs", + "converter-type": "Pārveidotāja tips", + "converter-json": "Json", + "json-name-expression": "Iekārtas nosaukuma json izteiksme", + "topic-name-expression": "Iekārtas nosaukuma temata izteiksme", + "json-type-expression": "Iekārtas tipa json izteiksme", + "topic-type-expression": "Iekārtas tipa temata izteiksme", + "attribute-key-expression": "Atribūtu atslēgas izteiksme", + "attr-json-key-expression": "Atribūtu atslēgas json izteiksme", + "attr-topic-key-expression": "Attribūtu atslēgas temata izteiksme", + "request-id-expression": "Pieprasīt id izteiksmi", + "request-id-json-expression": "Pieprasīt id json izteiksmi", + "request-id-topic-expression": "Pieprasīt id temata izteiksmi", + "response-topic-expression": "Atbildēt temata izteiksmi", + "value-expression": "Vērtības izteiksme", + "topic": "Temats", + "timeout": "Pārtraukums milisekundēs", + "converter-json-required": "json pārveidotājs ir nepieciešams.", + "converter-json-parse": "Nav iespējams parsēt pārveidotāju json.", + "filter-expression": "Filtra izteiksme", + "connect-requests": "Savienot pieprasījumus", + "add-connect-request": "Pievienot savienojuma pieprasījumus", + "disconnect-requests": "Atvienot pieprasījumus", + "add-disconnect-request": "Pievienot atvienot pieprasījumus", + "attribute-requests": "Attribūtu pieprasījumus", + "add-attribute-request": "Pievienot atribūtu pieprasījumu", + "attribute-updates": "Atribūtu atjaunojumi", + "add-attribute-update": "Pievienot atribūtu atjaunojumus", + "server-side-rpc": "Servera puses RPC", + "add-server-side-rpc-request": "Pievienot servera puses RPC pieprasījumus", + "device-name-filter": "Iekārtas nosaukuma filtrs", + "attribute-filter": "Atribūtu filtrs", + "method-filter": "Metodes filtrs", + "request-topic-expression": "Pieprasīt temata izteiksmi", + "response-timeout": "Atbildes pārtraukums milisekundēs", + "topic-expression": "Temata izteiksme", + "client-scope": "Klienta darbības joma", + "add-device": "Pievienot iekārtu", + "opc-server": "Serveris", + "opc-add-server": "Pievienot serveri", + "opc-add-server-prompt": "Lūdzu pievienot serveri", + "opc-application-name": "Aplikācijas nosaukums", + "opc-application-uri": "Aplikācijas uri", + "opc-scan-period-in-seconds": "Skanēt periodu sekundēs", + "opc-security": "Drošība", + "opc-identity": "Identitāte", + "opc-keystore": "Atslēgu veikals", + "opc-type": "Tips", + "opc-keystore-type": "Tips", + "opc-keystore-location": "Vieta *", + "opc-keystore-password": "Parole", + "opc-keystore-alias": "Segvārds", + "opc-keystore-key-password": "Atslēgas parole", + "opc-device-node-pattern": "Iekārtas nodes veids", + "opc-device-name-pattern": "Iekārtas nosaukuma veids", + "modbus-server": "Serveris/vergs", + "modbus-add-server": "Pievienot serveri/vergi", + "modbus-add-server-prompt": "Lūdzu pievienot serveri/vergu", + "modbus-transport": "Transports", + "modbus-tcp-reconnect": "Automātiski atkārtoti savienot", + "modbus-rtu-over-tcp": "RTU pa TCP", + "modbus-port-name": "Seriālā porta nosaukums", + "modbus-encoding": "Kodēšana", + "modbus-parity": "Paritāte", + "modbus-baudrate": "Pārraides ātrums", + "modbus-databits": "Datu bits", + "modbus-stopbits": "Stop bits", + "modbus-databits-range": "Datu bitiem jābūt no 7 līdz 8.", + "modbus-stopbits-range": "Stop bitiem jābūt no 1 līdz 2.", + "modbus-unit-id": "Iekārtas ID", + "modbus-unit-id-range": "Iekārtas ID jābūt no 1 līdz 247.", + "modbus-device-name": "Iekārtas nosaukums", + "modbus-poll-period": "Aptaujas periods (ms)", + "modbus-attributes-poll-period": "Atribūtu aptaujas periods (ms)", + "modbus-timeseries-poll-period": "Laika sērijas aptaujas periods (ms)", + "modbus-poll-period-range": "Aptaujas periodam jābūt pzitīvai vērtībai.", + "modbus-tag": "Etiķete", + "modbus-function": "Funkcija", + "modbus-register-address": "Reģistra adrese", + "modbus-register-address-range": "Reģistra adresei jābūt no 0 līdz 65535.", + "modbus-register-bit-index": "Bita indekss", + "modbus-register-bit-index-range": "Bita indeksam jābūt no 0 līdz 15.", + "modbus-register-count": "Reģistra skaitītājs", + "modbus-register-count-range": "Reģistra skaitītājam jābūt pzitīvai vērtībai.", + "modbus-byte-order": "Baitu kārtība", + "sync": { + "status": "Statuss", + "sync": "Sync", + "not-sync": "Nav sync", + "last-sync-time": "Pēdējais sync laiks", + "not-available": "Nav pieejams" + }, + "export-extensions-configuration": "Eksportēt paplašinājuma konfigurāciju", + "import-extensions-configuration": "Importēt paplašinājuma konfigurāciju", + "import-extensions": "Importēt paplašinājumus", + "import-extension": "Importēt paplašinājumu", + "export-extension": "Eksportēt paplašinājumu", + "file": "Paplašinājuma fails", + "invalid-file-error": "Invalīds paplašinājuma fails" + }, + "fullscreen": { + "expand": "Paplašināt uz pilnu ekrānu", + "exit": "Iziet no pilna ekrāna", + "toggle": "Pārslēgties uz pilna ekrāna režīmu", + "fullscreen": "Pilns ekrāns" + }, + "function": { + "function": "Funkcija" + }, + "grid": { + "delete-item-title": "Vai esat pārliecināts, ka vēlaties dzēst šo priekšmetu?", + "delete-item-text": "Esiet uzmanīgs, pēc apstiprinājuma šī priekšmeta dati nebūs atjaunojami.", + "delete-items-title": "Vai esat pārliecināts, ka vēlaties dzēst { count, plural, 1 {1 item} other {# priekšmetus} }?", + "delete-items-action-title": "Dzēst { count, plural, 1 {1 item} other {# priekšmeti} }", + "delete-items-text": "Esiet uzmanīgs, pēc apstiprinājuma visi atlasītie priekšmeti tiks noņemti un to saistītie dati nebūs atjaunojami.", + "add-item-text": "Pievienot jaunu priekšmetu", + "no-items-text": "Nav priekšmeti atrasti", + "item-details": "Priekšmetu detaļas", + "delete-item": "Dzēst priekšmetu", + "delete-items": "Dzēst priekšmetus", + "scroll-to-top": "Iet uz sākumu" + }, + "help": { + "goto-help-page": "Ejiet uz palīdzības lapu" + }, + "home": { + "home": "Sākums", + "profile": "Profils", + "logout": "Izlogoties", + "menu": "Izvēlne", + "avatar": "Avatars", + "open-user-menu": "Atvērt lietotāja izvēlni" + }, + "import": { + "no-file": "Nav fails izvēlēts", + "drop-file": "Nosviest JSON failu vai klikšķināt uz atlasīto failu augšupielādei.", + "drop-file-csv": "Nosviest CSV failu vai klikšķināt uz atlasīto failu augšupielādei.", + "column-value": "Vērtība", + "column-title": "Virsraksts", + "column-example": "Piemēra vērtību dati", + "column-key": "Atribūts/telemetrijas atslēga key", + "csv-delimiter": "CSV kolonu atdalītājs", + "csv-first-line-header": "Pirmā līnija satur kolonu nosaukumus", + "csv-update-data": "Atjaunot atribūtu/telemetrija", + "import-csv-number-columns-error": "Failā jābūt vismaz divām kolonām", + "import-csv-invalid-format-error": "Invalīds faila formāts. Līnija: '{{line}}'", + "column-type": { + "name": "Nosaukums", + "type": "Tips", + "column-type": "Kolonas tips", + "client-attribute": "Klienta atribūts", + "shared-attribute": "Dalītais atribūts", + "server-attribute": "Servera atribūts", + "timeseries": "Laika sērijas", + "entity-field": "Vienības lauks", + "access-token": "Piekļuves tokens" + }, + "stepper-text": { + "select-file": "Atlasīt failu", + "configuration": "Importēt konfigurāciju", + "column-type": "Atlasīt kolonas tipu", + "creat-entities": "Radīt jaunas vienības", + "done": "Darīts" + }, + "message": { + "create-entities": "{{count}} jaunas vienības sekmīgi radītas.", + "update-entities": "{{count}} vienības sekmīgi atjaunotas.", + "error-entities": "Te ir kļūda radot {{count}} vienības." + } + }, + "item": { + "selected": "Atlasīts" + }, + "js-func": { + "no-return-error": "Funkcijai vajag atgriezt rezultātu!", + "return-type-mismatch": "Funkcijai vajag atgriezt rezultātu '{{type}}' !", + "tidy": "Sakopt" + }, + "key-val": { + "key": "Atslēga", + "value": "Vērtība", + "remove-entry": "Noņemt ierakstu", + "add-entry": "Pievienot ierakstu", + "no-data": "Nav ierakstu" + }, + "layout": { + "layout": "Izkārtojums", + "manage": "Pārvaldīt izkārtojumu", + "settings": "Izkārtojuma iestatījumi", + "color": "Krāsa", + "main": "Galvenais", + "right": "Pa labi", + "select": "Atlasīt mērķa izkārtojumu" + }, + "legend": { + "direction": "Leģendas virziens", + "position": "Leģendas pozīcija", + "show-max": "Rādīt max vērtību", + "show-min": "Rādīt min vērtību", + "show-avg": "Rādīt vidējo vērtību", + "show-total": "Rādīt kopējo vērtību", + "settings": "Leģendas iestatījumi", + "min": "min", + "max": "max", + "avg": "vidējais", + "total": "total" + }, + "login": { + "login": "Login", + "request-password-reset": "Pieprasīt atiestatīt paroli", + "reset-password": "Atiestatīt paroli", + "create-password": "Radīt paroli", + "passwords-mismatch-error": "Ievadītajai parolei ir jāsakrīt!", + "password-again": "Atkārtot paroli", + "sign-in": "Lūdzu pierakstīties", + "username": "Lietotājvārds (email)", + "remember-me": "Atcerēties mani", + "forgot-password": "Aizmirsu paroli?", + "password-reset": "Paroli atiestatīt", + "new-password": "Jaunā parole", + "new-password-again": "Atkārtot jauno paroli", + "password-link-sent-message": "paroles atiestatīšanas saite sekmīgi nosūtīta!", + "email": "Email" + }, + "position": { + "top": "Sākums", + "bottom": "Beigas", + "left": "Pa kreisi", + "right": "Pa labi" + }, + "profile": { + "profile": "Profils", + "change-password": "Mainīt paroli", + "current-password": "Patreizējā parole" + }, + "relation": { + "relations": "Attiecības", + "direction": "Virziens", + "search-direction": { + "FROM": "No", + "TO": "Uz" + }, + "direction-type": { + "FROM": "No", + "TO": "Uz" + }, + "from-relations": "Izejošāsa attiecības", + "to-relations": "Ienākošās attiecības", + "selected-relations": "{ count, plural, 1 {1 relation} other {# attiecības} } atlasītas", + "type": "Tips", + "to-entity-type": "Uz vienību tipu", + "to-entity-name": "Uz vienību nosaukumu", + "from-entity-type": "No vienību tipa", + "from-entity-name": "No vienību nosaukuma", + "to-entity": "Uz vienību", + "from-entity": "No vienības", + "delete": "Dzēst attiecību", + "relation-type": "Attiecības tips", + "relation-type-required": "Attiecību tips ir nepieciešams.", + "any-relation-type": "Jebkura tipa", + "add": "Pievienot attiecību", + "edit": "Rediģēt attiecību", + "delete-to-relation-title": "Vai esat pārliecināts, ka vēlaties dzēst attiecību uz vienību '{{entityName}}'?", + "delete-to-relation-text": "Esiet uzmanīgs, pēc apstiprinājuma vienība '{{entityName}}' būs atsaistīta no patreizējās vienības.", + "delete-to-relations-title": "Vai esat pārliecināts, ka vēlaties dzēst { count, plural, 1 {1 relation} other {# attiecības} }?", + "delete-to-relations-text": "Esiet uzmanīgs, pēc apstiprināšanas visas atlasītās attiecības būs noņemtas un attiecīgās vienības būs atsaistītas no patreizējās vienības.", + "delete-from-relation-title": "Vai esat pārliecināts, ka vēlaties dzēst attiecību no vienības '{{entityName}}'?", + "delete-from-relation-text": "Esiet uzmanīgs, pēc apstiprinājuma patreizējā vienība būs atsaistīta no vienības '{{entityName}}'.", + "delete-from-relations-title": "Vai esat pārliecināts, ka vēlaties dzēst { count, plural, 1 {1 relation} other {# attiecības} }?", + "delete-from-relations-text": "Esiet uzmanīgs, pēc apstiprinājuma visas atlasītās attiecības būs noņemtas un patreizējā vienība tiks atsaistīta no attiecīgās vienības.", + "remove-relation-filter": "Noņemt attiecību filtru", + "add-relation-filter": "Pievienot attiecību filtru", + "any-relation": "Jebkura attiecība", + "relation-filters": "Attiecību filtrs", + "additional-info": "Papildus info (JSON)", + "invalid-additional-info": "Nav iespēja parsēt papildus info json." + }, + "rulechain": { + "rulechain": "Noteikumu ķēde", + "rulechains": "Noteikumu ķēdes", + "root": "Sakne", + "delete": "Dzēsts noteikumu ķēdi", + "name": "Nosaukums", + "name-required": "Nosaukums ir nepieciešams.", + "description": "Nosaukums ir nepieciešams", + "add": "Pievienot noteikumu ķēdi", + "set-root": "Veidot noteikumu ķēdi kā sakni", + "set-root-rulechain-title": "Vai esat pārliecināts, ka vēlaties veidot noteikumu ķēdi '{{ruleChainName}}' kā sakni?", + "set-root-rulechain-text": "Pēc apstiprinājuma noteikuma ķēde tiks veidota kā sakne un apstrādās visu ienākošo informāciju.", + "delete-rulechain-title": "Vai esat pārliecināts, ka vēlaties dzēst noteikumu ķēdi '{{ruleChainName}}'?", + "delete-rulechain-text": "Esiet uzmanīgs, pēc apstiprinājuma noteikumu ķēde un saistītā informācija nebūs atjaunojama.", + "delete-rulechains-title": "Vai esat pārliecināts, ka vēlaties dzēst { count, plural, 1 {1 rule chain} other {# noteikumu ķēdes} }?", + "delete-rulechains-action-title": "Dzēst { count, plural, 1 {1 rule chain} other {# noteikumu ķēdes} }", + "delete-rulechains-text": "Esiet uzmanīgs, pēc apstiprinājuma visas atlasītās noteikumu ķēdes tiks noņemtas un to saistītos datus nevarēs atjaunot.", + "add-rulechain-text": "Pievienot jaunu noteikumu ķēdi", + "no-rulechains-text": "Nav noteikumu ķēdes atrastas", + "rulechain-details": "Noteikumu ķēdes detaļas", + "details": "Detaļas", + "events": "Notikumi", + "system": "Sistēma", + "import": "Importēt noteikumu ķēdi", + "export": "Eksportēt noteikumu ķēdi", + "export-failed-error": "Nav iespējams eksportēt noteikumu ķēdi: {{error}}", + "create-new-rulechain": "Radīt jaunu noteikumu ķēdi", + "rulechain-file": "Noteikumu ķēdes fails", + "invalid-rulechain-file-error": "Nav iespējams importēt noteikumu ķēdi: Invalīda noteikumu ķēdes datu struktūra.", + "copyId": "Kopēt noteikumu ķēdes Id", + "idCopiedMessage": "Noteikumu ķēdes Id ir kopēta uz starpliktuves", + "select-rulechain": "Atlasīt noteikumu ķēdi", + "no-rulechains-matching": "Nav noteikumu ķēdes atbilstības '{{entity}}' atrastas.", + "rulechain-required": "Noteikumu ķēde ir nepieciešama", + "management": "Noteikumu pārvaldība", + "debug-mode": "Atkļūdošanas režīms" + }, + "rulenode": { + "details": "Detaļas", + "events": "Notikumi", + "search": "Meklēt nodes", + "open-node-library": "Atvērt node bibliotēku", + "add": "Pievienot noteikumu nodi", + "name": "Nosaukums", + "name-required": "Nosaukums ir nepieciešams.", + "type": "Tips", + "description": "Apraksts", + "delete": "Dzēst noteikumu nodi", + "select-all-objects": "Atlasīt visas nodes un savienojumus", + "deselect-all-objects": "Noņemt visas nodes un savienojumus", + "delete-selected-objects": "Dzēst atlasītās nodes un savienojumus", + "delete-selected": "Dzēst atlasītos", + "select-all": "Atlasīt visu", + "copy-selected": "Kopēt atlasīto", + "deselect-all": "Noņemt visu", + "rulenode-details": "Noteikumu nodes detaļas", + "debug-mode": "Atkļūdošanas mode", + "configuration": "Konfigurācija", + "link": "Saite", + "link-details": "Noteikumu nodes saites detaļas", + "add-link": "Pievienot saiti", + "link-label": "Saites etiķete", + "link-label-required": "Saites etiķete ir nepieciešama.", + "custom-link-label": "Klienta saites etiķete", + "custom-link-label-required": "Klienta saites etiķete ir nepieciešama.", + "link-labels": "Saites etiķetes", + "link-labels-required": "Saites etiķetes ir nepieciešamas.", + "no-link-labels-found":"Nav saites etiķetes atrastas", + "no-link-label-matching": "'{{label}}' nav atrasta.", + "create-new-link-label": "Radīt jaunu!", + "type-filter": "Filtrs", + "type-filter-details": "Filtrēt ienākošos ziņojumus ar konfigurētajiem stāvokļiem", + "type-enrichment": "Bagātināšana", + "type-enrichment-details": "Pievieno papildus informāciju ziņas metadatiem", + "type-transformation": "Transformācija", + "type-transformation-details": "Mainīt ziņas datu lauku un metatdatus", + "type-action": "Aktivitāte", + "type-action-details": "Veikt specifisku aktivitāti", + "type-external": "Ārējs", + "type-external-details": "Sadarboties ar ārējām sistēmām", + "type-rule-chain": "Noteikumu ķēde", + "type-rule-chain-details": "Pārsūta ienākošo ziņu uz specifisku noteikumu ķēdi ", + "type-input": "Ievads", + "type-input-details": "Loģiskais ievads noteikumu ķēdei, pārsūta ienākošās ziņas uz nākamo attiecināto noteikumu nodi", + "type-unknown": "Nezināms", + "type-unknown-details": "Neatrisināta noteikumu node", + "directive-is-not-loaded": "Noteikta konfigurācijas direktīva '{{directiveName}}' nav pieejama.", + "ui-resources-load-error": "Nesekmīgs mēģinājums ielādēt konfigurācijas ui resursus.", + "invalid-target-rulechain": "Nav iespējams atrisināt mērķa noteikumu ķēdi!", + "test-script-function": "Testēt skripta funkciju", + "message": "Ziņa", + "message-type": "Ziņas tips", + "select-message-type": "Atlasīt ziņas tipu", + "message-type-required": "Ziņas tips ir nepieciešams", + "metadata": "Metadati", + "metadata-required": "Metadatu ievadi nevar būt tukši.", + "output": "Izeja", + "test": "Tests", + "help": "Palīdzība", + "reset-debug-mode": "Atiestatīt atkļūdošanu visās nodēs" + }, + "tenant": { + "tenant": "Īrnieks", + "tenants": "Īrnieki", + "management": "Īrnieku pārvaldība", + "add": "Pievienot īrnieku", + "admins": "Administrātori", + "manage-tenant-admins": "Pārvaldīt īrnieku administrātorus", + "delete": "Dzēst īrnieku", + "add-tenant-text": "Pievienot jaunu īrnieku", + "no-tenants-text": "Nav īrnieki atrasti", + "tenant-details": "Īrnieka detaļas", + "delete-tenant-title": "Vai esat pārliecināts, ka vēlaties dzēst īrnieku '{{tenantTitle}}'?", + "delete-tenant-text": "Esiet uzmanīgs, pēc apstiprinājuma īrnieks un tā saistītie dati nebūs atjaunojami.", + "delete-tenants-title": "Vai esat pārliecināts, ka vēlaties dzēst { count, plural, 1 {1 tenant} other {# īrniekus} }?", + "delete-tenants-action-title": "Dzēst { count, plural, 1 {1 tenant} other {# īrniekus} }", + "delete-tenants-text": "Esiet uzmanīgs, pēc apstiprinājuma visi atlasītie īrnieki tiks noņemti un saistītie dati nebūs atjaunojami.", + "title": "Virsraksts", + "title-required": "Virsraksts ir nepieciešams.", + "description": "Apraksts", + "details": "Detaļas", + "events": "Notikumi", + "copyId": "Kopēt īrnieka Id", + "idCopiedMessage": "Īrnieka Id ir kopēta uz starpliktuvi", + "select-tenant": "Atlasīt īrnieku", + "no-tenants-matching": "Nav īrnieku saderības '{{entity}}' atrastas.", + "tenant-required": "Īrnieks ir nepieciešams" + }, + "timeinterval": { + "seconds-interval": "{ seconds, plural, 1 {1 second} other {# sekundes} }", + "minutes-interval": "{ minutes, plural, 1 {1 minute} other {# minūtes} }", + "hours-interval": "{ hours, plural, 1 {1 hour} other {# stundas} }", + "days-interval": "{ days, plural, 1 {1 day} other {# dienas} }", + "days": "Dienas", + "hours": "Stundas", + "minutes": "Minūtes", + "seconds": "Sekundes", + "advanced": "Pieredzējis lietotājs" + }, + "timewindow": { + "days": "{ days, plural, 1 { day } other {# dienas } }", + "hours": "{ hours, plural, 0 { hour } 1 {1 hour } other {# stundas } }", + "minutes": "{ minutes, plural, 0 { minute } 1 {1 minute } other {# minūtes } }", + "seconds": "{ seconds, plural, 0 { second } 1 {1 second } other {# sekundes } }", + "realtime": "Reālajā laikā", + "history": "Vēsture", + "last-prefix": "Pēdējās", + "period": "No {{ startTime }} to {{ endTime }}", + "edit": "Rediģēt laika logu", + "date-range": "Datumu diapazons", + "last": "Pēdējās", + "time-period": "Laika periods" + }, + "user": { + "user": "Lietotājs", + "users": "Lietotāji", + "customer-users": "Klienta lietotāji", + "tenant-admins": "Īrnieka administrātori", + "sys-admin": "Sistēmas administrātori", + "tenant-admin": "Īrnieka administrātors", + "customer": "Klients", + "anonymous": "Anonīmi", + "add": "Pievienot lietotāju", + "delete": "Dzēst lietotāju", + "add-user-text": "Pievienot jaunu lietotāju", + "no-users-text": "Nav lietotāji atrasti", + "user-details": "Lietotāja detaļas", + "delete-user-title": "Vai esat pārliecināts, ka vēlaties dzēst lietotāju '{{userEmail}}'?", + "delete-user-text": "Esiet uzmanīgs, pēc apstiprinājuma lietotājs un tā saistītie dati nebūs atjaunojami.", + "delete-users-title": "Vai esat pārliecināts, ka vēlaties dzēst { count, plural, 1 {1 user} other {# lietotājus} }?", + "delete-users-action-title": "Dzēst { count, plural, 1 {1 user} other {# lietotājus} }", + "delete-users-text": "Esiet uzmanīgs, pēc apstiprinājuma visi atlasītie lietotāji tiks noņemti un to saistītie dati nebūs atjaunojami.", + "activation-email-sent-message": "Aktivizācijas email ir sekmīgi nosūtīts!", + "resend-activation": "Atkārtoti nosūtīt aktivizāciju", + "email": "Email", + "email-required": "Email ir nepieciešams.", + "invalid-email-format": "Invalīds email formāts.", + "first-name": "Vārds", + "last-name": "Uzvārds", + "description": "Apraksts", + "default-dashboard": "Defaultais panelis", + "always-fullscreen": "Vienmēr pilnekrāna", + "select-user": "Izvēlēties lietotāju", + "no-users-matching": "Nav lietotāju atbilstības '{{entity}}' atrastas.", + "user-required": "Lietotājs ir nepieciešams", + "activation-method": "Aktivizācijas veids", + "display-activation-link": "Parādīt aktivizācijas saiti", + "send-activation-mail": "Nosūtīt aktivizācijas email", + "activation-link": "Lietotāja aktivizācijas saite", + "activation-link-text": "Lai aktivizētu lietotāju, lieto sekojošo aktivizācijas saiti :", + "copy-activation-link": "Kopēt aktivizācijas saiti", + "activation-link-copied-message": "Lietotāja aktivizācijas saite ir kopēta uz starpliktuvi", + "details": "Detaļas", + "login-as-tenant-admin": "Login kā īrnieka administrātors", + "login-as-customer-user": "Login kā klienta lietotājs" + }, + "value": { + "type": "Vērtības tips", + "string": "Teksts", + "string-value": "Teksta informācija", + "integer": "Skaitlis", + "integer-value": "Skaitļa vērtība", + "invalid-integer-value": "Invalīda skaitļa vērtība", + "double": "Skaitlis ar cipariem aiz komata", + "double-value": "Skaitļa ar cipariem aiz komata vērtība", + "boolean": "ir/nav", + "boolean-value": "ir/nav vērtības", + "false": "Nepareizi", + "true": "Patiesi", + "long": "Ilgāk" + }, + "widget": { + "widget-library": "Logrīku bibliotēka", + "widget-bundle": "Logrīku apkopojums", + "select-widgets-bundle": "Atlasīt logrīku apkopojumu", + "management": "Logrīku pārvaldība", + "editor": "Logrīku rediģētājs", + "widget-type-not-found": "Problēma ielādēt logrīka konfigurāciju.
    Iespējams, ka attiecīgais logrīka tips ir noņemts.", + "widget-type-load-error": "Logrīks nav ielādēts dēl sekojošajām kļūdām:", + "remove": "Noņemt logrīku", + "edit": "rediģēt logrīku", + "remove-widget-title": "Vai esat pārliecināts, ka vēlaties noņemt logrīku '{{widgetTitle}}'?", + "remove-widget-text": "Pēc apstiprinājuma logrīks un tā saistītā informācija nebūs atjaunojama.", + "timeseries": "Laika sērijas", + "search-data": "Meklēt datus", + "no-data-found": "Nav datu atrasti", + "latest-values": "Pedējās vērtības", + "rpc": "Kontroles logrīks", + "alarm": "Trauksmes logrīks", + "static": "Statiskais logrīks", + "select-widget-type": "Atlasīt logrīka tipu", + "missing-widget-title-error": "Logrīka virsrakstam vajag būt norādītam!", + "widget-saved": "Logrīks saglabāts", + "unable-to-save-widget-error": "Nav iespēja saglabāt logrīku! Logrīkam ir kļūdas!", + "save": "Saglabāt logrīku", + "saveAs": "Saglabāt logrīku kā", + "save-widget-type-as": "Saglabāt logrīka tipu kā", + "save-widget-type-as-text": "Lūdzu ievadīt jaunu logrīka virsrakstu un/vai atlasīt mērķa logrīku apkopojumu", + "toggle-fullscreen": "Parslēgt pilnekrānu", + "run": "Palaist logrīku", + "title": "Logrīka virsraksts", + "title-required": "Logrīka virsraksts ir nepieciešams.", + "type": "Logrīka tips", + "resources": "Resursi", + "resource-url": "JavaScript/CSS URL", + "remove-resource": "Noņemt resursus", + "add-resource": "Pievienot resursus", + "html": "HTML", + "tidy": "Tidy", + "css": "CSS", + "settings-schema": "Iestatījumu shēma", + "datakey-settings-schema": "Datu atslēgas iestatījumu shēma", + "javascript": "Javascript", + "remove-widget-type-title": "Vai esat pārliecināts, ka vēlaties noņemt logrīka tipu '{{widgetName}}'?", + "remove-widget-type-text": "Pēc apstiprinājuma logrīka tips un tā saistītie dati nebūs atjaunojami.", + "remove-widget-type": "Noņemt logrīka tipu", + "add-widget-type": "Pievienot jaunu logrīka tipu", + "widget-type-load-failed-error": "Neveiksme ielādēt logrīka tipu!", + "widget-template-load-failed-error": "Neveiksme ielādēt logrīka paraugu!", + "add": "Pievienot logrīku", + "undo": "Atcelt logrīka izmaiņas", + "export": "Eksportēt logrīku" + }, + "widget-action": { + "header-button": "Logrīka galvenes poga", + "open-dashboard-state": "Navigēt uz jaunu paneļa stāvokli", + "update-dashboard-state": "Atjaunot patreizējo paneļa stāvokli", + "open-dashboard": "Navigēt uz citu paneli", + "custom": "Klienta aktivitāte", + "target-dashboard-state": "Mērķa paneļa stāvoklis", + "target-dashboard-state-required": "Mērķa paneļa stāvoklis ir nepieciešams", + "set-entity-from-widget": "Uzstādīt vienību no logrīka", + "target-dashboard": "Mērķa panelis", + "open-right-layout": "Atvērt pareizo paneļa izkārtojumu (mobilais skats)" + }, + "widgets-bundle": { + "current": "Patreizējais apkopojums", + "widgets-bundles": "Logrīku apkopojuma", + "add": "Pievienot logrīku apkopojumu", + "delete": "Dzēst logrīku apkopojumu", + "title": "Virsraksts", + "title-required": "Virsraksts ir nepieciešams.", + "add-widgets-bundle-text": "Pievienot jaunu logrīku apkopojumu", + "no-widgets-bundles-text": "Nav logrīku apkopojumi atrasti", + "empty": "Logrīku apkopojums ir tukšs", + "details": "Detaļas", + "widgets-bundle-details": "Logrīku apkopojumu detaļas", + "delete-widgets-bundle-title": "Vai esat pārliecināts, ka vēlaties dzēst logrīku apkopojumu '{{widgetsBundleTitle}}'?", + "delete-widgets-bundle-text": "Esiet uzmanīgs, pēc apstiprinājuma logrīka apkopojums un tā saistītie dati nebūs atjaunojami.", + "delete-widgets-bundles-title": "vai esat pārliecināts, ka vēlaties dzēst { count, plural, 1 {1 widgets bundle} other {# logrīku apkopojumus} }?", + "delete-widgets-bundles-action-title": "Dzēst { count, plural, 1 {1 widgets bundle} other {# logrīku apkopojumus} }", + "delete-widgets-bundles-text": "Esiet uzmanīgs, pēc apstiprinājuma visi atlasītie logrīku apkopojumi tiks noņemti un to saistītie dati nebūs atjaunojami.", + "no-widgets-bundles-matching": "Nav logrīku apkopojumu saderības '{{widgetsBundle}}' atrastas.", + "widgets-bundle-required": "Logrīku apkopojums ir nepieciešams.", + "system": "Sistēma", + "import": "Importēt logrīku apkopojumu", + "export": "Eksportēt logrīku apkopojumu", + "export-failed-error": "Nav iespējams eksportēt logrīku apkopojumu: {{error}}", + "create-new-widgets-bundle": "Radīt jaunu logrīku apkopojumu", + "widgets-bundle-file": "Logrīku apkopojumu fails", + "invalid-widgets-bundle-file-error": "Nav iespējams importēt logrīku apkopojumu: Invalīda logrīku apkopojuma datu struktūra." + }, + "widget-config": { + "data": "Dati", + "settings": "Iestatījumi", + "advanced": "Paaugstināta līmeņa", + "title": "Virsraksts", + "general-settings": "Pamata iestatījumi", + "display-title": "Rādīt virsrakstu", + "drop-shadow": "Nomest ēnas", + "enable-fullscreen": "Iespējot pilnekrānu", + "background-color": "Fona krāsa", + "text-color": "Teksta krāsa", + "padding": "Polsterējums", + "margin": "Robežas", + "widget-style": "Logrīka stils", + "title-style": "Virsraksta stils", + "mobile-mode-settings": "Mobilās modes iestatījumi", + "order": "Pasūtīt", + "height": "Augstums", + "units": "Papildus simbols vērtības atrādīšanai", + "decimals": "Ciparu skaits pēc komata", + "timewindow": "Laika logs", + "use-dashboard-timewindow": "Lietot paneļa laika logu", + "display-timewindow": "Rādīt laika logu", + "display-legend": "Rādīt leģendu", + "datasources": "Datu avoti", + "maximum-datasources": "Maksimums { count, plural, 1 {1 datasource is allowed.} other {# datu avoti atļautie} }", + "datasource-type": "Tips", + "datasource-parameters": "Parametri", + "remove-datasource": "Noņemt datu avotu", + "add-datasource": "Pievienot datu avotu", + "target-device": "Mērķa iekārta", + "alarm-source": "Trauksmes avots", + "actions": "Aktivitātes", + "action": "Aktivitātes", + "add-action": "Pievienot aktivitāti", + "search-actions": "Meklēt aktivitātes", + "action-source": "Aktivitāšu avots", + "action-source-required": "Aktivitāšu avoti ir nepieciešami.", + "action-name": "Nosaukums", + "action-name-required": "Aktitiāšu nosaukums ir nepieciešams.", + "action-name-not-unique": "Cita aktivitāte ar tādu pašu nosaukumu jau eksistē.
    Aktitivātes nosaukumam ir jābūt unikālam vienā aktivitātes avotā.", + "action-icon": "Ikona", + "action-type": "Tips", + "action-type-required": "Aktivitātes tips ir nepieciešams.", + "edit-action": "Rediģēt aktivitāti", + "delete-action": "Dzēst aktivitāti", + "delete-action-title": "Dzēst logrīka aktivitāti", + "delete-action-text": "Vai esat pārliecināts, ka vēlaties dzēst logrīka aktivitāti ar nosaukumu '{{actionName}}'?" + }, + "widget-type": { + "import": "Importēt logrīka tipu", + "export": "Eksportēt logrīka tipu", + "export-failed-error": "Nav iespējams eksportēt logrīka tipu: {{error}}", + "create-new-widget-type": "Radīt jaunu logrīka tipu", + "widget-type-file": "Logrīka tipa fails", + "invalid-widget-type-file-error": "Nav iespējams importēt logrīka tipu: Invalīda logrīka tipa datu struktūra." + }, + "widgets": { + "date-range-navigator": { + "localizationMap": { + "Sun": "Svētdiena", + "Mon": "Pirmdiena", + "Tue": "Otrdiena", + "Wed": "Trešdiena", + "Thu": "Ceturdiena", + "Fri": "Piekdiena", + "Sat": "Sestdiena", + "Jan": "Janvāris", + "Feb": "Februāris", + "Mar": "Marts", + "Apr": "Aprīlis", + "May": "Maijs", + "Jun": "Jūnijs", + "Jul": "Jūlijs", + "Aug": "Augusts", + "Sep": "Septembris", + "Oct": "Oktobris", + "Nov": "Novembris", + "Dec": "Decembris", + "January": "Janvāris", + "February": "Februāris", + "March": "Marts", + "April": "Aprīlis", + "June": "Jūnijs", + "July": "Jūlijs", + "August": "Augusts", + "September": "Septembris", + "October": "Oktobris", + "November": "Novembris", + "December": "Decembris", + "Custom Date Range": "Lietotāja datu diapazons", + "Date Range Template": "Datu diapazona templeits", + "Today": "Šodien", + "Yesterday": "Vakardien", + "This Week": "Šī nedēļa", + "Last Week": "Pēdējā nedēļa", + "This Month": "Šis mēnesis", + "Last Month": "Pēdējais mēnesis", + "Year": "Gads", + "This Year": "Šis gads", + "Last Year": "Pagājušais gads", + "Date picker": "Datu atlasītājs", + "Hour": "Stunda", + "Day": "Diena", + "Week": "Nedēļa", + "2 weeks": "2 Nedēļas", + "Month": "Mēnesis", + "3 months": "3 Mēneši", + "6 months": "6 Mēneši", + "Custom interval": "Klienta intervāls", + "Interval": "Intervāls", + "Step size": "Soļa lielums", + "Ok": "Ok" + } + } + }, + "icon": { + "icon": "Ikona", + "select-icon": "Atlasīt ikonas", + "material-icons": "Materiālu ikonas", + "show-all": "Rādīt visas ikonas" + }, + "custom": { + "widget-action": { + "action-cell-button": "Aktivitātes šunas poga", + "row-click": "Uz rindas klikšķis", + "polygon-click": "Uz daudzstūra klikšķis", + "marker-click": "Uz marķiera klikšķis", + "tooltip-tag-action": "Rīku padomu darbība", + "node-selected": "Uz atlasīto nodi", + "element-click": "HTML elementa klikšķis" + } + }, + "language": { + "language": "Valodas" + } +} diff --git a/ui-ngx/src/assets/locale/locale.constant-pt_BR.json b/ui-ngx/src/assets/locale/locale.constant-pt_BR.json new file mode 100644 index 0000000000..f08a38e9d9 --- /dev/null +++ b/ui-ngx/src/assets/locale/locale.constant-pt_BR.json @@ -0,0 +1,2048 @@ +{ + "access": { + "unauthorized": "Não autorizado", + "unauthorized-access": "Acesso não autorizado", + "unauthorized-access-text": "Para acessar este recurso, você precisa se inscrever!", + "access-forbidden": "Acesso proibido", + "access-forbidden-text": "Você não tem direito de acesso a esta localização!
    Se ainda deseja acessar esta localização, tente inscrever-se com um nome de usuário diferente.", + "refresh-token-expired": "A sessão expirou", + "refresh-token-failed": "Impossível atualizar a sessão", + "permission-denied": "Permissão negada", + "permission-denied-text": "Você não tem permissão para executar esta operação!" + }, + "action": { + "activate": "Ativar", + "suspend": "Suspender", + "save": "Salvar", + "saveAs": "Salvar como", + "cancel": "Cancelar", + "ok": "OK", + "delete": "Excluir", + "add": "Adicionar", + "yes": "Sim", + "no": "Não", + "update": "Atualizar", + "remove": "Remover", + "select": "Selecionar", + "search": "Pesquisar", + "clear-search": "Limpar pesquisa", + "assign": "Atribuir", + "unassign": "Cancelar atribuição", + "share": "Compartilhar", + "make-private": "Tornar privado", + "apply": "Aplicar", + "apply-changes": "Aplicar alterações", + "edit-mode": "Modo de edição", + "enter-edit-mode": "Entrar no modo de edição", + "decline-changes": "Rejeitar alterações", + "close": "Fechar", + "back": "Voltar", + "run": "Executar", + "sign-in": "Inscreva-se!", + "edit": "Editar", + "view": "Visualizar", + "create": "Criar", + "drag": "Arrastar", + "refresh": "Atualizar", + "undo": "Desfazer", + "copy": "Copiar", + "paste": "Colar", + "copy-reference": "Copiar referência", + "paste-reference": "Colar referência", + "import": "Importar", + "export": "Exportar", + "share-via": "Compartilhar via {{provider}}", + "continue": "Continuar", + "discard-changes": "Descartar alterações", + "download": "Download" + }, + "aggregation": { + "aggregation": "Agregação", + "function": "Função de agregação de dados", + "limit": "Valores máx.", + "group-interval": "Intervalo de agrupamento", + "min": "Mín.", + "max": "Máx.", + "avg": "Média", + "sum": "Soma", + "count": "Contagem", + "none": "Nenhum" + }, + "admin": { + "general": "Geral", + "general-settings": "Configuração geral", + "outgoing-mail": "Servidor de e-mail", + "outgoing-mail-settings": "Configuração do servidor de saída de e-mail", + "system-settings": "Configuração do sistema", + "test-mail-sent": "O e-mail de teste foi enviado corretamente!", + "base-url": "URL de base", + "base-url-required": "O URL de base é obrigatório.", + "mail-from": "E-mail de", + "mail-from-required": "'E-mail de' é obrigatório.", + "smtp-protocol": "Protocolo SMTP", + "smtp-host": "Host SMTP", + "smtp-host-required": "O host SMTP é obrigatório.", + "smtp-port": "Porta SMTP", + "smtp-port-required": "É necessário informar a porta SMPT.", + "smtp-port-invalid": "Esta não parece ser uma porta SMTP válida.", + "timeout-msec": "Tempo limite (ms)", + "timeout-required": "O tempo limite é obrigatório.", + "timeout-invalid": "Este não parece ser um tempo limite válido.", + "enable-tls": "Habilitar TLS", + "tls-version": "Versão de TLS", + "enable-proxy": "Habilitar proxy", + "proxy-host": "Host de proxy", + "proxy-host-required": "O host de proxy é obrigatório.", + "proxy-port": "Porta do proxy", + "proxy-port-required": "A porta do proxy é obrigatória.", + "proxy-port-range": "A porta do proxy deve estar em um intervalo de 1 a 65535.", + "proxy-user": "Usuário do proxy", + "proxy-password": "Senha do proxy", + "send-test-mail": "Enviar e-mail de teste", + "security-settings": "Configuração de segurança", + "password-policy": "Política de senhas", + "minimum-password-length": "Comprimento mínimo da senha", + "minimum-password-length-required": "O comprimento mínimo da senha é obrigatório", + "minimum-password-length-range": "O comprimento mínimo da senha deve estar em um intervalo de 5 a 50.", + "minimum-uppercase-letters": "Número mínimo de letras maiúsculas", + "minimum-uppercase-letters-range": "O número mínimo de letras maiúsculas não pode ser negativo", + "minimum-lowercase-letters": "Número mínimo de letras minúsculas", + "minimum-lowercase-letters-range": "O número mínimo de letras minúsculas não pode ser negativo", + "minimum-digits": "Número mínimo de dígitos", + "minimum-digits-range": "O número mínimo de dígitos não pode ser negativo", + "minimum-special-characters": "Número mínimo de caracteres especiais", + "minimum-special-characters-range": "O número mínimo de caracteres especiais não pode ser negativo", + "password-expiration-period-days": "Período de expiração de senha em dias", + "password-expiration-period-days-range": "O período de expiração de senha não pode ser negativo", + "password-reuse-frequency-days": "Frequência de reutilização de senha em dias", + "password-reuse-frequency-days-range": "A frequência em dias de reutilização da senha não pode ser negativa", + "general-policy": "Política geral", + "max-failed-login-attempts": "Número máximo de tentativas de login com falha antes de bloquear a conta", + "minimum-max-failed-login-attempts-range": "O número máximo de tentativas de login com falha não pode ser negativo", + "user-lockout-notification-email": "Em caso de bloqueio de conta do usuário, enviar notificação para o endereço de e-mail" + }, + "alarm": { + "alarm": "Alarme", + "alarms": "Alarmes", + "select-alarm": "Selecionar alarme", + "no-alarms-matching": "Nenhum alarme encontrado que coincida com '{{entity}}'.", + "alarm-required": "O alarme é obrigatório", + "alarm-status": "Status do alarme", + "alarm-status-list": "Lista de status de alarmes", + "any-status": "Qualquer status", + "search-status": { + "ANY": "Qualquer", + "ACTIVE": "Ativo", + "CLEARED": "Limpo", + "ACK": "Confirmado", + "UNACK": "Não confirmado" + }, + "display-status": { + "ACTIVE_UNACK": "Não confirmado ativo", + "ACTIVE_ACK": "Confirmado ativo", + "CLEARED_UNACK": "Não confirmado limpo", + "CLEARED_ACK": "Confirmado limpo" + }, + "no-alarms-prompt": "Nenhum alarme encontrado", + "created-time": "Hora de criação", + "type": "Tipo", + "severity": "Severidade", + "originator": "Originador", + "originator-type": "Tipo de originador", + "details": "Detalhes", + "status": "Status", + "alarm-details": "Detalhes do alarme", + "start-time": "Hora de início", + "end-time": "Hora de término", + "ack-time": "Horário de confirmação", + "clear-time": "Horário de limpeza", + "alarm-severity-list": "Lista de severidade de alarmes", + "any-severity": "Qualquer severidade", + "severity-critical": "Crítica", + "severity-major": "Principal", + "severity-minor": "Menor", + "severity-warning": "Aviso", + "severity-indeterminate": "Indeterminado", + "acknowledge": "Confirmar", + "clear": "Limpar", + "search": "Pesquisar alarmes", + "selected-alarms": "{ count, plural, 1 {1 alarm} other {# alarms} } selecionado(s)", + "no-data": "Nenhum dado para exibição", + "polling-interval": "Intervalo seletivo de alarmes (s)", + "polling-interval-required": "O intervalo de chamada seletiva de alarmes é obrigatório.", + "min-polling-interval-message": "É permitido no mínimo 1 segundo de intervalo de chamada seletiva.", + "aknowledge-alarms-title": "Confirmar { count, plural, 1 {1 alarm} other {# alarms} }", + "aknowledge-alarms-text": "Tem certeza de que deseja confirmar { count, plural, 1 {1 alarm} other {# alarms} }?", + "aknowledge-alarm-title": "Confirmar alarme", + "aknowledge-alarm-text": "Tem certeza de que deseja confirmar o alarme?", + "clear-alarms-title": "Limpar { count, plural, 1 {1 alarm} other {# alarms} }", + "clear-alarms-text": "Tem certeza de que deseja limpar { count, plural, 1 {1 alarm} other {# alarms} }?", + "clear-alarm-title": "Limpar alarme", + "clear-alarm-text": "Tem certeza de que deseja limpar alarme?", + "alarm-status-filter": "Filtro de status de alarmes", + "alarm-filter": "Filtro de alarmes", + "max-count-load": "Número máximo de alarmes a serem carregados (0 - ilimitado)", + "max-count-load-required": "O número máximo de alarmes é obrigatório.", + "max-count-load-error-min": "O valor mínimo é 0.", + "fetch-size": "Tamanho da busca", + "fetch-size-required": "O tamanho da busca é obrigatório.", + "fetch-size-error-min": "O valor mínimo é 10.", + "alarm-type-list": "Lista de tipos de alarmes", + "any-type": "Qualquer tipo", + "search-propagated-alarms": "Pesquisar alarmes propagados" + }, + "alias": { + "add": "Adicionar alias", + "edit": "Editar alias", + "name": "Nome do alias", + "name-required": "O nome do alias é obrigatório", + "duplicate-alias": "Já existe um alias com o mesmo nome.", + "filter-type-single-entity": "Entidade individual", + "filter-type-entity-list": "Lista de entidades", + "filter-type-entity-name": "Nome da entidade", + "filter-type-state-entity": "Entidade do estado do dashboard", + "filter-type-state-entity-description": "Entidade obtida dos parâmetros de estado do dashboard", + "filter-type-asset-type": "Tipo de ativo", + "filter-type-asset-type-description": "Ativos do tipo '{{assetType}}'", + "filter-type-asset-type-and-name-description": "Ativos do tipo '{{assetType}}' cujo nome começa com '{{prefix}}'", + "filter-type-device-type": "Tipo de dispositivo", + "filter-type-device-type-description": "Dispositivos do tipo '{{deviceType}}'", + "filter-type-device-type-and-name-description": "Dispositivos do tipo '{{deviceType}}' cujo nome começa com '{{prefix}}'", + "filter-type-entity-view-type": "Tipo de exibições de entidades", + "filter-type-entity-view-type-description": "Exibições de entidades do tipo '{{entityView}}'", + "filter-type-entity-view-type-and-name-description": "Exibições de entidades do tipo '{{entityView}}' cujo nome começa com '{{prefix}}'", + "filter-type-relations-query": "Consulta de relações", + "filter-type-relations-query-description": "{{entities}} que tem relação de {{relationType}} {{direction}} {{rootEntity}}", + "filter-type-asset-search-query": "Consulta de pesquisa de ativo", + "filter-type-asset-search-query-description": "Ativos dos tipos {{assetTypes}} que têm relação de {{relationType}} {{direction}} {{rootEntity}}", + "filter-type-device-search-query": "Consulta de pesquisa de dispositivo", + "filter-type-device-search-query-description": "Dispositivos dos tipos {{deviceTypes}} que têm relação de {{relationType}} {{direction}} {{rootEntity}}", + "filter-type-entity-view-search-query": "Consulta de pesquisa de exibição de entidade", + "filter-type-entity-view-search-query-description": "Exibições de entidades dos tipos {{entityViewTypes}} que têm relação de {{relationType}} {{direction}} {{rootEntity}}", + "entity-filter": "Filtro de entidades", + "resolve-multiple": "Resolver como entidades múltiplas", + "filter-type": "Tipo de filtro", + "filter-type-required": "O tipo de filtro é obrigatório.", + "entity-filter-no-entity-matched": "Nenhuma entidade encontrada que coincida com o filtro especificado.", + "no-entity-filter-specified": "Nenhum filtro de entidade especificado", + "root-state-entity": "Usar entidade de estado do dashboard como raiz", + "last-level-relation": "Procurar apenas último nível de relação", + "root-entity": "Entidade raiz", + "state-entity-parameter-name": "Nome do parâmetro de entidade de estado", + "default-state-entity": "Entidade de estado padrão", + "default-entity-parameter-name": "Predefinido", + "max-relation-level": "Nível máx. de relação", + "unlimited-level": "Nível ilimitado", + "state-entity": "Entidade de estado do dashboard", + "all-entities": "Todas as entidades", + "any-relation": "qualquer" + }, + "asset": { + "asset": "Ativo", + "assets": "Ativos", + "management": "Gerenciamento de ativos", + "view-assets": "Visualizar ativos", + "add": "Adicionar ativo", + "assign-to-customer": "Atribuir a cliente", + "assign-asset-to-customer": "Atribuir ativo(s) a cliente", + "assign-asset-to-customer-text": "Selecione os ativos a serem atribuídos ao cliente", + "no-assets-text": "Nenhum ativo encontrado", + "assign-to-customer-text": "Selecione o cliente para atribuir o(s) ativo(s)", + "public": "Público", + "assignedToCustomer": "Atribuído ao cliente", + "make-public": "Tornar ativo público", + "make-private": "Tornar ativo privado", + "unassign-from-customer": "Remover atribuição a cliente", + "delete": "Excluir ativo", + "asset-public": "Ativo é público", + "asset-type": "Tipo de ativo", + "asset-type-required": "O tipo de ativo é obrigatório.", + "select-asset-type": "Selecionar tipo de ativo", + "enter-asset-type": "Inserir tipo de ativo", + "any-asset": "Qualquer ativo", + "no-asset-types-matching": "Nenhum tipo de ativo encontrado que coincida com '{{entitySubtype}}'.", + "asset-type-list-empty": "Nenhum tipo de ativo selecionado.", + "asset-types": "Tipos de ativos", + "name": "Nome", + "name-required": "O nome é obrigatório.", + "description": "Descrição", + "type": "Tipo", + "type-required": "O tipo é obrigatório.", + "details": "Detalhes", + "events": "Eventos", + "add-asset-text": "Adicionar novo ativo", + "asset-details": "Detalhes do ativo", + "assign-assets": "Atribuir ativos", + "assign-assets-text": "Atribuir { count, plural, 1 {1 asset} other {# assets} } a cliente", + "delete-assets": "Excluir ativos", + "unassign-assets": "Remover atribuição de ativos", + "unassign-assets-action-title": "Remover atribuição de { count, plural, 1 {1 asset} other {# assets} } a cliente", + "assign-new-asset": "Atribuir novo ativo", + "delete-asset-title": "Tem certeza de que deseja excluir o ativo '{{assetName}}'?", + "delete-asset-text": "Cuidado, após confirmar, não será possível recuperar o ativo e nenhum dado associado.", + "delete-assets-title": "Tem certeza de que deseja excluir { count, plural, 1 {1 asset} other {# assets} }?", + "delete-assets-action-title": "Excluir { count, plural, 1 {1 asset} other {# assets} }", + "delete-assets-text": "Cuidado, após confirmar, todos os ativos selecionados serão removidos e não será possível recuperar nenhum dado associado.", + "make-public-asset-title": "Tem certeza de que deseja tornar o ativo '{{assetName}}' público?", + "make-public-asset-text": "Após confirmar, o ativo e todos os dados associados a ele se tornarão públicos e poderão ser acessados por outros.", + "make-private-asset-title": "Tem certeza de que deseja tornar o ativo '{{assetName}}' privado?", + "make-private-asset-text": "Após confirmar, o ativo e todos os dados associados a ele se tornarão privados e não poderão ser acessados por outros.", + "unassign-asset-title": "Tem certeza de que deseja remover a atribuição do ativo '{{assetName}}'?", + "unassign-asset-text": "Após confirmar, a atribuição do ativo será removida e ele não poderá ser acessado pelo cliente.", + "unassign-asset": "Remover atribuição de ativo", + "unassign-assets-title": "Tem certeza de que deseja remover a atribuição de { count, plural, 1 {1 asset} other {# assets} }?", + "unassign-assets-text": "Após confirmar, a atribuição de todos os ativos selecionados será removida e eles não poderão ser acessados pelo cliente.", + "copyId": "Copiar ID do ativo", + "idCopiedMessage": "O ID do ativo foi copiado para a área de transferência", + "select-asset": "Selecionar ativo", + "no-assets-matching": "Nenhum ativo encontrado que coincida com '{{entity}}'.", + "asset-required": "O ativo é obrigatório", + "name-starts-with": "O nome do ativo começa com", + "import": "Importar ativos", + "asset-file": "Arquivo de ativos", + "search": "Pesquisar ativo", + "selected-assets": "{ count, plural, 1 {1 asset} other {# assets} } selecionado(s)", + "label": "Etiqueta" + }, + "attribute": { + "attributes": "Atributos", + "latest-telemetry": "Última telemetria", + "attributes-scope": "Escopo de atributos de entidade", + "scope-latest-telemetry": "Última telemetria", + "scope-client": "Atributos do cliente", + "scope-server": "Atributos do servidor", + "scope-shared": "Atributos compartilhados", + "add": "Adicionar atributo", + "key": "Chave", + "last-update-time": "Horário da última atualização", + "key-required": "A chave de atributo é obrigatória.", + "value": "Valor", + "value-required": "O valor do atributo é obrigatório.", + "delete-attributes-title": "Tem certeza de que deseja excluir { count, plural, 1 {1 attribute} other {# attributes} }?", + "delete-attributes-text": "Cuidado, após confirmar, todos os atributos selecionados serão removidos.", + "delete-attributes": "Excluir atributos", + "enter-attribute-value": "Inserir valor do atributo", + "show-on-widget": "Mostrar no widget", + "widget-mode": "Modo de widget", + "next-widget": "Próximo widget", + "prev-widget": "Widget anterior", + "add-to-dashboard": "Adicionar ao dashboard", + "add-widget-to-dashboard": "Adicionar widget ao dashboard", + "selected-attributes": "{ count, plural, 1 {1 attribute} other {# attributes} } selecionado(s)", + "selected-telemetry": "{ count, plural, 1 {1 telemetry unit} other {# telemetry units} } selecionado(s)", + "no-attributes-text": "Nenhum atributo encontrado", + "no-telemetry-text": "Nenhuma telemetria encontrada" + }, + "audit-log": { + "audit": "Auditoria", + "audit-logs": "Logs de auditoria", + "timestamp": "Carimbo de data/hora", + "entity-type": "Tipo de entidade", + "entity-name": "Nome da entidade", + "user": "Usuário", + "type": "Tipo", + "status": "Status", + "details": "Detalhes", + "type-added": "Adicionado", + "type-deleted": "Excluído", + "type-updated": "Atualizado", + "type-attributes-updated": "Atributos atualizados", + "type-attributes-deleted": "Atributos excluídos", + "type-rpc-call": "Chamada RPC", + "type-credentials-updated": "Credenciais atualizadas", + "type-assigned-to-customer": "Atribuído ao cliente", + "type-unassigned-from-customer": "Atribuição a cliente removida", + "type-activated": "Ativado", + "type-suspended": "Suspenso", + "type-credentials-read": "Credenciais lidas", + "type-attributes-read": "Atributos lidos", + "type-relation-add-or-update": "Relação atualizada", + "type-relation-delete": "Relação excluída", + "type-relations-delete": "Todas as relações excluídas", + "type-alarm-ack": "Confirmado", + "type-alarm-clear": "Limpo", + "type-login": "Login", + "type-logout": "Logout", + "type-lockout": "Bloqueio", + "status-success": "Êxito", + "status-failure": "Falha", + "audit-log-details": "Detalhes da trilha de auditoria", + "no-audit-logs-prompt": "Nenhum log encontrado", + "action-data": "Dados da ação", + "failure-details": "Detalhes da falha", + "search": "Pesquisar logs de auditoria", + "clear-search": "Limpar pesquisa", + "type-assigned-from-tenant": "Atribuído do locatário", + "type-assigned-to-tenant": "Atribuído a locatário" + }, + "confirm-on-exit": { + "message": "Existem alterações sem salvar. Tem certeza de que deseja sair desta página?", + "html-message": "Existem alterações sem salvar.
    Tem certeza de que deseja sair desta página?", + "title": "Alterações sem salvar" + }, + "contact": { + "country": "País", + "city": "Cidade", + "state": "Estado", + "postal-code": "CEP / Código postal", + "postal-code-invalid": "Formato de código postal / CEP inválido.", + "address": "Endereço", + "address2": "Endereço 2", + "phone": "Telefone", + "email": "E-mail", + "no-address": "Em endereço" + }, + "common": { + "username": "Nome de usuário", + "password": "Senha", + "enter-username": "Inserir nome de usuário", + "enter-password": "Inserir senha", + "enter-search": "Inserir pesquisa", + "created-time": "Hora de criação", + "loading": "Carregando..." + }, + "content-type": { + "json": "Json", + "text": "Texto", + "binary": "Binário (Base64)" + }, + "customer": { + "customer": "Cliente", + "customers": "Clientes", + "management": "Gerenciamento de clientes", + "dashboard": "Dashboard de clientes", + "dashboards": "Dashboards de clientes", + "devices": "Dispositivos do cliente", + "entity-views": "Exibições de entidades do cliente", + "assets": "Ativos do cliente", + "public-dashboards": "Dashboards públicos", + "public-devices": "Dispositivos públicos", + "public-assets": "Ativos públicos", + "public-entity-views": "Exibições de entidades públicas", + "add": "Adicionar cliente", + "delete": "Excluir cliente", + "manage-customer-users": "Gerenciar usuários do cliente", + "manage-customer-devices": "Gerenciar dispositivos do cliente", + "manage-customer-dashboards": "Gerenciar dashboards do cliente", + "manage-public-devices": "Gerenciar dispositivos públicos", + "manage-public-dashboards": "Gerenciar dashboards públicos", + "manage-customer-assets": "Gerenciar ativos do cliente", + "manage-public-assets": "Gerenciar ativos públicos", + "add-customer-text": "Adicionar novo cliente", + "no-customers-text": "Nenhum cliente encontrado", + "customer-details": "Detalhes do cliente", + "delete-customer-title": "Tem certeza de que deseja excluir o ativo cliente '{{customerTitle}}'?", + "delete-customer-text": "Cuidado, após confirmar, não será possível recuperar o cliente e nenhum dado associado.", + "delete-customers-title": "Tem certeza de que deseja excluir { count, plural, 1 {1 customer} other {# customers} }?", + "delete-customers-action-title": "Excluir { count, plural, 1 {1 customer} other {# customers} }", + "delete-customers-text": "Cuidado, após confirmar, todos os clientes selecionados serão removidos e não será possível recuperar nenhum dado associado.", + "manage-users": "Gerenciar usuários", + "manage-assets": "Gerenciar ativos", + "manage-devices": "Gerenciar dispositivos", + "manage-dashboards": "Gerenciar dashboards", + "title": "Título", + "title-required": "O título é obrigatório.", + "description": "Descrição", + "details": "Detalhes", + "events": "Eventos", + "copyId": "Copiar ID de cliente", + "idCopiedMessage": "O ID do cliente foi copiado para a área de transferência", + "select-customer": "Selecionar cliente", + "no-customers-matching": "Nenhum cliente encontrado que coincida com '{{entity}}'.", + "customer-required": "O cliente é obrigatório", + "select-default-customer": "Selecionar cliente predefinido", + "default-customer": "Cliente predefinido", + "default-customer-required": "O cliente predefinido é necessário para depurar o dashboard no nível de locatário", + "search": "Pesquisar clientes", + "selected-customers": "{ count, plural, 1 {1 customer} other {# customers} } selecionado(s)" + }, + "datetime": { + "date-from": "Data de", + "time-from": "Hora de", + "date-to": "Data até", + "time-to": "Hora até" + }, + "dashboard": { + "dashboard": "Dashboard", + "dashboards": "Dashboards", + "management": "Gerenciamento de dashboards", + "view-dashboards": "Visualizar dashboards", + "add": "Adicionar dashboard", + "assign-dashboard-to-customer": "Atribuir dashboard(s) a cliente", + "assign-dashboard-to-customer-text": "Selecione os dashboards a serem atribuídos ao cliente", + "assign-to-customer-text": "Selecione o cliente para atribuir o(s) dashboard(s)", + "assign-to-customer": "Atribuir a cliente", + "unassign-from-customer": "Remover atribuição a cliente", + "make-public": "Tornar dashboards público", + "make-private": "Tornar dashboards privado", + "manage-assigned-customers": "Gerenciar clientes atribuídos", + "assigned-customers": "Clientes atribuídos", + "assign-to-customers": "Atribuir Dashboard(s) a Clientes", + "assign-to-customers-text": "Selecione os clientes para atribuir o(s) dashboard(s)", + "unassign-from-customers": "Remover Atribuição de Dashboard(s) a Clientes", + "unassign-from-customers-text": "Selecione os clientes para remover a atribuição do(s) dashboard(s)", + "no-dashboards-text": "Nenhum dashboard encontrado", + "no-widgets": "Nenhum widgets configurado", + "add-widget": "Adicionar novo widget", + "title": "Título", + "select-widget-title": "Selecionar widget", + "select-widget-subtitle": "Lista de tipos de widget disponíveis", + "delete": "Excluir dashboard", + "title-required": "O título é obrigatório.", + "description": "Descrição", + "details": "Detalhes", + "dashboard-details": "Detalhes do dashboard", + "add-dashboard-text": "Adicionar novo dashboard", + "assign-dashboards": "Atribuir dashboards", + "assign-new-dashboard": "Atribuir novo dashboard", + "assign-dashboards-text": "Atribuir { count, plural, 1 {1 dashboard} other {# dashboards} } a clientes", + "unassign-dashboards-action-text": "Remover atribuição de { count, plural, 1 {1 dashboard} other {# dashboards} } a clientes", + "delete-dashboards": "Excluir dashboards", + "unassign-dashboards": "Remover atribuição de dashboards", + "unassign-dashboards-action-title": "Remover atribuição de { count, plural, 1 {1 dashboard} other {# dashboards} } a cliente", + "delete-dashboard-title": "Tem certeza de que deseja excluir o dashboard '{{dashboardTitle}}'?", + "delete-dashboard-text": "Cuidado, após confirmar, não será possível recuperar o dashboard e nenhum dado associado.", + "delete-dashboards-title": "Tem certeza de que deseja excluir { count, plural, 1 {1 dashboard} other {# dashboards} }?", + "delete-dashboards-action-title": "Excluir { count, plural, 1 {1 dashboard} other {# dashboards} }", + "delete-dashboards-text": "Cuidado, após confirmar, todos os dashboards selecionados serão removidos e não será possível recuperar nenhum dado associado.", + "unassign-dashboard-title": "Tem certeza de que deseja remover a atribuição do dashboard '{{dashboardTitle}}'?", + "unassign-dashboard-text": "Após confirmar, a atribuição do dashboard será removida e ele não poderá ser acessado pelo cliente.", + "unassign-dashboard": "Remover atribuição de dashboard", + "unassign-dashboards-title": "Tem certeza de que deseja remover a atribuição de { count, plural, 1 {1 dashboard} other {# dashboards} }?", + "unassign-dashboards-text": "Após confirmar, a atribuição de todos os dashboards selecionados será removida e eles não poderão ser acessados pelo cliente.", + "public-dashboard-title": "Agora o dashboard é público", + "public-dashboard-text": "Agora o dashboard {{dashboardTitle}} é público e pode ser acessado pelo próximo link público:", + "public-dashboard-notice": "Observação: Não se esqueça de tornar dispositivos relacionados públicos para poder acessar seus dados.", + "make-private-dashboard-title": "Tem certeza de que deseja tornar o dashboard '{{dashboardTitle}}' privado?", + "make-private-dashboard-text": "Após confirmar, o dashboard e todos os dados associados a ele se tornarão privados e não poderão ser acessados por outros.", + "make-private-dashboard": "Tornar dashboards privado", + "socialshare-text": "'{{dashboardTitle}}' da plataforma ThingsBoard", + "socialshare-title": "'{{dashboardTitle}}' da plataforma ThingsBoard", + "select-dashboard": "Selecionar dashboard", + "no-dashboards-matching": "Nenhum dashboard encontrado que coincida com '{{entity}}'.", + "dashboard-required": "O dashboard é obrigatório.", + "select-existing": "Selecionar dashboard existente", + "create-new": "Criar novo dashboard", + "new-dashboard-title": "Novo título de dashboard", + "open-dashboard": "Abrir dashboard", + "set-background": "Definir plano de fundo", + "background-color": "Cor de fundo", + "background-image": "Imagem de fundo", + "background-size-mode": "Modo de tamanho de fundo", + "no-image": "Nenhuma imagem selecionada", + "drop-image": "Solte uma imagem ou clique para selecionar um arquivo para carregar.", + "settings": "Configurações", + "columns-count": "Contagem de colunas", + "columns-count-required": "A contagem de colunas é obrigatória.", + "min-columns-count-message": "Somente é permitida uma contagem de no mínimo 10 colunas.", + "max-columns-count-message": "Somente é permitida uma contagem de no máximo 1000 colunas.", + "widgets-margins": "Margem entre widgets", + "margin-required": "O valor da margem é obrigatório.", + "min-margin-message": "Somente 0 é permitido como valor mínimo de margem.", + "max-margin-message": "Somente 50 é permitido como valor máximo de margem.", + "horizontal-margin": "Margem horizontal", + "horizontal-margin-required": "O valor da margem horizontal é obrigatório.", + "min-horizontal-margin-message": "Somente 0 é permitido como valor mínimo de margem horizontal.", + "max-horizontal-margin-message": "Somente 50 é permitido como valor máximo de margem horizontal.", + "vertical-margin": "Margem vertical", + "vertical-margin-required": "O valor da margem vertical é obrigatório.", + "min-vertical-margin-message": "Somente 0 é permitido como valor mínimo de margem vertical.", + "max-vertical-margin-message": "Somente 50 é permitido como valor máximo de margem vertical.", + "autofill-height": "Altura do layout de preenchimento automático", + "mobile-layout": "Configuração de layout móvel", + "mobile-row-height": "Altura de linha móvel, px", + "mobile-row-height-required": "O valor da altura de linha móvel é obrigatório.", + "min-mobile-row-height-message": "Somente 5 pixels é permitido como valor mínimo da altura de linha móvel.", + "max-mobile-row-height-message": "Somente 200 pixels é permitido como valor máximo da altura de linha móvel.", + "display-title": "Exibir título de dashboard", + "toolbar-always-open": "Manter barra de ferramentas aberto", + "title-color": "Cor do título", + "display-dashboards-selection": "Exibir seleção de dashboards", + "display-entities-selection": "Exibir seleção de entidades", + "display-filters": "Exibir filtros", + "display-dashboard-timewindow": "Exibir timewindow", + "display-dashboard-export": "Exibir exportação", + "import": "Importar dashboard", + "export": "Exportar dashboard", + "export-failed-error": "Impossível exportar dashboard: {{error}}", + "create-new-dashboard": "Criar novo dashboard", + "dashboard-file": "Arquivo de dashboard", + "invalid-dashboard-file-error": "Impossível importar dashboard: Estrutura de dados do dashboard inválida", + "dashboard-import-missing-aliases-title": "Configurar aliases usados por dashboard importado", + "create-new-widget": "Criar novo widget", + "import-widget": "Importar widget", + "widget-file": "Arquivo de widget", + "invalid-widget-file-error": "Impossível importar widget: Estrutura de dados do widget inválida.", + "widget-import-missing-aliases-title": "Configurar aliases usados por widget importado", + "open-toolbar": "Abrir barra de ferramentas de dashboard", + "close-toolbar": "Fechar barra de ferramentas", + "configuration-error": "Erro de configuração", + "alias-resolution-error-title": "Erro de configuração de aliases de dashboard", + "invalid-aliases-config": "Impossível encontrar dispositivos que coincidam com algum dos filtros de aliases.
    Para resolver esta questão, encontre em contato com seu administrador.", + "select-devices": "Selecionar dispositivos", + "assignedToCustomer": "Atribuído ao cliente", + "assignedToCustomers": "Atribuído aos clientes", + "public": "Público", + "public-link": "Link público", + "copy-public-link": "Copiar link público", + "public-link-copied-message": "O link público da dashboard foi copiado para a área de transferência", + "manage-states": "Gerenciar estados de dashboards", + "states": "Estados de dashboards", + "search-states": "Pesquisar estados de dashboards", + "selected-states": "{ count, plural, 1 {1 dashboard state} other {# dashboard states} } selecionado(s)", + "edit-state": "Editar estado de dashboard", + "delete-state": "Excluir estado de dashboard", + "add-state": "Adicionar estado de dashboard", + "no-states-text": "Nenhum estado encontrado", + "state": "Estado do dashboard", + "state-name": "Nome", + "state-name-required": "O nome do estado do dashboard é obrigatório.", + "state-id": "ID do estado", + "state-id-required": "O ID do estado do dashboard é obrigatório.", + "state-id-exists": "Já existe um estado de dashboard com o mesmo ID.", + "is-root-state": "Estado raiz", + "delete-state-title": "Excluir estado de dashboard", + "delete-state-text": "Tem certeza de que deseja excluir o estado de dashboard com o nome '{{stateName}}'?", + "show-details": "Mostrar detalhes", + "hide-details": "Ocultar detalhes", + "select-state": "Selecionar estado alvo", + "state-controller": "Controlador de estado", + "search": "Selecionar dashboards", + "selected-dashboards": "{ count, plural, 1 {1 dashboard} other {# dashboards} } selecionado(s)" + }, + "datakey": { + "settings": "Configurações", + "advanced": "Avançado", + "label": "Etiqueta", + "color": "Cor", + "units": "Símbolo especial a ser exibido ao lado do valor", + "decimals": "Número de dígitos após ponto de flutuação", + "data-generation-func": "Função de geração de dados", + "use-data-post-processing-func": "Usar função de pós-processamento de dados", + "configuration": "Configuração de dados-chave", + "timeseries": "Intervalos de tempo", + "attributes": "Atributos", + "entity-field": "Campo de entidade", + "alarm": "Campos de alarmes", + "timeseries-required": "Os intervalos de tempo de entidade são obrigatórios.", + "timeseries-or-attributes-required": "Os intervalos de tempo/atributos de entidade são obrigatórios.", + "alarm-fields-timeseries-or-attributes-required": "Os campos de alarmes ou intervalos de tempo/atributos de entidade são obrigatórios.", + "maximum-timeseries-or-attributes": "No máximo { count, plural, 1 {1 timeseries/attribute is allowed.} other {# timeseries/attributes are allowed} }", + "alarm-fields-required": "Os campos de alarmes são obrigatórios.", + "function-types": "Tipos de função", + "function-types-required": "Os tipos de função são obrigatórios.", + "maximum-function-types": "No máximo { count, plural, 1 {1 function type is allowed.} other {# function types are allowed} }", + "time-description": "carimbo de data/horário do valor atual;", + "value-description": "o valor atual;", + "prev-value-description": "resultado da chamada de função anterior;", + "time-prev-description": "carimbo de data/horário do valor anterior;", + "prev-orig-value-description": "valor original anterior;" + }, + "datasource": { + "type": "Tipo de fonte de dados", + "name": "Nome", + "add-datasource-prompt": "Adicione a fonte de dados" + }, + "details": { + "details": "Detalhes", + "edit-mode": "Modo de edição", + "edit-json": "Editar JSON", + "toggle-edit-mode": "Alternar o modo de edição" + }, + "device": { + "device": "Dispositivo", + "device-required": "O nome do dispositivo é obrigatório.", + "devices": "Dispositivos", + "management": "Gerenciamento de dispositivos", + "view-devices": "Exibir Dispositivos", + "device-alias": "Alias do dispositivo", + "aliases": "Aliases de dispositivos", + "no-alias-matching": "'{{alias}}' não encontrado.", + "no-aliases-found": "Nenhum alias encontrado.", + "no-key-matching": "'{{key}}' não encontrado.", + "no-keys-found": "Nenhuma chave encontrado.", + "create-new-alias": "Criar um novo!", + "create-new-key": "Criar um novo!", + "duplicate-alias-error": "Alias '{{alias}}' duplicado encontrado.
    Os aliases de dispositivos devem ser exclusivos no dashboard.", + "configure-alias": "Configurar alias de '{{alias}}'", + "no-devices-matching": "Nenhum dispositivo encontrado que coincida com '{{entity}}'.", + "alias": "Alias", + "alias-required": "O alias do dispositivo é obrigatório.", + "remove-alias": "Remover alias do dispositivo", + "add-alias": "Adicionar alias do dispositivo", + "name-starts-with": "O nome do dispositivo começa com", + "device-list": "Lista de dispositivos", + "use-device-name-filter": "Usar filtro", + "device-list-empty": "Nenhum dispositivo selecionado.", + "device-name-filter-required": "O filtro do nome do dispositivo é obrigatório.", + "device-name-filter-no-device-matched": "Nenhum dispositivo encontrado que comece com '{{device}}'.", + "add": "Adicionar Dispositivo", + "assign-to-customer": "Atribuir a cliente", + "assign-device-to-customer": "Atribuir Dispositivo(s) a Cliente", + "assign-device-to-customer-text": "Selecione os dispositivos a serem atribuídos ao cliente", + "make-public": "Tornar dispositivo público", + "make-private": "Tornar dispositivo privado", + "no-devices-text": "Nenhum dispositivo encontrado", + "assign-to-customer-text": "Selecione cliente para atribuir o(s) dispositivo(s)", + "device-details": "Detalhes do dispositivo", + "add-device-text": "Adicionar novo dispositivo", + "credentials": "Credenciais", + "manage-credentials": "Gerenciar credenciais", + "delete": "Excluir dispositivo", + "assign-devices": "Atribuir dispositivos", + "assign-devices-text": "Atribuir { count, plural, 1 {1 device} other {# devices} } a cliente", + "delete-devices": "Excluir dispositivos", + "unassign-from-customer": "Remover atribuição a cliente", + "unassign-devices": "Remover atribuição de dispositivos", + "unassign-devices-action-title": "Remover atribuição de { count, plural, 1 {1 device} other {# devices} } a cliente", + "assign-new-device": "Atribuir novo dispositivo", + "make-public-device-title": "Tem certeza de que deseja tornar o dispositivo '{{deviceName}}' público?", + "make-public-device-text": "Após confirmar, o dispositivo e todos os dados associados a ele se tornarão públicos e poderão ser acessados por outros.", + "make-private-device-title": "Tem certeza de que deseja tornar o dispositivo '{{deviceName}}' privado?", + "make-private-device-text": "Após confirmar, o dispositivo e todos os dados associados a ele se tornarão privados e não poderão ser acessados por outros.", + "view-credentials": "Exibir credenciais", + "delete-device-title": "Tem certeza de que deseja excluir o dispositivo '{{deviceName}}'?", + "delete-device-text": "Cuidado, após confirmar, não será possível recuperar o dispositivo e nenhum dado associado.", + "delete-devices-title": "Tem certeza de que deseja excluir { count, plural, 1 {1 device} other {# devices} }?", + "delete-devices-action-title": "Excluir { count, plural, 1 {1 device} other {# devices} }", + "delete-devices-text": "Cuidado, após confirmar, todos os dispositivos selecionados serão removidos e não será possível recuperar nenhum dado associado.", + "unassign-device-title": "Tem certeza de que deseja remover a atribuição do dispositivo '{{deviceName}}'?", + "unassign-device-text": "Após confirmar, a atribuição do dispositivo será removida e ele não poderá ser acessado pelo cliente.", + "unassign-device": "Remover atribuição de dispositivo", + "unassign-devices-title": "Tem certeza de que deseja remover a atribuição de { count, plural, 1 {1 device} other {# devices} }?", + "unassign-devices-text": "Após confirmar, a atribuição de todos os dispositivos selecionados será removida e eles não poderão ser acessados pelo cliente.", + "device-credentials": "Credenciais do dispositivo", + "credentials-type": "Tipo de credencial", + "access-token": "Token de acesso", + "access-token-required": "O token de acesso é obrigatório.", + "access-token-invalid": "O token de acesso deve ter de 1 a 20 caracteres.", + "rsa-key": "Chave pública RSA", + "rsa-key-required": "A chave pública RSA é obrigatória.", + "secret": "Segredo", + "secret-required": "O segredo é obrigatório.", + "device-type": "Tipo de dispositivo", + "device-type-required": "O tipo de dispositivo é obrigatório.", + "select-device-type": "Selecionar tipo de dispositivo", + "enter-device-type": "Inserir tipo de dispositivo", + "any-device": "Qualquer dispositivo", + "no-device-types-matching": "Nenhum tipo de dispositivo encontrado que coincida com '{{entitySubtype}}'.", + "device-type-list-empty": "Nenhum tipo de dispositivo selecionado.", + "device-types": "Tipos de dispositivos", + "name": "Nome", + "name-required": "O nome é obrigatório.", + "description": "Descrição", + "label": "Etiqueta", + "events": "Eventos", + "details": "Detalhes", + "copyId": "Copiar ID de dispositivo", + "copyAccessToken": "Copiar token de acesso", + "idCopiedMessage": "O ID do dispositivo foi copiado para a área de transferência", + "accessTokenCopiedMessage": "O token de acesso foi copiado para a área de transferência", + "assignedToCustomer": "Atribuído ao cliente", + "unable-delete-device-alias-title": "Impossível excluir alias do dispositivo", + "unable-delete-device-alias-text": "Não é possível excluir o alias do dispositivo '{{deviceAlias}}' porque ele é usado pelo(s) seguinte(s) widget(s):
    {{widgetsList}}", + "is-gateway": "É gateway", + "public": "Público", + "device-public": "O dispositivo é público", + "select-device": "Selecionar dispositivo", + "import": "Importar dispositivo", + "device-file": "Arquivo de dispositivo", + "search": "Pesquisar dispositivos", + "selected-devices": "{ count, plural, 1 {1 device} other {# devices} } selecionado(s)" + }, + "dialog": { + "close": "Fechar diálogo" + }, + "direction": { + "column": "Coluna", + "row": "Linha" + }, + "error": { + "unable-to-connect": "Impossível conectar ao servidor! Verifique sua conexão à Internet.", + "unhandled-error-code": "Código de erro sem tratamento: {{errorCode}}", + "unknown-error": "Erro desconhecido" + }, + "entity": { + "entity": "Entidade", + "entities": "Entidades", + "aliases": "Aliases de entidades", + "entity-alias": "Alias de entidade", + "unable-delete-entity-alias-title": "Impossível excluir alias de entidade", + "unable-delete-entity-alias-text": "Não é possível excluir o alias de entidade '{{entityAlias}}' porque ele é usado pelo(s) seguinte(s) widget(s):
    {{widgetsList}}", + "duplicate-alias-error": "Alias '{{alias}}' duplicado encontrado.
    Os aliases de entidades devem ser exclusivos no dashboard.", + "missing-entity-filter-error": "Filtro ausente para o alias '{{alias}}'.", + "configure-alias": "Configurar alias de '{{alias}}'", + "alias": "Alias", + "alias-required": "O alias de entidade é obrigatório.", + "remove-alias": "Remover alias de entidade", + "add-alias": "Adicionar alias de entidade", + "entity-list": "Lista de entidades", + "entity-type": "Tipo de entidade", + "entity-types": "Tipos de entidades", + "entity-type-list": "Lista de tipos de entidades", + "any-entity": "Qualquer entidade", + "enter-entity-type": "Inserir tipo de entidade", + "no-entities-matching": "Nenhuma entidade encontrada que coincida com '{{entity}}'.", + "no-entity-types-matching": "Nenhum tipo de entidade encontrado que coincida com '{{entityType}}'.", + "name-starts-with": "O nome começa com", + "use-entity-name-filter": "Usar filtro", + "entity-list-empty": "Nenhuma entidade selecionada.", + "entity-type-list-empty": "Nenhum tipo de entidade selecionado.", + "entity-name-filter-required": "O filtro do nome de entidade é obrigatório.", + "entity-name-filter-no-entity-matched": "Nenhuma entidade encontrada que comece com '{{entity}}'.", + "all-subtypes": "Tudo", + "select-entities": "Selecionar entidades", + "no-aliases-found": "Nenhum alias encontrado.", + "no-alias-matching": "'{{alias}}' não encontrado.", + "create-new-alias": "Criar um novo!", + "key": "Chave", + "key-name": "Nome da chave", + "no-keys-found": "Nenhuma chave encontrado.", + "no-key-matching": "'{{key}}' não encontrado.", + "create-new-key": "Criar um novo!", + "type": "Tipo", + "type-required": "O tipo de entidade é obrigatório.", + "type-device": "Dispositivo", + "type-devices": "Dispositivos", + "list-of-devices": "{ count, plural, 1 {One device} other {List of # devices} }", + "device-name-starts-with": "Dispositivos cujos nomes começam com '{{prefix}}'", + "type-asset": "Ativo", + "type-assets": "Ativos", + "list-of-assets": "{ count, plural, 1 {One asset} other {List of # assets} }", + "asset-name-starts-with": "Ativos cujos nomes começam com '{{prefix}}'", + "type-entity-view": "Exibição de entidade", + "type-entity-views": "Exibições de entidades", + "list-of-entity-views": "{ count, plural, 1 {One entity view} other {List of # entity views} }", + "entity-view-name-starts-with": "Exibições de entidades cujos nomes começam com '{{prefix}}'", + "type-rule": "Regra", + "type-rules": "Regras", + "list-of-rules": "{ count, plural, 1 {One rule} other {List of # rules} }", + "rule-name-starts-with": "Regras cujos nomes começam com '{{prefix}}'", + "type-plugin": "Plug-in", + "type-plugins": "Plug-ins", + "list-of-plugins": "{ count, plural, 1 {One plugin} other {List of # plugins} }", + "plugin-name-starts-with": "Plug-ins cujos nomes começam com '{{prefix}}'", + "type-tenant": "Locatário", + "type-tenants": "Locatários", + "list-of-tenants": "{ count, plural, 1 {One tenant} other {List of # tenants} }", + "tenant-name-starts-with": "Locatários cujos nomes começam com '{{prefix}}'", + "type-customer": "Cliente", + "type-customers": "Clientes", + "list-of-customers": "{ count, plural, 1 {One customer} other {List of # customers} }", + "customer-name-starts-with": "Clientes cujos nomes começam com '{{prefix}}'", + "type-user": "Usuário", + "type-users": "Usuários", + "list-of-users": "{ count, plural, 1 {One user} other {List of # users} }", + "user-name-starts-with": "Usuários cujos nomes começam com '{{prefix}}'", + "type-dashboard": "Dashboard", + "type-dashboards": "Dashboards", + "list-of-dashboards": "{ count, plural, 1 {One dashboard} other {List of # dashboards} }", + "dashboard-name-starts-with": "Dashboards cujos nomes começam com '{{prefix}}'", + "type-alarm": "Alarme", + "type-alarms": "Alarmes", + "list-of-alarms": "{ count, plural, 1 {One alarms} other {List of # alarms} }", + "alarm-name-starts-with": "Alarmes cujos nomes começam com '{{prefix}}'", + "type-rulechain": "Cadeia de regras", + "type-rulechains": "Cadeias de regras", + "list-of-rulechains": "{ count, plural, 1 {One rule chain} other {List of # rule chains} }", + "rulechain-name-starts-with": "Cadeias de regras cujos nomes começam com '{{prefix}}'", + "type-rulenode": "Nó de regra", + "type-rulenodes": "Nós de regras", + "list-of-rulenodes": "{ count, plural, 1 {One rule node} other {List of # rule nodes} }", + "rulenode-name-starts-with": "Nós de regras cujos nomes começam com '{{prefix}}'", + "type-current-customer": "Cliente atual", + "type-current-tenant": "Locatário atual", + "type-current-user": "Usuário atual", + "type-current-user-owner": "Proprietário de usuário atual", + "search": "Pesquisar entidades", + "selected-entities": "{ count, plural, 1 {1 entity} other {# entities} } selecionado(s)", + "entity-name": "Nome da entidade", + "entity-label": "Etiqueta de entidade", + "details": "Detalhes de entidades", + "no-entities-prompt": "Nenhuma entidade encontrada", + "no-data": "Nenhum dado para exibição", + "columns-to-display": "Colunas para Exibição" + }, + "entity-field": { + "created-time": "Hora de criação", + "name": "Nome", + "type": "Tipo", + "first-name": "Nome", + "last-name": "Sobrenome", + "email": "E-mail", + "title": "Título", + "country": "País", + "state": "Estado", + "city": "Cidade", + "address": "Endereço", + "address2": "Endereço 2", + "zip": "CEP", + "phone": "Telefone", + "label": "Etiqueta" + }, + "entity-view": { + "entity-view": "Exibição de entidade", + "entity-view-required": "A exibição de entidade é obrigatória.", + "entity-views": "Exibições de entidades", + "management": "Habilitar gerenciamento de exibições", + "view-entity-views": "Visualizar exibições de entidades", + "entity-view-alias": "Alias de exibição de entidade", + "aliases": "Aliases de exibições de entidades", + "no-alias-matching": "'{{alias}}' não encontrado.", + "no-aliases-found": "Nenhum alias encontrado.", + "no-key-matching": "'{{key}}' não encontrado.", + "no-keys-found": "Nenhuma chave encontrado.", + "create-new-alias": "Criar um novo!", + "create-new-key": "Criar um novo!", + "duplicate-alias-error": "Alias '{{alias}}' duplicado encontrado.
    Os aliases de exibições de entidades devem ser exclusivos no dashboard.", + "configure-alias": "Configurar alias de '{{alias}}'", + "no-entity-views-matching": "Nenhuma exibição de entidade encontrada que coincida com '{{entity}}'.", + "public": "Público", + "alias": "Alias", + "alias-required": "O alias de exibição de entidade é obrigatório.", + "remove-alias": "Remover alias de exibição de entidade", + "add-alias": "Adicionar alias de exibição de entidade", + "name-starts-with": "O nome da exibição de entidade começa com", + "entity-view-list": "Lista de exibições de entidades", + "use-entity-view-name-filter": "Usar filtro", + "entity-view-list-empty": "Nenhuma exibição de entidade selecionada.", + "entity-view-name-filter-required": "O filtro de nomes de exibições de entidades é obrigatório.", + "entity-view-name-filter-no-entity-view-matched": "Nenhuma exibição de entidade encontrada que comece com '{{entityView}}'.", + "add": "Adicionar exibição de entidade", + "entity-view-public": "A exibição de entidade é pública", + "assign-to-customer": "Atribuir a cliente", + "assign-entity-view-to-customer": "Atribuir Exibição(ões) de entidade(s) a Cliente", + "assign-entity-view-to-customer-text": "Selecione as exibições de entidades a serem atribuídas ao cliente", + "no-entity-views-text": "Nenhuma exibição de entidade encontrada", + "assign-to-customer-text": "Selecione o cliente para atribuir a(s) exibição(ões) de entidade(s)", + "entity-view-details": "Detalhes de exibições de entidades", + "add-entity-view-text": "Adicionar nova exibição de entidade", + "delete": "Excluir exibição de entidade", + "assign-entity-views": "Atribuir exibições de entidades", + "assign-entity-views-text": "Atribuir { count, plural, 1 {1 entity view} other {# entity views} } a cliente", + "delete-entity-views": "Excluir exibições de entidades", + "unassign-from-customer": "Remover atribuição a cliente", + "unassign-entity-views": "Remover atribuição de exibições de entidades", + "unassign-entity-views-action-title": "Remover atribuição de { count, plural, 1 {1 entity view} other {# entity views} } a cliente", + "assign-new-entity-view": "Atribuir nova exibição de entidade", + "delete-entity-view-title": "Tem certeza de que deseja excluir a exibição de entidade '{{entityViewName}}'?", + "delete-entity-view-text": "Cuidado, após confirmar, não será possível recuperar a exibição de entidade e todos os dados associados.", + "delete-entity-views-title": "Tem certeza de que deseja excluir { count, plural, 1 {1 entity view} other {# entity views} }?", + "delete-entity-views-action-title": "Excluir { count, plural, 1 {1 entity view} other {# entity views} }", + "delete-entity-views-text": "Cuidado, após confirmar, todas as exibições de entidades selecionadas serão removidas e não será possível recuperar nenhum dado associado.", + "unassign-entity-view-title": "Tem certeza de que deseja remover a atribuição da exibição de entidade '{{entityViewName}}'?", + "unassign-entity-view-text": "Após confirmar, a atribuição da exibição de entidade será removida e ela não poderá ser acessada pelo cliente.", + "unassign-entity-view": "Remover atribuição de exibição de entidade", + "unassign-entity-views-title": "Tem certeza de que deseja remover a atribuição de { count, plural, 1 {1 entity view} other {# entity views} }?", + "unassign-entity-views-text": "Após confirmar, a atribuição de todas as exibições de entidade selecionadas será removida e elas não poderão ser acessadas pelo cliente.", + "entity-view-type": "Tipo de exibições de entidades", + "entity-view-type-required": "O tipo de exibição de entidade é obrigatório.", + "select-entity-view-type": "Selecionar tipo de exibição de entidade", + "enter-entity-view-type": "Inserir tipo de exibição de entidade", + "any-entity-view": "Qualquer exibição de entidade", + "no-entity-view-types-matching": "Nenhum tipo de exibição de entidade encontrado que coincida com '{{entitySubtype}}'.", + "entity-view-type-list-empty": "Nenhum tipo de exibição de entidade selecionado.", + "entity-view-types": "Tipos de exibição de entidade", + "created-time": "Hora de criação", + "name": "Nome", + "name-required": "O nome é obrigatório.", + "description": "Descrição", + "events": "Eventos", + "details": "Detalhes", + "copyId": "Copiar ID de exibição de entidade", + "idCopiedMessage": "O ID de exibição de entidade foi copiado para a área de transferência", + "assignedToCustomer": "Atribuído ao cliente", + "unable-entity-view-device-alias-title": "Impossível excluir alias de exibição de entidade", + "unable-entity-view-device-alias-text": "Não é possível excluir o alias do dispositivo '{{entityViewAlias}}' porque ele é usado pelo(s) seguinte(s) widget(s):
    {{widgetsList}}", + "select-entity-view": "Selecionar exibição de entidade", + "make-public": "Tornar exibição de entidade pública", + "make-private": "Tornar exibição de entidade privada", + "start-date": "Data de início", + "start-ts": "Hora de início", + "end-date": "Data de término", + "end-ts": "Hora de término", + "date-limits": "Limites de datas", + "client-attributes": "Atributos do cliente", + "shared-attributes": "Atributos compartilhados", + "server-attributes": "Atributos do servidor", + "timeseries": "Intervalos de tempo", + "client-attributes-placeholder": "Atributos do cliente", + "shared-attributes-placeholder": "Atributos compartilhados", + "server-attributes-placeholder": "Atributos do servidor", + "timeseries-placeholder": "Intervalos de tempo", + "target-entity": "Entidade alvo", + "attributes-propagation": "Propagação de atributos", + "attributes-propagation-hint": "A exibição de entidade copiará automaticamente atributos especificados da entidade alvo sempre que essa exibição de entidade for salva ou atualizada. Por questões de desempenho, os atributos da entidade alvo não são propagados para a exibição de entidade em cada alteração de atributo. A propagação automática pode ser habilitada configurando o nó de regra \"copiar para exibição\" na cadeia de regras e vinculando mensagens de \"Pós-Atributos\" e \"Atributos Atualizados\" ao novo nó de regra.", + "timeseries-data": "Dados de intervalos de tempo", + "timeseries-data-hint": "Configurar chaves de dados de intervalos de tempo da entidade alvo que estará acessível para a exibição de entidade. Esses dados de intervalo de tempo são apenas de leitura.", + "make-public-entity-view-title": "Tem certeza de que deseja tornar a exibição de entidade '{{entityViewName}}' pública?", + "make-public-entity-view-text": "Após confirmar, a exibição de entidade e todos os dados associados a ela se tornarão públicos e poderão ser acessados por outros.", + "make-private-entity-view-title": "Tem certeza de que deseja tornar a exibição de entidade '{{entityViewName}}' privada?", + "make-private-entity-view-text": "Após confirmar, a exibição de entidade e todos os dados associados a ela se tornarão privados e não poderão ser acessados por outros.", + "search": "Pesquisar exibições de entidades", + "selected-entity-views": "{ count, plural, 1 {1 entity view} other {# entity views} } selecionado(s)" + }, + "event": { + "event-type": "Tipo de evento", + "type-error": "Erro", + "type-lc-event": "Evento de ciclo de vida", + "type-stats": "Estatísticas", + "type-debug-rule-node": "Depuração", + "type-debug-rule-chain": "Depuração", + "no-events-prompt": "Nenhum evento encontrado", + "error": "Erro", + "alarm": "Alarme", + "event-time": "Tempo de evento", + "server": "Servidor", + "body": "Corpo", + "method": "Método", + "type": "Tipo", + "entity": "Entidade", + "message-id": "ID de mensagem", + "message-type": "Tipo de Mensagem", + "data-type": "Tipo de Dados", + "relation-type": "Tipo de Relação", + "metadata": "Metadados", + "data": "Dados", + "event": "Evento", + "status": "Status", + "success": "Êxito", + "failed": "Falhou", + "messages-processed": "Mensagens processadas", + "errors-occurred": "Erros" + }, + "extension": { + "extensions": "Extensões", + "selected-extensions": "{ count, plural, 1 {1 extension} other {# extensions} } selecionado(s)", + "type": "Tipo", + "key": "Chave", + "value": "Valor", + "id": "ID", + "extension-id": "ID da extensão", + "extension-type": "Tipo de extensão", + "transformer-json": "JSON *", + "unique-id-required": "O ID de extensão atual já existe.", + "delete": "Excluir extensão", + "add": "Adicionar extensão", + "edit": "Editar extensão", + "delete-extension-title": "Tem certeza de que deseja excluir a extensão '{{extensionId}}'?", + "delete-extension-text": "Cuidado, após confirmar, não será possível recuperar a extensão e nenhum dado associado.", + "delete-extensions-title": "Tem certeza de que deseja excluir { count, plural, 1 {1 extension} other {# extensions} }?", + "delete-extensions-text": "Cuidado, após confirmar, todas as extensões selecionadas serão removidas.", + "converters": "Conversores", + "converter-id": "ID do conversor", + "configuration": "Configuração", + "converter-configurations": "Configurações de conversores", + "token": "Token de segurança", + "add-converter": "Adicionar conversor", + "add-config": "Adicionar configuração de conversor", + "device-name-expression": "Expressão do nome do dispositivo", + "device-type-expression": "Expressão do tipo de dispositivo", + "custom": "Personalizado", + "to-double": "Duplicar", + "transformer": "Transformador", + "json-required": "O json do transformador é obrigatório", + "json-parse": "Impossível analisar json do transformador", + "attributes": "Atributos", + "add-attribute": "Adicionar atributo", + "add-map": "Adicionar elemento de mapeamento", + "timeseries": "Intervalos de tempo", + "add-timeseries": "Adicionar intervalos de tempo", + "field-required": "O campo é obrigatório", + "brokers": "Brokers", + "add-broker": "Adicionar broker", + "host": "Host", + "port": "Porta", + "port-range": "A porta deve estar em um intervalo de 1 a 65535.", + "ssl": "Ssl", + "credentials": "Credenciais", + "username": "Nome de usuário", + "password": "Senha", + "retry-interval": "Intervalo de repetição em milissegundos", + "anonymous": "Anônimo", + "basic": "Básico", + "pem": "PEM", + "ca-cert": "Arquivo de certificado da Autoridade de Certificação *", + "private-key": "Arquivo de chave privada *", + "cert": "Arquivo de certificado *", + "no-file": "Nenhum arquivo selecionado.", + "drop-file": "Solte um arquivo ou clique para selecionar um arquivo para carregar.", + "mapping": "Mapeamento", + "topic-filter": "Filtro de tópico", + "converter-type": "Tipo de conversor", + "converter-json": "Json", + "json-name-expression": "Expressão json do nome do dispositivo", + "topic-name-expression": "Expressão de tópico do nome do dispositivo", + "json-type-expression": "Expressão json do tipo de dispositivo", + "topic-type-expression": "Expressão de tópico do tipo de dispositivo", + "attribute-key-expression": "Expressão da chave de atributo", + "attr-json-key-expression": "Expressão json da chave de atributo", + "attr-topic-key-expression": "Expressão de tópico da chave de atributo", + "request-id-expression": "Expressão de ID da solicitação", + "request-id-json-expression": "Expressão json de ID da solicitação", + "request-id-topic-expression": "Expressão de tópico de ID da solicitação", + "response-topic-expression": "Expressão de tópico de resposta", + "value-expression": "Expressão do valor", + "topic": "Tópico", + "timeout": "Tempo limite em milissegundos", + "converter-json-required": "Json do conversor é obrigatório.", + "converter-json-parse": "Impossível analisar json do conversor", + "filter-expression": "Expressão do filtro", + "connect-requests": "Solicitações de conexão", + "add-connect-request": "Adicionar solicitação de conexão", + "disconnect-requests": "Solicitações de desconexão", + "add-disconnect-request": "Adicionar solicitação de desconexão", + "attribute-requests": "Solicitações de atributo", + "add-attribute-request": "Adicionar solicitação de atributo", + "attribute-updates": "Atualizações de atributos", + "add-attribute-update": "Adicionar atualização de atributo", + "server-side-rpc": "RPC no lado do servidor", + "add-server-side-rpc-request": "Adicionar solicitação de RPC no lado do servidor", + "device-name-filter": "Filtro do nome do dispositivo", + "attribute-filter": "Filtro de atributo", + "method-filter": "Filtro de método", + "request-topic-expression": "Expressão de tópico de solicitação", + "response-timeout": "Tempo limite de resposta em milissegundos", + "topic-expression": "Expressão de tópico", + "client-scope": "Escopo de cliente", + "add-device": "Adicionar dispositivo", + "opc-server": "Servidores", + "opc-add-server": "Adicionar servidor", + "opc-add-server-prompt": "Adicione servidor", + "opc-application-name": "Nome do aplicativo", + "opc-application-uri": "Uri do aplicativo", + "opc-scan-period-in-seconds": "Período de digitalização em segundos", + "opc-security": "Segurança", + "opc-identity": "Identidade", + "opc-keystore": "Keystore", + "opc-type": "Tipo", + "opc-keystore-type": "Tipo", + "opc-keystore-location": "Localização*", + "opc-keystore-password": "Senha", + "opc-keystore-alias": "Alias", + "opc-keystore-key-password": "Senha principal", + "opc-device-node-pattern": "Padrão de nó de dispositivo", + "opc-device-name-pattern": "Padrão de nome de dispositivo", + "modbus-server": "Servidores/subordinados", + "modbus-add-server": "Adicionar servidor/subordinado", + "modbus-add-server-prompt": "Adicione servidor/subordinado", + "modbus-transport": "Transporte", + "modbus-tcp-reconnect": "Reconectar automaticamente", + "modbus-rtu-over-tcp": "RTU sobre TCP", + "modbus-port-name": "Nome da porta serial", + "modbus-encoding": "Codificação", + "modbus-parity": "Paridade", + "modbus-baudrate": "Taxa de transmissão", + "modbus-databits": "Bits de dados", + "modbus-stopbits": "Bits de parada", + "modbus-databits-range": "Os bits de dados devem estar em um intervalo de 7 a 8.", + "modbus-stopbits-range": "Os bits de parada devem estar em um intervalo de 1 a 2.", + "modbus-unit-id": "ID da unidade", + "modbus-unit-id-range": "O ID da unidade deve estar em um intervalo de 1 a 247.", + "modbus-device-name": "Nome do dispositivo", + "modbus-poll-period": "Período de sondagem (ms)", + "modbus-attributes-poll-period": "Atributos do período de sondagem (ms)", + "modbus-timeseries-poll-period": "Intervalos de tempo do período de sondagem (ms)", + "modbus-poll-period-range": "O período de sondagem deve ser um valor positivo.", + "modbus-tag": "Etiqueta", + "modbus-function": "Função", + "modbus-register-address": "Endereço do registro", + "modbus-register-address-range": "O endereço do registro deve estar em um intervalo de 0 a 65535.", + "modbus-register-bit-index": "Índice de bits", + "modbus-register-bit-index-range": "O índice de bits deve estar em um intervalo de 0 a 15.", + "modbus-register-count": "Contagem de registro", + "modbus-register-count-range": "A contagem de registro deve ser um valor positivo.", + "modbus-byte-order": "Ordem de byte", + "sync": { + "status": "Status", + "sync": "Sincronizar", + "not-sync": "Não sincronizar", + "last-sync-time": "Horário da última sincronização", + "not-available": "Não disponível" + }, + "export-extensions-configuration": "Exportar configuração de extensões", + "import-extensions-configuration": "Importar configuração de extensões", + "import-extensions": "Importar extensões", + "import-extension": "Importar extensão", + "export-extension": "Exportar extensão", + "file": "Arquivo de extensões", + "invalid-file-error": "Arquivo de extensão inválido" + }, + "filter": { + "add": "Adicionar filtro", + "edit": "Editar filtro", + "name": "Nome do filtro", + "name-required": "O nome do filtro é obrigatório.", + "duplicate-filter": "Já existe um filtro com o mesmo nome.", + "filters": "Filtros", + "unable-delete-filter-title": "Impossível excluir filtro", + "unable-delete-filter-text": "Não é possível excluir o filtro '{{filter}}' porque ele é usado pelo(s) seguinte(s) widget(s):
    {{widgetsList}}", + "duplicate-filter-error": "Filtro '{{filter}}' duplicado encontrado.
    Os filtros devem ser exclusivos no dashboard.", + "missing-key-filters-error": "Filtros chave ausentes para o filtro '{{filter}}'.", + "filter": "Filtro", + "editable": "Editável", + "no-filters-found": "Nenhum filtro encontrado.", + "no-filter-matching": "'{{filter}}' não encontrado.", + "create-new-filter": "Criar um novo!", + "filter-required": "O filtro é obrigatório.", + "operation": { + "operation": "Operação", + "equal": "igual", + "not-equal": "diferente", + "starts-with": "começa com", + "ends-with": "termina com", + "contains": "contém", + "not-contains": "não contém", + "greater": "maior que", + "less": "menor que", + "greater-or-equal": "maior ou igual", + "less-or-equal": "menor ou igual", + "and": "e", + "or": "ou" + }, + "ignore-case": "Ignorar caixa", + "value": "Valor", + "remove-filter": "Remover filtro", + "no-filters": "Nenhum filtro configurado", + "add-filter": "Adicionar filtro", + "add-complex-filter": "Adicionar filtro complexo", + "add-complex": "Adicionar complexo", + "complex-filter": "Filtro complexo", + "edit-complex-filter": "Editar filtro complexo", + "edit-filter-user-params": "Editar parâmetros de usuário de predicado de filtro", + "user-parameters": "Parâmetros de usuário", + "display-label": "Etiqueta para exibição", + "autogenerated-label": "Gerar etiqueta automaticamente", + "order-priority": "Prioridade de ordem de campo", + "key-filter": "Filtro chave", + "key-filters": "Filtros chave", + "key-name": "Nome da chave", + "key-name-required": "O nome da chave é obrigatório.", + "key-type": { + "key-type": "Tipo de chave", + "attribute": "Atributo", + "timeseries": "Intervalos de tempo", + "entity-field": "Campo de entidade" + }, + "value-type": { + "value-type": "Tipo de valor", + "string": "Cadeia de caracteres", + "numeric": "Numérico", + "boolean": "Booliano", + "date-time": "Data/Hora" + }, + "value-type-required": "O tipo de valor chave é obrigatório.", + "key-value-type-change-title": "Tem certeza de que deseja alterar o tipo de valor chave?", + "key-value-type-change-message": "Se confirmar o novo tipo de valor, todos os filtros chave inseridos serão removidos.", + "no-key-filters": "Nenhum filtro chave configurado", + "add-key-filter": "Adicionar filtro chave", + "remove-key-filter": "Remover filtro chave", + "edit-key-filter": "Editar filtro chave", + "date": "Data", + "time": "Hora", + "current-tenant": "Locatário atual", + "current-customer": "Cliente atual", + "current-user": "Usuário atual", + "default-value": "Valor padrão", + "dynamic-source-type": "Tipo de fonte dinâmica", + "no-dynamic-value": "Nenhum valor dinâmico", + "source-attribute": "Atributo da fonte", + "switch-to-dynamic-value": "Alternar para valor dinâmico", + "switch-to-default-value": "Alternar para valor padrão" + }, + "fullscreen": { + "expand": "Expandir para tela inteira", + "exit": "Sair de tela inteira", + "toggle": "Alternar modo de tela inteira", + "fullscreen": "Tela inteira" + }, + "function": { + "function": "Função" + }, + "gateway": { + "add-entry": "Adicionar configuração", + "connector-add": "Adicionar novo conector", + "connector-enabled": "Habilitar conector", + "connector-name": "Nome do conector", + "connector-name-required": "O nome do conector é obrigatório.", + "connector-type": "Tipo de conector", + "connector-type-required": "O tipo de conector é obrigatório.", + "connectors": "Configuração de conectores", + "create-new-gateway": "Criar um novo gateway", + "create-new-gateway-text": "Tem certeza de que deseja criar um novo gateway com o nome: '{{gatewayName}}'?", + "delete": "Excluir configuração", + "download-tip": "Download de arquivo de configuração", + "gateway": "Gateway", + "gateway-exists": "Já existe um dispositivo com o mesmo nome.", + "gateway-name": "Nome do gateway", + "gateway-name-required": "O nome do gateway é obrigatório.", + "gateway-saved": "A configuração do gateway foi salva corretamente.", + "json-parse": "JSON inválido.", + "json-required": "O campo não pode estar em branco.", + "no-connectors": "Sem conectores", + "no-data": "Sem configurações", + "no-gateway-found": "Nenhum gateway encontrado.", + "no-gateway-matching": " '{{item}}' não encontrado.", + "path-logs": "Caminho para arquivos de log", + "path-logs-required": "O caminho é obrigatório", + "remote": "Configuração remota", + "remote-logging-level": "Nível de registro em log", + "remove-entry": "Remover configuração", + "save-tip": "Salvar arquivo de configuração", + "security-type": "Tipo de segurança", + "security-types": { + "access-token": "Token de Acesso", + "tls": "TLS" + }, + "storage": "Armazenamento", + "storage-max-file-records": "Número máximo de registros em arquivo", + "storage-max-files": "Número máximo de arquivos", + "storage-max-files-min": "O número mínimo é 1.", + "storage-max-files-pattern": "O número não é válido.", + "storage-max-files-required": "O número é obrigatório.", + "storage-max-records": "Número máximo de registros em armazenamento", + "storage-max-records-min": "O número mínimo de registros é 1.", + "storage-max-records-pattern": "O número não é válido.", + "storage-max-records-required": "O número máximo de registros é obrigatório.", + "storage-pack-size": "Tamanho máximo de pacote de eventos", + "storage-pack-size-min": "O número mínimo é 1.", + "storage-pack-size-pattern": "O número não é válido.", + "storage-pack-size-required": "O tamanho máximo de pacote de eventos é obrigatório.", + "storage-path": "Caminho de armazenamento", + "storage-path-required": "O caminho de armazenamento é obrigatório.", + "storage-type": "Tipo de armazenamento", + "storage-types": { + "file-storage": "Armazenamento de arquivo", + "memory-storage": "Armazenamento de memória" + }, + "thingsboard": "ThingsBoard", + "thingsboard-host": "Host ThingsBoard", + "thingsboard-host-required": "O host é obrigatório.", + "thingsboard-port": "Porta ThingsBoard", + "thingsboard-port-max": "O número máximo de portas é 65535.", + "thingsboard-port-min": "O número mínimo de portas é 1.", + "thingsboard-port-pattern": "A porta não é válida.", + "thingsboard-port-required": "A porta é obrigatória.", + "tidy": "Tidy", + "tidy-tip": "Config Tidy JSON", + "title-connectors-json": "Configuração do conector {{typeName}}", + "tls-path-ca-certificate": "Caminho para certificado de Autoridade de Certificação no gateway", + "tls-path-client-certificate": "Caminho para certificado de cliente no gateway", + "tls-path-private-key": "Caminho para chave privada no gateway", + "toggle-fullscreen": "Alternar tela inteira", + "transformer-json-config": "Configuração JSON*", + "update-config": "Adicionar/atualizar configuração de JSON" + }, + "grid": { + "delete-item-title": "Tem certeza de que deseja excluir este item?", + "delete-item-text": "Cuidado, após confirmar, não será possível recuperar este item e nenhum dado associado.", + "delete-items-title": "Tem certeza de que deseja excluir { count, plural, 1 {1 item} other {# items} }?", + "delete-items-action-title": "Excluir { count, plural, 1 {1 item} other {# items} }", + "delete-items-text": "Cuidado, após confirmar, todos os itens selecionados serão removidos e não será possível recuperar nenhum dado associado.", + "add-item-text": "Adicionar novo item", + "no-items-text": "Nenhum item encontrado", + "item-details": "Detalhes do item", + "delete-item": "Excluir item", + "delete-items": "Excluir itens", + "scroll-to-top": "Rolar para o início" + }, + "help": { + "goto-help-page": "Ir para a página de Ajuda" + }, + "home": { + "home": "Página Inicial", + "profile": "Perfil", + "logout": "Logout", + "menu": "Menu", + "avatar": "Avatar", + "open-user-menu": "Abrir menu de usuário" + }, + "import": { + "no-file": "Nenhum arquivo selecionado", + "drop-file": "Solte um arquivo JSON ou clique para selecionar um arquivo para carregar.", + "drop-file-csv": "Solte um arquivo CSV ou clique para selecionar um arquivo para carregar.", + "column-value": "Valor", + "column-title": "Título", + "column-example": "Exemplo de dados de valor", + "column-key": "Chave de atributo/telemetria", + "csv-delimiter": "Delimitador de CSV", + "csv-first-line-header": "A primeira linha contém nomes de coluna", + "csv-update-data": "Atualizar atributos/telemetria", + "import-csv-number-columns-error": "Um arquivo deve conter no mínimo duas colunas", + "import-csv-invalid-format-error": "Formato de arquivo inválido. Linha: '{{line}}'", + "column-type": { + "name": "Nome", + "type": "Tipo", + "label": "Etiqueta", + "column-type": "Tipo de coluna", + "client-attribute": "Atributo do cliente", + "shared-attribute": "Atributo compartilhado", + "server-attribute": "Atributo do servidor", + "timeseries": "Intervalos de tempo", + "entity-field": "Campo de entidade", + "access-token": "Token de acesso", + "isgateway": "É Gateway", + "description": "Descrição" + }, + "stepper-text": { + "select-file": "Selecionar um arquivo", + "configuration": "Importar configuração", + "column-type": "Selecionar tipos de colunas", + "creat-entities": "Criar novas entidades", + "done": "Concluído" + }, + "message": { + "create-entities": "{{count}} novas entidades foram criadas corretamente.", + "update-entities": "{{count}} novas entidades foram atualizadas corretamente.", + "error-entities": "Ocorreu um erro ao criar {{count}} entidades." + } + }, + "item": { + "selected": "Selecionado" + }, + "js-func": { + "no-return-error": "A função deve retornar um valor!", + "return-type-mismatch": "A função deve retornar um valor do tipo '{{type}}'!", + "tidy": "Tidy", + "mini": "Mini" + }, + "key-val": { + "key": "Chave", + "value": "Valor", + "remove-entry": "Remover entrada", + "add-entry": "Adicionar entrada", + "no-data": "Sem entradas" + }, + "layout": { + "layout": "Layout", + "manage": "Gerenciar layouts", + "settings": "Configuração de layout", + "color": "Cor", + "main": "Principal", + "right": "Direita", + "select": "Selecionar layout alvo" + }, + "legend": { + "direction": "Direção da legenda", + "position": "Posição da legenda", + "show-max": "Mostrar valor máximo", + "show-min": "Mostrar valor mínimo", + "show-avg": "Mostrar valor médio", + "show-total": "Mostrar valor total", + "settings": "Configuração de legenda", + "min": "mín.", + "max": "máx.", + "avg": "méd.", + "total": "total", + "comparison-time-ago": { + "days": "(dia atrás)", + "weeks": "(semana atrás)", + "months": "(mês atrás)", + "years": "(ano atrás)" + } + }, + "login": { + "login": "Login", + "request-password-reset": "Solicitar redefinição de senha", + "reset-password": "Redefinir senha", + "create-password": "Criar senha", + "passwords-mismatch-error": "As senhas inseridas devem ser idênticas!", + "password-again": "Repetir senha", + "sign-in": "Inscreva-se", + "username": "Nome de usuário (e-mail)", + "remember-me": "Lembrar-me", + "forgot-password": "Esqueceu a senha?", + "password-reset": "Redefinir senha", + "expired-password-reset-message": "As credenciais de expiraram! Crie uma nova senha.", + "new-password": "Nova senha", + "new-password-again": "Repetir nova senha", + "password-link-sent-message": "O link de redefinição de senha foi enviado corretamente!", + "email": "E-mail", + "login-with": "Login com {{name}}", + "or": "ou", + "error": "Erro de login" + }, + "position": { + "top": "Acima", + "bottom": "Abaixo", + "left": "Esquerda", + "right": "Direita" + }, + "profile": { + "profile": "Perfil", + "last-login-time": "Último login", + "change-password": "Modificar senha", + "current-password": "Senha atual" + }, + "relation": { + "relations": "Relações", + "direction": "Direção", + "search-direction": { + "FROM": "De", + "TO": "Para" + }, + "direction-type": { + "FROM": "de", + "TO": "para" + }, + "from-relations": "Relações de saída", + "to-relations": "Relações de entrada", + "selected-relations": "{ count, plural, 1 {1 relation} other {# relations} } selecionado(s)", + "type": "Tipo", + "to-entity-type": "Para tipo de entidade", + "to-entity-name": "Para nome de entidade", + "from-entity-type": "De tipo de entidade", + "from-entity-name": "De nome de entidade", + "to-entity": "Para entidade", + "from-entity": "De entidade", + "delete": "Excluir relação", + "relation-type": "Tipo de relação", + "relation-type-required": "O tipo de relação é obrigatório.", + "any-relation-type": "Qualquer tipo", + "add": "Adicionar relação", + "edit": "Editar relação", + "delete-to-relation-title": "Tem certeza de que deseja excluir a relação com a entidade '{{entityName}}'?", + "delete-to-relation-text": "Cuidado, após confirmar, a relação da entidade '{{entityName}}' com a entidade atual será anulada.", + "delete-to-relations-title": "Tem certeza de que deseja excluir { count, plural, 1 {1 relation} other {# relations} }?", + "delete-to-relations-text": "Cuidado, após confirmar, todas as relações serão removidas e a relação das entidades correspondentes com a entidade atual será anulada.", + "delete-from-relation-title": "Tem certeza de que deseja excluir a relação da entidade '{{entityName}}'?", + "delete-from-relation-text": "Cuidado, após confirmar, a relação da entidade atual com a entidade '{{entityName}}' será anulada.", + "delete-from-relations-title": "Tem certeza de que deseja excluir { count, plural, 1 {1 relation} other {# relations} }?", + "delete-from-relations-text": "Cuidado, após confirmar, todas as relações serão removidas e a relação da entidade atual com as entidades correspondentes será anulada.", + "remove-relation-filter": "Remover filtro de relação", + "add-relation-filter": "Adicionar filtro de relação", + "any-relation": "Qualquer relação", + "relation-filters": "Filtros de relação", + "additional-info": "Informações adicionais (JSON)", + "invalid-additional-info": "Impossível analisar informações adicionais de json", + "no-relations-text": "Nenhuma relação encontrada" + }, + "rulechain": { + "rulechain": "Cadeia de regras", + "rulechains": "Cadeias de regras", + "root": "Raiz", + "delete": "Excluir cadeia de regras", + "name": "Nome", + "name-required": "O nome é obrigatório.", + "description": "Descrição", + "add": "Adicionar cadeia de regras", + "set-root": "Tornar cadeia de regras em raiz", + "set-root-rulechain-title": "Tem certeza de que deseja tornar a cadeia de regras '{{ruleChainName}}' em raiz?", + "set-root-rulechain-text": "Após confirmar, a cadeia de regras se tornará em raiz e irá tratar todas as mensagens de transporte recebidas.", + "delete-rulechain-title": "Tem certeza de que deseja excluir a cadeia de regras '{{ruleChainName}}'?", + "delete-rulechain-text": "Cuidado, após confirmar, não será possível recuperar a cadeia de regras e todos os dados associados.", + "delete-rulechains-title": "Tem certeza de que deseja excluir { count, plural, 1 {1 rule chain} other {# rule chains} }?", + "delete-rulechains-action-title": "Excluir { count, plural, 1 {1 rule chain} other {# rule chains} }", + "delete-rulechains-text": "Cuidado, após confirmar, todas as cadeias de regras serão removidas e não será possível recuperar nenhum dado associado.", + "add-rulechain-text": "Adicionar nova cadeia de regras", + "no-rulechains-text": "Nenhuma cadeia de regras encontrada", + "rulechain-details": "Detalhes de cadeia de regras", + "details": "Detalhes", + "events": "Eventos", + "system": "Sistema", + "import": "Importar cadeia de regras", + "export": "Exportar cadeia de regras", + "export-failed-error": "Impossível exportar cadeia de regras: {{error}}", + "create-new-rulechain": "Criar nova cadeia de regras", + "rulechain-file": "Arquivo de cadeia de regras", + "invalid-rulechain-file-error": "Impossível importar cadeia de regras: Estrutura de dados de cadeia de regras inválida.", + "copyId": "Copiar ID de cadeia de regras", + "idCopiedMessage": "O ID da cadeia de regras foi copiado para a área de transferência", + "select-rulechain": "Selecionar cadeia de regras", + "no-rulechains-matching": "Nenhuma cadeia de regras encontrada que coincida com '{{entity}}'.", + "rulechain-required": "A cadeia de regras é obrigatória", + "management": "Gerenciamento de regras", + "debug-mode": "Modo de depuração", + "search": "Pesquisar cadeias de regras", + "selected-rulechains": "{ count, plural, 1 {1 rule chain} other {# rule chains} } selecionada(s)", + "open-rulechain": "Abrir cadeia de regras" + }, + "rulenode": { + "details": "Detalhes", + "events": "Eventos", + "search": "Pesquisar nós", + "open-node-library": "Abrir biblioteca de nós", + "add": "Adicionar nó de regra", + "name": "Nome", + "name-required": "O nome é obrigatório.", + "type": "Tipo", + "description": "Descrição", + "delete": "Excluir nó de regras", + "select-all-objects": "Selecionar todos os nós e conexões", + "deselect-all-objects": "Desmarcar todos os nós e conexões", + "delete-selected-objects": "Excluir nós e conexões selecionados", + "delete-selected": "Excluir selecionado", + "select-all": "Selecionar tudo", + "copy-selected": "Copiar selecionado(s)", + "deselect-all": "Desmarcar tudo", + "rulenode-details": "Detalhes do nó de regras", + "debug-mode": "Modo de depuração", + "configuration": "Configuração", + "link": "Link", + "link-details": "Detalhes do link do nó de regras", + "add-link": "Adicionar link", + "link-label": "Etiqueta de link", + "link-label-required": "A etiqueta de link é obrigatória.", + "custom-link-label": "Etiqueta de link personalizada", + "custom-link-label-required": "A etiqueta de link personalizada é obrigatória.", + "link-labels": "Etiquetas de links", + "link-labels-required": "As etiquetas de links são obrigatórias.", + "no-link-labels-found": "Nenhuma etiqueta de link encontrada", + "no-link-label-matching": "'{{label}}' não encontrado.", + "create-new-link-label": "Criar um novo!", + "type-filter": "Filtrar", + "type-filter-details": "Filtrar mensagens recebidas com condições configuradas", + "type-enrichment": "Enriquecimento", + "type-enrichment-details": "Adicionar informações adicionais aos Metadados de mensagens", + "type-transformation": "Transformação", + "type-transformation-details": "Alterar payload e metadados de mensagens", + "type-action": "Ação", + "type-action-details": "Executar ação especial", + "type-external": "Externo", + "type-external-details": "Interage com sistema externo", + "type-rule-chain": "Cadeia de Regras", + "type-rule-chain-details": "Encaminha mensagens recebidas para cadeia de regras especificada", + "type-input": "Entrada", + "type-input-details": "Entrada lógica de cadeia de regras, encaminha mensagens recebidas para nó de regras associado", + "type-unknown": "Desconhecido", + "type-unknown-details": "Nó de regras não resolvido", + "directive-is-not-loaded": "Diretriz de configuração '{{directiveName}}' definida não está disponível.", + "ui-resources-load-error": "Erro ao carregar configuração de recursos de interface de usuário.", + "invalid-target-rulechain": "Impossível resolver cadeia de regras alvo!", + "test-script-function": "Testar funcionamento de script", + "message": "Mensagem", + "message-type": "Tipo de mensagem", + "select-message-type": "Selecionar tipo de mensagem", + "message-type-required": "O tipo de mensagem é obrigatório", + "metadata": "Metadados", + "metadata-required": "As entradas de metadados não podem estar em branco.", + "output": "Saída", + "test": "Teste", + "help": "Ajuda", + "reset-debug-mode": "Redefinir modo de depuração em todos os nós" + }, + "timezone": { + "timezone": "Fuso horário", + "select-timezone": "Selecionar fuso horário", + "no-timezones-matching": "Nenhum fuso horário encontrado que coincida com '{{timezone}}'.", + "timezone-required": "O fuso horário é obrigatório." + }, + "queue": { + "select_name": "Selecionar nome de fila", + "name": "Nome de Fila", + "name_required": "O nome de fila é obrigatório!" + }, + "tenant": { + "tenant": "Locatário", + "tenants": "Locatários", + "management": "Gerenciamento de locatários", + "add": "Adicionar Locatário", + "admins": "Administradores", + "manage-tenant-admins": "Gerenciar administradores de locatários", + "delete": "Excluir locatário", + "add-tenant-text": "Adicionar novo locatário", + "no-tenants-text": "Nenhum locatário encontrado", + "tenant-details": "Detalhes do locatário", + "delete-tenant-title": "Tem certeza de que deseja excluir o locatário '{{tenantTitle}}'?", + "delete-tenant-text": "Cuidado, após confirmar, não será possível recuperar o locatário e nenhum dado associado.", + "delete-tenants-title": "Tem certeza de que deseja excluir { count, plural, 1 {1 tenant} other {# tenants} }?", + "delete-tenants-action-title": "Excluir { count, plural, 1 {1 tenant} other {# tenants} }", + "delete-tenants-text": "Cuidado, após confirmar, todos os locatários selecionados serão removidos e não será possível recuperar nenhum dado associado.", + "title": "Título", + "title-required": "O título é obrigatório.", + "description": "Descrição", + "details": "Detalhes", + "events": "Eventos", + "copyId": "Copiar ID do locatário", + "idCopiedMessage": "O ID do locatário foi copiado para a área de transferência", + "select-tenant": "Selecionar locatário", + "no-tenants-matching": "Nenhum locatário encontrado que coincida com '{{entity}}'.", + "tenant-required": "O locatário é obrigatório", + "search": "Pesquisar locatários", + "selected-tenants": "{ count, plural, 1 {1 tenant} other {# tenants} } selecionado(s)", + "isolated-tb-core": "Processamento em contêiner isolado do ThingsBoard Core", + "isolated-tb-rule-engine": "Processamento em contêiner isolado do ThingsBoard Rule Engine", + "isolated-tb-core-details": "Exige microsserviço(s) separado(s) para cada locatário isolado", + "isolated-tb-rule-engine-details": "Exige microsserviço(s) separado(s) para cada locatário isolado" + }, + "timeinterval": { + "seconds-interval": "{ seconds, plural, 1 {1 second} other {# seconds} }", + "minutes-interval": "{ minutes, plural, 1 {1 minute} other {# minutes} }", + "hours-interval": "{ hours, plural, 1 {1 hour} other {# hours} }", + "days-interval": "{ days, plural, 1 {1 day} other {# days} }", + "days": "Dias", + "hours": "Horas", + "minutes": "Minutos", + "seconds": "Segundos", + "advanced": "Avançado" + }, + "timewindow": { + "days": "{ days, plural, 1 { day } other {# days } }", + "hours": "{ hours, plural, 0 { hour } 1 {1 hour } other {# hours } }", + "minutes": "{ minutes, plural, 0 { minute } 1 {1 minute } other {# minutes } }", + "seconds": "{ seconds, plural, 0 { second } 1 {1 second } other {# seconds } }", + "realtime": "Tempo real", + "history": "Histórico", + "last-prefix": "último", + "period": "de {{ startTime }} até {{ endTime }}", + "edit": "Editar janela de tempo", + "date-range": "Intervalo de datas", + "last": "Última", + "time-period": "Período de tempo", + "hide": "Ocultar" + }, + "user": { + "user": "Usuário", + "users": "Usuários", + "customer-users": "Usuários do cliente", + "tenant-admins": "Administradores de locatários", + "sys-admin": "Administrador do sistema", + "tenant-admin": "Administrador de locatários", + "customer": "Cliente", + "anonymous": "Anônimo", + "add": "Adicionar usuário", + "delete": "Excluir usuário", + "add-user-text": "Adicionar novo usuário", + "no-users-text": "Nenhum usuário encontrado", + "user-details": "Detalhes do usuário", + "delete-user-title": "Tem certeza de que deseja excluir o usuário '{{userEmail}}'?", + "delete-user-text": "Cuidado, após confirmar, não será possível recuperar o usuário e nenhum dado associado.", + "delete-users-title": "Tem certeza de que deseja excluir { count, plural, 1 {1 user} other {# users} }?", + "delete-users-action-title": "Excluir { count, plural, 1 {1 user} other {# users} }", + "delete-users-text": "Cuidado, após confirmar, todos os usuários selecionados serão removidos e não será possível recuperar nenhum dado associado.", + "activation-email-sent-message": "O e-mail de ativação foi enviado corretamente!", + "resend-activation": "Reenviar ativação", + "email": "E-mail", + "email-required": "O e-mail é obrigatório.", + "invalid-email-format": "Formato de e-mail inválido.", + "first-name": "Nome", + "last-name": "Sobrenome", + "description": "Descrição", + "default-dashboard": "Dashboard padrão", + "always-fullscreen": "Sempre tela inteira", + "select-user": "Selecionar usuário", + "no-users-matching": "Nenhum usuário encontrado que coincida com '{{entity}}'.", + "user-required": "O usuário é obrigatório", + "activation-method": "Método de ativação", + "display-activation-link": "Exibir link de ativação", + "send-activation-mail": "Enviar e-mail de ativação", + "activation-link": "Link de ativação de usuário", + "activation-link-text": "Para ativar o usuário, utilize o seguinte link de ativação:", + "copy-activation-link": "Copiar link de ativação", + "activation-link-copied-message": "O link de ativação foi copiado para a área de transferência", + "details": "Detalhes", + "login-as-tenant-admin": "Login como administrador de locatários", + "login-as-customer-user": "Login como usuário do cliente", + "search": "Pesquisar usuários", + "selected-users": "{ count, plural, 1 {1 user} other {# users} } selecionado(s)", + "disable-account": "Desativar conta de usuário", + "enable-account": "Ativar conta de usuário", + "enable-account-message": "A conta de usuário foi ativada corretamente!", + "disable-account-message": "A conta de usuário foi desativada corretamente!" + }, + "value": { + "type": "Tipo de valor", + "string": "Cadeia de caracteres", + "string-value": "Valor da cadeia de caracteres", + "string-value-required": "O valor da cadeia de caracteres é obrigatório", + "integer": "Número inteiro", + "integer-value": "Valor do número inteiro", + "integer-value-required": "O valor do número inteiro é obrigatório", + "invalid-integer-value": "Valor de número inteiro inválido", + "double": "Duplo", + "double-value": "Valor duplo", + "double-value-required": "O valor duplo de caracteres é obrigatório", + "boolean": "Booliano", + "boolean-value": "Valor booliano", + "false": "Falso", + "true": "Verdadeiro", + "long": "Longo", + "json": "JSON", + "json-value": "Valor de JSON", + "json-value-invalid": "O formato do valor de JSON é inválido", + "json-value-required": "O valor de JSON é obrigatório." + }, + "widget": { + "widget-library": "Biblioteca de widgets", + "widget-bundle": "Pacote de widgets", + "select-widgets-bundle": "Selecionar pacote de widgets", + "management": "Gerenciamento de widgets", + "editor": "Editor de widgets", + "widget-type-not-found": "Problema ao carregar configuração de widget.
    Provavelmente\n o tipo de widget foi removido.", + "widget-type-load-error": "O widget não foi carregado devido aos seguintes erros:", + "remove": "Remover widget", + "edit": "Editar widget", + "remove-widget-title": "Tem certeza de que deseja remover o widget '{{widgetTitle}}'?", + "remove-widget-text": "Após confirmar, não será possível recuperar o widget e nenhum dado associado.", + "timeseries": "Intervalos de tempo", + "search-data": "Pesquisar dados", + "no-data-found": "Nenhum dado encontrado", + "latest-values": "Últimos valores", + "rpc": "Widget de controle", + "alarm": "Widget de alarme", + "static": "Widget estático", + "select-widget-type": "Selecionar tipo de widget", + "missing-widget-title-error": "É necessário especificar o título de widget!", + "widget-saved": "Widget salvo", + "unable-to-save-widget-error": "Impossível salvar widget! O widget tem erros!", + "save": "Salvar widget", + "saveAs": "Salvar widget como", + "save-widget-type-as": "Salvar tipo de widget como", + "save-widget-type-as-text": "Inserir novo título de widget e/ou selecionar pacote de widgets alvo", + "toggle-fullscreen": "Alternar tela inteira", + "run": "Executar widget", + "title": "Título de widget", + "title-required": "O título do widget é obrigatório.", + "type": "Tipo de widget", + "resources": "Recursos", + "resource-url": "URL de JavaScript/CSS", + "resource-is-module": "É módulo", + "remove-resource": "Remover recurso", + "add-resource": "Adicionar recurso", + "html": "HTML", + "tidy": "Tidy", + "css": "CSS", + "settings-schema": "Esquema de configuração", + "datakey-settings-schema": "Esquema de configuração de chave de dados", + "javascript": "Javascript", + "js": "JS", + "remove-widget-type-title": "Tem certeza de que deseja remover o tipo de widget '{{widgetName}}'?", + "remove-widget-type-text": "Após confirmar, não será possível recuperar o tipo de widget e nenhum dado associado.", + "remove-widget-type": "Remover tipo de widget", + "add-widget-type": "Adicionar novo tipo de widget", + "widget-type-load-failed-error": "Erro ao carregar tipo de widget!", + "widget-template-load-failed-error": "Erro ao carregar modelo de widget!", + "add": "Adicionar widget", + "undo": "Desfazer alterações de widget", + "export": "Exportar widget", + "no-data": "Nenhum dado para exibição em widget", + "data-overflow": "O widget exibe {{count}} de um total de {{total}} entidades", + "alarm-data-overflow": "O widget exibe alarmes para (no máximo) {{allowedEntities}} entidades de um total de {{totalEntities}} entidades" + }, + "widget-action": { + "header-button": "Botão de cabeçalho de widget", + "open-dashboard-state": "Navegar para novo estado de dashboard", + "update-dashboard-state": "Atualizar estado de dashboard atual", + "open-dashboard": "Navegar para outro dashboard", + "custom": "Personalizar ação", + "custom-pretty": "Personalizar ação (com modelo de HTML)", + "target-dashboard-state": "Estado de dashboard alvo", + "target-dashboard-state-required": "O estado do dashboard alvo é obrigatório", + "set-entity-from-widget": "Definir entidade em widget", + "target-dashboard": "Dashboard alvo", + "open-right-layout": "Abrir layout do dashboard à direita (exibição móvel)" + }, + "widgets-bundle": { + "current": "Pacote atual", + "widgets-bundles": "Pacotes de widgets", + "add": "Adicionar pacote de widgets", + "delete": "Excluir pacote de widgets", + "title": "Título", + "title-required": "O título é obrigatório.", + "add-widgets-bundle-text": "Adicionar novo pacote de widgets", + "no-widgets-bundles-text": "Nenhum pacote de widgets encontrado", + "empty": "O pacote de widgets está vazio", + "details": "Detalhes", + "widgets-bundle-details": "Detalhes do pacote de widgets", + "delete-widgets-bundle-title": "Tem certeza de que deseja excluir o pacote de widgets '{{widgetsBundleTitle}}'?", + "delete-widgets-bundle-text": "Cuidado, após confirmar, não será possível recuperar o pacote de widgets e nenhum dado associado.", + "delete-widgets-bundles-title": "Tem certeza de que deseja excluir { count, plural, 1 {1 widgets bundle} other {# widgets bundles} }?", + "delete-widgets-bundles-action-title": "Excluir { count, plural, 1 {1 widgets bundle} other {# widgets bundles} }", + "delete-widgets-bundles-text": "Cuidado, após confirmar, todos os pacotes de widgets selecionados serão removidos e não será possível recuperar nenhum dado associado.", + "no-widgets-bundles-matching": "Nenhum conversor de pacotes de widgets encontrado que coincida com '{{widgetsBundle}}'.", + "widgets-bundle-required": "O pacote de widgets é obrigatório.", + "system": "Sistema", + "import": "Importar pacote de widgets", + "export": "Exportar pacote de widgets", + "export-failed-error": "Impossível exportar pacote de widgets: {{error}}", + "create-new-widgets-bundle": "Criar novo pacote de widgets", + "widgets-bundle-file": "Arquivo de pacote de widgets", + "invalid-widgets-bundle-file-error": "Impossível importar pacote de widgets: Estrutura de dados de pacote de widgets inválida.", + "search": "Pesquisar pacotes de widgets", + "selected-widgets-bundles": "{ count, plural, 1 {1 widgets bundle} other {# widgets bundles} } selecionado(s)", + "open-widgets-bundle": "Abrir pacote de widgets" + }, + "widget-config": { + "data": "Dados", + "settings": "Configurações", + "advanced": "Avançado", + "title": "Título", + "title-tooltip": "Título da dica de ferramenta", + "general-settings": "Configuração geral", + "display-title": "Exibir título", + "drop-shadow": "Sombra projetada", + "enable-fullscreen": "Habilitar tela inteira", + "background-color": "Cor de fundo", + "text-color": "Cor de texto", + "padding": "Preenchimento", + "margin": "Margem", + "widget-style": "Estilo de widget", + "title-style": "Estilo do título", + "mobile-mode-settings": "Configuração do modo móvel", + "order": "Ordem", + "height": "Altura", + "units": "Símbolo especial a ser exibido ao lado do valor", + "decimals": "Número de dígitos após ponto de flutuação", + "timewindow": "Janela de tempo", + "use-dashboard-timewindow": "Usar janela de tempo do dashboard", + "display-timewindow": "Exibir timewindow", + "display-legend": "Exibir legenda", + "datasources": "Fontes de dados", + "maximum-datasources": "No máximo { count, plural, 1 {1 datasource is allowed.} other {# datasources are allowed} }", + "datasource-type": "Tipo", + "datasource-parameters": "Parâmetros", + "remove-datasource": "Remover fonte de dados", + "add-datasource": "Adicionar fonte de dados", + "target-device": "Dispositivo alvo", + "alarm-source": "Fonte do alarme", + "actions": "Ações", + "action": "Ação", + "add-action": "Adicionar ação", + "search-actions": "Pesquisar ações", + "no-actions-text": "Nenhuma ação encontrada", + "action-source": "Fonte da ação", + "action-source-required": "A fonte da ação é obrigatória.", + "action-name": "Nome", + "action-name-required": "O nome da ação é obrigatório!", + "action-name-not-unique": "Já existe outra ação com o mesmo nome.
    O nome da ação na mesma fonte de ação deve ser exclusivo.", + "action-icon": "Ícone", + "action-type": "Tipo", + "action-type-required": "O tipo de ação é obrigatório.", + "edit-action": "Editar ação", + "delete-action": "Excluir ação", + "delete-action-title": "Excluir ação de widget", + "delete-action-text": "Tem certeza de que deseja excluir a ação de widget com o nome '{{actionName}}'?", + "display-icon": "Exibir ícone de título", + "icon-color": "Cor do ícone", + "icon-size": "Tamanho do ícone" + }, + "widget-type": { + "import": "Importar tipo de widget", + "export": "Exportar tipo de widget", + "export-failed-error": "Impossível exportar tipo de widget: {{error}}", + "create-new-widget-type": "Criar novo tipo de widget", + "widget-type-file": "Arquivo de tipo de widget", + "invalid-widget-type-file-error": "Impossível importar tipo de widget: Estrutura de dados de tipo de widget inválida." + }, + "widgets": { + "date-range-navigator": { + "localizationMap": { + "Sun": "Dom", + "Mon": "Seg", + "Tue": "Ter", + "Wed": "Qua", + "Thu": "Qui", + "Fri": "Sex", + "Sat": "Sáb", + "Jan": "Jan", + "Feb": "Fev", + "Mar": "Mar", + "Apr": "Abr", + "May": "Maio", + "Jun": "Jun", + "Jul": "Jul", + "Aug": "Ago", + "Sep": "Set", + "Oct": "Out", + "Nov": "Nov", + "Dec": "Dez", + "January": "Janeiro", + "February": "Fevereiro", + "March": "Março", + "April": "Abril", + "June": "Junho", + "July": "Julho", + "August": "Agosto", + "September": "Setembro", + "October": "Outubro", + "November": "Novembro", + "December": "Dezembro", + "Custom Date Range": "Personalizar intervalo de datas", + "Date Range Template": "Modelo de intervalo de datas", + "Today": "Hoje", + "Yesterday": "Ontem", + "This Week": "Esta semana", + "Last Week": "Semana passada", + "This Month": "Este mês", + "Last Month": "Mês passado", + "Year": "Ano", + "This Year": "Este ano", + "Last Year": "Ano passado", + "Date picker": "Seletor de data", + "Hour": "Hora", + "Day": "Dia", + "Week": "Semana", + "2 weeks": "2 semanas", + "Month": "Mês", + "3 months": "3 meses", + "6 months": "6 meses", + "Custom interval": "Intervalo personalizado", + "Interval": "Intervalo", + "Step size": "Tamanho da etapa", + "Ok": "OK" + } + }, + "input-widgets": { + "attribute-not-allowed": "O parâmetro de atributo não pode ser usado neste widget", + "blocked-location": "A geolocalização está bloqueada no seu navegador", + "claim-device": "Obter dispositivo", + "claim-failed": "Erro ao obter dispositivo!", + "claim-not-found": "Dispositivo não encontrado!", + "claim-successful": "O dispositivo foi obtido corretamente!", + "date": "Data", + "device-name": "Nome do dispositivo", + "device-name-required": "O nome do dispositivo é obrigatório", + "discard-changes": "Descartar alterações", + "entity-attribute-required": "O atributo da entidade é obrigatório", + "entity-coordinate-required": "Os dois campos, latitude e longitude, são obrigatórios", + "entity-timeseries-required": "Os intervalos de tempo de entidade são obrigatórios", + "get-location": "Obter localização atual", + "latitude": "Latitude", + "longitude": "Longitude", + "not-allowed-entity": "A entidade selecionada não pode ter atributos compartilhados", + "no-attribute-selected": "Nenhum atributo selecionado", + "no-datakey-selected": "Nenhuma chave de dados selecionada", + "no-coordinate-specified": "Chave de dados de latitude/longitude não especificada", + "no-entity-selected": "Nenhuma entidade selecionada", + "no-image": "Sem imagem", + "no-support-geolocation": "Seu navegador não é compatível com geolocalização", + "no-support-web-camera": "Nenhuma câmera Web compatível", + "no-timeseries-selected": "Nenhum intervalo de tempo selecionado", + "secret-key": "Chave de segredo", + "secret-key-required": "A chave de segredo é obrigatória", + "switch-attribute-value": "Alternar valor de atributo da entidade", + "switch-camera": "Alternar câmera", + "switch-timeseries-value": "Alternar valor de intervalo de tempo da entidade", + "take-photo": "Tirar foto", + "time": "Hora", + "timeseries-not-allowed": "O parâmetro de intervalo de série não pode ser usado neste widget", + "update-failed": "Atualização falhou", + "update-successful": "Atualização bem-sucedida", + "update-attribute": "Atualizar atributo", + "update-timeseries": "Atualizar intervalos de tempo", + "value": "Valor" + } + }, + "icon": { + "icon": "Ícone", + "select-icon": "Selecionar ícone", + "material-icons": "Ícones de materiais", + "show-all": "Mostrar todos os ícones" + }, + "custom": { + "widget-action": { + "action-cell-button": "Botão da célula de ação", + "row-click": "Clicar em linha", + "polygon-click": "Clique no polígono", + "marker-click": "Clique no marcador", + "tooltip-tag-action": "Ação do rótulo de dica de ferramenta", + "node-selected": "Selecionado no nó", + "element-click": "Clique em elemento de HTML", + "pie-slice-click": "Clique na fatia", + "row-double-click": "Dois clique na fila" + } + }, + "language": { + "language": "Idioma" + } +} diff --git a/ui-ngx/src/assets/locale/locale.constant-ro_RO.json b/ui-ngx/src/assets/locale/locale.constant-ro_RO.json new file mode 100644 index 0000000000..93326e512a --- /dev/null +++ b/ui-ngx/src/assets/locale/locale.constant-ro_RO.json @@ -0,0 +1,1799 @@ +{ + "access": { + "unauthorized": "Neautorizat", + "unauthorized-access": "Acces Neautorizat", + "unauthorized-access-text": "Pentru a accesa această resursă, utilizatorul trebuie să fie identificat", + "access-forbidden": "Acces Interzis", + "access-forbidden-text": "Nu ai drept de acces la această resursă!
    Pentru a obţine accesul, identifică-te cu alt nume de utilizator", + "refresh-token-expired": "Sesiunea a expirat", + "refresh-token-failed": "Sesiunea nu poate fi reîncărcată" + }, + "action": { + "activate": "Activează", + "suspend": "Suspendă", + "save": "Salvează", + "saveAs": "Salvează Cu Alt Nume", + "cancel": "Renunţă", + "ok": "OK", + "delete": "Şterge", + "add": "Adaugă", + "yes": "Da", + "no": "Nu", + "update": "Actualizează", + "remove": "Elimină", + "search": "Caută", + "clear-search": "Resetează Căutarea", + "assign": "Repartizează", + "unassign": "Şterge Repartizarea", + "share": "Partajare", + "make-private": "Declară Privat", + "apply": "Aplică", + "apply-changes": "Aplică Schimbările", + "edit-mode": "Mod Editare", + "enter-edit-mode": "Mod Editare", + "decline-changes": "Refuză Schimbările", + "close": "Închide", + "back": "Înapoi", + "run": "Execută-Rulează", + "sign-in": "Înregistrează Cont Nou", + "edit": "Editează", + "view": "Vizualizează", + "create": "Creează", + "drag": "Trage", + "refresh": "Reactualizează", + "undo": "Anulează Ultima Comandă", + "copy": "Copiere", + "paste": "Lipire", + "copy-reference": "Copiere Referință", + "paste-reference": "Lipire Referință", + "import": "Import", + "export": "Export", + "share-via": "Distribuie prin {{provider}}", + "continue": "Continuă", + "discard-changes": "Anulează Schimbări" + }, + "aggregation": { + "aggregation": "Agregare", + "function": "Funcţie Agregare Date", + "limit": "Valori Maxime", + "group-interval": "Interval Grupare", + "min": "Minim", + "max": "Maxim", + "avg": "Medie", + "sum": "Sumă", + "count": "Numără", + "none": "Nimic" + }, + "admin": { + "general": "General", + "general-settings": "Setări Generale", + "outgoing-mail": " Server eMail", + "outgoing-mail-settings": "Setări Pentru : Outgoing Mail Server", + "system-settings": "Setări Sistem", + "test-mail-sent": "Mesajul de test setări pentru email a fost trimis cu succes", + "base-url": "Adresa De Bază URL", + "base-url-required": "Adresa de bază URL este obligatorie", + "mail-from": "Mesaj eMail de la expeditor", + "mail-from-required": "Adresa eMail a expeditorului este obligatorie", + "smtp-protocol": "Setări Protocol SMTP", + "smtp-host": "Adresă SMTP", + "smtp-host-required": "Adresa SMTP este obligatorie", + "smtp-port": "Port SMTP", + "smtp-port-required": "Trebuie să precizaţi un port SMTP", + "smtp-port-invalid": "Textul introdus nu pare să fie al unui port SMTP", + "timeout-msec": "Timp expirare (milisecunde)", + "timeout-required": "Timpul de expirare este obligatoriu", + "timeout-invalid": "Timpul de expirare nu pare să fie valid", + "enable-tls": "Permite TLS", + "send-test-mail": "Trimite mesaj eMail test", + "security-settings": "Setări Securitate", + "password-policy": "Reguli Pentru Definirea Parolei", + "minimum-password-length": "Numărul Minim De Caractere Al Parolei", + "minimum-password-length-required": "Numărul minim de caractere al parolei este obligatoriu", + "minimum-password-length-range": "Numărul minim de caractere al parolei trebuie să fie între 5 - 50", + "minimum-uppercase-letters": "Numărul Minim De Caractere Scrise Cu MAJUSCULĂ Din Parolă", + "minimum-uppercase-letters-range": "Numărul minim de caractere scrise cu majusculă din parolă nu poate fi negativ", + "minimum-lowercase-letters": "Numărul Minim De Caractere Scrise Cu Literă mică Din Parolă", + "minimum-lowercase-letters-range": "Numărul minim de caractere scrise cu literă mică din parolă nu poate fi negativ", + "minimum-digits": "Numărul Minim De Cifre Din Parolă", + "minimum-digits-range": "Numărul minim de cifre din parolă nu poate fi negativ", + "minimum-special-characters": "Numărul Minim De Caractere Speciale Din Parolă", + "minimum-special-characters-range": "Numărul minim de caractere speciale din parolă nu poate fi negativ", + "password-expiration-period-days": "Perioada de expirare a parolei (zile)", + "password-expiration-period-days-range": "Perioada de expirare a parolei (zile) nu poate fi negativă", + "password-reuse-frequency-days": "Frecvenţa de refolosire a parolei (zile)", + "password-reuse-frequency-days-range": "Frecvenţa de refolosire a parolei(zile) nu poate fi negativă", + "general-policy": "Reguli Generale", + "max-failed-login-attempts": "Numărul maxim de încercări eşuate de accesare a paginii înainte de blocarea contului", + "minimum-max-failed-login-attempts-range": "Numărul maxim de încercări eşuate de accesare a paginii nu poate fi negativ", + "user-lockout-notification-email": "În Cazul Blocării Contului, Trimite eMail De Notificare" + }, + "alarm": { + "alarm": "Alarmă", + "alarms": "Alarme", + "select-alarm": "Selectează Alarmă", + "no-alarms-matching": "Nu au fost găsite alarme pentru '{{entity}}'", + "alarm-required": "Alarma este obligatorie", + "alarm-status": "Stare Alarmă", + "search-status": { + "ANY": "Oricare", + "ACTIVE": "Activă", + "CLEARED": "Ştearsă", + "ACK": "Observată", + "UNACK": "Neobservată" + }, + "display-status": { + "ACTIVE_UNACK": "Activă Neobservată", + "ACTIVE_ACK": "Activă Observată", + "CLEARED_UNACK": "Ștearsă Neobservată", + "CLEARED_ACK": "Ștearsă Observată" + }, + "no-alarms-prompt": "NiciO Alarmă Găsită", + "created-time": "Data Creării", + "type": "Tipul", + "severity": "Urgenţa", + "originator": "Iniţiator", + "originator-type": "Tip Iniţiator", + "details": "Detalii", + "status": "Stare", + "alarm-details": "Detalii Alarmă", + "start-time": "Început", + "end-time": "Sfârşit", + "ack-time": "Data Observării", + "clear-time": "Data Ştergerii", + "severity-critical": "Critică", + "severity-major": "Majoră", + "severity-minor": "Minoră", + "severity-warning": "Avertizare", + "severity-indeterminate": "Nedeterminată", + "acknowledge": "Marchează Observat", + "clear": "Şterge", + "search": "Caută Alarme", + "selected-alarms": "{ count, plural, 1 {o alarmă} other {# alarme} } selectate", + "no-data": "Nu există date de afişat", + "polling-interval": "Interval actualizare alarme (secunde)", + "polling-interval-required": "Intervalul pentru actualizarea alarmelor este obligatoriu", + "min-polling-interval-message": "Valoarea minimă permisă pentru interval actualizare alarme este o secundă", + "aknowledge-alarms-title": "Ai selectat { count, plural, 1 {o alarmă} other {# alarme} }", + "aknowledge-alarms-text": "Sigur vrei să marchezi ca 'Observat' { count, plural, 1 {o alarmă} other {# alarme} }?", + "aknowledge-alarm-title": "Alarmă Observată", + "aknowledge-alarm-text": "Sigur vrei să marchezi alarma ca 'Observat'?", + "clear-alarms-title": "Şterge { count, plural, 1 {o alarmă} other {# alarme} }", + "clear-alarms-text": "Sigur vrei să ștergi { count, plural, 1 {o alarmă} other {# alarme} }?", + "clear-alarm-title": "Şterge Alarma", + "clear-alarm-text": "Sigur vrei să ștergi alarma?", + "alarm-status-filter": "Stare Filtre Alarmă", + "max-count-load": "Număr maxim de alarme înregistrate (0=nelimitat)", + "max-count-load-required": "Numărul maxim de alarme înregistrate este obligatoriu", + "max-count-load-error-min": "Numărul maxim de alarme este 0", + "fetch-size": "Număr alarme afişate", + "fetch-size-required": "Numărul alarmelor afişate este obligatoriu", + "fetch-size-error-min": "Valoarea minimă este 10" + }, + "alias": { + "add": "Adaugă Pseudonim", + "edit": "Editează Pseudonim", + "name": "Denumire Pseudonim", + "name-required": "Denumirea pseudonimului este obligatorie", + "duplicate-alias": "Există deja un pseudonim cu aceeaşi denumire", + "filter-type-single-entity": "O Singură Entitate", + "filter-type-entity-list": "Listă Entităţi", + "filter-type-entity-name": "Nume Entitate", + "filter-type-state-entity": "Entitate din starea panoului", + "filter-type-state-entity-description": "Entitate luată din parametrii stării panoului", + "filter-type-asset-type": "Tip Proprietate", + "filter-type-asset-type-description": "Proprietăţi de tip: '{{assetType}}'", + "filter-type-asset-type-and-name-description": "proprietăţi de tipul: '{{assetType}}' a căror denumire începe cu: '{{prefix}}'", + "filter-type-device-type": "Tip Dispozitiv", + "filter-type-device-type-description": "Dispozitive tip: '{{deviceType}}'", + "filter-type-device-type-and-name-description": "Dispozitive Tip: '{{deviceType}}' a căror denumire începe cu: '{{prefix}}'", + "filter-type-entity-view-type": "Tip entitate definită", + "filter-type-entity-view-type-description": "Tip entități definite: '{{entityView}}'", + "filter-type-entity-view-type-and-name-description": "Entități definite de tip: '{{entityView}}' a căror denumire începe cu : '{{prefix}}'", + "filter-type-relations-query": "Relaţii Interogare", + "filter-type-relations-query-description": "{{entities}} care au relații tip {{relationType}} în direcția {{direction}} {{rootEntity}}", + "filter-type-asset-search-query": "Proprietate Asset search query", + "filter-type-asset-search-query-description": "Proprietăţi de tip {{assetTypes}} care au relații tip {{relationType}} în direcția {{direction}} {{rootEntity}}", + "filter-type-device-search-query": "Criteriu Căutare Dispozitiv", + "filter-type-device-search-query-description": "Dispozitive de tip {{deviceTypes}} care au relații tip {{relationType}} în direcția {{direction}} {{rootEntity}}", + "filter-type-entity-view-search-query": "Criteriu căutare entitate definită", + "filter-type-entity-view-search-query-description": "Entități definite de tip {{entityViewTypes}} care au relații tip {{relationType}} în direcția {{direction}} {{rootEntity}}", + "entity-filter": "Filtru Entitate", + "resolve-multiple": "Rezolvă Ca Entităţi Multiple", + "filter-type": "Tip Filtru", + "filter-type-required": "Tipul filtrului este obligatoriu", + "entity-filter-no-entity-matched": "Nu au fost găsite entităţi corespunzătoare filtrului specificat", + "no-entity-filter-specified": "Nu a fost specificat filtru pentru entitate", + "root-state-entity": "Foloseşte Entitatea Stare Panou Ca Origine", + "root-entity": "Entitate Rădăcină", + "state-entity-parameter-name": "Nume Parametru Stare Entitate", + "default-state-entity": "Stare Entitate Implicită", + "default-entity-parameter-name": "Implicită", + "max-relation-level": "Nivel Maxim Relaţie", + "unlimited-level": "Nivel Nelimitat", + "state-entity": "Entitate Stare Panou", + "all-entities": "Toate Entităţile", + "any-relation": "Oricare" + }, + "asset": { + "asset": "Proprietate", + "assets": "Proprietăţi", + "management": "Administrare Proprietăți", + "view-assets": "Vezi Proprietăţi", + "add": "Adaugă Proprietate", + "assign-to-customer": "Repartizează Proprietate", + "assign-asset-to-customer": "Repartizează proprietăţi clientului", + "assign-asset-to-customer-text": "Selectează proprietăţile care vor fi repartizate clientului", + "no-assets-text": "Nu au fost găsite proprietăţi", + "assign-to-customer-text": "Selectează clientul căruia îi vor fi repartizate proprietăţile", + "public": "Publică", + "assignedToCustomer": "Repartizată clientului", + "make-public": "Declară proprietate publică", + "make-private": "Declară proprietate privată", + "unassign-from-customer": "Şterge repartizare client", + "delete": "Şterge proprietate", + "asset-public": "Proprietate publică", + "asset-type": "Tip Proprietate", + "asset-type-required": "Tipul proprietății este obligatoriu", + "select-asset-type": "Alege tipul proprietății", + "enter-asset-type": "Introdu tipul proprietății", + "any-asset": "Orice Proprietate", + "no-asset-types-matching": "Nu a fost găsită nicio proprietate conținând '{{entitySubtype}}'", + "asset-type-list-empty": "Nu a fost selectat niciun tip de proprietate", + "asset-types": "Tipuri Proprietate", + "name": "Nume Proprietate", + "name-required": "Numele este obligatoriu", + "description": "Descriere Proprietate", + "type": "Tip Proprietate", + "type-required": "Tipul proprietății este obligatoriu", + "details": "Detalii Proprietate", + "events": "Evenimente", + "add-asset-text": "Adaugă proprietate", + "asset-details": "Detalii proprietate", + "assign-assets": "Repartizează proprietăţi", + "assign-assets-text": "Repartizează { count, plural, 1 {o proprietate} other {# proprietăţi} } clientului", + "delete-assets": "Şterge proprietăţi", + "unassign-assets": "Şterge repartizare proprietăţi", + "unassign-assets-action-title": "Şterge repartizare { count, plural, 1 {o proprietate} other {# proprietăţi} } clientului", + "assign-new-asset": "Repartizează proprietate nouă", + "delete-asset-title": "Sigur vrei să ștergi '{{assetName}}'?", + "delete-asset-text": "ATENŢIE! După confirmare, proprietatea şi toate datele referitoare la aceasta, vor fi șterse IREVERSIBIL!", + "delete-assets-title": "Sigur vrei să ștergi { count, plural, 1 {o proprietate} other {# proprietăţi} }?", + "delete-assets-action-title": "Ştergi { count, plural, 1 {o proprietate} other {# proprietăţi} }", + "delete-assets-text": "ATENŢIE! După confirmare, toate proprietăţile selectate şi toate datele referitoare la aceastea, vor fi șterse IREVERSIBIL!", + "make-public-asset-title": "Sigur vrei ca proprietatea '{{assetName}}' să devină publică ", + "make-public-asset-text": "ATENŢIE! După confirmare, proprietatea selectată şi toate datele referitoare la aceasta vor putea fi accesate de către oricine", + "make-private-asset-title": "Sigur vrei ca proprietatea '{{assetName}}' să devină privată?", + "make-private-asset-text": "ATENŢIE! După confirmare, proprietatea selectată şi toate datele referitoare la aceasta vor putea fi accesate doar de către proprietar", + "unassign-asset-title": "Sigur vrei să ştergi repartizarea pentru proprietatea '{{assetName}}'?", + "unassign-asset-text": "ATENŢIE! După confirmare, repartizarea proprietăţii nu va mai putea fi accesată de către client", + "unassign-asset": "Şterge repartizare proprietate", + "unassign-assets-title": "Sigur vrei să ştergi repartizarea pentru { count, plural, 1 {o proprietate} other {# proprietăţi} }?", + "unassign-assets-text": "ATENŢIE! După confirmare, repartizările proprietăţilor selectate nu vor mai putea fi accesate de către client", + "copyId": "Copiază ID proprietate", + "idCopiedMessage": "ID-ul proprietăţii a fost copiat în clipboard", + "select-asset": "Selectează proprietate", + "no-assets-matching": "Nu au fost găsite proprietăţi al căror nume conține '{{entity}}'", + "asset-required": "Proprietatea este obligatorie", + "name-starts-with": "Numele proprietăţii începe cu", + "import": "Importă proprietăţi", + "asset-file": "Fişier Proprietăţi", + "label": "Eticheta" + }, + "attribute": { + "attributes": "Atribute", + "latest-telemetry": "Ultimele Date Telemetrice", + "attributes-scope": "Scop Atribute Entitate", + "scope-latest-telemetry": "Ultimele Date Telemetrice", + "scope-client": "Atribute Client", + "scope-server": "Atribute Server", + "scope-shared": "Atribute Partajate", + "add": "Adaugă Atribut", + "key": "Cheie", + "last-update-time": "Ultima Actualizare", + "key-required": "Cheia atributului este obligatorie", + "value": "Valoare", + "value-required": "Valoarea atributului este obligatorie", + "delete-attributes-title": "Sigur vrei să ștergi { count, plural, 1 {un atribut} other {# atribute} }?", + "delete-attributes-text": "ATENŢIE! După confirmare, toate atributele selectate vor fi şterse", + "delete-attributes": "Şterge Atribute", + "enter-attribute-value": "Specifică Valoarea Atributului", + "show-on-widget": "Afişează În Widget", + "widget-mode": "Modul Widget", + "next-widget": "Widget Următor ", + "prev-widget": "Widget Precedent", + "add-to-dashboard": "Adaugă în panou", + "add-widget-to-dashboard": "Adaugă Widget În Panou", + "selected-attributes": "{ count, plural, 1 {un atribut} other {# atribute} } selectate", + "selected-telemetry": "{ count, plural, 1 {o unitate telemetrică} other {# unităţi telemetrice} } selectate" + }, + "audit-log": { + "audit": "Audit", + "audit-logs": "Jurnale Audit", + "timestamp": "Cronologie", + "entity-type": "Tip Entitate", + "entity-name": "Denumire Entitate", + "user": "Utilizator", + "type": "Tip", + "status": "Stare", + "details": "Detalii", + "type-added": "Adăugat", + "type-deleted": "Şters", + "type-updated": "Actualizat", + "type-attributes-updated": "Atribute actualizate", + "type-attributes-deleted": "Atribute şterse", + "type-rpc-call": "RPC call", + "type-credentials-updated": "Acreditări actualizate", + "type-assigned-to-customer": "Repartizat către client", + "type-unassigned-from-customer": "Anulează Repartizarea Către Client", + "type-activated": "Activat", + "type-suspended": "Suspendat", + "type-credentials-read": "Acreditări citite", + "type-attributes-read": "Atribute citite", + "type-relation-add-or-update": "Relaţie actualizată", + "type-relation-delete": "Relaţie ștearsă", + "type-relations-delete": "Toate relaţiile șterse", + "type-alarm-ack": "Confirmat", + "type-alarm-clear": "Şters", + "type-login": "Intră În Cont", + "type-logout": "Parăseşte Contul", + "type-lockout": "Blocat", + "status-success": "Succes", + "status-failure": "Eșec", + "audit-log-details": "Detalii Jurnale Audit", + "no-audit-logs-prompt": "Nu Au Fost Găsite Jurnale", + "action-data": "Detalii Acțiune", + "failure-details": "Detalii Eșec", + "search": "Caută Jurnale Audit", + "clear-search": "Resetează Căutarea" + }, + "confirm-on-exit": { + "message": "Au rămas modificări nesalvate. Doriţi să părăsiţi această pagină fară a salva modificările?", + "html-message": "Au rămas modificări nesalvate.
    Doriţi să părăsiţi această pagină fară a salva modificările?", + "title": "Modificări Nesalvate" + }, + "contact": { + "country": "Ţară", + "city": "Oraş", + "state": "Judeţ", + "postal-code": "Cod Poştal", + "postal-code-invalid": "Cod poștal incorect", + "address": "Adresă 1", + "address2": "Adresă 2", + "phone": "Telefon", + "email": "eMail", + "no-address": "Fără Adresă" + }, + "common": { + "username": "Nume Utilizator", + "password": "Parola", + "enter-username": "Introdu nume utilizator", + "enter-password": "Introdu parola", + "enter-search": "Definește căutarea", + "created-time": "Data Creării" + }, + "content-type": { + "json": "Json", + "text": "Text", + "binary": "Binary (Base64)" + }, + "customer": { + "customer": "Client", + "customers": "Clienţi", + "management": "Administrare Clienţi", + "dashboard": "Panou Control Client", + "dashboards": "Panouri Control Clienţi", + "devices": "Dispozitive Client", + "entity-views": "Entități Definite Client", + "assets": "Proprietăţi Client", + "public-dashboards": "Panouri Control Publice", + "public-devices": "Dispozitive Publice", + "public-assets": "Proprietăţi Publice", + "public-entity-views": "Definiții Entități Publice Client", + "add": "Adaugă Client", + "delete": "Şterge Client", + "manage-customer-users": "Administrare Utilizatori Client", + "manage-customer-devices": "Administrare Dispozitive Client", + "manage-customer-dashboards": "Administrare Panouri Control Client", + "manage-public-devices": "Administrare Dispozitive Publice", + "manage-public-dashboards": "Administrare Panouri Control Publice", + "manage-customer-assets": "Administrare Proprietăţi Client", + "manage-public-assets": "Administrare Proprietăţi Publice", + "add-customer-text": "Adăugare Client Nou", + "no-customers-text": "Nu au fost găsiţi clienţi", + "customer-details": "Detalii Client", + "delete-customer-title": "Vrei să ștergi clientul '{{customerTitle}}'?", + "delete-customer-text": "ATENŢIE! După confirmare, clientul şi toate datele referitoare la acesta vor fi șterse IREVERSIBIL!", + "delete-customers-title": "Vrei să ștergi { count, plural, 1 {un client} other {# clienţi} }?", + "delete-customers-action-title": "Şterge { count, plural, 1 {un client} other {# clienţi} }", + "delete-customers-text": "ATENŢIE! După confirmare, toaţi clienţii selectate şi toate datele referitoare la aceaştia, vor fi șterse IREVERSIBIL!", + "manage-users": "Administrare Utilizatori", + "manage-assets": "Administrare Proprietăţi", + "manage-devices": "Administrare Dispozitive", + "manage-dashboards": "Administrare Panouri Control Publice", + "title": "Titlu", + "title-required": "Titlul este obligatoriu", + "description": "Descriere", + "details": "Detalii", + "events": "Evenimente", + "copyId": "Copie ID Client", + "idCopiedMessage": "ID Client A Fost Copiat In Clipboard", + "select-customer": "Selectează Client", + "no-customers-matching": "Niciun client nu se potrivește cu:'{{entity}}'", + "customer-required": "Clientul este obligatoriu", + "select-default-customer": "Selecteaza Client Implicit", + "default-customer": "Client Implicit", + "default-customer-required": "Clientul implicit este obligatoriu pentru a putea depana panoul de control la nivel de PROPRIETAR" + }, + "datetime": { + "date-from": "Dată început:", + "time-from": "Oră început:", + "date-to": "Dată sfârșit:", + "time-to": "Oră sfârșit:" + }, + "dashboard": { + "dashboard": "Panou", + "dashboards": "Panouri", + "management": "Administrare Panouri", + "view-dashboards": "Afişează Panouri", + "add": "Adaugă Panou", + "assign-dashboard-to-customer": "Repartizează Panou/Panouri Clientului", + "assign-dashboard-to-customer-text": "Selectează panourile care vor fi repartizate clientului", + "assign-to-customer-text": "Alege Clientul Căruia îi vor fi repartizate Panourile", + "assign-to-customer": "Repartizează Clientului", + "unassign-from-customer": "Şterge Repartizarea Către Client", + "make-public": "Declară Panou Public", + "make-private": "Declară Panou Privat", + "manage-assigned-customers": "Administrare Clienţi Repartizaţi", + "assigned-customers": "Clienţi Repartizaţi", + "assign-to-customers": "Repartizează Panouri Către Clienţi", + "assign-to-customers-text": "Alege clienţii cărora le vor fi repartizate panourile", + "unassign-from-customers": "Şterge repartizarea panourilor către clienţi", + "unassign-from-customers-text": "Alege clienţii cărora le va fi ștearsă repartizarea panourilor", + "no-dashboards-text": "Nu au fost găsite panouri", + "no-widgets": "Nu sunt widgeturi configurate", + "add-widget": "Adăugare Widget Nou", + "title": "Titlu Widget", + "select-widget-title": "Alege Widget", + "select-widget-subtitle": "Listă Tipuri Widget", + "delete": "Şterge Panou", + "title-required": "Titlul este obligatoriu", + "description": "Descriere", + "details": "Detalii", + "dashboard-details": "Detalii Panou", + "add-dashboard-text": "Adăugare Panou Nou", + "assign-dashboards": "Repartizare Panouri", + "assign-new-dashboard": "Repartizare Panou Nou", + "assign-dashboards-text": "Repartizează { count, plural, 1 {un panou} other {# panouri} } clienţilor", + "unassign-dashboards-action-text": "Şterge repartizare { count, plural, 1 {un panou} other {# panouri} } către clienți", + "delete-dashboards": "Şterge Panouri", + "unassign-dashboards": "Şterge Repartizare Panouri", + "unassign-dashboards-action-title": "Şterge Repartizare { count, plural, 1 {un panou} other {# panouri} } către client", + "delete-dashboard-title": "Vrei să ștergi panoul '{{dashboardTitle}}'?", + "delete-dashboard-text": "ATENŢIE! După confirmare, panoul și datele aferente acestuia vor fi șterse IREVERSIBIL!", + "delete-dashboards-title": "Vrei să ștergi { count, plural, 1 {un panou} other {# panouri} }?", + "delete-dashboards-action-title": "Ştergere { count, plural, 1 {un panou} other {# panouri} }", + "delete-dashboards-text": "ATENŢIE! După confirmare, panourile selectate şi datele aferente acestora vor fi șterse IREVERSIBIL!", + "unassign-dashboard-title": "Vrei să ştergi repartizarea panoului '{{dashboardTitle}}'?", + "unassign-dashboard-text": "ATENŢIE! După confirmare, panoul nu va mai putea fi accesat de către client", + "unassign-dashboard": "Şterge Repartizare Panou", + "unassign-dashboards-title": "Vrei să ştergi repartizarea a { count, plural, 1 {un panou} other {# panouri} }?", + "unassign-dashboards-text": "ATENŢIE! După confirmare, panoul nu va mai putea fi accesat de către client", + "public-dashboard-title": "Panoul a devenit public", + "public-dashboard-text": "Panoul tău {{dashboardTitle}} a devenit public şi este accesibil la link:", + "public-dashboard-notice": "Notă: Nu uitaţi să definiţi ca publice şi dispozitivele aferente acestui panou, pentru a le face vizibile", + "make-private-dashboard-title": "Doriţi să definiţi panoul '{{dashboardTitle}}' ca privat?", + "make-private-dashboard-text": "ATENŢIE! După confirmare, panoul va putea fi accesat doar de către proprietar", + "make-private-dashboard": "Declară Panou Privat", + "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard", + "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard", + "select-dashboard": "Selectează Panou", + "no-dashboards-matching": "Nu au fost găsite panouri al căror nume conține '{{entity}}'", + "dashboard-required": "Panoul este obligatoriu", + "select-existing": "Selectează Un Panou Existent", + "create-new": "Creează Panou Nou", + "new-dashboard-title": "Denumire Panou Nou", + "open-dashboard": "Deschide Panou", + "set-background": "Setează Culoarea De Fundal (Background)", + "background-color": "Culoarea (Background)", + "background-image": "Imaginea (Background)", + "background-size-mode": "Mod Mărime (Background)", + "no-image": "Nicio imagine selectată", + "drop-image": "Trage o imagine sau alege un fişier", + "settings": "Setări", + "columns-count": "Număr Coloane", + "columns-count-required": "Numărul coloanelor este obligatoriu", + "min-columns-count-message": "Numărul minim permis de coloane este 10", + "max-columns-count-message": "Numărul maxim permis de coloane este 1000", + "widgets-margins": "Spaţiu vertical între widgeturi", + "horizontal-margin": "Spaţiu orizontal între widget-uri", + "horizontal-margin-required": "Valoarea spaţiului orizontal este obligatorie", + "min-horizontal-margin-message": "Valoarea minimă permisă pentru spaţiul orizontal între widgeturi este 0", + "max-horizontal-margin-message": "Valoarea maximă permisă pentru spaţiul orizontal între widgeturi este 0", + "vertical-margin": "Spaţiu vertical între widgeturi", + "vertical-margin-required": "Valoarea spaţiului vertical este obligatorie", + "min-vertical-margin-message": "Valoarea minimă permisă pentru spaţiul vertical între widgeturi este 0", + "max-vertical-margin-message": "Valoarea maximă permisă pentru spaţiul vertical între widgeturi este 50", + "autofill-height": "Auto Umplere Pe Înălţime", + "mobile-layout": "Setări Pagină Pentru Dispozitive Mobile", + "mobile-row-height": "Înălţimea liniei în pagina pentru dispozitive mobile (pixeli)", + "mobile-row-height-required": "Valoarea pentru înălţimea liniei este obligatorie", + "min-mobile-row-height-message": "Valoarea minimă permisă pentru înălţimea liniei în pagina pentru dispozitive mobile este 5 pixeli", + "max-mobile-row-height-message": "Valoarea maximă permisă pentru înălţimea liniei în pagina pentru dispozitive mobile este 200 pixeli", + "display-title": "Afişează Titlul Panoului", + "toolbar-always-open": "Menţine Deschisă Bara De Instrumente", + "title-color": "Culoare Titlu", + "display-dashboards-selection": "Afişează Selecţie Panouri", + "display-entities-selection": "Afişează Selecţie Entităţi", + "display-dashboard-timewindow": "Afişează Interval", + "display-dashboard-export": "Afişează Exportul", + "import": "Importă Panou", + "export": "Exportă Panou", + "export-failed-error": "Panoul nu poate fi exportat: {{error}}", + "create-new-dashboard": "Creează Panou Nou", + "dashboard-file": "Fişier Pentru Panou", + "invalid-dashboard-file-error": "Panoul nu poate fi importat; structură de date invalidă", + "dashboard-import-missing-aliases-title": "Configurează pseudonim pentru panoul importat", + "create-new-widget": "Creează Widget Nou", + "import-widget": "Importă Widget", + "widget-file": "Fişier Pentru Widget", + "invalid-widget-file-error": "Widgetul nu poate fi importat; structură de date invalidă", + "widget-import-missing-aliases-title": "Configurează pseudonim pentru widgetul importat", + "open-toolbar": "Deschide Bara De Instrumente Pentru Panou", + "close-toolbar": "Închide Bara De Instrumente", + "configuration-error": "Eroare De Configurare", + "alias-resolution-error-title": "Eroare configurare pseudonim panou", + "invalid-aliases-config": "Nu există nici un dispozitiv corespunzător filtrului de pseudonime.
    Pentru a rezolva această problemă, contactează administratorul", + "select-devices": "Selectează Dispozitiv", + "assignedToCustomer": "Repartizat Clientului", + "assignedToCustomers": "Repartizate Clienților", + "public": "Publice", + "public-link": "Adresă Pagină Publică", + "copy-public-link": "Copiază Adresa Paginii Publice", + "public-link-copied-message": "Adresa paginii publice a panoului a fost copiată în clipboard", + "manage-states": "Administrare Stări Panou", + "states": "Stări Panou", + "search-states": "Caută Stări Panou", + "selected-states": "{ count, plural, 1 {o stare panou} other {# stări panou} } selectate", + "edit-state": "Editează Stare Panou", + "delete-state": "Şterge Stare Panou", + "add-state": "Adaugă Stare Panou", + "state": "Stare Panou", + "state-name": "Denumire", + "state-name-required": "Denumirea stării panoului este obligatorie", + "state-id": "ID Stare Panou", + "state-id-required": "IDul Stării panoului este obligatoriu", + "state-id-exists": "O stare panou având acest ID este deja înregistrată", + "is-root-state": "Stare Rădăcină", + "delete-state-title": "Şterge Stare Panou", + "delete-state-text": "Sigur vrei să ștergi starea panou '{{stateName}}'?", + "show-details": "Afişează Detalii", + "hide-details": "Ascunde Detalii", + "select-state": "Selectează Stare Destinaţie", + "state-controller": "Controler Stări" + }, + "datakey": { + "settings": "Setări", + "advanced": "Avansat", + "label": "Etichetă", + "color": "Culoare", + "units": "Simbol special atașat valorii afişate", + "decimals": "Număr zecimale", + "data-generation-func": "Funcţie Generare Date", + "use-data-post-processing-func": "Utilizare Funcţie Post Procesare", + "configuration": "Configurare Chei Date", + "timeseries": "Serii Temporale", + "attributes": "Atribute", + "entity-field": "Câmp Entitate", + "alarm": "Câmpuri Alarmă", + "timeseries-required": "Seriile temporale pentru entitate sunt obligatorii", + "timeseries-or-attributes-required": "Seriile temporale sau atributele pentru entitate sunt obligatorii", + "maximum-timeseries-or-attributes": "{ count, plural, 1 { Este permisă maximum o serie temporală sau atribut} other {Sunt permise maximum # serii temporale sau atribute} }", + "alarm-fields-required": "Câmpurile pentru alarmă sunt obligatorii", + "function-types": "Tipuri Funcţie", + "function-types-required": "Tipurile de funcţie sunt obligatorii", + "maximum-function-types": "Maximum { count, plural, 1 {Este permis doar un tip de funcţie} other {Sunt permise doar # tipuri de funcţie} }", + "time-description": "Cronologie valoare curentă;", + "value-description": "valoare curentă;", + "prev-value-description": "Rezultat execuție funcţie precedentă;", + "time-prev-description": "Cronologie valoare precedentă;", + "prev-orig-value-description": "Valoare precedentă originală;" + }, + "datasource": { + "type": "Tip Sursă Date", + "name": "Denumire", + "add-datasource-prompt": "Adaugă Sursă Date" + }, + "details": { + "edit-mode": "Mod Editare", + "toggle-edit-mode": "Schimbă Mod Editare" + }, + "device": { + "device": "Dispozitiv", + "device-required": "Dispozitivul este obligatoriu", + "devices": "Dispozitive", + "management": "Administrare Dispozitive", + "view-devices": "Vizualizare Dispozitive", + "device-alias": "Pseudonim Dispozitiv", + "aliases": "Pseudonime Dispozitiv", + "no-alias-matching": "'{{alias}}' nu a fost găsit", + "no-aliases-found": "Nu au fost găsite pseudonime", + "no-key-matching": "'{{key}}' nu a fost găsită", + "no-keys-found": "Nu a fost găsită nicio cheie", + "create-new-alias": "Creează Pseudonim Nou", + "create-new-key": "Creează Cheie Nouă", + "duplicate-alias-error": "Pseudonimul ales este deja înregistrat : '{{alias}}'.
    Pseudonimele dispozitivelor trebuie să fie unice în acelaşi panou", + "configure-alias": "Configurează Pseudonim: '{{alias}}'", + "no-devices-matching": "Nu au fost găsite dispozitive al căror nume conține: '{{entity}}'", + "alias": "Pseudonim", + "alias-required": "Pseudonimul pentru dispozitiv este obligatoriu", + "remove-alias": "Şterge Pseudonim Dispozitiv", + "add-alias": "Adaugă Pseudonim Dispozitiv", + "name-starts-with": "Denumirea dispozitivului începe cu: ", + "device-list": "Listă Dispozitive", + "use-device-name-filter": "Utilizează Filtru Căutare", + "device-list-empty": "Nu este selectat niciun dispozitiv", + "device-name-filter-required": "Filtrul de căutare pentru nume dispozitiv este obligatoriu", + "device-name-filter-no-device-matched": "Nu au fost găsite dispozitive al căror nume începe cu '{{device}}'", + "add": "Adaugă Dispozitiv", + "assign-to-customer": "Repartizează Clientului", + "assign-device-to-customer": "Repartizează Dispozitiv(e) Clientului", + "assign-device-to-customer-text": "Selectează dispozitivele de repartizat clientului ", + "make-public": "Configurează Ca Dispozitiv Public", + "make-private": "Configurează Ca Dispozitiv Privat", + "no-devices-text": "Nu există dispozitive", + "assign-to-customer-text": "Selectrază clientul căruia să-i fie repartizat(e) dispozitiv(ele)", + "device-details": "Detalii Dispozitiv", + "add-device-text": "Adaugă Dispozitiv Nou", + "credentials": "Acreditări", + "manage-credentials": "Administrare Acreditări", + "delete": "Şterge Dispozitiv", + "assign-devices": "Repartizeză Dispozitive", + "assign-devices-text": "Repartizeză { count, plural, 1 {un dispozitiv} other {# dispozitive} } Clientului", + "delete-devices": "Şterge Dispozitive", + "unassign-from-customer": "Şterge repartizarea către client", + "unassign-devices": "Şterge repartizarea dispozitivelor", + "unassign-devices-action-title": "Şterge repartizarea a { count, plural, 1 {un dispozitiv} other {# dispozitive} } de la client", + "assign-new-device": "Repartizare Dispozitiv Nou", + "make-public-device-title": "Sigur vrei să faci dispozitivul '{{deviceName}}' public?", + "make-public-device-text": "După confirmare, dispozitivul și toate datele aferente acestuia vor fi făcute publice, fiind deci accesibile oricui", + "make-private-device-title": "Sigur vrei să faci dispozitivul '{{deviceName}}' privat?", + "make-private-device-text": "După confirmare, dispozitivul și toate datele aferente acestuia vor fi private, deci accesibile doar proprietarului", + "view-credentials": "Vezi Credențiale", + "delete-device-title": "Sigur vrei să ștergi dispozitivul '{{deviceName}}'?", + "delete-device-text": "ATENȚIE! După confirmare, dispozitivul împreună cu datele aferente acestuia vor fi șterse IREVERSIBIL!", + "delete-devices-title": "Sigur vrei să ștergi { count, plural, 1 {un dispozitiv} other {# dispozitive} }?", + "delete-devices-action-title": "Delete { count, plural, 1 {un dispozitiv} other {# dispozitive} }", + "delete-devices-text": "ATENȚIE! După confirmare, toate dispozitivele selectate, împreună cu datele aferente acestora, vor fi șterse IREVERSIBIL!", + "unassign-device-title": "Sigur vrei să ștergi repartizarea dispozitivului '{{deviceName}}'?", + "unassign-device-text": "ATENȚIE! După confirrmare, dispozitivul nu va mai fi accesibil clientului", + "unassign-device": "Șterge repartizare dispozitiv", + "unassign-devices-title": "Sigur vrei să ștergi repartizarea pentru { count, plural, 1 {un dispozitiv} other {# devices} }?", + "unassign-devices-text": "ATENȚIE! După confirmare, repartizarea dispozitivelor va fi ștearsă, acestea nemaifiind accesibile clientului", + "device-credentials": "Credențiale Dispozitiv", + "credentials-type": "Tip Credențiale", + "access-token": "Token Acces", + "access-token-required": "Tokenul de acces este necesar", + "access-token-invalid": "Dimensiunea tokenului de acces trebuie să fie de 1-20 caractere", + "rsa-key": "Cheie RSA publică", + "rsa-key-required": "Cheia RSA publică key este necesară", + "secret": "Cod Secret", + "secret-required": "Codul secret este necesar", + "device-type": "Tip Dispozitiv", + "device-type-required": "Tipul dispozitivului este obligatoriu", + "select-device-type": "Alege tipul dispozitivului", + "enter-device-type": "Introdu tipul dispozitivului", + "any-device": "Orice Dispozitiv", + "no-device-types-matching": "Nu au fost găsite dispozitive de tip '{{entitySubtype}}' ", + "device-type-list-empty": "Nu au fost selectate tipuri dispozitiv", + "device-types": "Tipuri dispozitiv", + "name": "Nume", + "name-required": "Numele este obligatoriu", + "description": "Descriere", + "label": "Etichetă", + "events": "Evenimente", + "details": "Detalii", + "copyId": "Copie ID Dispozitiv", + "copyAccessToken": "Copiază Token Acces", + "idCopiedMessage": "ID dispozitiv copiat în clipboard", + "accessTokenCopiedMessage": "Token acces dispozitiv copiat în clipboard", + "assignedToCustomer": "Repartizat clientului", + "unable-delete-device-alias-title": "Pseudonimul dispozitivului nu poate fi șters", + "unable-delete-device-alias-text": "Pseudonimul dispozitivului '{{deviceAlias}}' nu poate fi șters, întrucât este folosit de widget(urile):
    {{widgetsList}}", + "is-gateway": "Este gateway", + "public": "Public", + "device-public": "Dispozitivul este public", + "select-device": "Selectează Dispozitiv", + "import": "Importă Dispozitiv", + "device-file": "Fișier dispozitiv" + }, + "dialog": { + "close": "Închide Casetă Dialog" + }, + "direction": { + "column": "Coloană", + "row": "Linie" + }, + "error": { + "unable-to-connect": "Conexiunea cu serverul imposibilă! Ești conectat la Internet?", + "unhandled-error-code": "Cod eroare negestionată: {{errorCode}}", + "unknown-error": "Eroare necunoscută" + }, + "entity": { + "entity": "Entitate", + "entities": "Entități", + "aliases": "Pseudonime Entități", + "entity-alias": "Pseudonim Entitate", + "unable-delete-entity-alias-title": "Ștergerea pseudonimului entității este imposibilă", + "unable-delete-entity-alias-text": "Pseudonimul entității '{{entityAlias}}', fiind folosit de widgetul/widgeturile:
    {{widgetsList}}", + "duplicate-alias-error": "Pseudonimul entității este duplicat
    Pseudonimele entităților trebuie să fie unice, în același panou", + "missing-entity-filter-error": "Lipsă filtru pentru pseudonimul '{{alias}}'", + "configure-alias": "Configurează pseudonimul '{{alias}}'", + "alias": "Pseudonim", + "alias-required": "Pseudonimul entității este necesar", + "remove-alias": "șterge Alias Entitate", + "add-alias": "Adaugă Alias Entitate", + "entity-list": "Listă Entități", + "entity-type": "Tip Entitate", + "entity-types": "Tipuri Entități", + "entity-type-list": "Listă Tipuri Entități", + "any-entity": "Orice Entitate", + "enter-entity-type": "Introdu Tip Entitate", + "no-entities-matching": "Nu a fost găsită nicio entitate conținând '{{entity}}' ", + "no-entity-types-matching": "Nu au fost găsite tipuri de entități conținând '{{entityType}}'", + "name-starts-with": "Numele începe cu", + "use-entity-name-filter": "Folosește filtru", + "entity-list-empty": "Nicio entitate selectată", + "entity-type-list-empty": "Niciun tip entitate selectat", + "entity-name-filter-required": "Filtrul pentru nume entitate este necesar", + "entity-name-filter-no-entity-matched": "Nu a fost găsită nicio entitate al cărei nume începe cu '{{entity}}'", + "all-subtypes": "Toate", + "select-entities": "Selectează entități", + "no-aliases-found": "Niciun pseudonim găsit", + "no-alias-matching": "Nu am găsit pseudonimul '{{alias}}'", + "create-new-alias": "Creează unul nou!", + "key": "Cheie", + "key-name": "Nume Cheie", + "no-keys-found": "Nicio cheie găsită", + "no-key-matching": "Nu am găsit cheia '{{key}}'", + "create-new-key": "Creează una nouă!", + "type": "Tip", + "type-required": "Tipul entității este necesar", + "type-device": "Dispozitiv", + "type-devices": "Dispozitive", + "list-of-devices": "{ count, plural, 1 {un dispozitiv} other {Listă # dispozitive} }", + "device-name-starts-with": "Dispozitive a căror nume începe cu '{{prefix}}'", + "type-asset": "Proprietate", + "type-assets": "Proprietăți", + "list-of-assets": "{ count, plural, 1 {o proprietate} other {Listă # proprietăți} }", + "asset-name-starts-with": "Proprietate a cărei nume începe cu '{{prefix}}'", + "type-entity-view": "Entitate Definită", + "type-entity-views": "Entități Definite", + "list-of-entity-views": "{ count, plural, 1 {o entitate definită} other {Listă # entități definite} }", + "entity-view-name-starts-with": "Entități definite al căror nume începe cu '{{prefix}}'", + "type-rule": "Regulă", + "type-rules": "Reguli", + "list-of-rules": "{ count, plural, 1 {o regulă} other {Listă # reguli} }", + "rule-name-starts-with": "Reguli al căror nume începe cu '{{prefix}}'", + "type-plugin": "Plugin", + "type-plugins": "Plugin-uri", + "list-of-plugins": "{ count, plural, 1 {un plugin} other {Listă # plugin-uri} }", + "plugin-name-starts-with": "Plugin-uri al căror nume începe cu '{{prefix}}'", + "type-tenant": "Locatar", + "type-tenants": "Locatari", + "list-of-tenants": "{ count, plural, 1 {un locatar} other {Listă # locatari} }", + "tenant-name-starts-with": "Locatari al căror nume începe cu '{{prefix}}'", + "type-customer": "Client", + "type-customers": "Clienţi", + "list-of-customers": "{ count, plural, 1 {un client} other {Listă # Clienţi} }", + "customer-name-starts-with": "Clienţi al căror nume începe cu '{{prefix}}'", + "type-user": "Utilizator", + "type-users": "Utilizatori", + "list-of-users": "{ count, plural, 1 {un utilizator} other {Listă # utilizatori} }", + "user-name-starts-with": "Utilizatori al căror nume începe cu '{{prefix}}'", + "type-dashboard": "Panou Control", + "type-dashboards": "Panouri Control", + "list-of-dashboards": "{ count, plural, 1 {un panou control} other {Listă # panouri control} }", + "dashboard-name-starts-with": "Panouri control al căror nume începe cu '{{prefix}}'", + "type-alarm": "Alarmă", + "type-alarms": "Alarme", + "list-of-alarms": "{ count, plural, 1 {o alarmă} other {Listă # alarme} }", + "alarm-name-starts-with": "Alarme al căror nume începe cu '{{prefix}}'", + "type-rulechain": "Flux", + "type-rulechains": "Fluxuri", + "list-of-rulechains": "{ count, plural, 1 {un flux} other {Listă # fluxuri} }", + "rulechain-name-starts-with": "Fluxuri al căror nume începe cu '{{prefix}}'", + "type-rulenode": "Nod flux", + "type-rulenodes": "Noduri flux", + "list-of-rulenodes": "{ count, plural, 1 {un nod flux} other {Listă # noduri flux} }", + "rulenode-name-starts-with": "Noduri flux al căror nume începe cu '{{prefix}}'", + "type-current-customer": "Client Curent", + "search": "Caută Entități", + "selected-entities": "{ count, plural, 1 {o entitate} other {# entități} } selectate", + "entity-name": "Nume Entitate", + "entity-label": "Etichetă Entitate", + "details": "Detalii Entitate", + "no-entities-prompt": "Nu au fost găsite entități", + "no-data": "Nu există date de afișat", + "columns-to-display": "Coloane Afișate" + }, + "entity-field": { + "created-time": "Momentul Creării", + "name": "Denumire", + "type": "Tip", + "first-name": "Prenume", + "last-name": "Nume", + "email": "eMail", + "title": "Formula De Adresare", + "country": "Ţara", + "state": "Judeţ", + "city": "Oraş", + "address": "Adresa 1", + "address2": "Adresa 2", + "zip": "Cod Poştal", + "phone": "Telefon", + "label": "Etichetă" + }, + "entity-view": { + "entity-view": "Entitate Definită", + "entity-view-required": "Entitatea definită este obligatorie", + "entity-views": "Entități Definite", + "management": "Administrare Entități Definite", + "view-entity-views": "Afişează Entități Definite", + "entity-view-alias": "Pseudonim Entitate Definită", + "aliases": "Pseudonime Entități Definite", + "no-alias-matching": "'{{alias}}' Pseudonimul nu a fost găsit", + "no-aliases-found": "Nu au fost găsite pseudonime", + "no-key-matching": "Cheia '{{key}}' nu a fost găsită", + "no-keys-found": "Nu a fost găsită nicio cheie", + "create-new-alias": "Creează Alias Nou", + "create-new-key": "Creează Cheie Nouă", + "duplicate-alias-error": "Pseudonimul: '{{alias}}' este deja înregistrat
    Pseudonimele pentru entităţi trebuie să fie unice în acelaşi panou", + "configure-alias": "Configurează pseudonim'{{alias}}'", + "no-entity-views-matching": "Nu au fost găsite entități definite după criteriul: '{{entity}}'", + "alias": "Pseudonim", + "alias-required": "Pseudonimul entității definite este obligatoriu", + "remove-alias": "Şterge pseudonim entitate definită", + "add-alias": "Adaugă pseudonim entitate definită", + "name-starts-with": "Numele entității definite începe cu ", + "entity-view-list": "Listă Entități Definite", + "use-entity-view-name-filter": "Filtrează", + "entity-view-list-empty": "Nu au fost selectate entități definite", + "entity-view-name-filter-required": "Filtrul pentru numele entității definite este obligatoriu", + "entity-view-name-filter-no-entity-view-matched": "Nu au fost găsite entități definite al căror nume conține: '{{entityView}}'", + "add": "Adaugă Entitate Definită", + "assign-to-customer": "Repartizare către client", + "assign-entity-view-to-customer": "Repartizare Entități Definite Clientului", + "assign-entity-view-to-customer-text": "Selectează entitățile definite ce vor fi repartizate clientului", + "no-entity-views-text": "Nu există entități definite", + "assign-to-customer-text": "Selectează clientul căruia îi vor fi repartizate entitățile definite", + "entity-view-details": "Detalii Entitate Definită", + "add-entity-view-text": "Adaugă Entitate Definită", + "delete": "ßterge Entitate Definită", + "assign-entity-views": "Repartizează Entitate Definită", + "assign-entity-views-text": "Repartizează { count, plural, 1 {o entitate definită} other {# entități definite} } clientului", + "delete-entity-views": "Şterge Entități Definite", + "unassign-from-customer": "Ştergere Repartizare Către Client", + "unassign-entity-views": "Ştergere Repartizare Entități Definite", + "unassign-entity-views-action-title": "Şterge repartizare { count, plural, 1 {o entitate definită} other {# entități definite} } de la client ", + "assign-new-entity-view": "Repartizează Entitate Definită Nouă", + "delete-entity-view-title": "Sigur vrei să ștergi entitatea definită : '{{entityViewName}}'?", + "delete-entity-view-text": "ATENŢIE! După confirmare, entitatea definită şi toate datele asociate cu aceasta vor fi șterse IREVERSIBIL!", + "delete-entity-views-title": "Sigur vrei să ștergi{ count, plural, 1 {o entitate definită} other {# entități definite} }?", + "delete-entity-views-action-title": "Şterge { count, plural, 1 {o entitate definită} other {# entități definite} }", + "delete-entity-views-text": "ATENŢIE! După confirmare, toate entitățile definite și datele asociate acestora vor fi șterse IREVERSIBIL!", + "unassign-entity-view-title": "Sigur vrei să ștergi repartizarea entității definite : '{{entityViewName}}'?", + "unassign-entity-view-text": "ATENŢIE! După confirmare, clientul nu va mai putea accesa entitatea definită selectată", + "unassign-entity-view": "Şterge Repartizare Entitate Definită", + "unassign-entity-views-title": "Sigur vrei să ștergi repartizarea a { count, plural, 1 {o entitate definită} other {# entități definite} }?", + "unassign-entity-views-text": "ATENŢIE! După confirmare, clientul nu va mai putea accesa entitățile definite selectate", + "entity-view-type": "Tip Entitate Definită", + "entity-view-type-required": "Tipul ecranului pentru entitate este obligatoriu", + "select-entity-view-type": "Selectaţi Tipul Ecranului Pentru Entitate", + "enter-entity-view-type": "Introduceţi Tipul Ecranului Pentru Entitate", + "any-entity-view": "Orice Entitate Definită", + "no-entity-view-types-matching": "Nu au fost găsite tipuri de entitate definită care să corespundă criteriului '{{entitySubtype}}'", + "entity-view-type-list-empty": "Nu au fost selectate tipuri de entitate definită", + "entity-view-types": "Tipuri Entitate Definită", + "name": "Denumire", + "name-required": "Denumirea este obligatorie", + "description": "Descriere", + "events": "Evenimente", + "details": "Detalii", + "copyId": "Copie ID Entitate Definită", + "assignedToCustomer": "Repartizat Clientului", + "unable-entity-view-device-alias-title": "Pseudonimul entității definite nu poate fi şters", + "unable-entity-view-device-alias-text": "Pseudonimul dispozitivului : '{{entityViewAlias}}' nu poate fi șters, fiind folosit de widgetul/widgeturile :
    {{widgetsList}}", + "select-entity-view": "Selectează Entitate Definită", + "make-public": "Declară Entitate Definită Publică", + "make-private": "Declară Entitate Definită Privată", + "start-date": "Dată Început", + "start-ts": "Oră Început", + "end-date": "Dată Sfârșit", + "end-ts": "Oră Sfârșit", + "date-limits": "Limite Dată", + "client-attributes": "Atribute Client", + "shared-attributes": "Atribute Partajate", + "server-attributes": "Atribute Server", + "timeseries": "Serii Temporale", + "client-attributes-placeholder": "Atribute Client", + "shared-attributes-placeholder": "Atribute Partajate", + "server-attributes-placeholder": "Atribute Server", + "timeseries-placeholder": "Serii Temporale", + "target-entity": "Entitate Destinaţie", + "attributes-propagation": "Propagare Atribute", + "attributes-propagation-hint": "Entitatea definită va copia automat atributele specificate de la entitatea destinaţie, de fiecare dată când este salvată sau actualizată. Din motive de performanţă, atributele entităţii de destinaţie nu sunt propagate către entitatea definită la fiecare modificare de atribut. Poți activa propagarea automată a atributelor prin configurarea nodului în mod \"copiază pentru a vedea\" în fluxul tău și conectarea mesajelor \"Post Atribute\" și \"Atribute Actualizate\" la noul nod", + "timeseries-data": "Date Serii Temporale", + "timeseries-data-hint": "Configurează intervale timp pentru seriile temporale ale entităţii destinaţie, care vor fi accesibile prin entitatea definită. Acestea nu vor putea fi modificate", + "make-public-entity-view-title": "Sigur vrei să faci publică entitatea definită: '{{entityViewName}}'?", + "make-public-entity-view-text": "ATENŢIE! După confirmare, entitatea definită și datele aferente acesteia vor deveni publice, deci accesibile oricui", + "make-private-entity-view-title": "Sigur vrei să faci privată entitatea definită: '{{entityViewName}}'?", + "make-private-entity-view-text": "ATENŢIE! După confirmare, entitatea definită şi toate datele aferente acesteia, vor deveni private, la ele având acces doar proprietarul" + }, + "event": { + "event-type": "Tip Eveniment", + "type-error": "Eroare", + "type-lc-event": "Durată Eveniment", + "type-stats": "Statistică", + "type-debug-rule-node": "Depanare", + "type-debug-rule-chain": "Depanare", + "no-events-prompt": "Nu au fost găsite evenimente", + "error": "Eroare", + "alarm": "Alarmă", + "event-time": "Oră Eveniment", + "server": "Server", + "body": "Corp", + "method": "Metodă", + "type": "Tip", + "entity": "Entitate", + "message-id": "ID Mesaj", + "message-type": "Tip Mesaj", + "data-type": "Tip Date", + "relation-type": "Tip Relaţie", + "metadata": "Metadata", + "data": "Date", + "event": "Eveniment", + "status": "Stare", + "success": "Succes", + "failed": "Eşuat", + "messages-processed": "Mesaje procesate", + "errors-occurred": "Au apărut erori" + }, + "extension": { + "extensions": "Extensii", + "selected-extensions": "{ count, plural, 1 {o extensie selectată} other {# extensii selectate} }", + "type": "Tip", + "key": "Cheie", + "value": "Valoare", + "id": "ID", + "extension-id": "ID Extensie", + "extension-type": "Tip Extensie", + "transformer-json": "JSON *", + "unique-id-required": "ID-ul curent pentru extensie este deja înregistrat", + "delete": "Şterge Extensie", + "add": "Adaugă Extensie", + "edit": "Editează Extensie", + "delete-extension-title": "Sigur vrei să ștergi extensia: '{{extensionId}}'?", + "delete-extension-text": "ATENŢIE! După confirmare, extensia şi toate datele asociate acesteia, vor fi șterse IREVERSIBIL!", + "delete-extensions-title": "Sigur vrei să ștergi { count, plural, 1 {o extensie} other {# extensii} }?", + "delete-extensions-text": "ATENŢIE! După confirmare, toate extensiile selectate vor fi șterse IREVERSIBIL!", + "converters": "Convertoare", + "converter-id": "ID Convertoare", + "configuration": "Configurare", + "converter-configurations": "Configurator Convertoare", + "token": "Token De Securitate", + "add-converter": "Adaugă Convertor", + "add-config": "Adaugă Configurator Convertoare", + "device-name-expression": "Expresie Denumire Dispozitiv", + "device-type-expression": "Expresie Tip Dispozitiv", + "custom": "Definit De Utilizator", + "to-double": "To Double", + "transformer": "Transformare", + "json-required": "Este necesar transformer json", + "json-parse": "Analiză transformer json imposibilă", + "attributes": "Atribute", + "add-attribute": "Adaugă Atribut", + "add-map": "Adaugă Mapare", + "timeseries": "Date Cronologice", + "add-timeseries": "Adaugă Set Date Cronologice", + "field-required": "Câmpul Este Obligatoriu", + "brokers": "Brokeri", + "add-broker": "Adaugă Broker", + "host": "Gazdă", + "port": "Port", + "port-range": "Valoarea portului trebuie să fie în intervalul 1 - 65535", + "ssl": "SSL", + "credentials": "Acreditări", + "username": "Nume Utilizator", + "password": "Parolă", + "retry-interval": "Interval Reîncercare (milisecunde)", + "anonymous": "Anonim", + "basic": "Bazic", + "pem": "PEM", + "ca-cert": "Fişier Certificat CA", + "private-key": "Fişier Cheie Privată *", + "cert": "Fişier Certificat *", + "no-file": "Niciun fișier selectat", + "drop-file": "Trageţi un fişier sau selectaţi cu mouseul un fişier pentru a fi încărcat", + "mapping": "Mapare", + "topic-filter": "Filtru Topic", + "converter-type": "Tipul Convertor", + "converter-json": "Json", + "json-name-expression": "Expresie JSON Pentru Nume Dispozitiv", + "topic-name-expression": "Expresie TOPIC Pentru Nume Dispozitiv", + "json-type-expression": "Expresie JSON Pentru Tip Dispozitiv", + "topic-type-expression": "Expresie TOPIC Pentru Tip Dispozitiv", + "attribute-key-expression": "Expresie Cheie Atribut", + "attr-json-key-expression": "Expresie JSON Cheie Atribut", + "attr-topic-key-expression": "Expresie Topic Cheie Atribut", + "request-id-expression": "Expresie Cerere ID", + "request-id-json-expression": "Expresie JSON Cerere ID", + "request-id-topic-expression": "Expresie TOPIC Cerere ID", + "response-topic-expression": "Expresie Răspuns Topic", + "value-expression": "Valoare Expresie", + "topic": "Topic", + "timeout": "Timp De Expirare (milisecunde)", + "converter-json-required": "Convertorul JSON este obligatoriu", + "converter-json-parse": "Analiză converter JSON imposibilă", + "filter-expression": "Filtrează expresie", + "connect-requests": "Solicitări Conectare", + "add-connect-request": "Adaugă Solicitare Conectare", + "disconnect-requests": "Solicitări Deconectare", + "add-disconnect-request": "Adaugă Solicitare Deconectare", + "attribute-requests": "Solicitări Atribut", + "add-attribute-request": "Adaugă Solicitare Atribut", + "attribute-updates": "Actualizări Atribut", + "add-attribute-update": "Adaugă Actualizare Atribut", + "server-side-rpc": "Server RPC", + "add-server-side-rpc-request": "Adaugă Solicitare RPC Server-Side", + "device-name-filter": "Filtru Nume Dispozitiv", + "attribute-filter": "Filtru Atribut", + "method-filter": "Filtru Metodă", + "request-topic-expression": "Solicită expresie topic", + "response-timeout": "Timp răspuns în milisecunde", + "topic-expression": "Expresie Topic", + "client-scope": "Scop Client", + "add-device": "Adaugă Dispozitiv", + "opc-server": "Servere", + "opc-add-server": "Adaugă Server", + "opc-add-server-prompt": "Te rog, adaugă server ", + "opc-application-name": "Nume Aplicație", + "opc-application-uri": "URI Aplicație", + "opc-scan-period-in-seconds": "Perioadă Scanare (secunde)", + "opc-security": "Securitate", + "opc-identity": "Identitate", + "opc-keystore": "Keystore", + "opc-type": "Tip OPC", + "opc-keystore-type": "Tip Keystore", + "opc-keystore-location": "Localizare *", + "opc-keystore-password": "Parolă", + "opc-keystore-alias": "Pseudonim", + "opc-keystore-key-password": "Cheie Parolă", + "opc-device-node-pattern": "Structură Nod Dispozitiv", + "opc-device-name-pattern": "Structură Nume Dispozitiv", + "modbus-server": "Servere/sclavi", + "modbus-add-server": "Adaugă server/sclav", + "modbus-add-server-prompt": "Te rog adaugă server/sclav", + "modbus-transport": "Transport", + "modbus-tcp-reconnect": "Reconectare Automată", + "modbus-rtu-over-tcp": "RTU peste TCP", + "modbus-port-name": "Nume Port Serial", + "modbus-encoding": "Codificare", + "modbus-parity": "Paritate", + "modbus-baudrate": "Rată Baud", + "modbus-databits": "Biți Date", + "modbus-stopbits": "Biți Stop", + "modbus-databits-range": "Bitii de date trebuie să fie în intervalul 7-8", + "modbus-stopbits-range": "Bitii de stop trebuie să fie în intervalul 1-2", + "modbus-unit-id": "ID Unitate", + "modbus-unit-id-range": "ID-ul unității trebuie să fie în intervalul 1-247", + "modbus-device-name": "Nume Dispozitiv", + "modbus-poll-period": "Perioadă Interogare (ms)", + "modbus-attributes-poll-period": "Perioadă Interogare Atribute (ms)", + "modbus-timeseries-poll-period": "Perioadă Interogare serii temporale (ms)", + "modbus-poll-period-range": "Perioada de interogare trebuie să fie pozitivă", + "modbus-tag": "Tag", + "modbus-function": "Funcție", + "modbus-register-address": "Adresă Registru", + "modbus-register-address-range": "Adresa registrului trebuie să fie în intervalul 0-65535", + "modbus-register-bit-index": "Index biți", + "modbus-register-bit-index-range": "Indexul biților trebuie să fie în intervalul 0-15", + "modbus-register-count": "Număr Regiștri", + "modbus-register-count-range": "Numărul regiștrilor trebuie să fie pozitiv", + "modbus-byte-order": "Ordine Bytes", + "sync": { + "status": "Stare", + "sync": "Sincronizat", + "not-sync": "Nesincronizat", + "last-sync-time": "Ultima Sincronizare", + "not-available": "Indisponibil" + }, + "export-extensions-configuration": "Exportă configuraţie extensii", + "import-extensions-configuration": "Importă configuraţie extensii", + "import-extensions": "Importă Extensii", + "import-extension": "Importă Extensie", + "export-extension": "Exportă Extensie", + "file": "Fişier Extensii", + "invalid-file-error": "Fişier extensie invalid" + }, + "fullscreen": { + "expand": "Extinde Către Ecran Complet", + "exit": "Ieşire Din Ecran Complet", + "toggle": "Comutare Ecran Complet", + "fullscreen": "Ecran Complet" + }, + "function": { + "function": "Funcţie" + }, + "grid": { + "delete-item-title": "Sigur vrei să ștergi elementul?", + "delete-item-text": "ATENŢIE! După confirmare, elementul şi toate datele referitoare la acesta, vor fi șterse IREVERSIBIL!", + "delete-items-title": "Sigur vrei să ștergi { count, plural, 1 {un element} other {# elemente} }?", + "delete-items-action-title": "Şterge { count, plural, 1 {un element} other {# elemente} }", + "delete-items-text": "ATENŢIE! După confirmare, toate elementele selectate şi toate datele referitoare la acestea, vor fi șterse IREVERSIBIL!", + "add-item-text": "Adaugă Element Nou", + "no-items-text": "Nu Au Fost Găsite Elemente", + "item-details": "Detalii Element", + "delete-item": "Şterge Element", + "delete-items": "Şterge Elemente", + "scroll-to-top": "Derulare la începutul listei" + }, + "help": { + "goto-help-page": "Mergi la pagina de ajutor" + }, + "home": { + "home": "Acasă", + "profile": "Profil", + "logout": "Deconectare", + "menu": "Meniu", + "avatar": "Avatar", + "open-user-menu": "Deschide Meniu Utilizator" + }, + "import": { + "no-file": "Niciun fișier selectat", + "drop-file": "Trage un fişier de tip JSON sau selectează cu mausul un fişier de tip JSON pentru a fi încărcat", + "drop-file-csv": "Trage un fişier de tip CSV sau selectează cu mouse-ul un fişier de tip csv pentru a fi încărcat", + "column-value": "Valoare", + "column-title": "Denumire", + "column-example": "Exemplu valori date", + "column-key": "Cheie Atribut/Telemetrie", + "csv-delimiter": "Delimitator CSV", + "csv-first-line-header": "Prima linie conţine denumiri de coloane", + "csv-update-data": "Actualizare atribute/telemetrie", + "import-csv-number-columns-error": "Un fișier trebuie să conțină cel puțin două coloane", + "import-csv-invalid-format-error": "Format fișier incorect, linia: '{{line}}'", + "column-type": { + "name": "Denumire", + "type": "Tip", + "label": "Etichetă", + "column-type": "Tipul Coloanei", + "client-attribute": "Atribut Client", + "shared-attribute": "Atribut Partajat", + "server-attribute": "Atribut Server", + "timeseries": "Date Cronologice", + "entity-field": "Câmp Entitate", + "access-token": "Token De Acces" + }, + "stepper-text":{ + "select-file": "Selectează un fişier", + "configuration": "Importă configurație", + "column-type": "Selectează tipul de coloane", + "creat-entities": "Creează entităţi noi", + "done": "Terminat" + }, + "message": { + "create-entities": "{{count}} entităţi noi au fost create cu succes", + "update-entities": "{{count}} entităţi noi au fost actualizate cu succes", + "error-entities": "A intervenit o eroare la crearea a {{count}} entităţi" + } + }, + "item": { + "selected": "Selectat" + }, + "js-func": { + "no-return-error": "Funcţia trebuie să returneze o valoare", + "return-type-mismatch": "Funcţia trebuie să returneze o valoare de tip: '{{type}}'", + "tidy": "Tidy" + }, + "key-val": { + "key": "Cheie", + "value": "Valoare", + "remove-entry": "Șterge Intrare", + "add-entry": "Adaugă Intrare", + "no-data": "Nu sunt intrări" + }, + "layout": { + "layout": "Amplasament", + "manage": "Modifică Amplasamente", + "settings": "Configurare Amplasamente", + "color": "Culoare", + "main": "Principal", + "right": "Dreapta", + "select": "Alege Amplasament Țintă" + }, + "legend": { + "direction": "Direcţie Legendă", + "position": "Poziţie Legendă", + "show-max": "Afişează Valoare Maximă", + "show-min": "Afişează Valoare Minimă", + "show-avg": "Afişează Valoare Medie", + "show-total": "Afişează Valoare Totală", + "settings": "Setări Legendă", + "min": "Minim", + "max": "Maxim", + "avg": "Medie", + "total": "Total", + "comparison-time-ago": { + "days": "(Ziua Trecută (Ieri))", + "weeks": "(Săptamâna Trecută)", + "months": "(Luna Trecută)", + "years": "(Anul Trecut)" + } + }, + "login": { + "login": "Conectare", + "request-password-reset": "Solicită Resetarea Parolei", + "reset-password": "Resetează Parolă", + "create-password": "Creează Parolă", + "passwords-mismatch-error": "Parola reintrodusă trebuie să fie identică!", + "password-again": "Rescrie Parola", + "sign-in": "Intră în Cont", + "username": "Nume Utilizator (Adresa De eMail)", + "remember-me": "Ține-mă minte!", + "forgot-password": "Ai Uitat Parola?", + "password-reset": "Resetează Parola", + "expired-password-reset-message": "Parola ta a expirat! Este necesară schimbarea acesteia", + "new-password": "Parolă nouă", + "new-password-again": "Verificare parolă nouă", + "password-link-sent-message": "Ți-am trimis pe eMail un link pentru resetarea parolei", + "email": "eMail", + "login-with": "Conectare cu {{name}}", + "or": "sau" + }, + "position": { + "top": "Sus", + "bottom": "Jos", + "left": "Stânga", + "right": "Dreapta" + }, + "profile": { + "profile": "Profil", + "last-login-time": "Ultima Accesare", + "change-password": "Schimbă Parola", + "current-password": "Parola Actuală" + }, + "relation": { + "relations": "Relaţii", + "direction": "Direcţie", + "search-direction": { + "FROM": "Dinspre", + "TO": "Înspre" + }, + "direction-type": { + "FROM": "Dinspre", + "TO": "Către" + }, + "from-relations": "Ieșire", + "to-relations": "Intrare", + "selected-relations": "{ count, plural, 1 {o relaţie selectată } other {# relaţii selectate } }", + "type": "Tip", + "to-entity-type": "Către Tip Entitate", + "to-entity-name": "Către Nume Entitate", + "from-entity-type": "Dinspre Tip Entitate", + "from-entity-name": "Dinspre Nume Entitate", + "to-entity": "Către Entitate", + "from-entity": "Dinspre Entitate", + "delete": "Şterge Relaţie", + "relation-type": "Tip Relaţie", + "relation-type-required": "Tipul relației este obligatoriu", + "any-relation-type": "Orice Tip", + "add": "Adaugă Relaţie", + "edit": "Şterge Relaţie", + "delete-to-relation-title": "Sigur vrei să ștergi relația către entitatea '{{entityName}}'?", + "delete-to-relation-text": "ATENŢIE! După confirmare,relaţia către entitatea '{{entityName}}' va fi ştearsă", + "delete-to-relations-title": "Sigur vrei să ștergi { count, plural, 1 {o relaţie} other {# relaţii} }?", + "delete-to-relations-text": "ATENŢIE! După confirmare, relaţiile selectate către entităţile corespondente și toate referirile la acestea, vor fi șterse IREVERSIBIL!", + "delete-from-relation-title": "Sigur vrei să ștergi relația dinspre entitatea '{{entityName}}'?", + "delete-from-relation-text": "ATENŢIE! După confirmare,relaţia dinspre entitatea '{{entityName}}' va fi ștearsă", + "delete-from-relations-title": "Sigur vrei să ștergi { count, plural, 1 {o relaţie} other {# relaţii} }?", + "delete-from-relations-text": "ATENŢIE! După confirmare, relaţiile selectate către entităţile corespondente și toate referirile la acestea, vor fi șterse IREVERSIBIL!", + "remove-relation-filter": "Elimină Filtru Relaţie", + "add-relation-filter": "Adaugă Filtru Relaţie", + "any-relation": "Orice Relaţie", + "relation-filters": "Filtre Relaţie", + "additional-info": "Informaţii Adiţionale (JSON)", + "invalid-additional-info": "Informaţiile adiţionale (JSON) nu au putut fi analizate" + }, + "rulechain": { + "rulechain": "Flux", + "rulechains": "Fluxuri", + "root": "Origine", + "delete": "Şterge Fluxuri", + "name": "Denumire", + "name-required": "Denumirea este obligatorie", + "description": "Descriere", + "add": "Adaugă Fluxuri", + "set-root": "Stabileşte Originea Fluxurilor", + "set-root-rulechain-title": "Sigur vrei să setezi '{{ruleChainName}}' ca rădăcină?", + "set-root-rulechain-text": "ATENŢIE! După confirmare, fluxul va deveni rădăcină şi va gestiona toate mesajele de intrare", + "delete-rulechain-title": "Sigur vrei să ștergi fluxul '{{ruleChainName}}'?", + "delete-rulechain-text": "ATENŢIE! După confirmare, fluxul şi toate datele referitoare la acesta, vor fi șterse IREVERSIBIL!", + "delete-rulechains-title": "Sigur vrei să ștergi { count, plural, 1 {un flux} other {# fluxuri} }?", + "delete-rulechains-action-title": "Ştergi { count, plural, 1 {un flux} other {# fluxuri} }", + "delete-rulechains-text": "ATENŢIE! După confirmare, fluxul şi toate datele referitoare la acesta, vor fi șterse IREVERSIBIL!", + "add-rulechain-text": "Adaugă Flux Nou", + "no-rulechains-text": "Nu au fost găsite fluxuri", + "rulechain-details": "Detalii Flux", + "details": "Detalii", + "events": "Evenimente", + "system": "Sistem", + "import": "Importă Flux", + "export": "Exportă Flux", + "export-failed-error": "Fluxul nu poate fi exportat; {{error}}", + "create-new-rulechain": "Creează Flux Nou", + "rulechain-file": "Fişierul Flux", + "invalid-rulechain-file-error": "Fluxul nu poate fi importat; structură date invalidă", + "copyId": "Copiază ID Flux", + "idCopiedMessage": "ID Flux copiat în clipboard", + "select-rulechain": "Selectează Flux", + "no-rulechains-matching": "Nu au fost găsite fluxuri după criteriul: '{{entity}}'", + "rulechain-required": "Fluxul este obligatoriu", + "management": "Administrare Fluxuri", + "debug-mode": "Mod Depanare" + }, + "rulenode": { + "details": "Detalii", + "events": "Evenimente", + "search": "Noduri Căutare", + "open-node-library": "Bibliotecă Noduri", + "add": "Adaugă Regulă Nod", + "name": "Denumire", + "name-required": "Denumirea este obligatorie", + "type": "Tip", + "description": "Descriere", + "delete": "Şterge Regulă Nod", + "select-all-objects": "Selectează Toate Nodurile Şi Conexiunile", + "deselect-all-objects": "Deselectează Toate Nodurile Şi Conexiunile", + "delete-selected-objects": "Şterge Toate Nodurile Şi Conexiunile", + "delete-selected": "Şterge Selecţia", + "select-all": "Selectează Tot", + "copy-selected": "Copiază Selecţia", + "deselect-all": "Deselectează Tot", + "rulenode-details": "Detalii Regulă Nod", + "debug-mode": "Mod Depanare", + "configuration": "Configurare", + "link": "Link", + "link-details": "Detalii Link Regulă Nod", + "add-link": "Adaugă Link", + "link-label": "Etichetă Link", + "link-label-required": "Eticheta link-ului este obligatorie", + "custom-link-label": "Etichetă Link Definit de Utilizator", + "custom-link-label-required": "Eticheta pentru link, definită de către utilizator, este obligatorie", + "link-labels": "Etichete Link-uri)", + "link-labels-required": "Etichetele link-urilor sunt obligatorii", + "no-link-labels-found": "Nu au fost găsite etichete pentru link", + "no-link-label-matching": "Eticheta : '{{label}}' nu a fost găsită", + "create-new-link-label": "Creează Etichetă Nouă", + "type-filter": "Filtrează", + "type-filter-details": "Filtrează mesajele de intrare după condiţiile configurate", + "type-enrichment": "Îmbogățire", + "type-enrichment-details": "Adaugă informaţii adiţionale in metadata mesajului", + "type-transformation": "Transformare", + "type-transformation-details": "Schimbă payload şi metadata mesajului", + "type-action": "Acţiune", + "type-action-details": "Execută o acţiune specială", + "type-external": "Extern", + "type-external-details": "Interacţiuni cu sisteme externe", + "type-rule-chain": "Flux", + "type-rule-chain-details": "Transmite mesajele de intrare către fluxul specificat", + "type-input": "Intrare", + "type-input-details": "Intrarea logică pentru flux, transmite mesajele de intrare către următoarea regulă de nod înrudită", + "type-unknown": "Necunoscut", + "type-unknown-details": "Detalii regulă nod necunoscute", + "directive-is-not-loaded": "Configurarea definită pentru directiva : '{{directiveName}}' nu este disponibilă", + "ui-resources-load-error": "Eroare la încărcarea configurării resurselor UI", + "invalid-target-rulechain": "Fluxul Destinaţie nu a fost găsit", + "test-script-function": "Funcţie Test Script", + "message": "Mesaj", + "message-type": "Tip Mesaj", + "select-message-type": "Selectează Tipul Mesajului", + "message-type-required": "Tipul mesajului este obligatoriu", + "metadata": "Metadata", + "metadata-required": "Intrările metadata nu pot fi vide", + "output": "Ieşire", + "test": "Test", + "help": "Ajutor", + "reset-debug-mode": "Dezactivează modul depanare în toate nodurile" + }, + "tenant": { + "tenant": "Locatar", + "tenants": "Locatari", + "management": "Administrare Locatar", + "add": "Adaugă Locatar", + "admins": "Administratori", + "manage-tenant-admins": "Gestionare Administratori Locatar", + "delete": "Şterge Locatar", + "add-tenant-text": "Adaugă Locatar Nou", + "no-tenants-text": "Nu au fost găsiţi locatari", + "tenant-details": "Detalii Locatar", + "delete-tenant-title": "Sigur vrei să ștergi locatarul: '{{tenantTitle}}'?", + "delete-tenant-text": "ATENŢIE! După confirmare, locatarul şi toate datele referitoare la acesta, vor fi șterse IREVERSIBIL!", + "delete-tenants-title": "Sigur vrei să ștergi { count, plural, 1 {un locatar} other {# locatari} } ?", + "delete-tenants-action-title": "Şterge { count, plural, 1 {un locatar} other {# locatari} }", + "delete-tenants-text": "ATENŢIE! După confirmare, locatarii selectaţi şi datele aferente acestora, vor fi șterse IREVERSIBIL!", + "title": "Titlu", + "title-required": "Titlul este obligatoriu", + "description": "Descriere", + "details": "Detalii", + "events": "Evenimente", + "copyId": "Copiază ID Locatar", + "idCopiedMessage": "ID Locatar a fost copiat în clipboard", + "select-tenant": "Selectează locatar", + "no-tenants-matching": "Nu au fost găsiţi locatari după criteriul: '{{entity}}'", + "tenant-required": "Locatarul este obligatoriu" + }, + "timeinterval": { + "seconds-interval": "{ seconds, plural, 1 {o secundă} other {# secunde} }", + "minutes-interval": "{ minutes, plural, 1 {un minut} other {# minute} }", + "hours-interval": "{ hours, plural, 1 {o oră} other {# ore} }", + "days-interval": "{ days, plural, 1 {o zi} other {# zile} }", + "days": "Zile", + "hours": "Ore", + "minutes": "Minute", + "seconds": "Secunde", + "advanced": "Personalizat" + }, + "timewindow": { + "days": "{ days, plural, 1 {o zi} other {# zile} }", + "hours": "{ hours, plural, 1 {o oră} other {# ore} }", + "minutes": "{ minutes, plural, 1 {un minut} other {# minute} }", + "seconds": "{ seconds, plural, 1 {o secundă} other {# secunde} }", + "realtime": "Timp Real", + "history": "Istoric", + "last-prefix": "Interval:", + "period": "Început {{ startTime}} Sfârșit {{endTime}}", + "edit": "Editează Interval", + "date-range": "Interval Date", + "last": "Ultima/Ultimele", + "time-period": "Interval:", + "hide": "Ascunde" + }, + "user": { + "user": "Utilizator", + "users": "Utilizatori", + "customer-users": "Utilizatori Client", + "tenant-admins": "Administratori Locatar", + "sys-admin": "Administratori Sistem", + "tenant-admin": "Administrator Locatar", + "customer": "Clienţi", + "anonymous": "Anonim", + "add": "Adaugă Utilizator", + "delete": "Şterge Utilizator", + "add-user-text": "Adaugă Utilizator Nou", + "no-users-text": "Nu Există Utilizatori", + "user-details": "Detalii Utilizator", + "delete-user-title": "Sigur vrei să ștergi utilizatorul '{{userEmail}}'?", + "delete-user-text": "ATENŢIE! După confirmare, utilizatorul şi toate datele aferente acestuia, vor fi șterse IREVERSIBIL!", + "delete-users-title": "Sigur vrei să ștergi { count, plural, 1 {un utilizator} other {# utilizatori} }?", + "delete-users-action-title": "Ştergere { count, plural, 1 {un utilizator} other {# utilizatori} }", + "delete-users-text": "ATENŢIE! După confirmare, toţi utilizatorii selectaţi împreună cu datele aferente acestora, vor fi șterse IREVERSIBIL!", + "activation-email-sent-message": "Mesajul eMail pentru activare a fost trimis cu succes!", + "resend-activation": "Retrimite mesaj eMail de activare", + "email": "Adresă eMail", + "email-required": "Adresa eMail este obligatorie", + "invalid-email-format": "Adresa eMail este incorectă", + "first-name": "Prenume", + "last-name": "Nume", + "description": "Descriere", + "default-dashboard": "Panou Implicit", + "always-fullscreen": "Permanent Ecran Complet", + "select-user": "Selectează Utilizator", + "no-users-matching": "Nu există utilizatori care corespund criteriului: '{{entity}}'", + "user-required": "Utilizatorul este obligatoriu", + "activation-method": "Metoda De Activare", + "display-activation-link": "Afişează link activare", + "send-activation-mail": "Trimite mesaj eMail pentru activare", + "activation-link": "Link activare utilizator:", + "activation-link-text": "Pentru activarea contului, folosiți link: ", + "copy-activation-link": "Copiază link activare", + "activation-link-copied-message": "Link-ul de activare utilizator a fost copiat în clipboard", + "details": "Detalii", + "login-as-tenant-admin": "Acces ca locatar administrator", + "login-as-customer-user": "Acces ca utilizator client", + "disable-account": "Dezactivează cont utilizator", + "enable-account": "Activează cont utilizator", + "enable-account-message": "Cont utilizator activat!", + "disable-account-message": "Cont utilizator dezactivat!" + }, + "value": { + "type": "Tip Valoare", + "string": "Şir Caractere", + "string-value": "Valoare Şir Caractere", + "integer": "Număr Întreg", + "integer-value": "Valoare Număr Întreg", + "invalid-integer-value": "Valoare număr întreg incorectă", + "double": "Tip Double", + "double-value": "Valoare Tip Double", + "boolean": "Tip Bool: ", + "boolean-value": "Valoare Bool", + "false": "Fals", + "true": "Adevărat", + "long": "Tip Long" + }, + "widget": { + "widget-library": "Biblioteci Widgets", + "widget-bundle": "Pachete Widgets", + "select-widgets-bundle": "Selectează Pachete Widgets", + "management": "Administrare Widgets", + "editor": "Editor Widget", + "widget-type-not-found": "Eroare la încărcarea configuraţiei widgetului.
    Probabil Asocierea \n cu tipul de widget a fost eliminată", + "widget-type-load-error": "Widgetul nu a fost încărcat din cauza următoarelor erori:", + "remove": "Elimină Widget", + "edit": "Editează Widget", + "remove-widget-title": "Sigur vrei să ștergi widgetul '{{widgetTitle}}'?", + "remove-widget-text": "ATENŢIE! După confirmare, widgetul şi toate datele aferente acestuia, vor fi șterse IREVERSIBIL!", + "timeseries": "Serii Temporale", + "search-data": "Caută Date", + "no-data-found": "Nu Au Fost Găsite Date", + "latest-values": "Ultimele Valori", + "rpc": "Widget Control ", + "alarm": "Widget Alarmă", + "static": "Widget Static", + "select-widget-type": "Selectaţi Tip Widget", + "missing-widget-title-error": "Titlul widgetului trebuie specificat!", + "widget-saved": "Widget Salvat", + "unable-to-save-widget-error": "Widgetul conține erori și nu poate fi salvat!", + "save": "Salvează Widget", + "saveAs": "Salvează Widget Ca...", + "save-widget-type-as": "Salvează Tip Widget Ca...", + "save-widget-type-as-text": "Introduceţi titlu nou widget şi/sau selectați pachet widget destinație", + "toggle-fullscreen": "Comută Ecran Complet", + "run": "Execută Widget", + "title": "Titlu Widget", + "title-required": "Titlul widgetului este obligatoriu", + "type": "Tip Widget", + "resources": "Resurse", + "resource-url": "JavaScript/CSS URL", + "remove-resource": "Şterge Resursă", + "add-resource": "Adaugă Resursă", + "html": "HTML", + "tidy": "Tidy", + "css": "CSS", + "settings-schema": "Schemă setări", + "datakey-settings-schema": "Schemă setări chei date", + "javascript": "Javascript", + "js": "JS", + "remove-widget-type-title": "Sigur vrei să ștergi tip widget '{{widgetName}}'?", + "remove-widget-type-text": "ATENŢIE! După confirmare, tipul de widget şi toate datele aferente acestuia, vor fi șterse IREVERSIBIL!", + "remove-widget-type": "Şterge Tip Widget", + "add-widget-type": "Adaugă Tip Nou Widget", + "widget-type-load-failed-error": "Eroare încărcare tip widget!", + "widget-template-load-failed-error": "Eroare încarcare şablon widget!", + "add": "Adaugă Widget Nou", + "undo": "Anulează Modificări Widget", + "export": "Exportă Widget" + }, + "widget-action": { + "header-button": "Buton Principal Widget", + "open-dashboard-state": "Deschide Altă Stare a Panoului", + "update-dashboard-state": "Actualizează Starea Curentă A Panoului", + "open-dashboard": "Comută Către Alt Panou", + "custom": "Acţiuni Utilizator", + "custom-pretty": "Acţiuni Utilizator (cu şablon HTML)", + "target-dashboard-state": "Stare Panou Destinaţie", + "target-dashboard-state-required": "Starea panoului de destinaţie este obligatorie!", + "set-entity-from-widget": "Setează Entitate din Widget", + "target-dashboard": "Panou Destinaţie", + "open-right-layout": "Deschide Aspect Corect Al Panoului (accesare de pe mobil)" + }, + "widgets-bundle": { + "current": "Pachet Curent Widgeturi", + "widgets-bundles": "Pachete Widgeturi", + "add": "Adăugare Pachete Widgeturi", + "delete": "Ştergere Pachete Widgeturi", + "title": "Titlu", + "title-required": "Titlul este obligatoriu", + "add-widgets-bundle-text": "Adaugă pachet nou widgeturi", + "no-widgets-bundles-text": "Nu există pachete widgeturi", + "empty": "Pachetul de widgeturi este gol", + "details": "Detalii", + "widgets-bundle-details": "Detalii Pachet Widgeturi", + "delete-widgets-bundle-title": "Sigur vrei să ștergi pachetul de widgeturi '{{widgetsBundleTitle}}'?", + "delete-widgets-bundle-text": "ATENŢIE! După Confirmare, pachetul de widgeturi şi toate datele aferente acestuia, vor fi șterse IREVERSIBIL!", + "delete-widgets-bundles-title": "Sigur vrei să ștergi { count, plural, 1 {un pachet widgeturi} other {# pachete widgeturi} }?", + "delete-widgets-bundles-action-title": "Şterge { count, plural, 1 {un packet widgeturi} other {# pachete widgeturi} }", + "delete-widgets-bundles-text": "ATENŢIE! După confirmare, toate pachetele selectate de widget-uri şi datele aferente acestuia, vor fi șterse IREVERSIBIL!", + "no-widgets-bundles-matching": "Nu au fost găsite pachete de widgeturi conținând textul '{{widgetsBundle}}' ", + "widgets-bundle-required": "Denumirea pachetelor de widgeturi este obligatorie", + "system": "Sistem", + "import": "Importă Pachet Widgeturi", + "export": "Exportă Pachet Widgeturi", + "export-failed-error": "Export pachet widgeturi imposibil: {{error}}", + "create-new-widgets-bundle": "Definire Pachet Widgeturi Nou", + "widgets-bundle-file": "Alege fișier pachet widgeturi", + "invalid-widgets-bundle-file-error": "Export pachet widgeturi imposibil; Structură date invalidă" + }, + "widget-config": { + "data": "Date", + "settings": "Setări", + "advanced": "Setări Avansate", + "title": "Titlu", + "title-tooltip": "Mesaj Detalii Titlu", + "general-settings": "Setări Generale", + "display-title": "Titlu Afişat", + "drop-shadow": "Cu Umbră", + "enable-fullscreen": "Permite Ecran Complet", + "background-color": "Culoare Fundal", + "text-color": "Culoare Text", + "padding": "Margine Interioară", + "margin": "Margine Exterioară", + "widget-style": "Stil Widget", + "title-style": "Stil Titlu", + "mobile-mode-settings": "Setări Afișare Mobil", + "order": "Ordine", + "height": "Înăltime", + "units": "Unitate măsură", + "decimals": "Număr Zecimale", + "timewindow": "Interval Timp", + "use-dashboard-timewindow": "Folosire Interval Timp Panou", + "display-timewindow": "Afişare Interval Timp", + "display-legend": "Afişare Legendă", + "datasources": "Surse Date", + "maximum-datasources": "Maximum { count, plural, 1 {o sursă date permisă} other {# surse date permise} }", + "datasource-type": "Tip", + "datasource-parameters": "Parametri", + "remove-datasource": "Elimină Sursă Date", + "add-datasource": "Adaugă Sursă Date", + "target-device": "Dispozitiv Destinaţie", + "alarm-source": "Sursă Alarmă", + "actions": "Acţiuni", + "action": "Acţiune", + "add-action": "Adaugă Acţiune", + "search-actions": "Caută Acţiuni", + "action-source": "Sursa Acțiunii", + "action-source-required": "Sursa acțiunii este obligatorie", + "action-name": "Numele Acțiunii", + "action-name-required": "Numele acțiunii este obligatoriu", + "action-name-not-unique": "O acţiune cu acelaşi nume este deja definită
    Numele definit al acțiunii trebuie să fie unic in aceeaşi sursă de date", + "action-icon": "Pictogramă", + "action-type": "Tipul", + "action-type-required": "Tipul acțiunii este obligatoriu", + "edit-action": "Editare Acţiune", + "delete-action": "Ştergere Acţiune", + "delete-action-title": "Şterge acţiunea ", + "delete-action-text": "Ești sigur că vrei să ștergi acţiunea '{{actionName}}'?", + "display-icon": "Afişează Pictograma Titlului", + "icon-color": "Culoare Pictogramă", + "icon-size": "Mărime Pictogramă" + }, + "widget-type": { + "import": "Import Tip Widget", + "export": "Export Tip Widget", + "export-failed-error": "Eroare! Export imposibil pentru tip widget: {{error}}", + "create-new-widget-type": "Defineşte tip widget nou", + "widget-type-file": "Alege fişier pentru tip widget", + "invalid-widget-type-file-error": "Eroare! Tip widget nu poate fi importat; structură de date invalidă" + }, + "widgets": { + "date-range-navigator": { + "localizationMap": { + "Sun": "D", + "Mon": "L", + "Tue": "M", + "Wed": "M", + "Thu": "J", + "Fri": "V", + "Sat": "S", + "Jan": "Ian", + "Feb": "Feb", + "Mar": "Mar", + "Apr": "Apr", + "May": "Mai", + "Jun": "Iun", + "Jul": "Iul", + "Aug": "Aug", + "Sep": "Sep", + "Oct": "Oct", + "Nov": "Nov", + "Dec": "Dec", + "January": "Ianuarie", + "February": "Februarie", + "March": "Martie", + "April": "Aprilie", + "Maz": "Mai", + "June": "Iunie", + "July": "Iulie", + "August": "August", + "September": "Septembrie", + "October": "Octombrie", + "November": "Noiembrie", + "December": "Decembrie", + "Custom Date Range": "Interval Date Personalizat", + "Date Range Template": "Șablon Interval Date", + "Today": "Astăzi", + "Yesterday": "Ieri", + "This Week": "Săptămâna Aceasta", + "Last Week": "Săptămâna Trecută", + "This Month": "Luna Aceasta", + "Last Month": "Luna Trecută", + "Year": "Anul", + "This Year": "Anul Acesta", + "Last Year": "Anul Trecut", + "Date picker": "Alege Data", + "Hour": "Oră", + "Day": "Zi", + "Week": "Săptămână", + "2 weeks": "2 săptămâni", + "Month": "Lună", + "3 months": "3 luni", + "6 months": "6 luni", + "Custom interval": "Interval Personalizat", + "Interval": "Interval:", + "Step size": "Pas", + "Ok": "Ok" + } + }, + "input-widgets": { + "attribute-not-allowed": "Acest widget nu poate folosi atributul specificat", + "blocked-location": "Acest browser blochează localizarea", + "claim-device": "Revendică Dispozitiv", + "claim-failed": "Încercarea revendicare dispozitiv eșuată", + "claim-not-found": "Dispozitivul nu a fost găsit", + "claim-successful": "Dispozitivul a fost revendicat cu succes", + "date": "Data", + "device-name": "Nume Dispozitiv", + "device-name-required": "Numele dispozitivului este obligatoriu", + "discard-changes": "Anulare Modificări", + "entity-attribute-required": "Atributul entităţii este obligatoriu", + "entity-coordinate-required": "Atât latitudinea Şi longitudinea sunt obligatorii", + "entity-timeseries-required": "Seriile temporale pentru entitate sînt obligatorii", + "get-location": "Află locaţia GPS actuală", + "latitude": "Latitudine", + "longitude": "Longitudine", + "not-allowed-entity": "Entitatea selectată nu poate avea atribute partajate", + "no-attribute-selected": "Niciun Atribut Selectat", + "no-datakey-selected": "Nicio cheie selectată", + "no-coordinate-specified": "Cheie date latitude/longitude nespecificată", + "no-entity-selected": "Nicio entitate selectată", + "no-image": "Lipsă Imagine", + "no-support-geolocation": "Acest browser nu permite geolocalizarea", + "no-support-web-camera": "Cameră Web nesuportată", + "no-timeseries-selected": "Serii temporale nespecificate", + "secret-key": "Cheie Secretă", + "secret-key-required": "Cheia secretă este obligatorie", + "switch-attribute-value": "Schimbă valoare atribut entitate", + "switch-camera": "Schimbă Camera", + "switch-timeseries-value": "Schimbă valori serii temporale entitate", + "take-photo": "Captură Imagine", + "time": "Timp", + "timeseries-not-allowed": "Parametrul nu este compatibil cu acest widget", + "update-failed": "Actualizare eșuată", + "update-successful": "Actualizare reușită", + "update-attribute": "Actualizare Atribut", + "update-timeseries": "Actualizare Serii Temporale", + "value": "Valoare" + } + }, + "icon": { + "icon": "Pictogramă", + "select-icon": "Selectează Pictogramă", + "material-icons": "Material Pictogramă", + "show-all": "Afişează Toate Pictogramele" + }, + "custom": { + "widget-action": { + "action-cell-button": "Acțiune buton celulă", + "row-click": "Eveniment : Click pe linie tabel", + "polygon-click": "Eveniment : Click pe poligon", + "marker-click": "Eveniment : Click pe marker", + "tooltip-tag-action": "Acţiune marcaj detalii mesaj ", + "node-selected": "Eveniment : Nod Selectat", + "element-click": "eveniment : click Pe element HTML", + "pie-slice-click": "Eveniment : Click pe sector cerc", + "row-double-click": "Eveniment : Dublu click pe linia tabelului" + } + }, + "language": { + "language": "Limba" + } +} diff --git a/ui-ngx/src/assets/locale/locale.constant-ru_RU.json b/ui-ngx/src/assets/locale/locale.constant-ru_RU.json new file mode 100644 index 0000000000..5e0dca82aa --- /dev/null +++ b/ui-ngx/src/assets/locale/locale.constant-ru_RU.json @@ -0,0 +1,1802 @@ +{ + "access": { + "unauthorized": "Неавторизированный", + "unauthorized-access": "Несанкционированный доступ", + "unauthorized-access-text": "Вы должны войти в систему для получения доступа к этому ресурсу!", + "access-forbidden": "Доступ запрещен", + "access-forbidden-text": "У вас нет прав доступа к этому ресурсу!
    Для получения доступа попробуйте войти под другим пользователем.", + "refresh-token-expired": "Сессия истекла", + "refresh-token-failed": "Не удалось обновить сессию" + }, + "action": { + "activate": "Активировать", + "suspend": "Приостановить", + "save": "Сохранить", + "saveAs": "Сохранить как", + "cancel": "Отмена", + "ok": "ОК", + "delete": "Удалить", + "add": "Добавить", + "yes": "Да", + "no": "Нет", + "update": "Обновить", + "remove": "Удалить", + "search": "Поиск", + "clear-search": "Очистить", + "assign": "Присвоить", + "unassign": "Отозвать", + "share": "Поделиться", + "make-private": "Закрыть для общего доступа", + "apply": "Применить", + "apply-changes": "Применить изменения", + "edit-mode": "Режим редактирования", + "enter-edit-mode": "Режим редактирования", + "decline-changes": "Отменить изменения", + "close": "Закрыть", + "back": "Назад", + "run": "Запуск", + "sign-in": "Войти", + "edit": "Редактировать", + "view": "Просмотреть", + "create": "Создать", + "drag": "Переместить", + "refresh": "Обновить", + "undo": "Откатить", + "copy": "Копировать", + "paste": "Вставить", + "copy-reference": "Копировать ссылку", + "paste-reference": "Вставить ссылку", + "import": "Импортировать", + "export": "Экспортировать", + "share-via": "Поделиться в {{provider}}", + "continue": "Продолжить", + "discard-changes": "Отменить изменения" + }, + "aggregation": { + "aggregation": "Агрегация", + "function": "Тип агрегации данных", + "limit": "Максимальное значение", + "group-interval": "Интервал группировки", + "min": "Мин", + "max": "Maкс", + "avg": "Среднее", + "sum": "Сумма", + "count": "Количество", + "none": "Без агрегации" + }, + "admin": { + "general": "Общие", + "general-settings": "Общие настройки", + "outgoing-mail": "Исходящая почта", + "outgoing-mail-settings": "Настройки исходящей почты", + "system-settings": "Системные настройки", + "test-mail-sent": "Пробное письмо успешно отправлено!", + "base-url": "Базовая URL", + "base-url-required": "Базовая URL обязательна.", + "mail-from": "Отправитель", + "mail-from-required": "Отправитель обязателен.", + "smtp-protocol": "SMTP протокол", + "smtp-host": "SMTP хост", + "smtp-host-required": "SMTP хост обязателен.", + "smtp-port": "SMTP порт", + "smtp-port-required": "SMTP порт обязателен.", + "smtp-port-invalid": "Недействительный SMTP порт.", + "timeout-msec": "Таймаут (мс)", + "timeout-required": "Таймаут обязателен.", + "timeout-invalid": "Недействительный таймаут.", + "enable-tls": "Включить TLS", + "tls-version" : "Версия TLS", + "send-test-mail": "Отправить пробное письмо", + "security-settings": "Настройки безопасности", + "password-policy": "Политика паролей", + "minimum-password-length": "Минимальная длина пароля", + "minimum-password-length-required": "Требуется минимальная длина пароля", + "minimum-password-length-range": "Минимальная длина пароля должна быть в диапазоне от 5 до 50", + "minimum-uppercase-letters": "Минимальное количество прописных букв", + "minimum-uppercase-letters-range": "Минимальное количество прописных букв не может быть отрицательным", + "minimum-lowercase-letters": "Минимальное количество строчных букв", + "minimum-lowercase-letters-range": "Минимальное количество строчных букв не может быть отрицательным", + "minimum-digits": "Минимальное количество цифр", + "minimum-digits-range": "Минимальное количество цифр не может быть отрицательным", + "minimum-special-characters": "Минимальное количество специальных символов", + "minimum-special-characters-range": "Минимальное количество специальных символов не может быть отрицательным", + "password-expiration-period-days": "Срок действия пароля в днях", + "password-expiration-period-days-range": "Срок действия пароля в днях не может быть отрицательным", + "password-reuse-frequency-days": "Частота повторного использования пароля в днях", + "password-reuse-frequency-days-range": "Частота повторного использования пароля в днях не может быть отрицательной", + "general-policy": "Общая политика", + "max-failed-login-attempts": "Максимальное количество неудачных попыток входа в систему, прежде чем учетная запись заблокирована", + "minimum-max-failed-login-attempts-range": "Максимальное количество неудачных попыток входа в систему не может быть отрицательным", + "user-lockout-notification-email": "В случае блокировки учетной записи пользователя отправьте уведомление на электронную почту" + }, + "alarm": { + "alarm": "Оповещение", + "alarms": "Оповещения", + "select-alarm": "Выбрать оповещение", + "no-alarms-matching": "Оповещения '{{entity}}' не найдены.", + "alarm-required": "Оповещение обязательно", + "alarm-status": "Статус оповещения", + "search-status": { + "ANY": "Все", + "ACTIVE": "Активные", + "CLEARED": "Сброшенные", + "ACK": "Подтвержденные", + "UNACK": "Неподтвержденные" + }, + "display-status": { + "ACTIVE_UNACK": "Активные неподтвержденные", + "ACTIVE_ACK": "Активные подтвержденные", + "CLEARED_UNACK": "Сброшенные неподтвержденные", + "CLEARED_ACK": "Сброшенные подтвержденные" + }, + "no-alarms-prompt": "Оповещения отсутствуют", + "created-time": "Время создания", + "type": "Тип", + "severity": "Уровень", + "originator": "Инициатор", + "originator-type": "Тип инициатора", + "details": "Подробности", + "status": "Статус", + "alarm-details": "Подробности об оповещении", + "start-time": "Время начала", + "end-time": "Время окончания", + "ack-time": "Время подтверждения", + "clear-time": "Время сброса", + "severity-critical": "Критический", + "severity-major": "Основной", + "severity-minor": "Второстепенный", + "severity-warning": "Предупреждение", + "severity-indeterminate": "Неопределенный", + "acknowledge": "Подтвердить", + "clear": "Сбросить", + "search": "Поиск оповещений", + "selected-alarms": "Выбрано { count, plural, 1 {1 оповещение} few {# оповещения} other {# оповещений} }", + "no-data": "Нет данных для отображения", + "polling-interval": "Интервал опроса оповещений (сек)", + "polling-interval-required": "Интервал опроса оповещений обязателен.", + "min-polling-interval-message": "Минимальный интервал опроса оповещений 1 секунда.", + "aknowledge-alarms-title": "Подтвердить { count, plural, 1 {1 оповещение} other {# оповещений} }", + "aknowledge-alarms-text": "Вы точно хотите подтвердить { count, plural, 1 {1 оповещение} other {# оповещений} }?", + "aknowledge-alarm-title": "Подтвердить оповещение", + "aknowledge-alarm-text": "Вы точно хотите подтвердить оповещение?", + "clear-alarms-title": "Сбросить { count, plural, 1 {1 оповещение} other {# оповещений} }", + "clear-alarms-text": "Вы точно хотите сбросить { count, plural, 1 {1 оповещение} other {# оповещений} }?", + "clear-alarm-title": "Сбросить оповещение", + "clear-alarm-text": "Вы точно хотите сбросить оповещение?", + "alarm-status-filter": "Фильтр оповещений", + "max-count-load": "Максимальное количество оповещений для загрузки (0 - неограниченно)", + "max-count-load-required": "Максимальное количество оповещений для загрузки обязателен.", + "max-count-load-error-min": "Минимальное значение 0.", + "fetch-size": "Размер пакета для загрузки", + "fetch-size-required": "Размер пакета для загрузки обязателен.", + "fetch-size-error-min": "Минимальное значение 10." + }, + "alias": { + "add": "Добавить псевдоним", + "edit": "Редактировать псевдоним", + "name": "Псевдоним", + "name-required": "Псевдоним обязателен", + "duplicate-alias": "Такой псевдоним уже существует.", + "filter-type-single-entity": "Отдельный объект", + "filter-type-entity-list": "Список объектов", + "filter-type-entity-name": "Название объекта", + "filter-type-state-entity": "Объект из состояния дашборда", + "filter-type-state-entity-description": "Объект, полученный из параметров состояния дашборда", + "filter-type-asset-type": "Тип актива", + "filter-type-asset-type-description": "Активы типа '{{assetType}}'", + "filter-type-asset-type-and-name-description": "Активы типа '{{assetType}}' и названием, начинающимся с '{{prefix}}'", + "filter-type-device-type": "Тип устройства", + "filter-type-device-type-description": "Устройства типа '{{deviceType}}'", + "filter-type-device-type-and-name-description": "Устройства типа '{{deviceType}}' и названием, начинающимся с '{{prefix}}'", + "filter-type-entity-view-type": "Тип Представления Объекта", + "filter-type-entity-view-type-description": "Представления Объекта типа '{{entityView}}'", + "filter-type-entity-view-type-and-name-description": "Представления Объекта типа '{{entityView}}' и названием, начинающимся с '{{prefix}}'", + "filter-type-relations-query": "Запрос по типу отношений", + "filter-type-relations-query-description": "{{entities}}, имеющие отношение типа {{relationType}} {{direction}} {{rootEntity}}", + "filter-type-asset-search-query": "Поисковый запрос по активам", + "filter-type-asset-search-query-description": "Активы типа {{assetTypes}}, имеющие отношение типа {{relationType}} {{direction}} {{rootEntity}}", + "filter-type-device-search-query": "Поисковый запрос по устройствам", + "filter-type-device-search-query-description": "Устройства типа {{deviceTypes}}, имеющие отношение типа {{relationType}} {{direction}} {{rootEntity}}", + "filter-type-entity-view-search-query": "Поисковый запрос по представлениям объектов", + "filter-type-entity-view-search-query-description": "Представления объектов типа {{entityViewTypes}}, имеющие отношение типа {{relationType}} {{direction}} {{rootEntity}}", + "entity-filter": "Фильтр объектов", + "resolve-multiple": "Принять как несколько объектов", + "filter-type": "Тип фильтра", + "filter-type-required": "Тип фильтра обязателен.", + "entity-filter-no-entity-matched": "Объекты, соответствующие фильтру, не найдены.", + "no-entity-filter-specified": "Не указан фильтр объектов", + "root-state-entity": "Использовать объект, полученный из дашборда, как корневой", + "last-level-relation": "Использовать только отношения последнего уровня", + "root-entity": "Корневой объект", + "state-entity-parameter-name": "Название объекта состояния", + "default-state-entity": "Объект состояния по умолчанию", + "default-entity-parameter-name": "По умолчанию", + "max-relation-level": "Максимальная глубина отношений", + "unlimited-level": "Неограниченная глубина", + "state-entity": "Объект состояния дашборда", + "all-entities": "Все объекты", + "any-relation": "не указано" + }, + "asset": { + "asset": "Актив", + "assets": "Активы", + "management": "Управление активами", + "view-assets": "Просмотреть активы", + "add": "Добавить актив", + "assign-to-customer": "Присвоить клиенту", + "assign-asset-to-customer": "Присвоить актив(ы) клиенту", + "assign-asset-to-customer-text": "Пожалуйста, выберите активы, которые нужно присвоить объекту", + "no-assets-text": "Активы не найдены", + "assign-to-customer-text": "Пожалуйста, выберите клиента, которому нужно присвоить актив(ы)", + "public": "Общедоступные", + "assignedToCustomer": "Присвоить клиенту", + "make-public": "Открыть общий доступ к активу", + "make-private": "Закрыть общий доступ к активу", + "unassign-from-customer": "Отозвать у клиента", + "delete": "Удалить актив", + "asset-public": "Актив общедоступный", + "asset-type": "Тип актива", + "asset-type-required": "Тип актива обязателен.", + "select-asset-type": "Выберите тип актива", + "enter-asset-type": "Введите тип актива", + "any-asset": "Любой актив", + "no-asset-types-matching": "Активы типа '{{entitySubtype}}' не найдены.", + "asset-type-list-empty": "Типы активов не выбраны.", + "asset-types": "Типы активов", + "name": "Название", + "name-required": "Название обязательно.", + "description": "Описание", + "type": "Тип", + "type-required": "Тип обязателен.", + "details": "Подробности", + "events": "События", + "add-asset-text": "Добавить новый актив", + "asset-details": "Подробности об активе", + "assign-assets": "Присвоить активы", + "assign-assets-text": "Присвоить { count, plural, 1 {1 актив} few {# актива} other {# активов} } клиенту", + "delete-assets": "Удалить активы", + "unassign-assets": "Отозвать активы", + "unassign-assets-action-title": "Отозвать { count, plural, 1 {1 актив} few {# актива} other {# активов} } у клиента", + "assign-new-asset": "Присвоить новый актив", + "delete-asset-title": "Вы точно хотите удалить '{{assetName}}'?", + "delete-asset-text": "Внимание, после подтверждения актив и все связанные с ним данные будут безвозвратно удалены.", + "delete-assets-title": "Вы точно хотите удалить { count, plural, 1 {1 актив} few {# актива} other {# активов} }", + "delete-assets-action-title": "Удалить { count, plural, 1 {1 актив} few {# актива} other {# активов} }", + "delete-assets-text": "Внимание, после подтверждения выбранные активы и все связанные с ними данные будут безвозвратно удалены.", + "make-public-asset-title": "Вы точно хотите открыть общий доступ к активу '{{assetName}}'?", + "make-public-asset-text": "Внимание, после подтверждения актив и все связанные с ним данные станут общедоступными.", + "make-private-asset-title": "Вы точно хотите закрыть общий доступ к активу '{{assetName}}'?", + "make-private-asset-text": "После подтверждения актив и все связанные с ним данные будут закрыты для общего доступа", + "unassign-asset-title": "Вы точно хотите отозвать актив '{{assetName}}'?", + "unassign-asset-text": "После подтверждения актив будут отозван, и клиент потеряет к нему доступ.", + "unassign-asset": "Отозвать актив", + "unassign-assets-title": "Вы точно хотите отозвать { count, plural, 1 {1 актив} few {# актива} other {# активов} }?", + "unassign-assets-text": "После подтверждения активы будут отозваны, и клиент потеряет к ним доступ.", + "copyId": "Копировать ИД актива", + "idCopiedMessage": "ИД актива скопировано в буфер обмена", + "select-asset": "Выбрать активы", + "no-assets-matching": "Активы, соответствующие '{{entity}}', не найдены.", + "asset-required": "Актив обязателен", + "name-starts-with": "Название актива, начинающееся с", + "import": "Импортировать активы", + "asset-file": "Файл с активами", + "label": "Метка" + }, + "attribute": { + "attributes": "Атрибуты", + "latest-telemetry": "Последняя телеметрия", + "attributes-scope": "Контекст атрибутов объекта", + "scope-latest-telemetry": "Последняя телеметрия", + "scope-client": "Клиентские атрибуты", + "scope-server": "Серверные атрибуты", + "scope-shared": "Общие атрибуты", + "add": "Добавить атрибут", + "key": "Ключ", + "last-update-time": "Последнее обновление", + "key-required": "Ключ атрибута обязателен.", + "value": "Значение", + "value-required": "Значение атрибута обязательно.", + "delete-attributes-title": "Вы уверенны, что хотите удалить { count, plural, one {1 атрибут} few {# атрибута} other {# атрибутов} }? ", + "delete-attributes-text": "Внимание, после подтверждения выбранные атрибуты будут удалены.", + "delete-attributes": "Удалить атрибуты", + "enter-attribute-value": "Введите значение атрибута", + "show-on-widget": "Показать на виджете", + "widget-mode": "Виджет-режим", + "next-widget": "Следующий виджет", + "prev-widget": "Предыдущий виджет", + "add-to-dashboard": "Добавить на дашборд", + "add-widget-to-dashboard": "Добавить виджет на дашборд", + "selected-attributes": "{ count, plural, 1 {Выбран} other {Выбраны} } { count, plural, one {1 атрибут} few {# атрибута} other {# атрибутов} }", + "selected-telemetry": "{ count, plural, 1 {Выбран} other {Выбраны} } { count, plural, 1 {1 параметр} few {# параметра} other {# параметров} } телеметрии" + }, + "audit-log": { + "audit": "Аудит", + "audit-logs": "Логи аудита", + "timestamp": "Время", + "entity-type": "Тип объекта", + "entity-name": "Название объекта", + "user": "Пользователь", + "type": "Тип", + "status": "Статус", + "details": "Подробности", + "type-added": "Добавленный", + "type-deleted": "Удаленный", + "type-updated": "Обновленный", + "type-attributes-updated": "Обновлены атрибуты", + "type-attributes-deleted": "Удалены атрибуты", + "type-rpc-call": "RPC вызов", + "type-credentials-updated": "Обновлены учетные данные", + "type-assigned-to-customer": "Присвоен клиенту", + "type-unassigned-from-customer": "Отозван у клиента", + "type-activated": "Активирован", + "type-suspended": "Приостановлен", + "type-credentials-read": "Чтение учетные данных", + "type-attributes-read": "Чтение атрибутов", + "type-relation-add-or-update": "Обновлены отношения", + "type-relation-delete": "Удалены отношения", + "type-relations-delete": "Удалены все отношения", + "type-alarm-ack": "Подтвержден", + "type-alarm-clear": "Сброшен", + "type-login": "Вход", + "type-logout": "Выход", + "type-lockout": "Заблокирован", + "status-success": "Успех", + "status-failure": "Сбой", + "audit-log-details": "Подробности аудит лога", + "no-audit-logs-prompt": "Логи не найдены", + "action-data": "Данные действия", + "failure-details": "Подробности сбоя", + "search": "Поиск аудит логов", + "clear-search": "Очистить поиск" + }, + "confirm-on-exit": { + "message": "У вас есть несохраненные изменения. Вы точно хотите покинуть эту страницу?", + "html-message": "У вас есть несохраненные изменения.
    Вы точно хотите покинуть эту страницу?", + "title": "Несохраненные изменения" + }, + "contact": { + "country": "Страна", + "city": "Город", + "state": "Штат", + "postal-code": "Почтовый код", + "postal-code-invalid": "Допустимы только цифры", + "address": "Адрес", + "address2": "Адрес 2", + "phone": "Телефон", + "email": "Эл. адрес", + "no-address": "Адрес не указан" + }, + "common": { + "username": "Имя пользователя", + "password": "Пароль", + "enter-username": "Введите имя пользователя", + "enter-password": "Введите пароль", + "enter-search": "Введите условие поиска", + "created-time": "Время создания" + }, + "content-type": { + "json": "Json", + "text": "Текстовый", + "binary": "Бинарный (Base64)" + }, + "customer": { + "customer": "Клиент", + "customers": "Клиенты", + "management": "Управление клиентами", + "dashboard": "Дашборд клиента", + "dashboards": "Дашборды клиента", + "devices": "Устройства клиента", + "entity-views": "Представления объектов клиента", + "assets": "Активы клиента", + "public-dashboards": "Общедоступные дашборды", + "public-devices": "Общедоступные устройства", + "public-assets": "Общедоступные активы", + "public-entity-views": "Общедоступные представления объектов", + "add": "Добавить клиента", + "delete": "Удалить клиента", + "manage-customer-users": "Управление пользователями клиента", + "manage-customer-devices": "Управление устройствами клиента", + "manage-customer-dashboards": "Управление дашбордами клиента", + "manage-public-devices": "Управление общедоступными устройствами", + "manage-public-dashboards": "Управление общедоступными дашбордами", + "manage-customer-assets": "Управление активами клиента", + "manage-public-assets": "Управление общедоступными активами", + "add-customer-text": "Добавить нового клиента", + "no-customers-text": "Клиенты не найдены", + "customer-details": "Подробности о клиенте", + "delete-customer-title": "Вы точно хотите удалить клиента '{{customerTitle}}'?", + "delete-customer-text": "Внимание, после подтверждения клиент и все связанные с ним данные будут безвозвратно удалены.", + "delete-customers-title": "Вы точно хотите удалить { count, plural, 1 {1 клиент} few {# клиента} other {# клиентов} }?", + "delete-customers-action-title": "Удалить { count, plural, 1 {1 клиент} few {# клиента} other {# клиентов} }", + "delete-customers-text": "Внимание, после подтверждения выбранные клиенты и все связанные с ними данные будут безвозвратно удалены.", + "manage-users": "Управление пользователями", + "manage-assets": "Управление активами", + "manage-devices": "Управление устройствами", + "manage-dashboards": "Управление дашбордами", + "title": "Имя", + "title-required": "Название обязательно.", + "description": "Описание", + "details": "Подробности", + "events": "События", + "copyId": "Копировать ИД клиента", + "idCopiedMessage": "ИД клиента скопирован в буфер обмена", + "select-customer": "Выбрать клиента", + "no-customers-matching": "Клиенты, соответствующие '{{entity}}', не найдены.", + "customer-required": "Клиент обязателен", + "select-default-customer": "Выбрать клиента по умолчанию", + "default-customer": "Клиент по умолчанию", + "default-customer-required": "Клиент по умолчанию обязателен для отладки дашборда на уровне на уровне Владельца" + }, + "datetime": { + "date-from": "Дата с", + "time-from": "Время с", + "date-to": "Дата до", + "time-to": "Время до" + }, + "dashboard": { + "dashboard": "Дашборд", + "dashboards": "Дашборды", + "management": "Управление дашбордами", + "view-dashboards": "Просмотреть дашборды", + "add": "Добавить дашборд", + "assign-dashboard-to-customer": "Прикрепить дашборд(ы) к клиенту", + "assign-dashboard-to-customer-text": "Пожалуйста, выберите дашборды, которые нужно прикрепить к клиенту", + "assign-to-customer-text": "Пожалуйста, выберите клиента, к которому нужно прикрепить дашборд(ы)", + "assign-to-customer": "Прикрепить к клиенту", + "unassign-from-customer": "Отозвать у клиента", + "make-public": "Открыть дашборд для общего доступа", + "make-private": "Закрыть дашборд для общего доступа", + "manage-assigned-customers": "Управление назначенными клиентами", + "assigned-customers": "Назначенные клиенты", + "assign-to-customers": "Присвоить дашборд(ы) клиенту", + "assign-to-customers-text": "Пожалуйста, выбери клиентов, которым нужно присвоить дашборд(ы)", + "unassign-from-customers": "Отозвать дашборд(ы) у клиентов", + "unassign-from-customers-text": "Пожалуйста, выберите клиентов, у которых нужно отозвать дашборд(ы)", + "no-dashboards-text": "Дашборды не найдены", + "no-widgets": "Виджеты не сконфигурированы", + "add-widget": "Добавить новый виджет", + "title": "Название", + "select-widget-title": "Выберите виджет", + "select-widget-subtitle": "Список доступных виджетов", + "delete": "Удалить дашборд", + "title-required": "Название обязательно.", + "description": "Описание", + "details": "Подробности", + "dashboard-details": "Подробности о дашборде", + "add-dashboard-text": "Добавить новый дашборд", + "assign-dashboards": "Прикрепить дашборды", + "assign-new-dashboard": "Прикрепить новый дашборд", + "assign-dashboards-text": "Прикрепить { count, plural, 1 {1 дашборд} few {# дашборда} other {# дашбордов} } к клиенту", + "unassign-dashboards-action-text": "Отозвать { count, plural, 1 {1 дашборд} few {# дашборда} other {# дашбордов} } у клиента", + "delete-dashboards": "Удалить дашборды", + "unassign-dashboards": "Отозвать дашборды", + "unassign-dashboards-action-title": "Отозвать { count, plural, one {1 дашборд} few {# дашборда} other {# дашбордов} } у клиента", + "delete-dashboard-title": "Вы точно хотите удалить дашборд '{{dashboardTitle}}'?", + "delete-dashboard-text": "Внимание, после подтверждения дашборд и все связанные с ним данные будут безвозвратно утеряны.", + "delete-dashboards-title": "Вы точно хотите удалить { count, plural, one {1 дашборд} few {# дашборда} other {# дашбордов} }?", + "delete-dashboards-action-title": "Удалить { count, plural, one {1 дашборд} few {# дашборда} other {# дашбордов} }", + "delete-dashboards-text": "Внимание, после подтверждения дашборды и все связанные с ними данные будут безвозвратно утеряны.", + "unassign-dashboard-title": "Вы точно хотите отозвать дашборд '{{dashboardTitle}}'?", + "unassign-dashboard-text": "После подтверждения дашборд не будет доступен клиенту.", + "unassign-dashboard": "Отозвать дашборд", + "unassign-dashboards-title": "Вы точно хотите отозвать { count, plural, one {1 дашборд} few {# дашборда} other {# дашбордов} }?", + "unassign-dashboards-text": "После подтверждения выбранные дашборды не будут доступны клиенту.", + "public-dashboard-title": "Теперь дашборд общедоступный", + "public-dashboard-text": "Теперь ваш дашборд {{dashboardTitle}} доступен всем по ссылке:", + "public-dashboard-notice": "Примечание: Для получения доступа к данным устройства нужно открыть общий доступ к этому устройству.", + "make-private-dashboard-title": "Вы точно хотите закрыть общий доступ к дашборду '{{dashboardTitle}}'?", + "make-private-dashboard-text": "После подтверждения дашборд будет закрыт для общего доступа.", + "make-private-dashboard": "Закрыть дашборд для общего доступа", + "socialshare-text": "'{{dashboardTitle}}' сделано ThingsBoard", + "socialshare-title": "'{{dashboardTitle}}' сделано ThingsBoard", + "select-dashboard": "Выберите дашборд", + "no-dashboards-matching": "Дашборд '{{entity}}' не найден.", + "dashboard-required": "Дашборд обязателен.", + "select-existing": "Выберите существующий дашборд", + "create-new": "Создать новый дашборд", + "new-dashboard-title": "Новое название дашборда", + "open-dashboard": "Открыть дашборд", + "set-background": "Установить фон", + "background-color": "Фоновый цвет", + "background-image": "Фоновая картинка", + "background-size-mode": "Размер фона", + "no-image": "Картинка не выбрана", + "drop-image": "Перетащите картинку или кликните для выбора файла.", + "settings": "Настройки", + "columns-count": "Количество колонок", + "columns-count-required": "Количество колонок обязательно.", + "min-columns-count-message": "Минимальное число колонок - 10.", + "max-columns-count-message": "Максимальное число колонок - 1000.", + "widgets-margins": "Величина отступа между виджетами", + "horizontal-margin": "Величина горизонтального отступа", + "horizontal-margin-required": "Величина горизонтального отступа обязательна.", + "min-horizontal-margin-message": "Минимальная величина горизонтального отступа - 0.", + "max-horizontal-margin-message": "Максимальная величина горизонтального отступа - 50.", + "vertical-margin": "Величина вертикального отступа", + "vertical-margin-required": "Величина вертикального отступа обязательна.", + "min-vertical-margin-message": "Минимальная величина вертикального отступа - 0.", + "max-vertical-margin-message": "Максимальная величина вертикального отступа - 50.", + "autofill-height": "Автозаполнение по высоте", + "mobile-layout": "Настройки мобильного режима", + "mobile-row-height": "Высота строки в мобильном режиме, px", + "mobile-row-height-required": "Высота строки в мобильном режиме обязательна.", + "min-mobile-row-height-message": "Минимальная высота строки в мобильном режиме составляет 5 px.", + "max-mobile-row-height-message": "Максимальная высота строки в мобильном режиме составляет 200 px.", + "display-title": "Показать название дашборда", + "toolbar-always-open": "Отображать панель инструментов", + "title-color": "Цвет названия", + "display-dashboards-selection": "Отображать выборку дашбордов", + "display-entities-selection": "Отображать выбору объектов", + "display-dashboard-timewindow": "Показать временное окно", + "display-dashboard-export": "Показать экспорт", + "import": "Импортировать дашборд", + "export": "Экспортировать дашборд", + "export-failed-error": "Не удалось экспортировать дашборд: {{error}}", + "create-new-dashboard": "Создать новый дашборд", + "dashboard-file": "Файл дашборда", + "invalid-dashboard-file-error": "Не удалось импортировать дашборд: неизвестная схема данных дашборда.", + "dashboard-import-missing-aliases-title": "Настроить псевдонимы импортированного дашборда", + "create-new-widget": "Создать новый виджет", + "import-widget": "Импортировать виджет", + "widget-file": "Виджет-файл", + "invalid-widget-file-error": "Не удалось импортировать виджет: неправильный формат данных.", + "widget-import-missing-aliases-title": "Настроить псевдонимы, которые использует импортированный виджет", + "open-toolbar": "Открыть панель инструментов дашборда", + "close-toolbar": "Закрыть панель инструментов", + "configuration-error": "Ошибка в настройках", + "alias-resolution-error-title": "Ошибка в настройках псевдонимов дашборда", + "invalid-aliases-config": "Не удалось найти устройство, соответствующее фильтру псевдонимов.
    Пожалуйста, обратитесь к администратору для устранения неполадки.", + "select-devices": "Выберите устройства", + "assignedToCustomer": "Присвоенные клиенту", + "assignedToCustomers": "Присвоенные клиентам", + "public": "Публичный", + "public-link": "Публичная ссылка", + "copy-public-link": "Копировать публичную ссылку", + "public-link-copied-message": "Публичная ссылка на дашборд скопирована в буфер обмена.", + "manage-states": "Управление состоянием дашборда", + "states": "Состояния дашборда", + "search-states": "Поиск состояния дашборда", + "selected-states": "Выбрано { count, plural, 1 {1 состояние} few {# состояния} other {# состояний} } дашборда", + "edit-state": "Изменить состояние дашборда", + "delete-state": "Удалить состояние дашборда", + "add-state": "Добавить состояние дашборда", + "state": "Состояние дашборда", + "state-name": "Название", + "state-name-required": "Название состояния дашборда обязательно.", + "state-id": "ИД состояния", + "state-id-required": "ИД состояния дашборда обязателен.", + "state-id-exists": "Состояния дашборда с таким именем уже существует.", + "is-root-state": "Корневое состояние", + "delete-state-title": "Удалить состояние дашборда", + "delete-state-text": "Вы точно хотите удалить состояние дашборда '{{stateName}}'?", + "show-details": "Показать подробности", + "hide-details": "Скрыть подробности", + "select-state": "Выбрать состояние", + "state-controller": "Контроллер состояния" + }, + "datakey": { + "settings": "Настройки", + "advanced": "Дополнительно", + "label": "Метка", + "color": "Цвет", + "units": "Укажите символы, которые нужно указывать после значения", + "decimals": "Число знаков после запятой", + "data-generation-func": "Функция генерации данных", + "use-data-post-processing-func": "Использовать функцию пост-обработки данных", + "configuration": "Конфигурация ключа данных", + "timeseries": "Телеметрия", + "attributes": "Атрибуты", + "entity-field": "Поле объекта", + "alarm": "Параметры оповещения", + "timeseries-required": "Телеметрия объекта обязательна.", + "timeseries-or-attributes-required": "Телеметрия/атрибуты обязательны.", + "maximum-timeseries-or-attributes": "Максимальное количество параметров телеметрии или атрибутов равно {{count}}", + "alarm-fields-required": "Параметры оповещения обязательны.", + "function-types": "Тип функции", + "function-types-required": "Тип функции обязателен.", + "maximum-function-types": "Максимальное количество типов функции равно {{count}}", + "time-description": "время текущего значения;", + "value-description": "текущее значение;", + "prev-value-description": "результат предыдущего вызова функции;", + "time-prev-description": "время предыдущего значения;", + "prev-orig-value-description": "исходное предыдущее значение;" + }, + "datasource": { + "type": "Тип источника данных", + "name": "Название", + "add-datasource-prompt": "Пожалуйста, добавьте источник данных" + }, + "details": { + "edit-mode": "Режим редактирования", + "edit-json": "Редактировать JSON", + "toggle-edit-mode": "Режим редактирования" + }, + "device": { + "device": "Устройство", + "device-required": "Устройство обязательно.", + "devices": "Устройства", + "management": "Управление устройствами", + "view-devices": "Просмотреть устройства", + "device-alias": "Псевдоним устройства", + "aliases": "Псевдонимы устройства", + "no-alias-matching": "'{{alias}}' не найден.", + "no-aliases-found": "Псевдонимы не найдены.", + "no-key-matching": "'{{key}}' не найден.", + "no-keys-found": "Ключи не найдены.", + "create-new-alias": "Создать новый!", + "create-new-key": "Создать новый!", + "duplicate-alias-error": "Найден дублирующийся псевдоним '{{alias}}'.
    В рамках дашборда псевдонимы устройств должны быть уникальными.", + "configure-alias": "Настроить '{{alias}}' псевдоним", + "no-devices-matching": "Устройство '{{entity}}' не найдено.", + "alias": "Псевдоним", + "alias-required": "Псевдоним устройства обязателен.", + "remove-alias": "Удалить псевдоним устройства", + "add-alias": "Добавить псевдоним устройства", + "name-starts-with": "Название начинается с", + "device-list": "Список устройств", + "use-device-name-filter": "Использовать фильтр", + "device-list-empty": "Устройства не выбраны.", + "device-name-filter-required": "Фильтр названия устройства обязателен.", + "device-name-filter-no-device-matched": "Устройства, названия которых начинаются с '{{device}}', не найдены.", + "add": "Добавить устройство", + "assign-to-customer": "Присвоить клиенту", + "assign-device-to-customer": "Присвоить устройство(а) клиенту", + "assign-device-to-customer-text": "Пожалуйста, выберите устройства, которые нужно присвоить клиенту", + "make-public": "Открыть общий доступ к устройству", + "make-private": "Закрыть общий доступ к устройству", + "no-devices-text": "Устройства не найдены", + "assign-to-customer-text": "Пожалуйста, выберите клиента, которому нужно присвоить устройство(а)", + "device-details": "Подробности об устройстве", + "add-device-text": "Добавить новое устройство", + "credentials": "Учетные данные", + "manage-credentials": "Управление учетными данными", + "delete": "Удалить устройство", + "assign-devices": "Присвоить устройство", + "assign-devices-text": "Присвоить { count, plural, one {1 устройство} few {# устройства} other {# устройств} } клиенту", + "delete-devices": "Удалить устройства", + "unassign-from-customer": "Отозвать у клиенту", + "unassign-devices": "Отозвать устройства", + "unassign-devices-action-title": "Отозвать у клиента { count, plural, one {1 устройство} few {# устройства} other {# устройств} }", + "assign-new-device": "Присвоить новое устройство", + "make-public-device-title": "Вы точно хотите открыть общий доступ к устройству '{{deviceName}}'?", + "make-public-device-text": "После подтверждения устройство и все связанные с ним данные будут общедоступными.", + "make-private-device-title": "Вы точно хотите закрыть общий доступ к устройству '{{deviceName}}'", + "make-private-device-text": "После подтверждения устройство и все связанные с ним данные будут закрыты для общего доступа.", + "view-credentials": "Просмотреть учетные данные", + "delete-device-title": "Вы точно хотите удалить устройство '{{deviceName}}'?", + "delete-device-text": "Внимание, после подтверждения устройство и все связанные с ним данные будут безвозвратно утеряны.", + "delete-devices-title": "Вы точно хотите удалить { count, plural, one {1 устройство} few {# устройства} other {# устройств} }?", + "delete-devices-action-title": "Удалить { count, plural, one {1 устройство} few {# устройства} other {# устройств} }", + "delete-devices-text": "Внимание, после подтверждения выбранные устройства и все связанные с ними данные будут безвозвратно утеряны..", + "unassign-device-title": "Вы точно хотите отозвать устройство '{{deviceName}}'?", + "unassign-device-text": "После подтверждения устройство будет недоступно клиенту.", + "unassign-device": "Отозвать устройство", + "unassign-devices-title": "Вы точно хотите отозвать { count, plural, one {1 устройство} few {# устройства} other {# устройств} }?", + "unassign-devices-text": "После подтверждения выбранные устройства будут недоступны клиенту.", + "device-credentials": "Учетные данные устройства", + "credentials-type": "Тип учетных данных", + "access-token": "Токен", + "access-token-required": "Токен обязателен.", + "access-token-invalid": "Длина токена должна быть от 1 до 20 символов.", + "rsa-key": "Открытый ключ RSA", + "rsa-key-required": "Открытый ключ RSA обязателен.", + "secret": "Секрет", + "secret-required": "Секрет обязателен.", + "device-type": "Тип устройства", + "device-type-required": "Тип устройства обязатеен.", + "select-device-type": "Выберите тип устройства", + "enter-device-type": "Введите тип устройства", + "any-device": "Любое устройство", + "no-device-types-matching": "Тип устройства, соответствующий '{{entitySubtype}}', не найден.", + "device-type-list-empty": "Не выбран тип устройства.", + "device-types": "Типы устройств", + "name": "Название", + "name-required": "Название обязательно.", + "description": "Описание", + "events": "События", + "details": "Подробности", + "copyId": "Копировать идентификатор устройства", + "copyAccessToken": "Копировать токен", + "idCopiedMessage": "Идентификатор устройства скопирован в буфер обмена", + "accessTokenCopiedMessage": "Токен устройства скопирован в буфер обмена", + "assignedToCustomer": "Присвоен клиенту", + "unable-delete-device-alias-title": "Не удалось удалить псевдоним устройства", + "unable-delete-device-alias-text": "Не удалось удалить псевдоним '{{deviceAlias}}' устройства, т.к. он используется следующими виджетами:
    {{widgetsList}}", + "is-gateway": "Гейтвей", + "public": "Общедоступный", + "device-public": "Устройство общедоступно", + "select-device": "Выбрать устройство", + "import": "Импортировать устройства", + "device-file": "Файл с устройствами" + }, + "dialog": { + "close": "Закрыть диалог" + }, + "direction": { + "column": "Колонка", + "row": "Строка" + }, + "error": { + "unable-to-connect": "Не удалось подключиться к серверу! Пожалуйста, проверьте интернет-соединение.", + "unhandled-error-code": "Код необработанной ошибки: {{errorCode}}", + "unknown-error": "Неизвестная ошибка" + }, + "entity": { + "entity": "Объект", + "entities": "Объекты", + "aliases": "Псевдонимы объекта", + "entity-alias": "Псевдоним объекта", + "unable-delete-entity-alias-title": "Не удалось удалить псевдоним объекта", + "unable-delete-entity-alias-text": "Псевдоним объекта '{{entityAlias}}' не может быть удален, так как используется следующим(и) виджетом(ами):
    {{widgetsList}}", + "duplicate-alias-error": "Найден дубликат псевдонима '{{alias}}'.
    В рамках одного дашборда псевдонимы объектов должны быть уникальными.", + "missing-entity-filter-error": "Отсутствует фильтр для псевдонима '{{alias}}'.", + "configure-alias": "Настроить псевдоним '{{alias}}'", + "alias": "Псевдоним", + "alias-required": "Псевдоним объекта обязателен.", + "remove-alias": "Убрать псевдоним объекта", + "add-alias": "Добавить псевдоним объекта", + "entity-list": "Список объектов", + "entity-type": "Тип объекта", + "entity-types": "Типы объекта", + "entity-type-list": "Список типов объекта", + "any-entity": "Любой объект", + "enter-entity-type": "Введите тип объекта", + "no-entities-matching": "Объекты, соответствующие '{{entity}}', не найдены.", + "no-entity-types-matching": "Типы объектов, соответствующие '{{entityType}}', не найдены.", + "name-starts-with": "Название, начинающееся с", + "use-entity-name-filter": "Использовать фильтр", + "entity-list-empty": "Не выбраны объекты.", + "entity-type-list-empty": "Не выбраны типы объекта.", + "entity-name-filter-required": "Фильтр по названию объекта обязателен.", + "entity-name-filter-no-entity-matched": "Объекты, чье название начинается с '{{entity}}', не найдены.", + "all-subtypes": "Все", + "select-entities": "Выберите объекты", + "no-aliases-found": "Псевдонимы не найдены.", + "no-alias-matching": "Псевдоним '{{alias}}' не найден.", + "create-new-alias": "Создать новый!", + "key": "Ключ", + "key-name": "Название ключа", + "no-keys-found": "Ключ не найден.", + "no-key-matching": "Ключ '{{key}}' не найден.", + "create-new-key": "Создать новый!", + "type": "Тип", + "type-required": "Тип объекта обязателен.", + "type-device": "Устройство", + "type-devices": "Устройства", + "list-of-devices": "{ count, plural, 1 {Одно устройство} other {Список из # устройств} }", + "device-name-starts-with": "Устройства, чьи название начинается с '{{prefix}}'", + "type-asset": "Актив", + "type-assets": "Активы", + "list-of-assets": "{ count, plural, 1 {Один актив} other {Список из # активов} }", + "asset-name-starts-with": "Активы, чьи название начинается с '{{prefix}}'", + "type-entity-view": "Представление Объекта", + "type-entity-views": "Представления Объекта", + "list-of-entity-views": "{ count, plural, 1 {Одно представление объекта} other {Список из # представлений объекта} }", + "entity-view-name-starts-with": "Представления Объекта, чьи название начинается с '{{prefix}}'", + "type-rule": "Правило", + "type-rules": "Правила", + "list-of-rules": "{ count, plural, 1 {Одно правило} other {Список из # правил} }", + "rule-name-starts-with": "Правила, чьи названия начинаются с '{{prefix}}'", + "type-plugin": "Плагин", + "type-plugins": "Плагины", + "list-of-plugins": "{ count, plural, 1 {Один плагин} other {Список из # плагинов} }", + "plugin-name-starts-with": "Плагины, чьи имена начинаются с '{{prefix}}'", + "type-tenant": "Владелец", + "type-tenants": "Владельцы", + "list-of-tenants": "{ count, plural, 1 {Один владелец} other {Список из # владельцев} }", + "tenant-name-starts-with": "Владельцы, чьи имена начинаются с '{{prefix}}'", + "type-customer": "Клиент", + "type-customers": "Клиенты", + "list-of-customers": "{ count, plural, 1 {Один клиент} other {Список из # клиентов} }", + "customer-name-starts-with": "Клиенты, чьи имена начинаются с '{{prefix}}'", + "type-user": "Пользователь", + "type-users": "Пользователи", + "list-of-users": "{ count, plural, 1 {Один пользователь} other {Список из # пользователей} }", + "user-name-starts-with": "Пользователи, чьи имена начинаются с '{{prefix}}'", + "type-dashboard": "Дашборд", + "type-dashboards": "Дашборды", + "list-of-dashboards": "{ count, plural, 1 {Один дашборд} other {Список из # дашбордов} }", + "dashboard-name-starts-with": "Дашборды, чьи названия начинаются с '{{prefix}}'", + "type-alarm": "Оповещение", + "type-alarms": "Оповещения", + "list-of-alarms": "{ count, plural, 1 {Одно оповещение} other {Список из # оповещений} }", + "alarm-name-starts-with": "Оповещения, чьи названия начинаются с '{{prefix}}'", + "type-rulechain": "Цепочка правил", + "type-rulechains": "Цепочки правил", + "list-of-rulechains": "{ count, plural, 1 {Одна цепочка правил} other {Список из # цепочек правил} }", + "rulechain-name-starts-with": "Цепочки правил, чьи названия начинаются с '{{prefix}}'", + "type-rulenode": "Правило", + "type-rulenodes": "Правила", + "list-of-rulenodes": "{ count, plural, 1 {Одно правило} other {Список из # правил} }", + "rulenode-name-starts-with": "Правила, чьи названия начинаются с '{{prefix}}'", + "type-current-customer": "Текущий клиент", + "type-current-tenant": "Текущий владелец", + "search": "Поиск объектов", + "selected-entities": "Выбран(ы) { count, plural, 1 {1 объект} few {# объекта} other {# объектов} }", + "entity-name": "Название объекта", + "entity-label": "Метка объекта", + "details": "Подробности об объекте", + "no-entities-prompt": "Объекты не найдены", + "no-data": "Нет данных для отображения", + "columns-to-display": "Отобразить следующие колонки" + }, + "entity-field": { + "created-time": "Время создания", + "name": "Название", + "type": "Тип", + "first-name": "Имя", + "last-name": "Фамилия", + "email": "Электронная почта", + "title": "Название", + "country": "Страна", + "state": "Штат/Область", + "city": "Город", + "address": "Адрес", + "address2": "Адрес 2", + "zip": "Индекс", + "phone": "Телефон", + "label": "Метка" + }, + "entity-view": { + "entity-view": "Представление Объекта", + "entity-view-required": "Представление объекта обязательно.", + "entity-views": "Представления Объектов", + "management": "Управление представлениями объектов", + "view-entity-views": "Просмотр представлений объектов", + "entity-view-alias": "Псевдоним Представления Объекта", + "aliases": "Псевдонимы Представления Объекта", + "no-alias-matching": "Псевдоним '{{alias}}' не найден.", + "no-aliases-found": "Псевдонимы не найдены.", + "no-key-matching": "Ключ '{{key}}' не найден.", + "no-keys-found": "Ключи не найдены.", + "create-new-alias": "Создать новый!", + "create-new-key": "Создать новый!", + "duplicate-alias-error": "Найден дубликат псевдонима '{{alias}}'.
    В рамках одного дашборда псевдонимы представлений объектов должны быть уникальными.", + "configure-alias": "Настроить псевдоним '{{alias}}'", + "no-entity-views-matching": "Объекты, соответствующие '{{entity}}', не найдены.", + "alias": "Псевдоним", + "alias-required": "Псевдоним представления объекта обязателен.", + "remove-alias": "Убрать псевдоним представления объекта", + "add-alias": "Добавить псевдоним представления объекта", + "name-starts-with": "Представления объектов, чьи название начинается с", + "entity-view-list": "Список представлений объектов", + "use-entity-view-name-filter": "Использовать фильтр", + "entity-view-list-empty": "Не выбраны представления объектов.", + "entity-view-name-filter-required": "Для представлений объектов фильтр по названиям обязателен.", + "entity-view-name-filter-no-entity-view-matched": "Представление объектов, чьи название начинаются с '{{entityView}}', не найдены.", + "add": "Представление объекта", + "assign-to-customer": "Назначить клиенту", + "assign-entity-view-to-customer": "Назначить представление(я) объекта(ов) клиенту", + "assign-entity-view-to-customer-text": "Пожалуйста, выберите представления объектов, которые нужно назначить клиенту", + "no-entity-views-text": "Представления объектов не найдены", + "assign-to-customer-text": "Пожалуйста, выберите клиента, которому нужно назначить представления объектов", + "entity-view-details": "Подробности о представлении объекта", + "add-entity-view-text": "Добавить новое представление объекта", + "delete": "Удалить представление объекта", + "assign-entity-views": "Назначить представления объектов", + "assign-entity-views-text": "Назначить клиенту { count, plural, 1 {1 представление объекта} few {# представления объектов} other {# представлений объектов} }", + "delete-entity-views": "Удалить представления объектов", + "unassign-from-customer": "Отозвать у клиента", + "unassign-entity-views": "Отозвать представления объектов", + "unassign-entity-views-action-title": "Отозвать у клиента { count, plural, 1 {1 представление объекта} few {# представления объектов} other {# представлений объектов} }", + "assign-new-entity-view": "Назначит новое представление объекта", + "delete-entity-view-title": "Вы точно хотите удалить представление объекта '{{entityViewName}}'?", + "delete-entity-view-text": "Внимание, после подтверждения представление объекта и все связанные с ним данные будут безвозвратно удалены.", + "delete-entity-views-title": "Вы точно хотите удалить { count, plural, 1 {1 представление объекта} few {# представления объектов} other {# представлений объектов} }?", + "delete-entity-views-action-title": "Удалить { count, plural, 1 {1 представление объекта} few {# представления объектов} other {# представлений объектов} }", + "delete-entity-views-text": "Внимание, после подтверждения выбранные представления объектов и все связанные с ними данные будут безвозвратно удалены.", + "unassign-entity-view-title": "Вы точно хотите отозвать представление объекта '{{entityViewName}}'?", + "unassign-entity-view-text": "После подтверждение представление объекта будет недоступно клиенту.", + "unassign-entity-view": "Отозвать представление объекта", + "unassign-entity-views-title": "Вы точно хотите отозвать { count, plural, 1 {1 представление объекта} few {# представления объектов} other {# представлений объектов} }?", + "unassign-entity-views-text": "После подтверждение выбранные представления объектов будет недоступно клиенту.", + "entity-view-type": "Тип представления объекта", + "entity-view-type-required": "Тип представления объекта обязателен.", + "select-entity-view-type": "Выберите тип представления объекта", + "enter-entity-view-type": "Введите тип представления объекта", + "any-entity-view": "Любое представление объекта", + "no-entity-view-types-matching": "Типы представления объекта, соответствующие '{{entitySubtype}}', не найдены.", + "entity-view-type-list-empty": "Не выбраны типы представления объекта.", + "entity-view-types": "Типы представления объекта", + "name": "Название", + "name-required": "Название обязательно.", + "description": "Описание", + "events": "События", + "details": "Подробности", + "copyId": "Копировать ИД представление объекта", + "assignedToCustomer": "Назначено клиенту", + "unable-entity-view-device-alias-title": "Не удалось удалить псевдоним представления объекта", + "unable-entity-view-device-alias-text": "Не удалось удалить псевдоним устройства '{{entityViewAlias}}', т.к. он используется следующими виджетами:
    {{widgetsList}}", + "select-entity-view": "Выбрать представление объекта", + "make-public": "Открыть общий доступ к представлению объекта", + "make-private": "Закрыть общий доступ к представлению объекта", + "start-date": "Дата начала", + "start-ts": "Время начала", + "end-date": "Дата окончания", + "end-ts": "Время окончания", + "date-limits": "Временной лимит", + "client-attributes": "Клиентские атрибуты", + "shared-attributes": "Общие атрибуты", + "server-attributes": "Серверные атрибуты", + "timeseries": "Телеметрия", + "client-attributes-placeholder": "Клиентские атрибуты", + "shared-attributes-placeholder": "Общие атрибуты", + "server-attributes-placeholder": "Серверные атрибуты", + "timeseries-placeholder": "Телеметрия", + "target-entity": "Целевой объект", + "attributes-propagation": "Пробросить атрибуты", + "attributes-propagation-hint": "Представление объекта автоматически копирует указанные атрибуты с Целевого Объекта каждый раз, когда вы сохраняете или обновляете это представление. В целях производительности атрибуты целевого объекта не пробрасываются в представление объекта на каждом их изменении. Вы можете включить автоматический проброс, настроив в вашей цепочке правило \"copy to view\" и соединив его с сообщениями типа \"Post attributes\" и \"Attributes Updated\".", + "timeseries-data": "Данные телеметрии", + "timeseries-data-hint": "Настроить ключи данных телеметрии целевого объекта, которые будут доступны представлению объекта. Эти данные только для чтения.", + "make-public-entity-view-title": "Вы уверенны, что хотите открыть общий доступ к представленю объекта '{{entityViewName}}'?", + "make-public-entity-view-text": "После подтверждения представление объекта и все связанные с ним данные станут публичными и доступными для других пользователей.", + "make-private-entity-view-title": "Вы уверенны, что хотите закрыть общий доступ к представлению объекта '{{entityViewName}}'?", + "make-private-entity-view-text": "После подтверждения представление объекта и все звязанные с ним данные станут приватными и не будут доступны для других пользователей." + }, + "event": { + "event-type": "Тип события", + "type-error": "Ошибка", + "type-lc-event": "Событие жизненного цикла", + "type-stats": "Статистика", + "type-debug-rule-node": "Отладка", + "type-debug-rule-chain": "Отладка", + "no-events-prompt": "События не найдены", + "error": "Ошибка", + "alarm": "Аварийное оповещение", + "event-time": "Время возникновения события", + "server": "Сервер", + "body": "Тело", + "method": "Метод", + "type": "Тип", + "entity": "Объект", + "message-id": "ИД сообщения", + "message-type": "Тип сообщения", + "data-type": "Тип данных", + "relation-type": "Тип отношения", + "metadata": "Метаданные", + "data": "Данные", + "event": "Событие", + "status": "Статус", + "success": "Успех", + "failed": "Неудача", + "messages-processed": "Сообщения обработаны", + "errors-occurred": "Возникли ошибки" + }, + "extension": { + "extensions": "Расширение", + "selected-extensions": "Выбрано { count, plural, 1 {1 расширение} few {# расширения} other {# расширений} }", + "type": "Тип", + "key": "Ключ", + "value": "Значение", + "id": "ИД", + "extension-id": "ИД расширения", + "extension-type": "Тип расширения", + "transformer-json": "JSON *", + "unique-id-required": "Такое ИД расширения уже существует.", + "delete": "Удалить расширение", + "add": "Добавить расширение", + "edit": "Редактировать расширение", + "delete-extension-title": "Вы точно хотите удалить расширение '{{extensionId}}'?", + "delete-extension-text": "Внимание, после подтверждения расширение и все связанные с ним данные будут безвозвратно удалены.", + "delete-extensions-title": "Вы точно хотите удалить { count, plural, 1 {1 расширение} few {# расширения} other {# расширений} }?", + "delete-extensions-text": "Внимание, после подтверждения выбранные расширения будут удалены.", + "converters": "Конвертеры", + "converter-id": "ИД конвертера", + "configuration": "Конфигурация", + "converter-configurations": "Конфигурация конвертера", + "token": "Токен безопасности", + "add-converter": "Добавить конвертер", + "add-config": "Добавить конфигурацию конвертера", + "device-name-expression": "Маска названия устройства", + "device-type-expression": "Маска типа устройства", + "custom": "Пользовательский", + "to-double": "To Double", + "transformer": "Преобразователь", + "json-required": "JSON преобразователя обязателен.", + "json-parse": "Не удалось распознать JSON преобразователя.", + "attributes": "Атрибуты", + "add-attribute": "Добавить атрибут", + "add-map": "Добавить элемент сопоставления", + "timeseries": "Телеметрия", + "add-timeseries": "Добавить параметр телеметрии", + "field-required": "Параметр обязателен", + "brokers": "Брокеры", + "add-broker": "Добавить брокер", + "host": "Хост", + "port": "Порт", + "port-range": "Значение порта лежит в диапазоне от 1 до 65535.", + "ssl": "SSL", + "credentials": "Учетные данные", + "username": "Имя пользователя", + "password": "Пароль", + "retry-interval": "Интервал повтора в миллисекундах", + "anonymous": "Анонимный", + "basic": "Общий", + "pem": "PEM", + "ca-cert": "Файл CA сертификата *", + "private-key": "Файл приватного ключа *", + "cert": "Файл сертификата *", + "no-file": "Не выбран файл.", + "drop-file": "Перетяните файл или нажмите для выбора файла.", + "mapping": "Сопоставление", + "topic-filter": "Фильтр тем", + "converter-type": "Тип конвертера", + "converter-json": "JSON", + "json-name-expression": "JSON выражение для названия устройства", + "topic-name-expression": "Выражение для названия устройства в названии темы", + "json-type-expression": "JSON выражение для типа устройства", + "topic-type-expression": "Выражение для типа устройства в названии темы", + "attribute-key-expression": "Выражение для атрибута", + "attr-json-key-expression": "JSON выражение для атрибута", + "attr-topic-key-expression": "Выражение для атрибута в названии темы", + "request-id-expression": "Выражение для ИД запроса", + "request-id-json-expression": "JSON выражение для ИД запроса", + "request-id-topic-expression": "Выражение для ИД запроса в названии темы", + "response-topic-expression": "Выражение для темы ответов", + "value-expression": "Выражение для значения", + "topic": "Тема", + "timeout": "Таймаут в миллисекундах", + "converter-json-required": "JSON конвертер обязателен.", + "converter-json-parse": "Не удалось распознать JSON конвертера.", + "filter-expression": "Выражение для фильтрации", + "connect-requests": "Запросы о подключении устройства", + "add-connect-request": "Добавить запросы о подключении устройства", + "disconnect-requests": "Запросы об отсоединении устройства", + "add-disconnect-request": "Добавить запрос об отсоединении устройства", + "attribute-requests": "Запросы для атрибутов", + "add-attribute-request": "Добавить запрос для атрибутов", + "attribute-updates": "Обновление атрибутов", + "add-attribute-update": "Добавить обновление атрибутов", + "server-side-rpc": "Серверный RPC", + "add-server-side-rpc-request": "Добавить серверный RPC", + "device-name-filter": "Фильтр для названия устройства", + "attribute-filter": "Фильтр для атрибутов", + "method-filter": "Фильтр для процедур", + "request-topic-expression": "Выражение для темы запросов", + "response-timeout": "Время ожидания ответа в миллисекундах", + "topic-expression": "Выражение для названия темы", + "client-scope": "Клиентский", + "add-device": "Добавить устройство", + "opc-server": "Серверы", + "opc-add-server": "Добавить сервер", + "opc-add-server-prompt": "Пожалуйста, добавьте сервер", + "opc-application-name": "Название приложения", + "opc-application-uri": "URI приложения", + "opc-scan-period-in-seconds": "Частота сканирования в секундах", + "opc-security": "Безопасность", + "opc-identity": "Идентификация", + "opc-keystore": "Хранилище ключей", + "opc-type": "Тип", + "opc-keystore-type": "Тип", + "opc-keystore-location": "Расположение *", + "opc-keystore-password": "Пароль", + "opc-keystore-alias": "Псевдоним", + "opc-keystore-key-password": "Пароль для ключ", + "opc-device-node-pattern": "Паттерн OPC узла устройства", + "opc-device-name-pattern": "Паттерн названия устройства", + "modbus-server": "Серверы/ведомые устройства", + "modbus-add-server": "Добавить сервер/ведомое устройство", + "modbus-add-server-prompt": "Пожалуйста, добавить сервер/ведомое устройство", + "modbus-transport": "Транспорт", + "modbus-tcp-reconnect": "Переподключатсься автоматически", + "modbus-port-name": "Название последовательного порта", + "modbus-encoding": "Кодирование символов", + "modbus-parity": "Паритет", + "modbus-baudrate": "Скорость передачи", + "modbus-databits": "Биты данных", + "modbus-stopbits": "Стоп-биты", + "modbus-databits-range": "Параметр \"Биты данных\" может принимать значения 7 или 8.", + "modbus-stopbits-range": "Параметр \"Стоп-биты\" может принимать значения 1 или 2.", + "modbus-unit-id": "ИД устройства", + "modbus-unit-id-range": "ИД устройства должен быть в диапазоне от 1 до 247.", + "modbus-device-name": "Название устройства", + "modbus-poll-period": "Частота опроса (в миллисекундах)", + "modbus-attributes-poll-period": "Частота опроса для атрибутов (в миллисекундах)", + "modbus-timeseries-poll-period": "Частота опроса для телеметрии (в миллисекундах)", + "modbus-poll-period-range": "Значение параметра \"Частота опроса\" должно быть больше ноля.", + "modbus-tag": "Тег", + "modbus-function": "Modbus функция", + "modbus-register-address": "Адрес регистра", + "modbus-register-address-range": "Адрес регистра должен быть в диапазоне от 0 до 65535.", + "modbus-register-bit-index": "Номер бита", + "modbus-register-bit-index-range": "Номер бита должен быть в диапазоне от 0 до 15.", + "modbus-register-count": "Количество регистров", + "modbus-register-count-range": "Количество регистров должно быть больше ноля.", + "modbus-byte-order": "Порядок байтов", + "sync": { + "status": "Статус", + "sync": "Синхронизирован", + "not-sync": "Не синхронизирован", + "last-sync-time": "Время последней синхронизации", + "not-available": "Не доступен" + }, + "export-extensions-configuration": "Экспортировать конфигурацию расширений", + "import-extensions-configuration": "Импортировать конфигурацию расширений", + "import-extensions": "Импортировать расширения", + "import-extension": "Импортировать расширение", + "export-extension": "Экспортировать расширение", + "file": "Файл расширений", + "invalid-file-error": "Не правильный формат файла" + }, + "fullscreen": { + "expand": "Во весь экран", + "exit": "Выйти из полноэкранного режима", + "toggle": "Во весь экран", + "fullscreen": "Полноэкранный режим" + }, + "function": { + "function": "Функция" + }, + "grid": { + "delete-item-title": "Вы точно хотите удалить этот объект?", + "delete-item-text": "Внимание, после подтверждения объект и все связанные с ним данные будут безвозвратно утеряны.", + "delete-items-title": "Вы точно хотите удалить { count, plural, one {1 объект} few {# объекта} other {# объектов} }?", + "delete-items-action-title": "Удалить { count, plural, one {1 объект} few {# объекта} other {# объектов} }", + "delete-items-text": "Внимание, после подтверждения выбранные объекты и все связанные с ними данные будут безвозвратно утеряны.", + "add-item-text": "Добавить новый объект", + "no-items-text": "Объекты не найдены", + "item-details": "Подробности об объекте", + "delete-item": "Удалить объект", + "delete-items": "Удалить объекты", + "scroll-to-top": "Прокрутка к началу" + }, + "help": { + "goto-help-page": "Перейти к справке" + }, + "home": { + "home": "Главная", + "profile": "Профиль", + "logout": "Выйти из системы", + "menu": "Меню", + "avatar": "Аватар", + "open-user-menu": "Открыть меню пользователя" + }, + "import": { + "no-file": "Файл не выбран", + "drop-file": "Перетащите JSON файл или кликните для выбора файла.", + "drop-file-csv": "Перетащите CSV файл или кликните для выбора файла.", + "column-value": "Значение", + "column-title": "Название", + "column-example": "Пример значений данных", + "column-key": "Ключ атрибута/телеметрии", + "csv-delimiter": "Разделитель в CSV файле", + "csv-first-line-header": "Первая строка содержит названия колонок", + "csv-update-data": "Обновить атрибут/телеметрию", + "import-csv-number-columns-error": "Файл должен содержать как минимум две колонки", + "import-csv-invalid-format-error": "Неверный формат данных. Строка: '{{line}}'", + "column-type": { + "name": "Название", + "type": "Тип", + "label": "Метка", + "column-type": "Тип колонки", + "client-attribute": "Клиентский атрибут", + "shared-attribute": "Общий атрибут", + "server-attribute": "Серверный атрибут", + "timeseries": "Телеметрия", + "entity-field": "Entity field", + "access-token": "Токен" + }, + "stepper-text": { + "select-file": "Выберите файл", + "configuration": "Конфигурация импорта", + "column-type": "Выберите тип колонок", + "creat-entities": "Создание новых объектов", + "done": "Завершено" + }, + "message": { + "create-entities": "{{count}} новый(х) объект(ов) было успешно создано.", + "update-entities": "{{count}} объект(ов) успешно обновлено.", + "error-entities": "Возникла ошибка при создании {{count}} объекта(ов)." + } + }, + "item": { + "selected": "Выбранные" + }, + "js-func": { + "no-return-error": "Функция должна возвращать значение!", + "return-type-mismatch": "Функция должна возвращать значение типа '{{type}}'!" + }, + "key-val": { + "key": "Ключ", + "value": "Значение", + "remove-entry": "Удалить элемент", + "add-entry": "Добавить элемент", + "no-data": "Элементы отсутствуют" + }, + "layout": { + "layout": "Макет", + "manage": "Управление макетами", + "settings": "Настройки макета", + "color": "Цвет", + "main": "Основной", + "right": "Правый", + "select": "Выбрать макет" + }, + "legend": { + "direction": "Расположение элементов легенды", + "position": "Расположение легенды", + "show-max": "Показать максимальное значение", + "show-min": "Показать минимальное значение", + "show-avg": "Показать среднее значение", + "show-total": "Показать сумму", + "settings": "Настройки легенды", + "min": "Мин", + "max": "Макс", + "avg": "Среднее", + "total": "Сумма", + "comparison-time-ago": { + "days": "(день назад)", + "weeks": "(неделю назад)", + "months": "(месяц назад)", + "years": "(год назад)" + } + }, + "login": { + "login": "Войти", + "request-password-reset": "Запрос на сброс пароля", + "reset-password": "Сбросить пароль", + "create-password": "Создать пароль", + "passwords-mismatch-error": "Введенные пароли должны быть одинаковыми!", + "password-again": "Введите пароль еще раз", + "sign-in": "Пожалуйста, войдите в систему", + "username": "Имя пользователя (эл. адрес)", + "remember-me": "Запомнить меня", + "forgot-password": "Забыли пароль?", + "password-reset": "Пароль сброшен", + "expired-password-reset-message": "Срок действия Вашего пароля закончился! Пожалуйста, создайте новый пароль.", + "new-password": "Новый пароль", + "new-password-again": "Повторите новый пароль", + "password-link-sent-message": "Ссылка для сброса пароля была успешно отправлена!", + "email": "Эл. адрес", + "login-with": "Войти через {{name}}", + "or": "или" + }, + "position": { + "top": "Верх", + "bottom": "Низ", + "left": "Левый край", + "right": "Правый край" + }, + "profile": { + "profile": "Профиль", + "last-login-time": "Время последнего входа в систему", + "change-password": "Изменить пароль", + "current-password": "Текущий пароль" + }, + "relation": { + "relations": "Отношения", + "direction": "Направления", + "search-direction": { + "FROM": "От", + "TO": "К" + }, + "direction-type": { + "FROM": "от", + "TO": "к" + }, + "from-relations": "Исходящие отношения", + "to-relations": "Входящие отношения", + "selected-relations": "Выбрано { count, plural, 1 {1 отношение} few {# отношения} other {# отношений} }", + "type": "Тип", + "to-entity-type": "К типу объекта", + "to-entity-name": "К объекта", + "from-entity-type": "От типа объекта", + "from-entity-name": "От объекта", + "to-entity": "К объекту", + "from-entity": "От объекта", + "delete": "Удалить отношение", + "relation-type": "Тип отношения", + "relation-type-required": "Тип отношения обязателен.", + "any-relation-type": "Любой тип", + "add": "Добавить отношение", + "edit": "Редактировать отношение", + "delete-to-relation-title": "Вы точно хотите удалить отношение, идущее к объекту '{{entityName}}'?", + "delete-to-relation-text": "Внимание, после подтверждения объект '{{entityName}}' будет отвязан от текущего объекта.", + "delete-to-relations-title": "Вы точно хотите удалить { count, plural, 1 {1 отношение} few {# отношения} other {# отношений} }?", + "delete-to-relations-text": "Внимание, после подтверждения выбранные объекты будут отвязаны от текущего объекта.", + "delete-from-relation-title": "Вы точно хотите удалить отношение, идущее от объекта '{{entityName}}'?", + "delete-from-relation-text": "Внимание, после подтверждения текущий объект будет отвязан от объекта '{{entityName}}'.", + "delete-from-relations-title": "Вы точно хотите удалить { count, plural, 1 {1 отношение} few {# отношения} other {# отношений} }?", + "delete-from-relations-text": "Внимание, после подтверждения выбранные объекты будут отвязаны от соответствующих объектов.", + "remove-relation-filter": "Удалить фильтр отношений", + "add-relation-filter": "Добавить фильтр отношений", + "any-relation": "Любое отношение", + "relation-filters": "Фильтры отношений", + "additional-info": "Дополнительная информация (JSON)", + "invalid-additional-info": "Не удалось распознать JSON с дополнительной информацией." + }, + "rulechain": { + "rulechain": "Цепочка правил", + "rulechains": "Цепочки правил", + "root": "Корневая", + "delete": "Удалить цепочку правил", + "name": "Названия", + "name-required": "Название необходимо.", + "description": "Описание", + "add": "Добавить цепочку правил", + "set-root": "Сделать цепочку корневой", + "set-root-rulechain-title": "Вы точно хотите сделать цепочку правил '{{ruleChainName}}' корневой?", + "set-root-rulechain-text": "После подтверждения цепочка правил станет корневой и будет обрабатывать все входящие сообщения.", + "delete-rulechain-title": "Вы точно хотите удалить цепочку правил '{{ruleChainName}}'?", + "delete-rulechain-text": "Внимание, после подтверждения цепочка правил и все связанные с ней данные будут безвозвратно удалены.", + "delete-rulechains-title": "Вы точно хотите удалить { count, plural, 1 {1 цепочку правил} few {# цепочки правил} other {# цепочек правил} }?", + "delete-rulechains-action-title": "Удалить { count, plural, 1 {1 цепочку правил} few {# цепочки правил} other {# цепочек правил} }", + "delete-rulechains-text": "Внимание, после подтверждения выбранные цепочки правил и все связанные с ними данные будут безвозвратно удалены.", + "add-rulechain-text": "Добавить новую цепочку правил", + "no-rulechains-text": "Цепочки правил не найдены", + "rulechain-details": "Подробности о цепочке правил", + "details": "Подробности", + "events": "События", + "system": "Системная", + "import": "Импортировать цепочку правил", + "export": "Экспортировать цепочку правил", + "export-failed-error": "Не удалось экспортировать цепочку правил: {{error}}", + "create-new-rulechain": "Создать новую цепочку правил", + "rulechain-file": "Файл цепочки правил", + "invalid-rulechain-file-error": "Не удалось импортировать цепочку правил: неправильный формат.", + "copyId": "Копировать ИД цепочки правил", + "idCopiedMessage": "ИД цепочки правил скопирован в буфер обмена", + "select-rulechain": "Выбрать цепочку правил", + "no-rulechains-matching": "Цепочки правил, соответствующие '{{entity}}', не найдены.", + "rulechain-required": "Цепочка правил обязательна", + "management": "Управление цепочками правил", + "debug-mode": "Режим отладки" + }, + "rulenode": { + "details": "Подробности", + "events": "События", + "search": "Поиск правил", + "open-node-library": "Открыть библиотеку правил", + "add": "Добавить правило", + "name": "Название", + "name-required": "Название обязательно.", + "type": "Тип", + "description": "Описание", + "delete": "Удалить правило", + "select-all-objects": "Выделить все правила и связи", + "deselect-all-objects": "Отменить выделение правил и связей", + "delete-selected-objects": "Удалить выделенные правила и связи", + "delete-selected": "Удалить выделенные", + "select-all": "Выделить всё", + "copy-selected": "Копировать выделенное", + "deselect-all": "Отменить выделение", + "rulenode-details": "Подробности о правиле", + "debug-mode": "Режим отладки", + "configuration": "Настройки", + "link": "Связь", + "link-details": "Подробности о связи правила", + "add-link": "Добавить связь", + "link-label": "Метка связи", + "link-label-required": "Метка связи обязателен.", + "custom-link-label": "Пользовательская метка связи", + "custom-link-label-required": "Пользовательская метка связи обязателен.", + "link-labels": "Метки связи", + "link-labels-required": "Метки связи обязательны.", + "no-link-labels-found": "Метки связи не найдены", + "no-link-label-matching": "Метка '{{label}}' не найдена.", + "create-new-link-label": "Создать новую!", + "type-filter": "Фильтр", + "type-filter-details": "Фильтр входящих сообщений с заданными условиями", + "type-enrichment": "Насыщение", + "type-enrichment-details": "Добавить данные в метадату сообщения", + "type-transformation": "Преобразование", + "type-transformation-details": "Изменить содержимое сообщение и его метадату", + "type-action": "Действие", + "type-action-details": "Выполнить заданное действие", + "type-external": "Сторонние", + "type-external-details": "Взаимодействовать со сторонними системами", + "type-rule-chain": "Цепочка правил", + "type-rule-chain-details": "Перенаправить входящее сообщение в другую цепочку правил", + "type-input": "Вход", + "type-input-details": "Логический вход цепочки правил перенаправляет входящие сообщения в следующее правило", + "type-unknown": "Неизвестный", + "type-unknown-details": "Неопределенное правило", + "directive-is-not-loaded": "Указанная директива конфигурации '{{directiveName}}' не доступна.", + "ui-resources-load-error": "Не удалось загрузить UI ресурсы.", + "invalid-target-rulechain": "Не удалось определить целевую цепочку правил!", + "test-script-function": "Протестировать скрипт", + "message": "Сообщение", + "message-type": "Тип сообщения", + "select-message-type": "Выбрать тип сообщения", + "message-type-required": "Тип сообщения обязателен", + "metadata": "Метаданные", + "metadata-required": "Метаданные объекта не могут быть пустыми.", + "output": "Выход", + "test": "Протестировать", + "help": "Помощь", + "reset-debug-mode": "Сбросить режим отладки во всех правилах" + }, + "queue": { + "select_name": "Выберите имя для Queue", + "name": "Имя для Queue", + "name_required": "Поле 'Имя для Queue' обязательно к заполнению!" + }, + "tenant": { + "tenant": "Владелец", + "tenants": "Владельцы", + "management": "Управление владельцами", + "add": "Добавить владельца", + "admins": "Администраторы", + "manage-tenant-admins": "Управление администраторами владельца", + "delete": "Удалить владельца", + "add-tenant-text": "Добавить нового владельца", + "no-tenants-text": "Владельцы не найдены", + "tenant-details": "Подробности об владельце", + "delete-tenant-title": "Вы точно хотите удалить владельца '{{tenantTitle}}'?", + "delete-tenant-text": "Внимание, после подтверждения владелец и все связанные с ним данные будут безвозвратно утеряны.", + "delete-tenants-title": "Вы точно хотите удалить { count, plural, one {1 владельца} other {# владельцев} }?", + "delete-tenants-action-title": "Удалить { count, plural, one {1 владельца} other {# владельцев} }", + "delete-tenants-text": "Внимание, после подтверждения выбранные Владельцы и все связанные с ними данные будут безвозвратно утеряны.", + "title": "Имя", + "title-required": "Имя обязательно.", + "description": "Описание", + "details": "Подробности", + "events": "События", + "copyId": "Копировать ИД владельца", + "idCopiedMessage": "ИД владельца скопирован в буфер обмена", + "select-tenant": "Выбрать владельца", + "no-tenants-matching": "Владельцы, соответствующие '{{entity}}', не найдены.", + "tenant-required": "Владелец обязателен" + }, + "timeinterval": { + "seconds-interval": "{ seconds, plural, one {1 секунда} few {# секунды} other {# секунд} }", + "minutes-interval": "{ minutes, plural, one {1 минута} few {# минуты} other {# минут} }", + "hours-interval": "{ hours, plural, one {1 час} few {# часа} other {# часов} }", + "days-interval": "{ days, plural, one {1 день} few {# дня} other {# дней} }", + "days": "Дни", + "hours": "Часы", + "minutes": "Минуты", + "seconds": "Секунды", + "advanced": "Дополнительно" + }, + "timewindow": { + "days": "{ days, plural, one {1 день} few {# дня} other {# дней} }", + "hours": "{ hours, plural, one {1 час} few {# часа} other {# часов} }", + "minutes": "{ minutes, plural, one {1 минута} few {# минуты} other {# минут} }", + "seconds": "{ seconds, plural, one {1 секунда} few {# секунды} other {# секунд} }", + "realtime": "Режим реального времени", + "history": "История", + "last-prefix": "Последние", + "period": "с {{ startTime }} до {{ endTime }}", + "edit": "Изменить временное окно", + "date-range": "Диапазон дат", + "last": "Последние", + "time-period": "Период времени", + "hide": "Скрыть" + }, + "user": { + "user": "Пользователь", + "users": "Пользователи", + "customer-users": "Пользователи клиента", + "tenant-admins": "Администраторы владельца", + "sys-admin": "Системный администратор", + "tenant-admin": "Администратор владельца", + "customer": "Клиент", + "anonymous": "Аноним", + "add": "Добавить пользователя", + "delete": "Удалить пользователя", + "add-user-text": "Добавить нового пользователя", + "no-users-text": "Пользователи не найдены", + "user-details": "Подробности о пользователе", + "delete-user-title": "Вы точно хотите удалить пользователя '{{userEmail}}'?", + "delete-user-text": "Внимание, после подтверждения пользователь и все связанные с ним данные будут безвозвратно утеряны.", + "delete-users-title": "Вы точно хотите удалить { count, plural, one {1 пользователя} other {# пользователей} }?", + "delete-users-action-title": "Удалить { count, plural, one {1 пользователя} other {# пользователей} }", + "delete-users-text": "Внимание, после подтверждения выбранные пользователи и все связанные с ними данные будут безвозвратно утеряны.", + "activation-email-sent-message": "Активационное письмо успешно отправлено!", + "resend-activation": "Повторить отправку активационного письма", + "email": "Эл. адрес", + "email-required": "Эл. адрес обязателен.", + "invalid-email-format": "Не правильный формат письма.", + "first-name": "Имя", + "last-name": "Фамилия", + "description": "Описание", + "default-dashboard": "Дашборд по умолчанию", + "always-fullscreen": "Всегда в полноэкранном режиме", + "select-user": "Выбрать пользователя", + "no-users-matching": "Пользователи, соответствующие '{{entity}}', не найдены.", + "user-required": "Пользователь обязателен", + "activation-method": "Метод активации", + "display-activation-link": "Отобразить ссылку для активации", + "send-activation-mail": "Отправить активационное письмо", + "activation-link": "Активационная ссылка для пользователя", + "activation-link-text": "Для активации пользователя используйте ссылку :", + "copy-activation-link": "Копировать активационную ссылку", + "activation-link-copied-message": "Ссылка для активации пользователя скопировано в буфер обмена", + "details": "Подробности", + "login-as-tenant-admin": "Войти как администратор владельца", + "login-as-customer-user": "Войти как пользователь клиента", + "disable-account": "Отключить учетную запись пользователя", + "enable-account": "Включить учетную запись пользователя", + "enable-account-message": "Учетная запись пользователя была успешно включена!", + "disable-account-message": "Учетная запись пользователя была успешно отключена!" + }, + "value": { + "type": "Тип значения", + "string": "Строка", + "string-value": "Строковое значение", + "integer": "Целое число", + "integer-value": "Целочисленное значение", + "invalid-integer-value": "Неправильный формат целого числа", + "double": "Число двойной точности", + "double-value": "Значение двойной точности", + "boolean": "Логический тип", + "boolean-value": "Логическое значение", + "false": "Ложь", + "true": "Правда", + "long": "Целое число" + }, + "widget": { + "widget-library": "Галерея виджетов", + "widget-bundle": "Набор виджетов", + "select-widgets-bundle": "Выберите набор виджетов", + "management": "Управление виджетами", + "editor": "Редактор виджетов", + "widget-type-not-found": "Ошибка при загрузке конфигурации виджета.
    Возможно, связанный с ней\n тип виджета уже удален.", + "widget-type-load-error": "Не удалось загрузить виджет по следующим причинам:", + "remove": "Удалить виджет", + "edit": "Редактировать виджет", + "remove-widget-title": "Вы точно хотите удалить виджет '{{widgetTitle}}'?", + "remove-widget-text": "Внимание, после подтверждения виджет и все связанные с ним данные будут безвозвратно утеряны.", + "timeseries": "Телеметрия", + "search-data": "Поиск данных", + "no-data-found": "Данные не найдено", + "latest-values": "Последние значения", + "rpc": "Управляющий виджет", + "alarm": "Виджет оповещений", + "static": "Статический виджет", + "select-widget-type": "Выберите тип виджета", + "missing-widget-title-error": "Укажите название виджета!", + "widget-saved": "Виджет сохранен", + "unable-to-save-widget-error": "Не удалось сохранить виджет! Виджет содержит ошибки!", + "save": "Сохранить виджет", + "saveAs": "Сохранить виджет как", + "save-widget-type-as": "Сохранить тип виджета как", + "save-widget-type-as-text": "Пожалуйста, введите название виджета и/или укажите целевой набор виджетов", + "toggle-fullscreen": "Во весь экран", + "run": "Запустить виджет", + "title": "Название виджета", + "title-required": "Название виджета обязательно.", + "type": "Тип виджета", + "resources": "Ресурсы", + "resource-url": "JavaScript/CSS URL", + "remove-resource": "Удалить ресурс", + "add-resource": "Добавить ресурс", + "html": "HTML", + "tidy": "Форматировать", + "css": "CSS", + "settings-schema": "Схема конфигурации", + "datakey-settings-schema": "Схема конфигурации ключа данных", + "javascript": "Javascript", + "remove-widget-type-title": "Вы точно хотите удалить виджет '{{widgetName}}'?", + "remove-widget-type-text": "Внимание, после подтверждения тип виджета и все связанные с ним данные будут безвозвратно утеряны.", + "remove-widget-type": "Удалить тип виджета", + "add-widget-type": "Добавить новый тип виджета", + "widget-type-load-failed-error": "Не удалось загрузить тип виджета!", + "widget-template-load-failed-error": "Не удалось загрузить шаблон виджета!", + "add": "Добавить виджет", + "undo": "Откатить изменения в виджете", + "export": "Экспортировать виджет" + }, + "widget-action": { + "header-button": "Кнопка заголовка виджета", + "open-dashboard-state": "Перейти к новому состоянию дашборда", + "update-dashboard-state": "Обновить текущее состояние дашборда", + "open-dashboard": "Перейти к другому дашборду", + "custom": "Пользовательское действие", + "custom-pretty": "Пользовательское действие (с HTML шаблоном)", + "target-dashboard-state": "Целевое состояние дашборда", + "target-dashboard-state-required": "Целевое состояние дашборда обязательно", + "set-entity-from-widget": "Установить объект из виджета", + "target-dashboard": "Целевой дашборд", + "open-right-layout": "Открыть мобильный режим дашборда" + }, + "widgets-bundle": { + "current": "Текущий набор", + "widgets-bundles": "Наборы виджетов", + "add": "Добавить набор виджетов", + "delete": "Удалить набор виджетов", + "title": "Название", + "title-required": "Название обязательно.", + "add-widgets-bundle-text": "Добавить новый набор виджетов", + "no-widgets-bundles-text": "Наборы виджетов не найдены", + "empty": "Пустой набор виджетов", + "details": "Подробности", + "widgets-bundle-details": "Подробности о наборе виджетов", + "delete-widgets-bundle-title": "Вы точно хотите удалить набор виджетов '{{widgetsBundleTitle}}'?", + "delete-widgets-bundle-text": "Внимание, после подтверждения набор виджетов и все связанные с ним данные будут безвозвратно утеряны.", + "delete-widgets-bundles-title": "Вы точно хотите удалить { count, plural, one {1 набор виджетов} few {# набора виджетов} other {# наборов виджетов} }?", + "delete-widgets-bundles-action-title": "Удалить { count, plural, one {1 набор виджетов} few {# набора виджетов} other {# наборов виджетов} }", + "delete-widgets-bundles-text": "Внимание, после подтверждения выбранные наборы виджетов и все связанные с ними данные будут безвозвратно утеряны..", + "no-widgets-bundles-matching": "Набор виджетов '{{widgetsBundle}}' не найден.", + "widgets-bundle-required": "Набор виджетов обязателен.", + "system": "Системный", + "import": "Импортировать набор виджетов", + "export": "Экспортировать набор виджетов", + "export-failed-error": "Не удалось экспортировать набор виджетов: {{error}}", + "create-new-widgets-bundle": "Создать новый набор виджетов", + "widgets-bundle-file": "Файл набора виджетов", + "invalid-widgets-bundle-file-error": "Не удалось импортировать набор виджетов: неизвестная схема данных набора виджетов." + }, + "widget-config": { + "data": "Данные", + "settings": "Настройки", + "advanced": "Дополнительно", + "title": "Название", + "general-settings": "Общие настройки", + "display-title": "Показать название", + "drop-shadow": "Тень", + "enable-fullscreen": "Во весь экран", + "background-color": "Цвет фона", + "text-color": "Цвет текста", + "padding": "Отступ", + "margin": "Margin", + "widget-style": "Стиль виджета", + "title-style": "Стиль названия", + "mobile-mode-settings": "Настройки мобильного режима", + "order": "Порядок", + "height": "Высота", + "units": "Специальный символ после значения", + "decimals": "Количество цифр после запятой", + "timewindow": "Временное окно", + "use-dashboard-timewindow": "Использовать временное окно дашборда", + "display-timewindow": "Показывать временное окно", + "display-legend": "Показать легенду", + "datasources": "Источники данных", + "maximum-datasources": "Максимальной количество источников данных равно {{count}}", + "datasource-type": "Тип", + "datasource-parameters": "Параметры", + "remove-datasource": "Удалить источник данных", + "add-datasource": "Добавить источник данных", + "target-device": "Целевое устройство", + "alarm-source": "Источник оповещения", + "actions": "Действия", + "action": "Действие", + "add-action": "Добавить действие", + "search-actions": "Поиск действий", + "action-source": "Источник действий", + "action-source-required": "Источник действий обязателен.", + "action-name": "Название", + "action-name-required": "Название действия обязательно.", + "action-name-not-unique": "Действие с таким именем уже существует.
    Название должно быть уникально в рамках одного источника действий.", + "action-icon": "Иконка", + "action-type": "Тип", + "action-type-required": "Тип действий обязателен.", + "edit-action": "Редактировать действие", + "delete-action": "Удалить действие", + "delete-action-title": "Удалить действие виджета", + "delete-action-text": "Вы точно хотите удалить действие виджета '{{actionName}}'?", + "display-icon": "Показывать иконку в названии", + "icon-color": "Цвет иконки", + "icon-size": "Размер иконки" + }, + "widget-type": { + "import": "Импортировать тип виджета", + "export": "Экспортировать тип виджета", + "export-failed-error": "Не удалось экспортировать тип виджета: {{error}}", + "create-new-widget-type": "Создать новый тип виджета", + "widget-type-file": "Файл типа виджета", + "invalid-widget-type-file-error": "Не удалось импортировать виджет: неизвестная схема данных типа виджета." + }, + "widgets": { + "date-range-navigator": { + "localizationMap": { + "Sun": "Вс", + "Mon": "Пн", + "Tue": "Вт", + "Wed": "Ср", + "Thu": "Чт", + "Fri": "Пт", + "Sat": "Сб", + "Jan": "Янв.", + "Feb": "Февр.", + "Mar": "Март", + "Apr": "Апр.", + "May": "Май", + "Jun": "Июнь", + "Jul": "Июль", + "Aug": "Авг.", + "Sep": "Сент.", + "Oct": "Окт.", + "Nov": "Нояб.", + "Dec": "Дек.", + "January": "Январь", + "February": "Февраль", + "March": "Март", + "April": "Апрель", + "June": "Июнь", + "July": "Июль", + "August": "Август", + "September": "Сентябрь", + "October": "Октября", + "November": "Ноябрь", + "December": "Декабрь", + "Custom Date Range": "Пользовательский диапазон дат", + "Date Range Template": "Шаблон диапазона дат", + "Today": "Сегодня", + "Yesterday": "Вчера", + "This Week": "На этой неделе", + "Last Week": "Прошлая неделя", + "This Month": "Этот месяц", + "Last Month": "Прошлый месяц", + "Year": "Год", + "This Year": "В этом году", + "Last Year": "Прошлый год", + "Date picker": "Выбор даты", + "Hour": "Час", + "Day": "День", + "Week": "Неделю", + "2 weeks": "2 Недели", + "Month": "Месяц", + "3 months": "3 Месяца", + "6 months": "6 Месяцев", + "Custom interval": "Пользовательский интервал", + "Interval": "Интервал", + "Step size": "Размер шага", + "Ok": "Ok" + } + }, + "input-widgets": { + "attribute-not-allowed": "Атрибут не может быть выбран в этом виджете", + "date": "Дата", + "blocked-location": "Геолокация заблокирована в вашем браузере", + "claim-device": "Подтвердить устройство", + "claim-failed": "Не удалось подтвердить устройство!", + "claim-not-found": "Устройство не найдено!", + "claim-successful": "Устройство успешно подтверждено!", + "discard-changes": "Отменить изменения", + "device-name": "Название устройства", + "device-name-required": "Необходимо указать название устройства", + "entity-attribute-required": "Значение атрибута обязателено", + "entity-coordinate-required": "Необходимо указать широту и долготу", + "entity-timeseries-required": "Значение телеметрии обязательно", + "get-location": "Получить текущее местоположение", + "latitude": "Широта", + "longitude": "Долгота", + "not-allowed-entity": "Выбраный объект не имеет общих атрибутов", + "no-attribute-selected": "Атрибут не выбран", + "no-datakey-selected": "Ни один datakey не выбран", + "no-entity-selected": "Объект не выбран", + "no-coordinate-specified": "Ключ для широты/долготы не указан", + "no-support-geolocation": "Ваш браузер не поддерживает геолокацию", + "no-image": "Нет изображения", + "no-support-web-camera": "Нет поддерживаемой веб-камеры", + "no-timeseries-selected": "Параметр телеметрии не выбран", + "secret-key": "Секретный ключ", + "secret-key-required": "Необходимо указать секретный ключ", + "switch-attribute-value": "Изменить значение атрибута", + "switch-camera": "Изменить камеру", + "switch-timeseries-value": "Изменить значение телеметрии", + "take-photo": "Сделать фото", + "time": "Время", + "timeseries-not-allowed": "Телеметрия не может быть выбрана в этом виджете", + "update-failed": "Не удалось обновить", + "update-successful": "Успешно обновлено", + "update-attribute": "Обновить атрибут", + "update-timeseries": "Обновить телеметрию", + "value": "Значение" + } + }, + "icon": { + "icon": "Иконка", + "select-icon": "Выбрать иконку", + "material-icons": "Иконки в стиле Material", + "show-all": "Показать все иконки" + }, + "custom": { + "widget-action": { + "action-cell-button": "Кнопка действия в ячейке таблицы", + "row-click": "Действий при щелчке на строку", + "marker-click": "Действия при щелчке на маркер", + "polygon-click": "Действия при щелчке на полигон", + "tooltip-tag-action": "Действие при нажатии на ссылку в подсказке", + "node-selected": "Действий при выборе ноды", + "element-click": "Действий при щелчке на HTML элементе", + "pie-slice-click": "Действий при щелчке на секции круговой диаграммы", + "row-double-click": "Действий при двойном щелчке на строку" + } + }, + "language": { + "language": "Язык" + } +} diff --git a/ui-ngx/src/assets/locale/locale.constant-tr_TR.json b/ui-ngx/src/assets/locale/locale.constant-tr_TR.json new file mode 100644 index 0000000000..dadb85d7fd --- /dev/null +++ b/ui-ngx/src/assets/locale/locale.constant-tr_TR.json @@ -0,0 +1,1679 @@ +{ + "access": { + "unauthorized": "Yetkisiz", + "unauthorized-access": "Yetkisiz Erişim", + "unauthorized-access-text": "Bu kaynağa erişmek için giriş yapmalısınız!", + "access-forbidden": "Erişim Yasaklandı", + "access-forbidden-text": "Bu konuma erişim haklarınız yok!
    Bu yere hala erişmek istiyorsanız farklı kullanıcılarla oturum açmayı deneyin.", + "refresh-token-expired": "Oturum süresi doldu", + "refresh-token-failed": "Oturum yenilenemiyor" + }, + "action": { + "activate": "Etkinleştir", + "suspend": "Askıya al", + "save": "Kaydet", + "saveAs": "Farklı Kaydet", + "cancel": "İptal", + "ok": "Tamam", + "delete": "Sil", + "add": "Ekle", + "yes": "Evet", + "no": "Hayır", + "update": "Güncelle", + "remove": "Kaldır", + "search": "Ara", + "clear-search": "Aramayı Temizle", + "assign": "Ata", + "unassign": "Atamayı kaldır", + "share": "Paylaş", + "make-private": "Özel yap", + "apply": "Uygula", + "apply-changes": "Değişiklikleri Uygula", + "edit-mode": "Düzenleme Modu", + "enter-edit-mode": "Düzenleme moduna gir", + "decline-changes": "Değişiklikleri reddet", + "close": "Kapat", + "back": "Geri", + "run": "Çalıştır", + "sign-in": "Giriş yap!", + "edit": "Düzenle", + "view": "Görüntüle", + "create": "Oluştur", + "drag": "Sürükle", + "refresh": "Yenile", + "undo": "Geri al", + "copy": "Kopyala", + "paste": "Yapıştır", + "copy-reference": "Referansı kopyala", + "paste-reference": "Referansı yapıştır", + "import": "İçe aktar", + "export": "Dışa aktar", + "share-via": "{{provider}} ile paylaş" + }, + "aggregation": { + "aggregation": "Toplama", + "function": "Veri toplama işlevi", + "limit": "Maksimum değerler", + "group-interval": "Gruplama aralığı", + "min": "Min", + "max": "Maks", + "avg": "Ortalama", + "sum": "Toplam", + "count": "Sayı", + "none": "Yok" + }, + "admin": { + "general": "Genel", + "general-settings": "Genel Ayarlar", + "outgoing-mail": "Giden Posta", + "outgoing-mail-settings": "Giden Posta Ayarları", + "system-settings": "Sistem Ayarları", + "test-mail-sent": "Test e-postası başarıyla gönderildi!", + "base-url": "Temel URL", + "base-url-required": "Temel URL gerekli.", + "mail-from": "Gönderen Kişi", + "mail-from-required": "Gönderen Kişi gerekli.", + "smtp-protocol": "SMTP protokolü", + "smtp-host": "SMTP sunucusu", + "smtp-host-required": "SMTP sunucusu gerekli.", + "smtp-port": "SMTP portu", + "smtp-port-required": "Bir SMTP portu sağlamalısınız.", + "smtp-port-invalid": "Bu geçerli bir smtp portu gibi görünmüyor.", + "timeout-msec": "Zaman aşımı (milisaniye)", + "timeout-required": "Zaman aşımı değeri gerekli.", + "timeout-invalid": "Bu geçerli bir zaman aşımı gibi görünmüyor.", + "enable-tls": "TLS'i etkinleştir.", + "tls-version": "TLS sürümü", + "enable-proxy": "Proxy etkinleştir", + "proxy-host": "Proxy sunucusu", + "proxy-host-required": "Proxy sunucusu gereklidir", + "proxy-port": "Proxy portu", + "proxy-port-required": "Proxy portu gereklidir", + "proxy-port-range": "Proxy portu 1 ile 65535 aralığında olmalıdır", + "proxy-user": "Proxy kullanıcı adı", + "proxy-password": "Proxy şifre", + "security-settings": "Güvenlik Ayarları", + "password-policy": "Şifre politikası", + "minimum-password-length": "Minimum şifre uzunluğu", + "minimum-password-length-required": "Minimum şifre uzunluğu zorunludur", + "minimum-password-length-range": "Minimum şifre uzunluğu 5 ile 50 arasında olmalıdır", + "minimum-uppercase-letters": "Minimum büyük harf sayısı", + "minimum-uppercase-letters-range": "Minimum büyük harf sayısı negatif olamaz", + "minimum-lowercase-letters": "Minimum küçük harf sayısı", + "minimum-lowercase-letters-range": "Minimum küçük harf sayısı negatif olamaz", + "minimum-digits": "Minimum rakam sayısı", + "minimum-digits-range": "Minimum rakam sayısı negatif olamaz", + "minimum-special-characters": "Minimum özel karakter sayısı", + "minimum-special-characters-range": "Minimum özel karakter sayısı negatif olamaz", + "password-expiration-period-days": "Gün bazlı şifre son kullanma peryodu", + "password-expiration-period-days-range": "Gün bazlı şifre son kullanma peryodu negatif olamaz", + "password-reuse-frequency-days": "Gün bazlı şifre yeniden kullanım sıklığı", + "password-reuse-frequency-days-range": "Gün bazlı şifre yeniden kullanım sıklığı negatif olamaz", + "general-policy": "Genel politika", + "max-failed-login-attempts": "Hesap kilitlenmesi için gerekli maksimum hatalı giriş deneme sayısı", + "minimum-max-failed-login-attempts-range": "Hesap kilitlenmesi için gerekli maksimum hatalı giriş deneme sayısı negatif olamaz", + "user-lockout-notification-email": "Hesap kilidi kaldırıldığında bilgilendirme maili gönder", + "send-test-mail": "Test e-postası gönder" + }, + "alarm": { + "alarm": "Alarm", + "alarms": "Alarmlar", + "select-alarm": "Alarm seç", + "no-alarms-matching": "'{{entity}}' ile eşleşen alarm bulunamadı.", + "alarm-required": "Alarm gerekli", + "alarm-status": "Alarm durumu", + "search-status": { + "ANY": "Herhangi biri", + "ACTIVE": "Aktif", + "CLEARED": "Temizlendi", + "ACK": "Onaylandı", + "UNACK": "Onaylanmadı" + }, + "display-status": { + "ACTIVE_UNACK": "Aktif Onaylanmadı", + "ACTIVE_ACK": "Aktif Onaylandı", + "CLEARED_UNACK": "Temizlendi Onaylanmadı", + "CLEARED_ACK": "Temizlendi Onaylandı" + }, + "no-alarms-prompt": "Alarm bulunamadı", + "created-time": "Oluşma zamanı", + "type": "Tip", + "severity": "Şiddet", + "originator": "Kaynak", + "originator-type": "Kaynak tipi", + "details": "Detaylar", + "status": "Durum", + "alarm-details": "Alarm detayları", + "start-time": "Başlangıç zamanı", + "end-time": "Bitiş zamanı", + "ack-time": "Onaylanma zamanı", + "clear-time": "Temizlenme zamanı", + "severity-critical": "Kritik", + "severity-major": "Birincil", + "severity-minor": "İkincil", + "severity-warning": "Uyarı", + "severity-indeterminate": "Belirsiz", + "acknowledge": "Onayla", + "clear": "Temizle", + "search": "Alarm ara", + "selected-alarms": "{ count, plural, 1 {1 alarm} other {# alarm} } seçildi", + "no-data": "Görüntülenecek veri bulunmuyor", + "polling-interval": "Alarm yoklama aralığı (saniye)", + "polling-interval-required": "Alarm yoklama aralığı gerekli.", + "min-polling-interval-message": "Alarm yoklama aralığı en az 1 saniye olmalıdır.", + "aknowledge-alarms-title": "{ count, plural, 1 {1 alarmı} other {# alarmı} } onayla", + "aknowledge-alarms-text": "{ count, plural, 1 {1 alarmı} other {# alarmı} } onaylamak istediğinize emin misiniz?", + "clear-alarms-title": "{ count, plural, 1 {1 alarmı} other {# alarmı} } temizle", + "clear-alarms-text": "{ count, plural, 1 {1 alarmı} other {# alarmı} } temizlemek istediğinize emin misiniz?" + }, + "alias": { + "add": "Kısa ad ekle", + "edit": "Kısa ad düzenle", + "name": "Kısa ad", + "name-required": "Kısa ad gerekli", + "duplicate-alias": "Aynı kısa ad daha önce kullanılmış.", + "filter-type-single-entity": "Tek öğe", + "filter-type-entity-list": "Öğe listesi", + "filter-type-entity-name": "Öğe adı", + "filter-type-state-entity": "Kontrol panelinden öğe", + "filter-type-state-entity-description": "Kontrol tablosu durum parametrelerinden alınan öğeler", + "filter-type-asset-type": "Varlık türü", + "filter-type-asset-type-description": "'{{assetType}}' türünde varlıklar", + "filter-type-asset-type-and-name-description": "Adı '{{prefix}}' ile başlayan '{{assetType}}' türünde varlıklar", + "filter-type-device-type": "Aygıt türü", + "filter-type-device-type-description": "'{{deviceType}}' türünde aygıtlar", + "filter-type-device-type-and-name-description": "Adı '{{prefix}}' ile başlayan'{{deviceType}}' türünde aygıtlar", + "filter-type-relations-query": "İlişkiler sorgusu", + "filter-type-relations-query-description": "{{relationType}} türünde ilişkili olan varlıklar: {{entities}}. {{direction}}: {{rootEntity}}", + "filter-type-asset-search-query": "Varlık arama sorgusu", + "filter-type-asset-search-query-description": "{{relationType}} türünde ilişkisi olan varlıklar {{assetTypes}}. {{direction}}: {{rootEntity}}", + "filter-type-device-search-query": "Aygıt arama sorgusu", + "filter-type-device-search-query-description": "{{relationType}} türünde ilişkisi olan aygıt tipleri {{deviceTypes}}. {{direction}}: {{rootEntity}}", + "entity-filter": "Öğe filtresi", + "resolve-multiple": "Çoklu öğe olarak çözümle", + "filter-type": "Filtre tipi", + "filter-type-required": "Filtre tipi gerekli.", + "entity-filter-no-entity-matched": "Belirlenen filtre ile eşleşen bir öğe bulunamadı.", + "no-entity-filter-specified": "Hiçbir öğe filtresi belirtilmedi", + "root-state-entity": "Kontrol panelini kök olarak kullan", + "root-entity": "Kök öğe", + "state-entity-parameter-name": "Durum varlığı parametre adı", + "default-state-entity": "Varsayılan durum öğesi", + "default-entity-parameter-name": "Varsayılan", + "max-relation-level": "Maksimum ilişki düzeyi", + "unlimited-level": "Sınırsız seviye", + "state-entity": "Kontrol paneli öğesi", + "all-entities": "Tüm öğeler", + "any-relation": "Herhangi biri" + }, + "asset": { + "asset": "Varlık", + "assets": "Varlıklar", + "management": "Varlık Yönetimi", + "view-assets": "Varlıkları Görüntüle", + "add": "Varlık ekle", + "assign-to-customer": "Kullanıcı grubuna ata", + "assign-asset-to-customer": "Varlıkları Kullanıcı Grubuna Ata", + "assign-asset-to-customer-text": "Lütfen kullanıcı grubuna atanacak varlıkları seçin", + "no-assets-text": "Varlık bulunamadı", + "assign-to-customer-text": "Lütfen varlıkları atamak için kullanıcı grubu seçin", + "public": "Açık", + "assignedToCustomer": "Kullanıcı grubuna atandı", + "make-public": "Varlığı açık hale getir", + "make-private": "Varlığı özel hale getir", + "unassign-from-customer": "Kullanıcı grubundan atamayı kaldır", + "delete": "Varlığı sil", + "asset-public": "Varlık açık halde", + "asset-type": "Varlık türü", + "asset-type-required": "Varlık türü gerekli.", + "select-asset-type": "Varlık türü seçin", + "enter-asset-type": "Varlık türü girin", + "any-asset": "Herhangi bir varlık", + "no-asset-types-matching": "'{{entitySubtype}}' ile eşleşen varlık bulunamadı.", + "asset-type-list-empty": "Herhangi bir varlık türü bulunamadı.", + "asset-types": "Varlık türleri", + "name": "İsim", + "name-required": "İsim gerekli.", + "description": "Açıklama", + "type": "Tür", + "type-required": "Tür gerekli.", + "details": "Detaylar", + "events": "Olaylar", + "add-asset-text": "Yeni varlık ekle", + "asset-details": "Varlık detayları", + "assign-assets": "Varlıkları ata", + "assign-assets-text": "{ count, plural, 1 {1 varlığı} other {# varlığı} } kullanıcı grubuna ata", + "delete-assets": "Varlıkları sil", + "unassign-assets": "Varlıkların atamalarını kaldır", + "unassign-assets-action-title": "{ count, plural, 1 {1 varlığın} other {# varlığın} } atamalarını kullanıcı grubundan kaldır", + "assign-new-asset": "Yeni varlık ata", + "delete-asset-title": "'{{assetName}}' isimli varlığı silmek istediğinize emin misiniz?", + "delete-asset-text": "UYARI: Onaylandıktan sonra varlık ve ilgili tüm veriler geri yüklenemeyecek şekilde silinecek.", + "delete-assets-title": "{ count, plural, 1 {1 varlığı} other {# varlığı} } silmek istediğinize emin misiniz?", + "delete-assets-action-title": "{ count, plural, 1 {1 varlığı} other {# varlığı} } sil", + "delete-assets-text": "UYARI: Onaylandıktan sonra tüm seçili varlıklar ver ilgili tüm veriler geri yüklenemyeck şekilde silinecek.", + "make-public-asset-title": "'{{assetName}}' isimli varlığı açık hale getirmek istediğinize emin misiniz?", + "make-public-asset-text": "Onaylandıktan sonra varlık ve ilgili veriler açık hale gelecek ve başkaları tarafından erişilebilir olacaktır.", + "make-private-asset-title": "'{{assetName}}' isimli varlığı özel hale getirmek istediğinize emin misiniz?", + "make-private-asset-text": "Onaylandıktan sonra varlık ve ilgili veriler özel hale gelecek ve başkaları tarafından erişilemez olacaktır.", + "unassign-asset-title": "'{{assetName}}' isimli varlığın atamasını kaldırmak istediğinize emin misiniz?", + "unassign-asset-text": "Onaylandıktan sonra varlığın ataması kaldırılacak ve kullanıcı grubu tarafından erişilemez olacaktır.", + "unassign-asset": "Varlık atamasını kaldır", + "unassign-assets-title": " { count, plural, 1 {1 varlık} other {# varlık} } atamasını kaldırmak istediğinize emin misiniz?", + "unassign-assets-text": "Onaylandıktan sonra tüm seçili varlıkların ataması kaldırılacak ve kullanıcı grubu tarafından erişilemez olacaktır.", + "copyId": "Varlık kimliğini kopyala", + "idCopiedMessage": "Varlık kimliği panoya kopyalandı", + "select-asset": "Varlık seç", + "no-assets-matching": "'{{entity}}' isimli varlık bulunamadı.", + "asset-required": "Varlık gerekli", + "name-starts-with": "... ile başlayan varlık adı", + "label": "Etiket", + "import": "Varlıkları içe aktar", + "asset-file": "Varlık dosyası", + "search": "Varlık ara", + "selected-assets": "{ count, plural, 1 {1 varlık} other {# varlık} } selected" + }, + "attribute": { + "attributes": "Öznitelikler", + "latest-telemetry": "Son telemetri", + "attributes-scope": "Varlık öznitelik kapsamı", + "scope-latest-telemetry": "Son telemetri", + "scope-client": "İstemci öznitelikler", + "scope-server": "Sunucu öznitelikler", + "scope-shared": "Paylaşılan öznitelikler", + "add": "Öznitelik ekle", + "key": "Anahtar", + "last-update-time": "Son güncelleme zamanı", + "key-required": "Öznitelik anahtarı gerekli.", + "value": "Değer", + "value-required": "Öznitelik değeri gerekli.", + "delete-attributes-title": "Silmek istediğinize emin misiniz { count, plural, 1 {1 öznitelik} other {# öznitelik} }?", + "delete-attributes-text": "UYARI: Onaylandıktan sonra tüm seçili öznitelikler kaldırılacak.", + "delete-attributes": "Öznitelikleri sil", + "enter-attribute-value": "Öznitelik değeri gir", + "show-on-widget": "Göstergede göster", + "widget-mode": "Gösterge modu", + "next-widget": "Sonraki gösterge", + "prev-widget": "Önceki gösterge", + "add-to-dashboard": "Kontrol paneline ekle", + "add-widget-to-dashboard": "Göstergeyi kontrol paneline ekle", + "selected-attributes": "{ count, plural, 1 {1 öznitelik} other {# öznitelik} } seçildi", + "selected-telemetry": "{ count, plural, 1 {1 telemetri birimi} other {# telemetri birimi} } seçildi" + }, + "audit-log": { + "audit": "Log ve Hata Yönetimi", + "audit-logs": "Loglar ve Hatalar", + "timestamp": "Zaman", + "entity-type": "Kaynak", + "entity-name": "İsim", + "user": "Kullanıcı", + "type": "Tür", + "status": "Durum", + "details": "Detaylar", + "type-added": "Eklendi", + "type-deleted": "Silindi", + "type-updated": "Güncellendi", + "type-attributes-updated": "Özellikler güncellendi", + "type-attributes-deleted": "Özellikler silindi", + "type-rpc-call": "Uzaktan işlem çağrısı", + "type-credentials-updated": "Kimlik bilgileri güncellendi", + "type-assigned-to-customer": "Kullanıcı grubuna atandı", + "type-unassigned-from-customer": "Kullanıcı grubundan atama kaldırıldı", + "type-activated": "Etkinleştirildi", + "type-suspended": "Askıya alındı", + "type-credentials-read": "Kimlik bilgileri okundu", + "type-attributes-read": "Özellikler okundu", + "type-relation-add-or-update": "İlişki güncellendi", + "type-relation-delete": "İlişki silindi", + "type-relations-delete": "Tüm ilişki silindi", + "type-alarm-ack": "Kabul edilen", + "type-alarm-clear": "Temizlendi", + "status-success": "Başarılı", + "status-failure": "Başarısız", + "audit-log-details": "Log ve hata detayları", + "no-audit-logs-prompt": "Log ve hata bulunamadı", + "action-data": "Eylem verisi", + "failure-details": "Başarısız işlem detayları", + "search": "Hata ve Log Geçmişinde Ara", + "clear-search": "Aramayı temizle" + }, + "confirm-on-exit": { + "message": "Kaydedilmemiş değişiklikler var. Sayfadan ayrılmak istediğinize emin misiniz?", + "html-message": "Kaydedilmemiş değişiklikler var.
    Sayfadan ayrılmak istediğinize emin misiniz?", + "title": "Kaydedilmemiş Değişiklikler" + }, + "contact": { + "country": "Ülke", + "city": "Şehir", + "state": "Eyalet / İl", + "postal-code": "Posta Kodu", + "postal-code-invalid": "Geçersiz Posta Kodu.", + "address": "Adres", + "address2": "Adres 2", + "phone": "Telefon", + "email": "E-posta", + "no-address": "Adres yok" + }, + "common": { + "username": "Kullanıcı adı", + "password": "Parola", + "enter-username": "Kullanıcı adı gir", + "enter-password": "Parola gir", + "enter-search": "Arama gir", + "created-time": "Oluşma zamanı" + }, + "content-type": { + "json": "Json", + "text": "Metin", + "binary": "İkili (Base64)" + }, + "customer": { + "customer": "Kullanıcı Grubu", + "customers": "Kullanıcı Grupları", + "management": "Kullanıcı Grubu Yönetimi", + "dashboard": "Kullanıcı Grubu Kontrol Paneli", + "dashboards": "Kullanıcı Grubu Kontrol Panellleri", + "devices": "Kullanıcı Grubu Aygıtları", + "entity-views": "Müşteri Varlığı Görüntüleme Sayısı", + "assets": "Kullanıcı Grubu Varlıkları", + "public-dashboards": "Açık Kontrol Panelleri", + "public-devices": "Açık Aygıtlar", + "public-assets": "Açık Varlıklar", + "public-entity-views": "Kamu Varlık Görüntüleme Sayısı", + "add": "Kullanıcı grubu ekle", + "delete": "Kullanıcı grubunu sil", + "manage-customer-users": "Kullanıcı grubu kullanıcılarını yönet", + "manage-customer-devices": "Kullanıcı grubu aygıtlarını yönet", + "manage-customer-dashboards": "Kullanıcı grubu kontrol panellerini yönet", + "manage-public-devices": "Açık aygıtları yönet", + "manage-public-dashboards": "Açık kontrol panellerini yönet", + "manage-customer-assets": "Kullanıcı Grubu varlıklarını yönet", + "manage-public-assets": "Açık varlıkları yönet", + "add-customer-text": "Yeni Kullanıcı Grubu ekle", + "no-customers-text": "Kullanıcı Grubu bulunamadı", + "customer-details": "Kullanıcı Grubu detayları", + "delete-customer-title": "'{{customerTitle}}' isimli kullanıcı grubunu silmek istediğinize emin misiniz?", + "delete-customer-text": "UYARI: Onaylandıktan sonra kullanıcı grubu ve tüm ilişkili veriler geri yüklenemeyecek şekilde silinecek.", + "delete-customers-title": "{ count, plural, 1 {1 kullanıcı grubunu} other {# kullanıcı grubunu} } silmek istediğinize emin misiniz?", + "delete-customers-action-title": "{ count, plural, 1 {1 kullanıcı grubunu} other {# kullanıcı grubunu} } sil", + "delete-customers-text": "UYARI: Onaylandıktan sonra tüm seçili kullanıcı grupları ve ilişkili veriler geri yüklenemez şekilde silinecek.", + "manage-users": "Kullanıcıları yönet", + "manage-assets": "Varlıkları yönet", + "manage-devices": "Aygıtları yönet", + "manage-dashboards": "Kontrol panellerini yönet", + "title": "Başlık", + "title-required": "Başlık gerekli.", + "description": "Açıklama", + "details": "Detaylar", + "events": "Olaylar", + "copyId": "Kullanıcı kimliğini kopyala", + "idCopiedMessage": "Kullanıcı kimliği panoya kopyalandı", + "select-customer": "Kullanıcı grubunu seç", + "no-customers-matching": "'{{entity}}' ile eşleşen kullanıcı grubu bulunamadı.", + "customer-required": "Kullanıcı grubu gerekli", + "select-default-customer": "Varsayılan müşteriyi seç", + "default-customer": "Varsayılan müşteri", + "default-customer-required": "Kiracı düzeyinde gösterge tablosunda hata ayıklamak için varsayılan müşteri gerekiyor", + "search": "Kullanıcı grubu ara", + "selected-customers": "{ count, plural, 1 {1 kullanıcı grubu} other {# kullanıcı grubu} } seçildi" + }, + "datetime": { + "date-from": "Tarihinden", + "time-from": "Saatinden", + "date-to": "Tarihine", + "time-to": "Saatine" + }, + "dashboard": { + "dashboard": "Kontrol Paneli", + "dashboards": "Kontrol Panelleri", + "management": "Kontrol Paneli Yönetimi", + "view-dashboards": "Kontrol Panellerini Görüntüle", + "add": "Kontrol Paneli Ekle", + "assign-dashboard-to-customer": "Kullanıcı Grubuna Kontrol Panel(ler)i Ata", + "assign-dashboard-to-customer-text": "Lütfen kullanıcı grubuna atanacak kontrol panellerini seçin", + "assign-to-customer-text": "Lütfen kontrol panel(ler)ini atayacak kullanıcı grubu seçin", + "assign-to-customer": "Kullanıcı grubuna ata", + "unassign-from-customer": "Kullanıcı grubundan atamayı kaldır", + "make-public": "Kontrol panelini açık hale getir", + "make-private": "Kontrol panelini özel hale getir", + "manage-assigned-customers": "Atanan müşterileri yönet", + "assigned-customers": "Atanan müşteriler", + "assign-to-customers": "Gösterge Tablosunu / Müşterilerini Müşterilere Atama", + "assign-to-customers-text": "Lütfen gösterge panosunu atamak için müşterileri seçin", + "unassign-from-customers": "Müşterilerden Gösterge Tablosunu (Notlarını) Atama", + "unassign-from-customers-text": "Lütfen gösterge tablosundan atamak için müşterileri seçin", + "no-dashboards-text": "Kontrol paneli bulunamadı", + "no-widgets": "Hiçbir gösterge yapılandırılmadı", + "add-widget": "Yeni gösterge ekle", + "title": "Başlık", + "select-widget-title": "Gösterge seç", + "select-widget-subtitle": "Kullanılabilir gösterge türleri listesi", + "delete": "Kontrol paneli sil", + "title-required": "Başlık gerekli.", + "description": "Açıklama", + "details": "Detaylar", + "dashboard-details": "Kontrol paneli detayları", + "add-dashboard-text": "Yeni kontrol paneli ekle", + "assign-dashboards": "Kontrol panelleri ata", + "assign-new-dashboard": "Yeni kontrol paneli ata", + "assign-dashboards-text": "{ count, plural, 1 {1 kontrol panelini} other {# kontrol panelini} } kullanıcı grubuna ata", + "unassign-dashboards-action-text": "Müşterilerden atama { count, plural, 1 {1 gösterge tablosu} other {# panolar} }", + "delete-dashboards": "Kontrol panellerini sil", + "unassign-dashboards": "Kontrol panellerinden atamayı kaldır", + "unassign-dashboards-action-title": "{ count, plural, 1 {1 kontrol panelinin} other {# kontrol panelinin} } atamaları kullanıcı grubundan kaldır", + "delete-dashboard-title": "'{{dashboardTitle}}' isimli kontrol panelini silmek istediğinize emin misiniz?", + "delete-dashboard-text": "UYARI: Onaylandıktan sonra kontrol paneli ve ilişkili verileri geri yüklenemez şekilde silinecek.", + "delete-dashboards-title": "{ count, plural, 1 {1 kontrol panelini} other {# kontrol panelini} } silmek istediğinize emin misiniz?", + "delete-dashboards-action-title": "{ count, plural, 1 {1 kontrol panelini} other {# kontrol panelini} } sil", + "delete-dashboards-text": "UYARI: Onaylandıktan sonra tüm seçili kontrol panelleri ve ilişkili verileri geri yüklenemez şekilde silinecek.", + "unassign-dashboard-title": "'{{dashboardTitle}}' isimli kontrol panelindeki atamayı kaldırmak istediğinize emin misiniz?", + "unassign-dashboard-text": "Onaylandıktan sonra kontrol panelinin ataması kaldırılacak ve kullanıcı grubu tarafından erişilemez hale gelecektir.", + "unassign-dashboard": "Kontrol panelinin ataması kaldır", + "unassign-dashboards-title": "{count, plural, 1 {1 kontrol panelindeki} other {# kontrol panelindeki} } atamayı kaldırmak istediğinize emin misiniz?", + "unassign-dashboards-text": "Onaylandıktan {{dashboardTitle}}
    açık hale getirildi ve bu bağlantıdan erişilebilir durumda", + "public-dashboard-notice": "Not: Kontrol panelinden tüm verilere erişebilmek adına ilişkili aygıtları da açık hale getirmeniz gerekmektedir.", + "make-private-dashboard-title": "'{{dashboardTitle}}' isimli kontrol panelini özel hale getirmek istediğinize emin misiniz?", + "make-private-dashboard-text": "Onaylandıktan sonra kontrol paneli özel hale getirilecek ve başkaları tarafından erişilemez olacak.", + "make-private-dashboard": "Kontrol panelini özel hale getir", + "socialshare-text": "'{{dashboardTitle}}'", + "socialshare-title": "'{{dashboardTitle}}'", + "select-dashboard": "Kontrol paneli seç", + "no-dashboards-matching": "'{{entity}}' ile eşleşen kontrol paneli bulunamadı.", + "dashboard-required": "Kontrol paneli gerekli.", + "select-existing": "Var olan bir kontrol paneli seç", + "create-new": "Yeni bir kontrol paneli oluştur", + "new-dashboard-title": "Yeni kontrol paneli başlığı", + "open-dashboard": "Kontrol panelini aç", + "set-background": "Arka plan belirle", + "background-color": "Arka plan rengi", + "background-image": "Arka plan resmi", + "background-size-mode": "Arka plan boyutu modu", + "no-image": "Hiçbir resim seçilmedi", + "drop-image": "Bir resim bırakın veya yüklenecek dosyayı seçmek için tıklayın.", + "settings": "Ayarlar", + "columns-count": "Kolon sayısı", + "columns-count-required": "Kolon sayısı gerekli.", + "min-columns-count-message": "Kolon sayısı en az 10 olabilir.", + "max-columns-count-message": "Kolon sayısı en fazla 1000 olabilir.", + "widgets-margins": "Göstergeler arasındaki aralık", + "horizontal-margin": "Yatay aralık", + "horizontal-margin-required": "Yatay aralık değeri gerekli.", + "min-horizontal-margin-message": "Yatay aralık değeri en az 0 olabilir.", + "max-horizontal-margin-message": "Yatay aralık değeri en fazla 50 olabilir.", + "vertical-margin": "Dikey aralık", + "vertical-margin-required": "Dikey aralık değeri gerekli.", + "min-vertical-margin-message": "Dikey aralık değeri en az 0 olabilir.", + "max-vertical-margin-message": "Dikey aralık değeri en fazla 50 olabilir.", + "autofill-height": "Otomatik doldurma düzeni yüksekliği", + "mobile-layout": "Mobil düzen ayarları", + "mobile-row-height": "Mobil satır yüksekliği, px", + "mobile-row-height-required": "Mobil satır yüksekliği değeri gerekli.", + "min-mobile-row-height-message": "Mobil satır yükseliği değeri en az 5 px olabilir.", + "max-mobile-row-height-message": "Mobil satır yükseliği değeri en çok 200 px olabilir.", + "display-title": "Kontrol paneli başlığını göster", + "toolbar-always-open": "Araç çubuğunu her zaman açık tut", + "title-color": "Başlık rengi", + "display-dashboards-selection": "Kontrol paneli seçimlerinş göster", + "display-entities-selection": "Varlık seçimlerini göster", + "display-dashboard-timewindow": "Zaman aralığını göster", + "display-dashboard-export": "Dışa aktar seçeneğini göster", + "import": "Kontrol panelini içe aktar", + "export": "Kontrol panelini dışa aktar", + "export-failed-error": "Kontrol paneli dışa aktarılamıyor: {{error}}", + "create-new-dashboard": "Yeni kontrol paneli oluştur", + "dashboard-file": "Kontrol paneli dosyası", + "invalid-dashboard-file-error": "Kontrol paneli içe aktarılamadı: Geçersiz kontrol paneli veri yapısı.", + "dashboard-import-missing-aliases-title": "İçe aktarılan kontrol paneli tarafından kullanılan aygıt kısa adlarını yapılandırın", + "create-new-widget": "Yeni gösterge oluştur", + "import-widget": "Göstergeyi içe aktar", + "widget-file": "Gösterge dosyası", + "invalid-widget-file-error": "Gösterge içe aktarılamadı: Geçersiz gösterge veri yapısı.", + "widget-import-missing-aliases-title": "İçe aktarılan gösterge tarafından kullanılan aygıt kısa adlarını yapılandırın", + "open-toolbar": "Kontrol paneli araç çubuğunu aç", + "close-toolbar": "Araç çubuğunu kapat", + "configuration-error": "Yapılandırma hatası", + "alias-resolution-error-title": "Kontro paneli kısa adları yapılandırma hatası", + "invalid-aliases-config": "Kısa ad filtresiyle eşleşen aygıt bulunamadı.
    ", + "select-devices": "Aygıt seçin", + "assignedToCustomer": "Kullanıcı grubuna atandı", + "assignedToCustomers": "Kullanıcılara atandı", + "public": "Açık", + "public-link": "Açık bağlantı", + "copy-public-link": "Açık bağlantıyı kopyala", + "public-link-copied-message": "Kontrol paneli açık bağlantısı panoya kopyalandı", + "manage-states": "Kontrol paneli durumlarını yönet", + "states": "Kontrol paneli durumları", + "search-states": "Kontrol paneli durumu ara", + "selected-states": "{ count, plural, 1 {1 kontrol paneli durumu} other {# kontrol paneli durumu} } seçildi", + "edit-state": "Kontrol paneli durumu düzenle", + "delete-state": "Kontrol paneli durumunu sil", + "add-state": "Kontrol paneli durumu ekle", + "state": "Kontrol paneli durumu", + "state-name": "İsim", + "state-name-required": "Kontrol paneli durumu ismi gerekli.", + "state-id": "Durum Kimliği", + "state-id-required": "Kontrol paneli durum kimliği gerekli.", + "state-id-exists": "Aynı kimlikte bir kontrol paneli durumu mevcut.", + "is-root-state": "Kök durum", + "delete-state-title": "Kontrol paneli durumunu sil", + "delete-state-text": "'{{stateName}}' isimli kontrol paneli durumunu silmek istediğinize emin misiniz?", + "show-details": "Detayları göster", + "hide-details": "Detayları gizle", + "select-state": "Hedef durumu seç", + "state-controller": "Durum denetleyicisi", + "margin-required": "Kenar uzaklık(Margin) değeri zorunludur.", + "min-margin-message": "Kenar uzaklığı(Margin) minimum 0 olabilir.", + "max-margin-message": "Kenar uzaklığı(Margin) en fazla 50 olabilir.", + "display-filters": "Filtreleri göster", + "no-states-text": "Durum bulunamadı", + "search": "Kontrol paneli ara", + "selected-dashboards": "{ count, plural, 1 {1 kontrol paneli} other {# kontrol paneli} } seçildi" + }, + "datakey": { + "settings": "Ayarlar", + "advanced": "İleri düzey", + "label": "Etiket", + "color": "Renk", + "units": "Değerin yanında göstermek için özel simge", + "decimals": "Noktadan sonraki basamak sayısı", + "data-generation-func": "Veri oluşturma fonksiyonu", + "use-data-post-processing-func": "Veri işleme sonrası fonksiyonunu kullanın", + "configuration": "Veri anahtarı yapılandırması", + "timeseries": "Zaman serisi", + "attributes": "Öznitelikler", + "alarm": "Alarm alanları", + "timeseries-required": "Zaman serisi öğesi gerekli.", + "timeseries-or-attributes-required": "Zaman serisi/öznitelikler öğesi gerekli.", + "maximum-timeseries-or-attributes": "Maksimum { count, plural, 1 {1 zamanserisi/öznitelik kabul edilir.} other {# zamanserisi/öznitelik kabul edilir} }", + "alarm-fields-required": "Alarm alanları gerekli.", + "function-types": "Fonksiyon türleri", + "function-types-required": "Fonksiyon türleri gerekli.", + "maximum-function-types": "Maksimum { count, plural, 1 {1 fonksiyon türü kabul edilir.} other {# fonksiyon türü kabul edilir} }" + }, + "datasource": { + "type": "Veri kaynağı türü", + "name": "İsim", + "add-datasource-prompt": "Lütfen veri kaynağı ekleyin" + }, + "details": { + "edit-mode": "Düzenleme modu", + "toggle-edit-mode": "Düzenleme modunu aç/kapat" + }, + "device": { + "device": "Aygıt", + "device-required": "Aygıt gerekli.", + "devices": "Aygıtlar", + "management": "Aygıt Yönetimi", + "view-devices": "Aygıtları görüntüle", + "device-alias": "Aygıt kısa adı", + "aliases": "Aygıt kısa adları", + "no-alias-matching": "'{{alias}}' bulunamadı.", + "no-aliases-found": "Hiçbir kısa ad bulunamadı.", + "no-key-matching": "'{{key}}' bulunamadı.", + "no-keys-found": "Hiçbir anahtar bulunamadı.", + "create-new-alias": "Yeni bir tane oluştur!", + "create-new-key": "Yeni bir tane oluştur!", + "duplicate-alias-error": "'{{alias}}' daha önce kaydedilmiş.
    Aygıt kısa adları kontrol paneli özelinde emsalsiz olmalıdır.", + "configure-alias": "'{{alias}}' kısa adını yapılandırın", + "no-devices-matching": "'{{entity}}' ile eşleşen aygıt bulunamadı.", + "alias": "Kısa ad", + "alias-required": "Aygıt kısa adı gerekli.", + "remove-alias": "Aygıt kısa adını kaldır", + "add-alias": "Aygıt kısa adı ekle", + "name-starts-with": "... ile başlayan aygıt adı", + "device-list": "Aygıt listesi", + "use-device-name-filter": "Filtre kullan", + "device-list-empty": "Hiçbir aygıt seçilmedi.", + "device-name-filter-required": "Aygıt adı filtresi gerekli.", + "device-name-filter-no-device-matched": "'{{device}}' ile başlayan herhangi bir aygıt bulunamadı.", + "add": "Aygıt ekle", + "assign-to-customer": "Kullanıcı grubuna ata", + "assign-device-to-customer": "Aygıt(lar)ı Kullanıcı Grubuna Ata", + "assign-device-to-customer-text": "Lütfen kullanıcı grubuna atanacak aygıtları seçin", + "make-public": "Aygıtı açık hale getir", + "make-private": "Aygıtı gizli hale getir", + "no-devices-text": "Hiçbir aygıt bulunamadı", + "assign-to-customer-text": "Lütfen aygıt(lar)ı atayacak kullanıcı grubu seçin", + "device-details": "Aygıt detayları", + "add-device-text": "Yeni aygıt ekle", + "credentials": "Kimlik bilgileri", + "manage-credentials": "Kimlik bilgilerini yönet", + "delete": "Aygıt sil", + "assign-devices": "Aygıt ata", + "assign-devices-text": "{ count, plural, 1 {1 aygıtı} other {# aygıtı} } kullanıcı grubuna ata", + "delete-devices": "Aygıtları sil", + "unassign-from-customer": "Kullanıcı Grubundan atamayı kaldır", + "unassign-devices": "Aygıtlardan atamayı kaldır", + "unassign-devices-action-title": "{ count, plural, 1 {1 aygıtın} other {# aygıtın} } atamasını kullanıcı grubundan kaldır", + "assign-new-device": "Yeni aygıt ata", + "make-public-device-title": "'{{deviceName}}' isimli aygıtı açık hale getirmek istediğinizden emin misiniz?", + "make-public-device-text": "Onaylandıktan sonra aygıt ve verileri açık hale getirilecek ve diğerleri tarafından erişilebilir olacak.", + "make-private-device-title": "'{{deviceName}}' isimli aygıtı gizli hale getirmek istediğinizden emin misiniz?", + "make-private-device-text": "Onaylandıktan sonra aygıt ve verileri gizli hale getirilecek ve diğerleri tarafından erişilemez olacak.", + "view-credentials": "Kimlik bilgilerini görüntüle", + "delete-device-title": "'{{deviceName}}' isimli aygıtı silmek istediğinize emin misiniz?", + "delete-device-text": "UYARI: Onaylandıktan sonra aygıt ve ilişkili verileri geri yüklenemez şekilde silinecek.", + "delete-devices-title": "{ count, plural, 1 {1 aygıtı} other {# aygıtı} } silmek istediğinize emin misiniz?", + "delete-devices-action-title": "{ count, plural, 1 {1 aygıtı} other {# aygıtı} } sil", + "delete-devices-text": "UYARI: Onaylandıktan sonra tüm seçili aygıtlar ve ilişkili verileri geri yüklenemez şekilde silinecek.", + "unassign-device-title": "'{{deviceName}}' isimli aygıtın atamasını kaldırmak istediğinize emin misiniz?", + "unassign-device-text": "Onaylandıktan sonra aygıtın ataması kaldırılacak ve kullanıcı grubu tarafından erişilemez olacak.", + "unassign-device": "Aygıt atamasını kaldır", + "unassign-devices-title": "{ count, plural, 1 {1 aygıtın} other {# aygıtın} } atamasını kaldırmak istediğinize emin misiniz?", + "unassign-devices-text": "Onaylandıktan sonra seçili aygıtların atamaları kaldırılacak ve kullanıcı grubu tarafından erişilemez olacak.", + "device-credentials": "Aygıt Kimlik Bilgileri", + "credentials-type": "Kimlik Bilgi Türü", + "access-token": "Erişim şifresi", + "access-token-required": "Erişim şifresi gerekli.", + "access-token-invalid": "Erişim şifresi uzunluğu 1 ile 20 karakter arasında olmalıdır.", + "rsa-key": "RSA açık anahtarı", + "rsa-key-required": "RSA açık anahtarı gerekli.", + "secret": "Secret", + "secret-required": "Secret gerekli.", + "device-type": "Aygıt Türü", + "device-type-required": "Aygıt türü gereli.", + "select-device-type": "Aygıt türü seç", + "enter-device-type": "Aygıt türü gir", + "any-device": "Herhangi bir aygıt", + "no-device-types-matching": "'{{entitySubtype}}' ile eşleşen aygıt türü bulunamadı.", + "device-type-list-empty": "Hiçbir aygıt türü seçilmedi.", + "device-types": "Aygıt türleri", + "name": "İsim", + "name-required": "İsim gerekli.", + "description": "Açıklama", + "events": "Olaylar", + "details": "Detaylar", + "copyId": "Aygıt kimliğini kopyala", + "copyAccessToken": "Erişim şifresini kopyala", + "idCopiedMessage": "Aygıt kimliği panoya kopyalandı.", + "accessTokenCopiedMessage": "Aygıt erişim şifresi panoya kopyalandı", + "assignedToCustomer": "Kullanıcı Grubuna atandı", + "unable-delete-device-alias-title": "Aygıt kısa adı silinemedi", + "unable-delete-device-alias-text": "Aygıt kısa adı('{{deviceAlias}}'), şu göstergeler tarafından kullanıldığı için silinemedi:
    {{widgetsList}}", + "is-gateway": "Ağ geçidi mi?", + "public": "Açık", + "device-public": "Aygıt açık", + "select-device": "Aygıt seç", + "label": "Etiket", + "import": "Aygıt içe aktar", + "device-file": "Aygıt dosyası", + "search": "Aygıt ara", + "selected-devices": "{ count, plural, 1 {1 aygıt} other {# aygıt} } seçildi" + }, + "dialog": { + "close": "Kapat" + }, + "error": { + "unable-to-connect": "Sunucuya bağlanamadı! Lütfen internet bağlantınızı kontrol edin.", + "unhandled-error-code": "İşlenmeyen hata koud: {{errorCode}}", + "unknown-error": "Bilinmeyen hata" + }, + "entity": { + "entity": "Öğe", + "entities": "Öğeler", + "aliases": "Öğe kısa adları", + "entity-alias": "Öğe kısa adı", + "unable-delete-entity-alias-title": "Öğe kısa adı silinemedi", + "unable-delete-entity-alias-text": "Öğe kısa adı('{{entityAlias}}'), şu göstergeler tarafından kullanıldığı için silinemiyor:
    {{widgetsList}}", + "duplicate-alias-error": "'{{alias}}' daha önce kaydedilmiş.
    Öğe kısa adları kontrol paneli özelinde emsalsiz olmalı.", + "missing-entity-filter-error": "'{{alias}}' için filtre bulunmuyor.", + "configure-alias": "'{{alias}}' kısa adını yapılandır", + "alias": "Kısa ad", + "alias-required": "Öğe kısa adı gerekli.", + "remove-alias": "Öğe kısa adını kaldır", + "add-alias": "Öğe kısa adı ekle", + "entity-list": "Öğe listesi", + "entity-type": "Öğe türü", + "entity-types": "Öğe türleri", + "entity-type-list": "Öğe türü listesi", + "any-entity": "Herhangi bir öğe", + "enter-entity-type": "Öğe türü girin", + "no-entities-matching": "'{{entity}}' ile eşleşen öğe bulunamadı.", + "no-entity-types-matching": "'{{entityType}}' ile eşleşen öğe türü bulunamadı.", + "name-starts-with": "... ile başlayan isim", + "use-entity-name-filter": "Filtre kullan", + "entity-list-empty": "Hiçbir öğe seçilmedi.", + "entity-type-list-empty": "Hiçbir öğe türü seçilmedi.", + "entity-name-filter-required": "Öğe ismi filtresi gerekli.", + "entity-name-filter-no-entity-matched": "'{{entity}}' ile başlayan hiçbir öğe bulunamadı.", + "all-subtypes": "Tümü", + "select-entities": "Öğeleri seç", + "no-aliases-found": "Hiçbir kısa ad bulunamadı.", + "no-alias-matching": "'{{alias}}' bulunamadı.", + "create-new-alias": "Yeni bir tane oluştur!", + "key": "Anahtar", + "key-name": "Anahtar adı", + "no-keys-found": "Hiçbir anahtar bulunamadı.", + "no-key-matching": "'{{key}}' bulunamadı.", + "create-new-key": "Yeni bir tane oluştur!", + "type": "Tür", + "type-required": "Öğe türü gerekli.", + "type-device": "Aygıt", + "type-devices": "Aygıtlar", + "list-of-devices": "{ count, plural, 1 {Bir aygıt} other {# Aygıtın Listesi} }", + "device-name-starts-with": "İsimleri '{{prefix}}' ile başlayan aygıtlar", + "type-asset": "Varlık", + "type-assets": "Varlıklar", + "list-of-assets": "{ count, plural, 1 {Bir varlık} other {# Varlığın Listesi} }", + "asset-name-starts-with": "İsmi '{{prefix}}' ile başlayan varlıklar", + "type-entity-view": "Varlık Görünümü", + "type-entity-views": "Varlık Görünümleri", + "list-of-entity-views": "{ count, plural, 1 {Bir varlık görünümü} other {# varlık görüntüleme} } listesi", + "entity-view-name-starts-with": "Adı {{önek}} ile başlayan varlık görünümleri", + "type-rule": "Kural", + "type-rules": "Kurallar", + "list-of-rules": "{ count, plural, 1 {Bir kural} other {# Kuralın Listesi} }", + "rule-name-starts-with": "İsmi '{{prefix}}' ile başlayan kurallar", + "type-plugin": "Eklenti", + "type-plugins": "Eklentiler", + "list-of-plugins": "{ count, plural, 1 {Bir eklenti} other {# Eklentinin Listesi} }", + "plugin-name-starts-with": "İsmi '{{prefix}}' ile başlayan eklentiler", + "type-tenant": "Tenant", + "type-tenants": "Tenantlar", + "list-of-tenants": "{ count, plural, 1 {Bir tenant} other {# Tenantın Listesi} }", + "tenant-name-starts-with": "İsmi '{{prefix}}' ile başlayan tenantlar", + "type-customer": "Kullanıcı Grubu", + "type-customers": "Kullanıcı Grupları", + "list-of-customers": "{ count, plural, 1 {Bir Kullanıcı Grubu} other {# Kullanıcı Grupları} }", + "customer-name-starts-with": "İsmi '{{prefix}}' ile başlayan Kullanıcı Grupları", + "type-user": "Kullanıcı", + "type-users": "Kullanıcılar", + "list-of-users": "{ count, plural, 1 {Bir kullanıcı} other {# Kullanıcının Listesi} }", + "user-name-starts-with": "İsmi '{{prefix}}' ile başlayan kullanıcılar", + "type-dashboard": "Kontrol paneli", + "type-dashboards": "Kontrol panelleri", + "list-of-dashboards": "{ count, plural, 1 {Bir kontrol paneli} other {# Kontrol Panelinin Listesi} }", + "dashboard-name-starts-with": "İsmi '{{prefix}}' ile başlayan kontrol panelleri", + "type-alarm": "Alarm", + "type-alarms": "Alarmlar", + "list-of-alarms": "{ count, plural, 1 {Bir alarm} other {# Alarmın Listesi} }", + "alarm-name-starts-with": "İsmi '{{prefix}}' ile başlayan alarmlar", + "type-rulechain": "Kural zinciri", + "type-rulechains": "Kural zincirleri", + "list-of-rulechains": "{ count, plural, 1 {Bir kural zinciri} other {# kural zincirinin listesi} }", + "rulechain-name-starts-with": "İsimleri {{prefix}} ile başlayan kural zincirleri", + "type-rulenode": "Kural düğümü", + "type-rulenodes": "Kural düğümleri", + "list-of-rulenodes": "{ count, plural, 1 {Bir kural node} other {# kural düğümünün listesi} }", + "rulenode-name-starts-with": "İsimleri '{{prefix}} ile başlayan kural düğümleri", + "type-current-customer": "Mevcut Müşteri", + "search": "Öğeleri ara", + "selected-entities": "{ count, plural, 1 {1 öğe} other {# öğe} } seçildi", + "entity-name": "Öğe adı", + "details": "Öğe detayları", + "no-entities-prompt": "Hiçbir öğe bulunamadı", + "no-data": "Görüntülenecek veri yok" + }, + "entity-view": { + "entity-view": "Varlık Görünümü", + "entity-views": "Varlık Görünümleri", + "management": "Varlık Görünümü yönetimi", + "view-entity-views": "Varlık Görünümlerini Görüntüle", + "entity-view-alias": "Varlık Görünümü takma adı", + "aliases": "Varlık Görünümü takma adları", + "no-alias-matching": "'{{alias}} bulunamadı. ", + "no-aliases-found": "Takma ad bulunamadı", + "no-key-matching": "'{{key}}' anahtar bulunamadı.", + "no-keys-found": "Anahtar bulunamadı.", + "create-new-alias": "Yeni bir tane oluştur!", + "create-new-key": "Yeni bir tane oluştur!", + "duplicate-alias-error": "Yinelenen takma ad bulundu {{alias}} '.. Entity View diğer adlar, gösterge panosunda benzersiz olmalıdır. ", + "configure-alias": "Yapılandırma {{alias}} takma ad", + "no-entity-views-matching": "{{entity}} ile eşleşen hiçbir varlık yorumu bulunamadı. ", + "alias": "Alias", + "alias-required": "Varlık Görünümü takma adı gerekiyor.", + "remove-alias": "Varlık görünümü takma adını kaldır", + "add-alias": "Varlık görünümü takma adı ekle", + "name-starts-with": "Varlık Görünümü adı ile başlıyor", + "entity-view-list": "Varlık Görünümü listesi", + "use-entity-view-name-filter": "Filtre kullan", + "entity-view-list-empty": "Hiçbir varlık görüşü seçilmedi.", + "entity-view-name-filter-required": "Varlık görünüm adı filtresi gerekli.", + "entity-view-name-filter-no-entity-view-matched": "{{entityView}} ile başlayan hiçbir varlık sayısı bulunamadı.", + "add": "Varlık Görünümü Ekle", + "assign-to-customer": "Müşteriye atama", + "assign-entity-view-to-customer": "Varlık Görünümlerini Müşteriye Atama", + "assign-entity-view-to-customer-text": "Lütfen müşteriye atamak için varlık görünümlerini seçin", + "no-entity-views-text": "Varlık görüşü bulunamadı", + "assign-to-customer-text": "Lütfen varlık görünümlerini atamak için müşteriyi seçin", + "entity-view-details": "Varlık görünümü ayrıntıları", + "add-entity-view-text": "Yeni varlık görünümü ekle", + "delete": "Varlık görünümünü sil", + "assign-entity-views": "Varlık görünümleri atama", + "assign-entity-views-text": "Müşteriye { count, plural, 1 {1 entityView} other {# entityViews} } atayın ", + "delete-entity-views": "Varlık görünümlerini sil", + "unassign-from-customer": "Müşteriden atama", + "unassign-entity-views": "Varlık görünümlerini atama", + "unassign-entity-views-action-title": "Müşteriden atama { count, plural, 1 {1 entityView} other {# entityViews} }", + "assign-new-entity-view": "Yeni varlık görünümü atama", + "delete-entity-view-title": "Varlık görünümünü silmek istediğinizden emin misiniz?, {{entityViewName}} '? ", + "delete-entity-view-text": "Dikkatli olun, onaylandıktan sonra varlık görünümü ve ilgili tüm veriler kurtarılamayacak.", + "delete-entity-views-title": "{ count, plural, 1 {1 entityView} other {# entityViews} } varlık görünümüne sahip olmak istediğinizden emin misiniz?", + "delete-entity-views-action-title": "Sil { count, plural, 1 {1 entityView} other {# entityViews} }", + "delete-entity-views-text": "Dikkatli olun, onaylandıktan sonra tüm seçilen görünümler kaldırılacak ve ilgili tüm veriler kurtarılamayacaktır.", + "unassign-entity-view-title": "Varlık görünümünün atamasını kaldırmak istediğinizden emin misiniz? {{entityViewName}} '? ", + "unassign-entity-view-text": "Onaydan sonra varlık görünümü atanmamış olacak ve müşteri tarafından erişilemeyecektir.", + "unassign-entity-view": "Varlık görünümünün atamasını kaldır", + "unassign-entity-views-title": "{ count, plural, 1 {1 entityView} other {# entityViews} } hesabının atamasını kaldırmak istediğinizden emin misiniz?", + "unassign-entity-views-text": "Onaylandıktan sonra, seçilen tüm öğe görünümleri atamadan kaldırılacak ve müşteri tarafından erişilemeyecektir.", + "entity-view-type": "Varlık Görünümü türü", + "entity-view-type-required": "Varlık Görünümü türü gerekli.", + "select-entity-view-type": "Varlık görüntüleme türünü seç", + "enter-entity-view-type": "Varlık görüntüleme türünü girin", + "any-entity-view": "Herhangi bir varlık görünümü", + "no-entity-view-types-matching": "{{entitySubtype}} ile eşleşen hiçbir varlık görüntüleme türü bulunamadı. ", + "entity-view-type-list-empty": "Hiçbir varlık görünümü türü seçilmemiş.", + "entity-view-types": "Varlık Görünümü türleri", + "name": "Ad", + "name-required": "İsim gerekli.", + "description": "Açıklama", + "events": "Etkinlikler", + "details": "Ayrıntılar", + "copyId": "Varlık görüntüleme kimliğini kopyala", + "assignedToCustomer": "Müşteriye atandı", + "unable-entity-view-device-alias-title": "Varlık görünümü takma adı silinemiyor", + "unable-entity-view-device-alias-text": "Cihaz takma adı {{entityViewAlias}} ', aşağıdaki widget (lar) tarafından kullanıldığı şekliyle silinemez:
    {{widgetsList}} ", + "select-entity-view": "Varlık görünümünü seç", + "make-public": "Varlığı herkese görünür yap", + "start-ts": "Başlangıç zamanı", + "end-ts": "Bitiş zamanı", + "date-limits": "Tarih Aralığı", + "client-attributes": "İstemci özellikler", + "shared-attributes": "Paylaşılan özellikler", + "server-attributes": "Sunucu özellikler", + "timeseries": "Zaman serisi", + "client-attributes-placeholder": "İstemci özellikler", + "shared-attributes-placeholder": "Paylaşılan özellikler", + "server-attributes-placeholder": "Sunucu özellikler", + "timeseries-placeholder": "Zaman serisi", + "target-entity": "Hedef nesne", + "attributes-propagation": "Özelliklerin yayılması", + "attributes-propagation-hint": "Varlık görünümleri hedef nesne özelliklerindeki değişikliklikleri her varlık görünümü kaydet veya güncelleme işleminden sonra kopyalar. Performans sebebi ile hedef nesnede gerçekleşen her kaydetme ve güncelleme işlemi otomatik yansıtılmaz. Otomatik yansıma özelliğini \"Görünüme kopyala\" kuralını değiştirerek, \"Özellikleri kaydet\" and \"Özellikleri güncelle\" özelliklerini güncelleyerek gerçekleştirebilirsiniz.", + "timeseries-data": "Zaman serisi veri", + "timeseries-data-hint": "Varlık görünümünün ulaşabildiği, hedef nesnedeki zaman verisi anahtarlarını değiştirebilirsiniz. Bu zaman serisi verisi sadece okuma amaçlıdır, güncellenemez.", + "make-public-entity-view-title": "'{{entityViewName}}' varlık görünümünü açık hale getirmek istediğinize emin misiniz?", + "make-public-entity-view-text": "Varlık görünümünü açık hale getirdikten sonra, varlık görünümü ve tüm verileri başkaları tarafından erişilebilir hale gelecektir.", + "make-private-entity-view-title": "'{{entityViewName}}' varlık görünümünü özel hale getirmek istediğinize emin misiniz?", + "make-private-entity-view-text": "Varlık görünümünü özel hale getirdikten sonra, varlık görünümü ve tüm verileri başkaları tarafından erişilemez.", + "search": "Varlık görünümü ara", + "selected-entity-views": "{ count, plural, 1 {1 varlık görünümü} other {# varlık görünümü} } seçildi" + }, + "event": { + "event-type": "Olay türü", + "type-error": "Hata", + "type-lc-event": "Yaşam döngüsü olayı", + "type-stats": "İstatistikler", + "type-debug-rule-node": "Hata ayıklama", + "type-debug-rule-chain": "Hata ayıklama", + "no-events-prompt": "Hiçbir olay bulunamadı", + "error": "Hata", + "alarm": "Alarm", + "event-time": "Olay zamanı", + "server": "Sunucu", + "body": "İçerik //(Body)", + "method": "Yöntem", + "type": "Tür", + "entity": "Varlık", + "message-id": "Mesaj Kimliği", + "message-type": "Mesaj tipi", + "data-type": "Veri tipi", + "relation-type": "İlişki Türü", + "metadata": "Meta veri", + "data": "Veri", + "event": "Olay", + "status": "Durum", + "success": "Başarı", + "failed": "Başarısız oldu", + "messages-processed": "Mesajlar işlendi", + "errors-occurred": "Hatalar oluştu" + }, + "extension": { + "extensions": "Uzantılar", + "selected-extensions": "{ count, plural, 1 {1 uzantı} other {# extensions} } seçildi", + "type": "Tür", + "key": "Anahtar", + "value": "Değer", + "id": "İD", + "extension-id": "Uzantı kimliği", + "extension-type": "Uzatma tipi", + "transformer-json": "JSON *", + "unique-id-required": "Mevcut uzantı kimliği zaten mevcut.", + "delete": "Uzantıyı sil", + "add": "Uzantı eklemek", + "edit": "Uzantıyı düzenle", + "delete-extension-title": "{{ExtensionId}} uzantısını silmek istediğinizden emin misiniz? ", + "delete-extension-text": "Dikkatli olun, onaylamadan sonra uzantı ve ilgili tüm veriler kurtarılamaz.", + "delete-extensions-title": "{ count, plural, 1 {1 uzantı} other {# extensions} } silmek istediğinizden emin misiniz?", + "delete-extensions-text": "Dikkatli olun, onaylandıktan sonra tüm seçilen uzantılar kaldırılacak.", + "converters": "Dönüştürücü", + "converter-id": "Dönüştürücü kimliği", + "configuration": "Yapılandırma", + "converter-configurations": "Dönüştürücü yapılandırmaları", + "token": "Güvenlik belirteci", + "add-converter": "Dönüştürücü ekle", + "add-config": "Dönüştürücü yapılandırması ekle", + "device-name-expression": "Cihaz adı ifadesi", + "device-type-expression": "Cihaz tipi ifadesi", + "custom": "Özel", + "to-double": "Çifte", + "transformer": "Transformer", + "json-required": "Trafo jsonu gerekli.", + "json-parse": "Trafo json ayrıştırılamıyor.", + "attributes": "Öznitellikler", + "add-attribute": "Özellik ekle", + "add-map": "Eşleme elemanı ekle", + "timeseries": "Zaman serisi", + "add-timeseries": "Zaman çizelgeleri ekle", + "field-required": "Alan gereklidir", + "brokers": "Komisyoncular", + "add-broker": "Broker ekle", + "host": "Host", + "port": "Liman", + "port-range": "Liman 1'den 65535'e kadar olmalıdır.", + "ssl": "SSL", + "credentials": "Kimlik bilgileri", + "username": "Kullanıcı adı", + "password": "Parola", + "retry-interval": "Milisaniye cinsinden tekrar deneme aralığı", + "anonymous": "Anonim", + "basic": "Temel", + "pem": "PEM", + "ca-cert": "CA sertifika dosyası *", + "private-key": "Özel anahtar dosya *", + "cert": "Sertifika dosyası *", + "no-file": "Dosya seçilmedi.", + "drop-file": "Bir dosya bırakın veya yüklenecek bir dosya seçmek için tıklayın.", + "mapping": "Mapping", + "topic-filter": "Konu filtresi", + "converter-type": "Dönüştürücü tipi", + "converter-json": "Json", + "json-name-expression": "Cihaz adı json ifadesi", + "topic-name-expression": "Cihaz adı konu ifadesi", + "json-type-expression": "Cihaz tipi json ifadesi", + "topic-type-expression": "Cihaz tipi konu ifadesi", + "attribute-key-expression": "Öznitelik anahtar ifadesi", + "attr-json-key-expression": "Öznitelik anahtar json ifadesi", + "attr-topic-key-expression": "Öznitelik anahtar konu ifadesi", + "request-id-expression": "Kimlik ifadesi iste", + "request-id-json-expression": "Kimlik json ifadesi iste", + "request-id-topic-expression": "Kimlik konu ifadesini isteyin", + "response-topic-expression": "Yanıt konusu ifadesi", + "value-expression": "Değer ifadesi", + "topic": "Konu", + "timeout": "Zaman aşımı milisaniye cinsinden", + "converter-json-required": "Dönüştürücü json gerekli.", + "converter-json-parse": "Dönüştürücü json ayrıştırılamıyor.", + "filter-expression": "Filtre ifadesi", + "connect-requests": "İstekleri bağla", + "add-connect-request": "Bağlantı talebi ekle", + "disconnect-requests": "İstekleri kes", + "add-disconnect-request": "Bağlantıyı kes isteği ekle", + "attribute-requests": "Özellik istekleri", + "add-attribute-request": "Özellik isteği ekle", + "attribute-updates": "Öznitelik güncellemeleri", + "add-attribute-update": "Özellik güncellemesi ekle", + "server-side-rpc": "Sunucu tarafı RPC", + "add-server-side-rpc-request": "Sunucu tarafı RPC isteği ekle", + "device-name-filter": "Cihaz adı filtresi", + "attribute-filter": "Özellik filtresi", + "method-filter": "Yöntem filtresi", + "request-topic-expression": "Konu ifadesi iste", + "response-timeout": "Milisaniye cinsinden yanıt zaman aşımı", + "topic-expression": "Konu ifadesi", + "client-scope": "Müşteri kapsamı", + "add-device": "Cihaz ekle", + "opc-server": "Sunucular", + "opc-add-server": "Sunucu ekle", + "opc-add-server-prompt": "Lütfen sunucu ekle", + "opc-application-name": "Uygulama Adı", + "opc-application-uri": "Uygulama uri", + "opc-scan-period-in-seconds": "Saniyeler içinde tarama süresi", + "opc-security": "Güvenlik", + "opc-identity": "Kimlik", + "opc-keystore": "Keystore", + "opc-type": "Tür", + "opc-keystore-type": "Tür", + "opc-keystore-location": "Yer *", + "opc-keystore-password": "Parola", + "opc-keystore-alias": "Alias", + "opc-keystore-key-password": "Anahtar şifre", + "opc-device-node-pattern": "Cihaz düğümü modeli", + "opc-device-name-pattern": "Cihaz adı deseni", + "modbus-server": "Sunucular / köle", + "modbus-add-server": "Sunucu ekle / köle", + "modbus-add-server-prompt": "Lütfen sunucu / slave ekle", + "modbus-transport": "Taşıma", + "modbus-port-name": "Seri port adı", + "modbus-encoding": "Kodlama", + "modbus-parity": "Parite", + "modbus-baudrate": "Baud hızı", + "modbus-databits": "Veri bitleri", + "modbus-stopbits": "Bitleri durdur", + "modbus-databits-range": "Veri bitleri 7 ila 8 arasında olmalıdır", + "modbus-stopbits-range": "Durma bitleri 1'den 2'ye kadar olmalıdır.", + "modbus-unit-id": "Birim Kimliği", + "modbus-unit-id-range": "Birim numarası 1 ile 247 arasında olmalıdır.", + "modbus-device-name": "Cihaz adı", + "modbus-poll-period": "Anket dönemi (ms)", + "modbus-attributes-poll-period": "Nitelikler yoklama süresi (ms)", + "modbus-timeseries-poll-period": "Timeseries anket süresi (ms)", + "modbus-poll-period-range": "Anket dönemi pozitif değer olmalı", + "modbus-tag": "Etiket", + "modbus-function": "İşlev", + "modbus-register-address": "Kayıt adresi", + "modbus-register-address-range": "Kayıt adresi 0 ile 65535 arasında olmalıdır.", + "modbus-register-bit-index": "Bit endeksi", + "modbus-register-bit-index-range": "Bit endeksi 0 ile 15 arasında olmalıdır", + "modbus-register-count": "Kayıt sayısı", + "modbus-register-count-range": "Kayıt sayısı pozitif bir değer olmalıdır.", + "modbus-byte-order": "Bayt sırası", + "sync": { + "status": "Durum", + "sync": "Senkronizasyon", + "not-sync": "Eşitleme", + "last-sync-time": "Son senkronizasyon zamanı", + "not-available": "Müsait değil" + }, + "export-extensions-configuration": "İhracat uzantıları yapılandırması", + "import-extensions-configuration": "Uzantılarını içe aktarma yapılandırması", + "import-extensions": "Uzantıları içe aktar", + "import-extension": "Uzantı içe aktar", + "export-extension": "İhracat uzantısı", + "file": "Uzantılar dosyası", + "invalid-file-error": "Geçersiz uzantı dosyası" + }, + "fullscreen": { + "expand": "Tam ekran yap", + "exit": "Tam ekrandan çık", + "toggle": "Tam ekran modu aç/kapat", + "fullscreen": "Tam ekran" + }, + "function": { + "function": "Fonksiyon" + }, + "grid": { + "delete-item-title": "Bu öğeyi silmek istediğinizden emin misiniz?", + "delete-item-text": "UYARI: Onayladıktan sonra bu öğe ve ilişkili tüm verileri geri yüklenemez şekilde silinecektir.", + "delete-items-title": "{ count, plural, 1 {1 öğeyi} other {# öğeyi} } silmek istediğinizden emin misiniz?", + "delete-items-action-title": "{ count, plural, 1 {1 öğeyi} other {# öğeyi} } sil", + "delete-items-text": "UYARI: Onayladıktan sonra tüm seçili öğeler ve ilişkili tüm verileri geri yüklenemez şekilde silinecektir.", + "add-item-text": "Yeni öğe ekle", + "no-items-text": "Hiç bir öğe bulunamadı", + "item-details": "Öğe detayları", + "delete-item": "Öğeyi sil", + "delete-items": "Öğeleri sil", + "scroll-to-top": "Üste kaydır" + }, + "help": { + "goto-help-page": "Yardım sayfasına git" + }, + "home": { + "home": "Ana sayfa", + "profile": "Profil", + "logout": "Çıkış", + "menu": "Menü", + "avatar": "Avatar", + "open-user-menu": "Kullanıcı menüsünü aç" + }, + "import": { + "no-file": "Hiçbir dosya seçilmedi", + "drop-file": "Bir JSON dosyası bırakın veya yüklenecek bir dosyayı seçmek için tıklayın." + }, + "item": { + "selected": "Seçildi" + }, + "js-func": { + "no-return-error": "Fonksiyon bir değer dönmeli!", + "return-type-mismatch": "Fonksiyon '{{type}}' türünde bir değer dönmeli!", + "tidy": "Düzenli" + }, + "key-val": { + "key": "Anahtar", + "value": "Değer", + "remove-entry": "Girişi kaldır", + "add-entry": "Giriş ekle", + "no-data": "Giriş yok" + }, + "layout": { + "layout": "Arayüz Düzeni", + "manage": "Arayüz düzenini yönet", + "settings": "Arayüz düzeni ayarları", + "color": "Renk", + "main": "Ana", + "right": "Sağ", + "select": "Hedef düzen seç" + }, + "legend": { + "position": "Lejant konumu", + "show-max": "Maksimum değeri göster", + "show-min": "Minimum değeri göster", + "show-avg": "Ortalama değeri göster", + "show-total": "Toplam değeri göster", + "settings": "Lejant ayarları", + "min": "min", + "max": "maks", + "avg": "ort.", + "total": "toplam" + }, + "login": { + "login": "Giriş Yap", + "request-password-reset": "Parola Sıfırlama İsteği Gönder", + "reset-password": "Parola Sıfırla", + "create-password": "Parola Oluştur", + "passwords-mismatch-error": "Girilen parolalar eşleşmeli!", + "password-again": "Parola tekrarı", + "sign-in": "Lütfen girişi yapın", + "username": "Kullanıcı adı (e-posta)", + "remember-me": "Beni hatırla", + "forgot-password": "Parolamı unuttum", + "password-reset": "Parola sıfırla", + "new-password": "Yeni parola", + "new-password-again": "Yeni parola tekrarı", + "password-link-sent-message": "Parola sıfırlama e-postası başarıyla gönderildi!", + "email": "E-posta", + "login-with": "{{name}} ile Giriş Yap", + "or": "ya da" + }, + "position": { + "top": "Üst", + "bottom": "Alt", + "left": "Sol", + "right": "Sağ" + }, + "profile": { + "profile": "Profil", + "last-login-time": "Son giriş tarihi", + "change-password": "Şifre değiştir", + "current-password": "Şimdiki şifre" + }, + "relation": { + "relations": "İlişkiler", + "direction": "Yönelim", + "search-direction": { + "FROM": "KAYNAK", + "TO": "HEDEF" + }, + "direction-type": { + "FROM": "kaynak", + "TO": "hedef" + }, + "from-relations": "Giden ilişkiler", + "to-relations": "Gelen ilişkiler", + "selected-relations": "{ count, plural, 1 {1 ilişki} other {# ilişki} } seçildi", + "type": "Tür", + "to-entity-type": "Hedef Öğe Türü", + "to-entity-name": "Hedef Öğe Adı", + "from-entity-type": "Kaynak Öğe Türü", + "from-entity-name": "Kaynak Öğe Adı", + "to-entity": "Hedef Öğe", + "from-entity": "Kaynak Öğe", + "delete": "İlişkiyi sil", + "relation-type": "İlişki türü", + "relation-type-required": "İlişki türü gerekli.", + "any-relation-type": "Her hangi bir tür", + "add": "İlişki ekle", + "edit": "İlişki düzenle", + "delete-to-relation-title": "'{{entityName}}' öğesine olan ilişkiyi silmek istediğinize emin misiniz?", + "delete-to-relation-text": "UYARI: Onaylandıktan sonra '{{entityName}}' öğesinin şimdiki öğeyle olan ilişkisi sona erecektir.", + "delete-to-relations-title": "{ count, plural, 1 {1 ilişkiyi} other {# ilişkiyi} } silmek istediğinize emin misiniz?", + "delete-to-relations-text": "UYARI: Onaylandıktan sonra tüm seçili ilişkiler kaldırılacaktır ve ilgili öğelerin şimdiki öğeyle ilişkisi sona erecektir.", + "delete-from-relation-title": "'{{entityName}}' öğesinden ilişkiyi silmek istediğinize emin misiniz?", + "delete-from-relation-text": "UYARI: Onaylandıktan sonra şimdiki öğenin '{{entityName}}' öğesiyle ilişkisi sonlandırılacaktır.", + "delete-from-relations-title": "{ count, plural, 1 {1 ilişkiyi} other {# ilişkiyi} } silmek istediğinize emin misiniz?", + "delete-from-relations-text": "UYARI: Onaylandıktan sonra tüm seçili ilişkiler kaldırılacak ve şimdiki öğenin ilgili tüm öğelerle ilişkisi sona erecektir.", + "remove-relation-filter": "İlişki filtresini kaldır", + "add-relation-filter": "İlişkisi ekle", + "any-relation": "Herhangi bir ilişki", + "relation-filters": "İlişki filtreleri", + "additional-info": "Ek bilgi (JSON)", + "invalid-additional-info": "Ek bilgi JSON'ı parse edilip işlenemedi." + }, + "rulechain": { + "rulechain": "Kural", + "rulechains": "Kurallar", + "root": "Kök", + "delete": "Kuralı sil", + "name": "İsim", + "name-required": "İsim gerekli.", + "description": "Açıklama", + "add": "Kural Ekle", + "set-root": "Kural zincirinin kökü yap", + "set-root-rulechain-title": "Kural zincirini {{ruleChainName}} root? Yapmak istediğinizden emin misiniz?", + "set-root-rulechain-text": "Onaydan sonra kural zinciri kökleşecek ve gelen tüm iletilerle ilgilenecek.", + "delete-rulechain-title": "'{{ruleName}}' isimli kuralı silmek istediğinize emin misiniz?", + "delete-rulechain-text": "UYARI: Onaylandıktan sonra kural ve ilişkili tüm veriler geri yüklenemez şekilde silinecektir.", + "delete-rulechains-title": "{ count, plural, 1 {1 kuralı} other {# kuralı} } sikmek istediğinize emin misiniz?", + "delete-rulechains-action-title": "{ count, plural, 1 {1 kuralı} other {# kuralı} } sil", + "delete-rulechains-text": "UYARI: Onaylandıktan sonra seçili tüm kurallar ve ilişkili tüm veriler geri yüklenemez şekilde silinecektir.", + "add-rulechain-text": "Yeni kural ekle", + "no-rulechains-text": "Hiçbir kural bulunamadı", + "rulechain-details": "Kural detayları", + "details": "Detaylar", + "events": "Olaylar", + "system": "Sistem", + "import": "Kuralı içe aktar", + "export": "Kuralı dışa aktar", + "export-failed-error": "Kural dışa aktarılamadı: {{error}}", + "create-new-rule": "Yeni kural oluştur", + "rulechain-file": "Kural dosyası", + "invalid-rulechain-file-error": "Kural içe aktarılamadı: Geçersiz kural veri yapısı.", + "copyId": "Kural kimliğini kopyala", + "idCopiedMessage": "Kural kimliği panoya kopyalandı", + "select-rulechain": "Kural seç", + "no-rulechains-matching": "'{{entity}}' ile eşleşen kural bulunamadı.", + "rulechain-required": "Kural gerekli", + "management": "Kural yönetimi", + "debug-mode": "Hata ayıklama modu", + "search": "Kural Ara", + "selected-rulechains": "{ count, plural, 1 {1 kural} other {# kural} } seçildi", + "open-rulechain": "Kuralı Aç" + }, + "rulenode": { + "details": "Ayrıntılar", + "events": "Etkinlikler", + "search": "Arama düğümleri", + "open-node-library": "Düğüm kütüphanesini aç", + "add": "Kural düğümü ekle", + "name": "Ad", + "name-required": "İsim gerekli.", + "type": "Tür", + "description": "Açıklama", + "delete": "Kural düğümünü sil", + "select-all-objects": "Tüm düğümleri ve bağlantıları seç", + "deselect-all-objects": "Tüm düğümlerin ve bağlantıların seçimini kaldırın", + "delete-selected-objects": "Seçilen düğümleri ve bağlantıları sil", + "delete-selected": "Silme seçildi", + "select-all": "Hepsini seç", + "copy-selected": "Seçilenleri kopyala", + "deselect-all": "Hiçbirini seçme", + "rulenode-details": "Kural düğümü ayrıntıları", + "debug-mode": "Hata ayıklama modu", + "configuration": "Yapılandırma", + "link": "Bağlantı", + "link-details": "Kural düğüm bağlantı detayları", + "add-link": "Link ekle", + "link-label": "Bağlantı etiketi", + "link-label-required": "Bağlantı etiketi gerekli.", + "custom-link-label": "Özel bağlantı etiketi", + "custom-link-label-required": "Özel bağlantı etiketi gerekli.", + "link-labels": "Link etiketleri", + "link-labels-required": "Link etiketleri gerekli.", + "no-link-labels-found": "Bağlantı etiketi bulunamadı", + "no-link-label-matching": "{{label}} bulunamadı. ", + "create-new-link-label": "Yeni bir tane oluştur!", + "type-filter": "Filtre", + "type-filter-details": "Gelen iletileri yapılandırılmış koşullara göre filtrele", + "type-enrichment": "Zenginleştirme", + "type-enrichment-details": "Mesaj Meta Verilerine ek bilgi", + "type-transformation": "Dönüşüm", + "type-transformation-details": "Mesaj yükünü ve Meta Verileri Değiştir", + "type-action": "Aksiyon", + "type-action-details": "Özel eylem gerçekleştir", + "type-external": "Dış", + "type-external-details": "Dış sistemle etkileşir", + "type-rule-chain": "Kural Zinciri", + "type-rule-chain-details": "Belirtilen Kural Zincirine gelen mesajları ilet", + "type-input": "Giriş", + "type-input-details": "Kural Zinciri'nin mantıksal girdisi, bir sonraki ilgili Kural Düğümüne gelen iletileri iletme", + "type-unknown": "Bilinmeyen", + "type-unknown-details": "Çözümlenmemiş Kural Düğümü", + "directive-is-not-loaded": "Tanımlanmış yapılandırma yönergesi {{directiveName}} 'mevcut değil. ", + "ui-resources-load-error": "Yapılandırma kullanıcı arayüzü kaynakları yüklenemedi.", + "invalid-target-rulechain": "Hedef kural zinciri çözülemiyor!", + "test-script-function": "Test komut dosyası işlevi", + "message": "Mesaj", + "message-type": "Mesaj tipi", + "select-message-type": "Mesaj tipini seç", + "message-type-required": "Mesaj türü gerekli", + "metadata": "Meta veri", + "metadata-required": "Meta veri girişleri boş bırakılamaz.", + "output": "Çıktı", + "test": "Ölçek", + "help": "Yardım et" + }, + "tenant": { + "tenant": "Tenant", + "tenants": "Tenantlar", + "management": "Tenant yönetimi", + "add": "Tenant Ekle", + "admins": "Adminler", + "manage-tenant-admins": "Tenant Adminlerini Yönet", + "delete": "Tenant sil", + "add-tenant-text": "Yeni tenant ekle", + "no-tenants-text": "Hiçbir tenant bulunamadı", + "tenant-details": "Tenant detayları", + "delete-tenant-title": "'{{tenantTitle}}' isimli tenantı silmek istediğinize emin misiniz?", + "delete-tenant-text": "UYARI: Onaylandıktan sonra tenant ve ilişkili tüm veriler geri yüklenemez şekilde silinecektir.", + "delete-tenants-title": "{ count, plural, 1 {1 tenantı} other {# tenantı} } silmek istediğinize emin misiniz?", + "delete-tenants-action-title": "{ count, plural, 1 {1 tenantı} other {# tenantı} } sil", + "delete-tenants-text": "UYARI: Onaylandıktan sonra seçili tüm tenantlar ve ilişkili tüm veriler geri yüklenemez şekilde silinecektir", + "title": "Başlık", + "title-required": "Başlık gerekli.", + "description": "Açıklama", + "details": "Detaylar", + "events": "Olaylar", + "copyId": "Tenant kimliğini kopyala", + "idCopiedMessage": "Tenant kimliği panoya kopyalandı", + "select-tenant": "Tenant seç", + "no-tenants-matching": "'{{entity}}' ile eşleşen tenant bulunamadı.", + "tenant-required": "Tenant gerekli", + "search": "Tenantları ara", + "selected-tenants": "{ count, plural, 1 {1 tenant} other {# tenant} } seçildi", + "isolated-tb-core": "ThingsBoard soyutlanmış merkezi konteynerda işlensin", + "isolated-tb-rule-engine": "ThingsBoard soyutlanmış kural yönetimi konteynerda işlensin", + "isolated-tb-core-details": "Her soyutlanmış tenant ayrı bir mikro servis gerektirir", + "isolated-tb-rule-engine-details": "Her soyutlanmış tenant ayrı bir mikro servis gerektirir" + }, + "timeinterval": { + "seconds-interval": "{ seconds, plural, 1 {1 saniye} other {# saniye} }", + "minutes-interval": "{ minutes, plural, 1 {1 dakika} other {# dakika} }", + "hours-interval": "{ hours, plural, 1 {1 saat} other {# saat} }", + "days-interval": "{ days, plural, 1 {1 gün} other {# gün} }", + "days": "Gün", + "hours": "Saat", + "minutes": "Dakika", + "seconds": "Saniye", + "advanced": "İleri düzey" + }, + "timewindow": { + "days": "{ days, plural, 1 { gün } other {# gün } }", + "hours": "{ hours, plural, 0 { saat } 1 {1 saat } other {# saat } }", + "minutes": "{ minutes, plural, 0 { dakika } 1 {1 dakika } other {# dakika } }", + "seconds": "{ seconds, plural, 0 { saniye } 1 {1 saniye } other {# saniye } }", + "realtime": "Gerçek zaman", + "history": "Tarih", + "last-prefix": "son", + "period": "{{ startTime }}'dan {{ endTime }}'a kadar", + "edit": "Zaman aralığını düzenle", + "date-range": "Tarih aralığı", + "last": "Son", + "time-period": "Zaman periyodu" + }, + "user": { + "user": "Kullanıcı", + "users": "Kullanıcılar", + "customer-users": "Kullanıcılar", + "tenant-admins": "Tenant Adminleri", + "sys-admin": "Sistem yöneticisi", + "tenant-admin": "Tenant yöneticisi", + "customer": "Kullanıcı Grubu", + "anonymous": "Anonim", + "add": "Kullanıcı ekle", + "delete": "Kullanıcı sil", + "add-user-text": "Yeni kullanıcı ekle", + "no-users-text": "Hiçbir kullanıcı bulunamadı", + "user-details": "Kullanıcı detayları", + "delete-user-title": "'{{userEmail}}' kullanıcısını silmek istediğinize emin misiniz?", + "delete-user-text": "UYARI: Onaylandıktan sonra kullanıcı ve ilişkili tüm verileri geri yüklenemez şekilde silinecektir.", + "delete-users-title": "{ count, plural, 1 {1 kullanıcıyı} other {# kullanıcıyı} } sikmek istediğinize emin misiniz?", + "delete-users-action-title": "{ count, plural, 1 {1 kullancıyı} other {# kullanıcıyı} } sil", + "delete-users-text": "UYARI: Onaylandıktan sonra kullanıcı ve ilişkili tüm verileri geri yüklenemez şekilde silinecektir.", + "activation-email-sent-message": "Etkinleştirme e-postası başarılı bir şekilde gönderildi!", + "resend-activation": "Etkinleştirme e-postasını yeniden gönder", + "email": "E-posta", + "email-required": "E-posta gerekli.", + "invalid-email-format": "Geçersiz e-posta formatı.", + "first-name": "Ad", + "last-name": "Soyad", + "description": "Açıklama", + "default-dashboard": "Varsayılan kontrol paneli", + "always-fullscreen": "Her zaman tam ekran", + "select-user": "Kullanıcı se.", + "no-users-matching": "'{{entity}}' ile eşleşen kullanıcı bulunamadı.", + "user-required": "Kullanıcı gerekli", + "activation-method": "Etkinleştirme yöntemi", + "display-activation-link": "Etkinleştirme bağlantısını görüntüle", + "send-activation-mail": "Etkinleştirme e-postası gönder", + "activation-link": "Kullanıcı hesabını etkinleştirme bağlantısı", + "activation-link-text": "Kullanıcı hesabını etkinleştirmek için bağlantıyı kullanın:", + "copy-activation-link": "Etkinleştirme bağlantısını kopyala", + "activation-link-copied-message": "Kullanıcı hesabı etkinleştirme bağlantısı panoya kopyalandı", + "details": "Ayrıntılar", + "login-as-tenant-admin": "Tenant Yönetici Girişi", + "login-as-customer-user": "Kullanıcı olarak giriş yap" + }, + "value": { + "type": "Değer tğrğ", + "string": "String", + "string-value": "String değeri", + "integer": "Integer", + "integer-value": "Integer değeri", + "invalid-integer-value": "Geçersiz integer değeri", + "double": "Double", + "double-value": "Double değeri", + "boolean": "Boolean", + "boolean-value": "Boolean değeri", + "false": "Yanlış", + "true": "Doğru", + "long": "Uzun" + }, + "widget": { + "widget-library": "Gösterge Kütüphanesi", + "widget-bundle": "Gösterge Demeti", + "select-widgets-bundle": "Gösterge demeti seç", + "management": "Gösterge yönetimi", + "editor": "Gösterge düzenleyici", + "widget-type-not-found": "Gösterge yapılandırması yüklenemedi.
    Muhtemelen ilgili\n gösterge türü kaldırılmış.", + "widget-type-load-error": "Gösterge şu sebeplerden dolayı yüklenemedi:", + "remove": "Göstergeyi kaldır", + "edit": "Göstergeyi düzenle", + "remove-widget-title": "'{{widgetTitle}}' isimli göstermeyi kaldırmak istediğinizden emin misiniz?", + "remove-widget-text": "UYARI: Onaylandıktan sonra gösterge ve tüm ilişkili verileri geri yüklenemez şekilde silinecek.", + "timeseries": "Zaman serisi", + "search-data": "Arama verileri", + "no-data-found": "Veri bulunamadı", + "latest-values": "Son değerler", + "rpc": "Kontrol göstergesi", + "alarm": "Alarm göstergesi", + "static": "Statik gösterge", + "select-widget-type": "Gösterge türü seç", + "missing-widget-title-error": "Gösterge başlığı belirtilmelidir!", + "widget-saved": "Gösterge kaydedildi", + "unable-to-save-widget-error": "Gösterge kaydedilemedi! Göstergede hatalar mevcut!", + "save": "Göstergeyi kaydet", + "saveAs": "Göstergeyi farklı kaydet", + "save-widget-type-as": "Gösterge türünü farklı kaydet", + "save-widget-type-as-text": "Lütfen gösterge başlığı girin veya hedef gösterge demeti seçin", + "toggle-fullscreen": "Tam ekran aç/kapat", + "run": "Göstergeyi çalıştır", + "title": "Gösterge başlığı", + "title-required": "Gösterge başlığı gerekli.", + "type": "Gösterge türü", + "resources": "Kaynaklar", + "resource-url": "JavaScript / CSS URL", + "remove-resource": "Kaynağı kaldır", + "add-resource": "Kaynak ekle", + "html": "HTML", + "tidy": "Tertiple", + "css": "CSS", + "settings-schema": "Ayarlar şeması", + "datakey-settings-schema": "Veri anahtarı ayarları şeması", + "javascript": "Javascript", + "remove-widget-type-title": "'{{widgetName}}' isimli gösterge türünü kaldırmak istediğinizden emin misiniz?", + "remove-widget-type-text": "UYARI: Onaylandıktan sonra, gösterge türü ve ilgili tüm veriler geri yüklenemez şekilde silinecektir.", + "remove-widget-type": "Gösterge türünü kaldır", + "add-widget-type": "Yeni gösterge türü ekle", + "widget-type-load-failed-error": "Gösterge türü yüklenemedi!", + "widget-template-load-failed-error": "Gösterge şablonu yüklenemedi!", + "add": "Gösterge ekle", + "undo": "Gösterge değişikliklerini geri al", + "export": "Göstergeyi dışa aktar" + }, + "widget-action": { + "header-button": "Gösterge başlık butonu", + "open-dashboard-state": "Yeni kontrol paneli durumunua git", + "update-dashboard-state": "Kontrol paneli durumunu güncelle", + "open-dashboard": "Diğer kontrol paneline git", + "custom": "Özel eylem", + "target-dashboard-state": "Hedef kontrol paneli durumu", + "target-dashboard-state-required": "Hedef kontrol paneli durumu gerekli", + "set-entity-from-widget": "Göstergeden öğe belirle", + "target-dashboard": "Hedef kontrol paneli", + "open-right-layout": "Sağdaki kontrol paneli arayüz düzenini aç(mobil görünüm)" + }, + "widgets-bundle": { + "current": "Şimdiki demet", + "widgets-bundles": "Gösterge Demetleri", + "add": "Gösterge Demeti Ekle", + "delete": "Gösterge demeti sil", + "title": "Başlık", + "title-required": "Başlık gerekli.", + "add-widgets-bundle-text": "Yeni gösterge demeti ekle", + "no-widgets-bundles-text": "Hiçbir gösterge demeti bulunamadı", + "empty": "Gösterge demeti boş", + "details": "Detaylar", + "widgets-bundle-details": "Gösterge demeti detayları", + "delete-widgets-bundle-title": "'{{widgetsBundleTitle}}' isimli gösterge demetini silmek istediğinize emin misiniz?", + "delete-widgets-bundle-text": "UYARI: Onaylandıktan sonra gösterge demeti ve ilişkili tüm veriler geri yüklenemez şekilde silinecektir.", + "delete-widgets-bundles-title": "{ count, plural, 1 {1 gösterge demetini} other {# gösterge demetini} } silmek istediğinize emin misiniz?", + "delete-widgets-bundles-action-title": "{ count, plural, 1 {1 gösterge demetini} other {# gösterge demetini} } sil", + "delete-widgets-bundles-text": "UYARI: Onaylandıktan sonra seçili tüm gösterge demetleri ve ilişkili tüm veriler geri yüklenemez şekilde silinecektir.", + "no-widgets-bundles-matching": "'{{widgetsBundle}}' ile eşleşen gösterge demeti bulunamadı.", + "widgets-bundle-required": "Gösterge demeti gerekli.", + "system": "Sistem", + "import": "Gösterge demetini içe aktar", + "export": "Gösterge demetini dışa aktar", + "export-failed-error": "Gösterge demetini dışa aktaramadı: {{error}}", + "create-new-widgets-bundle": "Yeni gösterge demeti oluştur", + "widgets-bundle-file": "Gösterge demeti dosyası", + "invalid-widgets-bundle-file-error": "Gösterge demeti içe aktarılamadı: Geçersiz gösterge demeti veri yapısı.", + "search": "Gösterge demeti ara", + "selected-widgets-bundles": "{ count, plural, 1 {1 gösterge demeti} other {# gösterge demeti} } seçildi", + "open-widgets-bundle": "Gösterge demetlerini aç" + }, + "widget-config": { + "data": "Veri", + "settings": "Ayarlar", + "advanced": "İleri düzey", + "title": "Başlık", + "general-settings": "Genel ayarlar", + "display-title": "Başlığı göster", + "drop-shadow": "Gölge", + "enable-fullscreen": "Tam ekranı etkinleştir", + "background-color": "Arka plan rengi", + "text-color": "Yazı rengi", + "padding": "İç aralık (Padding)", + "margin": "Dış aralık (Margin)", + "widget-style": "Gösterge stili", + "title-style": "Başlık stili", + "mobile-mode-settings": "Mobil mod ayarları", + "order": "Sıra", + "height": "Yükseklik", + "units": "Değerin yanında göstermek için özel simge", + "decimals": "Noktadan sonraki basamak sayısı", + "timewindow": "Zaman aralığı", + "use-dashboard-timewindow": "Kontrol paneli zaman aralığı kullan", + "display-legend": "Lejant göster", + "datasources": "Veri kaynakları", + "maximum-datasources": "En fazla { count, plural, 1 {1 veri kaynağı kullanılabilir.} other {# veri kaynağı kullanılabilir} }", + "datasource-type": "Tür", + "datasource-parameters": "Parametreler", + "remove-datasource": "Veri kaynağını kaldır", + "add-datasource": "Veri kaynağı ekle", + "target-device": "Hedef aygıt", + "alarm-source": "Alarm kaynağı", + "actions": "Eylemler", + "action": "Eylem", + "add-action": "Eylem ekle", + "search-actions": "Eylem ara", + "action-source": "Eylem kaynağı", + "action-source-required": "Eylem kaynağı gerekli.", + "action-name": "İsim", + "action-name-required": "Eylem ismi gerekli.", + "action-name-not-unique": "Aynı ada sahip başka bir işlem zaten var.
    Eylem adı, aynı eylem kaynağı içinde emsalsiz olmalıdır.", + "action-icon": "İkon", + "action-type": "Tür", + "action-type-required": "Eylem türü gerekli.", + "edit-action": "Eylemi düzenle", + "delete-action": "Eylemi sil", + "delete-action-title": "Gösterge eylemini sil", + "delete-action-text": "'{{actionName}}' isimli gösterge eylemini silmek istediğinizden emin misiniz?" + }, + "widget-type": { + "import": "Gösterge türünü içer aktar", + "export": "Gösterge türünü dışa aktar", + "export-failed-error": "Gösterge türü dışa aktarılamadı: {{error}}", + "create-new-widget-type": "Yeni gösterge türü oluştur", + "widget-type-file": "Gösterge türü dosyası", + "invalid-widget-type-file-error": "Gösterge türü içe aktarılamadı: Geçersiz gösterge türü veri yapısı." + }, + "widgets": { + "date-range-navigator": { + "localizationMap": { + "Sun": "Paz", + "Mon": "Pzt", + "Tue": "Sal", + "Wed": "Çar", + "Thu": "Per", + "Fri": "Cum", + "Sat": "Cmt", + "Jan": "Oca", + "Feb": "Şub", + "Mar": "Mar", + "Apr": "Nis", + "May": "May", + "Jun": "Haz", + "Jul": "Tem", + "Aug": "Ağu", + "Sep": "Eyl", + "Oct": "Eki", + "Nov": "Kas", + "Dec": "Ara", + "January": "Ocak", + "February": "Şubat", + "March": "Mart", + "April": "Nisan", + "June": "Haziran", + "July": "Temmuz", + "August": "Ağustos", + "September": "Eylül", + "October": "Ekim", + "November": "Kasım", + "December": "Aralık", + "Custom Date Range": "Özel Tarih Aralığı", + "Date Range Template": "Tarih Aralığı Şablonu", + "Today": "Bugün", + "Yesterday": "Dün", + "This Week": "Bu hafta", + "Last Week": "Geçen hafta", + "This Month": "Bu ay", + "Last Month": "Geçen ay", + "Year": "Yıl", + "This Year": "Bu yıl", + "Last Year": "Geçen yıl", + "Date picker": "Tarih seçici", + "Hour": "Saat", + "Day": "Gün", + "Week": "Hafta", + "2 weeks": "2 Hafta", + "Month": "Ay", + "3 months": "3 Ay", + "6 months": "6 Ay", + "Custom interval": "Özel aralık", + "Interval": "Aralık", + "Step size": "Adım boyutu", + "Ok": "Ok" + } + } + }, + "icon": { + "icon": "İkon", + "select-icon": "İkon seç", + "material-icons": "Material konları", + "show-all": "Tüm ikonları göster" + }, + "custom": { + "widget-action": { + "action-cell-button": "Eylem hücre butonu", + "row-click": "Satır tıklama eylemi", + "polygon-click": "Satır tıklama eylemi", + "marker-click": "Çokgen tıklama eylemi", + "tooltip-tag-action": "İpucu etiket eylemi" + } + }, + "language": { + "language": "Dil" + } +} diff --git a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json new file mode 100644 index 0000000000..670aa58fc5 --- /dev/null +++ b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json @@ -0,0 +1,2408 @@ +{ + "access": { + "unauthorized": "Неавторизований", + "unauthorized-access": "Неавторизований доступ", + "unauthorized-access-text": "Щоб отримати доступ до цього ресурсу, потрібно ввійти в систему!", + "access-forbidden": "Доступ заборонено", + "access-forbidden-text": "Недостатньо прав для доступу!
    Спробуйте увійти як інший користувач, якщо ви все ще хочете отримати доступ до цього ресурсу.", + "refresh-token-expired": "Дані про сесію застарілі", + "refresh-token-failed": "Не вдається відновити сеанс" + }, + "action": { + "activate": "Активувати", + "suspend": "Призупинити", + "save": "Зберегти", + "saveAs": "Зберегти як", + "cancel": "Скасувати", + "ok": "OK", + "delete": "Видалити", + "add": "Додати", + "yes": "Так", + "no": "Ні", + "update": "Оновити", + "remove": "Видалити", + "search": "Пошук", + "clear-search": "Очистити пошук", + "assign": "Надати", + "unassign": "Позбавити", + "share": "Поділитися", + "make-private": "Зробити приватним", + "apply": "Застосувати", + "apply-changes": "Застосувати зміни", + "edit-mode": "Режим редагування", + "enter-edit-mode": "Ввійти в режим редагування", + "decline-changes": "Відхилити зміни", + "close": "Закрити", + "back": "Назад", + "run": "Запустити", + "sign-in": "Увійти!", + "edit": "Редагувати", + "view": "Переглянути", + "create": "Створити", + "drag": "Перетягнути", + "refresh": "Оновити", + "undo": "Скасувати", + "copy": "Скопіювати", + "paste": "Вставити", + "copy-reference": "Копіювати посилання", + "paste-reference": "Вставити посилання", + "import": "Імпортувати", + "export": "Експортувати", + "share-via": "Поділитися через {{provider}}", + "continue": "Продовжити", + "discard-changes": "Скасувати зміни", + "move": "Перемістити", + "select": "Вибрати" + }, + "aggregation": { + "aggregation": "Агрегація", + "function": "Функція агрегації даних", + "limit": "Максимальні значення", + "group-interval": "Інтервал групування", + "min": "Мінімальний", + "max": "Максимальний", + "avg": "Середній", + "sum": "Сума", + "count": "Рахувати", + "none": "Відсутня" + }, + "admin": { + "general": "Загальне", + "general-settings": "Загальні налаштування", + "outgoing-mail": "Поштовий сервер", + "outgoing-mail-settings": "Налаштування сервера вихідної пошти", + "system-settings": "Налаштування системи", + "test-mail-sent": "Тестовий лист успішно відправлено!", + "base-url": "Базова URL-адреса", + "base-url-required": "Базова URL-адреса обов'язкова.", + "mail-from": "Електронна адреса", + "mail-from-required": "Електронна адреса обов'язкова.", + "smtp-protocol": "Протокол SMTP", + "smtp-host": "Хост SMTP", + "smtp-host-required": "Хост SMTP обов'язковий.", + "smtp-port": "SMTP-порт", + "smtp-port-required": "Ви повинні надати SMTP-порт.", + "smtp-port-invalid": "Це не схоже на дійсний SMTP-порт.", + "timeout-msec": "Час очікування (msec)", + "timeout-required": "Необхідно задати час очікування.", + "timeout-invalid": "Це не схоже на правильний час очікування.", + "enable-tls": "Увімкнути TLS", + "tls-version" : "Версія TLS", + "send-test-mail": "Надіслати тестове повідомлення", + "use-system-mail-settings": "Використовувати параметри системного поштового сервера", + "mail-templates": "Шаблони електронної пошти", + "mail-template-settings": "Налаштування шаблонів електронної пошти", + "use-system-mail-template-settings": "Використовувати шаблони системної електронної пошти", + "mail-template": { + "mail-template": "Шаблон електронної пошти", + "test": "Тестове повідомлення", + "activation": "Повідомлення про активацію рахунку", + "account-activated": "Обліковий запис активовано", + "reset-password": "Відновити повідомлення пароля", + "password-was-reset": "Пароль було надіслано повідомленням" + }, + "mail-subject": "Тема повідомлення", + "mail-body": "Вміст повідомлення", + "security-settings": "Налаштування безпеки", + "password-policy": "Політика щодо паролів", + "minimum-password-length": "Мінімальна довжина пароля", + "minimum-password-length-required": "Потрібна мінімальна довжина пароля", + "minimum-password-length-range": "Мінімальна довжина пароля повинна бути в межах від 5 до 50", + "minimum-uppercase-letters": "Мінімальна кількість великих літер", + "minimum-uppercase-letters-range": "Мінімальна кількість великих літер не може бути негативною", + "minimum-lowercase-letters": "Мінімальна кількість малих літер", + "minimum-lowercase-letters-range": "Мінімальна кількість малих літер не може бути негативною", + "minimum-digits": "Мінімальна кількість цифр", + "minimum-digits-range": "Мінімальна кількість цифр не може бути негативною", + "minimum-special-characters": "Мінімальна кількість спеціальних символів", + "minimum-special-characters-range": "Мінімальна кількість спеціальних символів не може бути негативною", + "password-expiration-period-days": "Термін дії пароля в днях", + "password-expiration-period-days-range": "Термін дії пароля в днях не може бути негативним", + "password-reuse-frequency-days": "Частота повторного використання пароля в днях", + "password-reuse-frequency-days-range": "Частота повторного використання пароля в днях не може бути негативною", + "general-policy": "Загальна політика", + "max-failed-login-attempts": "Максимальна кількість невдалих спроб входу, перш ніж обліковий запис заблоковано", + "minimum-max-failed-login-attempts-range": "Максимальна кількість невдалих спроб входу не може бути негативною", + "user-lockout-notification-email": "У разі блокування облікового запису користувача, надішліть сповіщення на електронну пошту" + }, + "alarm": { + "alarm": "Сигнал тривоги", + "alarms": "Сигнали тривоги", + "select-alarm": "Вибрати сигнал тривоги", + "no-alarms-matching": "Сигналів тривоги '{{entity}}' не знайдено.", + "alarm-required": "Сигнал тривоги необхідний", + "alarm-status": "Статус сигналу тривоги", + "search-status": { + "ANY": "Будь які", + "ACTIVE": "Активні", + "CLEARED": "Неактивні", + "ACK": "Прийняті", + "UNACK": "Неприйняті" + }, + "display-status": { + "ACTIVE_UNACK": "Активні та неприйняті", + "ACTIVE_ACK": "Активні та прийняті", + "CLEARED_UNACK": "Неактивні та неприйняті", + "CLEARED_ACK": "Неактивні та прийняті" + }, + "no-alarms-prompt": "Сигналів тривоги не знайдено", + "created-time": "Час створення", + "type": "Тип", + "severity": "Серйозність", + "originator": "Ініціатор", + "originator-type": "Тип ініціатору", + "details": "Деталі", + "status": "Статус", + "alarm-details": "Деталі сигналу тривоги", + "start-time": "Початок", + "end-time": "Кінець", + "ack-time": "Час прийняття", + "clear-time": "Час деактивації", + "severity-critical": "Критичні", + "severity-major": "Важливі", + "severity-minor": "Неважливі", + "severity-warning": "Попередження", + "severity-indeterminate": "Невизначені", + "acknowledge": "Прийняти", + "clear": "Деактивувати", + "search": "Шукати сигнали тривоги", + "selected-alarms": "{ count, plural, 1 {1 сигнал тривоги} other {# сигнали тривоги} } вибрані", + "no-data": "Немає даних для відображення", + "polling-interval": "Інтервал опитування (сек)", + "polling-interval-required": "Необхідно задати інтервал опитування.", + "min-polling-interval-message": "Дозволяється щонайменше 1 секунда інтервалу очікування.", + "aknowledge-alarms-title": "Підтвердити { count, plural, 1 {1 сигнал тривоги} other {# сигнали тривоги} }", + "aknowledge-alarms-text": "Ви впевнені, що хочете підтвердити { count, plural, 1 {1 сигнал тривоги} other {# сигнали тривоги} }?", + "aknowledge-alarm-title": "Підтвердити сигнал тривоги", + "aknowledge-alarm-text": "Ви впевнені, що хочете підтвердити сигнал тривоги?", + "clear-alarms-title": "Деактивувати { count, plural, 1 {1 сигнал тривоги} other {# сигнали тривоги} }", + "clear-alarms-text": "Ви впевнені, що хочете деактивувати { count, plural, 1 {1 сигнал тривоги} other {# сигнали тривоги} }?", + "clear-alarm-title": "Деактивувати сигнал тривоги", + "clear-alarm-text": "Ви впевнені, що хочете деактивувати сигнал тривоги?", + "alarm-status-filter": "Фільтр статусу сигналу тривоги", + "max-count-load": "Максимальна кількість сигналів тривоги для завантаження (0 - необмежено)", + "max-count-load-required": "Необхідно задати максимальну кількість сигналів тривоги для завантаження.", + "max-count-load-error-min": "Мінімальне значення 0.", + "fetch-size": "Розмір пакету для завантаження", + "fetch-size-required": "Необхідно задати розмір пакету для завантаження.", + "fetch-size-error-min": "Мінімальне значення 10." + }, + "alias": { + "add": "Додати псевдонім ", + "edit": "Редагувати псевдонім", + "name": "Ім'я", + "name-required": "Необхідно вказати псевдонім", + "duplicate-alias": "Псевдонім з такою назвою вже існує.", + "filter-type-single-entity": "Єдина сутність", + "filter-type-entity-group": "Група сутностей", + "filter-type-entity-list": "Список сутностей", + "filter-type-entity-name": "Назва сутності", + "filter-type-entity-group-list": "Список груп сутностей", + "filter-type-entity-group-name": "Назва групи сутностей", + "filter-type-state-entity": "Сутність з стану панелі пристроїв", + "filter-type-state-entity-description": "Сутність, взята з параметрів стану панелі пристроїв", + "filter-type-asset-type": "Тип активу", + "filter-type-asset-type-description": "Тип активів '{{assetType}}'", + "filter-type-asset-type-and-name-description": "Тип активів '{{assetType}}' і ім'я, що починаються з '{{prefix}}'", + "filter-type-device-type": "Тип пристрою", + "filter-type-device-type-description": "Тип пристроїв '{{deviceType}}'", + "filter-type-device-type-and-name-description": "Тип пристроїв '{{deviceType}}' і ім'я, що починаються з '{{prefix}}'", + "filter-type-entity-view-type": "Тип перегляду сутності", + "filter-type-entity-view-type-description": "Перегляд сутності з типом '{{entityView}}'", + "filter-type-entity-view-type-and-name-description": "Перегляд сутності з типом'{{entityView}}' і іменем, що починаються з '{{prefix}}'", + "filter-type-relations-query": "Запит відносин", + "filter-type-relations-query-description": "{{entities}}, які мають {{relationType}} відношення {{direction}} {{rootEntity}}", + "filter-type-asset-search-query": "Запит пошуку активу", + "filter-type-asset-search-query-description": "Активи з типами {{assetTypes}}, які мають {{relationType}} відношення {{direction}} {{rootEntity}}", + "filter-type-device-search-query": "Запит пошуку пристрою", + "filter-type-device-search-query-description": "Пристрої з типами {{deviceTypes}}, які мають {{relationType}} відношення {{direction}} {{rootEntity}}", + "filter-type-entity-view-search-query": "Запит пошуку переглядів сутностей", + "filter-type-entity-view-search-query-description": "Перегляд сутності з типами {{entityViewTypes}}, які мають {{relationType}} відношення {{direction}} {{rootEntity}}", + "entity-filter": "Фільтр сутності", + "resolve-multiple": "Як декілька сутностей", + "filter-type": "Тип фільтра", + "filter-type-required": "Необхідно вказати тип фільтра.", + "entity-filter-no-entity-matched": "Не знайдено жодних сутностей, які відповідають вказаному фільтру.", + "no-entity-filter-specified": "Фільтр обїектів не вказано", + "root-state-entity": "Використовувати сутінсть стану як кореневу", + "last-level-relation": "Використовувати лише відношення останнього рівня", + "group-state-entity": "Використовувати групу сутностей стану як кореневу", + "root-entity": "Коренева сутність", + "state-entity-parameter-name": "Параметр сутності стану", + "default-state-entity": "Сутність стану за замовчуванням", + "default-state-entity-group": "Група сутностей стану за замовчуванням", + "default-entity-parameter-name": "За замовчуванням", + "max-relation-level": "Максимальна глибина відносин", + "unlimited-level": "Необмежена глибина", + "state-entity": "Сутність стану панелі пристроїв", + "entities-of-group-state-entity": "Сутності із групи сутностей стану", + "all-entities": "Всі сутності", + "any-relation": "не вказано" + }, + "asset": { + "asset": "Актив", + "assets": "Активи", + "management": "Управління активами", + "view-assets": "Переглянути активи", + "add": "Додати активи", + "assign-to-customer": "Надати клієнту", + "assign-asset-to-customer": "Надати активи клієнту", + "assign-asset-to-customer-text": "Будь ласка, виберіть ресурси, призначені для клієнта", + "no-assets-text": "Не знайдено активів", + "assign-to-customer-text": "Будь ласка, виберіть клієнта, щоб надати активи", + "public": "Публічно", + "assignedToCustomer": "Наданий клієнту", + "make-public": "Зробити актив(и) публічним(и)", + "make-private": "Зробити актив(и) приватним(и)", + "unassign-from-customer": "Позбавити клієнта", + "delete": "Видалити актив", + "asset-public": "Актив є загальнодоступним", + "asset-type": "Тип активу", + "asset-type-required": "Тип активу обов'язковий.", + "select-asset-type": "Виберіть тип активу", + "enter-asset-type": "Введіть тип активу", + "any-asset": "Будь-який актив", + "no-asset-types-matching": "Не знайдено жодних активів, що відповідають даному типу '{{entitySubtype}}'.", + "asset-type-list-empty": "Не вибрано жодного типу активів.", + "asset-types": "Типи активів", + "name": "Ім'я", + "name-required": "Ім'я обов'язкове.", + "description": "Опис", + "type": "Тип", + "type-required": "Тип обов'язковий.", + "details": "Подробиці", + "events": "Події", + "add-asset-text": "Додати новий актив", + "asset-details": "Інформація про актив", + "assign-assets": "Надати активи", + "assign-assets-text": "Надати { count, plural, 1 {1 актив} other {# активи} } клієнту", + "delete-assets": "Видалити активи", + "unassign-assets": "Позбавити активів", + "unassign-assets-action-title": "Позбавити { count, plural, 1 {1 актив} other {# активи} } клієнта", + "assign-new-asset": "Надати новий актив", + "delete-asset-title": "Ви впевнені, що хочете видалити актив '{{assetName}}'?", + "delete-asset-text": "Будьте обережні, після підтвердження, актив і всі пов'язані з ним дані буде втрачено", + "delete-assets-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 актив} other {# активи} }?", + "delete-assets-action-title": "Видалити{ count, plural, 1 {1 актив} other {# активи} }", + "delete-assets-text": "Будьте обережні, після підтвердження всі вибрані об'єкти буде видалено, і всі пов'язані з ними дані буде втрачено.", + "make-public-asset-title": "Ви дійсно хочете, щоб актив '{{assetName}}' був загальнодоступним?", + "make-public-asset-text": "Після підтвердження, актив і всі його дані будуть доступними для інших.", + "make-private-asset-title": "Ви впевнені, що хочете зробити актив {{assetName}} приватним?", + "make-private-asset-text": "Після підтвердження, актив та всі його дані будуть приватними та не будуть доступні іншим.", + "unassign-asset-title": "Ви впевнені, що хочете позбавити активу '{{assetName}}'?", + "unassign-asset-text": "Після підтвердження клієнт буде позбавлений активу. Дані активу не будуть доступні клієнту.", + "unassign-asset": "Позбавити активу", + "unassign-assets-title": "Ви впевнені, що хочете позбавити активів { count, plural, 1 {1 актив} other {# активи} }?", + "unassign-assets-text": "Після підтвердження, клієнт буде позбавлений усіх вибраних активів. Дані активів не будуть доступні клієнту.", + "copyId": "Копіювати Id активу", + "idCopiedMessage": "Id активу був скопійований у буфер обміну", + "select-asset": "Виберіть актив", + "no-assets-matching": "Не знайдено жодних активів, що відповідають'{{entity}}'.", + "asset-required": "Необхідно задати актив", + "name-starts-with": "Назва активу починається з", + "selected-assets": "{ count, plural, 1 {1 актив} other {# активи} } selected", + "search": "Пошук активів", + "select-group-to-add": "Виберіть цільову групу, щоб додати вибрані активи", + "select-group-to-move": "Виберіть цільову групу для переміщення вибраних активів", + "remove-assets-from-group": "Ви впевнені, що хочете видалити { count, plural, 1 {1 актив} other {# актив} } з групи '{entityGroup}'?", + "group": "Група активів", + "list-of-groups": "{ count, plural, 1 {Одна група активів} other {Список # груп активів} }", + "group-name-starts-with": "Групи активів, чиї імена починаються з '{{prefix}}'", + "import": "Імпортувати активи", + "asset-file": "Файл з активами", + "label": "Мітка" + }, + "attribute": { + "attributes": "Атрибути", + "latest-telemetry": "Остання телеметрія", + "attributes-scope": "Область видимості атрибутів", + "scope-latest-telemetry": "Остання телеметрія", + "scope-client": "Клієнтські атрибути", + "scope-server": "Серверні атрибути", + "scope-shared": "Спільні атрибути", + "add": "Додати атрибут", + "add-attribute-prompt": "Будь ласка, додайте атрибут", + "key": "Ключ", + "last-update-time": "Останнє оновлення", + "key-required": "Ключ атрибута обов'язковий.", + "value": "Значення", + "value-required": "Значення атрибута обов'язкове.", + "delete-attributes-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 attribute} other {# attributes} }?", + "delete-attributes-text": "Будьте обережні, після підтвердження, всі виділені атрибути будуть видалені.", + "delete-attributes": "Видалити атрибути", + "enter-attribute-value": "Введіть значення атрибута", + "show-on-widget": "Показати на віджеті", + "widget-mode": "Режим віджетів", + "next-widget": "Наступний віджет", + "prev-widget": "Попередній віджет", + "add-to-dashboard": "Додати до інформаційної панелі", + "add-widget-to-dashboard": "Додати віджет до інформаційної панелі", + "selected-attributes": "{ count, plural, 1 {1 attribute} other {# attributes} } selected ...вибрані вибрати", + "selected-telemetry": "{ count, plural, 1 {1 telemetry unit} other {# telemetry units} } selected" + }, + "audit-log": { + "audit": "Операція", + "audit-logs": "Журнал операцій", + "timestamp": "Тимчасова позначка", + "entity-type": "Тип одиниці", + "entity-name": "Назва організації", + "user": "Користувач", + "type": "Тип", + "status": "Статус", + "details": "Подробиці", + "type-added": "Додано", + "type-deleted": "Вилучено", + "type-updated": "Оновлено", + "type-attributes-updated": "Атрибути оновлені", + "type-attributes-deleted": "Атрибути видалені", + "type-rpc-call": "RPC дзвінок", + "type-credentials-updated": "Авторизаційні дані оновлено", + "type-assigned-to-customer": "Призначено клієнту", + "type-unassigned-from-customer": "Позбавлено від клієнта", + "type-activated": "Активовано", + "type-suspended": "Призупинено", + "type-credentials-read": "Авторизаційні дані прочитані", + "type-attributes-read": "Атрибути читаються", + "type-added-to-entity-group": "Додано до групи", + "type-removed-from-entity-group": "Вилучено з групи", + "type-relation-add-or-update": "Відношення оновлено", + "type-relation-delete": "Відношення видалено", + "type-relations-delete": "Всі відношення видалено", + "type-alarm-ack": "Визнано", + "type-alarm-clear": "Очищено", + "type-login": "Вхід", + "type-logout": "Вихід", + "type-lockout": "Заблокований", + "type-rest-api-rule-engine-call": "Rule engine REST API call", + "status-success": "Успішно", + "status-failure": "Невдало", + "audit-log-details": "Подробиці журналу операцій", + "no-audit-logs-prompt": "Жодних журналів операцій не знайдено", + "action-data": "Дані про дії", + "failure-details": "Невдалі подробиці", + "search": "Пошук журналів перевірки", + "clear-search": "Очистити пошук" + }, + "confirm-on-exit": { + "message": "У вас є незбережені зміни. Ви впевнені, що хочете залишити цю сторінку?", + "html-message": "У вас є незбережені зміни.
    Ви впевнені, що хочете залишити цю сторінку?", + "title": "Незбережені зміни" + }, + "contact": { + "country": "Країна", + "city": "Місто", + "state": "Штат / Провінція", + "postal-code": "Поштовий індекс", + "postal-code-invalid": "Неправильний формат поштового індексу.", + "address": "Адреса", + "address2": "Адреса 2", + "phone": "Телефон", + "email": "Електронна пошта", + "no-address": "Немає адреси" + }, + "common": { + "username": "Ім'я користувача", + "password": "Пароль", + "enter-username": "Введіть ім'я користувача", + "enter-password": "Введіть пароль", + "enter-search": "Введіть пошук", + "created-time": "Час створення" + }, + "converter": { + "converter": "Перетворювач даних", + "converters": "Перетворювачі даних", + "select-converter": "Виберіть перетворювач даних", + "no-converters-matching": "Не має перетворювачів даних, які відповідають '{{entity}}'.", + "converter-required": "Необхідно вказати перетворювач даних", + "delete": "Видалити перетворювач даних", + "management": "Управління перетворювачами даних", + "add-converter-text": "Додати новий перетворювач даних", + "no-converters-text": "Перетворювачів даних не знайдено", + "selected-converters": "{ count, plural, 1 {1 перетворювач даних} other {# перетворювачі даних} } вибраний", + "delete-converter-title": "Ви впевнені, що хочете видалити перетворювач даних '{{converterName}}'?", + "delete-converter-text": "Будьте обережні, після підтвердження, перетворювач даних та всі пов'язані з ним дані,стануть недоступними).", + "delete-converters-title": "Ви впевнені, що хочете видалити{ count, plural, 1 {1 перетворювач даних} other {# перетворювачі даних} }?", + "delete-converters-action-title": "Видалити { count, plural, 1 {1 перетворювач даних} other {# перетворювачі даних} }", + "delete-converters-text": "Будьте обережні, після підтвердження всі вибрані перетворювачі даних буде видалено, і всі пов'язані з ними дані буде втрачено.", + "events": "Події", + "add": "Додати перетворювач даних", + "converter-details": "Подробиці про перетворювач даних", + "details": "Подробиці", + "copyId": "Копіювати Id перетворювача даних", + "idCopiedMessage": "Id перетворювача даних було скопійовано у буфер обміну", + "debug-mode": "Режим налагодження", + "name": "Ім'я", + "name-required": "Ім'я обов'язкове.", + "description": "Опис", + "decoder": "Декодер", + "encoder": "Кодер", + "test-decoder-fuction": "Тестування функції декодера", + "test-encoder-fuction": "Тестування функції кодера", + "decoder-input-params": "Вхідні параметри декодера", + "encoder-input-params": "Вхідні параметри кодера", + "payload": "Вхідне повідомлення", + "payload-content-type": "Тип контенту вхідного повідомлення", + "payload-content": "Зміст вхідного повідомлення", + "message": "Повідомлення", + "message-type": "Тип повідомлення", + "message-type-required": "Необхідно задати тип повідомлення", + "test": "Тест", + "metadata": "Метадані", + "metadata-required": "Записи метаданих не можуть бути порожніми.", + "integration-metadata": "Метедані інтеграції", + "integration-metadata-required": "Параметри метаданих інтеграції не можуть бути порожніми.", + "output": "Вихідні дані", + "import": "Імпорт перетворювача даних", + "export": "Експорт перетворювача даних", + "export-failed-error": "Неможливо експортувати перетворювач даних: {{помилка}}", + "create-new-converter": "Створити новий перетворювач даних", + "converter-file": "Файл перетворювача даних(конвектер файл)", + "invalid-converter-file-error": "Неможливо імпортувати перетворювач даних: недійсна структура даних перетворювача.", + "type": "Тип", + "type-required": "Необхідно задати тип.", + "type-uplink": "Від пристрою", + "type-downlink": "До пристрою" + }, + "content-type": { + "json": "Json", + "text": "Текст", + "binary": "Бінарний (Base64)" + }, + "customer": { + "customer": "Клієнт", + "customers": "Клієнти", + "management": "Клієнтський менеджмент", + "dashboard": "Інформаційна панель клієнта", + "dashboards": "Інформаційні панелі клієнта", + "devices": "Пристрої клієнта", + "entity-views": "Представлення сутностей", + "assets": "Клієнтські активи", + "public-dashboards": "Публічні інформаційні панелі", + "public-devices": "Публічні пристрої", + "public-assets": "Публічні активи", + "public-entity-views": "Публічне представлення сутностей 440", + "add": "Додати клієнта", + "delete": "Видалити клієнта", + "manage-customer-users": "Керування користувачами клієнта", + "manage-customer-devices": "Керування пристроями клієнта", + "manage-customer-dashboards": "Керування інформаційними панелями клієнта", + "manage-public-devices": "Керувати загальнодоступними пристроями", + "manage-public-dashboards": "Керування загальнодоступними інформаційними панелями", + "manage-customer-assets": "Керування активами клієнта", + "manage-public-assets": "Керування загальнодоступними активами", + "add-customer-text": "Додати нового клієнта", + "no-customers-text": "Клієнтів не знайдено", + "customer-details": "Інформація про клієнта", + "delete-customer-title": "Ви впевнені, що хочете видалити клієнта '{{customerTitle}}'?", + "delete-customer-text": "Будьте обережні, після підтвердження, клієнт та всі пов'язані з ним дані, стануть недоступними.", + "delete-customers-title": "Ви впевнені, що хочете видалити {count, plural, 1 {1 клієнт} other {# клієнти} }?", + "delete-customers-action-title": "Видалити{ count, plural, 1 {1 клієнт} other {# клієнти} }", + "delete-customers-text": "Будьте обережні, після підтвердження, всі вибрані клієнти будуть видалені і всі пов'язані з ними дані, стануть недоступними.", + "manage-users": "Керування користувачами", + "manage-assets": "Керування активами", + "manage-devices": "Керування пристроями", + "manage-dashboards": "Керування інформаційними панелями", + "title": "Назва", + "title-required": "Необхідно задати назву.", + "description": "Опис", + "details": "Подробиці", + "events": "Події", + "copyId": "Копіювати Id клієнта", + "idCopiedMessage": "Id клієнта було скопійовано в буфер обміну", + "select-customer": "Виберіть клієнта", + "no-customers-matching": "Клієнтів, які відповідають '{{entity}}' не знайдено.", + "customer-required": "Необхідно задати клієнта", + "selected-customers": "{ count, plural, 1 {1 клієнт} other {# клієнти} } вибрано", + "search": "Пошук клієнтів", + "select-group-to-add": "Виберіть цільову групу, щоб додати вибраних клієнтів", + "select-group-to-move": "Виберіть цільову групу для переміщення вибраних клієнтів", + "remove-customers-from-group": "Ви впевнені, що хочете видалити{ count, plural, 1 {1 клієнт} other {# клієнти} } з групи'{entityGroup}'?", + "group": "Група клієнтів", + "list-of-groups": "{ count, plural, 1 {Одна група клієнтів} other {Список # груп клієнтів} }", + "group-name-starts-with": "Групи клієнтів, імена яких починаються з '{{prefix}}'", + "select-default-customer": "Вибрати клієнта за замовчуванням", + "default-customer": "Клієнт за замовчуванням", + "default-customer-required": "Необхідно вказати клієнта за замовчуванням для налагодження панелі візуалізації на рівні замовника", + "allow-white-labeling": "Дозволити брендування" + }, + "custom-translation": { + "custom-translation": "Переклад для користувача", + "translation-map": "Карта перекладу", + "key": "Ключ перекладу", + "import": "Імпорт перекладу", + "export": "Експорт перекладу", + "export-data": "Дані про експорт перекладу", + "import-data": "Дані про імпорт перекладу", + "translation-file": "Файл перекладу", + "invalid-translation-file-error": "Неможливо імпортувати файл перекладу: недійсна структура даних перекладу.", + "custom-translation-hint": "Визначте індивідуальний переклад в JSON нижче. Цей JSON перезапише переклад за замовчуванням. Натисніть 'Завантажити файл перекладу', щоб отримати існуючий переклад. Ви також можете скористатись завантаженим файлом як посиланням на наявні пари параметрів перекладу ключ-значення.", + "download-locale-file": "Завантажити файл перекладу" + }, + "datetime": { + "date-from": "Дата від", + "time-from": "Час від", + "date-to": "Дата до", + "time-to": "Час до" + }, + "dashboard": { + "dashboard": "Панель приладів", + "dashboards": "Панелі приладів", + "management": "Управління панеллю приладів", + "view-dashboards": "Переглянути панелі приладів", + "add": "Додати панель приладів", + "assign-dashboard-to-customer": "Призначити панель(і) приладів замовнику", + "assign-dashboard-to-customer-text": "Будь ласка, виберіть панелі пристроїв, щоб призначити їх клієнту", + "assign-to-customer-text": "Виберіть клієнта, щоб призначити панелі пристроїв", + "assign-to-customer": "Призначити клієнту", + "unassign-from-customer": "Позбавити клієнта", + "make-public": "Зробити панель приладів публічною", + "make-private": "Зробити панель приладів приватною", + "manage-assigned-customers": "Керування призначеними клієнтами", + "assigned-customers": "Призначені клієнтам", + "assign-to-customers": "Призначити панелі приладів клієнтам", + "assign-to-customers-text": "Виберіть клієнтів для призначення панелей приладів", + "unassign-from-customers": "Позбавити клієнтів призначенних панелей приладів", + "unassign-from-customers-text": "Виберіть клієнтів для позбавлення їх призначених панелей приладів", + "no-dashboards-text": "Панелі приладів не знайдені", + "no-widgets": "Не налаштовано жодних віджетів", + "add-widget": "Додати новий віджет", + "title": "Назва", + "select-widget-title": "Вибрати віджет", + "select-widget-subtitle": "Список доступних типів віджетів", + "delete": "Видалити панель приладів", + "title-required": "Необхідно задати назву.", + "description": "Опис", + "details": "подробиці", + "dashboard-details": "Подробиці панелі приладів", + "add-dashboard-text": "Додати нову панель приладів", + "assign-dashboards": "Призначити панель приладів", + "assign-new-dashboard": "Призначити нову панель приладів", + "assign-dashboards-text": "Призначити { count, plural, 1 {1 панель приладів} other {# панелі приладів} } користувачам", + "unassign-dashboards-action-text": "Позбавити { count, plural, 1 {1 палелі приладів} other {# панелей приладів} } клієнтів", + "delete-dashboards": "Видалити панель приладів", + "unassign-dashboards": "Позбавити панелей приладів", + "unassign-dashboards-action-title": "Позбавити { count, plural, 1 {1 палелі приладів} other {# панелей приладів} } клієнтів", + "delete-dashboard-title": "Ви впевнені, що хочете видалити панель приладів '{{назва панелі приладів}}'?", + "delete-dashboard-text": "Будьте обережні, після підтвердження, панель приладів і всі пов'язані з нею дані стануть недоступними.", + "delete-dashboards-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 панель приладів} other {# панелі приладів} }?", + "delete-dashboards-action-title": "Видалити { count, plural, 1 {1 панель приладів} other {# панелі приладів} }", + "delete-dashboards-text": "Будьте обережні, після підтвердження, всі вибрані панелі приладів буде видалено, і всі пов'язані з ними дані стануть недоступними.", + "unassign-dashboard-title": "Ви впевнені, що хочете позбавити панелі приладів '{{назва інформаційної панелі}}'?", + "unassign-dashboard-text": "Після підтвердження, клієнт буде позбавлений панелі приладів. Панель приладів і пов'язані з нею дані будуть недоступні клієнтові.", + "unassign-dashboard": "Позбавити панелі приладів", + "unassign-dashboards-title": "Ви впевнені, що хочете позбавити { count, plural, 1 {1 панелі приладів} other {# панелей приладів} }?", + "unassign-dashboards-text": "Після підтвердження, клієтн буде позбавлений усіх вибраних панелей приладів і даних, які з ними пов'язані.", + "public-dashboard-title": "Панель приладів тепер публічна", + "public-dashboard-text": "Ваша панель приладів {{dashboardTitle}} тепер публічна і доступна іншим link:", + "public-dashboard-notice": "Note: Не забудьте зробити спільні пристрої загальнодоступними, щоб отримати доступ до їхніх даних.", + "make-private-dashboard-title": "Ви впевнені, що хочете зробити панель приладів '{{назва панелі приладів}}' приватною?", + "make-private-dashboard-text": "Після підтвердження панель приладів стане приватною і не буде доступною іншим.", + "make-private-dashboard": "Зробити панель приладів приватною", + "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard", + "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard", + "select-dashboard": "Вибрати панель приладів", + "no-dashboards-matching": "Не знайдено жодних панелей прилодів'{{entity}}' які відповідають.", + "dashboard-required": "Необхідно задати панель приладів.", + "select-existing": "Виберіть існуючу панель приладів", + "create-new": "Створити нову панель приладів", + "new-dashboard-title": "Нова назва панелі приладів", + "open-dashboard": "Відрити панель приладів", + "set-background": "Встановити фон", + "background-color": "Колір фону", + "background-image": "Фонове зображення", + "background-size-mode": "Режим фонового розміру", + "no-image": "Не вибрано жодного зображення", + "drop-image": "Перетягніть зображення або клацніть, щоб вибрати файл для завантаження.", + "settings": "Налаштування", + "columns-count": "Кількість стовпців", + "columns-count-required": "Необхідно вказати кількість стовпців.", + "min-columns-count-message": "Дозволений мінімум -10 стовпців.", + "max-columns-count-message": "Дозволений максимум - 1000 стовпців.", + "widgets-margins": "Відступ між віджетами", + "horizontal-margin": "Горизонтальний відступ", + "horizontal-margin-required": "Необхідно вказати горизонтальний відступ.", + "min-horizontal-margin-message": "Допустиме мінімальне значення горизонтального відступу - 0.", + "max-horizontal-margin-message": "Допустиме максимальне значення горизонтального відступу - 50.", + "vertical-margin": "Вертикальний відступ", + "vertical-margin-required": "Необхідно вказати вертикальний відступ.", + "min-vertical-margin-message": "Допустиме мінімальне значення вертикального відступу - 0.", + "max-vertical-margin-message": "Допустиме максимальне значення вертикального відступу - 50.", + "autofill-height": "Висота автоматичного заповнення макета", + "mobile-layout": "Налаштування макета для мобільних пристроїв", + "mobile-row-height": "Висота рядка для мобільних пристроїв, px", + "mobile-row-height-required": "Потрібно вказати значення висоти рядка для мобільних пристроїв.", + "min-mobile-row-height-message": "Допустиме мінімальне значення висоти рядка для мобільних пристроїв - 5 пікселів.", + "max-mobile-row-height-message": "Допустиме максимальне значення висоти рядка для мобільних пристроїв - 200 пікселів.", + "display-title": "Відображати назву панелі візуалізації", + "toolbar-always-open": "Тримайте панель візуалізації відкритою", + "title-color": "Колір назви", + "display-dashboards-selection": "Відображення вибору панелей візуалізації", + "display-entities-selection": "Вибір відображення сутності", + "display-dashboard-timewindow": "Відобразити налаштування часового проміжку", + "display-dashboard-export": "Відображення експорту", + "import": "Імпортувати панель візуалізації", + "export": "Експортувати панель візуалізації", + "export-failed-error": "Неможливо експортувати панель візуалізації: {{error}}", + "export-pdf": "Експортувати як PDF", + "export-png": "Експортувати як PNG", + "export-jpg": "Експортувати як JPEG", + "export-json-config": "Експортувати конфігурацію JSON", + "download-dashboard-progress": "Генерування панелі візуалізації {{reportType}} ...", + "create-new-dashboard": "Створити нову панель візуалізації", + "dashboard-file": "Файл панелі візуалізації", + "invalid-dashboard-file-error": "Неможливо імпортувати панель візуалізації: неправильна структура даних панелі візуалізації.", + "dashboard-import-missing-aliases-title": "Configure aliases used by imported dashboard Налаштування псевдонімів, що використовуються імпортованою панеллю візуалізації", + "create-new-widget": "Створити новий віджет", + "import-widget": "Імпортувати віджет", + "widget-file": "Файл віджета", + "invalid-widget-file-error": "Неможливо імпортувати віджет: неправильна структура даних віджета.", + "widget-import-missing-aliases-title": "Налаштувати псевдоніми, що використовуються імпортованим віджетом", + "open-toolbar": "Відкрити панель інструменів ", + "close-toolbar": "Закрити панель інструменів", + "configuration-error": "Помилка конфігурації", + "alias-resolution-error-title": "Помилка конфігурації псевдонімів панелі візуалізації", + "invalid-aliases-config": "Неможливо знайти пристрої, які відповідають певному фільтру псевдонімів.
    Зверніться до свого адміністратора, щоб вирішити цю проблему.", + "select-devices": "Вибрати пристрої", + "assignedToCustomer": "Призначений клієнту", + "assignedToCustomers": "Призначений клієнтам", + "public": "Публічно", + "public-link": "Публічне посилання", + "copy-public-link": "Копіювати публічне посилання", + "public-link-copied-message": "Публічне посилання було скопійоване в буфер обміну панелі візуалізації", + "manage-states": "Керування станами панелі візуалізації", + "states": "Стани панелі візуалізації", + "search-states": "Пошук станів панелі візуалізації", + "selected-states": "{ count, plural, 1 {1 dashboard state} other {# dashboard states} } вибрано", + "edit-state": "Редагувати стан панелі візуалізації", + "delete-state": "Видалити стан панелі візуалізації ", + "add-state": "Додати стан панелі візуалізації", + "state": "Стан панелі візуалізації", + "state-name": "Ім'я", + "state-name-required": "Необхідно вказати назву стану панелі візуалізації.", + "state-id": "Стан Id", + "state-id-required": "Необхідно вказати id стану панелі візуалізації.", + "state-id-exists": "Стан інформаційної панелі з таким id вже існує.", + "is-root-state": "Основний стан", + "delete-state-title": "Видалити стан панелі візуалізації", + "delete-state-text": "Ви впевнені, що хочете видалити стан панелі візуалізації з іменем '{{stateName}}'?", + "show-details": "Показати деталі", + "hide-details": "Приховати деталі", + "select-state": "Виберіть цільовий стан", + "state-controller": "Контроллер стану" + }, + "datakey": { + "settings": "Налаштування", + "advanced": "Додатково", + "label": "Мітка", + "color": "Колір", + "units": "Спеціальний символ, який показує наступне значення", + "decimals": "Кількість цифр після плаваючої точки", + "data-generation-func": "Функція генерації даних", + "use-data-post-processing-func": "Використовувати функцію пост-обробки даних", + "configuration": "Конфігурація ключа даних", + "timeseries": "Телеметрія", + "attributes": "Атрибути", + "entity-field": "Поле сутності", + "alarm": "Поля сигнала тривоги", + "timeseries-required": "Необхідно вказати Телеметрія.", + "timeseries-or-attributes-required": "Необхідно вказати телеметрію/атрибути.", + "maximum-timeseries-or-attributes": "Максимальні { count, plural, 1 {1 телеметрія/атрибут дозволені.} other {# телеметрія/атрибути дозволені} }", + "alarm-fields-required": "Необхідно вказати поля сигнала тривоги.", + "function-types": "Типи функцій", + "function-types-required": "Необхідно вказати типи функцій.", + "maximum-function-types": "Maximum { count, plural, 1 {1 function type is allowed.} other {# function types are allowed} }", + "time-description": "мітка часу поточного значення;", + "value-description": "поточне значення;", + "prev-value-description": "результат попереднього виклику функції;", + "time-prev-description": "мітка часу попереднього значення;", + "prev-orig-value-description": "оригінальне попереднє значення;" + }, + "datasource": { + "type": "Тип джерела даних", + "name": "Ім'я", + "add-datasource-prompt": "Додайте джерело даних" + }, + "details": { + "details": "Деталі", + "edit-mode": "Режим редагування", + "edit-json": "Редагувати JSON", + "toggle-edit-mode": "Перемкнути режим редагування" + }, + "device": { + "device": "Пристрій", + "device-required": "Необхідно задати пристрій.", + "devices": "Пристрої", + "management": "Управління пристроєм", + "view-devices": "Перегляд пристроїв", + "device-alias": "Псевдонім пристрою", + "aliases": "Псевдонім пристроїв", + "no-alias-matching": "'{{alias}}' не знайдено.", + "no-aliases-found": "Псевдонімів не знайдено.", + "no-key-matching": "'{{key}}' не знайдено.", + "no-keys-found": "Ключі не знайдено.", + "create-new-alias": "Створити новий!", + "create-new-key": "Створити новий!", + "duplicate-alias-error": "Псевдонім з таким іменем '{{alias}}' вже існює.
    Псевдоніми пристроїв повинні бути унікальними на панелі візуалізації.", + "configure-alias": "Налаштувати псевдонім '{{alias}}'", + "no-devices-matching": "Не знайдено жодних пристроїв, які відповідають '{{entity}}'.", + "alias": "Псевдонім", + "alias-required": "Необхідно задати псевдонім пристрою.", + "remove-alias": "Видалити псевдонім пристрою", + "add-alias": "Додати псевдонім пристрою", + "name-starts-with": "Ім'я пристрою починається з", + "device-list": "Список пристроїв", + "use-device-name-filter": "Використати фільтр", + "device-list-empty": "Не вибрано жодного пристрою.", + "device-name-filter-required": "Необхідно задати назву фільтра пристрою.", + "device-name-filter-no-device-matched": "Не знайдено жодних пристроїв, що починаються з '{{device}}'.", + "add": "Додати пристрій", + "assign-to-customer": "Призначити клієнту", + "assign-device-to-customer": "Призначити пристрій (ої) клієнту", + "assign-device-to-customer-text": "Виберіть пристрої, які слід призначити клієнту", + "make-public": "Зробити пристрій публічним", + "make-private": "Зробити пристрій приватним", + "no-devices-text": "Не знайдено жодного пристрою", + "assign-to-customer-text": "Виберіть клієнта для призначення пристрою (їв)", + "device-details": "Деталі пристрою", + "add-device-text": "Додати новий пристрій", + "credentials": "Авторизаційні дані", + "manage-credentials": "Керування авторизаційними даними", + "delete": "Видалити пристрій", + "assign-devices": "Призначити пристрої", + "assign-devices-text": "Призначити { count, plural, 1 {1 пристрій} other {# пристрої} } клієнту", + "delete-devices": "Видалити пристрої", + "unassign-from-customer": "Позбавити клієнта пристроїв", + "unassign-devices": "Позбавити пристроїв", + "unassign-devices-action-title": "Позбавити клієнта { count, plural, 1 {1 пристрою} other {# пристроїв} }", + "assign-new-device": "Призначити новий пристрій", + "make-public-device-title": "Ви впевнені, що хочете зробити пристрій '{{deviceName}}' публічним?", + "make-public-device-text": "Після підтвердження пристрій і всі його дані будуть публічними та доступними для інших.", + "make-private-device-title": "Ви впевнені, що хочете зробити пристрій '{{deviceName}}' приватним?", + "make-private-device-text": "Після підтвердження пристрій і всі його дані будуть приватними та недоступними для інших.", + "view-credentials": "Переглянути авторизаційні дані", + "delete-device-title": "Ви впевнені, що хочете видалити пристрій '{{deviceName}}'?", + "delete-device-text": "Будьте обережні, після підтвердження, пристрій і всі пов'язані з ним дані стануть недоступними.", + "delete-devices-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 пристрій} other {# пристрої} }?", + "delete-devices-action-title": "Видалити { count, plural, 1 {1 пристрій} other {# пристрої} }", + "delete-devices-text": "Будьте обережні, після підтвердження, всі вибрані пристрої будуть видалені, і всі пов'язані з ними дані стануть недоступними.", + "unassign-device-title": "Ви впевнені, що хочете позбавити пристрою '{{deviceName}}'?", + "unassign-device-text": "Після підтвердження, клієнт буде позбавлений пристрою.", + "unassign-device": "Позбавити пристою", + "unassign-devices-title": "Ви впевненені, що хочете позбавити { count, plural, 1 {1 пристрою} other {# пристроїв} }?", + "unassign-devices-text": "Після підтвердження, клієнт буде позбавлений пристрою і пристрій стане не доступним клієнту", + "device-credentials": "Авторизаційні дані прстрою", + "credentials-type": "Тип авторизаційних даних", + "access-token": "Маркер доступу", + "access-token-required": "Необхідно вказати маркер доступу.", + "access-token-invalid": "Маркер доступу має бути від 1 до 20 символів.", + "rsa-key": "Публічний ключ RSA", + "rsa-key-required": "Необхідно вказати публічний ключ RSA.", + "secret": "Секрет", + "secret-required": "Необхідно вказати секрет.", + "device-type": "Тип пристрою", + "device-type-required": "Необхідно вказати тип пристрою.", + "select-device-type": "Виберіть тип пристрою", + "enter-device-type": "Введіть тип пристрою", + "any-device": "Будь-який пристрій", + "no-device-types-matching": "Не знайдено типів пристроїв, які відповідають '{{entitySubtype}}'.", + "device-type-list-empty": "Не вибрано типів пристроїв.", + "device-types": "Типи пристрою", + "name": "Ім'я", + "name-required": "Необхідно вказати ім'я.", + "description": "Опис", + "events": "Події", + "details": "Деталі", + "copyId": "Копіювати Id пристрою", + "copyAccessToken": "Копіювати маркер доступу", + "idCopiedMessage": "Id пристрою скопійовано в буфер обміну", + "accessTokenCopiedMessage": "Маркер доступу до пристрою скопійовано в буфер обміну", + "assignedToCustomer": "Призначений клієнту", + "unable-delete-device-alias-title": "Неможливо видалити псевдонім пристрою", + "unable-delete-device-alias-text": "Псевдонім пристрою '{{deviceAlias}}' не може бути видалений, оскільки він використовується наступним(и) віджетом(ами):
    {{widgetsList}}", + "is-gateway": "Шлюз", + "public": "Публічно", + "device-public": "Пристрій є публічним", + "select-device": "Виберіть пристрій", + "import": "Імпортувати пристрої", + "device-file": "Файл з пристроями", + "selected-devices": "{ count, plural, 1 {1 пристрій} other {# пристрої} } вибрано", + "search": "Шукати пристрої", + "select-group-to-add": "Виберіть цільову групу, щоб додати вибраний пристрій", + "select-group-to-move": "Виберіть цільову групу для переміщення вибраних пристроїв", + "remove-devices-from-group": "Ви впевнені, що хочете видалити { count, plural, 1 {1 пристрій} other {# пристрої} } з групи '{entityGroup}'?", + "group": "Група пристроїв", + "list-of-groups": "{ count, plural, 1 {Одна група пристроїв} other {Список # груп пристроїв} }", + "group-name-starts-with": "Групи пристроїв, імена яких починаються з '{{prefix}}'" + }, + "dialog": { + "close": "Закрити діалогове вікно" + }, + "direction": { + "column": "Колонка", + "row": "Рядок" + }, + "error": { + "unable-to-connect": "Неможливо підключитися до сервера! Перевірте підключення до Інтернету.", + "unhandled-error-code": "Неопрацьований помилковий код: {{errorCode}}", + "unknown-error": "Невідома помилка" + }, + "entity": { + "entity": "Сутність", + "entities": "Сутності", + "aliases": "Псевдоніми сутності", + "entity-alias": "Псевдонім сутності", + "unable-delete-entity-alias-title": "Неможливо видалити псевдонім сутності", + "unable-delete-entity-alias-text": "Псевдонім сутності'{{entityAlias}}' неможливо видалити, так як це використовується наступним віджетом(s):
    {{widgetsList}}", + "duplicate-alias-error": "Знайдено повторюваний псевдонім '{{alias}}'.
    Псевдоніми сутностей повинні бути унікальними на панелі візуалізації.", + "missing-entity-filter-error": "Відсутній фільтр для псевдоніма '{{alias}}'.", + "configure-alias": "Налаштувати '{{alias}}' псевдонім", + "alias": "Псевдонім", + "alias-required": "Необхідно вказати псевдонім сутності.", + "remove-alias": "Видалити псевдонім сутності", + "add-alias": "Додати псевдонім сутності", + "entity-list": "Список сутності", + "entity-type": "Тип сутності", + "entity-types": "Типи сутності", + "entity-type-list": "Список типу сутності", + "any-entity": "Будь-яка сутність", + "enter-entity-type": "Введіть тип сутності", + "no-entities-matching": "Не знайдено жожних сутностей, що відповідають '{{entity}}' що відповідають.", + "no-entity-types-matching": "Не знайдено жожних типів сутностей, що відповідають '{{entityType}}'.", + "name-starts-with": "Назва починається з", + "use-entity-name-filter": "Використовуйте фільтр", + "entity-list-empty": "Не вибрано жодних сутностей.", + "entity-type-list-empty": "Не вибрано жодних типів сутностей.", + "entity-name-filter-required": "Необхідно задати фільтр по імені.", + "entity-name-filter-no-entity-matched": "Не знайдено жодних сутностей, що починаються з '{{entity}}'.", + "all-subtypes": "Всі", + "select-entities": "Виберіть сутність", + "no-aliases-found": "Псевдонімів не знайдено.", + "no-alias-matching": "'{{alias}}' не знайдено.", + "create-new-alias": "Створити новий псевдонім!", + "key": "Ключ", + "key-name": "Ім'я ключа", + "no-keys-found": "No keys found.", + "no-key-matching": "'{{key}}' не знайдено.", + "create-new-key": "Створити новий ключ!", + "type": "Тип", + "type-required": "Необхідно задати тип сутності.", + "type-device": "Пристрій", + "type-devices": "Пристрої", + "list-of-devices": "{ count, plural, 1 {Один пристрій} other {Список # пристроїв} }", + "device-name-starts-with": "Пристрої, імена яких починаються з '{{prefix}}'", + "type-asset": "Актив", + "type-assets": "Активи", + "list-of-assets": "{ count, plural, 1 {Один актив} other {Список # активів} }", + "asset-name-starts-with": "Активи, імена яких починаються з '{{prefix}}'", + "type-entity-view": "Перегляд сутності", + "type-entity-views": "Перегляди сутності", + "list-of-entity-views": "{ count, plural, 1 {Один перегляд сутності} other {Список # переглядів сутності} }", + "entity-view-name-starts-with": "Перегляди сутностей, імена яких починаються з '{{prefix}}'", + "type-rule": "Правило", + "type-rules": "Правила", + "list-of-rules": "{ count, plural, 1 {Одне правило} other {Список # правил} }", + "rule-name-starts-with": "Правила, імена яких починаються з '{{prefix}}'", + "type-plugin": "Плагін", + "type-plugins": "Плагіни", + "list-of-plugins": "{ count, plural, 1 {Один плагін} other {Список # плагінів} }", + "plugin-name-starts-with": "Плагіни, імена яких починаються з '{{prefix}}'", + "type-tenant": "Власник", + "type-tenants": "Власники", + "list-of-tenants": "{ count, plural, 1 {Один власник} other {Список # власників} }", + "tenant-name-starts-with": "Власники, імена яких починаються з '{{prefix}}'", + "type-customer": "Клієнт", + "type-customers": "Клієнти", + "list-of-customers": "{ count, plural, 1 {Один клієнт} other {Список # клієнтів} }", + "customer-name-starts-with": "Клієнти, імена яких починаються з '{{prefix}}'", + "type-user": "Користувач", + "type-users": "Користувачі", + "list-of-users": "{ count, plural, 1 {Один користувач} other {Список # користувачів } }", + "user-name-starts-with": "Користувачі, імена яких починаються з '{{prefix}}'", + "type-dashboard": "Панель візуалізації", + "type-dashboards": "Панелі візуалізації", + "list-of-dashboards": "{ count, plural, 1 {Одна панель візуалізації} other {Список # панелей візуалізації} }", + "dashboard-name-starts-with": "Панелі візуалізації, імена яких починаються з '{{prefix}}'", + "type-alarm": "Сигнал тривоги", + "type-alarms": "Сигнали тривоги", + "list-of-alarms": "{ count, plural, 1 {Один сигнал тривоги} other {Список # сигналів тривоги} }", + "alarm-name-starts-with": "Сигнали тривоги, імена яких починаються '{{prefix}}'", + "type-rulechain": "Ланцюжок правил", + "type-rulechains": "Ланцюжки правил", + "list-of-rulechains": "{ count, plural, 1 {Один ланцюжок правил} other {Список # ланцюжків правил} }", + "rulechain-name-starts-with": "Правило ланцюжків, імена яких починаються '{{prefix}}'", + "type-scheduler-event": "Scheduler event", + "type-scheduler-events": "Scheduler events", + "list-of-scheduler-events": "{ count, plural, 1 {One scheduler event} other {List of # scheduler events} }", + "scheduler-event-name-starts-with": "Scheduler events whose names start with '{{prefix}}'", + "type-blob-entity": "Blob entity", + "type-blob-entities": "Blob entities", + "list-of-blob-entities": "{ count, plural, 1 {One blob entity} other {List of # blob entities} }", + "blob-entity-name-starts-with": "Blob entities whose names start with '{{prefix}}'", + "type-rulenode": "Правило", + "type-rulenodes": "Правила", + "list-of-rulenodes": "{ count, plural, 1 {Одне правило} other {Список # правил} }", + "rulenode-name-starts-with": "Список правил, імена яких починаються '{{prefix}}'", + "type-current-customer": "Поточний клієнт", + "type-current-tenant": "Поточний власник", + "search": "Пошук сутностей", + "selected-entities": "{ count, plural, 1 {1 сутність} other {# сутності} } вибрано", + "entity-name": "Ім'я сутності", + "entity-label": "Мітка сутності", + "details": "Подробиці сутності", + "no-entities-prompt": "Сутності не знайдено", + "no-data": "Немає даних для відображення", + "columns-to-display": "Стовпці для відображення", + "type-entity-group": "Група сутностей", + "type-converter": "Перетворювач даних", + "type-converters": "Перетворювачі даних", + "list-of-converters": "{ count, plural, 1 {Однин перетворювач даних} other {Список # перетворювачів даних} }", + "converter-name-starts-with": "Перетворювачі даних, імена яких починаються з '{{prefix}}'", + "type-integration": "Інтеграція", + "type-integrations": "Інтеграції", + "list-of-integrations": "{ count, plural, 1 {Одна інтеграція} other {Список # інтеграцій} }", + "integration-name-starts-with": "Інтеграції, імена яких починаються з '{{prefix}}'" + }, + "entity-field": { + "created-time": "Час створення", + "name": "Ім'я", + "type": "Тип", + "first-name": "Ім'я", + "last-name": "Прізвище", + "email": "Електронна пошта", + "title": "Назва", + "country": "Країна", + "state": "Штат", + "city": "Місто", + "address": "Адреса", + "address2": "Адреса 2", + "zip": "Zip", + "phone": "Телефон", + "label": "Мітка" + }, + "entity-group": { + "entity-group": "Група сутності", + "details": "Деталі", + "columns": "Стовпці", + "add-column": "Додати стовпець", + "column-value": "Значення", + "column-value-required": "Необхідно вказати значення.", + "column-title": "Назва", + "default-sort-order": "Основний порядок сортування", + "default-sort-order-required": "Необхідно вказати основний порядок сортування.", + "hide-in-mobile-view": "Мобільний приховано", + "use-cell-style-function": "Використовувати функцію стилю комірки", + "use-cell-content-function": "Use cell content function", + "edit-column": "Редагувати стовпець", + "column-details": "Деталі стовпця", + "actions": "Дії", + "settings": "Налаштування", + "delete": "Видалити групу сутностей", + "name": "Ім'я", + "name-required": "Необхідно вказати ім'я.", + "description": "Опис", + "add": "Додати групу сутностей", + "add-entity-group-text": "Додати нову групу сутностей", + "no-entity-groups-text": "Не знайдено жодних груп сутності", + "entity-group-details": "Деталі групи сутності", + "delete-entity-groups": "Видалити групи сутностей", + "delete-entity-group-title": "Ви впевнені, що хочете видалити групу сутності '{{entityGroupName}}'?", + "delete-entity-group-text": "Будьте обережні, після підтвердження, група сутностей і всі пов'язані з нею дані стануть недоступними.", + "delete-entity-groups-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 групу сутності} other {# групи сутностей} }?", + "delete-entity-groups-action-title": "Видалити { count, plural, 1 {1 групу сутності} other {# групи сутностей} }", + "delete-entity-groups-text": "Будьте обережні, після підтвердження, всі виділені групи сутностей і пов'язані з ними дані, стануть недоступними.", + "device-groups": "Групи пристроїв", + "asset-groups": "Групи активів", + "customer-groups": "Групи клієнтів", + "device-group": "Група пристроїв", + "asset-group": "Група активів", + "customer-group": "Група клієнтів", + "fetch-more": "Отримати більше", + "column-type": { + "column-type": "Тип стовпця", + "client-attribute": "Атрибут клієнта", + "shared-attribute": "Спільний атрибут", + "server-attribute": "Атрибут сервера", + "timeseries": "Телеметрія", + "entity-field": "Поле сутності" + }, + "column-type-required": "Необхідно вказати тип стовпця.", + "entity-field": { + "created-time": "Час створення", + "name": "Ім'я", + "type": "Тип", + "assigned_customer": "Призначений клієнт", + "authority": "Авторитет", + "first_name": "Ім'я", + "last_name": "Прізвище", + "email": "Електронна пошта", + "title": "Назва", + "country": "Країна", + "state": "Штат", + "city": "Місто", + "address": "Адреса", + "address2": "Адреса 2", + "zip": "Zip", + "phone": "Телефон" + }, + "sort-order": { + "asc": "У порядку зростання", + "desc": "У порядку зменшення", + "none": "Немає" + }, + "details-mode": { + "on-row-click": "Клацніть на рядок", + "on-action-button-click": "Клацніть на кнопку детелі", + "disabled": "Вимкнено" + }, + "add-to-group": "Додати до групи", + "move-to-group": "Перемістити до групи", + "select-entity-group": "Виберіть групу сутностей", + "no-entity-groups-matching": "Не знайдено жодних груп сутностей, що відповідають '{{entityGroup}}'.", + "target-entity-group-required": "Необхідно вказати цільову групу сутності.", + "remove-from-group": "Видалити з групи", + "group-table-title": "Group table title", + "enable-search": "Увімкнути пошук сутностей", + "enable-add": "Увімкнути додавання сутностей", + "enable-delete": "Увімкнути видалення сутностей", + "enable-selection": "Увімкнути вибір сутностей", + "enable-group-transfer": "Увімкнути дії групового перенесення", + "display-pagination": "Відображення сторінок", + "default-page-size": "Розмір сторінки за замовчуванням", + "enable-assignment-actions": "Увімкнути дії призначення", + "enable-credentials-management": "Увімкнути керування авторизаційними даними", + "enable-users-management": "Увімкнути керування користувачами", + "enable-assets-management": "Увімкнути керування активами", + "enable-devices-management": "Увімкнути керування пристроями", + "enable-dashboards-management": "Увімкнути керування панелями візуалізації", + "open-details-on": "Відкрити деталі сутності по", + "select-existing": "Виберіть існуючу групу сутностей", + "create-new": "Створити нову групу сутностей", + "new-entity-group-name": "Нове ім'я групи сутностей", + "entity-group-list": "Список групи сутностей", + "entity-group-list-empty": "Не вибрано жодної групи сутностей.", + "name-starts-with": "Назва групи сутностей починається з", + "entity-group-name-filter-required": "Необхідно задати назву групи сутностей." + }, + "entity-view": { + "entity-view": "Представлення сутності", + "entity-view-required": "Необхідно вказати представлення сутності.", + "entity-views": "Представлення сутностей", + "management": "Керування представленням сутностей", + "view-entity-views": "Переглянути представлення сутностей", + "entity-view-alias": "Псевдонім представлення сутності", + "aliases": "Псевдоніми представлення сутності", + "no-alias-matching": "Псевдонім'{{alias}}' не знайдено.", + "no-aliases-found": "Псевдоніми не знайдено.", + "no-key-matching": "'Ключ {{key}}' не знайдено.", + "no-keys-found": "Ключі не знайдено.", + "create-new-alias": "Створити новий!", + "create-new-key": "Створити новий!", + "duplicate-alias-error": "Псевдонім з такою назвою вже існує '{{alias}}'.
    Псевдоніми представлення повинні бути унікальними на панелі візуалізації.", + "configure-alias": "Налаштувати псевдонім '{{alias}}'", + "no-entity-views-matching": "Сутності, які відповідають '{{entity}}' не знайдені.", + "alias": "Псевдонім", + "alias-required": "Необхідно вказати псевдонім представлення сутності.", + "remove-alias": "Видалити псевдонім представлення сутності", + "add-alias": "Додати псевдонім представлення сутності", + "name-starts-with": "Ім'я представлення сутності починається з", + "entity-view-list": "Список представленнь сутності", + "use-entity-view-name-filter": "Використати фільтр", + "entity-view-list-empty": "Не вибрано жодного представлення сутності.", + "entity-view-name-filter-required": "Необхідно вказвти фільтр назв представлення сутності.", + "entity-view-name-filter-no-entity-view-matched": "Представлення сутностей, назви яких починаються з '{{entityView}}' не знайдено.", + "add": "Додати представлення сутності", + "assign-to-customer": "Призначити клієнту", + "assign-entity-view-to-customer": "Призначити представлення сутності(ей) клієнту", + "assign-entity-view-to-customer-text": "Будь ласка, виберіть представлення сутності для призначення клієнту", + "no-entity-views-text": "Представлення сутності не знайдено", + "assign-to-customer-text": "Будь ласка, виберіть клієнта, для призначиення представлення(ь) сутності(ей)", + "entity-view-details": "Деталі представлення сутності", + "add-entity-view-text": "Додати нове представлення сутності", + "delete": "Видалити представлення сутності", + "assign-entity-views": "Призначити представлення сутності", + "assign-entity-views-text": "Призначити { count, plural, 1 {1 представлення сутності} other {# представлення сутностей } } клієнту", + "delete-entity-views": "Видалити представлення сутностей", + "unassign-from-customer": "Відкликати у клієнта", + "unassign-entity-views": "Відкликати представлення сутностей", + "unassign-entity-views-action-title": "Відкликати { count, plural, 1 {1 представлення сутності} other {# представлень сутностей} } у клієнта", + "assign-new-entity-view": "Призначити нове представлення сутності", + "delete-entity-view-title": "Ви впевнені, що хочете видалити представлення сутності'{{entityViewName}}'?", + "delete-entity-view-text": "Будьте обережні, після підтвердження, представлення сутності та всі пов'язані з ним дані стануть недоступними.", + "delete-entity-views-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 представлення сутності } other {# представлення сутностей } }?", + "delete-entity-views-action-title": "Видалити { count, plural, 1 {1 представлення сутності } other {# представлення сутностей } }", + "delete-entity-views-text": "Будьте обережні, після підтвердження, всі виділені представлення сутностей та дні, пов'язані з ними стануть недоступними.", + "unassign-entity-view-title": "Ви впевнені, що хочете відкликати представлення сутності '{{entityViewName}}'?", + "unassign-entity-view-text": "Після підтвердження представлення сутності буде відкликане у клієнта. Дані представлення сутності не будуть доступні клієнту.", + "unassign-entity-view": "Відкликати представлення сутності", + "unassign-entity-views-title": "Ви впевнені, що хочете відкликати { count, plural, 1 {1 представлення сутності} other {# представлень сутностей} }?", + "unassign-entity-views-text": "Після підтвердження, клієнта буде позбавлено всіх виділених представлень сутностей. Дані представлень сутностей не будуть доступні клієнту.", + "entity-view-type": "Тип представлення сутності", + "entity-view-type-required": "Необхідно вказати тип представлення сутності.", + "select-entity-view-type": "Виберіть тип представлення сутності", + "enter-entity-view-type": "Введіть тип представлення сутності", + "any-entity-view": "Будь-яке представлення сутності", + "no-entity-view-types-matching": "Не знайдено жодних типів представлення сутності, що відповідають '{{entitySubtype}}'.", + "entity-view-type-list-empty": "Не вибрано тип представлення сутності.", + "entity-view-types": "Типи представлення сутності", + "name": "Ім'я", + "name-required": "Необхідно вказати ім'я.", + "description": "Опис", + "events": "Події", + "details": "Деталі", + "copyId": "Скопіювати Id представлення сутності", + "assignedToCustomer": "Призначений клієнту", + "unable-entity-view-device-alias-title": "Неможливо видалити псевдонім представлення сутності", + "unable-entity-view-device-alias-text": "Не вдалося видалити псевдонім пристрою'{{entityViewAlias}}', так як він використовується наступним(и) віджетом(ами):
    {{widgetsList}}", + "select-entity-view": "Вибрати представлення сутності", + "make-public": "Зробити представлення сутності публічним", + "make-private": "Зробити представлення сутності приватним", + "start-date": "Дата початку", + "start-ts": "Час початку", + "end-date": "Дата закінчення", + "end-ts": "Час завершення", + "date-limits": "Обмеження дати", + "client-attributes": "Атрибути клієнта", + "shared-attributes": "Спільні атрибути", + "server-attributes": "Атрибути сервера", + "timeseries": "Телеметрія", + "client-attributes-placeholder": "Атрибути клієнта", + "shared-attributes-placeholder": "Спільні атрибути", + "server-attributes-placeholder": "Атрибути сервера", + "timeseries-placeholder": "Телеметрія", + "target-entity": "Цільова сутність", + "attributes-propagation": "Поширення атрибутів", + "attributes-propagation-hint": "Представлення сутності автоматично копіюватиме вказані атрибути з цільової сутності кожного разу, коли ви зберігаєте або оновлюєте його. В цілях продуктивності, атрибути цільової сутності не поширюються на представлення сутності при кожній зміні її атрибутів. Можна ввімкнути автоматичне поширення, налаштувавши правило \"copy to view\" у вашому ланцюжку правил і пов'язуючи його з повідомленнями типу \"Post attributes\" і \"Attributes Updated\"..", + "timeseries-data": "Дані телеметрії", + "timeseries-data-hint": "Налаштуйте ключі даних телеметрії цільової сутності, які будуть доступні представленню сутності. Ці дані доступні лише для читання.", + "make-public-entity-view-title": "Ви впевнені, що бажаєте зробити представлення сутності '{{entityViewName}}' публічним?", + "make-public-entity-view-text": "Після підтвердження представлення сутності і всі пов'язані з ним дані стануть публічними і будуть доступні для інших користувачів.", + "make-private-entity-view-title": "Ви впевнені, що бажаєте зробити представлення сутності '{{entityViewName}}' приватним?", + "make-private-entity-view-text": "Після підтвердження представлення сутності і всі пов'язані з ним дані стануть приватними і не будуть доступні для інших користувачів." + }, + "event": { + "events": "Події", + "event-type": "Тип події", + "type-error": "Помилка", + "type-lc-event": "Подія життєвого циклу", + "type-stats": "Статистика", + "type-debug-converter": "Налагоджувати", + "type-debug-integration": "Налагоджувати", + "type-debug-rule-node": "Налагоджувати", + "type-debug-rule-chain": "Налагоджувати", + "no-events-prompt": "Не знайдено жодних подій", + "error": "Помилка", + "alarm": "Сигнал тривоги", + "event-time": "Час події", + "server": "Сервер", + "body": "Тіло", + "method": "Метод", + "type": "Тип", + "in": "In", + "out": "Out", + "metadata": "Метадані", + "message": "Повідомлення", + "entity": "Сутність", + "message-id": "Id повідомлення", + "message-type": "Тип повідомлення", + "data-type": "Тип даних", + "relation-type": "Тип зв'язку", + "data": "Дані", + "event": "Подія", + "status": "Статус", + "success": "Успіх", + "failed": "Невдача", + "messages-processed": "Повідомлення опрацьовані", + "errors-occurred": "Виникли помилки" + }, + "extension": { + "extensions": "Розширення", + "selected-extensions": " вибрано { count, plural, 1 {1 розширення} other {# розширення} }", + "type": "Тип", + "key": "Ключ", + "value": "Значення", + "id": "Id", + "extension-id": "Id розширення", + "extension-type": "Тип розширення", + "transformer-json": "JSON *", + "unique-id-required": "Таке Id розширення вже існує.", + "delete": "Видалити розширення", + "add": "Додати розширення", + "edit": "Редагувати розширення", + "delete-extension-title": "Ви дійсно бажаєте видалити розширення '{{extensionId}}'?", + "delete-extension-text": "Будьте обережні, після підтвердження, розширення та всі пов'язані з ним дані стануть недоступними.", + "delete-extensions-title": "Ви дійсно бажаєте видалити { count, plural, 1 {1 розширення} other {# розширення} }?", + "delete-extensions-text": "Будьте обережні, після підтвердження, всі вибрані розширення будуть видалені.", + "converters": "Перетворювачі", + "converter-id": "Id перетворювача", + "configuration": "Конфігурація", + "converter-configurations": "Конфігурації перетворювача", + "token": "Маркер безпеки", + "add-converter": "Додати конвертер", + "add-config": "Додати конфігурацію конвертера", + "device-name-expression": "Маска імені пристрою", + "device-type-expression": "Маска типу пристрою", + "custom": "Користувач", + "to-double": "Подвоїти", + "transformer": "Трансформатор", + "json-required": "Необхідно вказати json трансформатора.", + "json-parse": "Неможливо проаналізувати json трансформатора.", + "attributes": "Атрибути", + "add-attribute": "Додати атрибут", + "add-map": "Додати елемент відображення", + "timeseries": "Телеметрія", + "add-timeseries": "Додати параметри телеметрії", + "field-required": "Field is required", + "brokers": "Брокери", + "add-broker": "Додати брокера", + "host": "Хост", + "port": "Порт", + "port-range": "Значення порту має бути в діапазоні від 1 до 65535.", + "ssl": "Ssl", + "credentials": "Авторизаційні дані", + "username": "Ім'я користувача", + "password": "Пароль", + "retry-interval": "Інтервал повтору в мілісекундах", + "anonymous": "Анонімний", + "basic": "Основний", + "pem": "PEM", + "ca-cert": "Файл CA сертифіката *", + "private-key": "Файл приватного ключа *", + "cert": "Файл сертифіката *", + "no-file": "Не вибрано жодного файлу.", + "drop-file": "Перетягніть файл, або клацніть, щоб вибрати файл для завантаження.", + "mapping": "Зіставлення", + "topic-filter": "Фільтр тем", + "converter-type": "Тип конвертера", + "converter-json": "Json", + "json-name-expression": " Json вираз для назви пристрою", + "topic-name-expression": "Вираз для назви пристрою в назві теми", + "json-type-expression": "Json вираз для типу пристрою", + "topic-type-expression": "Вираз для типу пристрою в назві теми", + "attribute-key-expression": "Вираз для ключа атрибута", + "attr-json-key-expression": " Json вираз для ключа атрибута", + "attr-topic-key-expression": "Вираз для ключа атрибута в назві теми", + "request-id-expression": "Вираз для id запиту", + "request-id-json-expression": "Json вираз для id запиту", + "request-id-topic-expression": "Вираз для id запиту в назві теми", + "response-topic-expression": "Вираз для теми відповідей", + "value-expression": "Вираз для значення", + "topic": "Тема", + "timeout": "Час очікування в мілісекундах", + "converter-json-required": "Необхідно вказати json конвертер.", + "converter-json-parse": "Неможливо проаналізувати json конвертера.", + "filter-expression": "Вираз для фільтра", + "connect-requests": "Запити на підключення", + "add-connect-request": "Додати запит на підключення", + "disconnect-requests": "Відключення запитів", + "add-disconnect-request": "Додати запит на відключення", + "attribute-requests": "Запити атрибутів", + "add-attribute-request": "Додати запит атрибута", + "attribute-updates": "Оновлення атрибутів", + "add-attribute-update": "Додати оновлення атрибутів", + "server-side-rpc": "Серверна сторона RPC", + "add-server-side-rpc-request": "Додати RPC-запит на стороні сервера", + "device-name-filter": "Фільтр назви пристрою", + "attribute-filter": "Фільтр атрибутів", + "method-filter": "Фільтр методів", + "request-topic-expression": "Вираз для теми запитів", + "response-timeout": "Час очікування відповіді в мілісекундах", + "topic-expression": "Вираз для назви теми", + "client-scope": "Обсяг клієнта", + "add-device": "Додати пристрій", + "opc-server": "Сервери", + "opc-add-server": "Додати сервер", + "opc-add-server-prompt": "Будь ласка, додайте сервер", + "opc-application-name": "Назва програми", + "opc-application-uri": "URI програми", + "opc-scan-period-in-seconds": "Період сканування в секундах", + "opc-security": "Безпека", + "opc-identity": "Ідентифікація", + "opc-keystore": "Сховище ключів", + "opc-type": "Тип", + "opc-keystore-type": "Тип", + "opc-keystore-location": "Розташування *", + "opc-keystore-password": "Пароль", + "opc-keystore-alias": "Псевдонім", + "opc-keystore-key-password": "Пароль для ключа", + "opc-device-node-pattern": "Патерн OPC вузла пристрою", + "opc-device-name-pattern": "Патерн назви пристрою", + "modbus-server": "Сервери/ведені пристрої", + "modbus-add-server": "Додати сервер/ведений пристрій", + "modbus-add-server-prompt": "Будь ласка, додайте сервер/ведений пристрій", + "modbus-transport": "Транспорт", + "modbus-tcp-reconnect": "Перепідключатися автоматично", + "modbus-port-name": "Ім'я послідовного порту", + "modbus-encoding": "Кодування", + "modbus-parity": "Паритет", + "modbus-baudrate": "Швидкість передачі даних", + "modbus-databits": "Біти даних", + "modbus-stopbits": "Стоп-біти", + "modbus-databits-range": "Біти даних повинні знаходитися в діапазоні від 7 до 8.", + "modbus-stopbits-range": "Стоп-біти повинні знаходитися в діапазоні від 1 до 2.", + "modbus-unit-id": "Unit ID", + "modbus-unit-id-range": "Unit ID should be in a range from 1 to 247.", + "modbus-device-name": "Ім'я пристрою", + "modbus-poll-period": "Період опитування (мс)", + "modbus-attributes-poll-period": "Період опитування атрибутів (мс)", + "modbus-timeseries-poll-period": "Період опитування телеметрії (мс)", + "modbus-poll-period-range": "Період опитування повинен бути більше 0.", + "modbus-tag": "Тег", + "modbus-function": "Modbus функція", + "modbus-register-address": "Адреса регістру ", + "modbus-register-address-range": "Адреса регістру повинна бути в діапазоні від 0 до 65535.", + "modbus-register-bit-index": "Номер бітів", + "modbus-register-bit-index-range": "Номер бітів повинен знаходитися в діапазоні від 0 до 15.", + "modbus-register-count": "Рахунок регістру", + "modbus-register-count-range": "Рахунок регістру повинен бути більше 0.", + "modbus-byte-order": "Порядок байтів", + "sync": { + "status": "Статус", + "sync": "Синхронізований", + "not-sync": "Не синхронізований", + "last-sync-time": "Час останньої синхронізації", + "not-available": "Недоступний" + }, + "export-extensions-configuration": "Експортувати конфігурацію розширень", + "import-extensions-configuration": "Імпортувати конфігурацію розширень", + "import-extensions": "Імпортувати розширення", + "import-extension": "Імпортувати розширення", + "export-extension": "Експортувати розширення", + "file": "Файл розширень", + "invalid-file-error": "Не правильний формат файла" + }, + "fullscreen": { + "expand": "Відкрити у повноекранному режимі", + "exit": "Вийти з повноекранного режиму", + "toggle": "Перемкнути повноекранний режим", + "fullscreen": "Повноекранний режим" + }, + "function": { + "function": "Функція" + }, + "grid": { + "delete-item-title": "Ви впенені, що хочете видалити цей елемент?", + "delete-item-text": "Будьте обережні, після підтвердження, цей елемент і всі пов'язані з ним дані, стануть недоступними.", + "delete-items-title": "Ви впенені, що хочете видалити { count, plural, 1 {1 елемент} other {# елементи} }?", + "delete-items-action-title": "Видалити{ count, plural, 1 {1 елемент} other {# елементи} }", + "delete-items-text": "Будьте обережні, після підтвердження, всі виділені елементи і пов'язані з ними дані, стануть недоступними.", + "add-item-text": "Додати новий елемент", + "no-items-text": "Не знайдено жодного елемента", + "item-details": "Деталі елемента", + "delete-item": "Видалити елемент", + "delete-items": "Видалити елементи", + "scroll-to-top": "Перейти угору" + }, + "help": { + "goto-help-page": "Перейти на сторінку довідки" + }, + "home": { + "home": "Домашня сторінка", + "profile": "Профіль", + "logout": "Вийти", + "menu": "Меню", + "avatar": "Аватар", + "open-user-menu": "Відкрити меню користувача" + }, + "import": { + "no-file": "Не вибрано жодного файлу", + "drop-file": "Перетягніть JSON файл, або клацніть, щоб вибрати файл для завантаження.", + "drop-csv-file": "Перетягніть CSV файл, або клацніть, щоб вибрати файл для завантаження.", + "column-value": "Значення", + "column-title": "Назва", + "column-example": "Приклад значень даних", + "column-key": "Ключ атрибута/телеметрії", + "csv-delimiter": "Розділювач в CSV файлі", + "csv-first-line-header": "Перший рядок містить назви колонок", + "csv-update-data": "Оновити атрибути/телеметрію", + "import-csv-number-columns-error": "Файл має містити як мінімум дві колонки", + "import-csv-invalid-format-error": "Невірний формат даних. Рядок: '{{line}}'", + "column-type": { + "name": "Назва", + "type": "Тип", + "label": "Мітка", + "column-type": "Тип колонки", + "client-attribute": "Атрибут клієнта", + "shared-attribute": "Спільний атрибут", + "server-attribute": "Атрибут сервера", + "timeseries": "Телеметрія", + "entity-field": "Entity field", + "access-token": "Токен" + }, + "stepper-text": { + "select-file": "Виберіть файл", + "configuration": "Конфігурація імпорту", + "column-type": "Виберіть тип колонок", + "creat-entities": "Створення нових сутностей", + "done": "Завершено" + }, + "message": { + "create-entities": "{{count}} нову(их) сутність(ей) успішно створено.", + "update-entities": "{{count}} сутність(ей) успішно оновлено.", + "error-entities": "Виникла помилка при створенні {{count}} сутності(ей)." + } + }, + "integration": { + "integration": "Інтеграція", + "integrations": "Інтеграції", + "select-integration": "Виберіть інтеграцію", + "no-integrations-matching": "Не знайдено жодних інтеграцій, які відповідають '{{entity}}'.", + "integration-required": "Необхідно вказати інтеграцію", + "delete": "Видалити інтеграцію", + "management": "Управління інтеграціями", + "add-integration-text": "Додати нову інтеграцію", + "no-integrations-text": "Не знайдено жодної інтеграції", + "selected-integrations": "{ count, plural, 1 {1 інтеграція} other {# інтеграції} } вибрано", + "delete-integration-title": "Ви впевнені, що хочете видалити інтеграцію '{{integrationName}}'?", + "delete-integration-text": "Будьте обережні, після підтвердження інтеграція та всі пов'язані з нею дані стануть недоступними.", + "delete-integrations-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 інтеграцію} other {# інтеграції} }?", + "delete-integrations-action-title": "Видалити { count, plural, 1 {1 інтеграцію} other {# інтеграції} }", + "delete-integrations-text": "Будьте обережні, після підтвердження всі вибрані інтеграції будуть видалені, і всі пов'язані з ними дані стануть недоступними.", + "events": "Події", + "add": "Додати інтеграцію", + "integration-details": "Деталі інтеграції", + "details": "Деталі", + "copyId": "Копіювати Id інтеграції", + "idCopiedMessage": "Id інтеграції скопійовано в буфер обміну", + "debug-mode": "Режим налагодження", + "enable-security": "Увімкнути безпеку", + "headers-filter": "Заголовки фільтра", + "header": "Заголовок", + "no-headers-filter": "Немає фільтрів заголовків", + "downlink-url": "Downlink URL", + "application-uri": "URI програми", + "as-id": "AS ID", + "as-id-required": "Необхідно вказати AS ID.", + "as-key": "AS ключ", + "as-key-required": "Необхідно вказати AS ключ.", + "max-time-diff-in-seconds": "Максимальна різниця в часі (секунди)", + "max-time-diff-in-seconds-required": "Необхідно вказати максимальну різницю в часі.", + "name": "Ім'я", + "name-required": "Необхідно вказати ім'я.", + "description": "Опис", + "base-url": "Базова URL-адреса", + "base-url-required": "Необхідно вказати базову URL-адресу", + "security-key": "Ключ захисту", + "http-endpoint": "URL кінцевої точки HTTP", + "copy-http-endpoint-url": "Скопіювати URL-адресу кінцевої точки HTTP", + "http-endpoint-url-copied-message": "URL кінцевої точки HTTP скопійовано в буфер обміну", + "host": "Хост", + "host-required": "Необхідно вказати хост.", + "host-type": "Тип хоста", + "host-type-required": "Необхідно вказати тип хоста.", + "custom-host": "Хост користувача", + "custom-host-required": "Необхідний спеціальний хост.", + "port": "Порт", + "port-required": "Необхідно вказати порт.", + "port-range": "Порт має бути в діапазоні від 1 до 65535.", + "connect-timeout": "Час очікування з'єднання (сек)", + "connect-timeout-required": " Необхідно вказати час з'єднання підключення.", + "connect-timeout-range": "Час очікування з'єднання має бути в діапазоні від 1 до 200.", + "client-id": "ID клієнта", + "clean-session": "Очистити сеанс", + "enable-ssl": "Увімкнути SSL", + "credentials": "Авторизаційні дані", + "credentials-type": "Тип авторизаційних даних", + "credentials-type-required": "Необхідно вказати тип авторизаційних даних.", + "username": "Ім'я користувача", + "username-required": "Необхідно вказати ім'я користувача.", + "password": "Пароль", + "password-required": "Необхідно вказати пароль.", + "ca-cert": "Файл сертифіката CA *", + "private-key": "Файл приватного ключа *", + "private-key-password": "Пароль приватного ключа", + "cert": "Файл сертифіката*", + "no-file": "Не вибрано жодного файлу.", + "drop-file": "Перетягніть файл, або клацніть, щоб вибрати файл для завантаження.", + "topic-filters": "Тематичні фільтри", + "remove-topic-filter": "Видалити фільтр тем", + "add-topic-filter": "Додати фільтр тем", + "add-topic-filter-prompt": "Будь ласка, додайте фільтр тем", + "topic": "Тема", + "mqtt-qos": "QoS", + "mqtt-qos-at-most-once": "Не більше одного разу", + "mqtt-qos-at-least-once": "Принаймні, один раз", + "mqtt-qos-exactly-once": "Точно один раз", + "downlink-topic-pattern": "Downlink topic pattern", + "downlink-topic-pattern-required": "Downlink topic pattern is required.", + "aws-iot-endpoint": "AWS IoT Endpoint", + "aws-iot-endpoint-required": "AWS IoT Endpoint is required.", + "aws-iot-credentials": "Авторизаційні дані AWS IoT", + "application-credentials": "Авторизаційні дані додатків", + "api-key": "API ключ", + "api-key-required": "Необхідно вказати API ключ.", + "auth-token": "Маркер аутентифікації", + "auth-token-required": "Необхідно вказати маркер аутентифікації.", + "region": "Регіон", + "region-required": "Необхідно вказати регіон.", + "application-id": "ID програми", + "application-id-required": "Необхідно вказати ID програми.", + "access-key": "Ключ доступу", + "access-key-required": "Необхідно вказати ключ доступу.", + "connection-parameters": "Параметри підключення", + "service-bus-namespace-name": "Service Bus Namespace Name", + "service-bus-namespace-name-required": "Необхідно вказати Service Bus Namespace Name is required.", + "event-hub-name": "Event Hub Name", + "event-hub-name-required": "Необхідно вказати Event Hub Name is required.", + "sas-key-name": "Назва ключа SAS", + "sas-key-name-required": "Необхідно вказати назву ключа SAS.", + "sas-key": "Ключ SAS", + "sas-key-required": "SAS Key is required.", + "iot-hub-name": "IoT Hub Name (required for downlink)", + "metadata": "Метадані", + "type": "Тип", + "type-required": "Необхідно вказати тип.", + "uplink-converter": "Конвертер передачі даних", + "uplink-converter-required": "Необхідно вказати конвертер передачі даних.", + "downlink-converter": "Downlink data converter", + "type-http": "HTTP", + "type-ocean-connect": "OceanConnect", + "type-sigfox": "SigFox", + "type-thingpark": "ThingPark", + "type-tmobile-iot-cdp": "T-Mobile – IoT CDP", + "type-mqtt": "MQTT", + "type-aws-iot": "AWS IoT", + "type-ibm-watson-iot": "IBM Watson IoT", + "type-ttn": "TheThingsNetwork", + "type-azure-event-hub": "Azure Event Hub", + "type-opc-ua": "OPC-UA", + "type-ffb": "FFB", + "opc-ua-application-name": "Назва програми", + "opc-ua-application-uri": "Application uri", + "opc-ua-scan-period-in-seconds": "Період сканування в секундах", + "opc-ua-scan-period-in-seconds-required": "Необхідно вказати період сканування в секундах", + "opc-ua-timeout": "Час очікування в мілісекундах", + "opc-ua-timeout-required": "Необхідно вказати час очікування в мілісекундах", + "opc-ua-security": "Безпека", + "opc-ua-security-required": "Необхідно задати безпеку", + "opc-ua-identity": "Ідентифікація", + "opc-ua-identity-required": "Необхідно вказати ідентифікацію", + "opc-ua-keystore": "Сховище ключів", + "add-opc-ua-keystore-prompt": "Будь ласка, додайте файл сховища ключів", + "opc-ua-keystore-required": "Необхідно вказати сховище ключів", + "opc-ua-type": "Тип", + "opc-ua-keystore-type": "Тип сховища ключів", + "opc-ua-keystore-type-required": "Необхідно вказати тип", + "opc-ua-keystore-location": "Розташування *", + "opc-ua-keystore-password": "Пароль", + "opc-ua-keystore-password-required": "Необхідно вказати пароль", + "opc-ua-keystore-alias": "Псевдонім", + "opc-ua-keystore-alias-required": "Необхідно вказати псевдонім", + "opc-ua-keystore-key-password": "Пароль ключа", + "opc-ua-keystore-key-password-required": "Необхідно вказати пароль ключа", + "opc-ua-mapping": "Зіставлення", + "add-opc-ua-mapping-prompt": "Будь ласка, додайте зіставлення", + "opc-ua-mapping-type": "Тип зіставлення", + "opc-ua-mapping-type-required": "Необхідно вказати тип зіставлення", + "opc-ua-device-node-pattern": "Шаблон вузла пристрою", + "opc-ua-device-node-pattern-required": "Необхідно вказати шаблон вузла пристрою", + "opc-ua-add-map": "Додати елемент зіставлення", + "subscription-tags": "Теги передплати Теги підписки", + "remove-subscription-tag": "Видалити тег підписки", + "add-subscription-tag": "Додати тег підписки", + "add-subscription-tag-prompt": "Будь ласка, додайте тег підписки", + "key": "Ключ", + "path": "Шлях", + "required": "Необхідно" + }, + "item": { + "selected": "Вибрані" + }, + "js-func": { + "no-return-error": "Функція повинна повертати значення!", + "return-type-mismatch": "Функція повинна повернути значення типу '{{type}}'!" + }, + "key-val": { + "key": "Ключ", + "value": "Значення", + "remove-entry": "Видалити елемент", + "add-entry": "Додати елемент", + "no-data": "Елементи відсутні" + }, + "layout": { + "layout": "Макет", + "manage": "Керування макетами", + "settings": "Налаштування макета", + "color": "Колір", + "main": "Основний", + "right": "Правий", + "select": "Вибрати макет" + }, + "legend": { + "direction": "Розташування елементів легенди", + "position": "Розташування легенди", + "show-max": "Показати максимальне значення", + "show-min": "Показати мінімальне значення ", + "show-avg": "Показати середнє значення", + "show-total": "Показати суму", + "settings": "Налаштування легенди", + "min": "мін", + "max": "макс", + "avg": "середнє", + "total": "Сума", + "comparison-time-ago": { + "days": "(день тому)", + "weeks": "(тиждень тому)", + "months": "(місяць тому)", + "years": "(рік тому)" + } + }, + "login": { + "login": "Увійти", + "request-password-reset": "Запит скидання пароля", + "reset-password": "Скинути пароль", + "create-password": "Створити пароль", + "passwords-mismatch-error": "Введені паролі повинні бути однаковими!", + "password-again": "Введіть пароль ще раз", + "sign-in": "Будь ласка, увійдіть в систему", + "username": "Ім'я користувача (ел. пошта)", + "remember-me": "Запам'ятати мене", + "forgot-password": "Забули пароль?", + "password-reset": "Скидання пароля", + "expired-password-reset-message": "Термін дії Вашого паролю закінчився! Будь ласка, створіть новий пароль.", + "new-password": "Новий пароль", + "new-password-again": "Повторіть новий пароль", + "password-link-sent-message": "Посилання для скидання пароля було успішно надіслано!", + "email": "Електронна пошта", + "login-with": "Увійти через {{name}}", + "or": "або" + }, + "position": { + "top": "Угорі", + "bottom": "Знизу", + "left": "Ліворуч", + "right": "Праворуч" + }, + "profile": { + "profile": "Профіль", + "last-login-time": "Час останнього входу", + "change-password": "Змінити пароль", + "current-password": "Поточний пароль" + }, + "relation": { + "relations": "Відношення", + "direction": "Напрямок", + "search-direction": { + "FROM": "З", + "TO": "До" + }, + "direction-type": { + "FROM": "з", + "TO": "до" + }, + "from-relations": "Вихідні відношення", + "to-relations": "Вхідні відношення", + "selected-relations": "Вибрано { count, plural, 1 {1 відношення} other {# відношення} }", + "type": "Тип", + "to-entity-type": "До типу сутності", + "to-entity-name": "До імені сутності", + "from-entity-type": "Від типу сутності", + "from-entity-name": "Від імені сутності", + "to-entity": "До сутності", + "from-entity": "Від сутності", + "delete": "Видалити відношення", + "relation-type": "Тип відношення", + "relation-type-required": "Необхідно вказати тип відношення.", + "any-relation-type": "Будь-який тип", + "add": "Додати відношення", + "edit": "Редагувати відношення", + "delete-to-relation-title": "Ви впевнені, що хочете видалити відношення до сутності '{{entityName}}'?", + "delete-to-relation-text": "Будьте обережні, після підтвердження, сутність '{{entityName}}' не буде пов'язана з поточним об'єктом.", + "delete-to-relations-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 відношення} other {# відношення} }?", + "delete-to-relations-text": "Будьте обережні, після підтвердження, всі вибрані відношення стануть не пов'язані з поточною сутністю.", + "delete-from-relation-title": "Ви впевнені, що хочете видалити зв'язок, який йде від сутності '{{entityName}}'?", + "delete-from-relation-text": "Будьте обережні, після підтвердження поточна сутність буде відв'язана від сутності '{{entityName}}'.", + "delete-from-relations-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 відношення} other {# відношення} }?", + "delete-from-relations-text": "Будьте обережні, після підтвердження, всі вибрані відношення будуть видалені, а поточна сутність стане не зв'язаною з відповідними сутностями.", + "remove-relation-filter": "Видалити фільтр відношення", + "add-relation-filter": "Додати фільтр відношення", + "any-relation": "Будь-яке відношення", + "relation-filters": "Фільтри відношення", + "additional-info": "Додаткова інформація (JSON)", + "invalid-additional-info": "Не вдалося розібрати JSON з додатковою інформацією ." + }, + "rulechain": { + "rulechain": "Ланцюг правил", + "rulechains": "Ланцюги правил", + "root": "Основний", + "delete": "Видалити ланцюг правил", + "name": "Ім'я", + "name-required": "Необхідно вказати ім'я.", + "description": "Опис", + "add": "Додати ланцюг правил", + "set-root": "Зробити ланцюг правил основним", + "set-root-rulechain-title": "Ви впевнені, що хочете зробити ланцюг правил '{{ruleChainName}}' основним?", + "set-root-rulechain-text": "Після підтвердження ланцюг правил стане основним і буде обробляти всі вхідні транспортні повідомлення.", + "delete-rulechain-title": "Ви впевнені, що хочете видалити ланцюг правил '{{ruleChainName}}'?", + "delete-rulechain-text": "Будьте обережні, після підтвердження ланцюг правил і всі пов'язані з ним дані стануть недоступними.", + "delete-rulechains-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 ланцюг правил} other {# ланцюги правил} }?", + "delete-rulechains-action-title": "Видалити{ count, plural, 1 {1 ланцюг правил} other {# ланцюги правил} }", + "delete-rulechains-text": "Будьте обережні, після підтвердження, вибрані ланцюги правил і всі пов'язані з ними дані стануть недоступними.", + "add-rulechain-text": "Додати новий ланцюг правил", + "no-rulechains-text": "Ланцюг правил не знайдено", + "rulechain-details": "Деталі ланцюга правил", + "details": "Деталі", + "events": "Події", + "system": "Система", + "import": "Імпортувати ланцюг правил", + "export": "Експортувати ланцюг правил", + "export-failed-error": "Не вдалося експортувати ланцюг правил: {{error}}", + "create-new-rulechain": "Створити новий ланцюг правил", + "rulechain-file": "Файл ланцюга правил", + "invalid-rulechain-file-error": "Неможливо імпортувати ланцюг правил: недійсна структуру даних ланцюга правил.", + "copyId": "Копіювати Id ланцюга правил", + "idCopiedMessage": "Id ланцюга правил скопійовано в буфер обміну", + "select-rulechain": "Вибрати ланцюг правил", + "no-rulechains-matching": "Не знайдено жодних ланцюгів правил, які відповідають '{{entity}}'.", + "rulechain-required": "Необхідно вказати ланцюг правил", + "management": "Управління ланцюгами правил", + "debug-mode": "Режим налагодження" + }, + "rulenode": { + "details": "Деталі", + "events": "Події", + "search": "Пошук вузлів", + "open-node-library": "Відкрити бібліотеку вузлів", + "add": "Додати вузол правил", + "name": "Ім'я", + "name-required": "Необхідно вказати ім'я.", + "type": "Тип", + "description": "Опис", + "delete": "Видалити вузол правил", + "select-all-objects": "Вибрати усі вузли та з'єднання", + "deselect-all-objects": "Зняти виділення з усіх вузлів і з'єднань", + "delete-selected-objects": "Видалити вибрані вузли та з'єднання", + "delete-selected": "Видалити вибране", + "select-all": "Вибрати все", + "copy-selected": "Копіювати вибране", + "deselect-all": "Відмінити вибране", + "rulenode-details": "Деталі вузла правил", + "debug-mode": "Режим налагодження", + "configuration": "Конфігурація", + "link": "Посилання", + "link-details": "Деталі посилання про вузол правил", + "add-link": "Додати посилання", + "link-label": "Мітка посилання", + "link-label-required": "Необхідно вказати мітку посилання.", + "custom-link-label": "Мітка посилання користувача", + "custom-link-label-required": "Необхідно вказати мітку посилання користувача.", + "link-labels": "Мітки посилання", + "link-labels-required": "Необхідно вказати мітки посилання.", + "no-link-labels-found": "Не знайдено жодних міток посилання", + "no-link-label-matching": "Мітка'{{label}}' не знайдена.", + "create-new-link-label": "Створити нову!", + "type-filter": "Фільтр", + "type-filter-details": "Фільтрувати вхідні повідомлення з заданими умовами", + "type-enrichment": "Насичення", + "type-enrichment-details": "Додати додаткову інформацію до метаданих повідомлень", + "type-transformation": "Трансформація", + "type-transformation-details": "Змінити склад повідомлення та його метадані", + "type-action": "Дія", + "type-action-details": "Виконати задану дію", + "type-analytics": "Аналітика", + "type-analytics-details": "Виконує аналіз потокових або збережених даних", + "type-external": "Зовнішній", + "type-external-details": "Взаємодіє з зовнішньою системою", + "type-rule-chain": "Ланцюг правил", + "type-rule-chain-details": "Перенаправити вхідне повідомлення на вказаний ланцюг правил", + "type-input": "Вхід", + "type-input-details": "Логічний вхід ланцюга правил, перенаправляє вхідні повідомлення на наступний пов'язаний вузол правил", + "type-unknown": "Невідомий", + "type-unknown-details": "Невизначений вузол правил", + "directive-is-not-loaded": "Вказана директива конфігурації '{{directiveName}}' недоступна.", + "ui-resources-load-error": "Не вдалося завантажити UI ресурси.", + "invalid-target-rulechain": "Не вдається визначити цільовий ланцюг правил!", + "test-script-function": "Протестувати скрипт", + "message": "Повідомлення", + "message-type": "Тип повідомлення", + "select-message-type": "Вибрати тип повідомлення", + "message-type-required": "Необхідно вказати тип повідомлення", + "metadata": "Метадані", + "metadata-required": "Записи метаданих не можуть бути порожніми.", + "output": "Вихід", + "test": "Тест", + "help": "Допомога", + "reset-debug-mode": "Вимкнути режим налогодження у всіх правилах" + }, + "scheduler": { + "scheduler": "Планувальник", + "scheduler-event": "Подія планувальника", + "select-scheduler-event": "Вибрати подію", + "no-scheduler-events-matching": "Не знайдено жодних подій, які відповідають '{{entity}}'.", + "scheduler-event-required": "Необхвдно вказати заплановану подію", + "management": "Управління планувальником", + "scheduler-events": "Планування подій", + "add-scheduler-event": "Додати подію", + "search-scheduler-events": "Пошук події", + "created-time": "Час створення", + "name": "Ім'я", + "type": "Тип", + "created_customer": "Створено клієнтом", + "edit-scheduler-event": "Редагувати подію", + "delete-scheduler-event": "Видалити подію", + "no-scheduler-events": "Не знайдено жодних запланованих подій", + "selected-scheduler-events": "{ count, plural, 1 {1 запланована подія} other {# заплановані події} } вибрано", + "delete-scheduler-event-title": "Ви впевнені, що хочете видалити подію '{{schedulerEventName}}'?", + "delete-scheduler-event-text": "Будьте обережні, після підтвердження подія і всі пов'язані з нею дані стануть недоступними.", + "delete-scheduler-events-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 запланована подія} other {# заплановані події} }?", + "delete-scheduler-events-text": "Будьте обережні, після підтвердження всі вибрані події будуть видалені, і всі пов'язані з ними дані стануть недоступними.", + "create": "Створити подію планувальника", + "edit": "Змінити подію планувальника", + "name-required": "Необхідно задати ім'я", + "configuration": "Конфігурація", + "schedule": "Розклад", + "start": "Початок", + "date": "Дата", + "time": "Час", + "repeat": "Повтор", + "repeats": "Повтори", + "daily": "Щодня", + "weekly": "Щотижня", + "repeats-required": "Потрібно вказати повторення.", + "repeat-on": "Повторювати по", + "repeat-every": "Повторювати кожний(у)", + "ends-on": "Завершення", + "sunday-label": "Нд", + "monday-label": "Пн", + "tuesday-label": "Вт", + "wednesday-label": "Ср", + "thursday-label": "Чт", + "friday-label": "Пт", + "saturday-label": "Сб", + "repeat-on-sunday": "Повторити у неділю", + "repeat-on-monday": "Повторити в понеділок", + "repeat-on-tuesday": "Повторити у вівторок", + "repeat-on-wednesday": "Повторити в середу", + "repeat-on-thursday": "Повторити в червер", + "repeat-on-friday": "Повторити в п'ятницю", + "repeat-on-saturday": "Повторити в суботу", + "event-type": "Тип події", + "select-event-type": "Вибрати тип події", + "event-type-required": "Необхідно вказати типи події.", + "list-mode": "Перегляд списку", + "calendar-mode": "Перегляд календаря", + "calendar-view-type": "Тип перегляду календаря", + "month": "Місяць", + "week": "Тиждень", + "day": "День", + "agenda-week": "Порядок тижня", + "agenda-day": "Порялок дня", + "list-year": "Список року", + "list-month": "Список місяця", + "list-week": "Список тижня", + "list-day": "Список дня", + "today": "Сьогодні", + "navigate-before": "Перейти до", + "navigate-next": "Перейти далі", + "starting-from": "Починаючи з", + "until": "до", + "on": "в", + "sunday": "Неділя", + "monday": "Понеділок", + "tuesday": "Вівторок", + "wednesday": "Середа", + "thursday": "Четвер", + "friday": "П'ятниця", + "saturday": "Субота", + "originator": "Ініціатор", + "single-entity": "Єдина сутність", + "group-of-entities": "Група сутностей", + "single-device": "Один пристрій", + "group-of-devices": "Група пристроїв", + "message-body": "Текст повідомлення", + "target": "Ціль", + "rpc-method": "Метод", + "rpc-method-required": "Необхідно вказати метод", + "rpc-params": "Параметри", + "select-dashboard-state": "Виберіть стан панелі візуалізації", + "hours": "Години", + "minutes": "Хвилини", + "seconds": "Секунди", + "time-interval-required": "Необхідно вказати часовий інтервал", + "time-unit-required": "Необхідно вказати одиниці часу" + }, + "report": { + "report-config": "Конфігурація звіту", + "email-config": "Конфігурація електронної пошти", + "dashboard-state-param": "Значення параметра стану панелі візуалізації", + "base-url": "Базова URL-адреса", + "base-url-required": "Необхідно вказати базову URL-адресу.", + "use-dashboard-timewindow": "Використовуйте вікно часу на панелі інструментів", + "timewindow": "Вікно часу", + "name-pattern": "Шаблон імені звіту", + "name-pattern-required": "Необхідно задати шаблон назви звіту", + "type": "Report type", + "use-current-user-credentials": "Використовувати поточні авторизаційні дані користувача", + "customer-user-credentials": "Авторизаційні дані користувачів", + "customer-user-credentials-required": "Необхідно задати авторизаційні дані користувачів", + "generate-test-report": "Створити звіт про перевірку", + "send-email": "Відправити лист", + "from": "Від", + "from-required": "Необхідно вказати від кого.", + "to": "До", + "to-required": "Необхідно вказати до кого.", + "cc": "Cc", + "bcc": "Bcc", + "subject": "Тема", + "subject-required": "Необхідно вказати тему.", + "body": "Текст", + "body-required": "Лист не може бути пустим." + }, + "blob-entity": { + "blob-entity": "Blob сутності", + "select-blob-entity": "Вибрати blob сутності", + "no-blob-entities-matching": "Не знайдено жодних сутностей blob, які відповідають '{{entity}}'.", + "blob-entity-required": "Необхідно вказати blob сутності", + "files": "Файли", + "search": "Пошук файлів", + "clear-search": "Очистити пошук", + "no-blob-entities-prompt": "Файлів не знайдено", + "report": "Звіт", + "created-time": "Час створення", + "name": "Ім'я", + "type": "Тип", + "created_customer": "Створено клієнтом", + "download-blob-entity": "Завантажити файл", + "delete-blob-entity": "Видалити файл", + "delete-blob-entity-title": "Ви впевнені, що хочете видалити файл '{{blobEntityName}}'?", + "delete-blob-entity-text": "Будьте обережні, після підствердження, дані файлу стануть недоступними." + }, + "timezone": { + "timezone": "Часовий пояс", + "select-timezone": "Вибрати часовий пояс ", + "no-timezones-matching": "Не знайдено жодних часових поясів, які відповідають '{{timezone}}'.", + "timezone-required": "Необхідно вказати часовий пояс." + }, + "queue": { + "select_name": "Виберіть ім'я для Queue", + "name": "Iм'я для Queue", + "name_required": "Поле 'Имя для Queue' обязательно к заполнению!" + }, + "tenant": { + "tenant": "Власник", + "tenants": "Власники", + "management": "Управління власниками", + "add": "Додати власника", + "admins": "Адміністратори", + "manage-tenant-admins": "Керування адміністраторами власника", + "delete": "Видалити власника", + "add-tenant-text": "Додати нового власника", + "no-tenants-text": "Не знайдено жодного власника", + "tenant-details": "Подробиці про власника", + "delete-tenant-title": "Ви впевнені, що хочете видалити власника'{{tenantTitle}}'?", + "delete-tenant-text": "Будьте обережні, після підтвердження власник і всі пов'язані з ним дані стануть недоступними.", + "delete-tenants-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 власник} other {# власники} }?", + "delete-tenants-action-title": "Видалити { count, plural, 1 {1 власник} other {# власники} }", + "delete-tenants-text": "Будьте обережні, після підтвердження, усі вибрані власники будуть видалені, і всі пов'язані з ними дані стануть недоступними.", + "title": "Назва", + "title-required": "Необхідно вказати назву.", + "description": "Опис", + "details": "Деталі", + "events": "Події", + "copyId": "Копіювати Id власника", + "idCopiedMessage": "Id власника скопійовано в буфер обміну", + "select-tenant": "Вибрати власника", + "no-tenants-matching": "Не знайдено жодних власників, які відповідають '{{entity}}'.", + "tenant-required": "Необхідно вказати власника", + "selected-tenants": "{ count, plural, 1 {1 власник} other {# власники} } вибрано", + "search": "Пошук власників" + }, + "timeinterval": { + "seconds-interval": "{ seconds, plural, 1 {1 секунда} other {# секунди} }", + "minutes-interval": "{ minutes, plural, 1 {1 хвилина} other {# хвилини} }", + "hours-interval": "{ hours, plural, 1 {1 година} other {# години} }", + "days-interval": "{ days, plural, 1 {1 день} other {# дні} }", + "days": "Дні", + "hours": "Години", + "minutes": "Хвилини", + "seconds": "Секунди", + "advanced": "Додатково" + }, + "timewindow": { + "days": "{ days, plural, 1 { день } other {# дні } }", + "hours": "{ hours, plural, 0 { годин } 1 {1 година } other {# години } }", + "minutes": "{ minutes, plural, 0 { хвилин } 1 {1 хвилина } other {# хвилини } }", + "seconds": "{ seconds, plural, 0 { секунд } 1 {1 секунда } other {# секунди } }", + "realtime": "Реальний час", + "history": "Історія", + "last-prefix": "Останнє", + "period": "з {{ startTime }} до {{ endTime }}", + "edit": "Редагувати вікно часу", + "date-range": "Проміжок часу", + "last": "Останнє", + "time-period": "Період часу", + "hide": "Приховати" + }, + "user": { + "user": "Користувач", + "users": "Користувачі", + "customer-users": "Користувачі клієнта", + "tenant-admins": "Адміністратори власників", + "sys-admin": "Системний адміністратор", + "tenant-admin": "Адміністратор власника", + "customer": "Клієнт", + "anonymous": "Анонім", + "add": "Додати користувача", + "delete": "Видалити користувача", + "add-user-text": "Додати нового користувача", + "no-users-text": "Не знайдено жодного користувача", + "user-details": "Подробиці про користувача", + "delete-user-title": "Ви впевнені, що хочете видалити користувача'{{userEmail}}'?", + "delete-user-text": "Будьте обережні, після підтвердження, користувач і всі пов'язані з ним дані стануть недоступними.", + "delete-users-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 користувача} other {# користувачів} }?", + "delete-users-action-title": "Видалити { count, plural, 1 {1 користувача} other {# користувачів} }", + "delete-users-text": "Будьте обережні, після підтвердження, усіх виділених користувачів буде видалено, і всі пов'язані з ними дані стануть недоступними.", + "activation-email-sent-message": "Повідомлення про активацію успішно надіслано!", + "resend-activation": "Повторно надіслати активацію", + "email": "Електронна пошта", + "email-required": "Необхідно вказати електронну пошту.", + "invalid-email-format": "Недійсний формат електронної пошти.", + "first-name": "Ім'я", + "last-name": "Прізвище", + "description": "Опис", + "default-dashboard": "Стандартна панель візуалізації", + "always-fullscreen": "Завжди в повноекранному режимі", + "select-user": "Вибрати користувача", + "no-users-matching": "Не знайдено жодного користувача, що відповідає '{{entity}}'.", + "user-required": "Необхідно вказати користувача", + "activation-method": "Спосіб активації", + "display-activation-link": "Показати посилання для активації", + "send-activation-mail": "Надіслати активаційного листа", + "activation-link": "Активаційне посилання для користувача", + "activation-link-text": "Для активувації користувача, скористайтеся наступним activation link :", + "copy-activation-link": "Скопіювати активаційне посилання ", + "activation-link-copied-message": "Посилання на активацію користувача було скопійовано в буфер обміну", + "selected-users": "{ count, plural, 1 {1 користувач} other {# користувачі} } вибрано", + "search": "Пошук користувачів", + "details": "Подробиці", + "login-as-tenant-admin": "Увійти як адміністратор власника", + "login-as-customer-user": "Увійти як користувач клієнта", + "disable-account": "Вимкнути обліковий запис користувача", + "enable-account": "Увімкнути обліковий запис користувача", + "enable-account-message": "Обліковий запис користувача успішно увімкнено!", + "disabled-account-message": "Обліковий запис користувача успішно вимкнено!" + }, + "value": { + "type": "Тип значення", + "string": "Рядок", + "string-value": "Значення рядка", + "integer": "Ціле", + "integer-value": "Ціле значення", + "invalid-integer-value": "Недійсне ціле значення", + "double": "Подвійне", + "double-value": "Подвійне значення", + "boolean": "Логічне", + "boolean-value": "Логічне значення", + "false": "Помилкове", + "true": "Правдиве", + "long": "Довге" + }, + "widget": { + "widget-library": "Бібліотека віджетів", + "widget-bundle": "Пакет віджетів", + "select-widgets-bundle": "Виберіть пакет віджетів", + "management": "Керування віджетами", + "editor": "Редактор віджетів", + "widget-type-not-found": "Помилка завантаження конфігурації віджетів.
    Можливо, пов'язаний з нею\n тип віджета було видалено.", + "widget-type-load-error": "Віджет не вдалося завантажити з наступних причин:", + "remove": "Видалити віджет", + "edit": "Відредагувати віджет", + "remove-widget-title": "Ви впевнені, що хочете видалити віджет '{{widgetTitle}}'?", + "remove-widget-text": "Після підтвердження віджет і всі пов'язані з ним дані стануть недоступними.", + "timeseries": "Телеметрія", + "search-data": "Пошук даних", + "no-data-found": "Даних не знайдено", + "latest-values": "Останні значення", + "rpc": "Керуючий віджет", + "alarm": "Віджет сигнала тривоги", + "static": "Статичний віджет", + "select-widget-type": "Вибрати тип віджета", + "missing-widget-title-error": "Необхідно вказати назву віджета!", + "widget-saved": "Віджет збережено", + "unable-to-save-widget-error": "Неможливо зберегти віджет! Віджет має помилки!", + "save": "Зберегти віджет", + "saveAs": "Зберегти віджет як", + "save-widget-type-as": "Зберегти тип віджета як", + "save-widget-type-as-text": "Введіть новий заголовок віджета та / або виберіть цільові віджети", + "toggle-fullscreen": "Перейти в повноекранний режим", + "run": "Запустити віджет", + "title": "Назва віджета", + "title-required": "Необхідно вказати назву віджета.", + "type": "Тип віджета", + "resources": "Ресурси", + "resource-url": "JavaScript/CSS URL", + "remove-resource": "Видалити ресурс", + "add-resource": "Додати ресурс", + "html": "HTML", + "tidy": "Форматувати", + "css": "CSS", + "settings-schema": "Схема налаштувань", + "datakey-settings-schema": "Схема налаштувань ключів даних", + "javascript": "Javascript", + "remove-widget-type-title": "Ви впевнені, що хочете видалити тип віджета '{{widgetName}}'?", + "remove-widget-type-text": "Будьте обережні, після підтвердження, тип віджета і всі пов'язані з ним дані стануть недоступними.", + "remove-widget-type": "Видалити тип віджета", + "add-widget-type": "Додати новий тип віджета", + "widget-type-load-failed-error": "Не вдалося завантажити тип віджета!", + "widget-template-load-failed-error": "Не вдалося завантажити шаблон віджета!", + "add": "Додати віджет", + "undo": "Скасувати зміни віджета", + "export": "Експртувати віджет", + "export-data": "Експортувати дані віджетів", + "export-to-csv": "Експортувати дані в CSV...", + "export-to-excel": "Експортувати дані в XLS..." + }, + "widget-action": { + "header-button": "Кнопка заголовка віджета", + "open-dashboard-state": "Перейти до нового стану панелі візуалізації", + "update-dashboard-state": "Оновити поточний стан панелі візуалізації", + "open-dashboard": "Перейти до іншої панелі візуалізації", + "custom": "Дії користувачів", + "custom-pretty": "Дії користувачів (з HTML шаблоном)", + "target-dashboard-state": "Цільовий стан панелі візуалізації", + "target-dashboard-state-required": "Необхідно вказати цільовий стан панелі візуалізації", + "set-entity-from-widget": "Встановити сутність із віджета", + "target-dashboard": "Цільова панель візуалізації", + "open-right-layout": "Відкрити мобільний режим панелі візуалізації" + }, + "widgets-bundle": { + "current": "Поточний зв'язок", + "widgets-bundles": "Пакети віджетів", + "add": "Додати пакет віджетів", + "delete": "Видалити пакет віджетів", + "title": "Назва", + "title-required": "Необхідно вказати назву віджета.", + "add-widgets-bundle-text": "Додати новий пакет віджетів", + "no-widgets-bundles-text": "Не знайдено жодних пакетів віджетів", + "empty": "Пакет віджетів порожній", + "details": "Подробиці", + "widgets-bundle-details": "Деталі пакетів віджетів", + "delete-widgets-bundle-title": "Ви впевнені, що хочете видалити пакет віджетів '{{widgetsBundleTitle}}'?", + "delete-widgets-bundle-text": "Будьте обережні, після підтвердження, пакети віджетів і всі пов'язані з ними дані стануть недоступними.", + "delete-widgets-bundles-title": "Ви впевнені, що хочете видалити { count, plural, 1 {пакет віджетів} other {# пакети віджетів} }?", + "delete-widgets-bundles-action-title": "Видалити { count, plural, 1 {1 пакет віджетів} other {# пакет віджетів} }", + "delete-widgets-bundles-text": "Будьте обережні, після підтвердження, всі виділені пакети віджетів і всі пов'язані з ними дані стануть недоступними.", + "no-widgets-bundles-matching": "Не знайдено жодних пакетів віджетів, які відповідають '{{widgetsBundle}}'.", + "widgets-bundle-required": "Необхідно вказати пакет віджетів.", + "system": "Системний", + "import": "Імпортувати пакет віджетів", + "export": "Експортувати пакет віджетів", + "export-failed-error": "Неможливо експортувати пакет віджетів: {{error}}", + "create-new-widgets-bundle": "Створити новий пакет віджетів", + "widgets-bundle-file": "Файл набору віджетів", + "invalid-widgets-bundle-file-error": "Неможливо імпортувати пакет віджетів: недійсна структура даних пакету віджетів." + }, + "widget-config": { + "data": "Дані", + "settings": "Налаштування", + "advanced": "Додатково", + "title": "Назва", + "general-settings": "Загальні налаштування", + "display-title": "Відобразити назву", + "drop-shadow": "Тінь", + "enable-fullscreen": "Увімкнути повноекранний режим", + "enable-data-export": "Увімкнути експорт даних", + "background-color": "Колір фону", + "text-color": "Колір тексту", + "padding": "Відступ", + "margin": "Границі", + "widget-style": "Стиль віджетів", + "title-style": "Стиль заголовка", + "mobile-mode-settings": "Налаштування мобільного режиму", + "order": "Порядок", + "height": "Висота", + "units": "Спеціальний символ після значення", + "decimals": "Кількість цифр після коми", + "timewindow": "Вікно часу", + "use-dashboard-timewindow": "Використати вікно часу на панелі візуалізації", + "display-timewindow": "Показувати вікно часу", + "display-legend": "Показати легенду", + "datasources": "Джерела даних", + "maximum-datasources": "Максимально { count, plural, 1 {1 дозволене джерело даних.} other {# дозволені джерела даних } }", + "datasource-type": "Тип", + "datasource-parameters": "Параметри", + "remove-datasource": "Видалити джерело даних", + "add-datasource": "Додати джерело даних", + "target-device": "Цільовий пристрій", + "alarm-source": "Джерело сигнала тривоги", + "actions": "Дії", + "action": "Дія", + "add-action": "Додати дію", + "search-actions": "Пошук дії", + "action-source": "Джерело дії", + "action-source-required": "Необхідно вказати джерело дії.", + "action-name": "Ім'я дії", + "action-name-required": "Необхідно вказати ім'я дії.", + "action-name-not-unique": "Дія з такою назвою вже існує.
    Назва дії має бути унікальною в межах одного джерела дії.", + "action-icon": "Іконка", + "action-type": "Тип", + "action-type-required": "Необхідно вказати тип дії.", + "edit-action": "Редагувати дію", + "delete-action": "Видалити дію", + "delete-action-title": "Видалити дію віджета", + "delete-action-text": "Ви впевнені, що хочете видалити дію віджета '{{actionName}}'?", + "display-icon": "Показувати іконку у назві", + "icon-color": "Колір іконки", + "icon-size": "Розмір іконки" + }, + "widget-type": { + "import": "Імпортувати тип віджета", + "export": "Експортувати тип віджета", + "export-failed-error": "Неможливо експортувати тип віджета: {{error}}", + "create-new-widget-type": "Створити новий тип віджета", + "widget-type-file": "Файл типу віджета", + "invalid-widget-type-file-error": "Неможливо імпортувати тип віджету: неправильна структура даних типу віджета." + }, + "widgets": { + "date-range-navigator": { + "localizationMap": { + "Sun": "Нд", + "Mon": "Пн", + "Tue": "Вт", + "Wed": "Ср", + "Thu": "Чт", + "Fri": "Пт", + "Sat": "Сб", + "Jan": "Січ.", + "Feb": "Лют.", + "Mar": "Берез.", + "Apr": "Квіт.", + "May": "Трав.", + "Jun": "Черв.", + "Jul": "Лип.", + "Aug": "Серп.", + "Sep": "Верес.", + "Oct": "Жовт.", + "Nov": "Листоп.", + "Dec": "Груд.", + "January": "Січень", + "February": "Лютий", + "March": "Березень", + "April": "Квітень", + "June": "Червень", + "July": "Липень", + "August": "Серпень", + "September": "Вересень", + "October": "Жовтень", + "November": "Листопад", + "December": "Грудень", + "Custom Date Range": "Користувацький діапазон дат", + "Date Range Template": "Шаблон діапазону дат", + "Today": "Сьогодні", + "Yesterday": "Вчора", + "This Week": "Цього тижня", + "Last Week": "Минулий тиждень", + "This Month": "Цей місяць", + "Last Month": "Минулий місяць", + "Year": "Рік", + "This Year": "Цього року", + "Last Year": "Минулий рік", + "Date picker": "Вибір дати", + "Hour": "Година", + "Day": "День", + "Week": "Тиждень", + "2 weeks": "2 Тижні", + "Month": "Місяць", + "3 months": "3 Місяці", + "6 months": "6 Місяців", + "Custom interval": "Користувацький інтервал", + "Interval": "Інтервал", + "Step size": "Розмір кроку", + "Ok": "Ok" + } + }, + "input-widgets": { + "attribute-not-allowed": "Атрибут не може бути вибраний в цьому віджеті", + "date": "Дата", + "blocked-location": "Геолокація заблокована у вашому браузері", + "claim-device": "Підтвердити пристрій", + "claim-failed": "Не вдалося підтвердити пристрій!", + "claim-not-found": "Пристрій не знайдено!", + "claim-successful": "Пристрій успішно підтверджено!", + "discard-changes": "Скасувати зміни", + "device-name": "Назва пристрою", + "device-name-required": "Необхідно вказати назву пристрою", + "entity-attribute-required": "Значення атрибута обов'язкове", + "entity-coordinate-required": "Необхідно вказати широту та довготу", + "entity-timeseries-required": "Значення телеметрії обов'язкове", + "get-location": "Отримати поточне місцезнаходження", + "latitude": "Широта", + "longitude": "Довгота", + "not-allowed-entity": "Обрана сутність не має спільних атрибутів", + "no-attribute-selected": "Атрибут не вибрано", + "no-datakey-selected": "Ні один datakey не обраний", + "no-entity-selected": "Сутність не вибрано", + "no-coordinate-specified": "Ключ для широти/довготи не вказаний", + "no-support-geolocation": "Ваш браузер не підтримує геолокацію", + "no-image": "Немає зображення", + "no-support-web-camera": "Нет поддерживаемой веб-камеры", + "no-timeseries-selected": "Параметр телеметрії не вибрано", + "secret-key": "Секретний ключ", + "secret-key-required": "Необхідно вказати секретний ключ", + "switch-attribute-value": "Змінити значення атрибута", + "switch-camera": "Змінити камеру", + "switch-timeseries-value": "Змінити значення телеметрії", + "take-photo": "Зробити фото", + "time": "Час", + "timeseries-not-allowed": "Телеметрія не може бути вибрана в цьому віджеті", + "update-failed": "Не вдалося оновити", + "update-successful": "Успішно оновлено", + "update-attribute": "Оновити атрибут", + "update-timeseries": "Оновити телеметрію", + "value": "Значення" + } + }, + "white-labeling": { + "white-labeling": "Білий маркування", + "login-white-labeling": "Login White Labeling", + "preview": "Попередній перегляд", + "app-title": "Назва програми", + "favicon": "Іконка веб-сайту", + "favicon-description": "*.ico, *.gif or *.png image with maximum size {{kbSize}} KBytes.", + "favicon-size-error": "Зображення веб-сайту завелике. Максимально дозволений розмір зображення веб-сайту {{kbSize}} KBytes.", + "favicon-type-error": "Недійсний формат файлу зображення веб-сайту. Приймаються лише зображення ICO, GIF або PNG.", + "drop-favicon-image": "Зніміть зображення піктограми веб-сайту або клацніть, щоб вибрати файл для завантаження.", + "no-favicon-image": "Не вибрано жодної іконки", + "logo": "Логотип", + "logo-description": "Будь-яке зображення з максимальним розміром {{kbSize}} KBytes.", + "logo-size-error": "Зображення логотипу занадто велике. Максимально дозволений розмір зображення логотипу{{kbSize}} KBytes.", + "logo-type-error": "Недійсний формат файлу логотипу. Приймаються тільки зображення.", + "drop-logo-image": "Зніміть зображення логотипу або клацніть, щоб вибрати файл для завантаження.", + "no-logo-image": "Не вибрано жожного логотипу", + "logo-height": "Висота логотипу, px", + "primary-palette": "Основна палітра", + "accent-palette": "Палітра акцент", + "customize-palette": "Налаштування", + "edit-palette": "Редагувати палітру", + "save-palette": "Зберегти палітру", + "primary-background": "Первинний фон", + "secondary-background": "Вторинний фон", + "hue1": "HUE 1", + "hue2": "HUE 2", + "hue3": "HUE 3", + "page-background-color": "Колір фону сторінки", + "dark-foreground": "Темний передній план", + "domain-name": "Доменне ім'я" + }, + "icon": { + "icon": "Іконка", + "select-icon": "Виберіть Іконку", + "material-icons": "Іконки в стилі Material", + "show-all": "Показати всі іконки" + }, + "custom": { + "widget-action": { + "action-cell-button": "Кнопка дії в комірці таблиці", + "row-click": "Клацніть на рядок", + "marker-click": "Клацніть на маркер", + "polygon-click": "Дія при натисканні на полігон", + "tooltip-tag-action": "Дія при натисканні на посилання в підказці", + "node-selected": "Дії при виборі ноди", + "element-click": "Дії при натисканні на HTML елементі", + "pie-slice-click": "Дії при натисканні на секції кругової діаграми", + "row-double-click": "Дії при подвійному натисканні на рядок" + } + }, + "language": { + "language": "Мова" + } +} diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json new file mode 100644 index 0000000000..1b70a7f643 --- /dev/null +++ b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json @@ -0,0 +1,1608 @@ +{ + "access": { + "unauthorized": "未授权", + "unauthorized-access": "未授权访问", + "unauthorized-access-text": "您需要登陆才能访问这个资源!", + "access-forbidden": "禁止访问", + "access-forbidden-text": "您没有访问此位置的权限
    如果您仍希望访问此位置,请尝试使用其他用户登录。", + "refresh-token-expired": "会话已过期", + "refresh-token-failed": "无法刷新会话" + }, + "action": { + "activate": "激活", + "suspend": "暂停", + "save": "保存", + "saveAs": "另存为", + "cancel": "取消", + "ok": "确定", + "delete": "删除", + "add": "添加", + "yes": "是", + "no": "否", + "update": "更新", + "remove": "移除", + "search": "查询", + "clear-search": "清除查询", + "assign": "分配", + "unassign": "取消分配", + "share": "分享", + "make-private": "私有", + "apply": "应用", + "apply-changes": "应用更改", + "edit-mode": "编辑模式", + "enter-edit-mode": "进入编辑模式", + "decline-changes": "撤销更改", + "close": "关闭", + "back": "后退", + "run": "运行", + "sign-in": "登录!", + "edit": "编辑", + "view": "查看", + "create": "创建", + "drag": "拖拽", + "refresh": "刷新", + "undo": "撤销", + "copy": "复制", + "paste": "粘贴", + "copy-reference": "复制引用", + "paste-reference": "粘贴引用", + "import": "导入", + "export": "导出", + "share-via": "通过 {{provider}}分享" + }, + "aggregation": { + "aggregation": "聚合", + "function": "数据聚合功能", + "limit": "最大值", + "group-interval": "分组间隔", + "min": "最少值", + "max": "最大值", + "avg": "平均值", + "sum": "求和", + "count": "计数", + "none": "空" + }, + "admin": { + "general": "常规", + "general-settings": "常规设置", + "outgoing-mail": "发送邮件", + "outgoing-mail-settings": "发送邮件设置", + "system-settings": "系统设置", + "test-mail-sent": "测试邮件发送成功!", + "base-url": "基本URL", + "base-url-required": "基本URL必填。", + "mail-from": "邮件来自", + "mail-from-required": "邮件发件人必填。", + "smtp-protocol": "SMTP协议", + "smtp-host": "SMTP主机", + "smtp-host-required": "SMTP主机必填。", + "smtp-port": "SMTP端口", + "smtp-port-required": "您必须提供一个smtp端口。", + "smtp-port-invalid": "这看起来不是有效的smtp端口。", + "timeout-msec": "超时(ms)", + "timeout-required": "超时必填。", + "timeout-invalid": "这看起来不像有效的超时值。", + "enable-tls": "启用TLS", + "tls-version" : "TLS版本", + "send-test-mail": "发送测试邮件" + }, + "alarm": { + "alarm": "警告", + "alarms": "警告", + "select-alarm": "选择警告", + "no-alarms-matching": "没有找到匹配 '{{entity}}' 的警告", + "alarm-required": "警告必填", + "alarm-status": "警告状态", + "search-status": { + "ANY": "所有", + "ACTIVE": "已激活", + "CLEARED": "已清除", + "ACK": "已应答", + "UNACK": "未应答" + }, + "display-status": { + "ACTIVE_UNACK": "激活未应答", + "ACTIVE_ACK": "激活已应答", + "CLEARED_UNACK": "清除未应答", + "CLEARED_ACK": "清除已应答" + }, + "no-alarms-prompt": "未发现警告", + "created-time": "创建时间", + "type": "类型", + "severity": "严重程度", + "originator": "起因", + "originator-type": "起因类型", + "details": "详情", + "status": "状态", + "alarm-details": "警告详情", + "start-time": "开始时间", + "end-time": "结束时间", + "ack-time": "应答时间", + "clear-time": "创建时间", + "severity-critical": "危险", + "severity-major": "重要", + "severity-minor": "次要", + "severity-warning": "警告", + "severity-indeterminate": "不确定", + "acknowledge": "应答", + "clear": "清除", + "search": "搜索警告", + "selected-alarms": "已选择 { count, plural, 1 {1 警告} other {# 警告} } ", + "no-data": "无数据显示", + "polling-interval": "警告轮询间隔(秒)", + "polling-interval-required": "警告轮询间隔必填。", + "min-polling-interval-message": "轮询间隔至少是1秒。", + "aknowledge-alarms-title": "应答 { count, plural, 1 {1 警告} other {# 警告} }", + "aknowledge-alarms-text": "确定要应答 { count, plural, 1 {1 警告} other {# 警告} }?", + "clear-alarms-title": "清除 { count, plural, 1 {1 警告} other {# 警告} }", + "clear-alarms-text": "确定要清除 { count, plural, 1 {1 警告} other {# 警告} }?" + }, + "alias": { + "add": "添加别名", + "edit": "编辑别名", + "name": "别名", + "name-required": "别名必填", + "duplicate-alias": "别名已经存在。", + "filter-type-single-entity": "单个实体", + "filter-type-entity-list": "实体列表", + "filter-type-entity-name": "实体名称", + "filter-type-state-entity": "实体(仪表板状态)", + "filter-type-state-entity-description": "实体令牌(仪表板状态参数)", + "filter-type-asset-type": "资产类型", + "filter-type-asset-type-description": "类型为 '{{assetType}}' 的资产", + "filter-type-asset-type-and-name-description": "类型为 '{{assetType}}' 且以 '{{prefix}}' 开头的资产", + "filter-type-device-type": "设备类型", + "filter-type-device-type-description": "类型为 '{{deviceType}}' 的设备", + "filter-type-device-type-and-name-description": "类型为 '{{deviceType}}' 且以 '{{prefix}}' 开头的设备", + "filter-type-entity-view-type": "实体视图类型", + "filter-type-entity-view-type-description": "类型为 '{{entityView}}' 的实体视图", + "filter-type-entity-view-type-and-name-description": "类型为 {{entityView}}' 且以 '{{prefix}}' 开头的实体视图", + "filter-type-relations-query": "关系查询", + "filter-type-relations-query-description": "具有 {{relationType}} 关联 {{direction}} {{rootEntity}} 的 {{entities}} ", + "filter-type-asset-search-query": "资产搜索查询", + "filter-type-asset-search-query-description": "类型为 {{assetTypes}} 且具有 {{relationType}} 关联 {{direction}} {{rootEntity}} 的资产", + "filter-type-device-search-query": "设备搜索查询", + "filter-type-device-search-query-description": "类型为 {{deviceTypes}} 且具有 {{relationType}} 关联 {{direction}} {{rootEntity}} 的设备", + "filter-type-entity-view-search-query": "实体视图搜索查询", + "filter-type-entity-view-search-query-description": "类型为 {{entityViewTypes}} 且具有 {{relationType}} 关联 {{direction}} {{rootEntity}} 的实体视图", + "entity-filter": "实体过滤", + "resolve-multiple": "解决为多实体", + "filter-type": "过滤类型", + "filter-type-required": "过滤类型必填。", + "entity-filter-no-entity-matched": "未找到符合指定过滤条件的实体。", + "no-entity-filter-specified": "没有指定实体过滤条件", + "root-state-entity": "使用仪表板状态实体作为根实体", + "root-entity": "根实体", + "state-entity-parameter-name": "状态实体参数名称", + "default-state-entity": "默认状态实体", + "default-entity-parameter-name": "默认", + "max-relation-level": "最大关系层级", + "unlimited-level": "不限层级", + "state-entity": "仪表板状态实体", + "all-entities": "所有实体", + "any-relation": "不限" + }, + "asset": { + "asset": "资产", + "assets": "资产", + "management": "资产管理", + "view-assets": "查看资产", + "add": "添加资产", + "assign-to-customer": "分配给客户", + "assign-asset-to-customer": "将资产分配给客户", + "assign-asset-to-customer-text": "请选择要分配给客户的资产", + "no-assets-text": "未找到资产", + "assign-to-customer-text": "请选择客户以分配资产", + "public": "公开", + "assignedToCustomer": "分配客户", + "make-public": "资产设为公开", + "make-private": "资产设为私有", + "unassign-from-customer": "取消分配客户", + "delete": "删除资产", + "asset-public": "资产公开", + "asset-type": "资产类型", + "asset-type-required": "资产类型必填。", + "select-asset-type": "选择资产类型", + "enter-asset-type": "输入资产类型", + "any-asset": "任何资产", + "no-asset-types-matching": "没有找到匹配 '{{entitySubtype}}' 的资产类型。", + "asset-type-list-empty": "资产类型未选择。", + "asset-types": "资产类型", + "name": "名称", + "name-required": "名称必填。", + "description": "描述", + "type": "类型", + "type-required": "类型必填。", + "details": "详情", + "events": "事件", + "add-asset-text": "添加新资产", + "asset-details": "资产详情", + "assign-assets": "分配资产", + "assign-assets-text": "分配 { count, plural, 1 {1 资产} other {# 资产} } 给客户", + "delete-assets": "删除资产", + "unassign-assets": "取消分配资产", + "unassign-assets-action-title": "从客户处取消分配 { count, plural, 1 {1 资产} other {# 资产} } ", + "assign-new-asset": "分配新资产", + "delete-asset-title": "确定要删除资产 '{{assetName}}'?", + "delete-asset-text": "小心!确认后资产及其所有相关数据将不可恢复。", + "delete-assets-title": "确定要删除 { count, plural, 1 {1 资产} other {# 资产} }?", + "delete-assets-action-title": "删除 { count, plural, 1 {1 资产} other {# 资产} }", + "delete-assets-text": "小心,确认后,所有选定的资产将被删除,所有相关的数据将变得不可恢复。", + "make-public-asset-title": "你确定你想创建公开'{{assetName}}'资产?", + "make-public-asset-text": "确认后,资产及其所有数据将被公开并被他人访问。", + "make-private-asset-title": "你确定你想创建私有 '{{assetName}}' 资产?", + "make-private-asset-text": "确认后,资产及其所有数据将被私有化,无法被他人访问。", + "unassign-asset-title": "您确定要取消对'{{assetName}}'资产的分配吗?", + "unassign-asset-text": "确认后,资产将未分配,客户无法访问。", + "unassign-asset": "未分配资产", + "unassign-assets-title": "您确定要取消分配 { count, plural, 1 {1 资产} other {# 资产} }吗?", + "unassign-assets-text": "确认后,所有选定的资产将被分配,客户无法访问。", + "copyId": "复制资产ID", + "idCopiedMessage": "资产ID已经复制到粘贴板", + "select-asset": "选择资产", + "no-assets-matching": "没有找到匹配 '{{entity}}' 的资产。", + "asset-required": "资产必填", + "name-starts-with": "资产名称以此开头", + "label": "标签" + }, + "attribute": { + "attributes": "属性", + "latest-telemetry": "最新遥测", + "attributes-scope": "设备属性范围", + "scope-latest-telemetry": "最新遥测", + "scope-client": "客户端属性", + "scope-server": "服务端属性", + "scope-shared": "共享属性", + "add": "添加属性", + "key": "键", + "last-update-time": "最后更新时间", + "key-required": "属性键必填。", + "value": "值", + "value-required": "属性值必填。", + "delete-attributes-title": "您确定要删除 { count, plural, 1 {1 属性} other {# 属性} }吗?", + "delete-attributes-text": "注意,确认后所有选中的属性都会被删除。", + "delete-attributes": "删除属性", + "enter-attribute-value": "输入属性值", + "show-on-widget": "在部件上显示", + "widget-mode": "部件模式", + "next-widget": "下一个部件", + "prev-widget": "上一个部件", + "add-to-dashboard": "添加到仪表板", + "add-widget-to-dashboard": "将部件添加到仪表板", + "selected-attributes": "{ count, plural, 1 {1 属性} other {# 属性} } 被选中", + "selected-telemetry": "{ count, plural, 1 {1 遥测} other {# 遥测} } 被选中" + }, + "audit-log": { + "audit": "审计", + "audit-logs": "审计日志", + "timestamp": "时间戳", + "entity-type": "实体类型", + "entity-name": "实体名称", + "user": "用户", + "type": "类型", + "status": "状态", + "details": "详情", + "type-added": "添加", + "type-deleted": "删除", + "type-updated": "更新", + "type-attributes-updated": "更新属性", + "type-attributes-deleted": "删除属性", + "type-rpc-call": "RPC调用", + "type-credentials-updated": "更新凭证", + "type-assigned-to-customer": "分配给客户", + "type-unassigned-from-customer": "未分配给客户", + "type-activated": "激活", + "type-suspended": "暂停", + "type-credentials-read": "读取凭证", + "type-attributes-read": "读取属性", + "status-success": "成功", + "status-failure": "失败", + "audit-log-details": "审计日志详情", + "no-audit-logs-prompt": "找不到日志", + "action-data": "活动数据", + "failure-details": "失败详情", + "search": "查找审计日志", + "clear-search": "清空查找" + }, + "confirm-on-exit": { + "message": "您有未保存的更改。确定要离开此页吗?", + "html-message": "您有未保存的更改。
    确定要离开此页面吗?", + "title": "未保存的更改" + }, + "contact": { + "country": "国家", + "city": "城市", + "state": "州", + "postal-code": "邮政编码", + "postal-code-invalid": "只允许数字。", + "address": "地址", + "address2": "地址2", + "phone": "手机", + "email": "邮箱", + "no-address": "无地址" + }, + "common": { + "username": "用户名", + "password": "密码", + "enter-username": "输入用户名", + "enter-password": "输入密码", + "enter-search": "输入检索条件", + "created-time": "创建时间" + }, + "content-type": { + "json": "Json", + "text": "Text", + "binary": "Binary (Base64)" + }, + "customer": { + "customer": "客户", + "customers": "客户", + "management": "客户管理", + "dashboard": "客户仪表板", + "dashboards": "客户仪表板", + "devices": "客户设备", + "entity-views": "客户实体视图", + "assets": "客户资产", + "public-dashboards": "公共仪表板", + "public-devices": "公共设备", + "public-assets": "公共资产", + "public-entity-views": "公共实体视图", + "add": "添加客户", + "delete": "删除客户", + "manage-customer-users": "管理客户用户", + "manage-customer-devices": "管理客户设备", + "manage-customer-dashboards": "管理客户仪表板", + "manage-public-devices": "管理公共设备", + "manage-public-dashboards": "管理公共仪表板", + "manage-customer-assets": "管理客户资产", + "manage-public-assets": "管理公共资产", + "add-customer-text": "添加新客户", + "no-customers-text": "没有找到客户", + "customer-details": "客户详情", + "delete-customer-title": "您确定要删除客户'{{customerTitle}}'吗?", + "delete-customer-text": "小心!确认后,客户及其所有相关数据将不可恢复。", + "delete-customers-title": "您确定要删除 { count, plural, 1 {1 客户} other {# 客户} }吗?", + "delete-customers-action-title": "删除 { count, plural, 1 {1 客户} other {# 客户} }", + "delete-customers-text": "小心!确认后,所有选定的客户将被删除,所有相关数据将不可恢复。", + "manage-users": "管理用户", + "manage-assets": "管理资产", + "manage-devices": "管理设备", + "manage-dashboards": "管理仪表板", + "title": "标题", + "title-required": "需要标题", + "description": "描述", + "details": "详情", + "events": "事件", + "copyId": "复制客户ID", + "idCopiedMessage": "客户ID已复制到粘贴板", + "select-customer": "选择客户", + "no-customers-matching": "没有找到匹配 '{{entity}}' 的客户。", + "customer-required": "客户是必选项", + "select-default-customer": "选择默认的客户", + "default-customer": "默认客户", + "default-customer-required": "为了调试租户级别上的仪表板,需要默认客户。" + }, + "datetime": { + "date-from": "日期从", + "time-from": "时间从", + "date-to": "日期到", + "time-to": "时间到" + }, + "dashboard": { + "dashboard": "仪表板", + "dashboards": "仪表板库", + "management": "仪表板管理", + "view-dashboards": "查看仪表板", + "add": "添加仪表板", + "assign-dashboard-to-customer": "将仪表板分配给客户", + "assign-dashboard-to-customer-text": "请选择要分配给客户的仪表板", + "assign-to-customer-text": "请选择客户分配仪表板", + "assign-to-customer": "分配给客户", + "unassign-from-customer": "取消分配客户", + "make-public": "仪表板设为公开", + "make-private": "仪表板设为私有", + "manage-assigned-customers": "管理已分配的客户", + "assigned-customers": "已分配的客户", + "assign-to-customers": "将仪表板分配给客户", + "assign-to-customers-text": "请选择客户指定仪表板", + "unassign-from-customers": "客户未分配仪表板", + "unassign-from-customers-text": "请选择从仪表板中取消分配的客户", + "no-dashboards-text": "没有找到仪表板", + "no-widgets": "没有配置部件", + "add-widget": "添加新的部件", + "title": "标题", + "select-widget-title": "选择部件", + "select-widget-subtitle": "可用的部件类型列表", + "delete": "删除仪表板", + "title-required": "需要标题。", + "description": "描述", + "details": "详情", + "dashboard-details": "仪表板详情", + "add-dashboard-text": "添加新的仪表板", + "assign-dashboards": "分配仪表板", + "assign-new-dashboard": "分配新的仪表板", + "assign-dashboards-text": "分配 { count, plural, 1 {1 仪表板} other {# 仪表板} } 给客户", + "unassign-dashboards-action-text": "未分配 { count, plural, 1 {1 仪表板} other {# 仪表板} } 给客户", + "delete-dashboards": "删除仪表板", + "unassign-dashboards": "取消分配仪表板", + "unassign-dashboards-action-title": "从客户处取消分配 { count, plural, 1 {1 仪表板} other {# 仪表板} } ", + "delete-dashboard-title": "您确定要删除仪表板 '{{dashboardTitle}}'吗?", + "delete-dashboard-text": "小心!确认后仪表板及其所有相关数据将不可恢复。", + "delete-dashboards-title": "你确定你要删除 { count, plural, 1 {1 仪表板} other {# 仪表板} }吗?", + "delete-dashboards-action-title": "删除 { count, plural, 1 {1 仪表板} other {# 仪表板} }", + "delete-dashboards-text": "小心!确认后所有选定的仪表板将被删除,所有相关数据将不可恢复。", + "unassign-dashboard-title": "您确定要取消分配仪表板 '{{dashboardTitle}}'吗?", + "unassign-dashboard-text": "确认后,面板将被取消分配,客户将无法访问。", + "unassign-dashboard": "取消分配仪表板", + "unassign-dashboards-title": "您确定要取消分配仪表板 { count, plural, 1 {1 仪表板} other {# 仪表板} } 吗?", + "unassign-dashboards-text": "确认后,所有选定的仪表板将被取消分配,客户将无法访问。", + "public-dashboard-title": "仪表板现已公布", + "public-dashboard-text": "你的仪表板 {{dashboardTitle}} 已被公开,可通过如下 链接访问:", + "public-dashboard-notice": "提示: 不要忘记将相关设备公开以访问其数据。", + "make-private-dashboard-title": "您确定要将仪表板 '{{dashboardTitle}}' 设为私有吗?", + "make-private-dashboard-text": "确认后,仪表板将被设为私有,不能被其他人访问。", + "make-private-dashboard": "仪表板设为私有", + "socialshare-text": "'{{dashboardTitle}}' 由Thingsboard提供支持", + "socialshare-title": "'{{dashboardTitle}}' 由Thingsboard提供支持", + "select-dashboard": "选择仪表板", + "no-dashboards-matching": "找不到符合 '{{entity}}' 的仪表板。", + "dashboard-required": "仪表板必填。", + "select-existing": "选择现有仪表板", + "create-new": "创建新的仪表板", + "new-dashboard-title": "新仪表板标题", + "open-dashboard": "打开仪表板", + "set-background": "设置背景", + "background-color": "背景颜色", + "background-image": "背景图片", + "background-size-mode": "背景大小模式", + "no-image": "无图像选择", + "drop-image": "拖拽图像或单击以选择要上传的文件。", + "settings": "设置", + "columns-count": "列数", + "columns-count-required": "需要列数。", + "min-columns-count-message": "只允许最少10列", + "max-columns-count-message": "只允许最多1000列", + "widgets-margins": "部件间边距", + "horizontal-margin": "水平边距", + "horizontal-margin-required": "需要水平边距值。", + "min-horizontal-margin-message": "只允许0作为最小水平边距值。", + "max-horizontal-margin-message": "只允许50作为最大水平边距值。", + "vertical-margin": "垂直边距", + "vertical-margin-required": "需要垂直边距值。", + "min-vertical-margin-message": "只允许0作为最小垂直边距值。", + "max-vertical-margin-message": "只允许50作为最大垂直边距值。", + "autofill-height": "自动填充布局高度", + "mobile-layout": "移动端布局设置", + "mobile-row-height": "移动端行高距(px)", + "mobile-row-height-required": "移动端行高距必填。", + "min-mobile-row-height-message": "移动端行高距至少5px。", + "max-mobile-row-height-message": "移动端行高距最多200px。", + "display-title": "显示仪表板标题", + "toolbar-always-open": "工具栏常驻", + "title-color": "标题颜色", + "display-dashboards-selection": "显示仪表板选项", + "display-entities-selection": "显示实体选项", + "display-dashboard-timewindow": "显示时间窗口", + "display-dashboard-export": "显示导出", + "import": "导入仪表板", + "export": "导出仪表板", + "export-failed-error": "无法导出仪表板: {{error}}", + "create-new-dashboard": "创建新的仪表板", + "dashboard-file": "仪表板文件", + "invalid-dashboard-file-error": "无法导入仪表板: 仪表板数据结构无效。", + "dashboard-import-missing-aliases-title": "配置导入仪表板使用的别名", + "create-new-widget": "创建新部件", + "import-widget": "导入部件", + "widget-file": "部件文件", + "invalid-widget-file-error": "无法导入窗口部件: 窗口部件数据结构无效。", + "widget-import-missing-aliases-title": "配置导入的窗口部件使用的别名", + "open-toolbar": "打开仪表板工具栏", + "close-toolbar": "关闭工具栏", + "configuration-error": "配置错误", + "alias-resolution-error-title": "仪表板别名配置错误", + "invalid-aliases-config": "无法找到与某些别名过滤器匹配的任何设备。
    请联系您的管理员以解决此问题。", + "select-devices": "选择设备", + "assignedToCustomer": "分配给客户", + "public": "公共", + "public-link": "公共链接", + "copy-public-link": "复制公共链接", + "public-link-copied-message": "仪表板的公共链接已被复制到剪贴板", + "manage-states": "仪表板状态管理", + "states": "仪表板状态", + "search-states": "仪表板状态检索", + "selected-states": "{ count, plural, 1 {1 仪表板状态} other {# 仪表板状态} } 选中", + "edit-state": "仪表板状态编辑", + "delete-state": "删除仪表板状态", + "add-state": "添加仪表板状态", + "state": "仪表板状态", + "state-name": "状态名", + "state-name-required": "仪表板状态名必填。", + "state-id": "状态ID", + "state-id-required": "仪表板状态ID必填。", + "state-id-exists": "仪表板状态ID已经存在。", + "is-root-state": "根状态", + "delete-state-title": "删除仪表板状态", + "delete-state-text": "确定要删除仪表板状态 '{{stateName}}' 吗?", + "show-details": "显示详情", + "hide-details": "隐藏详情", + "select-state": "选择目标状态", + "state-controller": "状态控制" + }, + "datakey": { + "settings": "设置", + "advanced": "高级", + "label": "标签", + "color": "颜色", + "units": "单位符号", + "decimals": "小数位数", + "data-generation-func": "数据生成功能", + "use-data-post-processing-func": "使用数据后处理功能", + "configuration": "数据键配置", + "timeseries": "时间序列", + "attributes": "属性", + "alarm": "报警字段", + "timeseries-required": "需要设备时间序列。", + "timeseries-or-attributes-required": "设备时间/属性必填。", + "maximum-timeseries-or-attributes": "最大允许 { count, plural, 1 {1 时间序列/属性} other {# 时间序列/属性} }", + "alarm-fields-required": "警告字段必填。", + "function-types": "函数类型", + "function-types-required": "需要函数类型。", + "maximum-function-types": "至少需要 { count, plural, 1 {1 函数类型} other {# 函数类型} }" + }, + "datasource": { + "type": "数据源类型", + "name": "数据源名称", + "add-datasource-prompt": "请添加数据源" + }, + "details": { + "edit-mode": "编辑模式", + "toggle-edit-mode": "切换编辑模式" + }, + "device": { + "device": "设备", + "device-required": "设备必填", + "devices": "设备", + "management": "设备管理", + "view-devices": "查看设备", + "device-alias": "设备别名", + "aliases": "设备别名", + "no-alias-matching": "'{{alias}}' 没有找到。", + "no-aliases-found": "找不到别名。", + "no-key-matching": "'{{key}}' 没有找到。", + "no-keys-found": "找不到密钥。", + "create-new-alias": "创建一个新的!", + "create-new-key": "创建一个新的!", + "duplicate-alias-error": "找到重复别名 '{{alias}}'。
    设备别名必须是唯一的。", + "configure-alias": "配置 '{{alias}}' 别名", + "no-devices-matching": "找不到与 '{{entity}}' 匹配的设备。", + "alias": "别名", + "alias-required": "需要设备别名。", + "remove-alias": "删除设备别名", + "add-alias": "添加设备别名", + "name-starts-with": "名称前缀", + "device-list": "设备列表", + "use-device-name-filter": "使用过滤器", + "device-list-empty": "没有被选中的设备", + "device-name-filter-required": "设备名称过滤器必填。", + "device-name-filter-no-device-matched": "找不到以'{{device}}' 开头的设备。", + "add": "添加设备", + "assign-to-customer": "分配给客户", + "assign-device-to-customer": "将设备分配给客户", + "assign-device-to-customer-text": "请选择要分配给客户的设备", + "make-public": "公开", + "make-private": "私有", + "no-devices-text": "找不到设备", + "assign-to-customer-text": "请选择客户分配设备", + "device-details": "设备详细信息", + "add-device-text": "添加新设备", + "credentials": "凭据", + "manage-credentials": "管理凭据", + "delete": "删除设备", + "assign-devices": "分配设备", + "assign-devices-text": "将{count,plural,1 {1 设备} other {# 设备} }分配给客户", + "delete-devices": "删除设备", + "unassign-from-customer": "取消分配客户", + "unassign-devices": "取消分配设备", + "unassign-devices-action-title": "从客户处取消分配{count,plural,1 {1 设备} other {# 设备} }", + "assign-new-device": "分配新设备", + "make-public-device-title": "您确定要将设备 '{{deviceName}}' 设为公开吗?", + "make-public-device-text": "确认后,设备及其所有数据将被设为公开并可被其他人访问。", + "make-private-device-title": "您确定要将设备 '{{deviceName}}' 设为私有吗?", + "make-private-device-text": "确认后,设备及其所有数据将被设为私有,不被其他人访问。", + "view-credentials": "查看凭据", + "delete-device-title": "您确定要删除设备的{{deviceName}}吗?", + "delete-device-text": "小心!确认后设备及其所有相关数据将不可恢复。", + "delete-devices-title": "您确定要删除{count,plural,1 {1 设备} other {# 设备} } 吗?", + "delete-devices-action-title": "删除 {count,plural,1 {1 设备} other {# 设备} }", + "delete-devices-text": "小心!确认后所有选定的设备将被删除,所有相关数据将不可恢复。", + "unassign-device-title": "您确定要取消分配设备 '{{deviceName}}'?", + "unassign-device-text": "确认后,设备将被取消分配,客户将无法访问。", + "unassign-device": "取消分配设备", + "unassign-devices-title": "您确定要取消分配{count,plural,1 {1 设备} other {# 设备} } 吗?", + "unassign-devices-text": "确认后,所有选定的设备将被取消分配,并且客户将无法访问。", + "device-credentials": "设备凭据", + "credentials-type": "凭据类型", + "access-token": "访问令牌", + "access-token-required": "需要访问令牌", + "access-token-invalid": "访问令牌长度必须为1到20个字符。", + "rsa-key": "RSA公钥", + "rsa-key-required": "需要RSA公钥", + "secret": "密钥", + "secret-required": "密钥必填", + "device-type": "设备类型", + "device-type-required": "设备类型必填。", + "select-device-type": "选择设备类型", + "enter-device-type": "输入设备类型", + "any-device": "任意设备", + "no-device-types-matching": "没有找到符合 '{{entitySubtype}}' 的设备类型。", + "device-type-list-empty": "未选择设备类型", + "device-types": "设备类型", + "name": "名称", + "name-required": "名称必填。", + "description": "说明", + "events": "事件", + "details": "详细信息", + "copyId": "复制设备ID", + "copyAccessToken": "复制访问令牌", + "idCopiedMessage": "设备ID已复制到剪贴板", + "accessTokenCopiedMessage": "设备访问令牌已复制到剪贴板", + "assignedToCustomer": "分配给客户", + "unable-delete-device-alias-title": "无法删除设备别名", + "unable-delete-device-alias-text": "设备别名 '{{deviceAlias}}' 不能够被删除,因为它被下列部件所使用:
    {{widgetsList}}", + "is-gateway": "是网关", + "public": "公开", + "device-public": "设备公开", + "select-device": "选择设备" + }, + "dialog": { + "close": "关闭对话框" + }, + "error": { + "unable-to-connect": "无法连接到服务器!请检查您的互联网连接。", + "unhandled-error-code": "未处理的错误代码: {{errorCode}}", + "unknown-error": "未知错误" + }, + "entity": { + "entity": "实体", + "entities": "实体", + "aliases": "实体别名", + "entity-alias": "实体别名", + "unable-delete-entity-alias-title": "无法删除实体别名", + "unable-delete-entity-alias-text": "实体别名 '{{entityAlias}}' 被以下部件使用不能删除:
    {{widgetsList}}", + "duplicate-alias-error": "别名 '{{alias}}' 重复。
    同一仪表板别名必须唯一。", + "missing-entity-filter-error": "别名 '{{alias}}' 缺少过滤器", + "configure-alias": "别名 '{{alias}}' 配置", + "alias": "别名", + "alias-required": "实体别名必填。", + "remove-alias": "移除实体别名", + "add-alias": "添加实体别名", + "entity-list": "实体列表", + "entity-type": "实体类型", + "entity-types": "实体类型", + "entity-type-list": "实体类型列表", + "any-entity": "任意实体", + "enter-entity-type": "输入实体类型", + "no-entities-matching": "没有找到符合 '{{entity}}' 的实体。", + "no-entity-types-matching": "没有找到符合 '{{entityType}}' 类型的实体。", + "name-starts-with": "名称开始于", + "use-entity-name-filter": "用户过滤", + "entity-list-empty": "没有选择实体。", + "entity-type-list-empty": "没有选择实体类型。", + "entity-name-filter-required": "实体名过滤器必填。", + "entity-name-filter-no-entity-matched": "没有找到以 '{{entity}}' 开头的实体", + "all-subtypes": "所有", + "select-entities": "选择实体", + "no-aliases-found": "没有找到别名", + "no-alias-matching": "没有找到 '{{alias}}'", + "create-new-alias": "创建新别名", + "key": "键", + "key-name": "键名", + "no-keys-found": "没有找到键", + "no-key-matching": "没有找到键 '{{key}}'", + "create-new-key": "创建新键", + "type": "类型", + "type-required": "实体类型必填。", + "type-device": "设备", + "type-devices": "设备", + "list-of-devices": "{ count, plural, 1 {设备} other {# 设备列表} }", + "device-name-starts-with": "以 '{{prefix}}' 开头的设备", + "type-asset": "资产", + "type-assets": "资产", + "list-of-assets": "{ count, plural, 1 {资产} other {# 资产列表} }", + "asset-name-starts-with": "以 '{{prefix}}' 开头的资产", + "type-entity-view": "实体视图", + "type-entity-views": "实体视图", + "list-of-entity-views": "{ count, plural, 1 {实体视图} other {# 实体视图列表} }", + "entity-view-name-starts-with": "以 '{{prefix}}' 开头的实体视图", + "type-rule": "规则", + "type-rules": "规则", + "list-of-rules": "{ count, plural, 1 {规则} other {# 规则列表} }", + "rule-name-starts-with": "以 '{{prefix}}' 开头的规则", + "type-plugin": "插件", + "type-plugins": "插件", + "list-of-plugins": "{ count, plural, 1 {插件} other {# 插件列表} }", + "plugin-name-starts-with": "以 '{{prefix}}' 开头的插件", + "type-tenant": "租户", + "type-tenants": "租户", + "list-of-tenants": "{ count, plural, 1 {租户} other {# 租户列表} }", + "tenant-name-starts-with": "以 '{{prefix}}' 开头的租户", + "type-customer": "客户", + "type-customers": "客户", + "list-of-customers": "{ count, plural, 1 {客户} other {# 客户列表} }", + "customer-name-starts-with": "以 '{{prefix}}' 开头的客户", + "type-user": "用户", + "type-users": "用户", + "list-of-users": "{ count, plural, 1 {用户} other {# 用户列表} }", + "user-name-starts-with": "以 '{{prefix}}' 开头的用户", + "type-dashboard": "仪表板", + "type-dashboards": "仪表板", + "list-of-dashboards": "{ count, plural, 1 {仪表板} other {# 仪表板列表} }", + "dashboard-name-starts-with": "以 '{{prefix}}' 开头的仪表板", + "type-alarm": "警告", + "type-alarms": "警告", + "list-of-alarms": "{ count, plural, 1 {警告} other {# 警告列表} }", + "alarm-name-starts-with": "以 '{{prefix}}' 开头的警告", + "type-rulechain": "规则链", + "type-rulechains": "规则链库", + "list-of-rulechains": "{ count, plural, 1 {一个规则链} other {# 规则链列表} }", + "rulechain-name-starts-with": "规则链前缀名称 '{{prefix}}'", + "type-current-customer": "当前客户", + "search": "实体检索", + "selected-entities": "{ count, plural, 1 {1 实体} other {# 实体} } 被选中", + "entity-name": "实体名", + "details": "实体详情", + "no-entities-prompt": "没有找到实体", + "no-data": "无数据" + }, + "entity-view": { + "entity-view": "实体视图", + "entity-view-required": "实体视图必填。", + "entity-views": "实体视图", + "management": "实体视图管理", + "view-entity-views": "查看实体视图", + "entity-view-alias": "实体视图别名", + "aliases": "实体视图别名", + "no-alias-matching": "'{{alias}}' 没有找到。", + "no-aliases-found": "找不到别名。", + "no-key-matching": "'{{key}}' 没有找到。", + "no-keys-found": "找不到密钥。", + "create-new-alias": "创建一个新的!", + "create-new-key": "创建一个新的!", + "duplicate-alias-error": "找到重复别名 '{{alias}}'。
    实体视图别名必须是唯一的。", + "configure-alias": "配置 '{{alias}}' 别名", + "no-devices-matching": "找不到与 '{{entity}}' 匹配的实体视图。", + "alias": "别名", + "alias-required": "需要实体视图别名。", + "remove-alias": "删除实体视图别名", + "add-alias": "添加实体视图别名", + "name-starts-with": "名称前缀", + "entity-view-list": "实体视图列表", + "use-entity-view-name-filter": "使用过滤器", + "entity-view-list-empty": "没有被选中的实体视图", + "entity-view-name-filter-required": "实体视图名称过滤器必填。", + "entity-view-name-filter-no-entity-view-matched": "找不到以'{{entityView}}' 开头的实体视图。", + "add": "添加实体视图", + "assign-to-customer": "分配给客户", + "assign-entity-view-to-customer": "将实体视图分配给客户", + "assign-entity-view-to-customer-text": "请选择要分配给客户的实体视图", + "no-entity-views-text": "找不到实体视图", + "assign-to-customer-text": "请选择客户分配实体视图", + "entity-view-details": "实体视图详细信息", + "add-entity-view-text": "添加新实体视图", + "delete": "删除实体视图", + "assign-entity-views": "分配实体视图", + "assign-entity-views-text": "分配 { count, plural, 1 {1 实体视图} other {# 实体视图} } 给客户", + "delete-entity-views": "删除实体视图", + "unassign-from-customer": "取消分配客户", + "unassign-entity-views": "取消分配实体视图", + "unassign-entity-views-action-title": "从客户处取消分配{count,plural,1 {1 实体视图} other {# 实体视图} }", + "assign-new-entity-view": "分配新实体视图", + "delete-entity-view-title": "确定要删除实体视图 '{{entityViewName}}'?", + "delete-entity-view-text": "小心!确认后实体视图及其所有相关数据将不可恢复。", + "delete-entity-views-title": "确定要删除 { count, plural, 1 {1 实体视图} other {# 实体视图} }?", + "delete-entity-views-action-title": "删除 { count, plural, 1 {1 实体视图} other {# 实体视图} }", + "delete-entity-views-text": "B小心,确认后,所有选定的实体视图将被删除,所有相关的数据将变得不可恢复。", + "unassign-entity-view-title": "您确定要取消对 '{{entityViewName}}'实体视图的分配吗?", + "unassign-entity-view-text": "确认后,实体视图将未分配,客户无法访问。", + "unassign-entity-view": "未分配实体视图", + "unassign-entity-views-title": "您确定要取消分配 { count, plural, 1 {1 实体视图} other {# 实体视图} }吗?", + "unassign-entity-views-text": "确认后,所有选定的实体视图将被分配,客户无法访问。", + "entity-view-type": "实体视图类型", + "entity-view-type-required": "实体视图类型必填。", + "select-entity-view-type": "选择实体视图类型", + "enter-entity-view-type": "输入实体视图类型", + "any-entity-view": "任何实体视图", + "no-entity-view-types-matching": "没有找到匹配 '{{entitySubtype}}' 的实体视图类型。", + "entity-view-type-list-empty": "实体视图类型未选择。", + "entity-view-types": "实体视图类型", + "name": "名称", + "name-required": "名称必填。", + "description": "描述", + "events": "事件", + "details": "详情", + "copyId": "复制实体视图ID", + "assignedToCustomer": "分配给客户", + "unable-entity-view-device-alias-title": "无法删除实体视图别名", + "unable-entity-view-device-alias-text": "实体视图别名 '{{entityViewAlias}}' 不能够被删除,因为它被下列部件所使用:
    {{widgetsList}}", + "select-entity-view": "选择实体视图", + "make-public": "实体视图设为公开", + "make-private": "实体视图设为私有", + "start-date": "开始日期", + "start-ts": "开始时间", + "end-date": "结束日期", + "end-ts": "结束时间", + "date-limits": "日期限制", + "client-attributes": "客户端属性", + "shared-attributes": "共享属性", + "server-attributes": "服务端属性", + "timeseries": "时间序列", + "client-attributes-placeholder": "客户端属性", + "shared-attributes-placeholder": "共享属性", + "server-attributes-placeholder": "服务端属性", + "timeseries-placeholder": "时间序列", + "target-entity": "目标实体", + "attributes-propagation": "属性传播", + "attributes-propagation-hint": "每次保存或更新这个实体视图时,实体视图将自动从目标实体复制指定的属性。由于性能原因,目标实体属性不会在每次属性更改时传播到实体视图。您可以通过配置\"copy to view\"规则链中的规则节点,并将\"Post attributes\"和\"attributes Updated\"消息链接到新规则节点,从而启用自动传播。", + "timeseries-data": "时间序列数据", + "timeseries-data-hint": "配置目标实体的时间序列数据键,以便实体视图可以访问这些键。这个时间序列数据是只读的。", + "make-public-entity-view-title": "你确定你想创建公开 '{{entityViewName}}' 实体视图?", + "make-public-entity-view-text": "确认后,实体视图 及其所有数据将被公开并被他人访问。", + "make-private-entity-view-title": "你确定你想创建私有 '{{entityViewName}}' 实体视图?", + "make-private-entity-view-text": "确认后,实体视图及其所有数据将被私有化,无法被他人访问。" + }, + "event": { + "event-type": "事件类型", + "type-error": "错误", + "type-lc-event": "生命周期事件", + "type-stats": "类型统计", + "type-debug-rule-node": "调试", + "type-debug-rule-chain": "调试", + "no-events-prompt": "找不到事件", + "error": "错误", + "alarm": "报警", + "event-time": "事件时间", + "server": "服务器", + "body": "整体", + "method": "方法", + "type": "类型", + "entity": "实体", + "message-id": "消息ID", + "message-type": "消息类型", + "data-type": "数据类型", + "relation-type": "关系类型", + "metadata": "元数据", + "data": "数据", + "event": "事件", + "status": "状态", + "success": "成功", + "failed": "失败", + "messages-processed": "消息处理", + "errors-occurred": "错误发生" + }, + "extension": { + "extensions": "扩展", + "selected-extensions": "{ count, plural, 1 {1 扩展} other {# 扩展} } 被选择", + "type": "类型", + "key": "键名", + "value": "值", + "id": "ID", + "extension-id": "扩展ID", + "extension-type": "扩展类型", + "transformer-json": "JSON *", + "unique-id-required": "当前扩展ID已经存在。", + "delete": "删除扩展", + "add": "添加扩展", + "edit": "编辑扩展", + "delete-extension-title": "确实要删除扩展名'{{extensionId}}'吗?", + "delete-extension-text": "小心,确认后,扩展和所有相关数据将变得不可恢复。", + "delete-extensions-title": "您确定要删除 { count, plural, 1 {1 表达式} other {# 表达式} }吗?", + "delete-extensions-text": "小心,确认后,所有选定的扩展将被删除。", + "converters": "转换器", + "converter-id": "转换器序号", + "configuration": "配置", + "converter-configurations": "转换器的配置", + "token": "安全令牌", + "add-converter": "添加转换器", + "add-config": "添加转换器配置", + "device-name-expression": "设备名称表达式", + "device-type-expression": "设备类型表达式", + "custom": "顾客", + "to-double": "加倍", + "transformer": "转换器", + "json-required": "转换器JSON必填。", + "json-parse": "无法解析转换器JSON。", + "attributes": "属性", + "add-attribute": "添加属性", + "add-map": "添加映射元素", + "timeseries": "时间序列", + "add-timeseries": "添加时间序列", + "field-required": "必填字段", + "brokers": "代理服务器组", + "add-broker": "添加代理服务器", + "host": "主机", + "port": "端口", + "port-range": "端口应该在1到65535的范围内。", + "ssl": "Ssl", + "credentials": "证书", + "username": "用户名", + "password": "密码", + "retry-interval": "以毫秒为单位的重试间隔", + "anonymous": "匿名", + "basic": "Basic", + "pem": "PEM", + "ca-cert": "CA证书文件 *", + "private-key": "私钥文件 *", + "cert": "证书文件 *", + "no-file": "没有选择文件。", + "drop-file": "删除文件或单击以选择要上载的文件。", + "mapping": "映射", + "topic-filter": "主题滤波", + "converter-type": "转换类型", + "converter-json": "Json", + "json-name-expression": "设备名称JSON表达式", + "topic-name-expression": "设备名称主题表达式", + "json-type-expression": "设备类型JSON表达式", + "topic-type-expression": "设备类型主题表达式", + "attribute-key-expression": "属性关键字表达式", + "attr-json-key-expression": "属性键JSON表达式", + "attr-topic-key-expression": "属性关键字主题表达式", + "request-id-expression": "请求ID表达式", + "request-id-json-expression": "请求ID JSON表达式", + "request-id-topic-expression": "请求ID主题表达式", + "response-topic-expression": "响应主题表达式", + "value-expression": "值表达式", + "topic": "主题", + "timeout": "毫秒超时", + "converter-json-required": "转换JSON是必需的。", + "converter-json-parse": "无法解析转换JSON。", + "filter-expression": "过滤表达式", + "connect-requests": "连接请求", + "add-connect-request": "添加连接请求", + "disconnect-requests": "断开请求", + "add-disconnect-request": "添加断开请求", + "attribute-requests": "属性请求", + "add-attribute-request": "添加属性请求", + "attribute-updates": "属性更新", + "add-attribute-update": "添加属性更新", + "server-side-rpc": "服务端RPC", + "add-server-side-rpc-request": "添加服务端RPC请求", + "device-name-filter": "设备名称滤波", + "attribute-filter": "属性滤波", + "method-filter": "方法滤波", + "request-topic-expression": "请求主题表达式", + "response-timeout": "毫秒内响应超时", + "topic-expression": "主题表达", + "client-scope": "客户范围", + "add-device": "添加服务器", + "opc-server": "服务器组", + "opc-add-server": "添加服务器", + "opc-add-server-prompt": "请添加服务器", + "opc-application-name": "应用名称", + "opc-application-uri": "应用URI", + "opc-scan-period-in-seconds": "秒级扫描周期", + "opc-security": "安全性", + "opc-identity": "身份", + "opc-keystore": "密钥库", + "opc-type": "类型", + "opc-keystore-type": "类型", + "opc-keystore-location": "位置 *", + "opc-keystore-password": "密码", + "opc-keystore-alias": "别名", + "opc-keystore-key-password": "密钥密码", + "opc-device-node-pattern": "设备节点模式", + "opc-device-name-pattern": "设备名称模式", + "modbus-server": "Servers/slaves", + "modbus-add-server": "添加 server/slave", + "modbus-add-server-prompt": "请添加 server/slave", + "modbus-transport": "传输", + "modbus-port-name": "串口名称", + "modbus-encoding": "编码", + "modbus-parity": "奇偶性", + "modbus-baudrate": "波特率", + "modbus-databits": "数据位", + "modbus-stopbits": "停止位", + "modbus-databits-range": "数据位应该在7到8的范围内。", + "modbus-stopbits-range": "停止位应该在1到2的范围内。", + "modbus-unit-id": "单位编号", + "modbus-unit-id-range": "单位ID应该在1到247的范围内", + "modbus-device-name": "设备名称", + "modbus-poll-period": "轮询周期 (ms)", + "modbus-attributes-poll-period": "轮询属性周期 (ms)", + "modbus-timeseries-poll-period": "时间序列轮询周期 (ms)", + "modbus-poll-period-range": "轮询周期应为正值。", + "modbus-tag": "标签", + "modbus-function": "函数", + "modbus-register-address": "寄存器地址", + "modbus-register-address-range": "寄存器地址应该在0到65535的范围内。", + "modbus-register-bit-index": "位索引", + "modbus-register-bit-index-range": "位索引应该在0到15的范围内。", + "modbus-register-count": "寄存器计数", + "modbus-register-count-range": "寄存器计数应该是一个正值。", + "modbus-byte-order": "字节顺序", + "sync": { + "status": "状态", + "sync": "同步", + "not-sync": "不同步", + "last-sync-time": "最后同步时间", + "not-available": "无法使用" + }, + "export-extensions-configuration": "导出扩展配置", + "import-extensions-configuration": "导入扩展配置", + "import-extensions": "导入扩展", + "import-extension": "导入扩展", + "export-extension": "导出扩展", + "file": "扩展文件", + "invalid-file-error": "无效的扩展文件" + }, + "fullscreen": { + "expand": "展开到全屏", + "exit": "退出全屏", + "toggle": "切换全屏模式", + "fullscreen": "全屏" + }, + "function": { + "function": "函数" + }, + "grid": { + "delete-item-title": "您确定要删除此项吗?", + "delete-item-text": "注意,确认后此项及其所有相关数据将变得不可恢复。", + "delete-items-title": "你确定你要删除 { count, plural, 1 {1 项} other {# 项} }吗?", + "delete-items-action-title": "删除 { count, plural, 1 {1 项} other {# 项} }", + "delete-items-text": "注意,确认后所有选择的项目将被删除,所有相关数据将不可恢复。", + "add-item-text": "添加新项目", + "no-items-text": "没有找到项目", + "item-details": "项目详细信息", + "delete-item": "删除项目", + "delete-items": "删除项目", + "scroll-to-top": "滚动到顶部" + }, + "help": { + "goto-help-page": "转到帮助页面" + }, + "home": { + "home": "首页", + "profile": "属性", + "logout": "注销", + "menu": "菜单", + "avatar": "头像", + "open-user-menu": "打开用户菜单" + }, + "import": { + "no-file": "没有选择文件", + "drop-file": "拖动一个JSON文件或者单击以选择要上传的文件。" + }, + "item": { + "selected": "选择" + }, + "js-func": { + "no-return-error": "函数必须返回值!", + "return-type-mismatch": "函数必须返回 '{{type}}' 类型的值!", + "tidy": "整洁" + }, + "key-val": { + "key": "键名", + "value": "值", + "remove-entry": "删除条目", + "add-entry": "增加条目", + "no-data": "没有条目" + }, + "layout": { + "layout": "布局", + "manage": "布局管理", + "settings": "布局设置", + "color": "颜色", + "main": "主体", + "right": "右侧", + "select": "选择目标布局" + }, + "legend": { + "position": "图例位置", + "show-max": "显示最大值", + "show-min": "显示最小值", + "show-avg": "显示平均值", + "show-total": "显示总数", + "settings": "图例设置", + "min": "最小值", + "max": "最大值", + "avg": "平均值", + "total": "总数" + }, + "login": { + "login": "登录", + "request-password-reset": "请求密码重置", + "reset-password": "重置密码", + "create-password": "创建密码", + "passwords-mismatch-error": "输入的密码必须相同!", + "password-again": "再次输入密码", + "sign-in": "登录 ", + "username": "用户名(电子邮件)", + "remember-me": "记住我", + "forgot-password": "忘记密码?", + "password-reset": "密码重置", + "new-password": "新密码", + "new-password-again": "再次输入新密码", + "password-link-sent-message": "密码重置链接已成功发送!", + "email": "电子邮件" + }, + "position": { + "top": "顶部", + "bottom": "底部", + "left": "左侧", + "right": "右侧" + }, + "profile": { + "profile": "属性", + "change-password": "更改密码", + "current-password": "当前密码" + }, + "relation": { + "relations": "关联", + "direction": "方向", + "search-direction": { + "FROM": "从", + "TO": "到" + }, + "direction-type": { + "FROM": "从", + "TO": "到" + }, + "from-relations": "向外的关联", + "to-relations": "向内的关联", + "selected-relations": "{ count, plural, 1 {1 关联} other {# 关联} } 被选中", + "type": "类型", + "to-entity-type": "到实体类型", + "to-entity-name": "到实体名称", + "from-entity-type": "从实体类型", + "from-entity-name": "从实体类型", + "to-entity": "到实体", + "from-entity": "从实体", + "delete": "删除关联", + "relation-type": "关联类型", + "relation-type-required": "关联类型必填", + "any-relation-type": "任意类型", + "add": "添加关联", + "edit": "编辑关联", + "delete-to-relation-title": "确定要删除实体 '{{entityName}}' 的关联吗?", + "delete-to-relation-text": "确定删除后实体 '{{entityName}}' 将取消与当前实体的关联关系。", + "delete-to-relations-title": "确定要删除 { count, plural, 1 {1 关联} other {# 关联} }?", + "delete-to-relations-text": "确定删除所有选择的关联关系后,与当前实体对应的所有关联关系将被移除。", + "delete-from-relation-title": "确定要从实体 '{{entityName}}' 删除关联吗?", + "delete-from-relation-text": "确定删除后,当前实体将与实体 '{{entityName}}' 取消关联", + "delete-from-relations-title": "确定删除 { count, plural, 1 {1 关联} other {# 关联} } 吗?", + "delete-from-relations-text": "确定删除所有选择的关联关系后,当前实体将与对应的实体取消关联", + "remove-relation-filter": "移除关联过滤器", + "add-relation-filter": "添加关联过滤器", + "any-relation": "任意关联", + "relation-filters": "关联过滤器", + "additional-info": "附加信息 (JSON)", + "invalid-additional-info": "无法解析附加信息json。" + }, + "rulechain": { + "rulechain": "规则链", + "rulechains": "规则链库", + "root": "根实体", + "delete": "删除规则", + "activate": "激活规则", + "suspend": "暂停规则", + "active": "激活", + "suspended": "暂停", + "name": "名称", + "name-required": "名称必填。", + "description": "描述", + "add": "添加规则", + "set-root": "创建规则链根", + "set-root-rulechain-title": "您确定要生成规则链'{{RuleChainName}}'根吗?", + "set-root-rulechain-text": "确认之后,规则链将变为根规格链,并将处理所有传入的传输消息。", + "delete-rulechain-title": " 确实要删除规则链'{{ruleChainName}}'吗?", + "delete-rulechain-text": "小心,在确认规则链和所有相关数据将变得不可恢复。", + "delete-rulechains-title": "确实要删除{count, plural, 1 { 1 规则链} other {# 规则链库} }吗?", + "delete-rulechains-action-title": "删除 { count, plural, 1 {1 规则链} other {# 规则链库} }", + "delete-rulechains-text": "小心,确认后,所有选定的规则链将被删除,所有相关的数据将变得不可恢复。", + "add-rulechain-text": "添加新的规则链", + "no-rulechains-text": "规则链没有发现", + "rulechain-details": "规则链详情", + "details": "详情", + "events": "事件", + "system": "系统", + "import": "导入规则", + "export": "导出规则", + "export-failed-error": "无法导出规则:{{error}}", + "create-new-rulechain": "创建新的规则链", + "rulechain-file": "规则链文件", + "invalid-rulechain-file-error": "不能导入规则链:无效的规则链数据格式。", + "copyId": "复制规则链ID", + "idCopiedMessage": "规则ID已经复制到粘贴板", + "select-rulechain": "选择规则链", + "no-rulechains-matching": "没有发现匹配'{{entity}}'的规则链。", + "rulechain-required": "规则链必填", + "management": "规则集管理", + "debug-mode": "调试模式" + }, + "rulenode": { + "details": "详情", + "events": "事件", + "search": "搜索节点", + "open-node-library": "打开节点库", + "add": "添加规则节点", + "name": "名称", + "name-required": "名称必填。", + "type": "类型", + "description": "描述", + "delete": "删除规则节点", + "select-all-objects": "选择所有节点和连接", + "deselect-all-objects": "取消选择所有节点和连接", + "delete-selected-objects": "删除选定的节点和连接", + "delete-selected": "删除选定", + "select-all": "选择全部", + "copy-selected": "选择副本", + "deselect-all": "取消选择", + "rulenode-details": "规则节点详情", + "debug-mode": "调试模式", + "configuration": "配置", + "link": "链接", + "link-details": "规则节点链接详情", + "add-link": "添加链接", + "link-label": "链接标签", + "link-label-required": "链接标签必填", + "custom-link-label": "自定义链接标签", + "custom-link-label-required": "自定义链接标签必填", + "type-filter": "滤波器", + "type-filter-details": "使用配置条件过滤传入消息", + "type-enrichment": "属性集", + "type-enrichment-details": "向消息元数据中添加附加信息", + "type-transformation": "变换", + "type-transformation-details": "更改消息有效载荷和元数据", + "type-action": "动作", + "type-action-details": "执行特别动作", + "type-external": "外部的", + "type-external-details": "与外部系统交互", + "type-rule-chain": "规则链", + "type-rule-chain-details": "将传入消息转发到指定的规则链", + "type-input": "输入", + "type-input-details": "规则链的逻辑输入,将传入消息转发到下一个相关规则节点", + "directive-is-not-loaded": "定义的配置指令 '{{directiveName}}' 不可用。", + "ui-resources-load-error": "加载配置UI资源失败。", + "invalid-target-rulechain": "无法解析目标规则链!", + "test-script-function": "测试脚本功能", + "message": "消息", + "message-type": "消息类型", + "message-type-required": "消息类型必填", + "metadata": "元数据", + "metadata-required": "元数据项不能为空。", + "output": "输出", + "test": "测试", + "help": "帮助" + }, + "tenant": { + "tenant": "租户", + "tenants": "租户", + "management": "租户管理", + "add": "添加租户", + "admins": "管理员", + "manage-tenant-admins": "管理租户管理员", + "delete": "删除租户", + "add-tenant-text": "添加新租户", + "no-tenants-text": "没有找到租户", + "tenant-details": "租客详情", + "delete-tenant-title": "您确定要删除租户'{{tenantTitle}}'吗?", + "delete-tenant-text": "小心!确认后,租户和所有相关数据将不可恢复。", + "delete-tenants-title": "您确定要删除 {count,plural,1 {1 租户} other {# 租户} } 吗?", + "delete-tenants-action-title": "删除 { count, plural, 1 {1 租户} other {# 租户} }", + "delete-tenants-text": "小心!确认后,所有选定的租户将被删除,所有相关数据将不可恢复。", + "title": "标题", + "title-required": "标题必填。", + "description": "描述", + "details": "详情", + "events": "事件", + "copyId": "复制租户ID", + "idCopiedMessage": "租户ID已经复制到粘贴板", + "select-tenant": "选择租户", + "no-tenants-matching": "没有找到符合 '{{entity}}' 的租户", + "tenant-required": "租户必填" + }, + "timeinterval": { + "seconds-interval": "{ seconds, plural, 1 {1 秒} other {# 秒} }", + "minutes-interval": "{ minutes, plural, 1 {1 分} other {# 分} }", + "hours-interval": "{ hours, plural, 1 {1 小时} other {# 小时} }", + "days-interval": "{ days, plural, 1 {1 天} other {# 天} }", + "days": "天", + "hours": "时", + "minutes": "分", + "seconds": "秒", + "advanced": "高级" + }, + "timewindow": { + "days": "{ days, plural, 1 { 天 } other {# 天 } }", + "hours": "{ hours, plural, 0 { 小时 } 1 {1 小时 } other {# 小时 } }", + "minutes": "{ minutes, plural, 0 { 分 } 1 {1 分 } other {# 分 } }", + "seconds": "{ seconds, plural, 0 { 秒 } 1 {1 秒 } other {# 秒 } }", + "realtime": "实时", + "history": "历史", + "last-prefix": "最后", + "period": "从 {{ startTime }} 到 {{ endTime }}", + "edit": "编辑时间窗口", + "date-range": "日期范围", + "last": "最后", + "time-period": "时间段" + }, + "user": { + "user": "用户", + "users": "用户", + "customer-users": "客户用户", + "tenant-admins": "租户管理员", + "sys-admin": "系统管理员", + "tenant-admin": "租户管理员", + "customer": "客户", + "anonymous": "匿名", + "add": "添加用户", + "delete": "删除用户", + "add-user-text": "添加新用户", + "no-users-text": "找不到用户", + "user-details": "用户详细信息", + "delete-user-title": "您确定要删除用户 '{{userEmail}}' 吗?", + "delete-user-text": "小心!确认后,用户和所有相关数据将不可恢复。", + "delete-users-title": "你确定你要删除 { count, plural, 1 {1 用户} other {# 用户} } 吗?", + "delete-users-action-title": "删除 { count, plural, 1 {1 用户} other {# 用户} }", + "delete-users-text": "小心!确认后,所有选定的用户将被删除,所有相关数据将不可恢复。", + "activation-email-sent-message": "激活电子邮件已成功发送!", + "resend-activation": "重新发送激活", + "email": "电子邮件", + "email-required": "电子邮件必填。", + "invalid-email-format": "无效的邮件格式。", + "first-name": "名字", + "last-name": "姓", + "description": "描述", + "default-dashboard": "默认面板", + "always-fullscreen": "始终全屏", + "select-user": "选择用户", + "no-users-matching": "没有找到符合 '{{entity}}' 的用户。", + "user-required": "用户必填", + "activation-method": "激活方式", + "display-activation-link": "显示激活链接", + "send-activation-mail": "发送激活邮件", + "activation-link": "用户激活链接", + "activation-link-text": "使用该链接 激活 激活用户:", + "copy-activation-link": "复制用户激活链接", + "activation-link-copied-message": "用户激活链接已经复制到粘贴板", + "details": "详细信息" + }, + "value": { + "type": "值类型", + "string": "字符串", + "string-value": "字符串值", + "integer": "数字", + "integer-value": "数字值", + "invalid-integer-value": "整数值无效", + "double": "双精度小数", + "double-value": "双精度小数值", + "boolean": "布尔", + "boolean-value": "布尔值", + "false": "假", + "true": "真", + "long": "Long" + }, + "widget": { + "widget-library": "部件库", + "widget-bundle": "部件包", + "select-widgets-bundle": "选择部件包", + "management": "管理部件", + "editor": "部件编辑器", + "widget-type-not-found": "加载部件配置出错。
    可能关联的\n 部件已经删除了。", + "widget-type-load-error": "由于以下错误未加载小部件:", + "remove": "删除部件", + "edit": "编辑部件", + "remove-widget-title": "确实要删除 '{{widgetTitle}}'部件吗?", + "remove-widget-text": "确认后,控件和所有相关数据将变得不可恢复。", + "timeseries": "时间序列", + "search-data": "搜索数据", + "no-data-found": "没有找到数据", + "latest-values": "最新值", + "rpc": "控件部件", + "alarm": "警告部件", + "static": "静态部件", + "select-widget-type": "选择窗口部件类型", + "missing-widget-title-error": "部件标题必须指定!", + "widget-saved": "部件已保存", + "unable-to-save-widget-error": "无法保存部件!控件有错误!", + "save": "保存部件", + "saveAs": "部件另存为", + "save-widget-type-as": "部件类型另存为", + "save-widget-type-as-text": "请输入新的部件标题或选择目标部件包", + "toggle-fullscreen": "切换全屏", + "run": "运行部件", + "title": "部件标题", + "title-required": "需要部件标题。", + "type": "部件类型", + "resources": "资源", + "resource-url": "JavaScript/CSS URL", + "remove-resource": "删除资源", + "add-resource": "添加资源", + "html": "HTML", + "tidy": "整理", + "css": "CSS", + "settings-schema": "设置模式", + "datakey-settings-schema": "数据键设置模式", + "javascript": "Javascript", + "remove-widget-type-title": "您确定要删除部件类型 '{{widgetName}}'吗?", + "remove-widget-type-text": "确认后,窗口部件类型和所有相关数据将不可恢复。", + "remove-widget-type": "删除部件类型", + "add-widget-type": "添加新的部件类型", + "widget-type-load-failed-error": "无法加载部件类型!", + "widget-template-load-failed-error": "无法加载部件模板!", + "add": "添加部件", + "undo": "撤消部件更改", + "export": "导出部件" + }, + "widget-action": { + "header-button": "部件顶部按钮", + "open-dashboard-state": "切换到新仪表板状态", + "update-dashboard-state": "更新当前仪表板状态", + "open-dashboard": "切换到另一个仪表板", + "custom": "自定义动作", + "target-dashboard-state": "目标仪表板状态", + "target-dashboard-state-required": "目标仪表板状态必填", + "set-entity-from-widget": "从部件中设置实体", + "target-dashboard": "目标仪表板", + "open-right-layout": "打开右侧布局 (移动端视图)" + }, + "widgets-bundle": { + "current": "当前包", + "widgets-bundles": "部件包", + "add": "添加部件包", + "delete": "删除部件包", + "title": "标题", + "title-required": "标题必填。", + "add-widgets-bundle-text": "添加新的部件包", + "no-widgets-bundles-text": "找不到部件包", + "empty": "部件包是空的", + "details": "详情", + "widgets-bundle-details": "部件包详细信息", + "delete-widgets-bundle-title": "您确定要删除部件包 '{{widgetsBundleTitle}}'吗?", + "delete-widgets-bundle-text": "小心!确认后,部件包和所有相关数据将不可恢复。", + "delete-widgets-bundles-title": "你确定你要删除 { count, plural, 1 {1 部件包} other {# 部件包} } 吗?", + "delete-widgets-bundles-action-title": "删除 { count, plural, 1 {1 部件包} other {# 部件包} }", + "delete-widgets-bundles-text": "小心!确认后,所有选定的部件包将被删除,所有相关数据将不可恢复。", + "no-widgets-bundles-matching": "没有找到与 '{{widgetsBundle}}' 匹配的部件包。", + "widgets-bundle-required": "需要部件包。", + "system": "系统", + "import": "导入部件包", + "export": "导出部件包", + "export-failed-error": "无法导出部件包: {{error}}", + "create-new-widgets-bundle": "创建新的部件包", + "widgets-bundle-file": "部件包文件", + "invalid-widgets-bundle-file-error": "无法导入部件包:无效的部件包数据结构。" + }, + "widget-config": { + "data": "数据", + "settings": "设置", + "advanced": "高级", + "title": "标题", + "general-settings": "常规设置", + "display-title": "显示标题", + "drop-shadow": "阴影", + "enable-fullscreen": "启用全屏", + "background-color": "背景颜色", + "text-color": "文字颜色", + "padding": "填充", + "margin": "边缘", + "widget-style": "部件风格", + "title-style": "标题风格", + "mobile-mode-settings": "移动端设置", + "order": "顺序", + "height": "高度", + "units": "特殊符号展示值", + "decimals": "浮点数后的位数", + "timewindow": "时间窗口", + "use-dashboard-timewindow": "使用仪表板的时间窗口", + "display-legend": "显示图例", + "datasources": "数据源", + "maximum-datasources": "最大允许 { count, plural, 1 {1 数据} other {# 数据} }", + "datasource-type": "类型", + "datasource-parameters": "参数", + "remove-datasource": "移除数据源", + "add-datasource": "添加数据源", + "target-device": "目标设备", + "alarm-source": "警告源", + "actions": "动作", + "action": "动作", + "add-action": "添加动作", + "search-actions": "动作检索", + "action-source": "动作源", + "action-source-required": "动作源必填", + "action-name": "动作名称", + "action-name-required": "动作名称必填。", + "action-name-not-unique": "动作名称已经存在。
    统一动作源的动作名称必须唯一。", + "action-icon": "图标", + "action-type": "类型", + "action-type-required": "类型必填", + "edit-action": "编辑动作", + "delete-action": "删除动作", + "delete-action-title": "删除部件动作", + "delete-action-text": "确定要删除部件动作 '{{actionName}}' 吗?" + }, + "widget-type": { + "import": "导入部件类型", + "export": "导出部件类型", + "export-failed-error": "无法导出部件类型: {{error}}", + "create-new-widget-type": "创建新的部件类型", + "widget-type-file": "部件类型文件", + "invalid-widget-type-file-error": "无法导入部件类型:无效的部件类型数据结构。" + }, + "widgets": { + "date-range-navigator": { + "localizationMap": { + "Sun": "周日", + "Mon": "周一", + "Tue": "周二", + "Wed": "周三", + "Thu": "周四", + "Fri": "周五", + "Sat": "周六", + "Jan": "1月", + "Feb": "2月", + "Mar": "3月", + "Apr": "4月", + "May": "5月", + "Jun": "6月", + "Jul": "7月", + "Aug": "8月", + "Sep": "9月", + "Oct": "10月", + "Nov": "11月", + "Dec": "12月", + "January": "一月", + "February": "二月", + "March": "三月", + "April": "四月", + "June": "六月", + "July": "七月", + "August": "八月", + "September": "九月", + "October": "十月", + "November": "十一月", + "December": "十二月", + "Custom Date Range": "自定义日期范围", + "Date Range Template": "日期范围模板", + "Today": "今天", + "Yesterday": "昨天", + "This Week": "本星期", + "Last Week": "上个星期", + "This Month": "这个月", + "Last Month": "上个月", + "Year": "年", + "This Year": "今年", + "Last Year": "去年", + "Date picker": "日期选择器", + "Hour": "小时", + "Day": "天", + "Week": "周", + "2 weeks": "2周", + "Month": "月", + "3 months": "3个月", + "6 months": "6个月", + "Custom interval": "自定义间隔", + "Interval": "间隔", + "Step size": "步长", + "Ok": "Ok" + } + } + }, + "icon": { + "icon": "图标", + "select-icon": "选择图标", + "material-icons": "素材图标", + "show-all": "显示所有图标" + }, + "custom": { + "widget-action": { + "action-cell-button": "动作单元格按钮", + "row-click": "点击行", + "marker-click": "点击标记", + "polygon-click": "单击多边形", + "tooltip-tag-action": "提示标签动作" + } + }, + "language": { + "language": "语言" + } +} diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_TW.json b/ui-ngx/src/assets/locale/locale.constant-zh_TW.json new file mode 100644 index 0000000000..ed387affce --- /dev/null +++ b/ui-ngx/src/assets/locale/locale.constant-zh_TW.json @@ -0,0 +1,1608 @@ +{ + "access": { + "unauthorized": "未授權", + "unauthorized-access": "未授權存取", + "unauthorized-access-text": "您需要登入才能存取這個資源!", + "access-forbidden": "禁止存取", + "access-forbidden-text": "您沒有存取此位置的權限
    如果您仍希望存取此位置,請嘗試使用其他用戶登入。", + "refresh-token-expired": "Session 已過期", + "refresh-token-failed": "無法更新 Session" + }, + "action": { + "activate": "啟動", + "suspend": "暫停", + "save": "儲存", + "saveAs": "另存為", + "cancel": "取消", + "ok": "確定", + "delete": "刪除", + "add": "增加", + "yes": "是", + "no": "否", + "update": "更新", + "remove": "移除", + "search": "查詢", + "clear-search": "清除查詢", + "assign": "分配", + "unassign": "取消分配", + "share": "分享", + "make-private": "私有", + "apply": "應用", + "apply-changes": "應用更改", + "edit-mode": "編輯模式", + "enter-edit-mode": "進入編輯模式", + "decline-changes": "取消更改", + "close": "關閉", + "back": "返回", + "run": "執行", + "sign-in": "登入!", + "edit": "編輯", + "view": "查看", + "create": "建立", + "drag": "拖拉", + "refresh": "更新", + "undo": "取消", + "copy": "複製", + "paste": "貼上", + "copy-reference": "複製引用", + "paste-reference": "貼上引用", + "import": "匯入", + "export": "匯出", + "share-via": "通過 {{provider}}分享" + }, + "aggregation": { + "aggregation": "聚合", + "function": "資料聚合功能", + "limit": "最大值", + "group-interval": "分組間隔", + "min": "最少值", + "max": "最大值", + "avg": "平均值", + "sum": "總計", + "count": "Count", + "none": "空" + }, + "admin": { + "general": "一般", + "general-settings": "一般設定", + "outgoing-mail": "發送郵件", + "outgoing-mail-settings": "發送郵件設定", + "system-settings": "系統設定", + "test-mail-sent": "測試郵件發送成功!", + "base-url": "基本URL", + "base-url-required": "基本URL必填。", + "mail-from": "郵件來自", + "mail-from-required": "郵件發件人必填。", + "smtp-protocol": "SMTP協定", + "smtp-host": "SMTP主機", + "smtp-host-required": "SMTP主機必填。", + "smtp-port": "SMTP連接埠", + "smtp-port-required": "您必須提供一個smtp連接埠。", + "smtp-port-invalid": "這看起來不是有效的smtp連接埠。", + "timeout-msec": "超時(ms)", + "timeout-required": "超時必填。", + "timeout-invalid": "這看起來不像有效的超時值。", + "enable-tls": "啟用TLS", + "tls-version": "TLS版本", + "send-test-mail": "發送測試郵件" + }, + "alarm": { + "alarm": "警告", + "alarms": "警告", + "select-alarm": "選擇警告", + "no-alarms-matching": "沒有找到符合 '{{entity}}' 的警告", + "alarm-required": "警告必填", + "alarm-status": "警告狀態", + "search-status": { + "ANY": "所有", + "ACTIVE": "已啟動", + "CLEARED": "已清除", + "ACK": "已回覆", + "UNACK": "未回覆" + }, + "display-status": { + "ACTIVE_UNACK": "啟動未回覆", + "ACTIVE_ACK": "啟動已回覆", + "CLEARED_UNACK": "清除未回覆", + "CLEARED_ACK": "清除已回覆" + }, + "no-alarms-prompt": "未發現警告", + "created-time": "建立時間", + "type": "類型", + "severity": "嚴重程度", + "originator": "起因", + "originator-type": "起因類型", + "details": "詳細資訊", + "status": "狀態", + "alarm-details": "警告詳細資訊", + "start-time": "開始時間", + "end-time": "結束時間", + "ack-time": "回覆時間", + "clear-time": "建立時間", + "severity-critical": "危險", + "severity-major": "重要", + "severity-minor": "次要", + "severity-warning": "警告", + "severity-indeterminate": "不確定", + "acknowledge": "回覆", + "clear": "清除", + "search": "搜尋警告", + "selected-alarms": "已選擇 { count, plural, 1 {1 警告} other {# 警告} } ", + "no-data": "無資料顯示", + "polling-interval": "警告輪詢間隔(秒)", + "polling-interval-required": "警告輪詢間隔必填。", + "min-polling-interval-message": "輪詢間隔至少是1秒。", + "aknowledge-alarms-title": "回覆 { count, plural, 1 {1 警告} other {# 警告} }", + "aknowledge-alarms-text": "確定要回覆 { count, plural, 1 {1 警告} other {# 警告} }?", + "clear-alarms-title": "清除 { count, plural, 1 {1 警告} other {# 警告} }", + "clear-alarms-text": "確定要清除 { count, plural, 1 {1 警告} other {# 警告} }?" + }, + "alias": { + "add": "增加別名", + "edit": "編輯別名", + "name": "別名", + "name-required": "別名必填", + "duplicate-alias": "別名已經存在。", + "filter-type-single-entity": "單一實體", + "filter-type-entity-list": "實體列表", + "filter-type-entity-name": "實體名稱", + "filter-type-state-entity": "實體(儀表板狀態)", + "filter-type-state-entity-description": "實體令牌(儀表板狀態參數)", + "filter-type-asset-type": "資產類型", + "filter-type-asset-type-description": "類型為 '{{assetType}}' 的資產", + "filter-type-asset-type-and-name-description": "類型為 '{{assetType}}' 且以 '{{prefix}}' 開頭的資產", + "filter-type-device-type": "設備類型", + "filter-type-device-type-description": "類型為 '{{deviceType}}' 的設備", + "filter-type-device-type-and-name-description": "類型為 '{{deviceType}}' 且以 '{{prefix}}' 開頭的設備", + "filter-type-entity-view-type": "實體視圖類型", + "filter-type-entity-view-type-description": "類型為 '{{entityView}}' 的實體視圖", + "filter-type-entity-view-type-and-name-description": "類型為 {{entityView}}' 且以 '{{prefix}}' 開頭的實體視圖", + "filter-type-relations-query": "關係查詢", + "filter-type-relations-query-description": "具有 {{relationType}} 關聯 {{direction}} {{rootEntity}} 的 {{entities}} ", + "filter-type-asset-search-query": "資產搜尋查詢", + "filter-type-asset-search-query-description": "類型為 {{assetTypes}} 且具有 {{relationType}} 關聯 {{direction}} {{rootEntity}} 的資產", + "filter-type-device-search-query": "設備搜尋查詢", + "filter-type-device-search-query-description": "類型為 {{deviceTypes}} 且具有 {{relationType}} 關聯 {{direction}} {{rootEntity}} 的設備", + "filter-type-entity-view-search-query": "實體視圖搜尋查詢", + "filter-type-entity-view-search-query-description": "類型為 {{entityViewTypes}} 且具有 {{relationType}} 關聯 {{direction}} {{rootEntity}} 的實體視圖", + "entity-filter": "實體過濾", + "resolve-multiple": "解決為多實體", + "filter-type": "過濾類型", + "filter-type-required": "過濾類型必填。", + "entity-filter-no-entity-matched": "未找到符合指定過濾條件的實體。", + "no-entity-filter-specified": "沒有指定實體過濾條件", + "root-state-entity": "使用儀表板狀態實體作為根實體", + "root-entity": "根實體", + "state-entity-parameter-name": "狀態實體參數名稱", + "default-state-entity": "預設狀態實體", + "default-entity-parameter-name": "預設", + "max-relation-level": "最大關係層級", + "unlimited-level": "不限層級", + "state-entity": "儀表板狀態實體", + "all-entities": "所有實體", + "any-relation": "不限" + }, + "asset": { + "asset": "資產", + "assets": "資產", + "management": "資產管理", + "view-assets": "查看資產", + "add": "增加資產", + "assign-to-customer": "分配給客戶", + "assign-asset-to-customer": "將資產分配給客戶", + "assign-asset-to-customer-text": "請選擇要分配給客戶的資產", + "no-assets-text": "未找到資產", + "assign-to-customer-text": "請選擇客戶以分配資產", + "public": "公開", + "assignedToCustomer": "分配客戶", + "make-public": "資產設為公開", + "make-private": "資產設為私有", + "unassign-from-customer": "取消分配客戶", + "delete": "刪除資產", + "asset-public": "資產公開", + "asset-type": "資產類型", + "asset-type-required": "資產類型必填。", + "select-asset-type": "選擇資產類型", + "enter-asset-type": "輸入資產類型", + "any-asset": "任何資產", + "no-asset-types-matching": "沒有找到符合 '{{entitySubtype}}' 的資產類型。", + "asset-type-list-empty": "資產類型未選擇。", + "asset-types": "資產類型", + "name": "名稱", + "name-required": "名稱必填。", + "description": "描述", + "type": "類型", + "type-required": "類型必填。", + "details": "詳細資訊", + "events": "事件", + "add-asset-text": "增加新資產", + "asset-details": "資產詳細資訊", + "assign-assets": "分配資產", + "assign-assets-text": "分配 { count, plural, 1 {1 資產} other {# 資產} } 給客戶", + "delete-assets": "刪除資產", + "unassign-assets": "取消分配資產", + "unassign-assets-action-title": "從客戶處取消分配 { count, plural, 1 {1 資產} other {# 資產} } ", + "assign-new-asset": "分配新資產", + "delete-asset-title": "確定要刪除資產 '{{assetName}}'?", + "delete-asset-text": "小心!確認後資產及其所有相關資料將無法恢復。", + "delete-assets-title": "確定要刪除 { count, plural, 1 {1 資產} other {# 資產} }?", + "delete-assets-action-title": "刪除 { count, plural, 1 {1 資產} other {# 資產} }", + "delete-assets-text": "小心,確認後,所有選擇的資產將被刪除,所有相關的資料將變得無法恢復。", + "make-public-asset-title": "你確定你想建立公開'{{assetName}}'資產?", + "make-public-asset-text": "確認後,資產及其所有資料將被公開並被他人存取。", + "make-private-asset-title": "你確定你想建立私有 '{{assetName}}' 資產?", + "make-private-asset-text": "確認後,資產及其所有資料將被私有化,無法被他人存取。", + "unassign-asset-title": "您確定要取消對'{{assetName}}'資產的分配嗎?", + "unassign-asset-text": "確認後,資產將未分配,客戶無法存取。", + "unassign-asset": "未分配資產", + "unassign-assets-title": "您確定要取消分配 { count, plural, 1 {1 資產} other {# 資產} }嗎?", + "unassign-assets-text": "確認後,所有選擇的資產將被分配,客戶無法存取。", + "copyId": "複製資產ID", + "idCopiedMessage": "資產ID已經複製到剪貼簿", + "select-asset": "選擇資產", + "no-assets-matching": "沒有找到符合 '{{entity}}' 的資產。", + "asset-required": "資產必填", + "name-starts-with": "資產名稱以此開頭", + "label": "標籤" + }, + "attribute": { + "attributes": "屬性", + "latest-telemetry": "最新遙測", + "attributes-scope": "設備屬性範圍", + "scope-latest-telemetry": "最新遙測", + "scope-client": "客戶端屬性", + "scope-server": "服務端屬性", + "scope-shared": "共享屬性", + "add": "增加屬性", + "key": "鍵", + "last-update-time": "最後更新時間", + "key-required": "屬性鍵必填。", + "value": "值", + "value-required": "屬性值必填。", + "delete-attributes-title": "您確定要刪除 { count, plural, 1 {1 屬性} other {# 屬性} }嗎?", + "delete-attributes-text": "注意,確認後所有選中的屬性都會被刪除。", + "delete-attributes": "刪除屬性", + "enter-attribute-value": "輸入屬性值", + "show-on-widget": "在部件上顯示", + "widget-mode": "部件模式", + "next-widget": "下一個部件", + "prev-widget": "上一個部件", + "add-to-dashboard": "增加到儀表板", + "add-widget-to-dashboard": "將部件增加到儀表板", + "selected-attributes": "{ count, plural, 1 {1 屬性} other {# 屬性} } 被選中", + "selected-telemetry": "{ count, plural, 1 {1 遙測} other {# 遙測} } 被選中" + }, + "audit-log": { + "audit": "審計", + "audit-logs": "審計日誌", + "timestamp": "時間戳", + "entity-type": "實體類型", + "entity-name": "實體名稱", + "user": "用戶", + "type": "類型", + "status": "狀態", + "details": "詳細資訊", + "type-added": "增加", + "type-deleted": "刪除", + "type-updated": "更新", + "type-attributes-updated": "更新屬性", + "type-attributes-deleted": "刪除屬性", + "type-rpc-call": "RPC調用", + "type-credentials-updated": "更新憑證", + "type-assigned-to-customer": "分配給客戶", + "type-unassigned-from-customer": "未分配給客戶", + "type-activated": "啟動", + "type-suspended": "暫停", + "type-credentials-read": "讀取憑證", + "type-attributes-read": "讀取屬性", + "status-success": "成功", + "status-failure": "失敗", + "audit-log-details": "審計日誌詳細資訊", + "no-audit-logs-prompt": "找不到日誌", + "action-data": "活動資料", + "failure-details": "失敗詳細資訊", + "search": "查找審計日誌", + "clear-search": "清空查找" + }, + "confirm-on-exit": { + "message": "您有未儲存的更改。確定要離開此頁嗎?", + "html-message": "您有未儲存的更改。
    確定要離開此頁面嗎?", + "title": "未儲存的更改" + }, + "contact": { + "country": "國家", + "city": "城市", + "state": "州", + "postal-code": "郵政編碼", + "postal-code-invalid": "只允許數字。", + "address": "地址", + "address2": "地址2", + "phone": "手機", + "email": "郵箱", + "no-address": "無地址" + }, + "common": { + "username": "用戶名", + "password": "密碼", + "enter-username": "輸入用戶名", + "enter-password": "輸入密碼", + "enter-search": "輸入檢索條件", + "created-time": "建立時間" + }, + "content-type": { + "json": "Json", + "text": "Text", + "binary": "Binary (Base64)" + }, + "customer": { + "customer": "客戶", + "customers": "客戶", + "management": "客戶管理", + "dashboard": "客戶儀表板", + "dashboards": "客戶儀表板", + "devices": "客戶設備", + "entity-views": "客戶實體視圖", + "assets": "客戶資產", + "public-dashboards": "公共儀表板", + "public-devices": "公共設備", + "public-assets": "公共資產", + "public-entity-views": "公共實體視圖", + "add": "增加客戶", + "delete": "刪除客戶", + "manage-customer-users": "管理客戶用戶", + "manage-customer-devices": "管理客戶設備", + "manage-customer-dashboards": "管理客戶儀表板", + "manage-public-devices": "管理公共設備", + "manage-public-dashboards": "管理公共儀表板", + "manage-customer-assets": "管理客戶資產", + "manage-public-assets": "管理公共資產", + "add-customer-text": "增加新客戶", + "no-customers-text": "沒有找到客戶", + "customer-details": "客戶詳細資訊", + "delete-customer-title": "您確定要刪除客戶'{{customerTitle}}'嗎?", + "delete-customer-text": "小心!確認後,客戶及其所有相關資料將無法恢復。", + "delete-customers-title": "您確定要刪除 { count, plural, 1 {1 客戶} other {# 客戶} }嗎?", + "delete-customers-action-title": "刪除 { count, plural, 1 {1 客戶} other {# 客戶} }", + "delete-customers-text": "小心!確認後,所有選擇的客戶將被刪除,所有相關資料將無法恢復。", + "manage-users": "管理用戶", + "manage-assets": "管理資產", + "manage-devices": "管理設備", + "manage-dashboards": "管理儀表板", + "title": "標題", + "title-required": "需要標題", + "description": "描述", + "details": "詳細資訊", + "events": "事件", + "copyId": "複製客戶ID", + "idCopiedMessage": "客戶ID已複製到剪貼板", + "select-customer": "選擇客戶", + "no-customers-matching": "沒有找到符合 '{{entity}}' 的客戶。", + "customer-required": "客戶是必選項", + "select-default-customer": "選擇預設的客戶", + "default-customer": "預設客戶", + "default-customer-required": "為了測試租戶級別上的儀表板,需要預設客戶。" + }, + "datetime": { + "date-from": "日期從", + "time-from": "時間從", + "date-to": "日期到", + "time-to": "時間到" + }, + "dashboard": { + "dashboard": "儀表板", + "dashboards": "儀表板庫", + "management": "儀表板管理", + "view-dashboards": "查看儀表板", + "add": "增加儀表板", + "assign-dashboard-to-customer": "將儀表板分配給客戶", + "assign-dashboard-to-customer-text": "請選擇要分配給客戶的儀表板", + "assign-to-customer-text": "請選擇客戶分配儀表板", + "assign-to-customer": "分配給客戶", + "unassign-from-customer": "取消分配客戶", + "make-public": "儀表板設為公開", + "make-private": "儀表板設為私有", + "manage-assigned-customers": "管理已分配的客戶", + "assigned-customers": "已分配的客戶", + "assign-to-customers": "將儀表板分配給客戶", + "assign-to-customers-text": "請選擇客戶指定儀表板", + "unassign-from-customers": "客戶未分配儀表板", + "unassign-from-customers-text": "請選擇從儀表板中取消分配的客戶", + "no-dashboards-text": "沒有找到儀表板", + "no-widgets": "沒有配置部件", + "add-widget": "增加新的部件", + "title": "標題", + "select-widget-title": "選擇部件", + "select-widget-subtitle": "可用的部件類型列表", + "delete": "刪除儀表板", + "title-required": "需要標題。", + "description": "描述", + "details": "詳細資訊", + "dashboard-details": "儀表板詳細資訊", + "add-dashboard-text": "增加新的儀表板", + "assign-dashboards": "分配儀表板", + "assign-new-dashboard": "分配新的儀表板", + "assign-dashboards-text": "分配 { count, plural, 1 {1 儀表板} other {# 儀表板} } 給客戶", + "unassign-dashboards-action-text": "未分配 { count, plural, 1 {1 儀表板} other {# 儀表板} } 給客戶", + "delete-dashboards": "刪除儀表板", + "unassign-dashboards": "取消分配儀表板", + "unassign-dashboards-action-title": "從客戶處取消分配 { count, plural, 1 {1 儀表板} other {# 儀表板} } ", + "delete-dashboard-title": "您確定要刪除儀表板 '{{dashboardTitle}}'嗎?", + "delete-dashboard-text": "小心!確認後儀表板及其所有相關資料將無法恢復。", + "delete-dashboards-title": "你確定你要刪除 { count, plural, 1 {1 儀表板} other {# 儀表板} }嗎?", + "delete-dashboards-action-title": "刪除 { count, plural, 1 {1 儀表板} other {# 儀表板} }", + "delete-dashboards-text": "小心!確認後所有選擇的儀表板將被刪除,所有相關資料將無法恢復。", + "unassign-dashboard-title": "您確定要取消分配儀表板 '{{dashboardTitle}}'嗎?", + "unassign-dashboard-text": "確認後,面板將被取消分配,客戶將無法存取。", + "unassign-dashboard": "取消分配儀表板", + "unassign-dashboards-title": "您確定要取消分配儀表板 { count, plural, 1 {1 儀表板} other {# 儀表板} } 嗎?", + "unassign-dashboards-text": "確認後,所有選擇的儀表板將被取消分配,客戶將無法存取。", + "public-dashboard-title": "儀表板現已公佈", + "public-dashboard-text": "你的儀表板{{dashboardTitle}} 已被公開,可通過如下連結存取:", + "public-dashboard-notice": "提示: 不要忘記將相關設備公開以存取其資料。", + "make-private-dashboard-title": "您確定要將儀表板 '{{dashboardTitle}}' 設為私有嗎?", + "make-private-dashboard-text": "確認後,儀表板將被設為私有,不能被其他人存取。", + "make-private-dashboard": "儀表板設為私有", + "socialshare-text": "'{{dashboardTitle}}' 由Thingsboard提供支持", + "socialshare-title": "'{{dashboardTitle}}' 由Thingsboard提供支持", + "select-dashboard": "選擇儀表板", + "no-dashboards-matching": "找不到符合 '{{entity}}' 的儀表板。", + "dashboard-required": "儀表板必填。", + "select-existing": "選擇現有儀表板", + "create-new": "建立新的儀表板", + "new-dashboard-title": "新儀表板標題", + "open-dashboard": "打開儀表板", + "set-background": "設定背景", + "background-color": "背景顏色", + "background-image": "背景圖片", + "background-size-mode": "背景大小模式", + "no-image": "無圖像選擇", + "drop-image": "拖拉圖像或單擊以選擇要上傳的文件。", + "settings": "設定", + "columns-count": "列數", + "columns-count-required": "需要列數。", + "min-columns-count-message": "只允許最少10列", + "max-columns-count-message": "只允許最多1000列", + "widgets-margins": "部件間邊距", + "horizontal-margin": "水平邊距", + "horizontal-margin-required": "需要水平邊距值。", + "min-horizontal-margin-message": "只允許0作為最小水平邊距值。", + "max-horizontal-margin-message": "只允許50作為最大水平邊距值。", + "vertical-margin": "垂直邊距", + "vertical-margin-required": "需要垂直邊距值。", + "min-vertical-margin-message": "只允許0作為最小垂直邊距值。", + "max-vertical-margin-message": "只允許50作為最大垂直邊距值。", + "autofill-height": "自動填充佈局高度", + "mobile-layout": "移動端佈局設定", + "mobile-row-height": "移動端行高距(px)", + "mobile-row-height-required": "移動端行高距必填。", + "min-mobile-row-height-message": "移動端行高距至少5px。", + "max-mobile-row-height-message": "移動端行高距最多200px。", + "display-title": "顯示儀表板標題", + "toolbar-always-open": "工具欄常駐", + "title-color": "標題顏色", + "display-dashboards-selection": "顯示儀表板選項", + "display-entities-selection": "顯示實體選項", + "display-dashboard-timewindow": "顯示時間窗口", + "display-dashboard-export": "顯示匯出", + "import": "匯入儀表板", + "export": "匯出儀表板", + "export-failed-error": "無法匯出儀表板: {{error}}", + "create-new-dashboard": "建立新的儀表板", + "dashboard-file": "儀表板文件", + "invalid-dashboard-file-error": "無法匯入儀表板: 儀表板資料結構無效。", + "dashboard-import-missing-aliases-title": "配置匯入儀表板使用的別名", + "create-new-widget": "建立新部件", + "import-widget": "匯入部件", + "widget-file": "部件文件", + "invalid-widget-file-error": "無法匯入窗口部件: 窗口部件資料結構無效。", + "widget-import-missing-aliases-title": "配置匯入的窗口部件使用的別名", + "open-toolbar": "打開儀表板工具欄", + "close-toolbar": "關閉工具欄", + "configuration-error": "配置錯誤", + "alias-resolution-error-title": "儀表板別名配置錯誤", + "invalid-aliases-config": "無法找到與某些別名過濾器符合的任何設備。
    請聯繫您的管理員以解決此問題。", + "select-devices": "選擇設備", + "assignedToCustomer": "分配給客戶", + "public": "公共", + "public-link": "公共連結", + "copy-public-link": "複製公共連結", + "public-link-copied-message": "儀表板的公共連結已被複製到剪貼板", + "manage-states": "儀表板狀態管理", + "states": "儀表板狀態", + "search-states": "儀表板狀態檢索", + "selected-states": "{ count, plural, 1 {1 儀表板狀態} other {# 儀表板狀態} } 選中", + "edit-state": "儀表板狀態編輯", + "delete-state": "刪除儀表板狀態", + "add-state": "增加儀表板狀態", + "state": "儀表板狀態", + "state-name": "狀態名", + "state-name-required": "儀表板狀態名必填。", + "state-id": "狀態ID", + "state-id-required": "儀表板狀態ID必填。", + "state-id-exists": "儀表板狀態ID已經存在。", + "is-root-state": "根狀態", + "delete-state-title": "刪除儀表板狀態", + "delete-state-text": "確定要刪除儀表板狀態 '{{stateName}}' 嗎?", + "show-details": "顯示詳細資訊", + "hide-details": "隱藏詳細資訊", + "select-state": "選擇目標狀態", + "state-controller": "狀態控制" + }, + "datakey": { + "settings": "設定", + "advanced": "進階", + "label": "標籤", + "color": "顏色", + "units": "單位符號", + "decimals": "小數位數", + "data-generation-func": "資料生成功能", + "use-data-post-processing-func": "使用資料後處理功能", + "configuration": "資料鍵配置", + "timeseries": "時間序列", + "attributes": "屬性", + "alarm": "報警字段", + "timeseries-required": "需要設備時間序列。", + "timeseries-or-attributes-required": "設備時間/屬性必填。", + "maximum-timeseries-or-attributes": "最大允許 { count, plural, 1 {1 時間序列/屬性} other {# 時間序列/屬性} }", + "alarm-fields-required": "警告字段必填。", + "function-types": "函數類型", + "function-types-required": "需要函數類型。", + "maximum-function-types": "至少需要 { count, plural, 1 {1 函數類型} other {# 函數類型} }" + }, + "datasource": { + "type": "資料源類型", + "name": "資料源名稱", + "add-datasource-prompt": "請增加資料源" + }, + "details": { + "edit-mode": "編輯模式", + "toggle-edit-mode": "切換編輯模式" + }, + "device": { + "device": "設備", + "device-required": "設備必填", + "devices": "設備", + "management": "設備管理", + "view-devices": "查看設備", + "device-alias": "設備別名", + "aliases": "設備別名", + "no-alias-matching": "'{{alias}}' 沒有找到。", + "no-aliases-found": "找不到別名。", + "no-key-matching": "'{{key}}' 沒有找到。", + "no-keys-found": "找不到密鑰。", + "create-new-alias": "建立一個新的!", + "create-new-key": "建立一個新的!", + "duplicate-alias-error": "找到重複別名 '{{alias}}'。
    設備別名必須是唯一的。", + "configure-alias": "配置 '{{alias}}' 別名", + "no-devices-matching": "找不到與 '{{entity}}' 符合的設備。", + "alias": "別名", + "alias-required": "需要設備別名。", + "remove-alias": "刪除設備別名", + "add-alias": "增加設備別名", + "name-starts-with": "名稱前綴", + "device-list": "設備列表", + "use-device-name-filter": "使用過濾器", + "device-list-empty": "沒有被選中的設備", + "device-name-filter-required": "設備名稱過濾器必填。", + "device-name-filter-no-device-matched": "找不到以'{{device}}' 開頭的設備。", + "add": "增加設備", + "assign-to-customer": "分配給客戶", + "assign-device-to-customer": "將設備分配給客戶", + "assign-device-to-customer-text": "請選擇要分配給客戶的設備", + "make-public": "公開", + "make-private": "私有", + "no-devices-text": "找不到設備", + "assign-to-customer-text": "請選擇客戶分配設備", + "device-details": "設備詳細訊息", + "add-device-text": "增加新設備", + "credentials": "憑據", + "manage-credentials": "管理憑據", + "delete": "刪除設備", + "assign-devices": "分配設備", + "assign-devices-text": "將{count,plural,1 {1 設備} other {# 設備} }分配給客戶", + "delete-devices": "刪除設備", + "unassign-from-customer": "取消分配客戶", + "unassign-devices": "取消分配設備", + "unassign-devices-action-title": "從客戶處取消分配{count,plural,1 {1 設備} other {# 設備} }", + "assign-new-device": "分配新設備", + "make-public-device-title": "您確定要將設備 '{{deviceName}}' 設為公開嗎?", + "make-public-device-text": "確認後,設備及其所有資料將被設為公開並可被其他人存取。", + "make-private-device-title": "您確定要將設備 '{{deviceName}}' 設為私有嗎?", + "make-private-device-text": "確認後,設備及其所有資料將被設為私有,不被其他人存取。", + "view-credentials": "查看憑據", + "delete-device-title": "您確定要刪除設備的{{deviceName}}嗎?", + "delete-device-text": "小心!確認後設備及其所有相關資料將無法恢復。", + "delete-devices-title": "您確定要刪除{count,plural,1 {1 設備} other {# 設備} } 嗎?", + "delete-devices-action-title": "刪除 {count,plural,1 {1 設備} other {# 設備} }", + "delete-devices-text": "小心!確認後所有選擇的設備將被刪除,所有相關資料將無法恢復。", + "unassign-device-title": "您確定要取消分配設備 '{{deviceName}}'?", + "unassign-device-text": "確認後,設備將被取消分配,客戶將無法存取。", + "unassign-device": "取消分配設備", + "unassign-devices-title": "您確定要取消分配{count,plural,1 {1 設備} other {# 設備} } 嗎?", + "unassign-devices-text": "確認後,所有選擇的設備將被取消分配,並且客戶將無法存取。", + "device-credentials": "設備憑據", + "credentials-type": "憑據類型", + "access-token": "存取令牌", + "access-token-required": "需要存取令牌", + "access-token-invalid": "存取令牌長度必須為1到20個字符。", + "rsa-key": "RSA公鑰", + "rsa-key-required": "需要RSA公鑰", + "secret": "密鑰", + "secret-required": "密鑰必填", + "device-type": "設備類型", + "device-type-required": "設備類型必填。", + "select-device-type": "選擇設備類型", + "enter-device-type": "輸入設備類型", + "any-device": "任意設備", + "no-device-types-matching": "沒有找到符合 '{{entitySubtype}}' 的設備類型。", + "device-type-list-empty": "未選擇設備類型", + "device-types": "設備類型", + "name": "名稱", + "name-required": "名稱必填。", + "description": "說明", + "events": "事件", + "details": "詳細訊息", + "copyId": "複製設備ID", + "copyAccessToken": "複製存取令牌", + "idCopiedMessage": "設備ID已複製到剪貼板", + "accessTokenCopiedMessage": "設備存取令牌已複製到剪貼板", + "assignedToCustomer": "分配給客戶", + "unable-delete-device-alias-title": "無法刪除設備別名", + "unable-delete-device-alias-text": "設備別名 '{{deviceAlias}}' 不能夠被刪除,因為它被下列部件所使用:
    {{widgetsList}}", + "is-gateway": "是閘道", + "public": "公開", + "device-public": "設備公開", + "select-device": "選擇設備" + }, + "dialog": { + "close": "關閉對話框" + }, + "error": { + "unable-to-connect": "無法連接到伺服器!請檢查您的互聯網連接。", + "unhandled-error-code": "未處理的錯誤代碼: {{errorCode}}", + "unknown-error": "未知錯誤" + }, + "entity": { + "entity": "實體", + "entities": "實體", + "aliases": "實體別名", + "entity-alias": "實體別名", + "unable-delete-entity-alias-title": "無法刪除實體別名", + "unable-delete-entity-alias-text": "實體別名 '{{entityAlias}}' 被以下部件使用不能刪除:
    {{widgetsList}}", + "duplicate-alias-error": "別名 '{{alias}}' 重複。
    同一儀表板別名必須唯一。", + "missing-entity-filter-error": "別名 '{{alias}}' 缺少過濾器", + "configure-alias": "別名 '{{alias}}' 配置", + "alias": "別名", + "alias-required": "實體別名必填。", + "remove-alias": "移除實體別名", + "add-alias": "增加實體別名", + "entity-list": "實體列表", + "entity-type": "實體類型", + "entity-types": "實體類型", + "entity-type-list": "實體類型列表", + "any-entity": "任意實體", + "enter-entity-type": "輸入實體類型", + "no-entities-matching": "沒有找到符合 '{{entity}}' 的實體。", + "no-entity-types-matching": "沒有找到符合 '{{entityType}}' 類型的實體。", + "name-starts-with": "名稱開始於", + "use-entity-name-filter": "用戶過濾", + "entity-list-empty": "沒有選擇實體。", + "entity-type-list-empty": "沒有選擇實體類型。", + "entity-name-filter-required": "實體名過濾器必填。", + "entity-name-filter-no-entity-matched": "沒有找到以 '{{entity}}' 開頭的實體", + "all-subtypes": "所有", + "select-entities": "選擇實體", + "no-aliases-found": "沒有找到別名", + "no-alias-matching": "沒有找到 '{{alias}}'", + "create-new-alias": "建立新別名", + "key": "鍵", + "key-name": "鍵名", + "no-keys-found": "沒有找到鍵", + "no-key-matching": "沒有找到鍵 '{{key}}'", + "create-new-key": "建立新鍵", + "type": "類型", + "type-required": "實體類型必填。", + "type-device": "設備", + "type-devices": "設備", + "list-of-devices": "{ count, plural, 1 {設備} other {# 設備列表} }", + "device-name-starts-with": "以 '{{prefix}}' 開頭的設備", + "type-asset": "資產", + "type-assets": "資產", + "list-of-assets": "{ count, plural, 1 {資產} other {# 資產列表} }", + "asset-name-starts-with": "以 '{{prefix}}' 開頭的資產", + "type-entity-view": "實體視圖", + "type-entity-views": "實體視圖", + "list-of-entity-views": "{ count, plural, 1 {實體視圖} other {# 實體視圖列表} }", + "entity-view-name-starts-with": "以 '{{prefix}}' 開頭的實體視圖", + "type-rule": "規則", + "type-rules": "規則", + "list-of-rules": "{ count, plural, 1 {規則} other {# 規則列表} }", + "rule-name-starts-with": "以 '{{prefix}}' 開頭的規則", + "type-plugin": "插件", + "type-plugins": "插件", + "list-of-plugins": "{ count, plural, 1 {插件} other {# 插件列表} }", + "plugin-name-starts-with": "以 '{{prefix}}' 開頭的插件", + "type-tenant": "租戶", + "type-tenants": "租戶", + "list-of-tenants": "{ count, plural, 1 {租戶} other {# 租戶列表} }", + "tenant-name-starts-with": "以 '{{prefix}}' 開頭的租戶", + "type-customer": "客戶", + "type-customers": "客戶", + "list-of-customers": "{ count, plural, 1 {客戶} other {# 客戶列表} }", + "customer-name-starts-with": "以 '{{prefix}}' 開頭的客戶", + "type-user": "用戶", + "type-users": "用戶", + "list-of-users": "{ count, plural, 1 {用戶} other {# 用戶列表} }", + "user-name-starts-with": "以 '{{prefix}}' 開頭的用戶", + "type-dashboard": "儀表板", + "type-dashboards": "儀表板", + "list-of-dashboards": "{ count, plural, 1 {儀表板} other {# 儀表板列表} }", + "dashboard-name-starts-with": "以 '{{prefix}}' 開頭的儀表板", + "type-alarm": "警告", + "type-alarms": "警告", + "list-of-alarms": "{ count, plural, 1 {警告} other {# 警告列表} }", + "alarm-name-starts-with": "以 '{{prefix}}' 開頭的警告", + "type-rulechain": "規則鏈", + "type-rulechains": "規則鏈庫", + "list-of-rulechains": "{ count, plural, 1 {一個規則鏈} other {# 規則鏈列表} }", + "rulechain-name-starts-with": "規則鏈前綴名稱 '{{prefix}}'", + "type-current-customer": "當前客戶", + "search": "實體檢索", + "selected-entities": "{ count, plural, 1 {1 實體} other {# 實體} } 被選中", + "entity-name": "實體名", + "details": "實體詳細資訊", + "no-entities-prompt": "沒有找到實體", + "no-data": "無資料" + }, + "entity-view": { + "entity-view": "實體視圖", + "entity-view-required": "實體視圖必填。", + "entity-views": "實體視圖", + "management": "實體視圖管理", + "view-entity-views": "查看實體視圖", + "entity-view-alias": "實體視圖別名", + "aliases": "實體視圖別名", + "no-alias-matching": "'{{alias}}' 沒有找到。", + "no-aliases-found": "找不到別名。", + "no-key-matching": "'{{key}}' 沒有找到。", + "no-keys-found": "找不到密鑰。", + "create-new-alias": "建立一個新的!", + "create-new-key": "建立一個新的!", + "duplicate-alias-error": "找到重複別名 '{{alias}}'。
    實體視圖別名必須是唯一的。", + "configure-alias": "配置 '{{alias}}' 別名", + "no-devices-matching": "找不到與 '{{entity}}' 符合的實體視圖。", + "alias": "別名", + "alias-required": "需要實體視圖別名。", + "remove-alias": "刪除實體視圖別名", + "add-alias": "增加實體視圖別名", + "name-starts-with": "名稱前綴", + "entity-view-list": "實體視圖列表", + "use-entity-view-name-filter": "使用過濾器", + "entity-view-list-empty": "沒有被選中的實體視圖", + "entity-view-name-filter-required": "實體視圖名稱過濾器必填。", + "entity-view-name-filter-no-entity-view-matched": "找不到以'{{entityView}}' 開頭的實體視圖。", + "add": "增加實體視圖", + "assign-to-customer": "分配給客戶", + "assign-entity-view-to-customer": "將實體視圖分配給客戶", + "assign-entity-view-to-customer-text": "請選擇要分配給客戶的實體視圖", + "no-entity-views-text": "找不到實體視圖", + "assign-to-customer-text": "請選擇客戶分配實體視圖", + "entity-view-details": "實體視圖詳細訊息", + "add-entity-view-text": "增加新實體視圖", + "delete": "刪除實體視圖", + "assign-entity-views": "分配實體視圖", + "assign-entity-views-text": "分配 { count, plural, 1 {1 實體視圖} other {# 實體視圖} } 給客戶", + "delete-entity-views": "刪除實體視圖", + "unassign-from-customer": "取消分配客戶", + "unassign-entity-views": "取消分配實體視圖", + "unassign-entity-views-action-title": "從客戶處取消分配{count,plural,1 {1 實體視圖} other {# 實體視圖} }", + "assign-new-entity-view": "分配新實體視圖", + "delete-entity-view-title": "確定要刪除實體視圖 '{{entityViewName}}'?", + "delete-entity-view-text": "小心!確認後實體視圖及其所有相關資料將無法恢復。", + "delete-entity-views-title": "確定要刪除 { count, plural, 1 {1 實體視圖} other {# 實體視圖} }?", + "delete-entity-views-action-title": "刪除 { count, plural, 1 {1 實體視圖} other {# 實體視圖} }", + "delete-entity-views-text": "B小心,確認後,所有選擇的實體視圖將被刪除,所有相關的資料將變得無法恢復。", + "unassign-entity-view-title": "您確定要取消對 '{{entityViewName}}'實體視圖的分配嗎?", + "unassign-entity-view-text": "確認後,實體視圖將未分配,客戶無法存取。", + "unassign-entity-view": "未分配實體視圖", + "unassign-entity-views-title": "您確定要取消分配 { count, plural, 1 {1 實體視圖} other {# 實體視圖} }嗎?", + "unassign-entity-views-text": "確認後,所有選擇的實體視圖將被分配,客戶無法存取。", + "entity-view-type": "實體視圖類型", + "entity-view-type-required": "實體視圖類型必填。", + "select-entity-view-type": "選擇實體視圖類型", + "enter-entity-view-type": "輸入實體視圖類型", + "any-entity-view": "任何實體視圖", + "no-entity-view-types-matching": "沒有找到符合 '{{entitySubtype}}' 的實體視圖類型。", + "entity-view-type-list-empty": "實體視圖類型未選擇。", + "entity-view-types": "實體視圖類型", + "name": "名稱", + "name-required": "名稱必填。", + "description": "描述", + "events": "事件", + "details": "詳細資訊", + "copyId": "複製實體視圖ID", + "assignedToCustomer": "分配給客戶", + "unable-entity-view-device-alias-title": "無法刪除實體視圖別名", + "unable-entity-view-device-alias-text": "實體視圖別名 '{{entityViewAlias}}' 不能夠被刪除,因為它被下列部件所使用:
    {{widgetsList}}", + "select-entity-view": "選擇實體視圖", + "make-public": "實體視圖設為公開", + "make-private": "實體視圖設為私有", + "start-date": "開始日期", + "start-ts": "開始時間", + "end-date": "結束日期", + "end-ts": "結束時間", + "date-limits": "日期限製", + "client-attributes": "客戶端屬性", + "shared-attributes": "共享屬性", + "server-attributes": "服務端屬性", + "timeseries": "時間序列", + "client-attributes-placeholder": "客戶端屬性", + "shared-attributes-placeholder": "共享屬性", + "server-attributes-placeholder": "服務端屬性", + "timeseries-placeholder": "時間序列", + "target-entity": "目標實體", + "attributes-propagation": "屬性傳播", + "attributes-propagation-hint": "每次儲存或更新這個實體視圖時,實體視圖將自動從目標實體複製指定的屬性。由於性能原因,目標實體屬性不會在每次屬性更改時傳播到實體視圖。您可以通過配置\"copy to view\"規則鏈中的規則節點,並將\"Post attributes\"和\"attributes Updated\"消息連結到新規則節點,從而啟用自動傳播。", + "timeseries-data": "時間序列資料", + "timeseries-data-hint": "配置目標實體的時間序列資料鍵,以便實體視圖可以存取這些鍵。這個時間序列資料是只讀的。", + "make-public-entity-view-title": "你確定你想建立公開 '{{entityViewName}}' 實體視圖?", + "make-public-entity-view-text": "確認後,實體視圖 及其所有資料將被公開並被他人存取。", + "make-private-entity-view-title": "你確定你想建立私有 '{{entityViewName}}' 實體視圖?", + "make-private-entity-view-text": "確認後,實體視圖及其所有資料將被私有化,無法被他人存取。" + }, + "event": { + "event-type": "事件類型", + "type-error": "錯誤", + "type-lc-event": "生命週期事件", + "type-stats": "類型統計", + "type-debug-rule-node": "測試", + "type-debug-rule-chain": "測試", + "no-events-prompt": "找不到事件", + "error": "錯誤", + "alarm": "報警", + "event-time": "事件時間", + "server": "伺服器", + "body": "整體", + "method": "方法", + "type": "類型", + "entity": "實體", + "message-id": "消息ID", + "message-type": "消息類型", + "data-type": "資料類型", + "relation-type": "關係類型", + "metadata": "元資料", + "data": "資料", + "event": "事件", + "status": "狀態", + "success": "成功", + "failed": "失敗", + "messages-processed": "消息處理", + "errors-occurred": "錯誤發生" + }, + "extension": { + "extensions": "擴展", + "selected-extensions": "{ count, plural, 1 {1 擴展} other {# 擴展} } 被選擇", + "type": "類型", + "key": "鍵名", + "value": "值", + "id": "ID", + "extension-id": "擴展ID", + "extension-type": "擴展類型", + "transformer-json": "JSON *", + "unique-id-required": "當前擴展ID已經存在。", + "delete": "刪除擴展", + "add": "增加擴展", + "edit": "編輯擴展", + "delete-extension-title": "確實要刪除擴展名'{{extensionId}}'嗎?", + "delete-extension-text": "小心,確認後,擴展和所有相關資料將變得無法恢復。", + "delete-extensions-title": "您確定要刪除 { count, plural, 1 {1 表達式} other {# 表達式} }嗎?", + "delete-extensions-text": "小心,確認後,所有選擇的擴展將被刪除。", + "converters": "轉換器", + "converter-id": "轉換器序號", + "configuration": "配置", + "converter-configurations": "轉換器的配置", + "token": "安全令牌", + "add-converter": "增加轉換器", + "add-config": "增加轉換器配置", + "device-name-expression": "設備名稱表達式", + "device-type-expression": "設備類型表達式", + "custom": "顧客", + "to-double": "加倍", + "transformer": "轉換器", + "json-required": "轉換器JSON必填。", + "json-parse": "無法解析轉換器JSON。", + "attributes": "屬性", + "add-attribute": "增加屬性", + "add-map": "增加映射元素", + "timeseries": "時間序列", + "add-timeseries": "增加時間序列", + "field-required": "必填字段", + "brokers": "代理伺服器組", + "add-broker": "增加代理伺服器", + "host": "主機", + "port": "連接埠", + "port-range": "連接埠應該在1到65535的範圍內。", + "ssl": "Ssl", + "credentials": "證書", + "username": "用戶名", + "password": "密碼", + "retry-interval": "以毫秒為單位的重試間隔", + "anonymous": "匿名", + "basic": "Basic", + "pem": "PEM", + "ca-cert": "CA證書文件 *", + "private-key": "私鑰文件 *", + "cert": "證書文件 *", + "no-file": "沒有選擇文件。", + "drop-file": "刪除文件或單擊以選擇要上載的文件。", + "mapping": "映射", + "topic-filter": "主題濾波", + "converter-type": "轉換類型", + "converter-json": "Json", + "json-name-expression": "設備名稱JSON表達式", + "topic-name-expression": "設備名稱主題表達式", + "json-type-expression": "設備類型JSON表達式", + "topic-type-expression": "設備類型主題表達式", + "attribute-key-expression": "屬性關鍵字表達式", + "attr-json-key-expression": "屬性鍵JSON表達式", + "attr-topic-key-expression": "屬性關鍵字主題表達式", + "request-id-expression": "請求ID表達式", + "request-id-json-expression": "請求ID JSON表達式", + "request-id-topic-expression": "請求ID主題表達式", + "response-topic-expression": "響應主題表達式", + "value-expression": "值表達式", + "topic": "主題", + "timeout": "毫秒超時", + "converter-json-required": "轉換JSON是必需的。", + "converter-json-parse": "無法解析轉換JSON。", + "filter-expression": "過濾表達式", + "connect-requests": "連接請求", + "add-connect-request": "增加連接請求", + "disconnect-requests": "斷開請求", + "add-disconnect-request": "增加斷開請求", + "attribute-requests": "屬性請求", + "add-attribute-request": "增加屬性請求", + "attribute-updates": "屬性更新", + "add-attribute-update": "增加屬性更新", + "server-side-rpc": "服務端RPC", + "add-server-side-rpc-request": "增加服務端RPC請求", + "device-name-filter": "設備名稱濾波", + "attribute-filter": "屬性濾波", + "method-filter": "方法濾波", + "request-topic-expression": "請求主題表達式", + "response-timeout": "毫秒內響應超時", + "topic-expression": "主題表達", + "client-scope": "客戶範圍", + "add-device": "增加伺服器", + "opc-server": "伺服器組", + "opc-add-server": "增加伺服器", + "opc-add-server-prompt": "請增加伺服器", + "opc-application-name": "應用名稱", + "opc-application-uri": "應用URI", + "opc-scan-period-in-seconds": "秒級掃描週期", + "opc-security": "安全性", + "opc-identity": "身份", + "opc-keystore": "密鑰庫", + "opc-type": "類型", + "opc-keystore-type": "類型", + "opc-keystore-location": "位置 *", + "opc-keystore-password": "密碼", + "opc-keystore-alias": "別名", + "opc-keystore-key-password": "密鑰密碼", + "opc-device-node-pattern": "設備節點模式", + "opc-device-name-pattern": "設備名稱模式", + "modbus-server": "Servers/slaves", + "modbus-add-server": "增加 server/slave", + "modbus-add-server-prompt": "請增加 server/slave", + "modbus-transport": "傳輸", + "modbus-port-name": "串口名稱", + "modbus-encoding": "編碼", + "modbus-parity": "奇偶性", + "modbus-baudrate": "鮑率", + "modbus-databits": "資料位", + "modbus-stopbits": "停止位", + "modbus-databits-range": "資料位應該在7到8的範圍內。", + "modbus-stopbits-range": "停止位應該在1到2的範圍內。", + "modbus-unit-id": "單位編號", + "modbus-unit-id-range": "單位ID應該在1到247的範圍內", + "modbus-device-name": "設備名稱", + "modbus-poll-period": "輪詢週期 (ms)", + "modbus-attributes-poll-period": "輪詢屬性週期 (ms)", + "modbus-timeseries-poll-period": "時間序列輪詢週期 (ms)", + "modbus-poll-period-range": "輪詢週期應為正值。", + "modbus-tag": "標籤", + "modbus-function": "函數", + "modbus-register-address": "寄存器地址", + "modbus-register-address-range": "寄存器地址應該在0到65535的範圍內。", + "modbus-register-bit-index": "位索引", + "modbus-register-bit-index-range": "位索引應該在0到15的範圍內。", + "modbus-register-count": "寄存器計數", + "modbus-register-count-range": "寄存器計數應該是一個正值。", + "modbus-byte-order": "字節順序", + "sync": { + "status": "狀態", + "sync": "同步", + "not-sync": "不同步", + "last-sync-time": "最後同步時間", + "not-available": "無法使用" + }, + "export-extensions-configuration": "匯出擴展配置", + "import-extensions-configuration": "匯入擴展配置", + "import-extensions": "匯入擴展", + "import-extension": "匯入擴展", + "export-extension": "匯出擴展", + "file": "擴展文件", + "invalid-file-error": "無效的擴展文件" + }, + "fullscreen": { + "expand": "展開到全螢幕", + "exit": "退出全螢幕", + "toggle": "切換全螢幕模式", + "fullscreen": "全螢幕" + }, + "function": { + "function": "函數" + }, + "grid": { + "delete-item-title": "您確定要刪除此項嗎?", + "delete-item-text": "注意,確認後此項及其所有相關資料將變得無法恢復。", + "delete-items-title": "你確定你要刪除 { count, plural, 1 {1 項} other {# 項} }嗎?", + "delete-items-action-title": "刪除 { count, plural, 1 {1 項} other {# 項} }", + "delete-items-text": "注意,確認後所有選擇的項目將被刪除,所有相關資料將無法恢復。", + "add-item-text": "增加新項目", + "no-items-text": "沒有找到項目", + "item-details": "項目詳細訊息", + "delete-item": "刪除項目", + "delete-items": "刪除項目", + "scroll-to-top": "滾動到頂部" + }, + "help": { + "goto-help-page": "轉到幫助頁面" + }, + "home": { + "home": "首頁", + "profile": "屬性", + "logout": "註銷", + "menu": "菜單", + "avatar": "頭像", + "open-user-menu": "打開用戶菜單" + }, + "import": { + "no-file": "沒有選擇文件", + "drop-file": "拖動一個JSON文件或者單擊以選擇要上傳的文件。" + }, + "item": { + "selected": "選擇" + }, + "js-func": { + "no-return-error": "函數必須返回值!", + "return-type-mismatch": "函數必須返回 '{{type}}' 類型的值!", + "tidy": "整理" + }, + "key-val": { + "key": "鍵名", + "value": "值", + "remove-entry": "刪除條目", + "add-entry": "增加條目", + "no-data": "沒有條目" + }, + "layout": { + "layout": "佈局", + "manage": "佈局管理", + "settings": "佈局設定", + "color": "顏色", + "main": "主體", + "right": "右側", + "select": "選擇目標佈局" + }, + "legend": { + "position": "圖例位置", + "show-max": "顯示最大值", + "show-min": "顯示最小值", + "show-avg": "顯示平均值", + "show-total": "顯示總數", + "settings": "圖例設定", + "min": "最小值", + "max": "最大值", + "avg": "平均值", + "total": "總數" + }, + "login": { + "login": "登入", + "request-password-reset": "請求密碼重置", + "reset-password": "重置密碼", + "create-password": "建立密碼", + "passwords-mismatch-error": "輸入的密碼必須相同!", + "password-again": "再次輸入密碼", + "sign-in": "登入 ", + "username": "用戶名(電子郵件)", + "remember-me": "記住我", + "forgot-password": "忘記密碼?", + "password-reset": "密碼重置", + "new-password": "新密碼", + "new-password-again": "再次輸入新密碼", + "password-link-sent-message": "密碼重置連結已成功發送!", + "email": "電子郵件" + }, + "position": { + "top": "頂部", + "bottom": "底部", + "left": "左側", + "right": "右側" + }, + "profile": { + "profile": "屬性", + "change-password": "更改密碼", + "current-password": "當前密碼" + }, + "relation": { + "relations": "關聯", + "direction": "方向", + "search-direction": { + "FROM": "從", + "TO": "到" + }, + "direction-type": { + "FROM": "從", + "TO": "到" + }, + "from-relations": "向外的關聯", + "to-relations": "向內的關聯", + "selected-relations": "{ count, plural, 1 {1 關聯} other {# 關聯} } 被選中", + "type": "類型", + "to-entity-type": "到實體類型", + "to-entity-name": "到實體名稱", + "from-entity-type": "從實體類型", + "from-entity-name": "從實體類型", + "to-entity": "到實體", + "from-entity": "從實體", + "delete": "刪除關聯", + "relation-type": "關聯類型", + "relation-type-required": "關聯類型必填", + "any-relation-type": "任意類型", + "add": "增加關聯", + "edit": "編輯關聯", + "delete-to-relation-title": "確定要刪除實體 '{{entityName}}' 的關聯嗎?", + "delete-to-relation-text": "確定刪除後實體 '{{entityName}}' 將取消與當前實體的關聯關係。", + "delete-to-relations-title": "確定要刪除 { count, plural, 1 {1 關聯} other {# 關聯} }?", + "delete-to-relations-text": "確定刪除所有選擇的關聯關係後,與當前實體對應的所有關聯關係將被移除。", + "delete-from-relation-title": "確定要從實體 '{{entityName}}' 刪除關聯嗎?", + "delete-from-relation-text": "確定刪除後,當前實體將與實體 '{{entityName}}' 取消關聯", + "delete-from-relations-title": "確定刪除 { count, plural, 1 {1 關聯} other {# 關聯} } 嗎?", + "delete-from-relations-text": "確定刪除所有選擇的關聯關係後,當前實體將與對應的實體取消關聯", + "remove-relation-filter": "移除關聯過濾器", + "add-relation-filter": "增加關聯過濾器", + "any-relation": "任意關聯", + "relation-filters": "關聯過濾器", + "additional-info": "附加訊息 (JSON)", + "invalid-additional-info": "無法解析附加訊息json。" + }, + "rulechain": { + "rulechain": "規則鏈", + "rulechains": "規則鏈庫", + "root": "根實體", + "delete": "刪除規則", + "activate": "啟動規則", + "suspend": "暫停規則", + "active": "啟動", + "suspended": "暫停", + "name": "名稱", + "name-required": "名稱必填。", + "description": "描述", + "add": "增加規則", + "set-root": "建立規則鏈根", + "set-root-rulechain-title": "您確定要生成規則鏈'{{RuleChainName}}'根嗎?", + "set-root-rulechain-text": "確認之後,規則鏈將變為根規格鏈,並將處理所有傳入的傳輸消息。", + "delete-rulechain-title": " 確實要刪除規則鏈'{{ruleChainName}}'嗎?", + "delete-rulechain-text": "小心,在確認規則鏈和所有相關資料將變得無法恢復。", + "delete-rulechains-title": "確實要刪除{count, plural, 1 { 1 規則鏈} other {# 規則鏈庫} }嗎?", + "delete-rulechains-action-title": "刪除 { count, plural, 1 {1 規則鏈} other {# 規則鏈庫} }", + "delete-rulechains-text": "小心,確認後,所有選擇的規則鏈將被刪除,所有相關的資料將變得無法恢復。", + "add-rulechain-text": "增加新的規則鏈", + "no-rulechains-text": "規則鏈沒有發現", + "rulechain-details": "規則鏈詳細資訊", + "details": "詳細資訊", + "events": "事件", + "system": "系統", + "import": "匯入規則", + "export": "匯出規則", + "export-failed-error": "無法匯出規則:{{error}}", + "create-new-rulechain": "建立新的規則鏈", + "rulechain-file": "規則鏈文件", + "invalid-rulechain-file-error": "不能匯入規則鏈:無效的規則鏈資料格式。", + "copyId": "複製規則鏈ID", + "idCopiedMessage": "規則ID已經複製到剪貼板", + "select-rulechain": "選擇規則鏈", + "no-rulechains-matching": "沒有發現符合'{{entity}}'的規則鏈。", + "rulechain-required": "規則鏈必填", + "management": "規則集管理", + "debug-mode": "測試模式" + }, + "rulenode": { + "details": "詳細資訊", + "events": "事件", + "search": "搜尋節點", + "open-node-library": "打開節點庫", + "add": "增加規則節點", + "name": "名稱", + "name-required": "名稱必填。", + "type": "類型", + "description": "描述", + "delete": "刪除規則節點", + "select-all-objects": "選擇所有節點和連接", + "deselect-all-objects": "取消選擇所有節點和連接", + "delete-selected-objects": "刪除選擇的節點和連接", + "delete-selected": "刪除選擇", + "select-all": "選擇全部", + "copy-selected": "選擇副本", + "deselect-all": "取消選擇", + "rulenode-details": "規則節點詳細資訊", + "debug-mode": "測試模式", + "configuration": "配置", + "link": "連結", + "link-details": "規則節點連結詳細資訊", + "add-link": "增加連結", + "link-label": "連結標籤", + "link-label-required": "連結標籤必填", + "custom-link-label": "自定義連結標籤", + "custom-link-label-required": "自定義連結標籤必填", + "type-filter": "濾波器", + "type-filter-details": "使用配置條件過濾傳入消息", + "type-enrichment": "屬性集", + "type-enrichment-details": "向消息元資料中增加附加訊息", + "type-transformation": "變換", + "type-transformation-details": "更改消息有效載荷和元資料", + "type-action": "動作", + "type-action-details": "執行特別動作", + "type-external": "外部的", + "type-external-details": "與外部系統交互", + "type-rule-chain": "規則鏈", + "type-rule-chain-details": "將傳入消息轉發到指定的規則鏈", + "type-in​​put": "輸入", + "type-in​​put-details": "規則鏈的邏輯輸入,將傳入消息轉發到下一個相關規則節點", + "directive-is-not-loaded": "定義的配置指令 '{{directiveName}}' 不可用。", + "ui-resources-load-error": "加載配置UI資源失敗。", + "invalid-target-rulechain": "無法解析目標規則鏈!", + "test-script-function": "測試腳本功能", + "message": "消息", + "message-type": "消息類型", + "message-type-required": "消息類型必填", + "metadata": "元資料", + "metadata-required": "元資料項不能為空。", + "output": "輸出", + "test": "測試", + "help": "幫助" + }, + "tenant": { + "tenant": "租戶", + "tenants": "租戶", + "management": "租戶管理", + "add": "增加租戶", + "admins": "管理員", + "manage-tenant-admins": "管理租戶管理員", + "delete": "刪除租戶", + "add-tenant-text": "增加新租戶", + "no-tenants-text": "沒有找到租戶", + "tenant-details": "租客詳細資訊", + "delete-tenant-title": "您確定要刪除租戶'{{tenantTitle}}'嗎?", + "delete-tenant-text": "小心!確認後,租戶和所有相關資料將無法恢復。", + "delete-tenants-title": "您確定要刪除 {count,plural,1 {1 租戶} other {# 租戶} } 嗎?", + "delete-tenants-action-title": "刪除 { count, plural, 1 {1 租戶} other {# 租戶} }", + "delete-tenants-text": "小心!確認後,所有選擇的租戶將被刪除,所有相關資料將無法恢復。", + "title": "標題", + "title-required": "標題必填。", + "description": "描述", + "details": "詳細資訊", + "events": "事件", + "copyId": "複製租戶ID", + "idCopiedMessage": "租戶ID已經複製到剪貼板", + "select-tenant": "選擇租戶", + "no-tenants-matching": "沒有找到符合 '{{entity}}' 的租戶", + "tenant-required": "租戶必填" + }, + "timeinterval": { + "seconds-interval": "{ seconds, plural, 1 {1 秒} other {# 秒} }", + "minutes-interval": "{ minutes, plural, 1 {1 分} other {# 分} }", + "hours-interval": "{ hours, plural, 1 {1 小時} other {# 小時} }", + "days-interval": "{ days, plural, 1 {1 天} other {# 天} }", + "days": "天", + "hours": "時", + "minutes": "分", + "seconds": "秒", + "advanced": "高級" + }, + "timewindow": { + "days": "{ days, plural, 1 { 天 } other {# 天 } }", + "hours": "{ hours, plural, 0 { 小時 } 1 {1 小時 } other {# 小時 } }", + "minutes": "{ minutes, plural, 0 { 分 } 1 {1 分 } other {# 分 } }", + "seconds": "{ seconds, plural, 0 { 秒 } 1 {1 秒 } other {# 秒 } }", + "realtime": "實時", + "history": "歷史", + "last-prefix": "最後", + "period": "從 {{ startTime }} 到 {{ endTime }}", + "edit": "編輯時間窗口", + "date-range": "日期範圍", + "last": "最後", + "time-period": "時間段" + }, + "user": { + "user": "用戶", + "users": "用戶", + "customer-users": "客戶用戶", + "tenant-admins": "租戶管理員", + "sys-admin": "系統管理員", + "tenant-admin": "租戶管理員", + "customer": "客戶", + "anonymous": "匿名", + "add": "增加用戶", + "delete": "刪除用戶", + "add-user-text": "增加新用戶", + "no-users-text": "找不到用戶", + "user-details": "用戶詳細訊息", + "delete-user-title": "您確定要刪除用戶 '{{userEmail}}' 嗎?", + "delete-user-text": "小心!確認後,用戶和所有相關資料將無法恢復。", + "delete-users-title": "你確定你要刪除 { count, plural, 1 {1 用戶} other {# 用戶} } 嗎?", + "delete-users-action-title": "刪除 { count, plural, 1 {1 用戶} other {# 用戶} }", + "delete-users-text": "小心!確認後,所有選擇的用戶將被刪除,所有相關資料將無法恢復。", + "activation-email-sent-message": "啟動電子郵件已成功發送!", + "resend-activation": "重新發送啟動", + "email": "電子郵件", + "email-required": "電子郵件必填。", + "invalid-email-format": "無效的郵件格式。", + "first-name": "名字", + "last-name": "姓", + "description": "描述", + "default-dashboard": "預設面板", + "always-fullscreen": "始終全螢幕", + "select-user": "選擇用戶", + "no-users-matching": "沒有找到符合 '{{entity}}' 的用戶。", + "user-required": "用戶必填", + "activation-method": "啟動方式", + "display-activation-link": "顯示啟動連結", + "send-activation-mail": "發送啟動郵件", + "activation-link": "用戶啟動連結", + "activation-link-text": "使用該連結 啟動 啟動用戶:", + "copy-activation-link": "複製用戶啟動連結", + "activation-link-copied-message": "用戶啟動連結已經複製到剪貼板", + "details": "詳細訊息" + }, + "value": { + "type": "值類型", + "string": "字符串", + "string-value": "字符串值", + "integer": "數字", + "integer-value": "數字值", + "invalid-integer-value": "整數值無效", + "double": "雙精度浮點數", + "double-value": "雙精度浮點數值", + "boolean": "布林", + "boolean-value": "布林值", + "false": "假", + "true": "真", + "long": "Long" + }, + "widget": { + "widget-library": "部件庫", + "widget-bundle": "部件包", + "select-widgets-bundle": "選擇部件包", + "management": "管理部件", + "editor": "部件編輯器", + "widget-type-not-found": "加載部件配置出錯。
    可能關聯的\n 部件已經刪除了。", + "widget-type-load-error": "由於以下錯誤未加載小部件:", + "remove": "刪除部件", + "edit": "編輯部件", + "remove-widget-title": "確實要刪除 '{{widgetTitle}}'部件嗎?", + "remove-widget-text": "確認後,控件和所有相關資料將變得無法恢復。", + "timeseries": "時間序列", + "search-data": "搜尋資料", + "no-data-found": "沒有找到資料", + "latest-values": "最新值", + "rpc": "控件部件", + "alarm": "警告部件", + "static": "靜態部件", + "select-widget-type": "選擇窗口部件類型", + "missing-widget-title-error": "部件標題必須指定!", + "widget-saved": "部件已儲存", + "unable-to-save-widget-error": "無法儲存部件!控件有錯誤!", + "save": "儲存部件", + "saveAs": "部件另存為", + "save-widget-type-as": "部件類型另存為", + "save-widget-type-as-text": "請輸入新的部件標題或選擇目標部件包", + "toggle-fullscreen": "切換全螢幕", + "run": "執行部件", + "title": "部件標題", + "title-required": "需要部件標題。", + "type": "部件類型", + "resources": "資源", + "resource-url": "JavaScript/CSS URL", + "remove-resource": "刪除資源", + "add-resource": "增加資源", + "html": "HTML", + "tidy": "整理", + "css": "CSS", + "settings-schema": "設定模式", + "datakey-settings-schema": "資料鍵設定模式", + "javascript": "Javascript", + "remove-widget-type-title": "您確定要刪除部件類型 '{{widgetName}}'嗎?", + "remove-widget-type-text": "確認後,窗口部件類型和所有相關資料將無法恢復。", + "remove-widget-type": "刪除部件類型", + "add-widget-type": "增加新的部件類型", + "widget-type-load-failed-error": "無法加載部件類型!", + "widget-template-load-failed-error": "無法加載部件模板!", + "add": "增加部件", + "undo": "復原部件更改", + "export": "匯出部件" + }, + "widget-action": { + "header-button": "部件頂部按鈕", + "open-dashboard-state": "切換到新儀表板狀態", + "update-dashboard-state": "更新當前儀表板狀態", + "open-dashboard": "切換到另一個儀表板", + "custom": "自定義動作", + "target-dashboard-state": "目標儀表板狀態", + "target-dashboard-state-required": "目標儀表板狀態必填", + "set-entity-from-widget": "從部件中設定實體", + "target-dashboard": "目標儀表板", + "open-right-layout": "打開右側佈局 (移動端視圖)" + }, + "widgets-bundle": { + "current": "當前包", + "widgets-bundles": "部件包", + "add": "增加部件包", + "delete": "刪除部件包", + "title": "標題", + "title-required": "標題必填。", + "add-widgets-bundle-text": "增加新的部件包", + "no-widgets-bundles-text": "找不到部件包", + "empty": "部件包是空的", + "details": "詳細資訊", + "widgets-bundle-details": "部件包詳細訊息", + "delete-widgets-bundle-title": "您確定要刪除部件包 '{{widgetsBundleTitle}}'嗎?", + "delete-widgets-bundle-text": "小心!確認後,部件包和所有相關資料將無法恢復。", + "delete-widgets-bundles-title": "你確定你要刪除 { count, plural, 1 {1 部件包} other {# 部件包} } 嗎?", + "delete-widgets-bundles-action-title": "刪除 { count, plural, 1 {1 部件包} other {# 部件包} }", + "delete-widgets-bundles-text": "小心!確認後,所有選擇的部件包將被刪除,所有相關資料將無法恢復。", + "no-widgets-bundles-matching": "沒有找到與 '{{widgetsBundle}}' 符合的部件包。", + "widgets-bundle-required": "需要部件包。", + "system": "系統", + "import": "匯入部件包", + "export": "匯出部件包", + "export-failed-error": "無法匯出部件包: {{error}}", + "create-new-widgets-bundle": "建立新的部件包", + "widgets-bundle-file": "部件包文件", + "invalid-widgets-bundle-file-error": "無法匯入部件包:無效的部件包資料結構。" + }, + "widget-config": { + "data": "資料", + "settings": "設定", + "advanced": "高級", + "title": "標題", + "general-settings": "一般設定", + "display-title": "顯示標題", + "drop-shadow": "陰影", + "enable-fullscreen": "啟用全螢幕", + "background-color": "背景顏色", + "text-color": "文字顏色", + "padding": "填充", + "margin": "邊緣", + "widget-style": "部件風格", + "title-style": "標題風格", + "mobile-mode-settings": "移動端設定", + "order": "順序", + "height": "高度", + "units": "特殊符號展示值", + "decimals": "浮點數後的位數", + "timewindow": "時間窗口", + "use-dashboard-timewindow": "使用儀表板的時間窗口", + "display-legend": "顯示圖例", + "datasources": "資料源", + "maximum-datasources": "最大允許 { count, plural, 1 {1 資料} other {# 資料} }", + "datasource-type": "類型", + "datasource-parameters": "參數", + "remove-datasource": "移除資料源", + "add-datasource": "增加資料源", + "target-device": "目標設備", + "alarm-source": "警告源", + "actions": "動作", + "action": "動作", + "add-action": "增加動作", + "search-actions": "動作檢索", + "action-source": "動作源", + "action-source-required": "動作源必填", + "action-name": "動作名稱", + "action-name-required": "動作名稱必填。", + "action-name-not-unique": "動作名稱已經存在。
    統一動作源的動作名稱必須唯一。", + "action-icon": "圖示", + "action-type": "類型", + "action-type-required": "類型必填", + "edit-action": "編輯動作", + "delete-action": "刪除動作", + "delete-action-title": "刪除部件動作", + "delete-action-text": "確定要刪除部件動作 '{{actionName}}' 嗎?" + }, + "widget-type": { + "import": "匯入部件類型", + "export": "匯出部件類型", + "export-failed-error": "無法匯出部件類型: {{error}}", + "create-new-widget-type": "建立新的部件類型", + "widget-type-file": "部件類型文件", + "invalid-widget-type-file-error": "無法匯入部件類型:無效的部件類型資料結構。" + }, + "widgets": { + "date-range-navigator": { + "localizationMap": { + "Sun": "週日", + "Mon": "週一", + "Tue": "週二", + "Wed": "週三", + "Thu": "週四", + "Fri": "週五", + "Sat": "週六", + "Jan": "1月", + "Feb": "2月", + "Mar": "3月", + "Apr": "4月", + "May": "5月", + "Jun": "6月", + "Jul": "7月", + "Aug": "8月", + "Sep": "9月", + "Oct": "10月", + "Nov": "11月", + "Dec": "12月", + "January": "一月", + "February": "二月", + "March": "三月", + "April": "四月", + "June": "六月", + "July": "七月", + "August": "八月", + "September": "九月", + "October": "十月", + "November": "十一月", + "December": "十二月", + "Custom Date Range": "自定義日期範圍", + "Date Range Template": "日期範圍模板", + "Today": "今天", + "Yesterday": "昨天", + "This Week": "本星期", + "Last Week": "上個星期", + "This Month": "這個月", + "Last Month": "上個月", + "Year": "年", + "This Year": "今年", + "Last Year": "去年", + "Date picker": "日期選擇器", + "Hour": "小時", + "Day": "天", + "Week": "週", + "2 weeks": "2週", + "Month": "月", + "3 months": "3個月", + "6 months": "6個月", + "Custom interval": "自定義間隔", + "Interval": "間隔", + "Step size": "步長", + "Ok": "Ok" + } + } + }, + "icon": { + "icon": "圖示", + "select-icon": "選擇圖示", + "material-icons": "素材圖示", + "show-all": "顯示所有圖示" + }, + "custom": { + "widget-action": { + "action-cell-button": "動作單元格按鈕", + "row-click": "點選行", + "marker-click": "點選標記", + "polygon-click": "單擊多邊形", + "tooltip-tag-action": "提示標籤動作" + } + }, + "language": { + "language": "語言" + } +} diff --git a/ui/src/svg/logo_title_white.svg b/ui-ngx/src/assets/logo_title_white.svg similarity index 100% rename from ui/src/svg/logo_title_white.svg rename to ui-ngx/src/assets/logo_title_white.svg diff --git a/ui/src/svg/logo_white.svg b/ui-ngx/src/assets/logo_white.svg similarity index 100% rename from ui/src/svg/logo_white.svg rename to ui-ngx/src/assets/logo_white.svg diff --git a/ui-ngx/src/assets/mdi.svg b/ui-ngx/src/assets/mdi.svg new file mode 100644 index 0000000000..75e8ffea7a --- /dev/null +++ b/ui-ngx/src/assets/mdi.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui-ngx/src/assets/split.js/grips/horizontal.png b/ui-ngx/src/assets/split.js/grips/horizontal.png new file mode 100644 index 0000000000..cbe7da77dd Binary files /dev/null and b/ui-ngx/src/assets/split.js/grips/horizontal.png differ diff --git a/ui-ngx/src/assets/split.js/grips/vertical.png b/ui-ngx/src/assets/split.js/grips/vertical.png new file mode 100644 index 0000000000..0ac8fa1e03 Binary files /dev/null and b/ui-ngx/src/assets/split.js/grips/vertical.png differ diff --git a/ui-ngx/src/environments/environment.prod.ts b/ui-ngx/src/environments/environment.prod.ts new file mode 100644 index 0000000000..259e5c432e --- /dev/null +++ b/ui-ngx/src/environments/environment.prod.ts @@ -0,0 +1,25 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export const environment = { + appTitle: 'ThingsBoard', + production: true, +// @ts-ignore + tbVersion: TB_VERSION, +// @ts-ignore + supportedLangs: SUPPORTED_LANGS, + defaultLang: 'en_US' +}; diff --git a/ui-ngx/src/environments/environment.ts b/ui-ngx/src/environments/environment.ts new file mode 100644 index 0000000000..86e4923aad --- /dev/null +++ b/ui-ngx/src/environments/environment.ts @@ -0,0 +1,38 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. + +export const environment = { + appTitle: 'ThingsBoard', + production: false, +// @ts-ignore + tbVersion: TB_VERSION, +// @ts-ignore + supportedLangs: SUPPORTED_LANGS, + defaultLang: 'en_US' +}; + +/* + * For easier debugging in development mode, you can import the following file + * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. + * + * This import should be commented out in production mode because it will have a negative impact + * on performance if an error is thrown. + */ +// import 'zone.js/dist/zone-error'; // Included with Angular CLI. diff --git a/ui-ngx/src/index.html b/ui-ngx/src/index.html new file mode 100644 index 0000000000..af49c0c938 --- /dev/null +++ b/ui-ngx/src/index.html @@ -0,0 +1,31 @@ + + + + + + ThingsBoard + + + + + + + + + diff --git a/ui-ngx/src/karma.conf.js b/ui-ngx/src/karma.conf.js new file mode 100644 index 0000000000..a311d5bf0b --- /dev/null +++ b/ui-ngx/src/karma.conf.js @@ -0,0 +1,47 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function(config) { + config.set({ + basePath: "", + frameworks: ["jasmine", "@angular-devkit/build-angular"], + plugins: [ + require("karma-jasmine"), + require("karma-chrome-launcher"), + require("karma-jasmine-html-reporter"), + require("karma-coverage-istanbul-reporter"), + require("@angular-devkit/build-angular/plugins/karma"), + ], + client: { + clearContext: false, // leave Jasmine Spec Runner output visible in browser + }, + coverageIstanbulReporter: { + dir: require("path").join(__dirname, "../coverage/tb-license-server"), + reports: ["html", "lcovonly", "text-summary"], + fixWebpackSourcePaths: true, + }, + reporters: ["progress", "kjhtml"], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ["Chrome"], + singleRun: false, + restartOnFileChange: true, + }); +}; diff --git a/ui-ngx/src/main.ts b/ui-ngx/src/main.ts new file mode 100644 index 0000000000..1f2498814b --- /dev/null +++ b/ui-ngx/src/main.ts @@ -0,0 +1,30 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + + + +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from '@app/app.module'; +import { environment } from '@env/environment'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic().bootstrapModule(AppModule) + .catch(err => console.error(err)); diff --git a/ui-ngx/src/polyfills.ts b/ui-ngx/src/polyfills.ts new file mode 100644 index 0000000000..68a59ba5c3 --- /dev/null +++ b/ui-ngx/src/polyfills.ts @@ -0,0 +1,112 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/guide/browser-support + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** IE10 and IE11 requires the following for NgClass support on SVG elements */ +// import 'classlist.js'; // Run `npm install --save classlist.js`. + +/** + * Web Animations `@angular/platform-browser/animations` + * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. + * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). + */ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + * because those flags need to be set before `zone.js` being loaded, and webpack + * will put import in the top of bundle, so user need to create a separate file + * in this directory (for example: zone-flags.ts), and put the following flags + * into that file, and then add the following code before importing zone.js. + * import './zone-flags.ts'; + * + * The flags allowed in zone-flags.ts are listed here. + * + * The following flags will work for all browsers. + * + * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + * (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + * + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + * + * (window as any).__Zone_enable_cross_context_check = true; + * + */ + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ + +import './zone-flags'; +import 'zone.js/dist/zone'; // Included with Angular CLI. +import 'core-js/es/array'; +import moment from 'moment'; + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ + +(window as any).global = window; + +/*************************************************************************************************** + * WIDGETS IMPORTS + */ + +import cssjs from '@core/css/css'; +import { TbFlot } from '@home/components/widget/lib/flot-widget'; +import { TbAnalogueCompass } from '@home/components/widget/lib/analogue-compass'; +import { TbAnalogueRadialGauge } from '@home/components/widget/lib/analogue-radial-gauge'; +import { TbAnalogueLinearGauge } from '@home/components/widget/lib/analogue-linear-gauge'; +import { TbCanvasDigitalGauge } from '@home/components/widget/lib/digital-gauge'; +import { TbMapWidgetV2 } from '@home/components/widget/lib/maps/map-widget2'; +import { TbTripAnimationWidget } from '@app/modules/home/components/widget/trip-animation/trip-animation.component'; + +import * as tinycolor_ from 'tinycolor2'; + +const tinycolor = tinycolor_; + +(window as any).tinycolor = tinycolor; +(window as any).cssjs = cssjs; +(window as any).moment = moment; +(window as any).TbFlot = TbFlot; +(window as any).TbAnalogueCompass = TbAnalogueCompass; +(window as any).TbAnalogueRadialGauge = TbAnalogueRadialGauge; +(window as any).TbAnalogueLinearGauge = TbAnalogueLinearGauge; +(window as any).TbCanvasDigitalGauge = TbCanvasDigitalGauge; +(window as any).TbMapWidgetV2 = TbMapWidgetV2; +(window as any).TbTripAnimationWidget = TbTripAnimationWidget; diff --git a/ui/src/scss/animations.scss b/ui-ngx/src/scss/animations.scss similarity index 100% rename from ui/src/scss/animations.scss rename to ui-ngx/src/scss/animations.scss diff --git a/ui-ngx/src/scss/constants.scss b/ui-ngx/src/scss/constants.scss new file mode 100644 index 0000000000..01906743ca --- /dev/null +++ b/ui-ngx/src/scss/constants.scss @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +$mat-xs: "screen and (max-width: 599px)"; +$mat-sm: "screen and (min-width: 600px) and (max-width: 959px)"; +$mat-md: "screen and (min-width: 960px) and (max-width: 1279px)"; +$mat-lg: "screen and (min-width: 1280px) and (max-width: 1919px)"; +$mat-xl: "screen and (min-width: 1920px) and (max-width: 5000px)"; +$mat-lt-sm: "screen and (max-width: 599px)"; +$mat-lt-md: "screen and (max-width: 959px)"; +$mat-lt-lg: "screen and (max-width: 1279px)"; +$mat-lt-xl: "screen and (max-width: 1919px)"; +$mat-gt-xs: "screen and (min-width: 600px)"; +$mat-gt-sm: "screen and (min-width: 960px)"; +$mat-gt-md: "screen and (min-width: 1280px)"; +$mat-gt-xmd: "screen and (min-width: 1600px)"; +$mat-gt-xl: "screen and (min-width: 1920px)"; + +$primary-hue-3: rgb(207, 216, 220) !default; diff --git a/ui/src/scss/fonts.scss b/ui-ngx/src/scss/fonts.scss similarity index 100% rename from ui/src/scss/fonts.scss rename to ui-ngx/src/scss/fonts.scss diff --git a/ui-ngx/src/scss/mixins.scss b/ui-ngx/src/scss/mixins.scss new file mode 100644 index 0000000000..5aee61ee15 --- /dev/null +++ b/ui-ngx/src/scss/mixins.scss @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@mixin tb-mat-icon-size($size) { + width: #{$size}px; + min-width: #{$size}px; + height: #{$size}px; + min-height: #{$size}px; + font-size: #{$size}px; + line-height: #{$size}px; + svg { + width: #{$size}px; + height: #{$size}px; + } +} + +@mixin tb-mat-icon-button-size($size) { + width: #{$size}px; + height: #{$size}px; + line-height: #{$size}px; + padding: 0 !important; + .mat-icon { + display: block; + margin: auto; + } +} + +@mixin tb-checkered-bg() { + background-color: #fff; + background-image: + linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd), + linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd); + background-position: 0 0, 4px 4px; + background-size: 8px 8px; +} diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss new file mode 100644 index 0000000000..190fbb1ba7 --- /dev/null +++ b/ui-ngx/src/styles.scss @@ -0,0 +1,1056 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* You can add global styles to this file, and also import other style files */ + +@import '~material-design-icons/iconfont/material-icons.css'; +@import '~typeface-roboto/index.css'; +@import '~font-awesome/css/font-awesome.min.css'; +@import 'theme.scss'; +@import './scss/constants'; +@import './scss/animations'; +@import './scss/mixins'; +@import './scss/fonts'; + +body, html { + height: 100%; + min-height: 100%; + position: relative; + -webkit-tap-highlight-color: rgba(0,0,0,0); + -webkit-touch-callout: none; + + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + margin: 0; + padding: 0; + background-color: rgb(250,250,250); + overflow: hidden; +} + +tb-root { + margin: 0; + width: 100%; + min-height: 100%; + height: 100%; + display: flex; + flex-direction: row; + box-sizing: border-box; +} + +/*************** + * TYPE DEFAULTS + ***************/ + +body, +button, +html, +input, +select, +textarea, +td, +th { + font-family: Roboto, "Helvetica Neue", sans-serif; + font-size: 16px; +} + +body { + line-height: normal; +} + +a { + font-weight: 400; + color: #106cc8; + text-decoration: none; + border-bottom: 1px solid rgba(64, 84, 178, .25); + + transition: border-bottom .35s; +} + +a:hover, +a:focus { + border-bottom: 1px solid #4054b2; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + &, &.mat-headline { + margin-top: 1rem; + margin-bottom: 1rem; + } +} + +h1 { + font-size: 3.4rem; + font-weight: 400; + line-height: 4rem; +} + +h2 { + font-size: 2.4rem; + font-weight: 400; + line-height: 3.2rem; +} + +h3 { + font-size: 2rem; + font-weight: 500; + letter-spacing: .005em; +} + +h4 { + font-size: 1.6rem; + font-weight: 400; + line-height: 2.4rem; + letter-spacing: .01em; +} + +p { + margin: .8em 0 1.6em; + font-size: 1.6rem; + font-weight: 400; + line-height: 1.6em; + letter-spacing: .01em; +} + +strong { + font-weight: 500; +} + +blockquote { + padding-left: 16px; + margin-left: 0; + font-style: italic; + border-left: 3px solid rgba(0, 0, 0, .12); +} + +fieldset { + padding: 0; + margin: 0; + border: none; +} + +section.tb-header-buttons { + position: absolute; + top: 86px; + right: 0; + z-index: 3; + pointer-events: none; + + @media #{$mat-gt-sm} { + top: 86px; + } + + .tb-btn-header { + margin: 6px 8px; + position: relative !important; + display: inline-block !important; + animation: tbMoveFromTopFade .3s ease both; + + &.tb-hide { + animation: tbMoveToTopFade .3s ease both; + } + } +} + +section.tb-footer-buttons { + position: fixed; + right: 20px; + bottom: 20px; + z-index: 30; + pointer-events: none; + + .tb-btn-footer { + margin: 6px 8px; + position: relative !important; + display: inline-block !important; + animation: tbMoveFromBottomFade .3s ease both; + &.tb-hide { + animation: tbMoveToBottomFade .3s ease both; + } + } +} + + +.tb-details-buttons { + button { + margin: 6px 8px; + } +} + +label { + &.tb-title { + padding-bottom: 15px; + font-size: 13px; + font-weight: 400; + color: #666; + pointer-events: none; + + &.no-padding { + padding-bottom: 0; + } + + &.tb-required::after { + font-size: 13px; + color: rgba(0, 0, 0, .54); + vertical-align: top; + content: " *"; + } + + &.tb-error { + color: rgb(221, 44, 0); + + &.tb-required::after { + color: rgb(221, 44, 0); + } + } + } + &.tb-small { + font-size: 12px; + color: rgba(0, 0, 0, .54); + pointer-events: none; + } +} + +.tb-noselect { + user-select: none; +} + +.tb-readonly-label { + color: rgba(0, 0, 0, .54); +} + +.tb-disabled-label { + color: rgba(0, 0, 0, .44); +} + +div { + &.tb-small { + font-size: 14px; + color: rgba(0, 0, 0, .54); + } +} + +.tb-hint { + padding-bottom: 15px; + font-size: 12px; + line-height: 14px; + color: #808080; +} + +.mat-caption { + &.tb-required::after { + font-size: 10px; + color: rgba(0, 0, 0, .54); + vertical-align: top; + content: " *"; + } +} + +pre.tb-highlight { + display: block; + padding: 15px; + margin: 20px 0; + overflow-x: auto; + background-color: #f7f7f7; + + code { + box-sizing: border-box; + display: inline-block; + padding: 0; + font-family: monospace; + font-size: 16px; + font-weight: 700; + color: #303030; + vertical-align: bottom; + } +} + +.tb-notice { + padding: 15px; + font-size: 16px; + background-color: #f7f7f7; + border: 1px solid #ccc; +} + +.ace_editor { + font-size: 16px !important; +} + +.tb-timewindow-panel, .tb-legend-config-panel { + overflow: hidden; + background: #fff; + border-radius: 4px; + box-shadow: + 0 7px 8px -4px rgba(0, 0, 0, .2), + 0 13px 19px 2px rgba(0, 0, 0, .14), + 0 5px 24px 4px rgba(0, 0, 0, .12); +} + +.tb-panel-actions { + margin-bottom: 0; + padding: 8px 8px 8px 16px; + .mat-button+.mat-button, + .mat-button+.mat-raised-button, + .mat-raised-button+.mat-button, + .mat-raised-button+.mat-raised-button { + margin-left: 8px; + } +} + +.tb-container { + position: relative; + padding: 10px 0; + margin-top: 32px; +} + +.tb-prompt { + display: flex; + font-size: 18px; + font-weight: 400; + line-height: 18px; + color: rgba(0, 0, 0, .38); + &.required { + color: rgb(221, 44, 0); + } +} + +.tb-fullscreen { + position: fixed !important; + top: 0; + left: 0; + width: 100% !important; + height: 100% !important; +} + +.tb-fullscreen-parent { + background: #eee; + z-index: 0; +} + +mat-label { + &.tb-title { + font-size: 13px; + font-weight: 400; + color: #666; + pointer-events: none; + + &.no-padding { + padding-bottom: 0; + } + + &.tb-required::after { + font-size: 13px; + color: rgba(0, 0, 0, .54); + vertical-align: top; + content: " *"; + } + + &.tb-error { + color: rgb(221, 44, 0); + + &.tb-required::after { + color: rgb(221, 44, 0); + } + } + } +} + +.tb-error-messages { + height: 24px; //30px + margin-top: -6px; +} + +.tb-error-message { + transition: all .3s cubic-bezier(.55, 0, .55, .2); + padding: 10px 0 0 10px; + overflow: hidden; + font-size: 12px; + line-height: 14px; + color: rgb(221, 44, 0); +} + +.tb-autocomplete { + .mat-option { + display: block; + line-height: 24px; + height: auto !important; + padding-top: 8px; + border-bottom: 1px solid #eee; + font-size: 14px; + .mat-option-text { + line-height: 24px; + height: auto !important; + white-space: normal !important; + } + } + .mat-option.tb-not-found { + padding: 0; + border-bottom: none; + .mat-option-text { + display: block; + .tb-not-found-content { + padding: 8px 16px 7px; + border-bottom: 1px solid #eee; + } + } + } +} + +.tb-ace-doc-tooltip { + code { + color: #444; + &.title { + font-size: 14px; + } + } + div.tb-function-info { + font-size: 14px; + } + div.tb-function-return { + font-size: 1rem; + letter-spacing: 0.03rem; + color: #444; + code { + font-size: 14px; + letter-spacing: normal; + } + } + div.tb-api-title { + font-weight: bold; + font-size: 16px; + color: #6e6e6e; + padding-top: 12px; + padding-bottom: 12px; + } + table.tb-api-table { + width: 100%; + border-collapse: collapse; + tr { + border-bottom: 1px solid #a8a8a8; + &:last-child { + border-bottom: none; + } + td { + font-size: 14px; + line-height: 1.6rem; + &:first-child { + font-weight: 600; + padding-left: 16px; + width: 20%; + } + &.arg-description { + font-size: 1rem; + letter-spacing: .03rem; + color: #444; + } + } + } + } +} + +.tb-default, .tb-dark { + + /********************************* + * MATERIAL DESIGN CUSTOMIZATIONS + ********************************/ + + .mat-tooltip { + white-space: pre-line; + } + + button { + pointer-events: all; + } + + button.mat-menu-item { + font-size: 15px; + } + + button.mat-fab.mat-fab-bottom-right { + top: auto; + right: 20px; + bottom: 20px; + left: auto; + position: absolute; + } + + .layout-padding, .layout-padding > * { + @media #{$mat-lt-sm} { + padding: 4px; + } + @media #{$mat-gt-xs} { + padding: 8px; + } + } + + .mat-padding { + padding: 8px; + @media #{$mat-gt-sm} { + padding: 16px; + } + } + + .mat-content { + position: relative; + overflow: auto; + } + + .layout-wrap { + flex-wrap: wrap; + } + + mat-form-field.mat-block { + display: block; + } + + .mat-form-field{ + .mat-icon { + margin-right: 4px; + margin-left: 4px; + } + } + + button.mat-menu-item { + overflow: hidden; + fill: #737373; + .tb-alt-text { + float: right; + } + } + + // Material table + + mat-toolbar.mat-primary { + button.mat-icon-button { + mat-icon { + color: white; + } + } + } + + mat-toolbar.mat-table-toolbar { + background: #fff; + padding: 0 24px; + .mat-toolbar-tools { + padding: 0; + & > button.mat-icon-button:last-child { + margin-right: -12px; + } + } + } + + mat-toolbar.mat-table-toolbar:not(.mat-primary), .mat-cell, .mat-expansion-panel-header { + button.mat-icon-button { + mat-icon { + color: rgba(0, 0, 0, .54); + } + &[disabled][disabled] { + mat-icon { + color: rgba(0, 0, 0, .26); + } + } + } + } + + .mat-table { + width: 100%; + max-width: 100%; + display: table; + table-layout: auto; + border-collapse: separate; + margin: 0; + } + + mat-footer-row::after, mat-header-row::after, mat-row::after { + content: none; + } + + mat-header-row { + height: 60px; + } + + mat-footer-row, mat-row { + height: 52px; + } + + mat-header-row, mat-footer-row, mat-row { + min-height: auto; + } + + .mat-row, + .mat-header-row { + display: table-row; + } + + + .mat-header-row.mat-table-sticky { + background-clip: padding-box; + .mat-header-cell { + position: sticky; + top: 0; + z-index: 10; + background: inherit; + background-clip: padding-box; + &.mat-table-sticky { + z-index: 11 !important; + } + } + } + + .mat-cell.mat-table-sticky { + background-clip: padding-box; + } + + .mat-row { + transition: background-color .2s; + &:hover:not(.tb-current-entity) { + background-color: #f4f4f4; + } + &.tb-current-entity { + background-color: #e9e9e9; + } + } + + .mat-row:not(.mat-row-select), .mat-header-row:not(.mat-row-select) { + mat-cell:first-child, mat-footer-cell:first-child, mat-header-cell:first-child { + padding: 0 12px; + } + mat-cell:nth-child(n+2):nth-last-child(n+2), mat-footer-cell:nth-child(n+2):nth-last-child(n+2), mat-header-cell:nth-child(n+2):nth-last-child(n+2) { + padding: 0 28px 0 0; + } + } + + .mat-row.mat-row-select, .mat-header-row.mat-row-select { + mat-cell:first-child, mat-footer-cell:first-child, mat-header-cell:first-child { + width: 30px; + padding: 0 0 0 12px; + } + mat-cell:nth-child(2), mat-footer-cell:nth-child(2), mat-header-cell:nth-child(2) { + padding: 0 12px; + } + mat-cell:nth-child(n+3):nth-last-child(n+2), mat-footer-cell:nth-child(n+3):nth-last-child(n+2), mat-header-cell:nth-child(n+3):nth-last-child(n+2) { + padding: 0 28px 0 0; + } + &.mat-selected:not(.tb-current-entity) { + background-color: #ededed; + } + } + + .mat-cell, + .mat-header-cell { + min-width: 40px; + word-wrap: initial; + display: table-cell; + box-sizing: content-box; + line-break: unset; + width: 0; + overflow: hidden; + vertical-align: middle; + border-width: 0; + border-bottom-width: 1px; + border-bottom-color: rgba(0, 0, 0, 0.12); + border-style: solid; + text-overflow: ellipsis; + touch-action: auto !important; + &:last-child { + padding: 0 12px 0 0; + } + &.mat-column-select { + min-width: 30px; + max-width: 30px; + width: 30px; + padding: 0 0 0 12px; + } + &.mat-column-actions { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + .mat-header-cell { + white-space: nowrap; + button.mat-sort-header-button { + display: block; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + &.mat-number-cell { + .mat-sort-header-container { + justify-content: flex-end; + } + } + } + + .mat-cell { + &.mat-number-cell { + text-align: end; + } + mat-icon { + color: rgba(0, 0, 0, .54); + } + } + + .mat-cell, .mat-footer-cell { + font-size: 13px; + } + + .mat-cell, .mat-footer-cell, .mat-header-cell { + // fix for ie11 'align-items: center' + height: 20px; + } + + .mat-sort-header-sorted .mat-sort-header-arrow { + opacity: 1 !important; + } + + .mat-toolbar-tools { + //font-size: 20px; + letter-spacing: .005em; + //box-sizing: border-box; + font-weight: 400; + display: flex; + align-items: center; + flex-direction: row; + width: 100%; + height: 64px; + //max-height: 64px; + padding: 0 16px; + //margin: 0; + } + + .mat-icon { + vertical-align: bottom; + box-sizing: content-box; + &.tb-mat-16 { + @include tb-mat-icon-size(16); + } + &.tb-mat-18 { + @include tb-mat-icon-size(18); + } + &.tb-mat-20 { + @include tb-mat-icon-size(20); + } + &.tb-mat-32 { + @include tb-mat-icon-size(32); + } + &.tb-mat-96 { + @include tb-mat-icon-size(96); + } + } + + .mat-icon-button { + &.tb-mat-32 { + @include tb-mat-icon-button-size(32); + } + &.tb-mat-96 { + @include tb-mat-icon-button-size(96); + } + } + + .mat-snack-bar-container { + position: absolute; + background: none; + box-shadow: none; + margin: 0; + padding: 0; + border: none; + border-radius: inherit; + max-width: inherit; + min-width: inherit; + pointer-events: none; + display: flex; + } + + .mat-snack-bar-handset { + .mat-snack-bar-container { + position: relative !important; + width: 100% !important; + top: 0 !important; + left: 0 !important; + height: inherit !important; + tb-snack-bar-component { + width: 100%; + } + } + } + + .mat-drawer-side { + border: none; + } + + .mat-drawer-inner-container { + display: flex; + flex-direction: column; + overflow: hidden; + } + + mat-drawer.tb-details-drawer { + z-index: 59 !important; + width: 100% !important; + max-width: 100% !important; + @media #{$mat-gt-sm} { + width: 80% !important; + } + @media #{$mat-gt-md} { + width: 65% !important; + } + } + + .mat-card-subtitle, .mat-card-content { + font-size: 16px; + } + + .mat-toolbar > button:first-child { + margin-left: -8px; + } + + .mat-toolbar > button:last-child { + margin-right: -8px; + } + + .mat-toolbar { + line-height: normal; + + h1, h2, h3, h4, h5, h6 { + overflow: hidden; + text-overflow: ellipsis; + } + } + + mat-toolbar *, mat-toolbar :after, mat-toolbar :before { + box-sizing: border-box; + } + + .mat-button, .mat-flat-button, .mat-stroked-button, .mat-raised-button { + &:not(.mat-icon-button) { + @media #{$mat-lt-md} { + padding: 0 6px; + min-width: 88px; + } + mat-icon { + margin-right: 5px; + } + } + } + + .tb-dialog { + .mat-dialog-container { + padding: 0; + > *:first-child, form { + max-width: 100%; + min-width: 100%; + display: flex; + flex-direction: column; + } + .mat-dialog-content { + margin: 0; + padding: 24px; + } + .mat-dialog-actions { + margin-bottom: 0; + padding: 8px; + } + } + } + + .tb-fullscreen-dialog-gt-sm { + @media #{$mat-gt-sm} { + min-height: 100%; + min-width: 100%; + max-width: none !important; + position: absolute !important; + top: 0; + bottom: 0; + left: 0; + right: 0; + .mat-dialog-container { + > *:first-child, form { + min-width: 100% !important; + } + .mat-dialog-content { + max-height: 100%; + } + } + } + } + + .tb-fullscreen-dialog { + @media #{$mat-lt-sm} { + min-height: 100%; + min-width: 100%; + max-width: none !important; + position: absolute !important; + top: 0; + bottom: 0; + left: 0; + right: 0; + .mat-dialog-container { + > *:first-child, form { + min-width: 100% !important; + height: 100%; + } + .mat-dialog-content { + max-height: 100%; + } + } + } + } + + .tb-absolute-fill { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + } + + .tb-layout-fill { + margin: 0; + width: 100%; + min-height: 100%; + height: 100%; + } + + .tb-progress-cover { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 6; + background-color: #eee; + opacity: 1; + } + + .mat-button.tb-fullscreen-button-style, + .tb-fullscreen-button-style { + background: #ccc; + opacity: .85; + + mat-icon { + color: #666; + } + } + + span.no-data-found { + position: relative; + display: flex; + height: calc(100% - 60px); + text-align: center; + } + + + mat-tab-group.tb-headless { + margin-top: -50px; + } + + .tb-primary-background { + background-color: $primary; + } + + .mat-chip-list.dragging { + .mat-chip { + &.mat-standard-chip { + &::after { + transition: none; + } + &.dropping { + //border: dashed 2px; + //opacity: .5; + + //.md-chip-content { + // margin: -2px; + //} + } + + &.dropping-before { + &::after { + position: absolute; + top: 0; + right: 50%; + bottom: 0; + left: 0; + content: ""; + background-color: #fff; + border: dashed 2px; + border-radius: 16px; + opacity: .7; + } + } + + &.dropping-after { + &::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 50%; + content: ""; + background-color: #fff; + border: dashed 2px; + border-radius: 16px; + opacity: .7; + } + } + } + } + } + + .tb-color-preview { + cursor: pointer; + box-sizing: border-box; + position: relative; + width: 24px; + min-width: 24px; + height: 24px; + overflow: hidden; + content: ""; + border: 2px solid #fff; + border-radius: 50%; + box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .14), 0 2px 2px 0 rgba(0, 0, 0, .098), 0 1px 5px 0 rgba(0, 0, 0, .084); + + @include tb-checkered-bg(); + + .tb-color-result { + width: 100%; + height: 100%; + } + } + + .tb-tooltip-multiline { + max-width: 400px; + height: auto !important; + padding-top: 6px; + padding-bottom: 6px; + line-height: 1.5; + white-space: pre-line; + } + + .tb-toast-panel { + pointer-events: none !important; + } +} diff --git a/ui-ngx/src/test.ts b/ui-ngx/src/test.ts new file mode 100644 index 0000000000..bd1ccf1f3c --- /dev/null +++ b/ui-ngx/src/test.ts @@ -0,0 +1,36 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/dist/zone-testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +declare const require: any; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); diff --git a/ui-ngx/src/theme.scss b/ui-ngx/src/theme.scss new file mode 100644 index 0000000000..6907d6c29d --- /dev/null +++ b/ui-ngx/src/theme.scss @@ -0,0 +1,197 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '~@angular/material/theming'; +@import '~@mat-datetimepicker/core/datetimepicker/datetimepicker-theme.scss'; +@import './scss/constants'; + +@include mat-core(); + +$tb-primary-color: #305680; +$tb-secondary-color: #527dad; +$tb-hue3-color: #a7c1de; + +$tb-dark-primary-color: #9fa8da; + +$tb-mat-indigo: ( + 50: #e8eaf6, + 100: #c5cae9, + 200: #9fa8da, + 300: #7986cb, + 400: #5c6bc0, + 500: $tb-primary-color, + 600: $tb-secondary-color, + 700: #303f9f, + 800: #283593, + 900: #1a237e, + A100: $tb-hue3-color, + A200: #536dfe, + A400: #3d5afe, + A700: #304ffe, + contrast: ( + 50: $dark-primary-text, + 100: $dark-primary-text, + 200: $dark-primary-text, + 300: $light-primary-text, + 400: $light-primary-text, + 500: $light-primary-text, + 600: $light-primary-text, + 700: $light-primary-text, + 800: $light-primary-text, + 900: $light-primary-text, + A100: $dark-primary-text, + A200: $light-primary-text, + A400: $light-primary-text, + A700: $light-primary-text, + ) +); + +$tb-primary: mat-palette($tb-mat-indigo); +$tb-accent: mat-palette($mat-deep-orange); + +$background: (background: map_get($mat-grey, 200)); + +$tb-theme-background: map_merge($mat-light-theme-background, $background); + +$tb-mat-theme: mat-light-theme( + $tb-primary, + $tb-accent +); + +$tb-theme: map_merge($tb-mat-theme, (background: $tb-theme-background)); + +$primary: mat-color($tb-primary); +$accent: mat-color($tb-accent); + +$tb-dark-mat-indigo: ( + 50: #e8eaf6, + 100: #c5cae9, + 200: #9fa8da, + 300: #7986cb, + 400: #5c6bc0, + 500: $tb-dark-primary-color, + 600: $tb-secondary-color, + 700: #303f9f, + 800: $tb-primary-color, + 900: #1a237e, + A100: $tb-hue3-color, + A200: #536dfe, + A400: #3d5afe, + A700: #304ffe, + contrast: ( + 50: $dark-primary-text, + 100: $dark-primary-text, + 200: $dark-primary-text, + 300: $dark-primary-text, + 400: $dark-primary-text, + 500: map_get($tb-mat-indigo, 900), + 600: $light-primary-text, + 700: $light-primary-text, + 800: $light-primary-text, + 900: $light-primary-text, + A100: $dark-primary-text, + A200: $dark-primary-text, + A400: $dark-primary-text, + A700: $dark-primary-text, + ) +); + +$tb-dark-primary: mat-palette($tb-dark-mat-indigo); + +$tb-dark-theme-background: ( + status-bar: black, + app-bar: map_get($tb-dark-mat-indigo, 900), + background: map_get($tb-dark-mat-indigo, 800), + hover: rgba(white, 0.04), + card: map_get($tb-dark-mat-indigo, 800), + dialog: map_get($tb-dark-mat-indigo, 800), + disabled-button: rgba(white, 0.12), + raised-button: map-get($tb-dark-mat-indigo, 50), + focused-button: $light-focused, + selected-button: map_get($tb-dark-mat-indigo, 900), + selected-disabled-button: map_get($tb-dark-mat-indigo, 800), + disabled-button-toggle: black, + unselected-chip: map_get($tb-dark-mat-indigo, 700), + disabled-list-option: black, + tooltip: map_get($mat-grey, 700), +); + +@function get-tb-dark-theme($primary, $accent, $warn: mat-palette($mat-red)) { + @return ( + primary: $primary, + accent: $accent, + warn: $warn, + is-dark: true, + foreground: $mat-dark-theme-foreground, + background: $tb-dark-theme-background, + ); +} + +$tb-dark-theme: get-tb-dark-theme( + $tb-dark-primary, + $tb-accent +); + +@mixin mat-fab-toolbar-theme($theme) { + $primary: map-get($theme, primary); + $accent: map-get($theme, accent); + $warn: map-get($theme, warn); + $background: map-get($theme, background); + $foreground: map-get($theme, foreground); + + mat-fab-toolbar { + .mat-fab-toolbar-background { + background: mat-color($background, app-bar); + color: mat-color($foreground, text); + } + &.mat-primary { + .mat-fab-toolbar-background { + @include _mat-toolbar-color($primary); + } + } + &.mat-accent { + .mat-fab-toolbar-background { + @include _mat-toolbar-color($accent); + } + } + &.mat-warn { + .mat-fab-toolbar-background { + @include _mat-toolbar-color($warn); + } + } + } +} + +@mixin tb-components-theme($theme) { + $primary: map-get($theme, primary); + + mat-toolbar{ + &.mat-hue-3 { + background-color: mat-color($primary, 'A100'); + } + } + + @include mat-fab-toolbar-theme($tb-theme); +} + +.tb-default { + @include angular-material-theme($tb-theme); + @include mat-datetimepicker-theme($tb-theme); + @include tb-components-theme($tb-theme); +} + +.tb-dark { + @include angular-material-theme($tb-dark-theme); +} diff --git a/ui/src/thingsboard.ico b/ui-ngx/src/thingsboard.ico similarity index 100% rename from ui/src/thingsboard.ico rename to ui-ngx/src/thingsboard.ico diff --git a/ui-ngx/src/tsconfig.app.json b/ui-ngx/src/tsconfig.app.json new file mode 100644 index 0000000000..bcbbe5480d --- /dev/null +++ b/ui-ngx/src/tsconfig.app.json @@ -0,0 +1,22 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/app", + "types": ["node", "jquery", "flot", "tooltipster", "tinycolor2", "js-beautify", + "react", "react-dom", "jstree", "raphael", "canvas-gauges", "leaflet", "leaflet.markercluster"] + }, + "angularCompilerOptions": { + "fullTemplateTypeCheck": true + }, + "files": [ + "main.ts", + "polyfills.ts" + ], + "exclude": [ + "test.ts", + "**/*.spec.ts" + ], + "include": [ + "**/*.d.ts" + ] +} diff --git a/ui-ngx/src/tsconfig.spec.json b/ui-ngx/src/tsconfig.spec.json new file mode 100644 index 0000000000..de7733630e --- /dev/null +++ b/ui-ngx/src/tsconfig.spec.json @@ -0,0 +1,18 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/spec", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "test.ts", + "polyfills.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/ui-ngx/src/tslint.json b/ui-ngx/src/tslint.json new file mode 100644 index 0000000000..839fcd0d77 --- /dev/null +++ b/ui-ngx/src/tslint.json @@ -0,0 +1,17 @@ +{ + "extends": "../tslint.json", + "rules": { + "directive-selector": [ + true, + "attribute", + "tb", + "kebab-case" + ], + "component-selector": [ + true, + "element", + "tb", + "kebab-case" + ] + } +} diff --git a/ui-ngx/src/typings/add-marker.d.ts b/ui-ngx/src/typings/add-marker.d.ts new file mode 100644 index 0000000000..565ecdddd4 --- /dev/null +++ b/ui-ngx/src/typings/add-marker.d.ts @@ -0,0 +1,31 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import * as L from 'leaflet' + +declare module 'leaflet' { + + namespace Control { + class AddMarker extends L.Control { } + class AddPolygon extends L.Control { } + } + + namespace control { + function addMarker(options): Control.AddMarker; + function addPolygon(options): Control.AddPolygon; + } + +} diff --git a/ui-ngx/src/typings/jquery.flot.typings.d.ts b/ui-ngx/src/typings/jquery.flot.typings.d.ts new file mode 100644 index 0000000000..ae22e417d0 --- /dev/null +++ b/ui-ngx/src/typings/jquery.flot.typings.d.ts @@ -0,0 +1,129 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + + +interface JQueryPlot extends jquery.flot.plot { + destroy(): void; + highlight(series: jquery.flot.dataSeries | number, datapoint: jquery.flot.item | number): void; + clearSelection(): void; +} + +interface JQueryPlotPoint extends jquery.flot.point { + pageX: number; + pageY: number; + x2: number; +} + +interface JQueryPlotDataSeries extends jquery.flot.dataSeries, JQueryPlotSeriesOptions { + datapoints?: jquery.flot.datapoints; +} + +interface JQueryPlotOptions extends jquery.flot.plotOptions { + title?: string; + subtitile?: string; + shadowSize?: number; + HtmlText?: boolean; + selection?: JQueryPlotSelection; + xaxis?: JQueryPlotAxisOptions; + series?: JQueryPlotSeriesOptions; + crosshair?: JQueryPlotCrosshairOptions; +} + +interface JQueryPlotAxisOptions extends jquery.flot.axisOptions { + label?: string; + labelFont?: any; +} + +interface JQueryPlotAxis extends jquery.flot.axis, JQueryPlotAxisOptions { + options: JQueryPlotAxisOptions; +} + +interface JQueryPlotSeriesOptions extends jquery.flot.seriesOptions { + stack?: boolean; + curvedLines?: JQueryPlotCurvedLinesOptions; + pie?: JQueryPlotPieOptions; +} + +declare type JQueryPlotCrosshairMode = 'x' | 'y' | 'xy' | null; + +interface JQueryPlotCrosshairOptions { + mode?: JQueryPlotCrosshairMode; + color?: string; + lineWidth?: number; +} + +interface JQueryPlotCurvedLinesOptions { + active?: boolean; + apply?: boolean; + monotonicFit?: boolean; + tension?: number; + nrSplinePoints?: number; + legacyOverride?: any; +} + +interface JQueryPlotPieOptions { + show?: boolean; + radius?: any; + innerRadius?: any; + startAngle?: number; + tilt?: number; + offset?: { + top?: number; + left?: number; + }; + stroke?: { + color?: string; + width?: number; + }; + shadow?: { + top?: number; + left?: number; + alpha?: number; + }; + label?: { + show?: boolean; + formatter?: (label: string, slice?: any) => string; + radius?: any; + background?: { + color?: string; + opacity?: number; + }; + threshold?: number; + }; + combine?: { + threshold?: number; + color?: string; + label?: string; + }; + highlight?: number; +} + +declare type JQueryPlotSelectionMode = 'x' | 'y' | 'xy' | null; +declare type JQueryPlotSelectionShape = 'round' | 'mitter' | 'bevel'; + +interface JQueryPlotSelection { + mode?: JQueryPlotSelectionMode; + color?: string; + shape?: JQueryPlotSelectionShape; + minSize?: number; +} + +interface JQueryPlotSelectionRanges { + [axis: string]: { + from: number; + to: number; + }; +} diff --git a/ui-ngx/src/typings/jquery.jstree.typings.d.ts b/ui-ngx/src/typings/jquery.jstree.typings.d.ts new file mode 100644 index 0000000000..34bc1fb288 --- /dev/null +++ b/ui-ngx/src/typings/jquery.jstree.typings.d.ts @@ -0,0 +1,29 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +interface JSTreeEventData { + instance: JSTree; +} + +interface JSTreeModelEventData extends JSTreeEventData { + nodes: string[]; + parent: string; +} + +interface JQuery { + on(events: 'changed.jstree', handler: (e: Event, data: JSTreeEventData) => void): this; + on(events: 'model.jstree', handler: (e: Event, data: JSTreeModelEventData) => void): this; +} diff --git a/ui-ngx/src/typings/jquery.typings.d.ts b/ui-ngx/src/typings/jquery.typings.d.ts new file mode 100644 index 0000000000..dda80228ff --- /dev/null +++ b/ui-ngx/src/typings/jquery.typings.d.ts @@ -0,0 +1,19 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +interface JQuery { + terminal(options?: any): any; +} diff --git a/ui-ngx/src/typings/leadflet-editable.d.ts b/ui-ngx/src/typings/leadflet-editable.d.ts new file mode 100644 index 0000000000..cfaa6fd1b5 --- /dev/null +++ b/ui-ngx/src/typings/leadflet-editable.d.ts @@ -0,0 +1,275 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import * as leaflet from 'leaflet'; + +declare module 'leaflet' { + + /** + * Make geometries editable in Leaflet. + * + * This is not a plug and play UI, and will not. This is a minimal, lightweight, and fully extendable API to + * control editing of geometries. So you can easily build your own UI with your own needs and choices. + */ + interface EditableStatic { + new (map: Map, options: EditOptions): Editable; + } + + /** + * Options to pass to L.Editable when instanciating. + */ + interface EditOptions extends leaflet.MapOptions{ + /** + * Whether to create a L.Editable instance at map init or not. + */ + editable: boolean; + /** + * Class to be used when creating a new Polyline. + */ + polylineClass?: object; + + /** + * Class to be used when creating a new Polygon. + */ + polygonClass?: object; + + /** + * Class to be used when creating a new Marker. + */ + markerClass?: object; + + /** + * CSS class to be added to the map container while drawing. + */ + drawingCSSClass?: string; + + /** + * Layer used to store edit tools (vertex, line guide…). + */ + editLayer?: LayerGroup; + + /** + * Default layer used to store drawn features (marker, polyline…). + */ + featuresLayer?: LayerGroup; + + /** + * Class to be used as vertex, for path editing. + */ + vertexMarkerClass?: object; + + /** + * Class to be used as middle vertex, pulled by the user to create a new point in the middle of a path. + */ + middleMarkerClass?: object; + + /** + * Class to be used as Polyline editor. + */ + polylineEditorClass?: object; + + /** + * Class to be used as Polygon editor. + */ + polygonEditorClass?: object; + + /** + * Class to be used as Marker editor. + */ + markerEditorClass?: object; + + /** + * Options to be passed to the line guides. + */ + lineGuideOptions?: object; + + /** + * Set this to true if you don't want middle markers. + */ + skipMiddleMarkers?: boolean; + } + + /** + * Make geometries editable in Leaflet. + * + * This is not a plug and play UI, and will not. This is a minimal, lightweight, and fully extendable API to + * control editing of geometries. So you can easily build your own UI with your own needs and choices. + */ + interface Editable extends leaflet.Evented { + /** + * Options to pass to L.Editable when instanciating. + */ + options: EditOptions; + + currentPolygon: Polyline|Polygon|Marker; + + /** + * Start drawing a polyline. If latlng is given, a first point will be added. In any case, continuing on user + * click. If options is given, it will be passed to the polyline class constructor. + */ + startPolyline(latLng?: LatLng, options?: PolylineOptions): Polyline; + + /** + * Start drawing a polygon. If latlng is given, a first point will be added. In any case, continuing on user + * click. If options is given, it will be passed to the polygon class constructor. + */ + startPolygon(latLng?: LatLng, options?: PolylineOptions): Polygon; + + /** + * Start adding a marker. If latlng is given, the marker will be shown first at this point. In any case, it + * will follow the user mouse, and will have a final latlng on next click (or touch). If options is given, + * it will be passed to the marker class constructor. + */ + startMarker(latLng?: LatLng, options?: MarkerOptions): Marker; + + /** + * When you need to stop any ongoing drawing, without needing to know which editor is active. + */ + stopDrawing(): void; + + /** + * When you need to commit any ongoing drawing, without needing to know which editor is active. + */ + commitDrawing(): void; + } + + let Editable: EditableStatic; + + /** + * EditableMixin is included to L.Polyline, L.Polygon and L.Marker. It adds the following methods to them. + * + * When editing is enabled, the editor is accessible on the instance with the editor property. + */ + interface EditableMixin { + /** + * Enable editing, by creating an editor if not existing, and then calling enable on it. + */ + enableEdit(map: L.Map): any; + + /** + * Disable editing, also remove the editor property reference. + */ + disableEdit(): void; + + /** + * Enable or disable editing, according to current status. + */ + toggleEdit(): void; + + /** + * Return true if current instance has an editor attached, and this editor is enabled. + */ + editEnabled(): boolean; + } + + interface Map { + + + /** + * Options to pass to L.Editable when instanciating. + */ + editOptions: MapOptions; + + /** + * L.Editable plugin instance. + */ + editTools: Editable; + } + + // tslint:disable-next-line:no-empty-interface + interface Polyline extends EditableMixin {} + + namespace Map { + interface MapOptions { + /** + * Whether to create a L.Editable instance at map init or not. + */ + editable?: boolean; + + /** + * Options to pass to L.Editable when instanciating. + */ + editOptions?: MapOptions; + } + } + + /** + * When editing a feature (marker, polyline…), an editor is attached to it. This editor basically knows + * how to handle the edition. + */ + interface BaseEditor { + /** + * Set up the drawing tools for the feature to be editable. + */ + enable(): MarkerEditor|PolylineEditor|PolygonEditor; + + /** + * Remove editing tools. + */ + disable(): MarkerEditor|PolylineEditor|PolygonEditor; + } + + /** + * Inherit from L.Editable.BaseEditor. + * Inherited by L.Editable.PolylineEditor and L.Editable.PolygonEditor. + */ + interface PathEditor extends BaseEditor { + /** + * Rebuild edit elements (vertex, middlemarker, etc.). + */ + reset(): void; + } + + /** + * Inherit from L.Editable.PathEditor. + */ + interface PolylineEditor extends PathEditor { + /** + * Set up drawing tools to continue the line forward. + */ + continueForward(): void; + + /** + * Set up drawing tools to continue the line backward. + */ + continueBackward(): void; + } + + /** + * Inherit from L.Editable.PathEditor. + */ + interface PolygonEditor extends PathEditor { + /** + * Set up drawing tools for creating a new hole on the polygon. If the latlng param is given, a first + * point is created. + */ + newHole(latlng: LatLng): void; + } + + /** + * Inherit from L.Editable.BaseEditor. + */ + // tslint:disable-next-line:no-empty-interface + interface MarkerEditor extends BaseEditor {} + + interface Marker extends EditableMixin, MarkerEditor {} + + interface Polyline extends EditableMixin, PolylineEditor {} + + interface Polygon extends EditableMixin, PolygonEditor {} + + function map(element: string | HTMLElement, options?: EditOptions): Map; +} diff --git a/ui-ngx/src/typings/rawloader.typings.d.ts b/ui-ngx/src/typings/rawloader.typings.d.ts new file mode 100644 index 0000000000..25b579ef64 --- /dev/null +++ b/ui-ngx/src/typings/rawloader.typings.d.ts @@ -0,0 +1,20 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +declare module '!raw-loader!*' { + const contents: string; + export = contents; +} diff --git a/ui-ngx/src/typings/split.js.typings.d.ts b/ui-ngx/src/typings/split.js.typings.d.ts new file mode 100644 index 0000000000..193da2d474 --- /dev/null +++ b/ui-ngx/src/typings/split.js.typings.d.ts @@ -0,0 +1,39 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +interface SplitOptions { + sizes?: number[]; + minSize?: number[] | number; + gutterSize?: number; + snapOffset?: number; + direction?: 'horizontal' | 'vertical'; + cursor?: 'col-resize' | 'row-resize'; + gutter?: (index: number, direction: string) => HTMLElement; + elementStyle?: (dimension: string, elementSize: number, gutterSize: number) => any; + gutterStyle?: (dimension: string, gutterSize: number) => any; + onDrag?: () => void; + onDragStart?: () => void; + onDragEnd?: () => void; +} + +interface SplitObject { + setSizes: (sizes: number[]) => void; + getSizes: () => number[]; + collapse: (index: number) => void; + destroy: () => void; +} + +declare function Split(elements: HTMLElement | string[], options?: SplitOptions): SplitObject; diff --git a/ui-ngx/src/zone-flags.ts b/ui-ngx/src/zone-flags.ts new file mode 100644 index 0000000000..ec7f803fc4 --- /dev/null +++ b/ui-ngx/src/zone-flags.ts @@ -0,0 +1,19 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +(window as any).__Zone_disable_requestAnimationFrame = false; +(window as any).__Zone_disable_setTimeout = false; +(window as any).__Zone_disable_setInterval = false; diff --git a/ui-ngx/tsconfig.json b/ui-ngx/tsconfig.json new file mode 100644 index 0000000000..b814f9ff19 --- /dev/null +++ b/ui-ngx/tsconfig.json @@ -0,0 +1,45 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "sourceMap": true, + "declaration": false, + "module": "es2020", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "importHelpers": true, + "target": "es5", + "jsx": "react", + "typeRoots": [ + "node_modules/@types", + "src/typings/rawloader.typings.d.ts", + "src/typings/jquery.typings.d.ts", + "src/typings/jquery.flot.typings.d.ts", + "src/typings/jquery.jstree.typings.d.ts", + "src/typings/split.js.typings.d.ts", + "src/typings/add-marker.d.ts", + "src/typings/leaflet-editable.d.ts" + ], + "paths": { + "@app/*": ["src/app/*"], + "@env/*": [ + "src/environments/*" + ], + "@core/*": ["src/app/core/*"], + "@modules/*": ["src/app/modules/*"], + "@shared/*": ["src/app/shared/*"], + "@home/*": ["src/app/modules/home/*"], + "jszip": [ + "node_modules/jszip/dist/jszip.min.js" + ] + }, + "lib": [ + "es2018", + "es2019", + "dom" + ] + } +} diff --git a/ui-ngx/tslint.json b/ui-ngx/tslint.json new file mode 100644 index 0000000000..ba21b20bc6 --- /dev/null +++ b/ui-ngx/tslint.json @@ -0,0 +1,139 @@ +{ + "extends": "tslint:recommended", + "rulesDirectory": [ + "codelyzer" + ], + "rules": { + "align": { + "options": [ + "parameters", + "statements" + ] + }, + "array-type": false, + "arrow-parens": false, + "arrow-return-shorthand": true, + "curly": true, + "deprecation": { + "severity": "warn" + }, + "eofline": true, + "import-blacklist": [ + true, + "rxjs/Rx", + "^.*/public-api$" + ], + "import-spacing": true, + "indent": { + "options": [ + "spaces" + ] + }, + "interface-name": false, + "max-classes-per-file": false, + "max-line-length": [ + true, + 140 + ], + "member-access": false, + "member-ordering": [ + true, + { + "order": [ + "static-field", + "instance-field", + "static-method", + "instance-method" + ] + } + ], + "no-consecutive-blank-lines": false, + "no-console": [ + true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-empty": false, + "no-inferrable-types": [ + true, + "ignore-params" + ], + "no-non-null-assertion": true, + "no-redundant-jsdoc": true, + "no-switch-case-fall-through": true, + "no-var-requires": false, + "object-literal-key-quotes": [ + true, + "as-needed" + ], + "object-literal-sort-keys": false, + "ordered-imports": false, + "quotemark": [ + true, + "single" + ], + "semicolon": { + "options": [ + "always" + ] + }, + "space-before-function-paren": { + "options": { + "anonymous": "never", + "asyncArrow": "always", + "constructor": "never", + "method": "never", + "named": "never" + } + }, + "trailing-comma": false, + "no-output-on-prefix": true, + "typedef-whitespace": { + "options": [ + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + }, + { + "call-signature": "onespace", + "index-signature": "onespace", + "parameter": "onespace", + "property-declaration": "onespace", + "variable-declaration": "onespace" + } + ] + }, + "use-input-property-decorator": true, + "use-output-property-decorator": true, + "use-host-property-decorator": true, + "no-input-rename": true, + "no-output-rename": true, + "use-life-cycle-interface": true, + "use-pipe-transform-interface": true, + "component-class-suffix": true, + "directive-class-suffix": true + , "variable-name": { + "options": [ + "ban-keywords", + "check-format", + "allow-pascal-case" + ] + }, + "whitespace": { + "options": [ + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type", + "check-typecast" + ] + } +} +} diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock new file mode 100644 index 0000000000..dcdc0909c0 --- /dev/null +++ b/ui-ngx/yarn.lock @@ -0,0 +1,10245 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@angular-builders/custom-webpack@^10.0.1": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@angular-builders/custom-webpack/-/custom-webpack-10.0.1.tgz#9126c260ecfeb88c3ba6865e51b486bbe301e504" + integrity sha512-YDy5zEKVwXdoXLjmbsY6kGaEbmunQxaPipxrwLUc9hIjRLU2WcrX9vopf1R9Pgj4POad73IPBNGu+ibqNRFIEQ== + dependencies: + "@angular-devkit/architect" ">=0.1000.0 < 0.1100.0" + "@angular-devkit/build-angular" ">=0.1000.0 < 0.1100.0" + "@angular-devkit/core" "^10.0.0" + lodash "^4.17.15" + ts-node "^9.0.0" + webpack-merge "^4.2.2" + +"@angular-devkit/architect@0.1001.5", "@angular-devkit/architect@>=0.1000.0 < 0.1100.0": + version "0.1001.5" + resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1001.5.tgz#27bdac3c1ee1d3f179e57ce7cfcc1daa4bacdcee" + integrity sha512-W8ZqtbxwDtHnzPoqVyeyDEq24i+H0/i0fjIBuJ+XAMtd3U9JtPALIRLdhnunLXO7OLxjtxjzh0qLxKgiXGEd3g== + dependencies: + "@angular-devkit/core" "10.1.5" + rxjs "6.6.2" + +"@angular-devkit/build-angular@>=0.1000.0 < 0.1100.0", "@angular-devkit/build-angular@^0.1001.5": + version "0.1001.5" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-0.1001.5.tgz#3b03138c41441c26f18f7fe0af03712a9ef8cb8b" + integrity sha512-+KPlwHN2glkXg/H/dlMDWPfY+Io4QqEv4cBRgJjDsW42Di49woNUot8VpGrgnDhVeLaIDmLpD6GUj2DNRgTgcg== + dependencies: + "@angular-devkit/architect" "0.1001.5" + "@angular-devkit/build-optimizer" "0.1001.5" + "@angular-devkit/build-webpack" "0.1001.5" + "@angular-devkit/core" "10.1.5" + "@babel/core" "7.11.1" + "@babel/generator" "7.11.0" + "@babel/plugin-transform-runtime" "7.11.0" + "@babel/preset-env" "7.11.0" + "@babel/runtime" "7.11.2" + "@babel/template" "7.10.4" + "@jsdevtools/coverage-istanbul-loader" "3.0.5" + "@ngtools/webpack" "10.1.5" + autoprefixer "9.8.6" + babel-loader "8.1.0" + browserslist "^4.9.1" + cacache "15.0.5" + caniuse-lite "^1.0.30001032" + circular-dependency-plugin "5.2.0" + copy-webpack-plugin "6.0.3" + core-js "3.6.4" + css-loader "4.2.2" + cssnano "4.1.10" + file-loader "6.0.0" + find-cache-dir "3.3.1" + glob "7.1.6" + jest-worker "26.3.0" + karma-source-map-support "1.4.0" + less-loader "6.2.0" + license-webpack-plugin "2.3.0" + loader-utils "2.0.0" + mini-css-extract-plugin "0.10.0" + minimatch "3.0.4" + open "7.2.0" + parse5 "6.0.1" + parse5-htmlparser2-tree-adapter "6.0.1" + pnp-webpack-plugin "1.6.4" + postcss "7.0.32" + postcss-import "12.0.1" + postcss-loader "3.0.0" + raw-loader "4.0.1" + regenerator-runtime "0.13.7" + resolve-url-loader "3.1.1" + rimraf "3.0.2" + rollup "2.26.5" + rxjs "6.6.2" + sass "1.26.10" + sass-loader "10.0.1" + semver "7.3.2" + source-map "0.7.3" + source-map-loader "1.0.2" + source-map-support "0.5.19" + speed-measure-webpack-plugin "1.3.3" + style-loader "1.2.1" + stylus "0.54.8" + stylus-loader "3.0.2" + terser "5.3.0" + terser-webpack-plugin "4.1.0" + tree-kill "1.2.2" + webpack "4.44.1" + webpack-dev-middleware "3.7.2" + webpack-dev-server "3.11.0" + webpack-merge "4.2.2" + webpack-sources "1.4.3" + webpack-subresource-integrity "1.4.1" + worker-plugin "5.0.0" + +"@angular-devkit/build-optimizer@0.1001.5": + version "0.1001.5" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.1001.5.tgz#99532fcaa953a251ab519961f9517b390f10bf9f" + integrity sha512-N5zXJMs9JwFtbuDyEnNk1UX6clC/RFiTaHb/ofaTYbq39xEKGbZRVCFP8bGM4JEI5trF05m7JTD3wo3nHtZLqw== + dependencies: + loader-utils "2.0.0" + source-map "0.7.3" + tslib "2.0.1" + typescript "4.0.2" + webpack-sources "1.4.3" + +"@angular-devkit/build-webpack@0.1001.5": + version "0.1001.5" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1001.5.tgz#bb7d76b7b5a0565a1694ee6a7fc89ef51acf7c33" + integrity sha512-Z6U0jz6FOIC3Faiq642smQEjVZ8ZaLpxNd/QCnFWLkNAhySP8TVcRWvF8AzHJNXBLDcjm3uDy+3OX+lYALJibg== + dependencies: + "@angular-devkit/architect" "0.1001.5" + "@angular-devkit/core" "10.1.5" + rxjs "6.6.2" + +"@angular-devkit/core@10.1.5", "@angular-devkit/core@^10.0.0": + version "10.1.5" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-10.1.5.tgz#3eb4321cd929a4a92a887f6a4810bdc1c7eb593e" + integrity sha512-Ly97h90Z6ZLhSnTkk2baUDNLeOrKgj/bUPkcBEKWranx6IRx8FMzin/+ysIQasBlEXWPIc8QbBmCz7xXkO4p7g== + dependencies: + ajv "6.12.4" + fast-json-stable-stringify "2.1.0" + magic-string "0.25.7" + rxjs "6.6.2" + source-map "0.7.3" + +"@angular-devkit/schematics@10.1.5": + version "10.1.5" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-10.1.5.tgz#a0f13f084c0d84e7718d8bdd7a367775660afb5b" + integrity sha512-5bhQX/PC548wIPcgCx9Q0Oewe8/i8+0eZvD9qLVWzJvUEKqgbjgoA7r7KJIJx2WINbESJGTIjwbXSZ6JmAJNhA== + dependencies: + "@angular-devkit/core" "10.1.5" + ora "5.0.0" + rxjs "6.6.2" + +"@angular/animations@^10.1.5": + version "10.1.5" + resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-10.1.5.tgz#b89540ba81fc09fdb1b0ed8ec13773232bdc14d3" + integrity sha512-RbUIluxgE5pSWWdODlcEAQuRqc/D1A2v275zBsMFjwJg3/cZl/z+RWcFJedHpJHEtbz7Aay1UWHu9jhXfA8elg== + dependencies: + tslib "^2.0.0" + +"@angular/cdk@^10.2.4": + version "10.2.4" + resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-10.2.4.tgz#656095648af005e7fa02c4cc68865be4bf59fc10" + integrity sha512-Ccm/iRb6zELWwMem6qTnFCalMVX/aS17hhN65efpNKrH3ovhyQSPWtF4p9IaEJ3rZpfXqXMPBneJ9ZXAA/iKog== + dependencies: + tslib "^2.0.0" + optionalDependencies: + parse5 "^5.0.0" + +"@angular/cli@^10.1.5": + version "10.1.5" + resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-10.1.5.tgz#1e2eee9cdb54889e40144c64f80a0122d3fab594" + integrity sha512-HlJVDxuTfrmxp8CvABV1pn7Ffeo0q0PuAR7gNCDcVi2vN7EDmBRRnyxBvATO4KzE5DHiSIqF0xLIsokSS7JC6w== + dependencies: + "@angular-devkit/architect" "0.1001.5" + "@angular-devkit/core" "10.1.5" + "@angular-devkit/schematics" "10.1.5" + "@schematics/angular" "10.1.5" + "@schematics/update" "0.1001.5" + "@yarnpkg/lockfile" "1.1.0" + ansi-colors "4.1.1" + debug "4.1.1" + ini "1.3.5" + inquirer "7.3.3" + npm-package-arg "8.0.1" + npm-pick-manifest "6.1.0" + open "7.2.0" + pacote "9.5.12" + read-package-tree "5.3.1" + rimraf "3.0.2" + semver "7.3.2" + symbol-observable "1.2.0" + universal-analytics "0.4.23" + uuid "8.3.0" + +"@angular/common@^10.1.5": + version "10.1.5" + resolved "https://registry.yarnpkg.com/@angular/common/-/common-10.1.5.tgz#8a2cca7ea70091f4d5db8736292ec60ff143137a" + integrity sha512-xo10mSQYuf6x1XrnTfwt3Rs7JtSMkSyrJtAS/vNQKdBP/8zmn6pP9zRpp7vhQ5qF+W3HN8rPLb+YI2F6uaGjBg== + dependencies: + tslib "^2.0.0" + +"@angular/compiler-cli@^10.1.5": + version "10.1.5" + resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-10.1.5.tgz#65e586d8650ed6ac70034472be043127d040ad7a" + integrity sha512-AJ4eOHUxgDdfq/EagUlhJ6HaNlHajtmPkhXp2HmNMNN1nPN55VZSvN43Co2gdAHiFENqsTNlnQH630aXaDyVbQ== + dependencies: + canonical-path "1.0.0" + chokidar "^3.0.0" + convert-source-map "^1.5.1" + dependency-graph "^0.7.2" + fs-extra "4.0.2" + magic-string "^0.25.0" + minimist "^1.2.0" + reflect-metadata "^0.1.2" + semver "^6.3.0" + source-map "^0.6.1" + sourcemap-codec "^1.4.8" + tslib "^2.0.0" + yargs "15.3.0" + +"@angular/compiler@9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-9.0.0.tgz#87e0bef4c369b6cadae07e3a4295778fc93799d5" + integrity sha512-ctjwuntPfZZT2mNj2NDIVu51t9cvbhl/16epc5xEwyzyDt76pX9UgwvY+MbXrf/C/FWwdtmNtfP698BKI+9leQ== + +"@angular/compiler@^10.1.5": + version "10.1.5" + resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-10.1.5.tgz#0721759b2589faf172be7a6ddde4544dca2679f1" + integrity sha512-3LyFkEzs6P6YYKkE/6E4PasMd58EBddOt9kR9kPmj9Atv/BLY3nc5RSWkOe4rK4GnBVP+ByzQiT9Fn5CiQnG/g== + dependencies: + tslib "^2.0.0" + +"@angular/core@9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@angular/core/-/core-9.0.0.tgz#227dc53e1ac81824f998c6e76000b7efc522641e" + integrity sha512-6Pxgsrf0qF9iFFqmIcWmjJGkkCaCm6V5QNnxMy2KloO3SDq6QuMVRbN9RtC8Urmo25LP+eZ6ZgYqFYpdD8Hd9w== + +"@angular/core@^10.1.5": + version "10.1.5" + resolved "https://registry.yarnpkg.com/@angular/core/-/core-10.1.5.tgz#2a855edc013237db93d18620ad3d4d74ef4a11b4" + integrity sha512-B8j1B5vkBmzyan78kMJhw7dfhe7znmujbeDU7qRgRcIllc9pVJv7D133Yze6JFiLVg21PfyFYs8FBJNeq39hxQ== + dependencies: + tslib "^2.0.0" + +"@angular/flex-layout@^10.0.0-beta.32": + version "10.0.0-beta.32" + resolved "https://registry.yarnpkg.com/@angular/flex-layout/-/flex-layout-10.0.0-beta.32.tgz#a797ebd6f3689c71a63e99aa62f2b9cb933f5e2d" + integrity sha512-JvuY4dUoy5jyCTIrFiq7n30Znakh1pD3nbg0h0hs2r3t1OiDQb0ZSI1wcumosG/vYHsuJQTuNhbfaIZzA1x8nA== + dependencies: + tslib "^2.0.0" + +"@angular/forms@^10.1.5": + version "10.1.5" + resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-10.1.5.tgz#2cde5e119c6f1fe9d7afceb034b6a62e231223d9" + integrity sha512-fkXKCwXL0XeFMUkmzJpm+FHYrv1CCfFGxYEBQ/bzfd3Op+dFJqEPiOwK3wG943Y09THday6H509RwwEIyF/4yw== + dependencies: + tslib "^2.0.0" + +"@angular/language-service@^10.1.5": + version "10.1.5" + resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-10.1.5.tgz#bf5658075f7114364e2f266f2d0cc61d93d955a1" + integrity sha512-D3y97MciUx8txpwkRnMPOhPI1fyPJCGL0JwNOO0jq1qNKMzwRRetaacKUkv1apCZWU7r2PuL2GlJM6tIX5Ml3Q== + +"@angular/material@^10.2.4": + version "10.2.4" + resolved "https://registry.yarnpkg.com/@angular/material/-/material-10.2.4.tgz#9a3e4958ed72ae77c1638075efe5bff5158b2ba4" + integrity sha512-m5pRzCZQlpb7BZrc+LV+eeMU9M76obWVbNy8or6gvBPa6awfbwOz8uBryIvVngdkhoIieqAu1iV4bG7b7Xp3sg== + dependencies: + tslib "^2.0.0" + +"@angular/platform-browser-dynamic@^10.1.5": + version "10.1.5" + resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-10.1.5.tgz#05e58c1a3468371a553fad0291756d4f2d7d8b8e" + integrity sha512-wxHm1UFCtB+oU+IJ6pACGmjO9H8KVzJOLYL5hp2w0k8s7k7Zg73f6BdRgWWEEYv6uYIfF77qtKwgbH0X5H9S+w== + dependencies: + tslib "^2.0.0" + +"@angular/platform-browser@^10.1.5": + version "10.1.5" + resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-10.1.5.tgz#b166b6f520e34012c91e2586022d00c5e2be8f49" + integrity sha512-qMAoPHt6dgXMtieI4zx/s5yX7FFRRUDp1R4GMBCZHPN3p66WdEVxBJo4p5RWhZJioXpUwKz8Xvc+Rrh7r0KDBA== + dependencies: + tslib "^2.0.0" + +"@angular/router@^10.1.5": + version "10.1.5" + resolved "https://registry.yarnpkg.com/@angular/router/-/router-10.1.5.tgz#8cadac2d200c237522db6d99b60846d08c789304" + integrity sha512-tY88ZzoBrc9K67wi5V1NLnurd3r9bYR2csZ6/zJeOE+Vdxz9ChSaglgh9T0vQdbVEAjVGPP5QtYaFO2Xv4qOIg== + dependencies: + tslib "^2.0.0" + +"@ant-design/css-animation@^1.7.2": + version "1.7.3" + resolved "https://registry.yarnpkg.com/@ant-design/css-animation/-/css-animation-1.7.3.tgz#60a1c970014e86b28f940510d69e503e428f1136" + integrity sha512-LrX0OGZtW+W6iLnTAqnTaoIsRelYeuLZWsrmBJFUXDALQphPsN8cE5DCsmoSlL0QYb94BQxINiuS70Ar/8BNgA== + +"@auth0/angular-jwt@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@auth0/angular-jwt/-/angular-jwt-5.0.1.tgz#37851d3ca2a0e88b3e673afd7dd2891f0c61bdf5" + integrity sha512-djllMh6rthPscEj5n5T9zF223q8t+sDqnUuAYTJjdKoHvMAzYwwi2yP67HbojqjODG4ZLFAcPtRuzGgp+r7nDQ== + dependencies: + tslib "^2.0.0" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" + integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== + dependencies: + "@babel/highlight" "^7.10.4" + +"@babel/compat-data@^7.10.4", "@babel/compat-data@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.11.0.tgz#e9f73efe09af1355b723a7f39b11bad637d7c99c" + integrity sha512-TPSvJfv73ng0pfnEOh17bYMPQbI95+nGWc71Ss4vZdRBHTDqmM9Z8ZV4rYz8Ks7sfzc95n30k6ODIq5UGnXcYQ== + dependencies: + browserslist "^4.12.0" + invariant "^2.2.4" + semver "^5.5.0" + +"@babel/core@7.11.1": + version "7.11.1" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.1.tgz#2c55b604e73a40dc21b0e52650b11c65cf276643" + integrity sha512-XqF7F6FWQdKGGWAzGELL+aCO1p+lRY5Tj5/tbT3St1G8NaH70jhhDIKknIZaDans0OQBG5wRAldROLHSt44BgQ== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.11.0" + "@babel/helper-module-transforms" "^7.11.0" + "@babel/helpers" "^7.10.4" + "@babel/parser" "^7.11.1" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.11.0" + "@babel/types" "^7.11.0" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.1" + json5 "^2.1.2" + lodash "^4.17.19" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + +"@babel/core@^7.7.5": + version "7.11.6" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.6.tgz#3a9455dc7387ff1bac45770650bc13ba04a15651" + integrity sha512-Wpcv03AGnmkgm6uS6k8iwhIwTrcP0m17TL1n1sy7qD0qelDu4XNeW0dN0mHfa+Gei211yDaLoEe/VlbXQzM4Bg== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.11.6" + "@babel/helper-module-transforms" "^7.11.0" + "@babel/helpers" "^7.10.4" + "@babel/parser" "^7.11.5" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.11.5" + "@babel/types" "^7.11.5" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.1" + json5 "^2.1.2" + lodash "^4.17.19" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + +"@babel/generator@7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.0.tgz#4b90c78d8c12825024568cbe83ee6c9af193585c" + integrity sha512-fEm3Uzw7Mc9Xi//qU20cBKatTfs2aOtKqmvy/Vm7RkJEGFQ4xc9myCfbXxqK//ZS8MR/ciOHw6meGASJuKmDfQ== + dependencies: + "@babel/types" "^7.11.0" + jsesc "^2.5.1" + source-map "^0.5.0" + +"@babel/generator@^7.11.0", "@babel/generator@^7.11.5", "@babel/generator@^7.11.6": + version "7.11.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.6.tgz#b868900f81b163b4d464ea24545c61cbac4dc620" + integrity sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA== + dependencies: + "@babel/types" "^7.11.5" + jsesc "^2.5.1" + source-map "^0.5.0" + +"@babel/helper-annotate-as-pure@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3" + integrity sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA== + dependencies: + "@babel/types" "^7.10.4" + +"@babel/helper-builder-binary-assignment-operator-visitor@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz#bb0b75f31bf98cbf9ff143c1ae578b87274ae1a3" + integrity sha512-L0zGlFrGWZK4PbT8AszSfLTM5sDU1+Az/En9VrdT8/LmEiJt4zXt+Jve9DCAnQcbqDhCI+29y/L93mrDzddCcg== + dependencies: + "@babel/helper-explode-assignable-expression" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/helper-compilation-targets@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.4.tgz#804ae8e3f04376607cc791b9d47d540276332bd2" + integrity sha512-a3rYhlsGV0UHNDvrtOXBg8/OpfV0OKTkxKPzIplS1zpx7CygDcWWxckxZeDd3gzPzC4kUT0A4nVFDK0wGMh4MQ== + dependencies: + "@babel/compat-data" "^7.10.4" + browserslist "^4.12.0" + invariant "^2.2.4" + levenary "^1.1.1" + semver "^5.5.0" + +"@babel/helper-create-class-features-plugin@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz#9f61446ba80e8240b0a5c85c6fdac8459d6f259d" + integrity sha512-0nkdeijB7VlZoLT3r/mY3bUkw3T8WG/hNw+FATs/6+pG2039IJWjTYL0VTISqsNHMUTEnwbVnc89WIJX9Qed0A== + dependencies: + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-member-expression-to-functions" "^7.10.5" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.10.4" + +"@babel/helper-create-regexp-features-plugin@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz#fdd60d88524659a0b6959c0579925e425714f3b8" + integrity sha512-2/hu58IEPKeoLF45DBwx3XFqsbCXmkdAay4spVr2x0jYgRxrSNp+ePwvSsy9g6YSaNDcKIQVPXk1Ov8S2edk2g== + dependencies: + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-regex" "^7.10.4" + regexpu-core "^4.7.0" + +"@babel/helper-define-map@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz#b53c10db78a640800152692b13393147acb9bb30" + integrity sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ== + dependencies: + "@babel/helper-function-name" "^7.10.4" + "@babel/types" "^7.10.5" + lodash "^4.17.19" + +"@babel/helper-explode-assignable-expression@^7.10.4": + version "7.11.4" + resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.11.4.tgz#2d8e3470252cc17aba917ede7803d4a7a276a41b" + integrity sha512-ux9hm3zR4WV1Y3xXxXkdG/0gxF9nvI0YVmKVhvK9AfMoaQkemL3sJpXw+Xbz65azo8qJiEz2XVDUpK3KYhH3ZQ== + dependencies: + "@babel/types" "^7.10.4" + +"@babel/helper-function-name@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a" + integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ== + dependencies: + "@babel/helper-get-function-arity" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/helper-get-function-arity@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" + integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A== + dependencies: + "@babel/types" "^7.10.4" + +"@babel/helper-hoist-variables@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz#d49b001d1d5a68ca5e6604dda01a6297f7c9381e" + integrity sha512-wljroF5PgCk2juF69kanHVs6vrLwIPNp6DLD+Lrl3hoQ3PpPPikaDRNFA+0t81NOoMt2DL6WW/mdU8k4k6ZzuA== + dependencies: + "@babel/types" "^7.10.4" + +"@babel/helper-member-expression-to-functions@^7.10.4", "@babel/helper-member-expression-to-functions@^7.10.5": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz#ae69c83d84ee82f4b42f96e2a09410935a8f26df" + integrity sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q== + dependencies: + "@babel/types" "^7.11.0" + +"@babel/helper-module-imports@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz#4c5c54be04bd31670a7382797d75b9fa2e5b5620" + integrity sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw== + dependencies: + "@babel/types" "^7.10.4" + +"@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.10.5", "@babel/helper-module-transforms@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359" + integrity sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg== + dependencies: + "@babel/helper-module-imports" "^7.10.4" + "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-simple-access" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.11.0" + "@babel/template" "^7.10.4" + "@babel/types" "^7.11.0" + lodash "^4.17.19" + +"@babel/helper-optimise-call-expression@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673" + integrity sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg== + dependencies: + "@babel/types" "^7.10.4" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" + integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== + +"@babel/helper-regex@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.10.5.tgz#32dfbb79899073c415557053a19bd055aae50ae0" + integrity sha512-68kdUAzDrljqBrio7DYAEgCoJHxppJOERHOgOrDN7WjOzP0ZQ1LsSDRXcemzVZaLvjaJsJEESb6qt+znNuENDg== + dependencies: + lodash "^4.17.19" + +"@babel/helper-remap-async-to-generator@^7.10.4": + version "7.11.4" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.11.4.tgz#4474ea9f7438f18575e30b0cac784045b402a12d" + integrity sha512-tR5vJ/vBa9wFy3m5LLv2faapJLnDFxNWff2SAYkSE4rLUdbp7CdObYFgI7wK4T/Mj4UzpjPwzR8Pzmr5m7MHGA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-wrap-function" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/helper-replace-supers@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz#d585cd9388ea06e6031e4cd44b6713cbead9e6cf" + integrity sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.10.4" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/traverse" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/helper-simple-access@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz#0f5ccda2945277a2a7a2d3a821e15395edcf3461" + integrity sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw== + dependencies: + "@babel/template" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/helper-skip-transparent-expression-wrappers@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz#eec162f112c2f58d3af0af125e3bb57665146729" + integrity sha512-0XIdiQln4Elglgjbwo9wuJpL/K7AGCY26kmEt0+pRP0TAj4jjyNq1MjoRvikrTVqKcx4Gysxt4cXvVFXP/JO2Q== + dependencies: + "@babel/types" "^7.11.0" + +"@babel/helper-split-export-declaration@^7.10.4", "@babel/helper-split-export-declaration@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz#f8a491244acf6a676158ac42072911ba83ad099f" + integrity sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg== + dependencies: + "@babel/types" "^7.11.0" + +"@babel/helper-validator-identifier@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" + integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== + +"@babel/helper-wrap-function@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz#8a6f701eab0ff39f765b5a1cfef409990e624b87" + integrity sha512-6py45WvEF0MhiLrdxtRjKjufwLL1/ob2qDJgg5JgNdojBAZSAKnAjkyOCNug6n+OBl4VW76XjvgSFTdaMcW0Ug== + dependencies: + "@babel/helper-function-name" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/helpers@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.4.tgz#2abeb0d721aff7c0a97376b9e1f6f65d7a475044" + integrity sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA== + dependencies: + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/highlight@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" + integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/parser@^7.10.4", "@babel/parser@^7.11.1", "@babel/parser@^7.11.5": + version "7.11.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037" + integrity sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q== + +"@babel/plugin-proposal-async-generator-functions@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz#3491cabf2f7c179ab820606cec27fed15e0e8558" + integrity sha512-cNMCVezQbrRGvXJwm9fu/1sJj9bHdGAgKodZdLqOQIpfoH3raqmRPBM17+lh7CzhiKRRBrGtZL9WcjxSoGYUSg== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-remap-async-to-generator" "^7.10.4" + "@babel/plugin-syntax-async-generators" "^7.8.0" + +"@babel/plugin-proposal-class-properties@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz#a33bf632da390a59c7a8c570045d1115cd778807" + integrity sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-proposal-dynamic-import@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz#ba57a26cb98b37741e9d5bca1b8b0ddf8291f17e" + integrity sha512-up6oID1LeidOOASNXgv/CFbgBqTuKJ0cJjz6An5tWD+NVBNlp3VNSBxv2ZdU7SYl3NxJC7agAQDApZusV6uFwQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-dynamic-import" "^7.8.0" + +"@babel/plugin-proposal-export-namespace-from@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.10.4.tgz#570d883b91031637b3e2958eea3c438e62c05f54" + integrity sha512-aNdf0LY6/3WXkhh0Fdb6Zk9j1NMD8ovj3F6r0+3j837Pn1S1PdNtcwJ5EG9WkVPNHPxyJDaxMaAOVq4eki0qbg== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + +"@babel/plugin-proposal-json-strings@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.4.tgz#593e59c63528160233bd321b1aebe0820c2341db" + integrity sha512-fCL7QF0Jo83uy1K0P2YXrfX11tj3lkpN7l4dMv9Y9VkowkhkQDwFHFd8IiwyK5MZjE8UpbgokkgtcReH88Abaw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-json-strings" "^7.8.0" + +"@babel/plugin-proposal-logical-assignment-operators@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.11.0.tgz#9f80e482c03083c87125dee10026b58527ea20c8" + integrity sha512-/f8p4z+Auz0Uaf+i8Ekf1iM7wUNLcViFUGiPxKeXvxTSl63B875YPiVdUDdem7hREcI0E0kSpEhS8tF5RphK7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + +"@babel/plugin-proposal-nullish-coalescing-operator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz#02a7e961fc32e6d5b2db0649e01bf80ddee7e04a" + integrity sha512-wq5n1M3ZUlHl9sqT2ok1T2/MTt6AXE0e1Lz4WzWBr95LsAZ5qDXe4KnFuauYyEyLiohvXFMdbsOTMyLZs91Zlw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" + +"@babel/plugin-proposal-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.4.tgz#ce1590ff0a65ad12970a609d78855e9a4c1aef06" + integrity sha512-73/G7QoRoeNkLZFxsoCCvlg4ezE4eM+57PnOqgaPOozd5myfj7p0muD1mRVJvbUWbOzD+q3No2bWbaKy+DJ8DA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + +"@babel/plugin-proposal-object-rest-spread@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.11.0.tgz#bd81f95a1f746760ea43b6c2d3d62b11790ad0af" + integrity sha512-wzch41N4yztwoRw0ak+37wxwJM2oiIiy6huGCoqkvSTA9acYWcPfn9Y4aJqmFFJ70KTJUu29f3DQ43uJ9HXzEA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.0" + "@babel/plugin-transform-parameters" "^7.10.4" + +"@babel/plugin-proposal-optional-catch-binding@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.4.tgz#31c938309d24a78a49d68fdabffaa863758554dd" + integrity sha512-LflT6nPh+GK2MnFiKDyLiqSqVHkQnVf7hdoAvyTnnKj9xB3docGRsdPuxp6qqqW19ifK3xgc9U5/FwrSaCNX5g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" + +"@babel/plugin-proposal-optional-chaining@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz#de5866d0646f6afdaab8a566382fe3a221755076" + integrity sha512-v9fZIu3Y8562RRwhm1BbMRxtqZNFmFA2EG+pT2diuU8PT3H6T/KXoZ54KgYisfOFZHV6PfvAiBIZ9Rcz+/JCxA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0" + "@babel/plugin-syntax-optional-chaining" "^7.8.0" + +"@babel/plugin-proposal-private-methods@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.4.tgz#b160d972b8fdba5c7d111a145fc8c421fc2a6909" + integrity sha512-wh5GJleuI8k3emgTg5KkJK6kHNsGEr0uBTDBuQUBJwckk9xs1ez79ioheEVVxMLyPscB0LfkbVHslQqIzWV6Bw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-proposal-unicode-property-regex@^7.10.4", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.4.tgz#4483cda53041ce3413b7fe2f00022665ddfaa75d" + integrity sha512-H+3fOgPnEXFL9zGYtKQe4IDOPKYlZdF1kqFDQRRb8PK4B8af1vAGK04tF5iQAAsui+mHNBQSAtd2/ndEDe9wuA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-async-generators@^7.8.0": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz#6644e6a0baa55a61f9e3231f6c9eeb6ee46c124c" + integrity sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-dynamic-import@^7.8.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" + integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-export-namespace-from@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" + integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-syntax-json-strings@^7.8.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-top-level-await@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.10.4.tgz#4bbeb8917b54fcf768364e0a81f560e33a3ef57d" + integrity sha512-ni1brg4lXEmWyafKr0ccFWkJG0CeMt4WV1oyeBW6EFObF4oOHclbkj5cARxAPQyAQ2UTuplJyK4nfkXIMMFvsQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-arrow-functions@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.4.tgz#e22960d77e697c74f41c501d44d73dbf8a6a64cd" + integrity sha512-9J/oD1jV0ZCBcgnoFWFq1vJd4msoKb/TCpGNFyyLt0zABdcvgK3aYikZ8HjzB14c26bc7E3Q1yugpwGy2aTPNA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-async-to-generator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.4.tgz#41a5017e49eb6f3cda9392a51eef29405b245a37" + integrity sha512-F6nREOan7J5UXTLsDsZG3DXmZSVofr2tGNwfdrVwkDWHfQckbQXnXSPfD7iO+c/2HGqycwyLST3DnZ16n+cBJQ== + dependencies: + "@babel/helper-module-imports" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-remap-async-to-generator" "^7.10.4" + +"@babel/plugin-transform-block-scoped-functions@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.4.tgz#1afa595744f75e43a91af73b0d998ecfe4ebc2e8" + integrity sha512-WzXDarQXYYfjaV1szJvN3AD7rZgZzC1JtjJZ8dMHUyiK8mxPRahynp14zzNjU3VkPqPsO38CzxiWO1c9ARZ8JA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-block-scoping@^7.10.4": + version "7.11.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.11.1.tgz#5b7efe98852bef8d652c0b28144cd93a9e4b5215" + integrity sha512-00dYeDE0EVEHuuM+26+0w/SCL0BH2Qy7LwHuI4Hi4MH5gkC8/AqMN5uWFJIsoXZrAphiMm1iXzBw6L2T+eA0ew== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-classes@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.4.tgz#405136af2b3e218bc4a1926228bc917ab1a0adc7" + integrity sha512-2oZ9qLjt161dn1ZE0Ms66xBncQH4In8Sqw1YWgBUZuGVJJS5c0OFZXL6dP2MRHrkU/eKhWg8CzFJhRQl50rQxA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-define-map" "^7.10.4" + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.10.4" + globals "^11.1.0" + +"@babel/plugin-transform-computed-properties@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.4.tgz#9ded83a816e82ded28d52d4b4ecbdd810cdfc0eb" + integrity sha512-JFwVDXcP/hM/TbyzGq3l/XWGut7p46Z3QvqFMXTfk6/09m7xZHJUN9xHfsv7vqqD4YnfI5ueYdSJtXqqBLyjBw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-destructuring@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.4.tgz#70ddd2b3d1bea83d01509e9bb25ddb3a74fc85e5" + integrity sha512-+WmfvyfsyF603iPa6825mq6Qrb7uLjTOsa3XOFzlYcYDHSS4QmpOWOL0NNBY5qMbvrcf3tq0Cw+v4lxswOBpgA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-dotall-regex@^7.10.4", "@babel/plugin-transform-dotall-regex@^7.4.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.4.tgz#469c2062105c1eb6a040eaf4fac4b488078395ee" + integrity sha512-ZEAVvUTCMlMFAbASYSVQoxIbHm2OkG2MseW6bV2JjIygOjdVv8tuxrCTzj1+Rynh7ODb8GivUy7dzEXzEhuPaA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-duplicate-keys@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.4.tgz#697e50c9fee14380fe843d1f306b295617431e47" + integrity sha512-GL0/fJnmgMclHiBTTWXNlYjYsA7rDrtsazHG6mglaGSTh0KsrW04qml+Bbz9FL0LcJIRwBWL5ZqlNHKTkU3xAA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-exponentiation-operator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.4.tgz#5ae338c57f8cf4001bdb35607ae66b92d665af2e" + integrity sha512-S5HgLVgkBcRdyQAHbKj+7KyuWx8C6t5oETmUuwz1pt3WTWJhsUV0WIIXuVvfXMxl/QQyHKlSCNNtaIamG8fysw== + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-for-of@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.4.tgz#c08892e8819d3a5db29031b115af511dbbfebae9" + integrity sha512-ItdQfAzu9AlEqmusA/65TqJ79eRcgGmpPPFvBnGILXZH975G0LNjP1yjHvGgfuCxqrPPueXOPe+FsvxmxKiHHQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-function-name@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.4.tgz#6a467880e0fc9638514ba369111811ddbe2644b7" + integrity sha512-OcDCq2y5+E0dVD5MagT5X+yTRbcvFjDI2ZVAottGH6tzqjx/LKpgkUepu3hp/u4tZBzxxpNGwLsAvGBvQ2mJzg== + dependencies: + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-literals@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.4.tgz#9f42ba0841100a135f22712d0e391c462f571f3c" + integrity sha512-Xd/dFSTEVuUWnyZiMu76/InZxLTYilOSr1UlHV+p115Z/Le2Fi1KXkJUYz0b42DfndostYlPub3m8ZTQlMaiqQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-member-expression-literals@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.4.tgz#b1ec44fcf195afcb8db2c62cd8e551c881baf8b7" + integrity sha512-0bFOvPyAoTBhtcJLr9VcwZqKmSjFml1iVxvPL0ReomGU53CX53HsM4h2SzckNdkQcHox1bpAqzxBI1Y09LlBSw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-modules-amd@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.5.tgz#1b9cddaf05d9e88b3aad339cb3e445c4f020a9b1" + integrity sha512-elm5uruNio7CTLFItVC/rIzKLfQ17+fX7EVz5W0TMgIHFo1zY0Ozzx+lgwhL4plzl8OzVn6Qasx5DeEFyoNiRw== + dependencies: + "@babel/helper-module-transforms" "^7.10.5" + "@babel/helper-plugin-utils" "^7.10.4" + babel-plugin-dynamic-import-node "^2.3.3" + +"@babel/plugin-transform-modules-commonjs@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz#66667c3eeda1ebf7896d41f1f16b17105a2fbca0" + integrity sha512-Xj7Uq5o80HDLlW64rVfDBhao6OX89HKUmb+9vWYaLXBZOma4gA6tw4Ni1O5qVDoZWUV0fxMYA0aYzOawz0l+1w== + dependencies: + "@babel/helper-module-transforms" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-simple-access" "^7.10.4" + babel-plugin-dynamic-import-node "^2.3.3" + +"@babel/plugin-transform-modules-systemjs@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz#6270099c854066681bae9e05f87e1b9cadbe8c85" + integrity sha512-f4RLO/OL14/FP1AEbcsWMzpbUz6tssRaeQg11RH1BP/XnPpRoVwgeYViMFacnkaw4k4wjRSjn3ip1Uw9TaXuMw== + dependencies: + "@babel/helper-hoist-variables" "^7.10.4" + "@babel/helper-module-transforms" "^7.10.5" + "@babel/helper-plugin-utils" "^7.10.4" + babel-plugin-dynamic-import-node "^2.3.3" + +"@babel/plugin-transform-modules-umd@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.4.tgz#9a8481fe81b824654b3a0b65da3df89f3d21839e" + integrity sha512-mohW5q3uAEt8T45YT7Qc5ws6mWgJAaL/8BfWD9Dodo1A3RKWli8wTS+WiQ/knF+tXlPirW/1/MqzzGfCExKECA== + dependencies: + "@babel/helper-module-transforms" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.4.tgz#78b4d978810b6f3bcf03f9e318f2fc0ed41aecb6" + integrity sha512-V6LuOnD31kTkxQPhKiVYzYC/Jgdq53irJC/xBSmqcNcqFGV+PER4l6rU5SH2Vl7bH9mLDHcc0+l9HUOe4RNGKA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.10.4" + +"@babel/plugin-transform-new-target@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.4.tgz#9097d753cb7b024cb7381a3b2e52e9513a9c6888" + integrity sha512-YXwWUDAH/J6dlfwqlWsztI2Puz1NtUAubXhOPLQ5gjR/qmQ5U96DY4FQO8At33JN4XPBhrjB8I4eMmLROjjLjw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-object-super@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.4.tgz#d7146c4d139433e7a6526f888c667e314a093894" + integrity sha512-5iTw0JkdRdJvr7sY0vHqTpnruUpTea32JHmq/atIWqsnNussbRzjEDyWep8UNztt1B5IusBYg8Irb0bLbiEBCQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-replace-supers" "^7.10.4" + +"@babel/plugin-transform-parameters@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.5.tgz#59d339d58d0b1950435f4043e74e2510005e2c4a" + integrity sha512-xPHwUj5RdFV8l1wuYiu5S9fqWGM2DrYc24TMvUiRrPVm+SM3XeqU9BcokQX/kEUe+p2RBwy+yoiR1w/Blq6ubw== + dependencies: + "@babel/helper-get-function-arity" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-property-literals@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.4.tgz#f6fe54b6590352298785b83edd815d214c42e3c0" + integrity sha512-ofsAcKiUxQ8TY4sScgsGeR2vJIsfrzqvFb9GvJ5UdXDzl+MyYCaBj/FGzXuv7qE0aJcjWMILny1epqelnFlz8g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-regenerator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.4.tgz#2015e59d839074e76838de2159db421966fd8b63" + integrity sha512-3thAHwtor39A7C04XucbMg17RcZ3Qppfxr22wYzZNcVIkPHfpM9J0SO8zuCV6SZa265kxBJSrfKTvDCYqBFXGw== + dependencies: + regenerator-transform "^0.14.2" + +"@babel/plugin-transform-reserved-words@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.4.tgz#8f2682bcdcef9ed327e1b0861585d7013f8a54dd" + integrity sha512-hGsw1O6Rew1fkFbDImZIEqA8GoidwTAilwCyWqLBM9f+e/u/sQMQu7uX6dyokfOayRuuVfKOW4O7HvaBWM+JlQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-runtime@7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.11.0.tgz#e27f78eb36f19448636e05c33c90fd9ad9b8bccf" + integrity sha512-LFEsP+t3wkYBlis8w6/kmnd6Kb1dxTd+wGJ8MlxTGzQo//ehtqlVL4S9DNUa53+dtPSQobN2CXx4d81FqC58cw== + dependencies: + "@babel/helper-module-imports" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + resolve "^1.8.1" + semver "^5.5.1" + +"@babel/plugin-transform-shorthand-properties@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.4.tgz#9fd25ec5cdd555bb7f473e5e6ee1c971eede4dd6" + integrity sha512-AC2K/t7o07KeTIxMoHneyX90v3zkm5cjHJEokrPEAGEy3UCp8sLKfnfOIGdZ194fyN4wfX/zZUWT9trJZ0qc+Q== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-spread@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.11.0.tgz#fa84d300f5e4f57752fe41a6d1b3c554f13f17cc" + integrity sha512-UwQYGOqIdQJe4aWNyS7noqAnN2VbaczPLiEtln+zPowRNlD+79w3oi2TWfYe0eZgd+gjZCbsydN7lzWysDt+gw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0" + +"@babel/plugin-transform-sticky-regex@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.4.tgz#8f3889ee8657581130a29d9cc91d7c73b7c4a28d" + integrity sha512-Ddy3QZfIbEV0VYcVtFDCjeE4xwVTJWTmUtorAJkn6u/92Z/nWJNV+mILyqHKrUxXYKA2EoCilgoPePymKL4DvQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-regex" "^7.10.4" + +"@babel/plugin-transform-template-literals@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.5.tgz#78bc5d626a6642db3312d9d0f001f5e7639fde8c" + integrity sha512-V/lnPGIb+KT12OQikDvgSuesRX14ck5FfJXt6+tXhdkJ+Vsd0lDCVtF6jcB4rNClYFzaB2jusZ+lNISDk2mMMw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-typeof-symbol@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.4.tgz#9509f1a7eec31c4edbffe137c16cc33ff0bc5bfc" + integrity sha512-QqNgYwuuW0y0H+kUE/GWSR45t/ccRhe14Fs/4ZRouNNQsyd4o3PG4OtHiIrepbM2WKUBDAXKCAK/Lk4VhzTaGA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-unicode-escapes@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.4.tgz#feae523391c7651ddac115dae0a9d06857892007" + integrity sha512-y5XJ9waMti2J+e7ij20e+aH+fho7Wb7W8rNuu72aKRwCHFqQdhkdU2lo3uZ9tQuboEJcUFayXdARhcxLQ3+6Fg== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-unicode-regex@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.4.tgz#e56d71f9282fac6db09c82742055576d5e6d80a8" + integrity sha512-wNfsc4s8N2qnIwpO/WP2ZiSyjfpTamT2C9V9FDH/Ljub9zw6P3SjkXcFmc0RQUt96k2fmIvtla2MMjgTwIAC+A== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/preset-env@7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.11.0.tgz#860ee38f2ce17ad60480c2021ba9689393efb796" + integrity sha512-2u1/k7rG/gTh02dylX2kL3S0IJNF+J6bfDSp4DI2Ma8QN6Y9x9pmAax59fsCk6QUQG0yqH47yJWA+u1I1LccAg== + dependencies: + "@babel/compat-data" "^7.11.0" + "@babel/helper-compilation-targets" "^7.10.4" + "@babel/helper-module-imports" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-proposal-async-generator-functions" "^7.10.4" + "@babel/plugin-proposal-class-properties" "^7.10.4" + "@babel/plugin-proposal-dynamic-import" "^7.10.4" + "@babel/plugin-proposal-export-namespace-from" "^7.10.4" + "@babel/plugin-proposal-json-strings" "^7.10.4" + "@babel/plugin-proposal-logical-assignment-operators" "^7.11.0" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.10.4" + "@babel/plugin-proposal-numeric-separator" "^7.10.4" + "@babel/plugin-proposal-object-rest-spread" "^7.11.0" + "@babel/plugin-proposal-optional-catch-binding" "^7.10.4" + "@babel/plugin-proposal-optional-chaining" "^7.11.0" + "@babel/plugin-proposal-private-methods" "^7.10.4" + "@babel/plugin-proposal-unicode-property-regex" "^7.10.4" + "@babel/plugin-syntax-async-generators" "^7.8.0" + "@babel/plugin-syntax-class-properties" "^7.10.4" + "@babel/plugin-syntax-dynamic-import" "^7.8.0" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.0" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.0" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" + "@babel/plugin-syntax-optional-chaining" "^7.8.0" + "@babel/plugin-syntax-top-level-await" "^7.10.4" + "@babel/plugin-transform-arrow-functions" "^7.10.4" + "@babel/plugin-transform-async-to-generator" "^7.10.4" + "@babel/plugin-transform-block-scoped-functions" "^7.10.4" + "@babel/plugin-transform-block-scoping" "^7.10.4" + "@babel/plugin-transform-classes" "^7.10.4" + "@babel/plugin-transform-computed-properties" "^7.10.4" + "@babel/plugin-transform-destructuring" "^7.10.4" + "@babel/plugin-transform-dotall-regex" "^7.10.4" + "@babel/plugin-transform-duplicate-keys" "^7.10.4" + "@babel/plugin-transform-exponentiation-operator" "^7.10.4" + "@babel/plugin-transform-for-of" "^7.10.4" + "@babel/plugin-transform-function-name" "^7.10.4" + "@babel/plugin-transform-literals" "^7.10.4" + "@babel/plugin-transform-member-expression-literals" "^7.10.4" + "@babel/plugin-transform-modules-amd" "^7.10.4" + "@babel/plugin-transform-modules-commonjs" "^7.10.4" + "@babel/plugin-transform-modules-systemjs" "^7.10.4" + "@babel/plugin-transform-modules-umd" "^7.10.4" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.10.4" + "@babel/plugin-transform-new-target" "^7.10.4" + "@babel/plugin-transform-object-super" "^7.10.4" + "@babel/plugin-transform-parameters" "^7.10.4" + "@babel/plugin-transform-property-literals" "^7.10.4" + "@babel/plugin-transform-regenerator" "^7.10.4" + "@babel/plugin-transform-reserved-words" "^7.10.4" + "@babel/plugin-transform-shorthand-properties" "^7.10.4" + "@babel/plugin-transform-spread" "^7.11.0" + "@babel/plugin-transform-sticky-regex" "^7.10.4" + "@babel/plugin-transform-template-literals" "^7.10.4" + "@babel/plugin-transform-typeof-symbol" "^7.10.4" + "@babel/plugin-transform-unicode-escapes" "^7.10.4" + "@babel/plugin-transform-unicode-regex" "^7.10.4" + "@babel/preset-modules" "^0.1.3" + "@babel/types" "^7.11.0" + browserslist "^4.12.0" + core-js-compat "^3.6.2" + invariant "^2.2.2" + levenary "^1.1.1" + semver "^5.5.0" + +"@babel/preset-modules@^0.1.3": + version "0.1.4" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.4.tgz#362f2b68c662842970fdb5e254ffc8fc1c2e415e" + integrity sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" + "@babel/plugin-transform-dotall-regex" "^7.4.4" + "@babel/types" "^7.4.4" + esutils "^2.0.2" + +"@babel/runtime@7.11.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": + version "7.11.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" + integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== + dependencies: + regenerator-runtime "^0.13.4" + +"@babel/template@7.10.4", "@babel/template@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" + integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/parser" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/traverse@^7.10.4", "@babel/traverse@^7.11.0", "@babel/traverse@^7.11.5": + version "7.11.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.5.tgz#be777b93b518eb6d76ee2e1ea1d143daa11e61c3" + integrity sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.11.5" + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.11.0" + "@babel/parser" "^7.11.5" + "@babel/types" "^7.11.5" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.19" + +"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.11.5", "@babel/types@^7.4.4": + version "7.11.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.5.tgz#d9de577d01252d77c6800cee039ee64faf75662d" + integrity sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + lodash "^4.17.19" + to-fast-properties "^2.0.0" + +"@date-io/core@1.x": + version "1.3.13" + resolved "https://registry.yarnpkg.com/@date-io/core/-/core-1.3.13.tgz#90c71da493f20204b7a972929cc5c482d078b3fa" + integrity sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA== + +"@date-io/core@^2.10.6": + version "2.10.6" + resolved "https://registry.yarnpkg.com/@date-io/core/-/core-2.10.6.tgz#1a6e671b590a08af8bd0784f3a93670e5d2d5bd7" + integrity sha512-MGYt4GEB/4ZMdSbj6FS7/gPBvuhHUwnn5O6t8PlkSqGF1310qxypVyK4CZg5RQgev25L3R5eLVdNTyYrJOL8Rw== + +"@date-io/date-fns@^2.6.1": + version "2.10.6" + resolved "https://registry.yarnpkg.com/@date-io/date-fns/-/date-fns-2.10.6.tgz#d0afee6452d80112017f42af4912ba22d95b11b6" + integrity sha512-jUiIbs4VCmACy2Ml2xu3tqf0AUSZu4qQ3cRz8SoG4YPzeg1fqII8y/gTa7GJkXiH0bUKUWaf/G2dfJa9tUnmJA== + dependencies: + "@date-io/core" "^2.10.6" + +"@emotion/hash@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" + integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== + +"@flowjs/flow.js@^2.14.1": + version "2.14.1" + resolved "https://registry.yarnpkg.com/@flowjs/flow.js/-/flow.js-2.14.1.tgz#267d9f9d0958f32267ea5815c2a7cc09b9219304" + integrity sha512-99DWlPnksOOS8uHfo+bhSjvs8d2MfLTB/22JBDC2ONwz/OCdP+gL/iiM4puMSTE2wH4A2/+J0eMc7pKwusXunw== + +"@flowjs/ngx-flow@^0.4.4": + version "0.4.4" + resolved "https://registry.yarnpkg.com/@flowjs/ngx-flow/-/ngx-flow-0.4.4.tgz#e2338f17a212fe4b6d89e46b4eed93dddab683fe" + integrity sha512-eZWFexubeIpNAsCB4jXFFiiM+aiu+lFzisO2cqq+eJTIQwpDqtUHgvTNfkB/9pdLKyCVNe3otyWRLId1zochvw== + dependencies: + "@types/flowjs" "2.13.3" + tslib "^1.9.0" + +"@istanbuljs/schema@^0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" + integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== + +"@jsdevtools/coverage-istanbul-loader@3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@jsdevtools/coverage-istanbul-loader/-/coverage-istanbul-loader-3.0.5.tgz#2a4bc65d0271df8d4435982db4af35d81754ee26" + integrity sha512-EUCPEkaRPvmHjWAAZkWMT7JDzpw7FKB00WTISaiXsbNOd5hCHg77XLA8sLYLFDo1zepYLo2w7GstN8YBqRXZfA== + dependencies: + convert-source-map "^1.7.0" + istanbul-lib-instrument "^4.0.3" + loader-utils "^2.0.0" + merge-source-map "^1.1.0" + schema-utils "^2.7.0" + +"@juggle/resize-observer@^3.1.3": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.2.0.tgz#5e0b448d27fe3091bae6216456512c5904d05661" + integrity sha512-fsLxt0CHx2HCV9EL8lDoVkwHffsA0snUpddYjdLyXcG5E41xaamn9ZyQqOE9TUJdrRlH8/hjIf+UdOdDeKCUgg== + +"@mat-datetimepicker/core@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@mat-datetimepicker/core/-/core-5.1.0.tgz#62f3648ca316c621d12166c8db562e1da8d8bcae" + integrity sha512-behTHJFcgKOyC3fAViwVryQpQAG3Pz4X4GnTJuCA0UiA3iFvkafZghldaJF7QL3KCE5DpWDhMvqOp8RR1sDh+w== + dependencies: + tslib "^2.0.0" + +"@material-ui/core@^4.11.0": + version "4.11.0" + resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-4.11.0.tgz#b69b26e4553c9e53f2bfaf1053e216a0af9be15a" + integrity sha512-bYo9uIub8wGhZySHqLQ833zi4ZML+XCBE1XwJ8EuUVSpTWWG57Pm+YugQToJNFsEyiKFhPh8DPD0bgupz8n01g== + dependencies: + "@babel/runtime" "^7.4.4" + "@material-ui/styles" "^4.10.0" + "@material-ui/system" "^4.9.14" + "@material-ui/types" "^5.1.0" + "@material-ui/utils" "^4.10.2" + "@types/react-transition-group" "^4.2.0" + clsx "^1.0.4" + hoist-non-react-statics "^3.3.2" + popper.js "1.16.1-lts" + prop-types "^15.7.2" + react-is "^16.8.0" + react-transition-group "^4.4.0" + +"@material-ui/icons@^4.9.1": + version "4.9.1" + resolved "https://registry.yarnpkg.com/@material-ui/icons/-/icons-4.9.1.tgz#fdeadf8cb3d89208945b33dbc50c7c616d0bd665" + integrity sha512-GBitL3oBWO0hzBhvA9KxqcowRUsA0qzwKkURyC8nppnC3fw54KPKZ+d4V1Eeg/UnDRSzDaI9nGCdel/eh9AQMg== + dependencies: + "@babel/runtime" "^7.4.4" + +"@material-ui/pickers@^3.2.10": + version "3.2.10" + resolved "https://registry.yarnpkg.com/@material-ui/pickers/-/pickers-3.2.10.tgz#19df024895876eb0ec7cd239bbaea595f703f0ae" + integrity sha512-B8G6Obn5S3RCl7hwahkQj9sKUapwXWFjiaz/Bsw1fhYFdNMnDUolRiWQSoKPb1/oKe37Dtfszoywi1Ynbo3y8w== + dependencies: + "@babel/runtime" "^7.6.0" + "@date-io/core" "1.x" + "@types/styled-jsx" "^2.2.8" + clsx "^1.0.2" + react-transition-group "^4.0.0" + rifm "^0.7.0" + +"@material-ui/styles@^4.10.0": + version "4.10.0" + resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.10.0.tgz#2406dc23aa358217aa8cc772e6237bd7f0544071" + integrity sha512-XPwiVTpd3rlnbfrgtEJ1eJJdFCXZkHxy8TrdieaTvwxNYj42VnnCyFzxYeNW9Lhj4V1oD8YtQ6S5Gie7bZDf7Q== + dependencies: + "@babel/runtime" "^7.4.4" + "@emotion/hash" "^0.8.0" + "@material-ui/types" "^5.1.0" + "@material-ui/utils" "^4.9.6" + clsx "^1.0.4" + csstype "^2.5.2" + hoist-non-react-statics "^3.3.2" + jss "^10.0.3" + jss-plugin-camel-case "^10.0.3" + jss-plugin-default-unit "^10.0.3" + jss-plugin-global "^10.0.3" + jss-plugin-nested "^10.0.3" + jss-plugin-props-sort "^10.0.3" + jss-plugin-rule-value-function "^10.0.3" + jss-plugin-vendor-prefixer "^10.0.3" + prop-types "^15.7.2" + +"@material-ui/system@^4.9.14": + version "4.9.14" + resolved "https://registry.yarnpkg.com/@material-ui/system/-/system-4.9.14.tgz#4b00c48b569340cefb2036d0596b93ac6c587a5f" + integrity sha512-oQbaqfSnNlEkXEziDcJDDIy8pbvwUmZXWNqlmIwDqr/ZdCK8FuV3f4nxikUh7hvClKV2gnQ9djh5CZFTHkZj3w== + dependencies: + "@babel/runtime" "^7.4.4" + "@material-ui/utils" "^4.9.6" + csstype "^2.5.2" + prop-types "^15.7.2" + +"@material-ui/types@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@material-ui/types/-/types-5.1.0.tgz#efa1c7a0b0eaa4c7c87ac0390445f0f88b0d88f2" + integrity sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A== + +"@material-ui/utils@^4.10.2", "@material-ui/utils@^4.9.6": + version "4.10.2" + resolved "https://registry.yarnpkg.com/@material-ui/utils/-/utils-4.10.2.tgz#3fd5470ca61b7341f1e0468ac8f29a70bf6df321" + integrity sha512-eg29v74P7W5r6a4tWWDAAfZldXIzfyO1am2fIsC39hdUUHm/33k6pGOKPbgDjg/U/4ifmgAePy/1OjkKN6rFRw== + dependencies: + "@babel/runtime" "^7.4.4" + prop-types "^15.7.2" + react-is "^16.8.0" + +"@ngrx/effects@^10.0.1": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@ngrx/effects/-/effects-10.0.1.tgz#66011516735dd59955910a9790d33fbcb8d50400" + integrity sha512-pw0hRQNlyBBRHH1NRWl3TF+RtEAS4XOSnoTHPtQ84Ib/bEribvexsdEq3k6yLWvR3tLTudb5J6SYwYawcM6omA== + dependencies: + tslib "^2.0.0" + +"@ngrx/store-devtools@^10.0.1": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@ngrx/store-devtools/-/store-devtools-10.0.1.tgz#6f3529e7708870e44cf407733bf06389ee657c26" + integrity sha512-kwgF1yjjVn0FER+AG83OLCYSMuX4/E3L+DN4doSoZs4BNO9FdkYIIA4ul1nXT5d6SLiFFTmlufmbgc6HCF3pjQ== + dependencies: + tslib "^2.0.0" + +"@ngrx/store@^10.0.1": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@ngrx/store/-/store-10.0.1.tgz#74c3bb383cc507f927ba63710cc6622f2f2859db" + integrity sha512-ZbPvhp/tRYnS3jZ28mDOX2LH3jfySXT0uv8ffIboM/o9QxBGHpAJyBct2zkpy4duYBc3i/sIbRn+CEpAjLXjHw== + dependencies: + tslib "^2.0.0" + +"@ngtools/webpack@10.1.5": + version "10.1.5" + resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-10.1.5.tgz#08fe0c8cc9defb156f3b01e9f8a32994ed66fa0b" + integrity sha512-oebpaFwYk42DCYL3CTVeDUAhh6OrqkZxLJypVuRtb1iNZltwEQKRykoYCr4yQuNByzn4+i21CvlSuBwm0afXHg== + dependencies: + "@angular-devkit/core" "10.1.5" + enhanced-resolve "4.3.0" + webpack-sources "1.4.3" + +"@ngx-translate/core@^13.0.0": + version "13.0.0" + resolved "https://registry.yarnpkg.com/@ngx-translate/core/-/core-13.0.0.tgz#60547cb8a0845a2a0abfde6b0bf5ec6516a63fd6" + integrity sha512-+tzEp8wlqEnw0Gc7jtVRAJ6RteUjXw6JJR4O65KlnxOmJrCGPI0xjV/lKRnQeU0w4i96PQs/jtpL921Wrb7PWg== + dependencies: + tslib "^2.0.0" + +"@ngx-translate/http-loader@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@ngx-translate/http-loader/-/http-loader-6.0.0.tgz#041393ab5753f50ecf64262d624703046b8c7570" + integrity sha512-LCekn6qCbeXWlhESCxU1rAbZz33WzDG0lI7Ig0pYC1o5YxJWrkU9y3Y4tNi+jakQ7R6YhTR2D3ox6APxDtA0wA== + dependencies: + tslib "^2.0.0" + +"@nodelib/fs.scandir@2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" + integrity sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw== + dependencies: + "@nodelib/fs.stat" "2.0.3" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3" + integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976" + integrity sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ== + dependencies: + "@nodelib/fs.scandir" "2.1.3" + fastq "^1.6.0" + +"@npmcli/move-file@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.0.1.tgz#de103070dac0f48ce49cf6693c23af59c0f70464" + integrity sha512-Uv6h1sT+0DrblvIrolFtbvM1FgWm+/sy4B3pvLp67Zys+thcukzS5ekn7HsZFGpWP4Q3fYJCljbWQE/XivMRLw== + dependencies: + mkdirp "^1.0.4" + +"@schematics/angular@10.1.5": + version "10.1.5" + resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-10.1.5.tgz#964a50c070920f9bb42e0f26cedc9c86c8f48241" + integrity sha512-3VRcMB9WpjcMvlZ1y+78WGuZ4Ehp9pGw/T+zAR1VG9/16XHDQyfObsMuaU2EnEoufiHbTe3UpvVpYOu6tOCJrA== + dependencies: + "@angular-devkit/core" "10.1.5" + "@angular-devkit/schematics" "10.1.5" + jsonc-parser "2.3.0" + +"@schematics/update@0.1001.5": + version "0.1001.5" + resolved "https://registry.yarnpkg.com/@schematics/update/-/update-0.1001.5.tgz#367ea5696fea300a81469994632985f30b41b40b" + integrity sha512-DSomJ5IMs/5HUPx0RdPYubPWXh7kToxXUZbJywe0Q+TWTd+1xFfg8++O1DG4iW7E/Boqojx5VenAOzWY9jDWjA== + dependencies: + "@angular-devkit/core" "10.1.5" + "@angular-devkit/schematics" "10.1.5" + "@yarnpkg/lockfile" "1.1.0" + ini "1.3.5" + npm-package-arg "^8.0.0" + pacote "9.5.12" + semver "7.3.2" + semver-intersect "1.4.0" + +"@types/canvas-gauges@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@types/canvas-gauges/-/canvas-gauges-2.1.2.tgz#fb9ece324cb15ae137791ad21eb2db70e11a7210" + integrity sha512-oWCq0XjsTBXPtMKXoW23ORbMWguC2Fa8o5NiZVYiUoQMMrpNLKj1E+LDznlMpcib3iyWVIy+TEpc/ea6LMbW3Q== + +"@types/flot@^0.0.31": + version "0.0.31" + resolved "https://registry.yarnpkg.com/@types/flot/-/flot-0.0.31.tgz#0daca37c6c855b69a0a7e2e37dd0f84b3db8c8c1" + integrity sha512-X+RcMQCqPlQo8zPT6cUFTd/PoYBShMQlHUeOXf05jWlfYnvLuRmluB9z+2EsOKFgUzqzZve5brx+gnFxBaHEUw== + dependencies: + "@types/jquery" "*" + +"@types/flowjs@2.13.3": + version "2.13.3" + resolved "https://registry.yarnpkg.com/@types/flowjs/-/flowjs-2.13.3.tgz#4f1ba77d9259f4be83ecaa985db96fa758b2fd22" + integrity sha512-VeWuL+Whk6lUSWX/g0LzLNyZywyTB5wZ2L6mPvD8/u5pgLF2HwyV7nZ1UArOifalJ5UE1CcJbPLKS+jc5+Z2ig== + +"@types/geojson@*": + version "7946.0.7" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.7.tgz#c8fa532b60a0042219cdf173ca21a975ef0666ad" + integrity sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ== + +"@types/glob@^7.1.1": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" + integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w== + dependencies: + "@types/minimatch" "*" + "@types/node" "*" + +"@types/jasmine@*", "@types/jasmine@^3.5.12": + version "3.5.14" + resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.5.14.tgz#f41a14e8ffa939062a71cf9722e5ee7d4e1f94af" + integrity sha512-Fkgk536sHPqcOtd+Ow+WiUNuk0TSo/BntKkF8wSvcd6M2FvPjeXcUE6Oz/bwDZiUZEaXLslAgw00Q94Pnx6T4w== + +"@types/jasminewd2@^2.0.8": + version "2.0.8" + resolved "https://registry.yarnpkg.com/@types/jasminewd2/-/jasminewd2-2.0.8.tgz#67afe5098d5ef2386073a7b7384b69a840dfe93b" + integrity sha512-d9p31r7Nxk0ZH0U39PTH0hiDlJ+qNVGjlt1ucOoTUptxb2v+Y5VMnsxfwN+i3hK4yQnqBi3FMmoMFcd1JHDxdg== + dependencies: + "@types/jasmine" "*" + +"@types/jquery@*", "@types/jquery@^3.3.29", "@types/jquery@^3.5.2": + version "3.5.2" + resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.2.tgz#e17c1756ecf7bbb431766c6761674a5d1de16579" + integrity sha512-+MFOdKF5Zr41t3y2wfzJvK1PrUK0KtPLAFwYownp/0nCoMIANDDu5aFSpWfb8S0ZajCSNeaBnMrBGxksXK5yeg== + dependencies: + "@types/sizzle" "*" + +"@types/js-beautify@^1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@types/js-beautify/-/js-beautify-1.11.0.tgz#f1311fe280a5f83b1e6517cab1116aad63465cd0" + integrity sha512-RqTqKEenGBSa/vS3qHQuhudWE1d1NbollRDoArx85k1vUg4rugc+odFQW13c6O5re7hjf6zaRnWz9St/j8h15w== + +"@types/json-schema@^7.0.5": + version "7.0.6" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0" + integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw== + +"@types/jstree@^3.3.40": + version "3.3.40" + resolved "https://registry.yarnpkg.com/@types/jstree/-/jstree-3.3.40.tgz#835737a262ea2572df9ffafc68c08633f4554aa4" + integrity sha512-+6mdAX+vaj962NSd1nnzSLBWD2obUQf5+1yxR+4/g+abpEIQFsI+CcqP4l+cS6H9P07sTONMbR3Z09bPpDzkNg== + dependencies: + "@types/jquery" "*" + +"@types/leaflet-polylinedecorator@^1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@types/leaflet-polylinedecorator/-/leaflet-polylinedecorator-1.6.0.tgz#1572131ffedb3154c6e18e682d2fb700e203af19" + integrity sha512-Z2BXZDjKEqHclwrAmhYdF1RwyFfa/NFxsoF79sitzaj5D/4YWHp/zDRcUZar5cQFKRgK66AYEIF7nKVuMzUGdw== + dependencies: + "@types/leaflet" "*" + +"@types/leaflet.markercluster@^1.4.3": + version "1.4.3" + resolved "https://registry.yarnpkg.com/@types/leaflet.markercluster/-/leaflet.markercluster-1.4.3.tgz#5824d9be3dd5c0864a22a1fca36664550a96a76c" + integrity sha512-X/b/Enz84PzmcA9z7pxsHEBEUNghmvznEBcRQeuxyYL/QU6jAR7LIb/ot03ATNPO56wSFzbCnsOf7yJ+7FzS1Q== + dependencies: + "@types/leaflet" "*" + +"@types/leaflet@*", "@types/leaflet@^1.5.17": + version "1.5.17" + resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.5.17.tgz#b2153dc12c344e6896a93ffc6b61ac79da251e5b" + integrity sha512-2XYq9k6kNjhNI7PaTz8Rdxcc8Vzwu97OaS9CtcrTxnTSxFUGwjlGjTDvhTLJU+JRSfZ4lBwGcl0SjZHALdVr6g== + dependencies: + "@types/geojson" "*" + +"@types/lodash@^4.14.161": + version "4.14.161" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.161.tgz#a21ca0777dabc6e4f44f3d07f37b765f54188b18" + integrity sha512-EP6O3Jkr7bXvZZSZYlsgt5DIjiGr0dXP1/jVEwVLTFgg0d+3lWVQkRavYVQszV7dYUwvg0B8R0MBDpcmXg7XIA== + +"@types/minimatch@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" + integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== + +"@types/moment-timezone@^0.5.30": + version "0.5.30" + resolved "https://registry.yarnpkg.com/@types/moment-timezone/-/moment-timezone-0.5.30.tgz#340ed45fe3e715f4a011f5cfceb7cb52aad46fc7" + integrity sha512-aDVfCsjYnAQaV/E9Qc24C5Njx1CoDjXsEgkxtp9NyXDpYu4CCbmclb6QhWloS9UTU/8YROUEEdEkWI0D7DxnKg== + dependencies: + moment-timezone "*" + +"@types/mousetrap@1.6.3": + version "1.6.3" + resolved "https://registry.yarnpkg.com/@types/mousetrap/-/mousetrap-1.6.3.tgz#3159a01a2b21c9155a3d8f85588885d725dc987d" + integrity sha512-13gmo3M2qVvjQrWNseqM3+cR6S2Ss3grbR2NZltgMq94wOwqJYQdgn8qzwDshzgXqMlSUtyPZjysImmktu22ew== + +"@types/mousetrap@^1.6.0": + version "1.6.4" + resolved "https://registry.yarnpkg.com/@types/mousetrap/-/mousetrap-1.6.4.tgz#32503197fca4168b10bf251c1d677a9b5b1c2415" + integrity sha512-+Y900DGhe+f+4lRwHm9krsKfsiXcbdOhzTsLbytU4MiG8wE9xOw7CFKtgYKfqEAcUdWEGZRyuTxoyFl2Gx6Rdg== + +"@types/node@*": + version "14.11.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.5.tgz#fecad41c041cae7f2404ad4b2d0742fdb628b305" + integrity sha512-jVFzDV6NTbrLMxm4xDSIW/gKnk8rQLF9wAzLWIOg+5nU6ACrIMndeBdXci0FGtqJbP9tQvm6V39eshc96TO2wQ== + +"@types/prop-types@*": + version "15.7.3" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" + integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== + +"@types/q@^0.0.32": + version "0.0.32" + resolved "https://registry.yarnpkg.com/@types/q/-/q-0.0.32.tgz#bd284e57c84f1325da702babfc82a5328190c0c5" + integrity sha1-vShOV8hPEyXacCur/IKlMoGQwMU= + +"@types/q@^1.5.1": + version "1.5.4" + resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24" + integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug== + +"@types/raphael@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@types/raphael/-/raphael-2.3.0.tgz#f1f3ef3be33e357b8f01dd1e89b0067a3197c8fd" + integrity sha512-0clAhN2xOpCylsfHl8uMfBqe+XImaYFye6or5fucR+i8uC6ybZiMDlQtQ7Cx7yr8u2DLrvTnZKvUGKE+bodl1g== + +"@types/react-dom@^16.9.8": + version "16.9.8" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.8.tgz#fe4c1e11dfc67155733dfa6aa65108b4971cb423" + integrity sha512-ykkPQ+5nFknnlU6lDd947WbQ6TE3NNzbQAkInC2EKY1qeYdTKp7onFusmYZb+ityzx2YviqT6BXSu+LyWWJwcA== + dependencies: + "@types/react" "*" + +"@types/react-transition-group@^4.2.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.0.tgz#882839db465df1320e4753e6e9f70ca7e9b4d46d" + integrity sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w== + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@^16.9.51": + version "16.9.51" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.51.tgz#f8aa51ffa9996f1387f63686696d9b59713d2b60" + integrity sha512-lQa12IyO+DMlnSZ3+AGHRUiUcpK47aakMMoBG8f7HGxJT8Yfe+WE128HIXaHOHVPReAW0oDS3KAI0JI2DDe1PQ== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + +"@types/selenium-webdriver@^3.0.0": + version "3.0.17" + resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-3.0.17.tgz#50bea0c3c2acc31c959c5b1e747798b3b3d06d4b" + integrity sha512-tGomyEuzSC1H28y2zlW6XPCaDaXFaD6soTdb4GNdmte2qfHtrKqhy0ZFs4r/1hpazCfEZqeTSRLvSasmEx89uw== + +"@types/sizzle@*": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" + integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg== + +"@types/source-list-map@*": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" + integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA== + +"@types/styled-jsx@^2.2.8": + version "2.2.8" + resolved "https://registry.yarnpkg.com/@types/styled-jsx/-/styled-jsx-2.2.8.tgz#b50d13d8a3c34036282d65194554cf186bab7234" + integrity sha512-Yjye9VwMdYeXfS71ihueWRSxrruuXTwKCbzue4+5b2rjnQ//AtyM7myZ1BEhNhBQ/nL/RE7bdToUoLln2miKvg== + dependencies: + "@types/react" "*" + +"@types/tinycolor2@^1.4.2": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.2.tgz#721ca5c5d1a2988b4a886e35c2ffc5735b6afbdf" + integrity sha512-PeHg/AtdW6aaIO2a+98Xj7rWY4KC1E6yOy7AFknJQ7VXUGNrMlyxDFxJo7HqLtjQms/ZhhQX52mLVW/EX3JGOw== + +"@types/tooltipster@^0.0.30": + version "0.0.30" + resolved "https://registry.yarnpkg.com/@types/tooltipster/-/tooltipster-0.0.30.tgz#10deda04d1ce52e0fefc18079ae91548663e2400" + integrity sha512-JqgcjpkDOMjsiVg3tqMIDgxnealN9/+DY6+z9OMeSd4fRovHINr/tHKtBJXDHI2DKh+ygyqMHsPTct+flNS+bw== + dependencies: + "@types/jquery" "*" + +"@types/webpack-sources@^0.1.5": + version "0.1.8" + resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.8.tgz#078d75410435993ec8a0a2855e88706f3f751f81" + integrity sha512-JHB2/xZlXOjzjBB6fMOpH1eQAfsrpqVVIbneE0Rok16WXwFaznaI5vfg75U5WgGJm7V9W1c4xeRQDjX/zwvghA== + dependencies: + "@types/node" "*" + "@types/source-list-map" "*" + source-map "^0.6.1" + +"@webassemblyjs/ast@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" + integrity sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA== + dependencies: + "@webassemblyjs/helper-module-context" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/wast-parser" "1.9.0" + +"@webassemblyjs/floating-point-hex-parser@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz#3c3d3b271bddfc84deb00f71344438311d52ffb4" + integrity sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA== + +"@webassemblyjs/helper-api-error@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz#203f676e333b96c9da2eeab3ccef33c45928b6a2" + integrity sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw== + +"@webassemblyjs/helper-buffer@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz#a1442d269c5feb23fcbc9ef759dac3547f29de00" + integrity sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA== + +"@webassemblyjs/helper-code-frame@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz#647f8892cd2043a82ac0c8c5e75c36f1d9159f27" + integrity sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA== + dependencies: + "@webassemblyjs/wast-printer" "1.9.0" + +"@webassemblyjs/helper-fsm@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz#c05256b71244214671f4b08ec108ad63b70eddb8" + integrity sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw== + +"@webassemblyjs/helper-module-context@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz#25d8884b76839871a08a6c6f806c3979ef712f07" + integrity sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g== + dependencies: + "@webassemblyjs/ast" "1.9.0" + +"@webassemblyjs/helper-wasm-bytecode@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz#4fed8beac9b8c14f8c58b70d124d549dd1fe5790" + integrity sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw== + +"@webassemblyjs/helper-wasm-section@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz#5a4138d5a6292ba18b04c5ae49717e4167965346" + integrity sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-buffer" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/wasm-gen" "1.9.0" + +"@webassemblyjs/ieee754@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz#15c7a0fbaae83fb26143bbacf6d6df1702ad39e4" + integrity sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.9.0.tgz#f19ca0b76a6dc55623a09cffa769e838fa1e1c95" + integrity sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.9.0.tgz#04d33b636f78e6a6813227e82402f7637b6229ab" + integrity sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w== + +"@webassemblyjs/wasm-edit@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz#3fe6d79d3f0f922183aa86002c42dd256cfee9cf" + integrity sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-buffer" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/helper-wasm-section" "1.9.0" + "@webassemblyjs/wasm-gen" "1.9.0" + "@webassemblyjs/wasm-opt" "1.9.0" + "@webassemblyjs/wasm-parser" "1.9.0" + "@webassemblyjs/wast-printer" "1.9.0" + +"@webassemblyjs/wasm-gen@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz#50bc70ec68ded8e2763b01a1418bf43491a7a49c" + integrity sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/ieee754" "1.9.0" + "@webassemblyjs/leb128" "1.9.0" + "@webassemblyjs/utf8" "1.9.0" + +"@webassemblyjs/wasm-opt@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz#2211181e5b31326443cc8112eb9f0b9028721a61" + integrity sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-buffer" "1.9.0" + "@webassemblyjs/wasm-gen" "1.9.0" + "@webassemblyjs/wasm-parser" "1.9.0" + +"@webassemblyjs/wasm-parser@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz#9d48e44826df4a6598294aa6c87469d642fff65e" + integrity sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-api-error" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/ieee754" "1.9.0" + "@webassemblyjs/leb128" "1.9.0" + "@webassemblyjs/utf8" "1.9.0" + +"@webassemblyjs/wast-parser@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz#3031115d79ac5bd261556cecc3fa90a3ef451914" + integrity sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/floating-point-hex-parser" "1.9.0" + "@webassemblyjs/helper-api-error" "1.9.0" + "@webassemblyjs/helper-code-frame" "1.9.0" + "@webassemblyjs/helper-fsm" "1.9.0" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/wast-printer@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz#4935d54c85fef637b00ce9f52377451d00d47899" + integrity sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/wast-parser" "1.9.0" + "@xtuc/long" "4.2.2" + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +"@yarnpkg/lockfile@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" + integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== + +JSONStream@^1.3.4: + version "1.3.5" + resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" + integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== + dependencies: + jsonparse "^1.2.0" + through ">=2.2.7 <3" + +abab@^2.0.3: + version "2.0.5" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" + integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + +ace-builds@^1.4.12, ace-builds@^1.4.6: + version "1.4.12" + resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.4.12.tgz#888efa386e36f4345f40b5233fcc4fe4c588fae7" + integrity sha512-G+chJctFPiiLGvs3+/Mly3apXTcfgE45dT5yp12BcWZ1kUs+gm0qd3/fv4gsz6fVag4mM0moHVpjHDIgph6Psg== + +acorn@^6.4.1: + version "6.4.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" + integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== + +add-dom-event-listener@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/add-dom-event-listener/-/add-dom-event-listener-1.1.0.tgz#6a92db3a0dd0abc254e095c0f1dc14acbbaae310" + integrity sha512-WCxx1ixHT0GQU9hb0KI/mhgRQhnU+U3GvwY6ZvVjYq8rsihIGoaIOUbY0yMPBxLH5MDtr0kz3fisWGNcbWW7Jw== + dependencies: + object-assign "4.x" + +adjust-sourcemap-loader@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-2.0.0.tgz#6471143af75ec02334b219f54bc7970c52fb29a4" + integrity sha512-4hFsTsn58+YjrU9qKzML2JSSDqKvN8mUGQ0nNIrfPi8hmIONT4L3uUaT6MKdMsZ9AjsU6D2xDkZxCkbQPxChrA== + dependencies: + assert "1.4.1" + camelcase "5.0.0" + loader-utils "1.2.3" + object-path "0.11.4" + regex-parser "2.2.10" + +adm-zip@^0.4.9: + version "0.4.16" + resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.16.tgz#cf4c508fdffab02c269cbc7f471a875f05570365" + integrity sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg== + +after@0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" + integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8= + +agent-base@4, agent-base@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" + integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== + dependencies: + es6-promisify "^5.0.0" + +agent-base@~4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" + integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg== + dependencies: + es6-promisify "^5.0.0" + +agentkeepalive@^3.4.1: + version "3.5.2" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-3.5.2.tgz#a113924dd3fa24a0bc3b78108c450c2abee00f67" + integrity sha512-e0L/HNe6qkQ7H19kTlRRqUibEAwDK5AFk6y3PtMsuut2VAH6+Q4xZml1tNDJD7kSAyqmbG/K08K5WEJYtUrSlQ== + dependencies: + humanize-ms "^1.2.1" + +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + +ajv-errors@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" + integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ== + +ajv-keywords@^3.1.0, ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv@6.12.4: + version "6.12.4" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.4.tgz#0614facc4522127fa713445c6bfd3ebd376e2234" + integrity sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4: + version "6.12.5" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da" + integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +alphanum-sort@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" + integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= + +angular-gridster2@^10.1.6: + version "10.1.6" + resolved "https://registry.yarnpkg.com/angular-gridster2/-/angular-gridster2-10.1.6.tgz#7fb3f93e35c566be220ba6107550b581d952af78" + integrity sha512-k0aWhX2N8E3cux4goPVs4za3FiekH++NvfsxruKG/gI5jXUOYrz1qsK66oYgrJM0zy9zGcBUU0jKIZiC8pklag== + dependencies: + tslib "^2.0.0" + +angular2-hotkeys@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/angular2-hotkeys/-/angular2-hotkeys-2.2.0.tgz#7b52ba99c42c56656360953ab79d776f583d1ab8" + integrity sha512-2O2wtPyscQU/PtyPc+TefSHAql0VI51rrKyIt87YAvBaGUZEj5PZG2QtC7kYI3sFhXYlvrNefUxXoehFjEVQAQ== + dependencies: + "@types/mousetrap" "^1.6.0" + mousetrap "^1.6.0" + +ansi-colors@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-colors@^3.0.0: + version "3.2.4" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" + integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== + +ansi-escapes@^4.2.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61" + integrity sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA== + dependencies: + type-fest "^0.11.0" + +ansi-html@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" + integrity sha1-gTWEAhliqenm/QOflA0S9WynhZ4= + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= + +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +anymatch@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" + integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== + dependencies: + micromatch "^3.1.4" + normalize-path "^2.1.1" + +anymatch@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" + integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +app-root-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-3.0.0.tgz#210b6f43873227e18a4b810a032283311555d5ad" + integrity sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw== + +aproba@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +argparse@^1.0.7, argparse@^1.0.9: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +aria-query@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-3.0.0.tgz#65b3fcc1ca1155a8c9ae64d6eee297f15d5133cc" + integrity sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w= + dependencies: + ast-types-flow "0.0.7" + commander "^2.11.0" + +arity-n@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/arity-n/-/arity-n-1.0.4.tgz#d9e76b11733e08569c0847ae7b39b2860b30b745" + integrity sha1-2edrEXM+CFacCEeuezmyhgswt0U= + +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= + +arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + +array-flatten@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" + integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= + dependencies: + array-uniq "^1.0.1" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= + +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= + +arraybuffer.slice@~0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675" + integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog== + +arrify@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= + +asap@^2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= + +asn1.js@^5.2.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" + integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + safer-buffer "^2.1.0" + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +assert@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" + integrity sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE= + dependencies: + util "0.10.3" + +assert@^1.1.1: + version "1.5.0" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" + integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA== + dependencies: + object-assign "^4.1.1" + util "0.10.3" + +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= + +ast-types-flow@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" + integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0= + +async-each@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" + integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== + +async-limiter@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== + +async@^2.6.2, async@~2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" + integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== + dependencies: + lodash "^4.17.14" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + +attr-accept@^2.0.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b" + integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg== + +autoprefixer@9.8.6: + version "9.8.6" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f" + integrity sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg== + dependencies: + browserslist "^4.12.0" + caniuse-lite "^1.0.30001109" + colorette "^1.2.1" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + postcss "^7.0.32" + postcss-value-parser "^4.1.0" + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.10.1" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.1.tgz#e1e82e4f3e999e2cfd61b161280d16a111f86428" + integrity sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA== + +axobject-query@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.0.2.tgz#ea187abe5b9002b377f925d8bf7d1c561adf38f9" + integrity sha512-MCeek8ZH7hKyO1rWUbKNQBbl4l2eY0ntk7OGi+q0RlafrCnfPxC06WZA+uebCfmYp4mNU9jRBP1AhGyf8+W3ww== + dependencies: + ast-types-flow "0.0.7" + +babel-loader@8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.1.0.tgz#c611d5112bd5209abe8b9fa84c3e4da25275f1c3" + integrity sha512-7q7nC1tYOrqvUrN3LQK4GwSk/TQorZSOlO9C+RZDZpODgyN4ZlCqE5q9cDsyWOliN+aU9B4JX01xK9eJXowJLw== + dependencies: + find-cache-dir "^2.1.0" + loader-utils "^1.4.0" + mkdirp "^0.5.3" + pify "^4.0.1" + schema-utils "^2.6.5" + +babel-plugin-dynamic-import-node@^2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" + integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== + dependencies: + object.assign "^4.1.0" + +backo2@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" + integrity sha1-MasayLEpNjRj41s+u2n038+6eUc= + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +base64-arraybuffer@0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz#9818c79e059b1355f97e0428a017c838e90ba812" + integrity sha1-mBjHngWbE1X5fgQooBfIOOkLqBI= + +base64-arraybuffer@0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" + integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg= + +base64-js@^1.0.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" + integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== + +base64id@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" + integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== + +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY= + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +better-assert@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522" + integrity sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI= + dependencies: + callsite "1.0.0" + +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + +binary-extensions@^1.0.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" + integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== + +binary-extensions@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" + integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ== + +bindings@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" + +blob@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683" + integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig== + +blocking-proxy@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/blocking-proxy/-/blocking-proxy-1.0.1.tgz#81d6fd1fe13a4c0d6957df7f91b75e98dac40cb2" + integrity sha512-KE8NFMZr3mN2E0HcvCgRtX7DjhiIQrwle+nSVJVC/yqFb9+xznHl2ZcoBp2L9qzkI4t4cBFJ1efXF8Dwi132RA== + dependencies: + minimist "^1.2.0" + +bluebird@^3.5.1, bluebird@^3.5.3, bluebird@^3.5.5: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0: + version "4.11.9" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" + integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== + +bn.js@^5.1.1: + version "5.1.3" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b" + integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ== + +body-parser@1.19.0, body-parser@^1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== + dependencies: + bytes "3.1.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.7.2" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.7.0" + raw-body "2.4.0" + type-is "~1.6.17" + +bonjour@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" + integrity sha1-jokKGD2O6aI5OzhExpGkK897yfU= + dependencies: + array-flatten "^2.1.0" + deep-equal "^1.0.1" + dns-equal "^1.0.0" + dns-txt "^2.0.2" + multicast-dns "^6.0.1" + multicast-dns-service-types "^1.1.0" + +boolbase@^1.0.0, boolbase@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^2.3.1, braces@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + +braces@^3.0.1, braces@^3.0.2, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +brorand@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= + +browserify-aes@^1.0.0, browserify-aes@^1.0.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" + integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== + dependencies: + buffer-xor "^1.0.3" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.3" + inherits "^2.0.1" + safe-buffer "^5.0.1" + +browserify-cipher@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" + integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== + dependencies: + browserify-aes "^1.0.4" + browserify-des "^1.0.0" + evp_bytestokey "^1.0.0" + +browserify-des@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" + integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== + dependencies: + cipher-base "^1.0.1" + des.js "^1.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + +browserify-rsa@^4.0.0, browserify-rsa@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" + integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ= + dependencies: + bn.js "^4.1.0" + randombytes "^2.0.1" + +browserify-sign@^4.0.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.1.tgz#eaf4add46dd54be3bb3b36c0cf15abbeba7956c3" + integrity sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg== + dependencies: + bn.js "^5.1.1" + browserify-rsa "^4.0.1" + create-hash "^1.2.0" + create-hmac "^1.1.7" + elliptic "^6.5.3" + inherits "^2.0.4" + parse-asn1 "^5.1.5" + readable-stream "^3.6.0" + safe-buffer "^5.2.0" + +browserify-zlib@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" + integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== + dependencies: + pako "~1.0.5" + +browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.8.5, browserslist@^4.9.1: + version "4.14.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.5.tgz#1c751461a102ddc60e40993639b709be7f2c4015" + integrity sha512-Z+vsCZIvCBvqLoYkBFTwEYH3v5MCQbsAjp50ERycpOjnPmolg1Gjy4+KaWWpm8QOJt9GHkhdqAl14NpCX73CWA== + dependencies: + caniuse-lite "^1.0.30001135" + electron-to-chromium "^1.3.571" + escalade "^3.1.0" + node-releases "^1.1.61" + +browserstack@^1.5.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/browserstack/-/browserstack-1.6.0.tgz#5a56ab90987605d9c138d7a8b88128370297f9bf" + integrity sha512-HJDJ0TSlmkwnt9RZ+v5gFpa1XZTBYTj0ywvLwJ3241J7vMw2jAsGNVhKHtmCOyg+VxeLZyaibO9UL71AsUeDIw== + dependencies: + https-proxy-agent "^2.2.1" + +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +buffer-indexof@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" + integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== + +buffer-xor@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= + +buffer@^4.3.0: + version "4.9.2" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" + integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + +builtin-modules@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= + +builtin-status-codes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" + integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= + +builtins@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" + integrity sha1-y5T662HIaWRR2zZTThQi+U8K7og= + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= + +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + +cacache@15.0.5, cacache@^15.0.4, cacache@^15.0.5: + version "15.0.5" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.0.5.tgz#69162833da29170d6732334643c60e005f5f17d0" + integrity sha512-lloiL22n7sOjEEXdL8NAjTgv9a1u43xICE9/203qonkZUCj5X1UEWIdf2/Y0d6QcCtMzbKQyhrcDbdvlZTs/+A== + dependencies: + "@npmcli/move-file" "^1.0.1" + chownr "^2.0.0" + fs-minipass "^2.0.0" + glob "^7.1.4" + infer-owner "^1.0.4" + lru-cache "^6.0.0" + minipass "^3.1.1" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.2" + mkdirp "^1.0.3" + p-map "^4.0.0" + promise-inflight "^1.0.1" + rimraf "^3.0.2" + ssri "^8.0.0" + tar "^6.0.2" + unique-filename "^1.1.1" + +cacache@^12.0.0, cacache@^12.0.2: + version "12.0.4" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.4.tgz#668bcbd105aeb5f1d92fe25570ec9525c8faa40c" + integrity sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ== + dependencies: + bluebird "^3.5.5" + chownr "^1.1.1" + figgy-pudding "^3.5.1" + glob "^7.1.4" + graceful-fs "^4.1.15" + infer-owner "^1.0.3" + lru-cache "^5.1.1" + mississippi "^3.0.0" + mkdirp "^0.5.1" + move-concurrently "^1.0.1" + promise-inflight "^1.0.1" + rimraf "^2.6.3" + ssri "^6.0.1" + unique-filename "^1.1.1" + y18n "^4.0.0" + +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + +caller-callsite@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" + integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ= + dependencies: + callsites "^2.0.0" + +caller-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" + integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ= + dependencies: + caller-callsite "^2.0.0" + +callsite@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" + integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA= + +callsites@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" + integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= + +camelcase@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" + integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== + +camelcase@5.3.1, camelcase@^5.0.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.0.0.tgz#5259f7c30e35e278f1bdc2a4d91230b37cad981e" + integrity sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w== + +caniuse-api@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" + integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== + dependencies: + browserslist "^4.0.0" + caniuse-lite "^1.0.0" + lodash.memoize "^4.1.2" + lodash.uniq "^4.5.0" + +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001032, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001135: + version "1.0.30001146" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001146.tgz#c61fcb1474520c1462913689201fb292ba6f447c" + integrity sha512-VAy5RHDfTJhpxnDdp2n40GPPLp3KqNrXz1QqFv4J64HvArKs8nuNMOWkB3ICOaBTU/Aj4rYAo/ytdQDDFF/Pug== + +canonical-path@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/canonical-path/-/canonical-path-1.0.0.tgz#fcb470c23958def85081856be7a86e904f180d1d" + integrity sha512-feylzsbDxi1gPZ1IjystzIQZagYYLvfKrSuygUCgf7z6x790VEzze5QEkdSV1U58RA7Hi0+v6fv4K54atOzATg== + +canvas-gauges@^2.1.7: + version "2.1.7" + resolved "https://registry.yarnpkg.com/canvas-gauges/-/canvas-gauges-2.1.7.tgz#9f8d96960a19c64879083e72e66b773ed1ec8079" + integrity sha512-z9cXBVTZdaUIOh32g21NU8gwxEeaxpEMvkZr9t8Y0QDbZiCDq05SJ17aIt+DM12oTJAlWGluN21D+bQ0NCv5GA== + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +chalk@^1.1.1, chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0, chalk@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" + integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + +"chokidar@>=2.0.0 <4.0.0", chokidar@^3.0.0, chokidar@^3.4.1: + version "3.4.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.2.tgz#38dc8e658dec3809741eb3ef7bb0a47fe424232d" + integrity sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.4.0" + optionalDependencies: + fsevents "~2.1.2" + +chokidar@^2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" + integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== + dependencies: + anymatch "^2.0.0" + async-each "^1.0.1" + braces "^2.3.2" + glob-parent "^3.1.0" + inherits "^2.0.3" + is-binary-path "^1.0.0" + is-glob "^4.0.0" + normalize-path "^3.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.2.1" + upath "^1.1.1" + optionalDependencies: + fsevents "^1.2.7" + +chownr@^1.1.1, chownr@^1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + +chrome-trace-event@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4" + integrity sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ== + dependencies: + tslib "^1.9.0" + +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" + integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +circular-dependency-plugin@5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/circular-dependency-plugin/-/circular-dependency-plugin-5.2.0.tgz#e09dbc2dd3e2928442403e2d45b41cea06bc0a93" + integrity sha512-7p4Kn/gffhQaavNfyDFg7LS5S/UT1JAjyGd4UqR2+jzoYF02eDkj0Ec3+48TsIa4zghjLY87nQHIh/ecK9qLdw== + +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + +classnames@2.x, classnames@^2.2.1, classnames@^2.2.6: + version "2.2.6" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" + integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== + +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-spinners@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.4.0.tgz#c6256db216b878cfba4720e719cec7cf72685d7f" + integrity sha512-sJAofoarcm76ZGpuooaO0eDy8saEy+YoZBLjC4h8srt4jeBnkYeOgqxgsJQTpyt2LjI5PTfLJHSL+41Yu4fEJA== + +cli-width@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" + integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== + +clipboard@^2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.6.tgz#52921296eec0fdf77ead1749421b21c968647376" + integrity sha512-g5zbiixBRk/wyKakSwCKd7vQXDjFnAMGHoEyBogG/bw9kTD9GvdAvaoRR1ALcEzt3pVKxZR0pViekPMIS0QyGg== + dependencies: + good-listener "^1.2.2" + select "^1.1.2" + tiny-emitter "^2.0.0" + +cliui@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" + integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== + dependencies: + string-width "^3.1.0" + strip-ansi "^5.2.0" + wrap-ansi "^5.1.0" + +cliui@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" + integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^6.2.0" + +clone@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= + +clone@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" + integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= + +clsx@^1.0.2, clsx@^1.0.4: + version "1.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" + integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== + +coa@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3" + integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA== + dependencies: + "@types/q" "^1.5.1" + chalk "^2.4.1" + q "^1.1.2" + +codelyzer@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/codelyzer/-/codelyzer-6.0.1.tgz#c0e9668e847255b37c759e68fb2700b11e277d0f" + integrity sha512-cOyGQgMdhnRYtW2xrJUNrNYDjEgwQ+BrE2y93Bwz3h4DJ6vJRLfupemU5N3pbYsUlBHJf0u1j1UGk+NLW4d97g== + dependencies: + "@angular/compiler" "9.0.0" + "@angular/core" "9.0.0" + app-root-path "^3.0.0" + aria-query "^3.0.0" + axobject-query "2.0.2" + css-selector-tokenizer "^0.7.1" + cssauron "^1.4.0" + damerau-levenshtein "^1.0.4" + rxjs "^6.5.3" + semver-dsl "^1.0.1" + source-map "^0.5.7" + sprintf-js "^1.1.2" + tslib "^1.10.0" + zone.js "~0.10.3" + +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + +color-convert@^1.9.0, color-convert@^1.9.1: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +color-name@^1.0.0, color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-string@^1.5.2: + version "1.5.3" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc" + integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/color/-/color-3.1.2.tgz#68148e7f85d41ad7649c5fa8c8106f098d229e10" + integrity sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg== + dependencies: + color-convert "^1.9.1" + color-string "^1.5.2" + +colorette@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" + integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== + +colors@1.4.0, colors@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" + integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^2.11.0, commander@^2.12.1, commander@^2.19.0, commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= + +compass-sass-mixins@^0.12.7: + version "0.12.7" + resolved "https://registry.yarnpkg.com/compass-sass-mixins/-/compass-sass-mixins-0.12.7.tgz#2ac4d310f2ebe518b7d6aca4ae24f1d325409e8c" + integrity sha1-KsTTEPLr5Ri31qykriTx0yVAnow= + +component-bind@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1" + integrity sha1-AMYIq33Nk4l8AAllGx06jh5zu9E= + +component-emitter@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" + integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= + +component-emitter@^1.2.1, component-emitter@~1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + +component-inherit@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" + integrity sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM= + +compose-function@3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/compose-function/-/compose-function-3.0.3.tgz#9ed675f13cc54501d30950a486ff6a7ba3ab185f" + integrity sha1-ntZ18TzFRQHTCVCkhv9qe6OrGF8= + dependencies: + arity-n "^1.0.4" + +compressible@~2.0.16: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression-webpack-plugin@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-6.0.2.tgz#13482bfa81e0472e5d6af1165b6ee9f29f98178b" + integrity sha512-WUv7fTy2uCZKJ4iFMKJG42GDepCEocS5eqsEi8uIJZy97k/WvzxGz9dwE4+pIAkcrK4B7k+teKo71IrLu+tbqw== + dependencies: + cacache "^15.0.5" + find-cache-dir "^3.3.1" + schema-utils "^2.7.1" + serialize-javascript "^5.0.1" + webpack-sources "^1.4.3" + +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +concat-stream@^1.5.0: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +config-chain@^1.1.12: + version "1.1.12" + resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa" + integrity sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA== + dependencies: + ini "^1.3.4" + proto-list "~1.2.1" + +connect-history-api-fallback@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" + integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== + +connect@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8" + integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ== + dependencies: + debug "2.6.9" + finalhandler "1.1.2" + parseurl "~1.3.3" + utils-merge "1.0.1" + +console-browserify@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" + integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== + +constants-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" + integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= + +content-disposition@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +convert-source-map@1.7.0, convert-source-map@^1.5.1, convert-source-map@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" + integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== + dependencies: + safe-buffer "~5.1.1" + +convert-source-map@^0.3.3: + version "0.3.5" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-0.3.5.tgz#f1d802950af7dd2631a1febe0596550c86ab3190" + integrity sha1-8dgClQr33SYxof6+BZZVDIarMZA= + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s= + +cookie@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== + +copy-concurrently@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" + integrity sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A== + dependencies: + aproba "^1.1.1" + fs-write-stream-atomic "^1.0.8" + iferr "^0.1.5" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.0" + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= + +copy-webpack-plugin@6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-6.0.3.tgz#2b3d2bfc6861b96432a65f0149720adbd902040b" + integrity sha512-q5m6Vz4elsuyVEIUXr7wJdIdePWTubsqVbEMvf1WQnHGv0Q+9yPRu7MtYFPt+GBOXRav9lvIINifTQ1vSCs+eA== + dependencies: + cacache "^15.0.4" + fast-glob "^3.2.4" + find-cache-dir "^3.3.1" + glob-parent "^5.1.1" + globby "^11.0.1" + loader-utils "^2.0.0" + normalize-path "^3.0.0" + p-limit "^3.0.1" + schema-utils "^2.7.0" + serialize-javascript "^4.0.0" + webpack-sources "^1.4.3" + +core-js-compat@^3.6.2: + version "3.6.5" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.6.5.tgz#2a51d9a4e25dfd6e690251aa81f99e3c05481f1c" + integrity sha512-7ItTKOhOZbznhXAQ2g/slGg1PJV5zDO/WdkTwi7UEOJmkvsE32PWvx6mKtDjiMpjnR2CNf6BAD6sSxIlv7ptng== + dependencies: + browserslist "^4.8.5" + semver "7.0.0" + +core-js@3.6.4: + version "3.6.4" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.4.tgz#440a83536b458114b9cb2ac1580ba377dc470647" + integrity sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw== + +core-js@^3.6.5: + version "3.6.5" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a" + integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA== + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +cosmiconfig@^5.0.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" + integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== + dependencies: + import-fresh "^2.0.0" + is-directory "^0.3.1" + js-yaml "^3.13.1" + parse-json "^4.0.0" + +create-ecdh@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" + integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== + dependencies: + bn.js "^4.1.0" + elliptic "^6.5.3" + +create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" + integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + md5.js "^1.3.4" + ripemd160 "^2.0.1" + sha.js "^2.4.0" + +create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" + integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== + dependencies: + cipher-base "^1.0.3" + create-hash "^1.1.0" + inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +cross-spawn@^6.0.0: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +crypto-browserify@^3.11.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" + integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== + dependencies: + browserify-cipher "^1.0.0" + browserify-sign "^4.0.0" + create-ecdh "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.0" + diffie-hellman "^5.0.0" + inherits "^2.0.1" + pbkdf2 "^3.0.3" + public-encrypt "^4.0.0" + randombytes "^2.0.0" + randomfill "^1.0.3" + +css-color-names@0.0.4, css-color-names@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" + integrity sha1-gIrcLnnPhHOAabZGyyDsJ762KeA= + +css-declaration-sorter@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz#c198940f63a76d7e36c1e71018b001721054cb22" + integrity sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA== + dependencies: + postcss "^7.0.1" + timsort "^0.3.0" + +css-loader@4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-4.2.2.tgz#b668b3488d566dc22ebcf9425c5f254a05808c89" + integrity sha512-omVGsTkZPVwVRpckeUnLshPp12KsmMSLqYxs12+RzM9jRR5Y+Idn/tBffjXRvOE+qW7if24cuceFJqYR5FmGBg== + dependencies: + camelcase "^6.0.0" + cssesc "^3.0.0" + icss-utils "^4.1.1" + loader-utils "^2.0.0" + postcss "^7.0.32" + postcss-modules-extract-imports "^2.0.0" + postcss-modules-local-by-default "^3.0.3" + postcss-modules-scope "^2.2.0" + postcss-modules-values "^3.0.0" + postcss-value-parser "^4.1.0" + schema-utils "^2.7.0" + semver "^7.3.2" + +css-parse@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/css-parse/-/css-parse-2.0.0.tgz#a468ee667c16d81ccf05c58c38d2a97c780dbfd4" + integrity sha1-pGjuZnwW2BzPBcWMONKpfHgNv9Q= + dependencies: + css "^2.0.0" + +css-select-base-adapter@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" + integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w== + +css-select@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.1.0.tgz#6a34653356635934a81baca68d0255432105dbef" + integrity sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ== + dependencies: + boolbase "^1.0.0" + css-what "^3.2.1" + domutils "^1.7.0" + nth-check "^1.0.2" + +css-selector-tokenizer@^0.7.1: + version "0.7.3" + resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.3.tgz#735f26186e67c749aaf275783405cf0661fae8f1" + integrity sha512-jWQv3oCEL5kMErj4wRnK/OPoBi0D+P1FR2cDCKYPaMeD2eW3/mttav8HT4hT1CKopiJI/psEULjkClhvJo4Lvg== + dependencies: + cssesc "^3.0.0" + fastparse "^1.1.2" + +css-tree@1.0.0-alpha.37: + version "1.0.0-alpha.37" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22" + integrity sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg== + dependencies: + mdn-data "2.0.4" + source-map "^0.6.1" + +css-tree@1.0.0-alpha.39: + version "1.0.0-alpha.39" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.39.tgz#2bff3ffe1bb3f776cf7eefd91ee5cba77a149eeb" + integrity sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA== + dependencies: + mdn-data "2.0.6" + source-map "^0.6.1" + +css-vendor@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/css-vendor/-/css-vendor-2.0.8.tgz#e47f91d3bd3117d49180a3c935e62e3d9f7f449d" + integrity sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ== + dependencies: + "@babel/runtime" "^7.8.3" + is-in-browser "^1.0.2" + +css-what@^3.2.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.1.tgz#81cb70b609e4b1351b1e54cbc90fd9c54af86e2e" + integrity sha512-wHOppVDKl4vTAOWzJt5Ek37Sgd9qq1Bmj/T1OjvicWbU5W7ru7Pqbn0Jdqii3Drx/h+dixHKXNhZYx7blthL7g== + +css@^2.0.0: + version "2.2.4" + resolved "https://registry.yarnpkg.com/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929" + integrity sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw== + dependencies: + inherits "^2.0.3" + source-map "^0.6.1" + source-map-resolve "^0.5.2" + urix "^0.1.0" + +cssauron@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/cssauron/-/cssauron-1.4.0.tgz#a6602dff7e04a8306dc0db9a551e92e8b5662ad8" + integrity sha1-pmAt/34EqDBtwNuaVR6S6LVmKtg= + dependencies: + through X.X.X + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +cssnano-preset-default@^4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz#51ec662ccfca0f88b396dcd9679cdb931be17f76" + integrity sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA== + dependencies: + css-declaration-sorter "^4.0.1" + cssnano-util-raw-cache "^4.0.1" + postcss "^7.0.0" + postcss-calc "^7.0.1" + postcss-colormin "^4.0.3" + postcss-convert-values "^4.0.1" + postcss-discard-comments "^4.0.2" + postcss-discard-duplicates "^4.0.2" + postcss-discard-empty "^4.0.1" + postcss-discard-overridden "^4.0.1" + postcss-merge-longhand "^4.0.11" + postcss-merge-rules "^4.0.3" + postcss-minify-font-values "^4.0.2" + postcss-minify-gradients "^4.0.2" + postcss-minify-params "^4.0.2" + postcss-minify-selectors "^4.0.2" + postcss-normalize-charset "^4.0.1" + postcss-normalize-display-values "^4.0.2" + postcss-normalize-positions "^4.0.2" + postcss-normalize-repeat-style "^4.0.2" + postcss-normalize-string "^4.0.2" + postcss-normalize-timing-functions "^4.0.2" + postcss-normalize-unicode "^4.0.1" + postcss-normalize-url "^4.0.1" + postcss-normalize-whitespace "^4.0.2" + postcss-ordered-values "^4.1.2" + postcss-reduce-initial "^4.0.3" + postcss-reduce-transforms "^4.0.2" + postcss-svgo "^4.0.2" + postcss-unique-selectors "^4.0.1" + +cssnano-util-get-arguments@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz#ed3a08299f21d75741b20f3b81f194ed49cc150f" + integrity sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8= + +cssnano-util-get-match@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz#c0e4ca07f5386bb17ec5e52250b4f5961365156d" + integrity sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0= + +cssnano-util-raw-cache@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz#b26d5fd5f72a11dfe7a7846fb4c67260f96bf282" + integrity sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA== + dependencies: + postcss "^7.0.0" + +cssnano-util-same-parent@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz#574082fb2859d2db433855835d9a8456ea18bbf3" + integrity sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q== + +cssnano@4.1.10: + version "4.1.10" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-4.1.10.tgz#0ac41f0b13d13d465487e111b778d42da631b8b2" + integrity sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ== + dependencies: + cosmiconfig "^5.0.0" + cssnano-preset-default "^4.0.7" + is-resolvable "^1.0.0" + postcss "^7.0.0" + +csso@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/csso/-/csso-4.0.3.tgz#0d9985dc852c7cc2b2cacfbbe1079014d1a8e903" + integrity sha512-NL3spysxUkcrOgnpsT4Xdl2aiEiBG6bXswAABQVHcMrfjjBisFOKwLDOmf4wf32aPdcJws1zds2B0Rg+jqMyHQ== + dependencies: + css-tree "1.0.0-alpha.39" + +csstype@^2.5.2: + version "2.6.13" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.13.tgz#a6893015b90e84dd6e85d0e3b442a1e84f2dbe0f" + integrity sha512-ul26pfSQTZW8dcOnD2iiJssfXw0gdNVX9IJDH/X3K5DGPfj+fUYe3kB+swUY6BF3oZDxaID3AJt+9/ojSAE05A== + +csstype@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.3.tgz#2b410bbeba38ba9633353aff34b05d9755d065f8" + integrity sha512-jPl+wbWPOWJ7SXsWyqGRk3lGecbar0Cb0OvZF/r/ZU011R4YqiRehgkQ9p4eQfo9DSDLqLL3wHwfxeJiuIsNag== + +custom-event@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" + integrity sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU= + +cyclist@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" + integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= + +d@1, d@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" + integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== + dependencies: + es5-ext "^0.10.50" + type "^1.0.1" + +damerau-levenshtein@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz#143c1641cb3d85c60c32329e26899adea8701791" + integrity sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug== + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +data-urls@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" + integrity sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ== + dependencies: + abab "^2.0.3" + whatwg-mimetype "^2.3.0" + whatwg-url "^8.0.0" + +date-fns@^2.15.0: + version "2.16.1" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.16.1.tgz#05775792c3f3331da812af253e1a935851d3834b" + integrity sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ== + +date-format@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/date-format/-/date-format-2.1.0.tgz#31d5b5ea211cf5fd764cd38baf9d033df7e125cf" + integrity sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA== + +date-format@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/date-format/-/date-format-3.0.0.tgz#eb8780365c7d2b1511078fb491e6479780f3ad95" + integrity sha512-eyTcpKOcamdhWJXj56DpQMo1ylSQpcGtGKXcU0Tb97+K56/CF5amAqqqNj0+KvA0iw2ynxtHWFsPDSClCxe48w== + +debug@2.6.9, debug@^2.2.0, debug@^2.3.3: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@3.1.0, debug@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + +debug@4.1.1, debug@~4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +debug@^3.1.0, debug@^3.1.1, debug@^3.2.5: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +debug@^4.1.0, debug@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" + integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg== + dependencies: + ms "2.1.2" + +debuglog@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" + integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= + +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= + +deep-equal@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" + integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g== + dependencies: + is-arguments "^1.0.4" + is-date-object "^1.0.1" + is-regex "^1.0.4" + object-is "^1.0.1" + object-keys "^1.1.1" + regexp.prototype.flags "^1.2.0" + +deep-freeze-strict@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/deep-freeze-strict/-/deep-freeze-strict-1.1.1.tgz#77d0583ca24a69be4bbd9ac2fae415d55523e5b0" + integrity sha1-d9BYPKJKab5LvZrC+uQV1VUj5bA= + +default-gateway@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b" + integrity sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA== + dependencies: + execa "^1.0.0" + ip-regex "^2.1.0" + +defaults@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" + integrity sha1-xlYFHpgX2f8I7YgUd/P+QBnz730= + dependencies: + clone "^1.0.2" + +define-properties@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + dependencies: + object-keys "^1.0.12" + +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + +del@^2.2.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" + integrity sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag= + dependencies: + globby "^5.0.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + rimraf "^2.2.8" + +del@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4" + integrity sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ== + dependencies: + "@types/glob" "^7.1.1" + globby "^6.1.0" + is-path-cwd "^2.0.0" + is-path-in-cwd "^2.0.0" + p-map "^2.0.0" + pify "^4.0.1" + rimraf "^2.6.3" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +delegate@^3.1.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166" + integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +dependency-graph@^0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.7.2.tgz#91db9de6eb72699209d88aea4c1fd5221cac1c49" + integrity sha512-KqtH4/EZdtdfWX0p6MGP9jljvxSY6msy/pRUD4jgNwVpv3v1QmNLlsB3LDSSUg79BRVSn7jI1QPRtArGABovAQ== + +des.js@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" + integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA== + dependencies: + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + +detect-node@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" + integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== + +dezalgo@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" + integrity sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY= + dependencies: + asap "^2.0.0" + wrappy "1" + +di@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" + integrity sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw= + +diff-match-patch@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37" + integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +diffie-hellman@^5.0.0: + version "5.0.3" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" + integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== + dependencies: + bn.js "^4.1.0" + miller-rabin "^4.0.0" + randombytes "^2.0.0" + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +directory-tree@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/directory-tree/-/directory-tree-2.2.4.tgz#6d5bd7d82e48378e256a1e87b678a43c50076e2e" + integrity sha512-2N43msQptKbi3WMfIs+U09yi6bfyKL+MWyj5VMj8t1F/Tx04bt1cn/EEIU3o1JBltlJk7NQnzOEuTNa/KQvbWA== + +dns-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" + integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0= + +dns-packet@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.1.tgz#12aa426981075be500b910eedcd0b47dd7deda5a" + integrity sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg== + dependencies: + ip "^1.1.0" + safe-buffer "^5.0.1" + +dns-txt@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" + integrity sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY= + dependencies: + buffer-indexof "^1.0.0" + +dom-align@^1.7.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/dom-align/-/dom-align-1.12.0.tgz#56fb7156df0b91099830364d2d48f88963f5a29c" + integrity sha512-YkoezQuhp3SLFGdOlr5xkqZ640iXrnHAwVYcDg8ZKRUtO7mSzSC2BA5V0VuyAwPSJA4CLIc6EDDJh4bEsD2+zA== + +dom-helpers@^5.0.1: + version "5.2.0" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.0.tgz#57fd054c5f8f34c52a3eeffdb7e7e93cd357d95b" + integrity sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + +dom-serialize@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" + integrity sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs= + dependencies: + custom-event "~1.0.0" + ent "~2.2.0" + extend "^3.0.0" + void-elements "^2.0.0" + +dom-serializer@0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" + integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== + dependencies: + domelementtype "^2.0.1" + entities "^2.0.0" + +domain-browser@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" + integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== + +domelementtype@1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" + integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== + +domelementtype@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.2.tgz#f3b6e549201e46f588b59463dd77187131fe6971" + integrity sha512-wFwTwCVebUrMgGeAwRL/NhZtHAUyT9n9yg4IMDwf10+6iCMxSkVq9MGCVEH+QZWo1nNidy8kNvwmv4zWHDTqvA== + +domutils@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" + integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== + dependencies: + dom-serializer "0" + domelementtype "1" + +dot-prop@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" + integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== + dependencies: + is-obj "^2.0.0" + +duplexify@^3.4.2, duplexify@^3.6.0: + version "3.7.1" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" + integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +editorconfig@^0.15.3: + version "0.15.3" + resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.15.3.tgz#bef84c4e75fb8dcb0ce5cee8efd51c15999befc5" + integrity sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g== + dependencies: + commander "^2.19.0" + lru-cache "^4.1.5" + semver "^5.6.0" + sigmund "^1.0.1" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +electron-to-chromium@^1.3.571: + version "1.3.578" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.578.tgz#e6671936f4571a874eb26e2e833aa0b2c0b776e0" + integrity sha512-z4gU6dA1CbBJsAErW5swTGAaU2TBzc2mPAonJb00zqW1rOraDo2zfBMDRvaz9cVic+0JEZiYbHWPw/fTaZlG2Q== + +elliptic@^6.5.3: + version "6.5.3" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" + integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== + dependencies: + bn.js "^4.4.0" + brorand "^1.0.1" + hash.js "^1.0.0" + hmac-drbg "^1.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.0" + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emojis-list@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" + integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k= + +emojis-list@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" + integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + +encoding@^0.1.11: + version "0.1.13" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" + integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== + dependencies: + iconv-lite "^0.6.2" + +end-of-stream@^1.0.0, end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +engine.io-client@~3.4.0: + version "3.4.4" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.4.4.tgz#77d8003f502b0782dd792b073a4d2cf7ca5ab967" + integrity sha512-iU4CRr38Fecj8HoZEnFtm2EiKGbYZcPn3cHxqNGl/tmdWRf60KhK+9vE0JeSjgnlS/0oynEfLgKbT9ALpim0sQ== + dependencies: + component-emitter "~1.3.0" + component-inherit "0.0.3" + debug "~3.1.0" + engine.io-parser "~2.2.0" + has-cors "1.1.0" + indexof "0.0.1" + parseqs "0.0.6" + parseuri "0.0.6" + ws "~6.1.0" + xmlhttprequest-ssl "~1.5.4" + yeast "0.1.2" + +engine.io-parser@~2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.2.1.tgz#57ce5611d9370ee94f99641b589f94c97e4f5da7" + integrity sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg== + dependencies: + after "0.8.2" + arraybuffer.slice "~0.0.7" + base64-arraybuffer "0.1.4" + blob "0.0.5" + has-binary2 "~1.0.2" + +engine.io@~3.4.0: + version "3.4.2" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.4.2.tgz#8fc84ee00388e3e228645e0a7d3dfaeed5bd122c" + integrity sha512-b4Q85dFkGw+TqgytGPrGgACRUhsdKc9S9ErRAXpPGy/CXKs4tYoHDkvIRdsseAF7NjfVwjRFIn6KTnbw7LwJZg== + dependencies: + accepts "~1.3.4" + base64id "2.0.0" + cookie "0.3.1" + debug "~4.1.0" + engine.io-parser "~2.2.0" + ws "^7.1.2" + +enhanced-resolve@4.3.0, enhanced-resolve@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz#3b806f3bfafc1ec7de69551ef93cca46c1704126" + integrity sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ== + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.5.0" + tapable "^1.0.0" + +ent@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" + integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0= + +entities@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f" + integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ== + +err-code@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960" + integrity sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA= + +errno@^0.1.1, errno@^0.1.3, errno@~0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" + integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg== + dependencies: + prr "~1.0.1" + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5: + version "1.17.7" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c" + integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g== + dependencies: + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + is-callable "^1.2.2" + is-regex "^1.1.1" + object-inspect "^1.8.0" + object-keys "^1.1.1" + object.assign "^4.1.1" + string.prototype.trimend "^1.0.1" + string.prototype.trimstart "^1.0.1" + +es-abstract@^1.18.0-next.0, es-abstract@^1.18.0-next.1: + version "1.18.0-next.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68" + integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA== + dependencies: + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + is-callable "^1.2.2" + is-negative-zero "^2.0.0" + is-regex "^1.1.1" + object-inspect "^1.8.0" + object-keys "^1.1.1" + object.assign "^4.1.1" + string.prototype.trimend "^1.0.1" + string.prototype.trimstart "^1.0.1" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +es5-ext@^0.10.35, es5-ext@^0.10.50: + version "0.10.53" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1" + integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q== + dependencies: + es6-iterator "~2.0.3" + es6-symbol "~3.1.3" + next-tick "~1.0.0" + +es6-iterator@2.0.3, es6-iterator@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c= + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-promise@^4.0.3: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= + dependencies: + es6-promise "^4.0.3" + +es6-symbol@^3.1.1, es6-symbol@~3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" + integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== + dependencies: + d "^1.0.1" + ext "^1.1.2" + +escalade@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.0.tgz#e8e2d7c7a8b76f6ee64c2181d6b8151441602d4e" + integrity sha512-mAk+hPSO8fLDkhV7V0dXazH5pDc6MrjBTPyD3VeKzxnVFjH1MIxbCdqGZB9O8+EwWakZs3ZCbDS4IpRt79V1ig== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +eslint-scope@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" + integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg== + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esrecurse@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" + integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + +eve-raphael@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/eve-raphael/-/eve-raphael-0.5.0.tgz#17c754b792beef3fa6684d79cf5a47c63c4cda30" + integrity sha1-F8dUt5K+7z+maE15z1pHxjxM2jA= + +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +events@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379" + integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg== + +eventsource@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0" + integrity sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ== + dependencies: + original "^1.0.0" + +evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" + integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== + dependencies: + md5.js "^1.3.4" + safe-buffer "^5.1.1" + +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= + +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +express@^4.17.1: + version "4.17.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== + dependencies: + accepts "~1.3.7" + array-flatten "1.1.1" + body-parser "1.19.0" + content-disposition "0.5.3" + content-type "~1.0.4" + cookie "0.4.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.1.2" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.5" + qs "6.7.0" + range-parser "~1.2.1" + safe-buffer "5.1.2" + send "0.17.1" + serve-static "1.14.1" + setprototypeof "1.1.1" + statuses "~1.5.0" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +ext@^1.1.2: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244" + integrity sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A== + dependencies: + type "^2.0.0" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +extend@^3.0.0, extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +external-editor@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.1.1, fast-glob@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3" + integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.0" + merge2 "^1.3.0" + micromatch "^4.0.2" + picomatch "^2.2.1" + +fast-json-stable-stringify@2.1.0, fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fastparse@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9" + integrity sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ== + +fastq@^1.6.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481" + integrity sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q== + dependencies: + reusify "^1.0.4" + +faye-websocket@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4" + integrity sha1-TkkvjQTftviQA1B/btvy1QHnxvQ= + dependencies: + websocket-driver ">=0.5.1" + +faye-websocket@~0.11.1: + version "0.11.3" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e" + integrity sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA== + dependencies: + websocket-driver ">=0.5.1" + +figgy-pudding@^3.4.1, figgy-pudding@^3.5.1: + version "3.5.2" + resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" + integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw== + +figures@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + +file-loader@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.0.0.tgz#97bbfaab7a2460c07bcbd72d3a6922407f67649f" + integrity sha512-/aMOAYEFXDdjG0wytpTL5YQLfZnnTmLNjn+AIrJ/6HVnTfDqLsVKUUwkDf4I4kgex36BvjuXEn/TX9B/1ESyqQ== + dependencies: + loader-utils "^2.0.0" + schema-utils "^2.6.5" + +file-selector@^0.1.12: + version "0.1.13" + resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.1.13.tgz#5efd977ca2bca1700992df1b10e254f4e73d2df4" + integrity sha512-T2efCBY6Ps+jLIWdNQsmzt/UnAjKOEAlsZVdnQztg/BtAZGNL4uX1Jet9cMM8gify/x4CSudreji2HssGBNVIQ== + dependencies: + tslib "^2.0.1" + +file-uri-to-path@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" + integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.1.2, finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +find-cache-dir@3.3.1, find-cache-dir@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.1.tgz#89b33fad4a4670daa94f855f7fbe31d6d84fe880" + integrity sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ== + dependencies: + commondir "^1.0.1" + make-dir "^3.0.2" + pkg-dir "^4.1.0" + +find-cache-dir@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" + integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== + dependencies: + commondir "^1.0.1" + make-dir "^2.0.0" + pkg-dir "^3.0.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +find-up@^4.0.0, find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +flatted@^2.0.1, flatted@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" + integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== + +"flot.curvedlines@git://github.com/MichaelZinsmaier/CurvedLines.git#master": + version "1.1.1" + resolved "git://github.com/MichaelZinsmaier/CurvedLines.git#22ed1fc2a6ccafc816c2d07b36027cc123825c4b" + +"flot@git://github.com/thingsboard/flot.git#0.9-work": + version "0.9.0-alpha" + resolved "git://github.com/thingsboard/flot.git#0ff0c775db7c74e705f6c3c2bba92080a202ccd4" + +flush-write-stream@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" + integrity sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w== + dependencies: + inherits "^2.0.3" + readable-stream "^2.3.6" + +follow-redirects@^1.0.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db" + integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA== + +font-awesome@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/font-awesome/-/font-awesome-4.7.0.tgz#8fa8cf0411a1a31afd07b06d2902bb9fc815a133" + integrity sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM= + +for-in@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= + dependencies: + map-cache "^0.2.2" + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + +from2@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" + integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8= + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.0" + +fs-extra@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.2.tgz#f91704c53d1b461f893452b0c307d9997647ab6b" + integrity sha1-+RcExT0bRh+JNFKwwwfZmXZHq2s= + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-minipass@^1.2.5: + version "1.2.7" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" + integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA== + dependencies: + minipass "^2.6.0" + +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + +fs-write-stream-atomic@^1.0.8: + version "1.0.10" + resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" + integrity sha1-tH31NJPvkR33VzHnCp3tAYnbQMk= + dependencies: + graceful-fs "^4.1.2" + iferr "^0.1.5" + imurmurhash "^0.1.4" + readable-stream "1 || 2" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fsevents@^1.2.7: + version "1.2.13" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38" + integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw== + dependencies: + bindings "^1.5.0" + nan "^2.12.1" + +fsevents@~2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" + integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +genfun@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/genfun/-/genfun-5.0.0.tgz#9dd9710a06900a5c4a5bf57aca5da4e52fe76537" + integrity sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA== + +gensync@^1.0.0-beta.1: + version "1.0.0-beta.1" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" + integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== + +get-caller-file@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-stream@^4.0.0, get-stream@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +glob-parent@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= + dependencies: + is-glob "^3.1.0" + path-dirname "^1.0.0" + +glob-parent@^5.1.0, glob-parent@^5.1.1, glob-parent@~5.1.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" + integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== + dependencies: + is-glob "^4.0.1" + +glob@7.1.6, glob@^7.0.3, glob@^7.0.6, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globby@^11.0.1: + version "11.0.1" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357" + integrity sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.1.1" + ignore "^5.1.4" + merge2 "^1.3.0" + slash "^3.0.0" + +globby@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" + integrity sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0= + dependencies: + array-union "^1.0.1" + arrify "^1.0.0" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +globby@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" + integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw= + dependencies: + array-union "^1.0.1" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +good-listener@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" + integrity sha1-1TswzfkxPf+33JoNR3CWqm0UXFA= + dependencies: + delegate "^3.1.2" + +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" + integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== + +hammerjs@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/hammerjs/-/hammerjs-2.0.8.tgz#04ef77862cff2bb79d30f7692095930222bf60f1" + integrity sha1-BO93hiz/K7edMPdpIJWTAiK/YPE= + +handle-thing@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" + integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.3: + version "5.1.5" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" + integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== + dependencies: + ajv "^6.12.3" + har-schema "^2.0.0" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= + dependencies: + ansi-regex "^2.0.0" + +has-binary2@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.3.tgz#7776ac627f3ea77250cfc332dab7ddf5e4f5d11d" + integrity sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw== + dependencies: + isarray "2.0.1" + +has-cors@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39" + integrity sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk= + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-symbols@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" + integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== + +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +has@^1.0.0, has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hash-base@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" + integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== + dependencies: + inherits "^2.0.4" + readable-stream "^3.6.0" + safe-buffer "^5.2.0" + +hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + +hex-color-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" + integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== + +hmac-drbg@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +hoist-non-react-statics@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + +hosted-git-info@^2.1.4, hosted-git-info@^2.7.1: + version "2.8.8" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" + integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== + +hosted-git-info@^3.0.2: + version "3.0.5" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-3.0.5.tgz#bea87905ef7317442e8df3087faa3c842397df03" + integrity sha512-i4dpK6xj9BIpVOTboXIlKG9+8HMKggcrMX7WA24xZtKwX0TPelq/rbaS5rCKeNX8sJXZJGdSxpnEGtta+wismQ== + dependencies: + lru-cache "^6.0.0" + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + integrity sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI= + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +hsl-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e" + integrity sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4= + +hsla-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38" + integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg= + +html-comment-regex@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7" + integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ== + +html-entities@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.3.1.tgz#fb9a1a4b5b14c5daba82d3e34c6ae4fe701a0e44" + integrity sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA== + +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + +http-cache-semantics@^3.8.1: + version "3.8.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" + integrity sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w== + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc= + +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-errors@~1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-parser-js@>=0.5.1: + version "0.5.2" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.2.tgz#da2e31d237b393aae72ace43882dd7e270a8ff77" + integrity sha512-opCO9ASqg5Wy2FNo7A0sxy71yGbbkJJXLdgMK04Tcypw9jr2MgWbyubb0+WdmDmGnFflO7fRbqbaihh/ENDlRQ== + +http-proxy-agent@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405" + integrity sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg== + dependencies: + agent-base "4" + debug "3.1.0" + +http-proxy-middleware@0.19.1: + version "0.19.1" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz#183c7dc4aa1479150306498c210cdaf96080a43a" + integrity sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q== + dependencies: + http-proxy "^1.17.0" + is-glob "^4.0.0" + lodash "^4.17.11" + micromatch "^3.1.10" + +http-proxy@^1.17.0, http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +https-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" + integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= + +https-proxy-agent@^2.2.1, https-proxy-agent@^2.2.3: + version "2.2.4" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" + integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== + dependencies: + agent-base "^4.3.0" + debug "^3.1.0" + +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0= + dependencies: + ms "^2.0.0" + +hyphenate-style-name@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d" + integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ== + +iconv-lite@0.4.24, iconv-lite@^0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +iconv-lite@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.2.tgz#ce13d1875b0c3a674bd6a04b7f76b01b1b6ded01" + integrity sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +icss-utils@^4.0.0, icss-utils@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467" + integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA== + dependencies: + postcss "^7.0.14" + +ieee754@^1.1.4: + version "1.1.13" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== + +iferr@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" + integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE= + +ignore-walk@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37" + integrity sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw== + dependencies: + minimatch "^3.0.4" + +ignore@^5.1.4: + version "5.1.8" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" + integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== + +image-size@~0.5.0: + version "0.5.5" + resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" + integrity sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w= + +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= + +import-cwd@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" + integrity sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk= + dependencies: + import-from "^2.1.0" + +import-fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" + integrity sha1-2BNVwVYS04bGH53dOSLUMEgipUY= + dependencies: + caller-path "^2.0.0" + resolve-from "^3.0.0" + +import-from@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1" + integrity sha1-M1238qev/VOqpHHUuAId7ja387E= + dependencies: + resolve-from "^3.0.0" + +import-local@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" + integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== + dependencies: + pkg-dir "^3.0.0" + resolve-cwd "^2.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +indexes-of@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" + integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= + +indexof@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" + integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10= + +infer-owner@^1.0.3, infer-owner@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +ini@1.3.5, ini@^1.3.4: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + +inquirer@7.3.3: + version "7.3.3" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" + integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.19" + mute-stream "0.0.8" + run-async "^2.4.0" + rxjs "^6.6.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + +internal-ip@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" + integrity sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg== + dependencies: + default-gateway "^4.2.0" + ipaddr.js "^1.9.0" + +invariant@^2.2.2, invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + +ip-regex@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" + integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= + +ip@1.1.5, ip@^1.1.0, ip@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" + integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= + +ipaddr.js@1.9.1, ipaddr.js@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-absolute-url@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" + integrity sha1-UFMN+4T8yap9vnhS6Do3uTufKqY= + +is-absolute-url@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698" + integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q== + +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== + dependencies: + kind-of "^6.0.0" + +is-arguments@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" + integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + +is-binary-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= + dependencies: + binary-extensions "^1.0.0" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-callable@^1.1.4, is-callable@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" + integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA== + +is-color-stop@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345" + integrity sha1-z/9HGu5N1cnhWFmPvhKWe1za00U= + dependencies: + css-color-names "^0.0.4" + hex-color-regex "^1.1.0" + hsl-regex "^1.0.0" + hsla-regex "^1.0.0" + rgb-regex "^1.0.1" + rgba-regex "^1.0.0" + +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== + dependencies: + kind-of "^6.0.0" + +is-date-object@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" + integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + +is-directory@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" + integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= + +is-docker@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.1.1.tgz#4125a88e44e450d384e09047ede71adc2d144156" + integrity sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw== + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== + dependencies: + is-plain-object "^2.0.4" + +is-extglob@^2.1.0, is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= + dependencies: + is-extglob "^2.1.0" + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + +is-in-browser@^1.0.2, is-in-browser@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" + integrity sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU= + +is-interactive@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" + integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== + +is-negative-zero@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461" + integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE= + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= + dependencies: + kind-of "^3.0.2" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" + integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== + +is-path-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" + integrity sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0= + +is-path-cwd@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" + integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== + +is-path-in-cwd@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz#5ac48b345ef675339bd6c7a48a912110b241cf52" + integrity sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ== + dependencies: + is-path-inside "^1.0.0" + +is-path-in-cwd@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz#bfe2dca26c69f397265a4009963602935a053acb" + integrity sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ== + dependencies: + is-path-inside "^2.1.0" + +is-path-inside@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" + integrity sha1-jvW33lBDej/cprToZe96pVy0gDY= + dependencies: + path-is-inside "^1.0.1" + +is-path-inside@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-2.1.0.tgz#7c9810587d659a40d27bcdb4d5616eab059494b2" + integrity sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg== + dependencies: + path-is-inside "^1.0.2" + +is-plain-obj@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= + +is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-regex@^1.0.4, is-regex@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" + integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== + dependencies: + has-symbols "^1.0.1" + +is-resolvable@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" + integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== + +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + +is-svg@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75" + integrity sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ== + dependencies: + html-comment-regex "^1.1.0" + +is-symbol@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" + integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== + dependencies: + has-symbols "^1.0.1" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + +is-wsl@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" + integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= + +is-wsl@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isarray@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e" + integrity sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4= + +isbinaryfile@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.6.tgz#edcb62b224e2b4710830b67498c8e4e5a4d2610b" + integrity sha512-ORrEy+SNVqUhrCaal4hA4fBzhggQQ+BaLntyPOdoEiwlKZW9BZiJXjg3RMiruE4tPEI3pyVPpySHQF/dKWperg== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +istanbul-lib-coverage@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49" + integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA== + +istanbul-lib-coverage@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz#f5944a37c70b550b02a78a5c3b2055b280cec8ec" + integrity sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg== + +istanbul-lib-instrument@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d" + integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ== + dependencies: + "@babel/core" "^7.7.5" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.0.0" + semver "^6.3.0" + +istanbul-lib-report@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" + integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^3.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz#284997c48211752ec486253da97e3879defba8c8" + integrity sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^2.0.5" + make-dir "^2.1.0" + rimraf "^2.6.3" + source-map "^0.6.1" + +istanbul-reports@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.2.tgz#d593210e5000683750cb09fc0644e4b6e27fd53b" + integrity sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + +jasmine-core@^3.6.0, jasmine-core@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.6.0.tgz#491f3bb23941799c353ceb7a45b38a950ebc5a20" + integrity sha512-8uQYa7zJN8hq9z+g8z1bqCfdC8eoDAeVnM5sfqs7KHv9/ifoJ500m018fpFc7RDaO6SWCLCXwo/wPSNcdYTgcw== + +jasmine-core@~2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.8.0.tgz#bcc979ae1f9fd05701e45e52e65d3a5d63f1a24e" + integrity sha1-vMl5rh+f0FcB5F5S5l06XWPxok4= + +jasmine-spec-reporter@~5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/jasmine-spec-reporter/-/jasmine-spec-reporter-5.0.2.tgz#b61288ab074ad440dc2477c4d42840b0e74a6b95" + integrity sha512-6gP1LbVgJ+d7PKksQBc2H0oDGNRQI3gKUsWlswKaQ2fif9X5gzhQcgM5+kiJGCQVurOG09jqNhk7payggyp5+g== + dependencies: + colors "1.4.0" + +jasmine@2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-2.8.0.tgz#6b089c0a11576b1f16df11b80146d91d4e8b8a3e" + integrity sha1-awicChFXax8W3xG4AUbZHU6Lij4= + dependencies: + exit "^0.1.2" + glob "^7.0.6" + jasmine-core "~2.8.0" + +jasminewd2@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/jasminewd2/-/jasminewd2-2.2.0.tgz#e37cf0b17f199cce23bea71b2039395246b4ec4e" + integrity sha1-43zwsX8ZnM4jvqcbIDk5Uka07E4= + +jest-worker@26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.3.0.tgz#7c8a97e4f4364b4f05ed8bca8ca0c24de091871f" + integrity sha512-Vmpn2F6IASefL+DVBhPzI2J9/GJUsqzomdeN+P+dK8/jKxbh8R3BtFnx3FIta7wYlPU62cpJMJQo4kuOowcMnw== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^7.0.0" + +jest-worker@^26.3.0: + version "26.5.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.5.0.tgz#87deee86dbbc5f98d9919e0dadf2c40e3152fa30" + integrity sha512-kTw66Dn4ZX7WpjZ7T/SUDgRhapFRKWmisVAF0Rv4Fu8SLFD7eLbqpLvbxVqYhSgaWa7I+bW7pHnbyfNsH6stug== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^7.0.0" + +jquery.terminal@^2.18.3: + version "2.18.3" + resolved "https://registry.yarnpkg.com/jquery.terminal/-/jquery.terminal-2.18.3.tgz#4392fcbc5b2c0d187ea80fe3ecfed03112a5a107" + integrity sha512-zzMVGYlAC+luF7Omm9UY1/nuvp00mozSgcGImObWSS3uDRtcxnxlwxQLC8tvlTT+koyfOvCBaWgB6AD4DvWVpQ== + dependencies: + "@types/jquery" "^3.3.29" + jquery "^3.5.0" + prismjs "^1.21.0" + wcwidth "^1.0.1" + +jquery@>=1.9.1, jquery@^3.5.0, jquery@^3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.5.1.tgz#d7b4d08e1bfdb86ad2f1a3d039ea17304717abb5" + integrity sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg== + +js-beautify@^1.13.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.13.0.tgz#a056d5d3acfd4918549aae3ab039f9f3c51eebb2" + integrity sha512-/Tbp1OVzZjbwzwJQFIlYLm9eWQ+3aYbBXLSaqb1mEJzhcQAfrqMMQYtjb6io+U6KpD0ID4F+Id3/xcjH3l/sqA== + dependencies: + config-chain "^1.1.12" + editorconfig "^0.15.3" + glob "^7.1.3" + mkdirp "^1.0.4" + nopt "^5.0.0" + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.14.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" + integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= + +json-parse-better-errors@^1.0.0, json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-defaults@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/json-schema-defaults/-/json-schema-defaults-0.4.0.tgz#b63ee7e7aa83f29b54cb31d31ecddeb056c3306c" + integrity sha512-UsUrkDVNvHTneyeQOYHH9ZHb3+6OjwYfJ831SdO0yjtXtYZ7Jh8BKWsuJYUQW7qckP5JhHawsg4GI6A5fMaR/Q== + dependencies: + argparse "^1.0.9" + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +json3@^3.3.2: + version "3.3.3" + resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81" + integrity sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA== + +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + +json5@^2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" + integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== + dependencies: + minimist "^1.2.5" + +jsonc-parser@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.3.0.tgz#7c7fc988ee1486d35734faaaa866fadb00fa91ee" + integrity sha512-b0EBt8SWFNnixVdvoR2ZtEGa9ZqLhbJnOjezn+WP+8kspFm+PFYDN8Z4Bc7pRlDjvuVcADSUkroIuTWWn/YiIA== + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= + optionalDependencies: + graceful-fs "^4.1.6" + +jsonparse@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" + integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +jss-plugin-camel-case@^10.0.3: + version "10.4.0" + resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.4.0.tgz#46c75ff7fd61c304984c21af5817823f0f501ceb" + integrity sha512-9oDjsQ/AgdBbMyRjc06Kl3P8lDCSEts2vYZiPZfGAxbGCegqE4RnMob3mDaBby5H9vL9gWmyyImhLRWqIkRUCw== + dependencies: + "@babel/runtime" "^7.3.1" + hyphenate-style-name "^1.0.3" + jss "10.4.0" + +jss-plugin-default-unit@^10.0.3: + version "10.4.0" + resolved "https://registry.yarnpkg.com/jss-plugin-default-unit/-/jss-plugin-default-unit-10.4.0.tgz#2b10f01269eaea7f36f0f5fd1cfbfcc76ed42854" + integrity sha512-BYJ+Y3RUYiMEgmlcYMLqwbA49DcSWsGgHpVmEEllTC8MK5iJ7++pT9TnKkKBnNZZxTV75ycyFCR5xeLSOzVm4A== + dependencies: + "@babel/runtime" "^7.3.1" + jss "10.4.0" + +jss-plugin-global@^10.0.3: + version "10.4.0" + resolved "https://registry.yarnpkg.com/jss-plugin-global/-/jss-plugin-global-10.4.0.tgz#19449425a94e4e74e113139b629fd44d3577f97d" + integrity sha512-b8IHMJUmv29cidt3nI4bUI1+Mo5RZE37kqthaFpmxf5K7r2aAegGliAw4hXvA70ca6ckAoXMUl4SN/zxiRcRag== + dependencies: + "@babel/runtime" "^7.3.1" + jss "10.4.0" + +jss-plugin-nested@^10.0.3: + version "10.4.0" + resolved "https://registry.yarnpkg.com/jss-plugin-nested/-/jss-plugin-nested-10.4.0.tgz#017d0c02c0b6b454fd9d7d3fc33470a15eea9fd1" + integrity sha512-cKgpeHIxAP0ygeWh+drpLbrxFiak6zzJ2toVRi/NmHbpkNaLjTLgePmOz5+67ln3qzJiPdXXJB1tbOyYKAP4Pw== + dependencies: + "@babel/runtime" "^7.3.1" + jss "10.4.0" + tiny-warning "^1.0.2" + +jss-plugin-props-sort@^10.0.3: + version "10.4.0" + resolved "https://registry.yarnpkg.com/jss-plugin-props-sort/-/jss-plugin-props-sort-10.4.0.tgz#7110bf0b6049cc2080b220b506532bf0b70c0e07" + integrity sha512-j/t0R40/2fp+Nzt6GgHeUFnHVY2kPGF5drUVlgkcwYoHCgtBDOhTTsOfdaQFW6sHWfoQYgnGV4CXdjlPiRrzwA== + dependencies: + "@babel/runtime" "^7.3.1" + jss "10.4.0" + +jss-plugin-rule-value-function@^10.0.3: + version "10.4.0" + resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.4.0.tgz#7cff4a91e84973536fa49b6ebbdbf7f339b01c82" + integrity sha512-w8504Cdfu66+0SJoLkr6GUQlEb8keHg8ymtJXdVHWh0YvFxDG2l/nS93SI5Gfx0fV29dO6yUugXnKzDFJxrdFQ== + dependencies: + "@babel/runtime" "^7.3.1" + jss "10.4.0" + tiny-warning "^1.0.2" + +jss-plugin-vendor-prefixer@^10.0.3: + version "10.4.0" + resolved "https://registry.yarnpkg.com/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.4.0.tgz#2a78f3c5d57d1e024fe7ad7c41de34d04e72ecc0" + integrity sha512-DpF+/a+GU8hMh/948sBGnKSNfKkoHg2p9aRFUmyoyxgKjOeH9n74Ht3Yt8lOgdZsuWNJbPrvaa3U4PXKwxVpTQ== + dependencies: + "@babel/runtime" "^7.3.1" + css-vendor "^2.0.8" + jss "10.4.0" + +jss@10.4.0, jss@^10.0.3: + version "10.4.0" + resolved "https://registry.yarnpkg.com/jss/-/jss-10.4.0.tgz#473a6fbe42e85441020a07e9519dac1e8a2e79ca" + integrity sha512-l7EwdwhsDishXzqTc3lbsbyZ83tlUl5L/Hb16pHCvZliA9lRDdNBZmHzeJHP0sxqD0t1mrMmMR8XroR12JBYzw== + dependencies: + "@babel/runtime" "^7.3.1" + csstype "^3.0.2" + is-in-browser "^1.1.3" + tiny-warning "^1.0.2" + +jstree-bootstrap-theme@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/jstree-bootstrap-theme/-/jstree-bootstrap-theme-1.0.1.tgz#7d5edc73a846e8da7f94f57a1cc5ddee9d9eab4b" + integrity sha1-fV7cc6hG6Np/lPV6HMXd7p2eq0s= + dependencies: + jquery ">=1.9.1" + +jstree@^3.3.10: + version "3.3.10" + resolved "https://registry.yarnpkg.com/jstree/-/jstree-3.3.10.tgz#28d5b464a6bd4b5a93ccc2e978d005845596a408" + integrity sha512-TDhwTy24ZKCVei0gLRxnH5PQuX77nqlG7bhQh+UDTeOxC2xdhDrS1x7YtbjLVlSxmH7USnA/WIeVOGN/m3D0QA== + dependencies: + jquery ">=1.9.1" + +jszip@^3.1.3, jszip@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.5.0.tgz#b4fd1f368245346658e781fec9675802489e15f6" + integrity sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA== + dependencies: + lie "~3.3.0" + pako "~1.0.2" + readable-stream "~2.3.6" + set-immediate-shim "~1.0.1" + +karma-chrome-launcher@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.1.0.tgz#805a586799a4d05f4e54f72a204979f3f3066738" + integrity sha512-3dPs/n7vgz1rxxtynpzZTvb9y/GIaW8xjAwcIGttLbycqoFtI7yo1NGnQi6oFTherRE+GIhCAHZC4vEqWGhNvg== + dependencies: + which "^1.2.1" + +karma-coverage-istanbul-reporter@~3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-3.0.3.tgz#f3b5303553aadc8e681d40d360dfdc19bc7e9fe9" + integrity sha512-wE4VFhG/QZv2Y4CdAYWDbMmcAHeS926ZIji4z+FkB2aF/EposRb6DP6G5ncT/wXhqUfAb/d7kZrNKPonbvsATw== + dependencies: + istanbul-lib-coverage "^3.0.0" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^3.0.6" + istanbul-reports "^3.0.2" + minimatch "^3.0.4" + +karma-jasmine-html-reporter@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-1.5.4.tgz#669f33d694d88fce1b0ccfda57111de716cb0192" + integrity sha512-PtilRLno5O6wH3lDihRnz0Ba8oSn0YUJqKjjux1peoYGwo0AQqrWRbdWk/RLzcGlb+onTyXAnHl6M+Hu3UxG/Q== + +karma-jasmine@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/karma-jasmine/-/karma-jasmine-4.0.1.tgz#b99e073b6d99a5196fc4bffc121b89313b0abd82" + integrity sha512-h8XDAhTiZjJKzfkoO1laMH+zfNlra+dEQHUAjpn5JV1zCPtOIVWGQjLBrqhnzQa/hrU2XrZwSyBa6XjEBzfXzw== + dependencies: + jasmine-core "^3.6.0" + +karma-source-map-support@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz#58526ceccf7e8730e56effd97a4de8d712ac0d6b" + integrity sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A== + dependencies: + source-map-support "^0.5.5" + +karma@~5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/karma/-/karma-5.1.1.tgz#4e472c1e5352d73edbd2090726afdb01d7869d72" + integrity sha512-xAlOr5PMqUbiKXSv5PCniHWV3aiwj6wIZ0gUVcwpTCPVQm/qH2WAMFWxtnpM6KJqhkRWrIpovR4Rb0rn8GtJzQ== + dependencies: + body-parser "^1.19.0" + braces "^3.0.2" + chokidar "^3.0.0" + colors "^1.4.0" + connect "^3.7.0" + di "^0.0.1" + dom-serialize "^2.2.1" + flatted "^2.0.2" + glob "^7.1.6" + graceful-fs "^4.2.4" + http-proxy "^1.18.1" + isbinaryfile "^4.0.6" + lodash "^4.17.15" + log4js "^6.2.1" + mime "^2.4.5" + minimatch "^3.0.4" + qjobs "^1.2.0" + range-parser "^1.2.1" + rimraf "^3.0.2" + socket.io "^2.3.0" + source-map "^0.6.1" + tmp "0.2.1" + ua-parser-js "0.7.21" + yargs "^15.3.1" + +killable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" + integrity sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg== + +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +klona@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0" + integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA== + +leaflet-editable@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/leaflet-editable/-/leaflet-editable-1.2.0.tgz#a3a01001764ba58ea923381ee6a1c814708a0b84" + integrity sha512-wG11JwpL8zqIbypTop6xCRGagMuWw68ihYu4uqrqc5Ep0wnEJeyob7NB2Rt5t74Oih4rwJ3OfwaGbzdowOGfYQ== + +leaflet-polylinedecorator@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/leaflet-polylinedecorator/-/leaflet-polylinedecorator-1.6.0.tgz#9ef79fd1b5302d67b72efe959a8ecd2553f27266" + integrity sha1-nvef0bUwLWe3Lv6Vmo7NJVPycmY= + dependencies: + leaflet-rotatedmarker "^0.2.0" + +leaflet-providers@^1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/leaflet-providers/-/leaflet-providers-1.10.2.tgz#763c8e6655f26caf1afe3a1ef4add6c3e32de663" + integrity sha512-1l867LObxwuFBeyPeBewip8PAXKOnvEoujq4/9y2TKTiZNHH76ksBD6dfktGjgUrOF+IdjsGHkpASPE+v2DQLw== + +leaflet-rotatedmarker@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/leaflet-rotatedmarker/-/leaflet-rotatedmarker-0.2.0.tgz#4467f49f98d1bfd56959bd9c6705203dd2601277" + integrity sha1-RGf0n5jRv9VpWb2cZwUgPdJgEnc= + +leaflet.gridlayer.googlemutant@0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/leaflet.gridlayer.googlemutant/-/leaflet.gridlayer.googlemutant-0.10.0.tgz#1be79c9f2e341b5062058985ba77602e0d29eedc" + integrity sha512-UgB90Gl2PzEnvmkc0FrUlBQl6JcYAhHe0GsUmJ7huMo7LHKYDQd4z8EsJKzM4iUsd4A8ZizTRx/M+E91eDiOJA== + +leaflet.markercluster@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/leaflet.markercluster/-/leaflet.markercluster-1.4.1.tgz#b53f2c4f2ca7306ddab1dbb6f1861d5e8aa6c5e5" + integrity sha512-ZSEpE/EFApR0bJ1w/dUGwTSUvWlpalKqIzkaYdYB7jaftQA/Y2Jav+eT4CMtEYFj+ZK4mswP13Q2acnPBnhGOw== + +leaflet@^1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.7.1.tgz#10d684916edfe1bf41d688a3b97127c0322a2a19" + integrity sha512-/xwPEBidtg69Q3HlqPdU3DnrXQOvQU/CCHA1tcDQVzOwm91YMYaILjNp7L4Eaw5Z4sOYdbBz6koWyibppd8Zqw== + +less-loader@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/less-loader/-/less-loader-6.2.0.tgz#8b26f621c155b342eefc24f5bd6e9dc40c42a719" + integrity sha512-Cl5h95/Pz/PWub/tCBgT1oNMFeH1WTD33piG80jn5jr12T4XbxZcjThwNXDQ7AG649WEynuIzO4b0+2Tn9Qolg== + dependencies: + clone "^2.1.2" + less "^3.11.3" + loader-utils "^2.0.0" + schema-utils "^2.7.0" + +less@^3.11.3: + version "3.12.2" + resolved "https://registry.yarnpkg.com/less/-/less-3.12.2.tgz#157e6dd32a68869df8859314ad38e70211af3ab4" + integrity sha512-+1V2PCMFkL+OIj2/HrtrvZw0BC0sYLMICJfbQjuj/K8CEnlrFX6R5cKKgzzttsZDHyxQNL1jqMREjKN3ja/E3Q== + dependencies: + tslib "^1.10.0" + optionalDependencies: + errno "^0.1.1" + graceful-fs "^4.1.2" + image-size "~0.5.0" + make-dir "^2.1.0" + mime "^1.4.1" + native-request "^1.0.5" + source-map "~0.6.0" + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +levenary@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/levenary/-/levenary-1.1.1.tgz#842a9ee98d2075aa7faeedbe32679e9205f46f77" + integrity sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ== + dependencies: + leven "^3.1.0" + +license-webpack-plugin@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-2.3.0.tgz#c00f70d5725ba0408de208acb9e66612cc2eceda" + integrity sha512-JK/DXrtN6UeYQSgkg5q1+pgJ8aiKPL9tnz9Wzw+Ikkf+8mJxG56x6t8O+OH/tAeF/5NREnelTEMyFtbJNkjH4w== + dependencies: + "@types/webpack-sources" "^0.1.5" + webpack-sources "^1.2.0" + +lie@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" + integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== + dependencies: + immediate "~3.0.5" + +loader-runner@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" + integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== + +loader-utils@1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" + integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== + dependencies: + big.js "^5.2.2" + emojis-list "^2.0.0" + json5 "^1.0.1" + +loader-utils@2.0.0, loader-utils@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0" + integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + +loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" + integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^1.0.1" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= + +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= + +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= + +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= + +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= + +lodash@^4.0.1, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19: + version "4.17.20" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" + integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== + +log-symbols@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" + integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== + dependencies: + chalk "^4.0.0" + +log4js@^6.2.1: + version "6.3.0" + resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.3.0.tgz#10dfafbb434351a3e30277a00b9879446f715bcb" + integrity sha512-Mc8jNuSFImQUIateBFwdOQcmC6Q5maU0VVvdC2R6XMb66/VnT+7WS4D/0EeNMZu1YODmJe5NIn2XftCzEocUgw== + dependencies: + date-format "^3.0.0" + debug "^4.1.1" + flatted "^2.0.1" + rfdc "^1.1.4" + streamroller "^2.2.4" + +loglevel@^1.6.8: + version "1.7.0" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.0.tgz#728166855a740d59d38db01cf46f042caa041bb0" + integrity sha512-i2sY04nal5jDcagM3FMfG++T69GEEM8CYuOfeOIvmXzOIcwE9a/CJPR0MFM97pYMj/u10lzz7/zd7+qwhrBTqQ== + +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lru-cache@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" + integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +magic-string@0.25.7, magic-string@^0.25.0: + version "0.25.7" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" + integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== + dependencies: + sourcemap-codec "^1.4.4" + +make-dir@^2.0.0, make-dir@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" + integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== + dependencies: + pify "^4.0.1" + semver "^5.6.0" + +make-dir@^3.0.0, make-dir@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +make-fetch-happen@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-5.0.2.tgz#aa8387104f2687edca01c8687ee45013d02d19bd" + integrity sha512-07JHC0r1ykIoruKO8ifMXu+xEU8qOXDFETylktdug6vJDACnP+HKevOu3PXyNPzFyTSlz8vrBYlBO1JZRe8Cag== + dependencies: + agentkeepalive "^3.4.1" + cacache "^12.0.0" + http-cache-semantics "^3.8.1" + http-proxy-agent "^2.1.0" + https-proxy-agent "^2.2.3" + lru-cache "^5.1.1" + mississippi "^3.0.0" + node-fetch-npm "^2.0.2" + promise-retry "^1.1.1" + socks-proxy-agent "^4.0.0" + ssri "^6.0.0" + +make-plural@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/make-plural/-/make-plural-4.3.0.tgz#f23de08efdb0cac2e0c9ba9f315b0dff6b4c2735" + integrity sha512-xTYd4JVHpSCW+aqDof6w/MebaMVNTVYBZhbB/vi513xXdiPT92JMVCo0Jq8W2UZnzYRFeVbQiQ+I25l13JuKvA== + optionalDependencies: + minimist "^1.2.0" + +map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= + dependencies: + object-visit "^1.0.0" + +material-design-icons@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/material-design-icons/-/material-design-icons-3.0.1.tgz#9a71c48747218ebca51e51a66da682038cdcb7bf" + integrity sha1-mnHEh0chjrylHlGmbaaCA4zct78= + +md5.js@^1.3.4: + version "1.3.5" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" + integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + +mdn-data@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" + integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== + +mdn-data@2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978" + integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +memory-fs@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" + integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + +memory-fs@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c" + integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA== + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + +merge-source-map@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646" + integrity sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw== + dependencies: + source-map "^0.6.1" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +messageformat-formatters@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/messageformat-formatters/-/messageformat-formatters-2.0.1.tgz#0492c1402a48775f751c9b17c0354e92be012b08" + integrity sha512-E/lQRXhtHwGuiQjI7qxkLp8AHbMD5r2217XNe/SREbBlSawe0lOqsFb7rflZJmlQFSULNLIqlcjjsCPlB3m3Mg== + +messageformat-parser@^4.1.2: + version "4.1.3" + resolved "https://registry.yarnpkg.com/messageformat-parser/-/messageformat-parser-4.1.3.tgz#b824787f57fcda7d50769f5b63e8d4fda68f5b9e" + integrity sha512-2fU3XDCanRqeOCkn7R5zW5VQHWf+T3hH65SzuqRvjatBK7r4uyFa5mEX+k6F9Bd04LVM5G4/BHBTUJsOdW7uyg== + +messageformat@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/messageformat/-/messageformat-2.3.0.tgz#de263c49029d5eae65d7ee25e0754f57f425ad91" + integrity sha512-uTzvsv0lTeQxYI2y1NPa1lItL5VRI8Gb93Y2K2ue5gBPyrbJxfDi/EYWxh2PKv5yO42AJeeqblS9MJSh/IEk4w== + dependencies: + make-plural "^4.3.0" + messageformat-formatters "^2.0.1" + messageformat-parser "^4.1.2" + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + +micromatch@^3.1.10, micromatch@^3.1.4: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + +micromatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" + integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + dependencies: + braces "^3.0.1" + picomatch "^2.0.5" + +miller-rabin@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" + integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + +mime-db@1.44.0: + version "1.44.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" + integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== + +"mime-db@>= 1.43.0 < 2": + version "1.45.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea" + integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w== + +mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: + version "2.1.27" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" + integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== + dependencies: + mime-db "1.44.0" + +mime@1.6.0, mime@^1.4.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mime@^2.4.4, mime@^2.4.5: + version "2.4.6" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" + integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +mini-css-extract-plugin@0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.10.0.tgz#a0e6bfcad22a9c73f6c882a3c7557a98e2d3d27d" + integrity sha512-QgKgJBjaJhxVPwrLNqqwNS0AGkuQQ31Hp4xGXEK/P7wehEg6qmNtReHKai3zRXqY60wGVWLYcOMJK2b98aGc3A== + dependencies: + loader-utils "^1.1.0" + normalize-url "1.9.1" + schema-utils "^1.0.0" + webpack-sources "^1.1.0" + +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= + +minimatch@3.0.4, minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.0, minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +minipass-collect@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" + integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== + dependencies: + minipass "^3.0.0" + +minipass-flush@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" + integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== + dependencies: + minipass "^3.0.0" + +minipass-pipeline@^1.2.2: + version "1.2.4" + resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== + dependencies: + minipass "^3.0.0" + +minipass@^2.3.5, minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" + integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== + dependencies: + safe-buffer "^5.1.2" + yallist "^3.0.0" + +minipass@^3.0.0, minipass@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd" + integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg== + dependencies: + yallist "^4.0.0" + +minizlib@^1.2.1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" + integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q== + dependencies: + minipass "^2.9.0" + +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mississippi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" + integrity sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA== + dependencies: + concat-stream "^1.5.0" + duplexify "^3.4.2" + end-of-stream "^1.1.0" + flush-write-stream "^1.0.0" + from2 "^2.1.0" + parallel-transform "^1.1.0" + pump "^3.0.0" + pumpify "^1.3.3" + stream-each "^1.1.0" + through2 "^2.0.0" + +mixin-deep@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + +mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + +mkdirp@^1.0.3, mkdirp@^1.0.4, mkdirp@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +moment-timezone@*, moment-timezone@^0.5.31: + version "0.5.31" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.31.tgz#9c40d8c5026f0c7ab46eda3d63e49c155148de05" + integrity sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA== + dependencies: + moment ">= 2.9.0" + +"moment@>= 2.9.0", moment@^2.29.1: + version "2.29.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" + integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== + +mousetrap@1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.3.tgz#80fee49665fd478bccf072c9d46bdf1bfed3558a" + integrity sha512-bd+nzwhhs9ifsUrC2tWaSgm24/oo2c83zaRyZQF06hYA6sANfsXHtnZ19AbbbDXCDzeH5nZBSQ4NvCjgD62tJA== + +mousetrap@^1.6.0: + version "1.6.5" + resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.5.tgz#8a766d8c272b08393d5f56074e0b5ec183485bf9" + integrity sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA== + +move-concurrently@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" + integrity sha1-viwAX9oy4LKa8fBdfEszIUxwH5I= + dependencies: + aproba "^1.1.1" + copy-concurrently "^1.0.0" + fs-write-stream-atomic "^1.0.8" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.3" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + +ms@2.1.2, ms@^2.0.0, ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +multicast-dns-service-types@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" + integrity sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE= + +multicast-dns@^6.0.1: + version "6.2.3" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229" + integrity sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g== + dependencies: + dns-packet "^1.3.1" + thunky "^1.0.2" + +mute-stream@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + +nan@^2.12.1: + version "2.14.1" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" + integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== + +nanomatch@^1.2.9: + version "1.2.13" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +native-request@^1.0.5: + version "1.0.7" + resolved "https://registry.yarnpkg.com/native-request/-/native-request-1.0.7.tgz#ff742dc555b4c8f2f1c14b548639ba174e573856" + integrity sha512-9nRjinI9bmz+S7dgNtf4A70+/vPhnd+2krGpy4SUlADuOuSa24IDkNaZ+R/QT1wQ6S8jBdi6wE7fLekFZNfUpQ== + +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + +neo-async@^2.5.0, neo-async@^2.6.1, neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +next-tick@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" + integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= + +ngrx-store-freeze@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/ngrx-store-freeze/-/ngrx-store-freeze-0.2.4.tgz#146687cdf7e21244eb9003c7e883f2125847076c" + integrity sha512-90awpbbMa/x2H81eWWYniyli3LJ1PZU/FaztL10d9Rp/4kw2+97pqyLjdxSPxcOv9St//m9kfuWZ7gyoVDjgcg== + dependencies: + deep-freeze-strict "^1.1.1" + +ngx-clipboard@^13.0.1: + version "13.0.1" + resolved "https://registry.yarnpkg.com/ngx-clipboard/-/ngx-clipboard-13.0.1.tgz#393239d1a8f0ada8b99ef2d3ac80bc245c9ba3fe" + integrity sha512-e7QBsw7bX5ajhVR2++NAaYZYw90hKeEBlb006TW85WDUA3kmlrXpMDwOvVJuRewU6Nh+U1QiQMJq5a0ivk0zWg== + dependencies: + ngx-window-token ">=3.0.0" + +ngx-color-picker@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/ngx-color-picker/-/ngx-color-picker-10.1.0.tgz#19a6993a74bb3553024623b20ca6ebffd2c50f9c" + integrity sha512-Q3BILkQP+l+dcX0joe7+xuHDKydhGnG09sUG1FmlLZFYIEX4+AQqHULh+hUAci8kZlLZuOG+mB2Uq54QYadItw== + dependencies: + tslib "^2.0.0" + +ngx-daterangepicker-material@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/ngx-daterangepicker-material/-/ngx-daterangepicker-material-4.0.1.tgz#788c2e32eb4717629d4a0e60a60bf8d6430d8c13" + integrity sha512-0gY6DGU+dgYdmoAKrIJSB9xnDqBvj91Yis3II/ZJxxMfZVTG4qMMatck6w8FzdU+CYT64ArCq+Uwa6hJRHX6Nw== + dependencies: + tslib "^1.10.0" + +"ngx-flowchart@git://github.com/thingsboard/ngx-flowchart.git#master": + version "0.0.0" + resolved "git://github.com/thingsboard/ngx-flowchart.git#078bfd2cedeeab412dee922e8066a19be6da7278" + dependencies: + tslib "^1.13.0" + +ngx-hm-carousel@^2.0.0-rc.1: + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/ngx-hm-carousel/-/ngx-hm-carousel-2.0.0-rc.1.tgz#0ca7663da21a9ac60470b37d300637c29dc64b3a" + integrity sha512-vEMMFctBpQ+0hiwLo97IVmk39He9Gx2Xp6CudD5K1y4xxsuAkpqsGO5jh3KG3i2kMjP8OZY6plTJwIcATvBmLg== + dependencies: + hammerjs "^2.0.8" + resize-observer-polyfill "^1.5.1" + +ngx-sharebuttons@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/ngx-sharebuttons/-/ngx-sharebuttons-8.0.1.tgz#f48d86ec88360efd64d2beb0a8885b542ec4c2eb" + integrity sha512-oht6OZj+9KKyupI+MWmXI6g9jVi/INSUjy8Ym8eo3L7N4/UuWNnXkafj345OhucnWzye3LYdVUM08XZrVWnHnw== + dependencies: + tslib "^2.0.0" + +ngx-translate-messageformat-compiler@^4.8.0: + version "4.8.0" + resolved "https://registry.yarnpkg.com/ngx-translate-messageformat-compiler/-/ngx-translate-messageformat-compiler-4.8.0.tgz#557e39c33293865658669e99d1d748ab4a335298" + integrity sha512-A1Zg2sC0uCc1r8siT1M2DFcLhgjX6aEIu2g5NGnPh51KGtGqQqXHiXx2qCxz1U9sKMlYrvCZzfxzJ2kaCTtw+A== + dependencies: + tslib "^1.10.0" + +ngx-window-token@>=3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ngx-window-token/-/ngx-window-token-3.0.0.tgz#a5419befb133c6226e3e570737a247e66c825ab7" + integrity sha512-MDVIQB2SqFCbpoTqEXhO2529hsvpCYyw/iogjU6uskKqUKh79XVKWSMpRH9S1yTr0Ucgh8nFeNcpv2DnFdikJA== + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +node-fetch-npm@^2.0.2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/node-fetch-npm/-/node-fetch-npm-2.0.4.tgz#6507d0e17a9ec0be3bec516958a497cec54bf5a4" + integrity sha512-iOuIQDWDyjhv9qSDrj9aq/klt6F9z1p2otB3AV7v3zBDcL/x+OfGsvGQZZCcMZbUf4Ujw1xGNQkjvGnVT22cKg== + dependencies: + encoding "^0.1.11" + json-parse-better-errors "^1.0.0" + safe-buffer "^5.1.1" + +node-forge@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" + integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== + +node-libs-browser@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" + integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== + dependencies: + assert "^1.1.1" + browserify-zlib "^0.2.0" + buffer "^4.3.0" + console-browserify "^1.1.0" + constants-browserify "^1.0.0" + crypto-browserify "^3.11.0" + domain-browser "^1.1.1" + events "^3.0.0" + https-browserify "^1.0.0" + os-browserify "^0.3.0" + path-browserify "0.0.1" + process "^0.11.10" + punycode "^1.2.4" + querystring-es3 "^0.2.0" + readable-stream "^2.3.3" + stream-browserify "^2.0.1" + stream-http "^2.7.2" + string_decoder "^1.0.0" + timers-browserify "^2.0.4" + tty-browserify "0.0.0" + url "^0.11.0" + util "^0.11.0" + vm-browserify "^1.0.1" + +node-releases@^1.1.61: + version "1.1.61" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.61.tgz#707b0fca9ce4e11783612ba4a2fcba09047af16e" + integrity sha512-DD5vebQLg8jLCOzwupn954fbIiZht05DAZs0k2u8NStSe6h9XdsuIQL8hSRKYiU8WUQRznmSDrKGbv3ObOmC7g== + +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== + dependencies: + abbrev "1" + +normalize-package-data@^2.0.0, normalize-package-data@^2.4.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= + dependencies: + remove-trailing-separator "^1.0.1" + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= + +normalize-url@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c" + integrity sha1-LMDWazHqIwNkWENuNiDYWVTGbDw= + dependencies: + object-assign "^4.0.1" + prepend-http "^1.0.0" + query-string "^4.1.0" + sort-keys "^1.0.0" + +normalize-url@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" + integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg== + +npm-bundled@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.1.tgz#1edd570865a94cdb1bc8220775e29466c9fb234b" + integrity sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA== + dependencies: + npm-normalize-package-bin "^1.0.1" + +npm-install-checks@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/npm-install-checks/-/npm-install-checks-4.0.0.tgz#a37facc763a2fde0497ef2c6d0ac7c3fbe00d7b4" + integrity sha512-09OmyDkNLYwqKPOnbI8exiOZU2GVVmQp7tgez2BPi5OZC8M82elDAps7sxC4l//uSUtotWqoEIDwjRvWH4qz8w== + dependencies: + semver "^7.1.1" + +npm-normalize-package-bin@^1.0.0, npm-normalize-package-bin@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2" + integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== + +npm-package-arg@8.0.1, npm-package-arg@^8.0.0: + version "8.0.1" + resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-8.0.1.tgz#9d76f8d7667b2373ffda60bb801a27ef71e3e270" + integrity sha512-/h5Fm6a/exByzFSTm7jAyHbgOqErl9qSNJDQF32Si/ZzgwT2TERVxRxn3Jurw1wflgyVVAxnFR4fRHPM7y1ClQ== + dependencies: + hosted-git-info "^3.0.2" + semver "^7.0.0" + validate-npm-package-name "^3.0.0" + +npm-package-arg@^6.0.0, npm-package-arg@^6.1.0: + version "6.1.1" + resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-6.1.1.tgz#02168cb0a49a2b75bf988a28698de7b529df5cb7" + integrity sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg== + dependencies: + hosted-git-info "^2.7.1" + osenv "^0.1.5" + semver "^5.6.0" + validate-npm-package-name "^3.0.0" + +npm-packlist@^1.1.12: + version "1.4.8" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e" + integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A== + dependencies: + ignore-walk "^3.0.1" + npm-bundled "^1.0.1" + npm-normalize-package-bin "^1.0.1" + +npm-pick-manifest@6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-6.1.0.tgz#2befed87b0fce956790f62d32afb56d7539c022a" + integrity sha512-ygs4k6f54ZxJXrzT0x34NybRlLeZ4+6nECAIbr2i0foTnijtS1TJiyzpqtuUAJOps/hO0tNDr8fRV5g+BtRlTw== + dependencies: + npm-install-checks "^4.0.0" + npm-package-arg "^8.0.0" + semver "^7.0.0" + +npm-pick-manifest@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-3.0.2.tgz#f4d9e5fd4be2153e5f4e5f9b7be8dc419a99abb7" + integrity sha512-wNprTNg+X5nf+tDi+hbjdHhM4bX+mKqv6XmPh7B5eG+QY9VARfQPfCEH013H5GqfNj6ee8Ij2fg8yk0mzps1Vw== + dependencies: + figgy-pudding "^3.5.1" + npm-package-arg "^6.0.0" + semver "^5.4.1" + +npm-registry-fetch@^4.0.0: + version "4.0.7" + resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-4.0.7.tgz#57951bf6541e0246b34c9f9a38ab73607c9449d7" + integrity sha512-cny9v0+Mq6Tjz+e0erFAB+RYJ/AVGzkjnISiobqP8OWj9c9FLoZZu8/SPSKJWE17F1tk4018wfjV+ZbIbqC7fQ== + dependencies: + JSONStream "^1.3.4" + bluebird "^3.5.1" + figgy-pudding "^3.4.1" + lru-cache "^5.1.1" + make-fetch-happen "^5.0.0" + npm-package-arg "^6.1.0" + safe-buffer "^5.2.0" + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= + dependencies: + path-key "^2.0.0" + +nth-check@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" + integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== + dependencies: + boolbase "~1.0.0" + +num2fraction@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" + integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4= + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-assign@4.x, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +object-component@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291" + integrity sha1-8MaapQ78lbhmwYb0AKM3acsvEpE= + +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + +object-inspect@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" + integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== + +object-is@^1.0.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.3.tgz#2e3b9e65560137455ee3bd62aec4d90a2ea1cc81" + integrity sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.18.0-next.1" + +object-keys@^1.0.12, object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object-path@0.11.4: + version "0.11.4" + resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.4.tgz#370ae752fbf37de3ea70a861c23bba8915691949" + integrity sha1-NwrnUvvzfePqcKhhwju6iRVpGUk= + +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= + dependencies: + isobject "^3.0.0" + +object.assign@^4.1.0, object.assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.1.tgz#303867a666cdd41936ecdedfb1f8f3e32a478cdd" + integrity sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.18.0-next.0" + has-symbols "^1.0.1" + object-keys "^1.1.1" + +object.getownpropertydescriptors@^2.0.3, object.getownpropertydescriptors@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649" + integrity sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + +object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= + dependencies: + isobject "^3.0.1" + +object.values@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e" + integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + function-bind "^1.1.1" + has "^1.0.3" + +objectpath@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/objectpath/-/objectpath-2.0.0.tgz#c4463123fcf00469be8282a2ea51704fb9469cc1" + integrity sha512-IWH9JOBUJz4HHBtXm1qqwoPiDAB8Qp+ZBE4PpXsOlXVEnxGa+fAgfAZFwN6L1cUYvzPpFeJ1HsY1WAhoOqQq7Q== + +obuf@^1.0.0, obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +onetime@^5.1.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +open@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/open/-/open-7.2.0.tgz#212959bd7b0ce2e8e3676adc76e3cf2f0a2498b4" + integrity sha512-4HeyhxCvBTI5uBePsAdi55C5fmqnWZ2e2MlmvWi5KW5tdH5rxoiv/aMtbeVxKZc3eWkT1GymMnLG8XC4Rq4TDQ== + dependencies: + is-docker "^2.0.0" + is-wsl "^2.1.1" + +opn@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" + integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA== + dependencies: + is-wsl "^1.1.0" + +ora@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.0.0.tgz#4f0b34f2994877b49b452a707245ab1e9f6afccb" + integrity sha512-s26qdWqke2kjN/wC4dy+IQPBIMWBJlSU/0JZhk30ZDBLelW25rv66yutUWARMigpGPzcXHb+Nac5pNhN/WsARw== + dependencies: + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-spinners "^2.4.0" + is-interactive "^1.0.0" + log-symbols "^4.0.0" + mute-stream "0.0.8" + strip-ansi "^6.0.0" + wcwidth "^1.0.1" + +original@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f" + integrity sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg== + dependencies: + url-parse "^1.4.3" + +os-browserify@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" + integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= + +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= + +os-tmpdir@^1.0.0, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + +osenv@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" + integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= + +p-limit@^2.0.0, p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.1, p-limit@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.0.2.tgz#1664e010af3cadc681baafd3e2a437be7b0fb5fe" + integrity sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg== + dependencies: + p-try "^2.0.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-map@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" + integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== + +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + +p-retry@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-3.0.1.tgz#316b4c8893e2c8dc1cfa891f406c4b422bebf328" + integrity sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w== + dependencies: + retry "^0.12.0" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +pacote@9.5.12: + version "9.5.12" + resolved "https://registry.yarnpkg.com/pacote/-/pacote-9.5.12.tgz#1e11dd7a8d736bcc36b375a9804d41bb0377bf66" + integrity sha512-BUIj/4kKbwWg4RtnBncXPJd15piFSVNpTzY0rysSr3VnMowTYgkGKcaHrbReepAkjTr8lH2CVWRi58Spg2CicQ== + dependencies: + bluebird "^3.5.3" + cacache "^12.0.2" + chownr "^1.1.2" + figgy-pudding "^3.5.1" + get-stream "^4.1.0" + glob "^7.1.3" + infer-owner "^1.0.4" + lru-cache "^5.1.1" + make-fetch-happen "^5.0.0" + minimatch "^3.0.4" + minipass "^2.3.5" + mississippi "^3.0.0" + mkdirp "^0.5.1" + normalize-package-data "^2.4.0" + npm-normalize-package-bin "^1.0.0" + npm-package-arg "^6.1.0" + npm-packlist "^1.1.12" + npm-pick-manifest "^3.0.0" + npm-registry-fetch "^4.0.0" + osenv "^0.1.5" + promise-inflight "^1.0.1" + promise-retry "^1.1.1" + protoduck "^5.0.1" + rimraf "^2.6.2" + safe-buffer "^5.1.2" + semver "^5.6.0" + ssri "^6.0.1" + tar "^4.4.10" + unique-filename "^1.1.1" + which "^1.3.1" + +pako@~1.0.2, pako@~1.0.5: + version "1.0.11" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== + +parallel-transform@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc" + integrity sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg== + dependencies: + cyclist "^1.0.1" + inherits "^2.0.3" + readable-stream "^2.1.5" + +parse-asn1@^5.0.0, parse-asn1@^5.1.5: + version "5.1.6" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4" + integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw== + dependencies: + asn1.js "^5.2.0" + browserify-aes "^1.0.0" + evp_bytestokey "^1.0.0" + pbkdf2 "^3.0.3" + safe-buffer "^5.1.1" + +parse-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= + dependencies: + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + +parse5-htmlparser2-tree-adapter@6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6" + integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA== + dependencies: + parse5 "^6.0.1" + +parse5@6.0.1, parse5@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" + integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== + +parse5@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" + integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug== + +parseqs@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d" + integrity sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0= + dependencies: + better-assert "~1.0.0" + +parseqs@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.6.tgz#8e4bb5a19d1cdc844a08ac974d34e273afa670d5" + integrity sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w== + +parseuri@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a" + integrity sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo= + dependencies: + better-assert "~1.0.0" + +parseuri@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.6.tgz#e1496e829e3ac2ff47f39a4dd044b32823c4a25a" + integrity sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow== + +parseurl@~1.3.2, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= + +path-browserify@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" + integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== + +path-dirname@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-is-inside@^1.0.1, path-is-inside@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= + +path-key@^2.0.0, path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +pbkdf2@^3.0.3: + version "3.1.1" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.1.tgz#cb8724b0fada984596856d1a6ebafd3584654b94" + integrity sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg== + dependencies: + create-hash "^1.1.2" + create-hmac "^1.1.4" + ripemd160 "^2.0.1" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" + integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== + +pify@^2.0.0, pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= + +pkg-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" + integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== + dependencies: + find-up "^3.0.0" + +pkg-dir@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +pnp-webpack-plugin@1.6.4: + version "1.6.4" + resolved "https://registry.yarnpkg.com/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz#c9711ac4dc48a685dabafc86f8b6dd9f8df84149" + integrity sha512-7Wjy+9E3WwLOEL30D+m8TSTF7qJJUJLONBnwQp0518siuMxUQUbgZwssaFX+QKlZkjHZcw/IpZCt/H0srrntSg== + dependencies: + ts-pnp "^1.1.6" + +popper.js@1.16.1-lts: + version "1.16.1-lts" + resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1-lts.tgz#cf6847b807da3799d80ee3d6d2f90df8a3f50b05" + integrity sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA== + +portfinder@^1.0.26: + version "1.0.28" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778" + integrity sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA== + dependencies: + async "^2.6.2" + debug "^3.1.1" + mkdirp "^0.5.5" + +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= + +postcss-calc@^7.0.1: + version "7.0.5" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.5.tgz#f8a6e99f12e619c2ebc23cf6c486fdc15860933e" + integrity sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg== + dependencies: + postcss "^7.0.27" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.0.2" + +postcss-colormin@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-4.0.3.tgz#ae060bce93ed794ac71264f08132d550956bd381" + integrity sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw== + dependencies: + browserslist "^4.0.0" + color "^3.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-convert-values@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz#ca3813ed4da0f812f9d43703584e449ebe189a7f" + integrity sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ== + dependencies: + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-discard-comments@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz#1fbabd2c246bff6aaad7997b2b0918f4d7af4033" + integrity sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg== + dependencies: + postcss "^7.0.0" + +postcss-discard-duplicates@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz#3fe133cd3c82282e550fc9b239176a9207b784eb" + integrity sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ== + dependencies: + postcss "^7.0.0" + +postcss-discard-empty@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz#c8c951e9f73ed9428019458444a02ad90bb9f765" + integrity sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w== + dependencies: + postcss "^7.0.0" + +postcss-discard-overridden@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz#652aef8a96726f029f5e3e00146ee7a4e755ff57" + integrity sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg== + dependencies: + postcss "^7.0.0" + +postcss-import@12.0.1: + version "12.0.1" + resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-12.0.1.tgz#cf8c7ab0b5ccab5649024536e565f841928b7153" + integrity sha512-3Gti33dmCjyKBgimqGxL3vcV8w9+bsHwO5UrBawp796+jdardbcFl4RP5w/76BwNL7aGzpKstIfF9I+kdE8pTw== + dependencies: + postcss "^7.0.1" + postcss-value-parser "^3.2.3" + read-cache "^1.0.0" + resolve "^1.1.7" + +postcss-load-config@^2.0.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.1.2.tgz#c5ea504f2c4aef33c7359a34de3573772ad7502a" + integrity sha512-/rDeGV6vMUo3mwJZmeHfEDvwnTKKqQ0S7OHUi/kJvvtx3aWtyWG2/0ZWnzCt2keEclwN6Tf0DST2v9kITdOKYw== + dependencies: + cosmiconfig "^5.0.0" + import-cwd "^2.0.0" + +postcss-loader@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-3.0.0.tgz#6b97943e47c72d845fa9e03f273773d4e8dd6c2d" + integrity sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA== + dependencies: + loader-utils "^1.1.0" + postcss "^7.0.0" + postcss-load-config "^2.0.0" + schema-utils "^1.0.0" + +postcss-merge-longhand@^4.0.11: + version "4.0.11" + resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz#62f49a13e4a0ee04e7b98f42bb16062ca2549e24" + integrity sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw== + dependencies: + css-color-names "0.0.4" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + stylehacks "^4.0.0" + +postcss-merge-rules@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz#362bea4ff5a1f98e4075a713c6cb25aefef9a650" + integrity sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ== + dependencies: + browserslist "^4.0.0" + caniuse-api "^3.0.0" + cssnano-util-same-parent "^4.0.0" + postcss "^7.0.0" + postcss-selector-parser "^3.0.0" + vendors "^1.0.0" + +postcss-minify-font-values@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz#cd4c344cce474343fac5d82206ab2cbcb8afd5a6" + integrity sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg== + dependencies: + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-minify-gradients@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz#93b29c2ff5099c535eecda56c4aa6e665a663471" + integrity sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q== + dependencies: + cssnano-util-get-arguments "^4.0.0" + is-color-stop "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-minify-params@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz#6b9cef030c11e35261f95f618c90036d680db874" + integrity sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg== + dependencies: + alphanum-sort "^1.0.0" + browserslist "^4.0.0" + cssnano-util-get-arguments "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + uniqs "^2.0.0" + +postcss-minify-selectors@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz#e2e5eb40bfee500d0cd9243500f5f8ea4262fbd8" + integrity sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g== + dependencies: + alphanum-sort "^1.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-selector-parser "^3.0.0" + +postcss-modules-extract-imports@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e" + integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ== + dependencies: + postcss "^7.0.5" + +postcss-modules-local-by-default@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz#bb14e0cc78279d504dbdcbfd7e0ca28993ffbbb0" + integrity sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw== + dependencies: + icss-utils "^4.1.1" + postcss "^7.0.32" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.1.0" + +postcss-modules-scope@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee" + integrity sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ== + dependencies: + postcss "^7.0.6" + postcss-selector-parser "^6.0.0" + +postcss-modules-values@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10" + integrity sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg== + dependencies: + icss-utils "^4.0.0" + postcss "^7.0.6" + +postcss-normalize-charset@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz#8b35add3aee83a136b0471e0d59be58a50285dd4" + integrity sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g== + dependencies: + postcss "^7.0.0" + +postcss-normalize-display-values@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz#0dbe04a4ce9063d4667ed2be476bb830c825935a" + integrity sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ== + dependencies: + cssnano-util-get-match "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-positions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz#05f757f84f260437378368a91f8932d4b102917f" + integrity sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA== + dependencies: + cssnano-util-get-arguments "^4.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-repeat-style@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz#c4ebbc289f3991a028d44751cbdd11918b17910c" + integrity sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q== + dependencies: + cssnano-util-get-arguments "^4.0.0" + cssnano-util-get-match "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-string@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz#cd44c40ab07a0c7a36dc5e99aace1eca4ec2690c" + integrity sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA== + dependencies: + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-timing-functions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz#8e009ca2a3949cdaf8ad23e6b6ab99cb5e7d28d9" + integrity sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A== + dependencies: + cssnano-util-get-match "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-unicode@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz#841bd48fdcf3019ad4baa7493a3d363b52ae1cfb" + integrity sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg== + dependencies: + browserslist "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-url@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz#10e437f86bc7c7e58f7b9652ed878daaa95faae1" + integrity sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA== + dependencies: + is-absolute-url "^2.0.0" + normalize-url "^3.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-whitespace@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz#bf1d4070fe4fcea87d1348e825d8cc0c5faa7d82" + integrity sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA== + dependencies: + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-ordered-values@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz#0cf75c820ec7d5c4d280189559e0b571ebac0eee" + integrity sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw== + dependencies: + cssnano-util-get-arguments "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-reduce-initial@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz#7fd42ebea5e9c814609639e2c2e84ae270ba48df" + integrity sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA== + dependencies: + browserslist "^4.0.0" + caniuse-api "^3.0.0" + has "^1.0.0" + postcss "^7.0.0" + +postcss-reduce-transforms@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz#17efa405eacc6e07be3414a5ca2d1074681d4e29" + integrity sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg== + dependencies: + cssnano-util-get-match "^4.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-selector-parser@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz#b310f5c4c0fdaf76f94902bbaa30db6aa84f5270" + integrity sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA== + dependencies: + dot-prop "^5.2.0" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2: + version "6.0.4" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3" + integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw== + dependencies: + cssesc "^3.0.0" + indexes-of "^1.0.1" + uniq "^1.0.1" + util-deprecate "^1.0.2" + +postcss-svgo@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.2.tgz#17b997bc711b333bab143aaed3b8d3d6e3d38258" + integrity sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw== + dependencies: + is-svg "^3.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + svgo "^1.0.0" + +postcss-unique-selectors@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz#9446911f3289bfd64c6d680f073c03b1f9ee4bac" + integrity sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg== + dependencies: + alphanum-sort "^1.0.0" + postcss "^7.0.0" + uniqs "^2.0.0" + +postcss-value-parser@^3.0.0, postcss-value-parser@^3.2.3: + version "3.3.1" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" + integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== + +postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" + integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== + +postcss@7.0.21: + version "7.0.21" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.21.tgz#06bb07824c19c2021c5d056d5b10c35b989f7e17" + integrity sha512-uIFtJElxJo29QC753JzhidoAhvp/e/Exezkdhfmt8AymWT6/5B7W1WmponYWkHk2eg6sONyTch0A3nkMPun3SQ== + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + +postcss@7.0.32: + version "7.0.32" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d" + integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw== + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + +postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: + version "7.0.35" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24" + integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg== + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + +prepend-http@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" + integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= + +prettier@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.2.tgz#3050700dae2e4c8b67c4c3f666cdb8af405e1ce5" + integrity sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg== + +prismjs@^1.21.0: + version "1.21.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.21.0.tgz#36c086ec36b45319ec4218ee164c110f9fc015a3" + integrity sha512-uGdSIu1nk3kej2iZsLyDoJ7e9bnPzIgY0naW/HdknGj61zScaprVEVGHrPoXqI+M9sP0NDnTK2jpkvmldpuqDw== + optionalDependencies: + clipboard "^2.0.0" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= + +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= + +promise-retry@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-1.1.1.tgz#6739e968e3051da20ce6497fb2b50f6911df3d6d" + integrity sha1-ZznpaOMFHaIM5kl/srUPaRHfPW0= + dependencies: + err-code "^1.0.0" + retry "^0.10.0" + +prop-types@^15.5.10, prop-types@^15.6.2, prop-types@^15.7.2: + version "15.7.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" + integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.8.1" + +proto-list@~1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" + integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk= + +protoduck@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/protoduck/-/protoduck-5.0.1.tgz#03c3659ca18007b69a50fd82a7ebcc516261151f" + integrity sha512-WxoCeDCoCBY55BMvj4cAEjdVUFGRWed9ZxPlqTKYyw1nDDTQ4pqmnIMAGfJlg7Dx35uB/M+PHJPTmGOvaCaPTg== + dependencies: + genfun "^5.0.0" + +protractor@~7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/protractor/-/protractor-7.0.0.tgz#c3e263608bd72e2c2dc802b11a772711a4792d03" + integrity sha512-UqkFjivi4GcvUQYzqGYNe0mLzfn5jiLmO8w9nMhQoJRLhy2grJonpga2IWhI6yJO30LibWXJJtA4MOIZD2GgZw== + dependencies: + "@types/q" "^0.0.32" + "@types/selenium-webdriver" "^3.0.0" + blocking-proxy "^1.0.0" + browserstack "^1.5.1" + chalk "^1.1.3" + glob "^7.0.3" + jasmine "2.8.0" + jasminewd2 "^2.1.0" + q "1.4.1" + saucelabs "^1.5.0" + selenium-webdriver "3.6.0" + source-map-support "~0.4.0" + webdriver-js-extender "2.1.0" + webdriver-manager "^12.1.7" + yargs "^15.3.1" + +proxy-addr@~2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" + integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.9.1" + +prr@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" + integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= + +psl@^1.1.28: + version "1.8.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" + integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== + +public-encrypt@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" + integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== + dependencies: + bn.js "^4.1.0" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + parse-asn1 "^5.0.0" + randombytes "^2.0.1" + safe-buffer "^5.1.2" + +pump@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" + integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pumpify@^1.3.3: + version "1.5.1" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" + integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ== + dependencies: + duplexify "^3.6.0" + inherits "^2.0.3" + pump "^2.0.0" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= + +punycode@^1.2.4: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= + +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +q@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e" + integrity sha1-VXBbzZPF82c1MMLCy8DCs63cKG4= + +q@^1.1.2, q@^1.4.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= + +qjobs@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" + integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg== + +qs@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +query-string@^4.1.0: + version "4.3.4" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" + integrity sha1-u7aTucqRXCMlFbIosaArYJBD2+s= + dependencies: + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + +querystring-es3@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" + integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM= + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= + +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + +raf@^3.4.0, raf@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" + integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== + dependencies: + performance-now "^2.1.0" + +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +randomfill@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" + integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== + dependencies: + randombytes "^2.0.5" + safe-buffer "^5.1.0" + +range-parser@^1.2.1, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raphael@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/raphael/-/raphael-2.3.0.tgz#eabeb09dba861a1d4cee077eaafb8c53f3131f89" + integrity sha512-w2yIenZAQnp257XUWGni4bLMVxpUpcIl7qgxEgDIXtmSypYtlNxfXWpOBxs7LBTps5sDwhRnrToJrMUrivqNTQ== + dependencies: + eve-raphael "0.5.0" + +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== + dependencies: + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" + unpipe "1.0.0" + +raw-loader@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-4.0.1.tgz#14e1f726a359b68437e183d5a5b7d33a3eba6933" + integrity sha512-baolhQBSi3iNh1cglJjA0mYzga+wePk7vdEX//1dTFd+v4TsQlQE0jitJSNF1OIP82rdYulH7otaVmdlDaJ64A== + dependencies: + loader-utils "^2.0.0" + schema-utils "^2.6.5" + +rc-align@^4.0.0: + version "4.0.8" + resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-4.0.8.tgz#276c3f5dfadf0de4bb95392cb81568c9e947a668" + integrity sha512-2sRUkmB8z4UEXzaS+lDHzXMoR8HrtKH9nn2yHlHVNyUTnaucjMFbdEoCk+hO1g7cpIgW0MphG8i0EH2scSesfw== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "2.x" + dom-align "^1.7.0" + rc-util "^5.3.0" + resize-observer-polyfill "^1.5.1" + +rc-animate@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/rc-animate/-/rc-animate-3.1.1.tgz#defdd863f56816c222534e4dc68feddecd081386" + integrity sha512-8wg2Zg3EETy0k/9kYuis30NJNQg1D6/WSQwnCiz6SvyxQXNet/rVraRz3bPngwY6rcU2nlRvoShiYOorXyF7Sg== + dependencies: + "@ant-design/css-animation" "^1.7.2" + classnames "^2.2.6" + raf "^3.4.0" + rc-util "^4.15.3" + +rc-motion@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/rc-motion/-/rc-motion-1.1.2.tgz#07212f1b96c715b8245ec121339146c4a9b0968c" + integrity sha512-YC/E7SSWKBFakYg4PENhSRWD4ZLDqkI7FKmutJcrMewZ91/ZIWfoZSDvPaBdKO0hsFrrzWepFhXQIq0FNnCMWA== + dependencies: + "@babel/runtime" "^7.11.1" + classnames "^2.2.1" + raf "^3.4.1" + rc-util "^5.0.6" + +rc-select@~10.5.1: + version "10.5.1" + resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-10.5.1.tgz#4d4c5d4f8d2fd3b7e3dccf74e4c43142b2979247" + integrity sha512-fZraoNNhjUmDJccfk6VYgrgHBWFmHWh/NJZh2Xttcm/usOolYI1RjO9ikP4QGhzlJBFtnTLH2pE2nchjn3TCXA== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "2.x" + rc-animate "^3.0.0" + rc-trigger "^4.3.0" + rc-util "^5.0.1" + rc-virtual-list "^1.1.2" + warning "^4.0.3" + +rc-trigger@^4.3.0: + version "4.4.3" + resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-4.4.3.tgz#ed449cd6cce446555bc57f4394229c5c7154f4b0" + integrity sha512-yq/WyuiPwxd2q6jy+VPyy0GUCRFJ2eFqAaCwPE27AOftXeIupOcJ/2t1wakSq63cfk7qtzev5DKHUAjb8LOJCw== + dependencies: + "@babel/runtime" "^7.11.2" + classnames "^2.2.6" + raf "^3.4.1" + rc-align "^4.0.0" + rc-motion "^1.0.0" + rc-util "^5.0.1" + +rc-util@^4.15.3: + version "4.21.1" + resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-4.21.1.tgz#88602d0c3185020aa1053d9a1e70eac161becb05" + integrity sha512-Z+vlkSQVc1l8O2UjR3WQ+XdWlhj5q9BMQNLk2iOBch75CqPfrJyGtcWMcnhRlNuDu0Ndtt4kLVO8JI8BrABobg== + dependencies: + add-dom-event-listener "^1.1.0" + prop-types "^15.5.10" + react-is "^16.12.0" + react-lifecycles-compat "^3.0.4" + shallowequal "^1.1.0" + +rc-util@^5.0.0, rc-util@^5.0.1, rc-util@^5.0.6, rc-util@^5.3.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.4.0.tgz#688eaeecfdae9dae2bfdf10bedbe884591dba004" + integrity sha512-kXDn1JyLJTAWLBFt+fjkTcUtXhxKkipQCobQmxIEVrX62iXgo24z8YKoWehWfMxPZFPE+RXqrmEu9j5kHz/Lrg== + dependencies: + react-is "^16.12.0" + shallowequal "^1.1.0" + +rc-virtual-list@^1.1.2: + version "1.1.6" + resolved "https://registry.yarnpkg.com/rc-virtual-list/-/rc-virtual-list-1.1.6.tgz#b255baf9aacde149a8893324e6307214094f4c0a" + integrity sha512-u3+izqWL8p8bQy8nYH48qWpiGyxR/ye8D2k0zJlXmfYeL55/xh83YrzHqiDzO78uj0Ewag3nXDA0JTVrYO7ygQ== + dependencies: + classnames "^2.2.6" + raf "^3.4.1" + rc-util "^5.0.0" + +react-ace@^9.1.4: + version "9.1.4" + resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-9.1.4.tgz#7c45c361aa5fe1efa3313fa876bce30aa64a244f" + integrity sha512-4DBWvElbVR3WhsA2HhQ524K9Yoa/J/sjuBV9NUZ+yar3Q4BGJRTnhY6pM0INffH1IkBZHKIOyz34XHjc7RNTpw== + dependencies: + ace-builds "^1.4.6" + diff-match-patch "^1.0.4" + lodash.get "^4.4.2" + lodash.isequal "^4.5.0" + prop-types "^15.7.2" + +react-dom@^16.13.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f" + integrity sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + scheduler "^0.19.1" + +react-dropzone@^11.2.0: + version "11.2.0" + resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-11.2.0.tgz#4e54fa3479e6b6bb93f67914e4a27f1260807fdb" + integrity sha512-S/qaXQHCCg7MVlcrhqd05MLC6DupITLUB0CFn3iCLs6OTjzxdGDF1WTktTe5Jyq8jZdxYfMHNUZOHL0mg+K0Dw== + dependencies: + attr-accept "^2.0.0" + file-selector "^0.1.12" + prop-types "^15.7.2" + +react-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + +react-transition-group@^4.0.0, react-transition-group@^4.4.0: + version "4.4.1" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9" + integrity sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + +react@~16.14.0: + version "16.14.0" + resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d" + integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + +reactcss@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/reactcss/-/reactcss-1.2.3.tgz#c00013875e557b1cf0dfd9a368a1c3dab3b548dd" + integrity sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A== + dependencies: + lodash "^4.0.1" + +read-cache@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" + integrity sha1-5mTvMRYRZsl1HNvo28+GtftY93Q= + dependencies: + pify "^2.3.0" + +read-package-json@^2.0.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-2.1.2.tgz#6992b2b66c7177259feb8eaac73c3acd28b9222a" + integrity sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA== + dependencies: + glob "^7.1.1" + json-parse-even-better-errors "^2.3.0" + normalize-package-data "^2.0.0" + npm-normalize-package-bin "^1.0.0" + +read-package-tree@5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/read-package-tree/-/read-package-tree-5.3.1.tgz#a32cb64c7f31eb8a6f31ef06f9cedf74068fe636" + integrity sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw== + dependencies: + read-package-json "^2.0.0" + readdir-scoped-modules "^1.0.0" + util-promisify "^2.1.0" + +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.0.6, readable-stream@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdir-scoped-modules@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz#8d45407b4f870a0dcaebc0e28670d18e74514309" + integrity sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw== + dependencies: + debuglog "^1.0.1" + dezalgo "^1.0.0" + graceful-fs "^4.1.2" + once "^1.3.0" + +readdirp@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" + integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== + dependencies: + graceful-fs "^4.1.11" + micromatch "^3.1.10" + readable-stream "^2.0.2" + +readdirp@~3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada" + integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ== + dependencies: + picomatch "^2.2.1" + +reflect-metadata@^0.1.2: + version "0.1.13" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" + integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== + +regenerate-unicode-properties@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec" + integrity sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA== + dependencies: + regenerate "^1.4.0" + +regenerate@^1.4.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.1.tgz#cad92ad8e6b591773485fbe05a485caf4f457e6f" + integrity sha512-j2+C8+NtXQgEKWk49MMP5P/u2GhnahTtVkRIHr5R5lVRlbKvmQ+oS+A5aLKWp2ma5VkT8sh6v+v4hbH0YHR66A== + +regenerator-runtime@0.13.7, regenerator-runtime@^0.13.4: + version "0.13.7" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" + integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== + +regenerator-transform@^0.14.2: + version "0.14.5" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4" + integrity sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw== + dependencies: + "@babel/runtime" "^7.8.4" + +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== + dependencies: + extend-shallow "^3.0.2" + safe-regex "^1.1.0" + +regex-parser@2.2.10: + version "2.2.10" + resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.2.10.tgz#9e66a8f73d89a107616e63b39d4deddfee912b37" + integrity sha512-8t6074A68gHfU8Neftl0Le6KTDwfGAj7IyjPIMSfikI2wJUTHDMaIq42bUsfVnj8mhx0R+45rdUXHGpN164avA== + +regexp.prototype.flags@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75" + integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + +regexpu-core@^4.7.0: + version "4.7.1" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6" + integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ== + dependencies: + regenerate "^1.4.0" + regenerate-unicode-properties "^8.2.0" + regjsgen "^0.5.1" + regjsparser "^0.6.4" + unicode-match-property-ecmascript "^1.0.4" + unicode-match-property-value-ecmascript "^1.2.0" + +regjsgen@^0.5.1: + version "0.5.2" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733" + integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A== + +regjsparser@^0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.4.tgz#a769f8684308401a66e9b529d2436ff4d0666272" + integrity sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw== + dependencies: + jsesc "~0.5.0" + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= + +repeat-element@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" + integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== + +repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= + +request@^2.87.0, request@^2.88.2: + version "2.88.2" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" + integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= + +resize-observer-polyfill@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + +resolve-cwd@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" + integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo= + dependencies: + resolve-from "^3.0.0" + +resolve-from@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" + integrity sha1-six699nWiBvItuZTM17rywoYh0g= + +resolve-url-loader@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-3.1.1.tgz#28931895fa1eab9be0647d3b2958c100ae3c0bf0" + integrity sha512-K1N5xUjj7v0l2j/3Sgs5b8CjrrgtC70SmdCuZiJ8tSyb5J+uk3FoeZ4b7yTnH6j7ngI+Bc5bldHJIa8hYdu2gQ== + dependencies: + adjust-sourcemap-loader "2.0.0" + camelcase "5.3.1" + compose-function "3.0.3" + convert-source-map "1.7.0" + es6-iterator "2.0.3" + loader-utils "1.2.3" + postcss "7.0.21" + rework "1.0.1" + rework-visit "1.0.0" + source-map "0.6.1" + +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= + +resolve@^1.1.7, resolve@^1.10.0, resolve@^1.3.2, resolve@^1.8.1: + version "1.17.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" + integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== + dependencies: + path-parse "^1.0.6" + +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== + +retry@^0.10.0: + version "0.10.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4" + integrity sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q= + +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rework-visit@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/rework-visit/-/rework-visit-1.0.0.tgz#9945b2803f219e2f7aca00adb8bc9f640f842c9a" + integrity sha1-mUWygD8hni96ygCtuLyfZA+ELJo= + +rework@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/rework/-/rework-1.0.1.tgz#30806a841342b54510aa4110850cd48534144aa7" + integrity sha1-MIBqhBNCtUUQqkEQhQzUhTQUSqc= + dependencies: + convert-source-map "^0.3.3" + css "^2.0.0" + +rfdc@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.4.tgz#ba72cc1367a0ccd9cf81a870b3b58bd3ad07f8c2" + integrity sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug== + +rgb-regex@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1" + integrity sha1-wODWiC3w4jviVKR16O3UGRX+rrE= + +rgba-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" + integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= + +rifm@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/rifm/-/rifm-0.7.0.tgz#debe951a9c83549ca6b33e5919f716044c2230be" + integrity sha512-DSOJTWHD67860I5ojetXdEQRIBvF6YcpNe53j0vn1vp9EUb9N80EiZTxgP+FkDKorWC8PZw052kTF4C1GOivCQ== + dependencies: + "@babel/runtime" "^7.3.1" + +rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +rimraf@^2.2.8, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" + integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + +rollup@2.26.5: + version "2.26.5" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.26.5.tgz#5562ec36fcba3eed65cfd630bd78e037ad0e0307" + integrity sha512-rCyFG3ZtQdnn9YwfuAVH0l/Om34BdO5lwCA0W6Hq+bNB21dVEBbCRxhaHOmu1G7OBFDWytbzAC104u7rxHwGjA== + optionalDependencies: + fsevents "~2.1.2" + +run-async@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== + +run-parallel@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" + integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== + +run-queue@^1.0.0, run-queue@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" + integrity sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec= + dependencies: + aproba "^1.1.1" + +rxjs@6.6.2: + version "6.6.2" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.2.tgz#8096a7ac03f2cc4fe5860ef6e572810d9e01c0d2" + integrity sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg== + dependencies: + tslib "^1.9.0" + +rxjs@^6.5.3, rxjs@^6.6.0, rxjs@^6.6.3: + version "6.6.3" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552" + integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ== + dependencies: + tslib "^1.9.0" + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= + dependencies: + ret "~0.1.10" + +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@^2.1.2, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sass-loader@10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.0.1.tgz#10c0364d8034f22fee25ddcc9eded20f99bbe3b4" + integrity sha512-b2PSldKVTS3JcFPHSrEXh3BeAfR7XknGiGCAO5aHruR3Pf3kqLP3Gb2ypXLglRrAzgZkloNxLZ7GXEGDX0hBUQ== + dependencies: + klona "^2.0.3" + loader-utils "^2.0.0" + neo-async "^2.6.2" + schema-utils "^2.7.0" + semver "^7.3.2" + +sass@1.26.10: + version "1.26.10" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.26.10.tgz#851d126021cdc93decbf201d1eca2a20ee434760" + integrity sha512-bzN0uvmzfsTvjz0qwccN1sPm2HxxpNI/Xa+7PlUEMS+nQvbyuEK7Y0qFqxlPHhiNHb1Ze8WQJtU31olMObkAMw== + dependencies: + chokidar ">=2.0.0 <4.0.0" + +saucelabs@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/saucelabs/-/saucelabs-1.5.0.tgz#9405a73c360d449b232839919a86c396d379fd9d" + integrity sha512-jlX3FGdWvYf4Q3LFfFWS1QvPg3IGCGWxIc8QBFdPTbpTJnt/v17FHXYVAn7C8sHf1yUXo2c7yIM0isDryfYtHQ== + dependencies: + https-proxy-agent "^2.2.1" + +sax@>=0.6.0, sax@~1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + +scheduler@^0.19.1: + version "0.19.1" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196" + integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + +schema-inspector@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/schema-inspector/-/schema-inspector-1.7.0.tgz#b3f8b97fc26ba930ef16cd7b8fcf77201ce468db" + integrity sha512-Cj4XP6O3QfDhOq7bIPpz3Ev+sjR++nqFsIggBVIk/8axqFc2p+XSwNBWih9Ut/p8k36f1uCyXB+TzumZUsxVBQ== + dependencies: + async "~2.6.3" + +schema-utils@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" + integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g== + dependencies: + ajv "^6.1.0" + ajv-errors "^1.0.0" + ajv-keywords "^3.1.0" + +schema-utils@^2.6.5, schema-utils@^2.6.6, schema-utils@^2.7.0, schema-utils@^2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" + integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== + dependencies: + "@types/json-schema" "^7.0.5" + ajv "^6.12.4" + ajv-keywords "^3.5.2" + +screenfull@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-5.0.2.tgz#b9acdcf1ec676a948674df5cd0ff66b902b0bed7" + integrity sha512-cCF2b+L/mnEiORLN5xSAz6H3t18i2oHh9BA8+CQlAh5DRw2+NFAGQJOSYbcGw8B2k04g/lVvFcfZ83b3ysH5UQ== + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= + +select@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" + integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0= + +selenium-webdriver@3.6.0, selenium-webdriver@^3.0.1: + version "3.6.0" + resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz#2ba87a1662c020b8988c981ae62cb2a01298eafc" + integrity sha512-WH7Aldse+2P5bbFBO4Gle/nuQOdVwpHMTL6raL3uuBj/vPG07k6uzt3aiahu352ONBr5xXh0hDlM3LhtXPOC4Q== + dependencies: + jszip "^3.1.3" + rimraf "^2.5.4" + tmp "0.0.30" + xml2js "^0.4.17" + +selfsigned@^1.10.7: + version "1.10.8" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.8.tgz#0d17208b7d12c33f8eac85c41835f27fc3d81a30" + integrity sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w== + dependencies: + node-forge "^0.10.0" + +semver-dsl@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/semver-dsl/-/semver-dsl-1.0.1.tgz#d3678de5555e8a61f629eed025366ae5f27340a0" + integrity sha1-02eN5VVeimH2Ke7QJTZq5fJzQKA= + dependencies: + semver "^5.3.0" + +semver-intersect@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/semver-intersect/-/semver-intersect-1.4.0.tgz#bdd9c06bedcdd2fedb8cd352c3c43ee8c61321f3" + integrity sha512-d8fvGg5ycKAq0+I6nfWeCx6ffaWJCsBYU0H2Rq56+/zFePYfT8mXkB3tWBSjR5BerkHNZ5eTPIk1/LBYas35xQ== + dependencies: + semver "^5.0.0" + +"semver@2 || 3 || 4 || 5", semver@^5.0.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +semver@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" + integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== + +semver@7.3.2, semver@^7.0.0, semver@^7.1.1, semver@^7.3.2: + version "7.3.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" + integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== + +semver@^6.0.0, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +send@0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" + integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.7.2" + mime "1.6.0" + ms "2.1.1" + on-finished "~2.3.0" + range-parser "~1.2.1" + statuses "~1.5.0" + +serialize-javascript@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" + integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== + dependencies: + randombytes "^2.1.0" + +serialize-javascript@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4" + integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA== + dependencies: + randombytes "^2.1.0" + +serve-index@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + integrity sha1-03aNabHn2C5c4FD/9bRTvqEqkjk= + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + +serve-static@1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" + integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.17.1" + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + +set-immediate-shim@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" + integrity sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E= + +set-value@^2.0.0, set-value@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" + integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + +setimmediate@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + +sha.js@^2.4.0, sha.js@^2.4.8: + version "2.4.11" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" + integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +shallowequal@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" + integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + +sigmund@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" + integrity sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA= + +signal-exit@^3.0.0, signal-exit@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" + integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== + +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= + dependencies: + is-arrayish "^0.3.1" + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +smart-buffer@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.1.0.tgz#91605c25d91652f4661ea69ccf45f1b331ca21ba" + integrity sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw== + +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" + +socket.io-adapter@~1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz#ab3f0d6f66b8fc7fca3959ab5991f82221789be9" + integrity sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g== + +socket.io-client@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.3.0.tgz#14d5ba2e00b9bcd145ae443ab96b3f86cbcc1bb4" + integrity sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA== + dependencies: + backo2 "1.0.2" + base64-arraybuffer "0.1.5" + component-bind "1.0.0" + component-emitter "1.2.1" + debug "~4.1.0" + engine.io-client "~3.4.0" + has-binary2 "~1.0.2" + has-cors "1.1.0" + indexof "0.0.1" + object-component "0.0.3" + parseqs "0.0.5" + parseuri "0.0.5" + socket.io-parser "~3.3.0" + to-array "0.1.4" + +socket.io-parser@~3.3.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.1.tgz#f07d9c8cb3fb92633aa93e76d98fd3a334623199" + integrity sha512-1QLvVAe8dTz+mKmZ07Swxt+LAo4Y1ff50rlyoEx00TQmDFVQYPfcqGvIDJLGaBdhdNCecXtyKpD+EgKGcmmbuQ== + dependencies: + component-emitter "~1.3.0" + debug "~3.1.0" + isarray "2.0.1" + +socket.io-parser@~3.4.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.4.1.tgz#b06af838302975837eab2dc980037da24054d64a" + integrity sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A== + dependencies: + component-emitter "1.2.1" + debug "~4.1.0" + isarray "2.0.1" + +socket.io@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.3.0.tgz#cd762ed6a4faeca59bc1f3e243c0969311eb73fb" + integrity sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg== + dependencies: + debug "~4.1.0" + engine.io "~3.4.0" + has-binary2 "~1.0.2" + socket.io-adapter "~1.1.0" + socket.io-client "2.3.0" + socket.io-parser "~3.4.0" + +sockjs-client@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.4.0.tgz#c9f2568e19c8fd8173b4997ea3420e0bb306c7d5" + integrity sha512-5zaLyO8/nri5cua0VtOrFXBPK1jbL4+1cebT/mmKA1E1ZXOvJrII75bPu0l0k843G/+iAbhEqzyKr0w/eCCj7g== + dependencies: + debug "^3.2.5" + eventsource "^1.0.7" + faye-websocket "~0.11.1" + inherits "^2.0.3" + json3 "^3.3.2" + url-parse "^1.4.3" + +sockjs@0.3.20: + version "0.3.20" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.20.tgz#b26a283ec562ef8b2687b44033a4eeceac75d855" + integrity sha512-SpmVOVpdq0DJc0qArhF3E5xsxvaiqGNb73XfgBpK1y3UD5gs8DSo8aCTsuT5pX8rssdc2NDIzANwP9eCAiSdTA== + dependencies: + faye-websocket "^0.10.0" + uuid "^3.4.0" + websocket-driver "0.6.5" + +socks-proxy-agent@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz#3c8991f3145b2799e70e11bd5fbc8b1963116386" + integrity sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg== + dependencies: + agent-base "~4.2.1" + socks "~2.3.2" + +socks@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.3.3.tgz#01129f0a5d534d2b897712ed8aceab7ee65d78e3" + integrity sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA== + dependencies: + ip "1.1.5" + smart-buffer "^4.1.0" + +sort-keys@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" + integrity sha1-RBttTTRnmPG05J6JIK37oOVD+a0= + dependencies: + is-plain-obj "^1.0.0" + +source-list-map@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" + integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== + +source-map-loader@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-1.0.2.tgz#b0a6582b2eaa387ede1ecf8061ae0b93c23f9eb0" + integrity sha512-oX8d6ndRjN+tVyjj6PlXSyFPhDdVAPsZA30nD3/II8g4uOv8fCz0DMn5sy8KtVbDfKQxOpGwGJnK3xIW3tauDw== + dependencies: + data-urls "^2.0.0" + iconv-lite "^0.6.2" + loader-utils "^2.0.0" + schema-utils "^2.7.0" + source-map "^0.6.1" + +source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: + version "0.5.3" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" + integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== + dependencies: + atob "^2.1.2" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + +source-map-support@0.5.19, source-map-support@^0.5.17, source-map-support@^0.5.5, source-map-support@~0.5.12, source-map-support@~0.5.19: + version "0.5.19" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" + integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-support@~0.4.0: + version "0.4.18" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" + integrity sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA== + dependencies: + source-map "^0.5.6" + +source-map-url@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" + integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= + +source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@0.7.3, source-map@^0.7.3, source-map@~0.7.2: + version "0.7.3" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" + integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== + +source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + +sourcemap-codec@^1.4.4, sourcemap-codec@^1.4.8: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + +spdx-correct@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" + integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" + integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + +spdx-expression-parse@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" + integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.6" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz#c80757383c28abf7296744998cbc106ae8b854ce" + integrity sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw== + +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== + dependencies: + debug "^4.1.0" + detect-node "^2.0.4" + hpack.js "^2.1.6" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" + +spdy@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" + integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== + dependencies: + debug "^4.1.0" + handle-thing "^2.0.0" + http-deceiver "^1.2.7" + select-hose "^2.0.0" + spdy-transport "^3.0.0" + +speed-measure-webpack-plugin@1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/speed-measure-webpack-plugin/-/speed-measure-webpack-plugin-1.3.3.tgz#6ff894fc83e8a6310dde3af863a0329cd79da4f5" + integrity sha512-2ljD4Ch/rz2zG3HsLsnPfp23osuPBS0qPuz9sGpkNXTN1Ic4M+W9xB8l8rS8ob2cO4b1L+WTJw/0AJwWYVgcxQ== + dependencies: + chalk "^2.0.1" + +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== + dependencies: + extend-shallow "^3.0.0" + +split.js@^1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/split.js/-/split.js-1.6.2.tgz#b8c63aeef2b15d84a003ead09e7def6ad166bb40" + integrity sha512-72C7zcQePzlmWqPOKkB2Ro0sUmnWSx+qEWXjLJKk6Qp4jAkFRz1hJgJb+ay6ZQyz/Aw9r8N/PZiCEKbPVpFoDQ== + +sprintf-js@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673" + integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug== + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +sshpk@^1.7.0: + version "1.16.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +ssri@^6.0.0, ssri@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8" + integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA== + dependencies: + figgy-pudding "^3.5.1" + +ssri@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.0.tgz#79ca74e21f8ceaeddfcb4b90143c458b8d988808" + integrity sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA== + dependencies: + minipass "^3.1.1" + +stable@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" + integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== + +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + +"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +stream-browserify@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" + integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg== + dependencies: + inherits "~2.0.1" + readable-stream "^2.0.2" + +stream-each@^1.1.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae" + integrity sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw== + dependencies: + end-of-stream "^1.1.0" + stream-shift "^1.0.0" + +stream-http@^2.7.2: + version "2.8.3" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" + integrity sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw== + dependencies: + builtin-status-codes "^3.0.0" + inherits "^2.0.1" + readable-stream "^2.3.6" + to-arraybuffer "^1.0.0" + xtend "^4.0.0" + +stream-shift@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" + integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== + +streamroller@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-2.2.4.tgz#c198ced42db94086a6193608187ce80a5f2b0e53" + integrity sha512-OG79qm3AujAM9ImoqgWEY1xG4HX+Lw+yY6qZj9R1K2mhF5bEmQ849wvrb+4vt4jLMLzwXttJlQbOdPOQVRv7DQ== + dependencies: + date-format "^2.1.0" + debug "^4.1.1" + fs-extra "^8.1.0" + +strict-uri-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" + integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM= + +string-width@^3.0.0, string-width@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + +string.prototype.trimend@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" + integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + +string.prototype.trimstart@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54" + integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + +string_decoder@^1.0.0, string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= + +style-loader@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-1.2.1.tgz#c5cbbfbf1170d076cfdd86e0109c5bba114baa1a" + integrity sha512-ByHSTQvHLkWE9Ir5+lGbVOXhxX10fbprhLvdg96wedFZb4NDekDPxVKv5Fwmio+QcMlkkNfuK+5W1peQ5CUhZg== + dependencies: + loader-utils "^2.0.0" + schema-utils "^2.6.6" + +stylehacks@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5" + integrity sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g== + dependencies: + browserslist "^4.0.0" + postcss "^7.0.0" + postcss-selector-parser "^3.0.0" + +stylus-loader@3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/stylus-loader/-/stylus-loader-3.0.2.tgz#27a706420b05a38e038e7cacb153578d450513c6" + integrity sha512-+VomPdZ6a0razP+zinir61yZgpw2NfljeSsdUF5kJuEzlo3khXhY19Fn6l8QQz1GRJGtMCo8nG5C04ePyV7SUA== + dependencies: + loader-utils "^1.0.2" + lodash.clonedeep "^4.5.0" + when "~3.6.x" + +stylus@0.54.8: + version "0.54.8" + resolved "https://registry.yarnpkg.com/stylus/-/stylus-0.54.8.tgz#3da3e65966bc567a7b044bfe0eece653e099d147" + integrity sha512-vr54Or4BZ7pJafo2mpf0ZcwA74rpuYCZbxrHBsH8kbcXOwSfvBFwsRfpGO5OD5fhG5HDCFW737PKaawI7OqEAg== + dependencies: + css-parse "~2.0.0" + debug "~3.1.0" + glob "^7.1.6" + mkdirp "~1.0.4" + safer-buffer "^2.1.2" + sax "~1.2.4" + semver "^6.3.0" + source-map "^0.7.3" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.0.0, supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +svgo@^1.0.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167" + integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw== + dependencies: + chalk "^2.4.1" + coa "^2.0.2" + css-select "^2.0.0" + css-select-base-adapter "^0.1.1" + css-tree "1.0.0-alpha.37" + csso "^4.0.2" + js-yaml "^3.13.1" + mkdirp "~0.5.1" + object.values "^1.1.0" + sax "~1.2.4" + stable "^0.1.8" + unquote "~1.1.1" + util.promisify "~1.0.0" + +symbol-observable@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" + integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== + +systemjs@0.21.5: + version "0.21.5" + resolved "https://registry.yarnpkg.com/systemjs/-/systemjs-0.21.5.tgz#2fcef4edfe744003da4787f3f3d45d73f94462d2" + integrity sha512-GWzZhN/x7Fsae2CYkz2GF7OgOS+YDgKulcgd5L1kTogZHMKDrPx5T8zI8I0y5RoU9Dx78Z7j1XMfuFa1thD84A== + +tapable@^1.0.0, tapable@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" + integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== + +tar@^4.4.10: + version "4.4.13" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" + integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA== + dependencies: + chownr "^1.1.1" + fs-minipass "^1.2.5" + minipass "^2.8.6" + minizlib "^1.2.1" + mkdirp "^0.5.0" + safe-buffer "^5.1.2" + yallist "^3.0.3" + +tar@^6.0.2: + version "6.0.5" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.5.tgz#bde815086e10b39f1dcd298e89d596e1535e200f" + integrity sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + +terser-webpack-plugin@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-4.1.0.tgz#6e9d6ae4e1a900d88ddce8da6a47507ea61f44bc" + integrity sha512-0ZWDPIP8BtEDZdChbufcXUigOYk6dOX/P/X0hWxqDDcVAQLb8Yy/0FAaemSfax3PAA67+DJR778oz8qVbmy4hA== + dependencies: + cacache "^15.0.5" + find-cache-dir "^3.3.1" + jest-worker "^26.3.0" + p-limit "^3.0.2" + schema-utils "^2.6.6" + serialize-javascript "^4.0.0" + source-map "^0.6.1" + terser "^5.0.0" + webpack-sources "^1.4.3" + +terser-webpack-plugin@^1.4.3: + version "1.4.5" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz#a217aefaea330e734ffacb6120ec1fa312d6040b" + integrity sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw== + dependencies: + cacache "^12.0.2" + find-cache-dir "^2.1.0" + is-wsl "^1.1.0" + schema-utils "^1.0.0" + serialize-javascript "^4.0.0" + source-map "^0.6.1" + terser "^4.1.2" + webpack-sources "^1.4.0" + worker-farm "^1.7.0" + +terser@5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.3.0.tgz#c481f4afecdcc182d5e2bdd2ff2dc61555161e81" + integrity sha512-XTT3D3AwxC54KywJijmY2mxZ8nJiEjBHVYzq8l9OaYuRFWeQNBwvipuzzYEP4e+/AVcd1hqG/CqgsdIRyT45Fg== + dependencies: + commander "^2.20.0" + source-map "~0.6.1" + source-map-support "~0.5.12" + +terser@^4.1.2: + version "4.8.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" + integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== + dependencies: + commander "^2.20.0" + source-map "~0.6.1" + source-map-support "~0.5.12" + +terser@^5.0.0: + version "5.3.4" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.3.4.tgz#e510e05f86e0bd87f01835c3238839193f77a60c" + integrity sha512-dxuB8KQo8Gt6OVOeLg/rxfcxdNZI/V1G6ze1czFUzPeCFWZRtvZMgSzlZZ5OYBZ4HoG607F6pFPNLekJyV+yVw== + dependencies: + commander "^2.20.0" + source-map "~0.7.2" + source-map-support "~0.5.19" + +through2@^2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== + dependencies: + readable-stream "~2.3.6" + xtend "~4.0.1" + +"through@>=2.2.7 <3", through@X.X.X, through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +thunky@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" + integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== + +timers-browserify@^2.0.4: + version "2.0.11" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.11.tgz#800b1f3eee272e5bc53ee465a04d0e804c31211f" + integrity sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ== + dependencies: + setimmediate "^1.0.4" + +timsort@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" + integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= + +tiny-emitter@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" + integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== + +tiny-warning@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + +tinycolor2@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803" + integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA== + +tmp@0.0.30: + version "0.0.30" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.30.tgz#72419d4a8be7d6ce75148fd8b324e593a711c2ed" + integrity sha1-ckGdSovn1s51FI/YsyTlk6cRwu0= + dependencies: + os-tmpdir "~1.0.1" + +tmp@0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== + dependencies: + rimraf "^3.0.0" + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + +to-array@0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" + integrity sha1-F+bBH3PdTz10zaek/zI46a2b+JA= + +to-arraybuffer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" + integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + +tooltipster@^4.2.8: + version "4.2.8" + resolved "https://registry.yarnpkg.com/tooltipster/-/tooltipster-4.2.8.tgz#ad1970dd71ad853034e64e3fdd1745f7f3485071" + integrity sha512-Znmbt5UMzaiFCRlVaRtfRZYQqxrmNlj1+3xX/aT0OiA3xkQZhXYGbLJmZPigx0YiReYZpO7Lm2XKbUxXsiU/pg== + +tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +tr46@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.0.2.tgz#03273586def1595ae08fedb38d7733cee91d2479" + integrity sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg== + dependencies: + punycode "^2.1.1" + +tree-kill@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" + integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== + +ts-node@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.0.0.tgz#e7699d2a110cc8c0d3b831715e417688683460b3" + integrity sha512-/TqB4SnererCDR/vb4S/QvSZvzQMJN8daAslg7MeaiHvD8rDZsSfXmNeNumyZZzMned72Xoq/isQljYSt8Ynfg== + dependencies: + arg "^4.1.0" + diff "^4.0.1" + make-error "^1.1.1" + source-map-support "^0.5.17" + yn "3.1.1" + +ts-pnp@^1.1.6: + version "1.2.0" + resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" + integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw== + +tslib@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e" + integrity sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ== + +tslib@^1.10.0, tslib@^1.13.0, tslib@^1.8.1, tslib@^1.9.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.0.tgz#d624983f3e2c5e0b55307c3dd6c86acd737622c6" + integrity sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw== + +tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.2.tgz#462295631185db44b21b1ea3615b63cd1c038242" + integrity sha512-wAH28hcEKwna96/UacuWaVspVLkg4x1aDM9JlzqaQTOFczCktkVAb5fmXChgandR1EraDPs2w8P+ozM+oafwxg== + +tslint@~6.1.3: + version "6.1.3" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-6.1.3.tgz#5c23b2eccc32487d5523bd3a470e9aa31789d904" + integrity sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg== + dependencies: + "@babel/code-frame" "^7.0.0" + builtin-modules "^1.1.1" + chalk "^2.3.0" + commander "^2.12.1" + diff "^4.0.1" + glob "^7.1.1" + js-yaml "^3.13.1" + minimatch "^3.0.4" + mkdirp "^0.5.3" + resolve "^1.3.2" + semver "^5.3.0" + tslib "^1.13.0" + tsutils "^2.29.0" + +tsutils@^2.29.0: + version "2.29.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" + integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA== + dependencies: + tslib "^1.8.1" + +tty-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" + integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tv4@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/tv4/-/tv4-1.3.0.tgz#d020c846fadd50c855abb25ebaecc68fc10f7963" + integrity sha1-0CDIRvrdUMhVq7JeuuzGj8EPeWM= + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +type-fest@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" + integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== + +type-is@~1.6.17, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +type@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" + integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== + +type@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/type/-/type-2.1.0.tgz#9bdc22c648cf8cf86dd23d32336a41cfb6475e3f" + integrity sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA== + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= + +typeface-roboto@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/typeface-roboto/-/typeface-roboto-1.1.13.tgz#9c4517cb91e311706c74823e857b4bac9a764ae5" + integrity sha512-YXvbd3a1QTREoD+FJoEkl0VQNJoEjewR2H11IjVv4bp6ahuIcw0yyw/3udC4vJkHw3T3cUh85FTg8eWef3pSaw== + +typescript@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.2.tgz#7ea7c88777c723c681e33bf7988be5d008d05ac2" + integrity sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ== + +typescript@~4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.3.tgz#153bbd468ef07725c1df9c77e8b453f8d36abba5" + integrity sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg== + +ua-parser-js@0.7.21: + version "0.7.21" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.21.tgz#853cf9ce93f642f67174273cc34565ae6f308777" + integrity sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ== + +unicode-canonical-property-names-ecmascript@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" + integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ== + +unicode-match-property-ecmascript@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c" + integrity sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg== + dependencies: + unicode-canonical-property-names-ecmascript "^1.0.4" + unicode-property-aliases-ecmascript "^1.0.4" + +unicode-match-property-value-ecmascript@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz#0d91f600eeeb3096aa962b1d6fc88876e64ea531" + integrity sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ== + +unicode-property-aliases-ecmascript@^1.0.4: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4" + integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg== + +union-value@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" + integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^2.0.1" + +uniq@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" + integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= + +uniqs@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" + integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI= + +unique-filename@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" + integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== + dependencies: + unique-slug "^2.0.0" + +unique-slug@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" + integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== + dependencies: + imurmurhash "^0.1.4" + +universal-analytics@0.4.23: + version "0.4.23" + resolved "https://registry.yarnpkg.com/universal-analytics/-/universal-analytics-0.4.23.tgz#d915e676850c25c4156762471bdd7cf2eaaca8ac" + integrity sha512-lgMIH7XBI6OgYn1woDEmxhGdj8yDefMKg7GkWdeATAlQZFrMrNyxSkpDzY57iY0/6fdlzTbBV03OawvvzG+q7A== + dependencies: + debug "^4.1.1" + request "^2.88.2" + uuid "^3.0.0" + +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + +unquote@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" + integrity sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ= + +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + +upath@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" + integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== + +uri-js@^4.2.2: + version "4.4.0" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602" + integrity sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g== + dependencies: + punycode "^2.1.0" + +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= + +url-parse@^1.4.3: + version "1.4.7" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" + integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + +url@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +use@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== + +util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +util-promisify@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/util-promisify/-/util-promisify-2.1.0.tgz#3c2236476c4d32c5ff3c47002add7c13b9a82a53" + integrity sha1-PCI2R2xNMsX/PEcAKt18E7moKlM= + dependencies: + object.getownpropertydescriptors "^2.0.3" + +util.promisify@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee" + integrity sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.2" + has-symbols "^1.0.1" + object.getownpropertydescriptors "^2.1.0" + +util@0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" + integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk= + dependencies: + inherits "2.0.1" + +util@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" + integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ== + dependencies: + inherits "2.0.3" + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + +uuid@8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea" + integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ== + +uuid@^3.0.0, uuid@^3.3.2, uuid@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +validate-npm-package-name@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz#5fa912d81eb7d0c74afc140de7317f0ca7df437e" + integrity sha1-X6kS2B630MdK/BQN5zF/DKffQ34= + dependencies: + builtins "^1.0.3" + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + +vendors@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e" + integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w== + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +vm-browserify@^1.0.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" + integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== + +void-elements@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" + integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w= + +warning@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" + integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== + dependencies: + loose-envify "^1.0.0" + +watchpack-chokidar2@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz#9948a1866cbbd6cb824dea13a7ed691f6c8ddff0" + integrity sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA== + dependencies: + chokidar "^2.1.8" + +watchpack@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.4.tgz#6e9da53b3c80bb2d6508188f5b200410866cd30b" + integrity sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg== + dependencies: + graceful-fs "^4.1.2" + neo-async "^2.5.0" + optionalDependencies: + chokidar "^3.4.1" + watchpack-chokidar2 "^2.0.0" + +wbuf@^1.1.0, wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== + dependencies: + minimalistic-assert "^1.0.0" + +wcwidth@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" + integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g= + dependencies: + defaults "^1.0.3" + +webdriver-js-extender@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/webdriver-js-extender/-/webdriver-js-extender-2.1.0.tgz#57d7a93c00db4cc8d556e4d3db4b5db0a80c3bb7" + integrity sha512-lcUKrjbBfCK6MNsh7xaY2UAUmZwe+/ib03AjVOpFobX4O7+83BUveSrLfU0Qsyb1DaKJdQRbuU+kM9aZ6QUhiQ== + dependencies: + "@types/selenium-webdriver" "^3.0.0" + selenium-webdriver "^3.0.1" + +webdriver-manager@^12.1.7: + version "12.1.7" + resolved "https://registry.yarnpkg.com/webdriver-manager/-/webdriver-manager-12.1.7.tgz#ed4eaee8f906b33c146e869b55e850553a1b1162" + integrity sha512-XINj6b8CYuUYC93SG3xPkxlyUc3IJbD6Vvo75CVGuG9uzsefDzWQrhz0Lq8vbPxtb4d63CZdYophF8k8Or/YiA== + dependencies: + adm-zip "^0.4.9" + chalk "^1.1.1" + del "^2.2.0" + glob "^7.0.3" + ini "^1.3.4" + minimist "^1.2.0" + q "^1.4.1" + request "^2.87.0" + rimraf "^2.5.2" + semver "^5.3.0" + xml2js "^0.4.17" + +webidl-conversions@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" + integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== + +webpack-dev-middleware@3.7.2, webpack-dev-middleware@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz#0019c3db716e3fa5cecbf64f2ab88a74bab331f3" + integrity sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw== + dependencies: + memory-fs "^0.4.1" + mime "^2.4.4" + mkdirp "^0.5.1" + range-parser "^1.2.1" + webpack-log "^2.0.0" + +webpack-dev-server@3.11.0: + version "3.11.0" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz#8f154a3bce1bcfd1cc618ef4e703278855e7ff8c" + integrity sha512-PUxZ+oSTxogFQgkTtFndEtJIPNmml7ExwufBZ9L2/Xyyd5PnOL5UreWe5ZT7IU25DSdykL9p1MLQzmLh2ljSeg== + dependencies: + ansi-html "0.0.7" + bonjour "^3.5.0" + chokidar "^2.1.8" + compression "^1.7.4" + connect-history-api-fallback "^1.6.0" + debug "^4.1.1" + del "^4.1.1" + express "^4.17.1" + html-entities "^1.3.1" + http-proxy-middleware "0.19.1" + import-local "^2.0.0" + internal-ip "^4.3.0" + ip "^1.1.5" + is-absolute-url "^3.0.3" + killable "^1.0.1" + loglevel "^1.6.8" + opn "^5.5.0" + p-retry "^3.0.1" + portfinder "^1.0.26" + schema-utils "^1.0.0" + selfsigned "^1.10.7" + semver "^6.3.0" + serve-index "^1.9.1" + sockjs "0.3.20" + sockjs-client "1.4.0" + spdy "^4.0.2" + strip-ansi "^3.0.1" + supports-color "^6.1.0" + url "^0.11.0" + webpack-dev-middleware "^3.7.2" + webpack-log "^2.0.0" + ws "^6.2.1" + yargs "^13.3.2" + +webpack-log@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-2.0.0.tgz#5b7928e0637593f119d32f6227c1e0ac31e1b47f" + integrity sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg== + dependencies: + ansi-colors "^3.0.0" + uuid "^3.3.2" + +webpack-merge@4.2.2, webpack-merge@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.2.2.tgz#a27c52ea783d1398afd2087f547d7b9d2f43634d" + integrity sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g== + dependencies: + lodash "^4.17.15" + +webpack-sources@1.4.3, webpack-sources@^1.1.0, webpack-sources@^1.2.0, webpack-sources@^1.3.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" + integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== + dependencies: + source-list-map "^2.0.0" + source-map "~0.6.1" + +webpack-subresource-integrity@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/webpack-subresource-integrity/-/webpack-subresource-integrity-1.4.1.tgz#e8bf918b444277df46a66cd84542cbcdc5a6272d" + integrity sha512-XMLFInbGbB1HV7K4vHWANzc1CN0t/c4bBvnlvGxGwV45yE/S/feAXIm8dJsCkzqWtSKnmaEgTp/meyeThxG4Iw== + dependencies: + webpack-sources "^1.3.0" + +webpack@4.44.1: + version "4.44.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.44.1.tgz#17e69fff9f321b8f117d1fda714edfc0b939cc21" + integrity sha512-4UOGAohv/VGUNQJstzEywwNxqX417FnjZgZJpJQegddzPmTvph37eBIRbRTfdySXzVtJXLJfbMN3mMYhM6GdmQ== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-module-context" "1.9.0" + "@webassemblyjs/wasm-edit" "1.9.0" + "@webassemblyjs/wasm-parser" "1.9.0" + acorn "^6.4.1" + ajv "^6.10.2" + ajv-keywords "^3.4.1" + chrome-trace-event "^1.0.2" + enhanced-resolve "^4.3.0" + eslint-scope "^4.0.3" + json-parse-better-errors "^1.0.2" + loader-runner "^2.4.0" + loader-utils "^1.2.3" + memory-fs "^0.4.1" + micromatch "^3.1.10" + mkdirp "^0.5.3" + neo-async "^2.6.1" + node-libs-browser "^2.2.1" + schema-utils "^1.0.0" + tapable "^1.1.3" + terser-webpack-plugin "^1.4.3" + watchpack "^1.7.4" + webpack-sources "^1.4.1" + +webpack@^4.44.2: + version "4.44.2" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.44.2.tgz#6bfe2b0af055c8b2d1e90ed2cd9363f841266b72" + integrity sha512-6KJVGlCxYdISyurpQ0IPTklv+DULv05rs2hseIXer6D7KrUicRDLFb4IUM1S6LUAKypPM/nSiVSuv8jHu1m3/Q== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-module-context" "1.9.0" + "@webassemblyjs/wasm-edit" "1.9.0" + "@webassemblyjs/wasm-parser" "1.9.0" + acorn "^6.4.1" + ajv "^6.10.2" + ajv-keywords "^3.4.1" + chrome-trace-event "^1.0.2" + enhanced-resolve "^4.3.0" + eslint-scope "^4.0.3" + json-parse-better-errors "^1.0.2" + loader-runner "^2.4.0" + loader-utils "^1.2.3" + memory-fs "^0.4.1" + micromatch "^3.1.10" + mkdirp "^0.5.3" + neo-async "^2.6.1" + node-libs-browser "^2.2.1" + schema-utils "^1.0.0" + tapable "^1.1.3" + terser-webpack-plugin "^1.4.3" + watchpack "^1.7.4" + webpack-sources "^1.4.1" + +websocket-driver@0.6.5: + version "0.6.5" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.6.5.tgz#5cb2556ceb85f4373c6d8238aa691c8454e13a36" + integrity sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY= + dependencies: + websocket-extensions ">=0.1.1" + +websocket-driver@>=0.5.1: + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + +whatwg-mimetype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" + integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== + +whatwg-url@^8.0.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.3.0.tgz#d1e11e565334486cdb280d3101b9c3fd1c867582" + integrity sha512-BQRf/ej5Rp3+n7k0grQXZj9a1cHtsp4lqj01p59xBWFKdezR8sO37XnpafwNqiFac/v2Il12EIMjX/Y4VZtT8Q== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^2.0.2" + webidl-conversions "^6.1.0" + +when@~3.6.x: + version "3.6.4" + resolved "https://registry.yarnpkg.com/when/-/when-3.6.4.tgz#473b517ec159e2b85005497a13983f095412e34e" + integrity sha1-RztRfsFZ4rhQBUl6E5g/CVQS404= + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= + +which@^1.2.1, which@^1.2.9, which@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +worker-farm@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8" + integrity sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw== + dependencies: + errno "~0.1.7" + +worker-plugin@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/worker-plugin/-/worker-plugin-5.0.0.tgz#113b5fe1f4a5d6a957cecd29915bedafd70bb537" + integrity sha512-AXMUstURCxDD6yGam2r4E34aJg6kW85IiaeX72hi+I1cxyaMUtrvVY6sbfpGKAj5e7f68Acl62BjQF5aOOx2IQ== + dependencies: + loader-utils "^1.1.0" + +wrap-ansi@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" + integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== + dependencies: + ansi-styles "^3.2.0" + string-width "^3.0.0" + strip-ansi "^5.0.0" + +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +ws@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" + integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== + dependencies: + async-limiter "~1.0.0" + +ws@^7.1.2: + version "7.3.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8" + integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA== + +ws@~6.1.0: + version "6.1.4" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9" + integrity sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA== + dependencies: + async-limiter "~1.0.0" + +xml2js@^0.4.17: + version "0.4.23" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" + integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + +xmlbuilder@~11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" + integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== + +xmlhttprequest-ssl@~1.5.4: + version "1.5.5" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e" + integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4= + +xtend@^4.0.0, xtend@~4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +y18n@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" + integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= + +yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yargs-parser@^13.1.2: + version "13.1.2" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" + integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-parser@^18.1.0, yargs-parser@^18.1.2: + version "18.1.3" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" + integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs@15.3.0: + version "15.3.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.3.0.tgz#403af6edc75b3ae04bf66c94202228ba119f0976" + integrity sha512-g/QCnmjgOl1YJjGsnUg2SatC7NUYEiLXJqxNOQU9qSpjzGtGXda9b+OKccr1kLTy8BN9yqEyqfq5lxlwdc13TA== + dependencies: + cliui "^6.0.0" + decamelize "^1.2.0" + find-up "^4.1.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^4.2.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^18.1.0" + +yargs@^13.3.2: + version "13.3.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" + integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== + dependencies: + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.2" + +yargs@^15.3.1: + version "15.4.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" + integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== + dependencies: + cliui "^6.0.0" + decamelize "^1.2.0" + find-up "^4.1.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^4.2.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^18.1.2" + +yeast@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" + integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk= + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +zone.js@~0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.10.3.tgz#3e5e4da03c607c9dcd92e37dd35687a14a140c16" + integrity sha512-LXVLVEq0NNOqK/fLJo3d0kfzd4sxwn2/h67/02pjCjfKDxgx1i9QqpvtHD8CrBnSSwMw5+dy11O7FRX5mkO7Cg== diff --git a/ui/.babelrc b/ui/.babelrc deleted file mode 100644 index bbda4fe421..0000000000 --- a/ui/.babelrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "presets": [ - "@babel/preset-react", - "@babel/preset-env" - ], - "plugins": [ - "react-hot-loader/babel" - ] -} diff --git a/ui/.eslintrc b/ui/.eslintrc deleted file mode 100644 index 5b8920d93d..0000000000 --- a/ui/.eslintrc +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": [ - "eslint:recommended", - "plugin:import/errors", - "plugin:import/warnings", - "angular" - ], - "parser": "babel-eslint", - "settings": { - "import/ignore": [ - "node_modules", - "\\.tpl\\.html$" - ] - }, - "globals": { - "FileReader": true - } -} diff --git a/ui/.gitignore b/ui/.gitignore deleted file mode 100644 index 09daa48c35..0000000000 --- a/ui/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -.tern-project diff --git a/ui/.jshintrc b/ui/.jshintrc deleted file mode 100644 index 6f00218e37..0000000000 --- a/ui/.jshintrc +++ /dev/null @@ -1,13 +0,0 @@ -{ - "globalstrict": true, - "globals": { - "angular": false, - "describe": false, - "it": false, - "expect": false, - "beforeEach": false, - "afterEach": false, - "module": false, - "inject": false - } -} \ No newline at end of file diff --git a/ui/.stylelintrc b/ui/.stylelintrc deleted file mode 100644 index e878729b53..0000000000 --- a/ui/.stylelintrc +++ /dev/null @@ -1,292 +0,0 @@ -{ - "extends": ["stylelint-config-standard", "stylelint-config-recommended-scss"], - "plugins": [ - "stylelint-order" - ], - "rules": { - "at-rule-empty-line-before": ["always", { - "except": ["first-nested"], - "ignore": ["after-comment"] - }], - "at-rule-name-space-after": "always", - "at-rule-no-vendor-prefix": true, - "at-rule-semicolon-space-before": "never", - "block-closing-brace-empty-line-before": "never", - "block-closing-brace-newline-after": null, - "block-opening-brace-space-before": null, - "color-named": "never", - "declaration-block-semicolon-newline-after": "always-multi-line", - "declaration-block-semicolon-newline-before": "never-multi-line", - "declaration-block-semicolon-space-after": "always-single-line", - "declaration-empty-line-before": null, - "declaration-no-important": null, - "font-family-name-quotes": "always-where-recommended", - "font-weight-notation": [ - "numeric", { - "ignore": ["relative"] - }], - "function-url-no-scheme-relative": true, - "function-url-quotes": "always", - "length-zero-no-unit": true, - "max-empty-lines": 2, - "max-line-length": null, - "media-feature-name-no-vendor-prefix": true, - "media-feature-parentheses-space-inside": "never", - "media-feature-range-operator-space-after": "always", - "media-feature-range-operator-space-before": "never", - "no-descending-specificity": null, - "no-duplicate-selectors": true, - "number-leading-zero": "never", - "media-feature-name-no-unknown": [true, { - "ignoreMediaFeatureNames": ["prefers-reduced-motion"] - }], - "order/properties-order": [ - "position", - "top", - "right", - "bottom", - "left", - "z-index", - "box-sizing", - "display", - "flex", - "flex-align", - "flex-basis", - "flex-direction", - "flex-wrap", - "flex-flow", - "flex-shrink", - "flex-grow", - "flex-order", - "flex-pack", - "align-content", - "align-items", - "align-self", - "justify-content", - "order", - "float", - "width", - "min-width", - "max-width", - "height", - "min-height", - "max-height", - "padding", - "padding-top", - "padding-right", - "padding-bottom", - "padding-left", - "margin", - "margin-top", - "margin-right", - "margin-bottom", - "margin-left", - "overflow", - "overflow-x", - "overflow-y", - "-webkit-overflow-scrolling", - "-ms-overflow-x", - "-ms-overflow-y", - "-ms-overflow-style", - "columns", - "column-count", - "column-fill", - "column-gap", - "column-rule", - "column-rule-width", - "column-rule-style", - "column-rule-color", - "column-span", - "column-width", - "orphans", - "widows", - "clip", - "clear", - "font", - "font-family", - "font-size", - "font-style", - "font-weight", - "font-variant", - "font-size-adjust", - "font-stretch", - "font-effect", - "font-emphasize", - "font-emphasize-position", - "font-emphasize-style", - "font-smooth", - "src", - "hyphens", - "line-height", - "color", - "text-align", - "text-align-last", - "text-emphasis", - "text-emphasis-color", - "text-emphasis-style", - "text-emphasis-position", - "text-decoration", - "text-indent", - "text-justify", - "text-outline", - "-ms-text-overflow", - "text-overflow", - "text-overflow-ellipsis", - "text-overflow-mode", - "text-shadow", - "text-transform", - "text-wrap", - "-webkit-text-size-adjust", - "-ms-text-size-adjust", - "letter-spacing", - "-ms-word-break", - "word-break", - "word-spacing", - "-ms-word-wrap", - "word-wrap", - "overflow-wrap", - "tab-size", - "white-space", - "vertical-align", - "direction", - "unicode-bidi", - "list-style", - "list-style-position", - "list-style-type", - "list-style-image", - "pointer-events", - "-ms-touch-action", - "touch-action", - "cursor", - "visibility", - "zoom", - "table-layout", - "empty-cells", - "caption-side", - "border-spacing", - "border-collapse", - "content", - "quotes", - "counter-reset", - "counter-increment", - "resize", - "user-select", - "nav-index", - "nav-up", - "nav-right", - "nav-down", - "nav-left", - "background", - "background-color", - "background-image", - "filter", - "background-repeat", - "background-attachment", - "background-position", - "background-position-x", - "background-position-y", - "background-clip", - "background-origin", - "background-size", - "border", - "border-color", - "border-style", - "border-width", - "border-top", - "border-top-color", - "border-top-style", - "border-top-width", - "border-right", - "border-right-color", - "border-right-style", - "border-right-width", - "border-bottom", - "border-bottom-color", - "border-bottom-style", - "border-bottom-width", - "border-left", - "border-left-color", - "border-left-style", - "border-left-width", - "border-radius", - "border-top-left-radius", - "border-top-right-radius", - "border-bottom-right-radius", - "border-bottom-left-radius", - "border-image", - "border-image-source", - "border-image-slice", - "border-image-width", - "border-image-outset", - "border-image-repeat", - "outline", - "outline-width", - "outline-style", - "outline-color", - "outline-offset", - "box-shadow", - "opacity", - "-ms-interpolation-mode", - "page-break-after", - "page-break-before", - "page-break-inside", - "transition", - "transition-delay", - "transition-timing-function", - "transition-duration", - "transition-property", - "transform", - "transform-origin", - "perspective", - "appearance", - "animation", - "animation-name", - "animation-duration", - "animation-play-state", - "animation-timing-function", - "animation-delay", - "animation-iteration-count", - "animation-direction", - "animation-fill-mode", - "fill", - "stroke" - ], - "property-no-vendor-prefix": true, - "rule-empty-line-before": ["always", { - "except": ["first-nested"], - "ignore": ["after-comment"] - }], - "scss/dollar-variable-default": [true, { "ignore": "local" }], - "selector-attribute-quotes": "always", - "selector-list-comma-newline-after": "always", - "selector-list-comma-newline-before": "never-multi-line", - "selector-list-comma-space-after": "always-single-line", - "selector-list-comma-space-before": "never-single-line", - "selector-max-attribute": 2, - "selector-max-class": 6, - "selector-max-combinators": 8, - "selector-max-compound-selectors": 9, - "selector-max-empty-lines": 1, - "selector-max-id": 1, - "selector-max-specificity": null, - "selector-max-type": 5, - "selector-max-universal": 1, - "selector-no-qualifying-type": null, - "selector-no-vendor-prefix": true, - "selector-type-no-unknown": [true, { - "ignoreTypes": [ - "/^md-/", - "/^mdp-/", - "/^ng-/", - "/^tb-/", - "/^v-pane/" - ] - }], - "string-quotes": "double", - "value-keyword-case": "lower", - "value-list-comma-newline-after": "always-multi-line", - "value-list-comma-newline-before": "never-multi-line", - "value-list-comma-space-after": "always-single-line", - "value-no-vendor-prefix": true - } -} diff --git a/ui/package-lock.json b/ui/package-lock.json deleted file mode 100644 index 709137d524..0000000000 --- a/ui/package-lock.json +++ /dev/null @@ -1,14633 +0,0 @@ -{ - "name": "thingsboard", - "version": "2.5.3", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@babel/cli": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.8.4.tgz", - "integrity": "sha512-XXLgAm6LBbaNxaGhMAznXXaxtCWfuv6PIDJ9Alsy9JYTOh+j2jJz+L/162kkfU1j/pTSxK1xGmlwI4pdIMkoag==", - "dev": true, - "requires": { - "chokidar": "^2.1.8", - "commander": "^4.0.1", - "convert-source-map": "^1.1.0", - "fs-readdir-recursive": "^1.1.0", - "glob": "^7.0.0", - "lodash": "^4.17.13", - "make-dir": "^2.1.0", - "slash": "^2.0.0", - "source-map": "^0.5.0" - }, - "dependencies": { - "commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true - } - } - }, - "@babel/code-frame": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", - "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", - "dev": true, - "requires": { - "@babel/highlight": "^7.8.3" - } - }, - "@babel/compat-data": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.8.5.tgz", - "integrity": "sha512-jWYUqQX/ObOhG1UiEkbH5SANsE/8oKXiQWjj7p7xgj9Zmnt//aUvyz4dBkK0HNsS8/cbyC5NmmH87VekW+mXFg==", - "dev": true, - "requires": { - "browserslist": "^4.8.5", - "invariant": "^2.2.4", - "semver": "^5.5.0" - } - }, - "@babel/core": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.8.4.tgz", - "integrity": "sha512-0LiLrB2PwrVI+a2/IEskBopDYSd8BCb3rOvH7D5tzoWd696TBEduBvuLVm4Nx6rltrLZqvI3MCalB2K2aVzQjA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.8.4", - "@babel/helpers": "^7.8.4", - "@babel/parser": "^7.8.4", - "@babel/template": "^7.8.3", - "@babel/traverse": "^7.8.4", - "@babel/types": "^7.8.3", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.1", - "json5": "^2.1.0", - "lodash": "^4.17.13", - "resolve": "^1.3.2", - "semver": "^5.4.1", - "source-map": "^0.5.0" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@babel/generator": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.4.tgz", - "integrity": "sha512-PwhclGdRpNAf3IxZb0YVuITPZmmrXz9zf6fH8lT4XbrmfQKr6ryBzhv593P5C6poJRciFCL/eHGW2NuGrgEyxA==", - "dev": true, - "requires": { - "@babel/types": "^7.8.3", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" - } - }, - "@babel/helper-annotate-as-pure": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.8.3.tgz", - "integrity": "sha512-6o+mJrZBxOoEX77Ezv9zwW7WV8DdluouRKNY/IR5u/YTMuKHgugHOzYWlYvYLpLA9nPsQCAAASpCIbjI9Mv+Uw==", - "dev": true, - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.8.3.tgz", - "integrity": "sha512-5eFOm2SyFPK4Rh3XMMRDjN7lBH0orh3ss0g3rTYZnBQ+r6YPj7lgDyCvPphynHvUrobJmeMignBr6Acw9mAPlw==", - "dev": true, - "requires": { - "@babel/helper-explode-assignable-expression": "^7.8.3", - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-builder-react-jsx": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.8.3.tgz", - "integrity": "sha512-JT8mfnpTkKNCboTqZsQTdGo3l3Ik3l7QIt9hh0O9DYiwVel37VoJpILKM4YFbP2euF32nkQSb+F9cUk9b7DDXQ==", - "dev": true, - "requires": { - "@babel/types": "^7.8.3", - "esutils": "^2.0.0" - } - }, - "@babel/helper-call-delegate": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.8.3.tgz", - "integrity": "sha512-6Q05px0Eb+N4/GTyKPPvnkig7Lylw+QzihMpws9iiZQv7ZImf84ZsZpQH7QoWN4n4tm81SnSzPgHw2qtO0Zf3A==", - "dev": true, - "requires": { - "@babel/helper-hoist-variables": "^7.8.3", - "@babel/traverse": "^7.8.3", - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-compilation-targets": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.8.4.tgz", - "integrity": "sha512-3k3BsKMvPp5bjxgMdrFyq0UaEO48HciVrOVF0+lon8pp95cyJ2ujAh0TrBHNMnJGT2rr0iKOJPFFbSqjDyf/Pg==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.8.4", - "browserslist": "^4.8.5", - "invariant": "^2.2.4", - "levenary": "^1.1.1", - "semver": "^5.5.0" - } - }, - "@babel/helper-create-regexp-features-plugin": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.3.tgz", - "integrity": "sha512-Gcsm1OHCUr9o9TcJln57xhWHtdXbA2pgQ58S0Lxlks0WMGNXuki4+GLfX0p+L2ZkINUGZvfkz8rzoqJQSthI+Q==", - "dev": true, - "requires": { - "@babel/helper-regex": "^7.8.3", - "regexpu-core": "^4.6.0" - } - }, - "@babel/helper-define-map": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.8.3.tgz", - "integrity": "sha512-PoeBYtxoZGtct3md6xZOCWPcKuMuk3IHhgxsRRNtnNShebf4C8YonTSblsK4tvDbm+eJAw2HAPOfCr+Q/YRG/g==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.8.3", - "@babel/types": "^7.8.3", - "lodash": "^4.17.13" - } - }, - "@babel/helper-explode-assignable-expression": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.8.3.tgz", - "integrity": "sha512-N+8eW86/Kj147bO9G2uclsg5pwfs/fqqY5rwgIL7eTBklgXjcOJ3btzS5iM6AitJcftnY7pm2lGsrJVYLGjzIw==", - "dev": true, - "requires": { - "@babel/traverse": "^7.8.3", - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-function-name": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", - "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.8.3", - "@babel/template": "^7.8.3", - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", - "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", - "dev": true, - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.8.3.tgz", - "integrity": "sha512-ky1JLOjcDUtSc+xkt0xhYff7Z6ILTAHKmZLHPxAhOP0Nd77O+3nCsd6uSVYur6nJnCI029CrNbYlc0LoPfAPQg==", - "dev": true, - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz", - "integrity": "sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA==", - "dev": true, - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-module-imports": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz", - "integrity": "sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg==", - "dev": true, - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-module-transforms": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.8.3.tgz", - "integrity": "sha512-C7NG6B7vfBa/pwCOshpMbOYUmrYQDfCpVL/JCRu0ek8B5p8kue1+BCXpg2vOYs7w5ACB9GTOBYQ5U6NwrMg+3Q==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.8.3", - "@babel/helper-simple-access": "^7.8.3", - "@babel/helper-split-export-declaration": "^7.8.3", - "@babel/template": "^7.8.3", - "@babel/types": "^7.8.3", - "lodash": "^4.17.13" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz", - "integrity": "sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ==", - "dev": true, - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", - "dev": true - }, - "@babel/helper-regex": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.8.3.tgz", - "integrity": "sha512-BWt0QtYv/cg/NecOAZMdcn/waj/5P26DR4mVLXfFtDokSR6fyuG0Pj+e2FqtSME+MqED1khnSMulkmGl8qWiUQ==", - "dev": true, - "requires": { - "lodash": "^4.17.13" - } - }, - "@babel/helper-remap-async-to-generator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.8.3.tgz", - "integrity": "sha512-kgwDmw4fCg7AVgS4DukQR/roGp+jP+XluJE5hsRZwxCYGg+Rv9wSGErDWhlI90FODdYfd4xG4AQRiMDjjN0GzA==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.8.3", - "@babel/helper-wrap-function": "^7.8.3", - "@babel/template": "^7.8.3", - "@babel/traverse": "^7.8.3", - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-replace-supers": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.8.3.tgz", - "integrity": "sha512-xOUssL6ho41U81etpLoT2RTdvdus4VfHamCuAm4AHxGr+0it5fnwoVdwUJ7GFEqCsQYzJUhcbsN9wB9apcYKFA==", - "dev": true, - "requires": { - "@babel/helper-member-expression-to-functions": "^7.8.3", - "@babel/helper-optimise-call-expression": "^7.8.3", - "@babel/traverse": "^7.8.3", - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-simple-access": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz", - "integrity": "sha512-VNGUDjx5cCWg4vvCTR8qQ7YJYZ+HBjxOgXEl7ounz+4Sn7+LMD3CFrCTEU6/qXKbA2nKg21CwhhBzO0RpRbdCw==", - "dev": true, - "requires": { - "@babel/template": "^7.8.3", - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", - "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", - "dev": true, - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-wrap-function": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.8.3.tgz", - "integrity": "sha512-LACJrbUET9cQDzb6kG7EeD7+7doC3JNvUgTEQOx2qaO1fKlzE/Bf05qs9w1oXQMmXlPO65lC3Tq9S6gZpTErEQ==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.8.3", - "@babel/template": "^7.8.3", - "@babel/traverse": "^7.8.3", - "@babel/types": "^7.8.3" - } - }, - "@babel/helpers": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.8.4.tgz", - "integrity": "sha512-VPbe7wcQ4chu4TDQjimHv/5tj73qz88o12EPkO2ValS2QiQS/1F2SsjyIGNnAD0vF/nZS6Cf9i+vW6HIlnaR8w==", - "dev": true, - "requires": { - "@babel/template": "^7.8.3", - "@babel/traverse": "^7.8.4", - "@babel/types": "^7.8.3" - } - }, - "@babel/highlight": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", - "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", - "dev": true, - "requires": { - "chalk": "^2.0.0", - "esutils": "^2.0.2", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@babel/node": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/node/-/node-7.8.4.tgz", - "integrity": "sha512-MlczXI/VYRnoaWHjicqrzq2z4DhRPaWQIC+C3ISEQs5z+mEccBsn7IAI5Q97ZDTnFYw6ts5IUTzqArilC/g7nw==", - "dev": true, - "requires": { - "@babel/register": "^7.8.3", - "commander": "^4.0.1", - "core-js": "^3.2.1", - "lodash": "^4.17.13", - "node-environment-flags": "^1.0.5", - "regenerator-runtime": "^0.13.3", - "resolve": "^1.13.1", - "v8flags": "^3.1.1" - }, - "dependencies": { - "commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true - }, - "core-js": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.4.tgz", - "integrity": "sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==", - "dev": true - }, - "regenerator-runtime": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", - "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==", - "dev": true - } - } - }, - "@babel/parser": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.4.tgz", - "integrity": "sha512-0fKu/QqildpXmPVaRBoXOlyBb3MC+J0A66x97qEfLOMkn3u6nfY5esWogQwi/K0BjASYy4DbnsEWnpNL6qT5Mw==", - "dev": true - }, - "@babel/plugin-proposal-async-generator-functions": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.8.3.tgz", - "integrity": "sha512-NZ9zLv848JsV3hs8ryEh7Uaz/0KsmPLqv0+PdkDJL1cJy0K4kOCFa8zc1E3mp+RHPQcpdfb/6GovEsW4VDrOMw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/helper-remap-async-to-generator": "^7.8.3", - "@babel/plugin-syntax-async-generators": "^7.8.0" - } - }, - "@babel/plugin-proposal-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.8.3.tgz", - "integrity": "sha512-NyaBbyLFXFLT9FP+zk0kYlUlA8XtCUbehs67F0nnEg7KICgMc2mNkIeu9TYhKzyXMkrapZFwAhXLdnt4IYHy1w==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/plugin-syntax-dynamic-import": "^7.8.0" - } - }, - "@babel/plugin-proposal-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.8.3.tgz", - "integrity": "sha512-KGhQNZ3TVCQG/MjRbAUwuH+14y9q0tpxs1nWWs3pbSleRdDro9SAMMDyye8HhY1gqZ7/NqIc8SKhya0wRDgP1Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.0" - } - }, - "@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-TS9MlfzXpXKt6YYomudb/KU7nQI6/xnapG6in1uZxoxDghuSMZsPb6D2fyUwNYSAp4l1iR7QtFOjkqcRYcUsfw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0" - } - }, - "@babel/plugin-proposal-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-8qvuPwU/xxUCt78HocNlv0mXXo0wdh9VT1R04WU8HGOfaOob26pF+9P5/lYjN/q7DHOX1bvX60hnhOvuQUJdbA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.0" - } - }, - "@babel/plugin-proposal-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-0gkX7J7E+AtAw9fcwlVQj8peP61qhdg/89D5swOkjYbkboA2CVckn3kiyum1DE0wskGb7KJJxBdyEBApDLLVdw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.0" - } - }, - "@babel/plugin-proposal-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.8.3.tgz", - "integrity": "sha512-QIoIR9abkVn+seDE3OjA08jWcs3eZ9+wJCKSRgo3WdEU2csFYgdScb+8qHB3+WXsGJD55u+5hWCISI7ejXS+kg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.0" - } - }, - "@babel/plugin-proposal-unicode-property-regex": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.8.3.tgz", - "integrity": "sha512-1/1/rEZv2XGweRwwSkLpY+s60za9OZ1hJs4YDqFHCw0kYWYwL5IFljVY1MYBL+weT1l9pokDO2uhSTLVxzoHkQ==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-jsx": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.8.3.tgz", - "integrity": "sha512-WxdW9xyLgBdefoo0Ynn3MRSkhe5tFVxxKNVdnZSh318WrG2e2jH+E9wd/++JsqcLJZPfz87njQJ8j2Upjm0M0A==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-top-level-await": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.8.3.tgz", - "integrity": "sha512-kwj1j9lL/6Wd0hROD3b/OZZ7MSrZLqqn9RAZ5+cYYsflQ9HZBIKCUkr3+uL1MEJ1NePiUbf98jjiMQSv0NMR9g==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-transform-arrow-functions": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.8.3.tgz", - "integrity": "sha512-0MRF+KC8EqH4dbuITCWwPSzsyO3HIWWlm30v8BbbpOrS1B++isGxPnnuq/IZvOX5J2D/p7DQalQm+/2PnlKGxg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-transform-async-to-generator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.8.3.tgz", - "integrity": "sha512-imt9tFLD9ogt56Dd5CI/6XgpukMwd/fLGSrix2httihVe7LOGVPhyhMh1BU5kDM7iHD08i8uUtmV2sWaBFlHVQ==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/helper-remap-async-to-generator": "^7.8.3" - } - }, - "@babel/plugin-transform-block-scoped-functions": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.8.3.tgz", - "integrity": "sha512-vo4F2OewqjbB1+yaJ7k2EJFHlTP3jR634Z9Cj9itpqNjuLXvhlVxgnjsHsdRgASR8xYDrx6onw4vW5H6We0Jmg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-transform-block-scoping": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.8.3.tgz", - "integrity": "sha512-pGnYfm7RNRgYRi7bids5bHluENHqJhrV4bCZRwc5GamaWIIs07N4rZECcmJL6ZClwjDz1GbdMZFtPs27hTB06w==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "lodash": "^4.17.13" - } - }, - "@babel/plugin-transform-classes": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.8.3.tgz", - "integrity": "sha512-SjT0cwFJ+7Rbr1vQsvphAHwUHvSUPmMjMU/0P59G8U2HLFqSa082JO7zkbDNWs9kH/IUqpHI6xWNesGf8haF1w==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.8.3", - "@babel/helper-define-map": "^7.8.3", - "@babel/helper-function-name": "^7.8.3", - "@babel/helper-optimise-call-expression": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/helper-replace-supers": "^7.8.3", - "@babel/helper-split-export-declaration": "^7.8.3", - "globals": "^11.1.0" - } - }, - "@babel/plugin-transform-computed-properties": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.8.3.tgz", - "integrity": "sha512-O5hiIpSyOGdrQZRQ2ccwtTVkgUDBBiCuK//4RJ6UfePllUTCENOzKxfh6ulckXKc0DixTFLCfb2HVkNA7aDpzA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-transform-destructuring": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.8.3.tgz", - "integrity": "sha512-H4X646nCkiEcHZUZaRkhE2XVsoz0J/1x3VVujnn96pSoGCtKPA99ZZA+va+gK+92Zycd6OBKCD8tDb/731bhgQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-transform-dotall-regex": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.8.3.tgz", - "integrity": "sha512-kLs1j9Nn4MQoBYdRXH6AeaXMbEJFaFu/v1nQkvib6QzTj8MZI5OQzqmD83/2jEM1z0DLilra5aWO5YpyC0ALIw==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-transform-duplicate-keys": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.8.3.tgz", - "integrity": "sha512-s8dHiBUbcbSgipS4SMFuWGqCvyge5V2ZeAWzR6INTVC3Ltjig/Vw1G2Gztv0vU/hRG9X8IvKvYdoksnUfgXOEQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-transform-exponentiation-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.8.3.tgz", - "integrity": "sha512-zwIpuIymb3ACcInbksHaNcR12S++0MDLKkiqXHl3AzpgdKlFNhog+z/K0+TGW+b0w5pgTq4H6IwV/WhxbGYSjQ==", - "dev": true, - "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-transform-for-of": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.8.4.tgz", - "integrity": "sha512-iAXNlOWvcYUYoV8YIxwS7TxGRJcxyl8eQCfT+A5j8sKUzRFvJdcyjp97jL2IghWSRDaL2PU2O2tX8Cu9dTBq5A==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-transform-function-name": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.8.3.tgz", - "integrity": "sha512-rO/OnDS78Eifbjn5Py9v8y0aR+aSYhDhqAwVfsTl0ERuMZyr05L1aFSCJnbv2mmsLkit/4ReeQ9N2BgLnOcPCQ==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-transform-literals": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.8.3.tgz", - "integrity": "sha512-3Tqf8JJ/qB7TeldGl+TT55+uQei9JfYaregDcEAyBZ7akutriFrt6C/wLYIer6OYhleVQvH/ntEhjE/xMmy10A==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-transform-member-expression-literals": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.8.3.tgz", - "integrity": "sha512-3Wk2EXhnw+rP+IDkK6BdtPKsUE5IeZ6QOGrPYvw52NwBStw9V1ZVzxgK6fSKSxqUvH9eQPR3tm3cOq79HlsKYA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-transform-modules-amd": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.8.3.tgz", - "integrity": "sha512-MadJiU3rLKclzT5kBH4yxdry96odTUwuqrZM+GllFI/VhxfPz+k9MshJM+MwhfkCdxxclSbSBbUGciBngR+kEQ==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3", - "babel-plugin-dynamic-import-node": "^2.3.0" - } - }, - "@babel/plugin-transform-modules-commonjs": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.8.3.tgz", - "integrity": "sha512-JpdMEfA15HZ/1gNuB9XEDlZM1h/gF/YOH7zaZzQu2xCFRfwc01NXBMHHSTT6hRjlXJJs5x/bfODM3LiCk94Sxg==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/helper-simple-access": "^7.8.3", - "babel-plugin-dynamic-import-node": "^2.3.0" - } - }, - "@babel/plugin-transform-modules-systemjs": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.8.3.tgz", - "integrity": "sha512-8cESMCJjmArMYqa9AO5YuMEkE4ds28tMpZcGZB/jl3n0ZzlsxOAi3mC+SKypTfT8gjMupCnd3YiXCkMjj2jfOg==", - "dev": true, - "requires": { - "@babel/helper-hoist-variables": "^7.8.3", - "@babel/helper-module-transforms": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3", - "babel-plugin-dynamic-import-node": "^2.3.0" - } - }, - "@babel/plugin-transform-modules-umd": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.8.3.tgz", - "integrity": "sha512-evhTyWhbwbI3/U6dZAnx/ePoV7H6OUG+OjiJFHmhr9FPn0VShjwC2kdxqIuQ/+1P50TMrneGzMeyMTFOjKSnAw==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.8.3.tgz", - "integrity": "sha512-f+tF/8UVPU86TrCb06JoPWIdDpTNSGGcAtaD9mLP0aYGA0OS0j7j7DHJR0GTFrUZPUU6loZhbsVZgTh0N+Qdnw==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.8.3" - } - }, - "@babel/plugin-transform-new-target": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.8.3.tgz", - "integrity": "sha512-QuSGysibQpyxexRyui2vca+Cmbljo8bcRckgzYV4kRIsHpVeyeC3JDO63pY+xFZ6bWOBn7pfKZTqV4o/ix9sFw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-transform-object-super": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.8.3.tgz", - "integrity": "sha512-57FXk+gItG/GejofIyLIgBKTas4+pEU47IXKDBWFTxdPd7F80H8zybyAY7UoblVfBhBGs2EKM+bJUu2+iUYPDQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/helper-replace-supers": "^7.8.3" - } - }, - "@babel/plugin-transform-parameters": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.8.4.tgz", - "integrity": "sha512-IsS3oTxeTsZlE5KqzTbcC2sV0P9pXdec53SU+Yxv7o/6dvGM5AkTotQKhoSffhNgZ/dftsSiOoxy7evCYJXzVA==", - "dev": true, - "requires": { - "@babel/helper-call-delegate": "^7.8.3", - "@babel/helper-get-function-arity": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-transform-property-literals": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.8.3.tgz", - "integrity": "sha512-uGiiXAZMqEoQhRWMK17VospMZh5sXWg+dlh2soffpkAl96KAm+WZuJfa6lcELotSRmooLqg0MWdH6UUq85nmmg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-transform-react-display-name": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.8.3.tgz", - "integrity": "sha512-3Jy/PCw8Fe6uBKtEgz3M82ljt+lTg+xJaM4og+eyu83qLT87ZUSckn0wy7r31jflURWLO83TW6Ylf7lyXj3m5A==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-transform-react-jsx": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.8.3.tgz", - "integrity": "sha512-r0h+mUiyL595ikykci+fbwm9YzmuOrUBi0b+FDIKmi3fPQyFokWVEMJnRWHJPPQEjyFJyna9WZC6Viv6UHSv1g==", - "dev": true, - "requires": { - "@babel/helper-builder-react-jsx": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/plugin-syntax-jsx": "^7.8.3" - } - }, - "@babel/plugin-transform-react-jsx-self": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.8.3.tgz", - "integrity": "sha512-01OT7s5oa0XTLf2I8XGsL8+KqV9lx3EZV+jxn/L2LQ97CGKila2YMroTkCEIE0HV/FF7CMSRsIAybopdN9NTdg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/plugin-syntax-jsx": "^7.8.3" - } - }, - "@babel/plugin-transform-react-jsx-source": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.8.3.tgz", - "integrity": "sha512-PLMgdMGuVDtRS/SzjNEQYUT8f4z1xb2BAT54vM1X5efkVuYBf5WyGUMbpmARcfq3NaglIwz08UVQK4HHHbC6ag==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/plugin-syntax-jsx": "^7.8.3" - } - }, - "@babel/plugin-transform-regenerator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.8.3.tgz", - "integrity": "sha512-qt/kcur/FxrQrzFR432FGZznkVAjiyFtCOANjkAKwCbt465L6ZCiUQh2oMYGU3Wo8LRFJxNDFwWn106S5wVUNA==", - "dev": true, - "requires": { - "regenerator-transform": "^0.14.0" - } - }, - "@babel/plugin-transform-reserved-words": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.8.3.tgz", - "integrity": "sha512-mwMxcycN3omKFDjDQUl+8zyMsBfjRFr0Zn/64I41pmjv4NJuqcYlEtezwYtw9TFd9WR1vN5kiM+O0gMZzO6L0A==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-transform-shorthand-properties": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.8.3.tgz", - "integrity": "sha512-I9DI6Odg0JJwxCHzbzW08ggMdCezoWcuQRz3ptdudgwaHxTjxw5HgdFJmZIkIMlRymL6YiZcped4TTCB0JcC8w==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-transform-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.8.3.tgz", - "integrity": "sha512-CkuTU9mbmAoFOI1tklFWYYbzX5qCIZVXPVy0jpXgGwkplCndQAa58s2jr66fTeQnA64bDox0HL4U56CFYoyC7g==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-transform-sticky-regex": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.8.3.tgz", - "integrity": "sha512-9Spq0vGCD5Bb4Z/ZXXSK5wbbLFMG085qd2vhL1JYu1WcQ5bXqZBAYRzU1d+p79GcHs2szYv5pVQCX13QgldaWw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/helper-regex": "^7.8.3" - } - }, - "@babel/plugin-transform-template-literals": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.8.3.tgz", - "integrity": "sha512-820QBtykIQOLFT8NZOcTRJ1UNuztIELe4p9DCgvj4NK+PwluSJ49we7s9FB1HIGNIYT7wFUJ0ar2QpCDj0escQ==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-transform-typeof-symbol": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.8.4.tgz", - "integrity": "sha512-2QKyfjGdvuNfHsb7qnBBlKclbD4CfshH2KvDabiijLMGXPHJXGxtDzwIF7bQP+T0ysw8fYTtxPafgfs/c1Lrqg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-transform-unicode-regex": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.8.3.tgz", - "integrity": "sha512-+ufgJjYdmWfSQ+6NS9VGUR2ns8cjJjYbrbi11mZBTaWm+Fui/ncTLFF28Ei1okavY+xkojGr1eJxNsWYeA5aZw==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/preset-env": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.8.4.tgz", - "integrity": "sha512-HihCgpr45AnSOHRbS5cWNTINs0TwaR8BS8xIIH+QwiW8cKL0llV91njQMpeMReEPVs+1Ao0x3RLEBLtt1hOq4w==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.8.4", - "@babel/helper-compilation-targets": "^7.8.4", - "@babel/helper-module-imports": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/plugin-proposal-async-generator-functions": "^7.8.3", - "@babel/plugin-proposal-dynamic-import": "^7.8.3", - "@babel/plugin-proposal-json-strings": "^7.8.3", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-proposal-object-rest-spread": "^7.8.3", - "@babel/plugin-proposal-optional-catch-binding": "^7.8.3", - "@babel/plugin-proposal-optional-chaining": "^7.8.3", - "@babel/plugin-proposal-unicode-property-regex": "^7.8.3", - "@babel/plugin-syntax-async-generators": "^7.8.0", - "@babel/plugin-syntax-dynamic-import": "^7.8.0", - "@babel/plugin-syntax-json-strings": "^7.8.0", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0", - "@babel/plugin-syntax-object-rest-spread": "^7.8.0", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.0", - "@babel/plugin-syntax-optional-chaining": "^7.8.0", - "@babel/plugin-syntax-top-level-await": "^7.8.3", - "@babel/plugin-transform-arrow-functions": "^7.8.3", - "@babel/plugin-transform-async-to-generator": "^7.8.3", - "@babel/plugin-transform-block-scoped-functions": "^7.8.3", - "@babel/plugin-transform-block-scoping": "^7.8.3", - "@babel/plugin-transform-classes": "^7.8.3", - "@babel/plugin-transform-computed-properties": "^7.8.3", - "@babel/plugin-transform-destructuring": "^7.8.3", - "@babel/plugin-transform-dotall-regex": "^7.8.3", - "@babel/plugin-transform-duplicate-keys": "^7.8.3", - "@babel/plugin-transform-exponentiation-operator": "^7.8.3", - "@babel/plugin-transform-for-of": "^7.8.4", - "@babel/plugin-transform-function-name": "^7.8.3", - "@babel/plugin-transform-literals": "^7.8.3", - "@babel/plugin-transform-member-expression-literals": "^7.8.3", - "@babel/plugin-transform-modules-amd": "^7.8.3", - "@babel/plugin-transform-modules-commonjs": "^7.8.3", - "@babel/plugin-transform-modules-systemjs": "^7.8.3", - "@babel/plugin-transform-modules-umd": "^7.8.3", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.8.3", - "@babel/plugin-transform-new-target": "^7.8.3", - "@babel/plugin-transform-object-super": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.8.4", - "@babel/plugin-transform-property-literals": "^7.8.3", - "@babel/plugin-transform-regenerator": "^7.8.3", - "@babel/plugin-transform-reserved-words": "^7.8.3", - "@babel/plugin-transform-shorthand-properties": "^7.8.3", - "@babel/plugin-transform-spread": "^7.8.3", - "@babel/plugin-transform-sticky-regex": "^7.8.3", - "@babel/plugin-transform-template-literals": "^7.8.3", - "@babel/plugin-transform-typeof-symbol": "^7.8.4", - "@babel/plugin-transform-unicode-regex": "^7.8.3", - "@babel/types": "^7.8.3", - "browserslist": "^4.8.5", - "core-js-compat": "^3.6.2", - "invariant": "^2.2.2", - "levenary": "^1.1.1", - "semver": "^5.5.0" - } - }, - "@babel/preset-react": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.8.3.tgz", - "integrity": "sha512-9hx0CwZg92jGb7iHYQVgi0tOEHP/kM60CtWJQnmbATSPIQQ2xYzfoCI3EdqAhFBeeJwYMdWQuDUHMsuDbH9hyQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/plugin-transform-react-display-name": "^7.8.3", - "@babel/plugin-transform-react-jsx": "^7.8.3", - "@babel/plugin-transform-react-jsx-self": "^7.8.3", - "@babel/plugin-transform-react-jsx-source": "^7.8.3" - } - }, - "@babel/register": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.8.3.tgz", - "integrity": "sha512-t7UqebaWwo9nXWClIPLPloa5pN33A2leVs8Hf0e9g9YwUP8/H9NeR7DJU+4CXo23QtjChQv5a3DjEtT83ih1rg==", - "dev": true, - "requires": { - "find-cache-dir": "^2.0.0", - "lodash": "^4.17.13", - "make-dir": "^2.1.0", - "pirates": "^4.0.0", - "source-map-support": "^0.5.16" - } - }, - "@babel/runtime": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.4.tgz", - "integrity": "sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ==", - "requires": { - "regenerator-runtime": "^0.13.2" - }, - "dependencies": { - "regenerator-runtime": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", - "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==" - } - } - }, - "@babel/template": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz", - "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/parser": "^7.8.3", - "@babel/types": "^7.8.3" - } - }, - "@babel/traverse": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.4.tgz", - "integrity": "sha512-NGLJPZwnVEyBPLI+bl9y9aSnxMhsKz42so7ApAv9D+b4vAFPpY013FTS9LdKxcABoIYFU52HcYga1pPlx454mg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.8.4", - "@babel/helper-function-name": "^7.8.3", - "@babel/helper-split-export-declaration": "^7.8.3", - "@babel/parser": "^7.8.4", - "@babel/types": "^7.8.3", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@babel/types": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", - "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", - "dev": true, - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - }, - "@flowjs/ng-flow": { - "version": "2.7.8", - "resolved": "https://registry.npmjs.org/@flowjs/ng-flow/-/ng-flow-2.7.8.tgz", - "integrity": "sha1-HZ+dH4Ks2lNgMowxW6z9YNv9mBk=" - }, - "@mrmlnc/readdir-enhanced": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", - "integrity": "sha1-UkryQNGjYFJ7cwR17PoTRKpUDd4=", - "dev": true, - "requires": { - "call-me-maybe": "^1.0.1", - "glob-to-regexp": "^0.3.0" - } - }, - "@nodelib/fs.scandir": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", - "integrity": "sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.3", - "run-parallel": "^1.1.9" - }, - "dependencies": { - "@nodelib/fs.stat": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", - "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==", - "dev": true - } - } - }, - "@nodelib/fs.stat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", - "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz", - "integrity": "sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.3", - "fastq": "^1.6.0" - } - }, - "@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", - "dev": true - }, - "@types/events": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", - "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", - "dev": true - }, - "@types/glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", - "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", - "dev": true, - "requires": { - "@types/events": "*", - "@types/minimatch": "*", - "@types/node": "*" - } - }, - "@types/jquery": { - "version": "3.3.32", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.32.tgz", - "integrity": "sha512-UKoof2mnV/X1/Ix2g+V2Ny5sgHjV8nK/UJbiYxuo4zPwzGyFlZ/mp4KaePb2VqQrqJctmcDQNA57buU84/2uIw==", - "requires": { - "@types/sizzle": "*" - } - }, - "@types/minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", - "dev": true - }, - "@types/minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=", - "dev": true - }, - "@types/node": { - "version": "13.7.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.2.tgz", - "integrity": "sha512-uvilvAQbdJvnSBFcKJ2td4016urcGvsiR+N4dHGU87ml8O2Vl6l+ErOi9w0kXSPiwJ1AYlIW+0pDXDWWMOiWbw==", - "dev": true - }, - "@types/normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", - "dev": true - }, - "@types/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", - "dev": true - }, - "@types/sizzle": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz", - "integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==" - }, - "@types/unist": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz", - "integrity": "sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==", - "dev": true - }, - "@types/vfile": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/vfile/-/vfile-3.0.2.tgz", - "integrity": "sha512-b3nLFGaGkJ9rzOcuXRfHkZMdjsawuDD0ENL9fzTophtBg8FJHSGbH7daXkEpcwy3v7Xol3pAvsmlYyFhR4pqJw==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/unist": "*", - "@types/vfile-message": "*" - } - }, - "@types/vfile-message": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/vfile-message/-/vfile-message-2.0.0.tgz", - "integrity": "sha512-GpTIuDpb9u4zIO165fUy9+fXcULdD8HFRNli04GehoMVbeNq7D6OBnqSmg3lxZnC+UvgUhEWKxdKiwYUkGltIw==", - "dev": true, - "requires": { - "vfile-message": "*" - } - }, - "@webassemblyjs/ast": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", - "integrity": "sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ==", - "dev": true, - "requires": { - "@webassemblyjs/helper-module-context": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/wast-parser": "1.8.5" - } - }, - "@webassemblyjs/floating-point-hex-parser": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz", - "integrity": "sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ==", - "dev": true - }, - "@webassemblyjs/helper-api-error": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz", - "integrity": "sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA==", - "dev": true - }, - "@webassemblyjs/helper-buffer": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz", - "integrity": "sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q==", - "dev": true - }, - "@webassemblyjs/helper-code-frame": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz", - "integrity": "sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ==", - "dev": true, - "requires": { - "@webassemblyjs/wast-printer": "1.8.5" - } - }, - "@webassemblyjs/helper-fsm": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz", - "integrity": "sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow==", - "dev": true - }, - "@webassemblyjs/helper-module-context": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz", - "integrity": "sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "mamacro": "^0.0.3" - } - }, - "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz", - "integrity": "sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ==", - "dev": true - }, - "@webassemblyjs/helper-wasm-section": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz", - "integrity": "sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-buffer": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/wasm-gen": "1.8.5" - } - }, - "@webassemblyjs/ieee754": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz", - "integrity": "sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g==", - "dev": true, - "requires": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "@webassemblyjs/leb128": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.8.5.tgz", - "integrity": "sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A==", - "dev": true, - "requires": { - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/utf8": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.8.5.tgz", - "integrity": "sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw==", - "dev": true - }, - "@webassemblyjs/wasm-edit": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz", - "integrity": "sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-buffer": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/helper-wasm-section": "1.8.5", - "@webassemblyjs/wasm-gen": "1.8.5", - "@webassemblyjs/wasm-opt": "1.8.5", - "@webassemblyjs/wasm-parser": "1.8.5", - "@webassemblyjs/wast-printer": "1.8.5" - } - }, - "@webassemblyjs/wasm-gen": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz", - "integrity": "sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/ieee754": "1.8.5", - "@webassemblyjs/leb128": "1.8.5", - "@webassemblyjs/utf8": "1.8.5" - } - }, - "@webassemblyjs/wasm-opt": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz", - "integrity": "sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-buffer": "1.8.5", - "@webassemblyjs/wasm-gen": "1.8.5", - "@webassemblyjs/wasm-parser": "1.8.5" - } - }, - "@webassemblyjs/wasm-parser": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz", - "integrity": "sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-api-error": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/ieee754": "1.8.5", - "@webassemblyjs/leb128": "1.8.5", - "@webassemblyjs/utf8": "1.8.5" - } - }, - "@webassemblyjs/wast-parser": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz", - "integrity": "sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/floating-point-hex-parser": "1.8.5", - "@webassemblyjs/helper-api-error": "1.8.5", - "@webassemblyjs/helper-code-frame": "1.8.5", - "@webassemblyjs/helper-fsm": "1.8.5", - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/wast-printer": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz", - "integrity": "sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/wast-parser": "1.8.5", - "@xtuc/long": "4.2.2" - } - }, - "@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true - }, - "@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha1-+PLIh60Qv2f2NPAFtph/7TF5qsg=" - }, - "accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", - "dev": true, - "requires": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" - } - }, - "acorn": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", - "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==", - "dev": true - }, - "acorn-jsx": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz", - "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==", - "dev": true - }, - "acorn-walk": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.1.1.tgz", - "integrity": "sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ==", - "dev": true - }, - "add-dom-event-listener": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/add-dom-event-listener/-/add-dom-event-listener-1.1.0.tgz", - "integrity": "sha512-WCxx1ixHT0GQU9hb0KI/mhgRQhnU+U3GvwY6ZvVjYq8rsihIGoaIOUbY0yMPBxLH5MDtr0kz3fisWGNcbWW7Jw==", - "requires": { - "object-assign": "4.x" - } - }, - "aggregate-error": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", - "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==", - "dev": true, - "requires": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - } - }, - "ajv": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz", - "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-errors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", - "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", - "dev": true - }, - "ajv-keywords": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", - "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", - "dev": true - }, - "alter": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/alter/-/alter-0.2.0.tgz", - "integrity": "sha1-x1iICGF1cgNKrmJICvJrHU0cs80=", - "dev": true, - "requires": { - "stable": "~0.1.3" - } - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "dev": true - }, - "angular": { - "version": "1.5.8", - "resolved": "https://registry.npmjs.org/angular/-/angular-1.5.8.tgz", - "integrity": "sha1-klpTkrjCEtCVctxEbbfgEmTglMs=" - }, - "angular-animate": { - "version": "1.5.8", - "resolved": "https://registry.npmjs.org/angular-animate/-/angular-animate-1.5.8.tgz", - "integrity": "sha1-U19zaSJaun+xW/cTTHGxW8Y1xYY=" - }, - "angular-aria": { - "version": "1.5.8", - "resolved": "https://registry.npmjs.org/angular-aria/-/angular-aria-1.5.8.tgz", - "integrity": "sha1-VY+gfcgsb6V7FDjmYLvc/LgFovU=" - }, - "angular-breadcrumb": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/angular-breadcrumb/-/angular-breadcrumb-0.4.1.tgz", - "integrity": "sha1-W/9XgMeZzMYIglioQlbTR/6WkaU=" - }, - "angular-carousel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/angular-carousel/-/angular-carousel-1.1.0.tgz", - "integrity": "sha1-PmlA5ovRio85L8Qx2XGSrDSIMdE=" - }, - "angular-cookies": { - "version": "1.5.8", - "resolved": "https://registry.npmjs.org/angular-cookies/-/angular-cookies-1.5.8.tgz", - "integrity": "sha1-CPOfouF7bjqRbNO6c1L51wLoqf4=" - }, - "angular-drag-and-drop-lists": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/angular-drag-and-drop-lists/-/angular-drag-and-drop-lists-1.4.0.tgz", - "integrity": "sha1-KREEyTXhCkL4RZaW7TAGlQnXm/w=" - }, - "angular-fixed-table-header": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/angular-fixed-table-header/-/angular-fixed-table-header-0.2.1.tgz", - "integrity": "sha1-IypxTwohWKdd3DH8cvvVkjeVQY0=", - "requires": { - "angular": "^1.4.0" - } - }, - "angular-fullscreen": { - "version": "git://github.com/fabiobiondi/angular-fullscreen.git#8217174565761d3566807bc60a73b5ca015b8cb6", - "from": "git://github.com/fabiobiondi/angular-fullscreen.git#master" - }, - "angular-gridster": { - "version": "0.13.14", - "resolved": "https://registry.npmjs.org/angular-gridster/-/angular-gridster-0.13.14.tgz", - "integrity": "sha1-er6/Y9fJ++xFOLnMpFfg2oBdQgQ=" - }, - "angular-hotkeys": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/angular-hotkeys/-/angular-hotkeys-1.7.0.tgz", - "integrity": "sha1-BS5rb0TfzYJEJ3mLfxDhkX6HAKE=" - }, - "angular-jwt": { - "version": "0.1.11", - "resolved": "https://registry.npmjs.org/angular-jwt/-/angular-jwt-0.1.11.tgz", - "integrity": "sha512-793dv5vXOXaW5/cweMd+sqSf9dPhbazDya3szTVOQ84MDEj1nYLJrixBBa7WNtZeMqz7ylWX7djnFgqLjEWcHw==" - }, - "angular-material": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/angular-material/-/angular-material-1.1.19.tgz", - "integrity": "sha512-R46TfnVsAkhRDcsbrFR5DB+Ja4LpPGugAQDHYItcvLYCIkVMf07cIx1q6WyFPiZQVwz3j+ECMveqsjX+jEUrgg==" - }, - "angular-material-data-table": { - "version": "0.10.10", - "resolved": "https://registry.npmjs.org/angular-material-data-table/-/angular-material-data-table-0.10.10.tgz", - "integrity": "sha1-IptAJ0XsGhRvB9qNHlJr5lQISe4=" - }, - "angular-material-expansion-panel": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/angular-material-expansion-panel/-/angular-material-expansion-panel-0.7.2.tgz", - "integrity": "sha1-jCV5dN/xd8wHYgjQO9sKgy34Tqw=" - }, - "angular-material-icons": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/angular-material-icons/-/angular-material-icons-0.7.1.tgz", - "integrity": "sha1-5eq0hTthwzH1j55+blS24dZeT5s=", - "requires": { - "angular": ">=1.3.0" - } - }, - "angular-messages": { - "version": "1.5.8", - "resolved": "https://registry.npmjs.org/angular-messages/-/angular-messages-1.5.8.tgz", - "integrity": "sha1-H5HR5/P08uWrWt7+4smnGWsaNM8=" - }, - "angular-route": { - "version": "1.5.8", - "resolved": "https://registry.npmjs.org/angular-route/-/angular-route-1.5.8.tgz", - "integrity": "sha1-lkCT3n7I3FeZvVapOmzl9gCVZ0s=" - }, - "angular-sanitize": { - "version": "1.5.8", - "resolved": "https://registry.npmjs.org/angular-sanitize/-/angular-sanitize-1.5.8.tgz", - "integrity": "sha1-GDL2lIfuggohy+MAVaj7G98perg=" - }, - "angular-socialshare": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/angular-socialshare/-/angular-socialshare-2.3.11.tgz", - "integrity": "sha1-BTKLM9mF/Zt+FQAmPCCpqg+ELJk=" - }, - "angular-storage": { - "version": "0.0.15", - "resolved": "https://registry.npmjs.org/angular-storage/-/angular-storage-0.0.15.tgz", - "integrity": "sha1-AF1kgImChI2UlyXisr+Qod0nr8E=" - }, - "angular-touch": { - "version": "1.5.8", - "resolved": "https://registry.npmjs.org/angular-touch/-/angular-touch-1.5.8.tgz", - "integrity": "sha1-skQ8Q1+HdGF+55uxH7ae9PXVuPU=" - }, - "angular-translate": { - "version": "2.18.1", - "resolved": "https://registry.npmjs.org/angular-translate/-/angular-translate-2.18.1.tgz", - "integrity": "sha1-sp7Q0vm6xEB156rTKEFmxZ4VB5E=", - "requires": { - "angular": ">=1.2.26 <=1.7" - } - }, - "angular-translate-handler-log": { - "version": "2.18.1", - "resolved": "https://registry.npmjs.org/angular-translate-handler-log/-/angular-translate-handler-log-2.18.1.tgz", - "integrity": "sha1-icu1mCeALYb4EVJ1+/iNbYiWsNQ=", - "requires": { - "angular-translate": "~2.18.1" - } - }, - "angular-translate-interpolation-messageformat": { - "version": "2.18.1", - "resolved": "https://registry.npmjs.org/angular-translate-interpolation-messageformat/-/angular-translate-interpolation-messageformat-2.18.1.tgz", - "integrity": "sha1-FsUq4MYcJA8PJBZKBSGUPPi6QI4=", - "requires": { - "angular-translate": "~2.18.1", - "messageformat": "~1.0.2" - } - }, - "angular-translate-loader-static-files": { - "version": "2.18.1", - "resolved": "https://registry.npmjs.org/angular-translate-loader-static-files/-/angular-translate-loader-static-files-2.18.1.tgz", - "integrity": "sha1-rQw8iDsYsIm9uNsCu9Nm2QP4V8w=", - "requires": { - "angular-translate": "~2.18.1" - } - }, - "angular-translate-storage-cookie": { - "version": "2.18.1", - "resolved": "https://registry.npmjs.org/angular-translate-storage-cookie/-/angular-translate-storage-cookie-2.18.1.tgz", - "integrity": "sha1-j8vaspb6gkkOALQorxp0ahf0QVY=", - "requires": { - "angular-cookies": ">=1.2.26 <1.8", - "angular-translate": "~2.18.1" - } - }, - "angular-translate-storage-local": { - "version": "2.18.1", - "resolved": "https://registry.npmjs.org/angular-translate-storage-local/-/angular-translate-storage-local-2.18.1.tgz", - "integrity": "sha1-lHQP5NgBq3gpopofBeHDkFTIcwM=", - "requires": { - "angular-translate": "~2.18.1", - "angular-translate-storage-cookie": "~2.18.1" - } - }, - "angular-ui-ace": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/angular-ui-ace/-/angular-ui-ace-0.2.3.tgz", - "integrity": "sha1-PLkDQoEAYho2f8f2QUQOl6QqJtA=" - }, - "angular-ui-router": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/angular-ui-router/-/angular-ui-router-0.3.2.tgz", - "integrity": "sha1-wn4EljCcmSGNVlWYWxZKCWq1IKk=", - "requires": { - "angular": "^1.0.8" - } - }, - "angular-websocket": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/angular-websocket/-/angular-websocket-2.0.1.tgz", - "integrity": "sha1-D7TWDtkBi+WD0DHuxOVrRXy43O4=", - "requires": { - "angular": "*", - "ws": "^1.0.0" - } - }, - "ansi-colors": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", - "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", - "dev": true - }, - "ansi-escapes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", - "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=" - }, - "ansi-html": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", - "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=", - "dev": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - }, - "dependencies": { - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } - } - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha1-aALmJk79GMeQobDVF/DyYnvyyUo=", - "dev": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "dev": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha1-vNZ5HqWuCXJeF+WtmIE0zUCz2RE=", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha1-NgSLv/TntH4TZkQxbJlmnqWukfE=", - "dev": true - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, - "array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", - "dev": true - }, - "array-flatten": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", - "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", - "dev": true - }, - "array-includes": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.1.tgz", - "integrity": "sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0", - "is-string": "^1.0.5" - } - }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "dev": true, - "requires": { - "array-uniq": "^1.0.1" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "array.prototype.flat": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz", - "integrity": "sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1" - } - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true - }, - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dev": true, - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "asn1.js": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", - "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", - "dev": true, - "requires": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "assert": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", - "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", - "dev": true, - "requires": { - "object-assign": "^4.1.1", - "util": "0.10.3" - }, - "dependencies": { - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", - "dev": true - }, - "util": { - "version": "0.10.3", - "resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", - "dev": true, - "requires": { - "inherits": "2.0.1" - } - } - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true - }, - "ast-types": { - "version": "0.9.6", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.9.6.tgz", - "integrity": "sha1-ECyenpAF0+fjgpvwxPok7oYu6bk=", - "dev": true - }, - "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", - "dev": true - }, - "async": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/async/-/async-3.1.1.tgz", - "integrity": "sha512-X5Dj8hK1pJNC2Wzo2Rcp9FBVdJMGRR/S7V+lH46s8GVFhtbo5O4Le5GECCF/8PISVdkUA6mMPvgz7qTTD1rf1g==" - }, - "async-each": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", - "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", - "dev": true - }, - "async-foreach": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", - "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", - "dev": true - }, - "async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha1-bZUX654DDSQ2ZmZR6GvZ9vE1M8k=", - "dev": true - }, - "attr-accept": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-1.1.3.tgz", - "integrity": "sha512-iT40nudw8zmCweivz6j58g+RT33I4KbaIvRUhjNmDwO2WmsQUxFEZZYZ5w3vXe5x5MX9D7mfvA/XaLOZYFR9EQ==", - "requires": { - "core-js": "^2.5.0" - } - }, - "autoprefixer": { - "version": "9.7.4", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.7.4.tgz", - "integrity": "sha512-g0Ya30YrMBAEZk60lp+qfX5YQllG+S5W3GYCFvyHTvhOki0AEQJLPEcIuGRsqVwLi8FvXPVtwTGhfr38hVpm0g==", - "dev": true, - "requires": { - "browserslist": "^4.8.3", - "caniuse-lite": "^1.0.30001020", - "chalk": "^2.4.2", - "normalize-range": "^0.1.2", - "num2fraction": "^1.2.2", - "postcss": "^7.0.26", - "postcss-value-parser": "^4.0.2" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "browserslist": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.8.5.tgz", - "integrity": "sha512-4LMHuicxkabIB+n9874jZX/az1IaZ5a+EUuvD7KFOu9x/Bd5YHyO0DIz2ls/Kl8g0ItS4X/ilEgf4T1Br0lgSg==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001022", - "electron-to-chromium": "^1.3.338", - "node-releases": "^1.1.46" - } - }, - "caniuse-lite": { - "version": "1.0.30001023", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001023.tgz", - "integrity": "sha512-C5TDMiYG11EOhVOA62W1p3UsJ2z4DsHtMBQtjzp3ZsUglcQn62WOUgW0y795c7A5uZ+GCEIvzkMatLIlAsbNTA==", - "dev": true - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "electron-to-chromium": { - "version": "1.3.341", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.341.tgz", - "integrity": "sha512-iezlV55/tan1rvdvt7yg7VHRSkt+sKfzQ16wTDqTbQqtl4+pSUkKPXpQHDvEt0c7gKcUHHwUbffOgXz6bn096g==", - "dev": true - }, - "node-releases": { - "version": "1.1.47", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.47.tgz", - "integrity": "sha512-k4xjVPx5FpwBUj0Gw7uvFOTF4Ep8Hok1I6qjwL3pLfwe7Y0REQSAqOwwv9TWBCUtMHxcXfY4PgRLRozcChvTcA==", - "dev": true, - "requires": { - "semver": "^6.3.0" - } - }, - "postcss": { - "version": "7.0.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.26.tgz", - "integrity": "sha512-IY4oRjpXWYshuTDFxMVkJDtWIk2LhsTlu8bZnbEJA4+bYT16Lvpo8Qv6EvDumhYRgzjZl489pmsY3qVgJQ08nA==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - }, - "dependencies": { - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-value-parser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz", - "integrity": "sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", - "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==", - "dev": true - }, - "babel-eslint": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.0.3.tgz", - "integrity": "sha512-z3U7eMY6r/3f3/JB9mTsLjyxrv0Yb1zb8PCWCLpguxfCzBIZUwy23R1t/XKewP+8mEN2Ck8Dtr4q20z6ce6SoA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.0.0", - "@babel/traverse": "^7.0.0", - "@babel/types": "^7.0.0", - "eslint-visitor-keys": "^1.0.0", - "resolve": "^1.12.0" - } - }, - "babel-loader": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.0.6.tgz", - "integrity": "sha512-4BmWKtBOBm13uoUwd08UwjZlaw3O9GWf456R9j+5YykFZ6LUIjIKLc0zEZf+hauxPOJs96C8k6FvYD09vWzhYw==", - "dev": true, - "requires": { - "find-cache-dir": "^2.0.0", - "loader-utils": "^1.0.2", - "mkdirp": "^0.5.1", - "pify": "^4.0.1" - } - }, - "babel-plugin-dynamic-import-node": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz", - "integrity": "sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ==", - "dev": true, - "requires": { - "object.assign": "^4.1.0" - } - }, - "babel-polyfill": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.23.0.tgz", - "integrity": "sha1-g2TKYt+Or7gwSZ9pkXdGbDsDSZ0=", - "requires": { - "babel-runtime": "^6.22.0", - "core-js": "^2.4.0", - "regenerator-runtime": "^0.10.0" - }, - "dependencies": { - "regenerator-runtime": { - "version": "0.10.5", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", - "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=" - } - } - }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - } - }, - "bail": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", - "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==", - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha1-e95c7RRbbVUakNuH+DxVi060io8=", - "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "base64-js": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" - }, - "batch": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true - }, - "binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "dev": true - }, - "bind-decorator": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/bind-decorator/-/bind-decorator-1.0.11.tgz", - "integrity": "sha1-5BvAah9l3ZzsR2yRxdrzl4SIJS8=" - }, - "bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "optional": true, - "requires": { - "file-uri-to-path": "1.0.0" - } - }, - "block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "dev": true, - "requires": { - "inherits": "~2.0.0" - } - }, - "bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true - }, - "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", - "dev": true - }, - "body-parser": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", - "dev": true, - "requires": { - "bytes": "3.1.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" - }, - "dependencies": { - "bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", - "dev": true - }, - "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", - "dev": true - } - } - }, - "bonjour": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", - "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", - "dev": true, - "requires": { - "array-flatten": "^2.1.0", - "deep-equal": "^1.0.1", - "dns-equal": "^1.0.0", - "dns-txt": "^2.0.2", - "multicast-dns": "^6.0.1", - "multicast-dns-service-types": "^1.1.0" - } - }, - "boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", - "dev": true - }, - "bowser": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-1.9.4.tgz", - "integrity": "sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ==" - }, - "brace": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/brace/-/brace-0.10.0.tgz", - "integrity": "sha1-7e9OubCSi6HuX3F//BV3SabdXXY=", - "requires": { - "w3c-blob": "0.0.1" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha1-PH/L9SnYcibz0vUrlm/1Jx60Qd0=", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", - "dev": true - }, - "browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "dev": true, - "requires": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "dev": true, - "requires": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "dev": true, - "requires": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "browserify-rsa": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", - "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "randombytes": "^2.0.1" - } - }, - "browserify-sign": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", - "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", - "dev": true, - "requires": { - "bn.js": "^4.1.1", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.2", - "elliptic": "^6.0.0", - "inherits": "^2.0.1", - "parse-asn1": "^5.0.0" - } - }, - "browserify-zlib": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", - "dev": true, - "requires": { - "pako": "~1.0.5" - } - }, - "browserslist": { - "version": "4.8.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.8.7.tgz", - "integrity": "sha512-gFOnZNYBHrEyUML0xr5NJ6edFaaKbTFX9S9kQHlYfCP0Rit/boRIz4G+Avq6/4haEKJXdGGUnoolx+5MWW2BoA==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001027", - "electron-to-chromium": "^1.3.349", - "node-releases": "^1.1.49" - } - }, - "buffer": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", - "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", - "dev": true, - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true - }, - "buffer-indexof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", - "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", - "dev": true - }, - "buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", - "dev": true - }, - "builtin-status-codes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", - "dev": true - }, - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", - "dev": true - }, - "cacache": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-13.0.1.tgz", - "integrity": "sha512-5ZvAxd05HDDU+y9BVvcqYu2LLXmPnQ0hW62h32g4xBTgL/MppR4/04NHfj/ycM2y6lmTnbw6HVi+1eN0Psba6w==", - "dev": true, - "requires": { - "chownr": "^1.1.2", - "figgy-pudding": "^3.5.1", - "fs-minipass": "^2.0.0", - "glob": "^7.1.4", - "graceful-fs": "^4.2.2", - "infer-owner": "^1.0.4", - "lru-cache": "^5.1.1", - "minipass": "^3.0.0", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.2", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "p-map": "^3.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^2.7.1", - "ssri": "^7.0.0", - "unique-filename": "^1.1.1" - }, - "dependencies": { - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "requires": { - "yallist": "^3.0.2" - } - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - } - } - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha1-Cn9GQWgxyLZi7jb+TnxZ129marI=", - "dev": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - } - }, - "call-me-maybe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", - "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", - "dev": true - }, - "caller-callsite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", - "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", - "dev": true, - "requires": { - "callsites": "^2.0.0" - }, - "dependencies": { - "callsites": { - "version": "2.0.0", - "resolved": "http://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", - "dev": true - } - } - }, - "caller-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", - "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", - "dev": true, - "requires": { - "caller-callsite": "^2.0.0" - } - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "camel-case": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", - "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", - "dev": true, - "requires": { - "no-case": "^2.2.0", - "upper-case": "^1.1.1" - } - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "camelcase-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", - "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", - "dev": true, - "requires": { - "camelcase": "^2.0.0", - "map-obj": "^1.0.0" - }, - "dependencies": { - "camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", - "dev": true - } - } - }, - "caniuse-lite": { - "version": "1.0.30001028", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001028.tgz", - "integrity": "sha512-Vnrq+XMSHpT7E+LWoIYhs3Sne8h9lx9YJV3acH3THNCwU/9zV93/ta4xVfzTtnqd3rvnuVpVjE3DFqf56tr3aQ==", - "dev": true - }, - "canvas-gauges": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/canvas-gauges/-/canvas-gauges-2.1.5.tgz", - "integrity": "sha512-7GUd1uukePQPQPIoM8Sh4UrG8om+2RG+D8WN5BCkIp9wAfByzPuZZinsUkfFCyRrEOZ/rhuwBfFnb1ld8IfNrw==" - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true - }, - "ccount": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-1.0.5.tgz", - "integrity": "sha512-MOli1W+nfbPLlKEhInaxhRdp7KVLFxLN5ykwzHgLsLI3H3gs5jjFAK4Eoj3OzzcxCtumDaI8onoVDeQyWaNTkw==", - "dev": true - }, - "chain-function": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/chain-function/-/chain-function-1.0.1.tgz", - "integrity": "sha512-SxltgMwL9uCko5/ZCLiyG2B7R9fY4pDZUw7hJ4MhirdjBLosoDqkWABi3XMucddHdLiFJMb7PD2MZifZriuMTg==" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "change-emitter": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/change-emitter/-/change-emitter-0.1.6.tgz", - "integrity": "sha1-6LL+PX8at9aaMhma/5HqaTFAlRU=" - }, - "character-entities": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", - "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", - "dev": true - }, - "character-entities-html4": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-1.1.4.tgz", - "integrity": "sha512-HRcDxZuZqMx3/a+qrzxdBKBPUpxWEq9xw2OPZ3a/174ihfrQKVsFhqtthBInFy1zZ9GgZyFXOatNujm8M+El3g==", - "dev": true - }, - "character-entities-legacy": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", - "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", - "dev": true - }, - "character-reference-invalid": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", - "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", - "dev": true - }, - "chardet": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", - "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=" - }, - "chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - } - }, - "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true - }, - "chrome-trace-event": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", - "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha1-+TNprouafOAv1B+q0MqDAzGQxGM=", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "classnames": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", - "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" - }, - "clean-css": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", - "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", - "dev": true, - "requires": { - "source-map": "~0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" - }, - "clipboard": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-1.7.1.tgz", - "integrity": "sha1-Ng1taUbpmnof7zleQrqStem1oWs=", - "requires": { - "good-listener": "^1.2.2", - "select": "^1.1.2", - "tiny-emitter": "^2.0.0" - } - }, - "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - } - } - }, - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" - }, - "clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - } - }, - "clone-regexp": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-2.2.0.tgz", - "integrity": "sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q==", - "dev": true, - "requires": { - "is-regexp": "^2.0.0" - } - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "collapse-white-space": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.6.tgz", - "integrity": "sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==", - "dev": true - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", - "dev": true - }, - "compass-sass-mixins": { - "version": "0.12.7", - "resolved": "https://registry.npmjs.org/compass-sass-mixins/-/compass-sass-mixins-0.12.7.tgz", - "integrity": "sha1-KsTTEPLr5Ri31qykriTx0yVAnow=" - }, - "component-classes": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/component-classes/-/component-classes-1.2.6.tgz", - "integrity": "sha1-xkI5TDYYpNiwuJGe/Mu9kw5c1pE=", - "requires": { - "component-indexof": "0.0.3" - } - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "component-indexof": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/component-indexof/-/component-indexof-0.0.3.tgz", - "integrity": "sha1-EdCRMSI5648yyPJa6csAL/6NPCQ=" - }, - "compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dev": true, - "requires": { - "mime-db": ">= 1.43.0 < 2" - } - }, - "compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "dev": true, - "requires": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - } - }, - "compression-webpack-plugin": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/compression-webpack-plugin/-/compression-webpack-plugin-3.1.0.tgz", - "integrity": "sha512-iqTHj3rADN4yHwXMBrQa/xrncex/uEQy8QHlaTKxGchT/hC0SdlJlmL/5eRqffmWq2ep0/Romw6Ld39JjTR/ug==", - "dev": true, - "requires": { - "cacache": "^13.0.1", - "find-cache-dir": "^3.0.0", - "neo-async": "^2.5.0", - "schema-utils": "^2.6.1", - "serialize-javascript": "^2.1.2", - "webpack-sources": "^1.0.1" - }, - "dependencies": { - "find-cache-dir": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.2.0.tgz", - "integrity": "sha512-1JKclkYYsf1q9WIJKLZa9S9muC+08RIjzAlLrK4QcYLJMS6mk9yombQ9qf+zJ7H9LS800k0s44L4sDq9VYzqyg==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^3.0.0", - "pkg-dir": "^4.1.0" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "make-dir": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz", - "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==", - "dev": true, - "requires": { - "semver": "^6.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha1-kEvfGUzTEi/Gdcd/xKw9T/D9GjQ=", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "config-chain": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", - "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", - "requires": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" - } - }, - "connect-history-api-fallback": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", - "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", - "dev": true - }, - "console-browserify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", - "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true - }, - "constants-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", - "dev": true - }, - "contains-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", - "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", - "dev": true - }, - "content-disposition": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", - "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", - "dev": true, - "requires": { - "safe-buffer": "5.1.2" - } - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js=", - "dev": true - }, - "convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", - "dev": true - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", - "dev": true - }, - "copy-concurrently": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", - "integrity": "sha1-kilzmMrjSTf8r9bsgTnBgFHwteA=", - "dev": true, - "requires": { - "aproba": "^1.1.1", - "fs-write-stream-atomic": "^1.0.8", - "iferr": "^0.1.5", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.0" - } - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, - "copy-webpack-plugin": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-5.1.1.tgz", - "integrity": "sha512-P15M5ZC8dyCjQHWwd4Ia/dm0SgVvZJMYeykVIVYXbGyqO4dWB5oyPHp9i7wjwo5LhtlhKbiBCdS2NvM07Wlybg==", - "dev": true, - "requires": { - "cacache": "^12.0.3", - "find-cache-dir": "^2.1.0", - "glob-parent": "^3.1.0", - "globby": "^7.1.1", - "is-glob": "^4.0.1", - "loader-utils": "^1.2.3", - "minimatch": "^3.0.4", - "normalize-path": "^3.0.0", - "p-limit": "^2.2.1", - "schema-utils": "^1.0.0", - "serialize-javascript": "^2.1.2", - "webpack-log": "^2.0.0" - }, - "dependencies": { - "cacache": { - "version": "12.0.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.3.tgz", - "integrity": "sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==", - "dev": true, - "requires": { - "bluebird": "^3.5.5", - "chownr": "^1.1.1", - "figgy-pudding": "^3.5.1", - "glob": "^7.1.4", - "graceful-fs": "^4.1.15", - "infer-owner": "^1.0.3", - "lru-cache": "^5.1.1", - "mississippi": "^3.0.0", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "promise-inflight": "^1.0.1", - "rimraf": "^2.6.3", - "ssri": "^6.0.1", - "unique-filename": "^1.1.1", - "y18n": "^4.0.0" - } - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "requires": { - "yallist": "^3.0.2" - } - }, - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - }, - "ssri": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", - "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", - "dev": true, - "requires": { - "figgy-pudding": "^3.5.1" - } - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - } - } - }, - "core-js": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", - "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==" - }, - "core-js-compat": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.6.4.tgz", - "integrity": "sha512-zAa3IZPvsJ0slViBQ2z+vgyyTuhd3MFn1rBQjZSKVEgB0UMYhUkCj9jJUVPgGTGqWvsBVmfnruXgTcNyTlEiSA==", - "dev": true, - "requires": { - "browserslist": "^4.8.3", - "semver": "7.0.0" - }, - "dependencies": { - "semver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", - "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", - "dev": true - } - } - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "cosmiconfig": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", - "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", - "dev": true, - "requires": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.13.1", - "parse-json": "^4.0.0" - }, - "dependencies": { - "import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", - "dev": true, - "requires": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" - } - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - } - } - }, - "create-ecdh": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", - "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "elliptic": "^6.0.0" - } - }, - "create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "dev": true, - "requires": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dev": true, - "requires": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "create-react-class": { - "version": "15.6.3", - "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.3.tgz", - "integrity": "sha1-LXMjf7P5cK5uvgEanmb0bbyoADY=", - "requires": { - "fbjs": "^0.8.9", - "loose-envify": "^1.3.1", - "object-assign": "^4.1.1" - } - }, - "cross-env": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.2.1.tgz", - "integrity": "sha512-1yHhtcfAd1r4nwQgknowuUNfIT9E8dOMMspC36g45dN+iD1blloi7xp8X/xAIDnjHWyt1uQ8PHk2fkNaym7soQ==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.5" - } - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "dev": true, - "requires": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - } - }, - "css-animation": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/css-animation/-/css-animation-1.6.1.tgz", - "integrity": "sha512-/48+/BaEaHRY6kNQ2OIPzKf9A6g8WjZYjhiNDNuIVbsm5tXCGIAsHDjB4Xu1C4vXJtUWZo26O68OQkDpNBaPog==", - "requires": { - "babel-runtime": "6.x", - "component-classes": "^1.2.5" - } - }, - "css-loader": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.4.2.tgz", - "integrity": "sha512-jYq4zdZT0oS0Iykt+fqnzVLRIeiPWhka+7BqPn+oSIpWJAHak5tmB/WZrJ2a21JhCeFyNnnlroSl8c+MtVndzA==", - "dev": true, - "requires": { - "camelcase": "^5.3.1", - "cssesc": "^3.0.0", - "icss-utils": "^4.1.1", - "loader-utils": "^1.2.3", - "normalize-path": "^3.0.0", - "postcss": "^7.0.23", - "postcss-modules-extract-imports": "^2.0.0", - "postcss-modules-local-by-default": "^3.0.2", - "postcss-modules-scope": "^2.1.1", - "postcss-modules-values": "^3.0.0", - "postcss-value-parser": "^4.0.2", - "schema-utils": "^2.6.0" - } - }, - "css-select": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", - "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", - "dev": true, - "requires": { - "boolbase": "~1.0.0", - "css-what": "2.1", - "domutils": "1.5.1", - "nth-check": "~1.0.1" - } - }, - "css-what": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", - "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", - "dev": true - }, - "cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true - }, - "currently-unhandled": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", - "dev": true, - "requires": { - "array-find-index": "^1.0.1" - } - }, - "cyclist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", - "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=", - "dev": true - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "decamelize-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", - "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", - "dev": true, - "requires": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" - } - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true - }, - "deep-equal": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", - "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", - "dev": true, - "requires": { - "is-arguments": "^1.0.4", - "is-date-object": "^1.0.1", - "is-regex": "^1.0.4", - "object-is": "^1.0.1", - "object-keys": "^1.1.1", - "regexp.prototype.flags": "^1.2.0" - } - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "default-gateway": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", - "integrity": "sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "ip-regex": "^2.1.0" - } - }, - "defaults": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", - "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", - "requires": { - "clone": "^1.0.2" - } - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "requires": { - "object-keys": "^1.0.12" - } - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha1-1Flono1lS6d+AqgX+HENcCyxbp0=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "del": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", - "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", - "dev": true, - "requires": { - "@types/glob": "^7.1.1", - "globby": "^6.1.0", - "is-path-cwd": "^2.0.0", - "is-path-in-cwd": "^2.0.0", - "p-map": "^2.0.0", - "pify": "^4.0.1", - "rimraf": "^2.6.3" - }, - "dependencies": { - "globby": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", - "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", - "dev": true, - "requires": { - "array-union": "^1.0.1", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - } - } - }, - "p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", - "dev": true - } - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "delegate": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", - "integrity": "sha1-tmtxwxWFIuirV0T3INjKDCr1kWY=" - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "dev": true - }, - "des.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", - "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", - "dev": true - }, - "detect-file": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", - "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", - "dev": true - }, - "detect-node": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", - "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", - "dev": true - }, - "diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - } - }, - "dir-glob": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", - "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", - "dev": true, - "requires": { - "path-type": "^3.0.0" - } - }, - "directory-tree": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/directory-tree/-/directory-tree-2.2.4.tgz", - "integrity": "sha512-2N43msQptKbi3WMfIs+U09yi6bfyKL+MWyj5VMj8t1F/Tx04bt1cn/EEIU3o1JBltlJk7NQnzOEuTNa/KQvbWA==", - "dev": true - }, - "dns-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", - "dev": true - }, - "dns-packet": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz", - "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==", - "dev": true, - "requires": { - "ip": "^1.1.0", - "safe-buffer": "^5.0.1" - } - }, - "dns-txt": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", - "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", - "dev": true, - "requires": { - "buffer-indexof": "^1.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "dom-align": { - "version": "1.10.4", - "resolved": "https://registry.npmjs.org/dom-align/-/dom-align-1.10.4.tgz", - "integrity": "sha512-wytDzaru67AmqFOY4B9GUb/hrwWagezoYYK97D/vpK+ezg+cnuZO0Q2gltUPa7KfNmIqfRIYVCF8UhRDEHAmgQ==" - }, - "dom-converter": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", - "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", - "dev": true, - "requires": { - "utila": "~0.4" - } - }, - "dom-helpers": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", - "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", - "requires": { - "@babel/runtime": "^7.1.2" - } - }, - "dom-scroll-into-view": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/dom-scroll-into-view/-/dom-scroll-into-view-1.2.1.tgz", - "integrity": "sha1-6PNnMt0ImwIBqI14Fdw/iObWbH4=" - }, - "dom-serializer": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", - "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", - "dev": true, - "requires": { - "domelementtype": "^2.0.1", - "entities": "^2.0.0" - }, - "dependencies": { - "domelementtype": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", - "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==", - "dev": true - } - } - }, - "dom-walk": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz", - "integrity": "sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg=", - "dev": true - }, - "domain-browser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", - "integrity": "sha1-PTH1AZGmdJ3RN1p/Ui6CPULlTto=", - "dev": true - }, - "domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", - "dev": true - }, - "domhandler": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", - "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", - "dev": true, - "requires": { - "domelementtype": "1" - } - }, - "domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", - "dev": true, - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", - "dev": true, - "requires": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dev": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "editorconfig": { - "version": "0.15.3", - "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", - "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==", - "requires": { - "commander": "^2.19.0", - "lru-cache": "^4.1.5", - "semver": "^5.6.0", - "sigmund": "^1.0.1" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", - "dev": true - }, - "electron-to-chromium": { - "version": "1.3.355", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.355.tgz", - "integrity": "sha512-zKO/wS+2ChI/jz9WAo647xSW8t2RmgRLFdbUb/77cORkUTargO+SCj4ctTHjBn2VeNFrsLgDT7IuDVrd3F8mLQ==", - "dev": true - }, - "elliptic": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz", - "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==", - "dev": true, - "requires": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" - } - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "emojis-list": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", - "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", - "dev": true - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", - "dev": true - }, - "encoding": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", - "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", - "requires": { - "iconv-lite": "~0.4.13" - } - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "enhanced-resolve": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz", - "integrity": "sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.5.0", - "tapable": "^1.0.0" - }, - "dependencies": { - "memory-fs": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", - "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", - "dev": true, - "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - } - } - } - }, - "entities": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz", - "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==", - "dev": true - }, - "errno": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", - "integrity": "sha1-RoTXF3mtOa8Xfj8AeZb3xnyFJhg=", - "dev": true, - "requires": { - "prr": "~1.0.1" - } - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "es-abstract": { - "version": "1.17.4", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", - "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.1.5", - "is-regex": "^1.0.5", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimleft": "^2.1.1", - "string.prototype.trimright": "^2.1.1" - } - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "es6-templates": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/es6-templates/-/es6-templates-0.2.3.tgz", - "integrity": "sha1-XLmsn7He1usSOTQrgdeSu7QHjuQ=", - "dev": true, - "requires": { - "recast": "~0.11.12", - "through": "~2.3.6" - } - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "eslint": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", - "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "ajv": "^6.10.0", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^1.4.3", - "eslint-visitor-keys": "^1.1.0", - "espree": "^6.1.2", - "esquery": "^1.0.1", - "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", - "globals": "^12.1.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "inquirer": "^7.0.0", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.14", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.3", - "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^6.1.2", - "strip-ansi": "^5.2.0", - "strip-json-comments": "^3.0.1", - "table": "^5.2.3", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "dependencies": { - "ansi-escapes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.0.tgz", - "integrity": "sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg==", - "dev": true, - "requires": { - "type-fest": "^0.8.1" - } - }, - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true - }, - "cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "requires": { - "restore-cursor": "^3.1.0" - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "requires": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - } - }, - "figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "glob-parent": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", - "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "globals": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.3.0.tgz", - "integrity": "sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw==", - "dev": true, - "requires": { - "type-fest": "^0.8.1" - } - }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true - }, - "inquirer": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.4.tgz", - "integrity": "sha512-Bu5Td5+j11sCkqfqmUTiwv+tWisMtP0L7Q8WrqA2C/BbBhy1YTdFrvjjlrKq8oagA/tLQBski2Gcx/Sqyi2qSQ==", - "dev": true, - "requires": { - "ansi-escapes": "^4.2.1", - "chalk": "^2.4.2", - "cli-cursor": "^3.1.0", - "cli-width": "^2.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.15", - "mute-stream": "0.0.8", - "run-async": "^2.2.0", - "rxjs": "^6.5.3", - "string-width": "^4.1.0", - "strip-ansi": "^5.1.0", - "through": "^2.3.6" - } - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true - }, - "onetime": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", - "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "requires": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - } - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - } - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "eslint-config-angular": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/eslint-config-angular/-/eslint-config-angular-0.5.0.tgz", - "integrity": "sha1-4KrgEy4550Z98/dUf+yBpE02hcQ=", - "dev": true - }, - "eslint-import-resolver-node": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.3.tgz", - "integrity": "sha512-b8crLDo0M5RSe5YG8Pu2DYBj71tSB6OvXkfzwbJU2w7y8P4/yo0MyF8jU26IEuEuHF2K5/gcAJE3LhQGqBBbVg==", - "dev": true, - "requires": { - "debug": "^2.6.9", - "resolve": "^1.13.1" - } - }, - "eslint-loader": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/eslint-loader/-/eslint-loader-2.2.1.tgz", - "integrity": "sha512-RLgV9hoCVsMLvOxCuNjdqOrUqIj9oJg8hF44vzJaYqsAHuY9G2YAeN3joQ9nxP0p5Th9iFSIpKo+SD8KISxXRg==", - "dev": true, - "requires": { - "loader-fs-cache": "^1.0.0", - "loader-utils": "^1.0.2", - "object-assign": "^4.0.1", - "object-hash": "^1.1.4", - "rimraf": "^2.6.1" - } - }, - "eslint-module-utils": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.5.2.tgz", - "integrity": "sha512-LGScZ/JSlqGKiT8OC+cYRxseMjyqt6QO54nl281CK93unD89ijSeRV6An8Ci/2nvWVKe8K/Tqdm75RQoIOCr+Q==", - "dev": true, - "requires": { - "debug": "^2.6.9", - "pkg-dir": "^2.0.0" - }, - "dependencies": { - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "pkg-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", - "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", - "dev": true, - "requires": { - "find-up": "^2.1.0" - } - } - } - }, - "eslint-plugin-angular": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-angular/-/eslint-plugin-angular-4.0.1.tgz", - "integrity": "sha512-OaW5G461C2lIkOG+/bhnBoXB9UQm/r0Dj2Qf9uiIN0/ncvf2Llp30L0q1tqWkN8/CxyBwQKh1v0hpCLLDjaIKQ==", - "dev": true - }, - "eslint-plugin-import": { - "version": "2.20.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.20.1.tgz", - "integrity": "sha512-qQHgFOTjguR+LnYRoToeZWT62XM55MBVXObHM6SKFd1VzDcX/vqT1kAz8ssqigh5eMj8qXcRoXXGZpPP6RfdCw==", - "dev": true, - "requires": { - "array-includes": "^3.0.3", - "array.prototype.flat": "^1.2.1", - "contains-path": "^0.1.0", - "debug": "^2.6.9", - "doctrine": "1.5.0", - "eslint-import-resolver-node": "^0.3.2", - "eslint-module-utils": "^2.4.1", - "has": "^1.0.3", - "minimatch": "^3.0.4", - "object.values": "^1.1.0", - "read-pkg-up": "^2.0.0", - "resolve": "^1.12.0" - }, - "dependencies": { - "doctrine": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", - "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", - "dev": true, - "requires": { - "esutils": "^2.0.2", - "isarray": "^1.0.0" - } - } - } - }, - "eslint-scope": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", - "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - } - }, - "eslint-visitor-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", - "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", - "dev": true - }, - "espree": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.2.tgz", - "integrity": "sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==", - "dev": true, - "requires": { - "acorn": "^7.1.0", - "acorn-jsx": "^5.1.0", - "eslint-visitor-keys": "^1.1.0" - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "esquery": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.1.0.tgz", - "integrity": "sha512-MxYW9xKmROWF672KqjO75sszsA8Mxhw06YFeS5VHlB98KDHbOSurm3ArsjO60Eaf3QmGMCP1yn+0JQkNLo/97Q==", - "dev": true, - "requires": { - "estraverse": "^4.0.0" - } - }, - "esrecurse": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha1-AHo7n9vCs7uH5IeeoZyS/b05Qs8=", - "dev": true, - "requires": { - "estraverse": "^4.1.0" - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", - "dev": true - }, - "eve-raphael": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/eve-raphael/-/eve-raphael-0.5.0.tgz", - "integrity": "sha1-F8dUt5K+7z+maE15z1pHxjxM2jA=" - }, - "event-source-polyfill": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/event-source-polyfill/-/event-source-polyfill-0.0.9.tgz", - "integrity": "sha1-GMYgXRcKsJ24if/OqjPw5JPxSlA=" - }, - "eventemitter3": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz", - "integrity": "sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==", - "dev": true - }, - "events": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz", - "integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==", - "dev": true - }, - "eventsource": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz", - "integrity": "sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==", - "dev": true, - "requires": { - "original": "^1.0.0" - } - }, - "evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dev": true, - "requires": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "execall": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/execall/-/execall-2.0.0.tgz", - "integrity": "sha512-0FU2hZ5Hh6iQnarpRtQurM/aAvp3RIbfvgLHrcqJYzhXyV2KFruhuChf9NC6waAhiUR7FFtlugkI4p7f2Fqlow==", - "dev": true, - "requires": { - "clone-regexp": "^2.1.0" - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "expand-tilde": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1" - } - }, - "express": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", - "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", - "dev": true, - "requires": { - "accepts": "~1.3.7", - "array-flatten": "1.1.1", - "body-parser": "1.19.0", - "content-disposition": "0.5.3", - "content-type": "~1.0.4", - "cookie": "0.4.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.1.2", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.5", - "qs": "6.7.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.1.2", - "send": "0.17.1", - "serve-static": "1.14.1", - "setprototypeof": "1.1.1", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "dependencies": { - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", - "dev": true - }, - "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", - "dev": true - } - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "external-editor": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", - "integrity": "sha1-BFURz9jRM/OEZnPRBHwVTiFK09U=", - "requires": { - "chardet": "^0.4.0", - "iconv-lite": "^0.4.17", - "tmp": "^0.0.33" - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", - "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", - "dev": true - }, - "fast-glob": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", - "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==", - "dev": true, - "requires": { - "@mrmlnc/readdir-enhanced": "^2.2.1", - "@nodelib/fs.stat": "^1.1.2", - "glob-parent": "^3.1.0", - "is-glob": "^4.0.0", - "merge2": "^1.2.3", - "micromatch": "^3.1.10" - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "fastparse": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", - "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", - "dev": true - }, - "fastq": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.6.0.tgz", - "integrity": "sha512-jmxqQ3Z/nXoeyDmWAzF9kH1aGZSis6e/SbfPmJpUnyZ0ogr6iscHQaml4wsEepEWSdtmpy+eVXmCRIMpxaXqOA==", - "dev": true, - "requires": { - "reusify": "^1.0.0" - } - }, - "faye-websocket": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", - "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", - "dev": true, - "requires": { - "websocket-driver": ">=0.5.1" - } - }, - "fbjs": { - "version": "0.8.17", - "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz", - "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=", - "requires": { - "core-js": "^1.0.0", - "isomorphic-fetch": "^2.1.1", - "loose-envify": "^1.0.0", - "object-assign": "^4.1.0", - "promise": "^7.1.1", - "setimmediate": "^1.0.5", - "ua-parser-js": "^0.7.18" - }, - "dependencies": { - "core-js": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", - "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" - } - } - }, - "figgy-pudding": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", - "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==", - "dev": true - }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", - "dev": true, - "requires": { - "flat-cache": "^2.0.1" - } - }, - "file-loader": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-4.3.0.tgz", - "integrity": "sha512-aKrYPYjF1yG3oX0kWRrqrSMfgftm7oJW5M+m4owoldH5C51C0RkIwB++JbRvEW3IU6/ZG5n8UvEcdgwOt2UOWA==", - "dev": true, - "requires": { - "loader-utils": "^1.2.3", - "schema-utils": "^2.5.0" - } - }, - "file-type": { - "version": "10.11.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-10.11.0.tgz", - "integrity": "sha512-uzk64HRpUZyTGZtVuvrjP0FYxzQrBf4rojot6J65YMEbwBLB0CWm0CLojVpwpmFmxcE/lkvYICgfcGozbBq6rw==", - "dev": true - }, - "file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "optional": true - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "dev": true, - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - } - }, - "find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "findup-sync": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", - "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", - "dev": true, - "requires": { - "detect-file": "^1.0.0", - "is-glob": "^4.0.0", - "micromatch": "^3.0.4", - "resolve-dir": "^1.0.1" - } - }, - "flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", - "dev": true, - "requires": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" - }, - "dependencies": { - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "flatted": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", - "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", - "dev": true - }, - "flot": { - "version": "git://github.com/thingsboard/flot.git#6e1a37095868f174d31d5c627c3659b70f9b92dd", - "from": "git://github.com/thingsboard/flot.git#0.9-work" - }, - "flot.curvedlines": { - "version": "git://github.com/MichaelZinsmaier/CurvedLines.git#22ed1fc2a6ccafc816c2d07b36027cc123825c4b", - "from": "git://github.com/MichaelZinsmaier/CurvedLines.git#master" - }, - "flush-write-stream": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", - "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "readable-stream": "^2.3.6" - } - }, - "follow-redirects": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.10.0.tgz", - "integrity": "sha512-4eyLK6s6lH32nOvLLwlIOnr9zrL8Sm+OvW4pVTJNoXeGzYIkHVf+pADQi+OJ0E67hiuSLezPVPyBcIZO50TmmQ==", - "dev": true, - "requires": { - "debug": "^3.0.0" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "font-awesome": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz", - "integrity": "sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM=" - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", - "dev": true - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "requires": { - "map-cache": "^0.2.2" - } - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", - "dev": true - }, - "from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - } - }, - "fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "fs-readdir-recursive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", - "integrity": "sha1-4y/AMKLM7kSmtTcTCNpUvgs5fSc=", - "dev": true - }, - "fs-write-stream-atomic": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", - "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "iferr": "^0.1.5", - "imurmurhash": "^0.1.4", - "readable-stream": "1 || 2" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fsevents": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.11.tgz", - "integrity": "sha512-+ux3lx6peh0BpvY0JebGyZoiR4D+oYzdPZMKJwkZ+sFkNJzpL7tXc/wehS49gUAxg3tmMHPHZkA8JU2rhhgDHw==", - "dev": true, - "optional": true, - "requires": { - "bindings": "^1.5.0", - "nan": "^2.12.1", - "node-pre-gyp": "*" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.3", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "3.2.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.6.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "bundled": true, - "dev": true, - "optional": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true, - "optional": true - }, - "minipass": { - "version": "2.9.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.3.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.9.0" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.14.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4.4.2" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-normalize-package-bin": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.4.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.7.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.7.1", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.13", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "yallist": { - "version": "3.1.1", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha1-pWiZ0+o8m6uHS7l3O3xe3pL0iV0=", - "dev": true - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dev": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - } - } - }, - "gaze": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", - "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", - "dev": true, - "requires": { - "globule": "^1.0.0" - } - }, - "gensync": { - "version": "1.0.0-beta.1", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", - "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==", - "dev": true - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, - "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz", - "integrity": "sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo=", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.2", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "glob-to-regexp": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", - "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=", - "dev": true - }, - "global": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", - "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", - "dev": true, - "requires": { - "min-document": "^2.19.0", - "process": "^0.11.10" - } - }, - "global-modules": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", - "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", - "dev": true, - "requires": { - "global-prefix": "^3.0.0" - } - }, - "global-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", - "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", - "dev": true, - "requires": { - "ini": "^1.3.5", - "kind-of": "^6.0.2", - "which": "^1.3.1" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, - "globby": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz", - "integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=", - "dev": true, - "requires": { - "array-union": "^1.0.1", - "dir-glob": "^2.0.0", - "glob": "^7.1.2", - "ignore": "^3.3.5", - "pify": "^3.0.0", - "slash": "^1.0.0" - }, - "dependencies": { - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - }, - "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", - "dev": true - } - } - }, - "globjoin": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz", - "integrity": "sha1-L0SUrIkZ43Z8XLtpHp9GMyQoXUM=", - "dev": true - }, - "globule": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.1.tgz", - "integrity": "sha512-OVyWOHgw29yosRHCHo7NncwR1hW5ew0W/UrvtwvjefVJeQ26q4/8r8FmPsSF1hJ93IgWkyv16pCTz6WblMzm/g==", - "dev": true, - "requires": { - "glob": "~7.1.1", - "lodash": "~4.17.12", - "minimatch": "~3.0.2" - }, - "dependencies": { - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "gonzales-pe": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.2.4.tgz", - "integrity": "sha512-v0Ts/8IsSbh9n1OJRnSfa7Nlxi4AkXIsWB6vPept8FDbL4bXn3FNuxjYtO/nmBGu7GDkL9MFeGebeSu6l55EPQ==", - "dev": true, - "requires": { - "minimist": "1.1.x" - }, - "dependencies": { - "minimist": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.1.3.tgz", - "integrity": "sha1-O+39kaktOQFvz6ocaB6Pqhoe/ag=", - "dev": true - } - } - }, - "good-listener": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", - "integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=", - "requires": { - "delegate": "^3.1.2" - } - }, - "graceful-fs": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", - "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", - "dev": true - }, - "handle-thing": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz", - "integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ==", - "dev": true - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true - }, - "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "dev": true, - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - } - }, - "hard-rejection": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", - "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", - "dev": true - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "has-symbols": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", - "dev": true - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "hash-base": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", - "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true - }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "dev": true, - "requires": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "hoist-non-react-statics": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz", - "integrity": "sha1-qkSM8JhtVcxAdzsXF0t90GbLfPs=" - }, - "homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", - "dev": true, - "requires": { - "parse-passwd": "^1.0.0" - } - }, - "hosted-git-info": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.5.tgz", - "integrity": "sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==", - "dev": true - }, - "hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" - } - }, - "html-entities": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz", - "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=", - "dev": true - }, - "html-loader": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/html-loader/-/html-loader-0.5.5.tgz", - "integrity": "sha512-7hIW7YinOYUpo//kSYcPB6dCKoceKLmOwjEMmhIobHuWGDVl0Nwe4l68mdG/Ru0wcUxQjVMEoZpkalZ/SE7zog==", - "dev": true, - "requires": { - "es6-templates": "^0.2.3", - "fastparse": "^1.1.1", - "html-minifier": "^3.5.8", - "loader-utils": "^1.1.0", - "object-assign": "^4.1.1" - }, - "dependencies": { - "commander": { - "version": "2.17.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", - "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", - "dev": true - }, - "html-minifier": { - "version": "3.5.21", - "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.21.tgz", - "integrity": "sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==", - "dev": true, - "requires": { - "camel-case": "3.0.x", - "clean-css": "4.2.x", - "commander": "2.17.x", - "he": "1.2.x", - "param-case": "2.1.x", - "relateurl": "0.2.x", - "uglify-js": "3.4.x" - } - } - } - }, - "html-minifier": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-4.0.0.tgz", - "integrity": "sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==", - "dev": true, - "requires": { - "camel-case": "^3.0.0", - "clean-css": "^4.2.1", - "commander": "^2.19.0", - "he": "^1.2.0", - "param-case": "^2.1.1", - "relateurl": "^0.2.7", - "uglify-js": "^3.5.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "uglify-js": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.8.0.tgz", - "integrity": "sha512-ugNSTT8ierCsDHso2jkBHXYrU8Y5/fY2ZUprfrJUiD7YpuFvV4jODLFmb3h4btQjqr5Nh4TX4XtgDfCU1WdioQ==", - "dev": true, - "requires": { - "commander": "~2.20.3", - "source-map": "~0.6.1" - } - } - } - }, - "html-minifier-loader": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/html-minifier-loader/-/html-minifier-loader-1.4.1.tgz", - "integrity": "sha1-ZfBeKl+R9rBxPMIIGuTr5L/A1pY=", - "dev": true, - "requires": { - "loader-utils": "^1.1.0" - } - }, - "html-tags": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz", - "integrity": "sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==", - "dev": true - }, - "html-webpack-plugin": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", - "integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=", - "dev": true, - "requires": { - "html-minifier": "^3.2.3", - "loader-utils": "^0.2.16", - "lodash": "^4.17.3", - "pretty-error": "^2.0.2", - "tapable": "^1.0.0", - "toposort": "^1.0.0", - "util.promisify": "1.0.0" - }, - "dependencies": { - "big.js": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", - "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", - "dev": true - }, - "commander": { - "version": "2.17.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", - "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", - "dev": true - }, - "html-minifier": { - "version": "3.5.21", - "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.21.tgz", - "integrity": "sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==", - "dev": true, - "requires": { - "camel-case": "3.0.x", - "clean-css": "4.2.x", - "commander": "2.17.x", - "he": "1.2.x", - "param-case": "2.1.x", - "relateurl": "0.2.x", - "uglify-js": "3.4.x" - } - }, - "json5": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", - "dev": true - }, - "loader-utils": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", - "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", - "dev": true, - "requires": { - "big.js": "^3.1.3", - "emojis-list": "^2.0.0", - "json5": "^0.5.0", - "object-assign": "^4.0.1" - } - } - } - }, - "htmlparser2": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", - "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", - "dev": true, - "requires": { - "domelementtype": "^1.3.1", - "domhandler": "^2.3.0", - "domutils": "^1.5.1", - "entities": "^1.1.1", - "inherits": "^2.0.1", - "readable-stream": "^3.1.1" - }, - "dependencies": { - "entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", - "dev": true - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", - "dev": true - }, - "http-errors": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } - } - }, - "http-parser-js": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz", - "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=", - "dev": true - }, - "http-proxy": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.0.tgz", - "integrity": "sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ==", - "dev": true, - "requires": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - } - }, - "http-proxy-middleware": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz", - "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==", - "dev": true, - "requires": { - "http-proxy": "^1.17.0", - "is-glob": "^4.0.0", - "lodash": "^4.17.11", - "micromatch": "^3.1.10" - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "https-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", - "dev": true - }, - "hyphenate-style-name": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz", - "integrity": "sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ==" - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "icss-utils": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", - "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==", - "dev": true, - "requires": { - "postcss": "^7.0.14" - } - }, - "ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", - "dev": true - }, - "iferr": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", - "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", - "dev": true - }, - "ignore": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", - "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", - "dev": true - }, - "image-size": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", - "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", - "dev": true, - "optional": true - }, - "imagemin": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/imagemin/-/imagemin-6.1.0.tgz", - "integrity": "sha512-8ryJBL1CN5uSHpiBMX0rJw79C9F9aJqMnjGnrd/1CafegpNuA81RBAAru/jQQEOWlOJJlpRnlcVFF6wq+Ist0A==", - "dev": true, - "requires": { - "file-type": "^10.7.0", - "globby": "^8.0.1", - "make-dir": "^1.0.0", - "p-pipe": "^1.1.0", - "pify": "^4.0.1", - "replace-ext": "^1.0.0" - }, - "dependencies": { - "dir-glob": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.0.0.tgz", - "integrity": "sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag==", - "dev": true, - "requires": { - "arrify": "^1.0.1", - "path-type": "^3.0.0" - } - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "globby": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-8.0.2.tgz", - "integrity": "sha512-yTzMmKygLp8RUpG1Ymu2VXPSJQZjNAZPD4ywgYEaG7e4tBJeUQBO8OpXrf1RCNcEs5alsoJYPAMiIHP0cmeC7w==", - "dev": true, - "requires": { - "array-union": "^1.0.1", - "dir-glob": "2.0.0", - "fast-glob": "^2.0.2", - "glob": "^7.1.2", - "ignore": "^3.3.5", - "pify": "^3.0.0", - "slash": "^1.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } - } - }, - "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "dev": true, - "requires": { - "pify": "^3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } - } - }, - "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", - "dev": true - } - } - }, - "img-loader": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/img-loader/-/img-loader-3.0.1.tgz", - "integrity": "sha512-0jDJqexgzOuq3zlXwFTBKJlMcaP1uXyl5t4Qu6b1IgXb3IwBDjPfVylBC8vHFIIESDw/S+5QkBbtBrt4T8wESA==", - "dev": true, - "requires": { - "loader-utils": "^1.1.0" - } - }, - "immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" - }, - "import-cwd": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", - "integrity": "sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=", - "dev": true, - "requires": { - "import-from": "^2.1.0" - } - }, - "import-fresh": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", - "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "import-from": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-from/-/import-from-2.1.0.tgz", - "integrity": "sha1-M1238qev/VOqpHHUuAId7ja387E=", - "dev": true, - "requires": { - "resolve-from": "^3.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - } - } - }, - "import-lazy": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", - "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", - "dev": true - }, - "import-local": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", - "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", - "dev": true, - "requires": { - "pkg-dir": "^3.0.0", - "resolve-cwd": "^2.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "in-publish": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", - "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=", - "dev": true - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - }, - "indexes-of": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", - "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", - "dev": true - }, - "infer-owner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha1-7uJfVtscnsYIXgwid4CD9Zar+Sc=" - }, - "inline-style-prefixer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-2.0.5.tgz", - "integrity": "sha1-wVPH6I/YT+9cYC6VqBaLJ3BnH+c=", - "requires": { - "bowser": "^1.0.0", - "hyphenate-style-name": "^1.0.1" - } - }, - "inquirer": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.0.6.tgz", - "integrity": "sha1-4EqqnQW3o8ubD0B9BDdfBEcZA0c=", - "requires": { - "ansi-escapes": "^1.1.0", - "chalk": "^1.0.0", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^2.0.1", - "figures": "^2.0.0", - "lodash": "^4.3.0", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rx": "^4.1.0", - "string-width": "^2.0.0", - "strip-ansi": "^3.0.0", - "through": "^2.3.6" - } - }, - "internal-ip": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", - "integrity": "sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==", - "dev": true, - "requires": { - "default-gateway": "^4.2.0", - "ipaddr.js": "^1.9.0" - } - }, - "interpret": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", - "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", - "dev": true - }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha1-YQ88ksk1nOHbYW5TgAjSP/NRWOY=", - "dev": true, - "requires": { - "loose-envify": "^1.0.0" - } - }, - "invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", - "dev": true - }, - "ip": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", - "dev": true - }, - "ip-regex": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", - "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", - "dev": true - }, - "ipaddr.js": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", - "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==", - "dev": true - }, - "is-absolute-url": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz", - "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==", - "dev": true - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-alphabetical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", - "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", - "dev": true - }, - "is-alphanumeric": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-alphanumeric/-/is-alphanumeric-1.0.0.tgz", - "integrity": "sha1-Spzvcdr0wAHB2B1j0UDPU/1oifQ=", - "dev": true - }, - "is-alphanumerical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", - "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", - "dev": true, - "requires": { - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0" - } - }, - "is-arguments": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", - "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", - "dev": true - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "dev": true, - "requires": { - "binary-extensions": "^1.0.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha1-76ouqdqg16suoTqXsritUf776L4=", - "dev": true - }, - "is-callable": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", - "dev": true - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-date-object": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", - "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", - "dev": true - }, - "is-decimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", - "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", - "dev": true - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha1-cpyR4thXt6QZofmqZWhcTDP1hF0=", - "dev": true - } - } - }, - "is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", - "dev": true - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-finite": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", - "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-hexadecimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", - "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", - "dev": true - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "dev": true - }, - "is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", - "dev": true - }, - "is-path-in-cwd": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", - "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", - "dev": true, - "requires": { - "is-path-inside": "^2.1.0" - } - }, - "is-path-inside": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", - "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", - "dev": true, - "requires": { - "path-is-inside": "^1.0.2" - } - }, - "is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", - "dev": true - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha1-LBY7P6+xtgbZ0Xko8FwqHDjgdnc=", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" - }, - "is-regex": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", - "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-regexp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-2.1.0.tgz", - "integrity": "sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==", - "dev": true - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" - }, - "is-string": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", - "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", - "dev": true - }, - "is-symbol": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", - "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", - "dev": true, - "requires": { - "has-symbols": "^1.0.1" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true - }, - "is-whitespace-character": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz", - "integrity": "sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==", - "dev": true - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha1-0YUOuXkezRjmGCzhKjDzlmNLsZ0=", - "dev": true - }, - "is-word-character": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.4.tgz", - "integrity": "sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA==", - "dev": true - }, - "is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "isomorphic-fetch": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", - "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", - "requires": { - "node-fetch": "^1.0.1", - "whatwg-fetch": ">=0.10.0" - } - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, - "javascript-detect-element-resize": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/javascript-detect-element-resize/-/javascript-detect-element-resize-0.5.3.tgz", - "integrity": "sha1-GnHNUd/lZZB/KZAS/nOilBBAJd4=" - }, - "jquery": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz", - "integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==" - }, - "jquery.terminal": { - "version": "1.23.2", - "resolved": "https://registry.npmjs.org/jquery.terminal/-/jquery.terminal-1.23.2.tgz", - "integrity": "sha512-ULKxZNzL8W4CoeAx5CJZTVY80SrNoeetA4lhnBeHd792uaLAkfRXMeJeARLWhBOrzDWo1yqn2nm4td0a+EU0dg==", - "requires": { - "@types/jquery": "^3.3.6", - "jquery": "~3", - "prismjs": "^1.15.0", - "wcwidth": "^1.0.1" - } - }, - "js-base64": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.2.tgz", - "integrity": "sha512-Vg8czh0Q7sFBSUMWWArX/miJeBWYBPpdU/3M/DKSaekLMqrqVPaedp+5mZhie/r0lgrcaYBfwXatEew6gwgiQQ==", - "dev": true - }, - "js-beautify": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.10.3.tgz", - "integrity": "sha512-wfk/IAWobz1TfApSdivH5PJ0miIHgDoYb1ugSqHcODPmaYu46rYe5FVuIEkhjg8IQiv6rDNPyhsqbsohI/C2vQ==", - "requires": { - "config-chain": "^1.1.12", - "editorconfig": "^0.15.3", - "glob": "^7.1.3", - "mkdirp": "~0.5.1", - "nopt": "~4.0.1" - }, - "dependencies": { - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "nopt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - } - } - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true - }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true - }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha1-u4Z8+zRQ5pEHwTHRxRS6s9yLyqk=", - "dev": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "json-schema-defaults": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/json-schema-defaults/-/json-schema-defaults-0.2.0.tgz", - "integrity": "sha1-ic64b/BTffvBpt21LyvQPvqF/4k=" - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "json3": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz", - "integrity": "sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA==", - "dev": true - }, - "json5": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.1.tgz", - "integrity": "sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "jsonminify": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/jsonminify/-/jsonminify-0.4.1.tgz", - "integrity": "sha1-gF2vuzk5UYjO6atYLIHvlZ1+cQw=", - "dev": true - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "jstree": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/jstree/-/jstree-3.3.9.tgz", - "integrity": "sha512-jRIbhg+BHrIs1Wm6oiJt3oKTVBE6sWS0PCp2/RlkIUqsLUPWUYgV3q8LfKoi1/E+YMzGtP6BuK4okk+0mwfmhQ==", - "requires": { - "jquery": ">=1.9.1" - } - }, - "jstree-bootstrap-theme": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/jstree-bootstrap-theme/-/jstree-bootstrap-theme-1.0.1.tgz", - "integrity": "sha1-fV7cc6hG6Np/lPV6HMXd7p2eq0s=", - "requires": { - "jquery": ">=1.9.1" - } - }, - "jszip": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.2.2.tgz", - "integrity": "sha512-NmKajvAFQpbg3taXQXr/ccS2wcucR1AZ+NtyWp2Nq7HHVsXhcJFR8p0Baf32C2yVvBylFWVeKf+WI2AnvlPhpA==", - "requires": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "set-immediate-shim": "~1.0.1" - } - }, - "keycode": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.0.tgz", - "integrity": "sha1-PQr1bce4uOXLqNCpfxByBO7CKwQ=" - }, - "killable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", - "integrity": "sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==", - "dev": true - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, - "known-css-properties": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.17.0.tgz", - "integrity": "sha512-Vi3nxDGMm/z+lAaCjvAR1u+7fiv+sG6gU/iYDj5QOF8h76ytK9EW/EKfF0NeTyiGBi8Jy6Hklty/vxISrLox3w==", - "dev": true - }, - "lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "dev": true, - "requires": { - "invert-kv": "^1.0.0" - } - }, - "leaflet": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.6.0.tgz", - "integrity": "sha512-CPkhyqWUKZKFJ6K8umN5/D2wrJ2+/8UIpXppY7QDnUZW5bZL5+SEI2J7GBpwh4LIupOKqbNSQXgqmrEJopHVNQ==" - }, - "leaflet-polylinedecorator": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/leaflet-polylinedecorator/-/leaflet-polylinedecorator-1.6.0.tgz", - "integrity": "sha1-nvef0bUwLWe3Lv6Vmo7NJVPycmY=", - "requires": { - "leaflet-rotatedmarker": "^0.2.0" - } - }, - "leaflet-providers": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/leaflet-providers/-/leaflet-providers-1.9.1.tgz", - "integrity": "sha512-YpJB9y4/nT5NGicU9vuqlttJaCer6paD3J3b8Wrw+IIQvK9dtcdzE9CsTkDg7Dg9FeGp5NEr3hu17xcHbYI/2w==" - }, - "leaflet-rotatedmarker": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/leaflet-rotatedmarker/-/leaflet-rotatedmarker-0.2.0.tgz", - "integrity": "sha1-RGf0n5jRv9VpWb2cZwUgPdJgEnc=" - }, - "leaflet.markercluster": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.4.1.tgz", - "integrity": "sha512-ZSEpE/EFApR0bJ1w/dUGwTSUvWlpalKqIzkaYdYB7jaftQA/Y2Jav+eT4CMtEYFj+ZK4mswP13Q2acnPBnhGOw==" - }, - "less": { - "version": "3.11.1", - "resolved": "https://registry.npmjs.org/less/-/less-3.11.1.tgz", - "integrity": "sha512-tlWX341RECuTOvoDIvtFqXsKj072hm3+9ymRBe76/mD6O5ZZecnlAOVDlWAleF2+aohFrxNidXhv2773f6kY7g==", - "dev": true, - "requires": { - "clone": "^2.1.2", - "errno": "^0.1.1", - "graceful-fs": "^4.1.2", - "image-size": "~0.5.0", - "mime": "^1.4.1", - "mkdirp": "^0.5.0", - "promise": "^7.1.1", - "request": "^2.83.0", - "source-map": "~0.6.0", - "tslib": "^1.10.0" - }, - "dependencies": { - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true - } - } - }, - "less-loader": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-5.0.0.tgz", - "integrity": "sha512-bquCU89mO/yWLaUq0Clk7qCsKhsF/TZpJUzETRvJa9KSVEL9SO3ovCvdEHISBhrC81OwC8QSVX7E0bzElZj9cg==", - "dev": true, - "requires": { - "clone": "^2.1.1", - "loader-utils": "^1.1.0", - "pify": "^4.0.1" - }, - "dependencies": { - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true - } - } - }, - "leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true - }, - "levenary": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/levenary/-/levenary-1.1.1.tgz", - "integrity": "sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ==", - "dev": true, - "requires": { - "leven": "^3.1.0" - } - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "requires": { - "immediate": "~3.0.5" - } - }, - "lines-and-columns": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", - "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", - "dev": true - }, - "load-json-file": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", - "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "strip-bom": "^3.0.0" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - } - } - }, - "loader-fs-cache": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/loader-fs-cache/-/loader-fs-cache-1.0.2.tgz", - "integrity": "sha512-70IzT/0/L+M20jUlEqZhZyArTU6VKLRTYRDAYN26g4jfzpJqjipLL3/hgYpySqI9PwsVRHHFja0LfEmsx9X2Cw==", - "dev": true, - "requires": { - "find-cache-dir": "^0.1.1", - "mkdirp": "0.5.1" - }, - "dependencies": { - "find-cache-dir": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-0.1.1.tgz", - "integrity": "sha1-yN765XyKUqinhPnjHFfHQumToLk=", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "mkdirp": "^0.5.1", - "pkg-dir": "^1.0.0" - } - }, - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "^2.0.0" - } - }, - "pkg-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", - "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", - "dev": true, - "requires": { - "find-up": "^1.0.0" - } - } - } - }, - "loader-runner": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", - "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", - "dev": true - }, - "loader-utils": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", - "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^2.0.0", - "json5": "^1.0.1" - }, - "dependencies": { - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - } - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" - }, - "lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=", - "dev": true - }, - "lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" - }, - "lodash.isregexp": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isregexp/-/lodash.isregexp-4.0.1.tgz", - "integrity": "sha1-4T5kezDNVZdSoEzZEghvr32hwws=", - "dev": true - }, - "lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=", - "dev": true - }, - "lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" - }, - "lodash.throttle": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", - "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=" - }, - "log-symbols": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", - "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", - "dev": true, - "requires": { - "chalk": "^2.4.2" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "loglevel": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.7.tgz", - "integrity": "sha512-cY2eLFrQSAfVPhCgH1s7JI73tMbg9YC3v3+ZHVW67sBS7UxWzNEk/ZBbSfLykBWHp33dqqtOv82gjhKEi81T/A==", - "dev": true - }, - "longest-streak": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.4.tgz", - "integrity": "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==", - "dev": true - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", - "dev": true, - "requires": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.0" - } - }, - "lower-case": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", - "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=", - "dev": true - }, - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "make-plural": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/make-plural/-/make-plural-3.0.6.tgz", - "integrity": "sha1-IDOgO6wpC487uRJY9lud9+iwHKc=", - "requires": { - "minimist": "^1.2.0" - } - }, - "mamacro": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", - "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==", - "dev": true - }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, - "requires": { - "p-defer": "^1.0.0" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "requires": { - "object-visit": "^1.0.0" - } - }, - "markdown-escapes": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz", - "integrity": "sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==", - "dev": true - }, - "markdown-table": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-1.1.3.tgz", - "integrity": "sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q==", - "dev": true - }, - "material-design-icons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/material-design-icons/-/material-design-icons-3.0.1.tgz", - "integrity": "sha1-mnHEh0chjrylHlGmbaaCA4zct78=", - "dev": true - }, - "material-steppers": { - "version": "git://github.com/thingsboard/material-steppers.git#ee6241d63b97b7c4a3f3b240cdb347c02a3dcb0a", - "from": "git://github.com/thingsboard/material-steppers.git#master" - }, - "material-ui": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/material-ui/-/material-ui-0.16.7.tgz", - "integrity": "sha1-XsUIDV8ieBcJLESRBd+cvIgHlBw=", - "requires": { - "babel-runtime": "^6.11.6", - "inline-style-prefixer": "^2.0.1", - "keycode": "^2.1.1", - "lodash.merge": "^4.6.0", - "lodash.throttle": "^4.1.1", - "react-addons-create-fragment": "^15.0.0", - "react-addons-transition-group": "^15.0.0", - "react-event-listener": "^0.4.0", - "recompose": "^0.21.0", - "simple-assign": "^0.1.0", - "warning": "^3.0.0" - } - }, - "material-ui-number-input": { - "version": "5.0.24", - "resolved": "https://registry.npmjs.org/material-ui-number-input/-/material-ui-number-input-5.0.24.tgz", - "integrity": "sha1-CRJdGoK2whnpr4s8LGjJ7GwN+8Q=", - "requires": { - "bind-decorator": "^1.0.11", - "object-assign": "^4.1.1", - "prop-types": "^15.5.10" - } - }, - "mathml-tag-names": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", - "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==", - "dev": true - }, - "md-color-picker": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/md-color-picker/-/md-color-picker-0.2.6.tgz", - "integrity": "sha1-OCfUkssPzZcgIG4+MqpRU/rfaIs=", - "requires": { - "tinycolor2": "*" - } - }, - "md-date-range-picker": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/md-date-range-picker/-/md-date-range-picker-0.8.4.tgz", - "integrity": "sha512-TgLyozMJypi92yvXaljLcermTFhd1+0rlaVwV+Duo0EplbKfDJfFF3WohWhB7VmPwJNP//o44sUlecY+r/ZvXA==", - "requires": { - "angular": "^1.5.8", - "angular-animate": "^1.5.8", - "angular-aria": "^1.5.8", - "angular-material": "^1.1.0", - "angular-messages": "^1.5.8" - } - }, - "md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dev": true, - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "mdPickers": { - "version": "git://github.com/alenaksu/mdPickers.git#72592ae51c81a7260701055ea21870efa57fa7c8", - "from": "git://github.com/alenaksu/mdPickers.git#0.7.5" - }, - "mdast-util-compact": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mdast-util-compact/-/mdast-util-compact-1.0.4.tgz", - "integrity": "sha512-3YDMQHI5vRiS2uygEFYaqckibpJtKq5Sj2c8JioeOQBU6INpKbdWzfyLqFFnDwEcEnRFIdMsguzs5pC1Jp4Isg==", - "dev": true, - "requires": { - "unist-util-visit": "^1.1.0" - } - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", - "dev": true - }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - }, - "dependencies": { - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - } - } - }, - "memory-fs": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", - "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", - "dev": true, - "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - } - }, - "meow": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", - "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", - "dev": true, - "requires": { - "camelcase-keys": "^2.0.0", - "decamelize": "^1.1.2", - "loud-rejection": "^1.0.0", - "map-obj": "^1.0.1", - "minimist": "^1.1.3", - "normalize-package-data": "^2.3.4", - "object-assign": "^4.0.1", - "read-pkg-up": "^1.0.1", - "redent": "^1.0.0", - "trim-newlines": "^1.0.0" - }, - "dependencies": { - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - } - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "^2.0.0" - } - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "^0.2.0" - } - } - } - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", - "dev": true - }, - "merge2": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz", - "integrity": "sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==", - "dev": true - }, - "messageformat": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/messageformat/-/messageformat-1.0.2.tgz", - "integrity": "sha1-kI9GkfKf8o2uNcRUNqJM/5NAI4g=", - "requires": { - "glob": "~7.0.6", - "make-plural": "~3.0.6", - "messageformat-parser": "^1.0.0", - "nopt": "~3.0.6", - "reserved-words": "^0.1.1" - } - }, - "messageformat-parser": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/messageformat-parser/-/messageformat-parser-1.1.0.tgz", - "integrity": "sha1-E7oiUKdrvejg/KDbs0dflcWUqQo=" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "dev": true, - "requires": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - } - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha1-Ms2eXGRVO9WNGaVor0Uqz/BJgbE=", - "dev": true - }, - "mime-db": { - "version": "1.43.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", - "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==", - "dev": true - }, - "mime-types": { - "version": "2.1.26", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", - "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", - "dev": true, - "requires": { - "mime-db": "1.43.0" - } - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha1-ggyGo5M0ZA6ZUWkovQP8qIBX0CI=" - }, - "min-document": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", - "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=", - "dev": true, - "requires": { - "dom-walk": "^0.1.0" - } - }, - "min-indent": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.0.tgz", - "integrity": "sha1-z8RcN+nsDY8KDsPdTvf3w6vjklY=", - "dev": true - }, - "mini-css-extract-plugin": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.8.2.tgz", - "integrity": "sha512-a3Y4of27Wz+mqK3qrcd3VhYz6cU0iW5x3Sgvqzbj+XmlrSizmvu8QQMl5oMYJjgHOC4iyt+w7l4umP+dQeW3bw==", - "dev": true, - "requires": { - "loader-utils": "^1.1.0", - "normalize-url": "1.9.1", - "schema-utils": "^1.0.0", - "webpack-sources": "^1.1.0" - }, - "dependencies": { - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - } - } - }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - }, - "minimist-options": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.0.2.tgz", - "integrity": "sha512-seq4hpWkYSUh1y7NXxzucwAN9yVlBc3Upgdjz8vLCP97jG8kaOmzYrVH/m7tQ1NYD1wdtZbSLfdy4zFmRWuc/w==", - "dev": true, - "requires": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0" - } - }, - "minipass": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.1.tgz", - "integrity": "sha512-UFqVihv6PQgwj8/yTGvl9kPz7xIAY+R5z6XYjRInD3Gk3qx6QGSD6zEcpeG4Dy/lQnv1J6zv8ejV90hyYIKf3w==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - }, - "dependencies": { - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "dev": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "dev": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "minipass-pipeline": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.2.tgz", - "integrity": "sha512-3JS5A2DKhD2g0Gg8x3yamO0pj7YeKGwVlDS90pF++kxptwx/F+B//roxf9SqYil5tQo65bijy+dAuAFZmYOouA==", - "dev": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "mississippi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", - "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", - "dev": true, - "requires": { - "concat-stream": "^1.5.0", - "duplexify": "^3.4.2", - "end-of-stream": "^1.1.0", - "flush-write-stream": "^1.0.0", - "from2": "^2.1.0", - "parallel-transform": "^1.1.0", - "pump": "^3.0.0", - "pumpify": "^1.3.3", - "stream-each": "^1.1.0", - "through2": "^2.0.0" - } - }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - } - } - }, - "moment": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", - "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" - }, - "move-concurrently": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", - "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", - "dev": true, - "requires": { - "aproba": "^1.1.1", - "copy-concurrently": "^1.0.0", - "fs-write-stream-atomic": "^1.0.8", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.3" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "multicast-dns": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", - "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", - "dev": true, - "requires": { - "dns-packet": "^1.3.1", - "thunky": "^1.0.2" - } - }, - "multicast-dns-service-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", - "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", - "dev": true - }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" - }, - "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", - "dev": true - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha1-uHqKpPwN6P5r6IiVs4mD/yZb0Rk=", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - } - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "negotiator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", - "dev": true - }, - "neo-async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", - "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", - "dev": true - }, - "ng-annotate": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ng-annotate/-/ng-annotate-1.2.1.tgz", - "integrity": "sha1-64vBpnMccNCK9rAsPq8abj+55rs=", - "dev": true, - "requires": { - "acorn": "~2.6.4", - "alter": "~0.2.0", - "convert-source-map": "~1.1.2", - "optimist": "~0.6.1", - "ordered-ast-traverse": "~1.1.1", - "simple-fmt": "~0.1.0", - "simple-is": "~0.2.0", - "source-map": "~0.5.3", - "stable": "~0.1.5", - "stringmap": "~0.2.2", - "stringset": "~0.2.1", - "tryor": "~0.1.2" - }, - "dependencies": { - "acorn": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.6.4.tgz", - "integrity": "sha1-6x9FtKQ/ox0DcBpexG87Umc+kO4=", - "dev": true - }, - "convert-source-map": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", - "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=", - "dev": true - } - } - }, - "ng-annotate-loader": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/ng-annotate-loader/-/ng-annotate-loader-0.6.1.tgz", - "integrity": "sha1-6be3oVYrnHlzfVCIbVWN5/DfQlc=", - "dev": true, - "requires": { - "clone": "^2.1.1", - "loader-utils": "1.1.0", - "ng-annotate": "1.2.1", - "normalize-path": "2.0.1", - "source-map": "0.5.6" - }, - "dependencies": { - "big.js": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", - "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", - "dev": true - }, - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true - }, - "json5": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", - "dev": true - }, - "loader-utils": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", - "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", - "dev": true, - "requires": { - "big.js": "^3.1.3", - "emojis-list": "^2.0.0", - "json5": "^0.5.0" - } - }, - "normalize-path": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.0.1.tgz", - "integrity": "sha1-R4hqwWYnYNQmG32XnSQXCdPOP3o=", - "dev": true - }, - "source-map": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", - "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=", - "dev": true - } - } - }, - "ng-annotate-patched": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/ng-annotate-patched/-/ng-annotate-patched-1.11.1.tgz", - "integrity": "sha512-DmReqLu/cAdnXt7d0NpLC1hEDUH2z1CGs5ymQCjHd5+eAvWfkTl0k17pdFc0/C/EZRx+oed+4DJ7TsRILQVLUQ==", - "dev": true, - "requires": { - "acorn": "^7.0.0", - "acorn-walk": "^7.0.0", - "commander": "^3.0.1", - "convert-source-map": "^1.1.2", - "source-map": "^0.6.1" - }, - "dependencies": { - "commander": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz", - "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "ngFlowchart": { - "version": "git://github.com/thingsboard/ngFlowchart.git#1343a7478961f68280d81f0ecda4e722a2068e0f", - "from": "git://github.com/thingsboard/ngFlowchart.git#master" - }, - "ngclipboard": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/ngclipboard/-/ngclipboard-1.1.2.tgz", - "integrity": "sha1-RZ8B8+4eisQBWZquo3uXdQh9WW0=", - "requires": { - "clipboard": "~1.5.5" - }, - "dependencies": { - "clipboard": { - "version": "1.5.16", - "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-1.5.16.tgz", - "integrity": "sha1-kW1ec5sAZL5hsLSKU1cx7K7z02c=", - "requires": { - "good-listener": "^1.2.0", - "select": "^1.0.6", - "tiny-emitter": "^1.0.0" - } - }, - "tiny-emitter": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-1.2.0.tgz", - "integrity": "sha1-bchFBSywjr78GHRyO1jySmSMO28=" - } - } - }, - "ngreact": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/ngreact/-/ngreact-0.3.0.tgz", - "integrity": "sha1-g4u3Z5BGKb6ApduynAp76xKbRKA=" - }, - "ngtemplate-loader": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ngtemplate-loader/-/ngtemplate-loader-2.0.1.tgz", - "integrity": "sha1-nX7S6KI2NSOte2TXSqxALY2v8/M=", - "dev": true, - "requires": { - "jsesc": "^0.5.0", - "loader-utils": "^1.0.2" - }, - "dependencies": { - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", - "dev": true - } - } - }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "no-case": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", - "integrity": "sha1-YLgTOWvjmz8SiKTB7V0efSi0ZKw=", - "dev": true, - "requires": { - "lower-case": "^1.1.1" - } - }, - "node-environment-flags": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", - "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==", - "dev": true, - "requires": { - "object.getownpropertydescriptors": "^2.0.3", - "semver": "^5.7.0" - } - }, - "node-fetch": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", - "integrity": "sha1-mA9vcthSEaU0fGsrwYxbhMPrR+8=", - "requires": { - "encoding": "^0.1.11", - "is-stream": "^1.0.1" - } - }, - "node-forge": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz", - "integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==", - "dev": true - }, - "node-gyp": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", - "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", - "dev": true, - "requires": { - "fstream": "^1.0.0", - "glob": "^7.0.3", - "graceful-fs": "^4.1.2", - "mkdirp": "^0.5.0", - "nopt": "2 || 3", - "npmlog": "0 || 1 || 2 || 3 || 4", - "osenv": "0", - "request": "^2.87.0", - "rimraf": "2", - "semver": "~5.3.0", - "tar": "^2.0.0", - "which": "1" - }, - "dependencies": { - "semver": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", - "dev": true - } - } - }, - "node-libs-browser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", - "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", - "dev": true, - "requires": { - "assert": "^1.1.1", - "browserify-zlib": "^0.2.0", - "buffer": "^4.3.0", - "console-browserify": "^1.1.0", - "constants-browserify": "^1.0.0", - "crypto-browserify": "^3.11.0", - "domain-browser": "^1.1.1", - "events": "^3.0.0", - "https-browserify": "^1.0.0", - "os-browserify": "^0.3.0", - "path-browserify": "0.0.1", - "process": "^0.11.10", - "punycode": "^1.2.4", - "querystring-es3": "^0.2.0", - "readable-stream": "^2.3.3", - "stream-browserify": "^2.0.1", - "stream-http": "^2.7.2", - "string_decoder": "^1.0.0", - "timers-browserify": "^2.0.4", - "tty-browserify": "0.0.0", - "url": "^0.11.0", - "util": "^0.11.0", - "vm-browserify": "^1.0.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - } - } - }, - "node-modules-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", - "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", - "dev": true - }, - "node-releases": { - "version": "1.1.49", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.49.tgz", - "integrity": "sha512-xH8t0LS0disN0mtRCh+eByxFPie+msJUBL/lJDBuap53QGiYPa9joh83K4pCZgWJ+2L4b9h88vCVdXQ60NO2bg==", - "dev": true, - "requires": { - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "node-sass": { - "version": "4.13.1", - "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.13.1.tgz", - "integrity": "sha512-TTWFx+ZhyDx1Biiez2nB0L3YrCZ/8oHagaDalbuBSlqXgUPsdkUSzJsVxeDO9LtPB49+Fh3WQl3slABo6AotNw==", - "dev": true, - "requires": { - "async-foreach": "^0.1.3", - "chalk": "^1.1.1", - "cross-spawn": "^3.0.0", - "gaze": "^1.0.0", - "get-stdin": "^4.0.1", - "glob": "^7.0.3", - "in-publish": "^2.0.0", - "lodash": "^4.17.15", - "meow": "^3.7.0", - "mkdirp": "^0.5.1", - "nan": "^2.13.2", - "node-gyp": "^3.8.0", - "npmlog": "^4.0.0", - "request": "^2.88.0", - "sass-graph": "^2.2.4", - "stdout-stream": "^1.4.0", - "true-case-path": "^1.0.2" - }, - "dependencies": { - "cross-spawn": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", - "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "which": "^1.2.9" - } - } - } - }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "requires": { - "abbrev": "1" - } - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", - "dev": true - }, - "normalize-selector": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/normalize-selector/-/normalize-selector-0.2.0.tgz", - "integrity": "sha1-0LFF62kRicY6eNIB3E/bEpPvDAM=", - "dev": true - }, - "normalize-url": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", - "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", - "dev": true, - "requires": { - "object-assign": "^4.0.1", - "prepend-http": "^1.0.0", - "query-string": "^4.1.0", - "sort-keys": "^1.0.0" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha1-CKfyqL9zRgR3mp76StXMcXq7lUs=", - "dev": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "nth-check": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", - "dev": true, - "requires": { - "boolbase": "~1.0.0" - } - }, - "num2fraction": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", - "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", - "dev": true - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "object-hash": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz", - "integrity": "sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==", - "dev": true - }, - "object-inspect": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", - "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", - "dev": true - }, - "object-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.2.tgz", - "integrity": "sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ==", - "dev": true - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "requires": { - "isobject": "^3.0.0" - } - }, - "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - } - }, - "object.getownpropertydescriptors": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", - "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.5.1" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "object.values": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz", - "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1", - "function-bind": "^1.1.1", - "has": "^1.0.3" - } - }, - "objectpath": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/objectpath/-/objectpath-1.2.2.tgz", - "integrity": "sha512-ie+GY5tJsKt7daHH6qGROf3JqxfD2XhfBPLY+HQrVuRY8MQE1ySKVSqQ/TQz/Dx7jDwuy3etQALDE1cRJAC0cg==" - }, - "obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true - }, - "oclazyload": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/oclazyload/-/oclazyload-1.1.0.tgz", - "integrity": "sha1-qYBzIvGQggqBwCLy7xcB0DbYPoc=" - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "dev": true, - "requires": { - "ee-first": "1.1.1" - } - }, - "on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "opencollective": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/opencollective/-/opencollective-1.0.3.tgz", - "integrity": "sha1-ruY3K8KBRFg2kMPKja7PwSDdDvE=", - "requires": { - "babel-polyfill": "6.23.0", - "chalk": "1.1.3", - "inquirer": "3.0.6", - "minimist": "1.2.0", - "node-fetch": "1.6.3", - "opn": "4.0.2" - }, - "dependencies": { - "node-fetch": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.6.3.tgz", - "integrity": "sha1-3CNO3WSJmC1Y6PDbT2lQKavNjAQ=", - "requires": { - "encoding": "^0.1.11", - "is-stream": "^1.0.1" - } - } - } - }, - "opn": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/opn/-/opn-4.0.2.tgz", - "integrity": "sha1-erwi5kTf9jsKltWrfyeQwPAavJU=", - "requires": { - "object-assign": "^4.0.1", - "pinkie-promise": "^2.0.0" - } - }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - }, - "dependencies": { - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", - "dev": true - } - } - }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - } - }, - "options": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", - "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=" - }, - "ordered-ast-traverse": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ordered-ast-traverse/-/ordered-ast-traverse-1.1.1.tgz", - "integrity": "sha1-aEOhcLwO7otSDMjdwd3TqjD6BXw=", - "dev": true, - "requires": { - "ordered-esprima-props": "~1.1.0" - } - }, - "ordered-esprima-props": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ordered-esprima-props/-/ordered-esprima-props-1.1.0.tgz", - "integrity": "sha1-qYJwht9fAQqmDpvQK24DNc6i/8s=", - "dev": true - }, - "original": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", - "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", - "dev": true, - "requires": { - "url-parse": "^1.4.3" - } - }, - "os-browserify": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", - "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", - "dev": true - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" - }, - "os-locale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", - "dev": true, - "requires": { - "lcid": "^1.0.0" - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha1-hc36+uso6Gd/QW4odZK18/SepBA=", - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", - "dev": true - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, - "p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true - }, - "p-limit": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", - "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-map": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", - "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, - "p-pipe": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-pipe/-/p-pipe-1.2.0.tgz", - "integrity": "sha1-SxoROZoRUgpneQ7loMHViB1r7+k=", - "dev": true - }, - "p-retry": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz", - "integrity": "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==", - "dev": true, - "requires": { - "retry": "^0.12.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" - }, - "parallel-transform": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", - "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", - "dev": true, - "requires": { - "cyclist": "^1.0.1", - "inherits": "^2.0.3", - "readable-stream": "^2.1.5" - } - }, - "param-case": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", - "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=", - "dev": true, - "requires": { - "no-case": "^2.2.0" - } - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "parse-asn1": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.5.tgz", - "integrity": "sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ==", - "dev": true, - "requires": { - "asn1.js": "^4.0.0", - "browserify-aes": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" - } - }, - "parse-entities": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-1.2.2.tgz", - "integrity": "sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg==", - "dev": true, - "requires": { - "character-entities": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "character-reference-invalid": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-hexadecimal": "^1.0.0" - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "requires": { - "error-ex": "^1.2.0" - } - }, - "parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", - "dev": true - }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, - "path-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", - "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", - "dev": true - }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", - "dev": true - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", - "dev": true - }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } - } - }, - "pbkdf2": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", - "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", - "dev": true, - "requires": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "picomatch": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.1.tgz", - "integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==", - "dev": true - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "requires": { - "pinkie": "^2.0.0" - } - }, - "pirates": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", - "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", - "dev": true, - "requires": { - "node-modules-regexp": "^1.0.0" - } - }, - "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "dev": true, - "requires": { - "find-up": "^3.0.0" - } - }, - "portfinder": { - "version": "1.0.25", - "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.25.tgz", - "integrity": "sha512-6ElJnHBbxVA1XSLgBp7G1FiCkQdlqGzuF7DswL5tcea+E8UpuvPU7beVAjjRwCioTS9ZluNbu+ZyRvgTsmqEBg==", - "dev": true, - "requires": { - "async": "^2.6.2", - "debug": "^3.1.1", - "mkdirp": "^0.5.1" - }, - "dependencies": { - "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", - "dev": true, - "requires": { - "lodash": "^4.17.14" - } - }, - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true - }, - "postcss": { - "version": "7.0.27", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.27.tgz", - "integrity": "sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-html": { - "version": "0.36.0", - "resolved": "https://registry.npmjs.org/postcss-html/-/postcss-html-0.36.0.tgz", - "integrity": "sha512-HeiOxGcuwID0AFsNAL0ox3mW6MHH5cstWN1Z3Y+n6H+g12ih7LHdYxWwEA/QmrebctLjo79xz9ouK3MroHwOJw==", - "dev": true, - "requires": { - "htmlparser2": "^3.10.0" - } - }, - "postcss-jsx": { - "version": "0.36.4", - "resolved": "https://registry.npmjs.org/postcss-jsx/-/postcss-jsx-0.36.4.tgz", - "integrity": "sha512-jwO/7qWUvYuWYnpOb0+4bIIgJt7003pgU3P6nETBLaOyBXuTD55ho21xnals5nBrlpTIFodyd3/jBi6UO3dHvA==", - "dev": true, - "requires": { - "@babel/core": ">=7.2.2" - } - }, - "postcss-less": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/postcss-less/-/postcss-less-3.1.4.tgz", - "integrity": "sha512-7TvleQWNM2QLcHqvudt3VYjULVB49uiW6XzEUFmvwHzvsOEF5MwBrIXZDJQvJNFGjJQTzSzZnDoCJ8h/ljyGXA==", - "dev": true, - "requires": { - "postcss": "^7.0.14" - } - }, - "postcss-load-config": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.1.0.tgz", - "integrity": "sha512-4pV3JJVPLd5+RueiVVB+gFOAa7GWc25XQcMp86Zexzke69mKf6Nx9LRcQywdz7yZI9n1udOxmLuAwTBypypF8Q==", - "dev": true, - "requires": { - "cosmiconfig": "^5.0.0", - "import-cwd": "^2.0.0" - } - }, - "postcss-loader": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-3.0.0.tgz", - "integrity": "sha1-a5eUPkfHLYRfqeA/Jzdz1OjdbC0=", - "dev": true, - "requires": { - "loader-utils": "^1.1.0", - "postcss": "^7.0.0", - "postcss-load-config": "^2.0.0", - "schema-utils": "^1.0.0" - }, - "dependencies": { - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - } - } - }, - "postcss-markdown": { - "version": "0.36.0", - "resolved": "https://registry.npmjs.org/postcss-markdown/-/postcss-markdown-0.36.0.tgz", - "integrity": "sha512-rl7fs1r/LNSB2bWRhyZ+lM/0bwKv9fhl38/06gF6mKMo/NPnp55+K1dSTosSVjFZc0e1ppBlu+WT91ba0PMBfQ==", - "dev": true, - "requires": { - "remark": "^10.0.1", - "unist-util-find-all-after": "^1.0.2" - } - }, - "postcss-media-query-parser": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", - "integrity": "sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ=", - "dev": true - }, - "postcss-modules-extract-imports": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", - "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==", - "dev": true, - "requires": { - "postcss": "^7.0.5" - } - }, - "postcss-modules-local-by-default": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz", - "integrity": "sha512-jM/V8eqM4oJ/22j0gx4jrp63GSvDH6v86OqyTHHUvk4/k1vceipZsaymiZ5PvocqZOl5SFHiFJqjs3la0wnfIQ==", - "dev": true, - "requires": { - "icss-utils": "^4.1.1", - "postcss": "^7.0.16", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.0.0" - } - }, - "postcss-modules-scope": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.1.1.tgz", - "integrity": "sha512-OXRUPecnHCg8b9xWvldG/jUpRIGPNRka0r4D4j0ESUU2/5IOnpsjfPPmDprM3Ih8CgZ8FXjWqaniK5v4rWt3oQ==", - "dev": true, - "requires": { - "postcss": "^7.0.6", - "postcss-selector-parser": "^6.0.0" - } - }, - "postcss-modules-values": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz", - "integrity": "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==", - "dev": true, - "requires": { - "icss-utils": "^4.0.0", - "postcss": "^7.0.6" - } - }, - "postcss-reporter": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-6.0.1.tgz", - "integrity": "sha512-LpmQjfRWyabc+fRygxZjpRxfhRf9u/fdlKf4VHG4TSPbV2XNsuISzYW1KL+1aQzx53CAppa1bKG4APIB/DOXXw==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "lodash": "^4.17.11", - "log-symbols": "^2.2.0", - "postcss": "^7.0.7" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", - "dev": true, - "requires": { - "chalk": "^2.0.1" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-resolve-nested-selector": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz", - "integrity": "sha1-Kcy8fDfe36wwTp//C/FZaz9qDk4=", - "dev": true - }, - "postcss-safe-parser": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-4.0.1.tgz", - "integrity": "sha512-xZsFA3uX8MO3yAda03QrG3/Eg1LN3EPfjjf07vke/46HERLZyHrTsQ9E1r1w1W//fWEhtYNndo2hQplN2cVpCQ==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - } - }, - "postcss-sass": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/postcss-sass/-/postcss-sass-0.4.2.tgz", - "integrity": "sha512-hcRgnd91OQ6Ot9R90PE/khUDCJHG8Uxxd3F7Y0+9VHjBiJgNv7sK5FxyHMCBtoLmmkzVbSj3M3OlqUfLJpq0CQ==", - "dev": true, - "requires": { - "gonzales-pe": "^4.2.4", - "postcss": "^7.0.21" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss": { - "version": "7.0.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.26.tgz", - "integrity": "sha512-IY4oRjpXWYshuTDFxMVkJDtWIk2LhsTlu8bZnbEJA4+bYT16Lvpo8Qv6EvDumhYRgzjZl489pmsY3qVgJQ08nA==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-scss": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-2.0.0.tgz", - "integrity": "sha512-um9zdGKaDZirMm+kZFKKVsnKPF7zF7qBAtIfTSnZXD1jZ0JNZIxdB6TxQOjCnlSzLRInVl2v3YdBh/M881C4ug==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - } - }, - "postcss-selector-parser": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz", - "integrity": "sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==", - "dev": true, - "requires": { - "cssesc": "^3.0.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - }, - "postcss-sorting": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/postcss-sorting/-/postcss-sorting-5.0.1.tgz", - "integrity": "sha512-Y9fUFkIhfrm6i0Ta3n+89j56EFqaNRdUKqXyRp6kvTcSXnmgEjaVowCXH+JBe9+YKWqd4nc28r2sgwnzJalccA==", - "dev": true, - "requires": { - "lodash": "^4.17.14", - "postcss": "^7.0.17" - } - }, - "postcss-syntax": { - "version": "0.36.2", - "resolved": "https://registry.npmjs.org/postcss-syntax/-/postcss-syntax-0.36.2.tgz", - "integrity": "sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==", - "dev": true - }, - "postcss-value-parser": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.0.3.tgz", - "integrity": "sha512-N7h4pG+Nnu5BEIzyeaaIYWs0LI5XC40OrRh5L60z0QjFsqGWcHcbkBvpe1WYpcIS9yQ8sOi/vIPt1ejQCrMVrg==", - "dev": true - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", - "dev": true - }, - "pretty-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz", - "integrity": "sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=", - "dev": true, - "requires": { - "renderkid": "^2.0.1", - "utila": "~0.4" - } - }, - "prismjs": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.19.0.tgz", - "integrity": "sha512-IVFtbW9mCWm9eOIaEkNyo2Vl4NnEifis2GQ7/MLRG5TQe6t+4Sj9J5QWI9i3v+SS43uZBlCAOn+zYTVYQcPXJw==", - "requires": { - "clipboard": "^2.0.0" - }, - "dependencies": { - "clipboard": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.4.tgz", - "integrity": "sha512-Vw26VSLRpJfBofiVaFb/I8PVfdI1OxKcYShe6fm0sP/DtmiWQNCjhM/okTvdCo0G+lMMm1rMYbk4IK4x1X+kgQ==", - "optional": true, - "requires": { - "good-listener": "^1.2.2", - "select": "^1.1.2", - "tiny-emitter": "^2.0.0" - } - } - } - }, - "private": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha1-I4Hts2ifelPWUxkAYPz4ItLzaP8=", - "dev": true - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true - }, - "promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha1-BktyYCsY+Q8pGSuLG8QY/9Hr078=", - "requires": { - "asap": "~2.0.3" - } - }, - "promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", - "dev": true - }, - "prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.8.1" - } - }, - "proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=" - }, - "proxy-addr": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", - "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", - "dev": true, - "requires": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.9.0" - } - }, - "prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", - "dev": true - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" - }, - "psl": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz", - "integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==", - "dev": true - }, - "public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "pumpify": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", - "integrity": "sha1-NlE74karJ1cLGjdKXOJ4v9dDcM4=", - "dev": true, - "requires": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" - }, - "dependencies": { - "pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true - }, - "query-string": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", - "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", - "dev": true, - "requires": { - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" - } - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", - "dev": true - }, - "querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", - "dev": true - }, - "querystringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz", - "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==", - "dev": true - }, - "quick-lru": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", - "dev": true - }, - "raf": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", - "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", - "requires": { - "performance-now": "^2.1.0" - } - }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "dev": true, - "requires": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true - }, - "raphael": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/raphael/-/raphael-2.3.0.tgz", - "integrity": "sha512-w2yIenZAQnp257XUWGni4bLMVxpUpcIl7qgxEgDIXtmSypYtlNxfXWpOBxs7LBTps5sDwhRnrToJrMUrivqNTQ==", - "requires": { - "eve-raphael": "0.5.0" - } - }, - "raw-body": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", - "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", - "dev": true, - "requires": { - "bytes": "3.1.0", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "dependencies": { - "bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", - "dev": true - } - } - }, - "raw-loader": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-3.1.0.tgz", - "integrity": "sha512-lzUVMuJ06HF4rYveaz9Tv0WRlUMxJ0Y1hgSkkgg+50iEdaI0TthyEDe08KIHb0XsF6rn8WYTqPCaGTZg3sX+qA==", - "dev": true, - "requires": { - "loader-utils": "^1.1.0", - "schema-utils": "^2.0.1" - } - }, - "rc-align": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/rc-align/-/rc-align-2.4.5.tgz", - "integrity": "sha512-nv9wYUYdfyfK+qskThf4BQUSIadeI/dCsfaMZfNEoxm9HwOIioQ+LyqmMK6jWHAZQgOzMLaqawhuBXlF63vgjw==", - "requires": { - "babel-runtime": "^6.26.0", - "dom-align": "^1.7.0", - "prop-types": "^15.5.8", - "rc-util": "^4.0.4" - } - }, - "rc-animate": { - "version": "2.10.2", - "resolved": "https://registry.npmjs.org/rc-animate/-/rc-animate-2.10.2.tgz", - "integrity": "sha512-cE/A7piAzoWFSgUD69NmmMraqCeqVBa51UErod8NS3LUEqWfppSVagHfa0qHAlwPVPiIBg3emRONyny3eiH0Dg==", - "requires": { - "babel-runtime": "6.x", - "classnames": "^2.2.6", - "css-animation": "^1.3.2", - "prop-types": "15.x", - "raf": "^3.4.0", - "rc-util": "^4.15.3", - "react-lifecycles-compat": "^3.0.4" - } - }, - "rc-menu": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-5.1.4.tgz", - "integrity": "sha1-5d8I/ouDPoFGkTX/E7MKuPIf88Y=", - "requires": { - "babel-runtime": "6.x", - "classnames": "2.x", - "create-react-class": "^15.5.2", - "dom-scroll-into-view": "1.x", - "prop-types": "^15.5.6", - "rc-animate": "2.x", - "rc-util": "^4.0.2" - } - }, - "rc-select": { - "version": "6.9.8", - "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-6.9.8.tgz", - "integrity": "sha512-PO665G7iD0+CWTXQBd+wsVrDd/Y4FRrjppOYbc0R5O/di92Aj8NItVFYFtBN4olqvdUh5yXDkvf2TW0QAPQJvQ==", - "requires": { - "babel-runtime": "^6.23.0", - "classnames": "2.x", - "component-classes": "1.x", - "dom-scroll-into-view": "1.x", - "prop-types": "^15.5.8", - "rc-animate": "2.x", - "rc-menu": "^5.0.11", - "rc-trigger": "1.x", - "rc-util": "^4.0.4", - "warning": "^3.0.0" - } - }, - "rc-trigger": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/rc-trigger/-/rc-trigger-1.11.5.tgz", - "integrity": "sha1-+I+fhODnn44O8cjRv4rCIItxViA=", - "requires": { - "babel-runtime": "6.x", - "create-react-class": "15.x", - "prop-types": "15.x", - "rc-align": "2.x", - "rc-animate": "2.x", - "rc-util": "4.x" - } - }, - "rc-util": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-4.19.0.tgz", - "integrity": "sha512-mptALlLwpeczS3nrv83DbwJNeupolbuvlIEjcvimSiWI8NUBjpF0HgG3kWp1RymiuiRCNm9yhaXqDz0a99dpgQ==", - "requires": { - "add-dom-event-listener": "^1.1.0", - "babel-runtime": "6.x", - "prop-types": "^15.5.10", - "react-lifecycles-compat": "^3.0.4", - "shallowequal": "^1.1.0" - } - }, - "react": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", - "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", - "requires": { - "create-react-class": "^15.6.0", - "fbjs": "^0.8.9", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.0", - "prop-types": "^15.5.10" - } - }, - "react-ace": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/react-ace/-/react-ace-4.4.0.tgz", - "integrity": "sha1-be42amljyzOur8ndPCWaJGAXSSY=", - "requires": { - "brace": "^0.10.0", - "lodash.isequal": "^4.1.1", - "opencollective": "^1.0.3", - "prop-types": "^15.5.8" - } - }, - "react-addons-create-fragment": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react-addons-create-fragment/-/react-addons-create-fragment-15.6.2.tgz", - "integrity": "sha1-o5TefCx77Na1R1uhuXrEcs58dPg=", - "requires": { - "fbjs": "^0.8.4", - "loose-envify": "^1.3.1", - "object-assign": "^4.1.0" - } - }, - "react-addons-transition-group": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react-addons-transition-group/-/react-addons-transition-group-15.6.2.tgz", - "integrity": "sha1-i668Kukczb8kX+Kcn9PTb4tHGSM=", - "requires": { - "react-transition-group": "^1.2.0" - } - }, - "react-dom": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-15.6.2.tgz", - "integrity": "sha1-Qc+t9pO3V/rycIRDodH9WgK+9zA=", - "requires": { - "fbjs": "^0.8.9", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.0", - "prop-types": "^15.5.10" - } - }, - "react-dropzone": { - "version": "3.13.4", - "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-3.13.4.tgz", - "integrity": "sha1-hNomgVxAM5aRxJtFRMLvehaRLMw=", - "requires": { - "attr-accept": "^1.0.3", - "prop-types": "^15.5.7" - } - }, - "react-event-listener": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/react-event-listener/-/react-event-listener-0.4.5.tgz", - "integrity": "sha1-4+iVoJcM8U7o+JAROvaBl6vz0LE=", - "requires": { - "babel-runtime": "^6.20.0", - "fbjs": "^0.8.4", - "prop-types": "^15.5.4", - "warning": "^3.0.0" - } - }, - "react-hot-loader": { - "version": "4.12.19", - "resolved": "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.12.19.tgz", - "integrity": "sha512-p8AnA4QE2GtrvkdmqnKrEiijtVlqdTIDCHZOwItkI9kW51bt5XnQ/4Anz8giiWf9kqBpEQwsmnChDCAFBRyR/Q==", - "dev": true, - "requires": { - "fast-levenshtein": "^2.0.6", - "global": "^4.3.0", - "hoist-non-react-statics": "^3.3.0", - "loader-utils": "^1.1.0", - "prop-types": "^15.6.1", - "react-lifecycles-compat": "^3.0.4", - "shallowequal": "^1.1.0", - "source-map": "^0.7.3" - }, - "dependencies": { - "hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "dev": true, - "requires": { - "react-is": "^16.7.0" - } - }, - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - } - } - }, - "react-is": { - "version": "16.12.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz", - "integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==" - }, - "react-lifecycles-compat": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", - "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" - }, - "react-schema-form": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/react-schema-form/-/react-schema-form-0.3.11.tgz", - "integrity": "sha1-gWdjoGUuehisJBBzVsceOmfUHYE=", - "requires": { - "classnames": "^2.2.5", - "lodash": "^4.16.6", - "objectpath": "^1.2.1", - "tv4": "^1.2.7" - } - }, - "react-tap-event-plugin": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/react-tap-event-plugin/-/react-tap-event-plugin-2.0.1.tgz", - "integrity": "sha1-MWvrO8ZVbinshppyk+icgmqQdNI=", - "requires": { - "fbjs": "^0.8.6" - } - }, - "react-transition-group": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-1.2.1.tgz", - "integrity": "sha1-4R9yslf5IbITIpp3TfRmEjRsfKY=", - "requires": { - "chain-function": "^1.0.0", - "dom-helpers": "^3.2.0", - "loose-envify": "^1.3.1", - "prop-types": "^15.5.6", - "warning": "^3.0.0" - } - }, - "reactcss": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", - "integrity": "sha1-wAATh15Vexzw39mjaKHD2rO1SN0=", - "requires": { - "lodash": "^4.0.1" - } - }, - "read-pkg": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", - "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", - "dev": true, - "requires": { - "load-json-file": "^2.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^2.0.0" - }, - "dependencies": { - "path-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", - "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", - "dev": true, - "requires": { - "pify": "^2.0.0" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - } - } - }, - "read-pkg-up": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", - "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", - "dev": true, - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^2.0.0" - }, - "dependencies": { - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - } - } - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - } - }, - "recast": { - "version": "0.11.23", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.11.23.tgz", - "integrity": "sha1-RR/TAEqx5N+bTktmN2sqIZEkYtM=", - "dev": true, - "requires": { - "ast-types": "0.9.6", - "esprima": "~3.1.0", - "private": "~0.1.5", - "source-map": "~0.5.0" - }, - "dependencies": { - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true - } - } - }, - "recompose": { - "version": "0.21.2", - "resolved": "https://registry.npmjs.org/recompose/-/recompose-0.21.2.tgz", - "integrity": "sha1-/z+9sjl7HHfEfUUb4qY7kpXURoE=", - "requires": { - "change-emitter": "^0.1.2", - "fbjs": "^0.8.1", - "hoist-non-react-statics": "^1.0.0", - "symbol-observable": "^1.0.4" - } - }, - "redent": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", - "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", - "dev": true, - "requires": { - "indent-string": "^2.1.0", - "strip-indent": "^1.0.1" - }, - "dependencies": { - "indent-string": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", - "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", - "dev": true, - "requires": { - "repeating": "^2.0.0" - } - } - } - }, - "regenerate": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", - "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", - "dev": true - }, - "regenerate-unicode-properties": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz", - "integrity": "sha512-LGZzkgtLY79GeXLm8Dp0BVLdQlWICzBnJz/ipWUgo59qBaZ+BHtq51P2q1uVZlppMuUAT37SDk39qUbjTWB7bA==", - "dev": true, - "requires": { - "regenerate": "^1.4.0" - } - }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha1-vgWtf5v30i4Fb5cmzuUBf78Z4uk=" - }, - "regenerator-transform": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.1.tgz", - "integrity": "sha512-flVuee02C3FKRISbxhXl9mGzdbWUVHubl1SMaknjxkFB1/iqpJhArQUvRxOOPEc/9tAiX0BaQ28FJH10E4isSQ==", - "dev": true, - "requires": { - "private": "^0.1.6" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha1-H07OJ+ALC2XgJHpoEOaoXYOldSw=", - "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, - "regexp.prototype.flags": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", - "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1" - } - }, - "regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", - "dev": true - }, - "regexpu-core": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.6.0.tgz", - "integrity": "sha512-YlVaefl8P5BnFYOITTNzDvan1ulLOiXJzCNZxduTIosN17b87h3bvG9yHMoHaRuo88H4mQ06Aodj5VtYGGGiTg==", - "dev": true, - "requires": { - "regenerate": "^1.4.0", - "regenerate-unicode-properties": "^8.1.0", - "regjsgen": "^0.5.0", - "regjsparser": "^0.6.0", - "unicode-match-property-ecmascript": "^1.0.4", - "unicode-match-property-value-ecmascript": "^1.1.0" - } - }, - "regjsgen": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.1.tgz", - "integrity": "sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg==", - "dev": true - }, - "regjsparser": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.3.tgz", - "integrity": "sha512-8uZvYbnfAtEm9Ab8NTb3hdLwL4g/LQzEYP7Xs27T96abJCCE2d6r3cPZPQEsLKy0vRSGVNG+/zVGtLr86HQduA==", - "dev": true, - "requires": { - "jsesc": "~0.5.0" - }, - "dependencies": { - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", - "dev": true - } - } - }, - "relateurl": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", - "dev": true - }, - "remark": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/remark/-/remark-10.0.1.tgz", - "integrity": "sha512-E6lMuoLIy2TyiokHprMjcWNJ5UxfGQjaMSMhV+f4idM625UjjK4j798+gPs5mfjzDE6vL0oFKVeZM6gZVSVrzQ==", - "dev": true, - "requires": { - "remark-parse": "^6.0.0", - "remark-stringify": "^6.0.0", - "unified": "^7.0.0" - } - }, - "remark-parse": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-6.0.3.tgz", - "integrity": "sha512-QbDXWN4HfKTUC0hHa4teU463KclLAnwpn/FBn87j9cKYJWWawbiLgMfP2Q4XwhxxuuuOxHlw+pSN0OKuJwyVvg==", - "dev": true, - "requires": { - "collapse-white-space": "^1.0.2", - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-whitespace-character": "^1.0.0", - "is-word-character": "^1.0.0", - "markdown-escapes": "^1.0.0", - "parse-entities": "^1.1.0", - "repeat-string": "^1.5.4", - "state-toggle": "^1.0.0", - "trim": "0.0.1", - "trim-trailing-lines": "^1.0.0", - "unherit": "^1.0.4", - "unist-util-remove-position": "^1.0.0", - "vfile-location": "^2.0.0", - "xtend": "^4.0.1" - } - }, - "remark-stringify": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-6.0.4.tgz", - "integrity": "sha512-eRWGdEPMVudijE/psbIDNcnJLRVx3xhfuEsTDGgH4GsFF91dVhw5nhmnBppafJ7+NWINW6C7ZwWbi30ImJzqWg==", - "dev": true, - "requires": { - "ccount": "^1.0.0", - "is-alphanumeric": "^1.0.0", - "is-decimal": "^1.0.0", - "is-whitespace-character": "^1.0.0", - "longest-streak": "^2.0.1", - "markdown-escapes": "^1.0.0", - "markdown-table": "^1.1.0", - "mdast-util-compact": "^1.0.0", - "parse-entities": "^1.0.2", - "repeat-string": "^1.5.4", - "state-toggle": "^1.0.0", - "stringify-entities": "^1.0.1", - "unherit": "^1.0.4", - "xtend": "^4.0.1" - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "renderkid": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.3.tgz", - "integrity": "sha512-z8CLQp7EZBPCwCnncgf9C4XAi3WR0dv+uWu/PjIyhhAb5d6IJ/QZqlHFprHeKT+59//V6BNUsLbvN8+2LarxGA==", - "dev": true, - "requires": { - "css-select": "^1.1.0", - "dom-converter": "^0.2", - "htmlparser2": "^3.3.0", - "strip-ansi": "^3.0.0", - "utila": "^0.4.0" - } - }, - "repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "dev": true, - "requires": { - "is-finite": "^1.0.0" - } - }, - "replace-ext": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true - }, - "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", - "dev": true - }, - "reserved-words": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/reserved-words/-/reserved-words-0.1.2.tgz", - "integrity": "sha1-AKCUD5jNUBrqqsMWQR2a3FKzGrE=" - }, - "resolve": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", - "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - }, - "resolve-cwd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", - "dev": true, - "requires": { - "resolve-from": "^3.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - } - } - }, - "resolve-dir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", - "dev": true, - "requires": { - "expand-tilde": "^2.0.0", - "global-modules": "^1.0.0" - }, - "dependencies": { - "global-modules": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", - "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", - "dev": true, - "requires": { - "global-prefix": "^1.0.1", - "is-windows": "^1.0.1", - "resolve-dir": "^1.0.0" - } - }, - "global-prefix": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", - "dev": true, - "requires": { - "expand-tilde": "^2.0.2", - "homedir-polyfill": "^1.0.1", - "ini": "^1.3.4", - "is-windows": "^1.0.1", - "which": "^1.2.14" - } - } - } - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - } - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha1-uKSCXVvbH8P29Twrwz+BOIaBx7w=", - "dev": true - }, - "retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - }, - "dependencies": { - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dev": true, - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", - "requires": { - "is-promise": "^2.1.0" - } - }, - "run-parallel": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz", - "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==", - "dev": true - }, - "run-queue": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", - "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", - "dev": true, - "requires": { - "aproba": "^1.1.1" - } - }, - "rx": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", - "integrity": "sha1-pfE/957zt0D+MKqAP7CfmIBdR4I=" - }, - "rxjs": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz", - "integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha1-RPoWGwGHuVSd2Eu5GAL5vYOFzWo=" - }, - "sass-graph": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", - "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=", - "dev": true, - "requires": { - "glob": "^7.0.0", - "lodash": "^4.0.0", - "scss-tokenizer": "^0.2.3", - "yargs": "^7.0.0" - } - }, - "sass-loader": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-7.3.1.tgz", - "integrity": "sha512-tuU7+zm0pTCynKYHpdqaPpe+MMTQ76I9TPZ7i4/5dZsigE350shQWe5EZNl5dBidM49TPET75tNqRbcsUZWeNA==", - "dev": true, - "requires": { - "clone-deep": "^4.0.1", - "loader-utils": "^1.0.1", - "neo-async": "^2.5.0", - "pify": "^4.0.1", - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "sass-material-colors": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/sass-material-colors/-/sass-material-colors-0.0.5.tgz", - "integrity": "sha1-uURTkjbJTi1OzjXS7j+iLxCy7UM=" - }, - "schema-inspector": { - "version": "1.6.9", - "resolved": "https://registry.npmjs.org/schema-inspector/-/schema-inspector-1.6.9.tgz", - "integrity": "sha512-MNS3SOn6noecIv9R+gwroIgiOLQoRY1IRXToFvVBo2QMfnXy1E+SGRVWJFsJPqgy0lAivUfPLaVLhvAI35HKRg==", - "requires": { - "async": "^3.1.0" - } - }, - "schema-utils": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.4.tgz", - "integrity": "sha512-VNjcaUxVnEeun6B2fiiUDjXXBtD4ZSH7pdbfIu1pOFwgptDPLMo/z9jr4sUfsjFVPqDCEin/F7IYlq7/E6yDbQ==", - "dev": true, - "requires": { - "ajv": "^6.10.2", - "ajv-keywords": "^3.4.1" - } - }, - "scss-tokenizer": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", - "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", - "dev": true, - "requires": { - "js-base64": "^2.1.8", - "source-map": "^0.4.2" - }, - "dependencies": { - "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "dev": true, - "requires": { - "amdefine": ">=0.0.4" - } - } - } - }, - "select": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", - "integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=" - }, - "select-hose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", - "dev": true - }, - "selfsigned": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.7.tgz", - "integrity": "sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==", - "dev": true, - "requires": { - "node-forge": "0.9.0" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - }, - "send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", - "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", - "dev": true, - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.7.2", - "mime": "1.6.0", - "ms": "2.1.1", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" - }, - "dependencies": { - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - } - } - }, - "serialize-javascript": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", - "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==", - "dev": true - }, - "serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", - "dev": true, - "requires": { - "accepts": "~1.3.4", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" - }, - "dependencies": { - "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true - } - } - }, - "serve-static": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", - "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", - "dev": true, - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.17.1" - } - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true - }, - "set-immediate-shim": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" - }, - "set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" - }, - "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", - "dev": true - }, - "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "requires": { - "kind-of": "^6.0.2" - } - }, - "shallowequal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "sigmund": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", - "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=" - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" - }, - "simple-assign": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/simple-assign/-/simple-assign-0.1.0.tgz", - "integrity": "sha1-F/0wZqXz13OPUDIbsPFMooHMS6o=" - }, - "simple-fmt": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/simple-fmt/-/simple-fmt-0.1.0.tgz", - "integrity": "sha1-GRv1ZqWeZTBILLJatTtKjchcOms=", - "dev": true - }, - "simple-is": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/simple-is/-/simple-is-0.2.0.tgz", - "integrity": "sha1-Krt1qt453rXMgVzhDmGRFkhQuvA=", - "dev": true - }, - "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true - }, - "slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - } - } - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha1-ZJIufFZbDhQgS6GqfWlkJ40lGC0=", - "dev": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha1-bBdfhv8UvbByRWPo88GwIaKGhTs=", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha1-+VZHlIbyrNeXAGk/b3uAXkWrVuI=", - "dev": true, - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "sockjs": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", - "integrity": "sha1-2Xa76ACve9IK4IWY1YI5NQiZPA0=", - "dev": true, - "requires": { - "faye-websocket": "^0.10.0", - "uuid": "^3.0.1" - } - }, - "sockjs-client": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.4.0.tgz", - "integrity": "sha512-5zaLyO8/nri5cua0VtOrFXBPK1jbL4+1cebT/mmKA1E1ZXOvJrII75bPu0l0k843G/+iAbhEqzyKr0w/eCCj7g==", - "dev": true, - "requires": { - "debug": "^3.2.5", - "eventsource": "^1.0.7", - "faye-websocket": "~0.11.1", - "inherits": "^2.0.3", - "json3": "^3.3.2", - "url-parse": "^1.4.3" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "faye-websocket": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", - "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", - "dev": true, - "requires": { - "websocket-driver": ">=0.5.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "sort-keys": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", - "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", - "dev": true, - "requires": { - "is-plain-obj": "^1.0.0" - } - }, - "source-list-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - }, - "source-map-resolve": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", - "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", - "dev": true, - "requires": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-support": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz", - "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "dev": true - }, - "spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha1-meEZt6XaAOBUkcn6M4t5BII7QdA=", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", - "dev": true - }, - "spdy": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.1.tgz", - "integrity": "sha512-HeZS3PBdMA+sZSu0qwpCxl3DeALD5ASx8pAX0jZdKXSpPWbQ6SYGnlg3BBmYLx5LtiZrmkAZfErCm2oECBcioA==", - "dev": true, - "requires": { - "debug": "^4.1.0", - "handle-thing": "^2.0.0", - "http-deceiver": "^1.2.7", - "select-hose": "^2.0.0", - "spdy-transport": "^3.0.0" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "spdy-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", - "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", - "dev": true, - "requires": { - "debug": "^4.1.0", - "detect-node": "^2.0.4", - "hpack.js": "^2.1.6", - "obuf": "^1.1.2", - "readable-stream": "^3.0.6", - "wbuf": "^1.7.3" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "specificity": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/specificity/-/specificity-0.4.1.tgz", - "integrity": "sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==", - "dev": true - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha1-fLCd2jqGWFcFxks5pkZgOGguj+I=", - "dev": true, - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "split.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/split.js/-/split.js-1.3.5.tgz", - "integrity": "sha1-YuLOZtLPkcx3SqXwdJ/yUTgDn1A=" - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "ssri": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-7.1.0.tgz", - "integrity": "sha512-77/WrDZUWocK0mvA5NTRQyveUf+wsrIc6vyrxpS8tVvYBcX215QbafrJR3KtkpskIzoFLqqNuuYQvxaMjXJ/0g==", - "dev": true, - "requires": { - "figgy-pudding": "^3.5.1", - "minipass": "^3.1.1" - } - }, - "stable": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", - "dev": true - }, - "state-toggle": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz", - "integrity": "sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==", - "dev": true - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", - "dev": true - }, - "stdout-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", - "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", - "dev": true, - "requires": { - "readable-stream": "^2.0.1" - } - }, - "stream-browserify": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", - "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", - "dev": true, - "requires": { - "inherits": "~2.0.1", - "readable-stream": "^2.0.2" - } - }, - "stream-each": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", - "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "stream-shift": "^1.0.0" - } - }, - "stream-http": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", - "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", - "dev": true, - "requires": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.3.6", - "to-arraybuffer": "^1.0.0", - "xtend": "^4.0.0" - } - }, - "stream-shift": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", - "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", - "dev": true - }, - "strict-uri-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha1-q5Pyeo3BPSjKyBXEYhQ6bZASrp4=", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "string.prototype.trimleft": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", - "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - }, - "string.prototype.trimright": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", - "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "stringify-entities": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-1.3.2.tgz", - "integrity": "sha512-nrBAQClJAPN2p+uGCVJRPIPakKeKWZ9GtBCmormE7pWOSlHat7+x5A8gx85M7HM5Dt0BP3pP5RhVW77WdbJJ3A==", - "dev": true, - "requires": { - "character-entities-html4": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-hexadecimal": "^1.0.0" - } - }, - "stringmap": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stringmap/-/stringmap-0.2.2.tgz", - "integrity": "sha1-VWwTeyWPlCuHdvWy71gqoGnX0bE=", - "dev": true - }, - "stringset": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/stringset/-/stringset-0.2.1.tgz", - "integrity": "sha1-7yWcTjSTRDd/zRyRPdLoSMnAQrU=", - "dev": true - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, - "strip-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", - "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", - "dev": true, - "requires": { - "get-stdin": "^4.0.1" - } - }, - "strip-json-comments": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", - "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", - "dev": true - }, - "style-loader": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.23.1.tgz", - "integrity": "sha512-XK+uv9kWwhZMZ1y7mysB+zoihsEj4wneFWAS5qoiLwzW0WzSqMrrsIy+a3zkQJq0ipFtBpX5W3MqyRIBF/WFGg==", - "dev": true, - "requires": { - "loader-utils": "^1.1.0", - "schema-utils": "^1.0.0" - }, - "dependencies": { - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - } - } - }, - "style-search": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz", - "integrity": "sha1-eVjHk+R+MuB9K1yv5cC/jhLneQI=", - "dev": true - }, - "stylelint": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-13.0.0.tgz", - "integrity": "sha512-6sjgOJbM3iLhnUtmRO0J1vvxie9VnhIZX/2fCehjylv9Gl9u0ytehGCTm9Lhw2p1F8yaNZn5UprvhCB8C3g/Tg==", - "dev": true, - "requires": { - "autoprefixer": "^9.7.3", - "balanced-match": "^1.0.0", - "chalk": "^3.0.0", - "cosmiconfig": "^6.0.0", - "debug": "^4.1.1", - "execall": "^2.0.0", - "file-entry-cache": "^5.0.1", - "get-stdin": "^7.0.0", - "global-modules": "^2.0.0", - "globby": "^11.0.0", - "globjoin": "^0.1.4", - "html-tags": "^3.1.0", - "ignore": "^5.1.4", - "import-lazy": "^4.0.0", - "imurmurhash": "^0.1.4", - "known-css-properties": "^0.17.0", - "leven": "^3.1.0", - "lodash": "^4.17.15", - "log-symbols": "^3.0.0", - "mathml-tag-names": "^2.1.1", - "meow": "^6.0.0", - "micromatch": "^4.0.2", - "normalize-selector": "^0.2.0", - "postcss": "^7.0.26", - "postcss-html": "^0.36.0", - "postcss-jsx": "^0.36.3", - "postcss-less": "^3.1.4", - "postcss-markdown": "^0.36.0", - "postcss-media-query-parser": "^0.2.3", - "postcss-reporter": "^6.0.1", - "postcss-resolve-nested-selector": "^0.1.1", - "postcss-safe-parser": "^4.0.1", - "postcss-sass": "^0.4.2", - "postcss-scss": "^2.0.0", - "postcss-selector-parser": "^3.1.0", - "postcss-syntax": "^0.36.2", - "postcss-value-parser": "^4.0.2", - "resolve-from": "^5.0.0", - "slash": "^3.0.0", - "specificity": "^0.4.1", - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "style-search": "^0.1.0", - "sugarss": "^2.0.0", - "svg-tags": "^1.0.0", - "table": "^5.4.6", - "v8-compile-cache": "^2.1.0", - "write-file-atomic": "^3.0.1" - }, - "dependencies": { - "@nodelib/fs.stat": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", - "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==", - "dev": true - }, - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "camelcase-keys": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.1.1.tgz", - "integrity": "sha512-kEPCddRFChEzO0d6w61yh0WbBiSv9gBnfZWGfXRYPlGqIdIGef6HMR6pgqVSEWCYkrp8B0AtEpEXNY+Jx0xk1A==", - "dev": true, - "requires": { - "camelcase": "^5.3.1", - "map-obj": "^4.0.0", - "quick-lru": "^4.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.7.2" - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "dot-prop": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", - "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", - "dev": true, - "requires": { - "is-obj": "^2.0.0" - } - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "fast-glob": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.1.1.tgz", - "integrity": "sha512-nTCREpBY8w8r+boyFYAx21iL6faSsQynliPHM4Uf56SbkyohCNxpVPEH9xrF5TXKy+IsjkPUHDKiUkzBVRXn9g==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.0", - "merge2": "^1.3.0", - "micromatch": "^4.0.2" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "get-stdin": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz", - "integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==", - "dev": true - }, - "glob-parent": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", - "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "globby": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.0.tgz", - "integrity": "sha512-iuehFnR3xu5wBBtm4xi0dMe92Ob87ufyu/dHwpDYfbcpYpIbrO5OnS8M1vWvrBhSGEJ3/Ecj7gnX76P8YxpPEg==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.1.1", - "ignore": "^5.1.4", - "merge2": "^1.3.0", - "slash": "^3.0.0" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "ignore": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz", - "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==", - "dev": true - }, - "import-fresh": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", - "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - } - } - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, - "map-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.1.0.tgz", - "integrity": "sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g==", - "dev": true - }, - "meow": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-6.0.0.tgz", - "integrity": "sha512-x4rYsjigPBDAxY+BGuK83YLhUIqui5wYyZoqb6QJCUOs+0fiYq+i/NV4Jt8OgIfObZFxG9iTyvLDu4UTohGTFw==", - "dev": true, - "requires": { - "@types/minimist": "^1.2.0", - "camelcase-keys": "^6.1.1", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.0.0", - "minimist-options": "^4.0.1", - "normalize-package-data": "^2.5.0", - "read-pkg-up": "^7.0.0", - "redent": "^3.0.0", - "trim-newlines": "^3.0.0", - "type-fest": "^0.8.1", - "yargs-parser": "^16.1.0" - } - }, - "merge2": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz", - "integrity": "sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==", - "dev": true - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "parse-json": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", - "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1", - "lines-and-columns": "^1.1.6" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "postcss": { - "version": "7.0.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.26.tgz", - "integrity": "sha512-IY4oRjpXWYshuTDFxMVkJDtWIk2LhsTlu8bZnbEJA4+bYT16Lvpo8Qv6EvDumhYRgzjZl489pmsY3qVgJQ08nA==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-selector-parser": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", - "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", - "dev": true, - "requires": { - "dot-prop": "^5.2.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - }, - "postcss-value-parser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz", - "integrity": "sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ==", - "dev": true - }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - } - } - }, - "read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "requires": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - } - }, - "redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "requires": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - } - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "requires": { - "min-indent": "^1.0.0" - } - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", - "dev": true, - "requires": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "trim-newlines": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.0.tgz", - "integrity": "sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA==", - "dev": true - }, - "v8-compile-cache": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", - "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", - "dev": true - }, - "yargs-parser": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-16.1.0.tgz", - "integrity": "sha512-H/V41UNZQPkUMIT5h5hiwg4QKIY1RPvoBV4XcjUbRM8Bk2oKqqyZ0DIEbTFZB0XjbtSPG8SAa/0DxCQmiRgzKg==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "stylelint-config-recommended": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-3.0.0.tgz", - "integrity": "sha512-F6yTRuc06xr1h5Qw/ykb2LuFynJ2IxkKfCMf+1xqPffkxh0S09Zc902XCffcsw/XMFq/OzQ1w54fLIDtmRNHnQ==", - "dev": true - }, - "stylelint-config-recommended-scss": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-4.1.0.tgz", - "integrity": "sha512-4012ca0weVi92epm3RRBRZcRJIyl5vJjJ/tJAKng+Qat5+cnmuCwyOI2vXkKdjNfGd0gvzyKCKEkvTMDcbtd7Q==", - "dev": true, - "requires": { - "stylelint-config-recommended": "^3.0.0" - } - }, - "stylelint-config-standard": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/stylelint-config-standard/-/stylelint-config-standard-19.0.0.tgz", - "integrity": "sha512-VvcODsL1PryzpYteWZo2YaA5vU/pWfjqBpOvmeA8iB2MteZ/ZhI1O4hnrWMidsS4vmEJpKtjdhLdfGJmmZm6Cg==", - "dev": true, - "requires": { - "stylelint-config-recommended": "^3.0.0" - } - }, - "stylelint-order": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/stylelint-order/-/stylelint-order-4.0.0.tgz", - "integrity": "sha512-bXV0v+jfB0+JKsqIn3mLglg1Dj2QCYkFHNfL1c+rVMEmruZmW5LUqT/ARBERfBm8SFtCuXpEdatidw/3IkcoiA==", - "dev": true, - "requires": { - "lodash": "^4.17.15", - "postcss": "^7.0.26", - "postcss-sorting": "^5.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, - "postcss": { - "version": "7.0.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.26.tgz", - "integrity": "sha512-IY4oRjpXWYshuTDFxMVkJDtWIk2LhsTlu8bZnbEJA4+bYT16Lvpo8Qv6EvDumhYRgzjZl489pmsY3qVgJQ08nA==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "stylelint-scss": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-3.13.0.tgz", - "integrity": "sha512-SaLnvQyndaPcsgVJsMh6zJ1uKVzkRZJx+Wg/stzoB1mTBdEmGketbHrGbMQNymzH/0mJ06zDSpeCDvNxqIJE5A==", - "dev": true, - "requires": { - "lodash.isboolean": "^3.0.3", - "lodash.isregexp": "^4.0.1", - "lodash.isstring": "^4.0.1", - "postcss-media-query-parser": "^0.2.3", - "postcss-resolve-nested-selector": "^0.1.1", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.0.2" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz", - "integrity": "sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ==", - "dev": true - } - } - }, - "stylelint-webpack-plugin": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/stylelint-webpack-plugin/-/stylelint-webpack-plugin-1.2.1.tgz", - "integrity": "sha512-J2CFUliPYxirP8l4HUOZmKNMW6HETFPX6wxlQIlfddfV74GFaK6wDk31306LdA5bc8MOOCSsDg4u3FYVlFtF3A==", - "dev": true, - "requires": { - "arrify": "^2.0.1", - "micromatch": "^4.0.2", - "schema-utils": "^2.6.1" - }, - "dependencies": { - "arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", - "dev": true - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "schema-utils": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.4.tgz", - "integrity": "sha512-VNjcaUxVnEeun6B2fiiUDjXXBtD4ZSH7pdbfIu1pOFwgptDPLMo/z9jr4sUfsjFVPqDCEin/F7IYlq7/E6yDbQ==", - "dev": true, - "requires": { - "ajv": "^6.10.2", - "ajv-keywords": "^3.4.1" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "sugarss": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-2.0.0.tgz", - "integrity": "sha512-WfxjozUk0UVA4jm+U1d736AUpzSrNsQcIbyOkoE364GrtWmIrFdk5lksEupgWMD4VaT/0kVx1dobpiDumSgmJQ==", - "dev": true, - "requires": { - "postcss": "^7.0.2" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - }, - "svg-tags": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", - "integrity": "sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=", - "dev": true - }, - "symbol-observable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha1-wiaIrtTqs83C3+rLtWFmBWCgCAQ=" - }, - "table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", - "dev": true, - "requires": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "dev": true - }, - "tar": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", - "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", - "dev": true, - "requires": { - "block-stream": "*", - "fstream": "^1.0.12", - "inherits": "2" - } - }, - "terser": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.3.tgz", - "integrity": "sha512-Lw+ieAXmY69d09IIc/yqeBqXpEQIpDGZqT34ui1QWXIUpR2RjbqEkT8X7Lgex19hslSqcWM5iMN2kM11eMsESQ==", - "dev": true, - "requires": { - "commander": "^2.20.0", - "source-map": "~0.6.1", - "source-map-support": "~0.5.12" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "terser-webpack-plugin": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz", - "integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==", - "dev": true, - "requires": { - "cacache": "^12.0.2", - "find-cache-dir": "^2.1.0", - "is-wsl": "^1.1.0", - "schema-utils": "^1.0.0", - "serialize-javascript": "^2.1.2", - "source-map": "^0.6.1", - "terser": "^4.1.2", - "webpack-sources": "^1.4.0", - "worker-farm": "^1.7.0" - }, - "dependencies": { - "cacache": { - "version": "12.0.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.3.tgz", - "integrity": "sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==", - "dev": true, - "requires": { - "bluebird": "^3.5.5", - "chownr": "^1.1.1", - "figgy-pudding": "^3.5.1", - "glob": "^7.1.4", - "graceful-fs": "^4.1.15", - "infer-owner": "^1.0.3", - "lru-cache": "^5.1.1", - "mississippi": "^3.0.0", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "promise-inflight": "^1.0.1", - "rimraf": "^2.6.3", - "ssri": "^6.0.1", - "unique-filename": "^1.1.1", - "y18n": "^4.0.0" - } - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "requires": { - "yallist": "^3.0.2" - } - }, - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "ssri": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", - "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", - "dev": true, - "requires": { - "figgy-pudding": "^3.5.1" - } - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - } - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "thunky": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", - "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", - "dev": true - }, - "timers-browserify": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.11.tgz", - "integrity": "sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==", - "dev": true, - "requires": { - "setimmediate": "^1.0.4" - } - }, - "tiny-emitter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", - "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" - }, - "tinycolor2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz", - "integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=" - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha1-bTQzWIl2jSGyvNoKonfO07G/rfk=", - "requires": { - "os-tmpdir": "~1.0.2" - } - }, - "to-arraybuffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", - "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", - "dev": true - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha1-E8/dmzNlUvMLUfM6iuG0Knp1mc4=", - "dev": true, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - }, - "toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", - "dev": true - }, - "tooltipster": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/tooltipster/-/tooltipster-4.2.7.tgz", - "integrity": "sha512-W4tY3LG2eyPY2VQZRH3JcsNuRl3jPCEGmKBPOMTP/05E3+1kOJjASzPRRkcpP+uf9vqX7+896ivU86f6B8Esgw==" - }, - "toposort": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz", - "integrity": "sha1-LmhELZ9k7HILjMieZEOsbKqVACk=", - "dev": true - }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "trim": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", - "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=", - "dev": true - }, - "trim-newlines": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", - "dev": true - }, - "trim-trailing-lines": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.3.tgz", - "integrity": "sha512-4ku0mmjXifQcTVfYDfR5lpgV7zVqPg6zV9rdZmwOPqq0+Zq19xDqEgagqVbc4pOOShbncuAOIs59R3+3gcF3ZA==", - "dev": true - }, - "trough": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz", - "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==", - "dev": true - }, - "true-case-path": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", - "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", - "dev": true, - "requires": { - "glob": "^7.1.2" - }, - "dependencies": { - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "tryor": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/tryor/-/tryor-0.1.2.tgz", - "integrity": "sha1-gUXkynyv9ArN48z5Rui4u3W0Fys=", - "dev": true - }, - "tslib": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", - "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", - "dev": true - }, - "tty-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", - "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", - "dev": true - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tv4": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz", - "integrity": "sha1-0CDIRvrdUMhVq7JeuuzGj8EPeWM=" - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true - }, - "typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, - "requires": { - "is-typedarray": "^1.0.0" - } - }, - "typeface-roboto": { - "version": "0.0.22", - "resolved": "https://registry.npmjs.org/typeface-roboto/-/typeface-roboto-0.0.22.tgz", - "integrity": "sha1-A7YLsCsQ+VCaaDImsDmucEFj5WE=" - }, - "ua-parser-js": { - "version": "0.7.21", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz", - "integrity": "sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ==" - }, - "uglify-js": { - "version": "3.4.10", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz", - "integrity": "sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==", - "dev": true, - "requires": { - "commander": "~2.19.0", - "source-map": "~0.6.1" - }, - "dependencies": { - "commander": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", - "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", - "dev": true - } - } - }, - "uglifyjs-webpack-plugin": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-2.2.0.tgz", - "integrity": "sha512-mHSkufBmBuJ+KHQhv5H0MXijtsoA1lynJt1lXOaotja8/I0pR4L9oGaPIZw+bQBOFittXZg9OC1sXSGO9D9ZYg==", - "dev": true, - "requires": { - "cacache": "^12.0.2", - "find-cache-dir": "^2.1.0", - "is-wsl": "^1.1.0", - "schema-utils": "^1.0.0", - "serialize-javascript": "^1.7.0", - "source-map": "^0.6.1", - "uglify-js": "^3.6.0", - "webpack-sources": "^1.4.0", - "worker-farm": "^1.7.0" - }, - "dependencies": { - "cacache": { - "version": "12.0.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.3.tgz", - "integrity": "sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==", - "dev": true, - "requires": { - "bluebird": "^3.5.5", - "chownr": "^1.1.1", - "figgy-pudding": "^3.5.1", - "glob": "^7.1.4", - "graceful-fs": "^4.1.15", - "infer-owner": "^1.0.3", - "lru-cache": "^5.1.1", - "mississippi": "^3.0.0", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "promise-inflight": "^1.0.1", - "rimraf": "^2.6.3", - "ssri": "^6.0.1", - "unique-filename": "^1.1.1", - "y18n": "^4.0.0" - } - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "requires": { - "yallist": "^3.0.2" - } - }, - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - }, - "serialize-javascript": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.9.1.tgz", - "integrity": "sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "ssri": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", - "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", - "dev": true, - "requires": { - "figgy-pudding": "^3.5.1" - } - }, - "uglify-js": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.8.0.tgz", - "integrity": "sha512-ugNSTT8ierCsDHso2jkBHXYrU8Y5/fY2ZUprfrJUiD7YpuFvV4jODLFmb3h4btQjqr5Nh4TX4XtgDfCU1WdioQ==", - "dev": true, - "requires": { - "commander": "~2.20.3", - "source-map": "~0.6.1" - } - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - } - } - }, - "ultron": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz", - "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=" - }, - "unherit": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz", - "integrity": "sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==", - "dev": true, - "requires": { - "inherits": "^2.0.0", - "xtend": "^4.0.0" - } - }, - "unicode-canonical-property-names-ecmascript": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", - "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", - "dev": true - }, - "unicode-match-property-ecmascript": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", - "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", - "dev": true, - "requires": { - "unicode-canonical-property-names-ecmascript": "^1.0.4", - "unicode-property-aliases-ecmascript": "^1.0.4" - } - }, - "unicode-match-property-value-ecmascript": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz", - "integrity": "sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g==", - "dev": true - }, - "unicode-property-aliases-ecmascript": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz", - "integrity": "sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw==", - "dev": true - }, - "unified": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/unified/-/unified-7.1.0.tgz", - "integrity": "sha512-lbk82UOIGuCEsZhPj8rNAkXSDXd6p0QLzIuSsCdxrqnqU56St4eyOB+AlXsVgVeRmetPTYydIuvFfpDIed8mqw==", - "dev": true, - "requires": { - "@types/unist": "^2.0.0", - "@types/vfile": "^3.0.0", - "bail": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^1.1.0", - "trough": "^1.0.0", - "vfile": "^3.0.0", - "x-is-string": "^0.1.0" - } - }, - "union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - } - }, - "uniq": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", - "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", - "dev": true - }, - "unique-filename": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", - "dev": true, - "requires": { - "unique-slug": "^2.0.0" - } - }, - "unique-slug": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", - "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4" - } - }, - "unist-util-find-all-after": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/unist-util-find-all-after/-/unist-util-find-all-after-1.0.5.tgz", - "integrity": "sha512-lWgIc3rrTMTlK1Y0hEuL+k+ApzFk78h+lsaa2gHf63Gp5Ww+mt11huDniuaoq1H+XMK2lIIjjPkncxXcDp3QDw==", - "dev": true, - "requires": { - "unist-util-is": "^3.0.0" - } - }, - "unist-util-is": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz", - "integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==", - "dev": true - }, - "unist-util-remove-position": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-1.1.4.tgz", - "integrity": "sha512-tLqd653ArxJIPnKII6LMZwH+mb5q+n/GtXQZo6S6csPRs5zB0u79Yw8ouR3wTw8wxvdJFhpP6Y7jorWdCgLO0A==", - "dev": true, - "requires": { - "unist-util-visit": "^1.1.0" - } - }, - "unist-util-stringify-position": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.2.tgz", - "integrity": "sha512-nK5n8OGhZ7ZgUwoUbL8uiVRwAbZyzBsB/Ddrlbu6jwwubFza4oe15KlyEaLNMXQW1svOQq4xesUeqA85YrIUQA==", - "dev": true, - "requires": { - "@types/unist": "^2.0.2" - } - }, - "unist-util-visit": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.1.tgz", - "integrity": "sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==", - "dev": true, - "requires": { - "unist-util-visit-parents": "^2.0.0" - } - }, - "unist-util-visit-parents": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz", - "integrity": "sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==", - "dev": true, - "requires": { - "unist-util-is": "^3.0.0" - } - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", - "dev": true - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true - } - } - }, - "upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", - "dev": true - }, - "upper-case": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", - "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=", - "dev": true - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha1-lMVA4f93KVbiKZUHwBCupsiDjrA=", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, - "url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", - "dev": true, - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - }, - "dependencies": { - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", - "dev": true - } - } - }, - "url-loader": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-2.3.0.tgz", - "integrity": "sha512-goSdg8VY+7nPZKUEChZSEtW5gjbS66USIGCeSJ1OVOJ7Yfuh/36YxCwMi5HVEJh6mqUYOoy3NJ0vlOMrWsSHog==", - "dev": true, - "requires": { - "loader-utils": "^1.2.3", - "mime": "^2.4.4", - "schema-utils": "^2.5.0" - }, - "dependencies": { - "mime": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", - "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", - "dev": true - } - } - }, - "url-parse": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", - "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", - "dev": true, - "requires": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha1-1QyMrHmhn7wg8pEfVuuXP04QBw8=", - "dev": true - }, - "util": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", - "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", - "dev": true, - "requires": { - "inherits": "2.0.3" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "util.promisify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", - "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "object.getownpropertydescriptors": "^2.0.3" - } - }, - "utila": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", - "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=", - "dev": true - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", - "dev": true - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true - }, - "v-accordion": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/v-accordion/-/v-accordion-1.6.0.tgz", - "integrity": "sha1-8KiaFsLWlcEe4sq4uptRkevIthM=" - }, - "v8-compile-cache": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", - "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", - "dev": true - }, - "v8flags": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.3.tgz", - "integrity": "sha512-amh9CCg3ZxkzQ48Mhcb8iX7xpAfYJgePHxWMQCBWECpOSqJUXgY26ncA61UTV0BkPqfhcy6mzwCIoP4ygxpW8w==", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1" - } - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", - "dev": true - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "vfile": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-3.0.1.tgz", - "integrity": "sha512-y7Y3gH9BsUSdD4KzHsuMaCzRjglXN0W2EcMf0gpvu6+SbsGhMje7xDc8AEoeXy6mIwCKMI6BkjMsRjzQbhMEjQ==", - "dev": true, - "requires": { - "is-buffer": "^2.0.0", - "replace-ext": "1.0.0", - "unist-util-stringify-position": "^1.0.0", - "vfile-message": "^1.0.0" - }, - "dependencies": { - "is-buffer": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", - "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==", - "dev": true - }, - "unist-util-stringify-position": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz", - "integrity": "sha512-pNCVrk64LZv1kElr0N1wPiHEUoXNVFERp+mlTg/s9R5Lwg87f9bM/3sQB99w+N9D/qnM9ar3+AKDBwo/gm/iQQ==", - "dev": true - }, - "vfile-message": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-1.1.1.tgz", - "integrity": "sha512-1WmsopSGhWt5laNir+633LszXvZ+Z/lxveBf6yhGsqnQIhlhzooZae7zV6YVM1Sdkw68dtAW3ow0pOdPANugvA==", - "dev": true, - "requires": { - "unist-util-stringify-position": "^1.1.1" - } - } - } - }, - "vfile-location": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-2.0.6.tgz", - "integrity": "sha512-sSFdyCP3G6Ka0CEmN83A2YCMKIieHx0EDaj5IDP4g1pa5ZJ4FJDvpO0WODLxo4LUX4oe52gmSCK7Jw4SBghqxA==", - "dev": true - }, - "vfile-message": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.2.tgz", - "integrity": "sha512-gNV2Y2fDvDOOqq8bEe7cF3DXU6QgV4uA9zMR2P8tix11l1r7zju3zry3wZ8sx+BEfuO6WQ7z2QzfWTvqHQiwsA==", - "dev": true, - "requires": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^2.0.0" - } - }, - "vm-browserify": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", - "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", - "dev": true - }, - "w3c-blob": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/w3c-blob/-/w3c-blob-0.0.1.tgz", - "integrity": "sha1-sM01KhpQ9RVWNCD/1YYflQ8dhbg=" - }, - "warning": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", - "integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=", - "requires": { - "loose-envify": "^1.0.0" - } - }, - "watchpack": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", - "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==", - "dev": true, - "requires": { - "chokidar": "^2.0.2", - "graceful-fs": "^4.1.2", - "neo-async": "^2.5.0" - } - }, - "wbuf": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", - "dev": true, - "requires": { - "minimalistic-assert": "^1.0.0" - } - }, - "wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", - "requires": { - "defaults": "^1.0.3" - } - }, - "webpack": { - "version": "4.41.6", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.6.tgz", - "integrity": "sha512-yxXfV0Zv9WMGRD+QexkZzmGIh54bsvEs+9aRWxnN8erLWEOehAKUTeNBoUbA6HPEZPlRo7KDi2ZcNveoZgK9MA==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-module-context": "1.8.5", - "@webassemblyjs/wasm-edit": "1.8.5", - "@webassemblyjs/wasm-parser": "1.8.5", - "acorn": "^6.2.1", - "ajv": "^6.10.2", - "ajv-keywords": "^3.4.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^4.1.0", - "eslint-scope": "^4.0.3", - "json-parse-better-errors": "^1.0.2", - "loader-runner": "^2.4.0", - "loader-utils": "^1.2.3", - "memory-fs": "^0.4.1", - "micromatch": "^3.1.10", - "mkdirp": "^0.5.1", - "neo-async": "^2.6.1", - "node-libs-browser": "^2.2.1", - "schema-utils": "^1.0.0", - "tapable": "^1.1.3", - "terser-webpack-plugin": "^1.4.3", - "watchpack": "^1.6.0", - "webpack-sources": "^1.4.1" - }, - "dependencies": { - "acorn": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz", - "integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw==", - "dev": true - }, - "eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - } - } - }, - "webpack-cli": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.11.tgz", - "integrity": "sha512-dXlfuml7xvAFwYUPsrtQAA9e4DOe58gnzSxhgrO/ZM/gyXTBowrsYeubyN4mqGhYdpXMFNyQ6emjJS9M7OBd4g==", - "dev": true, - "requires": { - "chalk": "2.4.2", - "cross-spawn": "6.0.5", - "enhanced-resolve": "4.1.0", - "findup-sync": "3.0.0", - "global-modules": "2.0.0", - "import-local": "2.0.0", - "interpret": "1.2.0", - "loader-utils": "1.2.3", - "supports-color": "6.1.0", - "v8-compile-cache": "2.0.3", - "yargs": "13.2.4" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - } - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "enhanced-resolve": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", - "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.4.0", - "tapable": "^1.0.0" - } - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "v8-compile-cache": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz", - "integrity": "sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w==", - "dev": true - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - } - }, - "yargs": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz", - "integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==", - "dev": true, - "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "os-locale": "^3.1.0", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.0" - } - }, - "yargs-parser": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", - "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "webpack-dev-middleware": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz", - "integrity": "sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw==", - "dev": true, - "requires": { - "memory-fs": "^0.4.1", - "mime": "^2.4.4", - "mkdirp": "^0.5.1", - "range-parser": "^1.2.1", - "webpack-log": "^2.0.0" - }, - "dependencies": { - "mime": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", - "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", - "dev": true - } - } - }, - "webpack-dev-server": { - "version": "3.10.3", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.10.3.tgz", - "integrity": "sha512-e4nWev8YzEVNdOMcNzNeCN947sWJNd43E5XvsJzbAL08kGc2frm1tQ32hTJslRS+H65LCb/AaUCYU7fjHCpDeQ==", - "dev": true, - "requires": { - "ansi-html": "0.0.7", - "bonjour": "^3.5.0", - "chokidar": "^2.1.8", - "compression": "^1.7.4", - "connect-history-api-fallback": "^1.6.0", - "debug": "^4.1.1", - "del": "^4.1.1", - "express": "^4.17.1", - "html-entities": "^1.2.1", - "http-proxy-middleware": "0.19.1", - "import-local": "^2.0.0", - "internal-ip": "^4.3.0", - "ip": "^1.1.5", - "is-absolute-url": "^3.0.3", - "killable": "^1.0.1", - "loglevel": "^1.6.6", - "opn": "^5.5.0", - "p-retry": "^3.0.1", - "portfinder": "^1.0.25", - "schema-utils": "^1.0.0", - "selfsigned": "^1.10.7", - "semver": "^6.3.0", - "serve-index": "^1.9.1", - "sockjs": "0.3.19", - "sockjs-client": "1.4.0", - "spdy": "^4.0.1", - "strip-ansi": "^3.0.1", - "supports-color": "^6.1.0", - "url": "^0.11.0", - "webpack-dev-middleware": "^3.7.2", - "webpack-log": "^2.0.0", - "ws": "^6.2.1", - "yargs": "12.0.5" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "dev": true, - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - }, - "dependencies": { - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "opn": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", - "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==", - "dev": true, - "requires": { - "is-wsl": "^1.1.0" - } - }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, - "ws": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", - "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", - "dev": true, - "requires": { - "async-limiter": "~1.0.0" - } - }, - "yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" - } - }, - "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "webpack-hot-middleware": { - "version": "2.25.0", - "resolved": "https://registry.npmjs.org/webpack-hot-middleware/-/webpack-hot-middleware-2.25.0.tgz", - "integrity": "sha512-xs5dPOrGPCzuRXNi8F6rwhawWvQQkeli5Ro48PRuQh8pYPCPmNnltP9itiUPT4xI8oW+y0m59lyyeQk54s5VgA==", - "dev": true, - "requires": { - "ansi-html": "0.0.7", - "html-entities": "^1.2.0", - "querystring": "^0.2.0", - "strip-ansi": "^3.0.0" - } - }, - "webpack-log": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", - "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", - "dev": true, - "requires": { - "ansi-colors": "^3.0.0", - "uuid": "^3.3.2" - } - }, - "webpack-material-design-icons": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/webpack-material-design-icons/-/webpack-material-design-icons-0.1.0.tgz", - "integrity": "sha1-K0g5nxSBf3M9mRQ8l7gAjzQtL9Q=", - "dev": true, - "requires": { - "material-design-icons": "^3.0.1" - } - }, - "webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "dev": true, - "requires": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "websocket-driver": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz", - "integrity": "sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg==", - "dev": true, - "requires": { - "http-parser-js": ">=0.4.0 <0.4.11", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - } - }, - "websocket-extensions": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", - "integrity": "sha1-XS/yKXcAPsaHpLhwc9+7rBRszyk=", - "dev": true - }, - "whatwg-fetch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz", - "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==" - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", - "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", - "dev": true - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true - }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", - "dev": true - }, - "worker-farm": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", - "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", - "dev": true, - "requires": { - "errno": "~0.1.7" - } - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "dev": true, - "requires": { - "mkdirp": "^0.5.1" - } - }, - "write-file-atomic": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.1.tgz", - "integrity": "sha512-JPStrIyyVJ6oCSz/691fAjFtefZ6q+fP6tm+OS4Qw6o+TGQxNp1ziY2PgS+X/m0V8OWhZiO/m4xSj+Pr4RrZvw==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "ws": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.5.tgz", - "integrity": "sha1-y9nm514J/F0skAFfIfDECHXg3VE=", - "requires": { - "options": ">=0.0.5", - "ultron": "1.0.x" - } - }, - "x-is-string": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz", - "integrity": "sha1-R0tQhlrzpJqcRlfwWs0UVFj3fYI=", - "dev": true - }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true - }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" - }, - "yaml": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.7.2.tgz", - "integrity": "sha512-qXROVp90sb83XtAoqE8bP9RwAkTTZbugRUTm5YeFCBfNRPEp2YzTeqWiz7m5OORHzEvrA/qcGS8hp/E+MMROYw==", - "dev": true, - "requires": { - "@babel/runtime": "^7.6.3" - }, - "dependencies": { - "@babel/runtime": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.3.tgz", - "integrity": "sha512-fVHx1rzEmwB130VTkLnxR+HmxcTjGzH12LYQcFFoBwakMd3aOMD4OsRN7tGG/UOYE2ektgFrS8uACAoRk1CY0w==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.13.2" - } - }, - "regenerator-runtime": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", - "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==", - "dev": true - } - } - }, - "yargs": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", - "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", - "dev": true, - "requires": { - "camelcase": "^3.0.0", - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "os-locale": "^1.4.0", - "read-pkg-up": "^1.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^1.0.2", - "which-module": "^1.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^5.0.0" - }, - "dependencies": { - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true - }, - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - } - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "^2.0.0" - } - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "^0.2.0" - } - }, - "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", - "dev": true - } - } - }, - "yargs-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", - "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", - "dev": true, - "requires": { - "camelcase": "^3.0.0" - }, - "dependencies": { - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true - } - } - } - } -} diff --git a/ui/package.json b/ui/package.json deleted file mode 100644 index cbc4b8c6af..0000000000 --- a/ui/package.json +++ /dev/null @@ -1,179 +0,0 @@ -{ - "name": "thingsboard", - "private": true, - "version": "2.5.5", - "description": "ThingsBoard UI", - "licenses": [ - { - "type": "Apache-2.0", - "url": "http://www.apache.org/licenses/LICENSE-2.0" - } - ], - "scripts": { - "start": "babel-node --max_old_space_size=4096 server.js", - "build": "cross-env NODE_OPTIONS=--max_old_space_size=4096 NODE_ENV=production webpack" - }, - "dependencies": { - "@flowjs/ng-flow": "^2.7.1", - "angular": "1.5.8", - "angular-animate": "1.5.8", - "angular-aria": "1.5.8", - "angular-breadcrumb": "^0.4.1", - "angular-carousel": "^1.0.1", - "angular-cookies": "1.5.8", - "angular-drag-and-drop-lists": "^1.4.0", - "angular-fixed-table-header": "^0.2.1", - "angular-fullscreen": "git://github.com/fabiobiondi/angular-fullscreen.git#master", - "angular-gridster": "^0.13.14", - "angular-hotkeys": "^1.7.0", - "angular-jwt": "^0.1.11", - "angular-material": "1.1.19", - "angular-material-data-table": "^0.10.9", - "angular-material-expansion-panel": "^0.7.2", - "angular-material-icons": "^0.7.1", - "angular-messages": "1.5.8", - "angular-route": "1.5.8", - "angular-sanitize": "1.5.8", - "angular-socialshare": "^2.3.8", - "angular-storage": "0.0.15", - "angular-touch": "1.5.8", - "angular-translate": "2.18.1", - "angular-translate-handler-log": "2.18.1", - "angular-translate-interpolation-messageformat": "2.18.1", - "angular-translate-loader-static-files": "2.18.1", - "angular-translate-storage-cookie": "2.18.1", - "angular-translate-storage-local": "2.18.1", - "angular-ui-ace": "^0.2.3", - "angular-ui-router": "^0.3.1", - "angular-websocket": "^2.0.1", - "base64-js": "^1.2.1", - "brace": "^0.10.0", - "canvas-gauges": "^2.0.9", - "clipboard": "^1.5.15", - "compass-sass-mixins": "^0.12.7", - "event-source-polyfill": "0.0.9", - "flot": "git://github.com/thingsboard/flot.git#0.9-work", - "flot.curvedlines": "git://github.com/MichaelZinsmaier/CurvedLines.git#master", - "font-awesome": "^4.6.3", - "javascript-detect-element-resize": "^0.5.3", - "jquery": "^3.4.1", - "jquery.terminal": "^1.5.0", - "js-beautify": "^1.10.0", - "json-schema-defaults": "^0.2.0", - "jstree": "^3.3.8", - "jszip": "^3.2.2", - "jstree-bootstrap-theme": "^1.0.1", - "leaflet": "^1.5.1", - "leaflet-polylinedecorator": "^1.6.0", - "leaflet-providers": "^1.8.0", - "leaflet.markercluster": "^1.4.1", - "material-steppers": "git://github.com/thingsboard/material-steppers.git#master", - "material-ui": "^0.16.1", - "material-ui-number-input": "^5.0.16", - "md-color-picker": "0.2.6", - "md-date-range-picker": "^0.8.4", - "mdPickers": "git://github.com/alenaksu/mdPickers.git#0.7.5", - "moment": "^2.24.0", - "ngFlowchart": "git://github.com/thingsboard/ngFlowchart.git#master", - "ngclipboard": "^1.1.1", - "ngreact": "^0.3.0", - "objectpath": "^1.2.1", - "oclazyload": "^1.0.9", - "raphael": "^2.2.8", - "rc-select": "^6.6.1", - "react": "^15.4.1", - "react-ace": "^4.1.0", - "react-dom": "^15.4.1", - "react-dropzone": "^3.7.3", - "react-schema-form": "^0.3.1", - "react-tap-event-plugin": "^2.0.1", - "reactcss": "^1.0.9", - "sass-material-colors": "0.0.5", - "schema-inspector": "^1.6.6", - "split.js": "1.3.5", - "tinycolor2": "^1.4.1", - "tooltipster": "^4.2.4", - "typeface-roboto": "0.0.22", - "v-accordion": "^1.6.0" - }, - "devDependencies": { - "@babel/cli": "^7.5.5", - "@babel/core": "^7.5.5", - "@babel/node": "^7.5.5", - "@babel/preset-env": "^7.5.5", - "@babel/preset-react": "^7.0.0", - "babel-eslint": "^10.0.2", - "babel-loader": "^8.0.6", - "browserslist": "^4.6.6", - "compression-webpack-plugin": "^3.0.0", - "connect-history-api-fallback": "^1.6.0", - "copy-webpack-plugin": "^5.0.3", - "cross-env": "^5.2.0", - "css-loader": "^3.1.0", - "directory-tree": "^2.2.3", - "eslint": "^6.0.1", - "eslint-config-angular": "^0.5.0", - "eslint-loader": "^2.2.1", - "eslint-plugin-angular": "^4.0.1", - "eslint-plugin-import": "^2.18.1", - "file-loader": "^4.1.0", - "html-loader": "^0.5.5", - "html-minifier": "^4.0.0", - "html-minifier-loader": "^1.4.1", - "html-webpack-plugin": "^3.2.0", - "imagemin": "^6.0.0", - "img-loader": "^3.0.1", - "jsonminify": "^0.4.1", - "less": "^3.9.0", - "less-loader": "^5.0.0", - "mini-css-extract-plugin": "^0.8.0", - "ng-annotate-loader": "^0.6.1", - "ng-annotate-patched": "^1.10.0", - "ngtemplate-loader": "^2.0.1", - "node-sass": "^4.12.0", - "postcss-loader": "^3.0.0", - "raw-loader": "^3.1.0", - "react-hot-loader": "^4.12.8", - "sass-loader": "^7.1.0", - "style-loader": "^0.23.1", - "stylelint": "13.0.0", - "stylelint-config-recommended-scss": "4.1.0", - "stylelint-config-standard": "19.0.0", - "stylelint-order": "4.0.0", - "stylelint-scss": "3.13.0", - "stylelint-webpack-plugin": "^1.2.1", - "uglifyjs-webpack-plugin": "^2.1.3", - "url-loader": "^2.1.0", - "webpack": "^4.37.0", - "webpack-cli": "^3.3.6", - "webpack-dev-middleware": "^3.7.0", - "webpack-dev-server": "^3.7.2", - "webpack-hot-middleware": "^2.25.0", - "webpack-material-design-icons": "^0.1.0" - }, - "engines": { - "node": ">=8.0.0 <11.0.0" - }, - "nyc": { - "exclude": [ - "test", - "__tests__", - "node_modules", - "target" - ] - }, - "browserslist": [ - "> 0.5%", - "last 2 versions", - "Firefox ESR", - "not ie <= 10", - "not ie_mob <= 10", - "not bb <= 10", - "not op_mob <= 12.1" - ], - "postcss": { - "plugins": { - "autoprefixer": true - } - } -} diff --git a/ui/pom.xml b/ui/pom.xml deleted file mode 100644 index b27f492c97..0000000000 --- a/ui/pom.xml +++ /dev/null @@ -1,142 +0,0 @@ - - - 4.0.0 - - org.thingsboard - 2.5.5-SNAPSHOT - thingsboard - - org.thingsboard - ui - jar - - Thingsboard Server UI - https://thingsboard.io - - - UTF-8 - ${basedir}/.. - - - - - - ${project.build.directory}/generated-resources - - - - - com.github.eirslett - frontend-maven-plugin - 1.0 - - target - ${basedir} - - - - install node and npm - - install-node-and-npm - - - v10.16.0 - 6.4.1 - - - - npm install - - npm - - - install - - - - - - - - - npm-build - - true - - - - - com.github.eirslett - frontend-maven-plugin - 1.0 - - target - ${basedir} - - - - npm build - - npm - - - run build - - - - - - - - - npm-start - - - npm-start - - - - - - com.github.eirslett - frontend-maven-plugin - 1.0 - - target - ${basedir} - - - - npm start - - npm - - - - start - - - - - - - - - diff --git a/ui/server.js b/ui/server.js deleted file mode 100644 index e6cb1d650d..0000000000 --- a/ui/server.js +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-commonjs */ -/* eslint-disable global-require */ -/* eslint-disable import/no-nodejs-modules */ - -const path = require('path'); -const webpack = require('webpack'); -const historyApiFallback = require("connect-history-api-fallback"); -const webpackDevMiddleware = require('webpack-dev-middleware'); -const webpackHotMiddleware = require('webpack-hot-middleware'); -const config = require('./webpack.config'); - -const express = require('express'); -const http = require('http'); -const httpProxy = require('http-proxy'); -const forwardHost = 'localhost'; -const forwardPort = 8080; - -const ruleNodeUiforwardHost = 'localhost'; -const ruleNodeUiforwardPort = 8080; - -const app = express(); -const server = http.createServer(app); - -const PORT = 3000; - -const compiler = webpack(config); - -app.use(historyApiFallback()); -app.use(webpackDevMiddleware(compiler, {noInfo: true, publicPath: config.output.publicPath})); -app.use(webpackHotMiddleware(compiler)); - -const root = path.join(__dirname, '/src'); - -app.use('/static', express.static(root)); - -const apiProxy = httpProxy.createProxyServer({ - target: { - host: forwardHost, - port: forwardPort - } -}); - -const ruleNodeUiApiProxy = httpProxy.createProxyServer({ - target: { - host: ruleNodeUiforwardHost, - port: ruleNodeUiforwardPort - } -}); - -apiProxy.on('error', function (err, req, res) { - console.warn('API proxy error: ' + err); - res.end('Error.'); -}); - -ruleNodeUiApiProxy.on('error', function (err, req, res) { - console.warn('RuleNode UI API proxy error: ' + err); - res.end('Error.'); -}); - -console.info(`Forwarding API requests to http://${forwardHost}:${forwardPort}`); -console.info(`Forwarding Rule Node UI requests to http://${ruleNodeUiforwardHost}:${ruleNodeUiforwardPort}`); - -app.all('/api/*', (req, res) => { - apiProxy.web(req, res); -}); - -app.all('/static/rulenode/*', (req, res) => { - ruleNodeUiApiProxy.web(req, res); -}); - -app.get('*', function(req, res) { - res.sendFile(path.join(__dirname, 'src/index.html')); -}); - -server.on('upgrade', (req, socket, head) => { - apiProxy.ws(req, socket, head); -}); - -server.listen(PORT, '0.0.0.0', (error) => { - if (error) { - console.error(error); - } else { - console.info(`==> 🌎 Listening on port ${PORT}. Open up http://localhost:${PORT}/ in your browser.`); - } -}); diff --git a/ui/src/app/admin/admin.controller.js b/ui/src/app/admin/admin.controller.js deleted file mode 100644 index f949c7a2fa..0000000000 --- a/ui/src/app/admin/admin.controller.js +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './settings-card.scss'; - -/*@ngInject*/ -export default function AdminController(adminService, toast, $scope, $rootScope, $state, $translate) { - - var vm = this; - vm.save = save; - vm.sendTestMail = sendTestMail; - vm.smtpProtocols = ('smtp smtps').split(' ').map(function (protocol) { - return protocol; - }); - - vm.tlsVersions = ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3']; - - $translate('admin.test-mail-sent').then(function (translation) { - vm.testMailSent = translation; - }, function (translationId) { - vm.testMailSent = translationId; - }); - - - loadSettings(); - - function loadSettings() { - adminService.getAdminSettings($state.$current.data.key).then(function success(settings) { - vm.settings = settings; - if(vm.settings.jsonValue && vm.settings.jsonValue.enableTls && angular.isString(vm.settings.jsonValue.enableTls)){ - vm.settings.jsonValue.enableTls = vm.settings.jsonValue.enableTls === 'true'; - } - }); - } - - function save() { - adminService.saveAdminSettings(vm.settings).then(function success(settings) { - vm.settings = settings; - vm.settingsForm.$setPristine(); - }); - } - - function sendTestMail() { - adminService.sendTestMail(vm.settings).then(function success() { - toast.showSuccess($translate.instant('admin.test-mail-sent')); - }); - } - -} diff --git a/ui/src/app/admin/admin.routes.js b/ui/src/app/admin/admin.routes.js deleted file mode 100644 index c35117e27c..0000000000 --- a/ui/src/app/admin/admin.routes.js +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import generalSettingsTemplate from '../admin/general-settings.tpl.html'; -import outgoingMailSettingsTemplate from '../admin/outgoing-mail-settings.tpl.html'; -import securitySettingsTemplate from '../admin/security-settings.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function AdminRoutes($stateProvider) { - $stateProvider - .state('home.settings', { - url: '/settings', - module: 'private', - auth: ['SYS_ADMIN'], - redirectTo: 'home.settings.general', - ncyBreadcrumb: { - label: '{"icon": "settings", "label": "admin.system-settings"}' - } - }) - .state('home.settings.general', { - url: '/general', - module: 'private', - auth: ['SYS_ADMIN'], - views: { - "content@home": { - templateUrl: generalSettingsTemplate, - controllerAs: 'vm', - controller: 'AdminController' - } - }, - data: { - key: 'general', - pageTitle: 'admin.general-settings' - }, - ncyBreadcrumb: { - label: '{"icon": "settings_applications", "label": "admin.general"}' - } - }) - .state('home.settings.outgoing-mail', { - url: '/outgoing-mail', - module: 'private', - auth: ['SYS_ADMIN'], - views: { - "content@home": { - templateUrl: outgoingMailSettingsTemplate, - controllerAs: 'vm', - controller: 'AdminController' - } - }, - data: { - key: 'mail', - pageTitle: 'admin.outgoing-mail-settings' - }, - ncyBreadcrumb: { - label: '{"icon": "mail", "label": "admin.outgoing-mail"}' - } - }) - .state('home.settings.security-settings', { - url: '/security-settings', - module: 'private', - auth: ['SYS_ADMIN'], - views: { - "content@home": { - templateUrl: securitySettingsTemplate, - controllerAs: 'vm', - controller: 'SecuritySettingsController' - } - }, - data: { - pageTitle: 'admin.security-settings' - }, - ncyBreadcrumb: { - label: '{"icon": "security", "label": "admin.security-settings"}' - } - }); -} diff --git a/ui/src/app/admin/general-settings.tpl.html b/ui/src/app/admin/general-settings.tpl.html deleted file mode 100644 index 8a97f2300a..0000000000 --- a/ui/src/app/admin/general-settings.tpl.html +++ /dev/null @@ -1,53 +0,0 @@ - -
    - - - - admin.general-settings - - - - - -
    -
    - - - -
    -
    admin.base-url-required
    -
    -
    - - {{ 'admin.prohibit-different-url' | translate }} - -
    - admin.prohibit-different-url-hint -
    -
    - {{'action.save' | translate}} -
    -
    -
    -
    -
    -
    - diff --git a/ui/src/app/admin/index.js b/ui/src/app/admin/index.js deleted file mode 100644 index d0d47247f9..0000000000 --- a/ui/src/app/admin/index.js +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import uiRouter from 'angular-ui-router'; -import ngMaterial from 'angular-material'; -import ngMessages from 'angular-messages'; -import thingsboardApiAdmin from '../api/admin.service'; -import thingsboardConfirmOnExit from '../components/confirm-on-exit.directive'; -import thingsboardToast from '../services/toast'; - -import AdminRoutes from './admin.routes'; -import AdminController from './admin.controller'; -import SecuritySettingsController from './security-settings.controller'; - -export default angular.module('thingsboard.admin', [ - uiRouter, - ngMaterial, - ngMessages, - thingsboardApiAdmin, - thingsboardConfirmOnExit, - thingsboardToast -]) - .config(AdminRoutes) - .controller('AdminController', AdminController) - .controller('SecuritySettingsController', SecuritySettingsController) - .name; diff --git a/ui/src/app/admin/outgoing-mail-settings.tpl.html b/ui/src/app/admin/outgoing-mail-settings.tpl.html deleted file mode 100644 index 5f8668bc3d..0000000000 --- a/ui/src/app/admin/outgoing-mail-settings.tpl.html +++ /dev/null @@ -1,145 +0,0 @@ - -
    - - - - admin.outgoing-mail-settings - -
    -
    -
    - - - -
    -
    - - - -
    -
    admin.mail-from-required
    -
    -
    - - - - - {{smtpProtocol.toUpperCase()}} - - - -
    - - - -
    -
    admin.smtp-host-required
    -
    -
    - - - -
    -
    admin.smtp-port-required
    -
    admin.smtp-port-invalid
    -
    admin.smtp-port-invalid
    -
    -
    -
    - - - -
    -
    admin.timeout-required
    -
    admin.timeout-invalid
    -
    admin.timeout-invalid
    -
    -
    - - {{ 'admin.enable-tls' | translate }} - - - - - - {{tlsVersion}} - - - - - {{ 'admin.enable-proxy' | translate }} - -
    -
    - - - -
    -
    admin.proxy-host-required
    -
    -
    - - - -
    -
    admin.proxy-port-required
    -
    admin.proxy-port-invalid
    -
    admin.proxy-port-invalid
    -
    -
    -
    - - - - - - - - -
    - - - - - - - - -
    - {{'admin.send-test-mail' | translate}} - {{'action.save' | translate}} -
    -
    -
    -
    -
    -
    diff --git a/ui/src/app/admin/security-settings.controller.js b/ui/src/app/admin/security-settings.controller.js deleted file mode 100644 index 40152e6497..0000000000 --- a/ui/src/app/admin/security-settings.controller.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './settings-card.scss'; - -/*@ngInject*/ -export default function SecuritySettingsController(adminService, $mdExpansionPanel) { - - var vm = this; - vm.$mdExpansionPanel = $mdExpansionPanel; - - vm.save = save; - - loadSettings(); - - function loadSettings() { - adminService.getSecuritySettings().then(function success(securitySettings) { - vm.securitySettings = securitySettings; - }); - } - - function save() { - adminService.saveSecuritySettings(vm.securitySettings).then(function success(securitySettings) { - vm.securitySettings = securitySettings; - vm.settingsForm.$setPristine(); - }); - } - -} diff --git a/ui/src/app/admin/security-settings.tpl.html b/ui/src/app/admin/security-settings.tpl.html deleted file mode 100644 index 311e7c0600..0000000000 --- a/ui/src/app/admin/security-settings.tpl.html +++ /dev/null @@ -1,167 +0,0 @@ - -
    - - - - admin.security-settings - -
    -
    -
    - - - -
    -
    - - - -
    admin.general-policy
    - -
    - - -
    admin.general-policy
    - -
    - - - - -
    -
    admin.minimum-max-failed-login-attempts-range
    -
    -
    - - - - -
    -
    -
    - - -
    admin.password-policy
    - -
    - - -
    admin.password-policy
    - -
    - - - - -
    -
    admin.minimum-password-length-required
    -
    admin.minimum-password-length-range
    -
    admin.minimum-password-length-range
    -
    -
    - - - -
    -
    admin.minimum-uppercase-letters-range
    -
    -
    - - - -
    -
    admin.minimum-lowercase-letters-range
    -
    -
    - - - -
    -
    admin.minimum-digits-range
    -
    -
    - - - -
    -
    admin.minimum-special-characters-range
    -
    -
    - - - -
    -
    admin.password-expiration-period-days-range
    -
    -
    - - - -
    -
    admin.password-reuse-frequency-days-range
    -
    -
    -
    -
    -
    -
    -
    - {{'action.save' | translate}} -
    -
    -
    -
    -
    -
    diff --git a/ui/src/app/admin/settings-card.scss b/ui/src/app/admin/settings-card.scss deleted file mode 100644 index 4639207096..0000000000 --- a/ui/src/app/admin/settings-card.scss +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "../../scss/constants"; - -md-card.settings-card { - @media (min-width: $layout-breakpoint-sm) { - width: 60%; - } - - md-icon.md-expansion-panel-icon { - margin-right: 0; - } -} diff --git a/ui/src/app/alarm/alarm-details-dialog.controller.js b/ui/src/app/alarm/alarm-details-dialog.controller.js deleted file mode 100644 index 412c4218b9..0000000000 --- a/ui/src/app/alarm/alarm-details-dialog.controller.js +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import 'brace/ext/language_tools'; -import 'brace/ext/searchbox'; -import 'brace/mode/json'; -import 'brace/theme/github'; -import beautify from 'js-beautify'; - -import './alarm-details-dialog.scss'; - -const js_beautify = beautify.js; - -/*@ngInject*/ -export default function AlarmDetailsDialogController($mdDialog, $filter, $translate, types, - alarmService, alarmId, allowAcknowledgment, allowClear, displayDetails, showingCallback) { - - var vm = this; - - vm.alarmId = alarmId; - vm.allowAcknowledgment = allowAcknowledgment; - vm.allowClear = allowClear; - vm.displayDetails = displayDetails; - vm.types = types; - vm.alarm = null; - - vm.alarmUpdated = false; - - showingCallback.onShowing = function(scope, element) { - if (vm.displayDetails) { - updateEditorSize(element); - } - } - - vm.alarmDetailsOptions = { - useWrapMode: false, - mode: 'json', - showGutter: false, - showPrintMargin: false, - theme: 'github', - advanced: { - enableSnippets: false, - enableBasicAutocompletion: false, - enableLiveAutocompletion: false - }, - onLoad: function (_ace) { - vm.editor = _ace; - } - }; - - vm.close = close; - vm.acknowledge = acknowledge; - vm.clear = clear; - - loadAlarm(); - - function updateEditorSize(element) { - var newWidth = 600; - var newHeight = 200; - angular.element('#tb-alarm-details', element).height(newHeight.toString() + "px") - .width(newWidth.toString() + "px"); - vm.editor.resize(); - } - - function loadAlarm() { - alarmService.getAlarmInfo(vm.alarmId).then( - function success(alarm) { - vm.alarm = alarm; - loadAlarmFields(); - }, - function fail() { - vm.alarm = null; - } - ); - } - - function loadAlarmFields() { - vm.createdTime = $filter('date')(vm.alarm.createdTime, 'yyyy-MM-dd HH:mm:ss'); - vm.startTime = null; - if (vm.alarm.startTs) { - vm.startTime = $filter('date')(vm.alarm.startTs, 'yyyy-MM-dd HH:mm:ss'); - } - vm.endTime = null; - if (vm.alarm.endTs) { - vm.endTime = $filter('date')(vm.alarm.endTs, 'yyyy-MM-dd HH:mm:ss'); - } - vm.ackTime = null; - if (vm.alarm.ackTs) { - vm.ackTime = $filter('date')(vm.alarm.ackTs, 'yyyy-MM-dd HH:mm:ss') - } - vm.clearTime = null; - if (vm.alarm.clearTs) { - vm.clearTime = $filter('date')(vm.alarm.clearTs, 'yyyy-MM-dd HH:mm:ss'); - } - - vm.alarmSeverity = $translate.instant(types.alarmSeverity[vm.alarm.severity].name); - - vm.alarmStatus = $translate.instant('alarm.display-status.' + vm.alarm.status); - - vm.alarmDetails = null; - if (vm.alarm.details) { - vm.alarmDetails = angular.toJson(vm.alarm.details); - vm.alarmDetails = js_beautify(vm.alarmDetails, {indent_size: 4}); - } - } - - function acknowledge () { - alarmService.ackAlarm(vm.alarmId).then( - function success() { - vm.alarmUpdated = true; - loadAlarm(); - } - ); - } - - function clear () { - alarmService.clearAlarm(vm.alarmId).then( - function success() { - vm.alarmUpdated = true; - loadAlarm(); - } - ); - } - - function close () { - $mdDialog.hide(vm.alarmUpdated ? vm.alarm : null); - } - -} diff --git a/ui/src/app/alarm/alarm-details-dialog.scss b/ui/src/app/alarm/alarm-details-dialog.scss deleted file mode 100644 index a15e51044a..0000000000 --- a/ui/src/app/alarm/alarm-details-dialog.scss +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-alarm-details-panel { - height: 100%; - margin-left: 15px; - border: 1px solid #c0c0c0; - - #tb-alarm-details { - width: 100%; - min-width: 600px; - height: 100%; - min-height: 200px; - } -} diff --git a/ui/src/app/alarm/alarm-details-dialog.tpl.html b/ui/src/app/alarm/alarm-details-dialog.tpl.html deleted file mode 100644 index f5e7ec1a0b..0000000000 --- a/ui/src/app/alarm/alarm-details-dialog.tpl.html +++ /dev/null @@ -1,107 +0,0 @@ - - - -
    -

    alarm.alarm-details

    - - - - -
    -
    - -
    -
    - - - - - - - - -
    -
    - - - - - - - - - -
    -
    - - - - - - - - - -
    -
    - - - - - - - - - - - - -
    -
    alarm.details
    -
    -
    -
    -
    -
    -
    - - {{ 'alarm.acknowledge' | - translate }} - - {{ 'alarm.clear' | - translate }} - - - {{ 'action.close' | - translate }} - - -
    diff --git a/ui/src/app/alarm/alarm-header.directive.js b/ui/src/app/alarm/alarm-header.directive.js deleted file mode 100644 index 08e61bc741..0000000000 --- a/ui/src/app/alarm/alarm-header.directive.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import alarmHeaderTemplate from './alarm-header.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function AlarmHeaderDirective($compile, $templateCache) { - - var linker = function (scope, element) { - - var template = $templateCache.get(alarmHeaderTemplate); - element.html(template); - $compile(element.contents())(scope); - - } - - return { - restrict: "A", - replace: false, - link: linker, - scope: false - }; -} diff --git a/ui/src/app/alarm/alarm-header.tpl.html b/ui/src/app/alarm/alarm-header.tpl.html deleted file mode 100644 index 06fa429912..0000000000 --- a/ui/src/app/alarm/alarm-header.tpl.html +++ /dev/null @@ -1,23 +0,0 @@ - -
    alarm.created-time
    -
    alarm.originator
    -
    alarm.type
    -
    alarm.severity
    -
    alarm.status
    -
    alarm.details
    diff --git a/ui/src/app/alarm/alarm-row.directive.js b/ui/src/app/alarm/alarm-row.directive.js deleted file mode 100644 index 63afb57afe..0000000000 --- a/ui/src/app/alarm/alarm-row.directive.js +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import alarmDetailsDialogTemplate from './alarm-details-dialog.tpl.html'; - -import alarmRowTemplate from './alarm-row.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function AlarmRowDirective($compile, $templateCache, types, $mdDialog, $document) { - - var linker = function (scope, element, attrs) { - - var template = $templateCache.get(alarmRowTemplate); - element.html(template); - - scope.alarm = attrs.alarm; - scope.types = types; - - scope.showAlarmDetails = function($event) { - var onShowingCallback = { - onShowing: function(){} - } - $mdDialog.show({ - controller: 'AlarmDetailsDialogController', - controllerAs: 'vm', - templateUrl: alarmDetailsDialogTemplate, - locals: { - alarmId: scope.alarm.id.id, - allowAcknowledgment: true, - allowClear: true, - displayDetails: true, - showingCallback: onShowingCallback - }, - parent: angular.element($document[0].body), - targetEvent: $event, - fullscreen: true, - multiple: true, - onShowing: function(scope, element) { - onShowingCallback.onShowing(scope, element); - } - }).then(function (alarm) { - if (alarm) { - scope.alarm = alarm; - } - }); - } - - $compile(element.contents())(scope); - } - - return { - restrict: "A", - replace: false, - link: linker, - scope: false - }; -} diff --git a/ui/src/app/alarm/alarm-row.tpl.html b/ui/src/app/alarm/alarm-row.tpl.html deleted file mode 100644 index d71d4a7494..0000000000 --- a/ui/src/app/alarm/alarm-row.tpl.html +++ /dev/null @@ -1,37 +0,0 @@ - -
    {{alarm.createdTime | date : 'yyyy-MM-dd HH:mm:ss'}}
    -
    {{alarm.originatorName}}
    -
    {{alarm.type}}
    -
    - {{ alarm ? (types.alarmSeverity[alarm.severity].name | translate) : '' }} -
    -
    {{ alarm ? (('alarm.display-status.' + alarm.status) | translate) : '' }}
    -
    - - - {{ 'alarm.details' | translate }} - - - more_horiz - - -
    diff --git a/ui/src/app/alarm/alarm-table.directive.js b/ui/src/app/alarm/alarm-table.directive.js deleted file mode 100644 index 28b15392a9..0000000000 --- a/ui/src/app/alarm/alarm-table.directive.js +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './alarm.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import alarmTableTemplate from './alarm-table.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function AlarmTableDirective($compile, $templateCache, $rootScope, types, alarmService) { - - var linker = function (scope, element) { - - var template = $templateCache.get(alarmTableTemplate); - - element.html(template); - - scope.types = types; - - scope.alarmSearchStatus = types.alarmSearchStatus.any; - - var pageSize = 20; - var startTime = 0; - var endTime = 0; - - scope.timewindow = { - history: { - timewindowMs: 24 * 60 * 60 * 1000 // 1 day - } - }; - - scope.topIndex = 0; - - scope.theAlarms = { - getItemAtIndex: function (index) { - if (index > scope.alarms.data.length) { - scope.theAlarms.fetchMoreItems_(index); - return null; - } - var item = scope.alarms.data[index]; - if (item) { - item.indexNumber = index + 1; - } - return item; - }, - - getLength: function () { - if (scope.alarms.hasNext) { - return scope.alarms.data.length + scope.alarms.nextPageLink.limit; - } else { - return scope.alarms.data.length; - } - }, - - fetchMoreItems_: function () { - if (scope.alarms.hasNext && !scope.alarms.pending) { - if (scope.entityType && scope.entityId && scope.alarmSearchStatus) { - var promise = alarmService.getAlarms(scope.entityType, scope.entityId, - scope.alarms.nextPageLink, scope.alarmSearchStatus, null, true, false); - if (promise) { - scope.alarms.pending = true; - promise.then( - function success(alarms) { - scope.alarms.data = scope.alarms.data.concat(alarms.data); - scope.alarms.nextPageLink = alarms.nextPageLink; - scope.alarms.hasNext = alarms.hasNext; - if (scope.alarms.hasNext) { - scope.alarms.nextPageLink.limit = pageSize; - } - scope.alarms.pending = false; - }, - function fail() { - scope.alarms.hasNext = false; - scope.alarms.pending = false; - }); - } else { - scope.alarms.hasNext = false; - } - } else { - scope.alarms.hasNext = false; - } - } - } - }; - - scope.reload = reload; - - scope.$watch("entityId", function(newVal, prevVal) { - if (newVal && !angular.equals(newVal, prevVal)) { - resetFilter(); - reload(); - } - }); - - - - function destroyWatchers() { - if (scope.alarmSearchStatusWatchHandle) { - scope.alarmSearchStatusWatchHandle(); - scope.alarmSearchStatusWatchHandle = null; - } - if (scope.timewindowWatchHandle) { - scope.timewindowWatchHandle(); - scope.timewindowWatchHandle = null; - } - } - - function initWatchers() { - scope.alarmSearchStatusWatchHandle = scope.$watch("alarmSearchStatus", function(newVal, prevVal) { - if (newVal && !angular.equals(newVal, prevVal)) { - reload(); - } - }); - scope.timewindowWatchHandle = scope.$watch("timewindow", function(newVal, prevVal) { - if (newVal && !angular.equals(newVal, prevVal)) { - reload(); - } - }, true); - } - - function resetFilter() { - destroyWatchers(); - scope.timewindow = { - history: { - timewindowMs: 24 * 60 * 60 * 1000 // 1 day - } - }; - scope.alarmSearchStatus = types.alarmSearchStatus.any; - initWatchers(); - } - - function updateTimeWindowRange () { - if (scope.timewindow.history.timewindowMs) { - var currentTime = (new Date).getTime(); - startTime = currentTime - scope.timewindow.history.timewindowMs; - endTime = currentTime; - } else { - startTime = scope.timewindow.history.fixedTimewindow.startTimeMs; - endTime = scope.timewindow.history.fixedTimewindow.endTimeMs; - } - } - - function reload () { - scope.topIndex = 0; - scope.selected = []; - updateTimeWindowRange(); - scope.alarms = { - data: [], - nextPageLink: { - limit: pageSize, - startTime: startTime, - endTime: endTime - }, - hasNext: true, - pending: false - }; - scope.theAlarms.getItemAtIndex(pageSize); - } - - scope.noData = function() { - return scope.alarms.data.length == 0 && !scope.alarms.hasNext; - } - - scope.hasData = function() { - return scope.alarms.data.length > 0; - } - - scope.loading = function() { - return $rootScope.loading; - } - - scope.hasScroll = function() { - var repeatContainer = scope.repeatContainer[0]; - if (repeatContainer) { - var scrollElement = repeatContainer.children[0]; - if (scrollElement) { - return scrollElement.scrollHeight > scrollElement.clientHeight; - } - } - return false; - } - - reload(); - - initWatchers(); - - $compile(element.contents())(scope); - } - - return { - restrict: "E", - link: linker, - scope: { - entityType: '=', - entityId: '=' - } - }; -} diff --git a/ui/src/app/alarm/alarm-table.tpl.html b/ui/src/app/alarm/alarm-table.tpl.html deleted file mode 100644 index 38719cfe5c..0000000000 --- a/ui/src/app/alarm/alarm-table.tpl.html +++ /dev/null @@ -1,56 +0,0 @@ - - -
    - - - - - {{ ('alarm.search-status.' + searchStatus) | translate }} - - - - - - refresh - - {{ 'action.refresh' | translate }} - - -
    -
    - - - - - - alarm.no-alarms-prompt - - - - - - - - -
    -
    diff --git a/ui/src/app/alarm/alarm.scss b/ui/src/app/alarm/alarm.scss deleted file mode 100644 index 893327604d..0000000000 --- a/ui/src/app/alarm/alarm.scss +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-alarm-container { - overflow-x: auto; -} - -md-list.tb-alarm-table { - min-width: 700px; - padding: 0; - - md-list-item { - padding: 0; - } - - .tb-row { - height: 48px; - padding: 0; - overflow: hidden; - } - - .tb-row:hover { - background-color: #eee; - } - - .tb-header:hover { - background: none; - } - - .tb-header { - .tb-cell { - font-size: 12px; - font-weight: 700; - color: rgba(0, 0, 0, .54); - white-space: nowrap; - background: none; - } - } - - .tb-cell { - padding: 0 24px; - margin: auto 0; - overflow: hidden; - font-size: 13px; - color: rgba(0, 0, 0, .87); - text-align: left; - vertical-align: middle; - - .md-button { - padding: 0; - margin: 0; - } - } - - .tb-cell.tb-number { - text-align: right; - } -} - -#tb-alarm-content { - width: 100%; - min-width: 400px; - height: 100%; - min-height: 50px; -} diff --git a/ui/src/app/alarm/index.js b/ui/src/app/alarm/index.js deleted file mode 100644 index 221f2a6e73..0000000000 --- a/ui/src/app/alarm/index.js +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import AlarmDetailsDialogController from './alarm-details-dialog.controller'; -import AlarmHeaderDirective from './alarm-header.directive'; -import AlarmRowDirective from './alarm-row.directive'; -import AlarmTableDirective from './alarm-table.directive'; - -export default angular.module('thingsboard.alarm', []) - .controller('AlarmDetailsDialogController', AlarmDetailsDialogController) - .directive('tbAlarmHeader', AlarmHeaderDirective) - .directive('tbAlarmRow', AlarmRowDirective) - .directive('tbAlarmTable', AlarmTableDirective) - .name; diff --git a/ui/src/app/api/admin.service.js b/ui/src/app/api/admin.service.js deleted file mode 100644 index c679fc6532..0000000000 --- a/ui/src/app/api/admin.service.js +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default angular.module('thingsboard.api.admin', []) - .factory('adminService', AdminService) - .name; - -/*@ngInject*/ -function AdminService($http, $q) { - - var service = { - getAdminSettings: getAdminSettings, - saveAdminSettings: saveAdminSettings, - getSecuritySettings: getSecuritySettings, - saveSecuritySettings: saveSecuritySettings, - sendTestMail: sendTestMail, - checkUpdates: checkUpdates - } - - return service; - - function getAdminSettings(key) { - var deferred = $q.defer(); - var url = '/api/admin/settings/' + key; - $http.get(url, null).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function saveAdminSettings(settings) { - var deferred = $q.defer(); - var url = '/api/admin/settings'; - $http.post(url, settings).then(function success(response) { - deferred.resolve(response.data); - }, function fail(response) { - deferred.reject(response.data); - }); - return deferred.promise; - } - - function getSecuritySettings() { - var deferred = $q.defer(); - var url = '/api/admin/securitySettings'; - $http.get(url, null).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function saveSecuritySettings(securitySettings) { - var deferred = $q.defer(); - var url = '/api/admin/securitySettings'; - $http.post(url, securitySettings).then(function success(response) { - deferred.resolve(response.data); - }, function fail(response) { - deferred.reject(response.data); - }); - return deferred.promise; - } - - function sendTestMail(settings) { - var deferred = $q.defer(); - var url = '/api/admin/settings/testMail'; - $http.post(url, settings).then(function success() { - deferred.resolve(); - }, function fail(response) { - deferred.reject(response.data); - }); - return deferred.promise; - } - - function checkUpdates() { - var deferred = $q.defer(); - var url = '/api/admin/updates'; - $http.get(url, null).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } -} diff --git a/ui/src/app/api/alarm.service.js b/ui/src/app/api/alarm.service.js deleted file mode 100644 index f7f4111ec9..0000000000 --- a/ui/src/app/api/alarm.service.js +++ /dev/null @@ -1,356 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default angular.module('thingsboard.api.alarm', []) - .factory('alarmService', AlarmService) - .name; - -/*@ngInject*/ -function AlarmService($http, $q, $interval, $filter, $timeout, utils, types) { - - var alarmSourceListeners = {}; - - var simulatedAlarm = { - createdTime: (new Date).getTime(), - startTs: (new Date).getTime(), - endTs: 0, - ackTs: 0, - clearTs: 0, - originatorName: 'Simulated', - originator: { - entityType: "DEVICE", - id: "1" - }, - type: 'TEMPERATURE', - severity: "MAJOR", - status: types.alarmStatus.activeUnack, - details: { - message: "Temperature is high!" - } - }; - - var service = { - getAlarm: getAlarm, - getAlarmInfo: getAlarmInfo, - saveAlarm: saveAlarm, - ackAlarm: ackAlarm, - clearAlarm: clearAlarm, - deleteAlarm: deleteAlarm, - getAlarms: getAlarms, - getHighestAlarmSeverity: getHighestAlarmSeverity, - pollAlarms: pollAlarms, - cancelPollAlarms: cancelPollAlarms, - subscribeForAlarms: subscribeForAlarms, - unsubscribeFromAlarms: unsubscribeFromAlarms - } - - return service; - - function getAlarm(alarmId, ignoreErrors, config) { - var deferred = $q.defer(); - var url = '/api/alarm/' + alarmId; - if (!config) { - config = {}; - } - config = Object.assign(config, { ignoreErrors: ignoreErrors }); - $http.get(url, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getAlarmInfo(alarmId, ignoreErrors, config) { - var deferred = $q.defer(); - var url = '/api/alarm/info/' + alarmId; - if (!config) { - config = {}; - } - config = Object.assign(config, { ignoreErrors: ignoreErrors }); - $http.get(url, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function saveAlarm(alarm, ignoreErrors, config) { - var deferred = $q.defer(); - var url = '/api/alarm'; - if (!config) { - config = {}; - } - config = Object.assign(config, { ignoreErrors: ignoreErrors }); - $http.post(url, alarm, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function ackAlarm(alarmId, ignoreErrors, config) { - var deferred = $q.defer(); - var url = '/api/alarm/' + alarmId + '/ack'; - if (!config) { - config = {}; - } - config = Object.assign(config, { ignoreErrors: ignoreErrors }); - $http.post(url, null, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function clearAlarm(alarmId, ignoreErrors, config) { - var deferred = $q.defer(); - var url = '/api/alarm/' + alarmId + '/clear'; - if (!config) { - config = {}; - } - config = Object.assign(config, { ignoreErrors: ignoreErrors }); - $http.post(url, null, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function deleteAlarm(alarmId, ignoreErrors, config) { - var deferred = $q.defer(); - var url = '/api/alarm/' + alarmId; - if (!config) { - config = {}; - } - config = Object.assign(config, { ignoreErrors: ignoreErrors }); - $http.delete(url, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getAlarms(entityType, entityId, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator, ascOrder, config) { - var deferred = $q.defer(); - var url = '/api/alarm/' + entityType + '/' + entityId + '?limit=' + pageLink.limit; - - if (angular.isDefined(pageLink.startTime) && pageLink.startTime != null) { - url += '&startTime=' + pageLink.startTime; - } - if (angular.isDefined(pageLink.endTime) && pageLink.endTime != null) { - url += '&endTime=' + pageLink.endTime; - } - if (angular.isDefined(pageLink.idOffset) && pageLink.idOffset != null) { - url += '&offset=' + pageLink.idOffset; - } - if (alarmSearchStatus) { - url += '&searchStatus=' + alarmSearchStatus; - } - if (alarmStatus) { - url += '&status=' + alarmStatus; - } - if (fetchOriginator) { - url += '&fetchOriginator=' + ((fetchOriginator===true) ? 'true' : 'false'); - } - if (angular.isDefined(ascOrder) && ascOrder != null) { - url += '&ascOrder=' + (ascOrder ? 'true' : 'false'); - } - - $http.get(url, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getHighestAlarmSeverity(entityType, entityId, alarmSearchStatus, alarmStatus, config) { - var deferred = $q.defer(); - var url = '/api/alarm/highestSeverity/' + entityType + '/' + entityId; - - if (alarmSearchStatus) { - url += '?searchStatus=' + alarmSearchStatus; - } else if (alarmStatus) { - url += '?status=' + alarmStatus; - } - $http.get(url, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function fetchAlarms(alarmsQuery, pageLink, deferred, leftToLoad, alarmsList) { - getAlarms(alarmsQuery.entityType, alarmsQuery.entityId, - pageLink, alarmsQuery.alarmSearchStatus, alarmsQuery.alarmStatus, - alarmsQuery.fetchOriginator, false, {ignoreLoading: true}).then( - function success(alarms) { - if (!alarmsList) { - alarmsList = []; - } - alarmsList = alarmsList.concat(alarms.data); - if (angular.isDefined(leftToLoad)) { - leftToLoad -= pageLink.limit; - if (leftToLoad === 0) { - alarmsList = $filter('orderBy')(alarmsList, ['-createdTime']); - deferred.resolve(alarmsList); - return; - } - if (leftToLoad < pageLink.limit) { - alarms.nextPageLink.limit = leftToLoad; - } - } - if (alarms.hasNext && !alarmsQuery.limit) { - fetchAlarms(alarmsQuery, alarms.nextPageLink, deferred, leftToLoad, alarmsList); - } else { - alarmsList = $filter('orderBy')(alarmsList, ['-createdTime']); - deferred.resolve(alarmsList); - } - }, - function fail() { - deferred.reject(); - } - ); - } - - function getAlarmsByQuery(alarmsQuery) { - var deferred = $q.defer(); - var time = Date.now(); - var pageLink; - var leftToLoad; - if (alarmsQuery.limit) { - pageLink = { - limit: alarmsQuery.limit - }; - } else if (alarmsQuery.interval) { - pageLink = { - limit: alarmsQuery.alarmsFetchSize || 100, - startTime: time - alarmsQuery.interval - }; - } else if (alarmsQuery.startTime) { - pageLink = { - limit: alarmsQuery.alarmsFetchSize || 100, - startTime: Math.round(alarmsQuery.startTime) - }; - if (alarmsQuery.endTime) { - pageLink.endTime = Math.round(alarmsQuery.endTime); - } - } - - if (angular.isDefined(alarmsQuery.alarmsMaxCountLoad) && alarmsQuery.alarmsMaxCountLoad !== 0) { - leftToLoad = alarmsQuery.alarmsMaxCountLoad; - if (leftToLoad < pageLink.limit) { - pageLink.limit = leftToLoad; - } - } - - fetchAlarms(alarmsQuery, pageLink, deferred, leftToLoad); - return deferred.promise; - } - - function onPollAlarms(alarmsQuery) { - getAlarmsByQuery(alarmsQuery).then( - function success(alarms) { - alarmsQuery.onAlarms(alarms); - }, - function fail() {} - ); - } - - function pollAlarms(entityType, entityId, alarmStatus, interval, limit, pollingInterval, onAlarms) { - var alarmsQuery = { - entityType: entityType, - entityId: entityId, - alarmSearchStatus: null, - alarmStatus: alarmStatus, - fetchOriginator: false, - interval: interval, - limit: limit, - onAlarms: onAlarms - }; - onPollAlarms(alarmsQuery); - return $interval(onPollAlarms, pollingInterval, 0, false, alarmsQuery); - } - - function cancelPollAlarms(pollPromise) { - if (angular.isDefined(pollPromise)) { - $interval.cancel(pollPromise); - } - } - - function subscribeForAlarms(alarmSourceListener) { - alarmSourceListener.id = utils.guid(); - alarmSourceListeners[alarmSourceListener.id] = alarmSourceListener; - var alarmSource = alarmSourceListener.alarmSource; - if (alarmSource.type == types.datasourceType.function) { - $timeout(function() { - alarmSourceListener.alarmsUpdated([simulatedAlarm], false); - }); - } else if (alarmSource.entityType && alarmSource.entityId) { - var pollingInterval = alarmSourceListener.alarmsPollingInterval; - alarmSourceListener.alarmsQuery = { - entityType: alarmSource.entityType, - entityId: alarmSource.entityId, - alarmSearchStatus: alarmSourceListener.alarmSearchStatus, - alarmStatus: null, - alarmsMaxCountLoad: alarmSourceListener.alarmsMaxCountLoad, - alarmsFetchSize: alarmSourceListener.alarmsFetchSize - }; - var originatorKeys = $filter('filter')(alarmSource.dataKeys, {name: 'originator'}); - if (originatorKeys && originatorKeys.length) { - alarmSourceListener.alarmsQuery.fetchOriginator = true; - } - var subscriptionTimewindow = alarmSourceListener.subscriptionTimewindow; - if (subscriptionTimewindow.realtimeWindowMs) { - alarmSourceListener.alarmsQuery.startTime = subscriptionTimewindow.startTs; - } else { - alarmSourceListener.alarmsQuery.startTime = subscriptionTimewindow.fixedWindow.startTimeMs; - alarmSourceListener.alarmsQuery.endTime = subscriptionTimewindow.fixedWindow.endTimeMs; - } - alarmSourceListener.alarmsQuery.onAlarms = function(alarms) { - if (subscriptionTimewindow.realtimeWindowMs) { - var now = Date.now(); - if (alarmSourceListener.lastUpdateTs) { - var interval = now - alarmSourceListener.lastUpdateTs; - alarmSourceListener.alarmsQuery.startTime += interval; - } - alarmSourceListener.lastUpdateTs = now; - } - alarmSourceListener.alarmsUpdated(alarms, false); - } - onPollAlarms(alarmSourceListener.alarmsQuery); - alarmSourceListener.pollPromise = $interval(onPollAlarms, pollingInterval, - 0, false, alarmSourceListener.alarmsQuery); - } - - } - - function unsubscribeFromAlarms(alarmSourceListener) { - if (alarmSourceListener && alarmSourceListener.id) { - if (alarmSourceListener.pollPromise) { - $interval.cancel(alarmSourceListener.pollPromise); - alarmSourceListener.pollPromise = null; - } - delete alarmSourceListeners[alarmSourceListener.id]; - } - } -} diff --git a/ui/src/app/api/alias-controller.js b/ui/src/app/api/alias-controller.js deleted file mode 100644 index 3f570a904f..0000000000 --- a/ui/src/app/api/alias-controller.js +++ /dev/null @@ -1,322 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default class AliasController { - - constructor($scope, $q, $filter, utils, types, entityService, stateController, entityAliases) { - this.$scope = $scope; - this.$q = $q; - this.$filter = $filter; - this.utils = utils; - this.types = types; - this.entityService = entityService; - this.stateController = stateController; - this.entityAliases = angular.copy(entityAliases); - this.resolvedAliases = {}; - this.resolvedAliasesPromise = {}; - this.resolvedAliasesToStateEntities = {}; - } - - updateEntityAliases(newEntityAliases) { - var changedAliasIds = []; - for (var aliasId in newEntityAliases) { - var newEntityAlias = newEntityAliases[aliasId]; - var prevEntityAlias = this.entityAliases[aliasId]; - if (!angular.equals(newEntityAlias, prevEntityAlias)) { - changedAliasIds.push(aliasId); - this.setAliasUnresolved(aliasId); - } - } - for (aliasId in this.entityAliases) { - if (!newEntityAliases[aliasId]) { - changedAliasIds.push(aliasId); - this.setAliasUnresolved(aliasId); - } - } - this.entityAliases = angular.copy(newEntityAliases); - if (changedAliasIds.length) { - this.$scope.$broadcast('entityAliasesChanged', changedAliasIds); - } - } - - dashboardStateChanged() { - var changedAliasIds = []; - for (var aliasId in this.resolvedAliasesToStateEntities) { - var stateEntityInfo = this.resolvedAliasesToStateEntities[aliasId]; - var newEntityId = this.stateController.getEntityId(stateEntityInfo.entityParamName); - var prevEntityId = stateEntityInfo.entityId; - if (!angular.equals(newEntityId, prevEntityId)) { - changedAliasIds.push(aliasId); - this.setAliasUnresolved(aliasId); - } - } - if (changedAliasIds.length) { - this.$scope.$broadcast('entityAliasesChanged', changedAliasIds); - } - } - - setAliasUnresolved(aliasId) { - delete this.resolvedAliases[aliasId]; - delete this.resolvedAliasesPromise[aliasId]; - delete this.resolvedAliasesToStateEntities[aliasId]; - } - - getEntityAliases() { - return this.entityAliases; - } - - getEntityAliasId(aliasName) { - for (var aliasId in this.entityAliases) { - var alias = this.entityAliases[aliasId]; - if (alias.alias == aliasName) { - return aliasId; - } - } - return null; - } - - getAliasInfo(aliasId) { - var deferred = this.$q.defer(); - var aliasInfo = this.resolvedAliases[aliasId]; - if (aliasInfo) { - deferred.resolve(aliasInfo); - return deferred.promise; - } else if (this.resolvedAliasesPromise[aliasId]) { - return this.resolvedAliasesPromise[aliasId]; - } else { - this.resolvedAliasesPromise[aliasId] = deferred.promise; - var aliasCtrl = this; - var entityAlias = this.entityAliases[aliasId]; - if (entityAlias) { - this.entityService.resolveAlias(entityAlias, this.stateController.getStateParams()).then( - function success(aliasInfo) { - aliasCtrl.resolvedAliases[aliasId] = aliasInfo; - delete aliasCtrl.resolvedAliasesPromise[aliasId]; - if (aliasInfo.stateEntity) { - var stateEntityInfo = { - entityParamName: aliasInfo.entityParamName, - entityId: aliasCtrl.stateController.getEntityId(aliasInfo.entityParamName) - }; - aliasCtrl.resolvedAliasesToStateEntities[aliasId] = - stateEntityInfo; - } - aliasCtrl.$scope.$broadcast('entityAliasResolved', aliasId); - deferred.resolve(aliasInfo); - }, - function fail() { - deferred.reject(); - delete aliasCtrl.resolvedAliasesPromise[aliasId]; - } - ); - } else { - deferred.reject(); - delete aliasCtrl.resolvedAliasesPromise[aliasId]; - } - return this.resolvedAliasesPromise[aliasId]; - } - } - - resolveDatasource(datasource, isSingle) { - var deferred = this.$q.defer(); - if (datasource.type === this.types.datasourceType.entity) { - if (datasource.entityAliasId) { - this.getAliasInfo(datasource.entityAliasId).then( - function success(aliasInfo) { - datasource.aliasName = aliasInfo.alias; - if (aliasInfo.resolveMultiple && !isSingle) { - var newDatasource; - var resolvedEntities = aliasInfo.resolvedEntities; - if (resolvedEntities && resolvedEntities.length) { - var datasources = []; - for (var i=0;i 0 ? true : false; - datasources.push(newDatasource); - } - deferred.resolve(datasources); - } else { - if (aliasInfo.stateEntity) { - newDatasource = angular.copy(datasource); - newDatasource.unresolvedStateEntity = true; - deferred.resolve([newDatasource]); - } else { - deferred.reject(); - } - } - } else { - var entity = aliasInfo.currentEntity; - if (entity) { - if (entity.origEntity) { - datasource.entity = angular.copy(entity.origEntity); - } else { - datasource.entity = {}; - } - datasource.entityId = entity.id; - datasource.entityType = entity.entityType; - datasource.entityName = entity.name; - datasource.entityLabel = entity.label; - datasource.name = entity.name; - datasource.entityDescription = entity.entityDescription; - deferred.resolve([datasource]); - } else { - if (aliasInfo.stateEntity) { - datasource.unresolvedStateEntity = true; - deferred.resolve([datasource]); - } else { - deferred.reject(); - } - } - } - }, - function fail() { - deferred.reject(); - } - ); - } else { // entityId - datasource.aliasName = datasource.entityName; - datasource.name = datasource.entityName; - deferred.resolve([datasource]); - } - } else { // function - deferred.resolve([datasource]); - } - return deferred.promise; - } - - resolveAlarmSource(alarmSource) { - var deferred = this.$q.defer(); - var aliasCtrl = this; - this.resolveDatasource(alarmSource, true).then( - function success(datasources) { - var datasource = datasources[0]; - if (datasource.type === aliasCtrl.types.datasourceType.function) { - var name; - if (datasource.name && datasource.name.length) { - name = datasource.name; - } else { - name = aliasCtrl.types.datasourceType.function; - } - datasource.name = name; - datasource.aliasName = name; - datasource.entityName = name; - } else if (datasource.unresolvedStateEntity) { - datasource.name = "Unresolved"; - datasource.entityName = "Unresolved"; - } - deferred.resolve(datasource); - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - - resolveDatasources(datasources) { - - var aliasCtrl = this; - - function updateDataKeyLabel(dataKey, datasource) { - if (!dataKey.pattern) { - dataKey.pattern = angular.copy(dataKey.label); - } - dataKey.label = aliasCtrl.utils.createLabelFromDatasource(datasource, dataKey.pattern); - } - - function updateDatasourceKeyLabels(datasource) { - for (var dk = 0; dk < datasource.dataKeys.length; dk++) { - updateDataKeyLabel(datasource.dataKeys[dk], datasource); - } - } - - var deferred = this.$q.defer(); - var newDatasources = angular.copy(datasources); - var datasorceResolveTasks = []; - - newDatasources.forEach(function (datasource) { - var resolveDatasourceTask = aliasCtrl.resolveDatasource(datasource); - datasorceResolveTasks.push(resolveDatasourceTask); - }); - this.$q.all(datasorceResolveTasks).then( - function success(datasourcesArrays) { - var datasources = [].concat.apply([], datasourcesArrays); - datasources = aliasCtrl.$filter('orderBy')(datasources, '+generated'); - var index = 0; - var functionIndex = 0; - datasources.forEach(function(datasource) { - if (datasource.type === aliasCtrl.types.datasourceType.function) { - var name; - if (datasource.name && datasource.name.length) { - name = datasource.name; - } else { - functionIndex++; - name = aliasCtrl.types.datasourceType.function; - if (functionIndex > 1) { - name += ' ' + functionIndex; - } - } - datasource.name = name; - datasource.aliasName = name; - datasource.entityName = name; - } else if (datasource.unresolvedStateEntity) { - datasource.name = "Unresolved"; - datasource.entityName = "Unresolved"; - } - datasource.dataKeys.forEach(function(dataKey) { - if (datasource.generated) { - dataKey._hash = Math.random(); - dataKey.color = aliasCtrl.utils.getMaterialColor(index); - } - index++; - }); - updateDatasourceKeyLabels(datasource); - }); - deferred.resolve(datasources); - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - - getInstantAliasInfo(aliasId) { - return this.resolvedAliases[aliasId]; - } - - updateCurrentAliasEntity(aliasId, currentEntity) { - var aliasInfo = this.resolvedAliases[aliasId]; - if (aliasInfo) { - var prevCurrentEntity = aliasInfo.currentEntity; - if (!angular.equals(currentEntity, prevCurrentEntity)) { - aliasInfo.currentEntity = currentEntity; - this.$scope.$broadcast('entityAliasesChanged', [aliasId]); - } - } - } - -} \ No newline at end of file diff --git a/ui/src/app/api/asset.service.js b/ui/src/app/api/asset.service.js deleted file mode 100644 index a936e33915..0000000000 --- a/ui/src/app/api/asset.service.js +++ /dev/null @@ -1,292 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default angular.module('thingsboard.api.asset', []) - .factory('assetService', AssetService) - .name; - -/*@ngInject*/ -function AssetService($http, $q, customerService, userService) { - - var service = { - getAsset: getAsset, - getAssets: getAssets, - saveAsset: saveAsset, - deleteAsset: deleteAsset, - assignAssetToCustomer: assignAssetToCustomer, - unassignAssetFromCustomer: unassignAssetFromCustomer, - makeAssetPublic: makeAssetPublic, - getTenantAssets: getTenantAssets, - getCustomerAssets: getCustomerAssets, - findByQuery: findByQuery, - fetchAssetsByNameFilter: fetchAssetsByNameFilter, - getAssetTypes: getAssetTypes, - findByName: findByName - } - - return service; - - function getAsset(assetId, ignoreErrors, config) { - var deferred = $q.defer(); - var url = '/api/asset/' + assetId; - if (!config) { - config = {}; - } - config = Object.assign(config, { ignoreErrors: ignoreErrors }); - $http.get(url, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getAssets(assetIds, config) { - var deferred = $q.defer(); - var ids = ''; - for (var i=0;i0) { - ids += ','; - } - ids += assetIds[i]; - } - var url = '/api/assets?assetIds=' + ids; - $http.get(url, config).then(function success(response) { - var assets = response.data; - assets.sort(function (asset1, asset2) { - var id1 = asset1.id.id; - var id2 = asset2.id.id; - var index1 = assetIds.indexOf(id1); - var index2 = assetIds.indexOf(id2); - return index1 - index2; - }); - deferred.resolve(assets); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function saveAsset(asset, ignoreErrors, config) { - var deferred = $q.defer(); - var url = '/api/asset'; - if (!config) { - config = {}; - } - config = Object.assign(config, { ignoreErrors: ignoreErrors }); - $http.post(url, asset, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function deleteAsset(assetId, ignoreErrors, config) { - var deferred = $q.defer(); - var url = '/api/asset/' + assetId; - if (!config) { - config = {}; - } - config = Object.assign(config, { ignoreErrors: ignoreErrors }); - $http.delete(url, config).then(function success() { - deferred.resolve(); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function assignAssetToCustomer(customerId, assetId, ignoreErrors, config) { - var deferred = $q.defer(); - var url = '/api/customer/' + customerId + '/asset/' + assetId; - if (!config) { - config = {}; - } - config = Object.assign(config, { ignoreErrors: ignoreErrors }); - $http.post(url, null, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function unassignAssetFromCustomer(assetId, ignoreErrors, config) { - var deferred = $q.defer(); - var url = '/api/customer/asset/' + assetId; - if (!config) { - config = {}; - } - config = Object.assign(config, { ignoreErrors: ignoreErrors }); - $http.delete(url, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function makeAssetPublic(assetId, ignoreErrors, config) { - var deferred = $q.defer(); - var url = '/api/customer/public/asset/' + assetId; - if (!config) { - config = {}; - } - config = Object.assign(config, { ignoreErrors: ignoreErrors }); - $http.post(url, null, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getTenantAssets(pageLink, applyCustomersInfo, config, type) { - var deferred = $q.defer(); - var url = '/api/tenant/assets?limit=' + pageLink.limit; - if (angular.isDefined(pageLink.textSearch)) { - url += '&textSearch=' + pageLink.textSearch; - } - if (angular.isDefined(pageLink.idOffset)) { - url += '&idOffset=' + pageLink.idOffset; - } - if (angular.isDefined(pageLink.textOffset)) { - url += '&textOffset=' + pageLink.textOffset; - } - if (angular.isDefined(type) && type.length) { - url += '&type=' + type; - } - $http.get(url, config).then(function success(response) { - if (applyCustomersInfo) { - customerService.applyAssignedCustomersInfo(response.data.data).then( - function success(data) { - response.data.data = data; - deferred.resolve(response.data); - }, - function fail() { - deferred.reject(); - } - ); - } else { - deferred.resolve(response.data); - } - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getCustomerAssets(customerId, pageLink, applyCustomersInfo, config, type) { - var deferred = $q.defer(); - var url = '/api/customer/' + customerId + '/assets?limit=' + pageLink.limit; - if (angular.isDefined(pageLink.textSearch)) { - url += '&textSearch=' + pageLink.textSearch; - } - if (angular.isDefined(pageLink.idOffset)) { - url += '&idOffset=' + pageLink.idOffset; - } - if (angular.isDefined(pageLink.textOffset)) { - url += '&textOffset=' + pageLink.textOffset; - } - if (angular.isDefined(type) && type.length) { - url += '&type=' + type; - } - $http.get(url, config).then(function success(response) { - if (applyCustomersInfo) { - customerService.applyAssignedCustomerInfo(response.data.data, customerId).then( - function success(data) { - response.data.data = data; - deferred.resolve(response.data); - }, - function fail() { - deferred.reject(); - } - ); - } else { - deferred.resolve(response.data); - } - }, function fail() { - deferred.reject(); - }); - - return deferred.promise; - } - - function findByQuery(query, ignoreErrors, config) { - var deferred = $q.defer(); - var url = '/api/assets'; - if (!config) { - config = {}; - } - config = Object.assign(config, { ignoreErrors: ignoreErrors }); - $http.post(url, query, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function fetchAssetsByNameFilter(assetNameFilter, limit, applyCustomersInfo, config) { - var deferred = $q.defer(); - var user = userService.getCurrentUser(); - var promise; - var pageLink = {limit: limit, textSearch: assetNameFilter}; - if (user.authority === 'CUSTOMER_USER') { - var customerId = user.customerId; - promise = getCustomerAssets(customerId, pageLink, applyCustomersInfo, config); - } else { - promise = getTenantAssets(pageLink, applyCustomersInfo, config); - } - promise.then( - function success(result) { - if (result.data && result.data.length > 0) { - deferred.resolve(result.data); - } else { - deferred.resolve(null); - } - }, - function fail() { - deferred.resolve(null); - } - ); - return deferred.promise; - } - - function getAssetTypes(config) { - var deferred = $q.defer(); - var url = '/api/asset/types'; - $http.get(url, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function findByName(assetName, config) { - config = config || {}; - var deferred = $q.defer(); - var url = '/api/tenant/assets?assetName=' + assetName; - $http.get(url, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - -} diff --git a/ui/src/app/api/attribute.service.js b/ui/src/app/api/attribute.service.js deleted file mode 100644 index aa2286d938..0000000000 --- a/ui/src/app/api/attribute.service.js +++ /dev/null @@ -1,356 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default angular.module('thingsboard.api.attribute', []) - .factory('attributeService', AttributeService) - .name; - -/*@ngInject*/ -function AttributeService($http, $q, $filter, types, telemetryWebsocketService) { - - var entityAttributesSubscriptionMap = {}; - - var service = { - getEntityKeys: getEntityKeys, - getEntityTimeseriesValues: getEntityTimeseriesValues, - getEntityAttributesValues: getEntityAttributesValues, - getEntityAttributes: getEntityAttributes, - subscribeForEntityAttributes: subscribeForEntityAttributes, - unsubscribeForEntityAttributes: unsubscribeForEntityAttributes, - saveEntityAttributes: saveEntityAttributes, - deleteEntityAttributes: deleteEntityAttributes, - saveEntityTimeseries: saveEntityTimeseries, - deleteEntityTimeseries: deleteEntityTimeseries - } - - return service; - - function getEntityKeys(entityType, entityId, query, type, config) { - var deferred = $q.defer(); - var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/keys/'; - if (type === types.dataKeyType.timeseries) { - url += 'timeseries'; - } else if (type === types.dataKeyType.attribute) { - url += 'attributes'; - } - $http.get(url, config).then(function success(response) { - var result = []; - if (response.data) { - if (query) { - var dataKeys = response.data; - var lowercaseQuery = angular.lowercase(query); - for (var i=0; i -1) { - attribute = attributes[index]; - } else { - attribute = { - key: key - }; - index = attributes.push(attribute)-1; - keys[key] = index; - } - var attrData = data[key][0]; - attribute.lastUpdateTs = attrData[0]; - attribute.value = attrData[1]; - } - if (entityAttributesSubscription.subscriptionCallback) { - entityAttributesSubscription.subscriptionCallback(attributes); - } - } - } - - function subscribeForEntityAttributes(entityType, entityId, attributeScope) { - var subscriptionId = entityType + entityId + attributeScope; - var entityAttributesSubscription = entityAttributesSubscriptionMap[subscriptionId]; - if (!entityAttributesSubscription) { - var subscriptionCommand = { - entityType: entityType, - entityId: entityId, - scope: attributeScope - }; - - var type = attributeScope === types.latestTelemetry.value ? - types.dataKeyType.timeseries : types.dataKeyType.attribute; - - var subscriber = { - subscriptionCommands: [subscriptionCommand], - type: type, - onData: function (data) { - if (data.data) { - onSubscriptionData(data.data, subscriptionId); - } - } - }; - entityAttributesSubscription = { - subscriber: subscriber, - attributes: null - }; - entityAttributesSubscriptionMap[subscriptionId] = entityAttributesSubscription; - telemetryWebsocketService.subscribe(subscriber); - } - return subscriptionId; - } - - function unsubscribeForEntityAttributes(subscriptionId) { - var entityAttributesSubscription = entityAttributesSubscriptionMap[subscriptionId]; - if (entityAttributesSubscription) { - telemetryWebsocketService.unsubscribe(entityAttributesSubscription.subscriber); - delete entityAttributesSubscriptionMap[subscriptionId]; - } - } - - function saveEntityAttributes(entityType, entityId, attributeScope, attributes, config) { - config = config || {}; - var deferred = $q.defer(); - var attributesData = {}; - var deleteAttributes = []; - for (var a=0; a 0) { - keys += ','; - } - keys += attributes[i].key; - } - var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/' + attributeScope + '?keys=' + keys; - $http.delete(url, config).then(function success() { - deferred.resolve(); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function deleteEntityTimeseries(entityType, entityId, timeseries, config, deleteAllDataForKeys) { - config = config || {}; - deleteAllDataForKeys = deleteAllDataForKeys || false; - var deferred = $q.defer(); - var keys = ''; - for (var i = 0; i < timeseries.length; i++) { - if (i > 0) { - keys += ','; - } - keys += timeseries[i].key; - } - var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/delete' + - '?keys=' + keys + - '&deleteAllDataForKeys=' + deleteAllDataForKeys; - $http.delete(url, config).then(function success() { - deferred.resolve(); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - -} diff --git a/ui/src/app/api/audit-log.service.js b/ui/src/app/api/audit-log.service.js deleted file mode 100644 index 3cb18ca040..0000000000 --- a/ui/src/app/api/audit-log.service.js +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default angular.module('thingsboard.api.auditLog', []) - .factory('auditLogService', AuditLogService) - .name; - -/*@ngInject*/ -function AuditLogService($http, $q) { - - var service = { - getAuditLogsByEntityId: getAuditLogsByEntityId, - getAuditLogsByUserId: getAuditLogsByUserId, - getAuditLogsByCustomerId: getAuditLogsByCustomerId, - getAuditLogs: getAuditLogs - } - - return service; - - function getAuditLogsByEntityId (entityType, entityId, pageLink) { - var deferred = $q.defer(); - var url = `/api/audit/logs/entity/${entityType}/${entityId}?limit=${pageLink.limit}`; - - if (angular.isDefined(pageLink.startTime) && pageLink.startTime != null) { - url += '&startTime=' + pageLink.startTime; - } - if (angular.isDefined(pageLink.endTime) && pageLink.endTime != null) { - url += '&endTime=' + pageLink.endTime; - } - if (angular.isDefined(pageLink.idOffset) && pageLink.idOffset != null) { - url += '&offset=' + pageLink.idOffset; - } - $http.get(url, null).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getAuditLogsByUserId (userId, pageLink) { - var deferred = $q.defer(); - var url = `/api/audit/logs/user/${userId}?limit=${pageLink.limit}`; - - if (angular.isDefined(pageLink.startTime) && pageLink.startTime != null) { - url += '&startTime=' + pageLink.startTime; - } - if (angular.isDefined(pageLink.endTime) && pageLink.endTime != null) { - url += '&endTime=' + pageLink.endTime; - } - if (angular.isDefined(pageLink.idOffset) && pageLink.idOffset != null) { - url += '&offset=' + pageLink.idOffset; - } - $http.get(url, null).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getAuditLogsByCustomerId (customerId, pageLink) { - var deferred = $q.defer(); - var url = `/api/audit/logs/customer/${customerId}?limit=${pageLink.limit}`; - - if (angular.isDefined(pageLink.startTime) && pageLink.startTime != null) { - url += '&startTime=' + pageLink.startTime; - } - if (angular.isDefined(pageLink.endTime) && pageLink.endTime != null) { - url += '&endTime=' + pageLink.endTime; - } - if (angular.isDefined(pageLink.idOffset) && pageLink.idOffset != null) { - url += '&offset=' + pageLink.idOffset; - } - $http.get(url, null).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getAuditLogs (pageLink) { - var deferred = $q.defer(); - var url = `/api/audit/logs?limit=${pageLink.limit}`; - - if (angular.isDefined(pageLink.startTime) && pageLink.startTime != null) { - url += '&startTime=' + pageLink.startTime; - } - if (angular.isDefined(pageLink.endTime) && pageLink.endTime != null) { - url += '&endTime=' + pageLink.endTime; - } - if (angular.isDefined(pageLink.idOffset) && pageLink.idOffset != null) { - url += '&offset=' + pageLink.idOffset; - } - $http.get(url, null).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - -} diff --git a/ui/src/app/api/component-descriptor.service.js b/ui/src/app/api/component-descriptor.service.js deleted file mode 100644 index 3c17d6ca45..0000000000 --- a/ui/src/app/api/component-descriptor.service.js +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default angular.module('thingsboard.api.componentDescriptor', []) - .factory('componentDescriptorService', ComponentDescriptorService).name; - -/*@ngInject*/ -function ComponentDescriptorService($http, $q) { - - var componentsByType = {}; - var componentsByClazz = {}; - var actionsByPlugin = {}; - - var service = { - getComponentDescriptorsByType: getComponentDescriptorsByType, - getComponentDescriptorByClazz: getComponentDescriptorByClazz, - getPluginActionsByPluginClazz: getPluginActionsByPluginClazz, - getComponentDescriptorsByTypes: getComponentDescriptorsByTypes - } - - return service; - - function getComponentDescriptorsByType(componentType) { - var deferred = $q.defer(); - if (componentsByType[componentType]) { - deferred.resolve(componentsByType[componentType]); - } else { - var url = '/api/components/' + componentType; - $http.get(url, null).then(function success(response) { - componentsByType[componentType] = response.data; - for (var i = 0; i < componentsByType[componentType].length; i++) { - var component = componentsByType[componentType][i]; - componentsByClazz[component.clazz] = component; - } - deferred.resolve(componentsByType[componentType]); - }, function fail() { - deferred.reject(); - }); - - } - return deferred.promise; - } - - function getComponentDescriptorsByTypes(componentTypes) { - var deferred = $q.defer(); - var result = []; - for (var i=componentTypes.length-1;i>=0;i--) { - var componentType = componentTypes[i]; - if (componentsByType[componentType]) { - result = result.concat(componentsByType[componentType]); - componentTypes.splice(i, 1); - } - } - if (!componentTypes.length) { - deferred.resolve(result); - } else { - var url = '/api/components?componentTypes=' + componentTypes.join(','); - $http.get(url, null).then(function success(response) { - var components = response.data; - for (var i = 0; i < components.length; i++) { - var component = components[i]; - var componentsList = componentsByType[component.type]; - if (!componentsList) { - componentsList = []; - componentsByType[component.type] = componentsList; - } - componentsList.push(component); - componentsByClazz[component.clazz] = component; - } - result = result.concat(components); - deferred.resolve(components); - }, function fail() { - deferred.reject(); - }); - } - return deferred.promise; - } - - function getComponentDescriptorByClazz(componentDescriptorClazz) { - var deferred = $q.defer(); - if (componentsByClazz[componentDescriptorClazz]) { - deferred.resolve(componentsByClazz[componentDescriptorClazz]); - } else { - var url = '/api/component/' + componentDescriptorClazz; - $http.get(url, null).then(function success(response) { - componentsByClazz[componentDescriptorClazz] = response.data; - deferred.resolve(componentsByClazz[componentDescriptorClazz]); - }, function fail() { - deferred.reject(); - }); - } - return deferred.promise; - } - - function getPluginActionsByPluginClazz(pluginClazz) { - var deferred = $q.defer(); - if (actionsByPlugin[pluginClazz]) { - deferred.resolve(actionsByPlugin[pluginClazz]); - } else { - var url = '/api/components/actions/' + pluginClazz; - $http.get(url, null).then(function success(response) { - actionsByPlugin[pluginClazz] = response.data; - deferred.resolve(actionsByPlugin[pluginClazz]); - }, function fail() { - deferred.reject(); - }); - } - return deferred.promise; - } - -} diff --git a/ui/src/app/api/customer.service.js b/ui/src/app/api/customer.service.js deleted file mode 100644 index 2042130ae8..0000000000 --- a/ui/src/app/api/customer.service.js +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default angular.module('thingsboard.api.customer', []) - .factory('customerService', CustomerService) - .name; - -/*@ngInject*/ -function CustomerService($http, $q, types) { - - var service = { - getCustomers: getCustomers, - getCustomer: getCustomer, - getShortCustomerInfo: getShortCustomerInfo, - applyAssignedCustomersInfo: applyAssignedCustomersInfo, - applyAssignedCustomerInfo: applyAssignedCustomerInfo, - deleteCustomer: deleteCustomer, - saveCustomer: saveCustomer - } - - return service; - - function getCustomers(pageLink, config) { - var deferred = $q.defer(); - var url = '/api/customers?limit=' + pageLink.limit; - if (angular.isDefined(pageLink.textSearch)) { - url += '&textSearch=' + pageLink.textSearch; - } - if (angular.isDefined(pageLink.idOffset)) { - url += '&idOffset=' + pageLink.idOffset; - } - if (angular.isDefined(pageLink.textOffset)) { - url += '&textOffset=' + pageLink.textOffset; - } - $http.get(url, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getCustomer(customerId, config) { - var deferred = $q.defer(); - var url = '/api/customer/' + customerId; - $http.get(url, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail(response) { - deferred.reject(response.data); - }); - return deferred.promise; - } - - function getShortCustomerInfo(customerId) { - var deferred = $q.defer(); - var url = '/api/customer/' + customerId + '/shortInfo'; - $http.get(url, null).then(function success(response) { - deferred.resolve(response.data); - }, function fail(response) { - deferred.reject(response.data); - }); - return deferred.promise; - } - - function applyAssignedCustomersInfo(items) { - var deferred = $q.defer(); - var assignedCustomersMap = {}; - function loadNextCustomerInfoOrComplete(i) { - i++; - if (i < items.length) { - loadNextCustomerInfo(i); - } else { - deferred.resolve(items); - } - } - - function loadNextCustomerInfo(i) { - var item = items[i]; - item.assignedCustomer = {}; - if (item.customerId && item.customerId.id != types.id.nullUid) { - item.assignedCustomer.id = item.customerId.id; - var assignedCustomer = assignedCustomersMap[item.customerId.id]; - if (assignedCustomer){ - item.assignedCustomer = assignedCustomer; - loadNextCustomerInfoOrComplete(i); - } else { - getShortCustomerInfo(item.customerId.id).then( - function success(info) { - assignedCustomer = { - id: item.customerId.id, - title: info.title, - isPublic: info.isPublic - }; - assignedCustomersMap[assignedCustomer.id] = assignedCustomer; - item.assignedCustomer = assignedCustomer; - loadNextCustomerInfoOrComplete(i); - }, - function fail() { - loadNextCustomerInfoOrComplete(i); - } - ); - } - } else { - loadNextCustomerInfoOrComplete(i); - } - } - if (items.length > 0) { - loadNextCustomerInfo(0); - } else { - deferred.resolve(items); - } - return deferred.promise; - } - - function applyAssignedCustomerInfo(items, customerId) { - var deferred = $q.defer(); - getShortCustomerInfo(customerId).then( - function success(info) { - var assignedCustomer = { - id: customerId, - title: info.title, - isPublic: info.isPublic - } - items.forEach(function(item) { - item.assignedCustomer = assignedCustomer; - }); - deferred.resolve(items); - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - - function saveCustomer(customer) { - var deferred = $q.defer(); - var url = '/api/customer'; - $http.post(url, customer).then(function success(response) { - deferred.resolve(response.data); - }, function fail(response) { - deferred.reject(response.data); - }); - return deferred.promise; - } - - function deleteCustomer(customerId) { - var deferred = $q.defer(); - var url = '/api/customer/' + customerId; - $http.delete(url).then(function success() { - deferred.resolve(); - }, function fail(response) { - deferred.reject(response.data); - }); - return deferred.promise; - } - -} diff --git a/ui/src/app/api/dashboard.service.js b/ui/src/app/api/dashboard.service.js deleted file mode 100644 index dbe8f44d0a..0000000000 --- a/ui/src/app/api/dashboard.service.js +++ /dev/null @@ -1,295 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default angular.module('thingsboard.api.dashboard', []) - .factory('dashboardService', DashboardService).name; - -/*@ngInject*/ -function DashboardService($rootScope, $http, $q, $location, $filter) { - - var stDiffPromise; - - $rootScope.dadshboardServiceStateChangeStartHandle = $rootScope.$on('$stateChangeStart', function () { - stDiffPromise = undefined; - }); - - - var service = { - assignDashboardToCustomer: assignDashboardToCustomer, - getCustomerDashboards: getCustomerDashboards, - getServerTimeDiff: getServerTimeDiff, - getDashboard: getDashboard, - getDashboardInfo: getDashboardInfo, - getTenantDashboardsByTenantId: getTenantDashboardsByTenantId, - getTenantDashboards: getTenantDashboards, - deleteDashboard: deleteDashboard, - saveDashboard: saveDashboard, - unassignDashboardFromCustomer: unassignDashboardFromCustomer, - updateDashboardCustomers: updateDashboardCustomers, - addDashboardCustomers: addDashboardCustomers, - removeDashboardCustomers: removeDashboardCustomers, - makeDashboardPublic: makeDashboardPublic, - makeDashboardPrivate: makeDashboardPrivate, - getPublicDashboardLink: getPublicDashboardLink - } - - return service; - - function getTenantDashboardsByTenantId(tenantId, pageLink, config) { - var deferred = $q.defer(); - var url = '/api/tenant/' + tenantId + '/dashboards?limit=' + pageLink.limit; - if (angular.isDefined(pageLink.textSearch)) { - url += '&textSearch=' + pageLink.textSearch; - } - if (angular.isDefined(pageLink.idOffset)) { - url += '&idOffset=' + pageLink.idOffset; - } - if (angular.isDefined(pageLink.textOffset)) { - url += '&textOffset=' + pageLink.textOffset; - } - $http.get(url, config).then(function success(response) { - deferred.resolve(prepareDashboards(response.data)); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getTenantDashboards(pageLink, config) { - var deferred = $q.defer(); - var url = '/api/tenant/dashboards?limit=' + pageLink.limit; - if (angular.isDefined(pageLink.textSearch)) { - url += '&textSearch=' + pageLink.textSearch; - } - if (angular.isDefined(pageLink.idOffset)) { - url += '&idOffset=' + pageLink.idOffset; - } - if (angular.isDefined(pageLink.textOffset)) { - url += '&textOffset=' + pageLink.textOffset; - } - $http.get(url, config).then(function success(response) { - deferred.resolve(prepareDashboards(response.data)); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getCustomerDashboards(customerId, pageLink, config) { - var deferred = $q.defer(); - var url = '/api/customer/' + customerId + '/dashboards?limit=' + pageLink.limit; - if (angular.isDefined(pageLink.idOffset)) { - url += '&offset=' + pageLink.idOffset; - } - $http.get(url, config).then(function success(response) { - response.data = prepareDashboards(response.data); - if (pageLink.textSearch) { - response.data.data = $filter('filter')(response.data.data, {title: pageLink.textSearch}); - } - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getServerTimeDiff() { - if (stDiffPromise) { - return stDiffPromise; - } else { - var deferred = $q.defer(); - stDiffPromise = deferred.promise; - var url = '/api/dashboard/serverTime'; - var ct1 = Date.now(); - $http.get(url, {ignoreLoading: true}).then(function success(response) { - var ct2 = Date.now(); - var st = response.data; - var stDiff = Math.ceil(st - (ct1 + ct2) / 2); - deferred.resolve(stDiff); - }, function fail() { - deferred.reject(); - }); - } - return stDiffPromise; - } - - function getDashboard(dashboardId) { - var deferred = $q.defer(); - var url = '/api/dashboard/' + dashboardId; - $http.get(url, null).then(function success(response) { - deferred.resolve(prepareDashboard(response.data)); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getDashboardInfo(dashboardId, config) { - var deferred = $q.defer(); - var url = '/api/dashboard/info/' + dashboardId; - $http.get(url, config).then(function success(response) { - deferred.resolve(prepareDashboard(response.data)); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function saveDashboard(dashboard) { - var deferred = $q.defer(); - var url = '/api/dashboard'; - $http.post(url, cleanDashboard(dashboard)).then(function success(response) { - deferred.resolve(prepareDashboard(response.data)); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function deleteDashboard(dashboardId) { - var deferred = $q.defer(); - var url = '/api/dashboard/' + dashboardId; - $http.delete(url).then(function success() { - deferred.resolve(); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function assignDashboardToCustomer(customerId, dashboardId) { - var deferred = $q.defer(); - var url = '/api/customer/' + customerId + '/dashboard/' + dashboardId; - $http.post(url, null).then(function success(response) { - deferred.resolve(prepareDashboard(response.data)); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function unassignDashboardFromCustomer(customerId, dashboardId) { - var deferred = $q.defer(); - var url = '/api/customer/' + customerId + '/dashboard/' + dashboardId; - $http.delete(url).then(function success(response) { - deferred.resolve(prepareDashboard(response.data)); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function updateDashboardCustomers(dashboardId, customerIds) { - var deferred = $q.defer(); - var url = '/api/dashboard/' + dashboardId + '/customers'; - $http.post(url, customerIds).then(function success(response) { - deferred.resolve(prepareDashboard(response.data)); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function addDashboardCustomers(dashboardId, customerIds) { - var deferred = $q.defer(); - var url = '/api/dashboard/' + dashboardId + '/customers/add'; - $http.post(url, customerIds).then(function success(response) { - deferred.resolve(prepareDashboard(response.data)); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function removeDashboardCustomers(dashboardId, customerIds) { - var deferred = $q.defer(); - var url = '/api/dashboard/' + dashboardId + '/customers/remove'; - $http.post(url, customerIds).then(function success(response) { - deferred.resolve(prepareDashboard(response.data)); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function makeDashboardPublic(dashboardId) { - var deferred = $q.defer(); - var url = '/api/customer/public/dashboard/' + dashboardId; - $http.post(url, null).then(function success(response) { - deferred.resolve(prepareDashboard(response.data)); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function makeDashboardPrivate(dashboardId) { - var deferred = $q.defer(); - var url = '/api/customer/public/dashboard/' + dashboardId; - $http.delete(url).then(function success(response) { - deferred.resolve(prepareDashboard(response.data)); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getPublicDashboardLink(dashboard) { - var url = $location.protocol() + '://' + $location.host(); - var port = $location.port(); - if (port != 80 && port != 443) { - url += ":" + port; - } - url += "/dashboard/" + dashboard.id.id + "?publicId=" + dashboard.publicCustomerId; - return url; - } - - function prepareDashboards(dashboardsData) { - if (dashboardsData.data) { - for (var i = 0; i < dashboardsData.data.length; i++) { - dashboardsData.data[i] = prepareDashboard(dashboardsData.data[i]); - } - } - return dashboardsData; - } - - function prepareDashboard(dashboard) { - dashboard.publicCustomerId = null; - dashboard.assignedCustomersText = ""; - dashboard.assignedCustomersIds = []; - if (dashboard.assignedCustomers && dashboard.assignedCustomers.length) { - var assignedCustomersTitles = []; - for (var i = 0; i < dashboard.assignedCustomers.length; i++) { - var assignedCustomer = dashboard.assignedCustomers[i]; - dashboard.assignedCustomersIds.push(assignedCustomer.customerId.id); - if (assignedCustomer.public) { - dashboard.publicCustomerId = assignedCustomer.customerId.id; - } else { - assignedCustomersTitles.push(assignedCustomer.title); - } - } - dashboard.assignedCustomersText = assignedCustomersTitles.join(', '); - } - return dashboard; - } - - function cleanDashboard(dashboard) { - delete dashboard.publicCustomerId; - delete dashboard.assignedCustomersText; - delete dashboard.assignedCustomersIds; - return dashboard; - } - -} diff --git a/ui/src/app/api/data-aggregator.js b/ui/src/app/api/data-aggregator.js deleted file mode 100644 index 6efdeff4e7..0000000000 --- a/ui/src/app/api/data-aggregator.js +++ /dev/null @@ -1,316 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default class DataAggregator { - - constructor(onDataCb, tsKeyNames, startTs, limit, aggregationType, timeWindow, interval, - stateData, types, $timeout, $filter) { - this.onDataCb = onDataCb; - this.tsKeyNames = tsKeyNames; - this.dataBuffer = {}; - for (var k = 0; k < tsKeyNames.length; k++) { - this.dataBuffer[tsKeyNames[k]] = []; - } - this.startTs = startTs; - this.aggregationType = aggregationType; - this.types = types; - this.$timeout = $timeout; - this.$filter = $filter; - this.dataReceived = false; - this.resetPending = false; - this.noAggregation = aggregationType === types.aggregation.none.value; - this.limit = limit; - this.timeWindow = timeWindow; - this.interval = interval; - this.stateData = stateData; - if (this.stateData) { - this.lastPrevKvPairData = {}; - } - this.aggregationTimeout = Math.max(this.interval, 1000); - switch (aggregationType) { - case types.aggregation.min.value: - this.aggFunction = min; - break; - case types.aggregation.max.value: - this.aggFunction = max; - break; - case types.aggregation.avg.value: - this.aggFunction = avg; - break; - case types.aggregation.sum.value: - this.aggFunction = sum; - break; - case types.aggregation.count.value: - this.aggFunction = count; - break; - case types.aggregation.none.value: - this.aggFunction = none; - break; - default: - this.aggFunction = avg; - } - } - - reset(startTs, timeWindow, interval) { - if (this.intervalTimeoutHandle) { - this.$timeout.cancel(this.intervalTimeoutHandle); - this.intervalTimeoutHandle = null; - } - this.intervalScheduledTime = currentTime(); - this.startTs = startTs; - this.timeWindow = timeWindow; - this.interval = interval; - this.endTs = this.startTs + this.timeWindow; - this.elapsed = 0; - this.aggregationTimeout = Math.max(this.interval, 1000); - this.resetPending = true; - var self = this; - this.intervalTimeoutHandle = this.$timeout(function() { - self.onInterval(); - }, this.aggregationTimeout, false); - } - - onData(data, update, history, apply) { - if (!this.dataReceived || this.resetPending) { - var updateIntervalScheduledTime = true; - if (!this.dataReceived) { - this.elapsed = 0; - this.dataReceived = true; - this.endTs = this.startTs + this.timeWindow; - } - if (this.resetPending) { - this.resetPending = false; - updateIntervalScheduledTime = false; - } - if (update) { - this.aggregationMap = {}; - updateAggregatedData(this.aggregationMap, this.aggregationType === this.types.aggregation.count.value, - this.noAggregation, this.aggFunction, data.data, this.interval, this.startTs); - } else { - this.aggregationMap = processAggregatedData(data.data, this.aggregationType === this.types.aggregation.count.value, this.noAggregation); - } - if (updateIntervalScheduledTime) { - this.intervalScheduledTime = currentTime(); - } - this.onInterval(history, apply); - } else { - updateAggregatedData(this.aggregationMap, this.aggregationType === this.types.aggregation.count.value, - this.noAggregation, this.aggFunction, data.data, this.interval, this.startTs); - if (history) { - this.intervalScheduledTime = currentTime(); - this.onInterval(history, apply); - } - } - } - - onInterval(history, apply) { - var now = currentTime(); - this.elapsed += now - this.intervalScheduledTime; - this.intervalScheduledTime = now; - if (this.intervalTimeoutHandle) { - this.$timeout.cancel(this.intervalTimeoutHandle); - this.intervalTimeoutHandle = null; - } - if (!history) { - var delta = Math.floor(this.elapsed / this.interval); - if (delta || !this.data) { - this.startTs += delta * this.interval; - this.endTs += delta * this.interval; - this.data = this.updateData(); - this.elapsed = this.elapsed - delta * this.interval; - } - } else { - this.data = this.updateData(); - } - if (this.onDataCb) { - this.onDataCb(this.data, apply); - } - - var self = this; - if (!history) { - this.intervalTimeoutHandle = this.$timeout(function() { - self.onInterval(); - }, this.aggregationTimeout, false); - } - } - - updateData() { - for (var k = 0; k < this.tsKeyNames.length; k++) { - this.dataBuffer[this.tsKeyNames[k]] = []; - } - for (var key in this.aggregationMap) { - var aggKeyData = this.aggregationMap[key]; - var keyData = this.dataBuffer[key]; - for (var aggTimestamp in aggKeyData) { - if (aggTimestamp <= this.startTs) { - if (this.stateData && - (!this.lastPrevKvPairData[key] || this.lastPrevKvPairData[key][0] < aggTimestamp)) { - this.lastPrevKvPairData[key] = [Number(aggTimestamp), aggKeyData[aggTimestamp].aggValue]; - } - delete aggKeyData[aggTimestamp]; - } else if (aggTimestamp <= this.endTs) { - var aggData = aggKeyData[aggTimestamp]; - var kvPair = [Number(aggTimestamp), aggData.aggValue]; - keyData.push(kvPair); - } - } - keyData = this.$filter('orderBy')(keyData, '+this[0]'); - if (this.stateData) { - this.updateStateBounds(keyData, angular.copy(this.lastPrevKvPairData[key])); - } - if (keyData.length > this.limit) { - keyData = keyData.slice(keyData.length - this.limit); - } - this.dataBuffer[key] = keyData; - } - return this.dataBuffer; - } - - updateStateBounds(keyData, lastPrevKvPair) { - if (lastPrevKvPair) { - lastPrevKvPair[0] = this.startTs; - } - var firstKvPair; - if (!keyData.length) { - if (lastPrevKvPair) { - firstKvPair = lastPrevKvPair; - keyData.push(firstKvPair); - } - } else { - firstKvPair = keyData[0]; - } - if (firstKvPair && firstKvPair[0] > this.startTs) { - if (lastPrevKvPair) { - keyData.unshift(lastPrevKvPair); - } - } - if (keyData.length) { - var lastKvPair = keyData[keyData.length-1]; - if (lastKvPair[0] < this.endTs) { - lastKvPair = angular.copy(lastKvPair); - lastKvPair[0] = this.endTs; - keyData.push(lastKvPair); - } - } - } - - destroy() { - if (this.intervalTimeoutHandle) { - this.$timeout.cancel(this.intervalTimeoutHandle); - this.intervalTimeoutHandle = null; - } - this.aggregationMap = null; - } - -} - -/* eslint-disable */ -function currentTime() { - return window.performance && window.performance.now ? - window.performance.now() : Date.now(); -} -/* eslint-enable */ - -function processAggregatedData(data, isCount, noAggregation) { - var aggregationMap = {}; - for (var key in data) { - var aggKeyData = aggregationMap[key]; - if (!aggKeyData) { - aggKeyData = {}; - aggregationMap[key] = aggKeyData; - } - var keyData = data[key]; - for (var i = 0; i < keyData.length; i++) { - var kvPair = keyData[i]; - var timestamp = kvPair[0]; - var value = convertValue(kvPair[1], noAggregation); - var aggKey = timestamp; - var aggData = { - count: isCount ? value : 1, - sum: value, - aggValue: value - } - aggKeyData[aggKey] = aggData; - } - } - return aggregationMap; -} - -function updateAggregatedData(aggregationMap, isCount, noAggregation, aggFunction, data, interval, startTs) { - for (var key in data) { - var aggKeyData = aggregationMap[key]; - if (!aggKeyData) { - aggKeyData = {}; - aggregationMap[key] = aggKeyData; - } - var keyData = data[key]; - for (var i = 0; i < keyData.length; i++) { - var kvPair = keyData[i]; - var timestamp = kvPair[0]; - var value = convertValue(kvPair[1], noAggregation); - var aggTimestamp = noAggregation ? timestamp : (startTs + Math.floor((timestamp - startTs) / interval) * interval + interval/2); - var aggData = aggKeyData[aggTimestamp]; - if (!aggData) { - aggData = { - count: 1, - sum: value, - aggValue: isCount ? 1 : value - } - aggKeyData[aggTimestamp] = aggData; - } else { - aggFunction(aggData, value); - } - } - } -} - -function convertValue(value, noAggregation) { - if (!noAggregation || value && isNumeric(value)) { - return Number(value); - } else { - return value; - } -} - -function isNumeric(value) { - return (value - parseFloat( value ) + 1) >= 0; -} - -function avg(aggData, value) { - aggData.count++; - aggData.sum += value; - aggData.aggValue = aggData.sum / aggData.count; -} - -function min(aggData, value) { - aggData.aggValue = Math.min(aggData.aggValue, value); -} - -function max(aggData, value) { - aggData.aggValue = Math.max(aggData.aggValue, value); -} - -function sum(aggData, value) { - aggData.aggValue = aggData.aggValue + value; -} - -function count(aggData) { - aggData.count++; - aggData.aggValue = aggData.count; -} - -function none(aggData, value) { - aggData.aggValue = value; -} diff --git a/ui/src/app/api/datasource.service.js b/ui/src/app/api/datasource.service.js deleted file mode 100644 index 72a1a3087d..0000000000 --- a/ui/src/app/api/datasource.service.js +++ /dev/null @@ -1,747 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import thingsboardApiDevice from './device.service'; -import thingsboardApiTelemetryWebsocket from './telemetry-websocket.service'; -import thingsboardTypes from '../common/types.constant'; -import thingsboardUtils from '../common/utils.service'; -import DataAggregator from './data-aggregator'; - -export default angular.module('thingsboard.api.datasource', [thingsboardApiDevice, thingsboardApiTelemetryWebsocket, thingsboardTypes, thingsboardUtils]) - .factory('datasourceService', DatasourceService) - .name; - -const YEAR = 1000 * 60 * 60 * 24 * 365; - -/*@ngInject*/ -function DatasourceService($timeout, $filter, $log, telemetryWebsocketService, types, utils) { - - var subscriptions = {}; - - var service = { - subscribeToDatasource: subscribeToDatasource, - unsubscribeFromDatasource: unsubscribeFromDatasource - } - - return service; - - function subscribeToDatasource(listener) { - var datasource = listener.datasource; - - if (datasource.type === types.datasourceType.entity && (!listener.entityId || !listener.entityType)) { - return; - } - - var subscriptionDataKeys = []; - for (var d = 0; d < datasource.dataKeys.length; d++) { - var dataKey = datasource.dataKeys[d]; - var subscriptionDataKey = { - name: dataKey.name, - type: dataKey.type, - funcBody: dataKey.funcBody, - postFuncBody: dataKey.postFuncBody - } - subscriptionDataKeys.push(subscriptionDataKey); - } - - var datasourceSubscription = { - datasourceType: datasource.type, - dataKeys: subscriptionDataKeys, - type: listener.subscriptionType - }; - - if (listener.subscriptionType === types.widgetType.timeseries.value) { - datasourceSubscription.subscriptionTimewindow = angular.copy(listener.subscriptionTimewindow); - } - if (datasourceSubscription.datasourceType === types.datasourceType.entity) { - datasourceSubscription.entityType = listener.entityType; - datasourceSubscription.entityId = listener.entityId; - } - - listener.datasourceSubscriptionKey = utils.objectHashCode(datasourceSubscription); - var subscription; - if (subscriptions[listener.datasourceSubscriptionKey]) { - subscription = subscriptions[listener.datasourceSubscriptionKey]; - subscription.syncListener(listener); - } else { - subscription = new DatasourceSubscription(datasourceSubscription, telemetryWebsocketService, $timeout, $filter, $log, types, utils); - subscriptions[listener.datasourceSubscriptionKey] = subscription; - subscription.start(); - } - subscription.addListener(listener); - } - - function unsubscribeFromDatasource(listener) { - if (listener.datasourceSubscriptionKey) { - if (subscriptions[listener.datasourceSubscriptionKey]) { - var subscription = subscriptions[listener.datasourceSubscriptionKey]; - subscription.removeListener(listener); - if (!subscription.hasListeners()) { - subscription.unsubscribe(); - delete subscriptions[listener.datasourceSubscriptionKey]; - } - } - listener.datasourceSubscriptionKey = null; - } - } - -} - -function DatasourceSubscription(datasourceSubscription, telemetryWebsocketService, $timeout, $filter, $log, types, utils) { - - var listeners = []; - var datasourceType = datasourceSubscription.datasourceType; - var datasourceData = {}; - var dataSourceOrigData = {}; - var dataKeys = {}; - var subscribers = []; - var history = datasourceSubscription.subscriptionTimewindow && - datasourceSubscription.subscriptionTimewindow.fixedWindow; - var realtime = datasourceSubscription.subscriptionTimewindow && - datasourceSubscription.subscriptionTimewindow.realtimeWindowMs; - var timer; - var frequency; - var tickElapsed = 0; - var tickScheduledTime = 0; - var dataAggregator; - - var subscription = { - addListener: addListener, - hasListeners: hasListeners, - removeListener: removeListener, - syncListener: syncListener, - start: start, - unsubscribe: unsubscribe - } - - initializeSubscription(); - - return subscription; - - function initializeSubscription() { - for (var i = 0; i < datasourceSubscription.dataKeys.length; i++) { - var dataKey = angular.copy(datasourceSubscription.dataKeys[i]); - dataKey.index = i; - var key; - if (datasourceType === types.datasourceType.function) { - if (!dataKey.func) { - dataKey.func = new Function("time", "prevValue", dataKey.funcBody); - } - } else { - if (dataKey.postFuncBody && !dataKey.postFunc) { - dataKey.postFunc = new Function("time", "value", "prevValue", "timePrev", "prevOrigValue", dataKey.postFuncBody); - } - } - if (datasourceType === types.datasourceType.entity || datasourceSubscription.type === types.widgetType.timeseries.value) { - if (datasourceType === types.datasourceType.function) { - key = dataKey.name + '_' + dataKey.index + '_' + dataKey.type; - } else { - key = dataKey.name + '_' + dataKey.type; - } - var dataKeysList = dataKeys[key]; - if (!dataKeysList) { - dataKeysList = []; - dataKeys[key] = dataKeysList; - } - var index = dataKeysList.push(dataKey) - 1; - datasourceData[key + '_' + index] = { - data: [] - }; - } else { - key = utils.objectHashCode(dataKey); - datasourceData[key] = { - data: [] - }; - dataKeys[key] = dataKey; - } - dataSourceOrigData = angular.copy(datasourceData); - dataKey.key = key; - } - if (datasourceType === types.datasourceType.function) { - frequency = 1000; - if (datasourceSubscription.type === types.widgetType.timeseries.value) { - frequency = Math.min(datasourceSubscription.subscriptionTimewindow.aggregation.interval, 5000); - } - } - } - - function addListener(listener) { - listeners.push(listener); - if (history) { - start(); - } - } - - function hasListeners() { - return listeners.length > 0; - } - - function removeListener(listener) { - listeners.splice(listeners.indexOf(listener), 1); - } - - function syncListener(listener) { - var key; - var dataKey; - if (datasourceType === types.datasourceType.entity || datasourceSubscription.type === types.widgetType.timeseries.value) { - for (key in dataKeys) { - var dataKeysList = dataKeys[key]; - for (var i = 0; i < dataKeysList.length; i++) { - dataKey = dataKeysList[i]; - var datasourceKey = key + '_' + i; - listener.dataUpdated(datasourceData[datasourceKey], - listener.datasourceIndex, - dataKey.index, false); - } - } - } else { - for (key in dataKeys) { - dataKey = dataKeys[key]; - listener.dataUpdated(datasourceData[key], - listener.datasourceIndex, - dataKey.index, false); - } - } - } - - function start() { - if (history && !hasListeners()) { - return; - } - var subsTw = datasourceSubscription.subscriptionTimewindow; - var tsKeyNames = []; - var dataKey; - - if (datasourceType === types.datasourceType.entity) { - - //send subscribe command - - var tsKeys = ''; - var attrKeys = ''; - - for (var key in dataKeys) { - var dataKeysList = dataKeys[key]; - dataKey = dataKeysList[0]; - if (dataKey.type === types.dataKeyType.timeseries) { - if (tsKeys.length > 0) { - tsKeys += ','; - } - tsKeys += dataKey.name; - tsKeyNames.push(dataKey.name); - } else if (dataKey.type === types.dataKeyType.attribute) { - if (attrKeys.length > 0) { - attrKeys += ','; - } - attrKeys += dataKey.name; - } - } - - if (tsKeys.length > 0) { - - var subscriber; - - if (history) { - - var historyCommand = { - entityType: datasourceSubscription.entityType, - entityId: datasourceSubscription.entityId, - keys: tsKeys, - startTs: subsTw.fixedWindow.startTimeMs, - endTs: subsTw.fixedWindow.endTimeMs, - interval: subsTw.aggregation.interval, - limit: subsTw.aggregation.limit, - agg: subsTw.aggregation.type - }; - - subscriber = { - historyCommands: [ historyCommand ], - type: types.dataKeyType.timeseries, - subsTw: subsTw - }; - - if (subsTw.aggregation.stateData) { - subscriber.firstStateHistoryCommand = createFirstStateHistoryCommand(subsTw.fixedWindow.startTimeMs, tsKeys); - subscriber.historyCommands.push(subscriber.firstStateHistoryCommand); - } - - subscriber.onData = function (data, subscriptionId) { - if (this.subsTw.aggregation.stateData && - this.firstStateHistoryCommand && this.firstStateHistoryCommand.cmdId == subscriptionId) { - if (this.data) { - onStateHistoryData(data, this.data, this.subsTw.aggregation.limit, - subsTw.fixedWindow.startTimeMs, this.subsTw.fixedWindow.endTimeMs, - (data) => { - onData(data.data, types.dataKeyType.timeseries, true); - }); - } else { - this.firstStateData = data; - } - } else { - if (this.subsTw.aggregation.stateData) { - if (this.firstStateData) { - onStateHistoryData(this.firstStateData, data, this.subsTw.aggregation.limit, - this.subsTw.fixedWindow.startTimeMs, this.subsTw.fixedWindow.endTimeMs, - (data) => { - onData(data.data, types.dataKeyType.timeseries, true); - }); - } else { - this.data = data; - } - } else { - for (key in data.data) { - var keyData = data.data[key]; - data.data[key] = $filter('orderBy')(keyData, '+this[0]'); - } - onData(data.data, types.dataKeyType.timeseries, true); - } - } - }; - subscriber.onReconnected = function() {}; - telemetryWebsocketService.subscribe(subscriber); - subscribers.push(subscriber); - - } else { - - var subscriptionCommand = { - entityType: datasourceSubscription.entityType, - entityId: datasourceSubscription.entityId, - keys: tsKeys - }; - - subscriber = { - subscriptionCommands: [subscriptionCommand], - type: types.dataKeyType.timeseries - }; - - if (datasourceSubscription.type === types.widgetType.timeseries.value) { - subscriber.subsTw = subsTw; - updateRealtimeSubscriptionCommand(subscriptionCommand, subsTw); - - if (subsTw.aggregation.stateData) { - subscriber.firstStateSubscriptionCommand = createFirstStateHistoryCommand(subsTw.startTs, tsKeys); - subscriber.historyCommands = [subscriber.firstStateSubscriptionCommand]; - } - dataAggregator = createRealtimeDataAggregator(subsTw, tsKeyNames, types.dataKeyType.timeseries); - subscriber.onData = function(data, subscriptionId) { - if (this.subsTw.aggregation.stateData && - this.firstStateSubscriptionCommand && this.firstStateSubscriptionCommand.cmdId == subscriptionId) { - if (this.data) { - onStateHistoryData(data, this.data, this.subsTw.aggregation.limit, - this.subsTw.startTs, this.subsTw.startTs + this.subsTw.aggregation.timeWindow, - (data) => { - dataAggregator.onData(data, false, false, true); - }); - this.stateDataReceived = true; - } else { - this.firstStateData = data; - } - } else { - if (this.subsTw.aggregation.stateData && !this.stateDataReceived) { - if (this.firstStateData) { - onStateHistoryData(this.firstStateData, data, this.subsTw.aggregation.limit, - this.subsTw.startTs, this.subsTw.startTs + this.subsTw.aggregation.timeWindow, - (data) => { - dataAggregator.onData(data, false, false, true); - }); - this.stateDataReceived = true; - } else { - this.data = data; - } - } else { - dataAggregator.onData(data, false, false, true); - } - } - } - subscriber.onReconnected = function() { - var newSubsTw = null; - for (var i2 = 0; i2 < listeners.length; i2++) { - var listener = listeners[i2]; - if (!newSubsTw) { - newSubsTw = listener.updateRealtimeSubscription(); - } else { - listener.setRealtimeSubscription(newSubsTw); - } - } - this.subsTw = newSubsTw; - this.firstStateData = null; - this.data = null; - this.stateDataReceived = false; - updateRealtimeSubscriptionCommand(this.subscriptionCommands[0], this.subsTw); - if (this.subsTw.aggregation.stateData) { - updateFirstStateHistoryCommand(this.firstStateSubscriptionCommand, this.subsTw.startTs); - } - dataAggregator.reset(newSubsTw.startTs, newSubsTw.aggregation.timeWindow, newSubsTw.aggregation.interval); - } - } else { - subscriber.onReconnected = function() {} - subscriber.onData = function(data) { - if (data.data) { - onData(data.data, types.dataKeyType.timeseries, true); - } - } - } - - telemetryWebsocketService.subscribe(subscriber); - subscribers.push(subscriber); - - } - } - - if (attrKeys.length > 0) { - - var attrsSubscriptionCommand = { - entityType: datasourceSubscription.entityType, - entityId: datasourceSubscription.entityId, - keys: attrKeys - }; - - subscriber = { - subscriptionCommands: [attrsSubscriptionCommand], - type: types.dataKeyType.attribute, - onData: function (data) { - if (data.data) { - onData(data.data, types.dataKeyType.attribute, true); - } - }, - onReconnected: function() {} - }; - - telemetryWebsocketService.subscribe(subscriber); - subscribers.push(subscriber); - - } - - } else if (datasourceType === types.datasourceType.function) { - if (datasourceSubscription.type === types.widgetType.timeseries.value) { - for (key in dataKeys) { - var dataKeyList = dataKeys[key]; - for (var index = 0; index < dataKeyList.length; index++) { - dataKey = dataKeyList[index]; - tsKeyNames.push(dataKey.name+'_'+dataKey.index); - } - } - dataAggregator = createRealtimeDataAggregator(subsTw, tsKeyNames, types.dataKeyType.function); - } - tickScheduledTime = currentTime(); - if (history) { - onTick(false); - } else { - timer = $timeout( - function() { - onTick(true) - }, - 0, - false - ); - } - } - } - - function createFirstStateHistoryCommand(startTs, tsKeys) { - return { - entityType: datasourceSubscription.entityType, - entityId: datasourceSubscription.entityId, - keys: tsKeys, - startTs: startTs - YEAR, - endTs: startTs, - interval: 1000, - limit: 1, - agg: types.aggregation.none.value - }; - } - - function updateFirstStateHistoryCommand(stateHistoryCommand, startTs) { - stateHistoryCommand.startTs = startTs - YEAR; - stateHistoryCommand.endTs = startTs; - } - - function onStateHistoryData(firstStateData, data, limit, startTs, endTs, onData) { - for (var key in data.data) { - var keyData = data.data[key]; - data.data[key] = $filter('orderBy')(keyData, '+this[0]'); - keyData = data.data[key]; - if (keyData.length < limit) { - var firstStateKeyData = firstStateData.data[key]; - if (firstStateKeyData.length) { - var firstStateDataTsKv = firstStateKeyData[0]; - firstStateDataTsKv[0] = startTs; - firstStateKeyData = [ - [ startTs, firstStateKeyData[0][1] ] - ]; - keyData.unshift(firstStateDataTsKv); - } - } - if (keyData.length) { - var lastTsKv = angular.copy(keyData[keyData.length-1]); - lastTsKv[0] = endTs; - keyData.push(lastTsKv); - } - } - onData(data); - } - - function createRealtimeDataAggregator(subsTw, tsKeyNames, dataKeyType) { - return new DataAggregator( - function(data, apply) { - onData(data, dataKeyType, apply); - }, - tsKeyNames, - subsTw.startTs, - subsTw.aggregation.limit, - subsTw.aggregation.type, - subsTw.aggregation.timeWindow, - subsTw.aggregation.interval, - subsTw.aggregation.stateData, - types, - $timeout, - $filter - ); - } - - function updateRealtimeSubscriptionCommand(subscriptionCommand, subsTw) { - subscriptionCommand.startTs = subsTw.startTs; - subscriptionCommand.timeWindow = subsTw.aggregation.timeWindow; - subscriptionCommand.interval = subsTw.aggregation.interval; - subscriptionCommand.limit = subsTw.aggregation.limit; - subscriptionCommand.agg = subsTw.aggregation.type; - } - - function unsubscribe() { - if (timer) { - $timeout.cancel(timer); - timer = null; - } - if (datasourceType === types.datasourceType.entity) { - for (var i=0;i 0) { - prevSeries = datasourceKeyData[datasourceKeyData.length - 1]; - } else { - prevSeries = [0, 0]; - } - for (var time = startTime; time <= endTime && (timer || history); time += frequency) { - var series = []; - series.push(time); - var value = dataKey.func(time, prevSeries[1]); - series.push(value); - data.push(series); - prevSeries = series; - } - if (data.length > 0) { - dataKey.lastUpdateTime = data[data.length - 1][0]; - } - return data; - } - - function generateLatest(dataKey, apply) { - var prevSeries; - var datasourceKeyData = datasourceData[dataKey.key].data; - if (datasourceKeyData.length > 0) { - prevSeries = datasourceKeyData[datasourceKeyData.length - 1]; - } else { - prevSeries = [0, 0]; - } - var series = []; - var time = (new Date).getTime(); - series.push(time); - var value = dataKey.func(time, prevSeries[1]); - series.push(value); - datasourceData[dataKey.key].data = [series]; - for (var i = 0; i < listeners.length; i++) { - var listener = listeners[i]; - listener.dataUpdated(datasourceData[dataKey.key], - listener.datasourceIndex, - dataKey.index, apply); - } - } - - /* eslint-disable */ - function currentTime() { - return window.performance && window.performance.now ? - window.performance.now() : Date.now(); - } - /* eslint-enable */ - - - function onTick(apply) { - - var now = currentTime(); - tickElapsed += now - tickScheduledTime; - tickScheduledTime = now; - - if (timer) { - $timeout.cancel(timer); - } - - var key; - if (datasourceSubscription.type === types.widgetType.timeseries.value) { - var startTime; - var endTime; - var delta; - var generatedData = { - data: { - } - }; - if (!history) { - delta = Math.floor(tickElapsed / frequency); - } - var deltaElapsed = history ? frequency : delta * frequency; - tickElapsed = tickElapsed - deltaElapsed; - for (key in dataKeys) { - var dataKeyList = dataKeys[key]; - for (var index = 0; index < dataKeyList.length && (timer || history); index ++) { - var dataKey = dataKeyList[index]; - if (!startTime) { - if (realtime) { - if (dataKey.lastUpdateTime) { - startTime = dataKey.lastUpdateTime + frequency; - endTime = dataKey.lastUpdateTime + deltaElapsed; - } else { - startTime = datasourceSubscription.subscriptionTimewindow.startTs; - endTime = startTime + datasourceSubscription.subscriptionTimewindow.realtimeWindowMs + frequency; - if (datasourceSubscription.subscriptionTimewindow.aggregation.type == types.aggregation.none.value) { - var time = endTime - frequency * datasourceSubscription.subscriptionTimewindow.aggregation.limit; - startTime = Math.max(time, startTime); - } - } - } else { - startTime = datasourceSubscription.subscriptionTimewindow.fixedWindow.startTimeMs; - endTime = datasourceSubscription.subscriptionTimewindow.fixedWindow.endTimeMs; - } - } - var data = generateSeries(dataKey, index, startTime, endTime); - generatedData.data[dataKey.name+'_'+dataKey.index] = data; - } - } - if (dataAggregator) { - dataAggregator.onData(generatedData, true, history, apply); - } - } else if (datasourceSubscription.type === types.widgetType.latest.value) { - for (key in dataKeys) { - generateLatest(dataKeys[key], apply); - } - } - - if (!history) { - timer = $timeout(function() {onTick(true)}, frequency, false); - } - } - - function isNumeric(val) { - return (val - parseFloat( val ) + 1) >= 0; - } - - function convertValue(val) { - if (val && isNumeric(val)) { - return Number(val); - } else { - return val; - } - } - - function onData(sourceData, type, apply) { - for (var keyName in sourceData) { - var keyData = sourceData[keyName]; - var key = keyName + '_' + type; - var dataKeyList = dataKeys[key]; - for (var keyIndex = 0; dataKeyList && keyIndex < dataKeyList.length; keyIndex++) { - var datasourceKey = key + "_" + keyIndex; - if (datasourceData[datasourceKey].data) { - var dataKey = dataKeyList[keyIndex]; - var data = []; - var prevSeries; - var prevOrigSeries; - var datasourceKeyData; - var datasourceOrigKeyData; - var update = false; - if (realtime) { - datasourceKeyData = []; - datasourceOrigKeyData = []; - } else { - datasourceKeyData = datasourceData[datasourceKey].data; - datasourceOrigKeyData = dataSourceOrigData[datasourceKey].data; - } - if (datasourceKeyData.length > 0) { - prevSeries = datasourceKeyData[datasourceKeyData.length - 1]; - prevOrigSeries = datasourceOrigKeyData[datasourceOrigKeyData.length -1]; - } else { - prevSeries = [0, 0]; - prevOrigSeries = [0, 0]; - } - dataSourceOrigData[datasourceKey].data = []; - if (datasourceSubscription.type === types.widgetType.timeseries.value) { - var series, time, value; - for (var i = 0; i < keyData.length; i++) { - series = keyData[i]; - time = series[0]; - dataSourceOrigData[datasourceKey].data.push(series); - value = convertValue(series[1]); - if (dataKey.postFunc) { - value = dataKey.postFunc(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]); - } - prevOrigSeries = series; - series = [time, value]; - data.push(series); - prevSeries = series; - } - update = true; - } else if (datasourceSubscription.type === types.widgetType.latest.value) { - if (keyData.length > 0) { - series = keyData[0]; - time = series[0]; - dataSourceOrigData[datasourceKey].data.push(series); - value = convertValue(series[1]); - if (dataKey.postFunc) { - value = dataKey.postFunc(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]); - } - series = [time, value]; - data.push(series); - } - update = true; - } - if (update) { - datasourceData[datasourceKey].data = data; - for (var i2 = 0; i2 < listeners.length; i2++) { - var listener = listeners[i2]; - if (angular.isFunction(listener)) - continue; - listener.dataUpdated(datasourceData[datasourceKey], - listener.datasourceIndex, - dataKey.index, apply); - } - } - } - } - } - } -} diff --git a/ui/src/app/api/device.service.js b/ui/src/app/api/device.service.js deleted file mode 100644 index b803ec333a..0000000000 --- a/ui/src/app/api/device.service.js +++ /dev/null @@ -1,361 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import thingsboardTypes from '../common/types.constant'; - -export default angular.module('thingsboard.api.device', [thingsboardTypes]) - .factory('deviceService', DeviceService) - .name; - -/*@ngInject*/ -function DeviceService($http, $q, $window, userService, attributeService, customerService, types) { - - var service = { - assignDeviceToCustomer: assignDeviceToCustomer, - deleteDevice: deleteDevice, - getCustomerDevices: getCustomerDevices, - getDevice: getDevice, - getDevices: getDevices, - getDeviceCredentials: getDeviceCredentials, - getTenantDevices: getTenantDevices, - saveDevice: saveDevice, - saveDeviceCredentials: saveDeviceCredentials, - unassignDeviceFromCustomer: unassignDeviceFromCustomer, - makeDevicePublic: makeDevicePublic, - getDeviceAttributes: getDeviceAttributes, - subscribeForDeviceAttributes: subscribeForDeviceAttributes, - unsubscribeForDeviceAttributes: unsubscribeForDeviceAttributes, - saveDeviceAttributes: saveDeviceAttributes, - deleteDeviceAttributes: deleteDeviceAttributes, - sendOneWayRpcCommand: sendOneWayRpcCommand, - sendTwoWayRpcCommand: sendTwoWayRpcCommand, - findByQuery: findByQuery, - getDeviceTypes: getDeviceTypes, - findByName: findByName, - claimDevice: claimDevice, - unclaimDevice: unclaimDevice - }; - - return service; - - function getTenantDevices(pageLink, applyCustomersInfo, config, type) { - var deferred = $q.defer(); - var url = '/api/tenant/devices?limit=' + pageLink.limit; - if (angular.isDefined(pageLink.textSearch)) { - url += '&textSearch=' + pageLink.textSearch; - } - if (angular.isDefined(pageLink.idOffset)) { - url += '&idOffset=' + pageLink.idOffset; - } - if (angular.isDefined(pageLink.textOffset)) { - url += '&textOffset=' + pageLink.textOffset; - } - if (angular.isDefined(type) && type.length) { - url += '&type=' + type; - } - $http.get(url, config).then(function success(response) { - if (applyCustomersInfo) { - customerService.applyAssignedCustomersInfo(response.data.data).then( - function success(data) { - response.data.data = data; - deferred.resolve(response.data); - }, - function fail() { - deferred.reject(); - } - ); - } else { - deferred.resolve(response.data); - } - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getCustomerDevices(customerId, pageLink, applyCustomersInfo, config, type) { - var deferred = $q.defer(); - var url = '/api/customer/' + customerId + '/devices?limit=' + pageLink.limit; - if (angular.isDefined(pageLink.textSearch)) { - url += '&textSearch=' + pageLink.textSearch; - } - if (angular.isDefined(pageLink.idOffset)) { - url += '&idOffset=' + pageLink.idOffset; - } - if (angular.isDefined(pageLink.textOffset)) { - url += '&textOffset=' + pageLink.textOffset; - } - if (angular.isDefined(type) && type.length) { - url += '&type=' + type; - } - $http.get(url, config).then(function success(response) { - if (applyCustomersInfo) { - customerService.applyAssignedCustomerInfo(response.data.data, customerId).then( - function success(data) { - response.data.data = data; - deferred.resolve(response.data); - }, - function fail() { - deferred.reject(); - } - ); - } else { - deferred.resolve(response.data); - } - }, function fail() { - deferred.reject(); - }); - - return deferred.promise; - } - - function getDevice(deviceId, ignoreErrors, config) { - var deferred = $q.defer(); - var url = '/api/device/' + deviceId; - if (!config) { - config = {}; - } - config = Object.assign(config, {ignoreErrors: ignoreErrors}); - $http.get(url, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail(response) { - deferred.reject(response.data); - }); - return deferred.promise; - } - - function getDevices(deviceIds, config) { - var deferred = $q.defer(); - var ids = ''; - for (var i = 0; i < deviceIds.length; i++) { - if (i > 0) { - ids += ','; - } - ids += deviceIds[i]; - } - var url = '/api/devices?deviceIds=' + ids; - $http.get(url, config).then(function success(response) { - var devices = response.data; - devices.sort(function (device1, device2) { - var id1 = device1.id.id; - var id2 = device2.id.id; - var index1 = deviceIds.indexOf(id1); - var index2 = deviceIds.indexOf(id2); - return index1 - index2; - }); - deferred.resolve(devices); - }, function fail(response) { - deferred.reject(response.data); - }); - return deferred.promise; - } - - function saveDevice(device, config) { - config = config || {}; - var deferred = $q.defer(); - var url = '/api/device'; - $http.post(url, device, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function deleteDevice(deviceId) { - var deferred = $q.defer(); - var url = '/api/device/' + deviceId; - $http.delete(url).then(function success() { - deferred.resolve(); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getDeviceCredentials(deviceId, sync, config) { - config = config || {}; - var deferred = $q.defer(); - var url = '/api/device/' + deviceId + '/credentials'; - if (sync) { - var request = new $window.XMLHttpRequest(); - request.open('GET', url, false); - request.setRequestHeader("Accept", "application/json, text/plain, */*"); - userService.setAuthorizationRequestHeader(request); - request.send(null); - if (request.status === 200) { - deferred.resolve(angular.fromJson(request.responseText)); - } else { - deferred.reject(); - } - } else { - $http.get(url, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - } - return deferred.promise; - } - - function saveDeviceCredentials(deviceCredentials, config) { - config = config || {}; - var deferred = $q.defer(); - var url = '/api/device/credentials'; - $http.post(url, deviceCredentials, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function assignDeviceToCustomer(customerId, deviceId) { - var deferred = $q.defer(); - var url = '/api/customer/' + customerId + '/device/' + deviceId; - $http.post(url, null).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function unassignDeviceFromCustomer(deviceId) { - var deferred = $q.defer(); - var url = '/api/customer/device/' + deviceId; - $http.delete(url).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function makeDevicePublic(deviceId) { - var deferred = $q.defer(); - var url = '/api/customer/public/device/' + deviceId; - $http.post(url, null).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getDeviceAttributes(deviceId, attributeScope, query, successCallback, config) { - return attributeService.getEntityAttributes(types.entityType.device, deviceId, attributeScope, query, successCallback, config); - } - - function subscribeForDeviceAttributes(deviceId, attributeScope) { - return attributeService.subscribeForEntityAttributes(types.entityType.device, deviceId, attributeScope); - } - - function unsubscribeForDeviceAttributes(subscriptionId) { - attributeService.unsubscribeForEntityAttributes(subscriptionId); - } - - function saveDeviceAttributes(deviceId, attributeScope, attributes) { - return attributeService.saveEntityAttributes(types.entityType.device, deviceId, attributeScope, attributes); - } - - function deleteDeviceAttributes(deviceId, attributeScope, attributes) { - return attributeService.deleteEntityAttributes(types.entityType.device, deviceId, attributeScope, attributes); - } - - function sendOneWayRpcCommand(deviceId, requestBody) { - var deferred = $q.defer(); - var url = '/api/plugins/rpc/oneway/' + deviceId; - $http.post(url, requestBody).then(function success(response) { - deferred.resolve(response.data); - }, function fail(rejection) { - deferred.reject(rejection); - }); - return deferred.promise; - } - - function sendTwoWayRpcCommand(deviceId, requestBody) { - var deferred = $q.defer(); - var url = '/api/plugins/rpc/twoway/' + deviceId; - $http.post(url, requestBody).then(function success(response) { - deferred.resolve(response.data); - }, function fail(rejection) { - deferred.reject(rejection); - }); - return deferred.promise; - } - - function findByQuery(query, ignoreErrors, config) { - var deferred = $q.defer(); - var url = '/api/devices'; - if (!config) { - config = {}; - } - config = Object.assign(config, {ignoreErrors: ignoreErrors}); - $http.post(url, query, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getDeviceTypes(config) { - var deferred = $q.defer(); - var url = '/api/device/types'; - $http.get(url, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function findByName(deviceName, config) { - config = config || {}; - var deferred = $q.defer(); - var url = '/api/tenant/devices?deviceName=' + deviceName; - $http.get(url, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function claimDevice(deviceName, deviceSecret, config) { - deviceSecret = deviceSecret || {}; - config = config || {}; - const deferred = $q.defer(); - const url = '/api/customer/device/' + deviceName + '/claim'; - $http.post(url, deviceSecret, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail(rejection) { - deferred.reject(rejection); - }); - return deferred.promise; - } - - function unclaimDevice(deviceName) { - const deferred = $q.defer(); - const url = '/api/customer/device/' + deviceName + '/claim'; - $http.delete(url).then(function success(response) { - deferred.resolve(response.data); - }, function fail(rejection) { - deferred.reject(rejection); - }); - return deferred.promise; - } -} diff --git a/ui/src/app/api/entity-relation.service.js b/ui/src/app/api/entity-relation.service.js deleted file mode 100644 index cfa287da8e..0000000000 --- a/ui/src/app/api/entity-relation.service.js +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default angular.module('thingsboard.api.entityRelation', []) - .factory('entityRelationService', EntityRelationService) - .name; - -/*@ngInject*/ -function EntityRelationService($http, $q) { - - var service = { - saveRelation: saveRelation, - deleteRelation: deleteRelation, - deleteRelations: deleteRelations, - getRelation: getRelation, - findByFrom: findByFrom, - findInfoByFrom: findInfoByFrom, - findByFromAndType: findByFromAndType, - findByTo: findByTo, - findInfoByTo: findInfoByTo, - findByToAndType: findByToAndType, - findByQuery: findByQuery, - findInfoByQuery: findInfoByQuery - } - - return service; - - function saveRelation(relation) { - var deferred = $q.defer(); - var url = '/api/relation'; - $http.post(url, relation).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function deleteRelation(fromId, fromType, relationType, toId, toType) { - var deferred = $q.defer(); - var url = '/api/relation?fromId=' + fromId; - url += '&fromType=' + fromType; - url += '&relationType=' + relationType; - url += '&toId=' + toId; - url += '&toType=' + toType; - $http.delete(url).then(function success() { - deferred.resolve(); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function deleteRelations(entityId, entityType) { - var deferred = $q.defer(); - var url = '/api/relations?entityId=' + entityId; - url += '&entityType=' + entityType; - $http.delete(url).then(function success() { - deferred.resolve(); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getRelation(fromId, fromType, relationType, toId, toType) { - var deferred = $q.defer(); - var url = '/api/relation?fromId=' + fromId; - url += '&fromType=' + fromType; - url += '&relationType=' + relationType; - url += '&toId=' + toId; - url += '&toType=' + toType; - $http.get(url).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function findByFrom(fromId, fromType) { - var deferred = $q.defer(); - var url = '/api/relations?fromId=' + fromId; - url += '&fromType=' + fromType; - $http.get(url, null).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function findInfoByFrom(fromId, fromType) { - var deferred = $q.defer(); - var url = '/api/relations/info?fromId=' + fromId; - url += '&fromType=' + fromType; - $http.get(url, null).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function findByFromAndType(fromId, fromType, relationType) { - var deferred = $q.defer(); - var url = '/api/relations?fromId=' + fromId; - url += '&fromType=' + fromType; - url += '&relationType=' + relationType; - $http.get(url, null).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function findByTo(toId, toType) { - var deferred = $q.defer(); - var url = '/api/relations?toId=' + toId; - url += '&toType=' + toType; - $http.get(url, null).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function findInfoByTo(toId, toType) { - var deferred = $q.defer(); - var url = '/api/relations/info?toId=' + toId; - url += '&toType=' + toType; - $http.get(url, null).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function findByToAndType(toId, toType, relationType) { - var deferred = $q.defer(); - var url = '/api/relations?toId=' + toId; - url += '&toType=' + toType; - url += '&relationType=' + relationType; - $http.get(url, null).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function findByQuery(query, config) { - var deferred = $q.defer(); - var url = '/api/relations'; - $http.post(url, query, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail(e) { - deferred.reject(e); - }); - return deferred.promise; - } - - function findInfoByQuery(query, config) { - var deferred = $q.defer(); - var url = '/api/relations/info'; - $http.post(url, query, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - -} diff --git a/ui/src/app/api/entity-view.service.js b/ui/src/app/api/entity-view.service.js deleted file mode 100644 index 83e4b49fd0..0000000000 --- a/ui/src/app/api/entity-view.service.js +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import thingsboardTypes from '../common/types.constant'; - -export default angular.module('thingsboard.api.entityView', [thingsboardTypes]) - .factory('entityViewService', EntityViewService) - .name; - -/*@ngInject*/ -function EntityViewService($http, $q, $window, userService, attributeService, customerService, types) { - - var service = { - assignEntityViewToCustomer: assignEntityViewToCustomer, - deleteEntityView: deleteEntityView, - getCustomerEntityViews: getCustomerEntityViews, - getEntityView: getEntityView, - getTenantEntityViews: getTenantEntityViews, - saveEntityView: saveEntityView, - unassignEntityViewFromCustomer: unassignEntityViewFromCustomer, - getEntityViewAttributes: getEntityViewAttributes, - subscribeForEntityViewAttributes: subscribeForEntityViewAttributes, - unsubscribeForEntityViewAttributes: unsubscribeForEntityViewAttributes, - findByQuery: findByQuery, - getEntityViewTypes: getEntityViewTypes, - makeEntityViewPublic: makeEntityViewPublic - } - - return service; - - function getTenantEntityViews(pageLink, applyCustomersInfo, config, type) { - var deferred = $q.defer(); - var url = '/api/tenant/entityViews?limit=' + pageLink.limit; - if (angular.isDefined(pageLink.textSearch)) { - url += '&textSearch=' + pageLink.textSearch; - } - if (angular.isDefined(pageLink.idOffset)) { - url += '&idOffset=' + pageLink.idOffset; - } - if (angular.isDefined(pageLink.textOffset)) { - url += '&textOffset=' + pageLink.textOffset; - } - if (angular.isDefined(type) && type.length) { - url += '&type=' + type; - } - $http.get(url, config).then(function success(response) { - if (applyCustomersInfo) { - customerService.applyAssignedCustomersInfo(response.data.data).then( - function success(data) { - response.data.data = data; - deferred.resolve(response.data); - }, - function fail() { - deferred.reject(); - } - ); - } else { - deferred.resolve(response.data); - } - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getCustomerEntityViews(customerId, pageLink, applyCustomersInfo, config, type) { - var deferred = $q.defer(); - var url = '/api/customer/' + customerId + '/entityViews?limit=' + pageLink.limit; - if (angular.isDefined(pageLink.textSearch)) { - url += '&textSearch=' + pageLink.textSearch; - } - if (angular.isDefined(pageLink.idOffset)) { - url += '&idOffset=' + pageLink.idOffset; - } - if (angular.isDefined(pageLink.textOffset)) { - url += '&textOffset=' + pageLink.textOffset; - } - if (angular.isDefined(type) && type.length) { - url += '&type=' + type; - } - $http.get(url, config).then(function success(response) { - if (applyCustomersInfo) { - customerService.applyAssignedCustomerInfo(response.data.data, customerId).then( - function success(data) { - response.data.data = data; - deferred.resolve(response.data); - }, - function fail() { - deferred.reject(); - } - ); - } else { - deferred.resolve(response.data); - } - }, function fail() { - deferred.reject(); - }); - - return deferred.promise; - } - - function getEntityView(entityViewId, ignoreErrors, config) { - var deferred = $q.defer(); - var url = '/api/entityView/' + entityViewId; - if (!config) { - config = {}; - } - config = Object.assign(config, { ignoreErrors: ignoreErrors }); - $http.get(url, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail(response) { - deferred.reject(response.data); - }); - return deferred.promise; - } - - function saveEntityView(entityView) { - var deferred = $q.defer(); - var url = '/api/entityView'; - - $http.post(url, entityView).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function deleteEntityView(entityViewId) { - var deferred = $q.defer(); - var url = '/api/entityView/' + entityViewId; - $http.delete(url).then(function success() { - deferred.resolve(); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function assignEntityViewToCustomer(customerId, entityViewId) { - var deferred = $q.defer(); - var url = '/api/customer/' + customerId + '/entityView/' + entityViewId; - $http.post(url, null).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function unassignEntityViewFromCustomer(entityViewId) { - var deferred = $q.defer(); - var url = '/api/customer/entityView/' + entityViewId; - $http.delete(url).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getEntityViewAttributes(entityViewId, attributeScope, query, successCallback, config) { - return attributeService.getEntityAttributes(types.entityType.entityView, entityViewId, attributeScope, query, successCallback, config); - } - - function subscribeForEntityViewAttributes(entityViewId, attributeScope) { - return attributeService.subscribeForEntityAttributes(types.entityType.entityView, entityViewId, attributeScope); - } - - function unsubscribeForEntityViewAttributes(subscriptionId) { - attributeService.unsubscribeForEntityAttributes(subscriptionId); - } - - function findByQuery(query, ignoreErrors, config) { - var deferred = $q.defer(); - var url = '/api/entityViews'; - if (!config) { - config = {}; - } - config = Object.assign(config, { ignoreErrors: ignoreErrors }); - $http.post(url, query, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getEntityViewTypes(config) { - var deferred = $q.defer(); - var url = '/api/entityView/types'; - $http.get(url, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function makeEntityViewPublic(entityViewId) { - var deferred = $q.defer(); - var url = '/api/customer/public/entityView/' + entityViewId; - $http.post(url, null).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - -} diff --git a/ui/src/app/api/entity.service.js b/ui/src/app/api/entity.service.js deleted file mode 100644 index 30f7b7ae65..0000000000 --- a/ui/src/app/api/entity.service.js +++ /dev/null @@ -1,1616 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import thingsboardTypes from '../common/types.constant'; - -export default angular.module('thingsboard.api.entity', [thingsboardTypes]) - .factory('entityService', EntityService) - .name; - -/*@ngInject*/ -function EntityService($http, $q, $filter, $translate, $log, userService, deviceService, assetService, tenantService, - customerService, ruleChainService, dashboardService, entityRelationService, attributeService, - entityViewService, types, utils) { - var service = { - getEntity: getEntity, - getEntities: getEntities, - getEntitiesByNameFilter: getEntitiesByNameFilter, - resolveAlias: resolveAlias, - resolveAliasFilter: resolveAliasFilter, - checkEntityAlias: checkEntityAlias, - filterAliasByEntityTypes: filterAliasByEntityTypes, - getAliasFilterTypesByEntityTypes: getAliasFilterTypesByEntityTypes, - prepareAllowedEntityTypesList: prepareAllowedEntityTypesList, - getEntityKeys: getEntityKeys, - createDatasourcesFromSubscriptionsInfo: createDatasourcesFromSubscriptionsInfo, - createAlarmSourceFromSubscriptionInfo: createAlarmSourceFromSubscriptionInfo, - getRelatedEntities: getRelatedEntities, - saveRelatedEntity: saveRelatedEntity, - saveEntityParameters: saveEntityParameters, - getRelatedEntity: getRelatedEntity, - deleteRelatedEntity: deleteRelatedEntity, - moveEntity: moveEntity, - copyEntity: copyEntity - }; - - return service; - - function getEntityPromise(entityType, entityId, config) { - var promise; - switch (entityType) { - case types.entityType.device: - promise = deviceService.getDevice(entityId, true, config); - break; - case types.entityType.asset: - promise = assetService.getAsset(entityId, true, config); - break; - case types.entityType.entityView: - promise = entityViewService.getEntityView(entityId, true, config); - break; - case types.entityType.tenant: - promise = tenantService.getTenant(entityId, config); - break; - case types.entityType.customer: - promise = customerService.getCustomer(entityId, config); - break; - case types.entityType.dashboard: - promise = dashboardService.getDashboardInfo(entityId, config); - break; - case types.entityType.user: - promise = userService.getUser(entityId, true, config); - break; - case types.entityType.rulechain: - promise = ruleChainService.getRuleChain(entityId, config); - break; - case types.entityType.alarm: - $log.error('Get Alarm Entity is not implemented!'); - break; - } - return promise; - } - - function getEntity(entityType, entityId, config) { - var deferred = $q.defer(); - var promise = getEntityPromise(entityType, entityId, config); - if (promise) { - promise.then( - function success(result) { - deferred.resolve(result); - }, - function fail() { - deferred.reject(); - } - ); - } else { - deferred.reject(); - } - return deferred.promise; - } - - function getEntitiesByIdsPromise(fetchEntityFunction, entityIds) { - var tasks = []; - var deferred = $q.defer(); - for (var i=0;i entityViewService.getEntityView(id, config), entityIds); - break; - case types.entityType.tenant: - promise = getEntitiesByIdsPromise( - (id) => tenantService.getTenant(id, config), entityIds); - break; - case types.entityType.customer: - promise = getEntitiesByIdsPromise( - (id) => customerService.getCustomer(id, config), entityIds); - break; - case types.entityType.dashboard: - promise = getEntitiesByIdsPromise( - (id) => dashboardService.getDashboardInfo(id, config), entityIds); - break; - case types.entityType.user: - promise = getEntitiesByIdsPromise( - (id) => userService.getUser(id, true, config), entityIds); - break; - case types.entityType.alarm: - $log.error('Get Alarm Entity is not implemented!'); - break; - } - return promise; - } - - function getEntities(entityType, entityIds, config) { - var deferred = $q.defer(); - var promise = getEntitiesPromise(entityType, entityIds, config); - if (promise) { - promise.then( - function success(result) { - deferred.resolve(result); - }, - function fail() { - deferred.reject(); - } - ); - } else { - deferred.reject(); - } - return deferred.promise; - } - - function getSingleTenantByPageLinkPromise(pageLink, config) { - var user = userService.getCurrentUser(); - var tenantId = user.tenantId; - var deferred = $q.defer(); - tenantService.getTenant(tenantId, config).then( - function success(tenant) { - var tenantName = tenant.name; - var result = { - data: [], - nextPageLink: pageLink, - hasNext: false - }; - if (tenantName.toLowerCase().startsWith(pageLink.textSearch)) { - result.data.push(tenant); - } - deferred.resolve(result); - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - - function getSingleCustomerByPageLinkPromise(pageLink, config) { - var user = userService.getCurrentUser(); - var customerId = user.customerId; - var deferred = $q.defer(); - customerService.getCustomer(customerId, config).then( - function success(customer) { - var customerName = customer.name; - var result = { - data: [], - nextPageLink: pageLink, - hasNext: false - }; - if (customerName.toLowerCase().startsWith(pageLink.textSearch)) { - result.data.push(customer); - } - deferred.resolve(result); - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - - function getEntitiesByPageLinkPromise(entityType, pageLink, config, subType) { - var promise; - var user = userService.getCurrentUser(); - var customerId = user.customerId; - switch (entityType) { - case types.entityType.device: - if (user.authority === 'CUSTOMER_USER') { - promise = deviceService.getCustomerDevices(customerId, pageLink, false, config, subType); - } else { - promise = deviceService.getTenantDevices(pageLink, false, config, subType); - } - break; - case types.entityType.asset: - if (user.authority === 'CUSTOMER_USER') { - promise = assetService.getCustomerAssets(customerId, pageLink, false, config, subType); - } else { - promise = assetService.getTenantAssets(pageLink, false, config, subType); - } - break; - case types.entityType.entityView: - if (user.authority === 'CUSTOMER_USER') { - promise = entityViewService.getCustomerEntityViews(customerId, pageLink, false, config, subType); - } else { - promise = entityViewService.getTenantEntityViews(pageLink, false, config, subType); - } - break; - case types.entityType.tenant: - if (user.authority === 'TENANT_ADMIN') { - promise = getSingleTenantByPageLinkPromise(pageLink, config); - } else { - promise = tenantService.getTenants(pageLink, config); - } - break; - case types.entityType.customer: - if (user.authority === 'CUSTOMER_USER') { - promise = getSingleCustomerByPageLinkPromise(pageLink, config); - } else { - promise = customerService.getCustomers(pageLink, config); - } - break; - case types.entityType.rulechain: - promise = ruleChainService.getRuleChains(pageLink, config); - break; - case types.entityType.dashboard: - if (user.authority === 'CUSTOMER_USER') { - promise = dashboardService.getCustomerDashboards(customerId, pageLink, config); - } else { - promise = dashboardService.getTenantDashboards(pageLink, config); - } - break; - case types.entityType.user: - $log.error('Get User Entities is not implemented!'); - break; - case types.entityType.alarm: - $log.error('Get Alarm Entities is not implemented!'); - break; - } - return promise; - } - - function getEntitiesByPageLink(entityType, pageLink, config, subType, data, deferred) { - var promise = getEntitiesByPageLinkPromise(entityType, pageLink, config, subType); - if (promise) { - promise.then( - function success(result) { - data = data.concat(result.data); - if (result.hasNext) { - pageLink = result.nextPageLink; - getEntitiesByPageLink(entityType, pageLink, config, subType, data, deferred); - } else { - if (data && data.length > 0) { - deferred.resolve(data); - } else { - deferred.resolve(null); - } - } - }, - function fail() { - deferred.resolve(null); - } - ); - } else { - deferred.resolve(null); - } - } - - function getEntitiesByNameFilter(entityType, entityNameFilter, limit, config, subType) { - var deferred = $q.defer(); - var pageLink = {limit: limit, textSearch: entityNameFilter}; - if (limit == -1) { // all - var data = []; - pageLink.limit = 100; - getEntitiesByPageLink(entityType, pageLink, config, subType, data, deferred); - } else { - var promise = getEntitiesByPageLinkPromise(entityType, pageLink, config, subType); - if (promise) { - promise.then( - function success(result) { - if (result.data && result.data.length > 0) { - deferred.resolve(result.data); - } else { - deferred.resolve(null); - } - }, - function fail() { - deferred.resolve(null); - } - ); - } else { - deferred.resolve(null); - } - } - return deferred.promise; - } - - function entityToEntityInfo(entity) { - return { - origEntity: entity, - name: entity.name, - label: entity.label?entity.label:"", - entityType: entity.id.entityType, - id: entity.id.id, - entityDescription: entity.additionalInfo?entity.additionalInfo.description:"" - }; - } - - function entityRelationInfoToEntityInfoFunc(entityRelationInfo, direction) { - return function() { - var deferred = $q.defer(); - var entityId = direction == types.entitySearchDirection.from ? entityRelationInfo.to : entityRelationInfo.from; - getEntity(entityId.entityType, entityId.id, {ignoreLoading: true}).then( - function success(entity) { - deferred.resolve(entityToEntityInfo(entity)); - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - } - - function entitiesToEntitiesInfo(entities) { - var entitiesInfo = []; - if (entities) { - for (var d = 0; d < entities.length; d++) { - entitiesInfo.push(entityToEntityInfo(entities[d])); - } - } - return entitiesInfo; - } - - function entityRelationInfosToEntitiesInfo(entityRelations, direction) { - var deferred = $q.defer(); - if (entityRelations && entityRelations.length) { - var entityInfoTasks = []; - for (var d = 0; d < entityRelations.length; d++) { - entityInfoTasks.push(entityRelationInfoToEntityInfoFunc(entityRelations[d], direction)); - } - sendPackedRequests(entityInfoTasks).then( - function success(entityInfos) { - deferred.resolve(entityInfos); - }, function fail () { - deferred.reject(); - } - ); - } else { - deferred.resolve([]); - } - return deferred.promise; - } - - function sendPackedRequests(taskFunctions) { - var taskFunctionPacks = []; - for (var e = 0; e < taskFunctions.length; e+=100) { - taskFunctionPacks.push(taskFunctions.slice(e,e + 100)); - } - var deferred = $q.defer(); - var resolvePack = []; - doSendPackedRequests(taskFunctionPacks, 0, resolvePack, deferred); - return deferred.promise; - } - - function doSendPackedRequests (functionPack, index, resolvePack, deferred) { - var tasks = []; - var currentFunctionPack = functionPack[index]; - for (var i=0;i 0 ? filter.maxLevel : -1; - entityRelationService.findInfoByQuery(searchQuery, {ignoreLoading: true}).then( - function success(allRelations) { - if (allRelations && allRelations.length || !failOnEmpty) { - if (angular.isDefined(maxItems) && maxItems > 0 && allRelations) { - var limit = Math.min(allRelations.length, maxItems); - allRelations.length = limit; - } - entityRelationInfosToEntitiesInfo(allRelations, filter.direction).then( - function success(entities) { - result.entities = entities; - deferred.resolve(result); - }, - function fail() { - deferred.reject(); - } - ); - } else { - deferred.reject(); - } - }, - function fail() { - deferred.reject(); - } - ); - } else { - deferred.resolve(result); - } - break; - case types.aliasFilterType.assetSearchQuery.value: - case types.aliasFilterType.deviceSearchQuery.value: - case types.aliasFilterType.entityViewSearchQuery.value: - result.stateEntity = filter.rootStateEntity; - if (result.stateEntity && stateEntityId) { - rootEntityType = stateEntityId.entityType; - rootEntityId = stateEntityId.id; - } else if (!result.stateEntity) { - rootEntityType = filter.rootEntity.entityType; - rootEntityId = filter.rootEntity.id; - } - if (rootEntityType && rootEntityId) { - var searchQueryRootEntityId = resolveAliasEntityId(rootEntityType, rootEntityId); - searchQuery = { - parameters: { - rootId: searchQueryRootEntityId.id, - rootType: searchQueryRootEntityId.entityType, - direction: filter.direction, - fetchLastLevelOnly: filter.fetchLastLevelOnly - }, - relationType: filter.relationType - }; - searchQuery.parameters.maxLevel = filter.maxLevel && filter.maxLevel > 0 ? filter.maxLevel : -1; - var findByQueryPromise; - if (filter.type == types.aliasFilterType.assetSearchQuery.value) { - searchQuery.assetTypes = filter.assetTypes; - findByQueryPromise = assetService.findByQuery(searchQuery, false, {ignoreLoading: true}); - } else if (filter.type == types.aliasFilterType.deviceSearchQuery.value) { - searchQuery.deviceTypes = filter.deviceTypes; - findByQueryPromise = deviceService.findByQuery(searchQuery, false, {ignoreLoading: true}); - } else if (filter.type == types.aliasFilterType.entityViewSearchQuery.value) { - searchQuery.entityViewTypes = filter.entityViewTypes; - findByQueryPromise = entityViewService.findByQuery(searchQuery, false, {ignoreLoading: true}); - } - findByQueryPromise.then( - function success(entities) { - if (entities && entities.length || !failOnEmpty) { - if (angular.isDefined(maxItems) && maxItems > 0 && entities) { - var limit = Math.min(entities.length, maxItems); - entities.length = limit; - } - result.entities = entitiesToEntitiesInfo(entities); - deferred.resolve(result); - } else { - deferred.reject(); - } - }, - function fail() { - deferred.reject(); - } - ); - } else { - deferred.resolve(result); - } - break; - } - return deferred.promise; - } - - function filterAliasByEntityTypes(entityAlias, entityTypes) { - var filter = entityAlias.filter; - if (filterAliasFilterTypeByEntityTypes(filter.type, entityTypes)) { - switch (filter.type) { - case types.aliasFilterType.singleEntity.value: - return entityTypes.indexOf(filter.singleEntity.entityType) > -1 ? true : false; - case types.aliasFilterType.entityList.value: - return entityTypes.indexOf(filter.entityType) > -1 ? true : false; - case types.aliasFilterType.entityName.value: - return entityTypes.indexOf(filter.entityType) > -1 ? true : false; - case types.aliasFilterType.stateEntity.value: - return true; - case types.aliasFilterType.assetType.value: - return entityTypes.indexOf(types.entityType.asset) > -1 ? true : false; - case types.aliasFilterType.deviceType.value: - return entityTypes.indexOf(types.entityType.device) > -1 ? true : false; - case types.aliasFilterType.entityViewType.value: - return entityTypes.indexOf(types.entityType.entityView) > -1 ? true : false; - case types.aliasFilterType.relationsQuery.value: - if (filter.filters && filter.filters.length) { - var match = false; - for (var f=0;f -1) { - match = true; - break; - } - } - } else { - match = true; - break; - } - } - return match; - } else { - return true; - } - case types.aliasFilterType.assetSearchQuery.value: - return entityTypes.indexOf(types.entityType.asset) > -1 ? true : false; - case types.aliasFilterType.deviceSearchQuery.value: - return entityTypes.indexOf(types.entityType.device) > -1 ? true : false; - case types.aliasFilterType.entityViewSearchQuery.value: - return entityTypes.indexOf(types.entityType.entityView) > -1 ? true : false; - } - } - return false; - } - - function filterAliasFilterTypeByEntityType(aliasFilterType, entityType) { - switch (aliasFilterType) { - case types.aliasFilterType.singleEntity.value: - return true; - case types.aliasFilterType.entityList.value: - return true; - case types.aliasFilterType.entityName.value: - return true; - case types.aliasFilterType.stateEntity.value: - return true; - case types.aliasFilterType.assetType.value: - return entityType === types.entityType.asset; - case types.aliasFilterType.deviceType.value: - return entityType === types.entityType.device; - case types.aliasFilterType.entityViewType.value: - return entityType === types.entityType.entityView; - case types.aliasFilterType.relationsQuery.value: - return true; - case types.aliasFilterType.assetSearchQuery.value: - return entityType === types.entityType.asset; - case types.aliasFilterType.deviceSearchQuery.value: - return entityType === types.entityType.device; - case types.aliasFilterType.entityViewSearchQuery.value: - return entityType === types.entityType.entityView; - } - return false; - } - - function filterAliasFilterTypeByEntityTypes(aliasFilterType, entityTypes) { - if (!entityTypes || !entityTypes.length) { - return true; - } - var valid = false; - entityTypes.forEach(function(entityType) { - valid = valid || filterAliasFilterTypeByEntityType(aliasFilterType, entityType); - }); - return valid; - } - - function getAliasFilterTypesByEntityTypes(entityTypes) { - var allAliasFilterTypes = types.aliasFilterType; - if (!entityTypes || !entityTypes.length) { - return allAliasFilterTypes; - } - var result = {}; - for (var type in allAliasFilterTypes) { - var aliasFilterType = allAliasFilterTypes[type]; - if (filterAliasFilterTypeByEntityTypes(aliasFilterType.value, entityTypes)) { - result[type] = aliasFilterType; - } - } - return result; - } - - function prepareAllowedEntityTypesList(allowedEntityTypes, useAliasEntityTypes) { - var authority = userService.getAuthority(); - var entityTypes = {}; - switch(authority) { - case 'SYS_ADMIN': - entityTypes.tenant = types.entityType.tenant; - break; - case 'TENANT_ADMIN': - entityTypes.device = types.entityType.device; - entityTypes.asset = types.entityType.asset; - entityTypes.entityView = types.entityType.entityView; - entityTypes.tenant = types.entityType.tenant; - entityTypes.customer = types.entityType.customer; - entityTypes.dashboard = types.entityType.dashboard; - if (useAliasEntityTypes) { - entityTypes.current_customer = types.aliasEntityType.current_customer; - entityTypes.current_tenant = types.aliasEntityType.current_tenant; - } - break; - case 'CUSTOMER_USER': - entityTypes.device = types.entityType.device; - entityTypes.asset = types.entityType.asset; - entityTypes.entityView = types.entityType.entityView; - entityTypes.customer = types.entityType.customer; - entityTypes.dashboard = types.entityType.dashboard; - if (useAliasEntityTypes) { - entityTypes.current_customer = types.aliasEntityType.current_customer; - } - break; - } - - if (allowedEntityTypes) { - for (var entityType in entityTypes) { - if (allowedEntityTypes.indexOf(entityTypes[entityType]) === -1) { - delete entityTypes[entityType]; - } - } - } - return entityTypes; - } - - - function checkEntityAlias(entityAlias) { - var deferred = $q.defer(); - resolveAliasFilter(entityAlias.filter, null, 1, true).then( - function success(result) { - if (result.stateEntity) { - deferred.resolve(true); - } else { - var entities = result.entities; - if (entities && entities.length) { - deferred.resolve(true); - } else { - deferred.resolve(false); - } - } - }, - function fail() { - deferred.resolve(false); - } - ); - return deferred.promise; - } - - function getEntityFieldKeys (entityType, searchText) { - let entityFieldKeys = []; - let query = searchText ? searchText.toLowerCase() : ""; - switch(entityType) { - case types.entityType.user: - entityFieldKeys.push(types.entityField.name.keyName); - entityFieldKeys.push(types.entityField.email.keyName); - entityFieldKeys.push(types.entityField.firstName.keyName); - entityFieldKeys.push(types.entityField.lastName.keyName); - break; - case types.entityType.tenant: - case types.entityType.customer: - entityFieldKeys.push(types.entityField.title.keyName); - entityFieldKeys.push(types.entityField.email.keyName); - entityFieldKeys.push(types.entityField.country.keyName); - entityFieldKeys.push(types.entityField.state.keyName); - entityFieldKeys.push(types.entityField.city.keyName); - entityFieldKeys.push(types.entityField.address.keyName); - entityFieldKeys.push(types.entityField.address2.keyName); - entityFieldKeys.push(types.entityField.zip.keyName); - entityFieldKeys.push(types.entityField.phone.keyName); - break; - case types.entityType.entityView: - entityFieldKeys.push(types.entityField.name.keyName); - entityFieldKeys.push(types.entityField.type.keyName); - break; - case types.entityType.device: - case types.entityType.asset: - entityFieldKeys.push(types.entityField.name.keyName); - entityFieldKeys.push(types.entityField.type.keyName); - entityFieldKeys.push(types.entityField.label.keyName); - break; - case types.entityType.dashboard: - entityFieldKeys.push(types.entityField.title.keyName); - break; - } - - return query ? entityFieldKeys.filter((entityField) => entityField.toLowerCase().indexOf(query) === 0) : entityFieldKeys; - } - - function getEntityKeys(entityType, entityId, query, type, config) { - if (type === types.dataKeyType.entityField) { - return $q.when(getEntityFieldKeys(entityType, query)); - } - var deferred = $q.defer(); - var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/keys/'; - if (type === types.dataKeyType.timeseries) { - url += 'timeseries'; - } else if (type === types.dataKeyType.attribute) { - url += 'attributes'; - } - $http.get(url, config).then(function success(response) { - var result = []; - if (response.data) { - if (query) { - var dataKeys = response.data; - var lowercaseQuery = angular.lowercase(query); - for (var i=0; i 0) { - for (var i = 0; i < entities.length; i++) { - var entity = entities[i]; - createDatasourceFromSubscription(subscriptionInfo, datasources, entity); - } - } - index++; - processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred); - }, - function fail() { - index++; - processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred); - } - ) - } else { - index++; - processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred); - } - } else if (subscriptionInfo.type === types.datasourceType.function) { - createDatasourceFromSubscription(subscriptionInfo, datasources); - index++; - processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred); - } - } else { - deferred.resolve(datasources); - } - } - - function validateSubscriptionInfo(subscriptionInfo) { - if (subscriptionInfo.type === 'device') { - subscriptionInfo.type = types.datasourceType.entity; - subscriptionInfo.entityType = types.entityType.device; - if (subscriptionInfo.deviceId) { - subscriptionInfo.entityId = subscriptionInfo.deviceId; - } else if (subscriptionInfo.deviceName) { - subscriptionInfo.entityName = subscriptionInfo.deviceName; - } else if (subscriptionInfo.deviceNamePrefix) { - subscriptionInfo.entityNamePrefix = subscriptionInfo.deviceNamePrefix; - } else if (subscriptionInfo.deviceIds) { - subscriptionInfo.entityIds = subscriptionInfo.deviceIds; - } - } - return subscriptionInfo; - } - - function createDatasourceFromSubscription(subscriptionInfo, datasources, entity) { - var datasource; - if (subscriptionInfo.type === types.datasourceType.entity) { - datasource = { - type: subscriptionInfo.type, - entityName: entity.name ? entity.name : entity.title, - name: entity.name ? entity.name : entity.title, - entityType: subscriptionInfo.entityType, - entityId: entity.id.id, - dataKeys: [] - } - } else if (subscriptionInfo.type === types.datasourceType.function) { - datasource = { - type: subscriptionInfo.type, - name: subscriptionInfo.name || types.datasourceType.function, - dataKeys: [] - } - } - datasources.push(datasource); - if (subscriptionInfo.timeseries) { - createDatasourceKeys(subscriptionInfo.timeseries, types.dataKeyType.timeseries, datasource, datasources); - } - if (subscriptionInfo.attributes) { - createDatasourceKeys(subscriptionInfo.attributes, types.dataKeyType.attribute, datasource, datasources); - } - if (subscriptionInfo.functions) { - createDatasourceKeys(subscriptionInfo.functions, types.dataKeyType.function, datasource, datasources); - } - if (subscriptionInfo.alarmFields) { - createDatasourceKeys(subscriptionInfo.alarmFields, types.dataKeyType.alarm, datasource, datasources); - } - } - - function createDatasourceKeys(keyInfos, type, datasource, datasources) { - for (var i=0;i -1) { - var deleteRelatedEntityPromise = deleteRelatedEntity(relationEntityId, deleteRelatedEntityTypes); - deleteRelatedEntitiesTasks.push(deleteRelatedEntityPromise); - } - } - deleteRelatedEntitiesTasks.push(deleteEntityPromise(entityId)); - $q.all(deleteRelatedEntitiesTasks).then( - function success() { - deferred.resolve(); - }, - function fail() { - deferred.reject(); - } - ); - }, - function fail() { - deferred.reject(); - } - ) - } else { - deleteEntityPromise(entityId).then( - function success() { - deferred.resolve(); - }, - function fail() { - deferred.reject(); - } - ); - } - return deferred.promise; - } - - function moveEntity(entityId, prevParentId, targetParentId) { - var deferred = $q.defer(); - entityRelationService.deleteRelation(prevParentId.id, prevParentId.entityType, - types.entityRelationType.contains, entityId.id, entityId.entityType).then( - function success() { - var relation = { - from: targetParentId, - to: entityId, - type: types.entityRelationType.contains - }; - entityRelationService.saveRelation(relation).then( - function success() { - deferred.resolve(); - }, - function fail() { - deferred.reject(); - } - ); - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - - function copyEntity(entity, targetParentId, keys) { - var deferred = $q.defer(); - if (!entity.id && !entity.id.id) { - deferred.reject(); - } else { - getRelatedEntity(entity.id, keys).then( - function success(relatedEntity) { - delete relatedEntity.id.id; - relatedEntity.name = entity.name; - saveRelatedEntity(relatedEntity, targetParentId, keys).then( - function success(savedEntity) { - deferred.resolve(savedEntity); - }, - function fail() { - deferred.reject(); - } - ); - }, - function fail() { - deferred.reject(); - } - ); - } - return deferred.promise; - } - - function saveEntityPromise(entity) { - var entityType = entity.id.entityType; - if (!entity.id.id) { - delete entity.id; - } - if (entityType == types.entityType.asset) { - return assetService.saveAsset(entity); - } else if (entityType == types.entityType.device) { - return deviceService.saveDevice(entity); - } else if (entityType == types.entityType.entityView) { - return entityViewService.saveEntityView(entity); - } - } - - function addRelatedEntity(relatedEntity, parentEntityId, keys, deferred, relation, direction) { - var entity = {}; - entity.id = relatedEntity.id; - entity.name = relatedEntity.name; - entity.type = relatedEntity.type; - saveEntityPromise(entity).then( - function success(entity) { - relatedEntity.id = entity.id; - if (!relation) { - relation = { - type: types.entityRelationType.contains - }; - } - - if (direction == types.entitySearchDirection.from) { - relation.from = parentEntityId; - relation.to = relatedEntity.id; - } else { - relation.from = relatedEntity.id; - relation.to = parentEntityId; - } - - entityRelationService.saveRelation(relation).then( - function success() { - updateEntity(entity, relatedEntity, keys, deferred, relation); - }, - function fail() { - deferred.reject(); - } - ); - }, - function fail() { - deferred.reject(); - } - ); - } - - function updateRelatedEntity(relatedEntity, keys, deferred, relation, direction) { - getEntityPromise(relatedEntity.id.entityType, relatedEntity.id.id, {ignoreLoading: true}).then( - function success(entity) { - if (relation) { - if (direction == types.entitySearchDirection.from) { - relation.to = relatedEntity.id; - } else { - relation.from = relatedEntity.id; - } - entityRelationService.saveRelation(relation).then( - function success() { - updateEntity(entity, relatedEntity, keys, deferred); - }, - function fail() { - deferred.reject(); - } - ); - } else { - updateEntity(entity, relatedEntity, keys, deferred); - } - }, - function fail() { - deferred.reject(); - } - ); - } - - function updateEntity(entity, relatedEntity, keys, deferred) { - if (!angular.equals(entity.name, relatedEntity.name) || !angular.equals(entity.type, relatedEntity.type)) { - entity.name = relatedEntity.name; - entity.type = relatedEntity.type; - saveEntityPromise(entity).then( - function success (entity) { - updateEntityAttributes(entity, relatedEntity, keys, deferred); - }, - function fail() { - deferred.reject(); - } - ); - } else { - updateEntityAttributes(entity, relatedEntity, keys, deferred); - } - } - - function updateEntityAttributes(entity, relatedEntity, keys, deferred) { - var attributes = []; - for (var i = 0; i < keys.length; i++) { - var key = keys[i]; - attributes.push({key: key, value: relatedEntity[key]}); - } - attributeService.saveEntityAttributes(entity.id.entityType, entity.id.id, types.attributesScope.server.value, attributes) - .then( - function success() { - deferred.resolve(relatedEntity); - }, - function fail() { - deferred.reject(); - } - ); - } - - function constructRelatedEntitiesSearchQuery(rootEntityId, entityType, entitySubTypes, maxLevel, relationType, direction, fetchLastLevelOnly) { - - var searchQuery = { - parameters: { - rootId: rootEntityId.id, - rootType: rootEntityId.entityType, - direction: direction, - fetchLastLevelOnly: !!fetchLastLevelOnly - }, - relationType: relationType - }; - if (!direction) { - searchQuery.parameters.direction = types.entitySearchDirection.from; - } - if (!relationType) { - searchQuery.relationType = types.entityRelationType.contains; - } - - if (maxLevel) { - searchQuery.parameters.maxLevel = maxLevel; - } else { - searchQuery.parameters.maxLevel = 1; - } - - if (entityType == types.entityType.asset) { - searchQuery.assetTypes = entitySubTypes; - } else if (entityType == types.entityType.device) { - searchQuery.deviceTypes = entitySubTypes; - } else if (entityType == types.entityType.entityView) { - searchQuery.entityViewTypes = entitySubTypes; - } else { - return null; //Not supported - } - - return searchQuery; - } - - function constructEntity(entity, keys, typeTranslatePrefix) { - var deferred = $q.defer(); - if (typeTranslatePrefix) { - entity.typeName = $translate.instant(typeTranslatePrefix+'.'+entity.type); - } else { - entity.typeName = entity.type; - } - attributeService.getEntityAttributesValues(entity.id.entityType, entity.id.id, - types.attributesScope.server.value, keys.join(','), - {ignoreLoading: true}).then( - function success(attributes) { - if (attributes && attributes.length > 0) { - for (var i=0;i 0) { - return foundAttributes[0].value; - } else { - return null; - } - } - -} diff --git a/ui/src/app/api/event.service.js b/ui/src/app/api/event.service.js deleted file mode 100644 index 99cdae3e47..0000000000 --- a/ui/src/app/api/event.service.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default angular.module('thingsboard.api.event', []) - .factory('eventService', EventService) - .name; - -/*@ngInject*/ -function EventService($http, $q) { - - var service = { - getEvents: getEvents - } - - return service; - - function getEvents (entityType, entityId, eventType, tenantId, pageLink) { - var deferred = $q.defer(); - var url = '/api/events/'+entityType+'/'+entityId+'/'+eventType+'?tenantId=' + tenantId + '&limit=' + pageLink.limit; - - if (angular.isDefined(pageLink.startTime) && pageLink.startTime != null) { - url += '&startTime=' + pageLink.startTime; - } - if (angular.isDefined(pageLink.endTime) && pageLink.endTime != null) { - url += '&endTime=' + pageLink.endTime; - } - if (angular.isDefined(pageLink.idOffset) && pageLink.idOffset != null) { - url += '&offset=' + pageLink.idOffset; - } - $http.get(url, null).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - -} diff --git a/ui/src/app/api/login.service.js b/ui/src/app/api/login.service.js deleted file mode 100644 index 292268642f..0000000000 --- a/ui/src/app/api/login.service.js +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default angular.module('thingsboard.api.login', []) - .factory('loginService', LoginService) - .name; - -/*@ngInject*/ -function LoginService($http, $q, $rootScope) { - - var service = { - activate: activate, - changePassword: changePassword, - hasUser: hasUser, - login: login, - publicLogin: publicLogin, - resetPassword: resetPassword, - sendResetPasswordLink: sendResetPasswordLink, - loadOAuth2Clients: loadOAuth2Clients - } - - return service; - - function hasUser() { - return true; - } - - function login(user) { - var deferred = $q.defer(); - var loginRequest = { - username: user.name, - password: user.password - }; - $http.post('/api/auth/login', loginRequest).then(function success(response) { - deferred.resolve(response); - }, function fail(response) { - deferred.reject(response); - }); - return deferred.promise; - } - - function publicLogin(publicId) { - var deferred = $q.defer(); - var pubilcLoginRequest = { - publicId: publicId - }; - $http.post('/api/auth/login/public', pubilcLoginRequest).then(function success(response) { - deferred.resolve(response); - }, function fail(response) { - deferred.reject(response); - }); - return deferred.promise; - } - - function sendResetPasswordLink(email) { - var deferred = $q.defer(); - var url = '/api/noauth/resetPasswordByEmail'; - $http.post(url, {email: email}).then(function success(response) { - deferred.resolve(response); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function resetPassword(resetToken, password) { - var deferred = $q.defer(); - var url = '/api/noauth/resetPassword'; - $http.post(url, {resetToken: resetToken, password: password}).then(function success(response) { - deferred.resolve(response); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function activate(activateToken, password, sendActivationMail) { - var deferred = $q.defer(); - var url = '/api/noauth/activate'; - if(sendActivationMail === true || sendActivationMail === false) { - url += '?sendActivationMail=' + sendActivationMail; - } - $http.post(url, {activateToken: activateToken, password: password}).then(function success(response) { - deferred.resolve(response); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function changePassword(currentPassword, newPassword) { - var deferred = $q.defer(); - var url = '/api/auth/changePassword'; - $http.post(url, {currentPassword: currentPassword, newPassword: newPassword}).then(function success(response) { - deferred.resolve(response); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function loadOAuth2Clients(){ - var deferred = $q.defer(); - var url = '/api/noauth/oauth2Clients'; - $http.post(url).then(function success(response) { - $rootScope.oauth2Clients = response.data; - deferred.resolve(); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } -} diff --git a/ui/src/app/api/queue.service.js b/ui/src/app/api/queue.service.js deleted file mode 100644 index 0fe93e5fa6..0000000000 --- a/ui/src/app/api/queue.service.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export default angular.module('thingsboard.api.queue', []) - .factory('queueService', queueService) - .name; - -/*@ngInject*/ -function queueService($http, $q) { - var service = { - getTenantQueuesByServiceType: getTenantQueuesByServiceType - }; - - return service; - - function getTenantQueuesByServiceType(serviceType, config) { - let deferred = $q.defer(); - let url = '/api/tenant/queues?serviceType=' + serviceType; - - $http.get(url, config).then(function success(data) { - deferred.resolve(data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } -} \ No newline at end of file diff --git a/ui/src/app/api/rule-chain.service.js b/ui/src/app/api/rule-chain.service.js deleted file mode 100644 index 87d2df5cae..0000000000 --- a/ui/src/app/api/rule-chain.service.js +++ /dev/null @@ -1,302 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default angular.module('thingsboard.api.ruleChain', []) - .factory('ruleChainService', RuleChainService).name; - -/*@ngInject*/ -function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, componentDescriptorService) { - - var ruleNodeComponents = null; - - var service = { - getRuleChains: getRuleChains, - getRuleChain: getRuleChain, - saveRuleChain: saveRuleChain, - setRootRuleChain: setRootRuleChain, - deleteRuleChain: deleteRuleChain, - getRuleChainMetaData: getRuleChainMetaData, - saveRuleChainMetaData: saveRuleChainMetaData, - getRuleNodeComponents: getRuleNodeComponents, - getRuleNodeComponentByClazz: getRuleNodeComponentByClazz, - getRuleNodeSupportedLinks: getRuleNodeSupportedLinks, - ruleNodeAllowCustomLinks: ruleNodeAllowCustomLinks, - resolveTargetRuleChains: resolveTargetRuleChains, - testScript: testScript, - getLatestRuleNodeDebugInput: getLatestRuleNodeDebugInput - }; - - return service; - - function getRuleChains (pageLink, config) { - var deferred = $q.defer(); - var url = '/api/ruleChains?limit=' + pageLink.limit; - if (angular.isDefined(pageLink.textSearch)) { - url += '&textSearch=' + pageLink.textSearch; - } - if (angular.isDefined(pageLink.idOffset)) { - url += '&idOffset=' + pageLink.idOffset; - } - if (angular.isDefined(pageLink.textOffset)) { - url += '&textOffset=' + pageLink.textOffset; - } - $http.get(url, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getRuleChain(ruleChainId, config) { - var deferred = $q.defer(); - var url = '/api/ruleChain/' + ruleChainId; - $http.get(url, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function saveRuleChain(ruleChain) { - var deferred = $q.defer(); - var url = '/api/ruleChain'; - $http.post(url, ruleChain).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function setRootRuleChain(ruleChainId) { - var deferred = $q.defer(); - var url = '/api/ruleChain/' + ruleChainId + '/root'; - $http.post(url).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function deleteRuleChain(ruleChainId) { - var deferred = $q.defer(); - var url = '/api/ruleChain/' + ruleChainId; - $http.delete(url).then(function success() { - deferred.resolve(); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getRuleChainMetaData(ruleChainId, config) { - var deferred = $q.defer(); - var url = '/api/ruleChain/' + ruleChainId + '/metadata'; - $http.get(url, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function saveRuleChainMetaData(ruleChainMetaData) { - var deferred = $q.defer(); - var url = '/api/ruleChain/metadata'; - $http.post(url, ruleChainMetaData).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getRuleNodeSupportedLinks(component) { - var relationTypes = component.configurationDescriptor.nodeDefinition.relationTypes; - var linkLabels = {}; - for (var i=0;i { - resolveRuleNodeComponentsUiResources(components).then( - (components) => { - ruleNodeComponents = components; - ruleNodeComponents.push( - types.ruleChainNodeComponent - ); - ruleNodeComponents.sort( - (comp1, comp2) => { - var result = comp1.type.localeCompare(comp2.type); - if (result == 0) { - result = comp1.name.localeCompare(comp2.name); - } - return result; - } - ); - deferred.resolve(ruleNodeComponents); - }, - () => { - deferred.reject(); - } - ); - }, - () => { - deferred.reject(); - } - ); - } - return deferred.promise; - } - - function resolveRuleNodeComponentsUiResources(components) { - var deferred = $q.defer(); - var tasks = []; - for (var i=0;i { - deferred.resolve(components); - }, - () => { - deferred.resolve(components); - } - ); - return deferred.promise; - } - - function resolveRuleNodeComponentUiResources(component) { - var deferred = $q.defer(); - var uiResources = component.configurationDescriptor.nodeDefinition.uiResources; - if (uiResources && uiResources.length) { - var tasks = []; - for (var i=0;i { - deferred.resolve(component); - }, - () => { - component.configurationDescriptor.nodeDefinition.uiResourceLoadError = $translate.instant('rulenode.ui-resources-load-error'); - deferred.resolve(component); - } - ) - } else { - deferred.resolve(component); - } - return deferred.promise; - } - - function getRuleNodeComponentByClazz(clazz) { - var res = $filter('filter')(ruleNodeComponents, {clazz: clazz}, true); - if (res && res.length) { - return res[0]; - } - var unknownComponent = angular.copy(types.unknownNodeComponent); - unknownComponent.clazz = clazz; - unknownComponent.configurationDescriptor.nodeDefinition.details = "Unknown Rule Node class: " + clazz; - return unknownComponent; - } - - function resolveTargetRuleChains(ruleChainConnections) { - var deferred = $q.defer(); - if (ruleChainConnections && ruleChainConnections.length) { - var tasks = []; - for (var i = 0; i < ruleChainConnections.length; i++) { - tasks.push(resolveRuleChain(ruleChainConnections[i].targetRuleChainId.id)); - } - $q.all(tasks).then( - (ruleChains) => { - var ruleChainsMap = {}; - for (var i = 0; i < ruleChains.length; i++) { - ruleChainsMap[ruleChains[i].id.id] = ruleChains[i]; - } - deferred.resolve(ruleChainsMap); - }, - () => { - deferred.reject(); - } - ); - } else { - deferred.resolve({}); - } - return deferred.promise; - } - - function resolveRuleChain(ruleChainId) { - var deferred = $q.defer(); - getRuleChain(ruleChainId, {ignoreErrors: true}).then( - (ruleChain) => { - deferred.resolve(ruleChain); - }, - () => { - deferred.resolve({ - id: {id: ruleChainId, entityType: types.entityType.rulechain} - }); - } - ); - return deferred.promise; - } - - function loadRuleNodeComponents() { - return componentDescriptorService.getComponentDescriptorsByTypes(types.ruleNodeTypeComponentTypes); - } - - function testScript(inputParams) { - var deferred = $q.defer(); - var url = '/api/ruleChain/testScript'; - $http.post(url, inputParams).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getLatestRuleNodeDebugInput(ruleNodeId) { - var deferred = $q.defer(); - var url = '/api/ruleNode/' + ruleNodeId + '/debugIn'; - $http.get(url).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - -} diff --git a/ui/src/app/api/subscription.js b/ui/src/app/api/subscription.js deleted file mode 100644 index 3f4b0e0183..0000000000 --- a/ui/src/app/api/subscription.js +++ /dev/null @@ -1,1072 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* - options = { - type, - targetDeviceAliasIds, // RPC - targetDeviceIds, // RPC - datasources, - timeWindowConfig, - useDashboardTimewindow, - legendConfig, - decimals, - units, - callbacks - } - */ - -export default class Subscription { - constructor(subscriptionContext, options) { - - this.ctx = subscriptionContext; - this.type = options.type; - this.callbacks = options.callbacks; - this.id = this.ctx.utils.guid(); - this.cafs = {}; - this.registrations = []; - this.hasResolvedData = false; - - var subscription = this; - var deferred = this.ctx.$q.defer(); - - if (this.type === this.ctx.types.widgetType.rpc.value) { - this.callbacks.rpcStateChanged = this.callbacks.rpcStateChanged || function(){}; - this.callbacks.onRpcSuccess = this.callbacks.onRpcSuccess || function(){}; - this.callbacks.onRpcFailed = this.callbacks.onRpcFailed || function(){}; - this.callbacks.onRpcErrorCleared = this.callbacks.onRpcErrorCleared || function(){}; - - this.targetDeviceAliasIds = options.targetDeviceAliasIds; - this.targetDeviceIds = options.targetDeviceIds; - - this.targetDeviceAliasId = null; - this.targetDeviceId = null; - - this.rpcRejection = null; - this.rpcErrorText = null; - this.rpcEnabled = false; - this.executingRpcRequest = false; - this.executingPromises = []; - this.initRpc().then( - function() { - deferred.resolve(subscription); - } - ); - } else if (this.type === this.ctx.types.widgetType.alarm.value) { - this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || function(){}; - this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || function(){}; - this.callbacks.dataLoading = this.callbacks.dataLoading || function(){}; - this.callbacks.timeWindowUpdated = this.callbacks.timeWindowUpdated || function(){}; - this.alarmSource = options.alarmSource; - - this.alarmSearchStatus = angular.isDefined(options.alarmSearchStatus) ? - options.alarmSearchStatus : this.ctx.types.alarmSearchStatus.any; - this.alarmsPollingInterval = angular.isDefined(options.alarmsPollingInterval) ? - options.alarmsPollingInterval : 5000; - this.alarmsMaxCountLoad = angular.isDefined(options.alarmsMaxCountLoad) ? - options.alarmsMaxCountLoad : 0; - this.alarmsFetchSize = angular.isDefined(options.alarmsFetchSize) ? - options.alarmsFetchSize : 100; - - this.alarmSourceListener = null; - this.alarms = []; - - this.originalTimewindow = null; - this.timeWindow = {}; - this.useDashboardTimewindow = options.useDashboardTimewindow; - - if (this.useDashboardTimewindow) { - this.timeWindowConfig = angular.copy(options.dashboardTimewindow); - } else { - this.timeWindowConfig = angular.copy(options.timeWindowConfig); - } - - this.subscriptionTimewindow = null; - - this.loadingData = false; - this.displayLegend = false; - this.initAlarmSubscription().then( - function success() { - deferred.resolve(subscription); - }, - function fail() { - deferred.reject(); - } - ); - } else { - this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || function(){}; - this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || function(){}; - this.callbacks.dataLoading = this.callbacks.dataLoading || function(){}; - this.callbacks.legendDataUpdated = this.callbacks.legendDataUpdated || function(){}; - this.callbacks.timeWindowUpdated = this.callbacks.timeWindowUpdated || function(){}; - - this.datasources = this.ctx.utils.validateDatasources(options.datasources); - this.datasourceListeners = []; - - /* - * data = array of datasourceData - * datasourceData = { - * tbDatasource, - * dataKey, { name, config } - * data = array of [time, value] - * } - */ - this.data = []; - this.hiddenData = []; - this.originalTimewindow = null; - this.timeWindow = {}; - this.useDashboardTimewindow = options.useDashboardTimewindow; - this.stateData = options.stateData; - if (this.useDashboardTimewindow) { - this.timeWindowConfig = angular.copy(options.dashboardTimewindow); - } else { - this.timeWindowConfig = angular.copy(options.timeWindowConfig); - } - - this.subscriptionTimewindow = null; - this.comparisonEnabled = options.comparisonEnabled; - if (this.comparisonEnabled) { - this.timeForComparison = options.timeForComparison; - - this.comparisonTimeWindow = {}; - this.timewindowForComparison = null; - } - - this.units = options.units || ''; - this.decimals = angular.isDefined(options.decimals) ? options.decimals : 2; - - this.loadingData = false; - - if (options.legendConfig) { - this.legendConfig = options.legendConfig; - this.legendData = { - keys: [], - data: [] - }; - this.displayLegend = true; - } else { - this.displayLegend = false; - } - this.caulculateLegendData = this.displayLegend && - this.type === this.ctx.types.widgetType.timeseries.value && - (this.legendConfig.showMin === true || - this.legendConfig.showMax === true || - this.legendConfig.showAvg === true || - this.legendConfig.showTotal === true); - this.initDataSubscription().then( - function success() { - deferred.resolve(subscription); - }, - function fail() { - deferred.reject(); - } - ); - } - - return deferred.promise; - } - - getFirstEntityInfo() { - var entityId, entityName, entityLabel = null; - if (this.type === this.ctx.types.widgetType.rpc.value) { - if (this.targetDeviceId) { - entityId = { - entityType: this.ctx.types.entityType.device, - id: this.targetDeviceId - }; - entityName = this.targetDeviceName; - } - } else if (this.type == this.ctx.types.widgetType.alarm.value) { - if (this.alarmSource && this.alarmSource.entityType && this.alarmSource.entityId) { - entityId = { - entityType: this.alarmSource.entityType, - id: this.alarmSource.entityId - }; - entityName = this.alarmSource.entityName; - entityLabel = this.alarmSource.entityLabel; - } - } else { - for (var i=0;i { - this.timeWindow.stDiff = stDiff; - deferred.resolve(); - }, - () => { - this.timeWindow.stDiff = 0; - deferred.resolve(); - } - ); - } else { - if (this.timeWindow) { - this.timeWindow.stDiff = 0; - } - deferred.resolve(); - } - return deferred.promise; - } - - initAlarmSubscription() { - var deferred = this.ctx.$q.defer(); - var subscription = this; - this.loadStDiff().then(() => { - if (!subscription.ctx.aliasController) { - subscription.hasResolvedData = true; - subscription.configureAlarmsData(); - deferred.resolve(); - } else { - subscription.ctx.aliasController.resolveAlarmSource(subscription.alarmSource).then( - function success(alarmSource) { - subscription.alarmSource = alarmSource; - if (alarmSource) { - subscription.hasResolvedData = true; - } - subscription.configureAlarmsData(); - deferred.resolve(); - }, - function fail() { - deferred.reject(); - } - ); - } - }); - return deferred.promise; - } - - configureAlarmsData() { - var subscription = this; - var registration; - if (this.useDashboardTimewindow) { - registration = this.ctx.$scope.$on('dashboardTimewindowChanged', function (event, newDashboardTimewindow) { - if (!angular.equals(subscription.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) { - subscription.timeWindowConfig = angular.copy(newDashboardTimewindow); - subscription.update(); - } - }); - this.registrations.push(registration); - } else { - this.startWatchingTimewindow(); - } - registration = this.ctx.$scope.$watch(function () { - return subscription.alarmSearchStatus; - }, function (newAlarmSearchStatus, prevAlarmSearchStatus) { - if (!angular.equals(newAlarmSearchStatus, prevAlarmSearchStatus)) { - subscription.update(); - } - }, true); - this.registrations.push(registration); - } - - initDataSubscription() { - var deferred = this.ctx.$q.defer(); - var subscription = this; - this.loadStDiff().then(() => { - if (!subscription.ctx.aliasController) { - subscription.hasResolvedData = true; - subscription.configureData(); - deferred.resolve(); - } else { - subscription.ctx.aliasController.resolveDatasources(subscription.datasources).then( - function success(datasources) { - subscription.datasources = datasources; - if (datasources && datasources.length) { - subscription.hasResolvedData = true; - } - subscription.configureData(); - deferred.resolve(); - }, - function fail() { - subscription.notifyDataLoaded(); - deferred.reject(); - } - ); - } - }); - return deferred.promise; - } - - configureData() { - var additionalDatasources = []; - var dataIndex = 0; - var additionalKeysNumber = 0; - for (var i = 0; i < this.datasources.length; i++) { - var datasource = this.datasources[i]; - var additionalDataKeys = []; - let datasourceAdditionalKeysNumber = 0; - - for (var a = 0; a < datasource.dataKeys.length; a++) { - var dataKey = datasource.dataKeys[a]; - dataKey.hidden = dataKey.settings.hideDataByDefault ? true : false; - dataKey.inLegend = dataKey.settings.removeFromLegend ? false : true; - dataKey.pattern = angular.copy(dataKey.label); - - if (this.comparisonEnabled && dataKey.settings.comparisonSettings && dataKey.settings.comparisonSettings.showValuesForComparison) { - datasourceAdditionalKeysNumber++; - additionalKeysNumber++; - let additionalDataKey = this.ctx.utils.createAdditionalDataKey(dataKey,datasource, this.timeForComparison,this.datasources,additionalKeysNumber); - dataKey.settings.comparisonSettings.color = additionalDataKey.color; - - additionalDataKeys.push(additionalDataKey); - } - - var datasourceData = { - datasource: datasource, - dataKey: dataKey, - data: [] - }; - if (dataKey.type === this.ctx.types.dataKeyType.entityField) { - if(datasource.entity && datasource.entity[this.ctx.types.entityField[dataKey.name].value]){ - datasourceData.data.push([Date.now(), datasource.entity[this.ctx.types.entityField[dataKey.name].value]]); - } - } - this.data.push(datasourceData); - this.hiddenData.push({data: []}); - if (this.displayLegend) { - var legendKey = { - dataKey: dataKey, - dataIndex: dataIndex++ - }; - this.legendData.keys.push(legendKey); - var legendKeyData = { - min: null, - max: null, - avg: null, - total: null, - hidden: false - }; - this.legendData.data.push(legendKeyData); - } - } - - if (datasourceAdditionalKeysNumber > 0) { - let additionalDatasource = angular.copy(datasource); - additionalDatasource.dataKeys = additionalDataKeys; - additionalDatasource.isAdditional = true; - additionalDatasources.push(additionalDatasource); - } - } - - for (var j=0; j < additionalDatasources.length; j++) { - let additionalDatasource = additionalDatasources[j]; - for (var k=0; k < additionalDatasource.dataKeys.length; k++) { - let additionalDataKey = additionalDatasource.dataKeys[k]; - var additionalDatasourceData = { - datasource: additionalDatasource, - dataKey: additionalDataKey, - data: [] - }; - this.data.push(additionalDatasourceData); - this.hiddenData.push({data: []}); - if (this.displayLegend) { - var additionalLegendKey = { - dataKey: additionalDataKey, - dataIndex: dataIndex++ - }; - this.legendData.keys.push(additionalLegendKey); - var additionalLegendKeyData = { - min: null, - max: null, - avg: null, - total: null, - hidden: false - }; - this.legendData.data.push(additionalLegendKeyData); - } - } - } - - this.datasources = this.datasources.concat(additionalDatasources); - - var subscription = this; - var registration; - - if (this.displayLegend) { - this.legendData.keys = this.ctx.$filter('orderBy')(this.legendData.keys, '+label'); - registration = this.ctx.$scope.$watch( - function() { - return subscription.legendData.keys; - }, - function (newValue, oldValue) { - for(var i = 0; i < newValue.length; i++) { - if(newValue[i].dataKey.hidden != oldValue[i].dataKey.hidden) { - subscription.updateDataVisibility(i); - } - } - }, true); - this.registrations.push(registration); - } - - if (this.type === this.ctx.types.widgetType.timeseries.value) { - if (this.useDashboardTimewindow) { - registration = this.ctx.$scope.$on('dashboardTimewindowChanged', function (event, newDashboardTimewindow) { - if (!angular.equals(subscription.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) { - subscription.timeWindowConfig = angular.copy(newDashboardTimewindow); - subscription.update(); - } - }); - this.registrations.push(registration); - } else { - this.startWatchingTimewindow(); - } - } - } - - resetData() { - for (var i=0;i -1) { - this.registrations.splice(index, 1); - } - } - } - - initRpc() { - var deferred = this.ctx.$q.defer(); - if (this.targetDeviceAliasIds && this.targetDeviceAliasIds.length > 0) { - this.targetDeviceAliasId = this.targetDeviceAliasIds[0]; - var subscription = this; - this.ctx.aliasController.getAliasInfo(this.targetDeviceAliasId).then( - function success(aliasInfo) { - if (aliasInfo.currentEntity && aliasInfo.currentEntity.entityType == subscription.ctx.types.entityType.device) { - subscription.targetDeviceId = aliasInfo.currentEntity.id; - subscription.targetDeviceName = aliasInfo.currentEntity.name; - if (subscription.targetDeviceId) { - subscription.rpcEnabled = true; - } else { - subscription.rpcEnabled = subscription.ctx.$scope.widgetEditMode ? true : false; - } - subscription.hasResolvedData = subscription.rpcEnabled; - subscription.callbacks.rpcStateChanged(subscription); - deferred.resolve(); - } else { - subscription.rpcEnabled = false; - subscription.callbacks.rpcStateChanged(subscription); - deferred.resolve(); - } - }, - function fail () { - subscription.rpcEnabled = false; - subscription.callbacks.rpcStateChanged(subscription); - deferred.resolve(); - } - ); - } else { - if (this.targetDeviceIds && this.targetDeviceIds.length > 0) { - this.targetDeviceId = this.targetDeviceIds[0]; - } - if (this.targetDeviceId) { - this.rpcEnabled = true; - } else { - this.rpcEnabled = this.ctx.$scope.widgetEditMode ? true : false; - } - this.hasResolvedData = true; - this.callbacks.rpcStateChanged(this); - deferred.resolve(); - } - return deferred.promise; - } - - clearRpcError() { - this.rpcRejection = null; - this.rpcErrorText = null; - this.callbacks.onRpcErrorCleared(this); - } - - sendOneWayCommand(method, params, timeout) { - return this.sendCommand(true, method, params, timeout); - } - - sendTwoWayCommand(method, params, timeout) { - return this.sendCommand(false, method, params, timeout); - } - - sendCommand(oneWayElseTwoWay, method, params, timeout) { - if (!this.rpcEnabled) { - return this.ctx.$q.reject(); - } - - if (this.rpcRejection && this.rpcRejection.status !== 408) { - this.rpcRejection = null; - this.rpcErrorText = null; - this.callbacks.onRpcErrorCleared(this); - } - - var subscription = this; - - var requestBody = { - method: method, - params: params - }; - - if (timeout && timeout > 0) { - requestBody.timeout = timeout; - } - - var deferred = this.ctx.$q.defer(); - this.executingRpcRequest = true; - this.callbacks.rpcStateChanged(this); - if (this.ctx.$scope.widgetEditMode) { - this.ctx.$timeout(function() { - subscription.executingRpcRequest = false; - subscription.callbacks.rpcStateChanged(subscription); - if (oneWayElseTwoWay) { - deferred.resolve(); - } else { - deferred.resolve(requestBody); - } - }, 500); - } else { - this.executingPromises.push(deferred.promise); - var targetSendFunction = oneWayElseTwoWay ? this.ctx.deviceService.sendOneWayRpcCommand : this.ctx.deviceService.sendTwoWayRpcCommand; - targetSendFunction(this.targetDeviceId, requestBody).then( - function success(responseBody) { - subscription.rpcRejection = null; - subscription.rpcErrorText = null; - var index = subscription.executingPromises.indexOf(deferred.promise); - if (index >= 0) { - subscription.executingPromises.splice( index, 1 ); - } - subscription.executingRpcRequest = subscription.executingPromises.length > 0; - subscription.callbacks.onRpcSuccess(subscription); - deferred.resolve(responseBody); - }, - function fail(rejection) { - var index = subscription.executingPromises.indexOf(deferred.promise); - if (index >= 0) { - subscription.executingPromises.splice( index, 1 ); - } - subscription.executingRpcRequest = subscription.executingPromises.length > 0; - subscription.callbacks.rpcStateChanged(subscription); - if (!subscription.executingRpcRequest || rejection.status === 408) { - subscription.rpcRejection = rejection; - if (rejection.status === 408) { - subscription.rpcErrorText = 'Device is offline.'; - } else { - subscription.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText; - if (rejection.data && rejection.data.length > 0) { - subscription.rpcErrorText += '
    '; - subscription.rpcErrorText += rejection.data; - } - } - subscription.callbacks.onRpcFailed(subscription); - } - deferred.reject(rejection); - } - ); - } - return deferred.promise; - } - - updateDataVisibility(index) { - var hidden = this.legendData.keys[index].dataKey.hidden; - if (hidden) { - this.hiddenData[index].data = this.data[index].data; - this.data[index].data = []; - } else { - this.data[index].data = this.hiddenData[index].data; - this.hiddenData[index].data = []; - } - this.onDataUpdated(); - } - - onAliasesChanged(aliasIds) { - if (this.type === this.ctx.types.widgetType.rpc.value) { - return this.checkRpcTarget(aliasIds); - } else if (this.type === this.ctx.types.widgetType.alarm.value) { - return this.checkAlarmSource(aliasIds); - } else { - return this.checkSubscriptions(aliasIds); - } - } - - onDataUpdated(apply) { - if (this.cafs['dataUpdated']) { - this.cafs['dataUpdated'](); - this.cafs['dataUpdated'] = null; - } - var subscription = this; - this.cafs['dataUpdated'] = this.ctx.tbRaf(function() { - try { - subscription.callbacks.onDataUpdated(subscription, apply); - } catch (e) { - subscription.callbacks.onDataUpdateError(subscription, e); - } - }); - if (apply) { - this.ctx.$scope.$digest(); - } - } - - updateTimewindowConfig(newTimewindow) { - this.timeWindowConfig = newTimewindow; - } - - onResetTimewindow() { - if (this.useDashboardTimewindow) { - this.ctx.dashboardTimewindowApi.onResetTimewindow(); - } else { - if (this.originalTimewindow) { - this.stopWatchingTimewindow(); - this.timeWindowConfig = angular.copy(this.originalTimewindow); - this.originalTimewindow = null; - this.callbacks.timeWindowUpdated(this, this.timeWindowConfig); - this.update(); - this.startWatchingTimewindow(); - } - } - } - - onUpdateTimewindow(startTimeMs, endTimeMs) { - if (this.useDashboardTimewindow) { - this.ctx.dashboardTimewindowApi.onUpdateTimewindow(startTimeMs, endTimeMs); - } else { - this.stopWatchingTimewindow(); - if (!this.originalTimewindow) { - this.originalTimewindow = angular.copy(this.timeWindowConfig); - } - this.timeWindowConfig = this.ctx.timeService.toHistoryTimewindow(this.timeWindowConfig, startTimeMs, endTimeMs); - this.callbacks.timeWindowUpdated(this, this.timeWindowConfig); - this.update(); - this.startWatchingTimewindow(); - } - } - - notifyDataLoading() { - this.loadingData = true; - this.callbacks.dataLoading(this); - } - - notifyDataLoaded() { - this.loadingData = false; - this.callbacks.dataLoading(this); - } - - updateTimewindow() { - this.timeWindow.interval = this.subscriptionTimewindow.aggregation.interval || 1000; - if (this.subscriptionTimewindow.realtimeWindowMs) { - this.timeWindow.maxTime = (moment()).valueOf() + this.timeWindow.stDiff;//eslint-disable-line - this.timeWindow.minTime = this.timeWindow.maxTime - this.subscriptionTimewindow.realtimeWindowMs; - } else if (this.subscriptionTimewindow.fixedWindow) { - this.timeWindow.maxTime = this.subscriptionTimewindow.fixedWindow.endTimeMs; - this.timeWindow.minTime = this.subscriptionTimewindow.fixedWindow.startTimeMs; - } - } - - updateRealtimeSubscription(subscriptionTimewindow) { - if (subscriptionTimewindow) { - this.subscriptionTimewindow = subscriptionTimewindow; - } else { - this.subscriptionTimewindow = - this.ctx.timeService.createSubscriptionTimewindow( - this.timeWindowConfig, - this.timeWindow.stDiff, this.stateData); - } - this.updateTimewindow(); - return this.subscriptionTimewindow; - } - - updateComparisonTimewindow() { - this.comparisonTimeWindow.interval = this.timewindowForComparison.aggregation.interval || 1000; - if (this.timewindowForComparison.realtimeWindowMs) { - this.comparisonTimeWindow.maxTime = moment(this.timeWindow.maxTime).subtract(1, this.timeForComparison).valueOf(); //eslint-disable-line - this.comparisonTimeWindow.minTime = this.comparisonTimeWindow.maxTime - this.timewindowForComparison.realtimeWindowMs; - } else if (this.timewindowForComparison.fixedWindow) { - this.comparisonTimeWindow.maxTime = this.timewindowForComparison.fixedWindow.endTimeMs; - this.comparisonTimeWindow.minTime = this.timewindowForComparison.fixedWindow.startTimeMs; - } - } - - updateSubscriptionForComparison() { - if (!this.subscriptionTimewindow) { - this.subscriptionTimewindow = this.updateRealtimeSubscription(); - } - this.timewindowForComparison = this.ctx.timeService.createTimewindowForComparison(this.subscriptionTimewindow, this.timeForComparison); - this.updateComparisonTimewindow(); - return this.timewindowForComparison; - } - - dataUpdated(sourceData, datasourceIndex, dataKeyIndex, apply) { - for (var x = 0; x < this.datasourceListeners.length; x++) { - this.datasources[x].dataReceived = this.datasources[x].dataReceived === true; - if (this.datasourceListeners[x].datasourceIndex === datasourceIndex && sourceData.data.length > 0) { - this.datasources[x].dataReceived = true; - } - } - this.notifyDataLoaded(); - var update = true; - var currentData; - if (this.displayLegend && this.legendData.keys[datasourceIndex + dataKeyIndex].dataKey.hidden) { - currentData = this.hiddenData[datasourceIndex + dataKeyIndex]; - } else { - currentData = this.data[datasourceIndex + dataKeyIndex]; - } - if (this.type === this.ctx.types.widgetType.latest.value) { - var prevData = currentData.data; - if (!sourceData.data.length) { - update = false; - } else if (prevData && prevData[0] && prevData[0].length > 1 && sourceData.data.length > 0) { - var prevTs = prevData[0][0]; - var prevValue = prevData[0][1]; - if (prevTs === sourceData.data[0][0] && prevValue === sourceData.data[0][1]) { - update = false; - } - } - } - if (update) { - if (this.subscriptionTimewindow && this.subscriptionTimewindow.realtimeWindowMs) { - this.updateTimewindow(); - if (this.timewindowForComparison && this.timewindowForComparison.realtimeWindowMs) { - this.updateComparisonTimewindow(); - } - } - currentData.data = sourceData.data; - if (this.caulculateLegendData) { - this.updateLegend(datasourceIndex + dataKeyIndex, sourceData.data, apply); - } - this.onDataUpdated(apply); - } - } - - alarmsUpdated(alarms, apply) { - this.notifyDataLoaded(); - var updated = !this.alarms || !angular.equals(this.alarms, alarms); - this.alarms = alarms; - if (this.subscriptionTimewindow && this.subscriptionTimewindow.realtimeWindowMs) { - this.updateTimewindow(); - } - if (updated) { - this.onDataUpdated(apply); - } - } - - updateLegend(dataIndex, data, apply) { - var dataKey = this.legendData.keys[dataIndex].dataKey; - var decimals = angular.isDefined(dataKey.decimals) ? dataKey.decimals : this.decimals; - var units = dataKey.units && dataKey.units.length ? dataKey.units : this.units; - var legendKeyData = this.legendData.data[dataIndex]; - if (this.legendConfig.showMin) { - legendKeyData.min = this.ctx.widgetUtils.formatValue(calculateMin(data), decimals, units); - } - if (this.legendConfig.showMax) { - legendKeyData.max = this.ctx.widgetUtils.formatValue(calculateMax(data), decimals, units); - } - if (this.legendConfig.showAvg) { - legendKeyData.avg = this.ctx.widgetUtils.formatValue(calculateAvg(data), decimals, units); - } - if (this.legendConfig.showTotal) { - legendKeyData.total = this.ctx.widgetUtils.formatValue(calculateTotal(data), decimals, units); - } - this.callbacks.legendDataUpdated(this, apply !== false); - } - - update() { - this.unsubscribe(); - this.subscribe(); - } - - subscribe() { - if (this.type === this.ctx.types.widgetType.rpc.value) { - return; - } - if (this.type === this.ctx.types.widgetType.alarm.value) { - this.alarmsSubscribe(); - } else { - this.notifyDataLoading(); - if (this.type === this.ctx.types.widgetType.timeseries.value && this.timeWindowConfig) { - this.updateRealtimeSubscription(); - if (this.comparisonEnabled) { - this.updateSubscriptionForComparison(); - } - if (this.subscriptionTimewindow.fixedWindow) { - this.onDataUpdated(); - } - } - var index = 0; - for (var i = 0; i < this.datasources.length; i++) { - var datasource = this.datasources[i]; - if (angular.isFunction(datasource)) - continue; - - var subscription = this; - - var listener = { - subscriptionType: this.type, - subscriptionTimewindow: this.subscriptionTimewindow, - datasource: datasource, - entityType: datasource.entityType, - entityId: datasource.entityId, - dataUpdated: function (data, datasourceIndex, dataKeyIndex, apply) { - subscription.dataUpdated(data, datasourceIndex, dataKeyIndex, apply); - }, - updateRealtimeSubscription: function () { - this.subscriptionTimewindow = subscription.updateRealtimeSubscription(); - return this.subscriptionTimewindow; - }, - setRealtimeSubscription: function (subscriptionTimewindow) { - subscription.updateRealtimeSubscription(angular.copy(subscriptionTimewindow)); - }, - datasourceIndex: index - }; - - if (this.comparisonEnabled && datasource.isAdditional) { - listener.subscriptionTimewindow = this.timewindowForComparison; - listener.updateRealtimeSubscription = function () { - this.subscriptionTimewindow = subscription.updateSubscriptionForComparison(); - return this.subscriptionTimewindow; - }; - listener.setRealtimeSubscription = function () { - subscription.updateSubscriptionForComparison(); - }; - } - - var entityFieldKey = false; - - for (var a = 0; a < datasource.dataKeys.length; a++) { - if (datasource.dataKeys[a].type !== this.ctx.types.dataKeyType.entityField) { - this.data[index + a].data = []; - } else { - entityFieldKey = true; - } - } - - index += datasource.dataKeys.length; - - this.datasourceListeners.push(listener); - - if (datasource.dataKeys.length) { - this.ctx.datasourceService.subscribeToDatasource(listener); - } - - var forceUpdate = false; - if (datasource.unresolvedStateEntity || entityFieldKey || - !datasource.dataKeys.length || - (datasource.type === this.ctx.types.datasourceType.entity && !datasource.entityId) - ) { - forceUpdate = true; - } - - if (forceUpdate) { - this.notifyDataLoaded(); - this.onDataUpdated(); - } - } - } - } - - alarmsSubscribe() { - this.notifyDataLoading(); - if (this.timeWindowConfig) { - this.updateRealtimeSubscription(); - if (this.subscriptionTimewindow.fixedWindow) { - this.onDataUpdated(); - } - } - var subscription = this; - this.alarmSourceListener = { - subscriptionTimewindow: this.subscriptionTimewindow, - alarmSource: this.alarmSource, - alarmSearchStatus: this.alarmSearchStatus, - alarmsPollingInterval: this.alarmsPollingInterval, - alarmsMaxCountLoad: this.alarmsMaxCountLoad, - alarmsFetchSize: this.alarmsFetchSize, - alarmsUpdated: function(alarms, apply) { - subscription.alarmsUpdated(alarms, apply); - } - } - this.alarms = null; - - this.ctx.alarmService.subscribeForAlarms(this.alarmSourceListener); - - var forceUpdate = false; - if (this.alarmSource.unresolvedStateEntity || - (this.alarmSource.type === this.ctx.types.datasourceType.entity && !this.alarmSource.entityId) - ) { - forceUpdate = true; - } - - if (forceUpdate) { - this.notifyDataLoaded(); - this.onDataUpdated(); - } - } - - unsubscribe() { - if (this.type !== this.ctx.types.widgetType.rpc.value) { - if (this.type == this.ctx.types.widgetType.alarm.value) { - this.alarmsUnsubscribe(); - } else { - for (var i = 0; i < this.datasourceListeners.length; i++) { - var listener = this.datasourceListeners[i]; - this.ctx.datasourceService.unsubscribeFromDatasource(listener); - } - this.datasourceListeners = []; - this.resetData(); - } - } - } - - alarmsUnsubscribe() { - if (this.alarmSourceListener) { - this.ctx.alarmService.unsubscribeFromAlarms(this.alarmSourceListener); - this.alarmSourceListener = null; - } - } - - checkRpcTarget(aliasIds) { - if (aliasIds.indexOf(this.targetDeviceAliasId) > -1) { - return true; - } else { - return false; - } - } - - checkAlarmSource(aliasIds) { - if (this.alarmSource && this.alarmSource.entityAliasId) { - return aliasIds.indexOf(this.alarmSource.entityAliasId) > -1; - } else { - return false; - } - } - - checkSubscriptions(aliasIds) { - var subscriptionsChanged = false; - for (var i = 0; i < this.datasourceListeners.length; i++) { - var listener = this.datasourceListeners[i]; - if (listener.datasource.entityAliasId) { - if (aliasIds.indexOf(listener.datasource.entityAliasId) > -1) { - subscriptionsChanged = true; - break; - } - } - } - return subscriptionsChanged; - } - - isDataResolved() { - return this.hasResolvedData; - } - - destroy() { - this.unsubscribe(); - for (var cafId in this.cafs) { - if (this.cafs[cafId]) { - this.cafs[cafId](); - this.cafs[cafId] = null; - } - } - this.registrations.forEach(function (registration) { - registration(); - }); - this.registrations = []; - } -} - -function calculateMin(data) { - if (data.length > 0) { - var result = Number(data[0][1]); - for (var i=1;i 0) { - var result = Number(data[0][1]); - for (var i=1;i 0) { - return calculateTotal(data)/data.length; - } else { - return null; - } -} - -function calculateTotal(data) { - if (data.length > 0) { - var result = 0; - for (var i = 0; i < data.length; i++) { - result += Number(data[i][1]); - } - return result; - } else { - return null; - } -} diff --git a/ui/src/app/api/telemetry-websocket.service.js b/ui/src/app/api/telemetry-websocket.service.js deleted file mode 100644 index edc1fb3147..0000000000 --- a/ui/src/app/api/telemetry-websocket.service.js +++ /dev/null @@ -1,362 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import 'angular-websocket'; -import thingsboardTypes from '../common/types.constant'; - -export default angular.module('thingsboard.api.telemetryWebsocket', [thingsboardTypes]) - .factory('telemetryWebsocketService', TelemetryWebsocketService) - .name; - -const RECONNECT_INTERVAL = 2000; -const WS_IDLE_TIMEOUT = 90000; - -const MAX_PUBLISH_COMMANDS = 10; - -/*@ngInject*/ -function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, $mdUtil, $log, toast, types, userService) { - - var isOpening = false, - isOpened = false, - isActive = false, - isReconnect = false, - reconnectSubscribers = [], - lastCmdId = 0, - subscribers = {}, - subscribersCount = 0, - commands = {}, - cmdsWrapper = { - tsSubCmds: [], - historyCmds: [], - attrSubCmds: [] - }, - telemetryUri, - dataStream, - location = $window.location, - socketCloseTimer, - reconnectTimer; - - var port = location.port; - if (location.protocol === "https:") { - if (!port) { - port = "443"; - } - telemetryUri = "wss:"; - } else { - if (!port) { - port = "80"; - } - telemetryUri = "ws:"; - } - telemetryUri += "//" + location.hostname + ":" + port; - telemetryUri += "/api/ws/plugins/telemetry"; - - var service = { - subscribe: subscribe, - unsubscribe: unsubscribe - } - - $rootScope.telemetryWsLogoutHandle = $rootScope.$on('unauthenticated', function (event, doLogout) { - if (doLogout) { - reset(true); - } - }); - - $rootScope.telemetryWsLoginHandle = $rootScope.$on('authenticated', function () { - reset(true); - }); - - return service; - - function publishCommands () { - while(isOpened && hasCommands()) { - dataStream.send(preparePublishCommands()).then(function () { - checkToClose(); - }); - } - tryOpenSocket(); - } - - function hasCommands() { - return cmdsWrapper.tsSubCmds.length > 0 || - cmdsWrapper.historyCmds.length > 0 || - cmdsWrapper.attrSubCmds.length > 0; - } - - function preparePublishCommands() { - var preparedWrapper = {}; - var leftCount = MAX_PUBLISH_COMMANDS; - preparedWrapper.tsSubCmds = popCmds(cmdsWrapper.tsSubCmds, leftCount); - leftCount -= preparedWrapper.tsSubCmds.length; - preparedWrapper.historyCmds = popCmds(cmdsWrapper.historyCmds, leftCount); - leftCount -= preparedWrapper.historyCmds.length; - preparedWrapper.attrSubCmds = popCmds(cmdsWrapper.attrSubCmds, leftCount); - return preparedWrapper; - } - - function popCmds(cmds, leftCount) { - var toPublish = Math.min(cmds.length, leftCount); - if (toPublish > 0) { - return cmds.splice(0, toPublish); - } else { - return []; - } - } - - function onError (errorEvent) { - if (errorEvent) { - //showWsError(0, errorEvent); - $log.warn('WebSocket error event', errorEvent); - } - isOpening = false; - } - - function onOpen () { - isOpening = false; - isOpened = true; - if (reconnectTimer) { - $timeout.cancel(reconnectTimer); - reconnectTimer = null; - } - if (isReconnect) { - isReconnect = false; - for (var r=0; r 1000 && closeEvent.code !== 1006) { - showWsError(closeEvent.code, closeEvent.reason); - } - isOpening = false; - isOpened = false; - if (isActive) { - if (!isReconnect) { - reconnectSubscribers = []; - for (var id in subscribers) { - var subscriber = subscribers[id]; - if (reconnectSubscribers.indexOf(subscriber) === -1) { - reconnectSubscribers.push(subscriber); - } - } - reset(false); - isReconnect = true; - } - if (reconnectTimer) { - $timeout.cancel(reconnectTimer); - } - reconnectTimer = $timeout(tryOpenSocket, RECONNECT_INTERVAL, false); - } - } - - function onMessage (message) { - if (message.data) { - var data = angular.fromJson(message.data); - if (data.errorCode) { - showWsError(data.errorCode, data.errorMsg); - } else if (data.subscriptionId) { - var subscriber = subscribers[data.subscriptionId]; - if (subscriber && data) { - var keys = fetchKeys(data.subscriptionId); - if (!data.data) { - data.data = {}; - } - for (var k = 0; k < keys.length; k++) { - var key = keys[k]; - if (!data.data[key]) { - data.data[key] = []; - } - } - subscriber.onData(data, data.subscriptionId); - } - } - } - checkToClose(); - } - - function showWsError(errorCode, errorMsg) { - var message = 'WebSocket Error: '; - if (errorMsg) { - message += errorMsg; - } else { - message += "error code - " + errorCode + "."; - } - $mdUtil.nextTick(function () { - toast.showError(message); - }); - } - - function fetchKeys(subscriptionId) { - var command = commands[subscriptionId]; - if (command && command.keys && command.keys.length > 0) { - return command.keys.split(","); - } else { - return []; - } - } - - function nextCmdId () { - lastCmdId++; - return lastCmdId; - } - - function subscribe (subscriber) { - isActive = true; - var cmdId; - if (angular.isDefined(subscriber.subscriptionCommands)) { - for (var i=0;i -1) { - reconnectSubscribers.splice(index, 1); - } - subscribersCount--; - publishCommands(); - } - } - - function checkToClose () { - if (subscribersCount === 0 && isOpened) { - if (!socketCloseTimer) { - socketCloseTimer = $timeout(closeSocket, WS_IDLE_TIMEOUT, false); - } - } - } - - function tryOpenSocket () { - if (isActive) { - if (!isOpened && !isOpening) { - isOpening = true; - if (userService.isJwtTokenValid()) { - openSocket(userService.getJwtToken()); - } else { - userService.refreshJwtToken().then(function success() { - openSocket(userService.getJwtToken()); - }, function fail() { - isOpening = false; - $rootScope.$broadcast('unauthenticated'); - }); - } - } - if (socketCloseTimer) { - $timeout.cancel(socketCloseTimer); - socketCloseTimer = null; - } - } - } - - function openSocket(token) { - dataStream = $websocket(telemetryUri + '?token=' + token); - dataStream.onError(onError); - dataStream.onOpen(onOpen); - dataStream.onClose(onClose); - dataStream.onMessage(onMessage, {autoApply: false}); - } - - function closeSocket() { - isActive = false; - if (isOpened) { - dataStream.close(); - } - } - - function reset(close) { - if (socketCloseTimer) { - $timeout.cancel(socketCloseTimer); - socketCloseTimer = null; - } - lastCmdId = 0; - subscribers = {}; - subscribersCount = 0; - commands = {}; - cmdsWrapper.tsSubCmds = []; - cmdsWrapper.historyCmds = []; - cmdsWrapper.attrSubCmds = []; - if (close) { - closeSocket(); - } - } -} diff --git a/ui/src/app/api/tenant.service.js b/ui/src/app/api/tenant.service.js deleted file mode 100644 index dfc83a13f9..0000000000 --- a/ui/src/app/api/tenant.service.js +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default angular.module('thingsboard.api.tenant', []) - .factory('tenantService', TenantService) - .name; - -/*@ngInject*/ -function TenantService($http, $q) { - - var service = { - deleteTenant: deleteTenant, - getTenant: getTenant, - getTenants: getTenants, - saveTenant: saveTenant, - } - - return service; - - function getTenants (pageLink, config) { - var deferred = $q.defer(); - var url = '/api/tenants?limit=' + pageLink.limit; - if (angular.isDefined(pageLink.textSearch)) { - url += '&textSearch=' + pageLink.textSearch; - } - if (angular.isDefined(pageLink.idOffset)) { - url += '&idOffset=' + pageLink.idOffset; - } - if (angular.isDefined(pageLink.textOffset)) { - url += '&textOffset=' + pageLink.textOffset; - } - $http.get(url, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getTenant (tenantId, config) { - var deferred = $q.defer(); - var url = '/api/tenant/' + tenantId; - $http.get(url, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail(response) { - deferred.reject(response.data); - }); - return deferred.promise; - } - - function saveTenant (tenant) { - var deferred = $q.defer(); - var url = '/api/tenant'; - $http.post(url, tenant).then(function success(response) { - deferred.resolve(response.data); - }, function fail(response) { - deferred.reject(response.data); - }); - return deferred.promise; - } - - function deleteTenant (tenantId) { - var deferred = $q.defer(); - var url = '/api/tenant/' + tenantId; - $http.delete(url).then(function success() { - deferred.resolve(); - }, function fail(response) { - deferred.reject(response.data); - }); - return deferred.promise; - } - -} diff --git a/ui/src/app/api/time.service.js b/ui/src/app/api/time.service.js deleted file mode 100644 index b18bd8d179..0000000000 --- a/ui/src/app/api/time.service.js +++ /dev/null @@ -1,413 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default angular.module('thingsboard.api.time', []) - .factory('timeService', TimeService) - .name; - -const SECOND = 1000; -const MINUTE = 60 * SECOND; -const HOUR = 60 * MINUTE; -const DAY = 24 * HOUR; - -const MIN_INTERVAL = SECOND; -const MAX_INTERVAL = 365 * 20 * DAY; - -const MIN_LIMIT = 7; -//const AVG_LIMIT = 200; -//const MAX_LIMIT = 500; - -/*@ngInject*/ -function TimeService($translate, $http, $q, types) { - - var predefIntervals; - var maxDatapointsLimit; - - var service = { - loadMaxDatapointsLimit: loadMaxDatapointsLimit, - minIntervalLimit: minIntervalLimit, - maxIntervalLimit: maxIntervalLimit, - boundMinInterval: boundMinInterval, - boundMaxInterval: boundMaxInterval, - getIntervals: getIntervals, - matchesExistingInterval: matchesExistingInterval, - boundToPredefinedInterval: boundToPredefinedInterval, - defaultTimewindow: defaultTimewindow, - toHistoryTimewindow: toHistoryTimewindow, - createSubscriptionTimewindow: createSubscriptionTimewindow, - createTimewindowForComparison: createTimewindowForComparison, - getMaxDatapointsLimit: function () { - return maxDatapointsLimit; - }, - getMinDatapointsLimit: function () { - return MIN_LIMIT; - } - } - - return service; - - function loadMaxDatapointsLimit() { - var deferred = $q.defer(); - var url = '/api/dashboard/maxDatapointsLimit'; - $http.get(url, {ignoreLoading: true}).then(function success(response) { - maxDatapointsLimit = response.data; - if (!maxDatapointsLimit || maxDatapointsLimit <= MIN_LIMIT) { - maxDatapointsLimit = MIN_LIMIT + 1; - } - deferred.resolve(); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function minIntervalLimit(timewindow) { - var min = timewindow / 500; - return boundMinInterval(min); - } - - function avgInterval(timewindow) { - var avg = timewindow / 200; - return boundMinInterval(avg); - } - - function maxIntervalLimit(timewindow) { - var max = timewindow / MIN_LIMIT; - return boundMaxInterval(max); - } - - function boundMinInterval(min) { - return toBound(min, MIN_INTERVAL, MAX_INTERVAL, MIN_INTERVAL); - } - - function boundMaxInterval(max) { - return toBound(max, MIN_INTERVAL, MAX_INTERVAL, MAX_INTERVAL); - } - - function toBound(value, min, max, defValue) { - if (angular.isDefined(value)) { - value = Math.max(value, min); - value = Math.min(value, max); - return value; - } else { - return defValue; - } - } - - function getIntervals(min, max) { - min = boundMinInterval(min); - max = boundMaxInterval(max); - var intervals = []; - initPredefIntervals(); - for (var i in predefIntervals) { - var interval = predefIntervals[i]; - if (interval.value >= min && interval.value <= max) { - intervals.push(interval); - } - } - return intervals; - } - - function initPredefIntervals() { - if (!predefIntervals) { - predefIntervals = [ - { - name: $translate.instant('timeinterval.seconds-interval', {seconds: 1}, 'messageformat'), - value: 1 * SECOND - }, - { - name: $translate.instant('timeinterval.seconds-interval', {seconds: 5}, 'messageformat'), - value: 5 * SECOND - }, - { - name: $translate.instant('timeinterval.seconds-interval', {seconds: 10}, 'messageformat'), - value: 10 * SECOND - }, - { - name: $translate.instant('timeinterval.seconds-interval', {seconds: 15}, 'messageformat'), - value: 15 * SECOND - }, - { - name: $translate.instant('timeinterval.seconds-interval', {seconds: 30}, 'messageformat'), - value: 30 * SECOND - }, - { - name: $translate.instant('timeinterval.minutes-interval', {minutes: 1}, 'messageformat'), - value: 1 * MINUTE - }, - { - name: $translate.instant('timeinterval.minutes-interval', {minutes: 2}, 'messageformat'), - value: 2 * MINUTE - }, - { - name: $translate.instant('timeinterval.minutes-interval', {minutes: 5}, 'messageformat'), - value: 5 * MINUTE - }, - { - name: $translate.instant('timeinterval.minutes-interval', {minutes: 10}, 'messageformat'), - value: 10 * MINUTE - }, - { - name: $translate.instant('timeinterval.minutes-interval', {minutes: 15}, 'messageformat'), - value: 15 * MINUTE - }, - { - name: $translate.instant('timeinterval.minutes-interval', {minutes: 30}, 'messageformat'), - value: 30 * MINUTE - }, - { - name: $translate.instant('timeinterval.hours-interval', {hours: 1}, 'messageformat'), - value: 1 * HOUR - }, - { - name: $translate.instant('timeinterval.hours-interval', {hours: 2}, 'messageformat'), - value: 2 * HOUR - }, - { - name: $translate.instant('timeinterval.hours-interval', {hours: 5}, 'messageformat'), - value: 5 * HOUR - }, - { - name: $translate.instant('timeinterval.hours-interval', {hours: 10}, 'messageformat'), - value: 10 * HOUR - }, - { - name: $translate.instant('timeinterval.hours-interval', {hours: 12}, 'messageformat'), - value: 12 * HOUR - }, - { - name: $translate.instant('timeinterval.days-interval', {days: 1}, 'messageformat'), - value: 1 * DAY - }, - { - name: $translate.instant('timeinterval.days-interval', {days: 7}, 'messageformat'), - value: 7 * DAY - }, - { - name: $translate.instant('timeinterval.days-interval', {days: 30}, 'messageformat'), - value: 30 * DAY - } - ]; - } - } - - function matchesExistingInterval(min, max, intervalMs) { - var intervals = getIntervals(min, max); - for (var i in intervals) { - var interval = intervals[i]; - if (intervalMs === interval.value) { - return true; - } - } - return false; - } - - function boundToPredefinedInterval(min, max, intervalMs) { - var intervals = getIntervals(min, max); - var minDelta = MAX_INTERVAL; - var boundedInterval = intervalMs || min; - var matchedInterval; - for (var i in intervals) { - var interval = intervals[i]; - var delta = Math.abs(interval.value - boundedInterval); - if (delta < minDelta) { - matchedInterval = interval; - minDelta = delta; - } - } - boundedInterval = matchedInterval.value; - return boundedInterval; - } - - function defaultTimewindow() { - var currentTime = (new Date).getTime(); - var timewindow = { - displayValue: "", - selectedTab: 0, - hideInterval: false, - hideAggregation: false, - hideAggInterval: false, - realtime: { - interval: SECOND, - timewindowMs: MINUTE // 1 min by default - }, - history: { - historyType: 0, - interval: SECOND, - timewindowMs: MINUTE, // 1 min by default - fixedTimewindow: { - startTimeMs: currentTime - DAY, // 1 day by default - endTimeMs: currentTime - } - }, - aggregation: { - type: types.aggregation.avg.value, - limit: Math.floor(maxDatapointsLimit / 2) - } - } - return timewindow; - } - - function toHistoryTimewindow(timewindow, startTimeMs, endTimeMs, interval) { - if (timewindow.history) { - interval = angular.isDefined(interval) ? interval : timewindow.history.interval; - } else if (timewindow.realtime) { - interval = timewindow.realtime.interval; - } else { - interval = 0; - } - - var aggType; - var limit; - if (timewindow.aggregation) { - aggType = timewindow.aggregation.type || types.aggregation.avg.value; - limit = timewindow.aggregation.limit || maxDatapointsLimit; - } else { - aggType = types.aggregation.avg.value; - limit = maxDatapointsLimit; - } - - - var historyTimewindow = { - hideInterval: timewindow.hideInterval || false, - hideAggregation: timewindow.hideAggregation || false, - hideAggInterval: timewindow.hideAggInterval || false, - history: { - fixedTimewindow: { - startTimeMs: startTimeMs, - endTimeMs: endTimeMs - }, - interval: boundIntervalToTimewindow(endTimeMs - startTimeMs, interval, types.aggregation.avg.value) - }, - aggregation: { - type: aggType, - limit: limit - } - } - - return historyTimewindow; - } - - function createSubscriptionTimewindow(timewindow, stDiff, stateData) { - - var subscriptionTimewindow = { - fixedWindow: null, - realtimeWindowMs: null, - aggregation: { - interval: SECOND, - limit: maxDatapointsLimit, - type: types.aggregation.avg.value - } - }; - var aggTimewindow = 0; - if (stateData) { - subscriptionTimewindow.aggregation = { - interval: SECOND, - limit: maxDatapointsLimit, - type: types.aggregation.none.value, - stateData: true - }; - } else { - subscriptionTimewindow.aggregation = { - interval: SECOND, - limit: maxDatapointsLimit, - type: types.aggregation.avg.value - }; - } - - if (angular.isDefined(timewindow.aggregation) && !stateData) { - subscriptionTimewindow.aggregation = { - type: timewindow.aggregation.type || types.aggregation.avg.value, - limit: timewindow.aggregation.limit || maxDatapointsLimit - }; - } - if (angular.isDefined(timewindow.realtime)) { - subscriptionTimewindow.realtimeWindowMs = timewindow.realtime.timewindowMs; - subscriptionTimewindow.aggregation.interval = - boundIntervalToTimewindow(subscriptionTimewindow.realtimeWindowMs, timewindow.realtime.interval, - subscriptionTimewindow.aggregation.type); - subscriptionTimewindow.startTs = (new Date).getTime() + stDiff - subscriptionTimewindow.realtimeWindowMs; - var startDiff = subscriptionTimewindow.startTs % subscriptionTimewindow.aggregation.interval; - aggTimewindow = subscriptionTimewindow.realtimeWindowMs; - if (startDiff) { - subscriptionTimewindow.startTs -= startDiff; - aggTimewindow += subscriptionTimewindow.aggregation.interval; - } - } else if (angular.isDefined(timewindow.history)) { - if (angular.isDefined(timewindow.history.timewindowMs)) { - var currentTime = (new Date).getTime(); - subscriptionTimewindow.fixedWindow = { - startTimeMs: currentTime - timewindow.history.timewindowMs, - endTimeMs: currentTime - } - aggTimewindow = timewindow.history.timewindowMs; - - } else { - subscriptionTimewindow.fixedWindow = { - startTimeMs: timewindow.history.fixedTimewindow.startTimeMs, - endTimeMs: timewindow.history.fixedTimewindow.endTimeMs - } - aggTimewindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs; - } - subscriptionTimewindow.startTs = subscriptionTimewindow.fixedWindow.startTimeMs; - subscriptionTimewindow.aggregation.interval = - boundIntervalToTimewindow(aggTimewindow, timewindow.history.interval, subscriptionTimewindow.aggregation.type); - } - var aggregation = subscriptionTimewindow.aggregation; - aggregation.timeWindow = aggTimewindow; - if (aggregation.type !== types.aggregation.none.value) { - aggregation.limit = Math.ceil(aggTimewindow / subscriptionTimewindow.aggregation.interval); - } - return subscriptionTimewindow; - } - - function boundIntervalToTimewindow(timewindow, intervalMs, aggType) { - if (aggType === types.aggregation.none.value) { - return SECOND; - } else { - var min = minIntervalLimit(timewindow); - var max = maxIntervalLimit(timewindow); - if (intervalMs) { - return toBound(intervalMs, min, max, intervalMs); - } else { - return boundToPredefinedInterval(min, max, avgInterval(timewindow)); - } - } - } - - function createTimewindowForComparison(subscriptionTimewindow, timeUnit) { - var timewindowForComparison = { - fixedWindow: null, - realtimeWindowMs: null, - aggregation: subscriptionTimewindow.aggregation - }; - - if (subscriptionTimewindow.realtimeWindowMs) { - timewindowForComparison.startTs = moment(subscriptionTimewindow.startTs).subtract(1, timeUnit).valueOf(); //eslint-disable-line - timewindowForComparison.realtimeWindowMs = subscriptionTimewindow.realtimeWindowMs; - } else if (subscriptionTimewindow.fixedWindow) { - var timeInterval = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs; - var endTimeMs = moment(subscriptionTimewindow.fixedWindow.endTimeMs).subtract(1, timeUnit).valueOf(); //eslint-disable-line - - timewindowForComparison.startTs = endTimeMs - timeInterval; - timewindowForComparison.fixedWindow = { - startTimeMs: timewindowForComparison.startTs, - endTimeMs: endTimeMs - }; - } - - return timewindowForComparison; - } -} diff --git a/ui/src/app/api/user.service.js b/ui/src/app/api/user.service.js deleted file mode 100644 index b84bcf8428..0000000000 --- a/ui/src/app/api/user.service.js +++ /dev/null @@ -1,688 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import thingsboardApiLogin from './login.service'; -import angularStorage from 'angular-storage'; - -export default angular.module('thingsboard.api.user', [thingsboardApiLogin, - angularStorage]) - .factory('userService', UserService) - .name; - -/*@ngInject*/ -function UserService($http, $q, $rootScope, adminService, dashboardService, timeService, loginService, toast, store, jwtHelper, $translate, $state, $location, $mdDialog) { - var currentUser = null, - currentUserDetails = null, - lastPublicDashboardId = null, - allowedDashboardIds = [], - redirectParams = null, - userTokenAccessEnabled = false, - userLoaded = false; - - var refreshTokenQueue = []; - - var service = { - deleteUser: deleteUser, - getAuthority: getAuthority, - isPublic: isPublic, - getPublicId: getPublicId, - parsePublicId: parsePublicId, - isAuthenticated: isAuthenticated, - getCurrentUser: getCurrentUser, - getCustomerUsers: getCustomerUsers, - getUser: getUser, - getTenantAdmins: getTenantAdmins, - isUserLoaded: isUserLoaded, - saveUser: saveUser, - sendActivationEmail: sendActivationEmail, - getActivationLink: getActivationLink, - setUserFromJwtToken: setUserFromJwtToken, - getJwtToken: getJwtToken, - clearJwtToken: clearJwtToken, - isJwtTokenValid : isJwtTokenValid, - validateJwtToken: validateJwtToken, - refreshJwtToken: refreshJwtToken, - refreshTokenPending: refreshTokenPending, - updateAuthorizationHeader: updateAuthorizationHeader, - setAuthorizationRequestHeader: setAuthorizationRequestHeader, - setRedirectParams: setRedirectParams, - gotoDefaultPlace: gotoDefaultPlace, - forceDefaultPlace: forceDefaultPlace, - updateLastPublicDashboardId: updateLastPublicDashboardId, - logout: logout, - reloadUser: reloadUser, - isUserTokenAccessEnabled: isUserTokenAccessEnabled, - loginAsUser: loginAsUser, - setUserCredentialsEnabled: setUserCredentialsEnabled - } - - reloadUser(); - - return service; - - function reloadUser() { - userLoaded = false; - loadUser(true).then(function success() { - notifyUserLoaded(); - }, function fail() { - notifyUserLoaded(); - }); - } - - function updateAndValidateToken(token, prefix, notify) { - var valid = false; - var tokenData = jwtHelper.decodeToken(token); - var issuedAt = tokenData.iat; - var expTime = tokenData.exp; - if (issuedAt && expTime) { - var ttl = expTime - issuedAt; - if (ttl > 0) { - var clientExpiration = new Date().valueOf() + ttl*1000; - store.set(prefix, token); - store.set(prefix + '_expiration', clientExpiration); - valid = true; - } - } - if (!valid && notify) { - $rootScope.$broadcast('unauthenticated'); - } - } - - function clearTokenData() { - store.remove('jwt_token'); - store.remove('jwt_token_expiration'); - store.remove('refresh_token'); - store.remove('refresh_token_expiration'); - } - - function setUserFromJwtToken(jwtToken, refreshToken, notify, doLogout) { - currentUser = null; - currentUserDetails = null; - lastPublicDashboardId = null; - userTokenAccessEnabled = false; - allowedDashboardIds = []; - if (!jwtToken) { - clearTokenData(); - if (notify) { - $rootScope.$broadcast('unauthenticated', doLogout); - } - } else { - updateAndValidateToken(jwtToken, 'jwt_token', true); - updateAndValidateToken(refreshToken, 'refresh_token', true); - if (notify) { - loadUser(false).then(function success() { - $rootScope.$broadcast('authenticated'); - }, function fail() { - $rootScope.$broadcast('unauthenticated'); - }); - } else { - loadUser(false); - } - } - } - - function isAuthenticated() { - return store.get('jwt_token'); - } - - function getJwtToken() { - return store.get('jwt_token'); - } - - function logout() { - $http.post('/api/auth/logout', null, {ignoreErrors: true}).then(function success() { - clearJwtToken(true); - }, function fail() { - clearJwtToken(true); - }); - } - - function clearJwtToken(doLogout) { - setUserFromJwtToken(null, null, true, doLogout); - } - - function isJwtTokenValid() { - return isTokenValid('jwt_token'); - } - - function isTokenValid(prefix) { - var clientExpiration = store.get(prefix + '_expiration'); - return clientExpiration && clientExpiration > new Date().valueOf(); - } - - function validateJwtToken(doRefresh) { - var deferred = $q.defer(); - if (!isTokenValid('jwt_token')) { - if (doRefresh) { - refreshJwtToken().then(function success() { - deferred.resolve(); - }, function fail() { - deferred.reject(); - }); - } else { - clearJwtToken(false); - deferred.reject(); - } - } else { - deferred.resolve(); - } - return deferred.promise; - } - - function resolveRefreshTokenQueue(data) { - for (var q=0; q < refreshTokenQueue.length;q++) { - refreshTokenQueue[q].resolve(data); - } - refreshTokenQueue = []; - } - - function rejectRefreshTokenQueue(message) { - for (var q=0;q 0; - } - - function refreshJwtToken() { - var deferred = $q.defer(); - refreshTokenQueue.push(deferred); - if (refreshTokenQueue.length === 1) { - var refreshToken = store.get('refresh_token'); - var refreshTokenValid = isTokenValid('refresh_token'); - setUserFromJwtToken(null, null, false, false); - if (!refreshTokenValid) { - rejectRefreshTokenQueue($translate.instant('access.refresh-token-expired')); - } else { - var refreshTokenRequest = { - refreshToken: refreshToken - }; - $http.post('/api/auth/token', refreshTokenRequest).then(function success(response) { - var token = response.data.token; - var refreshToken = response.data.refreshToken; - setUserFromJwtToken(token, refreshToken, false); - resolveRefreshTokenQueue(response.data); - }, function fail() { - clearJwtToken(false); - rejectRefreshTokenQueue($translate.instant('access.refresh-token-failed')); - }); - } - } - return deferred.promise; - } - - function getCurrentUser() { - return currentUser; - } - - function getAuthority() { - if (currentUser) { - return currentUser.authority; - } else { - return ''; - } - } - - function isPublic() { - if (currentUser) { - return currentUser.isPublic; - } else { - return false; - } - } - - function getPublicId() { - if (isPublic()) { - return currentUser.sub; - } else { - return null; - } - } - - function parsePublicId() { - var token = getJwtToken(); - if (token) { - var tokenData = jwtHelper.decodeToken(token); - if (tokenData && tokenData.isPublic) { - return tokenData.sub; - } - } - return null; - } - - function isUserLoaded() { - return userLoaded; - } - - function loadUser(doTokenRefresh) { - - var deferred = $q.defer(); - - function fetchAllowedDashboardIds() { - var pageLink = {limit: 100}; - var fetchDashboardsPromise; - if (currentUser.authority === 'TENANT_ADMIN') { - fetchDashboardsPromise = dashboardService.getTenantDashboards(pageLink); - } else { - fetchDashboardsPromise = dashboardService.getCustomerDashboards(currentUser.customerId, pageLink); - } - fetchDashboardsPromise.then( - function success(result) { - var dashboards = result.data; - for (var d=0;d 0) { - currentUser.authority = currentUser.scopes[0]; - } else if (currentUser) { - currentUser.authority = "ANONYMOUS"; - } - var sysParamsPromise = loadSystemParams(); - if (currentUser.isPublic) { - $rootScope.forceFullscreen = true; - sysParamsPromise.then( - () => { fetchAllowedDashboardIds(); }, - () => { deferred.reject(); } - ); - } else if (currentUser.userId) { - getUser(currentUser.userId, true).then( - function success(user) { - sysParamsPromise.then( - () => { - currentUserDetails = user; - updateUserLang(); - $rootScope.forceFullscreen = false; - if (userForceFullscreen()) { - $rootScope.forceFullscreen = true; - } - if ($rootScope.forceFullscreen && (currentUser.authority === 'TENANT_ADMIN' || - currentUser.authority === 'CUSTOMER_USER')) { - fetchAllowedDashboardIds(); - } else { - deferred.resolve(); - } - }, - () => { - deferred.reject(); - logout(); - } - ); - }, - function fail() { - deferred.reject(); - logout(); - } - ) - } else { - deferred.reject(); - } - }, function fail() { - deferred.reject(); - }); - } - - if (!currentUser) { - var locationSearch = $location.search(); - if (locationSearch.publicId) { - loginService.publicLogin(locationSearch.publicId).then(function success(response) { - var token = response.data.token; - var refreshToken = response.data.refreshToken; - updateAndValidateToken(token, 'jwt_token', false); - updateAndValidateToken(refreshToken, 'refresh_token', false); - procceedJwtTokenValidate(); - }, function fail() { - $location.search('publicId', null); - deferred.reject(); - }); - } else if (locationSearch.accessToken) { - var token = locationSearch.accessToken; - var refreshToken = locationSearch.refreshToken; - $location.search('accessToken', null); - if (refreshToken) { - $location.search('refreshToken', null); - } - try { - updateAndValidateToken(token, 'jwt_token', false); - if (refreshToken) { - updateAndValidateToken(refreshToken, 'refresh_token', false); - } else { - store.remove('refresh_token'); - store.remove('refresh_token_expiration'); - } - } catch (e) { - deferred.reject(); - } - procceedJwtTokenValidate(); - } else if (locationSearch.username && locationSearch.password) { - var user = {}; - user.name = locationSearch.username; - user.password = locationSearch.password; - $location.search('username', null); - $location.search('password', null); - - loginService.login(user).then(function success(response) { - var token = response.data.token; - var refreshToken = response.data.refreshToken; - try { - updateAndValidateToken(token, 'jwt_token', false); - updateAndValidateToken(refreshToken, 'refresh_token', false); - } catch (e) { - deferred.reject(); - } - procceedJwtTokenValidate(); - }, function fail() { - deferred.reject(); - }); - } else if (locationSearch.loginError) { - showLoginErrorDialog(locationSearch.loginError); - $location.search('loginError', null); - deferred.reject(); - } else { - procceedJwtTokenValidate(); - } - } else { - deferred.resolve(); - } - return deferred.promise; - } - - function showLoginErrorDialog(loginError) { - $translate(['login.error', - 'action.close']).then(function (translations) { - var alert = $mdDialog.alert() - .title(translations['login.error']) - .htmlContent(loginError) - .ok(translations['action.close']); - $mdDialog.show(alert); - }); - } - - function loadIsUserTokenAccessEnabled() { - var deferred = $q.defer(); - if (currentUser.authority === 'SYS_ADMIN' || currentUser.authority === 'TENANT_ADMIN') { - var url = '/api/user/tokenAccessEnabled'; - $http.get(url).then(function success(response) { - userTokenAccessEnabled = response.data; - deferred.resolve(response.data); - }, function fail() { - userTokenAccessEnabled = false; - deferred.reject(); - }); - } else { - userTokenAccessEnabled = false; - deferred.resolve(false); - } - return deferred.promise; - } - - function loadSystemParams() { - var promises = []; - promises.push(loadIsUserTokenAccessEnabled()); - promises.push(timeService.loadMaxDatapointsLimit()); - return $q.all(promises); - } - - function notifyUserLoaded() { - if (!userLoaded) { - userLoaded = true; - $rootScope.$broadcast('userLoaded'); - } - } - - function updateAuthorizationHeader(headers) { - var jwtToken = store.get('jwt_token'); - if (jwtToken) { - headers['X-Authorization'] = 'Bearer ' + jwtToken; - } - return jwtToken; - } - - function setAuthorizationRequestHeader(request) { - var jwtToken = store.get('jwt_token'); - if (jwtToken) { - request.setRequestHeader('X-Authorization', 'Bearer ' + jwtToken); - } - return jwtToken; - } - - function getTenantAdmins(tenantId, pageLink) { - var deferred = $q.defer(); - var url = '/api/tenant/' + tenantId + '/users?limit=' + pageLink.limit; - if (angular.isDefined(pageLink.textSearch)) { - url += '&textSearch=' + pageLink.textSearch; - } - if (angular.isDefined(pageLink.idOffset)) { - url += '&idOffset=' + pageLink.idOffset; - } - if (angular.isDefined(pageLink.textOffset)) { - url += '&textOffset=' + pageLink.textOffset; - } - $http.get(url, null).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getCustomerUsers(customerId, pageLink) { - var deferred = $q.defer(); - var url = '/api/customer/' + customerId + '/users?limit=' + pageLink.limit; - if (angular.isDefined(pageLink.textSearch)) { - url += '&textSearch=' + pageLink.textSearch; - } - if (angular.isDefined(pageLink.idOffset)) { - url += '&idOffset=' + pageLink.idOffset; - } - if (angular.isDefined(pageLink.textOffset)) { - url += '&textOffset=' + pageLink.textOffset; - } - $http.get(url, null).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function saveUser(user, sendActivationMail) { - var deferred = $q.defer(); - var url = '/api/user'; - if (angular.isDefined(sendActivationMail)) { - url += '?sendActivationMail=' + sendActivationMail; - } - $http.post(url, user).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function setUserCredentialsEnabled(userId, userCredentialsEnabled) { - var deferred = $q.defer(); - var url = '/api/user/' + userId + '/userCredentialsEnabled'; - if (angular.isDefined(userCredentialsEnabled)) { - url += '?userCredentialsEnabled=' + userCredentialsEnabled; - } - $http.post(url, null).then(function success() { - deferred.resolve(); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getUser(userId, ignoreErrors, config) { - var deferred = $q.defer(); - var url = '/api/user/' + userId; - if (!config) { - config = {}; - } - config = Object.assign(config, { ignoreErrors: ignoreErrors }); - $http.get(url, config).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function deleteUser(userId) { - var deferred = $q.defer(); - var url = '/api/user/' + userId; - $http.delete(url).then(function success() { - deferred.resolve(); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function sendActivationEmail(email) { - var deferred = $q.defer(); - var url = '/api/user/sendActivationMail?email=' + email; - $http.post(url, null).then(function success(response) { - deferred.resolve(response); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getActivationLink(userId) { - var deferred = $q.defer(); - var url = `/api/user/${userId}/activationLink` - $http.get(url).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function forceDefaultPlace(to, params) { - if (currentUser && isAuthenticated()) { - if (currentUser.authority === 'TENANT_ADMIN' || currentUser.authority === 'CUSTOMER_USER') { - if ((userHasDefaultDashboard() && $rootScope.forceFullscreen) || isPublic()) { - if (to.name === 'home.profile') { - if (userHasProfile()) { - return false; - } else { - return true; - } - } else if ((to.name === 'home.dashboards.dashboard' || to.name === 'dashboard') - && allowedDashboardIds.indexOf(params.dashboardId) > -1) { - return false; - } else { - return true; - } - } - } - } - return false; - } - - function setRedirectParams(params) { - redirectParams = params; - } - - function gotoDefaultPlace(params) { - if (currentUser && isAuthenticated()) { - var place = redirectParams ? redirectParams.toName : 'home.links'; - params = redirectParams ? redirectParams.params : params; - redirectParams = null; - if (currentUser.authority === 'TENANT_ADMIN' || currentUser.authority === 'CUSTOMER_USER') { - if (userHasDefaultDashboard()) { - place = $rootScope.forceFullscreen ? 'dashboard' : 'home.dashboards.dashboard'; - params = {dashboardId: currentUserDetails.additionalInfo.defaultDashboardId}; - } else if (isPublic()) { - place = 'dashboard'; - params = {dashboardId: lastPublicDashboardId}; - } - } else if (currentUser.authority === 'SYS_ADMIN') { - adminService.checkUpdates().then( - function (updateMessage) { - if (updateMessage && updateMessage.updateAvailable) { - toast.showInfo(updateMessage.message, 0, null, 'bottom right'); - } - } - ); - } - $state.go(place, params, {reload: true}); - } else { - $state.go('login', params); - } - } - - function userHasDefaultDashboard() { - return currentUserDetails && - currentUserDetails.additionalInfo && - currentUserDetails.additionalInfo.defaultDashboardId; - } - - function userForceFullscreen() { - return (currentUser && currentUser.isPublic) || - (currentUserDetails.additionalInfo && - currentUserDetails.additionalInfo.defaultDashboardFullscreen && - currentUserDetails.additionalInfo.defaultDashboardFullscreen === true); - } - - function userHasProfile() { - return currentUser && !currentUser.isPublic; - } - - function updateLastPublicDashboardId(dashboardId) { - if (isPublic()) { - lastPublicDashboardId = dashboardId; - } - } - - function isUserTokenAccessEnabled() { - return userTokenAccessEnabled; - } - - function loginAsUser(userId) { - var url = '/api/user/' + userId + '/token'; - $http.get(url).then(function success(response) { - var token = response.data.token; - var refreshToken = response.data.refreshToken; - setUserFromJwtToken(token, refreshToken, true); - }, function fail() { - }); - } - -} diff --git a/ui/src/app/api/widget.service.js b/ui/src/app/api/widget.service.js deleted file mode 100644 index 4b706e827d..0000000000 --- a/ui/src/app/api/widget.service.js +++ /dev/null @@ -1,832 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import $ from 'jquery'; -import moment from 'moment'; -import tinycolor from 'tinycolor2'; - -import thingsboardLedLight from '../components/led-light.directive'; -import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-widget'; -import thingsboardAlarmsTableWidget from '../widget/lib/alarms-table-widget'; -import thingsboardEntitiesTableWidget from '../widget/lib/entities-table-widget'; -import thingsboardEntitiesHierarchyWidget from '../widget/lib/entities-hierarchy-widget'; -import thingsboardExtensionsTableWidget from '../widget/lib/extensions-table-widget'; -import thingsboardDateRangeNavigatorWidget from '../widget/lib/date-range-navigator/date-range-navigator'; -import thingsboardMultipleInputWidget from '../widget/lib/multiple-input-widget'; -import thingsboardWebCameraInputWidget from '../widget/lib/web-camera-input-widget'; - -import thingsboardRpcWidgets from '../widget/lib/rpc'; - -import thingsboardJsonToString from '../components/tb-json-to-string.directive'; - -import TbFlot from '../widget/lib/flot-widget'; -import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-gauge'; -import TbAnalogueRadialGauge from '../widget/lib/analogue-radial-gauge'; -import TbAnalogueCompass from '../widget/lib/analogue-compass'; -import TbCanvasDigitalGauge from '../widget/lib/canvas-digital-gauge'; -import TbMapWidget from '../widget/lib/map-widget'; -import TbMapWidgetV2 from '../widget/lib/map-widget2'; -import TripAnimationWidget from '../widget/lib/tripAnimation/trip-animation-widget'; - - -import 'jquery.terminal/js/jquery.terminal.min.js'; -import 'jquery.terminal/css/jquery.terminal.min.css'; - -import 'oclazyload'; -import cssjs from '../../vendor/css.js/css'; - -import thingsboardTypes from '../common/types.constant'; -import thingsboardUtils from '../common/utils.service'; - -export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, - thingsboardTimeseriesTableWidget, thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, - thingsboardEntitiesHierarchyWidget, thingsboardExtensionsTableWidget, thingsboardDateRangeNavigatorWidget, - thingsboardMultipleInputWidget, thingsboardWebCameraInputWidget, thingsboardRpcWidgets, thingsboardTypes, - thingsboardUtils, thingsboardJsonToString, TripAnimationWidget]) - .factory('widgetService', WidgetService) - .name; - -/*@ngInject*/ -function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, $translate, types, utils) { - - $window.$ = $; - $window.jQuery = $; - $window.moment = moment; - $window.tinycolor = tinycolor; - $window.lazyLoad = $ocLazyLoad; - - $window.TbFlot = TbFlot; - $window.TbAnalogueLinearGauge = TbAnalogueLinearGauge; - $window.TbAnalogueRadialGauge = TbAnalogueRadialGauge; - $window.TbAnalogueCompass = TbAnalogueCompass; - $window.TbCanvasDigitalGauge = TbCanvasDigitalGauge; - $window.TbMapWidget = TbMapWidget; - $window.TbMapWidgetV2 = TbMapWidgetV2; - - $window.cssjs = cssjs; - - var cssParser = new cssjs(); - cssParser.testMode = false; - - var missingWidgetType; - var errorWidgetType; - - var editingWidgetType; - - var widgetsInfoInMemoryCache = {}; - var widgetsTypeFunctionsInMemoryCache = {}; - var widgetsInfoFetchQueue = {}; - - var allWidgetsBundles = undefined; - var systemWidgetsBundles = undefined; - var tenantWidgetsBundles = undefined; - - $rootScope.widgetServiceStateChangeStartHandle = $rootScope.$on('$stateChangeStart', function () { - invalidateWidgetsBundleCache(); - }); - - initEditingWidgetType(); - initWidgetPlaceholders(); - - var service = { - getWidgetTemplate: getWidgetTemplate, - getSystemWidgetsBundles: getSystemWidgetsBundles, - getTenantWidgetsBundles: getTenantWidgetsBundles, - getAllWidgetsBundles: getAllWidgetsBundles, - getSystemWidgetsBundlesByPageLink: getSystemWidgetsBundlesByPageLink, - getTenantWidgetsBundlesByPageLink: getTenantWidgetsBundlesByPageLink, - getAllWidgetsBundlesByPageLink: getAllWidgetsBundlesByPageLink, - getWidgetsBundleByAlias: getWidgetsBundleByAlias, - saveWidgetsBundle: saveWidgetsBundle, - getWidgetsBundle: getWidgetsBundle, - deleteWidgetsBundle: deleteWidgetsBundle, - getBundleWidgetTypes: getBundleWidgetTypes, - getWidgetInfo: getWidgetInfo, - getWidgetTypeFunction: getWidgetTypeFunction, - getInstantWidgetInfo: getInstantWidgetInfo, - deleteWidgetType: deleteWidgetType, - saveWidgetType: saveWidgetType, - saveImportedWidgetType: saveImportedWidgetType, - getWidgetType: getWidgetType, - getWidgetTypeById: getWidgetTypeById, - toWidgetInfo: toWidgetInfo - } - - return service; - - function initEditingWidgetType() { - if ($rootScope.widgetEditMode) { - editingWidgetType = - toWidgetType({ - widgetName: $rootScope.editWidgetInfo.widgetName, - alias: 'customWidget', - type: $rootScope.editWidgetInfo.type, - sizeX: $rootScope.editWidgetInfo.sizeX, - sizeY: $rootScope.editWidgetInfo.sizeY, - resources: $rootScope.editWidgetInfo.resources, - templateHtml: $rootScope.editWidgetInfo.templateHtml, - templateCss: $rootScope.editWidgetInfo.templateCss, - controllerScript: $rootScope.editWidgetInfo.controllerScript, - settingsSchema: $rootScope.editWidgetInfo.settingsSchema, - dataKeySettingsSchema: $rootScope.editWidgetInfo.dataKeySettingsSchema, - defaultConfig: $rootScope.editWidgetInfo.defaultConfig - }, {id: '1'}, { id: types.id.nullUid }, 'customWidgetBundle'); - } - } - - function initWidgetPlaceholders() { - - missingWidgetType = { - widgetName: 'Widget type not found', - alias: 'undefined', - sizeX: 8, - sizeY: 6, - resources: [], - templateHtml: '
    widget.widget-type-not-found
    ', - templateCss: '', - controllerScript: 'self.onInit = function() {}', - settingsSchema: '{}\n', - dataKeySettingsSchema: '{}\n', - defaultConfig: '{\n' + - '"title": "Widget type not found",\n' + - '"datasources": [],\n' + - '"settings": {}\n' + - '}\n' - }; - - errorWidgetType = { - widgetName: 'Error loading widget', - alias: 'error', - sizeX: 8, - sizeY: 6, - resources: [], - templateHtml: '
    widget.widget-type-load-error
    ', - templateCss: '', - controllerScript: 'self.onInit = function() {}', - settingsSchema: '{}\n', - dataKeySettingsSchema: '{}\n', - defaultConfig: '{\n' + - '"title": "Widget failed to load",\n' + - '"datasources": [],\n' + - '"settings": {}\n' + - '}\n' - }; - } - - function toWidgetInfo(widgetType) { - - var widgetInfo = { - widgetName: widgetType.name, - alias: widgetType.alias - } - - var descriptor = widgetType.descriptor; - - widgetInfo.type = descriptor.type; - widgetInfo.sizeX = descriptor.sizeX; - widgetInfo.sizeY = descriptor.sizeY; - widgetInfo.resources = descriptor.resources; - widgetInfo.templateHtml = descriptor.templateHtml; - widgetInfo.templateCss = descriptor.templateCss; - widgetInfo.controllerScript = descriptor.controllerScript; - widgetInfo.settingsSchema = descriptor.settingsSchema; - widgetInfo.dataKeySettingsSchema = descriptor.dataKeySettingsSchema; - widgetInfo.defaultConfig = descriptor.defaultConfig; - - return widgetInfo; - } - - function toWidgetType(widgetInfo, id, tenantId, bundleAlias) { - var widgetType = { - id: id, - tenantId: tenantId, - bundleAlias: bundleAlias, - alias: widgetInfo.alias, - name: widgetInfo.widgetName - } - - var descriptor = { - type: widgetInfo.type, - sizeX: widgetInfo.sizeX, - sizeY: widgetInfo.sizeY, - resources: widgetInfo.resources, - templateHtml: widgetInfo.templateHtml, - templateCss: widgetInfo.templateCss, - controllerScript: widgetInfo.controllerScript, - settingsSchema: widgetInfo.settingsSchema, - dataKeySettingsSchema: widgetInfo.dataKeySettingsSchema, - defaultConfig: widgetInfo.defaultConfig - } - - widgetType.descriptor = descriptor; - - return widgetType; - } - - function getWidgetTemplate(type) { - var deferred = $q.defer(); - var templateWidgetType = types.widgetType.timeseries; - for (var t in types.widgetType) { - var widgetType = types.widgetType[t]; - if (widgetType.value === type) { - templateWidgetType = widgetType; - break; - } - } - getWidgetType(templateWidgetType.template.bundleAlias, - templateWidgetType.template.alias, true).then( - function success(widgetType) { - var widgetInfo = toWidgetInfo(widgetType); - widgetInfo.alias = undefined; - deferred.resolve(widgetInfo); - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - - /** Cache functions **/ - - function createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem) { - return (isSystem ? 'sys_' : '') + bundleAlias + '_' + widgetTypeAlias; - } - - function getWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem) { - var key = createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); - return widgetsInfoInMemoryCache[key]; - } - - function getWidgetTypeFunctionFromCache(bundleAlias, widgetTypeAlias, isSystem) { - var key = createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); - return widgetsTypeFunctionsInMemoryCache[key]; - } - - function putWidgetInfoToCache(widgetInfo, bundleAlias, widgetTypeAlias, isSystem) { - var key = createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); - widgetsInfoInMemoryCache[key] = widgetInfo; - } - - function putWidgetTypeFunctionToCache(widgetTypeFunction, bundleAlias, widgetTypeAlias, isSystem) { - var key = createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); - widgetsTypeFunctionsInMemoryCache[key] = widgetTypeFunction; - } - - function deleteWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem) { - var key = createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); - delete widgetsInfoInMemoryCache[key]; - delete widgetsTypeFunctionsInMemoryCache[key]; - } - - function deleteWidgetsBundleFromCache(bundleAlias, isSystem) { - var key = (isSystem ? 'sys_' : '') + bundleAlias; - for (var cacheKey in widgetsInfoInMemoryCache) { - if (cacheKey.startsWith(key)) { - delete widgetsInfoInMemoryCache[cacheKey]; - delete widgetsTypeFunctionsInMemoryCache[cacheKey]; - } - } - } - - /** Bundle functions **/ - - function invalidateWidgetsBundleCache() { - allWidgetsBundles = undefined; - systemWidgetsBundles = undefined; - tenantWidgetsBundles = undefined; - } - - function loadWidgetsBundleCache(config) { - var deferred = $q.defer(); - if (!allWidgetsBundles) { - var url = '/api/widgetsBundles'; - $http.get(url, config).then(function success(response) { - allWidgetsBundles = response.data; - systemWidgetsBundles = []; - tenantWidgetsBundles = []; - allWidgetsBundles = $filter('orderBy')(allWidgetsBundles, ['+title', '-createdTime']); - for (var i = 0; i < allWidgetsBundles.length; i++) { - var widgetsBundle = allWidgetsBundles[i]; - if (widgetsBundle.tenantId.id === types.id.nullUid) { - systemWidgetsBundles.push(widgetsBundle); - } else { - tenantWidgetsBundles.push(widgetsBundle); - } - } - deferred.resolve(); - }, function fail() { - deferred.reject(); - }); - } else { - deferred.resolve(); - } - return deferred.promise; - } - - - function getSystemWidgetsBundles(config) { - var deferred = $q.defer(); - loadWidgetsBundleCache(config).then( - function success() { - deferred.resolve(systemWidgetsBundles); - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - - function getTenantWidgetsBundles(config) { - var deferred = $q.defer(); - loadWidgetsBundleCache(config).then( - function success() { - deferred.resolve(tenantWidgetsBundles); - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - - function getAllWidgetsBundles(config) { - var deferred = $q.defer(); - loadWidgetsBundleCache(config).then( - function success() { - deferred.resolve(allWidgetsBundles); - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - - function getSystemWidgetsBundlesByPageLink(pageLink) { - var deferred = $q.defer(); - loadWidgetsBundleCache().then( - function success() { - utils.filterSearchTextEntities(systemWidgetsBundles, 'title', pageLink, deferred); - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - - function getTenantWidgetsBundlesByPageLink(pageLink) { - var deferred = $q.defer(); - loadWidgetsBundleCache().then( - function success() { - utils.filterSearchTextEntities(tenantWidgetsBundles, 'title', pageLink, deferred); - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - - function getAllWidgetsBundlesByPageLink(pageLink) { - var deferred = $q.defer(); - loadWidgetsBundleCache().then( - function success() { - utils.filterSearchTextEntities(allWidgetsBundles, 'title', pageLink, deferred); - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - - function getWidgetsBundleByAlias(bundleAlias) { - var deferred = $q.defer(); - loadWidgetsBundleCache().then( - function success() { - var widgetsBundles = $filter('filter')(allWidgetsBundles, {alias: bundleAlias}); - if (widgetsBundles.length > 0) { - deferred.resolve(widgetsBundles[0]); - } else { - deferred.resolve(); - } - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - - function saveWidgetsBundle(widgetsBundle) { - var deferred = $q.defer(); - var url = '/api/widgetsBundle'; - $http.post(url, widgetsBundle).then(function success(response) { - invalidateWidgetsBundleCache(); - deferred.resolve(response.data); - }, function fail(response) { - deferred.reject(response.data); - }); - return deferred.promise; - } - - function getWidgetsBundle(widgetsBundleId) { - var deferred = $q.defer(); - - var url = '/api/widgetsBundle/' + widgetsBundleId; - $http.get(url, null).then(function success(response) { - deferred.resolve(response.data); - }, function fail(response) { - deferred.reject(response.data); - }); - - return deferred.promise; - } - - function deleteWidgetsBundle(widgetsBundleId) { - var deferred = $q.defer(); - - getWidgetsBundle(widgetsBundleId).then( - function success(response) { - var widgetsBundle = response; - var url = '/api/widgetsBundle/' + widgetsBundleId; - $http.delete(url).then(function success() { - invalidateWidgetsBundleCache(); - deleteWidgetsBundleFromCache(widgetsBundle.alias, - widgetsBundle.tenantId.id === types.id.nullUid); - deferred.resolve(); - }, function fail(response) { - deferred.reject(response.data); - }); - }, - function fail() { - deferred.reject(); - } - ); - - return deferred.promise; - } - - function getBundleWidgetTypes(bundleAlias, isSystem) { - var deferred = $q.defer(); - var url = '/api/widgetTypes?isSystem=' + (isSystem ? 'true' : 'false') + - '&bundleAlias='+bundleAlias; - $http.get(url, null).then(function success(response) { - deferred.resolve(response.data); - }, function fail(response) { - deferred.reject(response.data); - }); - return deferred.promise; - } - - /** Widget type functions **/ - - function getInstantWidgetInfo(widget) { - var widgetInfo = getWidgetInfoFromCache(widget.bundleAlias, widget.typeAlias, widget.isSystemType); - if (widgetInfo) { - return widgetInfo; - } else { - return {}; - } - } - - function resolveWidgetsInfoFetchQueue(key, widgetInfo) { - var fetchQueue = widgetsInfoFetchQueue[key]; - if (fetchQueue) { - for (var q in fetchQueue) { - if (isNaN(q)) - continue; - fetchQueue[q].resolve(widgetInfo); - } - delete widgetsInfoFetchQueue[key]; - } - } - - function getWidgetInfo(bundleAlias, widgetTypeAlias, isSystem) { - var deferred = $q.defer(); - var widgetInfo = getWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem); - if (widgetInfo) { - deferred.resolve(widgetInfo); - } else { - if ($rootScope.widgetEditMode) { - loadWidget(editingWidgetType, bundleAlias, isSystem, deferred); - } else { - var key = createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); - var fetchQueue = widgetsInfoFetchQueue[key]; - if (fetchQueue) { - fetchQueue.push(deferred); - } else { - fetchQueue = []; - widgetsInfoFetchQueue[key] = fetchQueue; - getWidgetType(bundleAlias, widgetTypeAlias, isSystem).then( - function success(widgetType) { - loadWidget(widgetType, bundleAlias, isSystem, deferred); - }, function fail() { - deferred.resolve(missingWidgetType); - resolveWidgetsInfoFetchQueue(key, missingWidgetType); - } - ); - } - } - } - return deferred.promise; - } - - function getWidgetTypeFunction(bundleAlias, widgetTypeAlias, isSystem) { - var widgetType = getWidgetTypeFunctionFromCache(bundleAlias, widgetTypeAlias, isSystem); - return widgetType; - } - - function createWidgetTypeFunction(widgetInfo, name) { - var widgetTypeFunctionBody = 'return function '+ name +' (ctx) {\n' + - ' var self = this;\n' + - ' self.ctx = ctx;\n\n'; /*+ - - ' self.onInit = function() {\n\n' + - - ' }\n\n' + - - ' self.onDataUpdated = function() {\n\n' + - - ' }\n\n' + - - ' self.useCustomDatasources = function() {\n\n' + - - ' }\n\n' + - - ' self.typeParameters = function() {\n\n' + - return { - useCustomDatasources: false, - maxDatasources: -1, //unlimited - maxDataKeys: -1, //unlimited - dataKeysOptional: false, - stateData: false - }; - ' }\n\n' + - - ' self.actionSources = function() {\n\n' + - return { - 'headerButton': { - name: 'Header button', - multiple: true - } - }; - }\n\n' + - ' self.onResize = function() {\n\n' + - - ' }\n\n' + - - ' self.onEditModeChanged = function() {\n\n' + - - ' }\n\n' + - - ' self.onMobileModeChanged = function() {\n\n' + - - ' }\n\n' + - - ' self.getSettingsSchema = function() {\n\n' + - - ' }\n\n' + - - ' self.getDataKeySettingsSchema = function() {\n\n' + - - ' }\n\n' + - - ' self.onDestroy = function() {\n\n' + - - ' }\n\n' + - '}';*/ - - widgetTypeFunctionBody += widgetInfo.controllerScript; - widgetTypeFunctionBody += '\n};\n'; - try { - var widgetTypeFunction = new Function(widgetTypeFunctionBody); - var widgetType = widgetTypeFunction.apply(this); - var widgetTypeInstance = new widgetType(); - var result = { - widgetTypeFunction: widgetType - }; - if (angular.isFunction(widgetTypeInstance.getSettingsSchema)) { - result.settingsSchema = widgetTypeInstance.getSettingsSchema(); - } - if (angular.isFunction(widgetTypeInstance.getDataKeySettingsSchema)) { - result.dataKeySettingsSchema = widgetTypeInstance.getDataKeySettingsSchema(); - } - if (angular.isFunction(widgetTypeInstance.typeParameters)) { - result.typeParameters = widgetTypeInstance.typeParameters(); - } else { - result.typeParameters = {}; - } - if (angular.isFunction(widgetTypeInstance.useCustomDatasources)) { - result.typeParameters.useCustomDatasources = widgetTypeInstance.useCustomDatasources(); - } else { - result.typeParameters.useCustomDatasources = false; - } - if (angular.isUndefined(result.typeParameters.maxDatasources)) { - result.typeParameters.maxDatasources = -1; - } - if (angular.isUndefined(result.typeParameters.maxDataKeys)) { - result.typeParameters.maxDataKeys = -1; - } - if (angular.isUndefined(result.typeParameters.dataKeysOptional)) { - result.typeParameters.dataKeysOptional = false; - } - if (angular.isUndefined(result.typeParameters.stateData)) { - result.typeParameters.stateData = false; - } - if (angular.isFunction(widgetTypeInstance.actionSources)) { - result.actionSources = widgetTypeInstance.actionSources(); - } else { - result.actionSources = {}; - } - for (var actionSourceId in types.widgetActionSources) { - result.actionSources[actionSourceId] = angular.copy(types.widgetActionSources[actionSourceId]); - result.actionSources[actionSourceId].name = $translate.instant(result.actionSources[actionSourceId].name) + ''; - } - - return result; - } catch (e) { - utils.processWidgetException(e); - throw e; - } - } - - function processWidgetLoadError(errorMessages, cacheKey, deferred) { - var widgetInfo = angular.copy(errorWidgetType); - for (var e in errorMessages) { - var error = errorMessages[e]; - widgetInfo.templateHtml += '
    ' + error + '
    '; - } - widgetInfo.templateHtml += '
    '; - deferred.resolve(widgetInfo); - resolveWidgetsInfoFetchQueue(cacheKey, widgetInfo); - } - - function loadWidget(widgetType, bundleAlias, isSystem, deferred) { - var widgetInfo = toWidgetInfo(widgetType); - var key = createWidgetInfoCacheKey(bundleAlias, widgetInfo.alias, isSystem); - loadWidgetResources(widgetInfo, bundleAlias, isSystem).then( - function success() { - var widgetType = null; - try { - widgetType = createWidgetTypeFunction(widgetInfo, key); - } catch (e) { - var details = utils.parseException(e); - var errorMessage = 'Failed to compile widget script. \n Error: ' + details.message; - processWidgetLoadError([errorMessage], key, deferred); - } - if (widgetType) { - if (widgetType.settingsSchema) { - widgetInfo.typeSettingsSchema = widgetType.settingsSchema; - } - if (widgetType.dataKeySettingsSchema) { - widgetInfo.typeDataKeySettingsSchema = widgetType.dataKeySettingsSchema; - } - widgetInfo.typeParameters = widgetType.typeParameters; - widgetInfo.actionSources = widgetType.actionSources; - putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem); - putWidgetTypeFunctionToCache(widgetType.widgetTypeFunction, bundleAlias, widgetInfo.alias, isSystem); - deferred.resolve(widgetInfo); - resolveWidgetsInfoFetchQueue(key, widgetInfo); - } - }, function fail(errorMessages) { - processWidgetLoadError(errorMessages, key, deferred); - } - ); - } - - function getWidgetType(bundleAlias, widgetTypeAlias, isSystem) { - var deferred = $q.defer(); - var url = '/api/widgetType?isSystem=' + (isSystem ? 'true' : 'false') + - '&bundleAlias='+bundleAlias+'&alias='+widgetTypeAlias; - $http.get(url, null).then(function success(response) { - var widgetType = response.data; - deferred.resolve(widgetType); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function getWidgetTypeById(widgetTypeId) { - var deferred = $q.defer(); - var url = '/api/widgetType/' + widgetTypeId; - $http.get(url, null).then(function success(response) { - var widgetType = response.data; - deferred.resolve(widgetType); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function deleteWidgetType(bundleAlias, widgetTypeAlias, isSystem) { - var deferred = $q.defer(); - getWidgetType(bundleAlias, widgetTypeAlias, isSystem).then( - function success(widgetType) { - var url = '/api/widgetType/' + widgetType.id.id; - $http.delete(url).then(function success() { - deleteWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem); - deferred.resolve(); - }, function fail() { - deferred.reject(); - }); - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - - function saveWidgetType(widgetInfo, id, bundleAlias) { - var deferred = $q.defer(); - var widgetType = toWidgetType(widgetInfo, id, undefined, bundleAlias); - var url = '/api/widgetType'; - $http.post(url, widgetType).then(function success(response) { - var widgetType = response.data; - deleteWidgetInfoFromCache(widgetType.bundleAlias, widgetType.alias, widgetType.tenantId.id === types.id.nullUid); - deferred.resolve(widgetType); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function saveImportedWidgetType(widgetType) { - var deferred = $q.defer(); - var url = '/api/widgetType'; - $http.post(url, widgetType).then(function success(response) { - var widgetType = response.data; - deleteWidgetInfoFromCache(widgetType.bundleAlias, widgetType.alias, widgetType.tenantId.id === types.id.nullUid); - deferred.resolve(widgetType); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - function loadWidgetResources(widgetInfo, bundleAlias, isSystem) { - - var deferred = $q.defer(); - var errors = []; - - var widgetNamespace = "widget-type-" + (isSystem ? 'sys-' : '') + bundleAlias + '-' + widgetInfo.alias; - cssParser.cssPreviewNamespace = widgetNamespace; - cssParser.createStyleElement(widgetNamespace, widgetInfo.templateCss); - - function loadNextOrComplete(i) { - i++; - if (i < widgetInfo.resources.length) { - loadNext(i); - } else { - if (errors.length > 0) { - deferred.reject(errors); - } else { - deferred.resolve(); - } - } - } - - function loadNext(i) { - var resourceUrl = widgetInfo.resources[i].url; - if (resourceUrl && resourceUrl.length > 0) { - $ocLazyLoad.load(resourceUrl).then( - function success() { - loadNextOrComplete(i); - }, - function fail() { - errors.push('Failed to load widget resource: \'' + resourceUrl + '\''); - loadNextOrComplete(i); - } - ); - } else { - loadNextOrComplete(i); - } - } - - if (widgetInfo.resources.length > 0) { - loadNext(0); - } else { - deferred.resolve(); - } - - return deferred.promise; - } - -} diff --git a/ui/src/app/app.config.js b/ui/src/app/app.config.js deleted file mode 100644 index 82b4f923ce..0000000000 --- a/ui/src/app/app.config.js +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import injectTapEventPlugin from 'react-tap-event-plugin'; -import UrlHandler from './url.handler'; - -/* eslint-disable import/no-unresolved, import/default */ - -import mdiIconSet from '../svg/mdi.svg'; - -/* eslint-enable import/no-unresolved, import/default */ - -const PRIMARY_BACKGROUND_COLOR = "#305680";//#2856b6";//"#3f51b5"; -const SECONDARY_BACKGROUND_COLOR = "#527dad"; -const HUE3_COLOR = "#a7c1de"; - -/*@ngInject*/ -export default function AppConfig($provide, - $urlRouterProvider, - $locationProvider, - $mdIconProvider, - ngMdIconServiceProvider, - $mdThemingProvider, - $httpProvider, - $translateProvider, - storeProvider) { - - injectTapEventPlugin(); - $locationProvider.html5Mode(true); - $urlRouterProvider.otherwise(UrlHandler); - storeProvider.setCaching(false); - - $translateProvider.useSanitizeValueStrategy(null) - .useMissingTranslationHandler('tbMissingTranslationHandler') - .addInterpolation('$translateMessageFormatInterpolation') - .useStaticFilesLoader({ - files: [ - { - prefix: PUBLIC_PATH + 'locale/locale.constant-', //eslint-disable-line - suffix: '.json' - } - ] - }) - .registerAvailableLanguageKeys(SUPPORTED_LANGS, getLanguageAliases(SUPPORTED_LANGS)) //eslint-disable-line - .fallbackLanguage('en_US') // must be before determinePreferredLanguage - .uniformLanguageTag('java') // must be before determinePreferredLanguage - .determinePreferredLanguage(); - - $httpProvider.interceptors.push('globalInterceptor'); - - $provide.decorator("$exceptionHandler", ['$delegate', '$injector', function ($delegate/*, $injector*/) { - return function (exception, cause) { -/* var rootScope = $injector.get("$rootScope"); - var $window = $injector.get("$window"); - var utils = $injector.get("utils"); - if (rootScope.widgetEditMode) { - var parentScope = $window.parent.angular.element($window.frameElement).scope(); - var data = utils.parseException(exception); - parentScope.$emit('widgetException', data); - parentScope.$apply(); - }*/ - $delegate(exception, cause); - }; - }]); - - $mdIconProvider.iconSet('mdi', mdiIconSet); - - ngMdIconServiceProvider - .addShape('alpha-a-circle-outline', '') - .addShape('alpha-e-circle-outline', ''); - - configureTheme(); - - function blueGrayTheme() { - var tbPrimaryPalette = $mdThemingProvider.extendPalette('blue-grey'); - var tbAccentPalette = $mdThemingProvider.extendPalette('orange', { - 'contrastDefaultColor': 'light' - }); - - $mdThemingProvider.definePalette('tb-primary', tbPrimaryPalette); - $mdThemingProvider.definePalette('tb-accent', tbAccentPalette); - - $mdThemingProvider.theme('default') - .primaryPalette('tb-primary') - .accentPalette('tb-accent'); - - $mdThemingProvider.theme('tb-dark') - .primaryPalette('tb-primary') - .accentPalette('tb-accent') - .backgroundPalette('tb-primary') - .dark(); - } - - function indigoTheme() { - var tbPrimaryPalette = $mdThemingProvider.extendPalette('indigo', { - '500': PRIMARY_BACKGROUND_COLOR, - '600': SECONDARY_BACKGROUND_COLOR, - 'A100': HUE3_COLOR - }); - - var tbAccentPalette = $mdThemingProvider.extendPalette('deep-orange'); - - $mdThemingProvider.definePalette('tb-primary', tbPrimaryPalette); - $mdThemingProvider.definePalette('tb-accent', tbAccentPalette); - - var tbDarkPrimaryPalette = $mdThemingProvider.extendPalette('tb-primary', { - '500': '#9fa8da' - }); - - var tbDarkPrimaryBackgroundPalette = $mdThemingProvider.extendPalette('tb-primary', { - '800': PRIMARY_BACKGROUND_COLOR - }); - - $mdThemingProvider.definePalette('tb-dark-primary', tbDarkPrimaryPalette); - $mdThemingProvider.definePalette('tb-dark-primary-background', tbDarkPrimaryBackgroundPalette); - - $mdThemingProvider.theme('default') - .primaryPalette('tb-primary') - .accentPalette('tb-accent'); - - $mdThemingProvider.theme('tb-dark') - .primaryPalette('tb-dark-primary') - .accentPalette('tb-accent') - .backgroundPalette('tb-dark-primary-background') - .dark(); - } - - function configureTheme() { - - var theme = 'indigo'; - - if (theme === 'blueGray') { - blueGrayTheme(); - } else { - indigoTheme(); - } - - $mdThemingProvider.setDefaultTheme('default'); - //$mdThemingProvider.alwaysWatchTheme(true); - } - - function getLanguageAliases(supportedLangs) { - var aliases = {}; - - supportedLangs.sort().forEach(function(item, index, array) { - if (item.length === 2) { - aliases[item] = item; - aliases[item + '_*'] = item; - } else { - var key = item.slice(0, 2); - if (index === 0 || key !== array[index - 1].slice(0, 2)) { - aliases[key] = item; - aliases[key + '_*'] = item; - } else { - aliases[item] = item; - } - } - }); - - return aliases; - } -} diff --git a/ui/src/app/app.js b/ui/src/app/app.js deleted file mode 100644 index 0b48a76cdb..0000000000 --- a/ui/src/app/app.js +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './ie.support'; - -import 'event-source-polyfill'; - -import angular from 'angular'; -import ngMaterial from 'angular-material'; -import ngMdIcons from 'angular-material-icons'; -import ngCookies from 'angular-cookies'; -import angularSocialshare from 'angular-socialshare'; -import 'angular-translate'; -import 'angular-translate-loader-static-files'; -import 'angular-translate-storage-local'; -import 'angular-translate-storage-cookie'; -import 'angular-translate-handler-log'; -import 'angular-translate-interpolation-messageformat'; -import 'md-color-picker'; -import 'md-date-range-picker'; -import mdPickers from 'mdPickers'; -import ngSanitize from 'angular-sanitize'; -import FBAngular from 'angular-fullscreen'; -import vAccordion from 'v-accordion'; -import ngAnimate from 'angular-animate'; -import 'angular-websocket'; -import uiRouter from 'angular-ui-router'; -import angularJwt from 'angular-jwt'; -import 'angular-drag-and-drop-lists'; -import mdDataTable from 'angular-material-data-table'; -import fixedTableHeader from 'angular-fixed-table-header'; -import 'angular-material-expansion-panel'; -import ngTouch from 'angular-touch'; -import 'angular-carousel'; -import 'clipboard'; -import 'ngclipboard'; -import 'react'; -import 'react-dom'; -import 'material-ui'; -import 'react-schema-form'; -import react from 'ngreact'; -import '@flowjs/ng-flow/dist/ng-flow-standalone.min'; -import 'ngFlowchart/dist/ngFlowchart'; -import 'jstree/dist/jstree.min'; -import 'material-steppers/dist/material-steppers'; -import 'material-steppers/dist/material-steppers.css' -import 'jstree-bootstrap-theme/dist/themes/proton/style.min.css'; -import 'typeface-roboto'; -import 'font-awesome/css/font-awesome.min.css'; -import 'angular-material/angular-material.min.css'; -import 'angular-material-icons/angular-material-icons.css'; -import 'angular-gridster/dist/angular-gridster.min.css'; -import 'v-accordion/dist/v-accordion.min.css'; -import 'md-color-picker/dist/mdColorPicker.min.css'; -import 'mdPickers/dist/mdPickers.min.css'; -import 'angular-hotkeys/build/hotkeys.min.css'; -import 'angular-carousel/dist/angular-carousel.min.css'; -import 'angular-material-expansion-panel/dist/md-expansion-panel.min.css'; -import 'ngFlowchart/dist/flowchart.css'; -import 'md-date-range-picker/src/md-date-range-picker.css'; -import '../scss/main.scss'; - -import thingsboardThirdpartyFix from './common/thirdparty-fix'; -import thingsboardTranslateHandler from './locale/translate-handler'; -import thingsboardLogin from './login'; -import thingsboardDialogs from './components/datakey-config-dialog.controller'; -import thingsboardMenu from './services/menu.service'; -import thingsboardRaf from './common/raf.provider'; -import thingsboardUtils from './common/utils.service'; -import thingsboardDashboardUtils from './common/dashboard-utils.service'; -import thingsboardTypes from './common/types.constant'; -import thingsboardApiTime from './api/time.service'; -import thingsboardKeyboardShortcut from './components/keyboard-shortcut.filter'; -import thingsboardHelp from './help/help.directive'; -import thingsboardToast from './services/toast'; -import thingsboardClipboard from './services/clipboard.service'; -import thingsboardHome from './layout'; -import thingsboardApiLogin from './api/login.service'; -import thingsboardApiDevice from './api/device.service'; -import thingsboardApiEntityView from './api/entity-view.service'; -import thingsboardApiUser from './api/user.service'; -import thingsboardApiEntityRelation from './api/entity-relation.service'; -import thingsboardApiAsset from './api/asset.service'; -import thingsboardApiAttribute from './api/attribute.service'; -import thingsboardApiEntity from './api/entity.service'; -import thingsboardApiAlarm from './api/alarm.service'; -import thingsboardApiAuditLog from './api/audit-log.service'; -import thingsboardApiComponentDescriptor from './api/component-descriptor.service'; -import thingsboardApiRuleChain from './api/rule-chain.service'; - -import AppConfig from './app.config'; -import GlobalInterceptor from './global-interceptor.service'; -import AppRun from './app.run'; - -angular.module('thingsboard', [ - ngMaterial, - ngMdIcons, - ngCookies, - angularSocialshare, - 'pascalprecht.translate', - 'mdColorPicker', - 'ngMaterialDateRangePicker', - mdPickers, - ngSanitize, - FBAngular.name, - vAccordion, - ngAnimate, - 'ngWebSocket', - angularJwt, - 'dndLists', - mdDataTable, - fixedTableHeader, - 'material.components.expansionPanels', - ngTouch, - 'angular-carousel', - 'ngclipboard', - react.name, - 'flow', - 'flowchart', - 'mdSteppers', - thingsboardThirdpartyFix, - thingsboardTranslateHandler, - thingsboardLogin, - thingsboardDialogs, - thingsboardMenu, - thingsboardRaf, - thingsboardUtils, - thingsboardDashboardUtils, - thingsboardTypes, - thingsboardApiTime, - thingsboardKeyboardShortcut, - thingsboardHelp, - thingsboardToast, - thingsboardClipboard, - thingsboardHome, - thingsboardApiLogin, - thingsboardApiDevice, - thingsboardApiEntityView, - thingsboardApiUser, - thingsboardApiEntityRelation, - thingsboardApiAsset, - thingsboardApiAttribute, - thingsboardApiEntity, - thingsboardApiAlarm, - thingsboardApiAuditLog, - thingsboardApiComponentDescriptor, - thingsboardApiRuleChain, - uiRouter]) - .config(AppConfig) - .factory('globalInterceptor', GlobalInterceptor) - .run(AppRun); diff --git a/ui/src/app/app.run.js b/ui/src/app/app.run.js deleted file mode 100644 index 3255c0a917..0000000000 --- a/ui/src/app/app.run.js +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import Flow from '@flowjs/ng-flow/dist/ng-flow-standalone.min'; -import UrlHandler from './url.handler'; - -/*@ngInject*/ -export default function AppRun($rootScope, $window, $injector, $location, $log, $state, $mdDialog, $filter, $q, loginService, userService, $translate) { - - $window.Flow = Flow; - var frame = null; - try { - frame = $window.frameElement; - } catch(e) { - // ie11 fix - } - - var forbiddenDialog = null; - - $rootScope.iframeMode = false; - - if (frame) { - $rootScope.iframeMode = true; - var dataWidgetAttr = angular.element(frame).attr('data-widget'); - if (dataWidgetAttr) { - $rootScope.editWidgetInfo = angular.fromJson(dataWidgetAttr); - $rootScope.widgetEditMode = true; - } - } - - initWatchers(); - - var skipStateChange = false; - - function initWatchers() { - $rootScope.unauthenticatedHandle = $rootScope.$on('unauthenticated', function (event, doLogout) { - if (doLogout) { - gotoPublicModule('login'); - } else { - UrlHandler($injector, $location); - } - }); - - $rootScope.authenticatedHandle = $rootScope.$on('authenticated', function () { - UrlHandler($injector, $location); - }); - - $rootScope.forbiddenHandle = $rootScope.$on('forbidden', function () { - showForbiddenDialog(); - }); - - $rootScope.stateChangeStartHandle = $rootScope.$on('$stateChangeStart', function (evt, to, params) { - - if (skipStateChange) { - skipStateChange = false; - return; - } - - function waitForUserLoaded() { - if ($rootScope.userLoadedHandle) { - $rootScope.userLoadedHandle(); - } - $rootScope.userLoadedHandle = $rootScope.$on('userLoaded', function () { - $rootScope.userLoadedHandle(); - $state.go(to.name, params); - }); - } - - function reloadUserFromPublicId() { - userService.setUserFromJwtToken(null, null, false); - waitForUserLoaded(); - userService.reloadUser(); - } - - var locationSearch = $location.search(); - var publicId = locationSearch.publicId; - var activateToken = locationSearch.activateToken; - - if (to.url === '/createPassword?activateToken' && activateToken && activateToken.length) { - userService.setUserFromJwtToken(null, null, false); - } - - if (userService.isUserLoaded() === true) { - if (userService.isAuthenticated()) { - if (userService.isPublic()) { - if (userService.parsePublicId() !== publicId) { - evt.preventDefault(); - if (publicId && publicId.length > 0) { - reloadUserFromPublicId(); - } else { - userService.logout(); - } - return; - } - } - if (userService.forceDefaultPlace(to, params)) { - evt.preventDefault(); - gotoDefaultPlace(params); - } else { - var authority = userService.getAuthority(); - if (to.module === 'public') { - evt.preventDefault(); - gotoDefaultPlace(params); - } else if (angular.isDefined(to.auth) && - to.auth.indexOf(authority) === -1) { - evt.preventDefault(); - showForbiddenDialog(); - } else if (to.redirectTo) { - evt.preventDefault(); - $state.go(to.redirectTo, params); - } else if (to.name === 'home.dashboards.dashboard' && $rootScope.forceFullscreen) { - evt.preventDefault(); - $state.go('dashboard', params); - } - } - } else { - if (publicId && publicId.length > 0) { - evt.preventDefault(); - reloadUserFromPublicId(); - } else if (to.module === 'private') { - evt.preventDefault(); - var redirectParams = {}; - redirectParams.toName = to.name; - redirectParams.params = params; - userService.setRedirectParams(redirectParams); - gotoPublicModule('login', params); - } else { - evt.preventDefault(); - gotoPublicModule(to.name, params); - } - } - } else { - evt.preventDefault(); - waitForUserLoaded(); - } - }) - - $rootScope.pageTitle = 'ThingsBoard'; - - $rootScope.stateChangeSuccessHandle = $rootScope.$on('$stateChangeSuccess', function (evt, to, params) { - if (userService.isPublic() && to.name === 'dashboard') { - $location.search('publicId', userService.getPublicId()); - userService.updateLastPublicDashboardId(params.dashboardId); - } - if (angular.isDefined(to.data.pageTitle)) { - $translate(to.data.pageTitle).then(function (translation) { - $rootScope.pageTitle = 'ThingsBoard | ' + translation; - }, function (translationId) { - $rootScope.pageTitle = 'ThingsBoard | ' + translationId; - }); - } - }) - } - - function gotoDefaultPlace(params) { - userService.gotoDefaultPlace(params); - } - - function gotoPublicModule(name, params) { - let tasks = []; - if (name === "login") { - tasks.push(loginService.loadOAuth2Clients()); - } - $q.all(tasks).then( - () => { - skipStateChange = true; - $state.go(name, params); - }, - () => { - skipStateChange = true; - $state.go(name, params); - } - ); - } - - function showForbiddenDialog() { - if (forbiddenDialog === null) { - $translate(['access.access-forbidden', - 'access.access-forbidden-text', - 'action.cancel', - 'action.sign-in']).then(function (translations) { - if (forbiddenDialog === null) { - forbiddenDialog = $mdDialog.confirm() - .title(translations['access.access-forbidden']) - .htmlContent(translations['access.access-forbidden-text']) - .cancel(translations['action.cancel']) - .ok(translations['action.sign-in']); - $mdDialog.show(forbiddenDialog).then(function () { - forbiddenDialog = null; - userService.logout(); - }, function () { - forbiddenDialog = null; - }); - } - }); - } - } -} diff --git a/ui/src/app/asset/add-asset.tpl.html b/ui/src/app/asset/add-asset.tpl.html deleted file mode 100644 index fafd91329f..0000000000 --- a/ui/src/app/asset/add-asset.tpl.html +++ /dev/null @@ -1,45 +0,0 @@ - - -
    - -
    -

    asset.add

    - -
    - - - -
    -
    - - - -
    - -
    -
    - - - - {{ 'action.add' | translate }} - - {{ 'action.cancel' | translate }} - -
    -
    diff --git a/ui/src/app/asset/add-assets-to-customer.controller.js b/ui/src/app/asset/add-assets-to-customer.controller.js deleted file mode 100644 index 932fe2f78c..0000000000 --- a/ui/src/app/asset/add-assets-to-customer.controller.js +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/*@ngInject*/ -export default function AddAssetsToCustomerController(assetService, $mdDialog, $q, customerId, assets) { - - var vm = this; - - vm.assets = assets; - vm.searchText = ''; - - vm.assign = assign; - vm.cancel = cancel; - vm.hasData = hasData; - vm.noData = noData; - vm.searchAssetTextUpdated = searchAssetTextUpdated; - vm.toggleAssetSelection = toggleAssetSelection; - - vm.theAssets = { - getItemAtIndex: function (index) { - if (index > vm.assets.data.length) { - vm.theAssets.fetchMoreItems_(index); - return null; - } - var item = vm.assets.data[index]; - if (item) { - item.indexNumber = index + 1; - } - return item; - }, - - getLength: function () { - if (vm.assets.hasNext) { - return vm.assets.data.length + vm.assets.nextPageLink.limit; - } else { - return vm.assets.data.length; - } - }, - - fetchMoreItems_: function () { - if (vm.assets.hasNext && !vm.assets.pending) { - vm.assets.pending = true; - assetService.getTenantAssets(vm.assets.nextPageLink, false).then( - function success(assets) { - vm.assets.data = vm.assets.data.concat(assets.data); - vm.assets.nextPageLink = assets.nextPageLink; - vm.assets.hasNext = assets.hasNext; - if (vm.assets.hasNext) { - vm.assets.nextPageLink.limit = vm.assets.pageSize; - } - vm.assets.pending = false; - }, - function fail() { - vm.assets.hasNext = false; - vm.assets.pending = false; - }); - } - } - }; - - function cancel () { - $mdDialog.cancel(); - } - - function assign() { - var tasks = []; - for (var assetId in vm.assets.selections) { - tasks.push(assetService.assignAssetToCustomer(customerId, assetId)); - } - $q.all(tasks).then(function () { - $mdDialog.hide(); - }); - } - - function noData() { - return vm.assets.data.length == 0 && !vm.assets.hasNext; - } - - function hasData() { - return vm.assets.data.length > 0; - } - - function toggleAssetSelection($event, asset) { - $event.stopPropagation(); - var selected = angular.isDefined(asset.selected) && asset.selected; - asset.selected = !selected; - if (asset.selected) { - vm.assets.selections[asset.id.id] = true; - vm.assets.selectedCount++; - } else { - delete vm.assets.selections[asset.id.id]; - vm.assets.selectedCount--; - } - } - - function searchAssetTextUpdated() { - vm.assets = { - pageSize: vm.assets.pageSize, - data: [], - nextPageLink: { - limit: vm.assets.pageSize, - textSearch: vm.searchText - }, - selections: {}, - selectedCount: 0, - hasNext: true, - pending: false - }; - } - -} diff --git a/ui/src/app/asset/add-assets-to-customer.tpl.html b/ui/src/app/asset/add-assets-to-customer.tpl.html deleted file mode 100644 index 6bbf5a01c4..0000000000 --- a/ui/src/app/asset/add-assets-to-customer.tpl.html +++ /dev/null @@ -1,77 +0,0 @@ - - -
    - -
    -

    asset.assign-asset-to-customer

    - - - - -
    -
    - - - -
    -
    - asset.assign-asset-to-customer-text - - - - search - - - -
    - asset.no-assets-text - - - - - {{ asset.name }} - - - -
    -
    -
    -
    - - - - {{ 'action.assign' | translate }} - - {{ 'action.cancel' | - translate }} - - -
    -
    \ No newline at end of file diff --git a/ui/src/app/asset/asset-card.tpl.html b/ui/src/app/asset/asset-card.tpl.html deleted file mode 100644 index 3ef288d850..0000000000 --- a/ui/src/app/asset/asset-card.tpl.html +++ /dev/null @@ -1,22 +0,0 @@ - -
    -
    {{vm.item.type}}
    -
    {{'asset.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'
    -
    {{'asset.public' | translate}}
    -
    diff --git a/ui/src/app/asset/asset-fieldset.tpl.html b/ui/src/app/asset/asset-fieldset.tpl.html deleted file mode 100644 index cc08b40f0a..0000000000 --- a/ui/src/app/asset/asset-fieldset.tpl.html +++ /dev/null @@ -1,75 +0,0 @@ - -{{ 'asset.make-public' | translate }} -{{ 'asset.assign-to-customer' | translate }} -{{ isPublic ? 'asset.make-private' : 'asset.unassign-from-customer' | translate }} -{{ 'asset.delete' | translate }} - -
    - - - asset.copyId - -
    - - - - - - -
    - {{ 'asset.asset-public' | translate }} -
    -
    - - - -
    -
    asset.name-required
    -
    -
    - - - - - - - - - - -
    -
    diff --git a/ui/src/app/asset/asset.controller.js b/ui/src/app/asset/asset.controller.js deleted file mode 100644 index a25951b046..0000000000 --- a/ui/src/app/asset/asset.controller.js +++ /dev/null @@ -1,534 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import addAssetTemplate from './add-asset.tpl.html'; -import assetCard from './asset-card.tpl.html'; -import assignToCustomerTemplate from './assign-to-customer.tpl.html'; -import addAssetsToCustomerTemplate from './add-assets-to-customer.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export function AssetCardController(types) { - - var vm = this; - - vm.types = types; - - vm.isAssignedToCustomer = function() { - if (vm.item && vm.item.customerId && vm.parentCtl.assetsScope === 'tenant' && - vm.item.customerId.id != vm.types.id.nullUid && !vm.item.assignedCustomer.isPublic) { - return true; - } - return false; - } - - vm.isPublic = function() { - if (vm.item && vm.item.assignedCustomer && vm.parentCtl.assetsScope === 'tenant' && vm.item.assignedCustomer.isPublic) { - return true; - } - return false; - } -} - - -/*@ngInject*/ -export function AssetController($rootScope, userService, assetService, customerService, $state, $stateParams, - $document, $mdDialog, $q, $translate, types, importExport) { - - var customerId = $stateParams.customerId; - - var assetActionsList = []; - - var assetGroupActionsList = []; - - var assetAddItemActionsList = [ - { - onAction: function ($event) { - vm.grid.addItem($event); - }, - name: function() { return $translate.instant('action.add') }, - details: function() { return $translate.instant('asset.add-asset-text') }, - icon: "insert_drive_file" - }, - { - onAction: function ($event) { - importExport.importEntities($event, types.entityType.asset).then( - function() { - vm.grid.refreshList(); - } - ); - }, - name: function() { return $translate.instant('action.import') }, - details: function() { return $translate.instant('asset.import') }, - icon: "file_upload" - } - ]; - - var vm = this; - - vm.types = types; - - vm.assetGridConfig = { - deleteItemTitleFunc: deleteAssetTitle, - deleteItemContentFunc: deleteAssetText, - deleteItemsTitleFunc: deleteAssetsTitle, - deleteItemsActionTitleFunc: deleteAssetsActionTitle, - deleteItemsContentFunc: deleteAssetsText, - - saveItemFunc: saveAsset, - - getItemTitleFunc: getAssetTitle, - - itemCardController: 'AssetCardController', - itemCardTemplateUrl: assetCard, - parentCtl: vm, - - actionsList: assetActionsList, - groupActionsList: assetGroupActionsList, - addItemActions: assetAddItemActionsList, - - onGridInited: gridInited, - - addItemTemplateUrl: addAssetTemplate, - - addItemText: function() { return $translate.instant('asset.add-asset-text') }, - noItemsText: function() { return $translate.instant('asset.no-assets-text') }, - itemDetailsText: function() { return $translate.instant('asset.asset-details') }, - isDetailsReadOnly: isCustomerUser, - isSelectionEnabled: function () { - return !isCustomerUser(); - } - }; - - if (angular.isDefined($stateParams.items) && $stateParams.items !== null) { - vm.assetGridConfig.items = $stateParams.items; - } - - if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) { - vm.assetGridConfig.topIndex = $stateParams.topIndex; - } - - vm.assetsScope = $state.$current.data.assetsType; - - vm.assignToCustomer = assignToCustomer; - vm.makePublic = makePublic; - vm.unassignFromCustomer = unassignFromCustomer; - - initController(); - - function initController() { - var fetchAssetsFunction = null; - var deleteAssetFunction = null; - var refreshAssetsParamsFunction = null; - - var user = userService.getCurrentUser(); - - if (user.authority === 'CUSTOMER_USER') { - vm.assetsScope = 'customer_user'; - customerId = user.customerId; - } - if (customerId) { - vm.customerAssetsTitle = $translate.instant('customer.assets'); - customerService.getShortCustomerInfo(customerId).then( - function success(info) { - if (info.isPublic) { - vm.customerAssetsTitle = $translate.instant('customer.public-assets'); - } - } - ); - } - - if (vm.assetsScope === 'tenant') { - fetchAssetsFunction = function (pageLink, assetType) { - return assetService.getTenantAssets(pageLink, true, null, assetType); - }; - deleteAssetFunction = function (assetId) { - return assetService.deleteAsset(assetId); - }; - refreshAssetsParamsFunction = function() { - return {"topIndex": vm.topIndex}; - }; - - assetActionsList.push({ - onAction: function ($event, item) { - makePublic($event, item); - }, - name: function() { return $translate.instant('action.share') }, - details: function() { return $translate.instant('asset.make-public') }, - icon: "share", - isEnabled: function(asset) { - return asset && (!asset.customerId || asset.customerId.id === types.id.nullUid); - } - }); - - assetActionsList.push( - { - onAction: function ($event, item) { - assignToCustomer($event, [ item.id.id ]); - }, - name: function() { return $translate.instant('action.assign') }, - details: function() { return $translate.instant('asset.assign-to-customer') }, - icon: "assignment_ind", - isEnabled: function(asset) { - return asset && (!asset.customerId || asset.customerId.id === types.id.nullUid); - } - } - ); - - assetActionsList.push( - { - onAction: function ($event, item) { - unassignFromCustomer($event, item, false); - }, - name: function() { return $translate.instant('action.unassign') }, - details: function() { return $translate.instant('asset.unassign-from-customer') }, - icon: "assignment_return", - isEnabled: function(asset) { - return asset && asset.customerId && asset.customerId.id !== types.id.nullUid && !asset.assignedCustomer.isPublic; - } - } - ); - - assetActionsList.push({ - onAction: function ($event, item) { - unassignFromCustomer($event, item, true); - }, - name: function() { return $translate.instant('action.make-private') }, - details: function() { return $translate.instant('asset.make-private') }, - icon: "reply", - isEnabled: function(asset) { - return asset && asset.customerId && asset.customerId.id !== types.id.nullUid && asset.assignedCustomer.isPublic; - } - }); - - assetActionsList.push( - { - onAction: function ($event, item) { - vm.grid.deleteItem($event, item); - }, - name: function() { return $translate.instant('action.delete') }, - details: function() { return $translate.instant('asset.delete') }, - icon: "delete" - } - ); - - assetGroupActionsList.push( - { - onAction: function ($event, items) { - assignAssetsToCustomer($event, items); - }, - name: function() { return $translate.instant('asset.assign-assets') }, - details: function(selectedCount) { - return $translate.instant('asset.assign-assets-text', {count: selectedCount}, "messageformat"); - }, - icon: "assignment_ind" - } - ); - - assetGroupActionsList.push( - { - onAction: function ($event) { - vm.grid.deleteItems($event); - }, - name: function() { return $translate.instant('asset.delete-assets') }, - details: deleteAssetsActionTitle, - icon: "delete" - } - ); - - - - } else if (vm.assetsScope === 'customer' || vm.assetsScope === 'customer_user') { - fetchAssetsFunction = function (pageLink, assetType) { - return assetService.getCustomerAssets(customerId, pageLink, true, null, assetType); - }; - deleteAssetFunction = function (assetId) { - return assetService.unassignAssetFromCustomer(assetId); - }; - refreshAssetsParamsFunction = function () { - return {"customerId": customerId, "topIndex": vm.topIndex}; - }; - - if (vm.assetsScope === 'customer') { - assetActionsList.push( - { - onAction: function ($event, item) { - unassignFromCustomer($event, item, false); - }, - name: function() { return $translate.instant('action.unassign') }, - details: function() { return $translate.instant('asset.unassign-from-customer') }, - icon: "assignment_return", - isEnabled: function(asset) { - return asset && !asset.assignedCustomer.isPublic; - } - } - ); - assetActionsList.push( - { - onAction: function ($event, item) { - unassignFromCustomer($event, item, true); - }, - name: function() { return $translate.instant('action.make-private') }, - details: function() { return $translate.instant('asset.make-private') }, - icon: "reply", - isEnabled: function(asset) { - return asset && asset.assignedCustomer.isPublic; - } - } - ); - - assetGroupActionsList.push( - { - onAction: function ($event, items) { - unassignAssetsFromCustomer($event, items); - }, - name: function() { return $translate.instant('asset.unassign-assets') }, - details: function(selectedCount) { - return $translate.instant('asset.unassign-assets-action-title', {count: selectedCount}, "messageformat"); - }, - icon: "assignment_return" - } - ); - - vm.assetGridConfig.addItemAction = { - onAction: function ($event) { - addAssetsToCustomer($event); - }, - name: function() { return $translate.instant('asset.assign-assets') }, - details: function() { return $translate.instant('asset.assign-new-asset') }, - icon: "add" - }; - - - } else if (vm.assetsScope === 'customer_user') { - vm.assetGridConfig.addItemAction = {}; - } - vm.assetGridConfig.addItemActions = []; - - } - - vm.assetGridConfig.refreshParamsFunc = refreshAssetsParamsFunction; - vm.assetGridConfig.fetchItemsFunc = fetchAssetsFunction; - vm.assetGridConfig.deleteItemFunc = deleteAssetFunction; - - } - - function deleteAssetTitle(asset) { - return $translate.instant('asset.delete-asset-title', {assetName: asset.name}); - } - - function deleteAssetText() { - return $translate.instant('asset.delete-asset-text'); - } - - function deleteAssetsTitle(selectedCount) { - return $translate.instant('asset.delete-assets-title', {count: selectedCount}, 'messageformat'); - } - - function deleteAssetsActionTitle(selectedCount) { - return $translate.instant('asset.delete-assets-action-title', {count: selectedCount}, 'messageformat'); - } - - function deleteAssetsText () { - return $translate.instant('asset.delete-assets-text'); - } - - function gridInited(grid) { - vm.grid = grid; - } - - function getAssetTitle(asset) { - return asset ? asset.name : ''; - } - - function saveAsset(asset) { - var deferred = $q.defer(); - assetService.saveAsset(asset).then( - function success(savedAsset) { - $rootScope.$broadcast('assetSaved'); - var assets = [ savedAsset ]; - customerService.applyAssignedCustomersInfo(assets).then( - function success(items) { - if (items && items.length == 1) { - deferred.resolve(items[0]); - } else { - deferred.reject(); - } - }, - function fail() { - deferred.reject(); - } - ); - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - - function isCustomerUser() { - return vm.assetsScope === 'customer_user'; - } - - function assignToCustomer($event, assetIds) { - if ($event) { - $event.stopPropagation(); - } - var pageSize = 10; - customerService.getCustomers({limit: pageSize, textSearch: ''}).then( - function success(_customers) { - var customers = { - pageSize: pageSize, - data: _customers.data, - nextPageLink: _customers.nextPageLink, - selection: null, - hasNext: _customers.hasNext, - pending: false - }; - if (customers.hasNext) { - customers.nextPageLink.limit = pageSize; - } - $mdDialog.show({ - controller: 'AssignAssetToCustomerController', - controllerAs: 'vm', - templateUrl: assignToCustomerTemplate, - locals: {assetIds: assetIds, customers: customers}, - parent: angular.element($document[0].body), - fullscreen: true, - targetEvent: $event - }).then(function () { - vm.grid.refreshList(); - }, function () { - }); - }, - function fail() { - }); - } - - function addAssetsToCustomer($event) { - if ($event) { - $event.stopPropagation(); - } - var pageSize = 10; - assetService.getTenantAssets({limit: pageSize, textSearch: ''}, false).then( - function success(_assets) { - var assets = { - pageSize: pageSize, - data: _assets.data, - nextPageLink: _assets.nextPageLink, - selections: {}, - selectedCount: 0, - hasNext: _assets.hasNext, - pending: false - }; - if (assets.hasNext) { - assets.nextPageLink.limit = pageSize; - } - $mdDialog.show({ - controller: 'AddAssetsToCustomerController', - controllerAs: 'vm', - templateUrl: addAssetsToCustomerTemplate, - locals: {customerId: customerId, assets: assets}, - parent: angular.element($document[0].body), - fullscreen: true, - targetEvent: $event - }).then(function () { - vm.grid.refreshList(); - }, function () { - }); - }, - function fail() { - }); - } - - function assignAssetsToCustomer($event, items) { - var assetIds = []; - for (var id in items.selections) { - assetIds.push(id); - } - assignToCustomer($event, assetIds); - } - - function unassignFromCustomer($event, asset, isPublic) { - if ($event) { - $event.stopPropagation(); - } - var title; - var content; - var label; - if (isPublic) { - title = $translate.instant('asset.make-private-asset-title', {assetName: asset.name}); - content = $translate.instant('asset.make-private-asset-text'); - label = $translate.instant('asset.make-private'); - } else { - title = $translate.instant('asset.unassign-asset-title', {assetName: asset.name}); - content = $translate.instant('asset.unassign-asset-text'); - label = $translate.instant('asset.unassign-asset'); - } - var confirm = $mdDialog.confirm() - .targetEvent($event) - .title(title) - .htmlContent(content) - .ariaLabel(label) - .cancel($translate.instant('action.no')) - .ok($translate.instant('action.yes')); - $mdDialog.show(confirm).then(function () { - assetService.unassignAssetFromCustomer(asset.id.id).then(function success() { - vm.grid.refreshList(); - }); - }); - } - - function unassignAssetsFromCustomer($event, items) { - var confirm = $mdDialog.confirm() - .targetEvent($event) - .title($translate.instant('asset.unassign-assets-title', {count: items.selectedCount}, 'messageformat')) - .htmlContent($translate.instant('asset.unassign-assets-text')) - .ariaLabel($translate.instant('asset.unassign-asset')) - .cancel($translate.instant('action.no')) - .ok($translate.instant('action.yes')); - $mdDialog.show(confirm).then(function () { - var tasks = []; - for (var id in items.selections) { - tasks.push(assetService.unassignAssetFromCustomer(id)); - } - $q.all(tasks).then(function () { - vm.grid.refreshList(); - }); - }); - } - - function makePublic($event, asset) { - if ($event) { - $event.stopPropagation(); - } - var confirm = $mdDialog.confirm() - .targetEvent($event) - .title($translate.instant('asset.make-public-asset-title', {assetName: asset.name})) - .htmlContent($translate.instant('asset.make-public-asset-text')) - .ariaLabel($translate.instant('asset.make-public')) - .cancel($translate.instant('action.no')) - .ok($translate.instant('action.yes')); - $mdDialog.show(confirm).then(function () { - assetService.makeAssetPublic(asset.id.id).then(function success() { - vm.grid.refreshList(); - }); - }); - } -} diff --git a/ui/src/app/asset/asset.directive.js b/ui/src/app/asset/asset.directive.js deleted file mode 100644 index 2c2bd1f673..0000000000 --- a/ui/src/app/asset/asset.directive.js +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import assetFieldsetTemplate from './asset-fieldset.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function AssetDirective($compile, $templateCache, toast, $translate, types, assetService, customerService) { - var linker = function (scope, element) { - var template = $templateCache.get(assetFieldsetTemplate); - element.html(template); - - scope.types = types; - scope.isAssignedToCustomer = false; - scope.isPublic = false; - scope.assignedCustomer = null; - - scope.$watch('asset', function(newVal) { - if (newVal) { - if (scope.asset.customerId && scope.asset.customerId.id !== types.id.nullUid) { - scope.isAssignedToCustomer = true; - customerService.getShortCustomerInfo(scope.asset.customerId.id).then( - function success(customer) { - scope.assignedCustomer = customer; - scope.isPublic = customer.isPublic; - } - ); - } else { - scope.isAssignedToCustomer = false; - scope.isPublic = false; - scope.assignedCustomer = null; - } - } - }); - - scope.onAssetIdCopied = function() { - toast.showSuccess($translate.instant('asset.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left'); - }; - - - $compile(element.contents())(scope); - } - return { - restrict: "E", - link: linker, - scope: { - asset: '=', - isEdit: '=', - assetScope: '=', - theForm: '=', - onAssignToCustomer: '&', - onMakePublic: '&', - onUnassignFromCustomer: '&', - onDeleteAsset: '&' - } - }; -} diff --git a/ui/src/app/asset/asset.routes.js b/ui/src/app/asset/asset.routes.js deleted file mode 100644 index cfc4e77315..0000000000 --- a/ui/src/app/asset/asset.routes.js +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import assetsTemplate from './assets.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function AssetRoutes($stateProvider, types) { - $stateProvider - .state('home.assets', { - url: '/assets', - params: {'topIndex': 0}, - module: 'private', - auth: ['TENANT_ADMIN', 'CUSTOMER_USER'], - views: { - "content@home": { - templateUrl: assetsTemplate, - controller: 'AssetController', - controllerAs: 'vm' - } - }, - data: { - assetsType: 'tenant', - searchEnabled: true, - searchByEntitySubtype: true, - searchEntityType: types.entityType.asset, - pageTitle: 'asset.assets' - }, - ncyBreadcrumb: { - label: '{"icon": "domain", "label": "asset.assets"}' - } - }) - .state('home.customers.assets', { - url: '/:customerId/assets', - params: {'topIndex': 0}, - module: 'private', - auth: ['TENANT_ADMIN'], - views: { - "content@home": { - templateUrl: assetsTemplate, - controllerAs: 'vm', - controller: 'AssetController' - } - }, - data: { - assetsType: 'customer', - searchEnabled: true, - searchByEntitySubtype: true, - searchEntityType: types.entityType.asset, - pageTitle: 'customer.assets' - }, - ncyBreadcrumb: { - label: '{"icon": "domain", "label": "{{ vm.customerAssetsTitle }}", "translate": "false"}' - } - }); - -} diff --git a/ui/src/app/asset/assets.tpl.html b/ui/src/app/asset/assets.tpl.html deleted file mode 100644 index 7023c1104d..0000000000 --- a/ui/src/app/asset/assets.tpl.html +++ /dev/null @@ -1,75 +0,0 @@ - - - -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    diff --git a/ui/src/app/asset/assign-to-customer.controller.js b/ui/src/app/asset/assign-to-customer.controller.js deleted file mode 100644 index b5d331c8af..0000000000 --- a/ui/src/app/asset/assign-to-customer.controller.js +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/*@ngInject*/ -export default function AssignAssetToCustomerController(customerService, assetService, $mdDialog, $q, assetIds, customers) { - - var vm = this; - - vm.customers = customers; - vm.searchText = ''; - - vm.assign = assign; - vm.cancel = cancel; - vm.isCustomerSelected = isCustomerSelected; - vm.hasData = hasData; - vm.noData = noData; - vm.searchCustomerTextUpdated = searchCustomerTextUpdated; - vm.toggleCustomerSelection = toggleCustomerSelection; - - vm.theCustomers = { - getItemAtIndex: function (index) { - if (index > vm.customers.data.length) { - vm.theCustomers.fetchMoreItems_(index); - return null; - } - var item = vm.customers.data[index]; - if (item) { - item.indexNumber = index + 1; - } - return item; - }, - - getLength: function () { - if (vm.customers.hasNext) { - return vm.customers.data.length + vm.customers.nextPageLink.limit; - } else { - return vm.customers.data.length; - } - }, - - fetchMoreItems_: function () { - if (vm.customers.hasNext && !vm.customers.pending) { - vm.customers.pending = true; - customerService.getCustomers(vm.customers.nextPageLink).then( - function success(customers) { - vm.customers.data = vm.customers.data.concat(customers.data); - vm.customers.nextPageLink = customers.nextPageLink; - vm.customers.hasNext = customers.hasNext; - if (vm.customers.hasNext) { - vm.customers.nextPageLink.limit = vm.customers.pageSize; - } - vm.customers.pending = false; - }, - function fail() { - vm.customers.hasNext = false; - vm.customers.pending = false; - }); - } - } - }; - - function cancel() { - $mdDialog.cancel(); - } - - function assign() { - var tasks = []; - for (var i=0;i 0; - } - - function toggleCustomerSelection($event, customer) { - $event.stopPropagation(); - if (vm.isCustomerSelected(customer)) { - vm.customers.selection = null; - } else { - vm.customers.selection = customer; - } - } - - function isCustomerSelected(customer) { - return vm.customers.selection != null && customer && - customer.id.id === vm.customers.selection.id.id; - } - - function searchCustomerTextUpdated() { - vm.customers = { - pageSize: vm.customers.pageSize, - data: [], - nextPageLink: { - limit: vm.customers.pageSize, - textSearch: vm.searchText - }, - selection: null, - hasNext: true, - pending: false - }; - } -} diff --git a/ui/src/app/asset/assign-to-customer.tpl.html b/ui/src/app/asset/assign-to-customer.tpl.html deleted file mode 100644 index 031fc9fd1e..0000000000 --- a/ui/src/app/asset/assign-to-customer.tpl.html +++ /dev/null @@ -1,76 +0,0 @@ - - -
    - -
    -

    asset.assign-asset-to-customer

    - - - - -
    -
    - - - -
    -
    - asset.assign-to-customer-text - - - - search - - - -
    - customer.no-customers-text - - - - - {{ customer.title }} - - - -
    -
    -
    -
    - - - - {{ 'action.assign' | translate }} - - {{ 'action.cancel' | - translate }} - - -
    -
    \ No newline at end of file diff --git a/ui/src/app/asset/index.js b/ui/src/app/asset/index.js deleted file mode 100644 index c771826f61..0000000000 --- a/ui/src/app/asset/index.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import uiRouter from 'angular-ui-router'; -import thingsboardGrid from '../components/grid.directive'; -import thingsboardApiUser from '../api/user.service'; -import thingsboardApiAsset from '../api/asset.service'; -import thingsboardApiCustomer from '../api/customer.service'; - -import AssetRoutes from './asset.routes'; -import {AssetController, AssetCardController} from './asset.controller'; -import AssignAssetToCustomerController from './assign-to-customer.controller'; -import AddAssetsToCustomerController from './add-assets-to-customer.controller'; -import AssetDirective from './asset.directive'; - -export default angular.module('thingsboard.asset', [ - uiRouter, - thingsboardGrid, - thingsboardApiUser, - thingsboardApiAsset, - thingsboardApiCustomer -]) - .config(AssetRoutes) - .controller('AssetController', AssetController) - .controller('AssetCardController', AssetCardController) - .controller('AssignAssetToCustomerController', AssignAssetToCustomerController) - .controller('AddAssetsToCustomerController', AddAssetsToCustomerController) - .directive('tbAsset', AssetDirective) - .name; diff --git a/ui/src/app/audit/audit-log-details-dialog.controller.js b/ui/src/app/audit/audit-log-details-dialog.controller.js deleted file mode 100644 index 88d5d00ef6..0000000000 --- a/ui/src/app/audit/audit-log-details-dialog.controller.js +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import $ from 'jquery'; -import 'brace/ext/language_tools'; -import 'brace/ext/searchbox'; -import 'brace/mode/java'; -import 'brace/theme/github'; - -/* eslint-disable angular/angularelement */ - -import './audit-log-details-dialog.scss'; - -/*@ngInject*/ -export default function AuditLogDetailsDialogController($mdDialog, types, auditLog, showingCallback) { - - var vm = this; - - showingCallback.onShowing = function(scope, element) { - updateEditorSize(element, vm.actionData, 'tb-audit-log-action-data'); - vm.actionDataEditor.resize(); - if (vm.displayFailureDetails) { - updateEditorSize(element, vm.actionFailureDetails, 'tb-audit-log-failure-details'); - vm.failureDetailsEditor.resize(); - } - }; - - vm.types = types; - vm.auditLog = auditLog; - vm.displayFailureDetails = auditLog.actionStatus == types.auditLogActionStatus.FAILURE.value; - vm.actionData = auditLog.actionDataText; - vm.actionFailureDetails = auditLog.actionFailureDetails; - - vm.actionDataContentOptions = { - useWrapMode: false, - mode: 'java', - showGutter: false, - showPrintMargin: false, - theme: 'github', - advanced: { - enableSnippets: false, - enableBasicAutocompletion: false, - enableLiveAutocompletion: false - }, - onLoad: function (_ace) { - vm.actionDataEditor = _ace; - } - }; - - vm.failureDetailsContentOptions = { - useWrapMode: false, - mode: 'java', - showGutter: false, - showPrintMargin: false, - theme: 'github', - advanced: { - enableSnippets: false, - enableBasicAutocompletion: false, - enableLiveAutocompletion: false - }, - onLoad: function (_ace) { - vm.failureDetailsEditor = _ace; - } - }; - - function updateEditorSize(element, content, editorId) { - var newHeight = 200; - var newWidth = 600; - if (content && content.length > 0) { - var lines = content.split('\n'); - newHeight = 16 * lines.length + 16; - var maxLineLength = 0; - for (var i in lines) { - var line = lines[i].replace(/\t/g, ' ').replace(/\n/g, ''); - var lineLength = line.length; - maxLineLength = Math.max(maxLineLength, lineLength); - } - newWidth = 8 * maxLineLength + 16; - } - $('#'+editorId, element).height(newHeight.toString() + "px").css('min-height', newHeight.toString() + "px") - .width(newWidth.toString() + "px"); - } - - vm.close = close; - - function close () { - $mdDialog.hide(); - } - -} - -/* eslint-enable angular/angularelement */ diff --git a/ui/src/app/audit/audit-log-details-dialog.scss b/ui/src/app/audit/audit-log-details-dialog.scss deleted file mode 100644 index f2ec6e641b..0000000000 --- a/ui/src/app/audit/audit-log-details-dialog.scss +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#tb-audit-log-action-data, -#tb-audit-log-failure-details { - width: 100%; - min-width: 400px; - height: 100%; - min-height: 50px; - border: 1px solid #c0c0c0; -} diff --git a/ui/src/app/audit/audit-log-details-dialog.tpl.html b/ui/src/app/audit/audit-log-details-dialog.tpl.html deleted file mode 100644 index 7201082477..0000000000 --- a/ui/src/app/audit/audit-log-details-dialog.tpl.html +++ /dev/null @@ -1,49 +0,0 @@ - - - -
    -

    audit-log.audit-log-details

    - - - - -
    -
    - -
    - -
    -
    - - -
    -
    -
    -
    - - - {{ 'action.close' | - translate }} - - -
    diff --git a/ui/src/app/audit/audit-log-header.directive.js b/ui/src/app/audit/audit-log-header.directive.js deleted file mode 100644 index 56fd9638c2..0000000000 --- a/ui/src/app/audit/audit-log-header.directive.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import auditLogHeaderTemplate from './audit-log-header.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function AuditLogHeaderDirective($compile, $templateCache, types) { - - var linker = function (scope, element, attrs) { - - var template = $templateCache.get(auditLogHeaderTemplate); - element.html(template); - scope.auditLogMode = attrs.auditLogMode; - scope.types = types; - $compile(element.contents())(scope); - - }; - - return { - restrict: "A", - replace: false, - link: linker, - scope: false - }; -} diff --git a/ui/src/app/audit/audit-log-header.tpl.html b/ui/src/app/audit/audit-log-header.tpl.html deleted file mode 100644 index 6ff13a9bd3..0000000000 --- a/ui/src/app/audit/audit-log-header.tpl.html +++ /dev/null @@ -1,24 +0,0 @@ - -
    audit-log.timestamp
    -
    audit-log.entity-type
    -
    audit-log.entity-name
    -
    audit-log.user
    -
    audit-log.type
    -
    audit-log.status
    -
    audit-log.details
    diff --git a/ui/src/app/audit/audit-log-row.directive.js b/ui/src/app/audit/audit-log-row.directive.js deleted file mode 100644 index 7e17a97ecf..0000000000 --- a/ui/src/app/audit/audit-log-row.directive.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import auditLogDetailsDialogTemplate from './audit-log-details-dialog.tpl.html'; - -import auditLogRowTemplate from './audit-log-row.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function AuditLogRowDirective($compile, $templateCache, types, $mdDialog, $document) { - - var linker = function (scope, element, attrs) { - - var template = $templateCache.get(auditLogRowTemplate); - element.html(template); - - scope.auditLog = attrs.auditLog; - scope.auditLogMode = attrs.auditLogMode; - scope.types = types; - - scope.showAuditLogDetails = function($event) { - var onShowingCallback = { - onShowing: function(){} - } - $mdDialog.show({ - controller: 'AuditLogDetailsDialogController', - controllerAs: 'vm', - templateUrl: auditLogDetailsDialogTemplate, - locals: { - auditLog: scope.auditLog, - showingCallback: onShowingCallback - }, - parent: angular.element($document[0].body), - targetEvent: $event, - fullscreen: true, - multiple: true, - onShowing: function(scope, element) { - onShowingCallback.onShowing(scope, element); - } - }); - } - - $compile(element.contents())(scope); - } - - return { - restrict: "A", - replace: false, - link: linker, - scope: false - }; -} diff --git a/ui/src/app/audit/audit-log-row.tpl.html b/ui/src/app/audit/audit-log-row.tpl.html deleted file mode 100644 index c490b38d35..0000000000 --- a/ui/src/app/audit/audit-log-row.tpl.html +++ /dev/null @@ -1,36 +0,0 @@ - -
    {{ auditLog.createdTime | date : 'yyyy-MM-dd HH:mm:ss' }}
    -
    {{ auditLog.entityTypeText }}
    -
    {{ auditLog.entityName }}
    -
    {{ auditLog.userName }}
    -
    {{ auditLog.actionTypeText }}
    -
    {{ auditLog.actionStatusText }}
    -
    - - - {{ 'audit-log.details' | translate }} - - - more_horiz - - -
    diff --git a/ui/src/app/audit/audit-log-table.directive.js b/ui/src/app/audit/audit-log-table.directive.js deleted file mode 100644 index 1df16b88c5..0000000000 --- a/ui/src/app/audit/audit-log-table.directive.js +++ /dev/null @@ -1,262 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './audit-log.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import auditLogTableTemplate from './audit-log-table.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function AuditLogTableDirective($compile, $templateCache, $rootScope, $filter, $translate, types, auditLogService) { - - var linker = function (scope, element) { - - var template = $templateCache.get(auditLogTableTemplate); - - element.html(template); - - scope.types = types; - - var pageSize = 20; - var startTime = 0; - var endTime = 0; - - scope.timewindow = { - history: { - timewindowMs: 24 * 60 * 60 * 1000 // 1 day - } - } - - scope.topIndex = 0; - scope.searchText = ''; - - scope.theAuditLogs = { - getItemAtIndex: function (index) { - if (index > scope.auditLogs.filtered.length) { - scope.theAuditLogs.fetchMoreItems_(index); - return null; - } - return scope.auditLogs.filtered[index]; - }, - - getLength: function () { - if (scope.auditLogs.hasNext) { - return scope.auditLogs.filtered.length + scope.auditLogs.nextPageLink.limit; - } else { - return scope.auditLogs.filtered.length; - } - }, - - fetchMoreItems_: function () { - if (scope.auditLogs.hasNext && !scope.auditLogs.pending) { - var promise = getAuditLogsPromise(scope.auditLogs.nextPageLink); - if (promise) { - scope.auditLogs.pending = true; - promise.then( - function success(auditLogs) { - scope.auditLogs.data = scope.auditLogs.data.concat(prepareAuditLogsData(auditLogs.data)); - scope.auditLogs.filtered = $filter('filter')(scope.auditLogs.data, {$: scope.searchText}); - scope.auditLogs.nextPageLink = auditLogs.nextPageLink; - scope.auditLogs.hasNext = auditLogs.hasNext; - if (scope.auditLogs.hasNext) { - scope.auditLogs.nextPageLink.limit = pageSize; - } - scope.auditLogs.pending = false; - }, - function fail() { - scope.auditLogs.hasNext = false; - scope.auditLogs.pending = false; - }); - } else { - scope.auditLogs.hasNext = false; - } - } - } - }; - - function prepareAuditLogsData(data) { - data.forEach( - auditLog => { - auditLog.entityTypeText = $translate.instant(types.entityTypeTranslations[auditLog.entityId.entityType].type); - auditLog.actionTypeText = $translate.instant(types.auditLogActionType[auditLog.actionType].name); - auditLog.actionStatusText = $translate.instant(types.auditLogActionStatus[auditLog.actionStatus].name); - auditLog.actionDataText = auditLog.actionData ? angular.toJson(auditLog.actionData, true) : ''; - } - ); - return data; - } - - scope.$watch("entityId", function(newVal, prevVal) { - if (newVal && !angular.equals(newVal, prevVal)) { - resetFilter(); - scope.reload(); - } - }); - - scope.$watch("userId", function(newVal, prevVal) { - if (newVal && !angular.equals(newVal, prevVal)) { - resetFilter(); - scope.reload(); - } - }); - - scope.$watch("customerId", function(newVal, prevVal) { - if (newVal && !angular.equals(newVal, prevVal)) { - resetFilter(); - scope.reload(); - } - }); - - function getAuditLogsPromise(pageLink) { - switch(scope.auditLogMode) { - case types.auditLogMode.tenant: - return auditLogService.getAuditLogs(pageLink); - case types.auditLogMode.entity: - if (scope.entityType && scope.entityId) { - return auditLogService.getAuditLogsByEntityId(scope.entityType, scope.entityId, - pageLink); - } else { - return null; - } - case types.auditLogMode.user: - if (scope.userId) { - return auditLogService.getAuditLogsByUserId(scope.userId, pageLink); - } else { - return null; - } - case types.auditLogMode.customer: - if (scope.customerId) { - return auditLogService.getAuditLogsByCustomerId(scope.customerId, pageLink); - } else { - return null; - } - } - } - - function destroyWatchers() { - if (scope.timewindowWatchHandle) { - scope.timewindowWatchHandle(); - scope.timewindowWatchHandle = null; - } - if (scope.searchTextWatchHandle) { - scope.searchTextWatchHandle(); - scope.searchTextWatchHandle = null; - } - } - - function initWatchers() { - scope.timewindowWatchHandle = scope.$watch("timewindow", function(newVal, prevVal) { - if (newVal && !angular.equals(newVal, prevVal)) { - scope.reload(); - } - }, true); - - scope.searchTextWatchHandle = scope.$watch("searchText", function(newVal, prevVal) { - if (!angular.equals(newVal, prevVal)) { - scope.searchTextUpdated(); - } - }, true); - } - - function resetFilter() { - destroyWatchers(); - scope.timewindow = { - history: { - timewindowMs: 24 * 60 * 60 * 1000 // 1 day - } - }; - scope.searchText = ''; - initWatchers(); - } - - function updateTimeWindowRange () { - if (scope.timewindow.history.timewindowMs) { - var currentTime = (new Date).getTime(); - startTime = currentTime - scope.timewindow.history.timewindowMs; - endTime = currentTime; - } else { - startTime = scope.timewindow.history.fixedTimewindow.startTimeMs; - endTime = scope.timewindow.history.fixedTimewindow.endTimeMs; - } - } - - scope.reload = function() { - scope.topIndex = 0; - updateTimeWindowRange(); - scope.auditLogs = { - data: [], - filtered: [], - nextPageLink: { - limit: pageSize, - startTime: startTime, - endTime: endTime - }, - hasNext: true, - pending: false - }; - scope.theAuditLogs.getItemAtIndex(pageSize); - } - - scope.searchTextUpdated = function() { - scope.auditLogs.filtered = $filter('filter')(scope.auditLogs.data, {$: scope.searchText}); - scope.theAuditLogs.getItemAtIndex(pageSize); - } - - scope.noData = function() { - return scope.auditLogs.data.length == 0 && !scope.auditLogs.hasNext; - } - - scope.hasData = function() { - return scope.auditLogs.data.length > 0; - } - - scope.loading = function() { - return $rootScope.loading; - } - - scope.hasScroll = function() { - var repeatContainer = scope.repeatContainer[0]; - if (repeatContainer) { - var scrollElement = repeatContainer.children[0]; - if (scrollElement) { - return scrollElement.scrollHeight > scrollElement.clientHeight; - } - } - return false; - } - - scope.reload(); - - initWatchers(); - - $compile(element.contents())(scope); - } - - return { - restrict: "E", - link: linker, - scope: { - entityType: '=?', - entityId: '=?', - userId: '=?', - customerId: '=?', - auditLogMode: '@', - pageMode: '@?' - } - }; -} diff --git a/ui/src/app/audit/audit-log-table.tpl.html b/ui/src/app/audit/audit-log-table.tpl.html deleted file mode 100644 index b2fb4f8594..0000000000 --- a/ui/src/app/audit/audit-log-table.tpl.html +++ /dev/null @@ -1,68 +0,0 @@ - - -
    -
    - -
    - - search - - {{'audit-log.search' | translate}} - - - - - - - - close - - {{ 'audit-log.clear-search' | translate }} - - - - refresh - - {{ 'action.refresh' | translate }} - - -
    -
    -
    - - - - - - audit-log.no-audit-logs-prompt - - - - - - - - -
    -
    -
    diff --git a/ui/src/app/audit/audit-log.routes.js b/ui/src/app/audit/audit-log.routes.js deleted file mode 100644 index 762f4395ad..0000000000 --- a/ui/src/app/audit/audit-log.routes.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import auditLogsTemplate from './audit-logs.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function AuditLogRoutes($stateProvider) { - $stateProvider - .state('home.auditLogs', { - url: '/auditLogs', - module: 'private', - auth: ['TENANT_ADMIN'], - views: { - "content@home": { - templateUrl: auditLogsTemplate, - controller: 'AuditLogsController', - controllerAs: 'vm' - } - }, - data: { - searchEnabled: false, - pageTitle: 'audit-log.audit-logs' - }, - ncyBreadcrumb: { - label: '{"icon": "track_changes", "label": "audit-log.audit-logs"}' - } - }); -} diff --git a/ui/src/app/audit/audit-log.scss b/ui/src/app/audit/audit-log.scss deleted file mode 100644 index 728b77b254..0000000000 --- a/ui/src/app/audit/audit-log.scss +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-audit-logs { - background-color: #fff; - - .tb-audit-log-margin-18px { - margin-bottom: 18px; - } - - .tb-audit-log-toolbar { - font-size: 20px; - } - - md-input-container.tb-audit-log-search-input { - .md-errors-spacer { - min-height: 0; - } - } -} - -.tb-audit-log-container { - overflow-x: auto; -} - -md-list.tb-audit-log-table { - min-width: 700px; - padding: 0; - - &.tb-audit-log-table-full { - min-width: 900px; - } - - md-list-item { - padding: 0; - } - - .tb-row { - height: 48px; - padding: 0; - overflow: hidden; - } - - .tb-row:hover { - background-color: #eee; - } - - .tb-header:hover { - background: none; - } - - .tb-header { - .tb-cell { - font-size: 12px; - font-weight: 700; - color: rgba(0, 0, 0, .54); - white-space: nowrap; - background: none; - } - } - - .tb-cell { - padding: 0 24px; - margin: auto 0; - overflow: hidden; - font-size: 13px; - color: rgba(0, 0, 0, .87); - text-align: left; - vertical-align: middle; - - .md-button { - padding: 0; - margin: 0; - } - } - - .tb-cell.tb-number { - text-align: right; - } -} diff --git a/ui/src/app/audit/audit-logs.controller.js b/ui/src/app/audit/audit-logs.controller.js deleted file mode 100644 index 94df0a5e08..0000000000 --- a/ui/src/app/audit/audit-logs.controller.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/*@ngInject*/ -export default function AuditLogsController(types) { - - var vm = this; - - vm.types = types; - -} \ No newline at end of file diff --git a/ui/src/app/audit/audit-logs.tpl.html b/ui/src/app/audit/audit-logs.tpl.html deleted file mode 100644 index 0a3d377989..0000000000 --- a/ui/src/app/audit/audit-logs.tpl.html +++ /dev/null @@ -1,22 +0,0 @@ - - - diff --git a/ui/src/app/audit/index.js b/ui/src/app/audit/index.js deleted file mode 100644 index e2e29ea193..0000000000 --- a/ui/src/app/audit/index.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import AuditLogRoutes from './audit-log.routes'; -import AuditLogsController from './audit-logs.controller'; -import AuditLogDetailsDialogController from './audit-log-details-dialog.controller'; -import AuditLogHeaderDirective from './audit-log-header.directive'; -import AuditLogRowDirective from './audit-log-row.directive'; -import AuditLogTableDirective from './audit-log-table.directive'; - -export default angular.module('thingsboard.auditLog', []) - .config(AuditLogRoutes) - .controller('AuditLogsController', AuditLogsController) - .controller('AuditLogDetailsDialogController', AuditLogDetailsDialogController) - .directive('tbAuditLogHeader', AuditLogHeaderDirective) - .directive('tbAuditLogRow', AuditLogRowDirective) - .directive('tbAuditLogTable', AuditLogTableDirective) - .name; diff --git a/ui/src/app/common/dashboard-utils.service.js b/ui/src/app/common/dashboard-utils.service.js deleted file mode 100644 index fcad288865..0000000000 --- a/ui/src/app/common/dashboard-utils.service.js +++ /dev/null @@ -1,566 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default angular.module('thingsboard.dashboardUtils', []) - .factory('dashboardUtils', DashboardUtils) - .name; - -/*@ngInject*/ -function DashboardUtils(types, utils, timeService) { - - var service = { - validateAndUpdateDashboard: validateAndUpdateDashboard, - validateAndUpdateWidget: validateAndUpdateWidget, - getRootStateId: getRootStateId, - createSingleWidgetDashboard: createSingleWidgetDashboard, - createSingleEntityFilter: createSingleEntityFilter, - getStateLayoutsData: getStateLayoutsData, - createDefaultState: createDefaultState, - createDefaultLayoutData: createDefaultLayoutData, - setLayouts: setLayouts, - updateLayoutSettings: updateLayoutSettings, - addWidgetToLayout: addWidgetToLayout, - removeWidgetFromLayout: removeWidgetFromLayout, - isSingleLayoutDashboard: isSingleLayoutDashboard, - removeUnusedWidgets: removeUnusedWidgets, - getWidgetsArray: getWidgetsArray - }; - - return service; - - function validateAndUpdateEntityAliases(configuration, datasourcesByAliasId, targetDevicesByAliasId) { - var aliasId, entityAlias; - if (angular.isUndefined(configuration.entityAliases)) { - configuration.entityAliases = {}; - if (configuration.deviceAliases) { - var deviceAliases = configuration.deviceAliases; - for (aliasId in deviceAliases) { - var deviceAlias = deviceAliases[aliasId]; - entityAlias = validateAndUpdateDeviceAlias(aliasId, deviceAlias, datasourcesByAliasId, targetDevicesByAliasId); - configuration.entityAliases[entityAlias.id] = entityAlias; - } - delete configuration.deviceAliases; - } - } else { - var entityAliases = configuration.entityAliases; - for (aliasId in entityAliases) { - entityAlias = entityAliases[aliasId]; - entityAlias = validateAndUpdateEntityAlias(aliasId, entityAlias, datasourcesByAliasId, targetDevicesByAliasId); - if (aliasId != entityAlias.id) { - delete entityAliases[aliasId]; - } - entityAliases[entityAlias.id] = entityAlias; - } - } - return configuration; - } - - function validateAliasId(aliasId, datasourcesByAliasId, targetDevicesByAliasId) { - if (!aliasId || !angular.isString(aliasId) || aliasId.length != 36) { - var newAliasId = utils.guid(); - var aliasDatasources = datasourcesByAliasId[aliasId]; - if (aliasDatasources) { - aliasDatasources.forEach( - function(datasource) { - datasource.entityAliasId = newAliasId; - } - ); - } - var targetDeviceAliasIdsList = targetDevicesByAliasId[aliasId]; - if (targetDeviceAliasIdsList) { - targetDeviceAliasIdsList.forEach( - function(targetDeviceAliasIds) { - targetDeviceAliasIds[0] = newAliasId; - } - ); - } - return newAliasId; - } else { - return aliasId; - } - } - - function validateAndUpdateDeviceAlias(aliasId, deviceAlias, datasourcesByAliasId, targetDevicesByAliasId) { - aliasId = validateAliasId(aliasId, datasourcesByAliasId, targetDevicesByAliasId); - var alias = deviceAlias.alias; - var entityAlias = { - id: aliasId, - alias: alias, - filter: { - type: null, - entityType: types.entityType.device, - resolveMultiple: false - }, - } - if (deviceAlias.deviceFilter) { - entityAlias.filter.type = - deviceAlias.deviceFilter.useFilter ? types.aliasFilterType.entityName.value : types.aliasFilterType.entityList.value; - if (entityAlias.filter.type == types.aliasFilterType.entityList.value) { - entityAlias.filter.entityList = deviceAlias.deviceFilter.deviceList; - } else { - entityAlias.filter.entityNameFilter = deviceAlias.deviceFilter.deviceNameFilter; - } - } else { - entityAlias.filter.type = types.aliasFilterType.entityList.value; - entityAlias.filter.entityList = [deviceAlias.deviceId]; - } - return entityAlias; - } - - function validateAndUpdateEntityAlias(aliasId, entityAlias, datasourcesByAliasId, targetDevicesByAliasId) { - entityAlias.id = validateAliasId(aliasId, datasourcesByAliasId, targetDevicesByAliasId); - if (!entityAlias.filter) { - entityAlias.filter = { - type: entityAlias.entityFilter.useFilter ? types.aliasFilterType.entityName.value : types.aliasFilterType.entityList.value, - entityType: entityAlias.entityType, - resolveMultiple: false - } - if (entityAlias.filter.type == types.aliasFilterType.entityList.value) { - entityAlias.filter.entityList = entityAlias.entityFilter.entityList; - } else { - entityAlias.filter.entityNameFilter = entityAlias.entityFilter.entityNameFilter; - } - delete entityAlias.entityType; - delete entityAlias.entityFilter; - } - return entityAlias; - } - - function validateAndUpdateWidget(widget) { - if (!widget.config) { - widget.config = {}; - } - if (!widget.config.datasources) { - widget.config.datasources = []; - } - widget.config.datasources.forEach(function(datasource) { - if (datasource.type === 'device') { - datasource.type = types.datasourceType.entity; - } - if (datasource.deviceAliasId) { - datasource.entityAliasId = datasource.deviceAliasId; - delete datasource.deviceAliasId; - } - }); - //TODO: Temp workaround - if (widget.isSystemType && widget.bundleAlias == 'charts' && widget.typeAlias == 'timeseries') { - widget.typeAlias = 'basic_timeseries'; - } - return widget; - } - - function createDefaultLayoutData() { - return { - widgets: {}, - gridSettings: { - backgroundColor: '#eeeeee', - color: 'rgba(0,0,0,0.870588)', - columns: 24, - margins: [10, 10], - backgroundSizeMode: '100%' - } - }; - } - - function createDefaultLayouts() { - return { - 'main': createDefaultLayoutData() - }; - } - - function createDefaultState(name, root) { - return { - name: name, - root: root, - layouts: createDefaultLayouts() - } - } - - function validateAndUpdateDashboard(dashboard) { - if (!dashboard.configuration) { - dashboard.configuration = {}; - } - if (angular.isUndefined(dashboard.configuration.widgets)) { - dashboard.configuration.widgets = {}; - } else if (angular.isArray(dashboard.configuration.widgets)) { - var widgetsMap = {}; - dashboard.configuration.widgets.forEach(function (widget) { - if (!widget.id) { - widget.id = utils.guid(); - } - widgetsMap[widget.id] = widget; - }); - dashboard.configuration.widgets = widgetsMap; - } - for (var id in dashboard.configuration.widgets) { - var widget = dashboard.configuration.widgets[id]; - dashboard.configuration.widgets[id] = validateAndUpdateWidget(widget); - } - if (angular.isUndefined(dashboard.configuration.states)) { - dashboard.configuration.states = { - 'default': createDefaultState(dashboard.title, true) - }; - - var mainLayout = dashboard.configuration.states['default'].layouts['main']; - for (id in dashboard.configuration.widgets) { - widget = dashboard.configuration.widgets[id]; - mainLayout.widgets[id] = { - sizeX: widget.sizeX, - sizeY: widget.sizeY, - row: widget.row, - col: widget.col, - }; - if (angular.isDefined(widget.config.mobileHeight)) { - mainLayout.widgets[id].mobileHeight = widget.config.mobileHeight; - } - if (angular.isDefined(widget.config.mobileOrder)) { - mainLayout.widgets[id].mobileOrder = widget.config.mobileOrder; - } - } - } else { - var states = dashboard.configuration.states; - var rootFound = false; - for (var stateId in states) { - var state = states[stateId]; - if (angular.isUndefined(state.root)) { - state.root = false; - } else if (state.root) { - rootFound = true; - } - } - if (!rootFound) { - var firstStateId = Object.keys(states)[0]; - states[firstStateId].root = true; - } - } - - var datasourcesByAliasId = {}; - var targetDevicesByAliasId = {}; - for (var widgetId in dashboard.configuration.widgets) { - widget = dashboard.configuration.widgets[widgetId]; - widget.config.datasources.forEach(function (datasource) { - if (datasource.entityAliasId) { - var aliasId = datasource.entityAliasId; - var aliasDatasources = datasourcesByAliasId[aliasId]; - if (!aliasDatasources) { - aliasDatasources = []; - datasourcesByAliasId[aliasId] = aliasDatasources; - } - aliasDatasources.push(datasource); - } - }); - if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length) { - var aliasId = widget.config.targetDeviceAliasIds[0]; - var targetDeviceAliasIdsList = targetDevicesByAliasId[aliasId]; - if (!targetDeviceAliasIdsList) { - targetDeviceAliasIdsList = []; - targetDevicesByAliasId[aliasId] = targetDeviceAliasIdsList; - } - targetDeviceAliasIdsList.push(widget.config.targetDeviceAliasIds); - } - } - - dashboard.configuration = validateAndUpdateEntityAliases(dashboard.configuration, datasourcesByAliasId, targetDevicesByAliasId); - - if (angular.isUndefined(dashboard.configuration.timewindow)) { - dashboard.configuration.timewindow = timeService.defaultTimewindow(); - } - if (angular.isUndefined(dashboard.configuration.settings)) { - dashboard.configuration.settings = {}; - dashboard.configuration.settings.stateControllerId = 'entity'; - dashboard.configuration.settings.showTitle = false; - dashboard.configuration.settings.showDashboardsSelect = true; - dashboard.configuration.settings.showEntitiesSelect = true; - dashboard.configuration.settings.showDashboardTimewindow = true; - dashboard.configuration.settings.showDashboardExport = true; - dashboard.configuration.settings.toolbarAlwaysOpen = true; - } else { - if (angular.isUndefined(dashboard.configuration.settings.stateControllerId)) { - dashboard.configuration.settings.stateControllerId = 'entity'; - } - } - if (angular.isDefined(dashboard.configuration.gridSettings)) { - var gridSettings = dashboard.configuration.gridSettings; - if (angular.isDefined(gridSettings.showTitle)) { - dashboard.configuration.settings.showTitle = gridSettings.showTitle; - delete gridSettings.showTitle; - } - if (angular.isDefined(gridSettings.titleColor)) { - dashboard.configuration.settings.titleColor = gridSettings.titleColor; - delete gridSettings.titleColor; - } - if (angular.isDefined(gridSettings.showDevicesSelect)) { - dashboard.configuration.settings.showEntitiesSelect = gridSettings.showDevicesSelect; - delete gridSettings.showDevicesSelect; - } - if (angular.isDefined(gridSettings.showEntitiesSelect)) { - dashboard.configuration.settings.showEntitiesSelect = gridSettings.showEntitiesSelect; - delete gridSettings.showEntitiesSelect; - } - if (angular.isDefined(gridSettings.showDashboardTimewindow)) { - dashboard.configuration.settings.showDashboardTimewindow = gridSettings.showDashboardTimewindow; - delete gridSettings.showDashboardTimewindow; - } - if (angular.isDefined(gridSettings.showDashboardExport)) { - dashboard.configuration.settings.showDashboardExport = gridSettings.showDashboardExport; - delete gridSettings.showDashboardExport; - } - dashboard.configuration.states['default'].layouts['main'].gridSettings = gridSettings; - delete dashboard.configuration.gridSettings; - } - return dashboard; - } - - function getRootStateId(states) { - for (var stateId in states) { - var state = states[stateId]; - if (state.root) { - return stateId; - } - } - return Object.keys(states)[0]; - } - - function createSingleWidgetDashboard(widget) { - if (!widget.id) { - widget.id = utils.guid(); - } - var dashboard = {}; - dashboard = validateAndUpdateDashboard(dashboard); - dashboard.configuration.widgets[widget.id] = widget; - dashboard.configuration.states['default'].layouts['main'].widgets[widget.id] = { - sizeX: widget.sizeX, - sizeY: widget.sizeY, - row: widget.row, - col: widget.col, - }; - return dashboard; - } - - function createSingleEntityFilter(entityType, entityId) { - return { - type: types.aliasFilterType.singleEntity.value, - singleEntity: { entityType: entityType, id: entityId }, - resolveMultiple: false - }; - } - - function getStateLayoutsData(dashboard, targetState) { - var dashboardConfiguration = dashboard.configuration; - var states = dashboardConfiguration.states; - var state = states[targetState]; - if (state) { - var allWidgets = dashboardConfiguration.widgets; - var result = {}; - for (var l in state.layouts) { - var layout = state.layouts[l]; - if (layout) { - result[l] = { - widgets: [], - widgetLayouts: {}, - gridSettings: {} - } - for (var id in layout.widgets) { - result[l].widgets.push(allWidgets[id]); - } - result[l].widgetLayouts = layout.widgets; - result[l].gridSettings = layout.gridSettings; - } - } - return result; - } else { - return null; - } - } - - function setLayouts(dashboard, targetState, newLayouts) { - var dashboardConfiguration = dashboard.configuration; - var states = dashboardConfiguration.states; - var state = states[targetState]; - var addedCount = 0; - var removedCount = 0; - for (var l in state.layouts) { - if (!newLayouts[l]) { - removedCount++; - } - } - for (l in newLayouts) { - if (!state.layouts[l]) { - addedCount++; - } - } - state.layouts = newLayouts; - var layoutsCount = Object.keys(state.layouts).length; - var newColumns; - if (addedCount) { - for (l in state.layouts) { - newColumns = state.layouts[l].gridSettings.columns * (layoutsCount - addedCount) / layoutsCount; - state.layouts[l].gridSettings.columns = newColumns; - } - } - if (removedCount) { - for (l in state.layouts) { - newColumns = state.layouts[l].gridSettings.columns * (layoutsCount + removedCount) / layoutsCount; - state.layouts[l].gridSettings.columns = newColumns; - } - } - removeUnusedWidgets(dashboard); - } - - function updateLayoutSettings(layout, gridSettings) { - var prevGridSettings = layout.gridSettings; - var prevColumns = prevGridSettings ? prevGridSettings.columns : 24; - var ratio = gridSettings.columns / prevColumns; - layout.gridSettings = gridSettings; - var maxRow = 0; - for (var w in layout.widgets) { - var widget = layout.widgets[w]; - maxRow = Math.max(maxRow, widget.row + widget.sizeY); - } - var newMaxRow = Math.round(maxRow * ratio); - for (w in layout.widgets) { - widget = layout.widgets[w]; - if (widget.row + widget.sizeY == maxRow) { - widget.row = Math.round(widget.row * ratio); - widget.sizeY = newMaxRow - widget.row; - } else { - widget.row = Math.round(widget.row * ratio); - widget.sizeY = Math.round(widget.sizeY * ratio); - } - widget.sizeX = Math.round(widget.sizeX * ratio); - widget.col = Math.round(widget.col * ratio); - if (widget.col + widget.sizeX > gridSettings.columns) { - widget.sizeX = gridSettings.columns - widget.col; - } - } - } - - function addWidgetToLayout(dashboard, targetState, targetLayout, widget, originalColumns, originalSize, row, column) { - var dashboardConfiguration = dashboard.configuration; - var states = dashboardConfiguration.states; - var state = states[targetState]; - var layout = state.layouts[targetLayout]; - var layoutCount = Object.keys(state.layouts).length; - if (!widget.id) { - widget.id = utils.guid(); - } - if (!dashboardConfiguration.widgets[widget.id]) { - dashboardConfiguration.widgets[widget.id] = widget; - } - var widgetLayout = { - sizeX: originalSize ? originalSize.sizeX : widget.sizeX, - sizeY: originalSize ? originalSize.sizeY : widget.sizeY, - mobileOrder: widget.config.mobileOrder, - mobileHeight: widget.config.mobileHeight - }; - - if (angular.isUndefined(originalColumns)) { - originalColumns = 24; - } - - var gridSettings = layout.gridSettings; - var columns = 24; - if (gridSettings && gridSettings.columns) { - columns = gridSettings.columns; - } - - columns = columns * layoutCount; - - if (columns != originalColumns) { - var ratio = columns / originalColumns; - widgetLayout.sizeX *= ratio; - widgetLayout.sizeY *= ratio; - } - - if (row > -1 && column > - 1) { - widgetLayout.row = row; - widgetLayout.col = column; - } else { - row = 0; - for (var w in layout.widgets) { - var existingLayout = layout.widgets[w]; - var wRow = existingLayout.row ? existingLayout.row : 0; - var wSizeY = existingLayout.sizeY ? existingLayout.sizeY : 1; - var bottom = wRow + wSizeY; - row = Math.max(row, bottom); - } - widgetLayout.row = row; - widgetLayout.col = 0; - } - - layout.widgets[widget.id] = widgetLayout; - } - - function removeWidgetFromLayout(dashboard, targetState, targetLayout, widgetId) { - var dashboardConfiguration = dashboard.configuration; - var states = dashboardConfiguration.states; - var state = states[targetState]; - var layout = state.layouts[targetLayout]; - delete layout.widgets[widgetId]; - removeUnusedWidgets(dashboard); - } - - function isSingleLayoutDashboard(dashboard) { - var dashboardConfiguration = dashboard.configuration; - var states = dashboardConfiguration.states; - var stateKeys = Object.keys(states); - if (stateKeys.length === 1) { - var state = states[stateKeys[0]]; - var layouts = state.layouts; - var layoutKeys = Object.keys(layouts); - if (layoutKeys.length === 1) { - return { - state: stateKeys[0], - layout: layoutKeys[0] - } - } - } - return null; - } - - function removeUnusedWidgets(dashboard) { - var dashboardConfiguration = dashboard.configuration; - var states = dashboardConfiguration.states; - var widgets = dashboardConfiguration.widgets; - for (var widgetId in widgets) { - var found = false; - for (var s in states) { - var state = states[s]; - for (var l in state.layouts) { - var layout = state.layouts[l]; - if (layout.widgets[widgetId]) { - found = true; - break; - } - } - } - if (!found) { - delete dashboardConfiguration.widgets[widgetId]; - } - - } - } - - function getWidgetsArray(dashboard) { - var widgetsArray = []; - var dashboardConfiguration = dashboard.configuration; - var widgets = dashboardConfiguration.widgets; - for (var widgetId in widgets) { - var widget = widgets[widgetId]; - widgetsArray.push(widget); - } - return widgetsArray; - } -} diff --git a/ui/src/app/common/raf.provider.js b/ui/src/app/common/raf.provider.js deleted file mode 100644 index f0859d9c4f..0000000000 --- a/ui/src/app/common/raf.provider.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default angular.module('thingsboard.raf', []) - .provider('tbRaf', TbRAFProvider) - .name; - -function TbRAFProvider() { - /*@ngInject*/ - this.$get = function($window, $timeout) { - var requestAnimationFrame = $window.requestAnimationFrame || - $window.webkitRequestAnimationFrame; - - var cancelAnimationFrame = $window.cancelAnimationFrame || - $window.webkitCancelAnimationFrame || - $window.webkitCancelRequestAnimationFrame; - - var rafSupported = !!requestAnimationFrame; - var raf = rafSupported - ? function(fn) { - var id = requestAnimationFrame(fn); - return function() { - cancelAnimationFrame(id); - }; - } - : function(fn) { - var timer = $timeout(fn, 16.66, false); - return function() { - $timeout.cancel(timer); - }; - }; - - raf.supported = rafSupported; - - return raf; - }; -} diff --git a/ui/src/app/common/thirdparty-fix.js b/ui/src/app/common/thirdparty-fix.js deleted file mode 100644 index 3176ca1295..0000000000 --- a/ui/src/app/common/thirdparty-fix.js +++ /dev/null @@ -1,458 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import tinycolor from 'tinycolor2'; -import moment from 'moment'; - -export default angular.module('thingsboard.thirdpartyFix', []) - .factory('Fullscreen', Fullscreen) - .factory('$mdColorPicker', mdColorPicker) - .provider('$mdpDatePicker', mdpDatePicker) - .provider('$mdpTimePicker', mdpTimePicker) - .name; - -/*@ngInject*/ -function Fullscreen($document, $rootScope) { - - /* eslint-disable */ - - var document = $document[0]; - - // ensure ALLOW_KEYBOARD_INPUT is available and enabled - var isKeyboardAvailbleOnFullScreen = (typeof Element !== 'undefined' && 'ALLOW_KEYBOARD_INPUT' in Element) && Element.ALLOW_KEYBOARD_INPUT; - - var emitter = $rootScope.$new(); - - // listen event on document instead of element to avoid firefox limitation - // see https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Using_full_screen_mode - $document.on('fullscreenchange webkitfullscreenchange mozfullscreenchange MSFullscreenChange', function(){ - emitter.$emit('FBFullscreen.change', serviceInstance.isEnabled()); - }); - - var serviceInstance = { - $on: angular.bind(emitter, emitter.$on), - all: function() { - serviceInstance.enable( document.documentElement ); - }, - enable: function(element) { - if(element.requestFullScreen) { - element.requestFullScreen(); - } else if(element.mozRequestFullScreen) { - element.mozRequestFullScreen(); - } else if(element.webkitRequestFullscreen) { - // Safari temporary fix - //if (/Version\/[\d]{1,2}(\.[\d]{1,2}){1}(\.(\d){1,2}){0,1} Safari/.test(navigator.userAgent)) { - if (/Safari/.test(navigator.userAgent)) { - element.webkitRequestFullscreen(); - } else { - element.webkitRequestFullscreen(isKeyboardAvailbleOnFullScreen); - } - } else if (element.msRequestFullscreen) { - element.msRequestFullscreen(); - } - }, - cancel: function() { - if(document.cancelFullScreen) { - document.cancelFullScreen(); - } else if(document.mozCancelFullScreen) { - document.mozCancelFullScreen(); - } else if(document.webkitExitFullscreen) { - document.webkitExitFullscreen(); - } else if (document.msExitFullscreen) { - document.msExitFullscreen(); - } - }, - isEnabled: function(){ - var fullscreenElement = document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement; - return fullscreenElement ? true : false; - }, - toggleAll: function(){ - serviceInstance.isEnabled() ? serviceInstance.cancel() : serviceInstance.all(); - }, - isSupported: function(){ - var docElm = document.documentElement; - var requestFullscreen = docElm.requestFullScreen || docElm.mozRequestFullScreen || docElm.webkitRequestFullscreen || docElm.msRequestFullscreen; - return requestFullscreen ? true : false; - } - }; - - /* eslint-enable */ - - return serviceInstance; -} - -/*@ngInject*/ -function mdColorPicker($q, $mdDialog, mdColorPickerHistory) { - var dialog; - - /* eslint-disable angular/definedundefined */ - - return { - show: function (options) - { - if ( options === undefined ) { - options = {}; - } - //console.log( 'DIALOG OPTIONS', options ); - // Defaults - // Dialog Properties - options.hasBackdrop = options.hasBackdrop === undefined ? true : options.hasBackdrop; - options.clickOutsideToClose = options.clickOutsideToClose === undefined ? true : options.clickOutsideToClose; - options.defaultValue = options.defaultValue === undefined ? '#FFFFFF' : options.defaultValue; - options.focusOnOpen = options.focusOnOpen === undefined ? false : options.focusOnOpen; - options.preserveScope = options.preserveScope === undefined ? true : options.preserveScope; - if (options.skipHide !== undefined) { - options.multiple = options.skipHide; - } - if (options.multiple === undefined) { - options.multiple = true; - } - - // mdColorPicker Properties - options.mdColorAlphaChannel = options.mdColorAlphaChannel === undefined ? false : options.mdColorAlphaChannel; - options.mdColorSpectrum = options.mdColorSpectrum === undefined ? true : options.mdColorSpectrum; - options.mdColorSliders = options.mdColorSliders === undefined ? true : options.mdColorSliders; - options.mdColorGenericPalette = options.mdColorGenericPalette === undefined ? true : options.mdColorGenericPalette; - options.mdColorMaterialPalette = options.mdColorMaterialPalette === undefined ? true : options.mdColorMaterialPalette; - options.mdColorHistory = options.mdColorHistory === undefined ? true : options.mdColorHistory; - - - dialog = $mdDialog.show({ - templateUrl: 'mdColorPickerDialog.tpl.html', - hasBackdrop: options.hasBackdrop, - clickOutsideToClose: options.clickOutsideToClose, - - controller: ['$scope', 'options', function( $scope, options ) { - //console.log( "DIALOG CONTROLLER OPEN", Date.now() - dateClick ); - $scope.close = function close() - { - $mdDialog.cancel(); - }; - $scope.ok = function ok() - { - $mdDialog.hide( $scope.value ); - }; - $scope.hide = $scope.ok; - - - - $scope.value = options.value; - $scope.default = options.defaultValue; - $scope.random = options.random; - - $scope.mdColorAlphaChannel = options.mdColorAlphaChannel; - $scope.mdColorSpectrum = options.mdColorSpectrum; - $scope.mdColorSliders = options.mdColorSliders; - $scope.mdColorGenericPalette = options.mdColorGenericPalette; - $scope.mdColorMaterialPalette = options.mdColorMaterialPalette; - $scope.mdColorHistory = options.mdColorHistory; - $scope.mdColorDefaultTab = options.mdColorDefaultTab; - - }], - - locals: { - options: options, - }, - preserveScope: options.preserveScope, - multiple: options.multiple, - - targetEvent: options.$event, - focusOnOpen: options.focusOnOpen, - autoWrap: false, - onShowing: function() { - // console.log( "DIALOG OPEN START", Date.now() - dateClick ); - }, - onComplete: function() { - // console.log( "DIALOG OPEN COMPLETE", Date.now() - dateClick ); - } - }); - - dialog.then(function (value) { - mdColorPickerHistory.add(new tinycolor(value)); - }, function () { }); - - return dialog; - }, - hide: function() { - return dialog.hide(); - }, - cancel: function() { - return dialog.cancel(); - } - }; - - /* eslint-enable angular/definedundefined */ -} - -function DatePickerCtrl($scope, $mdDialog, $mdMedia, $timeout, currentDate, options) { - var self = this; - - this.date = moment(currentDate); - this.minDate = options.minDate && moment(options.minDate).isValid() ? moment(options.minDate) : null; - this.maxDate = options.maxDate && moment(options.maxDate).isValid() ? moment(options.maxDate) : null; - this.displayFormat = options.displayFormat || "ddd, MMM DD"; - this.dateFilter = angular.isFunction(options.dateFilter) ? options.dateFilter : null; - this.selectingYear = false; - - // validate min and max date - if (this.minDate && this.maxDate) { - if (this.maxDate.isBefore(this.minDate)) { - this.maxDate = moment(this.minDate).add(1, 'days'); - } - } - - if (this.date) { - // check min date - if (this.minDate && this.date.isBefore(this.minDate)) { - this.date = moment(this.minDate); - } - - // check max date - if (this.maxDate && this.date.isAfter(this.maxDate)) { - this.date = moment(this.maxDate); - } - } - - this.yearItems = { - currentIndex_: 0, - PAGE_SIZE: 5, - START: (self.minDate ? self.minDate.year() : 1900), - END: (self.maxDate ? self.maxDate.year() : 0), - getItemAtIndex: function(index) { - if(this.currentIndex_ < index) - this.currentIndex_ = index; - - return this.START + index; - }, - getLength: function() { - return Math.min( - this.currentIndex_ + Math.floor(this.PAGE_SIZE / 2), - Math.abs(this.START - this.END) + 1 - ); - } - }; - - $scope.$mdMedia = $mdMedia; - $scope.year = this.date.year(); - - this.selectYear = function(year) { - self.date.year(year); - $scope.year = year; - self.selectingYear = false; - self.animate(); - }; - - this.showYear = function() { - self.yearTopIndex = (self.date.year() - self.yearItems.START) + Math.floor(self.yearItems.PAGE_SIZE / 2); - self.yearItems.currentIndex_ = (self.date.year() - self.yearItems.START) + 1; - self.selectingYear = true; - }; - - this.showCalendar = function() { - self.selectingYear = false; - }; - - this.cancel = function() { - $mdDialog.cancel(); - }; - - this.confirm = function() { - var date = this.date; - - if (this.minDate && this.date.isBefore(this.minDate)) { - date = moment(this.minDate); - } - - if (this.maxDate && this.date.isAfter(this.maxDate)) { - date = moment(this.maxDate); - } - - $mdDialog.hide(date.toDate()); - }; - - this.animate = function() { - self.animating = true; - $timeout(angular.noop).then(function() { - self.animating = false; - }) - }; -} - -/*@ngInject*/ -function mdpDatePicker() { - var LABEL_OK = "OK", - LABEL_CANCEL = "Cancel", - DISPLAY_FORMAT = "ddd, MMM DD"; - - this.setDisplayFormat = function(format) { - DISPLAY_FORMAT = format; - }; - - this.setOKButtonLabel = function(label) { - LABEL_OK = label; - }; - - this.setCancelButtonLabel = function(label) { - LABEL_CANCEL = label; - }; - - /*@ngInject*/ - this.$get = function($mdDialog) { - var datePicker = function(currentDate, options) { - if (!angular.isDate(currentDate)) currentDate = Date.now(); - if (!angular.isObject(options)) options = {}; - - options.displayFormat = DISPLAY_FORMAT; - - return $mdDialog.show({ - controller: ['$scope', '$mdDialog', '$mdMedia', '$timeout', 'currentDate', 'options', DatePickerCtrl], - controllerAs: 'datepicker', - clickOutsideToClose: true, - template: '' + - '' + - '
    ' + - '' + - '{{ datepicker.date.format(\'YYYY\') }}' + - '{{ datepicker.date.format(datepicker.displayFormat) }} ' + - '' + - '
    ' + - '
    ' + - '
    ' + - '' + - '
    ' + - '{{ item }}' + - '
    ' + - '
    ' + - '
    ' + - '' + - '' + - '' + - '' + LABEL_CANCEL + '' + - '' + LABEL_OK + '' + - '' + - '
    ' + - '
    ' + - '
    ', - targetEvent: options.targetEvent, - locals: { - currentDate: currentDate, - options: options - }, - multiple: true - }); - }; - - return datePicker; - }; - -} - -function TimePickerCtrl($scope, $mdDialog, time, autoSwitch, $mdMedia) { - var self = this; - this.VIEW_HOURS = 1; - this.VIEW_MINUTES = 2; - this.currentView = this.VIEW_HOURS; - this.time = moment(time); - this.autoSwitch = !!autoSwitch; - - this.clockHours = parseInt(this.time.format("h")); - this.clockMinutes = parseInt(this.time.minutes()); - - $scope.$mdMedia = $mdMedia; - - this.switchView = function() { - self.currentView = self.currentView == self.VIEW_HOURS ? self.VIEW_MINUTES : self.VIEW_HOURS; - }; - - this.setAM = function() { - if(self.time.hours() >= 12) - self.time.hour(self.time.hour() - 12); - }; - - this.setPM = function() { - if(self.time.hours() < 12) - self.time.hour(self.time.hour() + 12); - }; - - this.cancel = function() { - $mdDialog.cancel(); - }; - - this.confirm = function() { - $mdDialog.hide(this.time.toDate()); - }; -} - -/*@ngInject*/ -function mdpTimePicker() { - var LABEL_OK = "OK", - LABEL_CANCEL = "Cancel"; - - this.setOKButtonLabel = function(label) { - LABEL_OK = label; - }; - - this.setCancelButtonLabel = function(label) { - LABEL_CANCEL = label; - }; - - /*@ngInject*/ - this.$get = function($mdDialog) { - var timePicker = function(time, options) { - if(!angular.isDate(time)) time = Date.now(); - if (!angular.isObject(options)) options = {}; - - return $mdDialog.show({ - controller: ['$scope', '$mdDialog', 'time', 'autoSwitch', '$mdMedia', TimePickerCtrl], - controllerAs: 'timepicker', - clickOutsideToClose: true, - template: '' + - '' + - '' + - '
    ' + - '{{ timepicker.time.format("h") }}:' + - '{{ timepicker.time.format("mm") }}' + - '
    ' + - '
    ' + - 'AM' + - 'PM' + - '
    ' + - '
    ' + - '
    ' + - '
    ' + - '' + - '' + - '
    ' + - - '' + - '' + - '' + LABEL_CANCEL + '' + - '' + LABEL_OK + '' + - '' + - '
    ' + - '
    ' + - '
    ', - targetEvent: options.targetEvent, - locals: { - time: time, - autoSwitch: options.autoSwitch - }, - multiple: true - }); - }; - - return timePicker; - }; -} diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js deleted file mode 100644 index da2913bdbd..0000000000 --- a/ui/src/app/common/types.constant.js +++ /dev/null @@ -1,997 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default angular.module('thingsboard.types', []) - .constant('types', - { - serverErrorCode: { - general: 2, - authentication: 10, - jwtTokenExpired: 11, - credentialsExpired: 15, - permissionDenied: 20, - invalidArguments: 30, - badRequestParams: 31, - itemNotFound: 32, - tooManyRequests: 33, - tooManyUpdates: 34 - }, - entryPoints: { - login: "/api/auth/login", - tokenRefresh: "/api/auth/token", - nonTokenBased: "/api/noauth" - }, - id: { - nullUid: "13814000-1dd2-11b2-8080-808080808080", - }, - aggregation: { - min: { - value: "MIN", - name: "aggregation.min" - }, - max: { - value: "MAX", - name: "aggregation.max" - }, - avg: { - value: "AVG", - name: "aggregation.avg" - }, - sum: { - value: "SUM", - name: "aggregation.sum" - }, - count: { - value: "COUNT", - name: "aggregation.count" - }, - none: { - value: "NONE", - name: "aggregation.none" - } - }, - alarmFields: { - createdTime: { - keyName: 'createdTime', - value: "createdTime", - name: "alarm.created-time", - time: true - }, - startTime: { - keyName: 'startTime', - value: "startTs", - name: "alarm.start-time", - time: true - }, - endTime: { - keyName: 'endTime', - value: "endTs", - name: "alarm.end-time", - time: true - }, - ackTime: { - keyName: 'ackTime', - value: "ackTs", - name: "alarm.ack-time", - time: true - }, - clearTime: { - keyName: 'clearTime', - value: "clearTs", - name: "alarm.clear-time", - time: true - }, - originator: { - keyName: 'originator', - value: "originatorName", - name: "alarm.originator" - }, - originatorType: { - keyName: 'originatorType', - value: "originator.entityType", - name: "alarm.originator-type" - }, - type: { - keyName: 'type', - value: "type", - name: "alarm.type" - }, - severity: { - keyName: 'severity', - value: "severity", - name: "alarm.severity" - }, - status: { - keyName: 'status', - value: "status", - name: "alarm.status" - } - }, - alarmStatus: { - activeUnack: "ACTIVE_UNACK", - activeAck: "ACTIVE_ACK", - clearedUnack: "CLEARED_UNACK", - clearedAck: "CLEARED_ACK" - }, - alarmSearchStatus: { - any: "ANY", - active: "ACTIVE", - cleared: "CLEARED", - ack: "ACK", - unack: "UNACK" - }, - alarmSeverity: { - "CRITICAL": { - name: "alarm.severity-critical", - class: "tb-critical", - color: "red" - }, - "MAJOR": { - name: "alarm.severity-major", - class: "tb-major", - color: "orange" - }, - "MINOR": { - name: "alarm.severity-minor", - class: "tb-minor", - color: "#ffca3d" - }, - "WARNING": { - name: "alarm.severity-warning", - class: "tb-warning", - color: "#abab00" - }, - "INDETERMINATE": { - name: "alarm.severity-indeterminate", - class: "tb-indeterminate", - color: "green" - } - }, - auditLogActionType: { - "ADDED": { - name: "audit-log.type-added" - }, - "DELETED": { - name: "audit-log.type-deleted" - }, - "UPDATED": { - name: "audit-log.type-updated" - }, - "ATTRIBUTES_UPDATED": { - name: "audit-log.type-attributes-updated" - }, - "ATTRIBUTES_DELETED": { - name: "audit-log.type-attributes-deleted" - }, - "RPC_CALL": { - name: "audit-log.type-rpc-call" - }, - "CREDENTIALS_UPDATED": { - name: "audit-log.type-credentials-updated" - }, - "ASSIGNED_TO_CUSTOMER": { - name: "audit-log.type-assigned-to-customer" - }, - "UNASSIGNED_FROM_CUSTOMER": { - name: "audit-log.type-unassigned-from-customer" - }, - "ACTIVATED": { - name: "audit-log.type-activated" - }, - "SUSPENDED": { - name: "audit-log.type-suspended" - }, - "CREDENTIALS_READ": { - name: "audit-log.type-credentials-read" - }, - "ATTRIBUTES_READ": { - name: "audit-log.type-attributes-read" - }, - "RELATION_ADD_OR_UPDATE": { - name: "audit-log.type-relation-add-or-update" - }, - "RELATION_DELETED": { - name: "audit-log.type-relation-delete" - }, - "RELATIONS_DELETED": { - name: "audit-log.type-relations-delete" - }, - "ALARM_ACK": { - name: "audit-log.type-alarm-ack" - }, - "ALARM_CLEAR": { - name: "audit-log.type-alarm-clear" - }, - "LOGIN": { - name: "audit-log.type-login" - }, - "LOGOUT": { - name: "audit-log.type-logout" - }, - "LOCKOUT": { - name: "audit-log.type-lockout" - }, - "ASSIGNED_FROM_TENANT": { - name: "audit-log.type-assigned-from-tenant" - }, - "ASSIGNED_TO_TENANT": { - name: "audit-log.type-assigned-to-tenant" - } - }, - auditLogActionStatus: { - "SUCCESS": { - value: "SUCCESS", - name: "audit-log.status-success" - }, - "FAILURE": { - value: "FAILURE", - name: "audit-log.status-failure" - } - }, - auditLogMode: { - tenant: "tenant", - entity: "entity", - user: "user", - customer: "customer" - }, - aliasFilterType: { - singleEntity: { - value: 'singleEntity', - name: 'alias.filter-type-single-entity' - }, - entityList: { - value: 'entityList', - name: 'alias.filter-type-entity-list' - }, - entityName: { - value: 'entityName', - name: 'alias.filter-type-entity-name' - }, - stateEntity: { - value: 'stateEntity', - name: 'alias.filter-type-state-entity' - }, - assetType: { - value: 'assetType', - name: 'alias.filter-type-asset-type' - }, - deviceType: { - value: 'deviceType', - name: 'alias.filter-type-device-type' - }, - entityViewType: { - value: 'entityViewType', - name: 'alias.filter-type-entity-view-type' - }, - relationsQuery: { - value: 'relationsQuery', - name: 'alias.filter-type-relations-query' - }, - assetSearchQuery: { - value: 'assetSearchQuery', - name: 'alias.filter-type-asset-search-query' - }, - deviceSearchQuery: { - value: 'deviceSearchQuery', - name: 'alias.filter-type-device-search-query' - }, - entityViewSearchQuery: { - value: 'entityViewSearchQuery', - name: 'alias.filter-type-entity-view-search-query' - } - }, - direction: { - column: { - value: "column", - name: "direction.column" - }, - row: { - value: "row", - name: "direction.row" - } - }, - position: { - top: { - value: "top", - name: "position.top" - }, - bottom: { - value: "bottom", - name: "position.bottom" - }, - left: { - value: "left", - name: "position.left" - }, - right: { - value: "right", - name: "position.right" - } - }, - datasourceType: { - function: "function", - entity: "entity" - }, - dataKeyType: { - timeseries: "timeseries", - attribute: "attribute", - function: "function", - alarm: "alarm", - entityField: "entityField" - }, - contentType: { - "JSON": { - value: "JSON", - name: "content-type.json", - code: "json" - }, - "TEXT": { - value: "TEXT", - name: "content-type.text", - code: "text" - }, - "BINARY": { - value: "BINARY", - name: "content-type.binary", - code: "text" - } - }, - componentType: { - enrichment: "ENRICHMENT", - filter: "FILTER", - transformation: "TRANSFORMATION", - action: "ACTION", - external: "EXTERNAL" - }, - entityType: { - device: "DEVICE", - asset: "ASSET", - tenant: "TENANT", - customer: "CUSTOMER", - user: "USER", - dashboard: "DASHBOARD", - alarm: "ALARM", - rulechain: "RULE_CHAIN", - rulenode: "RULE_NODE", - entityView: "ENTITY_VIEW" - }, - importEntityColumnType: { - name: { - name: 'import.column-type.name', - value: 'name' - }, - type: { - name: 'import.column-type.type', - value: 'type' - }, - label: { - name: 'import.column-type.label', - value: 'label' - }, - clientAttribute: { - name: 'import.column-type.client-attribute', - value: 'CLIENT_ATTRIBUTE' - }, - sharedAttribute: { - name: 'import.column-type.shared-attribute', - value: 'SHARED_ATTRIBUTE' - }, - serverAttribute: { - name: 'import.column-type.server-attribute', - value: 'SERVER_ATTRIBUTE' - }, - timeseries: { - name: 'import.column-type.timeseries', - value: 'TIMESERIES' - }, - entityField: { - name: 'import.column-type.entity-field', - value: 'ENTITY_FIELD' - }, - accessToken: { - name: 'import.column-type.access-token', - value: 'ACCESS_TOKEN' - }, - isGateway: { - name: 'import.column-type.isgateway', - value: 'gateway' - }, - description: { - name: 'import.column-type.description', - value: 'description' - } - }, - aliasEntityType: { - current_customer: "CURRENT_CUSTOMER", - current_tenant: "CURRENT_TENANT" - }, - entityTypeTranslations: { - "DEVICE": { - type: 'entity.type-device', - typePlural: 'entity.type-devices', - list: 'entity.list-of-devices', - nameStartsWith: 'entity.device-name-starts-with' - }, - "ASSET": { - type: 'entity.type-asset', - typePlural: 'entity.type-assets', - list: 'entity.list-of-assets', - nameStartsWith: 'entity.asset-name-starts-with' - }, - "ENTITY_VIEW": { - type: 'entity.type-entity-view', - typePlural: 'entity.type-entity-views', - list: 'entity.list-of-entity-views', - nameStartsWith: 'entity.entity-view-name-starts-with' - }, - "TENANT": { - type: 'entity.type-tenant', - typePlural: 'entity.type-tenants', - list: 'entity.list-of-tenants', - nameStartsWith: 'entity.tenant-name-starts-with' - }, - "CUSTOMER": { - type: 'entity.type-customer', - typePlural: 'entity.type-customers', - list: 'entity.list-of-customers', - nameStartsWith: 'entity.customer-name-starts-with' - }, - "USER": { - type: 'entity.type-user', - typePlural: 'entity.type-users', - list: 'entity.list-of-users', - nameStartsWith: 'entity.user-name-starts-with' - }, - "DASHBOARD": { - type: 'entity.type-dashboard', - typePlural: 'entity.type-dashboards', - list: 'entity.list-of-dashboards', - nameStartsWith: 'entity.dashboard-name-starts-with' - }, - "ALARM": { - type: 'entity.type-alarm', - typePlural: 'entity.type-alarms', - list: 'entity.list-of-alarms', - nameStartsWith: 'entity.alarm-name-starts-with' - }, - "RULE_CHAIN": { - type: 'entity.type-rulechain', - typePlural: 'entity.type-rulechains', - list: 'entity.list-of-rulechains', - nameStartsWith: 'entity.rulechain-name-starts-with' - }, - "RULE_NODE": { - type: 'entity.type-rulenode', - typePlural: 'entity.type-rulenodes', - list: 'entity.list-of-rulenodes', - nameStartsWith: 'entity.rulenode-name-starts-with' - }, - "CURRENT_CUSTOMER": { - type: 'entity.type-current-customer', - list: 'entity.type-current-customer' - }, - "CURRENT_TENANT": { - type: 'entity.type-current-tenant', - list: 'entity.type-current-tenant' - } - }, - entityField: { - createdTime: { - keyName: 'createdTime', - name: 'entity-field.created-time', - value: 'createdTime', - time: true - }, - name: { - keyName: 'name', - name: 'entity-field.name', - value: 'name' - }, - type: { - keyName: 'type', - name: 'entity-field.type', - value: 'type' - }, - firstName: { - keyName: 'firstName', - name: 'entity-field.first-name', - value: 'firstName' - }, - lastName: { - keyName: 'lastName', - name: 'entity-field.last-name', - value: 'lastName' - }, - email: { - keyName: 'email', - name: 'entity-field.email', - value: 'email' - }, - title: { - keyName: 'title', - name: 'entity-field.title', - value: 'title' - }, - country: { - keyName: 'country', - name: 'entity-field.country', - value: 'country' - }, - state: { - keyName: 'state', - name: 'entity-field.state', - value: 'state' - }, - city: { - keyName: 'city', - name: 'entity-field.city', - value: 'city' - }, - address: { - keyName: 'address', - name: 'entity-field.address', - value: 'address' - }, - address2: { - keyName: 'address2', - name: 'entity-field.address2', - value: 'address2' - }, - zip: { - keyName: 'zip', - name: 'entity-field.zip', - value: 'zip' - }, - phone: { - keyName: 'phone', - name: 'entity-field.phone', - value: 'phone' - }, - label: { - keyName: 'label', - name: 'entity-field.label', - value: 'label' - } - }, - entitySearchDirection: { - from: "FROM", - to: "TO" - }, - entityRelationType: { - contains: "Contains", - manages: "Manages" - }, - eventType: { - error: { - value: "ERROR", - name: "event.type-error" - }, - lcEvent: { - value: "LC_EVENT", - name: "event.type-lc-event" - }, - stats: { - value: "STATS", - name: "event.type-stats" - } - }, - debugEventType: { - debugRuleNode: { - value: "DEBUG_RULE_NODE", - name: "event.type-debug-rule-node" - }, - debugRuleChain: { - value: "DEBUG_RULE_CHAIN", - name: "event.type-debug-rule-chain" - } - }, - extensionType: { - http: "HTTP", - mqtt: "MQTT", - opc: "OPC UA", - modbus: "MODBUS" - }, - gatewayConfigType: { - mqtt: { - value: "mqtt", - name: "MQTT" - }, - modbus: { - value: "modbus", - name: "Modbus" - }, - opcua: { - value: "opcua", - name: "OPC-UA" - }, - ble: { - value: "ble", - name: "BLE" - }, - request: { - value: "request", - name: "Request" - }, - can: { - value: "can", - name: "CAN" - }, - bacnet: { - value: "bacnet", - name: "BACnet" - }, - custom: { - value: "custom", - name: "Custom" - } - }, - gatewayLogLevel: { - none: "NONE", - critical: "CRITICAL", - error: "ERROR", - warning: "WARNING", - info: "INFO", - debug: "DEBUG" - }, - extensionValueType: { - string: 'value.string', - long: 'value.long', - double: 'value.double', - boolean: 'value.boolean' - }, - extensionTransformerType: { - toDouble: 'extension.to-double', - custom: 'extension.custom' - }, - mqttConverterTypes: { - json: 'extension.converter-json', - custom: 'extension.custom' - }, - mqttCredentialTypes: { - anonymous: { - value: "anonymous", - name: "extension.anonymous" - }, - basic: { - value: "basic", - name: "extension.basic" - }, - pem: { - value: "cert.PEM", - name: "extension.pem" - } - }, - extensionOpcSecurityTypes: { - Basic128Rsa15: "Basic128Rsa15", - Basic256: "Basic256", - Basic256Sha256: "Basic256Sha256", - None: "None" - }, - extensionIdentityType: { - anonymous: "extension.anonymous", - username: "extension.username" - }, - extensionKeystoreType: { - PKCS12: "PKCS12", - JKS: "JKS" - }, - extensionModbusFunctionCodes: { - 1: "Read Coils (1)", - 2: "Read Discrete Inputs (2)", - 3: "Read Multiple Holding Registers (3)", - 4: "Read Input Registers (4)" - }, - extensionModbusTransports: { - tcp: "TCP", - udp: "UDP", - rtu: "RTU" - }, - extensionModbusRtuParities: { - none: "none", - even: "even", - odd: "odd" - }, - extensionModbusRtuEncodings: { - ascii: "ascii", - rtu: "rtu" - }, - latestTelemetry: { - value: "LATEST_TELEMETRY", - name: "attribute.scope-latest-telemetry", - clientSide: true - }, - attributesScope: { - client: { - value: "CLIENT_SCOPE", - name: "attribute.scope-client", - clientSide: true - }, - server: { - value: "SERVER_SCOPE", - name: "attribute.scope-server", - clientSide: false - }, - shared: { - value: "SHARED_SCOPE", - name: "attribute.scope-shared", - clientSide: false - } - }, - ruleNodeTypeComponentTypes: ["FILTER", "ENRICHMENT", "TRANSFORMATION", "ACTION", "EXTERNAL"], - ruleChainNodeComponent: { - type: 'RULE_CHAIN', - name: 'rule chain', - clazz: 'tb.internal.RuleChain', - configurationDescriptor: { - nodeDefinition: { - description: "", - details: "Forwards incoming messages to specified Rule Chain", - inEnabled: true, - outEnabled: false, - relationTypes: [], - customRelations: false, - defaultConfiguration: {} - } - } - }, - unknownNodeComponent: { - type: 'UNKNOWN', - name: 'unknown', - clazz: 'tb.internal.Unknown', - configurationDescriptor: { - nodeDefinition: { - description: "", - details: "", - inEnabled: true, - outEnabled: true, - relationTypes: [], - customRelations: false, - defaultConfiguration: {} - } - } - }, - inputNodeComponent: { - type: 'INPUT', - name: 'Input', - clazz: 'tb.internal.Input' - }, - ruleNodeType: { - FILTER: { - value: "FILTER", - name: "rulenode.type-filter", - details: "rulenode.type-filter-details", - nodeClass: "tb-filter-type", - icon: "filter_list" - }, - ENRICHMENT: { - value: "ENRICHMENT", - name: "rulenode.type-enrichment", - details: "rulenode.type-enrichment-details", - nodeClass: "tb-enrichment-type", - icon: "playlist_add" - }, - TRANSFORMATION: { - value: "TRANSFORMATION", - name: "rulenode.type-transformation", - details: "rulenode.type-transformation-details", - nodeClass: "tb-transformation-type", - icon: "transform" - }, - ACTION: { - value: "ACTION", - name: "rulenode.type-action", - details: "rulenode.type-action-details", - nodeClass: "tb-action-type", - icon: "flash_on" - }, - EXTERNAL: { - value: "EXTERNAL", - name: "rulenode.type-external", - details: "rulenode.type-external-details", - nodeClass: "tb-external-type", - icon: "cloud_upload" - }, - RULE_CHAIN: { - value: "RULE_CHAIN", - name: "rulenode.type-rule-chain", - details: "rulenode.type-rule-chain-details", - nodeClass: "tb-rule-chain-type", - icon: "settings_ethernet" - }, - INPUT: { - value: "INPUT", - name: "rulenode.type-input", - details: "rulenode.type-input-details", - nodeClass: "tb-input-type", - icon: "input", - special: true - }, - UNKNOWN: { - value: "UNKNOWN", - name: "rulenode.type-unknown", - details: "rulenode.type-unknown-details", - nodeClass: "tb-unknown-type", - icon: "help_outline" - } - }, - messageType: { - 'POST_ATTRIBUTES_REQUEST': { - name: 'Post attributes', - value: 'POST_ATTRIBUTES_REQUEST' - }, - 'POST_TELEMETRY_REQUEST': { - name: 'Post telemetry', - value: 'POST_TELEMETRY_REQUEST' - }, - 'TO_SERVER_RPC_REQUEST': { - name: 'RPC Request from Device', - value: 'TO_SERVER_RPC_REQUEST' - }, - 'RPC_CALL_FROM_SERVER_TO_DEVICE': { - name: 'RPC Request to Device', - value: 'RPC_CALL_FROM_SERVER_TO_DEVICE' - }, - 'ACTIVITY_EVENT': { - name: 'Activity Event', - value: 'ACTIVITY_EVENT' - }, - 'INACTIVITY_EVENT': { - name: 'Inactivity Event', - value: 'INACTIVITY_EVENT' - }, - 'CONNECT_EVENT': { - name: 'Connect Event', - value: 'CONNECT_EVENT' - }, - 'DISCONNECT_EVENT': { - name: 'Disconnect Event', - value: 'DISCONNECT_EVENT' - }, - 'ENTITY_CREATED': { - name: 'Entity Created', - value: 'ENTITY_CREATED' - }, - 'ENTITY_UPDATED': { - name: 'Entity Updated', - value: 'ENTITY_UPDATED' - }, - 'ENTITY_DELETED': { - name: 'Entity Deleted', - value: 'ENTITY_DELETED' - }, - 'ENTITY_ASSIGNED': { - name: 'Entity Assigned', - value: 'ENTITY_ASSIGNED' - }, - 'ENTITY_UNASSIGNED': { - name: 'Entity Unassigned', - value: 'ENTITY_UNASSIGNED' - }, - 'ATTRIBUTES_UPDATED': { - name: 'Attributes Updated', - value: 'ATTRIBUTES_UPDATED' - }, - 'ATTRIBUTES_DELETED': { - name: 'Attributes Deleted', - value: 'ATTRIBUTES_DELETED' - } - }, - valueType: { - string: { - value: "string", - name: "value.string", - icon: "mdi:format-text" - }, - integer: { - value: "integer", - name: "value.integer", - icon: "mdi:numeric" - }, - double: { - value: "double", - name: "value.double", - icon: "mdi:numeric" - }, - boolean: { - value: "boolean", - name: "value.boolean", - icon: "mdi:checkbox-marked-outline" - }, - json: { - value: "json", - name: "value.json", - icon: "mdi:json" - } - }, - widgetType: { - timeseries: { - value: "timeseries", - name: "widget.timeseries", - template: { - bundleAlias: "charts", - alias: "basic_timeseries" - } - }, - latest: { - value: "latest", - name: "widget.latest-values", - template: { - bundleAlias: "cards", - alias: "attributes_card" - } - }, - rpc: { - value: "rpc", - name: "widget.rpc", - template: { - bundleAlias: "gpio_widgets", - alias: "basic_gpio_control" - } - }, - alarm: { - value: "alarm", - name: "widget.alarm", - template: { - bundleAlias: "alarm_widgets", - alias: "alarms_table" - } - }, - static: { - value: "static", - name: "widget.static", - template: { - bundleAlias: "cards", - alias: "html_card" - } - } - }, - widgetActionSources: { - headerButton: { - name: 'widget-action.header-button', - value: 'headerButton', - multiple: true - } - }, - widgetActionTypes: { - openDashboardState: { - name: 'widget-action.open-dashboard-state', - value: 'openDashboardState' - }, - updateDashboardState: { - name: 'widget-action.update-dashboard-state', - value: 'updateDashboardState' - }, - openDashboard: { - name: 'widget-action.open-dashboard', - value: 'openDashboard' - }, - custom: { - name: 'widget-action.custom', - value: 'custom' - }, - customPretty: { - name: 'widget-action.custom-pretty', - value: 'customPretty' - } - }, - systemBundleAlias: { - charts: "charts", - cards: "cards" - }, - translate: { - customTranslationsPrefix: "custom." - } - } - ).name; diff --git a/ui/src/app/common/utf8-support.js b/ui/src/app/common/utf8-support.js deleted file mode 100644 index ce749a7732..0000000000 --- a/ui/src/app/common/utf8-support.js +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export function utf8Encode(str) { - var result; - - if (angular.isUndefined(Uint8Array)) { // eslint-disable-line no-undef - result = utf8ToBytes(str); - } else { - result = new Uint8Array(utf8ToBytes(str)); // eslint-disable-line no-undef - } - - return result; -} - -export function utf8Decode(bytes) { - return utf8Slice(bytes, 0, bytes.length); -} - -function utf8Slice (buf, start, end) { - var res = '' - var tmp = '' - end = Math.min(buf.length, end || Infinity) - start = start || 0; - - for (var i = start; i < end; i++) { - if (buf[i] <= 0x7F) { - res += decodeUtf8Char(tmp) + String.fromCharCode(buf[i]) - tmp = '' - } else { - tmp += '%' + buf[i].toString(16) - } - } - - return res + decodeUtf8Char(tmp) -} - -function decodeUtf8Char (str) { - try { - return decodeURIComponent(str) - } catch (err) { - return String.fromCharCode(0xFFFD) // UTF 8 invalid char - } -} - -function utf8ToBytes (string, units) { - units = units || Infinity - var codePoint - var length = string.length - var leadSurrogate = null - var bytes = [] - var i = 0 - - for (; i < length; i++) { - codePoint = string.charCodeAt(i) - - // is surrogate component - if (codePoint > 0xD7FF && codePoint < 0xE000) { - // last char was a lead - if (leadSurrogate) { - // 2 leads in a row - if (codePoint < 0xDC00) { - if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) - leadSurrogate = codePoint - continue - } else { - // valid surrogate pair - codePoint = leadSurrogate - 0xD800 << 10 | codePoint - 0xDC00 | 0x10000 - leadSurrogate = null - } - } else { - // no lead yet - - if (codePoint > 0xDBFF) { - // unexpected trail - if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) - continue - } else if (i + 1 === length) { - // unpaired lead - if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) - continue - } else { - // valid lead - leadSurrogate = codePoint - continue - } - } - } else if (leadSurrogate) { - // valid bmp char, but last char was a lead - if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) - leadSurrogate = null - } - - // encode utf8 - if (codePoint < 0x80) { - if ((units -= 1) < 0) break - bytes.push(codePoint) - } else if (codePoint < 0x800) { - if ((units -= 2) < 0) break - bytes.push( - codePoint >> 0x6 | 0xC0, - codePoint & 0x3F | 0x80 - ) - } else if (codePoint < 0x10000) { - if ((units -= 3) < 0) break - bytes.push( - codePoint >> 0xC | 0xE0, - codePoint >> 0x6 & 0x3F | 0x80, - codePoint & 0x3F | 0x80 - ) - } else if (codePoint < 0x200000) { - if ((units -= 4) < 0) break - bytes.push( - codePoint >> 0x12 | 0xF0, - codePoint >> 0xC & 0x3F | 0x80, - codePoint >> 0x6 & 0x3F | 0x80, - codePoint & 0x3F | 0x80 - ) - } else { - throw new Error('Invalid code point') - } - } - - return bytes -} \ No newline at end of file diff --git a/ui/src/app/common/utils.service.js b/ui/src/app/common/utils.service.js deleted file mode 100644 index d5e4852afd..0000000000 --- a/ui/src/app/common/utils.service.js +++ /dev/null @@ -1,619 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import materialIconsCodepoints from 'raw-loader!material-design-icons/iconfont/codepoints'; - -/* eslint-enable import/no-unresolved, import/default */ - -import tinycolor from 'tinycolor2'; -import jsonSchemaDefaults from 'json-schema-defaults'; -import base64js from 'base64-js'; -import {utf8Encode, utf8Decode} from './utf8-support'; - -import thingsboardTypes from './types.constant'; - -export default angular.module('thingsboard.utils', [thingsboardTypes]) - .factory('utils', Utils) - .name; - -const varsRegex = /\$\{([^}]*)\}/g; - -/*@ngInject*/ -function Utils($mdColorPalette, $rootScope, $window, $translate, $q, $timeout, types) { - - var predefinedFunctions = {}, - predefinedFunctionsList = [], - materialColors = [], - materialIcons = []; - - var commonMaterialIcons = [ 'more_horiz', 'more_vert', 'open_in_new', 'visibility', 'play_arrow', 'arrow_back', 'arrow_downward', - 'arrow_forward', 'arrow_upwards', 'close', 'refresh', 'menu', 'show_chart', 'multiline_chart', 'pie_chart', 'insert_chart', 'people', - 'person', 'domain', 'devices_other', 'now_widgets', 'dashboards', 'map', 'pin_drop', 'my_location', 'extension', 'search', - 'settings', 'notifications', 'notifications_active', 'info', 'info_outline', 'warning', 'list', 'file_download', 'import_export', - 'share', 'add', 'edit', 'done' ]; - - predefinedFunctions['Sin'] = "return Math.round(1000*Math.sin(time/5000));"; - predefinedFunctions['Cos'] = "return Math.round(1000*Math.cos(time/5000));"; - predefinedFunctions['Random'] = - "var value = prevValue + Math.random() * 100 - 50;\n" + - "var multiplier = Math.pow(10, 2 || 0);\n" + - "var value = Math.round(value * multiplier) / multiplier;\n" + - "if (value < -1000) {\n" + - " value = -1000;\n" + - "} else if (value > 1000) {\n" + - " value = 1000;\n" + - "}\n" + - "return value;"; - - for (var func in predefinedFunctions) { - predefinedFunctionsList.push(func); - } - - var colorPalettes = ['blue', 'green', 'red', 'amber', 'blue-grey', 'purple', 'light-green', 'indigo', 'pink', 'yellow', 'light-blue', 'orange', 'deep-purple', 'lime', 'teal', 'brown', 'cyan', 'deep-orange', 'grey']; - var colorSpectrum = ['500', 'A700', '600', '700', '800', '900', '300', '400', 'A200', 'A400']; - - angular.forEach($mdColorPalette, function (value, key) { - angular.forEach(value, function (color, label) { - if (colorSpectrum.indexOf(label) > -1) { - var rgb = 'rgb(' + color.value[0] + ',' + color.value[1] + ',' + color.value[2] + ')'; - color = tinycolor(rgb); - var isDark = color.isDark(); - var colorItem = { - value: color.toHexString(), - group: key, - label: label, - isDark: isDark - }; - materialColors.push(colorItem); - } - }); - }); - - materialColors.sort(function (colorItem1, colorItem2) { - var spectrumIndex1 = colorSpectrum.indexOf(colorItem1.label); - var spectrumIndex2 = colorSpectrum.indexOf(colorItem2.label); - var result = spectrumIndex1 - spectrumIndex2; - if (result === 0) { - var paletteIndex1 = colorPalettes.indexOf(colorItem1.group); - var paletteIndex2 = colorPalettes.indexOf(colorItem2.group); - result = paletteIndex1 - paletteIndex2; - } - return result; - }); - - var defaultDataKey = { - name: 'f(x)', - type: types.dataKeyType.function, - label: 'Sin', - color: getMaterialColor(0), - funcBody: getPredefinedFunctionBody('Sin'), - settings: {}, - _hash: Math.random() - }; - - var defaultDatasource = { - type: types.datasourceType.function, - name: types.datasourceType.function, - dataKeys: [angular.copy(defaultDataKey)] - }; - - var defaultAlarmFields = [ - types.alarmFields.createdTime.keyName, - types.alarmFields.originator.keyName, - types.alarmFields.type.keyName, - types.alarmFields.severity.keyName, - types.alarmFields.status.keyName - ]; - - var defaultAlarmDataKeys = []; - - var imageAspectMap = {}; - - var service = { - getDefaultDatasource: getDefaultDatasource, - generateObjectFromJsonSchema: generateObjectFromJsonSchema, - getDefaultDatasourceJson: getDefaultDatasourceJson, - getDefaultAlarmDataKeys: getDefaultAlarmDataKeys, - getMaterialColor: getMaterialColor, - getMaterialIcons: getMaterialIcons, - getCommonMaterialIcons: getCommonMaterialIcons, - getPredefinedFunctionBody: getPredefinedFunctionBody, - getPredefinedFunctionsList: getPredefinedFunctionsList, - genMaterialColor: genMaterialColor, - objectHashCode: objectHashCode, - parseException: parseException, - processWidgetException: processWidgetException, - isDescriptorSchemaNotEmpty: isDescriptorSchemaNotEmpty, - filterSearchTextEntities: filterSearchTextEntities, - guid: guid, - cleanCopy: cleanCopy, - isLocalUrl: isLocalUrl, - validateDatasources: validateDatasources, - createKey: createKey, - createAdditionalDataKey: createAdditionalDataKey, - createLabelFromDatasource: createLabelFromDatasource, - insertVariable: insertVariable, - customTranslation: customTranslation, - objToBase64: objToBase64, - base64toObj: base64toObj, - loadImageAspect: loadImageAspect, - sortObjectKeys: sortObjectKeys - } - - return service; - - function getPredefinedFunctionsList() { - return predefinedFunctionsList; - } - - function getPredefinedFunctionBody(func) { - return predefinedFunctions[func]; - } - - function getMaterialColor(index) { - var colorIndex = index % materialColors.length; - return materialColors[colorIndex].value; - } - - function getMaterialIcons() { - var deferred = $q.defer(); - if (materialIcons.length) { - deferred.resolve(materialIcons); - } else { - $timeout(function() { - var codepointsArray = materialIconsCodepoints.split("\n"); - codepointsArray.forEach(function (codepoint) { - if (codepoint && codepoint.length) { - var values = codepoint.split(' '); - if (values && values.length == 2) { - materialIcons.push(values[0]); - } - } - }); - deferred.resolve(materialIcons); - }); - } - return deferred.promise; - } - - function getCommonMaterialIcons() { - return commonMaterialIcons; - } - - function genMaterialColor(str) { - var hash = Math.abs(hashCode(str)); - return getMaterialColor(hash); - } - - function hashCode(str) { - var hash = 0; - var i, char; - if (str.length == 0) return hash; - for (i = 0; i < str.length; i++) { - char = str.charCodeAt(i); - hash = ((hash << 5) - hash) + char; - hash = hash & hash; // Convert to 32bit integer - } - return hash; - } - - function objectHashCode(obj) { - var hash = 0; - if (obj) { - var str = angular.toJson(obj); - hash = hashCode(str); - } - return hash; - } - - function parseException(exception, lineOffset) { - var data = {}; - if (exception) { - if (angular.isString(exception) || exception instanceof String) { - data.message = exception; - } else { - if (exception.name) { - data.name = exception.name; - } else { - data.name = 'UnknownError'; - } - if (exception.message) { - data.message = exception.message; - } - if (exception.lineNumber) { - data.lineNumber = exception.lineNumber; - if (exception.columnNumber) { - data.columnNumber = exception.columnNumber; - } - } else if (exception.stack) { - var lineInfoRegexp = /(.*):(\d*)(:)?(\d*)?/g; - var lineInfoGroups = lineInfoRegexp.exec(exception.stack); - if (lineInfoGroups != null && lineInfoGroups.length >= 3) { - if (angular.isUndefined(lineOffset)) { - lineOffset = -2; - } - data.lineNumber = Number(lineInfoGroups[2]) + lineOffset; - if (lineInfoGroups.length >= 5) { - data.columnNumber = lineInfoGroups[4]; - } - } - } - } - } - return data; - } - - function processWidgetException(exception) { - var parentScope = $window.parent.angular.element($window.frameElement).scope(); - var data = parseException(exception, -5); - if ($rootScope.widgetEditMode) { - parentScope.$emit('widgetException', data); - parentScope.$apply(); - } - return data; - } - - function getDefaultDatasource(dataKeySchema) { - var datasource = angular.copy(defaultDatasource); - if (angular.isDefined(dataKeySchema)) { - datasource.dataKeys[0].settings = generateObjectFromJsonSchema(dataKeySchema); - } - return datasource; - } - - function generateObjectFromJsonSchema(schema) { - var obj = jsonSchemaDefaults(schema); - deleteNullProperties(obj); - return obj; - } - - function deleteNullProperties(obj) { - if (angular.isUndefined(obj) || obj == null) { - return; - } - for (var propName in obj) { - if (obj[propName] === null || angular.isUndefined(obj[propName])) { - delete obj[propName]; - } else if (angular.isObject(obj[propName])) { - deleteNullProperties(obj[propName]); - } else if (angular.isArray(obj[propName])) { - for (var i=0;i 0) { - var comparison = text.localeCompare(pageLink.textOffset); - if (comparison === 0 - && createdTime < pageLink.createdTimeOffset) { - response.data.push(entity); - if (response.data.length === limit) { - break; - } - } else if (comparison > 0 && text.startsWith(textSearch)) { - response.data.push(entity); - if (response.data.length === limit) { - break; - } - } - } else if (textSearch.length > 0) { - if (text.startsWith(textSearch)) { - response.data.push(entity); - if (response.data.length === limit) { - break; - } - } - } else { - response.data.push(entity); - if (response.data.length === limit) { - break; - } - } - } - if (response.data.length === limit) { - var lastEntity = response.data[limit-1]; - response.nextPageLink = { - limit: pageLink.limit, - textSearch: textSearch, - idOffset: lastEntity.id.id, - createdTimeOffset: lastEntity.createdTime, - textOffset: lastEntity[searchTextField].toLowerCase() - }; - response.hasNext = true; - } - deferred.resolve(response); - } - - function guid() { - function s4() { - return Math.floor((1 + Math.random()) * 0x10000) - .toString(16) - .substring(1); - } - return s4() + s4() + '-' + s4() + '-' + s4() + '-' + - s4() + '-' + s4() + s4() + s4(); - } - - function cleanCopy(object) { - var copy = angular.copy(object); - for (var prop in copy) { - if (prop && prop.startsWith('$$')) { - delete copy[prop]; - } - } - return copy; - } - - function genNextColor(datasources, initialIndex) { - var index = initialIndex || 0; - if (datasources) { - for (var i = 0; i < datasources.length; i++) { - var datasource = datasources[i]; - index += datasource.dataKeys.length; - } - } - return getMaterialColor(index); - } - - function isLocalUrl(url) { - var parser = document.createElement('a'); //eslint-disable-line - parser.href = url; - var host = parser.hostname; - if (host === "localhost" || host === "127.0.0.1") { - return true; - } else { - return false; - } - } - - function validateDatasources(datasources) { - datasources.forEach(function (datasource) { - if (datasource.type === 'device') { - datasource.type = types.datasourceType.entity; - datasource.entityType = types.entityType.device; - if (datasource.deviceId) { - datasource.entityId = datasource.deviceId; - } else if (datasource.deviceAliasId) { - datasource.entityAliasId = datasource.deviceAliasId; - } - if (datasource.deviceName) { - datasource.entityName = datasource.deviceName; - } - } - if (datasource.type === types.datasourceType.entity && datasource.entityId) { - datasource.name = datasource.entityName; - } - }); - return datasources; - } - - function createKey(keyInfo, type, datasources) { - var label; - if (type === types.dataKeyType.alarm && !keyInfo.label) { - var alarmField = types.alarmFields[keyInfo.name]; - if (alarmField) { - label = $translate.instant(alarmField.name)+''; - } - } - if (!label) { - label = keyInfo.label || keyInfo.name; - } - var dataKey = { - name: keyInfo.name, - type: type, - label: label, - funcBody: keyInfo.funcBody, - settings: {}, - _hash: Math.random() - } - if (keyInfo.units) { - dataKey.units = keyInfo.units; - } - if (angular.isDefined(keyInfo.decimals)) { - dataKey.decimals = keyInfo.decimals; - } - if (keyInfo.color) { - dataKey.color = keyInfo.color; - } else { - dataKey.color = genNextColor(datasources); - } - if (keyInfo.postFuncBody && keyInfo.postFuncBody.length) { - dataKey.usePostProcessing = true; - dataKey.postFuncBody = keyInfo.postFuncBody; - } - return dataKey; - } - - function createAdditionalDataKey(dataKey, datasource, timeUnit, datasources, additionalKeysNumber) { - let additionalDataKey = angular.copy(dataKey); - if (dataKey.settings.comparisonSettings.comparisonValuesLabel) { - additionalDataKey.label = createLabelFromDatasource(datasource, dataKey.settings.comparisonSettings.comparisonValuesLabel); - } else { - additionalDataKey.label = dataKey.label + ' ' + $translate.instant('legend.comparison-time-ago.'+timeUnit); - } - additionalDataKey.pattern = additionalDataKey.label; - if (dataKey.settings.comparisonSettings.color) { - additionalDataKey.color = dataKey.settings.comparisonSettings.color; - } else { - additionalDataKey.color = genNextColor(datasources, additionalKeysNumber); - } - additionalDataKey._hash = Math.random(); - return additionalDataKey; - } - - function createLabelFromDatasource(datasource, pattern) { - var label = angular.copy(pattern); - var match = varsRegex.exec(pattern); - while (match !== null) { - var variable = match[0]; - var variableName = match[1]; - if (variableName === 'dsName') { - label = label.split(variable).join(datasource.name); - } else if (variableName === 'entityName') { - label = label.split(variable).join(datasource.entityName); - } else if (variableName === 'deviceName') { - label = label.split(variable).join(datasource.entityName); - } else if (variableName === 'entityLabel') { - label = label.split(variable).join(datasource.entityLabel || datasource.entityName); - } else if (variableName === 'aliasName') { - label = label.split(variable).join(datasource.aliasName); - } else if (variableName === 'entityDescription') { - label = label.split(variable).join(datasource.entityDescription); - } - match = varsRegex.exec(pattern); - } - return label; - } - - function insertVariable(pattern, name, value) { - var result = angular.copy(pattern); - var match = varsRegex.exec(pattern); - while (match !== null) { - var variable = match[0]; - var variableName = match[1]; - if (variableName === name) { - result = result.split(variable).join(value); - } - match = varsRegex.exec(pattern); - } - return result; - } - - function customTranslation(translationValue, defaultValue) { - var result = ''; - var translationId = types.translate.customTranslationsPrefix + translationValue; - var translation = $translate.instant(translationId); - if (translation != translationId) { - result = translation + ''; - } else { - result = defaultValue; - } - return result; - } - - function objToBase64(obj) { - var json = angular.toJson(obj); - var encoded = utf8Encode(json); - var b64Encoded = base64js.fromByteArray(encoded); - return b64Encoded; - } - - function base64toObj(b64Encoded) { - var encoded = base64js.toByteArray(b64Encoded); - var json = utf8Decode(encoded); - var obj = angular.fromJson(json); - return obj; - } - - function loadImageAspect(imageUrl) { - var deferred = $q.defer(); - if (imageUrl && imageUrl.length) { - var urlHashCode = hashCode(imageUrl); - var aspect = imageAspectMap[urlHashCode]; - if (angular.isUndefined(aspect)) { - var testImage = document.createElement('img'); // eslint-disable-line - testImage.style.position = 'absolute'; - testImage.style.left = '-99999px'; - testImage.style.top = '-99999px'; - testImage.onload = function() { - aspect = testImage.width / testImage.height; - document.body.removeChild(testImage); //eslint-disable-line - imageAspectMap[urlHashCode] = aspect; - deferred.resolve(aspect); - }; - testImage.onerror = function() { - aspect = 0; - imageAspectMap[urlHashCode] = aspect; - deferred.resolve(aspect); - }; - document.body.appendChild(testImage); //eslint-disable-line - testImage.src = imageUrl; - } else { - deferred.resolve(aspect); - } - } else { - deferred.resolve(0); - } - return deferred.promise; - } - - function sortObjectKeys(obj) { - var sortedObj = {}; - var keys = Object.keys(obj).sort(); - for (var i = 0; i < keys.length; i++) { - var key = keys[i]; - sortedObj[key] = obj[key]; - } - return sortedObj; - } - -} diff --git a/ui/src/app/components/ace-editor-fix.js b/ui/src/app/components/ace-editor-fix.js deleted file mode 100644 index 907307c20f..0000000000 --- a/ui/src/app/components/ace-editor-fix.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default function fixAceEditor(aceEditor) { - aceEditor.$blockScrolling = Infinity; - aceEditor.on("showGutterTooltip", function (tooltip) { - if (!tooltip.isAttachedToBody) { - document.body.appendChild(tooltip.$element); //eslint-disable-line - tooltip.isAttachedToBody = true; - onElementRemoved(tooltip.$parentNode, () => { - if (tooltip.$element.parentNode != null) { - tooltip.$element.parentNode.removeChild(tooltip.$element); - } - }); - } - }); -} - -function onElementRemoved(element, callback) { - if (!document.body.contains(element)) { //eslint-disable-line - callback(); - } else { - var observer; - observer = new MutationObserver(function(mutations) { //eslint-disable-line - if (!document.body.contains(element)) { //eslint-disable-line - callback(); - observer.disconnect(); - } - }); - observer.observe(document.body, {childList: true}); //eslint-disable-line - } -} diff --git a/ui/src/app/components/circular-progress.directive.js b/ui/src/app/components/circular-progress.directive.js deleted file mode 100644 index d33b057ce5..0000000000 --- a/ui/src/app/components/circular-progress.directive.js +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import $ from 'jquery'; - -export default angular.module('thingsboard.directives.circularProgress', []) - .directive('tbCircularProgress', CircularProgress) - .name; - -/* eslint-disable angular/angularelement */ - -/*@ngInject*/ -function CircularProgress($compile) { - - var linker = function (scope, element) { - - var circularProgressElement = angular.element(''); - - $compile(circularProgressElement)(scope); - - var children = null; - var cssWidth = element.prop('style')['width']; - var width = null; - if (!cssWidth) { - $(element).css('width', width + 'px'); - } - - scope.$watch('circularProgress', function (newCircularProgress, prevCircularProgress) { - if (newCircularProgress != prevCircularProgress) { - if (newCircularProgress) { - if (!cssWidth) { - $(element).css('width', ''); - width = element.prop('offsetWidth'); - $(element).css('width', width + 'px'); - } - children = $(element).children(); - $(element).empty(); - $(element).append($(circularProgressElement)); - } else { - $(element).empty(); - $(element).append(children); - if (cssWidth) { - $(element).css('width', cssWidth); - } else { - $(element).css('width', ''); - } - } - } - }); - - } - - return { - restrict: "A", - link: linker, - scope: { - circularProgress: "=tbCircularProgress" - } - }; -} - -/* eslint-enable angular/angularelement */ diff --git a/ui/src/app/components/confirm-on-exit.directive.js b/ui/src/app/components/confirm-on-exit.directive.js deleted file mode 100644 index 37ffe272c0..0000000000 --- a/ui/src/app/components/confirm-on-exit.directive.js +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default angular.module('thingsboard.directives.confirmOnExit', []) - .directive('tbConfirmOnExit', ConfirmOnExit) - .name; - -/*@ngInject*/ -function ConfirmOnExit($state, $mdDialog, $window, $filter, $parse, userService) { - return { - link: function ($scope, $element, $attributes) { - $scope.confirmForm = $scope.$eval($attributes.confirmForm); - $window.onbeforeunload = function () { - if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.$eval($attributes.isDirty))) { - return $filter('translate')('confirm-on-exit.message'); - } - } - $scope.$on('$stateChangeStart', function (event, next, current, params) { - if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.$eval($attributes.isDirty))) { - event.preventDefault(); - var confirm = $mdDialog.confirm() - .title($filter('translate')('confirm-on-exit.title')) - .htmlContent($filter('translate')('confirm-on-exit.html-message')) - .ariaLabel($filter('translate')('confirm-on-exit.title')) - .cancel($filter('translate')('action.cancel')) - .ok($filter('translate')('action.ok')); - $mdDialog.show(confirm).then(function () { - if ($scope.confirmForm) { - $scope.confirmForm.$setPristine(); - } else { - var remoteSetter = $parse($attributes.isDirty).assign; - remoteSetter($scope, false); - //$scope.isDirty = false; - } - $state.go(next.name, params); - }, function () { - }); - } - }); - }, - scope: false - }; -} \ No newline at end of file diff --git a/ui/src/app/components/contact-short.filter.js b/ui/src/app/components/contact-short.filter.js deleted file mode 100644 index d577d36389..0000000000 --- a/ui/src/app/components/contact-short.filter.js +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default angular.module('thingsboard.filters.contactShort', []) - .filter('contactShort', ContactShort) - .name; - -/*@ngInject*/ -function ContactShort($filter) { - return function (contact) { - var contactShort = ''; - if (contact) { - if (contact.address) { - contactShort += contact.address; - contactShort += ' '; - } - if (contact.address2) { - contactShort += contact.address2; - contactShort += ' '; - } - if (contact.city) { - contactShort += contact.city; - contactShort += ' '; - } - if (contact.state) { - contactShort += contact.state; - contactShort += ' '; - } - if (contact.zip) { - contactShort += contact.zip; - contactShort += ' '; - } - if (contact.country) { - contactShort += contact.country; - } - } - if (contactShort === '') { - contactShort = $filter('translate')('contact.no-address'); - } - return contactShort; - }; -} diff --git a/ui/src/app/components/contact.directive.js b/ui/src/app/components/contact.directive.js deleted file mode 100644 index 3746aefdd9..0000000000 --- a/ui/src/app/components/contact.directive.js +++ /dev/null @@ -1,323 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import contactTemplate from './contact.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -export default angular.module('thingsboard.directives.contact', []) - .directive('tbContact', Contact) - .name; - -/*@ngInject*/ -function Contact($compile, $templateCache) { - var countries = [ - "Afghanistan", - "Åland Islands", - "Albania", - "Algeria", - "American Samoa", - "Andorra", - "Angola", - "Anguilla", - "Antarctica", - "Antigua and Barbuda", - "Argentina", - "Armenia", - "Aruba", - "Australia", - "Austria", - "Azerbaijan", - "Bahamas", - "Bahrain", - "Bangladesh", - "Barbados", - "Belarus", - "Belgium", - "Belize", - "Benin", - "Bermuda", - "Bhutan", - "Bolivia", - "Bonaire, Sint Eustatius and Saba", - "Bosnia and Herzegovina", - "Botswana", - "Bouvet Island", - "Brazil", - "British Indian Ocean Territory", - "Brunei Darussalam", - "Bulgaria", - "Burkina Faso", - "Burundi", - "Cambodia", - "Cameroon", - "Canada", - "Cape Verde", - "Cayman Islands", - "Central African Republic", - "Chad", - "Chile", - "China", - "Christmas Island", - "Cocos (Keeling) Islands", - "Colombia", - "Comoros", - "Congo", - "Congo, The Democratic Republic of the", - "Cook Islands", - "Costa Rica", - "Côte d'Ivoire", - "Croatia", - "Cuba", - "Curaçao", - "Cyprus", - "Czech Republic", - "Denmark", - "Djibouti", - "Dominica", - "Dominican Republic", - "Ecuador", - "Egypt", - "El Salvador", - "Equatorial Guinea", - "Eritrea", - "Estonia", - "Ethiopia", - "Falkland Islands (Malvinas)", - "Faroe Islands", - "Fiji", - "Finland", - "France", - "French Guiana", - "French Polynesia", - "French Southern Territories", - "Gabon", - "Gambia", - "Georgia", - "Germany", - "Ghana", - "Gibraltar", - "Greece", - "Greenland", - "Grenada", - "Guadeloupe", - "Guam", - "Guatemala", - "Guernsey", - "Guinea", - "Guinea-Bissau", - "Guyana", - "Haiti", - "Heard Island and McDonald Islands", - "Holy See (Vatican City State)", - "Honduras", - "Hong Kong", - "Hungary", - "Iceland", - "India", - "Indonesia", - "Iran, Islamic Republic of", - "Iraq", - "Ireland", - "Isle of Man", - "Israel", - "Italy", - "Jamaica", - "Japan", - "Jersey", - "Jordan", - "Kazakhstan", - "Kenya", - "Kiribati", - "Korea, Democratic People's Republic of", - "Korea, Republic of", - "Kuwait", - "Kyrgyzstan", - "Lao People's Democratic Republic", - "Latvia", - "Lebanon", - "Lesotho", - "Liberia", - "Libya", - "Liechtenstein", - "Lithuania", - "Luxembourg", - "Macao", - "Macedonia, Republic Of", - "Madagascar", - "Malawi", - "Malaysia", - "Maldives", - "Mali", - "Malta", - "Marshall Islands", - "Martinique", - "Mauritania", - "Mauritius", - "Mayotte", - "Mexico", - "Micronesia, Federated States of", - "Moldova, Republic of", - "Monaco", - "Mongolia", - "Montenegro", - "Montserrat", - "Morocco", - "Mozambique", - "Myanmar", - "Namibia", - "Nauru", - "Nepal", - "Netherlands", - "New Caledonia", - "New Zealand", - "Nicaragua", - "Niger", - "Nigeria", - "Niue", - "Norfolk Island", - "Northern Mariana Islands", - "Norway", - "Oman", - "Pakistan", - "Palau", - "Palestinian Territory, Occupied", - "Panama", - "Papua New Guinea", - "Paraguay", - "Peru", - "Philippines", - "Pitcairn", - "Poland", - "Portugal", - "Puerto Rico", - "Qatar", - "Reunion", - "Romania", - "Russian Federation", - "Rwanda", - "Saint Barthélemy", - "Saint Helena, Ascension and Tristan da Cunha", - "Saint Kitts and Nevis", - "Saint Lucia", - "Saint Martin (French Part)", - "Saint Pierre and Miquelon", - "Saint Vincent and the Grenadines", - "Samoa", - "San Marino", - "Sao Tome and Principe", - "Saudi Arabia", - "Senegal", - "Serbia", - "Seychelles", - "Sierra Leone", - "Singapore", - "Sint Maarten (Dutch Part)", - "Slovakia", - "Slovenia", - "Solomon Islands", - "Somalia", - "South Africa", - "South Georgia and the South Sandwich Islands", - "South Sudan", - "Spain", - "Sri Lanka", - "Sudan", - "Suriname", - "Svalbard and Jan Mayen", - "Swaziland", - "Sweden", - "Switzerland", - "Syrian Arab Republic", - "Taiwan", - "Tajikistan", - "Tanzania, United Republic of", - "Thailand", - "Timor-Leste", - "Togo", - "Tokelau", - "Tonga", - "Trinidad and Tobago", - "Tunisia", - "Turkey", - "Turkmenistan", - "Turks and Caicos Islands", - "Tuvalu", - "Uganda", - "Ukraine", - "United Arab Emirates", - "United Kingdom", - "United States", - "United States Minor Outlying Islands", - "Uruguay", - "Uzbekistan", - "Vanuatu", - "Venezuela", - "Viet Nam", - "Virgin Islands, British", - "Virgin Islands, U.S.", - "Wallis and Futuna", - "Western Sahara", - "Yemen", - "Zambia", - "Zimbabwe" - ]; - - var postalCodePatterns = { - "United States": "(\\d{5}([\\-]\\d{4})?)", - "Australia": "[0-9]{4}", - "Austria": "[0-9]{4}", - "Belgium": "[0-9]{4}", - "Brazil": "[0-9]{5}[\\-]?[0-9]{3}", - "Canada": "^(?!.*[DFIOQU])[A-VXY][0-9][A-Z][ -]?[0-9][A-Z][0-9]$", - "Denmark": "[0-9]{3,4}", - "Faroe Islands": "[0-9]{3,4}", - "Netherlands": "[1-9][0-9]{3}\\s?[a-zA-Z]{2}", - "Germany": "[0-9]{5}", - "Hungary": "[0-9]{4}", - "Italy": "[0-9]{5}", - "Japan": "\\d{3}-\\d{4}", - "Luxembourg": "(L\\s*(-|—|–))\\s*?[\\d]{4}", - "Poland": "[0-9]{2}\\-[0-9]{3}", - "Spain": "((0[1-9]|5[0-2])|[1-4][0-9])[0-9]{3}", - "Sweden": "\\d{3}\\s?\\d{2}", - "United Kingdom": "[A-Za-z]{1,2}[0-9Rr][0-9A-Za-z]? [0-9][ABD-HJLNP-UW-Zabd-hjlnp-uw-z]{2}" - }; - - var linker = function (scope, element) { - - scope.countries = countries; - - scope.postalCodePatterns = postalCodePatterns; - - var template = $templateCache.get(contactTemplate); - - element.html(template); - - $compile(element.contents())(scope); - } - - return { - restrict: "E", - link: linker, - scope: { - contact: '=', - isEdit: '=', - theForm: '=' - } - }; -} diff --git a/ui/src/app/components/contact.tpl.html b/ui/src/app/components/contact.tpl.html deleted file mode 100644 index 57f1fdc6ad..0000000000 --- a/ui/src/app/components/contact.tpl.html +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - {{country}} - - - -
    - - - - - - - - - - - -
    -
    contact.postal-code-invalid
    -
    -
    -
    - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/ui/src/app/components/dashboard-autocomplete.directive.js b/ui/src/app/components/dashboard-autocomplete.directive.js deleted file mode 100644 index d8280bf102..0000000000 --- a/ui/src/app/components/dashboard-autocomplete.directive.js +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './dashboard-autocomplete.scss'; - -import thingsboardApiDashboard from '../api/dashboard.service'; -import thingsboardApiUser from '../api/user.service'; - -/* eslint-disable import/no-unresolved, import/default */ - -import dashboardAutocompleteTemplate from './dashboard-autocomplete.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - - -export default angular.module('thingsboard.directives.dashboardAutocomplete', [thingsboardApiDashboard, thingsboardApiUser]) - .directive('tbDashboardAutocomplete', DashboardAutocomplete) - .name; - -/*@ngInject*/ -function DashboardAutocomplete($compile, $templateCache, $q, dashboardService, userService) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - var template = $templateCache.get(dashboardAutocompleteTemplate); - element.html(template); - - scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false; - scope.dashboard = null; - scope.dashboardSearchText = ''; - - scope.fetchDashboards = function(searchText) { - var pageLink = {limit: 50, textSearch: searchText}; - - var deferred = $q.defer(); - - var promise; - if (scope.dashboardsScope === 'customer' || userService.getAuthority() === 'CUSTOMER_USER') { - if (scope.customerId) { - promise = dashboardService.getCustomerDashboards(scope.customerId, pageLink, {ignoreLoading: true}); - } else { - promise = $q.when({data: []}); - } - } else { - if (userService.getAuthority() === 'SYS_ADMIN') { - if (scope.tenantId) { - promise = dashboardService.getTenantDashboardsByTenantId(scope.tenantId, pageLink, {ignoreLoading: true}); - } else { - promise = $q.when({data: []}); - } - } else { - promise = dashboardService.getTenantDashboards(pageLink, {ignoreLoading: true}); - } - } - - promise.then(function success(result) { - deferred.resolve(result.data); - }, function fail() { - deferred.reject(); - }); - - return deferred.promise; - } - - scope.dashboardSearchTextChanged = function() { - } - - scope.updateView = function () { - if (!scope.disabled) { - ngModelCtrl.$setViewValue(scope.dashboard ? scope.dashboard.id.id : null); - } - } - - ngModelCtrl.$render = function () { - if (ngModelCtrl.$viewValue) { - dashboardService.getDashboardInfo(ngModelCtrl.$viewValue).then( - function success(dashboard) { - scope.dashboard = dashboard; - startWatchers(); - }, - function fail() { - scope.dashboard = null; - scope.updateView(); - startWatchers(); - } - ); - } else { - scope.dashboard = null; - startWatchers(); - } - } - - function startWatchers() { - scope.$watch('dashboard', function (newVal, prevVal) { - if (!angular.equals(newVal, prevVal)) { - scope.updateView(); - } - }); - scope.$watch('disabled', function (newVal, prevVal) { - if (!angular.equals(newVal, prevVal)) { - scope.updateView(); - } - }); - } - - if (scope.selectFirstDashboard) { - var pageLink = {limit: 1, textSearch: ''}; - scope.dashboardFetchFunction(pageLink).then(function success(result) { - var dashboards = result.data; - if (dashboards.length > 0) { - scope.dashboard = dashboards[0]; - scope.updateView(); - } - }, function fail() { - }); - } - - $compile(element.contents())(scope); - } - - return { - restrict: "E", - require: "^ngModel", - link: linker, - scope: { - dashboardsScope: '@', - tenantId: '=', - customerId: '=', - theForm: '=?', - tbRequired: '=?', - disabled:'=ngDisabled', - selectFirstDashboard: '=' - } - }; -} diff --git a/ui/src/app/components/dashboard-autocomplete.scss b/ui/src/app/components/dashboard-autocomplete.scss deleted file mode 100644 index 176918f5c4..0000000000 --- a/ui/src/app/components/dashboard-autocomplete.scss +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-dashboard-autocomplete { - .tb-not-found { - display: block; - height: 48px; - line-height: 1.5; - } - - .tb-dashboard-item { - display: block; - height: 48px; - } - - li { - height: auto !important; - white-space: normal !important; - } -} diff --git a/ui/src/app/components/dashboard-autocomplete.tpl.html b/ui/src/app/components/dashboard-autocomplete.tpl.html deleted file mode 100644 index 74b801280d..0000000000 --- a/ui/src/app/components/dashboard-autocomplete.tpl.html +++ /dev/null @@ -1,43 +0,0 @@ - - - -
    - {{item.title}} -
    -
    - -
    - dashboard.no-dashboards-matching -
    -
    -
    -
    dashboard.dashboard-required
    -
    -
    diff --git a/ui/src/app/components/dashboard-select-panel.controller.js b/ui/src/app/components/dashboard-select-panel.controller.js deleted file mode 100644 index 913fa89933..0000000000 --- a/ui/src/app/components/dashboard-select-panel.controller.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/*@ngInject*/ -export default function DashboardSelectPanelController(mdPanelRef, $scope, $filter, dashboards, dashboardId, onDashboardSelected) { - - var vm = this; - vm._mdPanelRef = mdPanelRef; - vm.dashboards = dashboards; - vm.dashboardId = dashboardId; - - vm.dashboardSelected = dashboardSelected; - - function dashboardSelected(dashboardId) { - if (onDashboardSelected) { - onDashboardSelected(dashboardId); - } - } -} diff --git a/ui/src/app/components/dashboard-select-panel.tpl.html b/ui/src/app/components/dashboard-select-panel.tpl.html deleted file mode 100644 index 17736dd536..0000000000 --- a/ui/src/app/components/dashboard-select-panel.tpl.html +++ /dev/null @@ -1,31 +0,0 @@ - - -
    - - - - - - {{dashboard.title}} - - - - -
    -
    diff --git a/ui/src/app/components/dashboard-select.directive.js b/ui/src/app/components/dashboard-select.directive.js deleted file mode 100644 index 872b2b76c7..0000000000 --- a/ui/src/app/components/dashboard-select.directive.js +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './dashboard-select.scss'; - -import thingsboardApiDashboard from '../api/dashboard.service'; -import thingsboardApiUser from '../api/user.service'; - -/* eslint-disable import/no-unresolved, import/default */ - -import dashboardSelectTemplate from './dashboard-select.tpl.html'; -import dashboardSelectPanelTemplate from './dashboard-select-panel.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -import DashboardSelectPanelController from './dashboard-select-panel.controller'; - - -export default angular.module('thingsboard.directives.dashboardSelect', [thingsboardApiDashboard, thingsboardApiUser]) - .directive('tbDashboardSelect', DashboardSelect) - .controller('DashboardSelectPanelController', DashboardSelectPanelController) - .name; - -/*@ngInject*/ -function DashboardSelect($compile, $templateCache, $q, $mdMedia, $mdPanel, $document, types, dashboardService, userService) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - var template = $templateCache.get(dashboardSelectTemplate); - element.html(template); - - scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false; - scope.dashboardId = null; - - var pageLink = {limit: 100}; - - var promise; - if (scope.dashboardsScope === 'customer' || userService.getAuthority() === 'CUSTOMER_USER') { - if (scope.customerId && scope.customerId != types.id.nullUid) { - promise = dashboardService.getCustomerDashboards(scope.customerId, pageLink, {ignoreLoading: true}); - } else { - promise = $q.when({data: []}); - } - } else { - promise = dashboardService.getTenantDashboards(pageLink, {ignoreLoading: true}); - } - - promise.then(function success(result) { - scope.dashboards = result.data; - }, function fail() { - scope.dashboards = []; - }); - - scope.updateView = function () { - ngModelCtrl.$setViewValue(scope.dashboardId); - } - - ngModelCtrl.$render = function () { - if (ngModelCtrl.$viewValue) { - scope.dashboardId = ngModelCtrl.$viewValue; - } else { - scope.dashboardId = null; - } - } - - scope.$watch('dashboardId', function () { - scope.updateView(); - }); - - scope.openDashboardSelectPanel = function (event) { - if (scope.disabled) { - return; - } - var position; - var panelHeight = $mdMedia('min-height: 350px') ? 250 : 150; - var panelWidth = 300; - var offset = element[0].getBoundingClientRect(); - var bottomY = offset.bottom - $(window).scrollTop(); //eslint-disable-line - var leftX = offset.left - $(window).scrollLeft(); //eslint-disable-line - var yPosition; - var xPosition; - if (bottomY + panelHeight > $( window ).height()) { //eslint-disable-line - yPosition = $mdPanel.yPosition.ABOVE; - } else { - yPosition = $mdPanel.yPosition.BELOW; - } - if (leftX + panelWidth > $( window ).width()) { //eslint-disable-line - xPosition = $mdPanel.xPosition.CENTER; - } else { - xPosition = $mdPanel.xPosition.ALIGN_START; - } - position = $mdPanel.newPanelPosition() - .relativeTo(element) - .addPanelPosition(xPosition, yPosition); - var config = { - attachTo: angular.element($document[0].body), - controller: 'DashboardSelectPanelController', - controllerAs: 'vm', - templateUrl: dashboardSelectPanelTemplate, - panelClass: 'tb-dashboard-select-panel', - position: position, - fullscreen: false, - locals: { - dashboards: scope.dashboards, - dashboardId: scope.dashboardId, - onDashboardSelected: (dashboardId) => { - if (scope.panelRef) { - scope.panelRef.close(); - } - scope.dashboardId = dashboardId; - } - }, - openFrom: event, - clickOutsideToClose: true, - escapeToClose: true, - focusOnOpen: false - }; - $mdPanel.open(config).then(function(result) { - scope.panelRef = result; - }); - } - - $compile(element.contents())(scope); - } - - return { - restrict: "E", - require: "^ngModel", - link: linker, - scope: { - dashboardsScope: '@', - customerId: '=', - tbRequired: '=?', - disabled:'=ngDisabled' - } - }; -} diff --git a/ui/src/app/components/dashboard-select.scss b/ui/src/app/components/dashboard-select.scss deleted file mode 100644 index ed582c3299..0000000000 --- a/ui/src/app/components/dashboard-select.scss +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "../../scss/constants"; - -tb-dashboard-select { - min-width: 52px; - - md-select { - max-width: 300px; - pointer-events: all; - } -} - -.tb-dashboard-select { - min-height: 32px; - - span { - pointer-events: all; - cursor: pointer; - } -} - -.md-panel { - &.tb-dashboard-select-panel { - position: absolute; - } -} - -.tb-dashboard-select-panel { - min-width: 300px; - max-width: 320px; - max-height: 150px; - overflow-x: hidden; - overflow-y: auto; - background: #fff; - border-radius: 4px; - box-shadow: - 0 7px 8px -4px rgba(0, 0, 0, .2), - 0 13px 19px 2px rgba(0, 0, 0, .14), - 0 5px 24px 4px rgba(0, 0, 0, .12); - - @media (min-height: 350px) { - max-height: 250px; - } - - @media (min-width: $layout-breakpoint-xs) { - max-width: 100%; - } - - md-content { - background-color: #fff; - } -} diff --git a/ui/src/app/components/dashboard-select.tpl.html b/ui/src/app/components/dashboard-select.tpl.html deleted file mode 100644 index c5694e71f5..0000000000 --- a/ui/src/app/components/dashboard-select.tpl.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - {{dashboard.title}} - - -
    - - - {{ 'dashboard.select-dashboard' | translate }} - - dashboards - -
    \ No newline at end of file diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js deleted file mode 100644 index c046c90c3b..0000000000 --- a/ui/src/app/components/dashboard.directive.js +++ /dev/null @@ -1,1095 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './dashboard.scss'; - -import 'javascript-detect-element-resize/detect-element-resize'; -import angularGridster from 'angular-gridster'; -import thingsboardTypes from '../common/types.constant'; -import thingsboardApiWidget from '../api/widget.service'; -import thingsboardWidget from './widget/widget.directive'; -import thingsboardToast from '../services/toast'; -import thingsboardTimewindow from './timewindow.directive'; -import thingsboardEvents from './tb-event-directives'; -import thingsboardMousepointMenu from './mousepoint-menu.directive'; - -/* eslint-disable import/no-unresolved, import/default */ - -import dashboardTemplate from './dashboard.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/* eslint-disable angular/angularelement */ - -export default angular.module('thingsboard.directives.dashboard', [thingsboardTypes, - thingsboardToast, - thingsboardApiWidget, - thingsboardWidget, - thingsboardTimewindow, - thingsboardEvents, - thingsboardMousepointMenu, - angularGridster.name]) - .directive('tbDashboard', Dashboard) - .name; - -/*@ngInject*/ -function Dashboard() { - return { - restrict: "E", - scope: true, - bindToController: { - widgets: '=', - widgetLayouts: '=?', - aliasController: '=', - stateController: '=', - dashboardTimewindow: '=?', - columns: '=', - margins: '=', - isEdit: '=', - autofillHeight: '=', - mobileAutofillHeight: '=?', - mobileRowHeight: '=?', - isMobile: '=', - isMobileDisabled: '=?', - isEditActionEnabled: '=', - isExportActionEnabled: '=', - isRemoveActionEnabled: '=', - onEditWidget: '&?', - onExportWidget: '&?', - onRemoveWidget: '&?', - onWidgetMouseDown: '&?', - onWidgetClicked: '&?', - prepareDashboardContextMenu: '&?', - prepareWidgetContextMenu: '&?', - loadWidgets: '&?', - onInit: '&?', - onInitFailed: '&?', - dashboardStyle: '=?', - dashboardClass: '=?', - ignoreLoading: '=?' - }, - controller: DashboardController, - controllerAs: 'vm', - templateUrl: dashboardTemplate - }; -} - -/*@ngInject*/ -function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $mdUtil, $q, timeService, types, utils) { - - var highlightedMode = false; - var highlightedWidget = null; - var selectedWidget = null; - - var gridsterParent = angular.element('#gridster-parent', $element); - var gridsterElement = angular.element('#gridster-child', gridsterParent); - - var vm = this; - - vm.gridster = null; - - vm.isMobileDisabled = angular.isDefined(vm.isMobileDisabled) ? vm.isMobileDisabled : false; - - vm.isMobileSize = false; - - if (!('dashboardTimewindow' in vm)) { - vm.dashboardTimewindow = timeService.defaultTimewindow(); - } - - vm.dashboardLoading = true; - vm.visibleRect = { - top: 0, - bottom: 0, - left: 0, - right: 0 - }; - vm.gridsterOpts = { - pushing: false, - floating: false, - swapping: false, - maxRows: 100, - columns: vm.columns ? vm.columns : 24, - margins: vm.margins ? vm.margins : [10, 10], - minSizeX: 1, - minSizeY: 1, - defaultSizeX: 8, - defaultSizeY: 6, - resizable: { - enabled: vm.isEdit - }, - draggable: { - enabled: vm.isEdit - }, - saveGridItemCalculatedHeightInMobile: true - }; - - updateMobileOpts(); - - vm.widgetLayoutInfo = { - }; - - vm.widgetIds = []; - - vm.widgetItemMap = { - sizeX: 'vm.widgetLayoutInfo[widget.id].sizeX', - sizeY: 'vm.widgetLayoutInfo[widget.id].sizeY', - row: 'vm.widgetLayoutInfo[widget.id].row', - col: 'vm.widgetLayoutInfo[widget.id].col', - minSizeY: 'widget.minSizeY', - maxSizeY: 'widget.maxSizeY' - }; - - /*vm.widgetItemMap = { - sizeX: 'vm.widgetSizeX(widget)', - sizeY: 'vm.widgetSizeY(widget)', - row: 'vm.widgetRow(widget)', - col: 'vm.widgetCol(widget)', - minSizeY: 'widget.minSizeY', - maxSizeY: 'widget.maxSizeY' - };*/ - - vm.isWidgetExpanded = false; - vm.isHighlighted = isHighlighted; - vm.isNotHighlighted = isNotHighlighted; - vm.selectWidget = selectWidget; - vm.getSelectedWidget = getSelectedWidget; - vm.highlightWidget = highlightWidget; - vm.resetHighlight = resetHighlight; - - vm.onWidgetFullscreenChanged = onWidgetFullscreenChanged; - - vm.isAutofillHeight = autofillHeight; - - vm.widgetMouseDown = widgetMouseDown; - vm.widgetClicked = widgetClicked; - - vm.widgetSizeX = widgetSizeX; - vm.widgetSizeY = widgetSizeY; - vm.widgetRow = widgetRow; - vm.widgetCol = widgetCol; - vm.widgetStyle = widgetStyle; - vm.showWidgetTitle = showWidgetTitle; - vm.showWidgetTitleIcon = showWidgetTitleIcon; - vm.hasWidgetTitleTemplate = hasWidgetTitleTemplate; - vm.widgetTitleTemplate = widgetTitleTemplate; - vm.showWidgetTitlePanel = showWidgetTitlePanel; - vm.showWidgetActions = showWidgetActions; - vm.widgetTitleStyle = widgetTitleStyle; - vm.widgetTitle = widgetTitle; - vm.widgetTitleIcon = widgetTitleIcon; - vm.widgetTitleIconStyle = widgetTitleIconStyle; - vm.widgetTitleTooltip = widgetTitleTooltip; - vm.customWidgetHeaderActions = customWidgetHeaderActions; - vm.widgetActions = widgetActions; - vm.dropWidgetShadow = dropWidgetShadow; - vm.enableWidgetFullscreen = enableWidgetFullscreen; - vm.hasTimewindow = hasTimewindow; - vm.hasAggregation = hasAggregation; - vm.editWidget = editWidget; - vm.exportWidget = exportWidget; - vm.removeWidget = removeWidget; - vm.loading = loading; - - vm.openDashboardContextMenu = openDashboardContextMenu; - vm.openWidgetContextMenu = openWidgetContextMenu; - - vm.getEventGridPosition = getEventGridPosition; - vm.reload = reload; - - vm.contextMenuItems = []; - vm.contextMenuEvent = null; - - vm.widgetContextMenuItems = []; - vm.widgetContextMenuEvent = null; - - vm.dashboardTimewindowApi = { - onResetTimewindow: function() { - $timeout(function() { - if (vm.originalDashboardTimewindow) { - vm.dashboardTimewindow = angular.copy(vm.originalDashboardTimewindow); - vm.originalDashboardTimewindow = null; - } - }, 0); - }, - onUpdateTimewindow: function(startTimeMs, endTimeMs, interval) { - if (!vm.originalDashboardTimewindow) { - vm.originalDashboardTimewindow = angular.copy(vm.dashboardTimewindow); - } - $timeout(function() { - vm.dashboardTimewindow = timeService.toHistoryTimewindow(vm.dashboardTimewindow, startTimeMs, endTimeMs, interval); - }, 0); - }, - }; - - addResizeListener(gridsterParent[0], onGridsterParentResize); // eslint-disable-line no-undef - - $scope.$on("$destroy", function () { - removeResizeListener(gridsterParent[0], onGridsterParentResize); // eslint-disable-line no-undef - }); - - function onGridsterParentResize() { - if (gridsterParent.height() && autofillHeight()) { - updateMobileOpts(); - } - } - - //TODO: widgets visibility - /*gridsterParent.scroll(function () { - updateVisibleRect(); - }); - - gridsterParent.resize(function () { - updateVisibleRect(); - });*/ - - function updateMobileOpts() { - var isMobileDisabled = vm.isMobileDisabled === true; - var isMobile = vm.isMobile === true && !isMobileDisabled; - var mobileBreakPoint = isMobileDisabled ? 0 : (isMobile ? 20000 : 960); - - if (!isMobile && !isMobileDisabled) { - isMobile = !$mdMedia('gt-sm'); - } - - if (vm.gridsterOpts.isMobile != isMobile) { - vm.gridsterOpts.isMobile = isMobile; - vm.gridsterOpts.mobileModeEnabled = isMobile; - } - if (vm.gridsterOpts.mobileBreakPoint != mobileBreakPoint) { - vm.gridsterOpts.mobileBreakPoint = mobileBreakPoint; - } - detectRowSize(isMobile).then( - function(rowHeight) { - if (vm.gridsterOpts.rowHeight != rowHeight) { - vm.gridsterOpts.rowHeight = rowHeight; - } - } - ); - vm.isMobileSize = checkIsMobileSize(); - } - - function checkIsMobileSize() { - var isMobileDisabled = vm.isMobileDisabled === true; - var isMobileSize = vm.isMobile === true && !isMobileDisabled; - if (!isMobileSize && !isMobileDisabled) { - isMobileSize = !$mdMedia('gt-sm'); - } - return isMobileSize; - } - - $scope.$watch('vm.columns', function () { - var columns = vm.columns ? vm.columns : 24; - if (vm.gridsterOpts.columns != columns) { - vm.gridsterOpts.columns = columns; - if (vm.gridster) { - vm.gridster.columns = vm.columns; - updateGridsterParams(); - } - //TODO: widgets visibility - //updateVisibleRect(); - } - }); - - $scope.$watch(function() { - return $mdMedia('gt-sm') + ',' + vm.isMobile + ',' + vm.isMobileDisabled; - }, function() { - updateMobileOpts(); - sortWidgets(); - }); - - $scope.$watch(function() { - return vm.autofillHeight + ',' + vm.mobileAutofillHeight + ',' + vm.mobileRowHeight; - }, function () { - updateMobileOpts(); - }); - - $scope.$watch('vm.margins', function () { - var margins = vm.margins ? vm.margins : [10, 10]; - if (!angular.equals(vm.gridsterOpts.margins, margins)) { - vm.gridsterOpts.margins = margins; - updateMobileOpts(); - if (vm.gridster) { - vm.gridster.margins = vm.margins; - updateGridsterParams(); - } - //TODO: widgets visibility - //updateVisibleRect(); - } - }); - - $scope.$watch('vm.isEdit', function () { - vm.gridsterOpts.resizable.enabled = vm.isEdit; - vm.gridsterOpts.draggable.enabled = vm.isEdit; - $scope.$broadcast('toggleDashboardEditMode', vm.isEdit); - }); - - $scope.$watch('vm.isMobileSize', function (newVal, prevVal) { - if (!angular.equals(newVal, prevVal)) { - $scope.$broadcast('mobileModeChanged', vm.isMobileSize); - } - }); - - $scope.$watchCollection('vm.widgets', function () { - var ids = []; - for (var i=0;i= 0) { - order = vm.widgetLayouts[widget.id].mobileOrder; - } else if (angular.isDefined(widget.config.mobileOrder) && widget.config.mobileOrder >= 0) { - order = widget.config.mobileOrder; - } else if (hasLayout) { - order = vm.widgetLayouts[widget.id].row; - } else { - order = widget.row; - } - return order; - } - - $scope.$on('widgetPositionChanged', function () { - sortWidgets(); - }); - - loadDashboard(); - - function sortWidgets() { - vm.widgets.sort(function (widget1, widget2) { - var row1 = widgetOrder(widget1); - var row2 = widgetOrder(widget2); - var res = row1 - row2; - if (res === 0) { - res = widgetCol(widget1) - widgetCol(widget2); - } - return res; - }); - } - - function reload() { - loadDashboard(); - } - - function loadDashboard() { - $timeout(function () { - if (vm.loadWidgets) { - var promise = vm.loadWidgets(); - if (promise) { - promise.then(function () { - dashboardLoaded(); - }, function () { - dashboardLoaded(); - }); - } else { - dashboardLoaded(); - } - } else { - dashboardLoaded(); - } - }, 0, false); - } - - function updateGridsterParams() { - if (vm.gridster) { - if (vm.gridster.colWidth === 'auto') { - vm.gridster.curColWidth = (vm.gridster.curWidth + (vm.gridster.outerMargin ? -vm.gridster.margins[1] : vm.gridster.margins[1])) / vm.gridster.columns; - } else { - vm.gridster.curColWidth = vm.gridster.colWidth; - } - vm.gridster.curRowHeight = vm.gridster.rowHeight; - if (angular.isString(vm.gridster.rowHeight)) { - if (vm.gridster.rowHeight === 'match') { - vm.gridster.curRowHeight = Math.round(vm.gridster.curColWidth); - } else if (vm.gridster.rowHeight.indexOf('*') !== -1) { - vm.gridster.curRowHeight = Math.round(vm.gridster.curColWidth * vm.gridster.rowHeight.replace('*', '').replace(' ', '')); - } else if (vm.gridster.rowHeight.indexOf('/') !== -1) { - vm.gridster.curRowHeight = Math.round(vm.gridster.curColWidth / vm.gridster.rowHeight.replace('/', '').replace(' ', '')); - } - } - } - } - - //TODO: widgets visibility - /*function updateVisibleRect (force, containerResized) { - if (vm.gridster) { - var position = $(vm.gridster.$element).position() - if (position) { - var viewportWidth = gridsterParent.width(); - var viewportHeight = gridsterParent.height(); - var top = -position.top; - var bottom = top + viewportHeight; - var left = -position.left; - var right = left + viewportWidth; - - var newVisibleRect = { - top: vm.gridster.pixelsToRows(top), - topPx: top, - bottom: vm.gridster.pixelsToRows(bottom), - bottomPx: bottom, - left: vm.gridster.pixelsToColumns(left), - right: vm.gridster.pixelsToColumns(right), - isMobile: vm.gridster.isMobile, - curRowHeight: vm.gridster.curRowHeight, - containerResized: containerResized - }; - - if (force || - newVisibleRect.top != vm.visibleRect.top || - newVisibleRect.topPx != vm.visibleRect.topPx || - newVisibleRect.bottom != vm.visibleRect.bottom || - newVisibleRect.bottomPx != vm.visibleRect.bottomPx || - newVisibleRect.left != vm.visibleRect.left || - newVisibleRect.right != vm.visibleRect.right || - newVisibleRect.isMobile != vm.visibleRect.isMobile || - newVisibleRect.curRowHeight != vm.visibleRect.curRowHeight || - newVisibleRect.containerResized != vm.visibleRect.containerResized) { - vm.visibleRect = newVisibleRect; - $scope.$broadcast('visibleRectChanged', vm.visibleRect); - } - } - } - }*/ - - function checkIsLocalGridsterElement (gridster) { - return gridsterElement && gridsterElement[0] === gridster.$element[0]; - } - - function onWidgetFullscreenChanged(expanded, widget) { - vm.isWidgetExpanded = expanded; - $scope.$broadcast('onWidgetFullscreenChanged', vm.isWidgetExpanded, widget); - } - - function widgetMouseDown ($event, widget) { - if (vm.onWidgetMouseDown) { - vm.onWidgetMouseDown({event: $event, widget: widget}); - } - } - - function widgetClicked ($event, widget) { - if (vm.onWidgetClicked) { - vm.onWidgetClicked({event: $event, widget: widget}); - } - } - - function openDashboardContextMenu($event, $mdOpenMousepointMenu) { - if (vm.prepareDashboardContextMenu) { - vm.contextMenuItems = vm.prepareDashboardContextMenu(); - if (vm.contextMenuItems && vm.contextMenuItems.length > 0) { - vm.contextMenuEvent = $event; - $mdOpenMousepointMenu($event); - } - } - } - - function openWidgetContextMenu($event, widget, $mdOpenMousepointMenu) { - if (vm.prepareWidgetContextMenu) { - vm.widgetContextMenuItems = vm.prepareWidgetContextMenu({widget: widget}); - if (vm.widgetContextMenuItems && vm.widgetContextMenuItems.length > 0) { - vm.widgetContextMenuEvent = $event; - $mdOpenMousepointMenu($event); - } - } - } - - function getEventGridPosition(event) { - var pos = { - row: 0, - column: 0 - } - if (!gridsterParent) { - return pos; - } - var offset = gridsterParent.offset(); - var x = event.pageX - offset.left + gridsterParent.scrollLeft(); - var y = event.pageY - offset.top + gridsterParent.scrollTop(); - if (vm.gridster) { - pos.row = vm.gridster.pixelsToRows(y); - pos.column = vm.gridster.pixelsToColumns(x); - } - return pos; - } - - function editWidget ($event, widget) { - if ($event) { - $event.stopPropagation(); - } - if (vm.isEditActionEnabled && vm.onEditWidget) { - vm.onEditWidget({event: $event, widget: widget}); - } - } - - function exportWidget ($event, widget) { - if ($event) { - $event.stopPropagation(); - } - if (vm.isExportActionEnabled && vm.onExportWidget) { - vm.onExportWidget({event: $event, widget: widget}); - } - } - - function removeWidget($event, widget) { - if ($event) { - $event.stopPropagation(); - } - if (vm.isRemoveActionEnabled && vm.onRemoveWidget) { - vm.onRemoveWidget({event: $event, widget: widget}); - } - } - - function highlightWidget(widget, delay) { - if (!highlightedMode || highlightedWidget != widget) { - highlightedMode = true; - highlightedWidget = widget; - scrollToWidget(widget, delay); - } - } - - function selectWidget(widget, delay) { - if (selectedWidget != widget) { - selectedWidget = widget; - scrollToWidget(widget, delay); - } - } - - function scrollToWidget(widget, delay) { - if (vm.gridster) { - var item = angular.element('.gridster-item', vm.gridster.$element)[vm.widgets.indexOf(widget)]; - if (item) { - var height = angular.element(item).outerHeight(true); - var rectHeight = gridsterParent.height(); - var offset = (rectHeight - height) / 2; - var scrollTop = item.offsetTop; - if (offset > 0) { - scrollTop -= offset; - } - gridsterParent.animate({ - scrollTop: scrollTop - }, delay); - } - } - } - - function getSelectedWidget() { - return selectedWidget; - } - - function resetHighlight() { - highlightedMode = false; - highlightedWidget = null; - selectedWidget = null; - } - - function isHighlighted(widget) { - return (highlightedMode && highlightedWidget === widget) || (selectedWidget === widget); - } - - function isNotHighlighted(widget) { - return highlightedMode && highlightedWidget != widget; - } - - function widgetSizeX(widget) { - if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) { - return vm.widgetLayouts[widget.id].sizeX; - } else { - return widget.sizeX; - } - } - - function setWidgetSizeX(widget, sizeX) { - if (!vm.gridsterOpts.isMobile) { - if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) { - vm.widgetLayouts[widget.id].sizeX = sizeX; - } else { - widget.sizeX = sizeX; - } - } - } - - function widgetSizeY(widget) { - if (vm.gridsterOpts.isMobile && !vm.mobileAutofillHeight) { - var mobileHeight; - if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) { - mobileHeight = vm.widgetLayouts[widget.id].mobileHeight; - } - if (!mobileHeight && widget.config.mobileHeight) { - mobileHeight = widget.config.mobileHeight; - } - if (mobileHeight) { - return mobileHeight; - } else { - return widget.sizeY * 24 / vm.gridsterOpts.columns; - } - } else { - if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) { - return vm.widgetLayouts[widget.id].sizeY; - } else { - return widget.sizeY; - } - } - } - - function setWidgetSizeY(widget, sizeY) { - if (!vm.gridsterOpts.isMobile && !vm.autofillHeight) { - if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) { - vm.widgetLayouts[widget.id].sizeY = sizeY; - } else { - widget.sizeY = sizeY; - } - } - } - - function widgetRow(widget) { - if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) { - return vm.widgetLayouts[widget.id].row; - } else { - return widget.row; - } - } - - function setWidgetRow(widget, row) { - if (!vm.gridsterOpts.isMobile) { - if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) { - vm.widgetLayouts[widget.id].row = row; - } else { - widget.row = row; - } - } - } - - function widgetCol(widget) { - if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) { - return vm.widgetLayouts[widget.id].col; - } else { - return widget.col; - } - } - - function setWidgetCol(widget, col) { - if (!vm.gridsterOpts.isMobile) { - if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) { - vm.widgetLayouts[widget.id].col = col; - } else { - widget.col = col; - } - } - } - - function widgetStyle(widget) { - var style = {cursor: 'pointer', - color: widgetColor(widget), - backgroundColor: widgetBackgroundColor(widget), - padding: widgetPadding(widget), - margin: widgetMargin(widget)}; - if (angular.isDefined(widget.config.widgetStyle)) { - Object.assign(style, widget.config.widgetStyle); - } - return style; - } - - function widgetColor(widget) { - if (widget.config.color) { - return widget.config.color; - } else { - return 'rgba(0, 0, 0, 0.87)'; - } - } - - function widgetBackgroundColor(widget) { - if (widget.config.backgroundColor) { - return widget.config.backgroundColor; - } else { - return '#fff'; - } - } - - function widgetPadding(widget) { - if (widget.config.padding) { - return widget.config.padding; - } else { - return '8px'; - } - } - - function widgetMargin(widget) { - if (widget.config.margin) { - return widget.config.margin; - } else { - return '0px'; - } - } - - function showWidgetTitle(widget) { - if (angular.isDefined(widget.config.showTitle)) { - return widget.config.showTitle; - } else { - return true; - } - } - - function showWidgetTitleIcon(widget) { - if (angular.isDefined(widget.config.showTitleIcon)) { - return widget.config.showTitleIcon; - } else { - return false; - } - } - - function hasWidgetTitleTemplate(widget) { - var ctx = widgetContext(widget); - if (ctx && ctx.widgetTitleTemplate) { - return true; - } else { - return false; - } - } - - function widgetTitleTemplate(widget) { - var ctx = widgetContext(widget); - if (ctx && ctx.widgetTitleTemplate) { - return ctx.widgetTitleTemplate; - } else { - return ''; - } - } - - function showWidgetTitlePanel(widget) { - var ctx = widgetContext(widget); - if (ctx && ctx.hideTitlePanel) { - return false; - } else { - return hasWidgetTitleTemplate(widget) || showWidgetTitle(widget) || hasTimewindow(widget); - } - } - - function showWidgetActions(widget) { - var ctx = widgetContext(widget); - if (ctx && ctx.hideTitlePanel) { - return false; - } else { - return true; - } - } - - function widgetTitleStyle(widget) { - if (angular.isDefined(widget.config.titleStyle)) { - return widget.config.titleStyle; - } else { - return {}; - } - } - - function widgetTitle(widget) { - var ctx = widgetContext(widget); - if (ctx && ctx.widgetTitle - && ctx.widgetTitle.length) { - return ctx.widgetTitle; - } else { - return widget.config.title; - } - } - - function widgetTitleIcon(widget) { - if (angular.isDefined(widget.config.titleIcon)) { - return widget.config.titleIcon; - } else { - return ''; - } - } - - function widgetTitleIconStyle(widget) { - var style = {}; - if (angular.isDefined(widget.config.iconColor)) { - style.color = widget.config.iconColor; - } - if (angular.isDefined(widget.config.iconSize)) { - style.fontSize = widget.config.iconSize; - } - return style; - } - - function widgetTitleTooltip(widget) { - var ctx = widgetContext(widget); - if (ctx && ctx.widgetTitleTooltip - && ctx.widgetTitleTooltip.length) { - return ctx.widgetTitleTooltip; - } else { - return widget.config.titleTooltip; - } - } - - function customWidgetHeaderActions(widget) { - var ctx = widgetContext(widget); - if (ctx && ctx.customHeaderActions && ctx.customHeaderActions.length) { - return ctx.customHeaderActions; - } else { - return []; - } - } - - function widgetActions(widget) { - var ctx = widgetContext(widget); - if (ctx && ctx.widgetActions && ctx.widgetActions.length) { - return ctx.widgetActions; - } else { - return []; - } - } - - function widgetContext(widget) { - var context; - if (widget.$ctx) { - context = widget.$ctx(); - } - return context; - } - - function dropWidgetShadow(widget) { - if (angular.isDefined(widget.config.dropShadow)) { - return widget.config.dropShadow; - } else { - return true; - } - } - - function enableWidgetFullscreen(widget) { - if (angular.isDefined(widget.config.enableFullscreen)) { - return widget.config.enableFullscreen; - } else { - return true; - } - } - - function hasTimewindow(widget) { - if (widget.type === types.widgetType.timeseries.value || widget.type === types.widgetType.alarm.value) { - return angular.isDefined(widget.config.useDashboardTimewindow) ? - (!widget.config.useDashboardTimewindow && (angular.isUndefined(widget.config.displayTimewindow) || widget.config.displayTimewindow)) : false; - } else { - return false; - } - } - - function hasAggregation(widget) { - return widget.type === types.widgetType.timeseries.value; - } - - function adoptMaxRows() { - if (vm.widgets) { - var maxRows = vm.gridsterOpts.maxRows; - for (var i = 0; i < vm.widgets.length; i++) { - var w = vm.widgets[i]; - var bottom = widgetRow(w) + widgetSizeY(w); - maxRows = Math.max(maxRows, bottom); - } - vm.gridsterOpts.maxRows = Math.max(maxRows, vm.gridsterOpts.maxRows); - } - } - - function dashboardLoaded() { - $mdUtil.nextTick(function () { - if (vm.dashboardTimewindowWatch) { - vm.dashboardTimewindowWatch(); - vm.dashboardTimewindowWatch = null; - } - vm.dashboardTimewindowWatch = $scope.$watch('vm.dashboardTimewindow', function () { - $scope.$broadcast('dashboardTimewindowChanged', vm.dashboardTimewindow); - }, true); - adoptMaxRows(); - vm.dashboardLoading = false; - if ($scope.gridsterScopeWatcher) { - $scope.gridsterScopeWatcher(); - } - $scope.gridsterScopeWatcher = $scope.$watch( - function() { - var hasScope = gridsterElement.scope() ? true : false; - return hasScope; - }, - function(hasScope) { - if (hasScope) { - $scope.gridsterScopeWatcher(); - $scope.gridsterScopeWatcher = null; - var gridsterScope = gridsterElement.scope(); - vm.gridster = gridsterScope.gridster; - setupGridster(vm.gridster); - if (vm.onInit) { - vm.onInit({dashboard: vm}); - } - } - } - ); - }); - } - - function setupGridster(gridster) { - if (gridster) { - if (!gridster.origMoveOverlappingItems) { - gridster.origMoveOverlappingItems = gridster.moveOverlappingItems; - gridster.moveOverlappingItems = () => {}; - } - } - } - - function loading() { - return !vm.ignoreLoading && $rootScope.loading; - } - -} - -/* eslint-enable angular/angularelement */ diff --git a/ui/src/app/components/dashboard.scss b/ui/src/app/components/dashboard.scss deleted file mode 100644 index 6ba550fbf2..0000000000 --- a/ui/src/app/components/dashboard.scss +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "~compass-sass-mixins/lib/compass"; - -div.tb-widget { - position: relative; - height: 100%; - margin: 0; - overflow: hidden; - outline: none; - - transition: all .2s ease-in-out; - - .tb-widget-title { - max-height: 65px; - padding-top: 5px; - padding-left: 5px; - overflow: hidden; - - tb-timewindow { - font-size: 14px; - opacity: .85; - } - - .title{ - width: 100%; - overflow: hidden; - text-overflow: ellipsis; - } - } - - .tb-widget-actions { - z-index: 19; - margin: 5px 0 0; - - &-absolute{ - position: absolute; - top: 3px; - right: 8px; - } - - .md-button.md-icon-button { - width: 32px; - min-width: 32px; - height: 32px; - min-height: 32px; - padding: 0 !important; - margin: 0 !important; - line-height: 20px; - - md-icon, - ng-md-icon { - width: 20px; - min-width: 20px; - height: 20px; - min-height: 20px; - font-size: 20px; - } - } - } - - .tb-widget-content { - tb-widget { - position: relative; - width: 100%; - } - } -} - -div.tb-widget.tb-highlighted { - border: 1px solid #039be5; - box-shadow: 0 0 20px #039be5; -} - -div.tb-widget.tb-not-highlighted { - opacity: .5; -} - -tb-dashboard { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; -} - -md-content.tb-dashboard-content { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - background: none; - outline: none; - - .gridster-item { - transition: none; - } -} - -.tb-widget-error-container { - position: absolute; - width: 100%; - height: 100%; - background-color: #fff; -} - -.tb-widget-error-msg { - padding: 5px; - font-size: 16px; - color: #f00; - word-wrap: break-word; -} diff --git a/ui/src/app/components/dashboard.tpl.html b/ui/src/app/components/dashboard.tpl.html deleted file mode 100644 index fd9eac4a99..0000000000 --- a/ui/src/app/components/dashboard.tpl.html +++ /dev/null @@ -1,158 +0,0 @@ - - - - - - - -
    -
    -
      -
    • - -
      -
      -
      -
      - - - {{vm.widgetTitleIcon(widget)}} - - {{vm.widgetTitle(widget)}} - - - - - -
      -
      - - - {{action.displayName}} - - - - - - {{ action.name | translate }} - - - - - - - {{ 'widget.edit' | translate }} - - - - - - {{ 'widget.export' | translate }} - - - - - - {{ 'widget.remove' | translate }} - - - -
      -
      -
      -
      -
      -
      -
      - - - - {{ item.shortcut | keyboardShortcut }} - {{item.icon}} - {{item.value}} - - - -
      -
    • -
    -
    -
    -
    - - - - {{ item.shortcut | keyboardShortcut }} - {{item.icon}} - {{item.value}} - - - -
    diff --git a/ui/src/app/components/datakey-config-dialog.controller.js b/ui/src/app/components/datakey-config-dialog.controller.js deleted file mode 100644 index 3e34420671..0000000000 --- a/ui/src/app/components/datakey-config-dialog.controller.js +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import thingsboardDatakeyConfig from './datakey-config.directive'; - -export default angular.module('thingsboard.dialogs.datakeyConfigDialog', [thingsboardDatakeyConfig]) - .controller('DatakeyConfigDialogController', DatakeyConfigDialogController) - .name; - -/*@ngInject*/ -function DatakeyConfigDialogController($scope, $mdDialog, $q, entityService, dataKey, dataKeySettingsSchema, entityAlias, aliasController) { - - var vm = this; - - vm.dataKey = dataKey; - vm.dataKeySettingsSchema = dataKeySettingsSchema; - vm.entityAlias = entityAlias; - vm.aliasController = aliasController; - - vm.hide = function () { - $mdDialog.hide(); - }; - - vm.cancel = function () { - $mdDialog.cancel(); - }; - - vm.fetchEntityKeys = function (entityAliasId, query, type) { - var deferred = $q.defer(); - vm.aliasController.getAliasInfo(entityAliasId).then( - function success(aliasInfo) { - var entity = aliasInfo.currentEntity; - if (entity) { - entityService.getEntityKeys(entity.entityType, entity.id, query, type, {ignoreLoading: true}).then( - function success(keys) { - deferred.resolve(keys); - }, - function fail() { - deferred.resolve([]); - } - ); - } else { - deferred.resolve([]); - } - }, - function fail() { - deferred.resolve([]); - } - ); - return deferred.promise; - }; - - vm.save = function () { - $scope.$broadcast('form-submit'); - if ($scope.theForm.$valid) { - $scope.theForm.$setPristine(); - $mdDialog.hide(vm.dataKey); - } - }; -} \ No newline at end of file diff --git a/ui/src/app/components/datakey-config-dialog.tpl.html b/ui/src/app/components/datakey-config-dialog.tpl.html deleted file mode 100644 index 05344bccd8..0000000000 --- a/ui/src/app/components/datakey-config-dialog.tpl.html +++ /dev/null @@ -1,46 +0,0 @@ - - -
    - -
    -

    datakey.configuration

    - - - - -
    -
    - - - - - - - - - - {{ 'action.save' | translate }} - - {{ 'action.cancel' | translate }} - -
    -
    diff --git a/ui/src/app/components/datakey-config.directive.js b/ui/src/app/components/datakey-config.directive.js deleted file mode 100644 index 593bbaa76d..0000000000 --- a/ui/src/app/components/datakey-config.directive.js +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './datakey-config.scss'; - -import thingsboardJsonForm from "./json-form.directive"; -import thingsboardTypes from '../common/types.constant'; -import thingsboardJsFunc from './js-func.directive'; - -/* eslint-disable import/no-unresolved, import/default */ - -import datakeyConfigTemplate from './datakey-config.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -export default angular.module('thingsboard.directives.datakeyConfig', [thingsboardTypes, thingsboardJsFunc, thingsboardJsonForm]) - .directive('tbDatakeyConfig', DatakeyConfig) - .name; - -/*@ngInject*/ -function DatakeyConfig($compile, $templateCache, $q, types) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - var template = $templateCache.get(datakeyConfigTemplate); - - if (scope.datakeySettingsSchema.schema) { - scope.dataKeySchema = scope.datakeySettingsSchema.schema; - scope.dataKeyForm = scope.datakeySettingsSchema.form || ['*']; - template = '\n' + - '\n' + - template + - '\n' + - '\n' + - '\n' + - '
    \n' + - '' + - '' + - '' + - '\n' + - '
    \n' + - '
    \n' + - '
    \n' + - '
    '; - } - - element.html(template); - - scope.types = types; - - scope.alarmFields = []; - for (var alarmField in types.alarmFields) { - scope.alarmFields.push(alarmField); - } - - scope.selectedKey = null; - scope.keySearchText = null; - scope.usePostProcessing = false; - - scope.functions = {}; - - ngModelCtrl.$render = function () { - scope.model = {}; - if (ngModelCtrl.$viewValue) { - scope.model.type = ngModelCtrl.$viewValue.type; - scope.model.name = ngModelCtrl.$viewValue.name; - scope.model.label = ngModelCtrl.$viewValue.label; - scope.model.color = ngModelCtrl.$viewValue.color; - scope.model.units = ngModelCtrl.$viewValue.units; - scope.model.decimals = ngModelCtrl.$viewValue.decimals; - scope.model.funcBody = ngModelCtrl.$viewValue.funcBody; - scope.model.postFuncBody = ngModelCtrl.$viewValue.postFuncBody; - scope.model.usePostProcessing = scope.model.postFuncBody ? true : false; - scope.model.settings = ngModelCtrl.$viewValue.settings; - } - }; - - scope.$watch('model', function (newVal, oldVal) { - if (newVal.usePostProcessing != oldVal.usePostProcessing) { - if (scope.model.usePostProcessing && !scope.model.postFuncBody) { - scope.model.postFuncBody = "return value;"; - } else if (!scope.model.usePostProcessing && scope.model.postFuncBody) { - delete scope.model.postFuncBody; - } - } - if (ngModelCtrl.$viewValue) { - var value = ngModelCtrl.$viewValue; - value.type = scope.model.type; - value.name = scope.model.name; - value.label = scope.model.label; - value.color = scope.model.color; - value.units = scope.model.units; - value.decimals = scope.model.decimals; - value.funcBody = scope.model.funcBody; - if (!scope.model.postFuncBody) { - delete value.postFuncBody; - } else { - value.postFuncBody = scope.model.postFuncBody; - } - ngModelCtrl.$setViewValue(value); - } - }, true); - - scope.keysSearch = function (searchText) { - if (scope.model.type === types.dataKeyType.alarm) { - var dataKeys = searchText ? scope.alarmFields.filter( - scope.createFilterForDataKey(searchText)) : scope.alarmFields; - dataKeys.push(searchText); - return dataKeys; - } else { - if (scope.entityAlias) { - var deferred = $q.defer(); - scope.fetchEntityKeys({ - entityAliasId: scope.entityAlias.id, - query: searchText, - type: scope.model.type - }) - .then(function (keys) { - keys.push(searchText); - deferred.resolve(keys); - }, function (e) { - deferred.reject(e); - }); - return deferred.promise; - } else { - return $q.when([]); - } - } - }; - - scope.createFilterForDataKey = function (query) { - var lowercaseQuery = angular.lowercase(query); - return function filterFn(dataKey) { - return (angular.lowercase(dataKey).indexOf(lowercaseQuery) === 0); - }; - }; - - $compile(element.contents())(scope); - } - - return { - restrict: 'E', - require: '^ngModel', - scope: { - entityAlias: '=', - fetchEntityKeys: '&', - datakeySettingsSchema: '=' - }, - link: linker - }; -} \ No newline at end of file diff --git a/ui/src/app/components/datakey-config.scss b/ui/src/app/components/datakey-config.scss deleted file mode 100644 index 1337dbe106..0000000000 --- a/ui/src/app/components/datakey-config.scss +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-datakey-config { - min-width: 500px !important; - min-height: 500px !important; - - md-content { - background-color: #fff; - } -} - -tb-datakey-config { - md-content { - background-color: #fff; - } -} diff --git a/ui/src/app/components/datakey-config.tpl.html b/ui/src/app/components/datakey-config.tpl.html deleted file mode 100644 index dde2fd40cc..0000000000 --- a/ui/src/app/components/datakey-config.tpl.html +++ /dev/null @@ -1,88 +0,0 @@ - - - - {{item}} - -
    - - - - -
    -
    -
    -
    - - - - - - - - -
    -
    - datakey.data-generation-func -
    - - -
    -
    - - {{ 'datakey.use-data-post-processing-func' | translate }} - - - - -
    -
    diff --git a/ui/src/app/components/datasource-entity.directive.js b/ui/src/app/components/datasource-entity.directive.js deleted file mode 100644 index edc891b2c8..0000000000 --- a/ui/src/app/components/datasource-entity.directive.js +++ /dev/null @@ -1,314 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './datasource-entity.scss'; - -import 'md-color-picker'; -import tinycolor from 'tinycolor2'; -import $ from 'jquery'; -import thingsboardTypes from '../common/types.constant'; -import thingsboardDatakeyConfigDialog from './datakey-config-dialog.controller'; -import thingsboardTruncate from './truncate.filter'; - -/* eslint-disable import/no-unresolved, import/default */ - -import datasourceEntityTemplate from './datasource-entity.tpl.html'; -import datakeyConfigDialogTemplate from './datakey-config-dialog.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/* eslint-disable angular/angularelement */ - -export default angular.module('thingsboard.directives.datasourceEntity', [thingsboardTruncate, thingsboardTypes, thingsboardDatakeyConfigDialog]) - .directive('tbDatasourceEntity', DatasourceEntity) - .name; - -/*@ngInject*/ -function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $document, $mdColorPicker, $mdConstant, types) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - var template = $templateCache.get(datasourceEntityTemplate); - element.html(template); - - scope.ngModelCtrl = ngModelCtrl; - scope.types = types; - - scope.alarmFields = []; - for (var alarmField in types.alarmFields) { - scope.alarmFields.push(alarmField); - } - - scope.selectedDataKey = null; - scope.dataKeySearchText = null; - - scope.selectedAlarmDataKey = null; - scope.alarmDataKeySearchText = null; - - scope.updateValidity = function () { - if (ngModelCtrl.$viewValue) { - var value = ngModelCtrl.$viewValue; - var dataValid = angular.isDefined(value) && value != null; - ngModelCtrl.$setValidity('entityData', dataValid); - if (dataValid) { - ngModelCtrl.$setValidity('entityAlias', - angular.isDefined(value.entityAliasId) && - value.entityAliasId != null); - if (scope.optDataKeys) { - ngModelCtrl.$setValidity('entityKeys', true); - } else { - ngModelCtrl.$setValidity('entityKeys', - angular.isDefined(value.dataKeys) && - value.dataKeys != null && - value.dataKeys.length > 0); - } - } - } - }; - - scope.$watch('entityAlias', function () { - if (ngModelCtrl.$viewValue) { - if (scope.entityAlias) { - ngModelCtrl.$viewValue.entityAliasId = scope.entityAlias.id; - } else { - ngModelCtrl.$viewValue.entityAliasId = null; - } - scope.updateValidity(); - scope.selectedEntityAliasChange(); - } - }); - - scope.$watch('dataKeys', function () { - updateDataKeys(); - }, true); - - scope.$watch('alarmDataKeys', function () { - updateDataKeys(); - }, true); - - function updateDataKeys() { - if (ngModelCtrl.$viewValue) { - var dataKeys = []; - dataKeys = dataKeys.concat(scope.dataKeys); - dataKeys = dataKeys.concat(scope.alarmDataKeys); - if (!angular.equals(ngModelCtrl.$viewValue.dataKeys, dataKeys)) - { - ngModelCtrl.$setDirty(); - ngModelCtrl.$viewValue.dataKeys = dataKeys; - } - scope.updateValidity(); - } - } - - ngModelCtrl.$render = function () { - if (ngModelCtrl.$viewValue) { - var entityAliasId = ngModelCtrl.$viewValue.entityAliasId; - var entityAliases = scope.aliasController.getEntityAliases(); - if (entityAliases[entityAliasId]) { - scope.entityAlias = entityAliases[entityAliasId]; - } else { - scope.entityAlias = null; - } - var dataKeys = []; - var alarmDataKeys = []; - for (var d in ngModelCtrl.$viewValue.dataKeys) { - var dataKey = ngModelCtrl.$viewValue.dataKeys[d]; - if ((dataKey.type === types.dataKeyType.timeseries) || (dataKey.type === types.dataKeyType.attribute) || (dataKey.type === types.dataKeyType.entityField)) { - dataKeys.push(dataKey); - } else if (dataKey.type === types.dataKeyType.alarm) { - alarmDataKeys.push(dataKey); - } - } - scope.dataKeys = dataKeys; - scope.alarmDataKeys = alarmDataKeys; - } - }; - - scope.textIsNotEmpty = function(text) { - return (text && text != null && text.length > 0) ? true : false; - } - - scope.selectedEntityAliasChange = function () { - if (!scope.dataKeySearchText || scope.dataKeySearchText === '') { - scope.dataKeySearchText = scope.dataKeySearchText === '' ? null : ''; - } - if (!scope.alarmDataKeySearchText || scope.alarmDataKeySearchText === '') { - scope.alarmDataKeySearchText = scope.alarmDataKeySearchText === '' ? null : ''; - } - }; - - scope.transformDataKeyChip = function (chip) { - if (scope.maxDataKeys > 0 && ngModelCtrl.$viewValue.dataKeys.length >= scope.maxDataKeys ) { - return null; - } else { - if (chip.type) { - return scope.generateDataKey({chip: chip.name, type: chip.type}); - } else { - if (scope.widgetType != types.widgetType.latest.value) { - return scope.generateDataKey({chip: chip, type: types.dataKeyType.timeseries}); - } else { - return null; - } - } - } - }; - - scope.transformAlarmDataKeyChip = function (chip) { - if (chip.type) { - return scope.generateDataKey({chip: chip.name, type: chip.type}); - } else { - return scope.generateDataKey({chip: chip, type: types.dataKeyType.alarm}); - } - }; - - scope.showColorPicker = function (event, dataKey) { - $mdColorPicker.show({ - value: dataKey.color, - defaultValue: '#fff', - random: tinycolor.random(), - clickOutsideToClose: false, - hasBackdrop: false, - multiple: true, - preserveScope: false, - - mdColorAlphaChannel: true, - mdColorSpectrum: true, - mdColorSliders: true, - mdColorGenericPalette: false, - mdColorMaterialPalette: true, - mdColorHistory: false, - mdColorDefaultTab: 2, - - $event: event - - }).then(function (color) { - dataKey.color = color; - ngModelCtrl.$setDirty(); - }); - }; - - scope.editDataKey = function (event, dataKey) { - - $mdDialog.show({ - controller: 'DatakeyConfigDialogController', - controllerAs: 'vm', - templateUrl: datakeyConfigDialogTemplate, - locals: { - dataKey: angular.copy(dataKey), - dataKeySettingsSchema: scope.datakeySettingsSchema, - entityAlias: scope.entityAlias, - aliasController: scope.aliasController - }, - parent: angular.element($document[0].body), - fullscreen: true, - targetEvent: event, - multiple: true, - onComplete: function () { - var w = angular.element($window); - w.triggerHandler('resize'); - } - }).then(function (newDataKey) { - if ((newDataKey.type === types.dataKeyType.timeseries) || (newDataKey.type === types.dataKeyType.attribute) || (newDataKey.type === types.dataKeyType.entityField)) { - let index = scope.dataKeys.indexOf(dataKey); - scope.dataKeys[index] = newDataKey; - } else if (newDataKey.type === types.dataKeyType.alarm) { - let index = scope.alarmDataKeys.indexOf(dataKey); - scope.alarmDataKeys[index] = newDataKey; - } - ngModelCtrl.$setDirty(); - }, function () { - }); - }; - - scope.dataKeysSearch = function (searchText) { - if (scope.widgetType == types.widgetType.alarm.value) { - var dataKeys = searchText ? scope.alarmFields.filter( - scope.createFilterForDataKey(searchText)) : scope.alarmFields; - return dataKeys; - } else { - if (scope.entityAlias) { - var deferred = $q.defer(); - scope.fetchEntityKeys({entityAliasId: scope.entityAlias.id, query: searchText, type: types.dataKeyType.timeseries}) - .then(function (dataKeys) { - var items = []; - for (var i = 0; i < dataKeys.length; i++) { - items.push({ name: dataKeys[i], type: types.dataKeyType.timeseries }); - } - if (scope.widgetType == types.widgetType.latest.value) { - var keysType = [types.dataKeyType.attribute, types.dataKeyType.entityField]; - var promises = []; - keysType.forEach((type) => { - promises.push(scope.fetchEntityKeys({entityAliasId: scope.entityAlias.id, query: searchText, type: type})); - }); - $q.all(promises).then(function (dataKeys) { - for (var i = 0; i < dataKeys.length; i++) { - for (var j = 0; j < dataKeys[i].length; j++) { - items.push({name: dataKeys[i][j], type: keysType[i]}); - } - } - deferred.resolve(items); - }, function (e) { - deferred.reject(e); - }); - } - else - deferred.resolve(items); - }, function (e) { - deferred.reject(e); - }); - return deferred.promise; - } else { - return $q.when([]); - } - } - }; - - scope.createFilterForDataKey = function (query) { - var lowercaseQuery = angular.lowercase(query); - return function filterFn(dataKey) { - return (angular.lowercase(dataKey).indexOf(lowercaseQuery) === 0); - }; - }; - - scope.createKey = function (event, type, chipsId) { - var chipsChild = $(chipsId, element)[0].firstElementChild; - var el = angular.element(chipsChild); - var chipBuffer = el.scope().$mdChipsCtrl.getChipBuffer(); - event.preventDefault(); - event.stopPropagation(); - el.scope().$mdChipsCtrl.appendChip({ name: chipBuffer.trim(), type: type}); - el.scope().$mdChipsCtrl.resetChipBuffer(); - } - - $compile(element.contents())(scope); - } - - return { - restrict: "E", - require: "^ngModel", - scope: { - widgetType: '=', - maxDataKeys: '=', - optDataKeys: '=', - aliasController: '=', - datakeySettingsSchema: '=', - generateDataKey: '&', - fetchEntityKeys: '&', - onCreateEntityAlias: '&' - }, - link: linker - }; -} - -/* eslint-enable angular/angularelement */ diff --git a/ui/src/app/components/datasource-entity.scss b/ui/src/app/components/datasource-entity.scss deleted file mode 100644 index 41465df44d..0000000000 --- a/ui/src/app/components/datasource-entity.scss +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "../../scss/constants"; - -.tb-entity-alias-autocomplete, -.tb-datakey-autocomplete, -.tb-timeseries-datakey-autocomplete, -.tb-attribute-datakey-autocomplete, -.tb-alarm-datakey-autocomplete { - .tb-not-found { - display: block; - height: 48px; - line-height: 1.5; - white-space: normal !important; - - .tb-no-entries { - line-height: 48px; - } - } - - li { - height: auto !important; - } -} - -tb-datasource-entity { - @media (min-width: $layout-breakpoint-sm) { - padding-right: 4px; - padding-left: 4px; - } - - tb-entity-alias-select { - @media (min-width: $layout-breakpoint-sm) { - width: 200px; - max-width: 200px; - } - } -} diff --git a/ui/src/app/components/datasource-entity.tpl.html b/ui/src/app/components/datasource-entity.tpl.html deleted file mode 100644 index 645e326360..0000000000 --- a/ui/src/app/components/datasource-entity.tpl.html +++ /dev/null @@ -1,182 +0,0 @@ - -
    - - -
    -
    - - - - {{'datakey.attributes' | translate }} - - - - {{'datakey.entity-field' | translate }} - - - - {{'datakey.timeseries' | translate }} - - - {{item.name}} - -
    -
    - entity.no-keys-found -
    -
    - entity.no-key-matching - {{'entity.create-new-key' | translate }} - - {{'datakey.attributes' | translate }} - - - - {{'datakey.entity-field' | translate }} - - - - {{'datakey.timeseries' | translate }} - - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    -
    -
    - - {{'datakey.attributes' | translate }} - - - - {{'datakey.entity-field' | translate }} - - - - {{'datakey.timeseries' | translate }} - - - {{$chip.label}} -
    -
    :
    -
    - {{$chip.name}} - f({{$chip.name}}) -
    -
    - - edit - -
    -
    -
    - - - {{item}} - -
    -
    - entity.no-keys-found -
    -
    - entity.no-key-matching - - entity.create-new-key - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    -
    -
    - {{$chip.label}} -
    -
    :
    -
    - {{$chip.name}} - f({{$chip.name}}) -
    -
    - - edit - -
    -
    -
    -
    - -
    -
    -
    diff --git a/ui/src/app/components/datasource-func.directive.js b/ui/src/app/components/datasource-func.directive.js deleted file mode 100644 index 6a8b38fbcb..0000000000 --- a/ui/src/app/components/datasource-func.directive.js +++ /dev/null @@ -1,243 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './datasource-func.scss'; - -import 'md-color-picker'; -import tinycolor from 'tinycolor2'; -import $ from 'jquery'; -import thingsboardTypes from '../common/types.constant'; -import thingsboardUtils from '../common/utils.service'; -import thingsboardDatakeyConfigDialog from './datakey-config-dialog.controller'; - -/* eslint-disable import/no-unresolved, import/default */ - -import datasourceFuncTemplate from './datasource-func.tpl.html'; -import datakeyConfigDialogTemplate from './datakey-config-dialog.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/* eslint-disable angular/angularelement */ - -export default angular.module('thingsboard.directives.datasourceFunc', [thingsboardTypes, thingsboardUtils, thingsboardDatakeyConfigDialog]) - .directive('tbDatasourceFunc', DatasourceFunc) - .name; - -/*@ngInject*/ -function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document, $mdColorPicker, types, utils) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - var template = $templateCache.get(datasourceFuncTemplate); - element.html(template); - - scope.ngModelCtrl = ngModelCtrl; - scope.types = types; - - scope.functionTypes = utils.getPredefinedFunctionsList(); - scope.alarmFields = []; - for (var alarmField in types.alarmFields) { - scope.alarmFields.push(alarmField); - } - - scope.selectedDataKey = null; - scope.dataKeySearchText = null; - - scope.selectedAlarmDataKey = null; - scope.alarmDataKeySearchText = null; - - scope.updateValidity = function () { - if (ngModelCtrl.$viewValue) { - var value = ngModelCtrl.$viewValue; - var dataValid = angular.isDefined(value) && value != null; - ngModelCtrl.$setValidity('deviceData', dataValid); - if (dataValid) { - if (scope.optDataKeys) { - ngModelCtrl.$setValidity('datasourceKeys', true); - } else { - ngModelCtrl.$setValidity('datasourceKeys', - angular.isDefined(value.dataKeys) && - value.dataKeys != null && - value.dataKeys.length > 0); - } - } - } - }; - - scope.$watch('funcDataKeys', function () { - updateDataKeys(); - }, true); - - scope.$watch('alarmDataKeys', function () { - updateDataKeys(); - }, true); - - function updateDataKeys() { - if (ngModelCtrl.$viewValue) { - var dataKeys = []; - dataKeys = dataKeys.concat(scope.funcDataKeys); - dataKeys = dataKeys.concat(scope.alarmDataKeys); - if (ngModelCtrl.$viewValue.dataKeys != dataKeys) - { - ngModelCtrl.$setDirty(); - ngModelCtrl.$viewValue.dataKeys = dataKeys; - } - scope.updateValidity(); - } - } - - scope.$watch('datasourceName', function () { - if (ngModelCtrl.$viewValue) { - ngModelCtrl.$viewValue.name = scope.datasourceName; - scope.updateValidity(); - } - }); - - ngModelCtrl.$render = function () { - if (ngModelCtrl.$viewValue) { - var funcDataKeys = []; - var alarmDataKeys = []; - if (ngModelCtrl.$viewValue.dataKeys) { - for (var d=0;d 0 && ngModelCtrl.$viewValue.dataKeys.length >= scope.maxDataKeys ) { - return null; - } else { - return scope.generateDataKey({chip: chip, type: types.dataKeyType.function}); - } - }; - - scope.transformAlarmDataKeyChip = function (chip) { - return scope.generateDataKey({chip: chip, type: types.dataKeyType.alarm}); - }; - - scope.showColorPicker = function (event, dataKey) { - $mdColorPicker.show({ - value: dataKey.color, - defaultValue: '#fff', - random: tinycolor.random(), - clickOutsideToClose: false, - hasBackdrop: false, - multiple: true, - preserveScope: false, - - mdColorAlphaChannel: true, - mdColorSpectrum: true, - mdColorSliders: true, - mdColorGenericPalette: false, - mdColorMaterialPalette: true, - mdColorHistory: false, - mdColorDefaultTab: 2, - - $event: event - - }).then(function (color) { - dataKey.color = color; - ngModelCtrl.$setDirty(); - }); - } - - scope.editDataKey = function (event, dataKey) { - - $mdDialog.show({ - controller: 'DatakeyConfigDialogController', - controllerAs: 'vm', - templateUrl: datakeyConfigDialogTemplate, - locals: { - dataKey: angular.copy(dataKey), - dataKeySettingsSchema: scope.datakeySettingsSchema, - entityAlias: null, - aliasController: null - }, - parent: angular.element($document[0].body), - fullscreen: true, - targetEvent: event, - multiple: true, - onComplete: function () { - var w = angular.element($window); - w.triggerHandler('resize'); - } - }).then(function (newDataKey) { - if (newDataKey.type === types.dataKeyType.function) { - let index = scope.funcDataKeys.indexOf(dataKey); - scope.funcDataKeys[index] = newDataKey; - } else if (newDataKey.type === types.dataKeyType.alarm) { - let index = scope.alarmDataKeys.indexOf(dataKey); - scope.alarmDataKeys[index] = newDataKey; - } - ngModelCtrl.$setDirty(); - }, function () { - }); - }; - - scope.textIsNotEmpty = function(text) { - return (text && text != null && text.length > 0) ? true : false; - } - - scope.dataKeysSearch = function (dataKeySearchText) { - var targetKeys = scope.widgetType == types.widgetType.alarm.value ? scope.alarmFields : scope.functionTypes; - var dataKeys = dataKeySearchText ? targetKeys.filter( - scope.createFilterForDataKey(dataKeySearchText)) : targetKeys; - return dataKeys; - }; - - scope.createFilterForDataKey = function (query) { - var lowercaseQuery = angular.lowercase(query); - return function filterFn(dataKey) { - return (angular.lowercase(dataKey).indexOf(lowercaseQuery) === 0); - }; - }; - - scope.createKey = function (event, chipsId) { - var chipsChild = $(chipsId, element)[0].firstElementChild; - var el = angular.element(chipsChild); - var chipBuffer = el.scope().$mdChipsCtrl.getChipBuffer(); - event.preventDefault(); - event.stopPropagation(); - el.scope().$mdChipsCtrl.appendChip(chipBuffer.trim()); - el.scope().$mdChipsCtrl.resetChipBuffer(); - } - - $compile(element.contents())(scope); - } - - return { - restrict: "E", - require: "^ngModel", - scope: { - widgetType: '=', - maxDataKeys: '=', - optDataKeys: '=', - generateDataKey: '&', - datakeySettingsSchema: '=' - }, - link: linker - }; -} - -/* eslint-enable angular/angularelement */ diff --git a/ui/src/app/components/datasource-func.scss b/ui/src/app/components/datasource-func.scss deleted file mode 100644 index 4d28989820..0000000000 --- a/ui/src/app/components/datasource-func.scss +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "../../scss/constants"; - -.tb-datasource-func { - @media (min-width: $layout-breakpoint-sm) { - padding-left: 8px; - } - - md-input-container.tb-datasource-name { - .md-errors-spacer { - display: none; - } - } -} - -.tb-func-datakey-autocomplete, -.tb-alarm-datakey-autocomplete { - .tb-not-found { - display: block; - height: 48px; - line-height: 1.5; - - .tb-no-entries { - line-height: 48px; - } - } - - li { - height: auto !important; - white-space: normal !important; - } -} diff --git a/ui/src/app/components/datasource-func.tpl.html b/ui/src/app/components/datasource-func.tpl.html deleted file mode 100644 index 97629ef6e3..0000000000 --- a/ui/src/app/components/datasource-func.tpl.html +++ /dev/null @@ -1,147 +0,0 @@ - -
    - - - -
    - - - {{item}} - -
    -
    - device.no-keys-found -
    -
    - entity.no-key-matching - - entity.create-new-key - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    -
    -
    - {{$chip.label}} -
    -
    :
    -
    - {{$chip.name}} -
    -
    - - edit - -
    -
    -
    - - - {{item}} - -
    -
    - entity.no-keys-found -
    -
    - entity.no-key-matching - - entity.create-new-key - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    -
    -
    - {{$chip.label}} -
    -
    :
    -
    - {{$chip.name}} - f({{$chip.name}}) -
    -
    - - edit - -
    -
    -
    - -
    -
    -
    diff --git a/ui/src/app/components/datasource.directive.js b/ui/src/app/components/datasource.directive.js deleted file mode 100644 index 64670f81f4..0000000000 --- a/ui/src/app/components/datasource.directive.js +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './datasource.scss'; - -import thingsboardTypes from '../common/types.constant'; -import thingsboardDatasourceFunc from './datasource-func.directive' -import thingsboardDatasourceEntity from './datasource-entity.directive'; - -/* eslint-disable import/no-unresolved, import/default */ - -import datasourceTemplate from './datasource.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -export default angular.module('thingsboard.directives.datasource', [thingsboardTypes, thingsboardDatasourceFunc, thingsboardDatasourceEntity]) - .directive('tbDatasource', Datasource) - .name; - -/*@ngInject*/ -function Datasource($compile, $templateCache, utils, types) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - - var template = $templateCache.get(datasourceTemplate); - element.html(template); - - scope.types = types; - - if (scope.functionsOnly) { - scope.datasourceTypes = [types.datasourceType.function]; - } else{ - scope.datasourceTypes = [types.datasourceType.entity, types.datasourceType.function]; - } - - scope.updateView = function () { - if (!scope.model.dataKeys) { - scope.model.dataKeys = []; - } - ngModelCtrl.$setViewValue(scope.model); - } - - scope.$watch('model.type', function (newType, prevType) { - if (newType && prevType && newType != prevType) { - if (scope.widgetType == types.widgetType.alarm.value) { - scope.model.dataKeys = utils.getDefaultAlarmDataKeys(); - } else { - scope.model.dataKeys = []; - } - } - }); - - scope.$watch('model', function () { - scope.updateView(); - }, true); - - ngModelCtrl.$render = function () { - if (ngModelCtrl.$viewValue) { - scope.model = ngModelCtrl.$viewValue; - } else { - scope.model = {}; - } - }; - - $compile(element.contents())(scope); - } - - return { - restrict: "E", - require: "^ngModel", - scope: { - aliasController: '=', - maxDataKeys: '=', - optDataKeys: '=', - widgetType: '=', - functionsOnly: '=', - datakeySettingsSchema: '=', - generateDataKey: '&', - fetchEntityKeys: '&', - onCreateEntityAlias: '&' - }, - link: linker - }; -} diff --git a/ui/src/app/components/datasource.scss b/ui/src/app/components/datasource.scss deleted file mode 100644 index 8e73561cbd..0000000000 --- a/ui/src/app/components/datasource.scss +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-datasource { - #entity-autocomplete { - height: 30px; - margin-top: 18px; - - md-autocomplete-wrap { - height: 30px; - } - - input, - input:not(.md-input) { - height: 30px; - } - } -} - -@mixin tb-checkered-bg() { - background-color: #fff; - background-image: - linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd), - linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd); - background-position: 0 0, 4px 4px; - background-size: 8px 8px; -} - -.tb-color-preview { - position: relative; - width: 24px; - min-width: 24px; - height: 24px; - overflow: hidden; - content: ""; - border: 2px solid #fff; - border-radius: 50%; - box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .14), 0 2px 2px 0 rgba(0, 0, 0, .098), 0 1px 5px 0 rgba(0, 0, 0, .084); - - @include tb-checkered-bg(); - - .tb-color-result { - width: 100%; - height: 100%; - } -} - -.tb-attribute-chip { - .tb-chip-drag-handle { - cursor: move; - - ng-md-icon { - pointer-events: none; - } - } - - .tb-chip-label { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - .tb-chip-separator { - white-space: pre; - } -} diff --git a/ui/src/app/components/datasource.tpl.html b/ui/src/app/components/datasource.tpl.html deleted file mode 100644 index 65bfc766bc..0000000000 --- a/ui/src/app/components/datasource.tpl.html +++ /dev/null @@ -1,51 +0,0 @@ - -
    - - - - {{ datasourceType | translate }} - - - -
    - - - - -
    -
    diff --git a/ui/src/app/components/datetime-period.directive.js b/ui/src/app/components/datetime-period.directive.js deleted file mode 100644 index f17bf316b7..0000000000 --- a/ui/src/app/components/datetime-period.directive.js +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './datetime-period.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import datetimePeriodTemplate from './datetime-period.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -export default angular.module('thingsboard.directives.datetimePeriod', []) - .directive('tbDatetimePeriod', DatetimePeriod) - .name; - -/*@ngInject*/ -function DatetimePeriod($compile, $templateCache) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - - var template = $templateCache.get(datetimePeriodTemplate); - element.html(template); - - ngModelCtrl.$render = function () { - var date = new Date(); - scope.startDate = new Date( - date.getFullYear(), - date.getMonth(), - date.getDate() - 1); - scope.endDate = date; - if (ngModelCtrl.$viewValue) { - var value = ngModelCtrl.$viewValue; - scope.startDate = new Date(value.startTimeMs); - scope.endDate = new Date(value.endTimeMs); - } - } - - scope.updateMinMaxDates = function () { - scope.maxStartDate = angular.copy(new Date(scope.endDate.getTime() - 1000)); - scope.minEndDate = angular.copy(new Date(scope.startDate.getTime() + 1000)); - scope.maxEndDate = new Date(); - } - - scope.updateView = function () { - var value = null; - if (scope.startDate && scope.endDate) { - value = { - startTimeMs: scope.startDate.getTime(), - endTimeMs: scope.endDate.getTime() - }; - ngModelCtrl.$setValidity('datetimePeriod', true); - } else { - ngModelCtrl.$setValidity('datetimePeriod', !scope.required); - } - ngModelCtrl.$setViewValue(value); - } - - scope.$watch('required', function () { - scope.updateView(); - }); - - scope.$watch('startDate', function (newDate) { - if (newDate) { - if (newDate.getTime() > scope.maxStartDate) { - scope.startDate = angular.copy(scope.maxStartDate); - } - scope.updateMinMaxDates(); - } - scope.updateView(); - }); - - scope.$watch('endDate', function (newDate) { - if (newDate) { - if (newDate.getTime() < scope.minEndDate) { - scope.endDate = angular.copy(scope.minEndDate); - } else if (newDate.getTime() > scope.maxEndDate) { - scope.endDate = angular.copy(scope.maxEndDate); - } - scope.updateMinMaxDates(); - } - scope.updateView(); - }); - - $compile(element.contents())(scope); - - } - - return { - restrict: "E", - require: "^ngModel", - scope: { - required: '=ngRequired' - }, - link: linker - }; -} diff --git a/ui/src/app/components/datetime-period.scss b/ui/src/app/components/datetime-period.scss deleted file mode 100644 index 9bac9c3145..0000000000 --- a/ui/src/app/components/datetime-period.scss +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -tb-datetime-period { - md-input-container { - margin-bottom: 0; - - .md-errors-spacer { - min-height: 0; - } - } - - mdp-date-picker { - .md-input { - width: 150px !important; - } - } -} diff --git a/ui/src/app/components/datetime-period.tpl.html b/ui/src/app/components/datetime-period.tpl.html deleted file mode 100644 index 4518b0ef13..0000000000 --- a/ui/src/app/components/datetime-period.tpl.html +++ /dev/null @@ -1,31 +0,0 @@ - -
    -
    - - -
    -
    - - -
    -
    \ No newline at end of file diff --git a/ui/src/app/components/details-sidenav.directive.js b/ui/src/app/components/details-sidenav.directive.js deleted file mode 100644 index dba2eca356..0000000000 --- a/ui/src/app/components/details-sidenav.directive.js +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './details-sidenav.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import detailsSidenavTemplate from './details-sidenav.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -export default angular.module('thingsboard.directives.detailsSidenav', []) - .directive('tbDetailsSidenav', DetailsSidenav) - .name; - -/*@ngInject*/ -function DetailsSidenav($timeout, $mdUtil, $q, $animate) { - - var linker = function (scope, element, attrs) { - - if (angular.isUndefined(attrs.isReadOnly)) { - attrs.isReadOnly = false; - } - - if (angular.isUndefined(scope.headerHeightPx)) { - scope.headerHeightPx = 100; - } - - if (angular.isDefined(attrs.isAlwaysEdit) && attrs.isAlwaysEdit) { - scope.isEdit = true; - } - - var backdrop; - var previousContainerStyles; - - if (Object.prototype.hasOwnProperty.call(attrs,'tbEnableBackdrop')) { - backdrop = $mdUtil.createBackdrop(scope, "md-sidenav-backdrop md-opaque ng-enter"); - element.on('$destroy', function() { - backdrop && backdrop.remove(); - }); - scope.$on('$destroy', function(){ - backdrop && backdrop.remove(); - }); - scope.$watch('isOpen', updateIsOpen); - } - - function updateIsOpen(isOpen) { - backdrop[isOpen ? 'on' : 'off']('click', (ev)=>{ - ev.preventDefault(); - scope.isOpen = false; - scope.$apply(); - }); - var parent = element.parent(); - var restorePositioning = updateContainerPositions(parent, isOpen); - - return $q.all([ - isOpen && backdrop ? $animate.enter(backdrop, parent) : backdrop ? - $animate.leave(backdrop) : $q.when(true) - ]).then(function() { - restorePositioning && restorePositioning(); - }); - } - - function updateContainerPositions(parent, willOpen) { - var drawerEl = element[0]; - var scrollTop = parent[0].scrollTop; - if (willOpen && scrollTop) { - previousContainerStyles = { - top: drawerEl.style.top, - bottom: drawerEl.style.bottom, - height: drawerEl.style.height - }; - var positionStyle = { - top: scrollTop + 'px', - bottom: 'auto', - height: parent[0].clientHeight + 'px' - }; - backdrop.css(positionStyle); - } - if (!willOpen && previousContainerStyles) { - return function() { - backdrop[0].style.top = null; - backdrop[0].style.bottom = null; - backdrop[0].style.height = null; - previousContainerStyles = null; - }; - } - } - - scope.toggleDetailsEditMode = function () { - if (!scope.isAlwaysEdit) { - if (!scope.isEdit) { - scope.isEdit = true; - } else { - scope.isEdit = false; - } - } - $timeout(function () { - scope.onToggleDetailsEditMode(); - }); - }; - - scope.detailsApply = function () { - $timeout(function () { - scope.onApplyDetails(); - }); - } - - scope.closeDetails = function () { - scope.isOpen = false; - $timeout(function () { - scope.onCloseDetails(); - }); - }; - } - - return { - restrict: "E", - transclude: { - headerPane: '?headerPane', - detailsButtons: '?detailsButtons' - }, - scope: { - headerTitle: '@', - headerSubtitle: '@', - headerHeightPx: '@', - isReadOnly: '=', - isOpen: '=', - isEdit: '=?', - isAlwaysEdit: '=?', - theForm: '=', - onCloseDetails: '&', - onToggleDetailsEditMode: '&', - onApplyDetails: '&' - }, - link: linker, - templateUrl: detailsSidenavTemplate - }; -} \ No newline at end of file diff --git a/ui/src/app/components/details-sidenav.scss b/ui/src/app/components/details-sidenav.scss deleted file mode 100644 index 414f123069..0000000000 --- a/ui/src/app/components/details-sidenav.scss +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "../../scss/constants"; - -.tb-details-title { - width: inherit; - margin: 20px 8px 0 0; - overflow: hidden; - font-size: 1rem; - font-weight: 400; - text-overflow: ellipsis; - text-transform: uppercase; - white-space: nowrap; - - @media (min-width: $layout-breakpoint-gt-sm) { - font-size: 1.6rem; - } -} - -.tb-details-subtitle { - width: inherit; - margin: 10px 0; - overflow: hidden; - font-size: 1rem; - text-overflow: ellipsis; - white-space: nowrap; - opacity: .8; -} - -md-sidenav.tb-sidenav-details { - .md-toolbar-tools { - height: 100%; - min-height: 100px; - max-height: 120px; - } - z-index: 59 !important; - width: 100% !important; - max-width: 100% !important; - - @media (min-width: $layout-breakpoint-gt-sm) { - width: 80% !important; - } - - @media (min-width: $layout-breakpoint-gt-md) { - width: 65% !important; - } - - tb-dashboard { - md-content { - background-color: $primary-hue-3; - } - } -} diff --git a/ui/src/app/components/details-sidenav.tpl.html b/ui/src/app/components/details-sidenav.tpl.html deleted file mode 100644 index dee66b0f40..0000000000 --- a/ui/src/app/components/details-sidenav.tpl.html +++ /dev/null @@ -1,61 +0,0 @@ - - -
    - -
    -
    - {{headerTitle}} - {{headerSubtitle}} - -
    -
    - - close - -
    -
    - - - {{ 'action.apply-changes' | translate }} - - - - - - {{ (isAlwaysEdit ? 'action.decline-changes' : 'details.toggle-edit-mode') | translate }} - - - -
    -
    -
    - -
    -
    -
    \ No newline at end of file diff --git a/ui/src/app/components/entity-alias-select.directive.js b/ui/src/app/components/entity-alias-select.directive.js deleted file mode 100644 index f1a0b065f3..0000000000 --- a/ui/src/app/components/entity-alias-select.directive.js +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import $ from 'jquery'; - -import './entity-alias-select.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import entityAliasSelectTemplate from './entity-alias-select.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - - -/* eslint-disable angular/angularelement */ - -export default angular.module('thingsboard.directives.entityAliasSelect', []) - .directive('tbEntityAliasSelect', EntityAliasSelect) - .name; - -/*@ngInject*/ -function EntityAliasSelect($compile, $templateCache, $mdConstant, entityService) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - var template = $templateCache.get(entityAliasSelectTemplate); - element.html(template); - - scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false; - - scope.ngModelCtrl = ngModelCtrl; - scope.entityAliasList = []; - scope.entityAlias = null; - - scope.updateValidity = function () { - var value = ngModelCtrl.$viewValue; - var valid = angular.isDefined(value) && value != null || !scope.tbRequired; - ngModelCtrl.$setValidity('entityAlias', valid); - }; - - scope.$watch('aliasController', function () { - scope.entityAliasList = []; - var entityAliases = scope.aliasController.getEntityAliases(); - for (var aliasId in entityAliases) { - if (scope.allowedEntityTypes) { - if (!entityService.filterAliasByEntityTypes(entityAliases[aliasId], scope.allowedEntityTypes)) { - continue; - } - } - scope.entityAliasList.push(entityAliases[aliasId]); - } - }); - - scope.$watch('entityAlias', function () { - scope.updateView(); - }); - - scope.entityAliasSearch = function (entityAliasSearchText) { - return entityAliasSearchText ? scope.entityAliasList.filter( - scope.createFilterForEntityAlias(entityAliasSearchText)) : scope.entityAliasList; - }; - - scope.createFilterForEntityAlias = function (query) { - var lowercaseQuery = angular.lowercase(query); - return function filterFn(entityAlias) { - return (angular.lowercase(entityAlias.alias).indexOf(lowercaseQuery) === 0); - }; - }; - - scope.updateView = function () { - ngModelCtrl.$setViewValue(scope.entityAlias); - scope.updateValidity(); - } - - ngModelCtrl.$render = function () { - if (ngModelCtrl.$viewValue) { - scope.entityAlias = ngModelCtrl.$viewValue; - } - } - - scope.textIsNotEmpty = function(text) { - return (text && text != null && text.length > 0) ? true : false; - } - - scope.entityAliasEnter = function($event) { - if ($event.keyCode === $mdConstant.KEY_CODE.ENTER) { - $event.preventDefault(); - if (!scope.entityAlias) { - var found = scope.entityAliasSearch(scope.entityAliasSearchText); - found = found.length > 0; - if (!found) { - scope.createEntityAlias($event, scope.entityAliasSearchText); - } - } - } - } - - scope.createEntityAlias = function (event, alias) { - var autoChild = $('#entity-autocomplete', element)[0].firstElementChild; - var el = angular.element(autoChild); - el.scope().$mdAutocompleteCtrl.hidden = true; - el.scope().$mdAutocompleteCtrl.hasNotFound = false; - event.preventDefault(); - var promise = scope.onCreateEntityAlias({event: event, alias: alias, allowedEntityTypes: scope.allowedEntityTypes}); - if (promise) { - promise.then( - function success(newAlias) { - el.scope().$mdAutocompleteCtrl.hasNotFound = true; - if (newAlias) { - scope.entityAliasList.push(newAlias); - scope.entityAlias = newAlias; - } - }, - function fail() { - el.scope().$mdAutocompleteCtrl.hasNotFound = true; - } - ); - } else { - el.scope().$mdAutocompleteCtrl.hasNotFound = true; - } - }; - - $compile(element.contents())(scope); - } - - return { - restrict: "E", - require: "^ngModel", - link: linker, - scope: { - tbRequired: '=?', - aliasController: '=', - allowedEntityTypes: '=?', - onCreateEntityAlias: '&' - } - }; -} - -/* eslint-enable angular/angularelement */ diff --git a/ui/src/app/components/entity-alias-select.scss b/ui/src/app/components/entity-alias-select.scss deleted file mode 100644 index 544602ba32..0000000000 --- a/ui/src/app/components/entity-alias-select.scss +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-entity-alias-autocomplete { - .tb-not-found { - display: block; - height: 48px; - line-height: 1.5; - - .tb-no-entries { - line-height: 48px; - } - } - - li { - height: auto !important; - white-space: normal !important; - } -} diff --git a/ui/src/app/components/entity-alias-select.tpl.html b/ui/src/app/components/entity-alias-select.tpl.html deleted file mode 100644 index 5192f3d758..0000000000 --- a/ui/src/app/components/entity-alias-select.tpl.html +++ /dev/null @@ -1,53 +0,0 @@ - -
    - - - {{item.alias}} - - -
    -
    - entity.no-aliases-found -
    -
    - entity.no-alias-matching - - entity.create-new-alias - -
    -
    -
    -
    - -
    diff --git a/ui/src/app/components/expand-fullscreen.directive.js b/ui/src/app/components/expand-fullscreen.directive.js deleted file mode 100644 index 4aafb5b69a..0000000000 --- a/ui/src/app/components/expand-fullscreen.directive.js +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './expand-fullscreen.scss'; - -import $ from 'jquery'; - -export default angular.module('thingsboard.directives.expandFullscreen', []) - .directive('tbExpandFullscreen', ExpandFullscreen) - .name; - -/* eslint-disable angular/angularelement */ - -/*@ngInject*/ -function ExpandFullscreen($compile, $document, $timeout) { - - var uniqueId = 1; - var linker = function (scope, element, attrs) { - - scope.body = angular.element($document.find('body').eq(0)); - scope.fullscreenParentId = 'fullscreen-parent' + uniqueId; - scope.fullscreenParent = $('#' + scope.fullscreenParentId, scope.body)[0]; - if (!scope.fullscreenParent) { - uniqueId++; - var fullscreenParent = angular.element('
    '); - scope.body.append(fullscreenParent); - scope.fullscreenParent = $('#' + scope.fullscreenParentId, scope.body)[0]; - scope.fullscreenParent = angular.element(scope.fullscreenParent); - scope.fullscreenParent.css('display', 'none'); - } else { - scope.fullscreenParent = angular.element(scope.fullscreenParent); - } - - scope.$on('$destroy', function () { - scope.fullscreenParent.remove(); - }); - - scope.elementParent = null; - scope.expanded = false; - scope.fullscreenZindex = scope.fullscreenZindex(); - - if (!scope.fullscreenZindex) { - scope.fullscreenZindex = '70'; - } - - scope.$watch('expanded', function (newExpanded, prevExpanded) { - if (newExpanded != prevExpanded) { - if (scope.expanded) { - scope.elementParent = element.parent(); - element.detach(); - if (scope.backgroundStyle) { - scope.fullscreenParent.attr("ng-style","backgroundStyle"); - $compile(scope.fullscreenParent)(scope); - } - scope.fullscreenParent.append(element); - scope.fullscreenParent.css('display', ''); - scope.fullscreenParent.css('z-index', scope.fullscreenZindex); - element.addClass('tb-fullscreen'); - } else { - if (scope.elementParent) { - element.detach(); - scope.elementParent.append(element); - scope.elementParent = null; - } - element.removeClass('tb-fullscreen'); - scope.fullscreenParent.css('display', 'none'); - scope.fullscreenParent.css('z-index', ''); - } - if (scope.onFullscreenChanged) { - scope.onFullscreenChanged({expanded: scope.expanded}); - } - } - }); - - scope.$watch(function () { - return scope.expand(); - }, function (newExpanded) { - scope.expanded = newExpanded; - }); - - scope.toggleExpand = function ($event) { - if ($event) { - $event.stopPropagation(); - } - scope.expanded = !scope.expanded; - } - - var buttonSize; - if (attrs.expandButtonSize) { - buttonSize = attrs.expandButtonSize; - } - - var tooltipDirection = angular.isDefined(attrs.expandTooltipDirection) ? attrs.expandTooltipDirection : 'top'; - - var html = '' + - '{{(expanded ? \'fullscreen.exit\' : \'fullscreen.expand\') | translate}}' + - '' + - '' + - ''; - - if (attrs.expandButtonId) { - $timeout(function() { - var expandButton = $('#' + attrs.expandButtonId, element)[0]; - renderExpandButton(expandButton); - }); - } else { - renderExpandButton(); - } - - function renderExpandButton(expandButton) { - if (expandButton) { - expandButton = angular.element(expandButton); - if (scope.hideExpandButton()) { - expandButton.remove(); - } else { - expandButton.attr('md-ink-ripple', 'false'); - expandButton.append(html); - - $compile(expandButton.contents())(scope); - - expandButton.on("click", scope.toggleExpand); - } - } else if (!scope.hideExpandButton()) { - var button = angular.element('' + - html + - ''); - - $compile(button)(scope); - - element.prepend(button); - } - } - } - - return { - restrict: "A", - link: linker, - scope: { - expand: "&tbExpandFullscreen", - hideExpandButton: "&hideExpandButton", - onFullscreenChanged: "&onFullscreenChanged", - fullscreenZindex: "&fullscreenZindex", - backgroundStyle: "=?fullscreenBackgroundStyle" - } - }; -} - -/* eslint-enable angular/angularelement */ diff --git a/ui/src/app/components/expand-fullscreen.scss b/ui/src/app/components/expand-fullscreen.scss deleted file mode 100644 index 843d616cea..0000000000 --- a/ui/src/app/components/expand-fullscreen.scss +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "../../scss/constants"; - -.tb-fullscreen { - position: fixed !important; - top: 0; - left: 0; - width: 100% !important; - height: 100% !important; - - section.header-buttons { - top: 25px; - } -} - -.tb-fullscreen-parent { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: $gray; -} - -.md-button.tb-fullscreen-button-style, -.tb-fullscreen-button-style { - background: #ccc; - opacity: .85; - - ng-md-icon { - color: #666; - } -} - -.md-button.tb-fullscreen-button-pos, -.tb-fullscreen-button-pos { - position: absolute; - top: 10px; - right: 10px; -} diff --git a/ui/src/app/components/finish-render.directive.js b/ui/src/app/components/finish-render.directive.js deleted file mode 100644 index 37add22505..0000000000 --- a/ui/src/app/components/finish-render.directive.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default angular.module('thingsboard.directives.finishRender', []) - .directive('tbOnFinishRender', OnFinishRender) - .name; - -/*@ngInject*/ -function OnFinishRender($timeout) { - return { - restrict: 'A', - link: function (scope, element, attr) { - if (scope.$last === true) { - $timeout(function () { - scope.$emit(attr.tbOnFinishRender); - }); - } - } - }; -} diff --git a/ui/src/app/components/gateway/gateway-config-dialog.tpl.html b/ui/src/app/components/gateway/gateway-config-dialog.tpl.html deleted file mode 100644 index 87195ac584..0000000000 --- a/ui/src/app/components/gateway/gateway-config-dialog.tpl.html +++ /dev/null @@ -1,75 +0,0 @@ - - -
    - -
    -

    - gateway.title-connectors-json -

    - - - - -
    -
    - -
    -
    -
    - - - - {{'gateway.tidy'|translate}} - {{'gateway.tidy-tip' | translate }} - - -
    -
    -
    -
    -
    - -
    -
    -
    - - - {{'action.save'|translate}} - - - {{'action.cancel'|translate }} - - -
    -
    diff --git a/ui/src/app/components/gateway/gateway-config-select.directive.js b/ui/src/app/components/gateway/gateway-config-select.directive.js deleted file mode 100644 index 79ff454475..0000000000 --- a/ui/src/app/components/gateway/gateway-config-select.directive.js +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './gateway-config-select.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import gatewaySelectTemplate from './gateway-config-select.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - - -/* eslint-disable angular/angularelement */ - -export default angular.module('thingsboard.directives.gatewayConfigSelect', []) - .directive('tbGatewayConfigSelect', GatewayConfigSelect) - .name; - -/*@ngInject*/ -function GatewayConfigSelect($compile, $templateCache, $mdConstant, $translate, $mdDialog) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - const template = $templateCache.get(gatewaySelectTemplate); - element.html(template); - - scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false; - scope.gateway = null; - scope.gatewaySearchText = ''; - - scope.updateValidity = function () { - var value = ngModelCtrl.$viewValue; - var valid = angular.isDefined(value) && value != null || !scope.tbRequired; - ngModelCtrl.$setValidity('gateway', valid); - }; - - function startWatchers() { - scope.$watch('gateway', function (newVal, prevVal) { - if (!angular.equals(newVal, prevVal) && newVal !== null) { - scope.updateView(); - } - }); - } - - scope.gatewayNameSearch = function (gatewaySearchText) { - return gatewaySearchText ? scope.gatewayList.filter( - scope.createFilterForGatewayName(gatewaySearchText)) : scope.gatewayList; - }; - - scope.createFilterForGatewayName = function (query) { - var lowercaseQuery = query.toLowerCase(); - return function filterFn(device) { - return (device.name.toLowerCase().indexOf(lowercaseQuery) === 0); - }; - }; - - scope.updateView = function () { - ngModelCtrl.$setViewValue(scope.gateway); - scope.updateValidity(); - scope.getAccessToken(scope.gateway.id); - }; - - ngModelCtrl.$render = function () { - if (ngModelCtrl.$viewValue) { - scope.gateway = ngModelCtrl.$viewValue; - startWatchers(); - } - }; - - scope.textIsEmpty = function (str) { - return (!str || 0 === str.length); - }; - - scope.gatewayNameEnter = function ($event) { - if ($event.keyCode === $mdConstant.KEY_CODE.ENTER) { - $event.preventDefault(); - let indexRes = scope.gatewayList.findIndex((element) => element.key === scope.gatewaySearchText); - if (indexRes === -1) { - scope.createNewGatewayDialog($event, scope.gatewaySearchText); - } - } - }; - - scope.createNewGatewayDialog = function ($event, deviceName) { - if ($event) { - $event.stopPropagation(); - } - var title = $translate.instant('gateway.create-new-gateway'); - var content = $translate.instant('gateway.create-new-gateway-text', {gatewayName: deviceName}); - var confirm = $mdDialog.confirm() - .targetEvent($event) - .title(title) - .htmlContent(content) - .ariaLabel(title) - .cancel($translate.instant('action.no')) - .ok($translate.instant('action.yes')); - $mdDialog.show(confirm).then( - () => { - let deviceObj = { - name: deviceName, - type: "Gateway", - additionalInfo: { - gateway: true - } - }; - scope.createDevice(deviceObj); - }, - () => { - scope.gatewaySearchText = ""; - } - ); - }; - $compile(element.contents())(scope); - }; - - return { - restrict: "E", - require: "^ngModel", - link: linker, - scope: { - tbRequired: '=?', - gatewayList: '=?', - getAccessToken: '=', - createDevice: '=', - theForm: '=' - } - }; -} - -/* eslint-enable angular/angularelement */ diff --git a/ui/src/app/components/gateway/gateway-config-select.scss b/ui/src/app/components/gateway/gateway-config-select.scss deleted file mode 100644 index da2e7ba432..0000000000 --- a/ui/src/app/components/gateway/gateway-config-select.scss +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-gateway-autocomplete { - .tb-not-found { - line-height: 1.5; - white-space: normal; - - .tb-no-gateway { - line-height: 48px; - } - } -} - -.tb-gateway-autocomplete-container.md-virtual-repeat-container.md-autocomplete-suggestions-container{ - z-index: 70; -} diff --git a/ui/src/app/components/gateway/gateway-config-select.tpl.html b/ui/src/app/components/gateway/gateway-config-select.tpl.html deleted file mode 100644 index 9a7f6ed655..0000000000 --- a/ui/src/app/components/gateway/gateway-config-select.tpl.html +++ /dev/null @@ -1,53 +0,0 @@ - -
    - - - {{item.name}} - - -
    -
    - gateway.no-gateway-found -
    -
    - gateway.no-gateway-matching - gateway.create-new-gateway -
    -
    -
    -
    -
    gateway.gateway-name-required
    -
    -
    -
    diff --git a/ui/src/app/components/gateway/gateway-config.directive.js b/ui/src/app/components/gateway/gateway-config.directive.js deleted file mode 100644 index 19e7f554b9..0000000000 --- a/ui/src/app/components/gateway/gateway-config.directive.js +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './gateway-config.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import gatewayConfigTemplate from './gateway-config.tpl.html'; -import gatewayConfigDialogTemplate from './gateway-config-dialog.tpl.html'; -import beautify from "js-beautify"; - -/* eslint-enable import/no-unresolved, import/default */ -const js_beautify = beautify.js; - -export default angular.module('thingsboard.directives.gatewayConfig', []) - .directive('tbGatewayConfig', GatewayConfig) - .name; - -/*@ngInject*/ -function GatewayConfig() { - return { - restrict: "E", - scope: true, - bindToController: { - disabled: '=ngDisabled', - gatewayConfig: '=', - changeAlignment: '=', - theForm: '=', - isReadOnly: '=' - }, - controller: GatewayConfigController, - controllerAs: 'vm', - templateUrl: gatewayConfigTemplate - }; -} - -/*@ngInject*/ -function GatewayConfigController($scope, $document, $mdDialog, $mdUtil, $window, types) { - let vm = this; - vm.types = types; - - vm.removeConnector = (index) => { - if (index > -1) { - vm.gatewayConfig.splice(index, 1); - } - }; - - vm.addNewConnector = () => { - vm.gatewayConfig.push({ - enabled: false, - configType: '', - config: {}, - name: '' - }); - }; - - vm.openConfigDialog = ($event, index, config, typeName) => { - if ($event) { - $event.stopPropagation(); - } - $mdDialog.show({ - controller: GatewayDialogController, - controllerAs: 'vm', - templateUrl: gatewayConfigDialogTemplate, - parent: angular.element($document[0].body), - locals: { - config: config, - typeName: typeName - }, - targetEvent: $event, - fullscreen: true, - multiple: true, - }).then(function (config) { - if (config && index > -1) { - console.log(config); //eslint-disable-line - if (!angular.equals(vm.gatewayConfig[index].config, config)) { - $scope.gatewayConfiguration.$setDirty(); - } - vm.gatewayConfig[index].config = config; - } - }); - - }; - - vm.changeConnectorType = (connector) => { - for (let gatewayConfigTypeKey in types.gatewayConfigType) { - if (types.gatewayConfigType[gatewayConfigTypeKey].value === connector.configType) { - if (!connector.name) { - connector.name = generateConnectorName(types.gatewayConfigType[gatewayConfigTypeKey].name, 0); - break; - } - } - } - }; - - vm.changeConnectorName = (connector, currentConnectorIndex) => { - connector.name = validateConnectorName(connector.name, 0, currentConnectorIndex); - }; - - function generateConnectorName(name, index) { - let newKeyName = index ? name + index : name; - let indexRes = vm.gatewayConfig.findIndex((element) => element.name === newKeyName); - return indexRes === -1 ? newKeyName : generateConnectorName(name, ++index); - } - - function validateConnectorName(name, index, currentConnectorIndex) { - for (let i = 0; i < vm.gatewayConfig.length; i++) { - let nameEq = (index === 0) ? name : name + index; - if (i !== currentConnectorIndex && vm.gatewayConfig[i].name === nameEq) { - index++; - validateConnectorName(name, index, currentConnectorIndex); - } - } - return (index === 0) ? name : name + index; - } - - vm.validateJSON = (config) => { - return angular.equals({}, config); - }; -} - -/*@ngInject*/ -function GatewayDialogController($scope, $mdDialog, $document, $window, config, typeName) { - let vm = this; - vm.config = js_beautify(angular.toJson(config), {indent_size: 4}); - vm.typeName = typeName; - vm.configAreaOptions = { - useWrapMode: true, - mode: 'json', - advanced: { - enableSnippets: true, - enableBasicAutocompletion: true, - enableLiveAutocompletion: true - }, - onLoad: function (_ace) { - _ace.$blockScrolling = 1; - } - }; - - vm.validateConfig = (model, editorName) => { - if (model && model.length) { - try { - angular.fromJson(model); - $scope.theForm[editorName].$setValidity('config', true); - } catch (e) { - $scope.theForm[editorName].$setValidity('config', false); - } - } - }; - - vm.save = () => { - $mdDialog.hide(angular.fromJson(vm.config)); - }; - - vm.cancel = () => { - $mdDialog.hide(); - }; - - vm.beautifyJson = () => { - vm.config = js_beautify(vm.config, {indent_size: 4}); - }; -} - diff --git a/ui/src/app/components/gateway/gateway-config.scss b/ui/src/app/components/gateway/gateway-config.scss deleted file mode 100644 index 45d9532927..0000000000 --- a/ui/src/app/components/gateway/gateway-config.scss +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.gateway-config { - span.no-data-found { - position: relative; - display: flex; - height: 40px; - text-transform: uppercase; - - &.disabled { - color: rgba(0, 0, 0, .38); - } - } - - .gateway-config-row{ - md-input-container{ - margin-bottom: 0; - } - - &.gateway-config-row-vertical { - flex-direction: column; - } - } - - .action-buttons.gateway-config-row-vertical { - flex-direction: column; - justify-content: space-evenly; - } -} - -.gateway-config-dialog{ - .md-button.tidy{ - min-width: 32px; - min-height: 15px; - padding: 4px; - margin: 0 5px 0 0; - font-size: .8rem; - line-height: 15px; - color: #7b7b7b; - background: rgba(220, 220, 220, .35); - } - - .tb-json-toolbar{ - height: 40px; - } -} - -.tb-json-panel { - height: calc(100% - 80px); - margin-left: 15px; - border: 1px solid #c0c0c0; - - .tb-json-input { - width: 100%; - min-width: 400px; - height: 100%; - - &:not(.fill-height) { - min-height: 200px; - } - } -} - -@media (max-width: 425px){ - .gateway-config-dialog{ - .tb-json-panel { - .tb-json-input { - min-width: 200px; - } - } - } -} diff --git a/ui/src/app/components/gateway/gateway-config.tpl.html b/ui/src/app/components/gateway/gateway-config.tpl.html deleted file mode 100644 index e4525b2bf2..0000000000 --- a/ui/src/app/components/gateway/gateway-config.tpl.html +++ /dev/null @@ -1,81 +0,0 @@ - -
    -
    -
    - - -
    -
    - - - - - {{configType.value}} - - -
    -
    gateway.connector-type-required
    -
    -
    - - -
    -
    gateway.connector-name-required
    -
    -
    -
    -
    - - more_horiz - - {{ 'gateway.update-config' | translate }} - - - - close - - {{ 'gateway.delete' | translate }} - - -
    -
    - {{'gateway.no-connectors'}} -
    - - - {{ 'gateway.connector-add' | translate }} - - action.add - -
    -
    diff --git a/ui/src/app/components/gateway/gateway-form.directive.js b/ui/src/app/components/gateway/gateway-form.directive.js deleted file mode 100644 index 436bc6bcd5..0000000000 --- a/ui/src/app/components/gateway/gateway-form.directive.js +++ /dev/null @@ -1,493 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './gateway-form.scss'; -/* eslint-disable import/no-unresolved, import/default */ - -import gatewayFormTemplate from './gateway-form.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -export default angular.module('thingsboard.directives.gatewayForm', []) - .directive('tbGatewayForm', GatewayForm) - .name; - -/*@ngInject*/ -function GatewayForm() { - return { - restrict: "E", - scope: true, - bindToController: { - formId: '=', - ctx: '=', - isStateForm: '=', - deviceName: '=', - isReadOnly: '=', - isState: '=' - }, - controller: GatewayFormController, - controllerAs: 'vm', - templateUrl: gatewayFormTemplate, - }; -} - -/*@ngInject*/ -function GatewayFormController($scope, $injector, $document, $mdExpansionPanel, toast, importExport, attributeService, deviceService, userService, $mdDialog, $mdUtil, types, $window, $q, entityService, utils, $translate) { - let vm = this; - const currentConfigurationAttribute = "current_configuration"; - const configurationDraftsAttribute = "configuration_drafts"; - const configurationAttribute = "configuration"; - const remoteLoggingLevelAttribute = "RemoteLoggingLevel"; - - const templateLogsConfig = '[loggers]}}keys=root, service, connector, converter, tb_connection, storage, extension}}[handlers]}}keys=consoleHandler, serviceHandler, connectorHandler, converterHandler, tb_connectionHandler, storageHandler, extensionHandler}}[formatters]}}keys=LogFormatter}}[logger_root]}}level=ERROR}}handlers=consoleHandler}}[logger_connector]}}level={ERROR}}}handlers=connectorHandler}}formatter=LogFormatter}}qualname=connector}}[logger_storage]}}level={ERROR}}}handlers=storageHandler}}formatter=LogFormatter}}qualname=storage}}[logger_tb_connection]}}level={ERROR}}}handlers=tb_connectionHandler}}formatter=LogFormatter}}qualname=tb_connection}}[logger_service]}}level={ERROR}}}handlers=serviceHandler}}formatter=LogFormatter}}qualname=service}}[logger_converter]}}level={ERROR}}}handlers=converterHandler}}formatter=LogFormatter}}qualname=converter}}[logger_extension]}}level={ERROR}}}handlers=connectorHandler}}formatter=LogFormatter}}qualname=extension}}[handler_consoleHandler]}}class=StreamHandler}}level={ERROR}}}formatter=LogFormatter}}args=(sys.stdout,)}}[handler_connectorHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}connector.log", "d", 1, 7,)}}[handler_storageHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}storage.log", "d", 1, 7,)}}[handler_serviceHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}service.log", "d", 1, 7,)}}[handler_converterHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}converter.log", "d", 1, 3,)}}[handler_extensionHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}extension.log", "d", 1, 3,)}}[handler_tb_connectionHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}tb_connection.log", "d", 1, 3,)}}[formatter_LogFormatter]}}format="%(asctime)s - %(levelname)s - [%(filename)s] - %(module)s - %(lineno)d - %(message)s" }}datefmt="%Y-%m-%d %H:%M:%S"'; - - vm.types = types; - vm.deviceNameForm = (vm.deviceName) ? vm.deviceName : null; - vm.idForm = Math.random().toString(36).replace(/^0\.[0-9]*/, ''); - vm.configurations = { - gateway: '', - host: $document[0].domain, - port: 1883, - remoteConfiguration: true, - accessToken: '', - storageType: "memoryStorage", - readRecordsCount: 100, - maxRecordsCount: 10000, - dataFolderPath: './data/', - maxFilesCount: 5, - securityType: "accessToken", - caCertPath: '/etc/thingsboard-gateway/ca.pem', - privateKeyPath: '/etc/thingsboard-gateway/privateKey.pem', - certPath: '/etc/thingsboard-gateway/certificate.pem', - connectors: [], - remoteLoggingLevel: "DEBUG", - remoteLoggingPathToLogs: './logs/' - }; - - let archiveFileName = ''; - let gatewayNameExists = ''; - let successfulSaved = ''; - vm.securityTypes = [{ - name: 'gateway.security-types.access-token', - value: 'accessToken' - }, { - name: 'gateway.security-types.tls', - value: 'tls' - }]; - - vm.storageTypes = [{ - name: 'gateway.storage-types.memory-storage', - value: 'memoryStorage' - }, { - name: 'gateway.storage-types.file-storage', - value: 'fileStorage' - }]; - - $scope.$watch('vm.ctx', function () { - if (vm.ctx) { - vm.isStateForm = $scope.isStateForm; - vm.settings = vm.ctx.settings; - vm.widgetConfig = vm.ctx.widgetConfig; - if (vm.ctx.datasources && vm.ctx.datasources.length) { - vm.deviceNameForm = vm.ctx.datasources[0].name; - } - } - initializeConfig(); - }); - - - $scope.$on('gateway-form-resize', function (event, formId) { - if (vm.formId == formId) { - updateWidgetDisplaying(); - } - }); - - function updateWidgetDisplaying() { - if (vm.ctx) { - vm.changeAlignment = (vm.ctx.$container[0].offsetWidth <= 425); - } - } - - function initWidgetSettings() { - let widgetTitle; - if (vm.settings) { - vm.isReadOnlyForm = (vm.settings.readOnly) ? vm.settings.readOnly : false; - if (vm.settings.gatewayTitle && vm.settings.gatewayTitle.length) { - widgetTitle = utils.customTranslation(vm.settings.gatewayTitle, vm.settings.gatewayTitle); - } - } else { - vm.isReadOnlyForm = false; - widgetTitle = $translate.instant('gateway.gateway'); - } - if (vm.ctx) { - vm.ctx.widgetTitle = widgetTitle; - } - - archiveFileName = vm.settings && vm.settings.archiveFileName && vm.settings.archiveFileName.length ? vm.settings.archiveFileName : 'gatewayConfiguration'; - if (vm.settings) { - gatewayNameExists = utils.customTranslation(vm.settings.deviceNameExist, vm.settings.deviceNameExist) || $translate.instant('gateway.gateway-exists'); - successfulSaved = utils.customTranslation(vm.settings.successfulSave, vm.settings.successfulSave) || $translate.instant('gateway.gateway-saved'); - } else { - gatewayNameExists = $translate.instant('gateway.gateway-exists'); - successfulSaved = $translate.instant('gateway.gateway-saved'); - } - } - - function initializeConfig() { - updateWidgetDisplaying(); - initWidgetSettings(); - getGatewaysList(true); - } - - - vm.getAccessToken = (deviceId) => { - if (deviceId.id) { - getDeviceCredentials(deviceId.id); - } - }; - - vm.collapsePanel = function (panelId) { - $mdExpansionPanel(panelId).collapse(); - }; - - function getDeviceCredentials(deviceId) { - return deviceService.getDeviceCredentials(deviceId).then( - (deviceCredentials) => { - vm.configurations.accessToken = deviceCredentials.credentialsId; - getAttributes(); - } - ); - } - - vm.createDevice = (deviceObj) => { - deviceService.findByName(deviceObj.name, {ignoreErrors: true}) - .then( - function () { - toast.showError(gatewayNameExists, angular.element('.gateway-form'), 'top left'); - }, - function () { - if (vm.settings.gatewayType && vm.settings.gatewayType.length) { - deviceObj.type = vm.settings.gatewayType; - } - deviceService.saveDevice(deviceObj).then( - (device) => { - getDeviceCredentials(device.id.id).then(() => { - getGatewaysList(); - }); - } - ); - }); - }; - - vm.saveAttributeConfig = () => { - $q.all([ - saveAttribute(configurationAttribute, $window.btoa(angular.toJson(getGatewayConfigJSON())), types.attributesScope.shared.value), - saveAttribute(configurationDraftsAttribute, $window.btoa(angular.toJson(getDraftConnectorJSON())), types.attributesScope.server.value), - saveAttribute(remoteLoggingLevelAttribute, vm.configurations.remoteLoggingLevel.toUpperCase(), types.attributesScope.shared.value) - ]).then(() => { - toast.showSuccess(successfulSaved, 2000, angular.element('.gateway-form'), 'top left'); - }) - }; - - function getAttributes() { - let promises = []; - promises.push(getAttribute(currentConfigurationAttribute, types.attributesScope.client.value)); - promises.push(getAttribute(configurationDraftsAttribute, types.attributesScope.server.value)); - promises.push(getAttribute(remoteLoggingLevelAttribute, types.attributesScope.shared.value)); - $q.all(promises).then((response) => { - processCurrentConfiguration(response[0]); - processConfigurationDrafts(response[1]); - processLoggingLevel(response[2]); - }); - } - - function getAttribute(attributeName, attributeScope) { - return attributeService.getEntityAttributesValues(vm.configurations.gateway.id.entityType, vm.configurations.gateway.id.id, attributeScope, attributeName); - } - - function saveAttribute(attributeName, attributeValue, attributeScope) { - let attributes = [{ - key: attributeName, - value: attributeValue - }]; - return attributeService.saveEntityAttributes(vm.configurations.gateway.id.entityType, vm.configurations.gateway.id.id, attributeScope, attributes); - } - - vm.exportConfig = () => { - let filesZip = {}; - filesZip["tb_gateway.yaml"] = generateYAMLConfigurationFile(); - generateConfigConnectorFiles(filesZip); - generateLogConfigFile(filesZip); - importExport.exportJSZip(filesZip, archiveFileName); - saveAttribute(remoteLoggingLevelAttribute, vm.configurations.remoteLoggingLevel.toUpperCase(), types.attributesScope.shared.value); - }; - - function generateYAMLConfigurationFile() { - let config; - config = 'thingsboard:\n'; - config += ' host: ' + vm.configurations.host + '\n'; - config += ' remoteConfiguration: ' + vm.configurations.remoteConfiguration + '\n'; - config += ' port: ' + vm.configurations.port + '\n'; - config += ' security:\n'; - if (vm.configurations.securityType === 'accessToken') { - config += ' access-token: ' + vm.configurations.accessToken + '\n'; - } else if (vm.configurations.securityType === 'tls') { - config += ' ca_cert: ' + vm.configurations.caCertPath + '\n'; - config += ' privateKey: ' + vm.configurations.privateKeyPath + '\n'; - config += ' cert: ' + vm.configurations.certPath + '\n'; - } - config += 'storage:\n'; - if (vm.configurations.storageType === 'memoryStorage') { - config += ' type: memory\n'; - config += ' read_records_count: ' + vm.configurations.readRecordsCount + '\n'; - config += ' max_records_count: ' + vm.configurations.maxRecordsCount + '\n'; - } else if (vm.configurations.storageType === 'fileStorage') { - config += ' type: file\n'; - config += ' data_folder_path: ' + vm.configurations.dataFolderPath + '\n'; - config += ' max_file_count: ' + vm.configurations.maxFilesCount + '\n'; - config += ' max_read_records_count: ' + vm.configurations.readRecordsCount + '\n'; - config += ' max_records_per_file: ' + vm.configurations.maxRecordsCount + '\n'; - } - config += 'connectors:\n'; - for (let i = 0; i < vm.configurations.connectors.length; i++) { - if (vm.configurations.connectors[i].enabled) { - config += ' -\n'; - config += ' name: ' + vm.configurations.connectors[i].name + '\n'; - config += ' type: ' + vm.configurations.connectors[i].configType + '\n'; - config += ' configuration: ' + generateFileName(vm.configurations.connectors[i].name) + '\n'; - } - } - return config; - } - - function generateConfigConnectorFiles(fileZipAdd) { - for (let i = 0; i < vm.configurations.connectors.length; i++) { - if (vm.configurations.connectors[i].enabled) { - fileZipAdd[generateFileName(vm.configurations.connectors[i].name)] = angular.toJson(vm.configurations.connectors[i].config); - } - } - } - - function generateLogConfigFile(fileZipAdd) { - fileZipAdd["logs.conf"] = getLogsConfig(); - } - - function getLogsConfig() { - return templateLogsConfig - .replace(/{ERROR}/g, vm.configurations.remoteLoggingLevel) - .replace(/{.\/logs\/}/g, vm.configurations.remoteLoggingPathToLogs); - } - - function getGatewayConfigJSON() { - let gatewayConfig = {}; - gatewayConfig["thingsboard"] = gatewayMainConfigJSON(); - gatewayConnectorConfigJSON(gatewayConfig); - return gatewayConfig; - } - - function gatewayMainConfigJSON() { - let configuration = {}; - - let thingsBoard = {}; - thingsBoard.host = vm.configurations.host; - thingsBoard.remoteConfiguration = vm.configurations.remoteConfiguration; - thingsBoard.port = vm.configurations.port; - let security = {}; - if (vm.configurations.securityType === 'accessToken') { - security.accessToken = (vm.configurations.accessToken) ? vm.configurations.accessToken : "" - } else { - security.caCert = vm.configurations.caCertPath; - security.privateKey = vm.configurations.privateKeyPath; - security.cert = vm.configurations.certPath; - } - thingsBoard.security = security; - configuration.thingsboard = thingsBoard; - - let storage = {}; - if (vm.configurations.storageType === 'memoryStorage') { - storage.type = "memory"; - storage.read_records_count = vm.configurations.readRecordsCount; - storage.max_records_count = vm.configurations.maxRecordsCount; - } else if (vm.configurations.storageType === 'fileStorage') { - storage.type = "file"; - storage.data_folder_path = vm.configurations.dataFolderPath; - storage.max_file_count = vm.configurations.maxFilesCount; - storage.max_read_records_count = vm.configurations.readRecordsCount; - storage.max_records_per_file = vm.configurations.maxRecordsCount; - } - configuration.storage = storage; - - let connectors = []; - for (let i = 0; i < vm.configurations.connectors.length; i++) { - if (vm.configurations.connectors[i].enabled) { - let connector = { - configuration: generateFileName(vm.configurations.connectors[i].name), - name: vm.configurations.connectors[i].name, - type: vm.configurations.connectors[i].configType - }; - connectors.push(connector); - } - } - configuration.connectors = connectors; - - configuration.logs = $window.btoa(getLogsConfig()); - - return configuration; - } - - function gatewayConnectorConfigJSON(gatewayConfiguration) { - for (let i = 0; i < vm.configurations.connectors.length; i++) { - if (vm.configurations.connectors[i].enabled) { - let typeConnector = vm.configurations.connectors[i].configType; - if (!angular.isArray(gatewayConfiguration[typeConnector])) { - gatewayConfiguration[typeConnector] = []; - } - - let connectorConfig = { - name: vm.configurations.connectors[i].name, - config: vm.configurations.connectors[i].config - }; - gatewayConfiguration[typeConnector].push(connectorConfig); - } - } - } - - function getDraftConnectorJSON() { - let draftConnector = {}; - for (let i = 0; i < vm.configurations.connectors.length; i++) { - if (!vm.configurations.connectors[i].enabled) { - let connector = { - connector: vm.configurations.connectors[i].configType, - config: vm.configurations.connectors[i].config - }; - draftConnector[vm.configurations.connectors[i].name] = connector; - } - } - return draftConnector; - } - - function getGatewaysList(firstInit) { - vm.gateways = []; - entityService.getEntitiesByNameFilter(types.entityType.device, "", -1).then((devices) => { - for (let i = 0; i < devices.length; i++) { - const device = devices[i]; - if (device.additionalInfo !== null && device.additionalInfo.gateway === true) { - vm.gateways.push(device); - if (vm.deviceNameForm && firstInit && vm.gateways.length && device.name === vm.deviceNameForm) { - vm.configurations.gateway = device; - vm.getAccessToken(device.id); - } else if (firstInit && vm.gateways.length && device.name === vm.gateways[0].name) { - vm.configurations.gateway = device; - vm.getAccessToken(device.id); - } - } - } - }); - } - - function processCurrentConfiguration(response) { - if (response.length > 0) { - vm.configurations.connectors = []; - let attribute = angular.fromJson($window.atob(response[0].value)); - for (var attributeKey in attribute) { - let keyValue = attribute[attributeKey]; - if (attributeKey === "thingsboard") { - if (keyValue !== null && Object.keys(keyValue).length > 0) { - setConfigGateway(keyValue); - } - } else { - for (let connectorType in keyValue) { - let name = "No name"; - if (Object.prototype.hasOwnProperty.call(keyValue[connectorType], 'name')) { - name = keyValue[connectorType].name; - } - let connector = { - enabled: true, - configType: attributeKey, - config: keyValue[connectorType].config, - name: name - }; - vm.configurations.connectors.push(connector); - } - } - } - } - } - - function processConfigurationDrafts(response) { - if (response.length > 0) { - let attribute = angular.fromJson($window.atob(response[0].value)); - for (let key in attribute) { - let connector = { - enabled: false, - configType: attribute[key].connector, - config: attribute[key].config, - name: key - }; - vm.configurations.connectors.push(connector); - } - } - } - - function processLoggingLevel(response) { - if (response.length > 0) { - if (vm.types.gatewayLogLevel[response[0].value.toLowerCase()]) { - vm.configurations.remoteLoggingLevel = response[0].value.toUpperCase(); - } - } else { - vm.configurations.remoteLoggingLevel = vm.types.gatewayLogLevel.debug; - } - } - - function setConfigGateway(keyValue) { - if (Object.prototype.hasOwnProperty.call(keyValue, 'thingsboard')) { - vm.configurations.host = keyValue.thingsboard.host; - vm.configurations.port = keyValue.thingsboard.port; - vm.configurations.remoteConfiguration = keyValue.thingsboard.remoteConfiguration; - if (Object.prototype.hasOwnProperty.call(keyValue.thingsboard.security, 'accessToken')) { - vm.configurations.securityType = 'accessToken'; - vm.configurations.accessToken = keyValue.thingsboard.security.accessToken; - } else { - vm.configurations.securityType = 'tls'; - vm.configurations.caCertPath = keyValue.thingsboard.security.caCert; - vm.configurations.privateKeyPath = keyValue.thingsboard.security.private_key; - vm.configurations.certPath = keyValue.thingsboard.security.cert; - } - } - - if (Object.prototype.hasOwnProperty.call(keyValue, 'storage') && Object.prototype.hasOwnProperty.call(keyValue.storage, 'type')) { - if (keyValue.storage.type === 'memory') { - vm.configurations.storageType = 'memoryStorage'; - vm.configurations.readRecordsCount = keyValue.storage.read_records_count; - vm.configurations.maxRecordsCount = keyValue.storage.max_records_count; - } else if (keyValue.storage.type === 'file') { - vm.configurations.storageType = 'fileStorage'; - vm.configurations.dataFolderPath = keyValue.storage.data_folder_path; - vm.configurations.maxFilesCount = keyValue.storage.max_file_count; - vm.configurations.readRecordsCount = keyValue.storage.read_records_count; - vm.configurations.maxRecordsCount = keyValue.storage.max_records_count; - } - } - } - - function generateFileName(fileName) { - return fileName.replace("_", "") - .replace("-", "") - .replace(/^\s+|\s+/g, '') - .toLowerCase() + '.json'; - } -} - - diff --git a/ui/src/app/components/gateway/gateway-form.scss b/ui/src/app/components/gateway/gateway-form.scss deleted file mode 100644 index f6851c7688..0000000000 --- a/ui/src/app/components/gateway/gateway-form.scss +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.gateway-form{ - height: 100%; - padding: 5px 5px 0; - background-color: transparent; - - .gateway-form-row{ - md-input-container{ - margin-bottom: 0; - } - - &.gateway-config-row-vertical{ - flex-direction: column; - - .md-select-container{ - margin-bottom: 14px; - } - } - } - - .security-type { - margin-top: 18px; - } - - .form-action-buttons{ - padding-top: 8px; - } -} diff --git a/ui/src/app/components/gateway/gateway-form.tpl.html b/ui/src/app/components/gateway/gateway-form.tpl.html deleted file mode 100644 index 13603b2cfc..0000000000 --- a/ui/src/app/components/gateway/gateway-form.tpl.html +++ /dev/null @@ -1,229 +0,0 @@ - - -
    - - - -
    {{ 'gateway.thingsboard' | translate | uppercase }}
    - - -
    - - -
    {{ 'gateway.thingsboard' | translate | uppercase }}
    - - -
    - - - - - - - - - - - - {{securityType.name | translate}} - - - -
    - - - -
    -
    gateway.thingsboard-host-required
    -
    -
    - - - -
    -
    gateway.thingsboard-port-required
    -
    gateway.thingsboard-port-max
    -
    gateway.thingsboard-port-min
    -
    gateway.thingsboard-port-pattern
    -
    -
    -
    -
    - - - - - - - - - - - - -
    - - {{ 'gateway.remote' | translate }} - -
    - - - - - {{logLevel}} - - - - - - -
    -
    gateway.path-logs-required
    -
    -
    -
    -
    -
    -
    - - -
    {{ 'gateway.storage' | translate | uppercase }}
    - - -
    - - -
    {{ 'gateway.storage' | translate | uppercase }}
    - - -
    - - - - - - {{storageType.name | translate}} - - - -
    - - - -
    -
    gateway.storage-pack-size-required
    -
    gateway.storage-pack-size-min
    -
    gateway.storage-pack-size-pattern
    -
    -
    - - - -
    -
    gateway.storage-max-records-required
    -
    gateway.storage-max-records-min
    -
    gateway.storage-max-records-pattern
    -
    -
    -
    -
    - - - -
    -
    gateway.storage-max-files-required
    -
    gateway.storage-max-files-min
    -
    gateway.storage-max-files-pattern
    -
    -
    - - - -
    -
    gateway.storage-path-required
    -
    -
    -
    -
    -
    -
    - - -
    {{ 'gateway.connectors' | translate | uppercase }}
    - - -
    - - -
    {{ 'gateway.connectors' | translate | uppercase }}
    - - -
    - - - - -
    -
    -
    -
    - - {{'action.download' | translate }} - {{'gateway.download-tip' | translate }} - - - {{'action.save' | translate }} - {{'gateway.save-tip' | translate }} - - -
    -
    -
    diff --git a/ui/src/app/components/grid.directive.js b/ui/src/app/components/grid.directive.js deleted file mode 100644 index e22c167a48..0000000000 --- a/ui/src/app/components/grid.directive.js +++ /dev/null @@ -1,718 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './grid.scss'; - -import thingsboardScopeElement from './scope-element.directive'; -import thingsboardDetailsSidenav from './details-sidenav.directive'; - -/* eslint-disable import/no-unresolved, import/default */ - -import gridTemplate from './grid.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -export default angular.module('thingsboard.directives.grid', [thingsboardScopeElement, thingsboardDetailsSidenav]) - .directive('tbGrid', Grid) - .controller('AddItemController', AddItemController) - .controller('ItemCardController', ItemCardController) - .directive('tbGridCardContent', GridCardContent) - .filter('range', RangeFilter) - .name; - -/*@ngInject*/ -function RangeFilter() { - return function(input, total) { - total = parseInt(total); - - for (var i=0; i= vm.items.rowData.length) { - vm.itemRows.fetchMoreItems_(index); - return null; - } - return vm.items.rowData[index]; - }, - - getLength: function () { - if (vm.items.hasNext && !vm.items.pending) { - return vm.items.rowData.length + pageSize; - } else { - return vm.items.rowData.length; - } - }, - - fetchMoreItems_: function () { - if (vm.items.hasNext && !vm.items.pending) { - var promise = vm.fetchItemsFunc(vm.items.nextPageLink, $scope.searchConfig.searchEntitySubtype); - if (promise) { - vm.items.pending = true; - promise.then( - function success(items) { - if (vm.items.reloadPending) { - vm.items.pending = false; - reload(); - } else { - vm.items.data = vm.items.data.concat(items.data); - var startIndex = vm.items.data.length - items.data.length; - var endIndex = vm.items.data.length; - for (var i = startIndex; i < endIndex; i++) { - var item = vm.items.data[i]; - item.index = i; - var row = Math.floor(i / vm.columns); - var itemRow = vm.items.rowData[row]; - if (!itemRow) { - itemRow = []; - vm.items.rowData.push(itemRow); - } - itemRow.push(item); - } - vm.items.nextPageLink = items.nextPageLink; - vm.items.hasNext = items.hasNext; - if (vm.items.hasNext) { - vm.items.nextPageLink.limit = pageSize; - } - vm.items.pending = false; - if (vm.items.loadCallback) { - vm.items.loadCallback(); - vm.items.loadCallback = null; - } - } - }, - function fail() { - vm.items.hasNext = false; - vm.items.pending = false; - }); - } else { - vm.items.hasNext = false; - } - } - } - }; - - function columnsUpdated(newColumns) { - if (vm.columns !== newColumns) { - var newTopIndex = Math.ceil(vm.columns * vm.topIndex / newColumns); - pageSize = 10 * newColumns; - vm.items.rowData = []; - if (vm.items.nextPageLink) { - vm.items.nextPageLink.limit = pageSize; - } - - for (var i = 0; i < vm.items.data.length; i++) { - var item = vm.items.data[i]; - var row = Math.floor(i / newColumns); - var itemRow = vm.items.rowData[row]; - if (!itemRow) { - itemRow = []; - vm.items.rowData.push(itemRow); - } - itemRow.push(item); - } - - vm.columns = newColumns; - vm.topIndex = newTopIndex; - vm.itemRows.getItemAtIndex(newTopIndex+pageSize); - $timeout(function() { - moveToIndex(newTopIndex); - }, 500); - } - } - - function initGridConfiguration() { - vm.gridConfiguration = vm.gridConfiguration || function () { - return {}; - }; - - vm.config = vm.gridConfiguration(); - - vm.itemHeight = vm.config.itemHeight || 199; - - vm.refreshParamsFunc = vm.config.refreshParamsFunc || function () { - return {"topIndex": vm.topIndex}; - }; - - vm.deleteItemTitleFunc = vm.config.deleteItemTitleFunc || function () { - return $translate.instant('grid.delete-item-title'); - }; - - vm.deleteItemContentFunc = vm.config.deleteItemContentFunc || function () { - return $translate.instant('grid.delete-item-text'); - }; - - vm.deleteItemsTitleFunc = vm.config.deleteItemsTitleFunc || function () { - return $translate.instant('grid.delete-items-title', {count: vm.items.selectedCount}, 'messageformat'); - }; - - vm.deleteItemsActionTitleFunc = vm.config.deleteItemsActionTitleFunc || function (selectedCount) { - return $translate.instant('grid.delete-items-action-title', {count: selectedCount}, 'messageformat'); - }; - - vm.deleteItemsContentFunc = vm.config.deleteItemsContentFunc || function () { - return $translate.instant('grid.delete-items-text'); - }; - - vm.fetchItemsFunc = vm.config.fetchItemsFunc || function () { - return $q.when([]); - }; - - vm.loadItemDetailsFunc = vm.config.loadItemDetailsFunc || function (item) { - return $q.when(item); - }; - - vm.saveItemFunc = vm.config.saveItemFunc || function (item) { - return $q.when(item); - }; - - vm.deleteItemFunc = vm.config.deleteItemFunc || function () { - return $q.when(); - }; - - vm.clickItemFunc = vm.config.clickItemFunc || function ($event, item) { - vm.openItem($event, item); - }; - - vm.itemCardTemplate = ''; - if (vm.config.itemCardTemplate) { - vm.itemCardTemplate = vm.config.itemCardTemplate; - } else if (vm.config.itemCardTemplateUrl) { - vm.itemCardTemplate = $templateCache.get(vm.config.itemCardTemplateUrl); - } - if (vm.config.itemCardController) { - vm.itemCardController = vm.config.itemCardController; - } else { - vm.itemCardController = 'ItemCardController'; - } - if (vm.config.addItemController) { - vm.addItemController = vm.config.addItemController; - } else { - vm.addItemController = 'AddItemController'; - } - - vm.parentCtl = vm.config.parentCtl || vm; - - vm.getItemTitleFunc = vm.config.getItemTitleFunc || function () { - return ''; - }; - - vm.actionsList = vm.config.actionsList || []; - - for (var i = 0; i < vm.actionsList.length; i++) { - var action = vm.actionsList[i]; - action.isEnabled = action.isEnabled || function() { - return true; - }; - } - - vm.groupActionsList = vm.config.groupActionsList || [ - { - onAction: function ($event) { - deleteItems($event); - }, - name: function() { return $translate.instant('action.delete') }, - details: vm.deleteItemsActionTitleFunc, - icon: "delete" - } - ]; - - vm.addItemText = vm.config.addItemText || function () { - return $translate.instant('grid.add-item-text'); - }; - - vm.addItemAction = vm.config.addItemAction || { - onAction: function ($event) { - addItem($event); - }, - name: function() { return $translate.instant('action.add') }, - details: function() { return vm.addItemText() }, - icon: "add" - }; - - vm.addItemActionsOpen = false; - - vm.addItemActions = vm.config.addItemActions || []; - - vm.onGridInited = vm.config.onGridInited || function () { - }; - - vm.addItemTemplateUrl = vm.config.addItemTemplateUrl; - - vm.noItemsText = vm.config.noItemsText || function () { - return $translate.instant('grid.no-items-text'); - }; - - vm.itemDetailsText = vm.config.itemDetailsText || function () { - return $translate.instant('grid.item-details'); - }; - - vm.isDetailsReadOnly = vm.config.isDetailsReadOnly || function () { - return false; - }; - - vm.isSelectionEnabled = vm.config.isSelectionEnabled || function () { - return true; - }; - - vm.topIndex = vm.config.topIndex || 0; - - vm.items = vm.config.items || { - data: [], - rowData: [], - nextPageLink: { - limit: vm.topIndex + pageSize, - textSearch: $scope.searchConfig.searchText - }, - selections: {}, - selectedCount: 0, - hasNext: true, - pending: false - }; - - vm.detailsConfig = { - isDetailsOpen: false, - isDetailsEditMode: false, - currentItem: null, - editingItem: null - }; - } - - $scope.$on('searchTextUpdated', function () { - reload(); - }); - - $scope.$on('searchEntitySubtypeUpdated', function () { - reload(); - }); - - vm.onGridInited(vm); - - vm.itemRows.getItemAtIndex(pageSize); - - function reload() { - if (vm.items && vm.items.pending) { - vm.items.reloadPending = true; - } else { - vm.items.data.length = 0; - vm.items.rowData.length = 0; - vm.items.nextPageLink = { - limit: pageSize, - textSearch: $scope.searchConfig.searchText - }; - vm.items.selections = {}; - vm.items.selectedCount = 0; - vm.items.hasNext = true; - vm.items.pending = false; - vm.detailsConfig.isDetailsOpen = false; - vm.items.reloadPending = false; - vm.itemRows.getItemAtIndex(pageSize); - } - } - - function refreshList() { - let preservedTopIndex = vm.topIndex; - vm.items.data.length = 0; - vm.items.rowData.length = 0; - vm.items.nextPageLink = { - limit: preservedTopIndex + pageSize, - textSearch: $scope.searchConfig.searchText - }; - vm.items.selections = {}; - vm.items.selectedCount = 0; - vm.items.hasNext = true; - vm.items.pending = false; - vm.detailsConfig.isDetailsOpen = false; - vm.items.reloadPending = false; - vm.items.loadCallback = () => { - $mdUtil.nextTick(() => { - moveToIndex(preservedTopIndex); - }); - }; - vm.itemRows.getItemAtIndex(preservedTopIndex+pageSize); - } - - function addItem($event) { - $mdDialog.show({ - controller: vm.addItemController, - controllerAs: 'vm', - templateUrl: vm.addItemTemplateUrl, - parent: angular.element($document[0].body), - locals: {saveItemFunction: vm.saveItemFunc}, - fullscreen: true, - targetEvent: $event - }).then(function () { - refreshList(); - }, function () { - }); - } - - function openItem($event, item) { - $event.stopPropagation(); - if (vm.detailsConfig.currentItem != null && vm.detailsConfig.currentItem.id.id === item.id.id) { - if (vm.detailsConfig.isDetailsOpen) { - vm.detailsConfig.isDetailsOpen = false; - return; - } - } - vm.loadItemDetailsFunc(item).then(function success(detailsItem) { - detailsItem.index = item.index; - vm.detailsConfig.currentItem = detailsItem; - vm.detailsConfig.isDetailsEditMode = false; - vm.detailsConfig.isDetailsOpen = true; - }); - } - - function isCurrentItem(item) { - if (item != null && vm.detailsConfig.currentItem != null && - vm.detailsConfig.currentItem.id.id === item.id.id) { - return vm.detailsConfig.isDetailsOpen; - } else { - return false; - } - } - - function onToggleDetailsEditMode(theForm) { - if (!vm.detailsConfig.isDetailsEditMode) { - theForm.$setPristine(); - } - } - - function onCloseDetails() { - vm.detailsConfig.currentItem = null; - } - - function operatingItem() { - if (!vm.detailsConfig.isDetailsEditMode) { - if (vm.detailsConfig.editingItem) { - vm.detailsConfig.editingItem = null; - } - return vm.detailsConfig.currentItem; - } else { - if (!vm.detailsConfig.editingItem) { - vm.detailsConfig.editingItem = angular.copy(vm.detailsConfig.currentItem); - } - return vm.detailsConfig.editingItem; - } - } - - function saveItem(theForm) { - vm.saveItemFunc(vm.detailsConfig.editingItem).then(function success(item) { - theForm.$setPristine(); - vm.detailsConfig.isDetailsEditMode = false; - var index = vm.detailsConfig.currentItem.index; - item.index = index; - vm.detailsConfig.currentItem = item; - vm.items.data[index] = item; - var row = Math.floor(index / vm.columns); - var itemRow = vm.items.rowData[row]; - var column = index % vm.columns; - itemRow[column] = item; - }); - } - - function deleteItem($event, item) { - if ($event) { - $event.stopPropagation(); - } - var confirm = $mdDialog.confirm() - .targetEvent($event) - .title(vm.deleteItemTitleFunc(item)) - .htmlContent(vm.deleteItemContentFunc(item)) - .ariaLabel($translate.instant('grid.delete-item')) - .cancel($translate.instant('action.no')) - .ok($translate.instant('action.yes')); - $mdDialog.show(confirm).then(function () { - vm.deleteItemFunc(item.id.id).then(function success() { - refreshList(); - }); - }, - function () { - }); - - } - - function deleteItems($event) { - var confirm = $mdDialog.confirm() - .targetEvent($event) - .title(vm.deleteItemsTitleFunc(vm.items.selectedCount)) - .htmlContent(vm.deleteItemsContentFunc()) - .ariaLabel($translate.instant('grid.delete-items')) - .cancel($translate.instant('action.no')) - .ok($translate.instant('action.yes')); - $mdDialog.show(confirm).then(function () { - var tasks = []; - for (var id in vm.items.selections) { - tasks.push(vm.deleteItemFunc(id)); - } - $q.all(tasks).then(function () { - refreshList(); - }); - }, - function () { - }); - } - - - function toggleItemSelection($event, item) { - $event.stopPropagation(); - var selected = angular.isDefined(item.selected) && item.selected; - item.selected = !selected; - if (item.selected) { - vm.items.selections[item.id.id] = true; - vm.items.selectedCount++; - } else { - delete vm.items.selections[item.id.id]; - vm.items.selectedCount--; - } - } - - function triggerResize() { - var w = angular.element($window); - w.triggerHandler('resize'); - } - - function isTenantAdmin() { - return userService.getAuthority() == 'TENANT_ADMIN'; - } - - function moveToTop() { - moveToIndex(0, true); - } - - function moveToIndex(index, animate) { - var repeatContainer = $scope.repeatContainer[0]; - var scrollElement = repeatContainer.children[0]; - var startY = scrollElement.scrollTop; - var stopY = index * vm.itemHeight; - if (stopY > 0) { - stopY+= 16; - } - var distance = Math.abs(startY - stopY); - if (distance < 100 || !animate) { - scrollElement.scrollTop = stopY; - return; - } - var upElseDown = stopY < startY; - var speed = Math.round(distance / 100); - if (speed >= 20) speed = 20; - var step = Math.round(distance / 25); - - var leapY = upElseDown ? startY - step : startY + step; - var timer = 0; - for (var i = startY; upElseDown ? (i > stopY) : (i < stopY); upElseDown ? (i -= step) : (i += step)) { - $timeout(function (topY) { - scrollElement.scrollTop = topY; - }, timer * speed, true, leapY); - if (upElseDown) { - leapY -= step; - if (leapY < stopY) { - leapY = stopY; - } - } else { - leapY += step; - if (leapY > stopY) { - leapY = stopY; - } - } - timer++; - } - - } - - function noData() { - return vm.items.data.length == 0 && !vm.items.hasNext; - } - - function hasData() { - return vm.items.data.length > 0; - } - -} - -/*@ngInject*/ -function AddItemController($scope, $mdDialog, saveItemFunction, helpLinks) { - - var vm = this; - - vm.helpLinks = helpLinks; - vm.item = {}; - - vm.add = add; - vm.cancel = cancel; - - function cancel() { - $mdDialog.cancel(); - } - - function add() { - saveItemFunction(vm.item).then(function success(item) { - vm.item = item; - $scope.theForm.$setPristine(); - $mdDialog.hide(); - }); - } -} \ No newline at end of file diff --git a/ui/src/app/components/grid.scss b/ui/src/app/components/grid.scss deleted file mode 100644 index 8e254c40b2..0000000000 --- a/ui/src/app/components/grid.scss +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "~compass-sass-mixins/lib/animate"; - -.tb-uppercase { - text-transform: uppercase; -} - -.tb-card-item { - transition: all .2s ease-in-out; - - md-card-content { - max-height: 53px; - padding-top: 0; - } - - md-card-title { - width: 100%; - - md-card-title-text { - min-width: 50%; - max-height: 32px; - - .md-headline { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - } - } -} - -.tb-current-item { - opacity: .5; - - transform: scale(1.05); -} - -#tb-vertical-container { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; -} diff --git a/ui/src/app/components/grid.tpl.html b/ui/src/app/components/grid.tpl.html deleted file mode 100644 index 900f5b86b5..0000000000 --- a/ui/src/app/components/grid.tpl.html +++ /dev/null @@ -1,122 +0,0 @@ - -{{vm.noItemsText()}} -
    -
    - -
    -
    -
    - -
    - -
    - - - -
    - - {{vm.getItemTitleFunc(rowItem[n])}} - -
    -
    - - - - - - - {{ action.details( rowItem[n] ) }} - - - - -
    -
    -
    -
    -
    -
    -
    -
    - - -
    -
    -
    -
    -
    -
    -
    - - \ No newline at end of file diff --git a/ui/src/app/components/js-func.directive.js b/ui/src/app/components/js-func.directive.js deleted file mode 100644 index 2d8dcd83bf..0000000000 --- a/ui/src/app/components/js-func.directive.js +++ /dev/null @@ -1,253 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './js-func.scss'; - -import ace from 'brace'; -import 'brace/ext/language_tools'; -import 'brace/ext/searchbox'; -import $ from 'jquery'; -import thingsboardToast from '../services/toast'; -import thingsboardUtils from '../common/utils.service'; -import thingsboardExpandFullscreen from './expand-fullscreen.directive'; - -import fixAceEditor from './ace-editor-fix'; - -/* eslint-disable import/no-unresolved, import/default */ - -import jsFuncTemplate from './js-func.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -import beautify from 'js-beautify'; - -const js_beautify = beautify.js; - -/* eslint-disable angular/angularelement */ - -export default angular.module('thingsboard.directives.jsFunc', [thingsboardToast, thingsboardUtils, thingsboardExpandFullscreen]) - .directive('tbJsFunc', JsFunc) - .name; - -/*@ngInject*/ -function JsFunc($compile, $templateCache, toast, utils, $translate) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - var template = $templateCache.get(jsFuncTemplate); - element.html(template); - - scope.functionName = attrs.functionName; - scope.functionArgs = scope.$eval(attrs.functionArgs); - scope.validationArgs = scope.$eval(attrs.validationArgs); - scope.resultType = attrs.resultType; - if (!scope.resultType || scope.resultType.length === 0) { - scope.resultType = "nocheck"; - } - - scope.validationTriggerArg = attrs.validationTriggerArg; - - scope.functionValid = true; - - var Range = ace.acequire("ace/range").Range; - scope.js_editor; - scope.errorMarkers = []; - - - scope.functionArgsString = ''; - for (var i = 0; i < scope.functionArgs.length; i++) { - if (scope.functionArgsString.length > 0) { - scope.functionArgsString += ', '; - } - scope.functionArgsString += scope.functionArgs[i]; - } - - scope.onFullscreenChanged = function () { - updateEditorSize(); - }; - - scope.beautifyJs = function () { - var res = js_beautify(scope.functionBody, {indent_size: 4, wrap_line_length: 60}); - scope.functionBody = res; - }; - - function updateEditorSize() { - if (scope.js_editor) { - scope.js_editor.resize(); - scope.js_editor.renderer.updateFull(); - } - } - - scope.jsEditorOptions = { - useWrapMode: true, - mode: 'javascript', - advanced: { - enableSnippets: true, - enableBasicAutocompletion: true, - enableLiveAutocompletion: true - }, - onLoad: function (_ace) { - scope.js_editor = _ace; - scope.js_editor.session.on("change", function () { - scope.cleanupJsErrors(); - }); - fixAceEditor(_ace); - } - }; - - scope.cleanupJsErrors = function () { - toast.hide(); - for (var i = 0; i < scope.errorMarkers.length; i++) { - scope.js_editor.session.removeMarker(scope.errorMarkers[i]); - } - scope.errorMarkers = []; - if (scope.errorAnnotationId && scope.errorAnnotationId > -1) { - var annotations = scope.js_editor.session.getAnnotations(); - annotations.splice(scope.errorAnnotationId, 1); - scope.js_editor.session.setAnnotations(annotations); - scope.errorAnnotationId = -1; - } - } - - scope.updateValidity = function () { - ngModelCtrl.$setValidity('functionBody', scope.functionValid); - }; - - scope.$watch('functionBody', function (newFunctionBody, oldFunctionBody) { - ngModelCtrl.$setViewValue(scope.functionBody); - if (!angular.equals(newFunctionBody, oldFunctionBody)) { - scope.functionValid = true; - } - scope.updateValidity(); - }); - - ngModelCtrl.$render = function () { - scope.functionBody = ngModelCtrl.$viewValue; - }; - - scope.showError = function (error) { - var toastParent = $('#tb-javascript-panel', element); - var dialogContent = toastParent.closest('md-dialog-content'); - if (dialogContent.length > 0) { - toastParent = dialogContent; - } - toast.showError(error, toastParent, 'bottom left'); - } - - scope.validate = function () { - try { - var toValidate = new Function(scope.functionArgsString, scope.functionBody); - if (scope.noValidate) { - return true; - } - var res; - var validationError; - for (var i=0;i -
    -
    - - - {{ - 'js-func.tidy' | translate }} - -
    -
    -
    -
    -
    -
    -
    - -
    -
    diff --git a/ui/src/app/components/json-content.directive.js b/ui/src/app/components/json-content.directive.js deleted file mode 100644 index c4281b15c2..0000000000 --- a/ui/src/app/components/json-content.directive.js +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './json-content.scss'; - -import 'brace/ext/language_tools'; -import 'brace/ext/searchbox'; -import 'brace/mode/json'; -import 'brace/mode/text'; -import 'brace/snippets/json'; -import 'brace/snippets/text'; - -import fixAceEditor from './ace-editor-fix'; - -/* eslint-disable import/no-unresolved, import/default */ - -import jsonContentTemplate from './json-content.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -import beautify from 'js-beautify'; - -const js_beautify = beautify.js; - -export default angular.module('thingsboard.directives.jsonContent', []) - .directive('tbJsonContent', JsonContent) - .name; - -/*@ngInject*/ -function JsonContent($compile, $templateCache, toast, types, utils) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - var template = $templateCache.get(jsonContentTemplate); - element.html(template); - - scope.label = attrs.label; - - scope.validationTriggerArg = attrs.validationTriggerArg; - - scope.contentValid = true; - - scope.json_editor; - - scope.onFullscreenChanged = function () { - updateEditorSize(); - }; - - scope.beautifyJSON = function () { - scope.contentBody = js_beautify(scope.contentBody, {indent_size: 4, wrap_line_length: 60}); - }; - - scope.minifyJSON = function () { - scope.contentBody = angular.toJson(angular.fromJson(scope.contentBody)); - }; - - function updateEditorSize() { - if (scope.json_editor) { - scope.json_editor.resize(); - scope.json_editor.renderer.updateFull(); - } - } - - var mode; - if (scope.contentType) { - mode = types.contentType[scope.contentType].code; - } else { - mode = 'text'; - } - - scope.jsonEditorOptions = { - useWrapMode: true, - mode: mode, - advanced: { - enableSnippets: true, - enableBasicAutocompletion: true, - enableLiveAutocompletion: true - }, - onLoad: function (_ace) { - scope.json_editor = _ace; - scope.json_editor.session.on("change", function () { - scope.cleanupJsonErrors(); - }); - fixAceEditor(_ace); - } - }; - - scope.$watch('contentType', () => { - var mode; - if (scope.contentType) { - mode = types.contentType[scope.contentType].code; - } else { - mode = 'text'; - } - if (scope.json_editor) { - scope.json_editor.session.setMode('ace/mode/' + mode); - } - }); - - scope.cleanupJsonErrors = function () { - toast.hide(); - }; - - scope.updateValidity = function () { - ngModelCtrl.$setValidity('contentBody', scope.contentValid); - }; - - scope.$watch('contentBody', function (newContent, oldContent) { - ngModelCtrl.$setViewValue(scope.contentBody); - if (!angular.equals(newContent, oldContent)) { - scope.contentValid = scope.validate(); - } - scope.updateValidity(); - }); - - ngModelCtrl.$render = function () { - scope.contentBody = ngModelCtrl.$viewValue; - }; - - scope.showError = function (error) { - var toastParent = angular.element('#tb-json-panel', element); - toast.showError(error, toastParent, 'bottom left'); - }; - - scope.validate = function () { - try { - if (scope.validateContent) { - if (scope.contentType == types.contentType.JSON.value) { - angular.fromJson(scope.contentBody); - } - } - return true; - } catch (e) { - if (!scope.hideErrorToast) { - var details = utils.parseException(e); - var errorInfo = 'Error:'; - if (details.name) { - errorInfo += ' ' + details.name + ':'; - } - if (details.message) { - errorInfo += ' ' + details.message; - } - scope.showError(errorInfo); - } - return false; - } - }; - - scope.$on('form-submit', function (event, args) { - if (!scope.readonly) { - if (!args || scope.validationTriggerArg && scope.validationTriggerArg == args) { - scope.cleanupJsonErrors(); - scope.contentValid = true; - scope.updateValidity(); - scope.contentValid = scope.validate(); - scope.updateValidity(); - } - } - }); - - scope.$on('update-ace-editor-size', function () { - updateEditorSize(); - }); - - $compile(element.contents())(scope); - }; - - return { - restrict: "E", - require: "^ngModel", - scope: { - contentType: '=', - validateContent: '=?', - hideErrorToast: '=?', - readonly:'=ngReadonly', - fillHeight:'=?' - }, - link: linker - }; -} diff --git a/ui/src/app/components/json-content.scss b/ui/src/app/components/json-content.scss deleted file mode 100644 index d444f8051f..0000000000 --- a/ui/src/app/components/json-content.scss +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -tb-json-content { - position: relative; - - .fill-height { - height: 100%; - } -} - -.tb-json-content-toolbar { - .md-button.tidy { - min-width: 32px; - min-height: 15px; - padding: 4px; - margin: 0 5px 0 0; - font-size: 12px; - line-height: 15px; - color: #7b7b7b; - background: rgba(220, 220, 220, .35); - } -} - -.tb-json-content-panel { - height: 100%; - margin-left: 15px; - border: 1px solid #c0c0c0; - - #tb-json-input { - width: 100%; - min-width: 200px; - height: 100%; - - &:not(.fill-height) { - min-height: 200px; - } - } -} diff --git a/ui/src/app/components/json-content.tpl.html b/ui/src/app/components/json-content.tpl.html deleted file mode 100644 index 2b7670942d..0000000000 --- a/ui/src/app/components/json-content.tpl.html +++ /dev/null @@ -1,38 +0,0 @@ - -
    -
    - - - {{ - 'js-func.tidy' | translate }} - - {{ - 'js-func.mini' | translate }} - - -
    -
    -
    -
    -
    -
    diff --git a/ui/src/app/components/json-form.directive.js b/ui/src/app/components/json-form.directive.js deleted file mode 100644 index 9b3d05d850..0000000000 --- a/ui/src/app/components/json-form.directive.js +++ /dev/null @@ -1,286 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './json-form.scss'; - -import tinycolor from 'tinycolor2'; -import ObjectPath from 'objectpath'; -import inspector from 'schema-inspector'; -import ReactSchemaForm from './react/json-form-react.jsx'; -import jsonFormTemplate from './json-form.tpl.html'; -import { utils } from 'react-schema-form'; - -import MaterialIconsDialogController from './material-icons-dialog.controller'; -import materialIconsDialogTemplate from './material-icons-dialog.tpl.html'; - -export default angular.module('thingsboard.directives.jsonForm', []) - .directive('tbJsonForm', JsonForm) - .controller('MaterialIconsDialogController', MaterialIconsDialogController) - .value('ReactSchemaForm', ReactSchemaForm) - .name; - -/*@ngInject*/ -function JsonForm($compile, $templateCache, $mdColorPicker, $mdDialog, $document) { - - var linker = function (scope, element) { - - var template = $templateCache.get(jsonFormTemplate); - - element.html(template); - - var childScope; - - var destroyModelChangeWatches = function() { - if (scope.modelWatchHandle) { - scope.modelWatchHandle(); - } - if (scope.modelRefWatchHandle) { - scope.modelRefWatchHandle(); - } - } - - var initModelChangeWatches = function() { - scope.modelWatchHandle = scope.$watch('model',function(newValue, prevValue) { - if (newValue && prevValue && !angular.equals(newValue,prevValue)) { - scope.validate(); - if (scope.formControl) { - scope.formControl.$setDirty(); - } - } - }, true); - scope.modelRefWatchHandle = scope.$watch('model',function(newValue, prevValue) { - if (newValue && newValue != prevValue) { - scope.updateValues(); - } - }); - }; - - var recompile = function() { - if (childScope) { - childScope.$destroy(); - } - childScope = scope.$new(); - $compile(element.contents())(childScope); - } - - scope.isFullscreen = false; - - scope.formProps = { - isFullscreen: false, - option: { - formDefaults: { - startEmpty: true - } - }, - onModelChange: function(key, val) { - if (angular.isString(val) && val === '') { - val = undefined; - } - selectOrSet(key, scope.model, val); - scope.formProps.model = scope.model; - }, - onColorClick: function(event, key, val) { - scope.showColorPicker(event, val); - }, - onIconClick: function(event) { - scope.openIconDialog(event); - }, - onToggleFullscreen: function() { - scope.isFullscreen = !scope.isFullscreen; - scope.formProps.isFullscreen = scope.isFullscreen; - } - }; - - scope.showColorPicker = function (event, color) { - $mdColorPicker.show({ - value: tinycolor(color).toRgbString(), - defaultValue: '#fff', - random: tinycolor.random(), - clickOutsideToClose: false, - hasBackdrop: false, - multiple: true, - preserveScope: false, - - mdColorAlphaChannel: true, - mdColorSpectrum: true, - mdColorSliders: true, - mdColorGenericPalette: false, - mdColorMaterialPalette: true, - mdColorHistory: false, - mdColorDefaultTab: 2, - - $event: event - - }).then(function (color) { - if (event.data && event.data.onValueChanged) { - event.data.onValueChanged(tinycolor(color).toRgb()); - } - }); - } - - scope.openIconDialog = function(event) { - $mdDialog.show({ - controller: 'MaterialIconsDialogController', - controllerAs: 'vm', - templateUrl: materialIconsDialogTemplate, - parent: angular.element($document[0].body), - locals: {icon: scope.icon}, - multiple: true, - fullscreen: true, - targetEvent: event - }).then(function (icon) { - if (event.data && event.data.onValueChanged) { - event.data.onValueChanged(icon); - } - }); - } - - scope.onFullscreenChanged = function() {} - - scope.validate = function(){ - if (scope.schema && scope.model) { - var result = utils.validateBySchema(scope.schema, scope.model); - if (scope.formControl) { - scope.formControl.$setValidity('jsonForm', result.valid); - } - } - } - - scope.updateValues = function(skipRerender) { - destroyModelChangeWatches(); - if (!skipRerender) { - element.html(template); - } - var readonly = (scope.readonly && scope.readonly === true) ? true : false; - var schema = scope.schema ? angular.copy(scope.schema) : { - type: 'object' - }; - schema.strict = true; - var form = scope.form ? angular.copy(scope.form) : [ "*" ]; - var groupInfoes = scope.groupInfoes ? angular.copy(scope.groupInfoes) : []; - var model = scope.model || {}; - scope.model = inspector.sanitize(schema, model).data; - scope.formProps.option.formDefaults.readonly = readonly; - scope.formProps.schema = schema; - scope.formProps.form = form; - scope.formProps.groupInfoes = groupInfoes; - scope.formProps.model = angular.copy(scope.model); - if (!skipRerender) { - recompile(); - } - initModelChangeWatches(); - } - - scope.updateValues(true); - - scope.$watch('readonly',function() { - scope.updateValues(); - }); - - scope.$watch('schema',function(newValue, prevValue) { - if (newValue && newValue != prevValue) { - scope.updateValues(); - scope.validate(); - } - }); - - scope.$watch('form',function(newValue, prevValue) { - if (newValue && newValue != prevValue) { - scope.updateValues(); - } - }); - - scope.$watch('groupInfoes',function(newValue, prevValue) { - if (newValue && newValue != prevValue) { - scope.updateValues(); - } - }); - - scope.validate(); - - recompile(); - - } - - return { - restrict: "E", - scope: { - schema: '=', - form: '=', - model: '=', - formControl: '=', - groupInfoes: '=', - readonly: '=' - }, - link: linker - }; - -} - -function setValue(obj, key, val) { - var changed = false; - if (obj) { - if (angular.isUndefined(val)) { - if (angular.isDefined(obj[key])) { - delete obj[key]; - changed = true; - } - } else { - changed = !angular.equals(obj[key], val); - obj[key] = val; - } - } - return changed; -} - -function selectOrSet(projection, obj, valueToSet) { - var numRe = /^\d+$/; - - if (!obj) { - obj = this; - } - - if (!obj) { - return false; - } - - var parts = angular.isString(projection) ? ObjectPath.parse(projection) : projection; - - if (parts.length === 1) { - return setValue(obj, parts[0], valueToSet); - } - - if (angular.isUndefined(obj[parts[0]])) { - obj[parts[0]] = parts.length > 2 && numRe.test(parts[1]) ? [] : {}; - } - - var value = obj[parts[0]]; - for (var i = 1; i < parts.length; i++) { - if (parts[i] === '') { - return false; - } - if (i === parts.length - 1) { - return setValue(value, parts[i], valueToSet); - } else { - var tmp = value[parts[i]]; - if (angular.isUndefined(tmp) || tmp === null) { - tmp = numRe.test(parts[i + 1]) ? [] : {}; - value[parts[i]] = tmp; - } - value = tmp; - } - } - return value; -} diff --git a/ui/src/app/components/json-form.scss b/ui/src/app/components/json-form.scss deleted file mode 100644 index 27e31bcfef..0000000000 --- a/ui/src/app/components/json-form.scss +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -tb-json-form { - padding-bottom: 14px !important; - overflow: auto; -} diff --git a/ui/src/app/components/json-form.tpl.html b/ui/src/app/components/json-form.tpl.html deleted file mode 100644 index 7586c00974..0000000000 --- a/ui/src/app/components/json-form.tpl.html +++ /dev/null @@ -1,20 +0,0 @@ - -
    - -
    diff --git a/ui/src/app/components/json-object-edit.directive.js b/ui/src/app/components/json-object-edit.directive.js deleted file mode 100644 index 538b1cfc41..0000000000 --- a/ui/src/app/components/json-object-edit.directive.js +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './json-object-edit.scss'; - -import 'brace/ext/language_tools'; -import 'brace/ext/searchbox'; -import 'brace/mode/json'; -import 'brace/snippets/json'; - -import fixAceEditor from './ace-editor-fix'; - -/* eslint-disable import/no-unresolved, import/default */ - -import jsonObjectEditTemplate from './json-object-edit.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -export default angular.module('thingsboard.directives.jsonObjectEdit', []) - .directive('tbJsonObjectEdit', JsonObjectEdit) - .name; - -/*@ngInject*/ -function JsonObjectEdit($compile, $templateCache, $document, toast, utils) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - var template = $templateCache.get(jsonObjectEditTemplate); - element.html(template); - - scope.label = attrs.label; - - scope.objectValid = true; - scope.validationError = ''; - - scope.json_editor; - - scope.onFullscreenChanged = function () { - updateEditorSize(); - }; - - scope.beautifyJSON = function () { - scope.contentBody = angular.toJson(scope.object, 4); - }; - - scope.minifyJSON = function () { - scope.contentBody = angular.toJson(scope.object); - }; - - function updateEditorSize() { - if (scope.json_editor) { - scope.json_editor.resize(); - scope.json_editor.renderer.updateFull(); - } - } - - scope.jsonEditorOptions = { - useWrapMode: true, - mode: 'json', - advanced: { - enableSnippets: true, - enableBasicAutocompletion: true, - enableLiveAutocompletion: true - }, - onLoad: function (_ace) { - scope.json_editor = _ace; - scope.json_editor.session.on("change", function () { - scope.cleanupJsonErrors(); - }); - fixAceEditor(_ace); - } - }; - - scope.cleanupJsonErrors = function () { - toast.hide(); - }; - - scope.updateValidity = function () { - ngModelCtrl.$setValidity('objectValid', scope.objectValid); - }; - - scope.$watch('contentBody', function (newVal, prevVal) { - if (!angular.equals(newVal, prevVal)) { - var object = scope.validate(); - if (scope.objectValid) { - if (object == null) { - scope.object = null; - } else { - if (scope.object == null) { - scope.object = {}; - } - Object.keys(scope.object).forEach(function (key) { - delete scope.object[key]; - }); - Object.keys(object).forEach(function (key) { - scope.object[key] = object[key]; - }); - } - ngModelCtrl.$setViewValue(scope.object); - } - scope.updateValidity(); - } - }); - - ngModelCtrl.$render = function () { - scope.object = ngModelCtrl.$viewValue; - var content = ''; - try { - if (scope.object) { - content = angular.toJson(scope.object, true); - } - } catch (e) { - // - } - scope.contentBody = content; - }; - - scope.showError = function (error) { - var toastParent = angular.element('#tb-json-panel', element); - toast.showError(error, toastParent, 'bottom left'); - }; - - scope.validate = function () { - if (!scope.contentBody || !scope.contentBody.length) { - if (scope.required) { - scope.validationError = 'Json object is required.'; - scope.objectValid = false; - } else { - scope.validationError = ''; - scope.objectValid = true; - } - return null; - } else { - try { - var object = angular.fromJson(scope.contentBody); - scope.validationError = ''; - scope.objectValid = true; - return object; - } catch (e) { - var details = utils.parseException(e); - var errorInfo = 'Error:'; - if (details.name) { - errorInfo += ' ' + details.name + ':'; - } - if (details.message) { - errorInfo += ' ' + details.message; - } - scope.validationError = errorInfo; - scope.objectValid = false; - return null; - } - } - }; - - scope.$on('form-submit', function () { - if (!scope.readonly) { - scope.cleanupJsonErrors(); - if (!scope.objectValid) { - scope.showError(scope.validationError); - } - } - }); - - scope.$on('update-ace-editor-size', function () { - updateEditorSize(); - }); - - $compile(element.contents())(scope); - }; - - return { - restrict: "E", - require: "^ngModel", - scope: { - required:'=ngRequired', - readonly:'=ngReadonly', - fillHeight:'=?' - }, - link: linker - }; -} diff --git a/ui/src/app/components/json-object-edit.scss b/ui/src/app/components/json-object-edit.scss deleted file mode 100644 index 9c9cdc7e44..0000000000 --- a/ui/src/app/components/json-object-edit.scss +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -tb-json-object-edit { - position: relative; - - .fill-height { - height: 100%; - } -} - -.tb-json-object-edit-toolbar { - .md-button.tidy { - min-width: 32px; - min-height: 15px; - padding: 4px; - margin: 0 5px 0 0; - font-size: 12px; - line-height: 15px; - color: #7b7b7b; - background: rgba(220, 220, 220, .35); - } -} - -.tb-json-object-panel { - height: 100%; - margin-left: 15px; - border: 1px solid #c0c0c0; - - #tb-json-input { - width: 100%; - min-width: 200px; - height: 100%; - - &:not(.fill-height) { - min-height: 200px; - } - } -} diff --git a/ui/src/app/components/json-object-edit.tpl.html b/ui/src/app/components/json-object-edit.tpl.html deleted file mode 100644 index 86f2a55650..0000000000 --- a/ui/src/app/components/json-object-edit.tpl.html +++ /dev/null @@ -1,40 +0,0 @@ - -
    -
    - - - - {{'js-func.tidy' | translate }} - - - {{'js-func.mini' | translate }} - - -
    -
    -
    -
    -
    -
    diff --git a/ui/src/app/components/keyboard-shortcut.filter.js b/ui/src/app/components/keyboard-shortcut.filter.js deleted file mode 100644 index 27fb27e3a5..0000000000 --- a/ui/src/app/components/keyboard-shortcut.filter.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default angular.module('thingsboard.filters.keyboardShortcut', []) - .filter('keyboardShortcut', KeyboardShortcut) - .name; - -/*@ngInject*/ -function KeyboardShortcut($window) { - return function(str) { - if (!str) return; - var keys = str.split('-'); - var isOSX = /Mac OS X/.test($window.navigator.userAgent); - - var seperator = (!isOSX || keys.length > 2) ? '+' : ''; - - var abbreviations = { - M: isOSX ? '⌘' : 'Ctrl', - A: isOSX ? 'Option' : 'Alt', - S: 'Shift' - }; - - return keys.map(function(key, index) { - var last = index == keys.length - 1; - return last ? key : abbreviations[key]; - }).join(seperator); - }; -} diff --git a/ui/src/app/components/kv-map.directive.js b/ui/src/app/components/kv-map.directive.js deleted file mode 100644 index fb93375e23..0000000000 --- a/ui/src/app/components/kv-map.directive.js +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './kv-map.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import kvMapTemplate from './kv-map.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -export default angular.module('thingsboard.directives.keyValMap', []) - .directive('tbKeyValMap', KeyValMap) - .name; - -/*@ngInject*/ -function KeyValMap() { - return { - restrict: "E", - scope: true, - bindToController: { - disabled:'=ngDisabled', - titleText: '@?', - keyPlaceholderText: '@?', - valuePlaceholderText: '@?', - noDataText: '@?', - keyValMap: '=' - }, - controller: KeyValMapController, - controllerAs: 'vm', - templateUrl: kvMapTemplate - }; -} - -/*@ngInject*/ -function KeyValMapController($scope, $mdUtil) { - - let vm = this; - - vm.kvList = []; - - vm.removeKeyVal = removeKeyVal; - vm.addKeyVal = addKeyVal; - - $scope.$watch('vm.keyValMap', () => { - stopWatchKvList(); - vm.kvList.length = 0; - if (vm.keyValMap) { - for (var property in vm.keyValMap) { - if (Object.prototype.hasOwnProperty.call(vm.keyValMap, property)) { - vm.kvList.push( - { - key: property + '', - value: vm.keyValMap[property] - } - ); - } - } - } - $mdUtil.nextTick(() => { - watchKvList(); - }); - }); - - function watchKvList() { - $scope.kvListWatcher = $scope.$watch('vm.kvList', () => { - if (!vm.keyValMap) { - return; - } - for (var property in vm.keyValMap) { - if (Object.prototype.hasOwnProperty.call(vm.keyValMap, property)) { - delete vm.keyValMap[property]; - } - } - for (var i=0;i -1) { - vm.kvList.splice(index, 1); - } - } - - function addKeyVal() { - if (!vm.kvList) { - vm.kvList = []; - } - vm.kvList.push( - { - key: '', - value: '' - } - ); - } -} diff --git a/ui/src/app/components/kv-map.scss b/ui/src/app/components/kv-map.scss deleted file mode 100644 index 3e7aa2cde0..0000000000 --- a/ui/src/app/components/kv-map.scss +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-kv-map { - span.no-data-found { - position: relative; - display: flex; - height: 40px; - text-transform: uppercase; - - &.disabled { - color: rgba(0, 0, 0, .38); - } - } -} diff --git a/ui/src/app/components/kv-map.tpl.html b/ui/src/app/components/kv-map.tpl.html deleted file mode 100644 index 063f0746a8..0000000000 --- a/ui/src/app/components/kv-map.tpl.html +++ /dev/null @@ -1,58 +0,0 @@ - -
    - -
    - - - - - - - - - {{ 'key-val.remove-entry' | translate }} - - - close - - -
    - {{vm.noDataText ? vm.noDataText : 'key-val.no-data'}} -
    - - - {{ 'key-val.add-entry' | translate }} - - action.add - -
    -
    diff --git a/ui/src/app/components/led-light.directive.js b/ui/src/app/components/led-light.directive.js deleted file mode 100644 index 175a5054d8..0000000000 --- a/ui/src/app/components/led-light.directive.js +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import Raphael from 'raphael'; -import tinycolor from 'tinycolor2'; -import $ from 'jquery'; - -/* eslint-disable angular/angularelement */ - -export default angular.module('thingsboard.directives.ledLight', []) - .directive('tbLedLight', LedLight).name; - -/*@ngInject*/ -function LedLight($compile) { - - var linker = function (scope, element) { - scope.offOpacity = scope.offOpacity || "0.4"; - scope.glowColor = tinycolor(scope.colorOn).lighten().toHexString(); - - scope.$watch('tbEnabled',function() { - scope.draw(); - }); - - scope.$watch('size',function() { - scope.update(); - }); - - scope.draw = function () { - if (scope.tbEnabled) { - scope.circleElement.attr("fill", scope.colorOn); - scope.circleElement.attr("stroke", scope.colorOn); - scope.circleElement.attr("opacity", "1"); - - if (scope.circleElement.theGlow) { - scope.circleElement.theGlow.remove(); - } - - scope.circleElement.theGlow = scope.circleElement.glow( - { - color: scope.glowColor, - width: scope.radius + scope.glowSize, - opacity: 0.8, - fill: true - }); - } else { - if (scope.circleElement.theGlow) { - scope.circleElement.theGlow.remove(); - } - - /*scope.circleElement.theGlow = scope.circleElement.glow( - { - color: scope.glowColor, - width: scope.radius + scope.glowSize, - opacity: 0.4, - fill: true - });*/ - - scope.circleElement.attr("fill", scope.colorOff); - scope.circleElement.attr("stroke", scope.colorOff); - scope.circleElement.attr("opacity", scope.offOpacity); - } - } - - scope.update = function() { - scope.size = scope.size || 50; - scope.canvasSize = scope.size; - scope.radius = scope.canvasSize / 4; - scope.glowSize = scope.radius / 5; - - var template = '
    '; - element.html(template); - $compile(element.contents())(scope); - scope.paper = new Raphael($('#canvas_container', element)[0], scope.canvasSize, scope.canvasSize); - var center = scope.canvasSize / 2; - scope.circleElement = scope.paper.circle(center, center, scope.radius); - scope.draw(); - } - - scope.update(); - } - - - return { - restrict: "E", - link: linker, - scope: { - size: '=?', - colorOn: '=', - colorOff: '=', - offOpacity: '=?', - //glowColor: '=', - tbEnabled: '=' - } - }; - -} - -/* eslint-enable angular/angularelement */ diff --git a/ui/src/app/components/legend-config-button.tpl.html b/ui/src/app/components/legend-config-button.tpl.html deleted file mode 100644 index a1c4fc725b..0000000000 --- a/ui/src/app/components/legend-config-button.tpl.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - legend.settings - diff --git a/ui/src/app/components/legend-config-panel.controller.js b/ui/src/app/components/legend-config-panel.controller.js deleted file mode 100644 index 359aab1764..0000000000 --- a/ui/src/app/components/legend-config-panel.controller.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/*@ngInject*/ -export default function LegendConfigPanelController(mdPanelRef, $scope, types, legendConfig, onLegendConfigUpdate) { - - var vm = this; - vm._mdPanelRef = mdPanelRef; - vm.legendConfig = legendConfig; - vm.onLegendConfigUpdate = onLegendConfigUpdate; - vm.positions = types.position; - vm.directions = types.direction; - vm.isRowDirection = vm.legendConfig.direction === types.direction.row.value; - - vm._mdPanelRef.config.onOpenComplete = function () { - $scope.theForm.$setPristine(); - }; - - $scope.$watch('vm.legendConfig', function () { - if (onLegendConfigUpdate) { - onLegendConfigUpdate(vm.legendConfig); - } - }, true); -} diff --git a/ui/src/app/components/legend-config-panel.tpl.html b/ui/src/app/components/legend-config-panel.tpl.html deleted file mode 100644 index 399423d53a..0000000000 --- a/ui/src/app/components/legend-config-panel.tpl.html +++ /dev/null @@ -1,56 +0,0 @@ - -
    -
    - -
    - - - - - - {{direction.name | translate}} - - - - - - - - {{pos.name | translate}} - - - - {{ 'legend.show-min' | translate }} - - {{ 'legend.show-max' | translate }} - - {{ 'legend.show-avg' | translate }} - - {{ 'legend.show-total' | translate }} - - -
    -
    -
    -
    \ No newline at end of file diff --git a/ui/src/app/components/legend-config.directive.js b/ui/src/app/components/legend-config.directive.js deleted file mode 100644 index eaeb8c6cab..0000000000 --- a/ui/src/app/components/legend-config.directive.js +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './legend-config.scss'; - -import $ from 'jquery'; - -/* eslint-disable import/no-unresolved, import/default */ - -import legendConfigButtonTemplate from './legend-config-button.tpl.html'; -import legendConfigPanelTemplate from './legend-config-panel.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -import LegendConfigPanelController from './legend-config-panel.controller'; - - -export default angular.module('thingsboard.directives.legendConfig', []) - .controller('LegendConfigPanelController', LegendConfigPanelController) - .directive('tbLegendConfig', LegendConfig) - .name; - -/* eslint-disable angular/angularelement */ -/*@ngInject*/ -function LegendConfig($compile, $templateCache, types, $mdPanel, $document) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - - /* tbLegendConfig (ng-model) - * { - * position: types.position.bottom.value, - * showMin: false, - * showMax: false, - * showAvg: true, - * showTotal: false - * } - */ - - var template = $templateCache.get(legendConfigButtonTemplate); - element.html(template); - - scope.openEditMode = function (event) { - if (scope.disabled) { - return; - } - var position; - var panelHeight = 220; - var panelWidth = 220; - var offset = element[0].getBoundingClientRect(); - var bottomY = offset.bottom - $(window).scrollTop(); //eslint-disable-line - var leftX = offset.left - $(window).scrollLeft(); //eslint-disable-line - var yPosition; - var xPosition; - if (bottomY + panelHeight > $( window ).height()) { //eslint-disable-line - yPosition = $mdPanel.yPosition.ABOVE; - } else { - yPosition = $mdPanel.yPosition.BELOW; - } - if (leftX + panelWidth > $( window ).width()) { //eslint-disable-line - xPosition = $mdPanel.xPosition.ALIGN_END; - } else { - xPosition = $mdPanel.xPosition.ALIGN_START; - } - position = $mdPanel.newPanelPosition() - .relativeTo(element) - .addPanelPosition(xPosition, yPosition); - var config = { - attachTo: angular.element($document[0].body), - controller: 'LegendConfigPanelController', - controllerAs: 'vm', - templateUrl: legendConfigPanelTemplate, - panelClass: 'tb-legend-config-panel', - position: position, - fullscreen: false, - locals: { - 'legendConfig': angular.copy(scope.model), - 'onLegendConfigUpdate': function (legendConfig) { - scope.model = legendConfig; - scope.updateView(); - } - }, - openFrom: event, - clickOutsideToClose: true, - escapeToClose: true, - focusOnOpen: false - }; - $mdPanel.open(config); - } - - scope.updateView = function () { - var value = {}; - var model = scope.model; - value.direction = model.direction; - value.position = model.position; - value.showMin = model.showMin; - value.showMax = model.showMax; - value.showAvg = model.showAvg; - value.showTotal = model.showTotal; - ngModelCtrl.$setViewValue(value); - } - - ngModelCtrl.$render = function () { - if (ngModelCtrl.$viewValue) { - var value = ngModelCtrl.$viewValue; - if (!scope.model) { - scope.model = {}; - } - var model = scope.model; - model.direction = value.direction || types.direction.column.value; - model.position = value.position || types.position.bottom.value; - model.showMin = angular.isDefined(value.showMin) ? value.showMin : false; - model.showMax = angular.isDefined(value.showMax) ? value.showMax : false; - model.showAvg = angular.isDefined(value.showAvg) ? value.showAvg : true; - model.showTotal = angular.isDefined(value.showTotal) ? value.showTotal : false; - } else { - scope.model = { - direction: types.direction.column.value, - position: types.position.bottom.value, - showMin: false, - showMax: false, - showAvg: true, - showTotal: false - } - } - } - - $compile(element.contents())(scope); - } - - return { - restrict: "E", - require: "^ngModel", - scope: { - disabled:'=ngDisabled' - }, - link: linker - }; - -} - -/* eslint-enable angular/angularelement */ \ No newline at end of file diff --git a/ui/src/app/components/legend-config.scss b/ui/src/app/components/legend-config.scss deleted file mode 100644 index 654d197544..0000000000 --- a/ui/src/app/components/legend-config.scss +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.md-panel { - &.tb-legend-config-panel { - position: absolute; - } -} - -.tb-legend-config-panel { - min-width: 220px; - max-height: 300px; - overflow: hidden; - background: #fff; - border-radius: 4px; - box-shadow: - 0 7px 8px -4px rgba(0, 0, 0, .2), - 0 13px 19px 2px rgba(0, 0, 0, .14), - 0 5px 24px 4px rgba(0, 0, 0, .12); - - form, - fieldset { - height: 100%; - } - - md-content { - overflow: hidden; - background-color: #fff; - } - - .md-padding { - padding: 12px 16px 0; - } -} - -tb-legend-config { - span { - pointer-events: all; - cursor: pointer; - } -} diff --git a/ui/src/app/components/legend.directive.js b/ui/src/app/components/legend.directive.js deleted file mode 100644 index 060a2f0277..0000000000 --- a/ui/src/app/components/legend.directive.js +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './legend.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import legendTemplate from './legend.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - - -export default angular.module('thingsboard.directives.legend', []) - .directive('tbLegend', Legend) - .name; - -/*@ngInject*/ -function Legend($compile, $templateCache, types) { - - var linker = function (scope, element) { - var template = $templateCache.get(legendTemplate); - element.html(template); - - scope.displayHeader = function() { - return scope.legendConfig.showMin === true || - scope.legendConfig.showMax === true || - scope.legendConfig.showAvg === true || - scope.legendConfig.showTotal === true; - } - - scope.isHorizontal = scope.legendConfig.position === types.position.bottom.value || - scope.legendConfig.position === types.position.top.value; - - scope.isRowDirection = scope.legendConfig.direction === types.direction.row.value; - - scope.toggleHideData = function(index) { - if (!scope.legendData.keys[index].dataKey.settings.disableDataHiding) { - scope.legendData.keys[index].dataKey.hidden = !scope.legendData.keys[index].dataKey.hidden; - } - } - - $compile(element.contents())(scope); - - } - - /* scope.legendData = { - keys: [], - data: [] - - key: { - dataKey: dataKey, - dataIndex: 0 - } - data: { - min: null, - max: null, - avg: null, - total: null - } - };*/ - - return { - restrict: "E", - link: linker, - scope: { - legendConfig: '=', - legendData: '=' - } - }; -} diff --git a/ui/src/app/components/legend.scss b/ui/src/app/components/legend.scss deleted file mode 100644 index 5a13c773e2..0000000000 --- a/ui/src/app/components/legend.scss +++ /dev/null @@ -1,84 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -table.tb-legend { - font-size: 12px; - - .tb-legend-header, - .tb-legend-value { - text-align: right; - } - - .tb-legend-header { - th { - padding: 0 10px 1px 0; - color: rgb(255, 110, 64); - white-space: nowrap; - } - } - - .tb-legend-keys { - td.tb-legend-label, - td.tb-legend-value { - padding: 2px 10px; - white-space: nowrap; - } - - .tb-legend-type { - min-width: 50px; - padding: 0 10px 1px 0; - font-weight: 700; - color: rgb(255, 110, 64); - text-align: right; - white-space: nowrap; - } - - .tb-legend-line { - display: inline-block; - width: 15px; - height: 3px; - vertical-align: middle; - } - - .tb-legend-label { - padding: 2px 10px; - text-align: left; - white-space: nowrap; - outline: none; - - &.tb-horizontal { - width: 95%; - } - - &.tb-hidden-label { - text-decoration: line-through; - opacity: .6; - } - } - - &.tb-row-direction { - display: inline-block; - } - } -} - -table.tb-legend-column { - width: 100%; -} - -table.tb-legend-row { - width: auto; - margin-left: auto; -} diff --git a/ui/src/app/components/legend.tpl.html b/ui/src/app/components/legend.tpl.html deleted file mode 100644 index 3f9bc80d32..0000000000 --- a/ui/src/app/components/legend.tpl.html +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    {{ 'legend.min' | translate }}{{ 'legend.max' | translate }}{{ 'legend.avg' | translate }}{{ 'legend.total' | translate }}
    - {{ legendKey.dataKey.label }} - {{ legendData.data[legendKey.dataIndex].min }}{{ legendData.data[legendKey.dataIndex].max }}{{ legendData.data[legendKey.dataIndex].avg }}{{ legendData.data[legendKey.dataIndex].total }}
    - - - {{ legendKey.dataKey.label }} - -
    {{ 'legend.min' | translate }} - {{ legendData.data[legendKey.dataIndex].min }} -
    {{ 'legend.max' | translate }} - {{ legendData.data[legendKey.dataIndex].max }} -
    {{ 'legend.avg' | translate }} - {{ legendData.data[legendKey.dataIndex].avg }} -
    {{ 'legend.total' | translate }} - {{ legendData.data[legendKey.dataIndex].total }} -
    diff --git a/ui/src/app/components/material-icon-select.directive.js b/ui/src/app/components/material-icon-select.directive.js deleted file mode 100644 index d2cb787e32..0000000000 --- a/ui/src/app/components/material-icon-select.directive.js +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './material-icon-select.scss'; - -import MaterialIconsDialogController from './material-icons-dialog.controller'; - -/* eslint-disable import/no-unresolved, import/default */ - -import materialIconSelectTemplate from './material-icon-select.tpl.html'; -import materialIconsDialogTemplate from './material-icons-dialog.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - - -export default angular.module('thingsboard.directives.materialIconSelect', []) - .controller('MaterialIconsDialogController', MaterialIconsDialogController) - .directive('tbMaterialIconSelect', MaterialIconSelect) - .name; - -/*@ngInject*/ -function MaterialIconSelect($compile, $templateCache, $document, $mdDialog) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - var template = $templateCache.get(materialIconSelectTemplate); - element.html(template); - - scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false; - scope.icon = null; - - scope.updateView = function () { - ngModelCtrl.$setViewValue(scope.icon); - } - - ngModelCtrl.$render = function () { - if (ngModelCtrl.$viewValue) { - scope.icon = ngModelCtrl.$viewValue; - } - if (!scope.icon || !scope.icon.length) { - scope.icon = 'more_horiz'; - } - } - - scope.$watch('icon', function () { - scope.updateView(); - }); - - scope.openIconDialog = function($event) { - if ($event) { - $event.stopPropagation(); - } - $mdDialog.show({ - controller: 'MaterialIconsDialogController', - controllerAs: 'vm', - templateUrl: materialIconsDialogTemplate, - parent: angular.element($document[0].body), - locals: {icon: scope.icon}, - multiple: true, - fullscreen: true, - targetEvent: $event - }).then(function (icon) { - scope.icon = icon; - }); - } - - $compile(element.contents())(scope); - } - - return { - restrict: "E", - require: "^ngModel", - link: linker, - scope: { - tbRequired: '=?', - } - }; -} diff --git a/ui/src/app/components/material-icon-select.scss b/ui/src/app/components/material-icon-select.scss deleted file mode 100644 index 102b7b5dc6..0000000000 --- a/ui/src/app/components/material-icon-select.scss +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-material-icon-select { - md-icon { - padding: 4px; - margin: 8px 4px 4px; - cursor: pointer; - border: solid 1px rgba(0, 0, 0, .27); - } - - md-input-container { - margin-bottom: 0; - } -} diff --git a/ui/src/app/components/material-icon-select.tpl.html b/ui/src/app/components/material-icon-select.tpl.html deleted file mode 100644 index ae3b111ccc..0000000000 --- a/ui/src/app/components/material-icon-select.tpl.html +++ /dev/null @@ -1,24 +0,0 @@ - -
    - {{icon}} - - - - -
    \ No newline at end of file diff --git a/ui/src/app/components/material-icons-dialog.controller.js b/ui/src/app/components/material-icons-dialog.controller.js deleted file mode 100644 index 211f23d08c..0000000000 --- a/ui/src/app/components/material-icons-dialog.controller.js +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './material-icons-dialog.scss'; - -/*@ngInject*/ -export default function MaterialIconsDialogController($scope, $mdDialog, $timeout, utils, icon) { - - var vm = this; - - vm.selectedIcon = icon; - - vm.showAll = false; - vm.loadingIcons = false; - - $scope.$watch('vm.showAll', function(showAll) { - if (showAll) { - vm.loadingIcons = true; - $timeout(function() { - utils.getMaterialIcons().then( - function success(icons) { - vm.icons = icons; - } - ); - }); - } else { - vm.icons = utils.getCommonMaterialIcons(); - } - }); - - $scope.$on('iconsLoadFinished', function() { - vm.loadingIcons = false; - }); - - vm.cancel = cancel; - vm.selectIcon = selectIcon; - - function cancel() { - $mdDialog.cancel(); - } - - function selectIcon($event, icon) { - vm.selectedIcon = icon; - $mdDialog.hide(vm.selectedIcon); - } -} diff --git a/ui/src/app/components/material-icons-dialog.scss b/ui/src/app/components/material-icons-dialog.scss deleted file mode 100644 index 2d94711ce6..0000000000 --- a/ui/src/app/components/material-icons-dialog.scss +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-material-icons-dialog { - button.md-icon-button.tb-select-icon-button { - width: 56px; - height: 56px; - padding: 16px; - margin: 10px; - border: solid 1px #ffa500; - border-radius: 0%; - } - - .tb-icons-load { - top: 64px; - z-index: 3; - background: rgba(255, 255, 255, .75); - } -} diff --git a/ui/src/app/components/material-icons-dialog.tpl.html b/ui/src/app/components/material-icons-dialog.tpl.html deleted file mode 100644 index e6a6050c42..0000000000 --- a/ui/src/app/components/material-icons-dialog.tpl.html +++ /dev/null @@ -1,62 +0,0 @@ - - -
    - -
    -

    {{ 'icon.select-icon' | translate }}

    - -
    - - - -
    - - - -
    -
    - - -
    - -
    - -
    - -
    - - {{icon}} - - {{ icon }} - - -
    -
    -
    -
    - - - - {{ 'action.cancel' | translate }} - - -
    -
    diff --git a/ui/src/app/components/md-chip-draggable.directive.js b/ui/src/app/components/md-chip-draggable.directive.js deleted file mode 100644 index ca98bc0871..0000000000 --- a/ui/src/app/components/md-chip-draggable.directive.js +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './md-chip-draggable.scss'; - -var globalDraggingChipsWrapId = null; - -export default angular.module('thingsboard.directives.mdChipDraggable', []) - .directive('tbChipDraggable', function () { - return { - restrict: 'A', - scope: {}, - bindToController: true, - controllerAs: 'vm', - controller: ['$document', '$scope', '$element', '$timeout', - function ($document, $scope, $element, $timeout) { - var handle = $element[0]; - var draggingClassName = 'dragging'; - var droppingClassName = 'dropping'; - var droppingBeforeClassName = 'dropping-before'; - var droppingAfterClassName = 'dropping-after'; - var dragging = false; - var preventDrag = false; - var dropPosition; - var dropTimeout; - var counter = 0; - - var move = function (from, to) { - this.splice(to, 0, this.splice(from, 1)[0]); - }; - - $element = angular.element($element[0].closest('md-chip')); - - $element.attr('draggable', true); - - $element.on('mousedown', function (event) { - if (event.target !== handle) { - preventDrag = true; - } - }); - - $document.on('mouseup', function () { - preventDrag = false; - }); - - $element.on('dragstart', function (event) { - if (preventDrag) { - event.preventDefault(); - - } else { - dragging = true; - - globalDraggingChipsWrapId = angular.element($element[0].closest('md-chips-wrap')).attr('id'); - - $element.addClass(draggingClassName); - - var dataTransfer = event.dataTransfer || event.originalEvent.dataTransfer; - - dataTransfer.effectAllowed = 'copyMove'; - dataTransfer.dropEffect = 'move'; - - dataTransfer.setData('text/plain', $scope.$parent.$mdChipsCtrl.items.indexOf($scope.$parent.$chip)); - } - }); - - $element.on('dragend', function () { - dragging = false; - globalDraggingChipsWrapId = null; - - $element.removeClass(draggingClassName); - }); - - var dragOverHandler = function (event) { - if (dragging) { - return; - } - - var targetChipsWrapId = angular.element($element[0].closest('md-chips-wrap')).attr('id'); - - event.preventDefault(); - - if (globalDraggingChipsWrapId !== targetChipsWrapId) { - return; - } - - var bounds = $element[0].getBoundingClientRect(); - - var props = { - width: bounds.right - bounds.left, - height: bounds.bottom - bounds.top, - x: (event.originalEvent || event).clientX - bounds.left, - y: (event.originalEvent || event).clientY - bounds.top, - }; - - var horizontalOffset = props.x; - var horizontalMidPoint = props.width / 2; - - var verticalOffset = props.y; - var verticalMidPoint = props.height / 2; - - $element.addClass(droppingClassName); - - $element.removeClass(droppingAfterClassName); - $element.removeClass(droppingBeforeClassName); - - if (horizontalOffset >= horizontalMidPoint || verticalOffset >= verticalMidPoint) { - dropPosition = 'after'; - $element.addClass(droppingAfterClassName); - } else { - dropPosition = 'before'; - $element.addClass(droppingBeforeClassName); - } - - }; - - var dropHandler = function (event) { - counter = 0; - event.preventDefault(); - - var targetChipsWrapId = angular.element($element[0].closest('md-chips-wrap')).attr('id'); - if (globalDraggingChipsWrapId !== targetChipsWrapId) { - return; - } - - var droppedItemIndex = parseInt((event.dataTransfer || event.originalEvent.dataTransfer).getData('text/plain'), 10); - var currentIndex = $scope.$parent.$mdChipsCtrl.items.indexOf($scope.$parent.$chip); - var newIndex = null; - - if (dropPosition === 'before') { - if (droppedItemIndex < currentIndex) { - newIndex = currentIndex - 1; - } else { - newIndex = currentIndex; - } - } else { - if (droppedItemIndex < currentIndex) { - newIndex = currentIndex; - } else { - newIndex = currentIndex + 1; - } - } - - // prevent event firing multiple times in firefox - $timeout.cancel(dropTimeout); - dropTimeout = $timeout(function () { - dropPosition = null; - - move.apply($scope.$parent.$mdChipsCtrl.items, [droppedItemIndex, newIndex]); - - $scope.$apply(function () { - $scope.$emit('mdChipDraggable:change', { - collection: $scope.$parent.$mdChipsCtrl.items, - item: $scope.$parent.$mdChipsCtrl.items[droppedItemIndex], - from: droppedItemIndex, - to: newIndex, - }); - }); - - $element.removeClass(droppingClassName); - $element.removeClass(droppingAfterClassName); - $element.removeClass(droppingBeforeClassName); - - $element.off('drop', dropHandler); - }, 1000 / 16); - }; - - $element.on('dragenter', function () { - counter++; - if (dragging) { - return; - } - - $element.off('dragover', dragOverHandler); - $element.off('drop', dropHandler); - - $element.on('dragover', dragOverHandler); - $element.on('drop', dropHandler); - }); - - $element.on('dragleave', function () { - counter--; - if (counter <=0) { - counter = 0; - $element.removeClass(droppingClassName); - $element.removeClass(droppingAfterClassName); - $element.removeClass(droppingBeforeClassName); - } - }); - - }], - }; - }) - .name; - -/* eslint-enable angular/angularelement */ diff --git a/ui/src/app/components/md-chip-draggable.scss b/ui/src/app/components/md-chip-draggable.scss deleted file mode 100644 index 29989592bd..0000000000 --- a/ui/src/app/components/md-chip-draggable.scss +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -md-chip { - &.dropping { - //border: dashed 2px; - //opacity: .5; - - //.md-chip-content { - // margin: -2px; - //} - } - - &.dropping-before { - &::after { - position: absolute; - top: 0; - right: 50%; - bottom: 0; - left: 0; - content: ""; - background-color: #fff; - border: dashed 2px; - border-radius: 16px; - opacity: .7; - } - } - - &.dropping-after { - &::after { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 50%; - content: ""; - background-color: #fff; - border: dashed 2px; - border-radius: 16px; - opacity: .7; - } - } -} diff --git a/ui/src/app/components/menu-link.directive.js b/ui/src/app/components/menu-link.directive.js deleted file mode 100644 index 1dec872b14..0000000000 --- a/ui/src/app/components/menu-link.directive.js +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './menu-link.scss'; - -import thingsboardMenu from '../services/menu.service'; - -/* eslint-disable import/no-unresolved, import/default */ - -import menulinkTemplate from './menu-link.tpl.html'; -import menutoggleTemplate from './menu-toggle.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -export default angular.module('thingsboard.directives.menuLink', [thingsboardMenu]) - .directive('tbMenuLink', MenuLink) - .filter('nospace', NoSpace) - .name; - -/*@ngInject*/ -function MenuLink($compile, $templateCache, menu) { - - var linker = function (scope, element) { - var template; - - if (scope.section.type === 'link') { - template = $templateCache.get(menulinkTemplate); - } else { - template = $templateCache.get(menutoggleTemplate); - - var parentNode = element[0].parentNode.parentNode.parentNode; - if (parentNode.classList.contains('parent-list-item')) { - var heading = parentNode.querySelector('h2'); - element[0].firstChild.setAttribute('aria-describedby', heading.id); - } - - scope.sectionActive = function () { - return menu.sectionActive(scope.section); - }; - - scope.sectionHeight = function () { - return menu.sectionHeight(scope.section); - }; - } - - element.html(template); - - $compile(element.contents())(scope); - } - - return { - restrict: "E", - link: linker, - scope: { - section: '=' - } - }; -} - -function NoSpace() { - return function (value) { - return (!value) ? '' : value.replace(/ /g, ''); - } -} diff --git a/ui/src/app/components/menu-link.scss b/ui/src/app/components/menu-link.scss deleted file mode 100644 index 948aef1cb5..0000000000 --- a/ui/src/app/components/menu-link.scss +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "~compass-sass-mixins/lib/compass"; - -.md-button-toggle .md-toggle-icon.tb-toggled { - transform: rotateZ(180deg); -} - -.tb-menu-toggle-list.ng-hide { - max-height: 0; -} - -.tb-menu-toggle-list { - position: relative; - z-index: 1; - overflow: hidden; - - transition: .75s cubic-bezier(.35, 0, .25, 1); - - transition-property: height; -} diff --git a/ui/src/app/components/menu-link.tpl.html b/ui/src/app/components/menu-link.tpl.html deleted file mode 100644 index 8108b59452..0000000000 --- a/ui/src/app/components/menu-link.tpl.html +++ /dev/null @@ -1,22 +0,0 @@ - - - {{section.icon}} - {{section.name | translate}} - \ No newline at end of file diff --git a/ui/src/app/components/menu-toggle.tpl.html b/ui/src/app/components/menu-toggle.tpl.html deleted file mode 100644 index a38ab8f1cd..0000000000 --- a/ui/src/app/components/menu-toggle.tpl.html +++ /dev/null @@ -1,33 +0,0 @@ - - - {{section.icon}} - {{section.name | translate}} - - -
      -
    • - -
    • -
    diff --git a/ui/src/app/components/mousepoint-menu.directive.js b/ui/src/app/components/mousepoint-menu.directive.js deleted file mode 100644 index 302f473c80..0000000000 --- a/ui/src/app/components/mousepoint-menu.directive.js +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default angular.module('thingsboard.directives.mousepointMenu', []) - .directive('tbMousepointMenu', MousepointMenu) - .name; - -/*@ngInject*/ -function MousepointMenu() { - - var linker = function ($scope, $element, $attrs, RightClickContextMenu) { - - $scope.$mdOpenMousepointMenu = function($event){ - RightClickContextMenu.offsets = function(){ - var offset = $element.offset(); - var x = $event.pageX - offset.left; - var y = $event.pageY - offset.top; - if ($attrs.tbOffsetX) { - x += Number($attrs.tbOffsetX); - } - if ($attrs.tbOffsetY) { - y += Number($attrs.tbOffsetY); - } - var offsets = { - left: x, - top: y - } - return offsets; - } - RightClickContextMenu.open($event); - }; - - $scope.$mdCloseMousepointMenu = function() { - RightClickContextMenu.close(); - } - } - - return { - restrict: "A", - link: linker, - require: 'mdMenu' - }; -} diff --git a/ui/src/app/components/nav-tree.directive.js b/ui/src/app/components/nav-tree.directive.js deleted file mode 100644 index e91ab04b58..0000000000 --- a/ui/src/app/components/nav-tree.directive.js +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './nav-tree.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import navTreeTemplate from './nav-tree.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -export default angular.module('thingsboard.directives.navTree', []) - .directive('tbNavTree', NavTree) - .name; - -/*@ngInject*/ -function NavTree() { - return { - restrict: "E", - scope: true, - bindToController: { - loadNodes: '=', - editCallbacks: '=', - enableSearch: '@?', - onNodeSelected: '&', - onNodesInserted: '&', - searchCallback: '&?' - }, - controller: NavTreeController, - controllerAs: 'vm', - templateUrl: navTreeTemplate - }; -} - -/*@ngInject*/ -function NavTreeController($scope, $element, types) { - - var vm = this; - vm.types = types; - - $scope.$watch('vm.loadNodes', (newVal) => { - if (newVal) { - initTree(); - } - }); - - function initTree() { - var config = { - core: { - multiple: false, - check_callback: true, - themes: { name: 'proton', responsive: true }, - data: vm.loadNodes - } - }; - - if (vm.enableSearch) { - config.plugins = ["search"]; - config.search = { - case_sensitive: false, - show_only_matches: true, - show_only_matches_children: false, - search_leaves_only: false - }; - if (vm.searchCallback) { - config.search.search_callback = (searchText, node) => vm.searchCallback({searchText: searchText, node: node}); - } - } - - vm.treeElement = angular.element('.tb-nav-tree-container', $element) - .jstree(config); - - vm.treeElement.on("changed.jstree", function (e, data) { - if (vm.onNodeSelected) { - vm.onNodeSelected({node: data.instance.get_selected(true)[0], event: e}); - } - }); - - vm.treeElement.on("model.jstree", function (e, data) { - if (vm.onNodesInserted) { - vm.onNodesInserted({nodes: data.nodes, parent: data.parent}); - } - }); - - if (vm.editCallbacks) { - vm.editCallbacks.selectNode = (id) => { - var node = vm.treeElement.jstree('get_node', id); - if (node) { - vm.treeElement.jstree('deselect_all', true); - vm.treeElement.jstree('select_node', node); - } - }; - vm.editCallbacks.deselectAll = () => { - vm.treeElement.jstree('deselect_all'); - }; - vm.editCallbacks.getNode = (id) => { - var node = vm.treeElement.jstree('get_node', id); - return node; - }; - vm.editCallbacks.getParentNodeId = (id) => { - var node = vm.treeElement.jstree('get_node', id); - if (node) { - return vm.treeElement.jstree('get_parent', node); - } - }; - vm.editCallbacks.openNode = (id, cb) => { - var node = vm.treeElement.jstree('get_node', id); - if (node) { - vm.treeElement.jstree('open_node', node, cb); - } - }; - vm.editCallbacks.nodeIsOpen = (id) => { - var node = vm.treeElement.jstree('get_node', id); - if (node) { - return vm.treeElement.jstree('is_open', node); - } else { - return true; - } - }; - vm.editCallbacks.nodeIsLoaded = (id) => { - var node = vm.treeElement.jstree('get_node', id); - if (node) { - return vm.treeElement.jstree('is_loaded', node); - } else { - return true; - } - }; - vm.editCallbacks.refreshNode = (id) => { - if (id === '#') { - vm.treeElement.jstree('refresh'); - vm.treeElement.jstree('redraw'); - } else { - var node = vm.treeElement.jstree('get_node', id); - if (node) { - var opened = vm.treeElement.jstree('is_open', node); - vm.treeElement.jstree('refresh_node', node); - vm.treeElement.jstree('redraw'); - if (node.children && opened/* && !node.children.length*/) { - vm.treeElement.jstree('open_node', node); - } - } - } - }; - vm.editCallbacks.updateNode = (id, newName) => { - var node = vm.treeElement.jstree('get_node', id); - if (node) { - vm.treeElement.jstree('rename_node', node, newName); - } - }; - vm.editCallbacks.createNode = (parentId, node, pos) => { - var parentNode = vm.treeElement.jstree('get_node', parentId); - if (parentNode) { - vm.treeElement.jstree('create_node', parentNode, node, pos); - } - }; - vm.editCallbacks.deleteNode = (id) => { - var node = vm.treeElement.jstree('get_node', id); - if (node) { - vm.treeElement.jstree('delete_node', node); - } - }; - vm.editCallbacks.disableNode = (id) => { - var node = vm.treeElement.jstree('get_node', id); - if (node) { - vm.treeElement.jstree('disable_node', node); - } - }; - vm.editCallbacks.enableNode = (id) => { - var node = vm.treeElement.jstree('get_node', id); - if (node) { - vm.treeElement.jstree('enable_node', node); - } - }; - vm.editCallbacks.setNodeHasChildren = (id, hasChildren) => { - var node = vm.treeElement.jstree('get_node', id); - if (node) { - if (!node.children || !node.children.length) { - node.children = hasChildren; - node.state.loaded = !hasChildren; - node.state.opened = false; - vm.treeElement.jstree('_node_changed', node.id); - vm.treeElement.jstree('redraw'); - } - } - }; - vm.editCallbacks.search = (searchText) => { - vm.treeElement.jstree('search', searchText); - }; - vm.editCallbacks.clearSearch = () => { - vm.treeElement.jstree('clear_search'); - }; - } - } -} diff --git a/ui/src/app/components/nav-tree.scss b/ui/src/app/components/nav-tree.scss deleted file mode 100644 index df903e747b..0000000000 --- a/ui/src/app/components/nav-tree.scss +++ /dev/null @@ -1,346 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-nav-tree-container { - padding: 15px; - font-family: Roboto, "Helvetica Neue", sans-serif; - - &.jstree-proton { - .jstree-node, - .jstree-icon { - background-image: url("../../png/jstree/32px.png"); - } - - .jstree-last { - background: transparent; - } - - .jstree-themeicon-custom { - background-image: none; - - &.material-icons { - font-size: 18px; - } - } - - .jstree-anchor { - font-size: 16px; - } - } - - &.jstree-proton-small { - .jstree-node, - .jstree-icon { - background-image: url("../../png/jstree/32px.png"); - } - - .jstree-last { - background: transparent; - } - - .jstree-themeicon-custom { - background-image: none; - - &.material-icons { - font-size: 14px; - } - } - - .jstree-anchor { - font-size: 14px; - } - } - - &.jstree-proton-large { - .jstree-node, - .jstree-icon { - background-image: url("../../png/jstree/32px.png"); - } - - .jstree-last { - background: transparent; - } - - .jstree-themeicon-custom { - background-image: none; - - &.material-icons { - font-size: 24px; - } - } - - .jstree-anchor { - font-size: 20px; - } - } - - a { - border-bottom: none; - - i.jstree-themeicon-custom { - &.tb-user-group { - &::before { - content: "account_circle"; - } - } - - &.tb-customer-group { - &::before { - content: "supervisor_account"; - } - } - - &.tb-asset-group { - &::before { - content: "domain"; - } - } - - &.tb-device-group { - &::before { - content: "devices_other"; - } - } - - &.tb-entity-view-group { - &::before { - content: "view_quilt"; - } - } - - &.tb-dashboard-group { - &::before { - content: "dashboard"; - } - } - - &.tb-customer { - &::before { - content: "supervisor_account"; - } - } - } - } -} - -@media (max-width: 768px) { - .tb-nav-tree-container { - &.jstree-proton-responsive { - .jstree-node, - .jstree-icon, - .jstree-node > .jstree-ocl, - .jstree-themeicon, - .jstree-checkbox { - background-image: url("../../png/jstree/40px.png"); - background-size: 120px 240px; - } - - .jstree-container-ul { - overflow: visible; - } - - .jstree-themeicon-custom { - background-color: transparent; - background-image: none; - background-position: 0 0; - - &.material-icons { - margin: 0; - font-size: 24px; - } - } - - .jstree-node, - .jstree-leaf > .jstree-ocl { - background: 0 0; - } - - .jstree-node { - min-width: 40px; - min-height: 40px; - margin-left: 40px; - line-height: 40px; - white-space: nowrap; - background-repeat: repeat-y; - background-position: -80px 0; - } - - .jstree-last { - background: 0 0; - } - - .jstree-anchor { - height: 40px; - font-size: 1.1em; - font-weight: 700; - line-height: 40px; - text-shadow: 1px 1px #fff; - } - - .jstree-icon, - .jstree-icon:empty { - width: 40px; - height: 40px; - line-height: 40px; - } - - > { - .jstree-container-ul > .jstree-node { - margin-right: 0; - margin-left: 0; - } - } - - .jstree-ocl, - .jstree-themeicon, - .jstree-checkbox { - background-size: 120px 240px; - } - - .jstree-leaf > .jstree-ocl { - background: 0 0; - background-position: -40px -120px; - } - - .jstree-last > .jstree-ocl { - background-position: -40px -160px; - } - - .jstree-open > .jstree-ocl { - background-position: 0 0 !important; - } - - .jstree-closed > .jstree-ocl { - background-position: 0 -40px !important; - } - - .jstree-themeicon { - background-position: -40px -40px; - } - - .jstree-checkbox, - .jstree-checkbox:hover { - background-position: -40px -80px; - } - - &.jstree-checkbox-selection { - .jstree-clicked > .jstree-checkbox, - .jstree-clicked > .jstree-checkbox:hover { - background-position: 0 -80px; - } - } - - .jstree-checked > .jstree-checkbox, - .jstree-checked > .jstree-checkbox:hover { - background-position: 0 -80px; - } - - .jstree-anchor > .jstree-undetermined, - .jstree-anchor > .jstree-undetermined:hover { - background-position: 0 -120px; - } - - .jstree-striped { - background: 0 0; - } - - .jstree-wholerow { - height: 40px; - background: #ebebeb; - border-top: 1px solid rgba(255, 255, 255, .7); - border-bottom: 1px solid rgba(64, 64, 64, .2); - } - - .jstree-wholerow-hovered { - background: #e7f4f9; - } - - .jstree-wholerow-clicked { - background: #beebff; - } - - .jstree-children { - .jstree-last > .jstree-wholerow { - box-shadow: inset 0 -6px 3px -5px #666; - } - - .jstree-open > .jstree-wholerow { - border-top: 0; - box-shadow: inset 0 6px 3px -5px #666; - } - - .jstree-open + .jstree-open { - box-shadow: none; - } - } - - &.jstree-rtl { - .jstree-node { - margin-right: 40px; - margin-left: 0; - } - - .jstree-container-ul > .jstree-node { - margin-right: 0; - } - - .jstree-closed > .jstree-ocl { - background-position: -40px 0 !important; - } - } - } - } -} - -.tb-nav-tree .md-button.tb-active { - font-weight: 500; - background-color: rgba(255, 255, 255, .15); -} - -.tb-nav-tree, -.tb-nav-tree ul { - margin-top: 0; - list-style: none; - - &:first-child { - padding: 0; - } - - li { - .md-button { - width: 100%; - max-height: 40px; - padding: 0 16px; - margin: 0; - overflow: hidden; - line-height: 40px; - color: inherit; - text-align: left; - text-decoration: none; - text-overflow: ellipsis; - text-transform: none; - text-rendering: optimizeLegibility; - white-space: nowrap; - cursor: pointer; - border-radius: 0; - - span { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - } - } -} diff --git a/ui/src/app/components/no-animate.directive.js b/ui/src/app/components/no-animate.directive.js deleted file mode 100644 index ed3dda6d51..0000000000 --- a/ui/src/app/components/no-animate.directive.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import ngAnimate from 'angular-animate'; - -export default angular.module('thingsboard.directives.noAnimate', [ngAnimate]) - .directive('tbNoAnimate', NoAnimate) - .name; - -/*@ngInject*/ -function NoAnimate($animate) { - return { - restrict: 'A', - link: function (scope, element) { - $animate.enabled(element, false); - scope.$watch(function () { - $animate.enabled(element, false) - }); - } - }; -} diff --git a/ui/src/app/components/queue/index.js b/ui/src/app/components/queue/index.js deleted file mode 100644 index 5236603270..0000000000 --- a/ui/src/app/components/queue/index.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import thingsboardApiQueue from '../../api/queue.service'; -import queueTypeList from "./queue-type-list.directive"; - -export default angular.module('thingsboard.queue', [ - thingsboardApiQueue -]) - .directive('tbQueueTypeList', queueTypeList) - .name; diff --git a/ui/src/app/components/queue/queue-type-list.directive.js b/ui/src/app/components/queue/queue-type-list.directive.js deleted file mode 100644 index 6a2f99d280..0000000000 --- a/ui/src/app/components/queue/queue-type-list.directive.js +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './queue-type-list.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import queueTypeListTemplate from './queue-type-list.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function QueueTypeList($compile, $templateCache, $q, $filter, queueService) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - var template = $templateCache.get(queueTypeListTemplate); - element.html(template); - - scope.queues = null; - scope.queue = null; - scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false; - scope.queueSearchText = ''; - - scope.fetchQueues = function(searchText) { - var deferred = $q.defer(); - loadQueues().then( - function success(queueArr) { - let result = $filter('filter')(queueArr, {'$': searchText}); - if (result && result.length) { - if (searchText && searchText.length && result.indexOf(searchText) === -1) { - result.push(searchText); - } - result.sort(); - deferred.resolve(result); - } else { - deferred.resolve([searchText]); - } - }, - function fail() { - deferred.reject(); - } - ); - - return deferred.promise; - }; - - scope.updateView = function () { - if (!scope.disabled) { - ngModelCtrl.$setViewValue(scope.queue); - } - }; - - function loadQueues() { - var deferred = $q.defer(); - if (!scope.queues) { - queueService.getTenantQueuesByServiceType(scope.queueType).then( - function success(queueArr) { - scope.queues = queueArr.data; - deferred.resolve(scope.queues); - }, - function fail() { - deferred.reject(); - } - ); - } else { - deferred.resolve(scope.queues); - } - return deferred.promise; - } - - ngModelCtrl.$render = function () { - scope.queue = ngModelCtrl.$viewValue; - }; - - scope.$watch('queue', function (newValue, prevValue) { - if (!angular.equals(newValue, prevValue)) { - scope.updateView(); - } - }); - - scope.$watch('disabled', function () { - scope.updateView(); - }); - - $compile(element.contents())(scope); - }; - - return { - restrict: "E", - require: "^ngModel", - link: linker, - scope: { - theForm: '=?', - tbRequired: '=?', - disabled:'=ngDisabled', - queueType: '=?' - } - }; -} \ No newline at end of file diff --git a/ui/src/app/components/queue/queue-type-list.tpl.html b/ui/src/app/components/queue/queue-type-list.tpl.html deleted file mode 100644 index 5fc700026e..0000000000 --- a/ui/src/app/components/queue/queue-type-list.tpl.html +++ /dev/null @@ -1,43 +0,0 @@ - - - -
    - {{item}} -
    -
    -
    -
    {{'queue.name_required' | translate}}
    -
    -
    \ No newline at end of file diff --git a/ui/src/app/components/react/json-form-ace-editor.jsx b/ui/src/app/components/react/json-form-ace-editor.jsx deleted file mode 100644 index 2329bbddf7..0000000000 --- a/ui/src/app/components/react/json-form-ace-editor.jsx +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './json-form-ace-editor.scss'; - -import React from 'react'; -import ThingsboardBaseComponent from './json-form-base-component.jsx'; -import reactCSS from 'reactcss'; -import AceEditor from 'react-ace'; -import FlatButton from 'material-ui/FlatButton'; -import 'brace/ext/language_tools'; -import 'brace/ext/searchbox'; -import 'brace/theme/github'; - -import fixAceEditor from './../ace-editor-fix'; - -class ThingsboardAceEditor extends React.Component { - - constructor(props) { - super(props); - this.onValueChanged = this.onValueChanged.bind(this); - this.onBlur = this.onBlur.bind(this); - this.onFocus = this.onFocus.bind(this); - this.onTidy = this.onTidy.bind(this); - this.onLoad = this.onLoad.bind(this); - this.onToggleFull = this.onToggleFull.bind(this); - var value = props.value ? props.value + '' : ''; - this.state = { - isFull: false, - value: value, - focused: false - }; - } - - onValueChanged(value) { - this.setState({ - value: value - }); - this.props.onChangeValidate({ - target: { - value: value - } - }); - } - - onBlur() { - this.setState({ focused: false }) - } - - onFocus() { - this.setState({ focused: true }) - } - - onTidy() { - if (!this.props.form.readonly) { - var value = this.state.value; - value = this.props.onTidy(value); - this.setState({ - value: value - }) - this.props.onChangeValidate({ - target: { - value: value - } - }); - } - } - - onLoad(editor) { - this.aceEditor = editor; - fixAceEditor(editor); - } - - onToggleFull() { - this.setState({ isFull: !this.state.isFull }); - this.props.onToggleFullscreen(); - this.updateAceEditorSize = true; - } - - componentDidUpdate() { - if (this.updateAceEditorSize) { - if (this.aceEditor) { - this.aceEditor.resize(); - this.aceEditor.renderer.updateFull(); - } - this.updateAceEditorSize = false; - } - } - - render() { - - const styles = reactCSS({ - 'default': { - tidyButtonStyle: { - color: '#7B7B7B', - minWidth: '32px', - minHeight: '15px', - lineHeight: '15px', - fontSize: '0.800rem', - margin: '0', - padding: '4px', - height: '23px', - borderRadius: '5px', - marginLeft: '5px' - } - } - }); - - var labelClass = "tb-label"; - if (this.props.form.required) { - labelClass += " tb-required"; - } - if (this.props.form.readonly) { - labelClass += " tb-readonly"; - } - if (this.state.focused) { - labelClass += " tb-focused"; - } - var containerClass = "tb-container"; - var style = this.props.form.style || {width: '100%'}; - if (this.state.isFull) { - containerClass += " fullscreen-form-field"; - } - return ( -
    - -
    -
    - - - -
    - -
    -
    {this.props.error}
    -
    - ); - } -} - -export default ThingsboardBaseComponent(ThingsboardAceEditor); diff --git a/ui/src/app/components/react/json-form-ace-editor.scss b/ui/src/app/components/react/json-form-ace-editor.scss deleted file mode 100644 index 17c7ab87b6..0000000000 --- a/ui/src/app/components/react/json-form-ace-editor.scss +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.fullscreen-form-field { - .json-form-ace-editor { - height: calc(100% - 60px); - } -} - -.json-form-ace-editor { - position: relative; - height: 100%; - border: 1px solid #c0c0c0; - - .title-panel { - position: absolute; - top: 10px; - right: 20px; - z-index: 5; - font-size: .8rem; - font-weight: 500; - - label { - padding: 4px; - color: #00acc1; - text-transform: uppercase; - background: rgba(220, 220, 220, .35); - border-radius: 5px; - } - - button.tidy-button { - background: rgba(220, 220, 220, .35) !important; - - span { - padding: 0 !important; - font-size: 12px !important; - } - } - } -} diff --git a/ui/src/app/components/react/json-form-array.jsx b/ui/src/app/components/react/json-form-array.jsx deleted file mode 100644 index eb414d8619..0000000000 --- a/ui/src/app/components/react/json-form-array.jsx +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React from 'react'; -import { utils } from 'react-schema-form'; -import ThingsboardBaseComponent from './json-form-base-component.jsx'; -import RaisedButton from 'material-ui/RaisedButton'; -import FontIcon from 'material-ui/FontIcon'; -import _ from 'lodash'; -import IconButton from 'material-ui/IconButton'; - -class ThingsboardArray extends React.Component { - - constructor(props) { - super(props); - this.onAppend = this.onAppend.bind(this); - this.onDelete = this.onDelete.bind(this); - var model = utils.selectOrSet(this.props.form.key, this.props.model) || []; - var keys = []; - for (var i=0;i 0) { - key = newKeys[newKeys.length-1]+1; - } - newKeys.push(key); - this.setState({ - model: newModel, - keys: newKeys - } - ); - this.props.onChangeValidate(this.state.model); - } - - onDelete(index) { - var newModel = this.state.model; - newModel.splice(index, 1); - var newKeys = this.state.keys; - newKeys.splice(index, 1); - this.setState( - { - model: newModel, - keys: newKeys - } - ); - this.props.onChangeValidate(this.state.model); - } - - setIndex(index) { - return function(form) { - if (form.key) { - form.key[form.key.indexOf('')] = index; - } - }; - }; - - copyWithIndex(form, index) { - var copy = _.cloneDeep(form); - copy.arrayIndex = index; - utils.traverseForm(copy, this.setIndex(index)); - return copy; - }; - - render() { - var arrays = []; - var fields = []; - var model = this.state.model; - var keys = this.state.keys; - var items = this.props.form.items; - for(var i = 0; i < model.length; i++ ) { - let removeButton = ''; - if (!this.props.form.readonly) { - let boundOnDelete = this.onDelete.bind(this, i) - removeButton = clear - } - let forms = this.props.form.items.map(function(form, index){ - var copy = this.copyWithIndex(form, i); - return this.props.builder(copy, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.onIconClick, this.props.onToggleFullscreen, this.props.mapper, this.props.builder); - }.bind(this)); - arrays.push( -
  • - {removeButton} - {forms} -
  • - ); - } - let addButton = ''; - if (!this.props.form.readonly) { - addButton = add} - onTouchTap={this.onAppend}>; - } - - return ( -
    -
    -
    {this.props.form.title}
    -
      - {arrays} -
    -
    - {addButton} -
    - ); - } -} - -export default ThingsboardBaseComponent(ThingsboardArray); diff --git a/ui/src/app/components/react/json-form-base-component.jsx b/ui/src/app/components/react/json-form-base-component.jsx deleted file mode 100644 index 768e7504b6..0000000000 --- a/ui/src/app/components/react/json-form-base-component.jsx +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React from 'react'; -import { utils } from 'react-schema-form'; - -export default ThingsboardBaseComponent => class extends React.Component { - - constructor(props) { - super(props); - this.onChangeValidate = this.onChangeValidate.bind(this); - let value = this.defaultValue(); - let validationResult = utils.validate(this.props.form, value); - this.state = { - value: value, - valid: !!(validationResult.valid || !value), - error: !validationResult.valid && value ? validationResult.error.message : null - }; - } - - componentDidMount() { - if (typeof this.state.value !== 'undefined') { - this.props.onChange(this.props.form.key, this.state.value); - } - } - - onChangeValidate(e) { - let value = null; - if (this.props.form.schema.type === 'integer' || this.props.form.schema.type === 'number') { - if (e.target.value === null || e.target.value === '') { - value = undefined; - } else if (e.target.value.indexOf('.') == -1) { - value = parseInt(e.target.value); - } else { - value = parseFloat(e.target.value); - } - } else if(this.props.form.schema.type === 'boolean') { - value = e.target.checked; - } else if(this.props.form.schema.type === 'date' || this.props.form.schema.type === 'array') { - value = e; - } else { // string - value = e.target.value; - } - let validationResult = utils.validate(this.props.form, value); - this.setState({ - value: value, - valid: validationResult.valid, - error: validationResult.valid ? null : validationResult.error.message - }); - this.props.onChange(this.props.form.key, value); - } - - defaultValue() { - let value = utils.selectOrSet(this.props.form.key, this.props.model); - if (this.props.form.schema.type === 'boolean') { - if (typeof value !== 'boolean' && this.props.form['default']) { - value = this.props.form['default']; - } - if (typeof value !== 'boolean' && this.props.form.schema && this.props.form.schema['default']) { - value = this.props.form.schema['default']; - } - if (typeof value !== 'boolean' && - this.props.form.schema && - this.props.form.required) { - value = false; - } - } else if (this.props.form.schema.type === 'integer' || this.props.form.schema.type === 'number') { - if (typeof value !== 'number' && this.props.form['default']) { - value = this.props.form['default']; - } - if (typeof value !== 'number' && this.props.form.schema && this.props.form.schema['default']) { - value = this.props.form.schema['default']; - } - if (typeof value !== 'number' && this.props.form.titleMap && this.props.form.titleMap[0].value) { - value = this.props.form.titleMap[0].value; - } - if (value && typeof value === 'string') { - if (value.indexOf('.') == -1) { - value = parseInt(value); - } else { - value = parseFloat(value); - } - } - } else { - if(!value && this.props.form['default']) { - value = this.props.form['default']; - } - if(!value && this.props.form.schema && this.props.form.schema['default']) { - value = this.props.form.schema['default']; - } - if(!value && this.props.form.titleMap && this.props.form.titleMap[0].value) { - value = this.props.form.titleMap[0].value; - } - } - return value; - } - - render() { - if (this.props.form && this.props.form.schema) { - return ; - } else { - return
    ; - } - } -}; diff --git a/ui/src/app/components/react/json-form-checkbox.jsx b/ui/src/app/components/react/json-form-checkbox.jsx deleted file mode 100644 index 3a445da319..0000000000 --- a/ui/src/app/components/react/json-form-checkbox.jsx +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React from 'react'; -import ThingsboardBaseComponent from './json-form-base-component.jsx'; -import Checkbox from 'material-ui/Checkbox'; - -class ThingsboardCheckbox extends React.Component { - render() { - return ( - {this.props.onChangeValidate(e)}} - style={{paddingTop: '14px'}} - /> - ); - } -} - -export default ThingsboardBaseComponent(ThingsboardCheckbox); \ No newline at end of file diff --git a/ui/src/app/components/react/json-form-color.jsx b/ui/src/app/components/react/json-form-color.jsx deleted file mode 100644 index 518428099c..0000000000 --- a/ui/src/app/components/react/json-form-color.jsx +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './json-form-color.scss'; - -import $ from 'jquery'; -import React from 'react'; -import ReactDOM from 'react-dom'; -import ThingsboardBaseComponent from './json-form-base-component.jsx'; -import reactCSS from 'reactcss'; -import tinycolor from 'tinycolor2'; -import TextField from 'material-ui/TextField'; -import IconButton from 'material-ui/IconButton'; - -class ThingsboardColor extends React.Component { - - constructor(props) { - super(props); - this.onValueChanged = this.onValueChanged.bind(this); - this.onSwatchClick = this.onSwatchClick.bind(this); - this.onClear = this.onClear.bind(this); - var value = props.value ? props.value + '' : null; - var color = value != null ? tinycolor(value).toRgb() : null; - this.state = { - color: color - }; - } - - componentDidMount() { - var node = ReactDOM.findDOMNode(this); - var colContainer = $(node).children('#color-container'); - colContainer.click(this, function(event) { - if (!event.data.props.form.readonly) { - event.data.onSwatchClick(event); - } - }); - } - - componentWillUnmount () { - var node = ReactDOM.findDOMNode(this); - var colContainer = $(node).children('#color-container'); - colContainer.off( "click" ); - } - - onValueChanged(value) { - var color = null; - if (value != null) { - color = tinycolor(value); - } - this.setState({ - color: value - }) - var colorValue = ''; - if (color != null && color.getAlpha() != 1) { - colorValue = color.toRgbString(); - } else if (color != null) { - colorValue = color.toHexString(); - } - this.props.onChangeValidate({ - target: { - value: colorValue - } - }); - } - - onSwatchClick(event) { - this.props.onColorClick(event, this.props.form.key, this.state.color); - } - - onClear(event) { - if (event) { - event.stopPropagation(); - } - this.onValueChanged(null); - } - - render() { - - var background = 'rgba(0,0,0,0)'; - if (this.state.color != null) { - background = `rgba(${ this.state.color.r }, ${ this.state.color.g }, ${ this.state.color.b }, ${ this.state.color.a })`; - } - - const styles = reactCSS({ - 'default': { - color: { - background: `${ background }` - }, - swatch: { - display: 'inline-block', - marginRight: '10px', - marginTop: 'auto', - marginBottom: 'auto', - cursor: 'pointer', - opacity: `${ this.props.form.readonly ? '0.6' : '1' }` - }, - swatchText: { - display: 'inline-block', - width: '100%' - }, - container: { - display: 'flex' - }, - colorContainer: { - display: 'flex', - width: '100%' - } - }, - }); - - var fieldClass = "tb-field"; - if (this.props.form.required) { - fieldClass += " tb-required"; - } - if (this.props.form.readonly) { - fieldClass += " tb-readonly"; - } - if (this.state.focused) { - fieldClass += " tb-focused"; - } - - var stringColor = ''; - if (this.state.color != null) { - var color = tinycolor(this.state.color); - stringColor = color.toRgbString(); - } - - return ( -
    -
    -
    -
    -
    - -
    - clear -
    - ); - } -} - -export default ThingsboardBaseComponent(ThingsboardColor); diff --git a/ui/src/app/components/react/json-form-color.scss b/ui/src/app/components/react/json-form-color.scss deleted file mode 100644 index 21ef91f886..0000000000 --- a/ui/src/app/components/react/json-form-color.scss +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@mixin tb-checkered-bg() { - background-color: #fff; - background-image: - linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd), - linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd); - background-position: 0 0, 4px 4px; - background-size: 8px 8px; -} - -.tb-color-preview { - position: relative; - width: 24px; - height: 24px; - overflow: hidden; - content: ""; - border: 2px solid #fff; - border-radius: 50%; - box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .14), 0 2px 2px 0 rgba(0, 0, 0, .098), 0 1px 5px 0 rgba(0, 0, 0, .084); - - @include tb-checkered-bg(); - - .tb-color-result { - width: 100%; - height: 100%; - } -} diff --git a/ui/src/app/components/react/json-form-css.jsx b/ui/src/app/components/react/json-form-css.jsx deleted file mode 100644 index 8955c394a9..0000000000 --- a/ui/src/app/components/react/json-form-css.jsx +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React from 'react'; -import ThingsboardAceEditor from './json-form-ace-editor.jsx'; -import 'brace/mode/css'; -import beautify from 'js-beautify'; - -const css_beautify = beautify.css; - -class ThingsboardCss extends React.Component { - - constructor(props) { - super(props); - this.onTidyCss = this.onTidyCss.bind(this); - } - - onTidyCss(css) { - return css_beautify(css, {indent_size: 4}); - } - - render() { - return ( - - ); - } -} - -export default ThingsboardCss; diff --git a/ui/src/app/components/react/json-form-date.jsx b/ui/src/app/components/react/json-form-date.jsx deleted file mode 100644 index ae4a9015ea..0000000000 --- a/ui/src/app/components/react/json-form-date.jsx +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React from 'react'; -import ThingsboardBaseComponent from './json-form-base-component.jsx'; -import DatePicker from 'material-ui/DatePicker/DatePicker'; - -class ThingsboardDate extends React.Component { - - constructor(props) { - super(props); - this.onDatePicked = this.onDatePicked.bind(this); - } - - - onDatePicked(empty, date) { - this.props.onChangeValidate(date); - } - - render() { - - var fieldClass = "tb-date-field"; - if (this.props.form.required) { - fieldClass += " tb-required"; - } - if (this.props.form.readonly) { - fieldClass += " tb-readonly"; - } - - return ( -
    - - -
    - ); - } -} - -export default ThingsboardBaseComponent(ThingsboardDate); \ No newline at end of file diff --git a/ui/src/app/components/react/json-form-fieldset.jsx b/ui/src/app/components/react/json-form-fieldset.jsx deleted file mode 100644 index f668ba774c..0000000000 --- a/ui/src/app/components/react/json-form-fieldset.jsx +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React from 'react'; - -class ThingsboardFieldSet extends React.Component { - - render() { - let forms = this.props.form.items.map(function(form, index){ - return this.props.builder(form, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.onIconClick, this.props.onToggleFullscreen, this.props.mapper, this.props.builder); - }.bind(this)); - - return ( -
    -
    - {this.props.form.title} -
    -
    - {forms} -
    -
    - ); - } -} - -export default ThingsboardFieldSet; diff --git a/ui/src/app/components/react/json-form-html.jsx b/ui/src/app/components/react/json-form-html.jsx deleted file mode 100644 index 5500228a7a..0000000000 --- a/ui/src/app/components/react/json-form-html.jsx +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React from 'react'; -import ThingsboardAceEditor from './json-form-ace-editor.jsx'; -import 'brace/mode/html'; -import beautify from 'js-beautify'; - -const html_beautify = beautify.html; - -class ThingsboardHtml extends React.Component { - - constructor(props) { - super(props); - this.onTidyHtml = this.onTidyHtml.bind(this); - } - - onTidyHtml(html) { - return html_beautify(html, {indent_size: 4}); - } - - render() { - return ( - - ); - } -} - -export default ThingsboardHtml; diff --git a/ui/src/app/components/react/json-form-icon.jsx b/ui/src/app/components/react/json-form-icon.jsx deleted file mode 100644 index 58aacf17ad..0000000000 --- a/ui/src/app/components/react/json-form-icon.jsx +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import $ from 'jquery'; -import React from 'react'; -import ReactDOM from 'react-dom'; -import ThingsboardBaseComponent from './json-form-base-component.jsx'; -import reactCSS from 'reactcss'; -import TextField from 'material-ui/TextField'; -import IconButton from 'material-ui/IconButton'; - -class ThingsboardIcon extends React.Component { - - constructor(props) { - super(props); - this.onValueChanged = this.onValueChanged.bind(this); - this.onIconClick = this.onIconClick.bind(this); - this.onClear = this.onClear.bind(this); - var icon = props.value ? props.value : ''; - this.state = { - icon: icon - }; - } - - componentDidMount() { - var node = ReactDOM.findDOMNode(this); - var iconContainer = $(node).children('#icon-container'); - iconContainer.click(this, function(event) { - event.data.onIconClick(event); - }); - } - - componentWillUnmount () { - var node = ReactDOM.findDOMNode(this); - var iconContainer = $(node).children('#icon-container'); - iconContainer.off( "click" ); - } - - onValueChanged(value) { - var icon = value; - - this.setState({ - icon: value - }) - this.props.onChange(this.props.form.key, value); - } - - onIconClick(event) { - this.props.onIconClick(event); - } - - onClear(event) { - if (event) { - event.stopPropagation(); - } - this.onValueChanged(''); - } - - render() { - - const styles = reactCSS({ - 'default': { - clear: { - marginTop: '15px' - }, - container: { - display: 'flex' - }, - icon: { - display: 'inline-block', - marginRight: '10px', - marginTop: '16px', - marginBottom: 'auto', - cursor: 'pointer', - border: 'solid 1px rgba(0, 0, 0, .27)' - }, - iconContainer: { - display: 'flex', - width: '100%' - }, - iconText: { - display: 'inline-block', - width: '100%' - }, - }, - }); - - var fieldClass = "tb-field"; - if (this.props.form.required) { - fieldClass += " tb-required"; - } - if (this.state.focused) { - fieldClass += " tb-focused"; - } - - var pickedIcon = 'more_horiz'; - if (this.state.icon != '') { - pickedIcon = this.state.icon; - } - - return ( -
    -
    - - {pickedIcon} - - -
    - clear -
    - ); - } -} - -export default ThingsboardBaseComponent(ThingsboardIcon); diff --git a/ui/src/app/components/react/json-form-image.jsx b/ui/src/app/components/react/json-form-image.jsx deleted file mode 100644 index c25b8cbc7c..0000000000 --- a/ui/src/app/components/react/json-form-image.jsx +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './json-form-image.scss'; - -import React from 'react'; -import ThingsboardBaseComponent from './json-form-base-component.jsx'; -import Dropzone from 'react-dropzone'; -import IconButton from 'material-ui/IconButton'; - -class ThingsboardImage extends React.Component { - - constructor(props) { - super(props); - this.onValueChanged = this.onValueChanged.bind(this); - this.onDrop = this.onDrop.bind(this); - this.onClear = this.onClear.bind(this); - var value = props.value ? props.value + '' : null; - this.state = { - imageUrl: value - }; - } - - onValueChanged(value) { - this.setState({ - imageUrl: value - }); - this.props.onChangeValidate({ - target: { - value: value - } - }); - } - - onDrop(files) { - var reader = new FileReader(); - reader.onload = (function(tImg) { - return function(event) { - tImg.onValueChanged(event.target.result); - }; - })(this); - reader.readAsDataURL(files[0]); - } - - onClear(event) { - if (event) { - event.stopPropagation(); - } - this.onValueChanged(""); - } - - render() { - - var labelClass = "tb-label"; - if (this.props.form.required) { - labelClass += " tb-required"; - } - if (this.props.form.readonly) { - labelClass += " tb-readonly"; - } - if (this.state.focused) { - labelClass += " tb-focused"; - } - - var previewComponent; - if (this.state.imageUrl) { - previewComponent = ; - } else { - previewComponent =
    No image selected
    ; - } - - return ( -
    - -
    -
    {previewComponent}
    -
    - clear -
    - -
    Drop an image or click to select a file to upload.
    -
    -
    -
    - ); - } -} - -export default ThingsboardBaseComponent(ThingsboardImage); \ No newline at end of file diff --git a/ui/src/app/components/react/json-form-image.scss b/ui/src/app/components/react/json-form-image.scss deleted file mode 100644 index d578badfda..0000000000 --- a/ui/src/app/components/react/json-form-image.scss +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -$previewSize: 100px !default; - -.tb-image-select-container { - position: relative; - width: 100%; - height: $previewSize; -} - -.tb-image-preview { - width: 100%; - max-width: $previewSize; - height: 100%; - max-height: $previewSize; -} - -.tb-image-preview-container { - position: relative; - float: left; - width: $previewSize; - height: $previewSize; - margin-right: 12px; - vertical-align: top; - border: solid 1px; - - div { - position: absolute; - top: 50%; - left: 50%; - width: 100%; - font-size: 18px; - text-align: center; - transform: translate(-50%, -50%); - } -} - -.tb-dropzone { - position: relative; - height: $previewSize; - padding: 0 8px; - overflow: hidden; - vertical-align: top; - border: dashed 2px; - - div { - position: absolute; - top: 50%; - left: 50%; - width: 100%; - font-size: 24px; - text-align: center; - transform: translate(-50%, -50%); - } -} - -.tb-image-clear-container { - position: relative; - float: right; - width: 48px; - height: $previewSize; -} - -.tb-image-clear-btn { - position: absolute !important; - top: 50%; - transform: translate(0%, -50%) !important; -} diff --git a/ui/src/app/components/react/json-form-javascript.jsx b/ui/src/app/components/react/json-form-javascript.jsx deleted file mode 100644 index 73ba1706da..0000000000 --- a/ui/src/app/components/react/json-form-javascript.jsx +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React from 'react'; -import ThingsboardAceEditor from './json-form-ace-editor.jsx'; -import 'brace/mode/javascript'; -import beautify from 'js-beautify'; - -const js_beautify = beautify.js; - -class ThingsboardJavaScript extends React.Component { - - constructor(props) { - super(props); - this.onTidyJavascript = this.onTidyJavascript.bind(this); - } - - onTidyJavascript(javascript) { - return js_beautify(javascript, {indent_size: 4, wrap_line_length: 60}); - } - - render() { - return ( - - ); - } -} - -export default ThingsboardJavaScript; diff --git a/ui/src/app/components/react/json-form-json.jsx b/ui/src/app/components/react/json-form-json.jsx deleted file mode 100644 index 264d87345f..0000000000 --- a/ui/src/app/components/react/json-form-json.jsx +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React from 'react'; -import ThingsboardAceEditor from './json-form-ace-editor.jsx'; -import 'brace/mode/json'; -import beautify from 'js-beautify'; - -const js_beautify = beautify.js; - -class ThingsboardJson extends React.Component { - - constructor(props) { - super(props); - this.onTidyJson = this.onTidyJson.bind(this); - } - - onTidyJson(json) { - return js_beautify(json, {indent_size: 4}); - } - - render() { - return ( - - ); - } -} - -export default ThingsboardJson; diff --git a/ui/src/app/components/react/json-form-number.jsx b/ui/src/app/components/react/json-form-number.jsx deleted file mode 100644 index 6203eb209d..0000000000 --- a/ui/src/app/components/react/json-form-number.jsx +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React from 'react'; -import ThingsboardBaseComponent from './json-form-base-component.jsx'; -import NumberInput from 'material-ui-number-input'; - -class ThingsboardNumber extends React.Component { - - constructor(props) { - super(props); - this.preValidationCheck = this.preValidationCheck.bind(this); - this.onBlur = this.onBlur.bind(this); - this.onFocus = this.onFocus.bind(this); - this.state = { - lastSuccessfulValue : this.props.value, - focused: false - } - } - - isNumeric(n) { - return n === null || n === '' || !isNaN(n) && isFinite(n); - } - - onBlur() { - this.setState({ focused: false }) - } - - onFocus() { - this.setState({ focused: true }) - } - - preValidationCheck(e) { - if (this.isNumeric(e.target.value)) { - this.setState({ - lastSuccessfulValue: e.target.value - }); - this.props.onChangeValidate(e); - } - } - - render() { - - var fieldClass = "tb-field"; - if (this.props.form.required) { - fieldClass += " tb-required"; - } - if (this.props.form.readonly) { - fieldClass += " tb-readonly"; - } - if (this.state.focused) { - fieldClass += " tb-focused"; - } - var value = this.state.lastSuccessfulValue; - if (typeof value !== 'undefined') { - value = Number(value); - } else { - value = null; - } - return ( - - ); - } -} - -export default ThingsboardBaseComponent(ThingsboardNumber); \ No newline at end of file diff --git a/ui/src/app/components/react/json-form-rc-select.jsx b/ui/src/app/components/react/json-form-rc-select.jsx deleted file mode 100644 index 04de29cb79..0000000000 --- a/ui/src/app/components/react/json-form-rc-select.jsx +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import 'rc-select/assets/index.css'; - -import React from 'react'; -import ThingsboardBaseComponent from './json-form-base-component.jsx'; -import Select, {Option} from 'rc-select'; - -class ThingsboardRcSelect extends React.Component { - - constructor(props) { - super(props); - this.onSelect = this.onSelect.bind(this); - this.onDeselect = this.onDeselect.bind(this); - this.onBlur = this.onBlur.bind(this); - this.onFocus = this.onFocus.bind(this); - this.state = { - currentValue: this.keyToCurrentValue(this.props.value, this.props.form.schema.type === 'array'), - items: this.props.form.items, - focused: false - }; - } - - keyToCurrentValue(key, isArray) { - var currentValue = isArray ? [] : null; - if (isArray) { - var keys = key; - if (keys) { - for (var i = 0; i < keys.length; i++) { - currentValue.push({key: keys[i], label: this.labelFromKey(keys[i])}); - } - } - } else { - currentValue = {key: key, label: this.labelFromKey(key)}; - } - return currentValue; - } - - labelFromKey(key) { - let label = key || ''; - if (key) { - for (var i=0;i -1) { - v.splice(index, 1); - } - this.setState({ - currentValue: v - }); - this.props.onChangeValidate(this.arrayValues(v)); - } - } - - onBlur() { - this.setState({ focused: false }) - } - - onFocus() { - this.setState({ focused: true }) - } - - render() { - let options = []; - if(this.state.items && this.state.items.length > 0) { - options = this.state.items.map((item, idx) => ( - - )); - } - - var labelClass = "tb-label"; - if (this.props.form.required) { - labelClass += " tb-required"; - } - if (this.props.form.readonly) { - labelClass += " tb-readonly"; - } - if (this.state.focused) { - labelClass += " tb-focused"; - } - - return ( -
    - - -
    {this.props.error}
    -
    - ); - } -} - -export default ThingsboardBaseComponent(ThingsboardRcSelect); diff --git a/ui/src/app/components/react/json-form-react.jsx b/ui/src/app/components/react/json-form-react.jsx deleted file mode 100644 index b7a42a101b..0000000000 --- a/ui/src/app/components/react/json-form-react.jsx +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './json-form.scss'; - -import React from 'react'; -import getMuiTheme from 'material-ui/styles/getMuiTheme'; -import thingsboardTheme from './styles/thingsboardTheme'; -import ThingsboardSchemaForm from './json-form-schema-form.jsx'; - -class ReactSchemaForm extends React.Component { - - getChildContext() { - return { - muiTheme: this.state.muiTheme - }; - } - - constructor(props) { - super(props); - this.state = { - muiTheme: getMuiTheme(thingsboardTheme) - }; - } - - render () { - if (this.props.form.length > 0) { - return ; - } else { - return
    ; - } - } -} - -ReactSchemaForm.propTypes = { - schema: React.PropTypes.object, - form: React.PropTypes.array, - groupInfoes: React.PropTypes.array, - model: React.PropTypes.object, - option: React.PropTypes.object, - onModelChange: React.PropTypes.func, - onColorClick: React.PropTypes.func, - onIconClick: React.PropTypes.func, - onToggleFullscreen: React.PropTypes.func -} - -ReactSchemaForm.defaultProps = { - schema: {}, - form: [ "*" ], - groupInfoes:[] -} - -ReactSchemaForm.childContextTypes = { - muiTheme: React.PropTypes.object -} - -export default ReactSchemaForm; diff --git a/ui/src/app/components/react/json-form-schema-form.jsx b/ui/src/app/components/react/json-form-schema-form.jsx deleted file mode 100644 index 04970f601b..0000000000 --- a/ui/src/app/components/react/json-form-schema-form.jsx +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React from 'react'; -import { utils } from 'react-schema-form'; - -import ThingsboardArray from './json-form-array.jsx'; -import ThingsboardJavaScript from './json-form-javascript.jsx'; -import ThingsboardJson from './json-form-json.jsx'; -import ThingsboardHtml from './json-form-html.jsx'; -import ThingsboardCss from './json-form-css.jsx'; -import ThingsboardColor from './json-form-color.jsx' -import ThingsboardRcSelect from './json-form-rc-select.jsx'; -import ThingsboardNumber from './json-form-number.jsx'; -import ThingsboardText from './json-form-text.jsx'; -import Select from 'react-schema-form/lib/Select'; -import Radios from 'react-schema-form/lib/Radios'; -import ThingsboardDate from './json-form-date.jsx'; -import ThingsboardImage from './json-form-image.jsx'; -import ThingsboardCheckbox from './json-form-checkbox.jsx'; -import Help from 'react-schema-form/lib/Help'; -import ThingsboardFieldSet from './json-form-fieldset.jsx'; -import ThingsboardIcon from './json-form-icon.jsx'; - -import _ from 'lodash'; - -class ThingsboardSchemaForm extends React.Component { - - constructor(props) { - super(props); - - this.mapper = { - 'number': ThingsboardNumber, - 'text': ThingsboardText, - 'password': ThingsboardText, - 'textarea': ThingsboardText, - 'select': Select, - 'radios': Radios, - 'date': ThingsboardDate, - 'image': ThingsboardImage, - 'checkbox': ThingsboardCheckbox, - 'help': Help, - 'array': ThingsboardArray, - 'javascript': ThingsboardJavaScript, - 'json': ThingsboardJson, - 'html': ThingsboardHtml, - 'css': ThingsboardCss, - 'color': ThingsboardColor, - 'rc-select': ThingsboardRcSelect, - 'fieldset': ThingsboardFieldSet, - 'icon': ThingsboardIcon - }; - - this.onChange = this.onChange.bind(this); - this.onColorClick = this.onColorClick.bind(this); - this.onIconClick = this.onIconClick.bind(this); - this.onToggleFullscreen = this.onToggleFullscreen.bind(this); - this.hasConditions = false; - } - - onChange(key, val) { - this.props.onModelChange(key, val); - if (this.hasConditions) { - this.forceUpdate(); - } - } - - onColorClick(event, key, val) { - this.props.onColorClick(event, key, val); - } - - onIconClick(event) { - this.props.onIconClick(event); - } - - onToggleFullscreen() { - this.props.onToggleFullscreen(); - } - - - builder(form, model, index, onChange, onColorClick, onIconClick, onToggleFullscreen, mapper) { - var type = form.type; - let Field = this.mapper[type]; - if(!Field) { - console.log('Invalid field: \"' + form.key[0] + '\"!'); - return null; - } - if(form.condition) { - this.hasConditions = true; - if (eval(form.condition) === false) { - return null; - } - } - return - } - - createSchema(theForm) { - let merged = utils.merge(this.props.schema, theForm, this.props.ignore, this.props.option); - let mapper = this.mapper; - if(this.props.mapper) { - mapper = _.merge(this.mapper, this.props.mapper); - } - let forms = merged.map(function(form, index) { - return this.builder(form, this.props.model, index, this.onChange, this.onColorClick, this.onIconClick, this.onToggleFullscreen, mapper); - }.bind(this)); - - let formClass = 'SchemaForm'; - if (this.props.isFullscreen) { - formClass += ' SchemaFormFullscreen'; - } - - return ( -
    {forms}
    - ); - } - - render() { - if(this.props.groupInfoes&&this.props.groupInfoes.length>0){ - let content=[]; - for(let info of this.props.groupInfoes){ - let forms = this.createSchema(this.props.form[info.formIndex]); - let item = ; - content.push(item); - } - return (
    {content}
    ); - } - else - return this.createSchema(this.props.form); - } -} -export default ThingsboardSchemaForm; - - -class ThingsboardSchemaGroup extends React.Component{ - constructor(props) { - super(props); - this.state={ - showGroup:true - } - } - - toogleGroup(index) { - this.setState({ - showGroup:!this.state.showGroup - }); - } - - render() { - let theCla = "pull-right fa fa-chevron-down md-toggle-icon"+(this.state.showGroup?"":" tb-toggled") - return (
    -
    {this.props.info.GroupTitle}
    -
    {this.props.forms}
    -
    ); - } -} diff --git a/ui/src/app/components/react/json-form-text.jsx b/ui/src/app/components/react/json-form-text.jsx deleted file mode 100644 index 3fead4f8f8..0000000000 --- a/ui/src/app/components/react/json-form-text.jsx +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React from 'react'; -import ThingsboardBaseComponent from './json-form-base-component.jsx'; -import TextField from 'material-ui/TextField'; - -class ThingsboardText extends React.Component { - - constructor(props) { - super(props); - this.onBlur = this.onBlur.bind(this); - this.onFocus = this.onFocus.bind(this); - this.state = { - focused: false - }; - } - - onBlur() { - this.setState({ focused: false }) - } - - onFocus() { - this.setState({ focused: true }) - } - - render() { - - var fieldClass = "tb-field"; - if (this.props.form.required) { - fieldClass += " tb-required"; - } - if (this.props.form.readonly) { - fieldClass += " tb-readonly"; - } - if (this.state.focused) { - fieldClass += " tb-focused"; - } - - var multiline = this.props.form.type === 'textarea'; - var rows = multiline ? this.props.form.rows : 1; - var rowsMax = multiline ? this.props.form.rowsMax : 1; - - return ( -
    - -
    - ); - } -} - -export default ThingsboardBaseComponent(ThingsboardText); \ No newline at end of file diff --git a/ui/src/app/components/react/json-form.scss b/ui/src/app/components/react/json-form.scss deleted file mode 100644 index 85825abf42..0000000000 --- a/ui/src/app/components/react/json-form.scss +++ /dev/null @@ -1,154 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "~compass-sass-mixins/lib/compass"; - -$swift-ease-out-duration: .4s !default; -$swift-ease-out-timing-function: cubic-bezier(.25, .8, .25, 1) !default; - -$input-label-float-offset: 6px !default; -$input-label-float-scale: .75 !default; - -.tb-fullscreen { - [name="ReactSchemaForm"] { - .SchemaForm { - &.SchemaFormFullscreen { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - - > div:not(.fullscreen-form-field) { - display: none !important; - } - - > div.fullscreen-form-field { - position: relative; - width: 100%; - height: 100%; - } - } - } - - > div { - > section { - margin: 0 !important; - box-shadow: none !important; - - .SchemaGroupname { - display: none !important; - } - - > div { - padding: 0 !important; - } - } - } - } -} - - -.json-form-error { - position: relative; - bottom: -5px; - font-size: 12px; - line-height: 12px; - color: rgb(244, 67, 54); - - transition: all 450ms cubic-bezier(.23, 1, .32, 1) 0ms; -} - -.tb-container { - position: relative; - padding: 10px 0; - margin-top: 32px; -} - -.tb-field { - &.tb-required { - label::after { - font-size: 13px; - color: rgba(0, 0, 0, .54); - vertical-align: top; - content: " *"; - } - } - - &.tb-focused:not(.tb-readonly) { - label::after { - color: rgb(221, 44, 0); - } - } -} - -.tb-date-field { - &.tb-required { - div > div:first-child::after { - font-size: 13px; - color: rgba(0, 0, 0, .54); - vertical-align: top; - content: " *"; - } - } - - &.tb-focused:not(.tb-readonly) { - div > div:first-child::after { - color: rgb(221, 44, 0); - } - } -} - -label.tb-label { - position: absolute; - right: auto; - bottom: 100%; - left: 0; - color: rgba(0, 0, 0, .54); - - transition: transform $swift-ease-out-timing-function $swift-ease-out-duration, width $swift-ease-out-timing-function $swift-ease-out-duration; - - transform: translate3d(0, $input-label-float-offset, 0) scale($input-label-float-scale); - transform-origin: left top; - -webkit-font-smoothing: antialiased; - - &.tb-focused { - color: rgb(96, 125, 139); - } - - &.tb-required::after { - font-size: 13px; - color: rgba(0, 0, 0, .54); - vertical-align: top; - content: " *"; - } - - &.tb-focused:not(.tb-readonly)::after { - color: rgb(221, 44, 0); - } -} - -.tb-head-label { - color: rgba(0, 0, 0, .54); -} - -.SchemaGroupname{ - padding: 10px 20px; - background-color: #f1f1f1; -} - -.invisible{ - display: none; -} diff --git a/ui/src/app/components/react/styles/thingsboardTheme.js b/ui/src/app/components/react/styles/thingsboardTheme.js deleted file mode 100644 index ed3a84d32e..0000000000 --- a/ui/src/app/components/react/styles/thingsboardTheme.js +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { /*blueGrey500, blueGrey700, blueGrey100, orange500,*/ - grey100, grey500, grey900, grey600, white, grey400, darkBlack, cyan500, fullBlack/*, indigo500*/, indigo700, indigo100, deepOrange500 } from 'material-ui/styles/colors'; -import {fade} from 'material-ui/utils/colorManipulator'; -import spacing from 'material-ui/styles/spacing'; - -const PRIMARY_BACKGROUND_COLOR = "#305680";//"#3f51b5"; - -/*var blueGrayPalette = { - primary1Color: blueGrey500, - primary2Color: blueGrey700, - primary3Color: blueGrey100, - accent1Color: orange500, - accent2Color: grey100, - accent3Color: grey500, - textColor: grey900, - secondaryTextColor: grey600, - alternateTextColor: white, - canvasColor: white, - borderColor: grey400, - disabledColor: fade(darkBlack, 0.3), - pickerHeaderColor: cyan500, - clockCircleColor: fade(darkBlack, 0.07), - shadowColor: fullBlack, -};*/ - -var indigoPalette = { - primary1Color: PRIMARY_BACKGROUND_COLOR, - primary2Color: indigo700, - primary3Color: indigo100, - accent1Color: deepOrange500, - accent2Color: grey100, - accent3Color: grey500, - textColor: grey900, - secondaryTextColor: grey600, - alternateTextColor: white, - canvasColor: white, - borderColor: grey400, - disabledColor: fade(darkBlack, 0.3), - pickerHeaderColor: cyan500, - clockCircleColor: fade(darkBlack, 0.07), - shadowColor: fullBlack, -}; - -export default { - spacing: spacing, - fontFamily: 'Roboto, \'Helvetica Neue\', sans-serif', - palette: indigoPalette, -}; \ No newline at end of file diff --git a/ui/src/app/components/related-entity-autocomplete.directive.js b/ui/src/app/components/related-entity-autocomplete.directive.js deleted file mode 100644 index 4fc131866d..0000000000 --- a/ui/src/app/components/related-entity-autocomplete.directive.js +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './related-entity-autocomplete.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import relatedEntityAutocompleteTemplate from './related-entity-autocomplete.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - - -export default angular.module('thingsboard.directives.relatedEntityAutocomplete', []) - .directive('tbRelatedEntityAutocomplete', RelatedEntityAutocomplete) - .name; - -/*@ngInject*/ -function RelatedEntityAutocomplete($compile, $templateCache, $q, $filter, entityService) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - var template = $templateCache.get(relatedEntityAutocompleteTemplate); - element.html(template); - - scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false; - scope.entity = null; - scope.entitySearchText = ''; - - scope.allEntities = null; - - scope.fetchEntities = function(searchText) { - var deferred = $q.defer(); - if (!scope.allEntities) { - entityService.getRelatedEntities(scope.rootEntityId, scope.entityType, scope.entitySubtypes, -1, []).then( - function success(entities) { - if (scope.excludeEntityIds && scope.excludeEntityIds.length) { - var filteredEntities = []; - entities.forEach(function(entity) { - if (scope.excludeEntityIds.indexOf(entity.id.id) == -1) { - filteredEntities.push(entity); - } - }); - entities = filteredEntities; - } - scope.allEntities = entities; - filterEntities(searchText, deferred); - }, - function fail() { - deferred.reject(); - } - ); - } else { - filterEntities(searchText, deferred); - } - return deferred.promise; - } - - function filterEntities(searchText, deferred) { - var result = $filter('filter')(scope.allEntities, {name: searchText}); - deferred.resolve(result); - } - - scope.entitySearchTextChanged = function() { - } - - scope.updateView = function () { - if (!scope.disabled) { - ngModelCtrl.$setViewValue(scope.entity ? scope.entity.id : null); - } - } - - ngModelCtrl.$render = function () { - if (ngModelCtrl.$viewValue) { - entityService.getRelatedEntity(ngModelCtrl.$viewValue).then( - function success(entity) { - scope.entity = entity; - }, - function fail() { - scope.entity = null; - } - ); - } else { - scope.entity = null; - } - } - - scope.$watch('entity', function () { - scope.updateView(); - }); - - scope.$watch('disabled', function () { - scope.updateView(); - }); - - $compile(element.contents())(scope); - } - - return { - restrict: "E", - require: "^ngModel", - link: linker, - scope: { - rootEntityId: '=', - entityType: '=', - entitySubtypes: '=', - excludeEntityIds: '=?', - theForm: '=?', - tbRequired: '=?', - disabled:'=ngDisabled', - placeholderText: '@', - notFoundText: '@', - requiredText: '@' - } - }; -} diff --git a/ui/src/app/components/related-entity-autocomplete.scss b/ui/src/app/components/related-entity-autocomplete.scss deleted file mode 100644 index f4e3dea190..0000000000 --- a/ui/src/app/components/related-entity-autocomplete.scss +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-related-entity-autocomplete { - .tb-not-found { - display: block; - height: 48px; - line-height: 1.5; - } - - .tb-entity-item { - display: block; - height: 48px; - } - - li { - height: auto !important; - white-space: normal !important; - } -} diff --git a/ui/src/app/components/related-entity-autocomplete.tpl.html b/ui/src/app/components/related-entity-autocomplete.tpl.html deleted file mode 100644 index 9bb8c492bb..0000000000 --- a/ui/src/app/components/related-entity-autocomplete.tpl.html +++ /dev/null @@ -1,43 +0,0 @@ - - - -
    - {{item.name}} -
    -
    - -
    - {{ notFoundText | translate:{entity: entitySearchText} }} -
    -
    -
    -
    {{ requiredText | translate }}
    -
    -
    diff --git a/ui/src/app/components/scope-element.directive.js b/ui/src/app/components/scope-element.directive.js deleted file mode 100644 index e354828da6..0000000000 --- a/ui/src/app/components/scope-element.directive.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default angular.module('thingsboard.directives.scopeElement', []) - .directive('tbScopeElement', ScopeElement) - .name; - -/*@ngInject*/ -function ScopeElement() { - var directiveDefinitionObject = { - restrict: "A", - compile: function compile() { - return { - pre: function preLink(scope, iElement, iAttrs) { - scope[iAttrs.tbScopeElement] = iElement; - } - }; - } - }; - return directiveDefinitionObject; -} diff --git a/ui/src/app/components/side-menu.directive.js b/ui/src/app/components/side-menu.directive.js deleted file mode 100644 index 31d7158970..0000000000 --- a/ui/src/app/components/side-menu.directive.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './side-menu.scss'; - -import thingsboardMenu from '../services/menu.service'; -import thingsboardMenuLink from './menu-link.directive'; - -/* eslint-disable import/no-unresolved, import/default */ - -import sidemenuTemplate from './side-menu.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -export default angular.module('thingsboard.directives.sideMenu', [thingsboardMenu, thingsboardMenuLink]) - .directive('tbSideMenu', SideMenu) - .name; - -/*@ngInject*/ -function SideMenu($compile, $templateCache, menu) { - - var linker = function (scope, element) { - - scope.sections = menu.getSections(); - - var template = $templateCache.get(sidemenuTemplate); - - element.html(template); - - $compile(element.contents())(scope); - } - - return { - restrict: "E", - link: linker, - scope: {} - }; -} diff --git a/ui/src/app/components/side-menu.scss b/ui/src/app/components/side-menu.scss deleted file mode 100644 index cac5752a8f..0000000000 --- a/ui/src/app/components/side-menu.scss +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "~compass-sass-mixins/lib/compass"; - -.tb-side-menu .md-button.tb-active { - font-weight: 500; - background-color: rgba(255, 255, 255, .15); -} - -.tb-side-menu, -.tb-side-menu ul { - padding: 0; - margin-top: 0; - list-style: none; -} - -.tb-side-menu .tb-menu-toggle-list a.md-button { - padding: 0 16px 0 32px; - font-weight: 500; - text-transform: none; - text-rendering: optimizeLegibility; -} - -.tb-side-menu .tb-menu-toggle-list .md-button { - padding: 0 16px 0 32px; - text-transform: none; -} - -.tb-side-menu .tb-menu-toggle-list .tb-menu-toggle-list a.md-button { - padding: 0 16px 0 48px; -} - -.tb-side-menu > li { - border-bottom: 1px solid rgba(0, 0, 0, .12); -} - -.tb-side-menu .md-button-toggle .md-toggle-icon { - display: inline-block; - width: 15px; - margin: auto 0 auto auto; - background-size: 100% auto; - - transition: transform .3s, ease-in-out; -} - -.tb-side-menu .md-button { - display: flex; - width: 100%; - max-height: 40px; - padding: 0 16px; - margin: 0; - overflow: hidden; - line-height: 40px; - color: inherit; - text-align: left; - text-decoration: none; - text-overflow: ellipsis; - white-space: nowrap; - cursor: pointer; - border-radius: 0; - - span { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } -} - -.tb-side-menu tb-menu-link span.md-toggle-icon { - padding-top: 12px; - padding-bottom: 12px; -} - -.tb-side-menu ng-md-icon { - margin-right: 8px; - margin-left: 0; -} - -.tb-side-menu md-icon { - margin-right: 8px; - margin-left: 0; -} diff --git a/ui/src/app/components/side-menu.tpl.html b/ui/src/app/components/side-menu.tpl.html deleted file mode 100644 index 70b3e4487e..0000000000 --- a/ui/src/app/components/side-menu.tpl.html +++ /dev/null @@ -1,23 +0,0 @@ - -
      -
    • - -
    • -
    \ No newline at end of file diff --git a/ui/src/app/components/socialshare-panel.directive.js b/ui/src/app/components/socialshare-panel.directive.js deleted file mode 100644 index ea5474daf8..0000000000 --- a/ui/src/app/components/socialshare-panel.directive.js +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import socialsharePanelTemplate from './socialshare-panel.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - - -export default angular.module('thingsboard.directives.socialsharePanel', []) - .directive('tbSocialSharePanel', SocialsharePanel) - .name; - -/*@ngInject*/ -function SocialsharePanel() { - return { - restrict: "E", - scope: true, - bindToController: { - shareTitle: '@', - shareText: '@', - shareLink: '@', - shareHashTags: '@' - }, - controller: SocialsharePanelController, - controllerAs: 'vm', - templateUrl: socialsharePanelTemplate - }; -} - -/*@ngInject*/ -function SocialsharePanelController(utils) { - - let vm = this; - - vm.isShareLinkLocal = function() { - if (vm.shareLink && vm.shareLink.length > 0) { - return utils.isLocalUrl(vm.shareLink); - } else { - return true; - } - } - -} \ No newline at end of file diff --git a/ui/src/app/components/socialshare-panel.tpl.html b/ui/src/app/components/socialshare-panel.tpl.html deleted file mode 100644 index bf47502160..0000000000 --- a/ui/src/app/components/socialshare-panel.tpl.html +++ /dev/null @@ -1,61 +0,0 @@ - -
    - - - - {{ 'action.share-via' | translate:{provider:'Facebook'} }} - - - - - - {{ 'action.share-via' | translate:{provider:'Twitter'} }} - - - - - - {{ 'action.share-via' | translate:{provider:'Linkedin'} }} - - - - - - {{ 'action.share-via' | translate:{provider:'Reddit'} }} - - -
    \ No newline at end of file diff --git a/ui/src/app/components/tb-event-directives.js b/ui/src/app/components/tb-event-directives.js deleted file mode 100644 index 11819410f2..0000000000 --- a/ui/src/app/components/tb-event-directives.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -const SPECIAL_CHARS_REGEXP = /([:\-_]+(.))/g; -const MOZ_HACK_REGEXP = /^moz([A-Z])/; -const PREFIX_REGEXP = /^((?:x|data)[:\-_])/i; - -var tbEventDirectives = {}; - -angular.forEach( - 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave contextmenu keydown keyup keypress submit focus blur copy cut paste'.split(' '), - function(eventName) { - var directiveName = directiveNormalize('tb-' + eventName); - tbEventDirectives[directiveName] = ['$parse', '$rootScope', function($parse) { - return { - restrict: 'A', - compile: function($element, attr) { - var fn = $parse(attr[directiveName], /* interceptorFn */ null, /* expensiveChecks */ true); - return function ngEventHandler(scope, element) { - element.on(eventName, function(event) { - var callback = function() { - fn(scope, {$event:event}); - }; - callback(); - }); - }; - } - }; - }]; - } -); - -export default angular.module('thingsboard.directives.event', []) - .directive(tbEventDirectives) - .name; - -function camelCase(name) { - return name. - replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) { - return offset ? letter.toUpperCase() : letter; - }). - replace(MOZ_HACK_REGEXP, 'Moz$1'); -} - -function directiveNormalize(name) { - return camelCase(name.replace(PREFIX_REGEXP, '')); -} \ No newline at end of file diff --git a/ui/src/app/components/tb-json-to-string.directive.js b/ui/src/app/components/tb-json-to-string.directive.js deleted file mode 100644 index 79bfadb540..0000000000 --- a/ui/src/app/components/tb-json-to-string.directive.js +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default angular.module('tbJsonToString', []) - .directive('tbJsonToString', InputJson) - .name; - -function InputJson() { - return { - restrict: 'A', - require: 'ngModel', - link: function(scope, element, attr, ngModelCtrl) { - function into(input) { - try { - ngModelCtrl.$setValidity('invalidJSON', true); - return angular.fromJson(input); - } catch (e) { - ngModelCtrl.$setValidity('invalidJSON', false); - } - } - function out(data) { - try { - ngModelCtrl.$setValidity('invalidJSON', true); - return angular.toJson(data); - } catch (e) { - ngModelCtrl.$setValidity('invalidJSON', false); - } - } - ngModelCtrl.$parsers.push(into); - ngModelCtrl.$formatters.push(out); - } - }; -} diff --git a/ui/src/app/components/timeinterval.directive.js b/ui/src/app/components/timeinterval.directive.js deleted file mode 100644 index 1f5d918ed3..0000000000 --- a/ui/src/app/components/timeinterval.directive.js +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './timeinterval.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import timeintervalTemplate from './timeinterval.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -export default angular.module('thingsboard.directives.timeinterval', []) - .directive('tbTimeinterval', Timeinterval) - .name; - -/*@ngInject*/ -function Timeinterval($compile, $templateCache, timeService) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - - var template = $templateCache.get(timeintervalTemplate); - element.html(template); - - scope.rendered = false; - scope.days = 0; - scope.hours = 0; - scope.mins = 1; - scope.secs = 0; - - scope.advanced = false; - - scope.boundInterval = function() { - var min = timeService.boundMinInterval(scope.min); - var max = timeService.boundMaxInterval(scope.max); - scope.intervals = timeService.getIntervals(scope.min, scope.max); - if (scope.rendered) { - var newIntervalMs = ngModelCtrl.$viewValue; - if (newIntervalMs < min) { - newIntervalMs = min; - } else if (newIntervalMs > max) { - newIntervalMs = max; - } - if (!scope.advanced) { - newIntervalMs = timeService.boundToPredefinedInterval(min, max, newIntervalMs); - } - if (newIntervalMs !== ngModelCtrl.$viewValue) { - scope.setIntervalMs(newIntervalMs); - scope.updateView(); - } - } - } - - scope.setIntervalMs = function (intervalMs) { - if (!scope.advanced) { - scope.intervalMs = intervalMs; - } - var intervalSeconds = Math.floor(intervalMs / 1000); - scope.days = Math.floor(intervalSeconds / 86400); - scope.hours = Math.floor((intervalSeconds % 86400) / 3600); - scope.mins = Math.floor(((intervalSeconds % 86400) % 3600) / 60); - scope.secs = intervalSeconds % 60; - } - - ngModelCtrl.$render = function () { - if (ngModelCtrl.$viewValue) { - var intervalMs = ngModelCtrl.$viewValue; - if (!scope.rendered) { - scope.advanced = !timeService.matchesExistingInterval(scope.min, scope.max, intervalMs); - } - scope.setIntervalMs(intervalMs); - } - scope.rendered = true; - } - - function calculateIntervalMs() { - return (scope.days * 86400 + - scope.hours * 3600 + - scope.mins * 60 + - scope.secs) * 1000; - } - - scope.updateView = function () { - if (!scope.rendered) { - return; - } - var value = null; - var intervalMs; - if (!scope.advanced) { - intervalMs = scope.intervalMs; - if (!intervalMs || isNaN(intervalMs)) { - intervalMs = calculateIntervalMs(); - } - } else { - intervalMs = calculateIntervalMs(); - } - if (!isNaN(intervalMs) && intervalMs > 0) { - value = intervalMs; - ngModelCtrl.$setValidity('tb-timeinterval', true); - } else { - ngModelCtrl.$setValidity('tb-timeinterval', !scope.required); - } - ngModelCtrl.$setViewValue(value); - scope.boundInterval(); - } - - scope.$watch('required', function (newRequired, prevRequired) { - if (angular.isDefined(newRequired) && newRequired !== prevRequired) { - scope.updateView(); - } - }); - - scope.$watch('min', function (newMin, prevMin) { - if (angular.isDefined(newMin) && newMin !== prevMin) { - scope.updateView(); - } - }); - - scope.$watch('max', function (newMax, prevMax) { - if (angular.isDefined(newMax) && newMax !== prevMax) { - scope.updateView(); - } - }); - - scope.$watch('intervalMs', function (newIntervalMs, prevIntervalMs) { - if (angular.isDefined(newIntervalMs) && newIntervalMs !== prevIntervalMs) { - scope.updateView(); - } - }); - - scope.$watch('advanced', function (newAdvanced, prevAdvanced) { - if (angular.isDefined(newAdvanced) && newAdvanced !== prevAdvanced) { - if (!scope.advanced) { - scope.intervalMs = calculateIntervalMs(); - } else { - var intervalMs = scope.intervalMs; - if (!intervalMs || isNaN(intervalMs)) { - intervalMs = calculateIntervalMs(); - } - scope.setIntervalMs(intervalMs); - } - scope.updateView(); - } - }); - - scope.$watch('secs', function (newSecs) { - if (angular.isUndefined(newSecs)) { - return; - } - if (newSecs < 0) { - if ((scope.days + scope.hours + scope.mins) > 0) { - scope.secs = newSecs + 60; - scope.mins--; - } else { - scope.secs = 0; - } - } else if (newSecs >= 60) { - scope.secs = newSecs - 60; - scope.mins++; - } - scope.updateView(); - }); - - scope.$watch('mins', function (newMins) { - if (angular.isUndefined(newMins)) { - return; - } - if (newMins < 0) { - if ((scope.days + scope.hours) > 0) { - scope.mins = newMins + 60; - scope.hours--; - } else { - scope.mins = 0; - } - } else if (newMins >= 60) { - scope.mins = newMins - 60; - scope.hours++; - } - scope.updateView(); - }); - - scope.$watch('hours', function (newHours) { - if (angular.isUndefined(newHours)) { - return; - } - if (newHours < 0) { - if (scope.days > 0) { - scope.hours = newHours + 24; - scope.days--; - } else { - scope.hours = 0; - } - } else if (newHours >= 24) { - scope.hours = newHours - 24; - scope.days++; - } - scope.updateView(); - }); - - scope.$watch('days', function (newDays) { - if (angular.isUndefined(newDays)) { - return; - } - if (newDays < 0) { - scope.days = 0; - } - scope.updateView(); - }); - - scope.boundInterval(); - - $compile(element.contents())(scope); - - } - - return { - restrict: "E", - require: "^ngModel", - scope: { - required: '=ngRequired', - min: '=?', - max: '=?', - predefinedName: '=?', - hideFlag: '=?', - isEdit: '=?' - }, - link: linker - }; -} diff --git a/ui/src/app/components/timeinterval.scss b/ui/src/app/components/timeinterval.scss deleted file mode 100644 index 36a8033b75..0000000000 --- a/ui/src/app/components/timeinterval.scss +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -tb-timeinterval { - min-width: 355px; - - md-input-container { - margin-bottom: 0; - - .md-errors-spacer { - min-height: 0; - } - } - - mdp-date-picker { - .md-input { - width: 150px; - } - } - - .md-input { - width: 70px !important; - } - - .advanced-switch { - margin-top: 0; - } - - .advanced-label { - margin: 5px 0; - } -} diff --git a/ui/src/app/components/timeinterval.tpl.html b/ui/src/app/components/timeinterval.tpl.html deleted file mode 100644 index d58100fdbc..0000000000 --- a/ui/src/app/components/timeinterval.tpl.html +++ /dev/null @@ -1,60 +0,0 @@ - -
    -
    - - - -
    -
    - -
    - - - - - - - - - - - - - - - - -
    -
    -
    - - - - - {{interval.name}} - - - -
    -
    - - - -
    -
    diff --git a/ui/src/app/components/timewindow-button.tpl.html b/ui/src/app/components/timewindow-button.tpl.html deleted file mode 100644 index a424fd00e5..0000000000 --- a/ui/src/app/components/timewindow-button.tpl.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - {{model.displayValue}} - \ No newline at end of file diff --git a/ui/src/app/components/timewindow-panel.controller.js b/ui/src/app/components/timewindow-panel.controller.js deleted file mode 100644 index eb854d9fbc..0000000000 --- a/ui/src/app/components/timewindow-panel.controller.js +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/*@ngInject*/ -export default function TimewindowPanelController(mdPanelRef, $scope, timeService, types, timewindow, historyOnly, aggregation, isEdit, onTimewindowUpdate) { - - var vm = this; - - vm._mdPanelRef = mdPanelRef; - vm.timewindow = timewindow; - vm.historyOnly = historyOnly; - vm.aggregation = aggregation; - vm.onTimewindowUpdate = onTimewindowUpdate; - vm.aggregationTypes = types.aggregation; - vm.showLimit = showLimit; - vm.showRealtimeAggInterval = showRealtimeAggInterval; - vm.showHistoryAggInterval = showHistoryAggInterval; - vm.minRealtimeAggInterval = minRealtimeAggInterval; - vm.maxRealtimeAggInterval = maxRealtimeAggInterval; - vm.minHistoryAggInterval = minHistoryAggInterval; - vm.maxHistoryAggInterval = maxHistoryAggInterval; - vm.minDatapointsLimit = minDatapointsLimit; - vm.maxDatapointsLimit = maxDatapointsLimit; - vm.isEdit = isEdit; - - if (vm.historyOnly) { - vm.timewindow.selectedTab = 1; - } - - vm._mdPanelRef.config.onOpenComplete = function () { - $scope.theForm.$setPristine(); - } - - $scope.$watch('vm.timewindow.selectedTab', function (newSelection, prevSelection) { - if (newSelection !== prevSelection) { - $scope.theForm.$setDirty(); - } - }); - - vm.cancel = function () { - vm._mdPanelRef && vm._mdPanelRef.close(); - }; - - vm.update = function () { - vm._mdPanelRef && vm._mdPanelRef.close().then(function () { - vm.onTimewindowUpdate && vm.onTimewindowUpdate(vm.timewindow); - }); - }; - - function showLimit() { - return vm.timewindow.aggregation.type === vm.aggregationTypes.none.value; - } - - function showRealtimeAggInterval() { - return vm.timewindow.aggregation.type !== vm.aggregationTypes.none.value && - vm.timewindow.selectedTab === 0; - } - - function showHistoryAggInterval() { - return vm.timewindow.aggregation.type !== vm.aggregationTypes.none.value && - vm.timewindow.selectedTab === 1; - } - - function minRealtimeAggInterval () { - return timeService.minIntervalLimit(vm.timewindow.realtime.timewindowMs); - } - - function maxRealtimeAggInterval () { - return timeService.maxIntervalLimit(vm.timewindow.realtime.timewindowMs); - } - - function minHistoryAggInterval () { - return timeService.minIntervalLimit(currentHistoryTimewindow()); - } - - function maxHistoryAggInterval () { - return timeService.maxIntervalLimit(currentHistoryTimewindow()); - } - - function minDatapointsLimit () { - return timeService.getMinDatapointsLimit(); - } - - function maxDatapointsLimit () { - return timeService.getMaxDatapointsLimit(); - } - - function currentHistoryTimewindow() { - if (vm.timewindow.history.historyType === 0) { - return vm.timewindow.history.timewindowMs; - } else { - return vm.timewindow.history.fixedTimewindow.endTimeMs - - vm.timewindow.history.fixedTimewindow.startTimeMs; - } - } - -} - diff --git a/ui/src/app/components/timewindow-panel.tpl.html b/ui/src/app/components/timewindow-panel.tpl.html deleted file mode 100644 index 46a1f002c1..0000000000 --- a/ui/src/app/components/timewindow-panel.tpl.html +++ /dev/null @@ -1,126 +0,0 @@ - -
    -
    - -
    - - - - - - - -
    -
    - - - -
    -
    - - - -
    - -
    -
    - -
    - timewindow.time-period - -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    - - - -
    -
    - - - - - {{type.name | translate}} - - - -
    -
    -
    -
    - - - -
    -
    - - aggregation.limit - - - - - - -
    -
    - - - - -
    -
    - -
    - - - {{ 'action.update' | translate }} - - - {{ 'action.cancel' | translate }} - -
    -
    -
    -
    diff --git a/ui/src/app/components/timewindow.directive.js b/ui/src/app/components/timewindow.directive.js deleted file mode 100644 index a8c0f3af0c..0000000000 --- a/ui/src/app/components/timewindow.directive.js +++ /dev/null @@ -1,307 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './timewindow.scss'; - -import $ from 'jquery'; -import thingsboardTimeinterval from './timeinterval.directive'; -import thingsboardDatetimePeriod from './datetime-period.directive'; - -/* eslint-disable import/no-unresolved, import/default */ - -import timewindowButtonTemplate from './timewindow-button.tpl.html'; -import timewindowTemplate from './timewindow.tpl.html'; -import timewindowPanelTemplate from './timewindow-panel.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -import TimewindowPanelController from './timewindow-panel.controller'; - -export default angular.module('thingsboard.directives.timewindow', [thingsboardTimeinterval, thingsboardDatetimePeriod]) - .controller('TimewindowPanelController', TimewindowPanelController) - .directive('tbTimewindow', Timewindow) - .filter('milliSecondsToTimeString', MillisecondsToTimeString) - .name; - -/* eslint-disable angular/angularelement */ -/*@ngInject*/ -function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdMedia, $translate, timeService) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - - /* tbTimewindow (ng-model) - * { - * hideInterval: false, - * hideAggregation: false, - * hideAggInterval: false, - * realtime: { - * interval: 0, - * timewindowMs: 0 - * }, - * history: { - * interval: 0, - * timewindowMs: 0, - * fixedTimewindow: { - * startTimeMs: 0, - * endTimeMs: 0 - * } - * }, - * aggregation: { - * type: types.aggregation.avg.value, - * limit: 200 - * } - * } - */ - - scope.historyOnly = angular.isDefined(attrs.historyOnly); - - scope.isEdit = attrs.isEdit === 'true'; - - scope.aggregation = scope.$eval(attrs.aggregation); - - scope.isToolbar = angular.isDefined(attrs.isToolbar); - - scope.hideLabel = function() { - return scope.isToolbar && !$mdMedia('gt-md'); - } - - var translationPending = false; - - $translate.onReady(function() { - if (translationPending) { - scope.updateDisplayValue(); - translationPending = false; - } - }); - - var template; - if (scope.asButton) { - template = $templateCache.get(timewindowButtonTemplate); - } else { - scope.direction = angular.isDefined(attrs.direction) ? attrs.direction : 'left'; - scope.tooltipDirection = angular.isDefined(attrs.tooltipDirection) ? attrs.tooltipDirection : 'top'; - template = $templateCache.get(timewindowTemplate); - } - element.html(template); - - scope.openEditMode = function (event) { - if (scope.timewindowDisabled) { - return; - } - var position; - var isGtSm = $mdMedia('gt-sm'); - if (isGtSm) { - var panelHeight = 375; - var panelWidth = 417; - var offset = element[0].getBoundingClientRect(); - var bottomY = offset.bottom - $(window).scrollTop(); //eslint-disable-line - var leftX = offset.left - $(window).scrollLeft(); //eslint-disable-line - var yPosition; - var xPosition; - if (bottomY + panelHeight > $( window ).height()) { //eslint-disable-line - yPosition = $mdPanel.yPosition.ABOVE; - } else { - yPosition = $mdPanel.yPosition.BELOW; - } - if (leftX + panelWidth > $( window ).width()) { //eslint-disable-line - xPosition = $mdPanel.xPosition.ALIGN_END; - } else { - xPosition = $mdPanel.xPosition.ALIGN_START; - } - position = $mdPanel.newPanelPosition() - .relativeTo(element) - .addPanelPosition(xPosition, yPosition); - } else { - position = $mdPanel.newPanelPosition() - .absolute() - .top('0%') - .left('0%'); - } - var config = { - attachTo: angular.element($document[0].body), - controller: 'TimewindowPanelController', - controllerAs: 'vm', - templateUrl: timewindowPanelTemplate, - panelClass: 'tb-timewindow-panel', - position: position, - fullscreen: !isGtSm, - locals: { - 'timewindow': angular.copy(scope.model), - 'historyOnly': scope.historyOnly, - 'aggregation': scope.aggregation, - 'isEdit': scope.isEdit, - 'onTimewindowUpdate': function (timewindow) { - scope.model = timewindow; - scope.updateView(); - } - }, - openFrom: event, - clickOutsideToClose: true, - escapeToClose: true, - focusOnOpen: false - }; - $mdPanel.open(config); - } - - scope.updateView = function () { - var value = {}; - var model = scope.model; - if (model.selectedTab === 0) { - value.realtime = { - interval: model.realtime.interval, - timewindowMs: model.realtime.timewindowMs - }; - } else { - if (model.history.historyType === 0) { - value.history = { - interval: model.history.interval, - timewindowMs: model.history.timewindowMs - }; - } else { - value.history = { - interval: model.history.interval, - fixedTimewindow: { - startTimeMs: model.history.fixedTimewindow.startTimeMs, - endTimeMs: model.history.fixedTimewindow.endTimeMs - } - }; - } - } - value.aggregation = { - type: model.aggregation.type, - limit: model.aggregation.limit - }; - value.hideInterval = model.hideInterval; - value.hideAggregation = model.hideAggregation; - value.hideAggInterval = model.hideAggInterval; - ngModelCtrl.$setViewValue(value); - scope.updateDisplayValue(); - } - - scope.updateDisplayValue = function () { - if ($translate.isReady()) { - if (scope.model.selectedTab === 0 && !scope.historyOnly) { - scope.model.displayValue = $translate.instant('timewindow.realtime') + ' - ' + - $translate.instant('timewindow.last-prefix') + ' ' + - $filter('milliSecondsToTimeString')(scope.model.realtime.timewindowMs); - } else { - scope.model.displayValue = !scope.historyOnly ? ($translate.instant('timewindow.history') + ' - ') : ''; - if (scope.model.history.historyType === 0) { - scope.model.displayValue += $translate.instant('timewindow.last-prefix') + ' ' + - $filter('milliSecondsToTimeString')(scope.model.history.timewindowMs); - } else { - var startString = $filter('date')(scope.model.history.fixedTimewindow.startTimeMs, 'yyyy-MM-dd HH:mm:ss'); - var endString = $filter('date')(scope.model.history.fixedTimewindow.endTimeMs, 'yyyy-MM-dd HH:mm:ss'); - scope.model.displayValue += $translate.instant('timewindow.period', {startTime: startString, endTime: endString}); - } - } - } else { - translationPending = true; - } - } - - function isTimewindowDisabled () { - return scope.disabled || (!scope.isEdit && scope.model.hideInterval && scope.model.hideAggregation && scope.model.hideAggInterval); - } - - ngModelCtrl.$render = function () { - scope.model = timeService.defaultTimewindow(); - if (ngModelCtrl.$viewValue) { - var value = ngModelCtrl.$viewValue; - var model = scope.model; - if (angular.isDefined(value.realtime)) { - model.selectedTab = 0; - model.realtime.interval = value.realtime.interval; - model.realtime.timewindowMs = value.realtime.timewindowMs; - } else { - model.selectedTab = 1; - model.history.interval = value.history.interval; - if (angular.isDefined(value.history.timewindowMs)) { - model.history.historyType = 0; - model.history.timewindowMs = value.history.timewindowMs; - } else { - model.history.historyType = 1; - model.history.fixedTimewindow.startTimeMs = value.history.fixedTimewindow.startTimeMs; - model.history.fixedTimewindow.endTimeMs = value.history.fixedTimewindow.endTimeMs; - } - } - if (angular.isDefined(value.aggregation)) { - if (angular.isDefined(value.aggregation.type) && value.aggregation.type.length > 0) { - model.aggregation.type = value.aggregation.type; - } - model.aggregation.limit = value.aggregation.limit || Math.floor(timeService.getMaxDatapointsLimit() / 2); - } - model.hideInterval = value.hideInterval; - model.hideAggregation = value.hideAggregation; - model.hideAggInterval = value.hideAggInterval; - } - scope.timewindowDisabled = isTimewindowDisabled(); - scope.updateDisplayValue(); - }; - - scope.$watchGroup(['disabled', 'isEdit'], function(newValue, oldValue) { - if (!angular.equals(newValue, oldValue)) { - scope.timewindowDisabled = isTimewindowDisabled(); - } - }); - - $compile(element.contents())(scope); - } - - return { - restrict: "E", - require: "^ngModel", - scope: { - asButton: '=asButton', - disabled:'=ngDisabled', - isEdit: '&?' - - }, - link: linker - }; -} - -/*@ngInject*/ -function MillisecondsToTimeString($translate) { - return function (millseconds) { - var seconds = Math.floor(millseconds / 1000); - var days = Math.floor(seconds / 86400); - var hours = Math.floor((seconds % 86400) / 3600); - var minutes = Math.floor(((seconds % 86400) % 3600) / 60); - seconds = seconds % 60; - var timeString = ''; - if (days > 0) timeString += $translate.instant('timewindow.days', {days: days}, 'messageformat'); - if (hours > 0) { - if (timeString.length === 0 && hours === 1) { - hours = 0; - } - timeString += $translate.instant('timewindow.hours', {hours: hours}, 'messageformat'); - } - if (minutes > 0) { - if (timeString.length === 0 && minutes === 1) { - minutes = 0; - } - timeString += $translate.instant('timewindow.minutes', {minutes: minutes}, 'messageformat'); - } - if (seconds > 0) { - if (timeString.length === 0 && seconds === 1) { - seconds = 0; - } - timeString += $translate.instant('timewindow.seconds', {seconds: seconds}, 'messageformat'); - } - return timeString; - } -} -/* eslint-enable angular/angularelement */ diff --git a/ui/src/app/components/timewindow.scss b/ui/src/app/components/timewindow.scss deleted file mode 100644 index 611ad25d47..0000000000 --- a/ui/src/app/components/timewindow.scss +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.md-panel { - &.tb-timewindow-panel { - position: absolute; - } -} - -.tb-timewindow-panel { - min-width: 417px; - max-height: 440px; - overflow: hidden; - background: #fff; - border-radius: 4px; - box-shadow: - 0 7px 8px -4px rgba(0, 0, 0, .2), - 0 13px 19px 2px rgba(0, 0, 0, .14), - 0 5px 24px 4px rgba(0, 0, 0, .12); - - form, - fieldset { - height: 100%; - } - - md-content { - overflow: hidden; - background-color: #fff; - } - - .md-padding { - padding: 0 16px; - } - - .md-radio-interactive { - md-select, - md-switch { - pointer-events: all; - } - } - - md-radio-button { - .md-label { - width: 100%; - } - - tb-timeinterval { - width: 355px; - - .advanced-switch { - max-width: 44px; - min-height: 30px; - } - } - } -} - -tb-timewindow { - min-width: 52px; - - section.tb-timewindow { - min-height: 32px; - padding: 0 6px; - - span { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - pointer-events: all; - cursor: pointer; - } - } -} diff --git a/ui/src/app/components/timewindow.tpl.html b/ui/src/app/components/timewindow.tpl.html deleted file mode 100644 index acc9a42c99..0000000000 --- a/ui/src/app/components/timewindow.tpl.html +++ /dev/null @@ -1,37 +0,0 @@ - -
    - - - {{ 'timewindow.edit' | translate }} - - - - - - {{ 'timewindow.edit' | translate }} - - {{model.displayValue}} - - - - {{ 'timewindow.edit' | translate }} - - - -
    diff --git a/ui/src/app/components/truncate.filter.js b/ui/src/app/components/truncate.filter.js deleted file mode 100644 index de32bbcd2a..0000000000 --- a/ui/src/app/components/truncate.filter.js +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default angular.module('thingsboard.filters.truncate', []) - .filter('truncate', Truncate) - .name; - -/*@ngInject*/ -function Truncate () { - return function (value, wordwise, max, tail) { - if (!value) return ''; - - max = parseInt(max, 10); - if (!max) return value; - if (value.length <= max) return value; - - value = value.substr(0, max); - if (wordwise) { - var lastspace = value.lastIndexOf(' '); - if (lastspace != -1) { - //Also remove . and , so its gives a cleaner result. - if (value.charAt(lastspace - 1) == '.' || value.charAt(lastspace - 1) == ',') { - lastspace = lastspace - 1; - } - value = value.substr(0, lastspace); - } - } - - return value + (tail || ' …'); - }; -} diff --git a/ui/src/app/components/widget/action/custom-action-pretty-editor.directive.js b/ui/src/app/components/widget/action/custom-action-pretty-editor.directive.js deleted file mode 100644 index 65150d44ce..0000000000 --- a/ui/src/app/components/widget/action/custom-action-pretty-editor.directive.js +++ /dev/null @@ -1,1006 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './custom-action-pretty-editor.scss'; -import customActionPrettyEditorTemplate from './custom-action-pretty-editor.tpl.html'; - -import 'brace/ext/language_tools'; -import 'brace/ext/searchbox'; -import 'brace/mode/html'; -import 'brace/mode/css'; -import 'brace/snippets/text'; -import 'brace/snippets/html'; -import 'brace/snippets/css'; - -import beautify from 'js-beautify'; -import Split from "split.js"; - -const html_beautify = beautify.html; -const css_beautify = beautify.css; - -export default angular.module('thingsboard.directives.customActionPrettyEditor', []) - .directive('tbCustomActionPrettyEditor', CustomActionPrettyEditor) - .name; - -/*@ngInject*/ -function CustomActionPrettyEditor($compile, $templateCache, $window, $timeout) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - var template = $templateCache.get(customActionPrettyEditorTemplate); - element.html(template); - var ace_editors = []; - scope.fullscreen = false; - scope.htmlEditorOptions = { - useWrapMode: true, - mode: 'html', - advanced: { - enableSnippets: true, - enableBasicAutocompletion: true, - enableLiveAutocompletion: true - }, - onLoad: function (_ace) { - ace_editors.push(_ace); - } - }; - scope.cssEditorOptions = { - useWrapMode: true, - mode: 'css', - advanced: { - enableSnippets: true, - enableBasicAutocompletion: true, - enableLiveAutocompletion: true - }, - onLoad: function (_ace) { - ace_editors.push(_ace); - } - }; - - scope.addResource = addResource; - scope.beautifyCss = beautifyCss; - scope.beautifyHtml = beautifyHtml; - scope.removeResource = removeResource; - scope.toggleFullscreen = toggleFullscreen; - - var sampleJsFunction = "/*=======================================================================*/\n" + - "/*===== There are three examples: for delete, edit and add entity =====*/\n" + - "/*=======================================================================*/\n" + - "/*======================= Delete entity example =======================*/\n" + - "/*=======================================================================*/\n" + - "//\n" + - "//var $injector = widgetContext.$scope.$injector;\n" + - "//var $mdDialog = $injector.get('$mdDialog'),\n" + - "// $document = $injector.get('$document'),\n" + - "// types = $injector.get('types'),\n" + - "// assetService = $injector.get('assetService'),\n" + - "// deviceService = $injector.get('deviceService')\n" + - "// $rootScope = $injector.get('$rootScope'),\n" + - "// $q = $injector.get('$q');\n" + - "//\n" + - "//openDeleteEntityDialog();\n" + - "//\n" + - "//function openDeleteEntityDialog() {\n" + - "// var title = 'Delete ' + entityId.entityType\n" + - "// .toLowerCase() + ' ' +\n" + - "// entityName;\n" + - "// var content = 'Are you sure you want to delete the ' +\n" + - "// entityId.entityType.toLowerCase() + ' ' +\n" + - "// entityName + '?';\n" + - "// var confirm = $mdDialog.confirm()\n" + - "// .targetEvent($event)\n" + - "// .title(title)\n" + - "// .htmlContent(content)\n" + - "// .ariaLabel(title)\n" + - "// .cancel('Cancel')\n" + - "// .ok('Delete');\n" + - "// $mdDialog.show(confirm).then(function() {\n" + - "// deleteEntity();\n" + - "// })\n" + - "//}\n" + - "//\n" + - "//function deleteEntity() {\n" + - "// deleteEntityPromise(entityId).then(\n" + - "// function success() {\n" + - "// updateAliasData();\n" + - "// },\n" + - "// function fail() {\n" + - "// showErrorDialog();\n" + - "// }\n" + - "// );\n" + - "//}\n" + - "//\n" + - "//function deleteEntityPromise(entityId) {\n" + - "// if (entityId.entityType == types.entityType.asset) {\n" + - "// return assetService.deleteAsset(entityId.id);\n" + - "// } else if (entityId.entityType == types.entityType.device) {\n" + - "// return deviceService.deleteDevice(entityId.id);\n" + - "// }\n" + - "//}\n" + - "//\n" + - "//function updateAliasData() {\n" + - "// var aliasIds = [];\n" + - "// for (var id in widgetContext.aliasController.resolvedAliases) {\n" + - "// aliasIds.push(id);\n" + - "// }\n" + - "// var tasks = [];\n" + - "// aliasIds.forEach(function(aliasId) {\n" + - "// widgetContext.aliasController.setAliasUnresolved(aliasId);\n" + - "// tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\n" + - "// });\n" + - "// $q.all(tasks).then(function() {\n" + - "// $rootScope.$broadcast('entityAliasesChanged', aliasIds);\n" + - "// });\n" + - "//}\n" + - "//\n" + - "//function showErrorDialog() {\n" + - "// var title = 'Error';\n" + - "// var content = 'An error occurred while deleting the entity. Please try again.';\n" + - "// var alert = $mdDialog.alert()\n" + - "// .title(title)\n" + - "// .htmlContent(content)\n" + - "// .ariaLabel(title)\n" + - "// .parent(angular.element($document[0].body))\n" + - "// .targetEvent($event)\n" + - "// .multiple(true)\n" + - "// .clickOutsideToClose(true)\n" + - "// .ok('CLOSE');\n" + - "// $mdDialog.show(alert);\n" + - "//}\n" + - "//\n" + - "/*=======================================================================*/\n" + - "/*======================== Edit entity example ========================*/\n" + - "/*=======================================================================*/\n" + - "//\n" + - "//var $injector = widgetContext.$scope.$injector;\n" + - "//var $mdDialog = $injector.get('$mdDialog'),\n" + - "// $document = $injector.get('$document'),\n" + - "// $q = $injector.get('$q'),\n" + - "// types = $injector.get('types'),\n" + - "// $rootScope = $injector.get('$rootScope'),\n" + - "// entityService = $injector.get('entityService'),\n" + - "// attributeService = $injector.get('attributeService'),\n" + - "// entityRelationService = $injector.get('entityRelationService');\n" + - "//\n" + - "//openEditEntityDialog();\n" + - "//\n" + - "//function openEditEntityDialog() {\n" + - "// $mdDialog.show({\n" + - "// controller: ['$scope','$mdDialog', EditEntityDialogController],\n" + - "// controllerAs: 'vm',\n" + - "// template: htmlTemplate,\n" + - "// locals: {\n" + - "// entityId: entityId\n" + - "// },\n" + - "// parent: angular.element($document[0].body),\n" + - "// targetEvent: $event,\n" + - "// multiple: true,\n" + - "// clickOutsideToClose: false\n" + - "// });\n" + - "//}\n" + - "//\n" + - "//function EditEntityDialogController($scope,$mdDialog) {\n" + - "// var vm = this;\n" + - "// vm.entityId = entityId;\n" + - "// vm.entityName = entityName;\n" + - "// vm.entityType = entityId.entityType;\n" + - "// vm.allowedEntityTypes = [types.entityType.asset, types.entityType.device];\n" + - "// vm.allowedRelatedEntityTypes = [];\n" + - "// vm.entitySearchDirection = types.entitySearchDirection;\n" + - "// vm.attributes = {};\n" + - "// vm.serverAttributes = {};\n" + - "// vm.relations = [];\n" + - "// vm.newRelations = [];\n" + - "// vm.relationsToDelete = [];\n" + - "// getEntityInfo();\n" + - "// \n" + - "// vm.addRelation = function() {\n" + - "// var relation = {\n" + - "// direction: null,\n" + - "// relationType: null,\n" + - "// relatedEntity: null\n" + - "// };\n" + - "// vm.newRelations.push(relation);\n" + - "// $scope.editEntityForm.$setDirty();\n" + - "// };\n" + - "// vm.removeRelation = function(index) {\n" + - "// if (index > -1) {\n" + - "// vm.newRelations.splice(index, 1);\n" + - "// $scope.editEntityForm.$setDirty();\n" + - "// }\n" + - "// };\n" + - "// vm.removeOldRelation = function(index, relation) {\n" + - "// if (index > -1) {\n" + - "// vm.relations.splice(index, 1);\n" + - "// vm.relationsToDelete.push(relation);\n" + - "// $scope.editEntityForm.$setDirty();\n" + - "// }\n" + - "// };\n" + - "// vm.save = function() {\n" + - "// saveAttributes();\n" + - "// saveRelations();\n" + - "// $scope.editEntityForm.$setPristine();\n" + - "// };\n" + - "// vm.cancel = function() {\n" + - "// $mdDialog.hide();\n" + - "// };\n" + - "// \n" + - "// function getEntityAttributes(attributes) {\n" + - "// for (var i = 0; i < attributes.length; i++) {\n" + - "// vm.attributes[attributes[i].key] = attributes[i].value; \n" + - "// }\n" + - "// vm.serverAttributes = angular.copy(vm.attributes);\n" + - "// }\n" + - "// \n" + - "// function getEntityRelations(relations) {\n" + - "// var relationsFrom = relations[0];\n" + - "// var relationsTo = relations[1];\n" + - "// for (var i=0; i < relationsFrom.length; i++) {\n" + - "// var relation = {\n" + - "// direction: types.entitySearchDirection.from,\n" + - "// relationType: relationsFrom[i].type,\n" + - "// relatedEntity: relationsFrom[i].to\n" + - "// };\n" + - "// vm.relations.push(relation);\n" + - "// }\n" + - "// for (var i=0; i < relationsTo.length; i++) {\n" + - "// var relation = {\n" + - "// direction: types.entitySearchDirection.to,\n" + - "// relationType: relationsTo[i].type,\n" + - "// relatedEntity: relationsTo[i].from\n" + - "// };\n" + - "// vm.relations.push(relation);\n" + - "// }\n" + - "// }\n" + - "// \n" + - "// function getEntityInfo() {\n" + - "// entityService.getEntity(entityId.entityType, entityId.id).then(\n" + - "// function(entity) {\n" + - "// vm.entity = entity;\n" + - "// vm.type = vm.entity.type;\n" + - "// });\n" + - "// attributeService.getEntityAttributesValues(entityId.entityType, entityId.id, 'SERVER_SCOPE').then(\n" + - "// function(data){\n" + - "// if (data.length) {\n" + - "// getEntityAttributes(data);\n" + - "// }\n" + - "// });\n" + - "// $q.all([entityRelationService.findInfoByFrom(entityId.id, entityId.entityType), entityRelationService.findInfoByTo(entityId.id, entityId.entityType)]).then(\n" + - "// function(relations){\n" + - "// getEntityRelations(relations);\n" + - "// });\n" + - "// }\n" + - "// \n" + - "// function saveAttributes() {\n" + - "// var attributesArray = [];\n" + - "// for (var key in vm.attributes) {\n" + - "// if (vm.attributes[key] !== vm.serverAttributes[key]) {\n" + - "// attributesArray.push({key: key, value: vm.attributes[key]});\n" + - "// }\n" + - "// }\n" + - "// if (attributesArray.length > 0) {\n" + - "// attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \"SERVER_SCOPE\", attributesArray);\n" + - "// } \n" + - "// }\n" + - "// \n" + - "// function saveRelations() {\n" + - "// var tasks = [];\n" + - "// for (var i=0; i < vm.newRelations.length; i++) {\n" + - "// var relation = {\n" + - "// type: vm.newRelations[i].relationType\n" + - "// };\n" + - "// if (vm.newRelations[i].direction == types.entitySearchDirection.from) {\n" + - "// relation.to = vm.newRelations[i].relatedEntity;\n" + - "// relation.from = entityId;\n" + - "// } else {\n" + - "// relation.to = entityId;\n" + - "// relation.from = vm.newRelations[i].relatedEntity;\n" + - "// }\n" + - "// tasks.push(entityRelationService.saveRelation(relation));\n" + - "// }\n" + - "// for (var i=0; i < vm.relationsToDelete.length; i++) {\n" + - "// var relation = {\n" + - "// type: vm.relationsToDelete[i].relationType\n" + - "// };\n" + - "// if (vm.relationsToDelete[i].direction == types.entitySearchDirection.from) {\n" + - "// relation.to = vm.relationsToDelete[i].relatedEntity;\n" + - "// relation.from = entityId;\n" + - "// } else {\n" + - "// relation.to = entityId;\n" + - "// relation.from = vm.relationsToDelete[i].relatedEntity;\n" + - "// }\n" + - "// tasks.push(entityRelationService.deleteRelation(relation.from.id, relation.from.entityType, relation.type, relation.to.id, relation.to.entityType));\n" + - "// }\n" + - "// $q.all(tasks).then(function(){\n" + - "// vm.relations = vm.relations.concat(vm.newRelations);\n" + - "// vm.newRelations = [];\n" + - "// vm.relationsToDelete = [];\n" + - "// updateAliasData();\n" + - "// });\n" + - "// }\n" + - "// \n" + - "// function updateAliasData() {\n" + - "// var aliasIds = [];\n" + - "// for (var id in widgetContext.aliasController.resolvedAliases) {\n" + - "// aliasIds.push(id);\n" + - "// }\n" + - "// var tasks = [];\n" + - "// aliasIds.forEach(function(aliasId) {\n" + - "// widgetContext.aliasController.setAliasUnresolved(aliasId);\n" + - "// tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\n" + - "// });\n" + - "// $q.all(tasks).then(function() {\n" + - "// $rootScope.$broadcast('entityAliasesChanged', aliasIds);\n" + - "// });\n" + - "// }\n" + - "//}\n" + - "//\n" + - "/*========================================================================*/\n" + - "/*========================= Add entity example =========================*/\n" + - "/*========================================================================*/\n" + - "//\n" + - "//var $injector = widgetContext.$scope.$injector;\n" + - "//var $mdDialog = $injector.get('$mdDialog'),\n" + - "// $document = $injector.get('$document'),\n" + - "// $q = $injector.get('$q'),\n" + - "// $rootScope = $injector.get('$rootScope'),\n" + - "// types = $injector.get('types'),\n" + - "// assetService = $injector.get('assetService'),\n" + - "// deviceService = $injector.get('deviceService'),\n" + - "// attributeService = $injector.get('attributeService'),\n" + - "// entityRelationService = $injector.get('entityRelationService');\n" + - "//\n" + - "//openAddEntityDialog();\n" + - "//\n" + - "//function openAddEntityDialog() {\n" + - "// $mdDialog.show({\n" + - "// controller: ['$scope','$mdDialog', AddEntityDialogController],\n" + - "// controllerAs: 'vm',\n" + - "// template: htmlTemplate,\n" + - "// locals: {\n" + - "// entityId: entityId\n" + - "// },\n" + - "// parent: angular.element($document[0].body),\n" + - "// targetEvent: $event,\n" + - "// multiple: true,\n" + - "// clickOutsideToClose: false\n" + - "// });\n" + - "//}\n" + - "//\n" + - "//function AddEntityDialogController($scope, $mdDialog) {\n" + - "// var vm = this;\n" + - "// vm.allowedEntityTypes = [types.entityType.asset, types.entityType.device];\n" + - "// vm.allowedRelatedEntityTypes = [];\n" + - "// vm.entitySearchDirection = types.entitySearchDirection;\n" + - "// vm.attributes = {};\n" + - "// vm.relations = [];\n" + - "// \n" + - "// vm.addRelation = function() {\n" + - "// var relation = {\n" + - "// direction: null,\n" + - "// relationType: null,\n" + - "// relatedEntity: null\n" + - "// };\n" + - "// vm.relations.push(relation);\n" + - "// };\n" + - "// vm.removeRelation = function(index) {\n" + - "// if (index > -1) {\n" + - "// vm.relations.splice(index, 1);\n" + - "// }\n" + - "// };\n" + - "// vm.save = function() {\n" + - "// $scope.addEntityForm.$setPristine();\n" + - "// saveEntityPromise().then(\n" + - "// function (entity) {\n" + - "// saveAttributes(entity.id);\n" + - "// saveRelations(entity.id);\n" + - "// $mdDialog.hide();\n" + - "// }\n" + - "// );\n" + - "// };\n" + - "// vm.cancel = function() {\n" + - "// $mdDialog.hide();\n" + - "// };\n" + - "// \n" + - "// \n" + - "// function saveEntityPromise() {\n" + - "// var entity = {\n" + - "// name: vm.entityName,\n" + - "// type: vm.type\n" + - "// };\n" + - "// if (vm.entityType == types.entityType.asset) {\n" + - "// return assetService.saveAsset(entity);\n" + - "// } else if (vm.entityType == types.entityType.device) {\n" + - "// return deviceService.saveDevice(entity);\n" + - "// }\n" + - "// }\n" + - "// \n" + - "// function saveAttributes(entityId) {\n" + - "// var attributesArray = [];\n" + - "// for (var key in vm.attributes) {\n" + - "// attributesArray.push({key: key, value: vm.attributes[key]});\n" + - "// }\n" + - "// if (attributesArray.length > 0) {\n" + - "// attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \"SERVER_SCOPE\", attributesArray);\n" + - "// } \n" + - "// }\n" + - "// \n" + - "// function saveRelations(entityId) {\n" + - "// var tasks = [];\n" + - "// for (var i=0; i < vm.relations.length; i++) {\n" + - "// var relation = {\n" + - "// type: vm.relations[i].relationType\n" + - "// };\n" + - "// if (vm.relations[i].direction == types.entitySearchDirection.from) {\n" + - "// relation.to = vm.relations[i].relatedEntity;\n" + - "// relation.from = entityId;\n" + - "// } else {\n" + - "// relation.to = entityId;\n" + - "// relation.from = vm.relations[i].relatedEntity;\n" + - "// }\n" + - "// tasks.push(entityRelationService.saveRelation(relation));\n" + - "// }\n" + - "// $q.all(tasks).then(function(){\n" + - "// updateAliasData();\n" + - "// });\n" + - "// }\n" + - "// \n" + - "// function updateAliasData() {\n" + - "// var aliasIds = [];\n" + - "// for (var id in widgetContext.aliasController.resolvedAliases) {\n" + - "// aliasIds.push(id);\n" + - "// }\n" + - "// var tasks = [];\n" + - "// aliasIds.forEach(function(aliasId) {\n" + - "// widgetContext.aliasController.setAliasUnresolved(aliasId);\n" + - "// tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\n" + - "// });\n" + - "// $q.all(tasks).then(function() {\n" + - "// $rootScope.$broadcast('widgetForceReInit');\n" + - "// });\n" + - "// }\n" + - "//}\n"; - - var sampleHtmlTemplate = '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n'; - - var sampleCss = '/*=======================================================================*/\n' + - '/*========== There are two examples: for edit and add entity ==========*/\n' + - '/*=======================================================================*/\n' + - '/*======================== Edit entity example ========================*/\n' + - '/*=======================================================================*/\n' + - '/*\n' + - '.edit-entity-form md-input-container {\n' + - ' padding-right: 10px;\n' + - '}\n' + - '\n' + - '.edit-entity-form .boolean-value-input {\n' + - ' padding-left: 5px;\n' + - '}\n' + - '\n' + - '.edit-entity-form .boolean-value-input .checkbox-label {\n' + - ' margin-bottom: 8px;\n' + - ' color: rgba(0,0,0,0.54);\n' + - ' font-size: 12px;\n' + - '}\n' + - '\n' + - '.relations-list .header {\n' + - ' padding-right: 5px;\n' + - ' padding-bottom: 5px;\n' + - ' padding-left: 5px;\n' + - '}\n' + - '\n' + - '.relations-list .header .cell {\n' + - ' padding-right: 5px;\n' + - ' padding-left: 5px;\n' + - ' font-size: 12px;\n' + - ' font-weight: 700;\n' + - ' color: rgba(0, 0, 0, .54);\n' + - ' white-space: nowrap;\n' + - '}\n' + - '\n' + - '.relations-list .body {\n' + - ' padding-right: 5px;\n' + - ' padding-bottom: 15px;\n' + - ' padding-left: 5px;\n' + - '}\n' + - '\n' + - '.relations-list .body .row {\n' + - ' padding-top: 5px;\n' + - '}\n' + - '\n' + - '.relations-list .body .cell {\n' + - ' padding-right: 5px;\n' + - ' padding-left: 5px;\n' + - '}\n' + - '\n' + - '.relations-list .body md-autocomplete-wrap md-input-container {\n' + - ' height: 30px;\n' + - '}\n' + - '\n' + - '.relations-list .body .md-button {\n' + - ' margin: 0;\n' + - '}\n' + - '\n' + - '.relations-list.old-relations tb-entity-select tb-entity-autocomplete button {\n' + - ' display: none;\n' + - '} \n' + - '*/\n' + - '/*========================================================================*/\n' + - '/*========================= Add entity example =========================*/\n' + - '/*========================================================================*/\n' + - '/*\n' + - '.add-entity-form md-input-container {\n' + - ' padding-right: 10px;\n' + - '}\n' + - '\n' + - '.add-entity-form .boolean-value-input {\n' + - ' padding-left: 5px;\n' + - '}\n' + - '\n' + - '.add-entity-form .boolean-value-input .checkbox-label {\n' + - ' margin-bottom: 8px;\n' + - ' color: rgba(0,0,0,0.54);\n' + - ' font-size: 12px;\n' + - '}\n' + - '\n' + - '.relations-list .header {\n' + - ' padding-right: 5px;\n' + - ' padding-bottom: 5px;\n' + - ' padding-left: 5px;\n' + - '}\n' + - '\n' + - '.relations-list .header .cell {\n' + - ' padding-right: 5px;\n' + - ' padding-left: 5px;\n' + - ' font-size: 12px;\n' + - ' font-weight: 700;\n' + - ' color: rgba(0, 0, 0, .54);\n' + - ' white-space: nowrap;\n' + - '}\n' + - '\n' + - '.relations-list .body {\n' + - ' padding-right: 5px;\n' + - ' padding-bottom: 15px;\n' + - ' padding-left: 5px;\n' + - '}\n' + - '\n' + - '.relations-list .body .row {\n' + - ' padding-top: 5px;\n' + - '}\n' + - '\n' + - '.relations-list .body .cell {\n' + - ' padding-right: 5px;\n' + - ' padding-left: 5px;\n' + - '}\n' + - '\n' + - '.relations-list .body md-autocomplete-wrap md-input-container {\n' + - ' height: 30px;\n' + - '}\n' + - '\n' + - '.relations-list .body .md-button {\n' + - ' margin: 0;\n' + - '}\n' + - '*/\n'; - - scope.$watch('action', function () { - ngModelCtrl.$setViewValue(scope.action); - }); - - ngModelCtrl.$render = function () { - scope.action = ngModelCtrl.$viewValue; - if (angular.isUndefined(scope.action.customHtml) && angular.isUndefined(scope.action.customCss) && angular.isUndefined(scope.action.customFunction)) { - scope.action.customFunction = sampleJsFunction; - scope.action.customHtml = sampleHtmlTemplate; - scope.action.customCss = sampleCss; - } - }; - - function removeResource(index) { - if (index > -1) { - scope.action.customResources.splice(index, 1); - scope.theForm.$setDirty(); - } - } - - function addResource() { - if (!scope.action.customResources) { - scope.action.customResources = []; - } - scope.action.customResources.push({url: ''}); - scope.theForm.$setDirty(); - } - - function beautifyHtml() { - var res = html_beautify(scope.action.customHtml, {indent_size: 4, wrap_line_length: 60}); - scope.action.customHtml = res; - } - - function beautifyCss() { - var res = css_beautify(scope.action.customCss, {indent_size: 4}); - scope.action.customCss = res; - } - - function toggleFullscreen() { - scope.fullscreen = !scope.fullscreen; - if (scope.fullscreen) { - scope.customActionEditorElement = angular.element('.tb-custom-action-editor'); - angular.element(scope.customActionEditorElement[0]).ready(function () { - var w = scope.customActionEditorElement.width(); - if (w > 0) { - initSplitLayout(); - } else { - scope.$watch( - function () { - return scope.customActionEditorElement[0].offsetWidth || parseInt(scope.customActionEditorElement.css('width'), 10); - }, - function (newSize) { - if (newSize > 0) { - initSplitLayout(); - } - } - ); - } - }); - } else { - scope.layoutInited = false; - } - } - - function onDividerDrag() { - scope.$broadcast('update-ace-editor-size'); - for (var i = 0; i < ace_editors.length; i++) { - var ace = ace_editors[i]; - ace.resize(); - ace.renderer.updateFull(); - } - } - - function initSplitLayout() { - if (!scope.layoutInited) { - Split([angular.element('#left-panel', scope.customActionEditorElement)[0], angular.element('#right-panel', scope.customActionEditorElement)[0]], { - sizes: [50, 50], - gutterSize: 8, - cursor: 'col-resize', - onDrag: function () { - onDividerDrag() - } - }); - - onDividerDrag(); - - scope.$applyAsync(function () { - scope.layoutInited = true; - var w = angular.element($window); - $timeout(function () { - w.triggerHandler('resize') - }); - }); - - } - } - - $compile(element.contents())(scope); - }; - - return { - restrict: "E", - require: "^ngModel", - scope: { - theForm: '=?', - }, - link: linker - }; -} diff --git a/ui/src/app/components/widget/action/custom-action-pretty-editor.scss b/ui/src/app/components/widget/action/custom-action-pretty-editor.scss deleted file mode 100644 index fa6319b44a..0000000000 --- a/ui/src/app/components/widget/action/custom-action-pretty-editor.scss +++ /dev/null @@ -1,98 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-custom-action-pretty { - box-sizing: border-box; - padding: 8px; - background-color: #fff; - - .tb-fullscreen-panel { - .tb-custom-action-editor-container { - height: calc(100% - 40px); - } - - #right-panel { - padding-top: 8px; - padding-left: 3px; - } - - tb-js-func .tb-js-func-panel { - box-sizing: border-box; - } - - md-tabs-content-wrapper, - md-tab-content { - height: 100%; - } - } - - .tb-split { - box-sizing: border-box; - overflow-x: hidden; - overflow-y: auto; - } - - .tb-content { - border: 1px solid #c0c0c0; - } - - .gutter { - background-color: #eee; - background-repeat: no-repeat; - background-position: 50%; - } - - .gutter.gutter-horizontal { - cursor: col-resize; - background-image: url("../../../../../node_modules/split.js/grips/vertical.png"); - } - - .tb-split.tb-split-horizontal, - .gutter.gutter-horizontal { - float: left; - height: 100%; - } - - .tb-action-expand-button { - position: absolute; - right: 14px; - z-index: 1; - - &.tb-fullscreen-editor { - position: relative; - right: 0; - } - - .md-button { - min-width: auto; - } - } - - .tb-custom-action-editor { - &.tb-fullscreen-editor { - height: 100%; - } - } -} - -.tb-custom-action-editor-container { - .html-panel, - .css-panel { - width: 100%; - min-width: 200px; - height: 100%; - min-height: 200px; - } -} diff --git a/ui/src/app/components/widget/action/custom-action-pretty-editor.tpl.html b/ui/src/app/components/widget/action/custom-action-pretty-editor.tpl.html deleted file mode 100644 index 79b0dd625c..0000000000 --- a/ui/src/app/components/widget/action/custom-action-pretty-editor.tpl.html +++ /dev/null @@ -1,186 +0,0 @@ - -
    -
    - - - {{ 'widget.toggle-fullscreen' | translate }} - - - fullscreen - - - fullscreen_exit - - widget.toggle-fullscreen - -
    -
    -
    - - -
    -
    -
    - - - - - - {{ 'widget.remove-resource' | translate }} - - - close - - -
    -
    - - - {{ 'widget.add-resource' | translate }} - - action.add - -
    -
    -
    -
    - -
    -
    - {{ 'widget.tidy' | translate }} - - -
    -
    -
    -
    - -
    -
    - {{ 'widget.tidy' | translate }} - - -
    -
    -
    -
    - - - - -
    -
    -
    -
    - - -
    -
    -
    - - - - - - {{ 'widget.remove-resource' | translate }} - - - close - - -
    -
    - - - {{ 'widget.add-resource' | translate }} - - action.add - -
    -
    -
    -
    - -
    -
    - {{ 'widget.tidy' | translate }} - - -
    -
    -
    -
    - -
    -
    - {{ 'widget.tidy' | translate }} - - -
    -
    -
    -
    -
    -
    -
    - - -
    -
    -
    -
    \ No newline at end of file diff --git a/ui/src/app/components/widget/action/manage-widget-actions.directive.js b/ui/src/app/components/widget/action/manage-widget-actions.directive.js deleted file mode 100644 index 1d45f2ef82..0000000000 --- a/ui/src/app/components/widget/action/manage-widget-actions.directive.js +++ /dev/null @@ -1,298 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/** - * Created by igor on 6/20/17. - */ -/* - * Copyright © 2016-2017 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import './manage-widget-actions.scss'; - -import thingsboardMaterialIconSelect from '../../material-icon-select.directive'; - -import WidgetActionDialogController from './widget-action-dialog.controller'; -import CustomActionPrettyEditor from './custom-action-pretty-editor.directive'; - -/* eslint-disable import/no-unresolved, import/default */ - -import manageWidgetActionsTemplate from './manage-widget-actions.tpl.html'; -import widgetActionDialogTemplate from './widget-action-dialog.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -export default angular.module('thingsboard.directives.widgetActions', [thingsboardMaterialIconSelect, CustomActionPrettyEditor]) - .controller('WidgetActionDialogController', WidgetActionDialogController) - .directive('tbManageWidgetActions', ManageWidgetActions) - .name; - -/*@ngInject*/ -function ManageWidgetActions() { - return { - restrict: "E", - scope: true, - bindToController: { - actionSources: '=', - widgetActions: '=', - fetchDashboardStates: '&', - }, - controller: ManageWidgetActionsController, - controllerAs: 'vm', - templateUrl: manageWidgetActionsTemplate - }; -} - -/* eslint-disable angular/angularelement */ - - -/*@ngInject*/ -function ManageWidgetActionsController($rootScope, $scope, $document, $mdDialog, $q, $filter, - $translate, $timeout, utils, types) { - - let vm = this; - - vm.allActions = []; - - vm.actions = []; - vm.actionsCount = 0; - - vm.query = { - order: 'actionSourceName', - limit: 10, - page: 1, - search: null - }; - - vm.enterFilterMode = enterFilterMode; - vm.exitFilterMode = exitFilterMode; - vm.onReorder = onReorder; - vm.onPaginate = onPaginate; - vm.addAction = addAction; - vm.editAction = editAction; - vm.deleteAction = deleteAction; - - $timeout(function(){ - $scope.manageWidgetActionsForm.querySearchInput.$pristine = false; - }); - - $scope.$watch('vm.widgetActions', function() { - if (vm.widgetActions) { - reloadActions(); - } - }); - - $scope.$watch("vm.query.search", function(newVal, prevVal) { - if (!angular.equals(newVal, prevVal) && vm.query.search != null) { - updateActions(); - } - }); - - function enterFilterMode (event) { - let $button = angular.element(event.currentTarget); - let $toolbarsContainer = $button.closest('.toolbarsContainer'); - - vm.query.search = ''; - - $timeout(()=>{ - $toolbarsContainer.find('.searchInput').focus(); - }) - } - - function exitFilterMode () { - vm.query.search = null; - updateActions(); - } - - function onReorder () { - updateActions(); - } - - function onPaginate () { - updateActions(); - } - - function addAction($event) { - if ($event) { - $event.stopPropagation(); - } - openWidgetActionDialog($event, null, true); - } - - function editAction ($event, action) { - if ($event) { - $event.stopPropagation(); - } - openWidgetActionDialog($event, action, false); - } - - function deleteAction($event, action) { - if ($event) { - $event.stopPropagation(); - } - if (action) { - var title = $translate.instant('widget-config.delete-action-title'); - var content = $translate.instant('widget-config.delete-action-text', {actionName: action.name}); - var confirm = $mdDialog.confirm() - .targetEvent($event) - .title(title) - .htmlContent(content) - .ariaLabel(title) - .cancel($translate.instant('action.no')) - .ok($translate.instant('action.yes')); - - confirm._options.multiple = true; - confirm._options.fullscreen = true; - - $mdDialog.show(confirm).then(function () { - var index = getActionIndex(action.id, vm.allActions); - if (index > -1) { - vm.allActions.splice(index, 1); - } - var targetActions = vm.widgetActions[action.actionSourceId]; - index = getActionIndex(action.id, targetActions); - if (index > -1) { - targetActions.splice(index, 1); - } - $scope.manageWidgetActionsForm.$setDirty(); - updateActions(); - }); - } - } - - function openWidgetActionDialog($event, action, isAdd) { - var prevActionId = null; - if (!isAdd) { - prevActionId = action.id; - } - var availableActionSources = {}; - for (var id in vm.actionSources) { - var actionSource = vm.actionSources[id]; - if (actionSource.multiple) { - availableActionSources[id] = actionSource; - } else { - if (!isAdd && action.actionSourceId == id) { - availableActionSources[id] = actionSource; - } else { - var result = $filter('filter')(vm.allActions, {actionSourceId: id}); - if (!result || !result.length) { - availableActionSources[id] = actionSource; - } - } - } - } - $mdDialog.show({ - controller: 'WidgetActionDialogController', - controllerAs: 'vm', - templateUrl: widgetActionDialogTemplate, - parent: angular.element($document[0].body), - locals: {isAdd: isAdd, fetchDashboardStates: vm.fetchDashboardStates, - actionSources: availableActionSources, widgetActions: vm.widgetActions, - action: angular.copy(action)}, - multiple: true, - fullscreen: true, - targetEvent: $event - }).then(function (action) { - saveAction(action, prevActionId); - updateActions(); - }); - } - - function getActionIndex(id, actions) { - var result = $filter('filter')(actions, {id: id}, true); - if (result && result.length) { - return actions.indexOf(result[0]); - } - return -1; - } - - function saveAction(action, prevActionId) { - var actionSourceName = vm.actionSources[action.actionSourceId].name; - action.actionSourceName = utils.customTranslation(actionSourceName, actionSourceName); - action.typeName = $translate.instant(types.widgetActionTypes[action.type].name); - var actionSourceId = action.actionSourceId; - var widgetAction = angular.copy(action); - delete widgetAction.actionSourceId; - delete widgetAction.actionSourceName; - delete widgetAction.typeName; - var targetActions = vm.widgetActions[actionSourceId]; - if (!targetActions) { - targetActions = []; - vm.widgetActions[actionSourceId] = targetActions; - } - if (prevActionId) { - const indexInTarget = getActionIndex(prevActionId, targetActions); - const indexInAllActions = getActionIndex(prevActionId, vm.allActions); - if (indexInTarget > -1) { - targetActions[indexInTarget] = widgetAction; - } else if (indexInAllActions > -1) { - const prevActionSourceId = vm.allActions[indexInAllActions].actionSourceId; - const index = getActionIndex(prevActionId,vm.widgetActions[prevActionSourceId]); - vm.widgetActions[prevActionSourceId].splice(index,1); - targetActions.push(widgetAction); - } - if (indexInAllActions > -1) { - vm.allActions[indexInAllActions] = action; - } - } else { - vm.allActions.push(action); - targetActions.push(widgetAction); - } - $scope.manageWidgetActionsForm.$setDirty(); - } - - function reloadActions() { - vm.allActions = []; - vm.actions = []; - vm.actionsCount = 0; - - for (var actionSourceId in vm.widgetActions) { - var actionSource = vm.actionSources[actionSourceId]; - var actionSourceActions = vm.widgetActions[actionSourceId]; - for (var i=0;i -
    - -
    - widget-config.actions - - - add - - {{ 'widget-config.add-action' | translate }} - - - - search - - {{ 'action.search' | translate }} - - -
    -
    - -
    - - search - - {{ 'widget-config.search-actions' | translate }} - - - - - - - - close - - {{ 'action.close' | translate }} - - -
    -
    - - - - - - - - - - - - - - - - - - - - -
    widget-config.action-sourcewidget-config.action-namewidget-config.action-iconwidget-config.action-type 
    {{action.actionSourceName}}{{action.name}} - {{action.icon}} - {{action.typeName}} - - edit - - {{ 'widget-config.edit-action' | translate }} - - - - delete - - {{ 'widget-config.delete-action' | translate }} - - -
    -
    - - -
    diff --git a/ui/src/app/components/widget/action/widget-action-dialog.controller.js b/ui/src/app/components/widget/action/widget-action-dialog.controller.js deleted file mode 100644 index 0e0e4beaed..0000000000 --- a/ui/src/app/components/widget/action/widget-action-dialog.controller.js +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/*@ngInject*/ -export default function WidgetActionDialogController($scope, $mdDialog, $filter, $q, dashboardService, dashboardUtils, types, toast, utils, - isAdd, fetchDashboardStates, actionSources, widgetActions, action) { - - var vm = this; - - vm.types = types; - - vm.isAdd = isAdd; - vm.fetchDashboardStates = fetchDashboardStates; - vm.actionSources = actionSources; - vm.widgetActions = widgetActions; - - vm.targetDashboardStateSearchText = ''; - - vm.selectedDashboardStateIds = []; - - if (vm.isAdd) { - vm.action = { - id: utils.guid() - }; - } else { - vm.action = action; - } - - vm.actionSourceName = actionSourceName; - - vm.targetDashboardStateSearchTextChanged = function() { - }; - - vm.dashboardStateSearch = dashboardStateSearch; - vm.cancel = cancel; - vm.save = save; - - $scope.$watch("vm.action.name", function(newVal, prevVal) { - if (!angular.equals(newVal, prevVal) && vm.action.name != null) { - checkActionName(); - } - }); - - $scope.$watch("vm.action.actionSourceId", function(newVal, prevVal) { - if (!angular.equals(newVal, prevVal) && vm.action.actionSourceId != null) { - checkActionName(); - } - }); - - $scope.$watch("vm.action.targetDashboardId", function() { - vm.selectedDashboardStateIds = []; - if (vm.action.targetDashboardId) { - dashboardService.getDashboard(vm.action.targetDashboardId).then( - function success(dashboard) { - dashboard = dashboardUtils.validateAndUpdateDashboard(dashboard); - var states = dashboard.configuration.states; - vm.selectedDashboardStateIds = Object.keys(states); - } - ); - } - }); - - $scope.$watch('vm.action.type', function(newType) { - if (newType) { - switch (newType) { - case vm.types.widgetActionTypes.openDashboardState.value: - case vm.types.widgetActionTypes.updateDashboardState.value: - case vm.types.widgetActionTypes.openDashboard.value: - if (angular.isUndefined(vm.action.setEntityId)) { - vm.action.setEntityId = true; - } - break; - } - } - }); - - function checkActionName() { - var actionNameIsUnique = true; - if (vm.action.actionSourceId && vm.action.name) { - var sourceActions = vm.widgetActions[vm.action.actionSourceId]; - if (sourceActions) { - var result = $filter('filter')(sourceActions, {name: vm.action.name}, true); - if (result && result.length && result[0].id !== vm.action.id) { - actionNameIsUnique = false; - } - } - } - $scope.theForm.name.$setValidity('actionNameNotUnique', actionNameIsUnique); - } - - function actionSourceName (actionSource) { - if (actionSource) { - return utils.customTranslation(actionSource.name, actionSource.name); - } else { - return ''; - } - } - - function dashboardStateSearch (query) { - if (vm.action.type == vm.types.widgetActionTypes.openDashboard.value) { - var deferred = $q.defer(); - var result = query ? vm.selectedDashboardStateIds.filter( - createFilterForDashboardState(query)) : vm.selectedDashboardStateIds; - if (result && result.length) { - deferred.resolve(result); - } else { - deferred.resolve([query]); - } - return deferred.promise; - } else { - return vm.fetchDashboardStates({query: query}); - } - } - - function createFilterForDashboardState (query) { - var lowercaseQuery = angular.lowercase(query); - return function filterFn(stateId) { - return (angular.lowercase(stateId).indexOf(lowercaseQuery) === 0); - }; - } - - function cleanupAction(action) { - var result = {}; - result.id = action.id; - result.actionSourceId = action.actionSourceId; - result.name = action.name; - result.icon = action.icon; - result.type = action.type; - switch (action.type) { - case vm.types.widgetActionTypes.openDashboardState.value: - case vm.types.widgetActionTypes.updateDashboardState.value: - result.targetDashboardStateId = action.targetDashboardStateId; - result.openRightLayout = action.openRightLayout; - result.setEntityId = action.setEntityId; - result.stateEntityParamName = action.stateEntityParamName; - break; - case vm.types.widgetActionTypes.openDashboard.value: - result.targetDashboardId = action.targetDashboardId; - result.targetDashboardStateId = action.targetDashboardStateId; - result.setEntityId = action.setEntityId; - result.stateEntityParamName = action.stateEntityParamName; - break; - case vm.types.widgetActionTypes.custom.value: - result.customFunction = action.customFunction; - break; - case vm.types.widgetActionTypes.customPretty.value: - result.customResources = action.customResources; - result.customHtml = action.customHtml; - result.customCss = action.customCss; - result.customFunction = action.customFunction; - break; - } - return result; - } - - function cancel() { - $mdDialog.cancel(); - } - - function save() { - $scope.theForm.$setPristine(); - $mdDialog.hide(cleanupAction(vm.action)); - } -} diff --git a/ui/src/app/components/widget/action/widget-action-dialog.tpl.html b/ui/src/app/components/widget/action/widget-action-dialog.tpl.html deleted file mode 100644 index 831e423ea1..0000000000 --- a/ui/src/app/components/widget/action/widget-action-dialog.tpl.html +++ /dev/null @@ -1,142 +0,0 @@ - - -
    - -
    -

    {{ (vm.isAdd ? 'widget-config.add-action' : 'widget-config.edit-action') | translate }}

    - - - - -
    -
    - - - -
    - -
    - - - - - {{vm.actionSourceName(actionSource)}} - - -
    -
    widget-config.action-source-required
    -
    -
    - - - -
    -
    widget-config.action-name-required
    -
    widget-config.action-name-not-unique
    -
    -
    - - - - - - - {{ actionType.name | translate }} - - -
    -
    widget-config.action-type-required
    -
    -
    -
    -
    widget-action.target-dashboard
    - - -
    - - -
    - {{item}} -
    -
    -
    -
    widget-action.target-dashboard-state-required
    -
    -
    - {{ 'widget-action.open-right-layout' | translate }} - -
    - {{ 'widget-action.set-entity-from-widget' | translate }} - - - - - -
    - - - -
    -
    -
    -
    - - - - {{ (vm.isAdd ? 'action.add' : 'action.save') | translate }} - - - {{ 'action.cancel' | translate }} - - -
    -
    diff --git a/ui/src/app/components/widget/widget-config.directive.js b/ui/src/app/components/widget/widget-config.directive.js deleted file mode 100644 index a06fa5d9ba..0000000000 --- a/ui/src/app/components/widget/widget-config.directive.js +++ /dev/null @@ -1,524 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import thingsboardTypes from '../../common/types.constant'; -import thingsboardUtils from '../../common/utils.service'; -import thingsboardEntityAliasSelect from '../entity-alias-select.directive'; -import thingsboardDatasource from '../datasource.directive'; -import thingsboardTimewindow from '../timewindow.directive'; -import thingsboardLegendConfig from '../legend-config.directive'; -import thingsboardJsonForm from '../json-form.directive'; -import thingsboardManageWidgetActions from './action/manage-widget-actions.directive'; -import 'angular-ui-ace'; - -import fixAceEditor from './../ace-editor-fix'; - -import './widget-config.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import widgetConfigTemplate from './widget-config.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/* eslint-disable angular/angularelement */ - -export default angular.module('thingsboard.directives.widgetConfig', [thingsboardTypes, - thingsboardUtils, - thingsboardJsonForm, - thingsboardEntityAliasSelect, - thingsboardDatasource, - thingsboardTimewindow, - thingsboardLegendConfig, - thingsboardManageWidgetActions, - 'ui.ace']) - .directive('tbWidgetConfig', WidgetConfig) - .name; - -/*@ngInject*/ -function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout, types, utils) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - - var template = $templateCache.get(widgetConfigTemplate); - - element.html(template); - - scope.types = types; - scope.widgetEditMode = $rootScope.widgetEditMode; - - scope.emptySettingsSchema = { - type: "object", - properties: {} - }; - - scope.emptySettingsGroupInfoes=[]; - scope.defaultSettingsForm = [ - '*' - ]; - - scope.styleEditorOptions = { - useWrapMode: true, - mode: 'json', - advanced: { - enableSnippets: true, - enableBasicAutocompletion: true, - enableLiveAutocompletion: true - }, - onLoad: function (_ace) { - fixAceEditor(_ace); - } - }; - - if (angular.isUndefined(scope.forceExpandDatasources)) { - scope.forceExpandDatasources = false; - } - - if (angular.isUndefined(scope.isDataEnabled)) { - scope.isDataEnabled = true; - } - - scope.currentSettingsSchema = {}; - scope.currentSettings = angular.copy(scope.emptySettingsSchema); - scope.currentSettingsGroupInfoes = angular.copy(scope.emptySettingsGroupInfoes); - - scope.targetDeviceAlias = { - value: null - } - - scope.alarmSource = { - value: null - } - - ngModelCtrl.$render = function () { - if (ngModelCtrl.$viewValue) { - var config = ngModelCtrl.$viewValue.config; - var layout = ngModelCtrl.$viewValue.layout; - if (config) { - scope.selectedTab = 0; - scope.title = config.title; - scope.showTitleIcon = angular.isDefined(config.showTitleIcon) ? config.showTitleIcon : false; - scope.titleIcon = angular.isDefined(config.titleIcon) ? config.titleIcon : ''; - scope.iconColor = angular.isDefined(config.iconColor) ? config.iconColor : 'rgba(0, 0, 0, 0.87)'; - scope.iconSize = angular.isDefined(config.iconSize) ? config.iconSize : '24px'; - scope.titleTooltip = angular.isDefined(config.titleTooltip) ? config.titleTooltip : ''; - scope.showTitle = config.showTitle; - scope.dropShadow = angular.isDefined(config.dropShadow) ? config.dropShadow : true; - scope.enableFullscreen = angular.isDefined(config.enableFullscreen) ? config.enableFullscreen : true; - scope.backgroundColor = config.backgroundColor; - scope.color = config.color; - scope.padding = config.padding; - scope.margin = config.margin; - scope.widgetStyle = - angular.toJson(angular.isDefined(config.widgetStyle) ? config.widgetStyle : {}, true); - scope.titleStyle = - angular.toJson(angular.isDefined(config.titleStyle) ? config.titleStyle : { - fontSize: '16px', - fontWeight: 400 - }, true); - scope.units = config.units; - scope.decimals = config.decimals; - scope.useDashboardTimewindow = angular.isDefined(config.useDashboardTimewindow) ? - config.useDashboardTimewindow : true; - scope.displayTimewindow = angular.isDefined(config.displayTimewindow) ? - config.displayTimewindow : true; - scope.timewindow = config.timewindow; - scope.showLegend = angular.isDefined(config.showLegend) ? - config.showLegend : scope.widgetType === types.widgetType.timeseries.value; - scope.legendConfig = config.legendConfig; - scope.actions = config.actions; - if (!scope.actions) { - scope.actions = {}; - } - if (scope.widgetType !== types.widgetType.rpc.value && - scope.widgetType !== types.widgetType.alarm.value && - scope.widgetType !== types.widgetType.static.value - && scope.isDataEnabled) { - if (scope.datasources) { - scope.datasources.splice(0, scope.datasources.length); - } else { - scope.datasources = []; - } - if (config.datasources) { - for (var i = 0; i < config.datasources.length; i++) { - scope.datasources.push({value: config.datasources[i]}); - } - } - } else if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) { - if (config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0) { - var aliasId = config.targetDeviceAliasIds[0]; - var entityAliases = scope.aliasController.getEntityAliases(); - if (entityAliases[aliasId]) { - scope.targetDeviceAlias.value = entityAliases[aliasId]; - } else { - scope.targetDeviceAlias.value = null; - } - } else { - scope.targetDeviceAlias.value = null; - } - } else if (scope.widgetType === types.widgetType.alarm.value && scope.isDataEnabled) { - scope.alarmSearchStatus = angular.isDefined(config.alarmSearchStatus) ? - config.alarmSearchStatus : types.alarmSearchStatus.any; - scope.alarmsPollingInterval = angular.isDefined(config.alarmsPollingInterval) ? - config.alarmsPollingInterval : 5; - scope.alarmsMaxCountLoad = angular.isDefined(config.alarmsMaxCountLoad) ? - config.alarmsMaxCountLoad : 0; - scope.alarmsFetchSize = angular.isDefined(config.alarmsFetchSize) ? - config.alarmsFetchSize : 100; - if (config.alarmSource) { - scope.alarmSource.value = config.alarmSource; - } else { - scope.alarmSource.value = null; - } - } - - scope.settings = config.settings; - - scope.updateSchemaForm(); - } - if (layout) { - scope.mobileOrder = layout.mobileOrder; - scope.mobileHeight = layout.mobileHeight; - } - } - }; - - scope.displayAdvanced = function() { - return scope.widgetSettingsSchema && scope.widgetSettingsSchema.schema; - } - - scope.updateSchemaForm = function() { - if (scope.widgetSettingsSchema && scope.widgetSettingsSchema.schema) { - scope.currentSettingsSchema = scope.widgetSettingsSchema.schema; - scope.currentSettingsForm = scope.widgetSettingsSchema.form || angular.copy(scope.defaultSettingsForm); - scope.currentSettingsGroupInfoes = scope.widgetSettingsSchema.groupInfoes; - scope.currentSettings = scope.settings; - } else { - scope.currentSettingsForm = angular.copy(scope.defaultSettingsForm); - scope.currentSettingsSchema = angular.copy(scope.emptySettingsSchema); - scope.currentSettingsGroupInfoes = angular.copy(scope.emptySettingsGroupInfoes); - scope.currentSettings = {}; - } - } - - scope.updateValidity = function () { - if (ngModelCtrl.$viewValue) { - var value = ngModelCtrl.$viewValue; - var config = value.config; - if (config) { - var valid; - if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) { - valid = config && config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0; - ngModelCtrl.$setValidity('targetDeviceAliasIds', valid); - } else if (scope.widgetType === types.widgetType.alarm.value && scope.isDataEnabled) { - valid = config && config.alarmSource; - ngModelCtrl.$setValidity('alarmSource', valid); - } else if (scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) { - valid = config && config.datasources && config.datasources.length > 0; - ngModelCtrl.$setValidity('datasources', valid); - } - try { - angular.fromJson(scope.widgetStyle); - ngModelCtrl.$setValidity('widgetStyle', true); - } catch (e) { - ngModelCtrl.$setValidity('widgetStyle', false); - } - try { - angular.fromJson(scope.titleStyle); - ngModelCtrl.$setValidity('titleStyle', true); - } catch (e) { - ngModelCtrl.$setValidity('titleStyle', false); - } - } - } - }; - - scope.$watch('title + showTitleIcon + titleIcon + iconColor + iconSize + titleTooltip + showTitle + dropShadow + enableFullscreen + backgroundColor + ' + - 'color + padding + margin + widgetStyle + titleStyle + mobileOrder + mobileHeight + units + decimals + useDashboardTimewindow + ' + - 'displayTimewindow + alarmSearchStatus + alarmsPollingInterval + alarmsMaxCountLoad + alarmsFetchSize + showLegend', function () { - if (ngModelCtrl.$viewValue) { - var value = ngModelCtrl.$viewValue; - if (value.config) { - var config = value.config; - config.title = scope.title; - config.showTitleIcon = scope.showTitleIcon; - config.titleIcon = scope.titleIcon; - config.iconColor = scope.iconColor; - config.iconSize = scope.iconSize; - config.titleTooltip = scope.titleTooltip; - config.showTitle = scope.showTitle; - config.dropShadow = scope.dropShadow; - config.enableFullscreen = scope.enableFullscreen; - config.backgroundColor = scope.backgroundColor; - config.color = scope.color; - config.padding = scope.padding; - config.margin = scope.margin; - try { - config.widgetStyle = angular.fromJson(scope.widgetStyle); - } catch (e) { - config.widgetStyle = {}; - } - try { - config.titleStyle = angular.fromJson(scope.titleStyle); - } catch (e) { - config.titleStyle = {}; - } - config.units = scope.units; - config.decimals = scope.decimals; - config.useDashboardTimewindow = scope.useDashboardTimewindow; - config.displayTimewindow = scope.displayTimewindow; - config.alarmSearchStatus = scope.alarmSearchStatus; - config.alarmsPollingInterval = scope.alarmsPollingInterval; - config.alarmsMaxCountLoad = scope.alarmsMaxCountLoad; - config.alarmsFetchSize = scope.alarmsFetchSize; - config.showLegend = scope.showLegend; - } - if (value.layout) { - var layout = value.layout; - layout.mobileOrder = angular.isNumber(scope.mobileOrder) ? scope.mobileOrder : undefined; - layout.mobileHeight = scope.mobileHeight; - } - ngModelCtrl.$setViewValue(value); - scope.updateValidity(); - } - }); - - scope.$watch('currentSettings', function () { - if (ngModelCtrl.$viewValue) { - var value = ngModelCtrl.$viewValue; - if (value.config) { - value.config.settings = scope.currentSettings; - ngModelCtrl.$setViewValue(value); - } - } - }, true); - - scope.$watch('timewindow', function () { - if (ngModelCtrl.$viewValue) { - var value = ngModelCtrl.$viewValue; - if (value.config) { - value.config.timewindow = scope.timewindow; - ngModelCtrl.$setViewValue(value); - } - } - }, true); - - scope.$watch('legendConfig', function () { - if (ngModelCtrl.$viewValue) { - var value = ngModelCtrl.$viewValue; - if (value.config) { - value.config.legendConfig = scope.legendConfig; - ngModelCtrl.$setViewValue(value); - } - } - }, true); - - scope.$watch('datasources', function () { - if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config - && scope.widgetType !== types.widgetType.rpc.value - && scope.widgetType !== types.widgetType.alarm.value - && scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) { - var value = ngModelCtrl.$viewValue; - var config = value.config; - if (config.datasources) { - config.datasources.splice(0, config.datasources.length); - } else { - config.datasources = []; - } - if (scope.datasources) { - for (var i = 0; i < scope.datasources.length; i++) { - config.datasources.push(scope.datasources[i].value); - } - } - ngModelCtrl.$setViewValue(value); - scope.updateValidity(); - } - }, true); - - scope.$watch('targetDeviceAlias.value', function () { - if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config && scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) { - var value = ngModelCtrl.$viewValue; - var config = value.config; - if (scope.targetDeviceAlias.value) { - config.targetDeviceAliasIds = [scope.targetDeviceAlias.value.id]; - } else { - config.targetDeviceAliasIds = []; - } - ngModelCtrl.$setViewValue(value); - scope.updateValidity(); - } - }); - - scope.$watch('alarmSource.value', function () { - if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config && scope.widgetType === types.widgetType.alarm.value && scope.isDataEnabled) { - var value = ngModelCtrl.$viewValue; - var config = value.config; - if (scope.alarmSource.value) { - config.alarmSource = scope.alarmSource.value; - } else { - config.alarmSource = null; - } - ngModelCtrl.$setViewValue(value); - scope.updateValidity(); - } - }); - - scope.$watch('actions', function () { - if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config) { - var value = ngModelCtrl.$viewValue; - var config = value.config; - config.actions = scope.actions; - ngModelCtrl.$setViewValue(value); - scope.updateValidity(); - /*if (scope.theForm) { - scope.theForm.$setDirty(); - }*/ - } - }, true); - - scope.addDatasource = function () { - var newDatasource; - if (scope.functionsOnly) { - newDatasource = angular.copy(utils.getDefaultDatasource(scope.datakeySettingsSchema.schema)); - newDatasource.dataKeys = [scope.generateDataKey('Sin', types.dataKeyType.function)]; - } else { - newDatasource = { type: types.datasourceType.entity, - dataKeys: [] - }; - } - var datasource = {value: newDatasource}; - scope.datasources.push(datasource); - if (scope.theForm) { - scope.theForm.$setDirty(); - } - } - - scope.removeDatasource = function ($event, datasource) { - var index = scope.datasources.indexOf(datasource); - if (index > -1) { - scope.datasources.splice(index, 1); - if (scope.theForm) { - scope.theForm.$setDirty(); - } - } - }; - - scope.generateDataKey = function (chip, type) { - - if (angular.isObject(chip)) { - chip._hash = Math.random(); - return chip; - } - - var label = chip; - if (type === types.dataKeyType.alarm || type === types.dataKeyType.entityField) { - var keyField = type === types.dataKeyType.alarm ? types.alarmFields[chip] : types.entityField[chip]; - if (keyField) { - label = $translate.instant(keyField.name)+''; - } - } - label = scope.genNextLabel(label); - - var result = { - name: chip, - type: type, - label: label, - color: scope.genNextColor(), - settings: {}, - _hash: Math.random() - }; - - if (type === types.dataKeyType.function) { - result.name = 'f(x)'; - result.funcBody = utils.getPredefinedFunctionBody(chip); - if (!result.funcBody) { - result.funcBody = "return prevValue + 1;"; - } - } - - if (angular.isDefined(scope.datakeySettingsSchema.schema)) { - result.settings = utils.generateObjectFromJsonSchema(scope.datakeySettingsSchema.schema); - } - - return result; - }; - - scope.genNextLabel = function (name) { - var label = name; - var value = ngModelCtrl.$viewValue; - var i = 1; - var matches = false; - do { - matches = false; - var datasources = scope.widgetType == types.widgetType.alarm.value ? [value.config.alarmSource] : value.config.datasources; - if (datasources) { - for (var d=0;d - - -
    -
    - {{ 'widget-config.use-dashboard-timewindow' | translate }} - - {{ 'widget-config.display-timewindow' | translate }} - -
    -
    - widget-config.timewindow - -
    -
    -
    -
    - - - - - {{ ('alarm.search-status.' + searchStatus) | translate }} - - - - - - -
    -
    alarm.polling-interval-required
    -
    alarm.min-polling-interval-message
    -
    -
    -
    -
    - - - -
    -
    alarm.max-count-load-required
    -
    alarm.max-count-load-error-min
    -
    -
    - - - -
    -
    alarm.fetch-size-required
    -
    alarm.fetch-size-error-min
    -
    -
    -
    -
    - - - -
    -
    {{ 'widget-config.datasources' | translate }}
    -
    -
    -
    - -
    - datasource.add-datasource-prompt -
    -
    -
    - -
    - widget-config.datasource-type - widget-config.datasource-parameters - -
    -
    -
    -
    - {{$index + 1}}. -
    - - - - {{ 'widget-config.remove-datasource' | translate }} - - - close - - -
    -
    -
    -
    -
    - - - {{ 'widget-config.add-datasource' | translate }} - - add - action.add - -
    -
    -
    -
    - - - - {{ 'widget-config.target-device' | translate }} - - - - - - - - - - - {{ 'widget-config.alarm-source' | translate }} - - - - - - -
    -
    - -
    - - widget-config.general-settings -
    - - - - -
    - -
    -
    -
    -
    -
    -
    - {{ 'widget-config.display-icon' | translate }} - -
    -
    - - -
    -
    - - - - - - - - -
    -
    -
    - {{ 'widget-config.display-title' | translate }} - -
    -
    - {{ 'widget-config.drop-shadow' | translate }} - -
    -
    - {{ 'widget-config.enable-fullscreen' | translate }} - -
    -
    - -
    -
    -
    -
    -
    -
    -
    - - - - - - - - -
    -
    - - - - - - - - -
    -
    - {{ 'widget-config.display-legend' | translate }} - -
    - -
    -
    - widget-config.mobile-mode-settings -
    - - - - - - - - -
    -
    -
    -
    - - - - - - - - - - - - - - - diff --git a/ui/src/app/components/widget/widget.controller.js b/ui/src/app/components/widget/widget.controller.js deleted file mode 100644 index 83225edcc2..0000000000 --- a/ui/src/app/components/widget/widget.controller.js +++ /dev/null @@ -1,1074 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import $ from 'jquery'; -import 'javascript-detect-element-resize/detect-element-resize'; -import Subscription from '../../api/subscription'; - -import 'oclazyload'; -import cssjs from '../../../vendor/css.js/css'; - -/* eslint-disable angular/angularelement */ - -/*@ngInject*/ -export default function WidgetController($scope, $state, $timeout, $window, $ocLazyLoad, $element, $q, $log, $injector, $filter, $compile, tbRaf, types, utils, timeService, - datasourceService, alarmService, entityService, dashboardService, deviceService, visibleRect, isEdit, isMobile, dashboardTimewindow, - dashboardTimewindowApi, dashboard, widget, aliasController, stateController, widgetInfo, widgetType, toast) { - - var vm = this; - - $scope.$timeout = $timeout; - $scope.$q = $q; - $scope.$injector = $injector; - $scope.tbRaf = tbRaf; - - $scope.rpcRejection = null; - $scope.rpcErrorText = null; - $scope.rpcEnabled = false; - $scope.executingRpcRequest = false; - - vm.dashboardTimewindow = dashboardTimewindow; - - $window.lazyLoad = $ocLazyLoad; - $window.cssjs = cssjs; - - var cssParser = new cssjs(); - cssParser.testMode = false; - - var gridsterItemInited = false; - var subscriptionInited = false; - var widgetSizeDetected = false; - - var cafs = {}; - - var actionDescriptorsBySourceId = {}; - if (widget.config.actions) { - for (var actionSourceId in widget.config.actions) { - var descriptors = widget.config.actions[actionSourceId]; - var actionDescriptors = []; - descriptors.forEach(function(descriptor) { - var actionDescriptor = angular.copy(descriptor); - actionDescriptor.displayName = utils.customTranslation(descriptor.name, descriptor.name); - actionDescriptors.push(actionDescriptor); - }); - actionDescriptorsBySourceId[actionSourceId] = actionDescriptors; - } - } - - var widgetContext = { - inited: false, - $container: null, - $containerParent: null, - width: 0, - height: 0, - hideTitlePanel: false, - isEdit: isEdit, - isMobile: isMobile, - dashboard: dashboard, - widgetConfig: widget.config, - settings: widget.config.settings, - units: widget.config.units || '', - decimals: angular.isDefined(widget.config.decimals) ? widget.config.decimals : 2, - subscriptions: {}, - defaultSubscription: null, - dashboardTimewindow: dashboardTimewindow, - timewindowFunctions: { - onUpdateTimewindow: function(startTimeMs, endTimeMs, interval) { - if (widgetContext.defaultSubscription) { - widgetContext.defaultSubscription.onUpdateTimewindow(startTimeMs, endTimeMs, interval); - } - }, - onResetTimewindow: function() { - if (widgetContext.defaultSubscription) { - widgetContext.defaultSubscription.onResetTimewindow(); - } - } - }, - subscriptionApi: { - createSubscription: function(options, subscribe) { - return createSubscription(options, subscribe); - }, - createSubscriptionFromInfo: function (type, subscriptionsInfo, options, useDefaultComponents, subscribe) { - return createSubscriptionFromInfo(type, subscriptionsInfo, options, useDefaultComponents, subscribe); - }, - removeSubscription: function(id) { - var subscription = widgetContext.subscriptions[id]; - if (subscription) { - subscription.destroy(); - delete widgetContext.subscriptions[id]; - } - } - }, - controlApi: { - sendOneWayCommand: function(method, params, timeout) { - if (widgetContext.defaultSubscription) { - return widgetContext.defaultSubscription.sendOneWayCommand(method, params, timeout); - } - return null; - }, - sendTwoWayCommand: function(method, params, timeout) { - if (widgetContext.defaultSubscription) { - return widgetContext.defaultSubscription.sendTwoWayCommand(method, params, timeout); - } - return null; - } - }, - utils: { - formatValue: formatValue - }, - actionsApi: { - actionDescriptorsBySourceId: actionDescriptorsBySourceId, - getActionDescriptors: getActionDescriptors, - handleWidgetAction: handleWidgetAction, - elementClick: elementClick, - getActiveEntityInfo: getActiveEntityInfo - }, - stateController: stateController, - aliasController: aliasController - }; - - widgetContext.customHeaderActions = []; - var headerActionsDescriptors = getActionDescriptors(types.widgetActionSources.headerButton.value); - for (var i=0;i 0) { - try { - if (!additionalParams) { - additionalParams = {}; - } - var customActionFunction = new Function('$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', customFunction); - customActionFunction($event, widgetContext, entityId, entityName, additionalParams); - } catch (e) { - // - } - } - break; - case types.widgetActionTypes.customPretty.value: - var customPrettyFunction = descriptor.customFunction; - var customHtml = descriptor.customHtml; - var customCss = descriptor.customCss; - var customResources = descriptor.customResources; - var actionNamespace = 'custom-action-pretty-'+descriptor.name.toLowerCase(); - var htmlTemplate = ''; - if (angular.isDefined(customHtml) && customHtml.length > 0) { - htmlTemplate = customHtml; - } - loadCustomActionResources(actionNamespace, customCss, customResources).then( - function success() { - if (angular.isDefined(customPrettyFunction) && customPrettyFunction.length > 0) { - try { - if (!additionalParams) { - additionalParams = {}; - } - var customActionPrettyFunction = new Function('$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams', customPrettyFunction); - customActionPrettyFunction($event, widgetContext, entityId, entityName, htmlTemplate, additionalParams); - } catch (e) { - // - } - } - }, - function fail(errorMessages) { - processResourcesLoadErrors(errorMessages); - } - ); - break; - } - } - - function loadCustomActionResources(actionNamespace, customCss, customResources) { - var deferred = $q.defer(); - - if (angular.isDefined(customCss) && customCss.length > 0) { - cssParser.cssPreviewNamespace = actionNamespace; - cssParser.createStyleElement(actionNamespace, customCss, 'nonamespace'); - } - - function loadNextOrComplete(i) { - i++; - if (i < customResources.length) { - loadNext(i); - } else { - if (errors.length > 0) { - deferred.reject(errors); - } else { - deferred.resolve(); - } - } - } - - function loadNext(i) { - var resourceUrl = customResources[i].url; - if (resourceUrl && resourceUrl.length > 0) { - $ocLazyLoad.load(resourceUrl).then( - function success () { - loadNextOrComplete(i); - }, - function fail() { - errors.push('Failed to load custom action resource: \'' + resourceUrl + '\''); - loadNextOrComplete(i); - } - ); - } else { - loadNextOrComplete(i); - } - } - - if (angular.isDefined(customResources) && customResources.length > 0) { - var errors = []; - loadNext(0); - } else { - deferred.resolve(); - } - - return deferred.promise; - } - - function processResourcesLoadErrors(errorMessages) { - var messageToShow = ''; - for (var e in errorMessages) { - var error = errorMessages[e]; - messageToShow += '
    ' + error + '
    '; - } - toast.showError(messageToShow); - } - - function getActiveEntityInfo() { - var entityInfo = widgetContext.activeEntityInfo; - if (!entityInfo) { - for (var id in widgetContext.subscriptions) { - var subscription = widgetContext.subscriptions[id]; - entityInfo = subscription.getFirstEntityInfo(); - if (entityInfo) { - break; - } - } - } - return entityInfo; - } - - function configureWidgetElement() { - - $scope.displayLegend = angular.isDefined(widget.config.showLegend) ? - widget.config.showLegend : widget.type === types.widgetType.timeseries.value; - - if ($scope.displayLegend) { - $scope.legendConfig = widget.config.legendConfig || - { - position: types.position.bottom.value, - showMin: false, - showMax: false, - showAvg: widget.type === types.widgetType.timeseries.value, - showTotal: false - }; - $scope.legendData = { - keys: [], - data: [] - }; - } - - var html = '
    ' + - 'Widget Error: {{ widgetErrorData.name + ": " + widgetErrorData.message}}' + - '
    ' + - '
    ' + - '' + - '
    '; - - var containerHtml = '
    ' + widgetInfo.templateHtml + '
    '; - if ($scope.displayLegend) { - var layoutType; - if ($scope.legendConfig.position === types.position.top.value || - $scope.legendConfig.position === types.position.bottom.value) { - layoutType = 'column'; - } else { - layoutType = 'row'; - } - - var legendStyle; - switch($scope.legendConfig.position) { - case types.position.top.value: - legendStyle = 'padding-bottom: 8px; max-height: 50%; overflow-y: auto;'; - break; - case types.position.bottom.value: - legendStyle = 'padding-top: 8px; max-height: 50%; overflow-y: auto;'; - break; - case types.position.left.value: - legendStyle = 'padding-right: 0px; max-width: 50%; overflow-y: auto;'; - break; - case types.position.right.value: - legendStyle = 'padding-left: 0px; max-width: 50%; overflow-y: auto;'; - break; - } - - var legendHtml = ''; - containerHtml = '
    ' + containerHtml + '
    '; - html += '
    '; - if ($scope.legendConfig.position === types.position.top.value || - $scope.legendConfig.position === types.position.left.value) { - html += legendHtml; - html += containerHtml; - } else { - html += containerHtml; - html += legendHtml; - } - html += '
    '; - } else { - html += containerHtml; - } - - //TODO: - /*if (progressElement) { - progressScope.$destroy(); - progressScope = null; - - progressElement.remove(); - progressElement = null; - }*/ - - $element.html(html); - - var containerElement = $scope.displayLegend ? angular.element($element[0].querySelector('#widget-container')) : $element; - widgetContext.$container = $('#container', containerElement); - widgetContext.$containerParent = $(containerElement); - - if (widgetSizeDetected) { - widgetContext.$container.css('height', widgetContext.height + 'px'); - widgetContext.$container.css('width', widgetContext.width + 'px'); - } - - widgetContext.$scope = $scope.$new(); - - $compile($element.contents())(widgetContext.$scope); - - addResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef - } - - function destroyWidgetElement() { - removeResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef - $element.html(''); - if (widgetContext.$scope) { - widgetContext.$scope.$destroy(); - } - widgetContext.$container = null; - widgetContext.$containerParent = null; - } - - function initialize() { - - $scope.$on('toggleDashboardEditMode', function (event, isEdit) { - onEditModeChanged(isEdit); - }); - - $scope.$watch(function () { - return widget.row + ',' + widget.col + ',' + widget.config.mobileOrder; - }, function () { - //updateBounds(); - $scope.$emit("widgetPositionChanged", widget); - }); - - $scope.$on('gridster-item-resized', function (event, item) { - if (!widgetContext.isMobile) { - widget.sizeX = item.sizeX; - widget.sizeY = item.sizeY; - } - }); - - $scope.$on('mobileModeChanged', function (event, newIsMobile) { - onMobileModeChanged(newIsMobile); - }); - - $scope.$on('entityAliasesChanged', function (event, aliasIds) { - var subscriptionChanged = false; - for (var id in widgetContext.subscriptions) { - var subscription = widgetContext.subscriptions[id]; - subscriptionChanged = subscriptionChanged || subscription.onAliasesChanged(aliasIds); - } - if (subscriptionChanged && !vm.typeParameters.useCustomDatasources) { - reInit(); - } - }); - - $scope.$on('dashboardTimewindowChanged', function (event, newDashboardTimewindow) { - vm.dashboardTimewindow = newDashboardTimewindow; - widgetContext.dashboardTimewindow = newDashboardTimewindow; - }); - - $scope.$on('widgetForceReInit', function () { - reInit(); - }); - - $scope.$on("$destroy", function () { - onDestroy(); - }); - - configureWidgetElement(); - var deferred = $q.defer(); - if (!vm.typeParameters.useCustomDatasources) { - createDefaultSubscription().then( - function success() { - subscriptionInited = true; - deferred.resolve(); - }, - function fail() { - subscriptionInited = true; - deferred.reject(); - } - ); - } else { - $scope.loadingData = false; - subscriptionInited = true; - deferred.resolve(); - } - return deferred.promise; - } - - function reInit() { - onDestroy(); - configureWidgetElement(); - if (!vm.typeParameters.useCustomDatasources) { - createDefaultSubscription().then( - function success() { - subscriptionInited = true; - onInit(); - }, - function fail() { - subscriptionInited = true; - onInit(); - } - ); - } else { - subscriptionInited = true; - onInit(); - } - } - - function handleWidgetException(e) { - $log.error(e); - $scope.widgetErrorData = utils.processWidgetException(e); - } - - function isReady() { - return subscriptionInited && gridsterItemInited && widgetSizeDetected; - } - - function onInit(skipSizeCheck) { - if (!widgetContext.$containerParent) { - return; - } - if (!skipSizeCheck) { - checkSize(); - } - if (!widgetContext.inited && isReady()) { - widgetContext.inited = true; - try { - if (displayWidgetInstance()) { - widgetTypeInstance.onInit(); - } else { - $scope.loadingData = false; - } - } catch (e) { - handleWidgetException(e); - } - if (!vm.typeParameters.useCustomDatasources && widgetContext.defaultSubscription) { - widgetContext.defaultSubscription.subscribe(); - } - } - } - - function checkSize() { - var width = widgetContext.$containerParent.width(); - var height = widgetContext.$containerParent.height(); - var sizeChanged = false; - - if (!widgetContext.width || widgetContext.width != width || !widgetContext.height || widgetContext.height != height) { - if (width > 0 && height > 0) { - widgetContext.$container.css('height', height + 'px'); - widgetContext.$container.css('width', width + 'px'); - widgetContext.width = width; - widgetContext.height = height; - sizeChanged = true; - widgetSizeDetected = true; - } - } - return sizeChanged; - } - - function onResize() { - if (checkSize()) { - if (widgetContext.inited) { - if (cafs['resize']) { - cafs['resize'](); - cafs['resize'] = null; - } - cafs['resize'] = tbRaf(function() { - try { - if (displayWidgetInstance()) { - widgetTypeInstance.onResize(); - } - } catch (e) { - handleWidgetException(e); - } - }); - } else { - onInit(true); - } - } - } - - function gridsterItemInitialized(item) { - if (item && item.gridster) { - gridsterItemInited = true; - onInit(); - // gridsterItemElement = $(item.$element); - //updateVisibility(); - } - } - - function onEditModeChanged(isEdit) { - if (widgetContext.isEdit != isEdit) { - widgetContext.isEdit = isEdit; - if (widgetContext.inited) { - if (cafs['editMode']) { - cafs['editMode'](); - cafs['editMode'] = null; - } - cafs['editMode'] = tbRaf(function() { - try { - if (displayWidgetInstance()) { - widgetTypeInstance.onEditModeChanged(); - } - } catch (e) { - handleWidgetException(e); - } - }); - } - } - } - - function onMobileModeChanged(isMobile) { - if (widgetContext.isMobile != isMobile) { - widgetContext.isMobile = isMobile; - if (widgetContext.inited) { - if (cafs['mobileMode']) { - cafs['mobileMode'](); - cafs['mobileMode'] = null; - } - cafs['mobileMode'] = tbRaf(function() { - try { - if (displayWidgetInstance()) { - widgetTypeInstance.onMobileModeChanged(); - } - } catch (e) { - handleWidgetException(e); - } - }); - } - } - } - - function isNumeric(val) { - return (val - parseFloat( val ) + 1) >= 0; - } - - function formatValue(value, dec, units, showZeroDecimals) { - if (angular.isDefined(value) && - value !== null && isNumeric(value)) { - var formatted = Number(value); - if (angular.isDefined(dec)) { - formatted = formatted.toFixed(dec); - } - if (!showZeroDecimals) { - formatted = (formatted * 1); - } - formatted = formatted.toString(); - if (angular.isDefined(units) && units.length > 0) { - formatted += ' ' + units; - } - return formatted; - } else { - return value; - } - } - - function displayWidgetInstance() { - if (widget.type !== types.widgetType.static.value) { - for (var id in widgetContext.subscriptions) { - if (widgetContext.subscriptions[id].isDataResolved()) { - return true; - } - } - return false; - } else { - return true; - } - } - - function onDestroy() { - var shouldDestroyWidgetInstance = displayWidgetInstance(); - for (var id in widgetContext.subscriptions) { - var subscription = widgetContext.subscriptions[id]; - subscription.destroy(); - } - subscriptionInited = false; - widgetContext.subscriptions = {}; - if (widgetContext.inited) { - widgetContext.inited = false; - for (var cafId in cafs) { - if (cafs[cafId]) { - cafs[cafId](); - cafs[cafId] = null; - } - } - try { - if (shouldDestroyWidgetInstance) { - widgetTypeInstance.onDestroy(); - } - } catch (e) { - handleWidgetException(e); - } - } - destroyWidgetElement(); - } - - //TODO: widgets visibility - /*function updateVisibility(forceRedraw) { - if (visibleRect) { - forceRedraw = forceRedraw || visibleRect.containerResized; - var newVisible = false; - if (visibleRect.isMobile && gridsterItemElement) { - var topPx = gridsterItemElement.position().top; - var bottomPx = topPx + widget.sizeY * visibleRect.curRowHeight; - newVisible = !(topPx > visibleRect.bottomPx || - bottomPx < visibleRect.topPx); - } else { - newVisible = !(bounds.left > visibleRect.right || - bounds.right < visibleRect.left || - bounds.top > visibleRect.bottom || - bounds.bottom < visibleRect.top); - } - if (visible != newVisible) { - visible = newVisible; - if (visible) { - onRedraw(50); - } - } else if (forceRedraw && visible) { - onRedraw(50); - } - } - }*/ - -/* function updateBounds() { - bounds = { - top: widget.row, - left: widget.col, - bottom: widget.row + widget.sizeY, - right: widget.col + widget.sizeX - }; - updateVisibility(true); - onRedraw(); - }*/ - -} - -/* eslint-enable angular/angularelement */ diff --git a/ui/src/app/components/widget/widget.directive.js b/ui/src/app/components/widget/widget.directive.js deleted file mode 100644 index c4caf61971..0000000000 --- a/ui/src/app/components/widget/widget.directive.js +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './widget.scss'; - -import thingsboardLegend from '../legend.directive'; -import thingsboardTypes from '../../common/types.constant'; -import thingsboardApiDatasource from '../../api/datasource.service'; - -import WidgetController from './widget.controller'; - -export default angular.module('thingsboard.directives.widget', [thingsboardLegend, thingsboardTypes, thingsboardApiDatasource]) - .controller('WidgetController', WidgetController) - .directive('tbWidget', Widget) - .name; - -/*@ngInject*/ -function Widget($controller, widgetService) { - return { - scope: true, - link: function (scope, elem, attrs) { - - var locals = scope.$eval(attrs.locals); - var widget = locals.widget; - - var widgetController; - var gridsterItem; - - //TODO: widgets visibility - /*scope.$on('visibleRectChanged', function (event, newVisibleRect) { - locals.visibleRect = newVisibleRect; - if (widgetController) { - widgetController.visibleRectChanged(newVisibleRect); - } - });*/ - - scope.$on('gridster-item-initialized', function (event, item) { - gridsterItem = item; - if (widgetController) { - widgetController.gridsterItemInitialized(gridsterItem); - } - }); - - //TODO: - //elem.html('
    ' + - // ' ' + - // '
    '); - - //var progressElement = angular.element(elem[0].querySelector('#progress-cover')); - //var progressScope = scope.$new(); - //$compile(elem.contents())(progressScope); - - widgetService.getWidgetInfo(widget.bundleAlias, widget.typeAlias, widget.isSystemType).then( - function(widgetInfo) { - loadFromWidgetInfo(widgetInfo); - } - ); - - function loadFromWidgetInfo(widgetInfo) { - - scope.loadingData = true; - - elem.addClass("tb-widget"); - - var widgetNamespace = "widget-type-" + (widget.isSystemType ? 'sys-' : '') - + widget.bundleAlias + '-' - + widget.typeAlias; - - elem.addClass(widgetNamespace); - - var widgetType = widgetService.getWidgetTypeFunction(widget.bundleAlias, widget.typeAlias, widget.isSystemType); - - angular.extend(locals, {$scope: scope, $element: elem, widgetInfo: widgetInfo, widgetType: widgetType}); - - widgetController = $controller('WidgetController', locals); - - if (gridsterItem) { - widgetController.gridsterItemInitialized(gridsterItem); - } - } - } - }; -} diff --git a/ui/src/app/components/widget/widget.scss b/ui/src/app/components/widget/widget.scss deleted file mode 100644 index 71a44b2c82..0000000000 --- a/ui/src/app/components/widget/widget.scss +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-widget { - .tb-widget-error { - display: flex; - align-items: center; - justify-content: center; - background: rgba(255, 255, 255, .5); - - span { - color: #f00; - } - } - - .tb-widget-loading { - z-index: 3; - background: rgba(255, 255, 255, .15); - } -} diff --git a/ui/src/app/components/widgets-bundle-select.directive.js b/ui/src/app/components/widgets-bundle-select.directive.js deleted file mode 100644 index 67c01a1000..0000000000 --- a/ui/src/app/components/widgets-bundle-select.directive.js +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './widgets-bundle-select.scss'; - -import thingsboardApiWidget from '../api/widget.service'; - -/* eslint-disable import/no-unresolved, import/default */ - -import widgetsBundleSelectTemplate from './widgets-bundle-select.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - - -export default angular.module('thingsboard.directives.widgetsBundleSelect', [thingsboardApiWidget]) - .directive('tbWidgetsBundleSelect', WidgetsBundleSelect) - .name; - -/*@ngInject*/ -function WidgetsBundleSelect($compile, $templateCache, widgetService, types) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - var template = $templateCache.get(widgetsBundleSelectTemplate); - element.html(template); - - scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false; - scope.widgetsBundle = null; - scope.widgetsBundles = []; - - var widgetsBundleFetchFunction = widgetService.getAllWidgetsBundles; - if (angular.isDefined(scope.bundlesScope)) { - if (scope.bundlesScope === 'system') { - widgetsBundleFetchFunction = widgetService.getSystemWidgetsBundles; - } else if (scope.bundlesScope === 'tenant') { - widgetsBundleFetchFunction = widgetService.getTenantWidgetsBundles; - } - } - - widgetsBundleFetchFunction({ignoreLoading: true}).then( - function success(widgetsBundles) { - scope.widgetsBundles = widgetsBundles; - if (scope.selectFirstBundle) { - if (widgetsBundles.length > 0) { - scope.widgetsBundle = widgetsBundles[0]; - } - } else if (angular.isDefined(scope.selectBundleAlias)) { - selectWidgetsBundleByAlias(scope.selectBundleAlias); - } - }, - function fail() { - } - ); - - function selectWidgetsBundleByAlias(alias) { - if (scope.widgetsBundles && alias) { - for (var w in scope.widgetsBundles) { - var widgetsBundle = scope.widgetsBundles[w]; - if (widgetsBundle.alias === alias) { - scope.widgetsBundle = widgetsBundle; - break; - } - } - } - } - - scope.isSystem = function(item) { - return item && item.tenantId.id === types.id.nullUid; - } - - scope.updateView = function () { - ngModelCtrl.$setViewValue(scope.widgetsBundle); - } - - ngModelCtrl.$render = function () { - if (ngModelCtrl.$viewValue) { - scope.widgetsBundle = ngModelCtrl.$viewValue; - } - } - - scope.$watch('widgetsBundle', function () { - scope.updateView(); - }); - - scope.$watch('selectBundleAlias', function (newVal, prevVal) { - if (newVal !== prevVal) { - selectWidgetsBundleByAlias(scope.selectBundleAlias); - } - }); - - $compile(element.contents())(scope); - } - - return { - restrict: "E", - require: "^ngModel", - link: linker, - scope: { - bundlesScope: '@', - theForm: '=?', - tbRequired: '=?', - selectFirstBundle: '=', - selectBundleAlias: '=?' - } - }; -} \ No newline at end of file diff --git a/ui/src/app/components/widgets-bundle-select.scss b/ui/src/app/components/widgets-bundle-select.scss deleted file mode 100644 index 1d4b202ab6..0000000000 --- a/ui/src/app/components/widgets-bundle-select.scss +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "../../scss/mixins"; - -tb-widgets-bundle-select { - md-select { - padding: 5px 20px; - margin: 0; - } - - .tb-bundle-item { - height: 24px; - line-height: 24px; - } -} - -.tb-widgets-bundle-select { - .tb-bundle-item { - height: 48px; - line-height: 48px; - } -} - -tb-widgets-bundle-select, -.tb-widgets-bundle-select { - .md-text { - display: block; - width: 100%; - } - - .tb-bundle-item { - display: inline-block; - width: 100%; - - span { - display: inline-block; - vertical-align: middle; - } - - .tb-bundle-system { - float: right; - font-size: .8rem; - opacity: .8; - } - } - - md-option { - height: auto !important; - white-space: normal !important; - } -} - -md-toolbar { - tb-widgets-bundle-select { - md-select { - background: rgba(255, 255, 255, .2); - - .md-select-value { - font-size: 1.2rem; - color: #fff; - - span:first-child::after { - color: #fff; - } - } - - .md-select-value.md-select-placeholder { - color: #fff; - opacity: .8; - } - } - - md-select.ng-invalid.ng-touched { - .md-select-value { - color: #fff !important; - } - } - } -} diff --git a/ui/src/app/components/widgets-bundle-select.tpl.html b/ui/src/app/components/widgets-bundle-select.tpl.html deleted file mode 100644 index fce4fcd5b1..0000000000 --- a/ui/src/app/components/widgets-bundle-select.tpl.html +++ /dev/null @@ -1,29 +0,0 @@ - - - -
    - {{item.title}} - widgets-bundle.system -
    -
    -
    diff --git a/ui/src/app/customer/add-customer.tpl.html b/ui/src/app/customer/add-customer.tpl.html deleted file mode 100644 index 54acc373dc..0000000000 --- a/ui/src/app/customer/add-customer.tpl.html +++ /dev/null @@ -1,46 +0,0 @@ - - -
    - -
    -

    customer.add

    - -
    - - - -
    -
    - - - -
    - -
    -
    - - - - {{ 'action.add' | translate }} - - {{ 'action.cancel' | translate }} - -
    -
    - diff --git a/ui/src/app/customer/customer-card.tpl.html b/ui/src/app/customer/customer-card.tpl.html deleted file mode 100644 index ec233d758c..0000000000 --- a/ui/src/app/customer/customer-card.tpl.html +++ /dev/null @@ -1,18 +0,0 @@ - -
    {{ item | contactShort }}
    \ No newline at end of file diff --git a/ui/src/app/customer/customer-fieldset.tpl.html b/ui/src/app/customer/customer-fieldset.tpl.html deleted file mode 100644 index 11b2d58fb8..0000000000 --- a/ui/src/app/customer/customer-fieldset.tpl.html +++ /dev/null @@ -1,49 +0,0 @@ - -{{ 'customer.manage-users' | translate }} -{{ 'customer.manage-assets' | translate }} -{{ 'customer.manage-devices' | translate }} -{{ 'customer.manage-dashboards' | translate }} -{{ 'customer.delete' | translate }} - -
    - - - customer.copyId - -
    - - -
    - - - -
    -
    customer.title-required
    -
    -
    - - - - - -
    -
    diff --git a/ui/src/app/customer/customer.controller.js b/ui/src/app/customer/customer.controller.js deleted file mode 100644 index f9399afedb..0000000000 --- a/ui/src/app/customer/customer.controller.js +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import addCustomerTemplate from './add-customer.tpl.html'; -import customerCard from './customer-card.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function CustomerController(customerService, $state, $stateParams, $translate, types) { - - var customerActionsList = [ - { - onAction: function ($event, item) { - openCustomerUsers($event, item); - }, - name: function() { return $translate.instant('user.users') }, - details: function() { return $translate.instant('customer.manage-customer-users') }, - icon: "account_circle", - isEnabled: function(customer) { - return customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic); - } - }, - { - onAction: function ($event, item) { - openCustomerAssets($event, item); - }, - name: function() { return $translate.instant('asset.assets') }, - details: function(customer) { - if (customer && customer.additionalInfo && customer.additionalInfo.isPublic) { - return $translate.instant('customer.manage-public-assets') - } else { - return $translate.instant('customer.manage-customer-assets') - } - }, - icon: "domain" - }, - { - onAction: function ($event, item) { - openCustomerDevices($event, item); - }, - name: function() { return $translate.instant('device.devices') }, - details: function(customer) { - if (customer && customer.additionalInfo && customer.additionalInfo.isPublic) { - return $translate.instant('customer.manage-public-devices') - } else { - return $translate.instant('customer.manage-customer-devices') - } - }, - icon: "devices_other" - }, - { - onAction: function ($event, item) { - openCustomerDashboards($event, item); - }, - name: function() { return $translate.instant('dashboard.dashboards') }, - details: function(customer) { - if (customer && customer.additionalInfo && customer.additionalInfo.isPublic) { - return $translate.instant('customer.manage-public-dashboards') - } else { - return $translate.instant('customer.manage-customer-dashboards') - } - }, - icon: "dashboard" - }, - { - onAction: function ($event, item) { - vm.grid.deleteItem($event, item); - }, - name: function() { return $translate.instant('action.delete') }, - details: function() { return $translate.instant('customer.delete') }, - icon: "delete", - isEnabled: function(customer) { - return customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic); - } - } - ]; - - var vm = this; - - vm.types = types; - - vm.customerGridConfig = { - - refreshParamsFunc: null, - - deleteItemTitleFunc: deleteCustomerTitle, - deleteItemContentFunc: deleteCustomerText, - deleteItemsTitleFunc: deleteCustomersTitle, - deleteItemsActionTitleFunc: deleteCustomersActionTitle, - deleteItemsContentFunc: deleteCustomersText, - - fetchItemsFunc: fetchCustomers, - saveItemFunc: saveCustomer, - deleteItemFunc: deleteCustomer, - - getItemTitleFunc: getCustomerTitle, - - itemCardTemplateUrl: customerCard, - - actionsList: customerActionsList, - - onGridInited: gridInited, - - addItemTemplateUrl: addCustomerTemplate, - - addItemText: function() { return $translate.instant('customer.add-customer-text') }, - noItemsText: function() { return $translate.instant('customer.no-customers-text') }, - itemDetailsText: function(customer) { - if (customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic)) { - return $translate.instant('customer.customer-details') - } else { - return ''; - } - }, - isSelectionEnabled: function (customer) { - return customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic); - }, - isDetailsReadOnly: function (customer) { - return customer && customer.additionalInfo && customer.additionalInfo.isPublic; - } - }; - - if (angular.isDefined($stateParams.items) && $stateParams.items !== null) { - vm.customerGridConfig.items = $stateParams.items; - } - - if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) { - vm.customerGridConfig.topIndex = $stateParams.topIndex; - } - - vm.openCustomerUsers = openCustomerUsers; - vm.openCustomerAssets = openCustomerAssets; - vm.openCustomerDevices = openCustomerDevices; - vm.openCustomerDashboards = openCustomerDashboards; - - function deleteCustomerTitle(customer) { - return $translate.instant('customer.delete-customer-title', {customerTitle: customer.title}); - } - - function deleteCustomerText() { - return $translate.instant('customer.delete-customer-text'); - } - - function deleteCustomersTitle(selectedCount) { - return $translate.instant('customer.delete-customers-title', {count: selectedCount}, 'messageformat'); - } - - function deleteCustomersActionTitle(selectedCount) { - return $translate.instant('customer.delete-customers-action-title', {count: selectedCount}, 'messageformat'); - } - - function deleteCustomersText() { - return $translate.instant('customer.delete-customers-text'); - } - - function gridInited(grid) { - vm.grid = grid; - } - - function fetchCustomers(pageLink) { - return customerService.getCustomers(pageLink); - } - - function saveCustomer(customer) { - return customerService.saveCustomer(customer); - } - - function deleteCustomer(customerId) { - return customerService.deleteCustomer(customerId); - } - - function getCustomerTitle(customer) { - return customer ? customer.title : ''; - } - - function openCustomerUsers($event, customer) { - if ($event) { - $event.stopPropagation(); - } - $state.go('home.customers.users', {customerId: customer.id.id}); - } - - function openCustomerAssets($event, customer) { - if ($event) { - $event.stopPropagation(); - } - $state.go('home.customers.assets', {customerId: customer.id.id}); - } - - function openCustomerDevices($event, customer) { - if ($event) { - $event.stopPropagation(); - } - $state.go('home.customers.devices', {customerId: customer.id.id}); - } - - function openCustomerDashboards($event, customer) { - if ($event) { - $event.stopPropagation(); - } - $state.go('home.customers.dashboards', {customerId: customer.id.id}); - } -} diff --git a/ui/src/app/customer/customer.directive.js b/ui/src/app/customer/customer.directive.js deleted file mode 100644 index ff1befaf9e..0000000000 --- a/ui/src/app/customer/customer.directive.js +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import customerFieldsetTemplate from './customer-fieldset.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function CustomerDirective($compile, $templateCache, $translate, toast) { - var linker = function (scope, element) { - var template = $templateCache.get(customerFieldsetTemplate); - element.html(template); - - scope.isPublic = false; - - scope.onCustomerIdCopied = function() { - toast.showSuccess($translate.instant('customer.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left'); - }; - - scope.$watch('customer', function(newVal) { - if (newVal) { - if (scope.customer.additionalInfo) { - scope.isPublic = scope.customer.additionalInfo.isPublic; - } else { - scope.isPublic = false; - } - } - }); - - $compile(element.contents())(scope); - - } - return { - restrict: "E", - link: linker, - scope: { - customer: '=', - isEdit: '=', - theForm: '=', - onManageUsers: '&', - onManageAssets: '&', - onManageDevices: '&', - onManageDashboards: '&', - onDeleteCustomer: '&' - } - }; -} diff --git a/ui/src/app/customer/customer.routes.js b/ui/src/app/customer/customer.routes.js deleted file mode 100644 index fbaa9edc74..0000000000 --- a/ui/src/app/customer/customer.routes.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import customersTemplate from './customers.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function CustomerRoutes($stateProvider) { - - $stateProvider - .state('home.customers', { - url: '/customers', - params: {'topIndex': 0}, - module: 'private', - auth: ['TENANT_ADMIN'], - views: { - "content@home": { - templateUrl: customersTemplate, - controllerAs: 'vm', - controller: 'CustomerController' - } - }, - data: { - searchEnabled: true, - pageTitle: 'customer.customers' - }, - ncyBreadcrumb: { - label: '{"icon": "supervisor_account", "label": "customer.customers"}' - } - }); - -} diff --git a/ui/src/app/customer/customers.tpl.html b/ui/src/app/customer/customers.tpl.html deleted file mode 100644 index 396fc6734b..0000000000 --- a/ui/src/app/customer/customers.tpl.html +++ /dev/null @@ -1,75 +0,0 @@ - - - -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    diff --git a/ui/src/app/customer/index.js b/ui/src/app/customer/index.js deleted file mode 100644 index bd2757afa2..0000000000 --- a/ui/src/app/customer/index.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import uiRouter from 'angular-ui-router'; -import thingsboardApiCustomer from '../api/customer.service'; -import thingsboardGrid from '../components/grid.directive'; -import thingsboardContact from '../components/contact.directive'; -import thingsboardContactShort from '../components/contact-short.filter'; - -import CustomerRoutes from './customer.routes'; -import CustomerController from './customer.controller'; -import CustomerDirective from './customer.directive'; - -export default angular.module('thingsboard.customer', [ - uiRouter, - thingsboardApiCustomer, - thingsboardGrid, - thingsboardContact, - thingsboardContactShort -]) - .config(CustomerRoutes) - .controller('CustomerController', CustomerController) - .directive('tbCustomer', CustomerDirective) - .name; diff --git a/ui/src/app/dashboard/add-dashboard.tpl.html b/ui/src/app/dashboard/add-dashboard.tpl.html deleted file mode 100644 index 64dddcecfa..0000000000 --- a/ui/src/app/dashboard/add-dashboard.tpl.html +++ /dev/null @@ -1,45 +0,0 @@ - - -
    - -
    -

    dashboard.add

    - -
    - - - -
    -
    - - - -
    - -
    -
    - - - - {{ 'action.add' | translate }} - - {{ 'action.cancel' | translate }} - -
    -
    \ No newline at end of file diff --git a/ui/src/app/dashboard/add-dashboards-to-customer.controller.js b/ui/src/app/dashboard/add-dashboards-to-customer.controller.js deleted file mode 100644 index 3e7df307af..0000000000 --- a/ui/src/app/dashboard/add-dashboards-to-customer.controller.js +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/*@ngInject*/ -export default function AddDashboardsToCustomerController(dashboardService, $mdDialog, $q, customerId, dashboards) { - - var vm = this; - - vm.dashboards = dashboards; - vm.searchText = ''; - - vm.assign = assign; - vm.cancel = cancel; - vm.hasData = hasData; - vm.noData = noData; - vm.searchDashboardTextUpdated = searchDashboardTextUpdated; - vm.toggleDashboardSelection = toggleDashboardSelection; - - vm.theDashboards = { - getItemAtIndex: function (index) { - if (index > vm.dashboards.data.length) { - vm.theDashboards.fetchMoreItems_(index); - return null; - } - var item = vm.dashboards.data[index]; - if (item) { - item.indexNumber = index + 1; - } - return item; - }, - - getLength: function () { - if (vm.dashboards.hasNext) { - return vm.dashboards.data.length + vm.dashboards.nextPageLink.limit; - } else { - return vm.dashboards.data.length; - } - }, - - fetchMoreItems_: function () { - if (vm.dashboards.hasNext && !vm.dashboards.pending) { - vm.dashboards.pending = true; - dashboardService.getTenantDashboards(vm.dashboards.nextPageLink).then( - function success(dashboards) { - vm.dashboards.data = vm.dashboards.data.concat(dashboards.data); - vm.dashboards.nextPageLink = dashboards.nextPageLink; - vm.dashboards.hasNext = dashboards.hasNext; - if (vm.dashboards.hasNext) { - vm.dashboards.nextPageLink.limit = vm.dashboards.pageSize; - } - vm.dashboards.pending = false; - }, - function fail() { - vm.dashboards.hasNext = false; - vm.dashboards.pending = false; - }); - } - } - } - - function cancel () { - $mdDialog.cancel(); - } - - function assign () { - var tasks = []; - for (var dashboardId in vm.dashboards.selections) { - tasks.push(dashboardService.assignDashboardToCustomer(customerId, dashboardId)); - } - $q.all(tasks).then(function () { - $mdDialog.hide(); - }); - } - - function noData () { - return vm.dashboards.data.length == 0 && !vm.dashboards.hasNext; - } - - function hasData () { - return vm.dashboards.data.length > 0; - } - - function toggleDashboardSelection ($event, dashboard) { - $event.stopPropagation(); - var selected = angular.isDefined(dashboard.selected) && dashboard.selected; - dashboard.selected = !selected; - if (dashboard.selected) { - vm.dashboards.selections[dashboard.id.id] = true; - vm.dashboards.selectedCount++; - } else { - delete vm.dashboards.selections[dashboard.id.id]; - vm.dashboards.selectedCount--; - } - } - - function searchDashboardTextUpdated () { - vm.dashboards = { - pageSize: vm.dashboards.pageSize, - data: [], - nextPageLink: { - limit: vm.dashboards.pageSize, - textSearch: vm.searchText - }, - selections: {}, - selectedCount: 0, - hasNext: true, - pending: false - }; - } -} \ No newline at end of file diff --git a/ui/src/app/dashboard/add-dashboards-to-customer.tpl.html b/ui/src/app/dashboard/add-dashboards-to-customer.tpl.html deleted file mode 100644 index 2f4b1dd6c2..0000000000 --- a/ui/src/app/dashboard/add-dashboards-to-customer.tpl.html +++ /dev/null @@ -1,77 +0,0 @@ - - -
    - -
    -

    dashboard.assign-dashboard-to-customer

    - - - - -
    -
    - - - -
    -
    - dashboard.assign-dashboard-to-customer-text - - - - search - - - -
    - dashboard.no-dashboards-text - - - - - {{ dashboard.title }} - - - -
    -
    -
    -
    - - - - {{ 'action.assign' | translate }} - - {{ 'action.cancel' | - translate }} - - -
    -
    \ No newline at end of file diff --git a/ui/src/app/dashboard/add-widget.controller.js b/ui/src/app/dashboard/add-widget.controller.js deleted file mode 100644 index e664c075ee..0000000000 --- a/ui/src/app/dashboard/add-widget.controller.js +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import entityAliasDialogTemplate from '../entity/alias/entity-alias-dialog.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function AddWidgetController($scope, widgetService, entityService, $mdDialog, $q, $document, types, dashboard, - aliasController, widget, widgetInfo) { - - var vm = this; - - vm.dashboard = dashboard; - vm.aliasController = aliasController; - vm.widget = widget; - vm.widgetInfo = widgetInfo; - - vm.functionsOnly = false; - - vm.helpLinkIdForWidgetType = helpLinkIdForWidgetType; - vm.add = add; - vm.cancel = cancel; - vm.fetchEntityKeys = fetchEntityKeys; - vm.fetchDashboardStates = fetchDashboardStates; - vm.createEntityAlias = createEntityAlias; - - vm.widgetConfig = { - config: vm.widget.config, - layout: {} - }; - - vm.widgetConfig.layout.mobileOrder = vm.widget.config.mobileOrder; - vm.widgetConfig.layout.mobileHeight = vm.widget.config.mobileHeight; - - var settingsSchema = vm.widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema; - var dataKeySettingsSchema = vm.widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema; - - if (!settingsSchema || settingsSchema === '') { - vm.settingsSchema = {}; - } else { - vm.settingsSchema = angular.fromJson(settingsSchema); - } - if (!dataKeySettingsSchema || dataKeySettingsSchema === '') { - vm.dataKeySettingsSchema = {}; - } else { - vm.dataKeySettingsSchema = angular.fromJson(dataKeySettingsSchema); - } - - function helpLinkIdForWidgetType() { - var link = 'widgetsConfig'; - if (vm.widget && vm.widget.type) { - switch (vm.widget.type) { - case types.widgetType.timeseries.value: { - link = 'widgetsConfigTimeseries'; - break; - } - case types.widgetType.latest.value: { - link = 'widgetsConfigLatest'; - break; - } - case types.widgetType.rpc.value: { - link = 'widgetsConfigRpc'; - break; - } - case types.widgetType.alarm.value: { - link = 'widgetsConfigAlarm'; - break; - } - case types.widgetType.static.value: { - link = 'widgetsConfigStatic'; - break; - } - } - } - return link; - } - - function cancel () { - $mdDialog.cancel(); - } - - function add () { - if ($scope.theForm.$valid) { - $scope.theForm.$setPristine(); - vm.widget.config = vm.widgetConfig.config; - vm.widget.config.mobileOrder = vm.widgetConfig.layout.mobileOrder; - vm.widget.config.mobileHeight = vm.widgetConfig.layout.mobileHeight; - $mdDialog.hide({widget: vm.widget}); - } - } - - function fetchEntityKeys (entityAliasId, query, type) { - var deferred = $q.defer(); - vm.aliasController.getAliasInfo(entityAliasId).then( - function success(aliasInfo) { - var entity = aliasInfo.currentEntity; - if (entity) { - entityService.getEntityKeys(entity.entityType, entity.id, query, type, {ignoreLoading: true}).then( - function success(keys) { - deferred.resolve(keys); - }, - function fail() { - deferred.resolve([]); - } - ); - } else { - deferred.resolve([]); - } - }, - function fail() { - deferred.resolve([]); - } - ); - return deferred.promise; - } - - function fetchDashboardStates (query) { - var deferred = $q.defer(); - var stateIds = Object.keys(vm.dashboard.configuration.states); - var result = query ? stateIds.filter( - createFilterForDashboardState(query)) : stateIds; - if (result && result.length) { - deferred.resolve(result); - } else { - deferred.resolve([query]); - } - return deferred.promise; - } - - function createFilterForDashboardState (query) { - var lowercaseQuery = angular.lowercase(query); - return function filterFn(stateId) { - return (angular.lowercase(stateId).indexOf(lowercaseQuery) === 0); - }; - } - - function createEntityAlias (event, alias, allowedEntityTypes) { - - var deferred = $q.defer(); - var singleEntityAlias = {id: null, alias: alias, filter: {}}; - - $mdDialog.show({ - controller: 'EntityAliasDialogController', - controllerAs: 'vm', - templateUrl: entityAliasDialogTemplate, - locals: { - isAdd: true, - allowedEntityTypes: allowedEntityTypes, - entityAliases: vm.dashboard.configuration.entityAliases, - alias: singleEntityAlias - }, - parent: angular.element($document[0].body), - fullscreen: true, - multiple: true, - targetEvent: event - }).then(function (singleEntityAlias) { - vm.dashboard.configuration.entityAliases[singleEntityAlias.id] = singleEntityAlias; - vm.aliasController.updateEntityAliases(vm.dashboard.configuration.entityAliases); - deferred.resolve(singleEntityAlias); - }, function () { - deferred.reject(); - }); - - return deferred.promise; - } -} diff --git a/ui/src/app/dashboard/add-widget.tpl.html b/ui/src/app/dashboard/add-widget.tpl.html deleted file mode 100644 index 6c865cab18..0000000000 --- a/ui/src/app/dashboard/add-widget.tpl.html +++ /dev/null @@ -1,62 +0,0 @@ - - -
    - -
    -

    widget.add

    - -
    - - - -
    -
    - - - -
    -
    - -
    -
    -
    - - - - {{ 'action.add' | translate }} - - {{ 'action.cancel' | - translate }} - - -
    -
    diff --git a/ui/src/app/dashboard/dashboard-card.scss b/ui/src/app/dashboard/dashboard-card.scss deleted file mode 100644 index 7b3a5ace0e..0000000000 --- a/ui/src/app/dashboard/dashboard-card.scss +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-dashboard-assigned-customers { - display: block; - display: -webkit-box; /* stylelint-disable-line value-no-vendor-prefix */ - height: 34px; - margin-bottom: 4px; - overflow: hidden; - text-overflow: ellipsis; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; /* stylelint-disable-line property-no-vendor-prefix */ -} - diff --git a/ui/src/app/dashboard/dashboard-card.tpl.html b/ui/src/app/dashboard/dashboard-card.tpl.html deleted file mode 100644 index 65f51f1d12..0000000000 --- a/ui/src/app/dashboard/dashboard-card.tpl.html +++ /dev/null @@ -1,20 +0,0 @@ - -
    {{'dashboard.assignedToCustomers' | translate}}: '{{vm.item.assignedCustomersText}}'
    -
    {{'dashboard.public' | translate}}
    - diff --git a/ui/src/app/dashboard/dashboard-fieldset.tpl.html b/ui/src/app/dashboard/dashboard-fieldset.tpl.html deleted file mode 100644 index bf3283b857..0000000000 --- a/ui/src/app/dashboard/dashboard-fieldset.tpl.html +++ /dev/null @@ -1,80 +0,0 @@ - -{{ 'dashboard.export' | translate }} -{{ 'dashboard.make-public' | translate }} -{{ 'dashboard.make-private' | translate }} -{{ 'dashboard.manage-assigned-customers' | translate }} -{{ 'dashboard.unassign-from-customer' | translate }} -{{ 'dashboard.delete' | translate }} - - - - - -
    - - -
    - - - - - - - - {{ 'dashboard.copy-public-link' | translate }} - - -
    -
    -
    - - - -
    -
    dashboard.title-required
    -
    -
    - - - - -
    -
    diff --git a/ui/src/app/dashboard/dashboard-settings.controller.js b/ui/src/app/dashboard/dashboard-settings.controller.js deleted file mode 100644 index 15c85b0eb1..0000000000 --- a/ui/src/app/dashboard/dashboard-settings.controller.js +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './dashboard-settings.scss'; - -/*@ngInject*/ -export default function DashboardSettingsController($scope, $mdDialog, statesControllerService, settings, gridSettings) { - - var vm = this; - - vm.cancel = cancel; - vm.save = save; - vm.imageAdded = imageAdded; - vm.clearImage = clearImage; - - vm.stateControllerIdChanged = stateControllerIdChanged; - - vm.settings = settings; - vm.gridSettings = gridSettings; - vm.stateControllers = statesControllerService.getStateControllers(); - - if (vm.settings) { - if (angular.isUndefined(vm.settings.stateControllerId)) { - vm.settings.stateControllerId = 'entity'; - } - - if (angular.isUndefined(vm.settings.showTitle)) { - vm.settings.showTitle = false; - } - - if (angular.isUndefined(vm.settings.titleColor)) { - vm.settings.titleColor = 'rgba(0,0,0,0.870588)'; - } - - if (angular.isUndefined(vm.settings.showDashboardsSelect)) { - vm.settings.showDashboardsSelect = true; - } - - if (angular.isUndefined(vm.settings.showEntitiesSelect)) { - vm.settings.showEntitiesSelect = true; - } - - if (angular.isUndefined(vm.settings.showDashboardTimewindow)) { - vm.settings.showDashboardTimewindow = true; - } - - if (angular.isUndefined(vm.settings.showDashboardExport)) { - vm.settings.showDashboardExport = true; - } - - if (angular.isUndefined(vm.settings.toolbarAlwaysOpen)) { - vm.settings.toolbarAlwaysOpen = true; - } - } - - if (vm.gridSettings) { - vm.gridSettings.backgroundColor = vm.gridSettings.backgroundColor || 'rgba(0,0,0,0)'; - vm.gridSettings.color = vm.gridSettings.color || 'rgba(0,0,0,0.870588)'; - vm.gridSettings.columns = vm.gridSettings.columns || 24; - vm.gridSettings.margins = vm.gridSettings.margins || [10, 10]; - vm.gridSettings.autoFillHeight = angular.isDefined(vm.gridSettings.autoFillHeight) ? vm.gridSettings.autoFillHeight : false; - vm.gridSettings.mobileAutoFillHeight = angular.isDefined(vm.gridSettings.mobileAutoFillHeight) ? vm.gridSettings.mobileAutoFillHeight : false; - vm.gridSettings.mobileRowHeight = angular.isDefined(vm.gridSettings.mobileRowHeight) ? vm.gridSettings.mobileRowHeight : 70; - vm.hMargin = vm.gridSettings.margins[0]; - vm.vMargin = vm.gridSettings.margins[1]; - vm.gridSettings.backgroundSizeMode = vm.gridSettings.backgroundSizeMode || '100%'; - } - - function cancel() { - $mdDialog.cancel(); - } - - function imageAdded($file) { - var reader = new FileReader(); - reader.onload = function(event) { - $scope.$apply(function() { - if (event.target.result && event.target.result.startsWith('data:image/')) { - $scope.theForm.$setDirty(); - vm.gridSettings.backgroundImageUrl = event.target.result; - } - }); - }; - reader.readAsDataURL($file.file); - } - - function clearImage() { - $scope.theForm.$setDirty(); - vm.gridSettings.backgroundImageUrl = null; - } - - function stateControllerIdChanged() { - if (vm.settings.stateControllerId != 'default') { - vm.settings.toolbarAlwaysOpen = true; - } - } - - function save() { - $scope.theForm.$setPristine(); - if (vm.gridSettings) { - vm.gridSettings.margins = [vm.hMargin, vm.vMargin]; - } - $mdDialog.hide( - { - settings: vm.settings, - gridSettings: vm.gridSettings - } - ); - } -} diff --git a/ui/src/app/dashboard/dashboard-settings.scss b/ui/src/app/dashboard/dashboard-settings.scss deleted file mode 100644 index 54692295c1..0000000000 --- a/ui/src/app/dashboard/dashboard-settings.scss +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -$previewSize: 100px !default; - -.tb-image-select-container { - position: relative; - width: 100%; - height: $previewSize; -} - -.tb-image-preview { - width: auto; - max-width: $previewSize; - height: auto; - max-height: $previewSize; -} - -.tb-image-preview-container { - position: relative; - float: left; - width: $previewSize; - height: $previewSize; - margin-right: 12px; - vertical-align: top; - border: solid 1px; - - div { - width: 100%; - font-size: 18px; - text-align: center; - } - - div, - .tb-image-preview { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - } -} - -.tb-image-clear-container { - position: relative; - float: right; - width: 48px; - height: $previewSize; -} - -.tb-image-clear-btn { - position: absolute !important; - top: 50%; - transform: translate(0%, -50%) !important; -} diff --git a/ui/src/app/dashboard/dashboard-settings.tpl.html b/ui/src/app/dashboard/dashboard-settings.tpl.html deleted file mode 100644 index 86f2427da2..0000000000 --- a/ui/src/app/dashboard/dashboard-settings.tpl.html +++ /dev/null @@ -1,203 +0,0 @@ - - -
    - -
    -

    {{vm.settings ? 'dashboard.settings' : 'layout.settings'}}

    - - - - -
    -
    - - - -
    -
    -
    - - - - - {{stateControllerId}} - - - -
    - {{ 'dashboard.toolbar-always-open' | translate }} - - {{ 'dashboard.display-title' | translate }} - -
    -
    -
    - {{ 'dashboard.display-dashboards-selection' | translate }} - - {{ 'dashboard.display-entities-selection' | translate }} - - {{ 'dashboard.display-dashboard-timewindow' | translate }} - - {{ 'dashboard.display-dashboard-export' | translate }} - -
    -
    -
    -
    - - - -
    -
    dashboard.columns-count-required
    -
    dashboard.min-columns-count-message
    -
    dashboard.max-columns-count-message
    -
    -
    - dashboard.widgets-margins -
    - - - -
    -
    dashboard.horizontal-margin-required
    -
    dashboard.min-horizontal-margin-message
    -
    dashboard.max-horizontal-margin-message
    -
    -
    - - - -
    -
    dashboard.vertical-margin-required
    -
    dashboard.min-vertical-margin-message
    -
    dashboard.max-vertical-margin-message
    -
    -
    -
    - {{ 'dashboard.autofill-height' | translate }} - -
    -
    - -
    -
    -
    dashboard.no-image
    - -
    -
    - - - {{ 'action.remove' | translate }} - - - close - - -
    -
    - - -
    -
    -
    - - - - Fit width - Fit height - Cover - Contain - Original size - - - dashboard.mobile-layout -
    - {{ 'dashboard.autofill-height' | translate }} - - - - -
    -
    dashboard.mobile-row-height-required
    -
    dashboard.min-mobile-row-height-message
    -
    dashboard.max-mobile-row-height-message
    -
    -
    -
    -
    -
    -
    -
    - - - - {{ 'action.save' | translate }} - - {{ 'action.cancel' | translate }} - -
    -
    diff --git a/ui/src/app/dashboard/dashboard-toolbar.directive.js b/ui/src/app/dashboard/dashboard-toolbar.directive.js deleted file mode 100644 index fe8de8c636..0000000000 --- a/ui/src/app/dashboard/dashboard-toolbar.directive.js +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './dashboard-toolbar.scss'; - -import 'javascript-detect-element-resize/detect-element-resize'; - -/* eslint-disable import/no-unresolved, import/default */ - -import dashboardToolbarTemplate from './dashboard-toolbar.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function DashboardToolbar() { - return { - restrict: "E", - scope: true, - transclude: true, - bindToController: { - toolbarOpened: '=', - forceFullscreen: '=', - onTriggerClick: '&' - }, - controller: DashboardToolbarController, - controllerAs: 'vm', - templateUrl: dashboardToolbarTemplate - }; -} - -/* eslint-disable angular/angularelement */ - - -/*@ngInject*/ -function DashboardToolbarController($scope, $element, $timeout, mdFabToolbarAnimation) { - - let vm = this; - - vm.mdFabToolbarElement = angular.element($element[0].querySelector('md-fab-toolbar')); - - function initElements() { - $timeout(function() { - vm.mdFabBackgroundElement = angular.element(vm.mdFabToolbarElement[0].querySelector('.md-fab-toolbar-background')); - vm.mdFabTriggerElement = angular.element(vm.mdFabToolbarElement[0].querySelector('md-fab-trigger button')); - if (!vm.mdFabBackgroundElement || !vm.mdFabBackgroundElement[0]) { - initElements(); - } else { - triggerFabResize(); - } - }); - } - - addResizeListener(vm.mdFabToolbarElement[0], triggerFabResize); // eslint-disable-line no-undef - - $scope.$on("$destroy", function () { - removeResizeListener(vm.mdFabToolbarElement[0], triggerFabResize); // eslint-disable-line no-undef - }); - - initElements(); - - function triggerFabResize() { - if (!vm.mdFabBackgroundElement || !vm.mdFabBackgroundElement[0]) { - return; - } - var ctrl = vm.mdFabToolbarElement.controller('mdFabToolbar'); - if (ctrl.isOpen) { - if (!vm.mdFabBackgroundElement[0].offsetWidth) { - mdFabToolbarAnimation.addClass(vm.mdFabToolbarElement, 'md-is-open', function () { - }); - } else { - var color = window.getComputedStyle(vm.mdFabTriggerElement[0]).getPropertyValue('background-color'); //eslint-disable-line - - var width = vm.mdFabToolbarElement[0].offsetWidth; - var scale = 2 * (width / vm.mdFabTriggerElement[0].offsetWidth); - vm.mdFabBackgroundElement[0].style.backgroundColor = color; - vm.mdFabBackgroundElement[0].style.borderRadius = width + 'px'; - - var transform = vm.mdFabBackgroundElement[0].style.transform; - var targetTransform = 'scale(' + scale + ')'; - if (!transform || !angular.equals(transform, targetTransform)) { - vm.mdFabBackgroundElement[0].style.transform = targetTransform; - } - } - } - } -} - diff --git a/ui/src/app/dashboard/dashboard-toolbar.scss b/ui/src/app/dashboard/dashboard-toolbar.scss deleted file mode 100644 index 96369c8ed2..0000000000 --- a/ui/src/app/dashboard/dashboard-toolbar.scss +++ /dev/null @@ -1,199 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "~compass-sass-mixins/lib/compass"; - -@import "../../scss/constants"; - -$toolbar-height: 50px !default; -$fullscreen-toolbar-height: 64px !default; -$mobile-toolbar-height: 80px !default; -$half-mobile-toolbar-height: 40px !default; -$mobile-toolbar-height-total: 84px !default; - -tb-dashboard-toolbar { - md-fab-toolbar { - &.md-is-open { - md-fab-trigger { - .md-button { - &.md-fab { - opacity: 1; - - transition: opacity .3s cubic-bezier(.55, 0, .55, .2); - - .md-fab-toolbar-background { - background-color: $primary-default !important; - } - } - } - } - } - - md-fab-trigger { - .md-button { - &.md-fab { - width: 36px; - height: 36px; - margin: 4px 0 0 4px; - line-height: 36px; - opacity: .5; - - transition: opacity .3s cubic-bezier(.55, 0, .55, .2) .2s; - - md-icon { - position: absolute; - top: 25%; - width: 18px; - min-width: 18px; - height: 18px; - min-height: 18px; - margin: 0; - line-height: 18px; - } - } - } - } - - &.is-fullscreen { - &.md-is-open { - md-fab-trigger { - .md-button { - &.md-fab { - .md-fab-toolbar-background { - transition-delay: 0ms !important; - transition-duration: 0ms !important; - } - } - } - } - } - - .md-fab-toolbar-wrapper { - height: $mobile-toolbar-height-total; - - @media (min-width: $layout-breakpoint-sm) { - height: $fullscreen-toolbar-height; - } - - md-toolbar { - height: $mobile-toolbar-height; - min-height: $mobile-toolbar-height; - - @media (min-width: $layout-breakpoint-sm) { - height: $fullscreen-toolbar-height; - min-height: $fullscreen-toolbar-height; - } - } - } - } - - .md-fab-toolbar-wrapper { - height: $mobile-toolbar-height-total; - - @media (min-width: $layout-breakpoint-sm) { - height: $toolbar-height; - } - - md-toolbar { - height: $mobile-toolbar-height; - min-height: $mobile-toolbar-height; - - @media (min-width: $layout-breakpoint-sm) { - height: $toolbar-height; - min-height: $toolbar-height; - } - - md-fab-actions { - margin-top: 0; - font-size: 16px; - - @media (max-width: $layout-breakpoint-sm) { - height: $mobile-toolbar-height; - max-height: $mobile-toolbar-height; - } - - .close-action { - margin-right: -18px; - } - - .md-fab-action-item { - width: 100%; - height: $mobile-toolbar-height; - - @media (min-width: $layout-breakpoint-sm) { - height: 46px; - } - - .tb-dashboard-action-panels { - flex-direction: column-reverse; - height: $mobile-toolbar-height; - - @media (min-width: $layout-breakpoint-sm) { - height: 46px; - } - - @media (min-width: $layout-breakpoint-sm) { - flex-direction: row-reverse; - } - - .tb-dashboard-action-panel { - flex-direction: row-reverse; - min-width: 0; - height: $half-mobile-toolbar-height; - - @media (max-width: $layout-breakpoint-sm) { - md-menu{ - margin: 0; - } - } - - @media (min-width: $layout-breakpoint-sm) { - height: 46px; - } - - div { - height: $half-mobile-toolbar-height; - - @media (min-width: $layout-breakpoint-sm) { - height: 46px; - } - } - - md-select { - margin: 0; - pointer-events: all; - } - - tb-states-component { - pointer-events: all; - } - - button.md-icon-button { - min-width: 40px; - - @media (max-width: $layout-breakpoint-sm) { - min-width: 28px; - padding: 2px; - margin: 0; - } - } - } - } - } - } - } - } - } -} diff --git a/ui/src/app/dashboard/dashboard-toolbar.tpl.html b/ui/src/app/dashboard/dashboard-toolbar.tpl.html deleted file mode 100644 index 53d1cdeef9..0000000000 --- a/ui/src/app/dashboard/dashboard-toolbar.tpl.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - {{ 'dashboard.open-toolbar' | translate }} - - more_horiz - - - - -
    -
    -
    -
    diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js deleted file mode 100644 index dc2a4a2f85..0000000000 --- a/ui/src/app/dashboard/dashboard.controller.js +++ /dev/null @@ -1,1076 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html'; -import dashboardSettingsTemplate from './dashboard-settings.tpl.html'; -import manageDashboardLayoutsTemplate from './layouts/manage-dashboard-layouts.tpl.html'; -import manageDashboardStatesTemplate from './states/manage-dashboard-states.tpl.html'; -import addWidgetTemplate from './add-widget.tpl.html'; -import selectTargetLayoutTemplate from './layouts/select-target-layout.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -import AliasController from '../api/alias-controller'; - -/*@ngInject*/ -export default function DashboardController(types, utils, dashboardUtils, widgetService, userService, - dashboardService, timeService, entityService, itembuffer, importExport, hotkeys, $window, $rootScope, - $scope, $element, $state, $stateParams, $mdDialog, $mdMedia, $timeout, $document, $q, $translate, $filter) { - - var vm = this; - - vm.user = userService.getCurrentUser(); - vm.dashboard = null; - vm.editingWidget = null; - vm.editingWidgetLayout = null; - vm.editingWidgetOriginal = null; - vm.editingWidgetLayoutOriginal = null; - vm.editingWidgetSubtitle = null; - vm.forceDashboardMobileMode = false; - vm.isAddingWidget = false; - vm.isEdit = false; - vm.isEditingWidget = false; - vm.latestWidgetTypes = []; - vm.timeseriesWidgetTypes = []; - vm.rpcWidgetTypes = []; - vm.alarmWidgetTypes = []; - vm.staticWidgetTypes = []; - vm.widgetEditMode = $state.$current.data.widgetEditMode; - vm.iframeMode = $rootScope.iframeMode; - - vm.isToolbarOpened = false; - - vm.thingsboardVersion = THINGSBOARD_VERSION; //eslint-disable-line - - vm.currentDashboardId = $stateParams.dashboardId; - if ($stateParams.customerId) { - vm.currentCustomerId = $stateParams.customerId; - vm.currentDashboardScope = 'customer'; - } else { - vm.currentDashboardScope = vm.user.authority === 'TENANT_ADMIN' ? 'tenant' : 'customer'; - vm.currentCustomerId = vm.user.customerId; - } - - Object.defineProperty(vm, 'toolbarOpened', { - get: function() { - return !vm.widgetEditMode && - (toolbarAlwaysOpen() || vm.isToolbarOpened || vm.isEdit || vm.showRightLayoutSwitch()); }, - set: function() { } - }); - - Object.defineProperty(vm, 'rightLayoutOpened', { - get: function() { - return !vm.isMobile || vm.isRightLayoutOpened; }, - set: function() { } - }); - - vm.layouts = { - main: { - show: false, - layoutCtx: { - id: 'main', - widgets: [], - widgetLayouts: {}, - gridSettings: {}, - ignoreLoading: false - } - }, - right: { - show: false, - layoutCtx: { - id: 'right', - widgets: [], - widgetLayouts: {}, - gridSettings: {}, - ignoreLoading: false - } - } - }; - - vm.openToolbar = function() { - $timeout(function() { - vm.isToolbarOpened = true; - }); - } - - vm.closeToolbar = function() { - $timeout(function() { - vm.isToolbarOpened = false; - }); - } - - vm.showCloseToolbar = function() { - return !vm.toolbarAlwaysOpen() && !vm.isEdit && !vm.showRightLayoutSwitch(); - } - - vm.toolbarAlwaysOpen = toolbarAlwaysOpen; - - vm.showRightLayoutSwitch = function() { - return vm.isMobile && vm.layouts.right.show; - } - - vm.toggleLayouts = function() { - vm.isRightLayoutOpened = !vm.isRightLayoutOpened; - } - - vm.openRightLayout = function() { - vm.isRightLayoutOpened = true; - } - - vm.isRightLayoutOpened = false; - vm.isMobile = !$mdMedia('gt-sm'); - - $scope.$watch(function() { return $mdMedia('gt-sm'); }, function(isGtSm) { - vm.isMobile = !isGtSm; - }); - - vm.mainLayoutWidth = function() { - if (vm.isEditingWidget && vm.editingLayoutCtx.id === 'main') { - return '100%'; - } else { - return vm.layouts.right.show && !vm.isMobile ? '50%' : '100%'; - } - } - - vm.mainLayoutHeight = function() { - if (!vm.isEditingWidget || vm.editingLayoutCtx.id === 'main') { - return '100%'; - } else { - return '0px'; - } - } - - vm.rightLayoutWidth = function() { - if (vm.isEditingWidget && vm.editingLayoutCtx.id === 'right') { - return '100%'; - } else { - return vm.isMobile ? '100%' : '50%'; - } - } - - vm.rightLayoutHeight = function() { - if (!vm.isEditingWidget || vm.editingLayoutCtx.id === 'right') { - return '100%'; - } else { - return '0px'; - } - } - - vm.addWidget = addWidget; - vm.addWidgetFromType = addWidgetFromType; - vm.exportDashboard = exportDashboard; - vm.importWidget = importWidget; - vm.isPublicUser = isPublicUser; - vm.isTenantAdmin = isTenantAdmin; - vm.isSystemAdmin = isSystemAdmin; - vm.dashboardConfigurationError = dashboardConfigurationError; - vm.showDashboardToolbar = showDashboardToolbar; - vm.onAddWidgetClosed = onAddWidgetClosed; - vm.onEditWidgetClosed = onEditWidgetClosed; - vm.openDashboardState = openDashboardState; - vm.openEntityAliases = openEntityAliases; - vm.openDashboardSettings = openDashboardSettings; - vm.manageDashboardLayouts = manageDashboardLayouts; - vm.manageDashboardStates = manageDashboardStates; - vm.saveDashboard = saveDashboard; - vm.saveWidget = saveWidget; - vm.toggleDashboardEditMode = toggleDashboardEditMode; - vm.onRevertWidgetEdit = onRevertWidgetEdit; - vm.helpLinkIdForWidgetType = helpLinkIdForWidgetType; - vm.displayTitle = displayTitle; - vm.displayExport = displayExport; - vm.displayDashboardTimewindow = displayDashboardTimewindow; - vm.displayDashboardsSelect = displayDashboardsSelect; - vm.displayEntitiesSelect = displayEntitiesSelect; - vm.hideFullscreenButton = hideFullscreenButton; - - vm.widgetsBundle; - - vm.dashboardCtx = { - state: null, - stateController: { - openRightLayout: function() { - vm.openRightLayout(); - } - }, - onAddWidget: function(event, layoutCtx) { - addWidget(event, layoutCtx); - }, - onEditWidget: function(event, layoutCtx, widget) { - editWidget(event, layoutCtx, widget); - }, - onExportWidget: function(event, layoutCtx, widget) { - exportWidget(event, layoutCtx, widget); - }, - onWidgetMouseDown: function(event, layoutCtx, widget) { - widgetMouseDown(event, layoutCtx, widget); - }, - onWidgetClicked: function(event, layoutCtx, widget) { - widgetClicked(event, layoutCtx, widget); - }, - prepareDashboardContextMenu: function(layoutCtx) { - return prepareDashboardContextMenu(layoutCtx); - }, - prepareWidgetContextMenu: function(layoutCtx, widget) { - return prepareWidgetContextMenu(layoutCtx, widget); - }, - onRemoveWidget: function(event, layoutCtx, widget) { - removeWidget(event, layoutCtx, widget); - }, - copyWidget: function($event, layoutCtx, widget) { - copyWidget($event, layoutCtx, widget); - }, - copyWidgetReference: function($event, layoutCtx, widget) { - copyWidgetReference($event, layoutCtx, widget); - }, - pasteWidget: function($event, layoutCtx, pos) { - pasteWidget($event, layoutCtx, pos); - }, - pasteWidgetReference: function($event, layoutCtx, pos) { - pasteWidgetReference($event, layoutCtx, pos); - } - }; - - $scope.$watch('vm.widgetsBundle', function (newVal, prevVal) { - if (newVal !== prevVal && !vm.widgetEditMode) { - loadWidgetLibrary(); - } - }); - - $scope.$watch('vm.currentDashboardId', function (newVal, prevVal) { - if (newVal !== prevVal && !vm.widgetEditMode) { - if (vm.currentDashboardScope === 'customer' && vm.user.authority === 'TENANT_ADMIN') { - $state.go('home.customers.dashboards.dashboard', { - customerId: vm.currentCustomerId, - dashboardId: vm.currentDashboardId - }); - } else { - if ($state.current.name === 'dashboard') { - $state.go('dashboard', {dashboardId: vm.currentDashboardId}); - } else { - $state.go('home.dashboards.dashboard', {dashboardId: vm.currentDashboardId}); - } - } - } - }); - - $scope.$on("$destroy", function () { - vm.dashboardCtx.stateController.cleanupPreservedStates(); - }); - - loadDashboard(); - - function loadWidgetLibrary() { - vm.latestWidgetTypes = []; - vm.timeseriesWidgetTypes = []; - vm.rpcWidgetTypes = []; - vm.alarmWidgetTypes = []; - vm.staticWidgetTypes = []; - if (vm.widgetsBundle) { - var bundleAlias = vm.widgetsBundle.alias; - var isSystem = vm.widgetsBundle.tenantId.id === types.id.nullUid; - - widgetService.getBundleWidgetTypes(bundleAlias, isSystem).then( - function (widgetTypes) { - - widgetTypes = $filter('orderBy')(widgetTypes, ['-createdTime']); - - var top = 0; - - if (widgetTypes.length > 0) { - loadNext(0); - } - - function loadNextOrComplete(i) { - i++; - if (i < widgetTypes.length) { - loadNext(i); - } - } - - function loadNext(i) { - var widgetType = widgetTypes[i]; - var widgetTypeInfo = widgetService.toWidgetInfo(widgetType); - var widget = { - isSystemType: isSystem, - bundleAlias: bundleAlias, - typeAlias: widgetTypeInfo.alias, - type: widgetTypeInfo.type, - title: widgetTypeInfo.widgetName, - sizeX: widgetTypeInfo.sizeX, - sizeY: widgetTypeInfo.sizeY, - row: top, - col: 0, - config: angular.fromJson(widgetTypeInfo.defaultConfig) - }; - widget.config.title = widgetTypeInfo.widgetName; - if (widgetTypeInfo.type === types.widgetType.timeseries.value) { - vm.timeseriesWidgetTypes.push(widget); - } else if (widgetTypeInfo.type === types.widgetType.latest.value) { - vm.latestWidgetTypes.push(widget); - } else if (widgetTypeInfo.type === types.widgetType.rpc.value) { - vm.rpcWidgetTypes.push(widget); - } else if (widgetTypeInfo.type === types.widgetType.alarm.value) { - vm.alarmWidgetTypes.push(widget); - } else if (widgetTypeInfo.type === types.widgetType.static.value) { - vm.staticWidgetTypes.push(widget); - } - top += widget.sizeY; - loadNextOrComplete(i); - - } - } - ); - } - } - - function loadDashboard() { - if (vm.widgetEditMode) { - var widget = { - isSystemType: true, - bundleAlias: 'customWidgetBundle', - typeAlias: 'customWidget', - type: $rootScope.editWidgetInfo.type, - title: 'My widget', - sizeX: $rootScope.editWidgetInfo.sizeX * 2, - sizeY: $rootScope.editWidgetInfo.sizeY * 2, - row: 2, - col: 4, - config: angular.fromJson($rootScope.editWidgetInfo.defaultConfig) - }; - widget.config.title = widget.config.title || $rootScope.editWidgetInfo.widgetName; - - vm.dashboard = dashboardUtils.createSingleWidgetDashboard(widget); - vm.dashboardConfiguration = vm.dashboard.configuration; - vm.dashboardCtx.dashboard = vm.dashboard; - vm.dashboardCtx.dashboardTimewindow = vm.dashboardConfiguration.timewindow; - vm.dashboardCtx.aliasController = new AliasController($scope, $q, $filter, utils, - types, entityService, vm.dashboardCtx.stateController, vm.dashboardConfiguration.entityAliases); - var parentScope = $window.parent.angular.element($window.frameElement).scope(); - parentScope.$root.$broadcast('widgetEditModeInited'); - parentScope.$root.$apply(); - } else { - dashboardService.getDashboard($stateParams.dashboardId) - .then(function success(dashboard) { - vm.dashboard = dashboardUtils.validateAndUpdateDashboard(dashboard); - vm.dashboardConfiguration = vm.dashboard.configuration; - vm.dashboardCtx.dashboard = vm.dashboard; - vm.dashboardCtx.dashboardTimewindow = vm.dashboardConfiguration.timewindow; - vm.dashboardCtx.aliasController = new AliasController($scope, $q, $filter, utils, - types, entityService, vm.dashboardCtx.stateController, vm.dashboardConfiguration.entityAliases); - }, function fail() { - vm.configurationError = true; - }); - } - } - - function openDashboardState(state, openRightLayout) { - var layoutsData = dashboardUtils.getStateLayoutsData(vm.dashboard, state); - if (layoutsData) { - vm.dashboardCtx.state = state; - vm.dashboardCtx.aliasController.dashboardStateChanged(); - var layoutVisibilityChanged = false; - for (var l in vm.layouts) { - var layout = vm.layouts[l]; - var showLayout; - if (layoutsData[l]) { - showLayout = true; - } else { - showLayout = false; - } - if (layout.show != showLayout) { - layout.show = showLayout; - layoutVisibilityChanged = !vm.isMobile; - } - } - vm.isRightLayoutOpened = openRightLayout ? true : false; - updateLayouts(layoutVisibilityChanged); - } - - function updateLayouts(layoutVisibilityChanged) { - for (l in vm.layouts) { - layout = vm.layouts[l]; - if (layoutsData[l]) { - var layoutInfo = layoutsData[l]; - if (layout.layoutCtx.id === 'main') { - layout.layoutCtx.ctrl.setResizing(layoutVisibilityChanged); - } - updateLayout(layout, layoutInfo.widgets, layoutInfo.widgetLayouts, layoutInfo.gridSettings); - } else { - updateLayout(layout, [], {}, null); - } - } - } - } - - function updateLayout(layout, widgets, widgetLayouts, gridSettings) { - if (gridSettings) { - layout.layoutCtx.gridSettings = gridSettings; - } - layout.layoutCtx.widgets = widgets; - layout.layoutCtx.widgetLayouts = widgetLayouts; - if (layout.show && layout.layoutCtx.ctrl) { - layout.layoutCtx.ctrl.reload(); - } - layout.layoutCtx.ignoreLoading = true; - } - - function isPublicUser() { - return vm.user.isPublic === true; - } - - function isTenantAdmin() { - return vm.user.authority === 'TENANT_ADMIN'; - } - - function isSystemAdmin() { - return vm.user.authority === 'SYS_ADMIN'; - } - - function dashboardConfigurationError() { - return vm.configurationError; - } - - function showDashboardToolbar() { - return true; - } - - function openEntityAliases($event) { - $mdDialog.show({ - controller: 'EntityAliasesController', - controllerAs: 'vm', - templateUrl: entityAliasesTemplate, - locals: { - config: { - entityAliases: angular.copy(vm.dashboard.configuration.entityAliases), - widgets: dashboardUtils.getWidgetsArray(vm.dashboard), - isSingleEntityAlias: false, - singleEntityAlias: null - } - }, - parent: angular.element($document[0].body), - multiple: true, - fullscreen: true, - targetEvent: $event - }).then(function (entityAliases) { - vm.dashboard.configuration.entityAliases = entityAliases; - entityAliasesUpdated(); - }, function () { - }); - } - - function openDashboardSettings($event) { - var gridSettings = null; - var layoutKeys = dashboardUtils.isSingleLayoutDashboard(vm.dashboard); - if (layoutKeys) { - gridSettings = angular.copy(vm.dashboard.configuration.states[layoutKeys.state].layouts[layoutKeys.layout].gridSettings) - } - $mdDialog.show({ - controller: 'DashboardSettingsController', - controllerAs: 'vm', - templateUrl: dashboardSettingsTemplate, - locals: { - settings: angular.copy(vm.dashboard.configuration.settings), - gridSettings: gridSettings - }, - parent: angular.element($document[0].body), - multiple: true, - fullscreen: true, - targetEvent: $event - }).then(function (data) { - vm.dashboard.configuration.settings = data.settings; - var gridSettings = data.gridSettings; - if (gridSettings) { - updateLayoutGrid(layoutKeys, gridSettings); - } - }, function () { - }); - } - - function manageDashboardLayouts($event) { - $mdDialog.show({ - controller: 'ManageDashboardLayoutsController', - controllerAs: 'vm', - templateUrl: manageDashboardLayoutsTemplate, - locals: { - layouts: angular.copy(vm.dashboard.configuration.states[vm.dashboardCtx.state].layouts) - }, - parent: angular.element($document[0].body), - multiple: true, - fullscreen: true, - targetEvent: $event - }).then(function (layouts) { - updateLayouts(layouts); - }, function () { - }); - } - - function manageDashboardStates($event) { - var dashboardConfiguration = vm.dashboard.configuration; - var states = angular.copy(dashboardConfiguration.states); - - $mdDialog.show({ - controller: 'ManageDashboardStatesController', - controllerAs: 'vm', - templateUrl: manageDashboardStatesTemplate, - locals: { - states: states - }, - parent: angular.element($document[0].body), - multiple: true, - fullscreen: true, - targetEvent: $event - }).then(function (states) { - updateStates(states); - }, function () { - }); - } - - function updateLayoutGrid(layoutKeys, gridSettings) { - var layout = vm.dashboard.configuration.states[layoutKeys.state].layouts[layoutKeys.layout]; - var layoutCtx = vm.layouts[layoutKeys.layout]; - layoutCtx.widgets = []; - dashboardUtils.updateLayoutSettings(layout, gridSettings); - var layoutsData = dashboardUtils.getStateLayoutsData(vm.dashboard, layoutKeys.state); - layoutCtx.widgets = layoutsData[layoutKeys.layout].widgets; - } - - function updateLayouts(layouts) { - dashboardUtils.setLayouts(vm.dashboard, vm.dashboardCtx.state, layouts); - openDashboardState(vm.dashboardCtx.state); - } - - function updateStates(states) { - vm.dashboard.configuration.states = states; - dashboardUtils.removeUnusedWidgets(vm.dashboard); - var targetState = vm.dashboardCtx.state; - if (!vm.dashboard.configuration.states[targetState]) { - targetState = dashboardUtils.getRootStateId(vm.dashboardConfiguration.states); - } - openDashboardState(targetState); - } - - function editWidget($event, layoutCtx, widget) { - $event.stopPropagation(); - if (vm.editingWidgetOriginal === widget) { - $timeout(onEditWidgetClosed()); - } else { - var transition = !vm.forceDashboardMobileMode; - vm.editingWidgetOriginal = widget; - vm.editingWidgetLayoutOriginal = layoutCtx.widgetLayouts[widget.id]; - vm.editingWidget = angular.copy(vm.editingWidgetOriginal); - vm.editingWidgetLayout = angular.copy(vm.editingWidgetLayoutOriginal); - vm.editingLayoutCtx = layoutCtx; - vm.editingWidgetSubtitle = widgetService.getInstantWidgetInfo(vm.editingWidget).widgetName; - vm.forceDashboardMobileMode = true; - vm.isEditingWidget = true; - if (layoutCtx) { - var delayOffset = transition ? 350 : 0; - var delay = transition ? 400 : 300; - $timeout(function () { - layoutCtx.ctrl.highlightWidget(vm.editingWidgetOriginal, delay); - }, delayOffset, false); - } - } - } - function exportDashboard($event) { - $event.stopPropagation(); - importExport.exportDashboard(vm.currentDashboardId); - } - - function exportWidget($event, layoutCtx, widget) { - $event.stopPropagation(); - importExport.exportWidget(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id, widget); - } - - function importWidget($event) { - $event.stopPropagation(); - importExport.importWidget($event, vm.dashboard, vm.dashboardCtx.state, - selectTargetLayout, entityAliasesUpdated).then( - function success(importData) { - var widget = importData.widget; - var layoutId = importData.layoutId; - vm.layouts[layoutId].layoutCtx.widgets.push(widget); - } - ); - } - - function widgetMouseDown($event, layoutCtx, widget) { - if (vm.isEdit && !vm.isEditingWidget) { - layoutCtx.ctrl.selectWidget(widget, 0); - } - } - - function widgetClicked($event, layoutCtx, widget) { - if (vm.isEditingWidget) { - editWidget($event, layoutCtx, widget); - } - } - - function prepareDashboardContextMenu(layoutCtx) { - var dashboardContextActions = []; - if (vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) { - dashboardContextActions.push( - { - action: openDashboardSettings, - enabled: true, - value: "dashboard.settings", - icon: "settings" - } - ); - dashboardContextActions.push( - { - action: openEntityAliases, - enabled: true, - value: "entity.aliases", - icon: "devices_other" - } - ); - dashboardContextActions.push( - { - action: function ($event) { - layoutCtx.ctrl.pasteWidget($event); - }, - enabled: itembuffer.hasWidget(), - value: "action.paste", - icon: "content_paste", - shortcut: "M-V" - } - ); - dashboardContextActions.push( - { - action: function ($event) { - layoutCtx.ctrl.pasteWidgetReference($event); - }, - enabled: itembuffer.canPasteWidgetReference(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id), - value: "action.paste-reference", - icon: "content_paste", - shortcut: "M-I" - } - ); - - } - return dashboardContextActions; - } - - function prepareWidgetContextMenu(layoutCtx) { - var widgetContextActions = []; - if (vm.isEdit && !vm.isEditingWidget) { - widgetContextActions.push( - { - action: function (event, widget) { - editWidget(event, layoutCtx, widget); - }, - enabled: true, - value: "action.edit", - icon: "edit" - } - ); - if (!vm.widgetEditMode) { - widgetContextActions.push( - { - action: function (event, widget) { - copyWidget(event, layoutCtx, widget); - }, - enabled: true, - value: "action.copy", - icon: "content_copy", - shortcut: "M-C" - } - ); - widgetContextActions.push( - { - action: function (event, widget) { - copyWidgetReference(event, layoutCtx, widget); - }, - enabled: true, - value: "action.copy-reference", - icon: "content_copy", - shortcut: "M-R" - } - ); - widgetContextActions.push( - { - action: function (event, widget) { - removeWidget(event, layoutCtx, widget); - }, - enabled: true, - value: "action.delete", - icon: "clear", - shortcut: "M-X" - } - ); - } - } - return widgetContextActions; - } - - function copyWidget($event, layoutCtx, widget) { - itembuffer.copyWidget(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id, widget); - } - - function copyWidgetReference($event, layoutCtx, widget) { - itembuffer.copyWidgetReference(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id, widget); - } - - function helpLinkIdForWidgetType() { - var link = 'widgetsConfig'; - if (vm.editingWidget && vm.editingWidget.type) { - switch (vm.editingWidget.type) { - case types.widgetType.timeseries.value: { - link = 'widgetsConfigTimeseries'; - break; - } - case types.widgetType.latest.value: { - link = 'widgetsConfigLatest'; - break; - } - case types.widgetType.rpc.value: { - link = 'widgetsConfigRpc'; - break; - } - case types.widgetType.alarm.value: { - link = 'widgetsConfigAlarm'; - break; - } - case types.widgetType.static.value: { - link = 'widgetsConfigStatic'; - break; - } - } - } - return link; - } - - function toolbarAlwaysOpen() { - if (vm.dashboard && vm.dashboard.configuration.settings && - angular.isDefined(vm.dashboard.configuration.settings.toolbarAlwaysOpen)) { - return vm.dashboard.configuration.settings.toolbarAlwaysOpen; - } else { - return true; - } - } - - function displayTitle() { - if (vm.dashboard && vm.dashboard.configuration.settings && - angular.isDefined(vm.dashboard.configuration.settings.showTitle)) { - return vm.dashboard.configuration.settings.showTitle; - } else { - return false; - } - } - - function displayExport() { - if (vm.dashboard && vm.dashboard.configuration.settings && - angular.isDefined(vm.dashboard.configuration.settings.showDashboardExport)) { - return vm.dashboard.configuration.settings.showDashboardExport; - } else { - return true; - } - } - - function displayDashboardTimewindow() { - if (vm.dashboard && vm.dashboard.configuration.settings && - angular.isDefined(vm.dashboard.configuration.settings.showDashboardTimewindow)) { - return vm.dashboard.configuration.settings.showDashboardTimewindow; - } else { - return true; - } - } - - function displayDashboardsSelect() { - if (vm.dashboard && vm.dashboard.configuration.settings && - angular.isDefined(vm.dashboard.configuration.settings.showDashboardsSelect)) { - return vm.dashboard.configuration.settings.showDashboardsSelect; - } else { - return true; - } - } - - function displayEntitiesSelect() { - if (vm.dashboard && vm.dashboard.configuration.settings && - angular.isDefined(vm.dashboard.configuration.settings.showEntitiesSelect)) { - return vm.dashboard.configuration.settings.showEntitiesSelect; - } else { - return true; - } - } - - function hideFullscreenButton() { - return vm.widgetEditMode || vm.iframeMode || $rootScope.forceFullscreen || $state.current.name === 'dashboard'; - } - - function onRevertWidgetEdit(widgetForm) { - if (widgetForm.$dirty) { - widgetForm.$setPristine(); - vm.editingWidget = angular.copy(vm.editingWidgetOriginal); - vm.editingWidgetLayout = angular.copy(vm.editingWidgetLayoutOriginal); - } - } - - function saveWidget(widgetForm) { - widgetForm.$setPristine(); - var widget = angular.copy(vm.editingWidget); - var widgetLayout = angular.copy(vm.editingWidgetLayout); - var id = vm.editingWidgetOriginal.id; - var index = vm.editingLayoutCtx.widgets.indexOf(vm.editingWidgetOriginal); - vm.dashboardConfiguration.widgets[id] = widget; - vm.editingWidgetOriginal = widget; - vm.editingWidgetLayoutOriginal = widgetLayout; - vm.editingLayoutCtx.widgets[index] = widget; - vm.editingLayoutCtx.widgetLayouts[widget.id] = widgetLayout; - vm.editingLayoutCtx.ctrl.highlightWidget(vm.editingWidgetOriginal, 0); - } - - function onEditWidgetClosed() { - vm.editingWidgetOriginal = null; - vm.editingWidget = null; - vm.editingWidgetLayoutOriginal = null; - vm.editingWidgetLayout = null; - vm.editingLayoutCtx = null; - vm.editingWidgetSubtitle = null; - vm.isEditingWidget = false; - resetHighlight(); - vm.forceDashboardMobileMode = false; - } - - function addWidget(event, layoutCtx) { - loadWidgetLibrary(); - vm.isAddingWidget = true; - vm.addingLayoutCtx = layoutCtx; - } - - function onAddWidgetClosed() { - vm.timeseriesWidgetTypes = []; - vm.latestWidgetTypes = []; - vm.rpcWidgetTypes = []; - vm.alarmWidgetTypes = []; - vm.staticWidgetTypes = []; - } - - function selectTargetLayout($event) { - var deferred = $q.defer(); - var layouts = vm.dashboardConfiguration.states[vm.dashboardCtx.state].layouts; - var layoutIds = Object.keys(layouts); - if (layoutIds.length > 1) { - $mdDialog.show({ - controller: 'SelectTargetLayoutController', - controllerAs: 'vm', - templateUrl: selectTargetLayoutTemplate, - parent: angular.element($document[0].body), - fullscreen: true, - multiple: true, - targetEvent: $event - }).then( - function success(layoutId) { - deferred.resolve(layoutId); - }, - function fail() { - deferred.reject(); - } - ); - } else { - deferred.resolve(layoutIds[0]); - } - return deferred.promise; - } - - function addWidgetFromType(event, widget) { - vm.onAddWidgetClosed(); - vm.isAddingWidget = false; - widgetService.getWidgetInfo(widget.bundleAlias, widget.typeAlias, widget.isSystemType).then( - function (widgetTypeInfo) { - var config = angular.fromJson(widgetTypeInfo.defaultConfig); - config.title = 'New ' + widgetTypeInfo.widgetName; - config.datasources = []; - var newWidget = { - isSystemType: widget.isSystemType, - bundleAlias: widget.bundleAlias, - typeAlias: widgetTypeInfo.alias, - type: widgetTypeInfo.type, - title: 'New widget', - sizeX: widgetTypeInfo.sizeX, - sizeY: widgetTypeInfo.sizeY, - config: config - }; - - function addWidgetToLayout(widget, layoutId) { - dashboardUtils.addWidgetToLayout(vm.dashboard, vm.dashboardCtx.state, layoutId, widget); - vm.layouts[layoutId].layoutCtx.widgets.push(widget); - } - - function addWidget(widget) { - if (vm.addingLayoutCtx) { - addWidgetToLayout(widget, vm.addingLayoutCtx.id); - vm.addingLayoutCtx = null; - } else { - selectTargetLayout(event).then( - function success(layoutId) { - addWidgetToLayout(widget, layoutId); - } - ); - } - } - - if (widgetTypeInfo.typeParameters.useCustomDatasources) { - addWidget(newWidget); - } else { - $mdDialog.show({ - controller: 'AddWidgetController', - controllerAs: 'vm', - templateUrl: addWidgetTemplate, - locals: { - dashboard: vm.dashboard, - aliasController: vm.dashboardCtx.aliasController, - widget: newWidget, - widgetInfo: widgetTypeInfo - }, - parent: angular.element($document[0].body), - fullscreen: true, - multiple: true, - targetEvent: event, - onComplete: function () { - var w = angular.element($window); - w.triggerHandler('resize'); - } - }).then(function (result) { - var widget = result.widget; - addWidget(widget); - }, function () { - }); - } - } - ); - } - - function removeWidget(event, layoutCtx, widget) { - var title = widget.config.title; - if (!title || title.length === 0) { - title = widgetService.getInstantWidgetInfo(widget).widgetName; - } - var confirm = $mdDialog.confirm() - .targetEvent(event) - .title($translate.instant('widget.remove-widget-title', {widgetTitle: title})) - .htmlContent($translate.instant('widget.remove-widget-text')) - .ariaLabel($translate.instant('widget.remove')) - .cancel($translate.instant('action.no')) - .ok($translate.instant('action.yes')); - $mdDialog.show(confirm).then(function () { - var index = layoutCtx.widgets.indexOf(widget); - if (index > -1) { - layoutCtx.widgets.splice(index, 1); - dashboardUtils.removeWidgetFromLayout(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id, widget.id); - } - }); - } - - function pasteWidget(event, layoutCtx, pos) { - itembuffer.pasteWidget(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id, pos, entityAliasesUpdated).then( - function (widget) { - if (widget) { - layoutCtx.widgets.push(widget); - } - } - ); - } - - function pasteWidgetReference(event, layoutCtx, pos) { - itembuffer.pasteWidgetReference(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id, pos).then( - function (widget) { - if (widget) { - layoutCtx.widgets.push(widget); - } - } - ); - } - - function setEditMode(isEdit, revert) { - vm.isEdit = isEdit; - if (vm.isEdit) { - vm.dashboardCtx.stateController.preserveState(); - vm.prevDashboard = angular.copy(vm.dashboard); - } else { - if (vm.widgetEditMode) { - if (revert) { - vm.dashboard = vm.prevDashboard; - } - } else { - resetHighlight(); - if (revert) { - vm.dashboard = vm.prevDashboard; - vm.dashboardConfiguration = vm.dashboard.configuration; - vm.dashboardCtx.dashboardTimewindow = vm.dashboardConfiguration.timewindow; - entityAliasesUpdated(); - } else { - vm.dashboard.configuration.timewindow = vm.dashboardCtx.dashboardTimewindow; - } - } - } - } - - function resetHighlight() { - for (var l in vm.layouts) { - if (vm.layouts[l].layoutCtx) { - if (vm.layouts[l].layoutCtx.ctrl) { - vm.layouts[l].layoutCtx.ctrl.resetHighlight(); - } - } - } - } - - function toggleDashboardEditMode() { - setEditMode(!vm.isEdit, true); - } - - function saveDashboard() { - setEditMode(false, false); - notifyDashboardUpdated(); - } - -/* function showAliasesResolutionError(error) { - var alert = $mdDialog.alert() - .parent(angular.element($document[0].body)) - .clickOutsideToClose(true) - .title($translate.instant('dashboard.alias-resolution-error-title')) - .htmlContent($translate.instant(error)) - .ariaLabel($translate.instant('dashboard.alias-resolution-error-title')) - .ok($translate.instant('action.close')) - alert._options.skipHide = true; - alert._options.fullscreen = true; - - $mdDialog.show(alert); - }*/ - - function entityAliasesUpdated() { - vm.dashboardCtx.aliasController.updateEntityAliases(vm.dashboard.configuration.entityAliases); - } - - function notifyDashboardUpdated() { - if (vm.widgetEditMode) { - var parentScope = $window.parent.angular.element($window.frameElement).scope(); - var widget = vm.layouts.main.layoutCtx.widgets[0]; - var layout = vm.layouts.main.layoutCtx.widgetLayouts[widget.id]; - widget.sizeX = layout.sizeX; - widget.sizeY = layout.sizeY; - parentScope.$root.$broadcast('widgetEditUpdated', widget); - parentScope.$root.$apply(); - } else { - dashboardService.saveDashboard(vm.dashboard); - } - } - -} diff --git a/ui/src/app/dashboard/dashboard.directive.js b/ui/src/app/dashboard/dashboard.directive.js deleted file mode 100644 index 37e3485d31..0000000000 --- a/ui/src/app/dashboard/dashboard.directive.js +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import dashboardFieldsetTemplate from './dashboard-fieldset.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function DashboardDirective($compile, $templateCache, $translate, types, toast, dashboardService) { - var linker = function (scope, element) { - var template = $templateCache.get(dashboardFieldsetTemplate); - element.html(template); - scope.publicLink = null; - scope.$watch('dashboard', function(newVal) { - if (newVal) { - if (scope.dashboard.publicCustomerId) { - scope.publicLink = dashboardService.getPublicDashboardLink(scope.dashboard); - } else { - scope.publicLink = null; - } - } - }); - - scope.onPublicLinkCopied = function() { - toast.showSuccess($translate.instant('dashboard.public-link-copied-message'), 750, angular.element(element).parent().parent(), 'bottom left'); - }; - - $compile(element.contents())(scope); - } - return { - restrict: "E", - link: linker, - scope: { - dashboard: '=', - isEdit: '=', - customerId: '=', - dashboardScope: '=', - theForm: '=', - onMakePublic: '&', - onMakePrivate: '&', - onManageAssignedCustomers: '&', - onUnassignFromCustomer: '&', - onExportDashboard: '&', - onDeleteDashboard: '&' - } - }; -} diff --git a/ui/src/app/dashboard/dashboard.routes.js b/ui/src/app/dashboard/dashboard.routes.js deleted file mode 100644 index 43ec230c6d..0000000000 --- a/ui/src/app/dashboard/dashboard.routes.js +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import dashboardsTemplate from './dashboards.tpl.html'; -import dashboardTemplate from './dashboard.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function DashboardRoutes($stateProvider) { - $stateProvider - .state('home.dashboards', { - url: '/dashboards', - params: {'topIndex': 0}, - module: 'private', - auth: ['TENANT_ADMIN', 'CUSTOMER_USER'], - views: { - "content@home": { - templateUrl: dashboardsTemplate, - controller: 'DashboardsController', - controllerAs: 'vm' - } - }, - data: { - dashboardsType: 'tenant', - searchEnabled: true, - pageTitle: 'dashboard.dashboards' - }, - ncyBreadcrumb: { - label: '{"icon": "dashboard", "label": "dashboard.dashboards"}' - } - }) - .state('home.customers.dashboards', { - url: '/:customerId/dashboards', - params: {'topIndex': 0}, - module: 'private', - auth: ['TENANT_ADMIN'], - views: { - "content@home": { - templateUrl: dashboardsTemplate, - controllerAs: 'vm', - controller: 'DashboardsController' - } - }, - data: { - dashboardsType: 'customer', - searchEnabled: true, - pageTitle: 'customer.dashboards' - }, - ncyBreadcrumb: { - label: '{"icon": "dashboard", "label": "{{ vm.customerDashboardsTitle }}", "translate": "false"}' - } - }) - .state('home.dashboards.dashboard', { - url: '/:dashboardId?state', - reloadOnSearch: false, - module: 'private', - auth: ['TENANT_ADMIN', 'CUSTOMER_USER'], - views: { - "content@home": { - templateUrl: dashboardTemplate, - controller: 'DashboardController', - controllerAs: 'vm' - } - }, - data: { - widgetEditMode: false, - searchEnabled: false, - pageTitle: 'dashboard.dashboard' - }, - ncyBreadcrumb: { - label: '{"icon": "dashboard", "label": "{{ vm.dashboard.title }}", "translate": "false"}' - } - }) - .state('dashboard', { - url: '/dashboard/:dashboardId?state', - reloadOnSearch: false, - module: 'private', - auth: ['TENANT_ADMIN', 'CUSTOMER_USER'], - views: { - "@": { - templateUrl: dashboardTemplate, - controller: 'DashboardController', - controllerAs: 'vm' - } - }, - data: { - widgetEditMode: false, - searchEnabled: false, - pageTitle: 'dashboard.dashboard' - } - }) - .state('home.customers.dashboards.dashboard', { - url: '/:dashboardId?state', - reloadOnSearch: false, - module: 'private', - auth: ['TENANT_ADMIN', 'CUSTOMER_USER'], - views: { - "content@home": { - templateUrl: dashboardTemplate, - controller: 'DashboardController', - controllerAs: 'vm' - } - }, - data: { - searchEnabled: false, - pageTitle: 'customer.dashboard' - }, - ncyBreadcrumb: { - label: '{"icon": "dashboard", "label": "customer.dashboard"}' - } - }) -} diff --git a/ui/src/app/dashboard/dashboard.scss b/ui/src/app/dashboard/dashboard.scss deleted file mode 100644 index a93aca40f3..0000000000 --- a/ui/src/app/dashboard/dashboard.scss +++ /dev/null @@ -1,149 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "~compass-sass-mixins/lib/compass"; - -@import "../../scss/constants"; - -$toolbar-height: 50px !default; -$fullscreen-toolbar-height: 64px !default; -$mobile-toolbar-height: 84px !default; - -section.tb-dashboard-title { - position: absolute; - top: 0; - left: 20px; -} - -input.tb-dashboard-title { - height: 38px; - font-size: 2rem; - font-weight: 500; - letter-spacing: .005em; -} - -div.tb-padded { - top: 60px; -} - -section.tb-padded { - top: 60px; -} - -div.tb-shrinked { - width: 40%; -} - -tb-details-sidenav.tb-widget-details-sidenav { - md-sidenav.tb-sidenav-details { - @media (min-width: $layout-breakpoint-sm) { - width: 85% !important; - } - - @media (min-width: $layout-breakpoint-md) { - width: 75% !important; - } - - @media (min-width: $layout-breakpoint-lg) { - width: 60% !important; - } - } -} - -/*********************** - * dashboard toolbar - ***********************/ - -section.tb-dashboard-toolbar { - position: absolute; - top: 0; - left: 0; - z-index: 13; - pointer-events: none; - - &.tb-dashboard-toolbar-opened { - right: 0; - // transition: right .3s cubic-bezier(.55, 0, .55, .2); - } - - &.tb-dashboard-toolbar-closed { - right: 18px; - - transition: right .3s cubic-bezier(.55, 0, .55, .2) .2s; - } -} - -.tb-dashboard-container { - &.tb-dashboard-toolbar-opened { - &.is-fullscreen { - margin-top: $mobile-toolbar-height; - - @media (min-width: $layout-breakpoint-sm) { - margin-top: $fullscreen-toolbar-height; - } - } - - &:not(.is-fullscreen) { - margin-top: $mobile-toolbar-height; - - @media (min-width: $layout-breakpoint-sm) { - margin-top: $toolbar-height; - } - - transition: margin-top .3s cubic-bezier(.55, 0, .55, .2); - } - } - - &.tb-dashboard-toolbar-closed { - margin-top: 0; - - transition: margin-top .3s cubic-bezier(.55, 0, .55, .2) .2s; - } - - .tb-dashboard-layouts { - md-backdrop { - z-index: 1; - } - - #tb-right-layout { - md-sidenav { - z-index: 1; - } - } - } -} - -/***************************** - * dashboard powered by footer - ****************************/ - -section.tb-powered-by-footer { - position: absolute; - right: 25px; - bottom: 5px; - z-index: 30; - pointer-events: none; - - span { - font-size: 12px; - - a { - font-weight: 700; - text-decoration: none; - pointer-events: all; - border: none; - } - } -} diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html deleted file mode 100644 index 1ce68abbd2..0000000000 --- a/ui/src/app/dashboard/dashboard.tpl.html +++ /dev/null @@ -1,336 +0,0 @@ - - -
    - -
    -
    - - - {{ 'dashboard.close-toolbar' | translate }} - - arrow_forward - - - - - - - {{ (vm.isRightLayoutOpened ? 'dashboard.hide-details' : 'dashboard.show-details') | translate }} - - - - - - - {{ 'dashboard.export' | translate }} - - file_download - - - - - - - - {{ 'entity.aliases' | translate }} - - devices_other - - - - {{ 'dashboard.settings' | translate }} - - settings - - - -
    -
    - - -
    - - - {{ 'dashboard.manage-states' | translate }} - - layers - - - - {{ 'layout.manage' | translate }} - - view_compact - -
    - - - - -
    -
    -
    -
    -
    -
    - - dashboard.configuration-error - -
    -
    -

    {{ vm.dashboard.title }}

    - - - - -
    -
    -
    - - -
    - - - - -
    - - -
    -
    -
    - - -
    -
    - - -
    - {{ 'widgets-bundle.current' | translate }} - - -
    -
    -
    - - - - - - - - - - - - - - - - - - - - - - - widgets-bundle.empty - widget.select-widgets-bundle -
    -
    - -
    - -
    diff --git a/ui/src/app/dashboard/dashboards.controller.js b/ui/src/app/dashboard/dashboards.controller.js deleted file mode 100644 index 3136e5afe4..0000000000 --- a/ui/src/app/dashboard/dashboards.controller.js +++ /dev/null @@ -1,614 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import addDashboardTemplate from './add-dashboard.tpl.html'; -import dashboardCard from './dashboard-card.tpl.html'; -import addDashboardsToCustomerTemplate from './add-dashboards-to-customer.tpl.html'; -import makeDashboardPublicDialogTemplate from './make-dashboard-public-dialog.tpl.html'; -import manageAssignedCustomersTemplate from './manage-assigned-customers.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -import './dashboard-card.scss'; - -/*@ngInject*/ -export function MakeDashboardPublicDialogController($mdDialog, $translate, toast, dashboardService, dashboard) { - - var vm = this; - - vm.dashboard = dashboard; - vm.publicLink = dashboardService.getPublicDashboardLink(dashboard); - - vm.onPublicLinkCopied = onPublicLinkCopied; - vm.close = close; - - function onPublicLinkCopied(){ - toast.showSuccess($translate.instant('dashboard.public-link-copied-message'), 750, angular.element('#make-dialog-public-content'), 'bottom left'); - } - - function close() { - $mdDialog.hide(); - } - -} - -/*@ngInject*/ -export function DashboardCardController(types) { - - var vm = this; - vm.types = types; - -} - -/*@ngInject*/ -export function DashboardsController(userService, dashboardService, customerService, importExport, types, - $state, $stateParams, $mdDialog, $document, $q, $translate) { - - var customerId = $stateParams.customerId; - - var dashboardActionsList = [ - { - onAction: function ($event, item) { - vm.grid.openItem($event, item); - }, - name: function() { return $translate.instant('dashboard.details') }, - details: function() { return $translate.instant('dashboard.dashboard-details') }, - icon: "edit" - } - ]; - - var dashboardGroupActionsList = []; - - var vm = this; - vm.types = types; - - vm.dashboardGridConfig = { - deleteItemTitleFunc: deleteDashboardTitle, - deleteItemContentFunc: deleteDashboardText, - deleteItemsTitleFunc: deleteDashboardsTitle, - deleteItemsActionTitleFunc: deleteDashboardsActionTitle, - deleteItemsContentFunc: deleteDashboardsText, - - loadItemDetailsFunc: loadDashboard, - - saveItemFunc: saveDashboard, - - clickItemFunc: openDashboard, - - getItemTitleFunc: getDashboardTitle, - itemCardController: 'DashboardCardController', - itemCardTemplateUrl: dashboardCard, - parentCtl: vm, - - actionsList: dashboardActionsList, - groupActionsList: dashboardGroupActionsList, - - onGridInited: gridInited, - - addItemTemplateUrl: addDashboardTemplate, - - addItemText: function() { return $translate.instant('dashboard.add-dashboard-text') }, - noItemsText: function() { return $translate.instant('dashboard.no-dashboards-text') }, - itemDetailsText: function() { return $translate.instant('dashboard.dashboard-details') }, - isDetailsReadOnly: function () { - return vm.dashboardsScope === 'customer_user'; - }, - isSelectionEnabled: function () { - return !(vm.dashboardsScope === 'customer_user'); - } - }; - - if (angular.isDefined($stateParams.items) && $stateParams.items !== null) { - vm.dashboardGridConfig.items = $stateParams.items; - } - - if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) { - vm.dashboardGridConfig.topIndex = $stateParams.topIndex; - } - - vm.dashboardsScope = $state.$current.data.dashboardsType; - - vm.makePublic = makePublic; - vm.makePrivate = makePrivate; - vm.manageAssignedCustomers = manageAssignedCustomers; - vm.unassignFromCustomer = unassignFromCustomer; - vm.exportDashboard = exportDashboard; - - initController(); - - function initController() { - var fetchDashboardsFunction = null; - var deleteDashboardFunction = null; - var refreshDashboardsParamsFunction = null; - - var user = userService.getCurrentUser(); - - if (user.authority === 'CUSTOMER_USER') { - vm.dashboardsScope = 'customer_user'; - customerId = user.customerId; - } - - if (customerId) { - vm.customerId = customerId; - vm.customerDashboardsTitle = $translate.instant('customer.dashboards'); - customerService.getShortCustomerInfo(customerId).then( - function success(info) { - if (info.isPublic) { - vm.customerDashboardsTitle = $translate.instant('customer.public-dashboards'); - } - } - ); - } - - if (vm.dashboardsScope === 'tenant') { - fetchDashboardsFunction = function (pageLink) { - return dashboardService.getTenantDashboards(pageLink); - }; - deleteDashboardFunction = function (dashboardId) { - return dashboardService.deleteDashboard(dashboardId); - }; - refreshDashboardsParamsFunction = function () { - return {"topIndex": vm.topIndex}; - }; - - dashboardActionsList.push( - { - onAction: function ($event, item) { - exportDashboard($event, item); - }, - name: function() { $translate.instant('action.export') }, - details: function() { return $translate.instant('dashboard.export') }, - icon: "file_download" - }); - - dashboardActionsList.push({ - onAction: function ($event, item) { - makePublic($event, item); - }, - name: function() { return $translate.instant('action.share') }, - details: function() { return $translate.instant('dashboard.make-public') }, - icon: "share", - isEnabled: function(dashboard) { - return dashboard && !dashboard.publicCustomerId; - } - }); - dashboardActionsList.push({ - onAction: function ($event, item) { - makePrivate($event, item); - }, - name: function() { return $translate.instant('action.make-private') }, - details: function() { return $translate.instant('dashboard.make-private') }, - icon: "reply", - isEnabled: function(dashboard) { - return dashboard && dashboard.publicCustomerId; - } - }); - dashboardActionsList.push({ - onAction: function ($event, item) { - manageAssignedCustomers($event, item); - }, - name: function() { return $translate.instant('action.assign') }, - details: function() { return $translate.instant('dashboard.manage-assigned-customers') }, - icon: "assignment_ind", - isEnabled: function(dashboard) { - return dashboard; - } - }); - - /*dashboardActionsList.push({ - onAction: function ($event, item) { - assignToCustomer($event, [ item.id.id ]); - }, - name: function() { return $translate.instant('action.assign') }, - details: function() { return $translate.instant('dashboard.assign-to-customer') }, - icon: "assignment_ind", - isEnabled: function(dashboard) { - return dashboard && (!dashboard.customerId || dashboard.customerId.id === types.id.nullUid); - } - });*/ - /*dashboardActionsList.push({ - onAction: function ($event, item) { - unassignFromCustomer($event, item, false); - }, - name: function() { return $translate.instant('action.unassign') }, - details: function() { return $translate.instant('dashboard.unassign-from-customer') }, - icon: "assignment_return", - isEnabled: function(dashboard) { - return dashboard && dashboard.customerId && dashboard.customerId.id !== types.id.nullUid && !dashboard.assignedCustomer.isPublic; - } - });*/ - - dashboardActionsList.push( - { - onAction: function ($event, item) { - vm.grid.deleteItem($event, item); - }, - name: function() { return $translate.instant('action.delete') }, - details: function() { return $translate.instant('dashboard.delete') }, - icon: "delete" - } - ); - - dashboardGroupActionsList.push( - { - onAction: function ($event, items) { - assignDashboardsToCustomers($event, items); - }, - name: function() { return $translate.instant('dashboard.assign-dashboards') }, - details: function(selectedCount) { - return $translate.instant('dashboard.assign-dashboards-text', {count: selectedCount}, "messageformat"); - }, - icon: "assignment_ind" - } - ); - dashboardGroupActionsList.push( - { - onAction: function ($event, items) { - unassignDashboardsFromCustomers($event, items); - }, - name: function() { return $translate.instant('dashboard.unassign-dashboards') }, - details: function(selectedCount) { - return $translate.instant('dashboard.unassign-dashboards-action-text', {count: selectedCount}, "messageformat"); - }, - icon: "assignment_return" } - ); - - dashboardGroupActionsList.push( - { - onAction: function ($event) { - vm.grid.deleteItems($event); - }, - name: function() { return $translate.instant('dashboard.delete-dashboards') }, - details: deleteDashboardsActionTitle, - icon: "delete" - } - ); - - vm.dashboardGridConfig.addItemActions = []; - vm.dashboardGridConfig.addItemActions.push({ - onAction: function ($event) { - vm.grid.addItem($event); - }, - name: function() { return $translate.instant('action.create') }, - details: function() { return $translate.instant('dashboard.create-new-dashboard') }, - icon: "insert_drive_file" - }); - vm.dashboardGridConfig.addItemActions.push({ - onAction: function ($event) { - importExport.importDashboard($event).then( - function() { - vm.grid.refreshList(); - } - ); - }, - name: function() { return $translate.instant('action.import') }, - details: function() { return $translate.instant('dashboard.import') }, - icon: "file_upload" - }); - } else if (vm.dashboardsScope === 'customer' || vm.dashboardsScope === 'customer_user') { - fetchDashboardsFunction = function (pageLink) { - return dashboardService.getCustomerDashboards(customerId, pageLink); - }; - deleteDashboardFunction = function (dashboardId) { - return dashboardService.unassignDashboardFromCustomer(customerId, dashboardId); - }; - refreshDashboardsParamsFunction = function () { - return {"customerId": customerId, "topIndex": vm.topIndex}; - }; - - if (vm.dashboardsScope === 'customer') { - dashboardActionsList.push( - { - onAction: function ($event, item) { - exportDashboard($event, item); - }, - name: function() { $translate.instant('action.export') }, - details: function() { return $translate.instant('dashboard.export') }, - icon: "file_download" - } - ); - - dashboardActionsList.push( - { - onAction: function ($event, item) { - makePrivate($event, item); - }, - name: function() { return $translate.instant('action.make-private') }, - details: function() { return $translate.instant('dashboard.make-private') }, - icon: "reply", - isEnabled: function(dashboard) { - return dashboard && customerId == dashboard.publicCustomerId; - } - } - ); - - dashboardActionsList.push( - { - onAction: function ($event, item) { - unassignFromCustomer($event, item, customerId); - }, - name: function() { return $translate.instant('action.unassign') }, - details: function() { return $translate.instant('dashboard.unassign-from-customer') }, - icon: "assignment_return", - isEnabled: function(dashboard) { - return dashboard && customerId != dashboard.publicCustomerId; - } - } - ); - - dashboardGroupActionsList.push( - { - onAction: function ($event, items) { - unassignDashboardsFromCustomer($event, items, customerId); - }, - name: function() { return $translate.instant('dashboard.unassign-dashboards') }, - details: function(selectedCount) { - return $translate.instant('dashboard.unassign-dashboards-action-title', {count: selectedCount}, "messageformat"); - }, - icon: "assignment_return" - } - ); - - vm.dashboardGridConfig.addItemAction = { - onAction: function ($event) { - addDashboardsToCustomer($event); - }, - name: function() { return $translate.instant('dashboard.assign-dashboards') }, - details: function() { return $translate.instant('dashboard.assign-new-dashboard') }, - icon: "add" - }; - } else if (vm.dashboardsScope === 'customer_user') { - vm.dashboardGridConfig.addItemAction = {}; - } - } - - vm.dashboardGridConfig.refreshParamsFunc = refreshDashboardsParamsFunction; - vm.dashboardGridConfig.fetchItemsFunc = fetchDashboardsFunction; - vm.dashboardGridConfig.deleteItemFunc = deleteDashboardFunction; - - } - - function deleteDashboardTitle (dashboard) { - return $translate.instant('dashboard.delete-dashboard-title', {dashboardTitle: dashboard.title}); - } - - function deleteDashboardText () { - return $translate.instant('dashboard.delete-dashboard-text'); - } - - function deleteDashboardsTitle (selectedCount) { - return $translate.instant('dashboard.delete-dashboards-title', {count: selectedCount}, 'messageformat'); - } - - function deleteDashboardsActionTitle(selectedCount) { - return $translate.instant('dashboard.delete-dashboards-action-title', {count: selectedCount}, 'messageformat'); - } - - function deleteDashboardsText () { - return $translate.instant('dashboard.delete-dashboards-text'); - } - - function gridInited(grid) { - vm.grid = grid; - } - - function getDashboardTitle(dashboard) { - return dashboard ? dashboard.title : ''; - } - - function loadDashboard(dashboard) { - return dashboardService.getDashboard(dashboard.id.id); - } - - function saveDashboard(dashboard) { - var deferred = $q.defer(); - dashboardService.saveDashboard(dashboard).then( - function success(savedDashboard) { - var dashboards = [ savedDashboard ]; - customerService.applyAssignedCustomersInfo(dashboards).then( - function success(items) { - if (items && items.length == 1) { - deferred.resolve(items[0]); - } else { - deferred.reject(); - } - }, - function fail() { - deferred.reject(); - } - ); - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - - function manageAssignedCustomers($event, dashboard) { - showManageAssignedCustomersDialog($event, [dashboard.id.id], 'manage', dashboard.assignedCustomersIds); - } - - function assignDashboardsToCustomers($event, items) { - var dashboardIds = []; - for (var id in items.selections) { - dashboardIds.push(id); - } - showManageAssignedCustomersDialog($event, dashboardIds, 'assign'); - } - - function unassignDashboardsFromCustomers($event, items) { - var dashboardIds = []; - for (var id in items.selections) { - dashboardIds.push(id); - } - showManageAssignedCustomersDialog($event, dashboardIds, 'unassign'); - } - - function showManageAssignedCustomersDialog($event, dashboardIds, actionType, assignedCustomers) { - if ($event) { - $event.stopPropagation(); - } - $mdDialog.show({ - controller: 'ManageAssignedCustomersController', - controllerAs: 'vm', - templateUrl: manageAssignedCustomersTemplate, - locals: {actionType: actionType, dashboardIds: dashboardIds, assignedCustomers: assignedCustomers}, - parent: angular.element($document[0].body), - fullscreen: true, - targetEvent: $event - }).then(function () { - vm.grid.refreshList(); - }, function () { - }); - } - - function addDashboardsToCustomer($event) { - if ($event) { - $event.stopPropagation(); - } - var pageSize = 10; - dashboardService.getTenantDashboards({limit: pageSize, textSearch: ''}).then( - function success(_dashboards) { - var dashboards = { - pageSize: pageSize, - data: _dashboards.data, - nextPageLink: _dashboards.nextPageLink, - selections: {}, - selectedCount: 0, - hasNext: _dashboards.hasNext, - pending: false - }; - if (dashboards.hasNext) { - dashboards.nextPageLink.limit = pageSize; - } - $mdDialog.show({ - controller: 'AddDashboardsToCustomerController', - controllerAs: 'vm', - templateUrl: addDashboardsToCustomerTemplate, - locals: {customerId: customerId, dashboards: dashboards}, - parent: angular.element($document[0].body), - fullscreen: true, - targetEvent: $event - }).then(function () { - vm.grid.refreshList(); - }, function () { - }); - }, - function fail() { - }); - } - - function unassignFromCustomer($event, dashboard, customerId) { - if ($event) { - $event.stopPropagation(); - } - var title = $translate.instant('dashboard.unassign-dashboard-title', {dashboardTitle: dashboard.title}); - var content = $translate.instant('dashboard.unassign-dashboard-text'); - var label = $translate.instant('dashboard.unassign-dashboard'); - var confirm = $mdDialog.confirm() - .targetEvent($event) - .title(title) - .htmlContent(content) - .ariaLabel(label) - .cancel($translate.instant('action.no')) - .ok($translate.instant('action.yes')); - $mdDialog.show(confirm).then(function () { - dashboardService.unassignDashboardFromCustomer(customerId, dashboard.id.id).then(function success() { - vm.grid.refreshList(); - }); - }); - } - - function makePublic($event, dashboard) { - if ($event) { - $event.stopPropagation(); - } - dashboardService.makeDashboardPublic(dashboard.id.id).then(function success(dashboard) { - $mdDialog.show({ - controller: 'MakeDashboardPublicDialogController', - controllerAs: 'vm', - templateUrl: makeDashboardPublicDialogTemplate, - locals: {dashboard: dashboard}, - parent: angular.element($document[0].body), - fullscreen: true, - targetEvent: $event - }).then(function () { - vm.grid.refreshList(); - }); - }); - } - - function makePrivate($event, dashboard) { - if ($event) { - $event.stopPropagation(); - } - var title = $translate.instant('dashboard.make-private-dashboard-title', {dashboardTitle: dashboard.title}); - var content = $translate.instant('dashboard.make-private-dashboard-text'); - var label = $translate.instant('dashboard.make-private-dashboard'); - var confirm = $mdDialog.confirm() - .targetEvent($event) - .title(title) - .htmlContent(content) - .ariaLabel(label) - .cancel($translate.instant('action.no')) - .ok($translate.instant('action.yes')); - $mdDialog.show(confirm).then(function () { - dashboardService.makeDashboardPrivate(dashboard.id.id).then(function success() { - vm.grid.refreshList(); - }); - }); - } - - function exportDashboard($event, dashboard) { - $event.stopPropagation(); - importExport.exportDashboard(dashboard.id.id); - } - - function unassignDashboardsFromCustomer($event, items, customerId) { - var confirm = $mdDialog.confirm() - .targetEvent($event) - .title($translate.instant('dashboard.unassign-dashboards-title', {count: items.selectedCount}, 'messageformat')) - .htmlContent($translate.instant('dashboard.unassign-dashboards-text')) - .ariaLabel($translate.instant('dashboard.unassign-dashboards')) - .cancel($translate.instant('action.no')) - .ok($translate.instant('action.yes')); - $mdDialog.show(confirm).then(function () { - var tasks = []; - for (var id in items.selections) { - tasks.push(dashboardService.unassignDashboardFromCustomer(customerId, id)); - } - $q.all(tasks).then(function () { - vm.grid.refreshList(); - }); - }); - } - - function openDashboard($event, dashboard) { - if ($event) { - $event.stopPropagation(); - } - if (vm.dashboardsScope === 'customer') { - $state.go('home.customers.dashboards.dashboard', { - customerId: customerId, - dashboardId: dashboard.id.id - }); - } else { - $state.go('home.dashboards.dashboard', {dashboardId: dashboard.id.id}); - } - } -} diff --git a/ui/src/app/dashboard/dashboards.tpl.html b/ui/src/app/dashboard/dashboards.tpl.html deleted file mode 100644 index 2fb4e35e72..0000000000 --- a/ui/src/app/dashboard/dashboards.tpl.html +++ /dev/null @@ -1,44 +0,0 @@ - - - -
    -
    - - - - - - - - - -
    diff --git a/ui/src/app/dashboard/edit-widget.directive.js b/ui/src/app/dashboard/edit-widget.directive.js deleted file mode 100644 index dffb704683..0000000000 --- a/ui/src/app/dashboard/edit-widget.directive.js +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import entityAliasDialogTemplate from '../entity/alias/entity-alias-dialog.tpl.html'; -import editWidgetTemplate from './edit-widget.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function EditWidgetDirective($compile, $templateCache, types, widgetService, entityService, $q, $document, $mdDialog) { - - var linker = function (scope, element) { - var template = $templateCache.get(editWidgetTemplate); - element.html(template); - - scope.$watch('widget', function () { - if (scope.widget) { - widgetService.getWidgetInfo(scope.widget.bundleAlias, - scope.widget.typeAlias, - scope.widget.isSystemType).then( - function(widgetInfo) { - scope.$applyAsync(function(scope) { - scope.widgetConfig = { - config: scope.widget.config, - layout: scope.widgetLayout - }; - var settingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema; - var dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema; - scope.typeParameters = widgetInfo.typeParameters; - scope.actionSources = widgetInfo.actionSources; - scope.isDataEnabled = !widgetInfo.typeParameters.useCustomDatasources; - if (!settingsSchema || settingsSchema === '') { - scope.settingsSchema = {}; - } else { - scope.settingsSchema = angular.fromJson(settingsSchema); - } - if (!dataKeySettingsSchema || dataKeySettingsSchema === '') { - scope.dataKeySettingsSchema = {}; - } else { - scope.dataKeySettingsSchema = angular.fromJson(dataKeySettingsSchema); - } - - scope.functionsOnly = scope.dashboard ? false : true; - - scope.theForm.$setPristine(); - }); - } - ); - } - }); - - scope.$watch('widgetLayout', function () { - if (scope.widgetLayout && scope.widgetConfig) { - scope.widgetConfig.layout = scope.widgetLayout; - } - }); - - scope.fetchEntityKeys = function (entityAliasId, query, type) { - var deferred = $q.defer(); - scope.aliasController.getAliasInfo(entityAliasId).then( - function success(aliasInfo) { - var entity = aliasInfo.currentEntity; - if (entity) { - entityService.getEntityKeys(entity.entityType, entity.id, query, type, {ignoreLoading: true}).then( - function success(keys) { - deferred.resolve(keys); - }, - function fail() { - deferred.resolve([]); - } - ); - } else { - deferred.resolve([]); - } - }, - function fail() { - deferred.resolve([]); - } - ); - return deferred.promise; - }; - - scope.fetchDashboardStates = function(query) { - var deferred = $q.defer(); - var stateIds = Object.keys(scope.dashboard.configuration.states); - var result = query ? stateIds.filter( - createFilterForDashboardState(query)) : stateIds; - if (result && result.length) { - deferred.resolve(result); - } else { - deferred.resolve([query]); - } - return deferred.promise; - } - - function createFilterForDashboardState (query) { - var lowercaseQuery = angular.lowercase(query); - return function filterFn(stateId) { - return (angular.lowercase(stateId).indexOf(lowercaseQuery) === 0); - }; - } - - scope.createEntityAlias = function (event, alias, allowedEntityTypes) { - - var deferred = $q.defer(); - var singleEntityAlias = {id: null, alias: alias, filter: {}}; - - $mdDialog.show({ - controller: 'EntityAliasDialogController', - controllerAs: 'vm', - templateUrl: entityAliasDialogTemplate, - locals: { - isAdd: true, - allowedEntityTypes: allowedEntityTypes, - entityAliases: scope.dashboard.configuration.entityAliases, - alias: singleEntityAlias - }, - parent: angular.element($document[0].body), - fullscreen: true, - multiple: true, - targetEvent: event - }).then(function (singleEntityAlias) { - scope.dashboard.configuration.entityAliases[singleEntityAlias.id] = singleEntityAlias; - scope.aliasController.updateEntityAliases(scope.dashboard.configuration.entityAliases); - deferred.resolve(singleEntityAlias); - }, function () { - deferred.reject(); - }); - - return deferred.promise; - }; - - $compile(element.contents())(scope); - } - - return { - restrict: "E", - link: linker, - scope: { - dashboard: '=', - aliasController: '=', - widgetEditMode: '=', - widget: '=', - widgetLayout: '=', - theForm: '=' - } - }; -} diff --git a/ui/src/app/dashboard/edit-widget.tpl.html b/ui/src/app/dashboard/edit-widget.tpl.html deleted file mode 100644 index c8b6e9e8a9..0000000000 --- a/ui/src/app/dashboard/edit-widget.tpl.html +++ /dev/null @@ -1,32 +0,0 @@ - -
    - -
    diff --git a/ui/src/app/dashboard/index.js b/ui/src/app/dashboard/index.js deleted file mode 100644 index 06cfb2af73..0000000000 --- a/ui/src/app/dashboard/index.js +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './dashboard.scss'; - -import uiRouter from 'angular-ui-router'; - -import thingsboardGrid from '../components/grid.directive'; -import thingsboardApiWidget from '../api/widget.service'; -import thingsboardApiUser from '../api/user.service'; -import thingsboardApiDashboard from '../api/dashboard.service'; -import thingsboardApiCustomer from '../api/customer.service'; -import thingsboardDetailsSidenav from '../components/details-sidenav.directive'; -import thingsboardWidgetConfig from '../components/widget/widget-config.directive'; -import thingsboardDashboardSelect from '../components/dashboard-select.directive'; -import thingsboardRelatedEntityAutocomplete from '../components/related-entity-autocomplete.directive'; -import thingsboardDashboard from '../components/dashboard.directive'; -import thingsboardExpandFullscreen from '../components/expand-fullscreen.directive'; -import thingsboardWidgetsBundleSelect from '../components/widgets-bundle-select.directive'; -import thingsboardSocialsharePanel from '../components/socialshare-panel.directive'; -import thingsboardTypes from '../common/types.constant'; -import thingsboardItemBuffer from '../services/item-buffer.service'; -import thingsboardImportExport from '../import-export'; -import dashboardLayouts from './layouts'; -import dashboardStates from './states'; - -import DashboardRoutes from './dashboard.routes'; -import {DashboardsController, DashboardCardController, MakeDashboardPublicDialogController} from './dashboards.controller'; -import DashboardController from './dashboard.controller'; -import DashboardSettingsController from './dashboard-settings.controller'; -import AddDashboardsToCustomerController from './add-dashboards-to-customer.controller'; -import ManageAssignedCustomersController from './manage-assigned-customers.controller'; -import AddWidgetController from './add-widget.controller'; -import DashboardDirective from './dashboard.directive'; -import EditWidgetDirective from './edit-widget.directive'; -import DashboardToolbar from './dashboard-toolbar.directive'; - -export default angular.module('thingsboard.dashboard', [ - uiRouter, - thingsboardTypes, - thingsboardItemBuffer, - thingsboardImportExport, - thingsboardGrid, - thingsboardApiWidget, - thingsboardApiUser, - thingsboardApiDashboard, - thingsboardApiCustomer, - thingsboardDetailsSidenav, - thingsboardWidgetConfig, - thingsboardDashboardSelect, - thingsboardRelatedEntityAutocomplete, - thingsboardDashboard, - thingsboardExpandFullscreen, - thingsboardWidgetsBundleSelect, - thingsboardSocialsharePanel, - dashboardLayouts, - dashboardStates -]) - .config(DashboardRoutes) - .controller('DashboardsController', DashboardsController) - .controller('DashboardCardController', DashboardCardController) - .controller('MakeDashboardPublicDialogController', MakeDashboardPublicDialogController) - .controller('DashboardController', DashboardController) - .controller('DashboardSettingsController', DashboardSettingsController) - .controller('AddDashboardsToCustomerController', AddDashboardsToCustomerController) - .controller('ManageAssignedCustomersController', ManageAssignedCustomersController) - .controller('AddWidgetController', AddWidgetController) - .directive('tbDashboardDetails', DashboardDirective) - .directive('tbEditWidget', EditWidgetDirective) - .directive('tbDashboardToolbar', DashboardToolbar) - .name; diff --git a/ui/src/app/dashboard/layouts/dashboard-layout.directive.js b/ui/src/app/dashboard/layouts/dashboard-layout.directive.js deleted file mode 100644 index 3f05c17241..0000000000 --- a/ui/src/app/dashboard/layouts/dashboard-layout.directive.js +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import dashboardLayoutTemplate from './dashboard-layout.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function DashboardLayout() { - return { - restrict: "E", - scope: true, - bindToController: { - layoutCtx: '=', - dashboardCtx: '=', - isEdit: '=', - isMobile: '=', - widgetEditMode: '=' - }, - controller: DashboardLayoutController, - controllerAs: 'vm', - templateUrl: dashboardLayoutTemplate - }; -} - -/*@ngInject*/ -function DashboardLayoutController($scope, $rootScope, $translate, $window, hotkeys, itembuffer) { - - var vm = this; - - vm.noData = noData; - vm.addWidget = addWidget; - vm.editWidget = editWidget; - vm.exportWidget = exportWidget; - vm.widgetMouseDown = widgetMouseDown; - vm.widgetClicked = widgetClicked; - vm.prepareDashboardContextMenu = prepareDashboardContextMenu; - vm.prepareWidgetContextMenu = prepareWidgetContextMenu; - vm.removeWidget = removeWidget; - vm.pasteWidget = pasteWidget; - vm.pasteWidgetReference = pasteWidgetReference; - vm.dashboardInited = dashboardInited; - vm.dashboardInitFailed = dashboardInitFailed; - - vm.reload = function() { - if (vm.dashboardContainer) { - vm.dashboardContainer.reload(); - } - }; - - vm.setResizing = function(resizing) { - if (vm.dashboardContainer) { - vm.dashboardContainer.isResizing = resizing; - } - } - - vm.resetHighlight = function() { - if (vm.dashboardContainer) { - vm.dashboardContainer.resetHighlight(); - } - }; - - vm.highlightWidget = function(widget, delay) { - if (vm.dashboardContainer) { - vm.dashboardContainer.highlightWidget(widget, delay); - } - }; - - vm.selectWidget = function(widget, delay) { - if (vm.dashboardContainer) { - vm.dashboardContainer.selectWidget(widget, delay); - } - }; - - vm.dashboardInitComplete = false; - - initHotKeys(); - - $scope.$on('$destroy', function() { - vm.dashboardContainer = null; - }); - - $scope.$watch('vm.layoutCtx', function () { - if (vm.layoutCtx) { - vm.layoutCtx.ctrl = vm; - } - }); - - function noData() { - return vm.dashboardInitComplete && vm.layoutCtx && - vm.layoutCtx.widgets && vm.layoutCtx.widgets.length == 0; - } - - function addWidget($event) { - if (vm.dashboardCtx.onAddWidget) { - vm.dashboardCtx.onAddWidget($event, vm.layoutCtx); - } - } - - function editWidget($event, widget) { - if (vm.dashboardCtx.onEditWidget) { - vm.dashboardCtx.onEditWidget($event, vm.layoutCtx, widget); - } - } - - function exportWidget($event, widget) { - if (vm.dashboardCtx.onExportWidget) { - vm.dashboardCtx.onExportWidget($event, vm.layoutCtx, widget); - } - } - - function widgetMouseDown($event, widget) { - if (vm.dashboardCtx.onWidgetMouseDown) { - vm.dashboardCtx.onWidgetMouseDown($event, vm.layoutCtx, widget); - } - } - - function widgetClicked($event, widget) { - if (vm.dashboardCtx.onWidgetClicked) { - vm.dashboardCtx.onWidgetClicked($event, vm.layoutCtx, widget); - } - } - - function prepareDashboardContextMenu() { - if (vm.dashboardCtx.prepareDashboardContextMenu) { - return vm.dashboardCtx.prepareDashboardContextMenu(vm.layoutCtx); - } - } - - function prepareWidgetContextMenu(widget) { - if (vm.dashboardCtx.prepareWidgetContextMenu) { - return vm.dashboardCtx.prepareWidgetContextMenu(vm.layoutCtx, widget); - } - } - - function removeWidget($event, widget) { - if (vm.dashboardCtx.onRemoveWidget) { - vm.dashboardCtx.onRemoveWidget($event, vm.layoutCtx, widget); - } - } - - function dashboardInitFailed() { - var parentScope = $window.parent.angular.element($window.frameElement).scope(); - parentScope.$emit('widgetEditModeInited'); - parentScope.$apply(); - vm.dashboardInitComplete = true; - } - - function dashboardInited(dashboardContainer) { - vm.dashboardContainer = dashboardContainer; - vm.dashboardInitComplete = true; - } - - function isHotKeyAllowed(event) { - var target = event.target || event.srcElement; - var scope = angular.element(target).scope(); - return scope && scope.$parent !== $rootScope; - } - - function initHotKeys() { - $translate(['action.copy', 'action.paste', 'action.delete']).then(function (translations) { - hotkeys.bindTo($scope) - .add({ - combo: 'ctrl+c', - description: translations['action.copy'], - callback: function (event) { - if (isHotKeyAllowed(event) && - vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) { - var widget = vm.dashboardContainer.getSelectedWidget(); - if (widget) { - event.preventDefault(); - copyWidget(event, widget); - } - } - } - }) - .add({ - combo: 'ctrl+r', - description: translations['action.copy-reference'], - callback: function (event) { - if (isHotKeyAllowed(event) && - vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) { - var widget = vm.dashboardContainer.getSelectedWidget(); - if (widget) { - event.preventDefault(); - copyWidgetReference(event, widget); - } - } - } - }) - .add({ - combo: 'ctrl+v', - description: translations['action.paste'], - callback: function (event) { - if (isHotKeyAllowed(event) && - vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) { - if (itembuffer.hasWidget()) { - event.preventDefault(); - pasteWidget(event); - } - } - } - }) - .add({ - combo: 'ctrl+i', - description: translations['action.paste-reference'], - callback: function (event) { - if (isHotKeyAllowed(event) && - vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) { - if (itembuffer.canPasteWidgetReference(vm.dashboardCtx.dashboard, - vm.dashboardCtx.state, vm.layoutCtx.id)) { - event.preventDefault(); - pasteWidgetReference(event); - } - } - } - }) - - .add({ - combo: 'ctrl+x', - description: translations['action.delete'], - callback: function (event) { - if (isHotKeyAllowed(event) && - vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) { - var widget = vm.dashboardContainer.getSelectedWidget(); - if (widget) { - event.preventDefault(); - vm.dashboardCtx.onRemoveWidget(event, vm.layoutCtx, widget); - } - } - } - }); - }); - } - - function copyWidget($event, widget) { - if (vm.dashboardCtx.copyWidget) { - vm.dashboardCtx.copyWidget($event, vm.layoutCtx, widget); - } - } - - function copyWidgetReference($event, widget) { - if (vm.dashboardCtx.copyWidgetReference) { - vm.dashboardCtx.copyWidgetReference($event, vm.layoutCtx, widget); - } - } - - function pasteWidget($event) { - var pos = vm.dashboardContainer.getEventGridPosition($event); - if (vm.dashboardCtx.pasteWidget) { - vm.dashboardCtx.pasteWidget($event, vm.layoutCtx, pos); - } - } - - function pasteWidgetReference($event) { - var pos = vm.dashboardContainer.getEventGridPosition($event); - if (vm.dashboardCtx.pasteWidgetReference) { - vm.dashboardCtx.pasteWidgetReference($event, vm.layoutCtx, pos); - } - } - -} diff --git a/ui/src/app/dashboard/layouts/dashboard-layout.tpl.html b/ui/src/app/dashboard/layouts/dashboard-layout.tpl.html deleted file mode 100644 index 4007add4e7..0000000000 --- a/ui/src/app/dashboard/layouts/dashboard-layout.tpl.html +++ /dev/null @@ -1,71 +0,0 @@ - - -
    - - dashboard.no-widgets - - - add - {{ 'dashboard.add-widget' | translate }} - -
    - - -
    diff --git a/ui/src/app/dashboard/layouts/index.js b/ui/src/app/dashboard/layouts/index.js deleted file mode 100644 index 1fdfb11f00..0000000000 --- a/ui/src/app/dashboard/layouts/index.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import ManageDashboardLayoutsController from './manage-dashboard-layouts.controller'; -import SelectTargetLayoutController from './select-target-layout.controller'; -import DashboardLayoutDirective from './dashboard-layout.directive'; - -export default angular.module('thingsboard.dashboard.layouts', []) - .controller('ManageDashboardLayoutsController', ManageDashboardLayoutsController) - .controller('SelectTargetLayoutController', SelectTargetLayoutController) - .directive('tbDashboardLayout', DashboardLayoutDirective) - .name; diff --git a/ui/src/app/dashboard/layouts/manage-dashboard-layouts.controller.js b/ui/src/app/dashboard/layouts/manage-dashboard-layouts.controller.js deleted file mode 100644 index 2fa1a5a304..0000000000 --- a/ui/src/app/dashboard/layouts/manage-dashboard-layouts.controller.js +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import dashboardSettingsTemplate from '../dashboard-settings.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function ManageDashboardLayoutsController($scope, $mdDialog, $document, dashboardUtils, layouts) { - - var vm = this; - - vm.openLayoutSettings = openLayoutSettings; - vm.cancel = cancel; - vm.save = save; - - vm.layouts = layouts; - vm.displayLayouts = { - main: angular.isDefined(vm.layouts.main), - right: angular.isDefined(vm.layouts.right) - } - - for (var l in vm.displayLayouts) { - if (!vm.layouts[l]) { - vm.layouts[l] = dashboardUtils.createDefaultLayoutData(); - } - } - - function openLayoutSettings($event, layoutId) { - var gridSettings = angular.copy(vm.layouts[layoutId].gridSettings); - $mdDialog.show({ - controller: 'DashboardSettingsController', - controllerAs: 'vm', - templateUrl: dashboardSettingsTemplate, - locals: { - settings: null, - gridSettings: gridSettings - }, - parent: angular.element($document[0].body), - multiple: true, - fullscreen: true, - targetEvent: $event - }).then(function (data) { - var gridSettings = data.gridSettings; - if (gridSettings) { - dashboardUtils.updateLayoutSettings(vm.layouts[layoutId], gridSettings); - } - $scope.theForm.$setDirty(); - }, function () { - }); - } - - function cancel() { - $mdDialog.cancel(); - } - - function save() { - $scope.theForm.$setPristine(); - for (var l in vm.displayLayouts) { - if (!vm.displayLayouts[l]) { - if (vm.layouts[l]) { - delete vm.layouts[l]; - } - } - } - $mdDialog.hide(vm.layouts); - } -} diff --git a/ui/src/app/dashboard/layouts/manage-dashboard-layouts.tpl.html b/ui/src/app/dashboard/layouts/manage-dashboard-layouts.tpl.html deleted file mode 100644 index ece9aa7987..0000000000 --- a/ui/src/app/dashboard/layouts/manage-dashboard-layouts.tpl.html +++ /dev/null @@ -1,65 +0,0 @@ - - -
    - -
    -

    {{ 'layout.manage' }}

    - - - - -
    -
    - - - -
    -
    -
    - {{ 'layout.main' | translate }} - - {{ 'layout.right' | translate }} - -
    -
    - - layout.main - - - layout.right - -
    -
    -
    -
    - - - - {{ 'action.save' | translate }} - - {{ 'action.cancel' | translate }} - -
    -
    diff --git a/ui/src/app/dashboard/layouts/select-target-layout.controller.js b/ui/src/app/dashboard/layouts/select-target-layout.controller.js deleted file mode 100644 index f8043e4b60..0000000000 --- a/ui/src/app/dashboard/layouts/select-target-layout.controller.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/*@ngInject*/ -export default function SelectTargetLayoutController($scope, $mdDialog) { - - var vm = this; - - vm.cancel = cancel; - vm.selectLayout = selectLayout; - - function cancel() { - $mdDialog.cancel(); - } - - function selectLayout($event, layoutId) { - $scope.theForm.$setPristine(); - $mdDialog.hide(layoutId); - } -} diff --git a/ui/src/app/dashboard/layouts/select-target-layout.tpl.html b/ui/src/app/dashboard/layouts/select-target-layout.tpl.html deleted file mode 100644 index e51ac94a5c..0000000000 --- a/ui/src/app/dashboard/layouts/select-target-layout.tpl.html +++ /dev/null @@ -1,48 +0,0 @@ - - -
    - -
    -

    {{ 'layout.select' }}

    - - - - -
    -
    - - - -
    -
    -
    - - layout.main - - - layout.right - -
    -
    -
    -
    -
    -
    diff --git a/ui/src/app/dashboard/make-dashboard-public-dialog.tpl.html b/ui/src/app/dashboard/make-dashboard-public-dialog.tpl.html deleted file mode 100644 index 5e0ff9d21e..0000000000 --- a/ui/src/app/dashboard/make-dashboard-public-dialog.tpl.html +++ /dev/null @@ -1,62 +0,0 @@ - - -
    - -
    -

    - - - - -
    -
    - -
    - - -
    -
    {{ vm.publicLink }}
    - - - - {{ 'dashboard.copy-public-link' | translate }} - - -
    -
    dashboard.public-dashboard-notice
    - - -
    -
    -
    - - - {{ 'action.ok' | - translate }} - - -
    -
    diff --git a/ui/src/app/dashboard/manage-assigned-customers.controller.js b/ui/src/app/dashboard/manage-assigned-customers.controller.js deleted file mode 100644 index 8ca931e6e4..0000000000 --- a/ui/src/app/dashboard/manage-assigned-customers.controller.js +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/*@ngInject*/ -export default function ManageAssignedCustomersController($mdDialog, $q, types, dashboardService, actionType, dashboardIds, assignedCustomers) { - - var vm = this; - - vm.types = types; - vm.actionType = actionType; - vm.dashboardIds = dashboardIds; - vm.assignedCustomers = assignedCustomers; - if (actionType != 'manage') { - vm.assignedCustomers = []; - } - - if (actionType == 'manage') { - vm.titleText = 'dashboard.manage-assigned-customers'; - vm.labelText = 'dashboard.assigned-customers'; - vm.actionName = 'action.update'; - } else if (actionType == 'assign') { - vm.titleText = 'dashboard.assign-to-customers'; - vm.labelText = 'dashboard.assign-to-customers-text'; - vm.actionName = 'action.assign'; - } else if (actionType == 'unassign') { - vm.titleText = 'dashboard.unassign-from-customers'; - vm.labelText = 'dashboard.unassign-from-customers-text'; - vm.actionName = 'action.unassign'; - } - - vm.submit = submit; - vm.cancel = cancel; - - function cancel () { - $mdDialog.cancel(); - } - - function submit () { - var tasks = []; - for (var i=0;i - -
    - -
    -

    {{vm.titleText}}

    - - - - -
    -
    - - - -
    -
    - {{vm.labelText}} - -
    -
    -
    - - - - {{ vm.actionName | translate }} - - {{ 'action.cancel' | - translate }} - - -
    -
    \ No newline at end of file diff --git a/ui/src/app/dashboard/states/dashboard-state-dialog.controller.js b/ui/src/app/dashboard/states/dashboard-state-dialog.controller.js deleted file mode 100644 index 0e6d5d5b38..0000000000 --- a/ui/src/app/dashboard/states/dashboard-state-dialog.controller.js +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/*@ngInject*/ -export default function DashboardStateDialogController($scope, $mdDialog, $filter, dashboardUtils, isAdd, allStates, state) { - - var vm = this; - - vm.isAdd = isAdd; - vm.allStates = allStates; - vm.state = state; - - vm.stateIdTouched = false; - - if (vm.isAdd) { - vm.state = dashboardUtils.createDefaultState('', false); - vm.state.id = ''; - vm.prevStateId = ''; - } else { - vm.state = state; - vm.prevStateId = vm.state.id; - } - - vm.cancel = cancel; - vm.save = save; - - $scope.$watch("vm.state.name", function(newVal, prevVal) { - if (!angular.equals(newVal, prevVal) && vm.state.name != null) { - checkStateName(); - } - }); - - $scope.$watch("vm.state.id", function(newVal, prevVal) { - if (!angular.equals(newVal, prevVal) && vm.state.id != null) { - checkStateId(); - } - }); - - function checkStateName() { - if (!vm.stateIdTouched && vm.isAdd) { - vm.state.id = vm.state.name.toLowerCase().replace(/\W/g,"_"); - } - } - - function checkStateId() { - var result = $filter('filter')(vm.allStates, {id: vm.state.id}, true); - if (result && result.length && result[0].id !== vm.prevStateId) { - $scope.theForm.stateId.$setValidity('stateExists', false); - } else { - $scope.theForm.stateId.$setValidity('stateExists', true); - } - } - - function cancel() { - $mdDialog.cancel(); - } - - function save() { - $scope.theForm.$setPristine(); - vm.state.id = vm.state.id.trim(); - $mdDialog.hide(vm.state); - } -} diff --git a/ui/src/app/dashboard/states/dashboard-state-dialog.tpl.html b/ui/src/app/dashboard/states/dashboard-state-dialog.tpl.html deleted file mode 100644 index e3bbd3c424..0000000000 --- a/ui/src/app/dashboard/states/dashboard-state-dialog.tpl.html +++ /dev/null @@ -1,69 +0,0 @@ - - -
    - -
    -

    {{ (vm.isAdd ? 'dashboard.add-state' : 'dashboard.edit-state') | translate }}

    - - - - -
    -
    - - - -
    - -
    - - - -
    -
    dashboard.state-name-required
    -
    -
    - - - -
    -
    dashboard.state-id-required
    -
    dashboard.state-id-exists
    -
    -
    - {{ 'dashboard.is-root-state' | translate }} - -
    -
    -
    -
    - - - - {{ (vm.isAdd ? 'action.add' : 'action.save') | translate }} - - - {{ 'action.cancel' | translate }} - - -
    -
    diff --git a/ui/src/app/dashboard/states/default-state-controller.js b/ui/src/app/dashboard/states/default-state-controller.js deleted file mode 100644 index 89c71e3ad8..0000000000 --- a/ui/src/app/dashboard/states/default-state-controller.js +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './default-state-controller.scss'; - -/*@ngInject*/ -export default function DefaultStateController($scope, $timeout, $location, $state, - $stateParams, utils, types, dashboardUtils, preservedState) { - - var vm = this; - - vm.inited = false; - - vm.skipStateChange = false; - - vm.openState = openState; - vm.updateState = updateState; - vm.resetState = resetState; - vm.getStateObject = getStateObject; - vm.navigatePrevState = navigatePrevState; - vm.getStateId = getStateId; - vm.getStateIndex = getStateIndex; - vm.getStateIdAtIndex = getStateIdAtIndex; - vm.getStateParams = getStateParams; - vm.getStateParamsByStateId = getStateParamsByStateId; - vm.getEntityId = getEntityId; - - vm.getStateName = getStateName; - - vm.displayStateSelection = displayStateSelection; - - function openState(id, params, openRightLayout) { - if (vm.states && vm.states[id]) { - if (!params) { - params = {}; - } - var newState = { - id: id, - params: params - } - //append new state - vm.stateObject[0] = newState; - gotoState(vm.stateObject[0].id, true, openRightLayout); - vm.skipStateChange = true; - } - } - - function updateState(id, params, openRightLayout) { - if (!id) { - id = getStateId(); - } - if (vm.states && vm.states[id]) { - if (!params) { - params = {}; - } - var newState = { - id: id, - params: params - } - //replace with new state - vm.stateObject[0] = newState; - gotoState(vm.stateObject[0].id, true, openRightLayout); - vm.skipStateChange = true; - } - } - - function resetState() { - var rootStateId = dashboardUtils.getRootStateId(vm.states); - vm.stateObject = [ { id: rootStateId, params: {} } ]; - gotoState(rootStateId, true); - } - - function getStateObject() { - return vm.stateObject; - } - - function navigatePrevState(index) { - if (index < vm.stateObject.length-1) { - vm.stateObject.splice(index+1, vm.stateObject.length-index-1); - gotoState(vm.stateObject[vm.stateObject.length-1].id, true); - vm.skipStateChange = true; - } - } - - function getStateId() { - if (vm.stateObject && vm.stateObject.length) { - return vm.stateObject[vm.stateObject.length-1].id; - } else { - return ''; - } - } - - function getStateIndex() { - if (vm.stateObject && vm.stateObject.length) { - return vm.stateObject.length-1; - } else { - return -1; - } - } - - function getStateIdAtIndex(index) { - if (vm.stateObject && vm.stateObject[index]) { - return vm.stateObject[index].id; - } else { - return ''; - } - } - - function getStateParams() { - if (vm.stateObject && vm.stateObject.length) { - return vm.stateObject[vm.stateObject.length - 1].params; - } else { - return {}; - } - } - - function getStateParamsByStateId(stateId) { - var stateObj = getStateObjById(stateId); - if (stateObj) { - return stateObj.params; - } else { - return null; - } - } - - function getEntityId() { - return null; - } - - function getStateObjById(id) { - for (var i=0; i < vm.stateObject.length; i++) { - if (vm.stateObject[i].id === id) { - return vm.stateObject[i]; - } - } - return null; - } - - function getStateName(id, state) { - return utils.customTranslation(state.name, id); - } - - function parseState(stateBase64) { - var result; - if (stateBase64) { - try { - result = utils.base64toObj(stateBase64); - } catch (e) { - result = [ { id: null, params: {} } ]; - } - } - if (!result) { - result = []; - } - if (!result.length) { - result[0] = { id: null, params: {} } - } else if (result.length > 1) { - var newResult = []; - newResult.push(result[result.length-1]); - result = newResult; - } - - var rootStateId = dashboardUtils.getRootStateId(vm.states); - if (!result[0].id) { - result[0].id = rootStateId; - } - if (!vm.states[result[0].id]) { - result[0].id = rootStateId; - } - var i = result.length; - while (i--) { - if (!result[i].id || !vm.states[result[i].id]) { - result.splice(i, 1); - } - } - return result; - } - - $scope.$watch('vm.states', function() { - if (vm.states) { - if (!vm.inited) { - vm.inited = true; - init(); - } - } - }); - - function displayStateSelection() { - return vm.states && Object.keys(vm.states).length > 1; - } - - function init() { - if (preservedState) { - vm.stateObject = preservedState; - gotoState(vm.stateObject[0].id, true); - } else { - var initialState = $stateParams.state; - vm.stateObject = parseState(initialState); - gotoState(vm.stateObject[0].id, false); - } - - $timeout(() => { - $scope.$watchCollection(function () { - return $state.params; - }, function () { - var currentState = $state.params.state; - vm.stateObject = parseState(currentState); - }); - - $scope.$watch('vm.dashboardCtrl.dashboardCtx.state', function () { - if (vm.stateObject[0].id !== vm.dashboardCtrl.dashboardCtx.state) { - vm.stateObject[0].id = vm.dashboardCtrl.dashboardCtx.state; - updateLocation(); - vm.skipStateChange = true; - } - }); - $scope.$watch('vm.stateObject', function(newVal, prevVal) { - if (!angular.equals(newVal, prevVal) && newVal) { - if (vm.skipStateChange) { - vm.skipStateChange = false; - } else { - gotoState(vm.stateObject[0].id, true); - } - } - }, true); - }); - } - - function gotoState(stateId, update, openRightLayout) { - if (vm.dashboardCtrl.dashboardCtx.state != stateId) { - vm.dashboardCtrl.openDashboardState(stateId, openRightLayout); - if (update) { - updateLocation(); - } - } - } - - function updateLocation() { - if (vm.stateObject[0].id) { - $location.search('state', utils.objToBase64(vm.stateObject)); - } - } -} diff --git a/ui/src/app/dashboard/states/default-state-controller.scss b/ui/src/app/dashboard/states/default-state-controller.scss deleted file mode 100644 index fb66480c9b..0000000000 --- a/ui/src/app/dashboard/states/default-state-controller.scss +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -md-select.default-state-controller { - margin: 0; -} diff --git a/ui/src/app/dashboard/states/default-state-controller.tpl.html b/ui/src/app/dashboard/states/default-state-controller.tpl.html deleted file mode 100644 index 3f320ec98b..0000000000 --- a/ui/src/app/dashboard/states/default-state-controller.tpl.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - {{vm.getStateName(stateId, state)}} - - diff --git a/ui/src/app/dashboard/states/entity-state-controller.js b/ui/src/app/dashboard/states/entity-state-controller.js deleted file mode 100644 index 8d9371574d..0000000000 --- a/ui/src/app/dashboard/states/entity-state-controller.js +++ /dev/null @@ -1,367 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './entity-state-controller.scss'; - -/*@ngInject*/ -export default function EntityStateController($scope, $timeout, $location, $state, $stateParams, - $q, $translate, utils, types, dashboardUtils, entityService, preservedState) { - - var vm = this; - - vm.inited = false; - - vm.skipStateChange = false; - - vm.openState = openState; - vm.updateState = updateState; - vm.resetState = resetState; - vm.getStateObject = getStateObject; - vm.navigatePrevState = navigatePrevState; - vm.getStateId = getStateId; - vm.getStateIndex = getStateIndex; - vm.getStateIdAtIndex = getStateIdAtIndex; - vm.getStateParams = getStateParams; - vm.getStateParamsByStateId = getStateParamsByStateId; - vm.getEntityId = getEntityId; - - vm.getStateName = getStateName; - - vm.selectedStateIndex = -1; - - function openState(id, params, openRightLayout) { - if (vm.states && vm.states[id]) { - resolveEntity(params).then( - function success() { - var newState = { - id: id, - params: params - } - //append new state - vm.stateObject.push(newState); - vm.selectedStateIndex = vm.stateObject.length-1; - gotoState(vm.stateObject[vm.stateObject.length-1].id, true, openRightLayout); - vm.skipStateChange = true; - } - ); - } - } - - function updateState(id, params, openRightLayout) { - if (!id) { - id = getStateId(); - } - if (vm.states && vm.states[id]) { - resolveEntity(params).then( - function success() { - var newState = { - id: id, - params: params - } - //replace with new state - vm.stateObject[vm.stateObject.length - 1] = newState; - gotoState(vm.stateObject[vm.stateObject.length - 1].id, true, openRightLayout); - vm.skipStateChange = true; - } - ); - } - } - - function resetState() { - var rootStateId = dashboardUtils.getRootStateId(vm.states); - vm.stateObject = [ { id: rootStateId, params: {} } ]; - gotoState(rootStateId, true); - } - - function getStateObject() { - return vm.stateObject; - } - - function navigatePrevState(index) { - if (index < vm.stateObject.length-1) { - vm.stateObject.splice(index+1, vm.stateObject.length-index-1); - vm.selectedStateIndex = vm.stateObject.length-1; - gotoState(vm.stateObject[vm.stateObject.length-1].id, true); - vm.skipStateChange = true; - } - } - - function getStateId() { - if (vm.stateObject && vm.stateObject.length) { - return vm.stateObject[vm.stateObject.length-1].id; - } else { - return ''; - } - } - - function getStateIndex() { - if (vm.stateObject && vm.stateObject.length) { - return vm.stateObject.length-1; - } else { - return -1; - } - } - - function getStateIdAtIndex(index) { - if (vm.stateObject && vm.stateObject[index]) { - return vm.stateObject[index].id; - } else { - return ''; - } - } - - function getStateParams() { - if (vm.stateObject && vm.stateObject.length) { - return vm.stateObject[vm.stateObject.length - 1].params; - } else { - return {}; - } - } - - function getStateParamsByStateId(stateId) { - var stateObj = getStateObjById(stateId); - if (stateObj) { - return stateObj.params; - } else { - return null; - } - } - - function getEntityId(entityParamName) { - var stateParams = getStateParams(); - if (!entityParamName || !entityParamName.length) { - return stateParams.entityId; - } else if (stateParams[entityParamName]) { - return stateParams[entityParamName].entityId; - } - return null; - } - - function getStateObjById(id) { - for (var i=0; i < vm.stateObject.length; i++) { - if (vm.stateObject[i].id === id) { - return vm.stateObject[i]; - } - } - return null; - } - - function getStateName(index) { - let result = ''; - if (vm.stateObject[index]) { - let stateName = vm.states[vm.stateObject[index].id].name; - stateName = utils.customTranslation(stateName, stateName); - var params = vm.stateObject[index].params; - - let entityName = params && params.entityName ? params.entityName : ''; - let entityLabel = params && params.entityLabel ? params.entityLabel : entityName; - - result = utils.insertVariable(stateName, 'entityName', entityName); - result = utils.insertVariable(result, 'entityLabel', entityLabel); - for (let prop in params) { - if (params[prop] && params[prop].entityName) { - result = utils.insertVariable(result, prop + ':entityName', params[prop].entityName); - } - if (params[prop] && params[prop].entityLabel) { - result = utils.insertVariable(result, prop + ':entityLabel', params[prop].entityLabel); - } - } - } - return result; - } - - function resolveEntity(params) { - var deferred = $q.defer(); - if (params && params.targetEntityParamName) { - params = params[params.targetEntityParamName]; - } - if (params && params.entityId && params.entityId.id && params.entityId.entityType) { - if (isEntityResolved(params)) { - deferred.resolve(); - } else { - entityService.getEntity(params.entityId.entityType, params.entityId.id, { - ignoreLoading: true, - ignoreErrors: true - }).then( - function success(entity) { - params.entityName = entity.name; - params.entityLabel = entity.label; - deferred.resolve(); - }, - function fail() { - deferred.reject(); - } - ); - } - } else { - deferred.resolve(); - } - return deferred.promise; - } - - function isEntityResolved(params) { - if (!params.entityName || !params.entityName.length) { - return false; - } - return true; - } - - function parseState(stateBase64) { - var result; - if (stateBase64) { - try { - result = utils.base64toObj(stateBase64); - } catch (e) { - result = [ { id: null, params: {} } ]; - } - } - if (!result) { - result = []; - } - if (!result.length) { - result[0] = { id: null, params: {} } - } - var rootStateId = dashboardUtils.getRootStateId(vm.states); - if (!result[0].id) { - result[0].id = rootStateId; - } - if (!vm.states[result[0].id]) { - result[0].id = rootStateId; - } - var i = result.length; - while (i--) { - if (!result[i].id || !vm.states[result[i].id]) { - result.splice(i, 1); - } - } - return result; - } - - $scope.$watch('vm.states', function() { - if (vm.states) { - if (!vm.inited) { - vm.inited = true; - init(); - } - } - }); - - function init() { - if (preservedState) { - vm.stateObject = preservedState; - vm.selectedStateIndex = vm.stateObject.length-1; - gotoState(vm.stateObject[vm.stateObject.length-1].id, true); - } else { - var initialState = $stateParams.state; - vm.stateObject = parseState(initialState); - vm.selectedStateIndex = vm.stateObject.length-1; - gotoState(vm.stateObject[vm.stateObject.length-1].id, false); - } - - $timeout(() => { - $scope.$watchCollection(function () { - return $state.params; - }, function () { - var currentState = $state.params.state; - vm.stateObject = parseState(currentState); - }); - - $scope.$watch('vm.dashboardCtrl.dashboardCtx.state', function () { - if (vm.stateObject[vm.stateObject.length - 1].id !== vm.dashboardCtrl.dashboardCtx.state) { - vm.stateObject[vm.stateObject.length - 1].id = vm.dashboardCtrl.dashboardCtx.state; - updateLocation(); - vm.skipStateChange = true; - } - }); - - $scope.$watch('vm.stateObject', function(newVal, prevVal) { - if (!angular.equals(newVal, prevVal) && newVal) { - if (vm.skipStateChange) { - vm.skipStateChange = false; - } else { - vm.selectedStateIndex = vm.stateObject.length - 1; - gotoState(vm.stateObject[vm.stateObject.length - 1].id, true); - } - } - }, true); - - if (vm.dashboardCtrl.isMobile) { - watchSelectedStateIndex(); - } - - $scope.$watch('vm.dashboardCtrl.isMobile', function (newVal, prevVal) { - if (!angular.equals(newVal, prevVal)) { - if (vm.dashboardCtrl.isMobile) { - watchSelectedStateIndex(); - } else { - stopWatchSelectedStateIndex(); - } - } - }); - }); - - } - - function stopWatchSelectedStateIndex() { - if (vm.selectedStateIndexWatcher) { - vm.selectedStateIndexWatcher(); - vm.selectedStateIndexWatcher = null; - } - } - - function watchSelectedStateIndex() { - vm.selectedStateIndexWatcher = $scope.$watch('vm.selectedStateIndex', function(newVal, prevVal) { - if (!angular.equals(newVal, prevVal)) { - navigatePrevState(vm.selectedStateIndex); - } - }); - } - - function gotoState(stateId, update, openRightLayout) { - vm.dashboardCtrl.openDashboardState(stateId, openRightLayout); - if (update) { - updateLocation(); - } - } - - function updateLocation() { - if (vm.stateObject[vm.stateObject.length-1].id) { - if (isDefaultState()) { - $location.search('state', null); - } else { - $location.search('state', utils.objToBase64(vm.stateObject)); - } - } - } - - function isDefaultState() { - if (vm.stateObject.length == 1) { - var state = vm.stateObject[0]; - var rootStateId = dashboardUtils.getRootStateId(vm.states); - if (state.id == rootStateId && (!state.params || isEmpty(state.params))) { - return true; - } - } - return false; - } - - function isEmpty(map) { - for(var key in map) { - return !Object.prototype.hasOwnProperty.call(map, key); - } - return true; - } - -} diff --git a/ui/src/app/dashboard/states/entity-state-controller.scss b/ui/src/app/dashboard/states/entity-state-controller.scss deleted file mode 100644 index 895c108063..0000000000 --- a/ui/src/app/dashboard/states/entity-state-controller.scss +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "../../../scss/constants"; - -tb-states-component { - min-width: 0; -} - -.entity-state-controller { - .state-divider { - padding-right: 15px; - padding-left: 15px; - overflow: hidden; - font-size: 18px; - text-overflow: ellipsis; - white-space: nowrap; - pointer-events: none; - } - - .state-entry { - overflow: hidden; - font-size: 18px; - text-overflow: ellipsis; - white-space: nowrap; - outline: none; - } - - md-select { - margin: 0; - - .md-text { - font-size: 18px; - font-weight: 700; - } - } -} diff --git a/ui/src/app/dashboard/states/entity-state-controller.tpl.html b/ui/src/app/dashboard/states/entity-state-controller.tpl.html deleted file mode 100644 index 57688964e8..0000000000 --- a/ui/src/app/dashboard/states/entity-state-controller.tpl.html +++ /dev/null @@ -1,31 +0,0 @@ - -
    -
    - - {{vm.getStateName($index)}} - > - -
    - - - {{vm.getStateName($index)}} - - -
    \ No newline at end of file diff --git a/ui/src/app/dashboard/states/index.js b/ui/src/app/dashboard/states/index.js deleted file mode 100644 index 7c52a37bff..0000000000 --- a/ui/src/app/dashboard/states/index.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import ManageDashboardStatesController from './manage-dashboard-states.controller'; -import DashboardStateDialogController from './dashboard-state-dialog.controller'; -import SelectTargetStateController from './select-target-state.controller'; -import StatesComponentDirective from './states-component.directive'; -import StatesControllerService from './states-controller.service'; - -export default angular.module('thingsboard.dashboard.states', []) - .controller('ManageDashboardStatesController', ManageDashboardStatesController) - .controller('DashboardStateDialogController', DashboardStateDialogController) - .controller('SelectTargetStateController', SelectTargetStateController) - .directive('tbStatesComponent', StatesComponentDirective) - .factory('statesControllerService', StatesControllerService) - .name; diff --git a/ui/src/app/dashboard/states/manage-dashboard-states.controller.js b/ui/src/app/dashboard/states/manage-dashboard-states.controller.js deleted file mode 100644 index ea828fe649..0000000000 --- a/ui/src/app/dashboard/states/manage-dashboard-states.controller.js +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './manage-dashboard-states.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import dashboardStateDialogTemplate from './dashboard-state-dialog.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function ManageDashboardStatesController($scope, $mdDialog, $filter, $document, $translate, states) { - - var vm = this; - - vm.allStates = []; - for (var id in states) { - var state = states[id]; - state.id = id; - vm.allStates.push(state); - } - - vm.states = []; - vm.statesCount = 0; - - vm.query = { - order: 'name', - limit: 5, - page: 1, - search: null - }; - - vm.enterFilterMode = enterFilterMode; - vm.exitFilterMode = exitFilterMode; - vm.onReorder = onReorder; - vm.onPaginate = onPaginate; - vm.addState = addState; - vm.editState = editState; - vm.deleteState = deleteState; - - vm.cancel = cancel; - vm.save = save; - - $scope.$watch("vm.query.search", function(newVal, prevVal) { - if (!angular.equals(newVal, prevVal) && vm.query.search != null) { - updateStates(); - } - }); - - updateStates (); - - function updateStates () { - var result = $filter('orderBy')(vm.allStates, vm.query.order); - if (vm.query.search != null) { - result = $filter('filter')(result, {$: vm.query.search}); - } - vm.statesCount = result.length; - var startIndex = vm.query.limit * (vm.query.page - 1); - vm.states = result.slice(startIndex, startIndex + vm.query.limit); - } - - function enterFilterMode () { - vm.query.search = ''; - } - - function exitFilterMode () { - vm.query.search = null; - updateStates(); - } - - function onReorder () { - updateStates(); - } - - function onPaginate () { - updateStates(); - } - - function addState ($event) { - openStateDialog($event, null, true); - } - - function editState ($event, alertRule) { - if ($event) { - $event.stopPropagation(); - } - openStateDialog($event, alertRule, false); - } - - function openStateDialog($event, state, isAdd) { - var prevStateId = null; - if (!isAdd) { - prevStateId = state.id; - } - $mdDialog.show({ - controller: 'DashboardStateDialogController', - controllerAs: 'vm', - templateUrl: dashboardStateDialogTemplate, - parent: angular.element($document[0].body), - locals: {isAdd: isAdd, allStates: vm.allStates, state: angular.copy(state)}, - multiple: true, - fullscreen: true, - targetEvent: $event - }).then(function (state) { - saveState(state, prevStateId); - updateStates(); - }); - } - - function getStateIndex(id) { - var result = $filter('filter')(vm.allStates, {id: id}, true); - if (result && result.length) { - return vm.allStates.indexOf(result[0]); - } - return -1; - } - - function saveState(state, prevStateId) { - if (prevStateId) { - var index = getStateIndex(prevStateId); - if (index > -1) { - vm.allStates[index] = state; - } - } else { - vm.allStates.push(state); - } - if (state.root) { - for (var i=0; i < vm.allStates.length; i++) { - var otherState = vm.allStates[i]; - if (otherState.id !== state.id) { - otherState.root = false; - } - } - } - $scope.theForm.$setDirty(); - } - - function deleteState ($event, state) { - if ($event) { - $event.stopPropagation(); - } - if (state) { - var title = $translate.instant('dashboard.delete-state-title'); - var content = $translate.instant('dashboard.delete-state-text', {stateName: state.name}); - var confirm = $mdDialog.confirm() - .targetEvent($event) - .title(title) - .htmlContent(content) - .ariaLabel(title) - .cancel($translate.instant('action.no')) - .ok($translate.instant('action.yes')); - - confirm._options.multiple = true; - confirm._options.fullscreen = true; - - $mdDialog.show(confirm).then(function () { - var index = getStateIndex(state.id); - if (index > -1) { - vm.allStates.splice(index, 1); - } - $scope.theForm.$setDirty(); - updateStates(); - }); - } - } - - function cancel() { - $mdDialog.cancel(); - } - - function save() { - $scope.theForm.$setPristine(); - var savedStates = {}; - for (var i=0;i - -
    - -
    -

    {{ 'dashboard.manage-states' }}

    - - - - -
    -
    - - - -
    -
    -
    - -
    - dashboard.states - - - add - - {{ 'dashboard.add-state' | translate }} - - - - search - - {{ 'action.search' | translate }} - - -
    -
    - -
    - - search - - {{ 'dashboard.search-states' | translate }} - - - - - - - - close - - {{ 'action.close' | translate }} - - -
    -
    - - - - - - - - - - - - - - - - - - -
    dashboard.state-namedashboard.state-iddashboard.is-root-state 
    {{state.name}}{{state.id}} - - - - - edit - - {{ 'dashboard.edit-state' | translate }} - - - - delete - - {{ 'dashboard.delete-state' | translate }} - - -
    -
    - - -
    -
    -
    -
    - - - - {{ 'action.save' | translate }} - - {{ 'action.cancel' | translate }} - -
    -
    diff --git a/ui/src/app/dashboard/states/select-target-state.controller.js b/ui/src/app/dashboard/states/select-target-state.controller.js deleted file mode 100644 index 182b27560d..0000000000 --- a/ui/src/app/dashboard/states/select-target-state.controller.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/*@ngInject*/ -export default function SelectTargetStateController($scope, $mdDialog, dashboardUtils, states) { - - var vm = this; - vm.states = states; - vm.stateId = dashboardUtils.getRootStateId(vm.states); - - vm.cancel = cancel; - vm.save = save; - - function cancel() { - $mdDialog.cancel(); - } - - function save() { - $scope.theForm.$setPristine(); - $mdDialog.hide(vm.stateId); - } -} diff --git a/ui/src/app/dashboard/states/select-target-state.tpl.html b/ui/src/app/dashboard/states/select-target-state.tpl.html deleted file mode 100644 index 9e3daa4828..0000000000 --- a/ui/src/app/dashboard/states/select-target-state.tpl.html +++ /dev/null @@ -1,50 +0,0 @@ - - -
    - -
    -

    {{ 'dashboard.select-state' }}

    - - - - -
    -
    - - - -
    -
    - - - {{state.name}} - - -
    -
    -
    - - - - {{ 'action.select' | translate }} - - {{ 'action.cancel' | translate }} - -
    -
    diff --git a/ui/src/app/dashboard/states/states-component.directive.js b/ui/src/app/dashboard/states/states-component.directive.js deleted file mode 100644 index cb722d0ea6..0000000000 --- a/ui/src/app/dashboard/states/states-component.directive.js +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/*@ngInject*/ -export default function StatesComponent($compile, $templateCache, $controller, statesControllerService) { - - var linker = function (scope, element) { - - function destroyStateController() { - if (scope.statesController && angular.isFunction(scope.statesController.$onDestroy)) { - scope.statesController.$onDestroy(); - } - } - - function init() { - - var stateController = scope.dashboardCtrl.dashboardCtx.stateController; - - stateController.openState = function(id, params, openRightLayout) { - if (scope.statesController) { - scope.statesController.openState(id, params, openRightLayout); - } - } - - stateController.updateState = function(id, params, openRightLayout) { - if (scope.statesController) { - scope.statesController.updateState(id, params, openRightLayout); - } - } - - stateController.resetState = function() { - if (scope.statesController) { - scope.statesController.resetState(); - } - } - - stateController.preserveState = function() { - if (scope.statesController) { - var state = scope.statesController.getStateObject(); - statesControllerService.preserveStateControllerState(scope.statesControllerId, state); - } - } - - stateController.cleanupPreservedStates = function() { - statesControllerService.cleanupPreservedStates(); - } - - stateController.navigatePrevState = function(index) { - if (scope.statesController) { - scope.statesController.navigatePrevState(index); - } - } - - stateController.getStateId = function() { - if (scope.statesController) { - return scope.statesController.getStateId(); - } else { - return ''; - } - } - - stateController.getStateIndex = function() { - if (scope.statesController) { - return scope.statesController.getStateIndex(); - } else { - return -1; - } - } - - stateController.getStateIdAtIndex = function(index) { - if (scope.statesController) { - return scope.statesController.getStateIdAtIndex(index); - } else { - return ''; - } - } - - stateController.getStateParams = function() { - if (scope.statesController) { - return scope.statesController.getStateParams(); - } else { - return {}; - } - } - - stateController.getStateParamsByStateId = function(id) { - if (scope.statesController) { - return scope.statesController.getStateParamsByStateId(id); - } else { - return null; - } - } - - stateController.getEntityId = function(entityParamName) { - if (scope.statesController) { - return scope.statesController.getEntityId(entityParamName); - } else { - return null; - } - } - - } - - scope.$on('$destroy', function callOnDestroyHook() { - destroyStateController(); - }); - - scope.$watch('scope.dashboardCtrl', function() { - if (scope.dashboardCtrl.dashboardCtx) { - init(); - } - }) - - scope.$watch('statesControllerId', function(newValue) { - if (newValue) { - if (scope.statesController) { - destroyStateController(); - } - var statesControllerInfo = statesControllerService.getStateController(scope.statesControllerId); - if (!statesControllerInfo) { - //fallback to default - statesControllerInfo = statesControllerService.getStateController('default'); - } - var template = $templateCache.get(statesControllerInfo.templateUrl); - element.html(template); - - var preservedState = statesControllerService.withdrawStateControllerState(scope.statesControllerId); - - var locals = { - preservedState: preservedState - }; - angular.extend(locals, {$scope: scope, $element: element}); - var controller = $controller(statesControllerInfo.controller, locals, true, 'vm'); - controller.instance = controller(); - scope.statesController = controller.instance; - scope.statesController.dashboardCtrl = scope.dashboardCtrl; - scope.statesController.states = scope.states; - $compile(element.contents())(scope); - } - }); - - scope.$watch('states', function() { - if (scope.statesController) { - scope.statesController.states = scope.states; - } - }); - - } - - return { - restrict: "E", - link: linker, - scope: { - statesControllerId: '=', - dashboardCtrl: '=', - states: '=' - } - }; -} diff --git a/ui/src/app/dashboard/states/states-controller.service.js b/ui/src/app/dashboard/states/states-controller.service.js deleted file mode 100644 index 160c82a4cd..0000000000 --- a/ui/src/app/dashboard/states/states-controller.service.js +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import defaultStateControllerTemplate from './default-state-controller.tpl.html'; -import entityStateControllerTemplate from './entity-state-controller.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -import DefaultStateController from './default-state-controller'; -import EntityStateController from './entity-state-controller'; - -/*@ngInject*/ -export default function StatesControllerService() { - - var statesControllers = {}; - statesControllers['default'] = { - controller: DefaultStateController, - templateUrl: defaultStateControllerTemplate - }; - statesControllers['entity'] = { - controller: EntityStateController, - templateUrl: entityStateControllerTemplate - }; - - var service = { - registerStatesController: registerStatesController, - getStateControllers: getStateControllers, - getStateController: getStateController, - preserveStateControllerState: preserveStateControllerState, - withdrawStateControllerState: withdrawStateControllerState, - cleanupPreservedStates: cleanupPreservedStates - }; - - return service; - - function registerStatesController(id, stateControllerInfo) { - statesControllers[id] = stateControllerInfo; - } - - function getStateControllers() { - return statesControllers; - } - - function getStateController(id) { - return statesControllers[id]; - } - - function preserveStateControllerState(id, state) { - statesControllers[id].state = angular.copy(state); - } - - function withdrawStateControllerState(id) { - var state = statesControllers[id].state; - statesControllers[id].state = null; - return state; - } - - function cleanupPreservedStates() { - for (var id in statesControllers) { - statesControllers[id].state = null; - } - } - -} diff --git a/ui/src/app/device/add-device.tpl.html b/ui/src/app/device/add-device.tpl.html deleted file mode 100644 index f3b8104d1c..0000000000 --- a/ui/src/app/device/add-device.tpl.html +++ /dev/null @@ -1,45 +0,0 @@ - - -
    - -
    -

    device.add

    - -
    - - - -
    -
    - - - -
    - -
    -
    - - - - {{ 'action.add' | translate }} - - {{ 'action.cancel' | translate }} - -
    -
    \ No newline at end of file diff --git a/ui/src/app/device/add-devices-to-customer.controller.js b/ui/src/app/device/add-devices-to-customer.controller.js deleted file mode 100644 index 8212b6cae3..0000000000 --- a/ui/src/app/device/add-devices-to-customer.controller.js +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/*@ngInject*/ -export default function AddDevicesToCustomerController(deviceService, $mdDialog, $q, customerId, devices) { - - var vm = this; - - vm.devices = devices; - vm.searchText = ''; - - vm.assign = assign; - vm.cancel = cancel; - vm.hasData = hasData; - vm.noData = noData; - vm.searchDeviceTextUpdated = searchDeviceTextUpdated; - vm.toggleDeviceSelection = toggleDeviceSelection; - - vm.theDevices = { - getItemAtIndex: function (index) { - if (index > vm.devices.data.length) { - vm.theDevices.fetchMoreItems_(index); - return null; - } - var item = vm.devices.data[index]; - if (item) { - item.indexNumber = index + 1; - } - return item; - }, - - getLength: function () { - if (vm.devices.hasNext) { - return vm.devices.data.length + vm.devices.nextPageLink.limit; - } else { - return vm.devices.data.length; - } - }, - - fetchMoreItems_: function () { - if (vm.devices.hasNext && !vm.devices.pending) { - vm.devices.pending = true; - deviceService.getTenantDevices(vm.devices.nextPageLink, false).then( - function success(devices) { - vm.devices.data = vm.devices.data.concat(devices.data); - vm.devices.nextPageLink = devices.nextPageLink; - vm.devices.hasNext = devices.hasNext; - if (vm.devices.hasNext) { - vm.devices.nextPageLink.limit = vm.devices.pageSize; - } - vm.devices.pending = false; - }, - function fail() { - vm.devices.hasNext = false; - vm.devices.pending = false; - }); - } - } - }; - - function cancel () { - $mdDialog.cancel(); - } - - function assign() { - var tasks = []; - for (var deviceId in vm.devices.selections) { - tasks.push(deviceService.assignDeviceToCustomer(customerId, deviceId)); - } - $q.all(tasks).then(function () { - $mdDialog.hide(); - }); - } - - function noData() { - return vm.devices.data.length == 0 && !vm.devices.hasNext; - } - - function hasData() { - return vm.devices.data.length > 0; - } - - function toggleDeviceSelection($event, device) { - $event.stopPropagation(); - var selected = angular.isDefined(device.selected) && device.selected; - device.selected = !selected; - if (device.selected) { - vm.devices.selections[device.id.id] = true; - vm.devices.selectedCount++; - } else { - delete vm.devices.selections[device.id.id]; - vm.devices.selectedCount--; - } - } - - function searchDeviceTextUpdated() { - vm.devices = { - pageSize: vm.devices.pageSize, - data: [], - nextPageLink: { - limit: vm.devices.pageSize, - textSearch: vm.searchText - }, - selections: {}, - selectedCount: 0, - hasNext: true, - pending: false - }; - } - -} diff --git a/ui/src/app/device/add-devices-to-customer.tpl.html b/ui/src/app/device/add-devices-to-customer.tpl.html deleted file mode 100644 index 61c4dc2c3a..0000000000 --- a/ui/src/app/device/add-devices-to-customer.tpl.html +++ /dev/null @@ -1,77 +0,0 @@ - - -
    - -
    -

    device.assign-device-to-customer

    - - - - -
    -
    - - - -
    -
    - device.assign-device-to-customer-text - - - - search - - - -
    - device.no-devices-text - - - - - {{ device.name }} - - - -
    -
    -
    -
    - - - - {{ 'action.assign' | translate }} - - {{ 'action.cancel' | - translate }} - - -
    -
    \ No newline at end of file diff --git a/ui/src/app/device/assign-to-customer.controller.js b/ui/src/app/device/assign-to-customer.controller.js deleted file mode 100644 index 2fca96d29c..0000000000 --- a/ui/src/app/device/assign-to-customer.controller.js +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/*@ngInject*/ -export default function AssignDeviceToCustomerController(customerService, deviceService, $mdDialog, $q, deviceIds, customers) { - - var vm = this; - - vm.customers = customers; - vm.searchText = ''; - - vm.assign = assign; - vm.cancel = cancel; - vm.isCustomerSelected = isCustomerSelected; - vm.hasData = hasData; - vm.noData = noData; - vm.searchCustomerTextUpdated = searchCustomerTextUpdated; - vm.toggleCustomerSelection = toggleCustomerSelection; - - vm.theCustomers = { - getItemAtIndex: function (index) { - if (index > vm.customers.data.length) { - vm.theCustomers.fetchMoreItems_(index); - return null; - } - var item = vm.customers.data[index]; - if (item) { - item.indexNumber = index + 1; - } - return item; - }, - - getLength: function () { - if (vm.customers.hasNext) { - return vm.customers.data.length + vm.customers.nextPageLink.limit; - } else { - return vm.customers.data.length; - } - }, - - fetchMoreItems_: function () { - if (vm.customers.hasNext && !vm.customers.pending) { - vm.customers.pending = true; - customerService.getCustomers(vm.customers.nextPageLink).then( - function success(customers) { - vm.customers.data = vm.customers.data.concat(customers.data); - vm.customers.nextPageLink = customers.nextPageLink; - vm.customers.hasNext = customers.hasNext; - if (vm.customers.hasNext) { - vm.customers.nextPageLink.limit = vm.customers.pageSize; - } - vm.customers.pending = false; - }, - function fail() { - vm.customers.hasNext = false; - vm.customers.pending = false; - }); - } - } - }; - - function cancel() { - $mdDialog.cancel(); - } - - function assign() { - var tasks = []; - for (var i=0;i 0; - } - - function toggleCustomerSelection($event, customer) { - $event.stopPropagation(); - if (vm.isCustomerSelected(customer)) { - vm.customers.selection = null; - } else { - vm.customers.selection = customer; - } - } - - function isCustomerSelected(customer) { - return vm.customers.selection != null && customer && - customer.id.id === vm.customers.selection.id.id; - } - - function searchCustomerTextUpdated() { - vm.customers = { - pageSize: vm.customers.pageSize, - data: [], - nextPageLink: { - limit: vm.customers.pageSize, - textSearch: vm.searchText - }, - selection: null, - hasNext: true, - pending: false - }; - } -} diff --git a/ui/src/app/device/assign-to-customer.tpl.html b/ui/src/app/device/assign-to-customer.tpl.html deleted file mode 100644 index 079c1fb276..0000000000 --- a/ui/src/app/device/assign-to-customer.tpl.html +++ /dev/null @@ -1,76 +0,0 @@ - - -
    - -
    -

    device.assign-device-to-customer

    - - - - -
    -
    - - - -
    -
    - device.assign-to-customer-text - - - - search - - - -
    - customer.no-customers-text - - - - - {{ customer.title }} - - - -
    -
    -
    -
    - - - - {{ 'action.assign' | translate }} - - {{ 'action.cancel' | - translate }} - - -
    -
    \ No newline at end of file diff --git a/ui/src/app/device/device-card.tpl.html b/ui/src/app/device/device-card.tpl.html deleted file mode 100644 index d6ec4d93a8..0000000000 --- a/ui/src/app/device/device-card.tpl.html +++ /dev/null @@ -1,23 +0,0 @@ - -
    -
    {{vm.item.type}}
    -
    {{vm.item.additionalInfo.description}}
    -
    {{'device.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'
    -
    {{'device.public' | translate}}
    -
    diff --git a/ui/src/app/device/device-credentials.controller.js b/ui/src/app/device/device-credentials.controller.js deleted file mode 100644 index 6a33b026f2..0000000000 --- a/ui/src/app/device/device-credentials.controller.js +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/*@ngInject*/ -export default function ManageDeviceCredentialsController(deviceService, $scope, $mdDialog, deviceId, isReadOnly) { - - var vm = this; - - vm.credentialsTypes = [ - { - name: 'Access token', - value: 'ACCESS_TOKEN' - }, - { - name: 'X.509 Certificate', - value: 'X509_CERTIFICATE' - } - ]; - - vm.deviceCredentials = {}; - vm.isReadOnly = isReadOnly; - - vm.valid = valid; - vm.cancel = cancel; - vm.save = save; - vm.clear = clear; - - loadDeviceCredentials(); - - function loadDeviceCredentials() { - deviceService.getDeviceCredentials(deviceId).then(function success(deviceCredentials) { - vm.deviceCredentials = deviceCredentials; - }); - } - - function cancel() { - $mdDialog.cancel(); - } - - function valid() { - return vm.deviceCredentials && - (vm.deviceCredentials.credentialsType === 'ACCESS_TOKEN' - && vm.deviceCredentials.credentialsId - && vm.deviceCredentials.credentialsId.length > 0 - || vm.deviceCredentials.credentialsType === 'X509_CERTIFICATE' - && vm.deviceCredentials.credentialsValue - && vm.deviceCredentials.credentialsValue.length > 0); - } - - function clear() { - vm.deviceCredentials.credentialsId = null; - vm.deviceCredentials.credentialsValue = null; - } - - function save() { - deviceService.saveDeviceCredentials(vm.deviceCredentials).then(function success(deviceCredentials) { - vm.deviceCredentials = deviceCredentials; - $scope.theForm.$setPristine(); - $mdDialog.hide(); - }); - } -} diff --git a/ui/src/app/device/device-credentials.tpl.html b/ui/src/app/device/device-credentials.tpl.html deleted file mode 100644 index ddea7b044a..0000000000 --- a/ui/src/app/device/device-credentials.tpl.html +++ /dev/null @@ -1,71 +0,0 @@ - - -
    - -
    -

    device.device-credentials

    - - - - -
    -
    - - - -
    -
    - - - - - {{credentialsType.name}} - - - - - - -
    -
    device.access-token-required
    -
    device.access-token-invalid
    -
    -
    - - - -
    -
    device.rsa-key-required
    -
    -
    -
    -
    -
    - - - - {{ 'action.save' | translate }} - - {{ (vm.isReadOnly ? 'action.close' : 'action.cancel') | translate }} - -
    -
    \ No newline at end of file diff --git a/ui/src/app/device/device-fieldset.tpl.html b/ui/src/app/device/device-fieldset.tpl.html deleted file mode 100644 index 76b85a60e4..0000000000 --- a/ui/src/app/device/device-fieldset.tpl.html +++ /dev/null @@ -1,88 +0,0 @@ - -{{ 'device.make-public' | translate }} -{{ 'device.assign-to-customer' | translate }} -{{ isPublic ? 'device.make-private' : 'device.unassign-from-customer' | translate }} -{{ (deviceScope === 'customer_user' ? 'device.view-credentials' : 'device.manage-credentials') | translate }} -{{ 'device.delete' | translate }} - -
    - - - device.copyId - - - - device.copyAccessToken - -
    - - - - - - -
    - {{ 'device.device-public' | translate }} -
    -
    - - - -
    -
    device.name-required
    -
    -
    - - - - - - - - {{ 'device.is-gateway' | translate }} - - - - - - -
    -
    diff --git a/ui/src/app/device/device.controller.js b/ui/src/app/device/device.controller.js deleted file mode 100644 index bf0a61336b..0000000000 --- a/ui/src/app/device/device.controller.js +++ /dev/null @@ -1,584 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import addDeviceTemplate from './add-device.tpl.html'; -import deviceCard from './device-card.tpl.html'; -import assignToCustomerTemplate from './assign-to-customer.tpl.html'; -import addDevicesToCustomerTemplate from './add-devices-to-customer.tpl.html'; -import deviceCredentialsTemplate from './device-credentials.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export function DeviceCardController(types) { - - var vm = this; - - vm.types = types; - - vm.isAssignedToCustomer = function() { - if (vm.item && vm.item.customerId && vm.parentCtl.devicesScope === 'tenant' && - vm.item.customerId.id != vm.types.id.nullUid && !vm.item.assignedCustomer.isPublic) { - return true; - } - return false; - } - - vm.isPublic = function() { - if (vm.item && vm.item.assignedCustomer && vm.parentCtl.devicesScope === 'tenant' && vm.item.assignedCustomer.isPublic) { - return true; - } - return false; - } -} - - -/*@ngInject*/ -export function DeviceController($rootScope, userService, deviceService, customerService, $state, $stateParams, - $document, $mdDialog, $q, $translate, types, importExport) { - - var customerId = $stateParams.customerId; - - var deviceActionsList = []; - - var deviceGroupActionsList = []; - - var deviceAddItemActionsList = [ - { - onAction: function ($event) { - vm.grid.addItem($event); - }, - name: function() { return $translate.instant('action.add') }, - details: function() { return $translate.instant('device.add-device-text') }, - icon: "insert_drive_file" - }, - { - onAction: function ($event) { - importExport.importEntities($event, types.entityType.device).then( - function() { - vm.grid.refreshList(); - } - ); - }, - name: function() { return $translate.instant('action.import') }, - details: function() { return $translate.instant('device.import') }, - icon: "file_upload" - } - ]; - - var vm = this; - - vm.types = types; - - vm.deviceGridConfig = { - deleteItemTitleFunc: deleteDeviceTitle, - deleteItemContentFunc: deleteDeviceText, - deleteItemsTitleFunc: deleteDevicesTitle, - deleteItemsActionTitleFunc: deleteDevicesActionTitle, - deleteItemsContentFunc: deleteDevicesText, - - saveItemFunc: saveDevice, - - getItemTitleFunc: getDeviceTitle, - - itemCardController: 'DeviceCardController', - itemCardTemplateUrl: deviceCard, - parentCtl: vm, - - actionsList: deviceActionsList, - groupActionsList: deviceGroupActionsList, - addItemActions: deviceAddItemActionsList, - - onGridInited: gridInited, - - addItemTemplateUrl: addDeviceTemplate, - - noItemsText: function() { return $translate.instant('device.no-devices-text') }, - itemDetailsText: function() { return $translate.instant('device.device-details') }, - isDetailsReadOnly: isCustomerUser, - isSelectionEnabled: function () { - return !isCustomerUser(); - } - }; - - if (angular.isDefined($stateParams.items) && $stateParams.items !== null) { - vm.deviceGridConfig.items = $stateParams.items; - } - - if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) { - vm.deviceGridConfig.topIndex = $stateParams.topIndex; - } - - vm.devicesScope = $state.$current.data.devicesType; - - vm.assignToCustomer = assignToCustomer; - vm.makePublic = makePublic; - vm.unassignFromCustomer = unassignFromCustomer; - vm.manageCredentials = manageCredentials; - - initController(); - - function initController() { - var fetchDevicesFunction = null; - var deleteDeviceFunction = null; - var refreshDevicesParamsFunction = null; - - var user = userService.getCurrentUser(); - - if (user.authority === 'CUSTOMER_USER') { - vm.devicesScope = 'customer_user'; - customerId = user.customerId; - } - if (customerId) { - vm.customerDevicesTitle = $translate.instant('customer.devices'); - customerService.getShortCustomerInfo(customerId).then( - function success(info) { - if (info.isPublic) { - vm.customerDevicesTitle = $translate.instant('customer.public-devices'); - } - } - ); - } - - if (vm.devicesScope === 'tenant') { - fetchDevicesFunction = function (pageLink, deviceType) { - return deviceService.getTenantDevices(pageLink, true, null, deviceType); - }; - deleteDeviceFunction = function (deviceId) { - return deviceService.deleteDevice(deviceId); - }; - refreshDevicesParamsFunction = function() { - return {"topIndex": vm.topIndex}; - }; - - deviceActionsList.push({ - onAction: function ($event, item) { - makePublic($event, item); - }, - name: function() { return $translate.instant('action.share') }, - details: function() { return $translate.instant('device.make-public') }, - icon: "share", - isEnabled: function(device) { - return device && (!device.customerId || device.customerId.id === types.id.nullUid); - } - }); - - deviceActionsList.push( - { - onAction: function ($event, item) { - assignToCustomer($event, [ item.id.id ]); - }, - name: function() { return $translate.instant('action.assign') }, - details: function() { return $translate.instant('device.assign-to-customer') }, - icon: "assignment_ind", - isEnabled: function(device) { - return device && (!device.customerId || device.customerId.id === types.id.nullUid); - } - } - ); - - deviceActionsList.push( - { - onAction: function ($event, item) { - unassignFromCustomer($event, item, false); - }, - name: function() { return $translate.instant('action.unassign') }, - details: function() { return $translate.instant('device.unassign-from-customer') }, - icon: "assignment_return", - isEnabled: function(device) { - return device && device.customerId && device.customerId.id !== types.id.nullUid && !device.assignedCustomer.isPublic; - } - } - ); - - deviceActionsList.push({ - onAction: function ($event, item) { - unassignFromCustomer($event, item, true); - }, - name: function() { return $translate.instant('action.make-private') }, - details: function() { return $translate.instant('device.make-private') }, - icon: "reply", - isEnabled: function(device) { - return device && device.customerId && device.customerId.id !== types.id.nullUid && device.assignedCustomer.isPublic; - } - }); - - deviceActionsList.push( - { - onAction: function ($event, item) { - manageCredentials($event, item); - }, - name: function() { return $translate.instant('device.credentials') }, - details: function() { return $translate.instant('device.manage-credentials') }, - icon: "security" - } - ); - - deviceActionsList.push( - { - onAction: function ($event, item) { - vm.grid.deleteItem($event, item); - }, - name: function() { return $translate.instant('action.delete') }, - details: function() { return $translate.instant('device.delete') }, - icon: "delete" - } - ); - - deviceGroupActionsList.push( - { - onAction: function ($event, items) { - assignDevicesToCustomer($event, items); - }, - name: function() { return $translate.instant('device.assign-devices') }, - details: function(selectedCount) { - return $translate.instant('device.assign-devices-text', {count: selectedCount}, "messageformat"); - }, - icon: "assignment_ind" - } - ); - - deviceGroupActionsList.push( - { - onAction: function ($event) { - vm.grid.deleteItems($event); - }, - name: function() { return $translate.instant('device.delete-devices') }, - details: deleteDevicesActionTitle, - icon: "delete" - } - ); - - - - } else if (vm.devicesScope === 'customer' || vm.devicesScope === 'customer_user') { - fetchDevicesFunction = function (pageLink, deviceType) { - return deviceService.getCustomerDevices(customerId, pageLink, true, null, deviceType); - }; - deleteDeviceFunction = function (deviceId) { - return deviceService.unassignDeviceFromCustomer(deviceId); - }; - refreshDevicesParamsFunction = function () { - return {"customerId": customerId, "topIndex": vm.topIndex}; - }; - - if (vm.devicesScope === 'customer') { - deviceActionsList.push( - { - onAction: function ($event, item) { - unassignFromCustomer($event, item, false); - }, - name: function() { return $translate.instant('action.unassign') }, - details: function() { return $translate.instant('device.unassign-from-customer') }, - icon: "assignment_return", - isEnabled: function(device) { - return device && !device.assignedCustomer.isPublic; - } - } - ); - deviceActionsList.push( - { - onAction: function ($event, item) { - unassignFromCustomer($event, item, true); - }, - name: function() { return $translate.instant('action.make-private') }, - details: function() { return $translate.instant('device.make-private') }, - icon: "reply", - isEnabled: function(device) { - return device && device.assignedCustomer.isPublic; - } - } - ); - - deviceActionsList.push( - { - onAction: function ($event, item) { - manageCredentials($event, item); - }, - name: function() { return $translate.instant('device.credentials') }, - details: function() { return $translate.instant('device.manage-credentials') }, - icon: "security" - } - ); - - deviceGroupActionsList.push( - { - onAction: function ($event, items) { - unassignDevicesFromCustomer($event, items); - }, - name: function() { return $translate.instant('device.unassign-devices') }, - details: function(selectedCount) { - return $translate.instant('device.unassign-devices-action-title', {count: selectedCount}, "messageformat"); - }, - icon: "assignment_return" - } - ); - - vm.deviceGridConfig.addItemAction = { - onAction: function ($event) { - addDevicesToCustomer($event); - }, - name: function() { return $translate.instant('device.assign-devices') }, - details: function() { return $translate.instant('device.assign-new-device') }, - icon: "add" - }; - - } else if (vm.devicesScope === 'customer_user') { - deviceActionsList.push( - { - onAction: function ($event, item) { - manageCredentials($event, item); - }, - name: function() { return $translate.instant('device.credentials') }, - details: function() { return $translate.instant('device.view-credentials') }, - icon: "security" - } - ); - - vm.deviceGridConfig.addItemAction = {}; - } - vm.deviceGridConfig.addItemActions = []; - - } - - vm.deviceGridConfig.refreshParamsFunc = refreshDevicesParamsFunction; - vm.deviceGridConfig.fetchItemsFunc = fetchDevicesFunction; - vm.deviceGridConfig.deleteItemFunc = deleteDeviceFunction; - - } - - function deleteDeviceTitle(device) { - return $translate.instant('device.delete-device-title', {deviceName: device.name}); - } - - function deleteDeviceText() { - return $translate.instant('device.delete-device-text'); - } - - function deleteDevicesTitle(selectedCount) { - return $translate.instant('device.delete-devices-title', {count: selectedCount}, 'messageformat'); - } - - function deleteDevicesActionTitle(selectedCount) { - return $translate.instant('device.delete-devices-action-title', {count: selectedCount}, 'messageformat'); - } - - function deleteDevicesText () { - return $translate.instant('device.delete-devices-text'); - } - - function gridInited(grid) { - vm.grid = grid; - } - - function getDeviceTitle(device) { - return device ? device.name : ''; - } - - function saveDevice(device) { - var deferred = $q.defer(); - deviceService.saveDevice(device).then( - function success(savedDevice) { - $rootScope.$broadcast('deviceSaved'); - var devices = [ savedDevice ]; - customerService.applyAssignedCustomersInfo(devices).then( - function success(items) { - if (items && items.length == 1) { - deferred.resolve(items[0]); - } else { - deferred.reject(); - } - }, - function fail() { - deferred.reject(); - } - ); - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - - function isCustomerUser() { - return vm.devicesScope === 'customer_user'; - } - - function assignToCustomer($event, deviceIds) { - if ($event) { - $event.stopPropagation(); - } - var pageSize = 10; - customerService.getCustomers({limit: pageSize, textSearch: ''}).then( - function success(_customers) { - var customers = { - pageSize: pageSize, - data: _customers.data, - nextPageLink: _customers.nextPageLink, - selection: null, - hasNext: _customers.hasNext, - pending: false - }; - if (customers.hasNext) { - customers.nextPageLink.limit = pageSize; - } - $mdDialog.show({ - controller: 'AssignDeviceToCustomerController', - controllerAs: 'vm', - templateUrl: assignToCustomerTemplate, - locals: {deviceIds: deviceIds, customers: customers}, - parent: angular.element($document[0].body), - fullscreen: true, - targetEvent: $event - }).then(function () { - vm.grid.refreshList(); - }, function () { - }); - }, - function fail() { - }); - } - - function addDevicesToCustomer($event) { - if ($event) { - $event.stopPropagation(); - } - var pageSize = 10; - deviceService.getTenantDevices({limit: pageSize, textSearch: ''}, false).then( - function success(_devices) { - var devices = { - pageSize: pageSize, - data: _devices.data, - nextPageLink: _devices.nextPageLink, - selections: {}, - selectedCount: 0, - hasNext: _devices.hasNext, - pending: false - }; - if (devices.hasNext) { - devices.nextPageLink.limit = pageSize; - } - $mdDialog.show({ - controller: 'AddDevicesToCustomerController', - controllerAs: 'vm', - templateUrl: addDevicesToCustomerTemplate, - locals: {customerId: customerId, devices: devices}, - parent: angular.element($document[0].body), - fullscreen: true, - targetEvent: $event - }).then(function () { - vm.grid.refreshList(); - }, function () { - }); - }, - function fail() { - }); - } - - function assignDevicesToCustomer($event, items) { - var deviceIds = []; - for (var id in items.selections) { - deviceIds.push(id); - } - assignToCustomer($event, deviceIds); - } - - function unassignFromCustomer($event, device, isPublic) { - if ($event) { - $event.stopPropagation(); - } - var title; - var content; - var label; - if (isPublic) { - title = $translate.instant('device.make-private-device-title', {deviceName: device.name}); - content = $translate.instant('device.make-private-device-text'); - label = $translate.instant('device.make-private'); - } else { - title = $translate.instant('device.unassign-device-title', {deviceName: device.name}); - content = $translate.instant('device.unassign-device-text'); - label = $translate.instant('device.unassign-device'); - } - var confirm = $mdDialog.confirm() - .targetEvent($event) - .title(title) - .htmlContent(content) - .ariaLabel(label) - .cancel($translate.instant('action.no')) - .ok($translate.instant('action.yes')); - $mdDialog.show(confirm).then(function () { - deviceService.unassignDeviceFromCustomer(device.id.id).then(function success() { - vm.grid.refreshList(); - }); - }); - } - - function unassignDevicesFromCustomer($event, items) { - var confirm = $mdDialog.confirm() - .targetEvent($event) - .title($translate.instant('device.unassign-devices-title', {count: items.selectedCount}, 'messageformat')) - .htmlContent($translate.instant('device.unassign-devices-text')) - .ariaLabel($translate.instant('device.unassign-device')) - .cancel($translate.instant('action.no')) - .ok($translate.instant('action.yes')); - $mdDialog.show(confirm).then(function () { - var tasks = []; - for (var id in items.selections) { - tasks.push(deviceService.unassignDeviceFromCustomer(id)); - } - $q.all(tasks).then(function () { - vm.grid.refreshList(); - }); - }); - } - - function makePublic($event, device) { - if ($event) { - $event.stopPropagation(); - } - var confirm = $mdDialog.confirm() - .targetEvent($event) - .title($translate.instant('device.make-public-device-title', {deviceName: device.name})) - .htmlContent($translate.instant('device.make-public-device-text')) - .ariaLabel($translate.instant('device.make-public')) - .cancel($translate.instant('action.no')) - .ok($translate.instant('action.yes')); - $mdDialog.show(confirm).then(function () { - deviceService.makeDevicePublic(device.id.id).then(function success() { - vm.grid.refreshList(); - }); - }); - } - - function manageCredentials($event, device) { - if ($event) { - $event.stopPropagation(); - } - $mdDialog.show({ - controller: 'ManageDeviceCredentialsController', - controllerAs: 'vm', - templateUrl: deviceCredentialsTemplate, - locals: {deviceId: device.id.id, isReadOnly: isCustomerUser()}, - parent: angular.element($document[0].body), - fullscreen: true, - targetEvent: $event - }).then(function () { - }, function () { - }); - } -} diff --git a/ui/src/app/device/device.directive.js b/ui/src/app/device/device.directive.js deleted file mode 100644 index 03874e5ca5..0000000000 --- a/ui/src/app/device/device.directive.js +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import deviceFieldsetTemplate from './device-fieldset.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function DeviceDirective($compile, $templateCache, toast, $translate, types, clipboardService, deviceService, customerService) { - var linker = function (scope, element) { - var template = $templateCache.get(deviceFieldsetTemplate); - element.html(template); - - scope.types = types; - scope.isAssignedToCustomer = false; - scope.isPublic = false; - scope.assignedCustomer = null; - - scope.$watch('device', function(newVal) { - if (newVal) { - if (scope.device.customerId && scope.device.customerId.id !== types.id.nullUid) { - scope.isAssignedToCustomer = true; - customerService.getShortCustomerInfo(scope.device.customerId.id).then( - function success(customer) { - scope.assignedCustomer = customer; - scope.isPublic = customer.isPublic; - } - ); - } else { - scope.isAssignedToCustomer = false; - scope.isPublic = false; - scope.assignedCustomer = null; - } - } - }); - - scope.onDeviceIdCopied = function() { - toast.showSuccess($translate.instant('device.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left'); - }; - - scope.copyAccessToken = function(e) { - const trigger = e.delegateTarget || e.currentTarget; - if (scope.device.id) { - deviceService.getDeviceCredentials(scope.device.id.id, true).then( - function success(credentials) { - var credentialsId = credentials.credentialsId; - clipboardService.copyToClipboard(trigger, credentialsId).then( - () => { - toast.showSuccess($translate.instant('device.accessTokenCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left'); - } - ); - } - ); - } - }; - - $compile(element.contents())(scope); - } - return { - restrict: "E", - link: linker, - scope: { - device: '=', - isEdit: '=', - deviceScope: '=', - theForm: '=', - onAssignToCustomer: '&', - onMakePublic: '&', - onUnassignFromCustomer: '&', - onManageCredentials: '&', - onDeleteDevice: '&' - } - }; -} diff --git a/ui/src/app/device/device.routes.js b/ui/src/app/device/device.routes.js deleted file mode 100644 index da7dbb89dc..0000000000 --- a/ui/src/app/device/device.routes.js +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import devicesTemplate from './devices.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function DeviceRoutes($stateProvider, types) { - $stateProvider - .state('home.devices', { - url: '/devices', - params: {'topIndex': 0}, - module: 'private', - auth: ['TENANT_ADMIN', 'CUSTOMER_USER'], - views: { - "content@home": { - templateUrl: devicesTemplate, - controller: 'DeviceController', - controllerAs: 'vm' - } - }, - data: { - devicesType: 'tenant', - searchEnabled: true, - searchByEntitySubtype: true, - searchEntityType: types.entityType.device, - pageTitle: 'device.devices' - }, - ncyBreadcrumb: { - label: '{"icon": "devices_other", "label": "device.devices"}' - } - }) - .state('home.customers.devices', { - url: '/:customerId/devices', - params: {'topIndex': 0}, - module: 'private', - auth: ['TENANT_ADMIN'], - views: { - "content@home": { - templateUrl: devicesTemplate, - controllerAs: 'vm', - controller: 'DeviceController' - } - }, - data: { - devicesType: 'customer', - searchEnabled: true, - searchByEntitySubtype: true, - searchEntityType: types.entityType.device, - pageTitle: 'customer.devices' - }, - ncyBreadcrumb: { - label: '{"icon": "devices_other", "label": "{{ vm.customerDevicesTitle }}", "translate": "false"}' - } - }); - -} diff --git a/ui/src/app/device/devices.tpl.html b/ui/src/app/device/devices.tpl.html deleted file mode 100644 index 14b4f90df3..0000000000 --- a/ui/src/app/device/devices.tpl.html +++ /dev/null @@ -1,83 +0,0 @@ - - - -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    diff --git a/ui/src/app/device/index.js b/ui/src/app/device/index.js deleted file mode 100644 index 35b721c9f5..0000000000 --- a/ui/src/app/device/index.js +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import uiRouter from 'angular-ui-router'; -import thingsboardGrid from '../components/grid.directive'; -import thingsboardApiUser from '../api/user.service'; -import thingsboardApiDevice from '../api/device.service'; -import thingsboardApiCustomer from '../api/customer.service'; - -import DeviceRoutes from './device.routes'; -import {DeviceController, DeviceCardController} from './device.controller'; -import AssignDeviceToCustomerController from './assign-to-customer.controller'; -import AddDevicesToCustomerController from './add-devices-to-customer.controller'; -import ManageDeviceCredentialsController from './device-credentials.controller'; -import DeviceDirective from './device.directive'; - -export default angular.module('thingsboard.device', [ - uiRouter, - thingsboardGrid, - thingsboardApiUser, - thingsboardApiDevice, - thingsboardApiCustomer -]) - .config(DeviceRoutes) - .controller('DeviceController', DeviceController) - .controller('DeviceCardController', DeviceCardController) - .controller('AssignDeviceToCustomerController', AssignDeviceToCustomerController) - .controller('AddDevicesToCustomerController', AddDevicesToCustomerController) - .controller('ManageDeviceCredentialsController', ManageDeviceCredentialsController) - .directive('tbDevice', DeviceDirective) - .name; diff --git a/ui/src/app/entity-view/add-entity-view.tpl.html b/ui/src/app/entity-view/add-entity-view.tpl.html deleted file mode 100644 index 9372f506bb..0000000000 --- a/ui/src/app/entity-view/add-entity-view.tpl.html +++ /dev/null @@ -1,45 +0,0 @@ - - -
    - -
    -

    entity-view.add

    - -
    - - - -
    -
    - - - -
    - -
    -
    - - - - {{ 'action.add' | translate }} - - {{ 'action.cancel' | translate }} - -
    -
    \ No newline at end of file diff --git a/ui/src/app/entity-view/add-entity-views-to-customer.controller.js b/ui/src/app/entity-view/add-entity-views-to-customer.controller.js deleted file mode 100644 index 540e82c8f4..0000000000 --- a/ui/src/app/entity-view/add-entity-views-to-customer.controller.js +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/*@ngInject*/ -export default function AddEntityViewsToCustomerController(entityViewService, $mdDialog, $q, customerId, entityViews) { - - var vm = this; - - vm.entityViews = entityViews; - vm.searchText = ''; - - vm.assign = assign; - vm.cancel = cancel; - vm.hasData = hasData; - vm.noData = noData; - vm.searchEntityViewTextUpdated = searchEntityViewTextUpdated; - vm.toggleEntityViewSelection = toggleEntityViewSelection; - - vm.theEntityViews = { - getItemAtIndex: function (index) { - if (index > vm.entityViews.data.length) { - vm.theEntityViews.fetchMoreItems_(index); - return null; - } - var item = vm.entityViews.data[index]; - if (item) { - item.indexNumber = index + 1; - } - return item; - }, - - getLength: function () { - if (vm.entityViews.hasNext) { - return vm.entityViews.data.length + vm.entityViews.nextPageLink.limit; - } else { - return vm.entityViews.data.length; - } - }, - - fetchMoreItems_: function () { - if (vm.entityViews.hasNext && !vm.entityViews.pending) { - vm.entityViews.pending = true; - entityViewService.getTenantEntityViews(vm.entityViews.nextPageLink, false).then( - function success(entityViews) { - vm.entityViews.data = vm.entityViews.data.concat(entityViews.data); - vm.entityViews.nextPageLink = entityViews.nextPageLink; - vm.entityViews.hasNext = entityViews.hasNext; - if (vm.entityViews.hasNext) { - vm.entityViews.nextPageLink.limit = vm.entityViews.pageSize; - } - vm.entityViews.pending = false; - }, - function fail() { - vm.entityViews.hasNext = false; - vm.entityViews.pending = false; - }); - } - } - }; - - function cancel () { - $mdDialog.cancel(); - } - - function assign() { - var tasks = []; - for (var entityViewId in vm.entityViews.selections) { - tasks.push(entityViewService.assignEntityViewToCustomer(customerId, entityViewId)); - } - $q.all(tasks).then(function () { - $mdDialog.hide(); - }); - } - - function noData() { - return vm.entityViews.data.length == 0 && !vm.entityViews.hasNext; - } - - function hasData() { - return vm.entityViews.data.length > 0; - } - - function toggleEntityViewSelection($event, entityView) { - $event.stopPropagation(); - var selected = angular.isDefined(entityView.selected) && entityView.selected; - entityView.selected = !selected; - if (entityView.selected) { - vm.entityViews.selections[entityView.id.id] = true; - vm.entityViews.selectedCount++; - } else { - delete vm.entityViews.selections[entityView.id.id]; - vm.entityViews.selectedCount--; - } - } - - function searchEntityViewTextUpdated() { - vm.entityViews = { - pageSize: vm.entityViews.pageSize, - data: [], - nextPageLink: { - limit: vm.entityViews.pageSize, - textSearch: vm.searchText - }, - selections: {}, - selectedCount: 0, - hasNext: true, - pending: false - }; - } - -} diff --git a/ui/src/app/entity-view/add-entity-views-to-customer.tpl.html b/ui/src/app/entity-view/add-entity-views-to-customer.tpl.html deleted file mode 100644 index ad3712d0f0..0000000000 --- a/ui/src/app/entity-view/add-entity-views-to-customer.tpl.html +++ /dev/null @@ -1,77 +0,0 @@ - - -
    - -
    -

    entity-view.assign-entity-view-to-customer

    - - - - -
    -
    - - - -
    -
    - entity-view.assign-entity-view-to-customer-text - - - - search - - - -
    - entity-view.no-entity-views-text - - - - - {{ entityView.name }} - - - -
    -
    -
    -
    - - - - {{ 'action.assign' | translate }} - - {{ 'action.cancel' | - translate }} - - -
    -
    \ No newline at end of file diff --git a/ui/src/app/entity-view/assign-to-customer.controller.js b/ui/src/app/entity-view/assign-to-customer.controller.js deleted file mode 100644 index 51399d8e5e..0000000000 --- a/ui/src/app/entity-view/assign-to-customer.controller.js +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/*@ngInject*/ -export default function AssignEntityViewToCustomerController(customerService, entityViewService, $mdDialog, $q, entityViewIds, customers) { - - var vm = this; - - vm.customers = customers; - vm.searchText = ''; - - vm.assign = assign; - vm.cancel = cancel; - vm.isCustomerSelected = isCustomerSelected; - vm.hasData = hasData; - vm.noData = noData; - vm.searchCustomerTextUpdated = searchCustomerTextUpdated; - vm.toggleCustomerSelection = toggleCustomerSelection; - - vm.theCustomers = { - getItemAtIndex: function (index) { - if (index > vm.customers.data.length) { - vm.theCustomers.fetchMoreItems_(index); - return null; - } - var item = vm.customers.data[index]; - if (item) { - item.indexNumber = index + 1; - } - return item; - }, - - getLength: function () { - if (vm.customers.hasNext) { - return vm.customers.data.length + vm.customers.nextPageLink.limit; - } else { - return vm.customers.data.length; - } - }, - - fetchMoreItems_: function () { - if (vm.customers.hasNext && !vm.customers.pending) { - vm.customers.pending = true; - customerService.getCustomers(vm.customers.nextPageLink).then( - function success(customers) { - vm.customers.data = vm.customers.data.concat(customers.data); - vm.customers.nextPageLink = customers.nextPageLink; - vm.customers.hasNext = customers.hasNext; - if (vm.customers.hasNext) { - vm.customers.nextPageLink.limit = vm.customers.pageSize; - } - vm.customers.pending = false; - }, - function fail() { - vm.customers.hasNext = false; - vm.customers.pending = false; - }); - } - } - }; - - function cancel() { - $mdDialog.cancel(); - } - - function assign() { - var tasks = []; - for (var i=0; i < entityViewIds.length;i++) { - tasks.push(entityViewService.assignEntityViewToCustomer(vm.customers.selection.id.id, entityViewIds[i])); - } - $q.all(tasks).then(function () { - $mdDialog.hide(); - }); - } - - function noData() { - return vm.customers.data.length == 0 && !vm.customers.hasNext; - } - - function hasData() { - return vm.customers.data.length > 0; - } - - function toggleCustomerSelection($event, customer) { - $event.stopPropagation(); - if (vm.isCustomerSelected(customer)) { - vm.customers.selection = null; - } else { - vm.customers.selection = customer; - } - } - - function isCustomerSelected(customer) { - return vm.customers.selection != null && customer && - customer.id.id === vm.customers.selection.id.id; - } - - function searchCustomerTextUpdated() { - vm.customers = { - pageSize: vm.customers.pageSize, - data: [], - nextPageLink: { - limit: vm.customers.pageSize, - textSearch: vm.searchText - }, - selection: null, - hasNext: true, - pending: false - }; - } -} diff --git a/ui/src/app/entity-view/assign-to-customer.tpl.html b/ui/src/app/entity-view/assign-to-customer.tpl.html deleted file mode 100644 index ac8e2b05af..0000000000 --- a/ui/src/app/entity-view/assign-to-customer.tpl.html +++ /dev/null @@ -1,76 +0,0 @@ - - -
    - -
    -

    entity-view.assign-entity-view-to-customer

    - - - - -
    -
    - - - -
    -
    - entity-view.assign-to-customer-text - - - - search - - - -
    - customer.no-customers-text - - - - - {{ customer.title }} - - - -
    -
    -
    -
    - - - - {{ 'action.assign' | translate }} - - {{ 'action.cancel' | - translate }} - - -
    -
    \ No newline at end of file diff --git a/ui/src/app/entity-view/entity-view-card.tpl.html b/ui/src/app/entity-view/entity-view-card.tpl.html deleted file mode 100644 index b941a06c8e..0000000000 --- a/ui/src/app/entity-view/entity-view-card.tpl.html +++ /dev/null @@ -1,22 +0,0 @@ - -
    -
    {{vm.item.type}}
    -
    {{vm.item.additionalInfo.description}}
    -
    {{'entity-view.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'
    -
    diff --git a/ui/src/app/entity-view/entity-view-fieldset.tpl.html b/ui/src/app/entity-view/entity-view-fieldset.tpl.html deleted file mode 100644 index 6e6f5dc2cb..0000000000 --- a/ui/src/app/entity-view/entity-view-fieldset.tpl.html +++ /dev/null @@ -1,212 +0,0 @@ - -{{ 'entity-view.make-public' | translate }} -{{ 'entity-view.assign-to-customer' | translate }} -{{'entity-view.unassign-from-customer' | translate }} -{{ 'entity-view.delete' | translate }} - -
    - - - entity-view.copyId - -
    - - - - - - -
    - - - -
    -
    entity-view.name-required
    -
    -
    - - -
    - - - -
    - - - -
    {{ 'entity-view.attributes-propagation' | translate }}
    - - -
    - - -
    {{ 'entity-view.attributes-propagation' | translate }}
    - - -
    - -
    entity-view.attributes-propagation-hint
    - - - - {{item}} - - - - - - {{item}} - - - - - - {{item}} - - -
    -
    -
    - - -
    {{ 'entity-view.timeseries-data' | translate }}
    - - -
    - - -
    {{ 'entity-view.timeseries-data' | translate }}
    - - -
    - -
    entity-view.timeseries-data-hint
    - - - - {{item}} - - -
    -
    -
    -
    -
    - - -
    -
    - - -
    - - - - -
    -
    diff --git a/ui/src/app/entity-view/entity-view.controller.js b/ui/src/app/entity-view/entity-view.controller.js deleted file mode 100644 index 58ccf8ff14..0000000000 --- a/ui/src/app/entity-view/entity-view.controller.js +++ /dev/null @@ -1,495 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import addEntityViewTemplate from './add-entity-view.tpl.html'; -import entityViewCard from './entity-view-card.tpl.html'; -import assignToCustomerTemplate from './assign-to-customer.tpl.html'; -import addEntityViewsToCustomerTemplate from './add-entity-views-to-customer.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export function EntityViewCardController(types) { - - var vm = this; - - vm.types = types; - - vm.isAssignedToCustomer = function() { - if (vm.item && vm.item.customerId && vm.parentCtl.entityViewsScope === 'tenant' && - vm.item.customerId.id != vm.types.id.nullUid && !vm.item.assignedCustomer.isPublic) { - return true; - } - return false; - } - - vm.isPublic = function() { - if (vm.item && vm.item.assignedCustomer && vm.parentCtl.entityViewsScope === 'tenant' && vm.item.assignedCustomer.isPublic) { - return true; - } - return false; - } -} - - -/*@ngInject*/ -export function EntityViewController($rootScope, userService, entityViewService, customerService, $state, $stateParams, - $document, $mdDialog, $q, $translate, types) { - - var customerId = $stateParams.customerId; - - var entityViewActionsList = []; - - var entityViewGroupActionsList = []; - - var vm = this; - - vm.types = types; - - vm.entityViewGridConfig = { - deleteItemTitleFunc: deleteEntityViewTitle, - deleteItemContentFunc: deleteEntityViewText, - deleteItemsTitleFunc: deleteEntityViewsTitle, - deleteItemsActionTitleFunc: deleteEntityViewsActionTitle, - deleteItemsContentFunc: deleteEntityViewsText, - - saveItemFunc: saveEntityView, - - getItemTitleFunc: getEntityViewTitle, - - itemCardController: 'EntityViewCardController', - itemCardTemplateUrl: entityViewCard, - parentCtl: vm, - - actionsList: entityViewActionsList, - groupActionsList: entityViewGroupActionsList, - - onGridInited: gridInited, - - addItemTemplateUrl: addEntityViewTemplate, - - addItemText: function() { return $translate.instant('entity-view.add-entity-view-text') }, - noItemsText: function() { return $translate.instant('entity-view.no-entity-views-text') }, - itemDetailsText: function() { return $translate.instant('entity-view.entity-view-details') }, - isDetailsReadOnly: isCustomerUser, - isSelectionEnabled: function () { - return !isCustomerUser(); - } - }; - - if (angular.isDefined($stateParams.items) && $stateParams.items !== null) { - vm.entityViewGridConfig.items = $stateParams.items; - } - - if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) { - vm.entityViewGridConfig.topIndex = $stateParams.topIndex; - } - - vm.entityViewsScope = $state.$current.data.entityViewsType; - - vm.assignToCustomer = assignToCustomer; - vm.makePublic = makePublic; - vm.unassignFromCustomer = unassignFromCustomer; - - initController(); - - function initController() { - var fetchEntityViewsFunction = null; - var deleteEntityViewFunction = null; - var refreshEntityViewsParamsFunction = null; - - var user = userService.getCurrentUser(); - - if (user.authority === 'CUSTOMER_USER') { - vm.entityViewsScope = 'customer_user'; - customerId = user.customerId; - } - if (customerId) { - vm.customerEntityViewsTitle = $translate.instant('customer.entity-views'); - customerService.getShortCustomerInfo(customerId).then( - function success(info) { - if (info.isPublic) { - vm.customerEntityViewsTitle = $translate.instant('customer.public-entity-views'); - } - } - ); - } - - if (vm.entityViewsScope === 'tenant') { - fetchEntityViewsFunction = function (pageLink, entityViewType) { - return entityViewService.getTenantEntityViews(pageLink, true, null, entityViewType); - }; - deleteEntityViewFunction = function (entityViewId) { - return entityViewService.deleteEntityView(entityViewId); - }; - refreshEntityViewsParamsFunction = function() { - return {"topIndex": vm.topIndex}; - }; - - entityViewActionsList.push({ - onAction: function ($event, item) { - makePublic($event, item); - }, - name: function() { return $translate.instant('action.share') }, - details: function() { return $translate.instant('entity-view.make-public') }, - icon: "share", - isEnabled: function(entityView) { - return entityView && (!entityView.customerId || entityView.customerId.id === types.id.nullUid); - } - }); - - entityViewActionsList.push( - { - onAction: function ($event, item) { - assignToCustomer($event, [ item.id.id ]); - }, - name: function() { return $translate.instant('action.assign') }, - details: function() { return $translate.instant('entity-view.assign-to-customer') }, - icon: "assignment_ind", - isEnabled: function(entityView) { - return entityView && (!entityView.customerId || entityView.customerId.id === types.id.nullUid); - } - } - ); - - entityViewActionsList.push( - { - onAction: function ($event, item) { - unassignFromCustomer($event, item, false); - }, - name: function() { return $translate.instant('action.unassign') }, - details: function() { return $translate.instant('entity-view.unassign-from-customer') }, - icon: "assignment_return", - isEnabled: function(entityView) { - return entityView && entityView.customerId && entityView.customerId.id !== types.id.nullUid && !entityView.assignedCustomer.isPublic; - } - } - ); - - entityViewActionsList.push({ - onAction: function ($event, item) { - unassignFromCustomer($event, item, true); - }, - name: function() { return $translate.instant('action.make-private') }, - details: function() { return $translate.instant('entity-view.make-private') }, - icon: "reply", - isEnabled: function(entityView) { - return entityView && entityView.customerId && entityView.customerId.id !== types.id.nullUid && entityView.assignedCustomer.isPublic; - } - }); - - entityViewActionsList.push( - { - onAction: function ($event, item) { - vm.grid.deleteItem($event, item); - }, - name: function() { return $translate.instant('action.delete') }, - details: function() { return $translate.instant('entity-view.delete') }, - icon: "delete" - } - ); - - entityViewGroupActionsList.push( - { - onAction: function ($event, items) { - assignEntityViewsToCustomer($event, items); - }, - name: function() { return $translate.instant('entity-view.assign-entity-views') }, - details: function(selectedCount) { - return $translate.instant('entity-view.assign-entity-views-text', {count: selectedCount}, "messageformat"); - }, - icon: "assignment_ind" - } - ); - - entityViewGroupActionsList.push( - { - onAction: function ($event) { - vm.grid.deleteItems($event); - }, - name: function() { return $translate.instant('entity-view.delete-entity-views') }, - details: deleteEntityViewsActionTitle, - icon: "delete" - } - ); - - - - } else if (vm.entityViewsScope === 'customer' || vm.entityViewsScope === 'customer_user') { - fetchEntityViewsFunction = function (pageLink, entityViewType) { - return entityViewService.getCustomerEntityViews(customerId, pageLink, true, null, entityViewType); - }; - deleteEntityViewFunction = function (entityViewId) { - return entityViewService.unassignEntityViewFromCustomer(entityViewId); - }; - refreshEntityViewsParamsFunction = function () { - return {"customerId": customerId, "topIndex": vm.topIndex}; - }; - - if (vm.entityViewsScope === 'customer') { - entityViewActionsList.push( - { - onAction: function ($event, item) { - unassignFromCustomer($event, item, false); - }, - name: function() { return $translate.instant('action.unassign') }, - details: function() { return $translate.instant('entity-view.unassign-from-customer') }, - icon: "assignment_return", - isEnabled: function(entityView) { - return entityView && !entityView.assignedCustomer.isPublic; - } - } - ); - - entityViewGroupActionsList.push( - { - onAction: function ($event, items) { - unassignEntityViewsFromCustomer($event, items); - }, - name: function() { return $translate.instant('entity-view.unassign-entity-views') }, - details: function(selectedCount) { - return $translate.instant('entity-view.unassign-entity-views-action-title', {count: selectedCount}, "messageformat"); - }, - icon: "assignment_return" - } - ); - - vm.entityViewGridConfig.addItemAction = { - onAction: function ($event) { - addEntityViewsToCustomer($event); - }, - name: function() { return $translate.instant('entity-view.assign-entity-views') }, - details: function() { return $translate.instant('entity-view.assign-new-entity-view') }, - icon: "add" - }; - - - } else if (vm.entityViewsScope === 'customer_user') { - vm.entityViewGridConfig.addItemAction = {}; - } - } - - vm.entityViewGridConfig.refreshParamsFunc = refreshEntityViewsParamsFunction; - vm.entityViewGridConfig.fetchItemsFunc = fetchEntityViewsFunction; - vm.entityViewGridConfig.deleteItemFunc = deleteEntityViewFunction; - - } - - function deleteEntityViewTitle(entityView) { - return $translate.instant('entity-view.delete-entity-view-title', {entityViewName: entityView.name}); - } - - function deleteEntityViewText() { - return $translate.instant('entity-view.delete-entity-view-text'); - } - - function deleteEntityViewsTitle(selectedCount) { - return $translate.instant('entity-view.delete-entity-views-title', {count: selectedCount}, 'messageformat'); - } - - function deleteEntityViewsActionTitle(selectedCount) { - return $translate.instant('entity-view.delete-entity-views-action-title', {count: selectedCount}, 'messageformat'); - } - - function deleteEntityViewsText () { - return $translate.instant('entity-view.delete-entity-views-text'); - } - - function gridInited(grid) { - vm.grid = grid; - } - - function getEntityViewTitle(entityView) { - return entityView ? entityView.name : ''; - } - - function saveEntityView(entityView) { - var deferred = $q.defer(); - entityViewService.saveEntityView(entityView).then( - function success(savedEntityView) { - $rootScope.$broadcast('entityViewSaved'); - var entityViews = [ savedEntityView ]; - customerService.applyAssignedCustomersInfo(entityViews).then( - function success(items) { - if (items && items.length == 1) { - deferred.resolve(items[0]); - } else { - deferred.reject(); - } - }, - function fail() { - deferred.reject(); - } - ); - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - - function isCustomerUser() { - return vm.entityViewsScope === 'customer_user'; - } - - function assignToCustomer($event, entityViewIds) { - if ($event) { - $event.stopPropagation(); - } - var pageSize = 10; - customerService.getCustomers({limit: pageSize, textSearch: ''}).then( - function success(_customers) { - var customers = { - pageSize: pageSize, - data: _customers.data, - nextPageLink: _customers.nextPageLink, - selection: null, - hasNext: _customers.hasNext, - pending: false - }; - if (customers.hasNext) { - customers.nextPageLink.limit = pageSize; - } - $mdDialog.show({ - controller: 'AssignEntityViewToCustomerController', - controllerAs: 'vm', - templateUrl: assignToCustomerTemplate, - locals: {entityViewIds: entityViewIds, customers: customers}, - parent: angular.element($document[0].body), - fullscreen: true, - targetEvent: $event - }).then(function () { - vm.grid.refreshList(); - }, function () { - }); - }, - function fail() { - }); - } - - function addEntityViewsToCustomer($event) { - if ($event) { - $event.stopPropagation(); - } - var pageSize = 10; - entityViewService.getTenantEntityViews({limit: pageSize, textSearch: ''}, false).then( - function success(_entityViews) { - var entityViews = { - pageSize: pageSize, - data: _entityViews.data, - nextPageLink: _entityViews.nextPageLink, - selections: {}, - selectedCount: 0, - hasNext: _entityViews.hasNext, - pending: false - }; - if (entityViews.hasNext) { - entityViews.nextPageLink.limit = pageSize; - } - $mdDialog.show({ - controller: 'AddEntityViewsToCustomerController', - controllerAs: 'vm', - templateUrl: addEntityViewsToCustomerTemplate, - locals: {customerId: customerId, entityViews: entityViews}, - parent: angular.element($document[0].body), - fullscreen: true, - targetEvent: $event - }).then(function () { - vm.grid.refreshList(); - }, function () { - }); - }, - function fail() { - }); - } - - function assignEntityViewsToCustomer($event, items) { - var entityViewIds = []; - for (var id in items.selections) { - entityViewIds.push(id); - } - assignToCustomer($event, entityViewIds); - } - - function unassignFromCustomer($event, entityView, isPublic) { - if ($event) { - $event.stopPropagation(); - } - var title; - var content; - var label; - if (isPublic) { - title = $translate.instant('entity-view.make-private-entity-view-title', {entityViewName: entityView.name}); - content = $translate.instant('entity-view.make-private-entity-view-text'); - label = $translate.instant('entity-view.make-private'); - } else { - title = $translate.instant('entity-view.unassign-entity-view-title', {entityViewName: entityView.name}); - content = $translate.instant('entity-view.unassign-entity-view-text'); - label = $translate.instant('entity-view.unassign-entity-view'); - } - var confirm = $mdDialog.confirm() - .targetEvent($event) - .title(title) - .htmlContent(content) - .ariaLabel(label) - .cancel($translate.instant('action.no')) - .ok($translate.instant('action.yes')); - $mdDialog.show(confirm).then(function () { - entityViewService.unassignEntityViewFromCustomer(entityView.id.id).then(function success() { - vm.grid.refreshList(); - }); - }); - } - - function unassignEntityViewsFromCustomer($event, items) { - var confirm = $mdDialog.confirm() - .targetEvent($event) - .title($translate.instant('entity-view.unassign-entity-views-title', {count: items.selectedCount}, 'messageformat')) - .htmlContent($translate.instant('entity-view.unassign-entity-views-text')) - .ariaLabel($translate.instant('entity-view.unassign-entity-view')) - .cancel($translate.instant('action.no')) - .ok($translate.instant('action.yes')); - $mdDialog.show(confirm).then(function () { - var tasks = []; - for (var id in items.selections) { - tasks.push(entityViewService.unassignEntityViewFromCustomer(id)); - } - $q.all(tasks).then(function () { - vm.grid.refreshList(); - }); - }); - } - - function makePublic($event, entityView) { - if ($event) { - $event.stopPropagation(); - } - var confirm = $mdDialog.confirm() - .targetEvent($event) - .title($translate.instant('entity-view.make-public-entity-view-title', {entityViewName: entityView.name})) - .htmlContent($translate.instant('entity-view.make-public-entity-view-text')) - .ariaLabel($translate.instant('entity-view.make-public')) - .cancel($translate.instant('action.no')) - .ok($translate.instant('action.yes')); - $mdDialog.show(confirm).then(function () { - entityViewService.makeEntityViewPublic(entityView.id.id).then(function success() { - vm.grid.refreshList(); - }); - }); - } -} diff --git a/ui/src/app/entity-view/entity-view.directive.js b/ui/src/app/entity-view/entity-view.directive.js deleted file mode 100644 index cf77489674..0000000000 --- a/ui/src/app/entity-view/entity-view.directive.js +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './entity-view.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import entityViewFieldsetTemplate from './entity-view-fieldset.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function EntityViewDirective($q, $compile, $templateCache, $filter, toast, $translate, $mdConstant, $mdExpansionPanel, - types, clipboardService, entityViewService, customerService, entityService) { - var linker = function (scope, element) { - var template = $templateCache.get(entityViewFieldsetTemplate); - element.html(template); - - scope.attributesPanelId = (Math.random()*1000).toFixed(0); - scope.timeseriesPanelId = (Math.random()*1000).toFixed(0); - scope.$mdExpansionPanel = $mdExpansionPanel; - - scope.types = types; - scope.isAssignedToCustomer = false; - scope.isPublic = false; - scope.assignedCustomer = null; - - scope.allowedEntityTypes = [types.entityType.device, types.entityType.asset]; - - var semicolon = 186; - scope.separatorKeys = [$mdConstant.KEY_CODE.ENTER, $mdConstant.KEY_CODE.COMMA, semicolon]; - - scope.$watch('entityView', function(newVal) { - if (newVal) { - if (scope.entityView.customerId && scope.entityView.customerId.id !== types.id.nullUid) { - scope.isAssignedToCustomer = true; - customerService.getShortCustomerInfo(scope.entityView.customerId.id).then( - function success(customer) { - scope.assignedCustomer = customer; - scope.isPublic = customer.isPublic; - } - ); - } else { - scope.isAssignedToCustomer = false; - scope.isPublic = false; - scope.assignedCustomer = null; - } - if (scope.entityView.startTimeMs > 0) { - scope.startTimeMs = new Date(scope.entityView.startTimeMs); - } else { - scope.startTimeMs = null; - } - if (scope.entityView.endTimeMs > 0) { - scope.endTimeMs = new Date(scope.entityView.endTimeMs); - } else { - scope.endTimeMs = null; - } - if (!scope.entityView.keys) { - scope.entityView.keys = {}; - scope.entityView.keys.timeseries = []; - scope.entityView.keys.attributes = {}; - scope.entityView.keys.attributes.ss = []; - scope.entityView.keys.attributes.cs = []; - scope.entityView.keys.attributes.sh = []; - } - } - }); - - scope.dataKeysSearch = function (searchText, type) { - var deferred = $q.defer(); - entityService.getEntityKeys(scope.entityView.entityId.entityType, scope.entityView.entityId.id, searchText, type, {ignoreLoading: true}).then( - function success(keys) { - deferred.resolve(keys); - }, - function fail() { - deferred.resolve([]); - } - ); - return deferred.promise; - - }; - - scope.$watch('startTimeMs', function (newDate) { - if (newDate) { - if (newDate.getTime() > scope.maxStartTimeMs) { - scope.startTimeMs = angular.copy(scope.maxStartTimeMs); - } - } - updateMinMaxDates(); - }); - - scope.$watch('endTimeMs', function (newDate) { - if (newDate) { - if (newDate.getTime() < scope.minEndTimeMs) { - scope.endTimeMs = angular.copy(scope.minEndTimeMs); - } - } - updateMinMaxDates(); - }); - - function updateMinMaxDates() { - if (scope.entityView) { - if (scope.endTimeMs) { - scope.maxStartTimeMs = angular.copy(new Date(scope.endTimeMs.getTime())); - scope.entityView.endTimeMs = scope.endTimeMs.getTime(); - } else { - scope.entityView.endTimeMs = 0; - } - if (scope.startTimeMs) { - scope.minEndTimeMs = angular.copy(new Date(scope.startTimeMs.getTime())); - scope.entityView.startTimeMs = scope.startTimeMs.getTime(); - } else { - scope.entityView.startTimeMs = 0; - } - } - } - - scope.onEntityViewIdCopied = function() { - toast.showSuccess($translate.instant('entity-view.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left'); - }; - - $compile(element.contents())(scope); - } - return { - restrict: "E", - link: linker, - scope: { - entityView: '=', - isEdit: '=', - entityViewScope: '=', - theForm: '=', - onAssignToCustomer: '&', - onMakePublic: '&', - onUnassignFromCustomer: '&', - onDeleteEntityView: '&' - } - }; -} diff --git a/ui/src/app/entity-view/entity-view.routes.js b/ui/src/app/entity-view/entity-view.routes.js deleted file mode 100644 index c55add94fa..0000000000 --- a/ui/src/app/entity-view/entity-view.routes.js +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import entityViewsTemplate from './entity-views.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function EntityViewRoutes($stateProvider, types) { - $stateProvider - .state('home.entityViews', { - url: '/entityViews', - params: {'topIndex': 0}, - module: 'private', - auth: ['TENANT_ADMIN', 'CUSTOMER_USER'], - views: { - "content@home": { - templateUrl: entityViewsTemplate, - controller: 'EntityViewController', - controllerAs: 'vm' - } - }, - data: { - entityViewsType: 'tenant', - searchEnabled: true, - searchByEntitySubtype: true, - searchEntityType: types.entityType.entityView, - pageTitle: 'entity-view.entity-views' - }, - ncyBreadcrumb: { - label: '{"icon": "view_quilt", "label": "entity-view.entity-views"}' - } - }) - .state('home.customers.entityViews', { - url: '/:customerId/entityViews', - params: {'topIndex': 0}, - module: 'private', - auth: ['TENANT_ADMIN'], - views: { - "content@home": { - templateUrl: entityViewsTemplate, - controllerAs: 'vm', - controller: 'EntityViewController' - } - }, - data: { - entityViewsType: 'customer', - searchEnabled: true, - searchByEntitySubtype: true, - searchEntityType: types.entityType.entityView, - pageTitle: 'customer.entity-views' - }, - ncyBreadcrumb: { - label: '{"icon": "view_quilt", "label": "{{ vm.customerEntityViewsTitle }}", "translate": "false"}' - } - }); - -} diff --git a/ui/src/app/entity-view/entity-view.scss b/ui/src/app/entity-view/entity-view.scss deleted file mode 100644 index c74a5df5c3..0000000000 --- a/ui/src/app/entity-view/entity-view.scss +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "../../scss/constants"; - -.tb-entity-view-panel-group { - .tb-panel-title { - min-width: 90px; - user-select: none; - - @media (min-width: $layout-breakpoint-sm) { - min-width: 180px; - } - } - - .tb-panel-prompt { - overflow: hidden; - font-size: 14px; - color: rgba(0, 0, 0, .87); - text-overflow: ellipsis; - white-space: nowrap; - } - - &.disabled { - .tb-panel-title, - .tb-panel-prompt { - color: rgba(0, 0, 0, .38); - } - } - - md-icon.md-expansion-panel-icon { - margin-right: 0; - } -} diff --git a/ui/src/app/entity-view/entity-views.tpl.html b/ui/src/app/entity-view/entity-views.tpl.html deleted file mode 100644 index 8aecf26de8..0000000000 --- a/ui/src/app/entity-view/entity-views.tpl.html +++ /dev/null @@ -1,75 +0,0 @@ - - - -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    diff --git a/ui/src/app/entity-view/index.js b/ui/src/app/entity-view/index.js deleted file mode 100644 index ee99a05eef..0000000000 --- a/ui/src/app/entity-view/index.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import uiRouter from 'angular-ui-router'; -import thingsboardGrid from '../components/grid.directive'; -import thingsboardApiUser from '../api/user.service'; -import thingsboardApiEntityView from '../api/entity-view.service'; -import thingsboardApiCustomer from '../api/customer.service'; - -import EntityViewRoutes from './entity-view.routes'; -import {EntityViewController, EntityViewCardController} from './entity-view.controller'; -import AssignEntityViewToCustomerController from './assign-to-customer.controller'; -import AddEntityViewsToCustomerController from './add-entity-views-to-customer.controller'; -import EntityViewDirective from './entity-view.directive'; - -export default angular.module('thingsboard.entityView', [ - uiRouter, - thingsboardGrid, - thingsboardApiUser, - thingsboardApiEntityView, - thingsboardApiCustomer -]) - .config(EntityViewRoutes) - .controller('EntityViewController', EntityViewController) - .controller('EntityViewCardController', EntityViewCardController) - .controller('AssignEntityViewToCustomerController', AssignEntityViewToCustomerController) - .controller('AddEntityViewsToCustomerController', AddEntityViewsToCustomerController) - .directive('tbEntityView', EntityViewDirective) - .name; diff --git a/ui/src/app/entity/alias/aliases-entity-select-button.tpl.html b/ui/src/app/entity/alias/aliases-entity-select-button.tpl.html deleted file mode 100644 index 5a0ce41cd5..0000000000 --- a/ui/src/app/entity/alias/aliases-entity-select-button.tpl.html +++ /dev/null @@ -1,31 +0,0 @@ - -
    - - - {{ 'entity.select-entities' | translate }} - - devices_other - - - - {{ 'entity.select-entities' | translate }} - - {{displayValue}} - -
    diff --git a/ui/src/app/entity/alias/aliases-entity-select-panel.controller.js b/ui/src/app/entity/alias/aliases-entity-select-panel.controller.js deleted file mode 100644 index ae50195b67..0000000000 --- a/ui/src/app/entity/alias/aliases-entity-select-panel.controller.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/*@ngInject*/ -export default function AliasesEntitySelectPanelController(mdPanelRef, $scope, $filter, types, aliasController, onEntityAliasesUpdate) { - - var vm = this; - vm._mdPanelRef = mdPanelRef; - vm.aliasController = aliasController; - vm.onEntityAliasesUpdate = onEntityAliasesUpdate; - vm.entityAliases = {}; - vm.entityAliasesInfo = {}; - - vm.currentAliasEntityChanged = currentAliasEntityChanged; - - var allEntityAliases = vm.aliasController.getEntityAliases(); - for (var aliasId in allEntityAliases) { - var aliasInfo = vm.aliasController.getInstantAliasInfo(aliasId); - if (aliasInfo && !aliasInfo.resolveMultiple && aliasInfo.currentEntity - && aliasInfo.resolvedEntities.length > 1) { - vm.entityAliasesInfo[aliasId] = angular.copy(aliasInfo); - vm.entityAliasesInfo[aliasId].selectedId = aliasInfo.currentEntity.id; - } - } - - function currentAliasEntityChanged(aliasId, selectedId) { - var resolvedEntities = vm.entityAliasesInfo[aliasId].resolvedEntities; - var selected = $filter('filter')(resolvedEntities, {id: selectedId}); - if (selected && selected.length) { - vm.aliasController.updateCurrentAliasEntity(aliasId, selected[0]); - if (onEntityAliasesUpdate) { - onEntityAliasesUpdate(); - } - } - } - -} diff --git a/ui/src/app/entity/alias/aliases-entity-select-panel.tpl.html b/ui/src/app/entity/alias/aliases-entity-select-panel.tpl.html deleted file mode 100644 index a0d24bf70f..0000000000 --- a/ui/src/app/entity/alias/aliases-entity-select-panel.tpl.html +++ /dev/null @@ -1,33 +0,0 @@ - - -
    - -
    - - - - - {{resolvedEntity.name}} - - - -
    -
    -
    -
    diff --git a/ui/src/app/entity/alias/aliases-entity-select.directive.js b/ui/src/app/entity/alias/aliases-entity-select.directive.js deleted file mode 100644 index 94e35b0f2d..0000000000 --- a/ui/src/app/entity/alias/aliases-entity-select.directive.js +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './aliases-entity-select.scss'; - -import $ from 'jquery'; - -/* eslint-disable import/no-unresolved, import/default */ - -import aliasesEntitySelectButtonTemplate from './aliases-entity-select-button.tpl.html'; -import aliasesEntitySelectPanelTemplate from './aliases-entity-select-panel.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/* eslint-disable angular/angularelement */ -/*@ngInject*/ -export default function AliasesEntitySelectDirective($compile, $templateCache, $mdMedia, types, $mdPanel, $document, $translate) { - - var linker = function (scope, element, attrs) { - - /* tbAliasesEntitySelect (ng-model) - * { - * "aliasId": { - * alias: alias, - * entityType: entityType, - * entityId: entityId - * } - * } - */ - - var template = $templateCache.get(aliasesEntitySelectButtonTemplate); - - scope.tooltipDirection = angular.isDefined(attrs.tooltipDirection) ? attrs.tooltipDirection : 'top'; - - element.html(template); - - scope.openEditMode = function (event) { - if (scope.disabled) { - return; - } - var position; - var panelHeight = $mdMedia('min-height: 350px') ? 250 : 150; - var panelWidth = 300; - var offset = element[0].getBoundingClientRect(); - var bottomY = offset.bottom - $(window).scrollTop(); //eslint-disable-line - var leftX = offset.left - $(window).scrollLeft(); //eslint-disable-line - var yPosition; - var xPosition; - if (bottomY + panelHeight > $( window ).height()) { //eslint-disable-line - yPosition = $mdPanel.yPosition.ABOVE; - } else { - yPosition = $mdPanel.yPosition.BELOW; - } - if (leftX + panelWidth > $( window ).width()) { //eslint-disable-line - xPosition = $mdPanel.xPosition.CENTER; - } else { - xPosition = $mdPanel.xPosition.ALIGN_START; - } - position = $mdPanel.newPanelPosition() - .relativeTo(element) - .addPanelPosition(xPosition, yPosition); - var config = { - attachTo: angular.element($document[0].body), - controller: 'AliasesEntitySelectPanelController', - controllerAs: 'vm', - templateUrl: aliasesEntitySelectPanelTemplate, - panelClass: 'tb-aliases-entity-select-panel', - position: position, - fullscreen: false, - locals: { - 'aliasController': scope.aliasController, - 'onEntityAliasesUpdate': function () { - scope.updateView(); - } - }, - openFrom: event, - clickOutsideToClose: true, - escapeToClose: true, - focusOnOpen: false - }; - $mdPanel.open(config); - } - - scope.$on('entityAliasesChanged', function() { - scope.updateView(); - }); - - scope.$on('entityAliasResolved', function() { - scope.updateView(); - }); - - scope.updateView = function () { - updateDisplayValue(); - } - - function updateDisplayValue() { - var displayValue; - var singleValue = true; - var currentAliasId; - var entityAliases = scope.aliasController.getEntityAliases(); - for (var aliasId in entityAliases) { - var entityAlias = entityAliases[aliasId]; - if (!entityAlias.filter.resolveMultiple) { - var resolvedAlias = scope.aliasController.getInstantAliasInfo(aliasId); - if (resolvedAlias && resolvedAlias.currentEntity) { - if (!currentAliasId) { - currentAliasId = aliasId; - } else { - singleValue = false; - break; - } - } - } - } - if (singleValue && currentAliasId) { - var aliasInfo = scope.aliasController.getInstantAliasInfo(currentAliasId); - displayValue = aliasInfo.currentEntity.name; - } else { - displayValue = $translate.instant('entity.entities'); - } - scope.displayValue = displayValue; - } - - $compile(element.contents())(scope); - } - - return { - restrict: "E", - scope: { - aliasController:'=' - }, - link: linker - }; - -} - -/* eslint-enable angular/angularelement */ \ No newline at end of file diff --git a/ui/src/app/entity/alias/aliases-entity-select.scss b/ui/src/app/entity/alias/aliases-entity-select.scss deleted file mode 100644 index 16e1c49028..0000000000 --- a/ui/src/app/entity/alias/aliases-entity-select.scss +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "../../../scss/constants"; - -tb-aliases-entity-select { - min-width: 52px; -} - -.md-panel { - &.tb-aliases-entity-select-panel { - position: absolute; - } -} - -.tb-aliases-entity-select-panel { - min-width: 300px; - max-height: 150px; - overflow-x: hidden; - overflow-y: auto; - background: #fff; - border-radius: 4px; - box-shadow: - 0 7px 8px -4px rgba(0, 0, 0, .2), - 0 13px 19px 2px rgba(0, 0, 0, .14), - 0 5px 24px 4px rgba(0, 0, 0, .12); - - @media (min-height: 350px) { - max-height: 250px; - } - - md-content { - background-color: #fff; - } -} - -section.tb-aliases-entity-select { - min-height: 32px; - padding: 0 6px; - - @media (max-width: $layout-breakpoint-sm) { - padding: 0; - } - - span { - max-width: 200px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - pointer-events: all; - cursor: pointer; - } -} diff --git a/ui/src/app/entity/alias/entity-alias-dialog.controller.js b/ui/src/app/entity/alias/entity-alias-dialog.controller.js deleted file mode 100644 index a753785a74..0000000000 --- a/ui/src/app/entity/alias/entity-alias-dialog.controller.js +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './entity-alias-dialog.scss'; - -/*@ngInject*/ -export default function EntityAliasDialogController($scope, $mdDialog, $q, $filter, utils, entityService, types, isAdd, allowedEntityTypes, entityAliases, alias) { - - var vm = this; - - vm.types = types; - vm.isAdd = isAdd; - vm.allowedEntityTypes = allowedEntityTypes; - if (angular.isArray(entityAliases)) { - vm.entityAliases = entityAliases; - } else { - vm.entityAliases = []; - for (var aliasId in entityAliases) { - vm.entityAliases.push(entityAliases[aliasId]); - } - } - if (vm.isAdd && !alias) { - vm.alias = { - alias: '', - filter: { - resolveMultiple: false - } - }; - } else { - vm.alias = alias; - } - - vm.cancel = cancel; - vm.save = save; - - $scope.$watch('vm.alias.alias', function (newAlias) { - if (newAlias) { - var valid = true; - var result = $filter('filter')(vm.entityAliases, {alias: newAlias}, true); - if (result && result.length) { - if (vm.isAdd || vm.alias.id != result[0].id) { - valid = false; - } - } - $scope.theForm.aliasName.$setValidity('duplicateAliasName', valid); - } - }); - - $scope.$watch('theForm.$pristine', function() { - if ($scope.theForm && !$scope.theForm.$pristine) { - $scope.theForm.$setValidity('entityFilter', true); - } - }); - - function validate() { - var deferred = $q.defer(); - var validationResult = { - entity: null, - stateEntity: false - } - entityService.resolveAliasFilter(vm.alias.filter, null, 1, true).then( - function success(result) { - validationResult.stateEntity = result.stateEntity; - var entities = result.entities; - if (entities.length) { - validationResult.entity = entities[0]; - } - deferred.resolve(validationResult); - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - - function cancel() { - $mdDialog.cancel(); - } - - function save() { - $scope.theForm.$setPristine(); - validate().then( - function success() { - if (vm.isAdd) { - vm.alias.id = utils.guid(); - } - $mdDialog.hide(vm.alias); - }, - function fail() { - $scope.theForm.$setValidity('entityFilter', false); - } - ) - } - -} diff --git a/ui/src/app/entity/alias/entity-alias-dialog.scss b/ui/src/app/entity/alias/entity-alias-dialog.scss deleted file mode 100644 index 35e71a7060..0000000000 --- a/ui/src/app/entity/alias/entity-alias-dialog.scss +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-entity-alias-dialog { - .tb-resolve-multiple-switch { - padding-left: 10px; - - .resolve-multiple-switch { - margin: 0; - } - - .resolve-multiple-label { - margin: 5px 0; - } - } -} diff --git a/ui/src/app/entity/alias/entity-alias-dialog.tpl.html b/ui/src/app/entity/alias/entity-alias-dialog.tpl.html deleted file mode 100644 index 83493c6621..0000000000 --- a/ui/src/app/entity/alias/entity-alias-dialog.tpl.html +++ /dev/null @@ -1,73 +0,0 @@ - - -
    - -
    -

    {{ (vm.isAdd ? 'alias.add' : 'alias.edit') | translate }}

    - - - - -
    -
    - - - -
    -
    -
    -
    - - - -
    -
    alias.name-required
    -
    alias.duplicate-alias
    -
    -
    -
    - - - -
    -
    - - - -
    -
    -
    -
    - - - - {{ (vm.isAdd ? 'action.add' : 'action.save') | translate }} - - {{ 'action.cancel' | translate }} - -
    -
    diff --git a/ui/src/app/entity/alias/entity-aliases.controller.js b/ui/src/app/entity/alias/entity-aliases.controller.js deleted file mode 100644 index c514e79ad7..0000000000 --- a/ui/src/app/entity/alias/entity-aliases.controller.js +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './entity-aliases.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import entityAliasDialogTemplate from './entity-alias-dialog.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function EntityAliasesController(utils, entityService, toast, $scope, $mdDialog, $document, $q, $translate, - types, config) { - - var vm = this; - - vm.types = types; - vm.entityAliases = []; - vm.title = config.customTitle ? config.customTitle : 'entity.aliases'; - vm.disableAdd = config.disableAdd; - vm.aliasToWidgetsMap = {}; - vm.allowedEntityTypes = config.allowedEntityTypes; - - vm.addAlias = addAlias; - vm.editAlias = editAlias; - vm.removeAlias = removeAlias; - - vm.cancel = cancel; - vm.save = save; - - initController(); - - function initController() { - var aliasId; - if (config.widgets) { - var widgetsTitleList, widget; - if (config.isSingleWidget && config.widgets.length == 1) { - widget = config.widgets[0]; - widgetsTitleList = [widget.config.title]; - for (aliasId in config.entityAliases) { - vm.aliasToWidgetsMap[aliasId] = widgetsTitleList; - } - } else { - for (var w in config.widgets) { - widget = config.widgets[w]; - if (widget.type === types.widgetType.rpc.value) { - if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) { - var targetDeviceAliasId = widget.config.targetDeviceAliasIds[0]; - widgetsTitleList = vm.aliasToWidgetsMap[targetDeviceAliasId]; - if (!widgetsTitleList) { - widgetsTitleList = []; - vm.aliasToWidgetsMap[targetDeviceAliasId] = widgetsTitleList; - } - widgetsTitleList.push(widget.config.title); - } - } else { - var datasources = utils.validateDatasources(widget.config.datasources); - for (var i=0;i -1) { - var widgetsTitleList = vm.aliasToWidgetsMap[entityAlias.id]; - if (widgetsTitleList) { - var widgetsListHtml = ''; - for (var t in widgetsTitleList) { - widgetsListHtml += '
    \'' + widgetsTitleList[t] + '\''; - } - var alert = $mdDialog.alert() - .parent(angular.element($document[0].body)) - .clickOutsideToClose(true) - .title($translate.instant('entity.unable-delete-entity-alias-title')) - .htmlContent($translate.instant('entity.unable-delete-entity-alias-text', {entityAlias: entityAlias.alias, widgetsList: widgetsListHtml})) - .ariaLabel($translate.instant('entity.unable-delete-entity-alias-title')) - .ok($translate.instant('action.close')) - .targetEvent($event); - alert._options.multiple = true; - alert._options.fullscreen = true; - - $mdDialog.show(alert); - } else { - vm.entityAliases.splice(index, 1); - if ($scope.theForm) { - $scope.theForm.$setDirty(); - } - } - } - } - - function cancel() { - $mdDialog.cancel(); - } - - function save() { - - var entityAliases = {}; - var uniqueAliasList = {}; - - var valid = true; - var message, aliasId, alias, filter; - - for (var i = 0; i < vm.entityAliases.length; i++) { - aliasId = vm.entityAliases[i].id; - alias = vm.entityAliases[i].alias; - filter = vm.entityAliases[i].filter; - if (uniqueAliasList[alias]) { - valid = false; - message = $translate.instant('entity.duplicate-alias-error', {alias: alias}); - break; - } else if (!filter || !filter.type) { - valid = false; - message = $translate.instant('entity.missing-entity-filter-error', {alias: alias}); - break; - } else { - uniqueAliasList[alias] = alias; - entityAliases[aliasId] = {id: aliasId, alias: alias, filter: filter}; - } - } - if (valid) { - $scope.theForm.$setPristine(); - $mdDialog.hide(entityAliases); - } else { - toast.showError(message); - } - } - -} diff --git a/ui/src/app/entity/alias/entity-aliases.scss b/ui/src/app/entity/alias/entity-aliases.scss deleted file mode 100644 index 140ac1b720..0000000000 --- a/ui/src/app/entity/alias/entity-aliases.scss +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-aliases-dialog { - .md-dialog-content { - padding-top: 0; - padding-bottom: 0; - } - - .tb-aliases-header { - min-height: 40px; - padding: 0 34px 0 34px; - margin: 5px; - - .tb-header-label { - font-size: 14px; - color: rgba(0, 0, 0, .570588); - } - } - - .tb-alias { - padding: 0 0 0 10px; - margin: 5px; - - md-input-container { - margin: 0; - } - - .tb-resolve-multiple-switch { - padding-left: 10px; - - .resolve-multiple-switch { - margin: 0; - } - } - - .md-button { - &.md-icon-button { - margin: 0; - } - } - } -} diff --git a/ui/src/app/entity/alias/entity-aliases.tpl.html b/ui/src/app/entity/alias/entity-aliases.tpl.html deleted file mode 100644 index 44b2ef1a1f..0000000000 --- a/ui/src/app/entity/alias/entity-aliases.tpl.html +++ /dev/null @@ -1,105 +0,0 @@ - - -
    - -
    -

    {{ vm.title | translate }}

    - - - - -
    -
    - - -
    - -
    - alias.name - alias.entity-filter - alias.resolve-multiple - -
    -
    - - -
    -
    -
    - {{$index + 1}}. -
    - - -
    -
    entity.alias-required
    -
    -
    - - -
    - - -
    - - - {{ 'alias.edit' | translate }} - - - edit - - - - - {{ 'entity.remove-alias' | translate }} - - - close - - -
    -
    -
    -
    -
    - - - - {{ 'alias.add' | translate }} - - alias.add - - - - {{ 'action.save' | translate }} - - {{ 'action.cancel' | translate }} - -
    -
    \ No newline at end of file diff --git a/ui/src/app/entity/attribute/add-attribute-dialog.controller.js b/ui/src/app/entity/attribute/add-attribute-dialog.controller.js deleted file mode 100644 index e1c711ba66..0000000000 --- a/ui/src/app/entity/attribute/add-attribute-dialog.controller.js +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import attributeDialogEditJsonTemplate from './attribute-dialog-edit-json.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -import AttributeDialogEditJsonController from './attribute-dialog-edit-json.controller'; - -/*@ngInject*/ -export default function AddAttributeDialogController($scope, $mdDialog, types, attributeService, entityType, entityId, attributeScope) { - - let vm = this; - - vm.attribute = {}; - - vm.valueTypes = types.valueType; - - vm.valueType = types.valueType.string; - - vm.add = add; - vm.cancel = cancel; - - function cancel() { - $mdDialog.cancel(); - } - - function add() { - $scope.theForm.$setPristine(); - attributeService.saveEntityAttributes(entityType, entityId, attributeScope, [vm.attribute]).then( - function success() { - $mdDialog.hide(); - } - ); - } - - $scope.$watch('vm.valueType', function () { - if (vm.valueType === types.valueType.boolean) { - vm.attribute.value = false; - } else if (vm.valueType === types.valueType.json) { - vm.attribute.value = {}; - } else { - vm.attribute.value = null; - } - }); - - vm.addJSON = ($event) => { - showJsonDialog($event, vm.attribute.value, false).then((response) => { - vm.attribute.value = response; - }) - }; - - function showJsonDialog($event, jsonValue, readOnly) { - if ($event) { - $event.stopPropagation(); - } - return $mdDialog.show({ - controller: AttributeDialogEditJsonController, - controllerAs: 'vm', - templateUrl: attributeDialogEditJsonTemplate, - locals: { - jsonValue: jsonValue, - readOnly: readOnly - }, - targetEvent: $event, - fullscreen: true, - multiple: true, - }); - } -} diff --git a/ui/src/app/entity/attribute/add-attribute-dialog.tpl.html b/ui/src/app/entity/attribute/add-attribute-dialog.tpl.html deleted file mode 100644 index 6520d7a541..0000000000 --- a/ui/src/app/entity/attribute/add-attribute-dialog.tpl.html +++ /dev/null @@ -1,117 +0,0 @@ - - -
    - -
    -

    attribute.add

    - - - - -
    -
    - - - -
    - -
    - - - -
    -
    attribute.key-required
    -
    -
    -
    - - - - - - {{type.name | translate}} - - - - - - -
    -
    attribute.value-required
    -
    -
    - - - -
    -
    attribute.value-required
    -
    value.invalid-integer-value
    -
    -
    - - - -
    -
    attribute.value-required
    -
    -
    -
    - - {{ (vm.attribute.value ? 'value.true' : 'value.false') | translate }} - -
    -
    - - - -
    -
    attribute.value-required
    -
    -
    - - - {{ 'action.edit' | translate }} - - - -
    -
    -
    -
    -
    -
    - - - - {{ 'action.add' | translate }} - - - {{ 'action.cancel' | translate }} - - -
    -
    diff --git a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js deleted file mode 100644 index 076bee89d5..0000000000 --- a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import selectTargetStateTemplate from '../../dashboard/states/select-target-state.tpl.html'; -import selectTargetLayoutTemplate from '../../dashboard/layouts/select-target-layout.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function AddWidgetToDashboardDialogController($scope, $mdDialog, $state, $q, $document, dashboardUtils, - utils, types, itembuffer, dashboardService, entityId, entityType, entityName, widget) { - - var vm = this; - - vm.widget = widget; - vm.dashboardId = null; - vm.addToDashboardType = 0; - vm.newDashboard = {}; - vm.openDashboard = false; - - vm.add = add; - vm.cancel = cancel; - - function cancel() { - $mdDialog.cancel(); - } - - function selectTargetState($event, dashboard) { - var deferred = $q.defer(); - var states = dashboard.configuration.states; - var stateIds = Object.keys(states); - if (stateIds.length > 1) { - $mdDialog.show({ - controller: 'SelectTargetStateController', - controllerAs: 'vm', - templateUrl: selectTargetStateTemplate, - parent: angular.element($document[0].body), - locals: { - states: states - }, - fullscreen: true, - multiple: true, - targetEvent: $event - }).then( - function success(stateId) { - deferred.resolve(stateId); - }, - function fail() { - deferred.reject(); - } - ); - - } else { - deferred.resolve(stateIds[0]); - } - return deferred.promise; - } - - function selectTargetLayout($event, dashboard, targetState) { - var deferred = $q.defer(); - var layouts = dashboard.configuration.states[targetState].layouts; - var layoutIds = Object.keys(layouts); - if (layoutIds.length > 1) { - $mdDialog.show({ - controller: 'SelectTargetLayoutController', - controllerAs: 'vm', - templateUrl: selectTargetLayoutTemplate, - parent: angular.element($document[0].body), - fullscreen: true, - multiple: true, - targetEvent: $event - }).then( - function success(layoutId) { - deferred.resolve(layoutId); - }, - function fail() { - deferred.reject(); - } - ); - } else { - deferred.resolve(layoutIds[0]); - } - return deferred.promise; - } - - function add($event) { - if (vm.addToDashboardType === 0) { - dashboardService.getDashboard(vm.dashboardId).then( - function success(dashboard) { - dashboard = dashboardUtils.validateAndUpdateDashboard(dashboard); - selectTargetState($event, dashboard).then( - function(targetState) { - selectTargetLayout($event, dashboard, targetState).then( - function(targetLayout) { - addWidgetToDashboard(dashboard, targetState, targetLayout); - } - ); - } - ); - }, - function fail() {} - ); - } else { - addWidgetToDashboard(vm.newDashboard, 'default', 'main'); - } - - } - - function addWidgetToDashboard(theDashboard, targetState, targetLayout) { - var aliasesInfo = { - datasourceAliases: {}, - targetDeviceAliases: {} - }; - aliasesInfo.datasourceAliases[0] = { - alias: entityName, - filter: dashboardUtils.createSingleEntityFilter(entityType, entityId) - }; - itembuffer.addWidgetToDashboard(theDashboard, targetState, targetLayout, vm.widget, aliasesInfo, null, 48, null, -1, -1).then( - function(theDashboard) { - dashboardService.saveDashboard(theDashboard).then( - function success(dashboard) { - $scope.theForm.$setPristine(); - $mdDialog.hide(); - if (vm.openDashboard) { - var stateParams = { - dashboardId: dashboard.id.id - } - var stateIds = Object.keys(dashboard.configuration.states); - var stateIndex = stateIds.indexOf(targetState); - if (stateIndex > 0) { - stateParams.state = utils.objToBase64([ {id: targetState, params: {}} ]); - } - $state.go('home.dashboards.dashboard', stateParams); - } - } - ); - } - ); - } -} diff --git a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.tpl.html b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.tpl.html deleted file mode 100644 index b6c1746b41..0000000000 --- a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.tpl.html +++ /dev/null @@ -1,83 +0,0 @@ - - -
    - -
    -

    attribute.add-widget-to-dashboard

    - - - - -
    -
    - - - -
    - -
    - - -
    - dashboard.select-existing - - -
    -
    - -
    - dashboard.create-new - - - -
    -
    dashboard.title-required
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - - - - {{ 'dashboard.open-dashboard' | translate }} - - - {{ 'action.add' | translate }} - - - {{ 'action.cancel' | translate }} - - -
    -
    diff --git a/ui/src/app/entity/attribute/attribute-dialog-edit-json.controller.js b/ui/src/app/entity/attribute/attribute-dialog-edit-json.controller.js deleted file mode 100644 index e53d168a68..0000000000 --- a/ui/src/app/entity/attribute/attribute-dialog-edit-json.controller.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-enable import/no-unresolved, import/default */ - -import './attribute-dialog-edit-json.scss'; - -/*@ngInject*/ -export default function AttributeDialogEditJsonController($mdDialog, types, jsonValue, readOnly) { - - let vm = this; - vm.json = angular.toJson(jsonValue, 4); - vm.readOnly = readOnly; - vm.contentType = types.contentType.JSON.value; - - vm.save = () => { - $mdDialog.hide(angular.fromJson(vm.json)); - }; - - vm.cancel = () => { - $mdDialog.cancel(); - }; -} diff --git a/ui/src/app/entity/attribute/attribute-dialog-edit-json.scss b/ui/src/app/entity/attribute/attribute-dialog-edit-json.scss deleted file mode 100644 index 84219ac11a..0000000000 --- a/ui/src/app/entity/attribute/attribute-dialog-edit-json.scss +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.attribute-edit-json-dialog{ - min-width: 400px; -} - -@media (max-width: 425px){ - .attribute-edit-json-dialog { - min-width: 200px; - } -} diff --git a/ui/src/app/entity/attribute/attribute-dialog-edit-json.tpl.html b/ui/src/app/entity/attribute/attribute-dialog-edit-json.tpl.html deleted file mode 100644 index 1c801edc10..0000000000 --- a/ui/src/app/entity/attribute/attribute-dialog-edit-json.tpl.html +++ /dev/null @@ -1,54 +0,0 @@ - - -
    - -
    -

    {{ 'details.edit-json' | translate }}

    - - - - -
    -
    - -
    - - -
    -
    - - - {{'action.save'|translate}} - - - {{'action.cancel'|translate }} - - -
    -
    diff --git a/ui/src/app/entity/attribute/attribute-table.directive.js b/ui/src/app/entity/attribute/attribute-table.directive.js deleted file mode 100644 index 59ea4fe91d..0000000000 --- a/ui/src/app/entity/attribute/attribute-table.directive.js +++ /dev/null @@ -1,462 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import 'angular-material-data-table/dist/md-data-table.min.css'; -import './attribute-table.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import attributeTableTemplate from './attribute-table.tpl.html'; -import addAttributeDialogTemplate from './add-attribute-dialog.tpl.html'; -import addWidgetToDashboardDialogTemplate from './add-widget-to-dashboard-dialog.tpl.html'; -import editAttributeValueTemplate from './edit-attribute-value.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -import EditAttributeValueController from './edit-attribute-value.controller'; -import AliasController from '../../api/alias-controller'; - -/*@ngInject*/ -export default function AttributeTableDirective($compile, $templateCache, $rootScope, $q, $mdEditDialog, $mdDialog, - $mdUtil, $document, $translate, $filter, $timeout, utils, types, dashboardUtils, - entityService, attributeService, widgetService) { - - var linker = function (scope, element, attrs) { - - var template = $templateCache.get(attributeTableTemplate); - - element.html(template); - - var getAttributeScopeByValue = function (attributeScopeValue) { - if (scope.types.latestTelemetry.value === attributeScopeValue) { - return scope.types.latestTelemetry; - } - for (var attrScope in scope.attributeScopes) { - if (scope.attributeScopes[attrScope].value === attributeScopeValue) { - return scope.attributeScopes[attrScope]; - } - } - }; - - scope.types = types; - - scope.entityType = attrs.entityType; - - if (scope.entityType === types.entityType.device || scope.entityType === types.entityType.entityView) { - scope.attributeScopes = types.attributesScope; - scope.attributeScopeSelectionReadonly = false; - } else { - scope.attributeScopes = {}; - scope.attributeScopes.server = types.attributesScope.server; - scope.attributeScopeSelectionReadonly = true; - } - - scope.attributeScope = getAttributeScopeByValue(attrs.defaultAttributeScope); - - if (scope.entityType != types.entityType.device) { - if (scope.attributeScope != types.latestTelemetry) { - scope.attributeScope = scope.attributeScopes.server; - } - } - - scope.attributes = { - count: 0, - data: [] - }; - - scope.selectedAttributes = []; - scope.mode = 'default'; // 'widget' - scope.subscriptionId = null; - - scope.query = { - order: 'key', - limit: 5, - page: 1, - search: null - }; - - scope.$watch("entityId", function (newVal) { - if (newVal) { - scope.resetFilter(); - scope.getEntityAttributes(false, true); - } - }); - - scope.$watch("attributeScope", function (newVal, prevVal) { - if (newVal && !angular.equals(newVal, prevVal)) { - scope.mode = 'default'; - scope.query.search = null; - scope.selectedAttributes = []; - scope.getEntityAttributes(false, true); - } - }); - - scope.resetFilter = function () { - scope.mode = 'default'; - scope.query.search = null; - scope.selectedAttributes = []; - scope.attributeScope = getAttributeScopeByValue(attrs.defaultAttributeScope); - }; - - scope.enterFilterMode = function (event) { - let $button = angular.element(event.currentTarget); - let $toolbarsContainer = $button.closest('.toolbarsContainer'); - - scope.query.search = ''; - - $timeout(() => { - $toolbarsContainer.find('.searchInput').focus(); - }) - }; - - scope.exitFilterMode = function () { - scope.query.search = null; - scope.getEntityAttributes(); - }; - - scope.$watch("query.search", function (newVal, prevVal) { - if (!angular.equals(newVal, prevVal) && scope.query.search != null) { - scope.getEntityAttributes(); - } - }); - - function success(attributes, update, apply) { - scope.attributes = attributes; - if (!update) { - scope.selectedAttributes = []; - } - if (apply) { - scope.$digest(); - } - } - - scope.onReorder = function () { - scope.getEntityAttributes(false, false); - }; - - scope.onPaginate = function () { - scope.getEntityAttributes(false, false); - }; - - scope.getEntityAttributes = function (forceUpdate, reset) { - if (scope.attributesDeferred) { - scope.attributesDeferred.resolve(); - } - if (scope.entityId && scope.entityType && scope.attributeScope) { - if (reset) { - scope.attributes = { - count: 0, - data: [] - }; - } - scope.checkSubscription(); - scope.attributesDeferred = attributeService.getEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value, - scope.query, function (attributes, update, apply) { - success(attributes, update || forceUpdate, apply); - } - ); - } else { - var deferred = $q.defer(); - scope.attributesDeferred = deferred; - success({ - count: 0, - data: [] - }); - deferred.resolve(); - } - }; - - scope.checkSubscription = function () { - var newSubscriptionId = null; - if (scope.entityId && scope.entityType && scope.attributeScope.clientSide && scope.mode != 'widget') { - newSubscriptionId = attributeService.subscribeForEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value); - } - if (scope.subscriptionId && scope.subscriptionId != newSubscriptionId) { - attributeService.unsubscribeForEntityAttributes(scope.subscriptionId); - } - scope.subscriptionId = newSubscriptionId; - }; - - scope.$on('$destroy', function () { - if (scope.subscriptionId) { - attributeService.unsubscribeForEntityAttributes(scope.subscriptionId); - } - }); - - scope.editAttribute = function ($event, attribute) { - if (!scope.attributeScope.clientSide) { - $event.stopPropagation(); - $mdEditDialog.show({ - controller: EditAttributeValueController, - templateUrl: editAttributeValueTemplate, - locals: { - attributeValue: attribute.value, - save: function (model) { - var updatedAttribute = angular.copy(attribute); - updatedAttribute.value = model.value; - attributeService.saveEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value, [updatedAttribute]).then( - function success() { - scope.getEntityAttributes(); - } - ); - } - }, - targetEvent: $event - }); - } - }; - - scope.addAttribute = function ($event) { - if (!scope.attributeScope.clientSide) { - $event.stopPropagation(); - $mdDialog.show({ - controller: 'AddAttributeDialogController', - controllerAs: 'vm', - templateUrl: addAttributeDialogTemplate, - parent: angular.element($document[0].body), - locals: { - entityType: scope.entityType, - entityId: scope.entityId, - attributeScope: scope.attributeScope.value - }, - fullscreen: true, - targetEvent: $event - }).then(function () { - scope.getEntityAttributes(); - }); - } - }; - - scope.deleteAttributes = function ($event) { - if (!scope.attributeScope.clientSide) { - $event.stopPropagation(); - var confirm = $mdDialog.confirm() - .targetEvent($event) - .title($translate.instant('attribute.delete-attributes-title', {count: scope.selectedAttributes.length}, 'messageformat')) - .htmlContent($translate.instant('attribute.delete-attributes-text')) - .ariaLabel($translate.instant('attribute.delete-attributes')) - .cancel($translate.instant('action.no')) - .ok($translate.instant('action.yes')); - $mdDialog.show(confirm).then(function () { - attributeService.deleteEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value, scope.selectedAttributes).then( - function success() { - scope.selectedAttributes = []; - scope.getEntityAttributes(); - } - ) - }); - } - }; - - scope.nextWidget = function () { - $mdUtil.nextTick(function () { - if (scope.widgetsCarousel.index < scope.widgetsList.length - 1) { - scope.widgetsCarousel.index++; - } - }); - }; - - scope.prevWidget = function () { - $mdUtil.nextTick(function () { - if (scope.widgetsCarousel.index > 0) { - scope.widgetsCarousel.index--; - } - }); - }; - - scope.enterWidgetMode = function () { - - if (scope.widgetsIndexWatch) { - scope.widgetsIndexWatch(); - scope.widgetsIndexWatch = null; - } - - if (scope.widgetsBundleWatch) { - scope.widgetsBundleWatch(); - scope.widgetsBundleWatch = null; - } - - scope.mode = 'widget'; - scope.checkSubscription(); - scope.widgetsList = []; - scope.widgetsListCache = []; - scope.widgetsLoaded = false; - scope.widgetsCarousel = { - index: 0 - } - scope.widgetsBundle = null; - scope.firstBundle = true; - scope.selectedWidgetsBundleAlias = types.systemBundleAlias.cards; - - var entityAlias = { - id: utils.guid(), - alias: scope.entityName, - filter: dashboardUtils.createSingleEntityFilter(scope.entityType, scope.entityId) - }; - var entitiAliases = {}; - entitiAliases[entityAlias.id] = entityAlias; - - var stateController = { - getStateParams: function () { - return {}; - } - }; - scope.aliasController = new AliasController(scope, $q, $filter, utils, - types, entityService, stateController, entitiAliases); - - var dataKeyType = scope.attributeScope === types.latestTelemetry ? - types.dataKeyType.timeseries : types.dataKeyType.attribute; - - var datasource = { - type: types.datasourceType.entity, - entityAliasId: entityAlias.id, - dataKeys: [] - }; - var i = 0; - for (var attr = 0; attr < scope.selectedAttributes.length; attr++) { - var attribute = scope.selectedAttributes[attr]; - var dataKey = { - name: attribute.key, - label: attribute.key, - type: dataKeyType, - color: utils.getMaterialColor(i), - settings: {}, - _hash: Math.random() - }; - datasource.dataKeys.push(dataKey); - i++; - } - - scope.widgetsIndexWatch = scope.$watch('widgetsCarousel.index', function (newVal, prevVal) { - if (scope.mode === 'widget' && (newVal != prevVal)) { - var index = scope.widgetsCarousel.index; - for (var i = 0; i < scope.widgetsList.length; i++) { - scope.widgetsList[i].splice(0, scope.widgetsList[i].length); - if (i === index) { - scope.widgetsList[i].push(scope.widgetsListCache[i][0]); - } - } - } - }); - - scope.widgetsBundleWatch = scope.$watch('widgetsBundle', function (newVal, prevVal) { - if (scope.mode === 'widget' && (scope.firstBundle === true || newVal != prevVal)) { - scope.widgetsList = []; - scope.widgetsListCache = []; - scope.widgetsCarousel.index = 0; - scope.firstBundle = false; - if (scope.widgetsBundle) { - scope.widgetsLoaded = false; - var bundleAlias = scope.widgetsBundle.alias; - var isSystem = scope.widgetsBundle.tenantId.id === types.id.nullUid; - widgetService.getBundleWidgetTypes(scope.widgetsBundle.alias, isSystem).then( - function success(widgetTypes) { - - widgetTypes = $filter('orderBy')(widgetTypes, ['-descriptor.type', '-createdTime']); - - for (var i = 0; i < widgetTypes.length; i++) { - var widgetType = widgetTypes[i]; - var widgetInfo = widgetService.toWidgetInfo(widgetType); - if (widgetInfo.type !== types.widgetType.static.value) { - var sizeX = widgetInfo.sizeX * 2; - var sizeY = widgetInfo.sizeY * 2; - var col = Math.floor(Math.max(0, (20 - sizeX) / 2)); - var widget = { - isSystemType: isSystem, - bundleAlias: bundleAlias, - typeAlias: widgetInfo.alias, - type: widgetInfo.type, - title: widgetInfo.widgetName, - sizeX: sizeX, - sizeY: sizeY, - row: 0, - col: col, - config: angular.fromJson(widgetInfo.defaultConfig) - }; - - widget.config.title = widgetInfo.widgetName; - widget.config.datasources = [datasource]; - var length; - if (scope.attributeScope === types.latestTelemetry && widgetInfo.type !== types.widgetType.rpc.value) { - length = scope.widgetsListCache.push([widget]); - scope.widgetsList.push(length === 1 ? [widget] : []); - } else if (widgetInfo.type === types.widgetType.latest.value) { - length = scope.widgetsListCache.push([widget]); - scope.widgetsList.push(length === 1 ? [widget] : []); - } - } - } - scope.widgetsLoaded = true; - } - ); - } - } - }); - }; - - scope.exitWidgetMode = function () { - if (scope.widgetsBundleWatch) { - scope.widgetsBundleWatch(); - scope.widgetsBundleWatch = null; - } - if (scope.widgetsIndexWatch) { - scope.widgetsIndexWatch(); - scope.widgetsIndexWatch = null; - } - scope.selectedWidgetsBundleAlias = null; - scope.mode = 'default'; - scope.getEntityAttributes(true); - }; - - scope.addWidgetToDashboard = function ($event) { - if (scope.mode === 'widget' && scope.widgetsListCache.length > 0) { - var widget = scope.widgetsListCache[scope.widgetsCarousel.index][0]; - $event.stopPropagation(); - $mdDialog.show({ - controller: 'AddWidgetToDashboardDialogController', - controllerAs: 'vm', - templateUrl: addWidgetToDashboardDialogTemplate, - parent: angular.element($document[0].body), - locals: { - entityId: scope.entityId, - entityType: scope.entityType, - entityName: scope.entityName, - widget: angular.copy(widget) - }, - fullscreen: true, - targetEvent: $event - }).then(function () { - - }); - } - }; - - scope.loading = function () { - return $rootScope.loading; - }; - - $compile(element.contents())(scope); - }; - - return { - restrict: "E", - link: linker, - scope: { - entityId: '=', - entityName: '=', - disableAttributeScopeSelection: '@?' - } - }; -} diff --git a/ui/src/app/entity/attribute/attribute-table.scss b/ui/src/app/entity/attribute/attribute-table.scss deleted file mode 100644 index 5a0bde0c68..0000000000 --- a/ui/src/app/entity/attribute/attribute-table.scss +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "../../../scss/constants"; - -$md-light: rgba(255, 255, 255, 100%) !default; -$md-edit-icon-fill: #757575 !default; - -md-toolbar.md-table-toolbar.alternate { - .md-toolbar-tools { - md-icon { - color: $md-light; - } - } -} - -.md-table { - &.tb-attribute-table { - table-layout: fixed; - - td.md-cell { - &.tb-value-cell { - overflow: auto; - } - } - } - - .md-cell { - ng-md-icon { - float: right; - height: 24px; - fill: $md-edit-icon-fill; - } - } -} - -.widgets-carousel { - position: relative; - height: calc(100% - 100px); - min-height: 150px !important; - margin: 0; - - tb-dashboard { - #gridster-parent { - padding: 0 7px; - } - } -} - -md-edit-dialog.tb-edit-dialog{ - z-index: 78; -} - -md-backdrop.md-edit-dialog-backdrop{ - z-index: 77; -} diff --git a/ui/src/app/entity/attribute/attribute-table.tpl.html b/ui/src/app/entity/attribute/attribute-table.tpl.html deleted file mode 100644 index 8844891e62..0000000000 --- a/ui/src/app/entity/attribute/attribute-table.tpl.html +++ /dev/null @@ -1,207 +0,0 @@ - - -
    - - - - - {{scope.name | translate}} - - - -
    -
    - -
    - {{ attributeScope.name }} - - - add - - {{ 'action.add' | translate }} - - - - search - - {{ 'action.search' | translate }} - - - - refresh - - {{ 'action.refresh' | translate }} - - -
    -
    - -
    - - search - - {{ 'action.search' | translate }} - - - - - - - - close - - {{ 'action.close' | translate }} - - -
    -
    - -
    - - - - delete - - {{ 'action.delete' | translate }} - - - - - {{ 'attribute.show-on-widget' | translate }} - - now_widgets - attribute.show-on-widget - -
    -
    - -
    -
    - {{ 'widgets-bundle.current' | translate }} - - -
    - - - {{ 'attribute.add-to-dashboard' | translate }} - - dashboard - attribute.add-to-dashboard - - - close - - {{ 'action.close' | translate }} - - -
    -
    - - - - - - - - - - - - - - - - -
    attribute.last-update-timeattribute.keyattribute.value
    {{attribute.lastUpdateTs | date : 'yyyy-MM-dd HH:mm:ss'}}{{attribute.key}} - {{attribute.value}} - -
    -
    - - - -
    -
    diff --git a/ui/src/app/entity/attribute/edit-attribute-value.controller.js b/ui/src/app/entity/attribute/edit-attribute-value.controller.js deleted file mode 100644 index 7cb550d4c1..0000000000 --- a/ui/src/app/entity/attribute/edit-attribute-value.controller.js +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-enable import/no-unresolved, import/default */ - -import AttributeDialogEditJsonController from "./attribute-dialog-edit-json.controller"; -import attributeDialogEditJsonTemplate from "./attribute-dialog-edit-json.tpl.html"; - -/*@ngInject*/ -export default function EditAttributeValueController($scope, $mdDialog, $q, $element, $document, types, attributeValue, save) { - - $scope.valueTypes = types.valueType; - - $scope.model = { - value: attributeValue - }; - - if ($scope.model.value === true || $scope.model.value === false) { - $scope.valueType = types.valueType.boolean; - } else if (angular.isNumber($scope.model.value)) { - if ($scope.model.value.toString().indexOf('.') == -1) { - $scope.valueType = types.valueType.integer; - } else { - $scope.valueType = types.valueType.double; - } - } else if (angular.isObject($scope.model.value)) { - $scope.valueType = types.valueType.json; - } else { - $scope.valueType = types.valueType.string; - } - - $scope.submit = submit; - $scope.dismiss = dismiss; - $scope.editJSON = editJSON; - - function dismiss() { - $element.remove(); - } - - function update() { - if ($scope.editDialog.$invalid) { - return $q.reject(); - } - if (angular.isFunction(save)) { - return $q.when(save($scope.model)); - } - return $q.resolve(); - } - - function submit() { - update().then(function () { - $scope.dismiss(); - }); - } - - - $scope.$watch('valueType', function (newVal, prevVal) { - if (newVal !== prevVal) { - if ($scope.valueType === types.valueType.boolean) { - $scope.model.value = false; - } else if ($scope.valueType === types.valueType.json) { - $scope.model.value = {}; - } else { - $scope.model.value = null; - } - } - }); - - function editJSON($event) { - $scope.hideDialog = true; - showJsonDialog($event, $scope.model.value, false).then((response) => { - $scope.hideDialog = false; - if (!angular.equals(response, $scope.model.value)) { - $scope.editDialog.$setDirty(); - } - $scope.model.value = response; - }) - } - - function showJsonDialog($event, jsonValue, readOnly) { - if ($event) { - $event.stopPropagation(); - } - return $mdDialog.show({ - controller: AttributeDialogEditJsonController, - controllerAs: 'vm', - templateUrl: attributeDialogEditJsonTemplate, - locals: { - jsonValue: jsonValue, - readOnly: readOnly - }, - targetEvent: $event, - fullscreen: true, - multiple: true - }); - } -} diff --git a/ui/src/app/entity/attribute/edit-attribute-value.tpl.html b/ui/src/app/entity/attribute/edit-attribute-value.tpl.html deleted file mode 100644 index 35abe746aa..0000000000 --- a/ui/src/app/entity/attribute/edit-attribute-value.tpl.html +++ /dev/null @@ -1,88 +0,0 @@ - - -
    -
    -
    -
    - - - - - - {{type.name | translate}} - - - - - - -
    -
    attribute.value-required
    -
    -
    - - - -
    -
    attribute.value-required
    -
    value.invalid-integer-value
    -
    -
    - - - -
    -
    attribute.value-required
    -
    -
    -
    - - {{ (model.value ? 'value.true' : 'value.false') | translate }} - -
    -
    - - - -
    -
    attribute.value-required
    -
    -
    - - - {{ 'action.edit' | translate }} - - - -
    -
    -
    -
    -
    - {{ 'action.cancel' | translate }} - - - {{ 'action.update' | translate }} - -
    -
    -
    diff --git a/ui/src/app/entity/entity-autocomplete.directive.js b/ui/src/app/entity/entity-autocomplete.directive.js deleted file mode 100644 index f726b50d94..0000000000 --- a/ui/src/app/entity/entity-autocomplete.directive.js +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './entity-autocomplete.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import entityAutocompleteTemplate from './entity-autocomplete.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function EntityAutocomplete($compile, $templateCache, $q, $filter, entityService, types) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - var template = $templateCache.get(entityAutocompleteTemplate); - element.html(template); - - scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false; - scope.entity = null; - scope.entitySearchText = ''; - - scope.fetchEntities = function(searchText) { - var deferred = $q.defer(); - var limit = 50; - if (scope.excludeEntityIds && scope.excludeEntityIds.length) { - limit += scope.excludeEntityIds.length; - } - var targetType = scope.entityType; - if (targetType == types.aliasEntityType.current_customer) { - targetType = types.entityType.customer; - } - - entityService.getEntitiesByNameFilter(targetType, searchText, limit, {ignoreLoading: true}, scope.entitySubtype).then(function success(result) { - if (result) { - if (scope.excludeEntityIds && scope.excludeEntityIds.length) { - var entities = []; - result.forEach(function(entity) { - if (scope.excludeEntityIds.indexOf(entity.id.id) == -1) { - entities.push(entity); - } - }); - deferred.resolve(entities); - } else { - deferred.resolve(result); - } - } else { - deferred.resolve([]); - } - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - scope.entitySearchTextChanged = function() { - } - - scope.updateView = function () { - if (!scope.disabled) { - ngModelCtrl.$setViewValue(scope.entity ? scope.entity.id.id : null); - } - } - - ngModelCtrl.$render = function () { - if (ngModelCtrl.$viewValue) { - var targetType = scope.entityType; - if (targetType == types.aliasEntityType.current_customer) { - targetType = types.entityType.customer; - } - entityService.getEntity(targetType, ngModelCtrl.$viewValue).then( - function success(entity) { - scope.entity = entity; - }, - function fail() { - scope.entity = null; - } - ); - } else { - scope.entity = null; - } - } - - scope.$watch('entityType', function () { - load(); - }); - - scope.$watch('entitySubtype', function () { - if (scope.entity && scope.entity.type != scope.entitySubtype) { - scope.entity = null; - scope.updateView(); - } - }); - - scope.$watch('entity', function (newVal, prevVal) { - if (!angular.equals(newVal, prevVal)) { - scope.updateView(); - } - }); - - scope.$watch('disabled', function (newVal, prevVal) { - if (!angular.equals(newVal, prevVal)) { - scope.updateView(); - } - }); - - - function load() { - switch (scope.entityType) { - case types.entityType.asset: - scope.selectEntityText = 'asset.select-asset'; - scope.entityText = 'asset.asset'; - scope.noEntitiesMatchingText = 'asset.no-assets-matching'; - scope.entityRequiredText = 'asset.asset-required'; - break; - case types.entityType.device: - scope.selectEntityText = 'device.select-device'; - scope.entityText = 'device.device'; - scope.noEntitiesMatchingText = 'device.no-devices-matching'; - scope.entityRequiredText = 'device.device-required'; - break; - case types.entityType.entityView: - scope.selectEntityText = 'entity-view.select-entity-view'; - scope.entityText = 'entity-view.entity-view'; - scope.noEntitiesMatchingText = 'entity-view.no-entity-views-matching'; - scope.entityRequiredText = 'entity-view.entity-view-required'; - break; - case types.entityType.rulechain: - scope.selectEntityText = 'rulechain.select-rulechain'; - scope.entityText = 'rulechain.rulechain'; - scope.noEntitiesMatchingText = 'rulechain.no-rulechains-matching'; - scope.entityRequiredText = 'rulechain.rulechain-required'; - break; - case types.entityType.tenant: - scope.selectEntityText = 'tenant.select-tenant'; - scope.entityText = 'tenant.tenant'; - scope.noEntitiesMatchingText = 'tenant.no-tenants-matching'; - scope.entityRequiredText = 'tenant.tenant-required'; - break; - case types.entityType.customer: - scope.selectEntityText = 'customer.select-customer'; - scope.entityText = 'customer.customer'; - scope.noEntitiesMatchingText = 'customer.no-customers-matching'; - scope.entityRequiredText = 'customer.customer-required'; - break; - case types.entityType.user: - scope.selectEntityText = 'user.select-user'; - scope.entityText = 'user.user'; - scope.noEntitiesMatchingText = 'user.no-users-matching'; - scope.entityRequiredText = 'user.user-required'; - break; - case types.entityType.dashboard: - scope.selectEntityText = 'dashboard.select-dashboard'; - scope.entityText = 'dashboard.dashboard'; - scope.noEntitiesMatchingText = 'dashboard.no-dashboards-matching'; - scope.entityRequiredText = 'dashboard.dashboard-required'; - break; - case types.entityType.alarm: - scope.selectEntityText = 'alarm.select-alarm'; - scope.entityText = 'alarm.alarm'; - scope.noEntitiesMatchingText = 'alarm.no-alarms-matching'; - scope.entityRequiredText = 'alarm.alarm-required'; - break; - case types.aliasEntityType.current_customer: - scope.selectEntityText = 'customer.select-default-customer'; - scope.entityText = 'customer.default-customer'; - scope.noEntitiesMatchingText = 'customer.no-customers-matching'; - scope.entityRequiredText = 'customer.default-customer-required'; - break; - } - if (scope.entity && scope.entity.id.entityType != scope.entityType) { - scope.entity = null; - scope.updateView(); - } - } - - $compile(element.contents())(scope); - } - - return { - restrict: "E", - require: "^ngModel", - link: linker, - scope: { - theForm: '=?', - tbRequired: '=?', - disabled:'=ngDisabled', - entityType: '=', - entitySubtype: '=?', - excludeEntityIds: '=?' - } - }; -} diff --git a/ui/src/app/entity/entity-autocomplete.scss b/ui/src/app/entity/entity-autocomplete.scss deleted file mode 100644 index c3053f3c48..0000000000 --- a/ui/src/app/entity/entity-autocomplete.scss +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-entity-autocomplete { - .tb-entity-item { - display: block; - height: 48px; - } - - li { - height: auto !important; - white-space: normal !important; - } -} diff --git a/ui/src/app/entity/entity-autocomplete.tpl.html b/ui/src/app/entity/entity-autocomplete.tpl.html deleted file mode 100644 index e9011ff36a..0000000000 --- a/ui/src/app/entity/entity-autocomplete.tpl.html +++ /dev/null @@ -1,45 +0,0 @@ - - - -
    - {{item.name}} -
    -
    - -
    - {{ noEntitiesMatchingText }} -
    -
    -
    -
    {{ entityRequiredText }}
    -
    -
    diff --git a/ui/src/app/entity/entity-filter-view.directive.js b/ui/src/app/entity/entity-filter-view.directive.js deleted file mode 100644 index 837313df28..0000000000 --- a/ui/src/app/entity/entity-filter-view.directive.js +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import entityFilterViewTemplate from './entity-filter-view.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -import './entity-filter-view.scss'; - -/*@ngInject*/ -export default function EntityFilterViewDirective($compile, $templateCache, $q, $document, $mdDialog, $translate, types/*, entityService*/) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - - var template = $templateCache.get(entityFilterViewTemplate); - element.html(template); - - scope.ngModelCtrl = ngModelCtrl; - scope.types = types; - scope.filterDisplayValue = ''; - - scope.$watch('filter', function () { - scope.updateDisplayValue(); - }); - - scope.updateDisplayValue = function() { - if (scope.filter && scope.filter.type) { - var entityType; - var prefix; - switch (scope.filter.type) { - case types.aliasFilterType.singleEntity.value: - entityType = scope.filter.singleEntity.entityType; - scope.filterDisplayValue = $translate.instant(types.entityTypeTranslations[entityType].list, {count: 1}, 'messageformat'); - break; - case types.aliasFilterType.entityList.value: - entityType = scope.filter.entityType; - var count = scope.filter.entityList.length; - scope.filterDisplayValue = $translate.instant(types.entityTypeTranslations[entityType].list, {count: count}, 'messageformat'); - break; - case types.aliasFilterType.entityName.value: - entityType = scope.filter.entityType; - prefix = scope.filter.entityNameFilter; - scope.filterDisplayValue = $translate.instant(types.entityTypeTranslations[entityType].nameStartsWith, {prefix: prefix}); - break; - case types.aliasFilterType.stateEntity.value: - scope.filterDisplayValue = $translate.instant('alias.filter-type-state-entity-description'); - break; - case types.aliasFilterType.assetType.value: - var assetType = scope.filter.assetType; - prefix = scope.filter.assetNameFilter; - if (prefix && prefix.length) { - scope.filterDisplayValue = $translate.instant('alias.filter-type-asset-type-and-name-description', {assetType: assetType, prefix: prefix}); - } else { - scope.filterDisplayValue = $translate.instant('alias.filter-type-asset-type-description', {assetType: assetType}); - } - break; - case types.aliasFilterType.deviceType.value: - var deviceType = scope.filter.deviceType; - prefix = scope.filter.deviceNameFilter; - if (prefix && prefix.length) { - scope.filterDisplayValue = $translate.instant('alias.filter-type-device-type-and-name-description', {deviceType: deviceType, prefix: prefix}); - } else { - scope.filterDisplayValue = $translate.instant('alias.filter-type-device-type-description', {deviceType: deviceType}); - } - break; - case types.aliasFilterType.entityViewType.value: - var entityViewType = scope.filter.entityViewType; - prefix = scope.filter.entityViewNameFilter; - if (prefix && prefix.length) { - scope.filterDisplayValue = $translate.instant('alias.filter-type-entity-view-type-and-name-description', {entityViewType: entityViewType, prefix: prefix}); - } else { - scope.filterDisplayValue = $translate.instant('alias.filter-type-entity-view-type-description', {entityViewType: entityViewType}); - } - break; - case types.aliasFilterType.relationsQuery.value: - var rootEntityText; - var directionText; - var allEntitiesText = $translate.instant('alias.all-entities'); - var anyRelationText = $translate.instant('alias.any-relation'); - if (scope.filter.rootStateEntity) { - rootEntityText = $translate.instant('alias.state-entity'); - } else { - rootEntityText = $translate.instant(types.entityTypeTranslations[scope.filter.rootEntity.entityType].type); - } - directionText = $translate.instant('relation.direction-type.' + scope.filter.direction); - var relationFilters = scope.filter.filters; - if (relationFilters && relationFilters.length) { - var relationFiltersDisplayValues = []; - relationFilters.forEach(function(relationFilter) { - var entitiesText; - if (relationFilter.entityTypes && relationFilter.entityTypes.length) { - var entitiesNamesList = []; - relationFilter.entityTypes.forEach(function(entityType) { - entitiesNamesList.push( - $translate.instant(types.entityTypeTranslations[entityType].typePlural) - ); - }); - entitiesText = entitiesNamesList.join(', '); - } else { - entitiesText = allEntitiesText; - } - var relationTypeText; - if (relationFilter.relationType && relationFilter.relationType.length) { - relationTypeText = "'" + relationFilter.relationType + "'"; - } else { - relationTypeText = anyRelationText; - } - var relationFilterDisplayValue = $translate.instant('alias.filter-type-relations-query-description', - { - entities: entitiesText, - relationType: relationTypeText, - direction: directionText, - rootEntity: rootEntityText - } - ); - relationFiltersDisplayValues.push(relationFilterDisplayValue); - }); - scope.filterDisplayValue = relationFiltersDisplayValues.join(', '); - } else { - scope.filterDisplayValue = $translate.instant('alias.filter-type-relations-query-description', - { - entities: allEntitiesText, - relationType: anyRelationText, - direction: directionText, - rootEntity: rootEntityText - } - ); - } - break; - case types.aliasFilterType.assetSearchQuery.value: - case types.aliasFilterType.deviceSearchQuery.value: - case types.aliasFilterType.entityViewSearchQuery.value: - allEntitiesText = $translate.instant('alias.all-entities'); - anyRelationText = $translate.instant('alias.any-relation'); - if (scope.filter.rootStateEntity) { - rootEntityText = $translate.instant('alias.state-entity'); - } else { - rootEntityText = $translate.instant(types.entityTypeTranslations[scope.filter.rootEntity.entityType].type); - } - directionText = $translate.instant('relation.direction-type.' + scope.filter.direction); - var relationTypeText; - if (scope.filter.relationType && scope.filter.relationType.length) { - relationTypeText = "'" + scope.filter.relationType + "'"; - } else { - relationTypeText = anyRelationText; - } - - var translationValues = { - relationType: relationTypeText, - direction: directionText, - rootEntity: rootEntityText - } - - if (scope.filter.type == types.aliasFilterType.assetSearchQuery.value) { - var assetTypesQuoted = []; - scope.filter.assetTypes.forEach(function(assetType) { - assetTypesQuoted.push("'"+assetType+"'"); - }); - var assetTypesText = assetTypesQuoted.join(', '); - translationValues.assetTypes = assetTypesText; - scope.filterDisplayValue = $translate.instant('alias.filter-type-asset-search-query-description', - translationValues - ); - } else if (scope.filter.type == types.aliasFilterType.deviceSearchQuery.value) { - var deviceTypesQuoted = []; - scope.filter.deviceTypes.forEach(function(deviceType) { - deviceTypesQuoted.push("'"+deviceType+"'"); - }); - var deviceTypesText = deviceTypesQuoted.join(', '); - translationValues.deviceTypes = deviceTypesText; - scope.filterDisplayValue = $translate.instant('alias.filter-type-device-search-query-description', - translationValues - ); - } else if (scope.filter.type == types.aliasFilterType.entityViewSearchQuery.value) { - var entityViewTypesQuoted = []; - scope.filter.entityViewTypes.forEach(function(entityViewType) { - entityViewTypesQuoted.push("'"+entityViewType+"'"); - }); - var entityViewTypesText = entityViewTypesQuoted.join(', '); - translationValues.entityViewTypes = entityViewTypesText; - scope.filterDisplayValue = $translate.instant('alias.filter-type-entity-view-search-query-description', - translationValues - ); - } - break; - default: - scope.filterDisplayValue = scope.filter.type; - break; - } - } else { - scope.filterDisplayValue = ''; - } - } - - ngModelCtrl.$render = function () { - if (ngModelCtrl.$viewValue) { - scope.filter = ngModelCtrl.$viewValue; - } else { - scope.filter = null; - } - } - - $compile(element.contents())(scope); - - } - - return { - restrict: "E", - require: "^ngModel", - link: linker, - scope: true - }; - -} diff --git a/ui/src/app/entity/entity-filter-view.scss b/ui/src/app/entity/entity-filter-view.scss deleted file mode 100644 index 538004a52e..0000000000 --- a/ui/src/app/entity/entity-filter-view.scss +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-entity-filter-view { - .entity-filter-empty { - font-size: 14px; - line-height: 16px; - color: rgba(221, 44, 0, .87); - } - - .entity-filter-type { - font-size: 14px; - line-height: 16px; - color: rgba(0, 0, 0, .570588); - } - - .entity-filter-value { - font-size: 14px; - line-height: 16px; - color: rgba(0, 0, 0, .570588); - } -} diff --git a/ui/src/app/entity/entity-filter-view.tpl.html b/ui/src/app/entity/entity-filter-view.tpl.html deleted file mode 100644 index cbc8f2af0a..0000000000 --- a/ui/src/app/entity/entity-filter-view.tpl.html +++ /dev/null @@ -1,23 +0,0 @@ - -
    -
    alias.no-entity-filter-specified
    -
    -
    {{ filterDisplayValue }}
    -
    -
    diff --git a/ui/src/app/entity/entity-filter.directive.js b/ui/src/app/entity/entity-filter.directive.js deleted file mode 100644 index 39e3892ff9..0000000000 --- a/ui/src/app/entity/entity-filter.directive.js +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import entityFilterTemplate from './entity-filter.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -import './entity-filter.scss'; - -/*@ngInject*/ -export default function EntityFilterDirective($compile, $templateCache, $q, $document, $mdDialog, types, entityService) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - - var template = $templateCache.get(entityFilterTemplate); - element.html(template); - - scope.ngModelCtrl = ngModelCtrl; - scope.types = types; - scope.aliasFilterTypes = entityService.getAliasFilterTypesByEntityTypes(scope.allowedEntityTypes); - - scope.$watch('filter.type', function (newType, prevType) { - if (newType && newType != prevType) { - updateFilter(); - } - }); - - function updateFilter() { - var filter = {}; - filter.type = scope.filter.type; - filter.resolveMultiple = true; - switch (filter.type) { - case types.aliasFilterType.singleEntity.value: - filter.singleEntity = null; - filter.resolveMultiple = false; - break; - case types.aliasFilterType.entityList.value: - filter.entityType = null; - filter.entityList = []; - break; - case types.aliasFilterType.entityName.value: - filter.entityType = null; - filter.entityNameFilter = ''; - break; - case types.aliasFilterType.stateEntity.value: - filter.stateEntityParamName = null; - filter.defaultStateEntity = null; - filter.resolveMultiple = false; - break; - case types.aliasFilterType.assetType.value: - filter.assetType = null; - filter.assetNameFilter = ''; - break; - case types.aliasFilterType.deviceType.value: - filter.deviceType = null; - filter.deviceNameFilter = ''; - break; - case types.aliasFilterType.entityViewType.value: - filter.entityViewType = null; - filter.entityViewNameFilter = ''; - break; - case types.aliasFilterType.relationsQuery.value: - case types.aliasFilterType.assetSearchQuery.value: - case types.aliasFilterType.deviceSearchQuery.value: - case types.aliasFilterType.entityViewSearchQuery.value: - filter.rootStateEntity = false; - filter.stateEntityParamName = null; - filter.defaultStateEntity = null; - filter.rootEntity = null; - filter.direction = types.entitySearchDirection.from; - filter.maxLevel = 1; - filter.fetchLastLevelOnly = false; - if (filter.type === types.aliasFilterType.relationsQuery.value) { - filter.filters = []; - } else if (filter.type === types.aliasFilterType.assetSearchQuery.value) { - filter.relationType = null; - filter.assetTypes = []; - } else if (filter.type === types.aliasFilterType.deviceSearchQuery.value) { - filter.relationType = null; - filter.deviceTypes = []; - } else if (filter.type === types.aliasFilterType.entityViewSearchQuery.value) { - filter.relationType = null; - filter.entityViewTypes = []; - } - break; - } - scope.filter = filter; - } - - scope.$watch('filter', function () { - scope.updateView(); - }); - - scope.updateView = function() { - ngModelCtrl.$setViewValue(scope.filter); - } - - ngModelCtrl.$render = function () { - if (ngModelCtrl.$viewValue) { - scope.filter = ngModelCtrl.$viewValue; - } else { - scope.filter = { - type: null, - resolveMultiple: false - } - } - } - - $compile(element.contents())(scope); - - } - - return { - restrict: "E", - require: "^ngModel", - link: linker, - scope: { - theForm: '=', - allowedEntityTypes: '=?' - } - }; - -} diff --git a/ui/src/app/entity/entity-filter.scss b/ui/src/app/entity/entity-filter.scss deleted file mode 100644 index b672555ada..0000000000 --- a/ui/src/app/entity/entity-filter.scss +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-entity-filter { - #relationsQueryFilter { - padding-top: 20px; - - tb-entity-select { - min-height: 92px; - } - } - - .tb-root-state-entity-switch { - padding-left: 10px; - - .root-state-entity-switch { - margin: 0; - } - - .root-state-entity-label { - margin: 5px 0; - } - } -} diff --git a/ui/src/app/entity/entity-filter.tpl.html b/ui/src/app/entity/entity-filter.tpl.html deleted file mode 100644 index bfc63a7d24..0000000000 --- a/ui/src/app/entity/entity-filter.tpl.html +++ /dev/null @@ -1,429 +0,0 @@ - -
    - - - - - {{type.name | translate}} - - -
    -
    alias.filter-type-required
    -
    -
    -
    - - -
    -
    - - - - -
    -
    - - - - - -
    -
    entity.entity-name-filter-required
    -
    -
    -
    -
    - - - - -
    - - - -
    -
    -
    - - - - - - -
    -
    - - - - - - -
    -
    - - - - - - -
    -
    - -
    - - - -
    -
    - - -
    -
    - - - - -
    - - - -
    -
    -
    -
    - - - -
    -
    -
    - - - - - {{ ('relation.search-direction.' + direction) | translate}} - - - - - - - -
    -
    relation.relation-filters
    - - -
    -
    - -
    - - - -
    -
    - - -
    -
    - - - - -
    - - - -
    -
    -
    -
    - - - -
    -
    -
    - - - - - {{ ('relation.search-direction.' + direction) | translate}} - - - - - - - -
    -
    relation.relation-type
    - - -
    asset.asset-types
    - - -
    -
    - -
    - - - -
    -
    - - -
    -
    - - - - -
    - - - -
    -
    -
    -
    - - - -
    -
    -
    - - - - - {{ ('relation.search-direction.' + direction) | translate}} - - - - - - - -
    -
    relation.relation-type
    - - -
    device.device-types
    - - -
    -
    - -
    - - - -
    -
    - - -
    -
    - - - - -
    - - - -
    -
    -
    -
    - - - -
    -
    -
    - - - - - {{ ('relation.search-direction.' + direction) | translate}} - - - - - - - -
    -
    relation.relation-type
    - - -
    entity-view.entity-view-types
    - - -
    -
    diff --git a/ui/src/app/entity/entity-list.directive.js b/ui/src/app/entity/entity-list.directive.js deleted file mode 100644 index 3fccecc348..0000000000 --- a/ui/src/app/entity/entity-list.directive.js +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import entityListTemplate from './entity-list.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -import './entity-list.scss'; - -/*@ngInject*/ -export default function EntityListDirective($compile, $templateCache, $q, $mdUtil, entityService) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - - var template = $templateCache.get(entityListTemplate); - element.html(template); - - scope.ngModelCtrl = ngModelCtrl; - - scope.$watch('tbRequired', function () { - scope.updateValidity(); - }); - - scope.fetchEntities = function(searchText, limit) { - var deferred = $q.defer(); - entityService.getEntitiesByNameFilter(scope.entityType, searchText, limit, {ignoreLoading: true}).then( - function success(result) { - if (result) { - deferred.resolve(result); - } else { - deferred.resolve([]); - } - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - - scope.updateValidity = function() { - var value = ngModelCtrl.$viewValue; - var valid = !scope.tbRequired || value && value.length > 0; - ngModelCtrl.$setValidity('entityList', valid); - } - - ngModelCtrl.$render = function () { - destroyWatchers(); - var value = ngModelCtrl.$viewValue; - scope.entityList = []; - if (value && value.length > 0) { - entityService.getEntities(scope.entityType, value).then(function (entities) { - scope.entityList = entities; - initWatchers(); - }); - } else { - initWatchers(); - } - } - - function initWatchers() { - scope.entityTypeDeregistration = scope.$watch('entityType', function (newEntityType, prevEntityType) { - if (!angular.equals(newEntityType, prevEntityType)) { - scope.entityList = []; - } - }); - scope.entityListDeregistration = scope.$watch('entityList', function () { - var ids = []; - if (scope.entityList && scope.entityList.length > 0) { - for (var i=0;i - - - - {{item.name}} - - - entity.no-entities-matching - - - - - {{$chip.name}} - - - - - \ No newline at end of file diff --git a/ui/src/app/entity/entity-select.directive.js b/ui/src/app/entity/entity-select.directive.js deleted file mode 100644 index e93a769eec..0000000000 --- a/ui/src/app/entity/entity-select.directive.js +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './entity-select.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import entitySelectTemplate from './entity-select.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function EntitySelect($compile, $templateCache, entityService, types) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - var template = $templateCache.get(entitySelectTemplate); - element.html(template); - - scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false; - scope.entityTypeCurrentTenant = types.aliasEntityType.current_tenant; - - var entityTypes = entityService.prepareAllowedEntityTypesList(scope.allowedEntityTypes, scope.useAliasEntityTypes); - - var entityTypeKeys = Object.keys(entityTypes); - - if (entityTypeKeys.length === 1) { - scope.displayEntityTypeSelect = false; - scope.defaultEntityType = entityTypes[entityTypeKeys[0]]; - } else { - scope.displayEntityTypeSelect = true; - } - - scope.model = { - entityType: scope.defaultEntityType - }; - - scope.updateView = function () { - if (!scope.disabled) { - var value = ngModelCtrl.$viewValue; - if (scope.model && scope.model.entityType && - (scope.model.entityId || scope.model.entityType === scope.entityTypeCurrentTenant)) { - if (!value) { - value = {}; - } - value.entityType = scope.model.entityType; - value.id = scope.model.entityId; - ngModelCtrl.$setViewValue(value); - } else { - ngModelCtrl.$setViewValue(null); - } - } - } - - ngModelCtrl.$render = function () { - destroyWatchers(); - if (ngModelCtrl.$viewValue) { - var value = ngModelCtrl.$viewValue; - scope.model.entityType = value.entityType; - scope.model.entityId = value.id; - } else { - scope.model.entityType = scope.defaultEntityType; - scope.model.entityId = null; - } - initWatchers(); - } - - function initWatchers() { - scope.entityTypeDeregistration = scope.$watch('model.entityType', function (newVal, prevVal) { - if (!angular.equals(newVal, prevVal)) { - scope.updateView(); - } - }); - - scope.entityIdDeregistration = scope.$watch('model.entityId', function (newVal, prevVal) { - if (!angular.equals(newVal, prevVal)) { - scope.updateView(); - } - }); - - scope.disabledDeregistration = scope.$watch('disabled', function (newVal, prevVal) { - if (!angular.equals(newVal, prevVal)) { - scope.updateView(); - } - }); - } - - function destroyWatchers() { - if (scope.entityTypeDeregistration) { - scope.entityTypeDeregistration(); - scope.entityTypeDeregistration = null; - } - if (scope.entityIdDeregistration) { - scope.entityIdDeregistration(); - scope.entityIdDeregistration = null; - } - if (scope.disabledDeregistration) { - scope.disabledDeregistration(); - scope.disabledDeregistration = null; - } - } - - $compile(element.contents())(scope); - } - - return { - restrict: "E", - require: "^ngModel", - link: linker, - scope: { - theForm: '=?', - tbRequired: '=?', - disabled:'=ngDisabled', - allowedEntityTypes: "=?", - useAliasEntityTypes: "=?" - } - }; -} diff --git a/ui/src/app/entity/entity-select.scss b/ui/src/app/entity/entity-select.scss deleted file mode 100644 index a622807890..0000000000 --- a/ui/src/app/entity/entity-select.scss +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* -.tb-entity-select { -} -*/ diff --git a/ui/src/app/entity/entity-select.tpl.html b/ui/src/app/entity/entity-select.tpl.html deleted file mode 100644 index 8800f9760f..0000000000 --- a/ui/src/app/entity/entity-select.tpl.html +++ /dev/null @@ -1,35 +0,0 @@ - -
    - - - - -
    diff --git a/ui/src/app/entity/entity-subtype-autocomplete.directive.js b/ui/src/app/entity/entity-subtype-autocomplete.directive.js deleted file mode 100644 index af590d181d..0000000000 --- a/ui/src/app/entity/entity-subtype-autocomplete.directive.js +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './entity-subtype-autocomplete.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import entitySubtypeAutocompleteTemplate from './entity-subtype-autocomplete.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function EntitySubtypeAutocomplete($compile, $templateCache, $q, $filter, assetService, deviceService, entityViewService, types) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - var template = $templateCache.get(entitySubtypeAutocompleteTemplate); - element.html(template); - - scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false; - scope.subType = null; - scope.subTypeSearchText = ''; - scope.entitySubtypes = null; - - var comparator = function(actual, expected) { - if (angular.isUndefined(actual)) { - return false; - } - if ((actual === null) || (expected === null)) { - return actual === expected; - } - return actual.startsWith(expected); - }; - - scope.fetchSubTypes = function(searchText) { - var deferred = $q.defer(); - loadSubTypes().then( - function success(subTypes) { - var result = $filter('filter')(subTypes, {'$': searchText}, comparator); - if (result && result.length) { - if (searchText && searchText.length && result.indexOf(searchText) === -1) { - result.push(searchText); - } - result.sort(); - deferred.resolve(result); - } else { - deferred.resolve([searchText]); - } - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - - scope.subTypeSearchTextChanged = function() { - //scope.subType = scope.subTypeSearchText; - } - - scope.updateView = function () { - if (!scope.disabled) { - ngModelCtrl.$setViewValue(scope.subType); - } - } - - ngModelCtrl.$render = function () { - scope.subType = ngModelCtrl.$viewValue; - } - - scope.$watch('entityType', function () { - load(); - }); - - scope.$watch('subType', function (newValue, prevValue) { - if (!angular.equals(newValue, prevValue)) { - scope.updateView(); - } - }); - - scope.$watch('disabled', function () { - scope.updateView(); - }); - - function loadSubTypes() { - var deferred = $q.defer(); - if (!scope.entitySubtypes) { - var entitySubtypesPromise; - if (scope.entityType == types.entityType.asset) { - entitySubtypesPromise = assetService.getAssetTypes({ignoreLoading: true}); - } else if (scope.entityType == types.entityType.device) { - entitySubtypesPromise = deviceService.getDeviceTypes({ignoreLoading: true}); - } else if (scope.entityType == types.entityType.entityView) { - entitySubtypesPromise = entityViewService.getEntityViewTypes({ignoreLoading: true}); - } - if (entitySubtypesPromise) { - entitySubtypesPromise.then( - function success(types) { - scope.entitySubtypes = []; - types.forEach(function (type) { - scope.entitySubtypes.push(type.type); - }); - deferred.resolve(scope.entitySubtypes); - }, - function fail() { - deferred.reject(); - } - ); - } else { - deferred.reject(); - } - } else { - deferred.resolve(scope.entitySubtypes); - } - return deferred.promise; - } - - function load() { - if (scope.entityType == types.entityType.asset) { - scope.selectEntitySubtypeText = 'asset.select-asset-type'; - scope.entitySubtypeText = 'asset.asset-type'; - scope.entitySubtypeRequiredText = 'asset.asset-type-required'; - scope.$on('assetSaved', function() { - scope.entitySubtypes = null; - }); - } else if (scope.entityType == types.entityType.device) { - scope.selectEntitySubtypeText = 'device.select-device-type'; - scope.entitySubtypeText = 'device.device-type'; - scope.entitySubtypeRequiredText = 'device.device-type-required'; - scope.$on('deviceSaved', function() { - scope.entitySubtypes = null; - }); - } else if (scope.entityType == types.entityType.entityView) { - scope.selectEntitySubtypeText = 'entity-view.select-entity-view-type'; - scope.entitySubtypeText = 'entity-view.entity-view-type'; - scope.entitySubtypeRequiredText = 'entity-view.entity-view-type-required'; - scope.$on('entityViewSaved', function() { - scope.entitySubtypes = null; - }); - } - } - - $compile(element.contents())(scope); - } - - return { - restrict: "E", - require: "^ngModel", - link: linker, - scope: { - theForm: '=?', - tbRequired: '=?', - disabled:'=ngDisabled', - entityType: "=" - } - }; -} diff --git a/ui/src/app/entity/entity-subtype-autocomplete.scss b/ui/src/app/entity/entity-subtype-autocomplete.scss deleted file mode 100644 index 26c1525e73..0000000000 --- a/ui/src/app/entity/entity-subtype-autocomplete.scss +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-entity-subtype-autocomplete { - .tb-entity-subtype-item { - display: block; - height: 48px; - } - - li { - height: auto !important; - white-space: normal !important; - } -} diff --git a/ui/src/app/entity/entity-subtype-autocomplete.tpl.html b/ui/src/app/entity/entity-subtype-autocomplete.tpl.html deleted file mode 100644 index 33d5c47299..0000000000 --- a/ui/src/app/entity/entity-subtype-autocomplete.tpl.html +++ /dev/null @@ -1,43 +0,0 @@ - - - -
    - {{item}} -
    -
    -
    -
    {{ entitySubtypeRequiredText }}
    -
    -
    diff --git a/ui/src/app/entity/entity-subtype-list.directive.js b/ui/src/app/entity/entity-subtype-list.directive.js deleted file mode 100644 index 83e585eff2..0000000000 --- a/ui/src/app/entity/entity-subtype-list.directive.js +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import entitySubtypeListTemplate from './entity-subtype-list.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -import './entity-subtype-list.scss'; - -/*@ngInject*/ -export default function EntitySubtypeListDirective($compile, $templateCache, $q, $mdUtil, $translate, $filter, types, assetService, deviceService, entityViewService) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - - var template = $templateCache.get(entitySubtypeListTemplate); - element.html(template); - - scope.ngModelCtrl = ngModelCtrl; - - - scope.entitySubtypesList = []; - scope.entitySubtypes = null; - - if (scope.entityType == types.entityType.asset) { - scope.placeholder = scope.tbRequired ? $translate.instant('asset.enter-asset-type') - : $translate.instant('asset.any-asset'); - scope.secondaryPlaceholder = '+' + $translate.instant('asset.asset-type'); - scope.noSubtypesMathingText = 'asset.no-asset-types-matching'; - scope.subtypeListEmptyText = 'asset.asset-type-list-empty'; - } else if (scope.entityType == types.entityType.device) { - scope.placeholder = scope.tbRequired ? $translate.instant('device.enter-device-type') - : $translate.instant('device.any-device'); - scope.secondaryPlaceholder = '+' + $translate.instant('device.device-type'); - scope.noSubtypesMathingText = 'device.no-device-types-matching'; - scope.subtypeListEmptyText = 'device.device-type-list-empty'; - } else if (scope.entityType == types.entityType.entityView) { - scope.placeholder = scope.tbRequired ? $translate.instant('entity-view.enter-entity-view-type') - : $translate.instant('entity-view.any-entity-view'); - scope.secondaryPlaceholder = '+' + $translate.instant('entity-view.entity-view-type'); - scope.noSubtypesMathingText = 'entity-view.no-entity-view-types-matching'; - scope.subtypeListEmptyText = 'entity-view.entity-view-type-list-empty'; - } - - scope.$watch('tbRequired', function () { - scope.updateValidity(); - }); - - scope.fetchEntitySubtypes = function(searchText) { - var deferred = $q.defer(); - loadSubTypes().then( - function success(subTypes) { - var result = $filter('filter')(subTypes, {'$': searchText}); - if (result && result.length) { - deferred.resolve(result); - } else { - deferred.resolve([searchText]); - } - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - - scope.updateValidity = function() { - var value = ngModelCtrl.$viewValue; - var valid = !scope.tbRequired || value && value.length > 0; - ngModelCtrl.$setValidity('entitySubtypesList', valid); - } - - ngModelCtrl.$render = function () { - scope.entitySubtypesList = ngModelCtrl.$viewValue; - if (!scope.entitySubtypesList) { - scope.entitySubtypesList = []; - } - } - - scope.$watch('entitySubtypesList', function () { - ngModelCtrl.$setViewValue(scope.entitySubtypesList); - scope.updateValidity(); - }, true); - - function loadSubTypes() { - var deferred = $q.defer(); - if (!scope.entitySubtypes) { - var entitySubtypesPromise; - if (scope.entityType == types.entityType.asset) { - entitySubtypesPromise = assetService.getAssetTypes({ignoreLoading: true}); - } else if (scope.entityType == types.entityType.device) { - entitySubtypesPromise = deviceService.getDeviceTypes({ignoreLoading: true}); - } else if (scope.entityType == types.entityType.entityView) { - entitySubtypesPromise = entityViewService.getEntityViewTypes({ignoreLoading: true}); - } - if (entitySubtypesPromise) { - entitySubtypesPromise.then( - function success(types) { - scope.entitySubtypes = []; - types.forEach(function (type) { - scope.entitySubtypes.push(type.type); - }); - deferred.resolve(scope.entitySubtypes); - }, - function fail() { - deferred.reject(); - } - ); - } else { - deferred.reject(); - } - } else { - deferred.resolve(scope.entitySubtypes); - } - return deferred.promise; - } - - $compile(element.contents())(scope); - - $mdUtil.nextTick(function(){ - var inputElement = angular.element('input', element); - inputElement.on('blur', function() { - scope.inputTouched = true; - } ); - }); - - } - - return { - restrict: "E", - require: "^ngModel", - link: linker, - scope: { - disabled:'=ngDisabled', - tbRequired: '=?', - entityType: "=" - } - }; - -} diff --git a/ui/src/app/entity/entity-subtype-list.scss b/ui/src/app/entity/entity-subtype-list.scss deleted file mode 100644 index 59f14a4f54..0000000000 --- a/ui/src/app/entity/entity-subtype-list.scss +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* -.tb-entity-subtype-list { - #entity_subtype_list_chips { - .md-chips { - padding-bottom: 1px; - } - } - .tb-error-messages { - margin-top: -11px; - height: 35px; - .tb-error-message { - padding-left: 1px; - } - } -} -*/ diff --git a/ui/src/app/entity/entity-subtype-list.tpl.html b/ui/src/app/entity/entity-subtype-list.tpl.html deleted file mode 100644 index 42f9e4151e..0000000000 --- a/ui/src/app/entity/entity-subtype-list.tpl.html +++ /dev/null @@ -1,53 +0,0 @@ - -
    - - - - {{item}} - - - {{noSubtypesMathingText}} - - - - - {{$chip}} - - - - -
    diff --git a/ui/src/app/entity/entity-subtype-select.directive.js b/ui/src/app/entity/entity-subtype-select.directive.js deleted file mode 100644 index f587fb9d66..0000000000 --- a/ui/src/app/entity/entity-subtype-select.directive.js +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './entity-subtype-select.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import entitySubtypeSelectTemplate from './entity-subtype-select.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function EntitySubtypeSelect($compile, $templateCache, $translate, assetService, deviceService, entityViewService, types) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - var template = $templateCache.get(entitySubtypeSelectTemplate); - element.html(template); - - if (angular.isDefined(attrs.hideLabel)) { - scope.showLabel = false; - } else { - scope.showLabel = true; - } - - scope.ngModelCtrl = ngModelCtrl; - - scope.entitySubtypes = []; - - scope.subTypeName = function(subType) { - if (subType && subType.length) { - if (scope.typeTranslatePrefix) { - return $translate.instant(scope.typeTranslatePrefix + '.' + subType); - } else { - return subType; - } - } else { - return $translate.instant('entity.all-subtypes'); - } - } - - scope.$watch('entityType', function () { - load(); - }); - - scope.$watch('entitySubtype', function (newValue, prevValue) { - if (!angular.equals(newValue, prevValue)) { - scope.updateView(); - } - }); - - scope.updateView = function () { - ngModelCtrl.$setViewValue(scope.entitySubtype); - }; - - ngModelCtrl.$render = function () { - scope.entitySubtype = ngModelCtrl.$viewValue; - }; - - function loadSubTypes() { - scope.entitySubtypes = []; - var entitySubtypesPromise; - if (scope.entityType == types.entityType.asset) { - entitySubtypesPromise = assetService.getAssetTypes({ignoreLoading: true}); - } else if (scope.entityType == types.entityType.device) { - entitySubtypesPromise = deviceService.getDeviceTypes({ignoreLoading: true}); - } else if (scope.entityType == types.entityType.entityView) { - entitySubtypesPromise = entityViewService.getEntityViewTypes({ignoreLoading: true}); - } - if (entitySubtypesPromise) { - entitySubtypesPromise.then( - function success(types) { - scope.entitySubtypes.push(''); - types.forEach(function(type) { - scope.entitySubtypes.push(type.type); - }); - if (scope.entitySubtypes.indexOf(scope.entitySubtype) == -1) { - scope.entitySubtype = ''; - } - }, - function fail() {} - ); - } - - } - - function load() { - if (scope.entityType == types.entityType.asset) { - scope.entitySubtypeTitle = 'asset.asset-type'; - scope.entitySubtypeRequiredText = 'asset.asset-type-required'; - } else if (scope.entityType == types.entityType.device) { - scope.entitySubtypeTitle = 'device.device-type'; - scope.entitySubtypeRequiredText = 'device.device-type-required'; - } else if (scope.entityType == types.entityType.entityView) { - scope.entitySubtypeTitle = 'entity-view.entity-view-type'; - scope.entitySubtypeRequiredText = 'entity-view.entity-view-type-required'; - } - scope.entitySubtypes.length = 0; - if (scope.entitySubtypesList && scope.entitySubtypesList.length) { - scope.entitySubtypesList.forEach(function(subType) { - scope.entitySubtypes.push(subType); - }); - } else { - loadSubTypes(); - if (scope.entityType == types.entityType.asset) { - scope.$on('assetSaved', function() { - loadSubTypes(); - }); - } else if (scope.entityType == types.entityType.device) { - scope.$on('deviceSaved', function() { - loadSubTypes(); - }); - } else if (scope.entityType == types.entityType.entityView) { - scope.$on('entityViewSaved', function() { - loadSubTypes(); - }); - } - } - } - - $compile(element.contents())(scope); - } - - return { - restrict: "E", - require: "^ngModel", - link: linker, - scope: { - theForm: '=?', - entityType: "=", - entitySubtypesList: "=?", - typeTranslatePrefix: "@?" - } - }; -} diff --git a/ui/src/app/entity/entity-subtype-select.scss b/ui/src/app/entity/entity-subtype-select.scss deleted file mode 100644 index 2f5f4c1bd2..0000000000 --- a/ui/src/app/entity/entity-subtype-select.scss +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -md-select.tb-entity-subtype-select { - min-width: 200px; -} diff --git a/ui/src/app/entity/entity-subtype-select.tpl.html b/ui/src/app/entity/entity-subtype-select.tpl.html deleted file mode 100644 index 94f6284733..0000000000 --- a/ui/src/app/entity/entity-subtype-select.tpl.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - {{ subTypeName(subType) }} - - -
    -
    {{ entitySubtypeRequiredText }}
    -
    -
    diff --git a/ui/src/app/entity/entity-type-list.directive.js b/ui/src/app/entity/entity-type-list.directive.js deleted file mode 100644 index d1fdcaea2f..0000000000 --- a/ui/src/app/entity/entity-type-list.directive.js +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import entityTypeListTemplate from './entity-type-list.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -import './entity-type-list.scss'; - -/*@ngInject*/ -export default function EntityTypeListDirective($compile, $templateCache, $q, $mdUtil, $translate, $filter, types, entityService) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - - var template = $templateCache.get(entityTypeListTemplate); - element.html(template); - - scope.ngModelCtrl = ngModelCtrl; - - scope.placeholder = scope.tbRequired ? $translate.instant('entity.enter-entity-type') - : $translate.instant('entity.any-entity'); - scope.secondaryPlaceholder = '+' + $translate.instant('entity.entity-type'); - - var entityTypes; - - if (scope.ignoreAuthorityFilter && scope.allowedEntityTypes - && scope.allowedEntityTypes.length) { - entityTypes = {}; - scope.allowedEntityTypes.forEach((entityTypeValue) => { - var entityType = entityTypeFromValue(entityTypeValue); - if (entityType) { - entityTypes[entityType] = entityTypeValue; - } - }); - } else { - entityTypes = entityService.prepareAllowedEntityTypesList(scope.allowedEntityTypes); - } - - function entityTypeFromValue(entityTypeValue) { - for (var entityType in types.entityType) { - if (types.entityType[entityType] === entityTypeValue) { - return entityType; - } - } - return null; - } - - scope.entityTypesList = []; - for (var type in entityTypes) { - var entityTypeInfo = {}; - entityTypeInfo.value = entityTypes[type]; - entityTypeInfo.name = $translate.instant(types.entityTypeTranslations[entityTypeInfo.value].type) + ''; - scope.entityTypesList.push(entityTypeInfo); - } - - scope.$watch('tbRequired', function () { - scope.updateValidity(); - }); - - scope.fetchEntityTypes = function(searchText) { - var deferred = $q.defer(); - var entityTypes = $filter('filter')(scope.entityTypesList, {name: searchText}); - deferred.resolve(entityTypes); - return deferred.promise; - } - - scope.updateValidity = function() { - var value = ngModelCtrl.$viewValue; - var valid = !scope.tbRequired || value && value.length > 0; - ngModelCtrl.$setValidity('entityTypeList', valid); - } - - ngModelCtrl.$render = function () { - if (scope.entityTypeListWatch) { - scope.entityTypeListWatch(); - scope.entityTypeListWatch = null; - } - var entityTypeList = []; - var value = ngModelCtrl.$viewValue; - if (value && value.length) { - value.forEach(function(type) { - var entityTypeInfo = {}; - entityTypeInfo.value = type; - entityTypeInfo.name = $translate.instant(types.entityTypeTranslations[entityTypeInfo.value].type) + ''; - entityTypeList.push(entityTypeInfo); - }); - } - scope.entityTypeList = entityTypeList; - scope.entityTypeListWatch = scope.$watch('entityTypeList', function (newVal, prevVal) { - if (!angular.equals(newVal, prevVal)) { - updateEntityTypeList(); - } - }, true); - } - - function updateEntityTypeList() { - var values = ngModelCtrl.$viewValue; - if (!values) { - values = []; - ngModelCtrl.$setViewValue(values); - } else { - values.length = 0; - } - if (scope.entityTypeList && scope.entityTypeList.length) { - scope.entityTypeList.forEach(function (entityType) { - values.push(entityType.value); - }); - } - scope.updateValidity(); - } - - $compile(element.contents())(scope); - - $mdUtil.nextTick(function(){ - var inputElement = angular.element('input', element); - inputElement.on('blur', function() { - scope.inputTouched = true; - } ); - }); - - } - - return { - restrict: "E", - require: "^ngModel", - link: linker, - scope: { - disabled:'=ngDisabled', - tbRequired: '=?', - allowedEntityTypes: '=?', - ignoreAuthorityFilter: '=?' - } - }; - -} diff --git a/ui/src/app/entity/entity-type-list.scss b/ui/src/app/entity/entity-type-list.scss deleted file mode 100644 index 069bb985f8..0000000000 --- a/ui/src/app/entity/entity-type-list.scss +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* -.tb-entity-type-list { - #entity_type_list_chips { - .md-chips { - padding-bottom: 1px; - } - } - .tb-error-messages { - margin-top: -11px; - height: 35px; - .tb-error-message { - padding-left: 1px; - } - } -} -*/ diff --git a/ui/src/app/entity/entity-type-list.tpl.html b/ui/src/app/entity/entity-type-list.tpl.html deleted file mode 100644 index f9c31665dd..0000000000 --- a/ui/src/app/entity/entity-type-list.tpl.html +++ /dev/null @@ -1,52 +0,0 @@ - -
    - - - - {{item.name}} - - - entity.no-entity-types-matching - - - - - {{$chip.name}} - - - - -
    diff --git a/ui/src/app/entity/entity-type-select.directive.js b/ui/src/app/entity/entity-type-select.directive.js deleted file mode 100644 index 0b232a0c71..0000000000 --- a/ui/src/app/entity/entity-type-select.directive.js +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './entity-type-select.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import entityTypeSelectTemplate from './entity-type-select.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function EntityTypeSelect($compile, $templateCache, utils, entityService, userService, types) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - var template = $templateCache.get(entityTypeSelectTemplate); - element.html(template); - - scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false; - - if (angular.isDefined(attrs.hideLabel)) { - scope.showLabel = false; - } else { - scope.showLabel = true; - } - - scope.ngModelCtrl = ngModelCtrl; - - scope.entityTypes = entityService.prepareAllowedEntityTypesList(scope.allowedEntityTypes, scope.useAliasEntityTypes); - - scope.typeName = function(type) { - return type ? types.entityTypeTranslations[type].type : ''; - } - - scope.updateValidity = function () { - var value = ngModelCtrl.$viewValue; - var valid = angular.isDefined(value) && value != null; - ngModelCtrl.$setValidity('entityType', valid); - }; - - scope.$watch('entityType', function (newValue, prevValue) { - if (!angular.equals(newValue, prevValue)) { - scope.updateView(); - } - }); - - scope.updateView = function () { - ngModelCtrl.$setViewValue(scope.entityType); - scope.updateValidity(); - }; - - ngModelCtrl.$render = function () { - if (ngModelCtrl.$viewValue) { - scope.entityType = ngModelCtrl.$viewValue; - } - }; - - $compile(element.contents())(scope); - } - - return { - restrict: "E", - require: "^ngModel", - link: linker, - scope: { - theForm: '=?', - tbRequired: '=?', - disabled:'=ngDisabled', - allowedEntityTypes: "=?", - useAliasEntityTypes: "=?" - } - }; -} diff --git a/ui/src/app/entity/entity-type-select.scss b/ui/src/app/entity/entity-type-select.scss deleted file mode 100644 index c94827702d..0000000000 --- a/ui/src/app/entity/entity-type-select.scss +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* -md-select.tb-entity-type-select { -} -*/ diff --git a/ui/src/app/entity/entity-type-select.tpl.html b/ui/src/app/entity/entity-type-select.tpl.html deleted file mode 100644 index e4be909d43..0000000000 --- a/ui/src/app/entity/entity-type-select.tpl.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - {{typeName(type) | translate}} - - -
    -
    entity.type-required
    -
    -
    diff --git a/ui/src/app/entity/index.js b/ui/src/app/entity/index.js deleted file mode 100644 index ba6fc6b276..0000000000 --- a/ui/src/app/entity/index.js +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import EntityAliasesController from './alias/entity-aliases.controller'; -import EntityAliasDialogController from './alias/entity-alias-dialog.controller'; -import EntityTypeSelectDirective from './entity-type-select.directive'; -import EntityTypeListDirective from './entity-type-list.directive'; -import EntitySubtypeListDirective from './entity-subtype-list.directive'; -import EntitySubtypeSelectDirective from './entity-subtype-select.directive'; -import EntitySubtypeAutocompleteDirective from './entity-subtype-autocomplete.directive'; -import EntityAutocompleteDirective from './entity-autocomplete.directive'; -import EntityListDirective from './entity-list.directive'; -import EntitySelectDirective from './entity-select.directive'; -import EntityFilterDirective from './entity-filter.directive'; -import EntityFilterViewDirective from './entity-filter-view.directive'; -import AliasesEntitySelectPanelController from './alias/aliases-entity-select-panel.controller'; -import AliasesEntitySelectDirective from './alias/aliases-entity-select.directive'; -import AddAttributeDialogController from './attribute/add-attribute-dialog.controller'; -import AddWidgetToDashboardDialogController from './attribute/add-widget-to-dashboard-dialog.controller'; -import AttributeTableDirective from './attribute/attribute-table.directive'; -import RelationFiltersDirective from './relation/relation-filters.directive'; -import RelationTableDirective from './relation/relation-table.directive'; -import RelationTypeAutocompleteDirective from './relation/relation-type-autocomplete.directive'; - -export default angular.module('thingsboard.entity', []) - .controller('EntityAliasesController', EntityAliasesController) - .controller('EntityAliasDialogController', EntityAliasDialogController) - .controller('AliasesEntitySelectPanelController', AliasesEntitySelectPanelController) - .controller('AddAttributeDialogController', AddAttributeDialogController) - .controller('AddWidgetToDashboardDialogController', AddWidgetToDashboardDialogController) - .directive('tbEntityTypeSelect', EntityTypeSelectDirective) - .directive('tbEntityTypeList', EntityTypeListDirective) - .directive('tbEntitySubtypeList', EntitySubtypeListDirective) - .directive('tbEntitySubtypeSelect', EntitySubtypeSelectDirective) - .directive('tbEntitySubtypeAutocomplete', EntitySubtypeAutocompleteDirective) - .directive('tbEntityAutocomplete', EntityAutocompleteDirective) - .directive('tbEntityList', EntityListDirective) - .directive('tbEntitySelect', EntitySelectDirective) - .directive('tbEntityFilter', EntityFilterDirective) - .directive('tbEntityFilterView', EntityFilterViewDirective) - .directive('tbAliasesEntitySelect', AliasesEntitySelectDirective) - .directive('tbAttributeTable', AttributeTableDirective) - .directive('tbRelationFilters', RelationFiltersDirective) - .directive('tbRelationTable', RelationTableDirective) - .directive('tbRelationTypeAutocomplete', RelationTypeAutocompleteDirective) - .name; diff --git a/ui/src/app/entity/relation/relation-dialog.controller.js b/ui/src/app/entity/relation/relation-dialog.controller.js deleted file mode 100644 index d427b1d2c5..0000000000 --- a/ui/src/app/entity/relation/relation-dialog.controller.js +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import 'brace/ext/language_tools'; -import 'brace/ext/searchbox'; -import 'brace/mode/json'; -import 'brace/theme/github'; -import beautify from 'js-beautify'; - -import './relation-dialog.scss'; - -const js_beautify = beautify.js; - -/*@ngInject*/ -export default function RelationDialogController($scope, $mdDialog, types, entityRelationService, isAdd, direction, relation, showingCallback) { - - var vm = this; - - vm.types = types; - vm.isAdd = isAdd; - vm.direction = direction; - - showingCallback.onShowing = function(scope, element) { - updateEditorSize(element); - } - - vm.relationAdditionalInfoOptions = { - useWrapMode: false, - mode: 'json', - showGutter: false, - showPrintMargin: false, - theme: 'github', - advanced: { - enableSnippets: false, - enableBasicAutocompletion: false, - enableLiveAutocompletion: false - }, - onLoad: function (_ace) { - vm.editor = _ace; - } - }; - - vm.relation = relation; - if (vm.isAdd) { - vm.relation.type = types.entityRelationType.contains; - vm.targetEntityId = {}; - } else { - if (vm.direction == vm.types.entitySearchDirection.from) { - vm.targetEntityId = vm.relation.to; - } else { - vm.targetEntityId = vm.relation.from; - } - } - - vm.save = save; - vm.cancel = cancel; - - vm.additionalInfo = ''; - - if (vm.relation.additionalInfo) { - vm.additionalInfo = angular.toJson(vm.relation.additionalInfo); - vm.additionalInfo = js_beautify(vm.additionalInfo, {indent_size: 4}); - } - - $scope.$watch('vm.additionalInfo', () => { - $scope.theForm.$setValidity("additionalInfo", true); - }); - - function updateEditorSize(element) { - var newHeight = 200; - angular.element('#tb-relation-additional-info', element).height(newHeight.toString() + "px"); - vm.editor.resize(); - } - - function cancel() { - $mdDialog.cancel(); - } - - function save() { - if (vm.isAdd) { - if (vm.direction == vm.types.entitySearchDirection.from) { - vm.relation.to = vm.targetEntityId; - } else { - vm.relation.from = vm.targetEntityId; - } - } - $scope.theForm.$setPristine(); - - var valid = true; - if (vm.additionalInfo && vm.additionalInfo.length) { - try { - vm.relation.additionalInfo = angular.fromJson(vm.additionalInfo); - } catch(e) { - valid = false; - } - } else { - vm.relation.additionalInfo = null; - } - - $scope.theForm.$setValidity("additionalInfo", valid); - - if (valid) { - entityRelationService.saveRelation(vm.relation).then( - function success() { - $mdDialog.hide(); - } - ); - } - } - -} diff --git a/ui/src/app/entity/relation/relation-dialog.scss b/ui/src/app/entity/relation/relation-dialog.scss deleted file mode 100644 index 8062063b9e..0000000000 --- a/ui/src/app/entity/relation/relation-dialog.scss +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-relation-additional-info-panel { - height: 100%; - margin-left: 15px; - border: 1px solid #c0c0c0; - - #tb-relation-additional-info { - width: 100%; - min-width: 200px; - height: 100%; - min-height: 200px; - } -} diff --git a/ui/src/app/entity/relation/relation-dialog.tpl.html b/ui/src/app/entity/relation/relation-dialog.tpl.html deleted file mode 100644 index 2d1d878fb1..0000000000 --- a/ui/src/app/entity/relation/relation-dialog.tpl.html +++ /dev/null @@ -1,73 +0,0 @@ - - -
    - -
    -

    {{ vm.isAdd ? 'relation.add' : 'relation.edit'}}

    - - - - -
    -
    - - - -
    - -
    - - - {{(vm.direction == vm.types.entitySearchDirection.from ? - 'relation.to-entity' : 'relation.from-entity') | translate}} - - -
    relation.additional-info
    -
    -
    -
    -
    - -
    -
    -
    -
    - - - - {{ (vm.isAdd ? 'action.add' : 'action.save') | translate }} - - {{ 'action.cancel' | - translate }} - - -
    -
    diff --git a/ui/src/app/entity/relation/relation-filters.directive.js b/ui/src/app/entity/relation/relation-filters.directive.js deleted file mode 100644 index 17400c5ce3..0000000000 --- a/ui/src/app/entity/relation/relation-filters.directive.js +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './relation-filters.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import relationFiltersTemplate from './relation-filters.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function RelationFilters($compile, $templateCache) { - - return { - restrict: "E", - require: "^ngModel", - scope: { - allowedEntityTypes: '=?' - }, - link: linker - }; - - function linker( scope, element, attrs, ngModelCtrl ) { - - var template = $templateCache.get(relationFiltersTemplate); - element.html(template); - - scope.relationFilters = []; - - scope.addFilter = addFilter; - scope.removeFilter = removeFilter; - - ngModelCtrl.$render = function () { - if (scope.relationFiltersWatch) { - scope.relationFiltersWatch(); - scope.relationFiltersWatch = null; - } - if (ngModelCtrl.$viewValue) { - var value = ngModelCtrl.$viewValue; - scope.relationFilters.length = 0; - value.forEach(function (filter) { - scope.relationFilters.push(filter); - }); - } - scope.relationFiltersWatch = scope.$watch('relationFilters', function (newVal, prevVal) { - if (!angular.equals(newVal, prevVal)) { - updateValue(); - } - }, true); - } - - function addFilter() { - var filter = { - relationType: null, - entityTypes: [] - }; - scope.relationFilters.push(filter); - } - - function removeFilter($event, filter) { - var index = scope.relationFilters.indexOf(filter); - if (index > -1) { - scope.relationFilters.splice(index, 1); - } - } - - function updateValue() { - var value = ngModelCtrl.$viewValue; - if (!value) { - value = []; - ngModelCtrl.$setViewValue(value); - } else { - value.length = 0; - } - scope.relationFilters.forEach(function (filter) { - value.push(filter); - }); - } - $compile(element.contents())(scope); - } -} diff --git a/ui/src/app/entity/relation/relation-filters.scss b/ui/src/app/entity/relation/relation-filters.scss deleted file mode 100644 index 29ff854cdf..0000000000 --- a/ui/src/app/entity/relation/relation-filters.scss +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-relation-filters { - .header { - padding-right: 5px; - padding-bottom: 5px; - padding-left: 5px; - - .cell { - padding-right: 5px; - padding-left: 5px; - font-size: 12px; - font-weight: 700; - color: rgba(0, 0, 0, .54); - white-space: nowrap; - } - } - - .body { - max-height: 300px; - padding-right: 5px; - padding-bottom: 20px; - padding-left: 5px; - overflow: auto; - - .row { - padding-top: 5px; - } - - .cell { - padding-right: 5px; - padding-left: 5px; - - md-select { - margin: 0 0 24px; - } - - md-input-container { - margin: 0; - } - - md-chips-wrap { - padding: 0; - margin: 0 0 24px; - - md-autocomplete { - height: 30px; - - md-autocomplete-wrap { - height: 30px; - } - } - } - - .md-chips .md-chip-input-container input { - height: 26px; - padding: 2px 2px 2px; - line-height: 26px; - } - } - - .md-button { - margin: 0; - } - } -} diff --git a/ui/src/app/entity/relation/relation-filters.tpl.html b/ui/src/app/entity/relation/relation-filters.tpl.html deleted file mode 100644 index d5c1b9ac63..0000000000 --- a/ui/src/app/entity/relation/relation-filters.tpl.html +++ /dev/null @@ -1,67 +0,0 @@ - -
    -
    -
    - relation.type - entity.entity-types -   -
    -
    -
    -
    -
    - - - - - - - {{ 'relation.remove-relation-filter' | translate }} - - - close - - -
    -
    -
    -
    - relation.any-relation -
    -
    - - - {{ 'relation.add-relation-filter' | translate }} - - - add - - {{ 'action.add' | translate }} - -
    -
    \ No newline at end of file diff --git a/ui/src/app/entity/relation/relation-table.directive.js b/ui/src/app/entity/relation/relation-table.directive.js deleted file mode 100644 index 95dc4990bb..0000000000 --- a/ui/src/app/entity/relation/relation-table.directive.js +++ /dev/null @@ -1,293 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import 'angular-material-data-table/dist/md-data-table.min.css'; -import './relation-table.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import relationTableTemplate from './relation-table.tpl.html'; -import relationTemplate from './relation-dialog.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -import RelationController from './relation-dialog.controller'; - -/*@ngInject*/ -export default function RelationTable() { - return { - restrict: "E", - scope: true, - bindToController: { - entityId: '=', - entityType: '@' - }, - controller: RelationTableController, - controllerAs: 'vm', - templateUrl: relationTableTemplate - }; -} - -/*@ngInject*/ -function RelationTableController($scope, $q, $mdDialog, $document, $translate, $filter, $timeout, utils, types, entityRelationService) { - - let vm = this; - - vm.types = types; - - vm.direction = vm.types.entitySearchDirection.from; - - vm.relations = []; - vm.relationsCount = 0; - vm.allRelations = []; - vm.selectedRelations = []; - - vm.query = { - order: 'type', - limit: 5, - page: 1, - search: null - }; - - vm.enterFilterMode = enterFilterMode; - vm.exitFilterMode = exitFilterMode; - vm.onReorder = onReorder; - vm.onPaginate = onPaginate; - vm.addRelation = addRelation; - vm.editRelation = editRelation; - vm.deleteRelation = deleteRelation; - vm.deleteRelations = deleteRelations; - vm.reloadRelations = reloadRelations; - vm.updateRelations = updateRelations; - - $scope.$watch("vm.entityId", function(newVal, prevVal) { - if (newVal && !angular.equals(newVal, prevVal)) { - reloadRelations(); - } - }); - - $scope.$watch("vm.direction", function(newVal, prevVal) { - if (newVal && !angular.equals(newVal, prevVal)) { - reloadRelations(); - } - }); - - $scope.$watch("vm.query.search", function(newVal, prevVal) { - if (!angular.equals(newVal, prevVal) && vm.query.search != null) { - updateRelations(); - } - }); - - function enterFilterMode (event) { - let $button = angular.element(event.currentTarget); - let $toolbarsContainer = $button.closest('.toolbarsContainer'); - - vm.query.search = ''; - - $timeout(()=>{ - $toolbarsContainer.find('.searchInput').focus(); - }) - } - - function exitFilterMode () { - vm.query.search = null; - updateRelations(); - } - - function onReorder () { - updateRelations(); - } - - function onPaginate () { - updateRelations(); - } - - function addRelation($event) { - if ($event) { - $event.stopPropagation(); - } - openRelationDialog($event); - } - - function editRelation($event, relation) { - if ($event) { - $event.stopPropagation(); - } - openRelationDialog($event, relation); - } - - function openRelationDialog($event, relation) { - if ($event) { - $event.stopPropagation(); - } - var isAdd = false; - if (!relation) { - isAdd = true; - var entityId = { - id: vm.entityId, - entityType: vm.entityType - }; - relation = {}; - if (vm.direction == vm.types.entitySearchDirection.from) { - relation.from = entityId; - } else { - relation.to = entityId; - } - } - var onShowingCallback = { - onShowing: function(){} - } - $mdDialog.show({ - controller: RelationController, - controllerAs: 'vm', - templateUrl: relationTemplate, - parent: angular.element($document[0].body), - locals: { isAdd: isAdd, - direction: vm.direction, - relation: relation, - showingCallback: onShowingCallback}, - targetEvent: $event, - fullscreen: true, - multiple: true, - onShowing: function(scope, element) { - onShowingCallback.onShowing(scope, element); - } - }).then(function () { - reloadRelations(); - }, function () { - }); - } - - function deleteRelation($event, relation) { - if ($event) { - $event.stopPropagation(); - } - if (relation) { - var title; - var content; - if (vm.direction == vm.types.entitySearchDirection.from) { - title = $translate.instant('relation.delete-to-relation-title', {entityName: relation.toName}); - content = $translate.instant('relation.delete-to-relation-text', {entityName: relation.toName}); - } else { - title = $translate.instant('relation.delete-from-relation-title', {entityName: relation.fromName}); - content = $translate.instant('relation.delete-from-relation-text', {entityName: relation.fromName}); - } - - var confirm = $mdDialog.confirm() - .targetEvent($event) - .title(title) - .htmlContent(content) - .ariaLabel(title) - .cancel($translate.instant('action.no')) - .ok($translate.instant('action.yes')); - $mdDialog.show(confirm).then(function () { - entityRelationService.deleteRelation( - relation.from.id, - relation.from.entityType, - relation.type, - relation.to.id, - relation.to.entityType).then( - function success() { - reloadRelations(); - } - ); - }); - } - } - - function deleteRelations($event) { - if ($event) { - $event.stopPropagation(); - } - if (vm.selectedRelations && vm.selectedRelations.length > 0) { - var title; - var content; - if (vm.direction == vm.types.entitySearchDirection.from) { - title = $translate.instant('relation.delete-to-relations-title', {count: vm.selectedRelations.length}, 'messageformat'); - content = $translate.instant('relation.delete-to-relations-text'); - } else { - title = $translate.instant('relation.delete-from-relations-title', {count: vm.selectedRelations.length}, 'messageformat'); - content = $translate.instant('relation.delete-from-relations-text'); - } - var confirm = $mdDialog.confirm() - .targetEvent($event) - .title(title) - .htmlContent(content) - .ariaLabel(title) - .cancel($translate.instant('action.no')) - .ok($translate.instant('action.yes')); - $mdDialog.show(confirm).then(function () { - var tasks = []; - for (var i=0;i - -
    - - - - - {{ ('relation.search-direction.' + direction) | translate}} - - - -
    -
    - -
    - {{(vm.direction == vm.types.entitySearchDirection.from ? - 'relation.from-relations' : 'relation.to-relations') | translate}} - - - add - - {{ 'action.add' | translate }} - - - - search - - {{ 'action.search' | translate }} - - - - refresh - - {{ 'action.refresh' | translate }} - - -
    -
    - -
    - - search - - {{ 'action.search' | translate }} - - - - - - - - close - - {{ 'action.close' | translate }} - - -
    -
    - -
    - relation.selected-relations - - - delete - - {{ 'action.delete' | translate }} - - -
    -
    - - - - - - - - - - - - - - - - - - - - - - -
    relation.typerelation.to-entity-typerelation.from-entity-typerelation.to-entity-namerelation.from-entity-name 
    {{ relation.type }}{{ relation.toEntityTypeName }}{{ relation.fromEntityTypeName }}{{ relation.toName }}{{ relation.fromName }} - - edit - - {{ 'relation.edit' | translate }} - - - - delete - - {{ 'relation.delete' | translate }} - - -
    -
    - - -
    -
    diff --git a/ui/src/app/entity/relation/relation-type-autocomplete.directive.js b/ui/src/app/entity/relation/relation-type-autocomplete.directive.js deleted file mode 100644 index 651d1d2e47..0000000000 --- a/ui/src/app/entity/relation/relation-type-autocomplete.directive.js +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './relation-type-autocomplete.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import relationTypeAutocompleteTemplate from './relation-type-autocomplete.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function RelationTypeAutocomplete($compile, $templateCache, $q, $filter, assetService, deviceService, types) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - var template = $templateCache.get(relationTypeAutocompleteTemplate); - element.html(template); - - scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false; - scope.hideLabel = angular.isDefined(attrs.hideLabel) ? true : false; - - scope.relationType = null; - scope.relationTypeSearchText = ''; - scope.relationTypes = []; - for (var type in types.entityRelationType) { - scope.relationTypes.push(types.entityRelationType[type]); - } - - scope.fetchRelationTypes = function(searchText) { - var deferred = $q.defer(); - var result = $filter('filter')(scope.relationTypes, {'$': searchText}); - if (result && result.length) { - deferred.resolve(result); - } else { - deferred.resolve([searchText]); - } - return deferred.promise; - } - - scope.relationTypeSearchTextChanged = function() { - } - - scope.updateView = function () { - if (!scope.disabled) { - ngModelCtrl.$setViewValue(scope.relationType); - } - } - - ngModelCtrl.$render = function () { - scope.relationType = ngModelCtrl.$viewValue; - } - - scope.$watch('relationType', function (newValue, prevValue) { - if (!angular.equals(newValue, prevValue)) { - scope.updateView(); - } - }); - - scope.$watch('disabled', function () { - scope.updateView(); - }); - - $compile(element.contents())(scope); - } - - return { - restrict: "E", - require: "^ngModel", - link: linker, - scope: { - theForm: '=?', - tbRequired: '=?', - disabled:'=ngDisabled' - } - }; -} diff --git a/ui/src/app/entity/relation/relation-type-autocomplete.scss b/ui/src/app/entity/relation/relation-type-autocomplete.scss deleted file mode 100644 index 209f58f769..0000000000 --- a/ui/src/app/entity/relation/relation-type-autocomplete.scss +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-relation-type-autocomplete { - .tb-relation-type-item { - display: block; - height: 48px; - } - - li { - height: auto !important; - white-space: normal !important; - } -} diff --git a/ui/src/app/entity/relation/relation-type-autocomplete.tpl.html b/ui/src/app/entity/relation/relation-type-autocomplete.tpl.html deleted file mode 100644 index 7a9147ecd9..0000000000 --- a/ui/src/app/entity/relation/relation-type-autocomplete.tpl.html +++ /dev/null @@ -1,40 +0,0 @@ - - - -
    - {{item}} -
    -
    -
    -
    relation.relation-type-required
    -
    -
    diff --git a/ui/src/app/event/event-content-dialog.controller.js b/ui/src/app/event/event-content-dialog.controller.js deleted file mode 100644 index 271a5f2337..0000000000 --- a/ui/src/app/event/event-content-dialog.controller.js +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import $ from 'jquery'; -import 'brace/ext/language_tools'; -import 'brace/ext/searchbox'; -import 'brace/mode/java'; -import 'brace/theme/github'; -import beautify from 'js-beautify'; - -/* eslint-disable angular/angularelement */ - -const js_beautify = beautify.js; - -/*@ngInject*/ -export default function EventContentDialogController($mdDialog, types, content, contentType, title, showingCallback) { - - var vm = this; - - showingCallback.onShowing = function(scope, element) { - updateEditorSize(element); - } - - vm.content = content; - vm.title = title; - - var mode; - if (contentType) { - mode = types.contentType[contentType].code; - if (contentType == types.contentType.JSON.value && vm.content) { - vm.content = js_beautify(vm.content, {indent_size: 4}); - } - } else { - mode = 'java'; - } - - vm.contentOptions = { - useWrapMode: false, - mode: mode, - showGutter: false, - showPrintMargin: false, - theme: 'github', - advanced: { - enableSnippets: false, - enableBasicAutocompletion: false, - enableLiveAutocompletion: false - }, - onLoad: function (_ace) { - vm.editor = _ace; - } - }; - - function updateEditorSize(element) { - var newHeight = 400; - var newWidth = 600; - if (vm.content && vm.content.length > 0) { - var lines = vm.content.split('\n'); - newHeight = 16 * lines.length + 16; - var maxLineLength = 0; - for (var i = 0; i < lines.length; i++) { - var line = lines[i].replace(/\t/g, ' ').replace(/\n/g, ''); - var lineLength = line.length; - maxLineLength = Math.max(maxLineLength, lineLength); - } - newWidth = 8 * maxLineLength + 16; - } - $('#tb-event-content', element).height(newHeight.toString() + "px") - .width(newWidth.toString() + "px"); - vm.editor.resize(); - } - - vm.close = close; - - function close () { - $mdDialog.hide(); - } - -} - -/* eslint-enable angular/angularelement */ diff --git a/ui/src/app/event/event-content-dialog.tpl.html b/ui/src/app/event/event-content-dialog.tpl.html deleted file mode 100644 index 3959da43f4..0000000000 --- a/ui/src/app/event/event-content-dialog.tpl.html +++ /dev/null @@ -1,42 +0,0 @@ - - - -
    -

    {{ vm.title }}

    - - - - -
    -
    - -
    -
    -
    -
    -
    - - - {{ 'action.close' | - translate }} - - -
    diff --git a/ui/src/app/event/event-header-debug-rulenode.tpl.html b/ui/src/app/event/event-header-debug-rulenode.tpl.html deleted file mode 100644 index 3e2267d961..0000000000 --- a/ui/src/app/event/event-header-debug-rulenode.tpl.html +++ /dev/null @@ -1,27 +0,0 @@ - -
    event.event-time
    -
    event.server
    -
    event.type
    -
    event.entity
    -
    event.message-id
    -
    event.message-type
    -
    event.relation-type
    -
    event.data
    -
    event.metadata
    -
    event.error
    diff --git a/ui/src/app/event/event-header-error.tpl.html b/ui/src/app/event/event-header-error.tpl.html deleted file mode 100644 index bdad0bf29a..0000000000 --- a/ui/src/app/event/event-header-error.tpl.html +++ /dev/null @@ -1,21 +0,0 @@ - -
    event.event-time
    -
    event.server
    -
    event.method
    -
    event.error
    diff --git a/ui/src/app/event/event-header-lc-event.tpl.html b/ui/src/app/event/event-header-lc-event.tpl.html deleted file mode 100644 index a96935aafb..0000000000 --- a/ui/src/app/event/event-header-lc-event.tpl.html +++ /dev/null @@ -1,22 +0,0 @@ - -
    event.event-time
    -
    event.server
    -
    event.event
    -
    event.status
    -
    event.error
    diff --git a/ui/src/app/event/event-header-stats.tpl.html b/ui/src/app/event/event-header-stats.tpl.html deleted file mode 100644 index 634e5cbaa3..0000000000 --- a/ui/src/app/event/event-header-stats.tpl.html +++ /dev/null @@ -1,21 +0,0 @@ - -
    event.event-time
    -
    event.server
    -
    event.messages-processed
    -
    event.errors-occurred
    diff --git a/ui/src/app/event/event-header.directive.js b/ui/src/app/event/event-header.directive.js deleted file mode 100644 index ce0fe4e81c..0000000000 --- a/ui/src/app/event/event-header.directive.js +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import eventHeaderLcEventTemplate from './event-header-lc-event.tpl.html'; -import eventHeaderStatsTemplate from './event-header-stats.tpl.html'; -import eventHeaderErrorTemplate from './event-header-error.tpl.html'; -import eventHeaderDebugRuleNodeTemplate from './event-header-debug-rulenode.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function EventHeaderDirective($compile, $templateCache, types) { - - var linker = function (scope, element, attrs) { - - var getTemplate = function(eventType) { - var template = ''; - switch(eventType) { - case types.eventType.lcEvent.value: - template = eventHeaderLcEventTemplate; - break; - case types.eventType.stats.value: - template = eventHeaderStatsTemplate; - break; - case types.eventType.error.value: - template = eventHeaderErrorTemplate; - break; - case types.debugEventType.debugRuleNode.value: - template = eventHeaderDebugRuleNodeTemplate; - break; - case types.debugEventType.debugRuleChain.value: - template = eventHeaderDebugRuleNodeTemplate; - break; - } - return $templateCache.get(template); - } - - scope.loadTemplate = function() { - element.html(getTemplate(attrs.eventType)); - $compile(element.contents())(scope); - } - - attrs.$observe('eventType', function() { - scope.loadTemplate(); - }); - - } - - return { - restrict: "A", - replace: false, - link: linker, - scope: false - }; -} diff --git a/ui/src/app/event/event-row-debug-rulenode.tpl.html b/ui/src/app/event/event-row-debug-rulenode.tpl.html deleted file mode 100644 index 19c2d9f3e3..0000000000 --- a/ui/src/app/event/event-row-debug-rulenode.tpl.html +++ /dev/null @@ -1,63 +0,0 @@ - -
    {{event.createdTime | date : 'yyyy-MM-dd HH:mm:ss'}}
    -
    {{event.body.server}}
    -
    {{event.body.type}}
    -
    {{event.body.entityName}}
    -
    {{event.body.msgId}}
    -
    {{event.body.msgType}}
    -
    {{event.body.relationType}}
    -
    - - - {{ 'action.view' | translate }} - - - more_horiz - - -
    -
    - - - {{ 'action.view' | translate }} - - - more_horiz - - -
    -
    - - - {{ 'action.view' | translate }} - - - more_horiz - - -
    diff --git a/ui/src/app/event/event-row-error.tpl.html b/ui/src/app/event/event-row-error.tpl.html deleted file mode 100644 index e81d0077bf..0000000000 --- a/ui/src/app/event/event-row-error.tpl.html +++ /dev/null @@ -1,33 +0,0 @@ - -
    {{event.createdTime | date : 'yyyy-MM-dd HH:mm:ss'}}
    -
    {{event.body.server}}
    -
    {{event.body.method}}
    -
    - - - {{ 'action.view' | translate }} - - - more_horiz - - -
    diff --git a/ui/src/app/event/event-row-lc-event.tpl.html b/ui/src/app/event/event-row-lc-event.tpl.html deleted file mode 100644 index 1b6715eafa..0000000000 --- a/ui/src/app/event/event-row-lc-event.tpl.html +++ /dev/null @@ -1,34 +0,0 @@ - -
    {{event.createdTime | date : 'yyyy-MM-dd HH:mm:ss'}}
    -
    {{event.body.server}}
    -
    {{event.body.event}}
    -
    {{event.body.success ? 'event.success' : 'event.failed'}}
    -
    - - - {{ 'action.view' | translate }} - - - more_horiz - - -
    diff --git a/ui/src/app/event/event-row-stats.tpl.html b/ui/src/app/event/event-row-stats.tpl.html deleted file mode 100644 index ce21d33b53..0000000000 --- a/ui/src/app/event/event-row-stats.tpl.html +++ /dev/null @@ -1,21 +0,0 @@ - -
    {{event.createdTime | date : 'yyyy-MM-dd HH:mm:ss'}}
    -
    {{event.body.server}}
    -
    {{event.body.messagesProcessed}}
    -
    {{event.body.errorsOccurred}}
    diff --git a/ui/src/app/event/event-row.directive.js b/ui/src/app/event/event-row.directive.js deleted file mode 100644 index ffd5cc4adf..0000000000 --- a/ui/src/app/event/event-row.directive.js +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import eventErrorDialogTemplate from './event-content-dialog.tpl.html'; - -import eventRowLcEventTemplate from './event-row-lc-event.tpl.html'; -import eventRowStatsTemplate from './event-row-stats.tpl.html'; -import eventRowErrorTemplate from './event-row-error.tpl.html'; -import eventRowDebugRuleNodeTemplate from './event-row-debug-rulenode.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function EventRowDirective($compile, $templateCache, $mdDialog, $document, types, utils) { - - var linker = function (scope, element, attrs) { - - var getTemplate = function(eventType) { - var template = ''; - switch(eventType) { - case types.eventType.lcEvent.value: - template = eventRowLcEventTemplate; - break; - case types.eventType.stats.value: - template = eventRowStatsTemplate; - break; - case types.eventType.error.value: - template = eventRowErrorTemplate; - break; - case types.debugEventType.debugRuleNode.value: - template = eventRowDebugRuleNodeTemplate; - break; - case types.debugEventType.debugRuleChain.value: - template = eventRowDebugRuleNodeTemplate; - break; - } - return $templateCache.get(template); - } - - scope.loadTemplate = function() { - element.html(getTemplate(attrs.eventType)); - $compile(element.contents())(scope); - } - - attrs.$observe('eventType', function() { - scope.loadTemplate(); - }); - - scope.types = types; - - scope.event = attrs.event; - - scope.showContent = function($event, content, title, contentType) { - var onShowingCallback = { - onShowing: function(){} - } - if (!contentType) { - contentType = null; - } - var sortedContent; - try { - sortedContent = angular.toJson(utils.sortObjectKeys(angular.fromJson(content))); - } - catch(err) { - sortedContent = content; - } - $mdDialog.show({ - controller: 'EventContentDialogController', - controllerAs: 'vm', - templateUrl: eventErrorDialogTemplate, - locals: {content: sortedContent, title: title, contentType: contentType, showingCallback: onShowingCallback}, - parent: angular.element($document[0].body), - fullscreen: true, - targetEvent: $event, - multiple: true, - onShowing: function(scope, element) { - onShowingCallback.onShowing(scope, element); - } - }); - } - - scope.checkTooltip = function($event) { - var el = $event.target; - var $el = angular.element(el); - if(el.offsetWidth < el.scrollWidth && !$el.attr('title')){ - $el.attr('title', $el.text()); - } - } - - $compile(element.contents())(scope); - } - - return { - restrict: "A", - replace: false, - link: linker, - scope: false - }; -} diff --git a/ui/src/app/event/event-table.directive.js b/ui/src/app/event/event-table.directive.js deleted file mode 100644 index ff002f069e..0000000000 --- a/ui/src/app/event/event-table.directive.js +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './event.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import eventTableTemplate from './event-table.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function EventTableDirective($compile, $templateCache, $rootScope, types, eventService) { - - var linker = function (scope, element, attrs) { - - var template = $templateCache.get(eventTableTemplate); - - element.html(template); - - if (attrs.disabledEventTypes) { - var disabledEventTypes = attrs.disabledEventTypes.split(','); - scope.eventTypes = {}; - for (var type in types.eventType) { - var eventType = types.eventType[type]; - var enabled = true; - for (var i=0;i scope.events.data.length) { - scope.theEvents.fetchMoreItems_(index); - return null; - } - var item = scope.events.data[index]; - if (item) { - item.indexNumber = index + 1; - } - return item; - }, - - getLength: function () { - if (scope.events.hasNext) { - return scope.events.data.length + scope.events.nextPageLink.limit; - } else { - return scope.events.data.length; - } - }, - - fetchMoreItems_: function () { - if (scope.events.hasNext && !scope.events.pending) { - if (scope.entityType && scope.entityId && scope.eventType && scope.tenantId) { - var promise = eventService.getEvents(scope.entityType, scope.entityId, - scope.eventType, scope.tenantId, scope.events.nextPageLink); - if (promise) { - scope.events.pending = true; - promise.then( - function success(events) { - scope.events.data = scope.events.data.concat(events.data); - scope.events.nextPageLink = events.nextPageLink; - scope.events.hasNext = events.hasNext; - if (scope.events.hasNext) { - scope.events.nextPageLink.limit = pageSize; - } - scope.events.pending = false; - }, - function fail() { - scope.events.hasNext = false; - scope.events.pending = false; - }); - } else { - scope.events.hasNext = false; - } - } else { - scope.events.hasNext = false; - } - } - } - }; - - scope.$watch("entityId", function(newVal, prevVal) { - if (newVal && !angular.equals(newVal, prevVal)) { - scope.resetFilter(); - scope.reload(); - } - }); - - scope.$watch("eventType", function(newVal, prevVal) { - if (newVal && !angular.equals(newVal, prevVal)) { - scope.reload(); - } - }); - - scope.$watch("timewindow", function(newVal, prevVal) { - if (newVal && !angular.equals(newVal, prevVal)) { - scope.reload(); - } - }, true); - - scope.resetFilter = function() { - scope.timewindow = { - history: { - timewindowMs: 24 * 60 * 60 * 1000 // 1 day - } - }; - scope.eventType = attrs.defaultEventType; - } - - scope.updateTimeWindowRange = function() { - if (scope.timewindow.history.timewindowMs) { - var currentTime = (new Date).getTime(); - startTime = currentTime - scope.timewindow.history.timewindowMs; - endTime = currentTime; - } else { - startTime = scope.timewindow.history.fixedTimewindow.startTimeMs; - endTime = scope.timewindow.history.fixedTimewindow.endTimeMs; - } - } - - scope.reload = function() { - scope.topIndex = 0; - scope.selected = []; - scope.updateTimeWindowRange(); - scope.events = { - data: [], - nextPageLink: { - limit: pageSize, - startTime: startTime, - endTime: endTime - }, - hasNext: true, - pending: false - }; - scope.theEvents.getItemAtIndex(pageSize); - } - - scope.noData = function() { - return scope.events.data.length == 0 && !scope.events.hasNext; - } - - scope.hasData = function() { - return scope.events.data.length > 0; - } - - scope.loading = function() { - return $rootScope.loading; - } - - scope.hasScroll = function() { - var repeatContainer = scope.repeatContainer[0]; - if (repeatContainer) { - var scrollElement = repeatContainer.children[0]; - if (scrollElement) { - return scrollElement.scrollHeight > scrollElement.clientHeight; - } - } - return false; - } - - scope.reload(); - - $compile(element.contents())(scope); - } - - return { - restrict: "E", - link: linker, - scope: { - entityType: '=', - entityId: '=', - tenantId: '=' - } - }; -} diff --git a/ui/src/app/event/event-table.tpl.html b/ui/src/app/event/event-table.tpl.html deleted file mode 100644 index 64b033cc5c..0000000000 --- a/ui/src/app/event/event-table.tpl.html +++ /dev/null @@ -1,54 +0,0 @@ - - -
    - - - - - {{type.name | translate}} - - - - - - refresh - - {{ 'action.refresh' | translate }} - - -
    - - - - - - event.no-events-prompt - - - - - - - - -
    diff --git a/ui/src/app/event/event.scss b/ui/src/app/event/event.scss deleted file mode 100644 index f1b5eb0d46..0000000000 --- a/ui/src/app/event/event.scss +++ /dev/null @@ -1,93 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -md-list.tb-event-table { - padding: 0; - - md-list-item { - padding: 0; - } - - .tb-row { - height: 48px; - padding: 0; - overflow: hidden; - - .tb-cell { - text-overflow: ellipsis; - - &.tb-scroll { - overflow-x: auto; - overflow-y: hidden; - white-space: nowrap; - } - - &.tb-nowrap { - white-space: nowrap; - } - } - } - - .tb-row:hover { - background-color: #eee; - } - - .tb-header:hover { - background: none; - } - - .tb-header { - .tb-cell { - font-size: 12px; - font-weight: 700; - color: rgba(0, 0, 0, .54); - white-space: nowrap; - background: none; - } - } - - .tb-cell { - &:first-child { - padding-left: 14px; - } - - &:last-child { - padding-right: 14px; - } - padding: 0 6px; - margin: auto 0; - overflow: hidden; - font-size: 13px; - color: rgba(0, 0, 0, .87); - text-align: left; - vertical-align: middle; - - .md-button { - padding: 0; - margin: 0; - } - } - - .tb-cell.tb-number { - text-align: right; - } -} - -#tb-event-content { - width: 100%; - min-width: 400px; - height: 100%; - min-height: 50px; -} diff --git a/ui/src/app/event/index.js b/ui/src/app/event/index.js deleted file mode 100644 index 69227eef8b..0000000000 --- a/ui/src/app/event/index.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import thingsboardApiEvent from '../api/event.service'; - -import EventContentDialogController from './event-content-dialog.controller'; -import EventHeaderDirective from './event-header.directive'; -import EventRowDirective from './event-row.directive'; -import EventTableDirective from './event-table.directive'; - -export default angular.module('thingsboard.event', [ - thingsboardApiEvent -]) - .controller('EventContentDialogController', EventContentDialogController) - .directive('tbEventHeader', EventHeaderDirective) - .directive('tbEventRow', EventRowDirective) - .directive('tbEventTable', EventTableDirective) - .name; diff --git a/ui/src/app/extension/extension-dialog.controller.js b/ui/src/app/extension/extension-dialog.controller.js deleted file mode 100644 index ceb5d198cd..0000000000 --- a/ui/src/app/extension/extension-dialog.controller.js +++ /dev/null @@ -1,297 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import beautify from 'js-beautify'; - -const js_beautify = beautify.js; - -/*@ngInject*/ -export default function ExtensionDialogController($scope, $mdDialog, $translate, isAdd, allExtensions, entityId, entityType, extension, types, attributeService) { - - var vm = this; - - vm.types = types; - vm.isAdd = isAdd; - vm.entityType = entityType; - vm.entityId = entityId; - vm.allExtensions = allExtensions; - - - if (extension) { - vm.extension = angular.copy(extension); - editTransformers(vm.extension); - } else { - vm.extension = {}; - } - - - vm.extensionTypeChange = function () { - - if (vm.extension.type === "HTTP") { - vm.extension.configuration = { - "converterConfigurations": [] - }; - } - if (vm.extension.type === "MQTT") { - vm.extension.configuration = { - "brokers": [] - }; - } - if (vm.extension.type === "OPC UA" || vm.extension.type === "MODBUS") { - vm.extension.configuration = { - "servers": [] - }; - } - }; - - vm.cancel = cancel; - function cancel() { - $mdDialog.cancel(); - } - - vm.save = save; - function save() { - let $errorElement = angular.element('[name=theForm]').find('.ng-invalid'); - - if ($errorElement.length) { - - let $mdDialogScroll = angular.element('md-dialog-content').scrollTop(); - let $mdDialogTop = angular.element('md-dialog-content').offset().top; - let $errorElementTop = angular.element('[name=theForm]').find('.ng-invalid').eq(0).offset().top; - - - if ($errorElementTop !== $mdDialogTop) { - angular.element('md-dialog-content').animate({ - scrollTop: $mdDialogScroll + ($errorElementTop - $mdDialogTop) - 50 - }, 500); - $errorElement.eq(0).focus(); - } - } else { - - if(vm.isAdd) { - vm.allExtensions.push(vm.extension); - } else { - var index = vm.allExtensions.indexOf(extension); - if(index > -1) { - vm.allExtensions[index] = vm.extension; - } - } - - $mdDialog.hide(); - saveTransformers(); - - var editedValue = angular.toJson(vm.allExtensions); - - attributeService - .saveEntityAttributes( - vm.entityType, - vm.entityId, - types.attributesScope.shared.value, - [{key:"configuration", value:editedValue}] - ) - .then(function success() { - }); - - } - } - - vm.validateId = function() { - var coincidenceArray = vm.allExtensions.filter(function(ext) { - return ext.id == vm.extension.id; - }); - if(coincidenceArray.length) { - if(!vm.isAdd) { - if(coincidenceArray[0].id == extension.id) { - $scope.theForm.extensionId.$setValidity('uniqueIdValidation', true); - } else { - $scope.theForm.extensionId.$setValidity('uniqueIdValidation', false); - } - } else { - $scope.theForm.extensionId.$setValidity('uniqueIdValidation', false); - } - } else { - $scope.theForm.extensionId.$setValidity('uniqueIdValidation', true); - } - }; - - function saveTransformers() { - if(vm.extension.type == types.extensionType.http) { - var config = vm.extension.configuration.converterConfigurations; - if(config && config.length > 0) { - for(let i=0;i 0) { - for(let i=0;i 0) { - for(let j=0;j 0) { - for(let j=0;j 0) { - for(let j=0;j 0) { - for(let j=0;j 0) { - for(let j=0;j 0) { - for(let j=0;j 0) { - for(let j=0;j 0) { - for(let j=0;j - -
    - -
    -

    {{ vm.isAdd ? 'extension.add' : 'extension.edit'}}

    - - - - -
    -
    - - - - - - -
    - -
    -
    - - - -
    -
    extension.field-required
    -
    extension.unique-id-required
    -
    -
    - - - - - - - {{value}} - - - -
    -
    extension.field-required
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - - - - {{ (vm.isAdd ? 'action.add' : 'action.save') | translate }} - - - {{ 'action.cancel' | translate }} - - -
    -
    \ No newline at end of file diff --git a/ui/src/app/extension/extension-table.directive.js b/ui/src/app/extension/extension-table.directive.js deleted file mode 100644 index 853c51d538..0000000000 --- a/ui/src/app/extension/extension-table.directive.js +++ /dev/null @@ -1,409 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import 'angular-material-data-table/dist/md-data-table.min.css'; -import './extension-table.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import extensionTableTemplate from './extension-table.tpl.html'; -import extensionDialogTemplate from './extension-dialog.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -import ExtensionDialogController from './extension-dialog.controller' -import $ from 'jquery'; - -/*@ngInject*/ -export default function ExtensionTableDirective() { - return { - restrict: "E", - scope: true, - bindToController: { - entityId: '=', - entityType: '@', - inWidget: '@?', - ctx: '=?', - entityName: '=' - }, - controller: ExtensionTableController, - controllerAs: 'vm', - templateUrl: extensionTableTemplate - }; -} - -/*@ngInject*/ -function ExtensionTableController($scope, $filter, $document, $translate, $timeout, $mdDialog, types, attributeService, telemetryWebsocketService, importExport) { - - let vm = this; - - vm.extensions = []; - vm.allExtensions = []; - vm.selectedExtensions = []; - vm.extensionsCount = 0; - - vm.query = { - order: 'id', - limit: 5, - page: 1, - search: null - }; - - vm.enterFilterMode = enterFilterMode; - vm.exitFilterMode = exitFilterMode; - vm.onReorder = onReorder; - vm.onPaginate = onPaginate; - vm.addExtension = addExtension; - vm.editExtension = editExtension; - vm.deleteExtension = deleteExtension; - vm.deleteExtensions = deleteExtensions; - vm.reloadExtensions = reloadExtensions; - vm.updateExtensions = updateExtensions; - - $scope.$watch("vm.entityId", function(newVal) { - if (newVal) { - if ($scope.subscriber) { - telemetryWebsocketService.unsubscribe($scope.subscriber); - $scope.subscriber = null; - } - - vm.subscribed = false; - vm.syncLastTime = $translate.instant('extension.sync.not-available'); - - subscribeForClientAttributes(); - - reloadExtensions(); - } - }); - - $scope.$on('$destroy', function() { - if ($scope.subscriber) { - telemetryWebsocketService.unsubscribe($scope.subscriber); - $scope.subscriber = null; - } - }); - - $scope.$watch("vm.query.search", function(newVal, prevVal) { - if (!angular.equals(newVal, prevVal) && vm.query.search != null) { - updateExtensions(); - } - }); - - $scope.$watch('vm.selectedExtensions.length', function (newLength) { - var selectionMode = newLength ? true : false; - if (vm.ctx) { - if (selectionMode) { - vm.ctx.hideTitlePanel = true; - $scope.$emit("selectedExtensions", true); - } else if (vm.query.search == null) { - vm.ctx.hideTitlePanel = false; - $scope.$emit("selectedExtensions", false); - } - } - }); - - $scope.$on("showSearch", function($event, source) { - if(source.entityId == vm.entityId) { - enterFilterMode(); - $scope.$emit("filterMode", true); - } - }); - $scope.$on("refreshExtensions", function($event, source) { - if(source.entityId == vm.entityId) { - reloadExtensions(); - } - }); - $scope.$on("addExtension", function($event, source) { - if(source.entityId == vm.entityId) { - addExtension(); - } - }); - $scope.$on("exportExtensions", function($event, source) { - if(source.entityId == vm.entityId) { - vm.exportExtensions(source.entityName); - } - }); - $scope.$on("importExtensions", function($event, source) { - if(source.entityId == vm.entityId) { - vm.importExtensions(); - } - }); - - function enterFilterMode(event) { - let $button = angular.element(event.currentTarget); - let $toolbarsContainer = $button.closest('.toolbarsContainer'); - - vm.query.search = ''; - if(vm.inWidget) { - vm.ctx.hideTitlePanel = true; - } - $timeout(()=>{ - $toolbarsContainer.find('.searchInput').focus(); - }) - } - - function exitFilterMode() { - vm.query.search = null; - updateExtensions(); - if(vm.inWidget) { - vm.ctx.hideTitlePanel = false; - $scope.$emit("filterMode", false); - } - } - - function onReorder() { - updateExtensions(); - } - - function onPaginate() { - updateExtensions(); - } - - function addExtension($event) { - if ($event) { - $event.stopPropagation(); - } - openExtensionDialog($event); - } - - function editExtension($event, extension) { - if ($event) { - $event.stopPropagation(); - } - openExtensionDialog($event, extension); - } - - function openExtensionDialog($event, extension) { - if ($event) { - $event.stopPropagation(); - } - var isAdd = false; - if(!extension) { - isAdd = true; - } - $mdDialog.show({ - controller: ExtensionDialogController, - controllerAs: 'vm', - templateUrl: extensionDialogTemplate, - parent: angular.element($document[0].body), - locals: { - isAdd: isAdd, - allExtensions: vm.allExtensions, - entityId: vm.entityId, - entityType: vm.entityType, - extension: extension - }, - bindToController: true, - targetEvent: $event, - fullscreen: true, - multiple: true - }).then(function() { - reloadExtensions(); - }, function () { - }); - } - - function deleteExtension($event, extension) { - if ($event) { - $event.stopPropagation(); - } - if(extension) { - var title = $translate.instant('extension.delete-extension-title', {extensionId: extension.id}); - var content = $translate.instant('extension.delete-extension-text'); - - var confirm = $mdDialog.confirm() - .targetEvent($event) - .title(title) - .htmlContent(content) - .ariaLabel(title) - .cancel($translate.instant('action.no')) - .ok($translate.instant('action.yes')); - $mdDialog.show(confirm).then(function() { - var editedExtensions = vm.allExtensions.filter(function(ext) { - return ext.id !== extension.id; - }); - var editedValue = angular.toJson(editedExtensions); - attributeService.saveEntityAttributes(vm.entityType, vm.entityId, types.attributesScope.shared.value, [{key:"configuration", value:editedValue}]).then( - function success() { - reloadExtensions(); - } - ); - }); - } - } - - function deleteExtensions($event) { - if ($event) { - $event.stopPropagation(); - } - if (vm.selectedExtensions && vm.selectedExtensions.length > 0) { - var title = $translate.instant('extension.delete-extensions-title', {count: vm.selectedExtensions.length}, 'messageformat'); - var content = $translate.instant('extension.delete-extensions-text'); - - var confirm = $mdDialog.confirm() - .targetEvent($event) - .title(title) - .htmlContent(content) - .ariaLabel(title) - .cancel($translate.instant('action.no')) - .ok($translate.instant('action.yes')); - $mdDialog.show(confirm).then(function () { - var editedExtensions = angular.copy(vm.allExtensions); - for (var i = 0; i < vm.selectedExtensions.length; i++) { - editedExtensions = editedExtensions.filter(function (ext) { - return ext.id !== vm.selectedExtensions[i].id; - }); - } - var editedValue = angular.toJson(editedExtensions); - attributeService.saveEntityAttributes(vm.entityType, vm.entityId, types.attributesScope.shared.value, [{key:"configuration", value:editedValue}]).then( - function success() { - reloadExtensions(); - } - ); - }); - } - } - - function reloadExtensions() { - vm.subscribed = false; - vm.allExtensions.length = 0; - vm.extensions.length = 0; - vm.extensionsPromise = attributeService.getEntityAttributesValues(vm.entityType, vm.entityId, types.attributesScope.shared.value, ["configuration"]); - vm.extensionsPromise.then( - function success(data) { - if (data.length) { - vm.allExtensions = angular.fromJson(data[0].value); - } else { - vm.allExtensions = []; - } - - vm.selectedExtensions = []; - updateExtensions(); - vm.extensionsPromise = null; - }, - function fail() { - vm.extensions = []; - vm.selectedExtensions = []; - updateExtensions(); - vm.extensionsPromise = null; - } - ); - } - - function updateExtensions() { - vm.selectedExtensions = []; - var result = $filter('orderBy')(vm.allExtensions, vm.query.order); - if (vm.query.search != null) { - result = $filter('filter')(result, function(extension) { - if(!vm.query.search || (extension.id.indexOf(vm.query.search) != -1) || (extension.type.indexOf(vm.query.search) != -1)) { - return true; - } - return false; - }); - } - vm.extensionsCount = result.length; - var startIndex = vm.query.limit * (vm.query.page - 1); - vm.extensions = result.slice(startIndex, startIndex + vm.query.limit); - - vm.extensionsJSON = angular.toJson(vm.extensions); - checkForSync(); - } - - function subscribeForClientAttributes() { - if (!vm.subscribed) { - if (vm.entityId && vm.entityType) { - $scope.subscriber = { - subscriptionCommands: [{ - entityType: vm.entityType, - entityId: vm.entityId, - scope: 'CLIENT_SCOPE' - }], - type: 'attribute', - onData: function (data) { - if (data.data) { - onSubscriptionData(data.data); - } - vm.subscribed = true; - } - }; - telemetryWebsocketService.subscribe($scope.subscriber); - } - } - } - function onSubscriptionData(data) { - - if ($.isEmptyObject(data)) { - vm.appliedConfiguration = undefined; - } else { - if (data.appliedConfiguration && data.appliedConfiguration[0] && data.appliedConfiguration[0][1]) { - vm.appliedConfiguration = data.appliedConfiguration[0][1]; - } - } - - updateExtensions(); - $scope.$digest(); - } - - - function checkForSync() { - if (vm.appliedConfiguration && vm.extensionsJSON && vm.appliedConfiguration === vm.extensionsJSON) { - vm.syncStatus = $translate.instant('extension.sync.sync'); - vm.syncLastTime = formatDate(); - $scope.isSync = true; - } else { - vm.syncStatus = $translate.instant('extension.sync.not-sync'); - - $scope.isSync = false; - } - } - - function formatDate(date) { - let d; - if (date) { - d = date; - } else { - d = new Date(); - } - - d = d.getFullYear() +'/'+ addZero(d.getMonth()+1) +'/'+ addZero(d.getDate()) + ' ' + addZero(d.getHours()) + ':' + addZero(d.getMinutes()) +':'+ addZero(d.getSeconds()); - return d; - - function addZero(num) { - if ((angular.isNumber(num) && num < 10) || (angular.isString(num) && num.length === 1)) { - num = '0' + num; - } - return num; - } - } - - vm.importExtensions = function($event) { - importExport.importExtension($event, {"entityType":vm.entityType, "entityId":vm.entityId, "successFunc":reloadExtensions}); - }; - vm.exportExtensions = function(widgetSourceEntityName) { - if(vm.inWidget) { - importExport.exportToPc(vm.extensionsJSON, widgetSourceEntityName + '_configuration.json'); - } else { - importExport.exportToPc(vm.extensionsJSON, vm.entityName + '_configuration.json'); - } - }; - - /*change function for widget implementing, like vm.exportExtensions*/ - vm.exportExtension = function($event, extension) { - if ($event) { - $event.stopPropagation(); - } - importExport.exportToPc(extension, vm.entityName +'_'+ extension.id +'_configuration.json'); - }; -} \ No newline at end of file diff --git a/ui/src/app/extension/extension-table.scss b/ui/src/app/extension/extension-table.scss deleted file mode 100644 index 259141540e..0000000000 --- a/ui/src/app/extension/extension-table.scss +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "../../scss/constants"; - - -.extension-table { - md-input-container .md-errors-spacer { - min-height: 0; - } - - /* - &.tb-data-table table.md-table tbody tr td.tb-action-cell, - &.tb-data-table table.md-table.md-row-select tbody tr td.tb-action-cell { - width: 114px; - } - */ - - .sync-widget { - max-height: 90px; - overflow: hidden; - } - - .toolbar-widget { - min-height: 39px; - max-height: 39px; - } -} - -.extension__syncStatus--black { - color: #000 !important; -} - -.extension__syncStatus--green { - color: #228634 !important; -} - -.extension__syncStatus--red { - color: #862222 !important; -} diff --git a/ui/src/app/extension/extension-table.tpl.html b/ui/src/app/extension/extension-table.tpl.html deleted file mode 100644 index 96178875db..0000000000 --- a/ui/src/app/extension/extension-table.tpl.html +++ /dev/null @@ -1,158 +0,0 @@ - - -
    - -
    - {{ 'extension.extensions' }} - - - - file_upload - - {{ 'extension.import-extensions-configuration' | translate }} - - - - file_download - - {{ 'extension.export-extensions-configuration' | translate }} - - - - add - - {{ 'action.add' | translate }} - - - - search - - {{ 'action.search' | translate }} - - - - refresh - - {{ 'action.refresh' | translate }} - - -
    -
    - -
    - - search - - {{ 'action.search' | translate }} - - - - - - - - close - - {{ 'action.close' | translate }} - - -
    -
    - -
    - extension.selected-extensions - - - delete - - {{ 'action.delete' | translate }} - - -
    -
    - -
    - - - - - - - - - -
    - - - - - - - - - - - - - - - - - -
    extension.idextension.type 
    {{ extension.id }}{{ extension.type }} - - - - - edit - - {{ 'extension.edit' | translate }} - - - - delete - - {{ 'extension.delete' | translate }} - - -
    - -
    - - -
    - -
    \ No newline at end of file diff --git a/ui/src/app/extension/extensions-forms/extension-form-http.directive.js b/ui/src/app/extension/extensions-forms/extension-form-http.directive.js deleted file mode 100644 index 5cc2594fea..0000000000 --- a/ui/src/app/extension/extensions-forms/extension-form-http.directive.js +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import 'brace/ext/language_tools'; -import 'brace/ext/searchbox'; -import 'brace/mode/json'; -import 'brace/theme/github'; - -import './extension-form.scss'; - -/* eslint-disable angular/log */ - -import extensionFormHttpTemplate from './extension-form-http.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function ExtensionFormHttpDirective($compile, $templateCache, $translate, types) { - - var linker = function(scope, element) { - - var template = $templateCache.get(extensionFormHttpTemplate); - element.html(template); - - scope.types = types; - scope.theForm = scope.$parent.theForm; - - scope.extensionCustomTransformerOptions = { - useWrapMode: false, - mode: 'json', - showGutter: true, - showPrintMargin: true, - theme: 'github', - advanced: { - enableSnippets: true, - enableBasicAutocompletion: true, - enableLiveAutocompletion: true - }, - onLoad: function(_ace) { - _ace.$blockScrolling = 1; - } - }; - - - scope.addConverterConfig = function() { - var newConverterConfig = {converterId:"", converters:[]}; - scope.converterConfigs.push(newConverterConfig); - - scope.converterConfigs[scope.converterConfigs.length - 1].converters = []; - scope.addConverter(scope.converterConfigs[scope.converterConfigs.length - 1].converters); - }; - - scope.removeConverterConfig = function(config) { - var index = scope.converterConfigs.indexOf(config); - if (index > -1) { - scope.converterConfigs.splice(index, 1); - } - }; - - scope.addConverter = function(converters) { - var newConverter = { - deviceNameJsonExpression:"", - deviceTypeJsonExpression:"", - attributes:[], - timeseries:[] - }; - converters.push(newConverter); - }; - - scope.removeConverter = function(converter, converters) { - var index = converters.indexOf(converter); - if (index > -1) { - converters.splice(index, 1); - } - }; - - scope.addAttribute = function(attributes) { - var newAttribute = {type:"", key:"", value:""}; - attributes.push(newAttribute); - }; - - scope.removeAttribute = function(attribute, attributes) { - var index = attributes.indexOf(attribute); - if (index > -1) { - attributes.splice(index, 1); - } - }; - - - if(scope.isAdd) { - scope.converterConfigs = scope.config.converterConfigurations; - scope.addConverterConfig(); - } else { - scope.converterConfigs = scope.config.converterConfigurations; - } - - scope.transformerTypeChange = function(attribute) { - attribute.transformer = ""; - }; - - scope.validateTransformer = function (model, editorName) { - if(model && model.length) { - try { - angular.fromJson(model); - scope.theForm[editorName].$setValidity('transformerJSON', true); - } catch(e) { - scope.theForm[editorName].$setValidity('transformerJSON', false); - } - } - }; - - scope.collapseValidation = function(index, id) { - var invalidState = angular.element('#'+id+':has(.ng-invalid)'); - if(invalidState.length) { - invalidState.addClass('inner-invalid'); - } - }; - - scope.expandValidation = function (index, id) { - var invalidState = angular.element('#'+id); - invalidState.removeClass('inner-invalid'); - }; - - $compile(element.contents())(scope); - }; - - return { - restrict: "A", - link: linker, - scope: { - config: "=", - isAdd: "=" - } - } -} \ No newline at end of file diff --git a/ui/src/app/extension/extensions-forms/extension-form-http.tpl.html b/ui/src/app/extension/extensions-forms/extension-form-http.tpl.html deleted file mode 100644 index 4e4f846b88..0000000000 --- a/ui/src/app/extension/extensions-forms/extension-form-http.tpl.html +++ /dev/null @@ -1,311 +0,0 @@ - - - - - extension.configuration - - - - - - - {{ 'extension.converter-configurations' | translate }} - - -
    -
      -
    1. - - - - {{ 'action.remove' | translate }} - - - - - - - - -
      -
      extension.field-required
      -
      -
      - - - - - - - - {{ 'extension.converters' | translate }} - - -
      -
        -
      1. - - - - {{ 'action.remove' | translate }} - - - - - - - -
        -
        extension.field-required
        -
        -
        - - - -
        -
        extension.field-required
        -
        -
        - - - - - {{ 'extension.attributes' | translate }} - - -
        -
          -
        1. - - - - {{ 'action.remove' | translate }} - - - - -
          - - - -
          -
          extension.field-required
          -
          -
          - - - - - {{attrTypeValue | translate}} - - -
          -
          extension.field-required
          -
          -
          -
          -
          - - - -
          -
          extension.field-required
          -
          -
          - - - - - - - {{value | translate}} - - - -
          - -
          -
          extension.transformer-json
          -
          -
          -
          -
          - -
          - - -
          -
          -
        2. -
        -
        -
        - - add - extension.add-attribute - -
        -
        -
        -
        - - - - - - {{ 'extension.timeseries' | translate }} - - -
        -
          -
        1. - - - - {{ 'action.remove' | translate }} - - - - -
          - - - -
          -
          extension.field-required
          -
          -
          - - - - - {{attrTypeValue | translate}} - - -
          -
          extension.field-required
          -
          -
          -
          -
          - - - -
          -
          extension.field-required
          -
          -
          - - - - - - - {{value | translate}} - - - -
          - -
          -
          extension.transformer-json
          -
          -
          -
          -
          - -
          - - -
          -
          -
        2. -
        -
        -
        - - add - extension.add-timeseries - -
        -
        -
        -
        -
        -
        -
      2. -
      -
      -
      - - add - extension.add-converter - -
      -
      -
      -
      - -
      -
      -
    2. -
    -
    -
    - - add - extension.add-config - -
    -
    -
    -
    - -
    -
    diff --git a/ui/src/app/extension/extensions-forms/extension-form-modbus.directive.js b/ui/src/app/extension/extensions-forms/extension-form-modbus.directive.js deleted file mode 100644 index 2e01d51591..0000000000 --- a/ui/src/app/extension/extensions-forms/extension-form-modbus.directive.js +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import 'brace/ext/language_tools'; -import 'brace/ext/searchbox'; -import 'brace/mode/json'; -import 'brace/theme/github'; - -import './extension-form.scss'; - -/* eslint-disable angular/log */ - -import extensionFormModbusTemplate from './extension-form-modbus.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function ExtensionFormModbusDirective($compile, $templateCache, $translate, types) { - - - var linker = function(scope, element) { - - function TcpTransport() { - this.type = "tcp", - this.host = "localhost", - this.port = 502, - this.timeout = 5000, - this.reconnect = true, - this.rtuOverTcp = false - } - - function UdpTransport() { - this.type = "udp", - this.host = "localhost", - this.port = 502, - this.timeout = 5000 - } - - function RtuTransport() { - this.type = "rtu", - this.portName = "COM1", - this.encoding = "ascii", - this.timeout = 5000, - this.baudRate = 115200, - this.dataBits = 7, - this.stopBits = 1, - this.parity ="even" - } - - function Server() { - this.transport = new TcpTransport(); - this.devices = [] - } - - function Device() { - this.unitId = 1; - this.deviceName = ""; - this.attributesPollPeriod = 1000; - this.timeseriesPollPeriod = 1000; - this.attributes = []; - this.timeseries = []; - } - - function Tag(globalPollPeriod) { - this.tag = ""; - this.type = "long"; - this.pollPeriod = globalPollPeriod; - this.functionCode = 3; - this.address = 0; - this.registerCount = 1; - this.bit = 0; - this.byteOrder = "BIG"; - } - - - var template = $templateCache.get(extensionFormModbusTemplate); - element.html(template); - - scope.types = types; - scope.theForm = scope.$parent.theForm; - - - if (!scope.configuration.servers.length) { - scope.configuration.servers.push(new Server()); - } - - scope.addServer = function(serversList) { - serversList.push(new Server()); - scope.theForm.$setDirty(); - }; - - scope.addDevice = function(deviceList) { - deviceList.push(new Device()); - scope.theForm.$setDirty(); - }; - - scope.addNewAttribute = function(device) { - device.attributes.push(new Tag(device.attributesPollPeriod)); - scope.theForm.$setDirty(); - }; - - scope.addNewTimeseries = function(device) { - device.timeseries.push(new Tag(device.timeseriesPollPeriod)); - scope.theForm.$setDirty(); - }; - - scope.removeItem = (item, itemList) => { - var index = itemList.indexOf(item); - if (index > -1) { - itemList.splice(index, 1); - } - scope.theForm.$setDirty(); - }; - - scope.onTransportChanged = function(server) { - var type = server.transport.type; - - if (type === "tcp") { - server.transport = new TcpTransport(); - } else if (type === "udp") { - server.transport = new UdpTransport(); - } else if (type === "rtu") { - server.transport = new RtuTransport(); - } - - scope.theForm.$setDirty(); - }; - - $compile(element.contents())(scope); - - - scope.collapseValidation = function(index, id) { - var invalidState = angular.element('#'+id+':has(.ng-invalid)'); - if(invalidState.length) { - invalidState.addClass('inner-invalid'); - } - }; - - scope.expandValidation = function (index, id) { - var invalidState = angular.element('#'+id); - invalidState.removeClass('inner-invalid'); - }; - - }; - - return { - restrict: "A", - link: linker, - scope: { - configuration: "=", - isAdd: "=" - } - } -} \ No newline at end of file diff --git a/ui/src/app/extension/extensions-forms/extension-form-modbus.tpl.html b/ui/src/app/extension/extensions-forms/extension-form-modbus.tpl.html deleted file mode 100644 index 25dece44c3..0000000000 --- a/ui/src/app/extension/extensions-forms/extension-form-modbus.tpl.html +++ /dev/null @@ -1,794 +0,0 @@ - - - - - extension.configuration - - - - - - - - {{ 'extension.modbus-server' | translate }} - - - -
    - extension.modbus-add-server-prompt -
    - -
    -
      -
    1. - - - - {{ 'action.remove' | translate }} - - - - - - -
      - - - - - - -
      -
      extension.field-required
      -
      -
      - -
      - -
      -
      - - - -
      -
      extension.field-required
      -
      -
      - - - - -
      -
      extension.field-required
      -
      extension.port-range
      -
      extension.port-range
      -
      -
      - - - - -
      -
      extension.field-required
      -
      -
      -
      - -
      - - {{ 'extension.modbus-tcp-reconnect' | translate }} - - - - - {{ 'extension.modbus-rtu-over-tcp' | translate }} - - -
      -
      - -
      - -
      - - - -
      -
      extension.field-required
      -
      -
      - - - - -
      -
      extension.field-required
      -
      -
      -
      - -
      - - - - - -
      -
      extension.field-required
      -
      -
      - - - - - - {{parityValue}} - - -
      -
      extension.field-required
      -
      -
      - -
      - -
      - - - -
      -
      extension.field-required
      -
      -
      - - - - -
      -
      extension.field-required
      -
      extension.modbus-databits-range
      -
      extension.modbus-databits-range
      -
      -
      - - - - -
      -
      extension.field-required
      -
      extension.modbus-stopbits-range
      -
      extension.modbus-stopbits-range
      -
      -
      -
      -
      - - - - - {{ 'extension.mapping' | translate }} - - -
      -
        -
      1. - - - - {{ 'action.remove' | translate }} - - - - - -
        - - - -
        -
        extension.field-required
        -
        -
        - - - - - -
        -
        extension.field-required
        -
        extension.modbus-unit-id-range
        -
        extension.modbus-unit-id-range
        -
        -
        -
        - -
        - - - - -
        -
        extension.field-required
        -
        extension.modbus-poll-period-range
        -
        -
        - - - - - -
        -
        extension.field-required
        -
        extension.modbus-poll-period-range
        -
        -
        - -
        - - - - - - {{ 'extension.attributes' | translate }} - - -
        -
          -
        1. - - - - {{ 'action.remove' | translate }} - - - - - -
          - - - -
          -
          extension.field-required
          -
          -
          - - - - - - {{attrTypeValue | translate}} - - -
          -
          extension.field-required
          -
          -
          - - - - - -
          -
          extension.modbus-poll-period-range
          -
          -
          -
          - -
          - - - - - {{functionName}} - - -
          -
          extension.field-required
          -
          -
          - - - - - -
          -
          extension.field-required
          -
          extension.modbus-register-address-range
          -
          extension.modbus-register-address-range
          -
          -
          - -
          - -
          - - - - -
          -
          extension.modbus-register-count-range
          -
          -
          - - - - - -
          -
          extension.field-required
          -
          extension.modbus-register-bit-index-range
          -
          extension.modbus-register-bit-index-range
          -
          -
          -
          - -
          - - - -
          -
          extension.field-required
          -
          -
          - -
          - - -
          -
          -
        2. -
        -
        -
        - - add - extension.add-attribute - -
        -
        -
        -
        - - - - - {{ 'extension.timeseries' | translate }} - - -
        -
          -
        1. - - - - {{ 'action.remove' | translate }} - - - - - -
          - - - -
          -
          extension.field-required
          -
          -
          - - - - - - {{attrTypeValue | translate}} - - -
          -
          extension.field-required
          -
          -
          - - - - - -
          -
          extension.modbus-poll-period-range
          -
          -
          -
          - -
          - - - - - {{functionName}} - - -
          -
          extension.field-required
          -
          -
          - - - - - -
          -
          extension.field-required
          -
          extension.modbus-register-address-range
          -
          extension.modbus-register-address-range
          -
          -
          -
          - -
          - - - - -
          -
          extension.modbus-register-count-range
          -
          -
          - - - - - -
          -
          extension.field-required
          -
          extension.modbus-register-bit-index-range
          -
          extension.modbus-register-bit-index-range
          -
          -
          -
          - -
          - - - -
          -
          extension.field-required
          -
          -
          - -
          -
          -
          -
        2. -
        -
        -
        - - add - extension.add-timeseries - -
        -
        -
        -
        - - -
        -
        -
      2. -
      -
      -
      - - add - extension.add-device - -
      -
      -
      -
      - -
      -
      -
    2. -
    - -
    - - add - extension.modbus-add-server - -
    - -
    -
    -
    -
    - -
    -
    \ No newline at end of file diff --git a/ui/src/app/extension/extensions-forms/extension-form-mqtt.directive.js b/ui/src/app/extension/extensions-forms/extension-form-mqtt.directive.js deleted file mode 100644 index 22c58187ae..0000000000 --- a/ui/src/app/extension/extensions-forms/extension-form-mqtt.directive.js +++ /dev/null @@ -1,346 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './extension-form.scss'; - -/* eslint-disable angular/log */ - -import extensionFormMqttTemplate from './extension-form-mqtt.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function ExtensionFormHttpDirective($compile, $templateCache, $translate, types) { - - var linker = function(scope, element) { - - var template = $templateCache.get(extensionFormMqttTemplate); - element.html(template); - - scope.types = types; - scope.theForm = scope.$parent.theForm; - - scope.deviceNameExpressions = { - deviceNameJsonExpression: "extension.converter-json", - deviceNameTopicExpression: "extension.topic" - }; - scope.deviceTypeExpressions = { - deviceTypeJsonExpression: "extension.converter-json", - deviceTypeTopicExpression: "extension.topic" - }; - scope.attributeKeyExpressions = { - attributeKeyJsonExpression: "extension.converter-json", - attributeKeyTopicExpression: "extension.topic" - }; - scope.requestIdExpressions = { - requestIdJsonExpression: "extension.converter-json", - requestIdTopicExpression: "extension.topic" - } - - scope.extensionCustomConverterOptions = { - useWrapMode: false, - mode: 'json', - showGutter: true, - showPrintMargin: true, - theme: 'github', - advanced: { - enableSnippets: true, - enableBasicAutocompletion: true, - enableLiveAutocompletion: true - }, - onLoad: function(_ace) { - _ace.$blockScrolling = 1; - } - }; - - scope.updateValidity = function () { - if(scope.brokers.length) { - for(let i=0;i -1) { - scope.brokers.splice(index, 1); - } - }; - - if(scope.isAdd) { - scope.brokers = []; - scope.config.brokers = scope.brokers; - scope.addBroker(); - } else { - scope.brokers = scope.config.brokers; - } - - scope.addMap = function(mapping) { - var newMap = {topicFilter:"sensors", converter:{attributes:[],timeseries:[]}}; - - mapping.push(newMap); - }; - - scope.removeMap = function(map, mapping) { - var index = mapping.indexOf(map); - if (index > -1) { - mapping.splice(index, 1); - } - }; - - scope.addAttribute = function(attributes) { - var newAttribute = {type:"", key:"", value:""}; - attributes.push(newAttribute); - }; - - scope.removeAttribute = function(attribute, attributes) { - var index = attributes.indexOf(attribute); - if (index > -1) { - attributes.splice(index, 1); - } - }; - - scope.addConnectRequest = function(requests, type) { - var newRequest = {}; - if(type == "connect") { - newRequest.topicFilter = "sensors/connect"; - } else { - newRequest.topicFilter = "sensors/disconnect"; - } - requests.push(newRequest); - }; - - scope.addAttributeRequest = function(requests) { - var newRequest = { - topicFilter: "sensors/attributes", - clientScope: false, - responseTopicExpression: "sensors/${deviceName}/attributes/${responseId}", - valueExpression: "${attributeValue}" - }; - requests.push(newRequest); - }; - - scope.addAttributeUpdate = function(updates) { - var newUpdate = { - deviceNameFilter: ".*", - attributeFilter: ".*", - topicExpression: "sensor/${deviceName}/${attributeKey}", - valueExpression: "{\"${attributeKey}\":\"${attributeValue}\"}" - } - updates.push(newUpdate); - }; - - scope.addServerSideRpc = function(rpcRequests) { - var newRpc = { - deviceNameFilter: ".*", - methodFilter: "echo", - requestTopicExpression: "sensor/${deviceName}/request/${methodName}/${requestId}", - responseTopicExpression: "sensor/${deviceName}/response/${methodName}/${requestId}", - responseTimeout: 10000, - valueExpression: "${params}" - }; - rpcRequests.push(newRpc); - }; - - scope.changeCredentials = function(broker) { - var type = broker.credentials.type; - broker.credentials = {}; - broker.credentials.type = type; - }; - - scope.changeConverterType = function(map) { - if(map.converterType == "custom"){ - map.converter = ""; - } - if(map.converterType == "json") { - map.converter = {attributes:[],timeseries:[]}; - } - }; - - scope.changeNameExpression = function(element, type) { - if(element.nameExp == "deviceNameJsonExpression") { - if(element.deviceNameTopicExpression) { - delete element.deviceNameTopicExpression; - } - if(type) { - element.deviceNameJsonExpression = "${$.serialNumber}"; - } - } - if(element.nameExp == "deviceNameTopicExpression") { - if(element.deviceNameJsonExpression) { - delete element.deviceNameJsonExpression; - } - if(type && type == "connect") { - element.deviceNameTopicExpression = "(?<=sensor\\/)(.*?)(?=\\/connect)"; - } - if(type && type == "disconnect") { - element.deviceNameTopicExpression = "(?<=sensor\\/)(.*?)(?=\\/disconnect)"; - } - if(type && type == "attribute") { - element.deviceNameTopicExpression = "(?<=sensors\\/)(.*?)(?=\\/attributes)"; - } - } - }; - - scope.changeTypeExpression = function(converter) { - if(converter.typeExp == "deviceTypeJsonExpression") { - if(converter.deviceTypeTopicExpression) { - delete converter.deviceTypeTopicExpression; - } - } - if(converter.typeExp == "deviceTypeTopicExpression") { - if(converter.deviceTypeJsonExpression) { - delete converter.deviceTypeJsonExpression; - } - } - }; - - scope.changeAttrKeyExpression = function(request) { - if(request.attrKey == "attributeKeyJsonExpression") { - if(request.attributeKeyTopicExpression) { - delete request.attributeKeyTopicExpression; - } - request.attributeKeyJsonExpression = "${$.key}"; - } - if(request.attrKey == "attributeKeyTopicExpression") { - if(request.attributeKeyJsonExpression) { - delete request.attributeKeyJsonExpression; - } - request.attributeKeyTopicExpression = "(?<=attributes\\/)(.*?)(?=\\/request)"; - } - }; - - scope.changeRequestIdExpression = function(request) { - if(request.requestId == "requestIdJsonExpression") { - if(request.requestIdTopicExpression) { - delete request.requestIdTopicExpression; - } - request.requestIdJsonExpression = "${$.requestId}"; - } - if(request.requestId == "requestIdTopicExpression") { - if(request.requestIdJsonExpression) { - delete request.requestIdJsonExpression; - } - request.requestIdTopicExpression = "(?<=request\\/)(.*?)($)"; - } - }; - - scope.validateCustomConverter = function(model, editorName) { - if(model && model.length) { - try { - angular.fromJson(model); - scope.theForm[editorName].$setValidity('converterJSON', true); - } catch(e) { - scope.theForm[editorName].$setValidity('converterJSON', false); - } - } - }; - - scope.fileAdded = function($file, broker, fileType) { - var reader = new FileReader(); - reader.onload = function(event) { - scope.$apply(function() { - if(event.target.result) { - scope.theForm.$setDirty(); - var addedFile = event.target.result; - if (addedFile && addedFile.length > 0) { - if(fileType == "caCert") { - broker.credentials.caCertFileName = $file.name; - broker.credentials.caCert = addedFile.replace(/^data.*base64,/, ""); - } - if(fileType == "privateKey") { - broker.credentials.privateKeyFileName = $file.name; - broker.credentials.privateKey = addedFile.replace(/^data.*base64,/, ""); - } - if(fileType == "Cert") { - broker.credentials.certFileName = $file.name; - broker.credentials.cert = addedFile.replace(/^data.*base64,/, ""); - } - } - } - }); - }; - reader.readAsDataURL($file.file); - }; - - scope.clearFile = function(broker, fileType) { - scope.theForm.$setDirty(); - if(fileType == "caCert") { - broker.credentials.caCertFileName = null; - broker.credentials.caCert = null; - } - if(fileType == "privateKey") { - broker.credentials.privateKeyFileName = null; - broker.credentials.privateKey = null; - } - if(fileType == "Cert") { - broker.credentials.certFileName = null; - broker.credentials.cert = null; - } - }; - - scope.collapseValidation = function(index, id) { - var invalidState = angular.element('#'+id+':has(.ng-invalid)'); - if(invalidState.length) { - invalidState.addClass('inner-invalid'); - } - }; - - scope.expandValidation = function (index, id) { - var invalidState = angular.element('#'+id); - invalidState.removeClass('inner-invalid'); - }; - - $compile(element.contents())(scope); - }; - - return { - restrict: "A", - link: linker, - scope: { - config: "=", - isAdd: "=" - } - } -} \ No newline at end of file diff --git a/ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html b/ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html deleted file mode 100644 index b215060298..0000000000 --- a/ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html +++ /dev/null @@ -1,860 +0,0 @@ - - - - - extension.configuration - - - - - - - {{ 'extension.brokers' | translate }} - - -
    -
      -
    1. - - - - {{ 'action.remove' | translate }} - - - - -
      - - - -
      -
      extension.field-required
      -
      extension.port-range
      -
      extension.port-range
      -
      -
      - - - -
      -
      extension.field-required
      -
      -
      -
      -
      - - - -
      -
      extension.field-required
      -
      -
      - - - - - {{credentialsValue.name | translate}} - - - - - {{ 'extension.ssl' | translate }} - - -
      -
      - - - -
      -
      extension.field-required
      -
      -
      - - - -
      -
      extension.field-required
      -
      -
      -
      - - - - - - {{ 'extension.mapping' | translate }} - - -
      -
        -
      1. - - - - {{ 'action.remove' | translate }} - - - - -
        - - - - - {{value | translate}} - - -
        -
        extension.field-required
        -
        -
        - - - -
        -
        extension.field-required
        -
        -
        -
        - -
        -
        - - - - - {{value | translate}} - - -
        -
        extension.field-required
        -
        -
        - - - -
        -
        extension.field-required
        -
        -
        - - - -
        -
        extension.field-required
        -
        -
        -
        -
        - - - - - {{value | translate}} - - -
        -
        extension.field-required
        -
        -
        - - - -
        -
        extension.field-required
        -
        -
        - - - -
        -
        extension.field-required
        -
        -
        -
        -
        - - - - - - - -
        -
        extension.field-required
        -
        -
        -
        -
        - -
        -
        extension.transformer-json
        -
        -
        -
        -
        - -
        - - - - - {{ 'extension.attributes' | translate }} - - -
        -
          -
        1. - - - - {{ 'action.remove' | translate }} - - - - -
          - - - -
          -
          extension.field-required
          -
          -
          - - - - - {{attrTypeValue | translate}} - - -
          -
          extension.field-required
          -
          -
          -
          - - - -
          -
          extension.field-required
          -
          -
          -
          -
          -
        2. -
        -
        -
        - - add - extension.add-attribute - -
        -
        -
        -
        - - - - - {{ 'extension.timeseries' | translate }} - - -
        -
          -
        1. - - - - {{ 'action.remove' | translate }} - - - - -
          - - - -
          -
          extension.field-required
          -
          -
          - - - - - {{attrTypeValue | translate}} - - -
          -
          extension.field-required
          -
          -
          -
          - - - -
          -
          extension.field-required
          -
          -
          -
          -
          -
        2. -
        -
        -
        - - add - extension.add-timeseries - -
        -
        -
        -
        - -
        -
        -
      2. -
      -
      -
      - - add - extension.add-map - -
      -
      -
      -
      - - - - - {{ 'extension.connect-requests' | translate }} - - -
      -
        -
      1. - - - - {{ 'action.remove' | translate }} - - - - - - - -
        -
        extension.field-required
        -
        -
        -
        - - - - - {{value | translate}} - - -
        -
        extension.field-required
        -
        -
        - - - -
        -
        extension.field-required
        -
        -
        - - - -
        -
        extension.field-required
        -
        -
        -
        -
        -
        -
      2. -
      -
      -
      - - add - extension.add-connect-request - -
      -
      -
      -
      - - - - - {{ 'extension.disconnect-requests' | translate }} - - -
      -
        -
      1. - - - - {{ 'action.remove' | translate }} - - - - - - - -
        -
        extension.field-required
        -
        -
        -
        - - - - - {{value | translate}} - - -
        -
        extension.field-required
        -
        -
        - - - -
        -
        extension.field-required
        -
        -
        - - - -
        -
        extension.field-required
        -
        -
        -
        -
        -
        -
      2. -
      -
      -
      - - add - extension.add-disconnect-request - -
      -
      -
      -
      - - - - - {{ 'extension.attribute-requests' | translate }} - - -
      -
        -
      1. - - - - {{ 'action.remove' | translate }} - - - - -
        - - - -
        -
        extension.field-required
        -
        -
        - - {{ 'extension.client-scope' | translate }} - - -
        -
        - - - - - {{value | translate}} - - -
        -
        extension.field-required
        -
        -
        - - - -
        -
        extension.field-required
        -
        -
        - - - -
        -
        extension.field-required
        -
        -
        -
        - -
        - - - - - {{value | translate}} - - -
        -
        extension.field-required
        -
        -
        - - - -
        -
        extension.field-required
        -
        -
        - - - -
        -
        extension.field-required
        -
        -
        -
        - -
        - - - - - {{value | translate}} - - -
        -
        extension.field-required
        -
        -
        - - - -
        -
        extension.field-required
        -
        -
        - - - -
        -
        extension.field-required
        -
        -
        -
        - - - - -
        -
        extension.field-required
        -
        -
        - - - -
        -
        extension.field-required
        -
        -
        -
        -
        -
      2. -
      -
      -
      - - add - extension.add-attribute-request - -
      -
      -
      -
      - - - - - {{ 'extension.attribute-updates' | translate }} - - -
      -
        -
      1. - - - - {{ 'action.remove' | translate }} - - - - -
        - - - -
        -
        extension.field-required
        -
        -
        - - - -
        -
        extension.field-required
        -
        -
        -
        - - - -
        -
        extension.field-required
        -
        -
        - - - -
        -
        extension.field-required
        -
        -
        -
        -
        -
      2. -
      -
      -
      - - add - extension.add-attribute-update - -
      -
      -
      -
      - - - - - {{ 'extension.server-side-rpc' | translate }} - - -
      -
        -
      1. - - - - {{ 'action.remove' | translate }} - - - - -
        - - - -
        -
        extension.field-required
        -
        -
        - - - -
        -
        extension.field-required
        -
        -
        -
        - - - -
        -
        extension.field-required
        -
        -
        - - - - -
        - - - - - - - -
        -
        extension.field-required
        -
        -
        -
        -
        -
        -
      2. -
      -
      -
      - - add - extension.add-server-side-rpc-request - -
      -
      -
      -
      - -
      -
      -
    2. -
    -
    - -
    - - add - extension.add-broker - -
    -
    -
    -
    - -
    -
    diff --git a/ui/src/app/extension/extensions-forms/extension-form-opc.directive.js b/ui/src/app/extension/extensions-forms/extension-form-opc.directive.js deleted file mode 100644 index ac3ee55ca2..0000000000 --- a/ui/src/app/extension/extensions-forms/extension-form-opc.directive.js +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import 'brace/ext/language_tools'; -import 'brace/ext/searchbox'; -import 'brace/mode/json'; -import 'brace/theme/github'; - -import './extension-form.scss'; - -/* eslint-disable angular/log */ - -import extensionFormOpcTemplate from './extension-form-opc.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function ExtensionFormOpcDirective($compile, $templateCache, $translate, types) { - - - var linker = function(scope, element) { - - - function Server() { - this.applicationName = "Thingsboard OPC-UA client"; - this.applicationUri = ""; - this.host = "localhost"; - this.port = 49320; - this.scanPeriodInSeconds = 10; - this.timeoutInMillis = 5000; - this.security = "Basic128Rsa15"; - this.identity = { - "type": "anonymous" - }; - this.keystore = { - "type": "PKCS12", - "location": "example.pfx", - "password": "secret", - "alias": "gateway", - "keyPassword": "secret" - }; - this.mapping = [] - } - - function Map() { - this.deviceNodePattern = "Channel1\\.Device\\d+$"; - this.deviceNamePattern = "Device ${_System._DeviceId}"; - this.attributes = []; - this.timeseries = []; - } - - function Attribute() { - this.key = "Tag1"; - this.type = "string"; - this.value = "${Tag1}"; - } - - function Timeseries() { - this.key = "Tag2"; - this.type = "long"; - this.value = "${Tag2}"; - } - - - var template = $templateCache.get(extensionFormOpcTemplate); - element.html(template); - - scope.types = types; - scope.theForm = scope.$parent.theForm; - - - if (!scope.configuration.servers.length) { - scope.configuration.servers.push(new Server()); - } - - scope.addServer = function(serversList) { - serversList.push(new Server()); - // scope.addMap(serversList[serversList.length-1].mapping); - - scope.theForm.$setDirty(); - }; - - scope.addMap = function(mappingList) { - mappingList.push(new Map()); - scope.theForm.$setDirty(); - }; - - scope.addNewAttribute = function(attributesList) { - attributesList.push(new Attribute()); - scope.theForm.$setDirty(); - }; - - scope.addNewTimeseries = function(timeseriesList) { - timeseriesList.push(new Timeseries()); - scope.theForm.$setDirty(); - }; - - - scope.removeItem = (item, itemList) => { - var index = itemList.indexOf(item); - if (index > -1) { - itemList.splice(index, 1); - } - scope.theForm.$setDirty(); - }; - - - $compile(element.contents())(scope); - - - scope.fileAdded = function($file, model, options) { - let reader = new FileReader(); - reader.onload = function(event) { - scope.$apply(function() { - if(event.target.result) { - scope.theForm.$setDirty(); - let addedFile = event.target.result; - - if (addedFile && addedFile.length > 0) { - model[options.location] = $file.name; - model[options.fileContent] = addedFile.replace(/^data.*base64,/, ""); - - } - } - }); - }; - reader.readAsDataURL($file.file); - - }; - - scope.clearFile = function(model, options) { - scope.theForm.$setDirty(); - - model[options.location] = null; - model[options.fileContent] = null; - - }; - - scope.collapseValidation = function(index, id) { - var invalidState = angular.element('#'+id+':has(.ng-invalid)'); - if(invalidState.length) { - invalidState.addClass('inner-invalid'); - } - }; - - scope.expandValidation = function (index, id) { - var invalidState = angular.element('#'+id); - invalidState.removeClass('inner-invalid'); - }; - - }; - - return { - restrict: "A", - link: linker, - scope: { - configuration: "=", - isAdd: "=" - } - } -} \ No newline at end of file diff --git a/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html b/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html deleted file mode 100644 index c955d08e82..0000000000 --- a/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html +++ /dev/null @@ -1,554 +0,0 @@ - - - - - extension.configuration - - - - - - - - {{ 'extension.opc-server' | translate }} - - - -
    - extension.opc-add-server-prompt -
    - -
    -
      -
    1. - - - - {{ 'action.remove' | translate }} - - - - - - -
      - - - -
      -
      extension.field-required
      -
      -
      - - - - - - -
      - - -
      - - - -
      -
      extension.field-required
      -
      -
      - - - - -
      -
      extension.field-required
      -
      extension.port-range
      -
      extension.port-range
      -
      -
      -
      - -
      - - - -
      -
      extension.field-required
      -
      -
      - - - - -
      -
      extension.field-required
      -
      -
      -
      - -
      - - - - - - -
      -
      extension.field-required
      -
      -
      - - - - - - -
      -
      extension.field-required
      -
      -
      -
      - -
      - -
      -
      - - - -
      -
      extension.field-required
      -
      -
      - - - - -
      -
      extension.field-required
      -
      -
      -
      - - - - - {{ 'extension.opc-keystore' | translate }} - - - - - - - - -
      -
      extension.field-required
      -
      -
      - - - - -
      - - - -
      -
      extension.field-required
      -
      -
      - - - - -
      -
      extension.field-required
      -
      -
      -
      - - - - -
      -
      extension.field-required
      -
      -
      - -
      -
      -
      - - - - - - {{ 'extension.mapping' | translate }} - - -
      -
        -
      1. - - - - {{ 'action.remove' | translate }} - - - - - -
        - - - -
        -
        extension.field-required
        -
        -
        - - - - -
        -
        extension.field-required
        -
        -
        -
        - - - - - - {{ 'extension.attributes' | translate }} - - -
        -
          -
        1. - - - - {{ 'action.remove' | translate }} - - - - - -
          - - - -
          -
          extension.field-required
          -
          -
          - - - - - {{attrTypeValue | translate}} - - -
          -
          extension.field-required
          -
          -
          -
          - -
          - - - -
          -
          extension.field-required
          -
          -
          - -
          - - -
          -
          -
        2. -
        -
        -
        - - add - extension.add-attribute - -
        -
        -
        -
        - - - - - {{ 'extension.timeseries' | translate }} - - -
        -
          -
        1. - - - - {{ 'action.remove' | translate }} - - - - -
          - - - -
          -
          extension.field-required
          -
          -
          - - - - - {{attrTypeValue | translate}} - - -
          -
          extension.field-required
          -
          -
          -
          -
          - - - -
          -
          extension.field-required
          -
          -
          -
          -
          -
          -
        2. -
        -
        -
        - - add - extension.add-timeseries - -
        -
        -
        -
        - - -
        -
        -
      2. -
      -
      -
      - - add - extension.add-map - -
      -
      -
      -
      - -
      -
      -
    2. -
    - -
    - - add - extension.opc-add-server - -
    - -
    -
    -
    -
    - -
    -
    \ No newline at end of file diff --git a/ui/src/app/extension/extensions-forms/extension-form.scss b/ui/src/app/extension/extensions-forms/extension-form.scss deleted file mode 100644 index 1143a603e2..0000000000 --- a/ui/src/app/extension/extensions-forms/extension-form.scss +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.extension-form { - li > .md-button { - margin: 0; - color: rgba(0, 0, 0, .7); - } - - .vAccordion--default { - padding-left: 3px; - margin-top: 0; - } - - .tb-container { - width: 100%; - } - - .dropdown-messages { - .tb-error-message { - padding: 5px 0 0 0; - } - } - - .dropdown-section { - margin-bottom: 30px; - } - - v-pane.inner-invalid > v-pane-header { - border-bottom: 2px solid rgb(221, 44, 0); - } -} - -.extension-form.extension-mqtt { - md-checkbox{ - margin-left: 10px; - } -} - -.tb-extension-custom-transformer-panel { - height: 100%; - margin-left: 15px; - border: 1px solid #c0c0c0; - - .tb-extension-custom-transformer { - width: 100%; - min-width: 600px; - height: 100%; - min-height: 200px; - } - - .ace_text-input { - position: absolute !important; - } -} - -.extensionDialog { - min-width: 1000px; -} - -.tb-container-for-select { - height: 58px; -} - -.tb-drop-file-input-hide { - position: absolute; - bottom: 0; - display: block; - width: 100%; - height: 200%; -} diff --git a/ui/src/app/extension/index.js b/ui/src/app/extension/index.js deleted file mode 100644 index 30b1ff79be..0000000000 --- a/ui/src/app/extension/index.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import ExtensionTableDirective from './extension-table.directive'; -import ExtensionFormHttpDirective from './extensions-forms/extension-form-http.directive'; -import ExtensionFormMqttDirective from './extensions-forms/extension-form-mqtt.directive' -import ExtensionFormOpcDirective from './extensions-forms/extension-form-opc.directive'; -import ExtensionFormModbusDirective from './extensions-forms/extension-form-modbus.directive'; - -import {ParseToNull} from './extension-dialog.controller'; - -export default angular.module('thingsboard.extension', []) - .directive('tbExtensionTable', ExtensionTableDirective) - .directive('tbExtensionFormHttp', ExtensionFormHttpDirective) - .directive('tbExtensionFormMqtt', ExtensionFormMqttDirective) - .directive('tbExtensionFormOpc', ExtensionFormOpcDirective) - .directive('tbExtensionFormModbus', ExtensionFormModbusDirective) - .directive('parseToNull', ParseToNull) - .name; \ No newline at end of file diff --git a/ui/src/app/global-interceptor.service.js b/ui/src/app/global-interceptor.service.js deleted file mode 100644 index 331a0d4134..0000000000 --- a/ui/src/app/global-interceptor.service.js +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/*@ngInject*/ -export default function GlobalInterceptor($rootScope, $q, $injector) { - - var toast; - var translate; - var userService; - var types; - var http; - var timeout; - - var internalUrlPrefixes = [ - '/api/auth/token', - '/api/plugins/rpc' - ]; - - var service = { - request: request, - requestError: requestError, - response: response, - responseError: responseError - } - - return service; - - function getToast() { - if (!toast) { - toast = $injector.get("toast"); - } - return toast; - } - - function getTranslate() { - if (!translate) { - translate = $injector.get("$translate"); - } - return translate; - } - - function getUserService() { - if (!userService) { - userService = $injector.get("userService"); - } - return userService; - } - - function getTypes() { - if (!types) { - types = $injector.get("types"); - } - return types; - } - - function getHttp() { - if (!http) { - http = $injector.get("$http"); - } - return http; - } - - function getTimeout() { - if (!timeout) { - timeout = $injector.get("$timeout"); - } - return timeout; - } - - function rejectionErrorCode(rejection) { - if (rejection && rejection.data && rejection.data.errorCode) { - return rejection.data.errorCode; - } else { - return undefined; - } - } - - function isTokenBasedAuthEntryPoint(url) { - return url.startsWith('/api/') && - !url.startsWith(getTypes().entryPoints.login) && - !url.startsWith(getTypes().entryPoints.tokenRefresh) && - !url.startsWith(getTypes().entryPoints.nonTokenBased); - } - - function refreshTokenAndRetry(request) { - return getUserService().refreshJwtToken().then(function success() { - getUserService().updateAuthorizationHeader(request.config.headers); - return getHttp()(request.config); - }, function fail(message) { - $rootScope.$broadcast('unauthenticated'); - request.status = 401; - request.data = {}; - request.data.message = message || getTranslate().instant('access.unauthorized'); - return $q.reject(request); - }); - } - - function isInternalUrlPrefix(url) { - for (var index in internalUrlPrefixes) { - if (url.startsWith(internalUrlPrefixes[index])) { - return true; - } - } - return false; - } - - function request(config) { - var rejected = false; - if (config.url.startsWith('/api/')) { - var isLoading = !isInternalUrlPrefix(config.url); - updateLoadingState(config, isLoading); - if (isTokenBasedAuthEntryPoint(config.url)) { - if (!getUserService().updateAuthorizationHeader(config.headers) && - !getUserService().refreshTokenPending()) { - updateLoadingState(config, false); - rejected = true; - getUserService().clearJwtToken(false); - return $q.reject({ data: {message: getTranslate().instant('access.unauthorized')}, status: 401, config: config}); - } else if (!getUserService().isJwtTokenValid()) { - return $q.reject({ refreshTokenPending: true, config: config }); - } - } - } - if (!rejected) { - return config; - } - } - - function requestError(rejection) { - if (rejection.config.url.startsWith('/api/')) { - updateLoadingState(rejection.config, false); - } - return $q.reject(rejection); - } - - function response(response) { - if (response.config.url.startsWith('/api/')) { - updateLoadingState(response.config, false); - } - return response; - } - - function retryRequest (httpConfig) { - var thisTimeout = 1000 + Math.random() * 3000; - return getTimeout()(function() { - return getHttp()(httpConfig); - }, thisTimeout); - } - - function responseError(rejection) { - if (rejection.config.url.startsWith('/api/')) { - updateLoadingState(rejection.config, false); - } - var unhandled = false; - var ignoreErrors = rejection.config.ignoreErrors; - var resendRequest = rejection.config.resendRequest; - if (rejection.refreshTokenPending || rejection.status === 401) { - var errorCode = rejectionErrorCode(rejection); - if (rejection.refreshTokenPending || (errorCode && errorCode === getTypes().serverErrorCode.jwtTokenExpired)) { - return refreshTokenAndRetry(rejection); - } else if (errorCode !== getTypes().serverErrorCode.credentialsExpired) { - unhandled = true; - } - } else if (rejection.status === 403) { - if (!ignoreErrors) { - $rootScope.$broadcast('forbidden'); - } - } else if (rejection.status === 429) { - if (resendRequest) { - return retryRequest(rejection.config); - } - } else if (rejection.status === 0 || rejection.status === -1) { - getToast().showError(getTranslate().instant('error.unable-to-connect')); - } else if (!rejection.config.url.startsWith('/api/plugins/rpc')) { - if (rejection.status === 404) { - if (!ignoreErrors) { - getToast().showError(rejection.config.method + ": " + rejection.config.url + "
    " + - rejection.status + ": " + rejection.statusText); - } - } else { - unhandled = true; - } - } - - if (unhandled && !ignoreErrors) { - if (rejection.data && !rejection.data.message) { - getToast().showError(rejection.data); - } else if (rejection.data && rejection.data.message) { - getToast().showError(rejection.data.message); - } else { - getToast().showError(getTranslate().instant('error.unhandled-error-code', {errorCode: rejection.status})); - } - } - return $q.reject(rejection); - } - - function updateLoadingState(config, isLoading) { - if (!config || angular.isUndefined(config.ignoreLoading) || !config.ignoreLoading) { - $rootScope.loading = isLoading; - } - } -} diff --git a/ui/src/app/help/help-links.constant.js b/ui/src/app/help/help-links.constant.js deleted file mode 100644 index b1227fc9af..0000000000 --- a/ui/src/app/help/help-links.constant.js +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -var ruleNodeClazzHelpLinkMap = { - 'org.thingsboard.rule.engine.filter.TbCheckRelationNode': 'ruleNodeCheckRelation', - 'org.thingsboard.rule.engine.filter.TbJsFilterNode': 'ruleNodeJsFilter', - 'org.thingsboard.rule.engine.filter.TbJsSwitchNode': 'ruleNodeJsSwitch', - 'org.thingsboard.rule.engine.filter.TbMsgTypeFilterNode': 'ruleNodeMessageTypeFilter', - 'org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode': 'ruleNodeMessageTypeSwitch', - 'org.thingsboard.rule.engine.filter.TbOriginatorTypeFilterNode': 'ruleNodeOriginatorTypeFilter', - 'org.thingsboard.rule.engine.filter.TbOriginatorTypeSwitchNode': 'ruleNodeOriginatorTypeSwitch', - 'org.thingsboard.rule.engine.metadata.TbGetAttributesNode': 'ruleNodeOriginatorAttributes', - 'org.thingsboard.rule.engine.metadata.TbGetOriginatorFieldsNode': 'ruleNodeOriginatorFields', - 'org.thingsboard.rule.engine.metadata.TbGetCustomerAttributeNode': 'ruleNodeCustomerAttributes', - 'org.thingsboard.rule.engine.metadata.TbGetDeviceAttrNode': 'ruleNodeDeviceAttributes', - 'org.thingsboard.rule.engine.metadata.TbGetRelatedAttributeNode': 'ruleNodeRelatedAttributes', - 'org.thingsboard.rule.engine.metadata.TbGetTenantAttributeNode': 'ruleNodeTenantAttributes', - 'org.thingsboard.rule.engine.transform.TbChangeOriginatorNode': 'ruleNodeChangeOriginator', - 'org.thingsboard.rule.engine.transform.TbTransformMsgNode': 'ruleNodeTransformMsg', - 'org.thingsboard.rule.engine.mail.TbMsgToEmailNode': 'ruleNodeMsgToEmail', - 'org.thingsboard.rule.engine.action.TbClearAlarmNode': 'ruleNodeClearAlarm', - 'org.thingsboard.rule.engine.action.TbCreateAlarmNode': 'ruleNodeCreateAlarm', - 'org.thingsboard.rule.engine.delay.TbMsgDelayNode': 'ruleNodeMsgDelay', - 'org.thingsboard.rule.engine.debug.TbMsgGeneratorNode': 'ruleNodeMsgGenerator', - 'org.thingsboard.rule.engine.action.TbLogNode': 'ruleNodeLog', - 'org.thingsboard.rule.engine.rpc.TbSendRPCReplyNode': 'ruleNodeRpcCallReply', - 'org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode': 'ruleNodeRpcCallRequest', - 'org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode': 'ruleNodeSaveAttributes', - 'org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode': 'ruleNodeSaveTimeseries', - 'tb.internal.RuleChain': 'ruleNodeRuleChain', - 'org.thingsboard.rule.engine.aws.sns.TbSnsNode': 'ruleNodeAwsSns', - 'org.thingsboard.rule.engine.aws.sqs.TbSqsNode': 'ruleNodeAwsSqs', - 'org.thingsboard.rule.engine.kafka.TbKafkaNode': 'ruleNodeKafka', - 'org.thingsboard.rule.engine.mqtt.TbMqttNode': 'ruleNodeMqtt', - 'org.thingsboard.rule.engine.mqtt.azure.TbAzureIotHubNode': 'ruleNodeAzureIotHub', - 'org.thingsboard.rule.engine.rabbitmq.TbRabbitMqNode': 'ruleNodeRabbitMq', - 'org.thingsboard.rule.engine.rest.TbRestApiCallNode': 'ruleNodeRestApiCall', - 'org.thingsboard.rule.engine.mail.TbSendEmailNode': 'ruleNodeSendEmail' -}; - -var helpBaseUrl = "https://thingsboard.io"; - -export default angular.module('thingsboard.help', []) - .constant('helpLinks', - { - linksMap: { - outgoingMailSettings: helpBaseUrl + "/docs/user-guide/ui/mail-settings", - securitySettings: helpBaseUrl + "/docs/user-guide/ui/security-settings", - ruleEngine: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/overview/", - ruleNodeCheckRelation: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/filter-nodes/#check-relation-filter-node", - ruleNodeJsFilter: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/filter-nodes/#script-filter-node", - ruleNodeJsSwitch: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/filter-nodes/#switch-node", - ruleNodeMessageTypeFilter: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/filter-nodes/#message-type-filter-node", - ruleNodeMessageTypeSwitch: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/filter-nodes/#message-type-switch-node", - ruleNodeOriginatorTypeFilter: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/filter-nodes/#originator-type-filter-node", - ruleNodeOriginatorTypeSwitch: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/filter-nodes/#originator-type-switch-node", - ruleNodeOriginatorAttributes: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/enrichment-nodes/#originator-attributes", - ruleNodeOriginatorFields: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/enrichment-nodes/#originator-fields", - ruleNodeCustomerAttributes: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/enrichment-nodes/#customer-attributes", - ruleNodeDeviceAttributes: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/enrichment-nodes/#device-attributes", - ruleNodeRelatedAttributes: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/enrichment-nodes/#related-attributes", - ruleNodeTenantAttributes: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/enrichment-nodes/#tenant-attributes", - ruleNodeChangeOriginator: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/transformation-nodes/#change-originator", - ruleNodeTransformMsg: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/transformation-nodes/#script-transformation-node", - ruleNodeMsgToEmail: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/transformation-nodes/#to-email-node", - ruleNodeClearAlarm: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#clear-alarm-node", - ruleNodeCreateAlarm: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#create-alarm-node", - ruleNodeMsgDelay: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#delay-node", - ruleNodeMsgGenerator: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#generator-node", - ruleNodeLog: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#log-node", - ruleNodeRpcCallReply: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#rpc-call-reply-node", - ruleNodeRpcCallRequest: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#rpc-call-request-node", - ruleNodeSaveAttributes: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#save-attributes-node", - ruleNodeSaveTimeseries: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#save-timeseries-node", - ruleNodeRuleChain: helpBaseUrl + "/docs/user-guide/ui/rule-chains/", - ruleNodeAwsSns: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/external-nodes/#aws-sns-node", - ruleNodeAwsSqs: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/external-nodes/#aws-sqs-node", - ruleNodeKafka: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/external-nodes/#kafka-node", - ruleNodeMqtt: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/external-nodes/#mqtt-node", - ruleNodeAzureIotHub: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/external-nodes/#azure-iot-hub-node", - ruleNodeRabbitMq: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/external-nodes/#rabbitmq-node", - ruleNodeRestApiCall: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/external-nodes/#rest-api-call-node", - ruleNodeSendEmail: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/external-nodes/#send-email-node", - rulechains: helpBaseUrl + "/docs/user-guide/ui/rule-chains/", - tenants: helpBaseUrl + "/docs/user-guide/ui/tenants", - customers: helpBaseUrl + "/docs/user-guide/ui/customers", - assets: helpBaseUrl + "/docs/user-guide/ui/assets", - devices: helpBaseUrl + "/docs/user-guide/ui/devices", - entityViews: helpBaseUrl + "/docs/user-guide/ui/entity-views", - entitiesImport: helpBaseUrl + "/docs/user-guide/bulk-provisioning", - dashboards: helpBaseUrl + "/docs/user-guide/ui/dashboards", - users: helpBaseUrl + "/docs/user-guide/ui/users", - widgetsBundles: helpBaseUrl + "/docs/user-guide/ui/widget-library#bundles", - widgetsConfig: helpBaseUrl + "/docs/user-guide/ui/dashboards#widget-configuration", - widgetsConfigTimeseries: helpBaseUrl + "/docs/user-guide/ui/dashboards#timeseries", - widgetsConfigLatest: helpBaseUrl + "/docs/user-guide/ui/dashboards#latest", - widgetsConfigRpc: helpBaseUrl + "/docs/user-guide/ui/dashboards#rpc", - widgetsConfigAlarm: helpBaseUrl + "/docs/user-guide/ui/dashboards#alarm", - widgetsConfigStatic: helpBaseUrl + "/docs/user-guide/ui/dashboards#static", - }, - getRuleNodeLink: function(ruleNode) { - if (ruleNode && ruleNode.component) { - if (ruleNode.component.configurationDescriptor && - ruleNode.component.configurationDescriptor.nodeDefinition && - ruleNode.component.configurationDescriptor.nodeDefinition.docUrl) { - return ruleNode.component.configurationDescriptor.nodeDefinition.docUrl; - } else if (ruleNode.component.clazz) { - if (ruleNodeClazzHelpLinkMap[ruleNode.component.clazz]) { - return ruleNodeClazzHelpLinkMap[ruleNode.component.clazz]; - } - } - } - return 'ruleEngine'; - } - } - ).name; diff --git a/ui/src/app/help/help.directive.js b/ui/src/app/help/help.directive.js deleted file mode 100644 index f0a47a5ba7..0000000000 --- a/ui/src/app/help/help.directive.js +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './help.scss'; - -import thingsboardHelpLinks from './help-links.constant'; - -import $ from 'jquery'; - -export default angular.module('thingsboard.directives.help', [thingsboardHelpLinks]) - .directive('tbHelp', Help) - .name; - -/* eslint-disable angular/angularelement */ - -/*@ngInject*/ -function Help($compile, $window, helpLinks) { - - var linker = function (scope, element, attrs) { - - scope.gotoHelpPage = function ($event) { - if ($event) { - $event.stopPropagation(); - } - var helpUrl = helpLinks.linksMap[scope.helpLinkId]; - if (!helpUrl && scope.helpLinkId && - (scope.helpLinkId.startsWith('http://') || scope.helpLinkId.startsWith('https://'))) { - helpUrl = scope.helpLinkId; - } - if (helpUrl) { - $window.open(helpUrl, '_blank'); - } - } - - var html = '' + - '{{\'help.goto-help-page\' | translate}}' + - '' + - '' + - 'help' + - ''; - - var helpButton = angular.element('' + - html + - ''); - - if (attrs.helpContainerId) { - var helpContainer = $('#' + attrs.helpContainerId, element)[0]; - helpContainer = angular.element(helpContainer); - helpContainer.append(helpButton); - $compile(helpContainer.contents())(scope); - } else { - $compile(helpButton)(scope); - element.append(helpButton); - } - } - - return { - restrict: "A", - link: linker, - scope: { - helpLinkId: "=tbHelp" - } - }; -} - -/* eslint-enable angular/angularelement */ diff --git a/ui/src/app/help/help.scss b/ui/src/app/help/help.scss deleted file mode 100644 index b7350ddf63..0000000000 --- a/ui/src/app/help/help.scss +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "../../scss/constants"; - -/* -.md-button.tb-help-button-style, -.tb-help-button-style { -} - -.md-button.tb-help-button-pos, -.tb-help-button-pos { -} -*/ diff --git a/ui/src/app/home/home-links.controller.js b/ui/src/app/home/home-links.controller.js deleted file mode 100644 index c290970478..0000000000 --- a/ui/src/app/home/home-links.controller.js +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './home-links.scss'; - -/*@ngInject*/ -export default function HomeLinksController($scope, $mdMedia, menu) { - - var vm = this; - - vm.sectionColspan = sectionColspan; - - $scope.$watch(function() { return $mdMedia('lg'); }, function() { - updateColumnCount(); - }); - - $scope.$watch(function() { return $mdMedia('gt-lg'); }, function() { - updateColumnCount(); - }); - - updateColumnCount(); - - vm.model = menu.getHomeSections(); - - function updateColumnCount() { - vm.cols = 2; - if ($mdMedia('lg')) { - vm.cols = 3; - } - if ($mdMedia('gt-lg')) { - vm.cols = 4; - } - } - - function sectionColspan(section) { - var colspan = vm.cols; - if (section && section.places && section.places.length <= colspan) { - colspan = section.places.length; - } - return colspan; - } - -} diff --git a/ui/src/app/home/home-links.routes.js b/ui/src/app/home/home-links.routes.js deleted file mode 100644 index db83e5b3c4..0000000000 --- a/ui/src/app/home/home-links.routes.js +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import homeLinksTemplate from './home-links.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function HomeLinksRoutes($stateProvider) { - - $stateProvider - .state('home.links', { - url: '/home', - module: 'private', - auth: ['SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER'], - views: { - "content@home": { - templateUrl: homeLinksTemplate, - controllerAs: 'vm', - controller: 'HomeLinksController' - } - }, - data: { - pageTitle: 'home.home' - }, - ncyBreadcrumb: { - label: '{"icon": "home", "label": "home.home"}', - icon: 'home' - } - }); -} diff --git a/ui/src/app/home/home-links.scss b/ui/src/app/home/home-links.scss deleted file mode 100644 index c4ed8c0c3a..0000000000 --- a/ui/src/app/home/home-links.scss +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "../../scss/constants"; - -.tb-home-links { - .md-headline { - font-size: 20px; - - @media (min-width: $layout-breakpoint-xmd) { - font-size: 24px; - } - } -} diff --git a/ui/src/app/home/home-links.tpl.html b/ui/src/app/home/home-links.tpl.html deleted file mode 100644 index 6888284ca7..0000000000 --- a/ui/src/app/home/home-links.tpl.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - {{ section.name }} - - - - - - - {{place.icon}} - {{place.name}} - - - - - - - \ No newline at end of file diff --git a/ui/src/app/home/index.js b/ui/src/app/home/index.js deleted file mode 100644 index fd1da0eedc..0000000000 --- a/ui/src/app/home/index.js +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import uiRouter from 'angular-ui-router'; - -import HomeLinksRoutes from './home-links.routes'; -import HomeLinksController from './home-links.controller'; - -export default angular.module('thingsboard.homeLinks', [ - uiRouter -]) - .config(HomeLinksRoutes) - .controller('HomeLinksController', HomeLinksController) - .name; diff --git a/ui/src/app/ie.support.js b/ui/src/app/ie.support.js deleted file mode 100644 index 9e9f04f936..0000000000 --- a/ui/src/app/ie.support.js +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -(function () { - if (!String.prototype.startsWith) { - String.prototype.startsWith = function(searchString, position) { - position = position || 0; - return this.indexOf(searchString, position) === position; - }; - } - if (!String.prototype.endsWith) { - String.prototype.endsWith = function (suffix) { - return this.indexOf(suffix, this.length - suffix.length) !== -1; - }; - } - if (!String.prototype.repeat) { - String.prototype.repeat = function(count) { - if (this == null) { - throw TypeError(); - } - var string = String(this); - // `ToInteger` - var n = count ? Number(count) : 0; - if (n != n) { // better `isNaN` - n = 0; - } - // Account for out-of-bounds indices - if (n < 0 || n == Infinity) { - throw RangeError(); - } - var result = ''; - while (n) { - if (n % 2 == 1) { - result += string; - } - if (n > 1) { - string += string; - } - n >>= 1; - } - return result; - }; - } - if (!String.prototype.includes) { - String.prototype.includes = function(search, start) { - if (angular.isNumber(start)) { - start = 0; - } - - if (start + search.length > this.length) { - return false; - } else { - return this.indexOf(search, start) !== -1; - } - }; - } - - (function (arr) { - arr.forEach(function (item) { - if (Object.prototype.hasOwnProperty.call(item, 'remove')) { - return; - } - Object.defineProperty(item, 'remove', { - configurable: true, - enumerable: true, - writable: true, - value: function remove() { - if (this.parentNode !== null) - this.parentNode.removeChild(this); - } - }); - }); - })([Element.prototype, CharacterData.prototype, DocumentType.prototype]); //eslint-disable-line - -})(); \ No newline at end of file diff --git a/ui/src/app/import-export/import-dialog-csv.controller.js b/ui/src/app/import-export/import-dialog-csv.controller.js deleted file mode 100644 index 4ae2a7a9f0..0000000000 --- a/ui/src/app/import-export/import-dialog-csv.controller.js +++ /dev/null @@ -1,251 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './import-dialog.scss'; - -/*@ngInject*/ -export default function ImportDialogCsvController($scope, $mdDialog, toast, importTitle, importFileLabel, entityType, importExport, types, $mdStepper, $timeout) { - - var vm = this; - - vm.cancel = cancel; - vm.fileAdded = fileAdded; - vm.clearFile = clearFile; - vm.nextStep = nextStep; - vm.previousStep = previousStep; - - vm.importParameters = { - delim: ',', - isUpdate: true, - isHeader: true - }; - - vm.importTitle = importTitle; - vm.importFileLabel = importFileLabel; - vm.entityType = entityType; - - vm.isVertical = true; - vm.isLinear = true; - vm.isAlternative = false; - vm.isMobileStepText = true; - vm.isImportData = false; - - vm.parseData = []; - - vm.delimiters = [{ - key: ',', - value: ',' - }, { - key: ';', - value: ';' - }, { - key: '|', - value: '|' - }, { - key: '\t', - value: 'Tab' - }]; - - vm.progressCreate = 0; - - var parseData = {}; - - function fileAdded($file) { - if ($file.getExtension() === 'csv') { - var reader = new FileReader(); - reader.onload = function (event) { - $scope.$apply(function () { - if (event.target.result) { - vm.theFormStep1.$setDirty(); - var importCSV = event.target.result; - if (importCSV && importCSV.length > 0) { - try { - vm.importData = importCSV; - vm.fileName = $file.name; - } catch (err) { - vm.fileName = null; - toast.showError(err.message); - } - } - } - }); - }; - reader.readAsText($file.file); - } - } - - function parseCSV(importData) { - var config = { - delim: vm.importParameters.delim, - header: vm.importParameters.isHeader - }; - return importExport.convertCSVToJson(importData, config); - } - - function createColumnsData(parseData) { - vm.columnsParam = []; - var columnParam = {}; - for (var i = 0; i < parseData.headers.length; i++) { - if (vm.importParameters.isHeader && parseData.headers[i].search(/^(name|type|label)$/im) === 0) { - columnParam = { - type: types.importEntityColumnType[parseData.headers[i].toLowerCase()].value, - key: parseData.headers[i].toLowerCase(), - sampleData: parseData.rows[0][i] - }; - } else { - columnParam = { - type: types.importEntityColumnType.serverAttribute.value, - key: vm.importParameters.isHeader ? parseData.headers[i] : "", - sampleData: parseData.rows[0][i] - }; - } - vm.columnsParam.push(columnParam); - } - } - - function addEntities (importData, parameterColumns) { - var entitiesData = []; - var sentDataLength = 0; - var config = { - ignoreErrors: true, - resendRequest: true - }; - - for (var i = 0; i < importData.rows.length; i++) { - var entityData = { - name: "", - type: "", - description: "", - gateway: null, - label: "", - accessToken: "", - attributes: { - server: [], - shared: [] - }, - timeseries: [] - }; - for (var j = 0; j < parameterColumns.length; j++) { - switch (parameterColumns[j].type) { - case types.importEntityColumnType.serverAttribute.value: - entityData.attributes.server.push({ - key: parameterColumns[j].key, - value: importData.rows[i][j] - }); - break; - case types.importEntityColumnType.timeseries.value: - entityData.timeseries.push({ - key: parameterColumns[j].key, - value: importData.rows[i][j] - }); - break; - case types.importEntityColumnType.sharedAttribute.value: - entityData.attributes.shared.push({ - key: parameterColumns[j].key, - value: importData.rows[i][j] - }); - break; - case types.importEntityColumnType.accessToken.value: - entityData.accessToken = importData.rows[i][j]; - break; - case types.importEntityColumnType.name.value: - entityData.name = importData.rows[i][j]; - break; - case types.importEntityColumnType.type.value: - entityData.type = importData.rows[i][j]; - break; - case types.importEntityColumnType.label.value: - entityData.label = importData.rows[i][j]; - break; - case types.importEntityColumnType.isGateway.value: - entityData.gateway = importData.rows[i][j]; - break; - case types.importEntityColumnType.description.value: - entityData.description = importData.rows[i][j]; - break; - } - } - entitiesData.push(entityData); - } - $scope.$on('createImportEntityCompleted', function () { - sentDataLength++; - vm.progressCreate = Math.round((sentDataLength / importData.rows.length) * 100); - }); - importExport.createMultiEntity(entitiesData, vm.entityType, vm.importParameters.isUpdate, config).then(function (response) { - vm.statistical = response; - vm.isImportData = false; - $mdStepper('import-stepper').next(); - }); - } - - function clearFile() { - vm.theFormStep1.$setDirty(); - vm.fileName = null; - vm.importData = null; - } - - function previousStep(step) { - let steppers = $mdStepper('import-stepper'); - switch (step) { - case 1: - steppers.back(); - $timeout(function () { - vm.theFormStep1.$setDirty(); - }); - break; - default: - steppers.back(); - break; - } - } - - function nextStep(step) { - let steppers = $mdStepper('import-stepper'); - switch (step) { - case 2: - steppers.next(); - break; - case 3: - parseData = parseCSV(vm.importData); - if (parseData === -1) { - steppers.back(); - $timeout(function () { - clearFile(); - }); - } else { - createColumnsData(parseData); - steppers.next(); - } - break; - case 4: - steppers.next(); - vm.isImportData = true; - addEntities(parseData, vm.columnsParam); - break; - case 6: - $mdDialog.hide(); - break; - } - - } - - function cancel() { - if($mdStepper('import-stepper').currentStep > 2){ - $mdDialog.hide(); - } else { - $mdDialog.cancel(); - } - } -} diff --git a/ui/src/app/import-export/import-dialog-csv.tpl.html b/ui/src/app/import-export/import-dialog-csv.tpl.html deleted file mode 100644 index 610e3d31c7..0000000000 --- a/ui/src/app/import-export/import-dialog-csv.tpl.html +++ /dev/null @@ -1,171 +0,0 @@ - - - -
    -

    {{ vm.importTitle }}

    - -
    - - - -
    -
    - - - - - - -
    -
    -
    -
    - -
    -
    - - - {{ 'action.remove' | translate }} - - - close - - -
    -
    - - -
    -
    -
    -
    -
    import.no-file
    -
    {{ vm.fileName }}
    -
    -
    -
    -
    -
    - - - - - {{ 'action.cancel' | translate }} - - - {{ 'action.continue' | translate }} - - -
    - - - -
    - - - - - {{delimiter.value}} - - - - - {{ 'import.csv-first-line-header' | translate }} - - - {{ 'import.csv-update-data' | translate }} - -
    -
    - - - Back - - - - {{ 'action.cancel' | translate }} - - - {{ 'action.continue' | translate }} - - -
    - - - -
    - -
    -
    - - - Back - - - - {{ 'action.cancel' | translate }} - - - {{ 'action.continue' | translate }} - - -
    - - - - - - - - -
    -

    import.message.create-entities

    -

    import.message.update-entities

    -

    import.message.error-entities

    -
    -
    - - - - {{ 'action.ok' | translate }} - - -
    - -
    -
    -
    diff --git a/ui/src/app/import-export/import-dialog.controller.js b/ui/src/app/import-export/import-dialog.controller.js deleted file mode 100644 index d71f137c78..0000000000 --- a/ui/src/app/import-export/import-dialog.controller.js +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './import-dialog.scss'; - -/*@ngInject*/ -export default function ImportDialogController($scope, $mdDialog, toast, importTitle, importFileLabel) { - - var vm = this; - - vm.cancel = cancel; - vm.importFromJson = importFromJson; - vm.fileAdded = fileAdded; - vm.clearFile = clearFile; - - vm.importTitle = importTitle; - vm.importFileLabel = importFileLabel; - - - function cancel() { - $mdDialog.cancel(); - } - - function fileAdded($file) { - if ($file.getExtension() === 'json') { - var reader = new FileReader(); - reader.onload = function(event) { - $scope.$apply(function() { - if (event.target.result) { - $scope.theForm.$setDirty(); - var importJson = event.target.result; - if (importJson && importJson.length > 0) { - try { - vm.importData = angular.fromJson(importJson); - vm.fileName = $file.name; - } catch (err) { - vm.fileName = null; - toast.showError(err.message); - } - } - } - }); - }; - reader.readAsText($file.file); - } - } - - function clearFile() { - $scope.theForm.$setDirty(); - vm.fileName = null; - vm.importData = null; - } - - function importFromJson() { - $scope.theForm.$setPristine(); - $mdDialog.hide(vm.importData); - } -} diff --git a/ui/src/app/import-export/import-dialog.scss b/ui/src/app/import-export/import-dialog.scss deleted file mode 100644 index b1f8151212..0000000000 --- a/ui/src/app/import-export/import-dialog.scss +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "../../scss/constants.scss"; - -$previewSize: 100px !default; - -.tb-file-select-container { - position: relative; - width: 100%; - height: $previewSize; -} - -.tb-file-preview { - width: auto; - max-width: $previewSize; - height: auto; - max-height: $previewSize; -} - -.tb-file-clear-container { - position: relative; - float: right; - width: 48px; - height: $previewSize; -} - -.tb-file-clear-btn { - position: absolute !important; - top: 50%; - transform: translate(0%, -50%) !important; -} - -.tb-table-select{ - md-input-container{ - margin: 0; - - .md-errors-spacer{ - min-height: 0; - } - } -} - -.md-stepper-indicator-wrapper{ - background-color: transparent; -} - -.tb-import-progress{ - margin: 7px 0; -} diff --git a/ui/src/app/import-export/import-dialog.tpl.html b/ui/src/app/import-export/import-dialog.tpl.html deleted file mode 100644 index 9100a8094b..0000000000 --- a/ui/src/app/import-export/import-dialog.tpl.html +++ /dev/null @@ -1,72 +0,0 @@ - - -
    - -
    -

    {{ vm.importTitle }}

    - - - - -
    -
    - - - -
    -
    -
    -
    - -
    -
    - - - {{ 'action.remove' | translate }} - - - close - - -
    -
    - - -
    -
    -
    -
    -
    import.no-file
    -
    {{ vm.fileName }}
    -
    -
    -
    -
    -
    - - - - {{ 'action.import' | translate }} - - {{ 'action.cancel' | translate }} - -
    -
    diff --git a/ui/src/app/import-export/import-export.service.js b/ui/src/app/import-export/import-export.service.js deleted file mode 100644 index b5dbfa6ef3..0000000000 --- a/ui/src/app/import-export/import-export.service.js +++ /dev/null @@ -1,1026 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default*/ - -import importDialogTemplate from './import-dialog.tpl.html'; -import importDialogCSVTemplate from './import-dialog-csv.tpl.html'; -import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html'; -import * as JSZip from 'jszip'; - -/* eslint-enable import/no-unresolved, import/default */ - - -/* eslint-disable no-undef, angular/window-service, angular/document-service */ - -/*@ngInject*/ -export default function ImportExport($log, $translate, $q, $mdDialog, $document, $http, itembuffer, utils, types, $rootScope, - dashboardUtils, entityService, dashboardService, ruleChainService, widgetService, toast, attributeService) { - - const ZIP_TYPE = { - mimeType: 'application/zip', - extension: 'zip' - }; - - var service = { - exportDashboard: exportDashboard, - importDashboard: importDashboard, - exportWidget: exportWidget, - importWidget: importWidget, - exportRuleChain: exportRuleChain, - importRuleChain: importRuleChain, - exportWidgetType: exportWidgetType, - importWidgetType: importWidgetType, - exportWidgetsBundle: exportWidgetsBundle, - importWidgetsBundle: importWidgetsBundle, - exportJSZip: exportJSZip, - exportExtension: exportExtension, - importExtension: importExtension, - importEntities: importEntities, - convertCSVToJson: convertCSVToJson, - exportToPc: exportToPc, - createMultiEntity: createMultiEntity - }; - - return service; - - // Widgets bundle functions - - function exportWidgetsBundle(widgetsBundleId) { - widgetService.getWidgetsBundle(widgetsBundleId).then( - function success(widgetsBundle) { - var bundleAlias = widgetsBundle.alias; - var isSystem = widgetsBundle.tenantId.id === types.id.nullUid; - widgetService.getBundleWidgetTypes(bundleAlias, isSystem).then( - function success(widgetTypes) { - prepareExport(widgetsBundle); - var widgetsBundleItem = { - widgetsBundle: prepareExport(widgetsBundle), - widgetTypes: [] - }; - for (var t in widgetTypes) { - var widgetType = widgetTypes[t]; - if (angular.isDefined(widgetType.bundleAlias)) { - delete widgetType.bundleAlias; - } - widgetsBundleItem.widgetTypes.push(prepareExport(widgetType)); - } - var name = widgetsBundle.title; - name = name.toLowerCase().replace(/\W/g, "_"); - exportToPc(widgetsBundleItem, name + '.json'); - }, - function fail(rejection) { - var message = rejection; - if (!message) { - message = $translate.instant('error.unknown-error'); - } - toast.showError($translate.instant('widgets-bundle.export-failed-error', {error: message})); - } - ); - }, - function fail(rejection) { - var message = rejection; - if (!message) { - message = $translate.instant('error.unknown-error'); - } - toast.showError($translate.instant('widgets-bundle.export-failed-error', {error: message})); - } - ); - } - - function importNextWidgetType(widgetTypes, bundleAlias, index, deferred) { - if (!widgetTypes || widgetTypes.length <= index) { - deferred.resolve(); - } else { - var widgetType = widgetTypes[index]; - widgetType.bundleAlias = bundleAlias; - widgetService.saveImportedWidgetType(widgetType).then( - function success() { - index++; - importNextWidgetType(widgetTypes, bundleAlias, index, deferred); - }, - function fail() { - deferred.reject(); - } - ); - - } - } - - function importWidgetsBundle($event) { - var deferred = $q.defer(); - openImportDialog($event, 'widgets-bundle.import', 'widgets-bundle.widgets-bundle-file').then( - function success(widgetsBundleItem) { - if (!validateImportedWidgetsBundle(widgetsBundleItem)) { - toast.showError($translate.instant('widgets-bundle.invalid-widgets-bundle-file-error')); - deferred.reject(); - } else { - var widgetsBundle = widgetsBundleItem.widgetsBundle; - widgetService.saveWidgetsBundle(widgetsBundle).then( - function success(savedWidgetsBundle) { - var bundleAlias = savedWidgetsBundle.alias; - var widgetTypes = widgetsBundleItem.widgetTypes; - importNextWidgetType(widgetTypes, bundleAlias, 0, deferred); - }, - function fail() { - deferred.reject(); - } - ); - } - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - - function validateImportedWidgetsBundle(widgetsBundleItem) { - if (angular.isUndefined(widgetsBundleItem.widgetsBundle)) { - return false; - } - if (angular.isUndefined(widgetsBundleItem.widgetTypes)) { - return false; - } - var widgetsBundle = widgetsBundleItem.widgetsBundle; - if (angular.isUndefined(widgetsBundle.title)) { - return false; - } - var widgetTypes = widgetsBundleItem.widgetTypes; - for (var t in widgetTypes) { - var widgetType = widgetTypes[t]; - if (!validateImportedWidgetType(widgetType)) { - return false; - } - } - - return true; - } - - // Widget type functions - - function exportWidgetType(widgetTypeId) { - widgetService.getWidgetTypeById(widgetTypeId).then( - function success(widgetType) { - if (angular.isDefined(widgetType.bundleAlias)) { - delete widgetType.bundleAlias; - } - var name = widgetType.name; - name = name.toLowerCase().replace(/\W/g, "_"); - exportToPc(prepareExport(widgetType), name + '.json'); - }, - function fail(rejection) { - var message = rejection; - if (!message) { - message = $translate.instant('error.unknown-error'); - } - toast.showError($translate.instant('widget-type.export-failed-error', {error: message})); - } - ); - } - - function importWidgetType($event, bundleAlias) { - var deferred = $q.defer(); - openImportDialog($event, 'widget-type.import', 'widget-type.widget-type-file').then( - function success(widgetType) { - if (!validateImportedWidgetType(widgetType)) { - toast.showError($translate.instant('widget-type.invalid-widget-type-file-error')); - deferred.reject(); - } else { - widgetType.bundleAlias = bundleAlias; - widgetService.saveImportedWidgetType(widgetType).then( - function success() { - deferred.resolve(); - }, - function fail() { - deferred.reject(); - } - ); - } - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - - function validateImportedWidgetType(widgetType) { - if (angular.isUndefined(widgetType.name) - || angular.isUndefined(widgetType.descriptor)) { - return false; - } - return true; - } - - // Rule chain functions - - function exportRuleChain(ruleChainId) { - ruleChainService.getRuleChain(ruleChainId).then( - (ruleChain) => { - ruleChainService.getRuleChainMetaData(ruleChainId).then( - (ruleChainMetaData) => { - var ruleChainExport = { - ruleChain: prepareRuleChain(ruleChain), - metadata: prepareRuleChainMetaData(ruleChainMetaData) - }; - var name = ruleChain.name; - name = name.toLowerCase().replace(/\W/g, "_"); - exportToPc(ruleChainExport, name + '.json'); - }, - (rejection) => { - processExportRuleChainRejection(rejection); - } - ); - }, - (rejection) => { - processExportRuleChainRejection(rejection); - } - ); - } - - function prepareRuleChain(ruleChain) { - ruleChain = prepareExport(ruleChain); - if (ruleChain.firstRuleNodeId) { - ruleChain.firstRuleNodeId = null; - } - ruleChain.root = false; - return ruleChain; - } - - function prepareRuleChainMetaData(ruleChainMetaData) { - delete ruleChainMetaData.ruleChainId; - for (var i = 0; i < ruleChainMetaData.nodes.length; i++) { - var node = ruleChainMetaData.nodes[i]; - delete node.ruleChainId; - ruleChainMetaData.nodes[i] = prepareExport(node); - } - return ruleChainMetaData; - } - - function processExportRuleChainRejection(rejection) { - var message = rejection; - if (!message) { - message = $translate.instant('error.unknown-error'); - } - toast.showError($translate.instant('rulechain.export-failed-error', {error: message})); - } - - function importRuleChain($event) { - var deferred = $q.defer(); - openImportDialog($event, 'rulechain.import', 'rulechain.rulechain-file').then( - function success(ruleChainImport) { - if (!validateImportedRuleChain(ruleChainImport)) { - toast.showError($translate.instant('rulechain.invalid-rulechain-file-error')); - deferred.reject(); - } else { - deferred.resolve(ruleChainImport); - } - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - - function validateImportedRuleChain(ruleChainImport) { - if (angular.isUndefined(ruleChainImport.ruleChain)) { - return false; - } - if (angular.isUndefined(ruleChainImport.metadata)) { - return false; - } - if (angular.isUndefined(ruleChainImport.ruleChain.name)) { - return false; - } - return true; - } - - // Widget functions - - function exportWidget(dashboard, sourceState, sourceLayout, widget) { - var widgetItem = itembuffer.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget); - var name = widgetItem.widget.config.title; - name = name.toLowerCase().replace(/\W/g, "_"); - exportToPc(prepareExport(widgetItem), name + '.json'); - } - - function prepareAliasesInfo(aliasesInfo) { - var datasourceAliases = aliasesInfo.datasourceAliases; - var targetDeviceAliases = aliasesInfo.targetDeviceAliases; - var datasourceIndex; - if (datasourceAliases || targetDeviceAliases) { - if (datasourceAliases) { - for (datasourceIndex in datasourceAliases) { - datasourceAliases[datasourceIndex] = prepareEntityAlias(datasourceAliases[datasourceIndex]); - } - } - if (targetDeviceAliases) { - for (datasourceIndex in targetDeviceAliases) { - targetDeviceAliases[datasourceIndex] = prepareEntityAlias(targetDeviceAliases[datasourceIndex]); - } - } - } - return aliasesInfo; - } - - function prepareEntityAlias(aliasInfo) { - var alias; - var filter; - if (aliasInfo.deviceId) { - alias = aliasInfo.aliasName; - filter = { - type: types.aliasFilterType.entityList.value, - entityType: types.entityType.device, - entityList: [aliasInfo.deviceId], - resolveMultiple: false - }; - } else if (aliasInfo.deviceFilter) { - alias = aliasInfo.aliasName; - filter = { - type: aliasInfo.deviceFilter.useFilter ? types.aliasFilterType.entityName.value : types.aliasFilterType.entityList.value, - entityType: types.entityType.device, - resolveMultiple: false - } - if (filter.type == types.aliasFilterType.entityList.value) { - filter.entityList = aliasInfo.deviceFilter.deviceList - } else { - filter.entityNameFilter = aliasInfo.deviceFilter.deviceNameFilter; - } - } else if (aliasInfo.entityFilter) { - alias = aliasInfo.aliasName; - filter = { - type: aliasInfo.entityFilter.useFilter ? types.aliasFilterType.entityName.value : types.aliasFilterType.entityList.value, - entityType: aliasInfo.entityType, - resolveMultiple: false - } - if (filter.type == types.aliasFilterType.entityList.value) { - filter.entityList = aliasInfo.entityFilter.entityList; - } else { - filter.entityNameFilter = aliasInfo.entityFilter.entityNameFilter; - } - } else { - alias = aliasInfo.alias; - filter = aliasInfo.filter; - } - return { - alias: alias, - filter: filter - }; - } - - function importWidget($event, dashboard, targetState, targetLayoutFunction, onAliasesUpdateFunction) { - var deferred = $q.defer(); - openImportDialog($event, 'dashboard.import-widget', 'dashboard.widget-file').then( - function success(widgetItem) { - if (!validateImportedWidget(widgetItem)) { - toast.showError($translate.instant('dashboard.invalid-widget-file-error')); - deferred.reject(); - } else { - var widget = widgetItem.widget; - widget = dashboardUtils.validateAndUpdateWidget(widget); - var aliasesInfo = prepareAliasesInfo(widgetItem.aliasesInfo); - var originalColumns = widgetItem.originalColumns; - var originalSize = widgetItem.originalSize; - - var datasourceAliases = aliasesInfo.datasourceAliases; - var targetDeviceAliases = aliasesInfo.targetDeviceAliases; - if (datasourceAliases || targetDeviceAliases) { - var entityAliases = {}; - var datasourceAliasesMap = {}; - var targetDeviceAliasesMap = {}; - var aliasId; - var datasourceIndex; - if (datasourceAliases) { - for (datasourceIndex in datasourceAliases) { - aliasId = utils.guid(); - datasourceAliasesMap[aliasId] = datasourceIndex; - entityAliases[aliasId] = datasourceAliases[datasourceIndex]; - entityAliases[aliasId].id = aliasId; - } - } - if (targetDeviceAliases) { - for (datasourceIndex in targetDeviceAliases) { - aliasId = utils.guid(); - targetDeviceAliasesMap[aliasId] = datasourceIndex; - entityAliases[aliasId] = targetDeviceAliases[datasourceIndex]; - entityAliases[aliasId].id = aliasId; - } - } - - var aliasIds = Object.keys(entityAliases); - if (aliasIds.length > 0) { - processEntityAliases(entityAliases, aliasIds).then( - function (missingEntityAliases) { - if (Object.keys(missingEntityAliases).length > 0) { - editMissingAliases($event, [widget], - true, 'dashboard.widget-import-missing-aliases-title', missingEntityAliases).then( - function success(updatedEntityAliases) { - for (var aliasId in updatedEntityAliases) { - var entityAlias = updatedEntityAliases[aliasId]; - var datasourceIndex; - if (datasourceAliasesMap[aliasId]) { - datasourceIndex = datasourceAliasesMap[aliasId]; - datasourceAliases[datasourceIndex] = entityAlias; - } else if (targetDeviceAliasesMap[aliasId]) { - datasourceIndex = targetDeviceAliasesMap[aliasId]; - targetDeviceAliases[datasourceIndex] = entityAlias; - } - } - addImportedWidget(dashboard, targetState, targetLayoutFunction, $event, widget, - aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, deferred); - }, - function fail() { - deferred.reject(); - } - ); - } else { - addImportedWidget(dashboard, targetState, targetLayoutFunction, $event, widget, - aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, deferred); - } - } - ); - } else { - addImportedWidget(dashboard, targetState, targetLayoutFunction, $event, widget, - aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, deferred); - } - } else { - addImportedWidget(dashboard, targetState, targetLayoutFunction, $event, widget, - aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, deferred); - } - } - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - - function validateImportedWidget(widgetItem) { - if (angular.isUndefined(widgetItem.widget) - || angular.isUndefined(widgetItem.aliasesInfo) - || angular.isUndefined(widgetItem.originalColumns)) { - return false; - } - var widget = widgetItem.widget; - if (angular.isUndefined(widget.isSystemType) || - angular.isUndefined(widget.bundleAlias) || - angular.isUndefined(widget.typeAlias) || - angular.isUndefined(widget.type)) { - return false; - } - return true; - } - - function addImportedWidget(dashboard, targetState, targetLayoutFunction, event, widget, - aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, deferred) { - targetLayoutFunction(event).then( - function success(targetLayout) { - itembuffer.addWidgetToDashboard(dashboard, targetState, targetLayout, widget, - aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, -1, -1).then( - function () { - deferred.resolve( - { - widget: widget, - layoutId: targetLayout - } - ); - } - ); - }, - function fail() { - deferred.reject(); - } - ); - } - - // Dashboard functions - - function exportDashboard(dashboardId) { - dashboardService.getDashboard(dashboardId).then( - function success(dashboard) { - var name = dashboard.title; - name = name.toLowerCase().replace(/\W/g, "_"); - exportToPc(prepareDashboardExport(dashboard), name + '.json'); - }, - function fail(rejection) { - var message = rejection; - if (!message) { - message = $translate.instant('error.unknown-error'); - } - toast.showError($translate.instant('dashboard.export-failed-error', {error: message})); - } - ); - } - - function prepareDashboardExport(dashboard) { - dashboard = prepareExport(dashboard); - delete dashboard.assignedCustomers; - delete dashboard.publicCustomerId; - delete dashboard.assignedCustomersText; - delete dashboard.assignedCustomersIds; - return dashboard; - } - - function importDashboard($event) { - var deferred = $q.defer(); - openImportDialog($event, 'dashboard.import', 'dashboard.dashboard-file').then( - function success(dashboard) { - if (!validateImportedDashboard(dashboard)) { - toast.showError($translate.instant('dashboard.invalid-dashboard-file-error')); - deferred.reject(); - } else { - dashboard = dashboardUtils.validateAndUpdateDashboard(dashboard); - var entityAliases = dashboard.configuration.entityAliases; - if (entityAliases) { - var aliasIds = Object.keys(entityAliases); - if (aliasIds.length > 0) { - processEntityAliases(entityAliases, aliasIds).then( - function (missingEntityAliases) { - if (Object.keys(missingEntityAliases).length > 0) { - editMissingAliases($event, dashboard.configuration.widgets, - false, 'dashboard.dashboard-import-missing-aliases-title', missingEntityAliases).then( - function success(updatedEntityAliases) { - for (var aliasId in updatedEntityAliases) { - entityAliases[aliasId] = updatedEntityAliases[aliasId]; - } - saveImportedDashboard(dashboard, deferred); - }, - function fail() { - deferred.reject(); - } - ); - } else { - saveImportedDashboard(dashboard, deferred); - } - } - ) - } else { - saveImportedDashboard(dashboard, deferred); - } - } else { - saveImportedDashboard(dashboard, deferred); - } - } - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - - function importEntities($event, entityType) { - var deferred = $q.defer(); - - switch (entityType) { - case types.entityType.device: - openImportDialogCSV($event, entityType, 'device.import', 'device.device-file').then( - function success() { - deferred.resolve(); - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - case types.entityType.asset: - openImportDialogCSV($event, entityType, 'asset.import', 'asset.asset-file').then( - function success() { - deferred.resolve(); - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - - } - - function saveImportedDashboard(dashboard, deferred) { - dashboardService.saveDashboard(dashboard).then( - function success() { - deferred.resolve(); - }, - function fail() { - deferred.reject(); - } - ) - } - - function validateImportedDashboard(dashboard) { - if (angular.isUndefined(dashboard.title) || angular.isUndefined(dashboard.configuration)) { - return false; - } - return true; - } - - - function exportExtension(extensionId) { - - getExtension(extensionId) - .then( - function success(extension) { - var name = extension.title; - name = name.toLowerCase().replace(/\W/g, "_"); - exportToPc(prepareExport(extension), name + '.json'); - }, - function fail(rejection) { - var message = rejection; - if (!message) { - message = $translate.instant('error.unknown-error'); - } - toast.showError($translate.instant('extension.export-failed-error', {error: message})); - } - ); - - function getExtension(extensionId) { - var deferred = $q.defer(); - var url = '/api/plugins/telemetry/DEVICE/' + extensionId; - $http.get(url, null) - .then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; - } - - } - - function importExtension($event, options) { - var deferred = $q.defer(); - openImportDialog($event, 'extension.import-extensions', 'extension.file') - .then( - function success(extension) { - if (!validateImportedExtension(extension)) { - toast.showError($translate.instant('extension.invalid-file-error')); - deferred.reject(); - } else { - attributeService - .saveEntityAttributes( - options.entityType, - options.entityId, - types.attributesScope.shared.value, - [{ - key: "configuration", - value: angular.toJson(extension) - }] - ) - .then(function success() { - options.successFunc(); - }); - } - }, - function fail() { - deferred.reject(); - } - ); - return deferred.promise; - } - - function validateImportedExtension(configuration) { - if (configuration.length) { - for (let i = 0; i < configuration.length; i++) { - if (angular.isUndefined(configuration[i].configuration) || angular.isUndefined(configuration[i].id) || angular.isUndefined(configuration[i].type)) { - return false; - } - } - } else { - return false; - } - return true; - } - - function processEntityAliases(entityAliases, aliasIds) { - var deferred = $q.defer(); - var missingEntityAliases = {}; - var index = -1; - checkNextEntityAliasOrComplete(index, aliasIds, entityAliases, missingEntityAliases, deferred); - return deferred.promise; - } - - function checkNextEntityAliasOrComplete(index, aliasIds, entityAliases, missingEntityAliases, deferred) { - index++; - if (index == aliasIds.length) { - deferred.resolve(missingEntityAliases); - } else { - checkEntityAlias(index, aliasIds, entityAliases, missingEntityAliases, deferred); - } - } - - function checkEntityAlias(index, aliasIds, entityAliases, missingEntityAliases, deferred) { - var aliasId = aliasIds[index]; - var entityAlias = entityAliases[aliasId]; - entityService.checkEntityAlias(entityAlias).then( - function (result) { - if (result) { - checkNextEntityAliasOrComplete(index, aliasIds, entityAliases, missingEntityAliases, deferred); - } else { - var missingEntityAlias = angular.copy(entityAlias); - missingEntityAlias.filter = null; - missingEntityAliases[aliasId] = missingEntityAlias; - checkNextEntityAliasOrComplete(index, aliasIds, entityAliases, missingEntityAliases, deferred); - } - } - ); - } - - function editMissingAliases($event, widgets, isSingleWidget, customTitle, missingEntityAliases) { - var deferred = $q.defer(); - $mdDialog.show({ - controller: 'EntityAliasesController', - controllerAs: 'vm', - templateUrl: entityAliasesTemplate, - locals: { - config: { - entityAliases: missingEntityAliases, - widgets: widgets, - isSingleWidget: isSingleWidget, - customTitle: customTitle, - disableAdd: true - } - }, - parent: angular.element($document[0].body), - multiple: true, - fullscreen: true, - targetEvent: $event - }).then(function (updatedEntityAliases) { - deferred.resolve(updatedEntityAliases); - }, function () { - deferred.reject(); - }); - return deferred.promise; - } - - function splitCSV(str, sep) { - for (var foo = str.split(sep = sep || ","), x = foo.length - 1, tl; x >= 0; x--) { - if (foo[x].replace(/"\s+$/, '"').charAt(foo[x].length - 1) == '"') { - if ((tl = foo[x].replace(/^\s+"/, '"')).length > 1 && tl.charAt(0) == '"') { - foo[x] = foo[x].replace(/^\s*"|"\s*$/g, '').replace(/""/g, '"'); - } else if (x) { - foo.splice(x - 1, 2, [foo[x - 1], foo[x]].join(sep)); - } else foo = foo.shift().split(sep).concat(foo); - } else foo[x].replace(/""/g, '"'); - } - return foo; - } - - function isNumeric(str) { - str = str.replace(',', '.'); - return !isNaN(parseFloat(str)) && isFinite(str); - } - - function convertStringToJSType(str) { - if (isNumeric(str.replace(',', '.'))) { - return parseFloat(str.replace(',', '.')); - } - if (str.search(/^(true|false)$/im) === 0) { - return str === 'true'; - } - if (str === "") { - return null; - } - return str; - } - - function convertCSVToJson(csvdata, config) { - config = config || {}; - const delim = config.delim || ","; - const header = config.header || false; - let result = {}; - - let csvlines = csvdata.split(/[\r\n]+/); - let csvheaders = splitCSV(csvlines[0], delim); - if (csvheaders.length < 2) { - toast.showError($translate.instant('import.import-csv-number-columns-error')); - return -1; - } - let csvrows = header ? csvlines.slice(1, csvlines.length) : csvlines; - - result.headers = csvheaders; - result.rows = []; - - for (let r in csvrows) { - if (Object.prototype.hasOwnProperty.call(csvrows, r)) { - let row = csvrows[r]; - - if (row.length === 0) - break; - - let rowitems = splitCSV(row, delim); - if (rowitems.length !== result.headers.length) { - toast.showError($translate.instant('import.import-csv-invalid-format-error', {line: (header ? result.rows.length + 2: result.rows.length + 1)})); - return -1; - } - for (let i = 0; i < rowitems.length; i++) { - rowitems[i] = convertStringToJSType(rowitems[i]); - } - result.rows.push(rowitems); - } - } - return result; - } - - function sumObject(obj1, obj2){ - Object.keys(obj2).map(function(key) { - if (angular.isObject(obj2[key])) { - obj1[key] = obj1[key] || {}; - angular.merge(obj1[key], sumObject(obj1[key], obj2[key])); - } else { - obj1[key] = (obj1[key] || 0) + obj2[key]; - } - }); - return obj1; - } - - function qAllWithProgress(promises) { - promises.forEach(function(p) { - p.then(function() { - $rootScope.$broadcast('createImportEntityCompleted', {}); - }); - }); - return $q.all(promises); - } - - function createMultiEntity(arrayData, entityType, updateData, config) { - let partSize = 100; - partSize = arrayData.length > partSize ? partSize : arrayData.length; - let allPromise = []; - let statisticalInfo = {}; - let deferred = $q.defer(); - - for(let i = 0; i < partSize; i++){ - const promise = entityService.saveEntityParameters(entityType, arrayData[i], updateData, config); - allPromise.push(promise); - } - - qAllWithProgress(allPromise).then(function success(response) { - for (let i = 0; i < response.length; i++){ - statisticalInfo = sumObject(statisticalInfo, response[i]); - } - arrayData.splice(0, partSize); - if(arrayData.length > 0){ - deferred.resolve(createMultiEntity(arrayData, entityType, updateData, config).then(function (response) { - return sumObject(statisticalInfo, response); - })); - } else { - deferred.resolve(statisticalInfo); - } - }); - return deferred.promise; - } - - // Common functions - - function prepareExport(data) { - var exportedData = angular.copy(data); - if (angular.isDefined(exportedData.id)) { - delete exportedData.id; - } - if (angular.isDefined(exportedData.createdTime)) { - delete exportedData.createdTime; - } - if (angular.isDefined(exportedData.tenantId)) { - delete exportedData.tenantId; - } - if (angular.isDefined(exportedData.customerId)) { - delete exportedData.customerId; - } - return exportedData; - } - - function exportToPc(data, filename) { - if (!data) { - $log.error('No data'); - return; - } - - if (!filename) { - filename = 'download.json'; - } - - if (angular.isObject(data)) { - data = angular.toJson(data, 2); - } - - var blob = new Blob([data], {type: 'text/json'}); - - // FOR IE: - - if (window.navigator && window.navigator.msSaveOrOpenBlob) { - window.navigator.msSaveOrOpenBlob(blob, filename); - } else { - var e = document.createEvent('MouseEvents'), - a = document.createElement('a'); - - a.download = filename; - a.href = window.URL.createObjectURL(blob); - a.dataset.downloadurl = ['text/json', a.download, a.href].join(':'); - e.initEvent('click', true, false, window, - 0, 0, 0, 0, 0, false, false, false, false, 0, null); - a.dispatchEvent(e); - } - } - - function openImportDialog($event, importTitle, importFileLabel) { - var deferred = $q.defer(); - $mdDialog.show({ - controller: 'ImportDialogController', - controllerAs: 'vm', - templateUrl: importDialogTemplate, - locals: { - importTitle: importTitle, - importFileLabel: importFileLabel - }, - parent: angular.element($document[0].body), - multiple: true, - fullscreen: true, - targetEvent: $event - }).then(function (importData) { - deferred.resolve(importData); - }, function () { - deferred.reject(); - }); - return deferred.promise; - } - - function openImportDialogCSV($event, entityType, importTitle, importFileLabel) { - var deferred = $q.defer(); - $mdDialog.show({ - controller: 'ImportDialogCSVController', - controllerAs: 'vm', - templateUrl: importDialogCSVTemplate, - locals: { - importTitle: importTitle, - importFileLabel: importFileLabel, - entityType: entityType - }, - parent: angular.element($document[0].body), - onComplete: fixedDialogSize, - multiple: true, - fullscreen: true, - targetEvent: $event - }).then(function () { - deferred.resolve(); - }, function () { - deferred.reject(); - }); - return deferred.promise; - } - - function fixedDialogSize(scope, element) { - let dialogElement = element[0].getElementsByTagName('md-dialog'); - dialogElement[0].style.width = dialogElement[0].offsetWidth + 2 + "px"; - } - - function exportJSZip(data, filename) { - let jsZip = new JSZip(); - for (let keyName in data) { - let valueData = data[keyName]; - jsZip.file(keyName, valueData); - } - jsZip.generateAsync({type: "blob"}).then(function (content) { - downloadFile(content, filename, ZIP_TYPE); - }); - } - - - function downloadFile(data, filename, fileType) { - if (!filename) { - filename = 'download'; - } - filename += '.' + fileType.extension; - var blob = new Blob([data], {type: fileType.mimeType}); - // FOR IE: - if (window.navigator && window.navigator.msSaveOrOpenBlob) { - window.navigator.msSaveOrOpenBlob(blob, filename); - } else { - var e = document.createEvent('MouseEvents'), - a = document.createElement('a'); - a.download = filename; - a.href = window.URL.createObjectURL(blob); - a.dataset.downloadurl = [fileType.mimeType, a.download, a.href].join(':'); - e.initEvent('click', true, false, window, - 0, 0, 0, 0, 0, false, false, false, false, 0, null); - a.dispatchEvent(e); - } - } -} - -/* eslint-enable no-undef, angular/window-service, angular/document-service */ diff --git a/ui/src/app/import-export/index.js b/ui/src/app/import-export/index.js deleted file mode 100644 index 86617c9515..0000000000 --- a/ui/src/app/import-export/index.js +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import ImportExport from './import-export.service'; -import ImportDialogController from './import-dialog.controller'; -import ImportDialogCSVController from './import-dialog-csv.controller'; -import TableColumnsAssignment from './table-columns-assignment.directive'; - - -export default angular.module('thingsboard.importexport', []) - .factory('importExport', ImportExport) - .controller('ImportDialogController', ImportDialogController) - .controller('ImportDialogCSVController', ImportDialogCSVController) - .directive('tbTableColumnsAssignment', TableColumnsAssignment) - .name; diff --git a/ui/src/app/import-export/table-columns-assignment.directive.js b/ui/src/app/import-export/table-columns-assignment.directive.js deleted file mode 100644 index a295621799..0000000000 --- a/ui/src/app/import-export/table-columns-assignment.directive.js +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import tableColumnsAssignment from './table-columns-assignment.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function TableColumnsAssignment() { - return { - restrict: "E", - scope: true, - bindToController: { - theForm: '=?', - columns: '=', - entityType: '=', - }, - templateUrl: tableColumnsAssignment, - controller: TableColumnsAssignmentController, - controllerAs: 'vm' - }; -} - -/*@ngInject*/ -function TableColumnsAssignmentController($scope, types, $timeout) { - var vm = this; - - vm.columnTypes = {}; - - vm.columnTypes.name = types.importEntityColumnType.name; - vm.columnTypes.type = types.importEntityColumnType.type; - vm.columnTypes.label = types.importEntityColumnType.label; - vm.columnTypes.description = types.importEntityColumnType.description; - - switch (vm.entityType) { - case types.entityType.device: - vm.columnTypes.sharedAttribute = types.importEntityColumnType.sharedAttribute; - vm.columnTypes.serverAttribute = types.importEntityColumnType.serverAttribute; - vm.columnTypes.timeseries = types.importEntityColumnType.timeseries; - vm.columnTypes.accessToken = types.importEntityColumnType.accessToken; - vm.columnTypes.gateway = types.importEntityColumnType.isGateway; - break; - case types.entityType.asset: - vm.columnTypes.serverAttribute = types.importEntityColumnType.serverAttribute; - vm.columnTypes.timeseries = types.importEntityColumnType.timeseries; - break; - } - - $scope.isColumnTypeDiffers = function(columnType) { - return columnType !== types.importEntityColumnType.name.value && - columnType !== types.importEntityColumnType.type.value && - columnType !== types.importEntityColumnType.label.value && - columnType !== types.importEntityColumnType.accessToken.value&& - columnType !== types.importEntityColumnType.isGateway.value&& - columnType !== types.importEntityColumnType.description.value; - }; - - $scope.$watch('vm.columns', function(newVal){ - if (newVal) { - var isSelectName = false; - var isSelectType = false; - var isSelectLabel = false; - var isSelectCredentials = false; - var isSelectGateway = false; - var isSelectDescription = false; - for (var i = 0; i < newVal.length; i++) { - switch (newVal[i].type) { - case types.importEntityColumnType.name.value: - isSelectName = true; - break; - case types.importEntityColumnType.type.value: - isSelectType = true; - break; - case types.importEntityColumnType.label.value: - isSelectLabel = true; - break; - case types.importEntityColumnType.accessToken.value: - isSelectCredentials = true; - break; - case types.importEntityColumnType.isGateway.value: - isSelectGateway = true; - break; - case types.importEntityColumnType.description.value: - isSelectDescription = true; - } - } - if (isSelectName && isSelectType) { - vm.theForm.$setDirty(); - } else { - vm.theForm.$setPristine(); - } - $timeout(function () { - vm.columnTypes.name.disable = isSelectName; - vm.columnTypes.type.disable = isSelectType; - vm.columnTypes.label.disable = isSelectLabel; - vm.columnTypes.gateway.disable = isSelectGateway; - vm.columnTypes.description.disable = isSelectDescription; - if (angular.isDefined(vm.columnTypes.accessToken)) { - vm.columnTypes.accessToken.disable = isSelectCredentials; - } - }); - } - }, true); -} diff --git a/ui/src/app/import-export/table-columns-assignment.tpl.html b/ui/src/app/import-export/table-columns-assignment.tpl.html deleted file mode 100644 index 023155d6ac..0000000000 --- a/ui/src/app/import-export/table-columns-assignment.tpl.html +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - - - - -
     {{ 'import.column-example' | translate }}{{ 'import.column-type.column-type' | translate }}{{ 'import.column-key' | translate }}
    {{$index + 1}}{{column.sampleData}} - - - {{type.name | translate}} - - - - - - -
    - -
    diff --git a/ui/src/app/jsonform/index.js b/ui/src/app/jsonform/index.js deleted file mode 100644 index 8dd3bd4f3c..0000000000 --- a/ui/src/app/jsonform/index.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import uiRouter from 'angular-ui-router'; -import ngMaterial from 'angular-material'; -import ngMessages from 'angular-messages'; -import thingsboardJsonForm from "../components/json-form.directive"; - -import JsonFormRoutes from './jsonform.routes'; -import JsonFormController from './jsonform.controller'; - -export default angular.module('thingsboard.jsonform', [ - uiRouter, - ngMaterial, - ngMessages, - thingsboardJsonForm -]) - .config(JsonFormRoutes) - .controller('JsonFormController', JsonFormController) - .name; diff --git a/ui/src/app/jsonform/jsonform.controller.js b/ui/src/app/jsonform/jsonform.controller.js deleted file mode 100644 index 29cf8406aa..0000000000 --- a/ui/src/app/jsonform/jsonform.controller.js +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './jsonform.scss'; - -/*@ngInject*/ -export default function JsonFormController($scope/*, $rootScope, $log*/) { - - var vm = this; - - vm.pretty = pretty; - vm.resetModel = resetModel; - vm.itParses = true; - vm.itParsesForm = true; - - vm.formJson = "[ \n" + - " {\n" + - " \"key\": \"name\",\n" + - "\t\"type\": \"text\" \n" + - " },\n" + - " {\n" + - "\t\"key\": \"name2\",\n" + - "\t\"type\": \"color\"\n" + - " },\n" + - " {\n" + - "\t\"key\": \"name3\",\n" + - "\t\"type\": \"javascript\"\n" + - " }, \n" + - "\t\"name4\"\n" + - "]"; - vm.schemaJson = "{\n" + - " \"type\": \"object\",\n" + - " \"title\": \"Comment\",\n" + - " \"properties\": {\n" + - " \"name\": {\n" + - " \"title\": \"Name 1\",\n" + - " \"type\": \"string\"\n" + - " },\n" + - " \"name2\": {\n" + - " \"title\": \"Name 2\",\n" + - " \"type\": \"string\"\n" + - " },\n" + - " \"name3\": {\n" + - " \"title\": \"Name 3\",\n" + - " \"type\": \"string\"\n" + - " },\n" + - " \"name4\": {\n" + - " \"title\": \"Name 4\",\n" + - " \"type\": \"number\"\n" + - " }\n" + - " },\n" + - " \"required\": [\n" + - " \"name1\", \"name2\", \"name3\", \"name4\"\n" + - " ]\n" + - "}"; -/* '{\n'+ - ' "type": "object",\n'+ - ' "title": "Comment",\n'+ - ' "properties": {\n'+ - ' "name": {\n'+ - ' "title": "Name",\n'+ - ' "type": "string"\n'+ - ' }\n'+ - ' },\n'+ - ' "required": [\n'+ - ' "name"\n'+ - ' ]\n'+ - '}';*/ - - vm.schema = angular.fromJson(vm.schemaJson); - vm.form = angular.fromJson(vm.formJson); - vm.model = { name: '#ccc' }; - - $scope.$watch('vm.schemaJson',function(val,old){ - if (val && val !== old) { - try { - vm.schema = angular.fromJson(vm.schemaJson); - vm.itParses = true; - } catch (e){ - vm.itParses = false; - } - } - }); - - - $scope.$watch('vm.formJson',function(val,old){ - if (val && val !== old) { - try { - vm.form = angular.fromJson(vm.formJson); - vm.itParsesForm = true; - } catch (e){ - vm.itParsesForm = false; - } - } - }); - - function pretty (){ - return angular.isString(vm.model) ? vm.model : angular.toJson(vm.model, true); - } - - function resetModel () { - $scope.ngform.$setPristine(); - vm.model = { name: 'New hello world!' }; - } -} diff --git a/ui/src/app/jsonform/jsonform.routes.js b/ui/src/app/jsonform/jsonform.routes.js deleted file mode 100644 index a1d3e5de1e..0000000000 --- a/ui/src/app/jsonform/jsonform.routes.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import jsonFormTemplate from './jsonform.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function JsonFormRoutes($stateProvider) { - $stateProvider - .state('home.jsonform', { - url: '/jsonform', - module: 'private', - auth: ['SYS_ADMIN'], - views: { - "content@home": { - templateUrl: jsonFormTemplate, - controllerAs: 'vm', - controller: 'JsonFormController' - } - }, - data: { - key: 'general', - pageTitle: 'admin.general-settings' - }, - ncyBreadcrumb: { - label: '{"icon": "settings", "label": "admin.system-settings"}' - } - }); -} diff --git a/ui/src/app/jsonform/jsonform.scss b/ui/src/app/jsonform/jsonform.scss deleted file mode 100644 index b04609d4c2..0000000000 --- a/ui/src/app/jsonform/jsonform.scss +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.form { height: 400px; } - -.schema { height: 800px; } diff --git a/ui/src/app/jsonform/jsonform.tpl.html b/ui/src/app/jsonform/jsonform.tpl.html deleted file mode 100644 index 3a2d1b0efe..0000000000 --- a/ui/src/app/jsonform/jsonform.tpl.html +++ /dev/null @@ -1,54 +0,0 @@ - - - -
    -

    The Generated Form

    -
    - - -
    - Readonly -
    -
    Form is valid
    -
    Form is not valid
    -
    Form is dirty
    -

    Model

    - Reset model -
    {{vm.pretty()}}
    -
    -
    - -
    -

    Form

    -
    -

    Schema

    -
    - -
    - -
    \ No newline at end of file diff --git a/ui/src/app/layout/breadcrumb-icon.filter.js b/ui/src/app/layout/breadcrumb-icon.filter.js deleted file mode 100644 index cafed08278..0000000000 --- a/ui/src/app/layout/breadcrumb-icon.filter.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default function BreadcrumbIcon() { - return function (bLabel) { - var labelObj = angular.fromJson(bLabel); - if (angular.isDefined(labelObj.icon)) { - return labelObj.icon; - } - return null; - }; -} diff --git a/ui/src/app/layout/breadcrumb-label.filter.js b/ui/src/app/layout/breadcrumb-label.filter.js deleted file mode 100644 index f304b5bf94..0000000000 --- a/ui/src/app/layout/breadcrumb-label.filter.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/*@ngInject*/ -export default function BreadcrumbLabel($translate) { - var labels = {}; - - var breadcrumbLabel = function (bLabel) { - - var labelObj; - labelObj = angular.fromJson(bLabel); - if (labelObj) { - var translate = !(labelObj.translate && labelObj.translate === 'false'); - var key = translate ? $translate.use() : 'orig'; - if (!labels[labelObj.label]) { - labels[labelObj.label] = {}; - } - if (!labels[labelObj.label][key]) { - labels[labelObj.label][key] = labelObj.label; - if (translate) { - $translate([labelObj.label]).then( - function (translations) { - labels[labelObj.label][key] = translations[labelObj.label]; - } - ) - } - } - return labels[labelObj.label][key]; - } else { - return ''; - } - }; - - breadcrumbLabel.$stateful = true; - - return breadcrumbLabel; -} diff --git a/ui/src/app/layout/breadcrumb.tpl.html b/ui/src/app/layout/breadcrumb.tpl.html deleted file mode 100644 index f948b06620..0000000000 --- a/ui/src/app/layout/breadcrumb.tpl.html +++ /dev/null @@ -1,38 +0,0 @@ - -
    -

    {{ steps[steps.length-1].ncyBreadcrumbLabel | breadcrumbLabel }}

    - - - - {{step.ncyBreadcrumbLabel | breadcrumbIcon}} - - {{step.ncyBreadcrumbLabel | breadcrumbLabel}} - - - - {{step.ncyBreadcrumbLabel | breadcrumbIcon}} - - {{step.ncyBreadcrumbLabel | breadcrumbLabel}} - > - -
    diff --git a/ui/src/app/layout/home.controller.js b/ui/src/app/layout/home.controller.js deleted file mode 100644 index df1dcc460f..0000000000 --- a/ui/src/app/layout/home.controller.js +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import $ from 'jquery'; - -/* eslint-disable import/no-unresolved, import/default */ - -import logoSvg from '../../svg/logo_title_white.svg'; - -/* eslint-enable import/no-unresolved, import/default */ - -/* eslint-disable angular/angularelement */ - -/*@ngInject*/ -export default function HomeController(types, loginService, userService, deviceService, Fullscreen, $scope, $element, $rootScope, $document, $state, - $window, $log, $mdMedia, $animate, $timeout) { - - var siteSideNav = $('.tb-site-sidenav', $element); - - var vm = this; - - vm.Fullscreen = Fullscreen; - vm.logoSvg = logoSvg; - - if (angular.isUndefined($rootScope.searchConfig)) { - $rootScope.searchConfig = { - searchEnabled: false, - searchByEntitySubtype: false, - searchEntityType: null, - showSearch: false, - searchText: "", - searchEntitySubtype: "" - }; - } - - vm.isShowSidenav = false; - vm.isLockSidenav = false; - - vm.displaySearchMode = displaySearchMode; - vm.displayEntitySubtypeSearch = displayEntitySubtypeSearch; - vm.openSidenav = openSidenav; - vm.goBack = goBack; - vm.searchTextUpdated = searchTextUpdated; - vm.sidenavClicked = sidenavClicked; - vm.toggleFullscreen = toggleFullscreen; - vm.openSearch = openSearch; - vm.closeSearch = closeSearch; - - $scope.$on('$stateChangeSuccess', function (evt, to, toParams, from) { - watchEntitySubtype(false); - if (angular.isDefined(to.data.searchEnabled)) { - $scope.searchConfig.searchEnabled = to.data.searchEnabled; - $scope.searchConfig.searchByEntitySubtype = to.data.searchByEntitySubtype; - $scope.searchConfig.searchEntityType = to.data.searchEntityType; - if ($scope.searchConfig.searchEnabled === false || to.name !== from.name) { - $scope.searchConfig.showSearch = false; - $scope.searchConfig.searchText = ""; - $scope.searchConfig.searchEntitySubtype = ""; - } - } else { - $scope.searchConfig.searchEnabled = false; - $scope.searchConfig.searchByEntitySubtype = false; - $scope.searchConfig.searchEntityType = null; - $scope.searchConfig.showSearch = false; - $scope.searchConfig.searchText = ""; - $scope.searchConfig.searchEntitySubtype = ""; - } - watchEntitySubtype($scope.searchConfig.searchByEntitySubtype); - }); - - vm.isGtSm = $mdMedia('gt-sm'); - if (vm.isGtSm) { - vm.isLockSidenav = true; - $animate.enabled(siteSideNav, false); - } - - $scope.$watch(function() { return $mdMedia('gt-sm'); }, function(isGtSm) { - vm.isGtSm = isGtSm; - vm.isLockSidenav = isGtSm; - vm.isShowSidenav = isGtSm; - if (!isGtSm) { - $timeout(function() { - $animate.enabled(siteSideNav, true); - }, 0, false); - } else { - $animate.enabled(siteSideNav, false); - } - }); - - function watchEntitySubtype(enableWatch) { - if ($scope.entitySubtypeWatch) { - $scope.entitySubtypeWatch(); - } - if (enableWatch) { - $scope.entitySubtypeWatch = $scope.$watch('searchConfig.searchEntitySubtype', function (newVal, prevVal) { - if (!angular.equals(newVal, prevVal)) { - $scope.$broadcast('searchEntitySubtypeUpdated'); - } - }); - } - } - - function displaySearchMode() { - return $scope.searchConfig.searchEnabled && - $scope.searchConfig.showSearch; - } - - function displayEntitySubtypeSearch() { - return $scope.searchConfig.searchByEntitySubtype && vm.isGtSm; - } - - function toggleFullscreen() { - if (Fullscreen.isEnabled()) { - Fullscreen.cancel(); - } else { - Fullscreen.all(); - } - } - - function openSearch() { - if ($scope.searchConfig.searchEnabled) { - $scope.searchConfig.showSearch = true; - $timeout(() => { - angular.element('#tb-search-text-input', $element).focus(); - }); - } - } - - function closeSearch() { - if ($scope.searchConfig.searchEnabled) { - $scope.searchConfig.showSearch = false; - if ($scope.searchConfig.searchText.length) { - $scope.searchConfig.searchText = ''; - searchTextUpdated(); - } - } - } - - function searchTextUpdated() { - $scope.$broadcast('searchTextUpdated'); - } - - function openSidenav() { - vm.isShowSidenav = true; - } - - function goBack() { - $window.history.back(); - } - - function closeSidenav() { - vm.isShowSidenav = false; - } - - function sidenavClicked() { - if (!vm.isLockSidenav) { - closeSidenav(); - } - } - -} - -/* eslint-enable angular/angularelement */ \ No newline at end of file diff --git a/ui/src/app/layout/home.routes.js b/ui/src/app/layout/home.routes.js deleted file mode 100644 index 78cd5cece3..0000000000 --- a/ui/src/app/layout/home.routes.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import breadcrumbTemplate from './breadcrumb.tpl.html'; -import homeTemplate from './home.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function HomeRoutes($stateProvider, $breadcrumbProvider) { - - $breadcrumbProvider.setOptions({ - prefixStateName: 'home', - templateUrl: breadcrumbTemplate - }); - - $stateProvider - .state('home', { - url: '', - module: 'private', - auth: ['SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER'], - views: { - "@": { - controller: 'HomeController', - controllerAs: 'vm', - templateUrl: homeTemplate - } - }, - data: { - pageTitle: 'home.home' - }, - ncyBreadcrumb: { - skip: true - } - }); -} diff --git a/ui/src/app/layout/home.scss b/ui/src/app/layout/home.scss deleted file mode 100644 index ad0857412a..0000000000 --- a/ui/src/app/layout/home.scss +++ /dev/null @@ -1,96 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "~compass-sass-mixins/lib/animate"; - -.tb-invisible { - display: none !important; -} - -.tb-primary-toolbar { - h1 { - font-size: 24px !important; - font-weight: 400 !important; - } -} - -.tb-breadcrumb { - font-size: 18px !important; - font-weight: 400 !important; - - h1, - a, - span { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - a { - border: none; - opacity: .75; - - transition: opacity .35s; - } - - a:hover, - a:focus { - text-decoration: none !important; - border: none; - opacity: 1; - } - - .divider { - padding: 0 30px; - } -} - -md-sidenav.tb-site-sidenav { - width: 250px; -} - -md-icon.tb-logo-title { - width: 200px; - height: 36px; -} - -.tb-nav-header { - z-index: 2; - flex-shrink: 0; - white-space: nowrap; -} - -.tb-nav-header-toolbar { - z-index: 2; - flex-shrink: 0; - white-space: nowrap; - border-bottom: 1px solid rgba(0, 0, 0, .12); - - .md-toolbar-tools { - flex-basis: auto; - } -} - -.tb-side-menu-toolbar { - overflow-y: auto; -} - -.tb-entity-subtype-search { - margin-top: 15px; -} - -.tb-entity-search { - margin-top: 34px; -} diff --git a/ui/src/app/layout/home.tpl.html b/ui/src/app/layout/home.tpl.html deleted file mode 100644 index 01a4564f76..0000000000 --- a/ui/src/app/layout/home.tpl.html +++ /dev/null @@ -1,84 +0,0 @@ - - -
    - -
    - -
    -
    -
    - - - -
    - -
    - -
    - - menu - - - arrow_back - - - arrow_back - -
    - -
    -
    - - - - - -
    - - search - - - - - - -
    -
    - - -
    - -
    -
    diff --git a/ui/src/app/layout/index.js b/ui/src/app/layout/index.js deleted file mode 100644 index f3b4d10e4f..0000000000 --- a/ui/src/app/layout/index.js +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './home.scss'; - -import uiRouter from 'angular-ui-router'; -import ngSanitize from 'angular-sanitize'; -import 'angular-breadcrumb'; - -import thingsboardMenu from '../services/menu.service'; -import thingsboardApiDevice from '../api/device.service'; -import thingsboardApiLogin from '../api/login.service'; -import thingsboardApiUser from '../api/user.service'; - -import thingsboardNoAnimate from '../components/no-animate.directive'; -import thingsboardOnFinishRender from '../components/finish-render.directive'; -import thingsboardSideMenu from '../components/side-menu.directive'; -import thingsboardNavTree from '../components/nav-tree.directive'; -import thingsboardDashboardAutocomplete from '../components/dashboard-autocomplete.directive'; -import thingsboardKvMap from '../components/kv-map.directive'; -import thingsboardGatewayConfig from '../components/gateway/gateway-config.directive'; -import thingsboardGatewayConfigSelect from '../components/gateway/gateway-config-select.directive'; -import thingsboardGatewayForm from '../components/gateway/gateway-form.directive'; -import thingsboardJsonObjectEdit from '../components/json-object-edit.directive'; -import thingsboardJsonContent from '../components/json-content.directive'; - -import thingsboardUserMenu from './user-menu.directive'; - -import thingsboardEntity from '../entity'; -import thingsboardEvent from '../event'; -import thingsboardAlarm from '../alarm'; -import thingsboardAuditLog from '../audit'; -import thingsboardExtension from '../extension'; -import thingsboardTenant from '../tenant'; -import thingsboardCustomer from '../customer'; -import thingsboardUser from '../user'; -import thingsboardHomeLinks from '../home'; -import thingsboardAdmin from '../admin'; -import thingsboardProfile from '../profile'; -import thingsboardAsset from '../asset'; -import thingsboardDevice from '../device'; -import thingsboardEntityView from '../entity-view'; -import thingsboardWidgetLibrary from '../widget'; -import thingsboardDashboard from '../dashboard'; -import thingsboardRuleChain from '../rulechain'; -import thingsboardQueue from '../components/queue'; - -import thingsboardJsonForm from '../jsonform'; - -import HomeRoutes from './home.routes'; -import HomeController from './home.controller'; -import BreadcrumbLabel from './breadcrumb-label.filter'; -import BreadcrumbIcon from './breadcrumb-icon.filter'; - -export default angular.module('thingsboard.home', [ - uiRouter, - ngSanitize, - 'ncy-angular-breadcrumb', - thingsboardMenu, - thingsboardHomeLinks, - thingsboardUserMenu, - thingsboardEntity, - thingsboardEvent, - thingsboardAlarm, - thingsboardAuditLog, - thingsboardExtension, - thingsboardTenant, - thingsboardCustomer, - thingsboardUser, - thingsboardAdmin, - thingsboardProfile, - thingsboardAsset, - thingsboardDevice, - thingsboardEntityView, - thingsboardWidgetLibrary, - thingsboardDashboard, - thingsboardRuleChain, - thingsboardQueue, - thingsboardJsonForm, - thingsboardApiDevice, - thingsboardApiLogin, - thingsboardApiUser, - thingsboardNoAnimate, - thingsboardOnFinishRender, - thingsboardSideMenu, - thingsboardNavTree, - thingsboardDashboardAutocomplete, - thingsboardKvMap, - thingsboardGatewayConfig, - thingsboardGatewayConfigSelect, - thingsboardGatewayForm, - thingsboardJsonObjectEdit, - thingsboardJsonContent -]) - .config(HomeRoutes) - .controller('HomeController', HomeController) - .filter('breadcrumbLabel', BreadcrumbLabel) - .filter('breadcrumbIcon', BreadcrumbIcon) - .name; diff --git a/ui/src/app/layout/user-menu.directive.js b/ui/src/app/layout/user-menu.directive.js deleted file mode 100644 index f64eab7221..0000000000 --- a/ui/src/app/layout/user-menu.directive.js +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './user-menu.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import userMenuTemplate from './user-menu.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -export default angular.module('thingsboard.directives.usermenu', []) - .directive('tbUserMenu', UserMenu) - .name; - -/*@ngInject*/ -function UserMenu() { - return { - restrict: "E", - scope: true, - bindToController: { - displayUserInfo: '=', - }, - controller: UserMenuController, - controllerAs: 'vm', - templateUrl: userMenuTemplate - }; -} - -/*@ngInject*/ -function UserMenuController($scope, userService, $translate, $state) { - - var vm = this; - - var dashboardUser = userService.getCurrentUser(); - - vm.authorityName = authorityName; - vm.logout = logout; - vm.openProfile = openProfile; - vm.userDisplayName = userDisplayName; - - function authorityName() { - var name = "user.anonymous"; - if (dashboardUser) { - var authority = dashboardUser.authority; - if (authority === 'SYS_ADMIN') { - name = 'user.sys-admin'; - } else if (authority === 'TENANT_ADMIN') { - name = 'user.tenant-admin'; - } else if (authority === 'CUSTOMER_USER') { - name = 'user.customer'; - } - } - return $translate.instant(name); - } - - function userDisplayName() { - var name = ""; - if (dashboardUser) { - if ((dashboardUser.firstName && dashboardUser.firstName.length > 0) || - (dashboardUser.lastName && dashboardUser.lastName.length > 0)) { - if (dashboardUser.firstName) { - name += dashboardUser.firstName; - } - if (dashboardUser.lastName) { - if (name.length > 0) { - name += " "; - } - name += dashboardUser.lastName; - } - } else { - name = dashboardUser.email; - } - } - return name; - } - - function openProfile() { - $state.go('home.profile'); - } - - function logout() { - userService.logout(); - } -} \ No newline at end of file diff --git a/ui/src/app/layout/user-menu.scss b/ui/src/app/layout/user-menu.scss deleted file mode 100644 index b66cf7a128..0000000000 --- a/ui/src/app/layout/user-menu.scss +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -div.tb-user-info { - line-height: 1.5; - - span { - text-align: left; - text-transform: none; - } - - span.tb-user-display-name { - font-size: .8rem; - font-weight: 300; - letter-spacing: .008em; - } - - span.tb-user-authority { - font-size: .8rem; - font-weight: 300; - letter-spacing: .005em; - opacity: .8; - } -} - -md-icon.tb-mini-avatar { - width: 36px; - height: 36px; - margin: auto 8px; - font-size: 36px; -} diff --git a/ui/src/app/layout/user-menu.tpl.html b/ui/src/app/layout/user-menu.tpl.html deleted file mode 100644 index f36936df0d..0000000000 --- a/ui/src/app/layout/user-menu.tpl.html +++ /dev/null @@ -1,45 +0,0 @@ - -
    - - - - more_vert - - - - - account_circle - home.profile - - - - - exit_to_app - home.logout - - - - -
    diff --git a/ui/src/app/locale/locale.constant-cs_CZ.json b/ui/src/app/locale/locale.constant-cs_CZ.json deleted file mode 100644 index 13247f188a..0000000000 --- a/ui/src/app/locale/locale.constant-cs_CZ.json +++ /dev/null @@ -1,1646 +0,0 @@ -{ - "access": { - "unauthorized": "Neautorizováno", - "unauthorized-access": "Neautorizovaný přístup", - "unauthorized-access-text": "Musíte se zaregistrovat, abyste měli přístup k tomuto zdroji!", - "access-forbidden": "Přístup zakázán", - "access-forbidden-text": "Pro přístup do tohoto umístění nemáte přístupová oprávnění!
    Pokud si stále přejete získat přístup do tohoto umístění, zkuste se zaregistrovat pod jiným uživatelem.", - "refresh-token-expired": "Relace vypršela", - "refresh-token-failed": "Nemohu aktualizovat relaci" - }, - "action": { - "activate": "Aktivovat", - "suspend": "Deaktivovat", - "save": "Uložit", - "saveAs": "Uložit jako", - "cancel": "Storno", - "ok": "OK", - "delete": "Smazat", - "add": "Přidat", - "yes": "Ano", - "no": "Ne", - "update": "Aktualizovat", - "remove": "Odstranit", - "search": "Vyhledat", - "clear-search": "Zrušit hledání", - "assign": "Přiřadit", - "unassign": "Odebrat", - "share": "Sdílet", - "make-private": "Učinit soukromým", - "apply": "Použít", - "apply-changes": "Uložit změny", - "edit-mode": "Režim editace", - "enter-edit-mode": "Vstoupit do režimu editace", - "decline-changes": "Zahodit změny", - "close": "Zavřít", - "back": "Zpět", - "run": "Spustit", - "sign-in": "Zaregistrovat!", - "edit": "Editovat", - "view": "Zobrazit", - "create": "Vytvořit", - "drag": "Táhnout", - "refresh": "Obnovit", - "undo": "Vrátit", - "copy": "Kopírovat", - "paste": "Vložit", - "copy-reference": "Kopírovat referenci", - "paste-reference": "Vložit referenci", - "import": "Importovat", - "export": "Exportovat", - "share-via": "Sdílet přes {{provider}}" - }, - "aggregation": { - "aggregation": "Agregace", - "function": "Funkce pro agregaci dat", - "limit": "Maximální hodnoty", - "group-interval": "Interval seskupení", - "min": "Min", - "max": "Max", - "avg": "Průměr", - "sum": "Suma", - "count": "Počet", - "none": "Žádná" - }, - "admin": { - "general": "Obecné", - "general-settings": "Obecná nastavení", - "outgoing-mail": "Odchozí email", - "outgoing-mail-settings": "Nastavení odchozího emailu", - "system-settings": "Systémová nastavení", - "test-mail-sent": "Testovací zpráva byla úspěšně odeslána!", - "base-url": "Základní URL", - "base-url-required": "Hodnota Základní URL je povinná.", - "mail-from": "Email od", - "mail-from-required": "Hodnota Email od je povinná.", - "smtp-protocol": "SMTP protokol", - "smtp-host": "SMTP host", - "smtp-host-required": "Hodnota SMTP host je povinná.", - "smtp-port": "SMTP port", - "smtp-port-required": "Musíte zadat smtp port.", - "smtp-port-invalid": "Tohle nevypadá jako platný smtp port.", - "timeout-msec": "Časový limit (msec)", - "timeout-required": "Hodnota Časový limit je povinná.", - "timeout-invalid": "Tohle nevypadá jako platný časový limit.", - "enable-tls": "Povolit TLS", - "tls-version": "Verze TLS", - "send-test-mail": "Odeslat testovací zprávu" - }, - "alarm": { - "alarm": "Alarm", - "alarms": "Alarmy", - "select-alarm": "Vybrat alarm", - "no-alarms-matching": "Žádné alarmy odpovídající '{{entity}}' nebyly nalezeny.", - "alarm-required": "Alarm je povinný", - "alarm-status": "Stav alarmu", - "search-status": { - "ANY": "Všechny", - "ACTIVE": "Aktivní", - "CLEARED": "Odstraněné", - "ACK": "Přijaté", - "UNACK": "Nepřijaté" - }, - "display-status": { - "ACTIVE_UNACK": "Aktivní nepřijaté", - "ACTIVE_ACK": "Aktivní přijaté", - "CLEARED_UNACK": "Odstraněné nepřijaté", - "CLEARED_ACK": "Odstraněné přijaté" - }, - "no-alarms-prompt": "Žádné alarmy nebyly nalezeny", - "created-time": "Datum vytvoření", - "type": "Typ", - "severity": "Závažnost", - "originator": "Původce", - "originator-type": "Typ původce", - "details": "Detail", - "status": "Stav", - "alarm-details": "Detail alarmu", - "start-time": "Datum zahájení", - "end-time": "Datum ukončení", - "ack-time": "Datum přijetí", - "clear-time": "Datum vyřešení", - "severity-critical": "Kritická", - "severity-major": "Vysoká", - "severity-minor": "Nízká", - "severity-warning": "Varování", - "severity-indeterminate": "Střední", - "acknowledge": "Přijmout", - "clear": "Vyřešit", - "search": "Vyhledat alarmy", - "selected-alarms": "Vybráno { count, plural, 1 {1 alarmů} other {# alarmů} }", - "no-data": "Nejsou zde žádná data", - "polling-interval": "Interval frekvence příjmu alarmů (vteřin)", - "polling-interval-required": "Interval frekvence příjmu alarmů je povinný.", - "min-polling-interval-message": "Minimální povolený interval frekvence příjmu alarmů je 1 vteřina.", - "aknowledge-alarms-title": "Přijmout { count, plural, 1 {1 alarm} other {# alarmů} }", - "aknowledge-alarms-text": "Jste si jisti že chcete přijmout { count, plural, 1 {1 alarm} other {# alarmů} }?", - "aknowledge-alarm-title": "Přijmout alarm", - "aknowledge-alarm-text": "Jste si jisti, že chcete přijmout alarm?", - "clear-alarms-title": "Odstranit { count, plural, 1 {1 alarm} other {# alarmů} }", - "clear-alarms-text": "Jste si jisti, že chcete odstranit { count, plural, 1 {1 alarm} other {# alarmů} }?", - "clear-alarm-title": "Odstranit alarm", - "clear-alarm-text": "Jste si jisti, že chcete alarm odstranit?", - "alarm-status-filter": "Filtr stavu alarmu" - }, - "alias": { - "add": "Přidat alias", - "edit": "Editovat alias", - "name": "Název aliasu", - "name-required": "Název aliasu je povinný", - "duplicate-alias": "Alias s identickým názvem již existuje.", - "filter-type-single-entity": "Jedna entita", - "filter-type-entity-list": "Seznam entit", - "filter-type-entity-name": "Název entity", - "filter-type-state-entity": "Entita ze stavu dashboardu", - "filter-type-state-entity-description": "Entita převzata z parametrů stavu dashboardu", - "filter-type-asset-type": "Typ aktiva", - "filter-type-asset-type-description": "Aktiva typu '{{assetType}}'", - "filter-type-asset-type-and-name-description": "Aktiva typu '{{assetType}}' s názvem začínajícím '{{prefix}}'", - "filter-type-device-type": "Typ zařízení", - "filter-type-device-type-description": "Zařízení typu '{{deviceType}}'", - "filter-type-device-type-and-name-description": "Zařízení typu '{{deviceType}}' s názvem začínajícím '{{prefix}}'", - "filter-type-entity-view-type": "Typ entitního pohledu", - "filter-type-entity-view-type-description": "Entitní pohledy typu '{{entityView}}'", - "filter-type-entity-view-type-and-name-description": "Entitní pohledy typu '{{entityView}}' s názvem začínajícím '{{prefix}}'", - "filter-type-relations-query": "Dotaz na vztahy", - "filter-type-relations-query-description": "{{entities}} se {{relationType}} vztahem {{direction}} {{rootEntity}}", - "filter-type-asset-search-query": "Dotaz na vyhledání aktiva", - "filter-type-asset-search-query-description": "Aktiva typů {{assetTypes}} se {{relationType}} vztahem {{direction}} {{rootEntity}}", - "filter-type-device-search-query": "Dotaz na vyhledání zařízení", - "filter-type-device-search-query-description": "Zařízení typů {{deviceTypes}} se {{relationType}} vztahem {{direction}} {{rootEntity}}", - "filter-type-entity-view-search-query": "Dotaz na vyhledání zobrazení entity", - "filter-type-entity-view-search-query-description": "Entitní pohledy typů {{entityViewTypes}} se {{relationType}} vztahem {{direction}} {{rootEntity}}", - "entity-filter": "Filtr entity", - "resolve-multiple": "Použít jako více entit", - "filter-type": "Typ filtru", - "filter-type-required": "Typ filtru je povinný.", - "entity-filter-no-entity-matched": "Žádné entity odpovídající specifikovanému filtru nebyly nalezeny.", - "no-entity-filter-specified": "Nebyl specifikován žádný filtr entit", - "root-state-entity": "Použít stav entity dashboard jako kořenovou", - "root-entity": "Kořenová entita", - "state-entity-parameter-name": "Název parametru stavu entity", - "default-state-entity": "Defaultní stav entity", - "default-entity-parameter-name": "Defaultně", - "max-relation-level": "Maximální úroveň vazeb", - "unlimited-level": "Neomezená úroveň", - "state-entity": "Dashboard stav entity", - "all-entities": "Všechny entity", - "any-relation": "všechny" - }, - "asset": { - "asset": "Aktivum", - "assets": "Aktiva", - "management": "Správa aktiv", - "view-assets": "Zobrazit aktiva", - "add": "Přidat aktivum", - "assign-to-customer": "Přiřadit zákazníkovi", - "assign-asset-to-customer": "Přiřadit aktiva zákazníkovi", - "assign-asset-to-customer-text": "Prosím vyberte aktiva, která mají být přiřazena zákazníkovi", - "no-assets-text": "Žádná aktiva nenalezena", - "assign-to-customer-text": "Prosím vyberte zákazníka, kterému mají být aktiva přiřazena", - "public": "Veřejné", - "assignedToCustomer": "Přiřazeno zákazníkovi", - "make-public": "Zveřejnit aktivum", - "make-private": "Učinit aktivum neveřejným", - "unassign-from-customer": "Odebrat aktivum zákazníkovi", - "delete": "Smazat aktivum", - "asset-public": "Aktivum je veřejné", - "asset-type": "Typ aktiva", - "asset-type-required": "Typ aktiva je povinný.", - "select-asset-type": "Vyberte typ aktiva", - "enter-asset-type": "Zadejte typ aktiva", - "any-asset": "Všechna aktiva", - "no-asset-types-matching": "Žádné typy aktiv odpovídající '{{entitySubtype}}' nebyly nalezeny.", - "asset-type-list-empty": "Žádné typy aktiv nebyly vybrány.", - "asset-types": "Typy aktiv", - "name": "Název", - "name-required": "Název je povinný.", - "description": "Popis", - "type": "Typ", - "type-required": "Typ je povinný.", - "details": "Detail", - "events": "Události", - "add-asset-text": "Přidat nové aktivum", - "asset-details": "Detail aktiva", - "assign-assets": "Přiřadit aktiva", - "assign-assets-text": "Přiřadit { count, plural, 1 {1 aktivum} other {# aktiva} } zákazníkovi", - "delete-assets": "Smazat aktiva", - "unassign-assets": "Odebrat aktiva", - "unassign-assets-action-title": "Odebrat { count, plural, 1 {1 aktivum} other {# aktiva} } zákazníkovi", - "assign-new-asset": "Přiřadit nové aktivum", - "delete-asset-title": "Jste si jisti, že chcete smazat aktivum '{{assetName}}'?", - "delete-asset-text": "Buďte opatrní, protože po potvrzení nebude možné aktivum ani žádná související data obnovit.", - "delete-assets-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 aktivum} other {# aktiva} }?", - "delete-assets-action-title": "Smazat { count, plural, 1 {1 aktivum} other {# aktiva} }", - "delete-assets-text": "Buďte opatrní, protože po potvrzení budou všechna vybraná aktiva odstraněna a žádná související data nebude možné obnovit.", - "make-public-asset-title": "Jste si jisti, že chcete aktivum '{{assetName}}' zveřejnit?", - "make-public-asset-text": "Po potvrzení se aktivum a všechna související data stanou veřejnými a dostupnými pro ostatní.", - "make-private-asset-title": "Jste si jisti, že chcete aktivum '{{assetName}}' učinit neveřejným?", - "make-private-asset-text": "Po potvrzení se aktivum a všechna související data stanou neveřejnými a nedostupnými pro ostatní.", - "unassign-asset-title": "Jste si jisti, že chcete odebrat aktivum '{{assetName}}'?", - "unassign-asset-text": "Po potvrzení bude aktivum odebráno a nebude pro zákazníka dostupné.", - "unassign-asset": "Odebrat aktivum", - "unassign-assets-title": "Jste si jisti, že chcete odebrat { count, plural, 1 {1 aktivum} other {# aktiva} }?", - "unassign-assets-text": "Po potvrzení budou všechna vybraná aktiva odebrána a nebudou pro zákazníka dostupná.", - "copyId": "Kopírovat Id aktiva", - "idCopiedMessage": "Id aktiva bylo zkopírováno do schránky", - "select-asset": "Vybrat aktivum", - "no-assets-matching": "Žádná aktiva odpovídající '{{entity}}' nebyla nalezena.", - "asset-required": "Aktivum je povinné", - "name-starts-with": "Název aktiva začíná", - "label": "Název" - }, - "attribute": { - "attributes": "Atributy", - "latest-telemetry": "Poslední telemetrie", - "attributes-scope": "Rozsah atributů entity", - "scope-latest-telemetry": "Poslední telemetrie", - "scope-client": "Atributy klienta", - "scope-server": "Atributy serveru", - "scope-shared": "Sdílené atributy", - "add": "Přidat atribut", - "key": "Klíč", - "last-update-time": "Čas poslední aktualizace", - "key-required": "atribut klíč je povinný.", - "value": "Hodnota", - "value-required": "Atribut hodnota je povinný.", - "delete-attributes-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 atribut} other {# atributů} }?", - "delete-attributes-text": "Buďte opatrní, protože po potvrzení budou všechny vybrané atributy odstraněny.", - "delete-attributes": "Smazat atributy", - "enter-attribute-value": "Zadejte hodnotu atributu", - "show-on-widget": "Zobrazit ve widgetu", - "widget-mode": "Režim widgetu", - "next-widget": "Další widget", - "prev-widget": "Předchozí widget", - "add-to-dashboard": "Přidat na dashboard", - "add-widget-to-dashboard": "Přidat widget na dashboard", - "selected-attributes": "Vybráno { count, plural, 1 {1 atributů} other {# atributů} }", - "selected-telemetry": "Vybráno { count, plural, 1 {1 jednotek telemetrie} other {# jednotek telemetrie} }" - }, - "audit-log": { - "audit": "Audit", - "audit-logs": "Záznamy auditu", - "timestamp": "Časová značka", - "entity-type": "Typ entity", - "entity-name": "Název entity", - "user": "Uživatel", - "type": "Typ", - "status": "Stav", - "details": "Detail", - "type-added": "Přidáno", - "type-deleted": "Smazáno", - "type-updated": "Aktualizováno", - "type-attributes-updated": "Atributy aktualizovány", - "type-attributes-deleted": "Atributy smazány", - "type-rpc-call": "RPC volání", - "type-credentials-updated": "Přístupové údaje aktualizovány", - "type-assigned-to-customer": "Přiřazeno zákazníkovi", - "type-unassigned-from-customer": "Odebráno zákazníkovi", - "type-activated": "Aktivováno", - "type-suspended": "Deaktivováno", - "type-credentials-read": "Zobrazení přístupových údajů", - "type-attributes-read": "Zobrazení atributů", - "type-relation-add-or-update": "Vztah aktualizován", - "type-relation-delete": "Vztah smazán", - "type-relations-delete": "Všechny vztahy smazány", - "type-alarm-ack": "Přijato", - "type-alarm-clear": "Odstraněno", - "status-success": "Úspěch", - "status-failure": "Chyba", - "audit-log-details": "Detail záznamu auditu", - "no-audit-logs-prompt": "Žádné záznamy nenalezeny", - "action-data": "Data akce", - "failure-details": "Detail chyby", - "search": "Prohledat záznamy auditu", - "clear-search": "Vymazat vyhledávání" - }, - "confirm-on-exit": { - "message": "Některé změny nebyly uloženy. Jste si jisti, že chcete tuto stránku opustit?", - "html-message": "Některé změny nebyly uloženy.
    Jste si jisti, že chcete tuto stránku opustit?", - "title": "Neuložené změny" - }, - "contact": { - "country": "Stát", - "city": "Město", - "state": "Region", - "postal-code": "PSČ", - "postal-code-invalid": "Formát PSČ neplatný.", - "address": "Adresa", - "address2": "Adresa 2", - "phone": "Telefon", - "email": "Email", - "no-address": "Žádná adresa" - }, - "common": { - "username": "Uživatelské jméno", - "password": "Heslo", - "enter-username": "Zadejte uživatelské jméno", - "enter-password": "Zadejte heslo", - "enter-search": "Zadejte hledaný řetězec" - }, - "content-type": { - "json": "JSON", - "text": "Text", - "binary": "Binární (Base64)" - }, - "customer": { - "customer": "Zákazník", - "customers": "Zákazníci", - "management": "Správa zákazníků", - "dashboard": "Dashboard zákazníka", - "dashboards": "Dashboardy zákazníka", - "devices": "Zařízení zákazníka", - "entity-views": "Entitní pohledy zákazníka", - "assets": "Aktiva zákazníka", - "public-dashboards": "Veřejné dashboardy", - "public-devices": "Veřejná zařízení", - "public-assets": "Veřejná aktiva", - "public-entity-views": "Veřejné entitní pohledy", - "add": "Přidat zákazníka", - "delete": "Smazat zákazníka", - "manage-customer-users": "Spravovat uživatele zákazníka", - "manage-customer-devices": "Spravovat zařízení zákazníka", - "manage-customer-dashboards": "Spravovat dashboardy zákazníka", - "manage-public-devices": "Spravovat veřejná zařízení", - "manage-public-dashboards": "Spravovat veřejné dashboardy", - "manage-customer-assets": "Spravovat aktiva zákazníka", - "manage-public-assets": "Spravovat veřejná aktiva", - "add-customer-text": "Přidat nového zákazníka", - "no-customers-text": "Žádní zákazníci nenalezeni", - "customer-details": "Detail zákazníka", - "delete-customer-title": "Jste si jisti, že chcete smazat zákazníka '{{customerTitle}}'?", - "delete-customer-text": "Buďte opatrní, protože po potvrzení nebude možné zákazníka ani žádná související data obnovit.", - "delete-customers-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 zákazníka} other {# zákazníků} }?", - "delete-customers-action-title": "Smazat { count, plural, 1 {1 zákazníka} other {# zákazníků} }", - "delete-customers-text": "Buďte opatrní, protože po potvrzení budou všichni vybraní zákazníci odstraněni a žádná související data nebude možné obnovit.", - "manage-users": "Spravovat uživatele", - "manage-assets": "Spravovat aktiva", - "manage-devices": "Spravovat zařízení", - "manage-dashboards": "Spravovat dashboardy", - "title": "Název", - "title-required": "Název je povinný.", - "description": "Popis", - "details": "Detail", - "events": "Události", - "copyId": "Kopírovat Id zákazníka", - "idCopiedMessage": "Id zákazníka bylo zkopírováno do schránky", - "select-customer": "Vybrat zákazníka", - "no-customers-matching": "Žádní zákazníky odpovídající '{{entity}}' nebyli nalezeni.", - "customer-required": "Zákazník je povinný", - "select-default-customer": "Vybrat defaultního zákazníka", - "default-customer": "Defaultní zákazník", - "default-customer-required": "Aby bylo možné ladit dashboard na úrovni tenanta, je nutné zadat defaultního zákazníka." - }, - "datetime": { - "date-from": "Datum od", - "time-from": "Čas od", - "date-to": "Datum do", - "time-to": "Čas do" - }, - "dashboard": { - "dashboard": "Dashboard", - "dashboards": "Dashboardy", - "management": "Správa dashboardů", - "view-dashboards": "Zobrazit dashboardy", - "add": "Přidat dashboard", - "assign-dashboard-to-customer": "Přiřadit dashboard(y) zákazníkovi", - "assign-dashboard-to-customer-text": "Prosím vyberte dashboardy, které mají být přiřazeny zákazníkovi", - "assign-to-customer-text": "Prosím vyberete zákazníka, kterému má být dashboard(y) přiřazen", - "assign-to-customer": "Přiřadit zákazníkovi", - "unassign-from-customer": "Odebrat zákazníkovi", - "make-public": "Zveřejnit dashboard", - "make-private": "Učinit dashboard neveřejným", - "manage-assigned-customers": "Spravovat přiřazené zákazníky", - "assigned-customers": "Přiřazení zákazníci", - "assign-to-customers": "Přiřadit dashboard(y) zákazníkovi", - "assign-to-customers-text": "Prosím vyberte zákazníky, kterým má být dashboard(y) přiřazen(y)", - "unassign-from-customers": "Odebrat dashboard(y) zákazníkům", - "unassign-from-customers-text": "Prosím vyberte zákazníky, kterým má být dashboard(y) odebrán(y)", - "no-dashboards-text": "Nebyly nalezeny žádné dashboardy", - "no-widgets": "Nejsou nastaveny žádné widgety", - "add-widget": "Přidat nový widget", - "title": "Název", - "select-widget-title": "Vybrat widget", - "select-widget-subtitle": "Seznam dostupných typů widgetu", - "delete": "Smazat dashboard", - "title-required": "Název je povinný.", - "description": "Popis", - "details": "Detail", - "dashboard-details": "Detail dashboardu", - "add-dashboard-text": "Přidat nový dashboard", - "assign-dashboards": "Přiřadit dashboardy", - "assign-new-dashboard": "Přiřadit nový dashboard", - "assign-dashboards-text": "Přiřadit { count, plural, 1 {1 dashboard} other {# dashboardů} } zákazníkům", - "unassign-dashboards-action-text": "Odebrat { count, plural, 1 {1 dashboard} other {# dashboardů} } zákazníkům", - "delete-dashboards": "Smazat dashboardy", - "unassign-dashboards": "Odebrat dashboardy", - "unassign-dashboards-action-title": "Odebrat { count, plural, 1 {1 dashboard} other {# dashboardů} } zákazníkovi", - "delete-dashboard-title": "Jste si jisti, že chcete odstranit dashboard '{{dashboardTitle}}'?", - "delete-dashboard-text": "Buďte opatrní, protože po potvrzení nebude možné dashboard ani žádná soubisející data obnovit.", - "delete-dashboards-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 dashboard} other {# dashboardů} }?", - "delete-dashboards-action-title": "Smazat { count, plural, 1 {1 dashboard} other {# dashboardů} }", - "delete-dashboards-text": "Buďte opatrní, protože po potvrzení budou všechny vybrané dashboardy smazány a žádná související data nebude možné obnovit.", - "unassign-dashboard-title": "Jste si jistí, že chcete odebrat dashboard '{{dashboardTitle}}'?", - "unassign-dashboard-text": "Po potvrzení bude dashboard odebrán a nebude pro zákazníka dostupný.", - "unassign-dashboard": "Odebrat dashboard", - "unassign-dashboards-title": "Jste si jisti, že chcete odebrat { count, plural, 1 {1 dashboard} other {# dashboardů} }?", - "unassign-dashboards-text": "Po potvrzení budou všechny vybrané dashboardy odebrány a nebudou pro zákazníka dostupné.", - "public-dashboard-title": "Dashboard je nyní veřejný", - "public-dashboard-text": "Váš dashboard {{dashboardTitle}} je nyní veřejný a dostupný prostřednictvím následujícího veřejného odkazu:", - "public-dashboard-notice": "Poznámka: Nezapomeňte zveřejnit také příslušná zařízení, aby bylo možné přistupovat k jejich datům.", - "make-private-dashboard-title": "Jste si jisti, že chcete dashboard '{{dashboardTitle}}' zneveřejnit?", - "make-private-dashboard-text": "Po potvrzení bude dashboard neveřejný a nebude pro ostatní dostupný.", - "make-private-dashboard": "Učinit dashboard neveřejným", - "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard", - "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard", - "select-dashboard": "Vybrat dashboard", - "no-dashboards-matching": "Žádné dashboardy odpovídající '{{entity}}' nebyly nalezeny.", - "dashboard-required": "Dashboard je povinný.", - "select-existing": "Vybrat existující dashboard", - "create-new": "Vytvořit nový dashboard", - "new-dashboard-title": "Název nového dashboardu", - "open-dashboard": "Otevřít dashboard", - "set-background": "Nastavit pozadí", - "background-color": "Barva pozadí", - "background-image": "Obrázek pozadí", - "background-size-mode": "Režim velikosti pozadí", - "no-image": "Žádný obrázek nebyl vybrán", - "drop-image": "Přetáhněte sem obrázek nebo klikněte pro výběr souboru pro nahrání.", - "settings": "Nastavení", - "columns-count": "Počet sloupců", - "columns-count-required": "Počet sloupců je povinný.", - "min-columns-count-message": "Minimální povolený počet sloupců je 10.", - "max-columns-count-message": "Maximální povolený počet sloupců je 1000.", - "widgets-margins": "Okraj mezi widgety", - "horizontal-margin": "Horizontální okraj", - "horizontal-margin-required": "Hodnota horizontálního okraje je povinná.", - "min-horizontal-margin-message": "Minimální povolená hodnota horizontálního okraje je 0.", - "max-horizontal-margin-message": "Maximální povolená hodnota horizontálního okraje je 50.", - "vertical-margin": "Vertikální okraj", - "vertical-margin-required": "Hodnota vertikálního okraje je povinná.", - "min-vertical-margin-message": "Minimální povolená hodnota vertikálního okraje je 0.", - "max-vertical-margin-message": "Maximální povolená hodnota vertikálního okraje je 50.", - "autofill-height": "Automaticky vyplnit na výšku rozmístění", - "mobile-layout": "Nastavení rozmístění pro mobilní zařízení", - "mobile-row-height": "Výška řádku pro mobilní zařízení, px", - "mobile-row-height-required": "Hodnota výšku řádku pro mobilní zařízení je povinná.", - "min-mobile-row-height-message": "Minimální povolená hodnota výšku řádku pro mobilní zařízení je 5 pixelů.", - "max-mobile-row-height-message": "Maximální povolená hodnota výšku řádku pro mobilní zařízení je 200 pixelů.", - "display-title": "Zobrazit název dashboardu", - "toolbar-always-open": "Ponechat nástrojovou lištu otevřenou", - "title-color": "Barva názvu", - "display-dashboards-selection": "Zobrazit výběr dashboardů", - "display-entities-selection": "Zobrazit výběr entit", - "display-dashboard-timewindow": "Zobrazit časové okno", - "display-dashboard-export": "Zobrazit export", - "import": "Importovat dashboard", - "export": "Exportovat dashboard", - "export-failed-error": "Dashboard nebylo možné exportovat: {{error}}", - "create-new-dashboard": "Vytvořit nový dashboard", - "dashboard-file": "Soubor dashboardu", - "invalid-dashboard-file-error": "Dashboard nebylo možné importovat: Neplatná datová struktura dashboardu.", - "dashboard-import-missing-aliases-title": "Konfigurovat aliasy používané importovaným dashboardem", - "create-new-widget": "Přidat nový widget", - "import-widget": "Importovat widget", - "widget-file": "Soubor widgetu", - "invalid-widget-file-error": "Widget nebylo možné importovat: Neplatná datová struktura widgetu.", - "widget-import-missing-aliases-title": "Konfigurovat aliasy používané importovaným widgetem", - "open-toolbar": "Otevřít nástrojovou lištu dashboardu", - "close-toolbar": "Zavřít nástrojovou lištu", - "configuration-error": "Chyba konfigurace", - "alias-resolution-error-title": "Chyba konfigurace aliasů dashboardu", - "invalid-aliases-config": "Nebylo možné nalézt žádná zařízení odpovídající některému z aliasů ve filtru.
    Pro vyřešení tohoto problému prosím kontaktujte vašeho administrátora.", - "select-devices": "Vybrat zařízení", - "assignedToCustomer": "Přiřazeno zákazníkovi", - "assignedToCustomers": "Přiřazeno zákazníkům", - "public": "Veřejné", - "public-link": "Veřejný odkaz", - "copy-public-link": "Kopírovat veřejný odkaz", - "public-link-copied-message": "Veřejný odkaz na dashboard byl zkopírován do schránky", - "manage-states": "Spravovat stavy dashboardu", - "states": "Stavy dashboardu", - "search-states": "Vyhledat stavy dashboardu", - "selected-states": "Vybráno { count, plural, 1 {1 stavů dashboardu} other {# stavů dashboardu} }", - "edit-state": "Editovat stav dashboardu", - "delete-state": "Smazat stav dashboardu", - "add-state": "Přidat stav dashboardu", - "state": "Stav dashboardu", - "state-name": "Název", - "state-name-required": "Název stavu dashboardu je povinný.", - "state-id": "Id stavu", - "state-id-required": "Id stavu dashboardu je povinné.", - "state-id-exists": "Stav dashboardu s identickým Id již existuje.", - "is-root-state": "Základní stav", - "delete-state-title": "Smazat stav dashboardu", - "delete-state-text": "Jste si jisti, že chcete odstranit stav dashboardu s názvem '{{stateName}}'?", - "show-details": "Zobrazit detaily", - "hide-details": "Skrýt detaily", - "select-state": "Vybrat cílový stav", - "state-controller": "Kontrolér stavu" - }, - "datakey": { - "settings": "Nastavení", - "advanced": "Rozšířené", - "label": "Název", - "color": "Barva", - "units": "Speciální symbol, který bude zobrazen vedle hodnoty", - "decimals": "Počet číslic za desetinnou čárkou", - "data-generation-func": "Funkce pro generování dat", - "use-data-post-processing-func": "Použít funkci pro následné zpracování", - "configuration": "Konfigurace datového klíče", - "timeseries": "Časové řady", - "attributes": "Atributy", - "alarm": "Pole alarmu", - "timeseries-required": "Časové řady entity jsou povinné.", - "timeseries-or-attributes-required": "Časové řady / atributy entity jsou povinné.", - "maximum-timeseries-or-attributes": "Maximálně { count, plural, 1 {1 časová řada/atribut je povolena.} other {# časových řad/atributů je povoleno} }", - "alarm-fields-required": "Pole alarmu jsou povinná.", - "function-types": "Typy funkcí", - "function-types-required": "Typy funkcí jsou povinné.", - "maximum-function-types": "Maximálně { count, plural, 1 {1 typ funkce je povolen.} other {# typů funkce je povoleno} }", - "time-description": "Časová značka aktuální hodnoty;", - "value-description": "Aktuální hodnota;", - "prev-value-description": "Výsledek předchozího volání funkce;", - "time-prev-description": "Časová značka předchozí hodnoty;", - "prev-orig-value-description": "Původní předchozí hodnota;" - }, - "datasource": { - "type": "Typ datového zdroje", - "name": "Název", - "add-datasource-prompt": "Přidejte prosím datový zdroj" - }, - "details": { - "edit-mode": "Režim editace", - "toggle-edit-mode": "Přepnout do režimu editace" - }, - "device": { - "device": "Zařízení", - "device-required": "Zařízení je povinné.", - "devices": "Zařízení", - "management": "Správa zařízení", - "view-devices": "Zobrazit zařízení", - "device-alias": "Alias zařízení", - "aliases": "Aliasy zařízení", - "no-alias-matching": "'{{alias}}' nenalezen.", - "no-aliases-found": "Žádné aliasy nebyly nalezeny.", - "no-key-matching": "'{{key}}' nenalezen.", - "no-keys-found": "Žádné klíče nenalezeny.", - "create-new-alias": "Vytvořit nový!", - "create-new-key": "Vytvořit nový!", - "duplicate-alias-error": "Byl nalezen duplicitní alias '{{alias}}'.
    Aliasy zařízení musí být v rámci dashboardu unikátní.", - "configure-alias": "Konfigurovat '{{alias}}' alias", - "no-devices-matching": "Žádná zařízení odpovídající '{{entity}}' nebyla nalezena.", - "alias": "Alias", - "alias-required": "Alias zařízení je povinný.", - "remove-alias": "Odebrat alias zařízení", - "add-alias": "Přidat alias zařízení", - "name-starts-with": "Název zařízení začíná", - "device-list": "Seznam zařízení", - "use-device-name-filter": "Použít filtr", - "device-list-empty": "Nebyla vybrána žádná zařízení.", - "device-name-filter-required": "Název filtru zařízení je povinný.", - "device-name-filter-no-device-matched": "Žádná zařízení začínající '{{device}}' nebyla nalezena.", - "add": "Přidat zařízení", - "assign-to-customer": "Přiřadit zákazníkovi", - "assign-device-to-customer": "Přiřadit zařízení zákazníkovi", - "assign-device-to-customer-text": "Vyberte prosím zařízení, která mají být přiřazena zákazníkovi", - "make-public": "Zveřejnit zařízení", - "make-private": "Učinit zařízení neveřejným", - "no-devices-text": "Žádná zařízení nebyla nalezena", - "assign-to-customer-text": "Vyberte prosím zákazníka, který má být přiřazen zařízení(m)", - "device-details": "Detail zařízení", - "add-device-text": "Přidat nové zařízení", - "credentials": "Přístupové údaje", - "manage-credentials": "Spravovat přístupové údaje", - "delete": "Smazat zařízení", - "assign-devices": "Přiřadit zařízení", - "assign-devices-text": "Přiřadit { count, plural, 1 {1 zařízení} other {# zařízení} } zákazníkovi", - "delete-devices": "Smazat zařízení", - "unassign-from-customer": "Odebrat zákazníkovi", - "unassign-devices": "Odebrat zařízení", - "unassign-devices-action-title": "Odebrat { count, plural, 1 {1 zařízení} other {# zařízení} } zákazníkovi", - "assign-new-device": "Přiřadit nové zařízení", - "make-public-device-title": "Jste si jisti, že chcete zařízení '{{deviceName}}' zveřejnit?", - "make-public-device-text": "Po potvrzení bude zařízení a všechna jeho data veřejná a dostupná pro ostatní.", - "make-private-device-title": "Jste si jisti, že chcete zařízení '{{deviceName}}' učinit neveřejným?", - "make-private-device-text": "Po potvrzení budou zařízení a všechna jeho data neveřejné a nedostupné pro ostatní.", - "view-credentials": "Zobrazit přístupové údaje", - "delete-device-title": "Jste si jisti, že chcete smazat zařízení '{{deviceName}}'?", - "delete-device-text": "Buďte opatrní, protože po potvrzení nebude možné zařízení ani žádná související data obnovit.", - "delete-devices-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 zařízení} other {# zařízení} }?", - "delete-devices-action-title": "Smazat { count, plural, 1 {1 zařízení} other {# zařízení} }", - "delete-devices-text": "Buďte opatrní, protože po potvrzení budou vybraná zařízení odstraněna a žádná související data nebude možné obnovit.", - "unassign-device-title": "Jste si jisti, že chcete odebrat zařízení '{{deviceName}}'?", - "unassign-device-text": "Po potvrzení bude zařízení odebráno a nebude pro zákazníka dostupné.", - "unassign-device": "Odebrat zařízení", - "unassign-devices-title": "Jste si jisti, že chcete odebrat { count, plural, 1 {1 zařízení} other {# zařízení} }?", - "unassign-devices-text": "Po potvrzení budou všechna vybraná zařízení odebrána a nebudou pro zákazníka dostupná.", - "device-credentials": "Přístupové údaje zařízení", - "credentials-type": "Typ přístupových údajů", - "access-token": "Přístupový token", - "access-token-required": "Přístupový token je povinný.", - "access-token-invalid": "Délka přístupového tokenu musí být od 1 do 20 znaků.", - "rsa-key": "RSA veřejný klíč", - "rsa-key-required": "RSA veřejný klíč je povinný.", - "secret": "Heslo", - "secret-required": "Heslo je povinné.", - "device-type": "Typ zařízení", - "device-type-required": "Typ zařízení je povinný.", - "select-device-type": "Vybrat typ zařízení", - "enter-device-type": "Zadejte typ zařízení", - "any-device": "Všechna zařízení", - "no-device-types-matching": "Žádné typy zařízení odpovídající '{{entitySubtype}}' nebyly nalezeny.", - "device-type-list-empty": "Nebyl vybrán typ zařízení.", - "device-types": "Typy zařízení", - "name": "Název", - "name-required": "Název je povinný.", - "description": "Popis", - "events": "Události", - "details": "Detail", - "copyId": "Kopírovat Id zařízení", - "copyAccessToken": "Kopírovat přístupový token", - "idCopiedMessage": "Id zařízení bylo zkopírováno do schránky", - "accessTokenCopiedMessage": "Přístupový token zařízení byl zkopírován do schránky", - "assignedToCustomer": "Přiřazeno zákazníkovi", - "unable-delete-device-alias-title": "Nebylo možné smazat alias zařízení", - "unable-delete-device-alias-text": "Alias zařízení '{{deviceAlias}}' nelze smazat, protože je používán následujícími widgety:
    {{widgetsList}}", - "is-gateway": "Je bránou", - "public": "Veřejné", - "device-public": "Zařízení je veřejné", - "select-device": "vybrat zařízení" - }, - "dialog": { - "close": "Zavřít dialog" - }, - "direction": { - "column": "Sloupec", - "row": "Řádek" - }, - "error": { - "unable-to-connect": "Nebylo možné se připojit k serveru! Zkontrolujte internetové připojení.", - "unhandled-error-code": "Neošetřený chybový kód: {{errorCode}}", - "unknown-error": "Neznámá chyba" - }, - "entity": { - "entity": "Entita", - "entities": "Entity", - "aliases": "Entitní aliasy", - "entity-alias": "Alias entity", - "unable-delete-entity-alias-title": "Alias entity nebylo možné smazat", - "unable-delete-entity-alias-text": "Alias entity '{{entityAlias}}' nelze smazat, protože je používán následujícími widgety:
    {{widgetsList}}", - "duplicate-alias-error": "Nalezen dupliticní alias '{{alias}}'.
    Aliasy entit musí být v rámci dashboardu unikátní.", - "missing-entity-filter-error": "Ve filtru chybí alias '{{alias}}'.", - "configure-alias": "Konfigurovat '{{alias}}' alias", - "alias": "Alias", - "alias-required": "Alias entity je povinný", - "remove-alias": "Odebrat alias entity", - "add-alias": "Přidat alias entity", - "entity-list": "Seznam entit", - "entity-type": "Typ entity", - "entity-types": "Typy entit", - "entity-type-list": "Seznam typů entit", - "any-entity": "Všechny entity", - "enter-entity-type": "Zadat typ entity", - "no-entities-matching": "Žádné entity odpovídající '{{entity}}' nebyly nalezeny.", - "no-entity-types-matching": "Žádné entity odpovídající '{{entityType}}' nebyly nalezeny.", - "name-starts-with": "Název začíná", - "use-entity-name-filter": "Použít filtr", - "entity-list-empty": "Žádné entity nebyly nalezeny.", - "entity-type-list-empty": "Nebyl vybrán žádný typ entity.", - "entity-name-filter-required": "Filtr názvu entity je povinný.", - "entity-name-filter-no-entity-matched": "Žádné entity začínající '{{entity}}' nebyly nalezeny.", - "all-subtypes": "Vše", - "select-entities": "Vybrat entity", - "no-aliases-found": "Žádné aliasy nebyly nalezeny.", - "no-alias-matching": "'{{alias}}' nebyl nalezen.", - "create-new-alias": "Vytvořit nový!", - "key": "Klíč", - "key-name": "Název klíče", - "no-keys-found": "Nebyly nalezeny žádné klíče.", - "no-key-matching": "'{{key}}' nebyl nalezen.", - "create-new-key": "Vytvořit nový!", - "type": "Typ", - "type-required": "Typ entity je povinný.", - "type-device": "Zařízení", - "type-devices": "Zařízení", - "list-of-devices": "{ count, plural, 1 {Jedno zařízení} other {Seznam # zařízení} }", - "device-name-starts-with": "Zařízení, jejichž název začíná '{{prefix}}'", - "type-asset": "Aktivum", - "type-assets": "Aktiva", - "list-of-assets": "{ count, plural, 1 {Jedno aktivum} other {Seznam # aktiv} }", - "asset-name-starts-with": "Aktiva, jejichž název začíná '{{prefix}}'", - "type-entity-view": "Entitní pohled", - "type-entity-views": "Entitní pohledy", - "list-of-entity-views": "{ count, plural, 1 {Jeden entitní pohled} other {Seznam # entitních pohledů} }", - "entity-view-name-starts-with": "Entitní pohledy, jejichž název začíná '{{prefix}}'", - "type-rule": "Pravidlo", - "type-rules": "Pravidla", - "list-of-rules": "{ count, plural, 1 {Jedno pravidlo} other {Seznam # pravidel} }", - "rule-name-starts-with": "Pravidla, jejichž název začíná '{{prefix}}'", - "type-plugin": "Zásuvný modul", - "type-plugins": "Zásuvné moduly", - "list-of-plugins": "{ count, plural, 1 {Jeden zásuvný modul} other {Seznam # zásuvných modulů} }", - "plugin-name-starts-with": "Závusné moduly, jejichž název začíná '{{prefix}}'", - "type-tenant": "Tenant", - "type-tenants": "Tenanti", - "list-of-tenants": "{ count, plural, 1 {Jeden tenant} other {Seznam # tenantů} }", - "tenant-name-starts-with": "Tenanti, jejichž název začíná '{{prefix}}'", - "type-customer": "Zákazník", - "type-customers": "Zákazníci", - "list-of-customers": "{ count, plural, 1 {Jeden zákazník} other {Seznam # zákazníků} }", - "customer-name-starts-with": "Zákazníci, jejichž název začíná '{{prefix}}'", - "type-user": "Uživatel", - "type-users": "Uživatelé", - "list-of-users": "{ count, plural, 1 {Jeden uživatel} other {Seznam # uživatelů} }", - "user-name-starts-with": "Uživatelé, jejichž název začíná '{{prefix}}'", - "type-dashboard": "Dashboard", - "type-dashboards": "Dashboardy", - "list-of-dashboards": "{ count, plural, 1 {Jeden dashboard} other {Seznam # dashboardů} }", - "dashboard-name-starts-with": "Dashboardy, jejichž název začíná '{{prefix}}'", - "type-alarm": "Alarm", - "type-alarms": "Alarmy", - "list-of-alarms": "{ count, plural, 1 {Jeden alarm} other {Seznam # alarmů} }", - "alarm-name-starts-with": "Alarmy, jejichž název začíná '{{prefix}}'", - "type-rulechain": "Řetěz pravidel", - "type-rulechains": "Řetězy pravidel", - "list-of-rulechains": "{ count, plural, 1 {Jeden řetěz pravidel} other {Seznam # řetězů pravidel} }", - "rulechain-name-starts-with": "Řetězy pravidel, jejichž název začíná '{{prefix}}'", - "type-rulenode": "Uzel pravidla", - "type-rulenodes": "Uzly pravidel", - "list-of-rulenodes": "{ count, plural, 1 {Jeden uzel pravidla} other {Seznam # uzlů pravidel} }", - "rulenode-name-starts-with": "Uzly pravidel, jejichž název začíná '{{prefix}}'", - "type-current-customer": "Stávající zákazník", - "type-current-tenant": "Stávající tenant", - "search": "Vyhledat entity", - "selected-entities": "{ count, plural, 1 {1 entita} other {# entit} } zvoleno", - "entity-name": "Název entity", - "details": "Detail entity", - "no-entities-prompt": "Nebyly nalezeny žádné entity", - "no-data": "Nelze zobrazit žádná data", - "columns-to-display": "Zobrazit sloupce" - }, - "entity-view": { - "entity-view": "Entitní pohled", - "entity-view-required": "Entitní pohled je povinný.", - "entity-views": "Entitní pohledy", - "management": "Správa entitních pohledů", - "view-entity-views": "Zobrazit entitní pohledy", - "entity-view-alias": "Alias entitního pohledu", - "aliases": "Aliasy entitního pohledu", - "no-alias-matching": "'{{alias}}' nenalezen.", - "no-aliases-found": "Žádné aliasy nebyly nalezeny.", - "no-key-matching": "'{{key}}' nenalezen.", - "no-keys-found": "Nebyly nalezeny žádné klíče.", - "create-new-alias": "Vytvořit nový!", - "create-new-key": "Vytvořit nový!", - "duplicate-alias-error": "Byl nalezen duplicitní alias '{{alias}}'.
    Aliasy entitních pohledů musí být v rámci dashboardu unikátní.", - "configure-alias": "Konfigurovat '{{alias}}' alias", - "no-entity-views-matching": "Žádné entitní pohledy odpovídající '{{entity}}' nebyly nalezeny.", - "alias": "Alias", - "alias-required": "Alias entitního pohledu je povinný.", - "remove-alias": "Odebrat alias entitního pohledu", - "add-alias": "Přidat alias entitního pohledu", - "name-starts-with": "Název entitního pohledu začíná", - "entity-view-list": "Seznam entitních pohledů", - "use-entity-view-name-filter": "Použít filtr", - "entity-view-list-empty": "Nebyly vybrány žádné entitní pohledy.", - "entity-view-name-filter-required": "Filtr názvu entitního pohledu je povinný.", - "entity-view-name-filter-no-entity-view-matched": "Žádné entitní pohledy začínající '{{entityView}}' nebyly nalezeny.", - "add": "Přidat entitní pohled", - "assign-to-customer": "Přiřadit zákazníkovi", - "assign-entity-view-to-customer": "Přiřadit entitní pohled(y) zákazníkovi", - "assign-entity-view-to-customer-text": "Vyberte prosím entitní pohledy, které mají být přiřazeny zákazníkovi", - "no-entity-views-text": "Žádné entitní pohledy nebyly nalezeny", - "assign-to-customer-text": "Vyberte prosím zákazníka, kterému má být entitní pohled(y) přiřazen(y)", - "entity-view-details": "Detail entitního pohledu", - "add-entity-view-text": "Přidat nový entitní pohled", - "delete": "Smazat entitní pohled", - "assign-entity-views": "Přiřadit entitní pohledy", - "assign-entity-views-text": "Přiřadit { count, plural, 1 {1 entitní pohled} other {# entitních pohledů} } zákazníkovi", - "delete-entity-views": "Smazat entitní pohledy", - "unassign-from-customer": "Odebrat zákazníkovi", - "unassign-entity-views": "Odebrat entitní pohledy", - "unassign-entity-views-action-title": "Odebrat { count, plural, 1 {1 entitní pohled} other {# entitních pohledů} } zákazníkovi", - "assign-new-entity-view": "Přiřadit nový entitní pohled", - "delete-entity-view-title": "Jste si jisti, že chcete smazat entitní pohled '{{entityViewName}}'?", - "delete-entity-view-text": "Buďte opatrní, protože po potvrzení nebude možné entitní pohled ani žádná související data obnovit.", - "delete-entity-views-title": "Jste si jisti, že chcete odstranit entitní pohled { count, plural, 1 {1 entitní pohled} other {# entitních pohledů} }?", - "delete-entity-views-action-title": "Smazat { count, plural, 1 {1 entitní pohled} other {# entitních pohledů} }", - "delete-entity-views-text": "Buďte opatrní, protože po potvrzení budou všechny vybrané entitní pohledy smazány a žádná související data nebude možné obnovit.", - "unassign-entity-view-title": "Jste si jisti, že chcete odebrat entitní pohled '{{entityViewName}}'?", - "unassign-entity-view-text": "Po potvrzení bude entitní pohled odebrán a nebude pro zákazníka dostupný.", - "unassign-entity-view": "Odebrat entitní pohled", - "unassign-entity-views-title": "Jste si jisti, že chcete odebrat { count, plural, 1 {1 entitní pohled} other {# entitních pohledů} }?", - "unassign-entity-views-text": "Po potvrzení budou všechny vybrané entitní pohledy odebrány a nebudou pro zákazníka dostupné.", - "entity-view-type": "Typ entitního pohledu", - "entity-view-type-required": "Typ entitního pohledu je povinný.", - "select-entity-view-type": "Vybrat typ entitního pohledu", - "enter-entity-view-type": "Zadat typ entitního pohledu", - "any-entity-view": "Všechny entitní pohledy", - "no-entity-view-types-matching": "Žádné typy entitních pohledů odpovídající '{{entitySubtype}}' nebyly nalezeny.", - "entity-view-type-list-empty": "Žádné typy entitních pohledů nebyly nalezeny.", - "entity-view-types": "Typy entitních pohledů", - "name": "Název", - "name-required": "Název je povinný.", - "description": "Popis", - "events": "Události", - "details": "Detail", - "copyId": "Kopírovat Id entitního pohledu", - "assignedToCustomer": "Přiřazeno zákazníkovi", - "unable-entity-view-device-alias-title": "Alias entitního typu nebylo možné smazat", - "unable-entity-view-device-alias-text": "Alias zařízení '{{entityViewAlias}}' nelze smazat, protože je používán následujícími widgety:
    {{widgetsList}}", - "select-entity-view": "Vybrat entitní pohled", - "make-public": "Zveřejnit entitní pohled", - "make-private": "Učinit entitní pohled neveřejným", - "start-date": "Datum zahájení", - "start-ts": "Čas zahájení", - "end-date": "Datum ukončení", - "end-ts": "Čas ukončení", - "date-limits": "Omezení data", - "client-attributes": "Klientské atributy", - "shared-attributes": "Sdílené atributy", - "server-attributes": "Serverové atributy", - "timeseries": "Časové řady", - "client-attributes-placeholder": "Klientské atributy", - "shared-attributes-placeholder": "Sdílené atributy", - "server-attributes-placeholder": "Serverové atributy", - "timeseries-placeholder": "Časové řady", - "target-entity": "Cílová entita", - "attributes-propagation": "Propagace atributů", - "attributes-propagation-hint": "Entitní pohled bude automaticky kopírovat specifikované atributy z cílové entity vždy, když uložíte nebo aktualizujete tento entitní pohled. Z výkonnostních důvodů nejsou atributy cílové entity propagovány při každé změně atributu. Automatickou propagaci můžete povolit konfigurací \"copy to view\" uzlu pravidla v rámci vašeho řetězu pravidel a provázáním \"Post attributes\" a \"Attributes Updated\" zpráv na nový uzel pravidla.", - "timeseries-data": "Data časových řad", - "timeseries-data-hint": "Nakonfigurujte klíče dat časových řad cílové entity, která budou dostupná pro entitní pohled. Tato data časových řad jsou pouze pro čtení.", - "make-public-entity-view-title": "Jste si jisti, že chcete entitní pohled '{{entityViewName}}' zveřejnit?", - "make-public-entity-view-text": "Po potvrzení bude entitní pohled a všechna jeho data veřejné a dostupné pro ostatní.", - "make-private-entity-view-title": "Jste si jisti, že chcete entitní pohled '{{entityViewName}}' učinit neveřejným?", - "make-private-entity-view-text": "Po potvrzení bude entitní pohled a všechna jeho data neveřejné a nebudou dostupné pro ostatní." - }, - "event": { - "event-type": "Typ události", - "type-error": "Chyba", - "type-lc-event": "Událost životního cyklu", - "type-stats": "Statistika", - "type-debug-rule-node": "Ladění", - "type-debug-rule-chain": "Ladění", - "no-events-prompt": "Nebyly nalezeny žádné události", - "error": "Chyba", - "alarm": "Alarm", - "event-time": "Čas události", - "server": "Server", - "body": "Tělo", - "method": "Metoda", - "type": "Typ", - "entity": "Entita", - "message-id": "Id zprávy", - "message-type": "Typ zprávy", - "data-type": "Typ dat", - "relation-type": "Typ vztahu", - "metadata": "Metadata", - "data": "Data", - "event": "Událost", - "status": "Stav", - "success": "Úspěch", - "failed": "Neúspěch", - "messages-processed": "Zpracované zprávy", - "errors-occurred": "Vyskytly se chyby" - }, - "extension": { - "extensions": "Rozšíření", - "selected-extensions": "Vybráno { count, plural, 1 {1 rozšíření} other {# rozšíření} }", - "type": "Typ", - "key": "Klíč", - "value": "Hodnota", - "id": "Id", - "extension-id": "Id rozšíření", - "extension-type": "Typ rozšíření", - "transformer-json": "JSON *", - "unique-id-required": "Id stávajícího rozšíření již existuje.", - "delete": "Smazat rozšíření", - "add": "Přidat rozšíření", - "edit": "Editovat rozšíření", - "delete-extension-title": "Jste si jisti, že chcete smazat rozšíření '{{extensionId}}'?", - "delete-extension-text": "Buďte opatrní, protože po potvrzení nebude možné rozšíření ani související data obnovit.", - "delete-extensions-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 rozšíření} other {# rozšíření} }?", - "delete-extensions-text": "Buďte opatrní, protože po potvrzení budou všechna vybraná rozšíření odstraněna.", - "converters": "Převodník", - "converter-id": "Id převodníku", - "configuration": "Konfigurace", - "converter-configurations": "Konfigurace převodníku", - "token": "Bezpečnostní token", - "add-converter": "Přidat převodník", - "add-config": "Přidat konfiguraci převodníku", - "device-name-expression": "Výraz názvu zařízení", - "device-type-expression": "Výraz typu zařízení", - "custom": "Vlastní", - "to-double": "Zdvojnásobit", - "transformer": "Transformátor", - "json-required": "JSON transformátoru je povinný.", - "json-parse": "Nebylo možné parsovat JSON transformátoru.", - "attributes": "Atributy", - "add-attribute": "Přidat atributy", - "add-map": "Přidat mapovací prvek", - "timeseries": "Časové řady", - "add-timeseries": "Přidat časové řady", - "field-required": "Pole je povinné", - "brokers": "Message brokers", - "add-broker": "Přidat message brokera", - "host": "Host", - "port": "Port", - "port-range": "Port by měl být v rozsahu mezi 1 a 65535.", - "ssl": "Ssl", - "credentials": "Přístupové údaje", - "username": "Uživatelské jméno", - "password": "Heslo", - "retry-interval": "Interval pro další pokus v milisekundách", - "anonymous": "Anonymní", - "basic": "Základní", - "pem": "PEM", - "ca-cert": "soubor CA certifikátu *", - "private-key": "Soubor privátního klíče *", - "cert": "Soubor certifikátu *", - "no-file": "Žádný soubor nebyl vybrán.", - "drop-file": "Přetáhněte sem soubor nebo klikněte pro výběr souboru pro nahrání.", - "mapping": "Mapování", - "topic-filter": "Filtr MQ fronty", - "converter-type": "Typ převodníku", - "converter-json": "JSON", - "json-name-expression": "JSON výraz názvu zařízení", - "topic-name-expression": "Výraz názvu MQ fronty", - "json-type-expression": "JSON Výraz typu zařízení", - "topic-type-expression": "Výraz MQ fronty typu zařízení", - "attribute-key-expression": "Výraz klíče atributu", - "attr-json-key-expression": "JSON výraz klíče atributu", - "attr-topic-key-expression": "Výraz MQ fronty klíče atributu", - "request-id-expression": "Výraz Id požadavku", - "request-id-json-expression": "JSON výraz Id požadavku", - "request-id-topic-expression": "Výraz MQ fronty ID požadavku", - "response-topic-expression": "Výraz fronty odpovědi", - "value-expression": "Výraz hodnoty", - "topic": " MQ fronta", - "timeout": "Timeout v milisekundách", - "converter-json-required": "JSON převodníku je povinný.", - "converter-json-parse": "JSON převodníku nebylo možné parsovat.", - "filter-expression": "Výraz filtru", - "connect-requests": "Požadavky na spojení", - "add-connect-request": "Přidat požadavek na spojení", - "disconnect-requests": "Požadavky na odpojení", - "add-disconnect-request": "Přidat požadavek na odpojení", - "attribute-requests": "Požadavky na atribut", - "add-attribute-request": "Přidat požadavek na atribut", - "attribute-updates": "Aktualizace atributu", - "add-attribute-update": "Přidat aktualizaci atributu", - "server-side-rpc": "RPC na straně serveru", - "add-server-side-rpc-request": "Přidat požadavek na RPC na straně serveru", - "device-name-filter": "Filtr názvu zařízení", - "attribute-filter": "Filtr atributu", - "method-filter": "Filtr metody", - "request-topic-expression": "Výraz požadavku na MQ frontu", - "response-timeout": "Timeout odpovědi v milisekundách", - "topic-expression": "Výraz MQ fronty", - "client-scope": "Scope klienta", - "add-device": "Přidat zařízení", - "opc-server": "Servery", - "opc-add-server": "Přidat server", - "opc-add-server-prompt": "Prosím přidejte server", - "opc-application-name": "Název aplikace", - "opc-application-uri": "URI aplikace", - "opc-scan-period-in-seconds": "Interval skenování ve vteřinách", - "opc-security": "Bezpečnost", - "opc-identity": "Identita", - "opc-keystore": "Úložiště klíčů", - "opc-type": "Typ", - "opc-keystore-type": "Typ", - "opc-keystore-location": "Umístění *", - "opc-keystore-password": "Heslo", - "opc-keystore-alias": "Alias", - "opc-keystore-key-password": "Heslo klíče", - "opc-device-node-pattern": "Vzor uzlu zařízení", - "opc-device-name-pattern": "Vzor názvu zařízení", - "modbus-server": "Servery/slaves", - "modbus-add-server": "Přidat server/slave", - "modbus-add-server-prompt": "Prosím přidejte server/slave", - "modbus-transport": "Transport", - "modbus-tcp-reconnect": "Automaticky znovu připojit", - "modbus-rtu-over-tcp": "RTU přes TCP", - "modbus-port-name": "Název sériového portu", - "modbus-encoding": "Šifrování", - "modbus-parity": "Parita", - "modbus-baudrate": "Baud rate", - "modbus-databits": "Data bits", - "modbus-stopbits": "Stop bits", - "modbus-databits-range": "Data bits by měly být v rozsahu od 7 do 8.", - "modbus-stopbits-range": "Stop bits by měly být v rozsahu od 1 do 2.", - "modbus-unit-id": "ID jednotky", - "modbus-unit-id-range": "ID jednotky by mělo být v rozsahu od 1 do 247.", - "modbus-device-name": "Název zařízení", - "modbus-poll-period": "Interval kontroly (ms)", - "modbus-attributes-poll-period": "Interval kontroly atributů (ms)", - "modbus-timeseries-poll-period": "Interval kontroly časových řad (ms)", - "modbus-poll-period-range": "Interval kontroly by měl mít kladnou hodnotu.", - "modbus-tag": "Štítek", - "modbus-function": "Funkce", - "modbus-register-address": "Adresa registrace", - "modbus-register-address-range": "Adresa registrace by měla být v rozsahu od 0 do 65535.", - "modbus-register-bit-index": "Bit index", - "modbus-register-bit-index-range": "Bit index by měl být v rozsahu od 0 do 15.", - "modbus-register-count": "Počet registrací", - "modbus-register-count-range": "Počet registrací by měl mít kladnou hodnotu.", - "modbus-byte-order": "Byte order", - "sync": { - "status": "Stav", - "sync": "Synchronizováno", - "not-sync": "Nesynchronizováno", - "last-sync-time": "Čas poslední synchronizace", - "not-available": "Nedostupné" - }, - "export-extensions-configuration": "Exportovat konfiguraci rozšíření", - "import-extensions-configuration": "Importovat konfiguraci rozšíření", - "import-extensions": "Importovat rozšíření", - "import-extension": "Importovat rozšíření", - "export-extension": "Exportovat rozšíření", - "file": "Soubor rozšíření", - "invalid-file-error": "Neplatný soubor rozšíření" - }, - "fullscreen": { - "expand": "Rozšířit do režimu celé obrazovky", - "exit": "Ukončit režim celé obrazovky", - "toggle": "Přepnout do režimu celé obrazovky", - "fullscreen": "Celá obrazovka" - }, - "function": { - "function": "Funkce" - }, - "grid": { - "delete-item-title": "Jste si jisti, že chcete smazat tuto položku?", - "delete-item-text": "Buďte opatrní, protože po potvrzení nebude možné tuto položku ani žádná související data obnovit.", - "delete-items-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 položku} other {# položek} }?", - "delete-items-action-title": "Smazat { count, plural, 1 {1 položku} other {# položek} }", - "delete-items-text": "Buďte opatrní, protože po potvrzení budou všechny vybrané položky odstraněny a žádná související data nebude možné obnovit.", - "add-item-text": "Přidat novou položku", - "no-items-text": "Žádné položky nebyly nalezeny", - "item-details": "Detail položky", - "delete-item": "Smazat položku", - "delete-items": "Smazat položky", - "scroll-to-top": "Nahoru" - }, - "help": { - "goto-help-page": "Jít na stránku nápovědy" - }, - "home": { - "home": "Domů", - "profile": "Profil", - "logout": "Odhlásit", - "menu": "Menu", - "avatar": "Avatar", - "open-user-menu": "Otevřít uživatelské menu" - }, - "import": { - "no-file": "Nebyl vybrán žádný soubor", - "drop-file": "Přetáhněte sem JSON soubor nebo klikněte pro výběr souboru pro nahrání." - }, - "item": { - "selected": "Vybráno" - }, - "js-func": { - "no-return-error": "Funkce musí vrátit hodnotu!", - "return-type-mismatch": "Funkce musí vrátit hodnotu '{{type}}' typu!", - "tidy": "Tidy" - }, - "key-val": { - "key": "Klíč", - "value": "Hodnota", - "remove-entry": "Odstranit záznam", - "add-entry": "Přidat záznam", - "no-data": "Žádné záznamy" - }, - "layout": { - "layout": "Rozmístění", - "manage": "Spravovat rozmístění", - "settings": "Nastavení rozmístění", - "color": "Barva", - "main": "Hlavní", - "right": "Vpravo", - "select": "Vybrat cílové rozmístění" - }, - "legend": { - "direction": "Směr legendy", - "position": "Pozice legendy", - "show-max": "Zobrazit max hodnotu", - "show-min": "Zobrazit min hodnotu", - "show-avg": "Zobrazit průměrnou hodnotu", - "show-total": "Zobrazit celkovou hodnotu", - "settings": "Nastavení legendy", - "min": "min", - "max": "max", - "avg": "průměr", - "total": "celkem" - }, - "login": { - "login": "Přihlásit se", - "request-password-reset": "Vyžádat reset hesla", - "reset-password": "Reset hesla", - "create-password": "Vytvořit heslo", - "passwords-mismatch-error": "Zadaná hesla se musí shodovat!", - "password-again": "Heslo znovu", - "sign-in": "Prosím zaregistrujte se", - "username": "Uživatelské jméno (email)", - "remember-me": "Zapamatovat si mě", - "forgot-password": "Zapomněli jste heslo?", - "password-reset": "Reset hesla", - "new-password": "Nové heslo", - "new-password-again": "Nové heslo znovu", - "password-link-sent-message": "Odkaz pro reset hesla byl úspěšně odeslán!", - "email": "Email", - "login-with": "Přihlásit se přes {{name}}", - "or": "nebo" - }, - "position": { - "top": "Nahoře", - "bottom": "Dole", - "left": "Vlevo", - "right": "Vpravo" - }, - "profile": { - "profile": "Profil", - "change-password": "Změnit heslo", - "current-password": "Stávající heslo" - }, - "relation": { - "relations": "Vztahy", - "direction": "Směr", - "search-direction": { - "FROM": "Od", - "TO": "K" - }, - "direction-type": { - "FROM": "od", - "TO": "k" - }, - "from-relations": "Odchozí vztahy", - "to-relations": "Příchozí vztahy", - "selected-relations": "Vybráno { count, plural, 1 {1 vztahů} other {# vztahů} }", - "type": "Typ", - "to-entity-type": "K typ entity", - "to-entity-name": "K název entity", - "from-entity-type": "Z typ entity", - "from-entity-name": "Z název entity", - "to-entity": "K entitě", - "from-entity": "Od entity", - "delete": "Smazat vztah", - "relation-type": "Typ vztahu", - "relation-type-required": "Typ vztahu je povinný.", - "any-relation-type": "Všechny typy", - "add": "Přidat vztah", - "edit": "Editovat vztah", - "delete-to-relation-title": "Jste si jisti, že chcete smazat vztah k entitě '{{entityName}}'?", - "delete-to-relation-text": "Buďte opatrní, protože po potvrzení bude vtah entity '{{entityName}}' k aktuální entitě zrušen.", - "delete-to-relations-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 vztah} other {# vztahů} }?", - "delete-to-relations-text": "Buďte opatrní, protože po potvrzení budou všechny vybrané vztahy odstraněny a vztah odpovídajících entit k aktuální entitě bude zrušen.", - "delete-from-relation-title": "Jste si jisti, že chcete smazat vztah z entity '{{entityName}}'?", - "delete-from-relation-text": "Buďte opatrní, protože po potvrzení bude zrušen vztah aktuální entity k entitě '{{entityName}}'.", - "delete-from-relations-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 vztah} other {# vztahů} }?", - "delete-from-relations-text": "Buďte opatrní, protože po potvrzení budou všechny vybrané vztahy odstraněny a bude zrušen vztah aktuální entity k odpovídajícím entitám.", - "remove-relation-filter": "Odebrat filtr vztahů", - "add-relation-filter": "Přidat filtr vztahu", - "any-relation": "Všechny vztahy", - "relation-filters": "Filtry vztahů", - "additional-info": "Další info (JSON)", - "invalid-additional-info": "Další informace v JSON nebylo možné parsovat." - }, - "rulechain": { - "rulechain": "Řetěz pravidel", - "rulechains": "Řetězy pravidel", - "root": "Základní", - "delete": "Smazat řetěz pravidel", - "name": "Název", - "name-required": "Název je povinný.", - "description": "Popis", - "add": "Přidat řetěz pravidel", - "set-root": "Učinit řetěz pravidel základním", - "set-root-rulechain-title": "Jste si jisti, že chcete učinit řetěz pravidel '{{ruleChainName}}' základním?", - "set-root-rulechain-text": "Po potvrzení se stane řetěz pravidel základním a bude zajišťovat zpracování všech příchozích transportních zpráv.", - "delete-rulechain-title": "Jste si jisti, že chcete smazat řetěz pravidel '{{ruleChainName}}'?", - "delete-rulechain-text": "Buďte opatrní, protože po potvrzení nebude možné řetěz pravidel ani žádná související data obnovit.", - "delete-rulechains-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 řetěz pravidel} other {# řetězů pravidel} }?", - "delete-rulechains-action-title": "Smazat { count, plural, 1 {1 řetěz pravidel} other {# řetězy pravidel} }", - "delete-rulechains-text": "Buďte opatrní, protože po potvrzení budou všechny vybrané řetězy pravidel odstraněny a žádná související data nebude možné obnovit.", - "add-rulechain-text": "Přidat nový řetěz pravidel", - "no-rulechains-text": "Žádné řetězy pravidel nebyly nalezeny", - "rulechain-details": "Detail řetězu pravidel", - "details": "Detail", - "events": "Události", - "system": "Systém", - "import": "Importovat řetěz pravidel", - "export": "Exportovat řetěz pravidel", - "export-failed-error": "Řetěz pravidel nebylo možné smazat: {{error}}", - "create-new-rulechain": "Vytvořit nový řetěz pravidel", - "rulechain-file": "Soubor řetězu pravidel", - "invalid-rulechain-file-error": "Řetěz pravidel nebylo možné importovat: neplatná datová struktura řetězu pravidel.", - "copyId": "Kopírovat Id řetězu pravidel", - "idCopiedMessage": "Id řetězu pravidel bylo zkopírováno do schránky", - "select-rulechain": "Vybrat řetěz pravidel", - "no-rulechains-matching": "Žádné řetězy pravidel odpovídající '{{entity}}' nebyly nalezeny.", - "rulechain-required": "Řetěz pravidel je povinný", - "management": "Správa pravidel", - "debug-mode": "Režim ladění" - }, - "rulenode": { - "details": "Detail", - "events": "Události", - "search": "Vyhledat uzly", - "open-node-library": "Otevřít knihovnu uzlů", - "add": "Přidat uzel pravidla", - "name": "Název", - "name-required": "Název je povinný.", - "type": "Typ", - "description": "Popis", - "delete": "Smazat uzel pravidla", - "select-all-objects": "Vybrat všechny uzly a spojení", - "deselect-all-objects": "Zrušit výběr všech uzlů a spojení", - "delete-selected-objects": "Smazat vybrané uzly a spojení", - "delete-selected": "Smazat vybrané", - "select-all": "Vybrat vše", - "copy-selected": "Kopírovat vybrané", - "deselect-all": "Zrušit výběr všech", - "rulenode-details": "Detail uzlu pravidla", - "debug-mode": "Režim ladění", - "configuration": "Konfigurace", - "link": "Odkaz", - "link-details": "Detail odkazu uzlu pravidla", - "add-link": "Přidat odkaz", - "link-label": "Název odkazu", - "link-label-required": "Název odkazu je povinný.", - "custom-link-label": "Vlastní název odkazu", - "custom-link-label-required": "Název vlastního odkazu je povinný.", - "link-labels": "Názvy odkazu", - "link-labels-required": "Názvy odkazu jsou povinné.", - "no-link-labels-found": "Žádné názvy odkazů nebyly nalezeny", - "no-link-label-matching": "'{{label}}' nenalezen.", - "create-new-link-label": "Vytvořit nový!", - "type-filter": "Filtr", - "type-filter-details": "Filtruje příchozí zprávy na základě definovaných podmínek", - "type-enrichment": "Obohacení", - "type-enrichment-details": "Přidá doplňující informace do metadat zprávy", - "type-transformation": "Transformace", - "type-transformation-details": "Změní zprávu a metadata", - "type-action": "Akce", - "type-action-details": "Provede speciální akci", - "type-external": "Externí", - "type-external-details": "Interaguje s externím systémem", - "type-rule-chain": "Řetěz pravidel", - "type-rule-chain-details": "Předá příchozí zprávy specifikovanému řetězu pravidel", - "type-input": "Vstup", - "type-input-details": "Logický vstup řetězu pravidel, předává příchozí zprávy dalšímu navazujícímu uzlu pravidla", - "type-unknown": "Neznámý", - "type-unknown-details": "Nevyřešený uzel pravidla", - "directive-is-not-loaded": "Definovaná konfigurační direktiva '{{directiveName}}' není dostupná.", - "ui-resources-load-error": "Nepodařilo se nahrát konfigurační ui zdroje.", - "invalid-target-rulechain": "Není možné interagovat s cílovým řetězem pravidel!", - "test-script-function": "Testovat funkci skriptu", - "message": "Zpráva", - "message-type": "Typ zprávy", - "select-message-type": "Vybrat typ zprávy", - "message-type-required": "Typ zprávy je povinný", - "metadata": "Metadata", - "metadata-required": "Záznam metadat nemůže být prázdný.", - "output": "Výstup", - "test": "Test", - "help": "Nápověda", - "reset-debug-mode": "Resetovat režim ladění na všech uzlech" - }, - "tenant": { - "tenant": "Tenant", - "tenants": "Tenanti", - "management": "Správa tenantů", - "add": "Přidat tenanta", - "admins": "Administrátoři", - "manage-tenant-admins": "Spravovat administrátory tenanta", - "delete": "Smazat tenanta", - "add-tenant-text": "Přidat nového tenanta", - "no-tenants-text": "Žádní tenanti nebyli nalezeni", - "tenant-details": "Detail tenanta", - "delete-tenant-title": "Jste si jisti, že chcete smazat tenanta '{{tenantTitle}}'?", - "delete-tenant-text": "Buďte opatrní, protože po potvrzení nebude možné tenanta ani žádná související data obnovit.", - "delete-tenants-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 tenanta} other {# tenantů} }?", - "delete-tenants-action-title": "Smazat { count, plural, 1 {1 tenanta} other {# tenantů} }", - "delete-tenants-text": "Buďte opatrní, protože po potvrzení budou všichni vybraní tenanti odstranění a žádná související data nebude možné obnovit.", - "title": "Název", - "title-required": "Název je povinný.", - "description": "Popis", - "details": "Detail", - "events": "Události", - "copyId": "Kopírovat Id tenanta", - "idCopiedMessage": "Id tenanta bylo zkopírováno do schránky", - "select-tenant": "Vybrat tenanta", - "no-tenants-matching": "Žádní tenanti odpovídající '{{entity}}' nebyli nalezeni.", - "tenant-required": "Tenant je povinný" - }, - "timeinterval": { - "seconds-interval": "{ seconds, plural, 1 {1 vteřina} other {# vteřin} }", - "minutes-interval": "{ minutes, plural, 1 {1 minuta} other {# minut} }", - "hours-interval": "{ hours, plural, 1 {1 hodina} other {# hodin} }", - "days-interval": "{ days, plural, 1 {1 den} other {# dnů} }", - "days": "Dny", - "hours": "Hodiny", - "minutes": "Minuty", - "seconds": "Vteřiny", - "advanced": "Rozšířené" - }, - "timewindow": { - "days": "{ days, plural, 1 { den } other {# days } }", - "hours": "{ hours, plural, 0 { hodina } 1 {1 hodina } other {# hodin } }", - "minutes": "{ minutes, plural, 0 { minuta } 1 {1 minuta } other {# minut } }", - "seconds": "{ seconds, plural, 0 { vteřina } 1 {1 vteřina } other {# vteřin } }", - "realtime": "V reálném čase", - "history": "Historie", - "last-prefix": "poslední", - "period": "od {{ startTime }} do {{ endTime }}", - "edit": "Editovat časové okno", - "date-range": "Rozsah data", - "last": "Poslední", - "time-period": "Časový interval" - }, - "user": { - "user": "Uživatel", - "users": "Uživatelé", - "customer-users": "Uživatelé zákazníka", - "tenant-admins": "Administrátoři tenanta", - "sys-admin": "Systémový administrátor", - "tenant-admin": "Administrátor tenanta", - "customer": "Zákazník", - "anonymous": "Anonymní", - "add": "Přidat uživatele", - "delete": "Smazat uživatele", - "add-user-text": "Přidat nového uživatele", - "no-users-text": "Žádní uživatelé nebyli nalezeni", - "user-details": "Detail uživatele", - "delete-user-title": "Jste si jisti, že chcete smazat uživatele '{{userEmail}}'?", - "delete-user-text": "Buďte opatrní, protože po potvrzení nebude možné uživatele ani žádná související data obnovit.", - "delete-users-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 uživatele} other {# uživatele} }?", - "delete-users-action-title": "Smazat { count, plural, 1 {1 uživatele} other {# uživatele} }", - "delete-users-text": "Buďte opatrní, protože po potvrzení budou všichni vybraní uživatelé odstraněni a žádná související data nebude možné obnovit.", - "activation-email-sent-message": "Aktivační email byl úspěšně odeslán!", - "resend-activation": "Znovu poslat aktivační email", - "email": "Email", - "email-required": "Email je povinný.", - "invalid-email-format": "Neplatný formát emailu.", - "first-name": "Jméno", - "last-name": "Příjmení", - "description": "Popis", - "default-dashboard": "Defaultní dashboard", - "always-fullscreen": "Zobrazení vždy na celé obrazovce", - "select-user": "Vybrat uživatele", - "no-users-matching": "Žádní uživatelé odpovídající '{{entity}}' nebyli nalezeni.", - "user-required": "Uživatel je povinný", - "activation-method": "Metoda aktivace", - "display-activation-link": "Zobrazit aktivační odkaz", - "send-activation-mail": "Odeslat aktivační email", - "activation-link": "Aktivační odkaz uživatele", - "activation-link-text": "Pro aktivaci uživatele použijte následující aktivační odkaz :", - "copy-activation-link": "Kopírovat aktivační odkaz", - "activation-link-copied-message": "Aktivační odkaz uživatele byl zkopírován do schránky", - "details": "Detail", - "login-as-tenant-admin": "Přihlásit se jako administrátor tenanta", - "login-as-customer-user": "Přihlásit se jako uživatel zákazníka" - }, - "value": { - "type": "Typ hodnoty", - "string": "Řetězec", - "string-value": "Hodnota řetězce", - "integer": "Celé číslo", - "integer-value": "Hodnota celého čísla", - "invalid-integer-value": "Neplatná hodnota celého čísla", - "double": "Číslo s plovoucí řádovou s dvojitou přesností", - "double-value": "Hodnota čísla s plovoucí řádovou řádkou", - "boolean": "Pravdivostní hodnota", - "boolean-value": "Hodnota pravdivostní hodnoty", - "false": "Nepravda", - "true": "Pravda", - "long": "Vysoké celé číslo" - }, - "widget": { - "widget-library": "Knihovna widgetů", - "widget-bundle": "Kategorie widgetů", - "select-widgets-bundle": "Vybrat kategorii widgetů", - "management": "Správa widgetů", - "editor": "Editor widgetů", - "widget-type-not-found": "Problém s nahráním konfigurace widgetu.
    Pravděpodobně byl asociovaný\n typ widgetu odstraněn.", - "widget-type-load-error": "Widget nebyl nahrán z důvodu následujících chyb:", - "remove": "Odebrat widget", - "edit": "Editovat widget", - "remove-widget-title": "Jste si jisti, že chcete odebrat widget '{{widgetTitle}}'?", - "remove-widget-text": "Po potvrzení nebude možné widget ani žádná související data obnovit.", - "timeseries": "Časové řady", - "search-data": "Vyhledat data", - "no-data-found": "Žádná data nebyla nalezena", - "latest-values": "Poslední hodnoty", - "rpc": "Ovládací widget", - "alarm": "Widgety alarmu", - "static": "Statické widgety", - "select-widget-type": "Vybrat typ widgetu", - "missing-widget-title-error": "Název widgetu musí být specifikován!", - "widget-saved": "Widget uložen", - "unable-to-save-widget-error": "Widget nebylo možné uložit! Widget obsahuje chyby!", - "save": "Uložit widget", - "saveAs": "Uložit widget jako", - "save-widget-type-as": "Uložit typ widgetu jako", - "save-widget-type-as-text": "Zadejte prosím název nového widgetu a/nebo vyberte cílovou kategorii widgetů", - "toggle-fullscreen": "Přepnout na celou obrazovku", - "run": "Spustit widget", - "title": "Název widgetu", - "title-required": "Název widgetu je povinný.", - "type": "Typ widgetu", - "resources": "Zdroje", - "resource-url": "JavaScript/CSS URL", - "remove-resource": "Odebrat zdroj", - "add-resource": "Přidat zdroj", - "html": "HTML", - "tidy": "Tidy", - "css": "CSS", - "settings-schema": "Schéma nastavení", - "datakey-settings-schema": "Schéma nastavení datového klíče", - "javascript": "Javascript", - "remove-widget-type-title": "Jste si jisti, že chcete odebrat typ widgetu '{{widgetName}}'?", - "remove-widget-type-text": "Po potvrzení nebude možné typ widgetu ani žádná související data obnovit.", - "remove-widget-type": "Odebrat typ widgetu", - "add-widget-type": "Přidat nový typ widgetu", - "widget-type-load-failed-error": "Nahrání typu widgetu selhalo!", - "widget-template-load-failed-error": "Nahrání šablony widgetu selhalo!", - "add": "Přidat widget", - "undo": "Vrátit změny widgetu", - "export": "Exportovat widget" - }, - "widget-action": { - "header-button": "Tlačítko hlavičky widgetu", - "open-dashboard-state": "Přejít k novému stavu dashboardu", - "update-dashboard-state": "Aktualizovat stávající stav dashboardu", - "open-dashboard": "Přejít k jinému dashboardu", - "custom": "Vlastní akce", - "target-dashboard-state": "Cílový stav dashboardu", - "target-dashboard-state-required": "Cílový stav dashboardu je povinný", - "set-entity-from-widget": "Nastavit entitu z widgetu", - "target-dashboard": "Cílový dashboard", - "open-right-layout": "Otevřít rozmístění dashboardu vpravo (mobilní zobrazení)" - }, - "widgets-bundle": { - "current": "Vybraná kategorie", - "widgets-bundles": "Kategorie widgetů", - "add": "Přidat kategorii widgetů", - "delete": "Smazat kategorii widgetů", - "title": "Název", - "title-required": "Název je povinný.", - "add-widgets-bundle-text": "Přidat novou kategorii widgetů", - "no-widgets-bundles-text": "Žádné kategorie widgetů nebyly nalezeny", - "empty": "Kategorie widgetů je prázdná", - "details": "Detail", - "widgets-bundle-details": "Detail kategorie widgetů", - "delete-widgets-bundle-title": "Jste si jisti, že chcete smazat kategorii widgetů '{{widgetsBundleTitle}}'?", - "delete-widgets-bundle-text": "Buďte opatrní, po potvrzení nebude možné kategorii widgetů ani žádná související data obnovit.", - "delete-widgets-bundles-title": "Jste si jisti, že chcete odstranit { count, plural, 1 {1 kategorii widgetů} other {# kategorií widgetů} }?", - "delete-widgets-bundles-action-title": "Smazat { count, plural, 1 {1 kategorii widgetů} other {# kategorií widgetů} }", - "delete-widgets-bundles-text": "Buďte opatrní, po potvrzení budou všechny vybrané kategorie widgetů odstraněny a žádná související data nebude možné obnovit.", - "no-widgets-bundles-matching": "Žádné kategorie widgetů odpovídající '{{widgetsBundle}}' nebyly nalezeny.", - "widgets-bundle-required": "Kategorie widgetů je povinná.", - "system": "Systém", - "import": "Importovat kategorii widgetů", - "export": "Exportovat kategorii widgetů", - "export-failed-error": "Kategorii widgetů nebylo možné exportovat: {{error}}", - "create-new-widgets-bundle": "Vytvořit novou kategorii widgetů", - "widgets-bundle-file": "Soubor kategorie widgetů", - "invalid-widgets-bundle-file-error": "Kategorii widgetů nebylo možné importovat: Neplatná datová struktura kategorie widgetů." - }, - "widget-config": { - "data": "Data", - "settings": "Nastavení", - "advanced": "Rozšířené", - "title": "Název", - "general-settings": "Obecná nastavení", - "display-title": "Zobrazovaný název", - "drop-shadow": "Stín", - "enable-fullscreen": "Povolit zobrazení na celé obrazovce", - "background-color": "Barva pozadí", - "text-color": "Barva textu", - "padding": "Šířka vnitřního okraje", - "margin": "Okraj", - "widget-style": "Styl widgetu", - "title-style": "Název stylu", - "mobile-mode-settings": "Nastavení mobilního režimu", - "order": "Pořadí", - "height": "Výška", - "units": "Speciální symbol zobrazovaný za hodnotou", - "decimals": "Počet číslic za desetinnou čárkou", - "timewindow": "Časové okno", - "use-dashboard-timewindow": "Použít časové okno dashboardu", - "display-legend": "Zobrazit legendu", - "datasources": "Datové zdroje", - "maximum-datasources": "Maximum { count, plural, 1 {1 datový zdroj je povolen.} other {# datových zdrojů je povoleno} }", - "datasource-type": "Typ", - "datasource-parameters": "Parametry", - "remove-datasource": "Odebrat datový zdroj", - "add-datasource": "Přidat datový zdroj", - "target-device": "Cílové zařízení", - "alarm-source": "Zdroj alarmu", - "actions": "Akce", - "action": "Akce", - "add-action": "Přidat akci", - "search-actions": "Vyhledat akce", - "action-source": "Zdroj akce", - "action-source-required": "Zdroj akce je povinný.", - "action-name": "Název", - "action-name-required": "Název akce je povinný.", - "action-name-not-unique": "Jiná akce s identickým názvem již existuje.
    Název akce by měl být v rámci zdroje akce unikátní.", - "action-icon": "Ikona", - "action-type": "Typ", - "action-type-required": "Typ akce je povinný.", - "edit-action": "Editovat akci", - "delete-action": "Smazat akci", - "delete-action-title": "Smazat akci widgetu", - "delete-action-text": "Jste si jisti, že chcete smazat akci widgetu s názvem '{{actionName}}'?" - }, - "widget-type": { - "import": "Importovat typ widgetu", - "export": "Exportovat typ widgetu", - "export-failed-error": "Typ widgetu nebylo možné exportovat: {{error}}", - "create-new-widget-type": "Vytvořit nový typ widgetu", - "widget-type-file": "Soubor typu widgetu", - "invalid-widget-type-file-error": "Typ widgetu nebylo možné importovat: Neplatná datová struktura typu widgetu." - }, - "widgets": { - "date-range-navigator": { - "localizationMap": { - "Sun": "Ne", - "Mon": "Po", - "Tue": "Út", - "Wed": "St", - "Thu": "Čt", - "Fri": "Pá", - "Sat": "So", - "Jan": "Led", - "Feb": "Úno", - "Mar": "Bře", - "Apr": "Dub", - "May": "Květen", - "Jun": "Čvn", - "Jul": "Čvc", - "Aug": "Srp", - "Sep": "Zář", - "Oct": "Říj", - "Nov": "Lis", - "Dec": "Pro", - "January": "Leden", - "February": "Únor", - "March": "Březen", - "April": "Duben", - "June": "Červen", - "July": "Červenec", - "August": "Srpen", - "September": "Září", - "October": "Říjen", - "November": "Listopad", - "December": "Prosinec", - "Custom Date Range": "Vlastní rozsah data", - "Date Range Template": "Šablona rozsahu data", - "Today": "Dnes", - "Yesterday": "Včera", - "This Week": "Tento týden", - "Last Week": "Minulý týden", - "This Month": "Tento měsíc", - "Last Month": "Minulý měsíc", - "Year": "Rok", - "This Year": "Tento rok", - "Last Year": "Minulý rok", - "Date picker": "Výběr data", - "Hour": "Hodina", - "Day": "Den", - "Week": "Týden", - "2 weeks": "2 týdny", - "Month": "Měsíc", - "3 months": "3 měsíce", - "6 months": "6 měsíců", - "Custom interval": "Vlastní interval", - "Interval": "Interval", - "Step size": "Velikost kroku", - "Ok": "Ok" - } - } - }, - "icon": { - "icon": "Ikona", - "select-icon": "Vybrat ikonu", - "material-icons": "Ikony Material", - "show-all": "Zobrazit všechny ikony" - }, - "custom": { - "widget-action": { - "action-cell-button": "Tlačítko buňky akce", - "row-click": "Při kliknutí na řádek", - "polygon-click": "Při kliknutí na polygon", - "marker-click": "Při kliknutí na značku", - "tooltip-tag-action": "Akce štítku nápovědy", - "node-selected": "Při výběru uzlu" - } - }, - "language": { - "language": "Jazyk" - } -} diff --git a/ui/src/app/locale/locale.constant-de_DE.json b/ui/src/app/locale/locale.constant-de_DE.json deleted file mode 100644 index 1f8b3b6fbe..0000000000 --- a/ui/src/app/locale/locale.constant-de_DE.json +++ /dev/null @@ -1,1693 +0,0 @@ -{ - "access": { - "unauthorized": "Nicht autorisiert", - "unauthorized-access": "Unautorisierter Zugriff", - "unauthorized-access-text": "Sie sollten sich anmelden, um Zugriff auf diese Daten zu erhalten!", - "access-forbidden": "Keine Zugangsberechtigung", - "access-forbidden-text": "Sie haben keine Zugangsberechtigung für diesen Bereich!
    Versuchen Sie sich mit einem anderen Benutzer anzumelden um Zugriff auf diesen Bereich zu bekommen.", - "refresh-token-expired": "Sitzung ist abgelaufen", - "refresh-token-failed": "Sitzung kann nicht aktualisiert werden" - }, - "action": { - "activate": "Aktivieren", - "suspend": "Unterbrechen", - "save": "Speichern", - "saveAs": "Speichern unter", - "cancel": "Abbrechen", - "ok": "OK", - "delete": "Löschen", - "add": "Hinzufügen", - "yes": "Ja", - "no": "Nein", - "update": "Aktualisieren", - "remove": "Löschen", - "search": "Suche", - "clear-search": "Suchanfrage löschen", - "assign": "Zuordnen", - "unassign": "Zuordnung aufheben", - "share": "Teilen", - "make-private": "Privat machen", - "apply": "Anwenden", - "apply-changes": "Änderungen übernehmen", - "edit-mode": "Bearbeitungsmodus", - "enter-edit-mode": "Zum Bearbeitungsmodus wechseln", - "decline-changes": "Änderungen nicht übernehmen", - "close": "Schließen", - "back": "Zurück", - "run": "Ausführen", - "sign-in": "Anmelden!", - "edit": "Bearbeiten", - "view": "Ansicht", - "create": "Erstellen", - "drag": "Ziehen", - "refresh": "Aktualisieren", - "undo": "Rückgängig machen", - "copy": "Kopieren", - "paste": "Einfügen", - "copy-reference": "Zeichen kopieren", - "paste-reference": "Zeichen einfügen", - "import": "Importieren", - "export": "Exportieren", - "share-via": "Teilen mit {{provider}}" - }, - "aggregation": { - "aggregation": "Aggregation", - "function": "Datenaggregationsfunktion", - "limit": "Höchstwerte", - "group-interval": "Gruppierungsintervall", - "min": "Minimal", - "max": "Maximal", - "avg": "Durchschnitt", - "sum": "Summe", - "count": "Anzahl", - "none": "kein Wert" - }, - "admin": { - "general": "Allgemein", - "general-settings": "Allgemeine Einstellungen", - "outgoing-mail": "E-Mail Versand", - "outgoing-mail-settings": "Konfiguration des Postausgangsservers", - "system-settings": "Systemeinstellungen", - "test-mail-sent": "Test E-Mail wurde erfolgreich versendet!", - "base-url": "Basis-URL", - "base-url-required": "Basis-URL ist erforderlich.", - "mail-from": "E-Mail von", - "mail-from-required": "E-Mail von ist erforderlich.", - "smtp-protocol": "SMTP Protokoll", - "smtp-host": "SMTP Host", - "smtp-host-required": "SMTP Host ist erforderlich.", - "smtp-port": "SMTP Port", - "smtp-port-required": "Sie müssen einen SMTP Port angeben.", - "smtp-port-invalid": "Das ist kein gültiger SMTP Port.", - "timeout-msec": "Wartezeit (msec)", - "timeout-required": "Wartezeit ist erforderlich.", - "timeout-invalid": "Das ist keine gültige Wartezeit.", - "enable-tls": "TLS aktivieren", - "tls-version" : "TLS-Version", - "send-test-mail": "Test E-Mail senden", - "security-settings": "Sicherheitseinstellungen", - "password-policy": "Kennwortrichtlinie", - "minimum-password-length": "Minimale Passwortlänge", - "minimum-password-length-required": "Minimale Passwortlänge ist erforderlich", - "minimum-password-length-range": "Die Mindestlänge des Passworts sollte zwischen 5 und 50 liegen", - "minimum-uppercase-letters": "Mindestanzahl von Großbuchstaben", - "minimum-uppercase-letters-range": "Die Mindestanzahl von Großbuchstaben kann nicht negativ sein", - "minimum-lowercase-letters": "Mindestanzahl von Kleinbuchstaben", - "minimum-lowercase-letters-range": "Die Mindestanzahl von Kleinbuchstaben kann nicht negativ sein", - "minimum-digits": "Mindestanzahl von Ziffern", - "minimum-digits-range": "Die Mindestanzahl von Ziffern kann nicht negativ sein", - "minimum-special-characters": "Mindestanzahl von Sonderzeichen", - "minimum-special-characters-range": "Die Mindestanzahl von Sonderzeichen kann nicht negativ sein", - "password-expiration-period-days": "Gültigkeitsdauer des Passworts in Tagen", - "password-expiration-period-days-range": "Die Gültigkeitsdauer des Passworts in Tagen kann nicht negativ sein", - "password-reuse-frequency-days": "Häufigkeit der Kennwortwiederverwendung in Tagen", - "password-reuse-frequency-days-range": "Die Häufigkeit der Kennwortwiederverwendung in Tagen kann nicht negativ sein", - "general-policy": "Allgemeine Politik", - "max-failed-login-attempts": "Maximale Anzahl fehlgeschlagener Anmeldeversuche, bevor das Konto gesperrt wird", - "minimum-max-failed-login-attempts-range": "Die maximale Anzahl fehlgeschlagener Anmeldeversuche kann nicht negativ sein", - "user-lockout-notification-email": "Wenn das Benutzerkonto gesperrt ist, senden Sie eine Benachrichtigung per E-Mail" - }, - "alarm": { - "alarm": "Alarm", - "alarms": "Alarme", - "select-alarm": "Alarm auswählen", - "no-alarms-matching": "Keine passenden Alarme zu '{{entity}}' wurden gefunden.", - "alarm-required": "Alarm ist erforderlich", - "alarm-status": "Alarm Status", - "search-status": { - "ANY": "Jeder", - "ACTIVE": "Aktiv", - "CLEARED": "Gelöscht", - "ACK": "Bestätigt", - "UNACK": "Nicht bestätigt" - }, - "display-status": { - "ACTIVE_UNACK": "Nicht bestätigt aktiv", - "ACTIVE_ACK": "Bestätigt aktiv", - "CLEARED_UNACK": "Nicht bestätigt", - "CLEARED_ACK": "Bestätigung gelöscht" - }, - "no-alarms-prompt": "Keine Alarme gefunden", - "created-time": "Erstellungszeit", - "type": "Typ", - "severity": "Schwere", - "originator": "Urheber", - "originator-type": "Urheber-Typ", - "details": "Details", - "status": "Status", - "alarm-details": "Alarm-Details", - "start-time": "Startzeit", - "end-time": "Endzeit", - "ack-time": "Bestätigungszeit", - "clear-time": "Zeit gelöscht", - "severity-critical": "Kritisch", - "severity-major": "Groß", - "severity-minor": "Klein", - "severity-warning": "Warnung", - "severity-indeterminate": "Unbestimmt", - "acknowledge": "Bestätigen", - "clear": "Löschen", - "search": "Alarme Suchen", - "selected-alarms": "{ count, plural, 1 {1 Alarm} other {# Alarme} } ausgewählt", - "no-data": "Keine Daten zum Anzeigen", - "polling-interval": "Alarmabfrageintervall (sec)", - "polling-interval-required": "Alarmabfrageintervall ist erforderlich.", - "min-polling-interval-message": "Mindestens 1 sec Abrufintervall ist zulässig.", - "aknowledge-alarms-title": "{ count, plural, 1 {1 Alarm} other {# Alarme} } bestätigen", - "aknowledge-alarms-text": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Alarm} other {# Alarme} } bestätigen möchten?", - "aknowledge-alarm-title": "Alarm bestätigen", - "aknowledge-alarm-text": "Möchten Sie den Alarm wirklich bestätigen?", - "clear-alarms-title": "{ count, plural, 1 {1 Alarm} other {# Alarme} } löschen", - "clear-alarms-text": "Möchten Sie wirklich { count, plural, 1 {1 Alarm} other {# Alarme} } löschen?", - "clear-alarm-title": "Alarm löschen", - "clear-alarm-text": "Möchten Sie den Alarm wirklich löschen?", - "alarm-status-filter": "Alarm Status Filter" - }, - "alias": { - "add": "Alias hinzufügen", - "edit": "Alias bearbeiten", - "name": "Aliasname", - "name-required": "Aliasname ist erforderlich", - "duplicate-alias": "Ein Alias mit demselben Namen ist bereits vorhanden.", - "filter-type-single-entity": "Einzelne Entität", - "filter-type-entity-list": "Entitätsliste", - "filter-type-entity-name": "Entitätsname", - "filter-type-state-entity": "Entität aus dem Dashboard Status", - "filter-type-state-entity-description": "Entität aus den Dashboard Status Parametern", - "filter-type-asset-type": "Objekttyp", - "filter-type-asset-type-description": "Objekte vom Typ '{{assetType}}'", - "filter-type-asset-type-and-name-description": "Objekte vom Typ '{{assetType}}' und Name beginnend mit '{{prefix}}'", - "filter-type-device-type": "Gerätetyp", - "filter-type-device-type-description": "Geräte vom Typ '{{deviceType}}'", - "filter-type-device-type-and-name-description": "Geräte vom Typ '{{deviceType}}' und Name beginnend mit '{{prefix}}'", - "filter-type-entity-view-type": "Entitätsansichtstyp", - "filter-type-entity-view-type-description": "Entitätsansichten vom Typ '{{entityView}}'", - "filter-type-entity-view-type-and-name-description": "Entitätsansichten vom Typ '{{entityView}}' und Name beginnend mit '{{prefix}}'", - "filter-type-relations-query": "Beziehungsabfrage", - "filter-type-relations-query-description": "{{entities}} mit {{relationType}} Beziehung {{direction}} {{rootEntity}}", - "filter-type-asset-search-query": "Objektabfrage", - "filter-type-asset-search-query-description": "Objekte vom Typ {{assetTypes}} mit {{relationType}} Beziehung {{direction}} {{rootEntity}}", - "filter-type-device-search-query": "Geräteabfrage", - "filter-type-device-search-query-description": "Geräte vom Typ {{deviceTypes}} mit {{relationType}} Beziehung {{direction}} {{rootEntity}}", - "filter-type-entity-view-search-query": "Entitätsansichtsabfrage", - "filter-type-entity-view-search-query-description": "Entitätsansichten vom Typ {{entityViewTypes}} mit {{relationType}} Beziehung {{direction}} {{rootEntity}}", - "entity-filter": "Entitätsfilter", - "resolve-multiple": "Als mehrere Entitäten auflösen", - "filter-type": "Filtertyp", - "filter-type-required": "Filtertyp ist erforderlich.", - "entity-filter-no-entity-matched": "Es wurden keine Entitäten gefunden, die dem angegebenen Filter entsprechen.", - "no-entity-filter-specified": "Es wurde kein Entitätsfilter angegeben", - "root-state-entity": "Dashboard Status Entität als Wurzel verwenden", - "root-entity": "Wurzelentität", - "state-entity-parameter-name": "Parameter-Name der Statusentität", - "default-state-entity": "Standard Statusentität", - "default-entity-parameter-name": "Standardmäßig", - "max-relation-level": "Maximale Beziehungstiefe", - "unlimited-level": "Unbegrenzte Ebenen", - "state-entity": "Dashboard Status Entität", - "all-entities": "Alle Entitäten", - "any-relation": "Jede Beziehung" - }, - "asset": { - "asset": "Objekt", - "assets": "Objekte", - "management": "Objektverwaltung", - "view-assets": "Objekte anzeigen", - "add": "Objekt hinzufügen", - "assign-to-customer": "Einem Kunden zuordnen", - "assign-asset-to-customer": "Objekte dem Kunden zuordnen", - "assign-asset-to-customer-text": "Bitte wählen Sie die Objekte aus, die dem Kunden zugeordnet werden sollen", - "no-assets-text": "Keine Objekte gefunden", - "assign-to-customer-text": "Bitte wählen Sie den Kunden aus, dem die Objekte zugeordnet werden sollen", - "public": "Öffentlich", - "assignedToCustomer": "Kunden zugeordnet", - "make-public": "Objekt öffentlich machen", - "make-private": "Objekt privat machen", - "unassign-from-customer": "Kundenzuordnung aufheben", - "delete": "Objekt löschen", - "asset-public": "Objekt ist öffentlich", - "asset-type": "Objekttyp", - "asset-type-required": "Objekttyp ist erforderlich.", - "select-asset-type": "Objekttyp auswählen", - "enter-asset-type": "Objekttyp bestätigen", - "any-asset": "Jedes Objekt", - "no-asset-types-matching": "Es wurden keine zu '{{entitySubtype}}' passenden Objekte gefunden.", - "asset-type-list-empty": "Keine Objekttypen ausgewählt.", - "asset-types": "Objekttypen", - "name": "Name", - "name-required": "Name ist erforderlich.", - "description": "Beschreibung", - "type": "Typ", - "type-required": "Typ ist erforderlich.", - "details": "Details", - "events": "Ereignisse", - "add-asset-text": "Neues Objekt hinzufügen", - "asset-details": "Objektdetails", - "assign-assets": "Objekte zuordnen", - "assign-assets-text": "Kunden { count, plural, 1 {1 Objekt} other {# Objekte} } zuordnen", - "delete-assets": "Objekte löschen", - "unassign-assets": "Objektzuordnungen aufheben", - "unassign-assets-action-title": "Kunden { count, plural, 1 {1 Objektzuordnung} other {# Objektzuordnungen} } aufheben", - "assign-new-asset": "Neues Objekt zuordnen", - "delete-asset-title": "Sind Sie sicher, dass Sie das Objekt '{{assetName}}' löschen möchten?", - "delete-asset-text": "Vorsicht, nach Bestätigung wird das Objekt und alle zugehörigen Daten nicht wiederherstellbar gelöscht.", - "delete-assets-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Objekt} other {# Objekte} } löschen möchten?", - "delete-assets-action-title": "{ count, plural, 1 {1 Objekt} other {# Objekte} } löschen", - "delete-assets-text": "Vorsicht, nach Bestätigung werden die ausgewählten Objekte und alle zugehörigen Daten nicht wiederherstellbar gelöscht", - "make-public-asset-title": "Sind Sie sicher, dass Sie das Objekt '{{assetName}}' öffentlich machen möchten?", - "make-public-asset-text": "Nach Bestätigung wird das Objekt und alle zugehörigen Daten anderen zugänglich gemacht.", - "make-private-asset-title": "Sind Sie sicher, dass Sie das Objekt '{{assetName}}' privat machen möchten?", - "make-private-asset-text": "Nach Bestätigung wird das Objekt und alle zugehörigen Daten privat und ist für andere nicht mehr zugänglich.", - "unassign-asset-title": "Sind Sie sicher, dass Sie die Zuordnung für das Objekt '{{assetName}}' aufheben möchten?", - "unassign-asset-text": "Nach Bestätigung wird die Zuordnung des Objekts aufgehoben und es ist für den Kunden nicht mehr zugänglich.", - "unassign-asset": "Zuordnung des Objekts aufheben", - "unassign-assets-title": "Möchten Sie die Zuordnung von { count, plural, 1 {1 Objekt} other {# Objekte} } aufheben?", - "unassign-assets-text": "Nach Bestätigung wird die Zuordnung der ausgewählten Objekte aufgehoben und sie sind für den Kunden nicht mehr zugänglich.", - "copyId": "Objekt-ID kopieren", - "idCopiedMessage": "Objekt-ID wurde in die Zwischenablage kopiert", - "select-asset": "Objekt auswählen", - "no-assets-matching": "Es wurden keine zu '{{entity}}' passenden Objekte gefunden.", - "asset-required": "Objekt ist erforderlich", - "name-starts-with": "Name des Objekts beginnt mit", - "label": "Bezeichnung" - }, - "attribute": { - "attributes": "Eigenschaften", - "latest-telemetry": "Neueste Telemetrie", - "attributes-scope": "Entitätseigenschaftsbereich", - "scope-latest-telemetry": "Neueste Telemetrie", - "scope-client": "Client Eigenschaften", - "scope-server": "Server Eigenschaften", - "scope-shared": "Gemeinsame Eigenschaften", - "add": "Eigenschaft hinzufügen", - "key": "Schlüssel", - "last-update-time": "Datum der letzten Aktualisierung", - "key-required": "Eigenschaftsschlüssel ist erforderlich.", - "value": "Wert", - "value-required": "Eigenschaftswert ist erforderlich.", - "delete-attributes-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Eigenschaft} other {# Eigenschaften} } löschen möchten?", - "delete-attributes-text": "Seien Sie vorsichtig, nach der Bestätigung werden alle ausgewählten Eigenschaften entfernt.", - "delete-attributes": "Eigenschaften löschen", - "enter-attribute-value": "Geben Sie den Eigenschaftswert ein", - "show-on-widget": "Im Widget anzeigen", - "widget-mode": "Widget-Modus", - "next-widget": "Nächstes Widget", - "prev-widget": "Vorheriges Widget", - "add-to-dashboard": "Zum Dashboard hinzufügen", - "add-widget-to-dashboard": "Widget zum Dashboard hinzufügen", - "selected-attributes": "{ count, plural, 1 {1 Eigenschaft} other {# Eigenschaften} } ausgewählt", - "selected-telemetry": "{ count, plural, 1 {1 Telemetrieeinheit } other {# Telemetrieeinheiten} } ausgewählt" - }, - "audit-log": { - "audit": "Audit", - "audit-logs": "Audit-Protokolle", - "timestamp": "Zeitstempel", - "entity-type": "Entitätstype", - "entity-name": "Entitätsname", - "user": "User", - "type": "Typ", - "status": "Status", - "details": "Details", - "type-added": "Hinzugefügt", - "type-deleted": "Gelöscht", - "type-updated": "Aktualisiert", - "type-attributes-updated": "Eigenschaften aktualisiert", - "type-attributes-deleted": "Eigenschaften gelöscht", - "type-rpc-call": "RPC Aufruf", - "type-credentials-updated": "Anmeldeinformationen wurden aktualisiert", - "type-assigned-to-customer": "Kunden Zuordnung", - "type-unassigned-from-customer": "Kunden Zuordnung aufgehoben", - "type-activated": "Aktiviert", - "type-suspended": "Ausgesetzt", - "type-credentials-read": "Anmeldeinformationen gelesen", - "type-attributes-read": "Eigenschaften gelesen", - "type-relation-add-or-update": "Beziehung aktualisiert", - "type-relation-delete": "Beziehung gelöscht", - "type-relations-delete": "Alle Beziehungen gelöscht", - "type-alarm-ack": "Bestätigt", - "type-alarm-clear": "Gelöscht", - "type-login": "Anmeldung", - "type-logout": "Ausloggen", - "type-lockout": "Aussperrung", - "status-success": "Erfolg", - "status-failure": "Fehler", - "audit-log-details": "Audit-Protokoll Details", - "no-audit-logs-prompt": "Keine Protokolle gefunden", - "action-data": "Aktionsdaten", - "failure-details": "Fehlerdetails", - "search": "Audit-Protokolle durchsuchen", - "clear-search": "Suche leeren" - }, - "confirm-on-exit": { - "message": "Sie haben nicht gespeicherte Änderungen. Möchten Sie diese Seite wirklich verlassen?", - "html-message": "Sie haben nicht gespeicherte Änderungen.
    Möchten Sie diese Seite wirklich verlassen?", - "title": "Nicht gespeicherte Änderungen" - }, - "contact": { - "country": "Land", - "city": "Stadt", - "state": "Bundesland", - "postal-code": "Postleitzahl", - "postal-code-invalid": "Ungültiges Format der Postleitzahl.", - "address": "Adresse", - "address2": "Adresse 2", - "phone": "Telefon", - "email": "E-Mail", - "no-address": "Keine Adresse" - }, - "common": { - "username": "Benutzername", - "password": "Passwort", - "enter-username": "Benutzername eingeben", - "enter-password": "Passwort eingeben", - "enter-search": "Suche eingeben" - }, - "content-type": { - "json": "Json", - "text": "Text", - "binary": "Binär (Base64)" - }, - "customer": { - "customer": "Kunde", - "customers": "Kunden", - "management": "Kundenverwaltung", - "dashboard": "Kunden Dashboard", - "dashboards": "Kunden Dashboards", - "devices": "Kundengeräte", - "entity-views": "Kunden Entitätsansichten", - "assets": "Kundenobjekte", - "public-dashboards": "Öffentliche Dashboards", - "public-devices": "Öffentliche Geräte", - "public-assets": "Öffentliche Objekte", - "public-entity-views": "Öffentliche Entitätsansichten", - "add": "Kunde hinzufügen", - "delete": "Kunde löschen", - "manage-customer-users": "Kundenbenutzer verwalten", - "manage-customer-devices": "Kundengeräte verwalten", - "manage-customer-dashboards": "Kunden-Dashboards verwalten", - "manage-public-devices": "Öffentliche Geräte verwalten", - "manage-public-dashboards": "Öffentliche Dashboards verwalten", - "manage-customer-assets": "Kundenobjekte verwalten", - "manage-public-assets": "Öffentliche Objekte verwalten", - "add-customer-text": "Neuen Kunden hinzufügen", - "no-customers-text": "Keine Kunden gefunden", - "customer-details": "Kundendetails", - "delete-customer-title": "Sind Sie sicher, dass der Kunde '{{customerTitle}}' gelöscht werden soll?", - "delete-customer-text": "Vorsicht, nach Bestätigung wird der Kunde und alle zugehörigen Daten nicht wiederherstellbar gelöscht.", - "delete-customers-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Kunde} other {# Kunden} } löschen möchten?", - "delete-customers-action-title": "{ count, plural, 1 {1 Kunde} other {# Kunden} } löschen", - "delete-customers-text": "Seien Sie vorsichtig, nach der Bestätigung werden alle ausgewählten Kunden und alle zugehörigen Daten nicht wiederherstellbar gelöscht.", - "manage-users": "User verwalten", - "manage-assets": "Objekte verwalten", - "manage-devices": "Geräte verwalten", - "manage-dashboards": "Dashboards verwalten", - "title": "Titel", - "title-required": "Titel ist erforderlich.", - "description": "Beschreibung", - "details": "Details", - "events": "Ereignisse", - "copyId": "Kunden-ID kopieren", - "idCopiedMessage": "Kunden-ID wurde in die Zwischenablage kopiert", - "select-customer": "Kunden auswählen", - "no-customers-matching": "Keine Kunden für '{{entity}}' gefunden.", - "customer-required": "Kunde ist erforderlich", - "select-default-customer": "Wählen Sie den Standardkunden aus.", - "default-customer": "Standardkunde", - "default-customer-required": "Ein Standardkunde ist erforderlich, um das Dashboard auf Mandantenebene zu testen." - }, - "datetime": { - "date-from": "Datum von", - "time-from": "Zeit von", - "date-to": "Datum bis", - "time-to": "Zeit bis" - }, - "dashboard": { - "dashboard": "Dashboard", - "dashboards": "Dashboards", - "management": "Dashboardverwaltung", - "view-dashboards": "Dashboards anzeigen", - "add": "Dashboard hinzufügen", - "assign-dashboard-to-customer": "Dashboard(s) dem Kunden zuordnen", - "assign-dashboard-to-customer-text": "Bitte wählen Sie die Dashboards aus, die Sie dem Kunden zuordnen möchten", - "assign-to-customer-text": "Bitte wählen Sie den Kunden aus, dem die Dashboards zugeordnet werden sollen", - "assign-to-customer": "Kunden zuordnen", - "unassign-from-customer": "Zuordnung zum Kunden aufheben", - "make-public": "Dashboard öffentlich machen", - "make-private": "Dashboard privat machen", - "manage-assigned-customers": "Zugeordnete Kunden verwalten", - "assigned-customers": "Zugeordnete Kunden", - "assign-to-customers": "Dashboard(s) zu Kunden zuweisen", - "assign-to-customers-text": "Bitte wählen Sie den Kunden aus, dem Sie das Dashboard(s) zuweisen möchten", - "unassign-from-customers": "Zuordnung von Dashboard(s) zu Kunden aufheben", - "unassign-from-customers-text": "Bitte wählen Sie die Kunden aus, für die die Zuordnung von Dashboard(s) aufgehoben werden soll", - "no-dashboards-text": "Keine Dashboard(s) gefunden", - "no-widgets": "Keine Widgets konfiguriert", - "add-widget": "Neues Widget hinzufügen", - "title": "Titel", - "select-widget-title": "Widget auswählen", - "select-widget-subtitle": "Liste der verfügbaren Widget-Typen", - "delete": "Dashboard löschen", - "title-required": "Titel ist erforderlich.", - "description": "Beschreibung", - "details": "Details", - "dashboard-details": "Dashboard-Details", - "add-dashboard-text": "Neues Dashboard hinzufügen", - "assign-dashboards": "Dashboards zuweisen", - "assign-new-dashboard": "Neues Dashboard zuweisen", - "assign-dashboards-text": "Zuordnen { count, plural, 1 {1 Dashboard} other {# Dashboards} } zu Kunden", - "unassign-dashboards-action-text": "Zuordnung { count, plural, 1 {1 Dashboard} other {# Dashboards} } vom Kunden aufheben", - "delete-dashboards": "Dashboards löschen", - "unassign-dashboards": "Zuordnung von Dashboards aufheben", - "unassign-dashboards-action-title": "Zuordnung { count, plural, 1 {1 Dashboard} other {# Dashboards} } vom Kunden aufheben", - "delete-dashboard-title": "Sind Sie sicher, dass Sie das Dashboard '{{dashboardTitle}}' löschen möchten?", - "delete-dashboard-text": "Vorsicht, nach Bestätigung werden das Dashboard und alle zugehörigen Daten nicht mehr wiederhergestellt.", - "delete-dashboards-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Dashboard} other {# Dashboards} } löschen möchten?", - "delete-dashboards-action-title": "Löschen { count, plural, 1 {1 Dashboard} other {# Dashboards} }", - "delete-dashboards-text": "Vorsicht, nach Bestätigung werden alle ausgewählten Dashboards entfernt und alle zugehörigen Daten nicht mehr wiederhergestellt.", - "unassign-dashboard-title": "Sind Sie sicher, dass Sie die Zuordnung zum Dashboard '{{dashboardTitle}}' aufheben möchten?", - "unassign-dashboard-text": "Nach der Bestätigung wird die Zuordnung des Dashboards aufgehoben und es ist für den Kunden nicht mehr zugänglich.", - "unassign-dashboard": "Zuordnung zum Kunden aufheben", - "unassign-dashboards-title": "Sind Sie sicher, dass Sie die Zuordug aufheben möchten { count, plural, 1 {1 Dashboard} other {# Dashboards} }?", - "unassign-dashboards-text": "Nach der Bestätigung wird die Zuordnung aller ausgewählten Dashboards aufgehoben und sie sind für den Kunden nicht mehr zugänglich.", - "public-dashboard-title": "Dashboard wurde veröffentlicht", - "public-dashboard-text": "Ihr Dashboard {{dashboardTitle}} ist jetzt öffentlich und über nächste Öffentlichkeit zugänglich link:", - "public-dashboard-notice": "Note: Vergessen Sie nicht, verwandte Geräte öffentlich zu machen, um auf Ihre Daten zugreifen zu können.", - "make-private-dashboard-title": "Sind Sie sicher, dass Sie das Dashboard '{{dashboardTitle}}' privatisieren möchten?", - "make-private-dashboard-text": "Nach der Bestätigung wird das Dashboard privatisiert und ist für andere nicht zugänglich.", - "make-private-dashboard": "Dashboard privatisieren", - "socialshare-text": "'{{dashboardTitle}}' Bereitgestellt vom ThingsBoard", - "socialshare-title": "'{{dashboardTitle}}' Bereitgestellt vom ThingsBoard", - "select-dashboard": "Dashboard auswählen", - "no-dashboards-matching": "Es wurden keine passenden Dashboards '{{entity}}' gefunden.", - "dashboard-required": "Dashboard ist erforderlich.", - "select-existing": "Existierendes Dashboard auswählen", - "create-new": "Neues Dashboard erstellen", - "new-dashboard-title": "Neuer Dashboard Titel", - "open-dashboard": "Dashboard öffnen", - "set-background": "Hintergrund einstellen", - "background-color": "Hintergrundfarbe", - "background-image": "Hintergrundbild", - "background-size-mode": "Hintergrundgrößenmodus", - "no-image": "Kein Bild ausgewählt", - "drop-image": "Legen Sie ein Bild ab oder klicken Sie, um eine hochzuladende Datei auszuwählen.", - "settings": "Einstellungen", - "columns-count": "Spalten zählen", - "columns-count-required": "Die Anzahl der Spalten ist erforderlich.", - "min-columns-count-message": "Es müssen mindestens 10 Spalten vorhanden sein.", - "max-columns-count-message": "Es sind maximal 100 Spalten zulässig.", - "widgets-margins": "Abstand zwischen den Widgets", - "horizontal-margin": "Horizontaler Abstand", - "horizontal-margin-required": "Horizontaler Abstandswert ist erforderlich.", - "min-horizontal-margin-message": "Der horizontale Abstandswert muss mindestens 0 betragen.", - "max-horizontal-margin-message": "Der horizontale Abstandswert beträgt maximal 50.", - "vertical-margin": "Vertikaler Abstand", - "vertical-margin-required": "Vertikaler Abstandswert ist erforderlich.", - "min-vertical-margin-message": "Der vertikale Abstandswert muss mindestens 0 betragen.", - "max-vertical-margin-message": "Der vertikale Abstandswert beträgt maximal 50.", - "autofill-height": "Layouthöhe automatisch füllen", - "mobile-layout": "Mobile Layouteinstellungen", - "mobile-row-height": "Mobile Zeilenhöhe, px", - "mobile-row-height-required": "Ein mobiler Zeilenhöhenwert ist erforderlich.", - "min-mobile-row-height-message": "Der Mindestwert für die mobile Zeilenhöhe beträgt 5 Pixel.", - "max-mobile-row-height-message": "Der Höchstwert für die mobile Zeilenhöhe beträgt 200 Pixel.", - "display-title": "Display Dashboard Titel", - "toolbar-always-open": "Werkzeugleiste geöffnet lassen", - "title-color": "Titelfarbe ", - "display-dashboards-selection": "Auswahl der Dashboards anzeigen", - "display-entities-selection": "Auswahl der Einheiten zulassen", - "display-dashboard-timewindow": "Zeitfenster anzeigen", - "display-dashboard-export": "Export anzeigen", - "import": "Dashboard importieren", - "export": "Dashboard exportieren", - "export-failed-error": "Dashboard kann nicht exportiert werden: {{error}}", - "create-new-dashboard": "Neues Dashboard erstellen", - "dashboard-file": "Dashboard-Datei", - "invalid-dashboard-file-error": "Dashboard kann nicht importiert werden: Ungültige Dashboard-Datenstruktur.", - "dashboard-import-missing-aliases-title": "Konfigurieren Sie die von importierten Dashboards verwendeten Aliasnamen", - "create-new-widget": "Neues Widget erstellen", - "import-widget": "Widget importieren", - "widget-file": "Widget-Datei", - "invalid-widget-file-error": "Widget kann nicht importiert werden: Ungültige Widget-Datenstruktur.", - "widget-import-missing-aliases-title": "Konfigurieren Sie die von importierten Widgets verwendeten Aliase", - "open-toolbar": "Dashboard-Werkzeugleiste öffnen", - "close-toolbar": "Werkzeugleiste schließen", - "configuration-error": "Konfigurationsfehler", - "alias-resolution-error-title": "Konfigurationsfehler für Dashboard-Aliasnamen", - "invalid-aliases-config": "Es konnten keine Geräte gefunden werden, die mit dem Aliase-Filter übereinstimmen.
    Bitte wenden Sie sich an Ihren Administrator, um dieses problem zu beheben.", - "select-devices": "Geräte auswählen", - "assignedToCustomer": "Dem Kunden zugewiesen", - "assignedToCustomers": "Kunden zugwiesen", - "public": "Öffentlich", - "public-link": "Öffentlicher Link", - "copy-public-link": "Öffentlichen Link kopieren", - "public-link-copied-message": "Der öffentliche Link des Dashboards wurde in die Zwischenablage kopiert", - "manage-states": "Dashboard-Status verwalten", - "states": "Dashboard-Status", - "search-states": "Dashboard-Status suchen", - "selected-states": "{ count, plural, 1 {1 dashboard state} other {# dashboard states} } ausgewählt", - "edit-state": "Dashboard-Status bearbeiten", - "delete-state": "Dashboard-Status löschen", - "add-state": "Dashboard-Status hinzufügen", - "state": "Dashboard-Status", - "state-name": "Name", - "state-name-required": "Name des Dashboard-Status ist erforderlich.", - "state-id": "Status-Id", - "state-id-required": "Dashboard-Status-ID ist erforderlich.", - "state-id-exists": "Dashboard-Status mit derselben ID ist bereits vorhanden .", - "is-root-state": "Grundzustand", - "delete-state-title": "Dashboard-Status löschen", - "delete-state-text": "Sind Sie sicher, dass Sie den Dashboard-Status '{{stateName}}' löschen möchten?", - "show-details": "Details anzeigen", - "hide-details": "Details ausblenden", - "select-state": "Soll-Zustand auswählen", - "state-controller": "Zustandssteuerung" - }, - "datakey": { - "settings": "Einstellungen", - "advanced": "Erweitert", - "label": "Bezeichnung", - "color": "Farbe", - "units": "Maßeinheit die neben dem Wert angezeigt wird", - "decimals": "Anzahl der Stellen nach dem Komma", - "data-generation-func": "Daten generieren", - "use-data-post-processing-func": "Datenverarbeitungsfunktion verwenden", - "configuration": "Datenschlüsselkonfiguration", - "timeseries": "Zeitreihe", - "attributes": "Eigenschaften", - "alarm": "Alarmfelder", - "timeseries-required": "Entity-Zeitreihen sind erforderlich.", - "timeseries-or-attributes-required": "Entity-Zeitreihen/Eigenschaften sind erforderlich.", - "maximum-timeseries-or-attributes": "Maximum { count, plural, 1 {1 Zeitreihe/Eigenschaft ist erlaubt} other {# Zeitreihen/Eigenschaften sind erlaubt} }.", - "alarm-fields-required": "Alarmfelder sind erforderlich.", - "function-types": "Funktionsarten", - "function-types-required": "Funktionstypen sind erforderlich.", - "maximum-function-types": "Maximal { count, plural, 1 {1 Funktionstyp ist erlaubt} other {# Funktionstypen sind erlaubt} }.", - "time-description": "Zeitstempel des aktuellen Wertes;", - "value-description": "Der aktuelle Wert;", - "prev-value-description": "Ergebnis des vorherigen Funktionsaufrufs;", - "time-prev-description": "Zeitmarke des vorherigen Wertes;", - "prev-orig-value-description": "Ursprünglicher vorheriger Wert;" - }, - "datasource": { - "type": "Datenquellentyp", - "name": "Name", - "add-datasource-prompt": "Bitte Datenquelle hinzufügen" - }, - "details": { - "edit-mode": "Bearbeitungsmodus", - "toggle-edit-mode": "Bearbeitungsmodus umschalten" - }, - "device": { - "device": "Gerät", - "device-required": "Gerät ist erforderlich.", - "devices": "Geräte", - "management": "Geräte verwalten", - "view-devices": "Geräte anzeigen", - "device-alias": "Geräte-Alias", - "aliases": "Gerätealiasnamen", - "no-alias-matching": "'{{alias}}' nicht gefunden.", - "no-aliases-found": "Keine Aliase gefunden.", - "no-key-matching": "'{{key}}' nicht gefunden.", - "no-keys-found": "Keine Schlüssel gefunden.", - "create-new-alias": "Neues Alias erstellen!", - "create-new-key": "Neuen Schlüssel erstellen!", - "duplicate-alias-error": "Doppelter Alias gefunden '{{alias}}'.
    Gerätealiasnamen müssen innerhalb des Dashboards eindeutig sein.", - "configure-alias": "Alias '{{alias}}' konfigurieren", - "no-devices-matching": "Keine passenden Geräte '{{entity}}' gefunden.", - "alias": "Alias", - "alias-required": "Geräte-Alias ist erforderlich.", - "remove-alias": "Geräte-Alias entfernen", - "add-alias": "Geräte-Alias hinzufügen", - "name-starts-with": "Gerätename beginnt mit", - "device-list": "Geräteliste", - "use-device-name-filter": "Filter verwenden", - "device-list-empty": "Keine Geräte ausgewählt.", - "device-name-filter-required": "Der Gerätenamefilter ist erforderlich.", - "device-name-filter-no-device-matched": "Keine Geräte beginnend mit '{{device}}' gefunden.", - "add": "Gerät hinzufügen", - "assign-to-customer": "Kunden zuordnen", - "assign-device-to-customer": "Gerät(e) dem Kunden zuordnen", - "assign-device-to-customer-text": "Bitte wählen Sie die Geräte aus, die Sie dem Kunden zuordnen möchten", - "make-public": "Gerät veröffentlichen", - "make-private": "Gerät privatisieren", - "no-devices-text": "Keine Geräte gefunden", - "assign-to-customer-text": "Bitte wählen Sie einen Kunden aus, dem die Geräte zugeordnet werden sollen", - "device-details": "Gerätedetails", - "add-device-text": "Neues Gerät hinzufügen", - "credentials": "Zugangsdaten", - "manage-credentials": "Zugangsdaten verwalten", - "delete": "Gerät löschen", - "assign-devices": "Gerät zuordnen", - "assign-devices-text": "{ count, plural, 1 {1 Gerät} other {# Geräte} } dem Kunden zuordnen", - "delete-devices": "Geräte löschen", - "unassign-from-customer": "Zuordnung zum Kunden aufheben", - "unassign-devices": "Nicht zugeordnete Geräte", - "unassign-devices-action-title": "Zuordnung von { count, plural, 1 {1 Gerät} other {# Geräte} } zum Kunden aufheben", - "assign-new-device": "Neues Gerät zuordnen", - "make-public-device-title": "Sind Sie sicher, dass Sie das Gerät '{{deviceName}}' öffentlich machen möchten?", - "make-public-device-text": "Nach der Bestätigung werden das Gerät und dessen Daten öffentlich und für andere zugänglich.", - "make-private-device-title": "Sind Sie sicher, dass Sie das Gerät '{{deviceName}}' privat machen möchten?", - "make-private-device-text": "Nach der Bestätigung werden das Gerät und dessen Daten privat und sind für andere nicht mehr zugänglich.", - "view-credentials": "Zugangsdaten anzeigen", - "delete-device-title": "Möchten Sie das Gerät '{{deviceName}}' wirklich löschen?", - "delete-device-text": "Vorsicht, nach Bestätigung werden das Gerät und alle zugehörigen Daten nicht mehr wiederhergestellt.", - "delete-devices-title": "Sind Sie sicher, dass Sie löschen möchten { count, plural, 1 {1 Gerät} other {# Geräte} }?", - "delete-devices-action-title": "Löschen { count, plural, 1 {1 Gerät} other {# Geräte} }", - "delete-devices-text": "Vorsicht, nach Bestätigung werden alle ausgewählten Geräte entfernt und alle zugehörigen Daten werden nicht mehr wiederhergestellt.", - "unassign-device-title": "Sind Sie sicher, dass Sie die Zuordnung zum Gerät '{{deviceName}}' wirklich aufheben möchten?", - "unassign-device-text": "Nach der Bestätigung ist das Gerät nicht zugeordnet und für den Kunden nicht zugänglich.", - "unassign-device": "Nicht zugeordnete Geräte", - "unassign-devices-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Gerät} other {# Geräte} } nicht mehr zuordnen möchten?", - "unassign-devices-text": "Nach der Bestätigung werden alle ausgewählten Geräte nicht zugewiesen und sind für den Kunden nicht zugänglich.", - "device-credentials": "Geräte Zugangsdaten", - "credentials-type": "Art der Zugangsdaten", - "access-token": "Zugangs-Token", - "access-token-required": "Zugangs-Token ist erforderlich.", - "access-token-invalid": "Die Länge des Zugangs-Tokens muss zwischen 1 und 20 Zeichen betragen.", - "rsa-key": "RSA öffentlicher Schlüssel", - "rsa-key-required": "RSA öffentlicher Schlüssel ist erforderlich.", - "secret": "Geheimnis", - "secret-required": "Geheimnis ist erforderlich.", - "device-type": "Gerätetyp", - "device-type-required": "Gerätetyp ist erforderlich.", - "select-device-type": "Gerätetyp auswählen", - "enter-device-type": "Gerätetyp eingeben", - "any-device": "Jedes Gerät", - "no-device-types-matching": "Keine passenden Gerätetypen '{{entitySubtype}}' gefunden.", - "device-type-list-empty": "Kein Gerätetyp ausgewählt.", - "device-types": "Gerätetypen", - "name": "Name", - "name-required": "Name ist erforderlich.", - "description": "Beschreibung", - "events": "Ereignisse", - "details": "Details", - "copyId": "Geräte-ID kopieren", - "copyAccessToken": "Zugangs-Token kopieren", - "idCopiedMessage": "Geräte-ID wurde in die Zwischenablage kopiert", - "accessTokenCopiedMessage": "Geräte-Zugangs-Token wurde in die Zwischenablage kopiert", - "assignedToCustomer": "Dem Kunden zuordnen", - "unable-delete-device-alias-title": "Geräte-Alias kann nicht gelöscht werden", - "unable-delete-device-alias-text": "Geräte-Alias '{{deviceAlias}}' kann nicht gelöscht werden, da er von den folgenden Widgets verwendet wird:
    {{widgetsList}}", - "is-gateway": "Ist ein Gateway", - "public": "Öffentlich", - "device-public": "Gerät ist öffentlich", - "select-device": "Gerät auswählen" - }, - "dialog": { - "close": "Dialog schließen" - }, - "error": { - "unable-to-connect": "Es konnte keine Verbindung zum Server hergestellt werden! Bitte überprüfen Sie Ihre Internetverbindung.", - "unhandled-error-code": "Unbehandelter Fehlercode: {{errorCode}}", - "unknown-error": "Unbekannter Fehler" - }, - "entity": { - "entity": "Entität", - "entities": "Entitäten", - "aliases": "Entitäts-Aliasnamen", - "entity-alias": "Entitätsalias", - "unable-delete-entity-alias-title": "Alias der Entität kann nicht gelöscht werden", - "unable-delete-entity-alias-text": "Alias der Entität '{{entityAlias}}' kann nicht gelöscht werden, da es von den folgenden Widget(s) verwendet wird:
    {{widgetsList}}", - "duplicate-alias-error": "Doppelte Alias gefunden '{{alias}}'.
    Die Aliase der Entität müssen innerhalb des Dashboards eindeutig sein.", - "missing-entity-filter-error": "Fehlender Filter für Alias '{{alias}}'.", - "configure-alias": "Alias '{{alias}}' konfigurieren", - "alias": "Alias", - "alias-required": "Alias der Entität ist erforderlich.", - "remove-alias": "Alias der Entität entfernen", - "add-alias": "Alias der Entität erforderlich", - "entity-list": "Entitätsliste", - "entity-type": "Entitätstyp", - "entity-types": "Entitätstypen", - "entity-type-list": "Liste der Entitätstyp", - "any-entity": "Jede Entität", - "enter-entity-type": "Entitätstyp eingeben", - "no-entities-matching": "Keine passenden Entitäten für '{{entity}}' gefunden.", - "no-entity-types-matching": "Keine passende Entitätstypen für '{{entityType}}' gefunden.", - "name-starts-with": "Name beginnt mit", - "use-entity-name-filter": "Filter verwenden", - "entity-list-empty": "Keine Entitäten ausgewählt.", - "entity-type-list-empty": "Keine Entitättypen ausgewählt.", - "entity-name-filter-required": "Entitätsnamenfilter ist erforderlich.", - "entity-name-filter-no-entity-matched": "Keine Entitäten beginnend mit '{{entity}}' gefunden.", - "all-subtypes": "Alle", - "select-entities": "Entitäten auswählen", - "no-aliases-found": "Keine Aliase gefunden.", - "no-alias-matching": "'{{alias}}' nicht gefunden.", - "create-new-alias": "Erstellen Sie einen neuen Alias!", - "key": "Schlüssel", - "key-name": "Name des Schlüssels", - "no-keys-found": "Kein Schlüssel gefunden.", - "no-key-matching": "'{{key}}' nicht gefunden.", - "create-new-key": "Erstellen Sie einen neuen Schlüssel!", - "type": "Typ", - "type-required": "Typ der Entität ist erforderlich.", - "type-device": "Gerät", - "type-devices": "Geräte", - "list-of-devices": "{ count, plural, 1 {Ein Gerät} other {Liste von # Geräten} }", - "device-name-starts-with": "Geräte beginnend mit '{{prefix}}'", - "type-asset": "Objekt", - "type-assets": "Objekte", - "list-of-assets": "{ count, plural, 1 {Ein Objekt} other {Liste von # Objekten} }", - "asset-name-starts-with": "Objekte beginnend mit '{{prefix}}'", - "type-entity-view": "Entitätsansicht", - "type-entity-views": "Entitätsansichten", - "list-of-entity-views": "{ count, plural, 1 {Eine Entitätsansicht} other {Liste von # Entitätsansichten} }", - "entity-view-name-starts-with": "Entitätsansichten beginnend mit'{{prefix}}'", - "type-rule": "Regel", - "type-rules": "Regeln", - "list-of-rules": "{ count, plural, 1 {Eine Regel} other {Liste von # Regeln} }", - "rule-name-starts-with": "Regeln beginnend mit '{{prefix}}'", - "type-plugin": "Plugin", - "type-plugins": "Plugins", - "list-of-plugins": "{ count, plural, 1 {Ein Plugin} other {Liste von # Plugins} }", - "plugin-name-starts-with": "Plugins beginnend mit '{{prefix}}'", - "type-tenant": "Mandant", - "type-tenants": "Mandanten", - "list-of-tenants": "{ count, plural, 1 {Ein Mandant} other {Liste von # Mandanten} }", - "tenant-name-starts-with": "Mandanten beginnend mit '{{prefix}}'", - "type-customer": "Kunde", - "type-customers": "Kunden", - "list-of-customers": "{ count, plural, 1 {Ein Kunde} other {Liste von # Kunden} }", - "customer-name-starts-with": "Kunden beginnend mit '{{prefix}}'", - "type-user": "Benutzer", - "type-users": "Benutzer", - "list-of-users": "{ count, plural, 1 {Ein Benutzer} other {Liste von # Benutzern} }", - "user-name-starts-with": "Benutzer beginnend mit '{{prefix}}'", - "type-dashboard": "Dashboard", - "type-dashboards": "Dashboards", - "list-of-dashboards": "{ count, plural, 1 {Ein Dashboard} other {Liste von # Dashboards} }", - "dashboard-name-starts-with": "Dashboards beginnend mit '{{prefix}}'", - "type-alarm": "Alarm", - "type-alarms": "Alarme", - "list-of-alarms": "{ count, plural, 1 {Ein Alarm} other {Liste von # Alarmen} }", - "alarm-name-starts-with": "Alarme, beginnend mit '{{prefix}}'", - "type-rulechain": "Regelkette", - "type-rulechains": "Regelketten", - "list-of-rulechains": "{ count, plural, 1 {Eine Regelkette} other {Liste von # Regelketten} }", - "rulechain-name-starts-with": "Regelketten beginnend mit '{{prefix}}'", - "type-rulenode": "Regelknoten", - "type-rulenodes": "Regelknoten", - "list-of-rulenodes": "{ count, plural, 1 {Ein Regelknoten} other {Liste von # Regelknoten} }", - "rulenode-name-starts-with": "Regelknoten beginnend mit '{{prefix}}'", - "type-current-customer": "Aktueller Kunde", - "search": "Entitäten suchen", - "selected-entities": "{ count, plural, 1 {Entität} other {# Entitäten} } ausgewählt", - "entity-name": "Entitätsname", - "details": "Entitätsdetails", - "no-entities-prompt": "Keine Entitäten gefunden", - "no-data": "Keine Daten zum Anzeigen", - "columns-to-display": "Anzuzeigende Spalten" - }, - "entity-view": { - "entity-view": "Entitätsansicht", - "entity-view-required": "Entitätsansicht ist erforderlich.", - "entity-views": "Entitätsansichten", - "management": "Entitätsansichten verwalten", - "view-entity-views": "Entitätsansichten anzeigen", - "entity-view-alias": "Entitätsansichtsalias", - "aliases": "Entitätsansichten-Aliase", - "no-alias-matching": "'{{alias}}' nicht gefunden.", - "no-aliases-found": "Keine Aliase gefunden.", - "no-key-matching": "'{{key}}' nicht gefunden.", - "no-keys-found": "Keine Schlüssel gefunden.", - "create-new-alias": "Neuen Alias erstellen!", - "create-new-key": "Neuen Schlüssel erstellen!", - "duplicate-alias-error": "Doppelter Alias gefunden '{{alias}}'.
    Aliase der Entitätsansicht müssen innerhalb des Dashboards eindeutig sein.", - "configure-alias": "Alias '{{alias}}' konfigurieren", - "no-entity-views-matching": "Keine passenden Entitätsansichten für '{{entity}}' gefunden.", - "alias": "Alias", - "alias-required": "Alias der Entitätsansicht erforderlich.", - "remove-alias": "Alias der Entitätsansicht entfernen", - "add-alias": "Alias für die Entitätsansicht hinzufügen", - "name-starts-with": "Entitätsansichtsname beginnend mit", - "entity-view-list": "Liste der Entitätsansichten", - "use-entity-view-name-filter": "Filter anwenden", - "entity-view-list-empty": "Keine der Entitätsansichten ausgewählt.", - "entity-view-name-filter-required": "Filterung nach Entitätsansichtenname erforderlich.", - "entity-view-name-filter-no-entity-view-matched": "Keine Entitätsansichten beginnend mit '{{entityView}}' wurden gefunden.", - "add": "Entitätsansicht hinzufügen", - "assign-to-customer": "Einem Kunden zuordnen", - "assign-entity-view-to-customer": "Entitätsansichten dem Kunden zuordnen", - "assign-entity-view-to-customer-text": "Bitte wählen Sie die Entitätsansichten aus, die dem Kunden zugeordnet werden sollen", - "no-entity-views-text": "Keine Entitätsansichten gefunden", - "assign-to-customer-text": "Bitte wählen Sie den Kunden aus, dem die Entitätsansichten zugeordnet werden sollen", - "entity-view-details": "Details der Entitätsansicht", - "add-entity-view-text": "Neue Entitätsansicht hinzufügen", - "delete": "Entitätsansicht löschen", - "assign-entity-views": "Entitätsansicht zuordnen", - "assign-entity-views-text": "Dem Kunden { count, plural, 1 {1 Entitätsansicht} other {# Entitätsansichten} } zuordnen", - "delete-entity-views": "Entitätsansichten löschen", - "unassign-from-customer": "Zuordnung zum Kunden aufheben", - "unassign-entity-views": "Zuordnung der Entitätsansichten aufheben", - "unassign-entity-views-action-title": "Die Zuordnung { count, plural, 1 {1 Entitätsansicht} other {# Entitätsansichten} } zum Kunden aufheben", - "assign-new-entity-view": "Neue Entitätsansicht zuordnen", - "delete-entity-view-title": "Möchten Sie die Entitätsansicht wirklich löschen '{{entityViewName}}'?", - "delete-entity-view-text": "Seien Sie vorsichtig, nach der Bestätigung werden die Entitätsansicht und alle zugehörigen Daten nicht wiederhergestellt.", - "delete-entity-views-title": "Sind Sie sicher, dass Sie die Entitätsansichten löschen möchten { count, plural, 1 {1 Entitätsansicht} other {# Entitätsansichten} }?", - "delete-entity-views-action-title": "Löschen { count, plural, 1 {1 Entitätsansicht} other {# Entitätsansichten} }", - "delete-entity-views-text": "Vorsicht, nach Bestätigung werden alle ausgewählten Entitätsansichten entfernt und alle zugehörigen Daten werden nicht wiederhergestellt.", - "unassign-entity-view-title": "Möchten Sie die Zuordnung der Entitätsansicht '{{entityViewName}}' wirklich aufheben?", - "unassign-entity-view-text": "Nach der Bestätigung wird die Zuordnung der Entitätsansicht aufgehoben und ist für den Kunden nicht mehr zugänglich.", - "unassign-entity-view": "Zuordnung der Entitätsansicht aufheben", - "unassign-entity-views-title": "Sind Sie sicher, dass Sie die Zuordnung aufheben möchten { count, plural, 1 {1 Entitätsansicht} other {# Entitätsansichten} }?", - "unassign-entity-views-text": "Nach der Bestätigung werden die Zuordnungen der ausgewählten Entitätsansichten aufgehoben und sind für den Kunden nicht mehr zugänglich.", - "entity-view-type": "Entitätsansichtstyp", - "entity-view-type-required": "Entitätsansichtstyp ist erforderlich.", - "select-entity-view-type": "Entitätsansichtstyp auswählen", - "enter-entity-view-type": "Entitätsansichtstyp eingeben", - "any-entity-view": "Jede Entitätsansicht", - "no-entity-view-types-matching": "Es wurden keine passenden Entitätsansichtstypen für '{{entitySubtype}}' gefunden.", - "entity-view-type-list-empty": "Keine Entitätsansichtstypen ausgewählt.", - "entity-view-types": "Entitätsansichtstypen", - "name": "Name", - "name-required": "Name ist erforderlich.", - "description": "Beschreibung", - "events": "Ereignisse", - "details": "Details", - "copyId": "Entitätsansichts-ID kopieren", - "assignedToCustomer": "Dem Kunden zuordnen", - "unable-entity-view-device-alias-title": "Alias der Entitätsansicht kann nicht gelöscht werden", - "unable-entity-view-device-alias-text": "Geräte-Alias '{{entityViewAlias}}' kann nicht gelöscht werden, da es von den folgenden widget(s):
    {{widgetsList}} verwendet wird", - "select-entity-view": "Entitätsansicht auswählen", - "make-public": "Entitätsansicht öffentlich machen", - "start-date": "Start-Datum", - "start-ts": "Start-Zeit", - "end-date": "Ende-Datum", - "end-ts": "Ende-Zeit", - "date-limits": "Datumslimits", - "client-attributes": "Client Eigenschaften", - "shared-attributes": "Gemeinsame Eigenschaften", - "server-attributes": "Server Eigenschaften", - "timeseries": "Zeitreihe", - "client-attributes-placeholder": "Client Eigenschaften", - "shared-attributes-placeholder": "Gemeinsame Eigenschaften", - "server-attributes-placeholder": "Server Eigenschaften", - "timeseries-placeholder": "Zeitreihe", - "target-entity": "Zielentität", - "attributes-propagation": "Eigenschaftsübertragung", - "attributes-propagation-hint": "Die Entitätsansicht kopiert automatisch die angegebenen Eigenschaften der Ziel-Entität, wenn Sie diese Entitätsansicht speichern oder aktualisieren. Aus Performance-Gründen werden die Attribute der Ziel-Entität nicht bei jeder Eigenschaftsänderung in die Entitätsansicht übertragen. Sie können die automatische Weitergabe aktivieren, indem Sie den Regelknoten \"copy to view\" in Ihrer Regelkette konfigurieren und die Nachrichten \"Post attributes\" und \"Attributes updated\" mit dem neuen Regelknoten verknüpfen.", - "timeseries-data": "Zeitreihendaten", - "timeseries-data-hint": "Konfigurieren Sie die Datensatzschlüssel der Zeitreihe der Zielentität, auf die die Entitätsansicht zugreifen kann. Die Daten dieser Zeitreihe sind schreibgeschützt." - }, - "event": { - "event-type": "Ereignistyp", - "type-error": "Fehler", - "type-lc-event": "Lebenszyklusereignis", - "type-stats": "Statistiken", - "type-debug-rule-node": "Fehlersuche", - "type-debug-rule-chain": "Fehlersuche", - "no-events-prompt": "Keine Ereignisse gefunden", - "error": "Fehler", - "alarm": "Alarm", - "event-time": "Ereigniszeit", - "server": "Server", - "body": "Inhalt", - "method": "Methode", - "type": "Typ", - "entity": "Entität", - "message-id": "Nachrichten-Id", - "message-type": "Nachrichten-Typ", - "data-type": "Datentyp", - "relation-type": "Beziehungstyp", - "metadata": "Meta-Daten", - "data": "Daten", - "event": "Ereignis", - "status": "Status", - "success": "Erfolg", - "failed": "Fehlgeschlagen", - "messages-processed": "Nachrichten verarbeitet", - "errors-occurred": "Fehler aufgetreten" - }, - "extension": { - "extensions": "Erweiterungen", - "selected-extensions": "{ count, plural, 1 {Erweiterung} other {# extensions} } ausgewählt", - "type": "Typ", - "key": "Schlüssel", - "value": "Wert", - "id": "ID", - "extension-id": "Erweiterungs-ID", - "extension-type": "Erweiterungstyp", - "transformer-json": "JSON *", - "unique-id-required": "Die aktuelle Erweiterungs-ID ist bereits vorhanden.", - "delete": "Erweiterung löschen", - "add": "Erweiterung hinzufügen", - "edit": "Erweiterung bearbeiten", - "delete-extension-title": "Möchten Sie die Erweiterung '{{extensionId}}' wirklich löschen?", - "delete-extension-text": "Vorsicht, nach Bestätigung werden die Erweiterung und alle zugehörigen Daten nicht wiederhergestellt.", - "delete-extensions-title": "Möchten Sie wirklich löschen? { count, plural, 1 {1 extension} other {# extensions} }?", - "delete-extensions-text": "Vorsicht, nach der Bestätigung werden alle ausgewählten Erweiterungen entfernt.", - "converters": "Konverter", - "converter-id": "Konverter-ID", - "configuration": "Konfiguration", - "converter-configurations": "Konvertierte Konfigurationen", - "token": "Sicherheitszeichen", - "add-converter": "Konverter hinzufügen", - "add-config": "Konvertierte Konfigurationen hinzufügen", - "device-name-expression": "Angabe des Gerätenamens", - "device-type-expression": "Angabe des Gerätetyps", - "custom": "Regel", - "to-double": "Duplizieren", - "transformer": "Transformator", - "json-required": "Transformer json ist erforderlich.", - "json-parse": "Transformer json kann nicht analysiert werden.", - "attributes": "Eigenschaften", - "add-attribute": "Eigenschaften hinzufügen", - "add-map": "Mapping-Element hinzufügen", - "timeseries": "Zeitreihe", - "add-timeseries": "Zeitreihe hinzufügen", - "field-required": "Feld ist erforderlich", - "brokers": "Vermittler", - "add-broker": "Vermittler hinzufügen", - "host": "Host", - "port": "Port", - "port-range": "Der Port sollte im Bereich von 1 bis 65535 liegen.", - "ssl": "Ssl", - "credentials": "Zugangsdaten", - "username": "Benutzername", - "password": "Passwort", - "retry-interval": "Wiederholungsintervall in Millisekunden", - "anonymous": "Anonym", - "basic": "Basic", - "pem": "PEM", - "ca-cert": "CA-Zertifikatsdatei *", - "private-key": "Privatschlüsseldatei *", - "cert": "Zertifikatsdatei *", - "no-file": "Keine Datei ausgewählt.", - "drop-file": "Legen Sie eine Datei ab oder wählen Sie eine Datei aus um diese hochzuladen.", - "mapping": "Mapping", - "topic-filter": "Themenfilter", - "converter-type": "Konvertierungstyp", - "converter-json": "Json", - "json-name-expression": "Angabe des Gerätenamens json", - "topic-name-expression": "Themenangabe des Gerätenamens", - "json-type-expression": "Angabe des Gerätenamens json", - "topic-type-expression": "Themenangabe des Gerätetyps", - "attribute-key-expression": "Angabe des Eigenschaftenschlüssels", - "attr-json-key-expression": "Angabe des Eigenschaftenschlüssels", - "attr-topic-key-expression": "Themenangabe des Eigenschaftenschlüssels", - "request-id-expression": "ID-Angabe anfordern", - "request-id-json-expression": "ID-Angabe anforern json", - "request-id-topic-expression": "Themenangabe der ID anfordern", - "response-topic-expression": "Antwort Themenangabe", - "value-expression": "Wertangabe", - "topic": "Thema", - "timeout": "Unterbrechung in Millisekunden", - "converter-json-required": "Konvertierte json ist erforderlich.", - "converter-json-parse": "Konvertierte json konnte nicht analysiert werden.", - "filter-expression": "Filterangabe", - "connect-requests": "Abfragen verbinden", - "add-connect-request": "Verbindungsabfrage hinzufügen", - "disconnect-requests": "Abfrage trennen", - "add-disconnect-request": "Trennung der Abfrage hinzufügen", - "attribute-requests": "Abfrage der Eigenschaften", - "add-attribute-request": "Abfrage der Eigenschaften hinzufügen", - "attribute-updates": "Aktualisierungen der Eigenschaften", - "add-attribute-update": "Aktualisierung der Eigenschaften hinzufügen", - "server-side-rpc": "Serverseite RPC", - "add-server-side-rpc-request": "Abfrage der Serverseite RPC hinzufügen", - "device-name-filter": "Gerätenamefilter", - "attribute-filter": "Eigenschaftenfilter", - "method-filter": "Methodenfilter", - "request-topic-expression": "Themenabgabe anfordern", - "response-timeout": "Antwortzeit in Millisekunden", - "topic-expression": "Themenangabe", - "client-scope": "Kundenumfrage", - "add-device": "Gerät hinzufügen", - "opc-server": "Servers", - "opc-add-server": "Server hinzufügen", - "opc-add-server-prompt": "Bitte einen Server hinzufügen", - "opc-application-name": "Anwendungsname", - "opc-application-uri": "Anwendung uri", - "opc-scan-period-in-seconds": "Scanzeitraum in Sekunden", - "opc-security": "Sicherheit", - "opc-identity": "Identifizierung", - "opc-keystore": "Schlüsselspeicher", - "opc-type": "Typ", - "opc-keystore-type": "Typ", - "opc-keystore-location": "Standort *", - "opc-keystore-password": "Passwort", - "opc-keystore-alias": "Alias", - "opc-keystore-key-password": "Schlüsselpasswort", - "opc-device-node-pattern": "Geräteknotenmuster", - "opc-device-name-pattern": "Gerätenamensmuster", - "modbus-server": "Servers/Folgegerät", - "modbus-add-server": "Server/Folgegerät hinzufügen", - "modbus-add-server-prompt": "Bitte Server/Folgegerät hinzufügen", - "modbus-transport": "Transport", - "modbus-tcp-reconnect": "Verbindung automatisch wiederherstellen", - "modbus-rtu-over-tcp": "RTU über TCP", - "modbus-port-name": "Name des Seriellen Anschlusses", - "modbus-encoding": "Verschlüsselung", - "modbus-parity": "Übereinstimmung", - "modbus-baudrate": "Datenübertragungsgeschwindigkeit", - "modbus-databits": "Daten Bits", - "modbus-stopbits": "Stopp-Bits", - "modbus-databits-range": "Datenbits sollten im Bereich von 7 bis 8 liegen.", - "modbus-stopbits-range": "Stoppbits sollten im Bereich von 1 bis 2 liegen.", - "modbus-unit-id": "ID der Einheit", - "modbus-unit-id-range": "Die Einheiten-ID sollte im Bereich von 1 bis 247 liegen.", - "modbus-device-name": "Gerätename", - "modbus-poll-period": "Abfragezeitraum in Millisekunden", - "modbus-attributes-poll-period": "Abfrageintervall der Eigenschaften in Millisekunden", - "modbus-timeseries-poll-period": "Abfrageintervall der Zeitreihen in Millisekunden", - "modbus-poll-period-range": "Das Abfrageintervall sollte einen positiven Wert haben.", - "modbus-tag": "Kennzeichnung", - "modbus-function": "Funktion", - "modbus-register-address": "Registeradresse", - "modbus-register-address-range": "Die Registeradresse sollte im Bereich zwischen 0 und 65535 liegen.", - "modbus-register-bit-index": "Bitindex", - "modbus-register-bit-index-range": "Der Bitindex sollte im Bereich von 0 bis 15 liegen.", - "modbus-register-count": "Registeranzahl", - "modbus-register-count-range": "Die Registeranzahl sollten einen positiven Wert haben.", - "modbus-byte-order": "Byte-Reihenfolge", - "sync": { - "status": "Status", - "sync": "Synchronisiert", - "not-sync": "Nicht synchronisiert", - "last-sync-time": "Zeit der letzten Synchronisierung", - "not-available": "Nicht verfügbar" - }, - "export-extensions-configuration": "Erweiterungskonfiguration exportieren", - "import-extensions-configuration": "Erweiterungskonfiguration importieren", - "import-extensions": "Erweiterungen importieren", - "import-extension": "Erweiterung importieren", - "export-extension": "Erweiterung exportieren", - "file": "Erweiterungsdatei", - "invalid-file-error": "Ungültige Erweiterungsdatei" - }, - "fullscreen": { - "expand": "Auf Vollbildmodus erweitern", - "exit": "Vollbildmodus verlassen", - "toggle": "Vollbildmodus umschalten", - "fullscreen": "Vollbild" - }, - "function": { - "function": "Funktion" - }, - "grid": { - "delete-item-title": "Möchten Sie dieses Element wirklich löschen?", - "delete-item-text": "Vorsicht, nach Bestätigung wird das Element und alle zugehörigen Daten nicht wiederhergestellt.", - "delete-items-title": "Sind Sie sicher, dass Sie löschen möchten { count, plural, 1 {Symbol} other {Symbole} }?", - "delete-items-action-title": "Löschen { count, plural, 1 {Symbol} other {# Symbole} }", - "delete-items-text": "Vorsicht, nach Bestätigung werden alle ausgewählten Elemente entfernt und alle zugehörigen Daten nicht wiederhergestellt.", - "add-item-text": "Neues Element hinzufügen", - "no-items-text": "Keine Elemente gefunden", - "item-details": "Elementdetails", - "delete-item": "Element löschen", - "delete-items": "Elemente löschen", - "scroll-to-top": "zum Seitenanfang" - }, - "help": { - "goto-help-page": "Gehen Sie zur Hilfeseite" - }, - "home": { - "home": "Startseite", - "profile": "Profil", - "logout": "Abmelden", - "menu": "Menü", - "avatar": "Benutzerbild", - "open-user-menu": "Benutzermenü öffnen" - }, - "import": { - "no-file": "Keine Datei ausgewählt", - "drop-file": "Legen Sie eine JSON-Datei ab oder wählen Sie eine Datei zum hochladen aus." - }, - "item": { - "selected": "Ausgewählt" - }, - "js-func": { - "no-return-error": "Funktion muss einen Wert zurückgeben!", - "return-type-mismatch": "Funktion muss einen Wert vom Typ '{{type}}' zurückgeben!", - "tidy": "Aufräumen" - }, - "key-val": { - "key": "Schlüssel", - "value": "Wert", - "remove-entry": "Eintrag entfernen", - "add-entry": "Eintag hinzufügen", - "no-data": "Keine Einträge" - }, - "layout": { - "layout": "Layout", - "manage": "Layouts verwalten", - "settings": "Layout-Einstellungen", - "color": "Farbe", - "main": "Hauptbereich", - "right": "Recht", - "select": "Wählen Sie das Ziellayout aus" - }, - "legend": { - "position": "Legendenposition", - "show-max": "Maximalwert anzeigen", - "show-min": "Minimalwert anzeigen", - "show-avg": "Durchschnittswert anzeigen", - "show-total": "Gesamtwert anzeigen", - "settings": "Legendeneinstellungen", - "min": "min.", - "max": "max.", - "avg": "mittelw.", - "total": "Gesamt" - }, - "login": { - "login": "Anmelden", - "request-password-reset": "Passwortzurücksetzung anfordern", - "reset-password": "Passwort zurücksetzen", - "create-password": "Passwort erstellen", - "passwords-mismatch-error": "Eingegebene Passwörter müssen identisch sein!", - "password-again": "Passwort wiederholen", - "sign-in": "Bitte anmelden", - "username": "Benutzername (E-Mail)", - "remember-me": "Login speichern", - "forgot-password": "Passwort vergessen?", - "password-reset": "Passwort zurücksetzen", - "new-password": "Neues Passwort", - "new-password-again": "Neues Passwort wiederholen", - "password-link-sent-message": "Der Link zum Zurücksetzen des Passworts wurde erfolgreich versendet!", - "email": "E-Mail", - "login-with": "Mit {{name}} anmelden", - "or": "oder" - }, - "position": { - "top": "Oben", - "bottom": "Unten", - "left": "Links", - "right": "Rechts" - }, - "profile": { - "profile": "Profil", - "last-login-time": "Letzte Anmeldung", - "change-password": "Passwort ändern", - "current-password": "Aktuelles Passwort" - }, - "relation": { - "relations": "Beziehungen", - "direction": "Richtung", - "search-direction": { - "FROM": "Von", - "TO": "Zu" - }, - "direction-type": { - "FROM": "von", - "TO": "zu" - }, - "from-relations": "Ausgehende Verbindungen", - "to-relations": "Eingehende Verbindungen", - "selected-relations": "{ count, plural, 1 {1 Beziehung} other {# Beziehungen} } ausgewählt", - "type": "Typ", - "to-entity-type": "Zum Entitätstyp", - "to-entity-name": "Zum Entitätsnamen", - "from-entity-type": "Vom Entitätstyp", - "from-entity-name": "Vom Entitätsnamen", - "to-entity": "Zur Entität", - "from-entity": "Von Entität", - "delete": "Beziehung löschen", - "relation-type": "Art der Beziehung", - "relation-type-required": "Art der Beziehung erforderlich.", - "any-relation-type": "Jede Art", - "add": "Beziehung hinzufügen", - "edit": "Beziehung bearbeiten", - "delete-to-relation-title": "Möchten Sie die Beziehung zur Einheit'{{entityName}}' wirklich löschen?", - "delete-to-relation-text": "Vorsicht, nach Bestätigung ist die Entität '{{entityName}}' nicht mehr mit der aktuellen Entität verbunden.", - "delete-to-relations-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Beziehung} other {# Beziehungen} } wirklich löschen?", - "delete-to-relations-text": "Vorsicht, nach Bestätigung werden alle ausgewählten Beziehungen entfernt und die entsprechenden Entitäten sind nicht mehr mit der aktuellen Entität verbunden.", - "delete-from-relation-title": "Sind Sie sicher, dass Sie die Verbindung aus der Entität '{{entityName}}' löschen möchten?", - "delete-from-relation-text": "Vorsicht, nach Bestätigung wird die aktuelle Entität '{{entityName}}' von der Entität unabhängig sein.", - "delete-from-relations-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Beziehung} other {# Beziehungen} } löschen möchten?", - "delete-from-relations-text": "Vorsicht, nach Bestätigung werden alle ausgewählten Beziehungen entfernt und die aktuellen Entität wird nicht mehr mit den entsprechenden Entitäten verknüpft sein.", - "remove-relation-filter": "Beziehungsfilter entfernen", - "add-relation-filter": "Beziehungsfilter hinzufügen", - "any-relation": "Jede Beziehung", - "relation-filters": "Beziehungsfilter", - "additional-info": "Zusätzliche Information (JSON)", - "invalid-additional-info": "Json der Zusätzlichen Informationen konnte nicht gelesen werden." - }, - "rulechain": { - "rulechain": "Regelkette", - "rulechains": "Regelketten", - "root": "Wurzel", - "delete": "Regelkette löschen", - "name": "Name", - "name-required": "Name ist erforderlich.", - "description": "Beschreibung", - "add": "Regelkette hinzufügen", - "set-root": "Regelkette zur Wurzel machen", - "set-root-rulechain-title": "Sind Sie sicher, dass Sie die Regelkette '{{ruleChainName}}' zur Wurzel machen möchten?", - "set-root-rulechain-text": "Nach der Bestätigung wird die Regelkette zur Wurzel und bearbeitet alle eingehenden Transportnachrichten.", - "delete-rulechain-title": "Sind Sie sicher, dass Sie die Regelkette '{{ruleChainName}}' löschen möchten?", - "delete-rulechain-text": "Vorsichtig, nach Bestätigung werden die Regelkette und alle zugehörigen Daten gelöscht.", - "delete-rulechains-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Regelkette} other {# Regelketten} } löschen möchten?", - "delete-rulechains-action-title": "{ count, plural, 1 {1 Regelkette} other {# Regelketten} } löschen", - "delete-rulechains-text": "Vorsichtig, nach Bestätigung werden alle ausgewählten Regelketten entfernt und alle zugehörigen Daten werden gelöscht.", - "add-rulechain-text": "Neue Regelkette hinzufügen", - "no-rulechains-text": "Keine Regelkette gefunden", - "rulechain-details": "Regelketten-Details", - "details": "Details", - "events": "Ereignisse", - "system": "System", - "import": "Regelkette importieren", - "export": "Regelkette exportieren", - "export-failed-error": "Regelkette konnte nicht exportiert werden: {{error}}", - "create-new-rulechain": "Neue Regelkette erstellen", - "rulechain-file": "Regelkettendatei", - "invalid-rulechain-file-error": "Regelkette konnte nicht importiert werden: Ungültige Regelkettendatenstruktur.", - "copyId": "Regelketten-ID kopieren", - "idCopiedMessage": "Regelketten-ID wurde in die Zwischenablage kopiert", - "select-rulechain": "Regelkette auswählen", - "no-rulechains-matching": "Es wurden keine passenden Regelketten für '{{entity}}' gefunden.", - "rulechain-required": "Regelkette ist erforderlich", - "management": "Regelverwaltung", - "debug-mode": "Modus zur Fehlersuche" - }, - "rulenode": { - "details": "Details", - "events": "Ereignisse", - "search": "Knoten suchen", - "open-node-library": "Knotenbibliothek öffnen", - "add": "Neuen Regelknoten hinzufügen", - "name": "Name", - "name-required": "Name ist erforderlich.", - "type": "Typ", - "description": "Beschreibung", - "delete": "Regelknoten löschen", - "select-all-objects": "Alle Knoten und Verbindungen auswählen", - "deselect-all-objects": "Auswahl aller Knoten und Verbindungen aufheben", - "delete-selected-objects": "Ausgewählte Knoten und Verbindungen löschen", - "delete-selected": "Auswahl löschen", - "select-all": "Alle auswählen", - "copy-selected": "Auswahl kopieren", - "deselect-all": "Nichts auswählen", - "rulenode-details": "Details der Regelknoten", - "debug-mode": "Modus zur Fehlersuche", - "configuration": "Konfiguration", - "link": "Verbindung", - "link-details": "Verbindungsdetails der Regelknoten", - "add-link": "Verbindung hinzufügen", - "link-label": "Verbindungsbeschriftung", - "link-label-required": "Verbindungsbeschriftung ist erforderlich.", - "custom-link-label": "Benutzerdefinierte Verbindungsbeschriftung", - "custom-link-label-required": "Benutzerdefinierte Verbindungsbeschriftung ist erforderlich.", - "link-labels": "Verbindungsbeschriftungen", - "link-labels-required": "Verbindungsbeschriftungen sind erforderlich.", - "no-link-labels-found": "Keine Verbindungsbeschriftungen gefunden", - "no-link-label-matching": "'{{label}}' nicht gefunden.", - "create-new-link-label": "Bitte erstellen Sie eine neue Verbindungsbeschriftung!", - "type-filter": "Filter", - "type-filter-details": "Eingehende Nachrichten mit konfigurierten Bedingungen filtern", - "type-enrichment": "Anreicherung", - "type-enrichment-details": "Fügen Sie zusätzliche Informationen zu den Nachrichtenmetadaten hinzu", - "type-transformation": "Transformation", - "type-transformation-details": "Ändern Sie die Nutzerdaten und Metadaten der Nachricht", - "type-action": "Aktion", - "type-action-details": "Besondere Aktion ausführen", - "type-external": "Extern", - "type-external-details": "Interagiert mit externem System", - "type-rule-chain": "Regelkette", - "type-rule-chain-details": "Leitet eingehende Nachrichten an die angegebene Regelkette weiter", - "type-input": "Input", - "type-input-details": "Logische Eingabe der Regelkette, leitet eingehende Nachrichten an die nächste zugehörige Regelkette weiter", - "type-unknown": "Unbekannt", - "type-unknown-details": "Nicht aufgelöster Regelknoten", - "directive-is-not-loaded": "Definierte Konfigurationsanweisung '{{directiveName}}' ist nicht verfügbar.", - "ui-resources-load-error": "Fehler beim Laden der Konfigurations-UI-Ressourcen.", - "invalid-target-rulechain": "Zielregelkette kann nicht aufgelöst werden!", - "test-script-function": "Skriptfunktion testen", - "message": "Nachricht", - "message-type": "Nachrichtentyp", - "select-message-type": "Nachrichtentyp auswählen", - "message-type-required": "Nachrichtentyp ist erforderlich", - "metadata": "Metadaten", - "metadata-required": "Metadateneinträge dürfen nicht leer sein.", - "output": "Ausgabe", - "test": "Test", - "help": "Hilfe" - }, - "tenant": { - "tenant": "Mandant", - "tenants": "Mandanten", - "management": "Mandantenverwaltung", - "add": "Mandant hinzufügen", - "admins": "Administratoren", - "manage-tenant-admins": "Mandantenadministratoren verwalten", - "delete": "Mandant löschen", - "add-tenant-text": "Neuen Mandanten hinzufügen", - "no-tenants-text": "Keine Mandanten gefunden", - "tenant-details": "Mandantendetails", - "delete-tenant-title": "Möchten Sie den Mandanten '{{tenantTitle}}' wirklich löschen?", - "delete-tenant-text": "Vorsicht, nach Bestätigung werden der Mandant und alle zugehörigen Daten gelöscht.", - "delete-tenants-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Mandant} other {# Mandanten} } löschen möchten?", - "delete-tenants-action-title": "{ count, plural, 1 {1 Mandant} other {# Mandanten} } löschen", - "delete-tenants-text": "Vorsicht, nach Bestätigung werden alle ausgewählten Mandanten entfernt und alle zugehörigen Daten werden gelöscht.", - "title": "Titel", - "title-required": "Titel ist erforderlich.", - "description": "Beschreibung", - "details": "Details", - "events": "Ereignisse", - "copyId": "Mandanten-ID kopieren", - "idCopiedMessage": "Mandanten-ID wurde in die Zwischenablage kopiert", - "select-tenant": "Mandant auswählen", - "no-tenants-matching": "Es wurden keine passenden Mandanten für '{{entity}}' gefunden.", - "tenant-required": "Mandant ist erforderlich" - }, - "timeinterval": { - "seconds-interval": "{ seconds, plural, 1 {1 Sekunde} other {# Sekunden} }", - "minutes-interval": "{ minutes, plural, 1 {1 Minute} other {# Minuten} }", - "hours-interval": "{ hours, plural, 1 {1 Stunde} other {# Stunden} }", - "days-interval": "{ days, plural, 1 {1 Tag} other {# Tage} }", - "days": "Tage", - "hours": "Stunden", - "minutes": "Minuten", - "seconds": "Sekunden", - "advanced": "Erweitert" - }, - "timewindow": { - "days": "{ days, plural, 1 { Tag } other {# Tage } }", - "hours": "{ hours, plural, 0 { Stunde } 1 {1 Stunde } other {# Stunden } }", - "minutes": "{ minutes, plural, 0 { Minute } 1 {1 Minute } other {# Minuten } }", - "seconds": "{ seconds, plural, 0 { Sekunde } 1 {1 Sekunde } other {# Sekunden } }", - "realtime": "Echtzeit", - "history": "Historie", - "last-prefix": "letzte", - "period": "von {{ startTime }} bis {{ endTime }}", - "edit": "Zeitfenster bearbeiten", - "date-range": "Datumsbereich", - "last": "Letzte", - "time-period": "Zeitfenster" - }, - "user": { - "user": "User", - "users": "Users", - "customer-users": "Kunden Users", - "tenant-admins": "Mandanten-Administratoren", - "sys-admin": "System-Administrator", - "tenant-admin": "Mandanten-Administrator", - "customer": "Kunde", - "anonymous": "Anonym", - "add": "Benutzer hinzufügen", - "delete": "Benutzer löschen", - "add-user-text": "Neuen Benutzer hinzufügen", - "no-users-text": "Keine Benutzer gefunden", - "user-details": "Benutzer-Details", - "delete-user-title": "Möchten Sie den Benutzer '{{userEmail}}' wirklich löschen?", - "delete-user-text": "Vorsicht, nach Bestätigung werden der Benutzer und alle zugehörigen Daten gelöscht.", - "delete-users-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Benutzer} other {# Benutzer} } löschen möchten??", - "delete-users-action-title": "{ count, plural, 1 {1 Benutzer} other {# Benutzer} } löschen", - "delete-users-text": "Vorsicht, nach Bestätigung werden alle ausgewählten Benutzer entfernt und alle zugehörigen Daten werden gelöscht.", - "activation-email-sent-message": "Aktivierungs E-Mail wurde erfolgreich gesendet!", - "resend-activation": "Aktivierung erneut senden", - "email": "E-Mail", - "email-required": "E-Mail ist erforderlich.", - "invalid-email-format": "Ungültiges E-Mail Format.", - "first-name": "Vorname", - "last-name": "Nachname", - "description": "Beschreibung", - "default-dashboard": "Standard-Dashboard", - "always-fullscreen": "Immer Vollbild", - "select-user": "Benutzer auswählen", - "no-users-matching": "Keine passenden Benutzer für '{{entity}}' gefunden.", - "user-required": "Benutzer ist erforderlich", - "activation-method": "Aktivierungsmethode", - "display-activation-link": "Aktivierungslink anzeigen", - "send-activation-mail": "Aktivierungs E-Mail senden", - "activation-link": "Link zur Benutzer-Aktivierung", - "activation-link-text": "Um den Benutzer zu aktivieren, verwenden Sie bitte folgenden Aktivierungslink:", - "copy-activation-link": "Aktivierungslink kopieren", - "activation-link-copied-message": "Der Link zur Benutzer-Aktivierung wurde in die Zwischenablage kopiert ", - "details": "Details", - "login-as-tenant-admin": "Als Mandanten-Administrator anmelden", - "login-as-customer-user": "Als Kunden-Benutzer anmelden", - "disable-account": "Benutzerkonto deaktivieren", - "enable-account": "Benutzerkonto aktivieren", - "enable-account-message": "Benutzerkonto wurde erfolgreich aktiviert!", - "disable-account-message": "Benutzerkonto wurde erfolgreich deaktiviert!" - }, - "value": { - "type": "Wertetyp", - "string": "Text", - "string-value": "Textwert", - "integer": "Ganzzahlig", - "integer-value": "Ganzzahliger Wert", - "invalid-integer-value": "Ungültiger ganzzahliger Wert", - "double": "Gleitkommazahl", - "double-value": "Gleitkomma Wert", - "boolean": "Binär", - "boolean-value": "Binärwert", - "false": "Falsch", - "true": "Wahr", - "long": "Lang" - }, - "widget": { - "widget-library": "Widget-Bibliothek", - "widget-bundle": "Widget-Paket", - "select-widgets-bundle": "Widget-Paket auswählen", - "management": "Widget Verwaltung", - "editor": "Widget Editor", - "widget-type-not-found": "Problem beim Laden der Widget-Konfiguration.
    Zugeordneter Widget-Typ wurde entfernt.", - "widget-type-load-error": "Widget wurde aufgrund der folgenden Fehler nicht geladen:", - "remove": "Widget entfernen", - "edit": "Widget bearbeiten", - "remove-widget-title": "Möchten Sie das Widget '{{widgetTitle}}' wirklich entfernen?", - "remove-widget-text": "Nach der Bestätigung werden das Widget und alle zugehörigen Daten nicht wiederhergestellt.", - "timeseries": "Zeitreihe", - "search-data": "Daten suchen", - "no-data-found": "Keine Daten gefunden", - "latest-values": "Neueste Werte", - "rpc": "Steuerungswidget", - "alarm": "Alarm-Widget", - "static": "Statisches Widget", - "select-widget-type": "Widget-Typ auswählen", - "missing-widget-title-error": "Widget-Titel muss angegeben werden!", - "widget-saved": "Widget gespeichert", - "unable-to-save-widget-error": "Das Widget kann nicht gespeichert werden! Fehlermeldung!", - "save": "Widget speichern", - "saveAs": "Widget speichern unter", - "save-widget-type-as": "Widget-Typ speichern unter", - "save-widget-type-as-text": "Bitte geben Sie den neuen Widget-Titel ein und/oder wählen Sie das Ziel-Widget-Paket aus", - "toggle-fullscreen": "Vollbild umschalten", - "run": "Widget ausführen", - "title": "Widget-Titel", - "title-required": "Widget-Titel ist erforderlich.", - "type": "Widget-Typ", - "resources": "Ressourcen", - "resource-url": "JavaScript/CSS URL", - "remove-resource": "Ressource entfernen", - "add-resource": "Ressource hinzufügen", - "html": "HTML", - "tidy": "Aufgeräumt", - "css": "CSS", - "settings-schema": "Einstellungsschema", - "datakey-settings-schema": "Datenschlüssel-Einstellungsschema", - "javascript": "Javascript", - "remove-widget-type-title": "Möchten Sie den Widget-Typ '{{widgetName}}' wirklich entfernen?", - "remove-widget-type-text": "Nach der Bestätigung werden der Widget-Typ und alle zugehörigen Daten nicht wiederhergestellt.", - "remove-widget-type": "Widget-Typ entfernen", - "add-widget-type": "Neuen Widget-Typ hinzufügen", - "widget-type-load-failed-error": "Widget-Typ konnte nicht geladen werden!", - "widget-template-load-failed-error": "Widget-Vorlage konnte nicht geladen werden!", - "add": "Widget hinzufügen", - "undo": "Widget-Änderungen widerrufen ", - "export": "Widget exportieren" - }, - "widget-action": { - "header-button": "Widget-Header-Schaltfläche", - "open-dashboard-state": "Zum neuen Dashboard-Status navigieren", - "update-dashboard-state": "Aktuellen Dashboard-Status aktualisieren", - "open-dashboard": "Zu einem anderen Dashboard navigieren", - "custom": "Benutzerdefinierte Aktion", - "target-dashboard-state": "Zielstatus des Dashboards", - "target-dashboard-state-required": "Der Zielstatus ist erforderlich", - "set-entity-from-widget": "Widget-Entität festlegen", - "target-dashboard": "Ziel-Dashboard", - "open-right-layout": "Das rechte Dashboard-Layout öffnen (mobile Ansicht)" - }, - "widgets-bundle": { - "current": "Aktuelles Paket", - "widgets-bundles": "Widget-Pakete", - "add": "Widget-Pakete hinzufügen", - "delete": "Widget-Pakete löschen", - "title": "Titel", - "title-required": "Titel ist erforderlich.", - "add-widgets-bundle-text": "Neues Widget-Paket hinzufügen", - "no-widgets-bundles-text": "Keine Widget-Pakete gefunden", - "empty": "Widget-Paket ist leer ", - "details": "Details", - "widgets-bundle-details": "Widget-Paket-Details", - "delete-widgets-bundle-title": "Möchten Sie das Widget-Paket '{{widgetsBundleTitle}}' wirklich löschen? ", - "delete-widgets-bundle-text": "Seien Sie vorsichtig, nach der Bestätigung werden das Widget-Paket und alle zugehörigen Daten gelöscht.", - "delete-widgets-bundles-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Widget-Paket} other {# Widget-Pakete} } löschen möchten?", - "delete-widgets-bundles-action-title": "{ count, plural, 1 {1 Widgets-Paket} other {# Widget-Pakete} } löschen", - "delete-widgets-bundles-text": "Vorsicht, nach Bestätigung werden alle ausgewählten Widget-Pakete entfernt und alle zugehörigen Daten werden gelöscht.", - "no-widgets-bundles-matching": "Keine passenden Widget-Pakete '{{widgetsBundle}}' gefunden.", - "widgets-bundle-required": "Widget-Paket ist erforderlich.", - "system": "System", - "import": "Widget-Paket importieren", - "export": "Widget-Paket exportieren", - "export-failed-error": "Widget-Paket kann nicht exportiert werden: {{error}}", - "create-new-widgets-bundle": "Neues Widget-Paket erstellen", - "widgets-bundle-file": "Widget-Paket-Datei", - "invalid-widgets-bundle-file-error": "Widget-Paket kann nicht importiert werden: Ungültige Widget-Paket-Datenstruktur." - }, - "widget-config": { - "data": "Daten", - "settings": "Einstellungen", - "advanced": "Erweitert ", - "title": "Titel", - "title-tooltip": "Titel Tooltip", - "general-settings": "Allgemeine Einstellungen", - "display-title": "Titel anzeigen", - "drop-shadow": "Schlagschatten", - "enable-fullscreen": "Vollbild aktivieren", - "background-color": "Hintergrundfarbe", - "text-color": "Textfarbe", - "padding": "Pufferung", - "margin": "Rand", - "widget-style": "Widget-Stil", - "title-style": "Titel-Stil", - "mobile-mode-settings": "Einstellungen für den mobilen Modus", - "order": "Reihenfolge", - "height": "Größe", - "units": "Spezielles Symbol, das neben dem Wert angezeigt wird", - "decimals": "Anzahl der Stellen nach dem Fließkomma", - "timewindow": "Zeitfenster", - "use-dashboard-timewindow": "Dashboard-Zeitfenster verwenden", - "display-timewindow": "Zeitfenster anzeigen", - "display-legend": "Legende anzeigen", - "datasources": "Datenquellen", - "maximum-datasources": "Maximal { count, plural, 1 {1 Datenquelle ist erlaubt} other {# Datenquellen sind erlaubt} }.", - "datasource-type": "Typ", - "datasource-parameters": "Parameter", - "remove-datasource": "Datenquelle entfernen", - "add-datasource": "Datenquelle hinzufügen ", - "target-device": "Zielgerät", - "alarm-source": "Alarmquelle", - "actions": "Aktionen", - "action": "Aktion", - "add-action": "Aktion hinzufügen", - "search-actions": "Aktion suchen", - "action-source": "Aktionsquelle", - "action-source-required": "Aktionsquelle ist erforderlich.", - "action-name": "Name", - "action-name-required": "Aktionsname ist erforderlich.", - "action-name-not-unique": "Eine andere Aktion mit demselben Namen ist bereits vorhanden.
    Der Aktionsname sollte innerhalb derselben Aktionsquelle eindeutig sein.", - "action-icon": "Symbol ", - "action-type": "Art", - "action-type-required": "Aktionsart ist erforderlich.", - "edit-action": "Aktion bearbeiten", - "delete-action": "Aktion löschen", - "delete-action-title": "Widget-Aktion löschen", - "delete-action-text": "Möchten Sie die Widget-Aktion mit Namen '{{actionName}}' wirklich löschen?" - }, - "widget-type": { - "import": "Widget-Typ importieren", - "export": "Widget-Typ exportieren", - "export-failed-error": "Widget-Typ kann nicht exportiert werden: {{error}}", - "create-new-widget-type": "Neuen Widget-Typ erstellen", - "widget-type-file": "Widget-Typdatei", - "invalid-widget-type-file-error": "Widget-Typ kann nicht importiert werden: Ungültige Datenstruktur des Widget-Typs." - }, - "widgets": { - "date-range-navigator": { - "localizationMap": { - "Sun": "So.", - "Mon": "Mo.", - "Tue": "Di.", - "Wed": "Mi.", - "Thu": "Do.", - "Fri": "Fr.", - "Sat": "Sa.", - "Jan": "Jan.", - "Feb": "Feb.", - "Mar": "März", - "Apr": "Apr.", - "May": "Mai", - "Jun": "Juni", - "Jul": "Juli", - "Aug": "Aug.", - "Sep": "Sep.", - "Oct": "Okt.", - "Nov": "Nov.", - "Dec": "Dez.", - "January": "Januar", - "February": "Februar", - "March": "März", - "April": "April", - "June": "Juni", - "July": "Juli", - "August": "August", - "September": "September", - "October": "Oktober", - "November": "November", - "December": "Dezember", - "Custom Date Range": "Benutzerdefinierter Datumsbereich", - "Date Range Template": "Datumsbereichsvorlage", - "Today": "Heute", - "Yesterday": "Gestern", - "This Week": "Diese Woche", - "Last Week": "Letzte Woche", - "This Month": "Diesen Monat", - "Last Month": "Im vergangenen Monat", - "Year": "Jahr", - "This Year": "Dieses Jahr", - "Last Year": "Vergangenes Jahr", - "Date picker": "Datumsauswahl", - "Hour": "Stunde", - "Day": "Tag", - "Week": "Woche", - "2 weeks": "2 Wochen", - "Month": "Monat", - "3 months": "3 Monate", - "6 months": "6 Monate", - "Custom interval": "Benutzerdefiniertes Intervall", - "Interval": "Intervall", - "Step size": "Schrittlänge", - "Ok": "Ok" - } - }, - "input-widgets": { - "attribute-not-allowed": "Attributparameter können in diesem Widget nicht verwendet werden", - "date": "Datum", - "discard-changes": "Änderungen verwerfen", - "entity-attribute-required": "Entitätsattribut ist erforderlich", - "entity-timeseries-required": "Zeitreihen für Entitäten sind erforderlich", - "not-allowed-entity": "Die ausgewählte Entität kann keine gemeinsamen Attribute haben", - "no-attribute-selected": "Es ist kein Attribut ausgewählt", - "no-datakey-selected": "Es ist kein Datenschlüssel ausgewählt", - "no-entity-selected": "Keine Entität ausgewählt", - "no-image": "Kein Bild", - "no-support-web-camera": "Keine unterstützte Webcam", - "no-timeseries-selected": "Keine Zeitreihen ausgewählt", - "switch-attribute-value": "Entitätsattributwert wechseln", - "switch-camera": "Kamera wechseln", - "switch-timeseries-value": "Wert für Zeitreihen von Entitäten wechseln", - "take-photo": "Foto machen", - "time": "Zeit", - "timeseries-not-allowed": "Der Timeseries-Parameter kann in diesem Widget nicht verwendet werden", - "update-failed": "Aktualisierung fehlgeschlagen", - "update-successful": "Aktualisierung erfolgreich", - "update-attribute": "Attribut aktualisieren", - "update-timeseries": "Zeitreihen aktualisieren", - "value": "Wert" - } - }, - "icon": { - "icon": "Symbol", - "select-icon": "Symbol auswählen", - "material-icons": "Material-Symbole", - "show-all": "Alle Symbole anzeigen" - }, - "custom": { - "widget-action": { - "action-cell-button": "Aktionszellenschaltfläche", - "row-click": "Klick auf Zeile", - "polygon-click": "Klick auf Polygon", - "marker-click": "Klick auf Marker", - "tooltip-tag-action": "Tooltip-Tag-Aktion", - "node-selected": "Klick auf Node", - "element-click": "Klick auf HTML element", - "pie-slice-click": "Klicken auf Slice", - "row-double-click": "Doppelklicken auf Zeile" - } - }, - "language": { - "language": "Sprache" - } -} diff --git a/ui/src/app/locale/locale.constant-el_GR.json b/ui/src/app/locale/locale.constant-el_GR.json deleted file mode 100644 index bf51056f94..0000000000 --- a/ui/src/app/locale/locale.constant-el_GR.json +++ /dev/null @@ -1,2608 +0,0 @@ -{ - "access": { - "unauthorized": "Χωρίς δικαιώματα πρόσβασης", - "unauthorized-access": "Μη εξουσιοδοτημένη πρόσβαση", - "unauthorized-access-text": "Θα πρέπει να συνδεθείτε για να έχετε πρόσβαση σε αυτόν τον πόρο!", - "access-forbidden": "Απαγορεύεται η πρόσβαση!", - "access-forbidden-text": "Δεν έχετε δικαιώματα πρόσβασης σε αυτήν την τοποθεσία!
    Συνδεθείτε με διαφορετικό όνομα χρήστη, αν εξακολουθείτε να θέλετε να έχετε πρόσβαση σε αυτήν την τοποθεσία.", - "refresh-token-expired": "Η περίοδος χρήσης έχει λήξει", - "refresh-token-failed": "Δεν είναι δυνατή η ανανέωση της περιόδου χρήσης", - "permission-denied": "Άρνηση πρόσβασης!", - "permission-denied-text": "Δεν έχετε δικαίωμα εκτέλεσης αυτής της λειτουργίας!" - }, - "action": { - "activate": "Ενεργοποίηση", - "suspend": "Αναστολή", - "save": "Αποθήκευση", - "saveAs": "Αποθήκευση ως", - "cancel": "Ακύρωση", - "ok": "OK", - "delete": "Διαγραφή", - "add": "Προσθήκη", - "yes": "Ναι", - "no": "Όχι", - "update": "Ενημέρωση", - "remove": "Διαγραφή", - "search": "Αναζήτηση", - "clear-search": "Καθαρισμός Αναζήτησης", - "assign": "Ανάθεση", - "unassign": "Ακύρωση Ανάθεσης", - "share": "Διαμοιρασμός", - "make-private": "Ιδιωτικό", - "apply": "Εφαρμογή", - "apply-changes": "Εφααρμογή Αλλαγών", - "edit-mode": "Λειτουργία Επεξεργασίας", - "enter-edit-mode": "Έναρξη Επεξεργασίας", - "decline-changes": "Απόρριψη Αλλαγών", - "close": "Κλείσιμο", - "back": "Πίσω", - "run": "Εκτέλεση", - "sign-in": "Σύνδεση!", - "edit": "Επεξεργασία", - "view": "Επισκόπηση", - "create": "Δημιουργία", - "drag": "Drag", - "refresh": "Ανανέωση", - "undo": "Αναίρεση", - "copy": "Αντιγραφή", - "paste": "Επικόλληση", - "copy-reference": "Αντιγραφή παραπομπής", - "paste-reference": "Επικόλληση παραπομπής", - "import": "Εισαγωγή", - "export": "Εξαγωγή", - "share-via": "Διαμοίραση μέσω {{provider}}", - "move": "Μετακίνηση", - "select": "Επιλογή", - "continue": "Συνέχεια" - }, - "aggregation": { - "aggregation": "Συνάθροιση", - "function": "Συνάρτηση συνάθροισης δεδομένων", - "limit": "Μέγιστες τιμές", - "group-interval": "Διάστημα ομαδοποίησης", - "min": "Min", - "max": "Max", - "avg": "Μέσος Όρος", - "sum": "Άθροισμα", - "count": "Καταμέτρηση", - "none": "Κανένα" - }, - "admin": { - "general": "Γενικά", - "general-settings": "Γενικές Ρυθμίσεις", - "outgoing-mail": "Διακομιστής Αλληλογραφίας", - "outgoing-mail-settings": "Διακομιστής Εξερχόμενης Αλληλογραφίας", - "system-settings": "Ρυθμίσεις Συστήματος", - "test-mail-sent": "Το δοκιμαστικό μήνυμα στάλθηκε με επιτυχία!", - "base-url": "Βασική διεύθυνση URL", - "base-url-required": "Απαιτείται ορισμός Base URL.", - "mail-from": "Αποστολέας", - "mail-from-required": "Απαιτείται βασική διεύθυνση URL.", - "smtp-protocol": "Πρωτόκολλο SMTP", - "smtp-host": "SMTP host", - "smtp-host-required": "Απαιτείται ορισμός SMTP host.", - "smtp-port": "Θύρα SMTP", - "smtp-port-required": "Πρέπει να εισάγετε SMTP port.", - "smtp-port-invalid": "Αυτή δε φαίνεται να είναι μια έγκυρη SMTP port.", - "timeout-msec": "Timeout (msec)", - "timeout-required": "Απαιτείται τιμή Timeout.", - "timeout-invalid": "Αυτή δε φαίνεται να είναι μια έγκυρη τιμή timeout.", - "enable-tls": "Ενεργοποίηση TLS", - "tls-version": "Έκδοση TLS", - "send-test-mail": "Αποστολή δοκιμαστικού μηνύματος", - "use-system-mail-settings": "Χρήση των ρυθμίσεων διακομιστή αλληλογραφίας συστήματος", - "mail-templates": "Πρότυπα αλληλογραφίας", - "mail-template-settings": "Ρυθμίσεις προτύπων αλληλογραφίας", - "use-system-mail-template-settings": "Χρήση προτύπων ηλεκτρονικού ταχυδρομείου συστήματος", - "mail-template": { - "mail-template": "Πρότυπο αλληλογραφίας", - "test": "Δοκιμαστικό μήνυμα ηλεκτρονικού ταχυδρομείου", - "activation": "Μήνυμα ενεργοποίησης λογαριασμού", - "account-activated": "Μήνυμα επιτυχούς ενεργοποίησης λογαριασμού", - "reset-password": "Μήνυμα επαναφοράς κωδικού πρόσβασης", - "password-was-reset": "Μήνυμα επαναφοράς κωδικού πρόσβασης", - "user-activated": "Μήνυμα ενεργοποίησης χρήστη", - "user-registered": "Μήνυμα καταχώρησης χρήστη" - }, - "mail-subject": "Θέμα μηνύματος (subject)", - "mail-body": "Κυρίως μήνυμα (body)" - }, - "alarm": { - "alarm": "Alarm", - "alarms": "Alarms", - "select-alarm": "Επιλογή alarm", - "no-alarms-matching": "Δεν βρέθηκαν alarms σχετικά με '{{entity}}'.", - "alarm-required": "Απαιτείται Alarm", - "alarm-status": "Κατάσταση Alarm", - "search-status": { - "ANY": "'Ολες", - "ACTIVE": "Ενεργό", - "CLEARED": "Εκκαθαρίστηκε", - "ACK": "Επιβεβαιώθηκε", - "UNACK": "Χωρίς επιβεβαίωση" - }, - "display-status": { - "ACTIVE_UNACK": "Ενεργό χωρίς επιβεβαίωση", - "ACTIVE_ACK": "Ενεργό επιβεβαιωμένο", - "CLEARED_UNACK": "Εκκαθαρίστηκε χωρίς επιβεβαίωση", - "CLEARED_ACK": "Εκκαθαρίστηκε επιβεβαιωμένο" - }, - "no-alarms-prompt": "Δεν βρέθηκαν alarm", - "created-time": "Ώρα δημιουργίας", - "type": "Τύπος", - "severity": "Βαρύτητα", - "originator": "Δημιουργός", - "originator-type": "Τύπος δημιουργού", - "details": "Λεπτομέρειες", - "status": "Κατάσταση", - "alarm-details": "Λεπτομέρειες Alarm", - "start-time": "Ώρα έναρξης", - "end-time": "Ώρα λήξης", - "ack-time": "Ώρα επιβεβαίωσης", - "clear-time": "Ώρα εκκαθάρισης", - "severity-critical": "Critical", - "severity-major": "Κρίσιμο", - "severity-minor": "Ασήμαντο", - "severity-warning": "Προειδοποίηση", - "severity-indeterminate": "Απροσδιόριστο", - "acknowledge": "Επιβεβαίωση", - "clear": "Εκκαθάριση", - "search": "Αναζήτηση alarm", - "selected-alarms": "{ count, plural, 1 {1 alarm} other {# alarms} } επιλέχθηκαν", - "no-data": "Δεν υπάρχουν δεδομένα για εμφάνιση", - "polling-interval": "Διάστημα δειγματοληψίας alarm(sec)", - "polling-interval-required": "Απαιτείται ορισμός διαστήματος δειγματοληψίας alarm.", - "min-polling-interval-message": "Η ελάχιστη επιτρεπόμενη τιμή διαστήματος δειγματοληψίας alarm είναι 1 sec.", - "aknowledge-alarms-title": "Επιβεβαίωση { count, plural, 1 {1 alarm} other {# alarms} }", - "aknowledge-alarms-text": "Είστε σίγουρος ότι θέλετε να επιβεβαιώσετε { count, plural, 1 {1 alarm} other {# alarms} };", - "aknowledge-alarm-title": "Επιβεβαίωση Alarm", - "aknowledge-alarm-text": "Είστε σίγουρος ότι θέλετε να επιβεβαιώσετε το Alarm?", - "clear-alarms-title": "Εκκαθάριση { count, plural, 1 {1 alarm} other {# alarms} }", - "clear-alarms-text": "Είστε σίγουρος ότι θέλετε να εκκαθαρίσετε { count, plural, 1 {1 alarm} other {# alarms} }?", - "clear-alarm-title": "Εκκαθάριση Alarm", - "clear-alarm-text": "Είστε σίγουρος ότι θέλετε να εκκαθαρίσετε το Alarm?", - "alarm-status-filter": "Φίλτρο κατάστασης Alarm" - }, - "alias": { - "add": "Προσθήκη ψευδωνύμου", - "edit": "Επεξεργασία ψευδωνύμου", - "name": "Ψευδώνυμο", - "name-required": "Απαιτείται Ψευδώνυμο", - "duplicate-alias": "Το ψευδώνυμο υπάρχει ήδη.", - "filter-type-single-entity": "Απλή Οντότητα", - "filter-type-entity-group": "Ομάδα Οντοτήτων", - "filter-type-entity-list": "Λίστα Οντοτήτων", - "filter-type-entity-name": "Όνομα Οντότητας", - "filter-type-entity-group-list": "Λίστα ομάδας Οντοτήτων", - "filter-type-entity-group-name": "Όνομα ομάδας Οντοτήτων", - "filter-type-state-entity": "Οντότητα από την κατάσταση του dashboard", - "filter-type-state-entity-description": "Οντότητα που λαμβάνεται από τις παραμέτρους κατάστασης του dashboard", - "filter-type-asset-type": "Τύπος Asset", - "filter-type-asset-type-description": "Assets του τύπου '{{assetType}}'", - "filter-type-asset-type-and-name-description": "Assets του τύπου '{{assetType}}' με όνομα που αρχίζει από '{{prefix}}'", - "filter-type-device-type": "Τύπος Συσκευής", - "filter-type-device-type-description": "Συσκευές του τύπου '{{deviceType}}'", - "filter-type-device-type-and-name-description": "Συσκευές του τύπου '{{deviceType}}' με όνομα που αρχίζει από '{{prefix}}'", - "filter-type-entity-view-type": "Τύπος προβολής Οντοτήτων", - "filter-type-entity-view-type-description": "Τύπος προβολής Οντοτήτων '{{entityView}}'", - "filter-type-entity-view-type-and-name-description": "Τύπος προβολής Οντοτήτων '{{entityView}}' με όνομα που αρχίζει από '{{prefix}}'", - "filter-type-relations-query": "Ερώτημα Σχέσεων", - "filter-type-relations-query-description": "{{entities}} που έχουν σχέση {{relationType}} {{direction}} {{rootEntity}}", - "filter-type-asset-search-query": "Ερώτημα αναζήτησης Asset", - "filter-type-asset-search-query-description": "Asset με τύπο {{assetTypes}} που έχουν σχέση {{relationType}} {{direction}} {{rootEntity}}", - "filter-type-device-search-query": "Ερώτημα αναζήτησης Συσκευών", - "filter-type-device-search-query-description": "Συσκευές με τύπο {{deviceTypes}} που έχουν σχέση {{relationType}} {{direction}} {{rootEntity}}", - "filter-type-entity-view-search-query": "Ερώτημα αναζήτησης Όψεων Οντοτήτων", - "filter-type-entity-view-search-query-description": "Όψεις Οντοτήτων με τύπο {{entityViewTypes}} που έχουν σχέση {{relationType}} {{direction}} {{rootEntity}}", - "entity-filter": "Φίλτρο Οντοτήτων", - "resolve-multiple": "Διακανονισμός ως πολλαπλές οντότητες", - "filter-type": "Είδος Φίλτρου", - "filter-type-required": "Απαιτείται καθορισμός του Είδους Φίλτρου.", - "entity-filter-no-entity-matched": "Δεν βρέθηκαν οντότητες που ταιριάζουν με το συγκεκριμένο φίλτρο.", - "no-entity-filter-specified": "Δεν έχει οριστεί Φίλτρο Οντοτήτων", - "root-state-entity": "Χρήση της οντότητας κατάστασης του dashboard ως ρίζα", - "group-state-entity": "Χρήση της οντότητας κατάστασης του dashboard ως ομάδας οντοτήτων", - "root-entity": "Οντότητα ρίζας", - "state-entity-parameter-name": "Όνομα παραμέτρου οντότητας κατάστασης", - "default-state-entity": "Προεπιλεγμένη οντότητα κατάστασης", - "default-state-entity-group": "Προεπιλεγμένη ομάδα καταστάσεων οντοτήτων", - "default-entity-parameter-name": "Εξ ορισμού", - "max-relation-level": "Μέγιστος βαθμός συσχέτισης", - "unlimited-level": "Απεριόριστος βαθμός", - "state-entity": "Οντότητα κατάστασης του Dashboard", - "entities-of-group-state-entity": "Οντότητες από την ομάδα οντοτήτων κατάστασης του dashboard", - "all-entities": "Όλες οι Οντότητες", - "any-relation": "Οποιαδήποτε" - }, - "asset": { - "asset": "Asset", - "assets": "Assets", - "management": "Διαχείριση Asset", - "view-assets": "Προβολή Asset", - "add": "Προσθήκη Asset", - "assign-to-customer": "Ανάθεση σε Πελάτη", - "assign-asset-to-customer": "Ανάθεση Asset(s) σε Πελάτη", - "assign-asset-to-customer-text": "Επιλέξτε τα Asset που θα αναθέσετε στον πελάτη", - "no-assets-text": "Δεν βρέθηκαν Asset", - "assign-to-customer-text": "Παρακαλώ επιλέξτε πελάτη για να αναθέσετε το Asset(s)", - "public": "Δημόσιο", - "assignedToCustomer": "Συνδεδεμένο με πελάτη", - "make-public": "Κάνε το Asset δημόσιο", - "make-private": "Κάνε το Asset ιδιωτικό", - "unassign-from-customer": "Αποσύνδεση από τον πελάτη", - "delete": "Διαγραφή Αsset", - "asset-public": "Το Asset είναι δημόσιο", - "asset-type": "Τύπος Asset", - "asset-type-required": "Απαιτείται ορισμός είδους Asset.", - "select-asset-type": "Επιλογή τύπου Αsset", - "enter-asset-type": "Εισαγωγή τύπου Αsset", - "any-asset": "Οποιοδήποτε Αsset", - "no-asset-types-matching": "Δεν βρέθηκαν τύποι Asset που να τιαιριάζουν με '{{entitySubtype}}'.", - "asset-type-list-empty": "Δεν επιλέχθηκε τύπος Asset.", - "asset-types": "Τύποι Asset", - "name": "Όνομα", - "name-required": "Απαιτείται Όνομα.", - "description": "Περιγραφή", - "type": "Τύπος", - "type-required": "Απαιτείται Τύπος.", - "details": "Λεπτομέρειες", - "events": "Γεγονότα", - "add-asset-text": "Προσθήκη νέου Asset", - "asset-details": "Λεπτομέρειες Asset", - "assign-assets": "Ανάθεση Assets", - "assign-assets-text": "Ανάθεση { count, plural, 1 {1 asset} other {# assets} } σε πελάτη", - "delete-assets": "Διαγραφή Assets", - "unassign-assets": "Αποσύνδεση Assets", - "unassign-assets-action-title": "Αποσύνδεση { count, plural, 1 {1 asset} other {# assets} } από πελάτη", - "assign-new-asset": "Ανάθεση νέου Asset", - "delete-asset-title": "Είστε βέβαιοι ότι θέλετε να διαγράψετε το Asset '{{assetName}}'?", - "delete-asset-text": "Προσοχή!, Μετά την επιβεβαίωση, το Asset και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "delete-assets-title": "Είστε βέβαιοι ότι θέλετε να διαγράψετε { count, plural, 1 {1 asset} other {# assets} };", - "delete-assets-action-title": "Διαγραφή { count, plural, 1 {1 asset} other {# assets} }", - "delete-assets-text": "Προσοχή! Μετά την επιβεβαίωση, όλα τα επιλεγμένα Asset και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "make-public-asset-title": "Είστε βέβαιοι ότι θέλετε να κάνετε το Asset '{{assetName}}' δημόσιο;", - "make-public-asset-text": "Μετά την επιβεβαίωση, το Asset και όλα τα στοιχεία του θα είναι προσβάσιμα από άλλους.", - "make-private-asset-title": "Είστε βέβαιοι ότι θέλετε να κάνετε το Asset '{{assetName}}' ιδιωτικό;", - "make-private-asset-text": "Μετά την επιβεβαίωση, το Asset και όλα τα στοιχεία του θα είναι ιδιωτικά και μη προσβάσιμα από άλλους.", - "unassign-asset-title": "Είστε βέβαιοι ότι θέλετε να αποσυνδέσετε το Asset '{{assetName}}';", - "unassign-asset-text": "Μετά την επιβεβαίωση, το Asset θα αποσυνδεθεί και δεν θα είναι προσβάσιμο από τον πελάτη.", - "unassign-asset": "Αποσύνδεση Asset", - "unassign-assets-title": "Είστε βέβαιοι ότι θέλετε να αποσυνδέσετε { count, plural, 1 {1 asset} other {# assets} };", - "unassign-assets-text": "Μετά την επιβεβαίωση, όλα τα επιλεγμένα Asset θα αποσυνδεθούν και δεν θα είναι προσβάσιμα από τον πελάτη.", - "copyId": "Αντιγραφή Asset Id", - "idCopiedMessage": "Το Asset Id αντιγράφηκε στο πρόχειρο", - "select-asset": "Επιλογή Asset", - "no-assets-matching": "Δεν βρέθηκαν Asset που να ταιριάζουν με '{{entity}}'.", - "asset-required": "Απαιτείται Asset", - "name-starts-with": "Όνομα Asset που ξεκινάει με", - "selected-assets": "{ count, plural, 1 {1 asset} other {# assets} } επιλέχθηκαν", - "search": "Αναζήτηση Asset", - "select-group-to-add": "Επιλέξτε ομάδα στόχο για να προσθέσετε επιλεγμένα Asset", - "select-group-to-move": "Επιλέξτε ομάδα στόχο για να μετακινήσετε επιλεγμένα assets", - "remove-assets-from-group": "Είστε βέβαιοι ότι θέλετε να καταργήσετε { count, plural, 1 {1 asset} other {# assets} } από την ομάδα '{entityGroup}';", - "group": "Ομάδα Asset", - "list-of-groups": "{ count, plural, 1 {One asset group} other {List of # asset groups} }", - "group-name-starts-with": "Ομάδες Asset των οποίων τα ονόματα ξεκινούν με '{{prefix}}'", - "import": "Εισαγωγή Asset", - "asset-file": "Αρχείο Asset" - }, - "attribute": { - "attributes": "Χαρακτηριστικά", - "latest-telemetry": "Τελευταία τηλεμετρία", - "attributes-scope": "Πεδίο εφαρμογής Χαρακτηριστικών Οντότητας", - "scope-latest-telemetry": "Τελευταία τηλεμετρία", - "scope-client": "Χαρακτηριστικά Client", - "scope-server": "Χαρακτηριστικά Server", - "scope-shared": "Κοινόχρηστα Χαρακτηριστικά", - "add": "Προσθήκη Χαρακτηριστικού", - "add-attribute-prompt": "Προσθέστε Χαρακτηριστικό", - "key": "Όνομα", - "last-update-time": "Ώρα τελευταίας ενημέρωσης", - "key-required": "Απαιτείται μεταβλητή χαρακτηριστικού.", - "value": "Τιμή", - "value-required": "Απαιτείται ορισμός τιμής χαρακτηριστικού.", - "delete-attributes-title": "Είστε βέβαιοι ότι θέλετε να διαγράψετε { count, plural, 1 {1 attribute} other {# attributes} };", - "delete-attributes-text": "Προσοχή! Μετά την επιβεβαίωση θα αφαιρεθούν όλα τα επιλεγμένα χαρακτηριστικά.", - "delete-attributes": "Διαγραφή χαρακτηριστικών", - "enter-attribute-value": "Καταχωρίστε την τιμή του χαρακτηριστικού", - "show-on-widget": "Εμφάνιση σε widget", - "widget-mode": "Είδος λειτουργίας widget", - "next-widget": "Επόμενο widget", - "prev-widget": "Προηγούμενο widget", - "add-to-dashboard": "Προσθήκη σε dashboard", - "add-widget-to-dashboard": "Προσθήκη widget στο dashboard", - "selected-attributes": "{ count, plural, 1 {1 attribute} other {# attributes} } επιλέχθηκαν", - "selected-telemetry": "{ count, plural, 1 {1 telemetry unit} other {# telemetry units} } επιλέχθηκαν" - }, - "audit-log": { - "audit": "Καταγραφή", - "audit-logs": "Ημερολόγια Καταγραφής", - "timestamp": "Ώρα", - "entity-type": "Είδος Οντότητας", - "entity-name": "Όνομα Οντότητας", - "user": "Χρήστης", - "type": "Τύπος", - "status": "Κατάσταση", - "details": "Λεπτομέρειες", - "type-added": "Προστέθηκε", - "type-deleted": "Διαγράφηκε", - "type-updated": "Ενημερώθηκε", - "type-attributes-updated": "Τα χαρακτηριστικά ενημερώθηκαν", - "type-attributes-deleted": "Τα χαρακτηριστικά διαγράφηκαν", - "type-rpc-call": "Κλήση RPC", - "type-credentials-updated": "Τα διαπιστευτήρια ενημερώθηκαν", - "type-assigned-to-customer": "Ανατέθηκε σε Πελάτη", - "type-unassigned-from-customer": "Αποσυνδέθηκε από Πελάτη", - "type-activated": "Ενεργοποιήθηκε", - "type-suspended": "Μπήκε σε αναστολή", - "type-credentials-read": "Τα διαπιστευτήρια διαβάστηκαν", - "type-attributes-read": "Τα χαρακτηριστικά διαβάστηκαν", - "type-added-to-entity-group": "Προστέθηκε στην ομάδα", - "type-removed-from-entity-group": "Αφαιρέθηκε από την ομάδα", - "type-relation-add-or-update": "Relation updated", - "type-relation-delete": "Η συσχέτιση ενημερώθηκε", - "type-relations-delete": "Όλες οι συσχετίσεις διαγράφηκαν", - "type-alarm-ack": "Επιβεβαιώθηκε", - "type-alarm-clear": "Εκκαθαρίστηκε", - "type-rest-api-rule-engine-call": "Κλήση Rule engine REST API", - "type-made-public": "Έγινε δημόσιο", - "type-made-private": "Έγινε ιδιωτικό", - "status-success": "Επιτυχία", - "status-failure": "Αποτυχία", - "audit-log-details": "΄Λεπτομέρειες καταγραφής", - "no-audit-logs-prompt": "Δεν βρέθηκαν αρχεία καταγραφής", - "action-data": "Δεδομένα ενεργειών", - "failure-details": "Λεπτομέρειες αποτυχίας", - "search": "Αναζήτηση αρχείων καταγραφής", - "clear-search": "΄Καθαρισμός αναζήτησης" - }, - "confirm-on-exit": { - "message": "Έχετε μη αποθηκευμένες αλλαγές. Είστε βέβαιοι ότι θέλετε να φύγετε από τη σελίδα;", - "html-message": "Έχετε μη αποθηκευμένες αλλαγές.
    Είστε βέβαιοι ότι θέλετε να φύγετε από τη σελίδα;", - "title": "Μη αποθηκευμένες αλλαγές" - }, - "contact": { - "country": "Χώρα", - "city": "Πόλη", - "state": "Περιφέρεια", - "postal-code": "Ταχυδρομικός Κώδικας", - "postal-code-invalid": "Μη έγκυρη μορφή ταχυδρομικού κώδικα.", - "address": "Διεύθυνση (γραμμή 1)", - "address2": "Διεύθυνση (γραμμή 2)", - "phone": "Τηλέφωνο", - "email": "Email", - "no-address": "Χωρίς διεύθυνση" - }, - "common": { - "username": "Όνομα χρήστη", - "password": "Κωδικός πρόσβασης", - "enter-username": "Εισάγετε Όνομα χρήστη", - "enter-password": "Εισάγετε Κωδικό πρόσβασης", - "enter-search": "Αναζήτηση" - }, - "converter": { - "converter": "Μετατροπέας δεδομένων", - "converters": "Μετατροπείς δεδομένων", - "select-converter": "Επιλογή μετατροπέα δεδομένων", - "no-converters-matching": "Δεν βρέθηκαν μετατροπείς δεδομένων για '{{entity}}'.", - "converter-required": "Απαιτείται ορισμός μετατροπέα δεδομένων", - "delete": "Διαγραφή μετατροπέα", - "management": "Διαχείριση μετατροπέων δεδομένων", - "add-converter-text": "Προσθήκη νέου μετατροπέα δεδομένων", - "no-converters-text": "Δεν βρέθηκαν μετατροπείς δεδομένων", - "selected-converters": "{ count, plural, 1 {1 data converter} other {# data converters} } επιλέχθηκαν", - "delete-converter-title": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τον μετατροπέα δεδομένων '{{converterName}}'?", - "delete-converter-text": "Προσέξτε, μετά την επιβεβαίωση, ο μετατροπέας δεδομένων και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "delete-converters-title": "Είστε βέβαιοι ότι θέλετε να διαγράψετε { count, plural, 1 {1 data converter} other {# data converters} };", - "delete-converters-action-title": "Διαγραφή { count, plural, 1 {1 data converter} other {# data converters} }", - "delete-converters-text": "Προσοχή! Μετά την επιβεβαίωση, όλοι οι επιλεγμένοι μετατροπείς δεδομένων και όλα τα σχετικά δεδομένα θα καταστούν μη ανακτήσιμα.", - "events": "Γεγονότα", - "add": "Προσθήκη μετατροπέα δεδομένων", - "converter-details": "Λεπτομέρειες μετατροπέα δεδομένων", - "details": "Λεπτομέρειες", - "copyId": "Αντιγραφή ID μετατροπέα δεδομένων", - "idCopiedMessage": "Το ID του μετατροπέα δεδομένων αντιγράφηκε στο πρόχειρο.", - "debug-mode": "Λειτουργία εντοπισμού σφαλμάτων", - "name": "Όνομα", - "name-required": "Απαιτείται Όνομα.", - "description": "Περιγραφή", - "decoder": "Αποκωδικοποιητής", - "encoder": "Kωδικοποιητής", - "test-decoder-fuction": "Δοκιμή λειτουργίας αποκωδικοποιητή", - "test-encoder-fuction": "Δοκιμή λειτουργίας κωδικοποιητή", - "decoder-input-params": "Είσοδο παραμέτρων αποκωδικοποιητή", - "encoder-input-params": "Είσοδο παραμέτρων κωδικοποιητή", - "payload": "Φορτίο", - "payload-content-type": "Τύπος περιεχόμενου φορτίου", - "payload-content": "Περιεχόμενο φορτίο", - "message": "Μήνυμα", - "message-type": "Τύπος μηνύματος", - "message-type-required": "Απαιτείται τύπος μηνύματος", - "test": "Δοκιμή", - "metadata": "Μεταδεδομένα", - "metadata-required": "Οι καταχωρίσεις μεταδεδομένων δεν μπορούν να είναι κενές.", - "integration-metadata": "Μεταδεδομένα ενσωμάτωσης", - "integration-metadata-required": "Οι καταχωρήσεις μεταδεδομένων ενσωμάτωσης δεν μπορούν να είναι κενές.", - "output": "Έξοδος", - "import": "Εισαγωγή μετατροπέα", - "export": "Εξαγωγή μετατροπέα", - "export-failed-error": "Δεν είναι δυνατή η εξαγωγή του μετατροπέα: {{error}}", - "create-new-converter": "Δημιουργία νέου μετατροπέα", - "converter-file": "Αρχείο μετατροπέα", - "invalid-converter-file-error": "Δεν είναι δυνατή η εισαγωγή του μετατροπέα: Μη έγκυρη δομή δεδομένων μετατροπέα.", - "type": "Τύπος", - "type-required": "Απαιτείται τύπος.", - "type-uplink": "Uplink", - "type-downlink": "Downlink" - }, - "content-type": { - "json": "Json", - "text": "Text", - "binary": "Binary (Base64)" - }, - "customer": { - "customer": "Πελάτης", - "customers": "Πελάτες", - "management": "Διαχείριση Πελατών", - "dashboard": "Dashboard Πελάτη", - "dashboards": "Dashboards Πελάτη", - "devices": "Συσκευές Πελάτη", - "entity-views": "Προβολές Οντοτήτων ΠελάτηCustomer Entity Views", - "assets": "Assets Πελάτη", - "public-dashboards": "Δημόσια Dashboards", - "public-devices": "Δημόσιες Devices", - "public-assets": "Δημόσια Assets", - "public-entity-views": "Δημόσιες Προβολές Οντοτήτων", - "add": "Προσθήκη Πελάτη", - "delete": "Διαγραφή πελάτη", - "manage-customer-user-groups": "Διαχείριση ομάδων χρηστών πελατών", - "manage-customer-groups": "Διαχείριση ομάδων πελατών", - "manage-customer-device-groups": "Διαχείριση ομάδων συσκευών πελατών", - "manage-customer-asset-groups": "Διαχείριση ομάδων Asset πελατών", - "manage-customer-entity-view-groups": "Διαχείριση ομάδων προβολής οντότητας πελατών", - "manage-customer-dashboard-groups": "Διαχείριση ομάδων dashboard πελατών", - "manage-customer-users": "Διαχείριση χρηστών πελατών", - "manage-customers": "Διαχείριση πελατών", - "manage-customer-devices": "Διαχείρηση συσκευών πελατών", - "manage-customer-entity-views": "Διαχείριση προβολής οντότητας πελατών", - "manage-customer-dashboards": "Διαχείριση dashboards πελατών", - "manage-public-devices": "Διαχείριση δημόσιων συσκευών", - "manage-public-dashboards": "Διαχείριση δημόσιων συσκευών", - "manage-customer-assets": "Διαχείριση οντοτήτων πελατών", - "manage-public-assets": "Διαχείριση δημόσιων οντοτήτων", - "add-customer-text": "Προσθήκη νέου πελάτη", - "no-customers-text": "Δεν βρέθηκαν πελάτες", - "customer-details": "Λεπτομέρειες πελάτη", - "delete-customer-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε τον πελάτη '{{customerTitle}}'?", - "delete-customer-text": "Προσέξτε, μετά την επιβεβαίωση, ο πελάτης και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "delete-customers-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 customer} other {# customers} }?", - "delete-customers-action-title": "Διαγραφή { count, plural, 1 {1 customer} other {# customers} }", - "delete-customers-text": "Προσέξτε, μετά την επιβεβαίωση, οι πελάτες και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "manage-user-groups": "Διαχείρηση ομαδών χρηστών", - "manage-asset-groups": "Διαχείρηση ομαδών οντοτήτων", - "manage-device-groups": "Διαχείρηση ομαδών συσκευών", - "manage-dashboard-groups": "Διαχείρηση ομαδών dashboards", - "manage-entity-view-groups": "Διαχείρηση ομαδών προβολής οντοτήτων", - "manage-users": "Διαχείρηση χρηστών", - "manage-assets": "Διαχείρηση οντοτήτων", - "manage-devices": "Διαχείρηση συσκευών", - "manage-dashboards": "Διαχείρηση dashboards", - "title": "Τίτλος", - "title-required": "Απαιτείται ένας τίτλος.", - "description": "Περιγραφή", - "details": "Λεπτομέρειες", - "events": "Γεγονότα", - "copyId": "Αντιγραφή ID πελάτη", - "idCopiedMessage": "Το ID του πελάτη έχει αντιγραφεί στο πρόχειρο", - "select-customer": "Επιλέξτε πελάτη", - "no-customers-matching": "Δεν βρέθηκαν πελάτες που να αντιστοιχούν σε '{{entity}}'.", - "customer-required": "Απαιτείται πελάτης", - "selected-customers": "{ count, plural, 1 {1 customer} other {# customers} } επιλέχθηκαν", - "search": "Αναζήτηση πελατών", - "select-group-to-add": "Επιλέξτε ομάδα για να προσθέσετε τους επιλεγμένους πελάτες", - "select-group-to-move": "Επιλέξτε ομάδα για να μετακινήσετε τους επιλεγμένους πελάτες", - "remove-customers-from-group": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε { count, plural, 1 {1 customer} other {# customers} } από την ομάδα '{entityGroup}'?", - "group": "Ομάδα πελατών", - "list-of-groups": "{ count, plural, 1 {One customer group} other {List of # customer groups} }", - "group-name-starts-with": "Πελάτες των οποίων το όνομα αρχίζει από '{{prefix}}'", - "select-default-customer": "Επιλογή προεπιλεγμένου πελάτη", - "default-customer": "Προεπιλεγμένος πελάτης", - "default-customer-required": "Ο προεπιλεγμένος πελάτης είναι υποχρεωτικός για να είναι δυνατή η απασφαλμάτωση του dashboard από τον Tenant", - "allow-white-labeling": "Επιτρέψτε το White Labeling" - }, - "customers-hierarchy": { - "customers-hierarchy": "Ιεραρχία Πελατών", - "open-nav-tree": "Άνοιγμα διακλάδωσης", - "return-to-top-level": "Επιστροφή στο αρχικό επίπεδο" - }, - "custom-menu": { - "custom-menu": "Προσαρμοσμένο Μενού", - "custom-menu-hint": "Ορίστε το προσαρμοσμένο μενού (JSON) παρακάτω. Αυτό το JSON περιέχει μια λίστα με προσαρμοσμένα στοιχεία μενού." - }, - "custom-translation": { - "custom-translation": "Μεταφράσεις", - "translation-map": "Χάρτης μετάφρασης", - "key": "Κλειδί", - "import": "Εισαγωγή μετάφρασης", - "export": "Εξαγωγή μετάφρασης", - "export-data": "Εξαγωγή δεδομένων μετάφρασης", - "import-data": "Εισαγωγή δεδομένων μετάφρασης", - "translation-file": "Αρχείο μετάφρασης", - "invalid-translation-file-error": "Δεν είναι δυνατή η εισαγωγή αρχείου μετάφρασης: Μη έγκυρη δομή δεδομένων μετάφρασης.", - "custom-translation-hint": "Καθορίστε την προσαρμοσμένη μετάφραση (JSON) παρακάτω. Αυτό το JSON θα αντικαταστήσει την προεπιλεγμένη μετάφραση. Κάνετε Λήψη αρχείου γλώσσας για να λάβετε την υπάρχουσα μετάφραση. Μπορείτε επίσης να χρησιμοποιήσετε το ληφθέν αρχείο ως αναφορά στα διαθέσιμα ζεύγη κλειδιών-τιμών μετάφρασης.", - "download-locale-file": "Λήψη αρχείου γλώσσας" - }, - "datetime": { - "date-from": "Ημ/νία από", - "time-from": "Ώρα από", - "date-to": "Ημ/νία έως", - "time-to": "Ώρα έως" - }, - "dashboard": { - "dashboard": "Dashboard", - "dashboards": "Dashboards", - "management": "Διαχείριση Dashboard", - "view-dashboards": "Προβολή Dashboards", - "add": "Προσθήκη Dashboard", - "assign-dashboard-to-customer": "Ανάθεση Dashboard(s) Σε Πελάτη", - "assign-dashboard-to-customer-text": "Παρακαλώ επιλέξτε τα dashboards που θέλετε να αναθέσετε σε πελάτη", - "assign-to-customer-text": "Παρακαλώ επιλέξτε τον πελάτη στον οποίο θέλετε να αναθέσετε το/τα dashboard(s)", - "assign-to-customer": "Ανάθεση σε πελάτη", - "unassign-from-customer": "Αφαίρεση από πελάτη", - "make-public": "Δημοσιοποιήση Dashboard", - "make-private": "Ιδιωτικοποίηση Dashboard", - "manage-assigned-customers": "Διαχείρηση ανατεθειμένων πελατών", - "assigned-customers": "Ανατεθειμένοι πελάτες", - "assign-to-customers": "Ανάθεση Dashboard(s) Σε Πελάτες", - "assign-to-customers-text": "Παρακαλώ επιλέξτε τους πελάτες στους οποίους θέλετε να αναθέσετε το/τα dashboard(s)", - "unassign-from-customers": "Μη Ανατεθειμένα Dashboard(s) Από Πελάτες", - "unassign-from-customers-text": "Παρακαλώ επιλέξτε τους πελάτες τους οποίους θέλετε να αφαιρέσετε από το/τα dashboard(s)", - "no-dashboards-text": "Δεν βρέθηκαν dashboards", - "no-widgets": "Δεν έχουν ρυθμιστεί widgets", - "add-widget": "Προσθέστε νέο widget", - "title": "Τίτλος", - "select-widget-title": "Επιλογή widget", - "select-widget-subtitle": "Λίστα με τους διαθέσιμους τύπους widget", - "delete": "Διαγραφή dashboard", - "title-required": "Απαιτείται ένας τίτλος.", - "description": "Περιγραφή", - "details": "Λεπτομέρειες", - "dashboard-details": "Λεπτομέρειες Dashboard", - "add-dashboard-text": "Προσθέστε νέο dashboard", - "assign-dashboards": "Αναθέστε dashboards", - "assign-new-dashboard": "Ανάθεση νέου dashboard", - "assign-dashboards-text": "Ανάθεση { count, plural, 1 {1 dashboard} other {# dashboards} } σε πελάτες", - "unassign-dashboards-action-text": "Μη ανατεθειμένο/α { count, plural, 1 {1 dashboard} other {# dashboards} } από πελάτες", - "delete-dashboards": "Διαγραφή dashboards", - "unassign-dashboards": "Αφαίρεση dashboards", - "unassign-dashboards-action-title": "Αφαίρεση { count, plural, 1 {1 dashboard} other {# dashboards} } από πελάτη", - "delete-dashboard-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε το dashboard '{{dashboardTitle}}'?", - "delete-dashboard-text": "Προσοχή, μετά την επιβεβαίωση, το dashboard και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "delete-dashboards-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 dashboard} other {# dashboards} }?", - "delete-dashboards-action-title": "Διααγραφή { count, plural, 1 {1 dashboard} other {# dashboards} }", - "delete-dashboards-text": "Προσοχή, μετά την επιβεβαίωση, όλα τα επιλεγμένα dashboards και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "unassign-dashboard-title": "Είστε σίγουρου ότι θέλετε να αφαιρέσετε το dashboard '{{dashboardTitle}}'?", - "unassign-dashboard-text": "Μετά την επιβεβαίωση το dashboard θα αφαιρεθεί και δεν θα είναι διαθέσιμο στον πελάτη.", - "unassign-dashboard": "Αφαίρεση dashboard", - "unassign-dashboards-title": "Είστε σίγουρου ότι θέλετε να αφαιρέσετε { count, plural, 1 {1 dashboard} other {# dashboards} }?", - "unassign-dashboards-text": "Μετά την επιβεβαίωση όλα τα επιλεγμένα dashboards θα αφαιρεθούν και δεν θα είναι διαθέσιμα στον πελάτη.", - "public-dashboard-title": "Το dashboard είναι δημόσιο", - "public-dashboard-text": "Το dashboard {{dashboardTitle}} είναι δημόσιο και διαθέσιμο μέσω του παρακάτω συνδέσμου:", - "public-dashboard-notice": "Σημείωση: Μην ξεχνάτε να κάνετε δημόσιες τις συσκευές που συσχετίζονται ώστε να είναι δυνατή η πρόσβαση στα δεδομένα τους.", - "public-dashboard-link": "Σύνδεσμος δημόσιου dashboard", - "public-dashboard-link-text": "Το δημόσιο dashboard {{dashboardTitle}} είναι πλέον διαθέσιμο μέσω μέσω του παρακάτω συνδέσμου:", - "public-dashboard-link-notice": "Σημείωση: Μην ξεχνάτε να κάνετε δημόσιες τις συσκευές, τα assets και την προβολή οντοτήτων που συσχετίζονται ώστε να είναι δυνατή η πρόσβαση στα δεδομένα τους.", - "make-private-dashboard-title": "Είστε σίγουροι ότι θέλετε να κάνετε το dashboard '{{dashboardTitle}}' ιδιωτικό", - "make-private-dashboard-text": "Μετά την επιβεβαίωση το dashboard θα γίνουν ιδιωτικά και δεν θα είναι διαθέσιμα από τρίτους.", - "make-private-dashboard": "Κάνε το dashboard ιδιωτικό", - "socialshare-text": "'{{dashboardTitle}}' powered by EyeTech", - "socialshare-title": "'{{dashboardTitle}}' powered by EyeTech", - "select-dashboard": "Επιλογή dashboard", - "no-dashboards-matching": "Δεν βρέθηκαν dashboards που να αντιστοιχούν σε '{{entity}}'.", - "dashboard-required": "Απαιτείται dashboard.", - "select-existing": "Επιλέξτε ένα υπάρχον dashboard", - "create-new": "Δημιουργία νέου dashboard", - "new-dashboard-title": "Τίτλος νέου dashboard", - "open-dashboard": "Άνοιγμα dashboard", - "set-background": "Ορίστε φόντο", - "background-color": "χρώμα φόντου", - "background-image": "εικόνα φόντου", - "background-size-mode": "μέγεθος φόντου", - "no-image": "Δεν επιλέχθηκε εικόνα", - "drop-image": "Ρίξτε εδώ μια εικόνα ή κάνετε κλικ για να επιλέξετε ένα αρχείο για να ανεβεί.", - "settings": "Ρυθμίσεις", - "columns-count": "Αρίθμιση στηλών", - "columns-count-required": "Απαιτείται η αρίθμιση στηλών.", - "min-columns-count-message": "Ελάχιστος αριθμός στηλών είναι 10.", - "max-columns-count-message": "Μέγιστος αριθμός στηλών είναι 1000.", - "widgets-margins": "Περιθώριο μεταξύ widgets", - "horizontal-margin": "Οριζόντιο περιθώριο", - "horizontal-margin-required": "Απαιτείται τιμή για το οριζόντιο περιθώριο.", - "min-horizontal-margin-message": "Ελάχιστη τιμή οριζόντιου περιθωρίου είναι 0.", - "max-horizontal-margin-message": "Μέγιστη τιμή οριζόντιου περιθωρίου είναι 50.", - "vertical-margin": "Κάθετο περιθώριο", - "vertical-margin-required": "Απαιτείται τιμή για το κάθετο περιθώριο", - "min-vertical-margin-message": "Ελάχιστη τιμή καθέτου περιθωρίου είναι 0.", - "max-vertical-margin-message": "Μέγιστη τιμή καθέτου περιθωρίου είναι 50.", - "autofill-height": "Αυτόματη συμπλήρωση ύψους διάταξης", - "mobile-layout": "Ρυθμίσεις διάταξης mobile", - "mobile-row-height": "ύψος γραμμής mobile, σε px", - "mobile-row-height-required": "Απαιτείται ύψος γραμμής mobile.", - "min-mobile-row-height-message": "Ελάχιστη τιμή ύψους γραμμής mobile είναι 5.", - "max-mobile-row-height-message": "Μέγιστη τιμή ύψους γραμμής mobile είναι 200.", - "display-title": "Τίτλος εμφάνισης dashboard", - "toolbar-always-open": "Γραμμή εργαλείων πάντα ανοιχτή", - "title-color": "Χρώμα τίτλου", - "display-dashboards-selection": "Εμφάνηση επιλογής dashboards", - "display-entities-selection": "Εμφάνιση επιλογής οντοτήτων", - "display-dashboard-timewindow": "Εμφάνιση χρονικού πλαισίου", - "display-dashboard-export": "Εμφάνιση εξαγωγής", - "import": "Εισαγωγή dashboard", - "export": "Εξαγωγή dashboard", - "export-failed-error": "Δεν ήταν δυνατή η εξαγωγή dashboard: {{error}}", - "export-pdf": "Εξαγωγή ως PDF", - "export-png": "Εξαγωγή ως PNG", - "export-jpg": "Εξαγωγή ως JPEG", - "export-json-config": "Εξαγωγή ρυθμίσεις JSON", - "download-dashboard-progress": "Δημιουργία dashboard {{reportType}} ...", - "create-new-dashboard": "Δημιουργία νέου dashboard", - "dashboard-file": "Αρχείο dashboard", - "invalid-dashboard-file-error": "Δεν ήταν δυνατή η εισαγωγή dashboard: Μη έγκυρη δομή δεδομένων dashboard.", - "dashboard-import-missing-aliases-title": "Διαμορφώστε τα ψευδώνυμα που χρησιμοποιούνται από το dashboard που εισαγάγατε", - "create-new-widget": "Δημιουργία νέου widget", - "import-widget": "Εισαγωγή widget", - "widget-file": "Αρχείο widget", - "invalid-widget-file-error": "Δεν ήταν δυνατή η εισαγωγή widget: Μη έγκυρη δομή δεδομένων dashboard.", - "widget-import-missing-aliases-title": "Διαμορφώστε τα ψευδώνυμα που χρησιμοποιούνται από το widget που εισαγάγατε", - "open-toolbar": "Άνοιγμα γραμμής εργαλείων dashboard", - "close-toolbar": "Κλείσιμο γραμμής εργαλείων", - "configuration-error": "Σφάλμα ρυθμίσεων", - "alias-resolution-error-title": "Σφάλμα ρυθμίσεων ψευδωνύμων dashboard", - "invalid-aliases-config": "Δεν βρέθηκε καμία συσκευή που να ταιριάζει με κάποιο από τα φίλτρα.
    Παρακαλούμε επικοινωνήστε με τον Διαχειριστή για την επίλυση του ζητήματος.", - "select-devices": "Επιλογή συσκευών", - "assignedToCustomer": "Ανατεθειμένο σε πελάτη", - "assignedToCustomers": "Ανατεθειμένο σε πελάτες", - "public": "Δημόσιο", - "public-link": "Δημόσιος σύνδεσμος", - "copy-public-link": "Αντιγραφή δημόσιου συνδέσμου", - "public-link-copied-message": "Ο δημόσιος σύνδεσμος έχει αντιγραφεί στο πρόχειρο", - "manage-states": "Διαχείρηση κατάστασης dashboard", - "states": "Κατάσταση dashboard", - "search-states": "Αναζήτηση σε κατάσταση dashboard", - "selected-states": "{ count, plural, 1 {1 dashboard state} other {# dashboard states} } επιλεγμένα", - "edit-state": "Επεξεργασία κατάστασης dashboard", - "delete-state": "Διαγραφή κατάστασης dashboard", - "add-state": "Προσθήκη κατάστασης dashboard", - "state": "Κατάσταση Dashboard", - "state-name": "Όνομα", - "state-name-required": "Απαιτείται όνομα κατάστασης dashboard.", - "state-id": "ID Κατάστασης", - "state-id-required": "Απαιτείται ID κατάστασης dashboard.", - "state-id-exists": "Υπάρχει ήδη κατάσταση dashboard με το ίδιο ID.", - "is-root-state": "Root state", - "delete-state-title": "Διαγραφή κατάστασης dashboard", - "delete-state-text": "Είστε σίγουροι ότι θέλετε να διαγράψετε την κατάσταση dashboard με όνομα '{{stateName}}'?", - "show-details": "Εμφάνιση λεπτομερειών", - "hide-details": "Απόκρυψη λεπτομερειών", - "select-state": "Επιλογή κατάστασης", - "state-controller": "Έλεγχος κατάστασης", - "selected-dashboards": "{ count, plural, 1 {1 dashboard} other {# dashboards} } επιλεγμένα", - "search": "Αναζήτηση dashboards", - "select-group-to-add": "Επιλογή ομάδας για να προστεθεί στα επιλεγμένα dashboards", - "select-group-to-move": "Επιλογή ομάδας για να μετακινηθεί στα επιλεγμένα dashboards", - "remove-dashboards-from-group": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε { count, plural, 1 {1 dashboard} other {# dashboards} } from group '{entityGroup}'?", - "group": "Ομάδα dashboards", - "list-of-groups": "{ count, plural, 1 {One dashboard group} other {List of # dashboard groups} }", - "group-name-starts-with": "Ομάδες dashboard των οποίων το όνομα αρχίζει από '{{prefix}}'" - }, - "datakey": { - "settings": "Ρυθμίσεις", - "advanced": "Προχωρημένες", - "label": "Ετικέτα", - "color": "Χρώμα", - "units": "Ειδικό σύμβολο που εμφανίζεται δίπλα από την τιμή", - "decimals": "Αριθμός ψηφίων μετά την υποδιαστολή", - "data-generation-func": "Λειτουργία δημιουργίας δεδομένων", - "use-data-post-processing-func": "Χρησιμοποιήστε τη λειτουργία μετα-επεξεργασίας δεδομένων", - "configuration": "Ρυθμίσεις ονομάτων δεδομένων", - "timeseries": "Χρονική σειρά", - "attributes": "Ιδιώτητες", - "alarm": "Πεδία alarm", - "timeseries-required": "Απαιτείται χρονική σειρά οντοτήτων.", - "timeseries-or-attributes-required": "Απαιτείται χρονική σειρά/ιδιώτητες οντοτήτων.", - "maximum-timeseries-or-attributes": "Μέγιστο { count, plural, 1 {1 timeseries/attribute is allowed.} other {# timeseries/attributes are allowed} }", - "alarm-fields-required": "Απαιτούνται πεδία alarm.", - "function-types": "Τύποι λειτουργιών", - "function-types-required": "Απαιτούνται τύποι λειτουργιών.", - "maximum-function-types": "Μέγιστο { count, plural, 1 {1 function type is allowed.} other {# function types are allowed} }", - "time-description": "χρονικό σημείο της συγκεκριμένης τιμής", - "value-description": "η συγκεκριμένη τιμή", - "prev-value-description": "αποτέλεσμα της προηγούμενης λειτουργίας", - "time-prev-description": "χρονσικό σημείο της προηγούμενης τιμής", - "prev-orig-value-description": "αρχική προηγούμενη τιμή" - }, - "datasource": { - "type": "Τύπος πηγής δεδομένων", - "name": "Όνομα", - "add-datasource-prompt": "Παρακαλούμε προσθέστε πηγή δεδομένων" - }, - "details": { - "details": "Λεπτομέρειες", - "edit-mode": "Λειτουργία Επεξεργασίας", - "toggle-edit-mode": "Εναλλαγή λειτουργίας επεξεργασίας" - }, - "device": { - "device": "Συσκευή", - "device-required": "Απαιτείται ορισμός συσκευής.", - "devices": "Συσκευές", - "management": "Διαχείριση Συσκευών", - "view-devices": "Προβολή Συσκευών", - "device-alias": "Ψευδώνυμο συσκευής", - "aliases": "Ψευδώνυμα συσκευής", - "no-alias-matching": "Δεν βρέθηκε '{{alias}}'.", - "no-aliases-found": "Δεν βρέθηκαν ψευδώνυμα.", - "no-key-matching": "Δεν βρέθηκε '{{key}}'.", - "no-keys-found": "Δεν βρέθηκαν ονόματα", - "create-new-alias": "Δημιουργήστε ένα νέο ψευδώνυμο!", - "create-new-key": "Δημιουργήστε ένα νέο!", - "duplicate-alias-error": "Βρέθηκε διπλότυπο το ψευδώνυμο '{{alias}}'.
    Τα ψευδώνυμα συσκευών πρέπει να είναι μοναδικά σε ένα dashboard.", - "configure-alias": "Ρύθμιση ψευδώνυμου '{{alias}}'", - "no-devices-matching": "Δεν βρέθηκε συσκευή η οποία να αντιστοιχεί σε '{{entity}}'.", - "alias": "Ψευδώνυμο", - "alias-required": "Απαιτείται ψευδώνυμο συσκευής.", - "remove-alias": "Αφαίρεση ψευδώνυμου συσκευής", - "add-alias": "Προσθήκη ψευδώνυμου συσκευής", - "name-starts-with": "Το όνομα συσκευής αρχίζει με", - "device-list": "Λίστα συσκευών", - "use-device-name-filter": "Χρήση φίλτρου", - "device-list-empty": "Δεν επιλέχθηκαν συσκευές.", - "device-name-filter-required": "Απαιτείται φίλτρο ονόματος συσκυεής.", - "device-name-filter-no-device-matched": "Δεν βρέθηκαν συσκευές οι οποίες να αρχίζουν από '{{device}}'.", - "add": "Προσθήκη συσκευής", - "assign-to-customer": "Ανάθεση σε πελάτη", - "assign-device-to-customer": "Ανάθεση Συσκευών Σε Πελάτη", - "assign-device-to-customer-text": "Παρακαλούμε επιλέξτε συσκευές προς ανάθεση σε πελάτη", - "make-public": "Δημοσιοποίηση συσκευών", - "make-private": "Ιδιωτικοποίηση συσκευών", - "no-devices-text": "Δεν βρέθηκαν συσκευές", - "assign-to-customer-text": "Παρακαλώ επιλέξτε πελάτη για να αναθέσετε τις συσκευές", - "device-details": "Λεπτομέρειες συσκευές", - "add-device-text": "Προσθήκη νέας συσκευής", - "credentials": "Διαπιστευτήρια", - "manage-credentials": "Διαχείρηση διαπιστευτηρίων", - "delete": "Διαγραφή συσκευής", - "assign-devices": "Ανάθεση συσκευών", - "assign-devices-text": "Ανάθεση { count, plural, 1 {1 device} other {# devices} } σε πελάτη", - "delete-devices": "Διαγραφή συσκευών", - "unassign-from-customer": "Αφαίρεση από πελάτη", - "unassign-devices": "Αφαίρεση συσκευών", - "unassign-devices-action-title": "Αφαίρεση { count, plural, 1 {1 device} other {# devices} } από πελάτη", - "assign-new-device": "Ανάθεση νέων συσκευών", - "make-public-device-title": "Είστε σίγουροι ότι θέλετε να κάνετε τη συσκευή '{{deviceName}}' δημόσια;", - "make-public-device-text": "Μετά από την επιβεβαίωσή σας η συσκευή και όλα τα δεδομένα της θα είναι δημόσια και διαθέσιμα σε τρίτους.", - "make-private-device-title": "Είστε σίγουροι ότι θέλετε να κάνετε τη συσκευή '{{deviceName}}' ιδιωτική;", - "make-private-device-text": "Μετά από την επιβεβαίωσή σας η συσκευή και όλα τα δεδομένα της θα είναι ιδιωτικά και δεν θα είναι διαθέσιμα σε τριτους.", - "view-credentials": "Προβολή διαπιστευτηρίων", - "delete-device-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε την συσκευή '{{deviceName}}';", - "delete-device-text": "Προσοχή, μετά την επιβεβαίωση, η συσκευή και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "delete-devices-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 device} other {# devices} };", - "delete-devices-action-title": "Διαγραφή { count, plural, 1 {1 device} other {# devices} }", - "delete-devices-text": "Προσοχή, μετά την επιβεβαίωση, όλες οι επιλεγμένες συσκευές θα αφαιρεθούν και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "unassign-device-title": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε την συσκευή '{{deviceName}}'?", - "unassign-device-text": "Μετά την επιβεβαίωση, η συσκευή θα αφαιρεθεί και δεν θα είναι προσβάσιμες από τον πελάτη.", - "unassign-device": "Αφαίρεση συσκευής", - "unassign-devices-title": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε { count, plural, 1 {1 device} other {# devices} }?", - "unassign-devices-text": "Μετά την επιβεβαίωση, όλες οι επιλεγμένες συσκευές θα καταργηθούν και δεν θα είναι προσβάσιμες από τον πελάτη.", - "device-credentials": "Πιστοποιητικά συσκευής", - "credentials-type": "Τύπος διαπιστευτηρίων", - "access-token": "Διακριτικό πρόσβασης", - "access-token-required": "Απαιτείται διακριτικό πρόσβασης.", - "access-token-invalid": "Access token length must be from 1 to 20 characters.", - "rsa-key": "RSA public key", - "rsa-key-required": "Απαιτείται RSA public key.", - "secret": "Secret", - "secret-required": "Απαιτείται secret.", - "device-type": "Τύπος συσκευής", - "device-type-required": "Απαιτείται τύπος συσκεύης.", - "select-device-type": "Επιλογή τύπου συσκευής", - "enter-device-type": "Είσοδο τύπου συσκευής", - "any-device": "Οποιαδήποτε συσκευή", - "no-device-types-matching": "Δεν βρέθηκε συσκευή η οποία να αντιστοιχεί σε '{{entitySubtype}}'.", - "device-type-list-empty": "Δεν έχουν επιλεχθεί τύποι συσκευής.", - "device-types": "Τύποι συσκευής", - "name": "Όνομα", - "name-required": "Απαιτείται όνομα.", - "description": "Περιγραφή", - "label": "Ετικέτα", - "events": "Γεγονότα", - "details": "Λεπτομέρειες", - "copyId": "Αντιγραφή ID συσκευής", - "copyAccessToken": "Αντιγραφή διακριτικού διαπιστευτηρίου", - "idCopiedMessage": "Το ID της συσκευής έχει αντιγραφεί στο πρόχειρο", - "accessTokenCopiedMessage": "Το διακριτικό διαπιστευτήριο έχει αντιγραφεί στο πρόχειρο", - "assignedToCustomer": "Ανάθεση σε πελάτη", - "unable-delete-device-alias-title": "Αδύνατο να διαγραφεί το ψευδώνυμο συσκευής", - "unable-delete-device-alias-text": "Το ψευδώνυμο συσκευής '{{deviceAlias}}' δεν μπορεί να διαγραφεί όσο εξακωλουθεί να χρησιμοποιείται από τα ακόλουθα widget(s):
    {{widgetsList}}", - "is-gateway": "είναι gateway", - "public": "Δημόσιο", - "device-public": "Η συσκευή είναι δημόσια", - "select-device": "Επιλογή συσκευής", - "selected-devices": "{ count, plural, 1 {1 device} other {# devices} } επιλέχθηκαν", - "search": "Αναζήτηση συσκευών", - "select-group-to-add": "Επιλέξτε την ομάδα στην οποία θα προστεθουν οι επιλεγμένες συσκευές", - "select-group-to-move": "Επιλέξτε την ομάδα στην οποία θα μετακινηθούν οι επιλεγμένες συσκευές", - "remove-devices-from-group": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε { count, plural, 1 {1 device} other {# devices} } from group '{entityGroup}'?", - "group": "Ομάδα Συσκευών", - "list-of-groups": "{ count, plural, 1 {One device group} other {List of # device groups} }", - "group-name-starts-with": "Ομάδες συσκευών των οποίων το όνομα αρχίζει από '{{prefix}}'", - "import": "Εισαγωγή συσκευής", - "device-file": "Αρχείο συσκευής" - }, - "dialog": { - "close": "Κλείσιμο διαλόγου" - }, - "direction": { - "column": "Στήλη", - "row": "Γραμμή" - }, - "error": { - "unable-to-connect": "Αδύνατον να συνδεθεί στον διακομιστή! Παρακαλούμε ελέγξτε την σύνδεσή σας στο ίντερνετ.", - "unhandled-error-code": "Μη διορθωμένος κωδικός σφάλματος: {{errorCode}}", - "unknown-error": "Άγνωστο σφάλμα" - }, - "entity": { - "entity": "Οντότητα", - "entities": "Οντότητες", - "aliases": "Ψευδώνυμα οντότητας", - "entity-alias": "Ψευδώνυμο οντότητας", - "unable-delete-entity-alias-title": "Αδύνατο να διαγραφεί το ψευδώνυμο οντότητας", - "unable-delete-entity-alias-text": "Το ψευδώνυμο οντότητας '{{entityAlias}}' δεν μπορεί να διαγραφεί όσο εξακολουθεί να χρησιμοποιείται από τα παρακάτω widget(s):
    {{widgetsList}}", - "duplicate-alias-error": "Βρέθηκε διπλότυπο το ψευδώνυμο '{{alias}}'.
    Τα ψευδώνυμα οντοτήτων πρέπει να είναι μοναδικά σε ένα dashboard.", - "missing-entity-filter-error": "Λείπει φίλτρο από το ψευδώνυμο '{{alias}}'.", - "configure-alias": "Ρύθμιση ψευδώνυμου '{{alias}}'", - "alias": "Ψευδώνυμο", - "alias-required": "Απαιτείται ψευδώνυμο οντότητας.", - "remove-alias": "Αφαίρεση ψευδώνυμου οντότητας", - "add-alias": "Προσθήκη ψευδώνυμου οντότητας", - "entity-list": "Λίστα οντοτήτων", - "entity-type": "Τύπος οντοτήτων", - "entity-types": "Τύποι οντοτήτων", - "entity-type-list": "Λίστα τύπων οντοτήτων", - "any-entity": "Οποιαδήποτε οντότητα", - "enter-entity-type": "Εισαγωγή τύπου οντότητας", - "no-entities-matching": "Δεν βρέθηκαν οντότητες ο οποίες να σχετίζονται με '{{entity}}'.", - "no-entity-types-matching": "Δεν βρέθηκαν τύποι οντότητας ο οποίοι να σχετίζονται με '{{entityType}}'.", - "name-starts-with": "το όνομα αρχίζει από", - "use-entity-name-filter": "χρήση φίλτρου", - "entity-list-empty": "Δεν έχουν επιλεγεί οντότητες.", - "entity-type-list-empty": "Δεν έχουν επιλεγεί τύποι οντότητας.", - "entity-name-filter-required": "Απαιτείται φίλτρο ονόματος οντότητας.", - "entity-name-filter-no-entity-matched": "Δεν βρέθηκαν οντότητες οι οποίες αρχίζουν από '{{entity}}'.", - "all-subtypes": "Όλοι", - "select-entities": "Επιλογή οντοτήτων", - "no-aliases-found": "Δεν βρέθηκαν ψευδώνυμα.", - "no-alias-matching": "Δεν βρέθηκε '{{alias}}'.", - "create-new-alias": "Δημιουργείστε ένα νέο!", - "key": "Κλειδί", - "key-name": "όνομα κλειδιού", - "no-keys-found": "δεν βρέθηκαν κλειδιά.", - "no-key-matching": "Δεν βρέθηκε '{{key}}'.", - "create-new-key": "Δημιουργείστε ένα νέο!", - "type": "Τύπος", - "type-required": "Απαιτείται τύπος οντότητας.", - "type-device": "Συσκευή", - "type-devices": "Συσκευές", - "list-of-devices": "{ count, plural, 1 {One device} other {List of # devices} }", - "device-name-starts-with": "Συσκευές των οποίων το όνομα αρχίζει από '{{prefix}}'", - "type-asset": "Asset", - "type-assets": "Assets", - "list-of-assets": "{ count, plural, 1 {One asset} other {List of # assets} }", - "asset-name-starts-with": "Assets των οποίων το όνομα αρχίζει από '{{prefix}}'", - "type-entity-view": "Προβολή οντότητας", - "type-entity-views": "Προβολές οντότητας", - "list-of-entity-views": "{ count, plural, 1 {One entity view} other {List of # entity views} }", - "entity-view-name-starts-with": "Προβολές οντότητας των οποίων το όνομα αρχίζει από '{{prefix}}'", - "type-rule": "Κανόνας", - "type-rules": "Κανόνες", - "list-of-rules": "{ count, plural, 1 {One rule} other {List of # rules} }", - "rule-name-starts-with": "Κανόνες των οποίων το όνομα αρχίζει από '{{prefix}}'", - "type-plugin": "Πρόσθετο", - "type-plugins": "Προσθετα", - "list-of-plugins": "{ count, plural, 1 {One plugin} other {List of # plugins} }", - "plugin-name-starts-with": "Πρόσθετα των οποίων το όνομα αρχίζει από '{{prefix}}'", - "type-tenant": "Μισθωτής", - "type-tenants": "Μισθωτές", - "list-of-tenants": "{ count, plural, 1 {One tenant} other {List of # tenants} }", - "tenant-name-starts-with": "Μισθωτές των οποίων το όνομα αρχίζει από '{{prefix}}'", - "type-customer": "Πελάτης", - "type-customers": "Πελάτες", - "list-of-customers": "{ count, plural, 1 {One customer} other {List of # customers} }", - "customer-name-starts-with": "Πελάτες των οποίων το όνομα αρχίζει από '{{prefix}}'", - "type-user": "Χρήστης", - "type-users": "Χρήστες", - "list-of-users": "{ count, plural, 1 {One user} other {List of # users} }", - "user-name-starts-with": "Χρήστες των οποίων το όνομα αρχίζει από '{{prefix}}'", - "type-dashboard": "Dashboard", - "type-dashboards": "Dashboards", - "list-of-dashboards": "{ count, plural, 1 {One dashboard} other {List of # dashboards} }", - "dashboard-name-starts-with": "Dashboards των οποίων το όνομα αρχίζει από '{{prefix}}'", - "type-alarm": "Alarm", - "type-alarms": "Alarms", - "list-of-alarms": "{ count, plural, 1 {One alarms} other {List of # alarms} }", - "alarm-name-starts-with": "Alarms των οποίων το όνομα αρχίζει από '{{prefix}}'", - "type-rulechain": "Αλυσίδα Κανόνων", - "type-rulechains": "Αλυσίδες Κανόνων", - "list-of-rulechains": "{ count, plural, 1 {One rule chain} other {List of # rule chains} }", - "rulechain-name-starts-with": "Αλυσίδες Κανόνων των οποίων το όνομα αρχίζει από '{{prefix}}'", - "type-scheduler-event": "Προγραμματιστής", - "type-scheduler-events": "Προγραμματιστής", - "list-of-scheduler-events": "{ count, plural, 1 {One scheduler event} other {List of # scheduler events} }", - "scheduler-event-name-starts-with": "Προγραμματιστές γεγονότων των οποίων το όνομα αρχίζει από '{{prefix}}'", - "type-blob-entity": "Ογκώδη οντότητα", - "type-blob-entities": "Ογκώδεις οντότητες", - "list-of-blob-entities": "{ count, plural, 1 {One blob entity} other {List of # blob entities} }", - "blob-entity-name-starts-with": "Ογκώδεις οντότητες των οποίων το όνομα αρχίζει από '{{prefix}}'", - "type-rulenode": "Κόμβος κανόνα", - "type-rulenodes": "Κόμβοι κανόνων", - "list-of-rulenodes": "{ count, plural, 1 {One rule node} other {List of # rule nodes} }", - "rulenode-name-starts-with": "Κόμβοι κανόνων των οποίων το όνομα αρχίζει από '{{prefix}}'", - "type-current-customer": "Τρέχον Πελάτης", - "search": "Αναζήτηση Οντότητες", - "selected-entities": "{ count, plural, 1 {1 entity} other {# entities} } επιλεγμένα", - "entity-name": "Όνομα οντότητας", - "details": "Λεπτομέρειες οντότητας", - "no-entities-prompt": "Δεν βρέθηκαν οντότητες", - "no-data": "Δεν υπάρχουν δεδομένα προς προβολή", - "columns-to-display": "στήλες που προβάλονται", - "type-entity-group": "Ομάδα Οντότητας", - "type-converter": "Μετατροπέας Δεδομένων", - "type-converters": "Μετατροπείς Δεδομένων", - "list-of-converters": "{ count, plural, 1 {One data converter} other {List of # data converters} }", - "converter-name-starts-with": "Μετατροπείς δεδομένων των οποίων το όνομα αρχίζει από '{{prefix}}'", - "type-integration": "Ενσωμάτωση", - "type-integrations": "Ενσωματώσεις", - "list-of-integrations": "{ count, plural, 1 {One integration} other {List of # integrations} }", - "integration-name-starts-with": "Ενσωματώσεις των οποίων το όνομα αρχίζει από '{{prefix}}'", - "type-role": "Ρόλος", - "type-roles": "Ρόλοι", - "list-of-roles": "{ count, plural, 1 {One role} other {List of # roles} }", - "role-name-starts-with": "Ρόλοι των οποίων το όνομα αρχίζει από '{{prefix}}'", - "type-group-permission": "Άδεια Ομάδας" - }, - "entity-group": { - "entity-group": "Ομάδα Οντότητας", - "details": "Λεπτομέρειες", - "columns": "Στήλες", - "add-column": "Προσθήκη στήλης", - "column-value": "Τιμή", - "column-value-required": "Απαιτείται τιμή στήλης.", - "column-title": "Τίτλος", - "default-sort-order": "Προεπιλογή ταξινόμισης", - "default-sort-order-required": "Απαιτείται προεπιλογή ταξινόμισης στήλης.", - "hide-in-mobile-view": "κρυφό σε mobile", - "use-cell-style-function": "Λειτουργία χρήσης στυλ κελιού", - "use-cell-content-function": "Λειτουργία χρήσης περιεχομένου κελιού", - "edit-column": "Επεξεργασία στήλης", - "column-details": "Λεπτομέρειες στήλης", - "actions": "Ενέργιες", - "settings": "Ρυθμίσεις", - "delete": "Διαγραφή ομάδας οντοτήτων", - "name": "Όνομα", - "name-required": "Απαιτείται όνομα.", - "description": "Περιγραφή", - "add": "Προσθήκη ομάδας οντοτήτων", - "add-entity-group-text": "Προσθήκη νέας ομάδας οντοτήτων", - "no-entity-groups-text": "Δεν βρέθηκαν ομάδες οντοτήτων", - "entity-group-details": "Λεπτομέρειες ομαδών οντοτήτων", - "delete-entity-groups": "Διαγραφή ομαδών οντοτήτων", - "delete-entity-group-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε την ομάδα οντοτήτων '{{entityGroupName}}'?", - "delete-entity-group-text": "Προσοχή, μετά την επιβεβαίωση, η ομάδα οντοτήτων και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "delete-entity-groups-title": "Είστε σίγουροι ότι θέλετε να διαγραφούν { count, plural, 1 {1 entity group} other {# entity groups} }?", - "delete-entity-groups-action-title": "Διαγραφή { count, plural, 1 {1 entity group} other {# entity groups} }", - "delete-entity-groups-text": "Προσοχή, αφού ολοκληρωθεί η επιβεβαίωση, όλες οι επιλεγμένες ομάδες οντοτήτων θα αφαιρεθούν και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "device-groups": "Ομάδες Συσκευών", - "asset-groups": "Ομάδες Asset", - "customer-groups": "Ομάδες Πελατών", - "device-group": "Ομάδα Πελατών", - "asset-group": "Ομάδα Asset", - "user-group": "Ομάδα Χρηστών", - "user-groups": "Ομάδες Χρηστών", - "customer-group": "Ομάδα Χρηστών", - "entity-view-groups": "Ομάδες Όψεων", - "entity-view-group": "Προβολή ομαδών οντοτήτων", - "dashboard-groups": "Ομάδες Dashboard", - "dashboard-group": "Ομάδα Dashboard", - "fetch-more": "Περισσότερα", - "column-type": { - "column-type": "Τύπος στήλης", - "client-attribute": "Χαρακτηριστικά Client", - "shared-attribute": "Κοινόχρηστα Χαρακτηριστικά", - "server-attribute": "Χαρακτηριστικό Server", - "timeseries": "Χρονική σειρά", - "entity-field": "Πεδίο οντότητας" - }, - "column-type-required": "Απαιτείται τύπος στήλης.", - "entity-field": { - "created-time": "Δημιουργήθηκε", - "name": "Όνομα", - "type": "Τύπος", - "assigned_customer": "Ανατεθειμένος πελάτης", - "authority": "Εξουσιοδότηση", - "first_name": "Όνομα", - "last_name": "Επίθετο", - "email": "Email", - "title": "Τίτλος", - "country": "Χώρα", - "state": "Νομός", - "city": "Πόλη", - "address": "Διεύθυνση", - "address2": "Διεύθυνση 2", - "zip": "Τ.Κ.", - "phone": "Τηλέφωνο" - }, - "sort-order": { - "asc": "Αύξουσα", - "desc": "Φθίνουσα", - "none": "Καμία" - }, - "details-mode": { - "on-row-click": "στο κλικ στη γραμμη", - "on-action-button-click": "στο κλικ στο κουμπί λεπτομέρειες", - "disabled": "μη διαθέσιμο" - }, - "change-owner": "Αλλαγή ιδιοκτήτη", - "select-target-owner": "Επιλογή ιδιοκτήτη", - "no-owners-matching": "Δεν βρέθηκε ιδιοκτήτης που να αντιστοιχεί σε '{{owner}}'.", - "target-owner-required": "Απαιτείται επιλεγμένος ιδιοκτήτης.", - "confirm-change-owner-title": "Είστε σίγουροι ότι θέλετε να αλλάξετε ιδιοκτήτη για { count, plural, 1 {1 selected entity} other {# selected entities} }?", - "confirm-change-owner-text": "Προσοχή, μετά την επιβεβαίωση, όλες οι επιλεγμένες οντότητες θα αφαιρεθούν από τον τρέχοντα ιδιοκτήτη και θα τοποθετηθούν στην ομάδα 'Όλα' του επιλεγμένου ιδιοκτήτη.", - "add-to-group": "Προσθήκη σε ομάδα", - "move-to-group": "Μετακίνηση σε ομάδα", - "select-entity-group": "Επιλογή ομάδας οντοτήτων", - "no-entity-groups-matching": "Δεν βρέθηκαν ομάδες οντοτήτων που να αντιστοιχούν με '{{entityGroup}}'.", - "target-entity-group-required": "Απαιτείται επιλεγμένη ομάδα οντοτήτων.", - "select-user-group": "Επιλογή ομάδας χρηστών", - "no-user-groups-matching": "Δεν βρέθηκαν ομάδες χρηστών οι οποίες να αντιστοιχούν σε '{{entityGroup}}'.", - "target-user-group-required": "Απαιτείται επιλεγμένη ομάδα χρηστών.", - "remove-from-group": "Αφαίρεση από ομάδα", - "group-table-title": "Τίτλος ομάδας πίνακα", - "enable-search": "Ενεργοποίηση αναζήτησης οντοτήτων", - "enable-add": "Ενεργοποίηση προσθήκης οντοτήτων", - "enable-delete": "Ενεργοποίηση διαγραφής οντοτήτων", - "enable-selection": "Ενεργοποίηση επιλογής οντοτήτων", - "enable-group-transfer": "Ενεργοποίηση μεταφοράς ομάδων", - "display-pagination": "Προβολή σελιδοποίησης", - "default-page-size": "Προεπιλεγμένο μέγεθος σελίδας", - "enable-assignment-actions": "Ενεργοποίηση ανάθεσης", - "enable-credentials-management": "Ενεργοποίηση διαχείρησης διαπιστευτηρίων", - "enable-login-as-user": "Ενεργοποίηση εισόδου ως χρήστη", - "enable-users-management": "Ενεργοποίηση διαχείρισης χρήστών", - "enable-customers-management": "Ενεργοποίηση διαχείρησης πελατών", - "enable-assets-management": "Ενεργοποίηση διαχείρησης assets", - "enable-devices-management": "Ενεργοποίηση διαχείρησης συσκευών", - "enable-entity-views-management": "Ενεργοποίηση διαχείρησης προβολής οντοτήτων", - "enable-dashboards-management": "Ενεργοποίηση διαχείρησης dashboard", - "open-details-on": "Άνοιγμα λεπτομερειών οντότητας σε", - "select-existing": "Ε[ιλογή υπάρχον ομάδας οντοτήτων", - "create-new": "Δημιουργία νέας ομάδας οντοτήτων", - "new-entity-group-name": "Νέο όνομα ομάδας οντοτήτων", - "entity-group-list": "Λίστα ομάδων οντοτήτων", - "entity-group-list-empty": "Δεν έχουν επιλεχθεί ομάδες οντοτήτων.", - "name-starts-with": "Το όνομα της ομάδας οντοτήτων αρχίζει από", - "entity-group-name-filter-required": "Απαιτείται φίλτρο ονόματος ομάδας οντοτήτων.", - "roles": "Ρόλοι", - "permissions": "Δικαιώματα", - "public": "Δημόσιο", - "entity-group-public": "Η ομάδα οντοτήτων είναι δημόσια", - "make-public": "Δημοσιοποίηση ομάδας οντοτήτων", - "make-private": "Ιδιωτικοποίηση ομάδας οντοτήτων", - "make-public-entity-group-title": "Είστε σίγουροι ότι θέλετε να κάνετε την ομάδα οντοτήτων '{{entityGroupName}}' δημόσια;", - "make-public-entity-group-text": "Μετά την επιβεβαίωση, η ομάδα οντοτήτων και όλες οι οντότητές της θα δημοσιοποιηθούν και θα είναι προσβάσιμες από τρίτους.", - "make-private-entity-group-title": "Είστε σίγουροι ότι θέλετε να κάνετε την ομάδα οντοτήτων '{{entityGroupName}}' ιδιωτική?", - "make-private-entity-group-text": "Μετά την επιβεβαίωση, η ομάδα οντοτήτων και όλες οι οντότητές της θα γίνουν ιδιωτικές και δεν θα είναι προσβάσιμες από τρίτους.", - "copyId": "Αντιγραφή ID ομάδας οντοτήτων", - "idCopiedMessage": "Το ID της ομάδας οντοτήτων έχει αντιγραφεί στο πρόχειρο" - }, - "entity-field": { - "created-time": "Δημιουργήθηκε", - "name": "Όνομα", - "type": "Τύπος", - "first-name": "Όνομα", - "last-name": "Επίθετο", - "email": "Email", - "title": "Τίτλος", - "country": "Χώρα", - "state": "Νομός", - "city": "Πόλη", - "address": "Διεύθυνση", - "address2": "Διεύθυνση 2", - "zip": "Τ.Κ.", - "phone": "Τηλέφωνο" - }, - "entity-view": { - "entity-view": "Όψη Οντότητας", - "entity-view-required": "Απαιτείται προβολή οντότητας.", - "entity-views": "Όψεις Οντοτήτων", - "management": "Διαχείριση Όψεων Οντοτήτων", - "view-entity-views": "Προβολή οντοτήτων", - "entity-view-alias": "Ψευδώνυμο προβολής οντότητας", - "aliases": "Ψευδώνυμα προβολής οντότητας", - "no-alias-matching": "'Δεν βρέθηκαν {{alias}}'.", - "no-aliases-found": "Δεν βρέθηκαν ψευδώνυμα.", - "no-key-matching": "'Δεν βρέθηκαν {{key}}'.", - "no-keys-found": "Δεν βρέθηκαν ονόματα.", - "create-new-alias": "Δημιουργήστε ένα νέο!", - "create-new-key": "Δημιουργήστε ένα νέο!", - "duplicate-alias-error": "Βρέθηκε διπλότυπο '{{alias}}'.
    Τα ψευδώνυμα προβολής οντότητας πρέπει να είναι μοναδικά εντός του dashboard.", - "configure-alias": "Ρύθμιση ψευδώνυμου '{{alias}}'", - "no-entity-views-matching": "Δεν βρέθηκαν προβολές οντοτήτων οι οποίες να αντιστοιχούν σε '{{entity}}'.", - "alias": "Ψευδώνυμο", - "alias-required": "Απαιτείται ψευδώνυμο προβολής οντοτήτων.", - "remove-alias": "Αφαίρεση ψευδωνύμου προβολής οντοτήτων", - "add-alias": "Προβολή ψευδωνύμου προβολής οντοτήτων", - "name-starts-with": "Το όνομα προβολής οντότητας αρχίζει από", - "entity-view-list": "Λίστα προβολής οντότητας", - "use-entity-view-name-filter": "Χρήση φίλτρου", - "entity-view-list-empty": "Δεν έχουν επιλεχθεί προβολές οντότητας.", - "entity-view-name-filter-required": "Απαιτείται φίλτρο ονόματος προβολής οντότητας.", - "entity-view-name-filter-no-entity-view-matched": "Δεν βρέθηκαν προβολές οντότητας οι οποίες να αρχίζουν από '{{entityView}}'.", - "add": "Προσθήκη Προβολής Οντότητας", - "assign-to-customer": "Ανάθεση σε πελάτη", - "assign-entity-view-to-customer": "Ανάθεση προβολής/ών οντότητας σε πελάτη", - "assign-entity-view-to-customer-text": "Παρακαλώ επιλέξτε τις προβολές οντότητας για να αναθέσετε σε πελάτη", - "no-entity-views-text": "Δεν βρέθηκαν προβολές οντότητας", - "assign-to-customer-text": "Παρακαλώ επιλέξτε πελάτη για να ανάθεση προβολών οντότητας", - "entity-view-details": "Λεπτομέρειες προβολής οντότητας", - "add-entity-view-text": "Προσθήκη νέας προβολής οντότητας", - "delete": "Διαγραφή προβολής οντότητας", - "assign-entity-views": "Ανάθεση προβολών οντότητας", - "assign-entity-views-text": "Ανάθεση { count, plural, 1 {1 entityView} other {# entityViews} } σε πελάτη", - "delete-entity-views": "Διαγραφή προβολών οντότητας", - "make-public": "Δημοσιοποίηση προβολής οντότητας", - "make-private": "Ιδιωτικοποίηση προβολής οντότητας", - "unassign-from-customer": "Αφαίρεση από πελάτη", - "unassign-entity-views": "Αφαίρεση προβολών οντότητας", - "unassign-entity-views-action-title": "Αφαίρεση { count, plural, 1 {1 entityView} other {# entityViews} } από πελάτη", - "assign-new-entity-view": "Ανάθεση νέας προβολής οντότητας", - "delete-entity-view-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε την προβολή οντότητας '{{entityViewName}}'?", - "delete-entity-view-text": "Προσοχή, μετά την επιβεβαίωση, η προβολή της οντότητας και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "delete-entity-views-title": "Είστε σίγουροι ότι θέλετε να προβάλετε την οντότητα { count, plural, 1 {1 entityView} other {# entityViews} };", - "delete-entity-views-action-title": "Διαγραφή { count, plural, 1 {1 entityView} other {# entityViews} }", - "delete-entity-views-text": "Προσοχή, μετά την επιβεβαίωση, όλες οι επιλεγμένες προβολές της οντότητας και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "make-public-entity-view-title": "Είστε σίγουροι ότι θέλετε να κάνετε την προβολή οντότητας '{{entityViewName}}' δημόσια?", - "make-public-entity-view-text": "Μετά την επιβεβαίωση, η προβολή της οντότητας και όλα τα δεδομένα της θα δημοσιοποιηθούν και θα είναι προσβάσιμα από τρίτους.", - "make-private-entity-view-title": "Είστε σίγουροι ότι θέλετε να κάνετε την προβολή οντότητας '{{entityViewName}}' ιδιωτική;", - "make-private-entity-view-text": "Μετά την επιβεβαίωση, η προβολή της οντότητας και όλα τα δεδομένα της θα γίνουν ιδιωτικά και θα δεν είναι προσβάσιμα από τρίτους.", - "unassign-entity-view-title": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε την προβολη οντότητας '{{entityViewName}}';", - "unassign-entity-view-text": "Μετά την επιβεβαίωση, η προβολή της οντότητας θα καταργηθεί και δεν θα είναι προσβάσιμη από τον πελάτη.", - "unassign-entity-view": "Αφαίρεση προβολής οντότητας", - "unassign-entity-views-title": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε { count, plural, 1 {1 entityView} other {# entityViews} };", - "unassign-entity-views-text": "Μετά την επιβεβαίωση, όλες οι επιλεγμένες προβολές οντοτήτων θα αφαιρεθούν και δεν θα είναι προσβάσιμες από τον πελάτη.", - "entity-view-type": "Τύπος Προβολής Οντότητας", - "entity-view-type-required": "Απαιτείται τύπος προβολής οντότητας.", - "select-entity-view-type": "Επιλογή τύπου προβολής οντότητας", - "enter-entity-view-type": "Εισαγωγή τύπου προβολής οντοτήτων", - "any-entity-view": "Οποιαδήποτε προβολή οντότητας", - "no-entity-view-types-matching": "Δεν βρέθηκαν τύποι προβολής οντότητας οι οποίοι να αντιστοιχούν με '{{entitySubtype}}'.", - "entity-view-type-list-empty": "Δεν έχουν επιλεχθεί τύποι προβολής οντότητας.", - "entity-view-types": "Τύποι Προβολής Οντότητας", - "name": "Όνομα", - "name-required": "Απαιτείται όνομα.", - "description": "Περιγραφή", - "events": "Γεγονότα", - "details": "Λεπτομέρειες", - "copyId": "Αντιγραφή ID προβολής οντότητας", - "idCopiedMessage": "Το ID της προβολής οντότητας έχει αντιγραφεί στο πρόχειρο", - "assignedToCustomer": "Αναθέση σε πελάτη", - "unable-entity-view-device-alias-title": "Αδύνατον να διαγραφεί το ψευδώνυμο προβολής οντότητας", - "unable-entity-view-device-alias-text": "Το ψευδώνυμο συσκευής '{{entityViewAlias}}' δεν μπορεί να διαγραφεί όσο εξακωλουθεί να χρησιμοποιείτε από τα παρακάτω widget(s):
    {{widgetsList}}", - "select-entity-view": "Επιλογή προβολής οντότητας", - "start-date": "Ημερομηνία έναρξης", - "start-ts": "Ώρα έναρξης", - "end-date": "Ημερομηνία λήξης", - "end-ts": "Ώρα λήξης", - "date-limits": "Όρια ημερομηνίας", - "client-attributes": "Χαρακτηριστικά Client", - "shared-attributes": "Κοινόχρηστα Χαρακτηριστικά", - "server-attributes": "Χαρακτηριστικά Server", - "timeseries": "Χρονική σειρά", - "client-attributes-placeholder": "Χαρακτηριστικά Client", - "shared-attributes-placeholder": "Κοινόχρηστα Χαρακτηριστικά", - "server-attributes-placeholder": "Χαρακτηριστικά Server", - "timeseries-placeholder": "Χρονική σειρά", - "target-entity": "Στοχευμένη οντότητα", - "attributes-propagation": "Διάδοση χαρακτηριστικών", - "attributes-propagation-hint": "Η προβολή οντοτήτων θα αντιγράφει αυτόματα καθορισμένα χαρακτηριστικά από την στοχευμένη οντότητα κάθε φορά που αποθηκεύετε ή ενημερώνετε αυτήν την προβολή οντότητας. Για λόγους απόδοσης, τα χαρακτηριστικά της στοχευμένης οντότητας δεν μεταδίδονται στην προβολή οντότητας με κάθε αλλαγή χαρακτηριστικών. Μπορείτε να ενεργοποιήσετε την αυτόματη διάδοση ρυθμίζοντας τον κόμβο \"αντιγραφή για προβολή \" στην αλυσίδα κανόνων σας και συνδέοντας τα μηνύματα \"Χαρακτηριστικά Post \" και \"Ενημερωμένα Χαρακτηριστικά \" στον νέο κόμβο.", - "timeseries-data": "Δεδομένα χρονικής σειράς", - "timeseries-data-hint": "Ρυθμίστε τα δεδομένα της χρονικής σειράς της στοχευμένης οντότητας που θα είναι διαθέσιμα στην προβολή οντοτητας. Αυτά τα δεδομένα χρονικής σειράς είναι μόνο για ανάγνωση.", - "selected-entity-views": "{ count, plural, 1 {1 entity view} other {# entity views} } επιλεγμένα", - "search": "Αναζήτηση προβολών οντότητας", - "select-group-to-add": "Επιλογή ομάδας για προσθήκη των επιλεγμένων προβολών οντότητας", - "select-group-to-move": "Επιλογή ομάδας για μετακίνηση των επιλεγμένων προβολών οντότητας", - "remove-entity-views-from-group": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε { count, plural, 1 {1 entity view} other {# entity views} } from group '{entityGroup}'?", - "group": "Ομάδα προβολών οντότητας", - "list-of-groups": "{ count, plural, 1 {One entity view group} other {List of # entity view groups} }", - "group-name-starts-with": "Ομάδες προβολής οντότητας των οποίων το όνομα αρχίζει από '{{prefix}}'" - }, - "event": { - "events": "Γεγονότα", - "event-type": "Τύπος Γεγονότος", - "type-error": "Σφάλμα", - "type-lc-event": "Γεγονός κύκλου ζωής", - "type-stats": "Στατιστική", - "type-debug-converter": "Αποσφαλμάτωση", - "type-debug-integration": "Αποσφαλμάτωση", - "type-debug-rule-node": "Αποσφαλμάτωση", - "type-debug-rule-chain": "Αποσφαλμάτωση", - "no-events-prompt": "Δεν βρέθηκαν γεγονότα", - "error": "Σφάλμα", - "alarm": "Alarm", - "event-time": "Ώρα Γεγονότος", - "server": "Διακομιστής", - "body": "Σώμα (body)", - "method": "Μέθοδος", - "type": "Τύπος", - "in": "Είσοδος", - "out": "Έξοδος", - "metadata": "Μεταδεδομένα", - "message": "Μήνυμα", - "entity": "Οντότητα", - "message-id": "ID Μηνύματος", - "message-type": "Τύπος Μηνύματος", - "data-type": "Τύπος Δεδομένων", - "relation-type": "Τύπος Σχέσης", - "data": "Δεδομέναα", - "event": "Γεγονός", - "status": "Κατάσταση", - "success": "Επιτυχία", - "failed": "Απέτυχε", - "messages-processed": "Επεξεργασμένα μηνύματα", - "errors-occurred": "Παρουσιάστηκαν σφάλματα" - }, - "extension": { - "extensions": "Επεκτάσεις", - "selected-extensions": "{ count, plural, 1 {1 extension} other {# extensions} } επιλέχθηκαν", - "type": "Τύπος", - "key": "Κλειδί", - "value": "Τιμή", - "id": "ID", - "extension-id": "ID επέκτασης", - "extension-type": "Τύπος επέκτασης", - "transformer-json": "JSON *", - "unique-id-required": "Το τρέχον ID επέκτασης υπάρχει ήδη.", - "delete": "Διαγραφή επέκτασης", - "add": "Προσθήκη επέκτασης", - "edit": "Επεξεργασία επέκτασης", - "view": "Προβολή επέκτασης", - "delete-extension-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε την επέκταση '{{extensionId}}';", - "delete-extension-text": "Προσοχή, μετά την επιβεβαίωση, η επέκταση και όλα τα σχετικά δεδομένα θα διαγραφούν μόνιμα.", - "delete-extensions-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 extension} other {# extensions} }?", - "delete-extensions-text": "Προσέξτε, μετά την επιβεβαίωση θα αφαιρεθούν όλες οι επιλεγμένες επεκτάσεις.", - "converters": "Μετατροπείς", - "converter-id": "ID Μετατροπέα", - "configuration": "Διαμόρφωση", - "converter-configurations": "Ρυθμίσεις μετατροπέα", - "token": "Ετικέτα ασφαλείας", - "add-converter": "Προσθήκη μετατροπέα", - "add-config": "Προσθήκη ρυθμήσεων μετατροπέα", - "device-name-expression": "Έκφραση ονόματος συσκευής", - "device-type-expression": "Έκφραση τύπου συσκευής", - "custom": "Προσαρμοσμένο", - "to-double": "Διπλασιασμός", - "transformer": "Μετατροπέας", - "json-required": "Απαιτείται μετατροπέας JSON.", - "json-parse": "Αδύνατον να γίνει αναλύση του μετατροπέα JSON.", - "attributes": "Χαρακτηριστικά", - "add-attribute": "Προσθήκη χαρακτηριστικού", - "add-map": "Προσθήκη στοιχείου χαρτογράφισης", - "timeseries": "Χρονική σειρά", - "add-timeseries": "Προσθήκη χρονικής σειράς", - "field-required": "Απαιτείται το πεδίο", - "brokers": "Brokers", - "add-broker": "Προσθήκη broker", - "host": "Host", - "port": "Port", - "port-range": "Η Port πρέπει να είναι από 1 ως 65535.", - "ssl": "Ssl", - "credentials": "Διαπιστευτήρια", - "username": "Όνομα Χρήστη", - "password": "Κωδικός", - "retry-interval": "Διάστημα επανάληψης σε χιλιοστά του δευτερολέπτου", - "anonymous": "Ανώνυμα", - "basic": "Βασικά", - "pem": "PEM", - "ca-cert": "Αρχείο πιστοποιητικού CA *", - "private-key": "Αρχείο ιδιωτικού κλειδιού *", - "cert": "Αρχείο πιστοποιητικού *", - "no-file": "Δεν έχει επιλεχθεί αρχείο.", - "drop-file": "Αποθέστε ένα αρχείο ή κάνετε κλικ για να επιλέξετε ένα αρχείο για ανέβασμα.", - "mapping": "χαρτογράφιση", - "topic-filter": "Φίλτρο θέματος", - "converter-type": "Τύπος μετατροπέα", - "converter-json": "JSON", - "json-name-expression": "Έκφραση json ονόματος συσκευής", - "topic-name-expression": "Έκφραση θέματος ονόματος συσκευής", - "json-type-expression": "Έκφραση json τύπου συσκευής", - "topic-type-expression": "Έκφραση θέματος τύπου συσκευής", - "attribute-key-expression": "Έκφραση χαρακτηριστικού κλειδιού", - "attr-json-key-expression": "Έκφραση JSON χαρακτηριστικού κλειδιού", - "attr-topic-key-expression": "Έκφραση θέματος χαρακτηριστικού κλειδιού", - "request-id-expression": "Αίτημα έκφρασης ID", - "request-id-json-expression": "Αίτημα ID έκφρασης JSON", - "request-id-topic-expression": "Αίτημα ID έκφρασης θέματος", - "response-topic-expression": "Απάντηση έκφρασης θέματος", - "value-expression": "Έκφραση τιμής", - "topic": "Θέμα", - "timeout": "Λήξη σε χιλιοστά του δευτερολέπτου", - "converter-json-required": "Απαιτείται μετατροπέας JSON.", - "converter-json-parse": "Αδύνατον να αναλυθεί ο μετατροπέας JSON.", - "filter-expression": "Έκφραση φίλτρου", - "connect-requests": "Αιτήματα σύνδεσης", - "add-connect-request": "Προσθήκη αιτήματος σύνδεσης", - "disconnect-requests": "Αιτήματα αποσύνδεσης", - "add-disconnect-request": "Προσθήκη αιτήματος αποσύνδεσης", - "attribute-requests": "Χαρακτιριστικό αιτημάτων", - "add-attribute-request": "Προσθήκη χαρακτηριστικού αιτήματος", - "attribute-updates": "Ανανεώσεις χαρακτηριστικού", - "add-attribute-update": "Προσθήκη ανανέωσης χαρακτηριστικού", - "server-side-rpc": "Server side RPC", - "add-server-side-rpc-request": "Προσθήκη αιτήματος server-side RPC", - "device-name-filter": "Φίλτρο ονόματος συσκευής", - "attribute-filter": "ίλτρο χαρακτηριστικού", - "method-filter": "Φίλτρο μεθόδου", - "request-topic-expression": "Αίτημα έκφρασης θέματος", - "response-timeout": "Λήξη απάντησης σε χιλιοστά του δευτερολέπτου", - "topic-expression": "Έκφρασου θέματος", - "client-scope": "Πεδίο εφαρμογής πελάτη", - "add-device": "Προσθήκη συσκευής", - "opc-server": "Servers", - "opc-add-server": "Προσθήκη server", - "opc-add-server-prompt": "Παρακαλούμε προσθέστε server", - "opc-application-name": "Όνομα εφαρμογής", - "opc-application-uri": "URI Εφαρμογής", - "opc-scan-period-in-seconds": "Περίοδος σάρωσης σε δευτερόλεπτα", - "opc-security": "Ασφάλεια", - "opc-identity": "Ταυτότητα", - "opc-keystore": "Keystore", - "opc-type": "Τύπος", - "opc-keystore-type": "Τύπος", - "opc-keystore-location": "τοποθεσία *", - "opc-keystore-password": "Κωδικός", - "opc-keystore-alias": "Ψευδώνυμο", - "opc-keystore-key-password": "Κωδικός Κλειδί", - "opc-device-node-pattern": "Μοτίβο κόμβου συσκευής", - "opc-device-name-pattern": "Μοτίβο ονόματος συσκευής", - "modbus-server": "Servers/slaves", - "modbus-add-server": "Προσθήκη server/slave", - "modbus-add-server-prompt": "Παρακαλούμε προσθέστε server/slave", - "modbus-transport": "Μεταφορά", - "modbus-tcp-reconnect": "Αυτόματη επανασύνδεση", - "modbus-rtu-over-tcp": "RTU over TCP", - "modbus-port-name": "Όνομα serial port", - "modbus-encoding": "Encoding", - "modbus-parity": "Parity", - "modbus-baudrate": "Baud rate", - "modbus-databits": "Data bits", - "modbus-stopbits": "Stop bits", - "modbus-databits-range": "Τα Data bits πρέπει να κυμαίνονται από 7 εώς 8.", - "modbus-stopbits-range": "Τα Stop bits πρέπει να κυμαίνονται από 1 εώς 2.", - "modbus-unit-id": "ID Μονάδας", - "modbus-unit-id-range": "Το ID Μονάδας πρέπει να κυμαίνεται από 1 εώς 247.", - "modbus-device-name": "Όνομα συσκευής", - "modbus-poll-period": "Poll period (ms)", - "modbus-attributes-poll-period": "Χαρακτηριστικά poll period (ms)", - "modbus-timeseries-poll-period": "Χρονική σειρά poll period (ms)", - "modbus-poll-period-range": "Το Poll period θα πρέπει να έχει θετική τιμή.", - "modbus-tag": "Ετικέτα", - "modbus-function": "Λειτουργία", - "modbus-register-address": "Καταχώριση διεύθυνσης", - "modbus-register-address-range": "η καταχωριμένη διεύθυνση πρεπει να κυμαίνεται από 0 ως 65535.", - "modbus-register-bit-index": "Bit index", - "modbus-register-bit-index-range": "Το Bit index πρέπει να κυμαίνεται από 0 ως 15.", - "modbus-register-count": "Μετρητής καταχώρησης", - "modbus-register-count-range": "Ο μετρητής καταχώρησης πρεέπει να έχει θετική τιμή.", - "modbus-byte-order": "Byte order", - "sync": { - "status": "Κατάσταση", - "sync": "Σε Συγχρονισμό", - "not-sync": "Δεν Συγχρονίζεται", - "last-sync-time": "Τελευταία φορά συγχρονισμού", - "not-available": "Μη διαθέσιμο" - }, - "export-extensions-configuration": "Εξαγωγή διαμόρφωσης επεκτάσεων", - "import-extensions-configuration": "Εισαγωγή διαμόρφωσης επεκτάσεων", - "import-extensions": "Εισαγωγή επεκτάσεων", - "import-extension": "Εισαγωγή επέκτασης", - "export-extension": "Εξαγωγή επέκτασης", - "file": "Επεκτάσεις αρχείου", - "invalid-file-error": "Μη έγκυρη επέκταση αρχείου" - }, - "fullscreen": { - "expand": "Ανάπτυξη σε πλήρη οθόνη", - "exit": "Έξοδος από πλήρη οθόνη", - "toggle": "Εναλλαγή σε πλήρη οθόνη", - "fullscreen": "Πλήρης οθόνη" - }, - "function": { - "function": "Λειτουργία" - }, - "grid": { - "delete-item-title": "Είστε σίγουροι ότι θέλετε να διαγραφεί αυτό το αντικείμενο?", - "delete-item-text": "Προσοχή, μετά την επιβεβαίωση, αυτό το στοιχείο και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "delete-items-title": "Είστε βέβαιοι ότι θέλετε να διαγράψετε { count, plural, 1 {1 item} other {# items} }?", - "delete-items-action-title": "Διαγραφή { count, plural, 1 {1 item} other {# items} }", - "delete-items-text": "Προσοχή, μετά την επιβεβαίωση, όλα τα επιλεγμένα στοιχεία θα καταργηθούν και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "add-item-text": "Προσθήκη νέου αντικειμένου", - "no-items-text": "Δεν βρέθηκαν αντικείμενα", - "item-details": "λεπτομέρειες αντικείμενου", - "delete-item": "Διαγραφή Αντικειμένου", - "delete-items": "Διαγραφή Αντικειμένων", - "scroll-to-top": "Κύλιση προς τα πάνω" - }, - "help": { - "goto-help-page": "Πηγαίνετε στη σελίδα" - }, - "home": { - "home": "Αρχικη", - "profile": "Προφίλ", - "logout": "Αποσυνδέση", - "menu": "Μενού", - "avatar": "Avatar", - "open-user-menu": "Άνοιγμα μενού χρήστη" - }, - "import": { - "no-file": "Δεν έχει επιλεχθεί αρχείο", - "drop-file": "Αποθέστε ένα αρχείο JSON ή κάνετε κλικ για να επιλέξετε ένα αρχείο για ανέβασμα.", - "drop-csv-file": "Αποθέστε ένα αρχείο CVS ή κάνετε κλικ για να επιλέξετε ένα αρχείο για ανέβασμα.", - "drop-file-csv": "Αποθέστε ένα αρχείο CSV ή κάνετε κλικ για να επιλέξετε ένα αρχείο για ανέβασμα.", - "column-value": "Τιμή", - "column-title": "Τίτλος", - "column-example": "Παράδειγμα δεδομένων", - "column-key": "Κλειδί χαρακτηριστικού/τηλεμετρίας", - "csv-delimiter": "Οριοθέτηση CSV", - "csv-first-line-header": "Η πρώτη σειρά περιέχει ονόματα στηλών", - "csv-update-data": "Ανανέωση χαρακτηριστικών/τηλεμετρίας", - "import-csv-number-columns-error": "Ένα αρχείο θα πρέπει να περιέχει τουλάχιστον δυο στήλες", - "import-csv-invalid-format-error": "Μη έγκυρη μορφή αρχείου. Σειρά: '{{line}}'", - "column-type": { - "name": "Όνομα", - "type": "Τύπος", - "column-type": "Τύπος στήλης", - "client-attribute": "Χαρακτηριστικό Client", - "shared-attribute": "Χαρακτηριστικό Shared", - "server-attribute": "Χαρακτηριστικό Server", - "timeseries": "Χρονική σειρά", - "entity-field": "Πεδίο Οντότητας", - "access-token": "Διακριτικό πρόσβασης" - }, - "stepper-text": { - "select-file": "Επιλογή αρχείου", - "configuration": "Εισαγωγή ρύθμισης", - "column-type": "Επιλογή τύπου στήλης", - "creat-entities": "Δημιουργία νέων οντοτήτων", - "done": "Ολοκληρώθηκε" - }, - "message": { - "create-entities": "{{count}} νέες οντότητες δημιουργήθηκαν με επιτυχία.", - "update-entities": "{{count}} οντότητες ενημερώθηκαν με επιτυχία.", - "error-entities": "Υπήρξε κάποιο σφάλμα κατά τη δημιουργία {{count}} οντοτήτων." - } - }, - "integration": { - "integration": "Ενσωμάτωση", - "integrations": "Ενσωματώσεις", - "select-integration": "Επιλογή ενσωμάτωσης", - "no-integrations-matching": "Δεν βρέθηκαν ενσωματώσεις πο να αντιστοιχούν σε '{{entity}}'.", - "integration-required": "Απαιτείται ενσωμάτωση", - "delete": "Διαγραφή ενσωμάτωσης", - "management": "Διαχείριση Ενσωματώσεων", - "add-integration-text": "Προσθήκη νέας ενσωμάτωσης", - "no-integrations-text": "Δεν βρέθηκαν ενσωματώσεις", - "selected-integrations": "{ count, plural, 1 {1 integration} other {# integrations} } επιλέχθηκαν", - "delete-integration-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε την ενσωμάτωση '{{integrationName}}';", - "delete-integration-text": "Προσοχή, μετά την επιβεβαίωση, η ενσωμάτωση και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "delete-integrations-title": "Είστε σίγουροι ότι θέλετε { count, plural, 1 {1 integration} other {# integrations} };", - "delete-integrations-action-title": "Διαγραφή { count, plural, 1 {1 integration} other {# integrations} }", - "delete-integrations-text": "Προσοχή, αφού ολοκληρωθεί η επιβεβαίωση, όλες οι επιλεγμένες ενσωματώσεις θα αφαιρεθούν και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "events": "Γεγονότα", - "add": "Προσθήκη Ενσωμάτωσης", - "integration-details": "Λεπτομέρειες Ενσωμάτωσης", - "details": "Λεπτομέρειες", - "copyId": "Αντιγραη ID ενσωμάτωσης", - "idCopiedMessage": "Το ID της ενσωμάτωσης έχει αντιγραφεί στο πρόχειρο.", - "debug-mode": "Λειτουργία απασφαλμάτωσης", - "enable-security": "Ενεργοποίηση ασφάλειας", - "headers-filter": "Φίλτρο Headers", - "header": "Header", - "no-headers-filter": "Όχι φίλτρο headers", - "downlink-url": "URL Λήψης", - "application-uri": "URI Εφαρμογής", - "as-id": "AS ID", - "as-id-required": "Απαιτείται AS ID.", - "as-key": "AS Key", - "as-key-required": "Απαιτείται AS Key.", - "max-time-diff-in-seconds": "Μέγιστη χρονική διαφορά (δευτερόλεπτα)", - "max-time-diff-in-seconds-required": "Απαιτείται μέγιστη χρονική διαφορά.", - "name": "Όνομα", - "name-required": "Απαιτείται όνομα.", - "description": "Περιγραφή", - "base-url": "Base URL", - "base-url-required": "Απαιτείται Base URL", - "security-key": "Κλειδί ασφάλειας", - "http-endpoint": "HTTP endpoint URL", - "copy-http-endpoint-url": "Αντιγραφή HTTP endpoint URL", - "http-endpoint-url-copied-message": "Το HTTP endpoint URL έχει αντιγραφεί στο πρόχειρο", - "host": "Host", - "host-required": "Απαιτείται Host.", - "host-type": "Τύπος Host", - "host-type-required": "Απαιτείται τύπος Host.", - "custom-host": "Προσαρμοσμένο host", - "custom-host-required": "Απαιτείται προσαρμοσμένο host.", - "port": "Port", - "port-required": "Απαιτείται Port.", - "port-range": "Η Port πρέπει να κυμαίνεται από 1 ως 65535.", - "connect-timeout": "Λήξη σύνδεσης (δευτερόλεπτα)", - "connect-timeout-required": "Απαιτείται λήξη σύνδεσης.", - "connect-timeout-range": "Η λήξη σύνδεσης πρέπει να κυμαίνεται από 1 ως 200.", - "client-id": "Client ID", - "clean-session": "Καθαρή συνεδρία session", - "enable-ssl": "Ενεργοποίηση SSL", - "credentials": "Διαπιστευτήρια", - "credentials-type": "Τύπος διαπιστευτηρίων", - "credentials-type-required": "Απαιτείται τύπος διαπιστευτηρίων.", - "username": "Όνομα Χρήστη", - "username-required": "Απαιτείται όνομα χρήστη.", - "password": "Κωδικός", - "password-required": "Απαιτείται κωδικός.", - "ca-cert": "Αρχείο πιστοποιητικού CA *", - "private-key": "Αρχείο ιδιωτικού κλειδιού *", - "private-key-password": "κωδικός ιδιωτικού κλειδιού", - "cert": "Αρχείο πιστοποιητικού *", - "no-file": "Δεν έχει επιλεχθεί αρχείο.", - "drop-file": "Αποθέστε αρχείο ή κάνετε κλικ για να επιλέξετε ένα αρχείο προς ανέβασμα.", - "topic-filters": "Φίτρο θέματος", - "remove-topic-filter": "Αφαίρεση φίλτρου θέματος", - "add-topic-filter": "προσθήκη φίλτρου θέματος", - "add-topic-filter-prompt": "παρακαλούμε προσθέστε φίλτρο θέματος", - "topic": "Θέμα", - "mqtt-qos": "QoS", - "mqtt-qos-at-most-once": "Το πολύ μια", - "mqtt-qos-at-least-once": "Τουλάχιστον μια", - "mqtt-qos-exactly-once": "Ακριβώς μια", - "downlink-topic-pattern": "Μοτίβο θέματος σύνδεσης", - "downlink-topic-pattern-required": "Απαιτείται μοτίβο θέματος σύνδεσης.", - "aws-iot-endpoint": "AWS IoT Endpoint", - "aws-iot-endpoint-required": "Απαιτείται AWS IoT Endpoint.", - "aws-iot-credentials": "AWS IoT Διαπιστευτήρια", - "application-credentials": "Διαπιστευτήρια Εφαρμογής", - "api-key": "API Key", - "api-key-required": "Απαιτείται API Key.", - "auth-token": "Τεκμίριο Ταυτοποίησης", - "auth-token-required": "Απαιτείται τεκμίριο ταυτοποίησης", - "region": "Περιοχή", - "region-required": "Απαιτείται περιοχή.", - "application-id": "ID Εφαρμογής", - "application-id-required": "Απαιτείται ID εφαρμογής.", - "access-key": "Κλειδί πρόσβασης", - "access-key-required": "Απαιτείται κλειδί πρόσβασης.", - "connection-parameters": "Παράμετροι σύνδεσης", - "service-bus-namespace-name": "Service Bus Namespace Name", - "service-bus-namespace-name-required": "Απαιτείται Service Bus Namespace Name.", - "event-hub-name": "Όνομα Event Hub", - "event-hub-name-required": "Απαιτείται όνομα Event Hub.", - "sas-key-name": "Όνομα SAS Key", - "sas-key-name-required": "Απαιτείται όνομα SAS Key.", - "sas-key": "SAS Key", - "sas-key-required": "Απαιτείται SAS Key.", - "iot-hub-name": "IoT Hub Name (απαιτείται για downlink)", - "metadata": "Μεταδεδομένα", - "type": "Τύπος", - "type-required": "Απαιτείται τύπος.", - "uplink-converter": "Μετατροπέας δεδομένων uplink", - "uplink-converter-required": "Απαιτείται μετατροπέας δεδομένων uplink.", - "downlink-converter": "Μετατροπέας δεδομένων downlink", - "type-http": "HTTP", - "type-ocean-connect": "OceanConnect", - "type-sigfox": "SigFox", - "type-thingpark": "ThingPark", - "type-tmobile-iot-cdp": "T-Mobile – IoT CDP", - "type-mqtt": "MQTT", - "type-aws-iot": "AWS IoT", - "type-ibm-watson-iot": "IBM Watson IoT", - "type-ttn": "TheThingsNetwork", - "type-azure-event-hub": "Azure Event Hub", - "type-opc-ua": "OPC-UA", - "opc-ua-application-name": "Όνομα εφαρμογής", - "opc-ua-application-uri": "URI Εφαρμογής", - "opc-ua-scan-period-in-seconds": "Περίοδος σάρωσης σε δευτερόλεπτα", - "opc-ua-scan-period-in-seconds-required": "Απαιτείται περίοδος σάρωσης", - "opc-ua-timeout": "Λήξη χρόνου σε χιλιοστά του δευτερολέπτου", - "opc-ua-timeout-required": "Απαιτείται λήξη χρόνου", - "opc-ua-security": "Ασφάλεια", - "opc-ua-security-required": "Απαιτειται ασφάλεια", - "opc-ua-identity": "Ταυτότητα", - "opc-ua-identity-required": "Απαιτείται ταυτότητα", - "opc-ua-keystore": "Keystore", - "add-opc-ua-keystore-prompt": "Παρακαλούμε προσθέστε αρχείο keystore", - "opc-ua-keystore-required": "Απαιτείται keystore", - "opc-ua-type": "Τύπος", - "opc-ua-keystore-type": "Τύπος", - "opc-ua-keystore-type-required": "Απαιτέιται τύπος", - "opc-ua-keystore-location": "Τοποθεσία *", - "opc-ua-keystore-password": "Κωδικός", - "opc-ua-keystore-password-required": "Απαιτείται κωδικός", - "opc-ua-keystore-alias": "Ψευδώνυμο", - "opc-ua-keystore-alias-required": "Απαιτείται ψευδώνυμο", - "opc-ua-keystore-key-password": "Κλειδί κωδικού", - "opc-ua-keystore-key-password-required": "Απαιτείται κλειδί κωδικού", - "opc-ua-mapping": "χαρτογράφιση", - "add-opc-ua-mapping-prompt": "Παρακαλούμε προσθέστε χαρτογράφιση", - "opc-ua-mapping-type": "Τύπος χαρτογράφισης", - "opc-ua-mapping-type-required": "Απαιτείται τύπος χαρτογράφισης", - "opc-ua-device-node-pattern": "Μοτίβο Κόμβου Συσκευής", - "opc-ua-device-node-pattern-required": "Απαιτείται Μοτίβο Κόμβου Συσκευής", - "opc-ua-namespace": "Namespace", - "opc-ua-add-map": "Προσθήκη στοιχείου χαρτογράφισης", - "subscription-tags": "Ετικέτες εγγραφής", - "remove-subscription-tag": "Αφαίρεση ετικέτας εγγραφής", - "add-subscription-tag": "Προσθήκη ετικέτας εγγραφής", - "add-subscription-tag-prompt": "Παρακαλούμε προσθέστε ετικέτα εγγραφής", - "key": "Κλειδί", - "path": "Μονοπάτι", - "required": "Απαιτείται" - }, - "item": { - "selected": "Επιλέχθηκε" - }, - "js-func": { - "no-return-error": "Η λειτουργία πρέπει να επιστρέφει τιμή!", - "return-type-mismatch": "Η λειτουργία πρέπει να επιστρέφει τιμή από '{{type}}' τύπο!", - "tidy": "Tidy" - }, - "key-val": { - "key": "Όνομα", - "value": "Τιμή", - "remove-entry": "Αφαίρεση καταχώρησης", - "add-entry": "Προσθήκη καταχωρησης", - "no-data": "Καμία καταχώρηση" - }, - "layout": { - "layout": "Διάταξη", - "manage": "Διαχείριση Διατάξεων", - "settings": "Ρυθμίσεις Διάταξης", - "color": "Χρώμα", - "main": "Κεντρικά", - "right": "Δεξιά", - "select": "Επιλογή διάταξης" - }, - "legend": { - "direction": "Κατεύθυνση Λεζάντας", - "position": "Θέση λεζάντας", - "show-max": "Προβολή μέγιστης τιμής", - "show-min": "Προβολή ελάχιστης τιμής", - "show-avg": "Προβολή μέσης τιμής", - "show-total": "Προβολή συνολικής τιμής", - "settings": "Ρυθμίσεις λεζάντας", - "min": "min", - "max": "max", - "avg": "Μ.Ο.", - "total": "Σύνολο" - }, - "login": { - "login": "Σύνδεση", - "request-password-reset": "Αίτημα επαναφοράς κωδικού πρόσβασης", - "reset-password": "Επαναφορά κωδικού πρόσβασης", - "create-password": "Δημιουργία κωδικού πρόσβασης", - "passwords-mismatch-error": "Οι καταχωρημένοι κωδικοί πρόσβασης πρέπει να είναι ίδιοι!", - "password-again": "Επανάληψη κωδικού πρόσβασης", - "sign-in": "Παρακαλώ, συνδεθείτε", - "username": "Όνομα Χρήστη (email)", - "remember-me": "Να με Θυμάσαι", - "forgot-password": "Ξεχάσατε τον κωδικό πρόσβασης;", - "password-reset": "Επαναφορά κωδικού πρόσβασης", - "new-password": "Νέος κωδικός πρόσβασης", - "new-password-again": "Επανάληψη νέου κωδικού πρόσβασης", - "password-link-sent-message": "Ο σύνδεσμος επαναφοράς κωδικού πρόσβασης στάλθηκε με επιτυχία!", - "email": "Email", - "no-account": "Δεν έχετε λογαριασμό;", - "create-account": "Δημιουργία λογαριασμού", - "login-with": "Σύνδεση μέσω {{name}}", - "or": "ή" - }, - "signup": { - "firstname": "Όνομα", - "lastname": "Επίθετο", - "email": "Email", - "signup": "Εγγραφή", - "create-password": "Δημιουργία κωδικού", - "repeat-password": "Επαναλάβετε τον κωδικό", - "have-account": "Έχετε ήδη έναν λογαριασμό;", - "signin": "Είσοδος", - "no-captcha-message": "Πρέπει να επιβεβαιώσετε ότι δεν είστε ρομπότ", - "password-length-message": "Ο κωδικός σας πρεπει να περιλαμβάνει τουλάχιστον 6 χαρακτήρες", - "email-verification": "Επιβεβαίωση email", - "email-verification-message": "Έχει σταλεί ένα email επιβεβαίωσης στην διεύθυνση email που καθορίσατε.
    Παρακαλούμε ακολουθήστε τις οδηγίες που συμπεριλαμβανονται στο email ώστε να ολοκληρωθεί η εγγραφή σας.
    Σημείωση: Αν δεν έχει εμφανιστεί σχετικά άμεσα το email, παρακαλούμε ελέγξτε στον φάκελο με την 'Ανεπιθύμητη Αλληλογραφία' (spam) ή ξαναπροσπαθείστε να στείλετε το email κάνοντας κλικ στο κουμπί 'Αποστολή εκ νέου'", - "account-activation-title": "Ενεργοποίηση λογαριασμού", - "account-activated": "Ο λογαριασμός ενεργοποιήθηκε με επιτυχία!", - "account-activated-text": "Συγχαρητήρια!
    Ο λογαριασμός σας έχει ενεργοποιηθεί.
    Τώρα μπορείται να κάνετε είσοδο στην πλατφόρμα.", - "resend": "Αποστολή εκ νέου", - "inactive-user-exists-title": "Ο ανενεργός χρήστης υπάρχει ήδη", - "inactive-user-exists-text": "Υπάρχει ήδη εγγεγραμμένος χρήστης με μη επιβεβαιωμένη διεύθυνση email.
    Click Πατήστε το κουμπί 'Επαναποστολή', αν θέλετε να στείλετε ξανά email επιβεβαίωσης.", - "activating-account": "Ενεργοποίηση λογαριασμού...", - "activating-account-text": "Ο λογαριασμός σας ενεργοποιείται αυτήν τη στιγμή. Παρακαλούμε περιμένετε...", - "accept-privacy-policy": "Αποδοχή Πολιτικής Απορρήτου", - "accept": "Αποδοχή", - "privacy-policy": "Πολιτική Απορρήτου" - }, - "position": { - "top": "Κορυφή", - "bottom": "Κάτω μέρος", - "left": "Αριστερά", - "right": "Δεξιά" - }, - "profile": { - "profile": "Προφίλ", - "change-password": "Αλλαγή κωδικού", - "current-password": "Τρέχων κωδικός" - }, - "relation": { - "relations": "Σχέσεις", - "direction": "Κατεύθυνση", - "search-direction": { - "FROM": "Από", - "TO": "Σε" - }, - "direction-type": { - "FROM": "από", - "TO": "σε" - }, - "from-relations": "Εξωτερικές σχέσεις", - "to-relations": "Εσωτερικές σχέσεις", - "selected-relations": "{ count, plural, 1 {1 relation} άλλες {# relations} } επιλέχθηκαν", - "type": "Τύπος", - "to-entity-type": "Στον τύπο οντότητας", - "to-entity-name": "Στο όνομα οντότητας", - "from-entity-type": "Από τύπο οντότητας", - "from-entity-name": "Από όνομα οντότητας", - "to-entity": "Σε οντότητα", - "from-entity": "Από οντότητα", - "delete": "Διαγραφή σχέσης", - "relation-type": "Relation type", - "relation-type-required": "Απαιτείται τύπος σχέσης.", - "any-relation-type": "Οποιοσδήποτε τύπος", - "add": "Προσθήκη σχέσης", - "edit": "Επεξεργασία σχέσης", - "delete-to-relation-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε τη σχέση με την οντότητα '{{entityName}}'?", - "delete-to-relation-text": "Προσοχή, μετά την επιβεβαίωση η οντότητα '{{entityName}}' δεν θα σχετίζεται με την τρέχουσα οντότητα.", - "delete-to-relations-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 relation} άλλες {# relations} };", - "delete-to-relations-text": "Προσοχή, μετά την επιβεβαίωση όλες οι επιλεγμένες σχέσεις θα αφαιρεθούν και οι αντίστοιχες οντότητες δεν θα σχετίζονται με την τρέχουσα οντότητα.", - "delete-from-relation-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε σχέση από την οντότητα '{{entityName}}';", - "delete-from-relation-text": "Προσοχή, μετά την επιβεβαίωση η τρέχουσα οντότητα δεν θα σχετίζεται με την οντότητα '{{entityName}}'.", - "delete-from-relations-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 relation} other {# relations} };", - "delete-from-relations-text": "Προσοχή, μετά την επιβεβαίωση όλες οι επιλεγμένες σχέσεις θα αφαιρεθούν και η τρέχουσα οντότητα δεν θα σχετίζεται με τις αντίστοιχες οντότητες.", - "remove-relation-filter": "Αφαίρεση φίλτρου σχέσης", - "add-relation-filter": "Προσθήκη φίλτρου σχέσης", - "any-relation": "Οποιαδήποτε σχέση", - "relation-filters": "Φίλτρα σχέσεων", - "additional-info": "Συμπληρωματικές πληροφορίες (JSON)", - "invalid-additional-info": "Δεν είναι δυνατή η ανάλυση των πρόσθετων πληροφοριών json." - }, - "rulechain": { - "rulechain": "Αλυσίδα Κανόνων", - "rulechains": "Κανόνες", - "root": "Ρίζα", - "delete": "Διαγραφή Αλυσίδας Κανόνων", - "name": "Όνομα", - "name-required": "Απαιτείται όνομα.", - "description": "Περιγραφή", - "add": "Προσθήκη Αλυσίδας Κανόνων", - "set-root": "Δημιουργία ριζικής Αλυσίδας Κανόνων", - "set-root-rulechain-title": "Είστε σίγουροι ότι θέλετε να δημιουργήσετε τη ριζική Αλυσίδα Κανόνων '{{ruleChainName}}' ;", - "set-root-rulechain-text": "Μετά την επιβεβαίωση, η Αλυσίδα Κανόνων θα γίνει ριζική και θα χειριστεί όλα τα εισερχόμενα μηνύματα μεταφοράς.", - "delete-rulechain-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε την Αλυσίδα Κανόνων '{{ruleChainName}}';", - "delete-rulechain-text": "Προσοχή, μετά την επιβεβαίωση η Αλυσίδα Κανόνων θα αφαιρεθεί και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "delete-rulechains-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 rule chain} other {# rule chains} };", - "delete-rulechains-action-title": "Διαγραφή { count, plural, 1 {1 rule chain} other {# rule chains} }", - "delete-rulechains-text": "Προσοχή, μετά την επιβεβαίωση όλες οι επιλεγμένες αλυσίδες κανόνων θα καταργηθούν και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "add-rulechain-text": "Προσθήκη νέας Αλυσίδας Κανόνων", - "no-rulechains-text": "Δεν βρέθηκαν Αλυσίδες Κανόνων", - "rulechain-details": "Λεπτομέρειες Αλυσίδας Κανόνων", - "details": "Λεπομέρειες", - "events": "Γεγονότα", - "system": "Σύστημα", - "import": "Εισαγωγή Αλυσίδας Κανόνων", - "export": "Εξαγωγή Αλυσίδας Κανόνων", - "export-failed-error": "Δεν είναι δυνατή η εξαγωγή Αλυσίδας Κανόνων: {{error}}", - "create-new-rulechain": "Δημιουργία νέας Αλυσίδας Κανόνων", - "rulechain-file": "Αρχείο Αλυσίδας Κανόνων", - "invalid-rulechain-file-error": "Δεν είναι δυνατή η εισαγωγή Αλυσίδας Κανόνων: Μη έγκυρη δομή δεδομένων Αλυσίδας Κανόνων.", - "copyId": "Αντιγραφή ταυτότητας Αλυσίδας Κανόνων", - "idCopiedMessage": "Η ταυτότητα της Αλυσίδας Κανόνων έχει αντιγραφεί στο πρόχειρο", - "select-rulechain": "Επιλογή Αλυσίδας Κανόνων", - "no-rulechains-matching": "Δεν βρέθηκαν Αλυσίδες Κανόνων που να ταιριάζουν '{{entity}}'.", - "rulechain-required": "Απαιτείται Αλυσίδα Κανόνων", - "management": "Διαχείριση κανόνων", - "debug-mode": "Λειτουργία Εκσφαλμάτωσης" - }, - "rulenode": { - "details": "Λεπτομέρειες", - "events": "Γεγονότα", - "search": "Αναζήτηση κόμβων", - "open-node-library": "Άνοιγμα βιβλιοθήκης κόμβων", - "add": "Προσθήκη κανόνα κόμβου", - "name": "Όνομα", - "name-required": "Απαιτείται όνομα.", - "type": "Τύπος", - "description": "Περιγραφή", - "delete": "Διαγραφή κανόνα κόμβου", - "select-all-objects": "Επιλογή όλων των κόμβων και συνδέσεων", - "deselect-all-objects": "Αποεπιλογή όλων των κόμβων και συνδέσεων", - "delete-selected-objects": "Διαγραφή επιλεγμένων κόμβων και συνδέσεων", - "delete-selected": "Delete selected", - "select-all": "Select all", - "copy-selected": "Επιλογή αντιγραφής", - "deselect-all": "Αποεπιλογή όλων", - "rulenode-details": "Λεπτομέρειες κανόνα κόμβου", - "debug-mode": "Λειτουργία εντοπισμού σφαλμάτων", - "configuration": "Διαμόρφωση", - "link": "Σύνδεσμος", - "link-details": "Λεπτομέρειες συνδέσμου κανόνα κόμβου", - "add-link": "Προσθήκη συνδέσμου", - "link-label": "Ετικέτα συνδέσμου", - "link-label-required": "Απαιτείται ετικετα συνδέσμου.", - "custom-link-label": "Ετικέτα προσαρμοσμένου συνδέσμου", - "custom-link-label-required": "Απαιτείται ετικέτα προσαρμοσμένου συνδέσμου.", - "link-labels": "Ετικέτες συνδέσμου", - "link-labels-required": "Απαιτούνται ετικέτες συνδέσμου.", - "no-link-labels-found": "Δεν βρέθηκαν ετικέτες συνδέσμου", - "no-link-label-matching": "Δεν βρέθηκαν '{{label}}'.", - "create-new-link-label": "Δημιουργήστε μια νέα!", - "type-filter": "Φίλτρο", - "type-filter-details": "Φιλτράρετε εισερχόμενα μηνύματα με διαμορφωμένες συνθήκες", - "type-enrichment": "Εμπλουτισμός", - "type-enrichment-details": "Προσθέστε επιπλέον πληροφορίες στα Μεταδεδομένα του μηνύματος", - "type-transformation": "Μεταμόρφωση", - "type-transformation-details": "Αλλαγή ωφέλιμου φορτίου και μεταδεδομένων μηνύματος", - "type-action": "Ενέργεια", - "type-action-details": "Εκτέλεση ειδικής ενέργειας", - "type-analytics": "Analytics", - "type-analytics-details": "Εκτελέστε ανάλυση δεδομένων που διαβιβάζονται με ροή ή συνεχίζονται", - "type-external": "Εξωτερικός", - "type-external-details": "Αλληλεπίδραση με εξωτερικό σύστημα", - "type-rule-chain": "Αλυσίδα κανόνων", - "type-rule-chain-details": "Προωθεί τα εισερχόμενα μηνύματα σε συγκεκριμένη αλυσίδα κανόνων", - "type-input": "Εισαγωγήγή", - "type-input-details": "Λογική εισαγωγή της αλυσίδας κανόνων, προωθεί τα εισερχόμενα μηνύματα στον επόμενο σχετικό κανόνα κόμβου", - "type-unknown": "Άγνωστο", - "type-unknown-details": "Ανεπεξέργαστος κανόνας κόμβου", - "directive-is-not-loaded": "Δεν είναι διαθέσιμη καθορισμένη οδηγία διαμόρφωσης '{{directiveName}}'.", - "ui-resources-load-error": "Αποτυχία φόρτωσης διαμόρφωσης πόρων ui.", - "invalid-target-rulechain": "Δεν είναι δυνατή η επίλυση της αλυσίδας κανόνα στόχου!", - "test-script-function": "Λειτουργία σεναρίου δοκιμής", - "message": "Μήνυμα", - "message-type": "Τύπος μηνύματος", - "select-message-type": "Επιλογή τύπου μηνύματος", - "message-type-required": "Απαιτείται τύπος μηνύματος", - "metadata": "Μεταδεδομένα", - "metadata-required": "Οι καταχωρίσεις μεταδεδομένων δεν μπορούν να είναι κενές.", - "output": "Απόδοση", - "test": "Τεστ", - "help": "Βοήθεια", - "reset-debug-mode": "Επαναφορά λειτουργίας εντοπισμού σφαλμάτων σε όλους τους κόμβους" - }, - "role": { - "role": "Ρόλος", - "roles": "Ρόλοι", - "management": "Διαχείριση ρόλων", - "view-roles": "Προβολή ρόλων", - "no-roles-matching": "Δεν βρέθηκαν ρόλοι που να ταιριάζουν '{{entity}}'.", - "role-list": "Λίστα ρόλων", - "add": "Προσθήκη ρόλου", - "view": "Προβολή ρόλου", - "no-roles-text": "Δεν βρέθηκαν ρόλοι", - "role-details": "Λεπτομέρειες ρόλου", - "add-role-text": "Προσθήκη νέου ρόλου", - "delete": "Διαγραφή ρόλου", - "delete-roles": "Διαγραφή ρόλων", - "delete-role-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε το ρόλο '{{roleName}}';", - "delete-role-text": "Προσοχή, μετά την επιβεβαίωση ο ρόλος και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "delete-roles-title": "Είστε σίγουροι ότι θέλετε να παίξετε { count, plural, 1 {1 role} other {# roles} };", - "delete-roles-action-title": "Διαγραφή { count, plural, 1 {1 role} other {# roles} }", - "delete-roles-text": "Προσοχή, μετά την επιβεβαίωση όλοι οι επιλεγμένοι ρόλοι θα καταργηθούν και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "role-type": "΄Τύπος ρόλου", - "role-type-required": "Απαιτείται τύπος ρόλου.", - "select-role-type": "Επιλογή τύπου ρόλου", - "enter-role-type": "Εισαγωγή τύπου ρόλου", - "any-role": "Οποιοσδήποτε ρόλος", - "no-role-types-matching": "Δεν βρέθηκαν τύποι ρόλων που να ταιριάζουν '{{entitySubtype}}'.", - "role-type-list-empty": "Δεν έγινε επιλογή τύπου ρόλου.", - "role-types": "Τύποι ρόλου", - "name": "Όνομα", - "name-required": "Απαιτείται όνομα.", - "description": "Περιγραφή", - "events": "Γεγονότα", - "details": "Λεπτομέρειες", - "copyId": "Αντιγραφή ταυτότητας ρόλου", - "idCopiedMessage": "Η ταυτότητα ρόλου έχει αντιγραφεί στο πρόχειρο", - "permissions": "Άδειες", - "role-required": "Απαιτείται ρόλος", - "display-type": { - "GENERIC": "Γενικός", - "GROUP": "Ομάδα" - } - }, - "group-permission": { - "user-group-roles": "Ρόλοι ομάδας χρηστών", - "entity-group-permissions": "Άδειες ομάδας Οντοτήτων", - "role-type": "Τύπος ρόλου", - "role-name": "Όνομα ρόλου", - "group-type": "Τύπος ομάδας", - "group-name": "Όνομα ομάδας", - "group-owner": "Κάτοχος ομάδας", - "user-group-name": "Όνομα χρήστη ομάδας", - "user-group-owner": "Κάτοχος χρήστη ομάδας", - "edit": "Επεξεργασία Αδειών", - "delete": "Διαγραφή αδειών", - "selected-group-permissions": "{ count, plural, 1 {1 group permission} other {# group permissions} } επιλέχθηκαν", - "delete-group-permission-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε την άδεια ομάδας '{{roleName}}';", - "delete-group-permission-text": "Προσοχή, μετά την επιβεβαίωση η άδεια ομάδας και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "delete-group-permission": "Διαγραφή άδειας ομάδας", - "delete-group-permissions-title": "Έίστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 group permission} other {# group permission} };", - "delete-group-permissions-text": "Προσοχή, μετά την επιβεβαίωση όλες οι επιλεγμένες άδειες ομάδας θα καταργηθούν και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "delete-group-permissions": "Διαγραφή αδειών ομάδας", - "add-group-permission": "Προσθήκη ομαδικής άδειας", - "edit-group-permission": "Επεξεργασία άδειας ομάδας", - "entity-group": "Ομάδα οντοτήτων", - "user-group": "Χρήστης ομάδας", - "no-owners-matching": "Δεν βρέθηκαν κάτοχοι που να ταιριάζουν '{{owner}}'.", - "target-owner-required": " Απαιτείται κάτοχος της ομάδας οντότητας.", - "target-user-group-owner-required": "Απαιτείται κάτοχος χρήστη ομάδας." - }, - "permission": { - "permissions-required": "Τουλάχιστον μία εγγραφή άδειας πρέπει να οριστεί.", - "remove-permission": "Καταργήστε την καταχώριση δικαιωμάτων", - "add-permission": "Προσθήκη καταχώρησης άδειας", - "resource": { - "resource": "Πηγή", - "select-resource": "Επιλογή πηγής", - "resource-required": "Απαιτείται πηγή", - "no-resources-matching": "Δεν βρέθηκαν πηγές που να ταιριάζουν '{{resource}}'.", - "display-type": { - "ALL": "Όλες", - "PROFILE": "Προφίλ", - "ADMIN_SETTINGS": "Ρυθμίσεις Διαχειριστή", - "ALARM": "Alarm", - "DEVICE": "Συσκευή", - "ASSET": "Asset", - "CUSTOMER": "Πελάτης", - "DASHBOARD": "Dashboard", - "ENTITY_VIEW": "Προβολή οντοτήτων", - "TENANT": "Μισθωτής", - "RULE_CHAIN": "Αλυσίδα Κανόνα", - "USER": "Χρήστης", - "WIDGETS_BUNDLE": "Δέσμη Widgets", - "WIDGET_TYPE": "Τύπος Widget", - "CONVERTER": "Μετατροπέας", - "INTEGRATION": "Ενσωμάτωση", - "SCHEDULER_EVENT": "Πρόγραμμα εκδηλώσεων", - "BLOB_ENTITY": "Δέσμη Οντότητας", - "CUSTOMER_GROUP": "Ομάδα Πελατών", - "DEVICE_GROUP": "Ομάδα Συσκευών", - "ASSET_GROUP": "Ομάδες Asset", - "USER_GROUP": "Ομάδα Χρηστών", - "ENTITY_VIEW_GROUP": "Ομάδα Οντοτήτων", - "DASHBOARD_GROUP": "Ομάδα Dashboard", - "ROLE": "Ρόλος", - "GROUP_PERMISSION": "Ομαδική Άδεια", - "WHITE_LABELING": "Εμφάνιση", - "AUDIT_LOG": "Αρχείο Ελέγχου" - } - }, - "operation": { - "operation": "Εργασία", - "operations": "Εργασίες", - "operations-required": "Τουλάχιστον μία εργασία πρέπει να καθοριστεί.", - "enter-operation": "Εισαγωγή εργασίας", - "no-operations-matching": "Δεν βρέθηκαν εργασίες που να ταιριάζουν '{{operation}}'.", - "display-type": { - "ALL": "'Ολες", - "CREATE": "Δημιουργία", - "READ": "Ανάγνωση", - "WRITE": "Εγγραφή", - "DELETE": "Διαγραφή", - "ASSIGN_TO_CUSTOMER": "Ανάθεση σε Πελάτη", - "UNASSIGN_FROM_CUSTOMER": "Αποσύνδεση από Πελάτη", - "RPC_CALL": "Κλήση RPC", - "READ_CREDENTIALS": "Ανάγνωση Διαπιστευτηρίων", - "WRITE_CREDENTIALS": "Γράψτε Διαπιστευτήρια", - "READ_ATTRIBUTES": "Ανάγνωση Χαρακτηριστικών", - "WRITE_ATTRIBUTES": "Γράψτε Χαρακτηριστικά", - "READ_TELEMETRY": "Ανάγνωση Τηλεμετρίας", - "WRITE_TELEMETRY": "Γράψτε Τηλεμετρία", - "CLAIM_DEVICES": "Αιτήματα Συσκευών", - "IMPERSONATE": "Impersonate", - "CHANGE_OWNER": "Αλλαγή Κατόχου", - "ADD_TO_GROUP": "Προσθήκη στην Ομάδα", - "REMOVE_FROM_GROUP": "Αφαίρεση από την Ομάδα" - } - } - }, - "scheduler": { - "scheduler": "Προγραμματιστής", - "scheduler-event": "Προγραμματισμένο γεγονός", - "select-scheduler-event": "Επιλέξτε προγραμματισμένα γεγονότα που να ταιριάζουν '{{entity}}'", - "scheduler-event-required": "Απαιτείται προγραμματισμένο γεγονός", - "management": "Διαχείριση Προγράμματος", - "scheduler-events": "Προγραμματισμένα γεγονότα", - "add-scheduler-event": "Προσθήκη προγραμματισμένου γεγονότος", - "search-scheduler-events": "Αναζήτηση προγραμματισμένων γεγονότων", - "created-time": "Χρόνος που δημιουργήθηκε", - "name": "Όνομα", - "type": "Τύπος", - "created_customer": "Πελάτης που δημιουργήθηκε", - "edit-scheduler-event": "Επεξεργασία προγραμματισμένου γεγονότος", - "view-scheduler-event": "Προβολή προγραμματισμένου γεγονότος", - "delete-scheduler-event": "Διαγραφή προγραμματισμένου γεγονότος", - "no-scheduler-events": "Δεν βρέθηκαν προγραμματισμένα γεγονότα", - "selected-scheduler-events": "{ count, plural, 1 {1 scheduler event} other {# scheduler events} } επιλέχθηκαν", - "delete-scheduler-event-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε το προγραμματισμένο γεγονός '{{schedulerEventName}}';", - "delete-scheduler-event-text": "Προσοχή, μετά την επιβεβαίωση το προγραμματισμένο γεγονός και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "delete-scheduler-events-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 scheduler event} other {# scheduler events} };", - "delete-scheduler-events-text": "Προσοχή, μετά την επιβεβαίωση όλα τα επιλεγμένα προγραμματισμένα γεγονότα θα καταργηθούν και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "create": "Δημιουργία προγραμματισμένου γεγονότος", - "edit": "Επεξεργασία προγραμματισμένου γεγονότος", - "view": "Προβολή προγραμματισμένου γεγονότος", - "name-required": "Απαιτείται Όνομα", - "configuration": "Διαμόρφωση", - "schedule": "Πρόγραμμα", - "start": "Έναρξη", - "date": "Ημερομηνία", - "time": "Ώρα", - "repeat": "Επανάληψη", - "repeats": "Επαναλήψεις", - "daily": "Καθημερινά", - "weekly": "Εβδομαδιαία", - "timer": "Χρονομετρητής", - "repeats-required": "Απαιτούνται επαναλήψεις.", - "repeat-on": "Επανάληψη σε", - "repeat-every": "Επανάληψη κάθε", - "ends-on": "Τελειώνει σε", - "sunday-label": "K", - "monday-label": "Δ", - "tuesday-label": "T", - "wednesday-label": "Τ", - "thursday-label": "Π", - "friday-label": "Π", - "saturday-label": "Σ", - "repeat-on-sunday": "Επανάληψη την Κυριακή", - "repeat-on-monday": "Επανάληψη τη Δευτέρα", - "repeat-on-tuesday": "Επανάληψη την Τρίτη", - "repeat-on-wednesday": "Επανάληψη την Τετάρτη", - "repeat-on-thursday": "Επανάληψη την Πέμπτη", - "repeat-on-friday": "Επανάληψη την Παρασκευή", - "repeat-on-saturday": "Επανάληψη το Σάββατο", - "event-type": "Τύπος Γεγονότος", - "select-event-type": "Επιλογή Τύπου Γεγονότος", - "event-type-required": "Απαιτείται Τύπος Γεγονότος.", - "list-mode": "Προβολή Λίστας", - "calendar-mode": "Προβολή Ημερολογίου", - "calendar-view-type": "Προβολή τύπου ημερολογίου", - "month": "Μήνας", - "week": "Εβδομάδα", - "day": "Ημέρα", - "agenda-week": "Εβδομαδιαία Ατζέντα", - "agenda-day": "Ημερήσια Ατζέντα", - "list-year": "Λίστα Έτους", - "list-month": "Λίστα Μήνα", - "list-week": "Λίστα Εβδομάδος", - "list-day": "Λίστα Ημέρας", - "today": "Σήμερα", - "navigate-before": "Πλοηγηθείτε Πριν", - "navigate-next": "Πλοηγηθείτε Μετά", - "starting-from": "Έναρξη Από", - "until": "μέχρι", - "on": "σε", - "sunday": "Κυριακή", - "monday": "Δευτέρα", - "tuesday": "Τρίτη", - "wednesday": "Τετάρτη", - "thursday": "Πέμπτη", - "friday": "Παρασκευή", - "saturday": "Σάββατο", - "originator": "Δημιουργός" , - "single-entity": "Ενιαία Οντότητα", - "group-of-entities": "Ομάδα Οντοτήτων", - "single-device": "Ενιαία Συσκευή", - "group-of-devices": "Ομάδα Συσκευών", - "message-body": "Σώμα μηνυμάτων", - "target": "Στόχος", - "rpc-method": "Μέθοδος", - "rpc-method-required": "Απαιτείται μέθοδος", - "rpc-params": "Παράμετροι", - "select-dashboard-state": "Επιλογή κατάστασης πίνακα ελέγχου", - "hours": "Ώρες", - "minutes": "Λεπτά", - "seconds": "Δευτερόλεπτα", - "time-interval-required": "Απαιτείται χρονικό διάστημα", - "time-unit-required": "Απαιτείται μονάδα ώρας" - }, - "report": { - "report-config": "Διαμόρφωση αναφοράς", - "email-config": "Διαμόρφωση email", - "dashboard-state-param": "Τιμή παραμέτρου κατάστασης πίνακα", - "base-url": "Βάση URL", - "base-url-required": "Απαιτείται Βάση URL.", - "use-dashboard-timewindow": "Χρησιμοποιήστε το χρονικό πλαίσιο του πίνακα ελέγχου", - "timewindow": "Χρονικό πλαίσιο", - "name-pattern": "Αναφορά πρότυπου ονόματος", - "name-pattern-required": "Απαιτείται αναφορά πρότυπου ονόματος", - "type": "Τύπος αναφοράς", - "use-current-user-credentials": "Χρησιμοποιήστε τα τρέχοντα διαπιστευτήρια του χρήστη", - "customer-user-credentials": "Διαπιστευτήρια του χρήστη του πελάτη", - "customer-user-credentials-required": "Απαιτούνται διαπιστευτήρια του χρήστη του πελάτη", - "generate-test-report": "Δημιουργία αναφοράς δοκιμής", - "send-email": "Αποστολή email", - "from": "Από", - "from-required": "Απαιτείται Από.", - "to": "Σε", - "to-required": "Απαιτείται Σε.", - "cc": "Cc", - "bcc": "Bcc", - "subject": "Θέμα", - "subject-required": "Απαιτείται Θέμα.", - "body": "Σώμα", - "body-required": "Απαιτείται Σώμα." - }, - "blob-entity": { - "blob-entity": "Ογκώδης οντότητα", - "select-blob-entity": "Επιλογή ογκώδους οντότητας", - "no-blob-entities-matching": "Δεν βρέθηκαν ογκώδεις οντότητες που να ταιριάζουν '{{entity}}'.", - "blob-entity-required": "Απαιτείται ογκώδης οντότητα", - "files": "Αρχεία", - "search": "Αναζήτηση αρχείων", - "clear-search": "Εκκαθάριση αναζήτησης", - "no-blob-entities-prompt": "Δεν βρέθηκαν αρχεία", - "report": "Αναφορά", - "created-time": "Χρόνος που δημιουργήθηκε", - "name": "Όνομα", - "type": "Τύπος", - "created_customer": "Δημιουργήθηκε από Πελάτη", - "download-blob-entity": "Λήψη αρχείου", - "delete-blob-entity": "Διαγραφή αρχείου", - "delete-blob-entity-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε το αρχείο '{{blobEntityName}}';", - "delete-blob-entity-text": "Προσοχή, μετά την επιβεβαίωση τα δεδομένα αρχείου θα διαγραφούν οριστικά." - }, - "timezone": { - "timezone": "Ζώνη Ώρας", - "select-timezone": "Επιλογή Ζώνης Ώρας", - "no-timezones-matching": "Δεν βρέθηκαν Ζώνες ώρας που να ταιριάζουν'{{timezone}}' .", - "timezone-required": "Απαιτείται Ζώνη Ώρας." - }, - "tenant": { - "tenant": "Μισθωτής", - "tenants": "Μισθωτές", - "management": "Διαχείριση Μισθωτών", - "add": "Πρόσθεση Μισθωτή", - "admins": "Διαχειριστές", - "manage-tenant-admins": "Επεξεργασία των διαχειριστών του Μισθωτή", - "delete": "Διαγραφή Μισθωτή", - "add-tenant-text": "Πρόσθεση νέου Μισθωτή", - "no-tenants-text": "Δεν βρέθηκαν Μισθωτές", - "tenant-details": "Λεπτομέρειες Μισθωτή", - "delete-tenant-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε τον Μισθωτή '{{tenantTitle}}';", - "delete-tenant-text": "Προσοχή, μετά την επιβεβαίωση ο Μισθωτής και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "delete-tenants-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 tenant} other {# tenants} };", - "delete-tenants-action-title": "Διαγραφή { count, plural, 1 {1 tenant} other {# tenants} }", - "delete-tenants-text": "Προσοχή, μετά την επιβεβαίωση όλοι οι επιλεγμένοι Μισθωτές θα αφαιρεθούν και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "title": "Τίτλος", - "title-required": "Απαιτείται Τίτλος.", - "description": "Περιγραφή", - "details": "Λεπτομέρειες", - "events": "Γεγονότα", - "copyId": "Αντιγραφή Ταυτότητας του Μισθωτή", - "idCopiedMessage": "Η Ταυτότητα του Μισθωτή έχει αντιγραφεί στο πρόχειρο", - "select-tenant": "Επιλογή Μισθωτή", - "no-tenants-matching": "Δεν βρέθηκαν Μισθωτές που να ταιριάζουν '{{entity}}'.", - "tenant-required": "Απαιτείται Μισθωτής", - "selected-tenants": "{ count, plural, 1 {1 tenant} other {# tenants} } επιλέχθηκαν", - "search": "Αναζήτηση Μισθωτών", - "allow-white-labeling": "Επιτρέπεται Προσαρμογή Εμφάνισης", - "allow-customer-white-labeling": "Επιτρέπεται Προσαρμογή Εμφάνισης Πελάτη" - }, - "timeinterval": { - "seconds-interval": "{ seconds, plural, 1 {1 second} other {# seconds} }", - "minutes-interval": "{ minutes, plural, 1 {1 minute} other {# minutes} }", - "hours-interval": "{ hours, plural, 1 {1 hour} other {# hours} }", - "days-interval": "{ days, plural, 1 {1 day} other {# days} }", - "days": "Ημέρες", - "hours": "Ώρες", - "minutes": "Λεπτά", - "seconds": "Δευτερόλεπτα", - "advanced": "Προηγμένος" - }, - "timewindow": { - "days": "{ days, plural, 1 { day } other {# days } }", - "hours": "{ hours, plural, 0 { hour } 1 {1 hour } other {# hours } }", - "minutes": "{ minutes, plural, 0 { minute } 1 {1 minute } other {# minutes } }", - "seconds": "{ seconds, plural, 0 { second } 1 {1 second } other {# seconds } }", - "realtime": "Πραγματικός Χρόνος", - "history": "Ιστορικό", - "last-prefix": "Τελευταίος", - "period": "από {{ startTime }} σε {{ endTime }}", - "edit": "Επεξεργασία Χρονικού Πλαισίου", - "date-range": "Εύρος ημερομηνιών", - "last": "Τελευταίος", - "time-period": "Χρονική Περίοδος" - }, - "user": { - "user": "Χρήστης", - "users": "Χρήστες", - "management": "Διαχείριση Χρηστών", - "customer-users": "Χρήστες του Πελάτη", - "tenant-admins": "Διαχειριστές Μισθωτή", - "sys-admin": "Διαχειριστής Συστήματος", - "tenant-admin": "Διαχειριστής Μισθωτή", - "customer": "Πελάτης", - "anonymous": "Ανώνυμος", - "add": "Προσθήκη χρήστη", - "delete": "Διαγραφή χρήστη", - "add-user-text": "Προσθήκη νέου Χρήστη", - "no-users-text": "Δεν βρέθηκαν Χρήστες", - "user-details": "Λεπτομέρειες Χρήστη", - "delete-users": "Διαγραφή Χρηστών", - "delete-user-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε το Χρήστη '{{userEmail}}'?", - "delete-user-text": "Προσοχή, μετά την επιβεβαίωση ο Χρήστης και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "delete-users-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 user} other {# users} };", - "delete-users-action-title": "Διαγραφή { count, plural, 1 {1 user} other {# users} }", - "delete-users-text": "Προσοχή, μετά την επιβεβαίωση όλοι οι επιλεγμένοι Χρήστες θα αφαιρεθούν και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "activation-email-sent-message": "Το email ενεργοποίησης στάλθηκε με επιτυχία!", - "resend-activation": "Επανάληψη ενεργοποίησης", - "email": "Email", - "email-required": "Απαιτείται email.", - "invalid-email-format": "Μη έγκυρη μορφή email.", - "first-name": "Όνομα", - "last-name": "Επίθετο", - "description": "Περιγραφή", - "default-dashboard": "Προκαθορισμένος πίνακας ελέγχου", - "always-fullscreen": "Πάντα με πλήρη οθόνη", - "select-user": "Επιλογή Χρήστη", - "no-users-matching": "Δεν βρέθηκαν χρήστες που να ταιριάζουν '{{entity}}'.", - "user-required": "Απαιτείται Χρήστης", - "activation-method": "Μέθοδος ενεργοποίησης", - "display-activation-link": "Εμφάνιση συνδέσμου ενεργοποίησης", - "send-activation-mail": "Αποστολή mail ενεργοποίησης", - "activation-link": "Σύνδεσμος ενεργοποίησης Χρήστη", - "activation-link-text": "΄Προκειμένου να ενεργοποιήσετε το Χρήστη, χρησιμοποιήστε το εξής activation link :", - "copy-activation-link": "Αντιγραφή συνδέσμου ενεργοποίησης", - "activation-link-copied-message": "Ο σύνδεσμος ενεργοποίησης χρήστη έχει αντιγραφεί στο πρόχειρο", - "selected-users": "{ count, plural, 1 {1 user} other {# users} } επιλέχθηκαν", - "search": "Αναζήτηση Χρηστών", - "details": "Λεπτομέρειες", - "login-as-tenant-admin": "Συνδεθείτε ως Διαχειριστής Μισθωτή", - "login-as-customer-user": "Συνδεθείτε ως Χρήστης του Πελάτη", - "select-group-to-add": "Επιλέξτε την ομάδα προορισμού για να προσθέσετε επιλεγμένους Χρήστες", - "select-group-to-move": "Επιλέξτε την ομάδα προορισμού για να μετακινήσετε επιλεγμένους χρήστες", - "remove-users-from-group": "Είστε σίγουροι ότι θέλετε να καταργήσετε { count, plural, 1 {1 user} other {# users} } από την ομάδα '{entityGroup}';", - "group": "Ομάδα από Χρήστες", - "list-of-groups": "{ count, plural, 1 {One user group} other {List of # user groups} }", - "group-name-starts-with": "Ομάδες Χρηστών των οποίων τα ονόματα ξεκινούν με'{{prefix}}'" - }, - "value": { - "type": "Είδος τιμής", - "string": "Συμβολοσειρά", - "string-value": "Τιμή συμβολοσειράς", - "integer": "Ακέραιος", - "integer-value": "Ακέραια τιμή", - "invalid-integer-value": "Μη έγκυρη ακέραια τιμή", - "double": "Πραγματικός", - "double-value": "Πραγματική τιμή", - "boolean": "Λογικό", - "boolean-value": "Λογική τιμή", - "false": "Εσφαλμένος", - "true": "Αληθής", - "long": "Μακρύς" - }, - "widget": { - "widget-library": "Βιβλιοθήκη Widget", - "widget-bundle": "Δέσμη Widget", - "select-widgets-bundle": "Επιλογή δέσμης Widgets", - "management": "Διαχείριση Widget", - "editor": "Συντάκτης Widget", - "widget-type-not-found": "Πρόβλημα φόρτωσης διαμόρφωσης Widget.
    Probably associated\n widget type was removed.", - "widget-type-load-error": "Το Widget δεν φορτώθηκε λόγω των παρακάτω σφαλμάτων:", - "remove": "Αφαίρεση Widget", - "edit": "Επεξεργασία Widget", - "remove-widget-title": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε το Widget '{{widgetTitle}}'?", - "remove-widget-text": "Προσοχή, μετά την επιβεβαίωση το widget και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "timeseries": "Χρονική σειρά", - "search-data": "Αναζήτηση δεδομένων", - "no-data-found": "Δεν βρέθηκαν δεδομένα", - "latest-values": "Τελευταίες αξίες", - "rpc": "Έλεγχος Widget", - "alarm": "Alarm widget", - "static": "Στατικό widget", - "select-widget-type": "Επιλογή τύπου Widget", - "missing-widget-title-error": "Ο τίτλος Widget πρέπει να καθοριστεί!", - "widget-saved": "Το Widget αποθηκεύτηκε", - "unable-to-save-widget-error": "Δεν είναι δυνατή η αποθήκευση του Widget! Το Widget έχει σφάλματα!", - "save": "Αποθήκευση widget", - "saveAs": "Αποθήκευση widget ως", - "save-widget-type-as": "Αποθήκευση τύπου Widget type ως", - "save-widget-type-as-text": "Παρακαλούμε εισάγετε νέο τίτλο Widget και/ή επιλέξετε στοχευμένη δέσμη widgets", - "toggle-fullscreen": "Λειτουργεία πλήρους οθόνης", - "run": "Εκτέλεση Widget", - "title": "Τίτλος Widget", - "title-required": "Απαιτείται τίτλος Widget.", - "type": "Τύπος Widget", - "resources": "Πόροι", - "resource-url": "JavaScript/CSS URL", - "remove-resource": "Αφαίρεση πηγής", - "add-resource": "Προσθήκη πηγής", - "html": "HTML", - "tidy": "Τακτοποιημένος", - "css": "CSS", - "settings-schema": "Ρυθμίσεις σχήματος", - "datakey-settings-schema": "Πλήκτρο δεδομένων σχήματος ρυθμίσεων", - "javascript": "Javascript", - "remove-widget-type-title": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε τον τύπο Widget '{{widgetName}}'?", - "remove-widget-type-text": "Προσοχή, μετά την επιβεβαίωση ο τύπος Widget και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "remove-widget-type": "Αφαίρεση τύπου Widget", - "add-widget-type": "Προσθήκη νέου τύπου Widget", - "widget-type-load-failed-error": "Αποτυχία φόρτωσης τύπου Widget!", - "widget-template-load-failed-error": "Αποτυχία φόρτωσης προτύπου Widget!", - "add": "Προσθήκη Widget", - "undo": "Αναίρεση αλλαγών Widget", - "export": "Εξαγωγή Widget", - "export-data": "Εξαγωγή δεδομένων Widget", - "export-to-csv": "Εξαγωγή δεδομένων σε CSV...", - "export-to-excel": "Εξαγωγή δεδομένων σε XLS...", - "no-data": "Δεν υπάρχουν δεδομένα για εμφάνιση στο Widget" - }, - "widget-action": { - "header-button": "Κουμπί κεφαλίδας στη νέα κατάσταση του πίνακα ελέγχου", - "update-dashboard-state": "Ενημέρωση της τρέχουσας κατάστασης του dashboard", - "open-dashboard": "Πλοήγηση σε άλλο dashboard", - "custom": "Προσαρμοσμένη ενέργεια", - "target-dashboard-state": "Κατάσταση προορισμού dashboard", - "target-dashboard-state-required": "Απαιτείται κατάσταση προορισμού dashboard", - "set-entity-from-widget": "Ορισμός οντότητας από Widget", - "target-dashboard": "dashboard προορισμού", - "open-right-layout": "Ανοίξτε τη δεξιά διάταξη του dashboard (προβολή κινητού)" - }, - "widgets-bundle": { - "current": "Τρέχουσα δέσμη", - "widgets-bundles": "Δέσμες Widgets", - "add": "Προσθήκη δέσμης Widgets", - "delete": "Διαγραφή δέσμης Widgets", - "title": "Τίτλος", - "title-required": "Απαιτείται τίτλος.", - "add-widgets-bundle-text": "Προσθήκη νέας δέσμης widgets", - "no-widgets-bundles-text": "Δεν βρέθηκαν δέσμες widgets", - "empty": "Η δέσμη Widgets είναι κενή", - "details": "Λεπτομέρειες", - "widgets-bundle-details": "Λεπτομέρειες δέσμης Widgets", - "delete-widgets-bundle-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε τη δέσμη Widgets '{{widgetsBundleTitle}}';", - "delete-widgets-bundle-text": "Προσοχή, μετά την επιβεβαίωση η δέσμη Widget και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "delete-widgets-bundles-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 widgets bundle} other {# widgets bundles} };", - "delete-widgets-bundles-action-title": "Διαγραφή { count, plural, 1 {1 widgets bundle} other {# widgets bundles} }", - "delete-widgets-bundles-text": "Προσοχή, μετά την επιβεβαίωση όλες οι επιλεγμένες δέσμες Widget και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", - "no-widgets-bundles-matching": "Δεν βρέθηκαν δέσμες Widgets που να ταιριάζουν'{{widgetsBundle}}'.", - "widgets-bundle-required": "Απαιτείται δέσμη Widgets.", - "system": "Σύστημα", - "import": "Εισαγωγή δέσμης Widgets", - "export": "Εξαγωγή δέσμης Widgets", - "export-failed-error": "Δεν είναι δυνατή η εξαγωγή δέσμης Widgets: {{error}}", - "create-new-widgets-bundle": "Δημιουργία νέας δέσμης Widgets", - "widgets-bundle-file": "Αρχείο δέσμης Widgets", - "invalid-widgets-bundle-file-error": "Δεν είναι δυνατή η εισαγωγή δέσμης Widgets: Μη έγκυρη δομή δεδομένων Widgets." - }, - "widget-config": { - "data": "Δεδομένα", - "settings": "Ρυθμίσεις", - "advanced": "Προηγμένος", - "title": "Τίτλος", - "general-settings": "Γενικές ρυθμίσεις", - "display-title": "Εμφάνιση τίτλου", - "drop-shadow": "Σκίαση", - "enable-fullscreen": "Ενεργοποίηση πλήρους οθόνης", - "enable-data-export": "Ενεργοποίηση εξαγωγής δεδομένων", - "background-color": "Χρώμα φόντου", - "text-color": "Χρώμα κειμένου", - "padding": "Εσωτερικό περιθώριο", - "margin": "Περιθώριο", - "widget-style": "Στυλ Widget", - "title-style": "Στυλ τίτλου", - "mobile-mode-settings": "Ρυθμίσεις λειτουργίας κινητού", - "order": "Εντολή", - "height": "Ύψος", - "units": "Ειδικό σύμβολο για εμφάνιση δίπλα στην αξία", - "decimals": "Αριθμός ψηφίων μετά το κυμαινόμενο σημείο", - "timewindow": "Timewindow", - "use-dashboard-timewindow": "Χρήση dashboard timewindow", - "display-timewindow": "Απεικόνιση timewindow", - "display-legend": "Απεικόνιση λεζάντας", - "datasources": "Πηγές δεδομένων", - "maximum-datasources": "Το μέγιστο { count, plural, 1 {1 datasource is allowed.} other {# datasources are allowed} }", - "datasource-type": "Τύπος", - "datasource-parameters": "Παράμετροι", - "remove-datasource": "Κατάργηση της πηγής δεδομένων", - "add-datasource": "Προσθήκη πηγής δεδομένων", - "target-device": "Target device", - "alarm-source": "Πηγή Alarm", - "actions": "Ενέργειες", - "action": "ενέργεια", - "add-action": "Προσθήκη ενέργειας", - "search-actions": "Αναζήτηση ενεργειών", - "action-source": "Πηγή ενέργειας", - "action-source-required": "Απαιτείται πηγή ενέργειας.", - "action-name": "Όνομα", - "action-name-required": "Απαιτείται όνομα ενέργειας.", - "action-name-not-unique": "Μια άλλη ενέργεια με το ίδιο όνομα υπάρχει ήδη.
    Το όνομα ενέργειας πρέπει να είναι μοναδικό μέσα στην ίδια πηγή ενέργειας.", - "action-icon": "Εικονίδιο", - "action-type": "Τύπος", - "action-type-required": "Απαιτείται τύπος ενέργειας.", - "edit-action": "Επεξεργασία ενέργειας", - "delete-action": "Διαγραφή ενέργειας", - "delete-action-title": "Διαγραφή ενέργειας Widget", - "delete-action-text": "Είστε σίγουροι ότι θέλετε να διαγράψετε δράση widget με όνομα '{{actionName}}';" - }, - "widget-type": { - "import": "Εισαγωγή τύπου Widget", - "export": "Εξαγωγή τύπου Widget", - "export-failed-error": "Δεν είναι δυνατή η εξαγωγή τύπου Widget: {{error}}", - "create-new-widget-type": "Δημιουργία νέου τύπου Widget", - "widget-type-file": "Αρχείο τύπου Widget", - "invalid-widget-type-file-error": "Δεν είναι δυνατή η εισαγωγή τύπου Widget: Μη έγκυρη δομή δεδομένων τύπου Widget." - }, - "self-registration": { - "self-registration": "Αυτόματη εγγραφή", - "self-registration-url": "Αυτόματη εγγραφή URL", - "captcha-site-key": "reCAPTCHA κλειδί ιστότοπου", - "captcha-secret-key": "reCAPTCHA μυστικό κλειδί", - "notification-email": "Εmail γνωστοποίησης", - "privacy-policy-text": "Κείμενο πολιτικής απορρήτου", - "text-message-page": "Μήνυμα κειμένου για τη σελίδα εγγραφής" - }, - "white-labeling": { - "white-labeling": "Εμφάνιση", - "login-white-labeling": "Εμφάνιση Σύδεσης", - "preview": "Προεπισκόπηση", - "app-title": "Τίτλος εφαρμογής", - "favicon": "Εικονίδιο Ιστότοπου", - "favicon-description": "Εικόνα *.ico, *.gif or *.png με μέγιστο μέγεθος {{kbSize}} KBytes.", - "favicon-size-error": "Η εικόνα ιστότοπου είναι πολύ μεγάλη. Μέγιστο επιτρεπόμενο μέγεθος {{kbSize}} KBytes.", - "favicon-type-error": "Μη έγκυρη μορφή αρχείου εικόνας ιστότοπου. Μόνο εικόνες ICO, GIF ή PNG γίνονται αποδεκτές.", - "drop-favicon-image": "Σύρετε ένα εικονίδιο ιστότοπου ή κάντε κλικ για να επιλέξετε ένα αρχείο για μεταφόρτωση.", - "no-favicon-image": "Δεν έχει επιλεχθεί εικονίδιο", - "logo": "Logo", - "logo-description": "Οποιαδήποτε εικόνα με μέγιστο μέγεθος {{kbSize}} KBytes.", - "logo-size-error": "Η εικόνα του λογότυπου είναι πολύ μεγάλη. Μέγιστο επιτρεπόμενο μέγεθος {{kbSize}} KBytes.", - "logo-type-error": "Μη έγκυρη μορφή αρχείου λογότυπου. Μόνο εικόνες είναι αποδεκτές.", - "drop-logo-image": "Σείρετε μια εικόνα λογότυπου ή κάντε κλικ για να επιλέξετε ένα αρχείο για μεταφόρτωση.", - "no-logo-image": "Δεν έχει επιλεγεί λογότυπο", - "logo-height": "Ύψος λογότυπου, px", - "primary-palette": "Κύρια παλέτα", - "accent-palette": "Παλέτα τονισμών", - "customize-palette": "Προσαρμογή", - "edit-palette": "Επεξεργασία παλέτας", - "save-palette": "Αποθήκευση παλέτας", - "primary-background": "Κύριο χρώμα υποβάθρου", - "secondary-background": "Δευτερεύον χρώμα υποβάθρου", - "hue1": "HUE 1", - "hue2": "HUE 2", - "hue3": "HUE 3", - "page-background-color": "Χρώμα υποβάθρου σελίδας", - "dark-foreground": "Σκοτεινό χρώμα προσκηνίου", - "domain-name": "Όνομα Domain", - "help-link-base-url": "Base url για συνδέσμους βοηθείας", - "enable-help-links": "Ενεργοποίηση συνδέσμων βοηθείας", - "error-verification-url": "Ένα όνομα domain δεν πρέπει να περιέχει σύμβολα '/' και ':'. Παράδειγμα: gprs.cloud", - "show-platform-name-version": "Εμφάνιση ονόματος και έκδοσης πλατφόρμας", - "platform-name": "Όνομα πλατφόρμας", - "platform-version": "Έκδοση πλατφόρμας", - "version-mask": "{{name}} v.{{verion}}", - "position": { - "label": "Όνομα πλατφόρμας και θέση έκδοσης", - "under-logo": "Κάτω από το λογότυπο", - "bottom": "Στο κάτω μέρος της φόρμας σύνδεσης" - } - }, - "widgets": { - "date-range-navigator": { - "localizationMap": { - "Sun": "Κυρ", - "Mon": "Δευ", - "Tue": "Τρι", - "Wed": "Τετ", - "Thu": "Πεμ", - "Fri": "Παρ", - "Sat": "Σαβ", - "Jan": "Ιαν", - "Feb": "Φεβ", - "Mar": "Μαρ", - "Apr": "Απρ", - "May": "Μάιος", - "Jun": "Ιουν", - "Jul": "Ιουλ", - "Aug": "Αυγ", - "Sep": "Σεπ", - "Oct": "Οκτ", - "Nov": "Νοε", - "Dec": "Δεκ", - "January": "Ιανουάριος", - "February": "Φεβρουάριος", - "March": "Μάρτιος", - "April": "Απρίλιος", - "June": "Ιούνιος", - "July": "Ιούλιος", - "August": "Αύγουστος", - "September": "Σεπτέμβριος", - "October": "Οκτώβριος", - "November": "Νοέμβριος", - "December": "Δεκέμβριος", - "Custom Date Range": "Προσαρμοσμένο εύρος ημερομηνιών", - "Date Range Template": "Πρότυπο εύρους ημερομηνιών", - "Today": "Σήμερα", - "Yesterday": "Χθες", - "This Week": "Αυτή την εβοδομάδα", - "Last Week": "Την προηγούμενη εβδομάδα", - "This Month": "Αυτόν τον μήνα", - "Last Month": "Τον προηγούμενο μήνα", - "Year": "Έτος", - "This Year": "Αυτό το χρόνο", - "Last Year": "Τον προηγούμενο χρόνο", - "Date picker": "Επιλογέας ημερομηνίας", - "Hour": "Ώρα", - "Day": "Ημέρα", - "Week": "Εβδομάδα", - "2 weeks": "2 Εβδομάδες", - "Month": "Μήνας", - "3 months": "3 Μήνες", - "6 months": "6 Μήνες", - "Custom interval": "Προσαρμοσμένο διάστημα", - "Interval": "Διάστημα", - "Step size": "Μέγεθος βήματος", - "Ok": "Ok" - } - } - }, - "icon": { - "icon": "Εικονίδιο", - "select-icon": "Επιλογή εικονιδίου ", - "material-icons": "Υλικά εικονίδια", - "show-all": "Προβολή όλων των εικονιδίων" - }, - "subscription": { - "entity-limit-text": "Ωστόσο, μπορείτε να αναβαθμίσετε το πρόγραμμα εγγραφής σας για να αυξήσετε τα όριά σας.", - "upgrade-your-plan": "Αναβάθμιση του σχεδίου συνδρομής", - "white-labeling-feature": "Εμφάνιση χαρακτηριστικού", - "white-labeling-text-full": "Αλλάξτε το διακριτικό τίτλο της πλατφόρμας με το λογότυπο της εταιρείας ή του προϊόντος σας και το συνδυασμό χρωμάτων σε 2 λεπτά.

    Αφαιρέστε το \"Powered By\" στο κάτω μέρος του dashboard.
    Δεν απαιτείται κώδικας ή επανεκκίνηση υπηρεσίας. Επιτρέψτε και στους πελάτες σας να αλλάξουν το περιβάλλον τους.", - "enable-white-labeling": "Ενεργοποίηση εμφάνισης χαρακτηριστικού τώρα αναβαθμίζοντας το σχέδιο εγγραφής σας!", - "read-more": "Διαβάστε περισσότερα", - "white-labeling-video-text": "Δείτε το εκπαιδευτικό βίντεο παρακάτω για να δείτε πώς λειτουργεί αυτό το χαρακτηριστικό!" - }, - "subscription-error": { - "title": "Παραβίαση συνδρομής", - "warning-title": "Προειδοποίηση συνδρομής", - "limit-reached": { - "device-count": "Έχετε φτάσει τις μέγιστες συσκευές ({{value}}) που επιτρέπονται από το πρόγραμμα εγγραφής σας!", - "asset-count": "Έχετε φτάσει τα μέγιστα assets ({{value}}) που επιτρέπονται από το πρόγραμμα εγγραφής σας!" - }, - "feature-disabled": { - "white-labeling": "Η εμφάνιση χαρακτηριστικού δεν επιτρέπεται από το σχέδιο εγγραφής σας!" - } - }, - "custom": { - "widget-action": { - "action-cell-button": "Κουμπί ενέργειας κελιών", - "row-click": "Κλικ στη σειρά", - "polygon-click": "Κλικ στο πολύγωνο", - "marker-click": "Κλικ στο δείκτη", - "tooltip-tag-action": "Ετικέτα εργαλείου ενέργειας", - "node-selected": "Στον επιλεγμένο κόμβο", - "element-click": "Κλικ στο στοιχείο HTML" - } - }, - "language": { - } -} diff --git a/ui/src/app/locale/locale.constant-en_US.json b/ui/src/app/locale/locale.constant-en_US.json deleted file mode 100644 index 046fa4df66..0000000000 --- a/ui/src/app/locale/locale.constant-en_US.json +++ /dev/null @@ -1,1928 +0,0 @@ -{ - "access": { - "unauthorized": "Unauthorized", - "unauthorized-access": "Unauthorized Access", - "unauthorized-access-text": "You should sign in to have access to this resource!", - "access-forbidden": "Access Forbidden", - "access-forbidden-text": "You haven't access rights to this location!
    Try to sign in with different user if you still wish to gain access to this location.", - "refresh-token-expired": "Session has expired", - "refresh-token-failed": "Unable to refresh session" - }, - "action": { - "activate": "Activate", - "suspend": "Suspend", - "save": "Save", - "saveAs": "Save as", - "cancel": "Cancel", - "ok": "OK", - "delete": "Delete", - "add": "Add", - "yes": "Yes", - "no": "No", - "update": "Update", - "remove": "Remove", - "search": "Search", - "clear-search": "Clear search", - "assign": "Assign", - "unassign": "Unassign", - "share": "Share", - "make-private": "Make private", - "apply": "Apply", - "apply-changes": "Apply changes", - "edit-mode": "Edit mode", - "enter-edit-mode": "Enter edit mode", - "decline-changes": "Decline changes", - "close": "Close", - "back": "Back", - "run": "Run", - "sign-in": "Sign in!", - "edit": "Edit", - "view": "View", - "create": "Create", - "drag": "Drag", - "refresh": "Refresh", - "undo": "Undo", - "copy": "Copy", - "paste": "Paste", - "copy-reference": "Copy reference", - "paste-reference": "Paste reference", - "import": "Import", - "export": "Export", - "share-via": "Share via {{provider}}", - "continue": "Continue", - "discard-changes": "Discard Changes", - "download": "Download" - }, - "aggregation": { - "aggregation": "Aggregation", - "function": "Data aggregation function", - "limit": "Max values", - "group-interval": "Grouping interval", - "min": "Min", - "max": "Max", - "avg": "Average", - "sum": "Sum", - "count": "Count", - "none": "None" - }, - "admin": { - "general": "General", - "general-settings": "General Settings", - "outgoing-mail": "Mail Server", - "outgoing-mail-settings": "Outgoing Mail Server Settings", - "system-settings": "System Settings", - "test-mail-sent": "Test mail was successfully sent!", - "base-url": "Base URL", - "base-url-required": "Base URL is required.", - "prohibit-different-url": "Prohibit to use hostname from the client request headers", - "prohibit-different-url-hint": "This setting should be enabled for production environments. May cause security issues when disabled", - "mail-from": "Mail From", - "mail-from-required": "Mail From is required.", - "smtp-protocol": "SMTP protocol", - "smtp-host": "SMTP host", - "smtp-host-required": "SMTP host is required.", - "smtp-port": "SMTP port", - "smtp-port-required": "You must supply a smtp port.", - "smtp-port-invalid": "That doesn't look like a valid smtp port.", - "timeout-msec": "Timeout (msec)", - "timeout-required": "Timeout is required.", - "timeout-invalid": "That doesn't look like a valid timeout.", - "enable-tls": "Enable TLS", - "tls-version": "TLS version", - "enable-proxy": "Enable proxy", - "proxy-host": "Proxy host", - "proxy-host-required": "Proxy host is required.", - "proxy-port": "Proxy port", - "proxy-port-required": "You must supply a proxy port.", - "proxy-port-invalid": "That doesn't look like a valid proxy port.", - "proxy-user": "Proxy user", - "proxy-password": "Proxy password", - "send-test-mail": "Send test mail", - "security-settings": "Security settings", - "password-policy": "Password policy", - "minimum-password-length": "Minimum password length", - "minimum-password-length-required": "Minimum password length is required", - "minimum-password-length-range": "Minimum password length should be in a range from 5 to 50", - "minimum-uppercase-letters": "Minimum number of uppercase letters", - "minimum-uppercase-letters-range": "Minimum number of uppercase letters can't be negative", - "minimum-lowercase-letters": "Minimum number of lowercase letters", - "minimum-lowercase-letters-range": "Minimum number of lowercase letters can't be negative", - "minimum-digits": "Minimum number of digits", - "minimum-digits-range": "Minimum number of digits can't be negative", - "minimum-special-characters": "Minimum number of special characters", - "minimum-special-characters-range": "Minimum number of special characters can't be negative", - "password-expiration-period-days": "Password expiration period in days", - "password-expiration-period-days-range": "Password expiration period in days can't be negative", - "password-reuse-frequency-days": "Password reuse frequency in days", - "password-reuse-frequency-days-range": "Password reuse frequency in days can't be negative", - "general-policy": "General policy", - "max-failed-login-attempts": "Maximum number of failed login attempts, before account is locked", - "minimum-max-failed-login-attempts-range": "Maximum number of failed login attempts can't be negative", - "user-lockout-notification-email": "In case user account lockout, send notification to email" - }, - "alarm": { - "alarm": "Alarm", - "alarms": "Alarms", - "select-alarm": "Select alarm", - "no-alarms-matching": "No alarms matching '{{entity}}' were found.", - "alarm-required": "Alarm is required", - "alarm-status": "Alarm status", - "search-status": { - "ANY": "Any", - "ACTIVE": "Active", - "CLEARED": "Cleared", - "ACK": "Acknowledged", - "UNACK": "Unacknowledged" - }, - "display-status": { - "ACTIVE_UNACK": "Active Unacknowledged", - "ACTIVE_ACK": "Active Acknowledged", - "CLEARED_UNACK": "Cleared Unacknowledged", - "CLEARED_ACK": "Cleared Acknowledged" - }, - "no-alarms-prompt": "No alarms found", - "created-time": "Created time", - "type": "Type", - "severity": "Severity", - "originator": "Originator", - "originator-type": "Originator type", - "details": "Details", - "status": "Status", - "alarm-details": "Alarm details", - "start-time": "Start time", - "end-time": "End time", - "ack-time": "Acknowledged time", - "clear-time": "Cleared time", - "severity-critical": "Critical", - "severity-major": "Major", - "severity-minor": "Minor", - "severity-warning": "Warning", - "severity-indeterminate": "Indeterminate", - "acknowledge": "Acknowledge", - "clear": "Clear", - "search": "Search alarms", - "selected-alarms": "{ count, plural, 1 {1 alarm} other {# alarms} } selected", - "no-data": "No data to display", - "polling-interval": "Alarms polling interval (sec)", - "polling-interval-required": "Alarms polling interval is required.", - "min-polling-interval-message": "At least 1 sec polling interval is allowed.", - "aknowledge-alarms-title": "Acknowledge { count, plural, 1 {1 alarm} other {# alarms} }", - "aknowledge-alarms-text": "Are you sure you want to acknowledge { count, plural, 1 {1 alarm} other {# alarms} }?", - "aknowledge-alarm-title": "Acknowledge Alarm", - "aknowledge-alarm-text": "Are you sure you want to acknowledge Alarm?", - "clear-alarms-title": "Clear { count, plural, 1 {1 alarm} other {# alarms} }", - "clear-alarms-text": "Are you sure you want to clear { count, plural, 1 {1 alarm} other {# alarms} }?", - "clear-alarm-title": "Clear Alarm", - "clear-alarm-text": "Are you sure you want to clear Alarm?", - "alarm-status-filter": "Alarm Status Filter", - "max-count-load": "Maximum number of alarms to load (0 - unlimited)", - "max-count-load-required": "Maximum number of alarms to load is required.", - "max-count-load-error-min": "Minimum value is 0.", - "fetch-size": "Fetch size", - "fetch-size-required": "Fetch size is required.", - "fetch-size-error-min": "Minimum value is 10." - }, - "alias": { - "add": "Add alias", - "edit": "Edit alias", - "name": "Alias name", - "name-required": "Alias name is required", - "duplicate-alias": "Alias with same name is already exists.", - "filter-type-single-entity": "Single entity", - "filter-type-entity-list": "Entity list", - "filter-type-entity-name": "Entity name", - "filter-type-state-entity": "Entity from dashboard state", - "filter-type-state-entity-description": "Entity taken from dashboard state parameters", - "filter-type-asset-type": "Asset type", - "filter-type-asset-type-description": "Assets of type '{{assetType}}'", - "filter-type-asset-type-and-name-description": "Assets of type '{{assetType}}' and with name starting with '{{prefix}}'", - "filter-type-device-type": "Device type", - "filter-type-device-type-description": "Devices of type '{{deviceType}}'", - "filter-type-device-type-and-name-description": "Devices of type '{{deviceType}}' and with name starting with '{{prefix}}'", - "filter-type-entity-view-type": "Entity View type", - "filter-type-entity-view-type-description": "Entity Views of type '{{entityView}}'", - "filter-type-entity-view-type-and-name-description": "Entity Views of type '{{entityView}}' and with name starting with '{{prefix}}'", - "filter-type-relations-query": "Relations query", - "filter-type-relations-query-description": "{{entities}} that have {{relationType}} relation {{direction}} {{rootEntity}}", - "filter-type-asset-search-query": "Asset search query", - "filter-type-asset-search-query-description": "Assets with types {{assetTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}", - "filter-type-device-search-query": "Device search query", - "filter-type-device-search-query-description": "Devices with types {{deviceTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}", - "filter-type-entity-view-search-query": "Entity view search query", - "filter-type-entity-view-search-query-description": "Entity views with types {{entityViewTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}", - "entity-filter": "Entity filter", - "resolve-multiple": "Resolve as multiple entities", - "filter-type": "Filter type", - "filter-type-required": "Filter type is required.", - "entity-filter-no-entity-matched": "No entities matching specified filter were found.", - "no-entity-filter-specified": "No entity filter specified", - "root-state-entity": "Use dashboard state entity as root", - "last-level-relation": "Fetch last level relation only", - "root-entity": "Root entity", - "state-entity-parameter-name": "State entity parameter name", - "default-state-entity": "Default state entity", - "default-entity-parameter-name": "By default", - "max-relation-level": "Max relation level", - "unlimited-level": "Unlimited level", - "state-entity": "Dashboard state entity", - "all-entities": "All entities", - "any-relation": "any" - }, - "asset": { - "asset": "Asset", - "assets": "Assets", - "management": "Asset management", - "view-assets": "View Assets", - "add": "Add Asset", - "assign-to-customer": "Assign to customer", - "assign-asset-to-customer": "Assign Asset(s) To Customer", - "assign-asset-to-customer-text": "Please select the assets to assign to the customer", - "no-assets-text": "No assets found", - "assign-to-customer-text": "Please select the customer to assign the asset(s)", - "public": "Public", - "assignedToCustomer": "Assigned to customer", - "make-public": "Make asset public", - "make-private": "Make asset private", - "unassign-from-customer": "Unassign from customer", - "delete": "Delete asset", - "asset-public": "Asset is public", - "asset-type": "Asset type", - "asset-type-required": "Asset type is required.", - "select-asset-type": "Select asset type", - "enter-asset-type": "Enter asset type", - "any-asset": "Any asset", - "no-asset-types-matching": "No asset types matching '{{entitySubtype}}' were found.", - "asset-type-list-empty": "No asset types selected.", - "asset-types": "Asset types", - "name": "Name", - "name-required": "Name is required.", - "description": "Description", - "type": "Type", - "type-required": "Type is required.", - "details": "Details", - "events": "Events", - "add-asset-text": "Add new asset", - "asset-details": "Asset details", - "assign-assets": "Assign assets", - "assign-assets-text": "Assign { count, plural, 1 {1 asset} other {# assets} } to customer", - "delete-assets": "Delete assets", - "unassign-assets": "Unassign assets", - "unassign-assets-action-title": "Unassign { count, plural, 1 {1 asset} other {# assets} } from customer", - "assign-new-asset": "Assign new asset", - "delete-asset-title": "Are you sure you want to delete the asset '{{assetName}}'?", - "delete-asset-text": "Be careful, after the confirmation the asset and all related data will become unrecoverable.", - "delete-assets-title": "Are you sure you want to delete { count, plural, 1 {1 asset} other {# assets} }?", - "delete-assets-action-title": "Delete { count, plural, 1 {1 asset} other {# assets} }", - "delete-assets-text": "Be careful, after the confirmation all selected assets will be removed and all related data will become unrecoverable.", - "make-public-asset-title": "Are you sure you want to make the asset '{{assetName}}' public?", - "make-public-asset-text": "After the confirmation the asset and all its data will be made public and accessible by others.", - "make-private-asset-title": "Are you sure you want to make the asset '{{assetName}}' private?", - "make-private-asset-text": "After the confirmation the asset and all its data will be made private and won't be accessible by others.", - "unassign-asset-title": "Are you sure you want to unassign the asset '{{assetName}}'?", - "unassign-asset-text": "After the confirmation the asset will be unassigned and won't be accessible by the customer.", - "unassign-asset": "Unassign asset", - "unassign-assets-title": "Are you sure you want to unassign { count, plural, 1 {1 asset} other {# assets} }?", - "unassign-assets-text": "After the confirmation all selected assets will be unassigned and won't be accessible by the customer.", - "copyId": "Copy asset Id", - "idCopiedMessage": "Asset Id has been copied to clipboard", - "select-asset": "Select asset", - "no-assets-matching": "No assets matching '{{entity}}' were found.", - "asset-required": "Asset is required", - "name-starts-with": "Asset name starts with", - "import": "Import assets", - "asset-file": "Asset file", - "label": "Label" - }, - "attribute": { - "attributes": "Attributes", - "latest-telemetry": "Latest telemetry", - "attributes-scope": "Entity attributes scope", - "scope-latest-telemetry": "Latest telemetry", - "scope-client": "Client attributes", - "scope-server": "Server attributes", - "scope-shared": "Shared attributes", - "add": "Add attribute", - "key": "Key", - "last-update-time": "Last update time", - "key-required": "Attribute key is required.", - "value": "Value", - "value-required": "Attribute value is required.", - "delete-attributes-title": "Are you sure you want to delete { count, plural, 1 {1 attribute} other {# attributes} }?", - "delete-attributes-text": "Be careful, after the confirmation all selected attributes will be removed.", - "delete-attributes": "Delete attributes", - "enter-attribute-value": "Enter attribute value", - "show-on-widget": "Show on widget", - "widget-mode": "Widget mode", - "next-widget": "Next widget", - "prev-widget": "Previous widget", - "add-to-dashboard": "Add to dashboard", - "add-widget-to-dashboard": "Add widget to dashboard", - "selected-attributes": "{ count, plural, 1 {1 attribute} other {# attributes} } selected", - "selected-telemetry": "{ count, plural, 1 {1 telemetry unit} other {# telemetry units} } selected" - }, - "audit-log": { - "audit": "Audit", - "audit-logs": "Audit Logs", - "timestamp": "Timestamp", - "entity-type": "Entity Type", - "entity-name": "Entity Name", - "user": "User", - "type": "Type", - "status": "Status", - "details": "Details", - "type-added": "Added", - "type-deleted": "Deleted", - "type-updated": "Updated", - "type-attributes-updated": "Attributes updated", - "type-attributes-deleted": "Attributes deleted", - "type-rpc-call": "RPC call", - "type-credentials-updated": "Credentials updated", - "type-assigned-to-customer": "Assigned to Customer", - "type-unassigned-from-customer": "Unassigned from Customer", - "type-activated": "Activated", - "type-suspended": "Suspended", - "type-credentials-read": "Credentials read", - "type-attributes-read": "Attributes read", - "type-relation-add-or-update": "Relation updated", - "type-relation-delete": "Relation deleted", - "type-relations-delete": "All relation deleted", - "type-alarm-ack": "Acknowledged", - "type-alarm-clear": "Cleared", - "type-login": "Login", - "type-logout": "Logout", - "type-lockout": "Lockout", - "status-success": "Success", - "status-failure": "Failure", - "audit-log-details": "Audit log details", - "no-audit-logs-prompt": "No logs found", - "action-data": "Action data", - "failure-details": "Failure details", - "search": "Search audit logs", - "clear-search": "Clear search", - "type-assigned-from-tenant": "Assigned from Tenant", - "type-assigned-to-tenant": "Assigned to Tenant" - }, - "confirm-on-exit": { - "message": "You have unsaved changes. Are you sure you want to leave this page?", - "html-message": "You have unsaved changes.
    Are you sure you want to leave this page?", - "title": "Unsaved changes" - }, - "contact": { - "country": "Country", - "city": "City", - "state": "State / Province", - "postal-code": "Zip / Postal Code", - "postal-code-invalid": "Invalid Zip / Postal Code format.", - "address": "Address", - "address2": "Address 2", - "phone": "Phone", - "email": "Email", - "no-address": "No address" - }, - "common": { - "username": "Username", - "password": "Password", - "enter-username": "Enter username", - "enter-password": "Enter password", - "enter-search": "Enter search" - }, - "content-type": { - "json": "Json", - "text": "Text", - "binary": "Binary (Base64)" - }, - "customer": { - "customer": "Customer", - "customers": "Customers", - "management": "Customer management", - "dashboard": "Customer Dashboard", - "dashboards": "Customer Dashboards", - "devices": "Customer Devices", - "entity-views": "Customer Entity Views", - "assets": "Customer Assets", - "public-dashboards": "Public Dashboards", - "public-devices": "Public Devices", - "public-assets": "Public Assets", - "public-entity-views": "Public Entity Views", - "add": "Add Customer", - "delete": "Delete customer", - "manage-customer-users": "Manage customer users", - "manage-customer-devices": "Manage customer devices", - "manage-customer-dashboards": "Manage customer dashboards", - "manage-public-devices": "Manage public devices", - "manage-public-dashboards": "Manage public dashboards", - "manage-customer-assets": "Manage customer assets", - "manage-public-assets": "Manage public assets", - "add-customer-text": "Add new customer", - "no-customers-text": "No customers found", - "customer-details": "Customer details", - "delete-customer-title": "Are you sure you want to delete the customer '{{customerTitle}}'?", - "delete-customer-text": "Be careful, after the confirmation the customer and all related data will become unrecoverable.", - "delete-customers-title": "Are you sure you want to delete { count, plural, 1 {1 customer} other {# customers} }?", - "delete-customers-action-title": "Delete { count, plural, 1 {1 customer} other {# customers} }", - "delete-customers-text": "Be careful, after the confirmation all selected customers will be removed and all related data will become unrecoverable.", - "manage-users": "Manage users", - "manage-assets": "Manage assets", - "manage-devices": "Manage devices", - "manage-dashboards": "Manage dashboards", - "title": "Title", - "title-required": "Title is required.", - "description": "Description", - "details": "Details", - "events": "Events", - "copyId": "Copy customer Id", - "idCopiedMessage": "Customer Id has been copied to clipboard", - "select-customer": "Select customer", - "no-customers-matching": "No customers matching '{{entity}}' were found.", - "customer-required": "Customer is required", - "select-default-customer": "Select default customer", - "default-customer": "Default customer", - "default-customer-required": "Default customer is required in order to debug dashboard on Tenant level" - }, - "datetime": { - "date-from": "Date from", - "time-from": "Time from", - "date-to": "Date to", - "time-to": "Time to" - }, - "dashboard": { - "dashboard": "Dashboard", - "dashboards": "Dashboards", - "management": "Dashboard management", - "view-dashboards": "View Dashboards", - "add": "Add Dashboard", - "assign-dashboard-to-customer": "Assign Dashboard(s) To Customer", - "assign-dashboard-to-customer-text": "Please select the dashboards to assign to the customer", - "assign-to-customer-text": "Please select the customer to assign the dashboard(s)", - "assign-to-customer": "Assign to customer", - "unassign-from-customer": "Unassign from customer", - "make-public": "Make dashboard public", - "make-private": "Make dashboard private", - "manage-assigned-customers": "Manage assigned customers", - "assigned-customers": "Assigned customers", - "assign-to-customers": "Assign Dashboard(s) To Customers", - "assign-to-customers-text": "Please select the customers to assign the dashboard(s)", - "unassign-from-customers": "Unassign Dashboard(s) From Customers", - "unassign-from-customers-text": "Please select the customers to unassign from the dashboard(s)", - "no-dashboards-text": "No dashboards found", - "no-widgets": "No widgets configured", - "add-widget": "Add new widget", - "title": "Title", - "select-widget-title": "Select widget", - "select-widget-subtitle": "List of available widget types", - "delete": "Delete dashboard", - "title-required": "Title is required.", - "description": "Description", - "details": "Details", - "dashboard-details": "Dashboard details", - "add-dashboard-text": "Add new dashboard", - "assign-dashboards": "Assign dashboards", - "assign-new-dashboard": "Assign new dashboard", - "assign-dashboards-text": "Assign { count, plural, 1 {1 dashboard} other {# dashboards} } to customers", - "unassign-dashboards-action-text": "Unassign { count, plural, 1 {1 dashboard} other {# dashboards} } from customers", - "delete-dashboards": "Delete dashboards", - "unassign-dashboards": "Unassign dashboards", - "unassign-dashboards-action-title": "Unassign { count, plural, 1 {1 dashboard} other {# dashboards} } from customer", - "delete-dashboard-title": "Are you sure you want to delete the dashboard '{{dashboardTitle}}'?", - "delete-dashboard-text": "Be careful, after the confirmation the dashboard and all related data will become unrecoverable.", - "delete-dashboards-title": "Are you sure you want to delete { count, plural, 1 {1 dashboard} other {# dashboards} }?", - "delete-dashboards-action-title": "Delete { count, plural, 1 {1 dashboard} other {# dashboards} }", - "delete-dashboards-text": "Be careful, after the confirmation all selected dashboards will be removed and all related data will become unrecoverable.", - "unassign-dashboard-title": "Are you sure you want to unassign the dashboard '{{dashboardTitle}}'?", - "unassign-dashboard-text": "After the confirmation the dashboard will be unassigned and won't be accessible by the customer.", - "unassign-dashboard": "Unassign dashboard", - "unassign-dashboards-title": "Are you sure you want to unassign { count, plural, 1 {1 dashboard} other {# dashboards} }?", - "unassign-dashboards-text": "After the confirmation all selected dashboards will be unassigned and won't be accessible by the customer.", - "public-dashboard-title": "Dashboard is now public", - "public-dashboard-text": "Your dashboard {{dashboardTitle}} is now public and accessible via next public link:", - "public-dashboard-notice": "Note: Do not forget to make related devices public in order to access their data.", - "make-private-dashboard-title": "Are you sure you want to make the dashboard '{{dashboardTitle}}' private?", - "make-private-dashboard-text": "After the confirmation the dashboard will be made private and won't be accessible by others.", - "make-private-dashboard": "Make dashboard private", - "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard", - "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard", - "select-dashboard": "Select dashboard", - "no-dashboards-matching": "No dashboards matching '{{entity}}' were found.", - "dashboard-required": "Dashboard is required.", - "select-existing": "Select existing dashboard", - "create-new": "Create new dashboard", - "new-dashboard-title": "New dashboard title", - "open-dashboard": "Open dashboard", - "set-background": "Set background", - "background-color": "Background color", - "background-image": "Background image", - "background-size-mode": "Background size mode", - "no-image": "No image selected", - "drop-image": "Drop an image or click to select a file to upload.", - "settings": "Settings", - "columns-count": "Columns count", - "columns-count-required": "Columns count is required.", - "min-columns-count-message": "Only 10 minimum column count is allowed.", - "max-columns-count-message": "Only 1000 maximum column count is allowed.", - "widgets-margins": "Margin between widgets", - "horizontal-margin": "Horizontal margin", - "horizontal-margin-required": "Horizontal margin value is required.", - "min-horizontal-margin-message": "Only 0 is allowed as minimum horizontal margin value.", - "max-horizontal-margin-message": "Only 50 is allowed as maximum horizontal margin value.", - "vertical-margin": "Vertical margin", - "vertical-margin-required": "Vertical margin value is required.", - "min-vertical-margin-message": "Only 0 is allowed as minimum vertical margin value.", - "max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value.", - "autofill-height": "Auto fill layout height", - "mobile-layout": "Mobile layout settings", - "mobile-row-height": "Mobile row height, px", - "mobile-row-height-required": "Mobile row height value is required.", - "min-mobile-row-height-message": "Only 5 pixels is allowed as minimum mobile row height value.", - "max-mobile-row-height-message": "Only 200 pixels is allowed as maximum mobile row height value.", - "display-title": "Display dashboard title", - "toolbar-always-open": "Keep toolbar opened", - "title-color": "Title color", - "display-dashboards-selection": "Display dashboards selection", - "display-entities-selection": "Display entities selection", - "display-dashboard-timewindow": "Display timewindow", - "display-dashboard-export": "Display export", - "import": "Import dashboard", - "export": "Export dashboard", - "export-failed-error": "Unable to export dashboard: {{error}}", - "create-new-dashboard": "Create new dashboard", - "dashboard-file": "Dashboard file", - "invalid-dashboard-file-error": "Unable to import dashboard: Invalid dashboard data structure.", - "dashboard-import-missing-aliases-title": "Configure aliases used by imported dashboard", - "create-new-widget": "Create new widget", - "import-widget": "Import widget", - "widget-file": "Widget file", - "invalid-widget-file-error": "Unable to import widget: Invalid widget data structure.", - "widget-import-missing-aliases-title": "Configure aliases used by imported widget", - "open-toolbar": "Open dashboard toolbar", - "close-toolbar": "Close toolbar", - "configuration-error": "Configuration error", - "alias-resolution-error-title": "Dashboard aliases configuration error", - "invalid-aliases-config": "Unable to find any devices matching to some of the aliases filter.
    Please contact your administrator in order to resolve this issue.", - "select-devices": "Select devices", - "assignedToCustomer": "Assigned to customer", - "assignedToCustomers": "Assigned to customers", - "public": "Public", - "public-link": "Public link", - "copy-public-link": "Copy public link", - "public-link-copied-message": "Dashboard public link has been copied to clipboard", - "manage-states": "Manage dashboard states", - "states": "Dashboard states", - "search-states": "Search dashboard states", - "selected-states": "{ count, plural, 1 {1 dashboard state} other {# dashboard states} } selected", - "edit-state": "Edit dashboard state", - "delete-state": "Delete dashboard state", - "add-state": "Add dashboard state", - "state": "Dashboard state", - "state-name": "Name", - "state-name-required": "Dashboard state name is required.", - "state-id": "State Id", - "state-id-required": "Dashboard state id is required.", - "state-id-exists": "Dashboard state with the same id is already exists.", - "is-root-state": "Root state", - "delete-state-title": "Delete dashboard state", - "delete-state-text": "Are you sure you want delete dashboard state with name '{{stateName}}'?", - "show-details": "Show details", - "hide-details": "Hide details", - "select-state": "Select target state", - "state-controller": "State controller" - }, - "datakey": { - "settings": "Settings", - "advanced": "Advanced", - "label": "Label", - "color": "Color", - "units": "Special symbol to show next to value", - "decimals": "Number of digits after floating point", - "data-generation-func": "Data generation function", - "use-data-post-processing-func": "Use data post-processing function", - "configuration": "Data key configuration", - "timeseries": "Timeseries", - "attributes": "Attributes", - "entity-field": "Entity field", - "alarm": "Alarm fields", - "timeseries-required": "Entity timeseries are required.", - "timeseries-or-attributes-required": "Entity timeseries/attributes are required.", - "maximum-timeseries-or-attributes": "Maximum { count, plural, 1 {1 timeseries/attribute is allowed.} other {# timeseries/attributes are allowed} }", - "alarm-fields-required": "Alarm fields are required.", - "function-types": "Function types", - "function-types-required": "Function types are required.", - "maximum-function-types": "Maximum { count, plural, 1 {1 function type is allowed.} other {# function types are allowed} }", - "time-description": "timestamp of the current value;", - "value-description": "the current value;", - "prev-value-description": "result of the previous function call;", - "time-prev-description": "timestamp of the previous value;", - "prev-orig-value-description": "original previous value;" - }, - "datasource": { - "type": "Datasource type", - "name": "Name", - "add-datasource-prompt": "Please add datasource" - }, - "details": { - "edit-mode": "Edit mode", - "edit-json": "Edit JSON", - "toggle-edit-mode": "Toggle edit mode" - }, - "device": { - "device": "Device", - "device-required": "Device is required.", - "devices": "Devices", - "management": "Device management", - "view-devices": "View Devices", - "device-alias": "Device alias", - "aliases": "Device aliases", - "no-alias-matching": "'{{alias}}' not found.", - "no-aliases-found": "No aliases found.", - "no-key-matching": "'{{key}}' not found.", - "no-keys-found": "No keys found.", - "create-new-alias": "Create a new one!", - "create-new-key": "Create a new one!", - "duplicate-alias-error": "Duplicate alias found '{{alias}}'.
    Device aliases must be unique whithin the dashboard.", - "configure-alias": "Configure '{{alias}}' alias", - "no-devices-matching": "No devices matching '{{entity}}' were found.", - "alias": "Alias", - "alias-required": "Device alias is required.", - "remove-alias": "Remove device alias", - "add-alias": "Add device alias", - "name-starts-with": "Device name starts with", - "device-list": "Device list", - "use-device-name-filter": "Use filter", - "device-list-empty": "No devices selected.", - "device-name-filter-required": "Device name filter is required.", - "device-name-filter-no-device-matched": "No devices starting with '{{device}}' were found.", - "add": "Add Device", - "assign-to-customer": "Assign to customer", - "assign-device-to-customer": "Assign Device(s) To Customer", - "assign-device-to-customer-text": "Please select the devices to assign to the customer", - "make-public": "Make device public", - "make-private": "Make device private", - "no-devices-text": "No devices found", - "assign-to-customer-text": "Please select the customer to assign the device(s)", - "device-details": "Device details", - "add-device-text": "Add new device", - "credentials": "Credentials", - "manage-credentials": "Manage credentials", - "delete": "Delete device", - "assign-devices": "Assign devices", - "assign-devices-text": "Assign { count, plural, 1 {1 device} other {# devices} } to customer", - "delete-devices": "Delete devices", - "unassign-from-customer": "Unassign from customer", - "unassign-devices": "Unassign devices", - "unassign-devices-action-title": "Unassign { count, plural, 1 {1 device} other {# devices} } from customer", - "assign-new-device": "Assign new device", - "make-public-device-title": "Are you sure you want to make the device '{{deviceName}}' public?", - "make-public-device-text": "After the confirmation the device and all its data will be made public and accessible by others.", - "make-private-device-title": "Are you sure you want to make the device '{{deviceName}}' private?", - "make-private-device-text": "After the confirmation the device and all its data will be made private and won't be accessible by others.", - "view-credentials": "View credentials", - "delete-device-title": "Are you sure you want to delete the device '{{deviceName}}'?", - "delete-device-text": "Be careful, after the confirmation the device and all related data will become unrecoverable.", - "delete-devices-title": "Are you sure you want to delete { count, plural, 1 {1 device} other {# devices} }?", - "delete-devices-action-title": "Delete { count, plural, 1 {1 device} other {# devices} }", - "delete-devices-text": "Be careful, after the confirmation all selected devices will be removed and all related data will become unrecoverable.", - "unassign-device-title": "Are you sure you want to unassign the device '{{deviceName}}'?", - "unassign-device-text": "After the confirmation the device will be unassigned and won't be accessible by the customer.", - "unassign-device": "Unassign device", - "unassign-devices-title": "Are you sure you want to unassign { count, plural, 1 {1 device} other {# devices} }?", - "unassign-devices-text": "After the confirmation all selected devices will be unassigned and won't be accessible by the customer.", - "device-credentials": "Device Credentials", - "credentials-type": "Credentials type", - "access-token": "Access token", - "access-token-required": "Access token is required.", - "access-token-invalid": "Access token length must be from 1 to 20 characters.", - "rsa-key": "RSA public key", - "rsa-key-required": "RSA public key is required.", - "secret": "Secret", - "secret-required": "Secret is required.", - "device-type": "Device type", - "device-type-required": "Device type is required.", - "select-device-type": "Select device type", - "enter-device-type": "Enter device type", - "any-device": "Any device", - "no-device-types-matching": "No device types matching '{{entitySubtype}}' were found.", - "device-type-list-empty": "No device types selected.", - "device-types": "Device types", - "name": "Name", - "name-required": "Name is required.", - "description": "Description", - "label": "Label", - "events": "Events", - "details": "Details", - "copyId": "Copy device Id", - "copyAccessToken": "Copy access token", - "idCopiedMessage": "Device Id has been copied to clipboard", - "accessTokenCopiedMessage": "Device access token has been copied to clipboard", - "assignedToCustomer": "Assigned to customer", - "unable-delete-device-alias-title": "Unable to delete device alias", - "unable-delete-device-alias-text": "Device alias '{{deviceAlias}}' can't be deleted as it used by the following widget(s):
    {{widgetsList}}", - "is-gateway": "Is gateway", - "public": "Public", - "device-public": "Device is public", - "select-device": "Select device", - "import": "Import device", - "device-file": "Device file" - }, - "dialog": { - "close": "Close dialog" - }, - "direction": { - "column": "Column", - "row": "Row" - }, - "error": { - "unable-to-connect": "Unable to connect to the server! Please check your internet connection.", - "unhandled-error-code": "Unhandled error code: {{errorCode}}", - "unknown-error": "Unknown error" - }, - "entity": { - "entity": "Entity", - "entities": "Entities", - "aliases": "Entity aliases", - "entity-alias": "Entity alias", - "unable-delete-entity-alias-title": "Unable to delete entity alias", - "unable-delete-entity-alias-text": "Entity alias '{{entityAlias}}' can't be deleted as it used by the following widget(s):
    {{widgetsList}}", - "duplicate-alias-error": "Duplicate alias found '{{alias}}'.
    Entity aliases must be unique whithin the dashboard.", - "missing-entity-filter-error": "Filter is missing for alias '{{alias}}'.", - "configure-alias": "Configure '{{alias}}' alias", - "alias": "Alias", - "alias-required": "Entity alias is required.", - "remove-alias": "Remove entity alias", - "add-alias": "Add entity alias", - "entity-list": "Entity list", - "entity-type": "Entity type", - "entity-types": "Entity types", - "entity-type-list": "Entity type list", - "any-entity": "Any entity", - "enter-entity-type": "Enter entity type", - "no-entities-matching": "No entities matching '{{entity}}' were found.", - "no-entity-types-matching": "No entity types matching '{{entityType}}' were found.", - "name-starts-with": "Name starts with", - "use-entity-name-filter": "Use filter", - "entity-list-empty": "No entities selected.", - "entity-type-list-empty": "No entity types selected.", - "entity-name-filter-required": "Entity name filter is required.", - "entity-name-filter-no-entity-matched": "No entities starting with '{{entity}}' were found.", - "all-subtypes": "All", - "select-entities": "Select entities", - "no-aliases-found": "No aliases found.", - "no-alias-matching": "'{{alias}}' not found.", - "create-new-alias": "Create a new one!", - "key": "Key", - "key-name": "Key name", - "no-keys-found": "No keys found.", - "no-key-matching": "'{{key}}' not found.", - "create-new-key": "Create a new one!", - "type": "Type", - "type-required": "Entity type is required.", - "type-device": "Device", - "type-devices": "Devices", - "list-of-devices": "{ count, plural, 1 {One device} other {List of # devices} }", - "device-name-starts-with": "Devices whose names start with '{{prefix}}'", - "type-asset": "Asset", - "type-assets": "Assets", - "list-of-assets": "{ count, plural, 1 {One asset} other {List of # assets} }", - "asset-name-starts-with": "Assets whose names start with '{{prefix}}'", - "type-entity-view": "Entity View", - "type-entity-views": "Entity Views", - "list-of-entity-views": "{ count, plural, 1 {One entity view} other {List of # entity views} }", - "entity-view-name-starts-with": "Entity Views whose names start with '{{prefix}}'", - "type-rule": "Rule", - "type-rules": "Rules", - "list-of-rules": "{ count, plural, 1 {One rule} other {List of # rules} }", - "rule-name-starts-with": "Rules whose names start with '{{prefix}}'", - "type-plugin": "Plugin", - "type-plugins": "Plugins", - "list-of-plugins": "{ count, plural, 1 {One plugin} other {List of # plugins} }", - "plugin-name-starts-with": "Plugins whose names start with '{{prefix}}'", - "type-tenant": "Tenant", - "type-tenants": "Tenants", - "list-of-tenants": "{ count, plural, 1 {One tenant} other {List of # tenants} }", - "tenant-name-starts-with": "Tenants whose names start with '{{prefix}}'", - "type-customer": "Customer", - "type-customers": "Customers", - "list-of-customers": "{ count, plural, 1 {One customer} other {List of # customers} }", - "customer-name-starts-with": "Customers whose names start with '{{prefix}}'", - "type-user": "User", - "type-users": "Users", - "list-of-users": "{ count, plural, 1 {One user} other {List of # users} }", - "user-name-starts-with": "Users whose names start with '{{prefix}}'", - "type-dashboard": "Dashboard", - "type-dashboards": "Dashboards", - "list-of-dashboards": "{ count, plural, 1 {One dashboard} other {List of # dashboards} }", - "dashboard-name-starts-with": "Dashboards whose names start with '{{prefix}}'", - "type-alarm": "Alarm", - "type-alarms": "Alarms", - "list-of-alarms": "{ count, plural, 1 {One alarms} other {List of # alarms} }", - "alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'", - "type-rulechain": "Rule chain", - "type-rulechains": "Rule chains", - "list-of-rulechains": "{ count, plural, 1 {One rule chain} other {List of # rule chains} }", - "rulechain-name-starts-with": "Rule chains whose names start with '{{prefix}}'", - "type-rulenode": "Rule node", - "type-rulenodes": "Rule nodes", - "list-of-rulenodes": "{ count, plural, 1 {One rule node} other {List of # rule nodes} }", - "rulenode-name-starts-with": "Rule nodes whose names start with '{{prefix}}'", - "type-current-customer": "Current Customer", - "type-current-tenant": "Current Tenant", - "search": "Search entities", - "selected-entities": "{ count, plural, 1 {1 entity} other {# entities} } selected", - "entity-name": "Entity name", - "entity-label": "Entity label", - "details": "Entity details", - "no-entities-prompt": "No entities found", - "no-data": "No data to display", - "columns-to-display": "Columns to Display" - }, - "entity-field": { - "created-time": "Created time", - "name": "Name", - "type": "Type", - "first-name": "First name", - "last-name": "Last name", - "email": "Email", - "title": "Title", - "country": "Country", - "state": "State", - "city": "City", - "address": "Address", - "address2": "Address 2", - "zip": "Zip", - "phone": "Phone", - "label": "Label" - }, - "entity-view": { - "entity-view": "Entity View", - "entity-view-required": "Entity view is required.", - "entity-views": "Entity Views", - "management": "Entity View management", - "view-entity-views": "View Entity Views", - "entity-view-alias": "Entity View alias", - "aliases": "Entity View aliases", - "no-alias-matching": "'{{alias}}' not found.", - "no-aliases-found": "No aliases found.", - "no-key-matching": "'{{key}}' not found.", - "no-keys-found": "No keys found.", - "create-new-alias": "Create a new one!", - "create-new-key": "Create a new one!", - "duplicate-alias-error": "Duplicate alias found '{{alias}}'.
    Entity View aliases must be unique within the dashboard.", - "configure-alias": "Configure '{{alias}}' alias", - "no-entity-views-matching": "No entity views matching '{{entity}}' were found.", - "alias": "Alias", - "alias-required": "Entity View alias is required.", - "remove-alias": "Remove entity view alias", - "add-alias": "Add entity view alias", - "name-starts-with": "Entity View name starts with", - "entity-view-list": "Entity View list", - "use-entity-view-name-filter": "Use filter", - "entity-view-list-empty": "No entity views selected.", - "entity-view-name-filter-required": "Entity view name filter is required.", - "entity-view-name-filter-no-entity-view-matched": "No entity views starting with '{{entityView}}' were found.", - "add": "Add Entity View", - "assign-to-customer": "Assign to customer", - "assign-entity-view-to-customer": "Assign Entity View(s) To Customer", - "assign-entity-view-to-customer-text": "Please select the entity views to assign to the customer", - "no-entity-views-text": "No entity views found", - "assign-to-customer-text": "Please select the customer to assign the entity view(s)", - "entity-view-details": "Entity view details", - "add-entity-view-text": "Add new entity view", - "delete": "Delete entity view", - "assign-entity-views": "Assign entity views", - "assign-entity-views-text": "Assign { count, plural, 1 {1 entity view} other {# entity views} } to customer", - "delete-entity-views": "Delete entity views", - "unassign-from-customer": "Unassign from customer", - "unassign-entity-views": "Unassign entity views", - "unassign-entity-views-action-title": "Unassign { count, plural, 1 {1 entity view} other {# entity views} } from customer", - "assign-new-entity-view": "Assign new entity view", - "delete-entity-view-title": "Are you sure you want to delete the entity view '{{entityViewName}}'?", - "delete-entity-view-text": "Be careful, after the confirmation the entity view and all related data will become unrecoverable.", - "delete-entity-views-title": "Are you sure you want to delete { count, plural, 1 {1 entity view} other {# entity views} }?", - "delete-entity-views-action-title": "Delete { count, plural, 1 {1 entity view} other {# entity views} }", - "delete-entity-views-text": "Be careful, after the confirmation all selected entity views will be removed and all related data will become unrecoverable.", - "unassign-entity-view-title": "Are you sure you want to unassign the entity view '{{entityViewName}}'?", - "unassign-entity-view-text": "After the confirmation the entity view will be unassigned and won't be accessible by the customer.", - "unassign-entity-view": "Unassign entity view", - "unassign-entity-views-title": "Are you sure you want to unassign { count, plural, 1 {1 entity view} other {# entity views} }?", - "unassign-entity-views-text": "After the confirmation all selected entity views will be unassigned and won't be accessible by the customer.", - "entity-view-type": "Entity View type", - "entity-view-type-required": "Entity View type is required.", - "select-entity-view-type": "Select entity view type", - "enter-entity-view-type": "Enter entity view type", - "any-entity-view": "Any entity view", - "no-entity-view-types-matching": "No entity view types matching '{{entitySubtype}}' were found.", - "entity-view-type-list-empty": "No entity view types selected.", - "entity-view-types": "Entity View types", - "name": "Name", - "name-required": "Name is required.", - "description": "Description", - "events": "Events", - "details": "Details", - "copyId": "Copy entity view Id", - "assignedToCustomer": "Assigned to customer", - "unable-entity-view-device-alias-title": "Unable to delete entity view alias", - "unable-entity-view-device-alias-text": "Device alias '{{entityViewAlias}}' can't be deleted as it used by the following widget(s):
    {{widgetsList}}", - "select-entity-view": "Select entity view", - "make-public": "Make entity view public", - "make-private": "Make entity view private", - "start-date": "Start date", - "start-ts": "Start time", - "end-date": "End date", - "end-ts": "End time", - "date-limits": "Date limits", - "client-attributes": "Client attributes", - "shared-attributes": "Shared attributes", - "server-attributes": "Server attributes", - "timeseries": "Timeseries", - "client-attributes-placeholder": "Client attributes", - "shared-attributes-placeholder": "Shared attributes", - "server-attributes-placeholder": "Server attributes", - "timeseries-placeholder": "Timeseries", - "target-entity": "Target entity", - "attributes-propagation": "Attributes propagation", - "attributes-propagation-hint": "Entity View will automatically copy specified attributes from Target Entity each time you save or update this entity view. For performance reasons target entity attributes are not propagated to entity view on each attribute change. You can enable automatic propagation by configuring \"copy to view\" rule node in your rule chain and linking \"Post attributes\" and \"Attributes Updated\" messages to the new rule node.", - "timeseries-data": "Timeseries data", - "timeseries-data-hint": "Configure timeseries data keys of the target entity that will be accessible to the entity view. This timeseries data is read-only.", - "make-public-entity-view-title": "Are you sure you want to make the entity view '{{entityViewName}}' public?", - "make-public-entity-view-text": "After the confirmation the entity view and all its data will be made public and accessible by others.", - "make-private-entity-view-title": "Are you sure you want to make the entity view '{{entityViewName}}' private?", - "make-private-entity-view-text": "After the confirmation the entity view and all its data will be made private and won't be accessible by others." - }, - "event": { - "event-type": "Event type", - "type-error": "Error", - "type-lc-event": "Lifecycle event", - "type-stats": "Statistics", - "type-debug-rule-node": "Debug", - "type-debug-rule-chain": "Debug", - "no-events-prompt": "No events found", - "error": "Error", - "alarm": "Alarm", - "event-time": "Event time", - "server": "Server", - "body": "Body", - "method": "Method", - "type": "Type", - "entity": "Entity", - "message-id": "Message Id", - "message-type": "Message Type", - "data-type": "Data Type", - "relation-type": "Relation Type", - "metadata": "Metadata", - "data": "Data", - "event": "Event", - "status": "Status", - "success": "Success", - "failed": "Failed", - "messages-processed": "Messages processed", - "errors-occurred": "Errors occurred" - }, - "extension": { - "extensions": "Extensions", - "selected-extensions": "{ count, plural, 1 {1 extension} other {# extensions} } selected", - "type": "Type", - "key": "Key", - "value": "Value", - "id": "Id", - "extension-id": "Extension id", - "extension-type": "Extension type", - "transformer-json": "JSON *", - "unique-id-required": "Current extension id already exists.", - "delete": "Delete extension", - "add": "Add extension", - "edit": "Edit extension", - "delete-extension-title": "Are you sure you want to delete the extension '{{extensionId}}'?", - "delete-extension-text": "Be careful, after the confirmation the extension and all related data will become unrecoverable.", - "delete-extensions-title": "Are you sure you want to delete { count, plural, 1 {1 extension} other {# extensions} }?", - "delete-extensions-text": "Be careful, after the confirmation all selected extensions will be removed.", - "converters": "Converters", - "converter-id": "Converter id", - "configuration": "Configuration", - "converter-configurations": "Converter configurations", - "token": "Security token", - "add-converter": "Add converter", - "add-config": "Add converter configuration", - "device-name-expression": "Device name expression", - "device-type-expression": "Device type expression", - "custom": "Custom", - "to-double": "To Double", - "transformer": "Transformer", - "json-required": "Transformer json is required.", - "json-parse": "Unable to parse transformer json.", - "attributes": "Attributes", - "add-attribute": "Add attribute", - "add-map": "Add mapping element", - "timeseries": "Timeseries", - "add-timeseries": "Add timeseries", - "field-required": "Field is required", - "brokers": "Brokers", - "add-broker": "Add broker", - "host": "Host", - "port": "Port", - "port-range": "Port should be in a range from 1 to 65535.", - "ssl": "Ssl", - "credentials": "Credentials", - "username": "Username", - "password": "Password", - "retry-interval": "Retry interval in milliseconds", - "anonymous": "Anonymous", - "basic": "Basic", - "pem": "PEM", - "ca-cert": "CA certificate file *", - "private-key": "Private key file *", - "cert": "Certificate file *", - "no-file": "No file selected.", - "drop-file": "Drop a file or click to select a file to upload.", - "mapping": "Mapping", - "topic-filter": "Topic filter", - "converter-type": "Converter type", - "converter-json": "Json", - "json-name-expression": "Device name json expression", - "topic-name-expression": "Device name topic expression", - "json-type-expression": "Device type json expression", - "topic-type-expression": "Device type topic expression", - "attribute-key-expression": "Attribute key expression", - "attr-json-key-expression": "Attribute key json expression", - "attr-topic-key-expression": "Attribute key topic expression", - "request-id-expression": "Request id expression", - "request-id-json-expression": "Request id json expression", - "request-id-topic-expression": "Request id topic expression", - "response-topic-expression": "Response topic expression", - "value-expression": "Value expression", - "topic": "Topic", - "timeout": "Timeout in milliseconds", - "converter-json-required": "Converter json is required.", - "converter-json-parse": "Unable to parse converter json.", - "filter-expression": "Filter expression", - "connect-requests": "Connect requests", - "add-connect-request": "Add connect request", - "disconnect-requests": "Disconnect requests", - "add-disconnect-request": "Add disconnect request", - "attribute-requests": "Attribute requests", - "add-attribute-request": "Add attribute request", - "attribute-updates": "Attribute updates", - "add-attribute-update": "Add attribute update", - "server-side-rpc": "Server side RPC", - "add-server-side-rpc-request": "Add server-side RPC request", - "device-name-filter": "Device name filter", - "attribute-filter": "Attribute filter", - "method-filter": "Method filter", - "request-topic-expression": "Request topic expression", - "response-timeout": "Response timeout in milliseconds", - "topic-expression": "Topic expression", - "client-scope": "Client scope", - "add-device": "Add device", - "opc-server": "Servers", - "opc-add-server": "Add server", - "opc-add-server-prompt": "Please add server", - "opc-application-name": "Application name", - "opc-application-uri": "Application uri", - "opc-scan-period-in-seconds": "Scan period in seconds", - "opc-security": "Security", - "opc-identity": "Identity", - "opc-keystore": "Keystore", - "opc-type": "Type", - "opc-keystore-type": "Type", - "opc-keystore-location": "Location *", - "opc-keystore-password": "Password", - "opc-keystore-alias": "Alias", - "opc-keystore-key-password": "Key password", - "opc-device-node-pattern": "Device node pattern", - "opc-device-name-pattern": "Device name pattern", - "modbus-server": "Servers/slaves", - "modbus-add-server": "Add server/slave", - "modbus-add-server-prompt": "Please add server/slave", - "modbus-transport": "Transport", - "modbus-tcp-reconnect": "Automatically reconnect", - "modbus-rtu-over-tcp": "RTU over TCP", - "modbus-port-name": "Serial port name", - "modbus-encoding": "Encoding", - "modbus-parity": "Parity", - "modbus-baudrate": "Baud rate", - "modbus-databits": "Data bits", - "modbus-stopbits": "Stop bits", - "modbus-databits-range": "Data bits should be in a range from 7 to 8.", - "modbus-stopbits-range": "Stop bits should be in a range from 1 to 2.", - "modbus-unit-id": "Unit ID", - "modbus-unit-id-range": "Unit ID should be in a range from 1 to 247.", - "modbus-device-name": "Device name", - "modbus-poll-period": "Poll period (ms)", - "modbus-attributes-poll-period": "Attributes poll period (ms)", - "modbus-timeseries-poll-period": "Timeseries poll period (ms)", - "modbus-poll-period-range": "Poll period should be positive value.", - "modbus-tag": "Tag", - "modbus-function": "Function", - "modbus-register-address": "Register address", - "modbus-register-address-range": "Register address should be in a range from 0 to 65535.", - "modbus-register-bit-index": "Bit index", - "modbus-register-bit-index-range": "Bit index should be in a range from 0 to 15.", - "modbus-register-count": "Register count", - "modbus-register-count-range": "Register count should be a positive value.", - "modbus-byte-order": "Byte order", - "sync": { - "status": "Status", - "sync": "Sync", - "not-sync": "Not sync", - "last-sync-time": "Last sync time", - "not-available": "Not available" - }, - "export-extensions-configuration": "Export extensions configuration", - "import-extensions-configuration": "Import extensions configuration", - "import-extensions": "Import extensions", - "import-extension": "Import extension", - "export-extension": "Export extension", - "file": "Extensions file", - "invalid-file-error": "Invalid extension file" - }, - "fullscreen": { - "expand": "Expand to fullscreen", - "exit": "Exit fullscreen", - "toggle": "Toggle fullscreen mode", - "fullscreen": "Fullscreen" - }, - "function": { - "function": "Function" - }, - "gateway": { - "add-entry": "Add configuration", - "connector-add": "Add new connector", - "connector-enabled": "Enable connector", - "connector-name": "Connector name", - "connector-name-required": "Connector name is required.", - "connector-type": "Connector type", - "connector-type-required": "Connector type is required.", - "connectors": "Connectors configuration", - "create-new-gateway": "Create a new gateway", - "create-new-gateway-text": "Are you sure you want create a new gateway with name: '{{gatewayName}}'?", - "delete": "Delete configuration", - "download-tip": "Download configuration file", - "gateway": "Gateway", - "gateway-exists": "Device with same name is already exists.", - "gateway-name": "Gateway name", - "gateway-name-required": "Gateway name is required.", - "gateway-saved": "Gateway configuration successfully saved.", - "json-parse": "Not valid JSON.", - "json-required": "Field cannot be empty.", - "no-connectors": "No connectors", - "no-data": "No configurations", - "no-gateway-found": "No gateway found.", - "no-gateway-matching": " '{{item}}' not found.", - "path-logs": "Path to log files", - "path-logs-required": "Path is required.", - "remote": "Remote configuration", - "remote-logging-level": "Logging level", - "remove-entry": "Remove configuration", - "save-tip": "Save configuration file", - "security-type": "Security type", - "security-types": { - "access-token": "Access Token", - "tls": "TLS" - }, - "storage": "Storage", - "storage-max-file-records": "Maximum records in file", - "storage-max-files": "Maximum number of files", - "storage-max-files-min": "Minimum number is 1.", - "storage-max-files-pattern": "Number is not valid.", - "storage-max-files-required": "Number is required.", - "storage-max-records": "Maximum records in storage", - "storage-max-records-min": "Minimum number of records is 1.", - "storage-max-records-pattern": "Number is not valid.", - "storage-max-records-required": "Maximum records is required.", - "storage-pack-size": "Maximum event pack size", - "storage-pack-size-min": "Minimum number is 1.", - "storage-pack-size-pattern": "Number is not valid.", - "storage-pack-size-required": "Maximum event pack size is required.", - "storage-path": "Storage path", - "storage-path-required": "Storage path is required.", - "storage-type": "Storage type", - "storage-types": { - "file-storage": "File storage", - "memory-storage": "Memory storage" - }, - "thingsboard": "ThingsBoard", - "thingsboard-host": "ThingsBoard host", - "thingsboard-host-required": "Host is required.", - "thingsboard-port": "ThingsBoard port", - "thingsboard-port-max": "Maximum port number is 65535.", - "thingsboard-port-min": "Minimum port number is 1.", - "thingsboard-port-pattern": "Port is not valid.", - "thingsboard-port-required": "Port is required.", - "tidy": "Tidy", - "tidy-tip": "Tidy config JSON", - "title-connectors-json": "Connector {{typeName}} configuration", - "tls-path-ca-certificate": "Path to CA certificate on gateway", - "tls-path-client-certificate": "Path to client certificate on gateway", - "tls-path-private-key": "Path to private key on gateway", - "toggle-fullscreen": "Toggle fullscreen", - "transformer-json-config": "Configuration JSON*", - "update-config": "Add/update configuration JSON", - "state-title": "Gateway state", - "show-config-tip": "Show gateway configuration", - "title-show-config": "Show gateway configuration", - "read-only": "Read only", - "read-write": "" - }, - "grid": { - "delete-item-title": "Are you sure you want to delete this item?", - "delete-item-text": "Be careful, after the confirmation this item and all related data will become unrecoverable.", - "delete-items-title": "Are you sure you want to delete { count, plural, 1 {1 item} other {# items} }?", - "delete-items-action-title": "Delete { count, plural, 1 {1 item} other {# items} }", - "delete-items-text": "Be careful, after the confirmation all selected items will be removed and all related data will become unrecoverable.", - "add-item-text": "Add new item", - "no-items-text": "No items found", - "item-details": "Item details", - "delete-item": "Delete Item", - "delete-items": "Delete Items", - "scroll-to-top": "Scroll to top" - }, - "help": { - "goto-help-page": "Go to help page" - }, - "home": { - "home": "Home", - "profile": "Profile", - "logout": "Logout", - "menu": "Menu", - "avatar": "Avatar", - "open-user-menu": "Open user menu" - }, - "import": { - "no-file": "No file selected", - "drop-file": "Drop a JSON file or click to select a file to upload.", - "drop-file-csv": "Drop a CSV file or click to select a file to upload.", - "column-value": "Value", - "column-title": "Title", - "column-example": "Example value data", - "column-key": "Attribute/telemetry key", - "csv-delimiter": "CSV delimiter", - "csv-first-line-header": "First line contains column names", - "csv-update-data": "Update attributes/telemetry", - "import-csv-number-columns-error": "A file should contain at least two columns", - "import-csv-invalid-format-error": "Invalid file format. Line: '{{line}}'", - "column-type": { - "name": "Name", - "type": "Type", - "label": "Label", - "column-type": "Column type", - "client-attribute": "Client attribute", - "shared-attribute": "Shared attribute", - "server-attribute": "Server attribute", - "timeseries": "Timeseries", - "entity-field": "Entity field", - "access-token": "Access token", - "isgateway": "Is Gateway", - "description": "Description" - }, - "stepper-text":{ - "select-file": "Select a file", - "configuration": "Import configuration", - "column-type": "Select columns type", - "creat-entities": "Creating new entities", - "done": "Done" - }, - "message": { - "create-entities": "{{count}} new entities were successfully created.", - "update-entities": "{{count}} entities were successfully updated.", - "error-entities": "There was an error creating {{count}} entities." - } - }, - "item": { - "selected": "Selected" - }, - "js-func": { - "no-return-error": "Function must return value!", - "return-type-mismatch": "Function must return value of '{{type}}' type!", - "tidy": "Tidy", - "mini": "Mini" - }, - "key-val": { - "key": "Key", - "value": "Value", - "remove-entry": "Remove entry", - "add-entry": "Add entry", - "no-data": "No entries" - }, - "layout": { - "layout": "Layout", - "manage": "Manage layouts", - "settings": "Layout settings", - "color": "Color", - "main": "Main", - "right": "Right", - "select": "Select target layout" - }, - "legend": { - "direction": "Legend direction", - "position": "Legend position", - "show-max": "Show max value", - "show-min": "Show min value", - "show-avg": "Show average value", - "show-total": "Show total value", - "settings": "Legend settings", - "min": "min", - "max": "max", - "avg": "avg", - "total": "total", - "comparison-time-ago": { - "days": "(day ago)", - "weeks": "(week ago)", - "months": "(month ago)", - "years": "(year ago)" - } - }, - "login": { - "login": "Login", - "request-password-reset": "Request Password Reset", - "reset-password": "Reset Password", - "create-password": "Create Password", - "passwords-mismatch-error": "Entered passwords must be same!", - "password-again": "Password again", - "sign-in": "Please sign in", - "username": "Username (email)", - "remember-me": "Remember me", - "forgot-password": "Forgot Password?", - "password-reset": "Password reset", - "expired-password-reset-message": "Your credentials has been expired! Please create new password.", - "new-password": "New password", - "new-password-again": "New password again", - "password-link-sent-message": "Password reset link was successfully sent!", - "email": "Email", - "login-with": "Login with {{name}}", - "or": "or", - "error": "Login error" - }, - "position": { - "top": "Top", - "bottom": "Bottom", - "left": "Left", - "right": "Right" - }, - "profile": { - "profile": "Profile", - "last-login-time": "Last Login", - "change-password": "Change Password", - "current-password": "Current password" - }, - "relation": { - "relations": "Relations", - "direction": "Direction", - "search-direction": { - "FROM": "From", - "TO": "To" - }, - "direction-type": { - "FROM": "from", - "TO": "to" - }, - "from-relations": "Outbound relations", - "to-relations": "Inbound relations", - "selected-relations": "{ count, plural, 1 {1 relation} other {# relations} } selected", - "type": "Type", - "to-entity-type": "To entity type", - "to-entity-name": "To entity name", - "from-entity-type": "From entity type", - "from-entity-name": "From entity name", - "to-entity": "To entity", - "from-entity": "From entity", - "delete": "Delete relation", - "relation-type": "Relation type", - "relation-type-required": "Relation type is required.", - "any-relation-type": "Any type", - "add": "Add relation", - "edit": "Edit relation", - "delete-to-relation-title": "Are you sure you want to delete relation to the entity '{{entityName}}'?", - "delete-to-relation-text": "Be careful, after the confirmation the entity '{{entityName}}' will be unrelated from the current entity.", - "delete-to-relations-title": "Are you sure you want to delete { count, plural, 1 {1 relation} other {# relations} }?", - "delete-to-relations-text": "Be careful, after the confirmation all selected relations will be removed and corresponding entities will be unrelated from the current entity.", - "delete-from-relation-title": "Are you sure you want to delete relation from the entity '{{entityName}}'?", - "delete-from-relation-text": "Be careful, after the confirmation current entity will be unrelated from the entity '{{entityName}}'.", - "delete-from-relations-title": "Are you sure you want to delete { count, plural, 1 {1 relation} other {# relations} }?", - "delete-from-relations-text": "Be careful, after the confirmation all selected relations will be removed and current entity will be unrelated from the corresponding entities.", - "remove-relation-filter": "Remove relation filter", - "add-relation-filter": "Add relation filter", - "any-relation": "Any relation", - "relation-filters": "Relation filters", - "additional-info": "Additional info (JSON)", - "invalid-additional-info": "Unable to parse additional info json." - }, - "rulechain": { - "rulechain": "Rule chain", - "rulechains": "Rule chains", - "root": "Root", - "delete": "Delete rule chain", - "name": "Name", - "name-required": "Name is required.", - "description": "Description", - "add": "Add Rule Chain", - "set-root": "Make rule chain root", - "set-root-rulechain-title": "Are you sure you want to make the rule chain '{{ruleChainName}}' root?", - "set-root-rulechain-text": "After the confirmation the rule chain will become root and will handle all incoming transport messages.", - "delete-rulechain-title": "Are you sure you want to delete the rule chain '{{ruleChainName}}'?", - "delete-rulechain-text": "Be careful, after the confirmation the rule chain and all related data will become unrecoverable.", - "delete-rulechains-title": "Are you sure you want to delete { count, plural, 1 {1 rule chain} other {# rule chains} }?", - "delete-rulechains-action-title": "Delete { count, plural, 1 {1 rule chain} other {# rule chains} }", - "delete-rulechains-text": "Be careful, after the confirmation all selected rule chains will be removed and all related data will become unrecoverable.", - "add-rulechain-text": "Add new rule chain", - "no-rulechains-text": "No rule chains found", - "rulechain-details": "Rule chain details", - "details": "Details", - "events": "Events", - "system": "System", - "import": "Import rule chain", - "export": "Export rule chain", - "export-failed-error": "Unable to export rule chain: {{error}}", - "create-new-rulechain": "Create new rule chain", - "rulechain-file": "Rule chain file", - "invalid-rulechain-file-error": "Unable to import rule chain: Invalid rule chain data structure.", - "copyId": "Copy rule chain Id", - "idCopiedMessage": "Rule chain Id has been copied to clipboard", - "select-rulechain": "Select rule chain", - "no-rulechains-matching": "No rule chains matching '{{entity}}' were found.", - "rulechain-required": "Rule chain is required", - "management": "Rules management", - "debug-mode": "Debug mode" - }, - "rulenode": { - "details": "Details", - "events": "Events", - "search": "Search nodes", - "open-node-library": "Open node library", - "add": "Add rule node", - "name": "Name", - "name-required": "Name is required.", - "type": "Type", - "description": "Description", - "delete": "Delete rule node", - "select-all-objects": "Select all nodes and connections", - "deselect-all-objects": "Deselect all nodes and connections", - "delete-selected-objects": "Delete selected nodes and connections", - "delete-selected": "Delete selected", - "select-all": "Select all", - "copy-selected": "Copy selected", - "deselect-all": "Deselect all", - "rulenode-details": "Rule node details", - "debug-mode": "Debug mode", - "configuration": "Configuration", - "link": "Link", - "link-details": "Rule node link details", - "add-link": "Add link", - "link-label": "Link label", - "link-label-required": "Link label is required.", - "custom-link-label": "Custom link label", - "custom-link-label-required": "Custom link label is required.", - "link-labels": "Link labels", - "link-labels-required": "Link labels is required.", - "no-link-labels-found": "No link labels found", - "no-link-label-matching": "'{{label}}' not found.", - "create-new-link-label": "Create a new one!", - "type-filter": "Filter", - "type-filter-details": "Filter incoming messages with configured conditions", - "type-enrichment": "Enrichment", - "type-enrichment-details": "Add additional information into Message Metadata", - "type-transformation": "Transformation", - "type-transformation-details": "Change Message payload and Metadata", - "type-action": "Action", - "type-action-details": "Perform special action", - "type-external": "External", - "type-external-details": "Interacts with external system", - "type-rule-chain": "Rule Chain", - "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain", - "type-input": "Input", - "type-input-details": "Logical input of Rule Chain, forwards incoming messages to next related Rule Node", - "type-unknown": "Unknown", - "type-unknown-details": "Unresolved Rule Node", - "directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.", - "ui-resources-load-error": "Failed to load configuration ui resources.", - "invalid-target-rulechain": "Unable to resolve target rule chain!", - "test-script-function": "Test script function", - "message": "Message", - "message-type": "Message type", - "select-message-type": "Select message type", - "message-type-required": "Message type is required", - "metadata": "Metadata", - "metadata-required": "Metadata entries can't be empty.", - "output": "Output", - "test": "Test", - "help": "Help", - "reset-debug-mode": "Reset debug mode in all nodes" - }, - "queue": { - "select_name": "Select queue name", - "name": "Queue Name", - "name_required": "Queue name is required!" - - }, - "tenant": { - "tenant": "Tenant", - "tenants": "Tenants", - "management": "Tenant management", - "add": "Add Tenant", - "admins": "Admins", - "manage-tenant-admins": "Manage tenant admins", - "delete": "Delete tenant", - "add-tenant-text": "Add new tenant", - "no-tenants-text": "No tenants found", - "tenant-details": "Tenant details", - "delete-tenant-title": "Are you sure you want to delete the tenant '{{tenantTitle}}'?", - "delete-tenant-text": "Be careful, after the confirmation the tenant and all related data will become unrecoverable.", - "delete-tenants-title": "Are you sure you want to delete { count, plural, 1 {1 tenant} other {# tenants} }?", - "delete-tenants-action-title": "Delete { count, plural, 1 {1 tenant} other {# tenants} }", - "delete-tenants-text": "Be careful, after the confirmation all selected tenants will be removed and all related data will become unrecoverable.", - "title": "Title", - "title-required": "Title is required.", - "description": "Description", - "details": "Details", - "events": "Events", - "copyId": "Copy tenant Id", - "idCopiedMessage": "Tenant Id has been copied to clipboard", - "select-tenant": "Select tenant", - "no-tenants-matching": "No tenants matching '{{entity}}' were found.", - "tenant-required": "Tenant is required", - "isolated-tb-core": "Processing in isolated ThingsBoard Core container", - "isolated-tb-rule-engine": "Processing in isolated ThingsBoard Rule Engine container", - "isolated-tb-core-details": "Requires separate microservice(s) per isolated Tenant", - "isolated-tb-rule-engine-details": "Requires separate microservice(s) per isolated Tenant" - }, - "timeinterval": { - "seconds-interval": "{ seconds, plural, 1 {1 second} other {# seconds} }", - "minutes-interval": "{ minutes, plural, 1 {1 minute} other {# minutes} }", - "hours-interval": "{ hours, plural, 1 {1 hour} other {# hours} }", - "days-interval": "{ days, plural, 1 {1 day} other {# days} }", - "days": "Days", - "hours": "Hours", - "minutes": "Minutes", - "seconds": "Seconds", - "advanced": "Advanced" - }, - "timewindow": { - "days": "{ days, plural, 1 { day } other {# days } }", - "hours": "{ hours, plural, 0 { hour } 1 {1 hour } other {# hours } }", - "minutes": "{ minutes, plural, 0 { minute } 1 {1 minute } other {# minutes } }", - "seconds": "{ seconds, plural, 0 { second } 1 {1 second } other {# seconds } }", - "realtime": "Realtime", - "history": "History", - "last-prefix": "last", - "period": "from {{ startTime }} to {{ endTime }}", - "edit": "Edit timewindow", - "date-range": "Date range", - "last": "Last", - "time-period": "Time period", - "hide": "Hide" - }, - "user": { - "user": "User", - "users": "Users", - "customer-users": "Customer Users", - "tenant-admins": "Tenant Admins", - "sys-admin": "System administrator", - "tenant-admin": "Tenant administrator", - "customer": "Customer", - "anonymous": "Anonymous", - "add": "Add User", - "delete": "Delete user", - "add-user-text": "Add new user", - "no-users-text": "No users found", - "user-details": "User details", - "delete-user-title": "Are you sure you want to delete the user '{{userEmail}}'?", - "delete-user-text": "Be careful, after the confirmation the user and all related data will become unrecoverable.", - "delete-users-title": "Are you sure you want to delete { count, plural, 1 {1 user} other {# users} }?", - "delete-users-action-title": "Delete { count, plural, 1 {1 user} other {# users} }", - "delete-users-text": "Be careful, after the confirmation all selected users will be removed and all related data will become unrecoverable.", - "activation-email-sent-message": "Activation email was successfully sent!", - "resend-activation": "Resend activation", - "email": "Email", - "email-required": "Email is required.", - "invalid-email-format": "Invalid email format.", - "first-name": "First Name", - "last-name": "Last Name", - "description": "Description", - "default-dashboard": "Default dashboard", - "always-fullscreen": "Always fullscreen", - "select-user": "Select user", - "no-users-matching": "No users matching '{{entity}}' were found.", - "user-required": "User is required", - "activation-method": "Activation method", - "display-activation-link": "Display activation link", - "send-activation-mail": "Send activation mail", - "activation-link": "User activation link", - "activation-link-text": "In order to activate user use the following activation link :", - "copy-activation-link": "Copy activation link", - "activation-link-copied-message": "User activation link has been copied to clipboard", - "details": "Details", - "login-as-tenant-admin": "Login as Tenant Admin", - "login-as-customer-user": "Login as Customer User", - "disable-account": "Disable User Account", - "enable-account": "Enable User Account", - "enable-account-message": "User account was successfully enabled!", - "disable-account-message": "User account was successfully disabled!" - }, - "value": { - "type": "Value type", - "string": "String", - "string-value": "String value", - "integer": "Integer", - "integer-value": "Integer value", - "invalid-integer-value": "Invalid integer value", - "double": "Double", - "double-value": "Double value", - "boolean": "Boolean", - "boolean-value": "Boolean value", - "false": "False", - "true": "True", - "long": "Long", - "json": "JSON", - "json-value": "JSON value" - }, - "widget": { - "widget-library": "Widgets Library", - "widget-bundle": "Widgets Bundle", - "select-widgets-bundle": "Select widgets bundle", - "management": "Widget management", - "editor": "Widget Editor", - "widget-type-not-found": "Problem loading widget configuration.
    Probably associated\n widget type was removed.", - "widget-type-load-error": "Widget wasn't loaded due to the following errors:", - "remove": "Remove widget", - "edit": "Edit widget", - "remove-widget-title": "Are you sure you want to remove the widget '{{widgetTitle}}'?", - "remove-widget-text": "After the confirmation the widget and all related data will become unrecoverable.", - "timeseries": "Time series", - "search-data": "Search data", - "no-data-found": "No data found", - "latest-values": "Latest values", - "rpc": "Control widget", - "alarm": "Alarm widget", - "static": "Static widget", - "select-widget-type": "Select widget type", - "missing-widget-title-error": "Widget title must be specified!", - "widget-saved": "Widget saved", - "unable-to-save-widget-error": "Unable to save widget! Widget has errors!", - "save": "Save widget", - "saveAs": "Save widget as", - "save-widget-type-as": "Save widget type as", - "save-widget-type-as-text": "Please enter new widget title and/or select target widgets bundle", - "toggle-fullscreen": "Toggle fullscreen", - "run": "Run widget", - "title": "Widget title", - "title-required": "Widget title is required.", - "type": "Widget type", - "resources": "Resources", - "resource-url": "JavaScript/CSS URL", - "remove-resource": "Remove resource", - "add-resource": "Add resource", - "html": "HTML", - "tidy": "Tidy", - "css": "CSS", - "settings-schema": "Settings schema", - "datakey-settings-schema": "Data key settings schema", - "javascript": "Javascript", - "js": "JS", - "remove-widget-type-title": "Are you sure you want to remove the widget type '{{widgetName}}'?", - "remove-widget-type-text": "After the confirmation the widget type and all related data will become unrecoverable.", - "remove-widget-type": "Remove widget type", - "add-widget-type": "Add new widget type", - "widget-type-load-failed-error": "Failed to load widget type!", - "widget-template-load-failed-error": "Failed to load widget template!", - "add": "Add Widget", - "undo": "Undo widget changes", - "export": "Export widget" - }, - "widget-action": { - "header-button": "Widget header button", - "open-dashboard-state": "Navigate to new dashboard state", - "update-dashboard-state": "Update current dashboard state", - "open-dashboard": "Navigate to other dashboard", - "custom": "Custom action", - "custom-pretty": "Custom action (with HTML template)", - "target-dashboard-state": "Target dashboard state", - "target-dashboard-state-required": "Target dashboard state is required", - "set-entity-from-widget": "Set entity from widget", - "target-dashboard": "Target dashboard", - "open-right-layout": "Open right dashboard layout (mobile view)" - }, - "widgets-bundle": { - "current": "Current bundle", - "widgets-bundles": "Widgets Bundles", - "add": "Add Widgets Bundle", - "delete": "Delete widgets bundle", - "title": "Title", - "title-required": "Title is required.", - "add-widgets-bundle-text": "Add new widgets bundle", - "no-widgets-bundles-text": "No widgets bundles found", - "empty": "Widgets bundle is empty", - "details": "Details", - "widgets-bundle-details": "Widgets bundle details", - "delete-widgets-bundle-title": "Are you sure you want to delete the widgets bundle '{{widgetsBundleTitle}}'?", - "delete-widgets-bundle-text": "Be careful, after the confirmation the widgets bundle and all related data will become unrecoverable.", - "delete-widgets-bundles-title": "Are you sure you want to delete { count, plural, 1 {1 widgets bundle} other {# widgets bundles} }?", - "delete-widgets-bundles-action-title": "Delete { count, plural, 1 {1 widgets bundle} other {# widgets bundles} }", - "delete-widgets-bundles-text": "Be careful, after the confirmation all selected widgets bundles will be removed and all related data will become unrecoverable.", - "no-widgets-bundles-matching": "No widgets bundles matching '{{widgetsBundle}}' were found.", - "widgets-bundle-required": "Widgets bundle is required.", - "system": "System", - "import": "Import widgets bundle", - "export": "Export widgets bundle", - "export-failed-error": "Unable to export widgets bundle: {{error}}", - "create-new-widgets-bundle": "Create new widgets bundle", - "widgets-bundle-file": "Widgets bundle file", - "invalid-widgets-bundle-file-error": "Unable to import widgets bundle: Invalid widgets bundle data structure." - }, - "widget-config": { - "data": "Data", - "settings": "Settings", - "advanced": "Advanced", - "title": "Title", - "title-tooltip": "Title Tooltip", - "general-settings": "General settings", - "display-title": "Display title", - "drop-shadow": "Drop shadow", - "enable-fullscreen": "Enable fullscreen", - "background-color": "Background color", - "text-color": "Text color", - "padding": "Padding", - "margin": "Margin", - "widget-style": "Widget style", - "title-style": "Title style", - "mobile-mode-settings": "Mobile mode settings", - "order": "Order", - "height": "Height", - "units": "Special symbol to show next to value", - "decimals": "Number of digits after floating point", - "timewindow": "Timewindow", - "use-dashboard-timewindow": "Use dashboard timewindow", - "display-timewindow": "Display timewindow", - "display-legend": "Display legend", - "datasources": "Datasources", - "maximum-datasources": "Maximum { count, plural, 1 {1 datasource is allowed.} other {# datasources are allowed} }", - "datasource-type": "Type", - "datasource-parameters": "Parameters", - "remove-datasource": "Remove datasource", - "add-datasource": "Add datasource", - "target-device": "Target device", - "alarm-source": "Alarm source", - "actions": "Actions", - "action": "Action", - "add-action": "Add action", - "search-actions": "Search actions", - "action-source": "Action source", - "action-source-required": "Action source is required.", - "action-name": "Name", - "action-name-required": "Action name is required.", - "action-name-not-unique": "Another action with the same name already exists.
    Action name should be unique within the same action source.", - "action-icon": "Icon", - "action-type": "Type", - "action-type-required": "Action type is required.", - "edit-action": "Edit action", - "delete-action": "Delete action", - "delete-action-title": "Delete widget action", - "delete-action-text": "Are you sure you want delete widget action with name '{{actionName}}'?", - "display-icon": "Display title icon", - "icon-color": "Icon color", - "icon-size": "Icon size" - }, - "widget-type": { - "import": "Import widget type", - "export": "Export widget type", - "export-failed-error": "Unable to export widget type: {{error}}", - "create-new-widget-type": "Create new widget type", - "widget-type-file": "Widget type file", - "invalid-widget-type-file-error": "Unable to import widget type: Invalid widget type data structure." - }, - "widgets": { - "date-range-navigator": { - "localizationMap": { - "Sun": "Sun", - "Mon": "Mon", - "Tue": "Tue", - "Wed": "Wed", - "Thu": "Thu", - "Fri": "Fri", - "Sat": "Sat", - "Jan": "Jan", - "Feb": "Feb", - "Mar": "Mar", - "Apr": "Apr", - "May": "May", - "Jun": "Jun", - "Jul": "Jul", - "Aug": "Aug", - "Sep": "Sep", - "Oct": "Oct", - "Nov": "Nov", - "Dec": "Dec", - "January": "January", - "February": "February", - "March": "March", - "April": "April", - "June": "June", - "July": "July", - "August": "August", - "September": "September", - "October": "October", - "November": "November", - "December": "December", - "Custom Date Range": "Custom Date Range", - "Date Range Template": "Date Range Template", - "Today": "Today", - "Yesterday": "Yesterday", - "This Week": "This Week", - "Last Week": "Last Week", - "This Month": "This Month", - "Last Month": "Last Month", - "Year": "Year", - "This Year": "This Year", - "Last Year": "Last Year", - "Date picker": "Date picker", - "Hour": "Hour", - "Day": "Day", - "Week": "Week", - "2 weeks": "2 Weeks", - "Month": "Month", - "3 months": "3 Months", - "6 months": "6 Months", - "Custom interval": "Custom interval", - "Interval": "Interval", - "Step size": "Step size", - "Ok": "Ok" - } - }, - "input-widgets": { - "attribute-not-allowed": "Attribute parameter cannot be used in this widget", - "blocked-location": "Geolocation is blocked in your browser", - "claim-device": "Claim device", - "claim-failed": "Failed to claim the device!", - "claim-not-found": "Device not found!", - "claim-successful": "Device was successfully claimed!", - "date": "Date", - "device-name": "Device name", - "device-name-required": "Device name is required", - "discard-changes": "Discard changes", - "entity-attribute-required": "Entity attribute is required", - "entity-coordinate-required": "Both fields, latitude and longitude, are required", - "entity-timeseries-required": "Entity timeseries is required", - "get-location": "Get current location", - "latitude": "Latitude", - "longitude": "Longitude", - "not-allowed-entity": "Selected entity cannot have shared attributes", - "no-attribute-selected": "No attribute is selected", - "no-datakey-selected": "No datakey is selected", - "no-coordinate-specified": "Datakey for latitude/longitude doesn't specified", - "no-entity-selected": "No entity selected", - "no-image": "No image", - "no-support-geolocation": "Your browser doesn't support geolocation", - "no-support-web-camera": "No supported web camera", - "no-timeseries-selected": "No timeseries selected", - "secret-key": "Secret key", - "secret-key-required": "Secret key is required", - "switch-attribute-value": "Switch entity attribute value", - "switch-camera": "Switch camera", - "switch-timeseries-value": "Switch entity timeseries value", - "take-photo": "Take photo", - "time": "Time", - "timeseries-not-allowed": "Timeseries parameter cannot be used in this widget", - "update-failed": "Update failed", - "update-successful": "Update successful", - "update-attribute": "Update attribute", - "update-timeseries": "Update timeseries", - "value": "Value" - } - }, - "icon": { - "icon": "Icon", - "select-icon": "Select icon", - "material-icons": "Material icons", - "show-all": "Show all icons" - }, - "custom": { - "widget-action": { - "action-cell-button": "Action cell button", - "row-click": "On row click", - "polygon-click": "On polygon click", - "marker-click": "On marker click", - "tooltip-tag-action": "Tooltip tag action", - "node-selected": "On node selected", - "element-click": "On HTML element click", - "pie-slice-click": "On slice click", - "row-double-click": "On row double click" - } - }, - "language": { - "language": "Language", - "locales": { - "de_DE": "Deutsch", - "fr_FR": "Français", - "zh_CN": "简体中文", - "zh_TW": "繁體中文", - "en_US": "English", - "it_IT": "Italiano", - "ko_KR": "한글", - "ru_RU": "Русский", - "es_ES": "Español", - "ja_JA": "日本語", - "tr_TR": "Türkçe", - "fa_IR": "فارسي", - "uk_UA": "Українська", - "cs_CZ": "Česky", - "el_GR": "Ελληνικά", - "ro_RO": "Română", - "lv_LV": "Latviešu" - } - } -} diff --git a/ui/src/app/locale/locale.constant-es_ES.json b/ui/src/app/locale/locale.constant-es_ES.json deleted file mode 100644 index df5752f54b..0000000000 --- a/ui/src/app/locale/locale.constant-es_ES.json +++ /dev/null @@ -1,1765 +0,0 @@ -{ - "access": { - "unauthorized": "No autorizado", - "unauthorized-access": "Acceso no autorizado", - "unauthorized-access-text": "¡Debe registrarse para tener acceso a este recurso!", - "access-forbidden": "Acceso Prohibido", - "access-forbidden-text": "No tiene permisos para acceder a esta ubicación!
    Intente registrarse con otro usuario si aún desea acceder a esta ubicación.", - "refresh-token-expired": "La sesión ha expirado", - "refresh-token-failed": "No se puede actualizar la sesión" - }, - "action": { - "activate": "Activar", - "suspend": "Suspender", - "save": "Guardar", - "saveAs": "Guardar como", - "cancel": "Cancelar", - "ok": "Aceptar", - "delete": "Eliminar", - "add": "Agregar", - "yes": "Si", - "no": "No", - "update": "Actualizar", - "remove": "Eliminar", - "search": "Buscar", - "clear-search": "Borrar búsqueda", - "assign": "Asignar", - "unassign": "Anular asignación", - "share": "Compartir", - "make-private": "Hacer privado", - "apply": "Aplicar", - "apply-changes": "Aplicar cambios", - "edit-mode": "Modo edición", - "enter-edit-mode": "Entrar en modo edición", - "decline-changes": "Descartar cambios", - "close": "Cerrar", - "back": "Atrás", - "run": "Ejecutar", - "sign-in": "¡Registrarse!", - "edit": "Editar", - "view": "Ver", - "create": "Crear", - "drag": "Arrastrar", - "refresh": "Actualizar", - "undo": "Deshacer", - "copy": "Copiar", - "paste": "Pegar", - "copy-reference": "Copiar referencia", - "paste-reference": "Pegar referencia", - "import": "Importar", - "export": "Exportar", - "share-via": "Compartir vía {{provider}}", - "discard-changes": "Cancelar los cambios", - "continue": "Continuar" - }, - "aggregation": { - "aggregation": "Agregación", - "function": "Función de agregación de datos", - "limit": "Valores máximos", - "group-interval": "Intervalo de agrupamiento", - "min": "Min", - "max": "Max", - "avg": "Promedio", - "sum": "Suma", - "count": "Contar", - "none": "Ninguno" - }, - "admin": { - "general": "General", - "general-settings": "Configuración general", - "outgoing-mail": "Servidor de correo", - "outgoing-mail-settings": "Configuración del servidor de correo de salida", - "system-settings": "Configuración del sistema", - "test-mail-sent": "¡El correo de prueba fue enviado correctamente!", - "base-url": "URL base", - "base-url-required": "URL base es requerida.", - "mail-from": "Correo desde", - "mail-from-required": "Correo desde es requerido.", - "smtp-protocol": "Protocolo SMTP", - "smtp-host": "Host SMTP", - "smtp-host-required": "Host SMTP es requerido.", - "smtp-port": "Puerto SMTP", - "smtp-port-required": "Debe suministrar un puerto SMTP", - "smtp-port-invalid": "Eso no parece un puerto SMTP válido.", - "timeout-msec": "Tiempo de espera (ms)", - "timeout-required": "Tiempo de espera es requerido.", - "timeout-invalid": "Eso no parece un tiempo de espera válido.", - "enable-tls": "Habilitar TLS", - "tls-version": "Versión TLS", - "send-test-mail": "Enviar correo de prueba", - "password-policy": "Política de contraseñas", - "security-settings": "Configuraciones de seguridad", - "minimum-password-length": "Longitud mínima de contraseña", - "minimum-password-length-required": "Se requiere una longitud mínima de contraseña", - "minimum-password-length-range": "La longitud mínima de la contraseña debe estar en un rango de 5 a 50", - "minimum-uppercase-letters": "Número mínimo de letras mayúsculas", - "minimum-uppercase-letters-range": "El número mínimo de letras mayúsculas no puede ser negativo", - "minimum-lowercase-letters": "Número mínimo de letras minúsculas", - "minimum-lowercase-letters-range": "El número mínimo de letras minúsculas no puede ser negativo", - "minimum-digits": "Número mínimo de dígitos", - "minimum-digits-range": "El número mínimo de dígitos no puede ser negativo", - "minimum-special-characters": "Número mínimo de caracteres especiales.", - "minimum-special-characters-range": "El número mínimo de caracteres especiales no puede ser negativo.", - "password-expiration-period-days": "Periodo de caducidad de contraseña en días", - "password-expiration-period-days-range": "El período de caducidad de la contraseña en días no puede ser negativo", - "password-reuse-frequency-days": "Frecuencia de reutilización de contraseña en días", - "password-reuse-frequency-days-range": "La frecuencia de reutilización de contraseña en días no puede ser negativa", - "general-policy": "Política general", - "max-failed-login-attempts": "Número máximo de intentos fallidos de inicio de sesión, antes de que la cuenta esté bloqueada", - "minimum-max-failed-login-attempts-range": "El número máximo de intentos fallidos de inicio de sesión no puede ser negativo", - "user-lockout-notification-email": "En caso de bloqueo de la cuenta del usuario, envíe una notificación por correo electrónico" - }, - "alarm": { - "alarm": "Alarma", - "alarms": "Alarmas", - "select-alarm": "Seleccionar alarma", - "no-alarms-matching": "Alarmas que coincidan con '{{entity}}' no fueron encontradas.", - "alarm-required": "Alarma es requerida", - "alarm-status": "Estado de la alarma", - "search-status": { - "ANY": "Todas", - "ACTIVE": "Activas", - "CLEARED": "Borradas", - "ACK": "Reconocidas", - "UNACK": "Ignoradas" - }, - "display-status": { - "ACTIVE_UNACK": "Activa ignorada", - "ACTIVE_ACK": "Activa reconocida", - "CLEARED_UNACK": "Borrada ignorada", - "CLEARED_ACK": "Borrada reconocida" - }, - "no-alarms-prompt": "No se encontraron alarmas", - "created-time": "Tiempo de creación", - "type": "Tipo", - "severity": "Criticidad", - "originator": "Origen", - "originator-type": "Tipo de origen", - "details": "Detalles", - "status": "Estado", - "alarm-details": "Detalles de la alarma", - "start-time": "Tiempo de inicio", - "end-time": "Tiempo de finalización", - "ack-time": "Tiempo de reconocimiento", - "clear-time": "Tiempo de borrado", - "severity-critical": "Crítica", - "severity-major": "Alta", - "severity-minor": "Baja", - "severity-warning": "Alerta", - "severity-indeterminate": "Indeterminada", - "acknowledge": "Reconocer", - "clear": "Borrar", - "search": "buscar alarmas", - "selected-alarms": "{ count, plural, 1 {1 alarma} other {# alarmas} } seleccionadas", - "no-data": "No hay datos para mostrar", - "polling-interval": "Intervalo de sondeo de alarmas (seg)", - "polling-interval-required": "Intervalo de sondeo de alarmas es requerido.", - "min-polling-interval-message": "Se permite al menos 1 segundo de intervalo de sondeo.", - "aknowledge-alarms-title": "Reconocer { count, plural, 1 {1 alarma} other {# alarmas} }", - "aknowledge-alarms-text": "¿Está seguro de que desea reconocer { count, plural, 1 {1 alarma} other {# alarmas} }?", - "aknowledge-alarm-title": "Reconocer alarma", - "aknowledge-alarm-text": "¿Está seguro que quiere reconocer la alarma?", - "clear-alarms-title": "Quitar { count, plural, 1 {1 alarma} other {# alarmas} }", - "clear-alarms-text": "¿Está seguro de que desea quitar { count, plural, 1 {1 alarma} other {# alarmas}?", - "clear-alarm-title": "Quitar alarma", - "clear-alarm-text": "¿Está seguro que quiere quitar la alarma?", - "alarm-status-filter": "Filtro de estado de alarma" - }, - "alias": { - "add": "Agregar alias", - "edit": "Editar alias", - "name": "Nombre de alias", - "name-required": "Nombre de alias es requerido", - "duplicate-alias": "Ya existe un alias con el mismo nombre.", - "filter-type-single-entity": "Entidad única", - "filter-type-entity-list": "Lista de entidades", - "filter-type-entity-name": "Nombre de entidad", - "filter-type-state-entity": "Entidad del panel de estados", - "filter-type-state-entity-description": "Entidad tomada desde los parámetros del panel de estados", - "filter-type-asset-type": "Tipo de activo", - "filter-type-asset-type-description": "Activos de tipo '{{assetType}}'", - "filter-type-asset-type-and-name-description": "Activos del tipo '{{assetType}}' y su nombre empieza con '{{prefix}}'", - "filter-type-device-type": "Tipo de dispositivo", - "filter-type-device-type-description": "Dispositivos de tipo '{{deviceType}}'", - "filter-type-device-type-and-name-description": "Dispositivos del tipo '{{deviceType}}' y su nombre empieza con '{{prefix}}'", - "filter-type-entity-view-type": "Tipo de vista de entidad", - "filter-type-entity-view-type-description": "Vista de entidad del tipo '{{entityView}}'", - "filter-type-entity-view-type-and-name-description": "Las vista de entidad del tipo '{{entityView}}' y cuyo nombre comienza con '{{prefix}}'", - "filter-type-relations-query": "Consulta de relaciones", - "filter-type-relations-query-description": "{{entities}} que tienen {{relationType}} relación {{direction}} {{rootEntity}}", - "filter-type-asset-search-query": "Consultar búsqueda de activos", - "filter-type-asset-search-query-description": "Activos con tipos {{assetTypes}} que tienen {{relationType}} relación {{direction}} {{rootEntity}}", - "filter-type-device-search-query": "Consultar búqueda de dispositivos", - "filter-type-device-search-query-description": "Dispositivos con tipos {{deviceTypes}} que tienen {{relationType}} relación {{direction}} {{rootEntity}}", - "filter-type-entity-view-search-query": "Consultar vista de entidad", - "filter-type-entity-view-search-query-description": "Las vista de entidad de tipo {{entityViewTypes}} que tienen {{relationType}} relación {{direction}} {{rootEntity}}", - "entity-filter": "Filtro de entidad", - "resolve-multiple": "Resolver como entidades múltiples", - "filter-type": "Tipo de filtro", - "filter-type-required": "Tipo de filtro es requerido.", - "entity-filter-no-entity-matched": "No se encontraron entidades que coincidan con el filtro especificado.", - "no-entity-filter-specified": "No se especificó el filtro de entidad", - "root-state-entity": "Utilizar la entidad del panel de estados como raíz", - "root-entity": "Entidad raíz", - "state-entity-parameter-name": "Nombre de parámetro de entidad de estado", - "default-state-entity": "Entidad de estado predeterminada", - "default-entity-parameter-name": "Por defecto", - "max-relation-level": "Nivel máximo de relación", - "unlimited-level": "Nivel ilimitado", - "state-entity": "Entidad del panel de estados", - "all-entities": "Todas las entidades", - "any-relation": "alguna" - }, - "asset": { - "asset": "Activo", - "assets": "Activos", - "management": "Gestión de activos", - "view-assets": "Ver activos", - "add": "Agregar activo", - "assign-to-customer": "Asignar al cliente", - "assign-asset-to-customer": "Asignar activo(s) al cliente", - "assign-asset-to-customer-text": "Por favor, seleccione los activos para asignar al cliente", - "no-assets-text": "No se encontraron activos", - "assign-to-customer-text": "Por favor, seleccione el cliente para asignar el(los) activo(s)", - "public": "Público", - "assignedToCustomer": "Asignado al cliente", - "make-public": "Hacer público el activo", - "make-private": "Hacer privado el activo", - "unassign-from-customer": "Anular asignación del cliente", - "delete": "Eliminar activo", - "asset-public": "El activo es público", - "asset-type": "Tipo de activo", - "asset-type-required": "El tipo de activo es requerido.", - "select-asset-type": "Seleccionar tipo de activo", - "enter-asset-type": "Introduzca el tipo de activo", - "any-asset": "Algún activo", - "no-asset-types-matching": "No se encontraron tipos de activos que coincidan con '{{entitySubtype}}'.", - "asset-type-list-empty": "No se seleccionaron tipos de activos.", - "asset-types": "Tipos de activos", - "name": "Nombre", - "name-required": "El nombre es requerido.", - "description": "Descripción", - "type": "Tipo", - "type-required": "El tipo es requerido.", - "details": "Detalles", - "events": "Eventos", - "add-asset-text": "Agregar nuevo activo", - "asset-details": "Detalles del activo", - "assign-assets": "Asignar activos", - "assign-assets-text": "Asignar { count, plural, 1 {1 activo} other {# activos} } al cliente", - "delete-assets": "Eliminar activos", - "unassign-assets": "Anular asignación de activos", - "unassign-assets-action-title": "Anular asignación { count, plural, 1 {1 activo} other {# activos} } del cliente", - "assign-new-asset": "Asignar nuevo activo", - "delete-asset-title": "¿Está seguro de que desea eliminar el activo '{{assetName}}'?", - "delete-asset-text": "¡Cuidado! Después de la confirmación, el activo y todos los datos relacionados serán irrecuperables.", - "delete-assets-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 activos} other {# activos} }?", - "delete-assets-action-title": "Eliminar { count, plural, 1 {1 activo} other {# activos} }", - "delete-assets-text": "¡Cuidado! Después de la confirmación se eliminarán todos los activos seleccionados y todos los datos relacionados serán irrecuperables.", - "make-public-asset-title": "¿Está seguro de que desea que el activo '{{assetName}}' sea público?", - "make-public-asset-text": "Después de la confirmación, el activo y todos sus datos se harán públicos y accesibles por otros.", - "make-private-asset-title": "¿Está seguro de que desea que el activo '{{assetName}}' sea privado?", - "make-private-asset-text": "Después de la confirmación, el activo y todos sus datos se harán privados y no serán accesibles para otros", - "unassign-asset-title": "¿Está seguro de que desea anular asignación del activo '{{assetName}}'?", - "unassign-asset-text": "Después de la confirmación, se anulará asignación del activo y no será accesible por el cliente.", - "unassign-asset": "Anular asignación activo", - "unassign-assets-title": "¿Está seguro de que desea anular asignación { count, plural, 1 {1 activo} other {# activos} }?", - "unassign-assets-text": "Después de la confirmación, se anulará asignación de todos los activos seleccionados y no serán accesibles por el cliente", - "copyId": "Copiar ID del activo", - "idCopiedMessage": "ID del activo has sido copiada al portapapeles", - "select-asset": "Seleccionar activo", - "no-assets-matching": "No se encontraron activos que coincidan con '{{entity}}'.", - "asset-required": "El activo es requerido", - "name-starts-with": "El nombre del activo comienza con", - "import": "Importar activos", - "asset-file": "Archivo del activo", - "label": "Etiqueta" - }, - "attribute": { - "attributes": "Atributos", - "latest-telemetry": "Última telemetría", - "attributes-scope": "Alcance de los atributos de la entidad", - "scope-latest-telemetry": "Última telemetría", - "scope-client": "Atributos del cliente", - "scope-server": "Atributos del servidor", - "scope-shared": "Atributos compartidos", - "add": "Agregar atributos", - "key": "Clave", - "last-update-time": "Hora de la última actualización", - "key-required": "La clave del aributo es requerida.", - "value": "Valor", - "value-required": "Valor del atributo es requerido.", - "delete-attributes-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 atributo} other {# atributos} }?", - "delete-attributes-text": "¡Cuidado! Después de la confirmación, se eliminarán todos los atributos seleccionados.", - "delete-attributes": "Eliminar atributos", - "enter-attribute-value": "Introduzca el valor del atributo", - "show-on-widget": "Mostrar en widget", - "widget-mode": "Modo widget", - "next-widget": "Widget siguiente", - "prev-widget": "Widget previo", - "add-to-dashboard": "Agregar al panel", - "add-widget-to-dashboard": "Agregar widget al panel", - "selected-attributes": "{ count, plural, 1 {1 atributo} other {# atributos} } seleccionados", - "selected-telemetry": "{ count, plural, 1 {1 telemetry unit} other {# telemetry units} } seleccionadas" - }, - "audit-log": { - "audit": "Auditoría", - "audit-logs": "Registros de auditoría", - "timestamp": "Marca de tiempo", - "entity-type": "Tipo de entidad", - "entity-name": "Nombre de entidad", - "user": "Usuario", - "type": "Tipo", - "status": "Estado", - "details": "Detalles", - "type-added": "Agregado", - "type-deleted": "Eliminado", - "type-updated": "Actualizado", - "type-attributes-updated": "Atributos actualizados", - "type-attributes-deleted": "Atributos eliminados", - "type-rpc-call": "Llamada RPC", - "type-credentials-updated": "Credenciales actualizadas", - "type-assigned-to-customer": "Asignado al cliente", - "type-unassigned-from-customer": "Asignación anulada del cliente", - "type-activated": "Activado", - "type-suspended": "Suspendido", - "type-credentials-read": "Credenciales leídas", - "type-attributes-read": "Atributos leídos", - "type-relation-add-or-update": "Relación actualizada", - "type-relation-delete": "Relación eliminada", - "type-relations-delete": "Toda relación eliminada", - "type-alarm-ack": "Reconocida", - "type-alarm-clear": "Borrada", - "type-login": "Inicio de sesión", - "type-logout": "Cierre de sesión", - "type-lockout": "Cierre patronal", - "status-success": "Correcto", - "status-failure": "Erróneo", - "audit-log-details": "Detalle del registro de auditoría", - "no-audit-logs-prompt": "No se encontraron registros", - "action-data": "Datos de acción", - "failure-details": "Detalles del error", - "search": "Buscar registros de auditoría", - "clear-search": "Borrar búsqueda" - }, - "confirm-on-exit": { - "message": "Tiene cambios sin guardar. ¿Está seguro de que desea salir de esta página?", - "html-message": "Tiene cambios sin guardar.
    ¿Está seguro de que desea salir de esta página?", - "title": "Cambios sin guardar" - }, - "contact": { - "country": "País", - "city": "Ciudad", - "state": "Estado / Provincia", - "postal-code": "Código Postal", - "postal-code-invalid": "Formato de código postal inválido.", - "address": "Dirección", - "address2": "Dirección 2", - "phone": "Teléfono", - "email": "Correo Electrónico", - "no-address": "Sin dirección" - }, - "common": { - "username": "Nombre de usuario", - "password": "Contraseña", - "enter-username": "Introduzca nombre de usuario", - "enter-password": "Introduzca contraseña", - "enter-search": "Introduzca búsqueda" - }, - "content-type": { - "json": "Json", - "text": "Texto", - "binary": "Binario (Base64)" - }, - "customer": { - "customer": "Cliente", - "customers": "Clientes", - "management": "Gestión del cliente", - "dashboard": "Panel del cliente", - "dashboards": "Paneles del cliente", - "devices": "Dispositivos del cliente", - "entity-views": "Vistas de entidad del cliente", - "assets": "Activos del Cliente", - "public-dashboards": "Paneles públicos", - "public-devices": "Dispositivos públicos", - "public-assets": "Activos públicos", - "public-entity-views": "Vista de entidad públicas", - "add": "Agregar cliente", - "delete": "Eliminar cliente", - "manage-customer-users": "Gestionar usuarios del cliente", - "manage-customer-devices": "Gestionar dispositivos del cliente", - "manage-customer-dashboards": "Gestionar paneles del cliente", - "manage-public-devices": "Gestionar dispositivos públicos", - "manage-public-dashboards": "Gestionar paneles públicos", - "manage-customer-assets": "Gestionar activos del cliente", - "manage-public-assets": "Gestionar activos públicos", - "add-customer-text": "Agregar nuevo cliente", - "no-customers-text": "No se encontraron clientes", - "customer-details": "Detalles del cliente", - "delete-customer-title": "¿Está seguro de que desea eliminar al cliente '{{customerTitle}}'?", - "delete-customer-text": "¡Cuidado! Después de la confirmación, el cliente y todos los datos relacionados serán irrecuperables.", - "delete-customers-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 cliente} other {# clientes} }?", - "delete-customers-action-title": "Eliminar { count, plural, 1 {1 cliente} other {# clientes} }", - "delete-customers-text": "¡Cuidado! Después de la confirmación, todos los clientes seleccionados serán eliminados y todos los datos relacionados serán irrecuperables.", - "manage-users": "Gestionar usuarios", - "manage-assets": "Gestionar activos", - "manage-devices": "Gestionar dispositivos", - "manage-dashboards": "Gestionar paneles", - "title": "Título", - "title-required": "El título es requerido.", - "description": "Descripción", - "details": "Detalles", - "events": "Eventos", - "copyId": "Copiar ID del cliente", - "idCopiedMessage": "ID del cliente ha sido copiada al portapapeles", - "select-customer": "Seleccionar cliente", - "no-customers-matching": "No se encontraron clientes que coincidan con '{{entity}}'.", - "customer-required": "El cliente es requerido", - "select-default-customer": "Seleccionar cliente predeterminado", - "default-customer": "Cliente predeterminado", - "default-customer-required": "Cliente predeterminado es requerido para depurar el panel en el nivel Organización" - }, - "datetime": { - "date-from": "Fecha desde", - "time-from": "Tiempo desde", - "date-to": "Fecha hasta", - "time-to": "Tiempo hasta" - }, - "dashboard": { - "dashboard": "Panel", - "dashboards": "Paneles", - "management": "Gestión del panel", - "view-dashboards": "Ver panel", - "add": "Agregar paneles", - "assign-dashboard-to-customer": "Asignar panel(es) al cliente", - "assign-dashboard-to-customer-text": "Por favor selecciona los paneles para asignar al cliente", - "assign-to-customer-text": "Por favor selecciona el cliente para asignar el(los) panel(es)", - "assign-to-customer": "Asignar al cliente", - "unassign-from-customer": "Anular asignación del cliente", - "make-public": "Hacer panel público", - "make-private": "Hacer panel privado", - "manage-assigned-customers": "Gestionar clientes asignados", - "assigned-customers": "Clientes asignados", - "assign-to-customers": "Asignar panel(es) al(los) cliente(s)", - "assign-to-customers-text": "Por favor seleccionar los clientes para asignar el(los) panel(es)", - "unassign-from-customers": "Anular asignación del(de los) panel(es) de los clientes", - "unassign-from-customers-text": "Por favor selecciona los clientes para anular asignación del(de los) panel(es)", - "no-dashboards-text": "No se encontraron paneles", - "no-widgets": "Sin widgets configurados", - "add-widget": "Agregar nuevo widget", - "title": "Título", - "select-widget-title": "Seleccionar widget", - "select-widget-subtitle": "Lista de tipos de widget disponibles", - "delete": "Eliminar panel", - "title-required": "El título es requerido.", - "description": "Descripción", - "details": "Detalles", - "dashboard-details": "Detalles del panel", - "add-dashboard-text": "Agregar nuevo panel", - "assign-dashboards": "Asignar paneles", - "assign-new-dashboard": "Asignar nuevo panel", - "assign-dashboards-text": "Asignar { count, plural, 1 {1 panel} other {# paneles} } a los clientes", - "unassign-dashboards-action-text": "Anular asignación { count, plural, 1 {1 dashboard} other {# dashboards} } de los clientes", - "delete-dashboards": "Eliminar paneles", - "unassign-dashboards": "Anular asignación de paneles", - "unassign-dashboards-action-title": "Anular asignación { count, plural, 1 {1 panel} other {# paneles} } del cliente", - "delete-dashboard-title": "¿Está seguro de que desea eliminar el panel '{{dashboardTitle}}'?", - "delete-dashboard-text": "¡Cuidado! Después de la confirmación, el panel y todos los datos relacionados serán irrecuperables.", - "delete-dashboards-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 dashboard} other {# dashboards} }?", - "delete-dashboards-action-title": "Eliminar { count, plural, 1 {1 dashboard} other {# dashboards} }", - "delete-dashboards-text": "¡Cuidado! Después de la confirmación, todos los paneles seleccionados serán eliminados y todos los datos relacionados serán irrecuperables.", - "unassign-dashboard-title": "¿Está seguro de que desea anular la asignación del panel '{{dashboardTitle}}'?", - "unassign-dashboard-text": "Después de la confirmación, se anulará la asignación del panel y no será accesible por el cliente.", - "unassign-dashboard": "Anular asignación del panel", - "unassign-dashboards-title": "¿Está seguro de que desea anular asignación { count, plural, 1 {1 panel} other {# paneles} }?", - "unassign-dashboards-text": "Después de la confirmación, se anulará la asignación de todos los paneles seleccionados y no serán accesibles por el cliente.", - "public-dashboard-title": "El panel es ahora público", - "public-dashboard-text": "Su panel {{dashboardTitle}} es ahora público y es accesible a través del siguiente enlace público :", - "public-dashboard-notice": "Nota No olvide hacer públicos los dispositivos relacionados para acceder a sus datos.", - "make-private-dashboard-title": "¿Está seguro de que desea hacer el panel '{{dashboardTitle}}' privado?", - "make-private-dashboard-text": "Después de la confirmación el panel se hará privado y no será accesible por otros.", - "make-private-dashboard": "Hacer el panel privado", - "socialshare-text": "'{{dashboardTitle}}' desarrollado por ThingsBoard.", - "socialshare-title": "'{{dashboardTitle}}' desarrollado por ThingsBoard", - "select-dashboard": "Seleccionar panel", - "no-dashboards-matching": "Paneles que coincidan con '{{entity}}' no fueron encontrados.", - "dashboard-required": "Panel es requerido.", - "select-existing": "Seleccionar panel existente", - "create-new": "Crear nuevo panel", - "new-dashboard-title": "Nuevo título de panel", - "open-dashboard": "Abrir panel", - "set-background": "Definir fondo", - "background-color": "Color de fondo", - "background-image": "Imagen de fondo", - "background-size-mode": "Modo de tamaño de fondo", - "no-image": "Ninguna imagen seleccionada", - "drop-image": "Colocar una imagen o hacer clic para seleccionar un archivo para cargar.", - "settings": "Configuración", - "columns-count": "Número de columnas", - "columns-count-required": "Número de columnas es requerido.", - "min-columns-count-message": "Solo está permitido 10 columnas como mínimo.", - "max-columns-count-message": "Sólo está permitido 1000 columnas como máximo.", - "widgets-margins": "Margen entre widgets", - "horizontal-margin": "Margen horizontal", - "horizontal-margin-required": "El valor del margen horizontal es requerido.", - "min-horizontal-margin-message": "Sólo está permitido el 0 como valor mínimo para el margen horizontal", - "max-horizontal-margin-message": "Sólo está permitido el 50 como valor máximo para el margen horizontal", - "vertical-margin": "Margen vertical", - "vertical-margin-required": "El valor del margen vertical es requerido.", - "min-vertical-margin-message": "Sólo está permitido el 0 como valor mínimo para el margen vertical.", - "max-vertical-margin-message": "Solo está permitido el 50 como valor máximo para el margen vertical", - "autofill-height": "Llenado automático de altura de diseño", - "mobile-layout": "Configuración de diseño para móvil", - "mobile-row-height": "Altura de fila para móvil, píxel", - "mobile-row-height-required": "Altura de fila para móvil es requerida.", - "min-mobile-row-height-message": "Sólo está permitido 5 píxeles como valor mínimo en la altura de fila para móvil.", - "max-mobile-row-height-message": "Sólo está permitido 200 píxeles como valor máximo en la altura de fila para móvil.", - "display-title": "Mostrar título del panel", - "toolbar-always-open": "Mantener la barra de herramientas abierta", - "title-color": "Color del título", - "display-dashboards-selection": "Mostrar selección del panel", - "display-entities-selection": "Mostrar selección de entidades", - "display-dashboard-timewindow": "Mostrar ventana de tiempo", - "display-dashboard-export": "Mostrar exportar", - "import": "Importar panel", - "export": "Exportar panel", - "export-failed-error": "No se puede exportar el panel: {{error}}", - "create-new-dashboard": "Crear nuevo panel", - "dashboard-file": "Archivo del panel", - "invalid-dashboard-file-error": "No se puede importar el panel: estructura de datos del panel no es válida.", - "dashboard-import-missing-aliases-title": "Configurar los alias utilizados por el panel importado", - "create-new-widget": "Crear nuevo widget", - "import-widget": "Importar widget", - "widget-file": "Archivo del widget", - "invalid-widget-file-error": "No se puede importar el widget: estructura de datos del widger no es válida.", - "widget-import-missing-aliases-title": "Configurar los alias utilizados por el widget importado", - "open-toolbar": "Abrir barra de herramientas del panel", - "close-toolbar": "Cerrar barra de herramientas", - "configuration-error": "Error de configuración", - "alias-resolution-error-title": "Error de configuración de los alias del panel", - "invalid-aliases-config": "No se puede encontrar algún dispositivo que coincida con algunos alias del filtro.
    Por favor, contacte a su administrador para resolver este problema.", - "select-devices": "Seleccionar dispositivos", - "assignedToCustomer": "Asignado al cliente", - "assignedToCustomers": "Asignado a los clientes", - "public": "Público", - "public-link": "Enlace público", - "copy-public-link": "Copiar enlace público", - "public-link-copied-message": "El enlace público del panel ha sido copiado al portapapeles", - "manage-states": "Gestionar estados del panel", - "states": "Estados del panel", - "search-states": "Buscar estados del panel", - "selected-states": "{ count, plural, 1 {1 estado del panel } other {# estados del panel } } seleccionados", - "edit-state": "Editar estado del panel", - "delete-state": "Eliminar estado del panel", - "add-state": "Agregar estado del panel", - "state": "Estado del panel", - "state-name": "Nombre", - "state-name-required": "El nombre del estado del panel es requerido.", - "state-id": "ID del estado", - "state-id-required": "ID del estado del panel es requerida.", - "state-id-exists": "Ya existe el estado del panel con el mismo ID.", - "is-root-state": "Estado raíz", - "delete-state-title": "Eliminar estado del panel", - "delete-state-text": "¿Está seguro de que desea eliminar el estado del panel con el nombre '{{stateName}}'?", - "show-details": "Mostrar detalles", - "hide-details": "Ocultar detalles", - "select-state": "Seleccionar estado objetivo", - "state-controller": "Estado del controlador" - }, - "datakey": { - "settings": "Configuración", - "advanced": "Avanzado", - "label": "Etiqueta", - "color": "Color", - "units": "Símbolo especial para mostrar al lado del valor", - "decimals": "Número de dígitos después del punto flotante", - "data-generation-func": "Función de generación de datos", - "use-data-post-processing-func": "Usar la función de post-procesamiento de datos", - "configuration": "Configuración de clave de datos", - "timeseries": "Series temporales", - "attributes": "Atributos", - "alarm": "Campos de alarma", - "timeseries-required": "Series temporales de la entidad son requeridas", - "timeseries-or-attributes-required": "Series temporales/atributos de la entidad son requeridos.", - "maximum-timeseries-or-attributes": "Máximo { count, plural, 1 {1 serie temporal/atributo permitido.} other {# series temporales/atributos permitidos} }", - "alarm-fields-required": "Campos de alarma son requeridos.", - "function-types": "Tipos de funciones", - "function-types-required": "Tipos de funciones son requeridos.", - "maximum-function-types": "Máximo { count, plural, 1 {1 function type is allowed.} other {# function types are allowed} }", - "time-description": "marca de tiempo del valor actual;", - "value-description": "el valor actual;", - "prev-value-description": "resultado de la llamada a la función anterior;", - "time-prev-description": "marca de tiempo del valor anterior;", - "prev-orig-value-description": "valor anterior original;" - }, - "datasource": { - "type": "Tipo de fuente de datos", - "name": "Nombre", - "add-datasource-prompt": "Por favor agregue fuente de datos" - }, - "details": { - "edit-mode": "Modo de edición", - "toggle-edit-mode": "Cambiar modo de edición" - }, - "device": { - "device": "Dispositivo", - "device-required": "Dispositivo es requerido.", - "devices": "Dispositivos", - "management": "Gestión del dispositivo", - "view-devices": "Ver dispositivos", - "device-alias": "Alias del dispositivo", - "aliases": "Alias de los dispositivos", - "no-alias-matching": "'{{alias}}' no encontrado.", - "no-aliases-found": "No se encontraron alias.", - "no-key-matching": "'{{key}}' no encontrado.", - "no-keys-found": "Claves no encontradas.", - "create-new-alias": "¡Crear uno nuevo!", - "create-new-key": "¡Crear una nueva!", - "duplicate-alias-error": "Alias duplicado encontrado '{{alias}}'.
    Los alias del dispositivo deben ser únicos dentro del panel.", - "configure-alias": "Configurar '{{alias}}' alias", - "no-devices-matching": "Dispositivos que coincidan con '{{entity}}' no fueron encontrados.", - "alias": "Alias", - "alias-required": "Alias del dispositivo es requerido.", - "remove-alias": "Eliminar alias del dispositivo", - "add-alias": "Agregar alias del dispositivo", - "name-starts-with": "El nombre del dispositivo comienza con", - "device-list": "Lista de dispositivos", - "use-device-name-filter": "Utilizar filtro", - "device-list-empty": "Ningún dispositivo seleccionado.", - "device-name-filter-required": "Filtro de nombre de dispositivo es requerido.", - "device-name-filter-no-device-matched": "Dispositivos que comienzan con '{{device}}' no fueron encontrados.", - "add": "Agregar Dispositivo", - "assign-to-customer": "Asignar al cliente", - "assign-device-to-customer": "Asignar Dispositivo(s) Al Cliente", - "assign-device-to-customer-text": "Por favor selecciona los dispositivos para asignar al cliente", - "make-public": "Hacer público el dispositivo", - "make-private": "Hacer privado el dispositivo", - "no-devices-text": "No se encontraron dispositivos", - "assign-to-customer-text": "Por favor seleccionar el cliente para asignar el(los) dispositivo(s)", - "device-details": "Detalles del dispositivo", - "add-device-text": "Agregar nuevo dispositivo", - "credentials": "Credenciales", - "manage-credentials": "Gestionar credenciales", - "delete": "Eliminar dispositivo", - "assign-devices": "Asignar dispositivos", - "assign-devices-text": "Asignar { count, plural, 1 {1 dispositivo} other {# dispositivos} } al cliente", - "delete-devices": "Eliminar dispositivos", - "unassign-from-customer": "Anular asignación del cliente", - "unassign-devices": "Desasignar dispositivos", - "unassign-devices-action-title": "Anular asignación { count, plural, 1 {1 dispositivo} other {# dispositivos} } del cliente", - "assign-new-device": "Asignar nuevo dispositivo", - "make-public-device-title": "¿Está seguro de que desea hacer el dispositivo '{{deviceName}}' público?", - "make-public-device-text": "Después de la confirmación, el dispositivo y todos sus datos se harán públicos y accesibles por otros.", - "make-private-device-title": "¿Está seguro de que desea hacer el dispositivo '{{deviceName}}' privado?", - "make-private-device-text": "Después de la confirmación, el dispositivo y todos sus datos se harán privados y no serán accesibles para otros.", - "view-credentials": "Ver credenciales", - "delete-device-title": "¿Está seguro de que desea hacer el dispositivo '{{deviceName}}'?", - "delete-device-text": "¡Cuidado! Después de la confirmación, el dispositivo y todos sus datos relacionados serán irrecuperables.", - "delete-devices-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 dispositivo} other {# dispositivos} }?", - "delete-devices-action-title": "Eliminar { count, plural, 1 {1 dispositivo} other {# dispositivos} }", - "delete-devices-text": "¡Cuidado! Después de la confirmación, todos los dispositivos seleccionados serán eliminados y todos los datos relacionados serán irrecuperables.", - "unassign-device-title": "¿Está seguro de que desea anular la asignación del dispositivo '{{deviceName}}'?", - "unassign-device-text": "Después de la confirmación, se anulará asignación del dispositivo y no será accesible por el cliente.", - "unassign-device": "Desasignar dispositivo", - "unassign-devices-title": "¿Está seguro de que desea desasignar { count, plural, 1 {1 dispositivo } other {# dispositivos} }?", - "unassign-devices-text": "Después de la confirmación, se anulará asignación de todos los dispositivos seleccionados y no serán accesibles por el cliente.", - "device-credentials": "Credenciales del dispositivo", - "credentials-type": "Tipo de credenciales", - "access-token": "Token de acceso", - "access-token-required": "Token de acceso es requerido.", - "access-token-invalid": "La longitud del token de acceso debe ser de 1 a 20 caracteres.", - "rsa-key": "Clave pública RSA", - "rsa-key-required": "Clave pública RSA es requerida.", - "secret": "Secreto", - "secret-required": "Secreto es requerido.", - "device-type": "Tipo de dispositivo", - "device-type-required": "Tipo de dispositivo es requerido.", - "select-device-type": "Seleccionar tipo de dispositivo", - "enter-device-type": "Teclee tipo de dispositivo", - "any-device": "Algún dispositivo", - "no-device-types-matching": "Tipos de dispositivos que coincidan con '{{entitySubtype}}' no fueron encontrados.", - "device-type-list-empty": "No se seleccionaron tipos de dispositivos.", - "device-types": "Tipo de dispositivos", - "name": "Nombre", - "name-required": "El nombre es requerido.", - "label": "Etiqueta", - "description": "Descripción", - "events": "Eventos", - "details": "Detalles", - "copyId": "Copiar ID del dispositivo", - "copyAccessToken": "Copiar token de acceso", - "idCopiedMessage": "ID del dispositivo ha sido copiada al portapapeles", - "accessTokenCopiedMessage": "Token de acceso al dispositivo ha sido copiado al portapapeles", - "assignedToCustomer": "Asignado al cliente", - "unable-delete-device-alias-title": "No se puede eliminar el alias del dispositivo", - "unable-delete-device-alias-text": "El alias del dispositivo '{{deviceAlias}}' no puede ser eliminado porque es usado por los siguientes widget(s):
    {{widgetsList}}", - "is-gateway": "Es puerta de entrada", - "public": "Público", - "device-public": "El dispositivo es público", - "select-device": "Seleccionar dispositivo", - "device-file": "Archivo de dispositivo", - "import": "Importar dispositivo" - }, - "dialog": { - "close": "Cerrar diálogo" - }, - "direction": { - "column": "Columna", - "row": "Fila" - }, - "error": { - "unable-to-connect": "¡No se puede conectar al servidor! Por favor, revise su conexión a Internet.", - "unhandled-error-code": "Código de error no controlado: {{errorCode}}", - "unknown-error": "Error desconocido" - }, - "entity": { - "entity": "Entidad", - "entities": "Entidades", - "aliases": "Alias de las entidades", - "entity-alias": "Alias de la entidad", - "unable-delete-entity-alias-title": "No se puede borrar alias de la entidad", - "unable-delete-entity-alias-text": "Alias de la entidad '{{entityAlias}}' no se puede eliminar porque es usado por los siguientes widget(s):
    {{widgetsList}}", - "duplicate-alias-error": "Alias duplicado fue encontrado '{{alias}}'.
    Alias de las entidades deben ser únicos dentro del panel.", - "missing-entity-filter-error": "Falta filtro para el alias '{{alias}}'.", - "configure-alias": "Configurar '{{alias}}' alias", - "alias": "Alias", - "alias-required": "Alias de la entidad es requerida.", - "remove-alias": "Eliminar alias de la entidad", - "add-alias": "Agregar alias de la entidad", - "entity-list": "Lista de entidades", - "entity-type": "Tipo de entidad", - "entity-types": "Tipos de entidades", - "entity-type-list": "Lista de tipos de entidades", - "any-entity": "Alguna entidad", - "enter-entity-type": "Teclee tipo de entidad", - "no-entities-matching": "No se encontraron entidades que coincidan con '{{entity}}'.", - "no-entity-types-matching": "No se encontraron tipos de entidades que coincidan con '{{entityType}}'.", - "name-starts-with": "El nombre comienza con", - "use-entity-name-filter": "Utilizar filtro", - "entity-list-empty": "Entidades no seleccionadas.", - "entity-type-list-empty": "Tipos de entidades no seleccionados.", - "entity-name-filter-required": "Filtro del nombre de la entidad es requerido.", - "entity-name-filter-no-entity-matched": "No se encontraron entidades que comienzan con '{{entity}}'.", - "all-subtypes": "Todas", - "select-entities": "Seleccionar entidades", - "no-aliases-found": "No se encontraron alias.", - "no-alias-matching": "'{{alias}}' no encontrado.", - "create-new-alias": "¡Crear uno nuevo!", - "key": "Clave", - "key-name": "Nombre de clave", - "no-keys-found": "No se encontraron claves.", - "no-key-matching": "'{{key}}' no encontrada.", - "create-new-key": "¡Crear una nueva!", - "type": "Tipo", - "type-required": "Tipo de entidad es requerido.", - "type-device": "Dispositivo", - "type-devices": "Dispositivos", - "list-of-devices": "{ count, plural, 1 {Un dispositivo} other {Lista de # dispositivos} }", - "device-name-starts-with": "Dispositivos cuyos nombres comienzan con '{{prefix}}'", - "type-asset": "Activo", - "type-assets": "Activos", - "list-of-assets": "{ count, plural, 1 {Un activo} other {Lista de # activos} }", - "asset-name-starts-with": "Activos cuyos nombres comienzan con '{{prefix}}'", - "type-entity-view": "Vista de entidad", - "type-entity-views": "Vistas de entidad", - "list-of-entity-views": "{ count, plural, 1 {Una vista de entidad} other {Lista de # vistas de entidad} }", - "entity-view-name-starts-with": "Vistas de entidad cuyos nombres que comienzan con '{{prefix}}'", - "type-rule": "Regla", - "type-rules": "Reglas", - "list-of-rules": "{ count, plural, 1 {Una regla} other {Lista de # reglas} }", - "rule-name-starts-with": "Reglas cuyos nombres comienzan con '{{prefix}}'", - "type-plugin": "Complemento", - "type-plugins": "Complementos", - "list-of-plugins": "{ count, plural, 1 {Un complemento} other {Lista de # complementos} }", - "plugin-name-starts-with": "Complementos cuyos nombres comienzan con '{{prefix}}'", - "type-tenant": "Organización", - "type-tenants": "Organizaciones", - "list-of-tenants": "{ count, plural, 1 {Una organización} other {Lista de # organizaciones} }", - "tenant-name-starts-with": "Organizaciones cuyos nombres comienzan con '{{prefix}}'", - "type-customer": "Cliente", - "type-customers": "Clientes", - "list-of-customers": "{ count, plural, 1 {Un cliente} other {Lista de # clientes} }", - "customer-name-starts-with": "Clientes cuyos nombres comienzan con '{{prefix}}'", - "type-user": "Usuario", - "type-users": "Usuarios", - "list-of-users": "{ count, plural, 1 {Un usuario} other {Lista de # usuarios} }", - "user-name-starts-with": "Usuarios cuyos nombres comienzan con '{{prefix}}'", - "type-dashboard": "Panel", - "type-dashboards": "Paneles", - "list-of-dashboards": "{ count, plural, 1 {One dashboard} other {List of # dashboards} }", - "dashboard-name-starts-with": "Paneles cuyos nombres comienzan con '{{prefix}}'", - "type-alarm": "Alarma", - "type-alarms": "Alarmas", - "list-of-alarms": "{ count, plural, 1 {Una alarma} other {Lista de # alarmas} }", - "alarm-name-starts-with": "Alarmas cuyos nombres comienzan con '{{prefix}}'", - "type-rulechain": "Cadena de reglas", - "type-rulechains": "Cadenas de reglas", - "list-of-rulechains": "{ count, plural, 1 {Una cadena de reglas} other {Lista de # cadenas de reglas} }", - "rulechain-name-starts-with": "Cadenas de reglas cuyos nombres comienzan con '{{prefix}}'", - "type-rulenode": "Nodo de reglas", - "type-rulenodes": "Nodos de reglas", - "list-of-rulenodes": "{ count, plural, 1 {Un nodo de reglas} other {Lista de # nodos de reglas} }", - "rulenode-name-starts-with": "Nodos de reglas cuyos nombres comienzan con '{{prefix}}'", - "type-current-customer": "Cliente Actual", - "search": "Buscar entidades", - "selected-entities": "{ count, plural, 1 {1 entidad} other {# entidades} } seleccionadas", - "entity-name": "Nombre de la entidad", - "details": "Detalles de la entidad", - "no-entities-prompt": "Entidades no encontradas", - "no-data": "No hay datos para mostrar", - "columns-to-display": "Columnas a mostrar" - }, - "entity-field": { - "created-time": "Tiempo de creación", - "name": "Nombre", - "type": "Tipo", - "first-name": "Nombre", - "last-name": "Apellido", - "email": "Correo electrónico", - "title": "Título", - "country": "País", - "state": "Estado", - "city": "Ciudad", - "address": "Dirección", - "address2": "Dirección 2", - "zip": "Código postal", - "phone": "Teléfono" - }, - "entity-view": { - "entity-view": "Vista de entidad", - "entity-view-required": "Vista de entidad es requerido.", - "entity-views": "Vistas de entidad", - "management": "Gestión de vistas de entidad", - "view-entity-views": "Ver vista de entidad", - "entity-view-alias": "Alias de vista de entidad", - "aliases": "Alias de vista de entidad", - "no-alias-matching": "'{{alias}}' no encontrado.", - "no-aliases-found": "No se encontraron alias.", - "no-key-matching": "'{{key}}' no encontrado.", - "no-keys-found": "No se encontraron claves.", - "create-new-alias": "¡Crear un nuevo!", - "create-new-key": "¡Crear una nueva!", - "duplicate-alias-error": "Alias duplicado'{{alias}}'.
    Los alias de Entity View deben ser únicos en el panel.", - "configure-alias": "Configurar alias '{{alias}}'", - "no-entity-views-matching": "No se encontraron vistas que coincidan con '{{entity}}'.", - "alias": "Alias", - "alias-required": "Alias de vista de entidad es requerido.", - "remove-alias": "Borrar alias de la vista de entidad", - "add-alias": "Añadir alias a la vista de entidad", - "name-starts-with": "Nombre de vista de entidad comienza con", - "entity-view-list": "Lista de vistas de entidad", - "use-entity-view-name-filter": "Usar el filtro", - "entity-view-list-empty": "No hay vistas de entidad seleccionadas.", - "entity-view-name-filter-required": "Nombre del filtro de vista de entidad es requerido.", - "entity-view-name-filter-no-entity-view-matched": "No se encontraron vistas de entidad que comiencen con '{{entityView}}'.", - "add": "Añadir vista de entidad", - "assign-to-customer": "Asignar a cliente", - "assign-entity-view-to-customer": "Asignar vista de entidad a cliente", - "assign-entity-view-to-customer-text": "Por favor, seleccione las vistas de entidad para asignar al cliente", - "no-entity-views-text": "No se encontraron vistas de entidad", - "assign-to-customer-text": "Por favor, seleccione el cliente para asignar la vista de entidad", - "entity-view-details": "Detalles de la vista de entidad", - "add-entity-view-text": "Añadir nueva vista de entidad", - "delete": "Borrar vista de entidad", - "assign-entity-views": "Asignar vistas de entidad", - "assign-entity-views-text": "Asignar { count, plural, 1 {1 vista de entidad} other {# vistas de entidad} } a cliente", - "delete-entity-views": "Borrar vistas de entidad", - "unassign-from-customer": "Anular asignación a cliente", - "unassign-entity-views": "Anular asignación de vistas de entidad", - "unassign-entity-views-action-title": "Anular asignación { count, plural, 1 {1 vista de entidad} other {# vistas de entidad} } al cliente", - "assign-new-entity-view": "Asignar nueva vista de entidad", - "delete-entity-view-title": "¿Está seguro que quiere borrar la vista de entidad '{{entityViewName}}'?", - "delete-entity-view-text": "¡Cuidado! Después de la confirmación, la vista de la entidad y todos los datos relacionados serán irrecuperables.", - "delete-entity-views-title": "¿Está seguro que quiere borrar las vistas de entidad { count, plural, 1 {1 entityView} other {# entityViews} }?", - "delete-entity-views-action-title": "Borrar { count, plural, 1 {1 vista de entidad} other {# vistas de entidad} }", - "delete-entity-views-text": "¡Cuidado! Después de la confirmación, todas las vistas de entidades seleccionadas se eliminarán y todos los datos relacionados serán irrecuperables.", - "unassign-entity-view-title": "¿Está seguro que quiere anular la asignación de la vista de entidad '{{entityViewName}}'?", - "unassign-entity-view-text": "Después de la confirmación, la vista de la entidad quedará sin asignar y el cliente no podrá acceder a ella.", - "unassign-entity-view": "Anular asignación de la vista de entidad", - "unassign-entity-views-title": "¿Está seguro que quiere anular la asignación de { count, plural, 1 {1 vista de entidad} other {# vistas de entidad} }?", - "unassign-entity-views-text": "Después de la confirmación, todas las vistas de entidades seleccionadas quedarán sin asignar y el cliente no podrá acceder a ellas.", - "entity-view-type": "Tipo de vista de entidad", - "entity-view-type-required": "Tipo de vista de entidad es requerido.", - "select-entity-view-type": "Seleccione el tipo de vista de entidad", - "enter-entity-view-type": "Teclee el tipo de vista de entidad", - "any-entity-view": "Cualquier vista de entidad", - "no-entity-view-types-matching": "No se encontraron tipos de vista de entidad que coincidan con '{{entitySubtype}}'.", - "entity-view-type-list-empty": "No hay tipos de vista de entidad seleccionados.", - "entity-view-types": "Tipos de vista de entidad", - "name": "Nombre", - "name-required": "Nombre es requerido.", - "description": "Descripción", - "events": "Eventos", - "details": "Detalles", - "copyId": "Copiar el Id de la vista de entidad", - "assignedToCustomer": "Asignado a cliente", - "unable-entity-view-device-alias-title": "No se puede eliminar el alias de vista de entidad", - "unable-entity-view-device-alias-text": "El alias del dispositivo '{{entityViewAlias}}' no se puede borrar porque está siendo usado por el widget(s):
    {{widgetsList}}", - "select-entity-view": "Seleccionar vista de entidad", - "make-public": "Hacer pública la vista de entidad", - "make-private": "Hacer que la vista de entidad sea privada", - "start-date": "Fecha de inicio", - "start-ts": "Tiempo de inicio", - "end-date": "Fecha de finalización", - "end-ts": "Tiempo de finalización", - "date-limits": "Limites de fecha", - "client-attributes": "Atributos de cliente", - "shared-attributes": "Atributos compartidos", - "server-attributes": "Atributos de servidor", - "timeseries": "Series temporales", - "client-attributes-placeholder": "Atributos de cliente", - "shared-attributes-placeholder": "Atributos compartidos", - "server-attributes-placeholder": "Atributos de servidor", - "timeseries-placeholder": "Series temporales", - "target-entity": "Entidad objetivo", - "attributes-propagation": "Propagación de atributos", - "attributes-propagation-hint": "La vista de entidad copiará automáticamente los atributos especificados de la entidad de destino cada vez que guarde o actualice esta vista de entidad. Por razones de rendimiento, los atributos de entidad objetivo no se propagan a la vista de entidad en cada cambio de atributo. Puede habilitar la propagación automática configurando el nodo de la regla \"copiar a la vista\" en su cadena de reglas y vincular los mensajes \"Atributos de la publicación\" y \"Atributos actualizados\" al nuevo nodo de la regla.", - "timeseries-data": "Datos de series temporales", - "timeseries-data-hint": "Configure las claves de los datos de las series temporales de la entidad de destino que serán accesibles para la vista de la entidad. Los datos de esta serie temporal son de solo lectura.", - "make-public-entity-view-title": "¿Está seguro de que desea que la vista de entidad '{{entityViewName}}' sea pública?", - "make-public-entity-view-text": "Después de la confirmación, la vista de la entidad y todos sus datos se harán públicos y accesibles para otros.", - "make-private-entity-view-title": "¿Está seguro de que desea que la vista de entidad '{{entityViewName}}' sea privada?", - "make-private-entity-view-text": "Después de la confirmación, la vista de la entidad y todos sus datos se harán privados y no serán accesibles para otros." - }, - "event": { - "event-type": "Tipo de evento", - "type-error": "Error", - "type-lc-event": "Ciclo de vida del evento", - "type-stats": "Estadísticas", - "type-debug-rule-node": "Depurar", - "type-debug-rule-chain": "Depurar", - "no-events-prompt": "No se encontraron eventos", - "error": "Error", - "alarm": "Alarma", - "event-time": "Tiempo del evento", - "server": "Servidor", - "body": "Cuerpo", - "method": "Método", - "type": "Tipo", - "entity": "Entidad", - "message-id": "ID del mensaje", - "message-type": "Tipo de mensaje", - "data-type": "Tipo de datos", - "relation-type": "Tipo de relación", - "metadata": "Metadatos", - "data": "Datos", - "event": "Evento", - "status": "Estado", - "success": "Correcto", - "failed": "Erróneo", - "messages-processed": "Mensajes procesados", - "errors-occurred": "Errores ocurridos" - }, - "extension": { - "extensions": "Extensiones", - "selected-extensions": "{ count, plural, 1 {1 extensión} other {# extensiones} } seleccionadas", - "type": "Tipo", - "key": "Clave", - "value": "Valor", - "id": "ID", - "extension-id": "ID de extensión", - "extension-type": "Tipo de extensión", - "transformer-json": "JSON *", - "unique-id-required": "Ya existe ID de extensión actual.", - "delete": "Eliminar extensión", - "add": "Agregar extensión", - "edit": "Editar extensión", - "delete-extension-title": "¿Está seguro de que desea eliminar la extensión '{{extensionId}}'?", - "delete-extension-text": "¡Cuidado! Después de la confirmación, la extensión y todos los datos relacionados serán irrecuperables.", - "delete-extensions-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 extensión} other {# extensiones} }?", - "delete-extensions-text": "¡Cuidado! Después de la confirmación, se eliminarán todas las extensiones seleccionadas.", - "converters": "Conversores", - "converter-id": "ID del conversor", - "configuration": "Configuración", - "converter-configurations": "Configuraciones del conversor", - "token": "Token de seguridad", - "add-converter": "Agregar conversor", - "add-config": "Agregar configuración del conversor", - "device-name-expression": "Expresión del nombre del dispositivo", - "device-type-expression": "Expresión del tipo del dispositivo", - "custom": "Personalizado", - "to-double": "Para duplicar", - "transformer": "Transformador", - "json-required": "Transformador json es requerido.", - "json-parse": "No se puede analizar el transformador json.", - "attributes": "Atributos", - "add-attribute": "Agregar atributos", - "add-map": "Agregar elemento de mapeo", - "timeseries": "Series temporales", - "add-timeseries": "Agregar series temporales", - "field-required": "Campo es requerido", - "brokers": "Agentes", - "add-broker": "Agregar agente", - "host": "Host", - "port": "Puerto", - "port-range": "El puerto debe estar en un rango desde 1 hasta 65535.", - "ssl": "SSL", - "credentials": "Credenciales", - "username": "Nombre de usuario", - "password": "Contraseña", - "retry-interval": "Intervalo de reintento en milisegundos", - "anonymous": "Anónimo", - "basic": "Básico", - "pem": "PEM", - "ca-cert": "Archivo de certificado CA *", - "private-key": "Archivo de clave privado *", - "cert": "Archivo de certificado *", - "no-file": "Ningún archivo seleccionado.", - "drop-file": "Colocar un archivo o hacer clic para seleccionar un archivo para cargar.", - "mapping": "Mapeo", - "topic-filter": "Filtro de tema", - "converter-type": "Tipo de conversor", - "converter-json": "Json", - "json-name-expression": "Expresión json para nombre del dispositivo", - "topic-name-expression": "Expresión temática para nombre del dispositivo", - "json-type-expression": "Expresión json para tipo de dispositivo", - "topic-type-expression": "Expresión temática para tipo de dispositivo", - "attribute-key-expression": "Expresión para clave de atributo", - "attr-json-key-expression": "Expresión json para clave de atributo", - "attr-topic-key-expression": "Expresión temática para clave de atributo", - "request-id-expression": "Expresión para solicitud de ID", - "request-id-json-expression": "Expresión json para solicitud de ID", - "request-id-topic-expression": "Expresión temática para solicitud de ID", - "response-topic-expression": "Expresión temática para respuesta", - "value-expression": "Expresión para valor", - "topic": "Tema", - "timeout": "Tiempo de espera en milisegundos", - "converter-json-required": "Conversor json es requerido.", - "converter-json-parse": "No se puede analizar el conversor json.", - "filter-expression": "Expresión para filtro", - "connect-requests": "Solicitudes de conexión", - "add-connect-request": "Agregar solicitudes de conexión", - "disconnect-requests": "Solicitudes de desconexión", - "add-disconnect-request": "Agregar solicitud de desconexión", - "attribute-requests": "Solicitudes de atributo", - "add-attribute-request": "Agregar solicitudes de atributo", - "attribute-updates": "Actualizaciones de atributo", - "add-attribute-update": "Agregar actualizaciones de atributo", - "server-side-rpc": "RPC lado servidor", - "add-server-side-rpc-request": "Agregar solicitud RPC lado servidor", - "device-name-filter": "Filtro de nombre de dispositivo", - "attribute-filter": "Filtro de atributo", - "method-filter": "Filtro de método", - "request-topic-expression": "Expresión temática para solicitud", - "response-timeout": "Tiempo de espera de respuesta en milisegundos", - "topic-expression": "Expresión temática", - "client-scope": "Alcance del cliente", - "add-device": "Agregar dispositivo", - "opc-server": "Servidores", - "opc-add-server": "Agregar servidor", - "opc-add-server-prompt": "Por favor agregar servidor", - "opc-application-name": "Nombre de aplicación", - "opc-application-uri": "Aplicación URI", - "opc-scan-period-in-seconds": "Período de exploración en segundos", - "opc-security": "Seguridad", - "opc-identity": "Identidad", - "opc-keystore": "Repositorio", - "opc-type": "Tipo", - "opc-keystore-type": "Tipo", - "opc-keystore-location": "Ubicación *", - "opc-keystore-password": "Contraseña", - "opc-keystore-alias": "Alias", - "opc-keystore-key-password": "Clave de contraseña", - "opc-device-node-pattern": "Patrón de nodo de dispositivo", - "opc-device-name-pattern": "Patrón de nombre de dispositivo", - "modbus-server": "Servidores/esclavos", - "modbus-add-server": "Agregar servidor/esclavo", - "modbus-add-server-prompt": "Por favor agregar servidor/esclavo", - "modbus-transport": "Transporte", - "modbus-tcp-reconnect": "Reconexión automática", - "modbus-rtu-over-tcp": "RTU sobre TCP", - "modbus-port-name": "Nombre del puerto serial", - "modbus-encoding": "Codificación", - "modbus-parity": "Paridad", - "modbus-baudrate": "Tasa de baudios", - "modbus-databits": "Bits de datos", - "modbus-stopbits": "Bits de parada", - "modbus-databits-range": "Bits de datos deben estar en un rango entre 7 y 8.", - "modbus-stopbits-range": "Bits de parada deben estar en un rango entre 1 a 2.", - "modbus-unit-id": "ID de unidad", - "modbus-unit-id-range": "ID de unidad debe estar en un rango entre 1 a 247.", - "modbus-device-name": "Nombre del dispositivo", - "modbus-poll-period": "Período de sondeo (ms)", - "modbus-attributes-poll-period": "Atributos del período de sondeo (ms)", - "modbus-timeseries-poll-period": "Período de sondeo de las series temporales (ms)", - "modbus-poll-period-range": "El período de sondeo debe ser una valor positivo.", - "modbus-tag": "Etiqueta", - "modbus-function": "Función", - "modbus-register-address": "Dirección del registro", - "modbus-register-address-range": "Dirección del registro debe estar en un rango entre 0 y 65535.", - "modbus-register-bit-index": "Índice de bit", - "modbus-register-bit-index-range": "Índice de bit debe estar en un rango entre 0 y 15.", - "modbus-register-count": "Contador del registro", - "modbus-register-count-range": "Contador del registro debe ser un valor positivo.", - "modbus-byte-order": "Orden del byte", - "sync": { - "status": "Estado", - "sync": "Sincronización", - "not-sync": "No sincronización", - "last-sync-time": "Ultima hora de sincronización", - "not-available": "No disponible" - }, - "export-extensions-configuration": "Exportar configuración de extensiones", - "import-extensions-configuration": "Importar configuración de extensiones", - "import-extensions": "Importar extensiones", - "import-extension": "Importar extensión", - "export-extension": "Exportar extensión", - "file": "Archivo de extensiones", - "invalid-file-error": "Archivo de extensión no válido" - }, - "fullscreen": { - "expand": "Expandir a pantalla completa", - "exit": "Salir de pantalla completa", - "toggle": "Alternar el modo de pantalla completa", - "fullscreen": "Pantalla completa" - }, - "function": { - "function": "Función" - }, - "grid": { - "delete-item-title": "¿Está seguro de que desea eliminar este ítem?", - "delete-item-text": "¡Cuidado! Después de la confirmación, este ítem y todos los datos relacionados serán irrecuperables.", - "delete-items-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 ítem} other {# ítems} }?", - "delete-items-action-title": "Eliminar { count, plural, 1 {1 ítem} other {# ítems} }", - "delete-items-text": "¡Cuidado! Después de la confirmación se eliminarán todos los ítems seleccionados y todos los datos relacionados serán irrecuperables.", - "add-item-text": "Agregar nuevo ítem", - "no-items-text": "No se encontraron ítems", - "item-details": "Detalles del ítem", - "delete-item": "Eliminar ítem", - "delete-items": "Eliminar ítems", - "scroll-to-top": "Desplazar al inicio" - }, - "help": { - "goto-help-page": "Ir a la página de ayuda" - }, - "home": { - "home": "Página principal", - "profile": "Perfil", - "logout": "Cerrar sesión", - "menu": "Menú", - "avatar": "Avatar", - "open-user-menu": "Abrir menú de usuario" - }, - "import": { - "no-file": "Ningún archivo seleccionado", - "drop-file": "Suelte un archivo JSON o haga clic para seleccionar un archivo para cargar.", - "column-value": "Valor", - "column-title": "Título", - "column-example": "Datos de ejemplo", - "drop-file-csv": "Suelte un archivo CSV o haga clic para seleccionar un archivo para cargar.", - "column-key": "Clave de atributo/telemetría", - "csv-delimiter": "Delimitador CSV", - "csv-first-line-header": "La primera línea contiene nombres de columna.", - "csv-update-data": "Actualizar atributos/telemetría", - "import-csv-number-columns-error": "Un archivo debe contener al menos dos columnas", - "import-csv-invalid-format-error": "Formato de archivo inválido. Línea: '{{line}}'", - "column-type": { - "access-token": "Token de acceso", - "client-attribute": "Atributo de cliente", - "column-type": "Tipo de columna", - "entity-field": "Campo de entidad", - "name": "Nombre", - "server-attribute": "Atributo de servidor", - "shared-attribute": "Atributo compartido", - "timeseries": "Series temporales", - "type": "Tipo" - }, - "stepper-text": { - "select-file": "Seleccione un archivo", - "configuration": "Importar configuración", - "column-type": "Seleccionar tipo de columnas", - "creat-entities": "Creando nuevas entidades", - "done": "Hecho" - }, - "message": { - "create-entities": "Se crearon{{count}} nuevas entidades correctamente.", - "update-entities": "{{count}} entidades se actualizaron correctamente.", - "error-entities": "Se produjo un error al crear {{count}} entidades." - } - }, - "item": { - "selected": "Seleccionado" - }, - "js-func": { - "no-return-error": "La función debe devolver el valor!", - "return-type-mismatch": "La función debe devolver el valor de '{{type}}' type!", - "tidy": "Formatear" - }, - "key-val": { - "key": "Clave", - "value": "Valor", - "remove-entry": "Eliminar entrada", - "add-entry": "Agregar entrada", - "no-data": "Ninguna entrada" - }, - "layout": { - "layout": "Diseño", - "manage": "Gestionar diseños", - "settings": "Configuración de diseño", - "color": "Color", - "main": "Principal", - "right": "Derecha", - "select": "Seleccionar diseño objetivo" - }, - "legend": { - "direction": "Dirección de la leyenda", - "position": "Posición de la leyenda", - "show-max": "Mostrar valor máximo", - "show-min": "Mostrar valor mínimo", - "show-avg": "Mostrar valor promedio", - "show-total": "Mostrar valor total", - "settings": "Configuración de la leyenda", - "min": "mínimo", - "max": "máximo", - "avg": "promedio", - "total": "total" - }, - "login": { - "login": "Iniciar sesión", - "request-password-reset": "Restablecer contraseña", - "reset-password": "Restablecer contraseña", - "create-password": "Crear contraseña", - "passwords-mismatch-error": "¡Las contraseñas introducidas deben ser iguales!", - "password-again": "Repita la contraseña de nuevo", - "sign-in": "Por favor, inicie sesión", - "username": "Nombre de usuario (correo electrónico)", - "remember-me": "Recordarme", - "forgot-password": "¿Olvidó la contraseña?", - "password-reset": "Restablecer contraseña", - "new-password": "Nueva contraseña", - "new-password-again": "Repita la nueva contraseña", - "password-link-sent-message": "¡El enlace para el restablecer la contraseña fue enviado correctamente!", - "email": "Correo electrónico", - "login-with": "Iniciar sesión con {{name}}", - "or": "o" - }, - "position": { - "top": "Superior", - "bottom": "Inferior", - "left": "Izquierda", - "right": "Derecha" - }, - "profile": { - "profile": "Perfil", - "last-login-time": "Último acceso", - "change-password": "Cambiar contraseña", - "current-password": "Contraseña actual" - }, - "relation": { - "relations": "Relaciones", - "direction": "Dirección", - "search-direction": { - "FROM": "Desde", - "TO": "Hacia" - }, - "direction-type": { - "FROM": "desde", - "TO": "hacia" - }, - "from-relations": "Relaciones salientes", - "to-relations": "Relaciones entrantes", - "selected-relations": "{ count, plural, 1 {1 relación} other {# relaciones} } selecciondas", - "type": "Tipo", - "to-entity-type": "Hacia tipo de entidad", - "to-entity-name": "Hacia nombre de entidad", - "from-entity-type": "Desde tipo de entidad", - "from-entity-name": "Desde nombre de entidad", - "to-entity": "Hacia entidad", - "from-entity": "Desde entidad", - "delete": "Eliminar relación", - "relation-type": "Tipo de relación", - "relation-type-required": "Tipo de relación es requerido.", - "any-relation-type": "Algún tipo", - "add": "Agregar relación", - "edit": "Editar relación", - "delete-to-relation-title": "¿Está seguro de que desea eliminar la relación hacia la entidad '{{entityName}}'?", - "delete-to-relation-text": "¡Cuidado! Después de la confirmación, la entidad '{{entityName}}' no estará relacionada desde la entidad actual.", - "delete-to-relations-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 relación} other {# relaciones} }?", - "delete-to-relations-text": "¡Cuidado! Después de la confirmación, se eliminarán todas las relaciones seleccionadas y las entidades correspondientes no estarán relacionadas desde la entidad actual.", - "delete-from-relation-title": "¿Está seguro de que desea eliminar la relación desde la entidad '{{entityName}}'?", - "delete-from-relation-text": "¡Cuidado! Después de la confirmación, la entidad actual no será relacionada desde la entidad '{{entityName}}'.", - "delete-from-relations-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 relación} other {# relaciones} }?", - "delete-from-relations-text": "¡Cuidado! Después de la confirmación, se eliminarán todas las relaciones seleccionadas y la entidad actual no será relacionada desde las correspondientes entidades.", - "remove-relation-filter": "Eliminar filtro de relación", - "add-relation-filter": "Agregar filtro de relación", - "any-relation": "Alguna relación", - "relation-filters": "Filtros de relación", - "additional-info": "Información adicional (JSON)", - "invalid-additional-info": "No se puede analizar información adicional json." - }, - "rulechain": { - "rulechain": "Cadena de reglas", - "rulechains": "Cadenas de reglas", - "root": "Raíz", - "delete": "Eliminar cadena de reglas", - "name": "Nombre", - "name-required": "El nombre es requerido.", - "description": "Descripción", - "add": "Agregar cadena de reglas", - "set-root": "Hacer la cadena de reglas raíz", - "set-root-rulechain-title": "¿Está seguro de que desea hacer la cadena de reglas '{{ruleChainName}}' root?", - "set-root-rulechain-text": "Después de la confirmación, la cadena de reglas se volverá raíz y manejará todos los mensajes de transporte entrantes.", - "delete-rulechain-title": "¿Está seguro de que desea eliminar la cadena de reglas '{{ruleChainName}}'?", - "delete-rulechain-text": "¡Cuidado! Después de la confirmación, la cadena de reglas y todos los datos relacionados serán irrecuperables.", - "delete-rulechains-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 cadena de reglas} other {# cadenas de reglas} }?", - "delete-rulechains-action-title": "Eliminar { count, plural, 1 {1 cadena de reglas} other {# cadenas de reglas} }", - "delete-rulechains-text": "¡Cuidado! Después de la confirmación se eliminarán todas las cadenas de reglas seleccionadas y todos los datos relacionados serán irrecuperables.", - "add-rulechain-text": "Agregar nueva cadena de reglas", - "no-rulechains-text": "Cadenas de reglas no encontradas", - "rulechain-details": "Detalles de la cadena de reglas", - "details": "Detalles", - "events": "Eventos", - "system": "Sistema", - "import": "Importar cadena de reglas", - "export": "Exportar cadena de reglas", - "export-failed-error": "No se puede exportar la cadena de reglas: {{error}}", - "create-new-rulechain": "Crear nueva cadena de reglas", - "rulechain-file": "Archivo de la cadena de reglas", - "invalid-rulechain-file-error": "No se puede importar la cadena de reglas: Estructura de datos de la cadena de reglas inválida.", - "copyId": "Copiar ID de la cadena de reglas", - "idCopiedMessage": "ID de la cadena de reglas ha sido copiada al portapapeles", - "select-rulechain": "Seleccionar cadena de reglas", - "no-rulechains-matching": "Cadenas de reglas que coincidan con '{{entity}}' no fueron encontradas.", - "rulechain-required": "Cadena de reglas es requerida", - "management": "Gestión de reglas", - "debug-mode": "Mode de depuración" - }, - "rulenode": { - "details": "Detalles", - "events": "Eventos", - "search": "Nodos de búsqueda", - "open-node-library": "Abrir librería de nodos", - "add": "Agregar nodo de reglas", - "name": "Nombre", - "name-required": "El nombre es requerido.", - "type": "Tipo", - "description": "Descripción", - "delete": "Eliminar nodo de reglas", - "select-all-objects": "Seleccionar todos los nodos y conexiones", - "deselect-all-objects": "Deshacer selección de todos los nodos y conexiones", - "delete-selected-objects": "Eliminar nodos y conexiones seleccionados", - "delete-selected": "Eliminar seleccionado", - "select-all": "Seleccionar todos", - "copy-selected": "Copiar seleccionado", - "deselect-all": "Deshace selección de todos", - "rulenode-details": "Detalles del nodo de reglas", - "debug-mode": "Modo de depuración", - "configuration": "Configuración", - "link": "Enlace", - "link-details": "Detalles del enlace del nodo de reglas", - "add-link": "Agregar enlace", - "link-label": "Etiqueta del enlace", - "link-label-required": "Etiqueta del enlace es requerida.", - "custom-link-label": "Etiqueta del enlace personalizada", - "custom-link-label-required": "Etiqueta del enlace personalizado es requerida.", - "link-labels": "Etiquetas del enlace", - "link-labels-required": "Etiquetas del enlace son requeridas.", - "no-link-labels-found": "No se encontraron etiquetas de enlaces", - "no-link-label-matching": "'{{label}}' no encontrada.", - "create-new-link-label": "Crear una nueva!", - "type-filter": "Filtro", - "type-filter-details": "Filtrar mensajes entrantes con las condiciones configuradas", - "type-enrichment": "Enriquecimiento", - "type-enrichment-details": "Agregar información adicional en mensajes de metadatos", - "type-transformation": "Transformación", - "type-transformation-details": "Cambiar carga útil del Mensaje y Metadatos", - "type-action": "Acción", - "type-action-details": "Ejecutar acción especial", - "type-external": "Externo", - "type-external-details": "Interactuar con sistemas externos", - "type-rule-chain": "Cadena de reglas", - "type-rule-chain-details": "Reenvía los mensajes entrantes a la cadena de reglas especificada", - "type-input": "Entrada", - "type-input-details": "Entrada lógica de la Cadena de Reglas, reenvíar los mensajes entrantes al siguiente nodo de regla relacionado.", - "type-unknown": "Desconocido", - "type-unknown-details": "Regla de nodo no resuelta", - "directive-is-not-loaded": "La directiva de configuración definida '{{directiveName}}' no está disponible.", - "ui-resources-load-error": "Error al cargar los recursos de configuración ui.", - "invalid-target-rulechain": "¡No se puede resolver la cadena de reglas objetivo!", - "test-script-function": "Probar función script", - "message": "Mensaje", - "message-type": "Tipo de mensaje", - "select-message-type": "Seleccionar tipo de mensaje", - "message-type-required": "Tipo de mensaje es requerido", - "metadata": "Metadatos", - "metadata-required": "La entradas de matadatos no pueden estar vacías.", - "output": "Salida", - "test": "Prueba", - "help": "Ayuda", - "reset-debug-mode": "Restablecer el modo de depuración en todos los nodos" - }, - "tenant": { - "tenant": "Organización", - "tenants": "Organizaciones", - "management": "Gestión de la organización", - "add": "Agregar organización", - "admins": "Administradores", - "manage-tenant-admins": "Gestionar administradores de la organización", - "delete": "Eliminar organización", - "add-tenant-text": "Agregar nueva organización", - "no-tenants-text": "Organizaciones no encontradas", - "tenant-details": "Detalles de la organización", - "delete-tenant-title": "¿Está seguro de que desea eliminar la organización '{{tenantTitle}}'?", - "delete-tenant-text": "¡Cuidado! Después de la confirmación, la organización y todos los datos relacionados serán irrecuperables.", - "delete-tenants-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 organización} other {# organizaciones} }?", - "delete-tenants-action-title": "Eliminar { count, plural, 1 {1 organización} other {# organizaciones} }", - "delete-tenants-text": "¡Cuidado! Después de la confirmación se eliminarán todas las organizaciones seleccionadas y todos los datos relacionados serán irrecuperables.", - "title": "Título", - "title-required": "Título es requerido.", - "description": "Descripción", - "details": "Detalles", - "events": "Eventos", - "copyId": "Copiar ID de la organización", - "idCopiedMessage": "ID de la organización ha sido copiado al portapapeles", - "select-tenant": "Seleccionar organización", - "no-tenants-matching": "No se encontraron organizaciones que coincidan con '{{entity}}'.", - "tenant-required": "Organización es requerida" - }, - "timeinterval": { - "seconds-interval": "{ seconds, plural, 1 {1 segundo} other {# segundos} }", - "minutes-interval": "{ minutes, plural, 1 {1 minuto} other {# minutos} }", - "hours-interval": "{ hours, plural, 1 {1 hora} other {# horas} }", - "days-interval": "{ days, plural, 1 {1 día} other {# días} }", - "days": "Días", - "hours": "Horas", - "minutes": "Minutos", - "seconds": "Segundos", - "advanced": "Avanzado" - }, - "timewindow": { - "days": "{ days, plural, 1 { día } other {# días } }", - "hours": "{ hours, plural, 0 { horas } 1 {1 hora } other {# horas } }", - "minutes": "{ minutes, plural, 0 { minutos } 1 {1 minuto } other {# minutos } }", - "seconds": "{ seconds, plural, 0 { segundos } 1 {1 segundo } other {# segundos } }", - "realtime": "Tiempo real", - "history": "Historia", - "last-prefix": "último(s)", - "period": "desde {{ startTime }} hasta {{ endTime }}", - "edit": "Editar ventana de tiempo", - "date-range": "Rango de fecha", - "last": "Último(s)", - "time-period": "Período de tiempo", - "hide": "Ocultar" - }, - "user": { - "user": "Usuario", - "users": "Usuarios", - "customer-users": "Usuarios cliente", - "tenant-admins": "Administradores de la Organización", - "sys-admin": "Administrador del sistema", - "tenant-admin": "Administrador de la organización", - "customer": "Cliente", - "anonymous": "Anónimo", - "add": "Agregar Usuario", - "delete": "Eliminar usuario", - "add-user-text": "Agregar nuevo usuario", - "no-users-text": "No se encontraron usuarios", - "user-details": "Detalles de usuario", - "delete-user-title": "¿Está seguro de que desea eliminar el usuario '{{userEmail}}'?", - "delete-user-text": "¡Cuidado! Después de la confirmación, el usuario y todos los datos relacionados serán irrecuperables.", - "delete-users-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 usuario} other {# usuarios} }?", - "delete-users-action-title": "Borrar { count, plural, 1 {1 usuario} other {# usuarios} }", - "delete-users-text": "¡Cuidado! Después de la confirmación se eliminarán todas los usuarios seleccionados y todos los datos relacionados serán irrecuperables.", - "activation-email-sent-message": "¡Correo electrónico de activación fue enviado correctamente!", - "resend-activation": "Reenviar activación", - "email": "Correo electrónico", - "email-required": "Correo electrónico es requerido.", - "invalid-email-format": "Formato de correo electrónico inválido.", - "first-name": "Nombre", - "last-name": "Apellido", - "description": "Descripción", - "default-dashboard": "Panel predeterminado", - "always-fullscreen": "Siempre pantalla completa", - "select-user": "Seleccionar usuario", - "no-users-matching": "Usuarios que coincidan con '{{entity}}' no fueron encontrados.", - "user-required": "Usuario es requerido", - "activation-method": "Método de activación", - "display-activation-link": "Mostrar enlace de activación", - "send-activation-mail": "Enviar correo electrónico de activación", - "activation-link": "Enlace de activación de usuario", - "activation-link-text": "Para activar el usuario, utilice el siguiente enlace de activación :", - "copy-activation-link": "Copiar enlace de activación", - "activation-link-copied-message": "El enlace de activación de usuario ha sido copiado al portapapeles", - "details": "Detalles", - "login-as-tenant-admin": "Iniciar sesión como Administrador de la Organización", - "login-as-customer-user": "Iniciar sesión como Usuario cliente", - "disable-account": "Deshabilitar cuenta de usuario", - "enable-account": "Habilitar cuenta de usuario", - "enable-account-message": "¡La cuenta de usuario se ha habilitado correctamente!", - "disable-account-message": "¡La cuenta de usuario se deshabilitó correctamente!" - }, - "value": { - "type": "Tipo de valor", - "string": "Cadena de caracteres", - "string-value": "Valor de la cadena de caracteres", - "integer": "Entero", - "integer-value": "Valor entero", - "invalid-integer-value": "Valor de entero inválido", - "double": "Doble", - "double-value": "Valor doble", - "boolean": "Booleano", - "boolean-value": "Valor booleano", - "false": "Falso", - "true": "Verdadero", - "long": "Largo" - }, - "widget": { - "widget-library": "Librería de widgets", - "widget-bundle": "Paquete de widgets", - "select-widgets-bundle": "Seleccionar paquete de widgets", - "management": "Gestión de widget", - "editor": "Editor de Widget", - "widget-type-not-found": "Problema cargando configuración de widget.
    Probablemente el tipo de widget asociado fue eliminado.", - "widget-type-load-error": "El widget no fue cargado debido a los siguientes errores:", - "remove": "Eliminar widget", - "edit": "Editar widget", - "remove-widget-title": "¿Está seguro de que desea eliminar el widget '{{widgetTitle}}'?", - "remove-widget-text": "Después de la confirmación, el widget y todos los datos relacionados serán irrecuperables.", - "timeseries": "Series temporales", - "search-data": "Buscar datos", - "no-data-found": "No se encontraron datos", - "latest-values": "Últimos valores", - "rpc": "Widget de control", - "alarm": "Widget de alarma", - "static": "Widget estático", - "select-widget-type": "Seleccionar tipo de widget", - "missing-widget-title-error": "¡Título del widget debe ser especificado!", - "widget-saved": "Widget guardado", - "unable-to-save-widget-error": "¡No se puede guardar widget! ¡El widget tiene errores!", - "save": "Guardar widget", - "saveAs": "Guardar widget como", - "save-widget-type-as": "Guardar tipo de widget como", - "save-widget-type-as-text": "Por favor escriba el nuevo título del widget y/o seleccionar paquete de widgets objetivo", - "toggle-fullscreen": "Alternar pantalla completa", - "run": "Ejecutar widget", - "title": "Título del widget", - "title-required": "Título del widget es requerido.", - "type": "Tipo de widget", - "resources": "Recursos", - "resource-url": "URL JavaScript/CSS", - "remove-resource": "Eliminar recurso", - "add-resource": "Agregar recurso", - "html": "HTML", - "tidy": "Formatear", - "css": "CSS", - "settings-schema": "Esquema de configuración", - "datakey-settings-schema": "Esquema de configuración de clave de datos", - "javascript": "Javascript", - "js": "JS", - "remove-widget-type-title": "¿Está seguro de que desea eliminar el tipo de widget '{{widgetName}}'?", - "remove-widget-type-text": "Después de la confirmación, el tipo de widget y todos los datos relacionados serán irrecuperables.", - "remove-widget-type": "Eliminar tipo de widget", - "add-widget-type": "Agregar nuevo tipo de widget", - "widget-type-load-failed-error": "¡Error al cargar el tipo de widget!", - "widget-template-load-failed-error": "¡Error al cargar la plantilla del widget!", - "add": "Agregar widget", - "undo": "Deshacer cambios en el widget", - "export": "Exportar widget" - }, - "widget-action": { - "header-button": "Botón del encabezado del widget", - "open-dashboard-state": "Navegar a nuevo estado del panel", - "update-dashboard-state": "Actualizar estado vigente del panel", - "open-dashboard": "Navegar a otro panel", - "custom": "Acción personalizada", - "target-dashboard-state": "Estado del panel objetivo", - "target-dashboard-state-required": "Estado del panel objetivo es requerido", - "set-entity-from-widget": "Asignar entidad desde widget", - "target-dashboard": "Panel objetivo", - "open-right-layout": "Abrir diseño del panel derecho (vista móvil)" - }, - "widgets-bundle": { - "current": "Paquete actual", - "widgets-bundles": "Paquetes de widgets", - "add": "Agregar paquete de widgets", - "delete": "Eliminar paquete de widgets", - "title": "Título", - "title-required": "Título es requerido.", - "add-widgets-bundle-text": "Agregar nuevo paquete de widgets", - "no-widgets-bundles-text": "No se encontraron paquetes de widgets", - "empty": "Paquete de widgets está vacío", - "details": "Detalles", - "widgets-bundle-details": "Detalles del paquete de widgets", - "delete-widgets-bundle-title": "¿Está seguro de que desea eliminar el paquete de widgets '{{widgetsBundleTitle}}'?", - "delete-widgets-bundle-text": "¡Cuidado! Después de la confirmación, el paquete de widgets y todos los datos relacionados serán irrecuperables.", - "delete-widgets-bundles-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 paquete de widgets} other {# paquetes de widgets} }?", - "delete-widgets-bundles-action-title": "Eliminar { count, plural, 1 {1 paquete de widgets} other {# paquetes de widgets} }", - "delete-widgets-bundles-text": "¡Cuidado! Después de la confirmación se eliminarán todas los paquetes de widgets seleccionados y todos los datos relacionados serán irrecuperables.", - "no-widgets-bundles-matching": "Paquetes de widgets que coincidan con '{{widgetsBundle}}' no fueron encontrados.", - "widgets-bundle-required": "Paquete de widgets es requerido.", - "system": "Sistema", - "import": "Importar paquete de widgets", - "export": "Exportar paquete de widgets", - "export-failed-error": "No se puede exportar paquete de widgets: {{error}}", - "create-new-widgets-bundle": "Crear nuevo paquete de widgets", - "widgets-bundle-file": "Archivo de paquete de widgets", - "invalid-widgets-bundle-file-error": "No se puede importar paquete de widgets: Estructura de datos del paquete de widgets inválida." - }, - "widget-config": { - "data": "Datos", - "settings": "Configuración", - "advanced": "Avanzado", - "title": "Título", - "title-tooltip": "Tooltip Título", - "general-settings": "Configuración general", - "display-title": "Mostrar título", - "drop-shadow": "Colocar sombra", - "enable-fullscreen": "Habilitar pantalla completa", - "background-color": "Color de fondo", - "text-color": "Color del texto", - "padding": "Relleno", - "margin": "Margen", - "widget-style": "Estilo del widget", - "title-style": "Estilo del título", - "mobile-mode-settings": "Configuración del modo móvil", - "order": "Orden", - "height": "Altura", - "units": "Símbolo especial para mostrar junto al valor.", - "decimals": "Número de dígitos después del punto flotante", - "timewindow": "Ventana de tiempo", - "use-dashboard-timewindow": "Utilizar ventana de tiempo del panel", - "display-timewindow": "Mostrar ventana de tiempo", - "display-legend": "Mostrar leyenda", - "datasources": "Orígenes de datos", - "maximum-datasources": "Máximo { count, plural, 1 {1 origen de datos permitido.} other {# orígenes de datos permitidos} }", - "datasource-type": "Tipo", - "datasource-parameters": "Parámetros", - "remove-datasource": "Eliminar origen de datos", - "add-datasource": "Agregar origen de datos", - "target-device": "Dispositivo objetivo", - "alarm-source": "Origen de la alarma", - "actions": "Acciones", - "action": "Acción", - "add-action": "Agregar acción", - "search-actions": "Buscar acciones", - "action-source": "Origen de acción", - "action-source-required": "Fuente de acción es requerida.", - "action-name": "Nombre", - "action-name-required": "Nombre de acción es requerido.", - "action-name-not-unique": "Ya existe otra acción con el mismo nombre.
    El nombre de la acción debe ser único dentro del mismo orígen de acción.", - "action-icon": "Icono", - "action-type": "Tipo", - "action-type-required": "Tipo de acción es requerido.", - "edit-action": "Editar acción", - "delete-action": "Eliminar acción", - "delete-action-title": "Eliminar acción del widget", - "delete-action-text": "¿Está seguro de que desea eliminar la acción del widget con nombre '{{actionName}}'?", - "display-icon": "Mostrar icono del título", - "icon-color": "Color del icono", - "icon-size": "Tamaño del icono" - }, - "widget-type": { - "import": "Importar tipo de widget", - "export": "Exportar tipo de widget", - "export-failed-error": "No se puede exportar tipo de widget: {{error}}", - "create-new-widget-type": "Crear nuevo tipo de widget", - "widget-type-file": "Archivo de tipo de widget", - "invalid-widget-type-file-error": "No se puede importar tipo de widget: Estructura de datos del tipo de widget es inválida." - }, - "widgets": { - "date-range-navigator": { - "localizationMap": { - "Sun": "Dom.", - "Mon": "Lun.", - "Tue": "Mar.", - "Wed": "Mié", - "Thu": "Jue.", - "Fri": "Vie.", - "Sat": "Sáb.", - "Jan": "Ene.", - "Feb": "Feb.", - "Mar": "Mar.", - "Apr": "Abr.", - "May": "May.", - "Jun": "Jun.", - "Jul": "Jul.", - "Aug": "Ago.", - "Sep": "Sept.", - "Oct": "Oct.", - "Nov": "Nov.", - "Dec": "Dic.", - "January": "Enero", - "February": "Febrero", - "March": "Marzo", - "April": "Abril", - "June": "Junio", - "July": "Julio", - "August": "Agosto", - "September": "Septiembre", - "October": "Octubre", - "November": "Noviembre", - "December": "Diciembre", - "Custom Date Range": "Intervalo de fechas personalizado", - "Date Range Template": "Plantilla de rango de fechas", - "Today": "Hoy", - "Yesterday": "Ayer", - "This Week": "Esta semana", - "Last Week": "La semana pasada", - "This Month": "Este mes", - "Last Month": "El mes pasado", - "Year": "Año", - "This Year": "Este año", - "Last Year": "Último", - "Date picker": "Date picker", - "Hour": "Hora", - "Day": "Día", - "Week": "Semana", - "2 weeks": "2 Semanas", - "Month": "Mes", - "3 months": "3 Meses", - "6 months": "6 Meses", - "Custom interval": "Intervalo personalizado", - "Interval": "Intervalo", - "Step size": "Numero de pie", - "Ok": "De acuerdo" - } - }, - "input-widgets": { - "attribute-not-allowed": "El parámetro de atributo no se puede usar en este widget", - "date": "Fecha", - "discard-changes": "Descartar los cambios", - "entity-attribute-required": "Se requiere atributo de entidad", - "entity-timeseries-required": "Se requiere la serie de tiempo de la entidad", - "not-allowed-entity": "La entidad seleccionada no puede tener atributos compartidos", - "no-attribute-selected": "No se seleccionó ningún atributo", - "no-datakey-selected": "No se seleccionó ninguna clave de datos", - "no-entity-selected": "Ninguna entidad seleccionada", - "no-image": "Sin imágen", - "no-support-web-camera": "No hay cámara web compatible", - "no-timeseries-selected": "No hay series de tiempo seleccionadas", - "switch-attribute-value": "Cambiar el valor del atributo de entidad", - "switch-camera": "Cambiar de cámara", - "switch-timeseries-value": "Cambiar el valor de la serie de tiempo de la entidad", - "take-photo": "Tomar foto", - "time": "Tiempo", - "timeseries-not-allowed": "El parámetro Timeseries no se puede usar en este widget", - "update-failed": "Actualización fallida", - "update-successful": "Actualización exitosa", - "update-attribute": "Actualizar atributo", - "update-timeseries": "Actualizar series de tiempo", - "value": "Valor" - } - }, - "icon": { - "icon": "Icono", - "select-icon": "Seleccionar icono", - "material-icons": "Iconos de material design", - "show-all": "Mostrar todos los iconos" - }, - "custom": { - "widget-action": { - "action-cell-button": "Botón de acción de celda", - "row-click": "Clic en la fila", - "polygon-click": "Clic en la fila", - "marker-click": "Clic en el polígono", - "tooltip-tag-action": "Acción de etiqueta para globo de ayuda", - "node-selected": "Clic en el nodo seleccionado", - "element-click": "Clic en el elemento HTML", - "pie-slice-click": "Clic en la rebanada", - "row-double-click": "Doble clic en la fila" - } - }, - "language": { - "language": "Lenguaje" - } -} diff --git a/ui/src/app/locale/locale.constant-fa_IR.json b/ui/src/app/locale/locale.constant-fa_IR.json deleted file mode 100644 index b4ca2eaf4a..0000000000 --- a/ui/src/app/locale/locale.constant-fa_IR.json +++ /dev/null @@ -1,1630 +0,0 @@ -{ - "access": { - "unauthorized": "غير مجاز", - "unauthorized-access": "دسترسي غير مجاز", - "unauthorized-access-text": "!شما بايد وارد شويد تا به اين منبع دسترسي پيدا کنيد", - "access-forbidden": "دسترسي ممنوع", - "access-forbidden-text": "!اگر هنوز تمايل داريد به اينجا دسترسي پيدا کنيد، تلاش کنيد با نام کاربري ديگري وارد شويد
    .شما حق دسترسي به اينجا را نداريد", - "refresh-token-expired": "اين بخش، منقضي شده است", - "refresh-token-failed": "بازيابي اين بخش ممکن نيست" - }, - "action": { - "activate": "فعال سازي", - "suspend": "معلّق", - "save": "ذخيره سازي", - "saveAs": "ذخيره سازي در", - "cancel": "لغو", - "ok": "قبول", - "delete": "حذف", - "add": "اضافه", - "yes": "بله", - "no": "خير", - "update": "به روز کردن", - "remove": "حذف", - "search": "جستجو", - "clear-search": "پاک کردن جستجو", - "assign": "تخصيص", - "unassign": "لغو تخصيص", - "share": "به اشتراک گذاري", - "make-private": "شخصي سازي", - "apply": "اعمال", - "apply-changes": "اعمال تغييرات", - "edit-mode": "حالت ويرايش", - "enter-edit-mode": "ورود به حالت ويرايش", - "decline-changes": "عدم پذيرش تغييرات", - "close": "بستن", - "back": "بازگشت", - "run": "اجرا", - "sign-in": "!ورود", - "edit": "ويرايش", - "view": "نمايش", - "create": "ايجاد", - "drag": "کشيدن", - "refresh": "بازيابي", - "undo": "برگرداندن آخرين عمل", - "copy": "رونوشت", - "paste": "الصاق رونوشت", - "copy-reference": "رونوشت مرجع", - "paste-reference": "رونوشت مرجع", - "import": "وارد کردن", - "export": "صدور", - "share-via": "{{provider}} اشتراک گذاري از طريق" - }, - "aggregation": { - "aggregation": "تجميع", - "function": "تابع تجميع داده ها", - "limit": "بيشترين مقادير", - "group-interval": "فاصله گروه بندي", - "min": "کمترين", - "max": "بيشترين", - "avg": "ميانگين", - "sum": "جمع", - "count": "شمارش", - "none": "هيچکدام" - }, - "admin": { - "general": "عمومي", - "general-settings": "تنظيمات عمومي", - "outgoing-mail": "پيام خروجي", - "outgoing-mail-settings": "تنظيمات پيام خروجي", - "system-settings": "تنظيمات سيستم", - "test-mail-sent": "!ارسال پيام آزمايشي موفقيت آميز بود", - "base-url": "مبنا URL", - "base-url-required": ".مبنا مورد نياز است URL", - "mail-from": "... پيام از", - "mail-from-required": ".پيام از ... مورد نياز است", - "smtp-protocol": "SMTP قرارداد", - "smtp-host": "SMTP ميزبان", - "smtp-host-required": ".مورد نياز است SMTP ميزبان", - "smtp-port": "SMTP درگاه", - "smtp-port-required": ".فراهم کنيد SMTP شما بايد يک درگاه", - "smtp-port-invalid": ".معتبر باشد SMTP به نظر نمي آيد يک درگاه", - "timeout-msec": "مهلت (msec)", - "timeout-required": ".مهلت مورد نياز است", - "timeout-invalid": ".مهلت، به نظر نمي آيد معتبر باشد", - "enable-tls": "TLS فعال سازي", - "tls-version": "نسخه TLS", - "send-test-mail": "ارسال پيام آزمايشي" - }, - "alarm": { - "alarm": "هشدار", - "alarms": "هشدارها", - "select-alarm": "انتخاب هشدار", - "no-alarms-matching": ".يافت نشد '{{entity}}' هيچ هشداري مطابق", - "alarm-required": "هشدار مورد نياز است", - "alarm-status": "وضعيت هشدار", - "search-status": { - "ANY": "هر", - "ACTIVE": "فعال", - "CLEARED": "پاک شده", - "ACK": "تصديق شده", - "UNACK": "تصديق نشده" - }, - "display-status": { - "ACTIVE_UNACK": "تصديق نشده فعال", - "ACTIVE_ACK": "تصديق شده فعال", - "CLEARED_UNACK": "تصديق نشده پاک شده", - "CLEARED_ACK": "تصديق شده پاک شده" - }, - "no-alarms-prompt": "هيچ هشداري يافت نشد", - "created-time": "زمان ايجاد", - "type": "نوع", - "severity": "شدت", - "originator": "مبدأ", - "originator-type": "نوع مبدأ", - "details": "جزئيات", - "status": "وضعيت", - "alarm-details": "جزئيات هشدار", - "start-time": "زمان شروع", - "end-time": "زمان پايان", - "ack-time": "زمان تصديق", - "clear-time": "زمان پاک شدن", - "severity-critical": "بحراني", - "severity-major": "مهم", - "severity-minor": "جزئي", - "severity-warning": "اخطار", - "severity-indeterminate": "نامشخص", - "acknowledge": "تصديق", - "clear": "پاک کردن", - "search": "جستجوي هشدارها", - "selected-alarms": "اننخاب شده { count, plural, 1 {1 هشدار} other {# هشدارها} }", - "no-data": "هيچ داده اي براي نمايش نيست", - "polling-interval": "هشدار دهنده فاصله نمونه برداري (sec)", - "polling-interval-required": ".هشدار دهنده فاصله نمونه برداري مورد نياز است", - "min-polling-interval-message": ".حداقل فاصله مجاز نمونه برداري، 1 ثانيه است", - "aknowledge-alarms-title": "{ count, plural, 1 {1 هشدار} other {# هشدارها} } تصديق", - "aknowledge-alarms-text": "اطمينان داريد؟ { count, plural, 1 {1 هشدار} other {# هشدارها} } آيا شما از تصديق", - "aknowledge-alarm-title": "تصديق هشدار", - "aknowledge-alarm-text": "آيا شما از تصديق هشدار اطمينان داريد؟", - "clear-alarms-title": "{ count, plural, 1 {1 هشدار} other {# هشدارها} } پاک کردن", - "clear-alarms-text": "اطمينان داريد؟ { count, plural, 1 {1 هشدار} other {# هشدارها} } آيا شما از پاک کردن", - "clear-alarm-title": "پاک کردن هشدار", - "clear-alarm-text": "آيا شما از پاک کردن هشدار اطمينان داريد؟", - "alarm-status-filter": "فيلتر وضعيت هشدار" - }, - "alias": { - "add": "افزودن نام مستعار", - "edit": "ويرايش نام مستعار", - "name": "نام مستعار", - "name-required": "نام مستعار مورد نياز است", - "duplicate-alias": ".در حال حاضر نام مستعار مشابهي وجود دارد", - "filter-type-single-entity": "موجودي تکي", - "filter-type-entity-list": "ليست موجودي", - "filter-type-entity-name": "نام موجودي", - "filter-type-state-entity": "موجودي از وضعيت داشبورد", - "filter-type-state-entity-description": "پارامترهاي موجودي گرفته شده از وضعيت داشبورد", - "filter-type-asset-type": "نوع دارايي", - "filter-type-asset-type-description": "'{{assetType}}' دارايي هاي نوع", - "filter-type-asset-type-and-name-description": ".شروع مي شود '{{prefix}}' که نامشان با '{{assetType}}' دارايي هاي نوع", - "filter-type-device-type": "نوع دستگاه", - "filter-type-device-type-description": "'{{deviceType}}' دستگاه هاي نوع", - "filter-type-device-type-and-name-description": ".شروع مي شود '{{prefix}}' که نامشان با '{{deviceType}}' دستگاه هاي نوع", - "filter-type-entity-view-type": "نوع نمايش موجودي", - "filter-type-entity-view-type-description": "'{{entityView}}' نمايش هاي موجودي نوع ", - "filter-type-entity-view-type-and-name-description": ".شروع مي شود '{{prefix}}' که نامشان با '{{entityView}}' نمايش هاي موجودي نوع", - "filter-type-relations-query": "پرس و جو درمورد ارتباطات", - "filter-type-relations-query-description": ". دارند {{direction}} {{rootEntity}} را {{relationType}} که ارتباط {{entities}}", - "filter-type-asset-search-query": "پرس و جو درمورد جستجوي دارايي", - "filter-type-asset-search-query-description": ".دارند {{direction}} {{rootEntity}} را {{relationType}} که ارتباط{{assetTypes}} دارايي ها از انواع", - "filter-type-device-search-query": "پرس و چو درمورد جستجوي دستگاه", - "filter-type-device-search-query-description": ".دارند {{direction}} {{rootEntity}} را {{relationType}} که ارتباط{{deviceTypes}} دستگاه ها از انواع", - "filter-type-entity-view-search-query": "پرس و جو درمورد جستجوي نمايش موجودي", - "filter-type-entity-view-search-query-description": ".دارند {{direction}} {{rootEntity}} را {{relationType}} که ارتباط{{entityViewTypes}} نمايش هاي موجودي از انواع", - "entity-filter": "فيلتر موجودي", - "resolve-multiple": "تصميم با توجه به موجودي هاي متعدد", - "filter-type": "نوع فيلتر", - "filter-type-required": ".نوع فيلتر مورد نياز است", - "entity-filter-no-entity-matched": ".هيچ موجودي منطبق بر فيلتر مشخص شده يافت نشد", - "no-entity-filter-specified": ".هيچ فيلتر موجودي اي تعيين نشده است", - "root-state-entity": "موجودي وضعيت داشبورد به عنوان پايه استفاده شود", - "root-entity": "موجودي پايه", - "state-entity-parameter-name": "نام پارامتر موجودي وضعيت", - "default-state-entity": "موجودي وضعيت پيش فرض", - "default-entity-parameter-name": "به صورت پيش فرض", - "max-relation-level": "بالاترين سطح ارتباط", - "unlimited-level": "سطح نامحدود", - "state-entity": "موجودي وضعيت داشبورد", - "all-entities": "تمام موجودي ها", - "any-relation": "هر" - }, - "asset": { - "asset": "دارايي", - "assets": "دارايي ها", - "management": "مديريت دارايي", - "view-assets": "نمايش دارايي ها", - "add": "افزودن دارايي", - "assign-to-customer": "تخصيص به مشتري", - "assign-asset-to-customer": "تخصيص دارايي(ها) به مشتري", - "assign-asset-to-customer-text": "لطفا دارايي ها را انتخاب کنيد تا به مشتري تخصيص يابد", - "no-assets-text": "هيچ دارايي اي يافت نشد", - "assign-to-customer-text": "لطفا مشتري را انتخاب کنيد تا دارايي(ها) تخصيص يابد", - "public": "عمومي", - "assignedToCustomer": "تخصيص يافته به مشتري", - "make-public": "عمومي سازي دارايي", - "make-private": "شخصي سازي دارايي", - "unassign-from-customer": "لغو تخصيص از مشتري", - "delete": "حذف دارايي", - "asset-public": "دارايي عمومي است", - "asset-type": "نوع دارايي", - "asset-type-required": ".نوع دارايي مورد نياز است", - "select-asset-type": "انتخاب کردن نوع دارايي", - "enter-asset-type": "وارد کردن نوع دارايي", - "any-asset": "هر دارايي", - "no-asset-types-matching": ".يافت نشد '{{entitySubtype}}' هيچ دارايي منطبق بر", - "asset-type-list-empty": ".هيچيک از انواع دارايي انتخاب نشد", - "asset-types": "انواع دارايي", - "name": "نام", - "name-required": ".نام مورد نياز است", - "description": "توصيف", - "type": "نوع", - "type-required": ".نوع مورد نياز است", - "details": "جزئيات", - "events": "رويدادها", - "add-asset-text": "افزودن دارايي جديد", - "asset-details": "جزئيات دارايي", - "assign-assets": "تخصيص دارايي ها", - "assign-assets-text": "به مشتري { count, plural, 1 {1 دارايي} other {# دارايي} } تخصيص", - "delete-assets": "حذف دارايي ها", - "unassign-assets": "لغو تخصيص دارايي ها", - "unassign-assets-action-title": "از مشتري { count, plural, 1 {1 دارايي} other {# دارايي} } لغو تخصيص", - "assign-new-asset": "تخصيص دارايي جديد", - "delete-asset-title": "مطمئنيد؟ '{{assetName}}' آيا از حذف دارايي", - "delete-asset-text": ".مراقب باشيد، پس از تأييد، دارايي و تمام داده هاي مربوطه غير قابل بازيابي مي شوند", - "delete-assets-title": "مطمئنيد؟ { count, plural, 1 {1 دارايي} other {# دارايي} } آيا از حذف", - "delete-assets-action-title": "{ count, plural, 1 {1 دارايي} other {# دارايي} } حذف", - "delete-assets-text": ".مراقب باشيد، پس از تأييد، تمام دارايي هاي انتخاب شده حذف، و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", - "make-public-asset-title": "مطمئنيد؟ '{{assetName}}' آيا از عمومي سازي", - "make-public-asset-text": ".پس از تأييد، دارايي و تمامي داده هايش عمومي و قابل دسترسي براي ديگران مي شود", - "make-private-asset-title": "مطمئنيد؟ '{{assetName}}' آيا از شخصي سازي دارايي", - "make-private-asset-text": ".پس از تأييد، دارايي و تمامي داده هايش شخصي و خارج از دسترس ديگران مي شوند", - "unassign-asset-title": "مطمئنيد؟ '{{assetName}}' آيا از لغو تخصيص دارايي", - "unassign-asset-text": ".پس از تأييد، دارايي، لغو تخصيص و خارج از دسترس مشتري مي شود", - "unassign-asset": "لغو تخصيص دارايي", - "unassign-assets-title": "مطمئنيد؟ { count, plural, 1 {1 دارايي} other {# دارايي} } آيا از لغو تخصيص", - "unassign-assets-text": ".پس از تأييد، تمام دارايي هاي انتخاب شده، لغو تخصيص و خارج از دسترس مشتري مي شوند", - "copyId": "دارايي ID رونوشت از", - "idCopiedMessage": "دارايي در حافظه موقت رونوشت شد ID", - "select-asset": "انتخاب دارايي", - "no-assets-matching": ".يافت نشد '{{entity}}' هيچ دارايي منطبق بر", - "asset-required": "دارايي مود نياز است", - "name-starts-with": "نام دارايي شروع مي شود با", - "label": "برچسب" - }, - "attribute": { - "attributes": "ويژگي ها", - "latest-telemetry": "آخرين سنجش", - "attributes-scope": "حوزه ويژگي هاي موجودي", - "scope-latest-telemetry": "آخرين سنجش", - "scope-client": "ويژگي هاي مشتري", - "scope-server": "ويژگي هاي سِروِر", - "scope-shared": "ويژگي هاي مشترک", - "add": "افزودن ويژگي ها", - "key": "کليد", - "last-update-time": "آخرين زمان به روز رساني", - "key-required": ".کليد ويژگي مورد نياز است", - "value": "مقدار", - "value-required": ".مقدار ويژگي مورد نياز است", - "delete-attributes-title": "مطمئنيد؟ { count, plural, 1 {1 ويژگي} other {# ويژگي} } آيا از حذف", - "delete-attributes-text": ".مراقب باشيد، پس از تأييد، تمام ويژگي هاي انتخاب شده حذف مي گردند", - "delete-attributes": "حذف ويژگي ها", - "enter-attribute-value": "وارد کردن مقدار ويژگي", - "show-on-widget": "نمايش بر ويجت", - "widget-mode": "حالت ويجت", - "next-widget": "ويجت بعد", - "prev-widget": "ويجت قبل", - "add-to-dashboard": "افزودن به داشبورد", - "add-widget-to-dashboard": "افزودن ويجت به داشبورد", - "selected-attributes": "انتخاب شدند { count, plural, 1 {1 ويژگي} other {# ويژگي} }", - "selected-telemetry": "انتخاب شد { count, plural, 1 {1 واحد سنجش} other {# واحد سنجش} }" - }, - "audit-log": { - "audit": "بازبيني", - "audit-logs": "داده هاي ثبت شده از بازبيني", - "timestamp": "برچسب زمان", - "entity-type": "نوع موحودي", - "entity-name": "نام موجودي", - "user": "کاربر", - "type": "نوع", - "status": "وضعيت", - "details": "جزئيات", - "type-added": "اضافه شده", - "type-deleted": "حذف شده", - "type-updated": "به روز", - "type-attributes-updated": "ويژگي ها به روز شد", - "type-attributes-deleted": "ويژگي ها حذف شد", - "type-rpc-call": "RPC فراخواني", - "type-credentials-updated": "اعتبارنامه ها به روز شد", - "type-assigned-to-customer": "به مشتري تخصيص يافت", - "type-unassigned-from-customer": "از مشتري لغو تخصيص شد", - "type-activated": "فعال شد", - "type-suspended": "معلق", - "type-credentials-read": "اعتبارنامه ها خوانده شد", - "type-attributes-read": "ويژگي ها خوانده شد", - "type-relation-add-or-update": "ارتباط به روز شد", - "type-relation-delete": "ارتباط حذف شد", - "type-relations-delete": "تمام ارتباطات حذف شد", - "type-alarm-ack": "تصديق شده", - "type-alarm-clear": "پاک شده", - "status-success": "موفقيت", - "status-failure": "عدم موفقيت", - "audit-log-details": "بازبيني جزئيات ثبت داده ها", - "no-audit-logs-prompt": "هيچ داده ثبت شده اي يافت نشد", - "action-data": "داده هاي اقدام", - "failure-details": "جزئيات عدم موفقيت", - "search": "جستجوي داده هاي ثبت شده از بازبيني", - "clear-search": "پاک کردن جستجو" - }, - "confirm-on-exit": { - "message": "شما تغييراتي ذخيره نشده داريد. از ترک اين صفحه مطمئنيد؟", - "html-message": "از ترک اين صفحه مطمئنيد؟
    .شما تغييراتي ذخيره نشده داريد", - "title": "تغييرات ذخيره نشده " - }, - "contact": { - "country": "کشور", - "city": "شهر", - "state": "استان / ايالت", - "postal-code": "کد پستي", - "postal-code-invalid": ".قالب کد پستي نامعتبر است", - "address": "نشاني", - "address2": "2 نشاني", - "phone": "تلفن", - "email": "پست الکترونيک", - "no-address": "بدون آدرس" - }, - "common": { - "username": "نام کاربري", - "password": "رمز عبور", - "enter-username": "وارد کردن نام کاربري", - "enter-password": "وارد کردن رمز عبور", - "enter-search": "وارد کردن جستجو" - }, - "content-type": { - "json": "JSON", - "text": "Text", - "binary": "Binary (Base64)" - }, - "customer": { - "customer": "مشتري", - "customers": "مشتريان", - "management": "مديريت مشتري", - "dashboard": "داشبورد مشتري", - "dashboards": "داشبوردهاي مشتري", - "devices": "دستگاه هاي مشتري", - "entity-views": "نمايش موجودي مشتري", - "assets": "دارايي هاي مشتري", - "public-dashboards": "داشبوردهاي عمومي", - "public-devices": "دستگاه هاي عمومي", - "public-assets": "دارايي هاي عمومي", - "public-entity-views": "نمايش موجودي عمومي", - "add": "افزودن مشتري", - "delete": "حذف مشتري", - "manage-customer-users": "مديريت کاربرهاي مشتري", - "manage-customer-devices": "مديريت دستگاه هاي مشتري", - "manage-customer-dashboards": "مديريت داشبوردهاي مشتري", - "manage-public-devices": "مديريت دستگاه هاي عمومي", - "manage-public-dashboards": "مديريت داشبوردهاي عمومي", - "manage-customer-assets": "مديريت دارايي هاي مشتري", - "manage-public-assets": "مديريت دارايي هاي عمومي", - "add-customer-text": "افزودن مشتري جديد", - "no-customers-text": "هيچ مشتري اي يافت نشد", - "customer-details": "جزئيات اطلاعات مشتري", - "delete-customer-title": "مطمئنيد؟ '{{customerTitle}}' از حذف مشتري", - "delete-customer-text": ".مراقب باشيد، پس از تأييد، مشتري و تمامي داده هاي مربوطه، غير قابل بازيابي مي شوند", - "delete-customers-title": "مطمئنيد؟ { count, plural, 1 {1 مشتري} other {# مشتري} } از حذف", - "delete-customers-action-title": "{ count, plural, 1 {1 مشتري} other {# مشتري} } حذف", - "delete-customers-text": ".مراقب باشيد، پس از تأييد، تمام مشتريانِ انتخاب شده حذف، و تمامي داده هاي مربوطه غير قابل دسترسي مي شوند", - "manage-users": "مديريت کاربرها", - "manage-assets": "مديريت دارايي ها", - "manage-devices": "مديريت دستگاه ها", - "manage-dashboards": "مديريت داشبوردها", - "title": "عنوان", - "title-required": ".عنوان مورد نياز است", - "description": "توصيف", - "details": "جزئيات", - "events": "رويدادها", - "copyId": "مشتري ID رونوشت از", - "idCopiedMessage": "مشتري در حافظه موقت رونوشت شد ID", - "select-customer": "انتخاب مشتري", - "no-customers-matching": ".يافت نشد '{{entity}}' هيچ مشتري منطبق بر", - "customer-required": "مشتري مورد نياز است", - "select-default-customer": "انتخاب مشتري پيش فرض", - "default-customer": "مشتري پيش فرض", - "default-customer-required": "جهت عيب يابي داشبورد در سطح کاربر مياني، مشتري پيش فرض مورد نياز است" - }, - "datetime": { - "date-from": "تاريخ از", - "time-from": "زمان از", - "date-to": "تاريخ تا", - "time-to": "زمان تا" - }, - "dashboard": { - "dashboard": "داشبورد", - "dashboards": "داشبوردها", - "management": "مديريت داشبورد", - "view-dashboards": "نمايش داشبوردها", - "add": "افزودن داشبورد", - "assign-dashboard-to-customer": "تخصيص داشبورد(ها) به مشتري", - "assign-dashboard-to-customer-text": "لطفا داشبوردها را، جهت تخصيص به مشتري، انتخاب کنيد", - "assign-to-customer-text": "لطفا مشتري را، جهت تخصيص داشبورد(ها)، انتخاب کنيد", - "assign-to-customer": "تخصيص به مشتري", - "unassign-from-customer": "لغو تخصيص از مشتري", - "make-public": "عمومي سازي مشتري", - "make-private": "شخصي سازي داشبورد", - "manage-assigned-customers": "مديريت مشتريان تخصيص داده شده", - "assigned-customers": "مشتريان تخصيص داده شده", - "assign-to-customers": "تخصيص داشبورد(ها) به مشتريان", - "assign-to-customers-text": "لطفا مشتريان را، جهت تخصيص داشبورد(ها)، انتخاب کنيد", - "unassign-from-customers": "لغو تخصيص داشبوردها از مشتريان", - "unassign-from-customers-text": "لطفا مشتريان را، جهت لغو تخصيص از داشبورد(ها)، انتخاب کنيد", - "no-dashboards-text": "هيچ داشبوردي يافت نشد", - "no-widgets": "هيچ ويجتي پيکربندي نشده است", - "add-widget": "افزودن ويجت جديد", - "title": "عنوان", - "select-widget-title": "انتخاب ويجت", - "select-widget-subtitle": "ليست انواع ويجت هاي در دسترس", - "delete": "حذف داشبورد", - "title-required": ".عنوان مورد نياز است", - "description": "توصيف", - "details": "جزئيات", - "dashboard-details": "جزئيات داشبورد", - "add-dashboard-text": "افزودن داشبورد جديد", - "assign-dashboards": "تخصيص داشبوردها", - "assign-new-dashboard": "تخصيص داشبورد جديد", - "assign-dashboards-text": "به مشتريان { count, plural, 1 {1 داشبورد} other {# داشبورد} } تخصيص", - "unassign-dashboards-action-text": "از مشتريان { count, plural, 1 {1 داشبورد} other {# داشبورد} } لغو تخصيص", - "delete-dashboards": "حذف داشبوردها", - "unassign-dashboards": "لغو تخصيص داشبوردها", - "unassign-dashboards-action-title": "از مشتري { count, plural, 1 {1 داشبورد} other {# داشبورد} } لغو تخصيص", - "delete-dashboard-title": "مطمئنيد؟ '{{dashboardTitle}}' از حذف", - "delete-dashboard-text": ".مراقب باشيد، پس از تأييد، داشبورد و تمامي داده هاي مربوطه، غير قابل بازيابي مي شوند", - "delete-dashboards-title": "مطمئنيد؟ { count, plural, 1 {1 داشبورد} other {# داشبورد} } از حذف", - "delete-dashboards-action-title": "{ count, plural, 1 {1 داشبورد} other {# داشبورد} } حذف", - "delete-dashboards-text": ".مراقب باشيد، پس از تأييد، تمام داشبوردهاي انتخاب شده حذف، و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند ", - "unassign-dashboard-title": "مطمئنيد؟ '{{dashboardTitle}}' از لغو تخصيص داشبورد", - "unassign-dashboard-text": ".پس از تأييد، داشبورد، لغو تخصيص و خارج از دسترس مشتري مي شود", - "unassign-dashboard": "لغو تخصيص داشبورد", - "unassign-dashboards-title": "مطمئنيد؟ { count, plural, 1 {1 داشبورد} other {# داشبورد} } از لغو تخصيص", - "unassign-dashboards-text": ".پس از تأييد، تمام داشبوردهاي انتخاب شده، لغو تخصيص و خارج از دسترس مشتري مي شوند", - "public-dashboard-title": "داشبورد اکنون عمومي است", - "public-dashboard-text": ":قابل دسترسي است اکنون عمومي بوده و از طريق پيوند عمومي ديگر ، {{dashboardTitle}} ،داشبورد شما", - "public-dashboard-notice": ".فراموش نکنيد براي دسترسي به داده هاي دستگاه هاي مربوطه، آنها را عمومي نماييد
    :توجه", - "make-private-dashboard-title": "مطمئنيد؟ '{{dashboardTitle}}' از شخصي سازي داشبورد", - "make-private-dashboard-text": ".پس از تأييد، داشبورد، شخصي و خارج از دسترس ديگران مي شود", - "make-private-dashboard": "شخصي سازي داشبورد", - "socialshare-text": "ThingsBoard طراحي شده توسط '{{dashboardTitle}}'", - "socialshare-title": "ThingsBoard طراحي شده توسط '{{dashboardTitle}}'", - "select-dashboard": "انتخاب داشبورد", - "no-dashboards-matching": ".يافت نشد '{{entity}}' هيچ داشبوردي منطبق بر", - "dashboard-required": ".داشبورد مورد نياز است", - "select-existing": "انتخاب داشبورد موجود", - "create-new": "ايجاد داشبورد جديد", - "new-dashboard-title": "عنوان داشبورد جديد", - "open-dashboard": "باز کردن داشبورد", - "set-background": "تنظيم پس زمينه", - "background-color": "رنگ پس زمينه", - "background-image": "تصوير پس زمينه", - "background-size-mode": "حالت اندازه پس زمينه", - "no-image": "هيچ تصويري انتخاب نشد", - "drop-image": ".جهت بارگذاري يک تصوير، آن را با موس کِشيده و رها کنيد، و يا روي آن کليک نماييد", - "settings": "تنظيمات", - "columns-count": "شمارش ستون ها", - "columns-count-required": ".شمارش ستون ها مورد نياز است", - "min-columns-count-message": ".کمترين تعداد مجاز ستون ها 10 عدد است", - "max-columns-count-message": ".بيشترين تعداد مجاز ستون ها 1000 عدد است", - "widgets-margins": "حاشيه بين ويجت ها", - "horizontal-margin": "حاشيه افقي", - "horizontal-margin-required": ".مقدار حاشيه افقي مورد نياز است", - "min-horizontal-margin-message": ".کمترين مقدار مجاز حاشيه افقي 0 است", - "max-horizontal-margin-message": ".بيشترين مقدار مجاز حاشيه افقي 50 است", - "vertical-margin": "حاشيه عمودي", - "vertical-margin-required": ".حاشيه عمودي مورد نياز است", - "min-vertical-margin-message": ".کمترين مقدار مجاز حاشيه عمودي 0 است", - "max-vertical-margin-message": ".بيشترين مقدار مجاز حاشيه افقي 50 است", - "autofill-height": "تنظيم خودکار ارتفاع چيدمان طرح", - "mobile-layout": "تنظيمات چيدمان طرح در تلفن همراه", - "mobile-row-height": "(px) ارتفاع رديف در تلفن همراه", - "mobile-row-height-required": ".مقدار ارتفاع ردبف در تلفن همراه مورد نياز است", - "min-mobile-row-height-message": ".کمترين مقدار مجاز ارتفاع رديف در تلفن همراه 5 پيکسل است", - "max-mobile-row-height-message": ".بيشترين مقدار مجاز ارتفاع رديف در تلفن همراه 200 پيکسل است", - "display-title": "نمايش عنوان داشبورد", - "toolbar-always-open": "باز نگه داشتن نوار ابزار", - "title-color": "رنگ عنوان", - "display-dashboards-selection": "نمايش انتخاب داشبوردها", - "display-entities-selection": "نمايش انتخاب موجودي ها", - "display-dashboard-timewindow": "نمايش پنجره زمان", - "display-dashboard-export": "نمايش صدور", - "import": "وارد کردن داشبورد", - "export": "صادر کردن داشبورد", - "export-failed-error": "{{error}} :صدور داشبورد ممکن نيست", - "create-new-dashboard": "ايجاد داشبورد جديد", - "dashboard-file": "پرونده داشبورد", - "invalid-dashboard-file-error": ".وارد کردن داشبورد ممکن نيست: ساختار داده داشبورد نامعتبر است", - "dashboard-import-missing-aliases-title": "پيکربندي نامهاي مستعار استفاده شده توسط داشبوردِ وارده", - "create-new-widget": "ايجاد ويجت جديد", - "import-widget": "وارد کردن ويجت", - "widget-file": "پرونده ويجت", - "invalid-widget-file-error": ".وارد کردن ويجت ممکن نيست: ساختار داده ويجت نامعتبر است", - "widget-import-missing-aliases-title": "پيکربندي نامهاي مستعار استفاده شده توسط ويجتِ وارده", - "open-toolbar": "باز کردن نوار ابزار داشبورد", - "close-toolbar": "بستن نوار ابزار", - "configuration-error": "خطاي پيکربندي", - "alias-resolution-error-title": "خطاي پيکربندي نامهاي مستعار داشبورد", - "invalid-aliases-config": ".لطفا جهت حل اين موضوع با مسئول مربوط به خود تماس بگيريد
    .يافتن دستگاهي منطبق بر فبلتر بعضي نامهاي مستعار ممکن نيست", - "select-devices": "انتخاب دستگاه ها", - "assignedToCustomer": "تخصيص يافته به مشتري", - "assignedToCustomers": "تخصيص يافته به مشتريان", - "public": "عمومي", - "public-link": "پيوند عمومي", - "copy-public-link": "رونوشت از پيوند عمومي", - "public-link-copied-message": "پيوند عمومي داشبورد در حافظه موقت رونوشت شد", - "manage-states": "مديريت وضعيت هاي داشبورد", - "states": "وضعيت هاي داشبورد", - "search-states": "جستجوي وضعيت هاي داشبورد", - "selected-states": "انتخاب شدند { count, plural, 1 {1 وضعيت داشبورد} other {# وضعيت داشبورد} }", - "edit-state": "ويرايش وضعيت داشبورد", - "delete-state": "حذف وضعيت داشبورد", - "add-state": "افزودن وضعيت داشبورد", - "state": "وضعيت داشبورد", - "state-name": "نام", - "state-name-required": ".نام وضعيت داشبورد مورد نياز است", - "state-id": "وضعيت ID", - "state-id-required": ".وضعيت داشبورد مورد نياز است ID", - "state-id-exists": ".مشابه موجود است ID در حال حاضر وضعيت داشبوردي با", - "is-root-state": "وضعيت پايه", - "delete-state-title": "حذف وضعيت داشبورد", - "delete-state-text": "مطمئنيد؟ '{{stateName}}' از حذف وضعيت داشبورد با نام", - "show-details": "نمايش جزئيات", - "hide-details": "پنهان کردن جزئيات", - "select-state": "انتخاب وضعيت هدف", - "state-controller": "کنترل کننده وضعيت" - }, - "datakey": { - "settings": "تنظيمات", - "advanced": "پيشرفته", - "label": "برچسب", - "color": "رنگ", - "units": "کارکتر خاص براي نمايش بعد از مقدار تعين شده", - "decimals": "تعداد ارقام بعد از مميّز شناور", - "data-generation-func": "تابع توليد داده", - "use-data-post-processing-func": "استفاده از تابع پس پردازش داده", - "configuration": "پيکربندي کليد داده", - "timeseries": "سري هاي زماني", - "attributes": "ويژگي ها", - "alarm": "حوزه هاي هشدار", - "timeseries-required": ".سري هاي زماني موجودي مورد نياز است", - "timeseries-or-attributes-required": ".سري هاي زماني / ويژگي هاي موجودي مورد نياز است", - "maximum-timeseries-or-attributes": "{ count, plural, 1 {.1 سري زماني / ويژگي مجاز است} other {# سري زماني / ويژگي مجازند} } بيشترين", - "alarm-fields-required": ".حوزه هاي هشدار مورد نياز است", - "function-types": "نوع توابع", - "function-types-required": ".نوع تابع مورد نياز است", - "maximum-function-types": "{ count, plural, 1 {.1 نوع تابع مجاز است} other {# نوع تابع مجازند} } بيشترين", - "time-description": "برچسب زماني مقدار فعلي؛", - "value-description": "مقدار فعلي؛", - "prev-value-description": "نتيجه ي فراخوانيِ تابع قبلي؛", - "time-prev-description": "برچسب زماني مقدار قبلي؛", - "prev-orig-value-description": "ممقدار اصلي قبلي" - }, - "datasource": { - "type": "نوع منبع داده", - "name": "نام", - "add-datasource-prompt": "لطفا منبع داده را اضافه کنيد" - }, - "details": { - "edit-mode": "حالت ويرايش", - "toggle-edit-mode": "حالت ويرايش را تغيير دهيد" - }, - "device": { - "device": "دستگاه", - "device-required": ".دستگاه مورد نياز است", - "devices": "دستگاه ها", - "management": "مديريت دستگاه", - "view-devices": "نمايش دستگاه ها", - "device-alias": "نام مستعار دستگاه", - "aliases": "نامهاي مستعار دستگاه", - "no-alias-matching": ".يافت نشد'{{alias}}'", - "no-aliases-found": ".هيچ نام مستعاري يافت نشد", - "no-key-matching": ".يافت نشد'{{key}}'", - "no-keys-found": ".هيچ کليدي يافت نشد", - "create-new-alias": "!ايجاد يک نام مستعار جديد", - "create-new-key": "!ايجاد يک کليد جديد", - "duplicate-alias-error": ".نام مستعار در داشبورد بايد يکتا باشد
    '{{alias}}'نام مستعار مشابه يافت شد", - "configure-alias": "نام مستعار '{{alias}}' پيکربندي", - "no-devices-matching": "مطابقت داشته باشد وجود ندارد '{{entity}}' هيچ دستگاهي که با ", - "alias": "نام مستعار", - "alias-required": ".نام مستعار مورد نياز است", - "remove-alias": "حذف نام مستعار دستگاه", - "add-alias": "افزودن نام مستعار دستگاه", - "name-starts-with": "اسم دستگاه شروع مي شود با", - "device-list": "ليست دستگاه ها", - "use-device-name-filter": "از فيلتر استفاده کنيد", - "device-list-empty": ".هيچ دستگاهي انتخاب نشده است", - "device-name-filter-required": ".فيلتر نام دستگاه مورد نياز است", - "device-name-filter-no-device-matched": ".شروع شود يافت نشد '{{device}}' هيچ دستگاهي که با", - "add": "افزودن دستگاه", - "assign-to-customer": "تخصيص به مشتري", - "assign-device-to-customer": "تخصيص دستگاه (ها) به مشتري", - "assign-device-to-customer-text": "لطفا دستگاه ها را انتخاب کنيد تا به مشتري تخصيص يابد", - "make-public": "عمومي سازي دستگاه", - "make-private": "شخصي سازي دستگاه", - "no-devices-text": "هيچ دستگاهي يافت نشد", - "assign-to-customer-text": "لطفا مشتري را انتخاب کنيد تا دستگاه(ها) تخصيص يابد", - "device-details": "جزئيات دستگاه", - "add-device-text": "افزودن دستگاه جديد", - "credentials": "اعتبارنامه ها", - "manage-credentials": "مديريت اعتبارنامه ها", - "delete": "حذف دستگاه", - "assign-devices": "تخصيص دستگاه ها", - "assign-devices-text": "به مشتري { count, plural, 1 {1 دستگاه} other {# دستگاه} } تخصيص", - "delete-devices": "حذف دستگاه ها", - "unassign-from-customer": "لغو تخصيص از مشتري", - "unassign-devices": "لغو تخصيص دستگاه ها", - "unassign-devices-action-title": "از مشتري { count, plural, 1 {1 دستگاه} other {# دستگاه} } لغو تخصيص", - "assign-new-device": "تخصيص دستگاه جديد", - "make-public-device-title": "مطمئنيد؟ '{{deviceName}}' از عمومي سازي دستگاه", - "make-public-device-text": ".پس از تأييد، دستگاه و تمامي داده هايش عمومي و قابل دسترسي براي ديگران مي شود", - "make-private-device-title": "مطمئنيد؟ '{{deviceName}}' از شخصي سازي دستگاه", - "make-private-device-text": ".پس از تأييد، دستگاه و تمامي داده هايش شخصي و خارج از دسترس ديگران مي شوند", - "view-credentials": "نمايش اعتبارنامه ها", - "delete-device-title": "مطمئنيد؟ '{{deviceName}}' از حذف", - "delete-device-text": ".مراقب باشيد، پس از تأييد، دستگاه و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", - "delete-devices-title": "مطمئنيد؟ { count, plural, 1 {1 دستگاه} other {# دستگاه} } از حذف", - "delete-devices-action-title": "{ count, plural, 1 {1 دستگاه} other {# دستگاه} } حذف", - "delete-devices-text": ".مراقب باشيد، پس از تأييد، تمام دستگاه هاي انتخاب شده، حذف، و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", - "unassign-device-title": "مطمئنيد؟ '{{deviceName}}' از لغو تخصيص", - "unassign-device-text": ".پس از تأييد، دستگاه، لغو تخصيص و خارج از دسترس مشتري مي شود", - "unassign-device": "لغو تخصيص دستگاه", - "unassign-devices-title": "مطمئنيد؟ { count, plural, 1 {1 دستگاه} other {# دستگاه} } از لغو تخصيص", - "unassign-devices-text": ".پس از تأييد، تمام دستگاه هاي انتخاب شده، لغو تخصيص و خارج از دسترس مشتري مي شوند", - "device-credentials": "اعتبارنامه هاي دستگاه", - "credentials-type": "نوع اعتبارنامه ها", - "access-token": "شناسه دسترسي", - "access-token-required": ".شناسه دسترسي مورد نياز است", - "access-token-invalid": ".طول شناسه دسترسي بايد از 1 تا 20 حرف باشد", - "rsa-key": "RSA کليد عمومي", - "rsa-key-required": ".مورد نياز است RSA کليد عمومي", - "secret": "محرمانه", - "secret-required": ".شناسه محرمانه مورد نياز است", - "device-type": "نوع دستگاه", - "device-type-required": ".نوع دستگاه مورد نياز است", - "select-device-type": "انتخاب نوع دستگاه", - "enter-device-type": "وارد کردن نوع دستگاه", - "any-device": "هر دستگاهي", - "no-device-types-matching": ".يافت نشد '{{entitySubtype}}' هيچ نوع دستگاهي منطبق بر", - "device-type-list-empty": ".هيچ نوع دستگاهي انتخاب نشد", - "device-types": "انواع دستگاه", - "name": "نام", - "name-required": ".نام مورد نياز است", - "description": "توصيف", - "events": "رويدادها", - "details": "جزئيات", - "copyId": "دستگاه ID رونوشت از", - "copyAccessToken": "رونوشت از شناسه دسترسي", - "idCopiedMessage": ".دستگاه در حافظه موقت رونوشت شد ID", - "accessTokenCopiedMessage": ".شناسه دسترسي دستگاه در حافظه موقت رونوشت شد", - "assignedToCustomer": "تخصيص يافته به مشتري", - "unable-delete-device-alias-title": "حذف نام مستعار دستگاه ممکن نيست", - "unable-delete-device-alias-text": "
    {{widgetsList}} :را تا زمان استفاده توسط ويجت(هاي) زير نمي توان حذف کرد ، '{{deviceAlias}}' ،نام مستعار دستگاه", - "is-gateway": "درگاه است", - "public": "عمومي", - "device-public": "دستگاه عمومي است", - "select-device": "انتخاب دستگاه" - }, - "dialog": { - "close": "بستن گفتگو" - }, - "error": { - "unable-to-connect": ".اتصال به سِروِر ممکن نيست! لطفا اتصال اينترنت خود را بررسي کنيد", - "unhandled-error-code": "{{errorCode}} :کد خطاي رسيدگي نشده", - "unknown-error": "خطاي ناشناخته" - }, - "entity": { - "entity": "موجودي", - "entities": "موجودي ها", - "aliases": "نامهاي مستعار موجودي", - "entity-alias": "نام مستعار موجودي", - "unable-delete-entity-alias-title": "حذف نام مستعار موجودي ممکن نيست", - "unable-delete-entity-alias-text": "
    {{widgetsList}} :را تا زمان استفاده توسط ويجت(هاي) زير نمي توان حذف کرد ، '{{entityAlias}}' ،نام مستعار موجودي", - "duplicate-alias-error": ".نامهاي مستعار موجودي بايد در داشبورد، منحصر بفرد باشند
    .يافت شد '{{alias}}' نام مستعار تکراري", - "missing-entity-filter-error": ".مفقود است '{{alias}}' فيلتر براي نام مستعار", - "configure-alias": "'{{alias}}' پيکربندي نام مستعار", - "alias": "نام مستعار", - "alias-required": ".نام مستعار موجودي مورد نياز است", - "remove-alias": "حذف نام مستعار موجودي", - "add-alias": "افزودن نام مستعار موجودي", - "entity-list": "ليست موجودي", - "entity-type": "نوع موجودي", - "entity-types": "انواع موجودي", - "entity-type-list": "ليست نوع موجودي", - "any-entity": "هر موجودي", - "enter-entity-type": "وارد کردن نوع موجودي", - "no-entities-matching": ".يافت نشد '{{entity}}' هيچ موجودي منطبق بر", - "no-entity-types-matching": ".يافت نشد '{{entityType}}' هيچ نوع موجودي منطبق بر", - "name-starts-with": "نام شروع مي شود با", - "use-entity-name-filter": "استفاده از فيلتر", - "entity-list-empty": ".هيچ موجودي اي انتخاب نشده است", - "entity-type-list-empty": ".هيچ نوع موجودي انتخاب نشده است", - "entity-name-filter-required": ".فيلتر نام موجودي مورد نياز است", - "entity-name-filter-no-entity-matched": ".شروع شود يافت نشد '{{entity}}' هيچ موجودي که با", - "all-subtypes": "همه", - "select-entities": "انتخاب موجودي ها", - "no-aliases-found": ".هيچ نام مستعاري يافت نشد", - "no-alias-matching": ".يافت نشد '{{alias}}'", - "create-new-alias": "!ايجاد يک نام مستعار جديد", - "key": "کليد", - "key-name": "نام کليد", - "no-keys-found": ".هيچ کليدي يافت نشد", - "no-key-matching": "'.يافت نشد {{key}}'", - "create-new-key": "!ايجاد يک کليد جديد", - "type": "نوع", - "type-required": ".نوع موجودي مورد نياز است", - "type-device": "دستگاه", - "type-devices": "دستگاه ها", - "list-of-devices": "{ count, plural, 1 {يک دستگاه} other {ليست # دستگاه} }", - "device-name-starts-with": "شروع مي شود '{{prefix}}' دستگاه هايي که نامشان با", - "type-asset": "دارايي", - "type-assets": "دارايي ها", - "list-of-assets": "{ count, plural, 1 {يک دارايي} other {ليست # دارايي} }", - "asset-name-starts-with": "شروع مي شود '{{prefix}}' دارايي هايي که نامشان با", - "type-entity-view": "نمايش موجودي", - "type-entity-views": "نمايش هاي موجودي", - "list-of-entity-views": "{ count, plural, 1 {يک نمايش موجودي} other {ليست # نمايش موجودي} }", - "entity-view-name-starts-with": "شروع مي شود '{{prefix}}' نمايش هاي موجودي که نامشان با", - "type-rule": "قاعده", - "type-rules": "قواعد", - "list-of-rules": "{ count, plural, 1 {يک قاعده} other {ليست # قاعده} }", - "rule-name-starts-with": "شروع مي شود '{{prefix}}' قواعدي که نامشان با", - "type-plugin": "ابزار جانبي", - "type-plugins": "ابزارهاي جانبي", - "list-of-plugins": "{ count, plural, 1 {يک ابزار جانبي} other {ليست # ابزار جانبي} }", - "plugin-name-starts-with": "شروع مي شود '{{prefix}}' ابزارهاي جانبي که نامشان با", - "type-tenant": "کاربر", - "type-tenants": "کاربران", - "list-of-tenants": "{ count, plural, 1 {يک کاربر} other {ليست # کاربر} }", - "tenant-name-starts-with": "شروع مي شود '{{prefix}}' کاربرهايي که نامشان با", - "type-customer": "مشتري", - "type-customers": "مشتريان", - "list-of-customers": "{ count, plural, 1 {يک مشتري} other {ليست # مشتري} }", - "customer-name-starts-with": "شروع مي شود '{{prefix}}' مشترياني که نامشان با", - "type-user": "کاربر", - "type-users": "کاربران", - "list-of-users": "{ count, plural, 1 {يک کاربر} other {ليست # کاربر} }", - "user-name-starts-with": "شروع مي شود '{{prefix}}' کاربرهايي که نامشان با", - "type-dashboard": "داشبورد", - "type-dashboards": "داشبوردها", - "list-of-dashboards": "{ count, plural, 1 {يک داشبورد} other {ليست # داشبورد} }", - "dashboard-name-starts-with": "شروع مي شود '{{prefix}}' داشبوردهايي که نامشان با", - "type-alarm": "هشدار", - "type-alarms": "هشدارها", - "list-of-alarms": "{ count, plural, 1 {يک هشدار} other {ليست # هشدار} }", - "alarm-name-starts-with": "شروع مي شود '{{prefix}}' هشدارهايي که نامشان با", - "type-rulechain": "زنجيره قواعد", - "type-rulechains": "زنجيره هاي قواعد", - "list-of-rulechains": "{ count, plural, 1 {يک زنجيره قواعد} other {ليست # زنجيره قواعد} }", - "rulechain-name-starts-with": "شروع مي شود '{{prefix}}' زنجيره هاي قواعدي که نامشان با", - "type-rulenode": "گره قواعد", - "type-rulenodes": "گره هاي قواعد", - "list-of-rulenodes": "{ count, plural, 1 {يک گره قواعد} other {ليست # گره قواعد} }", - "rulenode-name-starts-with": "شروع مي شود '{{prefix}}' گره هاي قواعدي که نامشان با", - "type-current-customer": "مشتري فعلي", - "search": "جستجوي موجودي ها", - "selected-entities": "انتخاب شدند { count, plural, 1 {1 موجودي} other {# موجودي} }", - "entity-name": "نام موجودي", - "details": "جزئيات موجودي", - "no-entities-prompt": "هيچ موجودي اي يافت نشد", - "no-data": "هيچ داده اي براي نمايش نيست", - "columns-to-display": "ستون ها براي نمايش" - }, - "entity-view": { - "entity-view": "نمايش موجودي", - "entity-view-required": ".نمايش موجودي مورد نياز است", - "entity-views": "نمايش هاي موجودي", - "management": "مديريت نمايش موجودي", - "view-entity-views": "نمايش نمايش هاي موجودي", - "entity-view-alias": "نام مستعار نمايش موجودي", - "aliases": "نامهاي مستعار نمايش موجودي", - "no-alias-matching": ".يافت نشد '{{alias}}'", - "no-aliases-found": ".هيچ نام مستعاري يافت نشد", - "no-key-matching": ".يافت نشد '{{key}}'", - "no-keys-found": ".هيچ کليدي يافت نشد", - "create-new-alias": "!ايجاد نام مستعار جديد", - "create-new-key": "!ايجاد کليد جديد", - "duplicate-alias-error": ".نامهاي مستعار نمايش موجودي بايد در داشبورد، منحصر بفرد باشند
    .يافت شد '{{alias}}' نام مستعار تکراري", - "configure-alias": "'{{alias}}' پيکربندي نام مستعار", - "no-entity-views-matching": ".يافت نشد '{{entity}}' هيچ موجودي منطبق بر", - "alias": "نام مستعار", - "alias-required": ".نام مستعار نمايش موجودي مورد نياز است", - "remove-alias": "حذف نام مستعار نمايش موجودي", - "add-alias": "افزودن نام مستعار نمايش موجودي", - "name-starts-with": "نام نمايش موجودي شروع مي شود با", - "entity-view-list": "ليست نمايش موجودي", - "use-entity-view-name-filter": "استفاده از فيلتر", - "entity-view-list-empty": ".هيچ نمايش موجودي انتخاب نشد", - "entity-view-name-filter-required": ".فيلتر نام نمايش موجودي مورد نياز است", - "entity-view-name-filter-no-entity-view-matched": ".شروع شود يافت نشد '{{entityView}}' هيچ نمايش موجودي که با", - "add": "افزودن نمايش موجودي", - "assign-to-customer": "تخصيص به مشتري", - "assign-entity-view-to-customer": "تخصيص نمايش(هاي) موجودي به مشتري", - "assign-entity-view-to-customer-text": ".لطفا نمايش هاي موجودي را انتخاب کنيد تا به مشتري تخصيص يابند", - "no-entity-views-text": "هيچ نمايش موجودي يافت نشد", - "assign-to-customer-text": ".لطفا مشتري را انتخاب کنيد تا نمايش(هاي) موجودي تخصيص يابد", - "entity-view-details": "جزئيات نمايش موجودي", - "add-entity-view-text": "افزودن نمايش موجودي جديد", - "delete": "حذف نمايش موجودي", - "assign-entity-views": "تخصيص نمايش هاي موجودي", - "assign-entity-views-text": "به مشتري { count, plural, 1 {1 نمايش موجودي} other {# نمايش موجودي} } تخصيص", - "delete-entity-views": "حذف نمايش هاي موجودي", - "unassign-from-customer": "لغو تخصيص از مشتري", - "unassign-entity-views": "لغو تخصيص نمايش هاي موجودي", - "unassign-entity-views-action-title": "از مشتري { count, plural, 1 {1 نمايش موجودي} other {# نمايش موجودي} } لغو تخصيص", - "assign-new-entity-view": "تخصيص نمايش موجودي جديد", - "delete-entity-view-title": "مطمئنيد؟ '{{entityViewName}}' از حذف نمايش موجودي", - "delete-entity-view-text": ".مراقب باشيد، پس از تأييد، نمايش موجودي و تمامي داده هاي مربوطه، غير قابل بازيابي مي شوند", - "delete-entity-views-title": "مطمئنيد؟ { count, plural, 1 {1 نمايش موجودي} other {# نمايش موجودي} } از حذف نمايش موجودي", - "delete-entity-views-action-title": "{ count, plural, 1 {1 نمايش موجودي} other {# نمايش موجودي} } حذف", - "delete-entity-views-text": ".مراقب باشيد، پس از تأييد، تمام نمايش هاي موجوديِ انتخاب شده حذف، و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", - "unassign-entity-view-title": "مطمئنيد؟ '{{entityViewName}}' از لغو تخصيص نمايش موجودي", - "unassign-entity-view-text": ".پس از تأييد، نمايش موجودي، لغو تخصيص و خارج از دسترس مشتري مي شود", - "unassign-entity-view": "لغو تخصيص نمايش موجودي", - "unassign-entity-views-title": "مطمئنيد؟ { count, plural, 1 {1 نمايش موجودي} other {# نمايش موجودي} } از لغو تخصيص", - "unassign-entity-views-text": ".پس از تأييد، تمام نمايش هاي موجوديِ انتخاب شده، لغو تخصيص و خارج از دسترس مشتري مي شوند", - "entity-view-type": "نوع نمايش موجودي", - "entity-view-type-required": ".نوع نمايش موجودي مورد نياز است", - "select-entity-view-type": "انتخاب نوع نمايش موجودي", - "enter-entity-view-type": "وارد کردن نوع نمايش موجودي", - "any-entity-view": "هر نمايش موجودي", - "no-entity-view-types-matching": ".يافت نشد '{{entitySubtype}}' هيچ نوع نمايش موجودي منطبق بر", - "entity-view-type-list-empty": ".هيچ نوع نمايش موجودي انتخاب نشد", - "entity-view-types": "انواع نمايش موجودي", - "name": "نام", - "name-required": ".نام مورد نياز است", - "description": "توصيف", - "events": "رويدادها", - "details": "جزئيات", - "copyId": "نمايش موجودي ID رونوشت از", - "assignedToCustomer": "تخصيص يافته به مشتري", - "unable-entity-view-device-alias-title": ".حذف نام مستعار نمايش موجودي ممکن نيست", - "unable-entity-view-device-alias-text": "
    {{widgetsList}} :را تا زمان استفاده توسط ويجت(هاي) زير نمي توان حذف کرد ، '{{entityViewAlias}}' ،نام مستعار دستگاه", - "select-entity-view": "انتخاب نمايش موجودي", - "make-public": "عمومي سازي نمايش موجودي", - "start-date": "تاريخ شروع", - "start-ts": "زمان شروع", - "end-date": "تاريخ پايان", - "end-ts": "زمان پايان", - "date-limits": "محدوده تاريخ", - "client-attributes": "ويژگي هاي مشتري", - "shared-attributes": "ويژگي هاي مشترک", - "server-attributes": "ويژگي هاي سِروِر", - "timeseries": "سري هاي زماني", - "client-attributes-placeholder": "ويژگي هاي مشتري", - "shared-attributes-placeholder": "ويژگي هاي مشترک", - "server-attributes-placeholder": "ويژگي هاي سِروِر", - "timeseries-placeholder": "سري هاي زماني", - "target-entity": "موجودي هدف", - "attributes-propagation": "انتشار ويژگي ها", - "attributes-propagation-hint": "هر بار که شما نمايش موجودي را بروز رساني يا ذخيره مي کنيد، نمايش موجودي بصور خودکار ويژگي هاي تعيين شده را از موجودي هدف کپي مي کند و به دلايل عملکردي، ويژگي هاي موجودي هدف، با هر بار تغيير ويژگي، در نمايش موجودي انتشار نمي يابند. مي توانيد با پيکربندي گره قواعد در زنجيره قواعد خود و پيوند دهي \"Post attributes\" و \"Attributes Updated\" آن به گره قواعد جديد، انتشار خودکار \"copy to view\" را ممکن سازيد ." , - "timeseries-data": "داده ي سري هاي زماني", - "timeseries-data-hint": "کليدهاي داده ي سري هاي زمانيِ موجوديِ هدف را پيکربندي کنيد تا در دسترسِ نمايش موجودي باشند. اين سري هاي زماني، فقط خواندني است" - }, - "event": { - "event-type": "نوع رويداد", - "type-error": "خطا", - "type-lc-event": "رويداد چرخه عمر", - "type-stats": "آمار", - "type-debug-rule-node": "اشکال زدايي", - "type-debug-rule-chain": "اشکال زدايي", - "no-events-prompt": "هيچ رويدادي يافت نشد", - "error": "خطا", - "alarm": "هشدار", - "event-time": "زمان رويداد", - "server": "سِروِر", - "body": "بدنه", - "method": "روش", - "type": "نوع", - "entity": "موجودي", - "message-id": "پيام ID", - "message-type": "نوع پيام", - "data-type": "نوع داده", - "relation-type": "نوع ارتباط", - "metadata": "فرا داده", - "data": "داده", - "event": "رويداد", - "status": "وضعيت", - "success": "موفقيت", - "failed": "عدم موفقيت", - "messages-processed": "پيام پردازش شد", - "errors-occurred": "خطاها رخ دادند" - }, - "extension": { - "extensions": "دنباله ها", - "selected-extensions": "انتخاب شدند { count, plural, 1 {1 افزونه} other {افزونه ها #} }", - "type": "نوع", - "key": "کليد", - "value": "مقدار", - "id": "ID", - "extension-id": " افزونه ID", - "extension-type": "نوع افزونه", - "transformer-json": "JSON *", - "unique-id-required": ".افزونه فعلي موجود است ID در حال حاضر", - "delete": "حذف دنباله", - "add": "افزودن دنباله", - "edit": "ويرايش دنباله", - "delete-extension-title": "مطمئنيد؟ '{{extensionId}}' از حذف افزونه", - "delete-extension-text": ".مراقب باشيد، پس از تأييد، افزونه و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", - "delete-extensions-title": "مطمئنيد؟ { count, plural, 1 {1 افزونه} other {# افزونه} } از حذف", - "delete-extensions-text": ".مراقب باشيد، پس از تأييد، تمام افزونه هاي انتخاب شده حذف مي گردند", - "converters": "مبدّل ها", - "converter-id": "مبدّل ID", - "configuration": "پيکربندي", - "converter-configurations": "پيکربندي هاي مبدّل", - "token": "نشانه امنيت", - "add-converter": "افزودن مبدّل", - "add-config": "افزودن پيکربندي مبدّل", - "device-name-expression": "عبارت نام دستگاه", - "device-type-expression": "عبارت نوع دستگاه", - "custom": "متداول", - "to-double": "دو برابر شدن", - "transformer": "مبدّل", - "json-required": ".مبدّل مورد نياز است JSON", - "json-parse": ".مبدّل ممکن نيست JSON تجزيه", - "attributes": "ويژگي ها", - "add-attribute": "افزودن ويژگي", - "add-map": "افزودن جزء نگاشت", - "timeseries": "سري هاي زماني", - "add-timeseries": "افزودن سري هاي زماني", - "field-required": "دامنه مورد نياز است", - "brokers": "واسطه ها", - "add-broker": "افزودن واسطه", - "host": "ميزبان", - "port": "درگاه", - "port-range": ".درگاه بايد در بازه اي بين 1 تا 65535 باشد", - "ssl": "Ssl", - "credentials": "اعتبارنامه ها", - "username": "نام کاربري", - "password": "رمز عبور", - "retry-interval": "بازخواني فاصله در ميلي ثانيه", - "anonymous": "بي نام", - "basic": "پايه", - "pem": "PEM", - "ca-cert": "CA پرونده گواهينامه *", - "private-key": "پرونده کليد شخصي *", - "cert": "پرونده گواهينامه *", - "no-file": ".هيچ پرونده اي انتخاب نشد", - "drop-file": ".جهت بارگذاري يک پرونده، آن را با موس کِشيده و رها کنيد، و يا روي آن کليک نماييد", - "mapping": "نگاشت", - "topic-filter": "فيلتر عنوان", - "converter-type": "نوع مبدّل", - "converter-json": "JSON", - "json-name-expression": "نام دستگاه JSON عبارت", - "topic-name-expression": "عبارت عنوان نام دستگاه", - "json-type-expression": "نوع دستگاه JSON عبارت", - "topic-type-expression": "عبارت عنوان نوع دستگاه", - "attribute-key-expression": "عبارت کليد ويژگي", - "attr-json-key-expression": "کليد ويژگي JSON عبارت", - "attr-topic-key-expression": "عبارت عنوان کليد ويژگي", - "request-id-expression": "ID درخواست عبارت", - "request-id-json-expression": "ID JSON درخواست عبارت", - "request-id-topic-expression": "ID درخواست عبارت عنوان", - "response-topic-expression": "عبارت عنوان پاسخ", - "value-expression": "عبارت مقدار", - "topic": "عنوان", - "timeout": "وقفه در ميلي ثانيه", - "converter-json-required": ".مبدّل مورد نياز است JSON", - "converter-json-parse": ".مبدّل ممکن نيست JSON تجزيه", - "filter-expression": "عبارت فيلتر", - "connect-requests": "درخواست هاي اتصال", - "add-connect-request": "افزودن درخواست اتصال", - "disconnect-requests": "درخواست هاي قطع اتصال", - "add-disconnect-request": "افزودن درخواست قطع اتصال", - "attribute-requests": "درخواست هاي ويژگي", - "add-attribute-request": "افزودن درخواست ويژگي", - "attribute-updates": "به روز رساني هاي ويژگي ", - "add-attribute-update": "افزودن به روز رساني ويژگي ", - "server-side-rpc": "سَمت سِروِر RPC", - "add-server-side-rpc-request": "سَمت سِروِر RPC افزودن درخواست", - "device-name-filter": "فيلتر نام دستگاه", - "attribute-filter": "فيلتر ويژگي ", - "method-filter": "فيلتر روش", - "request-topic-expression": "عبارت عنوان درخواست", - "response-timeout": "وقفه پاسخ در ميلي ثانيه", - "topic-expression": "بيان موضوع", - "client-scope": "حوزه مشتري", - "add-device": "افزودن دستگاه", - "opc-server": "سِروِرها", - "opc-add-server": "افزودن سِروِر", - "opc-add-server-prompt": "لطفا سِروِر را اضافه کنيد", - "opc-application-name": "نام برنامه کاربردي", - "opc-application-uri": "برنامه کاربردي URI", - "opc-scan-period-in-seconds": "دوره پويش در ثانيه", - "opc-security": "امنيت", - "opc-identity": "هويت", - "opc-keystore": "کي استور", - "opc-type": "نوع", - "opc-keystore-type": "نوع", - "opc-keystore-location": "* موقعيت مکاني", - "opc-keystore-password": "رمز عبور", - "opc-keystore-alias": "نام مستعار", - "opc-keystore-key-password": "کليد رمز عبور", - "opc-device-node-pattern": "الگوي گره دستگاه", - "opc-device-name-pattern": "الگوي نام دستگاه", - "modbus-server": "سِروِرها/جايگزين آماده به کار", - "modbus-add-server": "افزودن سِروِر/ جايگزين آماده به کار ", - "modbus-add-server-prompt": "لطفا سِروِرها/جايگزين آماده به کار را اضافه کنيد", - "modbus-transport": "انتقال", - "modbus-port-name": "نام در گاه سريال", - "modbus-encoding": "رمز گذاري", - "modbus-parity": "توازن", - "modbus-baudrate": "نرخ علامت در ثانيه", - "modbus-databits": "بيت هاي داده", - "modbus-stopbits": "بيت هاي توقف", - "modbus-databits-range": ".بيت هاي داده بايد در بازه اي بين 7 تا 8 باشند", - "modbus-stopbits-range": ".بيت هاي توقف بايد در بازه اي بين 1 تا 2 باشند", - "modbus-unit-id": "واحد ID", - "modbus-unit-id-range": ".واحد بايد در بازه اي بين 1 تا 247 باشد ID", - "modbus-device-name": "نام دستگاه", - "modbus-poll-period": "(ms) دوره نمونه برداري", - "modbus-attributes-poll-period": "(ms) دوره نمونه برداري ويژگي ها", - "modbus-timeseries-poll-period": "(ms) دوره نمونه برداري سري هاي زماني", - "modbus-poll-period-range": ".دوره نمونه برداري بايد مقداري مثبت باشد", - "modbus-tag": "برچسب", - "modbus-function": "تابع", - "modbus-register-address": "ثبت نام نشاني", - "modbus-register-address-range": ".نشاني ثبت بايد در بازه اي بين 0 تا 65535 باشد", - "modbus-register-bit-index": "شاخص بيت", - "modbus-register-bit-index-range": ".شاخص بيت بايد در بازه اي بين 0 تا 15 باشد", - "modbus-register-count": "شمارش ثبت", - "modbus-register-count-range": ".شمارش ثبت بايد مقداري مثبت باشد", - "modbus-byte-order": "ترتيب بايت", - - "sync": { - "status": "وضعيت", - "sync": "همگام", - "not-sync": "غير همگام", - "last-sync-time": "آخرين زمان همگام سازي", - "not-available": "خارج از دسترس" - }, - - "export-extensions-configuration": "صدور پيکربندي افزونه ها", - "import-extensions-configuration": "وارد کردن پيکربندي افزونه ها", - "import-extensions": "وارد کردن افزونه ها", - "import-extension": "وارد کردن افزونه", - "export-extension": "صدور افزونه", - "file": "پرونده افزونه ها", - "invalid-file-error": "پرونده افزونه نامعتبر است" - }, - "fullscreen": { - "expand": "بسط به حالت تمام صفحه", - "exit": "خروج از حالت تمام صفحه", - "toggle": "تغيير حالت تمام صفحه", - "fullscreen": "حالت تمام صفحه" - }, - "function": { - "function": "تابع" - }, - "grid": { - "delete-item-title": "از حذف اين مورد مطمئنيد؟", - "delete-item-text": ".مراقب باشيد، پس از تأييد، اين مورد و تمامي داده هاي مربوطه غيرقابل بازيابي مي شوند", - "delete-items-title": "مطمئنيد؟ { count, plural, 1 {1 مورد} other {# مورد} } از حذف", - "delete-items-action-title": "{ count, plural, 1 {1 مورد} other {# مورد} } حذف", - "delete-items-text": ".مراقب باشيد، پس از تأييد، تمام مواردِ انتخاب شده حذف، و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", - "add-item-text": "افزودن مورد جديد", - "no-items-text": "هيچ موردي يافت نشد", - "item-details": "جزئيات مورد", - "delete-item": "حذف مورد", - "delete-items": "حذف موارد", - "scroll-to-top": "پيمايش به بالا" - }, - "help": { - "goto-help-page": "رفتن به صفحه کمک" - }, - "home": { - "home": "خانه", - "profile": "پرونده شخصي", - "logout": "خروج", - "menu": "فهرست انتخاب", - "avatar": "آواتار", - "open-user-menu": "بازکردن فهرست انتخاب کاربر" - }, - "import": { - "no-file": "هيچ پرونده اي انتخاب نشد", - "drop-file": ".آن را با موس کِشيده و رها کنيد، و يا روي آن کليک نماييد ،JSON جهت بارگذاري يک پرونده" - }, - "item": { - "selected": "انتخاب شده" - }, - "js-func": { - "no-return-error": "!تابع بايد مقدار را برگرداند", - "return-type-mismatch": "!را برگرداند '{{type}}' تابع بايد مقدار نوع", - "tidy": "مرتب" - }, - "key-val": { - "key": "کليد", - "value": "مقدار", - "remove-entry": "حذف ورودي", - "add-entry": "افزودن ورودي", - "no-data": "هيچ ورودي وجود ندارد" - }, - "layout": { - "layout": "طرح بندي", - "manage": "مديريت طرح بندي ها", - "settings": "تنظيمات طرح بندي", - "color": "رنگ", - "main": "اصلي", - "right": "راست", - "select": "انتخاب طرح بندي هدف" - }, - "legend": { - "position": "محل فهرست علائم", - "show-max": "نمايش بيشترين مقدار", - "show-min": "نمايش کمترين مقدار", - "show-avg": "نمايش مقدار ميانگين", - "show-total": "نمايش مقدار مجموع", - "settings": "تنظيمات فهرست علائم", - "min": "کمترين", - "max": "بيشترين", - "avg": "ميانگين", - "total": "مجموع" - }, - "login": { - "login": "ورود", - "request-password-reset": "درخواست بازنشاني رمز عبور", - "reset-password": "بازنشاني رمز عبور", - "create-password": "ايجاد رمز عبور", - "passwords-mismatch-error": "!رمزهاي عبور وارد شده بايد مشابه باشند", - "password-again": "رمز عبور دوباره", - "sign-in": "لطفا وارد شويد", - "username": "(نام کاربري (پست الکترونيک", - "remember-me": "مرا به خاطر داشته باش", - "forgot-password": "رمز عبور را فراموش کرده ايد؟", - "password-reset": "بازنشاني رمز عبور", - "new-password": "رمز عبور جديد", - "new-password-again": "رمز عبور جديد دوباره", - "password-link-sent-message": "!پيوند بازنشاني رمز عبور با موفقيت ارسال شد", - "email": "پست الکترونيک" - }, - "position": { - "top": "بالا", - "bottom": "پايين", - "left": "چپ", - "right": "راست" - }, - "profile": { - "profile": "پرونده شخصي", - "change-password": "تغيير رمز عبور", - "current-password": "رمز عبور فعلي" - }, - "relation": { - "relations": "ارتباطات", - "direction": "جهت", - "search-direction": { - "FROM": "از", - "TO": "به" - }, - "direction-type": { - "FROM": "از", - "TO": "به" - }, - "from-relations": "ارتباطات خارج از محدوده", - "to-relations": "ارتباطات داخل محدوده", - "selected-relations": "انتخاب شدند { count, plural, 1 {1 ارتباط} other {ارتباط #} }", - "type": "نوع", - "to-entity-type": "به نوع موجودي", - "to-entity-name": "به نام موجودي", - "from-entity-type": "از نوع موجودي", - "from-entity-name": "از نام موجودي", - "to-entity": "به موجودي", - "from-entity": "از موجودي", - "delete": "حذف ارتباط", - "relation-type": "نوع ارتباط", - "relation-type-required": ".نوع ارتباط مورد نياز است", - "any-relation-type": "هر نوع", - "add": "افزودن ارتباط", - "edit": "ويرايش ارتباط", - "delete-to-relation-title": "مطمئنيد؟ '{{entityName}}' از حذف ارتباط با موجودي", - "delete-to-relation-text": ".غيرمرتبط با موجودي فعلي مي شود '{{entityName}}' مراقب باشيد، پس از تأييد، موجودي", - "delete-to-relations-title": "مطمئنيد؟ { count, plural, 1 {1 ارتباط} other {# ارتباط} } از حذف", - "delete-to-relations-text": ".مراقب باشيد، پس از تأييد، تمام روابطِ انتخاب شده حذف، و موجودي هاي مربوطه، غيرمرتبط با موجودي فعلي مي شوند", - "delete-from-relation-title": "مطمئنيد؟ '{{entityName}}' از حذف ارتباط از موجودي", - "delete-from-relation-text": ".مي شود '{{entityName}}' مراقب باشيد، پس از تأييد، موجودي فعلي، غيرمرتبط از جانب موجودي", - "delete-from-relations-title": "مطمئنيد؟ { count, plural, 1 {1 ارتباط} other {# ارتباط} } از حذف", - "delete-from-relations-text": ".مراقب باشيد، پس از تأييد، تمام روابطِ انتخاب شده حذف، و موجودي فعلي، غيرمرتبط از جانب موجودي هاي مربوطه مي شود", - "remove-relation-filter": "حذف فيلتر ارتباط", - "add-relation-filter": "افزودن فيلتر ارتباط", - "any-relation": "هر ارتباط", - "relation-filters": "فيلترهاي ارتباط", - "additional-info": "(JSON) اطلاعات تکميلي", - "invalid-additional-info": "اطلاعات تکميلي ممکن نيست JSON تجزيه" - }, - "rulechain": { - "rulechain": "زنجيره قواعد", - "rulechains": "زنجيره هاي قواعد", - "root": "پايه", - "delete": "حذف زنجيره قواعد", - "name": "نام", - "name-required": ".نام مورد نياز است", - "description": "توصيف", - "add": "افزودن زنجيره قواعد", - "set-root": "ريشه زنجيره قواعد را ايجاد کنيد", - "set-root-rulechain-title": "به عنوان ريشه مطمئنيد؟ '{{ruleChainName}}' از قرار دادن زنجيره قواعد", - "set-root-rulechain-text": ".پس از تأييد، زنجيره قواعد، به عنوان ريشه تعيين شده و به تمام پيامهاي انتقالي رسيدگي مي کند", - "delete-rulechain-title": "مطمئنيد؟ '{{ruleChainName}}' از حذف زنجيره قواعد", - "delete-rulechain-text": ".مراقب باشيد، پس از تأييد، زنجيره قواعد و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", - "delete-rulechains-title": "مطمئنيد؟ { count, plural, 1 {1 زنجيره قواعد} other {# زنجيره قواعد} } از حذف", - "delete-rulechains-action-title": "{ count, plural, 1 {1 زنجيره قواعد} other {# زنجيره قواعد} } حذف", - "delete-rulechains-text": ".مراقب باشيد، پس از تأييد، تمام زنجيره هاي قواعدِ انتخاب شده حذف، و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", - "add-rulechain-text": "افزودن زنجيره قواعد جديد", - "no-rulechains-text": "هيچ زنجيره قواعدي يافت نشد", - "rulechain-details": "جزئيات زنجيره قواعد", - "details": "جزئيات", - "events": "رويدادها", - "system": "سيستم", - "import": "وارد کردن زنجيره قواعد", - "export": "صدور زنجيره قواعد", - "export-failed-error": "{{error}} :صدور زنجيره قواعد ممکن نيست", - "create-new-rulechain": "ايجاد زنجيره قواعد جديد", - "rulechain-file": "پرونده زنجيره قواعد", - "invalid-rulechain-file-error": ".وارد کردن زنجيره قواعد ممکن نيست: ساختار داده زنجيره قواعد نامعتبر است", - "copyId": "زنجيره قواعد ID رونوشت", - "idCopiedMessage": "زنجيره قواعد در حافظه موقت رونوشت شد ID", - "select-rulechain": "انتخاب زنجيره قواعد", - "no-rulechains-matching": ".يافت نشد '{{entity}}' هيچ زنجيره قواعدي منطبق بر", - "rulechain-required": "زنجيره قواعد مورد نياز است", - "management": "مديريت قواعد", - "debug-mode": "حالت اشکال زدايي" - }, - "rulenode": { - "details": "جزئيات", - "events": "رويدادها", - "search": "جستجوي گره ها", - "open-node-library": "باز کردن کتابخانه گره ها", - "add": "افزودن گره قواعد", - "name": "نام", - "name-required": ".نام مورد نياز است", - "type": "نوع", - "description": "توصيف", - "delete": "حذف گره قواعد", - "select-all-objects": "انتخاب تمام گره ها و اتصالات", - "deselect-all-objects": "لغو انتخاب تمام گره ها و اتصالات", - "delete-selected-objects": "حذف گره ها و اتصالاتِ انتخاب شده", - "delete-selected": "حذفِ انتخاب شده", - "select-all": "انتخاب همه", - "copy-selected": "رونوشتِ انتخاب شده", - "deselect-all": "لغو انتخاب همه", - "rulenode-details": "جزئيات گره قواعد", - "debug-mode": "حالت اشکال زدايي", - "configuration": "پيکربندي", - "link": "پيوند", - "link-details": "جزئيات پيوند گره قواعد", - "add-link": "افزودن پيوند", - "link-label": "برچسب پيوند", - "link-label-required": ".برچسب پيوند مورد نياز است", - "custom-link-label": "برچسب پيوند متداول", - "custom-link-label-required": ".برچسب پيوند متداول مورد نياز است", - "link-labels": "برچسب هاي پيوند", - "link-labels-required": ".برچسب هاي پيوند مورد نيازند", - "no-link-labels-found": "هيچ برچسب پيوندي يافت نشد", - "no-link-label-matching": ".پيدا نشد'{{label}}'", - "create-new-link-label": "!برچسب پيوند ايجاد کنيد", - "type-filter": "فيلتر", - "type-filter-details": "پيام هاي ورودي را با شرايط پيکربندي فيلتر کنيد", - "type-enrichment": "افزودن", - "type-enrichment-details": "افزودن اطلاعات تکميلي به فرا داده ي پيام", - "type-transformation": "تبديل", - "type-transformation-details": "تغيير بازده و فرا داده ي پيام", - "type-action": "اقدام", - "type-action-details": "انجام اقدام ويژه", - "type-external": "خارجي", - "type-external-details": "ارتباط متقابل با سيستم خارجي", - "type-rule-chain": "زنجيره قواعد", - "type-rule-chain-details": "ارسال پيامهاي وارده به زنجيره قواعدي مشخص", - "type-input": "ورودي", - "type-input-details": "ورودي منطقي زنجيره قواعد، پيامهاي ورودي را به گره قواعد مرتبط بعدي ارسال مي کند", - "type-unknown": "ناشناخته", - "type-unknown-details": "گره قواعدِ حل نشده", - "directive-is-not-loaded": ".در دسترس نيست '{{directiveName}}' دستورالعمل پيکربنديِ مشخص شده", - "ui-resources-load-error": ".پيکربندي UI عدم موفقيت در بارگذاري منابع", - "invalid-target-rulechain": "!حلّ زنجيره قواعد هدف ممکن نيست", - "test-script-function": "آزمايش تابع اسکريپت", - "message": "پيام", - "message-type": "نوع پيام", - "select-message-type": "انتخاب نوع پيام", - "message-type-required": "نوع پيام مورد نياز است", - "metadata": "فوق داده", - "metadata-required": ".ورودي هاي فرا داده نمي تواند خالي باشد", - "output": "خروجي", - "test": "آزمايش", - "help": "کمک" - }, - "tenant": { - "tenant": "کاربر", - "tenants": "کاربران", - "management": "مديريت کاربران", - "add": "افزودن کاربر", - "admins": "سرپرستان", - "manage-tenant-admins": "مديريت مديران کاربر", - "delete": "حذف کاربر", - "add-tenant-text": "افزودن کاربر جديد", - "no-tenants-text": "هيچ کاربري يافت نشد", - "tenant-details": "جزئيات کاربر", - "delete-tenant-title": "مطمئنيد؟ '{{tenantTitle}}' از حذف کاربر", - "delete-tenant-text": ".مراقب باشيد، پس از تأييد، کاربر و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", - "delete-tenants-title": "مطمئنيد؟ { count, plural, 1 {1 کاربر} other {# کاربر} } از حذف", - "delete-tenants-action-title": "{ count, plural, 1 {1 کاربر} other {# کاربر} } حذف", - "delete-tenants-text": ".مراقب باشيد، پس از تأييد، تمام کاربران حذف، و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", - "title": "عنوان", - "title-required": ".عنوان مورد نياز است", - "description": "توصيف", - "details": "جزئيات", - "events": "رويدادها", - "copyId": "متصرّف ID رونوشت", - "idCopiedMessage": "کاربر در حافظه موقت رونوشت شد ID", - "select-tenant": "انتخاب کاربر", - "no-tenants-matching": ".يافت نشد '{{entity}}' هيچ کاربري منطبق بر", - "tenant-required": "کاربر مورد نياز است" - }, - "timeinterval": { - "seconds-interval": "{ seconds, plural, 1 {1 ثانيه} other {ثانيه #} }", - "minutes-interval": "{ minutes, plural, 1 {1 دقيقه} other {دقيقه #} }", - "hours-interval": "{ hours, plural, 1 {1 ساعت} other {ساعت #} }", - "days-interval": "{ days, plural, 1 {1 روز} other {روز #} }", - "days": "روزها", - "hours": "ساعات", - "minutes": "دقايق", - "seconds": "ثانيه ها", - "advanced": "پيشرفته" - }, - "timewindow": { - "days": "{ days, plural, 1 { روز } other {روز #} }", - "hours": "{ hours, plural, 0 { 1ساعت } 1 { ساعت } other {# ساعت } }", - "minutes": "{ minutes, plural, 0 { 1دقيقه } 1 { دقيقه } other {دقيقه # } }", - "seconds": "{ seconds, plural, 0 { 1ثانيه } 1 { ثانيه } other {ثانيه # } }", - "realtime": "بي درنگ", - "history": "تاريخچه", - "last-prefix": "آخرين", - "period": "{{ endTime }} تا {{ startTime }} از", - "edit": "ويرايش پنجره زماني", - "date-range": "بازه داده", - "last": "آخرين", - "time-period": "دوره زماني" - }, - "user": { - "user": "کاربر", - "users": "کاربرها", - "customer-users": "کاربرهاي مشتري", - "tenant-admins": "مديران کاربر", - "sys-admin": "مدير سيستم", - "tenant-admin": "کاربر مدير", - "customer": "مشتري", - "anonymous": "بي نام", - "add": "افزودن کاربر", - "delete": "حذف کاربر", - "add-user-text": "افزودن کاربر جديد", - "no-users-text": "هيچ کاربري يافت نشد", - "user-details": "جزئيات کاربر", - "delete-user-title": "مطمئنيد؟ '{{userEmail}}' از حذف کاربر", - "delete-user-text": ".مراقب باشيد، پس از تأييد، کاربر و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", - "delete-users-title": "مطمئنيد؟ { count, plural, 1 {1 کاربر} other {# کاربر} } از حذف", - "delete-users-action-title": "{ count, plural, 1 {1 کاربر} other {کاربر #} } حذف", - "delete-users-text": ".مراقب باشيد، پس از تأييد، تمام کاربرهاي انتخاب شده حذف، و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", - "activation-email-sent-message": "!پست الکترونيک فعال سازي با موفقيت ارسال شد", - "resend-activation": "ارسال مجدد فعال سازي", - "email": "پست الکترونيک", - "email-required": ".پست الکترونيک مورد نياز است", - "invalid-email-format": ".قالب نامعتبر پست الکترونيک", - "first-name": "نام", - "last-name": "نام خانوادگي", - "description": "توصيف", - "default-dashboard": "داشبورد پيش فرض", - "always-fullscreen": "همواره در حالت تمام صفحه", - "select-user": "انتخاب کاربر", - "no-users-matching": ".يافت نشد '{{entity}}' هيچ کاربري منطبق بر", - "user-required": "کاربر مورد نياز است", - "activation-method": "روش فعال سازي", - "display-activation-link": "نمايش پيوند فعال سازي", - "send-activation-mail": "ارسال پست الکترونيک فعال سازي", - "activation-link": "پيوند فعال سازي کاربر", - "activation-link-text": ":
    استفاده کنيد جهت فعال سازي کاربر، از پيوند فعال سازي زير", - "copy-activation-link": "رونوشت پيوند فعال سازي", - "activation-link-copied-message": "پيوند فعال سازي کاربر در حافظه موقت رونوشت شد", - "details": "جزئيات", - "login-as-tenant-admin": "ورود به عنوان کاربر مدير", - "login-as-customer-user": "ورود به عنوان کاربر مشتري" - }, - "value": { - "type": "نوع مقدار", - "string": "رشته", - "string-value": "مقدار رشته", - "integer": "عدد صحيح", - "integer-value": "مقدار عدد صحيح", - "invalid-integer-value": "عدد صحيح نامعتبر", - "double": "دو برابر", - "double-value": "مقدار دو برابر", - "boolean": "بولين", - "boolean-value": "مقدار بولين", - "false": "نادرست", - "true": "صحيح", - "long": "بلند" - }, - "widget": { - "widget-library": "کتابخانه ويجت ها", - "widget-bundle": "بسته ويجت", - "select-widgets-bundle": "انتخاب بسته ويجت", - "management": "مديريت ويجت", - "editor": "ويرايشگر ويجت", - "widget-type-not-found": ".نوع ويجت حذف شد \n احتمالا مرتبط است
    .مشکل بارگذاري پيکربندي ويجت", - "widget-type-load-error": ":ويجت، به علت خطاهاي زير، بارگذاري نشد", - "remove": "حذف ويجت", - "edit": "ويرايش ويجت", - "remove-widget-title": "مطمئنيد؟ '{{widgetTitle}}' از حذف ويجت", - "remove-widget-text": ".پس از تأييد، ويجت و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", - "timeseries": "سري هاي زماني", - "search-data": "جستجوي داده", - "no-data-found": "هيچ داده اي يافت نشد", - "latest-values": "آخرين مقادير", - "rpc": "ويجت کنترل", - "alarm": "ويجت هشدار", - "static": "ويجت ايستا", - "select-widget-type": "انتخاب نوع ويجت", - "missing-widget-title-error": "!عنوان ويجت بايد مشخص شود", - "widget-saved": "ويجت ذخيره شد", - "unable-to-save-widget-error": "!ذخيره سازي ويجت ممکن نيست! ويجت خطاهايي دارد", - "save": "ذخيره سازي ويجت", - "saveAs": "ذخيره سازي ويجت به عنوان", - "save-widget-type-as": "ذخيره سازي نوع ويجت به عنوان", - "save-widget-type-as-text": "لطفا عنوان ويجت جديد را وارد کنيد و/يا بسته ويجت هدف را انتخاب نماييد", - "toggle-fullscreen": "حالت تمام صفحه را تغيير دهيد", - "run": "اجراي ويجت", - "title": "عنوان ويجت", - "title-required": ".عنوان ويجت مورد نياز است", - "type": "نوع ويجت", - "resources": "منابع", - "resource-url": "JavaScript/CSS URL", - "remove-resource": "حذف منبع", - "add-resource": "افزودن منبع", - "html": "HTML", - "tidy": "مرتب", - "css": "CSS", - "settings-schema": "طرح تنظيمات", - "datakey-settings-schema": "طرح تنظيمات کليد داده", - "javascript": "Javascript", - "remove-widget-type-title": "مطمئنيد؟ '{{widgetName}}' از حذف ويجت نوع", - "remove-widget-type-text": ".پس از تأييد، نوع ويجت و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", - "remove-widget-type": "حذف نوع ويجت", - "add-widget-type": "افزودن نوع ويجت جديد", - "widget-type-load-failed-error": "!عدم موفقيت در بارگذاري نوع ويجت", - "widget-template-load-failed-error": "!عدم موفقيت در بارگذاري قالب ويجت", - "add": "افزودن ويجت", - "undo": "برگرداندن تغييرات ويجت", - "export": "صدور ويجت" - }, - "widget-action": { - "header-button": "دکمه هدر ويجت", - "open-dashboard-state": "هدايت به وضعيت داشبورد جديد", - "update-dashboard-state": "به روز رساني وضعيت داشبورد فعلي", - "open-dashboard": "هدايت به داشبورد ديگر", - "custom": "اقدام متداول", - "target-dashboard-state": "وضعيت داشبورد هدف", - "target-dashboard-state-required": "وضعيت داشبورد هدف مورد نياز است", - "set-entity-from-widget": "تنظيم موجودي از ويجت", - "target-dashboard": "داشبورد هدف", - "open-right-layout": "(طرح داشبورد سمت راست را باز کنيد (نماي تلفن همراه" - }, - "widgets-bundle": { - "current": "بسته فعلي", - "widgets-bundles": "بسته هاي ويجت", - "add": "افزودن بسته ويجت", - "delete": "حذف بسته ويجت", - "title": "عنوان", - "title-required": ".عنوان مورد نياز است", - "add-widgets-bundle-text": "افزودن بسته ويجت جديد", - "no-widgets-bundles-text": "هيچ بسته ويجتي يافت نشد", - "empty": "بسته ويجت خالي است", - "details": "جزئيات", - "widgets-bundle-details": "جزئيات بسته ويجت", - "delete-widgets-bundle-title": "مطمئنيد؟ '{{widgetsBundleTitle}}' از حذف بسته ويجت", - "delete-widgets-bundle-text": ".مراقب باشيد، پس از تأييد، بسته ويجت و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", - "delete-widgets-bundles-title": "مطمئنيد؟ { count, plural, 1 {1 بسته ويجت} other {# بسته ويجت} } از حذف", - "delete-widgets-bundles-action-title": "{ count, plural, 1 {1 بسته ويجت} other {# بسته ويجت} } حذف", - "delete-widgets-bundles-text": ".مراقب باشيد، پس از تأييد، تمام بسته هاي ويجتِ انتخاب شده حذف، و تمامي داده هاي مربوطه غير قابل بازيابي مي شوند", - "no-widgets-bundles-matching": "يافت نشد '{{widgetsBundle}}' هيچ بسته ويجتي منطبق بر", - "widgets-bundle-required": ".بسته ويجت مورد نياز است", - "system": "سيستم", - "import": "وارد کردن بسته ويجت", - "export": "صدور بسته ويجت", - "export-failed-error": "{{error}} :صدور بسته ويجت ممکن نيست", - "create-new-widgets-bundle": "ايجاد بسته ويجت جديد", - "widgets-bundle-file": "پرونده بسته ويجت", - "invalid-widgets-bundle-file-error": ".وارد کردن بسته ويجت ممکن نيست: ساختار داده بسته ويجت نامعتبر است" - }, - "widget-config": { - "data": "داده", - "settings": "تنظيمات", - "advanced": "پيشرفته", - "title": "عنوان", - "general-settings": "تنظيمات عمومي", - "display-title": "نمايش عنوان", - "drop-shadow": "سايه افتادن", - "enable-fullscreen": "فعال سازي حالت تمام صفحه", - "background-color": "رنگ پس زمينه", - "text-color": "رنگ متن", - "padding": "حاشيه داخلي", - "margin": "حاشيه", - "widget-style": "سبک ويجت", - "title-style": "سبک عنوان", - "mobile-mode-settings": "تنظيمات حالت تلفن همراه", - "order": "ترتيب", - "height": "ارتفاع", - "units": "کارکتر خاص براي نمايش بعد از مقدار تعين شده", - "decimals": "تعداد ارقام بعد از مميّز شناور", - "timewindow": "پنجره زمان", - "use-dashboard-timewindow": "استفاده از پنجره زمان داشبورد", - "display-legend": "نمايش فهرست علائم", - "datasources": "منابع داده", - "maximum-datasources": "{ count, plural, 1 {.1 منبع داده مجاز است} other {# منبع داده مجازند.} } بيشترين", - "datasource-type": "نوع", - "datasource-parameters": "پارامترها", - "remove-datasource": "حذف منبع داده", - "add-datasource": "افزودن منبع داده", - "target-device": "دستگاه هدف", - "alarm-source": "منشأ هشدار", - "actions": "اقدامات", - "action": "اقدام", - "add-action": "افزودن اقدام", - "search-actions": "جستجوي اقدامات", - "action-source": "منشأ اقدام", - "action-source-required": ".منشأ اقدام مورد نياز است", - "action-name": "نام", - "action-name-required": ".نام اقدام مورد نياز است", - "action-name-not-unique": ".در حيطه يک منشأ اقدام، نام اقدام بايد منحصر بفرد باشد
    .در حال حاضر اقدامي ديگر با نام مشابه موجود است", - "action-icon": "شمايل", - "action-type": "نوع", - "action-type-required": ".نوع اقدام مورد نياز است", - "edit-action": "ويرايش اقدام", - "delete-action": "حذف اقدام", - "delete-action-title": "حذف اقدام ويجت", - "delete-action-text": "مطمئنيد؟ '{{actionName}}' از حذف اقدام ويجت با نام" - }, - "widget-type": { - "import": "وارد کردن نوع ويجت", - "export": "صدور نوع ويجت", - "export-failed-error": "{{error}} :صدور نوع ويجت ممکن نيست", - "create-new-widget-type": "ايجاد نوع جديد ويجت", - "widget-type-file": "پرونده نوع ويجت", - "invalid-widget-type-file-error": ".وارد کردن نوع ويجت ممکن نيست: ساختار داده نوع ويجت نامعتبر است" - }, - "widgets": { - "date-range-navigator": { - "localizationMap": { - "Sun": "یکشنبه", - "Mon": "دوشنبه", - "Tue": "سه‌شنبه", - "Wed": "چهارشنبه", - "Thu": "پنجشنبه", - "Fri": "جمعه", - "Sat": "شنبه", - "Jan": "ژانویهٔ", - "Feb": "فوریهٔ", - "Mar": "مارس", - "Apr": "آوریل", - "May": "مهٔ", - "Jun": "ژوئن", - "Jul": "ژوئیهٔ", - "Aug": "اوت", - "Sep": "سپتامبر", - "Oct": "اکتبر", - "Nov": "نوامبر", - "Dec": "دسامبر", - "January": "January", - "February": "February", - "March": "March", - "April": "April", - "June": "June", - "July": "July", - "August": "August", - "September": "September", - "October": "October", - "November": "November", - "December": "December", - "Custom Date Range": "Custom Date Range", - "Date Range Template": "Date Range Template", - "Today": "Today", - "Yesterday": "Yesterday", - "This Week": "This Week", - "Last Week": "Last Week", - "This Month": "This Month", - "Last Month": "Last Month", - "Year": "Year", - "This Year": "This Year", - "Last Year": "Last Year", - "Date picker": "Date picker", - "Hour": "Hour", - "Day": "Day", - "Week": "Week", - "2 weeks": "2 weeks", - "Month": "Month", - "3 months": "3 months", - "6 months": "6 months", - "Custom interval": "Custom interval", - "Interval": "Interval", - "Step size": "Step size", - "Ok": "Ok" - } - } - }, - "icon": { - "icon": "آيکون", - "select-icon": "انتخاب آيکون", - "material-icons": "آيکونهاي اجسام", - "show-all": "نمايش تمام آيکونها" - }, - "custom": { - "widget-action": { - "action-cell-button": "دکمه سلول عملياتي", - "row-click": "در رديف کليک کنيد", - "marker-click": "روي نشانگر کليک کنيد", - "tooltip-tag-action": "اقدام برچسب راهنماي ابزار" - } - }, - "language": { - "language": "زبان" - } -} diff --git a/ui/src/app/locale/locale.constant-fr_FR.json b/ui/src/app/locale/locale.constant-fr_FR.json deleted file mode 100644 index 274063445f..0000000000 --- a/ui/src/app/locale/locale.constant-fr_FR.json +++ /dev/null @@ -1,1720 +0,0 @@ -{ - "access": { - "access-forbidden": "Accès interdit", - "access-forbidden-text": "Vous n'avez pas accès à cet emplacement!
    Essayez de vous connecter avec un autre utilisateur si vous souhaitez toujours accéder à cet emplacement.", - "refresh-token-expired": "La session a expiré", - "refresh-token-failed": "Impossible de rafraîchir la session", - "unauthorized": "non autorisé", - "unauthorized-access": "accès non autorisé", - "unauthorized-access-text": "Vous devez vous connecter pour avoir accès à cette ressource!" - }, - "action": { - "activate": "Activer", - "add": "Ajouter", - "apply": "Appliquer", - "apply-changes": "Appliquer les modifications", - "assign": "Attribuer", - "back": "retour", - "cancel": "Annuler", - "clear-search": "Effacer la recherche", - "close": "Fermer", - "continue": "Continue", - "copy": "Copier", - "copy-reference": "Copier la référence", - "create": "Créer", - "decline-changes": "Refuser les modifications", - "delete": "Supprimer", - "discard-changes": "Annuler les modifications", - "drag": "Drag", - "edit": "Modifier", - "edit-mode": "Mode édition", - "enter-edit-mode": "Entrer en mode édition", - "export": "Exporter", - "import": "Importer", - "make-private": "Rendre privé", - "no": "Non", - "ok": "OK", - "paste": "coller", - "paste-reference": "Coller référence", - "refresh": "Rafraîchir", - "remove": "Supprimer", - "run": "Exécuter", - "save": "Enregistrer", - "saveAs": "Enregistrer sous", - "search": "Rechercher", - "share": "Partager", - "share-via": "Partager via {{provider}}", - "sign-in": "Connectez-vous!", - "suspend": "Suspendre", - "unassign": "Retirer", - "undo": "Annuler", - "update": "mise à jour", - "view": "Afficher", - "yes": "Oui" - }, - "admin": { - "base-url": "URL de base", - "base-url-required": "L'URL de base est requise.", - "enable-tls": "Activer TLS", - "tls-version": "Version TLS", - "general": "Général", - "general-settings": "Paramètres généraux", - "mail-from": "Mail de", - "mail-from-required": "Mail de est requis.", - "outgoing-mail": "courrier sortant", - "outgoing-mail-settings": "Paramètres de courrier sortant", - "send-test-mail": "Envoyer un mail de test", - "smtp-host": "Hôte SMTP", - "smtp-host-required": "L'hôte SMTP est requis.", - "smtp-port": "Port SMTP", - "smtp-port-invalid": "Cela ne ressemble pas à un port smtp valide.", - "smtp-port-required": "Vous devez fournir un port smtp.", - "smtp-protocol": "Protocole SMTP", - "system-settings": "Paramètres système", - "test-mail-sent": "Le courrier de test a été envoyé avec succés!", - "timeout-invalid": "Cela ne ressemble pas à un délai d'expiration valide.", - "timeout-msec": "Délai (msec)", - "timeout-required": "Le délai est requis.", - "security-settings": "Les paramètres de sécurité", - "password-policy": "Politique de mot de passe", - "minimum-password-length": "Longueur minimale du mot de passe", - "minimum-password-length-required": "La longueur minimale du mot de passe est requise", - "minimum-password-length-range": "La longueur minimale du mot de passe doit être comprise entre 5 et 50.", - "minimum-uppercase-letters": "Nombre minimum de lettres majuscules", - "minimum-uppercase-letters-range": "Le nombre minimum de lettres majuscules ne peut pas être négatif", - "minimum-lowercase-letters": "Nombre minimum de lettres minuscules", - "minimum-lowercase-letters-range": "Le nombre minimum de lettres minuscules ne peut pas être négatif", - "minimum-digits": "Nombre minimum de chiffres", - "minimum-digits-range": "Le nombre minimum de chiffres ne peut pas être négatif", - "minimum-special-characters": "Nombre minimum de caractères spéciaux", - "minimum-special-characters-range": "Le nombre minimum de caractères spéciaux ne peut pas être négatif", - "password-expiration-period-days": "Délai d'expiration du mot de passe en jours", - "password-expiration-period-days-range": "La période d'expiration du mot de passe en jours ne peut pas être négative", - "password-reuse-frequency-days": "Fréquence de réutilisation du mot de passe en jours", - "password-reuse-frequency-days-range": "La fréquence de réutilisation du mot de passe en jours ne peut être négative", - "general-policy": "Politique générale", - "max-failed-login-attempts": "Nombre maximal de tentatives de connexion infructueuses avant que le compte ne soit verrouillé", - "minimum-max-failed-login-attempts-range": "Le nombre maximal de tentatives de connexion ayant échoué ne peut pas être négatif", - "user-lockout-notification-email": "En cas de verrouillage du compte d'utilisateur, envoyez une notification par courrier électronique." - }, - "aggregation": { - "aggregation": "agrégation", - "avg": "Moyenne", - "count": "Compte", - "function": "Fonction d'agrégation de données", - "group-interval": "Intervalle de regroupement", - "limit": "Valeurs maximales", - "max": "Max", - "min": "Min", - "none": "Aucune", - "sum": "Somme" - }, - "alarm": { - "ack-time": "Heure d'acquittement", - "acknowledge": "Acquitter", - "aknowledge-alarm-text": "Êtes-vous sûr de vouloir reconnaître l'alarme?", - "aknowledge-alarm-title": "Reconnaître l'alarme", - "aknowledge-alarms-text": "Êtes-vous sûr de vouloir acquitter {count, plural, 1 {1 alarme} other {# alarmes}}?", - "aknowledge-alarms-title": "Acquitter {count, plural, 1 {1 alarme} other {# alarmes}}", - "alarm": "Alarme", - "alarm-details": "Détails de l'alarme", - "alarm-required": "Une alarme est requise", - "alarm-status": "État d'alarme", - "alarm-status-filter": "Filtre d'état d'alarme", - "alarms": "Alarmes", - "clear": "Effacer", - "clear-alarm-text": "Êtes-vous sûr de vouloir effacer l'alarme?", - "clear-alarm-title": "Effacer l'alarme", - "clear-alarms-text": "Êtes-vous sûr de vouloir effacer {count, plural, 1 {1 alarme} other {# alarmes}}?", - "clear-alarms-title": "Effacer {count, plural, 1 {1 alarme} other {# alarmes}}", - "clear-time": "Heure d'éffacement", - "created-time": "Heure de création", - "details": "Détails", - "display-status": { - "ACTIVE_ACK": "Active acquittée", - "ACTIVE_UNACK": "Active non acquittée", - "CLEARED_ACK": "effacée acquittée", - "CLEARED_UNACK": "effacée non acquittée" - }, - "end-time": "Heure de fin", - "min-polling-interval-message": "Un intervalle d'interrogation d'au moins 1 seconde est autorisé.", - "no-alarms-matching": "Aucune alarme correspondant à {{entity}} n'a été trouvée. ", - "no-alarms-prompt": "Aucune alarme", - "no-data": "Aucune donnée à afficher", - "originator": "Source", - "originator-type": "Type de Source", - "polling-interval": "Intervalle d'interrogation des alarmes (sec)", - "polling-interval-required": "L'intervalle d'interrogation des alarmes est requis.", - "search": "Rechercher des alarmes", - "search-status": { - "ACK": "acquitté", - "ACTIVE": "active", - "ANY": "Toutes", - "CLEARED": "effacée", - "UNACK": "non acquittée" - }, - "select-alarm": "Sélectionnez une alarme", - "selected-alarms": "{count, plural, 1 {1 alarme} other {# alarmes}} sélectionnées", - "severity": "Gravité", - "severity-critical": "Critique", - "severity-indeterminate": "indéterminée", - "severity-major": "Majeure", - "severity-minor": "mineure", - "severity-warning": "Avertissement", - "start-time": "Heure de début", - "status": "État", - "type": "Type" - }, - "alias": { - "add": "Ajouter un alias", - "all-entities": "Toutes les entités", - "any-relation": "toutes", - "default-entity-parameter-name": "Par défaut", - "default-state-entity": "Entité d'état par défaut", - "duplicate-alias": "Un alias portant le même nom existe déjà.", - "edit": "Modifier l'alias", - "entity-filter": "Filtre d'entité", - "entity-filter-no-entity-matched": "Aucune entité correspondant au filtre spécifié n'a été trouvée.", - "filter-type": "Type de filtre", - "filter-type-asset-search-query": "requête de recherche d'actifs", - "filter-type-asset-search-query-description": "Actifs de types {{assetTypes}} ayant {{relationType}} relation {{direction}} {{rootEntity}}", - "filter-type-asset-type": "type d'actif", - "filter-type-asset-type-and-name-description": "Actifs de type '{{assetType}}' et dont le nom commence par '{{prefix}}'", - "filter-type-asset-type-description": "Actifs de type '{{assetType}}'", - "filter-type-device-search-query": "Requête de recherche de dispositif", - "filter-type-device-search-query-description": "Dispositifs de types {{deviceTypes}} ayant {{relationType}} relation {{direction}} {{rootEntity}}", - "filter-type-device-type": "Type de dispositif", - "filter-type-device-type-and-name-description": "Dispositifs de type '{{deviceType}}' et dont le nom commence par '{{prefix}}'", - "filter-type-device-type-description": "Dispositifs de type '{{deviceType}}'", - "filter-type-entity-list": "Liste d'entités", - "filter-type-entity-name": "Nom d'entité", - "filter-type-entity-view-search-query": "Requête de recherche vue d'entité", - "filter-type-entity-view-search-query-description": "Vues d'entité avec les types {{entityViewTypes}} ayant {{relationType}} relation {{direction}} {{rootEntity}}", - "filter-type-entity-view-type": "Type de vue d'entité", - "filter-type-entity-view-type-and-name-description": "Vues d'entité de type '{{entityView}}' et dont le nom commence par '{{prefix}}'", - "filter-type-entity-view-type-description": "Vues d'entité de type '{{entityView}}'", - "filter-type-relations-query": "Interrogation des relations", - "filter-type-relations-query-description": "{{entities}} ayant {{relationType}} relation {{direction}} {{rootEntity}}", - "filter-type-required": "Le type de filtre est requis.", - "filter-type-single-entity": "Entité unique", - "filter-type-state-entity": "Entité de l'état du tableau de bord", - "filter-type-state-entity-description": "Entité extraite des paramétres d'état du tableau de bord", - "max-relation-level": "Niveau de relation maximum", - "name": "Nom de l'alias", - "name-required": "Le nom d'alias est requis", - "no-entity-filter-specified": "Aucun filtre d'entité spécifié", - "resolve-multiple": "Résoudre en plusieurs entités", - "root-entity": "Entité racine", - "root-state-entity": "Utiliser l'entité d'état du tableau de bord en tant que racine", - "state-entity": "Entité d'état du tableau de bord", - "state-entity-parameter-name": "Nom du paramétre d'entité d'état", - "unlimited-level": "niveau illimité" - }, - "asset": { - "add": "Ajouter un actif", - "add-asset-text": "Ajouter un nouvel actif", - "any-asset": "Tout actif", - "asset": "Actif", - "asset-details": "Détails de l'actif", - "asset-file": "Actif file", - "asset-public": "L'actif est public", - "asset-required": "Actif requis", - "asset-type": "Type d'actif", - "asset-type-list-empty": "Aucun type d'actif sélectionné.", - "asset-type-required": "Le type d'actif est requis.", - "asset-types": "Types d'actif", - "assets": "Actifs", - "assign-asset-to-customer": "Attribuer des actifs au client", - "assign-asset-to-customer-text": "Veuillez sélectionner les actifs à attribuer au client", - "assign-assets": "Attribuer des actifs", - "assign-assets-text": "Attribuer {count, plural, 1 {1 asset} other {# assets}} au client", - "assign-new-asset": "Attribuer un nouvel Asset", - "assign-to-customer": "Attribuer au client", - "assign-to-customer-text": "Veuillez sélectionner le client pour attribuer le ou les actifs", - "assignedToCustomer": "attribué au client", - "copyId": "Copier l'Id de l'actif", - "delete": "Supprimer un actif", - "delete-asset-text": "Faites attention, après la confirmation, l'actif et toutes les données associées deviendront irrécupérables.", - "delete-asset-title": "Êtes-vous sûr de vouloir supprimer l'actif '{{assetName}}'?", - "delete-assets": "Supprimer des actifs", - "delete-assets-action-title": "Supprimer {count, plural, 1 {1 asset} other {# assets}}", - "delete-assets-text": "Attention, après la confirmation, tous les actifs sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", - "delete-assets-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 asset} other {# assets}}?", - "description": "Description", - "details": "Détails", - "enter-asset-type": "Entrez le type d'actif", - "events": "Evénements", - "idCopiedMessage": "L'Id d'asset a été copié dans le presse-papier", - "import": "Import actifs", - "make-private": "Rendre l'actif privé", - "make-private-asset-text": "Après la confirmation, l'actif et toutes ses données seront rendus privés et ne seront pas accessibles par d'autres.", - "make-private-asset-title": "Êtes-vous sûr de vouloir rendre l'actif '{{assetName}}' privé '?", - "make-public": "Rendre l'actif public", - "make-public-asset-text": "Après la confirmation, l'asset et toutes ses données seront rendus publics et accessibles aux autres.", - "make-public-asset-title": "Êtes-vous sûr de vouloir rendre l'actif '{{assetName}}' public '?", - "management": "Gestion d'actifs", - "name": "Nom", - "name-required": "Nom est requis.", - "name-starts-with": "Le nom de l'actif commence par", - "no-asset-types-matching": "Aucun type d'actif correspondant à {{entitySubtype}} n'a été trouvé. ", - "no-assets-matching": "Aucun actif correspondant à {{entity}} n'a été trouvé. ", - "no-assets-text": "Aucun actif trouvé", - "public": "Public", - "select-asset": "Sélectionner un actif", - "select-asset-type": "Sélectionner le type d'actif", - "type": "Type", - "type-required": "Le type est requis.", - "unassign-asset": "Retirer l'actif", - "unassign-asset-text": "Après la confirmation, l'actif sera non attribué et ne sera pas accessible au client.", - "unassign-asset-title": "Êtes-vous sûr de vouloir retirer l'attribution de l'actif '{{assetName}}'?", - "unassign-assets": "Retirer les actifs", - "unassign-assets-action-title": "Retirer {count, plural, 1 {1 asset} other {# assets}} du client", - "unassign-assets-text": "Après la confirmation, tous les actifs sélectionnés ne seront pas attribués et ne seront pas accessibles au client.", - "unassign-assets-title": "Êtes-vous sûr de vouloir retirer l'attribution de {count, plural, 1 {1 asset} other {# assets}}?", - "unassign-from-customer": "Retirer du client", - "view-assets": "Afficher les actifs", - "label": "Label" - }, - "attribute": { - "add": "Ajouter un attribut", - "add-to-dashboard": "Ajouter au tableau de bord", - "add-widget-to-dashboard": "Ajouter un widget au tableau de bord", - "attributes": "Attributs", - "attributes-scope": "Étendue des attributs d'entité", - "delete-attributes": "Supprimer les attributs", - "delete-attributes-text": "Attention, après la confirmation, tous les attributs sélectionnés seront supprimés.", - "delete-attributes-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 attribut} other {# attributs}}?", - "enter-attribute-value": "Entrez la valeur de l'attribut", - "key": "Clé", - "key-required": "La Clé d'attribut est requise.", - "last-update-time": "Dernière mise à jour", - "latest-telemetry": "Dernière télémétrie", - "next-widget": "Widget suivant", - "prev-widget": "Widget précédent", - "scope-client": "Attributs du client", - "scope-latest-telemetry": "Dernière télémétrie", - "scope-server": "Attributs du serveur", - "scope-shared": "Attributs partagés", - "selected-attributes": "{count, plural, 1 {1 attribut} other {# attributs}} sélectionnés", - "selected-telemetry": "{count, plural, 1 {1 unité de télémétrie} other {# unités de télémétrie}} sélectionnées", - "show-on-widget": "Afficher sur le widget", - "value": "Valeur", - "value-required": "La valeur d'attribut est obligatoire.", - "widget-mode": "Mode du widget" - }, - "audit-log": { - "action-data": "Action data", - "audit": "Audit", - "audit-log-details": "Détails du journal d'audit", - "audit-logs": "Journaux d'audit", - "clear-search": "Effacer la recherche", - "details": "Détails", - "entity-name": "Nom de l'entité", - "entity-type": "Type d'entité", - "failure-details": "Détails de l'échec", - "no-audit-logs-prompt": "Aucun journal trouvé", - "search": "Rechercher les journaux d'audit", - "status": "État", - "status-failure": "Échec", - "status-success": "Succès", - "timestamp": "Horodatage", - "type": "Type", - "type-activated": "Activé", - "type-added": "Ajouté", - "type-alarm-ack": "Acquitté", - "type-alarm-clear": "Effacé", - "type-assigned-to-customer": "Attribué au client", - "type-attributes-deleted": "Attributs supprimés", - "type-attributes-read": "Attributs lus", - "type-attributes-updated": "Attributs mis à jour", - "type-credentials-read": "Lecture des informations d'identification", - "type-credentials-updated": "Informations d'identification actualisées", - "type-deleted": "Supprimé", - "type-login": "Login", - "type-logout": "Connectez - Out", - "type-lockout": "Verrouillage", - "type-relation-add-or-update": "Relation mise à jour", - "type-relation-delete": "Relation supprimée", - "type-relations-delete": "Toutes les relations ont été supprimées", - "type-rpc-call": "Appel RPC", - "type-suspended": "Suspendu", - "type-unassigned-from-customer": "Non attribué du client", - "type-updated": "Mise à jour", - "user": "Utilisateur" - }, - "common": { - "enter-password": "Entrez le mot de passe", - "enter-search": "Entrez la recherche", - "enter-username": "Entrez le nom d'utilisateur", - "password": "Mot de passe", - "username": "Nom d'utilisateur" - }, - "confirm-on-exit": { - "html-message": "Vous avez des modifications non enregistrées.
    Êtes-vous sûr de vouloir quitter cette page?", - "message": "Vous avez des modifications non enregistrées. Êtes-vous sûr de vouloir quitter cette page?", - "title": "Modifications non enregistrées" - }, - "contact": { - "address": "Adresse", - "address2": "adresse 2", - "city": "Ville", - "country": "Pays", - "email": "Email", - "no-address": "Pas d'adresse", - "phone": "Téléphone", - "postal-code": "Code postal", - "postal-code-invalid": "Format de code postal / code postal invalide", - "state": "Province" - }, - "content-type": { - "binary": "Binaire (Base64)", - "json": "Json", - "text": "Texte" - }, - "custom": { - "widget-action": { - "action-cell-button": "Bouton de cellule d'action", - "marker-click": "Sur le marqueur cliquez", - "row-click": "Au rang, cliquez", - "polygon-click": "Cliquez sur le polygone", - "tooltip-tag-action": "Action de balise d'info-bulle", - "node-selected": "Sur le noeud sélectionné", - "element-click": "Sur l'élément HTML, cliquez sur", - "pie-slice-click": "Sur tranche cliquez", - "row-double-click": "Sur la ligne double clic" - } - }, - "customer": { - "add": "Ajouter un client", - "add-customer-text": "Ajouter un nouveau client", - "assets": "Actifs du client", - "copyId": "Copier l'id du client", - "customer": "Client", - "customer-details": "Détails du client", - "customer-required": "Le client est requis", - "customers": "Clients", - "dashboard": "Tableau de bord du client", - "dashboards": "tableaux de bord du client", - "default-customer": "Client par défaut", - "default-customer-required": "Le client par défaut est requis pour déboguer le tableau de bord au niveau du Tenant", - "delete": "Supprimer le client", - "delete-customer-text": "Faites attention, après la confirmation, le client et toutes les données associées deviendront irrécupérables.", - "delete-customer-title": "Êtes-vous sûr de vouloir supprimer le client '{{customerTitle}}'?", - "delete-customers-action-title": "Supprimer {count, plural, 1 {1 customer} other {# customers}}", - "delete-customers-text": "Faites attention, après la confirmation, tous les clients sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", - "delete-customers-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 customer} other {# customers}}?", - "description": "Description", - "details": "Détails", - "devices": "Dispositifs du client", - "entity-views": "Vues de l'entité client", - "events": "Événements", - "idCopiedMessage": "L'Id du client a été copié dans le presse-papier", - "manage-assets": "Gérer les actifs", - "manage-customer-assets": "Gérer les actifs du client", - "manage-customer-dashboards": "Gérer les tableaux de bord du client", - "manage-customer-devices": "Gérer les dispositifs du client", - "manage-customer-users": "Gérer les utilisateurs du client", - "manage-dashboards": "Gérer les tableaux de bord", - "manage-devices": "Gérer les dispositifs", - "manage-public-assets": "Gérer les actifs publics", - "manage-public-dashboards": "Gérer les tableaux de bord publics", - "manage-public-devices": "Gérer les dispositifs publics", - "manage-users": "Gérer les utilisateurs", - "management": "Gestion des clients", - "no-customers-matching": "Aucun client correspondant à '{{entity}} n'a été trouvé.", - "no-customers-text": "Aucun client trouvé", - "public-assets": "Actifs publics", - "public-dashboards": "Tableaux de bord publics", - "public-devices": "Dispositifs publics", - "public-entity-views": "Vues d'entités publiques", - "select-customer": "Sélectionner un client", - "select-default-customer": "Sélectionnez le client par défaut", - "title": "Titre", - "title-required": "Le titre est requis." - }, - "dashboard": { - "add": "Ajouter un tableau de bord", - "add-dashboard-text": "Ajouter un nouveau tableau de bord", - "add-state": "Ajouter un état du tableau de bord", - "add-widget": "Ajouter un nouveau widget", - "alias-resolution-error-title": "Erreur de configuration des alias de tableau de bord", - "assign-dashboard-to-customer": "Attribuer des tableaux de bord au client", - "assign-dashboard-to-customer-text": "Veuillez sélectionner les tableaux de bord à affecter au client", - "assign-dashboards": "Attribuer des tableaux de bord", - "assign-dashboards-text": "Attribuer {count, plural, 1 {1 tableau de bord} other {# tableaux de bord}} aux clients", - "assign-new-dashboard": "Attribuer un nouveau tableau de bord", - "assign-to-customer": "Attribuer au client", - "assign-to-customer-text": "Veuillez sélectionner le client pour attribuer le ou les tableaux de bord", - "assign-to-customers": "Attribuer des tableaux de bord aux clients", - "assign-to-customers-text": "Veuillez sélectionner les clients pour attribuer les tableaux de bord", - "assigned-customers": "clients affectés", - "assignedToCustomer": "Attribué au client", - "assignedToCustomers": "attribué aux clients", - "autofill-height": "Hauteur de remplissage automatique", - "background-color": "Couleur de fond", - "background-image": "Image d'arriére-plan", - "background-size-mode": "Mode de taille d'arriére-plan", - "close-toolbar": "Fermer la barre d'outils", - "columns-count": "Nombre de colonnes", - "columns-count-required": "Le nombre de colonnes est requis.", - "configuration-error": "Erreur de configuration", - "copy-public-link": "Copier le lien public", - "create-new": "Créer un nouveau tableau de bord", - "create-new-dashboard": "Créer un nouveau tableau de bord", - "create-new-widget": "Créer un nouveau widget", - "dashboard": "Tableau de bord", - "dashboard-details": "Détails du tableau de bord", - "dashboard-file": "Fichier du tableau de bord", - "dashboard-import-missing-aliases-title": "Configurer les alias utilisés par le tableau de bord importé", - "dashboard-required": "Le tableau de bord est requis.", - "dashboards": "Tableaux de bord", - "delete": "Supprimer le tableau de bord", - "delete-dashboard-text": "Faites attention, après la confirmation, le tableau de bord et toutes les données associées deviendront irrécupérables.", - "delete-dashboard-title": "Êtes-vous sûr de vouloir supprimer le tableau de bord '{{dashboardTitle}}'?", - "delete-dashboards": "Supprimer les tableaux de bord", - "delete-dashboards-action-title": "Supprimer {count, plural, 1 {1 tableau de bord} other {# tableaux de bord}}", - "delete-dashboards-text": "Attention, après la confirmation, tous les tableaux de bord sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", - "delete-dashboards-title": "Voulez-vous vraiment supprimer {count, plural, 1 {1 tableau de bord} other {# tableaux de bord}}?", - "delete-state": "Supprimer l'état du tableau de bord", - "delete-state-text": "Etes-vous sûr de vouloir supprimer l'état du tableau de bord avec le nom '{{stateName}}'?", - "delete-state-title": "Supprimer l'état du tableau de bord", - "description": "Description", - "details": "Détails", - "display-dashboard-export": "Afficher l'exportation", - "display-dashboard-timewindow": "Afficher fenêtre de temps", - "display-dashboards-selection": "Afficher la sélection des tableaux de bord", - "display-entities-selection": "Afficher la sélection des entités", - "display-title": "Afficher le titre du tableau de bord", - "drop-image": "Déposer une image ou cliquez pour sélectionner un fichier à télécharger.", - "edit-state": "Modifier l'état du tableau de bord", - "export": "Exporter le tableau de bord", - "export-failed-error": "Impossible d'exporter le tableau de bord: {{error}}", - "hide-details": "Masquer les détails", - "horizontal-margin": "Marge horizontale", - "horizontal-margin-required": "Une valeur de marge horizontale est requise.", - "import": "Importer le tableau de bord", - "import-widget": "Importer un widget", - "invalid-aliases-config": "Impossible de trouver des dispositifs correspondant à certains filtres d'alias.
    Veuillez contacter votre administrateur pour résoudre ce problème.", - "invalid-dashboard-file-error": "Impossible d'importer le tableau de bord: structure de données du tableau de bord non valide", - "invalid-widget-file-error": "Impossible d'importer le widget: structure de données de widget invalide.", - "is-root-state": "État racine", - "make-private": "Rendre privé le tableau de bord", - "make-private-dashboard": "Rendre privé le tableau de bord", - "make-private-dashboard-text": "Après la confirmation, le tableau de bord sera rendu privé et ne sera plus accessible aux autres.", - "make-private-dashboard-title": "Êtes-vous sûr de vouloir rendre le tableau de bord '{{dashboardTitle}}' privé?", - "make-public": "Rendre public le tableau de bord", - "manage-assigned-customers": "Gérer les clients affectés", - "manage-states": "Gérer les états du tableau de bord", - "management": "Gestion du tableau de bord", - "max-columns-count-message": "Seulement 1000 colonnes maximum sont autorisées.", - "max-horizontal-margin-message": "Seulement 50 sont autorisés en tant que valeur de marge horizontale maximale.", - "max-mobile-row-height-message": "Seuls 200 pixels sont autorisés en tant que valeur maximale de hauteur de ligne mobile.", - "max-vertical-margin-message": "Seulement 50 sont autorisés en tant que valeur de marge verticale maximale.", - "min-columns-count-message": "Seul un nombre minimum de 10 colonnes est autorisé.", - "min-horizontal-margin-message": "Seul 0 est autorisé comme valeur de marge horizontale minimale.", - "min-mobile-row-height-message": "Seuls 5 pixels sont autorisés en tant que valeur minimale de hauteur de ligne mobile.", - "min-vertical-margin-message": "Seul 0 est autorisé comme valeur de marge verticale minimale.", - "mobile-layout": "Paramètres de mise en page mobiles", - "mobile-row-height": "Hauteur de ligne mobile, px", - "mobile-row-height-required": "Une valeur de hauteur de ligne mobile est requise.", - "new-dashboard-title": "Nouveau titre du tableau de bord", - "no-dashboards-matching": "Aucun tableau de bord correspondant à {{entity}} n'a été trouvé. ", - "no-dashboards-text": "Aucun tableau de bord trouvé", - "no-image": "Aucune image sélectionnée", - "no-widgets": "Aucun widget configuré", - "open-dashboard": "Ouvrir le tableau de bord", - "open-toolbar": "Ouvrir la barre d'outils du tableau de bord", - "public": "Public", - "public-dashboard-notice": " Remarque: N'oubliez pas de rendre publics les dispositifs associés pour accéder à leurs données.", - "public-dashboard-text": "Votre tableau de bord {{dashboardTitle}} est maintenant public et accessible via le lien public
    : ", - "public-dashboard-title": "Le tableau de bord est maintenant public", - "public-link": "Lien public", - "public-link-copied-message": "Le lien public du tableau de bord a été copié dans le presse-papier", - "search-states": "Recherche des états du tableau de bord", - "select-dashboard": "Sélectionner le tableau de bord", - "select-devices": "Selectionner les dispositifs", - "select-existing": "Sélectionnez un tableau de bord existant", - "select-state": "Sélectionnez l'état cible", - "select-widget-subtitle": "Liste des types de widgets disponibles", - "select-widget-title": "Sélectionner un widget", - "selected-states": "{count, plural, 1 {1 état du tableau de bord} other {# états du tableau de bord}} sélectionnés", - "set-background": "Définir l'arrière-plan", - "settings": "Paramètres", - "show-details": "Afficher les détails", - "socialshare-text": "'{{dashboardTitle}}' propulsé par ThingsBoard", - "socialshare-title": "'{{dashboardTitle}}' propulsé par ThingsBoard", - "state": "État du tableau de bord", - "state-controller": "Contrôleur d'état", - "state-id": "ID d'état", - "state-id-exists": "L'état du tableau de bord avec le même Id existe déjà.", - "state-id-required": "L'Id d'état du tableau de bord est requis.", - "state-name": "Nom", - "state-name-required": "Le nom de l'état du tableau de bord est requis", - "states": "États du tableau de bord", - "title": "Titre", - "title-color": "Couleur du titre", - "title-required": "Le titre est requis.", - "toolbar-always-open": "Garder la barre d'outils ouverte", - "unassign-dashboard": "Retirer le tableau de bord", - "unassign-dashboard-text": "Après la confirmation, le tableau de bord ne sera pas attribué et ne sera pas accessible au client.", - "unassign-dashboard-title": "Êtes-vous sûr de vouloir annuler l'affectation du tableau de bord '{{dashboardTitle}}'?", - "unassign-dashboards": "Retirer les tableaux de bord", - "unassign-dashboards-action-text": "Annuler l'affectation {count, plural, 1 {1 tableau de bord} other {# tableaux de bord}} des clients", - "unassign-dashboards-action-title": "Annuler l'affectation {count, plural, 1 {1 tableau de bord} other {# tableaux de bord}} du client", - "unassign-dashboards-text": "Après la confirmation, tous les tableaux de bord sélectionnés ne seront pas attribués et ne seront pas accessibles au client.", - "unassign-dashboards-title": "Etes-vous sûr de vouloir annuler l'affectation {count, plural, 1 {1 tableau de bord} other {# tableaux de bord}}?", - "unassign-from-customer": "Retirer du client", - "unassign-from-customers": "Retirer les tableaux de bord des clients", - "unassign-from-customers-text": "Veuillez sélectionner les clients à annuler l'affectation du ou des tableaux de bord", - "vertical-margin": "Marge verticale", - "vertical-margin-required": "Une valeur de marge verticale est requise", - "view-dashboards": "Afficher les tableaux de bord", - "widget-file": "Fichier du Widget", - "widget-import-missing-aliases-title": "Configurer les alias utilisés par le widget importé", - "widgets-margins": "Marge entre les widgets" - }, - "datakey": { - "advanced": "Avancé", - "alarm": "Champs d'alarme", - "alarm-fields-required": "Les champs d'alarme sont obligatoires.", - "attributes": "Attributs", - "color": "Couleur", - "configuration": "Configuration de la clé de données", - "data-generation-func": "Fonction de génération de données", - "decimals": "Nombre de chiffres après virgule flottante", - "function-types": "Types de fonctions", - "function-types-required": "Les types de fonctions sont obligatoires", - "label": "Label", - "maximum-function-types": "Maximum {count, plural, 1 {1 type de fonction est autorisé.} other {# types de fonctions sont autorisés}}", - "maximum-timeseries-or-attributes": "Maximum {count, plural, 1 {1 timeseries / attribut est autorisé.} other {# timeseries / attributs sont autorisés}}", - "prev-orig-value-description": "valeur précédente d'origine;", - "prev-value-description": "résultat de l'appel de fonction précédent;", - "settings": "Paramètres", - "time-description": "horodatage de la valeur actuelle;", - "time-prev-description": "horodatage de la valeur précédente;", - "timeseries": "Timeseries", - "timeseries-or-attributes-required": "Les timeseries / attributs d'entité sont obligatoires.", - "timeseries-required": "Les Timeseries de l'entité sont obligatoires.", - "units": "Symbole spécial à afficher à côté de la valeur", - "use-data-post-processing-func": "Utiliser la fonction de post-traitement des données", - "value-description": "la valeur actuelle;" - }, - "datasource": { - "add-datasource-prompt": "Veuillez ajouter une source de données", - "name": "Nom", - "type": "Type de source de données" - }, - "datetime": { - "date-from": "Date de", - "date-to": "Date à", - "time-from": "Heure de", - "time-to": "Heure à" - }, - "details": { - "edit-mode": "Mode édition", - "toggle-edit-mode": "Activer le mode édition" - }, - "device": { - "access-token": "Jeton d'accès", - "access-token-invalid": "La longueur du jeton d'accès doit être comprise entre 1 et 20 caractéres.", - "access-token-required": "Le jeton d'accès est requis.", - "accessTokenCopiedMessage": "Le jeton d'accès au dispositif a été copié dans le presse-papier", - "add": "Ajouter un dispositif", - "add-alias": "Ajouter un alias de dispositif", - "add-device-text": "Ajouter un nouveau dispositif", - "alias": "Alias", - "alias-required": "Un alias du dispositif est requis.", - "aliases": "Alias des dispositifs", - "any-device": "N'importe quel dispositif", - "assign-device-to-customer": "Affecter des dispositifs au client", - "assign-device-to-customer-text": "Veuillez sélectionner les dispositif à affecter au client", - "assign-devices": "Attribuer des dispositifs", - "assign-devices-text": "Attribuer {count, plural, 1 {1 dispositif} other {# dispositifs}} au client", - "assign-new-device": "Attribuer un nouveau dispositif", - "assign-to-customer": "Attribuer au client", - "assign-to-customer-text": "Veuillez sélectionner le client pour attribuer le ou les dispositifs", - "assignedToCustomer": "Attribué au client", - "configure-alias": "Configurer '{{alias}}' alias", - "copyAccessToken": "Copier le jeton d'accès", - "copyId": "Copier l'Id du dispositif", - "create-new-alias": "Créez un nouveau!", - "create-new-key": "Créez un nouveau!", - "credentials": "Informations d'identification", - "credentials-type": "Type d'identification", - "delete": "Supprimer le dispositif", - "delete-device-text": "Faites attention, après la confirmation, le dispositif et toutes les données associées deviendront irrécupérables.", - "delete-device-title": "Êtes-vous sûr de vouloir supprimer le dispositif '{{deviceName}}'?", - "delete-devices": "Supprimer les dispositifs", - "delete-devices-action-title": "Supprimer {count, plural, 1 {1 device} other {# devices}}", - "delete-devices-text": "Faites attention, après la confirmation, tous les dispositifs sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", - "delete-devices-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 device} other {# devices}}?", - "description": "Description", - "details": "Détails", - "device": "Dispositif", - "device-alias": "Alias ​​du dispositif", - "device-credentials": "Informations d'identification du dispositif", - "device-details": "Détails du dispositif", - "device-list": "Liste des dispositifs", - "device-list-empty": "Aucun dispositif sélectionné.", - "device-name-filter-no-device-matched": "Aucun dispositif commençant par '{{device}} n'a été trouvé.", - "device-name-filter-required": "Le filtre de nom de dispositif est requis.", - "device-public": "Le dispositif est public", - "device-required": "Le dispositif est requis.", - "device-type": "Type de dispositif", - "device-type-list-empty": "Aucun type de dispositif sélectionné.", - "device-type-required": "Le type de dispositif est requis.", - "device-types": "Types de dispositif", - "devices": "Dispositifs", - "duplicate-alias-error": "Alias ??en double trouvé '{{alias}}'.
    Les alias de dispositifs doivent être uniques dans le tableau de bord.", - "enter-device-type": "Entrez le type de dispositif", - "events": "Événements", - "idCopiedMessage": "l'Id du dispositif a été copié dans le presse-papiers", - "is-gateway": "Est une passerelle", - "label": "Label", - "make-private": "Rendre le dispositif privé", - "make-private-device-text": "Après la confirmation, le dispositif et toutes ses données seront rendues privées et ne seront pas accessibles par d'autres.", - "make-private-device-title": "Êtes-vous sûr de vouloir rendre le dispositif {{deviceName}} privé?", - "make-public": "Rendre le dispositif public", - "make-public-device-text": "Après la confirmation, le dispositif et toutes ses données seront rendus publics et accessibles par d'autres.", - "make-public-device-title": "Êtes-vous sûr de vouloir rendre le dispositif {{deviceName}} 'public?", - "manage-credentials": "Gérer les informations d'identification", - "management": "Gestion des dispositifs", - "name": "Nom", - "name-required": "Le nom est requis.", - "name-starts-with": "Le nom du dispositif commence par", - "no-alias-matching": "'{{alias}}' introuvable.", - "no-aliases-found": "Aucun alias trouvé.", - "no-device-types-matching": "Aucun type de dispositif correspondant à {{entitySubtype}} n'a été trouvé.", - "no-devices-matching": "Aucun dispositif correspondant à '{{entity}} n'a été trouvé.", - "no-devices-text": "Aucun dispositif trouvé", - "no-key-matching": "'{{key}}' introuvable.", - "no-keys-found": "Aucune clé trouvée", - "public": "Public", - "remove-alias": "Supprimer l'alias du dispositif", - "rsa-key": "Clé publique RSA", - "rsa-key-required": "La clé publique RSA est requise.", - "secret": "Secret", - "secret-required": "Code secret est requis.", - "select-device": "Selectionner un dispositif", - "select-device-type": "Sélectionner le type d'appareil", - "unable-delete-device-alias-text": "L'alias du dispositif '{{deviceAlias}}' ne peut pas être supprimé car il est utilisé par les widgets suivants:
    {{widgetsList}}", - "unable-delete-device-alias-title": "Impossible de supprimer l'alias du dispositif", - "unassign-device": "Annuler l'affectation du dispositif", - "unassign-device-text": "Après la confirmation, le dispositif ne sera pas attribué et ne sera pas accessible au client.", - "unassign-device-title": "Êtes-vous sûr de vouloir annuler l'affection du dispositif {{deviceName}} '?", - "unassign-devices": "Annuler l'affectation des dispositifs", - "unassign-devices-action-title": "Annuler l'affectation de {count, plural, 1 {1 device} other {#devices}} du client", - "unassign-devices-text": "Après la confirmation, tous les dispositifs sélectionnés ne seront pas attribues et ne seront pas accessibles par le client.", - "unassign-devices-title": "Voulez-vous vraiment annuler l'affectation de {count, plural, 1 {1 device} other {# devices}}?", - "unassign-from-customer": "Retirer du client", - "use-device-name-filter": "Utiliser le filtre", - "view-credentials": "Afficher les informations d'identification", - "view-devices": "Afficher les dispositifs" - }, - "dialog": { - "close": "Fermer le dialogue" - }, - "entity": { - "add-alias": "Ajouter un alias d'entité", - "alarm-name-starts-with": "Les actifs dont le nom commence par '{{prefix}}'", - "alias": "Alias", - "alias-required": "Un alias d'entité est requis.", - "aliases": "alias d'entité", - "all-subtypes": "Tout", - "any-entity": "Toute entité", - "asset-name-starts-with": "Les Assets dont le nom commence par '{{prefix}}'", - "columns-to-display": "Colonnes à afficher", - "configure-alias": "Configurer '{{alias}}' alias", - "create-new-alias": "Créez un nouveau!", - "create-new-key": "Créez un nouveau!", - "customer-name-starts-with": "Les clients dont les noms commencent par '{{prefix}}'", - "dashboard-name-starts-with": "Les tableaux de bord dont les noms commencent par '{{prefix}}'", - "details": "Détails de l'entité", - "device-name-starts-with": "Dispositifs dont le nom commence par '{{prefix}}'", - "duplicate-alias-error": "Alias ​​en double trouvé '{{alias}}'.
    Les alias d'entité doivent être uniques dans le tableau de bord.", - "enter-entity-type": "Entrez le type d'entité", - "entities": "Entités", - "entity": "Entité", - "entity-alias": "Alias de l'entité", - "entity-list": "Liste d'entités", - "entity-list-empty": "Aucune entité sélectionnée.", - "entity-name": "Nom de l'entité", - "entity-name-filter-no-entity-matched": "Aucune entité commençant par '{{entity}}' n'a été trouvée.", - "entity-name-filter-required": "Le filtre de nom d'entité est requis.", - "entity-type": "Type d'entité", - "entity-type-list": "Liste de types d'entités", - "entity-type-list-empty": "Aucun type d'entité sélectionné.", - "entity-types": "Types d'entité", - "entity-view-name-starts-with": "Les vues d'entité dont le nom commence par '{{prefix}}'", - "key": "Clé", - "key-name": "Nom de la clé", - "list-of-alarms": "{count, plural, 1 {Une alarme} other {Liste de # alarmes}}", - "list-of-assets": "{count, plural, 1 {Un Asset} other {Liste de # Assets}}", - "list-of-customers": "{count, plural, 1 {Un client} other {Liste de # clients}}", - "list-of-dashboards": "{count, plural, 1 {Un tableau de bord} other {Liste de # tableaux de bord}}", - "list-of-devices": "{count, plural, 1 {Un dispositif} other {Liste de # dispositifs}}", - "list-of-plugins": "{count, plural, 1 {Un plugin} other {Liste de # plugins}}", - "list-of-rulechains": "{count, plural, 1 {Une chaîne de règles} other {Liste de # chaînes de règles}}", - "list-of-rulenodes": "{count, plural, 1 {Un noeud de règles} other {Liste de # noeuds de règles}}", - "list-of-rules": "{count, plural, 1 {Une règle} other {Liste de # règles}}", - "list-of-tenants": "{count, plural, 1 {Un tenant} other {Liste de # tenants}}", - "list-of-users": "{count, plural, 1 {Un utilisateur} other {Liste de # utilisateurs}}", - "missing-entity-filter-error": "Le filtre est manquant pour l'alias '{{alias}}'.", - "name-starts-with": "Nom commence par", - "no-alias-matching": "'{{alias}}' introuvable.", - "no-aliases-found": "Aucun alias trouvé.", - "no-data": "Aucune donnée à afficher", - "no-entities-matching": "Aucune entité correspondant à '{{entity}}' n'a été trouvée.", - "no-entities-prompt": "Aucune entité trouvée", - "no-entity-types-matching": "Aucun type d'entité correspondant à {{entityType}} n'a été trouvé. ", - "no-key-matching": "'{{key}}' introuvable.", - "no-keys-found": "Aucune clé trouvée", - "plugin-name-starts-with": "Plugins dont les noms commencent par '{{prefix}}'", - "remove-alias": "Supprimer l'alias d'entité", - "rule-name-starts-with": "Régles dont les noms commencent par '{{prefix}}'", - "rulechain-name-starts-with": "Chaînes de régles dont les noms commencent par '{{prefix}}'", - "rulenode-name-starts-with": "Les noeuds de régles dont le nom commence par '{{prefix}}'", - "search": "Recherche d'entités", - "select-entities": "Sélectionner des entités", - "selected-entities": "{count, plural, 1 {1 entité} other {# entités}} sélectionnées", - "tenant-name-starts-with": "Les Tenant dont le nom commence par '{{prefix}}'", - "type": "Type", - "type-alarm": "Alarme", - "type-alarms": "Alarmes", - "type-asset": "Actif", - "type-assets": "Actifs", - "type-current-customer": "Client actuel", - "type-customer": "Client", - "type-customers": "Clients", - "type-dashboard": "Tableau de bord", - "type-dashboards": "Tableaux de bord", - "type-device": "Dispositif", - "type-devices": "Dispositifs", - "type-entity-view": "Vue d'entité", - "type-entity-views": "Vues d'entités", - "type-plugin": "Plugin", - "type-plugins": "Plugins", - "type-required": "Le type d'entité est obligatoire.", - "type-rule": "Régle", - "type-rulechain": "Chaîne de régles", - "type-rulechains": "Chaînes de régles", - "type-rulenode": "Noeud de régle", - "type-rulenodes": "Noeuds de régle", - "type-rules": "Régles", - "type-tenant": "Tenant", - "type-tenants": "Tenants", - "type-user": "Utilisateur", - "type-users": "Utilisateurs", - "unable-delete-entity-alias-text": "L'alias d'entité '{{entityAlias}}' ne peut pas être supprimé car il est utilisé par les widgets suivants:
    {{widgetsList}}", - "unable-delete-entity-alias-title": "Impossible de supprimer l'alias d'entité", - "use-entity-name-filter": "Utiliser un filtre", - "user-name-starts-with": "Utilisateurs dont les noms commencent par '{{prefix}}'" - }, - "entity-field": { - "address": "Adresse", - "address2": "Adresse 2", - "city": "Ville", - "country": "Pays", - "created-time": "Heure de création", - "email": "Email", - "first-name": "Prénom", - "last-name": "Nom de famille", - "name": "Nom", - "phone": "Téléphone", - "state": "Prov", - "title": "Titre", - "type": "Type", - "zip": "Code postal" - }, - "entity-view": { - "add": "Ajouter une vue d'entité", - "add-alias": "Ajouter un alias de vue d'entité", - "add-entity-view-text": "Ajouter une nouvelle vue d'entité", - "alias": "Alias", - "alias-required": "Un alias de vue d'entité est requis.", - "aliases": "Alias de vue d'entité", - "any-entity-view": "Toute vue d'entité", - "assign-entity-view-to-customer": "Attribuer une (des) vue (s) d'entité à un client", - "assign-entity-view-to-customer-text": "Veuillez sélectionner les vues d'entité à affecter au client", - "assign-entity-views": "Attribuer des vues d'entité", - "assign-entity-views-text": "Attribuer { count, plural, 1 {1 entityView} other {# entityViews} } au client", - "assign-new-entity-view": "Attribuer une nouvelle vue d'entité", - "assign-to-customer": "Attribuer au client", - "assign-to-customer-text": "Veuillez sélectionner le client auquel attribuer la ou les vues d'entité.", - "assignedToCustomer": "Assigné au client", - "attributes-propagation": "Propagation des attributs", - "attributes-propagation-hint": "La vue d'entité copiera automatiquement les attributs spécifiés de l'entité cible chaque fois que vous enregistrez ou mettez à jour cette vue d'entité. Pour des raisons de performances, les attributs d'entité cible ne sont pas propagés à la vue d'entité à chaque changement d'attribut. Vous pouvez activer la propagation automatique en configurant le noeud de règle \" copier pour afficher \" dans votre chaîne de règles et en liant les messages \"Post attributs \" et \"attributs mis à jour \" au nouveau noeud de règle.", - "client-attributes": "Attributs du client", - "client-attributes-placeholder": "Attributs du client", - "configure-alias": "Configurez l'alias '{{alias}}'", - "copyId": "Copier l'ID de la vue d'entité", - "create-new-alias": "Créer un nouveau!", - "create-new-key": "Créer un nouveau!", - "date-limits": "Limites de date", - "delete": "Supprimer la vue d'entité", - "delete-entity-view-text": "Attention, après la confirmation, la vue de l'entité et toutes les données associées deviendront irrécupérables.", - "delete-entity-view-title": "Êtes-vous sûr de vouloir supprimer la vue de l'entité '{{entityViewName}}'?", - "delete-entity-views": "Supprimer les vues d'entité", - "delete-entity-views-action-title": "Supprimer { count, plural, 1 {1 entityView} other {# entityViews} }", - "delete-entity-views-text": "Attention, après la confirmation, toutes les vues d'entité sélectionnées seront supprimées et toutes les données associées deviendront irrécupérables.", - "delete-entity-views-title": "Êtes-vous sûr de vouloir voir l'entité { count, plural, 1 {1 entityView} other {# entityViews} }?", - "description": "Description", - "details": "Détails", - "duplicate-alias-error": "Alias '{{alias}}' existe déjà.
    Les alias de vue d'entité doivent être uniques dans le tableau de bord.", - "end-date": "Date de fin", - "end-ts": "Heure de fin", - "enter-entity-view-type": "Entrer le type de vue d'entité", - "entity-view": "Vue d'entité", - "entity-view-alias": "Alias de vue d'entité", - "entity-view-details": "Détails de la vue d'entité", - "entity-view-list": "Liste de vues d'entités", - "entity-view-list-empty": "Aucune vue d'entité sélectionnée.", - "entity-view-name-filter-no-entity-view-matched": "Aucune vue d'entité commençant par '{{entityView}}' n'a été trouvée.", - "entity-view-name-filter-required": "Un filtre de nom de vue d'entité est requis.", - "entity-view-required": "Une vue d'entité est requise.", - "entity-view-type": "Type de vue d'entité", - "entity-view-type-list-empty": "Aucun type de vue d'entité sélectionné.", - "entity-view-type-required": "Le type d'entité est requis.", - "entity-view-types": "Types de vues d'entité", - "entity-views": "Vues d'entité", - "events": "Événements", - "make-private": "Rendre la vue d'entité privée", - "make-private-entity-view-text": "Après la confirmation, la vue de l'entité et toutes ses données seront rendues privées et ne seront pas accessibles par d'autres", - "make-private-entity-view-title": "Êtes-vous sûr de vouloir rendre la vue d'entité '{{entityViewName}}' privée?", - "make-public": "Rendre la vue d'entité publique", - "make-public-entity-view-text": "Après la confirmation, la vue de l'entité et toutes ses données seront rendues publiques et accessibles à d'autres", - "make-public-entity-view-title": "Voulez-vous vraiment que la vue de l'entité '{{entityViewName}}' soit publique?", - "management": "Gestion de vue d'entité", - "name": "Nom", - "name-required": "Un nom est requis.", - "name-starts-with": "Le nom de la vue d'entité commence par", - "no-alias-matching": "'{{alias}}' non trouvé.", - "no-aliases-found": "Aucun alias trouvé.", - "no-entity-view-types-matching": "Aucun type de vue d'entité correspondant à '{{entitySubtype}}' n'a été trouvé.", - "no-entity-views-matching": "Aucune vue d'entité correspondant à '{{entity}}' n'a été trouvée.", - "no-entity-views-text": "Aucune vue d'entité trouvée.", - "no-key-matching": "'{{key}}' non trouvé.", - "no-keys-found": "Aucune clé trouvée.", - "remove-alias": "Supprimer un alias de vue d'entité", - "select-entity-view": "Sélectionner une vue d'entité", - "select-entity-view-type": "Sélectionner le type de vue d'entité", - "server-attributes": "Attributs du serveur", - "server-attributes-placeholder": "Attributs du serveur", - "shared-attributes": "Attributs partagés", - "shared-attributes-placeholder": "Attributs partagés", - "start-date": "Date de début", - "start-ts": "Heure de début", - "target-entity": "Entité cible", - "timeseries": "Séries chronologiques", - "timeseries-data": "Données de séries chronologiques", - "timeseries-data-hint": "Configurez les clés de données de séries chronologiques de l'entité cible qui seront accessibles à la vue de l'entité. Ces données temporelles sont en lecture seule.", - "timeseries-placeholder": "Séries chronologiques", - "unable-entity-view-device-alias-text": "L'alias de dispositif '{{entityViewAlias}}' ne peut pas être supprimé car il est utilisé par les widgets suivants:
    {{widgetsList}}", - "unable-entity-view-device-alias-title": "Impossible de supprimer l'alias de la vue d'entité.", - "unassign-entity-view": "Annuler l'affectation de la vue d'entité", - "unassign-entity-view-text": "Après la confirmation, la vue de l'entité sera non attribuée et ne sera pas accessible par le client.", - "unassign-entity-view-title": "Voulez-vous vraiment annuler l'attribution de la vue d'entité '{{entityViewName}}'?", - "unassign-entity-views": "Annuler l'attribution des vues d'entité", - "unassign-entity-views-action-title": "Annuler l'attribution { count, plural, 1 {1 entityView} other {# entityViews} } du client", - "unassign-entity-views-text": "Après la confirmation, toutes les vues des entités sélectionnées seront non attribuées et ne seront pas accessibles par le client.", - "unassign-entity-views-title": "Êtes-vous sûr de vouloir annuler l'attribution { count, plural, 1 {1 entityView} other {# entityViews} }?", - "unassign-from-customer": "Annuler l'attribution au client", - "use-entity-view-name-filter": "Use filter", - "view-entity-views": "Voir les vues d'entité" - }, - "error": { - "unable-to-connect": "Impossible de se connecter au serveur! Veuillez vérifier votre connexion Internet.", - "unhandled-error-code": "Code d'erreur non géré: {{errorCode}}", - "unknown-error": "Erreur inconnue" - }, - "event": { - "alarm": "Alarme", - "body": "Corps", - "data": "Données", - "data-type": "Type de données", - "entity": "Entité", - "error": "erreur", - "errors-occurred": "Des erreurs sont survenues", - "event": "événement", - "event-time": "Heure de l'événement", - "event-type": "Type d'événement", - "failed": "Échec", - "message-id": "Message Id", - "message-type": "Type de message", - "messages-processed": "Messages traités", - "metadata": "Métadonnées", - "method": "Méthode", - "no-events-prompt": "Aucun événement trouvé", - "relation-type": "Type de relation", - "server": "Serveur", - "status": "État", - "success": "Succès", - "type": "Type", - "type-debug-rule-chain": "Debug", - "type-debug-rule-node": "Debug", - "type-error": "Erreur", - "type-lc-event": "Evénement du cycle de vie", - "type-stats": "Statistiques" - }, - "extension": { - "add": "Ajouter une extension", - "add-attribute": "Ajouter un attribut", - "add-attribute-request": "Ajouter une demande d'attribut", - "add-attribute-update": "Ajouter une mise à jour d'attribut", - "add-broker": "Ajouter un Broker", - "add-config": "Ajouter une configuration de convertisseur", - "add-connect-request": "Ajouter une demande de connexion", - "add-converter": "Ajouter un convertisseur", - "add-device": "Ajouter un dispositif", - "add-disconnect-request": "Ajouter une demande de déconnexion", - "add-map": "Ajouter un élément de mappage", - "add-server-side-rpc-request": "Ajouter une requête RPC côté serveur", - "add-timeseries": "Ajouter des timeseries", - "anonymous": "Anonyme", - "attr-json-key-expression": "Expression json de la clé d'attribut", - "attr-topic-key-expression": "Expression du topic de la clé d'attribut", - "attribute-filter": "Filtre d'attribut", - "attribute-key-expression": "Expression de clé d'attribut", - "attribute-requests": "Demandes d'attributs", - "attribute-updates": "Mises à jour des attributs", - "attributes": "Attributs", - "basic": "Basic", - "brokers": "Brokers", - "ca-cert": "Fichier de certificat CA", - "cert": "Fichier de certificat *", - "client-scope": "Portée client", - "configuration": "Configuration", - "connect-requests": "Demandes de connexion", - "converter-configurations": "Configurations du convertisseur", - "converter-id": "ID du convertisseur", - "converter-json": "Json", - "converter-json-parse": "Impossible d'analyser le convertisseur json.", - "converter-json-required": "Le convertisseur json est requis.", - "converter-type": "Type de convertisseur", - "converters": "Convertisseurs", - "credentials": "Informations d'identification", - "custom": "Sur mesure", - "delete": "Supprimer l'extension", - "delete-extension-text": "Attention, après la confirmation, l'extension et toutes les données associées deviendront irrécupérables.", - "delete-extension-title": "Êtes-vous sûr de vouloir supprimer l'extension '{{extensionId}}'?", - "delete-extensions-text": "Attention, après la confirmation, toutes les extensions sélectionnées seront supprimées.", - "delete-extensions-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 extension} other {# extensions}}?", - "device-name-expression": "expression du nom du dispositif", - "device-name-filter": "Filtre de nom de dispositif", - "device-type-expression": "expression de type de dispositif", - "disconnect-requests": "Demandes de déconnection", - "drop-file": "Déposez un fichier ou cliquez pour sélectionner un fichier à télécharger.", - "edit": "Modifier l'extension", - "export-extension": "Exporter l'extension", - "export-extensions-configuration": "Exporter la configuration des extensions", - "extension-id": "Id de l'extension", - "extension-type": "Type d'extension", - "extensions": "Extensions", - "field-required": "Le champ est obligatoire", - "file": "Fichier d'extensions", - "filter-expression": "Expression du filtre", - "host": "Hôte", - "id": "Id", - "import-extension": "Importer une extension", - "import-extensions": "Importer des extensions", - "import-extensions-configuration": "Importer la configuration des extensions", - "invalid-file-error": "Fichier d'extension non valide", - "json-name-expression": "Expression json du nom du dispositif", - "json-parse": "Impossible d'analyser json transformer.", - "json-required": "Transformer json est requis.", - "json-type-expression": "Expression json du type de dispositif", - "key": "Clé", - "mapping": "Mappage", - "method-filter": "Filtre de méthode", - "modbus-add-server": "Ajouter serveur/esclave", - "modbus-add-server-prompt": "Veuillez ajouter serveur/esclave", - "modbus-attributes-poll-period": "Période d'interrogation des attributs (ms)", - "modbus-baudrate": "Débit en bauds", - "modbus-byte-order": "Ordre des octets", - "modbus-databits": "Bits de données", - "modbus-databits-range": "Les bits de données doivent être compris entre 7 et 8.", - "modbus-device-name": "Nom du dispositif", - "modbus-encoding": "Encodage", - "modbus-function": "Fonction", - "modbus-parity": "parité", - "modbus-poll-period": "Période d'interrogation (ms)", - "modbus-poll-period-range": "La période d'interrogation doit être une valeur positive.", - "modbus-port-name": "Nom du port série", - "modbus-register-address": "Adresse du registre", - "modbus-register-address-range": "L'adresse du registre doit être comprise entre 0 et 65535.", - "modbus-register-bit-index": "Bit index", - "modbus-register-bit-index-range": "L'index de bit doit être compris entre 0 et 15.", - "modbus-register-count": "Nombre de registre", - "modbus-register-count-range": "Le nombre de registres doit être une valeur positive.", - "modbus-server": "Serveurs / esclaves", - "modbus-stopbits": "Bits d'arrêt", - "modbus-stopbits-range": "Les bits d'arrêt doivent être compris entre 1 et 2.", - "modbus-tag": "Tag", - "modbus-timeseries-poll-period": "Période d'interrogation des Timeseries (ms)", - "modbus-transport": "Transport", - "modbus-unit-id": "Id de l'unité", - "modbus-unit-id-range": "L'ID de l'unité doit être compris entre 1 et 247.", - "no-file": "Aucun fichier sélectionné.", - "opc-add-server": "Ajouter un serveur", - "opc-add-server-prompt": "Veuillez ajouter un serveur", - "opc-application-name": "Nom de l'application", - "opc-application-uri": "Uri de l'application", - "opc-device-name-pattern": "modèle de nom du dispositif", - "opc-device-node-pattern": "modèle de noeud de dispositif", - "opc-identity": "Identité", - "opc-keystore": "Magasin de clés", - "opc-keystore-alias": "Alias", - "opc-keystore-key-password": "Mot de passe de la clé", - "opc-keystore-location": "Emplacement *", - "opc-keystore-password": "Mot de passe", - "opc-keystore-type": "Type", - "opc-scan-period-in-seconds": "Période d'analyse en secondes", - "opc-security": "Sécurité", - "opc-server": "Serveurs", - "opc-type": "Type", - "password": "Mot de passe", - "pem": "PEM", - "port": "Port", - "port-range": "Le port doit être compris entre 1 et 65535.", - "private-key": "Fichier de clé privée *", - "request-id-expression": "Expression de demande d'id", - "request-id-json-expression": "Expression json de la demande d'id", - "request-id-topic-expression": "Expression de la demande d'id du topic", - "request-topic-expression": "Expression de la demande du topic", - "response-timeout": "Délai de réponse en millisecondes", - "response-topic-expression": "Expression du topic de la réponse", - "retry-interval": "Intervalle de nouvelle tentative en millisecondes", - "selected-extensions": "{count, plural, 1 {1 extension} other {# extensions}} sélectionné", - "server-side-rpc": "RPC côté serveur", - "ssl": "Ssl", - "sync": { - "last-sync-time": "Dernière heure de synchronisation", - "not-available": "Non disponible", - "not-sync": "Non sync", - "status": "Status", - "sync": "Sync" - }, - "timeout": "Délai d'attente en millisecondes", - "timeseries": "Timeseries", - "to-double": "Au double", - "token": "Jeton de sécurité", - "topic": "Topic", - "topic-expression": "Expression du topic", - "topic-filter": "Filtre du topic", - "topic-name-expression": "Expression du nom du dispositif (topic)", - "topic-type-expression": "Expression de type de dispositif (topic)", - "transformer": "Transformer", - "transformer-json": "JSON *", - "type": "Type", - "unique-id-required": "L'identifiant d'extension actuel existe déjà.", - "username": "Nom d'utilisateur", - "value": "Valeur", - "value-expression": "Expression de la valeur" - }, - "fullscreen": { - "exit": "Quitter le plein écran", - "expand": "Afficher en plein écran", - "fullscreen": "Plein écran", - "toggle": "Activer le mode plein écran" - }, - "function": { - "function": "Fonction" - }, - "grid": { - "add-item-text": "Ajouter un nouvel élément", - "delete-item": "Supprimer l'élément", - "delete-item-text": "Faites attention, après la confirmation, cet élément et toutes les données associées deviendront irrécupérables.", - "delete-item-title": "Êtes-vous sûr de vouloir supprimer cet élément?", - "delete-items": "Supprimer les éléments", - "delete-items-action-title": "Supprimer {count, plural, 1 {1 élément} other {# éléments}}", - "delete-items-text": "Attention, après la confirmation, tous les éléments sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", - "delete-items-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 élément} other {# éléments}}?", - "item-details": "Détails de l'élément", - "no-items-text": "Aucun élément trouvé", - "scroll-to-top": "Défiler vers le haut" - }, - "help": { - "goto-help-page": "Aller à la page d'aide" - }, - "home": { - "avatar": "Avatar", - "home": "Accueil", - "logout": "Déconnexion", - "menu": "Menu", - "open-user-menu": "Ouvrir le menu utilisateur", - "profile": "Profile" - }, - "icon": { - "icon": "Icône", - "material-icons": "Icônes matérielles", - "select-icon": "Sélectionner l'icône", - "show-all": "Afficher toutes les icônes" - }, - "import": { - "drop-file": "Déposez un fichier JSON ou cliquez pour sélectionner un fichier à télécharger.", - "no-file": "Aucun fichier sélectionné" - }, - "item": { - "selected": "Sélectionné" - }, - "js-func": { - "no-return-error": "La fonction doit renvoyer une valeur!", - "return-type-mismatch": "La fonction doit renvoyer une valeur de type '{{type}}' !", - "tidy": "Nettoyer" - }, - "key-val": { - "add-entry": "Ajouter une entrée", - "key": "Clé", - "no-data": "Aucune entrée", - "remove-entry": "Supprimer l'entrée", - "value": "Valeur" - }, - "language": { - "language": "Language" - }, - "layout": { - "color": "Couleur", - "layout": "Mise en page", - "main": "Principal", - "manage": "Gérer les mises en page", - "right": "Droite", - "select": "Sélectionner la mise en page cible", - "settings": "Paramètres de mise en page" - }, - "legend": { - "avg": "moy", - "max": "max", - "min": "min", - "position": "Position de la légende", - "settings": "Paramètres de la légende", - "show-avg": "Afficher la valeur moyenne", - "show-max": "Afficher la valeur maximale", - "show-min": "Afficher la valeur min", - "show-total": "Afficher la valeur totale", - "total": "total" - }, - "login": { - "create-password": "Créer un mot de passe", - "email": "Email", - "forgot-password": "Mot de passe oublié?", - "login": "Connexion", - "new-password": "Nouveau mot de passe", - "new-password-again": "nouveau mot de passe", - "password-again": "Mot de passe à nouveau", - "password-link-sent-message": "Le lien de réinitialisation du mot de passe a été envoyé avec succès!", - "password-reset": "Mot de passe réinitialisé", - "passwords-mismatch-error": "Les mots de passe saisis doivent être identiques!", - "remember-me": "Se souvenir de moi", - "request-password-reset": "Demander la réinitialisation du mot de passe", - "reset-password": "Réinitialiser le mot de passe", - "sign-in": "Veuillez vous connecter", - "username": "Nom d'utilisateur (courriel)", - "login-with": "Se connecter avec {{name}}", - "or": "ou" - }, - "position": { - "bottom": "Bas", - "left": "Gauche", - "right": "Droite", - "top": "Haut" - }, - "profile": { - "change-password": "Modifier le mot de passe", - "current-password": "Mot de passe actuel", - "last-login-time": "Dernière connexion", - "profile": "Profile" - }, - "relation": { - "add": "Ajouter une relation", - "add-relation-filter": "Ajouter un filtre de relation", - "additional-info": "Informations supplémentaires (JSON)", - "any-relation": "toute relation", - "any-relation-type": "N'importe quel type", - "delete": "Supprimer la relation", - "delete-from-relation-text": "Attention, après la confirmation, l'entité actuelle ne sera pas liée à l'entité '{{entityName}}'.", - "delete-from-relation-title": "Êtes-vous sûr de vouloir supprimer la relation de l'entité '{{entityName}}'?", - "delete-from-relations-text": "Attention, après la confirmation, toutes les relations sélectionnées seront supprimées et l'entité actuelle ne sera pas liée aux entités correspondantes.", - "delete-from-relations-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 relation} other {# relations}}?", - "delete-to-relation-text": "Attention, après la confirmation, l'entité '{{entityName}} ne sera plus liée à l'entité actuelle.", - "delete-to-relation-title": "Êtes-vous sûr de vouloir supprimer la relation avec l'entité '{{entityName}}'?", - "delete-to-relations-text": "Attention, après la confirmation, toutes les relations sélectionnées seront supprimées et les entités correspondantes ne seront pas liées à l'entité en cours.", - "delete-to-relations-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 relation} other {# relations}}?", - "direction": "Sens", - "direction-type": { - "FROM": "de", - "TO": "à" - }, - "edit": "Modifier la relation", - "from-entity": "De l'entité", - "from-entity-name": "Du nom d'entité", - "from-entity-type": "Du type d'entité", - "from-relations": "Relations sortantes", - "invalid-additional-info": "Impossible d'analyser les informations supplémentaires json.", - "relation-filters": "Filtres de relation", - "relation-type": "Type de relation", - "relation-type-required": "Le type de relation est requis.", - "relations": "Relations", - "remove-relation-filter": "Supprimer le filtre de relation", - "search-direction": { - "FROM": "De", - "TO": "Vers" - }, - "selected-relations": "{count, plural, 1 {1 relation} other {# relations}} sélectionné", - "to-entity": "Vers l'entité", - "to-entity-name": "vers le nom de l'entité", - "to-entity-type": "Vers le type d'entité", - "to-relations": "Relations entrantes", - "type": "Type" - }, - "rulechain": { - "add": "Ajouter une chaîne de règles", - "add-rulechain-text": "Ajouter une nouvelle chaîne de règles", - "copyId": "Copier l'identifiant de la chaîne de règles", - "create-new-rulechain": "Créer une nouvelle chaîne de règles", - "debug-mode": "Mode de débogage", - "delete": "Supprimer la chaîne de règles", - "delete-rulechain-text": "Attention, après la confirmation, la chaîne de règles et toutes les données associées deviendront irrécupérables.", - "delete-rulechain-title": "Voulez-vous vraiment supprimer la chaîne de règles '{{ruleChainName}}'?", - "delete-rulechains-action-title": "Supprimer {count, plural, 1 {1 chaîne de règles} other {# chaînes de règles}}", - "delete-rulechains-text": "Attention, après la confirmation, toutes les chaînes de règles sélectionnées seront supprimées et toutes les données associées deviendront irrécupérables.", - "delete-rulechains-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 chaîne de règles} other {# chaînes de règles}}?", - "description": "Description", - "details": "Détails", - "events": "Evénements", - "export": "Exporter la chaîne de règles", - "export-failed-error": "Impossible d'exporter la chaîne de règles: {{error}}", - "idCopiedMessage": "L'ID de la chaîne de règles a été copié dans le presse-papier", - "import": "Importer la chaîne de règles", - "invalid-rulechain-file-error": "Impossible d'importer la chaîne de règles: structure de données de la chaîne de règles non valide", - "management": "Gestion des règles", - "name": "Nom", - "name-required": "Le nom est requis.", - "no-rulechains-matching": "Aucune chaîne de règles correspondant à {{entity}} n'a été trouvée.", - "no-rulechains-text": "Aucune chaîne de règles trouvée", - "root": "Racine", - "rulechain": "Chaîne de règles", - "rulechain-details": "Détails de la chaîne de règles", - "rulechain-file": "Fichier de chaîne de règles", - "rulechain-required": "Chaîne de règles requise", - "rulechains": "Chaînes de règles", - "select-rulechain": "Sélectionner la chaîne de règles", - "set-root": "Rend la chaîne de règles racine (root) ", - "set-root-rulechain-text": "Après la confirmation, la chaîne de règles deviendra racine (root) et gérera tous les messages de transport entrants.", - "set-root-rulechain-title": "Voulez-vous vraiment que la chaîne de règles '{{ruleChainName}} soit racine (root) ?", - "system": "Système" - }, - "rulenode": { - "add": "Ajouter un noeud de règle", - "add-link": "Ajouter un lien", - "configuration": "Configuration", - "copy-selected": "Copier les éléments sélectionnés", - "create-new-link-label": "Créez un nouveau!", - "custom-link-label": "Etiquette de lien personnalisée", - "custom-link-label-required": "Une étiquette de lien personnalisée est requise", - "debug-mode": "Mode de débogage", - "delete": "Supprimer le noeud de règle", - "delete-selected": "Supprimer les éléments sélectionnés", - "delete-selected-objects": "Supprimer les nœuds et les connexions sélectionnés", - "description": "Description", - "deselect-all": "Désélectionner tout", - "deselect-all-objects": "Désélectionnez tous les nœuds et toutes les connexions", - "details": "Détails", - "directive-is-not-loaded": "La directive de configuration définie '{{directiveName}} n'est pas disponible.", - "events": "Événements", - "help": "Aide", - "invalid-target-rulechain": "Impossible de résoudre la chaîne de règles cible!", - "link": "Lien", - "link-details": "Détails du lien du noeud de la règle", - "link-label": "Étiquette du lien", - "link-label-required": "L'étiquette du lien est obligatoire", - "link-labels": "Étiquettes de lien", - "link-labels-required": "Les étiquettes de lien sont obligatoires", - "message": "Message", - "message-type": "Type de message", - "message-type-required": "Le type de message est obligatoire", - "metadata": "Métadonnées", - "metadata-required": "Les entrées de métadonnées ne peuvent pas être vides.", - "name": "Nom", - "name-required": "Le nom est requis.", - "no-link-label-matching": "'{{label}}' introuvable.", - "no-link-labels-found": "Aucune étiquette de lien trouvée", - "open-node-library": "Ouvrir la bibliothèque de noeud", - "output": "Output", - "rulenode-details": "Détails du noeud de la régle", - "search": "Recherche de noeuds", - "select-all": "Tout sélectionner", - "select-all-objects": "Sélectionnez tous les noeuds et connexions", - "select-message-type": "Sélectionner le type de message", - "test": "Test", - "test-script-function": "Tester le script", - "type": "Type", - "type-action": "Action", - "type-action-details": "Effectuer une action spéciale", - "type-enrichment": "Enrichissement", - "type-enrichment-details": "Ajouter des informations supplémentaires dans les métadonnées de message", - "type-external": "Externe", - "type-external-details": "Interagit avec le systéme externe", - "type-filter": "Filtre", - "type-filter-details": "Filtrer les messages entrants avec des conditions configurées", - "type-input": "Input", - "type-input-details": "Entrée logique de la chaîne de règles, transmet les messages entrants au prochain nœud de règle associé", - "type-rule-chain": "Chaîne de régles", - "type-rule-chain-details": "Transmet les messages entrants à la chaîne de régles spécifiée", - "type-transformation": "Transformation", - "type-transformation-details": "Modifier le payload du message et les métadonnées ", - "type-unknown": "Inconnu", - "type-unknown-details": "Noeud de règle non résolu", - "ui-resources-load-error": "Impossible de charger les ressources de configuration de l'interface utilisateur." - }, - "tenant": { - "add": "Ajouter un Tenant", - "add-tenant-text": "Ajouter un nouveau Tenant", - "admins": "Admins", - "copyId": "Copier l'Id du Tenant", - "delete": "Supprimer le Tenant", - "delete-tenant-text": "Attention, après la confirmation, le Tenant et toutes les données associées deviendront irrécupérables.", - "delete-tenant-title": "Êtes-vous sûr de vouloir supprimer le tenant '{{tenantTitle}}'?", - "delete-tenants-action-title": "Supprimer {count, plural, 1 {1 tenant} other {# tenants}}", - "delete-tenants-text": "Attention, après la confirmation, tous les Tenants sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", - "delete-tenants-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 tenant} other {# tenants}}?", - "description": "Description", - "details": "Détails", - "events": "Événements", - "idCopiedMessage": "L'Id du Tenant a été copié dans le Presse-papiers", - "manage-tenant-admins": "Gérer les administrateurs du Tenant", - "management": "Gestion des Tenants", - "no-tenants-matching": "Aucun Tenant correspondant à {{entity}} n'a été trouvé. ", - "no-tenants-text": "Aucun Tenant trouvé", - "select-tenant": "Sélectionner un Tenant", - "tenant": "Tenant", - "tenant-details": "Détails du Tenant", - "tenant-required": "Tenant requis", - "tenants": "Tenants", - "title": "Titre", - "title-required": "Le titre est requis." - }, - "timeinterval": { - "advanced": "Avancé", - "days": "Jours", - "days-interval": "{days, plural, 1 {1 jour} other {# jours}}", - "hours": "Heures", - "hours-interval": "{hours, plural, 1 {1 heure} other {# heures}}", - "minutes": "Minutes", - "minutes-interval": "{minutes, plural, 1 {1 minute} other {# minutes}}", - "seconds": "Secondes", - "seconds-interval": "{seconds, plural, 1 {1 seconde} other {# secondes}}" - }, - "timewindow": { - "date-range": "Plage de dates", - "days": "{days, plural, 1 {jour} other {# jours}}", - "edit": "Modifier timewindow", - "history": "Historique", - "hours": "{hours, plural, 0 {heure} 1 {1 heure} other {# heures}}", - "last": "Dernier", - "last-prefix": "dernier", - "minutes": "{minutes, plural, 0 {minute} 1 {1 minute} other {# minutes}}", - "period": "de {{startTime}} à {{endTime}}", - "realtime": "Temps réel", - "seconds": "{seconds, plural, 0 {second} 1 {1 second} other {# seconds}}", - "time-period": "Période", - "hide": "Masquer" - }, - "user": { - "activation-email-sent-message": "Le courriel d'activation a été envoyé avec succès!", - "activation-link": "Lien d'activation utilisateur", - "activation-link-copied-message": "le lien d'activation de l'utilisateur a été copié dans le presse-papier", - "activation-link-text": "Pour activer l'utilisateur, utilisez le lien d'activation suivant: ", - "activation-method": "Méthode d'activation", - "add": "Ajouter un utilisateur", - "add-user-text": "Ajouter un nouvel utilisateur", - "always-fullscreen": "Toujours en plein écran", - "anonymous": "Anonyme", - "copy-activation-link": "Copier le lien d'activation", - "customer": "Client", - "customer-users": "Utilisateurs du client", - "default-dashboard": "Tableau de bord par défaut", - "delete": "Supprimer l'utilisateur", - "delete-user-text": "Attention, après la confirmation, l'utilisateur et toutes les données associées deviendront irrécupérables.", - "delete-user-title": "Êtes-vous sûr de vouloir supprimer l'utilisateur '{{userEmail}}'?", - "delete-users-action-title": "Supprimer {count, plural, 1 {1 utilisateur} other {# utilisateurs}}", - "delete-users-text": "Attention, après la confirmation, tous les utilisateurs sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", - "delete-users-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 utilisateur} other {# utilisateurs}}?", - "description": "Description", - "details": "Détails", - "disable-account": "Désactiver le compte d'utilisateur", - "disable-account-message": "Le compte d'utilisateur a été désactivé avec succès!", - "display-activation-link": "Afficher le lien d'activation", - "email": "Email", - "email-required": "Email est requis.", - "enable-account": "Activer le compte d'utilisateur", - "enable-account-message": "Le compte d'utilisateur a été activé avec succès!", - "first-name": "Prénom", - "invalid-email-format": "Format de courrier électronique non valide", - "last-name": "Nom de famille", - "login-as-customer-user": "Se connecter en tant qu'utilisateur client", - "login-as-tenant-admin": "Connectez-vous en tant qu'administrateur Tenant", - "no-users-matching": "Aucun utilisateur correspondant à '{{entity}}' n'a été trouvé.", - "no-users-text": "Aucun utilisateur trouvé", - "resend-activation": "Renvoyer l'activation", - "select-user": "Sélectionner l'utilisateur", - "send-activation-mail": "Envoyer un mail d'activation", - "sys-admin": "Administrateur du système", - "tenant-admin": "Administrateur du Tenant", - "tenant-admins": "administrateurs du Tenant", - "user": "utilisateur", - "user-details": "Détails de l'utilisateur", - "user-required": "L'utilisateur est requis", - "users": "Utilisateurs" - }, - "value": { - "boolean": "booléen", - "boolean-value": "Valeur booléenne", - "double": "Double", - "double-value": "Valeur double", - "false": "Faux", - "integer": "Entier", - "integer-value": "Valeur entière", - "invalid-integer-value": "Valeur entière invalide", - "long": "Long", - "string": "String", - "string-value": "Valeur String", - "true": "Vrai", - "type": "Type de valeur" - }, - "widget": { - "add": "Ajouter un widget", - "add-resource": "Ajouter une ressource", - "add-widget-type": "Ajouter un nouveau type de widget", - "alarm": "Widget d'alarme", - "css": "CSS", - "datakey-settings-schema": "Schéma des paramètres de Data key", - "edit": "Modifier le widget", - "editor": " Editeur de widget", - "export": "Exporter widget", - "html": "HTML", - "javascript": "Javascript", - "latest-values": "Dernières valeurs", - "management": "Gestion des widgets", - "missing-widget-title-error": "Le titre du widget doit être spécifié!", - "no-data-found": "Aucune donnée trouvée", - "remove": "Supprimer le widget", - "remove-resource": "Supprimer une ressource", - "remove-widget-text": "Après la confirmation, le widget et toutes les données associées deviendront irrécupérables.", - "remove-widget-title": "Êtes-vous sûr de vouloir supprimer le widget '{{widgetTitle}}'?", - "remove-widget-type": "Supprimer le type de widget", - "remove-widget-type-text": "Après la confirmation, le type de widget et toutes les données associées deviendront irrécupérables.", - "remove-widget-type-title": "Êtes-vous sûr de vouloir supprimer le type de widget '{{widgetName}}'?", - "resource-url": "URL JavaScript / CSS", - "resources": "Ressources", - "rpc": "Widget de contrôle", - "run": "Exécuter un widget", - "save": "Enregistrer le widget", - "save-widget-type-as": "Enregistrer le type de widget sous", - "save-widget-type-as-text": "Veuillez saisir un nouveau titre de widget et / ou sélectionner un ensemble de widgets cibles", - "saveAs": "Enregistrer le widget sous", - "search-data": "Rechercher des données", - "select-widget-type": "Sélectionnez le type de widget", - "select-widgets-bundle": "Sélectionner un ensemble de widgets", - "settings-schema": "Schéma des paramétres", - "static": "Widget statique", - "tidy": "Nettoyer", - "timeseries": "Séries chronologiques", - "title": "Titre du widget", - "title-required": "Le titre du widget est requis.", - "toggle-fullscreen": "Basculer le mode plein écran", - "type": "Type de widget", - "unable-to-save-widget-error": "Impossible de sauvegarder le widget! Le widget a des erreurs!", - "undo": "Annuler les modifications du widget", - "widget-bundle": "Ensemble de widget", - "widget-library": "Bibliothèque de widgets", - "widget-saved": "Widget enregistré", - "widget-template-load-failed-error": "Impossible de charger le modéle de widget!", - "widget-type-load-error": "Le widget n'a pas été chargé à cause des erreurs suivantes:", - "widget-type-load-failed-error": "Impossible de charger le type de widget!", - "widget-type-not-found": "Problème de chargement de la configuration du widget.
    Le type de widget associé a probablement été supprimé." - }, - "widget-action": { - "custom": "Action personnalisée", - "header-button": "Bouton d'en-tête de widget", - "open-dashboard": "Naviguer vers un autre tableau de bord", - "open-dashboard-state": "Naviguer vers un nouvel état du tableau de bord", - "open-right-layout": "Ouvrir la disposition du tableau de bord droite (vue mobile)", - "set-entity-from-widget": "Définir l'entité à partir du widget", - "target-dashboard": "Tableau de bord cible", - "target-dashboard-state": "État du tableau de bord cible", - "target-dashboard-state-required": "L'état du tableau de bord cible est requis", - "update-dashboard-state": "Mettre à jour l'état actuel du tableau de bord" - }, - "widget-config": { - "action": "Action", - "action-icon": "Icône", - "action-name": "Nom", - "action-name-not-unique": "Une autre action portant le même nom existe déjà.
    Le nom de l'action doit être unique dans la même source d'action.", - "action-name-required": "Le nom de l'action est requis", - "action-source": "Source de l'action", - "action-source-required": "Une source d'action est requise.", - "action-type": "Type", - "action-type-required": "Le type d'action est requis.", - "actions": "Actions", - "add-action": "Ajouter une action", - "add-datasource": "Ajouter une source de données", - "advanced": "Avancé", - "alarm-source": "Source d'alarme", - "background-color": "couleur de fond", - "data": "Données", - "datasource-parameters": "Paramètres", - "datasource-type": "Type", - "datasources": "Sources de données", - "decimals": "Nombre de chiffres après virgule flottante", - "delete-action": "Supprimer l'action", - "delete-action-text": "Êtes-vous sûr de vouloir supprimer l'action du widget nommé '{{actionName}}'?", - "delete-action-title": "Supprimer l'action du widget", - "display-timewindow": "Afficher fenêtre de temps", - "display-legend": "Afficher la légende", - "display-title": "Afficher le titre", - "drop-shadow": "Ombre portée", - "edit-action": "Modifier l'action", - "enable-fullscreen": "Activer le plein écran", - "general-settings": "Paramètres généraux", - "height": "Hauteur", - "margin": "Marge", - "maximum-datasources": "Maximum {count, plural, 1 {1 datasource est autorisé.} other {# datasources sont autorisés}}", - "mobile-mode-settings": "Paramètres du mode mobile", - "order": "Ordre", - "padding": "Padding", - "remove-datasource": "Supprimer la source de données", - "search-actions": "Recherche d'actions", - "settings": "Paramètres", - "target-device": "Dispositif cible", - "text-color": "Couleur du texte", - "timewindow": "Fenêtre de temps", - "title": "Titre", - "title-style": "Style de titre", - "title-tooltip": "Tooltip de titre", - "units": "Symbole spécial à afficher à côté de la valeur", - "use-dashboard-timewindow": "Utiliser la fenêtre de temps du tableau de bord", - "widget-style": "Style du widget", - "display-icon": "Afficher l'icône du titre", - "icon-color": "Couleur de l'icône", - "icon-size": "Taille de l'icône" - }, - "widget-type": { - "create-new-widget-type": "Créer un nouveau type de widget", - "export": "Exporter le type de widget", - "export-failed-error": "Impossible d'exporter le type de widget: {{error}}", - "import": "Importer le type de widget", - "invalid-widget-type-file-error": "Impossible d'importer le type de widget: structure de données de type widget invalide.", - "widget-type-file": "Fichier de type Widget" - }, - "widgets": { - "date-range-navigator": { - "localizationMap": { - "Sun": "Dim.", - "Mon": "Lun.", - "Tue": "Mar.", - "Wed": "Mer.", - "Thu": "Jeu.", - "Fri": "Ven.", - "Sat": "Sam.", - "Jan": "Janv.", - "Feb": "Févr.", - "Mar": "Mars", - "Apr": "Avr.", - "May": "Mai", - "Jun": "Juin", - "Jul": "Juil.", - "Aug": "Août", - "Sep": "Sept.", - "Oct": "Oct.", - "Nov": "Nov.", - "Dec": "Déc.", - "January": "Janvier", - "February": "Février", - "March": "Mars", - "April": "Avril", - "June": "Juin", - "July": "Juillet", - "August": "Août", - "September": "Septembre", - "October": "Octobre", - "November": "Novembre", - "December": "Décembre", - "Custom Date Range": "Plage de dates personnalisée", - "Date Range Template": "Modèle de plage de dates", - "Today": "Aujourd'hui", - "Yesterday": "Hier", - "This Week": "Cette semaine", - "Last Week": "La semaine dernière", - "This Month": "Ce mois-ci", - "Last Month": "Le mois dernier", - "Year": "Année", - "This Year": "Cette année", - "Last Year": "L'année dernière", - "Date picker": "Sélecteur de date", - "Hour": "Heure", - "Day": "Journée", - "Week": "La semaine", - "2 weeks": "2 Semaines", - "Month": "Mois", - "3 months": "3 Mois", - "6 months": "6 Mois", - "Custom interval": "Intervalle personnalisé", - "Interval": "Intervalle", - "Step size": "Taille de pas", - "Ok": "Ok" - } - }, - "input-widgets": { - "attribute-not-allowed": "Le paramètre d'attribut ne peut pas être utilisé dans ce widget", - "date": "Date", - "discard-changes": "Annuler les modifications", - "entity-attribute-required": "L'attribut d'entité est requis", - "entity-timeseries-required": "Entité timeseries est requis", - "not-allowed-entity": "L'entité sélectionnée ne peut pas avoir d'attributs partagés", - "no-attribute-selected": "Aucun attribut n'est sélectionné", - "no-datakey-selected": "Aucune date n'est sélectionnée", - "no-entity-selected": "Aucune entité sélectionnée", - "no-image": "Pas d'image", - "no-support-web-camera": "Pas de webcam supportée", - "no-timeseries-selected": "Aucune série temporelle sélectionnée", - "switch-attribute-value": "Changer la valeur de l'attribut d'entité", - "switch-camera": "Changer de caméra", - "switch-timeseries-value": "Changer la valeur de l'entité série temporelle", - "take-photo": "Prendre une photo", - "time": "Temps", - "timeseries-not-allowed": "Le paramètre série temporelle ne peut pas être utilisé dans ce widget", - "update-failed": "Mise à jour a échoué", - "update-successful": "Mise à jour réussie", - "update-attribute": "Attribut de mise à jour", - "update-timeseries": "Mise à jour de la série temporelle", - "value": "Valeur" - } - }, - "widgets-bundle": { - "add": "Ajouter un groupe de widgets", - "add-widgets-bundle-text": "Ajouter un nouveau groupe de widgets", - "create-new-widgets-bundle": "Créer un nouveau groupe de widgets", - "current": "Groupe actuel", - "delete": "Supprimer le groupe de widgets", - "delete-widgets-bundle-text": "Attention, après la confirmation, le groupe de widgets et toutes les données associées deviendront irrécupérables.", - "delete-widgets-bundle-title": "Êtes-vous sûr de vouloir supprimer le groupe de widgets '{{widgetsBundleTitle}}'?", - "delete-widgets-bundles-action-title": "Supprimer {count, plural, 1 {1 groupe de widgets} other {# groupes de widgets}}", - "delete-widgets-bundles-text": "Attention, après la confirmation, tous les groupes de widgets sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", - "delete-widgets-bundles-title": "Voulez-vous vraiment supprimer {count, plural, 1 {1 groupe de widgets} other {# groupes de widgets}}?", - "details": "Détails", - "empty": "Le groupe de widgets est vide", - "export": "Exporter le groupe de widgets", - "export-failed-error": "Impossible d'exporter le groupe de widgets: {{error}}", - "import": "Importer un groupe de widgets", - "invalid-widgets-bundle-file-error": "Impossible d'importer un groupe de widgets: structure de données du groupe de widgets non valides.", - "no-widgets-bundles-matching": "Aucun groupe de widgets correspondant à {{widgetsBundle}} n'a été trouvé.", - "no-widgets-bundles-text": "Aucun groupe de widgets trouvé", - "system": "Système", - "title": "Titre", - "title-required": "Le titre est requis.", - "widgets-bundle-details": "Détails des groupes de widgets", - "widgets-bundle-file": "Fichier de groupe de widgets", - "widgets-bundle-required": "Un groupe de widgets est requis.", - "widgets-bundles": "Groupes de widgets" - } -} diff --git a/ui/src/app/locale/locale.constant-it_IT.json b/ui/src/app/locale/locale.constant-it_IT.json deleted file mode 100644 index 8d246c361d..0000000000 --- a/ui/src/app/locale/locale.constant-it_IT.json +++ /dev/null @@ -1,1708 +0,0 @@ -{ - "access": { - "unauthorized": "Non autorizzato", - "unauthorized-access": "Accesso non autorizzato", - "unauthorized-access-text": "Devi effettuare il login per accedere a questa risorsa!", - "access-forbidden": "Accesso Vietato", - "access-forbidden-text": "Non hai i diritti di accesso a questa posizione!
    Prova ad effettuare il login con un diverso account.", - "refresh-token-expired": "Sessione scaduta", - "refresh-token-failed": "Impossibile aggiornare la sessione" - }, - "action": { - "activate": "Attiva", - "suspend": "Sospendi", - "save": "Salva", - "saveAs": "Salva come", - "cancel": "Cancella", - "ok": "OK", - "delete": "Elimina", - "add": "Aggiungi", - "yes": "Sì", - "no": "No", - "update": "Aggiorna", - "remove": "Rimuovi", - "search": "Cerca", - "clear-search": "Cancella ricerca", - "assign": "Assegna", - "unassign": "Annulla assegnazione", - "share": "Condividi", - "make-private": "Rendi privato", - "apply": "Applica", - "apply-changes": "Applica modifiche", - "edit-mode": "Modalità modifica", - "enter-edit-mode": "Attiva modalità di modifica", - "decline-changes": "Annulla modifiche", - "close": "Chiudi", - "back": "Indietro", - "run": "Esegui", - "sign-in": "Registrati!", - "edit": "Modifica", - "view": "Visualizza", - "create": "Crea", - "drag": "Trascina", - "refresh": "Aggiorna", - "undo": "Annulla", - "copy": "Copia", - "paste": "Incolla", - "copy-reference": "Copia riferimento", - "paste-reference": "Incolla riferimento", - "import": "Importa", - "export": "Esporta", - "share-via": "Condividi con {{provider}}", - "discard-changes": "Annulla le modifiche" - }, - "aggregation": { - "aggregation": "Aggregazione", - "function": "Funzione di aggregazione dati", - "limit": "Valori max", - "group-interval": "Intervallo di raggruppamento", - "min": "Min", - "max": "Max", - "avg": "Media", - "sum": "Somma", - "count": "Conteggio", - "none": "Nessuna" - }, - "admin": { - "general": "Generale", - "general-settings": "Impostazioni Generali", - "outgoing-mail": "Posta in uscita", - "outgoing-mail-settings": "Impostazioni Posta in uscita", - "system-settings": "Impostazioni di sistema", - "test-mail-sent": "Mail di test inviata con successo!", - "base-url": "URL di base", - "base-url-required": "URL di base obbligatoria.", - "mail-from": "Mittente", - "mail-from-required": "Mittente obbligatorio.", - "smtp-protocol": "Protocollo SMTP", - "smtp-host": "Host SMTP", - "smtp-host-required": "Host SMTP obbligatorio.", - "smtp-port": "Porta SMTP", - "smtp-port-required": "Porta SMTP obbligatoria.", - "smtp-port-invalid": "Numero di porta SMTP non valido.", - "timeout-msec": "Timeout (msec)", - "timeout-required": "Timeout obbligatorio.", - "timeout-invalid": "Timeout non valido.", - "enable-tls": "Abilita TLS", - "tls-version" : "Versione TLS", - "send-test-mail": "Invia mail di test", - "security-settings": "Settaggi di sicurezza", - "password-policy": "Politica password", - "minimum-password-length": "Lunghezza minima password", - "minimum-password-length-required": "È richiesta una lunghezza minima della password", - "minimum-password-length-range": "La lunghezza minima della password deve essere compresa tra 5 e 50", - "minimum-uppercase-letters": "Numero minimo di lettere maiuscole", - "minimum-uppercase-letters-range": "Il numero minimo di lettere maiuscole non può essere negativo", - "minimum-lowercase-letters": "Numero minimo di lettere minuscole", - "minimum-lowercase-letters-range": "Il numero minimo di lettere minuscole non può essere negativo", - "minimum-digits": "Numero minimo di cifre", - "minimum-digits-range": "Il numero minimo di cifre non può essere negativo", - "minimum-special-characters": "Numero minimo di caratteri speciali", - "minimum-special-characters-range": "Il numero minimo di caratteri speciali non può essere negativo", - "password-expiration-period-days": "Periodo di scadenza della password in giorni", - "password-expiration-period-days-range": "Il periodo di scadenza della password in giorni non può essere negativo", - "password-reuse-frequency-days": "Frequenza di riutilizzo della password in giorni", - "password-reuse-frequency-days-range": "La frequenza di riutilizzo della password in giorni non può essere negativa", - "general-policy": "Politica generale", - "max-failed-login-attempts": "Numero massimo di tentativi di accesso non riusciti, prima che l'account sia bloccato", - "minimum-max-failed-login-attempts-range": "Il numero massimo di tentativi di accesso non riusciti non può essere negativo", - "user-lockout-notification-email": "In caso di blocco dell'account utente, inviare una notifica via e-mail" - }, - "alarm": { - "alarm": "Allarme", - "alarms": "Allarmi", - "select-alarm": "Seleziona un allarme", - "no-alarms-matching": "Nessun allarme corrispondente a '{{entity}}' è stato trovato.", - "alarm-required": "Allarme richiesto", - "alarm-status": "Stato Allarme", - "search-status": { - "ANY": "Qualsiasi", - "ACTIVE": "Attivo", - "CLEARED": "Cancellato", - "ACK": "Riconosciuto", - "UNACK": "Non riconosciuto" - }, - "display-status": { - "ACTIVE_UNACK": "Active Unacknowledged", - "ACTIVE_ACK": "Active Acknowledged", - "CLEARED_UNACK": "Cleared Unacknowledged", - "CLEARED_ACK": "Cleared Acknowledged" - }, - "no-alarms-prompt": "Nessun allarme trovato", - "created-time": "Orario di creazione", - "type": "Tipo", - "severity": "Livello di gravità", - "originator": "Origine", - "originator-type": "Tipo origine", - "details": "Dettagli", - "status": "Stato", - "alarm-details": "Dettagli allarme", - "start-time": "Orario inizio", - "end-time": "Orario fine", - "ack-time": "Orario conferma", - "clear-time": "Orario cancellazione", - "severity-critical": "Critico", - "severity-major": "Maggiore", - "severity-minor": "Minore", - "severity-warning": "Avviso", - "severity-indeterminate": "Indeterminato", - "acknowledge": "Conferma", - "clear": "Cancella", - "search": "Cerca allarmi", - "selected-alarms": "{ count, plural, 1 {1 allarme selezionato} other {# allarmi selezionati} }", - "no-data": "Nessun dato da visualizzare", - "polling-interval": "Intervallo di polling (sec) Allarmi", - "polling-interval-required": "Intervallo di polling Allarmi richiesto.", - "min-polling-interval-message": "L'intervallo di polling deve essere di almeno 1 sec.", - "aknowledge-alarms-title": "Conferma { count, plural, 1 {1 allarme} other {# allarmi} }", - "aknowledge-alarms-text": "Sei sicuro di voler confermare { count, plural, 1 {1 allarme} other {# allarmi} }?", - "aknowledge-alarm-title": "Conferma allarme", - "aknowledge-alarm-text": "Sei sicuro di voler confermare l'allarme?", - "clear-alarms-title": "Elimina { count, plural, 1 {1 allarme} other {# allarmi} }", - "clear-alarms-text": "Sei sicuro di voler eliminare { count, plural, 1 {1 allarme} other {# allarmi} }?", - "clear-alarm-title": "Elimina allarme", - "clear-alarm-text": "Sei sicuro di voler eliminare l'allarme?", - "alarm-status-filter": "Filtro stato allarme" - }, - "alias": { - "add": "Aggiungi alias", - "edit": "Modifica alias", - "name": "Nome Alias", - "name-required": "Nome Alias obbligatorio", - "duplicate-alias": "Un Alias con lo stesso nome è già presente.", - "filter-type-single-entity": "Singola entità", - "filter-type-entity-list": "Lista Entità", - "filter-type-entity-name": "Nome Entità", - "filter-type-state-entity": "Entità dallo stato della dashboard", - "filter-type-state-entity-description": "Entità prelevata dai parametri di stato della dashboard", - "filter-type-asset-type": "Tipo di Asset", - "filter-type-asset-type-description": "Asset di tipo '{{assetType}}'", - "filter-type-asset-type-and-name-description": "Asset di tipo '{{assetType}}' e con un nome che inizia per '{{prefix}}'", - "filter-type-device-type": "Tipo di dispositivo", - "filter-type-device-type-description": "Dispositivi di tipo '{{deviceType}}'", - "filter-type-device-type-and-name-description": "Dispositivi di tipo '{{deviceType}}' e con un nome che inizia per '{{prefix}}'", - "filter-type-entity-view-type": "Tipo vista entità", - "filter-type-entity-view-type-description": "Viste entità di tipo '{{entityView}}'", - "filter-type-entity-view-type-and-name-description": "Viste entità di tipo '{{entityView}}' e con un nome che inizia per '{{prefix}}'", - "filter-type-relations-query": "Query relazioni", - "filter-type-relations-query-description": "{{entities}} che hanno una relazione {{relationType}} {{direction}} {{rootEntity}}", - "filter-type-asset-search-query": "Query ricerca asset", - "filter-type-asset-search-query-description": "Asset di tipo {{assetTypes}} che hanno una relazione {{relationType}} {{direction}} {{rootEntity}}", - "filter-type-device-search-query": "Query ricerca dispositivo", - "filter-type-device-search-query-description": "Dispositivi di tipo {{deviceTypes}} che hanno una relazione {{relationType}} {{direction}} {{rootEntity}}", - "filter-type-entity-view-search-query": "Query ricerca Vista entità", - "filter-type-entity-view-search-query-description": "Viste entità di tipo {{entityViewTypes}} che hanno una relazione {{relationType}} {{direction}} {{rootEntity}}", - "entity-filter": "Filtro entità", - "resolve-multiple": "Risolvi come entità multiple", - "filter-type": "Tipo di filtro", - "filter-type-required": "Tipo di filtro richiesto.", - "entity-filter-no-entity-matched": "Nessuna entità corrispondente al filtro specificato è stata trovata.", - "no-entity-filter-specified": "Nessun filtro di entità specificato", - "root-state-entity": "Usa l'entità di stato della dashboard come radice", - "root-entity": "Entità radice", - "state-entity-parameter-name": "Nome parametro entità di stato", - "default-state-entity": "Entità di stato predefinita", - "default-entity-parameter-name": "Predefinito", - "max-relation-level": "Massimo livello relazione", - "unlimited-level": "Illimitato", - "state-entity": "Entità di stato della dashboard", - "all-entities": "Tutte le entità", - "any-relation": "qualsiasi" - }, - "asset": { - "asset": "Asset", - "assets": "Asset", - "management": "Gestione Asset", - "view-assets": "Visualizza Asset", - "add": "Aggiungi Asset", - "assign-to-customer": "Assegna a cliente", - "assign-asset-to-customer": "Assegna Asset al Cliente", - "assign-asset-to-customer-text": "Seleziona gli asset da assegnare al cliente", - "no-assets-text": "Nessun asset trovato", - "assign-to-customer-text": "Seleziona il cliente a cui assegnare l'asset / gli asset", - "public": "Pubblico", - "assignedToCustomer": "Assegnato al cliente", - "make-public": "Rendi pubblico l'asset", - "make-private": "Rendi privato l'asset", - "unassign-from-customer": "Assegnazione annullata dal cliente", - "delete": "Cancella asset", - "asset-public": "L'Asset è pubblico", - "asset-type": "Tipo di Asset", - "asset-type-required": "Tipo di Asset richiesto.", - "select-asset-type": "Seleziona tipo di asset", - "enter-asset-type": "Inserisci tipo di asset", - "any-asset": "Qualsiasi asset", - "no-asset-types-matching": "Nessun asset corrispondente al tipo '{{entitySubtype}}' è stato trovato.", - "asset-type-list-empty": "Nessun tipo di asset selezionato.", - "asset-types": "Tipi di Asset", - "name": "Nome", - "name-required": "Nome obbligatorio.", - "description": "Descrizione", - "type": "Tipo", - "type-required": "Tipo obbligatorio.", - "details": "Dettagli", - "events": "Eventi", - "add-asset-text": "Aggiungi un nuovo asset", - "asset-details": "Dettagli Asset", - "assign-assets": "Assegna asset", - "assign-assets-text": "Assegna { count, plural, 1 {1 asset} other {# asset} } al cliente", - "delete-assets": "Cancella asset", - "unassign-assets": "Annulla assegnazione asset", - "unassign-assets-action-title": "Annulla assegnazione { count, plural, 1 {1 asset} other {# asset} } al cliente", - "assign-new-asset": "Assegna un nuovo asset", - "delete-asset-title": "Sei sicuro di voler cancellare l'asset '{{assetName}}'?", - "delete-asset-text": "Attenzione, dopo la conferma l'asset e tutti i relativi dati non saranno più recuperabili.", - "delete-assets-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 asset} other {# asset} }?", - "delete-assets-action-title": "Elimina { count, plural, 1 {1 asset} other {# asset} }", - "delete-assets-text": "Attenzione, dopo la modifica tutti gli asset selezionati saranno rimossi e tutti i relativi dati non saranno più recuperabili.", - "make-public-asset-title": "Sei sicuro di voler rendere pubblico l'asset '{{assetName}}'?", - "make-public-asset-text": "Dopo la conferma l'asset e tutti i suoi dati saranno resi pubblici e accessibili dagli altri.", - "make-private-asset-title": "Sei sicuro di voler rendere privato l'asset '{{assetName}}'?", - "make-private-asset-text": "Dopo la conferma l'asset e tutti i suoi dati saranno resi privati e non accessibili dagli altri.", - "unassign-asset-title": "Sei sicuro di voler annullare l'assegnazione dell'asset '{{assetName}}'?", - "unassign-asset-text": "Dopo la conferma l'assegnazione dell'asset sarà annullata e l'asset non sarà più accessibile dal cliente.", - "unassign-asset": "Annulla assegnazione asset", - "unassign-assets-title": "Sei sicuro di voler annullare l'assegnazione di { count, plural, 1 {1 asset} other {# asset} }?", - "unassign-assets-text": "Dopo la conferma sarà annullata l'assegnazione di tutti gli asset selezionati e questi non saranno più accessibili dal cliente.", - "copyId": "Copia Id asset", - "idCopiedMessage": "Id Asset copiato negli Appunti", - "select-asset": "Seleziona asset", - "no-assets-matching": "Nessun asset corrispondente a '{{entity}}' è stato trovato.", - "asset-required": "Asset obbligatorio", - "name-starts-with": "Asset con nome che inizia per", - "label": "Etichetta" - }, - "attribute": { - "attributes": "Attributi", - "latest-telemetry": "Ultima telemetria", - "attributes-scope": "Visibilità attributi entità", - "scope-latest-telemetry": "Ultima telemetria", - "scope-client": "Attributi client", - "scope-server": "Attributi server", - "scope-shared": "Attributi condivisi", - "add": "Aggiungi attributo", - "key": "Chiave", - "last-update-time": "Ultimo aggiornamento", - "key-required": "Attributo chiave richiesto.", - "value": "Valore", - "value-required": "Attributo valore richiesto.", - "delete-attributes-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 attributo} other {# attributi} }?", - "delete-attributes-text": "Attenzione, dopo la conferma tutti gli attributi selezionati saranno rimossi.", - "delete-attributes": "Elimina attributi", - "enter-attribute-value": "Inserisci il valore dell'attributo", - "show-on-widget": "Mostra sul widget", - "widget-mode": "Modalità Widget", - "next-widget": "Widget successivo", - "prev-widget": "Widget precedente", - "add-to-dashboard": "Aggiungi alla dashboard", - "add-widget-to-dashboard": "Aggiungi widget alla dashboard", - "selected-attributes": "{ count, plural, 1 {1 attributo selezionato} other {# attributi selezionati} }", - "selected-telemetry": "{ count, plural, 1 {1 unità di telemetria selezionata} other {# unità di telemetria selezionate} }" - }, - "audit-log": { - "audit": "Audit", - "audit-logs": "Log Audit", - "timestamp": "Timestamp", - "entity-type": "Tipo Entità", - "entity-name": "Nome Entità", - "user": "Utente", - "type": "Tipo", - "status": "Stato", - "details": "Dettagli", - "type-added": "Aggiunto", - "type-deleted": "Eliminato", - "type-updated": "Aggiornato", - "type-attributes-updated": "Attributi aggiornati", - "type-attributes-deleted": "Attributi eliminati", - "type-rpc-call": "Chiamata RPC", - "type-credentials-updated": "Credenziali aggiornate", - "type-assigned-to-customer": "Assegnato al Cliente", - "type-unassigned-from-customer": "Assegnazione annullata dal Cliente", - "type-activated": "Attivato", - "type-suspended": "Sospeso", - "type-credentials-read": "Credenziali lette", - "type-attributes-read": "Attributi letti", - "type-relation-add-or-update": "Relazione aggiornata", - "type-relation-delete": "Relazione eliminata", - "type-relations-delete": "Eliminate tutte le relazioni", - "type-alarm-ack": "Confermato", - "type-alarm-clear": "Eliminato", - "type-login": "Accesso", - "type-logout": "Disconnettersi", - "type-lockout": "Bloccato", - "status-success": "Successo", - "status-failure": "Fallito", - "audit-log-details": "Dettaglio log audit", - "no-audit-logs-prompt": "Log non trovati", - "action-data": "Action data", - "failure-details": "Dettagli fallimento", - "search": "Cerca log audit", - "clear-search": "Cancella ricerca" - }, - "confirm-on-exit": { - "message": "Alcune modifiche non sono state salvate. Sei sicuro di voler abbandonare questa pagina?", - "html-message": "Alcune modifiche non sono state salvate.
    Sei sicuro di voler abbandonare questa pagina?", - "title": "Modifiche non salvate" - }, - "contact": { - "country": "Nazione", - "city": "Città", - "state": "Stato / Provincia", - "postal-code": "CAP", - "postal-code-invalid": "Formato CAP non valido.", - "address": "Indirizzo", - "address2": "Indirizzo 2", - "phone": "Telefono", - "email": "Email", - "no-address": "Nessun indirizzo" - }, - "common": { - "username": "Nome utente", - "password": "Password", - "enter-username": "Inserisci nome utente", - "enter-password": "Inserisci password", - "enter-search": "Cerca ..." - }, - "content-type": { - "json": "Json", - "text": "Testo", - "binary": "Binario (Base64)" - }, - "customer": { - "customer": "Cliente", - "customers": "Clienti", - "management": "Gestione cliente", - "dashboard": "Dashboard cliente", - "dashboards": "Dashboard cliente", - "devices": "Dispositivi cliente", - "entity-views": "Viste entità cliente", - "assets": "Asset cliente", - "public-dashboards": "Dashboard pubbliche", - "public-devices": "Dispositivi pubblici", - "public-assets": "Asset pubblici", - "public-entity-views": "Viste entità pubbliche", - "add": "Aggiungi cliente", - "delete": "Elimina cliente", - "manage-customer-users": "Gestisci utenti cliente", - "manage-customer-devices": "Gestisci dispositivi cliente", - "manage-customer-dashboards": "Gestisci dashboard cliente", - "manage-public-devices": "Gestisci dispositivi pubblici", - "manage-public-dashboards": "Gestisci dashboard pubbliche", - "manage-customer-assets": "Gestisci asset cliente", - "manage-public-assets": "Gestisci asset pubblici", - "add-customer-text": "Aggiungi nuovo cliente", - "no-customers-text": "Nessun cliente trovato", - "customer-details": "Dettagli cliente", - "delete-customer-title": "Sei sicuro di voler eliminare il cliente '{{customerTitle}}'?", - "delete-customer-text": "Attenzione, dopo la conferma il cliente e tutti i suoi dati non saranno più recuperabili.", - "delete-customers-title": "Sei sicuro di voler cancellare { count, plural, 1 {1 cliente} other {# clienti} }?", - "delete-customers-action-title": "Elimina { count, plural, 1 {1 cliente} other {# clienti} }", - "delete-customers-text": "Attenzione, dopo la conferma tutti i clienti selezionati saranno rimossi e i loro dati non saranno più recuperabili.", - "manage-users": "Gestisci utenti", - "manage-assets": "Gestisci asset", - "manage-devices": "Gestisci dispositivi", - "manage-dashboards": "Gestisci dashboard", - "title": "Titolo", - "title-required": "Titolo obbligatorio.", - "description": "Descrizione", - "details": "Dettagli", - "events": "Eventi", - "copyId": "Copia Id cliente", - "idCopiedMessage": "Id cliente copiato negli appunti", - "select-customer": "Seleziona cliente", - "no-customers-matching": "Nessun cliente corrispondente a '{{entity}}' è stato trovato.", - "customer-required": "Cliente obbligatorio", - "select-default-customer": "Seleziona cliente di default", - "default-customer": "Cliente di default", - "default-customer-required": "Il cliente di default è obbligatorio per il debug della dashboard a livello di Tenant" - }, - "datetime": { - "date-from": "Data da", - "time-from": "Orario da", - "date-to": "Data a", - "time-to": "Orario a" - }, - "dashboard": { - "dashboard": "Dashboard", - "dashboards": "Dashboard", - "management": "Gestione Dashboard", - "view-dashboards": "Mostra Dashboard", - "add": "Aggiungi Dashboard", - "assign-dashboard-to-customer": "Assegna Dashboard al cliente", - "assign-dashboard-to-customer-text": "Seleziona le dashboard da assegnare al client", - "assign-to-customer-text": "Seleziona il cliente a cui assegnare la/le dashboard", - "assign-to-customer": "Assegna al cliente", - "unassign-from-customer": "Annulla assegnazione al cliente", - "make-public": "Rendi pubblica la dashboard", - "make-private": "Rendi privata la dashboard", - "manage-assigned-customers": "Gestisci clienti assegnati", - "assigned-customers": "Clienti assegnati", - "assign-to-customers": "Assegna Dashboard ai Clienti", - "assign-to-customers-text": "Seleziona i clienti da assegnare alla/alle dashboard", - "unassign-from-customers": "Annulla assegnazione Dashboard ai Clienti", - "unassign-from-customers-text": "Seleziona i clienti di cui annullare l'assegnazione alla/alle dashboard", - "no-dashboards-text": "Nessuna dashboard trovata", - "no-widgets": "Nessun widget configurato", - "add-widget": "Aggiungi nuovo widget", - "title": "Titolo", - "select-widget-title": "Seleziona widget", - "select-widget-subtitle": "Elenco tipi di widget disponibili", - "delete": "Elimina dashboard", - "title-required": "Titolo obbligatorio.", - "description": "Descrizione", - "details": "Dettagli", - "dashboard-details": "Dettagli dashboard", - "add-dashboard-text": "Aggiungi nuova dashboard", - "assign-dashboards": "Assegna dashboard", - "assign-new-dashboard": "Assegna nuova dashboard", - "assign-dashboards-text": "Assegna { count, plural, 1 {1 dashboard} other {# dashboard} } ai clienti", - "unassign-dashboards-action-text": "Annulla assegnazione { count, plural, 1 {1 dashboard} other {# dashboard} } ai clienti", - "delete-dashboards": "Elimina dashboard", - "unassign-dashboards": "Annulla assegnazione dashboard", - "unassign-dashboards-action-title": "Annulla assegnazione { count, plural, 1 {1 dashboard} other {# dashboard} } al cliente", - "delete-dashboard-title": "Sei sicuro di voler cancellare la dashboard '{{dashboardTitle}}'?", - "delete-dashboard-text": "Attenzione, dopo la conferma la dashboard e tutti i suoi dati non saranno più recuperabili.", - "delete-dashboards-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 dashboard} other {# dashboard} }?", - "delete-dashboards-action-title": "Cancella { count, plural, 1 {1 dashboard} other {# dashboard} }", - "delete-dashboards-text": "Attenzione, dopo la conferma tutte le dashboard selezionate saranno eliminate e tutti i loro dati non saranno più recuperabili.", - "unassign-dashboard-title": "Sei sicuro di voler annullare l'assegnazione della dashboard '{{dashboardTitle}}'?", - "unassign-dashboard-text": "Dopo la conferma sarà annullata l'assegnazione della dashboard e questa non sarà più accessibile dal cliente.", - "unassign-dashboard": "Annulla assegnazione dashboard", - "unassign-dashboards-title": "Sei sicuro di voler annullare l'assegnazione di { count, plural, 1 {1 dashboard} other {# dashboard} }?", - "unassign-dashboards-text": "Dopo la conferma sarà annullata l'assegnazione di tutte le dashboard selezionate e queste non saranno più accessibili dal cliente.", - "public-dashboard-title": "La Dashboard è ora pubblica", - "public-dashboard-text": "La dashboard {{dashboardTitle}} è ora pubblica e accessibile al link:", - "public-dashboard-notice": "Nota: Ricorda di rendere pubblici i relativi dispositivi per accedere ai loro dati.", - "make-private-dashboard-title": "Sei sicuro di voler rendere privata la dashboard '{{dashboardTitle}}'?", - "make-private-dashboard-text": "Dopo la conferma la dashboard sarà resa privata e non più accessibile dagli altri.", - "make-private-dashboard": "Rendi privata la dashboard", - "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard", - "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard", - "select-dashboard": "Seleziona dashboard", - "no-dashboards-matching": "Nessuna dashboard corrispondente a '{{entity}}' è stata trovata.", - "dashboard-required": "Dashboard obbligatoria.", - "select-existing": "Seleziona una dashboard esistente", - "create-new": "Crea nuova dashboard", - "new-dashboard-title": "Titolo nuova dashboard", - "open-dashboard": "Apri dashboard", - "set-background": "Imposta sfondo", - "background-color": "Colore sfondo", - "background-image": "Immagine sfondo", - "background-size-mode": "Modalità dimensione sfondo", - "no-image": "Nessuna immagine selezionata", - "drop-image": "Trascina un'immagine o fai clic per selezionare un file da caricare.", - "settings": "Impostazioni", - "columns-count": "Numero colonne", - "columns-count-required": "Numero colonne obbligatorio.", - "min-columns-count-message": "Ammesso un numero minimo di colonne pari a 10.", - "max-columns-count-message": "Ammesso un numero massimo di colonne pari a 1000.", - "widgets-margins": "Margine tra i widget", - "horizontal-margin": "Margine orizzontale", - "horizontal-margin-required": "Margine orizzontale obbligatorio.", - "min-horizontal-margin-message": "Ammesso un margine orizzontale minimo pari a 0.", - "max-horizontal-margin-message": "Ammesso un margine orizzontale massimo pari a 50.", - "vertical-margin": "Margine verticale", - "vertical-margin-required": "Margine verticale obbligatorio.", - "min-vertical-margin-message": "Ammesso un margine verticale minimo pari a 0.", - "max-vertical-margin-message": "Ammesso un margine verticale massimo pari a 50.", - "autofill-height": "Riempi automaticamente altezza layout", - "mobile-layout": "Impostazioni layout mobile", - "mobile-row-height": "Altezza riga mobile (px)", - "mobile-row-height-required": "Altezza riga mobile è richiesta.", - "min-mobile-row-height-message": "5 pixel è il minimo concesso al valore altezza riga mobile.", - "max-mobile-row-height-message": "200 pixel è il massimo concesso al valore altezza riga mobile.", - "display-title": "Mostra titolo dashboard", - "toolbar-always-open": "Mantieni aperta la barra degli strumenti", - "title-color": "Colore titolo", - "display-dashboards-selection": "Mostra selezione dashboard", - "display-entities-selection": "Mostra selezione entità", - "display-dashboard-timewindow": "Mostra intervallo temporale", - "display-dashboard-export": "Mostra esportazione", - "import": "Importa dashboard", - "export": "Esporta dashboard", - "export-failed-error": "Impossibile esportare la dashboard: {{error}}", - "create-new-dashboard": "Crea nuova dashboard", - "dashboard-file": "File dashboard", - "invalid-dashboard-file-error": "Impossibile importare la dashboard: struttura dati della dashboard non valida.", - "dashboard-import-missing-aliases-title": "Configura alias utilizzati dalla dashboard importata", - "create-new-widget": "Crea nuovo widget", - "import-widget": "Importa widget", - "widget-file": "Widget file", - "invalid-widget-file-error": "Impossibile importare il widget: struttura dati del widget non valida.", - "widget-import-missing-aliases-title": "Configura gli alias utilizzati dai widget importati", - "open-toolbar": "Apri barra degli strumenti", - "close-toolbar": "Chiudi barra degli strumenti", - "configuration-error": "Errore di configurazione", - "alias-resolution-error-title": "Errore di configurazione degli alias della dashboard", - "invalid-aliases-config": "Impossibile trovare un dispositivo corrispondente ad un qualche filtro degli alias.
    Contatta l'amministratore per risolvere il problema.", - "select-devices": "Seleziona dispositivi", - "assignedToCustomer": "Assegnato al cliente", - "assignedToCustomers": "Assegnato ai clienti", - "public": "Pubblico", - "public-link": "Link pubblico", - "copy-public-link": "Copia link pubblico", - "public-link-copied-message": "Link pubblico della dashboard copiato negli appunti", - "manage-states": "Gestisci stati dashboard", - "states": "Stati dashboard", - "search-states": "Ricerca stati dashboard", - "selected-states": "{ count, plural, 1 {1 stato dashboard selezionato} other {# stati dashboard selezionati} }", - "edit-state": "Modifica stato dashboard", - "delete-state": "Elimina stato dashboard", - "add-state": "Aggiungi stato dashboard", - "state": "Stato dashboard", - "state-name": "Nome", - "state-name-required": "Nome stato dashboard obbligatorio.", - "state-id": "Id stato", - "state-id-required": "Id stato dashboard obbligatorio.", - "state-id-exists": "Uno stato della dashboard con lo stesso id è già presente.", - "is-root-state": "Stato radice", - "delete-state-title": "Elimina stato dashboard", - "delete-state-text": "Sei sicuro di voler eliminare lo stato della dashboard di nome '{{stateName}}'?", - "show-details": "Mostra dettagli", - "hide-details": "Nascondi dettagli", - "select-state": "Seleziona stato target", - "state-controller": "Stato controller" - }, - "datakey": { - "settings": "Impostazioni", - "advanced": "Avanzate", - "label": "Etichetta", - "color": "Colore", - "units": "Simbolo speciale da mostrare accanto al valore", - "decimals": "Numero cifre decimali", - "data-generation-func": "Funzione generazione dati", - "use-data-post-processing-func": "Usa funzione dopo il processamento dei dati", - "configuration": "Configurazione data key", - "timeseries": "Serie temporali", - "attributes": "Attributi", - "alarm": "Campi allarme", - "timeseries-required": "Le serie temporali dell'entità sono richieste.", - "timeseries-or-attributes-required": "Le serie temporali o gli attributi dell'entità sono richiesti.", - "maximum-timeseries-or-attributes": "Massimo { count, plural, 1 {1 serie temporale/attributo consentito.} other {# serie temporali/attributi consentiti.} }", - "alarm-fields-required": "Campi allarme obbligatori.", - "function-types": "Tipi funzione", - "function-types-required": "Tipi funzione obbligatorio.", - "maximum-function-types": "Massimo { count, plural, 1 {1 tipo di funzione consentito.} other {# tipi di funzione consentiti} }", - "time-description": "timestamp del valore corrente;", - "value-description": "il valore corrente;", - "prev-value-description": "risultato della precedente chiamata alla funzione;", - "time-prev-description": "timestamp del valore precedente;", - "prev-orig-value-description": "valore precedente originale;" - }, - "datasource": { - "type": "Tipo sorgente dati", - "name": "Nome", - "add-datasource-prompt": "Aggiungi una sorgente dati" - }, - "details": { - "edit-mode": "Modalità modifica", - "toggle-edit-mode": "Attiva/disattiva modalità di modifica" - }, - "device": { - "device": "Dispositivo", - "device-required": "Dispositivo richiesto.", - "devices": "Dispositivi", - "management": "Gestione dispositivo", - "view-devices": "Visualizza Dispositivi", - "device-alias": "Alias dispositivo", - "aliases": "Alias dispositivo", - "no-alias-matching": "'{{alias}}' non trovato.", - "no-aliases-found": "Nessun alias trovato.", - "no-key-matching": "'{{key}}' non trovata.", - "no-keys-found": "Nessuna chiave trovata.", - "create-new-alias": "Creane uno nuovo!", - "create-new-key": "Creane una nuova!", - "duplicate-alias-error": "Sono stati trovati dei duplicati dell'alias '{{alias}}'.
    Gli alias di un dispositivo devono essere univoci all'interno della dashboard.", - "configure-alias": "Configura alias '{{alias}}'", - "no-devices-matching": "Nessun dispositivo corrispondente a '{{entity}}' è stato trovato.", - "alias": "Alias", - "alias-required": "Alias dispositivo richiesto.", - "remove-alias": "Rimuovi alias dispositivo", - "add-alias": "Aggiungi alias dispositivo", - "name-starts-with": "Dispositivo il cui nome inizia per", - "device-list": "Lista dispositivi", - "use-device-name-filter": "Usa filtro", - "device-list-empty": "Nessun dispositivo selezionato.", - "device-name-filter-required": "Filtro nome dispositivo obbligatorio.", - "device-name-filter-no-device-matched": "Nessun dispositivo il cui nome inizia per '{{device}}' è stato trovato.", - "add": "Aggiungi Dispositivo", - "assign-to-customer": "Assegna al cliente", - "assign-device-to-customer": "Assegna dispositivo/dispositivi al Cliente", - "assign-device-to-customer-text": "Seleziona i dispositivi da assegnare al cliente", - "make-public": "Rendi pubblico il dispositivo", - "make-private": "Rendi privato il dispositivo", - "no-devices-text": "Nessun dispositivo trovato", - "assign-to-customer-text": "Seleziona il cliente a cui assegnare il dispositivo/i dispositivi", - "device-details": "Dettagli dispositivo", - "add-device-text": "Aggiungi nuovo dispositivo", - "credentials": "Credenziali", - "manage-credentials": "Gestisci credenziali", - "delete": "Elimina dispositivo", - "assign-devices": "Assegna dispositivi", - "assign-devices-text": "Assegna { count, plural, 1 {1 dispositivo} other {# dispositivi} } al cliente", - "delete-devices": "Elimina dispositivi", - "unassign-from-customer": "Annulla assegnazione al cliente", - "unassign-devices": "Annulla assegnazione dispositivi", - "unassign-devices-action-title": "Annulla assegnazione { count, plural, 1 {1 dispositivo} other {# dispositivi} } al cliente", - "assign-new-device": "Assegna nuovo dispositivo", - "make-public-device-title": "Sei sicuro di voler rendere pubblico il dispositivo '{{deviceName}}'?", - "make-public-device-text": "Dopo la conferma il dispositivo e tutti i suoi dati saranno resi pubblici e accessibili dagli altri.", - "make-private-device-title": "Sei sicuro di voler rendere privato il dispositivo '{{deviceName}}'?", - "make-private-device-text": "Dopo la conferma il dispositivo e tutti i suoi dati saranno resi privati e non più accessibili da altri utenti.", - "view-credentials": "Visualizza credenziali", - "delete-device-title": "Sei sicuro di voler eliminare il dispositivo '{{deviceName}}'?", - "delete-device-text": "Attenzione, dopo la conferma il dispositivo e tutti i suoi dati non saranno più recuperabili.", - "delete-devices-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 dispositivo} other {# dispositivi} }?", - "delete-devices-action-title": "Elimina { count, plural, 1 {1 dispositivo} other {# dispositivi} }", - "delete-devices-text": "Attenzione, dopo la conferma tutti i dispositivi selezionati saranno eliminati e i relativi dati non saranno più recuperabili.", - "unassign-device-title": "Sei sicuro di voler annullare l'assegnazione del dispositivo '{{deviceName}}'?", - "unassign-device-text": "Dopo la conferma sarà annullata l'assegnazione del dispositivo e questo non sarà più accessibile dal cliente.", - "unassign-device": "Annulla assegnazione dispositivo", - "unassign-devices-title": "Sei sicuro di voler annullare l'assegnazione di { count, plural, 1 {1 dispositivo} other {# dispositivi} }?", - "unassign-devices-text": "Dopo la conferma sarà annullata l'assegnazione di tutti i dispositivi selezionati e questi non saranno più accessibili dal cliente.", - "device-credentials": "Credenziali Dispositivo", - "credentials-type": "Tipo credenziali", - "access-token": "Token di accesso", - "access-token-required": "Token di accesso obbligatorio.", - "access-token-invalid": "Il token di accesso deve avere una lunghezza compresa tra 1 e 20 caratteri.", - "rsa-key": "Chiave pubblica RSA", - "rsa-key-required": "Chiave pubblica RSA obbligatoria.", - "secret": "Secret", - "secret-required": "Secret obbligatorio.", - "device-type": "Tipo dispositivo", - "device-type-required": "Tipo dispositivo obbligatorio.", - "select-device-type": "Seleziona tipo dispositivo", - "enter-device-type": "Inserisci typo dispositivo", - "any-device": "Qualsiasi dispositivo", - "no-device-types-matching": "Nessun dispositivo corrispondente a '{{entitySubtype}}' è stato trovato.", - "device-type-list-empty": "Nessun tipo di dispositivo selezionato.", - "device-types": "Tipi dispositivo", - "name": "Nome", - "name-required": "Nome obbligatorio.", - "description": "Descrizione", - "events": "Eventi", - "details": "Dettagli", - "copyId": "Copia Id dispositivo", - "copyAccessToken": "Copia token di accesso", - "idCopiedMessage": "Id dispositivo copiato negli Appunti", - "accessTokenCopiedMessage": "Token di accesso del dispositivo copiato negli Appunti", - "assignedToCustomer": "Assegnato al cliente", - "unable-delete-device-alias-title": "Impossibile rimuovere l'alias del dispositivo", - "unable-delete-device-alias-text": "L'alias del dispositivo '{{deviceAlias}}' non può essere eliminato perché utilizzato dai seguenti widget:
    {{widgetsList}}", - "is-gateway": "È un gateway", - "public": "Pubblico", - "device-public": "Il dispositivo è pubblico", - "select-device": "Seleziona dispositivo" - }, - "dialog": { - "close": "Close dialog" - }, - "direction": { - "column": "Colonna", - "row": "Riga" - }, - "error": { - "unable-to-connect": "Impossibile connettersi al server! Controlla la connessione ad Internet.", - "unhandled-error-code": "Codice errore non gestito: {{errorCode}}", - "unknown-error": "Errore sconosciuto" - }, - "entity": { - "entity": "Entità", - "entities": "Entità", - "aliases": "Alias entità", - "entity-alias": "Alias entità", - "unable-delete-entity-alias-title": "Impossibile eliminare alias entità", - "unable-delete-entity-alias-text": "L'alias dell'entità '{{entityAlias}}' non può essere eliminato perché utilizzato dai seguenti widget:
    {{widgetsList}}", - "duplicate-alias-error": "Trovato un duplicato dell'alias '{{alias}}'.
    Gli alias dell'entità devono essere univoci all'interno della dashboard.", - "missing-entity-filter-error": "Manca il filtro per l'alias '{{alias}}'.", - "configure-alias": "Configura '{{alias}}' alias", - "alias": "Alias", - "alias-required": "Alias entità obbligatorio.", - "remove-alias": "Rimuovi alias entità", - "add-alias": "Aggiungi alias entità", - "entity-list": "Lista entità", - "entity-type": "Tipo entità", - "entity-types": "Tipi entità", - "entity-type-list": "Lista tipo entità", - "any-entity": "Qualsiasi entità", - "enter-entity-type": "Inserisci tipo entità", - "no-entities-matching": "Nessuna entità corrispondente a '{{entity}}' è stata trovata.", - "no-entity-types-matching": "Nessun tipo di entità corrispondente a '{{entityType}}' è stato trovato.", - "name-starts-with": "Nome inizia per", - "use-entity-name-filter": "Usa filtro", - "entity-list-empty": "Nessuna entità selezionata.", - "entity-type-list-empty": "Nessun tipo di entità selezionato.", - "entity-name-filter-required": "Filtro nome entità obbligatorio.", - "entity-name-filter-no-entity-matched": "Nessuna entità che inizia per '{{entity}}' è stata trovata.", - "all-subtypes": "Tutte", - "select-entities": "Seleziona entità", - "no-aliases-found": "Nessun alias trovato.", - "no-alias-matching": "'{{alias}}' non trovato.", - "create-new-alias": "Creane uno nuovo!", - "key": "Chiave", - "key-name": "Nome chiave", - "no-keys-found": "Nessuna chiave trovata.", - "no-key-matching": "'{{key}}' non trovata.", - "create-new-key": "Creane una nuova!", - "type": "Tipo", - "type-required": "Tipo entità obbligatorio.", - "type-device": "Dispositivo", - "type-devices": "Dispositivi", - "list-of-devices": "{ count, plural, 1 {Un dispositivo} other {Lista di # dispositivi} }", - "device-name-starts-with": "Dispositivi i cui nomi iniziano per '{{prefix}}'", - "type-asset": "Asset", - "type-assets": "Asset", - "list-of-assets": "{ count, plural, 1 {Un asset} other {Lista di # asset} }", - "asset-name-starts-with": "Asset i cui nomi iniziano per '{{prefix}}'", - "type-entity-view": "Vista entità", - "type-entity-views": "Viste entità", - "list-of-entity-views": "{ count, plural, 1 {Una vista entità} other {Lista di # viste entità} }", - "entity-view-name-starts-with": "Viste entità i cui nomi iniziano per '{{prefix}}'", - "type-rule": "Regola", - "type-rules": "Regole", - "list-of-rules": "{ count, plural, 1 {Una regola} other {Lista di # regole} }", - "rule-name-starts-with": "Regole i cui nomi iniziano per '{{prefix}}'", - "type-plugin": "Plugin", - "type-plugins": "Plugin", - "list-of-plugins": "{ count, plural, 1 {Un plugin} other {Lista di # plugin} }", - "plugin-name-starts-with": "Plugin i cui nomi iniziano per '{{prefix}}'", - "type-tenant": "Tenant", - "type-tenants": "Tenants", - "list-of-tenants": "{ count, plural, 1 {One tenant} other {Lista di # tenants} }", - "tenant-name-starts-with": "Tenants whose names start with '{{prefix}}'", - "type-customer": "Cliente", - "type-customers": "Clienti", - "list-of-customers": "{ count, plural, 1 {Un cliente} other {Lista di # clienti} }", - "customer-name-starts-with": "Clienti i cui nomi iniziano per '{{prefix}}'", - "type-user": "Utente", - "type-users": "Utenti", - "list-of-users": "{ count, plural, 1 {Un utente} other {Lista di # utenti} }", - "user-name-starts-with": "Utenti i cui nomi iniziano per '{{prefix}}'", - "type-dashboard": "Dashboard", - "type-dashboards": "Dashboard", - "list-of-dashboards": "{ count, plural, 1 {Una dashboard} other {Lista di # dashboard} }", - "dashboard-name-starts-with": "Dashboard i cui nomi iniziano per '{{prefix}}'", - "type-alarm": "Allarme", - "type-alarms": "Allarmi", - "list-of-alarms": "{ count, plural, 1 {Un allarme} other {Lista di # allarmi} }", - "alarm-name-starts-with": "Allarmi i cui nomi iniziano per '{{prefix}}'", - "type-rulechain": "Rule chain", - "type-rulechains": "Rule chain", - "list-of-rulechains": "{ count, plural, 1 {Una rule chain} other {Lista di # catene di regole} }", - "rulechain-name-starts-with": "Catene di regole i cui nomi iniziano per '{{prefix}}'", - "type-rulenode": "Nodo regola", - "type-rulenodes": "Nodi regola", - "list-of-rulenodes": "{ count, plural, 1 {Un nodo regola} other {Lista di # nodi regola} }", - "rulenode-name-starts-with": "Nodi regola i cui nomi iniziano per '{{prefix}}'", - "type-current-customer": "Cliente attuale", - "search": "Ricerca entità", - "selected-entities": "{ count, plural, 1 {1 entità selezionata} other {# entità selezionate} }", - "entity-name": "Nome entità", - "details": "Dettagli entità", - "no-entities-prompt": "Nessuna entità trovata", - "no-data": "Nessun dato da mostrare", - "columns-to-display": "Colonne da mostrare" - }, - "entity-view": { - "entity-view": "Vista entità", - "entity-view-required": "Vista entità richiesta.", - "entity-views": "Viste entità", - "management": "Gestione viste entità", - "view-entity-views": "Visualizza Viste entità", - "entity-view-alias": "Alias vista entità", - "aliases": "Alias vista entità", - "no-alias-matching": "'{{alias}}' non trovato.", - "no-aliases-found": "Nessun alias trovato.", - "no-key-matching": "'{{key}}' non trovata.", - "no-keys-found": "Nessuna chiave trovata.", - "create-new-alias": "Creane uno nuovo!", - "create-new-key": "Creane una nuova!", - "duplicate-alias-error": "Sono stati trovati dei duplicati dell'alias '{{alias}}'.
    Gli alias di una vista entità devono essere univoci all'interno della dashboard.", - "configure-alias": "Configura alias '{{alias}}'", - "no-entity-views-matching": "Nessuna vista entità corrispondente a '{{entity}}' è stata trovato.", - "alias": "Alias", - "alias-required": "Alias vista entità richiesto.", - "remove-alias": "Rimuovi alias vista entità", - "add-alias": "Aggiungi alias vista entità", - "name-starts-with": "Vista entità il cui nome inizia per", - "entity-view-list": "Lista viste entità", - "use-entity-view-name-filter": "Usa filtro", - "entity-view-list-empty": "Nessuna vista entità selezionata.", - "entity-view-name-filter-required": "Filtro nome vista entità obbligatorio.", - "entity-view-name-filter-no-entity-view-matched": "Nessuna vista entità il cui nome inizia per '{{entity-view}}' è stata trovata.", - "add": "Aggiungi Vista entità", - "assign-to-customer": "Assegna al cliente", - "assign-entity-view-to-customer": "Assegna vista entità/viste entità al Cliente", - "assign-entity-view-to-customer-text": "Seleziona la vista entità da assegnare al cliente", - "no-entity-views-text": "Nessuna vista entità trovata", - "assign-to-customer-text": "Seleziona il cliente a cui assegnare la vista entità/le vista entità", - "entity-view-details": "Dettagli vista entità", - "add-entity-view-text": "Aggiungi nuova vista entità", - "delete": "Elimina vista entità", - "assign-entity-views": "Assegna viste entità", - "assign-entity-views-text": "Assegna { count, plural, 1 {1 vista entità} other {# viste entità} } al cliente", - "delete-entity-views": "Elimina viste entità", - "unassign-from-customer": "Annulla assegnazione al cliente", - "unassign-entity-views": "Annulla assegnazione viste entità", - "unassign-entity-views-action-title": "Annulla assegnazione { count, plural, 1 {1 vista entità} other {# viste entità} } al cliente", - "assign-new-entity-view": "Assegna nuova vista entità", - "delete-entity-view-title": "Sei sicuro di voler eliminare la vista entità '{{entity-viewName}}'?", - "delete-entity-view-text": "Attenzione, dopo la conferma la vista entità e tutti i suoi dati non saranno più recuperabili.", - "delete-entity-views-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 vista entità} other {# viste entità} }?", - "delete-entity-views-action-title": "Elimina { count, plural, 1 {1 vista entità} other {# viste entità} }", - "delete-entity-views-text": "Attenzione, dopo la conferma tutte le vista entità selezionati saranno eliminate e i relativi dati non saranno più recuperabili.", - "unassign-entity-view-title": "Sei sicuro di voler annullare l'assegnazione della vista entità '{{entity-viewName}}'?", - "unassign-entity-view-text": "Dopo la conferma sarà annullata l'assegnazione della vista entità e questa non sarà più accessibile dal cliente.", - "unassign-entity-view": "Annulla assegnazione vista entità", - "unassign-entity-views-title": "Sei sicuro di voler annullare l'assegnazione di { count, plural, 1 {1 vista entità} other {# viste entità} }?", - "unassign-entity-views-text": "Dopo la conferma sarà annullata l'assegnazione di tutte le vista entità selezionate e queste non saranno più accessibili dal cliente.", - "entity-view-type": "Tipo vista entità", - "entity-view-type-required": "Tipo vista entità obbligatorio.", - "select-entity-view-type": "Seleziona tipo vista entità", - "enter-entity-view-type": "Inserisci tipo vista entità", - "any-entity-view": "Qualsiasi vista entità", - "no-entity-view-types-matching": "Nessuna vista entità corrispondente a '{{entitySubtype}}' è stata trovata.", - "entity-view-type-list-empty": "Nessun tipo di vista entità selezionato.", - "entity-view-types": "Tipi vista entità", - "name": "Nome", - "name-required": "Nome obbligatorio.", - "description": "Descrizione", - "events": "Eventi", - "details": "Dettagli", - "copyId": "Copia Id vista entità", - "assignedToCustomer": "Assegnata al cliente", - "unable-entity-view-device-alias-title": "Impossibile rimuovere l'alias del vista entità", - "unable-entity-view-device-alias-text": "L'alias del vista entità '{{entity-viewAlias}}' non può essere eliminato perché utilizzato dai seguenti widget:
    {{widgetsList}}", - "select-entity-view": "Seleziona vista entità", - "make-public": "Rendi pubblica la vista entità", - "make-private": "Rendi privata la vista entità", - "start-date": "Data inizio", - "start-ts": "Ora inizio", - "end-date": "Data fine", - "end-ts": "Ora fine", - "date-limits": "Limiti temporali", - "client-attributes": "Attributi cliente", - "shared-attributes": "Attributi condivisi", - "server-attributes": "Attributi server", - "timeseries": "Serie temporali", - "client-attributes-placeholder": "Attributi cliente", - "shared-attributes-placeholder": "Attributi condivisi", - "server-attributes-placeholder": "Attributi server", - "timeseries-placeholder": "Serie temporali", - "target-entity": "Entità target", - "attributes-propagation": "Propagazione degli attributi", - "attributes-propagation-hint": "La vista entità copierà automaticamente gli attributi specificati dall'entità target ogni volta che questa vista entità sarà salvata e aggiornata. Per ragioni di performance, gli attributi dell'entità target non sono propagati alle viste entità ogni cambiamento di attributo. È possibile abilitare la propagazione automatica configurando il nodo regola \"Copia alla vista\" nella rule chain e collegando i messaggi \"Post attributes\" a \"Attributes Updated\" al nuovo nodo regola.", - "timeseries-data": "Dati delle serie temporali", - "timeseries-data-hint": "Imposta le chiavi delle serie temporali dell'entità target che saranno accessibili alla vista entità. Questi dati sono di sola lettura.", - "make-public-entity-view-title": "Sei sicuro di voler rendere pubblica la vista entità '{{entity-viewName}}'?", - "make-public-entity-view-text": "Dopo la conferma la vista entità e tutti i suoi dati saranno resi pubblici e accessibili dagli altri.", - "make-private-entity-view-title": "Sei sicuro di voler rendere privata la vista entità '{{entity-viewName}}'?", - "make-private-entity-view-text": "Dopo la conferma la vista entità e tutti i suoi dati saranno resi privati e non più accessibili da altri utenti." - }, - "event": { - "event-type": "Tipo evento", - "type-error": "Errore", - "type-lc-event": "Ciclo di vita evento", - "type-stats": "Statistiche", - "type-debug-rule-node": "Debug", - "type-debug-rule-chain": "Debug", - "no-events-prompt": "Nessun evento trovato", - "error": "Errore", - "alarm": "Allarme", - "event-time": "Orario evento", - "server": "Server", - "body": "Body", - "method": "Metodo", - "type": "Tipo", - "entity": "Entità", - "message-id": "Id Messaggio", - "message-type": "Tipo Messaggio", - "data-type": "Tipo di dato", - "relation-type": "Tipo di relazione", - "metadata": "Metadati", - "data": "Dati", - "event": "Evento", - "status": "Stato", - "success": "Success", - "failed": "Failed", - "messages-processed": "Messaggi elaborati", - "errors-occurred": "Si sono verificati degli errori" - }, - "extension": { - "extensions": "Estensioni", - "selected-extensions": "{ count, plural, 1 {1 estensione selezionata} other {# estensioni selezionate} }", - "type": "Tipo", - "key": "Chiave", - "value": "Valore", - "id": "Id", - "extension-id": "Id Estensione", - "extension-type": "Tipo Estensione", - "transformer-json": "JSON *", - "unique-id-required": "Id estensione corrente già esistente.", - "delete": "Elimina estensione", - "add": "Aggiungi estensione", - "edit": "Modifica estensione", - "delete-extension-title": "Sei sicuro di voler eliminare l'estensione '{{extensionId}}'?", - "delete-extension-text": "Attenzione, dopo la conferma l'estensione e tutti i suoi data non saranno più recuperabili.", - "delete-extensions-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 estensione} other {# estensioni} }?", - "delete-extensions-text": "Attenzione, dopo la conferma tutte le estensioni selezionate saranno eliminate.", - "converters": "Convertitori", - "converter-id": "Id convertitore", - "configuration": "Configurazione", - "converter-configurations": "Configurazioni convertitore", - "token": "Token di sicurezza", - "add-converter": "Aggiungi convertitore", - "add-config": "Aggiungi configurazione convertitore", - "device-name-expression": "Espressione nome dispositivo", - "device-type-expression": "Espressione tipo dispositivo", - "custom": "Custom", - "to-double": "To Double", - "transformer": "Transformer", - "json-required": "Transformer json is required.", - "json-parse": "Unable to parse transformer json.", - "attributes": "Attributi", - "add-attribute": "Aggiungi attributo", - "add-map": "Add mapping element", - "timeseries": "Serie temporali", - "add-timeseries": "Add timeseries", - "field-required": "Campo obbligatorio", - "brokers": "Broker", - "add-broker": "Aggiungi broker", - "host": "Host", - "port": "Porta", - "port-range": "Il numero di porta deve essere compreso tra 1 e 65535.", - "ssl": "Ssl", - "credentials": "Credenziali", - "username": "Nome utente", - "password": "Password", - "retry-interval": "Intervallo di ripetizione in millisecondi", - "anonymous": "Anonimo", - "basic": "Basic", - "pem": "PEM", - "ca-cert": "File certificato CA *", - "private-key": "File chiave privata *", - "cert": "File certificato *", - "no-file": "Nessun file selezionato.", - "drop-file": "Trascina un file o fai clic per selezionare un file da caricare.", - "mapping": "Mapping", - "topic-filter": "Filtro topic", - "converter-type": "Tipo convertitore", - "converter-json": "Json", - "json-name-expression": "Device name json expression", - "topic-name-expression": "Device name topic expression", - "json-type-expression": "Device type json expression", - "topic-type-expression": "Device type topic expression", - "attribute-key-expression": "Attribute key expression", - "attr-json-key-expression": "Attribute key json expression", - "attr-topic-key-expression": "Attribute key topic expression", - "request-id-expression": "Request id expression", - "request-id-json-expression": "Request id json expression", - "request-id-topic-expression": "Request id topic expression", - "response-topic-expression": "Response topic expression", - "value-expression": "Value expression", - "topic": "Topic", - "timeout": "Timeout in millisecondi", - "converter-json-required": "Convertitore json obbligatorio.", - "converter-json-parse": "Unable to parse converter json.", - "filter-expression": "Filter expression", - "connect-requests": "Richieste di connessione", - "add-connect-request": "Aggiungi richiesta di connessione", - "disconnect-requests": "Richieste di disconnessione", - "add-disconnect-request": "Aggiungi richiesta di disconnessione", - "attribute-requests": "Attribute requests", - "add-attribute-request": "Add attribute request", - "attribute-updates": "Attribute updates", - "add-attribute-update": "Add attribute update", - "server-side-rpc": "RPC lato server", - "add-server-side-rpc-request": "Add server-side RPC request", - "device-name-filter": "Filtro nome dispositivo", - "attribute-filter": "Filtro attributo", - "method-filter": "Filtro metodo", - "request-topic-expression": "Request topic expression", - "response-timeout": "Response timeout in milliseconds", - "topic-expression": "Topic expression", - "client-scope": "Visibilità client", - "add-device": "Aggiungi dispositivo", - "opc-server": "Server", - "opc-add-server": "Aggiungi server", - "opc-add-server-prompt": "Aggiungi server", - "opc-application-name": "Nome applicazione", - "opc-application-uri": "Uri applicazione", - "opc-scan-period-in-seconds": "Intervallo di scansione in secondi", - "opc-security": "Sicurezza", - "opc-identity": "Identità", - "opc-keystore": "Keystore", - "opc-type": "Tipo", - "opc-keystore-type": "Tipo", - "opc-keystore-location": "Location *", - "opc-keystore-password": "Password", - "opc-keystore-alias": "Alias", - "opc-keystore-key-password": "Chiave password", - "opc-device-node-pattern": "Device node pattern", - "opc-device-name-pattern": "Device name pattern", - "modbus-server": "Server/slave", - "modbus-add-server": "Aggiungi server/slave", - "modbus-add-server-prompt": "Aggiungi server/slave", - "modbus-transport": "Transport", - "modbus-tcp-reconnect": "Riconnessione automatica", - "modbus-rtu-over-tcp": "RTU over TCP", - "modbus-port-name": "Nome porta seriale", - "modbus-encoding": "Codifica", - "modbus-parity": "Parità", - "modbus-baudrate": "Baud rate", - "modbus-databits": "Data bits", - "modbus-stopbits": "Stop bits", - "modbus-databits-range": "Data bits deve essere compreso nell'intervallo 7-8.", - "modbus-stopbits-range": "Stop bits deve essere compreso nell'intervallo 1-2.", - "modbus-unit-id": "ID unità", - "modbus-unit-id-range": "ID unità deve essere compreso nell'intervallo 1-247.", - "modbus-device-name": "Nome dispositivo", - "modbus-poll-period": "Intervallo di polling (ms)", - "modbus-attributes-poll-period": "Intervallo di polling degli attributi (ms)", - "modbus-timeseries-poll-period": "Intervallo di polling delle serie temporali (ms)", - "modbus-poll-period-range": "L'intervallo di polling deve essere un valore positivo.", - "modbus-tag": "Tag", - "modbus-function": "Funzione", - "modbus-register-address": "Indirizzo registro", - "modbus-register-address-range": "Indirizzo registro deve essere compreso tra 0 e 65535.", - "modbus-register-bit-index": "Bit index", - "modbus-register-bit-index-range": "Bit index should be in a range from 0 to 15.", - "modbus-register-count": "Register count", - "modbus-register-count-range": "Register count should be a positive value.", - "modbus-byte-order": "Byte order", - "sync": { - "status": "Stato", - "sync": "Sincronizzato", - "not-sync": "Non sincronizzato", - "last-sync-time": "Ultima sincronizzazione", - "not-available": "Non disponibile" - }, - "export-extensions-configuration": "Esporta configurazione estensioni", - "import-extensions-configuration": "Importa configurazione estensioni", - "import-extensions": "Importa estensione", - "import-extension": "Importa estensione", - "export-extension": "Esporta estensione", - "file": "File estensione", - "invalid-file-error": "File estensione non valido" - }, - "fullscreen": { - "expand": "Espandi a tutto schermo", - "exit": "Esci da schermo intero", - "toggle": "Commuta modalità schermo intero", - "fullscreen": "Schermo intero" - }, - "function": { - "function": "Funzione" - }, - "grid": { - "delete-item-title": "Sei sicuro di voler eliminare questo elemento?", - "delete-item-text": "Attenzione, dopo la conferma questo elemento e tutti i suoi dati non saranno più recuperabili.", - "delete-items-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 elemento} other {# elementi} }?", - "delete-items-action-title": "Elimina { count, plural, 1 {1 elemento} other {# elementi} }", - "delete-items-text": "Attenzione, dopo la conferma tutti gli elementi selezionati saranno rimossi e i relativi dati non saranno più recuperabili.", - "add-item-text": "Aggiungi nuovo elemento", - "no-items-text": "Nessun elemento trovato", - "item-details": "Dettagli elemento", - "delete-item": "Elimina elemento", - "delete-items": "Elimina elementi", - "scroll-to-top": "Scorri verso l'alto" - }, - "help": { - "goto-help-page": "Vai all'help" - }, - "home": { - "home": "Home", - "profile": "Profilo", - "logout": "Logout", - "menu": "Menu", - "avatar": "Avatar", - "open-user-menu": "Apri menu utente" - }, - "import": { - "no-file": "Nessun file selezionato", - "drop-file": "Trascina un file JSON o fai clic per selezionare un file da caricare." - }, - "item": { - "selected": "Selezionata" - }, - "js-func": { - "no-return-error": "La funzione deve restituire un valore!", - "return-type-mismatch": "La funzione deve restituire un valore di tipo '{{type}}'!", - "tidy": "Tidy" - }, - "key-val": { - "key": "Chiave", - "value": "Valore", - "remove-entry": "Rimuovi voce", - "add-entry": "Aggiungi voce", - "no-data": "Nessuna voce" - }, - "layout": { - "layout": "Layout", - "manage": "Gestisci layout", - "settings": "Impostazioni layout", - "color": "Colore", - "main": "Main", - "right": "Destra", - "select": "Select target layout" - }, - "legend": { - "direction": "Direzione", - "position": "Posizione Legenda", - "show-max": "Mostra valore max", - "show-min": "Mostra valore min", - "show-avg": "Mostra valore medio", - "show-total": "Mostra valore totale", - "settings": "Impostazioni legenda", - "min": "min", - "max": "max", - "avg": "avg", - "total": "totale" - }, - "login": { - "login": "Accedi", - "request-password-reset": "Richiesta reset password", - "reset-password": "Reset Password", - "create-password": "Crea Password", - "passwords-mismatch-error": "Le password inserite devono corrispondere!", - "password-again": "Ripeti Password", - "sign-in": "Please sign in", - "username": "Nome utente (email)", - "remember-me": "Ricordami", - "forgot-password": "Password dimenticata?", - "password-reset": "Password reset", - "new-password": "Nuova password", - "new-password-again": "Ripeti nuova password", - "password-link-sent-message": "Link reset password inviato con successo!", - "email": "Email", - "login-with": "Accedi con {{name}}", - "or": "o" - }, - "position": { - "top": "Alto", - "bottom": "Basso", - "left": "Sinistra", - "right": "Destra" - }, - "profile": { - "profile": "Profilo", - "last-login-time": "Ultimo accesso", - "change-password": "Modifica Password", - "current-password": "Password attuale" - }, - "relation": { - "relations": "Relazioni", - "direction": "Direzione", - "search-direction": { - "FROM": "Da", - "TO": "A" - }, - "direction-type": { - "FROM": "da", - "TO": "a" - }, - "from-relations": "Relazioni in uscita", - "to-relations": "Relazioni in ingresso", - "selected-relations": "{ count, plural, 1 {1 relazione selezionata} other {# relazioni selezionate} }", - "type": "Tipo", - "to-entity-type": "A tipo entità", - "to-entity-name": "A nome entità", - "from-entity-type": "Da tipo entità", - "from-entity-name": "Da nome entità", - "to-entity": "A entità", - "from-entity": "Da entità", - "delete": "Elimina relazione", - "relation-type": "Tipo di relazione", - "relation-type-required": "Tipo di relazione obbligatorio.", - "any-relation-type": "Ogni tipo", - "add": "Aggiungi relazione", - "edit": "Modifica relazione", - "delete-to-relation-title": "Sei sicuro di voler eliminare la relazione con l'entità '{{entityName}}'?", - "delete-to-relation-text": "Attenzione, dopo la conferma l'entità '{{entityName}}' sarà scollegata dall'entità corrente.", - "delete-to-relations-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 relazione} other {# relazioni} }?", - "delete-to-relations-text": "Attenzione, dopo la conferma tutte le relazioni selezionate saranno rimosse e le corrispondenti entità scollegate da quella corrente.", - "delete-from-relation-title": "Sei sicuro di voler eliminare la relazione dall'entità '{{entityName}}'?", - "delete-from-relation-text": "Attenzione, dopo la conferma l'entità corrente sarà scollegata dall'entità '{{entityName}}'.", - "delete-from-relations-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 relazione} other {# relazioni} }?", - "delete-from-relations-text": "Attenzione, dopo la conferma tutte le relazioni selezionate saranno rimosse e l'entità corrente scollegata dalle corrispondenti entità.", - "remove-relation-filter": "Rimuovi filtro relazioni", - "add-relation-filter": "Aggiungi filtro relazioni", - "any-relation": "Qualsiasi relazione", - "relation-filters": "Filtri relazioni", - "additional-info": "Informazioni aggiuntive (JSON)", - "invalid-additional-info": "Impossibile analizzare le informazioni aggiuntive in JSON." - }, - "rulechain": { - "rulechain": "Rule chain", - "rulechains": "Rule chain", - "root": "Root", - "delete": "Cancella rule chain", - "name": "Nome", - "name-required": "Nome obbligatorio.", - "description": "Descrizione", - "add": "Aggiungi Rule Chain", - "set-root": "Imposta la rule chain come root", - "set-root-rulechain-title": "Sei sicuro di voler impostare la rule chain '{{ruleChainName}}' come root?", - "set-root-rulechain-text": "Dopo la conferma la rule chain diverrà root a gestirà tutti i messaggi in arrivo.", - "delete-rulechain-title": "Sei sicuro di voler eliminare la rule chain '{{ruleChainName}}'?", - "delete-rulechain-text": "Attenzione, dopo la conferma la rule chain e tutti i dati relativi non saranno più recuperabili.", - "delete-rulechains-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 rule chain} other {# rule chain} }?", - "delete-rulechains-action-title": "Elimina { count, plural, 1 {1 rule chain} other {# rule chain} }", - "delete-rulechains-text": "Attenzione, dopo la conferma tutte le rule chain selezionate saranno rimosse e tutti i relativi dati non saranno più recuperabili.", - "add-rulechain-text": "Aggiungi nuova rule chain", - "no-rulechains-text": "Nessuna rule chain trovata", - "rulechain-details": "Dettagli rule chain", - "details": "Dettagli", - "events": "Eventi", - "system": "Sistema", - "import": "Importa rule chain", - "export": "Esporta rule chain", - "export-failed-error": "Impossibile esportare rule chain: {{error}}", - "create-new-rulechain": "Crea nuova rule chain", - "rulechain-file": "File rule chain", - "invalid-rulechain-file-error": "Impossibile importare rule chain: struttura dati rule chain non valida.", - "copyId": "Copia Id rule chain", - "idCopiedMessage": "Id rule chain copiato negli appunti", - "select-rulechain": "Seleziona rule chain", - "no-rulechains-matching": "Nessuna rule chain corrispondente a '{{entity}}' è stata trovata.", - "rulechain-required": "Rule chain obbligatoria", - "management": "Gestione regole", - "debug-mode": "Modalità debug" - }, - "rulenode": { - "details": "Dettagli", - "events": "Eventi", - "search": "Ricerca nodi", - "open-node-library": "Apri libreria nodi", - "add": "Aggiungi nodo regola", - "name": "Nome", - "name-required": "Nome obbligatorio.", - "type": "Tipo", - "description": "Descrizione", - "delete": "Elimina nodo regola", - "select-all-objects": "Seleziona tutti i nodi e le connessioni", - "deselect-all-objects": "Deseleziona tutti i nodi e le connessioni", - "delete-selected-objects": "Cancella nodi e connessioni selezionate", - "delete-selected": "Elimina selezionati", - "select-all": "Seleziona tutto", - "copy-selected": "Copia selezionata", - "deselect-all": "Deseleziona tutto", - "rulenode-details": "Dettagli nodo regola", - "debug-mode": "Modalità debug", - "configuration": "Configurazione", - "link": "Link", - "link-details": "Dettagli link nodo regola", - "add-link": "Aggiungi link", - "link-label": "Etichetta link", - "link-label-required": "Etichetta link obbligatoria.", - "custom-link-label": "Etichetta link personalizzata", - "custom-link-label-required": "Etichetta link personalizzata obbligatoria.", - "link-labels": "Etichette link", - "link-labels-required": "Etichette link richieste.", - "no-link-labels-found": "Nessuna etichetta link trovata.", - "no-link-label-matching": "'{{label}}' non trovata.", - "create-new-link-label": "Creane una nuova!", - "type-filter": "Filtro", - "type-filter-details": "Filtra i messaggi in arrivo con le condizioni configurate", - "type-enrichment": "Enrichment", - "type-enrichment-details": "Aggiungi informazioni addizionali nei metadati del messaggio", - "type-transformation": "Transformation", - "type-transformation-details": "Change Message payload and Metadata", - "type-action": "Azioni", - "type-action-details": "Perform special action", - "type-external": "External", - "type-external-details": "Interacts with external system", - "type-rule-chain": "Rule Chain", - "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain", - "type-input": "Input", - "type-input-details": "Logical input of Rule Chain, forwards incoming messages to next related Rule Node", - "type-unknown": "Sconosciuto", - "type-unknown-details": "Nodo regola non trovato", - "directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.", - "ui-resources-load-error": "Failed to load configuration ui resources.", - "invalid-target-rulechain": "Unable to resolve target rule chain!", - "test-script-function": "Test script function", - "message": "Messaggio", - "message-type": "Tipo messaggio", - "select-message-type": "Seleziona tipo messaggio", - "message-type-required": "Tipo messaggio obbligatorio", - "metadata": "Metadata", - "metadata-required": "Metadata entries can't be empty.", - "output": "Output", - "test": "Test", - "help": "Aiuto" - }, - "tenant": { - "tenant": "Tenant", - "tenants": "Tenant", - "management": "Gestione Tenant", - "add": "Aggiungi Tenant", - "admins": "Amministratori", - "manage-tenant-admins": "Gestisci amministratori tenant", - "delete": "Cancella tenant", - "add-tenant-text": "Aggiungi nuovo tenant", - "no-tenants-text": "Nessun tenant trovato", - "tenant-details": "Dettagli tenant", - "delete-tenant-title": "Sei sicuro di voler eliminare il tenant '{{tenantTitle}}'?", - "delete-tenant-text": "Attenzione, dopo la conferma il tenant e tutti i suoi dati non saranno più recuperabili.", - "delete-tenants-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 tenant} other {# tenant} }?", - "delete-tenants-action-title": "Elimina { count, plural, 1 {1 tenant} other {# tenant} }", - "delete-tenants-text": "Attenzione, dopo la conferma tutti i tenant selezionati saranno eliminati e tutti i loro dati non saranno più recuperabili.", - "title": "Titolo", - "title-required": "Titolo obbligatorio.", - "description": "Descrizione", - "details": "Dettagli", - "events": "Eventi", - "copyId": "Copia Id tenant", - "idCopiedMessage": "Id tenant copiato negli appunti", - "select-tenant": "Seleziona tenant", - "no-tenants-matching": "Nessun tenant corrispondente a '{{entity}}' è stato trovato.", - "tenant-required": "Tenant obbligatorio" - }, - "timeinterval": { - "seconds-interval": "{ seconds, plural, 1 {1 secondo} other {# secondi} }", - "minutes-interval": "{ minutes, plural, 1 {1 minuto} other {# minuti} }", - "hours-interval": "{ hours, plural, 1 {1 ora} other {# ore} }", - "days-interval": "{ days, plural, 1 {1 giorno} other {# giorni} }", - "days": "Giorni", - "hours": "Ore", - "minutes": "Minuti", - "seconds": "Secondi", - "advanced": "Avanzate" - }, - "timewindow": { - "days": "{ days, plural, 1 { giorno } other {# giorni } }", - "hours": "{ hours, plural, 0 { ora } 1 {1 ora } other {# ore } }", - "minutes": "{ minutes, plural, 0 { minuto } 1 {1 minuto } other {# minuti } }", - "seconds": "{ seconds, plural, 0 { secondo } 1 {1 secondo } other {# secondi } }", - "realtime": "Realtime", - "history": "Cronologia", - "last-prefix": "ultimo", - "period": "da {{ startTime }} a {{ endTime }}", - "edit": "Modifica intervallo temporale", - "date-range": "Intervallo date", - "last": "Ultimo", - "time-period": "Intervallo temporale", - "hide": "Nascondi" - }, - "user": { - "user": "Utente", - "users": "Utenti", - "customer-users": "Utente cliente", - "tenant-admins": "Amministratori Tenant", - "sys-admin": "Amministratore di sistema", - "tenant-admin": "Amministratore tenant", - "customer": "Cliente", - "anonymous": "Anonimo", - "add": "Aggiungi Utente", - "delete": "Elimina utente", - "add-user-text": "Aggiungi nuovo utente", - "no-users-text": "Nessun utente trovato", - "user-details": "Dettagli utente", - "delete-user-title": "Sei sicuro di voler eliminare l'utente '{{userEmail}}'?", - "delete-user-text": "Attenzione, dopo la conferma l'utente e tutti i suoi dati non saranno più recuperabili.", - "delete-users-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 utente} other {# utenti} }?", - "delete-users-action-title": "Elimina { count, plural, 1 {1 utente} other {# utenti} }", - "delete-users-text": "Attenzione, dopo la conferma tutti gli utenti selezionati saranno eliminati e tutti i relativi dati non saranno più recuperabili.", - "activation-email-sent-message": "Email di attivazione inviata con successo!", - "resend-activation": "Invia di nuovo attivazione", - "email": "Email", - "email-required": "Email obbligatoria.", - "invalid-email-format": "Formato email non valido.", - "first-name": "Nome", - "last-name": "Cognome", - "description": "Descrizione", - "default-dashboard": "Dashboard di default", - "always-fullscreen": "Sempre a schermo intero", - "select-user": "Seleziona utente", - "no-users-matching": "Nessun utente corrispondente a '{{entity}}' è stato trovato.", - "user-required": "Utente obbligatorio", - "activation-method": "Metodo di attivazione", - "display-activation-link": "Mostra link di attivazione", - "send-activation-mail": "Invia email di attivazione", - "activation-link": "Link attivazione utente", - "activation-link-text": "Per attivare l'utente utilizza il seguente link di attivazione :", - "copy-activation-link": "Copia link di attivazione", - "activation-link-copied-message": "Link di attivazione utente copiato negli appunti", - "details": "Dettagli", - "login-as-tenant-admin": "Accedi come Amministratore tenant", - "login-as-customer-user": "Accedi come Utente cliente", - "disable-account": "Disabilita account utente", - "enable-account": "Abilita account utente", - "enable-account-message": "L'account utente è stato abilitato correttamente!", - "disable-account-message": "L'account utente è stato disabilitato correttamente!" - }, - "value": { - "type": "Tipo valore", - "string": "String", - "string-value": "Valore string", - "integer": "Integer", - "integer-value": "Valore integer", - "invalid-integer-value": "Valore integer non valido", - "double": "Double", - "double-value": "Valore double", - "boolean": "Boolean", - "boolean-value": "Valore boolean", - "false": "Falso", - "true": "Vero", - "long": "Long" - }, - "widget": { - "widget-library": "Libreria Widget", - "widget-bundle": "Bundle widget", - "select-widgets-bundle": "Seleziona bundle widget", - "management": "Gestione widget", - "editor": "Editor Widget", - "widget-type-not-found": "Problem loading widget configuration.
    Probably associated\n widget type was removed.", - "widget-type-load-error": "Widget non caricato a causa dei seguenti errori:", - "remove": "Elimina widget", - "edit": "Modifica widget", - "remove-widget-title": "sei sicuro di voler eliminare il widget '{{widgetTitle}}'?", - "remove-widget-text": "Dopo la conferma il widget e tutti i suoi dati non saranno più recuperabili.", - "timeseries": "Time series", - "search-data": "Cerca dati", - "no-data-found": "Nessun dato trovato", - "latest-values": "Ultimi valori", - "rpc": "Control widget", - "alarm": "Alarm widget", - "static": "Static widget", - "select-widget-type": "Seleziona tipo widget", - "missing-widget-title-error": "Il tiolo del widget deve essere specificato!", - "widget-saved": "Widget salvato", - "unable-to-save-widget-error": "Impossibile salvare il widget! Sono presenti degli errori!", - "save": "Salva widget", - "saveAs": "Salva widget come", - "save-widget-type-as": "Salva tipo widget come", - "save-widget-type-as-text": "Please enter new widget title and/or select target widgets bundle", - "toggle-fullscreen": "Commuta modalità schermo intero", - "run": "Esegui widget", - "title": "Titolo widget", - "title-required": "Titolo widget obbligatorio.", - "type": "Tipo widget", - "resources": "Risorse", - "resource-url": "JavaScript/CSS URL", - "remove-resource": "Rimuovi risorsa", - "add-resource": "Aggiungi risorsa", - "html": "HTML", - "tidy": "Tidy", - "css": "CSS", - "settings-schema": "Impostazioni schema", - "datakey-settings-schema": "Impostazioni Data key schema", - "javascript": "Javascript", - "remove-widget-type-title": "Sei sicuro di voler rimuovere il tipo di widget '{{widgetName}}'?", - "remove-widget-type-text": "Dopo la conferma il tipo di widget e tutti i suoi dati non saranno più recuperabili.", - "remove-widget-type": "Rimuovi tipo widget", - "add-widget-type": "Aggiungi nuovo tipo widget", - "widget-type-load-failed-error": "Caricamento tipo widget fallito!", - "widget-template-load-failed-error": "Caricamento template widget fallito!", - "add": "Aggiungi Widget", - "undo": "Annulla modifiche widget", - "export": "Esporta widget" - }, - "widget-action": { - "header-button": "Widget header button", - "open-dashboard-state": "Navigate to new dashboard state", - "update-dashboard-state": "Update current dashboard state", - "open-dashboard": "Navigate to other dashboard", - "custom": "Custom action", - "target-dashboard-state": "Target dashboard state", - "target-dashboard-state-required": "Target dashboard state is required", - "set-entity-from-widget": "Set entity from widget", - "target-dashboard": "Target dashboard", - "open-right-layout": "Open right dashboard layout (mobile view)" - }, - "widgets-bundle": { - "current": "Bundle corrente", - "widgets-bundles": "Bundle Widget", - "add": "Aggiungi Bundle Widget", - "delete": "Cancella bundle widget", - "title": "Titolo", - "title-required": "Titolo obbligatorio.", - "add-widgets-bundle-text": "Aggiungi nuovo bundle widget", - "no-widgets-bundles-text": "Nessun bundle widget trovato", - "empty": "Bundle widget vuoto", - "details": "Dettagli", - "widgets-bundle-details": "Dettagli bundle widget", - "delete-widgets-bundle-title": "Sei sicuro di voler eliminare il bundle widget '{{widgetsBundleTitle}}'?", - "delete-widgets-bundle-text": "Attenzione, dopo la conferma il bundle widget e tutti i suoi dati non saranno più recuperabili.", - "delete-widgets-bundles-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 bundle widget} other {# bundle widget} }?", - "delete-widgets-bundles-action-title": "Elimina { count, plural, 1 {1 bundle widget} other {# bundle widget} }", - "delete-widgets-bundles-text": "Attenzione, dopo la conferma tutti i bundle widget selezionati saranno rimossi e tutti i loro dati non saranno più recuperabili.", - "no-widgets-bundles-matching": "Nessun bundle widget corrispondente a '{{widgetsBundle}}' è stato trovato.", - "widgets-bundle-required": "Bundle widget obbligatorio.", - "system": "Sistema", - "import": "Importa bundle widget", - "export": "Esporta bundle widget", - "export-failed-error": "Impossibile esportare bundle widget: {{error}}", - "create-new-widgets-bundle": "Crea nuovo bundle widget", - "widgets-bundle-file": "File bundle widget", - "invalid-widgets-bundle-file-error": "Impossibile importare bundle widget: struttura dati non valida." - }, - "widget-config": { - "data": "Dati", - "settings": "Impostazioni", - "advanced": "Avanzate", - "title": "Titolo", - "title-tooltip": "Tooltip titolo", - "general-settings": "Impostazioni generali", - "display-title": "Mostra titolo", - "drop-shadow": "Drop shadow", - "enable-fullscreen": "Abilita schermo intero", - "background-color": "Colore sfondo", - "text-color": "Colore testo", - "padding": "Padding", - "margin": "Margin", - "widget-style": "Stile Widget", - "title-style": "Stile titolo", - "mobile-mode-settings": "Impostazioni modalità mobile", - "order": "Ordinamento", - "height": "Altezza", - "units": "Simbolo speciale da mostrare accanto al valore", - "decimals": "Numero di cifre decimali", - "timewindow": "Intervallo temporale", - "use-dashboard-timewindow": "Usa intervallo temporale dashboard", - "display-timewindow": "Mostra intervallo temporale", - "display-legend": "Mostra legenda", - "datasources": "Sorgenti dei dati", - "maximum-datasources": "Massimo { count, plural, 1 {1 sorgente dati consentita.} other {# sorgenti dati consentite} }", - "datasource-type": "Tipo", - "datasource-parameters": "Parametri", - "remove-datasource": "Rimuovi sorgente dati", - "add-datasource": "Aggiungi sorgente dati", - "target-device": "Dispositivo Target", - "alarm-source": "Sorgente Allarme", - "actions": "Azioni", - "action": "Azione", - "add-action": "Aggiungi azione", - "search-actions": "Ricerca azioni", - "action-source": "Sorgente azione", - "action-source-required": "Sorgente azione obbligatoria.", - "action-name": "Nome", - "action-name-required": "Nome azione obbligatorio.", - "action-name-not-unique": "Un'altra azione con lo stesso nome è già presente.
    Il nome di una azione dovrebbe essere univoco all'interno della stessa sorgente.", - "action-icon": "Icona", - "action-type": "Tipo", - "action-type-required": "Tipo azione obbligatorio.", - "edit-action": "Modifica azione", - "delete-action": "Cancella azione", - "delete-action-title": "Cancella azione del widget", - "delete-action-text": "Sei sicuro di voler cancellare l'azione del widget '{{actionName}}'?", - "display-icon": "Mostra icona titolo", - "icon-color": "Colore dell'icona", - "icon-size": "Dimensione dell'icona" - }, - "widget-type": { - "import": "Importa un tipo di widget", - "export": "Esporta un tipo di widget", - "export-failed-error": "Impossibile esportare il tipo di widget: {{error}}", - "create-new-widget-type": "Crea un nuovo tipo di widget", - "widget-type-file": "File tipo di widget", - "invalid-widget-type-file-error": "Impossibile importare un tipo di widget: struttura dati del widget non valida." - }, - "widgets": { - "date-range-navigator": { - "localizationMap": { - "Sun": "Dom", - "Mon": "Lun", - "Tue": "Mar", - "Wed": "Mer", - "Thu": "Gio", - "Fri": "Ven", - "Sat": "Sab", - "Jan": "Gen", - "Feb": "Feb", - "Mar": "Mar", - "Apr": "Apr", - "May": "Mag", - "Jun": "Giu", - "Jul": "Lug", - "Aug": "Ago", - "Sep": "Set", - "Oct": "Ott", - "Nov": "Nov", - "Dec": "Dic", - "January": "Gennaio", - "February": "Febbraio", - "March": "Marzo", - "April": "Aprile", - "June": "Giugno", - "July": "Luglio", - "August": "Agosto", - "September": "Settembre", - "October": "Ottobre", - "November": "Novembre", - "December": "Dicembre", - "Custom Date Range": "Intervallo di date personalizzato", - "Date Range Template": "Modello di intervallo di date", - "Today": "Oggi", - "Yesterday": "Ieri", - "This Week": "Questa settimana", - "Last Week": "La settimana scorsa", - "This Month": "Questo mese", - "Last Month": "Lo scorso mese", - "Year": "Anno", - "This Year": "Quest'anno", - "Last Year": "L'anno scorso", - "Date picker": "Date picker", - "Hour": "Ora", - "Day": "Giorno", - "Week": "Settimana", - "2 weeks": "2 Settimane", - "Month": "Mese", - "3 months": "3 Mesi", - "6 months": "6 Mesi", - "Custom interval": "Intervallo personalizzato", - "Interval": "Intervallo", - "Step size": "Dimensione del passo", - "Ok": "Ok" - } - }, - "input-widgets": { - "attribute-not-allowed": "Questo widget non può usare un parametro di tipo attributo", - "date": "Data", - "discard-changes": "Annulla modifiche", - "entity-attribute-required": "E' richiesta un'entità di tipo attributo", - "entity-timeseries-required": "E' richiesta un'entità di tipo serie temporale", - "not-allowed-entity": "L'entità selezionata non può avere attributi condivisi", - "no-attribute-selected": "Nessun attributo selezionato", - "no-datakey-selected": "Nessuna datakey selezionata", - "no-entity-selected": "Nessuna entità selezionata", - "no-image": "Nessuna immagine", - "no-support-web-camera": "Web camera non supportata", - "no-timeseries-selected": "Nessuna serie temporale selezionata", - "switch-attribute-value": "Cambia il valore dell'attributo", - "switch-camera": "Cambia camera", - "switch-timeseries-value": "Cambia il valore della serie temporale", - "take-photo": "Fai una foto", - "time": "Tempo", - "timeseries-not-allowed": "Questo widget non può usare un parametro di tipo serie temporale", - "update-failed": "Aggiornamento fallito", - "update-successful": "Aggiornamento eseguito con successo", - "update-attribute": "Aggiorna attributo", - "update-timeseries": "Aggiorna serie temporale", - "value": "Valore" - } - }, - "icon": { - "icon": "Icona", - "select-icon": "Seleziona icona", - "material-icons": "Icone Material", - "show-all": "Mostra tutte le icone" - }, - "custom": { - "widget-action": { - "action-cell-button": "Pulsante azione cella", - "row-click": "Click sulla riga", - "polygon-click": "Click sul poligono", - "marker-click": "Click sul marker", - "tooltip-tag-action": "Azione tooltip", - "node-selected": "Click su nodo selezionato", - "element-click": "Click su elemento HTML", - "pie-slice-click": "Click sulla fetta", - "row-double-click": "Doppio click sulla riga" - } - }, - "language": { - "language": "Lingua" - } -} diff --git a/ui/src/app/locale/locale.constant-ja_JA.json b/ui/src/app/locale/locale.constant-ja_JA.json deleted file mode 100644 index ec372ce513..0000000000 --- a/ui/src/app/locale/locale.constant-ja_JA.json +++ /dev/null @@ -1,1516 +0,0 @@ -{ - "access": { - "unauthorized": "無許可", - "unauthorized-access": "不正アクセス", - "unauthorized-access-text": "このリソースにアクセスするにはサインインする必要があります。", - "access-forbidden": "アクセス禁止", - "access-forbidden-text": "あなたはこの場所へのアクセス権を持っていません!この場所にアクセスしたい場合は、別のユーザーとサインインしてみてください。", - "refresh-token-expired": "セッションが終了しました", - "refresh-token-failed": "セッションをリフレッシュできません" - }, - "action": { - "activate": "アクティブ化する", - "suspend": "サスペンド", - "save": "セーブ", - "saveAs": "名前を付けて保存", - "cancel": "キャンセル", - "ok": "[OK]", - "delete": "削除", - "add": "追加", - "yes": "はい", - "no": "いいえ", - "update": "更新", - "remove": "削除する", - "search": "サーチ", - "clear-search": "検索をクリアする", - "assign": "割り当てます", - "unassign": "割り当て解除", - "share": "シェア", - "make-private": "プライベートにする", - "apply": "適用", - "apply-changes": "変更を適用する", - "edit-mode": "編集モード", - "enter-edit-mode": "編集モードに入る", - "decline-changes": "変更を拒否する", - "close": "閉じる", - "back": "バック", - "run": "走る", - "sign-in": "サインイン!", - "edit": "編集", - "view": "ビュー", - "create": "作成する", - "drag": "ドラッグ", - "refresh": "リフレッシュ", - "undo": "元に戻す", - "copy": "コピー", - "paste": "ペースト", - "copy-reference": "コピーリファレンス", - "paste-reference": "参照貼り付け", - "import": "インポート", - "export": "輸出する", - "share-via": "{{provider}}" - }, - "aggregation": { - "aggregation": "集約", - "function": "データ集約機能", - "limit": "最大値", - "group-interval": "グループ化の間隔", - "min": "分", - "max": "最大", - "avg": "平均", - "sum": "和", - "count": "カウント", - "none": "なし" - }, - "admin": { - "general": "一般", - "general-settings": "一般設定", - "outgoing-mail": "送信メール", - "outgoing-mail-settings": "送信メールの設定", - "system-settings": "システム設定", - "test-mail-sent": "テストメールが正常に送信されました!", - "base-url": "ベースURL", - "base-url-required": "ベースURLは必須です。", - "mail-from": "メール", - "mail-from-required": "メールの送信元が必要です。", - "smtp-protocol": "SMTPプロトコル", - "smtp-host": "SMTPホスト", - "smtp-host-required": "SMTPホストが必要です。", - "smtp-port": "SMTPポート", - "smtp-port-required": "smtpポートを指定する必要があります。", - "smtp-port-invalid": "それは有効なsmtpポートのようには見えません。", - "timeout-msec": "タイムアウト(ミリ秒)", - "timeout-required": "タイムアウトが必要です。", - "timeout-invalid": "それは有効なタイムアウトのようには見えません。", - "enable-tls": "TLSを有効にする", - "tls-version": "TLSバージョン", - "send-test-mail": "テストメールを送信する" - }, - "alarm": { - "alarm": "警報", - "alarms": "アラーム", - "select-alarm": "アラームを選択", - "no-alarms-matching": "'{{entity}}'発見されました。", - "alarm-required": "アラームが必要です", - "alarm-status": "アラーム状態", - "search-status": { - "ANY": "どれか", - "ACTIVE": "アクティブ", - "CLEARED": "クリアされた", - "ACK": "承認された", - "UNACK": "未確認の" - }, - "display-status": { - "ACTIVE_UNACK": "アクティブ未確認", - "ACTIVE_ACK": "Active Acknowledged", - "CLEARED_UNACK": "クリアされた未確認のメッセージ", - "CLEARED_ACK": "承認された承認済み" - }, - "no-alarms-prompt": "アラームが見つかりません", - "created-time": "作成時刻", - "type": "タイプ", - "severity": "重大度", - "originator": "創始者", - "originator-type": "発信者タイプ", - "details": "詳細", - "status": "状態", - "alarm-details": "アラームの詳細", - "start-time": "始まる時間", - "end-time": "終了時間", - "ack-time": "確認された時間", - "clear-time": "クリアされた時間", - "severity-critical": "クリティカル", - "severity-major": "メジャー", - "severity-minor": "マイナー", - "severity-warning": "警告", - "severity-indeterminate": "不確定", - "acknowledge": "認める", - "clear": "クリア", - "search": "アラームの検索", - "selected-alarms": "{ count, plural, 1 {1 alarm} other {# alarms} }選択された", - "no-data": "表示するデータがありません", - "polling-interval": "アラームポーリング間隔(秒)", - "polling-interval-required": "アラームのポーリング間隔が必要です。", - "min-polling-interval-message": "少なくとも1秒間のポーリング間隔が許可されます。", - "aknowledge-alarms-title": "{ count, plural, 1 {1 alarm} other {# alarms} }", - "aknowledge-alarms-text": "{ count, plural, 1 {1 alarm} other {# alarms} }?", - "clear-alarms-title": "{ count, plural, 1 {1 alarm} other {# alarms} }", - "clear-alarms-text": "{ count, plural, 1 {1 alarm} other {# alarms} }?" - }, - "alias": { - "add": "エイリアスを追加する", - "edit": "エイリアスを編集する", - "name": "エイリアス名", - "name-required": "エイリアス名は必須です", - "duplicate-alias": "同じ名前のエイリアスは既に存在します。", - "filter-type-single-entity": "単一のエンティティ", - "filter-type-entity-list": "エンティティリスト", - "filter-type-entity-name": "エンティティ名", - "filter-type-state-entity": "ダッシュボード状態からのエンティティ", - "filter-type-state-entity-description": "ダッシュボードの状態パラメータから取得されたエンティティ", - "filter-type-asset-type": "資産の種類", - "filter-type-asset-type-description": "'{{assetType}}'", - "filter-type-asset-type-and-name-description": "'{{assetType}}''{{prefix}}'", - "filter-type-device-type": "デバイスタイプ", - "filter-type-device-type-description": "'{{deviceType}}'", - "filter-type-device-type-and-name-description": "'{{deviceType}}''{{prefix}}'", - "filter-type-relations-query": "関係クエリ", - "filter-type-relations-query-description": "{{entities}}{{relationType}}{{direction}}{{rootEntity}}", - "filter-type-asset-search-query": "資産検索クエリ", - "filter-type-asset-search-query-description": "{{assetTypes}}{{relationType}}{{direction}}{{rootEntity}}", - "filter-type-device-search-query": "デバイス検索クエリ", - "filter-type-device-search-query-description": "{{deviceTypes}}{{relationType}}{{direction}}{{rootEntity}}", - "entity-filter": "エンティティフィルタ", - "resolve-multiple": "複数のエンティティとして解決する", - "filter-type": "フィルタタイプ", - "filter-type-required": "フィルタタイプが必要です。", - "entity-filter-no-entity-matched": "指定されたフィルタに一致するエンティティは見つかりませんでした。", - "no-entity-filter-specified": "エンティティフィルタが指定されていない", - "root-state-entity": "ルートとしてダッシュボードの状態エンティティを使用する", - "root-entity": "ルートエンティティ", - "state-entity-parameter-name": "状態エンティティのパラメータ名", - "default-state-entity": "デフォルト状態エンティティ", - "default-entity-parameter-name": "デフォルトでは", - "max-relation-level": "最大関連レベル", - "unlimited-level": "無制限レベル", - "state-entity": "ダッシュボードの状態エンティティ", - "all-entities": "すべてのエンティティ", - "any-relation": "どれか" - }, - "asset": { - "asset": "資産", - "assets": "資産", - "management": "資産運用管理", - "view-assets": "アセットの表示", - "add": "アセットを追加", - "assign-to-customer": "顧客に割り当てる", - "assign-asset-to-customer": "顧客に資産を割り当てる", - "assign-asset-to-customer-text": "顧客に割り当てる資産を選択してください", - "no-assets-text": "アセットが見つかりません", - "assign-to-customer-text": "資産を割り当てる顧客を選択してください", - "public": "パブリック", - "assignedToCustomer": "顧客に割り当てられた", - "make-public": "アセットを公開する", - "make-private": "アセットをプライベートにする", - "unassign-from-customer": "顧客からの割り当て解除", - "delete": "アセットを削除", - "asset-public": "資産は公開されています", - "asset-type": "資産の種類", - "asset-type-required": "資産の種類が必要です。", - "select-asset-type": "アセットタイプを選択", - "enter-asset-type": "アセットタイプを入力", - "any-asset": "すべてのアセット", - "no-asset-types-matching": "'{{entitySubtype}}'発見されました。", - "asset-type-list-empty": "選択されたアセットタイプはありません。", - "asset-types": "資産タイプ", - "name": "名", - "name-required": "名前は必須です。", - "description": "説明", - "type": "タイプ", - "type-required": "タイプが必要です。", - "details": "詳細", - "events": "イベント", - "add-asset-text": "新しいアセットを追加する", - "asset-details": "資産の詳細", - "assign-assets": "アセットの割り当て", - "assign-assets-text": "{ count, plural, 1 {1 asset} other {# assets} }顧客に", - "delete-assets": "アセットを削除する", - "unassign-assets": "アセットの割り当てを解除する", - "unassign-assets-action-title": "{ count, plural, 1 {1 asset} other {# assets} }顧客から", - "assign-new-asset": "新しいアセットを割り当てる", - "delete-asset-title": "'{{assetName}}'?", - "delete-asset-text": "確認後、資産と関連するすべてのデータが回復不能になることに注意してください。", - "delete-assets-title": "{ count, plural, 1 {1 asset} other {# assets} }?", - "delete-assets-action-title": "{ count, plural, 1 {1 asset} other {# assets} }", - "delete-assets-text": "確認後、選択したすべての資産が削除され、関連するすべてのデータは回復不能になりますので注意してください。", - "make-public-asset-title": "'{{assetName}}'パブリック?", - "make-public-asset-text": "確認後、資産とそのすべてのデータは公開され、他の人がアクセスできるようになります。", - "make-private-asset-title": "'{{assetName}}'プライベート?", - "make-private-asset-text": "確認後、資産とそのすべてのデータは非公開にされ、他の人がアクセスすることはできません。", - "unassign-asset-title": "'{{assetName}}'?", - "unassign-asset-text": "確認後、資産は割り当て解除され、顧客はアクセスできなくなります。", - "unassign-asset": "アセットの割り当てを解除する", - "unassign-assets-title": "{ count, plural, 1 {1 asset} other {# assets} }?", - "unassign-assets-text": "確認後、選択されたすべての資産が割り当て解除され、顧客がアクセスできなくなります。", - "copyId": "アセットIDをコピーする", - "idCopiedMessage": "アセットIDがクリップボードにコピーされました", - "select-asset": "アセットを選択", - "no-assets-matching": "'{{entity}}'発見されました。", - "asset-required": "資産が必要です", - "name-starts-with": "アセット名はで始まります", - "label": "ラベル" - }, - "attribute": { - "attributes": "属性", - "latest-telemetry": "最新テレメトリ", - "attributes-scope": "エンティティ属性のスコープ", - "scope-latest-telemetry": "最新テレメトリ", - "scope-client": "クライアントの属性", - "scope-server": "サーバーの属性", - "scope-shared": "共有属性", - "add": "属性を追加する", - "key": "キー", - "last-update-time": "最終更新時間", - "key-required": "属性キーは必須です。", - "value": "値", - "value-required": "属性値は必須です。", - "delete-attributes-title": "{ count, plural, 1 {1 attribute} other {# attributes} }?", - "delete-attributes-text": "注意してください。確認後、選択したすべての属性が削除されます。", - "delete-attributes": "属性を削除する", - "enter-attribute-value": "属性値を入力", - "show-on-widget": "ウィジェットで表示", - "widget-mode": "ウィジェットモード", - "next-widget": "次のウィジェット", - "prev-widget": "前のウィジェット", - "add-to-dashboard": "ダッシュボードに追加", - "add-widget-to-dashboard": "ウィジェットをダッシュ​​ボードに追加する", - "selected-attributes": "{ count, plural, 1 {1 attribute} other {# attributes} }選択された", - "selected-telemetry": "{ count, plural, 1 {1 telemetry unit} other {# telemetry units} }選択された" - }, - "audit-log": { - "audit": "監査", - "audit-logs": "監査ログ", - "timestamp": "タイムスタンプ", - "entity-type": "エンティティタイプ", - "entity-name": "エンティティ名", - "user": "ユーザー", - "type": "タイプ", - "status": "状態", - "details": "詳細", - "type-added": "追加された", - "type-deleted": "削除済み", - "type-updated": "更新しました", - "type-attributes-updated": "属性が更新されました", - "type-attributes-deleted": "属性が削除されました", - "type-rpc-call": "RPC呼び出し", - "type-credentials-updated": "資格が更新されました", - "type-assigned-to-customer": "顧客に割り当てられた", - "type-unassigned-from-customer": "顧客から割り当てられていない", - "type-activated": "活性化", - "type-suspended": "一時停止中", - "type-credentials-read": "信用証明書を読む", - "type-attributes-read": "読み取られた属性", - "type-relation-add-or-update": "関係が更新されました", - "type-relation-delete": "関係が削除されました", - "type-relations-delete": "すべてのリレーションを削除", - "type-alarm-ack": "承認された", - "type-alarm-clear": "クリアされた", - "status-success": "成功", - "status-failure": "失敗", - "audit-log-details": "監査ログの詳細", - "no-audit-logs-prompt": "ログが見つかりません", - "action-data": "行動データ", - "failure-details": "失敗の詳細", - "search": "監査ログの検索", - "clear-search": "検索をクリアする" - }, - "confirm-on-exit": { - "message": "保存されていない変更があります。あなたは本当にこのページを出るのですか?", - "html-message": "保存していない変更があります。
    このページを終了してもよろしいですか?", - "title": "保存されていない変更" - }, - "contact": { - "country": "国", - "city": "シティ", - "state": "州/県", - "postal-code": "郵便番号", - "postal-code-invalid": "無効な郵便番号形式です。", - "address": "住所", - "address2": "アドレス2", - "phone": "電話", - "email": "Eメール", - "no-address": "住所がありません" - }, - "common": { - "username": "ユーザー名", - "password": "パスワード", - "enter-username": "ユーザーネームを入力してください", - "enter-password": "パスワードを入力する", - "enter-search": "検索を入力" - }, - "content-type": { - "json": "Json", - "text": "テキスト", - "binary": "バイナリ(Base64)" - }, - "customer": { - "customer": "顧客", - "customers": "顧客", - "management": "顧客管理", - "dashboard": "カスタマーダッシュボード", - "dashboards": "カスタマーダッシュボード", - "devices": "顧客デバイス", - "assets": "顧客資産", - "public-dashboards": "パブリックダッシュボード", - "public-devices": "パブリックデバイス", - "public-assets": "公的資産", - "add": "顧客を追加", - "delete": "顧客を削除する", - "manage-customer-users": "顧客ユーザーを管理する", - "manage-customer-devices": "顧客のデバイスを管理する", - "manage-customer-dashboards": "顧客ダッシュボードの管理", - "manage-public-devices": "パブリックデバイスを管理する", - "manage-public-dashboards": "公開ダッシュボードの管理", - "manage-customer-assets": "顧客資産の管理", - "manage-public-assets": "公的資産を管理する", - "add-customer-text": "新規顧客を追加", - "no-customers-text": "顧客が見つかりません", - "customer-details": "お客様情報", - "delete-customer-title": "'{{customerTitle}}'?", - "delete-customer-text": "確認後、お客様および関連するすべてのデータが回復不能になるので注意してください。", - "delete-customers-title": "{ count, plural, 1 {1 customer} other {# customers} }?", - "delete-customers-action-title": "{ count, plural, 1 {1 customer} other {# customers} }", - "delete-customers-text": "確認後、選択したすべての顧客は削除され、関連するすべてのデータは回復不能になります。", - "manage-users": "ユーザーを管理する", - "manage-assets": "アセットを管理する", - "manage-devices": "デバイスを管理する", - "manage-dashboards": "ダッシュボードの管理", - "title": "タイトル", - "title-required": "タイトルは必須です。", - "description": "説明", - "details": "詳細", - "events": "イベント", - "copyId": "顧客IDをコピー", - "idCopiedMessage": "顧客IDがクリップボードにコピーされました", - "select-customer": "顧客を選択", - "no-customers-matching": "'{{entity}}'発見されました。", - "customer-required": "顧客は必須です", - "select-default-customer": "デフォルトの顧客を選択", - "default-customer": "デフォルトの顧客", - "default-customer-required": "テナントレベルのダッシュボードをデバッグするには、デフォルトの顧客が必要です" - }, - "datetime": { - "date-from": "デートから", - "time-from": "からの時間", - "date-to": "日付", - "time-to": "の時間" - }, - "dashboard": { - "dashboard": "ダッシュボード", - "dashboards": "ダッシュボード", - "management": "ダッシュボード管理", - "view-dashboards": "ダッシュボードを表示する", - "add": "ダッシュボードを追加", - "assign-dashboard-to-customer": "顧客にダッシュボードを割り当てる", - "assign-dashboard-to-customer-text": "顧客に割り当てるダッシュボードを選択してください", - "assign-to-customer-text": "ダッシュボードを割り当てる顧客を選択してください", - "assign-to-customer": "顧客に割り当てる", - "unassign-from-customer": "顧客からの割り当て解除", - "make-public": "ダッシュボードを公開する", - "make-private": "ダッシュボードを非公開にする", - "manage-assigned-customers": "割り当てられた顧客を管理する", - "assigned-customers": "割り当てられた顧客", - "assign-to-customers": "顧客にダッシュボードを割り当てる", - "assign-to-customers-text": "ダッシュボードを割り当てる顧客を選択してください", - "unassign-from-customers": "顧客からのダッシュボードの割り当て解除", - "unassign-from-customers-text": "ダッシュボードから割り当て解除する顧客を選択してください", - "no-dashboards-text": "ダッシュボードが見つかりません", - "no-widgets": "ウィジェットは設定されていません", - "add-widget": "新しいウィジェットを追加", - "title": "タイトル", - "select-widget-title": "ウィジェットを選択", - "select-widget-subtitle": "利用可能なウィジェットタイプのリスト", - "delete": "ダッシュボードの削除", - "title-required": "タイトルは必須です。", - "description": "説明", - "details": "詳細", - "dashboard-details": "ダッシュボードの詳細", - "add-dashboard-text": "新しいダッシュボードを追加する", - "assign-dashboards": "ダッシュボードの割り当て", - "assign-new-dashboard": "新しいダッシュボードを割り当てる", - "assign-dashboards-text": "{ count, plural, 1 {1 dashboard} other {# dashboards} }顧客に", - "unassign-dashboards-action-text": "{ count, plural, 1 {1 dashboard} other {# dashboards} }顧客から", - "delete-dashboards": "ダッシュボードの削除", - "unassign-dashboards": "ダッシュボードの割り当てを解除する", - "unassign-dashboards-action-title": "{ count, plural, 1 {1 dashboard} other {# dashboards} }顧客から", - "delete-dashboard-title": "'{{dashboardTitle}}'?", - "delete-dashboard-text": "確認後、ダッシュボードとすべての関連データが回復不能になるので注意してください。", - "delete-dashboards-title": "{ count, plural, 1 {1 dashboard} other {# dashboards} }?", - "delete-dashboards-action-title": "{ count, plural, 1 {1 dashboard} other {# dashboards} }", - "delete-dashboards-text": "注意してください。確認後、選択したダッシュボードはすべて削除され、関連するすべてのデータは回復不能になります。", - "unassign-dashboard-title": "'{{dashboardTitle}}'?", - "unassign-dashboard-text": "確認後、ダッシュボードは割り当てられなくなり、顧客はアクセスできなくなります。", - "unassign-dashboard": "ダッシュボードの割り当てを解除する", - "unassign-dashboards-title": "{ count, plural, 1 {1 dashboard} other {# dashboards} }?", - "unassign-dashboards-text": "確認の後、選択したすべてのダッシュボードは割り当てられなくなり、顧客はアクセスできなくなります。", - "public-dashboard-title": "ダッシュボードは公開されました", - "public-dashboard-text": "{{dashboardTitle}} is now public and accessible via next public link:", - "public-dashboard-notice": "注:データにアクセスするために、関連するデバイスを公開することを忘れないでください。", - "make-private-dashboard-title": "'{{dashboardTitle}}'プライベート?", - "make-private-dashboard-text": "確認の後、ダッシュボードはプライベートにされ、他の人がアクセスすることはできません。", - "make-private-dashboard": "ダッシュボードを非公開にする", - "socialshare-text": "'{{dashboardTitle}}'ThingsBoardを搭載", - "socialshare-title": "'{{dashboardTitle}}'ThingsBoardを搭載", - "select-dashboard": "ダッシュボードを選択", - "no-dashboards-matching": "'{{entity}}'発見されました。", - "dashboard-required": "ダッシュボードが必要です。", - "select-existing": "既存のダッシュボードを選択", - "create-new": "新しいダッシュボードを作成する", - "new-dashboard-title": "新しいダッシュボードのタイトル", - "open-dashboard": "ダッシュボードを開く", - "set-background": "背景を設定する", - "background-color": "背景色", - "background-image": "背景画像", - "background-size-mode": "背景サイズモード", - "no-image": "選択した画像がありません", - "drop-image": "画像をドロップするか、クリックしてアップロードするファイルを選択します。", - "settings": "設定", - "columns-count": "列数", - "columns-count-required": "列数が必要です。", - "min-columns-count-message": "わずか10の最小列数が許可されます。", - "max-columns-count-message": "最大1000の列カウントのみが許可されます。", - "widgets-margins": "ウィジェット間のマージン", - "horizontal-margin": "水平マージン", - "horizontal-margin-required": "水平余白値が必要です。", - "min-horizontal-margin-message": "最小水平マージン値としては0だけが許容されます。", - "max-horizontal-margin-message": "最大水平マージン値は50だけです。", - "vertical-margin": "垂直マージン", - "vertical-margin-required": "垂直マージン値が必要です。", - "min-vertical-margin-message": "最小の垂直マージン値として0のみが許可されます。", - "max-vertical-margin-message": "最大垂直マージン値は50のみです。", - "autofill-height": "自動レイアウトの高さ", - "mobile-layout": "モバイルレイアウトの設定", - "mobile-row-height": "モバイル行の高さ、px", - "mobile-row-height-required": "モバイル行の高さ値が必要です。", - "min-mobile-row-height-message": "最小の行の高さの値として、5ピクセルしか許可されません。", - "max-mobile-row-height-message": "移動可能な行の高さの最大値として許可されるのは200ピクセルだけです。", - "display-title": "ダッシュボードのタイトルを表示する", - "toolbar-always-open": "ツールバーを開いたままにする", - "title-color": "タイトルカラー", - "display-dashboards-selection": "ダッシュボードの選択を表示する", - "display-entities-selection": "エンティティの選択を表示する", - "display-dashboard-timewindow": "タイムウィンドウを表示する", - "display-dashboard-export": "エクスポートの表示", - "import": "インポートダッシュボード", - "export": "エクスポートダッシュボード", - "export-failed-error": "{{error}}", - "create-new-dashboard": "新しいダッシュボードを作成する", - "dashboard-file": "ダッシュボードファイル", - "invalid-dashboard-file-error": "ダッシュボードをインポートできません:ダッシュボードのデータ構造が無効です。", - "dashboard-import-missing-aliases-title": "インポートされたダッシュボードで使用されるエイリアスを設定する", - "create-new-widget": "新しいウィジェットを作成する", - "import-widget": "インポートウィジェット", - "widget-file": "ウィジェットファイル", - "invalid-widget-file-error": "ウィジェットをインポートできません:ウィジェットのデータ構造が無効です。", - "widget-import-missing-aliases-title": "インポートされたウィジェットで使用されるエイリアスを設定する", - "open-toolbar": "ダッシュボードツールバーを開く", - "close-toolbar": "ツールバーを閉じる", - "configuration-error": "設定エラー", - "alias-resolution-error-title": "ダッシュボードエイリアス設定エラー", - "invalid-aliases-config": "エイリアスフィルタの一部に一致するデバイスを見つけることができません。
    この問題を解決するには、管理者に連絡してください。", - "select-devices": "デバイスの選択", - "assignedToCustomer": "顧客に割り当てられた", - "assignedToCustomers": "顧客に割り当てられた", - "public": "パブリック", - "public-link": "パブリックリンク", - "copy-public-link": "パブリックリンクをコピーする", - "public-link-copied-message": "ダッシュボードのパブリックリンクがクリップボードにコピーされました", - "manage-states": "ダッシュボードの状態を管理する", - "states": "ダッシュボードの状態", - "search-states": "検索ダッシュボードの状態", - "selected-states": "{ count, plural, 1 {1 dashboard state} other {# dashboard states} }選択された", - "edit-state": "ダッシュボードの状態を編集する", - "delete-state": "ダッシュボードの状態を削除する", - "add-state": "ダッシュボードの状態を追加する", - "state": "ダッシュボードの状態", - "state-name": "名", - "state-name-required": "ダッシュボードの状態名は必須です。", - "state-id": "状態ID", - "state-id-required": "ダッシュボードの状態IDは必須です。", - "state-id-exists": "同じIDを持つダッシュボードの状態は既に存在します。", - "is-root-state": "ルート状態", - "delete-state-title": "ダッシュボードの状態を削除する", - "delete-state-text": "'{{stateName}}'?", - "show-details": "詳細を表示", - "hide-details": "詳細を隠す", - "select-state": "ターゲット状態を選択する", - "state-controller": "状態コントローラ" - }, - "datakey": { - "settings": "設定", - "advanced": "上級", - "label": "ラベル", - "color": "色", - "units": "値の隣に表示する特別なシンボル", - "decimals": "浮動小数点の後の桁数", - "data-generation-func": "データ生成関数", - "use-data-post-processing-func": "データ後処理機能を使用する", - "configuration": "データキー設定", - "timeseries": "タイムズ", - "attributes": "属性", - "alarm": "アラームフィールド", - "timeseries-required": "エンティティの時系列データが必要です。", - "timeseries-or-attributes-required": "エンティティのtimeseries /属性は必須です。", - "maximum-timeseries-or-attributes": "{ count, plural, 1 {1 timeseries/attribute is allowed.} other {# timeseries/attributes are allowed} }", - "alarm-fields-required": "アラームフィールドが必要です。", - "function-types": "関数型", - "function-types-required": "関数型が必要です。", - "maximum-function-types": "{ count, plural, 1 {1 function type is allowed.} other {# function types are allowed} }" - }, - "datasource": { - "type": "データソースタイプ", - "name": "名", - "add-datasource-prompt": "データソースを追加してください" - }, - "details": { - "edit-mode": "編集モード", - "toggle-edit-mode": "編集モードを切り替える" - }, - "device": { - "device": "デバイス", - "device-required": "デバイスが必要です。", - "devices": "デバイス", - "management": "端末管理", - "view-devices": "デバイスの表示", - "device-alias": "デバイスエイリアス", - "aliases": "デバイスエイリアス", - "no-alias-matching": "'{{alias}}'見つかりません。", - "no-aliases-found": "別名は見つかりませんでした。", - "no-key-matching": "'{{key}}'見つかりません。", - "no-keys-found": "キーが見つかりません。", - "create-new-alias": "新しいものを作成してください!", - "create-new-key": "新しいものを作成してください!", - "duplicate-alias-error": "'{{alias}}'
    デバイスエイリアスは、ダッシュボード内で一意である必要があります。", - "configure-alias": "'{{alias}}'エイリアス", - "no-devices-matching": "'{{entity}}'発見されました。", - "alias": "エイリアス", - "alias-required": "デバイスエイリアスが必要です。", - "remove-alias": "デバイスエイリアスを削除する", - "add-alias": "デバイスエイリアスを追加する", - "name-starts-with": "デバイス名はで始まります", - "device-list": "デバイスリスト", - "use-device-name-filter": "フィルタを使用する", - "device-list-empty": "デバイスが選択されていません。", - "device-name-filter-required": "デバイス名フィルタが必要です。", - "device-name-filter-no-device-matched": "'{{device}}'発見されました。", - "add": "デバイスを追加", - "assign-to-customer": "顧客に割り当てる", - "assign-device-to-customer": "顧客にデバイスを割り当てる", - "assign-device-to-customer-text": "顧客に割り当てるデバイスを選択してください", - "make-public": "端末を公開する", - "make-private": "デバイスを非公開にする", - "no-devices-text": "デバイスが見つかりません", - "assign-to-customer-text": "デバイスを割り当てる顧客を選択してください", - "device-details": "デバイスの詳細", - "add-device-text": "新しいデバイスを追加する", - "credentials": "資格情報", - "manage-credentials": "資格情報を管理する", - "delete": "デバイスを削除する", - "assign-devices": "デバイスを割り当てる", - "assign-devices-text": "{ count, plural, 1 {1 device} other {# devices} }顧客に", - "delete-devices": "デバイスを削除する", - "unassign-from-customer": "顧客からの割り当て解除", - "unassign-devices": "デバイスの割り当てを解除する", - "unassign-devices-action-title": "{ count, plural, 1 {1 device} other {# devices} }顧客から", - "assign-new-device": "新しいデバイスを割り当てる", - "make-public-device-title": "'{{deviceName}}'パブリック?", - "make-public-device-text": "確認後、デバイスとそのすべてのデータは公開され、他のユーザーがアクセスできるようになります。", - "make-private-device-title": "'{{deviceName}}'プライベート?", - "make-private-device-text": "確認後、デバイスとそのすべてのデータは非公開になり、他人がアクセスできなくなります。", - "view-credentials": "資格情報を表示する", - "delete-device-title": "'{{deviceName}}'?", - "delete-device-text": "確認後、デバイスと関連するすべてのデータが回復不能になるので注意してください。", - "delete-devices-title": "{ count, plural, 1 {1 device} other {# devices} }?", - "delete-devices-action-title": "{ count, plural, 1 {1 device} other {# devices} }", - "delete-devices-text": "注意してください。確認後、選択したすべてのデバイスが削除され、関連するすべてのデータは回復不能になります。", - "unassign-device-title": "'{{deviceName}}'?", - "unassign-device-text": "確認の後、デバイスは割り当てが解除され、顧客がアクセスできなくなります。", - "unassign-device": "デバイスの割り当てを解除する", - "unassign-devices-title": "{ count, plural, 1 {1 device} other {# devices} }?", - "unassign-devices-text": "確認の後、選択されたすべてのデバイスが割り当て解除され、顧客がアクセスできなくなります。", - "device-credentials": "デバイス資格情報", - "credentials-type": "資格情報タイプ", - "access-token": "アクセストークン", - "access-token-required": "アクセストークンが必要です。", - "access-token-invalid": "アクセストークンの長さは、1〜20文字でなければなりません。", - "rsa-key": "RSA公開鍵", - "rsa-key-required": "RSA公開鍵が必要です。", - "secret": "秘密", - "secret-required": "秘密が必要です。", - "device-type": "デバイスタイプ", - "device-type-required": "デバイスタイプが必要です。", - "select-device-type": "デバイスタイプを選択", - "enter-device-type": "デバイスタイプを入力", - "any-device": "すべてのデバイス", - "no-device-types-matching": "'{{entitySubtype}}'発見されました。", - "device-type-list-empty": "選択されたデバイスタイプはありません。", - "device-types": "デバイスの種類", - "name": "名", - "name-required": "名前は必須です。", - "description": "説明", - "events": "イベント", - "details": "詳細", - "copyId": "デバイスIDをコピーする", - "copyAccessToken": "コピーアクセストークン", - "idCopiedMessage": "デバイスIDがクリップボードにコピーされました", - "accessTokenCopiedMessage": "デバイスアクセストークンがクリップボードにコピーされました", - "assignedToCustomer": "顧客に割り当てられた", - "unable-delete-device-alias-title": "デバイスエイリアスを削除できません", - "unable-delete-device-alias-text": "'{{deviceAlias}}'{{widgetsList}}", - "is-gateway": "ゲートウェイです", - "public": "パブリック", - "device-public": "デバイスは公開されています", - "select-device": "デバイスの選択" - }, - "dialog": { - "close": "ダイアログを閉じる" - }, - "error": { - "unable-to-connect": "サーバーに接続できません!インターネット接続を確認してください。", - "unhandled-error-code": "{{errorCode}}", - "unknown-error": "不明なエラー" - }, - "entity": { - "entity": "エンティティ", - "entities": "エンティティ", - "aliases": "エンティティエイリアス", - "entity-alias": "エンティティエイリアス", - "unable-delete-entity-alias-title": "エンティティエイリアスを削除できません", - "unable-delete-entity-alias-text": "'{{entityAlias}}'{{widgetsList}}", - "duplicate-alias-error": "'{{alias}}'
    エンティティのエイリアスは、ダッシュボード内で一意である必要があります。", - "missing-entity-filter-error": "'{{alias}}'.", - "configure-alias": "'{{alias}}'エイリアス", - "alias": "エイリアス", - "alias-required": "エンティティエイリアスが必要です。", - "remove-alias": "エンティティエイリアスを削除する", - "add-alias": "エンティティエイリアスを追加する", - "entity-list": "エンティティリスト", - "entity-type": "エンティティタイプ", - "entity-types": "エンティティタイプ", - "entity-type-list": "エンティティタイプリスト", - "any-entity": "任意のエンティティ", - "enter-entity-type": "エンティティタイプを入力", - "no-entities-matching": "'{{entity}}'発見されました。", - "no-entity-types-matching": "'{{entityType}}'発見されました。", - "name-starts-with": "名前はで始まる", - "use-entity-name-filter": "フィルタを使用する", - "entity-list-empty": "選択されたエンティティはありません", - "entity-type-list-empty": "エンティティタイプは選択されていません。", - "entity-name-filter-required": "エンティティ名フィルタが必要です。", - "entity-name-filter-no-entity-matched": "'{{entity}}'発見されました。", - "all-subtypes": "すべて", - "select-entities": "エンティティの選択", - "no-aliases-found": "別名は見つかりませんでした。", - "no-alias-matching": "'{{alias}}'見つかりません。", - "create-new-alias": "新しいものを作成してください!", - "key": "キー", - "key-name": "キー名", - "no-keys-found": "キーが見つかりません。", - "no-key-matching": "'{{key}}'見つかりません。", - "create-new-key": "新しいものを作成してください!", - "type": "タイプ", - "type-required": "エンティティタイプが必要です。", - "type-device": "デバイス", - "type-devices": "デバイス", - "list-of-devices": "{ count, plural, 1 {One device} other {List of # devices} }", - "device-name-starts-with": "'{{prefix}}'", - "type-asset": "資産", - "type-assets": "資産", - "list-of-assets": "{ count, plural, 1 {One asset} other {List of # assets} }", - "asset-name-starts-with": "'{{prefix}}'", - "type-rule": "ルール", - "type-rules": "ルール", - "list-of-rules": "{ count, plural, 1 {One rule} other {List of # rules} }", - "rule-name-starts-with": "'{{prefix}}'", - "type-plugin": "プラグイン", - "type-plugins": "プラグイン", - "list-of-plugins": "{ count, plural, 1 {One plugin} other {List of # plugins} }", - "plugin-name-starts-with": "'{{prefix}}'", - "type-tenant": "テナント", - "type-tenants": "テナント", - "list-of-tenants": "{ count, plural, 1 {One tenant} other {List of # tenants} }", - "tenant-name-starts-with": "'{{prefix}}'", - "type-customer": "顧客", - "type-customers": "顧客", - "list-of-customers": "{ count, plural, 1 {One customer} other {List of # customers} }", - "customer-name-starts-with": "'{{prefix}}'", - "type-user": "ユーザー", - "type-users": "ユーザー", - "list-of-users": "{ count, plural, 1 {One user} other {List of # users} }", - "user-name-starts-with": "'{{prefix}}'", - "type-dashboard": "ダッシュボード", - "type-dashboards": "ダッシュボード", - "list-of-dashboards": "{ count, plural, 1 {One dashboard} other {List of # dashboards} }", - "dashboard-name-starts-with": "'{{prefix}}'", - "type-alarm": "警報", - "type-alarms": "アラーム", - "list-of-alarms": "{ count, plural, 1 {One alarms} other {List of # alarms} }", - "alarm-name-starts-with": "'{{prefix}}'", - "type-rulechain": "ルールチェーン", - "type-rulechains": "ルールチェーン", - "list-of-rulechains": "{ count, plural, 1 {One rule chain} other {List of # rule chains} }", - "rulechain-name-starts-with": "'{{prefix}}'", - "type-rulenode": "ルールノード", - "type-rulenodes": "ルールノード", - "list-of-rulenodes": "{ count, plural, 1 {One rule node} other {List of # rule nodes} }", - "rulenode-name-starts-with": "'{{prefix}}'", - "type-current-customer": "現在の顧客", - "search": "検索エンティティ", - "selected-entities": "{ count, plural, 1 {1 entity} other {# entities} }選択された", - "entity-name": "エンティティ名", - "details": "エンティティの詳細", - "no-entities-prompt": "エンティティが見つかりません", - "no-data": "表示するデータがありません" - }, - "event": { - "event-type": "イベントタイプ", - "type-error": "エラー", - "type-lc-event": "ライフサイクルイベント", - "type-stats": "統計", - "type-debug-rule-node": "デバッグ", - "type-debug-rule-chain": "デバッグ", - "no-events-prompt": "イベントは見つかりませんでした", - "error": "エラー", - "alarm": "警報", - "event-time": "イベント時間", - "server": "サーバ", - "body": "体", - "method": "方法", - "type": "タイプ", - "entity": "エンティティ", - "message-id": "メッセージID", - "message-type": "メッセージタイプ", - "data-type": "データ・タイプ", - "relation-type": "関係タイプ", - "metadata": "メタデータ", - "data": "データ", - "event": "イベント", - "status": "状態", - "success": "成功", - "failed": "失敗", - "messages-processed": "処理されたメッセージ", - "errors-occurred": "エラーが発生しました" - }, - "extension": { - "extensions": "拡張機能", - "selected-extensions": "{ count, plural, 1 {1 extension} other {# extensions} }選択された", - "type": "タイプ", - "key": "キー", - "value": "値", - "id": "イド", - "extension-id": "内線番号", - "extension-type": "拡張タイプ", - "transformer-json": "JSON *", - "unique-id-required": "現在の拡張IDは既に存在します。", - "delete": "拡張子を削除", - "add": "内線番号を追加", - "edit": "拡張機能を編集する", - "delete-extension-title": "'{{extensionId}}'?", - "delete-extension-text": "確認後、拡張子と関連するすべてのデータが回復不能になることに注意してください。", - "delete-extensions-title": "{ count, plural, 1 {1 extension} other {# extensions} }?", - "delete-extensions-text": "注意してください。確認後、選択したすべての内線番号が削除されます。", - "converters": "コンバーター", - "converter-id": "コンバーターID", - "configuration": "構成", - "converter-configurations": "コンバータ構成", - "token": "セキュリティトークン", - "add-converter": "コンバータを追加する", - "add-config": "コンバータ設定を追加する", - "device-name-expression": "デバイス名式", - "device-type-expression": "デバイスタイプの式", - "custom": "カスタム", - "to-double": "ダブル", - "transformer": "トランス", - "json-required": "トランスフォーマーjsonが必要です。", - "json-parse": "変圧器jsonを解析できません。", - "attributes": "属性", - "add-attribute": "属性を追加する", - "add-map": "マッピング要素を追加する", - "timeseries": "タイムズ", - "add-timeseries": "時系列を追加する", - "field-required": "フィールドは必須項目です", - "brokers": "ブローカー", - "add-broker": "ブローカーを追加", - "host": "ホスト", - "port": "ポート", - "port-range": "ポートは1〜65535の範囲内にある必要があります。", - "ssl": "SSL", - "credentials": "資格情報", - "username": "ユーザー名", - "password": "パスワード", - "retry-interval": "ミリ秒単位の再試行間隔", - "anonymous": "匿名", - "basic": "ベーシック", - "pem": "PEM", - "ca-cert": "CA証明書ファイル*", - "private-key": "秘密鍵ファイル*", - "cert": "証明書ファイル*", - "no-file": "ファイルが選択されていません。", - "drop-file": "ファイルをドロップするか、クリックしてアップロードするファイルを選択します。", - "mapping": "マッピング", - "topic-filter": "トピックフィルタ", - "converter-type": "コンバータタイプ", - "converter-json": "Json", - "json-name-expression": "デバイス名json式", - "topic-name-expression": "デバイス名トピック表現", - "json-type-expression": "デバイスタイプjson式", - "topic-type-expression": "デバイスタイプトピック表現", - "attribute-key-expression": "属性キー式", - "attr-json-key-expression": "属性キーjson式", - "attr-topic-key-expression": "属性キートピック式", - "request-id-expression": "要求ID式", - "request-id-json-expression": "リクエストID json式", - "request-id-topic-expression": "リクエストIDトピック表現", - "response-topic-expression": "応答トピック表現", - "value-expression": "値式", - "topic": "トピック", - "timeout": "タイムアウト(ミリ秒)", - "converter-json-required": "コンバータjsonが必要です。", - "converter-json-parse": "コンバータjsonを解析できません。", - "filter-expression": "フィルタ式", - "connect-requests": "接続要求", - "add-connect-request": "接続要求を追加", - "disconnect-requests": "切断要求", - "add-disconnect-request": "切断リクエストを追加する", - "attribute-requests": "属性要求", - "add-attribute-request": "属性要求を追加する", - "attribute-updates": "属性の更新", - "add-attribute-update": "属性の更新を追加する", - "server-side-rpc": "サーバー側RPC", - "add-server-side-rpc-request": "サーバー側RPC要求を追加する", - "device-name-filter": "デバイス名フィルタ", - "attribute-filter": "属性フィルタ", - "method-filter": "方法フィルター", - "request-topic-expression": "トピック表現を要求する", - "response-timeout": "応答タイムアウト(ミリ秒)", - "topic-expression": "トピック表現", - "client-scope": "クライアントスコープ", - "add-device": "デバイスを追加", - "opc-server": "サーバー", - "opc-add-server": "サーバーを追加", - "opc-add-server-prompt": "サーバーを追加してください", - "opc-application-name": "アプリケーション名", - "opc-application-uri": "アプリケーションURI", - "opc-scan-period-in-seconds": "スキャン時間(秒)", - "opc-security": "セキュリティ", - "opc-identity": "身元", - "opc-keystore": "キーストア", - "opc-type": "タイプ", - "opc-keystore-type": "タイプ", - "opc-keystore-location": "ロケーション*", - "opc-keystore-password": "パスワード", - "opc-keystore-alias": "エイリアス", - "opc-keystore-key-password": "キーのパスワード", - "opc-device-node-pattern": "デバイスノードパターン", - "opc-device-name-pattern": "デバイス名パターン", - "modbus-server": "サーバー/スレーブ", - "modbus-add-server": "サーバー/スレーブを追加する", - "modbus-add-server-prompt": "サーバー/スレーブを追加してください", - "modbus-transport": "輸送", - "modbus-port-name": "シリアルポート名", - "modbus-encoding": "エンコーディング", - "modbus-parity": "パリティ", - "modbus-baudrate": "ボーレート", - "modbus-databits": "データビット", - "modbus-stopbits": "ストップビット", - "modbus-databits-range": "データビットは7〜8の範囲内にある必要があります。", - "modbus-stopbits-range": "ストップビットは1〜2の範囲内でなければなりません。", - "modbus-unit-id": "ユニットID", - "modbus-unit-id-range": "ユニットIDは1〜247の範囲で指定してください。", - "modbus-device-name": "装置名", - "modbus-poll-period": "投票期間(ミリ秒)", - "modbus-attributes-poll-period": "属性のポーリング期間(ミリ秒)", - "modbus-timeseries-poll-period": "時系列ポーリング期間(ミリ秒)", - "modbus-poll-period-range": "投票期間は正の値でなければなりません。", - "modbus-tag": "タグ", - "modbus-function": "関数", - "modbus-register-address": "登録アドレス", - "modbus-register-address-range": "レジスタのアドレスは0〜65535の範囲内である必要があります。", - "modbus-register-bit-index": "ビットインデックス", - "modbus-register-bit-index-range": "ビットインデックスは0〜15の範囲内である必要があります。", - "modbus-register-count": "レジスタ数", - "modbus-register-count-range": "レジスタ数は正の値でなければなりません。", - "modbus-byte-order": "バイト順", - "sync": { - "status": "状態", - "sync": "同期", - "not-sync": "同期しない", - "last-sync-time": "前回の同期時間", - "not-available": "利用不可" - }, - "export-extensions-configuration": "エクステンション設定のエクスポート", - "import-extensions-configuration": "エクステンション設定のインポート", - "import-extensions": "拡張機能のインポート", - "import-extension": "インポート拡張", - "export-extension": "輸出延長", - "file": "拡張機能ファイル", - "invalid-file-error": "無効な拡張ファイル" - }, - "fullscreen": { - "expand": "フルスクリーンに拡大", - "exit": "全画面表示を終了", - "toggle": "フルスクリーンモードを切り替える", - "fullscreen": "全画面表示" - }, - "function": { - "function": "関数" - }, - "grid": { - "delete-item-title": "このアイテムを削除してもよろしいですか?", - "delete-item-text": "注意してください。確認後、この項目と関連するすべてのデータは回復不能になります。", - "delete-items-title": "{ count, plural, 1 {1 item} other {# items} }?", - "delete-items-action-title": "{ count, plural, 1 {1 item} other {# items} }", - "delete-items-text": "注意してください。確認後、選択したすべてのアイテムが削除され、関連するすべてのデータは回復不能になります。", - "add-item-text": "新しいアイテムを追加", - "no-items-text": "項目は見つかりませんでした", - "item-details": "商品詳細", - "delete-item": "アイテムを削除", - "delete-items": "アイテムを削除する", - "scroll-to-top": "トップにスクロールします" - }, - "help": { - "goto-help-page": "ヘルプページに行く" - }, - "home": { - "home": "ホーム", - "profile": "プロフィール", - "logout": "ログアウト", - "menu": "メニュー", - "avatar": "アバター", - "open-user-menu": "ユーザーメニューを開く" - }, - "import": { - "no-file": "ファイルが選択されていません", - "drop-file": "JSONファイルをドロップするか、アップロードするファイルをクリックして選択します。" - }, - "item": { - "selected": "選択された" - }, - "js-func": { - "no-return-error": "関数は値を返す必要があります!", - "return-type-mismatch": "'{{type}}'タイプ!", - "tidy": "きちんとした" - }, - "key-val": { - "key": "キー", - "value": "値", - "remove-entry": "エントリを削除", - "add-entry": "エントリを追加", - "no-data": "エントリなし" - }, - "layout": { - "layout": "レイアウト", - "manage": "レイアウトの管理", - "settings": "レイアウト設定", - "color": "色", - "main": "メイン", - "right": "右", - "select": "ターゲットレイアウトを選択" - }, - "legend": { - "position": "伝説の位置", - "show-max": "最大値を表示", - "show-min": "最小値を表示する", - "show-avg": "平均値を表示", - "show-total": "合計値を表示", - "settings": "凡例の設定", - "min": "分", - "max": "最大", - "avg": "平均", - "total": "合計" - }, - "login": { - "login": "ログイン", - "request-password-reset": "リクエストパスワードのリセット", - "reset-password": "パスワードを再設定する", - "create-password": "パスワードの作成", - "passwords-mismatch-error": "入力されたパスワードは同じでなければなりません!", - "password-again": "パスワードをもう一度", - "sign-in": "サインインしてください", - "username": "ユーザー名(電子メール)", - "remember-me": "私を覚えてますか", - "forgot-password": "パスワードをお忘れですか?", - "password-reset": "パスワードのリセット", - "new-password": "新しいパスワード", - "new-password-again": "新しいパスワードを再入力", - "password-link-sent-message": "パスワードリセットリンクが正常に送信されました!", - "email": "Eメール", - "login-with": "{{name}}でログイン", - "or": "または" - }, - "position": { - "top": "上", - "bottom": "ボトム", - "left": "左", - "right": "右" - }, - "profile": { - "profile": "プロフィール", - "change-password": "パスワードを変更する", - "current-password": "現在のパスワード" - }, - "relation": { - "relations": "関係", - "direction": "方向", - "search-direction": { - "FROM": "から", - "TO": "に" - }, - "direction-type": { - "FROM": "から", - "TO": "に" - }, - "from-relations": "アウトバウンド関係", - "to-relations": "インバウンド関係", - "selected-relations": "{ count, plural, 1 {1 relation} other {# relations} }選択された", - "type": "タイプ", - "to-entity-type": "エンティティタイプへ", - "to-entity-name": "エンティティ名に", - "from-entity-type": "エンティティタイプから", - "from-entity-name": "エンティティ名から", - "to-entity": "実体へ", - "from-entity": "エンティティから", - "delete": "関係を削除する", - "relation-type": "関係タイプ", - "relation-type-required": "関係タイプが必要です。", - "any-relation-type": "いかなるタイプ", - "add": "関係を追加する", - "edit": "関係を編集する", - "delete-to-relation-title": "'{{entityName}}'?", - "delete-to-relation-text": "'{{entityName}}'現在のエンティティとは無関係です。", - "delete-to-relations-title": "{ count, plural, 1 {1 relation} other {# relations} }?", - "delete-to-relations-text": "注意してください。確認後、選択されたリレーションはすべて削除され、対応するエンティティは現在のエンティティとは無関係になります。", - "delete-from-relation-title": "'{{entityName}}'?", - "delete-from-relation-text": "'{{entityName}}'.", - "delete-from-relations-title": "{ count, plural, 1 {1 relation} other {# relations} }?", - "delete-from-relations-text": "注意してください。確認後、選択されたリレーションはすべて削除され、現在のエンティティは対応するエンティティとは無関係になります。", - "remove-relation-filter": "関係フィルタを削除する", - "add-relation-filter": "関係フィルタを追加する", - "any-relation": "関係", - "relation-filters": "関係フィルタ", - "additional-info": "追加情報(JSON)", - "invalid-additional-info": "追加情報jsonを解析できません。" - }, - "rulechain": { - "rulechain": "ルールチェーン", - "rulechains": "ルールチェーン", - "root": "ルート", - "delete": "ルールチェーンの削除", - "name": "名", - "name-required": "名前は必須です。", - "description": "説明", - "add": "ルールチェーンを追加する", - "set-root": "ルールチェーンのルートを作る", - "set-root-rulechain-title": "'{{ruleChainName}}'ルート?", - "set-root-rulechain-text": "確認後、ルールチェーンはルートになり、すべての受信トランスポートメッセージを処理します。", - "delete-rulechain-title": "'{{ruleChainName}}'?", - "delete-rulechain-text": "確認後、ルールチェーンと関連するすべてのデータが回復不能になるので注意してください。", - "delete-rulechains-title": "{ count, plural, 1 {1 rule chain} other {# rule chains} }?", - "delete-rulechains-action-title": "{ count, plural, 1 {1 rule chain} other {# rule chains} }", - "delete-rulechains-text": "確認後、選択したすべてのルールチェーンが削除され、関連するすべてのデータが回復不能になるので注意してください。", - "add-rulechain-text": "新しいルールチェーンを追加する", - "no-rulechains-text": "ルールチェーンが見つかりません", - "rulechain-details": "ルールチェーンの詳細", - "details": "詳細", - "events": "イベント", - "system": "システム", - "import": "ルールチェーンのインポート", - "export": "ルールチェーンのエクスポート", - "export-failed-error": "{{error}}", - "create-new-rulechain": "新しいルールチェーンを作成する", - "rulechain-file": "ルールチェーンファイル", - "invalid-rulechain-file-error": "ルールチェーンをインポートできません:ルールチェーンのデータ構造が無効です。", - "copyId": "ルールチェーンIDのコピー", - "idCopiedMessage": "ルールチェーンIDがクリップボードにコピーされました", - "select-rulechain": "ルールチェーンの選択", - "no-rulechains-matching": "'{{entity}}'発見されました。", - "rulechain-required": "ルールチェーンが必要です", - "management": "ルール管理", - "debug-mode": "デバッグモード" - }, - "rulenode": { - "details": "詳細", - "events": "イベント", - "search": "検索ノード", - "open-node-library": "オープンノードライブラリ", - "add": "ルールノードを追加する", - "name": "名", - "name-required": "名前は必須です。", - "type": "タイプ", - "description": "説明", - "delete": "ルールノードを削除", - "select-all-objects": "すべてのノードと接続を選択する", - "deselect-all-objects": "すべてのノードと接続の選択を解除する", - "delete-selected-objects": "選択したノードと接続を削除する", - "delete-selected": "選択を削除します", - "select-all": "すべて選択", - "copy-selected": "選択したコピー", - "deselect-all": "すべての選択を解除", - "rulenode-details": "ルールノードの詳細", - "debug-mode": "デバッグモード", - "configuration": "構成", - "link": "リンク", - "link-details": "ルールノードのリンクの詳細", - "add-link": "リンクを追加", - "link-label": "リンクラベル", - "link-label-required": "リンクラベルが必要です。", - "custom-link-label": "カスタムリンクラベル", - "custom-link-label-required": "カスタムリンクラベルが必要です。", - "link-labels": "リンクラベル", - "link-labels-required": "リンクラベルが必要です。", - "no-link-labels-found": "リンクラベルが見つかりません", - "no-link-label-matching": "'{{label}}'見つかりません。", - "create-new-link-label": "新しいものを作成してください!", - "type-filter": "フィルタ", - "type-filter-details": "設定された条件で着信メッセージをフィルタリングする", - "type-enrichment": "豊かな", - "type-enrichment-details": "メッセージメタデータに追加情報を追加する", - "type-transformation": "変換", - "type-transformation-details": "メッセージペイロードとメタデータの変更", - "type-action": "アクション", - "type-action-details": "特別なアクションを実行する", - "type-external": "外部", - "type-external-details": "外部システムとの相互作用", - "type-rule-chain": "ルールチェーン", - "type-rule-chain-details": "受信したメッセージを指定したルールチェーンに転送する", - "type-input": "入力", - "type-input-details": "ルールチェーンの論理入力、次の関連ルールノードへの着信メッセージの転送", - "type-unknown": "未知の", - "type-unknown-details": "未解決のルールノード", - "directive-is-not-loaded": "'{{directiveName}}'利用できません。", - "ui-resources-load-error": "構成UIリソースをロードできませんでした。", - "invalid-target-rulechain": "ターゲットルールチェーンを解決できません!", - "test-script-function": "テストスクリプト機能", - "message": "メッセージ", - "message-type": "メッセージタイプ", - "select-message-type": "メッセージタイプを選択", - "message-type-required": "メッセージタイプは必須です", - "metadata": "メタデータ", - "metadata-required": "メタデータのエントリを空にすることはできません。", - "output": "出力", - "test": "テスト", - "help": "助けて" - }, - "tenant": { - "tenant": "テナント", - "tenants": "テナント", - "management": "テナント管理", - "add": "テナントを追加", - "admins": "管理者", - "manage-tenant-admins": "テナント管理者の管理", - "delete": "テナントの削除", - "add-tenant-text": "新しいテナントを追加する", - "no-tenants-text": "テナントは見つかりませんでした", - "tenant-details": "テナントの詳細", - "delete-tenant-title": "'{{tenantTitle}}'?", - "delete-tenant-text": "確認後、テナントと関連するすべてのデータが回復不能になるので注意してください。", - "delete-tenants-title": "{ count, plural, 1 {1 tenant} other {# tenants} }?", - "delete-tenants-action-title": "{ count, plural, 1 {1 tenant} other {# tenants} }", - "delete-tenants-text": "注意してください。確認後、選択されたすべてのテナントが削除され、関連するすべてのデータは回復不能になります。", - "title": "タイトル", - "title-required": "タイトルは必須です。", - "description": "説明", - "details": "詳細", - "events": "イベント", - "copyId": "テナントIDをコピーする", - "idCopiedMessage": "テナントIDがクリップボードにコピーされました", - "select-tenant": "テナントを選択", - "no-tenants-matching": "'{{entity}}'発見されました。", - "tenant-required": "テナントが必要です" - }, - "timeinterval": { - "seconds-interval": "{ seconds, plural, 1 {1 second} other {# seconds} }", - "minutes-interval": "{ minutes, plural, 1 {1 minute} other {# minutes} }", - "hours-interval": "{ hours, plural, 1 {1 hour} other {# hours} }", - "days-interval": "{ days, plural, 1 {1 day} other {# days} }", - "days": "日々", - "hours": "時間", - "minutes": "分", - "seconds": "秒", - "advanced": "上級" - }, - "timewindow": { - "days": "{ days, plural, 1 { day } other {# days } }", - "hours": "{ hours, plural, 0 { hour } 1 {1 hour } other {# hours } }", - "minutes": "{ minutes, plural, 0 { minute } 1 {1 minute } other {# minutes } }", - "seconds": "{ seconds, plural, 0 { second } 1 {1 second } other {# seconds } }", - "realtime": "リアルタイム", - "history": "歴史", - "last-prefix": "最終", - "period": "{{ startTime }}{{ endTime }}", - "edit": "タイムウィンドウを編集", - "date-range": "期間", - "last": "最終", - "time-period": "期間" - }, - "user": { - "user": "ユーザー", - "users": "ユーザー", - "customer-users": "顧客ユーザー", - "tenant-admins": "テナント管理者", - "sys-admin": "システム管理者", - "tenant-admin": "テナント管理者", - "customer": "顧客", - "anonymous": "匿名", - "add": "ユーザーを追加する", - "delete": "ユーザーを削除", - "add-user-text": "新しいユーザーを追加", - "no-users-text": "ユーザが見つかりませんでした", - "user-details": "ユーザーの詳細", - "delete-user-title": "'{{userEmail}}'?", - "delete-user-text": "確認後、ユーザーと関連するすべてのデータが回復不能になるので注意してください。", - "delete-users-title": "{ count, plural, 1 {1 user} other {# users} }?", - "delete-users-action-title": "{ count, plural, 1 {1 user} other {# users} }", - "delete-users-text": "注意してください。確認後、選択したすべてのユーザーが削除され、関連するすべてのデータは回復不能になります。", - "activation-email-sent-message": "アクティベーション電子メールが正常に送信されました!", - "resend-activation": "アクティブ化を再送", - "email": "Eメール", - "email-required": "電子メールが必要です。", - "invalid-email-format": "メールフォーマットが無効です。", - "first-name": "ファーストネーム", - "last-name": "苗字", - "description": "説明", - "default-dashboard": "デフォルトのダッシュボード", - "always-fullscreen": "常に全画面表示", - "select-user": "ユーザーを選択", - "no-users-matching": "'{{entity}}'発見されました。", - "user-required": "ユーザーは必須です", - "activation-method": "起動方法", - "display-activation-link": "アクティブ化リンクを表示する", - "send-activation-mail": "アクティベーションメールを送信する", - "activation-link": "ユーザーアクティベーションリンク", - "activation-link-text": "activation link :", - "copy-activation-link": "アクティブ化リンクをコピーする", - "activation-link-copied-message": "ユーザーのアクティベーションリンクがクリップボードにコピーされました", - "details": "詳細" - }, - "value": { - "type": "値のタイプ", - "string": "文字列", - "string-value": "文字列値", - "integer": "整数", - "integer-value": "整数値", - "invalid-integer-value": "整数値が無効です", - "double": "ダブル", - "double-value": "二重価値", - "boolean": "ブール", - "boolean-value": "ブール値", - "false": "偽", - "true": "真", - "long": "長いです" - }, - "widget": { - "widget-library": "ウィジェットライブラリ", - "widget-bundle": "ウィジェットバンドル", - "select-widgets-bundle": "ウィジェットのバンドルを選択", - "management": "ウィジェット管理", - "editor": "ウィジェットエディタ", - "widget-type-not-found": "ウィジェットの設定を読み込む際に問題が発生しました。
    おそらく関連付けられているウィジェットのタイプが削除されています。", - "widget-type-load-error": "次のエラーのためにウィジェットが読み込まれませんでした:", - "remove": "ウィジェットを削除", - "edit": "ウィジェットの編集", - "remove-widget-title": "'{{widgetTitle}}'?", - "remove-widget-text": "確認後、ウィジェットと関連するすべてのデータは回復不能になります。", - "timeseries": "時系列", - "search-data": "検索データ", - "no-data-found": "何もデータが見つかりませんでした", - "latest-values": "最新の値", - "rpc": "コントロールウィジェット", - "alarm": "アラームウィジェット", - "static": "静的ウィジェット", - "select-widget-type": "ウィジェットタイプを選択", - "missing-widget-title-error": "ウィジェットのタイトルを指定する必要があります!", - "widget-saved": "ウィジェットが保存されました", - "unable-to-save-widget-error": "ウィジェットを保存できません!ウィジェットにエラーがあります!", - "save": "ウィジェットを保存", - "saveAs": "ウィジェットを次のように保存する", - "save-widget-type-as": "ウィジェットタイプを次のように保存します", - "save-widget-type-as-text": "新しいウィジェットのタイトルを入力したり、ターゲットウィジェットのバンドルを選択してください", - "toggle-fullscreen": "フルスクリーン切り替え", - "run": "ウィジェットを実行する", - "title": "ウィジェットのタイトル", - "title-required": "ウィジェットのタイトルが必要です。", - "type": "ウィジェットタイプ", - "resources": "リソース", - "resource-url": "JavaScript / CSS URL", - "remove-resource": "リソースを削除する", - "add-resource": "リソースを追加", - "html": "HTML", - "tidy": "きちんとした", - "css": "CSS", - "settings-schema": "設定スキーマ", - "datakey-settings-schema": "データキー設定のスキーマ", - "javascript": "Javascript", - "remove-widget-type-title": "'{{widgetName}}'?", - "remove-widget-type-text": "確認後、ウィジェットのタイプと関連するすべてのデータは回復不能になります。", - "remove-widget-type": "ウィジェットタイプを削除", - "add-widget-type": "新しいウィジェットタイプを追加する", - "widget-type-load-failed-error": "ウィジェットタイプの読み込みに失敗しました!", - "widget-template-load-failed-error": "ウィジェットテンプレートを読み込めませんでした!", - "add": "ウィジェットを追加", - "undo": "ウィジェットの変更を元に戻す", - "export": "ウィジェットの書き出し" - }, - "widget-action": { - "header-button": "ウィジェットのヘッダーボタン", - "open-dashboard-state": "新しいダッシュボードの状態に移動する", - "update-dashboard-state": "現在のダッシュボードの状態を更新する", - "open-dashboard": "他のダッシュボードに移動する", - "custom": "カスタムアクション", - "target-dashboard-state": "ターゲットダッシュボードの状態", - "target-dashboard-state-required": "ターゲットダッシュボードの状態が必要です", - "set-entity-from-widget": "エンティティをウィジェットから設定する", - "target-dashboard": "ターゲットダッシュボード", - "open-right-layout": "右ダッシュボードレイアウトを開く(モバイルビュー)" - }, - "widgets-bundle": { - "current": "現在のバンドル", - "widgets-bundles": "ウィジェットバンドル", - "add": "ウィジェットのバンドルを追加", - "delete": "ウィジェットのバンドルを削除する", - "title": "タイトル", - "title-required": "タイトルは必須です。", - "add-widgets-bundle-text": "新しいウィジェットのバンドルを追加する", - "no-widgets-bundles-text": "ウィジェットバンドルが見つかりません", - "empty": "ウィジェットのバンドルが空です", - "details": "詳細", - "widgets-bundle-details": "ウィジェットのバンドルの詳細", - "delete-widgets-bundle-title": "'{{widgetsBundleTitle}}'?", - "delete-widgets-bundle-text": "確認後、ウィジェットはバンドルされ、関連するすべてのデータは回復不能になります。", - "delete-widgets-bundles-title": "{ count, plural, 1 {1 widgets bundle} other {# widgets bundles} }?", - "delete-widgets-bundles-action-title": "{ count, plural, 1 {1 widgets bundle} other {# widgets bundles} }", - "delete-widgets-bundles-text": "確認後、選択したすべてのウィジェットバンドルは削除され、関連するすべてのデータは回復不能になります。", - "no-widgets-bundles-matching": "'{{widgetsBundle}}'発見されました。", - "widgets-bundle-required": "ウィジェットバンドルが必要です。", - "system": "システム", - "import": "インポートウィジェットバンドル", - "export": "ウィジェットのエクスポートバンドル", - "export-failed-error": "{{error}}", - "create-new-widgets-bundle": "新しいウィジェットバンドルを作成する", - "widgets-bundle-file": "ウィジェットのバンドルファイル", - "invalid-widgets-bundle-file-error": "ウィジェットをインポートできません。bundle:データ構造が無効です。" - }, - "widget-config": { - "data": "データ", - "settings": "設定", - "advanced": "上級", - "title": "タイトル", - "general-settings": "一般設定", - "display-title": "タイトルを表示", - "drop-shadow": "影を落とす", - "enable-fullscreen": "フルスクリーンを有効にする", - "background-color": "背景色", - "text-color": "テキストの色", - "padding": "パディング", - "margin": "マージン", - "widget-style": "ウィジェットスタイル", - "title-style": "タイトルスタイル", - "mobile-mode-settings": "モバイルモードの設定", - "order": "注文", - "height": "高さ", - "units": "値の隣に表示する特別なシンボル", - "decimals": "浮動小数点の後の桁数", - "timewindow": "タイムウィンドウ", - "use-dashboard-timewindow": "ダッシュボードのタイムウィンドウを使用する", - "display-legend": "伝説を表示", - "datasources": "データソース", - "maximum-datasources": "{ count, plural, 1 {1 datasource is allowed.} other {# datasources are allowed} }", - "datasource-type": "タイプ", - "datasource-parameters": "パラメーター", - "remove-datasource": "データソースを削除", - "add-datasource": "データソースを追加", - "target-device": "ターゲットデバイス", - "alarm-source": "アラームソース", - "actions": "行動", - "action": "アクション", - "add-action": "アクションを追加", - "search-actions": "検索アクション", - "action-source": "アクションソース", - "action-source-required": "アクションソースが必要です。", - "action-name": "名", - "action-name-required": "アクション名は必須です。", - "action-name-not-unique": "同じ名前の別のアクションがすでに存在します。
    アクション名は、同じアクションソース内で一意である必要があります。", - "action-icon": "アイコン", - "action-type": "タイプ", - "action-type-required": "アクションタイプが必要です。", - "edit-action": "アクションの編集", - "delete-action": "アクションの削除", - "delete-action-title": "ウィジェットアクションを削除する", - "delete-action-text": "'{{actionName}}'?" - }, - "widget-type": { - "import": "インポートウィジェットタイプ", - "export": "ウィジェットのタイプをエクスポートする", - "export-failed-error": "{{error}}", - "create-new-widget-type": "新しいウィジェットタイプを作成する", - "widget-type-file": "ウィジェットタイプファイル", - "invalid-widget-type-file-error": "ウィジェットタイプをインポートできません:ウィジェットタイプのデータ構造が無効です。" - }, - "widgets": { - "date-range-navigator": { - "localizationMap": { - "Sun": "日", - "Mon": "月", - "Tue": "火", - "Wed": "水", - "Thu": "木", - "Fri": "金", - "Sat": "土", - "Jan": "1月", - "Feb": "2月", - "Mar": "3月", - "Apr": "4月", - "May": "5月", - "Jun": "6月", - "Jul": "7月", - "Aug": "8月", - "Sep": "9月", - "Oct": "10月", - "Nov": "11月", - "Dec": "12月", - "January": "1月", - "February": "2月", - "March": "行進", - "April": "4月", - "June": "六月", - "July": "7月", - "August": "8月", - "September": "9月", - "October": "10月", - "November": "11月", - "December": "12月", - "Custom Date Range": "カスタム期間", - "Date Range Template": "日付範囲テンプレート", - "Today": "今日", - "Yesterday": "昨日", - "This Week": "今週", - "Last Week": "先週", - "This Month": "今月", - "Last Month": "先月", - "Year": "年", - "This Year": "今年", - "Last Year": "昨年", - "Date picker": "日付ピッカー", - "Hour": "時", - "Day": "日", - "Week": "週間", - "2 weeks": "2週間", - "Month": "月", - "3 months": "3ヶ月", - "6 months": "6ヵ月", - "Custom interval": "カスタム間隔", - "Interval": "間隔", - "Step size": "刻み幅", - "Ok": "Ok" - } - } - }, - "icon": { - "icon": "アイコン", - "select-icon": "選択アイコン", - "material-icons": "マテリアルアイコン", - "show-all": "すべてのアイコンを表示する" - }, - "custom": { - "widget-action": { - "action-cell-button": "アクションセルボタン", - "row-click": "行のクリック", - "polygon-click": "ポリゴンクリック", - "marker-click": "マーカークリック", - "tooltip-tag-action": "ツールチップのタグアクション" - } - }, - "language": { - "language": "言語" - } -} diff --git a/ui/src/app/locale/locale.constant-ko_KR.json b/ui/src/app/locale/locale.constant-ko_KR.json deleted file mode 100644 index e5f09184b9..0000000000 --- a/ui/src/app/locale/locale.constant-ko_KR.json +++ /dev/null @@ -1,1391 +0,0 @@ -{ - "access": { - "unauthorized": "권한 없음.", - "unauthorized-access": "허가되지 않은 접근", - "unauthorized-access-text": "이 리소스에 접근하려면 로그인해야 합니다!", - "access-forbidden": "접근 금지", - "access-forbidden-text": "접근 권한이 없습니다.!
    만일 이 페이지에 계속 접근하려면 다른 사용자로 로그인 하세요.", - "refresh-token-expired": "세션이 만료되었습니다.", - "refresh-token-failed": "세션을 새로 고칠 수 없습니다." - }, - "action": { - "activate": "활설화", - "suspend": "비활성화", - "save": "저장", - "saveAs": "다른 이름으로 저장", - "cancel": "취소", - "ok": "확인", - "delete": "삭제", - "add": "추가", - "yes": "네", - "no": "아니오", - "update": "업데이트", - "remove": "제거", - "search": "검색", - "clear-search": "Clear search", - "assign": "할당", - "unassign": "비할당", - "share": "Share", - "make-private": "Make private", - "apply": "적용", - "apply-changes": "변경사항 적용", - "edit-mode": "수정 모드", - "enter-edit-mode": "수정 모드 진입", - "decline-changes": "변경사항 포기", - "close": "닫기", - "back": "뒤로", - "run": "실행", - "sign-in": "로그인!", - "edit": "수정", - "view": "보기", - "create": "만들기", - "drag": "끌기", - "refresh": "새로고침", - "undo": "취소", - "copy": "복사", - "paste": "붙여넣기", - "copy-reference": "Copy reference", - "paste-reference": "Paste reference", - "import": "가져오기", - "export": "내보내기", - "share-via": "Share via {{provider}}" - }, - "aggregation": { - "aggregation": "집합", - "function": "데이터 집합 함수", - "limit": "최대 값", - "group-interval": "그룹 간격", - "min": "최소", - "max": "최대", - "avg": "평균", - "sum": "합계", - "count": "숫자", - "none": "없음" - }, - "admin": { - "general": "일반", - "general-settings": "일반 설정", - "outgoing-mail": "메일 전송", - "outgoing-mail-settings": "메일 전송 설정", - "system-settings": "시스템 설정", - "test-mail-sent": "테스트 메일이 성공적으로 전송되었습니다!", - "base-url": "기본 URL", - "base-url-required": "기본 URL을 입력해야 합니다.", - "mail-from": "보내는 사람", - "mail-from-required": "보내는 사람을 입력해야 합니다.", - "smtp-protocol": "SMTP 프로토콜", - "smtp-host": "SMTP 호스트", - "smtp-host-required": "SMTP 호스트를 입력해야 합니다.", - "smtp-port": "SMTP 포트", - "smtp-port-required": "SMTP 포트를 입력해야 합니다.", - "smtp-port-invalid": "올바른 SMTP 포트가 아닙니다.", - "timeout-msec": "제한시간 (msec)", - "timeout-required": "제한시간을 입력해야 합니다.", - "timeout-invalid": "올바른 제한시간이 아닙니다.", - "enable-tls": "TLS 사용", - "tls-version" : "TLS 버전", - "send-test-mail": "테스트 메일 보내기" - }, - "alarm": { - "alarm": "Alarm", - "alarms": "Alarms", - "select-alarm": "Select alarm", - "no-alarms-matching": "No alarms matching '{{entity}}' were found.", - "alarm-required": "Alarm is required", - "alarm-status": "Alarm status", - "search-status": { - "ANY": "Any", - "ACTIVE": "Active", - "CLEARED": "Cleared", - "ACK": "Acknowledged", - "UNACK": "Unacknowledged" - }, - "display-status": { - "ACTIVE_UNACK": "Active Unacknowledged", - "ACTIVE_ACK": "Active Acknowledged", - "CLEARED_UNACK": "Cleared Unacknowledged", - "CLEARED_ACK": "Cleared Acknowledged" - }, - "no-alarms-prompt": "No alarms found", - "created-time": "Created time", - "type": "Type", - "severity": "Severity", - "originator": "Originator", - "originator-type": "Originator type", - "details": "Details", - "status": "Status", - "alarm-details": "Alarm details", - "start-time": "Start time", - "end-time": "End time", - "ack-time": "Acknowledged time", - "clear-time": "Cleared time", - "severity-critical": "Critical", - "severity-major": "Major", - "severity-minor": "Minor", - "severity-warning": "Warning", - "severity-indeterminate": "Indeterminate", - "acknowledge": "Acknowledge", - "clear": "Clear", - "search": "Search alarms", - "selected-alarms": "{ count, plural, 1 {1 alarm} other {# alarms} } selected", - "no-data": "No data to display", - "polling-interval": "Alarms polling interval (sec)", - "polling-interval-required": "Alarms polling interval is required.", - "min-polling-interval-message": "At least 1 sec polling interval is allowed.", - "aknowledge-alarms-title": "Acknowledge { count, plural, 1 {1 alarm} other {# alarms} }", - "aknowledge-alarms-text": "Are you sure you want to acknowledge { count, plural, 1 {1 alarm} other {# alarms} }?", - "clear-alarms-title": "Clear { count, plural, 1 {1 alarm} other {# alarms} }", - "clear-alarms-text": "Are you sure you want to clear { count, plural, 1 {1 alarm} other {# alarms} }?" - }, - "alias": { - "add": "Add alias", - "edit": "Edit alias", - "name": "Alias name", - "name-required": "Alias name is required", - "duplicate-alias": "Alias with same name is already exists.", - "filter-type-single-entity": "Single entity", - "filter-type-entity-list": "Entity list", - "filter-type-entity-name": "Entity name", - "filter-type-state-entity": "Entity from dashboard state", - "filter-type-state-entity-description": "Entity taken from dashboard state parameters", - "filter-type-asset-type": "Asset type", - "filter-type-asset-type-description": "Assets of type '{{assetType}}'", - "filter-type-asset-type-and-name-description": "Assets of type '{{assetType}}' and with name starting with '{{prefix}}'", - "filter-type-device-type": "Device type", - "filter-type-device-type-description": "Devices of type '{{deviceType}}'", - "filter-type-device-type-and-name-description": "Devices of type '{{deviceType}}' and with name starting with '{{prefix}}'", - "filter-type-relations-query": "Relations query", - "filter-type-relations-query-description": "{{entities}} that have {{relationType}} relation {{direction}} {{rootEntity}}", - "filter-type-asset-search-query": "Asset search query", - "filter-type-asset-search-query-description": "Assets with types {{assetTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}", - "filter-type-device-search-query": "Device search query", - "filter-type-device-search-query-description": "Devices with types {{deviceTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}", - "entity-filter": "Entity filter", - "resolve-multiple": "Resolve as multiple entities", - "filter-type": "Filter type", - "filter-type-required": "Filter type is required.", - "entity-filter-no-entity-matched": "No entities matching specified filter were found.", - "no-entity-filter-specified": "No entity filter specified", - "root-state-entity": "Use dashboard state entity as root", - "root-entity": "Root entity", - "state-entity-parameter-name": "State entity parameter name", - "default-state-entity": "Default state entity", - "default-entity-parameter-name": "By default", - "max-relation-level": "Max relation level", - "unlimited-level": "Unlimited level", - "state-entity": "Dashboard state entity", - "all-entities": "All entities", - "any-relation": "any" - }, - "asset": { - "asset": "Asset", - "assets": "Assets", - "management": "Asset management", - "view-assets": "View Assets", - "add": "Add Asset", - "assign-to-customer": "Assign to customer", - "assign-asset-to-customer": "Assign Asset(s) To Customer", - "assign-asset-to-customer-text": "Please select the assets to assign to the customer", - "no-assets-text": "No assets found", - "assign-to-customer-text": "Please select the customer to assign the asset(s)", - "public": "Public", - "assignedToCustomer": "Assigned to customer", - "make-public": "Make asset public", - "make-private": "Make asset private", - "unassign-from-customer": "Unassign from customer", - "delete": "Delete asset", - "asset-public": "Asset is public", - "asset-type": "Asset type", - "asset-type-required": "Asset type is required.", - "select-asset-type": "Select asset type", - "enter-asset-type": "Enter asset type", - "any-asset": "Any asset", - "no-asset-types-matching": "No asset types matching '{{entitySubtype}}' were found.", - "asset-type-list-empty": "No asset types selected.", - "asset-types": "Asset types", - "name": "Name", - "name-required": "Name is required.", - "description": "Description", - "type": "Type", - "type-required": "Type is required.", - "details": "Details", - "events": "Events", - "add-asset-text": "Add new asset", - "asset-details": "Asset details", - "assign-assets": "Assign assets", - "assign-assets-text": "Assign { count, plural, 1 {1 asset} other {# assets} } to customer", - "delete-assets": "Delete assets", - "unassign-assets": "Unassign assets", - "unassign-assets-action-title": "Unassign { count, plural, 1 {1 asset} other {# assets} } from customer", - "assign-new-asset": "Assign new asset", - "delete-asset-title": "Are you sure you want to delete the asset '{{assetName}}'?", - "delete-asset-text": "Be careful, after the confirmation the asset and all related data will become unrecoverable.", - "delete-assets-title": "Are you sure you want to delete { count, plural, 1 {1 asset} other {# assets} }?", - "delete-assets-action-title": "Delete { count, plural, 1 {1 asset} other {# assets} }", - "delete-assets-text": "Be careful, after the confirmation all selected assets will be removed and all related data will become unrecoverable.", - "make-public-asset-title": "Are you sure you want to make the asset '{{assetName}}' public?", - "make-public-asset-text": "After the confirmation the asset and all its data will be made public and accessible by others.", - "make-private-asset-title": "Are you sure you want to make the asset '{{assetName}}' private?", - "make-private-asset-text": "After the confirmation the asset and all its data will be made private and won't be accessible by others.", - "unassign-asset-title": "Are you sure you want to unassign the asset '{{assetName}}'?", - "unassign-asset-text": "After the confirmation the asset will be unassigned and won't be accessible by the customer.", - "unassign-asset": "Unassign asset", - "unassign-assets-title": "Are you sure you want to unassign { count, plural, 1 {1 asset} other {# assets} }?", - "unassign-assets-text": "After the confirmation all selected assets will be unassigned and won't be accessible by the customer.", - "copyId": "Copy asset Id", - "idCopiedMessage": "Asset Id has been copied to clipboard", - "select-asset": "Select asset", - "no-assets-matching": "No assets matching '{{entity}}' were found.", - "asset-required": "Asset is required", - "name-starts-with": "Asset name starts with" - }, - "attribute": { - "attributes": "속성", - "latest-telemetry": "최근 데이터", - "attributes-scope": "디바이스 속성 범위", - "scope-latest-telemetry": "최근 데이터", - "scope-client": "클라이언트 속성", - "scope-server": "서버 속성", - "scope-shared": "공유 속성", - "add": "속성 추가", - "key": "Key", - "key-required": "속성 key를 입력하세요.", - "value": "Value", - "value-required": "속성 value를 입력하세요.", - "delete-attributes-title": "{ count, plural, 1 {속성} other {여러 속성들을} } 삭제하시겠습니까??", - "delete-attributes-text": "모든 선택된 속성들이 제거 될 것이므로 주의하십시오.", - "delete-attributes": "속성 삭제", - "enter-attribute-value": "속성 값 입력", - "show-on-widget": "위젯 보기", - "widget-mode": "위젯 모드", - "next-widget": "다음 위젯", - "prev-widget": "이전 위젯", - "add-to-dashboard": "대시보드에 추가", - "add-widget-to-dashboard": "대시보드에 위젯 추가", - "selected-attributes": "{ count, plural, 1 {속성 1개} other {속성 #개} } 선택됨", - "selected-telemetry": "{ count, plural, 1 {최근 데이터 1개} other {최근 데이터 #개} } 선택됨" - }, - "audit-log": { - "audit": "Audit", - "audit-logs": "Audit Logs", - "timestamp": "Timestamp", - "entity-type": "Entity Type", - "entity-name": "Entity Name", - "user": "User", - "type": "Type", - "status": "Status", - "details": "Details", - "type-added": "Added", - "type-deleted": "Deleted", - "type-updated": "Updated", - "type-attributes-updated": "Attributes updated", - "type-attributes-deleted": "Attributes deleted", - "type-rpc-call": "RPC call", - "type-credentials-updated": "Credentials updated", - "type-assigned-to-customer": "Assigned to Customer", - "type-unassigned-from-customer": "Unassigned from Customer", - "type-activated": "Activated", - "type-suspended": "Suspended", - "type-credentials-read": "Credentials read", - "type-attributes-read": "Attributes read", - "status-success": "Success", - "status-failure": "Failure", - "audit-log-details": "Audit log details", - "no-audit-logs-prompt": "No logs found", - "action-data": "Action data", - "failure-details": "Failure details", - "search": "Search audit logs", - "clear-search": "Clear search" - }, - "confirm-on-exit": { - "message": "변경 사항을 저장하지 않았습니다. 이 페이지를 나가시겠습니까?", - "html-message": "변경 사항을 저장하지 않았습니다.
    이 페이지를 나가시겠습니까?", - "title": "저장되지 않은 변경사항" - }, - "contact": { - "country": "국가", - "city": "시", - "state": "도", - "postal-code": "우편 번호", - "postal-code-invalid": "숫자만 입력하세요.", - "address": "주소", - "address2": "상세주소", - "phone": "전화번호", - "email": "Email", - "no-address": "주소 정보 없음" - }, - "common": { - "username": "사용자명", - "password": "비밀번호", - "enter-username": "사용자명을 입력하세요.", - "enter-password": "비밀번호를 입력하세요.", - "enter-search": "검색어 입력" - }, - "content-type": { - "json": "Json", - "text": "Text", - "binary": "Binary (Base64)" - }, - "customer": { - "customers": "커스터머", - "management": "커스터머 관리", - "dashboard": "커스터머 대시보드", - "dashboards": "커스터머 대시보드", - "devices": "커스터머 디바이스", - "add": "커스터머 추가", - "delete": "커스터머 삭제", - "manage-customer-users": "커스터머 사용자 관리", - "manage-customer-devices": "커스터머 디바이스 관리", - "manage-customer-dashboards": "커스터머 대시보드 관리", - "manage-public-devices": "Manage public devices", - "manage-public-dashboards": "Manage public dashboards", - "manage-customer-assets": "Manage customer assets", - "manage-public-assets": "Manage public assets", - "add-customer-text": "커스터머 추가", - "no-customers-text": "커스터머가 없습니다.", - "customer-details": "커스터머 상세정보", - "delete-customer-title": "'{{customerTitle}}' 커스터머를 삭제하시겠습니까?", - "delete-customer-text": "커스터머 및 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", - "delete-customers-title": "{ count, plural, 1 {커스터머 1개} other {커스터머 #개} }를 삭제하시겠습니까?", - "delete-customers-action-title": "{ count, plural, 1 {커스터머 1개} other {커스터머 #개} } 삭제", - "delete-customers-text": "선택된 커스터머는 삭제되고 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", - "manage-users": "사용자 관리", - "manage-devices": "디바이스 관리", - "manage-dashboards": "대시보드 관리", - "title": "타이틀", - "title-required": "타이틀을 입력하세요.", - "description": "설명", - "details": "Details", - "events": "Events", - "copyId": "Copy customer Id", - "idCopiedMessage": "Customer Id has been copied to clipboard", - "select-customer": "Select customer", - "no-customers-matching": "No customers matching '{{entity}}' were found.", - "customer-required": "Customer is required", - "select-default-customer": "Select default customer", - "default-customer": "Default customer", - "default-customer-required": "Default customer is required in order to debug dashboard on Tenant level" - }, - "datetime": { - "date-from": "시작 날짜", - "time-from": "시작 시간", - "date-to": "종료 날짜", - "time-to": "종료 시간" - }, - "dashboard": { - "dashboard": "대시보드", - "dashboards": "대시보드", - "management": "대시보드 관리", - "view-dashboards": "대시보드 보기", - "add": "대시보드 추가", - "assign-dashboard-to-customer": "대시보드 커스터머 선택", - "assign-dashboard-to-customer-text": "대시보드 커스터머를 선택하세요.", - "assign-to-customer-text": "대시보드 커스터머를 선택하세요.", - "assign-to-customer": "커스터머 선택", - "unassign-from-customer": "커스터머 해제", - "no-dashboards-text": "대시보드가 없습니다", - "no-widgets": "설정된 위젯 없음", - "add-widget": "위젯 추가", - "title": "타이틀", - "select-widget-title": "위젯 선택", - "select-widget-subtitle": "사용가능한 위젯 타입 목록", - "delete": "대시보드 삭제", - "title-required": "타이틀을 입력하세요.", - "description": "설명", - "details": "상세", - "dashboard-details": "대시보드 상세정보", - "add-dashboard-text": "대시보드 추가", - "assign-dashboards": "대시보드 지정", - "assign-new-dashboard": "새 대시보드 할당", - "assign-dashboards-text": "{ count, plural, 1 {대시보드 1개} other {대시보드 #개} }를 커스터머 할당", - "delete-dashboards": "대시보드 삭제", - "unassign-dashboards": "대시보드 할당 취소", - "unassign-dashboards-action-title": "{ count, plural, 1 {대시보드 1개} other {대시보드 #개} }를 커스터머 할당 취소", - "delete-dashboard-title": "'{{dashboardTitle}}' 대시보드를 삭제하시겠습니까?", - "delete-dashboard-text": "대시보드 및 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", - "delete-dashboards-title": "{ count, plural, 1 {대시보드 1개} other {대시보드 #개} }를 삭제하시겠습니까?", - "delete-dashboards-action-title": "{ count, plural, 1 {대시보드 1개} other {대시보드 #개} } 삭제", - "delete-dashboards-text": "선택된 대시보드가 삭제되고 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", - "unassign-dashboard-title": "'{{dashboardTitle}}' 대시보드 할당을 해제하시겠습니까?", - "unassign-dashboard-text": "대시보드가 할당 해제되고 커스터머는 액세스 할 수 없게됩니다.", - "unassign-dashboard": "대시보드 할달 취소", - "unassign-dashboards-title": "{ count, plural, 1 {대시보드 1개} other {대시보드 #개} }의 할당을 취소하시겠습니까?", - "unassign-dashboards-text": "선택된 대시보드가 할당 해제되고 커스터머는 액세스 할 수 없게됩니다.", - "select-dashboard": "대시보드 선택", - "no-dashboards-matching": "'{{entity}}'와 일치하는 대시보드가 없습니다.", - "dashboard-required": "대시보드를 입력하세요.", - "select-existing": "기존 대시보드 선택", - "create-new": "대시보드 생성", - "new-dashboard-title": "새로운 대시보드 타이틀", - "open-dashboard": "대시보드 열기", - "set-background": "대시보드 설정", - "background-color": "배경색", - "background-image": "배경 이미지", - "background-size-mode": "배경 사이즈 모드", - "no-image": "이미지 없음", - "drop-image": "이곳에 이미지를 끌어다놓거나 이곳을 클릭하여 파일을 선택하고 업로드하세요.", - "settings": "설정", - "columns-count": "열 개수", - "columns-count-required": "열 개수를 입력하세요.", - "min-columns-count-message": "열 개수를 최소 10 이상 입력하세요.", - "max-columns-count-message": "열 개수를 최대 100 이하로 입력하세요.", - "widgets-margins": "위젯 사이 여백 크기", - "horizontal-margin": "세로 여백", - "horizontal-margin-required": "세로 여백 값을 입력하세요.", - "min-horizontal-margin-message": "세로 여백 값을 최소 0 이상 입력하세요.", - "max-horizontal-margin-message": "세로 여백 값을 최대 50 이하로 입력하세요.", - "vertical-margin": "가로 여백", - "vertical-margin-required": "가로 여백 값을 입력하세요.", - "min-vertical-margin-message": "가로 여백 값을 최소 0 이상 입력하세요.", - "max-vertical-margin-message": "가로 여백 값을 최대 50 이하로 입력하세요.", - "display-title": "대시보드 타이틀 표시", - "title-color": "타이틀 색상", - "import": "대시보드 가져오기", - "export": "대시보드 내보내기", - "export-failed-error": "대시보드 내보내기를 할 수 없습니다.: {error}", - "create-new-dashboard": "대시보드 생성", - "dashboard-file": "대시보드 파일", - "invalid-dashboard-file-error": "대시보드 가져오기를 할 수 없습니다.: 대시보드 데이터 구조가 잘못되었습니다.", - "dashboard-import-missing-aliases-title": "대시보드 앨리어스를 위해 누락 된 디바이스 선택", - "create-new-widget": "새로운 위젯 생성", - "import-widget": "위젯 가져오기", - "widget-file": "위젯 파일", - "invalid-widget-file-error": "위젯 가져오기를 할 수 없습니다: 위젯 데이터 구조가 잘못되었습니다.", - "widget-import-missing-aliases-title": "위젯에서 사용하는 누락 된 디바이스 선택", - "open-toolbar": "대시보드 툴바 열기", - "close-toolbar": "툴바 닫기", - "configuration-error": "구성 오류", - "alias-resolution-error-title": "대시보드 앨리어스 구성 오류", - "invalid-aliases-config": "일부 앨리어스 필터와 일치하는 디바이스를 찾을 수 없습니다.
    이 문제를 해결하려면 관리자에게 문의하십시오.", - "select-devices": "디바이스 선택", - "assignedToCustomer": "커스터머에 할당됨" - }, - "datakey": { - "settings": "설정", - "advanced": "고급", - "label": "Label", - "color": "색상", - "data-generation-func": "데이터 생성 기능", - "use-data-post-processing-func": "데이터 후처리 기능 사용", - "configuration": "데이터 key 구성", - "timeseries": "Timeseries", - "attributes": "Attributes", - "timeseries-required": "디바이스 timeseries 를 입력하세요.", - "timeseries-or-attributes-required": "디바이스 timeseries/attributes 를 입력하세요.", - "maximum-timeseries-or-attributes": "Maximum { count, plural, 1 {1 timeseries/attribute is allowed.} other {# timeseries/attributes are allowed} }", - "alarm-fields-required": "Alarm fields are required.", - "function-types": "함수 유형", - "function-types-required": "함수 유형을 입력하세요.", - "maximum-function-types": "Maximum { count, plural, 1 {1 function type is allowed.} other {# function types are allowed} }" - }, - "datasource": { - "type": "데이터소스 유형", - "name": "Name", - "add-datasource-prompt": "데이터소스를 추가하세요." - }, - "details": { - "edit-mode": "편집 모드", - "toggle-edit-mode": "편집 모드 전환" - }, - "device": { - "device": "디바이스", - "device-required": "디바이스를 입력하세요.", - "devices": "디바이스", - "management": "디바이스 관리", - "view-devices": "디바이스 보기", - "device-alias": "디바이스 앨리어스", - "aliases": "디바이스 앨리어스", - "no-alias-matching": "'{{alias}}' 를 찾을 수 없습니다.", - "no-aliases-found": "앨리어스가 없습니다.", - "no-key-matching": "'{{key}}' 를 찾을 수 없습니다.", - "no-keys-found": "Key가 없습니다.", - "create-new-alias": "새로 만들기!", - "create-new-key": "새로 만들기!", - "duplicate-alias-error": "중복된 '{{alias}}' 앨리어스가 있습니다.
    디바이스 앨리어스는 대시보드 내에서 고유해야 합니다.", - "configure-alias": "'{{alias}}' 앨리어스 구성", - "no-devices-matching": "'{{entity}}'와 일치하는 디바이스를 찾을 수 없습니다.", - "alias": "앨리어스", - "alias-required": "디바이스 앨리어스를 입력하세요.", - "remove-alias": "디바이스 앨리어스 삭제", - "add-alias": "디바이스 앨리어스 추가", - "name-starts-with": "시작되는 이름", - "device-list": "디바이스 리스트", - "use-device-name-filter": "필터 사용", - "device-list-empty": "선택된 디바이스가 없습니다.", - "device-name-filter-required": "디바이스 필터 이름을 입력하세요.", - "device-name-filter-no-device-matched": "'{{device}}' 로 시작되는 디바이스를 찾을 수 없습니다.", - "add": "디바이스 추가", - "assign-to-customer": "커스터머에게 할당", - "assign-device-to-customer": "디바이스를 커스터머에게 할당", - "assign-device-to-customer-text": "고객에게 할당할 디바이스를 선택하십시오.", - "no-devices-text": "디바이스 없음", - "assign-to-customer-text": "디바이스를 할당할 커스터머를 선택하세요.", - "device-details": "디바이스 상세정보", - "add-device-text": "디바이스 추가", - "credentials": "크리덴셜", - "manage-credentials": "크리덴셜 관리", - "delete": "디바이스 삭제", - "assign-devices": "디바이스 할당", - "assign-devices-text": "{ count, plural, 1 {디바이스 1개} other {디바이스 #개} }를 커서터머에 할당", - "delete-devices": "디바이스 삭제", - "unassign-from-customer": "커스터머 할당 해제", - "unassign-devices": "디바이스 할당 취소", - "unassign-devices-action-title": "{ count, plural, 1 {디바이스 1개} other {디바이스 #개} }를 커스터머에게서 할당 해제", - "assign-new-device": "새로운 디바이스 할당", - "view-credentials": "크리덴셜 보기", - "delete-device-title": "'{{deviceName}}' 디바이스를 삭제하시겠습니까?", - "delete-device-text": "디바이스 및 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", - "delete-devices-title": "{ count, plural, 1 {디바이스 1개} other {디바이스 #개} }를 삭제하시겠습니까?", - "delete-devices-action-title": "{ count, plural, 1 {디바이스 1개} other {디바이스 #개} } 삭제", - "delete-devices-text": "선택된 디바이스가 삭제되고 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", - "unassign-device-title": "'{{deviceName}}' 디바이스 할당을 해제하시겠습니까?", - "unassign-device-text": "디바이스가 할당 해제되고 커스터머는 액세스 할 수 없게됩니다.", - "unassign-device": "디바이스 할당 취소", - "unassign-devices-title": "{ count, plural, 1 {디바이스 1개} other {디바이스 #개} }의 할당을 해제하시겠습니까??", - "unassign-devices-text": "선택된 디바이스가 할당 해제되고 커스터머는 액세스 할 수 없게됩니다.", - "device-credentials": "디바이스 크리덴셜", - "credentials-type": "크리덴셜 타입", - "access-token": "억세스 토큰", - "access-token-required": "액세스 토큰을 입력하세요.", - "access-token-invalid": "액세스 토큰 길이는 1 - 20 자 여야합니다.", - "rsa-key": "RSA public key", - "rsa-key-required": "RSA public key 를 입력하세요.", - "secret": "시크릿", - "secret-required": "시크릿을 입력하세요.", - "name": "이름", - "name-required": "이름을 입력하세요.", - "description": "설명", - "events": "이벤트", - "details": "상세", - "copyId": "디바이스 아이디 복사", - "copyAccessToken": "억세스 토큰 복사", - "idCopiedMessage": "디바이스 아이디가 클립보드에 복사되었습니다.", - "accessTokenCopiedMessage": "디바이스 억세스 토큰이 클립보드에 복사되었습니다.", - "assignedToCustomer": "커스터머에 할당됨", - "unable-delete-device-alias-title": "디바이스 앨리어스를 삭제할 수 없습니다.", - "unable-delete-device-alias-text": "'{{deviceAlias}}' 디바이스 앨리어스를 삭제할 수 없습니다. 다음 위젯에서 사용하고 있습니다.
    {{widgetsList}}", - "is-gateway": "게이트웨이 여부" - }, - "dialog": { - "close": "다이얼로그 닫기" - }, - "error": { - "unable-to-connect": "서버에 연결할 수 없습니다! 인터넷 연결을 확인하십시오.", - "unhandled-error-code": "처리되지 않은 오류 코드: {{errorCode}}", - "unknown-error": "알 수 없는 오류" - }, - "entity": { - "entity": "Entity", - "entities": "Entities", - "aliases": "Entity aliases", - "entity-alias": "Entity alias", - "unable-delete-entity-alias-title": "Unable to delete entity alias", - "unable-delete-entity-alias-text": "Entity alias '{{entityAlias}}' can't be deleted as it used by the following widget(s):
    {{widgetsList}}", - "duplicate-alias-error": "Duplicate alias found '{{alias}}'.
    Entity aliases must be unique whithin the dashboard.", - "missing-entity-filter-error": "Filter is missing for alias '{{alias}}'.", - "configure-alias": "Configure '{{alias}}' alias", - "alias": "Alias", - "alias-required": "Entity alias is required.", - "remove-alias": "Remove entity alias", - "add-alias": "Add entity alias", - "entity-list": "Entity list", - "entity-type": "Entity type", - "entity-types": "Entity types", - "entity-type-list": "Entity type list", - "any-entity": "Any entity", - "enter-entity-type": "Enter entity type", - "no-entities-matching": "No entities matching '{{entity}}' were found.", - "no-entity-types-matching": "No entity types matching '{{entityType}}' were found.", - "name-starts-with": "Name starts with", - "use-entity-name-filter": "Use filter", - "entity-list-empty": "No entities selected.", - "entity-type-list-empty": "No entity types selected.", - "entity-name-filter-required": "Entity name filter is required.", - "entity-name-filter-no-entity-matched": "No entities starting with '{{entity}}' were found.", - "all-subtypes": "All", - "select-entities": "Select entities", - "no-aliases-found": "No aliases found.", - "no-alias-matching": "'{{alias}}' not found.", - "create-new-alias": "Create a new one!", - "key": "Key", - "key-name": "Key name", - "no-keys-found": "No keys found.", - "no-key-matching": "'{{key}}' not found.", - "create-new-key": "Create a new one!", - "type": "Type", - "type-required": "Entity type is required.", - "type-device": "Device", - "type-devices": "Devices", - "list-of-devices": "{ count, plural, 1 {One device} other {List of # devices} }", - "device-name-starts-with": "Devices whose names start with '{{prefix}}'", - "type-asset": "Asset", - "type-assets": "Assets", - "list-of-assets": "{ count, plural, 1 {One asset} other {List of # assets} }", - "asset-name-starts-with": "Assets whose names start with '{{prefix}}'", - "type-rule": "Rule", - "type-rules": "Rules", - "list-of-rules": "{ count, plural, 1 {One rule} other {List of # rules} }", - "rule-name-starts-with": "Rules whose names start with '{{prefix}}'", - "type-plugin": "Plugin", - "type-plugins": "Plugins", - "list-of-plugins": "{ count, plural, 1 {One plugin} other {List of # plugins} }", - "plugin-name-starts-with": "Plugins whose names start with '{{prefix}}'", - "type-tenant": "Tenant", - "type-tenants": "Tenants", - "list-of-tenants": "{ count, plural, 1 {One tenant} other {List of # tenants} }", - "tenant-name-starts-with": "Tenants whose names start with '{{prefix}}'", - "type-customer": "Customer", - "type-customers": "Customers", - "list-of-customers": "{ count, plural, 1 {One customer} other {List of # customers} }", - "customer-name-starts-with": "Customers whose names start with '{{prefix}}'", - "type-user": "User", - "type-users": "Users", - "list-of-users": "{ count, plural, 1 {One user} other {List of # users} }", - "user-name-starts-with": "Users whose names start with '{{prefix}}'", - "type-dashboard": "Dashboard", - "type-dashboards": "Dashboards", - "list-of-dashboards": "{ count, plural, 1 {One dashboard} other {List of # dashboards} }", - "dashboard-name-starts-with": "Dashboards whose names start with '{{prefix}}'", - "type-alarm": "Alarm", - "type-alarms": "Alarms", - "list-of-alarms": "{ count, plural, 1 {One alarms} other {List of # alarms} }", - "alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'", - "type-rulechain": "Rule chain", - "type-rulechains": "Rule chains", - "list-of-rulechains": "{ count, plural, 1 {One rule chain} other {List of # rule chains} }", - "rulechain-name-starts-with": "Rule chains whose names start with '{{prefix}}'", - "type-current-customer": "Current Customer", - "search": "Search entities", - "selected-entities": "{ count, plural, 1 {1 entity} other {# entities} } selected", - "entity-name": "Entity name", - "details": "Entity details", - "no-entities-prompt": "No entities found", - "no-data": "No data to display" - }, - "event": { - "event-type": "이벤트 타입", - "type-error": "에러", - "type-lc-event": "주기적 이벤트", - "type-stats": "통계", - "type-debug-rule-node": "Debug", - "type-debug-rule-chain": "Debug", - "no-events-prompt": "이벤트 없음", - "error": "에러", - "alarm": "알람", - "event-time": "이벤트 발생 시간", - "server": "서버", - "body": "Body", - "method": "Method", - "type": "Type", - "entity": "Entity", - "message-id": "Message Id", - "message-type": "Message Type", - "data-type": "Data Type", - "relation-type": "Relation Type", - "metadata": "Metadata", - "data": "Data", - "event": "이벤트", - "status": "상태", - "success": "성공", - "failed": "실패", - "messages-processed": "처리된 메시지", - "errors-occurred": "오류가 발생했습니다" - }, - "extension": { - "extensions": "Extensions", - "selected-extensions": "{ count, plural, 1 {1 extension} other {# extensions} } selected", - "type": "Type", - "key": "Key", - "value": "Value", - "id": "Id", - "extension-id": "Extension id", - "extension-type": "Extension type", - "transformer-json": "JSON *", - "unique-id-required": "Current extension id already exists.", - "delete": "Delete extension", - "add": "Add extension", - "edit": "Edit extension", - "delete-extension-title": "Are you sure you want to delete the extension '{{extensionId}}'?", - "delete-extension-text": "Be careful, after the confirmation the extension and all related data will become unrecoverable.", - "delete-extensions-title": "Are you sure you want to delete { count, plural, 1 {1 extension} other {# extensions} }?", - "delete-extensions-text": "Be careful, after the confirmation all selected extensions will be removed.", - "converters": "Converters", - "converter-id": "Converter id", - "configuration": "Configuration", - "converter-configurations": "Converter configurations", - "token": "Security token", - "add-converter": "Add converter", - "add-config": "Add converter configuration", - "device-name-expression": "Device name expression", - "device-type-expression": "Device type expression", - "custom": "Custom", - "to-double": "To Double", - "transformer": "Transformer", - "json-required": "Transformer json is required.", - "json-parse": "Unable to parse transformer json.", - "attributes": "Attributes", - "add-attribute": "Add attribute", - "add-map": "Add mapping element", - "timeseries": "Timeseries", - "add-timeseries": "Add timeseries", - "field-required": "Field is required", - "brokers": "Brokers", - "add-broker": "Add broker", - "host": "Host", - "port": "Port", - "port-range": "Port should be in a range from 1 to 65535.", - "ssl": "Ssl", - "credentials": "Credentials", - "username": "Username", - "password": "Password", - "retry-interval": "Retry interval in milliseconds", - "anonymous": "Anonymous", - "basic": "Basic", - "pem": "PEM", - "ca-cert": "CA certificate file *", - "private-key": "Private key file *", - "cert": "Certificate file *", - "no-file": "No file selected.", - "drop-file": "Drop a file or click to select a file to upload.", - "mapping": "Mapping", - "topic-filter": "Topic filter", - "converter-type": "Converter type", - "converter-json": "Json", - "json-name-expression": "Device name json expression", - "topic-name-expression": "Device name topic expression", - "json-type-expression": "Device type json expression", - "topic-type-expression": "Device type topic expression", - "attribute-key-expression": "Attribute key expression", - "attr-json-key-expression": "Attribute key json expression", - "attr-topic-key-expression": "Attribute key topic expression", - "request-id-expression": "Request id expression", - "request-id-json-expression": "Request id json expression", - "request-id-topic-expression": "Request id topic expression", - "response-topic-expression": "Response topic expression", - "value-expression": "Value expression", - "topic": "Topic", - "timeout": "Timeout in milliseconds", - "converter-json-required": "Converter json is required.", - "converter-json-parse": "Unable to parse converter json.", - "filter-expression": "Filter expression", - "connect-requests": "Connect requests", - "add-connect-request": "Add connect request", - "disconnect-requests": "Disconnect requests", - "add-disconnect-request": "Add disconnect request", - "attribute-requests": "Attribute requests", - "add-attribute-request": "Add attribute request", - "attribute-updates": "Attribute updates", - "add-attribute-update": "Add attribute update", - "server-side-rpc": "Server side RPC", - "add-server-side-rpc-request": "Add server-side RPC request", - "device-name-filter": "Device name filter", - "attribute-filter": "Attribute filter", - "method-filter": "Method filter", - "request-topic-expression": "Request topic expression", - "response-timeout": "Response timeout in milliseconds", - "topic-expression": "Topic expression", - "client-scope": "Client scope", - "add-device": "Add device", - "opc-server": "Servers", - "opc-add-server": "Add server", - "opc-add-server-prompt": "Please add server", - "opc-application-name": "Application name", - "opc-application-uri": "Application uri", - "opc-scan-period-in-seconds": "Scan period in seconds", - "opc-security": "Security", - "opc-identity": "Identity", - "opc-keystore": "Keystore", - "opc-type": "Type", - "opc-keystore-type": "Type", - "opc-keystore-location": "Location *", - "opc-keystore-password": "Password", - "opc-keystore-alias": "Alias", - "opc-keystore-key-password": "Key password", - "opc-device-node-pattern": "Device node pattern", - "opc-device-name-pattern": "Device name pattern", - "modbus-server": "Servers/slaves", - "modbus-add-server": "Add server/slave", - "modbus-add-server-prompt": "Please add server/slave", - "modbus-transport": "Transport", - "modbus-port-name": "Serial port name", - "modbus-encoding": "Encoding", - "modbus-parity": "Parity", - "modbus-baudrate": "Baud rate", - "modbus-databits": "Data bits", - "modbus-stopbits": "Stop bits", - "modbus-databits-range": "Data bits should be in a range from 7 to 8.", - "modbus-stopbits-range": "Stop bits should be in a range from 1 to 2.", - "modbus-unit-id": "Unit ID", - "modbus-unit-id-range": "Unit ID should be in a range from 1 to 247.", - "modbus-device-name": "Device name", - "modbus-poll-period": "Poll period (ms)", - "modbus-attributes-poll-period": "Attributes poll period (ms)", - "modbus-timeseries-poll-period": "Timeseries poll period (ms)", - "modbus-poll-period-range": "Poll period should be positive value.", - "modbus-tag": "Tag", - "modbus-function": "Function", - "modbus-register-address": "Register address", - "modbus-register-address-range": "Register address should be in a range from 0 to 65535.", - "modbus-register-bit-index": "Bit index", - "modbus-register-bit-index-range": "Bit index should be in a range from 0 to 15.", - "modbus-register-count": "Register count", - "modbus-register-count-range": "Register count should be a positive value.", - "modbus-byte-order": "Byte order", - "sync": { - "status": "Status", - "sync": "Sync", - "not-sync": "Not sync", - "last-sync-time": "Last sync time", - "not-available": "Not available" - }, - "export-extensions-configuration": "Export extensions configuration", - "import-extensions-configuration": "Import extensions configuration", - "import-extensions": "Import extensions", - "import-extension": "Import extension", - "export-extension": "Export extension", - "file": "Extensions file", - "invalid-file-error": "Invalid extension file" - }, - "fullscreen": { - "expand": "전체화면으로 확장", - "exit": "전체화면 종료", - "toggle": "전체화면 모드 전환", - "fullscreen": "전체화면" - }, - "function": { - "function": "기능" - }, - "grid": { - "delete-item-title": "이 항목을 삭제 하시겠습니까?", - "delete-item-text": "항목과 모든 관련 데이터를 복구 할 수 없으므로 주의하십시오.", - "delete-items-title": "{ count, plural, 1 {아이템 1개} other {아이템 #개} }를 삭제하시겠습니까?", - "delete-items-action-title": "{ count, plural, 1 {아이템 1개} other {아이템 #개} } 삭제", - "delete-items-text": "선택한 모든 아이템이 제거되고 관련된 모든 데이터는 복구 할 수 없으므로 주의하십시오.", - "add-item-text": "새로운 아이템 추가", - "no-items-text": "아이템이 없습니다.", - "item-details": "아이템 상세", - "delete-item": "아이템 삭제", - "delete-items": "아이템 삭제", - "scroll-to-top": "스크롤 맨 위로" - }, - "help": { - "goto-help-page": "도움" - }, - "home": { - "home": "홈", - "profile": "프로파일", - "logout": "로그아웃", - "menu": "메뉴", - "avatar": "Avatar", - "open-user-menu": "사용자 메뉴 열기" - }, - "import": { - "no-file": "선택된 파일이 없습니다.", - "drop-file": "JSON 파일을 끌어다 놓거나 클릭하여 업로드 할 파일을 선택하십시오." - }, - "item": { - "selected": "선택됨" - }, - "js-func": { - "no-return-error": "함수는 값을 반환해야 합니다!", - "return-type-mismatch": "함수는 '{{type}}' 유형의 값을 반환해야 합니다!", - "tidy": "Tidy" - }, - "key-val": { - "key": "Key", - "value": "Value", - "remove-entry": "Remove entry", - "add-entry": "Add entry", - "no-data": "No entries" - }, - "layout": { - "layout": "Layout", - "manage": "Manage layouts", - "settings": "Layout settings", - "color": "Color", - "main": "Main", - "right": "Right", - "select": "Select target layout" - }, - "legend": { - "position": "범례 위치", - "show-max": "최대값 표시", - "show-min": "최소값 표시", - "show-avg": "평균값 표시", - "show-total": "총합 표시", - "settings": "범례 설정", - "min": "최소", - "max": "최대", - "avg": "평균", - "total": "합계" - }, - "login": { - "login": "로그인", - "request-password-reset": "비밀번호 재설정", - "reset-password": "비밀번호 재설정", - "create-password": "비밀번호 생성", - "passwords-mismatch-error": "입력된 비밀번호는 같아야 합니다!", - "password-again": "비밀번호 확인", - "sign-in": "로그인", - "username": "사용자명 (이메일)", - "remember-me": "아이디 저장", - "forgot-password": "비밀번호찾기", - "password-reset": "비밀번호 재설정", - "new-password": "새 비밀번호", - "new-password-again": "새 비밀번호 확인", - "password-link-sent-message": "비밀번호 재설정 링크가 성공적으로 전송되었습니다!", - "email": "이메일", - "login-with": "{{name}}으로 로그인", - "or": "또는" - }, - "position": { - "top": "상단", - "bottom": "하단", - "left": "왼쪽", - "right": "오른쪽" - }, - "profile": { - "profile": "프로파일", - "change-password": "비밀번호 변경", - "current-password": "현재 비밀번호" - }, - "relation": { - "relations": "Relations", - "direction": "Direction", - "search-direction": { - "FROM": "From", - "TO": "To" - }, - "direction-type": { - "FROM": "from", - "TO": "to" - }, - "from-relations": "Outbound relations", - "to-relations": "Inbound relations", - "selected-relations": "{ count, plural, 1 {1 relation} other {# relations} } selected", - "type": "Type", - "to-entity-type": "To entity type", - "to-entity-name": "To entity name", - "from-entity-type": "From entity type", - "from-entity-name": "From entity name", - "to-entity": "To entity", - "from-entity": "From entity", - "delete": "Delete relation", - "relation-type": "Relation type", - "relation-type-required": "Relation type is required.", - "any-relation-type": "Any type", - "add": "Add relation", - "edit": "Edit relation", - "delete-to-relation-title": "Are you sure you want to delete relation to the entity '{{entityName}}'?", - "delete-to-relation-text": "Be careful, after the confirmation the entity '{{entityName}}' will be unrelated from the current entity.", - "delete-to-relations-title": "Are you sure you want to delete { count, plural, 1 {1 relation} other {# relations} }?", - "delete-to-relations-text": "Be careful, after the confirmation all selected relations will be removed and corresponding entities will be unrelated from the current entity.", - "delete-from-relation-title": "Are you sure you want to delete relation from the entity '{{entityName}}'?", - "delete-from-relation-text": "Be careful, after the confirmation current entity will be unrelated from the entity '{{entityName}}'.", - "delete-from-relations-title": "Are you sure you want to delete { count, plural, 1 {1 relation} other {# relations} }?", - "delete-from-relations-text": "Be careful, after the confirmation all selected relations will be removed and current entity will be unrelated from the corresponding entities.", - "remove-relation-filter": "Remove relation filter", - "add-relation-filter": "Add relation filter", - "any-relation": "Any relation", - "relation-filters": "Relation filters", - "additional-info": "Additional info (JSON)", - "invalid-additional-info": "Unable to parse additional info json." - }, - "rulechain": { - "rulechain": "Rule chain", - "rulechains": "Rule chains", - "root": "Root", - "delete": "Delete rule chain", - "name": "Name", - "name-required": "Name is required.", - "description": "Description", - "add": "Add Rule Chain", - "set-root": "Make rule chain root", - "set-root-rulechain-title": "Are you sure you want to make the rule chain '{{ruleChainName}}' root?", - "set-root-rulechain-text": "After the confirmation the rule chain will become root and will handle all incoming transport messages.", - "delete-rulechain-title": "Are you sure you want to delete the rule chain '{{ruleChainName}}'?", - "delete-rulechain-text": "Be careful, after the confirmation the rule chain and all related data will become unrecoverable.", - "delete-rulechains-title": "Are you sure you want to delete { count, plural, 1 {1 rule chain} other {# rule chains} }?", - "delete-rulechains-action-title": "Delete { count, plural, 1 {1 rule chain} other {# rule chains} }", - "delete-rulechains-text": "Be careful, after the confirmation all selected rule chains will be removed and all related data will become unrecoverable.", - "add-rulechain-text": "Add new rule chain", - "no-rulechains-text": "No rule chains found", - "rulechain-details": "Rule chain details", - "details": "Details", - "events": "Events", - "system": "System", - "import": "Import rule chain", - "export": "Export rule chain", - "export-failed-error": "Unable to export rule chain: {{error}}", - "create-new-rulechain": "Create new rule chain", - "rulechain-file": "Rule chain file", - "invalid-rulechain-file-error": "Unable to import rule chain: Invalid rule chain data structure.", - "copyId": "Copy rule chain Id", - "idCopiedMessage": "Rule chain Id has been copied to clipboard", - "select-rulechain": "Select rule chain", - "no-rulechains-matching": "No rule chains matching '{{entity}}' were found.", - "rulechain-required": "Rule chain is required", - "management": "Rules management", - "debug-mode": "Debug mode" - }, - "rulenode": { - "details": "Details", - "events": "Events", - "search": "Search nodes", - "open-node-library": "Open node library", - "add": "Add rule node", - "name": "Name", - "name-required": "Name is required.", - "type": "Type", - "description": "Description", - "delete": "Delete rule node", - "select-all-objects": "Select all nodes and connections", - "deselect-all-objects": "Deselect all nodes and connections", - "delete-selected-objects": "Delete selected nodes and connections", - "delete-selected": "Delete selected", - "select-all": "Select all", - "copy-selected": "Copy selected", - "deselect-all": "Deselect all", - "rulenode-details": "Rule node details", - "debug-mode": "Debug mode", - "configuration": "Configuration", - "link": "Link", - "link-details": "Rule node link details", - "add-link": "Add link", - "link-label": "Link label", - "link-label-required": "Link label is required.", - "custom-link-label": "Custom link label", - "custom-link-label-required": "Custom link label is required.", - "type-filter": "Filter", - "type-filter-details": "Filter incoming messages with configured conditions", - "type-enrichment": "Enrichment", - "type-enrichment-details": "Add additional information into Message Metadata", - "type-transformation": "Transformation", - "type-transformation-details": "Change Message payload and Metadata", - "type-action": "Action", - "type-action-details": "Perform special action", - "type-external": "External", - "type-external-details": "Interacts with external system", - "type-rule-chain": "Rule Chain", - "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain", - "type-input": "Input", - "type-input-details": "Logical input of Rule Chain, forwards incoming messages to next related Rule Node", - "directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.", - "ui-resources-load-error": "Failed to load configuration ui resources.", - "invalid-target-rulechain": "Unable to resolve target rule chain!", - "test-script-function": "Test script function", - "message": "Message", - "message-type": "Message type", - "message-type-required": "Message type is required", - "metadata": "Metadata", - "metadata-required": "Metadata entries can't be empty.", - "output": "Output", - "test": "Test", - "help": "Help" - }, - "tenant": { - "tenants": "테넌트", - "management": "테넌트 관리", - "add": "테넌트 추가", - "admins": "Admins", - "manage-tenant-admins": "테넌트 관리자 관리", - "delete": "테넌트 삭제", - "add-tenant-text": "테넌트 추가", - "no-tenants-text": "테넌트가 없습니다.", - "tenant-details": "테넌트 상세정보", - "delete-tenant-title": "'{{tenantTitle}}' 테넌트를 삭제하시겠습니까?", - "delete-tenant-text": "테넌트와 관련된 모든 정보를 복구할 수 없으므로 주의하십시오.", - "delete-tenants-title": "{ count, plural, 1 {테넌트 1개} other {테넌트 #개} }를 삭제하시겠습니까?", - "delete-tenants-action-title": "{ count, plural, 1 {테넌트 1개} other {테넌트 #개} } 삭제", - "delete-tenants-text": "선택된 테넌트가 삭제되고 관련된 모든 정보를 복구할 수 없으므로 주의하십시오.", - "title": "타이틀", - "title-required": "타이틀을 입력하세요.", - "description": "설명", - "details": "Details", - "events": "Events", - "copyId": "Copy tenant Id", - "idCopiedMessage": "Tenant Id has been copied to clipboard", - "select-tenant": "Select tenant", - "no-tenants-matching": "No tenants matching '{{entity}}' were found.", - "tenant-required": "Tenant is required" - }, - "timeinterval": { - "seconds-interval": "{ seconds, plural, 1 {1 second} other {# seconds} }", - "minutes-interval": "{ minutes, plural, 1 {1 minute} other {# minutes} }", - "hours-interval": "{ hours, plural, 1 {1 hour} other {# hours} }", - "days-interval": "{ days, plural, 1 {1 day} other {# days} }", - "days": "Days", - "hours": "Hours", - "minutes": "Minutes", - "seconds": "Seconds", - "advanced": "고급" - }, - "timewindow": { - "days": "{ days, plural, 1 { day } other {# days } }", - "hours": "{ hours, plural, 0 { hour } 1 {1 hour } other {# hours } }", - "minutes": "{ minutes, plural, 0 { minute } 1 {1 minute } other {# minutes } }", - "seconds": "{ seconds, plural, 0 { second } 1 {1 second } other {# seconds } }", - "realtime": "Realtime", - "history": "History", - "last-prefix": "last", - "period": "from {{ startTime }} to {{ endTime }}", - "edit": "타임윈도우 편집", - "date-range": "날짜 범위", - "last": "Last", - "time-period": "기간" - }, - "user": { - "users": "사용자", - "customer-users": "커스터머 사용자", - "tenant-admins": "테넌트 관리자", - "sys-admin": "시스템 관리자", - "tenant-admin": "테넌트 관리자", - "customer": "커스터머", - "anonymous": "Anonymous", - "add": "사용자 추가", - "delete": "사용자 삭제", - "add-user-text": "새로운 사용자 추가", - "no-users-text": "사용자가 없습니다.", - "user-details": "사용자 상세정보", - "delete-user-title": "'{{userEmail}}' 사용자를 삭제하시겠습니까?", - "delete-user-text": "사용자와 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", - "delete-users-title": "{ count, plural, 1 {사용자 1명} other {사용자 #명} }을 삭제하시겠니까?", - "delete-users-action-title": "{ count, plural, 1 {사용자 1명} other {사용자 #명} } 삭제", - "delete-users-text": "선택된 사용자가 삭제된고 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", - "activation-email-sent-message": "활성화 이메일을 보냈습니다!", - "resend-activation": "활성화 재전송", - "email": "Email", - "email-required": "Email을 입력하세요.", - "first-name": "이름", - "last-name": "성", - "description": "설명", - "default-dashboard": "기본 대시보드", - "always-fullscreen": "항상 전체화면", - "select-user": "Select user", - "no-users-matching": "No users matching '{{entity}}' were found.", - "user-required": "User is required", - "activation-method": "Activation method", - "display-activation-link": "Display activation link", - "send-activation-mail": "Send activation mail", - "activation-link": "User activation link", - "activation-link-text": "In order to activate user use the following activation link :", - "copy-activation-link": "Copy activation link", - "activation-link-copied-message": "User activation link has been copied to clipboard", - "details": "Details" - }, - "value": { - "type": "Value type", - "string": "String", - "string-value": "String value", - "integer": "Integer", - "integer-value": "Integer value", - "invalid-integer-value": "Invalid integer value", - "double": "Double", - "double-value": "Double value", - "boolean": "Boolean", - "boolean-value": "Boolean value", - "false": "False", - "true": "True" - }, - "widget": { - "widget-library": "위젯 저장소", - "widget-bundle": "위젯 번들", - "select-widgets-bundle": "위젯 번들 선택", - "management": "위젯 관리", - "editor": "위젯 편집기", - "widget-type-not-found": "위젯 구성을 로드하는 중 문제가 발생했습니다.
    연결된 위젯 타입이 삭제되었습니다.", - "widget-type-load-error": "다음과 같은 오류로 인해 위젯이로드되지 않았습니다:", - "remove": "위젯 삭제", - "edit": "위젯 수정", - "remove-widget-title": "'{{widgetTitle}}' 위젯을 삭제하시겠습니까?", - "remove-widget-text": "위젯과 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", - "timeseries": "Time series", - "latest-values": "Latest values", - "rpc": "Control 위젯", - "static": "Static 위젯", - "select-widget-type": "위젯 타입 선택", - "missing-widget-title-error": "위젯 타이틀을 입력하세요!", - "widget-saved": "위젯이 저장되었습니다.", - "unable-to-save-widget-error": "위젯을 저장할 수 없습니다! 위젯에 오류가 있습니다!", - "save": "위젯 저장", - "saveAs": "다른 이름으로 위젯 저장", - "save-widget-type-as": "다른 이름으로 위젯 타입 저장", - "save-widget-type-as-text": "새로운 위젯 이름과 위젯 번들을 선택하세요.", - "toggle-fullscreen": "전체화면 전환", - "run": "위젯 실행", - "title": "위젯 타이틀", - "title-required": "위젯 타이틀을 입력하세요.", - "type": "위젯 타입", - "resources": "리소스", - "resource-url": "JavaScript/CSS URI", - "remove-resource": "리소스 삭제", - "add-resource": "리소스 추가", - "html": "HTML", - "tidy": "Tidy", - "css": "CSS", - "settings-schema": "스키마 설정", - "datakey-settings-schema": "데이터 키 설정 스키마", - "javascript": "Javascript", - "remove-widget-type-title": "'{{widgetName}}' 위젯 타입을 삭제하시겠습니까?", - "remove-widget-type-text": "위젯 타입과 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", - "remove-widget-type": "위젯 타입 삭제", - "add-widget-type": "새로운 위젯 타입 추가", - "widget-type-load-failed-error": "위젯 타입을 로드하지 못했습니다!", - "widget-template-load-failed-error": "위젯 템플릿을 로드하지 못했습니다!", - "add": "위젯 추가", - "undo": "위젯 변경사항 취소", - "export": "위젯 내보내기" - }, - "widget-action": { - "header-button": "Widget header button", - "open-dashboard-state": "Navigate to new dashboard state", - "update-dashboard-state": "Update current dashboard state", - "open-dashboard": "Navigate to other dashboard", - "custom": "Custom action", - "target-dashboard-state": "Target dashboard state", - "target-dashboard-state-required": "Target dashboard state is required", - "set-entity-from-widget": "Set entity from widget", - "target-dashboard": "Target dashboard", - "open-right-layout": "Open right dashboard layout (mobile view)" - }, - "widgets-bundle": { - "current": "현재 번들", - "widgets-bundles": "위젯 번들", - "add": "위젯 번들 추가", - "delete": "위젯 번들 삭제", - "title": "타이틀", - "title-required": "타이틀을 입력하세요.", - "add-widgets-bundle-text": "위젯 번들 추가", - "no-widgets-bundles-text": "위젯 번들이 없습니다.", - "empty": "위젯 번들이 비어있습니다.", - "details": "상세", - "widgets-bundle-details": "위젯 번들 상세정보", - "delete-widgets-bundle-title": "'{{widgetsBundleTitle}}' 위젯 번들을 삭제하시겠습니까?", - "delete-widgets-bundle-text": "위젯 번들과 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", - "delete-widgets-bundles-title": "{ count, plural, 1 {위젯 번들 1개} other {위젯 번들 #개} }를 삭제하시겠습니까?", - "delete-widgets-bundles-action-title": "{ count, plural, 1 {위젯 번들 1개} other {위젯 번들 #개} } 삭제", - "delete-widgets-bundles-text": "선택된 위젯 번들이 삭제되고 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", - "no-widgets-bundles-matching": "'{{widgetsBundle}}' 와(과) 일치하는 위젯 번들을 찾을 수 없습니다.", - "widgets-bundle-required": "위젯 번들을 입력하세요.", - "system": "시스템", - "import": "위젯 번들 가져오기", - "export": "위젯 번들 내보내기", - "export-failed-error": "위젯 번들을 내보내기 할 수 없습니다.: {{error}}", - "create-new-widgets-bundle": "새로운 위젯 번들 생성", - "widgets-bundle-file": "위젯 번들 파일", - "invalid-widgets-bundle-file-error": "위젯 번들을 가져오기 할 수 없습니다.: 잘못된 위젯 번들 데이터 구조입니다." - }, - "widget-config": { - "data": "데이터", - "settings": "설정", - "advanced": "고급", - "title": "타이틀", - "general-settings": "일반 설정", - "display-title": "타이틀 표시", - "drop-shadow": "그림자", - "enable-fullscreen": "전체화면 사용 ", - "background-color": "배경 색", - "text-color": "글자 색", - "padding": "패딩", - "title-style": "타이틀 스타일", - "mobile-mode-settings": "모바일 모드 설정", - "order": "순서", - "height": "높이", - "units": "값 옆에 표시할 특수 기호", - "decimals": "소수점 이하 자릿수", - "timewindow": "타임윈도우", - "use-dashboard-timewindow": "대시보드 타임윈도우", - "display-legend": "범례 표시", - "datasources": "데이터소스", - "datasource-type": "유형", - "datasource-parameters": "파라미터", - "remove-datasource": "데이터소스 삭제", - "add-datasource": "데이터소스 추가", - "target-device": "대상 디바이스" - }, - "widget-type": { - "import": "위젯 타입 가져오기", - "export": "위젯 타입 내보내기", - "export-failed-error": "위젯 타입을 내보내기 할 수 없습니다.: {{error}}", - "create-new-widget-type": "새로운 위젯 타입 생성", - "widget-type-file": "위젯 타입 파일", - "invalid-widget-type-file-error": "위젯 타입을 가져오기 할 수 없습니다.: 잘못된 위젯 타입 데이터 구조입니다." - }, - "widgets": { - "date-range-navigator": { - "localizationMap": { - "Sun": "일", - "Mon": "월", - "Tue": "화", - "Wed": "수", - "Thu": "목", - "Fri": "금", - "Sat": "토", - "Jan": "1월", - "Feb": "2월", - "Mar": "3월", - "Apr": "4월", - "May": "5월", - "Jun": "6월", - "Jul": "7월", - "Aug": "8월", - "Sep": "9월", - "Oct": "10월", - "Nov": "11월", - "Dec": "12월", - "January": "일월", - "February": "이월", - "March": "행진", - "April": "4 월", - "June": "유월", - "July": "칠월", - "August": "팔월", - "September": "구월", - "October": "십월", - "November": "십일월", - "December": "12 월", - "Custom Date Range": "맞춤 기간", - "Date Range Template": "기간 템플릿", - "Today": "오늘", - "Yesterday": "어제", - "This Week": "이번 주", - "Last Week": "지난주", - "This Month": "이번 달", - "Last Month": "지난 달", - "Year": "년", - "This Year": "올해", - "Last Year": "작년", - "Date picker": "날짜 선택기", - "Hour": "시간", - "Day": "일", - "Week": "주", - "2 weeks": "이주", - "Month": "달", - "3 months": "3 개월", - "6 months": "6 개월", - "Custom interval": "사용자 지정 간격", - "Interval": "간격", - "Step size": "단계 크기", - "Ok": "Ok" - } - } - }, - "icon": { - "icon": "Icon", - "select-icon": "Select icon", - "material-icons": "Material icons", - "show-all": "Show all icons" - }, - "custom": { - "widget-action": { - "action-cell-button": "Action cell button", - "row-click": "On row click", - "polygon-click": "On polygon click", - "marker-click": "On marker click", - "tooltip-tag-action": "Tooltip tag action" - } - }, - "language": { - "language": "언어" - } -} diff --git a/ui/src/app/locale/locale.constant-lv_LV.json b/ui/src/app/locale/locale.constant-lv_LV.json deleted file mode 100644 index 7fafb97af2..0000000000 --- a/ui/src/app/locale/locale.constant-lv_LV.json +++ /dev/null @@ -1,1682 +0,0 @@ -{ - "access": { - "unauthorized": "Neatļauta", - "unauthorized-access": "Neatļauta piekļuve", - "unauthorized-access-text": "Lai piekļūtu šim resursam, jums jāpierakstās!", - "access-forbidden": "Piekļuve aizliegta", - "access-forbidden-text": "Jums nav piekļuves tiesību!
    Mēģiniet pierakstīties ar citu lietotājvārdu.", - "refresh-token-expired": "Sesija ir beigusies", - "refresh-token-failed": "Nevar atjaunot sesiju" - }, - "action": { - "activate": "Aktivizēt", - "suspend": "Apturēt", - "save": "Saglabāt", - "saveAs": "Saglabāt kā", - "cancel": "Atcelt", - "ok": "OK", - "delete": "Dzēst", - "add": "Pievienot", - "yes": "Jā", - "no": "Nē", - "update": "Atjaunināt", - "remove": "Noņemt", - "search": "Meklēt", - "clear-search": "Notīrīt meklēšanu", - "assign": "Piešķirt", - "unassign": "Noņemt", - "share": "Dalīties", - "make-private": "Padarīt privātu", - "apply": "Pielietot", - "apply-changes": "Pielietot izmaiņas", - "edit-mode": "Rediģēšanas režīms", - "enter-edit-mode": "Ievadiet rediģēšanas režīmu", - "decline-changes": "Noraidīt izmaiņas", - "close": "Aizvērt", - "back": "Atpakaļ", - "run": "Uz priekšu", - "sign-in": "Pierakstīties!", - "edit": "Rediģēt", - "view": "Skatīt", - "create": "Radīt", - "drag": "Velciet", - "refresh": "Atjaunot", - "undo": "Atsaukt", - "copy": "Kopēt", - "paste": "Ielīmēt", - "copy-reference": "Kopija atsauce", - "paste-reference": "Ielīmēt atsauce", - "import": "Importēt", - "export": "Eksportēt", - "share-via": "Dalīties caur {{pakalpojuma sniedzējs}}", - "continue": "Turpināt" - }, - "aggregation": { - "aggregation": "Sakopojums", - "function": "Datu sakopojuma funkcija", - "limit": "Limits", - "group-interval": "Grupas intervāls", - "min": "Min", - "max": "Max", - "avg": "Vidējais", - "sum": "Sum", - "count": "Skaits", - "none": "Neviena" - }, - "admin": { - "general": "Vispārīgi", - "general-settings": "Vispārīgie iestatījumi", - "outgoing-mail": "Pasta serveris", - "outgoing-mail-settings": "Izejošā pasta servera iestatījumi", - "system-settings": "Sistēmas iestatījumi", - "test-mail-sent": "Testa pasts sekmīgi nosūtīts!", - "base-url": "pamata URL", - "base-url-required": "Pamata URL ir nepieciešams.", - "mail-from": "Pasts no", - "mail-from-required": "Pasts no ir nepieciešams.", - "smtp-protocol": "SMTP protokols", - "smtp-host": "SMTP saimnieks", - "smtp-host-required": "SMTP saimnieks ir nepieciešams.", - "smtp-port": "SMTP ports", - "smtp-port-required": "Jums vajag nodrošināt SMTP portu.", - "smtp-port-invalid": "Tas neizskatās pēc atļauta SMTP porta.", - "timeout-msec": "Pārtraukums (msec)", - "timeout-required": "Pārtraukums ir nepieciešams.", - "timeout-invalid": "Tas neizskatās pēc atļauta pārtraukuma.", - "enable-tls": "Iespējot TLS", - "send-test-mail": "Nosūtīt testa pastu" - }, - "alarm": { - "alarm": "Trauksme", - "alarms": "Trauksmes", - "select-alarm": "Atlasīt trauksmi", - "no-alarms-matching": "Nav atbilstošu trauksmju '{{entity}}' .", - "alarm-required": "Trauksme ir nepieciešama", - "alarm-status": "Trauksmes statuss", - "search-status": { - "ANY": "Jebkura", - "ACTIVE": "Aktīvs", - "CLEARED": "Dzēsts", - "ACK": "Apstiprināts", - "UNACK": "Neapstiprināts" - }, - "display-status": { - "ACTIVE_UNACK": "Aktīvs Neapstiprināts", - "ACTIVE_ACK": "Aktīvs Apstiprināts", - "CLEARED_UNACK": "Dzēsts Neapstiprināts", - "CLEARED_ACK": "Dzēsts Apstiprināts" - }, - "no-alarms-prompt": "Trauksmes nav atrastas", - "created-time": "Izveidošanas laiks", - "type": "Tips", - "severity": "Smaguma pakāpe", - "originator": "Iniciātors", - "originator-type": "Iniciātora tips", - "details": "Detaļas", - "status": "Statuss", - "alarm-details": "Trauksmes detaļas", - "start-time": "Sākuma laiks", - "end-time": "Beigu laiks", - "ack-time": "Apstiprinājuma laiks", - "clear-time": "Notīrīšanas laiks", - "severity-critical": "Smaguma pakāpe - kritiska", - "severity-major": "Būtiska", - "severity-minor": "Minora", - "severity-warning": "Brīdinājums", - "severity-indeterminate": "Nenoteikts", - "acknowledge": "Apstiprināt", - "clear": "Notīrīt", - "search": "Meklēt trauksmes", - "selected-alarms": "{ count, plural, 1 {1 alarm} other {# trauksmes} } selected", - "no-data": "Nav datu ko attēlot", - "polling-interval": "Trauksmju pārbaužu intervāls (sec)", - "polling-interval-required": "Trauksmju pārbaužu intervāls ir nepieciešams.", - "min-polling-interval-message": "Vismaz 1 sekundes pārbaužu intervāls ir atļauts.", - "aknowledge-alarms-title": "Apstiprināt { count, plural, 1 {1 alarm} other {# trauksmes} }", - "aknowledge-alarms-text": "Vai Jūs tiešām vēlaties apstirpināt { count, plural, 1 {1 alarm} other {# trauksmes} }?", - "aknowledge-alarm-title": "Apstiprināt trauksmi", - "aknowledge-alarm-text": "Vai Jūs tiešām vēlaties apstiprināt trauksmi?", - "clear-alarms-title": "Dzēst { count, plural, 1 {1 alarm} other {# trauksmes} }", - "clear-alarms-text": "Vai Jūs tiešām vēlaties dzēst { count, plural, 1 {1 alarm} other {# trauksmes} }?", - "clear-alarm-title": "Dzēst trauksmi", - "clear-alarm-text": "Vai Jūs tiešām vēlaties dzēst trauksmi?", - "alarm-status-filter": "Trauksmes statusa filtrs" - }, - "alias": { - "add": "Pievienot segvārdu", - "edit": "Rediģēt segvārdu", - "name": "Segvārda nosaukums", - "name-required": "Segvārda nosaukums vārds ir nepieciešams", - "duplicate-alias": "Segvārds ar tādu pašu nosaukumu jau eksistē.", - "filter-type-single-entity": "Viena vienība", - "filter-type-entity-list": "Vienību saraksts", - "filter-type-entity-name": "Vienības vārds", - "filter-type-state-entity": "Vienība no paneļa stāvokļa", - "filter-type-state-entity-description": "Vienība ņemta no paneļa stāvokļa parametriem", - "filter-type-asset-type": "Aktīvu tips", - "filter-type-asset-type-description": "Aktīvu tipi '{{assetType}}'", - "filter-type-asset-type-and-name-description": "Aktīvu tips '{{assetType}}' un ar vārdu sākot ar '{{prefix}}'", - "filter-type-device-type": "Iekārtas tips", - "filter-type-device-type-description": "Iekārtas tipi '{{deviceType}}'", - "filter-type-device-type-and-name-description": "Iekārtas tipi '{{deviceType}}' un ar vārdu sākot ar '{{prefix}}'", - "filter-type-entity-view-type": "Vienības skata tips", - "filter-type-entity-view-type-description": "Vienības skats tipam '{{entityView}}'", - "filter-type-entity-view-type-and-name-description": "Vienības skats tipam '{{entityView}}' un ar vārdu sākot ar '{{prefix}}'", - "filter-type-relations-query": "Attiecību vaicājums", - "filter-type-relations-query-description": "{{entities}} kam ir {{relationType}} attiecība {{direction}} {{rootEntity}}", - "filter-type-asset-search-query": "Aktīvu meklēšanas vaicājums", - "filter-type-asset-search-query-description": "Aktīvi ar tipu {{assetTypes}} kam ir {{relationType}} attiecība {{direction}} {{rootEntity}}", - "filter-type-device-search-query": "Iekārtu meklēšanas vaicājums", - "filter-type-device-search-query-description": "Iekārtas ar tipu {{deviceTypes}} kam ir {{relationType}} attiecība {{direction}} {{rootEntity}}", - "filter-type-entity-view-search-query": "Vienības skata meklēšanas vaicājuma", - "filter-type-entity-view-search-query-description": "Vienību skats ar tipu {{entityViewTypes}} kam ir {{relationType}} attiecība {{direction}} {{rootEntity}}", - "entity-filter": "Vienību filtrs", - "resolve-multiple": "Atrisināt kā daudzas vienības", - "filter-type": "Filtra tips", - "filter-type-required": "Filtra tips ir nepieciešams.", - "entity-filter-no-entity-matched": "Nav atrastas vienības kam atbilst filtru iestatījumi.", - "no-entity-filter-specified": "Nav vienību filtrs specificēts", - "root-state-entity": "Lieto paneļa statusa vienību kā sakni ", - "root-entity": "Saknes vienības", - "state-entity-parameter-name": "Statusa vienības parametra vārds", - "default-state-entity": "Noklusējuma statusa vienība", - "default-entity-parameter-name": "Pēc noklusējuma", - "max-relation-level": "Maksimālais attiecību līmenis", - "unlimited-level": "Nelimitēts līmenis", - "state-entity": "Paneļa statusa vienība", - "all-entities": "Visas vienības", - "any-relation": "Jebkura" - }, - "asset": { - "asset": "Aktīvs", - "assets": "Aktīvi", - "management": "Aktīvu pārvaldība", - "view-assets": "Skatīt aktīvus", - "add": "Pievienot aktīvu", - "assign-to-customer": "Pieškirt klientam", - "assign-asset-to-customer": "Piešķirt aktīvu klientam", - "assign-asset-to-customer-text": "Lūdzu izvēlēties aktīvu lai pieškirtu klientam", - "no-assets-text": "Aktīvi nav atrasti", - "assign-to-customer-text": "Lūdzu izvēlēties klientu lai pieškirtu aktīvu", - "public": "Publisks", - "assignedToCustomer": "Pieškirts klientam", - "make-public": "Veidot aktīvu publisku", - "make-private": "Veidot aktīvu privātu", - "unassign-from-customer": "Noņemt klientam", - "delete": "Dzēst aktīvu", - "asset-public": "Aktīvs ir publisks", - "asset-type": "Aktīva tips", - "asset-type-required": "Aktīva tips ir nepieciešams.", - "select-asset-type": "Izvēlies aktīva tipu", - "enter-asset-type": "Ievadi aktīva tipu", - "any-asset": "Jebkurš aktīvs", - "no-asset-types-matching": "Nav atbilstošs aktīvu tips '{{entitySubtype}}' atrasts.", - "asset-type-list-empty": "Nav aktīvu tipi izvēlēti.", - "asset-types": "Aktīvu tipi", - "name": "Vārds", - "name-required": "Vārds ir nepieciešams.", - "description": "Apraksts", - "type": "Tips", - "type-required": "Tips ir nepieciešams.", - "details": "Detaļas", - "events": "Notikumi", - "add-asset-text": "Pievieno jaunu aktīvu", - "asset-details": "Aktīvu detaļas", - "assign-assets": "Piešķirt aktīvus", - "assign-assets-text": "Piešķirt { count, plural, 1 {1 asset} other {# aktīvus} } klientam", - "delete-assets": "Dzēst aktīvus", - "unassign-assets": "Noņemt aktīvus", - "unassign-assets-action-title": "Noņemt { count, plural, 1 {1 asset} other {# aktīvus} } no klienta", - "assign-new-asset": "Pieškirt jaunu aktīvu", - "delete-asset-title": "Vai esat pārliecināts,ka vēlaties dzēst aktīvu '{{assetName}}'?", - "delete-asset-text": "Esiet uzmanīgs, pēc apstiprināšanas aktīvs un saistītie dati nebūs atjaunojami.", - "delete-assets-title": "Vai esat pārliecināts ka vēlaties dzēst { count, plural, 1 {1 asset} other {# aktīvus} }?", - "delete-assets-action-title": "Dzēst { count, plural, 1 {1 asset} citu {# aktīvus} }", - "delete-assets-text": "Esiet uzmanīgs, pēc apstiprinājuma visi izvēlētie aktīvi tiks dzēsti un saistītā informācija nebūs atjaunojama.", - "make-public-asset-title": "Vai esat pārliecināts ka vēlaties aktīvu '{{assetName}}' veidot publisku?", - "make-public-asset-text": "Pēc apstiprinājuma aktīvs un tā dati tiks publiski pieejami.", - "make-private-asset-title": "Vai esat pārliecināts ka vēlaties aktīvu '{{assetName}}' veidot privātu?", - "make-private-asset-text": "Pēc apstiprinājums aktīvs un tā saistītie dati būs privāti un nebūs pieejami citiem.", - "unassign-asset-title": "Vai esat pārliecināts ka vēlaties noņemt aktīvu '{{assetName}}'?", - "unassign-asset-text": "Pēc apstiprināšanas aktīvs tiks noņemts un nebūs pieejams klientiem.", - "unassign-asset": "Noņemt aktīvu", - "unassign-assets-title": "Vai esat pārliecināts ka vēlaties noņemt { count, plural, 1 {1 asset} citu {# aktīvus} }?", - "unassign-assets-text": "Pēc apstiprināšanas visi izvēlētie aktīvi būs noņemti un nebūs pieejami klientiem.", - "copyId": "Kopēt aktīva Id", - "idCopiedMessage": "Aktīva Id ir kopēts uz starpliktuvi", - "select-asset": "Atlasīt aktīvu", - "no-assets-matching": "Nav atbilstošs aktīvs '{{entity}}' atrasts.", - "asset-required": "Aktīvs ir nepieciešams", - "name-starts-with": "Aktīva vārds sākas ar", - "import": "Importēt aktīvus", - "asset-file": "Aktīvu fails" - }, - "attribute": { - "attributes": "Attribūti", - "latest-telemetry": "Jaunākā telemetrija", - "attributes-scope": "Vienības atribūtu darbības joma", - "scope-latest-telemetry": "Jaunākā telemetrija", - "scope-client": "Klientu atribūti", - "scope-server": "Servera atribūti", - "scope-shared": "Dalītie atribūti", - "add": "Pievieno atribūtu", - "key": "Atslēga", - "last-update-time": "Pēdēja atjaunojuma laiks", - "key-required": "Atribūta atslēga ir nepieciešama.", - "value": "Vērtība", - "value-required": "Atribūta vērtība ir nepieciešama.", - "delete-attributes-title": "Vai esat pārliecināts ka vēlaties dzēst { count, plural, 1 {1 attribute} other {# attribūtus} }?", - "delete-attributes-text": "Esiet uzmanīgs, pēc apstiprinājuma visi izvēlētie atribūti tiks dzēsti.", - "delete-attributes": "Dzēst atribūtu", - "enter-attribute-value": "Ievadiet atribūta vērtību", - "show-on-widget": "Parādīt logrīkā", - "widget-mode": "Logrīka režīms", - "next-widget": "Nākamais logrīks", - "prev-widget": "Iepriekšējais logrīks", - "add-to-dashboard": "Pievienot panelim", - "add-widget-to-dashboard": "Pievienot logrīku panelim", - "selected-attributes": "{ count, plural, 1 {1 attribute} other {# atribūtus} } izvēlētajam", - "selected-telemetry": "{ count, plural, 1 {1 telemetry unit} other {# telemetrijas vienības} } izvēlētas" - }, - "audit-log": { - "audit": "Audits", - "audit-logs": "Audita logs", - "timestamp": "Laika zīmogs", - "entity-type": "Vienības tips", - "entity-name": "Vienības vārds", - "user": "Lietotājs", - "type": "Tips", - "status": "Statuss", - "details": "Detaļas", - "type-added": "Pievienots", - "type-deleted": "Dzēsts", - "type-updated": "Atjaunots", - "type-attributes-updated": "Atribūti atjaunoti", - "type-attributes-deleted": "Atribūti dzēsti", - "type-rpc-call": "RPC izsaukumi", - "type-credentials-updated": "Akreditācijas dati atjaunoti", - "type-assigned-to-customer": "Pieškirts klientam", - "type-unassigned-from-customer": "Noņemts no klienta", - "type-activated": "Aktivizēts", - "type-suspended": "Apturēts", - "type-credentials-read": "Akreditācijas datu nolasījums", - "type-attributes-read": "Atribūtu nolasījums", - "type-relation-add-or-update": "Attiecība atjaunota", - "type-relation-delete": "Atiecība dzēsta", - "type-relations-delete": "Visas attiecības dzēstas", - "type-alarm-ack": "Apstiprinājums", - "type-alarm-clear": "Notīrīts", - "status-success": "Sekmīgi", - "status-failure": "Neveiksme", - "audit-log-details": "Audita loga detaļas", - "no-audit-logs-prompt": "Nav logu atrastu", - "action-data": "Aktivitāšu dati", - "failure-details": "Neveiksmju detaļas", - "search": "Meklēt audita logus", - "clear-search": "Notīrīt meklēšanu" - }, - "confirm-on-exit": { - "message": "Jums ir nesaglabātas izmaiņas. Vai tiešām vēlaties pamest šo lapu?", - "html-message": "Jums ir nesaglabātas izmaiņas.
    Vai tiešām vēlaties pamest šo lapu?", - "title": "Nesaglabātas izmaiņas" - }, - "contact": { - "country": "Valsts", - "city": "Pilsēta", - "state": "Štats/Province", - "postal-code": "Zip / Pasta kods", - "postal-code-invalid": "Invalīds Zip / Pasta koda formāts.", - "address": "Adrese", - "address2": "Adrese 2", - "phone": "Telefons", - "email": "Email", - "no-address": "Nav adreses" - }, - "common": { - "username": "Lietotājvārdse", - "password": "Parole", - "enter-username": "Ievadiet lietotājvārdu", - "enter-password": "Ievadiet paroli", - "enter-search": "Ievadiet meklēt" - }, - "content-type": { - "json": "Json", - "text": "Teksts", - "binary": "Bināri (Base64)" - }, - "customer": { - "customer": "Klients", - "customers": "Klienti", - "management": "Klientu pārvaldība", - "dashboard": "Klientu panelis", - "dashboards": "Klientu paneļi", - "devices": "Klienta iekārtas", - "entity-views": "Klienta vienību skati", - "assets": "Klienta aktīvi", - "public-dashboards": "Publiskie paneļi", - "public-devices": "Publiskās iekārtas", - "public-assets": "Publiskie aktīvi", - "public-entity-views": "Publisko vienību skati", - "add": "Pievienot klientu", - "delete": "Dzēst klientu", - "manage-customer-users": "Pārvaldīt klienta lietotājus", - "manage-customer-devices": "Pārvaldīt klienta iekārtas", - "manage-customer-dashboards": "Pārvaldīt klienta paneļus", - "manage-public-devices": "Pārvaldīt publiskās iekārtas", - "manage-public-dashboards": "Pārvaldīt publiskos paneļus", - "manage-customer-assets": "Pārvaldīt klienta aktīvus", - "manage-public-assets": "Pārvaldīt publiskos aktīvus", - "add-customer-text": "Pievienot jaunu klientu", - "no-customers-text": "Nav klienti atrasti", - "customer-details": "Klienta detaļas", - "delete-customer-title": "Vai esat pārliecināts, ka vēlaties dzēst klientu '{{customerTitle}}'?", - "delete-customer-text": "Esiet uzmanīgs, pēc apstiprinājuma klients un tā saistītie dati nebūs atjaunojami.", - "delete-customers-title": "Vai esat pārliecināts, ka vēlaties dzēst { count, plural, 1 {1 customer} other {# klientus} }?", - "delete-customers-action-title": "Dzēst { count, plural, 1 {1 customer} other {# klientus} }", - "delete-customers-text": "Esiet uzmanīgs, pēc apstiprinājuma visi izvēlētie klienti tisk dzēsti un to saistītie dati nebūs atjaunojami.", - "manage-users": "Pārvaldīt lietotājus", - "manage-assets": "Pārvaldīt aktīvus", - "manage-devices": "Pārvaldīt iekārtas", - "manage-dashboards": "Pārvaldīt paneļus", - "title": "Virsraksts", - "title-required": "Virsraksts ir nepieciešams.", - "description": "Apraksts", - "details": "Detaļas", - "events": "Notikumi", - "copyId": "Kopēt klienta Id", - "idCopiedMessage": "Klienta Id ir kopēts uz starpliktuvi", - "select-customer": "Atlasīt klientu", - "no-customers-matching": "Nav atbilstoši klienti '{{entity}}' atrasti.", - "customer-required": "Klients ir nepieciešams", - "select-default-customer": "Atlasīt pamata klientu", - "default-customer": "Pamata klients", - "default-customer-required": "Pamata klients ir nepieciešams lai atkļūdotu paneli īrnieka līmenī" - }, - "datetime": { - "date-from": "Datums no", - "time-from": "Laiks no", - "date-to": "Datums līdz", - "time-to": "Laiks līdz" - }, - "dashboard": { - "dashboard": "Panelis", - "dashboards": "Paneļi", - "management": "Paneļu pārvaldība", - "view-dashboards": "Skatīt paneļus", - "add": "Pievienot paneļus", - "assign-dashboard-to-customer": "Piešķirt paneļus klientam", - "assign-dashboard-to-customer-text": "Lūdzu izvēlēties paneļus lai piešķirtu tos klientam", - "assign-to-customer-text": "Lūdzu izvēlēties klientu, kuram piešķirt paneļus", - "assign-to-customer": "Piešķirt klientam", - "unassign-from-customer": "Noņemt no klienta", - "make-public": "Veidot paneli publisku", - "make-private": "Veidot paneli privātu", - "manage-assigned-customers": "Pārvaldīt piešķirtos klientus", - "assigned-customers": "Piešķirtie klienti", - "assign-to-customers": "Piešķirt paneļus klientiem", - "assign-to-customers-text": "Lūdzu atlasīt klientus lai pieškirtu paneļus", - "unassign-from-customers": "Noņemt no klientiem paneļus", - "unassign-from-customers-text": "Lūdzu atlasīt klientus kuriem noņemt paneļus", - "no-dashboards-text": "Nav paneļi atrasti", - "no-widgets": "Nav logrīki konfigurēti", - "add-widget": "Pievienot jaunu logrīku", - "title": "Virsraksts", - "select-widget-title": "Atlasīt logrīku", - "select-widget-subtitle": "Pieejamo logrīku tipu saraksts", - "delete": "Dzēst paneli", - "title-required": "Virsraksts ir nepieciešams.", - "description": "Apraksts", - "details": "Detaļas", - "dashboard-details": "Paneļa detaļas", - "add-dashboard-text": "Pievienot jaunu paneli", - "assign-dashboards": "Pieškirt paneļus", - "assign-new-dashboard": "Pieškirt jaunu paneli", - "assign-dashboards-text": "Pieškirt { count, plural, 1 {1 dashboard} other {# paneļus} } klientiem", - "unassign-dashboards-action-text": "Noņemt { count, plural, 1 {1 dashboard} other {# paneļus} } no klientiem", - "delete-dashboards": "Dzēst paneļus", - "unassign-dashboards": "Noņemt paneļus", - "unassign-dashboards-action-title": "Noņemt { count, plural, 1 {1 dashboard} other {# paneļus} } no klienta", - "delete-dashboard-title": "Vai esat pārliecināts ka vēlaties dzēst paneli '{{dashboardTitle}}'?", - "delete-dashboard-text": "Esiet uzmanīgs, pēc apstiprinājuma panelis un visi tā saistītie dati nebūs atjaunojami.", - "delete-dashboards-title": "Vai esat pārliecināts ka vēlaties dzēst { count, plural, 1 {1 dashboard} other {# paneļus} }?", - "delete-dashboards-action-title": "Dzēst { count, plural, 1 {1 dashboard} other {# paneļus} }", - "delete-dashboards-text": "Esiet uzmanīgs, pēc apstiprinājuma visi izvēlētie paneļi būs noņemti un visi saistitie dati nebūs atjaunojami.", - "unassign-dashboard-title": "Vai esat pārliecināts, ka vēlaties noņemt paneli '{{dashboardTitle}}'?", - "unassign-dashboard-text": "Pēc apstiprinājuma panelis tiks noņemts un nebūs pieejams klientam.", - "unassign-dashboard": "Noņemt paneli", - "unassign-dashboards-title": "Vai esat pārliecināts ka vēlaties noņemt { count, plural, 1 {1 dashboard} other {# paneļus} }?", - "unassign-dashboards-text": "Pēc apstiprinājuma visi izvēlētie paneļi būs noņemti un nebūs pieejami klientam.", - "public-dashboard-title": "Panelis tagad ir publisks", - "public-dashboard-text": "Jūsu panelis {{dashboardTitle}} tagad ir publisks un pieejams pēc saites link:", - "public-dashboard-notice": "Note: Neaizmirstie veidot attiecīgās iekārtas publiski pieejamas lai piekļutu to datiem.", - "make-private-dashboard-title": "Vai esat pārliecināts, ka vēlaties veidot paneli '{{dashboardTitle}}' privātu?", - "make-private-dashboard-text": "Pēc apstiprinājuma panelis būs privāts un nebūs pieejams citiem.", - "make-private-dashboard": "Veidot paneli privātu", - "socialshare-text": "'{{dashboardTitle}}' atbalsts no TeT", - "socialshare-title": "'{{dashboardTitle}}' atbalsts no TeT", - "select-dashboard": "Atlasīt paneli", - "no-dashboards-matching": "Nav atbilstoši paneļi '{{entity}}' atrasti.", - "dashboard-required": "Penelis ir nepieciešams.", - "select-existing": "Atlasīt paneli", - "create-new": "Radīt jaunu paneli", - "new-dashboard-title": "Jauns paneļa Virsraksts", - "open-dashboard": "Atvērt paneli", - "set-background": "Iestatīt fonu", - "background-color": "Fona krāsa", - "background-image": "Fona attēls", - "background-size-mode": "Fona lieluma mode", - "no-image": "Nav izvēlēts attēls", - "drop-image": "Nomest attēlu vai noklikšķiniet lai atlasītu failu augšupielādei.", - "settings": "Iestatījumi", - "columns-count": "Kolonu skaitīšana", - "columns-count-required": "Kolonu skaitīšana ir nepieciešams.", - "min-columns-count-message": "Tikai minimums 10 kolonu skaitīšana ir atļauta.", - "max-columns-count-message": "Tikai maksimums 100 kolonu skaitīšana ir atļauta.", - "widgets-margins": "Robeža starp logrīkiem", - "horizontal-margin": "Horizontālā robeža", - "horizontal-margin-required": "Horizontālās robežas vērtība ir nepieciešama.", - "min-horizontal-margin-message": "Tikai 0 ir atļauta kā minimālā horizontālās robežas vērtība.", - "max-horizontal-margin-message": "Tikai 50 ir atļauta kā maksimālā horizontālās robežas vērtība.", - "vertical-margin": "Vertikālā robeža", - "vertical-margin-required": "Vertikālās robežas vērtība ir nepieciešama.", - "min-vertical-margin-message": "Tikai 0 ir atļauta kā minimālā vertikālās robežas vērtība.", - "max-vertical-margin-message": "Tikai 50 ir atļauta kā maksimālā vertikālās robežas vērtība.", - "autofill-height": "Automātiskās aizpildīšanas izkārtojuma augstums", - "mobile-layout": "Mobilā izkārtojuma iestatījumi", - "mobile-row-height": "Mobilās rindas augstums, px", - "mobile-row-height-required": "Mobile row height value is required.", - "min-mobile-row-height-message": "Tikai 5 pikseļi ir atļauti kā minimālās mobilās rindas augstuma vērtības.", - "max-mobile-row-height-message": "Tikai 200 pikseļi ir atļauti kā maksimālās mobilās rindas augstuma vērtības.", - "display-title": "Parādīt paneļa virsrakstu", - "toolbar-always-open": "Turēt rīkjoslu atvērtu", - "title-color": "Virsraksta krāsa", - "display-dashboards-selection": "Parādīt paneļa izvēli", - "display-entities-selection": "Parādīt vienību izvēli", - "display-dashboard-timewindow": "Parādīt laika logu", - "display-dashboard-export": "Parādīt eksportu", - "import": "Importēt paneli", - "export": "Eksportēt panelis", - "export-failed-error": "Nav iespējams eksportēt paneli: {{error}}", - "create-new-dashboard": "Radīt jaunu paneli", - "dashboard-file": "Paneļa fails", - "invalid-dashboard-file-error": "Nav iespējams importēt paneli: Invalīda paneļa datu struktūra.", - "dashboard-import-missing-aliases-title": "Jānokonfigurē segvārdi kas lietoti importētajā panelī", - "create-new-widget": "Radīt jaunu logrīku", - "import-widget": "Importēt logrīku", - "widget-file": "Logrīka fails", - "invalid-widget-file-error": "Nav iespējams importēt logrīku: Invalīda logrīka datu struktūra.", - "widget-import-missing-aliases-title": "Jānokonfigurē segvārdi kas lietoti importētajā logrīkā", - "open-toolbar": "Atvērt paneļa rīkjoslu", - "close-toolbar": "Aizvērt rīkjoslu", - "configuration-error": "Konfigurācijas kļūda", - "alias-resolution-error-title": "Paneļa segvārdu konfigurācijas kļūda", - "invalid-aliases-config": "Nav iespējams atrast nevienu iekārtu kam atbilst kāds no segvārdu filtriem.
    Lūdzu sazinieties ar savu administrātoru.", - "select-devices": "Atlasīt iekārtas", - "assignedToCustomer": "Pišķirtas klientam", - "assignedToCustomers": "Piešķirtas klientiem", - "public": "Publisks", - "public-link": "Publiska saite", - "copy-public-link": "Kopēt publisku saiti", - "public-link-copied-message": "Paneļa publiskā saite ir kopēta starpliktuvē", - "manage-states": "Pārvaldīt paneļa stāvokļus", - "states": "Paneļa stāvokļi", - "search-states": "Meklēt paneļa stāvokļus", - "selected-states": "{ count, plural, 1 {1 dashboard state} other {# paneļu statusus} } atlasītos", - "edit-state": "Rediģēt paneļa stāvokli", - "delete-state": "Dzēst paneļa stāvokli", - "add-state": "Pievienot paneļa stāvokli", - "state": "Paneļa stāvoklis", - "state-name": "Nosaukums", - "state-name-required": "Paneļa stāvokļa nosaukums ir nepieciešams.", - "state-id": "Stāvokļa Id", - "state-id-required": "Paneļa stāvokļa Id ir nepieciešams.", - "state-id-exists": "Paneļa stāvoklis ar šādu Id jau eksistē.", - "is-root-state": "Saknes stāvoklis", - "delete-state-title": "Dzēst paneļa stāvokli", - "delete-state-text": "Vai esat pārliecināts ka vēlaties dzēst paneļa stāvokli ar nosaukumu '{{stateName}}'?", - "show-details": "Rādīt detaļas", - "hide-details": "Noslēpt detaļas", - "select-state": "Atlasīt mērķa stāvokli", - "state-controller": "Stavokļa kontrolieris" - }, - "datakey": { - "settings": "Iestatījumi", - "advanced": "Pieredzējis lietotājs", - "label": "Etiķete", - "color": "Krāsa", - "units": "Speciāls simbols, ko parādīt pēc vērtība", - "decimals": "Ciparu skaits aiz komata", - "data-generation-func": "Datu ģenerācijas funkcija", - "use-data-post-processing-func": "Lietot datu pēcapstrādes funkciju", - "configuration": "Datu atslēgu konfigurācija", - "timeseries": "Laika periodi", - "attributes": "Atribūti", - "alarm": "Trauksme", - "timeseries-required": "Vienības laika periodi ir nepieciešami.", - "timeseries-or-attributes-required": "Vienības laika periodi/atribūti ir nepieciešami.", - "maximum-timeseries-or-attributes": "Maksimums { count, plural, 1 {1 timeseries/attribute is allowed.} other {# laika sērijas/atribūti ir atļauti} }", - "alarm-fields-required": "Trauksmes lauki ir nepieciešami.", - "function-types": "Funkciju tipi", - "function-types-required": "Funkciju tipi ir nepieciešami.", - "maximum-function-types": "Maksimums { count, plural, 1 {1 function type is allowed.} other {# funkciju tipi ir atļauti} }", - "time-description": "Laika zīmogs patreizējai vērtībai;", - "value-description": "patreizējā vērtība;", - "prev-value-description": "rezultāts no iepriekšējā funkciju pieprasījuma;", - "time-prev-description": "Laika zīmogs no iepriekšējās vērtības;", - "prev-orig-value-description": "Oriģinālā iepriekšējā vērtība;" - }, - "datasource": { - "type": "Datu avota tips", - "name": "Nosaukums", - "add-datasource-prompt": "Lūdzu pievienot datu avotu" - }, - "details": { - "edit-mode": "Rediģēšanas mode", - "toggle-edit-mode": "Pārslēgt rediģēšanas modi" - }, - "device": { - "device": "Iekārta", - "device-required": "Iekārta ir nepieciešama.", - "devices": "Iekārtas", - "management": "Iekārtu pārvaldība", - "view-devices": "Skatīt iekārtas", - "device-alias": "Iekārtu segvārdi", - "aliases": "Iekārtas segvārdi", - "no-alias-matching": "'{{alias}}' nav atrasti.", - "no-aliases-found": "Nav segvārdi atrasti.", - "no-key-matching": "'{{key}}' nav atrasti.", - "no-keys-found": "Nav atslēgas atrastas.", - "create-new-alias": "Radīt jaunu!", - "create-new-key": "Radīt jaunu!", - "duplicate-alias-error": "Dublēti segvārdi atrasti '{{alias}}'.
    Iekārtas segvārdiem ir jābūt unikāliem panelī.", - "configure-alias": "Konfigurēt '{{alias}}' segvārdus", - "no-devices-matching": "Nav iekārtu atbilstības '{{entity}}' atrastas.", - "alias": "Segvārdi", - "alias-required": "Iekārtu segvārdi ir nepieciešami.", - "remove-alias": "Noņemt iekārtas segvārdus", - "add-alias": "Pievienot iekārtas segvārdus", - "name-starts-with": "Iekārtas nosaukums sākas ar", - "device-list": "Iekārtu saraksts", - "use-device-name-filter": "Lietot filtru", - "device-list-empty": "Nav iekārtas atlasītas.", - "device-name-filter-required": "Iekārtas nosaukuma filtrs ir nepieciešams.", - "device-name-filter-no-device-matched": "Nav iekārtas kas sākas ar '{{device}}' atrastas.", - "add": "Pievienot iekārtu", - "assign-to-customer": "Piešķirt klientam", - "assign-device-to-customer": "Piešķirt iekārtas klientam", - "assign-device-to-customer-text": "Lūdzu atlasīt iekārtas lai pieškirtu klientam", - "make-public": "Veidot iekārtu publisku", - "make-private": "Veidot iekārtu privātu", - "no-devices-text": "Nav iekārtas atrastas", - "assign-to-customer-text": "Lūdzu atlasīt klientu lai pieškirtu iekārtas", - "device-details": "iekārtas detaļas", - "add-device-text": "Pievienot jaunu iekārtu", - "credentials": "Akreditācijas dati", - "manage-credentials": "Pārvaldīt akreditācijas datus", - "delete": "Dzēst iekārtu", - "assign-devices": "Pieškirt iekārtas", - "assign-devices-text": "Piešķirt { count, plural, 1 {1 device} other {# iekārtas} } klientam", - "delete-devices": "Dzēst iekārtas", - "unassign-from-customer": "Noņemt no klienta", - "unassign-devices": "Noņemt iekārtas", - "unassign-devices-action-title": "Noņemt { count, plural, 1 {1 device} other {# iekārtas} } no klienta", - "assign-new-device": "Pieškirt jaunu iekārtu", - "make-public-device-title": "Vai esat pārliecināts ka vēlaties veidot iekārtu '{{deviceName}}' publisku?", - "make-public-device-text": "Pēc apstiprinājuma iekārta un tās saistītie dati būs pieejami publiski un pieejami citiem.", - "make-private-device-title": "vai esat pārliecināts ka vēlaties veidot iekārtu '{{deviceName}}' privāti?", - "make-private-device-text": "Pēc apstiprinājuma iekārta un tās saistītie dati būs pieejami privāti un nebūs pieejami citiem.", - "view-credentials": "Skatīt akreditācijas datus", - "delete-device-title": "Vai esat pārliecināts, ka vēlaties dzēst iekārtu '{{deviceName}}'?", - "delete-device-text": "Esat uzmanīgs, pēc apstiprinājuma iekārta un tās saistītie dati nebūs atjaunojami.", - "delete-devices-title": "Vai esat pārliecināts, ka vēlaties dzēst { count, plural, 1 {1 device} other {# iekārtas} }?", - "delete-devices-action-title": "Dzēst { count, plural, 1 {1 device} other {# iekārtas} }", - "delete-devices-text": "Esat uzmanīgs, pēc apstiprinājuma iekārtas un to saistītie dati tiks noņemti un nebūs atjaunojami.", - "unassign-device-title": "Vai esat pārliecināts, ka vēlaties noņemt iekārtu '{{deviceName}}'?", - "unassign-device-text": "Pēc apstiprinājuma iekārta tiks noņemta un nebūs pieejama klientam.", - "unassign-device": "Noņemt iekārtu", - "unassign-devices-title": "Vai esat pārliecināts, ka vēlaties noņemt { count, plural, 1 {1 device} other {# iekārtas} }?", - "unassign-devices-text": "Pēc apstipinājuma visas atlasītās iekārtas būs noņemtas un nebūs pieejamas klientam.", - "device-credentials": "iekārtas akreditācijas dati", - "credentials-type": "Akreditācijas datu tips", - "access-token": "Piekļuves tokens", - "access-token-required": "Piekļuves tokens ir nepieciešams.", - "access-token-invalid": "Piekļuves tokena garumam ir jābūt no 1 līdz 20 rakstzīmēm.", - "rsa-key": "RSA publiskā atslēga", - "rsa-key-required": "RSA publiskā atslēga ir nepieciešama.", - "secret": "Noslēpums", - "secret-required": "Noslēpums ir nepieciešams.", - "device-type": "Iekārtas tips", - "device-type-required": "Iekārtas tips ir nepieciešams.", - "select-device-type": "Atlasīt iekārtas tipu", - "enter-device-type": "Ievadīt iekārtas tipu", - "any-device": "Jebkura iekārta", - "no-device-types-matching": "Nav iekārtas tipa saderības '{{entitySubtype}}' atrastas.", - "device-type-list-empty": "Nav iekārtas tipi izvēlēti.", - "device-types": "Iekārtas tipi", - "name": "Nosaukums", - "name-required": "Nosaukums ir nepieciešams.", - "description": "Apraksts", - "label": "Etiķete", - "events": "Notikumi", - "details": "Detaļas", - "copyId": "Kopēt iekārtas Id", - "copyAccessToken": "Kopēt piekļuves tokenu", - "idCopiedMessage": "iekārtas Id ir kopēts uz starpliktuvi", - "accessTokenCopiedMessage": "Iekārtas piekļuves tokens ir kopēts uz starpliktuvi", - "assignedToCustomer": "Piešķirts klientam", - "unable-delete-device-alias-title": "Nav iespējas dzēst iekārtas segvārdus", - "unable-delete-device-alias-text": "Iekārtas segvārdi '{{deviceAlias}}' nevar būt dzēsti, jo tie lietoti sekojošajos logrīkos:
    {{widgetsList}}", - "is-gateway": "Tā ir vārteja", - "public": "Publisks", - "device-public": "Iekārta ir publiska", - "select-device": "Atlasīt iekārtu", - "import": "Importēt iekārtu", - "device-file": "Iekārtas fails" - }, - "dialog": { - "close": "Aizvērt dialogu" - }, - "direction": { - "column": "Kolona", - "row": "Rinda" - }, - "error": { - "unable-to-connect": "Nav iespējams pievienoties serverim! Lūdzu pārbaudīt interneta savienojumu.", - "unhandled-error-code": "Neapstrādāta kļūda: {{errorCode}}", - "unknown-error": "Nezināma kļūda" - }, - "entity": { - "entity": "Vienība", - "entities": "Vienības", - "aliases": "Vienību segvārdi", - "entity-alias": "Vienību segvārdi", - "unable-delete-entity-alias-title": "Nav iespējams dzēst vienību segvārdus", - "unable-delete-entity-alias-text": "Vienību segvārdi '{{entityAlias}}' nevar tikt dzēsti, jo tos izmanto sekojošie logrīki:
    {{widgetsList}}", - "duplicate-alias-error": "Dublikāti segvārdi atrasti '{{alias}}'.
    Vienību segvārdiem ir jābūt unikāliem paneļos.", - "missing-entity-filter-error": "Filtrs trūkst priekš segvārda '{{alias}}'.", - "configure-alias": "Konfigurēt '{{alias}}' segvārdus", - "alias": "Segvārds", - "alias-required": "Vienību segvārds ir nepieciešams.", - "remove-alias": "Noņemt vienību segvārdu", - "add-alias": "Pievienot vienību segvārdu", - "entity-list": "Vienību saraksts", - "entity-type": "Vienības tips", - "entity-types": "Vienības tipi", - "entity-type-list": "Vienības tipu saraksts", - "any-entity": "Jebkura vienība", - "enter-entity-type": "Ievadīt vienības tipu", - "no-entities-matching": "Nav vienības saderības '{{entity}}' atrastas.", - "no-entity-types-matching": "Nav vienības tipu saderības '{{entityType}}' atrastas.", - "name-starts-with": "Nosaukums sākas ar", - "use-entity-name-filter": "Lietot filtru", - "entity-list-empty": "Nav vienības atlasītas.", - "entity-type-list-empty": "Nav vienības tipi atlasīti.", - "entity-name-filter-required": "Vienību nosaukuma filtri ir vajadzīgi.", - "entity-name-filter-no-entity-matched": "Nav vienības kas sākas ar '{{entity}}' atrastas.", - "all-subtypes": "Visi", - "select-entities": "Atlasīt vienības", - "no-aliases-found": "Nav segvārdi atrasti.", - "no-alias-matching": "'{{alias}}' nav atrasts.", - "create-new-alias": "Radīt jaunu!", - "key": "Atslēga", - "key-name": "Atslēgas nosaukums", - "no-keys-found": "Nav atslēgas atrastas.", - "no-key-matching": "'{{key}}' nav atrasta.", - "create-new-key": "Radīt jaunu!", - "type": "Tips", - "type-required": "Vienības tips ir nepieciešams.", - "type-device": "Iekārta", - "type-devices": "Iekārtas", - "list-of-devices": "{ count, plural, 1 {One device} other {List of # iekārtas} }", - "device-name-starts-with": "Iekārtas, kuras nosaukumi sākas ar '{{prefix}}'", - "type-asset": "Aktīvs", - "type-assets": "Aktīvi", - "list-of-assets": "{ count, plural, 1 {One asset} other {List of # aktīvi} }", - "asset-name-starts-with": "Aktīvi, kuru nosaukumi sākas ar '{{prefix}}'", - "type-entity-view": "Vienības skats View", - "type-entity-views": "Vienības skati", - "list-of-entity-views": "{ count, plural, 1 {One entity view} other {List of # vienību skati} }", - "entity-view-name-starts-with": "Vienibas skati, kuru nosaukumi sākas ar '{{prefix}}'", - "type-rule": "Noteikums", - "type-rules": "Noteikumi", - "list-of-rules": "{ count, plural, 1 {One rule} other {List of # noteikumi} }", - "rule-name-starts-with": "Noteikumi, kuru nosaukumi sākas ar '{{prefix}}'", - "type-plugin": "Spraudnis", - "type-plugins": "Spraudņi", - "list-of-plugins": "{ count, plural, 1 {One plugin} other {List of # spraudņi} }", - "plugin-name-starts-with": "Spraudņi, kuru vārds sākas ar '{{prefix}}'", - "type-tenant": "Īrnieks", - "type-tenants": "Īrnieki", - "list-of-tenants": "{ count, plural, 1 {One tenant} other {List of # īrnieki} }", - "tenant-name-starts-with": "Īrnieki, kuru nosaukumi sākas ar '{{prefix}}'", - "type-customer": "Klients", - "type-customers": "Klienti", - "list-of-customers": "{ count, plural, 1 {One customer} other {List of # klienti} }", - "customer-name-starts-with": "Klienti, kuru nosaukumi sākas ar '{{prefix}}'", - "type-user": "Lietotājs", - "type-users": "Lietotāji", - "list-of-users": "{ count, plural, 1 {One user} other {List of # lietotāji} }", - "user-name-starts-with": "Lietotāji, kuru nosaukums sākas ar '{{prefix}}'", - "type-dashboard": "Panelis", - "type-dashboards": "Paneļi", - "list-of-dashboards": "{ count, plural, 1 {One dashboard} other {List of # paneļi} }", - "dashboard-name-starts-with": "Paneļi, kuru nosaukums sākas ar '{{prefix}}'", - "type-alarm": "Trauksme", - "type-alarms": "Trauksmes", - "list-of-alarms": "{ count, plural, 1 {One alarms} other {List of # trauksmes} }", - "alarm-name-starts-with": "Trauksmes, kuru nosaukumi sākas ar '{{prefix}}'", - "type-rulechain": "Noteikumu ķēde", - "type-rulechains": "Noteikumu ķēdes", - "list-of-rulechains": "{ count, plural, 1 {One rule chain} other {List of # noteikumu ķēdes} }", - "rulechain-name-starts-with": "Noteikumu ķēdes, kuru nosaukumi sākas ar '{{prefix}}'", - "type-rulenode": "Noteikumu node", - "type-rulenodes": "Noteikumu nodes", - "list-of-rulenodes": "{ count, plural, 1 {One rule node} other {List of # noteikumu nodes} }", - "rulenode-name-starts-with": "Noteikumu nodes, juru nosaukumi sākas ar '{{prefix}}'", - "type-current-customer": "Pašreizējais klients", - "search": "Meklēšanas vienības", - "selected-entities": "{ count, plural, 1 {1 entity} other {# vienības} } atlasītas", - "entity-name": "Vienības nosaukums", - "details": "Vienības detaļas", - "no-entities-prompt": "Nav vienības atrastas", - "no-data": "Nav datu ko attēlot", - "columns-to-display": "Kolonas ko attēlot" - }, - "entity-view": { - "entity-view": "Vienības skats", - "entity-view-required": "Vienības skats ir nepieciešams.", - "entity-views": "Vienības skati", - "management": "Vienības skatu pārvaldība", - "view-entity-views": "Skatīt vienību skatus", - "entity-view-alias": "Vienību skatu segvārdi", - "aliases": "Vienību skatu segvārdi", - "no-alias-matching": "'{{alias}}' nav atrasts.", - "no-aliases-found": "Nav segvārds atrasts.", - "no-key-matching": "'{{key}}' nav atrasts.", - "no-keys-found": "Nav atslēgas atrastas.", - "create-new-alias": "Radīt jaunu!", - "create-new-key": "Radīt jaunu!", - "duplicate-alias-error": "Dublēti segvārdi atrasti '{{alias}}'.
    Vienību skatu segvārdiem ir jābūt unikāliem paneļa ietvaros.", - "configure-alias": "Konfigurēt '{{alias}}' segvārdu", - "no-entity-views-matching": "Nav vienību skata atbilstības '{{entity}}' atrastas.", - "alias": "Segvārds", - "alias-required": "Vienību skatu segvārdi ir nepieciešami.", - "remove-alias": "Noņemt vienību skatu segvārdu", - "add-alias": "Pievienot vienību skatu segvārdu", - "name-starts-with": "Vienību skata nosaukums sākas ar", - "entity-view-list": "Vienību skata saraksts", - "use-entity-view-name-filter": "Lietot filtru", - "entity-view-list-empty": "Nav vienību skati atlasīti.", - "entity-view-name-filter-required": "Vienību skatu nosaukumu filtri ir nepieciešami.", - "entity-view-name-filter-no-entity-view-matched": "Nav vienību skati kas sākas ar '{{entityView}}' atrasti.", - "add": "Pievienot vienību skatu", - "assign-to-customer": "Pieškirt klientam", - "assign-entity-view-to-customer": "Piešķirt vienību skatus klientam", - "assign-entity-view-to-customer-text": "Lūdzu izvēlēties vienību skatus ko pieškirt klientam", - "no-entity-views-text": "Nav vienību skati atrasti", - "assign-to-customer-text": "Lūdzu izvēlēties klientu lai pieškirtu vienības skatus", - "entity-view-details": "Vienību skata detaļas", - "add-entity-view-text": "Pievienot jaunu vienību skatu", - "delete": "Dzēsts vienību skatu", - "assign-entity-views": "Piešķirt vienību skatus", - "assign-entity-views-text": "Piešķirt { count, plural, 1 {1 entityView} other {# vienību skati} } klientam", - "delete-entity-views": "Dzēst vienību skatus", - "unassign-from-customer": "Noņemt no klienta", - "unassign-entity-views": "Noņemt vienību skatus", - "unassign-entity-views-action-title": "Noņemt { count, plural, 1 {1 entityView} other {# vienību skati} } no klienta", - "assign-new-entity-view": "Piešķirt jaunu vienību skatu", - "delete-entity-view-title": "Vai esat pārliecināts,ka vēlaties dzēst vienību skatu '{{entityViewName}}'?", - "delete-entity-view-text": "Esiet uzmanīgs, pēc apstiprinājuma vienību skats un tā sasitītie dati nebūs atjaunojami.", - "delete-entity-views-title": "Vai esat pārliecināts, ka vēlaties vienību skatu { count, plural, 1 {1 entityView} other {# vienību skati} }?", - "delete-entity-views-action-title": "Dzēst { count, plural, 1 {1 entityView} other {# vienību skati} }", - "delete-entity-views-text": "Esiet uzmanīgs, pēc apstiprinājuma visir atlasītie vienības skati tiks noņemti un to saistītie dati nebūs atjaunojami.", - "unassign-entity-view-title": "Vai esat pārliecināts, ka vēlaties atspējot vienību skatu '{{entityViewName}}'?", - "unassign-entity-view-text": "Pēc apstiprinājuma vienību skats tiks atspējots un nebūs pieejams klientam.", - "unassign-entity-view": "Atspējot vienību skatu", - "unassign-entity-views-title": "Vai esat pārliecināts, ka vēlaties atspējot { count, plural, 1 {1 entityView} other {# vienību skatus} }?", - "unassign-entity-views-text": "Pēc apstiprinājuma visi atlasītie vienību skati būs atspējoti un nebūs pieejami klientiem.", - "entity-view-type": "Vienību skata tips", - "entity-view-type-required": "Vienību skata tips ir nepieciešams.", - "select-entity-view-type": "Atlasīt vienību skata tipu", - "enter-entity-view-type": "Ievadīt vienību skata tipu", - "any-entity-view": "Jebkurš vienību skats", - "no-entity-view-types-matching": "Nav vienību skata atbilstības '{{entitySubtype}}' atrastas.", - "entity-view-type-list-empty": "Nav vienību skatu tipi atlasīti.", - "entity-view-types": "Vienību skatu tipi", - "name": "Nosaukums", - "name-required": "Nosaukums ir nepieciešams.", - "description": "Apraksts", - "events": "Notikumi", - "details": "Detaļas", - "copyId": "Kopēt vienību skata Id", - "assignedToCustomer": "Piešķirta klientam", - "unable-entity-view-device-alias-title": "Nav iespējas dzēst vienību skata segvārdu", - "unable-entity-view-device-alias-text": "Iekārtas segvārds '{{entityViewAlias}}' nevar tikt dzēsts, jo to izmanto sekojošs logrīks:
    {{widgetsList}}", - "select-entity-view": "Atlasīt vienību skatu", - "make-public": "Veidot vienību skatu publisku", - "make-private": "Veidot vienību skatu privātu", - "start-date": "Starta datums", - "start-ts": "Starta laiks", - "end-date": "Beigu datums", - "end-ts": "Beigu laiks", - "date-limits": "Datuma limits", - "client-attributes": "Klienta atribūti", - "shared-attributes": "Dalītie atribūti", - "server-attributes": "Servera atribūti", - "timeseries": "Laika sērijas", - "client-attributes-placeholder": "Klienta atribūti", - "shared-attributes-placeholder": "Dalītie atribūti", - "server-attributes-placeholder": "Servera atribūti", - "timeseries-placeholder": "Laika sērijas", - "target-entity": "Mērķa vienība", - "attributes-propagation": "Atribūtu izplatīšana", - "attributes-propagation-hint": "Vienību skats automātiski kopē specificētos atribūtus no mērķa vienības katru reizi kad jūs saglabājat vai atjaunojat vienību skatu.", - "timeseries-data": "Laika sērijas dati", - "timeseries-data-hint": "Configure timeseries data keys of the target entity that will be accessible to the entity view. This timeseries data is read-only.", - "make-public-entity-view-title": "Vai esat pārliecināts, ka vēlaties veidot vienību skatu '{{entityViewName}}' publisku?", - "make-public-entity-view-text": "Pēc apstiprinājuma vienību skats un tā saistītie dati būs publiski un pieejami citiem.", - "make-private-entity-view-title": "Vai esat pārliecināts, ka vēlaties veidot vienību skatu '{{entityViewName}}' privātu?", - "make-private-entity-view-text": "Pēc apstiprinājuma vienību skats un tā saistītie dati būs privāti un nebūs pieejami citiem." - }, - "event": { - "event-type": "Notikuma tips", - "type-error": "Kļūda", - "type-lc-event": "Dzīves cikla notikums", - "type-stats": "Statistika", - "type-debug-rule-node": "Atkļūdot", - "type-debug-rule-chain": "Atkļūdot", - "no-events-prompt": "Nav notikumi atrasti", - "error": "Kļūda", - "alarm": "Trauksme", - "event-time": "Notikuma laiks", - "server": "Serveris", - "body": "Galvenā daļa", - "method": "Metode", - "type": "Tips", - "entity": "Vienība", - "message-id": "Ziņojuma Id", - "message-type": "Ziņojuma tips", - "data-type": "Datu tips", - "relation-type": "Attiecību tips", - "metadata": "Metadata", - "data": "Dati", - "event": "Notikumi", - "status": "Statuss", - "success": "Sekmīgi", - "failed": "Kļūda", - "messages-processed": "Ziņojumi apstrādāti", - "errors-occurred": "Kļūdas konstatētas" - }, - "extension": { - "extensions": "Paplašinājumi", - "selected-extensions": "{ count, plural, 1 {1 extension} other {# paplašinājumi} } atlasītie", - "type": "Tips", - "key": "Atslēga", - "value": "Vērtība", - "id": "Id", - "extension-id": "Paplašinājuma id", - "extension-type": "Paplašinājuma tips", - "transformer-json": "JSON *", - "unique-id-required": "Patreizējā paplašinājuma id jau eksistē.", - "delete": "Dzēst paplašinājumu", - "add": "Pievienot paplašinājumu", - "edit": "Rediģēt paplašinājumu", - "delete-extension-title": "Vai esat pārliecināts, ka vēlaties dzēst paplašinājumu '{{extensionId}}'?", - "delete-extension-text": "Esiet uzmanīgs, pēc apstiprinājuma paplašinājums un visi tā saistītie dati nebūs atjaunojami.", - "delete-extensions-title": "Vai esat pārliecināts, ka vēlaties dzēst { count, plural, 1 {1 extension} other {# paplašinājumus} }?", - "delete-extensions-text": "Esiet uzmanīgs, pēc apstiprinājuma visi atlasītie paplašinājumi tiks dzēsti.", - "converters": "Pārveidotāji", - "converter-id": "Pārveidotāja id", - "configuration": "Konfigurācija", - "converter-configurations": "Pārveidotāja konfigurācija", - "token": "Drošības tokens", - "add-converter": "Pievienot pārveidotāju", - "add-config": "Pievienot pārveidotāja konfigurāciju", - "device-name-expression": "Iekārtas nosaukuma izteiksme", - "device-type-expression": "Iekārtas tipa izteiksme", - "custom": "Pielāgot", - "to-double": "Dubutot", - "transformer": "Pārveidotājs", - "json-required": "json pārveidotājs ir nepieciešams.", - "json-parse": "Nav iespējams parsēt json pārveidotāju.", - "attributes": "Atribūti", - "add-attribute": "Pievienot atribūtus", - "add-map": "Pievienot kartēšanas elementu", - "timeseries": "Laika sērijas", - "add-timeseries": "Pievienot laika sērijas", - "field-required": "Lauks ir nepieciešams", - "brokers": "Brokeris", - "add-broker": "Pievienot brokeri", - "host": "Saimnieks", - "port": "Ports", - "port-range": "Portam jābūt robežās no 1 līdz 65535.", - "ssl": "Ssl", - "credentials": "Akreditācijas dati", - "username": "Lietotājvārds", - "password": "Parole", - "retry-interval": "Mēģināt vēlreiz intervāls milisekundēs", - "anonymous": "Anonīmi", - "basic": "Pamata", - "pem": "PEM", - "ca-cert": "CA sertifikācijas fails *", - "private-key": "Privātās atslēgas fails *", - "cert": "Srtifikāta fails *", - "no-file": "Nav fails izvēlēts.", - "drop-file": "Nosviest failu vai klikšķināt izvēlēto failu augšupielādei.", - "mapping": "Kartēšana", - "topic-filter": "Temata filtrs", - "converter-type": "Pārveidotāja tips", - "converter-json": "Json", - "json-name-expression": "Iekārtas nosaukuma json izteiksme", - "topic-name-expression": "Iekārtas nosaukuma temata izteiksme", - "json-type-expression": "Iekārtas tipa json izteiksme", - "topic-type-expression": "Iekārtas tipa temata izteiksme", - "attribute-key-expression": "Atribūtu atslēgas izteiksme", - "attr-json-key-expression": "Atribūtu atslēgas json izteiksme", - "attr-topic-key-expression": "Attribūtu atslēgas temata izteiksme", - "request-id-expression": "Pieprasīt id izteiksmi", - "request-id-json-expression": "Pieprasīt id json izteiksmi", - "request-id-topic-expression": "Pieprasīt id temata izteiksmi", - "response-topic-expression": "Atbildēt temata izteiksmi", - "value-expression": "Vērtības izteiksme", - "topic": "Temats", - "timeout": "Pārtraukums milisekundēs", - "converter-json-required": "json pārveidotājs ir nepieciešams.", - "converter-json-parse": "Nav iespējams parsēt pārveidotāju json.", - "filter-expression": "Filtra izteiksme", - "connect-requests": "Savienot pieprasījumus", - "add-connect-request": "Pievienot savienojuma pieprasījumus", - "disconnect-requests": "Atvienot pieprasījumus", - "add-disconnect-request": "Pievienot atvienot pieprasījumus", - "attribute-requests": "Attribūtu pieprasījumus", - "add-attribute-request": "Pievienot atribūtu pieprasījumu", - "attribute-updates": "Atribūtu atjaunojumi", - "add-attribute-update": "Pievienot atribūtu atjaunojumus", - "server-side-rpc": "Servera puses RPC", - "add-server-side-rpc-request": "Pievienot servera puses RPC pieprasījumus", - "device-name-filter": "Iekārtas nosaukuma filtrs", - "attribute-filter": "Atribūtu filtrs", - "method-filter": "Metodes filtrs", - "request-topic-expression": "Pieprasīt temata izteiksmi", - "response-timeout": "Atbildes pārtraukums milisekundēs", - "topic-expression": "Temata izteiksme", - "client-scope": "Klienta darbības joma", - "add-device": "Pievienot iekārtu", - "opc-server": "Serveris", - "opc-add-server": "Pievienot serveri", - "opc-add-server-prompt": "Lūdzu pievienot serveri", - "opc-application-name": "Aplikācijas nosaukums", - "opc-application-uri": "Aplikācijas uri", - "opc-scan-period-in-seconds": "Skanēt periodu sekundēs", - "opc-security": "Drošība", - "opc-identity": "Identitāte", - "opc-keystore": "Atslēgu veikals", - "opc-type": "Tips", - "opc-keystore-type": "Tips", - "opc-keystore-location": "Vieta *", - "opc-keystore-password": "Parole", - "opc-keystore-alias": "Segvārds", - "opc-keystore-key-password": "Atslēgas parole", - "opc-device-node-pattern": "Iekārtas nodes veids", - "opc-device-name-pattern": "Iekārtas nosaukuma veids", - "modbus-server": "Serveris/vergs", - "modbus-add-server": "Pievienot serveri/vergi", - "modbus-add-server-prompt": "Lūdzu pievienot serveri/vergu", - "modbus-transport": "Transports", - "modbus-tcp-reconnect": "Automātiski atkārtoti savienot", - "modbus-rtu-over-tcp": "RTU pa TCP", - "modbus-port-name": "Seriālā porta nosaukums", - "modbus-encoding": "Kodēšana", - "modbus-parity": "Paritāte", - "modbus-baudrate": "Pārraides ātrums", - "modbus-databits": "Datu bits", - "modbus-stopbits": "Stop bits", - "modbus-databits-range": "Datu bitiem jābūt no 7 līdz 8.", - "modbus-stopbits-range": "Stop bitiem jābūt no 1 līdz 2.", - "modbus-unit-id": "Iekārtas ID", - "modbus-unit-id-range": "Iekārtas ID jābūt no 1 līdz 247.", - "modbus-device-name": "Iekārtas nosaukums", - "modbus-poll-period": "Aptaujas periods (ms)", - "modbus-attributes-poll-period": "Atribūtu aptaujas periods (ms)", - "modbus-timeseries-poll-period": "Laika sērijas aptaujas periods (ms)", - "modbus-poll-period-range": "Aptaujas periodam jābūt pzitīvai vērtībai.", - "modbus-tag": "Etiķete", - "modbus-function": "Funkcija", - "modbus-register-address": "Reģistra adrese", - "modbus-register-address-range": "Reģistra adresei jābūt no 0 līdz 65535.", - "modbus-register-bit-index": "Bita indekss", - "modbus-register-bit-index-range": "Bita indeksam jābūt no 0 līdz 15.", - "modbus-register-count": "Reģistra skaitītājs", - "modbus-register-count-range": "Reģistra skaitītājam jābūt pzitīvai vērtībai.", - "modbus-byte-order": "Baitu kārtība", - "sync": { - "status": "Statuss", - "sync": "Sync", - "not-sync": "Nav sync", - "last-sync-time": "Pēdējais sync laiks", - "not-available": "Nav pieejams" - }, - "export-extensions-configuration": "Eksportēt paplašinājuma konfigurāciju", - "import-extensions-configuration": "Importēt paplašinājuma konfigurāciju", - "import-extensions": "Importēt paplašinājumus", - "import-extension": "Importēt paplašinājumu", - "export-extension": "Eksportēt paplašinājumu", - "file": "Paplašinājuma fails", - "invalid-file-error": "Invalīds paplašinājuma fails" - }, - "fullscreen": { - "expand": "Paplašināt uz pilnu ekrānu", - "exit": "Iziet no pilna ekrāna", - "toggle": "Pārslēgties uz pilna ekrāna režīmu", - "fullscreen": "Pilns ekrāns" - }, - "function": { - "function": "Funkcija" - }, - "grid": { - "delete-item-title": "Vai esat pārliecināts, ka vēlaties dzēst šo priekšmetu?", - "delete-item-text": "Esiet uzmanīgs, pēc apstiprinājuma šī priekšmeta dati nebūs atjaunojami.", - "delete-items-title": "Vai esat pārliecināts, ka vēlaties dzēst { count, plural, 1 {1 item} other {# priekšmetus} }?", - "delete-items-action-title": "Dzēst { count, plural, 1 {1 item} other {# priekšmeti} }", - "delete-items-text": "Esiet uzmanīgs, pēc apstiprinājuma visi atlasītie priekšmeti tiks noņemti un to saistītie dati nebūs atjaunojami.", - "add-item-text": "Pievienot jaunu priekšmetu", - "no-items-text": "Nav priekšmeti atrasti", - "item-details": "Priekšmetu detaļas", - "delete-item": "Dzēst priekšmetu", - "delete-items": "Dzēst priekšmetus", - "scroll-to-top": "Iet uz sākumu" - }, - "help": { - "goto-help-page": "Ejiet uz palīdzības lapu" - }, - "home": { - "home": "Sākums", - "profile": "Profils", - "logout": "Izlogoties", - "menu": "Izvēlne", - "avatar": "Avatars", - "open-user-menu": "Atvērt lietotāja izvēlni" - }, - "import": { - "no-file": "Nav fails izvēlēts", - "drop-file": "Nosviest JSON failu vai klikšķināt uz atlasīto failu augšupielādei.", - "drop-file-csv": "Nosviest CSV failu vai klikšķināt uz atlasīto failu augšupielādei.", - "column-value": "Vērtība", - "column-title": "Virsraksts", - "column-example": "Piemēra vērtību dati", - "column-key": "Atribūts/telemetrijas atslēga key", - "csv-delimiter": "CSV kolonu atdalītājs", - "csv-first-line-header": "Pirmā līnija satur kolonu nosaukumus", - "csv-update-data": "Atjaunot atribūtu/telemetrija", - "import-csv-number-columns-error": "Failā jābūt vismaz divām kolonām", - "import-csv-invalid-format-error": "Invalīds faila formāts. Līnija: '{{line}}'", - "column-type": { - "name": "Nosaukums", - "type": "Tips", - "column-type": "Kolonas tips", - "client-attribute": "Klienta atribūts", - "shared-attribute": "Dalītais atribūts", - "server-attribute": "Servera atribūts", - "timeseries": "Laika sērijas", - "entity-field": "Vienības lauks", - "access-token": "Piekļuves tokens" - }, - "stepper-text": { - "select-file": "Atlasīt failu", - "configuration": "Importēt konfigurāciju", - "column-type": "Atlasīt kolonas tipu", - "creat-entities": "Radīt jaunas vienības", - "done": "Darīts" - }, - "message": { - "create-entities": "{{count}} jaunas vienības sekmīgi radītas.", - "update-entities": "{{count}} vienības sekmīgi atjaunotas.", - "error-entities": "Te ir kļūda radot {{count}} vienības." - } - }, - "item": { - "selected": "Atlasīts" - }, - "js-func": { - "no-return-error": "Funkcijai vajag atgriezt rezultātu!", - "return-type-mismatch": "Funkcijai vajag atgriezt rezultātu '{{type}}' !", - "tidy": "Sakopt" - }, - "key-val": { - "key": "Atslēga", - "value": "Vērtība", - "remove-entry": "Noņemt ierakstu", - "add-entry": "Pievienot ierakstu", - "no-data": "Nav ierakstu" - }, - "layout": { - "layout": "Izkārtojums", - "manage": "Pārvaldīt izkārtojumu", - "settings": "Izkārtojuma iestatījumi", - "color": "Krāsa", - "main": "Galvenais", - "right": "Pa labi", - "select": "Atlasīt mērķa izkārtojumu" - }, - "legend": { - "direction": "Leģendas virziens", - "position": "Leģendas pozīcija", - "show-max": "Rādīt max vērtību", - "show-min": "Rādīt min vērtību", - "show-avg": "Rādīt vidējo vērtību", - "show-total": "Rādīt kopējo vērtību", - "settings": "Leģendas iestatījumi", - "min": "min", - "max": "max", - "avg": "vidējais", - "total": "total" - }, - "login": { - "login": "Login", - "request-password-reset": "Pieprasīt atiestatīt paroli", - "reset-password": "Atiestatīt paroli", - "create-password": "Radīt paroli", - "passwords-mismatch-error": "Ievadītajai parolei ir jāsakrīt!", - "password-again": "Atkārtot paroli", - "sign-in": "Lūdzu pierakstīties", - "username": "Lietotājvārds (email)", - "remember-me": "Atcerēties mani", - "forgot-password": "Aizmirsu paroli?", - "password-reset": "Paroli atiestatīt", - "new-password": "Jaunā parole", - "new-password-again": "Atkārtot jauno paroli", - "password-link-sent-message": "paroles atiestatīšanas saite sekmīgi nosūtīta!", - "email": "Email" - }, - "position": { - "top": "Sākums", - "bottom": "Beigas", - "left": "Pa kreisi", - "right": "Pa labi" - }, - "profile": { - "profile": "Profils", - "change-password": "Mainīt paroli", - "current-password": "Patreizējā parole" - }, - "relation": { - "relations": "Attiecības", - "direction": "Virziens", - "search-direction": { - "FROM": "No", - "TO": "Uz" - }, - "direction-type": { - "FROM": "No", - "TO": "Uz" - }, - "from-relations": "Izejošāsa attiecības", - "to-relations": "Ienākošās attiecības", - "selected-relations": "{ count, plural, 1 {1 relation} other {# attiecības} } atlasītas", - "type": "Tips", - "to-entity-type": "Uz vienību tipu", - "to-entity-name": "Uz vienību nosaukumu", - "from-entity-type": "No vienību tipa", - "from-entity-name": "No vienību nosaukuma", - "to-entity": "Uz vienību", - "from-entity": "No vienības", - "delete": "Dzēst attiecību", - "relation-type": "Attiecības tips", - "relation-type-required": "Attiecību tips ir nepieciešams.", - "any-relation-type": "Jebkura tipa", - "add": "Pievienot attiecību", - "edit": "Rediģēt attiecību", - "delete-to-relation-title": "Vai esat pārliecināts, ka vēlaties dzēst attiecību uz vienību '{{entityName}}'?", - "delete-to-relation-text": "Esiet uzmanīgs, pēc apstiprinājuma vienība '{{entityName}}' būs atsaistīta no patreizējās vienības.", - "delete-to-relations-title": "Vai esat pārliecināts, ka vēlaties dzēst { count, plural, 1 {1 relation} other {# attiecības} }?", - "delete-to-relations-text": "Esiet uzmanīgs, pēc apstiprināšanas visas atlasītās attiecības būs noņemtas un attiecīgās vienības būs atsaistītas no patreizējās vienības.", - "delete-from-relation-title": "Vai esat pārliecināts, ka vēlaties dzēst attiecību no vienības '{{entityName}}'?", - "delete-from-relation-text": "Esiet uzmanīgs, pēc apstiprinājuma patreizējā vienība būs atsaistīta no vienības '{{entityName}}'.", - "delete-from-relations-title": "Vai esat pārliecināts, ka vēlaties dzēst { count, plural, 1 {1 relation} other {# attiecības} }?", - "delete-from-relations-text": "Esiet uzmanīgs, pēc apstiprinājuma visas atlasītās attiecības būs noņemtas un patreizējā vienība tiks atsaistīta no attiecīgās vienības.", - "remove-relation-filter": "Noņemt attiecību filtru", - "add-relation-filter": "Pievienot attiecību filtru", - "any-relation": "Jebkura attiecība", - "relation-filters": "Attiecību filtrs", - "additional-info": "Papildus info (JSON)", - "invalid-additional-info": "Nav iespēja parsēt papildus info json." - }, - "rulechain": { - "rulechain": "Noteikumu ķēde", - "rulechains": "Noteikumu ķēdes", - "root": "Sakne", - "delete": "Dzēsts noteikumu ķēdi", - "name": "Nosaukums", - "name-required": "Nosaukums ir nepieciešams.", - "description": "Nosaukums ir nepieciešams", - "add": "Pievienot noteikumu ķēdi", - "set-root": "Veidot noteikumu ķēdi kā sakni", - "set-root-rulechain-title": "Vai esat pārliecināts, ka vēlaties veidot noteikumu ķēdi '{{ruleChainName}}' kā sakni?", - "set-root-rulechain-text": "Pēc apstiprinājuma noteikuma ķēde tiks veidota kā sakne un apstrādās visu ienākošo informāciju.", - "delete-rulechain-title": "Vai esat pārliecināts, ka vēlaties dzēst noteikumu ķēdi '{{ruleChainName}}'?", - "delete-rulechain-text": "Esiet uzmanīgs, pēc apstiprinājuma noteikumu ķēde un saistītā informācija nebūs atjaunojama.", - "delete-rulechains-title": "Vai esat pārliecināts, ka vēlaties dzēst { count, plural, 1 {1 rule chain} other {# noteikumu ķēdes} }?", - "delete-rulechains-action-title": "Dzēst { count, plural, 1 {1 rule chain} other {# noteikumu ķēdes} }", - "delete-rulechains-text": "Esiet uzmanīgs, pēc apstiprinājuma visas atlasītās noteikumu ķēdes tiks noņemtas un to saistītos datus nevarēs atjaunot.", - "add-rulechain-text": "Pievienot jaunu noteikumu ķēdi", - "no-rulechains-text": "Nav noteikumu ķēdes atrastas", - "rulechain-details": "Noteikumu ķēdes detaļas", - "details": "Detaļas", - "events": "Notikumi", - "system": "Sistēma", - "import": "Importēt noteikumu ķēdi", - "export": "Eksportēt noteikumu ķēdi", - "export-failed-error": "Nav iespējams eksportēt noteikumu ķēdi: {{error}}", - "create-new-rulechain": "Radīt jaunu noteikumu ķēdi", - "rulechain-file": "Noteikumu ķēdes fails", - "invalid-rulechain-file-error": "Nav iespējams importēt noteikumu ķēdi: Invalīda noteikumu ķēdes datu struktūra.", - "copyId": "Kopēt noteikumu ķēdes Id", - "idCopiedMessage": "Noteikumu ķēdes Id ir kopēta uz starpliktuves", - "select-rulechain": "Atlasīt noteikumu ķēdi", - "no-rulechains-matching": "Nav noteikumu ķēdes atbilstības '{{entity}}' atrastas.", - "rulechain-required": "Noteikumu ķēde ir nepieciešama", - "management": "Noteikumu pārvaldība", - "debug-mode": "Atkļūdošanas režīms" - }, - "rulenode": { - "details": "Detaļas", - "events": "Notikumi", - "search": "Meklēt nodes", - "open-node-library": "Atvērt node bibliotēku", - "add": "Pievienot noteikumu nodi", - "name": "Nosaukums", - "name-required": "Nosaukums ir nepieciešams.", - "type": "Tips", - "description": "Apraksts", - "delete": "Dzēst noteikumu nodi", - "select-all-objects": "Atlasīt visas nodes un savienojumus", - "deselect-all-objects": "Noņemt visas nodes un savienojumus", - "delete-selected-objects": "Dzēst atlasītās nodes un savienojumus", - "delete-selected": "Dzēst atlasītos", - "select-all": "Atlasīt visu", - "copy-selected": "Kopēt atlasīto", - "deselect-all": "Noņemt visu", - "rulenode-details": "Noteikumu nodes detaļas", - "debug-mode": "Atkļūdošanas mode", - "configuration": "Konfigurācija", - "link": "Saite", - "link-details": "Noteikumu nodes saites detaļas", - "add-link": "Pievienot saiti", - "link-label": "Saites etiķete", - "link-label-required": "Saites etiķete ir nepieciešama.", - "custom-link-label": "Klienta saites etiķete", - "custom-link-label-required": "Klienta saites etiķete ir nepieciešama.", - "link-labels": "Saites etiķetes", - "link-labels-required": "Saites etiķetes ir nepieciešamas.", - "no-link-labels-found":"Nav saites etiķetes atrastas", - "no-link-label-matching": "'{{label}}' nav atrasta.", - "create-new-link-label": "Radīt jaunu!", - "type-filter": "Filtrs", - "type-filter-details": "Filtrēt ienākošos ziņojumus ar konfigurētajiem stāvokļiem", - "type-enrichment": "Bagātināšana", - "type-enrichment-details": "Pievieno papildus informāciju ziņas metadatiem", - "type-transformation": "Transformācija", - "type-transformation-details": "Mainīt ziņas datu lauku un metatdatus", - "type-action": "Aktivitāte", - "type-action-details": "Veikt specifisku aktivitāti", - "type-external": "Ārējs", - "type-external-details": "Sadarboties ar ārējām sistēmām", - "type-rule-chain": "Noteikumu ķēde", - "type-rule-chain-details": "Pārsūta ienākošo ziņu uz specifisku noteikumu ķēdi ", - "type-input": "Ievads", - "type-input-details": "Loģiskais ievads noteikumu ķēdei, pārsūta ienākošās ziņas uz nākamo attiecināto noteikumu nodi", - "type-unknown": "Nezināms", - "type-unknown-details": "Neatrisināta noteikumu node", - "directive-is-not-loaded": "Noteikta konfigurācijas direktīva '{{directiveName}}' nav pieejama.", - "ui-resources-load-error": "Nesekmīgs mēģinājums ielādēt konfigurācijas ui resursus.", - "invalid-target-rulechain": "Nav iespējams atrisināt mērķa noteikumu ķēdi!", - "test-script-function": "Testēt skripta funkciju", - "message": "Ziņa", - "message-type": "Ziņas tips", - "select-message-type": "Atlasīt ziņas tipu", - "message-type-required": "Ziņas tips ir nepieciešams", - "metadata": "Metadati", - "metadata-required": "Metadatu ievadi nevar būt tukši.", - "output": "Izeja", - "test": "Tests", - "help": "Palīdzība", - "reset-debug-mode": "Atiestatīt atkļūdošanu visās nodēs" - }, - "tenant": { - "tenant": "Īrnieks", - "tenants": "Īrnieki", - "management": "Īrnieku pārvaldība", - "add": "Pievienot īrnieku", - "admins": "Administrātori", - "manage-tenant-admins": "Pārvaldīt īrnieku administrātorus", - "delete": "Dzēst īrnieku", - "add-tenant-text": "Pievienot jaunu īrnieku", - "no-tenants-text": "Nav īrnieki atrasti", - "tenant-details": "Īrnieka detaļas", - "delete-tenant-title": "Vai esat pārliecināts, ka vēlaties dzēst īrnieku '{{tenantTitle}}'?", - "delete-tenant-text": "Esiet uzmanīgs, pēc apstiprinājuma īrnieks un tā saistītie dati nebūs atjaunojami.", - "delete-tenants-title": "Vai esat pārliecināts, ka vēlaties dzēst { count, plural, 1 {1 tenant} other {# īrniekus} }?", - "delete-tenants-action-title": "Dzēst { count, plural, 1 {1 tenant} other {# īrniekus} }", - "delete-tenants-text": "Esiet uzmanīgs, pēc apstiprinājuma visi atlasītie īrnieki tiks noņemti un saistītie dati nebūs atjaunojami.", - "title": "Virsraksts", - "title-required": "Virsraksts ir nepieciešams.", - "description": "Apraksts", - "details": "Detaļas", - "events": "Notikumi", - "copyId": "Kopēt īrnieka Id", - "idCopiedMessage": "Īrnieka Id ir kopēta uz starpliktuvi", - "select-tenant": "Atlasīt īrnieku", - "no-tenants-matching": "Nav īrnieku saderības '{{entity}}' atrastas.", - "tenant-required": "Īrnieks ir nepieciešams" - }, - "timeinterval": { - "seconds-interval": "{ seconds, plural, 1 {1 second} other {# sekundes} }", - "minutes-interval": "{ minutes, plural, 1 {1 minute} other {# minūtes} }", - "hours-interval": "{ hours, plural, 1 {1 hour} other {# stundas} }", - "days-interval": "{ days, plural, 1 {1 day} other {# dienas} }", - "days": "Dienas", - "hours": "Stundas", - "minutes": "Minūtes", - "seconds": "Sekundes", - "advanced": "Pieredzējis lietotājs" - }, - "timewindow": { - "days": "{ days, plural, 1 { day } other {# dienas } }", - "hours": "{ hours, plural, 0 { hour } 1 {1 hour } other {# stundas } }", - "minutes": "{ minutes, plural, 0 { minute } 1 {1 minute } other {# minūtes } }", - "seconds": "{ seconds, plural, 0 { second } 1 {1 second } other {# sekundes } }", - "realtime": "Reālajā laikā", - "history": "Vēsture", - "last-prefix": "Pēdējās", - "period": "No {{ startTime }} to {{ endTime }}", - "edit": "Rediģēt laika logu", - "date-range": "Datumu diapazons", - "last": "Pēdējās", - "time-period": "Laika periods" - }, - "user": { - "user": "Lietotājs", - "users": "Lietotāji", - "customer-users": "Klienta lietotāji", - "tenant-admins": "Īrnieka administrātori", - "sys-admin": "Sistēmas administrātori", - "tenant-admin": "Īrnieka administrātors", - "customer": "Klients", - "anonymous": "Anonīmi", - "add": "Pievienot lietotāju", - "delete": "Dzēst lietotāju", - "add-user-text": "Pievienot jaunu lietotāju", - "no-users-text": "Nav lietotāji atrasti", - "user-details": "Lietotāja detaļas", - "delete-user-title": "Vai esat pārliecināts, ka vēlaties dzēst lietotāju '{{userEmail}}'?", - "delete-user-text": "Esiet uzmanīgs, pēc apstiprinājuma lietotājs un tā saistītie dati nebūs atjaunojami.", - "delete-users-title": "Vai esat pārliecināts, ka vēlaties dzēst { count, plural, 1 {1 user} other {# lietotājus} }?", - "delete-users-action-title": "Dzēst { count, plural, 1 {1 user} other {# lietotājus} }", - "delete-users-text": "Esiet uzmanīgs, pēc apstiprinājuma visi atlasītie lietotāji tiks noņemti un to saistītie dati nebūs atjaunojami.", - "activation-email-sent-message": "Aktivizācijas email ir sekmīgi nosūtīts!", - "resend-activation": "Atkārtoti nosūtīt aktivizāciju", - "email": "Email", - "email-required": "Email ir nepieciešams.", - "invalid-email-format": "Invalīds email formāts.", - "first-name": "Vārds", - "last-name": "Uzvārds", - "description": "Apraksts", - "default-dashboard": "Defaultais panelis", - "always-fullscreen": "Vienmēr pilnekrāna", - "select-user": "Izvēlēties lietotāju", - "no-users-matching": "Nav lietotāju atbilstības '{{entity}}' atrastas.", - "user-required": "Lietotājs ir nepieciešams", - "activation-method": "Aktivizācijas veids", - "display-activation-link": "Parādīt aktivizācijas saiti", - "send-activation-mail": "Nosūtīt aktivizācijas email", - "activation-link": "Lietotāja aktivizācijas saite", - "activation-link-text": "Lai aktivizētu lietotāju, lieto sekojošo aktivizācijas saiti :", - "copy-activation-link": "Kopēt aktivizācijas saiti", - "activation-link-copied-message": "Lietotāja aktivizācijas saite ir kopēta uz starpliktuvi", - "details": "Detaļas", - "login-as-tenant-admin": "Login kā īrnieka administrātors", - "login-as-customer-user": "Login kā klienta lietotājs" - }, - "value": { - "type": "Vērtības tips", - "string": "Teksts", - "string-value": "Teksta informācija", - "integer": "Skaitlis", - "integer-value": "Skaitļa vērtība", - "invalid-integer-value": "Invalīda skaitļa vērtība", - "double": "Skaitlis ar cipariem aiz komata", - "double-value": "Skaitļa ar cipariem aiz komata vērtība", - "boolean": "ir/nav", - "boolean-value": "ir/nav vērtības", - "false": "Nepareizi", - "true": "Patiesi", - "long": "Ilgāk" - }, - "widget": { - "widget-library": "Logrīku bibliotēka", - "widget-bundle": "Logrīku apkopojums", - "select-widgets-bundle": "Atlasīt logrīku apkopojumu", - "management": "Logrīku pārvaldība", - "editor": "Logrīku rediģētājs", - "widget-type-not-found": "Problēma ielādēt logrīka konfigurāciju.
    Iespējams, ka attiecīgais logrīka tips ir noņemts.", - "widget-type-load-error": "Logrīks nav ielādēts dēl sekojošajām kļūdām:", - "remove": "Noņemt logrīku", - "edit": "rediģēt logrīku", - "remove-widget-title": "Vai esat pārliecināts, ka vēlaties noņemt logrīku '{{widgetTitle}}'?", - "remove-widget-text": "Pēc apstiprinājuma logrīks un tā saistītā informācija nebūs atjaunojama.", - "timeseries": "Laika sērijas", - "search-data": "Meklēt datus", - "no-data-found": "Nav datu atrasti", - "latest-values": "Pedējās vērtības", - "rpc": "Kontroles logrīks", - "alarm": "Trauksmes logrīks", - "static": "Statiskais logrīks", - "select-widget-type": "Atlasīt logrīka tipu", - "missing-widget-title-error": "Logrīka virsrakstam vajag būt norādītam!", - "widget-saved": "Logrīks saglabāts", - "unable-to-save-widget-error": "Nav iespēja saglabāt logrīku! Logrīkam ir kļūdas!", - "save": "Saglabāt logrīku", - "saveAs": "Saglabāt logrīku kā", - "save-widget-type-as": "Saglabāt logrīka tipu kā", - "save-widget-type-as-text": "Lūdzu ievadīt jaunu logrīka virsrakstu un/vai atlasīt mērķa logrīku apkopojumu", - "toggle-fullscreen": "Parslēgt pilnekrānu", - "run": "Palaist logrīku", - "title": "Logrīka virsraksts", - "title-required": "Logrīka virsraksts ir nepieciešams.", - "type": "Logrīka tips", - "resources": "Resursi", - "resource-url": "JavaScript/CSS URL", - "remove-resource": "Noņemt resursus", - "add-resource": "Pievienot resursus", - "html": "HTML", - "tidy": "Tidy", - "css": "CSS", - "settings-schema": "Iestatījumu shēma", - "datakey-settings-schema": "Datu atslēgas iestatījumu shēma", - "javascript": "Javascript", - "remove-widget-type-title": "Vai esat pārliecināts, ka vēlaties noņemt logrīka tipu '{{widgetName}}'?", - "remove-widget-type-text": "Pēc apstiprinājuma logrīka tips un tā saistītie dati nebūs atjaunojami.", - "remove-widget-type": "Noņemt logrīka tipu", - "add-widget-type": "Pievienot jaunu logrīka tipu", - "widget-type-load-failed-error": "Neveiksme ielādēt logrīka tipu!", - "widget-template-load-failed-error": "Neveiksme ielādēt logrīka paraugu!", - "add": "Pievienot logrīku", - "undo": "Atcelt logrīka izmaiņas", - "export": "Eksportēt logrīku" - }, - "widget-action": { - "header-button": "Logrīka galvenes poga", - "open-dashboard-state": "Navigēt uz jaunu paneļa stāvokli", - "update-dashboard-state": "Atjaunot patreizējo paneļa stāvokli", - "open-dashboard": "Navigēt uz citu paneli", - "custom": "Klienta aktivitāte", - "target-dashboard-state": "Mērķa paneļa stāvoklis", - "target-dashboard-state-required": "Mērķa paneļa stāvoklis ir nepieciešams", - "set-entity-from-widget": "Uzstādīt vienību no logrīka", - "target-dashboard": "Mērķa panelis", - "open-right-layout": "Atvērt pareizo paneļa izkārtojumu (mobilais skats)" - }, - "widgets-bundle": { - "current": "Patreizējais apkopojums", - "widgets-bundles": "Logrīku apkopojuma", - "add": "Pievienot logrīku apkopojumu", - "delete": "Dzēst logrīku apkopojumu", - "title": "Virsraksts", - "title-required": "Virsraksts ir nepieciešams.", - "add-widgets-bundle-text": "Pievienot jaunu logrīku apkopojumu", - "no-widgets-bundles-text": "Nav logrīku apkopojumi atrasti", - "empty": "Logrīku apkopojums ir tukšs", - "details": "Detaļas", - "widgets-bundle-details": "Logrīku apkopojumu detaļas", - "delete-widgets-bundle-title": "Vai esat pārliecināts, ka vēlaties dzēst logrīku apkopojumu '{{widgetsBundleTitle}}'?", - "delete-widgets-bundle-text": "Esiet uzmanīgs, pēc apstiprinājuma logrīka apkopojums un tā saistītie dati nebūs atjaunojami.", - "delete-widgets-bundles-title": "vai esat pārliecināts, ka vēlaties dzēst { count, plural, 1 {1 widgets bundle} other {# logrīku apkopojumus} }?", - "delete-widgets-bundles-action-title": "Dzēst { count, plural, 1 {1 widgets bundle} other {# logrīku apkopojumus} }", - "delete-widgets-bundles-text": "Esiet uzmanīgs, pēc apstiprinājuma visi atlasītie logrīku apkopojumi tiks noņemti un to saistītie dati nebūs atjaunojami.", - "no-widgets-bundles-matching": "Nav logrīku apkopojumu saderības '{{widgetsBundle}}' atrastas.", - "widgets-bundle-required": "Logrīku apkopojums ir nepieciešams.", - "system": "Sistēma", - "import": "Importēt logrīku apkopojumu", - "export": "Eksportēt logrīku apkopojumu", - "export-failed-error": "Nav iespējams eksportēt logrīku apkopojumu: {{error}}", - "create-new-widgets-bundle": "Radīt jaunu logrīku apkopojumu", - "widgets-bundle-file": "Logrīku apkopojumu fails", - "invalid-widgets-bundle-file-error": "Nav iespējams importēt logrīku apkopojumu: Invalīda logrīku apkopojuma datu struktūra." - }, - "widget-config": { - "data": "Dati", - "settings": "Iestatījumi", - "advanced": "Paaugstināta līmeņa", - "title": "Virsraksts", - "general-settings": "Pamata iestatījumi", - "display-title": "Rādīt virsrakstu", - "drop-shadow": "Nomest ēnas", - "enable-fullscreen": "Iespējot pilnekrānu", - "background-color": "Fona krāsa", - "text-color": "Teksta krāsa", - "padding": "Polsterējums", - "margin": "Robežas", - "widget-style": "Logrīka stils", - "title-style": "Virsraksta stils", - "mobile-mode-settings": "Mobilās modes iestatījumi", - "order": "Pasūtīt", - "height": "Augstums", - "units": "Papildus simbols vērtības atrādīšanai", - "decimals": "Ciparu skaits pēc komata", - "timewindow": "Laika logs", - "use-dashboard-timewindow": "Lietot paneļa laika logu", - "display-timewindow": "Rādīt laika logu", - "display-legend": "Rādīt leģendu", - "datasources": "Datu avoti", - "maximum-datasources": "Maksimums { count, plural, 1 {1 datasource is allowed.} other {# datu avoti atļautie} }", - "datasource-type": "Tips", - "datasource-parameters": "Parametri", - "remove-datasource": "Noņemt datu avotu", - "add-datasource": "Pievienot datu avotu", - "target-device": "Mērķa iekārta", - "alarm-source": "Trauksmes avots", - "actions": "Aktivitātes", - "action": "Aktivitātes", - "add-action": "Pievienot aktivitāti", - "search-actions": "Meklēt aktivitātes", - "action-source": "Aktivitāšu avots", - "action-source-required": "Aktivitāšu avoti ir nepieciešami.", - "action-name": "Nosaukums", - "action-name-required": "Aktitiāšu nosaukums ir nepieciešams.", - "action-name-not-unique": "Cita aktivitāte ar tādu pašu nosaukumu jau eksistē.
    Aktitivātes nosaukumam ir jābūt unikālam vienā aktivitātes avotā.", - "action-icon": "Ikona", - "action-type": "Tips", - "action-type-required": "Aktivitātes tips ir nepieciešams.", - "edit-action": "Rediģēt aktivitāti", - "delete-action": "Dzēst aktivitāti", - "delete-action-title": "Dzēst logrīka aktivitāti", - "delete-action-text": "Vai esat pārliecināts, ka vēlaties dzēst logrīka aktivitāti ar nosaukumu '{{actionName}}'?" - }, - "widget-type": { - "import": "Importēt logrīka tipu", - "export": "Eksportēt logrīka tipu", - "export-failed-error": "Nav iespējams eksportēt logrīka tipu: {{error}}", - "create-new-widget-type": "Radīt jaunu logrīka tipu", - "widget-type-file": "Logrīka tipa fails", - "invalid-widget-type-file-error": "Nav iespējams importēt logrīka tipu: Invalīda logrīka tipa datu struktūra." - }, - "widgets": { - "date-range-navigator": { - "localizationMap": { - "Sun": "Svētdiena", - "Mon": "Pirmdiena", - "Tue": "Otrdiena", - "Wed": "Trešdiena", - "Thu": "Ceturdiena", - "Fri": "Piekdiena", - "Sat": "Sestdiena", - "Jan": "Janvāris", - "Feb": "Februāris", - "Mar": "Marts", - "Apr": "Aprīlis", - "May": "Maijs", - "Jun": "Jūnijs", - "Jul": "Jūlijs", - "Aug": "Augusts", - "Sep": "Septembris", - "Oct": "Oktobris", - "Nov": "Novembris", - "Dec": "Decembris", - "January": "Janvāris", - "February": "Februāris", - "March": "Marts", - "April": "Aprīlis", - "June": "Jūnijs", - "July": "Jūlijs", - "August": "Augusts", - "September": "Septembris", - "October": "Oktobris", - "November": "Novembris", - "December": "Decembris", - "Custom Date Range": "Lietotāja datu diapazons", - "Date Range Template": "Datu diapazona templeits", - "Today": "Šodien", - "Yesterday": "Vakardien", - "This Week": "Šī nedēļa", - "Last Week": "Pēdējā nedēļa", - "This Month": "Šis mēnesis", - "Last Month": "Pēdējais mēnesis", - "Year": "Gads", - "This Year": "Šis gads", - "Last Year": "Pagājušais gads", - "Date picker": "Datu atlasītājs", - "Hour": "Stunda", - "Day": "Diena", - "Week": "Nedēļa", - "2 weeks": "2 Nedēļas", - "Month": "Mēnesis", - "3 months": "3 Mēneši", - "6 months": "6 Mēneši", - "Custom interval": "Klienta intervāls", - "Interval": "Intervāls", - "Step size": "Soļa lielums", - "Ok": "Ok" - } - } - }, - "icon": { - "icon": "Ikona", - "select-icon": "Atlasīt ikonas", - "material-icons": "Materiālu ikonas", - "show-all": "Rādīt visas ikonas" - }, - "custom": { - "widget-action": { - "action-cell-button": "Aktivitātes šunas poga", - "row-click": "Uz rindas klikšķis", - "polygon-click": "Uz daudzstūra klikšķis", - "marker-click": "Uz marķiera klikšķis", - "tooltip-tag-action": "Rīku padomu darbība", - "node-selected": "Uz atlasīto nodi", - "element-click": "HTML elementa klikšķis" - } - }, - "language": { - "language": "Valodas" - } -} diff --git a/ui/src/app/locale/locale.constant-ro_RO.json b/ui/src/app/locale/locale.constant-ro_RO.json deleted file mode 100644 index 0ec2c81794..0000000000 --- a/ui/src/app/locale/locale.constant-ro_RO.json +++ /dev/null @@ -1,1798 +0,0 @@ -{ - "access": { - "unauthorized": "Neautorizat", - "unauthorized-access": "Acces Neautorizat", - "unauthorized-access-text": "Pentru a accesa această resursă, utilizatorul trebuie să fie identificat", - "access-forbidden": "Acces Interzis", - "access-forbidden-text": "Nu ai drept de acces la această resursă!
    Pentru a obţine accesul, identifică-te cu alt nume de utilizator", - "refresh-token-expired": "Sesiunea a expirat", - "refresh-token-failed": "Sesiunea nu poate fi reîncărcată" - }, - "action": { - "activate": "Activează", - "suspend": "Suspendă", - "save": "Salvează", - "saveAs": "Salvează Cu Alt Nume", - "cancel": "Renunţă", - "ok": "OK", - "delete": "Şterge", - "add": "Adaugă", - "yes": "Da", - "no": "Nu", - "update": "Actualizează", - "remove": "Elimină", - "search": "Caută", - "clear-search": "Resetează Căutarea", - "assign": "Repartizează", - "unassign": "Şterge Repartizarea", - "share": "Partajare", - "make-private": "Declară Privat", - "apply": "Aplică", - "apply-changes": "Aplică Schimbările", - "edit-mode": "Mod Editare", - "enter-edit-mode": "Mod Editare", - "decline-changes": "Refuză Schimbările", - "close": "Închide", - "back": "Înapoi", - "run": "Execută-Rulează", - "sign-in": "Înregistrează Cont Nou", - "edit": "Editează", - "view": "Vizualizează", - "create": "Creează", - "drag": "Trage", - "refresh": "Reactualizează", - "undo": "Anulează Ultima Comandă", - "copy": "Copiere", - "paste": "Lipire", - "copy-reference": "Copiere Referință", - "paste-reference": "Lipire Referință", - "import": "Import", - "export": "Export", - "share-via": "Distribuie prin {{provider}}", - "continue": "Continuă", - "discard-changes": "Anulează Schimbări" - }, - "aggregation": { - "aggregation": "Agregare", - "function": "Funcţie Agregare Date", - "limit": "Valori Maxime", - "group-interval": "Interval Grupare", - "min": "Minim", - "max": "Maxim", - "avg": "Medie", - "sum": "Sumă", - "count": "Numără", - "none": "Nimic" - }, - "admin": { - "general": "General", - "general-settings": "Setări Generale", - "outgoing-mail": " Server eMail", - "outgoing-mail-settings": "Setări Pentru : Outgoing Mail Server", - "system-settings": "Setări Sistem", - "test-mail-sent": "Mesajul de test setări pentru email a fost trimis cu succes", - "base-url": "Adresa De Bază URL", - "base-url-required": "Adresa de bază URL este obligatorie", - "mail-from": "Mesaj eMail de la expeditor", - "mail-from-required": "Adresa eMail a expeditorului este obligatorie", - "smtp-protocol": "Setări Protocol SMTP", - "smtp-host": "Adresă SMTP", - "smtp-host-required": "Adresa SMTP este obligatorie", - "smtp-port": "Port SMTP", - "smtp-port-required": "Trebuie să precizaţi un port SMTP", - "smtp-port-invalid": "Textul introdus nu pare să fie al unui port SMTP", - "timeout-msec": "Timp expirare (milisecunde)", - "timeout-required": "Timpul de expirare este obligatoriu", - "timeout-invalid": "Timpul de expirare nu pare să fie valid", - "enable-tls": "Permite TLS", - "send-test-mail": "Trimite mesaj eMail test", - "security-settings": "Setări Securitate", - "password-policy": "Reguli Pentru Definirea Parolei", - "minimum-password-length": "Numărul Minim De Caractere Al Parolei", - "minimum-password-length-required": "Numărul minim de caractere al parolei este obligatoriu", - "minimum-password-length-range": "Numărul minim de caractere al parolei trebuie să fie între 5 - 50", - "minimum-uppercase-letters": "Numărul Minim De Caractere Scrise Cu MAJUSCULĂ Din Parolă", - "minimum-uppercase-letters-range": "Numărul minim de caractere scrise cu majusculă din parolă nu poate fi negativ", - "minimum-lowercase-letters": "Numărul Minim De Caractere Scrise Cu Literă mică Din Parolă", - "minimum-lowercase-letters-range": "Numărul minim de caractere scrise cu literă mică din parolă nu poate fi negativ", - "minimum-digits": "Numărul Minim De Cifre Din Parolă", - "minimum-digits-range": "Numărul minim de cifre din parolă nu poate fi negativ", - "minimum-special-characters": "Numărul Minim De Caractere Speciale Din Parolă", - "minimum-special-characters-range": "Numărul minim de caractere speciale din parolă nu poate fi negativ", - "password-expiration-period-days": "Perioada de expirare a parolei (zile)", - "password-expiration-period-days-range": "Perioada de expirare a parolei (zile) nu poate fi negativă", - "password-reuse-frequency-days": "Frecvenţa de refolosire a parolei (zile)", - "password-reuse-frequency-days-range": "Frecvenţa de refolosire a parolei(zile) nu poate fi negativă", - "general-policy": "Reguli Generale", - "max-failed-login-attempts": "Numărul maxim de încercări eşuate de accesare a paginii înainte de blocarea contului", - "minimum-max-failed-login-attempts-range": "Numărul maxim de încercări eşuate de accesare a paginii nu poate fi negativ", - "user-lockout-notification-email": "În Cazul Blocării Contului, Trimite eMail De Notificare" - }, - "alarm": { - "alarm": "Alarmă", - "alarms": "Alarme", - "select-alarm": "Selectează Alarmă", - "no-alarms-matching": "Nu au fost găsite alarme pentru '{{entity}}'", - "alarm-required": "Alarma este obligatorie", - "alarm-status": "Stare Alarmă", - "search-status": { - "ANY": "Oricare", - "ACTIVE": "Activă", - "CLEARED": "Ştearsă", - "ACK": "Observată", - "UNACK": "Neobservată" - }, - "display-status": { - "ACTIVE_UNACK": "Activă Neobservată", - "ACTIVE_ACK": "Activă Observată", - "CLEARED_UNACK": "Ștearsă Neobservată", - "CLEARED_ACK": "Ștearsă Observată" - }, - "no-alarms-prompt": "NiciO Alarmă Găsită", - "created-time": "Data Creării", - "type": "Tipul", - "severity": "Urgenţa", - "originator": "Iniţiator", - "originator-type": "Tip Iniţiator", - "details": "Detalii", - "status": "Stare", - "alarm-details": "Detalii Alarmă", - "start-time": "Început", - "end-time": "Sfârşit", - "ack-time": "Data Observării", - "clear-time": "Data Ştergerii", - "severity-critical": "Critică", - "severity-major": "Majoră", - "severity-minor": "Minoră", - "severity-warning": "Avertizare", - "severity-indeterminate": "Nedeterminată", - "acknowledge": "Marchează Observat", - "clear": "Şterge", - "search": "Caută Alarme", - "selected-alarms": "{ count, plural, 1 {o alarmă} other {# alarme} } selectate", - "no-data": "Nu există date de afişat", - "polling-interval": "Interval actualizare alarme (secunde)", - "polling-interval-required": "Intervalul pentru actualizarea alarmelor este obligatoriu", - "min-polling-interval-message": "Valoarea minimă permisă pentru interval actualizare alarme este o secundă", - "aknowledge-alarms-title": "Ai selectat { count, plural, 1 {o alarmă} other {# alarme} }", - "aknowledge-alarms-text": "Sigur vrei să marchezi ca 'Observat' { count, plural, 1 {o alarmă} other {# alarme} }?", - "aknowledge-alarm-title": "Alarmă Observată", - "aknowledge-alarm-text": "Sigur vrei să marchezi alarma ca 'Observat'?", - "clear-alarms-title": "Şterge { count, plural, 1 {o alarmă} other {# alarme} }", - "clear-alarms-text": "Sigur vrei să ștergi { count, plural, 1 {o alarmă} other {# alarme} }?", - "clear-alarm-title": "Şterge Alarma", - "clear-alarm-text": "Sigur vrei să ștergi alarma?", - "alarm-status-filter": "Stare Filtre Alarmă", - "max-count-load": "Număr maxim de alarme înregistrate (0=nelimitat)", - "max-count-load-required": "Numărul maxim de alarme înregistrate este obligatoriu", - "max-count-load-error-min": "Numărul maxim de alarme este 0", - "fetch-size": "Număr alarme afişate", - "fetch-size-required": "Numărul alarmelor afişate este obligatoriu", - "fetch-size-error-min": "Valoarea minimă este 10" - }, - "alias": { - "add": "Adaugă Pseudonim", - "edit": "Editează Pseudonim", - "name": "Denumire Pseudonim", - "name-required": "Denumirea pseudonimului este obligatorie", - "duplicate-alias": "Există deja un pseudonim cu aceeaşi denumire", - "filter-type-single-entity": "O Singură Entitate", - "filter-type-entity-list": "Listă Entităţi", - "filter-type-entity-name": "Nume Entitate", - "filter-type-state-entity": "Entitate din starea panoului", - "filter-type-state-entity-description": "Entitate luată din parametrii stării panoului", - "filter-type-asset-type": "Tip Proprietate", - "filter-type-asset-type-description": "Proprietăţi de tip: '{{assetType}}'", - "filter-type-asset-type-and-name-description": "proprietăţi de tipul: '{{assetType}}' a căror denumire începe cu: '{{prefix}}'", - "filter-type-device-type": "Tip Dispozitiv", - "filter-type-device-type-description": "Dispozitive tip: '{{deviceType}}'", - "filter-type-device-type-and-name-description": "Dispozitive Tip: '{{deviceType}}' a căror denumire începe cu: '{{prefix}}'", - "filter-type-entity-view-type": "Tip entitate definită", - "filter-type-entity-view-type-description": "Tip entități definite: '{{entityView}}'", - "filter-type-entity-view-type-and-name-description": "Entități definite de tip: '{{entityView}}' a căror denumire începe cu : '{{prefix}}'", - "filter-type-relations-query": "Relaţii Interogare", - "filter-type-relations-query-description": "{{entities}} care au relații tip {{relationType}} în direcția {{direction}} {{rootEntity}}", - "filter-type-asset-search-query": "Proprietate Asset search query", - "filter-type-asset-search-query-description": "Proprietăţi de tip {{assetTypes}} care au relații tip {{relationType}} în direcția {{direction}} {{rootEntity}}", - "filter-type-device-search-query": "Criteriu Căutare Dispozitiv", - "filter-type-device-search-query-description": "Dispozitive de tip {{deviceTypes}} care au relații tip {{relationType}} în direcția {{direction}} {{rootEntity}}", - "filter-type-entity-view-search-query": "Criteriu căutare entitate definită", - "filter-type-entity-view-search-query-description": "Entități definite de tip {{entityViewTypes}} care au relații tip {{relationType}} în direcția {{direction}} {{rootEntity}}", - "entity-filter": "Filtru Entitate", - "resolve-multiple": "Rezolvă Ca Entităţi Multiple", - "filter-type": "Tip Filtru", - "filter-type-required": "Tipul filtrului este obligatoriu", - "entity-filter-no-entity-matched": "Nu au fost găsite entităţi corespunzătoare filtrului specificat", - "no-entity-filter-specified": "Nu a fost specificat filtru pentru entitate", - "root-state-entity": "Foloseşte Entitatea Stare Panou Ca Origine", - "root-entity": "Entitate Rădăcină", - "state-entity-parameter-name": "Nume Parametru Stare Entitate", - "default-state-entity": "Stare Entitate Implicită", - "default-entity-parameter-name": "Implicită", - "max-relation-level": "Nivel Maxim Relaţie", - "unlimited-level": "Nivel Nelimitat", - "state-entity": "Entitate Stare Panou", - "all-entities": "Toate Entităţile", - "any-relation": "Oricare" - }, - "asset": { - "asset": "Proprietate", - "assets": "Proprietăţi", - "management": "Administrare Proprietăți", - "view-assets": "Vezi Proprietăţi", - "add": "Adaugă Proprietate", - "assign-to-customer": "Repartizează Proprietate", - "assign-asset-to-customer": "Repartizează proprietăţi clientului", - "assign-asset-to-customer-text": "Selectează proprietăţile care vor fi repartizate clientului", - "no-assets-text": "Nu au fost găsite proprietăţi", - "assign-to-customer-text": "Selectează clientul căruia îi vor fi repartizate proprietăţile", - "public": "Publică", - "assignedToCustomer": "Repartizată clientului", - "make-public": "Declară proprietate publică", - "make-private": "Declară proprietate privată", - "unassign-from-customer": "Şterge repartizare client", - "delete": "Şterge proprietate", - "asset-public": "Proprietate publică", - "asset-type": "Tip Proprietate", - "asset-type-required": "Tipul proprietății este obligatoriu", - "select-asset-type": "Alege tipul proprietății", - "enter-asset-type": "Introdu tipul proprietății", - "any-asset": "Orice Proprietate", - "no-asset-types-matching": "Nu a fost găsită nicio proprietate conținând '{{entitySubtype}}'", - "asset-type-list-empty": "Nu a fost selectat niciun tip de proprietate", - "asset-types": "Tipuri Proprietate", - "name": "Nume Proprietate", - "name-required": "Numele este obligatoriu", - "description": "Descriere Proprietate", - "type": "Tip Proprietate", - "type-required": "Tipul proprietății este obligatoriu", - "details": "Detalii Proprietate", - "events": "Evenimente", - "add-asset-text": "Adaugă proprietate", - "asset-details": "Detalii proprietate", - "assign-assets": "Repartizează proprietăţi", - "assign-assets-text": "Repartizează { count, plural, 1 {o proprietate} other {# proprietăţi} } clientului", - "delete-assets": "Şterge proprietăţi", - "unassign-assets": "Şterge repartizare proprietăţi", - "unassign-assets-action-title": "Şterge repartizare { count, plural, 1 {o proprietate} other {# proprietăţi} } clientului", - "assign-new-asset": "Repartizează proprietate nouă", - "delete-asset-title": "Sigur vrei să ștergi '{{assetName}}'?", - "delete-asset-text": "ATENŢIE! După confirmare, proprietatea şi toate datele referitoare la aceasta, vor fi șterse IREVERSIBIL!", - "delete-assets-title": "Sigur vrei să ștergi { count, plural, 1 {o proprietate} other {# proprietăţi} }?", - "delete-assets-action-title": "Ştergi { count, plural, 1 {o proprietate} other {# proprietăţi} }", - "delete-assets-text": "ATENŢIE! După confirmare, toate proprietăţile selectate şi toate datele referitoare la aceastea, vor fi șterse IREVERSIBIL!", - "make-public-asset-title": "Sigur vrei ca proprietatea '{{assetName}}' să devină publică ", - "make-public-asset-text": "ATENŢIE! După confirmare, proprietatea selectată şi toate datele referitoare la aceasta vor putea fi accesate de către oricine", - "make-private-asset-title": "Sigur vrei ca proprietatea '{{assetName}}' să devină privată?", - "make-private-asset-text": "ATENŢIE! După confirmare, proprietatea selectată şi toate datele referitoare la aceasta vor putea fi accesate doar de către proprietar", - "unassign-asset-title": "Sigur vrei să ştergi repartizarea pentru proprietatea '{{assetName}}'?", - "unassign-asset-text": "ATENŢIE! După confirmare, repartizarea proprietăţii nu va mai putea fi accesată de către client", - "unassign-asset": "Şterge repartizare proprietate", - "unassign-assets-title": "Sigur vrei să ştergi repartizarea pentru { count, plural, 1 {o proprietate} other {# proprietăţi} }?", - "unassign-assets-text": "ATENŢIE! După confirmare, repartizările proprietăţilor selectate nu vor mai putea fi accesate de către client", - "copyId": "Copiază ID proprietate", - "idCopiedMessage": "ID-ul proprietăţii a fost copiat în clipboard", - "select-asset": "Selectează proprietate", - "no-assets-matching": "Nu au fost găsite proprietăţi al căror nume conține '{{entity}}'", - "asset-required": "Proprietatea este obligatorie", - "name-starts-with": "Numele proprietăţii începe cu", - "import": "Importă proprietăţi", - "asset-file": "Fişier Proprietăţi", - "label": "Eticheta" - }, - "attribute": { - "attributes": "Atribute", - "latest-telemetry": "Ultimele Date Telemetrice", - "attributes-scope": "Scop Atribute Entitate", - "scope-latest-telemetry": "Ultimele Date Telemetrice", - "scope-client": "Atribute Client", - "scope-server": "Atribute Server", - "scope-shared": "Atribute Partajate", - "add": "Adaugă Atribut", - "key": "Cheie", - "last-update-time": "Ultima Actualizare", - "key-required": "Cheia atributului este obligatorie", - "value": "Valoare", - "value-required": "Valoarea atributului este obligatorie", - "delete-attributes-title": "Sigur vrei să ștergi { count, plural, 1 {un atribut} other {# atribute} }?", - "delete-attributes-text": "ATENŢIE! După confirmare, toate atributele selectate vor fi şterse", - "delete-attributes": "Şterge Atribute", - "enter-attribute-value": "Specifică Valoarea Atributului", - "show-on-widget": "Afişează În Widget", - "widget-mode": "Modul Widget", - "next-widget": "Widget Următor ", - "prev-widget": "Widget Precedent", - "add-to-dashboard": "Adaugă în panou", - "add-widget-to-dashboard": "Adaugă Widget În Panou", - "selected-attributes": "{ count, plural, 1 {un atribut} other {# atribute} } selectate", - "selected-telemetry": "{ count, plural, 1 {o unitate telemetrică} other {# unităţi telemetrice} } selectate" - }, - "audit-log": { - "audit": "Audit", - "audit-logs": "Jurnale Audit", - "timestamp": "Cronologie", - "entity-type": "Tip Entitate", - "entity-name": "Denumire Entitate", - "user": "Utilizator", - "type": "Tip", - "status": "Stare", - "details": "Detalii", - "type-added": "Adăugat", - "type-deleted": "Şters", - "type-updated": "Actualizat", - "type-attributes-updated": "Atribute actualizate", - "type-attributes-deleted": "Atribute şterse", - "type-rpc-call": "RPC call", - "type-credentials-updated": "Acreditări actualizate", - "type-assigned-to-customer": "Repartizat către client", - "type-unassigned-from-customer": "Anulează Repartizarea Către Client", - "type-activated": "Activat", - "type-suspended": "Suspendat", - "type-credentials-read": "Acreditări citite", - "type-attributes-read": "Atribute citite", - "type-relation-add-or-update": "Relaţie actualizată", - "type-relation-delete": "Relaţie ștearsă", - "type-relations-delete": "Toate relaţiile șterse", - "type-alarm-ack": "Confirmat", - "type-alarm-clear": "Şters", - "type-login": "Intră În Cont", - "type-logout": "Parăseşte Contul", - "type-lockout": "Blocat", - "status-success": "Succes", - "status-failure": "Eșec", - "audit-log-details": "Detalii Jurnale Audit", - "no-audit-logs-prompt": "Nu Au Fost Găsite Jurnale", - "action-data": "Detalii Acțiune", - "failure-details": "Detalii Eșec", - "search": "Caută Jurnale Audit", - "clear-search": "Resetează Căutarea" - }, - "confirm-on-exit": { - "message": "Au rămas modificări nesalvate. Doriţi să părăsiţi această pagină fară a salva modificările?", - "html-message": "Au rămas modificări nesalvate.
    Doriţi să părăsiţi această pagină fară a salva modificările?", - "title": "Modificări Nesalvate" - }, - "contact": { - "country": "Ţară", - "city": "Oraş", - "state": "Judeţ", - "postal-code": "Cod Poştal", - "postal-code-invalid": "Cod poștal incorect", - "address": "Adresă 1", - "address2": "Adresă 2", - "phone": "Telefon", - "email": "eMail", - "no-address": "Fără Adresă" - }, - "common": { - "username": "Nume Utilizator", - "password": "Parola", - "enter-username": "Introdu nume utilizator", - "enter-password": "Introdu parola", - "enter-search": "Definește căutarea" - }, - "content-type": { - "json": "Json", - "text": "Text", - "binary": "Binary (Base64)" - }, - "customer": { - "customer": "Client", - "customers": "Clienţi", - "management": "Administrare Clienţi", - "dashboard": "Panou Control Client", - "dashboards": "Panouri Control Clienţi", - "devices": "Dispozitive Client", - "entity-views": "Entități Definite Client", - "assets": "Proprietăţi Client", - "public-dashboards": "Panouri Control Publice", - "public-devices": "Dispozitive Publice", - "public-assets": "Proprietăţi Publice", - "public-entity-views": "Definiții Entități Publice Client", - "add": "Adaugă Client", - "delete": "Şterge Client", - "manage-customer-users": "Administrare Utilizatori Client", - "manage-customer-devices": "Administrare Dispozitive Client", - "manage-customer-dashboards": "Administrare Panouri Control Client", - "manage-public-devices": "Administrare Dispozitive Publice", - "manage-public-dashboards": "Administrare Panouri Control Publice", - "manage-customer-assets": "Administrare Proprietăţi Client", - "manage-public-assets": "Administrare Proprietăţi Publice", - "add-customer-text": "Adăugare Client Nou", - "no-customers-text": "Nu au fost găsiţi clienţi", - "customer-details": "Detalii Client", - "delete-customer-title": "Vrei să ștergi clientul '{{customerTitle}}'?", - "delete-customer-text": "ATENŢIE! După confirmare, clientul şi toate datele referitoare la acesta vor fi șterse IREVERSIBIL!", - "delete-customers-title": "Vrei să ștergi { count, plural, 1 {un client} other {# clienţi} }?", - "delete-customers-action-title": "Şterge { count, plural, 1 {un client} other {# clienţi} }", - "delete-customers-text": "ATENŢIE! După confirmare, toaţi clienţii selectate şi toate datele referitoare la aceaştia, vor fi șterse IREVERSIBIL!", - "manage-users": "Administrare Utilizatori", - "manage-assets": "Administrare Proprietăţi", - "manage-devices": "Administrare Dispozitive", - "manage-dashboards": "Administrare Panouri Control Publice", - "title": "Titlu", - "title-required": "Titlul este obligatoriu", - "description": "Descriere", - "details": "Detalii", - "events": "Evenimente", - "copyId": "Copie ID Client", - "idCopiedMessage": "ID Client A Fost Copiat In Clipboard", - "select-customer": "Selectează Client", - "no-customers-matching": "Niciun client nu se potrivește cu:'{{entity}}'", - "customer-required": "Clientul este obligatoriu", - "select-default-customer": "Selecteaza Client Implicit", - "default-customer": "Client Implicit", - "default-customer-required": "Clientul implicit este obligatoriu pentru a putea depana panoul de control la nivel de PROPRIETAR" - }, - "datetime": { - "date-from": "Dată început:", - "time-from": "Oră început:", - "date-to": "Dată sfârșit:", - "time-to": "Oră sfârșit:" - }, - "dashboard": { - "dashboard": "Panou", - "dashboards": "Panouri", - "management": "Administrare Panouri", - "view-dashboards": "Afişează Panouri", - "add": "Adaugă Panou", - "assign-dashboard-to-customer": "Repartizează Panou/Panouri Clientului", - "assign-dashboard-to-customer-text": "Selectează panourile care vor fi repartizate clientului", - "assign-to-customer-text": "Alege Clientul Căruia îi vor fi repartizate Panourile", - "assign-to-customer": "Repartizează Clientului", - "unassign-from-customer": "Şterge Repartizarea Către Client", - "make-public": "Declară Panou Public", - "make-private": "Declară Panou Privat", - "manage-assigned-customers": "Administrare Clienţi Repartizaţi", - "assigned-customers": "Clienţi Repartizaţi", - "assign-to-customers": "Repartizează Panouri Către Clienţi", - "assign-to-customers-text": "Alege clienţii cărora le vor fi repartizate panourile", - "unassign-from-customers": "Şterge repartizarea panourilor către clienţi", - "unassign-from-customers-text": "Alege clienţii cărora le va fi ștearsă repartizarea panourilor", - "no-dashboards-text": "Nu au fost găsite panouri", - "no-widgets": "Nu sunt widgeturi configurate", - "add-widget": "Adăugare Widget Nou", - "title": "Titlu Widget", - "select-widget-title": "Alege Widget", - "select-widget-subtitle": "Listă Tipuri Widget", - "delete": "Şterge Panou", - "title-required": "Titlul este obligatoriu", - "description": "Descriere", - "details": "Detalii", - "dashboard-details": "Detalii Panou", - "add-dashboard-text": "Adăugare Panou Nou", - "assign-dashboards": "Repartizare Panouri", - "assign-new-dashboard": "Repartizare Panou Nou", - "assign-dashboards-text": "Repartizează { count, plural, 1 {un panou} other {# panouri} } clienţilor", - "unassign-dashboards-action-text": "Şterge repartizare { count, plural, 1 {un panou} other {# panouri} } către clienți", - "delete-dashboards": "Şterge Panouri", - "unassign-dashboards": "Şterge Repartizare Panouri", - "unassign-dashboards-action-title": "Şterge Repartizare { count, plural, 1 {un panou} other {# panouri} } către client", - "delete-dashboard-title": "Vrei să ștergi panoul '{{dashboardTitle}}'?", - "delete-dashboard-text": "ATENŢIE! După confirmare, panoul și datele aferente acestuia vor fi șterse IREVERSIBIL!", - "delete-dashboards-title": "Vrei să ștergi { count, plural, 1 {un panou} other {# panouri} }?", - "delete-dashboards-action-title": "Ştergere { count, plural, 1 {un panou} other {# panouri} }", - "delete-dashboards-text": "ATENŢIE! După confirmare, panourile selectate şi datele aferente acestora vor fi șterse IREVERSIBIL!", - "unassign-dashboard-title": "Vrei să ştergi repartizarea panoului '{{dashboardTitle}}'?", - "unassign-dashboard-text": "ATENŢIE! După confirmare, panoul nu va mai putea fi accesat de către client", - "unassign-dashboard": "Şterge Repartizare Panou", - "unassign-dashboards-title": "Vrei să ştergi repartizarea a { count, plural, 1 {un panou} other {# panouri} }?", - "unassign-dashboards-text": "ATENŢIE! După confirmare, panoul nu va mai putea fi accesat de către client", - "public-dashboard-title": "Panoul a devenit public", - "public-dashboard-text": "Panoul tău {{dashboardTitle}} a devenit public şi este accesibil la link:", - "public-dashboard-notice": "Notă: Nu uitaţi să definiţi ca publice şi dispozitivele aferente acestui panou, pentru a le face vizibile", - "make-private-dashboard-title": "Doriţi să definiţi panoul '{{dashboardTitle}}' ca privat?", - "make-private-dashboard-text": "ATENŢIE! După confirmare, panoul va putea fi accesat doar de către proprietar", - "make-private-dashboard": "Declară Panou Privat", - "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard", - "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard", - "select-dashboard": "Selectează Panou", - "no-dashboards-matching": "Nu au fost găsite panouri al căror nume conține '{{entity}}'", - "dashboard-required": "Panoul este obligatoriu", - "select-existing": "Selectează Un Panou Existent", - "create-new": "Creează Panou Nou", - "new-dashboard-title": "Denumire Panou Nou", - "open-dashboard": "Deschide Panou", - "set-background": "Setează Culoarea De Fundal (Background)", - "background-color": "Culoarea (Background)", - "background-image": "Imaginea (Background)", - "background-size-mode": "Mod Mărime (Background)", - "no-image": "Nicio imagine selectată", - "drop-image": "Trage o imagine sau alege un fişier", - "settings": "Setări", - "columns-count": "Număr Coloane", - "columns-count-required": "Numărul coloanelor este obligatoriu", - "min-columns-count-message": "Numărul minim permis de coloane este 10", - "max-columns-count-message": "Numărul maxim permis de coloane este 1000", - "widgets-margins": "Spaţiu vertical între widgeturi", - "horizontal-margin": "Spaţiu orizontal între widget-uri", - "horizontal-margin-required": "Valoarea spaţiului orizontal este obligatorie", - "min-horizontal-margin-message": "Valoarea minimă permisă pentru spaţiul orizontal între widgeturi este 0", - "max-horizontal-margin-message": "Valoarea maximă permisă pentru spaţiul orizontal între widgeturi este 0", - "vertical-margin": "Spaţiu vertical între widgeturi", - "vertical-margin-required": "Valoarea spaţiului vertical este obligatorie", - "min-vertical-margin-message": "Valoarea minimă permisă pentru spaţiul vertical între widgeturi este 0", - "max-vertical-margin-message": "Valoarea maximă permisă pentru spaţiul vertical între widgeturi este 50", - "autofill-height": "Auto Umplere Pe Înălţime", - "mobile-layout": "Setări Pagină Pentru Dispozitive Mobile", - "mobile-row-height": "Înălţimea liniei în pagina pentru dispozitive mobile (pixeli)", - "mobile-row-height-required": "Valoarea pentru înălţimea liniei este obligatorie", - "min-mobile-row-height-message": "Valoarea minimă permisă pentru înălţimea liniei în pagina pentru dispozitive mobile este 5 pixeli", - "max-mobile-row-height-message": "Valoarea maximă permisă pentru înălţimea liniei în pagina pentru dispozitive mobile este 200 pixeli", - "display-title": "Afişează Titlul Panoului", - "toolbar-always-open": "Menţine Deschisă Bara De Instrumente", - "title-color": "Culoare Titlu", - "display-dashboards-selection": "Afişează Selecţie Panouri", - "display-entities-selection": "Afişează Selecţie Entităţi", - "display-dashboard-timewindow": "Afişează Interval", - "display-dashboard-export": "Afişează Exportul", - "import": "Importă Panou", - "export": "Exportă Panou", - "export-failed-error": "Panoul nu poate fi exportat: {{error}}", - "create-new-dashboard": "Creează Panou Nou", - "dashboard-file": "Fişier Pentru Panou", - "invalid-dashboard-file-error": "Panoul nu poate fi importat; structură de date invalidă", - "dashboard-import-missing-aliases-title": "Configurează pseudonim pentru panoul importat", - "create-new-widget": "Creează Widget Nou", - "import-widget": "Importă Widget", - "widget-file": "Fişier Pentru Widget", - "invalid-widget-file-error": "Widgetul nu poate fi importat; structură de date invalidă", - "widget-import-missing-aliases-title": "Configurează pseudonim pentru widgetul importat", - "open-toolbar": "Deschide Bara De Instrumente Pentru Panou", - "close-toolbar": "Închide Bara De Instrumente", - "configuration-error": "Eroare De Configurare", - "alias-resolution-error-title": "Eroare configurare pseudonim panou", - "invalid-aliases-config": "Nu există nici un dispozitiv corespunzător filtrului de pseudonime.
    Pentru a rezolva această problemă, contactează administratorul", - "select-devices": "Selectează Dispozitiv", - "assignedToCustomer": "Repartizat Clientului", - "assignedToCustomers": "Repartizate Clienților", - "public": "Publice", - "public-link": "Adresă Pagină Publică", - "copy-public-link": "Copiază Adresa Paginii Publice", - "public-link-copied-message": "Adresa paginii publice a panoului a fost copiată în clipboard", - "manage-states": "Administrare Stări Panou", - "states": "Stări Panou", - "search-states": "Caută Stări Panou", - "selected-states": "{ count, plural, 1 {o stare panou} other {# stări panou}} selectate", - "edit-state": "Editează Stare Panou", - "delete-state": "Şterge Stare Panou", - "add-state": "Adaugă Stare Panou", - "state": "Stare Panou", - "state-name": "Denumire", - "state-name-required": "Denumirea stării panoului este obligatorie", - "state-id": "ID Stare Panou", - "state-id-required": "IDul Stării panoului este obligatoriu", - "state-id-exists": "O stare panou având acest ID este deja înregistrată", - "is-root-state": "Stare Rădăcină", - "delete-state-title": "Şterge Stare Panou", - "delete-state-text": "Sigur vrei să ștergi starea panou '{{stateName}}'?", - "show-details": "Afişează Detalii", - "hide-details": "Ascunde Detalii", - "select-state": "Selectează Stare Destinaţie", - "state-controller": "Controler Stări" - }, - "datakey": { - "settings": "Setări", - "advanced": "Avansat", - "label": "Etichetă", - "color": "Culoare", - "units": "Simbol special atașat valorii afişate", - "decimals": "Număr zecimale", - "data-generation-func": "Funcţie Generare Date", - "use-data-post-processing-func": "Utilizare Funcţie Post Procesare", - "configuration": "Configurare Chei Date", - "timeseries": "Serii Temporale", - "attributes": "Atribute", - "entity-field": "Câmp Entitate", - "alarm": "Câmpuri Alarmă", - "timeseries-required": "Seriile temporale pentru entitate sunt obligatorii", - "timeseries-or-attributes-required": "Seriile temporale sau atributele pentru entitate sunt obligatorii", - "maximum-timeseries-or-attributes": "{ count, plural, 1 { Este permisă maximum o serie temporală sau atribut} other {Sunt permise maximum # serii temporale sau atribute}}", - "alarm-fields-required": "Câmpurile pentru alarmă sunt obligatorii", - "function-types": "Tipuri Funcţie", - "function-types-required": "Tipurile de funcţie sunt obligatorii", - "maximum-function-types": "Maximum { count, plural, 1 {Este permis doar un tip de funcţie} other {Sunt permise doar # tipuri de funcţie} }", - "time-description": "Cronologie valoare curentă;", - "value-description": "valoare curentă;", - "prev-value-description": "Rezultat execuție funcţie precedentă;", - "time-prev-description": "Cronologie valoare precedentă;", - "prev-orig-value-description": "Valoare precedentă originală;" - }, - "datasource": { - "type": "Tip Sursă Date", - "name": "Denumire", - "add-datasource-prompt": "Adaugă Sursă Date" - }, - "details": { - "edit-mode": "Mod Editare", - "toggle-edit-mode": "Schimbă Mod Editare" - }, - "device": { - "device": "Dispozitiv", - "device-required": "Dispozitivul este obligatoriu", - "devices": "Dispozitive", - "management": "Administrare Dispozitive", - "view-devices": "Vizualizare Dispozitive", - "device-alias": "Pseudonim Dispozitiv", - "aliases": "Pseudonime Dispozitiv", - "no-alias-matching": "'{{alias}}' nu a fost găsit", - "no-aliases-found": "Nu au fost găsite pseudonime", - "no-key-matching": "'{{key}}' nu a fost găsită", - "no-keys-found": "Nu a fost găsită nicio cheie", - "create-new-alias": "Creează Pseudonim Nou", - "create-new-key": "Creează Cheie Nouă", - "duplicate-alias-error": "Pseudonimul ales este deja înregistrat : '{{alias}}'.
    Pseudonimele dispozitivelor trebuie să fie unice în acelaşi panou", - "configure-alias": "Configurează Pseudonim: '{{alias}}'", - "no-devices-matching": "Nu au fost găsite dispozitive al căror nume conține: '{{entity}}'", - "alias": "Pseudonim", - "alias-required": "Pseudonimul pentru dispozitiv este obligatoriu", - "remove-alias": "Şterge Pseudonim Dispozitiv", - "add-alias": "Adaugă Pseudonim Dispozitiv", - "name-starts-with": "Denumirea dispozitivului începe cu: ", - "device-list": "Listă Dispozitive", - "use-device-name-filter": "Utilizează Filtru Căutare", - "device-list-empty": "Nu este selectat niciun dispozitiv", - "device-name-filter-required": "Filtrul de căutare pentru nume dispozitiv este obligatoriu", - "device-name-filter-no-device-matched": "Nu au fost găsite dispozitive al căror nume începe cu '{{device}}'", - "add": "Adaugă Dispozitiv", - "assign-to-customer": "Repartizează Clientului", - "assign-device-to-customer": "Repartizează Dispozitiv(e) Clientului", - "assign-device-to-customer-text": "Selectează dispozitivele de repartizat clientului ", - "make-public": "Configurează Ca Dispozitiv Public", - "make-private": "Configurează Ca Dispozitiv Privat", - "no-devices-text": "Nu există dispozitive", - "assign-to-customer-text": "Selectrază clientul căruia să-i fie repartizat(e) dispozitiv(ele)", - "device-details": "Detalii Dispozitiv", - "add-device-text": "Adaugă Dispozitiv Nou", - "credentials": "Acreditări", - "manage-credentials": "Administrare Acreditări", - "delete": "Şterge Dispozitiv", - "assign-devices": "Repartizeză Dispozitive", - "assign-devices-text": "Repartizeză { count, plural, 1 {un dispozitiv} other {# dispozitive} } Clientului", - "delete-devices": "Şterge Dispozitive", - "unassign-from-customer": "Şterge repartizarea către client", - "unassign-devices": "Şterge repartizarea dispozitivelor", - "unassign-devices-action-title": "Şterge repartizarea a { count, plural, 1 {un dispozitiv} other {# dispozitive} } de la client", - "assign-new-device": "Repartizare Dispozitiv Nou", - "make-public-device-title": "Sigur vrei să faci dispozitivul '{{deviceName}}' public?", - "make-public-device-text": "După confirmare, dispozitivul și toate datele aferente acestuia vor fi făcute publice, fiind deci accesibile oricui", - "make-private-device-title": "Sigur vrei să faci dispozitivul '{{deviceName}}' privat?", - "make-private-device-text": "După confirmare, dispozitivul și toate datele aferente acestuia vor fi private, deci accesibile doar proprietarului", - "view-credentials": "Vezi Credențiale", - "delete-device-title": "Sigur vrei să ștergi dispozitivul '{{deviceName}}'?", - "delete-device-text": "ATENȚIE! După confirmare, dispozitivul împreună cu datele aferente acestuia vor fi șterse IREVERSIBIL!", - "delete-devices-title": "Sigur vrei să ștergi { count, plural, 1 {un dispozitiv} other {# dispozitive} }?", - "delete-devices-action-title": "Delete { count, plural, 1 {un dispozitiv} other {# dispozitive} }", - "delete-devices-text": "ATENȚIE! După confirmare, toate dispozitivele selectate, împreună cu datele aferente acestora, vor fi șterse IREVERSIBIL!", - "unassign-device-title": "Sigur vrei să ștergi repartizarea dispozitivului '{{deviceName}}'?", - "unassign-device-text": "ATENȚIE! După confirrmare, dispozitivul nu va mai fi accesibil clientului", - "unassign-device": "Șterge repartizare dispozitiv", - "unassign-devices-title": "Sigur vrei să ștergi repartizarea pentru { count, plural, 1 {un dispozitiv} other {# devices} }?", - "unassign-devices-text": "ATENȚIE! După confirmare, repartizarea dispozitivelor va fi ștearsă, acestea nemaifiind accesibile clientului", - "device-credentials": "Credențiale Dispozitiv", - "credentials-type": "Tip Credențiale", - "access-token": "Token Acces", - "access-token-required": "Tokenul de acces este necesar", - "access-token-invalid": "Dimensiunea tokenului de acces trebuie să fie de 1-20 caractere", - "rsa-key": "Cheie RSA publică", - "rsa-key-required": "Cheia RSA publică key este necesară", - "secret": "Cod Secret", - "secret-required": "Codul secret este necesar", - "device-type": "Tip Dispozitiv", - "device-type-required": "Tipul dispozitivului este obligatoriu", - "select-device-type": "Alege tipul dispozitivului", - "enter-device-type": "Introdu tipul dispozitivului", - "any-device": "Orice Dispozitiv", - "no-device-types-matching": "Nu au fost găsite dispozitive de tip '{{entitySubtype}}' ", - "device-type-list-empty": "Nu au fost selectate tipuri dispozitiv", - "device-types": "Tipuri dispozitiv", - "name": "Nume", - "name-required": "Numele este obligatoriu", - "description": "Descriere", - "label": "Etichetă", - "events": "Evenimente", - "details": "Detalii", - "copyId": "Copie ID Dispozitiv", - "copyAccessToken": "Copiază Token Acces", - "idCopiedMessage": "ID dispozitiv copiat în clipboard", - "accessTokenCopiedMessage": "Token acces dispozitiv copiat în clipboard", - "assignedToCustomer": "Repartizat clientului", - "unable-delete-device-alias-title": "Pseudonimul dispozitivului nu poate fi șters", - "unable-delete-device-alias-text": "Pseudonimul dispozitivului '{{deviceAlias}}' nu poate fi șters, întrucât este folosit de widget(urile):
    {{widgetsList}}", - "is-gateway": "Este gateway", - "public": "Public", - "device-public": "Dispozitivul este public", - "select-device": "Selectează Dispozitiv", - "import": "Importă Dispozitiv", - "device-file": "Fișier dispozitiv" - }, - "dialog": { - "close": "Închide Casetă Dialog" - }, - "direction": { - "column": "Coloană", - "row": "Linie" - }, - "error": { - "unable-to-connect": "Conexiunea cu serverul imposibilă! Ești conectat la Internet?", - "unhandled-error-code": "Cod eroare negestionată: {{errorCode}}", - "unknown-error": "Eroare necunoscută" - }, - "entity": { - "entity": "Entitate", - "entities": "Entități", - "aliases": "Pseudonime Entități", - "entity-alias": "Pseudonim Entitate", - "unable-delete-entity-alias-title": "Ștergerea pseudonimului entității este imposibilă", - "unable-delete-entity-alias-text": "Pseudonimul entității '{{entityAlias}}', fiind folosit de widgetul/widgeturile:
    {{widgetsList}}", - "duplicate-alias-error": "Pseudonimul entității este duplicat
    Pseudonimele entităților trebuie să fie unice, în același panou", - "missing-entity-filter-error": "Lipsă filtru pentru pseudonimul '{{alias}}'", - "configure-alias": "Configurează pseudonimul '{{alias}}'", - "alias": "Pseudonim", - "alias-required": "Pseudonimul entității este necesar", - "remove-alias": "șterge Alias Entitate", - "add-alias": "Adaugă Alias Entitate", - "entity-list": "Listă Entități", - "entity-type": "Tip Entitate", - "entity-types": "Tipuri Entități", - "entity-type-list": "Listă Tipuri Entități", - "any-entity": "Orice Entitate", - "enter-entity-type": "Introdu Tip Entitate", - "no-entities-matching": "Nu a fost găsită nicio entitate conținând '{{entity}}' ", - "no-entity-types-matching": "Nu au fost găsite tipuri de entități conținând '{{entityType}}'", - "name-starts-with": "Numele începe cu", - "use-entity-name-filter": "Folosește filtru", - "entity-list-empty": "Nicio entitate selectată", - "entity-type-list-empty": "Niciun tip entitate selectat", - "entity-name-filter-required": "Filtrul pentru nume entitate este necesar", - "entity-name-filter-no-entity-matched": "Nu a fost găsită nicio entitate al cărei nume începe cu '{{entity}}'", - "all-subtypes": "Toate", - "select-entities": "Selectează entități", - "no-aliases-found": "Niciun pseudonim găsit", - "no-alias-matching": "Nu am găsit pseudonimul '{{alias}}'", - "create-new-alias": "Creează unul nou!", - "key": "Cheie", - "key-name": "Nume Cheie", - "no-keys-found": "Nicio cheie găsită", - "no-key-matching": "Nu am găsit cheia '{{key}}'", - "create-new-key": "Creează una nouă!", - "type": "Tip", - "type-required": "Tipul entității este necesar", - "type-device": "Dispozitiv", - "type-devices": "Dispozitive", - "list-of-devices": "{ count, plural, 1 {un dispozitiv} other {Listă # dispozitive} }", - "device-name-starts-with": "Dispozitive a căror nume începe cu '{{prefix}}'", - "type-asset": "Proprietate", - "type-assets": "Proprietăți", - "list-of-assets": "{ count, plural, 1 {o proprietate} other {Listă # proprietăți} }", - "asset-name-starts-with": "Proprietate a cărei nume începe cu '{{prefix}}'", - "type-entity-view": "Entitate Definită", - "type-entity-views": "Entități Definite", - "list-of-entity-views": "{ count, plural, 1 {o entitate definită} other {Listă # entități definite} }", - "entity-view-name-starts-with": "Entități definite al căror nume începe cu '{{prefix}}'", - "type-rule": "Regulă", - "type-rules": "Reguli", - "list-of-rules": "{ count, plural, 1 {o regulă} other {Listă # reguli} }", - "rule-name-starts-with": "Reguli al căror nume începe cu '{{prefix}}'", - "type-plugin": "Plugin", - "type-plugins": "Plugin-uri", - "list-of-plugins": "{ count, plural, 1 {un plugin} other {Listă # plugin-uri} }", - "plugin-name-starts-with": "Plugin-uri al căror nume începe cu '{{prefix}}'", - "type-tenant": "Locatar", - "type-tenants": "Locatari", - "list-of-tenants": "{ count, plural, 1 {un locatar} other {Listă # locatari} }", - "tenant-name-starts-with": "Locatari al căror nume începe cu '{{prefix}}'", - "type-customer": "Client", - "type-customers": "Clienţi", - "list-of-customers": "{ count, plural, 1 {un client} other {Listă # Clienţi} }", - "customer-name-starts-with": "Clienţi al căror nume începe cu '{{prefix}}'", - "type-user": "Utilizator", - "type-users": "Utilizatori", - "list-of-users": "{ count, plural, 1 {un utilizator} other {Listă # utilizatori} }", - "user-name-starts-with": "Utilizatori al căror nume începe cu '{{prefix}}'", - "type-dashboard": "Panou Control", - "type-dashboards": "Panouri Control", - "list-of-dashboards": "{ count, plural, 1 {un panou control} other {Listă # panouri control} }", - "dashboard-name-starts-with": "Panouri control al căror nume începe cu '{{prefix}}'", - "type-alarm": "Alarmă", - "type-alarms": "Alarme", - "list-of-alarms": "{ count, plural, 1 {o alarmă} other {Listă # alarme} }", - "alarm-name-starts-with": "Alarme al căror nume începe cu '{{prefix}}'", - "type-rulechain": "Flux", - "type-rulechains": "Fluxuri", - "list-of-rulechains": "{ count, plural, 1 {un flux} other {Listă # fluxuri} }", - "rulechain-name-starts-with": "Fluxuri al căror nume începe cu '{{prefix}}'", - "type-rulenode": "Nod flux", - "type-rulenodes": "Noduri flux", - "list-of-rulenodes": "{ count, plural, 1 {un nod flux} other {Listă # noduri flux} }", - "rulenode-name-starts-with": "Noduri flux al căror nume începe cu '{{prefix}}'", - "type-current-customer": "Client Curent", - "search": "Caută Entități", - "selected-entities": "{ count, plural, 1 {o entitate} other {# entități} } selectate", - "entity-name": "Nume Entitate", - "entity-label": "Etichetă Entitate", - "details": "Detalii Entitate", - "no-entities-prompt": "Nu au fost găsite entități", - "no-data": "Nu există date de afișat", - "columns-to-display": "Coloane Afișate" - }, - "entity-field": { - "created-time": "Momentul Creării", - "name": "Denumire", - "type": "Tip", - "first-name": "Prenume", - "last-name": "Nume", - "email": "eMail", - "title": "Formula De Adresare", - "country": "Ţara", - "state": "Judeţ", - "city": "Oraş", - "address": "Adresa 1", - "address2": "Adresa 2", - "zip": "Cod Poştal", - "phone": "Telefon", - "label": "Etichetă" - }, - "entity-view": { - "entity-view": "Entitate Definită", - "entity-view-required": "Entitatea definită este obligatorie", - "entity-views": "Entități Definite", - "management": "Administrare Entități Definite", - "view-entity-views": "Afişează Entități Definite", - "entity-view-alias": "Pseudonim Entitate Definită", - "aliases": "Pseudonime Entități Definite", - "no-alias-matching": "'{{alias}}' Pseudonimul nu a fost găsit", - "no-aliases-found": "Nu au fost găsite pseudonime", - "no-key-matching": "Cheia '{{key}}' nu a fost găsită", - "no-keys-found": "Nu a fost găsită nicio cheie", - "create-new-alias": "Creează Alias Nou", - "create-new-key": "Creează Cheie Nouă", - "duplicate-alias-error": "Pseudonimul: '{{alias}}' este deja înregistrat
    Pseudonimele pentru entităţi trebuie să fie unice în acelaşi panou", - "configure-alias": "Configurează pseudonim'{{alias}}'", - "no-entity-views-matching": "Nu au fost găsite entități definite după criteriul: '{{entity}}'", - "alias": "Pseudonim", - "alias-required": "Pseudonimul entității definite este obligatoriu", - "remove-alias": "Şterge pseudonim entitate definită", - "add-alias": "Adaugă pseudonim entitate definită", - "name-starts-with": "Numele entității definite începe cu ", - "entity-view-list": "Listă Entități Definite", - "use-entity-view-name-filter": "Filtrează", - "entity-view-list-empty": "Nu au fost selectate entități definite", - "entity-view-name-filter-required": "Filtrul pentru numele entității definite este obligatoriu", - "entity-view-name-filter-no-entity-view-matched": "Nu au fost găsite entități definite al căror nume conține: '{{entityView}}'", - "add": "Adaugă Entitate Definită", - "assign-to-customer": "Repartizare către client", - "assign-entity-view-to-customer": "Repartizare Entități Definite Clientului", - "assign-entity-view-to-customer-text": "Selectează entitățile definite ce vor fi repartizate clientului", - "no-entity-views-text": "Nu există entități definite", - "assign-to-customer-text": "Selectează clientul căruia îi vor fi repartizate entitățile definite", - "entity-view-details": "Detalii Entitate Definită", - "add-entity-view-text": "Adaugă Entitate Definită", - "delete": "ßterge Entitate Definită", - "assign-entity-views": "Repartizează Entitate Definită", - "assign-entity-views-text": "Repartizează { count, plural, 1 {o entitate definită} other {# entități definite} } clientului", - "delete-entity-views": "Şterge Entități Definite", - "unassign-from-customer": "Ştergere Repartizare Către Client", - "unassign-entity-views": "Ştergere Repartizare Entități Definite", - "unassign-entity-views-action-title": "Şterge repartizare { count, plural, 1 {o entitate definită} other {# entități definite} } de la client ", - "assign-new-entity-view": "Repartizează Entitate Definită Nouă", - "delete-entity-view-title": "Sigur vrei să ștergi entitatea definită : '{{entityViewName}}'?", - "delete-entity-view-text": "ATENŢIE! După confirmare, entitatea definită şi toate datele asociate cu aceasta vor fi șterse IREVERSIBIL!", - "delete-entity-views-title": "Sigur vrei să ștergi{ count, plural, 1 {o entitate definită} other {# entități definite} }?", - "delete-entity-views-action-title": "Şterge { count, plural, 1 {o entitate definită} other {# entități definite} }", - "delete-entity-views-text": "ATENŢIE! După confirmare, toate entitățile definite și datele asociate acestora vor fi șterse IREVERSIBIL!", - "unassign-entity-view-title": "Sigur vrei să ștergi repartizarea entității definite : '{{entityViewName}}'?", - "unassign-entity-view-text": "ATENŢIE! După confirmare, clientul nu va mai putea accesa entitatea definită selectată", - "unassign-entity-view": "Şterge Repartizare Entitate Definită", - "unassign-entity-views-title": "Sigur vrei să ștergi repartizarea a { count, plural, 1 {o entitate definită} other {# entități definite} }?", - "unassign-entity-views-text": "ATENŢIE! După confirmare, clientul nu va mai putea accesa entitățile definite selectate", - "entity-view-type": "Tip Entitate Definită", - "entity-view-type-required": "Tipul ecranului pentru entitate este obligatoriu", - "select-entity-view-type": "Selectaţi Tipul Ecranului Pentru Entitate", - "enter-entity-view-type": "Introduceţi Tipul Ecranului Pentru Entitate", - "any-entity-view": "Orice Entitate Definită", - "no-entity-view-types-matching": "Nu au fost găsite tipuri de entitate definită care să corespundă criteriului '{{entitySubtype}}'", - "entity-view-type-list-empty": "Nu au fost selectate tipuri de entitate definită", - "entity-view-types": "Tipuri Entitate Definită", - "name": "Denumire", - "name-required": "Denumirea este obligatorie", - "description": "Descriere", - "events": "Evenimente", - "details": "Detalii", - "copyId": "Copie ID Entitate Definită", - "assignedToCustomer": "Repartizat Clientului", - "unable-entity-view-device-alias-title": "Pseudonimul entității definite nu poate fi şters", - "unable-entity-view-device-alias-text": "Pseudonimul dispozitivului : '{{entityViewAlias}}' nu poate fi șters, fiind folosit de widgetul/widgeturile :
    {{widgetsList}}", - "select-entity-view": "Selectează Entitate Definită", - "make-public": "Declară Entitate Definită Publică", - "make-private": "Declară Entitate Definită Privată", - "start-date": "Dată Început", - "start-ts": "Oră Început", - "end-date": "Dată Sfârșit", - "end-ts": "Oră Sfârșit", - "date-limits": "Limite Dată", - "client-attributes": "Atribute Client", - "shared-attributes": "Atribute Partajate", - "server-attributes": "Atribute Server", - "timeseries": "Serii Temporale", - "client-attributes-placeholder": "Atribute Client", - "shared-attributes-placeholder": "Atribute Partajate", - "server-attributes-placeholder": "Atribute Server", - "timeseries-placeholder": "Serii Temporale", - "target-entity": "Entitate Destinaţie", - "attributes-propagation": "Propagare Atribute", - "attributes-propagation-hint": "Entitatea definită va copia automat atributele specificate de la entitatea destinaţie, de fiecare dată când este salvată sau actualizată. Din motive de performanţă, atributele entităţii de destinaţie nu sunt propagate către entitatea definită la fiecare modificare de atribut. Poți activa propagarea automată a atributelor prin configurarea nodului în mod \"copiază pentru a vedea\" în fluxul tău și conectarea mesajelor \"Post Atribute\" și \"Atribute Actualizate\" la noul nod", - "timeseries-data": "Date Serii Temporale", - "timeseries-data-hint": "Configurează intervale timp pentru seriile temporale ale entităţii destinaţie, care vor fi accesibile prin entitatea definită. Acestea nu vor putea fi modificate", - "make-public-entity-view-title": "Sigur vrei să faci publică entitatea definită: '{{entityViewName}}'?", - "make-public-entity-view-text": "ATENŢIE! După confirmare, entitatea definită și datele aferente acesteia vor deveni publice, deci accesibile oricui", - "make-private-entity-view-title": "Sigur vrei să faci privată entitatea definită: '{{entityViewName}}'?", - "make-private-entity-view-text": "ATENŢIE! După confirmare, entitatea definită şi toate datele aferente acesteia, vor deveni private, la ele având acces doar proprietarul" - }, - "event": { - "event-type": "Tip Eveniment", - "type-error": "Eroare", - "type-lc-event": "Durată Eveniment", - "type-stats": "Statistică", - "type-debug-rule-node": "Depanare", - "type-debug-rule-chain": "Depanare", - "no-events-prompt": "Nu au fost găsite evenimente", - "error": "Eroare", - "alarm": "Alarmă", - "event-time": "Oră Eveniment", - "server": "Server", - "body": "Corp", - "method": "Metodă", - "type": "Tip", - "entity": "Entitate", - "message-id": "ID Mesaj", - "message-type": "Tip Mesaj", - "data-type": "Tip Date", - "relation-type": "Tip Relaţie", - "metadata": "Metadata", - "data": "Date", - "event": "Eveniment", - "status": "Stare", - "success": "Succes", - "failed": "Eşuat", - "messages-processed": "Mesaje procesate", - "errors-occurred": "Au apărut erori" - }, - "extension": { - "extensions": "Extensii", - "selected-extensions": "{ count, plural, 1 {o extensie selectată} other {# extensii selectate} }", - "type": "Tip", - "key": "Cheie", - "value": "Valoare", - "id": "ID", - "extension-id": "ID Extensie", - "extension-type": "Tip Extensie", - "transformer-json": "JSON *", - "unique-id-required": "ID-ul curent pentru extensie este deja înregistrat", - "delete": "Şterge Extensie", - "add": "Adaugă Extensie", - "edit": "Editează Extensie", - "delete-extension-title": "Sigur vrei să ștergi extensia: '{{extensionId}}'?", - "delete-extension-text": "ATENŢIE! După confirmare, extensia şi toate datele asociate acesteia, vor fi șterse IREVERSIBIL!", - "delete-extensions-title": "Sigur vrei să ștergi { count, plural, 1 {o extensie} other {# extensii} }?", - "delete-extensions-text": "ATENŢIE! După confirmare, toate extensiile selectate vor fi șterse IREVERSIBIL!", - "converters": "Convertoare", - "converter-id": "ID Convertoare", - "configuration": "Configurare", - "converter-configurations": "Configurator Convertoare", - "token": "Token De Securitate", - "add-converter": "Adaugă Convertor", - "add-config": "Adaugă Configurator Convertoare", - "device-name-expression": "Expresie Denumire Dispozitiv", - "device-type-expression": "Expresie Tip Dispozitiv", - "custom": "Definit De Utilizator", - "to-double": "To Double", - "transformer": "Transformare", - "json-required": "Este necesar transformer json", - "json-parse": "Analiză transformer json imposibilă", - "attributes": "Atribute", - "add-attribute": "Adaugă Atribut", - "add-map": "Adaugă Mapare", - "timeseries": "Date Cronologice", - "add-timeseries": "Adaugă Set Date Cronologice", - "field-required": "Câmpul Este Obligatoriu", - "brokers": "Brokeri", - "add-broker": "Adaugă Broker", - "host": "Gazdă", - "port": "Port", - "port-range": "Valoarea portului trebuie să fie în intervalul 1 - 65535", - "ssl": "SSL", - "credentials": "Acreditări", - "username": "Nume Utilizator", - "password": "Parolă", - "retry-interval": "Interval Reîncercare (milisecunde)", - "anonymous": "Anonim", - "basic": "Bazic", - "pem": "PEM", - "ca-cert": "Fişier Certificat CA", - "private-key": "Fişier Cheie Privată *", - "cert": "Fişier Certificat *", - "no-file": "Niciun fișier selectat", - "drop-file": "Trageţi un fişier sau selectaţi cu mouseul un fişier pentru a fi încărcat", - "mapping": "Mapare", - "topic-filter": "Filtru Topic", - "converter-type": "Tipul Convertor", - "converter-json": "Json", - "json-name-expression": "Expresie JSON Pentru Nume Dispozitiv", - "topic-name-expression": "Expresie TOPIC Pentru Nume Dispozitiv", - "json-type-expression": "Expresie JSON Pentru Tip Dispozitiv", - "topic-type-expression": "Expresie TOPIC Pentru Tip Dispozitiv", - "attribute-key-expression": "Expresie Cheie Atribut", - "attr-json-key-expression": "Expresie JSON Cheie Atribut", - "attr-topic-key-expression": "Expresie Topic Cheie Atribut", - "request-id-expression": "Expresie Cerere ID", - "request-id-json-expression": "Expresie JSON Cerere ID", - "request-id-topic-expression": "Expresie TOPIC Cerere ID", - "response-topic-expression": "Expresie Răspuns Topic", - "value-expression": "Valoare Expresie", - "topic": "Topic", - "timeout": "Timp De Expirare (milisecunde)", - "converter-json-required": "Convertorul JSON este obligatoriu", - "converter-json-parse": "Analiză converter JSON imposibilă", - "filter-expression": "Filtrează expresie", - "connect-requests": "Solicitări Conectare", - "add-connect-request": "Adaugă Solicitare Conectare", - "disconnect-requests": "Solicitări Deconectare", - "add-disconnect-request": "Adaugă Solicitare Deconectare", - "attribute-requests": "Solicitări Atribut", - "add-attribute-request": "Adaugă Solicitare Atribut", - "attribute-updates": "Actualizări Atribut", - "add-attribute-update": "Adaugă Actualizare Atribut", - "server-side-rpc": "Server RPC", - "add-server-side-rpc-request": "Adaugă Solicitare RPC Server-Side", - "device-name-filter": "Filtru Nume Dispozitiv", - "attribute-filter": "Filtru Atribut", - "method-filter": "Filtru Metodă", - "request-topic-expression": "Solicită expresie topic", - "response-timeout": "Timp răspuns în milisecunde", - "topic-expression": "Expresie Topic", - "client-scope": "Scop Client", - "add-device": "Adaugă Dispozitiv", - "opc-server": "Servere", - "opc-add-server": "Adaugă Server", - "opc-add-server-prompt": "Te rog, adaugă server ", - "opc-application-name": "Nume Aplicație", - "opc-application-uri": "URI Aplicație", - "opc-scan-period-in-seconds": "Perioadă Scanare (secunde)", - "opc-security": "Securitate", - "opc-identity": "Identitate", - "opc-keystore": "Keystore", - "opc-type": "Tip OPC", - "opc-keystore-type": "Tip Keystore", - "opc-keystore-location": "Localizare *", - "opc-keystore-password": "Parolă", - "opc-keystore-alias": "Pseudonim", - "opc-keystore-key-password": "Cheie Parolă", - "opc-device-node-pattern": "Structură Nod Dispozitiv", - "opc-device-name-pattern": "Structură Nume Dispozitiv", - "modbus-server": "Servere/sclavi", - "modbus-add-server": "Adaugă server/sclav", - "modbus-add-server-prompt": "Te rog adaugă server/sclav", - "modbus-transport": "Transport", - "modbus-tcp-reconnect": "Reconectare Automată", - "modbus-rtu-over-tcp": "RTU peste TCP", - "modbus-port-name": "Nume Port Serial", - "modbus-encoding": "Codificare", - "modbus-parity": "Paritate", - "modbus-baudrate": "Rată Baud", - "modbus-databits": "Biți Date", - "modbus-stopbits": "Biți Stop", - "modbus-databits-range": "Bitii de date trebuie să fie în intervalul 7-8", - "modbus-stopbits-range": "Bitii de stop trebuie să fie în intervalul 1-2", - "modbus-unit-id": "ID Unitate", - "modbus-unit-id-range": "ID-ul unității trebuie să fie în intervalul 1-247", - "modbus-device-name": "Nume Dispozitiv", - "modbus-poll-period": "Perioadă Interogare (ms)", - "modbus-attributes-poll-period": "Perioadă Interogare Atribute (ms)", - "modbus-timeseries-poll-period": "Perioadă Interogare serii temporale (ms)", - "modbus-poll-period-range": "Perioada de interogare trebuie să fie pozitivă", - "modbus-tag": "Tag", - "modbus-function": "Funcție", - "modbus-register-address": "Adresă Registru", - "modbus-register-address-range": "Adresa registrului trebuie să fie în intervalul 0-65535", - "modbus-register-bit-index": "Index biți", - "modbus-register-bit-index-range": "Indexul biților trebuie să fie în intervalul 0-15", - "modbus-register-count": "Număr Regiștri", - "modbus-register-count-range": "Numărul regiștrilor trebuie să fie pozitiv", - "modbus-byte-order": "Ordine Bytes", - "sync": { - "status": "Stare", - "sync": "Sincronizat", - "not-sync": "Nesincronizat", - "last-sync-time": "Ultima Sincronizare", - "not-available": "Indisponibil" - }, - "export-extensions-configuration": "Exportă configuraţie extensii", - "import-extensions-configuration": "Importă configuraţie extensii", - "import-extensions": "Importă Extensii", - "import-extension": "Importă Extensie", - "export-extension": "Exportă Extensie", - "file": "Fişier Extensii", - "invalid-file-error": "Fişier extensie invalid" - }, - "fullscreen": { - "expand": "Extinde Către Ecran Complet", - "exit": "Ieşire Din Ecran Complet", - "toggle": "Comutare Ecran Complet", - "fullscreen": "Ecran Complet" - }, - "function": { - "function": "Funcţie" - }, - "grid": { - "delete-item-title": "Sigur vrei să ștergi elementul?", - "delete-item-text": "ATENŢIE! După confirmare, elementul şi toate datele referitoare la acesta, vor fi șterse IREVERSIBIL!", - "delete-items-title": "Sigur vrei să ștergi { count, plural, 1 {un element} other {# elemente} }?", - "delete-items-action-title": "Şterge { count, plural, 1 {un element} other {# elemente} }", - "delete-items-text": "ATENŢIE! După confirmare, toate elementele selectate şi toate datele referitoare la acestea, vor fi șterse IREVERSIBIL!", - "add-item-text": "Adaugă Element Nou", - "no-items-text": "Nu Au Fost Găsite Elemente", - "item-details": "Detalii Element", - "delete-item": "Şterge Element", - "delete-items": "Şterge Elemente", - "scroll-to-top": "Derulare la începutul listei" - }, - "help": { - "goto-help-page": "Mergi la pagina de ajutor" - }, - "home": { - "home": "Acasă", - "profile": "Profil", - "logout": "Deconectare", - "menu": "Meniu", - "avatar": "Avatar", - "open-user-menu": "Deschide Meniu Utilizator" - }, - "import": { - "no-file": "Niciun fișier selectat", - "drop-file": "Trage un fişier de tip JSON sau selectează cu mausul un fişier de tip JSON pentru a fi încărcat", - "drop-file-csv": "Trage un fişier de tip CSV sau selectează cu mouse-ul un fişier de tip csv pentru a fi încărcat", - "column-value": "Valoare", - "column-title": "Denumire", - "column-example": "Exemplu valori date", - "column-key": "Cheie Atribut/Telemetrie", - "csv-delimiter": "Delimitator CSV", - "csv-first-line-header": "Prima linie conţine denumiri de coloane", - "csv-update-data": "Actualizare atribute/telemetrie", - "import-csv-number-columns-error": "Un fișier trebuie să conțină cel puțin două coloane", - "import-csv-invalid-format-error": "Format fișier incorect, linia: '{{line}}'", - "column-type": { - "name": "Denumire", - "type": "Tip", - "label": "Etichetă", - "column-type": "Tipul Coloanei", - "client-attribute": "Atribut Client", - "shared-attribute": "Atribut Partajat", - "server-attribute": "Atribut Server", - "timeseries": "Date Cronologice", - "entity-field": "Câmp Entitate", - "access-token": "Token De Acces" - }, - "stepper-text":{ - "select-file": "Selectează un fişier", - "configuration": "Importă configurație", - "column-type": "Selectează tipul de coloane", - "creat-entities": "Creează entităţi noi", - "done": "Terminat" - }, - "message": { - "create-entities": "{{count}} entităţi noi au fost create cu succes", - "update-entities": "{{count}} entităţi noi au fost actualizate cu succes", - "error-entities": "A intervenit o eroare la crearea a {{count}} entităţi" - } - }, - "item": { - "selected": "Selectat" - }, - "js-func": { - "no-return-error": "Funcţia trebuie să returneze o valoare", - "return-type-mismatch": "Funcţia trebuie să returneze o valoare de tip: '{{type}}'", - "tidy": "Tidy" - }, - "key-val": { - "key": "Cheie", - "value": "Valoare", - "remove-entry": "Șterge Intrare", - "add-entry": "Adaugă Intrare", - "no-data": "Nu sunt intrări" - }, - "layout": { - "layout": "Amplasament", - "manage": "Modifică Amplasamente", - "settings": "Configurare Amplasamente", - "color": "Culoare", - "main": "Principal", - "right": "Dreapta", - "select": "Alege Amplasament Țintă" - }, - "legend": { - "direction": "Direcţie Legendă", - "position": "Poziţie Legendă", - "show-max": "Afişează Valoare Maximă", - "show-min": "Afişează Valoare Minimă", - "show-avg": "Afişează Valoare Medie", - "show-total": "Afişează Valoare Totală", - "settings": "Setări Legendă", - "min": "Minim", - "max": "Maxim", - "avg": "Medie", - "total": "Total", - "comparison-time-ago": { - "days": "(Ziua Trecută (Ieri))", - "weeks": "(Săptamâna Trecută)", - "months": "(Luna Trecută)", - "years": "(Anul Trecut)" - } - }, - "login": { - "login": "Conectare", - "request-password-reset": "Solicită Resetarea Parolei", - "reset-password": "Resetează Parolă", - "create-password": "Creează Parolă", - "passwords-mismatch-error": "Parola reintrodusă trebuie să fie identică!", - "password-again": "Rescrie Parola", - "sign-in": "Intră în Cont", - "username": "Nume Utilizator (Adresa De eMail)", - "remember-me": "Ține-mă minte!", - "forgot-password": "Ai Uitat Parola?", - "password-reset": "Resetează Parola", - "expired-password-reset-message": "Parola ta a expirat! Este necesară schimbarea acesteia", - "new-password": "Parolă nouă", - "new-password-again": "Verificare parolă nouă", - "password-link-sent-message": "Ți-am trimis pe eMail un link pentru resetarea parolei", - "email": "eMail", - "login-with": "Conectare cu {{name}}", - "or": "sau" - }, - "position": { - "top": "Sus", - "bottom": "Jos", - "left": "Stânga", - "right": "Dreapta" - }, - "profile": { - "profile": "Profil", - "last-login-time": "Ultima Accesare", - "change-password": "Schimbă Parola", - "current-password": "Parola Actuală" - }, - "relation": { - "relations": "Relaţii", - "direction": "Direcţie", - "search-direction": { - "FROM": "Dinspre", - "TO": "Înspre" - }, - "direction-type": { - "FROM": "Dinspre", - "TO": "Către" - }, - "from-relations": "Ieșire", - "to-relations": "Intrare", - "selected-relations": "{ count, plural, 1 {o relaţie selectată } other {# relaţii selectate } }", - "type": "Tip", - "to-entity-type": "Către Tip Entitate", - "to-entity-name": "Către Nume Entitate", - "from-entity-type": "Dinspre Tip Entitate", - "from-entity-name": "Dinspre Nume Entitate", - "to-entity": "Către Entitate", - "from-entity": "Dinspre Entitate", - "delete": "Şterge Relaţie", - "relation-type": "Tip Relaţie", - "relation-type-required": "Tipul relației este obligatoriu", - "any-relation-type": "Orice Tip", - "add": "Adaugă Relaţie", - "edit": "Şterge Relaţie", - "delete-to-relation-title": "Sigur vrei să ștergi relația către entitatea '{{entityName}}'?", - "delete-to-relation-text": "ATENŢIE! După confirmare,relaţia către entitatea '{{entityName}}' va fi ştearsă", - "delete-to-relations-title": "Sigur vrei să ștergi { count, plural, 1 {o relaţie} other {# relaţii} }?", - "delete-to-relations-text": "ATENŢIE! După confirmare, relaţiile selectate către entităţile corespondente și toate referirile la acestea, vor fi șterse IREVERSIBIL!", - "delete-from-relation-title": "Sigur vrei să ștergi relația dinspre entitatea '{{entityName}}'?", - "delete-from-relation-text": "ATENŢIE! După confirmare,relaţia dinspre entitatea '{{entityName}}' va fi ștearsă", - "delete-from-relations-title": "Sigur vrei să ștergi { count, plural, 1 {o relaţie} other {# relaţii} }?", - "delete-from-relations-text": "ATENŢIE! După confirmare, relaţiile selectate către entităţile corespondente și toate referirile la acestea, vor fi șterse IREVERSIBIL!", - "remove-relation-filter": "Elimină Filtru Relaţie", - "add-relation-filter": "Adaugă Filtru Relaţie", - "any-relation": "Orice Relaţie", - "relation-filters": "Filtre Relaţie", - "additional-info": "Informaţii Adiţionale (JSON)", - "invalid-additional-info": "Informaţiile adiţionale (JSON) nu au putut fi analizate" - }, - "rulechain": { - "rulechain": "Flux", - "rulechains": "Fluxuri", - "root": "Origine", - "delete": "Şterge Fluxuri", - "name": "Denumire", - "name-required": "Denumirea este obligatorie", - "description": "Descriere", - "add": "Adaugă Fluxuri", - "set-root": "Stabileşte Originea Fluxurilor", - "set-root-rulechain-title": "Sigur vrei să setezi '{{ruleChainName}}' ca rădăcină?", - "set-root-rulechain-text": "ATENŢIE! După confirmare, fluxul va deveni rădăcină şi va gestiona toate mesajele de intrare", - "delete-rulechain-title": "Sigur vrei să ștergi fluxul '{{ruleChainName}}'?", - "delete-rulechain-text": "ATENŢIE! După confirmare, fluxul şi toate datele referitoare la acesta, vor fi șterse IREVERSIBIL!", - "delete-rulechains-title": "Sigur vrei să ștergi { count, plural, 1 {un flux} other {# fluxuri} }?", - "delete-rulechains-action-title": "Ştergi { count, plural, 1 {un flux} other {# fluxuri} }", - "delete-rulechains-text": "ATENŢIE! După confirmare, fluxul şi toate datele referitoare la acesta, vor fi șterse IREVERSIBIL!", - "add-rulechain-text": "Adaugă Flux Nou", - "no-rulechains-text": "Nu au fost găsite fluxuri", - "rulechain-details": "Detalii Flux", - "details": "Detalii", - "events": "Evenimente", - "system": "Sistem", - "import": "Importă Flux", - "export": "Exportă Flux", - "export-failed-error": "Fluxul nu poate fi exportat; {{error}}", - "create-new-rulechain": "Creează Flux Nou", - "rulechain-file": "Fişierul Flux", - "invalid-rulechain-file-error": "Fluxul nu poate fi importat; structură date invalidă", - "copyId": "Copiază ID Flux", - "idCopiedMessage": "ID Flux copiat în clipboard", - "select-rulechain": "Selectează Flux", - "no-rulechains-matching": "Nu au fost găsite fluxuri după criteriul: '{{entity}}'", - "rulechain-required": "Fluxul este obligatoriu", - "management": "Administrare Fluxuri", - "debug-mode": "Mod Depanare" - }, - "rulenode": { - "details": "Detalii", - "events": "Evenimente", - "search": "Noduri Căutare", - "open-node-library": "Bibliotecă Noduri", - "add": "Adaugă Regulă Nod", - "name": "Denumire", - "name-required": "Denumirea este obligatorie", - "type": "Tip", - "description": "Descriere", - "delete": "Şterge Regulă Nod", - "select-all-objects": "Selectează Toate Nodurile Şi Conexiunile", - "deselect-all-objects": "Deselectează Toate Nodurile Şi Conexiunile", - "delete-selected-objects": "Şterge Toate Nodurile Şi Conexiunile", - "delete-selected": "Şterge Selecţia", - "select-all": "Selectează Tot", - "copy-selected": "Copiază Selecţia", - "deselect-all": "Deselectează Tot", - "rulenode-details": "Detalii Regulă Nod", - "debug-mode": "Mod Depanare", - "configuration": "Configurare", - "link": "Link", - "link-details": "Detalii Link Regulă Nod", - "add-link": "Adaugă Link", - "link-label": "Etichetă Link", - "link-label-required": "Eticheta link-ului este obligatorie", - "custom-link-label": "Etichetă Link Definit de Utilizator", - "custom-link-label-required": "Eticheta pentru link, definită de către utilizator, este obligatorie", - "link-labels": "Etichete Link-uri)", - "link-labels-required": "Etichetele link-urilor sunt obligatorii", - "no-link-labels-found": "Nu au fost găsite etichete pentru link", - "no-link-label-matching": "Eticheta : '{{label}}' nu a fost găsită", - "create-new-link-label": "Creează Etichetă Nouă", - "type-filter": "Filtrează", - "type-filter-details": "Filtrează mesajele de intrare după condiţiile configurate", - "type-enrichment": "Îmbogățire", - "type-enrichment-details": "Adaugă informaţii adiţionale in metadata mesajului", - "type-transformation": "Transformare", - "type-transformation-details": "Schimbă payload şi metadata mesajului", - "type-action": "Acţiune", - "type-action-details": "Execută o acţiune specială", - "type-external": "Extern", - "type-external-details": "Interacţiuni cu sisteme externe", - "type-rule-chain": "Flux", - "type-rule-chain-details": "Transmite mesajele de intrare către fluxul specificat", - "type-input": "Intrare", - "type-input-details": "Intrarea logică pentru flux, transmite mesajele de intrare către următoarea regulă de nod înrudită", - "type-unknown": "Necunoscut", - "type-unknown-details": "Detalii regulă nod necunoscute", - "directive-is-not-loaded": "Configurarea definită pentru directiva : '{{directiveName}}' nu este disponibilă", - "ui-resources-load-error": "Eroare la încărcarea configurării resurselor UI", - "invalid-target-rulechain": "Fluxul Destinaţie nu a fost găsit", - "test-script-function": "Funcţie Test Script", - "message": "Mesaj", - "message-type": "Tip Mesaj", - "select-message-type": "Selectează Tipul Mesajului", - "message-type-required": "Tipul mesajului este obligatoriu", - "metadata": "Metadata", - "metadata-required": "Intrările metadata nu pot fi vide", - "output": "Ieşire", - "test": "Test", - "help": "Ajutor", - "reset-debug-mode": "Dezactivează modul depanare în toate nodurile" - }, - "tenant": { - "tenant": "Locatar", - "tenants": "Locatari", - "management": "Administrare Locatar", - "add": "Adaugă Locatar", - "admins": "Administratori", - "manage-tenant-admins": "Gestionare Administratori Locatar", - "delete": "Şterge Locatar", - "add-tenant-text": "Adaugă Locatar Nou", - "no-tenants-text": "Nu au fost găsiţi locatari", - "tenant-details": "Detalii Locatar", - "delete-tenant-title": "Sigur vrei să ștergi locatarul: '{{tenantTitle}}'?", - "delete-tenant-text": "ATENŢIE! După confirmare, locatarul şi toate datele referitoare la acesta, vor fi șterse IREVERSIBIL!", - "delete-tenants-title": "Sigur vrei să ștergi { count, plural, 1 {un locatar} other {# locatari} } ?", - "delete-tenants-action-title": "Şterge { count, plural, 1 {un locatar} other {# locatari} }", - "delete-tenants-text": "ATENŢIE! După confirmare, locatarii selectaţi şi datele aferente acestora, vor fi șterse IREVERSIBIL!", - "title": "Titlu", - "title-required": "Titlul este obligatoriu", - "description": "Descriere", - "details": "Detalii", - "events": "Evenimente", - "copyId": "Copiază ID Locatar", - "idCopiedMessage": "ID Locatar a fost copiat în clipboard", - "select-tenant": "Selectează locatar", - "no-tenants-matching": "Nu au fost găsiţi locatari după criteriul: '{{entity}}'", - "tenant-required": "Locatarul este obligatoriu" - }, - "timeinterval": { - "seconds-interval": "{ seconds, plural, 1 {o secundă} other {# secunde} }", - "minutes-interval": "{ minutes, plural, 1 {un minut} other {# minute} }", - "hours-interval": "{ hours, plural, 1 {o oră} other {# ore} }", - "days-interval": "{ days, plural, 1 {o zi} other {# zile} }", - "days": "Zile", - "hours": "Ore", - "minutes": "Minute", - "seconds": "Secunde", - "advanced": "Personalizat" - }, - "timewindow": { - "days": "{ days, plural, 1 {o zi} other {# zile} }", - "hours": "{ hours, plural, 1 {o oră} other {# ore} }", - "minutes": "{ minutes, plural, 1 {un minut} other {# minute} }", - "seconds": "{ seconds, plural, 1 {o secundă} other {# secunde} }", - "realtime": "Timp Real", - "history": "Istoric", - "last-prefix": "Interval:", - "period": "Început {{ startTime}} Sfârșit {{endTime}}", - "edit": "Editează Interval", - "date-range": "Interval Date", - "last": "Ultima/Ultimele", - "time-period": "Interval:", - "hide": "Ascunde" - }, - "user": { - "user": "Utilizator", - "users": "Utilizatori", - "customer-users": "Utilizatori Client", - "tenant-admins": "Administratori Locatar", - "sys-admin": "Administratori Sistem", - "tenant-admin": "Administrator Locatar", - "customer": "Clienţi", - "anonymous": "Anonim", - "add": "Adaugă Utilizator", - "delete": "Şterge Utilizator", - "add-user-text": "Adaugă Utilizator Nou", - "no-users-text": "Nu Există Utilizatori", - "user-details": "Detalii Utilizator", - "delete-user-title": "Sigur vrei să ștergi utilizatorul '{{userEmail}}'?", - "delete-user-text": "ATENŢIE! După confirmare, utilizatorul şi toate datele aferente acestuia, vor fi șterse IREVERSIBIL!", - "delete-users-title": "Sigur vrei să ștergi { count, plural, 1 {un utilizator} other {# utilizatori} }?", - "delete-users-action-title": "Ştergere { count, plural, 1 {un utilizator} other {# utilizatori} }", - "delete-users-text": "ATENŢIE! După confirmare, toţi utilizatorii selectaţi împreună cu datele aferente acestora, vor fi șterse IREVERSIBIL!", - "activation-email-sent-message": "Mesajul eMail pentru activare a fost trimis cu succes!", - "resend-activation": "Retrimite mesaj eMail de activare", - "email": "Adresă eMail", - "email-required": "Adresa eMail este obligatorie", - "invalid-email-format": "Adresa eMail este incorectă", - "first-name": "Prenume", - "last-name": "Nume", - "description": "Descriere", - "default-dashboard": "Panou Implicit", - "always-fullscreen": "Permanent Ecran Complet", - "select-user": "Selectează Utilizator", - "no-users-matching": "Nu există utilizatori care corespund criteriului: '{{entity}}'", - "user-required": "Utilizatorul este obligatoriu", - "activation-method": "Metoda De Activare", - "display-activation-link": "Afişează link activare", - "send-activation-mail": "Trimite mesaj eMail pentru activare", - "activation-link": "Link activare utilizator:", - "activation-link-text": "Pentru activarea contului, folosiți link: ", - "copy-activation-link": "Copiază link activare", - "activation-link-copied-message": "Link-ul de activare utilizator a fost copiat în clipboard", - "details": "Detalii", - "login-as-tenant-admin": "Acces ca locatar administrator", - "login-as-customer-user": "Acces ca utilizator client", - "disable-account": "Dezactivează cont utilizator", - "enable-account": "Activează cont utilizator", - "enable-account-message": "Cont utilizator activat!", - "disable-account-message": "Cont utilizator dezactivat!" - }, - "value": { - "type": "Tip Valoare", - "string": "Şir Caractere", - "string-value": "Valoare Şir Caractere", - "integer": "Număr Întreg", - "integer-value": "Valoare Număr Întreg", - "invalid-integer-value": "Valoare număr întreg incorectă", - "double": "Tip Double", - "double-value": "Valoare Tip Double", - "boolean": "Tip Bool: ", - "boolean-value": "Valoare Bool", - "false": "Fals", - "true": "Adevărat", - "long": "Tip Long" - }, - "widget": { - "widget-library": "Biblioteci Widgets", - "widget-bundle": "Pachete Widgets", - "select-widgets-bundle": "Selectează Pachete Widgets", - "management": "Administrare Widgets", - "editor": "Editor Widget", - "widget-type-not-found": "Eroare la încărcarea configuraţiei widgetului.
    Probabil Asocierea \n cu tipul de widget a fost eliminată", - "widget-type-load-error": "Widgetul nu a fost încărcat din cauza următoarelor erori:", - "remove": "Elimină Widget", - "edit": "Editează Widget", - "remove-widget-title": "Sigur vrei să ștergi widgetul '{{widgetTitle}}'?", - "remove-widget-text": "ATENŢIE! După confirmare, widgetul şi toate datele aferente acestuia, vor fi șterse IREVERSIBIL!", - "timeseries": "Serii Temporale", - "search-data": "Caută Date", - "no-data-found": "Nu Au Fost Găsite Date", - "latest-values": "Ultimele Valori", - "rpc": "Widget Control ", - "alarm": "Widget Alarmă", - "static": "Widget Static", - "select-widget-type": "Selectaţi Tip Widget", - "missing-widget-title-error": "Titlul widgetului trebuie specificat!", - "widget-saved": "Widget Salvat", - "unable-to-save-widget-error": "Widgetul conține erori și nu poate fi salvat!", - "save": "Salvează Widget", - "saveAs": "Salvează Widget Ca...", - "save-widget-type-as": "Salvează Tip Widget Ca...", - "save-widget-type-as-text": "Introduceţi titlu nou widget şi/sau selectați pachet widget destinație", - "toggle-fullscreen": "Comută Ecran Complet", - "run": "Execută Widget", - "title": "Titlu Widget", - "title-required": "Titlul widgetului este obligatoriu", - "type": "Tip Widget", - "resources": "Resurse", - "resource-url": "JavaScript/CSS URL", - "remove-resource": "Şterge Resursă", - "add-resource": "Adaugă Resursă", - "html": "HTML", - "tidy": "Tidy", - "css": "CSS", - "settings-schema": "Schemă setări", - "datakey-settings-schema": "Schemă setări chei date", - "javascript": "Javascript", - "js": "JS", - "remove-widget-type-title": "Sigur vrei să ștergi tip widget '{{widgetName}}'?", - "remove-widget-type-text": "ATENŢIE! După confirmare, tipul de widget şi toate datele aferente acestuia, vor fi șterse IREVERSIBIL!", - "remove-widget-type": "Şterge Tip Widget", - "add-widget-type": "Adaugă Tip Nou Widget", - "widget-type-load-failed-error": "Eroare încărcare tip widget!", - "widget-template-load-failed-error": "Eroare încarcare şablon widget!", - "add": "Adaugă Widget Nou", - "undo": "Anulează Modificări Widget", - "export": "Exportă Widget" - }, - "widget-action": { - "header-button": "Buton Principal Widget", - "open-dashboard-state": "Deschide Altă Stare a Panoului", - "update-dashboard-state": "Actualizează Starea Curentă A Panoului", - "open-dashboard": "Comută Către Alt Panou", - "custom": "Acţiuni Utilizator", - "custom-pretty": "Acţiuni Utilizator (cu şablon HTML)", - "target-dashboard-state": "Stare Panou Destinaţie", - "target-dashboard-state-required": "Starea panoului de destinaţie este obligatorie!", - "set-entity-from-widget": "Setează Entitate din Widget", - "target-dashboard": "Panou Destinaţie", - "open-right-layout": "Deschide Aspect Corect Al Panoului (accesare de pe mobil)" - }, - "widgets-bundle": { - "current": "Pachet Curent Widgeturi", - "widgets-bundles": "Pachete Widgeturi", - "add": "Adăugare Pachete Widgeturi", - "delete": "Ştergere Pachete Widgeturi", - "title": "Titlu", - "title-required": "Titlul este obligatoriu", - "add-widgets-bundle-text": "Adaugă pachet nou widgeturi", - "no-widgets-bundles-text": "Nu există pachete widgeturi", - "empty": "Pachetul de widgeturi este gol", - "details": "Detalii", - "widgets-bundle-details": "Detalii Pachet Widgeturi", - "delete-widgets-bundle-title": "Sigur vrei să ștergi pachetul de widgeturi '{{widgetsBundleTitle}}'?", - "delete-widgets-bundle-text": "ATENŢIE! După Confirmare, pachetul de widgeturi şi toate datele aferente acestuia, vor fi șterse IREVERSIBIL!", - "delete-widgets-bundles-title": "Sigur vrei să ștergi { count, plural, 1 {un pachet widgeturi} other {# pachete widgeturi} }?", - "delete-widgets-bundles-action-title": "Şterge { count, plural, 1 {un packet widgeturi} other {# pachete widgeturi} }", - "delete-widgets-bundles-text": "ATENŢIE! După confirmare, toate pachetele selectate de widget-uri şi datele aferente acestuia, vor fi șterse IREVERSIBIL!", - "no-widgets-bundles-matching": "Nu au fost găsite pachete de widgeturi conținând textul '{{widgetsBundle}}' ", - "widgets-bundle-required": "Denumirea pachetelor de widgeturi este obligatorie", - "system": "Sistem", - "import": "Importă Pachet Widgeturi", - "export": "Exportă Pachet Widgeturi", - "export-failed-error": "Export pachet widgeturi imposibil: {{error}}", - "create-new-widgets-bundle": "Definire Pachet Widgeturi Nou", - "widgets-bundle-file": "Alege fișier pachet widgeturi", - "invalid-widgets-bundle-file-error": "Export pachet widgeturi imposibil; Structură date invalidă" - }, - "widget-config": { - "data": "Date", - "settings": "Setări", - "advanced": "Setări Avansate", - "title": "Titlu", - "title-tooltip": "Mesaj Detalii Titlu", - "general-settings": "Setări Generale", - "display-title": "Titlu Afişat", - "drop-shadow": "Cu Umbră", - "enable-fullscreen": "Permite Ecran Complet", - "background-color": "Culoare Fundal", - "text-color": "Culoare Text", - "padding": "Margine Interioară", - "margin": "Margine Exterioară", - "widget-style": "Stil Widget", - "title-style": "Stil Titlu", - "mobile-mode-settings": "Setări Afișare Mobil", - "order": "Ordine", - "height": "Înăltime", - "units": "Unitate măsură", - "decimals": "Număr Zecimale", - "timewindow": "Interval Timp", - "use-dashboard-timewindow": "Folosire Interval Timp Panou", - "display-timewindow": "Afişare Interval Timp", - "display-legend": "Afişare Legendă", - "datasources": "Surse Date", - "maximum-datasources": "Maximum { count, plural, 1 {o sursă date permisă} other {# surse date permise} }", - "datasource-type": "Tip", - "datasource-parameters": "Parametri", - "remove-datasource": "Elimină Sursă Date", - "add-datasource": "Adaugă Sursă Date", - "target-device": "Dispozitiv Destinaţie", - "alarm-source": "Sursă Alarmă", - "actions": "Acţiuni", - "action": "Acţiune", - "add-action": "Adaugă Acţiune", - "search-actions": "Caută Acţiuni", - "action-source": "Sursa Acțiunii", - "action-source-required": "Sursa acțiunii este obligatorie", - "action-name": "Numele Acțiunii", - "action-name-required": "Numele acțiunii este obligatoriu", - "action-name-not-unique": "O acţiune cu acelaşi nume este deja definită
    Numele definit al acțiunii trebuie să fie unic in aceeaşi sursă de date", - "action-icon": "Pictogramă", - "action-type": "Tipul", - "action-type-required": "Tipul acțiunii este obligatoriu", - "edit-action": "Editare Acţiune", - "delete-action": "Ştergere Acţiune", - "delete-action-title": "Şterge acţiunea ", - "delete-action-text": "Ești sigur că vrei să ștergi acţiunea '{{actionName}}'?", - "display-icon": "Afişează Pictograma Titlului", - "icon-color": "Culoare Pictogramă", - "icon-size": "Mărime Pictogramă" - }, - "widget-type": { - "import": "Import Tip Widget", - "export": "Export Tip Widget", - "export-failed-error": "Eroare! Export imposibil pentru tip widget: {{error}}", - "create-new-widget-type": "Defineşte tip widget nou", - "widget-type-file": "Alege fişier pentru tip widget", - "invalid-widget-type-file-error": "Eroare! Tip widget nu poate fi importat; structură de date invalidă" - }, - "widgets": { - "date-range-navigator": { - "localizationMap": { - "Sun": "D", - "Mon": "L", - "Tue": "M", - "Wed": "M", - "Thu": "J", - "Fri": "V", - "Sat": "S", - "Jan": "Ian", - "Feb": "Feb", - "Mar": "Mar", - "Apr": "Apr", - "May": "Mai", - "Jun": "Iun", - "Jul": "Iul", - "Aug": "Aug", - "Sep": "Sep", - "Oct": "Oct", - "Nov": "Nov", - "Dec": "Dec", - "January": "Ianuarie", - "February": "Februarie", - "March": "Martie", - "April": "Aprilie", - "Maz": "Mai", - "June": "Iunie", - "July": "Iulie", - "August": "August", - "September": "Septembrie", - "October": "Octombrie", - "November": "Noiembrie", - "December": "Decembrie", - "Custom Date Range": "Interval Date Personalizat", - "Date Range Template": "Șablon Interval Date", - "Today": "Astăzi", - "Yesterday": "Ieri", - "This Week": "Săptămâna Aceasta", - "Last Week": "Săptămâna Trecută", - "This Month": "Luna Aceasta", - "Last Month": "Luna Trecută", - "Year": "Anul", - "This Year": "Anul Acesta", - "Last Year": "Anul Trecut", - "Date picker": "Alege Data", - "Hour": "Oră", - "Day": "Zi", - "Week": "Săptămână", - "2 weeks": "2 săptămâni", - "Month": "Lună", - "3 months": "3 luni", - "6 months": "6 luni", - "Custom interval": "Interval Personalizat", - "Interval": "Interval:", - "Step size": "Pas", - "Ok": "Ok" - } - }, - "input-widgets": { - "attribute-not-allowed": "Acest widget nu poate folosi atributul specificat", - "blocked-location": "Acest browser blochează localizarea", - "claim-device": "Revendică Dispozitiv", - "claim-failed": "Încercarea revendicare dispozitiv eșuată", - "claim-not-found": "Dispozitivul nu a fost găsit", - "claim-successful": "Dispozitivul a fost revendicat cu succes", - "date": "Data", - "device-name": "Nume Dispozitiv", - "device-name-required": "Numele dispozitivului este obligatoriu", - "discard-changes": "Anulare Modificări", - "entity-attribute-required": "Atributul entităţii este obligatoriu", - "entity-coordinate-required": "Atât latitudinea Şi longitudinea sunt obligatorii", - "entity-timeseries-required": "Seriile temporale pentru entitate sînt obligatorii", - "get-location": "Află locaţia GPS actuală", - "latitude": "Latitudine", - "longitude": "Longitudine", - "not-allowed-entity": "Entitatea selectată nu poate avea atribute partajate", - "no-attribute-selected": "Niciun Atribut Selectat", - "no-datakey-selected": "Nicio cheie selectată", - "no-coordinate-specified": "Cheie date latitude/longitude nespecificată", - "no-entity-selected": "Nicio entitate selectată", - "no-image": "Lipsă Imagine", - "no-support-geolocation": "Acest browser nu permite geolocalizarea", - "no-support-web-camera": "Cameră Web nesuportată", - "no-timeseries-selected": "Serii temporale nespecificate", - "secret-key": "Cheie Secretă", - "secret-key-required": "Cheia secretă este obligatorie", - "switch-attribute-value": "Schimbă valoare atribut entitate", - "switch-camera": "Schimbă Camera", - "switch-timeseries-value": "Schimbă valori serii temporale entitate", - "take-photo": "Captură Imagine", - "time": "Timp", - "timeseries-not-allowed": "Parametrul nu este compatibil cu acest widget", - "update-failed": "Actualizare eșuată", - "update-successful": "Actualizare reușită", - "update-attribute": "Actualizare Atribut", - "update-timeseries": "Actualizare Serii Temporale", - "value": "Valoare" - } - }, - "icon": { - "icon": "Pictogramă", - "select-icon": "Selectează Pictogramă", - "material-icons": "Material Pictogramă", - "show-all": "Afişează Toate Pictogramele" - }, - "custom": { - "widget-action": { - "action-cell-button": "Acțiune buton celulă", - "row-click": "Eveniment : Click pe linie tabel", - "polygon-click": "Eveniment : Click pe poligon", - "marker-click": "Eveniment : Click pe marker", - "tooltip-tag-action": "Acţiune marcaj detalii mesaj ", - "node-selected": "Eveniment : Nod Selectat", - "element-click": "eveniment : click Pe element HTML", - "pie-slice-click": "Eveniment : Click pe sector cerc", - "row-double-click": "Eveniment : Dublu click pe linia tabelului" - } - }, - "language": { - "language": "Limba" - } -} diff --git a/ui/src/app/locale/locale.constant-ru_RU.json b/ui/src/app/locale/locale.constant-ru_RU.json deleted file mode 100644 index 473698a091..0000000000 --- a/ui/src/app/locale/locale.constant-ru_RU.json +++ /dev/null @@ -1,1802 +0,0 @@ -{ - "access": { - "unauthorized": "Неавторизированный", - "unauthorized-access": "Несанкционированный доступ", - "unauthorized-access-text": "Вы должны войти в систему для получения доступа к этому ресурсу!", - "access-forbidden": "Доступ запрещен", - "access-forbidden-text": "У вас нет прав доступа к этому ресурсу!
    Для получения доступа попробуйте войти под другим пользователем.", - "refresh-token-expired": "Сессия истекла", - "refresh-token-failed": "Не удалось обновить сессию" - }, - "action": { - "activate": "Активировать", - "suspend": "Приостановить", - "save": "Сохранить", - "saveAs": "Сохранить как", - "cancel": "Отмена", - "ok": "ОК", - "delete": "Удалить", - "add": "Добавить", - "yes": "Да", - "no": "Нет", - "update": "Обновить", - "remove": "Удалить", - "search": "Поиск", - "clear-search": "Очистить", - "assign": "Присвоить", - "unassign": "Отозвать", - "share": "Поделиться", - "make-private": "Закрыть для общего доступа", - "apply": "Применить", - "apply-changes": "Применить изменения", - "edit-mode": "Режим редактирования", - "enter-edit-mode": "Режим редактирования", - "decline-changes": "Отменить изменения", - "close": "Закрыть", - "back": "Назад", - "run": "Запуск", - "sign-in": "Войти", - "edit": "Редактировать", - "view": "Просмотреть", - "create": "Создать", - "drag": "Переместить", - "refresh": "Обновить", - "undo": "Откатить", - "copy": "Копировать", - "paste": "Вставить", - "copy-reference": "Копировать ссылку", - "paste-reference": "Вставить ссылку", - "import": "Импортировать", - "export": "Экспортировать", - "share-via": "Поделиться в {{provider}}", - "continue": "Продолжить", - "discard-changes": "Отменить изменения" - }, - "aggregation": { - "aggregation": "Агрегация", - "function": "Тип агрегации данных", - "limit": "Максимальное значение", - "group-interval": "Интервал группировки", - "min": "Мин", - "max": "Maкс", - "avg": "Среднее", - "sum": "Сумма", - "count": "Количество", - "none": "Без агрегации" - }, - "admin": { - "general": "Общие", - "general-settings": "Общие настройки", - "outgoing-mail": "Исходящая почта", - "outgoing-mail-settings": "Настройки исходящей почты", - "system-settings": "Системные настройки", - "test-mail-sent": "Пробное письмо успешно отправлено!", - "base-url": "Базовая URL", - "base-url-required": "Базовая URL обязательна.", - "mail-from": "Отправитель", - "mail-from-required": "Отправитель обязателен.", - "smtp-protocol": "SMTP протокол", - "smtp-host": "SMTP хост", - "smtp-host-required": "SMTP хост обязателен.", - "smtp-port": "SMTP порт", - "smtp-port-required": "SMTP порт обязателен.", - "smtp-port-invalid": "Недействительный SMTP порт.", - "timeout-msec": "Таймаут (мс)", - "timeout-required": "Таймаут обязателен.", - "timeout-invalid": "Недействительный таймаут.", - "enable-tls": "Включить TLS", - "tls-version" : "Версия TLS", - "send-test-mail": "Отправить пробное письмо", - "security-settings": "Настройки безопасности", - "password-policy": "Политика паролей", - "minimum-password-length": "Минимальная длина пароля", - "minimum-password-length-required": "Требуется минимальная длина пароля", - "minimum-password-length-range": "Минимальная длина пароля должна быть в диапазоне от 5 до 50", - "minimum-uppercase-letters": "Минимальное количество прописных букв", - "minimum-uppercase-letters-range": "Минимальное количество прописных букв не может быть отрицательным", - "minimum-lowercase-letters": "Минимальное количество строчных букв", - "minimum-lowercase-letters-range": "Минимальное количество строчных букв не может быть отрицательным", - "minimum-digits": "Минимальное количество цифр", - "minimum-digits-range": "Минимальное количество цифр не может быть отрицательным", - "minimum-special-characters": "Минимальное количество специальных символов", - "minimum-special-characters-range": "Минимальное количество специальных символов не может быть отрицательным", - "password-expiration-period-days": "Срок действия пароля в днях", - "password-expiration-period-days-range": "Срок действия пароля в днях не может быть отрицательным", - "password-reuse-frequency-days": "Частота повторного использования пароля в днях", - "password-reuse-frequency-days-range": "Частота повторного использования пароля в днях не может быть отрицательной", - "general-policy": "Общая политика", - "max-failed-login-attempts": "Максимальное количество неудачных попыток входа в систему, прежде чем учетная запись заблокирована", - "minimum-max-failed-login-attempts-range": "Максимальное количество неудачных попыток входа в систему не может быть отрицательным", - "user-lockout-notification-email": "В случае блокировки учетной записи пользователя отправьте уведомление на электронную почту" - }, - "alarm": { - "alarm": "Оповещение", - "alarms": "Оповещения", - "select-alarm": "Выбрать оповещение", - "no-alarms-matching": "Оповещения '{{entity}}' не найдены.", - "alarm-required": "Оповещение обязательно", - "alarm-status": "Статус оповещения", - "search-status": { - "ANY": "Все", - "ACTIVE": "Активные", - "CLEARED": "Сброшенные", - "ACK": "Подтвержденные", - "UNACK": "Неподтвержденные" - }, - "display-status": { - "ACTIVE_UNACK": "Активные неподтвержденные", - "ACTIVE_ACK": "Активные подтвержденные", - "CLEARED_UNACK": "Сброшенные неподтвержденные", - "CLEARED_ACK": "Сброшенные подтвержденные" - }, - "no-alarms-prompt": "Оповещения отсутствуют", - "created-time": "Время создания", - "type": "Тип", - "severity": "Уровень", - "originator": "Инициатор", - "originator-type": "Тип инициатора", - "details": "Подробности", - "status": "Статус", - "alarm-details": "Подробности об оповещении", - "start-time": "Время начала", - "end-time": "Время окончания", - "ack-time": "Время подтверждения", - "clear-time": "Время сброса", - "severity-critical": "Критический", - "severity-major": "Основной", - "severity-minor": "Второстепенный", - "severity-warning": "Предупреждение", - "severity-indeterminate": "Неопределенный", - "acknowledge": "Подтвердить", - "clear": "Сбросить", - "search": "Поиск оповещений", - "selected-alarms": "Выбрано { count, plural, 1 {1 оповещение} few {# оповещения} other {# оповещений} }", - "no-data": "Нет данных для отображения", - "polling-interval": "Интервал опроса оповещений (сек)", - "polling-interval-required": "Интервал опроса оповещений обязателен.", - "min-polling-interval-message": "Минимальный интервал опроса оповещений 1 секунда.", - "aknowledge-alarms-title": "Подтвердить { count, plural, 1 {1 оповещение} other {# оповещений} }", - "aknowledge-alarms-text": "Вы точно хотите подтвердить { count, plural, 1 {1 оповещение} other {# оповещений} }?", - "aknowledge-alarm-title": "Подтвердить оповещение", - "aknowledge-alarm-text": "Вы точно хотите подтвердить оповещение?", - "clear-alarms-title": "Сбросить { count, plural, 1 {1 оповещение} other {# оповещений} }", - "clear-alarms-text": "Вы точно хотите сбросить { count, plural, 1 {1 оповещение} other {# оповещений} }?", - "clear-alarm-title": "Сбросить оповещение", - "clear-alarm-text": "Вы точно хотите сбросить оповещение?", - "alarm-status-filter": "Фильтр оповещений", - "max-count-load": "Максимальное количество оповещений для загрузки (0 - неограниченно)", - "max-count-load-required": "Максимальное количество оповещений для загрузки обязателен.", - "max-count-load-error-min": "Минимальное значение 0.", - "fetch-size": "Размер пакета для загрузки", - "fetch-size-required": "Размер пакета для загрузки обязателен.", - "fetch-size-error-min": "Минимальное значение 10." - }, - "alias": { - "add": "Добавить псевдоним", - "edit": "Редактировать псевдоним", - "name": "Псевдоним", - "name-required": "Псевдоним обязателен", - "duplicate-alias": "Такой псевдоним уже существует.", - "filter-type-single-entity": "Отдельный объект", - "filter-type-entity-list": "Список объектов", - "filter-type-entity-name": "Название объекта", - "filter-type-state-entity": "Объект из состояния дашборда", - "filter-type-state-entity-description": "Объект, полученный из параметров состояния дашборда", - "filter-type-asset-type": "Тип актива", - "filter-type-asset-type-description": "Активы типа '{{assetType}}'", - "filter-type-asset-type-and-name-description": "Активы типа '{{assetType}}' и названием, начинающимся с '{{prefix}}'", - "filter-type-device-type": "Тип устройства", - "filter-type-device-type-description": "Устройства типа '{{deviceType}}'", - "filter-type-device-type-and-name-description": "Устройства типа '{{deviceType}}' и названием, начинающимся с '{{prefix}}'", - "filter-type-entity-view-type": "Тип Представления Объекта", - "filter-type-entity-view-type-description": "Представления Объекта типа '{{entityView}}'", - "filter-type-entity-view-type-and-name-description": "Представления Объекта типа '{{entityView}}' и названием, начинающимся с '{{prefix}}'", - "filter-type-relations-query": "Запрос по типу отношений", - "filter-type-relations-query-description": "{{entities}}, имеющие отношение типа {{relationType}} {{direction}} {{rootEntity}}", - "filter-type-asset-search-query": "Поисковый запрос по активам", - "filter-type-asset-search-query-description": "Активы типа {{assetTypes}}, имеющие отношение типа {{relationType}} {{direction}} {{rootEntity}}", - "filter-type-device-search-query": "Поисковый запрос по устройствам", - "filter-type-device-search-query-description": "Устройства типа {{deviceTypes}}, имеющие отношение типа {{relationType}} {{direction}} {{rootEntity}}", - "filter-type-entity-view-search-query": "Поисковый запрос по представлениям объектов", - "filter-type-entity-view-search-query-description": "Представления объектов типа {{entityViewTypes}}, имеющие отношение типа {{relationType}} {{direction}} {{rootEntity}}", - "entity-filter": "Фильтр объектов", - "resolve-multiple": "Принять как несколько объектов", - "filter-type": "Тип фильтра", - "filter-type-required": "Тип фильтра обязателен.", - "entity-filter-no-entity-matched": "Объекты, соответствующие фильтру, не найдены.", - "no-entity-filter-specified": "Не указан фильтр объектов", - "root-state-entity": "Использовать объект, полученный из дашборда, как корневой", - "last-level-relation": "Использовать только отношения последнего уровня", - "root-entity": "Корневой объект", - "state-entity-parameter-name": "Название объекта состояния", - "default-state-entity": "Объект состояния по умолчанию", - "default-entity-parameter-name": "По умолчанию", - "max-relation-level": "Максимальная глубина отношений", - "unlimited-level": "Неограниченная глубина", - "state-entity": "Объект состояния дашборда", - "all-entities": "Все объекты", - "any-relation": "не указано" - }, - "asset": { - "asset": "Актив", - "assets": "Активы", - "management": "Управление активами", - "view-assets": "Просмотреть активы", - "add": "Добавить актив", - "assign-to-customer": "Присвоить клиенту", - "assign-asset-to-customer": "Присвоить актив(ы) клиенту", - "assign-asset-to-customer-text": "Пожалуйста, выберите активы, которые нужно присвоить объекту", - "no-assets-text": "Активы не найдены", - "assign-to-customer-text": "Пожалуйста, выберите клиента, которому нужно присвоить актив(ы)", - "public": "Общедоступные", - "assignedToCustomer": "Присвоить клиенту", - "make-public": "Открыть общий доступ к активу", - "make-private": "Закрыть общий доступ к активу", - "unassign-from-customer": "Отозвать у клиента", - "delete": "Удалить актив", - "asset-public": "Актив общедоступный", - "asset-type": "Тип актива", - "asset-type-required": "Тип актива обязателен.", - "select-asset-type": "Выберите тип актива", - "enter-asset-type": "Введите тип актива", - "any-asset": "Любой актив", - "no-asset-types-matching": "Активы типа '{{entitySubtype}}' не найдены.", - "asset-type-list-empty": "Типы активов не выбраны.", - "asset-types": "Типы активов", - "name": "Название", - "name-required": "Название обязательно.", - "description": "Описание", - "type": "Тип", - "type-required": "Тип обязателен.", - "details": "Подробности", - "events": "События", - "add-asset-text": "Добавить новый актив", - "asset-details": "Подробности об активе", - "assign-assets": "Присвоить активы", - "assign-assets-text": "Присвоить { count, plural, 1 {1 актив} few {# актива} other {# активов} } клиенту", - "delete-assets": "Удалить активы", - "unassign-assets": "Отозвать активы", - "unassign-assets-action-title": "Отозвать { count, plural, 1 {1 актив} few {# актива} other {# активов} } у клиента", - "assign-new-asset": "Присвоить новый актив", - "delete-asset-title": "Вы точно хотите удалить '{{assetName}}'?", - "delete-asset-text": "Внимание, после подтверждения актив и все связанные с ним данные будут безвозвратно удалены.", - "delete-assets-title": "Вы точно хотите удалить { count, plural, 1 {1 актив} few {# актива} other {# активов} }", - "delete-assets-action-title": "Удалить { count, plural, 1 {1 актив} few {# актива} other {# активов} }", - "delete-assets-text": "Внимание, после подтверждения выбранные активы и все связанные с ними данные будут безвозвратно удалены.", - "make-public-asset-title": "Вы точно хотите открыть общий доступ к активу '{{assetName}}'?", - "make-public-asset-text": "Внимание, после подтверждения актив и все связанные с ним данные станут общедоступными.", - "make-private-asset-title": "Вы точно хотите закрыть общий доступ к активу '{{assetName}}'?", - "make-private-asset-text": "После подтверждения актив и все связанные с ним данные будут закрыты для общего доступа", - "unassign-asset-title": "Вы точно хотите отозвать актив '{{assetName}}'?", - "unassign-asset-text": "После подтверждения актив будут отозван, и клиент потеряет к нему доступ.", - "unassign-asset": "Отозвать актив", - "unassign-assets-title": "Вы точно хотите отозвать { count, plural, 1 {1 актив} few {# актива} other {# активов} }?", - "unassign-assets-text": "После подтверждения активы будут отозваны, и клиент потеряет к ним доступ.", - "copyId": "Копировать ИД актива", - "idCopiedMessage": "ИД актива скопировано в буфер обмена", - "select-asset": "Выбрать активы", - "no-assets-matching": "Активы, соответствующие '{{entity}}', не найдены.", - "asset-required": "Актив обязателен", - "name-starts-with": "Название актива, начинающееся с", - "import": "Импортировать активы", - "asset-file": "Файл с активами", - "label": "Метка" - }, - "attribute": { - "attributes": "Атрибуты", - "latest-telemetry": "Последняя телеметрия", - "attributes-scope": "Контекст атрибутов объекта", - "scope-latest-telemetry": "Последняя телеметрия", - "scope-client": "Клиентские атрибуты", - "scope-server": "Серверные атрибуты", - "scope-shared": "Общие атрибуты", - "add": "Добавить атрибут", - "key": "Ключ", - "last-update-time": "Последнее обновление", - "key-required": "Ключ атрибута обязателен.", - "value": "Значение", - "value-required": "Значение атрибута обязательно.", - "delete-attributes-title": "Вы уверенны, что хотите удалить { count, plural, one {1 атрибут} few {# атрибута} other {# атрибутов} }? ", - "delete-attributes-text": "Внимание, после подтверждения выбранные атрибуты будут удалены.", - "delete-attributes": "Удалить атрибуты", - "enter-attribute-value": "Введите значение атрибута", - "show-on-widget": "Показать на виджете", - "widget-mode": "Виджет-режим", - "next-widget": "Следующий виджет", - "prev-widget": "Предыдущий виджет", - "add-to-dashboard": "Добавить на дашборд", - "add-widget-to-dashboard": "Добавить виджет на дашборд", - "selected-attributes": "{ count, plural, 1 {Выбран} other {Выбраны} } { count, plural, one {1 атрибут} few {# атрибута} other {# атрибутов} }", - "selected-telemetry": "{ count, plural, 1 {Выбран} other {Выбраны} } { count, plural, 1 {1 параметр} few {# параметра} other {# параметров} } телеметрии" - }, - "audit-log": { - "audit": "Аудит", - "audit-logs": "Логи аудита", - "timestamp": "Время", - "entity-type": "Тип объекта", - "entity-name": "Название объекта", - "user": "Пользователь", - "type": "Тип", - "status": "Статус", - "details": "Подробности", - "type-added": "Добавленный", - "type-deleted": "Удаленный", - "type-updated": "Обновленный", - "type-attributes-updated": "Обновлены атрибуты", - "type-attributes-deleted": "Удалены атрибуты", - "type-rpc-call": "RPC вызов", - "type-credentials-updated": "Обновлены учетные данные", - "type-assigned-to-customer": "Присвоен клиенту", - "type-unassigned-from-customer": "Отозван у клиента", - "type-activated": "Активирован", - "type-suspended": "Приостановлен", - "type-credentials-read": "Чтение учетные данных", - "type-attributes-read": "Чтение атрибутов", - "type-relation-add-or-update": "Обновлены отношения", - "type-relation-delete": "Удалены отношения", - "type-relations-delete": "Удалены все отношения", - "type-alarm-ack": "Подтвержден", - "type-alarm-clear": "Сброшен", - "type-login": "Вход", - "type-logout": "Выход", - "type-lockout": "Заблокирован", - "status-success": "Успех", - "status-failure": "Сбой", - "audit-log-details": "Подробности аудит лога", - "no-audit-logs-prompt": "Логи не найдены", - "action-data": "Данные действия", - "failure-details": "Подробности сбоя", - "search": "Поиск аудит логов", - "clear-search": "Очистить поиск" - }, - "confirm-on-exit": { - "message": "У вас есть несохраненные изменения. Вы точно хотите покинуть эту страницу?", - "html-message": "У вас есть несохраненные изменения.
    Вы точно хотите покинуть эту страницу?", - "title": "Несохраненные изменения" - }, - "contact": { - "country": "Страна", - "city": "Город", - "state": "Штат", - "postal-code": "Почтовый код", - "postal-code-invalid": "Допустимы только цифры", - "address": "Адрес", - "address2": "Адрес 2", - "phone": "Телефон", - "email": "Эл. адрес", - "no-address": "Адрес не указан" - }, - "common": { - "username": "Имя пользователя", - "password": "Пароль", - "enter-username": "Введите имя пользователя", - "enter-password": "Введите пароль", - "enter-search": "Введите условие поиска" - }, - "content-type": { - "json": "Json", - "text": "Текстовый", - "binary": "Бинарный (Base64)" - }, - "customer": { - "customer": "Клиент", - "customers": "Клиенты", - "management": "Управление клиентами", - "dashboard": "Дашборд клиента", - "dashboards": "Дашборды клиента", - "devices": "Устройства клиента", - "entity-views": "Представления объектов клиента", - "assets": "Активы клиента", - "public-dashboards": "Общедоступные дашборды", - "public-devices": "Общедоступные устройства", - "public-assets": "Общедоступные активы", - "public-entity-views": "Общедоступные представления объектов", - "add": "Добавить клиента", - "delete": "Удалить клиента", - "manage-customer-users": "Управление пользователями клиента", - "manage-customer-devices": "Управление устройствами клиента", - "manage-customer-dashboards": "Управление дашбордами клиента", - "manage-public-devices": "Управление общедоступными устройствами", - "manage-public-dashboards": "Управление общедоступными дашбордами", - "manage-customer-assets": "Управление активами клиента", - "manage-public-assets": "Управление общедоступными активами", - "add-customer-text": "Добавить нового клиента", - "no-customers-text": "Клиенты не найдены", - "customer-details": "Подробности о клиенте", - "delete-customer-title": "Вы точно хотите удалить клиента '{{customerTitle}}'?", - "delete-customer-text": "Внимание, после подтверждения клиент и все связанные с ним данные будут безвозвратно удалены.", - "delete-customers-title": "Вы точно хотите удалить { count, plural, 1 {1 клиент} few {# клиента} other {# клиентов} }?", - "delete-customers-action-title": "Удалить { count, plural, 1 {1 клиент} few {# клиента} other {# клиентов} }", - "delete-customers-text": "Внимание, после подтверждения выбранные клиенты и все связанные с ними данные будут безвозвратно удалены.", - "manage-users": "Управление пользователями", - "manage-assets": "Управление активами", - "manage-devices": "Управление устройствами", - "manage-dashboards": "Управление дашбордами", - "title": "Имя", - "title-required": "Название обязательно.", - "description": "Описание", - "details": "Подробности", - "events": "События", - "copyId": "Копировать ИД клиента", - "idCopiedMessage": "ИД клиента скопирован в буфер обмена", - "select-customer": "Выбрать клиента", - "no-customers-matching": "Клиенты, соответствующие '{{entity}}', не найдены.", - "customer-required": "Клиент обязателен", - "select-default-customer": "Выбрать клиента по умолчанию", - "default-customer": "Клиент по умолчанию", - "default-customer-required": "Клиент по умолчанию обязателен для отладки дашборда на уровне на уровне Владельца" - }, - "datetime": { - "date-from": "Дата с", - "time-from": "Время с", - "date-to": "Дата до", - "time-to": "Время до" - }, - "dashboard": { - "dashboard": "Дашборд", - "dashboards": "Дашборды", - "management": "Управление дашбордами", - "view-dashboards": "Просмотреть дашборды", - "add": "Добавить дашборд", - "assign-dashboard-to-customer": "Прикрепить дашборд(ы) к клиенту", - "assign-dashboard-to-customer-text": "Пожалуйста, выберите дашборды, которые нужно прикрепить к клиенту", - "assign-to-customer-text": "Пожалуйста, выберите клиента, к которому нужно прикрепить дашборд(ы)", - "assign-to-customer": "Прикрепить к клиенту", - "unassign-from-customer": "Отозвать у клиента", - "make-public": "Открыть дашборд для общего доступа", - "make-private": "Закрыть дашборд для общего доступа", - "manage-assigned-customers": "Управление назначенными клиентами", - "assigned-customers": "Назначенные клиенты", - "assign-to-customers": "Присвоить дашборд(ы) клиенту", - "assign-to-customers-text": "Пожалуйста, выбери клиентов, которым нужно присвоить дашборд(ы)", - "unassign-from-customers": "Отозвать дашборд(ы) у клиентов", - "unassign-from-customers-text": "Пожалуйста, выберите клиентов, у которых нужно отозвать дашборд(ы)", - "no-dashboards-text": "Дашборды не найдены", - "no-widgets": "Виджеты не сконфигурированы", - "add-widget": "Добавить новый виджет", - "title": "Название", - "select-widget-title": "Выберите виджет", - "select-widget-subtitle": "Список доступных виджетов", - "delete": "Удалить дашборд", - "title-required": "Название обязательно.", - "description": "Описание", - "details": "Подробности", - "dashboard-details": "Подробности о дашборде", - "add-dashboard-text": "Добавить новый дашборд", - "assign-dashboards": "Прикрепить дашборды", - "assign-new-dashboard": "Прикрепить новый дашборд", - "assign-dashboards-text": "Прикрепить { count, plural, 1 {1 дашборд} few {# дашборда} other {# дашбордов} } к клиенту", - "unassign-dashboards-action-text": "Отозвать { count, plural, 1 {1 дашборд} few {# дашборда} other {# дашбордов} } у клиента", - "delete-dashboards": "Удалить дашборды", - "unassign-dashboards": "Отозвать дашборды", - "unassign-dashboards-action-title": "Отозвать { count, plural, one {1 дашборд} few {# дашборда} other {# дашбордов} } у клиента", - "delete-dashboard-title": "Вы точно хотите удалить дашборд '{{dashboardTitle}}'?", - "delete-dashboard-text": "Внимание, после подтверждения дашборд и все связанные с ним данные будут безвозвратно утеряны.", - "delete-dashboards-title": "Вы точно хотите удалить { count, plural, one {1 дашборд} few {# дашборда} other {# дашбордов} }?", - "delete-dashboards-action-title": "Удалить { count, plural, one {1 дашборд} few {# дашборда} other {# дашбордов} }", - "delete-dashboards-text": "Внимание, после подтверждения дашборды и все связанные с ними данные будут безвозвратно утеряны.", - "unassign-dashboard-title": "Вы точно хотите отозвать дашборд '{{dashboardTitle}}'?", - "unassign-dashboard-text": "После подтверждения дашборд не будет доступен клиенту.", - "unassign-dashboard": "Отозвать дашборд", - "unassign-dashboards-title": "Вы точно хотите отозвать { count, plural, one {1 дашборд} few {# дашборда} other {# дашбордов} }?", - "unassign-dashboards-text": "После подтверждения выбранные дашборды не будут доступны клиенту.", - "public-dashboard-title": "Теперь дашборд общедоступный", - "public-dashboard-text": "Теперь ваш дашборд {{dashboardTitle}} доступен всем по ссылке:", - "public-dashboard-notice": "Примечание: Для получения доступа к данным устройства нужно открыть общий доступ к этому устройству.", - "make-private-dashboard-title": "Вы точно хотите закрыть общий доступ к дашборду '{{dashboardTitle}}'?", - "make-private-dashboard-text": "После подтверждения дашборд будет закрыт для общего доступа.", - "make-private-dashboard": "Закрыть дашборд для общего доступа", - "socialshare-text": "'{{dashboardTitle}}' сделано ThingsBoard", - "socialshare-title": "'{{dashboardTitle}}' сделано ThingsBoard", - "select-dashboard": "Выберите дашборд", - "no-dashboards-matching": "Дашборд '{{entity}}' не найден.", - "dashboard-required": "Дашборд обязателен.", - "select-existing": "Выберите существующий дашборд", - "create-new": "Создать новый дашборд", - "new-dashboard-title": "Новое название дашборда", - "open-dashboard": "Открыть дашборд", - "set-background": "Установить фон", - "background-color": "Фоновый цвет", - "background-image": "Фоновая картинка", - "background-size-mode": "Размер фона", - "no-image": "Картинка не выбрана", - "drop-image": "Перетащите картинку или кликните для выбора файла.", - "settings": "Настройки", - "columns-count": "Количество колонок", - "columns-count-required": "Количество колонок обязательно.", - "min-columns-count-message": "Минимальное число колонок - 10.", - "max-columns-count-message": "Максимальное число колонок - 1000.", - "widgets-margins": "Величина отступа между виджетами", - "horizontal-margin": "Величина горизонтального отступа", - "horizontal-margin-required": "Величина горизонтального отступа обязательна.", - "min-horizontal-margin-message": "Минимальная величина горизонтального отступа - 0.", - "max-horizontal-margin-message": "Максимальная величина горизонтального отступа - 50.", - "vertical-margin": "Величина вертикального отступа", - "vertical-margin-required": "Величина вертикального отступа обязательна.", - "min-vertical-margin-message": "Минимальная величина вертикального отступа - 0.", - "max-vertical-margin-message": "Максимальная величина вертикального отступа - 50.", - "autofill-height": "Автозаполнение по высоте", - "mobile-layout": "Настройки мобильного режима", - "mobile-row-height": "Высота строки в мобильном режиме, px", - "mobile-row-height-required": "Высота строки в мобильном режиме обязательна.", - "min-mobile-row-height-message": "Минимальная высота строки в мобильном режиме составляет 5 px.", - "max-mobile-row-height-message": "Максимальная высота строки в мобильном режиме составляет 200 px.", - "display-title": "Показать название дашборда", - "toolbar-always-open": "Отображать панель инструментов", - "title-color": "Цвет названия", - "display-dashboards-selection": "Отображать выборку дашбордов", - "display-entities-selection": "Отображать выбору объектов", - "display-dashboard-timewindow": "Показать временное окно", - "display-dashboard-export": "Показать экспорт", - "import": "Импортировать дашборд", - "export": "Экспортировать дашборд", - "export-failed-error": "Не удалось экспортировать дашборд: {{error}}", - "create-new-dashboard": "Создать новый дашборд", - "dashboard-file": "Файл дашборда", - "invalid-dashboard-file-error": "Не удалось импортировать дашборд: неизвестная схема данных дашборда.", - "dashboard-import-missing-aliases-title": "Настроить псевдонимы импортированного дашборда", - "create-new-widget": "Создать новый виджет", - "import-widget": "Импортировать виджет", - "widget-file": "Виджет-файл", - "invalid-widget-file-error": "Не удалось импортировать виджет: неправильный формат данных.", - "widget-import-missing-aliases-title": "Настроить псевдонимы, которые использует импортированный виджет", - "open-toolbar": "Открыть панель инструментов дашборда", - "close-toolbar": "Закрыть панель инструментов", - "configuration-error": "Ошибка в настройках", - "alias-resolution-error-title": "Ошибка в настройках псевдонимов дашборда", - "invalid-aliases-config": "Не удалось найти устройство, соответствующее фильтру псевдонимов.
    Пожалуйста, обратитесь к администратору для устранения неполадки.", - "select-devices": "Выберите устройства", - "assignedToCustomer": "Присвоенные клиенту", - "assignedToCustomers": "Присвоенные клиентам", - "public": "Публичный", - "public-link": "Публичная ссылка", - "copy-public-link": "Копировать публичную ссылку", - "public-link-copied-message": "Публичная ссылка на дашборд скопирована в буфер обмена.", - "manage-states": "Управление состоянием дашборда", - "states": "Состояния дашборда", - "search-states": "Поиск состояния дашборда", - "selected-states": "Выбрано { count, plural, 1 {1 состояние} few {# состояния} other {# состояний} } дашборда", - "edit-state": "Изменить состояние дашборда", - "delete-state": "Удалить состояние дашборда", - "add-state": "Добавить состояние дашборда", - "state": "Состояние дашборда", - "state-name": "Название", - "state-name-required": "Название состояния дашборда обязательно.", - "state-id": "ИД состояния", - "state-id-required": "ИД состояния дашборда обязателен.", - "state-id-exists": "Состояния дашборда с таким именем уже существует.", - "is-root-state": "Корневое состояние", - "delete-state-title": "Удалить состояние дашборда", - "delete-state-text": "Вы точно хотите удалить состояние дашборда '{{stateName}}'?", - "show-details": "Показать подробности", - "hide-details": "Скрыть подробности", - "select-state": "Выбрать состояние", - "state-controller": "Контроллер состояния" - }, - "datakey": { - "settings": "Настройки", - "advanced": "Дополнительно", - "label": "Метка", - "color": "Цвет", - "units": "Укажите символы, которые нужно указывать после значения", - "decimals": "Число знаков после запятой", - "data-generation-func": "Функция генерации данных", - "use-data-post-processing-func": "Использовать функцию пост-обработки данных", - "configuration": "Конфигурация ключа данных", - "timeseries": "Телеметрия", - "attributes": "Атрибуты", - "entity-field": "Поле объекта", - "alarm": "Параметры оповещения", - "timeseries-required": "Телеметрия объекта обязательна.", - "timeseries-or-attributes-required": "Телеметрия/атрибуты обязательны.", - "maximum-timeseries-or-attributes": "Максимальное количество параметров телеметрии или атрибутов равно {{count}}", - "alarm-fields-required": "Параметры оповещения обязательны.", - "function-types": "Тип функции", - "function-types-required": "Тип функции обязателен.", - "maximum-function-types": "Максимальное количество типов функции равно {{count}}", - "time-description": "время текущего значения;", - "value-description": "текущее значение;", - "prev-value-description": "результат предыдущего вызова функции;", - "time-prev-description": "время предыдущего значения;", - "prev-orig-value-description": "исходное предыдущее значение;" - }, - "datasource": { - "type": "Тип источника данных", - "name": "Название", - "add-datasource-prompt": "Пожалуйста, добавьте источник данных" - }, - "details": { - "edit-mode": "Режим редактирования", - "edit-json": "Редактировать JSON", - "toggle-edit-mode": "Режим редактирования" - }, - "device": { - "device": "Устройство", - "device-required": "Устройство обязательно.", - "devices": "Устройства", - "management": "Управление устройствами", - "view-devices": "Просмотреть устройства", - "device-alias": "Псевдоним устройства", - "aliases": "Псевдонимы устройства", - "no-alias-matching": "'{{alias}}' не найден.", - "no-aliases-found": "Псевдонимы не найдены.", - "no-key-matching": "'{{key}}' не найден.", - "no-keys-found": "Ключи не найдены.", - "create-new-alias": "Создать новый!", - "create-new-key": "Создать новый!", - "duplicate-alias-error": "Найден дублирующийся псевдоним '{{alias}}'.
    В рамках дашборда псевдонимы устройств должны быть уникальными.", - "configure-alias": "Настроить '{{alias}}' псевдоним", - "no-devices-matching": "Устройство '{{entity}}' не найдено.", - "alias": "Псевдоним", - "alias-required": "Псевдоним устройства обязателен.", - "remove-alias": "Удалить псевдоним устройства", - "add-alias": "Добавить псевдоним устройства", - "name-starts-with": "Название начинается с", - "device-list": "Список устройств", - "use-device-name-filter": "Использовать фильтр", - "device-list-empty": "Устройства не выбраны.", - "device-name-filter-required": "Фильтр названия устройства обязателен.", - "device-name-filter-no-device-matched": "Устройства, названия которых начинаются с '{{device}}', не найдены.", - "add": "Добавить устройство", - "assign-to-customer": "Присвоить клиенту", - "assign-device-to-customer": "Присвоить устройство(а) клиенту", - "assign-device-to-customer-text": "Пожалуйста, выберите устройства, которые нужно присвоить клиенту", - "make-public": "Открыть общий доступ к устройству", - "make-private": "Закрыть общий доступ к устройству", - "no-devices-text": "Устройства не найдены", - "assign-to-customer-text": "Пожалуйста, выберите клиента, которому нужно присвоить устройство(а)", - "device-details": "Подробности об устройстве", - "add-device-text": "Добавить новое устройство", - "credentials": "Учетные данные", - "manage-credentials": "Управление учетными данными", - "delete": "Удалить устройство", - "assign-devices": "Присвоить устройство", - "assign-devices-text": "Присвоить { count, plural, one {1 устройство} few {# устройства} other {# устройств} } клиенту", - "delete-devices": "Удалить устройства", - "unassign-from-customer": "Отозвать у клиенту", - "unassign-devices": "Отозвать устройства", - "unassign-devices-action-title": "Отозвать у клиента { count, plural, one {1 устройство} few {# устройства} other {# устройств} }", - "assign-new-device": "Присвоить новое устройство", - "make-public-device-title": "Вы точно хотите открыть общий доступ к устройству '{{deviceName}}'?", - "make-public-device-text": "После подтверждения устройство и все связанные с ним данные будут общедоступными.", - "make-private-device-title": "Вы точно хотите закрыть общий доступ к устройству '{{deviceName}}'", - "make-private-device-text": "После подтверждения устройство и все связанные с ним данные будут закрыты для общего доступа.", - "view-credentials": "Просмотреть учетные данные", - "delete-device-title": "Вы точно хотите удалить устройство '{{deviceName}}'?", - "delete-device-text": "Внимание, после подтверждения устройство и все связанные с ним данные будут безвозвратно утеряны.", - "delete-devices-title": "Вы точно хотите удалить { count, plural, one {1 устройство} few {# устройства} other {# устройств} }?", - "delete-devices-action-title": "Удалить { count, plural, one {1 устройство} few {# устройства} other {# устройств} }", - "delete-devices-text": "Внимание, после подтверждения выбранные устройства и все связанные с ними данные будут безвозвратно утеряны..", - "unassign-device-title": "Вы точно хотите отозвать устройство '{{deviceName}}'?", - "unassign-device-text": "После подтверждения устройство будет недоступно клиенту.", - "unassign-device": "Отозвать устройство", - "unassign-devices-title": "Вы точно хотите отозвать { count, plural, one {1 устройство} few {# устройства} other {# устройств} }?", - "unassign-devices-text": "После подтверждения выбранные устройства будут недоступны клиенту.", - "device-credentials": "Учетные данные устройства", - "credentials-type": "Тип учетных данных", - "access-token": "Токен", - "access-token-required": "Токен обязателен.", - "access-token-invalid": "Длина токена должна быть от 1 до 20 символов.", - "rsa-key": "Открытый ключ RSA", - "rsa-key-required": "Открытый ключ RSA обязателен.", - "secret": "Секрет", - "secret-required": "Секрет обязателен.", - "device-type": "Тип устройства", - "device-type-required": "Тип устройства обязатеен.", - "select-device-type": "Выберите тип устройства", - "enter-device-type": "Введите тип устройства", - "any-device": "Любое устройство", - "no-device-types-matching": "Тип устройства, соответствующий '{{entitySubtype}}', не найден.", - "device-type-list-empty": "Не выбран тип устройства.", - "device-types": "Типы устройств", - "name": "Название", - "name-required": "Название обязательно.", - "description": "Описание", - "events": "События", - "details": "Подробности", - "copyId": "Копировать идентификатор устройства", - "copyAccessToken": "Копировать токен", - "idCopiedMessage": "Идентификатор устройства скопирован в буфер обмена", - "accessTokenCopiedMessage": "Токен устройства скопирован в буфер обмена", - "assignedToCustomer": "Присвоен клиенту", - "unable-delete-device-alias-title": "Не удалось удалить псевдоним устройства", - "unable-delete-device-alias-text": "Не удалось удалить псевдоним '{{deviceAlias}}' устройства, т.к. он используется следующими виджетами:
    {{widgetsList}}", - "is-gateway": "Гейтвей", - "public": "Общедоступный", - "device-public": "Устройство общедоступно", - "select-device": "Выбрать устройство", - "import": "Импортировать устройства", - "device-file": "Файл с устройствами" - }, - "dialog": { - "close": "Закрыть диалог" - }, - "direction": { - "column": "Колонка", - "row": "Строка" - }, - "error": { - "unable-to-connect": "Не удалось подключиться к серверу! Пожалуйста, проверьте интернет-соединение.", - "unhandled-error-code": "Код необработанной ошибки: {{errorCode}}", - "unknown-error": "Неизвестная ошибка" - }, - "entity": { - "entity": "Объект", - "entities": "Объекты", - "aliases": "Псевдонимы объекта", - "entity-alias": "Псевдоним объекта", - "unable-delete-entity-alias-title": "Не удалось удалить псевдоним объекта", - "unable-delete-entity-alias-text": "Псевдоним объекта '{{entityAlias}}' не может быть удален, так как используется следующим(и) виджетом(ами):
    {{widgetsList}}", - "duplicate-alias-error": "Найден дубликат псевдонима '{{alias}}'.
    В рамках одного дашборда псевдонимы объектов должны быть уникальными.", - "missing-entity-filter-error": "Отсутствует фильтр для псевдонима '{{alias}}'.", - "configure-alias": "Настроить псевдоним '{{alias}}'", - "alias": "Псевдоним", - "alias-required": "Псевдоним объекта обязателен.", - "remove-alias": "Убрать псевдоним объекта", - "add-alias": "Добавить псевдоним объекта", - "entity-list": "Список объектов", - "entity-type": "Тип объекта", - "entity-types": "Типы объекта", - "entity-type-list": "Список типов объекта", - "any-entity": "Любой объект", - "enter-entity-type": "Введите тип объекта", - "no-entities-matching": "Объекты, соответствующие '{{entity}}', не найдены.", - "no-entity-types-matching": "Типы объектов, соответствующие '{{entityType}}', не найдены.", - "name-starts-with": "Название, начинающееся с", - "use-entity-name-filter": "Использовать фильтр", - "entity-list-empty": "Не выбраны объекты.", - "entity-type-list-empty": "Не выбраны типы объекта.", - "entity-name-filter-required": "Фильтр по названию объекта обязателен.", - "entity-name-filter-no-entity-matched": "Объекты, чье название начинается с '{{entity}}', не найдены.", - "all-subtypes": "Все", - "select-entities": "Выберите объекты", - "no-aliases-found": "Псевдонимы не найдены.", - "no-alias-matching": "Псевдоним '{{alias}}' не найден.", - "create-new-alias": "Создать новый!", - "key": "Ключ", - "key-name": "Название ключа", - "no-keys-found": "Ключ не найден.", - "no-key-matching": "Ключ '{{key}}' не найден.", - "create-new-key": "Создать новый!", - "type": "Тип", - "type-required": "Тип объекта обязателен.", - "type-device": "Устройство", - "type-devices": "Устройства", - "list-of-devices": "{ count, plural, 1 {Одно устройство} other {Список из # устройств} }", - "device-name-starts-with": "Устройства, чьи название начинается с '{{prefix}}'", - "type-asset": "Актив", - "type-assets": "Активы", - "list-of-assets": "{ count, plural, 1 {Один актив} other {Список из # активов} }", - "asset-name-starts-with": "Активы, чьи название начинается с '{{prefix}}'", - "type-entity-view": "Представление Объекта", - "type-entity-views": "Представления Объекта", - "list-of-entity-views": "{ count, plural, 1 {Одно представление объекта} other {Список из # представлений объекта} }", - "entity-view-name-starts-with": "Представления Объекта, чьи название начинается с '{{prefix}}'", - "type-rule": "Правило", - "type-rules": "Правила", - "list-of-rules": "{ count, plural, 1 {Одно правило} other {Список из # правил} }", - "rule-name-starts-with": "Правила, чьи названия начинаются с '{{prefix}}'", - "type-plugin": "Плагин", - "type-plugins": "Плагины", - "list-of-plugins": "{ count, plural, 1 {Один плагин} other {Список из # плагинов} }", - "plugin-name-starts-with": "Плагины, чьи имена начинаются с '{{prefix}}'", - "type-tenant": "Владелец", - "type-tenants": "Владельцы", - "list-of-tenants": "{ count, plural, 1 {Один владелец} other {Список из # владельцев} }", - "tenant-name-starts-with": "Владельцы, чьи имена начинаются с '{{prefix}}'", - "type-customer": "Клиент", - "type-customers": "Клиенты", - "list-of-customers": "{ count, plural, 1 {Один клиент} other {Список из # клиентов} }", - "customer-name-starts-with": "Клиенты, чьи имена начинаются с '{{prefix}}'", - "type-user": "Пользователь", - "type-users": "Пользователи", - "list-of-users": "{ count, plural, 1 {Один пользователь} other {Список из # пользователей} }", - "user-name-starts-with": "Пользователи, чьи имена начинаются с '{{prefix}}'", - "type-dashboard": "Дашборд", - "type-dashboards": "Дашборды", - "list-of-dashboards": "{ count, plural, 1 {Один дашборд} other {Список из # дашбордов} }", - "dashboard-name-starts-with": "Дашборды, чьи названия начинаются с '{{prefix}}'", - "type-alarm": "Оповещение", - "type-alarms": "Оповещения", - "list-of-alarms": "{ count, plural, 1 {Одно оповещение} other {Список из # оповещений} }", - "alarm-name-starts-with": "Оповещения, чьи названия начинаются с '{{prefix}}'", - "type-rulechain": "Цепочка правил", - "type-rulechains": "Цепочки правил", - "list-of-rulechains": "{ count, plural, 1 {Одна цепочка правил} other {Список из # цепочек правил} }", - "rulechain-name-starts-with": "Цепочки правил, чьи названия начинаются с '{{prefix}}'", - "type-rulenode": "Правило", - "type-rulenodes": "Правила", - "list-of-rulenodes": "{ count, plural, 1 {Одно правило} other {Список из # правил} }", - "rulenode-name-starts-with": "Правила, чьи названия начинаются с '{{prefix}}'", - "type-current-customer": "Текущий клиент", - "type-current-tenant": "Текущий владелец", - "search": "Поиск объектов", - "selected-entities": "Выбран(ы) { count, plural, 1 {1 объект} few {# объекта} other {# объектов} }", - "entity-name": "Название объекта", - "entity-label": "Метка объекта", - "details": "Подробности об объекте", - "no-entities-prompt": "Объекты не найдены", - "no-data": "Нет данных для отображения", - "columns-to-display": "Отобразить следующие колонки" - }, - "entity-field": { - "created-time": "Время создания", - "name": "Название", - "type": "Тип", - "first-name": "Имя", - "last-name": "Фамилия", - "email": "Электронная почта", - "title": "Название", - "country": "Страна", - "state": "Штат/Область", - "city": "Город", - "address": "Адрес", - "address2": "Адрес 2", - "zip": "Индекс", - "phone": "Телефон", - "label": "Метка" - }, - "entity-view": { - "entity-view": "Представление Объекта", - "entity-view-required": "Представление объекта обязательно.", - "entity-views": "Представления Объектов", - "management": "Управление представлениями объектов", - "view-entity-views": "Просмотр представлений объектов", - "entity-view-alias": "Псевдоним Представления Объекта", - "aliases": "Псевдонимы Представления Объекта", - "no-alias-matching": "Псевдоним '{{alias}}' не найден.", - "no-aliases-found": "Псевдонимы не найдены.", - "no-key-matching": "Ключ '{{key}}' не найден.", - "no-keys-found": "Ключи не найдены.", - "create-new-alias": "Создать новый!", - "create-new-key": "Создать новый!", - "duplicate-alias-error": "Найден дубликат псевдонима '{{alias}}'.
    В рамках одного дашборда псевдонимы представлений объектов должны быть уникальными.", - "configure-alias": "Настроить псевдоним '{{alias}}'", - "no-entity-views-matching": "Объекты, соответствующие '{{entity}}', не найдены.", - "alias": "Псевдоним", - "alias-required": "Псевдоним представления объекта обязателен.", - "remove-alias": "Убрать псевдоним представления объекта", - "add-alias": "Добавить псевдоним представления объекта", - "name-starts-with": "Представления объектов, чьи название начинается с", - "entity-view-list": "Список представлений объектов", - "use-entity-view-name-filter": "Использовать фильтр", - "entity-view-list-empty": "Не выбраны представления объектов.", - "entity-view-name-filter-required": "Для представлений объектов фильтр по названиям обязателен.", - "entity-view-name-filter-no-entity-view-matched": "Представление объектов, чьи название начинаются с '{{entityView}}', не найдены.", - "add": "Представление объекта", - "assign-to-customer": "Назначить клиенту", - "assign-entity-view-to-customer": "Назначить представление(я) объекта(ов) клиенту", - "assign-entity-view-to-customer-text": "Пожалуйста, выберите представления объектов, которые нужно назначить клиенту", - "no-entity-views-text": "Представления объектов не найдены", - "assign-to-customer-text": "Пожалуйста, выберите клиента, которому нужно назначить представления объектов", - "entity-view-details": "Подробности о представлении объекта", - "add-entity-view-text": "Добавить новое представление объекта", - "delete": "Удалить представление объекта", - "assign-entity-views": "Назначить представления объектов", - "assign-entity-views-text": "Назначить клиенту { count, plural, 1 {1 представление объекта} few {# представления объектов} other {# представлений объектов} }", - "delete-entity-views": "Удалить представления объектов", - "unassign-from-customer": "Отозвать у клиента", - "unassign-entity-views": "Отозвать представления объектов", - "unassign-entity-views-action-title": "Отозвать у клиента { count, plural, 1 {1 представление объекта} few {# представления объектов} other {# представлений объектов} }", - "assign-new-entity-view": "Назначит новое представление объекта", - "delete-entity-view-title": "Вы точно хотите удалить представление объекта '{{entityViewName}}'?", - "delete-entity-view-text": "Внимание, после подтверждения представление объекта и все связанные с ним данные будут безвозвратно удалены.", - "delete-entity-views-title": "Вы точно хотите удалить { count, plural, 1 {1 представление объекта} few {# представления объектов} other {# представлений объектов} }?", - "delete-entity-views-action-title": "Удалить { count, plural, 1 {1 представление объекта} few {# представления объектов} other {# представлений объектов} }", - "delete-entity-views-text": "Внимание, после подтверждения выбранные представления объектов и все связанные с ними данные будут безвозвратно удалены.", - "unassign-entity-view-title": "Вы точно хотите отозвать представление объекта '{{entityViewName}}'?", - "unassign-entity-view-text": "После подтверждение представление объекта будет недоступно клиенту.", - "unassign-entity-view": "Отозвать представление объекта", - "unassign-entity-views-title": "Вы точно хотите отозвать { count, plural, 1 {1 представление объекта} few {# представления объектов} other {# представлений объектов} }?", - "unassign-entity-views-text": "После подтверждение выбранные представления объектов будет недоступно клиенту.", - "entity-view-type": "Тип представления объекта", - "entity-view-type-required": "Тип представления объекта обязателен.", - "select-entity-view-type": "Выберите тип представления объекта", - "enter-entity-view-type": "Введите тип представления объекта", - "any-entity-view": "Любое представление объекта", - "no-entity-view-types-matching": "Типы представления объекта, соответствующие '{{entitySubtype}}', не найдены.", - "entity-view-type-list-empty": "Не выбраны типы представления объекта.", - "entity-view-types": "Типы представления объекта", - "name": "Название", - "name-required": "Название обязательно.", - "description": "Описание", - "events": "События", - "details": "Подробности", - "copyId": "Копировать ИД представление объекта", - "assignedToCustomer": "Назначено клиенту", - "unable-entity-view-device-alias-title": "Не удалось удалить псевдоним представления объекта", - "unable-entity-view-device-alias-text": "Не удалось удалить псевдоним устройства '{{entityViewAlias}}', т.к. он используется следующими виджетами:
    {{widgetsList}}", - "select-entity-view": "Выбрать представление объекта", - "make-public": "Открыть общий доступ к представлению объекта", - "make-private": "Закрыть общий доступ к представлению объекта", - "start-date": "Дата начала", - "start-ts": "Время начала", - "end-date": "Дата окончания", - "end-ts": "Время окончания", - "date-limits": "Временной лимит", - "client-attributes": "Клиентские атрибуты", - "shared-attributes": "Общие атрибуты", - "server-attributes": "Серверные атрибуты", - "timeseries": "Телеметрия", - "client-attributes-placeholder": "Клиентские атрибуты", - "shared-attributes-placeholder": "Общие атрибуты", - "server-attributes-placeholder": "Серверные атрибуты", - "timeseries-placeholder": "Телеметрия", - "target-entity": "Целевой объект", - "attributes-propagation": "Пробросить атрибуты", - "attributes-propagation-hint": "Представление объекта автоматически копирует указанные атрибуты с Целевого Объекта каждый раз, когда вы сохраняете или обновляете это представление. В целях производительности атрибуты целевого объекта не пробрасываются в представление объекта на каждом их изменении. Вы можете включить автоматический проброс, настроив в вашей цепочке правило \"copy to view\" и соединив его с сообщениями типа \"Post attributes\" и \"Attributes Updated\".", - "timeseries-data": "Данные телеметрии", - "timeseries-data-hint": "Настроить ключи данных телеметрии целевого объекта, которые будут доступны представлению объекта. Эти данные только для чтения.", - "make-public-entity-view-title": "Вы уверенны, что хотите открыть общий доступ к представленю объекта '{{entityViewName}}'?", - "make-public-entity-view-text": "После подтверждения представление объекта и все связанные с ним данные станут публичными и доступными для других пользователей.", - "make-private-entity-view-title": "Вы уверенны, что хотите закрыть общий доступ к представлению объекта '{{entityViewName}}'?", - "make-private-entity-view-text": "После подтверждения представление объекта и все звязанные с ним данные станут приватными и не будут доступны для других пользователей." - }, - "event": { - "event-type": "Тип события", - "type-error": "Ошибка", - "type-lc-event": "Событие жизненного цикла", - "type-stats": "Статистика", - "type-debug-rule-node": "Отладка", - "type-debug-rule-chain": "Отладка", - "no-events-prompt": "События не найдены", - "error": "Ошибка", - "alarm": "Аварийное оповещение", - "event-time": "Время возникновения события", - "server": "Сервер", - "body": "Тело", - "method": "Метод", - "type": "Тип", - "entity": "Объект", - "message-id": "ИД сообщения", - "message-type": "Тип сообщения", - "data-type": "Тип данных", - "relation-type": "Тип отношения", - "metadata": "Метаданные", - "data": "Данные", - "event": "Событие", - "status": "Статус", - "success": "Успех", - "failed": "Неудача", - "messages-processed": "Сообщения обработаны", - "errors-occurred": "Возникли ошибки" - }, - "extension": { - "extensions": "Расширение", - "selected-extensions": "Выбрано { count, plural, 1 {1 расширение} few {# расширения} other {# расширений} }", - "type": "Тип", - "key": "Ключ", - "value": "Значение", - "id": "ИД", - "extension-id": "ИД расширения", - "extension-type": "Тип расширения", - "transformer-json": "JSON *", - "unique-id-required": "Такое ИД расширения уже существует.", - "delete": "Удалить расширение", - "add": "Добавить расширение", - "edit": "Редактировать расширение", - "delete-extension-title": "Вы точно хотите удалить расширение '{{extensionId}}'?", - "delete-extension-text": "Внимание, после подтверждения расширение и все связанные с ним данные будут безвозвратно удалены.", - "delete-extensions-title": "Вы точно хотите удалить { count, plural, 1 {1 расширение} few {# расширения} other {# расширений} }?", - "delete-extensions-text": "Внимание, после подтверждения выбранные расширения будут удалены.", - "converters": "Конвертеры", - "converter-id": "ИД конвертера", - "configuration": "Конфигурация", - "converter-configurations": "Конфигурация конвертера", - "token": "Токен безопасности", - "add-converter": "Добавить конвертер", - "add-config": "Добавить конфигурацию конвертера", - "device-name-expression": "Маска названия устройства", - "device-type-expression": "Маска типа устройства", - "custom": "Пользовательский", - "to-double": "To Double", - "transformer": "Преобразователь", - "json-required": "JSON преобразователя обязателен.", - "json-parse": "Не удалось распознать JSON преобразователя.", - "attributes": "Атрибуты", - "add-attribute": "Добавить атрибут", - "add-map": "Добавить элемент сопоставления", - "timeseries": "Телеметрия", - "add-timeseries": "Добавить параметр телеметрии", - "field-required": "Параметр обязателен", - "brokers": "Брокеры", - "add-broker": "Добавить брокер", - "host": "Хост", - "port": "Порт", - "port-range": "Значение порта лежит в диапазоне от 1 до 65535.", - "ssl": "SSL", - "credentials": "Учетные данные", - "username": "Имя пользователя", - "password": "Пароль", - "retry-interval": "Интервал повтора в миллисекундах", - "anonymous": "Анонимный", - "basic": "Общий", - "pem": "PEM", - "ca-cert": "Файл CA сертификата *", - "private-key": "Файл приватного ключа *", - "cert": "Файл сертификата *", - "no-file": "Не выбран файл.", - "drop-file": "Перетяните файл или нажмите для выбора файла.", - "mapping": "Сопоставление", - "topic-filter": "Фильтр тем", - "converter-type": "Тип конвертера", - "converter-json": "JSON", - "json-name-expression": "JSON выражение для названия устройства", - "topic-name-expression": "Выражение для названия устройства в названии темы", - "json-type-expression": "JSON выражение для типа устройства", - "topic-type-expression": "Выражение для типа устройства в названии темы", - "attribute-key-expression": "Выражение для атрибута", - "attr-json-key-expression": "JSON выражение для атрибута", - "attr-topic-key-expression": "Выражение для атрибута в названии темы", - "request-id-expression": "Выражение для ИД запроса", - "request-id-json-expression": "JSON выражение для ИД запроса", - "request-id-topic-expression": "Выражение для ИД запроса в названии темы", - "response-topic-expression": "Выражение для темы ответов", - "value-expression": "Выражение для значения", - "topic": "Тема", - "timeout": "Таймаут в миллисекундах", - "converter-json-required": "JSON конвертер обязателен.", - "converter-json-parse": "Не удалось распознать JSON конвертера.", - "filter-expression": "Выражение для фильтрации", - "connect-requests": "Запросы о подключении устройства", - "add-connect-request": "Добавить запросы о подключении устройства", - "disconnect-requests": "Запросы об отсоединении устройства", - "add-disconnect-request": "Добавить запрос об отсоединении устройства", - "attribute-requests": "Запросы для атрибутов", - "add-attribute-request": "Добавить запрос для атрибутов", - "attribute-updates": "Обновление атрибутов", - "add-attribute-update": "Добавить обновление атрибутов", - "server-side-rpc": "Серверный RPC", - "add-server-side-rpc-request": "Добавить серверный RPC", - "device-name-filter": "Фильтр для названия устройства", - "attribute-filter": "Фильтр для атрибутов", - "method-filter": "Фильтр для процедур", - "request-topic-expression": "Выражение для темы запросов", - "response-timeout": "Время ожидания ответа в миллисекундах", - "topic-expression": "Выражение для названия темы", - "client-scope": "Клиентский", - "add-device": "Добавить устройство", - "opc-server": "Серверы", - "opc-add-server": "Добавить сервер", - "opc-add-server-prompt": "Пожалуйста, добавьте сервер", - "opc-application-name": "Название приложения", - "opc-application-uri": "URI приложения", - "opc-scan-period-in-seconds": "Частота сканирования в секундах", - "opc-security": "Безопасность", - "opc-identity": "Идентификация", - "opc-keystore": "Хранилище ключей", - "opc-type": "Тип", - "opc-keystore-type": "Тип", - "opc-keystore-location": "Расположение *", - "opc-keystore-password": "Пароль", - "opc-keystore-alias": "Псевдоним", - "opc-keystore-key-password": "Пароль для ключ", - "opc-device-node-pattern": "Паттерн OPC узла устройства", - "opc-device-name-pattern": "Паттерн названия устройства", - "modbus-server": "Серверы/ведомые устройства", - "modbus-add-server": "Добавить сервер/ведомое устройство", - "modbus-add-server-prompt": "Пожалуйста, добавить сервер/ведомое устройство", - "modbus-transport": "Транспорт", - "modbus-tcp-reconnect": "Переподключатсься автоматически", - "modbus-port-name": "Название последовательного порта", - "modbus-encoding": "Кодирование символов", - "modbus-parity": "Паритет", - "modbus-baudrate": "Скорость передачи", - "modbus-databits": "Биты данных", - "modbus-stopbits": "Стоп-биты", - "modbus-databits-range": "Параметр \"Биты данных\" может принимать значения 7 или 8.", - "modbus-stopbits-range": "Параметр \"Стоп-биты\" может принимать значения 1 или 2.", - "modbus-unit-id": "ИД устройства", - "modbus-unit-id-range": "ИД устройства должен быть в диапазоне от 1 до 247.", - "modbus-device-name": "Название устройства", - "modbus-poll-period": "Частота опроса (в миллисекундах)", - "modbus-attributes-poll-period": "Частота опроса для атрибутов (в миллисекундах)", - "modbus-timeseries-poll-period": "Частота опроса для телеметрии (в миллисекундах)", - "modbus-poll-period-range": "Значение параметра \"Частота опроса\" должно быть больше ноля.", - "modbus-tag": "Тег", - "modbus-function": "Modbus функция", - "modbus-register-address": "Адрес регистра", - "modbus-register-address-range": "Адрес регистра должен быть в диапазоне от 0 до 65535.", - "modbus-register-bit-index": "Номер бита", - "modbus-register-bit-index-range": "Номер бита должен быть в диапазоне от 0 до 15.", - "modbus-register-count": "Количество регистров", - "modbus-register-count-range": "Количество регистров должно быть больше ноля.", - "modbus-byte-order": "Порядок байтов", - "sync": { - "status": "Статус", - "sync": "Синхронизирован", - "not-sync": "Не синхронизирован", - "last-sync-time": "Время последней синхронизации", - "not-available": "Не доступен" - }, - "export-extensions-configuration": "Экспортировать конфигурацию расширений", - "import-extensions-configuration": "Импортировать конфигурацию расширений", - "import-extensions": "Импортировать расширения", - "import-extension": "Импортировать расширение", - "export-extension": "Экспортировать расширение", - "file": "Файл расширений", - "invalid-file-error": "Не правильный формат файла" - }, - "fullscreen": { - "expand": "Во весь экран", - "exit": "Выйти из полноэкранного режима", - "toggle": "Во весь экран", - "fullscreen": "Полноэкранный режим" - }, - "function": { - "function": "Функция" - }, - "grid": { - "delete-item-title": "Вы точно хотите удалить этот объект?", - "delete-item-text": "Внимание, после подтверждения объект и все связанные с ним данные будут безвозвратно утеряны.", - "delete-items-title": "Вы точно хотите удалить { count, plural, one {1 объект} few {# объекта} other {# объектов} }?", - "delete-items-action-title": "Удалить { count, plural, one {1 объект} few {# объекта} other {# объектов}", - "delete-items-text": "Внимание, после подтверждения выбранные объекты и все связанные с ними данные будут безвозвратно утеряны.", - "add-item-text": "Добавить новый объект", - "no-items-text": "Объекты не найдены", - "item-details": "Подробности об объекте", - "delete-item": "Удалить объект", - "delete-items": "Удалить объекты", - "scroll-to-top": "Прокрутка к началу" - }, - "help": { - "goto-help-page": "Перейти к справке" - }, - "home": { - "home": "Главная", - "profile": "Профиль", - "logout": "Выйти из системы", - "menu": "Меню", - "avatar": "Аватар", - "open-user-menu": "Открыть меню пользователя" - }, - "import": { - "no-file": "Файл не выбран", - "drop-file": "Перетащите JSON файл или кликните для выбора файла.", - "drop-file-csv": "Перетащите CSV файл или кликните для выбора файла.", - "column-value": "Значение", - "column-title": "Название", - "column-example": "Пример значений данных", - "column-key": "Ключ атрибута/телеметрии", - "csv-delimiter": "Разделитель в CSV файле", - "csv-first-line-header": "Первая строка содержит названия колонок", - "csv-update-data": "Обновить атрибут/телеметрию", - "import-csv-number-columns-error": "Файл должен содержать как минимум две колонки", - "import-csv-invalid-format-error": "Неверный формат данных. Строка: '{{line}}'", - "column-type": { - "name": "Название", - "type": "Тип", - "label": "Метка", - "column-type": "Тип колонки", - "client-attribute": "Клиентский атрибут", - "shared-attribute": "Общий атрибут", - "server-attribute": "Серверный атрибут", - "timeseries": "Телеметрия", - "entity-field": "Entity field", - "access-token": "Токен" - }, - "stepper-text": { - "select-file": "Выберите файл", - "configuration": "Конфигурация импорта", - "column-type": "Выберите тип колонок", - "creat-entities": "Создание новых объектов", - "done": "Завершено" - }, - "message": { - "create-entities": "{{count}} новый(х) объект(ов) было успешно создано.", - "update-entities": "{{count}} объект(ов) успешно обновлено.", - "error-entities": "Возникла ошибка при создании {{count}} объекта(ов)." - } - }, - "item": { - "selected": "Выбранные" - }, - "js-func": { - "no-return-error": "Функция должна возвращать значение!", - "return-type-mismatch": "Функция должна возвращать значение типа '{{type}}'!" - }, - "key-val": { - "key": "Ключ", - "value": "Значение", - "remove-entry": "Удалить элемент", - "add-entry": "Добавить элемент", - "no-data": "Элементы отсутствуют" - }, - "layout": { - "layout": "Макет", - "manage": "Управление макетами", - "settings": "Настройки макета", - "color": "Цвет", - "main": "Основной", - "right": "Правый", - "select": "Выбрать макет" - }, - "legend": { - "direction": "Расположение элементов легенды", - "position": "Расположение легенды", - "show-max": "Показать максимальное значение", - "show-min": "Показать минимальное значение", - "show-avg": "Показать среднее значение", - "show-total": "Показать сумму", - "settings": "Настройки легенды", - "min": "Мин", - "max": "Макс", - "avg": "Среднее", - "total": "Сумма", - "comparison-time-ago": { - "days": "(день назад)", - "weeks": "(неделю назад)", - "months": "(месяц назад)", - "years": "(год назад)" - } - }, - "login": { - "login": "Войти", - "request-password-reset": "Запрос на сброс пароля", - "reset-password": "Сбросить пароль", - "create-password": "Создать пароль", - "passwords-mismatch-error": "Введенные пароли должны быть одинаковыми!", - "password-again": "Введите пароль еще раз", - "sign-in": "Пожалуйста, войдите в систему", - "username": "Имя пользователя (эл. адрес)", - "remember-me": "Запомнить меня", - "forgot-password": "Забыли пароль?", - "password-reset": "Пароль сброшен", - "expired-password-reset-message": "Срок действия Вашего пароля закончился! Пожалуйста, создайте новый пароль.", - "new-password": "Новый пароль", - "new-password-again": "Повторите новый пароль", - "password-link-sent-message": "Ссылка для сброса пароля была успешно отправлена!", - "email": "Эл. адрес", - "login-with": "Войти через {{name}}", - "or": "или" - }, - "position": { - "top": "Верх", - "bottom": "Низ", - "left": "Левый край", - "right": "Правый край" - }, - "profile": { - "profile": "Профиль", - "last-login-time": "Время последнего входа в систему", - "change-password": "Изменить пароль", - "current-password": "Текущий пароль" - }, - "relation": { - "relations": "Отношения", - "direction": "Направления", - "search-direction": { - "FROM": "От", - "TO": "К" - }, - "direction-type": { - "FROM": "от", - "TO": "к" - }, - "from-relations": "Исходящие отношения", - "to-relations": "Входящие отношения", - "selected-relations": "Выбрано { count, plural, 1 {1 отношение} few {# отношения} other {# отношений} }", - "type": "Тип", - "to-entity-type": "К типу объекта", - "to-entity-name": "К объекта", - "from-entity-type": "От типа объекта", - "from-entity-name": "От объекта", - "to-entity": "К объекту", - "from-entity": "От объекта", - "delete": "Удалить отношение", - "relation-type": "Тип отношения", - "relation-type-required": "Тип отношения обязателен.", - "any-relation-type": "Любой тип", - "add": "Добавить отношение", - "edit": "Редактировать отношение", - "delete-to-relation-title": "Вы точно хотите удалить отношение, идущее к объекту '{{entityName}}'?", - "delete-to-relation-text": "Внимание, после подтверждения объект '{{entityName}}' будет отвязан от текущего объекта.", - "delete-to-relations-title": "Вы точно хотите удалить { count, plural, 1 {1 отношение} few {# отношения} other {# отношений} }?", - "delete-to-relations-text": "Внимание, после подтверждения выбранные объекты будут отвязаны от текущего объекта.", - "delete-from-relation-title": "Вы точно хотите удалить отношение, идущее от объекта '{{entityName}}'?", - "delete-from-relation-text": "Внимание, после подтверждения текущий объект будет отвязан от объекта '{{entityName}}'.", - "delete-from-relations-title": "Вы точно хотите удалить { count, plural, 1 {1 отношение} few {# отношения} other {# отношений} }?", - "delete-from-relations-text": "Внимание, после подтверждения выбранные объекты будут отвязаны от соответствующих объектов.", - "remove-relation-filter": "Удалить фильтр отношений", - "add-relation-filter": "Добавить фильтр отношений", - "any-relation": "Любое отношение", - "relation-filters": "Фильтры отношений", - "additional-info": "Дополнительная информация (JSON)", - "invalid-additional-info": "Не удалось распознать JSON с дополнительной информацией." - }, - "rulechain": { - "rulechain": "Цепочка правил", - "rulechains": "Цепочки правил", - "root": "Корневая", - "delete": "Удалить цепочку правил", - "name": "Названия", - "name-required": "Название необходимо.", - "description": "Описание", - "add": "Добавить цепочку правил", - "set-root": "Сделать цепочку корневой", - "set-root-rulechain-title": "Вы точно хотите сделать цепочку правил '{{ruleChainName}}' корневой?", - "set-root-rulechain-text": "После подтверждения цепочка правил станет корневой и будет обрабатывать все входящие сообщения.", - "delete-rulechain-title": "Вы точно хотите удалить цепочку правил '{{ruleChainName}}'?", - "delete-rulechain-text": "Внимание, после подтверждения цепочка правил и все связанные с ней данные будут безвозвратно удалены.", - "delete-rulechains-title": "Вы точно хотите удалить { count, plural, 1 {1 цепочку правил} few {# цепочки правил} other {# цепочек правил} }?", - "delete-rulechains-action-title": "Удалить { count, plural, 1 {1 цепочку правил} few {# цепочки правил} other {# цепочек правил} }", - "delete-rulechains-text": "Внимание, после подтверждения выбранные цепочки правил и все связанные с ними данные будут безвозвратно удалены.", - "add-rulechain-text": "Добавить новую цепочку правил", - "no-rulechains-text": "Цепочки правил не найдены", - "rulechain-details": "Подробности о цепочке правил", - "details": "Подробности", - "events": "События", - "system": "Системная", - "import": "Импортировать цепочку правил", - "export": "Экспортировать цепочку правил", - "export-failed-error": "Не удалось экспортировать цепочку правил: {{error}}", - "create-new-rulechain": "Создать новую цепочку правил", - "rulechain-file": "Файл цепочки правил", - "invalid-rulechain-file-error": "Не удалось импортировать цепочку правил: неправильный формат.", - "copyId": "Копировать ИД цепочки правил", - "idCopiedMessage": "ИД цепочки правил скопирован в буфер обмена", - "select-rulechain": "Выбрать цепочку правил", - "no-rulechains-matching": "Цепочки правил, соответствующие '{{entity}}', не найдены.", - "rulechain-required": "Цепочка правил обязательна", - "management": "Управление цепочками правил", - "debug-mode": "Режим отладки" - }, - "rulenode": { - "details": "Подробности", - "events": "События", - "search": "Поиск правил", - "open-node-library": "Открыть библиотеку правил", - "add": "Добавить правило", - "name": "Название", - "name-required": "Название обязательно.", - "type": "Тип", - "description": "Описание", - "delete": "Удалить правило", - "select-all-objects": "Выделить все правила и связи", - "deselect-all-objects": "Отменить выделение правил и связей", - "delete-selected-objects": "Удалить выделенные правила и связи", - "delete-selected": "Удалить выделенные", - "select-all": "Выделить всё", - "copy-selected": "Копировать выделенное", - "deselect-all": "Отменить выделение", - "rulenode-details": "Подробности о правиле", - "debug-mode": "Режим отладки", - "configuration": "Настройки", - "link": "Связь", - "link-details": "Подробности о связи правила", - "add-link": "Добавить связь", - "link-label": "Метка связи", - "link-label-required": "Метка связи обязателен.", - "custom-link-label": "Пользовательская метка связи", - "custom-link-label-required": "Пользовательская метка связи обязателен.", - "link-labels": "Метки связи", - "link-labels-required": "Метки связи обязательны.", - "no-link-labels-found": "Метки связи не найдены", - "no-link-label-matching": "Метка '{{label}}' не найдена.", - "create-new-link-label": "Создать новую!", - "type-filter": "Фильтр", - "type-filter-details": "Фильтр входящих сообщений с заданными условиями", - "type-enrichment": "Насыщение", - "type-enrichment-details": "Добавить данные в метадату сообщения", - "type-transformation": "Преобразование", - "type-transformation-details": "Изменить содержимое сообщение и его метадату", - "type-action": "Действие", - "type-action-details": "Выполнить заданное действие", - "type-external": "Сторонние", - "type-external-details": "Взаимодействовать со сторонними системами", - "type-rule-chain": "Цепочка правил", - "type-rule-chain-details": "Перенаправить входящее сообщение в другую цепочку правил", - "type-input": "Вход", - "type-input-details": "Логический вход цепочки правил перенаправляет входящие сообщения в следующее правило", - "type-unknown": "Неизвестный", - "type-unknown-details": "Неопределенное правило", - "directive-is-not-loaded": "Указанная директива конфигурации '{{directiveName}}' не доступна.", - "ui-resources-load-error": "Не удалось загрузить UI ресурсы.", - "invalid-target-rulechain": "Не удалось определить целевую цепочку правил!", - "test-script-function": "Протестировать скрипт", - "message": "Сообщение", - "message-type": "Тип сообщения", - "select-message-type": "Выбрать тип сообщения", - "message-type-required": "Тип сообщения обязателен", - "metadata": "Метаданные", - "metadata-required": "Метаданные объекта не могут быть пустыми.", - "output": "Выход", - "test": "Протестировать", - "help": "Помощь", - "reset-debug-mode": "Сбросить режим отладки во всех правилах" - }, - "queue": { - "select_name": "Выберите имя для Queue", - "name": "Имя для Queue", - "name_required": "Поле 'Имя для Queue' обязательно к заполнению!" - - }, - "tenant": { - "tenant": "Владелец", - "tenants": "Владельцы", - "management": "Управление владельцами", - "add": "Добавить владельца", - "admins": "Администраторы", - "manage-tenant-admins": "Управление администраторами владельца", - "delete": "Удалить владельца", - "add-tenant-text": "Добавить нового владельца", - "no-tenants-text": "Владельцы не найдены", - "tenant-details": "Подробности об владельце", - "delete-tenant-title": "Вы точно хотите удалить владельца '{{tenantTitle}}'?", - "delete-tenant-text": "Внимание, после подтверждения владелец и все связанные с ним данные будут безвозвратно утеряны.", - "delete-tenants-title": "Вы точно хотите удалить { count, plural, one {1 владельца} other {# владельцев} }?", - "delete-tenants-action-title": "Удалить { count, plural, one {1 владельца} other {# владельцев} }", - "delete-tenants-text": "Внимание, после подтверждения выбранные Владельцы и все связанные с ними данные будут безвозвратно утеряны.", - "title": "Имя", - "title-required": "Имя обязательно.", - "description": "Описание", - "details": "Подробности", - "events": "События", - "copyId": "Копировать ИД владельца", - "idCopiedMessage": "ИД владельца скопирован в буфер обмена", - "select-tenant": "Выбрать владельца", - "no-tenants-matching": "Владельцы, соответствующие '{{entity}}', не найдены.", - "tenant-required": "Владелец обязателен" - }, - "timeinterval": { - "seconds-interval": "{ seconds, plural, one {1 секунда} few {# секунды} other {# секунд} }", - "minutes-interval": "{ minutes, plural, one {1 минута} few {# минуты} other {# минут} }", - "hours-interval": "{ hours, plural, one {1 час} few {# часа} other {# часов} }", - "days-interval": "{ days, plural, one {1 день} few {# дня} other {# дней} }", - "days": "Дни", - "hours": "Часы", - "minutes": "Минуты", - "seconds": "Секунды", - "advanced": "Дополнительно" - }, - "timewindow": { - "days": "{ days, plural, one {1 день} few {# дня} other {# дней} }", - "hours": "{ hours, plural, one {1 час} few {# часа} other {# часов} }", - "minutes": "{ minutes, plural, one {1 минута} few {# минуты} other {# минут} }", - "seconds": "{ seconds, plural, one {1 секунда} few {# секунды} other {# секунд} }", - "realtime": "Режим реального времени", - "history": "История", - "last-prefix": "Последние", - "period": "с {{ startTime }} до {{ endTime }}", - "edit": "Изменить временное окно", - "date-range": "Диапазон дат", - "last": "Последние", - "time-period": "Период времени", - "hide": "Скрыть" - }, - "user": { - "user": "Пользователь", - "users": "Пользователи", - "customer-users": "Пользователи клиента", - "tenant-admins": "Администраторы владельца", - "sys-admin": "Системный администратор", - "tenant-admin": "Администратор владельца", - "customer": "Клиент", - "anonymous": "Аноним", - "add": "Добавить пользователя", - "delete": "Удалить пользователя", - "add-user-text": "Добавить нового пользователя", - "no-users-text": "Пользователи не найдены", - "user-details": "Подробности о пользователе", - "delete-user-title": "Вы точно хотите удалить пользователя '{{userEmail}}'?", - "delete-user-text": "Внимание, после подтверждения пользователь и все связанные с ним данные будут безвозвратно утеряны.", - "delete-users-title": "Вы точно хотите удалить { count, plural, one {1 пользователя} other {# пользователей} }?", - "delete-users-action-title": "Удалить { count, plural, one {1 пользователя} other {# пользователей} }", - "delete-users-text": "Внимание, после подтверждения выбранные пользователи и все связанные с ними данные будут безвозвратно утеряны.", - "activation-email-sent-message": "Активационное письмо успешно отправлено!", - "resend-activation": "Повторить отправку активационного письма", - "email": "Эл. адрес", - "email-required": "Эл. адрес обязателен.", - "invalid-email-format": "Не правильный формат письма.", - "first-name": "Имя", - "last-name": "Фамилия", - "description": "Описание", - "default-dashboard": "Дашборд по умолчанию", - "always-fullscreen": "Всегда в полноэкранном режиме", - "select-user": "Выбрать пользователя", - "no-users-matching": "Пользователи, соответствующие '{{entity}}', не найдены.", - "user-required": "Пользователь обязателен", - "activation-method": "Метод активации", - "display-activation-link": "Отобразить ссылку для активации", - "send-activation-mail": "Отправить активационное письмо", - "activation-link": "Активационная ссылка для пользователя", - "activation-link-text": "Для активации пользователя используйте ссылку :", - "copy-activation-link": "Копировать активационную ссылку", - "activation-link-copied-message": "Ссылка для активации пользователя скопировано в буфер обмена", - "details": "Подробности", - "login-as-tenant-admin": "Войти как администратор владельца", - "login-as-customer-user": "Войти как пользователь клиента", - "disable-account": "Отключить учетную запись пользователя", - "enable-account": "Включить учетную запись пользователя", - "enable-account-message": "Учетная запись пользователя была успешно включена!", - "disable-account-message": "Учетная запись пользователя была успешно отключена!" - }, - "value": { - "type": "Тип значения", - "string": "Строка", - "string-value": "Строковое значение", - "integer": "Целое число", - "integer-value": "Целочисленное значение", - "invalid-integer-value": "Неправильный формат целого числа", - "double": "Число двойной точности", - "double-value": "Значение двойной точности", - "boolean": "Логический тип", - "boolean-value": "Логическое значение", - "false": "Ложь", - "true": "Правда", - "long": "Целое число" - }, - "widget": { - "widget-library": "Галерея виджетов", - "widget-bundle": "Набор виджетов", - "select-widgets-bundle": "Выберите набор виджетов", - "management": "Управление виджетами", - "editor": "Редактор виджетов", - "widget-type-not-found": "Ошибка при загрузке конфигурации виджета.
    Возможно, связанный с ней\n тип виджета уже удален.", - "widget-type-load-error": "Не удалось загрузить виджет по следующим причинам:", - "remove": "Удалить виджет", - "edit": "Редактировать виджет", - "remove-widget-title": "Вы точно хотите удалить виджет '{{widgetTitle}}'?", - "remove-widget-text": "Внимание, после подтверждения виджет и все связанные с ним данные будут безвозвратно утеряны.", - "timeseries": "Телеметрия", - "search-data": "Поиск данных", - "no-data-found": "Данные не найдено", - "latest-values": "Последние значения", - "rpc": "Управляющий виджет", - "alarm": "Виджет оповещений", - "static": "Статический виджет", - "select-widget-type": "Выберите тип виджета", - "missing-widget-title-error": "Укажите название виджета!", - "widget-saved": "Виджет сохранен", - "unable-to-save-widget-error": "Не удалось сохранить виджет! Виджет содержит ошибки!", - "save": "Сохранить виджет", - "saveAs": "Сохранить виджет как", - "save-widget-type-as": "Сохранить тип виджета как", - "save-widget-type-as-text": "Пожалуйста, введите название виджета и/или укажите целевой набор виджетов", - "toggle-fullscreen": "Во весь экран", - "run": "Запустить виджет", - "title": "Название виджета", - "title-required": "Название виджета обязательно.", - "type": "Тип виджета", - "resources": "Ресурсы", - "resource-url": "JavaScript/CSS URL", - "remove-resource": "Удалить ресурс", - "add-resource": "Добавить ресурс", - "html": "HTML", - "tidy": "Форматировать", - "css": "CSS", - "settings-schema": "Схема конфигурации", - "datakey-settings-schema": "Схема конфигурации ключа данных", - "javascript": "Javascript", - "remove-widget-type-title": "Вы точно хотите удалить виджет '{{widgetName}}'?", - "remove-widget-type-text": "Внимание, после подтверждения тип виджета и все связанные с ним данные будут безвозвратно утеряны.", - "remove-widget-type": "Удалить тип виджета", - "add-widget-type": "Добавить новый тип виджета", - "widget-type-load-failed-error": "Не удалось загрузить тип виджета!", - "widget-template-load-failed-error": "Не удалось загрузить шаблон виджета!", - "add": "Добавить виджет", - "undo": "Откатить изменения в виджете", - "export": "Экспортировать виджет" - }, - "widget-action": { - "header-button": "Кнопка заголовка виджета", - "open-dashboard-state": "Перейти к новомум состоянию дашборда", - "update-dashboard-state": "Обновить текущее состояние дашборда", - "open-dashboard": "Перейти к другому дашборду", - "custom": "Пользовательское действие", - "custom-pretty": "Пользовательское действие (с HTML шаблоном)", - "target-dashboard-state": "Целевое состояние дашборда", - "target-dashboard-state-required": "Целевое состояние дашборда обязательно", - "set-entity-from-widget": "Установить объект из виджета", - "target-dashboard": "Целевой дашборд", - "open-right-layout": "Открыть мобильный режим дашборда" - }, - "widgets-bundle": { - "current": "Текущий набор", - "widgets-bundles": "Наборы виджетов", - "add": "Добавить набор виджетов", - "delete": "Удалить набор виджетов", - "title": "Название", - "title-required": "Название обязательно.", - "add-widgets-bundle-text": "Добавить новый набор виджетов", - "no-widgets-bundles-text": "Наборы виджетов не найдены", - "empty": "Пустой набор виджетов", - "details": "Подробности", - "widgets-bundle-details": "Подробности о наборе виджетов", - "delete-widgets-bundle-title": "Вы точно хотите удалить набор виджетов '{{widgetsBundleTitle}}'?", - "delete-widgets-bundle-text": "Внимание, после подтверждения набор виджетов и все связанные с ним данные будут безвозвратно утеряны.", - "delete-widgets-bundles-title": "Вы точно хотите удалить { count, plural, one {1 набор виджетов} few {# набора виджетов} other {# наборов виджетов} }?", - "delete-widgets-bundles-action-title": "Удалить { count, plural, one {1 набор виджетов} few {# набора виджетов} other {# наборов виджетов} }", - "delete-widgets-bundles-text": "Внимание, после подтверждения выбранные наборы виджетов и все связанные с ними данные будут безвозвратно утеряны..", - "no-widgets-bundles-matching": "Набор виджетов '{{widgetsBundle}}' не найден.", - "widgets-bundle-required": "Набор виджетов обязателен.", - "system": "Системный", - "import": "Импортировать набор виджетов", - "export": "Экспортировать набор виджетов", - "export-failed-error": "Не удалось экспортировать набор виджетов: {{error}}", - "create-new-widgets-bundle": "Создать новый набор виджетов", - "widgets-bundle-file": "Файл набора виджетов", - "invalid-widgets-bundle-file-error": "Не удалось импортировать набор виджетов: неизвестная схема данных набора виджетов." - }, - "widget-config": { - "data": "Данные", - "settings": "Настройки", - "advanced": "Дополнительно", - "title": "Название", - "general-settings": "Общие настройки", - "display-title": "Показать название", - "drop-shadow": "Тень", - "enable-fullscreen": "Во весь экран", - "background-color": "Цвет фона", - "text-color": "Цвет текста", - "padding": "Отступ", - "margin": "Margin", - "widget-style": "Стиль виджета", - "title-style": "Стиль названия", - "mobile-mode-settings": "Настройки мобильного режима", - "order": "Порядок", - "height": "Высота", - "units": "Специальный символ после значения", - "decimals": "Количество цифр после запятой", - "timewindow": "Временное окно", - "use-dashboard-timewindow": "Использовать временное окно дашборда", - "display-timewindow": "Показывать временное окно", - "display-legend": "Показать легенду", - "datasources": "Источники данных", - "maximum-datasources": "Максимальной количество источников данных равно {{count}}", - "datasource-type": "Тип", - "datasource-parameters": "Параметры", - "remove-datasource": "Удалить источник данных", - "add-datasource": "Добавить источник данных", - "target-device": "Целевое устройство", - "alarm-source": "Источник оповещения", - "actions": "Действия", - "action": "Действие", - "add-action": "Добавить действие", - "search-actions": "Поиск действий", - "action-source": "Источник действий", - "action-source-required": "Источник действий обязателен.", - "action-name": "Название", - "action-name-required": "Название действия обязательно.", - "action-name-not-unique": "Действие с таким именем уже существует.
    Название должно быть уникально в рамках одного источника действий.", - "action-icon": "Иконка", - "action-type": "Тип", - "action-type-required": "Тип действий обязателен.", - "edit-action": "Редактировать действие", - "delete-action": "Удалить действие", - "delete-action-title": "Удалить действие виджета", - "delete-action-text": "Вы точно хотите удалить действие виджета '{{actionName}}'?", - "display-icon": "Показывать иконку в названии", - "icon-color": "Цвет иконки", - "icon-size": "Размер иконки" - }, - "widget-type": { - "import": "Импортировать тип виджета", - "export": "Экспортировать тип виджета", - "export-failed-error": "Не удалось экспортировать тип виджета: {{error}}", - "create-new-widget-type": "Создать новый тип виджета", - "widget-type-file": "Файл типа виджета", - "invalid-widget-type-file-error": "Не удалось импортировать виджет: неизвестная схема данных типа виджета." - }, - "widgets": { - "date-range-navigator": { - "localizationMap": { - "Sun": "Вс", - "Mon": "Пн", - "Tue": "Вт", - "Wed": "Ср", - "Thu": "Чт", - "Fri": "Пт", - "Sat": "Сб", - "Jan": "Янв.", - "Feb": "Февр.", - "Mar": "Март", - "Apr": "Апр.", - "May": "Май", - "Jun": "Июнь", - "Jul": "Июль", - "Aug": "Авг.", - "Sep": "Сент.", - "Oct": "Окт.", - "Nov": "Нояб.", - "Dec": "Дек.", - "January": "Январь", - "February": "Февраль", - "March": "Март", - "April": "Апрель", - "June": "Июнь", - "July": "Июль", - "August": "Август", - "September": "Сентябрь", - "October": "Октября", - "November": "Ноябрь", - "December": "Декабрь", - "Custom Date Range": "Пользовательский диапазон дат", - "Date Range Template": "Шаблон диапазона дат", - "Today": "Сегодня", - "Yesterday": "Вчера", - "This Week": "На этой неделе", - "Last Week": "Прошлая неделя", - "This Month": "Этот месяц", - "Last Month": "Прошлый месяц", - "Year": "Год", - "This Year": "В этом году", - "Last Year": "Прошлый год", - "Date picker": "Выбор даты", - "Hour": "Час", - "Day": "День", - "Week": "Неделю", - "2 weeks": "2 Недели", - "Month": "Месяц", - "3 months": "3 Месяца", - "6 months": "6 Месяцев", - "Custom interval": "Пользовательский интервал", - "Interval": "Интервал", - "Step size": "Размер шага", - "Ok": "Ok" - } - }, - "input-widgets": { - "attribute-not-allowed": "Атрибут не может быть выбран в этом виджете", - "date": "Дата", - "blocked-location": "Геолокация заблокирована в вашем браузере", - "claim-device": "Подтвердить устройство", - "claim-failed": "Не удалось подтвердить устройство!", - "claim-not-found": "Устройство не найдено!", - "claim-successful": "Устройство успешно подтверждено!", - "discard-changes": "Отменить изменения", - "device-name": "Название устройства", - "device-name-required": "Необходимо указать название устройства", - "entity-attribute-required": "Значение атрибута обязателено", - "entity-coordinate-required": "Необходимо указать широту и долготу", - "entity-timeseries-required": "Значение телеметрии обязательно", - "get-location": "Получить текущее местоположение", - "latitude": "Широта", - "longitude": "Долгота", - "not-allowed-entity": "Выбраный объект не имеет общих атрибутов", - "no-attribute-selected": "Атрибут не выбран", - "no-datakey-selected": "Ни один datakey не выбран", - "no-entity-selected": "Объект не выбран", - "no-coordinate-specified": "Ключ для широты/долготы не указан", - "no-support-geolocation": "Ваш браузер не поддерживает геолокацию", - "no-image": "Нет изображения", - "no-support-web-camera": "Нет поддерживаемой веб-камеры", - "no-timeseries-selected": "Параметр телеметрии не выбран", - "secret-key": "Секретный ключ", - "secret-key-required": "Необходимо указать секретный ключ", - "switch-attribute-value": "Изменить значение атрибута", - "switch-camera": "Изменить камеру", - "switch-timeseries-value": "Изменить значение телеметрии", - "take-photo": "Сделать фото", - "time": "Время", - "timeseries-not-allowed": "Телеметрия не может быть выбрана в этом виджете", - "update-failed": "Не удалось обновить", - "update-successful": "Успешно обновлено", - "update-attribute": "Обновить атрибут", - "update-timeseries": "Обновить телеметрию", - "value": "Значение" - } - }, - "icon": { - "icon": "Иконка", - "select-icon": "Выбрать иконку", - "material-icons": "Иконки в стиле Material", - "show-all": "Показать все иконки" - }, - "custom": { - "widget-action": { - "action-cell-button": "Кнопка действия в ячейке таблицы", - "row-click": "Действий при щелчке на строку", - "marker-click": "Действия при щелчке на маркер", - "polygon-click": "Действия при щелчке на полигон", - "tooltip-tag-action": "Действие при нажатии на ссылку в подсказке", - "node-selected": "Действий при выборе ноды", - "element-click": "Действий при щелчке на HTML элементе", - "pie-slice-click": "Действий при щелчке на секции круговой диаграммы", - "row-double-click": "Действий при двойном щелчке на строку" - } - }, - "language": { - "language": "Язык" - } -} diff --git a/ui/src/app/locale/locale.constant-tr_TR.json b/ui/src/app/locale/locale.constant-tr_TR.json deleted file mode 100644 index 3f4441f79e..0000000000 --- a/ui/src/app/locale/locale.constant-tr_TR.json +++ /dev/null @@ -1,1598 +0,0 @@ -{ - "access": { - "unauthorized": "Yetkisiz", - "unauthorized-access": "Yetkisiz Erişim", - "unauthorized-access-text": "Bu kaynağa erişmek için giriş yapmalısınız!", - "access-forbidden": "Erişim Yasaklandı", - "access-forbidden-text": "Bu konuma erişim haklarınız yok!
    Bu yere hala erişmek istiyorsanız farklı kullanıcılarla oturum açmayı deneyin.", - "refresh-token-expired": "Oturum süresi doldu", - "refresh-token-failed": "Oturum yenilenemiyor" - }, - "action": { - "activate": "Etkinleştir", - "suspend": "Askıya al", - "save": "Kaydet", - "saveAs": "Farklı Kaydet", - "cancel": "İptal", - "ok": "Tamam", - "delete": "Sil", - "add": "Ekle", - "yes": "Evet", - "no": "Hayır", - "update": "Güncelle", - "remove": "Kaldır", - "search": "Ara", - "clear-search": "Aramayı Temizle", - "assign": "Ata", - "unassign": "Atamayı kaldır", - "share": "Paylaş", - "make-private": "Özel yap", - "apply": "Uygula", - "apply-changes": "Değişiklikleri Uygula", - "edit-mode": "Düzenleme Modu", - "enter-edit-mode": "Düzenleme moduna gir", - "decline-changes": "Değişiklikleri reddet", - "close": "Kapat", - "back": "Geri", - "run": "Çalıştır", - "sign-in": "Giriş yap!", - "edit": "Düzenle", - "view": "Görüntüle", - "create": "Oluştur", - "drag": "Sürükle", - "refresh": "Yenile", - "undo": "Geri al", - "copy": "Kopyala", - "paste": "Yapıştır", - "copy-reference": "Referansı kopyala", - "paste-reference": "Referansı yapıştır", - "import": "İçe aktar", - "export": "Dışa aktar", - "share-via": "{{provider}} ile paylaş" - }, - "aggregation": { - "aggregation": "Toplama", - "function": "Veri toplama işlevi", - "limit": "Maksimum değerler", - "group-interval": "Gruplama aralığı", - "min": "Min", - "max": "Maks", - "avg": "Ortalama", - "sum": "Toplam", - "count": "Sayı", - "none": "Yok" - }, - "admin": { - "general": "Genel", - "general-settings": "Genel Ayarlar", - "outgoing-mail": "Giden Posta", - "outgoing-mail-settings": "Giden Posta Ayarları", - "system-settings": "Sistem Ayarları", - "test-mail-sent": "Test e-postası başarıyla gönderildi!", - "base-url": "Temel URL", - "base-url-required": "Temel URL gerekli.", - "mail-from": "Gönderen Kişi", - "mail-from-required": "Gönderen Kişi gerekli.", - "smtp-protocol": "SMTP protokolü", - "smtp-host": "SMTP sunucusu", - "smtp-host-required": "SMTP sunucusu gerekli.", - "smtp-port": "SMTP portu", - "smtp-port-required": "Bir SMTP portu sağlamalısınız.", - "smtp-port-invalid": "Bu geçerli bir smtp portu gibi görünmüyor.", - "timeout-msec": "Zaman aşımı (milisaniye)", - "timeout-required": "Zaman aşımı değeri gerekli.", - "timeout-invalid": "Bu geçerli bir zaman aşımı gibi görünmüyor.", - "enable-tls": "TLS'i etkinleştir.", - "tls-version" : "TLS sürümü", - "send-test-mail": "Test e-postası gönder" - }, - "alarm": { - "alarm": "Alarm", - "alarms": "Alarmlar", - "select-alarm": "Alarm seç", - "no-alarms-matching": "'{{entity}}' ile eşleşen alarm bulunamadı.", - "alarm-required": "Alarm gerekli", - "alarm-status": "Alarm durumu", - "search-status": { - "ANY": "Herhangi biri", - "ACTIVE": "Aktif", - "CLEARED": "Temizlendi", - "ACK": "Onaylandı", - "UNACK": "Onaylanmadı" - }, - "display-status": { - "ACTIVE_UNACK": "Aktif Onaylanmadı", - "ACTIVE_ACK": "Aktif Onaylandı", - "CLEARED_UNACK": "Temizlendi Onaylanmadı", - "CLEARED_ACK": "Temizlendi Onaylandı" - }, - "no-alarms-prompt": "Alarm bulunamadı", - "created-time": "Oluşma zamanı", - "type": "Tip", - "severity": "Şiddet", - "originator": "Kaynak", - "originator-type": "Kaynak tipi", - "details": "Detaylar", - "status": "Durum", - "alarm-details": "Alarm detayları", - "start-time": "Başlangıç zamanı", - "end-time": "Bitiş zamanı", - "ack-time": "Onaylanma zamanı", - "clear-time": "Temizlenme zamanı", - "severity-critical": "Kritik", - "severity-major": "Birincil", - "severity-minor": "İkincil", - "severity-warning": "Uyarı", - "severity-indeterminate": "Belirsiz", - "acknowledge": "Onayla", - "clear": "Temizle", - "search": "Alarm ara", - "selected-alarms": "{ count, plural, 1 {1 alarm} other {# alarm} } seçildi", - "no-data": "Görüntülenecek veri bulunmuyor", - "polling-interval": "Alarm yoklama aralığı (saniye)", - "polling-interval-required": "Alarm yoklama aralığı gerekli.", - "min-polling-interval-message": "Alarm yoklama aralığı en az 1 saniye olmalıdır.", - "aknowledge-alarms-title": "{ count, plural, 1 {1 alarmı} other {# alarmı} } onayla", - "aknowledge-alarms-text": "{ count, plural, 1 {1 alarmı} other {# alarmı} } onaylamak istediğinize emin misiniz?", - "clear-alarms-title": "{ count, plural, 1 {1 alarmı} other {# alarmı} } temizle", - "clear-alarms-text": "{ count, plural, 1 {1 alarmı} other {# alarmı} } temizlemek istediğinize emin misiniz?" - }, - "alias": { - "add": "Kısa ad ekle", - "edit": "Kısa ad düzenle", - "name": "Kısa ad", - "name-required": "Kısa ad gerekli", - "duplicate-alias": "Aynı kısa ad daha önce kullanılmış.", - "filter-type-single-entity": "Tek öğe", - "filter-type-entity-list": "Öğe listesi", - "filter-type-entity-name": "Öğe adı", - "filter-type-state-entity": "Kontrol panelinden öğe", - "filter-type-state-entity-description": "Kontrol tablosu durum parametrelerinden alınan öğeler", - "filter-type-asset-type": "Varlık türü", - "filter-type-asset-type-description": "'{{assetType}}' türünde varlıklar", - "filter-type-asset-type-and-name-description": "Adı '{{prefix}}' ile başlayan '{{assetType}}' türünde varlıklar", - "filter-type-device-type": "Aygıt türü", - "filter-type-device-type-description": "'{{deviceType}}' türünde aygıtlar", - "filter-type-device-type-and-name-description": "Adı '{{prefix}}' ile başlayan'{{deviceType}}' türünde aygıtlar", - "filter-type-relations-query": "İlişkiler sorgusu", - "filter-type-relations-query-description": "{{relationType}} türünde ilişkili olan varlıklar: {{entities}}. {{direction}}: {{rootEntity}}", - "filter-type-asset-search-query": "Varlık arama sorgusu", - "filter-type-asset-search-query-description": "{{relationType}} türünde ilişkisi olan varlıklar {{assetTypes}}. {{direction}}: {{rootEntity}}", - "filter-type-device-search-query": "Aygıt arama sorgusu", - "filter-type-device-search-query-description": "{{relationType}} türünde ilişkisi olan aygıt tipleri {{deviceTypes}}. {{direction}}: {{rootEntity}}", - "entity-filter": "Öğe filtresi", - "resolve-multiple": "Çoklu öğe olarak çözümle", - "filter-type": "Filtre tipi", - "filter-type-required": "Filtre tipi gerekli.", - "entity-filter-no-entity-matched": "Belirlenen filtre ile eşleşen bir öğe bulunamadı.", - "no-entity-filter-specified": "Hiçbir öğe filtresi belirtilmedi", - "root-state-entity": "Kontrol panelini kök olarak kullan", - "root-entity": "Kök öğe", - "state-entity-parameter-name": "Durum varlığı parametre adı", - "default-state-entity": "Varsayılan durum öğesi", - "default-entity-parameter-name": "Varsayılan", - "max-relation-level": "Maksimum ilişki düzeyi", - "unlimited-level": "Sınırsız seviye", - "state-entity": "Kontrol paneli öğesi", - "all-entities": "Tüm öğeler", - "any-relation": "Herhangi biri" - }, - "asset": { - "asset": "Varlık", - "assets": "Varlıklar", - "management": "Varlık Yönetimi", - "view-assets": "Varlıkları Görüntüle", - "add": "Varlık ekle", - "assign-to-customer": "Kullanıcı grubuna ata", - "assign-asset-to-customer": "Varlıkları Kullanıcı Grubuna Ata", - "assign-asset-to-customer-text": "Lütfen kullanıcı grubuna atanacak varlıkları seçin", - "no-assets-text": "Varlık bulunamadı", - "assign-to-customer-text": "Lütfen varlıkları atamak için kullanıcı grubu seçin", - "public": "Açık", - "assignedToCustomer": "Kullanıcı grubuna atandı", - "make-public": "Varlığı açık hale getir", - "make-private": "Varlığı özel hale getir", - "unassign-from-customer": "Kullanıcı grubundan atamayı kaldır", - "delete": "Varlığı sil", - "asset-public": "Varlık açık halde", - "asset-type": "Varlık türü", - "asset-type-required": "Varlık türü gerekli.", - "select-asset-type": "Varlık türü seçin", - "enter-asset-type": "Varlık türü girin", - "any-asset": "Herhangi bir varlık", - "no-asset-types-matching": "'{{entitySubtype}}' ile eşleşen varlık bulunamadı.", - "asset-type-list-empty": "Herhangi bir varlık türü bulunamadı.", - "asset-types": "Varlık türleri", - "name": "İsim", - "name-required": "İsim gerekli.", - "description": "Açıklama", - "type": "Tür", - "type-required": "Tür gerekli.", - "details": "Detaylar", - "events": "Olaylar", - "add-asset-text": "Yeni varlık ekle", - "asset-details": "Varlık detayları", - "assign-assets": "Varlıkları ata", - "assign-assets-text": "{ count, plural, 1 {1 varlığı} other {# varlığı} } kullanıcı grubuna ata", - "delete-assets": "Varlıkları sil", - "unassign-assets": "Varlıkların atamalarını kaldır", - "unassign-assets-action-title": "{ count, plural, 1 {1 varlığın} other {# varlığın} } atamalarını kullanıcı grubundan kaldır", - "assign-new-asset": "Yeni varlık ata", - "delete-asset-title": "'{{assetName}}' isimli varlığı silmek istediğinize emin misiniz?", - "delete-asset-text": "UYARI: Onaylandıktan sonra varlık ve ilgili tüm veriler geri yüklenemeyecek şekilde silinecek.", - "delete-assets-title": "{ count, plural, 1 {1 varlığı} other {# varlığı} } silmek istediğinize emin misiniz?", - "delete-assets-action-title": "{ count, plural, 1 {1 varlığı} other {# varlığı} } sil", - "delete-assets-text": "UYARI: Onaylandıktan sonra tüm seçili varlıklar ver ilgili tüm veriler geri yüklenemyeck şekilde silinecek.", - "make-public-asset-title": "'{{assetName}}' isimli varlığı açık hale getirmek istediğinize emin misiniz?", - "make-public-asset-text": "Onaylandıktan sonra varlık ve ilgili veriler açık hale gelecek ve başkaları tarafından erişilebilir olacaktır.", - "make-private-asset-title": "'{{assetName}}' isimli varlığı özel hale getirmek istediğinize emin misiniz?", - "make-private-asset-text": "Onaylandıktan sonra varlık ve ilgili veriler özel hale gelecek ve başkaları tarafından erişilemez olacaktır.", - "unassign-asset-title": "'{{assetName}}' isimli varlığın atamasını kaldırmak istediğinize emin misiniz?", - "unassign-asset-text": "Onaylandıktan sonra varlığın ataması kaldırılacak ve kullanıcı grubu tarafından erişilemez olacaktır.", - "unassign-asset": "Varlık atamasını kaldır", - "unassign-assets-title": " { count, plural, 1 {1 varlık} other {# varlık} } atamasını kaldırmak istediğinize emin misiniz?", - "unassign-assets-text": "Onaylandıktan sonra tüm seçili varlıkların ataması kaldırılacak ve kullanıcı grubu tarafından erişilemez olacaktır.", - "copyId": "Varlık kimliğini kopyala", - "idCopiedMessage": "Varlık kimliği panoya kopyalandı", - "select-asset": "Varlık seç", - "no-assets-matching": "'{{entity}}' isimli varlık bulunamadı.", - "asset-required": "Varlık gerekli", - "name-starts-with": "... ile başlayan varlık adı", - "label": "Etiket" - }, - "attribute": { - "attributes": "Öznitelikler", - "latest-telemetry": "Son telemetri", - "attributes-scope": "Varlık öznitelik kapsamı", - "scope-latest-telemetry": "Son telemetri", - "scope-client": "İstemci öznitelikler", - "scope-server": "Sunucu öznitelikler", - "scope-shared": "Paylaşılan öznitelikler", - "add": "Öznitelik ekle", - "key": "Anahtar", - "last-update-time": "Son güncelleme zamanı", - "key-required": "Öznitelik anahtarı gerekli.", - "value": "Değer", - "value-required": "Öznitelik değeri gerekli.", - "delete-attributes-title": "Silmek istediğinize emin misiniz { count, plural, 1 {1 öznitelik} other {# öznitelik} }?", - "delete-attributes-text": "UYARI: Onaylandıktan sonra tüm seçili öznitelikler kaldırılacak.", - "delete-attributes": "Öznitelikleri sil", - "enter-attribute-value": "Öznitelik değeri gir", - "show-on-widget": "Göstergede göster", - "widget-mode": "Gösterge modu", - "next-widget": "Sonraki gösterge", - "prev-widget": "Önceki gösterge", - "add-to-dashboard": "Kontrol paneline ekle", - "add-widget-to-dashboard": "Göstergeyi kontrol paneline ekle", - "selected-attributes": "{ count, plural, 1 {1 öznitelik} other {# öznitelik} } seçildi", - "selected-telemetry": "{ count, plural, 1 {1 telemetri birimi} other {# telemetri birimi} } seçildi" - }, - "audit-log": { - "audit": "Log ve Hata Yönetimi", - "audit-logs": "Loglar ve Hatalar", - "timestamp": "Zaman", - "entity-type": "Kaynak", - "entity-name": "İsim", - "user": "Kullanıcı", - "type": "Tür", - "status": "Durum", - "details": "Detaylar", - "type-added": "Eklendi", - "type-deleted": "Silindi", - "type-updated": "Güncellendi", - "type-attributes-updated": "Özellikler güncellendi", - "type-attributes-deleted": "Özellikler silindi", - "type-rpc-call": "Uzaktan işlem çağrısı", - "type-credentials-updated": "Kimlik bilgileri güncellendi", - "type-assigned-to-customer": "Kullanıcı grubuna atandı", - "type-unassigned-from-customer": "Kullanıcı grubundan atama kaldırıldı", - "type-activated": "Etkinleştirildi", - "type-suspended": "Askıya alındı", - "type-credentials-read": "Kimlik bilgileri okundu", - "type-attributes-read": "Özellikler okundu", - "type-relation-add-or-update": "İlişki güncellendi", - "type-relation-delete": "İlişki silindi", - "type-relations-delete": "Tüm ilişki silindi", - "type-alarm-ack": "Kabul edilen", - "type-alarm-clear": "Temizlendi", - "status-success": "Başarılı", - "status-failure": "Başarısız", - "audit-log-details": "Log ve hata detayları", - "no-audit-logs-prompt": "Log ve hata bulunamadı", - "action-data": "Eylem verisi", - "failure-details": "Başarısız işlem detayları", - "search": "Hata ve Log Geçmişinde Ara", - "clear-search": "Aramayı temizle" - }, - "confirm-on-exit": { - "message": "Kaydedilmemiş değişiklikler var. Sayfadan ayrılmak istediğinize emin misiniz?", - "html-message": "Kaydedilmemiş değişiklikler var.
    Sayfadan ayrılmak istediğinize emin misiniz?", - "title": "Kaydedilmemiş Değişiklikler" - }, - "contact": { - "country": "Ülke", - "city": "Şehir", - "state": "Eyalet / İl", - "postal-code": "Posta Kodu", - "postal-code-invalid": "Geçersiz Posta Kodu.", - "address": "Addres", - "address2": "Addres 2", - "phone": "Telefon", - "email": "E-posta", - "no-address": "Adres yok" - }, - "common": { - "username": "Kullanıcı adı", - "password": "Parola", - "enter-username": "Kullanıcı adı gir", - "enter-password": "Parola gir", - "enter-search": "Arama gir" - }, - "content-type": { - "json": "Json", - "text": "Metin", - "binary": "İkili (Base64)" - }, - "customer": { - "customer": "Kullanıcı Grubu", - "customers": "Kullanıcı Grupları", - "management": "Kullanıcı Grubu Yönetimi", - "dashboard": "Kullanıcı Grubu Kontrol Paneli", - "dashboards": "Kullanıcı Grubu Kontrol Panellleri", - "devices": "Kullanıcı Grubu Aygıtları", - "entity-views": "Müşteri Varlığı Görüntüleme Sayısı", - "assets": "Kullanıcı Grubu Varlıkları", - "public-dashboards": "Açık Kontrol Panelleri", - "public-devices": "Açık Aygıtlar", - "public-assets": "Açık Varlıklar", - "public-entity-views": "Kamu Varlık Görüntüleme Sayısı", - "add": "Kullanıcı grubu ekle", - "delete": "Kullanıcı grubunu sil", - "manage-customer-users": "Kullanıcı grubu kullanıcılarını yönet", - "manage-customer-devices": "Kullanıcı grubu aygıtlarını yönet", - "manage-customer-dashboards": "Kullanıcı grubu kontrol panellerini yönet", - "manage-public-devices": "Açık aygıtları yönet", - "manage-public-dashboards": "Açık kontrol panellerini yönet", - "manage-customer-assets": "Kullanıcı Grubu varlıklarını yönet", - "manage-public-assets": "Açık varlıkları yönet", - "add-customer-text": "Yeni Kullanıcı Grubu ekle", - "no-customers-text": "Kullanıcı Grubu bulunamadı", - "customer-details": "Kullanıcı Grubu detayları", - "delete-customer-title": "'{{customerTitle}}' isimli kullanıcı grubunu silmek istediğinize emin misiniz?", - "delete-customer-text": "UYARI: Onaylandıktan sonra kullanıcı grubu ve tüm ilişkili veriler geri yüklenemeyecek şekilde silinecek.", - "delete-customers-title": "{ count, plural, 1 {1 kullanıcı grubunu} other {# kullanıcı grubunu} } silmek istediğinize emin misiniz?", - "delete-customers-action-title": "{ count, plural, 1 {1 kullanıcı grubunu} other {# kullanıcı grubunu} } sil", - "delete-customers-text": "UYARI: Onaylandıktan sonra tüm seçili kullanıcı grupları ve ilişkili veriler geri yüklenemez şekilde silinecek.", - "manage-users": "Kullanıcıları yönet", - "manage-assets": "Varlıkları yönet", - "manage-devices": "Aygıtları yönet", - "manage-dashboards": "Kontrol panellerini yönet", - "title": "Başlık", - "title-required": "Başlık gerekli.", - "description": "Açıklama", - "details": "Detaylar", - "events": "Olaylar", - "copyId": "Kullanıcı kimliğini kopyala", - "idCopiedMessage": "Kullanıcı kimliği panoya kopyalandı", - "select-customer": "Kullanıcı grubunu seç", - "no-customers-matching": "'{{entity}}' ile eşleşen kullanıcı grubu bulunamadı.", - "customer-required": "Kullanıcı grubu gerekli", - "select-default-customer": "Varsayılan müşteriyi seç", - "default-customer": "Varsayılan müşteri", - "default-customer-required": "Kiracı düzeyinde gösterge tablosunda hata ayıklamak için varsayılan müşteri gerekiyor" - }, - "datetime": { - "date-from": "Tarihinden", - "time-from": "Saatinden", - "date-to": "Tarihine", - "time-to": "Saatine" - }, - "dashboard": { - "dashboard": "Kontrol Paneli", - "dashboards": "Kontrol Panelleri", - "management": "Kontrol Paneli Yönetimi", - "view-dashboards": "Kontrol Panellerini Görüntüle", - "add": "Kontrol Paneli Ekle", - "assign-dashboard-to-customer": "Kullanıcı Grubuna Kontrol Panel(ler)i Ata", - "assign-dashboard-to-customer-text": "Lütfen kullanıcı grubuna atanacak kontrol panellerini seçin", - "assign-to-customer-text": "Lütfen kontrol panel(ler)ini atayacak kullanıcı grubu seçin", - "assign-to-customer": "Kullanıcı grubuna ata", - "unassign-from-customer": "Kullanıcı grubundan atamayı kaldır", - "make-public": "Kontrol panelini açık hale getir", - "make-private": "Kontrol panelini özel hale getir", - "manage-assigned-customers": "Atanan müşterileri yönet", - "assigned-customers": "Atanan müşteriler", - "assign-to-customers": "Gösterge Tablosunu / Müşterilerini Müşterilere Atama", - "assign-to-customers-text": "Lütfen gösterge panosunu atamak için müşterileri seçin", - "unassign-from-customers": "Müşterilerden Gösterge Tablosunu (Notlarını) Atama", - "unassign-from-customers-text": "Lütfen gösterge tablosundan atamak için müşterileri seçin", - "no-dashboards-text": "Kontrol paneli bulunamadı", - "no-widgets": "Hiçbir gösterge yapılandırılmadı", - "add-widget": "Yeni gösterge ekle", - "title": "Başlık", - "select-widget-title": "Gösterge seç", - "select-widget-subtitle": "Kullanılabilir gösterge türleri listesi", - "delete": "Kontrol paneli sil", - "title-required": "Başlık gerekli.", - "description": "Açıklama", - "details": "Detaylar", - "dashboard-details": "Kontrol paneli detayları", - "add-dashboard-text": "Yeni kontrol paneli ekle", - "assign-dashboards": "Kontrol panelleri ata", - "assign-new-dashboard": "Yeni kontrol paneli ata", - "assign-dashboards-text": "{ count, plural, 1 {1 kontrol panelini} other {# kontrol panelini} } kullanıcı grubuna ata", - "unassign-dashboards-action-text": "Müşterilerden atama { count, plural, 1 {1 gösterge tablosu} other {# panolar}}", - "delete-dashboards": "Kontrol panellerini sil", - "unassign-dashboards": "Kontrol panellerinden atamayı kaldır", - "unassign-dashboards-action-title": "{ count, plural, 1 {1 kontrol panelinin} other {# kontrol panelinin} } atamaları kullanıcı grubundan kaldır", - "delete-dashboard-title": "'{{dashboardTitle}}' isimli kontrol panelini silmek istediğinize emin misiniz?", - "delete-dashboard-text": "UYARI: Onaylandıktan sonra kontrol paneli ve ilişkili verileri geri yüklenemez şekilde silinecek.", - "delete-dashboards-title": "{ count, plural, 1 {1 kontrol panelini} other {# kontrol panelini} } silmek istediğinize emin misiniz?", - "delete-dashboards-action-title": "{ count, plural, 1 {1 kontrol panelini} other {# kontrol panelini} } sil", - "delete-dashboards-text": "UYARI: Onaylandıktan sonra tüm seçili kontrol panelleri ve ilişkili verileri geri yüklenemez şekilde silinecek.", - "unassign-dashboard-title": "'{{dashboardTitle}}' isimli kontrol panelindeki atamayı kaldırmak istediğinize emin misiniz?", - "unassign-dashboard-text": "Onaylandıktan sonra kontrol panelinin ataması kaldırılacak ve kullanıcı grubu tarafından erişilemez hale gelecektir.", - "unassign-dashboard": "Kontrol panelinin ataması kaldır", - "unassign-dashboards-title": "{count, plural, 1 {1 kontrol panelindeki} other {# kontrol panelindeki} } atamayı kaldırmak istediğinize emin misiniz?", - "unassign-dashboards-text": "Onaylandıktan {{dashboardTitle}}
    açık hale getirildi ve bu bağlantıdan erişilebilir durumda", - "public-dashboard-notice": "Not: Kontrol panelinden tüm verilere erişebilmek adına ilişkili aygıtları da açık hale getirmeniz gerekmektedir.", - "make-private-dashboard-title": "'{{dashboardTitle}}' isimli kontrol panelini özel hale getirmek istediğinize emin misiniz?", - "make-private-dashboard-text": "Onaylandıktan sonra kontrol paneli özel hale getirilecek ve başkaları tarafından erişilemez olacak.", - "make-private-dashboard": "Kontrol panelini özel hale getir", - "socialshare-text": "'{{dashboardTitle}}'", - "socialshare-title": "'{{dashboardTitle}}'", - "select-dashboard": "Kontrol paneli seç", - "no-dashboards-matching": "'{{entity}}' ile eşleşen kontrol paneli bulunamadı.", - "dashboard-required": "Kontrol paneli gerekli.", - "select-existing": "Var olan bir kontrol paneli seç", - "create-new": "Yeni bir kontrol paneli oluştur", - "new-dashboard-title": "Yeni kontrol paneli başlığı", - "open-dashboard": "Kontrol panelini aç", - "set-background": "Arka plan belirle", - "background-color": "Arka plan rengi", - "background-image": "Arka plan resmi", - "background-size-mode": "Arka plan boyutu modu", - "no-image": "Hiçbir resim seçilmedi", - "drop-image": "Bir resim bırakın veya yüklenecek dosyayı seçmek için tıklayın.", - "settings": "Ayarlar", - "columns-count": "Kolon sayısı", - "columns-count-required": "Kolon sayısı gerekli.", - "min-columns-count-message": "Kolon sayısı en az 10 olabilir.", - "max-columns-count-message": "Kolon sayısı en fazla 1000 olabilir.", - "widgets-margins": "Göstergeler arasındaki aralık", - "horizontal-margin": "Yatay aralık", - "horizontal-margin-required": "Yatay aralık değeri gerekli.", - "min-horizontal-margin-message": "Yatay aralık değeri en az 0 olabilir.", - "max-horizontal-margin-message": "Yatay aralık değeri en fazla 50 olabilir.", - "vertical-margin": "Dikey aralık", - "vertical-margin-required": "Dikey aralık değeri gerekli.", - "min-vertical-margin-message": "Dikey aralık değeri en az 0 olabilir.", - "max-vertical-margin-message": "Dikey aralık değeri en fazla 50 olabilir.", - "autofill-height": "Otomatik doldurma düzeni yüksekliği", - "mobile-layout": "Mobil düzen ayarları", - "mobile-row-height": "Mobil satır yüksekliği, px", - "mobile-row-height-required": "Mobil satır yüksekliği değeri gerekli.", - "min-mobile-row-height-message": "Mobil satır yükseliği değeri en az 5 px olabilir.", - "max-mobile-row-height-message": "Mobil satır yükseliği değeri en çok 200 px olabilir.", - "display-title": "Kontrol paneli başlığını göster", - "toolbar-always-open": "Araç çubuğunu her zaman açık tut", - "title-color": "Başlık rengi", - "display-dashboards-selection": "Kontrol paneli seçimlerinş göster", - "display-entities-selection": "Varlık seçimlerini göster", - "display-dashboard-timewindow": "Zaman aralığını göster", - "display-dashboard-export": "Dışa aktar seçeneğini göster", - "import": "Kontrol panelini içe aktar", - "export": "Kontrol panelini dışa aktar", - "export-failed-error": "Kontrol paneli dışa aktarılamıyor: {{error}}", - "create-new-dashboard": "Yeni kontrol paneli oluştur", - "dashboard-file": "Kontrol paneli dosyası", - "invalid-dashboard-file-error": "Kontrol paneli içe aktarılamadı: Geçersiz kontrol paneli veri yapısı.", - "dashboard-import-missing-aliases-title": "İçe aktarılan kontrol paneli tarafından kullanılan aygıt kısa adlarını yapılandırın", - "create-new-widget": "Yeni gösterge oluştur", - "import-widget": "Göstergeyi içe aktar", - "widget-file": "Gösterge dosyası", - "invalid-widget-file-error": "Gösterge içe aktarılamadı: Geçersiz gösterge veri yapısı.", - "widget-import-missing-aliases-title": "İçe aktarılan gösterge tarafından kullanılan aygıt kısa adlarını yapılandırın", - "open-toolbar": "Kontrol paneli araç çubuğunu aç", - "close-toolbar": "Araç çubuğunu kapat", - "configuration-error": "Yapılandırma hatası", - "alias-resolution-error-title": "Kontro paneli kısa adları yapılandırma hatası", - "invalid-aliases-config": "Kısa ad filtresiyle eşleşen aygıt bulunamadı.
    ", - "select-devices": "Aygıt seçin", - "assignedToCustomer": "Kullanıcı grubuna atandı", - "assignedToCustomers": "Kullanıcılara atandı", - "public": "Açık", - "public-link": "Açık bağlantı", - "copy-public-link": "Açık bağlantıyı kopyala", - "public-link-copied-message": "Kontrol paneli açık bağlantısı panoya kopyalandı", - "manage-states": "Kontrol paneli durumlarını yönet", - "states": "Kontrol paneli durumları", - "search-states": "Kontrol paneli durumu ara", - "selected-states": "{ count, plural, 1 {1 kontrol paneli durumu} other {# kontrol paneli durumu} } seçildi", - "edit-state": "Kontrol paneli durumu düzenle", - "delete-state": "Kontrol paneli durumunu sil", - "add-state": "Kontrol paneli durumu ekle", - "state": "Kontrol paneli durumu", - "state-name": "İsim", - "state-name-required": "Kontrol paneli durumu ismi gerekli.", - "state-id": "Durum Kimliği", - "state-id-required": "Kontrol paneli durum kimliği gerekli.", - "state-id-exists": "Aynı kimlikte bir kontrol paneli durumu mevcut.", - "is-root-state": "Kök durum", - "delete-state-title": "Kontrol paneli durumunu sil", - "delete-state-text": "'{{stateName}}' isimli kontrol paneli durumunu silmek istediğinize emin misiniz?", - "show-details": "Detayları göster", - "hide-details": "Detayları gizle", - "select-state": "Hedef durumu seç", - "state-controller": "Durum denetleyicisi" - }, - "datakey": { - "settings": "Ayarlar", - "advanced": "İleri düzey", - "label": "Etiket", - "color": "Renk", - "units": "Değerin yanında göstermek için özel simge", - "decimals": "Noktadan sonraki basamak sayısı", - "data-generation-func": "Veri oluşturma fonksiyonu", - "use-data-post-processing-func": "Veri işleme sonrası fonksiyonunu kullanın", - "configuration": "Veri anahtarı yapılandırması", - "timeseries": "Zaman serisi", - "attributes": "Öznitelikler", - "alarm": "Alarm alanları", - "timeseries-required": "Zaman serisi öğesi gerekli.", - "timeseries-or-attributes-required": "Zaman serisi/öznitelikler öğesi gerekli.", - "maximum-timeseries-or-attributes": "Maksimum { count, plural, 1 {1 zamanserisi/öznitelik kabul edilir.} other {# zamanserisi/öznitelik kabul edilir} }", - "alarm-fields-required": "Alarm alanları gerekli.", - "function-types": "Fonksiyon türleri", - "function-types-required": "Fonksiyon türleri gerekli.", - "maximum-function-types": "Maksimum { count, plural, 1 {1 fonksiyon türü kabul edilir.} other {# fonksiyon türü kabul edilir} }" - }, - "datasource": { - "type": "Veri kaynağı türü", - "name": "İsim", - "add-datasource-prompt": "Lütfen veri kaynağı ekleyin" - }, - "details": { - "edit-mode": "Düzenleme modu", - "toggle-edit-mode": "Düzenleme modunu aç/kapat" - }, - "device": { - "device": "Aygıt", - "device-required": "Aygıt gerekli.", - "devices": "Aygıtlar", - "management": "Aygıt Yönetimi", - "view-devices": "Aygıtları görüntüle", - "device-alias": "Aygıt kısa adı", - "aliases": "Aygıt kısa adları", - "no-alias-matching": "'{{alias}}' bulunamadı.", - "no-aliases-found": "Hiçbir kısa ad bulunamadı.", - "no-key-matching": "'{{key}}' bulunamadı.", - "no-keys-found": "Hiçbir anahtar bulunamadı.", - "create-new-alias": "Yeni bir tane oluştur!", - "create-new-key": "Yeni bir tane oluştur!", - "duplicate-alias-error": "'{{alias}}' daha önce kaydedilmiş.
    Aygıt kısa adları kontrol paneli özelinde emsalsiz olmalıdır.", - "configure-alias": "'{{alias}}' kısa adını yapılandırın", - "no-devices-matching": "'{{entity}}' ile eşleşen aygıt bulunamadı.", - "alias": "Kısa ad", - "alias-required": "Aygıt kısa adı gerekli.", - "remove-alias": "Aygıt kısa adını kaldır", - "add-alias": "Aygıt kısa adı ekle", - "name-starts-with": "... ile başlayan aygıt adı", - "device-list": "Aygıt listesi", - "use-device-name-filter": "Filtre kullan", - "device-list-empty": "Hiçbir aygıt seçilmedi.", - "device-name-filter-required": "Aygıt adı filtresi gerekli.", - "device-name-filter-no-device-matched": "'{{device}}' ile başlayan herhangi bir aygıt bulunamadı.", - "add": "Aygıt ekle", - "assign-to-customer": "Kullanıcı grubuna ata", - "assign-device-to-customer": "Aygıt(lar)ı Kullanıcı Grubuna Ata", - "assign-device-to-customer-text": "Lütfen kullanıcı grubuna atanacak aygıtları seçin", - "make-public": "Aygıtı açık hale getir", - "make-private": "Aygıtı gizli hale getir", - "no-devices-text": "Hiçbir aygıt bulunamadı", - "assign-to-customer-text": "Lütfen aygıt(lar)ı atayacak kullanıcı grubu seçin", - "device-details": "Aygıt detayları", - "add-device-text": "Yeni aygıt ekle", - "credentials": "Kimlik bilgileri", - "manage-credentials": "Kimlik bilgilerini yönet", - "delete": "Aygıt sil", - "assign-devices": "Aygıt ata", - "assign-devices-text": "{ count, plural, 1 {1 aygıtı} other {# aygıtı} } kullanıcı grubuna ata", - "delete-devices": "Aygıtları sil", - "unassign-from-customer": "Kullanıcı Grubundan atamayı kaldır", - "unassign-devices": "Aygıtlardan atamayı kaldır", - "unassign-devices-action-title": "{ count, plural, 1 {1 aygıtın} other {# aygıtın} } atamasını kullanıcı grubundan kaldır", - "assign-new-device": "Yeni aygıt ata", - "make-public-device-title": "'{{deviceName}}' isimli aygıtı açık hale getirmek istediğinizden emin misiniz?", - "make-public-device-text": "Onaylandıktan sonra aygıt ve verileri açık hale getirilecek ve diğerleri tarafından erişilebilir olacak.", - "make-private-device-title": "'{{deviceName}}' isimli aygıtı gizli hale getirmek istediğinizden emin misiniz?", - "make-private-device-text": "Onaylandıktan sonra aygıt ve verileri gizli hale getirilecek ve diğerleri tarafından erişilemez olacak.", - "view-credentials": "Kimlik bilgilerini görüntüle", - "delete-device-title": "'{{deviceName}}' isimli aygıtı silmek istediğinize emin misiniz?", - "delete-device-text": "UYARI: Onaylandıktan sonra aygıt ve ilişkili verileri geri yüklenemez şekilde silinecek.", - "delete-devices-title": "{ count, plural, 1 {1 aygıtı} other {# aygıtı} } silmek istediğinize emin misiniz?", - "delete-devices-action-title": "{ count, plural, 1 {1 aygıtı} other {# aygıtı} } sil", - "delete-devices-text": "UYARI: Onaylandıktan sonra tüm seçili aygıtlar ve ilişkili verileri geri yüklenemez şekilde silinecek.", - "unassign-device-title": "'{{deviceName}}' isimli aygıtın atamasını kaldırmak istediğinize emin misiniz?", - "unassign-device-text": "Onaylandıktan sonra aygıtın ataması kaldırılacak ve kullanıcı grubu tarafından erişilemez olacak.", - "unassign-device": "Aygıt atamasını kaldır", - "unassign-devices-title": "{ count, plural, 1 {1 aygıtın} other {# aygıtın} } atamasını kaldırmak istediğinize emin misiniz?", - "unassign-devices-text": "Onaylandıktan sonra seçili aygıtların atamaları kaldırılacak ve kullanıcı grubu tarafından erişilemez olacak.", - "device-credentials": "Aygıt Kimlik Bilgileri", - "credentials-type": "Kimlik Bilgi Türü", - "access-token": "Erişim şifresi", - "access-token-required": "Erişim şifresi gerekli.", - "access-token-invalid": "Erişim şifresi uzunluğu 1 ile 20 karakter arasında olmalıdır.", - "rsa-key": "RSA açık anahtarı", - "rsa-key-required": "RSA açık anahtarı gerekli.", - "secret": "Secret", - "secret-required": "Secret gerekli.", - "device-type": "Aygıt Türü", - "device-type-required": "Aygıt türü gereli.", - "select-device-type": "Aygıt türü seç", - "enter-device-type": "Aygıt türü gir", - "any-device": "Herhangi bir aygıt", - "no-device-types-matching": "'{{entitySubtype}}' ile eşleşen aygıt türü bulunamadı.", - "device-type-list-empty": "Hiçbir aygıt türü seçilmedi.", - "device-types": "Aygıt türleri", - "name": "İsim", - "name-required": "İsim gerekli.", - "description": "Açıklama", - "events": "Olaylar", - "details": "Detaylar", - "copyId": "Aygıt kimliğini kopyala", - "copyAccessToken": "Erişim şifresini kopyala", - "idCopiedMessage": "Aygıt kimliği panoya kopyalandı.", - "accessTokenCopiedMessage": "Aygıt erişim şifresi panoya kopyalandı", - "assignedToCustomer": "Kullanıcı Grubuna atandı", - "unable-delete-device-alias-title": "Aygıt kısa adı silinemedi", - "unable-delete-device-alias-text": "Aygıt kısa adı('{{deviceAlias}}'), şu göstergeler tarafından kullanıldığı için silinemedi:
    {{widgetsList}}", - "is-gateway": "Ağ geçidi mi?", - "public": "Açık", - "device-public": "Aygıt açık", - "select-device": "Aygıt seç" - }, - "dialog": { - "close": "Kapat" - }, - "error": { - "unable-to-connect": "Sunucuya bağlanamadı! Lütfen internet bağlantınızı kontrol edin.", - "unhandled-error-code": "İşlenmeyen hata koud: {{errorCode}}", - "unknown-error": "Bilinmeyen hata" - }, - "entity": { - "entity": "Öğe", - "entities": "Öğeler", - "aliases": "Öğe kısa adları", - "entity-alias": "Öğe kısa adı", - "unable-delete-entity-alias-title": "Öğe kısa adı silinemedi", - "unable-delete-entity-alias-text": "Öğe kısa adı('{{entityAlias}}'), şu göstergeler tarafından kullanıldığı için silinemiyor:
    {{widgetsList}}", - "duplicate-alias-error": "'{{alias}}' daha önce kaydedilmiş.
    Öğe kısa adları kontrol paneli özelinde emsalsiz olmalı.", - "missing-entity-filter-error": "'{{alias}}' için filtre bulunmuyor.", - "configure-alias": "'{{alias}}' kısa adını yapılandır", - "alias": "Kısa ad", - "alias-required": "Öğe kısa adı gerekli.", - "remove-alias": "Öğe kısa adını kaldır", - "add-alias": "Öğe kısa adı ekle", - "entity-list": "Öğe listesi", - "entity-type": "Öğe türü", - "entity-types": "Öğe türleri", - "entity-type-list": "Öğe türü listesi", - "any-entity": "Herhangi bir öğe", - "enter-entity-type": "Öğe türü girin", - "no-entities-matching": "'{{entity}}' ile eşleşen öğe bulunamadı.", - "no-entity-types-matching": "'{{entityType}}' ile eşleşen öğe türü bulunamadı.", - "name-starts-with": "... ile başlayan isim", - "use-entity-name-filter": "Filtre kullan", - "entity-list-empty": "Hiçbir öğe seçilmedi.", - "entity-type-list-empty": "Hiçbir öğe türü seçilmedi.", - "entity-name-filter-required": "Öğe ismi filtresi gerekli.", - "entity-name-filter-no-entity-matched": "'{{entity}}' ile başlayan hiçbir öğe bulunamadı.", - "all-subtypes": "Tümü", - "select-entities": "Öğeleri seç", - "no-aliases-found": "Hiçbir kısa ad bulunamadı.", - "no-alias-matching": "'{{alias}}' bulunamadı.", - "create-new-alias": "Yeni bir tane oluştur!", - "key": "Anahtar", - "key-name": "Anahtar adı", - "no-keys-found": "Hiçbir anahtar bulunamadı.", - "no-key-matching": "'{{key}}' bulunamadı.", - "create-new-key": "Yeni bir tane oluştur!", - "type": "Tür", - "type-required": "Öğe türü gerekli.", - "type-device": "Aygıt", - "type-devices": "Aygıtlar", - "list-of-devices": "{ count, plural, 1 {Bir aygıt} other {# Aygıtın Listesi} }", - "device-name-starts-with": "İsimleri '{{prefix}}' ile başlayan aygıtlar", - "type-asset": "Varlık", - "type-assets": "Varlıklar", - "list-of-assets": "{ count, plural, 1 {Bir varlık} other {# Varlığın Listesi} }", - "asset-name-starts-with": "İsmi '{{prefix}}' ile başlayan varlıklar", - "type-entity-view": "Varlık Görünümü", - "type-entity-views": "Varlık Görünümleri", - "list-of-entity-views": "{ count, plural, 1 {Bir varlık görünümü} other {# varlık görüntüleme}} listesi", - "entity-view-name-starts-with": "Adı {{önek}} ile başlayan varlık görünümleri", - "type-rule": "Kural", - "type-rules": "Kurallar", - "list-of-rules": "{ count, plural, 1 {Bir kural} other {# Kuralın Listesi} }", - "rule-name-starts-with": "İsmi '{{prefix}}' ile başlayan kurallar", - "type-plugin": "Eklenti", - "type-plugins": "Eklentiler", - "list-of-plugins": "{ count, plural, 1 {Bir eklenti} other {# Eklentinin Listesi} }", - "plugin-name-starts-with": "İsmi '{{prefix}}' ile başlayan eklentiler", - "type-tenant": "Tenant", - "type-tenants": "Tenantlar", - "list-of-tenants": "{ count, plural, 1 {Bir tenant} other {# Tenantın Listesi} }", - "tenant-name-starts-with": "İsmi '{{prefix}}' ile başlayan tenantlar", - "type-customer": "Kullanıcı Grubu", - "type-customers": "Kullanıcı Grupları", - "list-of-customers": "{ count, plural, 1 {Bir Kullanıcı Grubu} other {# Kullanıcı Grupları} }", - "customer-name-starts-with": "İsmi '{{prefix}}' ile başlayan Kullanıcı Grupları", - "type-user": "Kullanıcı", - "type-users": "Kullanıcılar", - "list-of-users": "{ count, plural, 1 {Bir kullanıcı} other {# Kullanıcının Listesi} }", - "user-name-starts-with": "İsmi '{{prefix}}' ile başlayan kullanıcılar", - "type-dashboard": "Kontrol paneli", - "type-dashboards": "Kontrol panelleri", - "list-of-dashboards": "{ count, plural, 1 {Bir kontrol paneli} other {# Kontrol Panelinin Listesi} }", - "dashboard-name-starts-with": "İsmi '{{prefix}}' ile başlayan kontrol panelleri", - "type-alarm": "Alarm", - "type-alarms": "Alarmlar", - "list-of-alarms": "{ count, plural, 1 {Bir alarm} other {# Alarmın Listesi} }", - "alarm-name-starts-with": "İsmi '{{prefix}}' ile başlayan alarmlar", - "type-rulechain": "Kural zinciri", - "type-rulechains": "Kural zincirleri", - "list-of-rulechains": "{ count, plural, 1 {Bir kural zinciri} other {# kural zincirinin listesi}}", - "rulechain-name-starts-with": "İsimleri {{prefix}} ile başlayan kural zincirleri", - "type-rulenode": "Kural düğümü", - "type-rulenodes": "Kural düğümleri", - "list-of-rulenodes": "{ count, plural, 1 {Bir kural node} other {# kural düğümünün listesi}}", - "rulenode-name-starts-with": "İsimleri '{{prefix}} ile başlayan kural düğümleri", - "type-current-customer": "Mevcut Müşteri", - "search": "Öğeleri ara", - "selected-entities": "{ count, plural, 1 {1 öğe} other {# öğe} } seçildi", - "entity-name": "Öğe adı", - "details": "Öğe detayları", - "no-entities-prompt": "Hiçbir öğe bulunamadı", - "no-data": "Görüntülenecek veri yok" - }, - "entity-view": { - "entity-view": "Varlık Görünümü", - "entity-views": "Varlık Görünümleri", - "management": "Varlık Görünümü yönetimi", - "view-entity-views": "Varlık Görünümlerini Görüntüle", - "entity-view-alias": "Varlık Görünümü takma adı", - "aliases": "Varlık Görünümü takma adları", - "no-alias-matching": "'{{alias}} bulunamadı. ", - "no-aliases-found": "Takma ad bulunamadı", - "no-key-matching": "'{{anahtar bulunamadı.", - "no-keys-found": "Anahtar bulunamadı.", - "create-new-alias": "Yeni bir tane oluştur!", - "create-new-key": "Yeni bir tane oluştur!", - "duplicate-alias-error": "Yinelenen takma ad bulundu {{alias}} '.. Entity View diğer adlar, gösterge panosunda benzersiz olmalıdır. ", - "configure-alias": "Yapılandırma {{alias}} takma ad", - "no-entity-views-matching": "{{entity}} ile eşleşen hiçbir varlık yorumu bulunamadı. ", - "alias": "Alias", - "alias-required": "Varlık Görünümü takma adı gerekiyor.", - "remove-alias": "Varlık görünümü takma adını kaldır", - "add-alias": "Varlık görünümü takma adı ekle", - "name-starts-with": "Varlık Görünümü adı ile başlıyor", - "entity-view-list": "Varlık Görünümü listesi", - "use-entity-view-name-filter": "Filtre kullan", - "entity-view-list-empty": "Hiçbir varlık görüşü seçilmedi.", - "entity-view-name-filter-required": "Varlık görünüm adı filtresi gerekli.", - "entity-view-name-filter-no-entity-view-matched": "{{entityView}} ile başlayan hiçbir varlık sayısı bulunamadı.", - "add": "Varlık Görünümü Ekle", - "assign-to-customer": "Müşteriye atama", - "assign-entity-view-to-customer": "Varlık Görünümlerini Müşteriye Atama", - "assign-entity-view-to-customer-text": "Lütfen müşteriye atamak için varlık görünümlerini seçin", - "no-entity-views-text": "Varlık görüşü bulunamadı", - "assign-to-customer-text": "Lütfen varlık görünümlerini atamak için müşteriyi seçin", - "entity-view-details": "Varlık görünümü ayrıntıları", - "add-entity-view-text": "Yeni varlık görünümü ekle", - "delete": "Varlık görünümünü sil", - "assign-entity-views": "Varlık görünümleri atama", - "assign-entity-views-text": "Müşteriye { count, plural, 1 {1 entityView} other {# entityViews}} atayın ", - "delete-entity-views": "Varlık görünümlerini sil", - "unassign-from-customer": "Müşteriden atama", - "unassign-entity-views": "Varlık görünümlerini atama", - "unassign-entity-views-action-title": "Müşteriden atama { count, plural, 1 {1 entityView} other {# entityViews}}", - "assign-new-entity-view": "Yeni varlık görünümü atama", - "delete-entity-view-title": "Varlık görünümünü silmek istediğinizden emin misiniz?, {{entityViewName}} '? ", - "delete-entity-view-text": "Dikkatli olun, onaylandıktan sonra varlık görünümü ve ilgili tüm veriler kurtarılamayacak.", - "delete-entity-views-title": "{ count, plural, 1 {1 entityView} other {# entityViews}} varlık görünümüne sahip olmak istediğinizden emin misiniz?", - "delete-entity-views-action-title": "Sil { count, plural, 1 {1 entityView} other {# entityViews}}", - "delete-entity-views-text": "Dikkatli olun, onaylandıktan sonra tüm seçilen görünümler kaldırılacak ve ilgili tüm veriler kurtarılamayacaktır.", - "unassign-entity-view-title": "Varlık görünümünün atamasını kaldırmak istediğinizden emin misiniz? {{entityViewName}} '? ", - "unassign-entity-view-text": "Onaydan sonra varlık görünümü atanmamış olacak ve müşteri tarafından erişilemeyecektir.", - "unassign-entity-view": "Varlık görünümünün atamasını kaldır", - "unassign-entity-views-title": "{ count, plural, 1 {1 entityView} other {# entityViews}} hesabının atamasını kaldırmak istediğinizden emin misiniz?", - "unassign-entity-views-text": "Onaylandıktan sonra, seçilen tüm öğe görünümleri atamadan kaldırılacak ve müşteri tarafından erişilemeyecektir.", - "entity-view-type": "Varlık Görünümü türü", - "entity-view-type-required": "Varlık Görünümü türü gerekli.", - "select-entity-view-type": "Varlık görüntüleme türünü seç", - "enter-entity-view-type": "Varlık görüntüleme türünü girin", - "any-entity-view": "Herhangi bir varlık görünümü", - "no-entity-view-types-matching": "{{entitySubtype}} ile eşleşen hiçbir varlık görüntüleme türü bulunamadı. ", - "entity-view-type-list-empty": "Hiçbir varlık görünümü türü seçilmemiş.", - "entity-view-types": "Varlık Görünümü türleri", - "name": "Ad", - "name-required": "İsim gerekli.", - "description": "Açıklama", - "events": "Etkinlikler", - "details": "Ayrıntılar", - "copyId": "Varlık görüntüleme kimliğini kopyala", - "assignedToCustomer": "Müşteriye atandı", - "unable-entity-view-device-alias-title": "Varlık görünümü takma adı silinemiyor", - "unable-entity-view-device-alias-text": "Cihaz takma adı {{entityViewAlias}} ', aşağıdaki widget (lar) tarafından kullanıldığı şekliyle silinemez:
    {{widgetsList}} ", - "select-entity-view": "Varlık görünümünü seç", - "make-public": "Varlığı herkese görünür yap", - "start-ts": "Ts", - "end-ts": "End ts" - }, - "event": { - "event-type": "Olay türü", - "type-error": "Hata", - "type-lc-event": "Yaşam döngüsü olayı", - "type-stats": "İstatistikler", - "type-debug-rule-node": "Hata ayıklama", - "type-debug-rule-chain": "Hata ayıklama", - "no-events-prompt": "Hiçbir olay bulunamadı", - "error": "Hata", - "alarm": "Alarm", - "event-time": "Olay zamanı", - "server": "Sunucu", - "body": "İçerik //(Body)", - "method": "Yöntem", - "type": "Tür", - "entity": "Varlık", - "message-id": "Mesaj Kimliği", - "message-type": "Mesaj tipi", - "data-type": "Veri tipi", - "relation-type": "İlişki Türü", - "metadata": "Meta veri", - "data": "Veri", - "event": "Olay", - "status": "Durum", - "success": "Başarı", - "failed": "Başarısız oldu", - "messages-processed": "Mesajlar işlendi", - "errors-occurred": "Hatalar oluştu" - }, - "extension": { - "extensions": "Uzantılar", - "selected-extensions": "{ count, plural, 1 {1 uzantı} other {# extensions}} seçildi", - "type": "Tür", - "key": "Anahtar", - "value": "Değer", - "id": "İD", - "extension-id": "Uzantı kimliği", - "extension-type": "Uzatma tipi", - "transformer-json": "JSON *", - "unique-id-required": "Mevcut uzantı kimliği zaten mevcut.", - "delete": "Uzantıyı sil", - "add": "Uzantı eklemek", - "edit": "Uzantıyı düzenle", - "delete-extension-title": "{{ExtensionId}} uzantısını silmek istediğinizden emin misiniz? ", - "delete-extension-text": "Dikkatli olun, onaylamadan sonra uzantı ve ilgili tüm veriler kurtarılamaz.", - "delete-extensions-title": "{ count, plural, 1 {1 uzantı} other {# extensions}} silmek istediğinizden emin misiniz?", - "delete-extensions-text": "Dikkatli olun, onaylandıktan sonra tüm seçilen uzantılar kaldırılacak.", - "converters": "Dönüştürücü", - "converter-id": "Dönüştürücü kimliği", - "configuration": "Yapılandırma", - "converter-configurations": "Dönüştürücü yapılandırmaları", - "token": "Güvenlik belirteci", - "add-converter": "Dönüştürücü ekle", - "add-config": "Dönüştürücü yapılandırması ekle", - "device-name-expression": "Cihaz adı ifadesi", - "device-type-expression": "Cihaz tipi ifadesi", - "custom": "Özel", - "to-double": "Çifte", - "transformer": "Transformer", - "json-required": "Trafo jsonu gerekli.", - "json-parse": "Trafo json ayrıştırılamıyor.", - "attributes": "Öznitellikler", - "add-attribute": "Özellik ekle", - "add-map": "Eşleme elemanı ekle", - "timeseries": "Zaman serisi", - "add-timeseries": "Zaman çizelgeleri ekle", - "field-required": "Alan gereklidir", - "brokers": "Komisyoncular", - "add-broker": "Broker ekle", - "host": "Host", - "port": "Liman", - "port-range": "Liman 1'den 65535'e kadar olmalıdır.", - "ssl": "SSL", - "credentials": "Kimlik bilgileri", - "username": "Kullanıcı adı", - "password": "Parola", - "retry-interval": "Milisaniye cinsinden tekrar deneme aralığı", - "anonymous": "Anonim", - "basic": "Temel", - "pem": "PEM", - "ca-cert": "CA sertifika dosyası *", - "private-key": "Özel anahtar dosya *", - "cert": "Sertifika dosyası *", - "no-file": "Dosya seçilmedi.", - "drop-file": "Bir dosya bırakın veya yüklenecek bir dosya seçmek için tıklayın.", - "mapping": "Mapping", - "topic-filter": "Konu filtresi", - "converter-type": "Dönüştürücü tipi", - "converter-json": "Json", - "json-name-expression": "Cihaz adı json ifadesi", - "topic-name-expression": "Cihaz adı konu ifadesi", - "json-type-expression": "Cihaz tipi json ifadesi", - "topic-type-expression": "Cihaz tipi konu ifadesi", - "attribute-key-expression": "Öznitelik anahtar ifadesi", - "attr-json-key-expression": "Öznitelik anahtar json ifadesi", - "attr-topic-key-expression": "Öznitelik anahtar konu ifadesi", - "request-id-expression": "Kimlik ifadesi iste", - "request-id-json-expression": "Kimlik json ifadesi iste", - "request-id-topic-expression": "Kimlik konu ifadesini isteyin", - "response-topic-expression": "Yanıt konusu ifadesi", - "value-expression": "Değer ifadesi", - "topic": "Konu", - "timeout": "Zaman aşımı milisaniye cinsinden", - "converter-json-required": "Dönüştürücü json gerekli.", - "converter-json-parse": "Dönüştürücü json ayrıştırılamıyor.", - "filter-expression": "Filtre ifadesi", - "connect-requests": "İstekleri bağla", - "add-connect-request": "Bağlantı talebi ekle", - "disconnect-requests": "İstekleri kes", - "add-disconnect-request": "Bağlantıyı kes isteği ekle", - "attribute-requests": "Özellik istekleri", - "add-attribute-request": "Özellik isteği ekle", - "attribute-updates": "Öznitelik güncellemeleri", - "add-attribute-update": "Özellik güncellemesi ekle", - "server-side-rpc": "Sunucu tarafı RPC", - "add-server-side-rpc-request": "Sunucu tarafı RPC isteği ekle", - "device-name-filter": "Cihaz adı filtresi", - "attribute-filter": "Özellik filtresi", - "method-filter": "Yöntem filtresi", - "request-topic-expression": "Konu ifadesi iste", - "response-timeout": "Milisaniye cinsinden yanıt zaman aşımı", - "topic-expression": "Konu ifadesi", - "client-scope": "Müşteri kapsamı", - "add-device": "Cihaz ekle", - "opc-server": "Sunucular", - "opc-add-server": "Sunucu ekle", - "opc-add-server-prompt": "Lütfen sunucu ekle", - "opc-application-name": "Uygulama Adı", - "opc-application-uri": "Uygulama uri", - "opc-scan-period-in-seconds": "Saniyeler içinde tarama süresi", - "opc-security": "Güvenlik", - "opc-identity": "Kimlik", - "opc-keystore": "Keystore", - "opc-type": "Tür", - "opc-keystore-type": "Tür", - "opc-keystore-location": "Yer *", - "opc-keystore-password": "Parola", - "opc-keystore-alias": "Alias", - "opc-keystore-key-password": "Anahtar şifre", - "opc-device-node-pattern": "Cihaz düğümü modeli", - "opc-device-name-pattern": "Cihaz adı deseni", - "modbus-server": "Sunucular / köle", - "modbus-add-server": "Sunucu ekle / köle", - "modbus-add-server-prompt": "Lütfen sunucu / slave ekle", - "modbus-transport": "Taşıma", - "modbus-port-name": "Seri port adı", - "modbus-encoding": "Kodlama", - "modbus-parity": "Parite", - "modbus-baudrate": "Baud hızı", - "modbus-databits": "Veri bitleri", - "modbus-stopbits": "Bitleri durdur", - "modbus-databits-range": "Veri bitleri 7 ila 8 arasında olmalıdır", - "modbus-stopbits-range": "Durma bitleri 1'den 2'ye kadar olmalıdır.", - "modbus-unit-id": "Birim Kimliği", - "modbus-unit-id-range": "Birim numarası 1 ile 247 arasında olmalıdır.", - "modbus-device-name": "Cihaz adı", - "modbus-poll-period": "Anket dönemi (ms)", - "modbus-attributes-poll-period": "Nitelikler yoklama süresi (ms)", - "modbus-timeseries-poll-period": "Timeseries anket süresi (ms)", - "modbus-poll-period-range": "Anket dönemi pozitif değer olmalı", - "modbus-tag": "Etiket", - "modbus-function": "İşlev", - "modbus-register-address": "Kayıt adresi", - "modbus-register-address-range": "Kayıt adresi 0 ile 65535 arasında olmalıdır.", - "modbus-register-bit-index": "Bit endeksi", - "modbus-register-bit-index-range": "Bit endeksi 0 ile 15 arasında olmalıdır", - "modbus-register-count": "Kayıt sayısı", - "modbus-register-count-range": "Kayıt sayısı pozitif bir değer olmalıdır.", - "modbus-byte-order": "Bayt sırası", - "sync": { - "status": "Durum", - "sync": "Senkronizasyon", - "not-sync": "Eşitleme", - "last-sync-time": "Son senkronizasyon zamanı", - "not-available": "Müsait değil" - }, - "export-extensions-configuration": "İhracat uzantıları yapılandırması", - "import-extensions-configuration": "Uzantılarını içe aktarma yapılandırması", - "import-extensions": "Uzantıları içe aktar", - "import-extension": "Uzantı içe aktar", - "export-extension": "İhracat uzantısı", - "file": "Uzantılar dosyası", - "invalid-file-error": "Geçersiz uzantı dosyası" - }, - "fullscreen": { - "expand": "Tam ekran yap", - "exit": "Tam ekrandan çık", - "toggle": "Tam ekran modu aç/kapat", - "fullscreen": "Tam ekran" - }, - "function": { - "function": "Fonksiyon" - }, - "grid": { - "delete-item-title": "Bu öğeyi silmek istediğinizden emin misiniz?", - "delete-item-text": "UYARI: Onayladıktan sonra bu öğe ve ilişkili tüm verileri geri yüklenemez şekilde silinecektir.", - "delete-items-title": "{ count, plural, 1 {1 öğeyi} other {# öğeyi} } silmek istediğinizden emin misiniz?", - "delete-items-action-title": "{ count, plural, 1 {1 öğeyi} other {# öğeyi} } sil", - "delete-items-text": "UYARI: Onayladıktan sonra tüm seçili öğeler ve ilişkili tüm verileri geri yüklenemez şekilde silinecektir.", - "add-item-text": "Yeni öğe ekle", - "no-items-text": "Hiç bir öğe bulunamadı", - "item-details": "Öğe detayları", - "delete-item": "Öğeyi sil", - "delete-items": "Öğeleri sil", - "scroll-to-top": "Üste kaydır" - }, - "help": { - "goto-help-page": "Yardım sayfasına git" - }, - "home": { - "home": "Ana sayfa", - "profile": "Profil", - "logout": "Çıkış", - "menu": "Menü", - "avatar": "Avatar", - "open-user-menu": "Kullanıcı menüsünü aç" - }, - "import": { - "no-file": "Hiçbir dosya seçilmedi", - "drop-file": "Bir JSON dosyası bırakın veya yüklenecek bir dosyayı seçmek için tıklayın." - }, - "item": { - "selected": "Seçildi" - }, - "js-func": { - "no-return-error": "Fonksiyon bir değer dönmeli!", - "return-type-mismatch": "Fonksiyon '{{type}}' türünde bir değer dönmeli!", - "tidy": "Düzenli" - }, - "key-val": { - "key": "Anahtar", - "value": "Değer", - "remove-entry": "Girişi kaldır", - "add-entry": "Giriş ekle", - "no-data": "Giriş yok" - }, - "layout": { - "layout": "Arayüz Düzeni", - "manage": "Arayüz düzenini yönet", - "settings": "Arayüz düzeni ayarları", - "color": "Renk", - "main": "Ana", - "right": "Sağ", - "select": "Hedef düzen seç" - }, - "legend": { - "position": "Lejant konumu", - "show-max": "Maksimum değeri göster", - "show-min": "Minimum değeri göster", - "show-avg": "Ortalama değeri göster", - "show-total": "Toplam değeri göster", - "settings": "Lejant ayarları", - "min": "min", - "max": "maks", - "avg": "ort.", - "total": "toplam" - }, - "login": { - "login": "Giriş Yap", - "request-password-reset": "Parola Sıfırlama İsteği Gönder", - "reset-password": "Parola Sıfırla", - "create-password": "Parola Oluştur", - "passwords-mismatch-error": "Girilen parolalar eşleşmeli!", - "password-again": "Parola tekrarı", - "sign-in": "Lütfen girişi yapın", - "username": "Kullanıcı adı (e-posta)", - "remember-me": "Beni hatırla", - "forgot-password": "Parolamı unuttum", - "password-reset": "Parola sıfırla", - "new-password": "Yeni parola", - "new-password-again": "Yeni parola tekrarı", - "password-link-sent-message": "Parola sıfırlama e-postası başarıyla gönderildi!", - "email": "E-posta", - "login-with": "{{name}} ile Giriş Yap", - "or": "ya da" - }, - "position": { - "top": "Üst", - "bottom": "Alt", - "left": "Sol", - "right": "Sağ" - }, - "profile": { - "profile": "Profil", - "change-password": "Şifre değiştir", - "current-password": "Şimdiki şifre" - }, - "relation": { - "relations": "İlişkiler", - "direction": "Yönelim", - "search-direction": { - "FROM": "KAYNAK", - "TO": "HEDEF" - }, - "direction-type": { - "FROM": "kaynak", - "TO": "hedef" - }, - "from-relations": "Giden ilişkiler", - "to-relations": "Gelen ilişkiler", - "selected-relations": "{ count, plural, 1 {1 ilişki} other {# ilişki} } seçildi", - "type": "Tür", - "to-entity-type": "Hedef Öğe Türü", - "to-entity-name": "Hedef Öğe Adı", - "from-entity-type": "Kaynak Öğe Türü", - "from-entity-name": "Kaynak Öğe Adı", - "to-entity": "Hedef Öğe", - "from-entity": "Kaynak Öğe", - "delete": "İlişkiyi sil", - "relation-type": "İlişki türü", - "relation-type-required": "İlişki türü gerekli.", - "any-relation-type": "Her hangi bir tür", - "add": "İlişki ekle", - "edit": "İlişki düzenle", - "delete-to-relation-title": "'{{entityName}}' öğesine olan ilişkiyi silmek istediğinize emin misiniz?", - "delete-to-relation-text": "UYARI: Onaylandıktan sonra '{{entityName}}' öğesinin şimdiki öğeyle olan ilişkisi sona erecektir.", - "delete-to-relations-title": "{ count, plural, 1 {1 ilişkiyi} other {# ilişkiyi} } silmek istediğinize emin misiniz?", - "delete-to-relations-text": "UYARI: Onaylandıktan sonra tüm seçili ilişkiler kaldırılacaktır ve ilgili öğelerin şimdiki öğeyle ilişkisi sona erecektir.", - "delete-from-relation-title": "'{{entityName}}' öğesinden ilişkiyi silmek istediğinize emin misiniz?", - "delete-from-relation-text": "UYARI: Onaylandıktan sonra şimdiki öğenin '{{entityName}}' öğesiyle ilişkisi sonlandırılacaktır.", - "delete-from-relations-title": "{ count, plural, 1 {1 ilişkiyi} other {# ilişkiyi} } silmek istediğinize emin misiniz?", - "delete-from-relations-text": "UYARI: Onaylandıktan sonra tüm seçili ilişkiler kaldırılacak ve şimdiki öğenin ilgili tüm öğelerle ilişkisi sona erecektir.", - "remove-relation-filter": "İlişki filtresini kaldır", - "add-relation-filter": "İlişkisi ekle", - "any-relation": "Herhangi bir ilişki", - "relation-filters": "İlişki filtreleri", - "additional-info": "Ek bilgi (JSON)", - "invalid-additional-info": "Ek bilgi JSON'ı parse edilip işlenemedi." - }, - "rulechain": { - "rulechain": "Kural", - "rulechains": "Kurallar", - "root": "Kök", - "delete": "Kuralı sil", - "name": "İsim", - "name-required": "İsim gerekli.", - "description": "Açıklama", - "add": "Kural Ekle", - "set-root": "Kural zincirinin kökü yap", - "set-root-rulechain-title": "Kural zincirini {{ruleChainName}} root? Yapmak istediğinizden emin misiniz?", - "set-root-rulechain-text": "Onaydan sonra kural zinciri kökleşecek ve gelen tüm iletilerle ilgilenecek.", - "delete-rulechain-title": "'{{ruleName}}' isimli kuralı silmek istediğinize emin misiniz?", - "delete-rulechain-text": "UYARI: Onaylandıktan sonra kural ve ilişkili tüm veriler geri yüklenemez şekilde silinecektir.", - "delete-rulechains-title": "{ count, plural, 1 {1 kuralı} other {# kuralı} } sikmek istediğinize emin misiniz?", - "delete-rulechains-action-title": "{ count, plural, 1 {1 kuralı} other {# kuralı} } sil", - "delete-rulechains-text": "UYARI: Onaylandıktan sonra seçili tüm kurallar ve ilişkili tüm veriler geri yüklenemez şekilde silinecektir.", - "add-rulechain-text": "Yeni kural ekle", - "no-rulechains-text": "Hiçbir kural bulunamadı", - "rulechain-details": "Kural detayları", - "details": "Detaylar", - "events": "Olaylar", - "system": "Sistem", - "import": "Kuralı içe aktar", - "export": "Kuralı dışa aktar", - "export-failed-error": "Kural dışa aktarılamadı: {{error}}", - "create-new-rule": "Yeni kural oluştur", - "rulechain-file": "Kural dosyası", - "invalid-rulechain-file-error": "Kural içe aktarılamadı: Geçersiz kural veri yapısı.", - "copyId": "Kural kimliğini kopyala", - "idCopiedMessage": "Kural kimliği panoya kopyalandı", - "select-rulechain": "Kural seç", - "no-rulechains-matching": "'{{entity}}' ile eşleşen kural bulunamadı.", - "rulechain-required": "Kural gerekli", - "management": "Kural yönetimi", - "debug-mode": "Hata ayıklama modu" - }, - "rulenode": { - "details": "Ayrıntılar", - "events": "Etkinlikler", - "search": "Arama düğümleri", - "open-node-library": "Düğüm kütüphanesini aç", - "add": "Kural düğümü ekle", - "name": "Ad", - "name-required": "İsim gerekli.", - "type": "Tür", - "description": "Açıklama", - "delete": "Kural düğümünü sil", - "select-all-objects": "Tüm düğümleri ve bağlantıları seç", - "deselect-all-objects": "Tüm düğümlerin ve bağlantıların seçimini kaldırın", - "delete-selected-objects": "Seçilen düğümleri ve bağlantıları sil", - "delete-selected": "Silme seçildi", - "select-all": "Hepsini seç", - "copy-selected": "Seçilenleri kopyala", - "deselect-all": "Hiçbirini seçme", - "rulenode-details": "Kural düğümü ayrıntıları", - "debug-mode": "Hata ayıklama modu", - "configuration": "Yapılandırma", - "link": "Bağlantı", - "link-details": "Kural düğüm bağlantı detayları", - "add-link": "Link ekle", - "link-label": "Bağlantı etiketi", - "link-label-required": "Bağlantı etiketi gerekli.", - "custom-link-label": "Özel bağlantı etiketi", - "custom-link-label-required": "Özel bağlantı etiketi gerekli.", - "link-labels": "Link etiketleri", - "link-labels-required": "Link etiketleri gerekli.", - "no-link-labels-found": "Bağlantı etiketi bulunamadı", - "no-link-label-matching": "{{label}} bulunamadı. ", - "create-new-link-label": "Yeni bir tane oluştur!", - "type-filter": "Filtre", - "type-filter-details": "Gelen iletileri yapılandırılmış koşullara göre filtrele", - "type-enrichment": "Zenginleştirme", - "type-enrichment-details": "Mesaj Meta Verilerine ek bilgi", - "type-transformation": "Dönüşüm", - "type-transformation-details": "Mesaj yükünü ve Meta Verileri Değiştir", - "type-action": "Aksiyon", - "type-action-details": "Özel eylem gerçekleştir", - "type-external": "Dış", - "type-external-details": "Dış sistemle etkileşir", - "type-rule-chain": "Kural Zinciri", - "type-rule-chain-details": "Belirtilen Kural Zincirine gelen mesajları ilet", - "type-input": "Giriş", - "type-input-details": "Kural Zinciri'nin mantıksal girdisi, bir sonraki ilgili Kural Düğümüne gelen iletileri iletme", - "type-unknown": "Bilinmeyen", - "type-unknown-details": "Çözümlenmemiş Kural Düğümü", - "directive-is-not-loaded": "Tanımlanmış yapılandırma yönergesi {{directiveName}} 'mevcut değil. ", - "ui-resources-load-error": "Yapılandırma kullanıcı arayüzü kaynakları yüklenemedi.", - "invalid-target-rulechain": "Hedef kural zinciri çözülemiyor!", - "test-script-function": "Test komut dosyası işlevi", - "message": "Mesaj", - "message-type": "Mesaj tipi", - "select-message-type": "Mesaj tipini seç", - "message-type-required": "Mesaj türü gerekli", - "metadata": "Meta veri", - "metadata-required": "Meta veri girişleri boş bırakılamaz.", - "output": "Çıktı", - "test": "Ölçek", - "help": "Yardım et" - }, - "tenant": { - "tenant": "Tenant", - "tenants": "Tenantlar", - "management": "Tenant yönetimi", - "add": "Tenant Ekle", - "admins": "Adminler", - "manage-tenant-admins": "Tenant Adminlerini Yönet", - "delete": "Tenant sil", - "add-tenant-text": "Yeni tenant ekle", - "no-tenants-text": "Hiçbir tenant bulunamadı", - "tenant-details": "Tenant detayları", - "delete-tenant-title": "'{{tenantTitle}}' isimli tenantı silmek istediğinize emin misiniz?", - "delete-tenant-text": "UYARI: Onaylandıktan sonra tenant ve ilişkili tüm veriler geri yüklenemez şekilde silinecektir.", - "delete-tenants-title": "{ count, plural, 1 {1 tenantı} other {# tenantı} } silmek istediğinize emin misiniz?", - "delete-tenants-action-title": "{ count, plural, 1 {1 tenantı} other {# tenantı} } sil", - "delete-tenants-text": "UYARI: Onaylandıktan sonra seçili tüm tenantlar ve ilişkili tüm veriler geri yüklenemez şekilde silinecektir", - "title": "Başlık", - "title-required": "Başlık gerekli.", - "description": "Açıklama", - "details": "Detaylar", - "events": "Olaylar", - "copyId": "Tenant kimliğini kopyala", - "idCopiedMessage": "Tenant kimliği panoya kopyalandı", - "select-tenant": "Tenant seç", - "no-tenants-matching": "'{{entity}}' ile eşleşen tenant bulunamadı.", - "tenant-required": "Tenant gerekli" - }, - "timeinterval": { - "seconds-interval": "{ seconds, plural, 1 {1 saniye} other {# saniye} }", - "minutes-interval": "{ minutes, plural, 1 {1 dakika} other {# dakika} }", - "hours-interval": "{ hours, plural, 1 {1 saat} other {# saat} }", - "days-interval": "{ days, plural, 1 {1 gün} other {# gün} }", - "days": "Gün", - "hours": "Saat", - "minutes": "Dakika", - "seconds": "Saniye", - "advanced": "İleri düzey" - }, - "timewindow": { - "days": "{ days, plural, 1 { gün } other {# gün } }", - "hours": "{ hours, plural, 0 { saat } 1 {1 saat } other {# saat } }", - "minutes": "{ minutes, plural, 0 { dakika } 1 {1 dakika } other {# dakika } }", - "seconds": "{ seconds, plural, 0 { saniye } 1 {1 saniye } other {# saniye } }", - "realtime": "Gerçek zaman", - "history": "Tarih", - "last-prefix": "son", - "period": "{{ startTime }}'dan {{ endTime }}'a kadar", - "edit": "Zaman aralığını düzenle", - "date-range": "Tarih aralığı", - "last": "Son", - "time-period": "Zaman periyodu" - }, - "user": { - "user": "Kullanıcı", - "users": "Kullanıcılar", - "customer-users": "Kullanıcılar", - "tenant-admins": "Tenant Adminleri", - "sys-admin": "Sistem yöneticisi", - "tenant-admin": "Tenant yöneticisi", - "customer": "Kullanıcı Grubu", - "anonymous": "Anonim", - "add": "Kullanıcı ekle", - "delete": "Kullanıcı sil", - "add-user-text": "Yeni kullanıcı ekle", - "no-users-text": "Hiçbir kullanıcı bulunamadı", - "user-details": "Kullanıcı detayları", - "delete-user-title": "'{{userEmail}}' kullanıcısını silmek istediğinize emin misiniz?", - "delete-user-text": "UYARI: Onaylandıktan sonra kullanıcı ve ilişkili tüm verileri geri yüklenemez şekilde silinecektir.", - "delete-users-title": "{ count, plural, 1 {1 kullanıcıyı} other {# kullanıcıyı} } sikmek istediğinize emin misiniz?", - "delete-users-action-title": "{ count, plural, 1 {1 kullancıyı} other {# kullanıcıyı} } sil", - "delete-users-text": "UYARI: Onaylandıktan sonra kullanıcı ve ilişkili tüm verileri geri yüklenemez şekilde silinecektir.", - "activation-email-sent-message": "Etkinleştirme e-postası başarılı bir şekilde gönderildi!", - "resend-activation": "Etkinleştirme e-postasını yeniden gönder", - "email": "E-posta", - "email-required": "E-posta gerekli.", - "invalid-email-format": "Geçersiz e-posta formatı.", - "first-name": "Ad", - "last-name": "Soyad", - "description": "Açıklama", - "default-dashboard": "Varsayılan kontrol paneli", - "always-fullscreen": "Her zaman tam ekran", - "select-user": "Kullanıcı se.", - "no-users-matching": "'{{entity}}' ile eşleşen kullanıcı bulunamadı.", - "user-required": "Kullanıcı gerekli", - "activation-method": "Etkinleştirme yöntemi", - "display-activation-link": "Etkinleştirme bağlantısını görüntüle", - "send-activation-mail": "Etkinleştirme e-postası gönder", - "activation-link": "Kullanıcı hesabını etkinleştirme bağlantısı", - "activation-link-text": "Kullanıcı hesabını etkinleştirmek için bağlantıyı kullanın:", - "copy-activation-link": "Etkinleştirme bağlantısını kopyala", - "activation-link-copied-message": "Kullanıcı hesabı etkinleştirme bağlantısı panoya kopyalandı", - "details": "Ayrıntılar", - "login-as-tenant-admin": "Tenant Yönetici Girişi", - "login-as-customer-user": "Kullanıcı olarak giriş yap" - }, - "value": { - "type": "Değer tğrğ", - "string": "String", - "string-value": "String değeri", - "integer": "Integer", - "integer-value": "Integer değeri", - "invalid-integer-value": "Geçersiz integer değeri", - "double": "Double", - "double-value": "Double değeri", - "boolean": "Boolean", - "boolean-value": "Boolean değeri", - "false": "Yanlış", - "true": "Doğru", - "long": "Uzun" - }, - "widget": { - "widget-library": "Gösterge Kütüphanesi", - "widget-bundle": "Gösterge Demeti", - "select-widgets-bundle": "Gösterge demeti seç", - "management": "Gösterge yönetimi", - "editor": "Gösterge düzenleyici", - "widget-type-not-found": "Gösterge yapılandırması yüklenemedi.
    Muhtemelen ilgili\n gösterge türü kaldırılmış.", - "widget-type-load-error": "Gösterge şu sebeplerden dolayı yüklenemedi:", - "remove": "Göstergeyi kaldır", - "edit": "Göstergeyi düzenle", - "remove-widget-title": "'{{widgetTitle}}' isimli göstermeyi kaldırmak istediğinizden emin misiniz?", - "remove-widget-text": "UYARI: Onaylandıktan sonra gösterge ve tüm ilişkili verileri geri yüklenemez şekilde silinecek.", - "timeseries": "Zaman serisi", - "search-data": "Arama verileri", - "no-data-found": "Veri bulunamadı", - "latest-values": "Son değerler", - "rpc": "Kontrol göstergesi", - "alarm": "Alarm göstergesi", - "static": "Statik gösterge", - "select-widget-type": "Gösterge türü seç", - "missing-widget-title-error": "Gösterge başlığı belirtilmelidir!", - "widget-saved": "Gösterge kaydedildi", - "unable-to-save-widget-error": "Gösterge kaydedilemedi! Göstergede hatalar mevcut!", - "save": "Göstergeyi kaydet", - "saveAs": "Göstergeyi farklı kaydet", - "save-widget-type-as": "Gösterge türünü farklı kaydet", - "save-widget-type-as-text": "Lütfen gösterge başlığı girin veya hedef gösterge demeti seçin", - "toggle-fullscreen": "Tam ekran aç/kapat", - "run": "Göstergeyi çalıştır", - "title": "Gösterge başlığı", - "title-required": "Gösterge başlığı gerekli.", - "type": "Gösterge türü", - "resources": "Kaynaklar", - "resource-url": "JavaScript / CSS URL", - "remove-resource": "Kaynağı kaldır", - "add-resource": "Kaynak ekle", - "html": "HTML", - "tidy": "Tertiple", - "css": "CSS", - "settings-schema": "Ayarlar şeması", - "datakey-settings-schema": "Veri anahtarı ayarları şeması", - "javascript": "Javascript", - "remove-widget-type-title": "'{{widgetName}}' isimli gösterge türünü kaldırmak istediğinizden emin misiniz?", - "remove-widget-type-text": "UYARI: Onaylandıktan sonra, gösterge türü ve ilgili tüm veriler geri yüklenemez şekilde silinecektir.", - "remove-widget-type": "Gösterge türünü kaldır", - "add-widget-type": "Yeni gösterge türü ekle", - "widget-type-load-failed-error": "Gösterge türü yüklenemedi!", - "widget-template-load-failed-error": "Gösterge şablonu yüklenemedi!", - "add": "Gösterge ekle", - "undo": "Gösterge değişikliklerini geri al", - "export": "Göstergeyi dışa aktar" - }, - "widget-action": { - "header-button": "Gösterge başlık butonu", - "open-dashboard-state": "Yeni kontrol paneli durumunua git", - "update-dashboard-state": "Kontrol paneli durumunu güncelle", - "open-dashboard": "Diğer kontrol paneline git", - "custom": "Özel eylem", - "target-dashboard-state": "Hedef kontrol paneli durumu", - "target-dashboard-state-required": "Hedef kontrol paneli durumu gerekli", - "set-entity-from-widget": "Göstergeden öğe belirle", - "target-dashboard": "Hedef kontrol paneli", - "open-right-layout": "Sağdaki kontrol paneli arayüz düzenini aç(mobil görünüm)" - }, - "widgets-bundle": { - "current": "Şimdiki demet", - "widgets-bundles": "Gösterge Demetleri", - "add": "Gösterge Demeti Ekle", - "delete": "Gösterge demeti sil", - "title": "Başlık", - "title-required": "Başlık gerekli.", - "add-widgets-bundle-text": "Yeni gösterge demeti ekle", - "no-widgets-bundles-text": "Hiçbir gösterge demeti bulunamadı", - "empty": "Gösterge demeti boş", - "details": "Detaylar", - "widgets-bundle-details": "Gösterge demeti detayları", - "delete-widgets-bundle-title": "'{{widgetsBundleTitle}}' isimli gösterge demetini silmek istediğinize emin misiniz?", - "delete-widgets-bundle-text": "UYARI: Onaylandıktan sonra gösterge demeti ve ilişkili tüm veriler geri yüklenemez şekilde silinecektir.", - "delete-widgets-bundles-title": "{ count, plural, 1 {1 gösterge demetini} other {# gösterge demetini} } silmek istediğinize emin misiniz?", - "delete-widgets-bundles-action-title": "{ count, plural, 1 {1 gösterge demetini} other {# gösterge demetini} } sil", - "delete-widgets-bundles-text": "UYARI: Onaylandıktan sonra seçili tüm gösterge demetleri ve ilişkili tüm veriler geri yüklenemez şekilde silinecektir.", - "no-widgets-bundles-matching": "'{{widgetsBundle}}' ile eşleşen gösterge demeti bulunamadı.", - "widgets-bundle-required": "Gösterge demeti gerekli.", - "system": "Sistem", - "import": "Gösterge demetini içe aktar", - "export": "Gösterge demetini dışa aktar", - "export-failed-error": "Gösterge demetini dışa aktaramadı: {{error}}", - "create-new-widgets-bundle": "Yeni gösterge demeti oluştur", - "widgets-bundle-file": "Gösterge demeti dosyası", - "invalid-widgets-bundle-file-error": "Gösterge demeti içe aktarılamadı: Geçersiz gösterge demeti veri yapısı." - }, - "widget-config": { - "data": "Veri", - "settings": "Ayarlar", - "advanced": "İleri düzey", - "title": "Başlık", - "general-settings": "Genel ayarlar", - "display-title": "Başlığı göster", - "drop-shadow": "Gölge", - "enable-fullscreen": "Tam ekranı etkinleştir", - "background-color": "Arka plan rengi", - "text-color": "Yazı rengi", - "padding": "İç aralık (Padding)", - "margin": "Dış aralık (Margin)", - "widget-style": "Gösterge stili", - "title-style": "Başlık stili", - "mobile-mode-settings": "Mobil mod ayarları", - "order": "Sıra", - "height": "Yükseklik", - "units": "Değerin yanında göstermek için özel simge", - "decimals": "Noktadan sonraki basamak sayısı", - "timewindow": "Zaman aralığı", - "use-dashboard-timewindow": "Kontrol paneli zaman aralığı kullan", - "display-legend": "Lejant göster", - "datasources": "Veri kaynakları", - "maximum-datasources": "En fazla { count, plural, 1 {1 veri kaynağı kullanılabilir.} other {# veri kaynağı kullanılabilir} }", - "datasource-type": "Tür", - "datasource-parameters": "Parametreler", - "remove-datasource": "Veri kaynağını kaldır", - "add-datasource": "Veri kaynağı ekle", - "target-device": "Hedef aygıt", - "alarm-source": "Alarm kaynağı", - "actions": "Eylemler", - "action": "Eylem", - "add-action": "Eylem ekle", - "search-actions": "Eylem ara", - "action-source": "Eylem kaynağı", - "action-source-required": "Eylem kaynağı gerekli.", - "action-name": "İsim", - "action-name-required": "Eylem ismi gerekli.", - "action-name-not-unique": "Aynı ada sahip başka bir işlem zaten var.
    Eylem adı, aynı eylem kaynağı içinde emsalsiz olmalıdır.", - "action-icon": "İkon", - "action-type": "Tür", - "action-type-required": "Eylem türü gerekli.", - "edit-action": "Eylemi düzenle", - "delete-action": "Eylemi sil", - "delete-action-title": "Gösterge eylemini sil", - "delete-action-text": "'{{actionName}}' isimli gösterge eylemini silmek istediğinizden emin misiniz?" - }, - "widget-type": { - "import": "Gösterge türünü içer aktar", - "export": "Gösterge türünü dışa aktar", - "export-failed-error": "Gösterge türü dışa aktarılamadı: {{error}}", - "create-new-widget-type": "Yeni gösterge türü oluştur", - "widget-type-file": "Gösterge türü dosyası", - "invalid-widget-type-file-error": "Gösterge türü içe aktarılamadı: Geçersiz gösterge türü veri yapısı." - }, - "widgets": { - "date-range-navigator": { - "localizationMap": { - "Sun": "Paz", - "Mon": "Pzt", - "Tue": "Sal", - "Wed": "Çar", - "Thu": "Per", - "Fri": "Cum", - "Sat": "Cmt", - "Jan": "Oca", - "Feb": "Şub", - "Mar": "Mar", - "Apr": "Nis", - "May": "May", - "Jun": "Haz", - "Jul": "Tem", - "Aug": "Ağu", - "Sep": "Eyl", - "Oct": "Eki", - "Nov": "Kas", - "Dec": "Ara", - "January": "Ocak", - "February": "Şubat", - "March": "Mart", - "April": "Nisan", - "June": "Haziran", - "July": "Temmuz", - "August": "Ağustos", - "September": "Eylül", - "October": "Ekim", - "November": "Kasım", - "December": "Aralık", - "Custom Date Range": "Özel Tarih Aralığı", - "Date Range Template": "Tarih Aralığı Şablonu", - "Today": "Bugün", - "Yesterday": "Dün", - "This Week": "Bu hafta", - "Last Week": "Geçen hafta", - "This Month": "Bu ay", - "Last Month": "Geçen ay", - "Year": "Yıl", - "This Year": "Bu yıl", - "Last Year": "Geçen yıl", - "Date picker": "Tarih seçici", - "Hour": "Saat", - "Day": "Gün", - "Week": "Hafta", - "2 weeks": "2 Hafta", - "Month": "Ay", - "3 months": "3 Ay", - "6 months": "6 Ay", - "Custom interval": "Özel aralık", - "Interval": "Aralık", - "Step size": "Adım boyutu", - "Ok": "Ok" - } - } - }, - "icon": { - "icon": "İkon", - "select-icon": "İkon seç", - "material-icons": "Material konları", - "show-all": "Tüm ikonları göster" - }, - "custom": { - "widget-action": { - "action-cell-button": "Eylem hücre butonu", - "row-click": "Satır tıklama eylemi", - "polygon-click": "Satır tıklama eylemi", - "marker-click": "Çokgen tıklama eylemi", - "tooltip-tag-action": "İpucu etiket eylemi" - } - }, - "language": { - "language": "Dil" - } -} diff --git a/ui/src/app/locale/locale.constant-uk_UA.json b/ui/src/app/locale/locale.constant-uk_UA.json deleted file mode 100644 index 79c2ece00f..0000000000 --- a/ui/src/app/locale/locale.constant-uk_UA.json +++ /dev/null @@ -1,2408 +0,0 @@ -{ - "access": { - "unauthorized": "Неавторизований", - "unauthorized-access": "Неавторизований доступ", - "unauthorized-access-text": "Щоб отримати доступ до цього ресурсу, потрібно ввійти в систему!", - "access-forbidden": "Доступ заборонено", - "access-forbidden-text": "Недостатньо прав для доступу!
    Спробуйте увійти як інший користувач, якщо ви все ще хочете отримати доступ до цього ресурсу.", - "refresh-token-expired": "Дані про сесію застарілі", - "refresh-token-failed": "Не вдається відновити сеанс" - }, - "action": { - "activate": "Активувати", - "suspend": "Призупинити", - "save": "Зберегти", - "saveAs": "Зберегти як", - "cancel": "Скасувати", - "ok": "OK", - "delete": "Видалити", - "add": "Додати", - "yes": "Так", - "no": "Ні", - "update": "Оновити", - "remove": "Видалити", - "search": "Пошук", - "clear-search": "Очистити пошук", - "assign": "Надати", - "unassign": "Позбавити", - "share": "Поділитися", - "make-private": "Зробити приватним", - "apply": "Застосувати", - "apply-changes": "Застосувати зміни", - "edit-mode": "Режим редагування", - "enter-edit-mode": "Ввійти в режим редагування", - "decline-changes": "Відхилити зміни", - "close": "Закрити", - "back": "Назад", - "run": "Запустити", - "sign-in": "Увійти!", - "edit": "Редагувати", - "view": "Переглянути", - "create": "Створити", - "drag": "Перетягнути", - "refresh": "Оновити", - "undo": "Скасувати", - "copy": "Скопіювати", - "paste": "Вставити", - "copy-reference": "Копіювати посилання", - "paste-reference": "Вставити посилання", - "import": "Імпортувати", - "export": "Експортувати", - "share-via": "Поділитися через {{provider}}", - "continue": "Продовжити", - "discard-changes": "Скасувати зміни", - "move": "Перемістити", - "select": "Вибрати" - }, - "aggregation": { - "aggregation": "Агрегація", - "function": "Функція агрегації даних", - "limit": "Максимальні значення", - "group-interval": "Інтервал групування", - "min": "Мінімальний", - "max": "Максимальний", - "avg": "Середній", - "sum": "Сума", - "count": "Рахувати", - "none": "Відсутня" - }, - "admin": { - "general": "Загальне", - "general-settings": "Загальні налаштування", - "outgoing-mail": "Поштовий сервер", - "outgoing-mail-settings": "Налаштування сервера вихідної пошти", - "system-settings": "Налаштування системи", - "test-mail-sent": "Тестовий лист успішно відправлено!", - "base-url": "Базова URL-адреса", - "base-url-required": "Базова URL-адреса обов'язкова.", - "mail-from": "Електронна адреса", - "mail-from-required": "Електронна адреса обов'язкова.", - "smtp-protocol": "Протокол SMTP", - "smtp-host": "Хост SMTP", - "smtp-host-required": "Хост SMTP обов'язковий.", - "smtp-port": "SMTP-порт", - "smtp-port-required": "Ви повинні надати SMTP-порт.", - "smtp-port-invalid": "Це не схоже на дійсний SMTP-порт.", - "timeout-msec": "Час очікування (msec)", - "timeout-required": "Необхідно задати час очікування.", - "timeout-invalid": "Це не схоже на правильний час очікування.", - "enable-tls": "Увімкнути TLS", - "tls-version" : "Версія TLS", - "send-test-mail": "Надіслати тестове повідомлення", - "use-system-mail-settings": "Використовувати параметри системного поштового сервера", - "mail-templates": "Шаблони електронної пошти", - "mail-template-settings": "Налаштування шаблонів електронної пошти", - "use-system-mail-template-settings": "Використовувати шаблони системної електронної пошти", - "mail-template": { - "mail-template": "Шаблон електронної пошти", - "test": "Тестове повідомлення", - "activation": "Повідомлення про активацію рахунку", - "account-activated": "Обліковий запис активовано", - "reset-password": "Відновити повідомлення пароля", - "password-was-reset": "Пароль було надіслано повідомленням" - }, - "mail-subject": "Тема повідомлення", - "mail-body": "Вміст повідомлення", - "security-settings": "Налаштування безпеки", - "password-policy": "Політика щодо паролів", - "minimum-password-length": "Мінімальна довжина пароля", - "minimum-password-length-required": "Потрібна мінімальна довжина пароля", - "minimum-password-length-range": "Мінімальна довжина пароля повинна бути в межах від 5 до 50", - "minimum-uppercase-letters": "Мінімальна кількість великих літер", - "minimum-uppercase-letters-range": "Мінімальна кількість великих літер не може бути негативною", - "minimum-lowercase-letters": "Мінімальна кількість малих літер", - "minimum-lowercase-letters-range": "Мінімальна кількість малих літер не може бути негативною", - "minimum-digits": "Мінімальна кількість цифр", - "minimum-digits-range": "Мінімальна кількість цифр не може бути негативною", - "minimum-special-characters": "Мінімальна кількість спеціальних символів", - "minimum-special-characters-range": "Мінімальна кількість спеціальних символів не може бути негативною", - "password-expiration-period-days": "Термін дії пароля в днях", - "password-expiration-period-days-range": "Термін дії пароля в днях не може бути негативним", - "password-reuse-frequency-days": "Частота повторного використання пароля в днях", - "password-reuse-frequency-days-range": "Частота повторного використання пароля в днях не може бути негативною", - "general-policy": "Загальна політика", - "max-failed-login-attempts": "Максимальна кількість невдалих спроб входу, перш ніж обліковий запис заблоковано", - "minimum-max-failed-login-attempts-range": "Максимальна кількість невдалих спроб входу не може бути негативною", - "user-lockout-notification-email": "У разі блокування облікового запису користувача, надішліть сповіщення на електронну пошту" - }, - "alarm": { - "alarm": "Сигнал тривоги", - "alarms": "Сигнали тривоги", - "select-alarm": "Вибрати сигнал тривоги", - "no-alarms-matching": "Сигналів тривоги '{{entity}}' не знайдено.", - "alarm-required": "Сигнал тривоги необхідний", - "alarm-status": "Статус сигналу тривоги", - "search-status": { - "ANY": "Будь які", - "ACTIVE": "Активні", - "CLEARED": "Неактивні", - "ACK": "Прийняті", - "UNACK": "Неприйняті" - }, - "display-status": { - "ACTIVE_UNACK": "Активні та неприйняті", - "ACTIVE_ACK": "Активні та прийняті", - "CLEARED_UNACK": "Неактивні та неприйняті", - "CLEARED_ACK": "Неактивні та прийняті" - }, - "no-alarms-prompt": "Сигналів тривоги не знайдено", - "created-time": "Час створення", - "type": "Тип", - "severity": "Серйозність", - "originator": "Ініціатор", - "originator-type": "Тип ініціатору", - "details": "Деталі", - "status": "Статус", - "alarm-details": "Деталі сигналу тривоги", - "start-time": "Початок", - "end-time": "Кінець", - "ack-time": "Час прийняття", - "clear-time": "Час деактивації", - "severity-critical": "Критичні", - "severity-major": "Важливі", - "severity-minor": "Неважливі", - "severity-warning": "Попередження", - "severity-indeterminate": "Невизначені", - "acknowledge": "Прийняти", - "clear": "Деактивувати", - "search": "Шукати сигнали тривоги", - "selected-alarms": "{ count, plural, 1 {1 сигнал тривоги} other {# сигнали тривоги} } вибрані", - "no-data": "Немає даних для відображення", - "polling-interval": "Інтервал опитування (сек)", - "polling-interval-required": "Необхідно задати інтервал опитування.", - "min-polling-interval-message": "Дозволяється щонайменше 1 секунда інтервалу очікування.", - "aknowledge-alarms-title": "Підтвердити { count, plural, 1 {1 сигнал тривоги} other {# сигнали тривоги} }", - "aknowledge-alarms-text": "Ви впевнені, що хочете підтвердити { count, plural, 1 {1 сигнал тривоги} other {# сигнали тривоги} }?", - "aknowledge-alarm-title": "Підтвердити сигнал тривоги", - "aknowledge-alarm-text": "Ви впевнені, що хочете підтвердити сигнал тривоги?", - "clear-alarms-title": "Деактивувати { count, plural, 1 {1 сигнал тривоги} other {# сигнали тривоги} }", - "clear-alarms-text": "Ви впевнені, що хочете деактивувати { count, plural, 1 {1 сигнал тривоги} other {# сигнали тривоги} }?", - "clear-alarm-title": "Деактивувати сигнал тривоги", - "clear-alarm-text": "Ви впевнені, що хочете деактивувати сигнал тривоги?", - "alarm-status-filter": "Фільтр статусу сигналу тривоги", - "max-count-load": "Максимальна кількість сигналів тривоги для завантаження (0 - необмежено)", - "max-count-load-required": "Необхідно задати максимальну кількість сигналів тривоги для завантаження.", - "max-count-load-error-min": "Мінімальне значення 0.", - "fetch-size": "Розмір пакету для завантаження", - "fetch-size-required": "Необхідно задати розмір пакету для завантаження.", - "fetch-size-error-min": "Мінімальне значення 10." - }, - "alias": { - "add": "Додати псевдонім ", - "edit": "Редагувати псевдонім", - "name": "Ім'я", - "name-required": "Необхідно вказати псевдонім", - "duplicate-alias": "Псевдонім з такою назвою вже існує.", - "filter-type-single-entity": "Єдина сутність", - "filter-type-entity-group": "Група сутностей", - "filter-type-entity-list": "Список сутностей", - "filter-type-entity-name": "Назва сутності", - "filter-type-entity-group-list": "Список груп сутностей", - "filter-type-entity-group-name": "Назва групи сутностей", - "filter-type-state-entity": "Сутність з стану панелі пристроїв", - "filter-type-state-entity-description": "Сутність, взята з параметрів стану панелі пристроїв", - "filter-type-asset-type": "Тип активу", - "filter-type-asset-type-description": "Тип активів '{{assetType}}'", - "filter-type-asset-type-and-name-description": "Тип активів '{{assetType}}' і ім'я, що починаються з '{{prefix}}'", - "filter-type-device-type": "Тип пристрою", - "filter-type-device-type-description": "Тип пристроїв '{{deviceType}}'", - "filter-type-device-type-and-name-description": "Тип пристроїв '{{deviceType}}' і ім'я, що починаються з '{{prefix}}'", - "filter-type-entity-view-type": "Тип перегляду сутності", - "filter-type-entity-view-type-description": "Перегляд сутності з типом '{{entityView}}'", - "filter-type-entity-view-type-and-name-description": "Перегляд сутності з типом'{{entityView}}' і іменем, що починаються з '{{prefix}}'", - "filter-type-relations-query": "Запит відносин", - "filter-type-relations-query-description": "{{entities}}, які мають {{relationType}} відношення {{direction}} {{rootEntity}}", - "filter-type-asset-search-query": "Запит пошуку активу", - "filter-type-asset-search-query-description": "Активи з типами {{assetTypes}}, які мають {{relationType}} відношення {{direction}} {{rootEntity}}", - "filter-type-device-search-query": "Запит пошуку пристрою", - "filter-type-device-search-query-description": "Пристрої з типами {{deviceTypes}}, які мають {{relationType}} відношення {{direction}} {{rootEntity}}", - "filter-type-entity-view-search-query": "Запит пошуку переглядів сутностей", - "filter-type-entity-view-search-query-description": "Перегляд сутності з типами {{entityViewTypes}}, які мають {{relationType}} відношення {{direction}} {{rootEntity}}", - "entity-filter": "Фільтр сутності", - "resolve-multiple": "Як декілька сутностей", - "filter-type": "Тип фільтра", - "filter-type-required": "Необхідно вказати тип фільтра.", - "entity-filter-no-entity-matched": "Не знайдено жодних сутностей, які відповідають вказаному фільтру.", - "no-entity-filter-specified": "Фільтр обїектів не вказано", - "root-state-entity": "Використовувати сутінсть стану як кореневу", - "last-level-relation": "Використовувати лише відношення останнього рівня", - "group-state-entity": "Використовувати групу сутностей стану як кореневу", - "root-entity": "Коренева сутність", - "state-entity-parameter-name": "Параметр сутності стану", - "default-state-entity": "Сутність стану за замовчуванням", - "default-state-entity-group": "Група сутностей стану за замовчуванням", - "default-entity-parameter-name": "За замовчуванням", - "max-relation-level": "Максимальна глибина відносин", - "unlimited-level": "Необмежена глибина", - "state-entity": "Сутність стану панелі пристроїв", - "entities-of-group-state-entity": "Сутності із групи сутностей стану", - "all-entities": "Всі сутності", - "any-relation": "не вказано" - }, - "asset": { - "asset": "Актив", - "assets": "Активи", - "management": "Управління активами", - "view-assets": "Переглянути активи", - "add": "Додати активи", - "assign-to-customer": "Надати клієнту", - "assign-asset-to-customer": "Надати активи клієнту", - "assign-asset-to-customer-text": "Будь ласка, виберіть ресурси, призначені для клієнта", - "no-assets-text": "Не знайдено активів", - "assign-to-customer-text": "Будь ласка, виберіть клієнта, щоб надати активи", - "public": "Публічно", - "assignedToCustomer": "Наданий клієнту", - "make-public": "Зробити актив(и) публічним(и)", - "make-private": "Зробити актив(и) приватним(и)", - "unassign-from-customer": "Позбавити клієнта", - "delete": "Видалити актив", - "asset-public": "Актив є загальнодоступним", - "asset-type": "Тип активу", - "asset-type-required": "Тип активу обов'язковий.", - "select-asset-type": "Виберіть тип активу", - "enter-asset-type": "Введіть тип активу", - "any-asset": "Будь-який актив", - "no-asset-types-matching": "Не знайдено жодних активів, що відповідають даному типу '{{entitySubtype}}'.", - "asset-type-list-empty": "Не вибрано жодного типу активів.", - "asset-types": "Типи активів", - "name": "Ім'я", - "name-required": "Ім'я обов'язкове.", - "description": "Опис", - "type": "Тип", - "type-required": "Тип обов'язковий.", - "details": "Подробиці", - "events": "Події", - "add-asset-text": "Додати новий актив", - "asset-details": "Інформація про актив", - "assign-assets": "Надати активи", - "assign-assets-text": "Надати { count, plural, 1 {1 актив} other {# активи} } клієнту", - "delete-assets": "Видалити активи", - "unassign-assets": "Позбавити активів", - "unassign-assets-action-title": "Позбавити { count, plural, 1 {1 актив} other {# активи} } клієнта", - "assign-new-asset": "Надати новий актив", - "delete-asset-title": "Ви впевнені, що хочете видалити актив '{{assetName}}'?", - "delete-asset-text": "Будьте обережні, після підтвердження, актив і всі пов'язані з ним дані буде втрачено", - "delete-assets-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 актив} other {# активи} }?", - "delete-assets-action-title": "Видалити{ count, plural, 1 {1 актив} other {# активи} }", - "delete-assets-text": "Будьте обережні, після підтвердження всі вибрані об'єкти буде видалено, і всі пов'язані з ними дані буде втрачено.", - "make-public-asset-title": "Ви дійсно хочете, щоб актив '{{assetName}}' був загальнодоступним?", - "make-public-asset-text": "Після підтвердження, актив і всі його дані будуть доступними для інших.", - "make-private-asset-title": "Ви впевнені, що хочете зробити актив {{assetName}} приватним?", - "make-private-asset-text": "Після підтвердження, актив та всі його дані будуть приватними та не будуть доступні іншим.", - "unassign-asset-title": "Ви впевнені, що хочете позбавити активу '{{assetName}}'?", - "unassign-asset-text": "Після підтвердження клієнт буде позбавлений активу. Дані активу не будуть доступні клієнту.", - "unassign-asset": "Позбавити активу", - "unassign-assets-title": "Ви впевнені, що хочете позбавити активів { count, plural, 1 {1 актив} other {# активи} }?", - "unassign-assets-text": "Після підтвердження, клієнт буде позбавлений усіх вибраних активів. Дані активів не будуть доступні клієнту.", - "copyId": "Копіювати Id активу", - "idCopiedMessage": "Id активу був скопійований у буфер обміну", - "select-asset": "Виберіть актив", - "no-assets-matching": "Не знайдено жодних активів, що відповідають'{{entity}}'.", - "asset-required": "Необхідно задати актив", - "name-starts-with": "Назва активу починається з", - "selected-assets": "{ count, plural, 1 {1 актив} other {# активи} } selected", - "search": "Пошук активів", - "select-group-to-add": "Виберіть цільову групу, щоб додати вибрані активи", - "select-group-to-move": "Виберіть цільову групу для переміщення вибраних активів", - "remove-assets-from-group": "Ви впевнені, що хочете видалити { count, plural, 1 {1 актив} other {# актив} } з групи '{entityGroup}'?", - "group": "Група активів", - "list-of-groups": "{ count, plural, 1 {Одна група активів} other {Список # груп активів} }", - "group-name-starts-with": "Групи активів, чиї імена починаються з '{{prefix}}'", - "import": "Імпортувати активи", - "asset-file": "Файл з активами", - "label": "Мітка" - }, - "attribute": { - "attributes": "Атрибути", - "latest-telemetry": "Остання телеметрія", - "attributes-scope": "Область видимості атрибутів", - "scope-latest-telemetry": "Остання телеметрія", - "scope-client": "Клієнтські атрибути", - "scope-server": "Серверні атрибути", - "scope-shared": "Спільні атрибути", - "add": "Додати атрибут", - "add-attribute-prompt": "Будь ласка, додайте атрибут", - "key": "Ключ", - "last-update-time": "Останнє оновлення", - "key-required": "Ключ атрибута обов'язковий.", - "value": "Значення", - "value-required": "Значення атрибута обов'язкове.", - "delete-attributes-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 attribute} other {# attributes} }?", - "delete-attributes-text": "Будьте обережні, після підтвердження, всі виділені атрибути будуть видалені.", - "delete-attributes": "Видалити атрибути", - "enter-attribute-value": "Введіть значення атрибута", - "show-on-widget": "Показати на віджеті", - "widget-mode": "Режим віджетів", - "next-widget": "Наступний віджет", - "prev-widget": "Попередній віджет", - "add-to-dashboard": "Додати до інформаційної панелі", - "add-widget-to-dashboard": "Додати віджет до інформаційної панелі", - "selected-attributes": "{ count, plural, 1 {1 attribute} other {# attributes} } selected ...вибрані вибрати", - "selected-telemetry": "{ count, plural, 1 {1 telemetry unit} other {# telemetry units} } selected" - }, - "audit-log": { - "audit": "Операція", - "audit-logs": "Журнал операцій", - "timestamp": "Тимчасова позначка", - "entity-type": "Тип одиниці", - "entity-name": "Назва організації", - "user": "Користувач", - "type": "Тип", - "status": "Статус", - "details": "Подробиці", - "type-added": "Додано", - "type-deleted": "Вилучено", - "type-updated": "Оновлено", - "type-attributes-updated": "Атрибути оновлені", - "type-attributes-deleted": "Атрибути видалені", - "type-rpc-call": "RPC дзвінок", - "type-credentials-updated": "Авторизаційні дані оновлено", - "type-assigned-to-customer": "Призначено клієнту", - "type-unassigned-from-customer": "Позбавлено від клієнта", - "type-activated": "Активовано", - "type-suspended": "Призупинено", - "type-credentials-read": "Авторизаційні дані прочитані", - "type-attributes-read": "Атрибути читаються", - "type-added-to-entity-group": "Додано до групи", - "type-removed-from-entity-group": "Вилучено з групи", - "type-relation-add-or-update": "Відношення оновлено", - "type-relation-delete": "Відношення видалено", - "type-relations-delete": "Всі відношення видалено", - "type-alarm-ack": "Визнано", - "type-alarm-clear": "Очищено", - "type-login": "Вхід", - "type-logout": "Вихід", - "type-lockout": "Заблокований", - "type-rest-api-rule-engine-call": "Rule engine REST API call", - "status-success": "Успішно", - "status-failure": "Невдало", - "audit-log-details": "Подробиці журналу операцій", - "no-audit-logs-prompt": "Жодних журналів операцій не знайдено", - "action-data": "Дані про дії", - "failure-details": "Невдалі подробиці", - "search": "Пошук журналів перевірки", - "clear-search": "Очистити пошук" - }, - "confirm-on-exit": { - "message": "У вас є незбережені зміни. Ви впевнені, що хочете залишити цю сторінку?", - "html-message": "У вас є незбережені зміни.
    Ви впевнені, що хочете залишити цю сторінку?", - "title": "Незбережені зміни" - }, - "contact": { - "country": "Країна", - "city": "Місто", - "state": "Штат / Провінція", - "postal-code": "Поштовий індекс", - "postal-code-invalid": "Неправильний формат поштового індексу.", - "address": "Адреса", - "address2": "Адреса 2", - "phone": "Телефон", - "email": "Електронна пошта", - "no-address": "Немає адреси" - }, - "common": { - "username": "Ім'я користувача", - "password": "Пароль", - "enter-username": "Введіть ім'я користувача", - "enter-password": "Введіть пароль", - "enter-search": "Введіть пошук" - }, - "converter": { - "converter": "Перетворювач даних", - "converters": "Перетворювачі даних", - "select-converter": "Виберіть перетворювач даних", - "no-converters-matching": "Не має перетворювачів даних, які відповідають '{{entity}}'.", - "converter-required": "Необхідно вказати перетворювач даних", - "delete": "Видалити перетворювач даних", - "management": "Управління перетворювачами даних", - "add-converter-text": "Додати новий перетворювач даних", - "no-converters-text": "Перетворювачів даних не знайдено", - "selected-converters": "{ count, plural, 1 {1 перетворювач даних} other {# перетворювачі даних} } вибраний", - "delete-converter-title": "Ви впевнені, що хочете видалити перетворювач даних '{{converterName}}'?", - "delete-converter-text": "Будьте обережні, після підтвердження, перетворювач даних та всі пов'язані з ним дані,стануть недоступними).", - "delete-converters-title": "Ви впевнені, що хочете видалити{ count, plural, 1 {1 перетворювач даних} other {# перетворювачі даних} }?", - "delete-converters-action-title": "Видалити { count, plural, 1 {1 перетворювач даних} other {# перетворювачі даних} }", - "delete-converters-text": "Будьте обережні, після підтвердження всі вибрані перетворювачі даних буде видалено, і всі пов'язані з ними дані буде втрачено.", - "events": "Події", - "add": "Додати перетворювач даних", - "converter-details": "Подробиці про перетворювач даних", - "details": "Подробиці", - "copyId": "Копіювати Id перетворювача даних", - "idCopiedMessage": "Id перетворювача даних було скопійовано у буфер обміну", - "debug-mode": "Режим налагодження", - "name": "Ім'я", - "name-required": "Ім'я обов'язкове.", - "description": "Опис", - "decoder": "Декодер", - "encoder": "Кодер", - "test-decoder-fuction": "Тестування функції декодера", - "test-encoder-fuction": "Тестування функції кодера", - "decoder-input-params": "Вхідні параметри декодера", - "encoder-input-params": "Вхідні параметри кодера", - "payload": "Вхідне повідомлення", - "payload-content-type": "Тип контенту вхідного повідомлення", - "payload-content": "Зміст вхідного повідомлення", - "message": "Повідомлення", - "message-type": "Тип повідомлення", - "message-type-required": "Необхідно задати тип повідомлення", - "test": "Тест", - "metadata": "Метадані", - "metadata-required": "Записи метаданих не можуть бути порожніми.", - "integration-metadata": "Метедані інтеграції", - "integration-metadata-required": "Параметри метаданих інтеграції не можуть бути порожніми.", - "output": "Вихідні дані", - "import": "Імпорт перетворювача даних", - "export": "Експорт перетворювача даних", - "export-failed-error": "Неможливо експортувати перетворювач даних: {{помилка}}", - "create-new-converter": "Створити новий перетворювач даних", - "converter-file": "Файл перетворювача даних(конвектер файл)", - "invalid-converter-file-error": "Неможливо імпортувати перетворювач даних: недійсна структура даних перетворювача.", - "type": "Тип", - "type-required": "Необхідно задати тип.", - "type-uplink": "Від пристрою", - "type-downlink": "До пристрою" - }, - "content-type": { - "json": "Json", - "text": "Текст", - "binary": "Бінарний (Base64)" - }, - "customer": { - "customer": "Клієнт", - "customers": "Клієнти", - "management": "Клієнтський менеджмент", - "dashboard": "Інформаційна панель клієнта", - "dashboards": "Інформаційні панелі клієнта", - "devices": "Пристрої клієнта", - "entity-views": "Представлення сутностей", - "assets": "Клієнтські активи", - "public-dashboards": "Публічні інформаційні панелі", - "public-devices": "Публічні пристрої", - "public-assets": "Публічні активи", - "public-entity-views": "Публічне представлення сутностей 440", - "add": "Додати клієнта", - "delete": "Видалити клієнта", - "manage-customer-users": "Керування користувачами клієнта", - "manage-customer-devices": "Керування пристроями клієнта", - "manage-customer-dashboards": "Керування інформаційними панелями клієнта", - "manage-public-devices": "Керувати загальнодоступними пристроями", - "manage-public-dashboards": "Керування загальнодоступними інформаційними панелями", - "manage-customer-assets": "Керування активами клієнта", - "manage-public-assets": "Керування загальнодоступними активами", - "add-customer-text": "Додати нового клієнта", - "no-customers-text": "Клієнтів не знайдено", - "customer-details": "Інформація про клієнта", - "delete-customer-title": "Ви впевнені, що хочете видалити клієнта '{{customerTitle}}'?", - "delete-customer-text": "Будьте обережні, після підтвердження, клієнт та всі пов'язані з ним дані, стануть недоступними.", - "delete-customers-title": "Ви впевнені, що хочете видалити {count, plural, 1 {1 клієнт}, інші {# клієнти}}?", - "delete-customers-action-title": "Видалити{ count, plural, 1 {1 клієнт} other {# клієнти} }", - "delete-customers-text": "Будьте обережні, після підтвердження, всі вибрані клієнти будуть видалені і всі пов'язані з ними дані, стануть недоступними.", - "manage-users": "Керування користувачами", - "manage-assets": "Керування активами", - "manage-devices": "Керування пристроями", - "manage-dashboards": "Керування інформаційними панелями", - "title": "Назва", - "title-required": "Необхідно задати назву.", - "description": "Опис", - "details": "Подробиці", - "events": "Події", - "copyId": "Копіювати Id клієнта", - "idCopiedMessage": "Id клієнта було скопійовано в буфер обміну", - "select-customer": "Виберіть клієнта", - "no-customers-matching": "Клієнтів, які відповідають '{{entity}}' не знайдено.", - "customer-required": "Необхідно задати клієнта", - "selected-customers": "{ count, plural, 1 {1 клієнт} інші {# клієнти} } вибрано", - "search": "Пошук клієнтів", - "select-group-to-add": "Виберіть цільову групу, щоб додати вибраних клієнтів", - "select-group-to-move": "Виберіть цільову групу для переміщення вибраних клієнтів", - "remove-customers-from-group": "Ви впевнені, що хочете видалити{ count, plural, 1 {1 клієнт} other {# клієнти} } з групи'{entityGroup}'?", - "group": "Група клієнтів", - "list-of-groups": "{ count, plural, 1 {Одна група клієнтів} other {Список # груп клієнтів} }", - "group-name-starts-with": "Групи клієнтів, імена яких починаються з '{{prefix}}'", - "select-default-customer": "Вибрати клієнта за замовчуванням", - "default-customer": "Клієнт за замовчуванням", - "default-customer-required": "Необхідно вказати клієнта за замовчуванням для налагодження панелі візуалізації на рівні замовника", - "allow-white-labeling": "Дозволити брендування" - }, - "custom-translation": { - "custom-translation": "Переклад для користувача", - "translation-map": "Карта перекладу", - "key": "Ключ перекладу", - "import": "Імпорт перекладу", - "export": "Експорт перекладу", - "export-data": "Дані про експорт перекладу", - "import-data": "Дані про імпорт перекладу", - "translation-file": "Файл перекладу", - "invalid-translation-file-error": "Неможливо імпортувати файл перекладу: недійсна структура даних перекладу.", - "custom-translation-hint": "Визначте індивідуальний переклад в JSON нижче. Цей JSON перезапише переклад за замовчуванням. Натисніть 'Завантажити файл перекладу', щоб отримати існуючий переклад. Ви також можете скористатись завантаженим файлом як посиланням на наявні пари параметрів перекладу ключ-значення.", - "download-locale-file": "Завантажити файл перекладу" - }, - "datetime": { - "date-from": "Дата від", - "time-from": "Час від", - "date-to": "Дата до", - "time-to": "Час до" - }, - "dashboard": { - "dashboard": "Панель приладів", - "dashboards": "Панелі приладів", - "management": "Управління панеллю приладів", - "view-dashboards": "Переглянути панелі приладів", - "add": "Додати панель приладів", - "assign-dashboard-to-customer": "Призначити панель(і) приладів замовнику", - "assign-dashboard-to-customer-text": "Будь ласка, виберіть панелі пристроїв, щоб призначити їх клієнту", - "assign-to-customer-text": "Виберіть клієнта, щоб призначити панелі пристроїв", - "assign-to-customer": "Призначити клієнту", - "unassign-from-customer": "Позбавити клієнта", - "make-public": "Зробити панель приладів публічною", - "make-private": "Зробити панель приладів приватною", - "manage-assigned-customers": "Керування призначеними клієнтами", - "assigned-customers": "Призначені клієнтам", - "assign-to-customers": "Призначити панелі приладів клієнтам", - "assign-to-customers-text": "Виберіть клієнтів для призначення панелей приладів", - "unassign-from-customers": "Позбавити клієнтів призначенних панелей приладів", - "unassign-from-customers-text": "Виберіть клієнтів для позбавлення їх призначених панелей приладів", - "no-dashboards-text": "Панелі приладів не знайдені", - "no-widgets": "Не налаштовано жодних віджетів", - "add-widget": "Додати новий віджет", - "title": "Назва", - "select-widget-title": "Вибрати віджет", - "select-widget-subtitle": "Список доступних типів віджетів", - "delete": "Видалити панель приладів", - "title-required": "Необхідно задати назву.", - "description": "Опис", - "details": "подробиці", - "dashboard-details": "Подробиці панелі приладів", - "add-dashboard-text": "Додати нову панель приладів", - "assign-dashboards": "Призначити панель приладів", - "assign-new-dashboard": "Призначити нову панель приладів", - "assign-dashboards-text": "Призначити { count, plural, 1 {1 панель приладів} other {# панелі приладів} } користувачам", - "unassign-dashboards-action-text": "Позбавити { count, plural, 1 {1 палелі приладів} other {# панелей приладів} } клієнтів", - "delete-dashboards": "Видалити панель приладів", - "unassign-dashboards": "Позбавити панелей приладів", - "unassign-dashboards-action-title": "Позбавити { count, plural, 1 {1 палелі приладів} other {# панелей приладів} } клієнтів", - "delete-dashboard-title": "Ви впевнені, що хочете видалити панель приладів '{{назва панелі приладів}}'?", - "delete-dashboard-text": "Будьте обережні, після підтвердження, панель приладів і всі пов'язані з нею дані стануть недоступними.", - "delete-dashboards-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 панель приладів} other {# панелі приладів} }?", - "delete-dashboards-action-title": "Видалити { count, plural, 1 {1 панель приладів} other {# панелі приладів} }", - "delete-dashboards-text": "Будьте обережні, після підтвердження, всі вибрані панелі приладів буде видалено, і всі пов'язані з ними дані стануть недоступними.", - "unassign-dashboard-title": "Ви впевнені, що хочете позбавити панелі приладів '{{назва інформаційної панелі}}'?", - "unassign-dashboard-text": "Після підтвердження, клієнт буде позбавлений панелі приладів. Панель приладів і пов'язані з нею дані будуть недоступні клієнтові.", - "unassign-dashboard": "Позбавити панелі приладів", - "unassign-dashboards-title": "Ви впевнені, що хочете позбавити { count, plural, 1 {1 панелі приладів} other {# панелей приладів} }?", - "unassign-dashboards-text": "Після підтвердження, клієтн буде позбавлений усіх вибраних панелей приладів і даних, які з ними пов'язані.", - "public-dashboard-title": "Панель приладів тепер публічна", - "public-dashboard-text": "Ваша панель приладів {{dashboardTitle}} тепер публічна і доступна іншим link:", - "public-dashboard-notice": "Note: Не забудьте зробити спільні пристрої загальнодоступними, щоб отримати доступ до їхніх даних.", - "make-private-dashboard-title": "Ви впевнені, що хочете зробити панель приладів '{{назва панелі приладів}}' приватною?", - "make-private-dashboard-text": "Після підтвердження панель приладів стане приватною і не буде доступною іншим.", - "make-private-dashboard": "Зробити панель приладів приватною", - "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard", - "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard", - "select-dashboard": "Вибрати панель приладів", - "no-dashboards-matching": "Не знайдено жодних панелей прилодів'{{entity}}' які відповідають.", - "dashboard-required": "Необхідно задати панель приладів.", - "select-existing": "Виберіть існуючу панель приладів", - "create-new": "Створити нову панель приладів", - "new-dashboard-title": "Нова назва панелі приладів", - "open-dashboard": "Відрити панель приладів", - "set-background": "Встановити фон", - "background-color": "Колір фону", - "background-image": "Фонове зображення", - "background-size-mode": "Режим фонового розміру", - "no-image": "Не вибрано жодного зображення", - "drop-image": "Перетягніть зображення або клацніть, щоб вибрати файл для завантаження.", - "settings": "Налаштування", - "columns-count": "Кількість стовпців", - "columns-count-required": "Необхідно вказати кількість стовпців.", - "min-columns-count-message": "Дозволений мінімум -10 стовпців.", - "max-columns-count-message": "Дозволений максимум - 1000 стовпців.", - "widgets-margins": "Відступ між віджетами", - "horizontal-margin": "Горизонтальний відступ", - "horizontal-margin-required": "Необхідно вказати горизонтальний відступ.", - "min-horizontal-margin-message": "Допустиме мінімальне значення горизонтального відступу - 0.", - "max-horizontal-margin-message": "Допустиме максимальне значення горизонтального відступу - 50.", - "vertical-margin": "Вертикальний відступ", - "vertical-margin-required": "Необхідно вказати вертикальний відступ.", - "min-vertical-margin-message": "Допустиме мінімальне значення вертикального відступу - 0.", - "max-vertical-margin-message": "Допустиме максимальне значення вертикального відступу - 50.", - "autofill-height": "Висота автоматичного заповнення макета", - "mobile-layout": "Налаштування макета для мобільних пристроїв", - "mobile-row-height": "Висота рядка для мобільних пристроїв, px", - "mobile-row-height-required": "Потрібно вказати значення висоти рядка для мобільних пристроїв.", - "min-mobile-row-height-message": "Допустиме мінімальне значення висоти рядка для мобільних пристроїв - 5 пікселів.", - "max-mobile-row-height-message": "Допустиме максимальне значення висоти рядка для мобільних пристроїв - 200 пікселів.", - "display-title": "Відображати назву панелі візуалізації", - "toolbar-always-open": "Тримайте панель візуалізації відкритою", - "title-color": "Колір назви", - "display-dashboards-selection": "Відображення вибору панелей візуалізації", - "display-entities-selection": "Вибір відображення сутності", - "display-dashboard-timewindow": "Відобразити налаштування часового проміжку", - "display-dashboard-export": "Відображення експорту", - "import": "Імпортувати панель візуалізації", - "export": "Експортувати панель візуалізації", - "export-failed-error": "Неможливо експортувати панель візуалізації: {{error}}", - "export-pdf": "Експортувати як PDF", - "export-png": "Експортувати як PNG", - "export-jpg": "Експортувати як JPEG", - "export-json-config": "Експортувати конфігурацію JSON", - "download-dashboard-progress": "Генерування панелі візуалізації {{reportType}} ...", - "create-new-dashboard": "Створити нову панель візуалізації", - "dashboard-file": "Файл панелі візуалізації", - "invalid-dashboard-file-error": "Неможливо імпортувати панель візуалізації: неправильна структура даних панелі візуалізації.", - "dashboard-import-missing-aliases-title": "Configure aliases used by imported dashboard Налаштування псевдонімів, що використовуються імпортованою панеллю візуалізації", - "create-new-widget": "Створити новий віджет", - "import-widget": "Імпортувати віджет", - "widget-file": "Файл віджета", - "invalid-widget-file-error": "Неможливо імпортувати віджет: неправильна структура даних віджета.", - "widget-import-missing-aliases-title": "Налаштувати псевдоніми, що використовуються імпортованим віджетом", - "open-toolbar": "Відкрити панель інструменів ", - "close-toolbar": "Закрити панель інструменів", - "configuration-error": "Помилка конфігурації", - "alias-resolution-error-title": "Помилка конфігурації псевдонімів панелі візуалізації", - "invalid-aliases-config": "Неможливо знайти пристрої, які відповідають певному фільтру псевдонімів.
    Зверніться до свого адміністратора, щоб вирішити цю проблему.", - "select-devices": "Вибрати пристрої", - "assignedToCustomer": "Призначений клієнту", - "assignedToCustomers": "Призначений клієнтам", - "public": "Публічно", - "public-link": "Публічне посилання", - "copy-public-link": "Копіювати публічне посилання", - "public-link-copied-message": "Публічне посилання було скопійоване в буфер обміну панелі візуалізації", - "manage-states": "Керування станами панелі візуалізації", - "states": "Стани панелі візуалізації", - "search-states": "Пошук станів панелі візуалізації", - "selected-states": "{ count, plural, 1 {1 dashboard state} other {# dashboard states} } вибрано", - "edit-state": "Редагувати стан панелі візуалізації", - "delete-state": "Видалити стан панелі візуалізації ", - "add-state": "Додати стан панелі візуалізації", - "state": "Стан панелі візуалізації", - "state-name": "Ім'я", - "state-name-required": "Необхідно вказати назву стану панелі візуалізації.", - "state-id": "Стан Id", - "state-id-required": "Необхідно вказати id стану панелі візуалізації.", - "state-id-exists": "Стан інформаційної панелі з таким id вже існує.", - "is-root-state": "Основний стан", - "delete-state-title": "Видалити стан панелі візуалізації", - "delete-state-text": "Ви впевнені, що хочете видалити стан панелі візуалізації з іменем '{{stateName}}'?", - "show-details": "Показати деталі", - "hide-details": "Приховати деталі", - "select-state": "Виберіть цільовий стан", - "state-controller": "Контроллер стану" - }, - "datakey": { - "settings": "Налаштування", - "advanced": "Додатково", - "label": "Мітка", - "color": "Колір", - "units": "Спеціальний символ, який показує наступне значення", - "decimals": "Кількість цифр після плаваючої точки", - "data-generation-func": "Функція генерації даних", - "use-data-post-processing-func": "Використовувати функцію пост-обробки даних", - "configuration": "Конфігурація ключа даних", - "timeseries": "Телеметрія", - "attributes": "Атрибути", - "entity-field": "Поле сутності", - "alarm": "Поля сигнала тривоги", - "timeseries-required": "Необхідно вказати Телеметрія.", - "timeseries-or-attributes-required": "Необхідно вказати телеметрію/атрибути.", - "maximum-timeseries-or-attributes": "Максимальні { count, plural, 1 {1 телеметрія/атрибут дозволені.} other {# телеметрія/атрибути дозволені} }", - "alarm-fields-required": "Необхідно вказати поля сигнала тривоги.", - "function-types": "Типи функцій", - "function-types-required": "Необхідно вказати типи функцій.", - "maximum-function-types": "Maximum { count, plural, 1 {1 function type is allowed.} other {# function types are allowed} }", - "time-description": "мітка часу поточного значення;", - "value-description": "поточне значення;", - "prev-value-description": "результат попереднього виклику функції;", - "time-prev-description": "мітка часу попереднього значення;", - "prev-orig-value-description": "оригінальне попереднє значення;" - }, - "datasource": { - "type": "Тип джерела даних", - "name": "Ім'я", - "add-datasource-prompt": "Додайте джерело даних" - }, - "details": { - "details": "Деталі", - "edit-mode": "Режим редагування", - "edit-json": "Редагувати JSON", - "toggle-edit-mode": "Перемкнути режим редагування" - }, - "device": { - "device": "Пристрій", - "device-required": "Необхідно задати пристрій.", - "devices": "Пристрої", - "management": "Управління пристроєм", - "view-devices": "Перегляд пристроїв", - "device-alias": "Псевдонім пристрою", - "aliases": "Псевдонім пристроїв", - "no-alias-matching": "'{{alias}}' не знайдено.", - "no-aliases-found": "Псевдонімів не знайдено.", - "no-key-matching": "'{{key}}' не знайдено.", - "no-keys-found": "Ключі не знайдено.", - "create-new-alias": "Створити новий!", - "create-new-key": "Створити новий!", - "duplicate-alias-error": "Псевдонім з таким іменем '{{alias}}' вже існює.
    Псевдоніми пристроїв повинні бути унікальними на панелі візуалізації.", - "configure-alias": "Налаштувати псевдонім '{{alias}}'", - "no-devices-matching": "Не знайдено жодних пристроїв, які відповідають '{{entity}}'.", - "alias": "Псевдонім", - "alias-required": "Необхідно задати псевдонім пристрою.", - "remove-alias": "Видалити псевдонім пристрою", - "add-alias": "Додати псевдонім пристрою", - "name-starts-with": "Ім'я пристрою починається з", - "device-list": "Список пристроїв", - "use-device-name-filter": "Використати фільтр", - "device-list-empty": "Не вибрано жодного пристрою.", - "device-name-filter-required": "Необхідно задати назву фільтра пристрою.", - "device-name-filter-no-device-matched": "Не знайдено жодних пристроїв, що починаються з '{{device}}'.", - "add": "Додати пристрій", - "assign-to-customer": "Призначити клієнту", - "assign-device-to-customer": "Призначити пристрій (ої) клієнту", - "assign-device-to-customer-text": "Виберіть пристрої, які слід призначити клієнту", - "make-public": "Зробити пристрій публічним", - "make-private": "Зробити пристрій приватним", - "no-devices-text": "Не знайдено жодного пристрою", - "assign-to-customer-text": "Виберіть клієнта для призначення пристрою (їв)", - "device-details": "Деталі пристрою", - "add-device-text": "Додати новий пристрій", - "credentials": "Авторизаційні дані", - "manage-credentials": "Керування авторизаційними даними", - "delete": "Видалити пристрій", - "assign-devices": "Призначити пристрої", - "assign-devices-text": "Призначити { count, plural, 1 {1 пристрій} other {# пристрої} } клієнту", - "delete-devices": "Видалити пристрої", - "unassign-from-customer": "Позбавити клієнта пристроїв", - "unassign-devices": "Позбавити пристроїв", - "unassign-devices-action-title": "Позбавити клієнта { count, plural, 1 {1 пристрою} other {# пристроїв} }", - "assign-new-device": "Призначити новий пристрій", - "make-public-device-title": "Ви впевнені, що хочете зробити пристрій '{{deviceName}}' публічним?", - "make-public-device-text": "Після підтвердження пристрій і всі його дані будуть публічними та доступними для інших.", - "make-private-device-title": "Ви впевнені, що хочете зробити пристрій '{{deviceName}}' приватним?", - "make-private-device-text": "Після підтвердження пристрій і всі його дані будуть приватними та недоступними для інших.", - "view-credentials": "Переглянути авторизаційні дані", - "delete-device-title": "Ви впевнені, що хочете видалити пристрій '{{deviceName}}'?", - "delete-device-text": "Будьте обережні, після підтвердження, пристрій і всі пов'язані з ним дані стануть недоступними.", - "delete-devices-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 пристрій} other {# пристрої} }?", - "delete-devices-action-title": "Видалити { count, plural, 1 {1 пристрій} other {# пристрої} }", - "delete-devices-text": "Будьте обережні, після підтвердження, всі вибрані пристрої будуть видалені, і всі пов'язані з ними дані стануть недоступними.", - "unassign-device-title": "Ви впевнені, що хочете позбавити пристрою '{{deviceName}}'?", - "unassign-device-text": "Після підтвердження, клієнт буде позбавлений пристрою.", - "unassign-device": "Позбавити пристою", - "unassign-devices-title": "Ви впевненені, що хочете позбавити { count, plural, 1 {1 пристрою} other {# пристроїв} }?", - "unassign-devices-text": "Після підтвердження, клієнт буде позбавлений пристрою і пристрій стане не доступним клієнту", - "device-credentials": "Авторизаційні дані прстрою", - "credentials-type": "Тип авторизаційних даних", - "access-token": "Маркер доступу", - "access-token-required": "Необхідно вказати маркер доступу.", - "access-token-invalid": "Маркер доступу має бути від 1 до 20 символів.", - "rsa-key": "Публічний ключ RSA", - "rsa-key-required": "Необхідно вказати публічний ключ RSA.", - "secret": "Секрет", - "secret-required": "Необхідно вказати секрет.", - "device-type": "Тип пристрою", - "device-type-required": "Необхідно вказати тип пристрою.", - "select-device-type": "Виберіть тип пристрою", - "enter-device-type": "Введіть тип пристрою", - "any-device": "Будь-який пристрій", - "no-device-types-matching": "Не знайдено типів пристроїв, які відповідають '{{entitySubtype}}'.", - "device-type-list-empty": "Не вибрано типів пристроїв.", - "device-types": "Типи пристрою", - "name": "Ім'я", - "name-required": "Необхідно вказати ім'я.", - "description": "Опис", - "events": "Події", - "details": "Деталі", - "copyId": "Копіювати Id пристрою", - "copyAccessToken": "Копіювати маркер доступу", - "idCopiedMessage": "Id пристрою скопійовано в буфер обміну", - "accessTokenCopiedMessage": "Маркер доступу до пристрою скопійовано в буфер обміну", - "assignedToCustomer": "Призначений клієнту", - "unable-delete-device-alias-title": "Неможливо видалити псевдонім пристрою", - "unable-delete-device-alias-text": "Псевдонім пристрою '{{deviceAlias}}' не може бути видалений, оскільки він використовується наступним(и) віджетом(ами):
    {{widgetsList}}", - "is-gateway": "Шлюз", - "public": "Публічно", - "device-public": "Пристрій є публічним", - "select-device": "Виберіть пристрій", - "import": "Імпортувати пристрої", - "device-file": "Файл з пристроями", - "selected-devices": "{ count, plural, 1 {1 пристрій} other {# пристрої} } вибрано", - "search": "Шукати пристрої", - "select-group-to-add": "Виберіть цільову групу, щоб додати вибраний пристрій", - "select-group-to-move": "Виберіть цільову групу для переміщення вибраних пристроїв", - "remove-devices-from-group": "Ви впевнені, що хочете видалити { count, plural, 1 {1 пристрій} other {# пристрої} } з групи '{entityGroup}'?", - "group": "Група пристроїв", - "list-of-groups": "{ count, plural, 1 {Одна група пристроїв} other {Список # груп пристроїв} }", - "group-name-starts-with": "Групи пристроїв, імена яких починаються з '{{prefix}}'" - }, - "dialog": { - "close": "Закрити діалогове вікно" - }, - "direction": { - "column": "Колонка", - "row": "Рядок" - }, - "error": { - "unable-to-connect": "Неможливо підключитися до сервера! Перевірте підключення до Інтернету.", - "unhandled-error-code": "Неопрацьований помилковий код: {{errorCode}}", - "unknown-error": "Невідома помилка" - }, - "entity": { - "entity": "Сутність", - "entities": "Сутності", - "aliases": "Псевдоніми сутності", - "entity-alias": "Псевдонім сутності", - "unable-delete-entity-alias-title": "Неможливо видалити псевдонім сутності", - "unable-delete-entity-alias-text": "Псевдонім сутності'{{entityAlias}}' неможливо видалити, так як це використовується наступним віджетом(s):
    {{widgetsList}}", - "duplicate-alias-error": "Знайдено повторюваний псевдонім '{{alias}}'.
    Псевдоніми сутностей повинні бути унікальними на панелі візуалізації.", - "missing-entity-filter-error": "Відсутній фільтр для псевдоніма '{{alias}}'.", - "configure-alias": "Налаштувати '{{alias}}' псевдонім", - "alias": "Псевдонім", - "alias-required": "Необхідно вказати псевдонім сутності.", - "remove-alias": "Видалити псевдонім сутності", - "add-alias": "Додати псевдонім сутності", - "entity-list": "Список сутності", - "entity-type": "Тип сутності", - "entity-types": "Типи сутності", - "entity-type-list": "Список типу сутності", - "any-entity": "Будь-яка сутність", - "enter-entity-type": "Введіть тип сутності", - "no-entities-matching": "Не знайдено жожних сутностей, що відповідають '{{entity}}' що відповідають.", - "no-entity-types-matching": "Не знайдено жожних типів сутностей, що відповідають '{{entityType}}'.", - "name-starts-with": "Назва починається з", - "use-entity-name-filter": "Використовуйте фільтр", - "entity-list-empty": "Не вибрано жодних сутностей.", - "entity-type-list-empty": "Не вибрано жодних типів сутностей.", - "entity-name-filter-required": "Необхідно задати фільтр по імені.", - "entity-name-filter-no-entity-matched": "Не знайдено жодних сутностей, що починаються з '{{entity}}'.", - "all-subtypes": "Всі", - "select-entities": "Виберіть сутність", - "no-aliases-found": "Псевдонімів не знайдено.", - "no-alias-matching": "'{{alias}}' не знайдено.", - "create-new-alias": "Створити новий псевдонім!", - "key": "Ключ", - "key-name": "Ім'я ключа", - "no-keys-found": "No keys found.", - "no-key-matching": "'{{key}}' не знайдено.", - "create-new-key": "Створити новий ключ!", - "type": "Тип", - "type-required": "Необхідно задати тип сутності.", - "type-device": "Пристрій", - "type-devices": "Пристрої", - "list-of-devices": "{ count, plural, 1 {Один пристрій} other {Список # пристроїв} }", - "device-name-starts-with": "Пристрої, імена яких починаються з '{{prefix}}'", - "type-asset": "Актив", - "type-assets": "Активи", - "list-of-assets": "{ count, plural, 1 {Один актив} other {Список # активів} }", - "asset-name-starts-with": "Активи, імена яких починаються з '{{prefix}}'", - "type-entity-view": "Перегляд сутності", - "type-entity-views": "Перегляди сутності", - "list-of-entity-views": "{ count, plural, 1 {Один перегляд сутності} other {Список # переглядів сутності} }", - "entity-view-name-starts-with": "Перегляди сутностей, імена яких починаються з '{{prefix}}'", - "type-rule": "Правило", - "type-rules": "Правила", - "list-of-rules": "{ count, plural, 1 {Одне правило} other {Список # правил} }", - "rule-name-starts-with": "Правила, імена яких починаються з '{{prefix}}'", - "type-plugin": "Плагін", - "type-plugins": "Плагіни", - "list-of-plugins": "{ count, plural, 1 {Один плагін} other {Список # плагінів} }", - "plugin-name-starts-with": "Плагіни, імена яких починаються з '{{prefix}}'", - "type-tenant": "Власник", - "type-tenants": "Власники", - "list-of-tenants": "{ count, plural, 1 {Один власник} other {Список # власників} }", - "tenant-name-starts-with": "Власники, імена яких починаються з '{{prefix}}'", - "type-customer": "Клієнт", - "type-customers": "Клієнти", - "list-of-customers": "{ count, plural, 1 {Один клієнт} other {Список # клієнтів} }", - "customer-name-starts-with": "Клієнти, імена яких починаються з '{{prefix}}'", - "type-user": "Користувач", - "type-users": "Користувачі", - "list-of-users": "{ count, plural, 1 {Один користувач} other {Список # користувачів } }", - "user-name-starts-with": "Користувачі, імена яких починаються з '{{prefix}}'", - "type-dashboard": "Панель візуалізації", - "type-dashboards": "Панелі візуалізації", - "list-of-dashboards": "{ count, plural, 1 {Одна панель візуалізації} other {Список # панелей візуалізації} }", - "dashboard-name-starts-with": "Панелі візуалізації, імена яких починаються з '{{prefix}}'", - "type-alarm": "Сигнал тривоги", - "type-alarms": "Сигнали тривоги", - "list-of-alarms": "{ count, plural, 1 {Один сигнал тривоги} other {Список # сигналів тривоги} }", - "alarm-name-starts-with": "Сигнали тривоги, імена яких починаються '{{prefix}}'", - "type-rulechain": "Ланцюжок правил", - "type-rulechains": "Ланцюжки правил", - "list-of-rulechains": "{ count, plural, 1 {Один ланцюжок правил} other {Список # ланцюжків правил} }", - "rulechain-name-starts-with": "Правило ланцюжків, імена яких починаються '{{prefix}}'", - "type-scheduler-event": "Scheduler event", - "type-scheduler-events": "Scheduler events", - "list-of-scheduler-events": "{ count, plural, 1 {One scheduler event} other {List of # scheduler events} }", - "scheduler-event-name-starts-with": "Scheduler events whose names start with '{{prefix}}'", - "type-blob-entity": "Blob entity", - "type-blob-entities": "Blob entities", - "list-of-blob-entities": "{ count, plural, 1 {One blob entity} other {List of # blob entities} }", - "blob-entity-name-starts-with": "Blob entities whose names start with '{{prefix}}'", - "type-rulenode": "Правило", - "type-rulenodes": "Правила", - "list-of-rulenodes": "{ count, plural, 1 {Одне правило} other {Список # правил} }", - "rulenode-name-starts-with": "Список правил, імена яких починаються '{{prefix}}'", - "type-current-customer": "Поточний клієнт", - "type-current-tenant": "Поточний власник", - "search": "Пошук сутностей", - "selected-entities": "{ count, plural, 1 {1 сутність} other {# сутності} } вибрано", - "entity-name": "Ім'я сутності", - "entity-label": "Мітка сутності", - "details": "Подробиці сутності", - "no-entities-prompt": "Сутності не знайдено", - "no-data": "Немає даних для відображення", - "columns-to-display": "Стовпці для відображення", - "type-entity-group": "Група сутностей", - "type-converter": "Перетворювач даних", - "type-converters": "Перетворювачі даних", - "list-of-converters": "{ count, plural, 1 {Однин перетворювач даних} other {Список # перетворювачів даних} }", - "converter-name-starts-with": "Перетворювачі даних, імена яких починаються з '{{prefix}}'", - "type-integration": "Інтеграція", - "type-integrations": "Інтеграції", - "list-of-integrations": "{ count, plural, 1 {Одна інтеграція} other {Список # інтеграцій} }", - "integration-name-starts-with": "Інтеграції, імена яких починаються з '{{prefix}}'" - }, - "entity-field": { - "created-time": "Час створення", - "name": "Ім'я", - "type": "Тип", - "first-name": "Ім'я", - "last-name": "Прізвище", - "email": "Електронна пошта", - "title": "Назва", - "country": "Країна", - "state": "Штат", - "city": "Місто", - "address": "Адреса", - "address2": "Адреса 2", - "zip": "Zip", - "phone": "Телефон", - "label": "Мітка" - }, - "entity-group": { - "entity-group": "Група сутності", - "details": "Деталі", - "columns": "Стовпці", - "add-column": "Додати стовпець", - "column-value": "Значення", - "column-value-required": "Необхідно вказати значення.", - "column-title": "Назва", - "default-sort-order": "Основний порядок сортування", - "default-sort-order-required": "Необхідно вказати основний порядок сортування.", - "hide-in-mobile-view": "Мобільний приховано", - "use-cell-style-function": "Використовувати функцію стилю комірки", - "use-cell-content-function": "Use cell content function", - "edit-column": "Редагувати стовпець", - "column-details": "Деталі стовпця", - "actions": "Дії", - "settings": "Налаштування", - "delete": "Видалити групу сутностей", - "name": "Ім'я", - "name-required": "Необхідно вказати ім'я.", - "description": "Опис", - "add": "Додати групу сутностей", - "add-entity-group-text": "Додати нову групу сутностей", - "no-entity-groups-text": "Не знайдено жодних груп сутності", - "entity-group-details": "Деталі групи сутності", - "delete-entity-groups": "Видалити групи сутностей", - "delete-entity-group-title": "Ви впевнені, що хочете видалити групу сутності '{{entityGroupName}}'?", - "delete-entity-group-text": "Будьте обережні, після підтвердження, група сутностей і всі пов'язані з нею дані стануть недоступними.", - "delete-entity-groups-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 групу сутності} other {# групи сутностей} }?", - "delete-entity-groups-action-title": "Видалити { count, plural, 1 {1 групу сутності} other {# групи сутностей} }", - "delete-entity-groups-text": "Будьте обережні, після підтвердження, всі виділені групи сутностей і пов'язані з ними дані, стануть недоступними.", - "device-groups": "Групи пристроїв", - "asset-groups": "Групи активів", - "customer-groups": "Групи клієнтів", - "device-group": "Група пристроїв", - "asset-group": "Група активів", - "customer-group": "Група клієнтів", - "fetch-more": "Отримати більше", - "column-type": { - "column-type": "Тип стовпця", - "client-attribute": "Атрибут клієнта", - "shared-attribute": "Спільний атрибут", - "server-attribute": "Атрибут сервера", - "timeseries": "Телеметрія", - "entity-field": "Поле сутності" - }, - "column-type-required": "Необхідно вказати тип стовпця.", - "entity-field": { - "created-time": "Час створення", - "name": "Ім'я", - "type": "Тип", - "assigned_customer": "Призначений клієнт", - "authority": "Авторитет", - "first_name": "Ім'я", - "last_name": "Прізвище", - "email": "Електронна пошта", - "title": "Назва", - "country": "Країна", - "state": "Штат", - "city": "Місто", - "address": "Адреса", - "address2": "Адреса 2", - "zip": "Zip", - "phone": "Телефон" - }, - "sort-order": { - "asc": "У порядку зростання", - "desc": "У порядку зменшення", - "none": "Немає" - }, - "details-mode": { - "on-row-click": "Клацніть на рядок", - "on-action-button-click": "Клацніть на кнопку детелі", - "disabled": "Вимкнено" - }, - "add-to-group": "Додати до групи", - "move-to-group": "Перемістити до групи", - "select-entity-group": "Виберіть групу сутностей", - "no-entity-groups-matching": "Не знайдено жодних груп сутностей, що відповідають '{{entityGroup}}'.", - "target-entity-group-required": "Необхідно вказати цільову групу сутності.", - "remove-from-group": "Видалити з групи", - "group-table-title": "Group table title", - "enable-search": "Увімкнути пошук сутностей", - "enable-add": "Увімкнути додавання сутностей", - "enable-delete": "Увімкнути видалення сутностей", - "enable-selection": "Увімкнути вибір сутностей", - "enable-group-transfer": "Увімкнути дії групового перенесення", - "display-pagination": "Відображення сторінок", - "default-page-size": "Розмір сторінки за замовчуванням", - "enable-assignment-actions": "Увімкнути дії призначення", - "enable-credentials-management": "Увімкнути керування авторизаційними даними", - "enable-users-management": "Увімкнути керування користувачами", - "enable-assets-management": "Увімкнути керування активами", - "enable-devices-management": "Увімкнути керування пристроями", - "enable-dashboards-management": "Увімкнути керування панелями візуалізації", - "open-details-on": "Відкрити деталі сутності по", - "select-existing": "Виберіть існуючу групу сутностей", - "create-new": "Створити нову групу сутностей", - "new-entity-group-name": "Нове ім'я групи сутностей", - "entity-group-list": "Список групи сутностей", - "entity-group-list-empty": "Не вибрано жодної групи сутностей.", - "name-starts-with": "Назва групи сутностей починається з", - "entity-group-name-filter-required": "Необхідно задати назву групи сутностей." - }, - "entity-view": { - "entity-view": "Представлення сутності", - "entity-view-required": "Необхідно вказати представлення сутності.", - "entity-views": "Представлення сутностей", - "management": "Керування представленням сутностей", - "view-entity-views": "Переглянути представлення сутностей", - "entity-view-alias": "Псевдонім представлення сутності", - "aliases": "Псевдоніми представлення сутності", - "no-alias-matching": "Псевдонім'{{alias}}' не знайдено.", - "no-aliases-found": "Псевдоніми не знайдено.", - "no-key-matching": "'Ключ {{key}}' не знайдено.", - "no-keys-found": "Ключі не знайдено.", - "create-new-alias": "Створити новий!", - "create-new-key": "Створити новий!", - "duplicate-alias-error": "Псевдонім з такою назвою вже існує '{{alias}}'.
    Псевдоніми представлення повинні бути унікальними на панелі візуалізації.", - "configure-alias": "Налаштувати псевдонім '{{alias}}'", - "no-entity-views-matching": "Сутності, які відповідають '{{entity}}' не знайдені.", - "alias": "Псевдонім", - "alias-required": "Необхідно вказати псевдонім представлення сутності.", - "remove-alias": "Видалити псевдонім представлення сутності", - "add-alias": "Додати псевдонім представлення сутності", - "name-starts-with": "Ім'я представлення сутності починається з", - "entity-view-list": "Список представленнь сутності", - "use-entity-view-name-filter": "Використати фільтр", - "entity-view-list-empty": "Не вибрано жодного представлення сутності.", - "entity-view-name-filter-required": "Необхідно вказвти фільтр назв представлення сутності.", - "entity-view-name-filter-no-entity-view-matched": "Представлення сутностей, назви яких починаються з '{{entityView}}' не знайдено.", - "add": "Додати представлення сутності", - "assign-to-customer": "Призначити клієнту", - "assign-entity-view-to-customer": "Призначити представлення сутності(ей) клієнту", - "assign-entity-view-to-customer-text": "Будь ласка, виберіть представлення сутності для призначення клієнту", - "no-entity-views-text": "Представлення сутності не знайдено", - "assign-to-customer-text": "Будь ласка, виберіть клієнта, для призначиення представлення(ь) сутності(ей)", - "entity-view-details": "Деталі представлення сутності", - "add-entity-view-text": "Додати нове представлення сутності", - "delete": "Видалити представлення сутності", - "assign-entity-views": "Призначити представлення сутності", - "assign-entity-views-text": "Призначити { count, plural, 1 {1 представлення сутності} other {# представлення сутностей } } клієнту", - "delete-entity-views": "Видалити представлення сутностей", - "unassign-from-customer": "Відкликати у клієнта", - "unassign-entity-views": "Відкликати представлення сутностей", - "unassign-entity-views-action-title": "Відкликати { count, plural, 1 {1 представлення сутності} other {# представлень сутностей} } у клієнта", - "assign-new-entity-view": "Призначити нове представлення сутності", - "delete-entity-view-title": "Ви впевнені, що хочете видалити представлення сутності'{{entityViewName}}'?", - "delete-entity-view-text": "Будьте обережні, після підтвердження, представлення сутності та всі пов'язані з ним дані стануть недоступними.", - "delete-entity-views-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 представлення сутності } other {# представлення сутностей } }?", - "delete-entity-views-action-title": "Видалити { count, plural, 1 {1 представлення сутності } other {# представлення сутностей } }", - "delete-entity-views-text": "Будьте обережні, після підтвердження, всі виділені представлення сутностей та дні, пов'язані з ними стануть недоступними.", - "unassign-entity-view-title": "Ви впевнені, що хочете відкликати представлення сутності '{{entityViewName}}'?", - "unassign-entity-view-text": "Після підтвердження представлення сутності буде відкликане у клієнта. Дані представлення сутності не будуть доступні клієнту.", - "unassign-entity-view": "Відкликати представлення сутності", - "unassign-entity-views-title": "Ви впевнені, що хочете відкликати { count, plural, 1 {1 представлення сутності} other {# представлень сутностей} }?", - "unassign-entity-views-text": "Після підтвердження, клієнта буде позбавлено всіх виділених представлень сутностей. Дані представлень сутностей не будуть доступні клієнту.", - "entity-view-type": "Тип представлення сутності", - "entity-view-type-required": "Необхідно вказати тип представлення сутності.", - "select-entity-view-type": "Виберіть тип представлення сутності", - "enter-entity-view-type": "Введіть тип представлення сутності", - "any-entity-view": "Будь-яке представлення сутності", - "no-entity-view-types-matching": "Не знайдено жодних типів представлення сутності, що відповідають '{{entitySubtype}}'.", - "entity-view-type-list-empty": "Не вибрано тип представлення сутності.", - "entity-view-types": "Типи представлення сутності", - "name": "Ім'я", - "name-required": "Необхідно вказати ім'я.", - "description": "Опис", - "events": "Події", - "details": "Деталі", - "copyId": "Скопіювати Id представлення сутності", - "assignedToCustomer": "Призначений клієнту", - "unable-entity-view-device-alias-title": "Неможливо видалити псевдонім представлення сутності", - "unable-entity-view-device-alias-text": "Не вдалося видалити псевдонім пристрою'{{entityViewAlias}}', так як він використовується наступним(и) віджетом(ами):
    {{widgetsList}}", - "select-entity-view": "Вибрати представлення сутності", - "make-public": "Зробити представлення сутності публічним", - "make-private": "Зробити представлення сутності приватним", - "start-date": "Дата початку", - "start-ts": "Час початку", - "end-date": "Дата закінчення", - "end-ts": "Час завершення", - "date-limits": "Обмеження дати", - "client-attributes": "Атрибути клієнта", - "shared-attributes": "Спільні атрибути", - "server-attributes": "Атрибути сервера", - "timeseries": "Телеметрія", - "client-attributes-placeholder": "Атрибути клієнта", - "shared-attributes-placeholder": "Спільні атрибути", - "server-attributes-placeholder": "Атрибути сервера", - "timeseries-placeholder": "Телеметрія", - "target-entity": "Цільова сутність", - "attributes-propagation": "Поширення атрибутів", - "attributes-propagation-hint": "Представлення сутності автоматично копіюватиме вказані атрибути з цільової сутності кожного разу, коли ви зберігаєте або оновлюєте його. В цілях продуктивності, атрибути цільової сутності не поширюються на представлення сутності при кожній зміні її атрибутів. Можна ввімкнути автоматичне поширення, налаштувавши правило \"copy to view\" у вашому ланцюжку правил і пов'язуючи його з повідомленнями типу \"Post attributes\" і \"Attributes Updated\"..", - "timeseries-data": "Дані телеметрії", - "timeseries-data-hint": "Налаштуйте ключі даних телеметрії цільової сутності, які будуть доступні представленню сутності. Ці дані доступні лише для читання.", - "make-public-entity-view-title": "Ви впевнені, що бажаєте зробити представлення сутності '{{entityViewName}}' публічним?", - "make-public-entity-view-text": "Після підтвердження представлення сутності і всі пов'язані з ним дані стануть публічними і будуть доступні для інших користувачів.", - "make-private-entity-view-title": "Ви впевнені, що бажаєте зробити представлення сутності '{{entityViewName}}' приватним?", - "make-private-entity-view-text": "Після підтвердження представлення сутності і всі пов'язані з ним дані стануть приватними і не будуть доступні для інших користувачів." - }, - "event": { - "events": "Події", - "event-type": "Тип події", - "type-error": "Помилка", - "type-lc-event": "Подія життєвого циклу", - "type-stats": "Статистика", - "type-debug-converter": "Налагоджувати", - "type-debug-integration": "Налагоджувати", - "type-debug-rule-node": "Налагоджувати", - "type-debug-rule-chain": "Налагоджувати", - "no-events-prompt": "Не знайдено жодних подій", - "error": "Помилка", - "alarm": "Сигнал тривоги", - "event-time": "Час події", - "server": "Сервер", - "body": "Тіло", - "method": "Метод", - "type": "Тип", - "in": "In", - "out": "Out", - "metadata": "Метадані", - "message": "Повідомлення", - "entity": "Сутність", - "message-id": "Id повідомлення", - "message-type": "Тип повідомлення", - "data-type": "Тип даних", - "relation-type": "Тип зв'язку", - "data": "Дані", - "event": "Подія", - "status": "Статус", - "success": "Успіх", - "failed": "Невдача", - "messages-processed": "Повідомлення опрацьовані", - "errors-occurred": "Виникли помилки" - }, - "extension": { - "extensions": "Розширення", - "selected-extensions": " вибрано { count, plural, 1 {1 розширення} other {# розширення} }", - "type": "Тип", - "key": "Ключ", - "value": "Значення", - "id": "Id", - "extension-id": "Id розширення", - "extension-type": "Тип розширення", - "transformer-json": "JSON *", - "unique-id-required": "Таке Id розширення вже існує.", - "delete": "Видалити розширення", - "add": "Додати розширення", - "edit": "Редагувати розширення", - "delete-extension-title": "Ви дійсно бажаєте видалити розширення '{{extensionId}}'?", - "delete-extension-text": "Будьте обережні, після підтвердження, розширення та всі пов'язані з ним дані стануть недоступними.", - "delete-extensions-title": "Ви дійсно бажаєте видалити { count, plural, 1 {1 розширення} other {# розширення} }?", - "delete-extensions-text": "Будьте обережні, після підтвердження, всі вибрані розширення будуть видалені.", - "converters": "Перетворювачі", - "converter-id": "Id перетворювача", - "configuration": "Конфігурація", - "converter-configurations": "Конфігурації перетворювача", - "token": "Маркер безпеки", - "add-converter": "Додати конвертер", - "add-config": "Додати конфігурацію конвертера", - "device-name-expression": "Маска імені пристрою", - "device-type-expression": "Маска типу пристрою", - "custom": "Користувач", - "to-double": "Подвоїти", - "transformer": "Трансформатор", - "json-required": "Необхідно вказати json трансформатора.", - "json-parse": "Неможливо проаналізувати json трансформатора.", - "attributes": "Атрибути", - "add-attribute": "Додати атрибут", - "add-map": "Додати елемент відображення", - "timeseries": "Телеметрія", - "add-timeseries": "Додати параметри телеметрії", - "field-required": "Field is required", - "brokers": "Брокери", - "add-broker": "Додати брокера", - "host": "Хост", - "port": "Порт", - "port-range": "Значення порту має бути в діапазоні від 1 до 65535.", - "ssl": "Ssl", - "credentials": "Авторизаційні дані", - "username": "Ім'я користувача", - "password": "Пароль", - "retry-interval": "Інтервал повтору в мілісекундах", - "anonymous": "Анонімний", - "basic": "Основний", - "pem": "PEM", - "ca-cert": "Файл CA сертифіката *", - "private-key": "Файл приватного ключа *", - "cert": "Файл сертифіката *", - "no-file": "Не вибрано жодного файлу.", - "drop-file": "Перетягніть файл, або клацніть, щоб вибрати файл для завантаження.", - "mapping": "Зіставлення", - "topic-filter": "Фільтр тем", - "converter-type": "Тип конвертера", - "converter-json": "Json", - "json-name-expression": " Json вираз для назви пристрою", - "topic-name-expression": "Вираз для назви пристрою в назві теми", - "json-type-expression": "Json вираз для типу пристрою", - "topic-type-expression": "Вираз для типу пристрою в назві теми", - "attribute-key-expression": "Вираз для ключа атрибута", - "attr-json-key-expression": " Json вираз для ключа атрибута", - "attr-topic-key-expression": "Вираз для ключа атрибута в назві теми", - "request-id-expression": "Вираз для id запиту", - "request-id-json-expression": "Json вираз для id запиту", - "request-id-topic-expression": "Вираз для id запиту в назві теми", - "response-topic-expression": "Вираз для теми відповідей", - "value-expression": "Вираз для значення", - "topic": "Тема", - "timeout": "Час очікування в мілісекундах", - "converter-json-required": "Необхідно вказати json конвертер.", - "converter-json-parse": "Неможливо проаналізувати json конвертера.", - "filter-expression": "Вираз для фільтра", - "connect-requests": "Запити на підключення", - "add-connect-request": "Додати запит на підключення", - "disconnect-requests": "Відключення запитів", - "add-disconnect-request": "Додати запит на відключення", - "attribute-requests": "Запити атрибутів", - "add-attribute-request": "Додати запит атрибута", - "attribute-updates": "Оновлення атрибутів", - "add-attribute-update": "Додати оновлення атрибутів", - "server-side-rpc": "Серверна сторона RPC", - "add-server-side-rpc-request": "Додати RPC-запит на стороні сервера", - "device-name-filter": "Фільтр назви пристрою", - "attribute-filter": "Фільтр атрибутів", - "method-filter": "Фільтр методів", - "request-topic-expression": "Вираз для теми запитів", - "response-timeout": "Час очікування відповіді в мілісекундах", - "topic-expression": "Вираз для назви теми", - "client-scope": "Обсяг клієнта", - "add-device": "Додати пристрій", - "opc-server": "Сервери", - "opc-add-server": "Додати сервер", - "opc-add-server-prompt": "Будь ласка, додайте сервер", - "opc-application-name": "Назва програми", - "opc-application-uri": "URI програми", - "opc-scan-period-in-seconds": "Період сканування в секундах", - "opc-security": "Безпека", - "opc-identity": "Ідентифікація", - "opc-keystore": "Сховище ключів", - "opc-type": "Тип", - "opc-keystore-type": "Тип", - "opc-keystore-location": "Розташування *", - "opc-keystore-password": "Пароль", - "opc-keystore-alias": "Псевдонім", - "opc-keystore-key-password": "Пароль для ключа", - "opc-device-node-pattern": "Патерн OPC вузла пристрою", - "opc-device-name-pattern": "Патерн назви пристрою", - "modbus-server": "Сервери/ведені пристрої", - "modbus-add-server": "Додати сервер/ведений пристрій", - "modbus-add-server-prompt": "Будь ласка, додайте сервер/ведений пристрій", - "modbus-transport": "Транспорт", - "modbus-tcp-reconnect": "Перепідключатися автоматично", - "modbus-port-name": "Ім'я послідовного порту", - "modbus-encoding": "Кодування", - "modbus-parity": "Паритет", - "modbus-baudrate": "Швидкість передачі даних", - "modbus-databits": "Біти даних", - "modbus-stopbits": "Стоп-біти", - "modbus-databits-range": "Біти даних повинні знаходитися в діапазоні від 7 до 8.", - "modbus-stopbits-range": "Стоп-біти повинні знаходитися в діапазоні від 1 до 2.", - "modbus-unit-id": "Unit ID", - "modbus-unit-id-range": "Unit ID should be in a range from 1 to 247.", - "modbus-device-name": "Ім'я пристрою", - "modbus-poll-period": "Період опитування (мс)", - "modbus-attributes-poll-period": "Період опитування атрибутів (мс)", - "modbus-timeseries-poll-period": "Період опитування телеметрії (мс)", - "modbus-poll-period-range": "Період опитування повинен бути більше 0.", - "modbus-tag": "Тег", - "modbus-function": "Modbus функція", - "modbus-register-address": "Адреса регістру ", - "modbus-register-address-range": "Адреса регістру повинна бути в діапазоні від 0 до 65535.", - "modbus-register-bit-index": "Номер бітів", - "modbus-register-bit-index-range": "Номер бітів повинен знаходитися в діапазоні від 0 до 15.", - "modbus-register-count": "Рахунок регістру", - "modbus-register-count-range": "Рахунок регістру повинен бути більше 0.", - "modbus-byte-order": "Порядок байтів", - "sync": { - "status": "Статус", - "sync": "Синхронізований", - "not-sync": "Не синхронізований", - "last-sync-time": "Час останньої синхронізації", - "not-available": "Недоступний" - }, - "export-extensions-configuration": "Експортувати конфігурацію розширень", - "import-extensions-configuration": "Імпортувати конфігурацію розширень", - "import-extensions": "Імпортувати розширення", - "import-extension": "Імпортувати розширення", - "export-extension": "Експортувати розширення", - "file": "Файл розширень", - "invalid-file-error": "Не правильний формат файла" - }, - "fullscreen": { - "expand": "Відкрити у повноекранному режимі", - "exit": "Вийти з повноекранного режиму", - "toggle": "Перемкнути повноекранний режим", - "fullscreen": "Повноекранний режим" - }, - "function": { - "function": "Функція" - }, - "grid": { - "delete-item-title": "Ви впенені, що хочете видалити цей елемент?", - "delete-item-text": "Будьте обережні, після підтвердження, цей елемент і всі пов'язані з ним дані, стануть недоступними.", - "delete-items-title": "Ви впенені, що хочете видалити { count, plural, 1 {1 елемент} other {# елементи} }?", - "delete-items-action-title": "Видалити{ count, plural, 1 {1 елемент} other {# елементи} }", - "delete-items-text": "Будьте обережні, після підтвердження, всі виділені елементи і пов'язані з ними дані, стануть недоступними.", - "add-item-text": "Додати новий елемент", - "no-items-text": "Не знайдено жодного елемента", - "item-details": "Деталі елемента", - "delete-item": "Видалити елемент", - "delete-items": "Видалити елементи", - "scroll-to-top": "Перейти угору" - }, - "help": { - "goto-help-page": "Перейти на сторінку довідки" - }, - "home": { - "home": "Домашня сторінка", - "profile": "Профіль", - "logout": "Вийти", - "menu": "Меню", - "avatar": "Аватар", - "open-user-menu": "Відкрити меню користувача" - }, - "import": { - "no-file": "Не вибрано жодного файлу", - "drop-file": "Перетягніть JSON файл, або клацніть, щоб вибрати файл для завантаження.", - "drop-csv-file": "Перетягніть CSV файл, або клацніть, щоб вибрати файл для завантаження.", - "column-value": "Значення", - "column-title": "Назва", - "column-example": "Приклад значень даних", - "column-key": "Ключ атрибута/телеметрії", - "csv-delimiter": "Розділювач в CSV файлі", - "csv-first-line-header": "Перший рядок містить назви колонок", - "csv-update-data": "Оновити атрибути/телеметрію", - "import-csv-number-columns-error": "Файл має містити як мінімум дві колонки", - "import-csv-invalid-format-error": "Невірний формат даних. Рядок: '{{line}}'", - "column-type": { - "name": "Назва", - "type": "Тип", - "label": "Мітка", - "column-type": "Тип колонки", - "client-attribute": "Атрибут клієнта", - "shared-attribute": "Спільний атрибут", - "server-attribute": "Атрибут сервера", - "timeseries": "Телеметрія", - "entity-field": "Entity field", - "access-token": "Токен" - }, - "stepper-text": { - "select-file": "Виберіть файл", - "configuration": "Конфігурація імпорту", - "column-type": "Виберіть тип колонок", - "creat-entities": "Створення нових сутностей", - "done": "Завершено" - }, - "message": { - "create-entities": "{{count}} нову(их) сутність(ей) успішно створено.", - "update-entities": "{{count}} сутність(ей) успішно оновлено.", - "error-entities": "Виникла помилка при створенні {{count}} сутності(ей)." - } - }, - "integration": { - "integration": "Інтеграція", - "integrations": "Інтеграції", - "select-integration": "Виберіть інтеграцію", - "no-integrations-matching": "Не знайдено жодних інтеграцій, які відповідають '{{entity}}'.", - "integration-required": "Необхідно вказати інтеграцію", - "delete": "Видалити інтеграцію", - "management": "Управління інтеграціями", - "add-integration-text": "Додати нову інтеграцію", - "no-integrations-text": "Не знайдено жодної інтеграції", - "selected-integrations": "{ count, plural, 1 {1 інтеграція} other {# інтеграції} } вибрано", - "delete-integration-title": "Ви впевнені, що хочете видалити інтеграцію '{{integrationName}}'?", - "delete-integration-text": "Будьте обережні, після підтвердження інтеграція та всі пов'язані з нею дані стануть недоступними.", - "delete-integrations-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 інтеграцію} other {# інтеграції} }?", - "delete-integrations-action-title": "Видалити { count, plural, 1 {1 інтеграцію} other {# інтеграції} }", - "delete-integrations-text": "Будьте обережні, після підтвердження всі вибрані інтеграції будуть видалені, і всі пов'язані з ними дані стануть недоступними.", - "events": "Події", - "add": "Додати інтеграцію", - "integration-details": "Деталі інтеграції", - "details": "Деталі", - "copyId": "Копіювати Id інтеграції", - "idCopiedMessage": "Id інтеграції скопійовано в буфер обміну", - "debug-mode": "Режим налагодження", - "enable-security": "Увімкнути безпеку", - "headers-filter": "Заголовки фільтра", - "header": "Заголовок", - "no-headers-filter": "Немає фільтрів заголовків", - "downlink-url": "Downlink URL", - "application-uri": "URI програми", - "as-id": "AS ID", - "as-id-required": "Необхідно вказати AS ID.", - "as-key": "AS ключ", - "as-key-required": "Необхідно вказати AS ключ.", - "max-time-diff-in-seconds": "Максимальна різниця в часі (секунди)", - "max-time-diff-in-seconds-required": "Необхідно вказати максимальну різницю в часі.", - "name": "Ім'я", - "name-required": "Необхідно вказати ім'я.", - "description": "Опис", - "base-url": "Базова URL-адреса", - "base-url-required": "Необхідно вказати базову URL-адресу", - "security-key": "Ключ захисту", - "http-endpoint": "URL кінцевої точки HTTP", - "copy-http-endpoint-url": "Скопіювати URL-адресу кінцевої точки HTTP", - "http-endpoint-url-copied-message": "URL кінцевої точки HTTP скопійовано в буфер обміну", - "host": "Хост", - "host-required": "Необхідно вказати хост.", - "host-type": "Тип хоста", - "host-type-required": "Необхідно вказати тип хоста.", - "custom-host": "Хост користувача", - "custom-host-required": "Необхідний спеціальний хост.", - "port": "Порт", - "port-required": "Необхідно вказати порт.", - "port-range": "Порт має бути в діапазоні від 1 до 65535.", - "connect-timeout": "Час очікування з'єднання (сек)", - "connect-timeout-required": " Необхідно вказати час з'єднання підключення.", - "connect-timeout-range": "Час очікування з'єднання має бути в діапазоні від 1 до 200.", - "client-id": "ID клієнта", - "clean-session": "Очистити сеанс", - "enable-ssl": "Увімкнути SSL", - "credentials": "Авторизаційні дані", - "credentials-type": "Тип авторизаційних даних", - "credentials-type-required": "Необхідно вказати тип авторизаційних даних.", - "username": "Ім'я користувача", - "username-required": "Необхідно вказати ім'я користувача.", - "password": "Пароль", - "password-required": "Необхідно вказати пароль.", - "ca-cert": "Файл сертифіката CA *", - "private-key": "Файл приватного ключа *", - "private-key-password": "Пароль приватного ключа", - "cert": "Файл сертифіката*", - "no-file": "Не вибрано жодного файлу.", - "drop-file": "Перетягніть файл, або клацніть, щоб вибрати файл для завантаження.", - "topic-filters": "Тематичні фільтри", - "remove-topic-filter": "Видалити фільтр тем", - "add-topic-filter": "Додати фільтр тем", - "add-topic-filter-prompt": "Будь ласка, додайте фільтр тем", - "topic": "Тема", - "mqtt-qos": "QoS", - "mqtt-qos-at-most-once": "Не більше одного разу", - "mqtt-qos-at-least-once": "Принаймні, один раз", - "mqtt-qos-exactly-once": "Точно один раз", - "downlink-topic-pattern": "Downlink topic pattern", - "downlink-topic-pattern-required": "Downlink topic pattern is required.", - "aws-iot-endpoint": "AWS IoT Endpoint", - "aws-iot-endpoint-required": "AWS IoT Endpoint is required.", - "aws-iot-credentials": "Авторизаційні дані AWS IoT", - "application-credentials": "Авторизаційні дані додатків", - "api-key": "API ключ", - "api-key-required": "Необхідно вказати API ключ.", - "auth-token": "Маркер аутентифікації", - "auth-token-required": "Необхідно вказати маркер аутентифікації.", - "region": "Регіон", - "region-required": "Необхідно вказати регіон.", - "application-id": "ID програми", - "application-id-required": "Необхідно вказати ID програми.", - "access-key": "Ключ доступу", - "access-key-required": "Необхідно вказати ключ доступу.", - "connection-parameters": "Параметри підключення", - "service-bus-namespace-name": "Service Bus Namespace Name", - "service-bus-namespace-name-required": "Необхідно вказати Service Bus Namespace Name is required.", - "event-hub-name": "Event Hub Name", - "event-hub-name-required": "Необхідно вказати Event Hub Name is required.", - "sas-key-name": "Назва ключа SAS", - "sas-key-name-required": "Необхідно вказати назву ключа SAS.", - "sas-key": "Ключ SAS", - "sas-key-required": "SAS Key is required.", - "iot-hub-name": "IoT Hub Name (required for downlink)", - "metadata": "Метадані", - "type": "Тип", - "type-required": "Необхідно вказати тип.", - "uplink-converter": "Конвертер передачі даних", - "uplink-converter-required": "Необхідно вказати конвертер передачі даних.", - "downlink-converter": "Downlink data converter", - "type-http": "HTTP", - "type-ocean-connect": "OceanConnect", - "type-sigfox": "SigFox", - "type-thingpark": "ThingPark", - "type-tmobile-iot-cdp": "T-Mobile – IoT CDP", - "type-mqtt": "MQTT", - "type-aws-iot": "AWS IoT", - "type-ibm-watson-iot": "IBM Watson IoT", - "type-ttn": "TheThingsNetwork", - "type-azure-event-hub": "Azure Event Hub", - "type-opc-ua": "OPC-UA", - "type-ffb": "FFB", - "opc-ua-application-name": "Назва програми", - "opc-ua-application-uri": "Application uri", - "opc-ua-scan-period-in-seconds": "Період сканування в секундах", - "opc-ua-scan-period-in-seconds-required": "Необхідно вказати період сканування в секундах", - "opc-ua-timeout": "Час очікування в мілісекундах", - "opc-ua-timeout-required": "Необхідно вказати час очікування в мілісекундах", - "opc-ua-security": "Безпека", - "opc-ua-security-required": "Необхідно задати безпеку", - "opc-ua-identity": "Ідентифікація", - "opc-ua-identity-required": "Необхідно вказати ідентифікацію", - "opc-ua-keystore": "Сховище ключів", - "add-opc-ua-keystore-prompt": "Будь ласка, додайте файл сховища ключів", - "opc-ua-keystore-required": "Необхідно вказати сховище ключів", - "opc-ua-type": "Тип", - "opc-ua-keystore-type": "Тип сховища ключів", - "opc-ua-keystore-type-required": "Необхідно вказати тип", - "opc-ua-keystore-location": "Розташування *", - "opc-ua-keystore-password": "Пароль", - "opc-ua-keystore-password-required": "Необхідно вказати пароль", - "opc-ua-keystore-alias": "Псевдонім", - "opc-ua-keystore-alias-required": "Необхідно вказати псевдонім", - "opc-ua-keystore-key-password": "Пароль ключа", - "opc-ua-keystore-key-password-required": "Необхідно вказати пароль ключа", - "opc-ua-mapping": "Зіставлення", - "add-opc-ua-mapping-prompt": "Будь ласка, додайте зіставлення", - "opc-ua-mapping-type": "Тип зіставлення", - "opc-ua-mapping-type-required": "Необхідно вказати тип зіставлення", - "opc-ua-device-node-pattern": "Шаблон вузла пристрою", - "opc-ua-device-node-pattern-required": "Необхідно вказати шаблон вузла пристрою", - "opc-ua-add-map": "Додати елемент зіставлення", - "subscription-tags": "Теги передплати Теги підписки", - "remove-subscription-tag": "Видалити тег підписки", - "add-subscription-tag": "Додати тег підписки", - "add-subscription-tag-prompt": "Будь ласка, додайте тег підписки", - "key": "Ключ", - "path": "Шлях", - "required": "Необхідно" - }, - "item": { - "selected": "Вибрані" - }, - "js-func": { - "no-return-error": "Функція повинна повертати значення!", - "return-type-mismatch": "Функція повинна повернути значення типу '{{type}}'!" - }, - "key-val": { - "key": "Ключ", - "value": "Значення", - "remove-entry": "Видалити елемент", - "add-entry": "Додати елемент", - "no-data": "Елементи відсутні" - }, - "layout": { - "layout": "Макет", - "manage": "Керування макетами", - "settings": "Налаштування макета", - "color": "Колір", - "main": "Основний", - "right": "Правий", - "select": "Вибрати макет" - }, - "legend": { - "direction": "Розташування елементів легенди", - "position": "Розташування легенди", - "show-max": "Показати максимальне значення", - "show-min": "Показати мінімальне значення ", - "show-avg": "Показати середнє значення", - "show-total": "Показати суму", - "settings": "Налаштування легенди", - "min": "мін", - "max": "макс", - "avg": "середнє", - "total": "Сума", - "comparison-time-ago": { - "days": "(день тому)", - "weeks": "(тиждень тому)", - "months": "(місяць тому)", - "years": "(рік тому)" - } - }, - "login": { - "login": "Увійти", - "request-password-reset": "Запит скидання пароля", - "reset-password": "Скинути пароль", - "create-password": "Створити пароль", - "passwords-mismatch-error": "Введені паролі повинні бути однаковими!", - "password-again": "Введіть пароль ще раз", - "sign-in": "Будь ласка, увійдіть в систему", - "username": "Ім'я користувача (ел. пошта)", - "remember-me": "Запам'ятати мене", - "forgot-password": "Забули пароль?", - "password-reset": "Скидання пароля", - "expired-password-reset-message": "Термін дії Вашого паролю закінчився! Будь ласка, створіть новий пароль.", - "new-password": "Новий пароль", - "new-password-again": "Повторіть новий пароль", - "password-link-sent-message": "Посилання для скидання пароля було успішно надіслано!", - "email": "Електронна пошта", - "login-with": "Увійти через {{name}}", - "or": "або" - }, - "position": { - "top": "Угорі", - "bottom": "Знизу", - "left": "Ліворуч", - "right": "Праворуч" - }, - "profile": { - "profile": "Профіль", - "last-login-time": "Час останнього входу", - "change-password": "Змінити пароль", - "current-password": "Поточний пароль" - }, - "relation": { - "relations": "Відношення", - "direction": "Напрямок", - "search-direction": { - "FROM": "З", - "TO": "До" - }, - "direction-type": { - "FROM": "з", - "TO": "до" - }, - "from-relations": "Вихідні відношення", - "to-relations": "Вхідні відношення", - "selected-relations": "Вибрано { count, plural, 1 {1 відношення} other {# відношення} }", - "type": "Тип", - "to-entity-type": "До типу сутності", - "to-entity-name": "До імені сутності", - "from-entity-type": "Від типу сутності", - "from-entity-name": "Від імені сутності", - "to-entity": "До сутності", - "from-entity": "Від сутності", - "delete": "Видалити відношення", - "relation-type": "Тип відношення", - "relation-type-required": "Необхідно вказати тип відношення.", - "any-relation-type": "Будь-який тип", - "add": "Додати відношення", - "edit": "Редагувати відношення", - "delete-to-relation-title": "Ви впевнені, що хочете видалити відношення до сутності '{{entityName}}'?", - "delete-to-relation-text": "Будьте обережні, після підтвердження, сутність '{{entityName}}' не буде пов'язана з поточним об'єктом.", - "delete-to-relations-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 відношення} other {# відношення} }?", - "delete-to-relations-text": "Будьте обережні, після підтвердження, всі вибрані відношення стануть не пов'язані з поточною сутністю.", - "delete-from-relation-title": "Ви впевнені, що хочете видалити зв'язок, який йде від сутності '{{entityName}}'?", - "delete-from-relation-text": "Будьте обережні, після підтвердження поточна сутність буде відв'язана від сутності '{{entityName}}'.", - "delete-from-relations-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 відношення} other {# відношення} }?", - "delete-from-relations-text": "Будьте обережні, після підтвердження, всі вибрані відношення будуть видалені, а поточна сутність стане не зв'язаною з відповідними сутностями.", - "remove-relation-filter": "Видалити фільтр відношення", - "add-relation-filter": "Додати фільтр відношення", - "any-relation": "Будь-яке відношення", - "relation-filters": "Фільтри відношення", - "additional-info": "Додаткова інформація (JSON)", - "invalid-additional-info": "Не вдалося розібрати JSON з додатковою інформацією ." - }, - "rulechain": { - "rulechain": "Ланцюг правил", - "rulechains": "Ланцюги правил", - "root": "Основний", - "delete": "Видалити ланцюг правил", - "name": "Ім'я", - "name-required": "Необхідно вказати ім'я.", - "description": "Опис", - "add": "Додати ланцюг правил", - "set-root": "Зробити ланцюг правил основним", - "set-root-rulechain-title": "Ви впевнені, що хочете зробити ланцюг правил '{{ruleChainName}}' основним?", - "set-root-rulechain-text": "Після підтвердження ланцюг правил стане основним і буде обробляти всі вхідні транспортні повідомлення.", - "delete-rulechain-title": "Ви впевнені, що хочете видалити ланцюг правил '{{ruleChainName}}'?", - "delete-rulechain-text": "Будьте обережні, після підтвердження ланцюг правил і всі пов'язані з ним дані стануть недоступними.", - "delete-rulechains-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 ланцюг правил} other {# ланцюги правил} }?", - "delete-rulechains-action-title": "Видалити{ count, plural, 1 {1 ланцюг правил} other {# ланцюги правил} }", - "delete-rulechains-text": "Будьте обережні, після підтвердження, вибрані ланцюги правил і всі пов'язані з ними дані стануть недоступними.", - "add-rulechain-text": "Додати новий ланцюг правил", - "no-rulechains-text": "Ланцюг правил не знайдено", - "rulechain-details": "Деталі ланцюга правил", - "details": "Деталі", - "events": "Події", - "system": "Система", - "import": "Імпортувати ланцюг правил", - "export": "Експортувати ланцюг правил", - "export-failed-error": "Не вдалося експортувати ланцюг правил: {{error}}", - "create-new-rulechain": "Створити новий ланцюг правил", - "rulechain-file": "Файл ланцюга правил", - "invalid-rulechain-file-error": "Неможливо імпортувати ланцюг правил: недійсна структуру даних ланцюга правил.", - "copyId": "Копіювати Id ланцюга правил", - "idCopiedMessage": "Id ланцюга правил скопійовано в буфер обміну", - "select-rulechain": "Вибрати ланцюг правил", - "no-rulechains-matching": "Не знайдено жодних ланцюгів правил, які відповідають '{{entity}}'.", - "rulechain-required": "Необхідно вказати ланцюг правил", - "management": "Управління ланцюгами правил", - "debug-mode": "Режим налагодження" - }, - "rulenode": { - "details": "Деталі", - "events": "Події", - "search": "Пошук вузлів", - "open-node-library": "Відкрити бібліотеку вузлів", - "add": "Додати вузол правил", - "name": "Ім'я", - "name-required": "Необхідно вказати ім'я.", - "type": "Тип", - "description": "Опис", - "delete": "Видалити вузол правил", - "select-all-objects": "Вибрати усі вузли та з'єднання", - "deselect-all-objects": "Зняти виділення з усіх вузлів і з'єднань", - "delete-selected-objects": "Видалити вибрані вузли та з'єднання", - "delete-selected": "Видалити вибране", - "select-all": "Вибрати все", - "copy-selected": "Копіювати вибране", - "deselect-all": "Відмінити вибране", - "rulenode-details": "Деталі вузла правил", - "debug-mode": "Режим налагодження", - "configuration": "Конфігурація", - "link": "Посилання", - "link-details": "Деталі посилання про вузол правил", - "add-link": "Додати посилання", - "link-label": "Мітка посилання", - "link-label-required": "Необхідно вказати мітку посилання.", - "custom-link-label": "Мітка посилання користувача", - "custom-link-label-required": "Необхідно вказати мітку посилання користувача.", - "link-labels": "Мітки посилання", - "link-labels-required": "Необхідно вказати мітки посилання.", - "no-link-labels-found": "Не знайдено жодних міток посилання", - "no-link-label-matching": "Мітка'{{label}}' не знайдена.", - "create-new-link-label": "Створити нову!", - "type-filter": "Фільтр", - "type-filter-details": "Фільтрувати вхідні повідомлення з заданими умовами", - "type-enrichment": "Насичення", - "type-enrichment-details": "Додати додаткову інформацію до метаданих повідомлень", - "type-transformation": "Трансформація", - "type-transformation-details": "Змінити склад повідомлення та його метадані", - "type-action": "Дія", - "type-action-details": "Виконати задану дію", - "type-analytics": "Аналітика", - "type-analytics-details": "Виконує аналіз потокових або збережених даних", - "type-external": "Зовнішній", - "type-external-details": "Взаємодіє з зовнішньою системою", - "type-rule-chain": "Ланцюг правил", - "type-rule-chain-details": "Перенаправити вхідне повідомлення на вказаний ланцюг правил", - "type-input": "Вхід", - "type-input-details": "Логічний вхід ланцюга правил, перенаправляє вхідні повідомлення на наступний пов'язаний вузол правил", - "type-unknown": "Невідомий", - "type-unknown-details": "Невизначений вузол правил", - "directive-is-not-loaded": "Вказана директива конфігурації '{{directiveName}}' недоступна.", - "ui-resources-load-error": "Не вдалося завантажити UI ресурси.", - "invalid-target-rulechain": "Не вдається визначити цільовий ланцюг правил!", - "test-script-function": "Протестувати скрипт", - "message": "Повідомлення", - "message-type": "Тип повідомлення", - "select-message-type": "Вибрати тип повідомлення", - "message-type-required": "Необхідно вказати тип повідомлення", - "metadata": "Метадані", - "metadata-required": "Записи метаданих не можуть бути порожніми.", - "output": "Вихід", - "test": "Тест", - "help": "Допомога", - "reset-debug-mode": "Вимкнути режим налогодження у всіх правилах" - }, - "queue": { - "select_name": "Виберіть ім'я для Queue", - "name": "Iм'я для Queue", - "name_required": "Поле 'Имя для Queue' обязательно к заполнению!" - - }, - "scheduler": { - "scheduler": "Планувальник", - "scheduler-event": "Подія планувальника", - "select-scheduler-event": "Вибрати подію", - "no-scheduler-events-matching": "Не знайдено жодних подій, які відповідають '{{entity}}'.", - "scheduler-event-required": "Необхвдно вказати заплановану подію", - "management": "Управління планувальником", - "scheduler-events": "Планування подій", - "add-scheduler-event": "Додати подію", - "search-scheduler-events": "Пошук події", - "created-time": "Час створення", - "name": "Ім'я", - "type": "Тип", - "created_customer": "Створено клієнтом", - "edit-scheduler-event": "Редагувати подію", - "delete-scheduler-event": "Видалити подію", - "no-scheduler-events": "Не знайдено жодних запланованих подій", - "selected-scheduler-events": "{ count, plural, 1 {1 запланована подія} other {# заплановані події} } вибрано", - "delete-scheduler-event-title": "Ви впевнені, що хочете видалити подію '{{schedulerEventName}}'?", - "delete-scheduler-event-text": "Будьте обережні, після підтвердження подія і всі пов'язані з нею дані стануть недоступними.", - "delete-scheduler-events-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 запланована подія} other {# заплановані події} }?", - "delete-scheduler-events-text": "Будьте обережні, після підтвердження всі вибрані події будуть видалені, і всі пов'язані з ними дані стануть недоступними.", - "create": "Створити подію планувальника", - "edit": "Змінити подію планувальника", - "name-required": "Необхідно задати ім'я", - "configuration": "Конфігурація", - "schedule": "Розклад", - "start": "Початок", - "date": "Дата", - "time": "Час", - "repeat": "Повтор", - "repeats": "Повтори", - "daily": "Щодня", - "weekly": "Щотижня", - "repeats-required": "Потрібно вказати повторення.", - "repeat-on": "Повторювати по", - "repeat-every": "Повторювати кожний(у)", - "ends-on": "Завершення", - "sunday-label": "Нд", - "monday-label": "Пн", - "tuesday-label": "Вт", - "wednesday-label": "Ср", - "thursday-label": "Чт", - "friday-label": "Пт", - "saturday-label": "Сб", - "repeat-on-sunday": "Повторити у неділю", - "repeat-on-monday": "Повторити в понеділок", - "repeat-on-tuesday": "Повторити у вівторок", - "repeat-on-wednesday": "Повторити в середу", - "repeat-on-thursday": "Повторити в червер", - "repeat-on-friday": "Повторити в п'ятницю", - "repeat-on-saturday": "Повторити в суботу", - "event-type": "Тип події", - "select-event-type": "Вибрати тип події", - "event-type-required": "Необхідно вказати типи події.", - "list-mode": "Перегляд списку", - "calendar-mode": "Перегляд календаря", - "calendar-view-type": "Тип перегляду календаря", - "month": "Місяць", - "week": "Тиждень", - "day": "День", - "agenda-week": "Порядок тижня", - "agenda-day": "Порялок дня", - "list-year": "Список року", - "list-month": "Список місяця", - "list-week": "Список тижня", - "list-day": "Список дня", - "today": "Сьогодні", - "navigate-before": "Перейти до", - "navigate-next": "Перейти далі", - "starting-from": "Починаючи з", - "until": "до", - "on": "в", - "sunday": "Неділя", - "monday": "Понеділок", - "tuesday": "Вівторок", - "wednesday": "Середа", - "thursday": "Четвер", - "friday": "П'ятниця", - "saturday": "Субота", - "originator": "Ініціатор", - "single-entity": "Єдина сутність", - "group-of-entities": "Група сутностей", - "single-device": "Один пристрій", - "group-of-devices": "Група пристроїв", - "message-body": "Текст повідомлення", - "target": "Ціль", - "rpc-method": "Метод", - "rpc-method-required": "Необхідно вказати метод", - "rpc-params": "Параметри", - "select-dashboard-state": "Виберіть стан панелі візуалізації", - "hours": "Години", - "minutes": "Хвилини", - "seconds": "Секунди", - "time-interval-required": "Необхідно вказати часовий інтервал", - "time-unit-required": "Необхідно вказати одиниці часу" - }, - "report": { - "report-config": "Конфігурація звіту", - "email-config": "Конфігурація електронної пошти", - "dashboard-state-param": "Значення параметра стану панелі візуалізації", - "base-url": "Базова URL-адреса", - "base-url-required": "Необхідно вказати базову URL-адресу.", - "use-dashboard-timewindow": "Використовуйте вікно часу на панелі інструментів", - "timewindow": "Вікно часу", - "name-pattern": "Шаблон імені звіту", - "name-pattern-required": "Необхідно задати шаблон назви звіту", - "type": "Report type", - "use-current-user-credentials": "Використовувати поточні авторизаційні дані користувача", - "customer-user-credentials": "Авторизаційні дані користувачів", - "customer-user-credentials-required": "Необхідно задати авторизаційні дані користувачів", - "generate-test-report": "Створити звіт про перевірку", - "send-email": "Відправити лист", - "from": "Від", - "from-required": "Необхідно вказати від кого.", - "to": "До", - "to-required": "Необхідно вказати до кого.", - "cc": "Cc", - "bcc": "Bcc", - "subject": "Тема", - "subject-required": "Необхідно вказати тему.", - "body": "Текст", - "body-required": "Лист не може бути пустим." - }, - "blob-entity": { - "blob-entity": "Blob сутності", - "select-blob-entity": "Вибрати blob сутності", - "no-blob-entities-matching": "Не знайдено жодних сутностей blob, які відповідають '{{entity}}'.", - "blob-entity-required": "Необхідно вказати blob сутності", - "files": "Файли", - "search": "Пошук файлів", - "clear-search": "Очистити пошук", - "no-blob-entities-prompt": "Файлів не знайдено", - "report": "Звіт", - "created-time": "Час створення", - "name": "Ім'я", - "type": "Тип", - "created_customer": "Створено клієнтом", - "download-blob-entity": "Завантажити файл", - "delete-blob-entity": "Видалити файл", - "delete-blob-entity-title": "Ви впевнені, що хочете видалити файл '{{blobEntityName}}'?", - "delete-blob-entity-text": "Будьте обережні, після підствердження, дані файлу стануть недоступними." - }, - "timezone": { - "timezone": "Часовий пояс", - "select-timezone": "Вибрати часовий пояс ", - "no-timezones-matching": "Не знайдено жодних часових поясів, які відповідають '{{timezone}}'.", - "timezone-required": "Необхідно вказати часовий пояс." - }, - "tenant": { - "tenant": "Власник", - "tenants": "Власники", - "management": "Управління власниками", - "add": "Додати власника", - "admins": "Адміністратори", - "manage-tenant-admins": "Керування адміністраторами власника", - "delete": "Видалити власника", - "add-tenant-text": "Додати нового власника", - "no-tenants-text": "Не знайдено жодного власника", - "tenant-details": "Подробиці про власника", - "delete-tenant-title": "Ви впевнені, що хочете видалити власника'{{tenantTitle}}'?", - "delete-tenant-text": "Будьте обережні, після підтвердження власник і всі пов'язані з ним дані стануть недоступними.", - "delete-tenants-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 власник} other {# власники} }?", - "delete-tenants-action-title": "Видалити { count, plural, 1 {1 власник} other {# власники} }", - "delete-tenants-text": "Будьте обережні, після підтвердження, усі вибрані власники будуть видалені, і всі пов'язані з ними дані стануть недоступними.", - "title": "Назва", - "title-required": "Необхідно вказати назву.", - "description": "Опис", - "details": "Деталі", - "events": "Події", - "copyId": "Копіювати Id власника", - "idCopiedMessage": "Id власника скопійовано в буфер обміну", - "select-tenant": "Вибрати власника", - "no-tenants-matching": "Не знайдено жодних власників, які відповідають '{{entity}}'.", - "tenant-required": "Необхідно вказати власника", - "selected-tenants": "{ count, plural, 1 {1 власник} other {# власники} } вибрано", - "search": "Пошук власників" - }, - "timeinterval": { - "seconds-interval": "{ seconds, plural, 1 {1 секунда} other {# секунди} }", - "minutes-interval": "{ minutes, plural, 1 {1 хвилина} other {# хвилини} }", - "hours-interval": "{ hours, plural, 1 {1 година} other {# години} }", - "days-interval": "{ days, plural, 1 {1 день} other {# дні} }", - "days": "Дні", - "hours": "Години", - "minutes": "Хвилини", - "seconds": "Секунди", - "advanced": "Додатково" - }, - "timewindow": { - "days": "{ days, plural, 1 { день } other {# дні } }", - "hours": "{ hours, plural, 0 { годин } 1 {1 година } other {# години } }", - "minutes": "{ minutes, plural, 0 { хвилин } 1 {1 хвилина } other {# хвилини } }", - "seconds": "{ seconds, plural, 0 { секунд } 1 {1 секунда } other {# секунди } }", - "realtime": "Реальний час", - "history": "Історія", - "last-prefix": "Останнє", - "period": "з {{ startTime }} до {{ endTime }}", - "edit": "Редагувати вікно часу", - "date-range": "Проміжок часу", - "last": "Останнє", - "time-period": "Період часу", - "hide": "Приховати" - }, - "user": { - "user": "Користувач", - "users": "Користувачі", - "customer-users": "Користувачі клієнта", - "tenant-admins": "Адміністратори власників", - "sys-admin": "Системний адміністратор", - "tenant-admin": "Адміністратор власника", - "customer": "Клієнт", - "anonymous": "Анонім", - "add": "Додати користувача", - "delete": "Видалити користувача", - "add-user-text": "Додати нового користувача", - "no-users-text": "Не знайдено жодного користувача", - "user-details": "Подробиці про користувача", - "delete-user-title": "Ви впевнені, що хочете видалити користувача'{{userEmail}}'?", - "delete-user-text": "Будьте обережні, після підтвердження, користувач і всі пов'язані з ним дані стануть недоступними.", - "delete-users-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 користувача} other {# користувачів} }?", - "delete-users-action-title": "Видалити { count, plural, 1 {1 користувача} other {# користувачів} }", - "delete-users-text": "Будьте обережні, після підтвердження, усіх виділених користувачів буде видалено, і всі пов'язані з ними дані стануть недоступними.", - "activation-email-sent-message": "Повідомлення про активацію успішно надіслано!", - "resend-activation": "Повторно надіслати активацію", - "email": "Електронна пошта", - "email-required": "Необхідно вказати електронну пошту.", - "invalid-email-format": "Недійсний формат електронної пошти.", - "first-name": "Ім'я", - "last-name": "Прізвище", - "description": "Опис", - "default-dashboard": "Стандартна панель візуалізації", - "always-fullscreen": "Завжди в повноекранному режимі", - "select-user": "Вибрати користувача", - "no-users-matching": "Не знайдено жодного користувача, що відповідає '{{entity}}'.", - "user-required": "Необхідно вказати користувача", - "activation-method": "Спосіб активації", - "display-activation-link": "Показати посилання для активації", - "send-activation-mail": "Надіслати активаційного листа", - "activation-link": "Активаційне посилання для користувача", - "activation-link-text": "Для активувації користувача, скористайтеся наступним activation link :", - "copy-activation-link": "Скопіювати активаційне посилання ", - "activation-link-copied-message": "Посилання на активацію користувача було скопійовано в буфер обміну", - "selected-users": "{ count, plural, 1 {1 користувач} other {# користувачі} } вибрано", - "search": "Пошук користувачів", - "details": "Подробиці", - "login-as-tenant-admin": "Увійти як адміністратор власника", - "login-as-customer-user": "Увійти як користувач клієнта", - "disable-account": "Вимкнути обліковий запис користувача", - "enable-account": "Увімкнути обліковий запис користувача", - "enable-account-message": "Обліковий запис користувача успішно увімкнено!", - "disabled-account-message": "Обліковий запис користувача успішно вимкнено!" - }, - "value": { - "type": "Тип значення", - "string": "Рядок", - "string-value": "Значення рядка", - "integer": "Ціле", - "integer-value": "Ціле значення", - "invalid-integer-value": "Недійсне ціле значення", - "double": "Подвійне", - "double-value": "Подвійне значення", - "boolean": "Логічне", - "boolean-value": "Логічне значення", - "false": "Помилкове", - "true": "Правдиве", - "long": "Довге" - }, - "widget": { - "widget-library": "Бібліотека віджетів", - "widget-bundle": "Пакет віджетів", - "select-widgets-bundle": "Виберіть пакет віджетів", - "management": "Керування віджетами", - "editor": "Редактор віджетів", - "widget-type-not-found": "Помилка завантаження конфігурації віджетів.
    Можливо, пов'язаний з нею\n тип віджета було видалено.", - "widget-type-load-error": "Віджет не вдалося завантажити з наступних причин:", - "remove": "Видалити віджет", - "edit": "Відредагувати віджет", - "remove-widget-title": "Ви впевнені, що хочете видалити віджет '{{widgetTitle}}'?", - "remove-widget-text": "Після підтвердження віджет і всі пов'язані з ним дані стануть недоступними.", - "timeseries": "Телеметрія", - "search-data": "Пошук даних", - "no-data-found": "Даних не знайдено", - "latest-values": "Останні значення", - "rpc": "Керуючий віджет", - "alarm": "Віджет сигнала тривоги", - "static": "Статичний віджет", - "select-widget-type": "Вибрати тип віджета", - "missing-widget-title-error": "Необхідно вказати назву віджета!", - "widget-saved": "Віджет збережено", - "unable-to-save-widget-error": "Неможливо зберегти віджет! Віджет має помилки!", - "save": "Зберегти віджет", - "saveAs": "Зберегти віджет як", - "save-widget-type-as": "Зберегти тип віджета як", - "save-widget-type-as-text": "Введіть новий заголовок віджета та / або виберіть цільові віджети", - "toggle-fullscreen": "Перейти в повноекранний режим", - "run": "Запустити віджет", - "title": "Назва віджета", - "title-required": "Необхідно вказати назву віджета.", - "type": "Тип віджета", - "resources": "Ресурси", - "resource-url": "JavaScript/CSS URL", - "remove-resource": "Видалити ресурс", - "add-resource": "Додати ресурс", - "html": "HTML", - "tidy": "Форматувати", - "css": "CSS", - "settings-schema": "Схема налаштувань", - "datakey-settings-schema": "Схема налаштувань ключів даних", - "javascript": "Javascript", - "remove-widget-type-title": "Ви впевнені, що хочете видалити тип віджета '{{widgetName}}'?", - "remove-widget-type-text": "Будьте обережні, після підтвердження, тип віджета і всі пов'язані з ним дані стануть недоступними.", - "remove-widget-type": "Видалити тип віджета", - "add-widget-type": "Додати новий тип віджета", - "widget-type-load-failed-error": "Не вдалося завантажити тип віджета!", - "widget-template-load-failed-error": "Не вдалося завантажити шаблон віджета!", - "add": "Додати віджет", - "undo": "Скасувати зміни віджета", - "export": "Експртувати віджет", - "export-data": "Експортувати дані віджетів", - "export-to-csv": "Експортувати дані в CSV...", - "export-to-excel": "Експортувати дані в XLS..." - }, - "widget-action": { - "header-button": "Кнопка заголовка віджета", - "open-dashboard-state": "Перейти до нового стану панелі візуалізації", - "update-dashboard-state": "Оновити поточний стан панелі візуалізації", - "open-dashboard": "Перейти до іншої панелі візуалізації", - "custom": "Дії користувачів", - "custom-pretty": "Дії користувачів (з HTML шаблоном)", - "target-dashboard-state": "Цільовий стан панелі візуалізації", - "target-dashboard-state-required": "Необхідно вказати цільовий стан панелі візуалізації", - "set-entity-from-widget": "Встановити сутність із віджета", - "target-dashboard": "Цільова панель візуалізації", - "open-right-layout": "Відкрити мобільний режим панелі візуалізації" - }, - "widgets-bundle": { - "current": "Поточний зв'язок", - "widgets-bundles": "Пакети віджетів", - "add": "Додати пакет віджетів", - "delete": "Видалити пакет віджетів", - "title": "Назва", - "title-required": "Необхідно вказати назву віджета.", - "add-widgets-bundle-text": "Додати новий пакет віджетів", - "no-widgets-bundles-text": "Не знайдено жодних пакетів віджетів", - "empty": "Пакет віджетів порожній", - "details": "Подробиці", - "widgets-bundle-details": "Деталі пакетів віджетів", - "delete-widgets-bundle-title": "Ви впевнені, що хочете видалити пакет віджетів '{{widgetsBundleTitle}}'?", - "delete-widgets-bundle-text": "Будьте обережні, після підтвердження, пакети віджетів і всі пов'язані з ними дані стануть недоступними.", - "delete-widgets-bundles-title": "Ви впевнені, що хочете видалити { count, plural, 1 {пакет віджетів} other {# пакети віджетів} }?", - "delete-widgets-bundles-action-title": "Видалити { count, plural, 1 {1 пакет віджетів} other {# пакет віджетів} }", - "delete-widgets-bundles-text": "Будьте обережні, після підтвердження, всі виділені пакети віджетів і всі пов'язані з ними дані стануть недоступними.", - "no-widgets-bundles-matching": "Не знайдено жодних пакетів віджетів, які відповідають '{{widgetsBundle}}'.", - "widgets-bundle-required": "Необхідно вказати пакет віджетів.", - "system": "Системний", - "import": "Імпортувати пакет віджетів", - "export": "Експортувати пакет віджетів", - "export-failed-error": "Неможливо експортувати пакет віджетів: {{error}}", - "create-new-widgets-bundle": "Створити новий пакет віджетів", - "widgets-bundle-file": "Файл набору віджетів", - "invalid-widgets-bundle-file-error": "Неможливо імпортувати пакет віджетів: недійсна структура даних пакету віджетів." - }, - "widget-config": { - "data": "Дані", - "settings": "Налаштування", - "advanced": "Додатково", - "title": "Назва", - "general-settings": "Загальні налаштування", - "display-title": "Відобразити назву", - "drop-shadow": "Тінь", - "enable-fullscreen": "Увімкнути повноекранний режим", - "enable-data-export": "Увімкнути експорт даних", - "background-color": "Колір фону", - "text-color": "Колір тексту", - "padding": "Відступ", - "margin": "Границі", - "widget-style": "Стиль віджетів", - "title-style": "Стиль заголовка", - "mobile-mode-settings": "Налаштування мобільного режиму", - "order": "Порядок", - "height": "Висота", - "units": "Спеціальний символ після значення", - "decimals": "Кількість цифр після коми", - "timewindow": "Вікно часу", - "use-dashboard-timewindow": "Використати вікно часу на панелі візуалізації", - "display-timewindow": "Показувати вікно часу", - "display-legend": "Показати легенду", - "datasources": "Джерела даних", - "maximum-datasources": "Максимально { count, plural, 1 {1 дозволене джерело даних.} other {# дозволені джерела даних } }", - "datasource-type": "Тип", - "datasource-parameters": "Параметри", - "remove-datasource": "Видалити джерело даних", - "add-datasource": "Додати джерело даних", - "target-device": "Цільовий пристрій", - "alarm-source": "Джерело сигнала тривоги", - "actions": "Дії", - "action": "Дія", - "add-action": "Додати дію", - "search-actions": "Пошук дії", - "action-source": "Джерело дії", - "action-source-required": "Необхідно вказати джерело дії.", - "action-name": "Ім'я дії", - "action-name-required": "Необхідно вказати ім'я дії.", - "action-name-not-unique": "Дія з такою назвою вже існує.
    Назва дії має бути унікальною в межах одного джерела дії.", - "action-icon": "Іконка", - "action-type": "Тип", - "action-type-required": "Необхідно вказати тип дії.", - "edit-action": "Редагувати дію", - "delete-action": "Видалити дію", - "delete-action-title": "Видалити дію віджета", - "delete-action-text": "Ви впевнені, що хочете видалити дію віджета '{{actionName}}'?", - "display-icon": "Показувати іконку у назві", - "icon-color": "Колір іконки", - "icon-size": "Розмір іконки" - }, - "widget-type": { - "import": "Імпортувати тип віджета", - "export": "Експортувати тип віджета", - "export-failed-error": "Неможливо експортувати тип віджета: {{error}}", - "create-new-widget-type": "Створити новий тип віджета", - "widget-type-file": "Файл типу віджета", - "invalid-widget-type-file-error": "Неможливо імпортувати тип віджету: неправильна структура даних типу віджета." - }, - "widgets": { - "date-range-navigator": { - "localizationMap": { - "Sun": "Нд", - "Mon": "Пн", - "Tue": "Вт", - "Wed": "Ср", - "Thu": "Чт", - "Fri": "Пт", - "Sat": "Сб", - "Jan": "Січ.", - "Feb": "Лют.", - "Mar": "Берез.", - "Apr": "Квіт.", - "May": "Трав.", - "Jun": "Черв.", - "Jul": "Лип.", - "Aug": "Серп.", - "Sep": "Верес.", - "Oct": "Жовт.", - "Nov": "Листоп.", - "Dec": "Груд.", - "January": "Січень", - "February": "Лютий", - "March": "Березень", - "April": "Квітень", - "June": "Червень", - "July": "Липень", - "August": "Серпень", - "September": "Вересень", - "October": "Жовтень", - "November": "Листопад", - "December": "Грудень", - "Custom Date Range": "Користувацький діапазон дат", - "Date Range Template": "Шаблон діапазону дат", - "Today": "Сьогодні", - "Yesterday": "Вчора", - "This Week": "Цього тижня", - "Last Week": "Минулий тиждень", - "This Month": "Цей місяць", - "Last Month": "Минулий місяць", - "Year": "Рік", - "This Year": "Цього року", - "Last Year": "Минулий рік", - "Date picker": "Вибір дати", - "Hour": "Година", - "Day": "День", - "Week": "Тиждень", - "2 weeks": "2 Тижні", - "Month": "Місяць", - "3 months": "3 Місяці", - "6 months": "6 Місяців", - "Custom interval": "Користувацький інтервал", - "Interval": "Інтервал", - "Step size": "Розмір кроку", - "Ok": "Ok" - } - }, - "input-widgets": { - "attribute-not-allowed": "Атрибут не може бути вибраний в цьому віджеті", - "date": "Дата", - "blocked-location": "Геолокація заблокована у вашому браузері", - "claim-device": "Підтвердити пристрій", - "claim-failed": "Не вдалося підтвердити пристрій!", - "claim-not-found": "Пристрій не знайдено!", - "claim-successful": "Пристрій успішно підтверджено!", - "discard-changes": "Скасувати зміни", - "device-name": "Назва пристрою", - "device-name-required": "Необхідно вказати назву пристрою", - "entity-attribute-required": "Значення атрибута обов'язкове", - "entity-coordinate-required": "Необхідно вказати широту та довготу", - "entity-timeseries-required": "Значення телеметрії обов'язкове", - "get-location": "Отримати поточне місцезнаходження", - "latitude": "Широта", - "longitude": "Довгота", - "not-allowed-entity": "Обрана сутність не має спільних атрибутів", - "no-attribute-selected": "Атрибут не вибрано", - "no-datakey-selected": "Ні один datakey не обраний", - "no-entity-selected": "Сутність не вибрано", - "no-coordinate-specified": "Ключ для широти/довготи не вказаний", - "no-support-geolocation": "Ваш браузер не підтримує геолокацію", - "no-image": "Немає зображення", - "no-support-web-camera": "Нет поддерживаемой веб-камеры", - "no-timeseries-selected": "Параметр телеметрії не вибрано", - "secret-key": "Секретний ключ", - "secret-key-required": "Необхідно вказати секретний ключ", - "switch-attribute-value": "Змінити значення атрибута", - "switch-camera": "Змінити камеру", - "switch-timeseries-value": "Змінити значення телеметрії", - "take-photo": "Зробити фото", - "time": "Час", - "timeseries-not-allowed": "Телеметрія не може бути вибрана в цьому віджеті", - "update-failed": "Не вдалося оновити", - "update-successful": "Успішно оновлено", - "update-attribute": "Оновити атрибут", - "update-timeseries": "Оновити телеметрію", - "value": "Значення" - } - }, - "white-labeling": { - "white-labeling": "Білий маркування", - "login-white-labeling": "Login White Labeling", - "preview": "Попередній перегляд", - "app-title": "Назва програми", - "favicon": "Іконка веб-сайту", - "favicon-description": "*.ico, *.gif or *.png image with maximum size {{kbSize}} KBytes.", - "favicon-size-error": "Зображення веб-сайту завелике. Максимально дозволений розмір зображення веб-сайту {{kbSize}} KBytes.", - "favicon-type-error": "Недійсний формат файлу зображення веб-сайту. Приймаються лише зображення ICO, GIF або PNG.", - "drop-favicon-image": "Зніміть зображення піктограми веб-сайту або клацніть, щоб вибрати файл для завантаження.", - "no-favicon-image": "Не вибрано жодної іконки", - "logo": "Логотип", - "logo-description": "Будь-яке зображення з максимальним розміром {{kbSize}} KBytes.", - "logo-size-error": "Зображення логотипу занадто велике. Максимально дозволений розмір зображення логотипу{{kbSize}} KBytes.", - "logo-type-error": "Недійсний формат файлу логотипу. Приймаються тільки зображення.", - "drop-logo-image": "Зніміть зображення логотипу або клацніть, щоб вибрати файл для завантаження.", - "no-logo-image": "Не вибрано жожного логотипу", - "logo-height": "Висота логотипу, px", - "primary-palette": "Основна палітра", - "accent-palette": "Палітра акцент", - "customize-palette": "Налаштування", - "edit-palette": "Редагувати палітру", - "save-palette": "Зберегти палітру", - "primary-background": "Первинний фон", - "secondary-background": "Вторинний фон", - "hue1": "HUE 1", - "hue2": "HUE 2", - "hue3": "HUE 3", - "page-background-color": "Колір фону сторінки", - "dark-foreground": "Темний передній план", - "domain-name": "Доменне ім'я" - }, - "icon": { - "icon": "Іконка", - "select-icon": "Виберіть Іконку", - "material-icons": "Іконки в стилі Material", - "show-all": "Показати всі іконки" - }, - "custom": { - "widget-action": { - "action-cell-button": "Кнопка дії в комірці таблиці", - "row-click": "Клацніть на рядок", - "marker-click": "Клацніть на маркер", - "polygon-click": "Дія при натисканні на полігон", - "tooltip-tag-action": "Дія при натисканні на посилання в підказці", - "node-selected": "Дії при виборі ноди", - "element-click": "Дії при натисканні на HTML елементі", - "pie-slice-click": "Дії при натисканні на секції кругової діаграми", - "row-double-click": "Дії при подвійному натисканні на рядок" - } - }, - "language": { - "language": "Мова" - } -} diff --git a/ui/src/app/locale/locale.constant-zh_CN.json b/ui/src/app/locale/locale.constant-zh_CN.json deleted file mode 100644 index 811045c8aa..0000000000 --- a/ui/src/app/locale/locale.constant-zh_CN.json +++ /dev/null @@ -1,1607 +0,0 @@ -{ - "access": { - "unauthorized": "未授权", - "unauthorized-access": "未授权访问", - "unauthorized-access-text": "您需要登陆才能访问这个资源!", - "access-forbidden": "禁止访问", - "access-forbidden-text": "您没有访问此位置的权限
    如果您仍希望访问此位置,请尝试使用其他用户登录。", - "refresh-token-expired": "会话已过期", - "refresh-token-failed": "无法刷新会话" - }, - "action": { - "activate": "激活", - "suspend": "暂停", - "save": "保存", - "saveAs": "另存为", - "cancel": "取消", - "ok": "确定", - "delete": "删除", - "add": "添加", - "yes": "是", - "no": "否", - "update": "更新", - "remove": "移除", - "search": "查询", - "clear-search": "清除查询", - "assign": "分配", - "unassign": "取消分配", - "share": "分享", - "make-private": "私有", - "apply": "应用", - "apply-changes": "应用更改", - "edit-mode": "编辑模式", - "enter-edit-mode": "进入编辑模式", - "decline-changes": "撤销更改", - "close": "关闭", - "back": "后退", - "run": "运行", - "sign-in": "登录!", - "edit": "编辑", - "view": "查看", - "create": "创建", - "drag": "拖拽", - "refresh": "刷新", - "undo": "撤销", - "copy": "复制", - "paste": "粘贴", - "copy-reference": "复制引用", - "paste-reference": "粘贴引用", - "import": "导入", - "export": "导出", - "share-via": "通过 {{provider}}分享" - }, - "aggregation": { - "aggregation": "聚合", - "function": "数据聚合功能", - "limit": "最大值", - "group-interval": "分组间隔", - "min": "最少值", - "max": "最大值", - "avg": "平均值", - "sum": "求和", - "count": "计数", - "none": "空" - }, - "admin": { - "general": "常规", - "general-settings": "常规设置", - "outgoing-mail": "发送邮件", - "outgoing-mail-settings": "发送邮件设置", - "system-settings": "系统设置", - "test-mail-sent": "测试邮件发送成功!", - "base-url": "基本URL", - "base-url-required": "基本URL必填。", - "mail-from": "邮件来自", - "mail-from-required": "邮件发件人必填。", - "smtp-protocol": "SMTP协议", - "smtp-host": "SMTP主机", - "smtp-host-required": "SMTP主机必填。", - "smtp-port": "SMTP端口", - "smtp-port-required": "您必须提供一个smtp端口。", - "smtp-port-invalid": "这看起来不是有效的smtp端口。", - "timeout-msec": "超时(ms)", - "timeout-required": "超时必填。", - "timeout-invalid": "这看起来不像有效的超时值。", - "enable-tls": "启用TLS", - "tls-version" : "TLS版本", - "send-test-mail": "发送测试邮件" - }, - "alarm": { - "alarm": "警告", - "alarms": "警告", - "select-alarm": "选择警告", - "no-alarms-matching": "没有找到匹配 '{{entity}}' 的警告", - "alarm-required": "警告必填", - "alarm-status": "警告状态", - "search-status": { - "ANY": "所有", - "ACTIVE": "已激活", - "CLEARED": "已清除", - "ACK": "已应答", - "UNACK": "未应答" - }, - "display-status": { - "ACTIVE_UNACK": "激活未应答", - "ACTIVE_ACK": "激活已应答", - "CLEARED_UNACK": "清除未应答", - "CLEARED_ACK": "清除已应答" - }, - "no-alarms-prompt": "未发现警告", - "created-time": "创建时间", - "type": "类型", - "severity": "严重程度", - "originator": "起因", - "originator-type": "起因类型", - "details": "详情", - "status": "状态", - "alarm-details": "警告详情", - "start-time": "开始时间", - "end-time": "结束时间", - "ack-time": "应答时间", - "clear-time": "创建时间", - "severity-critical": "危险", - "severity-major": "重要", - "severity-minor": "次要", - "severity-warning": "警告", - "severity-indeterminate": "不确定", - "acknowledge": "应答", - "clear": "清除", - "search": "搜索警告", - "selected-alarms": "已选择 { count, plural, 1 {1 警告} other {# 警告} } ", - "no-data": "无数据显示", - "polling-interval": "警告轮询间隔(秒)", - "polling-interval-required": "警告轮询间隔必填。", - "min-polling-interval-message": "轮询间隔至少是1秒。", - "aknowledge-alarms-title": "应答 { count, plural, 1 {1 警告} other {# 警告} }", - "aknowledge-alarms-text": "确定要应答 { count, plural, 1 {1 警告} other {# 警告} }?", - "clear-alarms-title": "清除 { count, plural, 1 {1 警告} other {# 警告} }", - "clear-alarms-text": "确定要清除 { count, plural, 1 {1 警告} other {# 警告} }?" - }, - "alias": { - "add": "添加别名", - "edit": "编辑别名", - "name": "别名", - "name-required": "别名必填", - "duplicate-alias": "别名已经存在。", - "filter-type-single-entity": "单个实体", - "filter-type-entity-list": "实体列表", - "filter-type-entity-name": "实体名称", - "filter-type-state-entity": "实体(仪表板状态)", - "filter-type-state-entity-description": "实体令牌(仪表板状态参数)", - "filter-type-asset-type": "资产类型", - "filter-type-asset-type-description": "类型为 '{{assetType}}' 的资产", - "filter-type-asset-type-and-name-description": "类型为 '{{assetType}}' 且以 '{{prefix}}' 开头的资产", - "filter-type-device-type": "设备类型", - "filter-type-device-type-description": "类型为 '{{deviceType}}' 的设备", - "filter-type-device-type-and-name-description": "类型为 '{{deviceType}}' 且以 '{{prefix}}' 开头的设备", - "filter-type-entity-view-type": "实体视图类型", - "filter-type-entity-view-type-description": "类型为 '{{entityView}}' 的实体视图", - "filter-type-entity-view-type-and-name-description": "类型为 {{entityView}}' 且以 '{{prefix}}' 开头的实体视图", - "filter-type-relations-query": "关系查询", - "filter-type-relations-query-description": "具有 {{relationType}} 关联 {{direction}} {{rootEntity}} 的 {{entities}} ", - "filter-type-asset-search-query": "资产搜索查询", - "filter-type-asset-search-query-description": "类型为 {{assetTypes}} 且具有 {{relationType}} 关联 {{direction}} {{rootEntity}} 的资产", - "filter-type-device-search-query": "设备搜索查询", - "filter-type-device-search-query-description": "类型为 {{deviceTypes}} 且具有 {{relationType}} 关联 {{direction}} {{rootEntity}} 的设备", - "filter-type-entity-view-search-query": "实体视图搜索查询", - "filter-type-entity-view-search-query-description": "类型为 {{entityViewTypes}} 且具有 {{relationType}} 关联 {{direction}} {{rootEntity}} 的实体视图", - "entity-filter": "实体过滤", - "resolve-multiple": "解决为多实体", - "filter-type": "过滤类型", - "filter-type-required": "过滤类型必填。", - "entity-filter-no-entity-matched": "未找到符合指定过滤条件的实体。", - "no-entity-filter-specified": "没有指定实体过滤条件", - "root-state-entity": "使用仪表板状态实体作为根实体", - "root-entity": "根实体", - "state-entity-parameter-name": "状态实体参数名称", - "default-state-entity": "默认状态实体", - "default-entity-parameter-name": "默认", - "max-relation-level": "最大关系层级", - "unlimited-level": "不限层级", - "state-entity": "仪表板状态实体", - "all-entities": "所有实体", - "any-relation": "不限" - }, - "asset": { - "asset": "资产", - "assets": "资产", - "management": "资产管理", - "view-assets": "查看资产", - "add": "添加资产", - "assign-to-customer": "分配给客户", - "assign-asset-to-customer": "将资产分配给客户", - "assign-asset-to-customer-text": "请选择要分配给客户的资产", - "no-assets-text": "未找到资产", - "assign-to-customer-text": "请选择客户以分配资产", - "public": "公开", - "assignedToCustomer": "分配客户", - "make-public": "资产设为公开", - "make-private": "资产设为私有", - "unassign-from-customer": "取消分配客户", - "delete": "删除资产", - "asset-public": "资产公开", - "asset-type": "资产类型", - "asset-type-required": "资产类型必填。", - "select-asset-type": "选择资产类型", - "enter-asset-type": "输入资产类型", - "any-asset": "任何资产", - "no-asset-types-matching": "没有找到匹配 '{{entitySubtype}}' 的资产类型。", - "asset-type-list-empty": "资产类型未选择。", - "asset-types": "资产类型", - "name": "名称", - "name-required": "名称必填。", - "description": "描述", - "type": "类型", - "type-required": "类型必填。", - "details": "详情", - "events": "事件", - "add-asset-text": "添加新资产", - "asset-details": "资产详情", - "assign-assets": "分配资产", - "assign-assets-text": "分配 { count, plural, 1 {1 资产} other {# 资产} } 给客户", - "delete-assets": "删除资产", - "unassign-assets": "取消分配资产", - "unassign-assets-action-title": "从客户处取消分配 { count, plural, 1 {1 资产} other {# 资产} } ", - "assign-new-asset": "分配新资产", - "delete-asset-title": "确定要删除资产 '{{assetName}}'?", - "delete-asset-text": "小心!确认后资产及其所有相关数据将不可恢复。", - "delete-assets-title": "确定要删除 { count, plural, 1 {1 资产} other {# 资产} }?", - "delete-assets-action-title": "删除 { count, plural, 1 {1 资产} other {# 资产} }", - "delete-assets-text": "小心,确认后,所有选定的资产将被删除,所有相关的数据将变得不可恢复。", - "make-public-asset-title": "你确定你想创建公开'{{assetName}}'资产?", - "make-public-asset-text": "确认后,资产及其所有数据将被公开并被他人访问。", - "make-private-asset-title": "你确定你想创建私有 '{{assetName}}' 资产?", - "make-private-asset-text": "确认后,资产及其所有数据将被私有化,无法被他人访问。", - "unassign-asset-title": "您确定要取消对'{{assetName}}'资产的分配吗?", - "unassign-asset-text": "确认后,资产将未分配,客户无法访问。", - "unassign-asset": "未分配资产", - "unassign-assets-title": "您确定要取消分配 { count, plural, 1 {1 资产} other {# 资产} }吗?", - "unassign-assets-text": "确认后,所有选定的资产将被分配,客户无法访问。", - "copyId": "复制资产ID", - "idCopiedMessage": "资产ID已经复制到粘贴板", - "select-asset": "选择资产", - "no-assets-matching": "没有找到匹配 '{{entity}}' 的资产。", - "asset-required": "资产必填", - "name-starts-with": "资产名称以此开头", - "label": "标签" - }, - "attribute": { - "attributes": "属性", - "latest-telemetry": "最新遥测", - "attributes-scope": "设备属性范围", - "scope-latest-telemetry": "最新遥测", - "scope-client": "客户端属性", - "scope-server": "服务端属性", - "scope-shared": "共享属性", - "add": "添加属性", - "key": "键", - "last-update-time": "最后更新时间", - "key-required": "属性键必填。", - "value": "值", - "value-required": "属性值必填。", - "delete-attributes-title": "您确定要删除 { count, plural, 1 {1 属性} other {# 属性} }吗?", - "delete-attributes-text": "注意,确认后所有选中的属性都会被删除。", - "delete-attributes": "删除属性", - "enter-attribute-value": "输入属性值", - "show-on-widget": "在部件上显示", - "widget-mode": "部件模式", - "next-widget": "下一个部件", - "prev-widget": "上一个部件", - "add-to-dashboard": "添加到仪表板", - "add-widget-to-dashboard": "将部件添加到仪表板", - "selected-attributes": "{ count, plural, 1 {1 属性} other {# 属性} } 被选中", - "selected-telemetry": "{ count, plural, 1 {1 遥测} other {# 遥测} } 被选中" - }, - "audit-log": { - "audit": "审计", - "audit-logs": "审计日志", - "timestamp": "时间戳", - "entity-type": "实体类型", - "entity-name": "实体名称", - "user": "用户", - "type": "类型", - "status": "状态", - "details": "详情", - "type-added": "添加", - "type-deleted": "删除", - "type-updated": "更新", - "type-attributes-updated": "更新属性", - "type-attributes-deleted": "删除属性", - "type-rpc-call": "RPC调用", - "type-credentials-updated": "更新凭证", - "type-assigned-to-customer": "分配给客户", - "type-unassigned-from-customer": "未分配给客户", - "type-activated": "激活", - "type-suspended": "暂停", - "type-credentials-read": "读取凭证", - "type-attributes-read": "读取属性", - "status-success": "成功", - "status-failure": "失败", - "audit-log-details": "审计日志详情", - "no-audit-logs-prompt": "找不到日志", - "action-data": "活动数据", - "failure-details": "失败详情", - "search": "查找审计日志", - "clear-search": "清空查找" - }, - "confirm-on-exit": { - "message": "您有未保存的更改。确定要离开此页吗?", - "html-message": "您有未保存的更改。
    确定要离开此页面吗?", - "title": "未保存的更改" - }, - "contact": { - "country": "国家", - "city": "城市", - "state": "州", - "postal-code": "邮政编码", - "postal-code-invalid": "只允许数字。", - "address": "地址", - "address2": "地址2", - "phone": "手机", - "email": "邮箱", - "no-address": "无地址" - }, - "common": { - "username": "用户名", - "password": "密码", - "enter-username": "输入用户名", - "enter-password": "输入密码", - "enter-search": "输入检索条件" - }, - "content-type": { - "json": "Json", - "text": "Text", - "binary": "Binary (Base64)" - }, - "customer": { - "customer": "客户", - "customers": "客户", - "management": "客户管理", - "dashboard": "客户仪表板", - "dashboards": "客户仪表板", - "devices": "客户设备", - "entity-views": "客户实体视图", - "assets": "客户资产", - "public-dashboards": "公共仪表板", - "public-devices": "公共设备", - "public-assets": "公共资产", - "public-entity-views": "公共实体视图", - "add": "添加客户", - "delete": "删除客户", - "manage-customer-users": "管理客户用户", - "manage-customer-devices": "管理客户设备", - "manage-customer-dashboards": "管理客户仪表板", - "manage-public-devices": "管理公共设备", - "manage-public-dashboards": "管理公共仪表板", - "manage-customer-assets": "管理客户资产", - "manage-public-assets": "管理公共资产", - "add-customer-text": "添加新客户", - "no-customers-text": "没有找到客户", - "customer-details": "客户详情", - "delete-customer-title": "您确定要删除客户'{{customerTitle}}'吗?", - "delete-customer-text": "小心!确认后,客户及其所有相关数据将不可恢复。", - "delete-customers-title": "您确定要删除 { count, plural, 1 {1 客户} other {# 客户} }吗?", - "delete-customers-action-title": "删除 { count, plural, 1 {1 客户} other {# 客户} }", - "delete-customers-text": "小心!确认后,所有选定的客户将被删除,所有相关数据将不可恢复。", - "manage-users": "管理用户", - "manage-assets": "管理资产", - "manage-devices": "管理设备", - "manage-dashboards": "管理仪表板", - "title": "标题", - "title-required": "需要标题", - "description": "描述", - "details": "详情", - "events": "事件", - "copyId": "复制客户ID", - "idCopiedMessage": "客户ID已复制到粘贴板", - "select-customer": "选择客户", - "no-customers-matching": "没有找到匹配 '{{entity}}' 的客户。", - "customer-required": "客户是必选项", - "select-default-customer": "选择默认的客户", - "default-customer": "默认客户", - "default-customer-required": "为了调试租户级别上的仪表板,需要默认客户。" - }, - "datetime": { - "date-from": "日期从", - "time-from": "时间从", - "date-to": "日期到", - "time-to": "时间到" - }, - "dashboard": { - "dashboard": "仪表板", - "dashboards": "仪表板库", - "management": "仪表板管理", - "view-dashboards": "查看仪表板", - "add": "添加仪表板", - "assign-dashboard-to-customer": "将仪表板分配给客户", - "assign-dashboard-to-customer-text": "请选择要分配给客户的仪表板", - "assign-to-customer-text": "请选择客户分配仪表板", - "assign-to-customer": "分配给客户", - "unassign-from-customer": "取消分配客户", - "make-public": "仪表板设为公开", - "make-private": "仪表板设为私有", - "manage-assigned-customers": "管理已分配的客户", - "assigned-customers": "已分配的客户", - "assign-to-customers": "将仪表板分配给客户", - "assign-to-customers-text": "请选择客户指定仪表板", - "unassign-from-customers": "客户未分配仪表板", - "unassign-from-customers-text": "请选择从仪表板中取消分配的客户", - "no-dashboards-text": "没有找到仪表板", - "no-widgets": "没有配置部件", - "add-widget": "添加新的部件", - "title": "标题", - "select-widget-title": "选择部件", - "select-widget-subtitle": "可用的部件类型列表", - "delete": "删除仪表板", - "title-required": "需要标题。", - "description": "描述", - "details": "详情", - "dashboard-details": "仪表板详情", - "add-dashboard-text": "添加新的仪表板", - "assign-dashboards": "分配仪表板", - "assign-new-dashboard": "分配新的仪表板", - "assign-dashboards-text": "分配 { count, plural, 1 {1 仪表板} other {# 仪表板} } 给客户", - "unassign-dashboards-action-text": "未分配 { count, plural, 1 {1 仪表板} other {# 仪表板} } 给客户", - "delete-dashboards": "删除仪表板", - "unassign-dashboards": "取消分配仪表板", - "unassign-dashboards-action-title": "从客户处取消分配 { count, plural, 1 {1 仪表板} other {# 仪表板} } ", - "delete-dashboard-title": "您确定要删除仪表板 '{{dashboardTitle}}'吗?", - "delete-dashboard-text": "小心!确认后仪表板及其所有相关数据将不可恢复。", - "delete-dashboards-title": "你确定你要删除 { count, plural, 1 {1 仪表板} other {# 仪表板} }吗?", - "delete-dashboards-action-title": "删除 { count, plural, 1 {1 仪表板} other {# 仪表板} }", - "delete-dashboards-text": "小心!确认后所有选定的仪表板将被删除,所有相关数据将不可恢复。", - "unassign-dashboard-title": "您确定要取消分配仪表板 '{{dashboardTitle}}'吗?", - "unassign-dashboard-text": "确认后,面板将被取消分配,客户将无法访问。", - "unassign-dashboard": "取消分配仪表板", - "unassign-dashboards-title": "您确定要取消分配仪表板 { count, plural, 1 {1 仪表板} other {# 仪表板} } 吗?", - "unassign-dashboards-text": "确认后,所有选定的仪表板将被取消分配,客户将无法访问。", - "public-dashboard-title": "仪表板现已公布", - "public-dashboard-text": "你的仪表板 {{dashboardTitle}} 已被公开,可通过如下 链接访问:", - "public-dashboard-notice": "提示: 不要忘记将相关设备公开以访问其数据。", - "make-private-dashboard-title": "您确定要将仪表板 '{{dashboardTitle}}' 设为私有吗?", - "make-private-dashboard-text": "确认后,仪表板将被设为私有,不能被其他人访问。", - "make-private-dashboard": "仪表板设为私有", - "socialshare-text": "'{{dashboardTitle}}' 由Thingsboard提供支持", - "socialshare-title": "'{{dashboardTitle}}' 由Thingsboard提供支持", - "select-dashboard": "选择仪表板", - "no-dashboards-matching": "找不到符合 '{{entity}}' 的仪表板。", - "dashboard-required": "仪表板必填。", - "select-existing": "选择现有仪表板", - "create-new": "创建新的仪表板", - "new-dashboard-title": "新仪表板标题", - "open-dashboard": "打开仪表板", - "set-background": "设置背景", - "background-color": "背景颜色", - "background-image": "背景图片", - "background-size-mode": "背景大小模式", - "no-image": "无图像选择", - "drop-image": "拖拽图像或单击以选择要上传的文件。", - "settings": "设置", - "columns-count": "列数", - "columns-count-required": "需要列数。", - "min-columns-count-message": "只允许最少10列", - "max-columns-count-message": "只允许最多1000列", - "widgets-margins": "部件间边距", - "horizontal-margin": "水平边距", - "horizontal-margin-required": "需要水平边距值。", - "min-horizontal-margin-message": "只允许0作为最小水平边距值。", - "max-horizontal-margin-message": "只允许50作为最大水平边距值。", - "vertical-margin": "垂直边距", - "vertical-margin-required": "需要垂直边距值。", - "min-vertical-margin-message": "只允许0作为最小垂直边距值。", - "max-vertical-margin-message": "只允许50作为最大垂直边距值。", - "autofill-height": "自动填充布局高度", - "mobile-layout": "移动端布局设置", - "mobile-row-height": "移动端行高距(px)", - "mobile-row-height-required": "移动端行高距必填。", - "min-mobile-row-height-message": "移动端行高距至少5px。", - "max-mobile-row-height-message": "移动端行高距最多200px。", - "display-title": "显示仪表板标题", - "toolbar-always-open": "工具栏常驻", - "title-color": "标题颜色", - "display-dashboards-selection": "显示仪表板选项", - "display-entities-selection": "显示实体选项", - "display-dashboard-timewindow": "显示时间窗口", - "display-dashboard-export": "显示导出", - "import": "导入仪表板", - "export": "导出仪表板", - "export-failed-error": "无法导出仪表板: {{error}}", - "create-new-dashboard": "创建新的仪表板", - "dashboard-file": "仪表板文件", - "invalid-dashboard-file-error": "无法导入仪表板: 仪表板数据结构无效。", - "dashboard-import-missing-aliases-title": "配置导入仪表板使用的别名", - "create-new-widget": "创建新部件", - "import-widget": "导入部件", - "widget-file": "部件文件", - "invalid-widget-file-error": "无法导入窗口部件: 窗口部件数据结构无效。", - "widget-import-missing-aliases-title": "配置导入的窗口部件使用的别名", - "open-toolbar": "打开仪表板工具栏", - "close-toolbar": "关闭工具栏", - "configuration-error": "配置错误", - "alias-resolution-error-title": "仪表板别名配置错误", - "invalid-aliases-config": "无法找到与某些别名过滤器匹配的任何设备。
    请联系您的管理员以解决此问题。", - "select-devices": "选择设备", - "assignedToCustomer": "分配给客户", - "public": "公共", - "public-link": "公共链接", - "copy-public-link": "复制公共链接", - "public-link-copied-message": "仪表板的公共链接已被复制到剪贴板", - "manage-states": "仪表板状态管理", - "states": "仪表板状态", - "search-states": "仪表板状态检索", - "selected-states": "{ count, plural, 1 {1 仪表板状态} other {# 仪表板状态} } 选中", - "edit-state": "仪表板状态编辑", - "delete-state": "删除仪表板状态", - "add-state": "添加仪表板状态", - "state": "仪表板状态", - "state-name": "状态名", - "state-name-required": "仪表板状态名必填。", - "state-id": "状态ID", - "state-id-required": "仪表板状态ID必填。", - "state-id-exists": "仪表板状态ID已经存在。", - "is-root-state": "根状态", - "delete-state-title": "删除仪表板状态", - "delete-state-text": "确定要删除仪表板状态 '{{stateName}}' 吗?", - "show-details": "显示详情", - "hide-details": "隐藏详情", - "select-state": "选择目标状态", - "state-controller": "状态控制" - }, - "datakey": { - "settings": "设置", - "advanced": "高级", - "label": "标签", - "color": "颜色", - "units": "单位符号", - "decimals": "小数位数", - "data-generation-func": "数据生成功能", - "use-data-post-processing-func": "使用数据后处理功能", - "configuration": "数据键配置", - "timeseries": "时间序列", - "attributes": "属性", - "alarm": "报警字段", - "timeseries-required": "需要设备时间序列。", - "timeseries-or-attributes-required": "设备时间/属性必填。", - "maximum-timeseries-or-attributes": "最大允许 { count, plural, 1 {1 时间序列/属性} other {# 时间序列/属性} }", - "alarm-fields-required": "警告字段必填。", - "function-types": "函数类型", - "function-types-required": "需要函数类型。", - "maximum-function-types": "至少需要 { count, plural, 1 {1 函数类型} other {# 函数类型} }" - }, - "datasource": { - "type": "数据源类型", - "name": "数据源名称", - "add-datasource-prompt": "请添加数据源" - }, - "details": { - "edit-mode": "编辑模式", - "toggle-edit-mode": "切换编辑模式" - }, - "device": { - "device": "设备", - "device-required": "设备必填", - "devices": "设备", - "management": "设备管理", - "view-devices": "查看设备", - "device-alias": "设备别名", - "aliases": "设备别名", - "no-alias-matching": "'{{alias}}' 没有找到。", - "no-aliases-found": "找不到别名。", - "no-key-matching": "'{{key}}' 没有找到。", - "no-keys-found": "找不到密钥。", - "create-new-alias": "创建一个新的!", - "create-new-key": "创建一个新的!", - "duplicate-alias-error": "找到重复别名 '{{alias}}'。
    设备别名必须是唯一的。", - "configure-alias": "配置 '{{alias}}' 别名", - "no-devices-matching": "找不到与 '{{entity}}' 匹配的设备。", - "alias": "别名", - "alias-required": "需要设备别名。", - "remove-alias": "删除设备别名", - "add-alias": "添加设备别名", - "name-starts-with": "名称前缀", - "device-list": "设备列表", - "use-device-name-filter": "使用过滤器", - "device-list-empty": "没有被选中的设备", - "device-name-filter-required": "设备名称过滤器必填。", - "device-name-filter-no-device-matched": "找不到以'{{device}}' 开头的设备。", - "add": "添加设备", - "assign-to-customer": "分配给客户", - "assign-device-to-customer": "将设备分配给客户", - "assign-device-to-customer-text": "请选择要分配给客户的设备", - "make-public": "公开", - "make-private": "私有", - "no-devices-text": "找不到设备", - "assign-to-customer-text": "请选择客户分配设备", - "device-details": "设备详细信息", - "add-device-text": "添加新设备", - "credentials": "凭据", - "manage-credentials": "管理凭据", - "delete": "删除设备", - "assign-devices": "分配设备", - "assign-devices-text": "将{count,plural,1 {1 设备} other {# 设备}}分配给客户", - "delete-devices": "删除设备", - "unassign-from-customer": "取消分配客户", - "unassign-devices": "取消分配设备", - "unassign-devices-action-title": "从客户处取消分配{count,plural,1 {1 设备} other {# 设备}}", - "assign-new-device": "分配新设备", - "make-public-device-title": "您确定要将设备 '{{deviceName}}' 设为公开吗?", - "make-public-device-text": "确认后,设备及其所有数据将被设为公开并可被其他人访问。", - "make-private-device-title": "您确定要将设备 '{{deviceName}}' 设为私有吗?", - "make-private-device-text": "确认后,设备及其所有数据将被设为私有,不被其他人访问。", - "view-credentials": "查看凭据", - "delete-device-title": "您确定要删除设备的{{deviceName}}吗?", - "delete-device-text": "小心!确认后设备及其所有相关数据将不可恢复。", - "delete-devices-title": "您确定要删除{count,plural,1 {1 设备} other {# 设备}} 吗?", - "delete-devices-action-title": "删除 {count,plural,1 {1 设备} other {# 设备}}", - "delete-devices-text": "小心!确认后所有选定的设备将被删除,所有相关数据将不可恢复。", - "unassign-device-title": "您确定要取消分配设备 '{{deviceName}}'?", - "unassign-device-text": "确认后,设备将被取消分配,客户将无法访问。", - "unassign-device": "取消分配设备", - "unassign-devices-title": "您确定要取消分配{count,plural,1 {1 设备} other {# 设备}} 吗?", - "unassign-devices-text": "确认后,所有选定的设备将被取消分配,并且客户将无法访问。", - "device-credentials": "设备凭据", - "credentials-type": "凭据类型", - "access-token": "访问令牌", - "access-token-required": "需要访问令牌", - "access-token-invalid": "访问令牌长度必须为1到20个字符。", - "rsa-key": "RSA公钥", - "rsa-key-required": "需要RSA公钥", - "secret": "密钥", - "secret-required": "密钥必填", - "device-type": "设备类型", - "device-type-required": "设备类型必填。", - "select-device-type": "选择设备类型", - "enter-device-type": "输入设备类型", - "any-device": "任意设备", - "no-device-types-matching": "没有找到符合 '{{entitySubtype}}' 的设备类型。", - "device-type-list-empty": "未选择设备类型", - "device-types": "设备类型", - "name": "名称", - "name-required": "名称必填。", - "description": "说明", - "events": "事件", - "details": "详细信息", - "copyId": "复制设备ID", - "copyAccessToken": "复制访问令牌", - "idCopiedMessage": "设备ID已复制到剪贴板", - "accessTokenCopiedMessage": "设备访问令牌已复制到剪贴板", - "assignedToCustomer": "分配给客户", - "unable-delete-device-alias-title": "无法删除设备别名", - "unable-delete-device-alias-text": "设备别名 '{{deviceAlias}}' 不能够被删除,因为它被下列部件所使用:
    {{widgetsList}}", - "is-gateway": "是网关", - "public": "公开", - "device-public": "设备公开", - "select-device": "选择设备" - }, - "dialog": { - "close": "关闭对话框" - }, - "error": { - "unable-to-connect": "无法连接到服务器!请检查您的互联网连接。", - "unhandled-error-code": "未处理的错误代码: {{errorCode}}", - "unknown-error": "未知错误" - }, - "entity": { - "entity": "实体", - "entities": "实体", - "aliases": "实体别名", - "entity-alias": "实体别名", - "unable-delete-entity-alias-title": "无法删除实体别名", - "unable-delete-entity-alias-text": "实体别名 '{{entityAlias}}' 被以下部件使用不能删除:
    {{widgetsList}}", - "duplicate-alias-error": "别名 '{{alias}}' 重复。
    同一仪表板别名必须唯一。", - "missing-entity-filter-error": "别名 '{{alias}}' 缺少过滤器", - "configure-alias": "别名 '{{alias}}' 配置", - "alias": "别名", - "alias-required": "实体别名必填。", - "remove-alias": "移除实体别名", - "add-alias": "添加实体别名", - "entity-list": "实体列表", - "entity-type": "实体类型", - "entity-types": "实体类型", - "entity-type-list": "实体类型列表", - "any-entity": "任意实体", - "enter-entity-type": "输入实体类型", - "no-entities-matching": "没有找到符合 '{{entity}}' 的实体。", - "no-entity-types-matching": "没有找到符合 '{{entityType}}' 类型的实体。", - "name-starts-with": "名称开始于", - "use-entity-name-filter": "用户过滤", - "entity-list-empty": "没有选择实体。", - "entity-type-list-empty": "没有选择实体类型。", - "entity-name-filter-required": "实体名过滤器必填。", - "entity-name-filter-no-entity-matched": "没有找到以 '{{entity}}' 开头的实体", - "all-subtypes": "所有", - "select-entities": "选择实体", - "no-aliases-found": "没有找到别名", - "no-alias-matching": "没有找到 '{{alias}}'", - "create-new-alias": "创建新别名", - "key": "键", - "key-name": "键名", - "no-keys-found": "没有找到键", - "no-key-matching": "没有找到键 '{{key}}'", - "create-new-key": "创建新键", - "type": "类型", - "type-required": "实体类型必填。", - "type-device": "设备", - "type-devices": "设备", - "list-of-devices": "{ count, plural, 1 {设备} other {# 设备列表} }", - "device-name-starts-with": "以 '{{prefix}}' 开头的设备", - "type-asset": "资产", - "type-assets": "资产", - "list-of-assets": "{ count, plural, 1 {资产} other {# 资产列表} }", - "asset-name-starts-with": "以 '{{prefix}}' 开头的资产", - "type-entity-view": "实体视图", - "type-entity-views": "实体视图", - "list-of-entity-views": "{ count, plural, 1 {实体视图} other {# 实体视图列表} }", - "entity-view-name-starts-with": "以 '{{prefix}}' 开头的实体视图", - "type-rule": "规则", - "type-rules": "规则", - "list-of-rules": "{ count, plural, 1 {规则} other {# 规则列表} }", - "rule-name-starts-with": "以 '{{prefix}}' 开头的规则", - "type-plugin": "插件", - "type-plugins": "插件", - "list-of-plugins": "{ count, plural, 1 {插件} other {# 插件列表} }", - "plugin-name-starts-with": "以 '{{prefix}}' 开头的插件", - "type-tenant": "租户", - "type-tenants": "租户", - "list-of-tenants": "{ count, plural, 1 {租户} other {# 租户列表} }", - "tenant-name-starts-with": "以 '{{prefix}}' 开头的租户", - "type-customer": "客户", - "type-customers": "客户", - "list-of-customers": "{ count, plural, 1 {客户} other {# 客户列表} }", - "customer-name-starts-with": "以 '{{prefix}}' 开头的客户", - "type-user": "用户", - "type-users": "用户", - "list-of-users": "{ count, plural, 1 {用户} other {# 用户列表} }", - "user-name-starts-with": "以 '{{prefix}}' 开头的用户", - "type-dashboard": "仪表板", - "type-dashboards": "仪表板", - "list-of-dashboards": "{ count, plural, 1 {仪表板} other {# 仪表板列表} }", - "dashboard-name-starts-with": "以 '{{prefix}}' 开头的仪表板", - "type-alarm": "警告", - "type-alarms": "警告", - "list-of-alarms": "{ count, plural, 1 {警告} other {# 警告列表} }", - "alarm-name-starts-with": "以 '{{prefix}}' 开头的警告", - "type-rulechain": "规则链", - "type-rulechains": "规则链库", - "list-of-rulechains": "{ count, plural, 1 {一个规则链} other {# 规则链列表} }", - "rulechain-name-starts-with": "规则链前缀名称 '{{prefix}}'", - "type-current-customer": "当前客户", - "search": "实体检索", - "selected-entities": "{ count, plural, 1 {1 实体} other {# 实体} } 被选中", - "entity-name": "实体名", - "details": "实体详情", - "no-entities-prompt": "没有找到实体", - "no-data": "无数据" - }, - "entity-view": { - "entity-view": "实体视图", - "entity-view-required": "实体视图必填。", - "entity-views": "实体视图", - "management": "实体视图管理", - "view-entity-views": "查看实体视图", - "entity-view-alias": "实体视图别名", - "aliases": "实体视图别名", - "no-alias-matching": "'{{alias}}' 没有找到。", - "no-aliases-found": "找不到别名。", - "no-key-matching": "'{{key}}' 没有找到。", - "no-keys-found": "找不到密钥。", - "create-new-alias": "创建一个新的!", - "create-new-key": "创建一个新的!", - "duplicate-alias-error": "找到重复别名 '{{alias}}'。
    实体视图别名必须是唯一的。", - "configure-alias": "配置 '{{alias}}' 别名", - "no-devices-matching": "找不到与 '{{entity}}' 匹配的实体视图。", - "alias": "别名", - "alias-required": "需要实体视图别名。", - "remove-alias": "删除实体视图别名", - "add-alias": "添加实体视图别名", - "name-starts-with": "名称前缀", - "entity-view-list": "实体视图列表", - "use-entity-view-name-filter": "使用过滤器", - "entity-view-list-empty": "没有被选中的实体视图", - "entity-view-name-filter-required": "实体视图名称过滤器必填。", - "entity-view-name-filter-no-entity-view-matched": "找不到以'{{entityView}}' 开头的实体视图。", - "add": "添加实体视图", - "assign-to-customer": "分配给客户", - "assign-entity-view-to-customer": "将实体视图分配给客户", - "assign-entity-view-to-customer-text": "请选择要分配给客户的实体视图", - "no-entity-views-text": "找不到实体视图", - "assign-to-customer-text": "请选择客户分配实体视图", - "entity-view-details": "实体视图详细信息", - "add-entity-view-text": "添加新实体视图", - "delete": "删除实体视图", - "assign-entity-views": "分配实体视图", - "assign-entity-views-text": "分配 { count, plural, 1 {1 实体视图} other {# 实体视图} } 给客户", - "delete-entity-views": "删除实体视图", - "unassign-from-customer": "取消分配客户", - "unassign-entity-views": "取消分配实体视图", - "unassign-entity-views-action-title": "从客户处取消分配{count,plural,1 {1 实体视图} other {# 实体视图}}", - "assign-new-entity-view": "分配新实体视图", - "delete-entity-view-title": "确定要删除实体视图 '{{entityViewName}}'?", - "delete-entity-view-text": "小心!确认后实体视图及其所有相关数据将不可恢复。", - "delete-entity-views-title": "确定要删除 { count, plural, 1 {1 实体视图} other {# 实体视图} }?", - "delete-entity-views-action-title": "删除 { count, plural, 1 {1 实体视图} other {# 实体视图} }", - "delete-entity-views-text": "B小心,确认后,所有选定的实体视图将被删除,所有相关的数据将变得不可恢复。", - "unassign-entity-view-title": "您确定要取消对 '{{entityViewName}}'实体视图的分配吗?", - "unassign-entity-view-text": "确认后,实体视图将未分配,客户无法访问。", - "unassign-entity-view": "未分配实体视图", - "unassign-entity-views-title": "您确定要取消分配 { count, plural, 1 {1 实体视图} other {# 实体视图} }吗?", - "unassign-entity-views-text": "确认后,所有选定的实体视图将被分配,客户无法访问。", - "entity-view-type": "实体视图类型", - "entity-view-type-required": "实体视图类型必填。", - "select-entity-view-type": "选择实体视图类型", - "enter-entity-view-type": "输入实体视图类型", - "any-entity-view": "任何实体视图", - "no-entity-view-types-matching": "没有找到匹配 '{{entitySubtype}}' 的实体视图类型。", - "entity-view-type-list-empty": "实体视图类型未选择。", - "entity-view-types": "实体视图类型", - "name": "名称", - "name-required": "名称必填。", - "description": "描述", - "events": "事件", - "details": "详情", - "copyId": "复制实体视图ID", - "assignedToCustomer": "分配给客户", - "unable-entity-view-device-alias-title": "无法删除实体视图别名", - "unable-entity-view-device-alias-text": "实体视图别名 '{{entityViewAlias}}' 不能够被删除,因为它被下列部件所使用:
    {{widgetsList}}", - "select-entity-view": "选择实体视图", - "make-public": "实体视图设为公开", - "make-private": "实体视图设为私有", - "start-date": "开始日期", - "start-ts": "开始时间", - "end-date": "结束日期", - "end-ts": "结束时间", - "date-limits": "日期限制", - "client-attributes": "客户端属性", - "shared-attributes": "共享属性", - "server-attributes": "服务端属性", - "timeseries": "时间序列", - "client-attributes-placeholder": "客户端属性", - "shared-attributes-placeholder": "共享属性", - "server-attributes-placeholder": "服务端属性", - "timeseries-placeholder": "时间序列", - "target-entity": "目标实体", - "attributes-propagation": "属性传播", - "attributes-propagation-hint": "每次保存或更新这个实体视图时,实体视图将自动从目标实体复制指定的属性。由于性能原因,目标实体属性不会在每次属性更改时传播到实体视图。您可以通过配置\"copy to view\"规则链中的规则节点,并将\"Post attributes\"和\"attributes Updated\"消息链接到新规则节点,从而启用自动传播。", - "timeseries-data": "时间序列数据", - "timeseries-data-hint": "配置目标实体的时间序列数据键,以便实体视图可以访问这些键。这个时间序列数据是只读的。", - "make-public-entity-view-title": "你确定你想创建公开 '{{entityViewName}}' 实体视图?", - "make-public-entity-view-text": "确认后,实体视图 及其所有数据将被公开并被他人访问。", - "make-private-entity-view-title": "你确定你想创建私有 '{{entityViewName}}' 实体视图?", - "make-private-entity-view-text": "确认后,实体视图及其所有数据将被私有化,无法被他人访问。" - }, - "event": { - "event-type": "事件类型", - "type-error": "错误", - "type-lc-event": "生命周期事件", - "type-stats": "类型统计", - "type-debug-rule-node": "调试", - "type-debug-rule-chain": "调试", - "no-events-prompt": "找不到事件", - "error": "错误", - "alarm": "报警", - "event-time": "事件时间", - "server": "服务器", - "body": "整体", - "method": "方法", - "type": "类型", - "entity": "实体", - "message-id": "消息ID", - "message-type": "消息类型", - "data-type": "数据类型", - "relation-type": "关系类型", - "metadata": "元数据", - "data": "数据", - "event": "事件", - "status": "状态", - "success": "成功", - "failed": "失败", - "messages-processed": "消息处理", - "errors-occurred": "错误发生" - }, - "extension": { - "extensions": "扩展", - "selected-extensions": "{ count, plural, 1 {1 扩展} other {# 扩展} } 被选择", - "type": "类型", - "key": "键名", - "value": "值", - "id": "ID", - "extension-id": "扩展ID", - "extension-type": "扩展类型", - "transformer-json": "JSON *", - "unique-id-required": "当前扩展ID已经存在。", - "delete": "删除扩展", - "add": "添加扩展", - "edit": "编辑扩展", - "delete-extension-title": "确实要删除扩展名'{{extensionId}}'吗?", - "delete-extension-text": "小心,确认后,扩展和所有相关数据将变得不可恢复。", - "delete-extensions-title": "您确定要删除 { count, plural, 1 {1 表达式} other {# 表达式} }吗?", - "delete-extensions-text": "小心,确认后,所有选定的扩展将被删除。", - "converters": "转换器", - "converter-id": "转换器序号", - "configuration": "配置", - "converter-configurations": "转换器的配置", - "token": "安全令牌", - "add-converter": "添加转换器", - "add-config": "添加转换器配置", - "device-name-expression": "设备名称表达式", - "device-type-expression": "设备类型表达式", - "custom": "顾客", - "to-double": "加倍", - "transformer": "转换器", - "json-required": "转换器JSON必填。", - "json-parse": "无法解析转换器JSON。", - "attributes": "属性", - "add-attribute": "添加属性", - "add-map": "添加映射元素", - "timeseries": "时间序列", - "add-timeseries": "添加时间序列", - "field-required": "必填字段", - "brokers": "代理服务器组", - "add-broker": "添加代理服务器", - "host": "主机", - "port": "端口", - "port-range": "端口应该在1到65535的范围内。", - "ssl": "Ssl", - "credentials": "证书", - "username": "用户名", - "password": "密码", - "retry-interval": "以毫秒为单位的重试间隔", - "anonymous": "匿名", - "basic": "Basic", - "pem": "PEM", - "ca-cert": "CA证书文件 *", - "private-key": "私钥文件 *", - "cert": "证书文件 *", - "no-file": "没有选择文件。", - "drop-file": "删除文件或单击以选择要上载的文件。", - "mapping": "映射", - "topic-filter": "主题滤波", - "converter-type": "转换类型", - "converter-json": "Json", - "json-name-expression": "设备名称JSON表达式", - "topic-name-expression": "设备名称主题表达式", - "json-type-expression": "设备类型JSON表达式", - "topic-type-expression": "设备类型主题表达式", - "attribute-key-expression": "属性关键字表达式", - "attr-json-key-expression": "属性键JSON表达式", - "attr-topic-key-expression": "属性关键字主题表达式", - "request-id-expression": "请求ID表达式", - "request-id-json-expression": "请求ID JSON表达式", - "request-id-topic-expression": "请求ID主题表达式", - "response-topic-expression": "响应主题表达式", - "value-expression": "值表达式", - "topic": "主题", - "timeout": "毫秒超时", - "converter-json-required": "转换JSON是必需的。", - "converter-json-parse": "无法解析转换JSON。", - "filter-expression": "过滤表达式", - "connect-requests": "连接请求", - "add-connect-request": "添加连接请求", - "disconnect-requests": "断开请求", - "add-disconnect-request": "添加断开请求", - "attribute-requests": "属性请求", - "add-attribute-request": "添加属性请求", - "attribute-updates": "属性更新", - "add-attribute-update": "添加属性更新", - "server-side-rpc": "服务端RPC", - "add-server-side-rpc-request": "添加服务端RPC请求", - "device-name-filter": "设备名称滤波", - "attribute-filter": "属性滤波", - "method-filter": "方法滤波", - "request-topic-expression": "请求主题表达式", - "response-timeout": "毫秒内响应超时", - "topic-expression": "主题表达", - "client-scope": "客户范围", - "add-device": "添加服务器", - "opc-server": "服务器组", - "opc-add-server": "添加服务器", - "opc-add-server-prompt": "请添加服务器", - "opc-application-name": "应用名称", - "opc-application-uri": "应用URI", - "opc-scan-period-in-seconds": "秒级扫描周期", - "opc-security": "安全性", - "opc-identity": "身份", - "opc-keystore": "密钥库", - "opc-type": "类型", - "opc-keystore-type": "类型", - "opc-keystore-location": "位置 *", - "opc-keystore-password": "密码", - "opc-keystore-alias": "别名", - "opc-keystore-key-password": "密钥密码", - "opc-device-node-pattern": "设备节点模式", - "opc-device-name-pattern": "设备名称模式", - "modbus-server": "Servers/slaves", - "modbus-add-server": "添加 server/slave", - "modbus-add-server-prompt": "请添加 server/slave", - "modbus-transport": "传输", - "modbus-port-name": "串口名称", - "modbus-encoding": "编码", - "modbus-parity": "奇偶性", - "modbus-baudrate": "波特率", - "modbus-databits": "数据位", - "modbus-stopbits": "停止位", - "modbus-databits-range": "数据位应该在7到8的范围内。", - "modbus-stopbits-range": "停止位应该在1到2的范围内。", - "modbus-unit-id": "单位编号", - "modbus-unit-id-range": "单位ID应该在1到247的范围内", - "modbus-device-name": "设备名称", - "modbus-poll-period": "轮询周期 (ms)", - "modbus-attributes-poll-period": "轮询属性周期 (ms)", - "modbus-timeseries-poll-period": "时间序列轮询周期 (ms)", - "modbus-poll-period-range": "轮询周期应为正值。", - "modbus-tag": "标签", - "modbus-function": "函数", - "modbus-register-address": "寄存器地址", - "modbus-register-address-range": "寄存器地址应该在0到65535的范围内。", - "modbus-register-bit-index": "位索引", - "modbus-register-bit-index-range": "位索引应该在0到15的范围内。", - "modbus-register-count": "寄存器计数", - "modbus-register-count-range": "寄存器计数应该是一个正值。", - "modbus-byte-order": "字节顺序", - "sync": { - "status": "状态", - "sync": "同步", - "not-sync": "不同步", - "last-sync-time": "最后同步时间", - "not-available": "无法使用" - }, - "export-extensions-configuration": "导出扩展配置", - "import-extensions-configuration": "导入扩展配置", - "import-extensions": "导入扩展", - "import-extension": "导入扩展", - "export-extension": "导出扩展", - "file": "扩展文件", - "invalid-file-error": "无效的扩展文件" - }, - "fullscreen": { - "expand": "展开到全屏", - "exit": "退出全屏", - "toggle": "切换全屏模式", - "fullscreen": "全屏" - }, - "function": { - "function": "函数" - }, - "grid": { - "delete-item-title": "您确定要删除此项吗?", - "delete-item-text": "注意,确认后此项及其所有相关数据将变得不可恢复。", - "delete-items-title": "你确定你要删除 { count, plural, 1 {1 项} other {# 项} }吗?", - "delete-items-action-title": "删除 { count, plural, 1 {1 项} other {# 项} }", - "delete-items-text": "注意,确认后所有选择的项目将被删除,所有相关数据将不可恢复。", - "add-item-text": "添加新项目", - "no-items-text": "没有找到项目", - "item-details": "项目详细信息", - "delete-item": "删除项目", - "delete-items": "删除项目", - "scroll-to-top": "滚动到顶部" - }, - "help": { - "goto-help-page": "转到帮助页面" - }, - "home": { - "home": "首页", - "profile": "属性", - "logout": "注销", - "menu": "菜单", - "avatar": "头像", - "open-user-menu": "打开用户菜单" - }, - "import": { - "no-file": "没有选择文件", - "drop-file": "拖动一个JSON文件或者单击以选择要上传的文件。" - }, - "item": { - "selected": "选择" - }, - "js-func": { - "no-return-error": "函数必须返回值!", - "return-type-mismatch": "函数必须返回 '{{type}}' 类型的值!", - "tidy": "整洁" - }, - "key-val": { - "key": "键名", - "value": "值", - "remove-entry": "删除条目", - "add-entry": "增加条目", - "no-data": "没有条目" - }, - "layout": { - "layout": "布局", - "manage": "布局管理", - "settings": "布局设置", - "color": "颜色", - "main": "主体", - "right": "右侧", - "select": "选择目标布局" - }, - "legend": { - "position": "图例位置", - "show-max": "显示最大值", - "show-min": "显示最小值", - "show-avg": "显示平均值", - "show-total": "显示总数", - "settings": "图例设置", - "min": "最小值", - "max": "最大值", - "avg": "平均值", - "total": "总数" - }, - "login": { - "login": "登录", - "request-password-reset": "请求密码重置", - "reset-password": "重置密码", - "create-password": "创建密码", - "passwords-mismatch-error": "输入的密码必须相同!", - "password-again": "再次输入密码", - "sign-in": "登录 ", - "username": "用户名(电子邮件)", - "remember-me": "记住我", - "forgot-password": "忘记密码?", - "password-reset": "密码重置", - "new-password": "新密码", - "new-password-again": "再次输入新密码", - "password-link-sent-message": "密码重置链接已成功发送!", - "email": "电子邮件" - }, - "position": { - "top": "顶部", - "bottom": "底部", - "left": "左侧", - "right": "右侧" - }, - "profile": { - "profile": "属性", - "change-password": "更改密码", - "current-password": "当前密码" - }, - "relation": { - "relations": "关联", - "direction": "方向", - "search-direction": { - "FROM": "从", - "TO": "到" - }, - "direction-type": { - "FROM": "从", - "TO": "到" - }, - "from-relations": "向外的关联", - "to-relations": "向内的关联", - "selected-relations": "{ count, plural, 1 {1 关联} other {# 关联} } 被选中", - "type": "类型", - "to-entity-type": "到实体类型", - "to-entity-name": "到实体名称", - "from-entity-type": "从实体类型", - "from-entity-name": "从实体类型", - "to-entity": "到实体", - "from-entity": "从实体", - "delete": "删除关联", - "relation-type": "关联类型", - "relation-type-required": "关联类型必填", - "any-relation-type": "任意类型", - "add": "添加关联", - "edit": "编辑关联", - "delete-to-relation-title": "确定要删除实体 '{{entityName}}' 的关联吗?", - "delete-to-relation-text": "确定删除后实体 '{{entityName}}' 将取消与当前实体的关联关系。", - "delete-to-relations-title": "确定要删除 { count, plural, 1 {1 关联} other {# 关联} }?", - "delete-to-relations-text": "确定删除所有选择的关联关系后,与当前实体对应的所有关联关系将被移除。", - "delete-from-relation-title": "确定要从实体 '{{entityName}}' 删除关联吗?", - "delete-from-relation-text": "确定删除后,当前实体将与实体 '{{entityName}}' 取消关联", - "delete-from-relations-title": "确定删除 { count, plural, 1 {1 关联} other {# 关联} } 吗?", - "delete-from-relations-text": "确定删除所有选择的关联关系后,当前实体将与对应的实体取消关联", - "remove-relation-filter": "移除关联过滤器", - "add-relation-filter": "添加关联过滤器", - "any-relation": "任意关联", - "relation-filters": "关联过滤器", - "additional-info": "附加信息 (JSON)", - "invalid-additional-info": "无法解析附加信息json。" - }, - "rulechain": { - "rulechain": "规则链", - "rulechains": "规则链库", - "root": "根实体", - "delete": "删除规则", - "activate": "激活规则", - "suspend": "暂停规则", - "active": "激活", - "suspended": "暂停", - "name": "名称", - "name-required": "名称必填。", - "description": "描述", - "add": "添加规则", - "set-root": "创建规则链根", - "set-root-rulechain-title": "您确定要生成规则链'{{RuleChainName}}'根吗?", - "set-root-rulechain-text": "确认之后,规则链将变为根规格链,并将处理所有传入的传输消息。", - "delete-rulechain-title": " 确实要删除规则链'{{ruleChainName}}'吗?", - "delete-rulechain-text": "小心,在确认规则链和所有相关数据将变得不可恢复。", - "delete-rulechains-title": "确实要删除{count, plural, 1 { 1 规则链}其他{# 规则链库}}吗?", - "delete-rulechains-action-title": "删除 { count, plural, 1 {1 规则链} other {# 规则链库} }", - "delete-rulechains-text": "小心,确认后,所有选定的规则链将被删除,所有相关的数据将变得不可恢复。", - "add-rulechain-text": "添加新的规则链", - "no-rulechains-text": "规则链没有发现", - "rulechain-details": "规则链详情", - "details": "详情", - "events": "事件", - "system": "系统", - "import": "导入规则", - "export": "导出规则", - "export-failed-error": "无法导出规则:{{error}}", - "create-new-rulechain": "创建新的规则链", - "rulechain-file": "规则链文件", - "invalid-rulechain-file-error": "不能导入规则链:无效的规则链数据格式。", - "copyId": "复制规则链ID", - "idCopiedMessage": "规则ID已经复制到粘贴板", - "select-rulechain": "选择规则链", - "no-rulechains-matching": "没有发现匹配'{{entity}}'的规则链。", - "rulechain-required": "规则链必填", - "management": "规则集管理", - "debug-mode": "调试模式" - }, - "rulenode": { - "details": "详情", - "events": "事件", - "search": "搜索节点", - "open-node-library": "打开节点库", - "add": "添加规则节点", - "name": "名称", - "name-required": "名称必填。", - "type": "类型", - "description": "描述", - "delete": "删除规则节点", - "select-all-objects": "选择所有节点和连接", - "deselect-all-objects": "取消选择所有节点和连接", - "delete-selected-objects": "删除选定的节点和连接", - "delete-selected": "删除选定", - "select-all": "选择全部", - "copy-selected": "选择副本", - "deselect-all": "取消选择", - "rulenode-details": "规则节点详情", - "debug-mode": "调试模式", - "configuration": "配置", - "link": "链接", - "link-details": "规则节点链接详情", - "add-link": "添加链接", - "link-label": "链接标签", - "link-label-required": "链接标签必填", - "custom-link-label": "自定义链接标签", - "custom-link-label-required": "自定义链接标签必填", - "type-filter": "滤波器", - "type-filter-details": "使用配置条件过滤传入消息", - "type-enrichment": "属性集", - "type-enrichment-details": "向消息元数据中添加附加信息", - "type-transformation": "变换", - "type-transformation-details": "更改消息有效载荷和元数据", - "type-action": "动作", - "type-action-details": "执行特别动作", - "type-external": "外部的", - "type-external-details": "与外部系统交互", - "type-rule-chain": "规则链", - "type-rule-chain-details": "将传入消息转发到指定的规则链", - "type-input": "输入", - "type-input-details": "规则链的逻辑输入,将传入消息转发到下一个相关规则节点", - "directive-is-not-loaded": "定义的配置指令 '{{directiveName}}' 不可用。", - "ui-resources-load-error": "加载配置UI资源失败。", - "invalid-target-rulechain": "无法解析目标规则链!", - "test-script-function": "测试脚本功能", - "message": "消息", - "message-type": "消息类型", - "message-type-required": "消息类型必填", - "metadata": "元数据", - "metadata-required": "元数据项不能为空。", - "output": "输出", - "test": "测试", - "help": "帮助" - }, - "tenant": { - "tenant": "租户", - "tenants": "租户", - "management": "租户管理", - "add": "添加租户", - "admins": "管理员", - "manage-tenant-admins": "管理租户管理员", - "delete": "删除租户", - "add-tenant-text": "添加新租户", - "no-tenants-text": "没有找到租户", - "tenant-details": "租客详情", - "delete-tenant-title": "您确定要删除租户'{{tenantTitle}}'吗?", - "delete-tenant-text": "小心!确认后,租户和所有相关数据将不可恢复。", - "delete-tenants-title": "您确定要删除 {count,plural,1 {1 租户} other {# 租户}} 吗?", - "delete-tenants-action-title": "删除 { count, plural, 1 {1 租户} other {# 租户} }", - "delete-tenants-text": "小心!确认后,所有选定的租户将被删除,所有相关数据将不可恢复。", - "title": "标题", - "title-required": "标题必填。", - "description": "描述", - "details": "详情", - "events": "事件", - "copyId": "复制租户ID", - "idCopiedMessage": "租户ID已经复制到粘贴板", - "select-tenant": "选择租户", - "no-tenants-matching": "没有找到符合 '{{entity}}' 的租户", - "tenant-required": "租户必填" - }, - "timeinterval": { - "seconds-interval": "{ seconds, plural, 1 {1 秒} other {# 秒} }", - "minutes-interval": "{ minutes, plural, 1 {1 分} other {# 分} }", - "hours-interval": "{ hours, plural, 1 {1 小时} other {# 小时} }", - "days-interval": "{ days, plural, 1 {1 天} other {# 天} }", - "days": "天", - "hours": "时", - "minutes": "分", - "seconds": "秒", - "advanced": "高级" - }, - "timewindow": { - "days": "{ days, plural, 1 { 天 } other {# 天 } }", - "hours": "{ hours, plural, 0 { 小时 } 1 {1 小时 } other {# 小时 } }", - "minutes": "{ minutes, plural, 0 { 分 } 1 {1 分 } other {# 分 } }", - "seconds": "{ seconds, plural, 0 { 秒 } 1 {1 秒 } other {# 秒 } }", - "realtime": "实时", - "history": "历史", - "last-prefix": "最后", - "period": "从 {{ startTime }} 到 {{ endTime }}", - "edit": "编辑时间窗口", - "date-range": "日期范围", - "last": "最后", - "time-period": "时间段" - }, - "user": { - "user": "用户", - "users": "用户", - "customer-users": "客户用户", - "tenant-admins": "租户管理员", - "sys-admin": "系统管理员", - "tenant-admin": "租户管理员", - "customer": "客户", - "anonymous": "匿名", - "add": "添加用户", - "delete": "删除用户", - "add-user-text": "添加新用户", - "no-users-text": "找不到用户", - "user-details": "用户详细信息", - "delete-user-title": "您确定要删除用户 '{{userEmail}}' 吗?", - "delete-user-text": "小心!确认后,用户和所有相关数据将不可恢复。", - "delete-users-title": "你确定你要删除 { count, plural, 1 {1 用户} other {# 用户} } 吗?", - "delete-users-action-title": "删除 { count, plural, 1 {1 用户} other {# 用户} }", - "delete-users-text": "小心!确认后,所有选定的用户将被删除,所有相关数据将不可恢复。", - "activation-email-sent-message": "激活电子邮件已成功发送!", - "resend-activation": "重新发送激活", - "email": "电子邮件", - "email-required": "电子邮件必填。", - "invalid-email-format": "无效的邮件格式。", - "first-name": "名字", - "last-name": "姓", - "description": "描述", - "default-dashboard": "默认面板", - "always-fullscreen": "始终全屏", - "select-user": "选择用户", - "no-users-matching": "没有找到符合 '{{entity}}' 的用户。", - "user-required": "用户必填", - "activation-method": "激活方式", - "display-activation-link": "显示激活链接", - "send-activation-mail": "发送激活邮件", - "activation-link": "用户激活链接", - "activation-link-text": "使用该链接 激活 激活用户:", - "copy-activation-link": "复制用户激活链接", - "activation-link-copied-message": "用户激活链接已经复制到粘贴板", - "details": "详细信息" - }, - "value": { - "type": "值类型", - "string": "字符串", - "string-value": "字符串值", - "integer": "数字", - "integer-value": "数字值", - "invalid-integer-value": "整数值无效", - "double": "双精度小数", - "double-value": "双精度小数值", - "boolean": "布尔", - "boolean-value": "布尔值", - "false": "假", - "true": "真", - "long": "Long" - }, - "widget": { - "widget-library": "部件库", - "widget-bundle": "部件包", - "select-widgets-bundle": "选择部件包", - "management": "管理部件", - "editor": "部件编辑器", - "widget-type-not-found": "加载部件配置出错。
    可能关联的\n 部件已经删除了。", - "widget-type-load-error": "由于以下错误未加载小部件:", - "remove": "删除部件", - "edit": "编辑部件", - "remove-widget-title": "确实要删除 '{{widgetTitle}}'部件吗?", - "remove-widget-text": "确认后,控件和所有相关数据将变得不可恢复。", - "timeseries": "时间序列", - "search-data": "搜索数据", - "no-data-found": "没有找到数据", - "latest-values": "最新值", - "rpc": "控件部件", - "alarm": "警告部件", - "static": "静态部件", - "select-widget-type": "选择窗口部件类型", - "missing-widget-title-error": "部件标题必须指定!", - "widget-saved": "部件已保存", - "unable-to-save-widget-error": "无法保存部件!控件有错误!", - "save": "保存部件", - "saveAs": "部件另存为", - "save-widget-type-as": "部件类型另存为", - "save-widget-type-as-text": "请输入新的部件标题或选择目标部件包", - "toggle-fullscreen": "切换全屏", - "run": "运行部件", - "title": "部件标题", - "title-required": "需要部件标题。", - "type": "部件类型", - "resources": "资源", - "resource-url": "JavaScript/CSS URL", - "remove-resource": "删除资源", - "add-resource": "添加资源", - "html": "HTML", - "tidy": "整理", - "css": "CSS", - "settings-schema": "设置模式", - "datakey-settings-schema": "数据键设置模式", - "javascript": "Javascript", - "remove-widget-type-title": "您确定要删除部件类型 '{{widgetName}}'吗?", - "remove-widget-type-text": "确认后,窗口部件类型和所有相关数据将不可恢复。", - "remove-widget-type": "删除部件类型", - "add-widget-type": "添加新的部件类型", - "widget-type-load-failed-error": "无法加载部件类型!", - "widget-template-load-failed-error": "无法加载部件模板!", - "add": "添加部件", - "undo": "撤消部件更改", - "export": "导出部件" - }, - "widget-action": { - "header-button": "部件顶部按钮", - "open-dashboard-state": "切换到新仪表板状态", - "update-dashboard-state": "更新当前仪表板状态", - "open-dashboard": "切换到另一个仪表板", - "custom": "自定义动作", - "target-dashboard-state": "目标仪表板状态", - "target-dashboard-state-required": "目标仪表板状态必填", - "set-entity-from-widget": "从部件中设置实体", - "target-dashboard": "目标仪表板", - "open-right-layout": "打开右侧布局 (移动端视图)" - }, - "widgets-bundle": { - "current": "当前包", - "widgets-bundles": "部件包", - "add": "添加部件包", - "delete": "删除部件包", - "title": "标题", - "title-required": "标题必填。", - "add-widgets-bundle-text": "添加新的部件包", - "no-widgets-bundles-text": "找不到部件包", - "empty": "部件包是空的", - "details": "详情", - "widgets-bundle-details": "部件包详细信息", - "delete-widgets-bundle-title": "您确定要删除部件包 '{{widgetsBundleTitle}}'吗?", - "delete-widgets-bundle-text": "小心!确认后,部件包和所有相关数据将不可恢复。", - "delete-widgets-bundles-title": "你确定你要删除 { count, plural, 1 {1 部件包} other {# 部件包} } 吗?", - "delete-widgets-bundles-action-title": "删除 { count, plural, 1 {1 部件包} other {# 部件包} }", - "delete-widgets-bundles-text": "小心!确认后,所有选定的部件包将被删除,所有相关数据将不可恢复。", - "no-widgets-bundles-matching": "没有找到与 '{{widgetsBundle}}' 匹配的部件包。", - "widgets-bundle-required": "需要部件包。", - "system": "系统", - "import": "导入部件包", - "export": "导出部件包", - "export-failed-error": "无法导出部件包: {{error}}", - "create-new-widgets-bundle": "创建新的部件包", - "widgets-bundle-file": "部件包文件", - "invalid-widgets-bundle-file-error": "无法导入部件包:无效的部件包数据结构。" - }, - "widget-config": { - "data": "数据", - "settings": "设置", - "advanced": "高级", - "title": "标题", - "general-settings": "常规设置", - "display-title": "显示标题", - "drop-shadow": "阴影", - "enable-fullscreen": "启用全屏", - "background-color": "背景颜色", - "text-color": "文字颜色", - "padding": "填充", - "margin": "边缘", - "widget-style": "部件风格", - "title-style": "标题风格", - "mobile-mode-settings": "移动端设置", - "order": "顺序", - "height": "高度", - "units": "特殊符号展示值", - "decimals": "浮点数后的位数", - "timewindow": "时间窗口", - "use-dashboard-timewindow": "使用仪表板的时间窗口", - "display-legend": "显示图例", - "datasources": "数据源", - "maximum-datasources": "最大允许 { count, plural, 1 {1 数据} other {# 数据} }", - "datasource-type": "类型", - "datasource-parameters": "参数", - "remove-datasource": "移除数据源", - "add-datasource": "添加数据源", - "target-device": "目标设备", - "alarm-source": "警告源", - "actions": "动作", - "action": "动作", - "add-action": "添加动作", - "search-actions": "动作检索", - "action-source": "动作源", - "action-source-required": "动作源必填", - "action-name": "动作名称", - "action-name-required": "动作名称必填。", - "action-name-not-unique": "动作名称已经存在。
    统一动作源的动作名称必须唯一。", - "action-icon": "图标", - "action-type": "类型", - "action-type-required": "类型必填", - "edit-action": "编辑动作", - "delete-action": "删除动作", - "delete-action-title": "删除部件动作", - "delete-action-text": "确定要删除部件动作 '{{actionName}}' 吗?" - }, - "widget-type": { - "import": "导入部件类型", - "export": "导出部件类型", - "export-failed-error": "无法导出部件类型: {{error}}", - "create-new-widget-type": "创建新的部件类型", - "widget-type-file": "部件类型文件", - "invalid-widget-type-file-error": "无法导入部件类型:无效的部件类型数据结构。" - }, - "widgets": { - "date-range-navigator": { - "localizationMap": { - "Sun": "周日", - "Mon": "周一", - "Tue": "周二", - "Wed": "周三", - "Thu": "周四", - "Fri": "周五", - "Sat": "周六", - "Jan": "1月", - "Feb": "2月", - "Mar": "3月", - "Apr": "4月", - "May": "5月", - "Jun": "6月", - "Jul": "7月", - "Aug": "8月", - "Sep": "9月", - "Oct": "10月", - "Nov": "11月", - "Dec": "12月", - "January": "一月", - "February": "二月", - "March": "三月", - "April": "四月", - "June": "六月", - "July": "七月", - "August": "八月", - "September": "九月", - "October": "十月", - "November": "十一月", - "December": "十二月", - "Custom Date Range": "自定义日期范围", - "Date Range Template": "日期范围模板", - "Today": "今天", - "Yesterday": "昨天", - "This Week": "本星期", - "Last Week": "上个星期", - "This Month": "这个月", - "Last Month": "上个月", - "Year": "年", - "This Year": "今年", - "Last Year": "去年", - "Date picker": "日期选择器", - "Hour": "小时", - "Day": "天", - "Week": "周", - "2 weeks": "2周", - "Month": "月", - "3 months": "3个月", - "6 months": "6个月", - "Custom interval": "自定义间隔", - "Interval": "间隔", - "Step size": "步长", - "Ok": "Ok" - } - } - }, - "icon": { - "icon": "图标", - "select-icon": "选择图标", - "material-icons": "素材图标", - "show-all": "显示所有图标" - }, - "custom": { - "widget-action": { - "action-cell-button": "动作单元格按钮", - "row-click": "点击行", - "marker-click": "点击标记", - "polygon-click": "单击多边形", - "tooltip-tag-action": "提示标签动作" - } - }, - "language": { - "language": "语言" - } -} diff --git a/ui/src/app/locale/locale.constant-zh_TW.json b/ui/src/app/locale/locale.constant-zh_TW.json deleted file mode 100644 index 99693103f8..0000000000 --- a/ui/src/app/locale/locale.constant-zh_TW.json +++ /dev/null @@ -1,1607 +0,0 @@ -{ -    "access": { -        "unauthorized": "未授權", -        "unauthorized-access": "未授權存取", -        "unauthorized-access-text": "您需要登入才能存取這個資源!", -        "access-forbidden": "禁止存取", -        "access-forbidden-text": "您沒有存取此位置的權限
    如果您仍希望存取此位置,請嘗試使用其他用戶登入。", -        "refresh-token-expired": "Session 已過期", -        "refresh-token-failed": "無法更新 Session" -    }, -    "action": { -        "activate": "啟動", -        "suspend": "暫停", -        "save": "儲存", -        "saveAs": "另存為", -        "cancel": "取消", -        "ok": "確定", -        "delete": "刪除", -        "add": "增加", -        "yes": "是", -        "no": "否", -        "update": "更新", -        "remove": "移除", -        "search": "查詢", -        "clear-search": "清除查詢", -        "assign": "分配", -        "unassign": "取消分配", -        "share": "分享", -        "make-private": "私有", -        "apply": "應用", -        "apply-changes": "應用更改", -        "edit-mode": "編輯模式", -        "enter-edit-mode": "進入編輯模式", -        "decline-changes": "取消更改", -        "close": "關閉", -        "back": "返回", -        "run": "執行", -        "sign-in": "登入!", -        "edit": "編輯", -        "view": "查看", -        "create": "建立", -        "drag": "拖拉", -        "refresh": "更新", -        "undo": "取消", -        "copy": "複製", -        "paste": "貼上", -        "copy-reference": "複製引用", -        "paste-reference": "貼上引用", -        "import": "匯入", -        "export": "匯出", -        "share-via": "通過 {{provider}}分享" -    }, -    "aggregation": { -        "aggregation": "聚合", -        "function": "資料聚合功能", -        "limit": "最大值", -        "group-interval": "分組間隔", -        "min": "最少值", -        "max": "最大值", -        "avg": "平均值", -        "sum": "總計", -        "count": "Count", -        "none": "空" -    }, -    "admin": { -        "general": "一般", -        "general-settings": "一般設定", -        "outgoing-mail": "發送郵件", -        "outgoing-mail-settings": "發送郵件設定", -        "system-settings": "系統設定", -        "test-mail-sent": "測試郵件發送成功!", -        "base-url": "基本URL", -        "base-url-required": "基本URL必填。", -        "mail-from": "郵件來自", -        "mail-from-required": "郵件發件人必填。", -        "smtp-protocol": "SMTP協定", -        "smtp-host": "SMTP主機", -        "smtp-host-required": "SMTP主機必填。", -        "smtp-port": "SMTP連接埠", -        "smtp-port-required": "您必須提供一個smtp連接埠。", -        "smtp-port-invalid": "這看起來不是有效的smtp連接埠。", -        "timeout-msec": "超時(ms)", -        "timeout-required": "超時必填。", -        "timeout-invalid": "這看起來不像有效的超時值。", -        "enable-tls": "啟用TLS", - "tls-version": "TLS版本", -        "send-test-mail": "發送測試郵件" -    }, -    "alarm": { -        "alarm": "警告", -        "alarms": "警告", -        "select-alarm": "選擇警告", -        "no-alarms-matching": "沒有找到符合 '{{entity}}' 的警告", -        "alarm-required": "警告必填", -        "alarm-status": "警告狀態", -        "search-status": { -            "ANY": "所有", -            "ACTIVE": "已啟動", -            "CLEARED": "已清除", -            "ACK": "已回覆", -            "UNACK": "未回覆" -        }, -        "display-status": { -            "ACTIVE_UNACK": "啟動未回覆", -            "ACTIVE_ACK": "啟動已回覆", -            "CLEARED_UNACK": "清除未回覆", -            "CLEARED_ACK": "清除已回覆" -        }, -        "no-alarms-prompt": "未發現警告", -        "created-time": "建立時間", -        "type": "類型", -        "severity": "嚴重程度", -        "originator": "起因", -        "originator-type": "起因類型", -        "details": "詳細資訊", -        "status": "狀態", -        "alarm-details": "警告詳細資訊", -        "start-time": "開始時間", -        "end-time": "結束時間", -        "ack-time": "回覆時間", -        "clear-time": "建立時間", -        "severity-critical": "危險", -        "severity-major": "重要", -        "severity-minor": "次要", -        "severity-warning": "警告", -        "severity-indeterminate": "不確定", -        "acknowledge": "回覆", -        "clear": "清除", -        "search": "搜尋警告", -        "selected-alarms": "已選擇 { count, plural, 1 {1 警告} other {# 警告} } ", -        "no-data": "無資料顯示", -        "polling-interval": "警告輪詢間隔(秒)", -        "polling-interval-required": "警告輪詢間隔必填。", -        "min-polling-interval-message": "輪詢間隔至少是1秒。", -        "aknowledge-alarms-title": "回覆 { count, plural, 1 {1 警告} other {# 警告} }", -        "aknowledge-alarms-text": "確定要回覆 { count, plural, 1 {1 警告} other {# 警告} }?", -        "clear-alarms-title": "清除 { count, plural, 1 {1 警告} other {# 警告} }", -        "clear-alarms-text": "確定要清除 { count, plural, 1 {1 警告} other {# 警告} }?" -    }, -    "alias": { -        "add": "增加別名", -        "edit": "編輯別名", -        "name": "別名", -        "name-required": "別名必填", -        "duplicate-alias": "別名已經存在。", -        "filter-type-single-entity": "單一實體", -        "filter-type-entity-list": "實體列表", -        "filter-type-entity-name": "實體名稱", -        "filter-type-state-entity": "實體(儀表板狀態)", -        "filter-type-state-entity-description": "實體令牌(儀表板狀態參數)", -        "filter-type-asset-type": "資產類型", -        "filter-type-asset-type-description": "類型為 '{{assetType}}' 的資產", -        "filter-type-asset-type-and-name-description": "類型為 '{{assetType}}' 且以 '{{prefix}}' 開頭的資產", -        "filter-type-device-type": "設備類型", -        "filter-type-device-type-description": "類型為 '{{deviceType}}' 的設備", - "filter-type-device-type-and-name-description": "類型為 '{{deviceType}}' 且以 '{{prefix}}' 開頭的設備", -        "filter-type-entity-view-type": "實體視圖類型", -        "filter-type-entity-view-type-description": "類型為 '{{entityView}}' 的實體視圖", -        "filter-type-entity-view-type-and-name-description": "類型為 {{entityView}}' 且以 '{{prefix}}' 開頭的實體視圖", -        "filter-type-relations-query": "關係查詢", -        "filter-type-relations-query-description": "具有 {{relationType}} 關聯 {{direction}} {{rootEntity}} 的 {{entities}} ", -        "filter-type-asset-search-query": "資產搜尋查詢", -        "filter-type-asset-search-query-description": "類型為 {{assetTypes}} 且具有 {{relationType}} 關聯 {{direction}} {{rootEntity}} 的資產", -        "filter-type-device-search-query": "設備搜尋查詢", -        "filter-type-device-search-query-description": "類型為 {{deviceTypes}} 且具有 {{relationType}} 關聯 {{direction}} {{rootEntity}} 的設備", -        "filter-type-entity-view-search-query": "實體視圖搜尋查詢", -        "filter-type-entity-view-search-query-description": "類型為 {{entityViewTypes}} 且具有 {{relationType}} 關聯 {{direction}} {{rootEntity}} 的實體視圖", -        "entity-filter": "實體過濾", -        "resolve-multiple": "解決為多實體", -        "filter-type": "過濾類型", -        "filter-type-required": "過濾類型必填。", -        "entity-filter-no-entity-matched": "未找到符合指定過濾條件的實體。", -        "no-entity-filter-specified": "沒有指定實體過濾條件", -        "root-state-entity": "使用儀表板狀態實體作為根實體", -        "root-entity": "根實體", -        "state-entity-parameter-name": "狀態實體參數名稱", -        "default-state-entity": "預設狀態實體", -        "default-entity-parameter-name": "預設", -        "max-relation-level": "最大關係層級", -        "unlimited-level": "不限層級", -        "state-entity": "儀表板狀態實體", -        "all-entities": "所有實體", -        "any-relation": "不限" -    }, -    "asset": { -        "asset": "資產", -        "assets": "資產", -        "management": "資產管理", -        "view-assets": "查看資產", -        "add": "增加資產", -        "assign-to-customer": "分配給客戶", -        "assign-asset-to-customer": "將資產分配給客戶", -        "assign-asset-to-customer-text": "請選擇要分配給客戶的資產", -        "no-assets-text": "未找到資產", -        "assign-to-customer-text": "請選擇客戶以分配資產", -        "public": "公開", -        "assignedToCustomer": "分配客戶", -        "make-public": "資產設為公開", -        "make-private": "資產設為私有", -        "unassign-from-customer": "取消分配客戶", -        "delete": "刪除資產", -        "asset-public": "資產公開", -        "asset-type": "資產類型", -        "asset-type-required": "資產類型必填。", -        "select-asset-type": "選擇資產類型", -        "enter-asset-type": "輸入資產類型", -        "any-asset": "任何資產", -        "no-asset-types-matching": "沒有找到符合 '{{entitySubtype}}' 的資產類型。", -        "asset-type-list-empty": "資產類型未選擇。", -        "asset-types": "資產類型", -        "name": "名稱", -        "name-required": "名稱必填。", -        "description": "描述", -        "type": "類型", -        "type-required": "類型必填。", -        "details": "詳細資訊", -        "events": "事件", -        "add-asset-text": "增加新資產", -        "asset-details": "資產詳細資訊", -        "assign-assets": "分配資產", -        "assign-assets-text": "分配 { count, plural, 1 {1 資產} other {# 資產} } 給客戶", -        "delete-assets": "刪除資產", -        "unassign-assets": "取消分配資產", -        "unassign-assets-action-title": "從客戶處取消分配 { count, plural, 1 {1 資產} other {# 資產} } ", -        "assign-new-asset": "分配新資產", -        "delete-asset-title": "確定要刪除資產 '{{assetName}}'?", -        "delete-asset-text": "小心!確認後資產及其所有相關資料將無法恢復。", -        "delete-assets-title": "確定要刪除 { count, plural, 1 {1 資產} other {# 資產} }?", -        "delete-assets-action-title": "刪除 { count, plural, 1 {1 資產} other {# 資產} }", -        "delete-assets-text": "小心,確認後,所有選擇的資產將被刪除,所有相關的資料將變得無法恢復。", -        "make-public-asset-title": "你確定你想建立公開'{{assetName}}'資產?", -        "make-public-asset-text": "確認後,資產及其所有資料將被公開並被他人存取。", -        "make-private-asset-title": "你確定你想建立私有 '{{assetName}}' 資產?", -        "make-private-asset-text": "確認後,資產及其所有資料將被私有化,無法被他人存取。", -        "unassign-asset-title": "您確定要取消對'{{assetName}}'資產的分配嗎?", -        "unassign-asset-text": "確認後,資產將未分配,客戶無法存取。", -        "unassign-asset": "未分配資產", -        "unassign-assets-title": "您確定要取消分配 { count, plural, 1 {1 資產} other {# 資產} }嗎?", -        "unassign-assets-text": "確認後,所有選擇的資產將被分配,客戶無法存取。", -        "copyId": "複製資產ID", -        "idCopiedMessage": "資產ID已經複製到剪貼簿", -        "select-asset": "選擇資產", -        "no-assets-matching": "沒有找到符合 '{{entity}}' 的資產。", -        "asset-required": "資產必填", -        "name-starts-with": "資產名稱以此開頭", -        "label": "標籤" -    }, -    "attribute": { -        "attributes": "屬性", -        "latest-telemetry": "最新遙測", -        "attributes-scope": "設備屬性範圍", -        "scope-latest-telemetry": "最新遙測", -        "scope-client": "客戶端屬性", -        "scope-server": "服務端屬性", -        "scope-shared": "共享屬性", -        "add": "增加屬性", -        "key": "鍵", -        "last-update-time": "最後更新時間", -        "key-required": "屬性鍵必填。", -        "value": "值", -        "value-required": "屬性值必填。", -        "delete-attributes-title": "您確定要刪除 { count, plural, 1 {1 屬性} other {# 屬性} }嗎?", -        "delete-attributes-text": "注意,確認後所有選中的屬性都會被刪除。", - "delete-attributes": "刪除屬性", -        "enter-attribute-value": "輸入屬性值", -        "show-on-widget": "在部件上顯示", -        "widget-mode": "部件模式", -        "next-widget": "下一個部件", -        "prev-widget": "上一個部件", -        "add-to-dashboard": "增加到儀表板", -        "add-widget-to-dashboard": "將部件增加到儀表板", -        "selected-attributes": "{ count, plural, 1 {1 屬性} other {# 屬性} } 被選中", -        "selected-telemetry": "{ count, plural, 1 {1 遙測} other {# 遙測} } 被選中" -    }, -    "audit-log": { -        "audit": "審計", -        "audit-logs": "審計日誌", -        "timestamp": "時間戳", -        "entity-type": "實體類型", -        "entity-name": "實體名稱", -        "user": "用戶", -        "type": "類型", -        "status": "狀態", -        "details": "詳細資訊", -        "type-added": "增加", -        "type-deleted": "刪除", -        "type-updated": "更新", -        "type-attributes-updated": "更新屬性", -        "type-attributes-deleted": "刪除屬性", -        "type-rpc-call": "RPC調用", -        "type-credentials-updated": "更新憑證", -        "type-assigned-to-customer": "分配給客戶", -        "type-unassigned-from-customer": "未分配給客戶", -        "type-activated": "啟動", -        "type-suspended": "暫停", -        "type-credentials-read": "讀取憑證", -        "type-attributes-read": "讀取屬性", -        "status-success": "成功", -        "status-failure": "失敗", -        "audit-log-details": "審計日誌詳細資訊", -        "no-audit-logs-prompt": "找不到日誌", -        "action-data": "活動資料", -        "failure-details": "失敗詳細資訊", -        "search": "查找審計日誌", -        "clear-search": "清空查找" -    }, - "confirm-on-exit": { -        "message": "您有未儲存的更改。確定要離開此頁嗎?", -        "html-message": "您有未儲存的更改。
    確定要離開此頁面嗎?", -        "title": "未儲存的更改" -    }, -    "contact": { -        "country": "國家", -        "city": "城市", -        "state": "州", -        "postal-code": "郵政編碼", -        "postal-code-invalid": "只允許數字。", -        "address": "地址", -        "address2": "地址2", -        "phone": "手機", -        "email": "郵箱", -        "no-address": "無地址" -    }, -    "common": { -        "username": "用戶名", -        "password": "密碼", -        "enter-username": "輸入用戶名", -        "enter-password": "輸入密碼", -        "enter-search": "輸入檢索條件" -    }, -    "content-type": { -        "json": "Json", -        "text": "Text", -        "binary": "Binary (Base64)" -    }, -    "customer": { -        "customer": "客戶", -        "customers": "客戶", -        "management": "客戶管理", -        "dashboard": "客戶儀表板", -        "dashboards": "客戶儀表板", -        "devices": "客戶設備", -        "entity-views": "客戶實體視圖", -        "assets": "客戶資產", -        "public-dashboards": "公共儀表板", -        "public-devices": "公共設備", -        "public-assets": "公共資產", -        "public-entity-views": "公共實體視圖", -        "add": "增加客戶", -        "delete": "刪除客戶", -        "manage-customer-users": "管理客戶用戶", -        "manage-customer-devices": "管理客戶設備", -        "manage-customer-dashboards": "管理客戶儀表板", -        "manage-public-devices": "管理公共設備", -        "manage-public-dashboards": "管理公共儀表板", -        "manage-customer-assets": "管理客戶資產", -        "manage-public-assets": "管理公共資產", -        "add-customer-text": "增加新客戶", -        "no-customers-text": "沒有找到客戶", -        "customer-details": "客戶詳細資訊", -        "delete-customer-title": "您確定要刪除客戶'{{customerTitle}}'嗎?", -        "delete-customer-text": "小心!確認後,客戶及其所有相關資料將無法恢復。", -        "delete-customers-title": "您確定要刪除 { count, plural, 1 {1 客戶} other {# 客戶} }嗎?", -        "delete-customers-action-title": "刪除 { count, plural, 1 {1 客戶} other {# 客戶} }", -        "delete-customers-text": "小心!確認後,所有選擇的客戶將被刪除,所有相關資料將無法恢復。", -        "manage-users": "管理用戶", -        "manage-assets": "管理資產", -        "manage-devices": "管理設備", -        "manage-dashboards": "管理儀表板", -        "title": "標題", -        "title-required": "需要標題", -        "description": "描述", -        "details": "詳細資訊", -        "events": "事件", -        "copyId": "複製客戶ID", -        "idCopiedMessage": "客戶ID已複製到剪貼板", -        "select-customer": "選擇客戶", -        "no-customers-matching": "沒有找到符合 '{{entity}}' 的客戶。", -        "customer-required": "客戶是必選項", -        "select-default-customer": "選擇預設的客戶", -        "default-customer": "預設客戶", -        "default-customer-required": "為了測試租戶級別上的儀表板,需要預設客戶。" -    }, - "datetime": { - "date-from": "日期從", - "time-from": "時間從", - "date-to": "日期到", - "time-to": "時間到" - }, - "dashboard": { -        "dashboard": "儀表板", -        "dashboards": "儀表板庫", -        "management": "儀表板管理", -        "view-dashboards": "查看儀表板", -        "add": "增加儀表板", -        "assign-dashboard-to-customer": "將儀表板分配給客戶", -        "assign-dashboard-to-customer-text": "請選擇要分配給客戶的儀表板", -        "assign-to-customer-text": "請選擇客戶分配儀表板", -        "assign-to-customer": "分配給客戶", -        "unassign-from-customer": "取消分配客戶", -        "make-public": "儀表板設為公開", -        "make-private": "儀表板設為私有", -        "manage-assigned-customers": "管理已分配的客戶", -        "assigned-customers": "已分配的客戶", -        "assign-to-customers": "將儀表板分配給客戶", -        "assign-to-customers-text": "請選擇客戶指定儀表板", -        "unassign-from-customers": "客戶未分配儀表板", -        "unassign-from-customers-text": "請選擇從儀表板中取消分配的客戶", -        "no-dashboards-text": "沒有找到儀表板", -        "no-widgets": "沒有配置部件", -        "add-widget": "增加新的部件", -        "title": "標題", -        "select-widget-title": "選擇部件", -        "select-widget-subtitle": "可用的部件類型列表", -        "delete": "刪除儀表板", -        "title-required": "需要標題。", -        "description": "描述", -        "details": "詳細資訊", -        "dashboard-details": "儀表板詳細資訊", -        "add-dashboard-text": "增加新的儀表板", -        "assign-dashboards": "分配儀表板", -        "assign-new-dashboard": "分配新的儀表板", -        "assign-dashboards-text": "分配 { count, plural, 1 {1 儀表板} other {# 儀表板} } 給客戶", -        "unassign-dashboards-action-text": "未分配 { count, plural, 1 {1 儀表板} other {# 儀表板} } 給客戶", -        "delete-dashboards": "刪除儀表板", -        "unassign-dashboards": "取消分配儀表板", - "unassign-dashboards-action-title": "從客戶處取消分配 { count, plural, 1 {1 儀表板} other {# 儀表板} } ", -        "delete-dashboard-title": "您確定要刪除儀表板 '{{dashboardTitle}}'嗎?", -        "delete-dashboard-text": "小心!確認後儀表板及其所有相關資料將無法恢復。", -        "delete-dashboards-title": "你確定你要刪除 { count, plural, 1 {1 儀表板} other {# 儀表板} }嗎?", -        "delete-dashboards-action-title": "刪除 { count, plural, 1 {1 儀表板} other {# 儀表板} }", -        "delete-dashboards-text": "小心!確認後所有選擇的儀表板將被刪除,所有相關資料將無法恢復。", -        "unassign-dashboard-title": "您確定要取消分配儀表板 '{{dashboardTitle}}'嗎?", -        "unassign-dashboard-text": "確認後,面板將被取消分配,客戶將無法存取。", -        "unassign-dashboard": "取消分配儀表板", -        "unassign-dashboards-title": "您確定要取消分配儀表板 { count, plural, 1 {1 儀表板} other {# 儀表板} } 嗎?", -        "unassign-dashboards-text": "確認後,所有選擇的儀表板將被取消分配,客戶將無法存取。", -        "public-dashboard-title": "儀表板現已公佈", -        "public-dashboard-text": "你的儀表板{{dashboardTitle}} 已被公開,可通過如下連結存取:", -        "public-dashboard-notice": "提示: 不要忘記將相關設備公開以存取其資料。", -        "make-private-dashboard-title": "您確定要將儀表板 '{{dashboardTitle}}' 設為私有嗎?", -        "make-private-dashboard-text": "確認後,儀表板將被設為私有,不能被其他人存取。", -        "make-private-dashboard": "儀表板設為私有", -        "socialshare-text": "'{{dashboardTitle}}' 由Thingsboard提供支持", -        "socialshare-title": "'{{dashboardTitle}}' 由Thingsboard提供支持", -        "select-dashboard": "選擇儀表板", -        "no-dashboards-matching": "找不到符合 '{{entity}}' 的儀表板。", -        "dashboard-required": "儀表板必填。", -        "select-existing": "選擇現有儀表板", -        "create-new": "建立新的儀表板", -        "new-dashboard-title": "新儀表板標題", -        "open-dashboard": "打開儀表板", -        "set-background": "設定背景", -        "background-color": "背景顏色", -        "background-image": "背景圖片", -        "background-size-mode": "背景大小模式", -        "no-image": "無圖像選擇", - "drop-image": "拖拉圖像或單擊以選擇要上傳的文件。", -        "settings": "設定", -        "columns-count": "列數", -        "columns-count-required": "需要列數。", -        "min-columns-count-message": "只允許最少10列", -        "max-columns-count-message": "只允許最多1000列", -        "widgets-margins": "部件間邊距", -        "horizontal-margin": "水平邊距", -        "horizontal-margin-required": "需要水平邊距值。", -        "min-horizontal-margin-message": "只允許0作為最小水平邊距值。", -        "max-horizontal-margin-message": "只允許50作為最大水平邊距值。", -        "vertical-margin": "垂直邊距", -        "vertical-margin-required": "需要垂直邊距值。", -        "min-vertical-margin-message": "只允許0作為最小垂直邊距值。", -        "max-vertical-margin-message": "只允許50作為最大垂直邊距值。", -        "autofill-height": "自動填充佈局高度", -        "mobile-layout": "移動端佈局設定", -        "mobile-row-height": "移動端行高距(px)", -        "mobile-row-height-required": "移動端行高距必填。", -        "min-mobile-row-height-message": "移動端行高距至少5px。", -        "max-mobile-row-height-message": "移動端行高距最多200px。", -        "display-title": "顯示儀表板標題", -        "toolbar-always-open": "工具欄常駐", -        "title-color": "標題顏色", -        "display-dashboards-selection": "顯示儀表板選項", -        "display-entities-selection": "顯示實體選項", -        "display-dashboard-timewindow": "顯示時間窗口", -        "display-dashboard-export": "顯示匯出", -        "import": "匯入儀表板", -        "export": "匯出儀表板", -        "export-failed-error": "無法匯出儀表板: {{error}}", -        "create-new-dashboard": "建立新的儀表板", -        "dashboard-file": "儀表板文件", -        "invalid-dashboard-file-error": "無法匯入儀表板: 儀表板資料結構無效。", -        "dashboard-import-missing-aliases-title": "配置匯入儀表板使用的別名", -        "create-new-widget": "建立新部件", -        "import-widget": "匯入部件", -        "widget-file": "部件文件", - "invalid-widget-file-error": "無法匯入窗口部件: 窗口部件資料結構無效。", -        "widget-import-missing-aliases-title": "配置匯入的窗口部件使用的別名", -        "open-toolbar": "打開儀表板工具欄", -        "close-toolbar": "關閉工具欄", -        "configuration-error": "配置錯誤", -        "alias-resolution-error-title": "儀表板別名配置錯誤", -        "invalid-aliases-config": "無法找到與某些別名過濾器符合的任何設備。
    請聯繫您的管理員以解決此問題。", -        "select-devices": "選擇設備", -        "assignedToCustomer": "分配給客戶", -        "public": "公共", -        "public-link": "公共連結", -        "copy-public-link": "複製公共連結", -        "public-link-copied-message": "儀表板的公共連結已被複製到剪貼板", -        "manage-states": "儀表板狀態管理", -        "states": "儀表板狀態", -        "search-states": "儀表板狀態檢索", -        "selected-states": "{ count, plural, 1 {1 儀表板狀態} other {# 儀表板狀態} } 選中", -        "edit-state": "儀表板狀態編輯", -        "delete-state": "刪除儀表板狀態", -        "add-state": "增加儀表板狀態", -        "state": "儀表板狀態", -        "state-name": "狀態名", -        "state-name-required": "儀表板狀態名必填。", -        "state-id": "狀態ID", -        "state-id-required": "儀表板狀態ID必填。", -        "state-id-exists": "儀表板狀態ID已經存在。", -        "is-root-state": "根狀態", -        "delete-state-title": "刪除儀表板狀態", -        "delete-state-text": "確定要刪除儀表板狀態 '{{stateName}}' 嗎?", -        "show-details": "顯示詳細資訊", -        "hide-details": "隱藏詳細資訊", -        "select-state": "選擇目標狀態", -        "state-controller": "狀態控制" - }, - "datakey": { -        "settings": "設定", -        "advanced": "進階", -        "label": "標籤", -        "color": "顏色", -        "units": "單位符號", -        "decimals": "小數位數", -        "data-generation-func": "資料生成功能", -        "use-data-post-processing-func": "使用資料後處理功能", -        "configuration": "資料鍵配置", -        "timeseries": "時間序列", -        "attributes": "屬性", -        "alarm": "報警字段", -        "timeseries-required": "需要設備時間序列。", -        "timeseries-or-attributes-required": "設備時間/屬性必填。", -        "maximum-timeseries-or-attributes": "最大允許 { count, plural, 1 {1 時間序列/屬性} other {# 時間序列/屬性} }", -        "alarm-fields-required": "警告字段必填。", -        "function-types": "函數類型", -        "function-types-required": "需要函數類型。", -        "maximum-function-types": "至少需要 { count, plural, 1 {1 函數類型} other {# 函數類型} }" -    }, - "datasource": { -        "type": "資料源類型", -        "name": "資料源名稱", -        "add-datasource-prompt": "請增加資料源" -    }, -    "details": { -        "edit-mode": "編輯模式", -        "toggle-edit-mode": "切換編輯模式" -    }, -    "device": { -        "device": "設備", -        "device-required": "設備必填", -        "devices": "設備", -        "management": "設備管理", -        "view-devices": "查看設備", -        "device-alias": "設備別名", -        "aliases": "設備別名", -        "no-alias-matching": "'{{alias}}' 沒有找到。", -        "no-aliases-found": "找不到別名。", -        "no-key-matching": "'{{key}}' 沒有找到。", -        "no-keys-found": "找不到密鑰。", -        "create-new-alias": "建立一個新的!", -        "create-new-key": "建立一個新的!", -        "duplicate-alias-error": "找到重複別名 '{{alias}}'。
    設備別名必須是唯一的。", -        "configure-alias": "配置 '{{alias}}' 別名", -        "no-devices-matching": "找不到與 '{{entity}}' 符合的設備。", -        "alias": "別名", -        "alias-required": "需要設備別名。", -        "remove-alias": "刪除設備別名", -        "add-alias": "增加設備別名", - "name-starts-with": "名稱前綴", -        "device-list": "設備列表", -        "use-device-name-filter": "使用過濾器", -        "device-list-empty": "沒有被選中的設備", -        "device-name-filter-required": "設備名稱過濾器必填。", -        "device-name-filter-no-device-matched": "找不到以'{{device}}' 開頭的設備。", -        "add": "增加設備", -        "assign-to-customer": "分配給客戶", -        "assign-device-to-customer": "將設備分配給客戶", -        "assign-device-to-customer-text": "請選擇要分配給客戶的設備", -        "make-public": "公開", -        "make-private": "私有", -        "no-devices-text": "找不到設備", -        "assign-to-customer-text": "請選擇客戶分配設備", -        "device-details": "設備詳細訊息", -        "add-device-text": "增加新設備", -        "credentials": "憑據", -        "manage-credentials": "管理憑據", -        "delete": "刪除設備", -        "assign-devices": "分配設備", -        "assign-devices-text": "將{count,plural,1 {1 設備} other {# 設備}}分配給客戶", -        "delete-devices": "刪除設備", -        "unassign-from-customer": "取消分配客戶", -        "unassign-devices": "取消分配設備", -        "unassign-devices-action-title": "從客戶處取消分配{count,plural,1 {1 設備} other {# 設備}}", -        "assign-new-device": "分配新設備", - "make-public-device-title": "您確定要將設備 '{{deviceName}}' 設為公開嗎?", -        "make-public-device-text": "確認後,設備及其所有資料將被設為公開並可被其他人存取。", -        "make-private-device-title": "您確定要將設備 '{{deviceName}}' 設為私有嗎?", -        "make-private-device-text": "確認後,設備及其所有資料將被設為私有,不被其他人存取。", -        "view-credentials": "查看憑據", -        "delete-device-title": "您確定要刪除設備的{{deviceName}}嗎?", -        "delete-device-text": "小心!確認後設備及其所有相關資料將無法恢復。", -        "delete-devices-title": "您確定要刪除{count,plural,1 {1 設備} other {# 設備}} 嗎?", -        "delete-devices-action-title": "刪除 {count,plural,1 {1 設備} other {# 設備}}", -        "delete-devices-text": "小心!確認後所有選擇的設備將被刪除,所有相關資料將無法恢復。", -        "unassign-device-title": "您確定要取消分配設備 '{{deviceName}}'?", -        "unassign-device-text": "確認後,設備將被取消分配,客戶將無法存取。", -        "unassign-device": "取消分配設備", -        "unassign-devices-title": "您確定要取消分配{count,plural,1 {1 設備} other {# 設備}} 嗎?", -        "unassign-devices-text": "確認後,所有選擇的設備將被取消分配,並且客戶將無法存取。", -        "device-credentials": "設備憑據", -        "credentials-type": "憑據類型", -        "access-token": "存取令牌", -        "access-token-required": "需要存取令牌", -        "access-token-invalid": "存取令牌長度必須為1到20個字符。", -        "rsa-key": "RSA公鑰", -        "rsa-key-required": "需要RSA公鑰", -        "secret": "密鑰", - "secret-required": "密鑰必填", -        "device-type": "設備類型", -        "device-type-required": "設備類型必填。", -        "select-device-type": "選擇設備類型", -        "enter-device-type": "輸入設備類型", -        "any-device": "任意設備", -        "no-device-types-matching": "沒有找到符合 '{{entitySubtype}}' 的設備類型。", -        "device-type-list-empty": "未選擇設備類型", -        "device-types": "設備類型", -        "name": "名稱", -        "name-required": "名稱必填。", -        "description": "說明", -        "events": "事件", -        "details": "詳細訊息", -        "copyId": "複製設備ID", -        "copyAccessToken": "複製存取令牌", -        "idCopiedMessage": "設備ID已複製到剪貼板", -        "accessTokenCopiedMessage": "設備存取令牌已複製到剪貼板", -        "assignedToCustomer": "分配給客戶", -        "unable-delete-device-alias-title": "無法刪除設備別名", -        "unable-delete-device-alias-text": "設備別名 '{{deviceAlias}}' 不能夠被刪除,因為它被下列部件所使用:
    {{widgetsList}}", -        "is-gateway": "是閘道", -        "public": "公開", -        "device-public": "設備公開", -        "select-device": "選擇設備" -    }, - "dialog": { -        "close": "關閉對話框" -    }, -    "error": { -        "unable-to-connect": "無法連接到伺服器!請檢查您的互聯網連接。", -        "unhandled-error-code": "未處理的錯誤代碼: {{errorCode}}", -        "unknown-error": "未知錯誤" -    }, -    "entity": { -        "entity": "實體", -        "entities": "實體", -        "aliases": "實體別名", -        "entity-alias": "實體別名", -        "unable-delete-entity-alias-title": "無法刪除實體別名", -        "unable-delete-entity-alias-text": "實體別名 '{{entityAlias}}' 被以下部件使用不能刪除:
    {{widgetsList}}", -        "duplicate-alias-error": "別名 '{{alias}}' 重複。
    同一儀表板別名必須唯一。", -        "missing-entity-filter-error": "別名 '{{alias}}' 缺少過濾器", -        "configure-alias": "別名 '{{alias}}' 配置", -        "alias": "別名", -        "alias-required": "實體別名必填。", -        "remove-alias": "移除實體別名", -        "add-alias": "增加實體別名", -        "entity-list": "實體列表", -        "entity-type": "實體類型", -        "entity-types": "實體類型", -        "entity-type-list": "實體類型列表", -        "any-entity": "任意實體", -        "enter-entity-type": "輸入實體類型", -        "no-entities-matching": "沒有找到符合 '{{entity}}' 的實體。", -        "no-entity-types-matching": "沒有找到符合 '{{entityType}}' 類型的實體。", -        "name-starts-with": "名稱開始於", -        "use-entity-name-filter": "用戶過濾", -        "entity-list-empty": "沒有選擇實體。", -        "entity-type-list-empty": "沒有選擇實體類型。", -        "entity-name-filter-required": "實體名過濾器必填。", -        "entity-name-filter-no-entity-matched": "沒有找到以 '{{entity}}' 開頭的實體", -        "all-subtypes": "所有", - "select-entities": "選擇實體", -        "no-aliases-found": "沒有找到別名", -        "no-alias-matching": "沒有找到 '{{alias}}'", -        "create-new-alias": "建立新別名", -        "key": "鍵", -        "key-name": "鍵名", -        "no-keys-found": "沒有找到鍵", -        "no-key-matching": "沒有找到鍵 '{{key}}'", -        "create-new-key": "建立新鍵", -        "type": "類型", -        "type-required": "實體類型必填。", -        "type-device": "設備", -        "type-devices": "設備", -        "list-of-devices": "{ count, plural, 1 {設備} other {# 設備列表} }", -        "device-name-starts-with": "以 '{{prefix}}' 開頭的設備", -        "type-asset": "資產", -        "type-assets": "資產", -        "list-of-assets": "{ count, plural, 1 {資產} other {# 資產列表} }", -        "asset-name-starts-with": "以 '{{prefix}}' 開頭的資產", -        "type-entity-view": "實體視圖", -        "type-entity-views": "實體視圖", -        "list-of-entity-views": "{ count, plural, 1 {實體視圖} other {# 實體視圖列表} }", -        "entity-view-name-starts-with": "以 '{{prefix}}' 開頭的實體視圖", -        "type-rule": "規則", -        "type-rules": "規則", -        "list-of-rules": "{ count, plural, 1 {規則} other {# 規則列表} }", -        "rule-name-starts-with": "以 '{{prefix}}' 開頭的規則", -        "type-plugin": "插件", -        "type-plugins": "插件", -        "list-of-plugins": "{ count, plural, 1 {插件} other {# 插件列表} }", -        "plugin-name-starts-with": "以 '{{prefix}}' 開頭的插件", -        "type-tenant": "租戶", -        "type-tenants": "租戶", -        "list-of-tenants": "{ count, plural, 1 {租戶} other {# 租戶列表} }", -        "tenant-name-starts-with": "以 '{{prefix}}' 開頭的租戶", -        "type-customer": "客戶", -        "type-customers": "客戶", - "list-of-customers": "{ count, plural, 1 {客戶} other {# 客戶列表} }", -        "customer-name-starts-with": "以 '{{prefix}}' 開頭的客戶", -        "type-user": "用戶", -        "type-users": "用戶", -        "list-of-users": "{ count, plural, 1 {用戶} other {# 用戶列表} }", -        "user-name-starts-with": "以 '{{prefix}}' 開頭的用戶", -        "type-dashboard": "儀表板", -        "type-dashboards": "儀表板", -        "list-of-dashboards": "{ count, plural, 1 {儀表板} other {# 儀表板列表} }", -        "dashboard-name-starts-with": "以 '{{prefix}}' 開頭的儀表板", -        "type-alarm": "警告", -        "type-alarms": "警告", -        "list-of-alarms": "{ count, plural, 1 {警告} other {# 警告列表} }", -        "alarm-name-starts-with": "以 '{{prefix}}' 開頭的警告", -        "type-rulechain": "規則鏈", -        "type-rulechains": "規則鏈庫", -        "list-of-rulechains": "{ count, plural, 1 {一個規則鏈} other {# 規則鏈列表} }", -        "rulechain-name-starts-with": "規則鏈前綴名稱 '{{prefix}}'", -        "type-current-customer": "當前客戶", -        "search": "實體檢索", -        "selected-entities": "{ count, plural, 1 {1 實體} other {# 實體} } 被選中", -        "entity-name": "實體名", -        "details": "實體詳細資訊", -        "no-entities-prompt": "沒有找到實體", -        "no-data": "無資料" -    }, - "entity-view": { -        "entity-view": "實體視圖", -        "entity-view-required": "實體視圖必填。", -        "entity-views": "實體視圖", -        "management": "實體視圖管理", -        "view-entity-views": "查看實體視圖", -        "entity-view-alias": "實體視圖別名", -        "aliases": "實體視圖別名", -        "no-alias-matching": "'{{alias}}' 沒有找到。", -        "no-aliases-found": "找不到別名。", -        "no-key-matching": "'{{key}}' 沒有找到。", -        "no-keys-found": "找不到密鑰。", -        "create-new-alias": "建立一個新的!", -        "create-new-key": "建立一個新的!", -        "duplicate-alias-error": "找到重複別名 '{{alias}}'。
    實體視圖別名必須是唯一的。", -        "configure-alias": "配置 '{{alias}}' 別名", -        "no-devices-matching": "找不到與 '{{entity}}' 符合的實體視圖。", -        "alias": "別名", -        "alias-required": "需要實體視圖別名。", -        "remove-alias": "刪除實體視圖別名", -        "add-alias": "增加實體視圖別名", -        "name-starts-with": "名稱前綴", -        "entity-view-list": "實體視圖列表", -        "use-entity-view-name-filter": "使用過濾器", -        "entity-view-list-empty": "沒有被選中的實體視圖", -        "entity-view-name-filter-required": "實體視圖名稱過濾器必填。", -        "entity-view-name-filter-no-entity-view-matched": "找不到以'{{entityView}}' 開頭的實體視圖。", -        "add": "增加實體視圖", -        "assign-to-customer": "分配給客戶", -        "assign-entity-view-to-customer": "將實體視圖分配給客戶", -        "assign-entity-view-to-customer-text": "請選擇要分配給客戶的實體視圖", -        "no-entity-views-text": "找不到實體視圖", -        "assign-to-customer-text": "請選擇客戶分配實體視圖", -        "entity-view-details": "實體視圖詳細訊息", -        "add-entity-view-text": "增加新實體視圖", -        "delete": "刪除實體視圖", -        "assign-entity-views": "分配實體視圖", -        "assign-entity-views-text": "分配 { count, plural, 1 {1 實體視圖} other {# 實體視圖} } 給客戶", -        "delete-entity-views": "刪除實體視圖", -        "unassign-from-customer": "取消分配客戶", -        "unassign-entity-views": "取消分配實體視圖", - "unassign-entity-views-action-title": "從客戶處取消分配{count,plural,1 {1 實體視圖} other {# 實體視圖}}", -        "assign-new-entity-view": "分配新實體視圖", -        "delete-entity-view-title": "確定要刪除實體視圖 '{{entityViewName}}'?", -        "delete-entity-view-text": "小心!確認後實體視圖及其所有相關資料將無法恢復。", -        "delete-entity-views-title": "確定要刪除 { count, plural, 1 {1 實體視圖} other {# 實體視圖} }?", -        "delete-entity-views-action-title": "刪除 { count, plural, 1 {1 實體視圖} other {# 實體視圖} }", -        "delete-entity-views-text": "B小心,確認後,所有選擇的實體視圖將被刪除,所有相關的資料將變得無法恢復。", -        "unassign-entity-view-title": "您確定要取消對 '{{entityViewName}}'實體視圖的分配嗎?", -        "unassign-entity-view-text": "確認後,實體視圖將未分配,客戶無法存取。", -        "unassign-entity-view": "未分配實體視圖", -        "unassign-entity-views-title": "您確定要取消分配 { count, plural, 1 {1 實體視圖} other {# 實體視圖} }嗎?", -        "unassign-entity-views-text": "確認後,所有選擇的實體視圖將被分配,客戶無法存取。", -        "entity-view-type": "實體視圖類型", -        "entity-view-type-required": "實體視圖類型必填。", -        "select-entity-view-type": "選擇實體視圖類型", -        "enter-entity-view-type": "輸入實體視圖類型", -        "any-entity-view": "任何實體視圖", -        "no-entity-view-types-matching": "沒有找到符合 '{{entitySubtype}}' 的實體視圖類型。", -        "entity-view-type-list-empty": "實體視圖類型未選擇。", -        "entity-view-types": "實體視圖類型", -        "name": "名稱", -        "name-required": "名稱必填。", -        "description": "描述", -        "events": "事件", -        "details": "詳細資訊", -        "copyId": "複製實體視圖ID", - "assignedToCustomer": "分配給客戶", -        "unable-entity-view-device-alias-title": "無法刪除實體視圖別名", -        "unable-entity-view-device-alias-text": "實體視圖別名 '{{entityViewAlias}}' 不能夠被刪除,因為它被下列部件所使用:
    {{widgetsList}}", -        "select-entity-view": "選擇實體視圖", -        "make-public": "實體視圖設為公開", -        "make-private": "實體視圖設為私有", -        "start-date": "開始日期", -        "start-ts": "開始時間", -        "end-date": "結束日期", -        "end-ts": "結束時間", -        "date-limits": "日期限製", -        "client-attributes": "客戶端屬性", -        "shared-attributes": "共享屬性", -        "server-attributes": "服務端屬性", -        "timeseries": "時間序列", -        "client-attributes-placeholder": "客戶端屬性", -        "shared-attributes-placeholder": "共享屬性", -        "server-attributes-placeholder": "服務端屬性", -        "timeseries-placeholder": "時間序列", -        "target-entity": "目標實體", -        "attributes-propagation": "屬性傳播", -        "attributes-propagation-hint": "每次儲存或更新這個實體視圖時,實體視圖將自動從目標實體複製指定的屬性。由於性能原因,目標實體屬性不會在每次屬性更改時傳播到實體視圖。您可以通過配置\"copy to view\"規則鏈中的規則節點,並將\"Post attributes\"和\"attributes Updated\"消息連結到新規則節點,從而啟用自動傳播。", -        "timeseries-data": "時間序列資料", -        "timeseries-data-hint": "配置目標實體的時間序列資料鍵,以便實體視圖可以存取這些鍵。這個時間序列資料是只讀的。", -        "make-public-entity-view-title": "你確定你想建立公開 '{{entityViewName}}' 實體視圖?", -        "make-public-entity-view-text": "確認後,實體視圖 及其所有資料將被公開並被他人存取。", -        "make-private-entity-view-title": "你確定你想建立私有 '{{entityViewName}}' 實體視圖?", -        "make-private-entity-view-text": "確認後,實體視圖及其所有資料將被私有化,無法被他人存取。" - }, - "event": { -        "event-type": "事件類型", -        "type-error": "錯誤", -        "type-lc-event": "生命週期事件", -        "type-stats": "類型統計", -        "type-debug-rule-node": "測試", -        "type-debug-rule-chain": "測試", -        "no-events-prompt": "找不到事件", -        "error": "錯誤", -        "alarm": "報警", -        "event-time": "事件時間", -        "server": "伺服器", -        "body": "整體", -        "method": "方法", -        "type": "類型", -        "entity": "實體", -        "message-id": "消息ID", -        "message-type": "消息類型", -        "data-type": "資料類型", -        "relation-type": "關係類型", -        "metadata": "元資料", -        "data": "資料", -        "event": "事件", -        "status": "狀態", -        "success": "成功", -        "failed": "失敗", -        "messages-processed": "消息處理", -        "errors-occurred": "錯誤發生" -    }, - "extension": { -        "extensions": "擴展", -        "selected-extensions": "{ count, plural, 1 {1 擴展} other {# 擴展} } 被選擇", -        "type": "類型", -        "key": "鍵名", -        "value": "值", -        "id": "ID", -        "extension-id": "擴展ID", -        "extension-type": "擴展類型", -        "transformer-json": "JSON *", -        "unique-id-required": "當前擴展ID已經存在。", -        "delete": "刪除擴展", -        "add": "增加擴展", -        "edit": "編輯擴展", -        "delete-extension-title": "確實要刪除擴展名'{{extensionId}}'嗎?", -        "delete-extension-text": "小心,確認後,擴展和所有相關資料將變得無法恢復。", -        "delete-extensions-title": "您確定要刪除 { count, plural, 1 {1 表達式} other {# 表達式} }嗎?", -        "delete-extensions-text": "小心,確認後,所有選擇的擴展將被刪除。", -        "converters": "轉換器", -        "converter-id": "轉換器序號", -        "configuration": "配置", -        "converter-configurations": "轉換器的配置", -        "token": "安全令牌", -        "add-converter": "增加轉換器", -        "add-config": "增加轉換器配置", -        "device-name-expression": "設備名稱表達式", -        "device-type-expression": "設備類型表達式", -        "custom": "顧客", -        "to-double": "加倍", -        "transformer": "轉換器", -        "json-required": "轉換器JSON必填。", -        "json-parse": "無法解析轉換器JSON。", -        "attributes": "屬性", -        "add-attribute": "增加屬性", -        "add-map": "增加映射元素", -        "timeseries": "時間序列", -        "add-timeseries": "增加時間序列", -        "field-required": "必填字段", -        "brokers": "代理伺服器組", -        "add-broker": "增加代理伺服器", -        "host": "主機", -        "port": "連接埠", -        "port-range": "連接埠應該在1到65535的範圍內。", -        "ssl": "Ssl", -        "credentials": "證書", -        "username": "用戶名", -        "password": "密碼", -        "retry-interval": "以毫秒為單位的重試間隔", -        "anonymous": "匿名", -        "basic": "Basic", -        "pem": "PEM", - "ca-cert": "CA證書文件 *", -        "private-key": "私鑰文件 *", -        "cert": "證書文件 *", -        "no-file": "沒有選擇文件。", -        "drop-file": "刪除文件或單擊以選擇要上載的文件。", -        "mapping": "映射", -        "topic-filter": "主題濾波", -        "converter-type": "轉換類型", -        "converter-json": "Json", -        "json-name-expression": "設備名稱JSON表達式", -        "topic-name-expression": "設備名稱主題表達式", -        "json-type-expression": "設備類型JSON表達式", -        "topic-type-expression": "設備類型主題表達式", -        "attribute-key-expression": "屬性關鍵字表達式", -        "attr-json-key-expression": "屬性鍵JSON表達式", -        "attr-topic-key-expression": "屬性關鍵字主題表達式", -        "request-id-expression": "請求ID表達式", -        "request-id-json-expression": "請求ID JSON表達式", -        "request-id-topic-expression": "請求ID主題表達式", -        "response-topic-expression": "響應主題表達式", -        "value-expression": "值表達式", -        "topic": "主題", -        "timeout": "毫秒超時", -        "converter-json-required": "轉換JSON是必需的。", -        "converter-json-parse": "無法解析轉換JSON。", -        "filter-expression": "過濾表達式", -        "connect-requests": "連接請求", -        "add-connect-request": "增加連接請求", -        "disconnect-requests": "斷開請求", -        "add-disconnect-request": "增加斷開請求", -        "attribute-requests": "屬性請求", -        "add-attribute-request": "增加屬性請求", -        "attribute-updates": "屬性更新", -        "add-attribute-update": "增加屬性更新", -        "server-side-rpc": "服務端RPC", -        "add-server-side-rpc-request": "增加服務端RPC請求", -        "device-name-filter": "設備名稱濾波", -        "attribute-filter": "屬性濾波", -        "method-filter": "方法濾波", -        "request-topic-expression": "請求主題表達式", -        "response-timeout": "毫秒內響應超時", -        "topic-expression": "主題表達", -        "client-scope": "客戶範圍", -        "add-device": "增加伺服器", -        "opc-server": "伺服器組", -        "opc-add-server": "增加伺服器", -        "opc-add-server-prompt": "請增加伺服器", -        "opc-application-name": "應用名稱", -        "opc-application-uri": "應用URI", -        "opc-scan-period-in-seconds": "秒級掃描週期", -        "opc-security": "安全性", -        "opc-identity": "身份", -        "opc-keystore": "密鑰庫", -        "opc-type": "類型", -        "opc-keystore-type": "類型", -        "opc-keystore-location": "位置 *", -        "opc-keystore-password": "密碼", -        "opc-keystore-alias": "別名", -        "opc-keystore-key-password": "密鑰密碼", -        "opc-device-node-pattern": "設備節點模式", -        "opc-device-name-pattern": "設備名稱模式", -        "modbus-server": "Servers/slaves", -        "modbus-add-server": "增加 server/slave", -        "modbus-add-server-prompt": "請增加 server/slave", -        "modbus-transport": "傳輸", -        "modbus-port-name": "串口名稱", -        "modbus-encoding": "編碼", -        "modbus-parity": "奇偶性", -        "modbus-baudrate": "鮑率", -        "modbus-databits": "資料位", -        "modbus-stopbits": "停止位", - "modbus-databits-range": "資料位應該在7到8的範圍內。", -        "modbus-stopbits-range": "停止位應該在1到2的範圍內。", -        "modbus-unit-id": "單位編號", -        "modbus-unit-id-range": "單位ID應該在1到247的範圍內", -        "modbus-device-name": "設備名稱", -        "modbus-poll-period": "輪詢週期 (ms)", -        "modbus-attributes-poll-period": "輪詢屬性週期 (ms)", -        "modbus-timeseries-poll-period": "時間序列輪詢週期 (ms)", -        "modbus-poll-period-range": "輪詢週期應為正值。", -        "modbus-tag": "標籤", -        "modbus-function": "函數", -        "modbus-register-address": "寄存器地址", -        "modbus-register-address-range": "寄存器地址應該在0到65535的範圍內。", -        "modbus-register-bit-index": "位索引", -        "modbus-register-bit-index-range": "位索引應該在0到15的範圍內。", -        "modbus-register-count": "寄存器計數", -        "modbus-register-count-range": "寄存器計數應該是一個正值。", -        "modbus-byte-order": "字節順序", -        "sync": { -            "status": "狀態", -            "sync": "同步", -            "not-sync": "不同步", -            "last-sync-time": "最後同步時間", -            "not-available": "無法使用" -        }, -        "export-extensions-configuration": "匯出擴展配置", -        "import-extensions-configuration": "匯入擴展配置", -        "import-extensions": "匯入擴展", -        "import-extension": "匯入擴展", -        "export-extension": "匯出擴展", -        "file": "擴展文件", -        "invalid-file-error": "無效的擴展文件" - }, - "fullscreen": { -        "expand": "展開到全螢幕", -        "exit": "退出全螢幕", -        "toggle": "切換全螢幕模式", -        "fullscreen": "全螢幕" -    }, -    "function": { -        "function": "函數" -    }, -    "grid": { -        "delete-item-title": "您確定要刪除此項嗎?", -        "delete-item-text": "注意,確認後此項及其所有相關資料將變得無法恢復。", -        "delete-items-title": "你確定你要刪除 { count, plural, 1 {1 項} other {# 項} }嗎?", -        "delete-items-action-title": "刪除 { count, plural, 1 {1 項} other {# 項} }", -        "delete-items-text": "注意,確認後所有選擇的項目將被刪除,所有相關資料將無法恢復。", -        "add-item-text": "增加新項目", -        "no-items-text": "沒有找到項目", -        "item-details": "項目詳細訊息", -        "delete-item": "刪除項目", -        "delete-items": "刪除項目", -        "scroll-to-top": "滾動到頂部" -    }, -    "help": { -        "goto-help-page": "轉到幫助頁面" -    }, -    "home": { -        "home": "首頁", -        "profile": "屬性", -        "logout": "註銷", -        "menu": "菜單", -        "avatar": "頭像", -        "open-user-menu": "打開用戶菜單" -    }, -    "import": { -        "no-file": "沒有選擇文件", -        "drop-file": "拖動一個JSON文件或者單擊以選擇要上傳的文件。" -    }, -    "item": { -        "selected": "選擇" -    }, -    "js-func": { -        "no-return-error": "函數必須返回值!", -        "return-type-mismatch": "函數必須返回 '{{type}}' 類型的值!", -        "tidy": "整理" -    }, - "key-val": { -        "key": "鍵名", -        "value": "值", -        "remove-entry": "刪除條目", -        "add-entry": "增加條目", -        "no-data": "沒有條目" -    }, -    "layout": { -        "layout": "佈局", -        "manage": "佈局管理", -        "settings": "佈局設定", -        "color": "顏色", -        "main": "主體", -        "right": "右側", -        "select": "選擇目標佈局" -    }, -    "legend": { -        "position": "圖例位置", -        "show-max": "顯示最大值", -        "show-min": "顯示最小值", -        "show-avg": "顯示平均值", -        "show-total": "顯示總數", -        "settings": "圖例設定", -        "min": "最小值", -        "max": "最大值", -        "avg": "平均值", -        "total": "總數" -    }, -    "login": { -        "login": "登入", -        "request-password-reset": "請求密碼重置", -        "reset-password": "重置密碼", -        "create-password": "建立密碼", -        "passwords-mismatch-error": "輸入的密碼必須相同!", -        "password-again": "再次輸入密碼", -        "sign-in": "登入 ", -        "username": "用戶名(電子郵件)", -        "remember-me": "記住我", -        "forgot-password": "忘記密碼?", -        "password-reset": "密碼重置", -        "new-password": "新密碼", -        "new-password-again": "再次輸入新密碼", -        "password-link-sent-message": "密碼重置連結已成功發送!", -        "email": "電子郵件" -    }, - "position": { -        "top": "頂部", -        "bottom": "底部", -        "left": "左側", -        "right": "右側" -    }, -    "profile": { -        "profile": "屬性", -        "change-password": "更改密碼", -        "current-password": "當前密碼" -    }, -    "relation": { -        "relations": "關聯", -        "direction": "方向", -        "search-direction": { -            "FROM": "從", -            "TO": "到" -        }, -        "direction-type": { -            "FROM": "從", -            "TO": "到" -        }, -        "from-relations": "向外的關聯", -        "to-relations": "向內的關聯", -        "selected-relations": "{ count, plural, 1 {1 關聯} other {# 關聯} } 被選中", -        "type": "類型", -        "to-entity-type": "到實體類型", -        "to-entity-name": "到實體名稱", -        "from-entity-type": "從實體類型", -        "from-entity-name": "從實體類型", -        "to-entity": "到實體", -        "from-entity": "從實體", -        "delete": "刪除關聯", -        "relation-type": "關聯類型", -        "relation-type-required": "關聯類型必填", -        "any-relation-type": "任意類型", -        "add": "增加關聯", -        "edit": "編輯關聯", -        "delete-to-relation-title": "確定要刪除實體 '{{entityName}}' 的關聯嗎?", -        "delete-to-relation-text": "確定刪除後實體 '{{entityName}}' 將取消與當前實體的關聯關係。", -        "delete-to-relations-title": "確定要刪除 { count, plural, 1 {1 關聯} other {# 關聯} }?", -        "delete-to-relations-text": "確定刪除所有選擇的關聯關係後,與當前實體對應的所有關聯關係將被移除。", -        "delete-from-relation-title": "確定要從實體 '{{entityName}}' 刪除關聯嗎?", -        "delete-from-relation-text": "確定刪除後,當前實體將與實體 '{{entityName}}' 取消關聯", -        "delete-from-relations-title": "確定刪除 { count, plural, 1 {1 關聯} other {# 關聯} } 嗎?", -        "delete-from-relations-text": "確定刪除所有選擇的關聯關係後,當前實體將與對應的實體取消關聯", -        "remove-relation-filter": "移除關聯過濾器", -        "add-relation-filter": "增加關聯過濾器", -        "any-relation": "任意關聯", -        "relation-filters": "關聯過濾器", -        "additional-info": "附加訊息 (JSON)", -        "invalid-additional-info": "無法解析附加訊息json。" -    }, - "rulechain": { -        "rulechain": "規則鏈", -        "rulechains": "規則鏈庫", -        "root": "根實體", -        "delete": "刪除規則", -        "activate": "啟動規則", -        "suspend": "暫停規則", -        "active": "啟動", -        "suspended": "暫停", -        "name": "名稱", -        "name-required": "名稱必填。", -        "description": "描述", -        "add": "增加規則", -        "set-root": "建立規則鏈根", -        "set-root-rulechain-title": "您確定要生成規則鏈'{{RuleChainName}}'根嗎?", -        "set-root-rulechain-text": "確認之後,規則鏈將變為根規格鏈,並將處理所有傳入的傳輸消息。", -        "delete-rulechain-title": " 確實要刪除規則鏈'{{ruleChainName}}'嗎?", -        "delete-rulechain-text": "小心,在確認規則鏈和所有相關資料將變得無法恢復。", -        "delete-rulechains-title": "確實要刪除{count, plural, 1 { 1 規則鏈}其他{# 規則鏈庫}}嗎?", -        "delete-rulechains-action-title": "刪除 { count, plural, 1 {1 規則鏈} other {# 規則鏈庫} }", -        "delete-rulechains-text": "小心,確認後,所有選擇的規則鏈將被刪除,所有相關的資料將變得無法恢復。", -        "add-rulechain-text": "增加新的規則鏈", -        "no-rulechains-text": "規則鏈沒有發現", -        "rulechain-details": "規則鏈詳細資訊", -        "details": "詳細資訊", -        "events": "事件", -        "system": "系統", -        "import": "匯入規則", -        "export": "匯出規則", -        "export-failed-error": "無法匯出規則:{{error}}", -        "create-new-rulechain": "建立新的規則鏈", -        "rulechain-file": "規則鏈文件", -        "invalid-rulechain-file-error": "不能匯入規則鏈:無效的規則鏈資料格式。", -        "copyId": "複製規則鏈ID", -        "idCopiedMessage": "規則ID已經複製到剪貼板", -        "select-rulechain": "選擇規則鏈", -        "no-rulechains-matching": "沒有發現符合'{{entity}}'的規則鏈。", -        "rulechain-required": "規則鏈必填", -        "management": "規則集管理", -        "debug-mode": "測試模式" -    }, - "rulenode": { -        "details": "詳細資訊", -        "events": "事件", -        "search": "搜尋節點", -        "open-node-library": "打開節點庫", -        "add": "增加規則節點", -        "name": "名稱", -        "name-required": "名稱必填。", -        "type": "類型", -        "description": "描述", -        "delete": "刪除規則節點", -        "select-all-objects": "選擇所有節點和連接", -        "deselect-all-objects": "取消選擇所有節點和連接", -        "delete-selected-objects": "刪除選擇的節點和連接", -        "delete-selected": "刪除選擇", -        "select-all": "選擇全部", -        "copy-selected": "選擇副本", -        "deselect-all": "取消選擇", -        "rulenode-details": "規則節點詳細資訊", -        "debug-mode": "測試模式", -        "configuration": "配置", -        "link": "連結", -        "link-details": "規則節點連結詳細資訊", -        "add-link": "增加連結", -        "link-label": "連結標籤", -        "link-label-required": "連結標籤必填", -        "custom-link-label": "自定義連結標籤", -        "custom-link-label-required": "自定義連結標籤必填", -        "type-filter": "濾波器", -        "type-filter-details": "使用配置條件過濾傳入消息", -        "type-enrichment": "屬性集", -        "type-enrichment-details": "向消息元資料中增加附加訊息", -        "type-transformation": "變換", -        "type-transformation-details": "更改消息有效載荷和元資料", -        "type-action": "動作", -        "type-action-details": "執行特別動作", -        "type-external": "外部的", -        "type-external-details": "與外部系統交互", -        "type-rule-chain": "規則鏈", -        "type-rule-chain-details": "將傳入消息轉發到指定的規則鏈", -        "type-in​​put": "輸入", -        "type-in​​put-details": "規則鏈的邏輯輸入,將傳入消息轉發到下一個相關規則節點", -        "directive-is-not-loaded": "定義的配置指令 '{{directiveName}}' 不可用。", -        "ui-resources-load-error": "加載配置UI資源失敗。", -        "invalid-target-rulechain": "無法解析目標規則鏈!", -        "test-script-function": "測試腳本功能", -        "message": "消息", -        "message-type": "消息類型", -        "message-type-required": "消息類型必填", -        "metadata": "元資料", -        "metadata-required": "元資料項不能為空。", -        "output": "輸出", -        "test": "測試", -        "help": "幫助" -    }, -    "tenant": { -        "tenant": "租戶", -        "tenants": "租戶", -        "management": "租戶管理", -        "add": "增加租戶", -        "admins": "管理員", -        "manage-tenant-admins": "管理租戶管理員", -        "delete": "刪除租戶", -        "add-tenant-text": "增加新租戶", -        "no-tenants-text": "沒有找到租戶", -        "tenant-details": "租客詳細資訊", -        "delete-tenant-title": "您確定要刪除租戶'{{tenantTitle}}'嗎?", -        "delete-tenant-text": "小心!確認後,租戶和所有相關資料將無法恢復。", -        "delete-tenants-title": "您確定要刪除 {count,plural,1 {1 租戶} other {# 租戶}} 嗎?", -        "delete-tenants-action-title": "刪除 { count, plural, 1 {1 租戶} other {# 租戶} }", -        "delete-tenants-text": "小心!確認後,所有選擇的租戶將被刪除,所有相關資料將無法恢復。", -        "title": "標題", -        "title-required": "標題必填。", -        "description": "描述", -        "details": "詳細資訊", -        "events": "事件", -        "copyId": "複製租戶ID", -        "idCopiedMessage": "租戶ID已經複製到剪貼板", -        "select-tenant": "選擇租戶", -        "no-tenants-matching": "沒有找到符合 '{{entity}}' 的租戶", -        "tenant-required": "租戶必填" -    }, -    "timeinterval": { -        "seconds-interval": "{ seconds, plural, 1 {1 秒} other {# 秒} }", -        "minutes-interval": "{ minutes, plural, 1 {1 分} other {# 分} }", -        "hours-interval": "{ hours, plural, 1 {1 小時} other {# 小時} }", -        "days-interval": "{ days, plural, 1 {1 天} other {# 天} }", -        "days": "天", -        "hours": "時", -        "minutes": "分", -        "seconds": "秒", -        "advanced": "高級" -    }, - "timewindow": { -        "days": "{ days, plural, 1 { 天 } other {# 天 } }", -        "hours": "{ hours, plural, 0 { 小時 } 1 {1 小時 } other {# 小時 } }", -        "minutes": "{ minutes, plural, 0 { 分 } 1 {1 分 } other {# 分 } }", -        "seconds": "{ seconds, plural, 0 { 秒 } 1 {1 秒 } other {# 秒 } }", -        "realtime": "實時", -        "history": "歷史", -        "last-prefix": "最後", -        "period": "從 {{ startTime }} 到 {{ endTime }}", -        "edit": "編輯時間窗口", -        "date-range": "日期範圍", -        "last": "最後", -        "time-period": "時間段" -    }, -    "user": { -        "user": "用戶", -        "users": "用戶", -        "customer-users": "客戶用戶", -        "tenant-admins": "租戶管理員", -        "sys-admin": "系統管理員", -        "tenant-admin": "租戶管理員", -        "customer": "客戶", -        "anonymous": "匿名", -        "add": "增加用戶", -        "delete": "刪除用戶", -        "add-user-text": "增加新用戶", -        "no-users-text": "找不到用戶", -        "user-details": "用戶詳細訊息", -        "delete-user-title": "您確定要刪除用戶 '{{userEmail}}' 嗎?", -        "delete-user-text": "小心!確認後,用戶和所有相關資料將無法恢復。", -        "delete-users-title": "你確定你要刪除 { count, plural, 1 {1 用戶} other {# 用戶} } 嗎?", -        "delete-users-action-title": "刪除 { count, plural, 1 {1 用戶} other {# 用戶} }", -        "delete-users-text": "小心!確認後,所有選擇的用戶將被刪除,所有相關資料將無法恢復。", -        "activation-email-sent-message": "啟動電子郵件已成功發送!", -        "resend-activation": "重新發送啟動", -        "email": "電子郵件", -        "email-required": "電子郵件必填。", -        "invalid-email-format": "無效的郵件格式。", -        "first-name": "名字", -        "last-name": "姓", -        "description": "描述", -        "default-dashboard": "預設面板", -        "always-fullscreen": "始終全螢幕", -        "select-user": "選擇用戶", -        "no-users-matching": "沒有找到符合 '{{entity}}' 的用戶。", -        "user-required": "用戶必填", -        "activation-method": "啟動方式", -        "display-activation-link": "顯示啟動連結", -        "send-activation-mail": "發送啟動郵件", -        "activation-link": "用戶啟動連結", -        "activation-link-text": "使用該連結 啟動 啟動用戶:", -        "copy-activation-link": "複製用戶啟動連結", -        "activation-link-copied-message": "用戶啟動連結已經複製到剪貼板", -        "details": "詳細訊息" -    }, -    "value": { -        "type": "值類型", -        "string": "字符串", -        "string-value": "字符串值", -        "integer": "數字", -        "integer-value": "數字值", -        "invalid-integer-value": "整數值無效", -        "double": "雙精度浮點數", -        "double-value": "雙精度浮點數值", -        "boolean": "布林", -        "boolean-value": "布林值", -        "false": "假", -        "true": "真", -        "long": "Long" -    }, -    "widget": { -        "widget-library": "部件庫", -        "widget-bundle": "部件包", -        "select-widgets-bundle": "選擇部件包", -        "management": "管理部件", -        "editor": "部件編輯器", -        "widget-type-not-found": "加載部件配置出錯。
    可能關聯的\n 部件已經刪除了。", -        "widget-type-load-error": "由於以下錯誤未加載小部件:", -        "remove": "刪除部件", -        "edit": "編輯部件", -        "remove-widget-title": "確實要刪除 '{{widgetTitle}}'部件嗎?", -        "remove-widget-text": "確認後,控件和所有相關資料將變得無法恢復。", -        "timeseries": "時間序列", -        "search-data": "搜尋資料", -        "no-data-found": "沒有找到資料", -        "latest-values": "最新值", -        "rpc": "控件部件", -        "alarm": "警告部件", -        "static": "靜態部件", -        "select-widget-type": "選擇窗口部件類型", -        "missing-widget-title-error": "部件標題必須指定!", -        "widget-saved": "部件已儲存", -        "unable-to-save-widget-error": "無法儲存部件!控件有錯誤!", -        "save": "儲存部件", -        "saveAs": "部件另存為", -        "save-widget-type-as": "部件類型另存為", -        "save-widget-type-as-text": "請輸入新的部件標題或選擇目標部件包", -        "toggle-fullscreen": "切換全螢幕", -        "run": "執行部件", -        "title": "部件標題", -        "title-required": "需要部件標題。", -        "type": "部件類型", -        "resources": "資源", -        "resource-url": "JavaScript/CSS URL", -        "remove-resource": "刪除資源", -        "add-resource": "增加資源", -        "html": "HTML", -        "tidy": "整理", -        "css": "CSS", -        "settings-schema": "設定模式", -        "datakey-settings-schema": "資料鍵設定模式", -        "javascript": "Javascript", -        "remove-widget-type-title": "您確定要刪除部件類型 '{{widgetName}}'嗎?", -        "remove-widget-type-text": "確認後,窗口部件類型和所有相關資料將無法恢復。", -        "remove-widget-type": "刪除部件類型", -        "add-widget-type": "增加新的部件類型", -        "widget-type-load-failed-error": "無法加載部件類型!", -        "widget-template-load-failed-error": "無法加載部件模板!", -        "add": "增加部件", -        "undo": "復原部件更改", -        "export": "匯出部件" -    }, - "widget-action": { -        "header-button": "部件頂部按鈕", -        "open-dashboard-state": "切換到新儀表板狀態", -        "update-dashboard-state": "更新當前儀表板狀態", -        "open-dashboard": "切換到另一個儀表板", -        "custom": "自定義動作", -        "target-dashboard-state": "目標儀表板狀態", -        "target-dashboard-state-required": "目標儀表板狀態必填", -        "set-entity-from-widget": "從部件中設定實體", -        "target-dashboard": "目標儀表板", -        "open-right-layout": "打開右側佈局 (移動端視圖)" -    }, -    "widgets-bundle": { -        "current": "當前包", -        "widgets-bundles": "部件包", -        "add": "增加部件包", -        "delete": "刪除部件包", -        "title": "標題", -        "title-required": "標題必填。", -        "add-widgets-bundle-text": "增加新的部件包", -        "no-widgets-bundles-text": "找不到部件包", -        "empty": "部件包是空的", -        "details": "詳細資訊", -        "widgets-bundle-details": "部件包詳細訊息", -        "delete-widgets-bundle-title": "您確定要刪除部件包 '{{widgetsBundleTitle}}'嗎?", -        "delete-widgets-bundle-text": "小心!確認後,部件包和所有相關資料將無法恢復。", -        "delete-widgets-bundles-title": "你確定你要刪除 { count, plural, 1 {1 部件包} other {# 部件包} } 嗎?", -        "delete-widgets-bundles-action-title": "刪除 { count, plural, 1 {1 部件包} other {# 部件包} }", -        "delete-widgets-bundles-text": "小心!確認後,所有選擇的部件包將被刪除,所有相關資料將無法恢復。", -        "no-widgets-bundles-matching": "沒有找到與 '{{widgetsBundle}}' 符合的部件包。", -        "widgets-bundle-required": "需要部件包。", -        "system": "系統", -        "import": "匯入部件包", -        "export": "匯出部件包", -        "export-failed-error": "無法匯出部件包: {{error}}", -        "create-new-widgets-bundle": "建立新的部件包", -        "widgets-bundle-file": "部件包文件", -        "invalid-widgets-bundle-file-error": "無法匯入部件包:無效的部件包資料結構。" -    }, -    "widget-config": { -        "data": "資料", -        "settings": "設定", -        "advanced": "高級", -        "title": "標題", -        "general-settings": "一般設定", -        "display-title": "顯示標題", -        "drop-shadow": "陰影", -        "enable-fullscreen": "啟用全螢幕", -        "background-color": "背景顏色", -        "text-color": "文字顏色", -        "padding": "填充", -        "margin": "邊緣", -        "widget-style": "部件風格", -        "title-style": "標題風格", -        "mobile-mode-settings": "移動端設定", -        "order": "順序", -        "height": "高度", -        "units": "特殊符號展示值", -        "decimals": "浮點數後的位數", -        "timewindow": "時間窗口", -        "use-dashboard-timewindow": "使用儀表板的時間窗口", -        "display-legend": "顯示圖例", -        "datasources": "資料源", -        "maximum-datasources": "最大允許 { count, plural, 1 {1 資料} other {# 資料} }", -        "datasource-type": "類型", -        "datasource-parameters": "參數", -        "remove-datasource": "移除資料源", -        "add-datasource": "增加資料源", -        "target-device": "目標設備", -        "alarm-source": "警告源", -        "actions": "動作", -        "action": "動作", -        "add-action": "增加動作", -        "search-actions": "動作檢索", -        "action-source": "動作源", -        "action-source-required": "動作源必填", -        "action-name": "動作名稱", -        "action-name-required": "動作名稱必填。", -        "action-name-not-unique": "動作名稱已經存在。
    統一動作源的動作名稱必須唯一。", -        "action-icon": "圖示", -        "action-type": "類型", -        "action-type-required": "類型必填", -        "edit-action": "編輯動作", -        "delete-action": "刪除動作", -        "delete-action-title": "刪除部件動作", -        "delete-action-text": "確定要刪除部件動作 '{{actionName}}' 嗎?" -    }, -    "widget-type": { -        "import": "匯入部件類型", -        "export": "匯出部件類型", -        "export-failed-error": "無法匯出部件類型: {{error}}", -        "create-new-widget-type": "建立新的部件類型", -        "widget-type-file": "部件類型文件", -        "invalid-widget-type-file-error": "無法匯入部件類型:無效的部件類型資料結構。" -    }, - "widgets": { -        "date-range-navigator": { -            "localizationMap": { -                "Sun": "週日", -                "Mon": "週一", -                "Tue": "週二", -                "Wed": "週三", -                "Thu": "週四", -                "Fri": "週五", -                "Sat": "週六", -                "Jan": "1月", -                "Feb": "2月", -                "Mar": "3月", -                "Apr": "4月", -                "May": "5月", -                "Jun": "6月", -                "Jul": "7月", -                "Aug": "8月", -                "Sep": "9月", -                "Oct": "10月", -                "Nov": "11月", -                "Dec": "12月", -                "January": "一月", -                "February": "二月", -                "March": "三月", -                "April": "四月", -                "June": "六月", -                "July": "七月", -                "August": "八月", -                "September": "九月", -                "October": "十月", -                "November": "十一月", -                "December": "十二月", -                "Custom Date Range": "自定義日期範圍", -                "Date Range Template": "日期範圍模板", -                "Today": "今天", -                "Yesterday": "昨天", -                "This Week": "本星期", -                "Last Week": "上個星期", -                "This Month": "這個月", -                "Last Month": "上個月", -                "Year": "年", -                "This Year": "今年", -                "Last Year": "去年", -                "Date picker": "日期選擇器", -                "Hour": "小時", -                "Day": "天", -                "Week": "週", -                "2 weeks": "2週", -                "Month": "月", -                "3 months": "3個月", -                "6 months": "6個月", -                "Custom interval": "自定義間隔", -                "Interval": "間隔", -                "Step size": "步長", -                "Ok": "Ok" -            } -        } -    }, -    "icon": { -        "icon": "圖示", -        "select-icon": "選擇圖示", -        "material-icons": "素材圖示", -        "show-all": "顯示所有圖示" -    }, -    "custom": { -        "widget-action": { -            "action-cell-button": "動作單元格按鈕", -            "row-click": "點選行", -            "marker-click": "點選標記", -            "polygon-click": "單擊多邊形", -            "tooltip-tag-action": "提示標籤動作" -        } -    }, -    "language": { -        "language": "語言" -    } -} diff --git a/ui/src/app/locale/translate-handler.js b/ui/src/app/locale/translate-handler.js deleted file mode 100644 index 5dd5796c76..0000000000 --- a/ui/src/app/locale/translate-handler.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - export default angular.module('thingsboard.locale', []) - .factory('tbMissingTranslationHandler', ThingsboardMissingTranslateHandler) - .name; - -/*@ngInject*/ -function ThingsboardMissingTranslateHandler(/*$log, types*/) { - - return function (/*translationId*/) { - /*if (translationId && !translationId.startsWith(types.translate.customTranslationsPrefix)) { - $log.warn('Translation for ' + translationId + ' doesn\'t exist'); - }*/ - }; - -} \ No newline at end of file diff --git a/ui/src/app/login/create-password.controller.js b/ui/src/app/login/create-password.controller.js deleted file mode 100644 index 0802036a67..0000000000 --- a/ui/src/app/login/create-password.controller.js +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/*@ngInject*/ -export default function CreatePasswordController($stateParams, $translate, toast, loginService, userService) { - var vm = this; - - vm.password = ''; - vm.password2 = ''; - - vm.createPassword = createPassword; - - function createPassword() { - if (vm.password !== vm.password2) { - toast.showError($translate.instant('login.passwords-mismatch-error')); - } else { - loginService.activate($stateParams.activateToken, vm.password).then(function success(response) { - var token = response.data.token; - var refreshToken = response.data.refreshToken; - userService.setUserFromJwtToken(token, refreshToken, true); - }, function fail() { - }); - } - } -} diff --git a/ui/src/app/login/create-password.tpl.html b/ui/src/app/login/create-password.tpl.html deleted file mode 100644 index bedba02ce4..0000000000 --- a/ui/src/app/login/create-password.tpl.html +++ /dev/null @@ -1,56 +0,0 @@ - - - - diff --git a/ui/src/app/login/index.js b/ui/src/app/login/index.js deleted file mode 100644 index 1ff8d3dc55..0000000000 --- a/ui/src/app/login/index.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './login.scss'; - -import uiRouter from 'angular-ui-router'; -import thingsboardApiLogin from '../api/login.service'; -import thingsboardApiUser from '../api/user.service'; -import thingsboardToast from '../services/toast'; - -import LoginRoutes from './login.routes'; -import LoginController from './login.controller'; -import ResetPasswordRequestController from './reset-password-request.controller'; -import ResetPasswordController from './reset-password.controller'; -import CreatePasswordController from './create-password.controller'; - -export default angular.module('thingsboard.login', [ - uiRouter, - thingsboardApiLogin, - thingsboardApiUser, - thingsboardToast -]) - .config(LoginRoutes) - .controller('LoginController', LoginController) - .controller('ResetPasswordRequestController', ResetPasswordRequestController) - .controller('ResetPasswordController', ResetPasswordController) - .controller('CreatePasswordController', CreatePasswordController) - .name; diff --git a/ui/src/app/login/login.controller.js b/ui/src/app/login/login.controller.js deleted file mode 100644 index 8dcd2a9d8e..0000000000 --- a/ui/src/app/login/login.controller.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import logoSvg from '../../svg/logo_title_white.svg'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function LoginController(toast, loginService, userService, types, $state/*, $rootScope, $log, $translate*/) { - var vm = this; - - vm.logoSvg = logoSvg; - - vm.user = { - name: '', - password: '' - }; - - vm.login = login; - - function doLogin() { - loginService.login(vm.user).then(function success(response) { - var token = response.data.token; - var refreshToken = response.data.refreshToken; - userService.setUserFromJwtToken(token, refreshToken, true); - }, function fail(response) { - /*if (response && response.data && response.data.message) { - toast.showError(response.data.message); - } else if (response && response.statusText) { - toast.showError(response.statusText); - } else { - toast.showError($translate.instant('error.unknown-error')); - }*/ - if (response && response.data && response.data.errorCode) { - if (response.data.errorCode === types.serverErrorCode.credentialsExpired) { - $state.go('login.resetExpiredPassword', {resetToken: response.data.resetToken}); - } - } - }); - } - - function login() { - doLogin(); - } -} diff --git a/ui/src/app/login/login.routes.js b/ui/src/app/login/login.routes.js deleted file mode 100644 index ac9401147b..0000000000 --- a/ui/src/app/login/login.routes.js +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import loginTemplate from './login.tpl.html'; -import resetPasswordTemplate from './reset-password.tpl.html'; -import resetPasswordRequestTemplate from './reset-password-request.tpl.html'; -import createPasswordTemplate from './create-password.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function LoginRoutes($stateProvider) { - $stateProvider.state('login', { - url: '/login', - module: 'public', - views: { - "@": { - controller: 'LoginController', - controllerAs: 'vm', - templateUrl: loginTemplate - } - }, - data: { - pageTitle: 'login.login' - } - }).state('login.resetPasswordRequest', { - url: '/resetPasswordRequest', - module: 'public', - views: { - "@": { - controller: 'ResetPasswordRequestController', - controllerAs: 'vm', - templateUrl: resetPasswordRequestTemplate - } - }, - data: { - pageTitle: 'login.request-password-reset' - } - }).state('login.resetPassword', { - url: '/resetPassword?resetToken', - module: 'public', - views: { - "@": { - controller: 'ResetPasswordController', - controllerAs: 'vm', - templateUrl: resetPasswordTemplate - } - }, - data: { - pageTitle: 'login.reset-password' - } - }).state('login.resetExpiredPassword', { - url: '/resetExpiredPassword?resetToken', - module: 'public', - views: { - "@": { - controller: 'ResetPasswordController', - controllerAs: 'vm', - templateUrl: resetPasswordTemplate - } - }, - data: { - expiredPassword: true, - pageTitle: 'login.reset-password' - } - }).state('login.createPassword', { - url: '/createPassword?activateToken', - module: 'public', - views: { - "@": { - controller: 'CreatePasswordController', - controllerAs: 'vm', - templateUrl: createPasswordTemplate - } - }, - data: { - pageTitle: 'login.create-password' - } - }); -} diff --git a/ui/src/app/login/login.scss b/ui/src/app/login/login.scss deleted file mode 100644 index 16520544fb..0000000000 --- a/ui/src/app/login/login.scss +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "../../scss/constants"; - -md-card.tb-login-card { - width: 330px !important; - - @media (min-width: $layout-breakpoint-sm) { - width: 450px !important; - } - - .tb-padding { - padding: 8px; - } - - md-card-title { - img.tb-login-logo { - height: 50px; - } - } - - md-card-content { - margin-top: -50px; - } - - md-input-container .md-errors-spacer { - display: none; - } - - .oauth-container{ - .container-divider { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - width: 100%; - margin: 10px 0; - - .line { - flex: 1; - } - - .text { - padding-right: 10px; - padding-left: 10px; - } - } - - .material-icons{ - width: 20px; - min-width: 20px; - height: 20px; - min-height: 20px; - margin: 0 4px; - } - } -} diff --git a/ui/src/app/login/login.tpl.html b/ui/src/app/login/login.tpl.html deleted file mode 100644 index f12a207856..0000000000 --- a/ui/src/app/login/login.tpl.html +++ /dev/null @@ -1,64 +0,0 @@ - - - - diff --git a/ui/src/app/login/reset-password-request.controller.js b/ui/src/app/login/reset-password-request.controller.js deleted file mode 100644 index 21b4a820c6..0000000000 --- a/ui/src/app/login/reset-password-request.controller.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/*@ngInject*/ -export default function ResetPasswordRequestController($translate, toast, loginService) { - var vm = this; - - vm.email = ''; - - vm.sendResetPasswordLink = sendResetPasswordLink; - - function sendResetPasswordLink() { - loginService.sendResetPasswordLink(vm.email).then(function success() { - toast.showSuccess($translate.instant('login.password-link-sent-message')); - }, function fail() { - }); - } -} diff --git a/ui/src/app/login/reset-password-request.tpl.html b/ui/src/app/login/reset-password-request.tpl.html deleted file mode 100644 index adbb004c6c..0000000000 --- a/ui/src/app/login/reset-password-request.tpl.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - diff --git a/ui/src/app/login/reset-password.controller.js b/ui/src/app/login/reset-password.controller.js deleted file mode 100644 index b1e87d3a35..0000000000 --- a/ui/src/app/login/reset-password.controller.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/*@ngInject*/ -export default function ResetPasswordController($stateParams, $state, $translate, toast, loginService, userService) { - var vm = this; - - vm.newPassword = ''; - vm.newPassword2 = ''; - - vm.resetPassword = resetPassword; - - vm.isExpiredPassword = $state.$current.data.expiredPassword === true; - - function resetPassword() { - if (vm.newPassword !== vm.newPassword2) { - toast.showError($translate.instant('login.passwords-mismatch-error')); - } else { - loginService.resetPassword($stateParams.resetToken, vm.newPassword).then(function success(response) { - var token = response.data.token; - var refreshToken = response.data.refreshToken; - userService.setUserFromJwtToken(token, refreshToken, true); - }, function fail() { - }); - } - } -} diff --git a/ui/src/app/login/reset-password.tpl.html b/ui/src/app/login/reset-password.tpl.html deleted file mode 100644 index c3c83da648..0000000000 --- a/ui/src/app/login/reset-password.tpl.html +++ /dev/null @@ -1,57 +0,0 @@ - - - - diff --git a/ui/src/app/profile/change-password.controller.js b/ui/src/app/profile/change-password.controller.js deleted file mode 100644 index 3e35461e32..0000000000 --- a/ui/src/app/profile/change-password.controller.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/*@ngInject*/ -export default function ChangePasswordController($scope, $translate, toast, $mdDialog, loginService) { - var vm = this; - - vm.currentPassword = ''; - vm.newPassword = ''; - vm.newPassword2 = ''; - - vm.cancel = cancel; - vm.changePassword = changePassword; - - function cancel() { - $mdDialog.cancel(); - } - - function changePassword() { - if (vm.newPassword !== vm.newPassword2) { - toast.showError($translate.instant('login.passwords-mismatch-error')); - } else { - loginService.changePassword(vm.currentPassword, vm.newPassword).then(function success() { - $scope.theForm.$setPristine(); - $mdDialog.hide(); - }); - } - } -} diff --git a/ui/src/app/profile/change-password.tpl.html b/ui/src/app/profile/change-password.tpl.html deleted file mode 100644 index 04abe20c4f..0000000000 --- a/ui/src/app/profile/change-password.tpl.html +++ /dev/null @@ -1,64 +0,0 @@ - - -
    - -
    -

    profile.change-password

    - - - - -
    -
    - - - -
    - - - - lock - - - - - - - lock - - - - - - - lock - - - -
    -
    - - - - {{ 'profile.change-password' | translate }} - - {{ 'action.cancel' | translate }} - -
    -
    diff --git a/ui/src/app/profile/index.js b/ui/src/app/profile/index.js deleted file mode 100644 index 6f4c460018..0000000000 --- a/ui/src/app/profile/index.js +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import uiRouter from 'angular-ui-router'; -import ngMaterial from 'angular-material'; -import ngMessages from 'angular-messages'; -import thingsboardApiUser from '../api/user.service'; -import thingsboardApiLogin from '../api/login.service'; -import thingsboardConfirmOnExit from '../components/confirm-on-exit.directive'; - -import ProfileRoutes from './profile.routes'; -import ProfileController from './profile.controller'; -import ChangePasswordController from './change-password.controller'; - -export default angular.module('thingsboard.profile', [ - uiRouter, - ngMaterial, - ngMessages, - thingsboardApiUser, - thingsboardApiLogin, - thingsboardConfirmOnExit -]) - .config(ProfileRoutes) - .controller('ProfileController', ProfileController) - .controller('ChangePasswordController', ChangePasswordController) - .name; diff --git a/ui/src/app/profile/profile.controller.js b/ui/src/app/profile/profile.controller.js deleted file mode 100644 index b28235134d..0000000000 --- a/ui/src/app/profile/profile.controller.js +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import changePasswordTemplate from './change-password.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function ProfileController(userService, $scope, $document, $mdDialog, $translate) { - var vm = this; - - vm.profileUser = {}; - vm.save = save; - vm.changePassword = changePassword; - vm.languageList = SUPPORTED_LANGS; //eslint-disable-line - - loadProfile(); - - function loadProfile() { - userService.getUser(userService.getCurrentUser().userId).then(function success(user) { - vm.profileUser = user; - if (!vm.profileUser.additionalInfo) { - vm.profileUser.additionalInfo = {}; - } - if (!vm.profileUser.additionalInfo.lang) { - vm.profileUser.additionalInfo.lang = $translate.use(); - } - }); - } - - function save() { - userService.saveUser(vm.profileUser).then(function success(user) { - $translate.use(vm.profileUser.additionalInfo.lang); - vm.profileUser = user; - $scope.theForm.$setPristine(); - }); - } - - function changePassword($event) { - $mdDialog.show({ - controller: 'ChangePasswordController', - controllerAs: 'vm', - templateUrl: changePasswordTemplate, - parent: angular.element($document[0].body), - fullscreen: true, - targetEvent: $event - }).then(function () { - }, function () { - }); - } -} diff --git a/ui/src/app/profile/profile.routes.js b/ui/src/app/profile/profile.routes.js deleted file mode 100644 index c49255762c..0000000000 --- a/ui/src/app/profile/profile.routes.js +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import profileTemplate from './profile.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function ProfileRoutes($stateProvider) { - - $stateProvider - .state('home.profile', { - url: '/profile', - module: 'private', - auth: ['SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER'], - views: { - "content@home": { - templateUrl: profileTemplate, - controllerAs: 'vm', - controller: 'ProfileController' - } - }, - data: { - pageTitle: 'profile.profile' - }, - ncyBreadcrumb: { - label: '{"icon": "account_circle", "label": "profile.profile"}' - } - }); - -} diff --git a/ui/src/app/profile/profile.tpl.html b/ui/src/app/profile/profile.tpl.html deleted file mode 100644 index f7478fff60..0000000000 --- a/ui/src/app/profile/profile.tpl.html +++ /dev/null @@ -1,68 +0,0 @@ - -
    - - - - profile.profile - {{ vm.profileUser.email }} - - - profile.last-login-time - {{ vm.profileUser.additionalInfo.lastLoginTs | date : 'yyyy-MM-dd HH:mm:ss' }} - - - - - -
    -
    - - - - - - - - - - - - - - - - - {{ lang ? ('language.locales.' + lang | translate) : ''}} - - - - {{ 'profile.change-password' | translate }} - -
    - {{ 'action.save' | translate }} - -
    -
    -
    -
    -
    -
    - diff --git a/ui/src/app/rulechain/add-link.tpl.html b/ui/src/app/rulechain/add-link.tpl.html deleted file mode 100644 index dd6f70876e..0000000000 --- a/ui/src/app/rulechain/add-link.tpl.html +++ /dev/null @@ -1,48 +0,0 @@ - - -
    - -
    -

    rulenode.add-link

    - -
    - - - -
    -
    - - - -
    - -
    -
    - - - - {{ 'action.add' | translate }} - - {{ 'action.cancel' | - translate }} - - -
    -
    diff --git a/ui/src/app/rulechain/add-rulechain.tpl.html b/ui/src/app/rulechain/add-rulechain.tpl.html deleted file mode 100644 index 113b4e2c6a..0000000000 --- a/ui/src/app/rulechain/add-rulechain.tpl.html +++ /dev/null @@ -1,48 +0,0 @@ - - -
    - -
    -

    rulechain.add

    - -
    - - - -
    -
    - - - -
    - -
    -
    - - - - {{ 'action.add' | translate }} - - {{ 'action.cancel' | - translate }} - - -
    -
    diff --git a/ui/src/app/rulechain/add-rulenode.tpl.html b/ui/src/app/rulechain/add-rulenode.tpl.html deleted file mode 100644 index 3adb08cd30..0000000000 --- a/ui/src/app/rulechain/add-rulenode.tpl.html +++ /dev/null @@ -1,48 +0,0 @@ - - -
    - -
    -

    rulenode.add

    - -
    - - - -
    -
    - - - -
    - -
    -
    - - - - {{ 'action.add' | translate }} - - {{ 'action.cancel' | - translate }} - - -
    -
    diff --git a/ui/src/app/rulechain/index.js b/ui/src/app/rulechain/index.js deleted file mode 100644 index bcc6126295..0000000000 --- a/ui/src/app/rulechain/index.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import RuleChainRoutes from './rulechain.routes'; -import RuleChainsController from './rulechains.controller'; -import {RuleChainController, AddRuleNodeController, AddRuleNodeLinkController} from './rulechain.controller'; -import NodeScriptTestController from './script/node-script-test.controller'; -import RuleChainDirective from './rulechain.directive'; -import RuleNodeDefinedConfigDirective from './rulenode-defined-config.directive'; -import RuleNodeConfigDirective from './rulenode-config.directive'; -import RuleNodeDirective from './rulenode.directive'; -import LinkDirective from './link.directive'; -import MessageTypeAutocompleteDirective from './message-type-autocomplete.directive'; -import NodeScriptTest from './script/node-script-test.service'; - -export default angular.module('thingsboard.ruleChain', []) - .config(RuleChainRoutes) - .controller('RuleChainsController', RuleChainsController) - .controller('RuleChainController', RuleChainController) - .controller('AddRuleNodeController', AddRuleNodeController) - .controller('AddRuleNodeLinkController', AddRuleNodeLinkController) - .controller('NodeScriptTestController', NodeScriptTestController) - .directive('tbRuleChain', RuleChainDirective) - .directive('tbRuleNodeDefinedConfig', RuleNodeDefinedConfigDirective) - .directive('tbRuleNodeConfig', RuleNodeConfigDirective) - .directive('tbRuleNode', RuleNodeDirective) - .directive('tbRuleNodeLink', LinkDirective) - .directive('tbMessageTypeAutocomplete', MessageTypeAutocompleteDirective) - .factory('ruleNodeScriptTest', NodeScriptTest) - .name; diff --git a/ui/src/app/rulechain/link-fieldset.tpl.html b/ui/src/app/rulechain/link-fieldset.tpl.html deleted file mode 100644 index e0780e7d66..0000000000 --- a/ui/src/app/rulechain/link-fieldset.tpl.html +++ /dev/null @@ -1,60 +0,0 @@ - - -
    - - - - {{item}} - -
    -
    - rulenode.no-link-labels-found -
    -
    - rulenode.no-link-label-matching - - rulenode.create-new-link-label - -
    -
    -
    -
    - - {{$chip.name}} - -
    - -
    -
    diff --git a/ui/src/app/rulechain/link.directive.js b/ui/src/app/rulechain/link.directive.js deleted file mode 100644 index 94ea4b94c3..0000000000 --- a/ui/src/app/rulechain/link.directive.js +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './link.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import linkFieldsetTemplate from './link-fieldset.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function LinkDirective($compile, $templateCache, $filter) { - var linker = function (scope, element, attrs, ngModelCtrl) { - var template = $templateCache.get(linkFieldsetTemplate); - element.html(template); - - scope.selectedLabel = null; - scope.labelSearchText = null; - - scope.ngModelCtrl = ngModelCtrl; - - var labelsList = []; - - scope.transformLinkLabelChip = function (chip) { - var res = $filter('filter')(labelsList, {name: chip}, true); - var result; - if (res && res.length) { - result = angular.copy(res[0]); - } else { - result = { - name: chip, - value: chip - }; - } - return result; - }; - - scope.labelsSearch = function (searchText) { - var labels = searchText ? $filter('filter')(labelsList, {name: searchText}) : labelsList; - return labels.map((label) => label.name); - }; - - scope.createLinkLabel = function (event, chipsId) { - var chipsChild = angular.element(chipsId, element)[0].firstElementChild; - var el = angular.element(chipsChild); - var chipBuffer = el.scope().$mdChipsCtrl.getChipBuffer(); - event.preventDefault(); - event.stopPropagation(); - el.scope().$mdChipsCtrl.appendChip(chipBuffer.trim()); - el.scope().$mdChipsCtrl.resetChipBuffer(); - }; - - - ngModelCtrl.$render = function () { - labelsList.length = 0; - for (var label in scope.allowedLabels) { - var linkLabel = { - name: scope.allowedLabels[label].name, - value: scope.allowedLabels[label].value - }; - labelsList.push(linkLabel); - } - - var link = ngModelCtrl.$viewValue; - var labels = []; - if (link && link.labels) { - for (var i = 0; i < link.labels.length; i++) { - label = link.labels[i]; - if (scope.allowedLabels[label]) { - labels.push(angular.copy(scope.allowedLabels[label])); - } else { - labels.push({ - name: label, - value: label - }); - } - } - } - scope.labels = labels; - scope.$watch('labels', function (newVal, prevVal) { - if (!angular.equals(newVal, prevVal)) { - updateLabels(); - } - }, true); - }; - - function updateLabels() { - if (ngModelCtrl.$viewValue) { - var labels = []; - for (var i = 0; i < scope.labels.length; i++) { - labels.push(scope.labels[i].value); - } - ngModelCtrl.$viewValue.labels = labels; - ngModelCtrl.$viewValue.label = labels.join(' / '); - updateValidity(); - } - } - - function updateValidity() { - var valid = ngModelCtrl.$viewValue.labels && - ngModelCtrl.$viewValue.labels.length ? true : false; - ngModelCtrl.$setValidity('linkLabels', valid); - } - - $compile(element.contents())(scope); - } - return { - restrict: "E", - require: "^ngModel", - link: linker, - scope: { - allowedLabels: '=', - allowCustom: '=', - isEdit: '=', - isReadOnly: '=' - } - }; -} diff --git a/ui/src/app/rulechain/link.scss b/ui/src/app/rulechain/link.scss deleted file mode 100644 index baa2489a13..0000000000 --- a/ui/src/app/rulechain/link.scss +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-link-label-autocomplete { - .tb-not-found { - display: block; - height: 48px; - line-height: 1.5; - - .tb-no-entries { - line-height: 48px; - } - } - - li { - height: auto !important; - white-space: normal !important; - } -} diff --git a/ui/src/app/rulechain/message-type-autocomplete.directive.js b/ui/src/app/rulechain/message-type-autocomplete.directive.js deleted file mode 100644 index 088dea659e..0000000000 --- a/ui/src/app/rulechain/message-type-autocomplete.directive.js +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './message-type-autocomplete.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import messageTypeAutocompleteTemplate from './message-type-autocomplete.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function MessageTypeAutocomplete($compile, $templateCache, $q, $filter, types) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - var template = $templateCache.get(messageTypeAutocompleteTemplate); - element.html(template); - - var messageTypeList = []; - for (var t in types.messageType) { - var type = types.messageType[t]; - messageTypeList.push(type); - } - - scope.messageType = null; - scope.messageTypeSearchText = ''; - - scope.fetchMessageTypes = function(searchText) { - var deferred = $q.defer(); - var result = $filter('filter')(messageTypeList, {'name': searchText}); - if (result && result.length) { - deferred.resolve(result); - } else { - deferred.resolve([{name: searchText, value: searchText}]); - } - return deferred.promise; - }; - - scope.messageTypeSearchTextChanged = function() { - }; - - scope.updateView = function () { - if (!scope.disabled) { - var value = null; - if (scope.messageType) { - value = scope.messageType.value; - } - ngModelCtrl.$setViewValue(value); - } - }; - - ngModelCtrl.$render = function () { - var value = ngModelCtrl.$viewValue; - if (value) { - var result = $filter('filter')(messageTypeList, {'value': value}, true); - if (result && result.length) { - scope.messageType = result[0]; - } else { - scope.messageType = { - name: value, - value: value - }; - } - } else { - scope.messageType = null; - } - }; - - scope.$watch('messageType', function (newValue, prevValue) { - if (!angular.equals(newValue, prevValue)) { - scope.updateView(); - } - }); - - $compile(element.contents())(scope); - } - - return { - restrict: "E", - require: "^ngModel", - link: linker, - scope: { - theForm: '=?', - disabled:'=ngDisabled', - required:'=ngRequired' - } - }; -} diff --git a/ui/src/app/rulechain/message-type-autocomplete.scss b/ui/src/app/rulechain/message-type-autocomplete.scss deleted file mode 100644 index b88211993e..0000000000 --- a/ui/src/app/rulechain/message-type-autocomplete.scss +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-message-type-autocomplete { - .tb-message-type-item { - display: block; - height: 48px; - } - - li { - height: auto !important; - white-space: normal !important; - } -} diff --git a/ui/src/app/rulechain/message-type-autocomplete.tpl.html b/ui/src/app/rulechain/message-type-autocomplete.tpl.html deleted file mode 100644 index 5c96969847..0000000000 --- a/ui/src/app/rulechain/message-type-autocomplete.tpl.html +++ /dev/null @@ -1,41 +0,0 @@ - - - -
    - {{item.name}} -
    -
    -
    -
    rulenode.message-type-required
    -
    -
    diff --git a/ui/src/app/rulechain/rulechain-card.tpl.html b/ui/src/app/rulechain/rulechain-card.tpl.html deleted file mode 100644 index dad4ab624d..0000000000 --- a/ui/src/app/rulechain/rulechain-card.tpl.html +++ /dev/null @@ -1,18 +0,0 @@ - -
    rulechain.root
    diff --git a/ui/src/app/rulechain/rulechain-fieldset.tpl.html b/ui/src/app/rulechain/rulechain-fieldset.tpl.html deleted file mode 100644 index 4afd6ee96e..0000000000 --- a/ui/src/app/rulechain/rulechain-fieldset.tpl.html +++ /dev/null @@ -1,57 +0,0 @@ - -{{ 'rulechain.export' | translate }} -{{ 'rulechain.set-root' | translate }} -{{ 'rulechain.delete' | translate }} - -
    - - - rulechain.copyId - -
    - - -
    - - - -
    -
    rulechain.name-required
    -
    -
    - - {{ 'rulechain.debug-mode' | translate }} - - - - - - -
    -
    diff --git a/ui/src/app/rulechain/rulechain.controller.js b/ui/src/app/rulechain/rulechain.controller.js deleted file mode 100644 index af7bdbf8d9..0000000000 --- a/ui/src/app/rulechain/rulechain.controller.js +++ /dev/null @@ -1,1410 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './rulechain.scss'; - -import 'tooltipster/dist/css/tooltipster.bundle.min.css'; -import 'tooltipster/dist/js/tooltipster.bundle.min.js'; -import 'tooltipster/dist/css/plugins/tooltipster/sideTip/themes/tooltipster-sideTip-shadow.min.css'; - -/* eslint-disable import/no-unresolved, import/default */ - -import addRuleNodeTemplate from './add-rulenode.tpl.html'; -import addRuleNodeLinkTemplate from './add-link.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $timeout, $mdExpansionPanel, $window, $document, $mdDialog, - $filter, $translate, hotkeys, types, ruleChainService, itembuffer, Modelfactory, flowchartConstants, - ruleChain, ruleChainMetaData, ruleNodeComponents, helpLinks) { - - var vm = this; - - vm.$mdExpansionPanel = $mdExpansionPanel; - vm.types = types; - - if ($state.current.data.import && !ruleChain) { - $state.go('home.ruleChains'); - return; - } - - vm.isImport = $state.current.data.import; - vm.isConfirmOnExit = false; - - $scope.$watch(function() { - return vm.isDirty || vm.isImport; - }, (val) => { - vm.isConfirmOnExit = val; - }); - - vm.errorTooltips = {}; - - vm.isFullscreen = false; - - vm.editingRuleNode = null; - vm.isEditingRuleNode = false; - - vm.editingRuleNodeLink = null; - vm.isEditingRuleNodeLink = false; - - vm.isLibraryOpen = true; - vm.enableHotKeys = true; - - Object.defineProperty(vm, 'isLibraryOpenReadonly', { - get: function() { return vm.isLibraryOpen }, - set: function() {} - }); - - vm.ruleNodeSearch = ''; - - vm.ruleChain = ruleChain; - vm.ruleChainMetaData = ruleChainMetaData; - - vm.canvasControl = {}; - - vm.ruleChainModel = { - nodes: [], - edges: [] - }; - - vm.ruleNodeTypesModel = {}; - vm.ruleNodeTypesCanvasControl = {}; - vm.ruleChainLibraryLoaded = false; - for (var type in types.ruleNodeType) { - if (!types.ruleNodeType[type].special) { - vm.ruleNodeTypesModel[type] = { - model: { - nodes: [], - edges: [] - }, - selectedObjects: [] - }; - vm.ruleNodeTypesCanvasControl[type] = {}; - } - } - - - - vm.selectedObjects = []; - - vm.modelservice = Modelfactory(vm.ruleChainModel, vm.selectedObjects); - - vm.saveRuleChain = saveRuleChain; - vm.revertRuleChain = revertRuleChain; - - vm.objectsSelected = objectsSelected; - vm.deleteSelected = deleteSelected; - - vm.isDebugModeEnabled = isDebugModeEnabled; - vm.resetDebugModeInAllNodes = resetDebugModeInAllNodes; - - vm.triggerResize = triggerResize; - - vm.openRuleChainContextMenu = openRuleChainContextMenu; - - vm.helpLinkIdForRuleNodeType = helpLinkIdForRuleNodeType; - - initHotKeys(); - - function openRuleChainContextMenu($event, $mdOpenMousepointMenu) { - if (vm.canvasControl.modelservice && !$event.ctrlKey && !$event.metaKey) { - var x = $event.clientX; - var y = $event.clientY; - var item = vm.canvasControl.modelservice.getItemInfoAtPoint(x, y); - vm.contextInfo = prepareContextMenu(item); - if (vm.contextInfo.items && vm.contextInfo.items.length > 0) { - vm.contextMenuEvent = $event; - $mdOpenMousepointMenu($event); - return false; - } - } - } - - function prepareContextMenu(item) { - if (objectsSelected() || (!item.node && !item.edge)) { - return prepareRuleChainContextMenu(); - } else if (item.node) { - return prepareRuleNodeContextMenu(item.node); - } else if (item.edge) { - return prepareEdgeContextMenu(item.edge); - } - } - - function prepareRuleChainContextMenu() { - var contextInfo = { - headerClass: 'tb-rulechain', - icon: 'settings_ethernet', - title: vm.ruleChain.name, - subtitle: $translate.instant('rulechain.rulechain') - }; - contextInfo.items = []; - if (vm.modelservice.nodes.getSelectedNodes().length) { - contextInfo.items.push( - { - action: function () { - copyRuleNodes(); - }, - enabled: true, - value: "rulenode.copy-selected", - icon: "content_copy", - shortcut: "M-C" - } - ); - } - contextInfo.items.push( - { - action: function ($event) { - pasteRuleNodes($event); - }, - enabled: itembuffer.hasRuleNodes(), - value: "action.paste", - icon: "content_paste", - shortcut: "M-V" - } - ); - contextInfo.items.push( - { - divider: true - } - ); - if (objectsSelected()) { - contextInfo.items.push( - { - action: function () { - vm.modelservice.deselectAll(); - }, - enabled: true, - value: "rulenode.deselect-all", - icon: "tab_unselected", - shortcut: "Esc" - } - ); - contextInfo.items.push( - { - action: function () { - vm.modelservice.deleteSelected(); - }, - enabled: true, - value: "rulenode.delete-selected", - icon: "clear", - shortcut: "Del" - } - ); - } else { - contextInfo.items.push( - { - action: function () { - vm.modelservice.selectAll(); - }, - enabled: true, - value: "rulenode.select-all", - icon: "select_all", - shortcut: "M-A" - } - ); - } - contextInfo.items.push( - { - divider: true - } - ); - contextInfo.items.push( - { - action: function () { - vm.saveRuleChain(); - }, - enabled: !(vm.isInvalid || (!vm.isDirty && !vm.isImport)), - value: "action.apply-changes", - icon: "done", - shortcut: "M-S" - } - ); - contextInfo.items.push( - { - action: function () { - vm.revertRuleChain(); - }, - enabled: vm.isDirty, - value: "action.decline-changes", - icon: "close", - shortcut: "M-Z" - } - ); - return contextInfo; - } - - function prepareRuleNodeContextMenu(node) { - var contextInfo = { - headerClass: node.nodeClass, - icon: node.icon, - iconUrl: node.iconUrl, - title: node.name, - subtitle: node.component.name - }; - contextInfo.items = []; - if (!node.readonly) { - contextInfo.items.push( - { - action: function () { - openNodeDetails(node); - }, - enabled: true, - value: "rulenode.details", - icon: "menu" - } - ); - contextInfo.items.push( - { - action: function () { - copyNode(node); - }, - enabled: true, - value: "action.copy", - icon: "content_copy" - } - ); - contextInfo.items.push( - { - action: function () { - vm.canvasControl.modelservice.nodes.delete(node); - }, - enabled: true, - value: "action.delete", - icon: "clear", - shortcut: "M-X" - } - ); - } - return contextInfo; - } - - function prepareEdgeContextMenu(edge) { - var contextInfo = { - headerClass: 'tb-link', - icon: 'trending_flat', - title: edge.label, - subtitle: $translate.instant('rulenode.link') - }; - contextInfo.items = []; - var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source); - if (sourceNode.component.type != types.ruleNodeType.INPUT.value) { - contextInfo.items.push( - { - action: function () { - openLinkDetails(edge); - }, - enabled: true, - value: "rulenode.details", - icon: "menu" - } - ); - } - contextInfo.items.push( - { - action: function () { - vm.canvasControl.modelservice.edges.delete(edge); - }, - enabled: true, - value: "action.delete", - icon: "clear", - shortcut: "M-X" - } - ); - return contextInfo; - } - - function initHotKeys() { - hotkeys.bindTo($scope) - .add({ - combo: 'ctrl+a', - description: $translate.instant('rulenode.select-all-objects'), - allowIn: ['INPUT', 'SELECT', 'TEXTAREA'], - callback: function (event) { - if (vm.enableHotKeys) { - event.preventDefault(); - vm.modelservice.selectAll(); - } - } - }) - .add({ - combo: 'ctrl+c', - description: $translate.instant('rulenode.copy-selected'), - allowIn: ['INPUT', 'SELECT', 'TEXTAREA'], - callback: function (event) { - if (vm.enableHotKeys) { - event.preventDefault(); - copyRuleNodes(); - } - } - }) - .add({ - combo: 'ctrl+v', - description: $translate.instant('action.paste'), - allowIn: ['INPUT', 'SELECT', 'TEXTAREA'], - callback: function (event) { - if (vm.enableHotKeys) { - event.preventDefault(); - if (itembuffer.hasRuleNodes()) { - pasteRuleNodes(); - } - } - } - }) - .add({ - combo: 'esc', - description: $translate.instant('rulenode.deselect-all-objects'), - allowIn: ['INPUT', 'SELECT', 'TEXTAREA'], - callback: function (event) { - if (vm.enableHotKeys) { - event.preventDefault(); - event.stopPropagation(); - vm.modelservice.deselectAll(); - } - } - }) - .add({ - combo: 'ctrl+s', - description: $translate.instant('action.apply'), - allowIn: ['INPUT', 'SELECT', 'TEXTAREA'], - callback: function (event) { - if (vm.enableHotKeys) { - event.preventDefault(); - vm.saveRuleChain(); - } - } - }) - .add({ - combo: 'ctrl+z', - description: $translate.instant('action.decline-changes'), - allowIn: ['INPUT', 'SELECT', 'TEXTAREA'], - callback: function (event) { - if (vm.enableHotKeys) { - event.preventDefault(); - vm.revertRuleChain(); - } - } - }) - .add({ - combo: 'del', - description: $translate.instant('rulenode.delete-selected-objects'), - allowIn: ['INPUT', 'SELECT', 'TEXTAREA'], - callback: function (event) { - if (vm.enableHotKeys) { - event.preventDefault(); - vm.modelservice.deleteSelected(); - } - } - }) - } - - vm.onEditRuleNodeClosed = function() { - vm.editingRuleNode = null; - }; - - vm.onEditRuleNodeLinkClosed = function() { - vm.editingRuleNodeLink = null; - }; - - vm.saveRuleNode = function(theForm) { - $scope.$broadcast('form-submit'); - if (theForm.$valid) { - theForm.$setPristine(); - if (vm.editingRuleNode.error) { - delete vm.editingRuleNode.error; - } - vm.ruleChainModel.nodes[vm.editingRuleNodeIndex] = vm.editingRuleNode; - vm.editingRuleNode = angular.copy(vm.editingRuleNode); - updateRuleNodesHighlight(); - } - }; - - vm.saveRuleNodeLink = function(theForm) { - theForm.$setPristine(); - vm.ruleChainModel.edges[vm.editingRuleNodeLinkIndex] = vm.editingRuleNodeLink; - vm.editingRuleNodeLink = angular.copy(vm.editingRuleNodeLink); - }; - - vm.onRevertRuleNodeEdit = function(theForm) { - theForm.$setPristine(); - var node = vm.ruleChainModel.nodes[vm.editingRuleNodeIndex]; - vm.editingRuleNode = angular.copy(node); - }; - - vm.onRevertRuleNodeLinkEdit = function(theForm) { - theForm.$setPristine(); - var edge = vm.ruleChainModel.edges[vm.editingRuleNodeLinkIndex]; - vm.editingRuleNodeLink = angular.copy(edge); - }; - - vm.nodeLibCallbacks = { - nodeCallbacks: { - 'mouseEnter': function (event, node) { - displayLibNodeDescriptionTooltip(event, node); - }, - 'mouseLeave': function () { - destroyTooltips(); - }, - 'mouseDown': function () { - destroyTooltips(); - } - } - }; - - vm.typeHeaderMouseEnter = function(event, typeId) { - var ruleNodeType = types.ruleNodeType[typeId]; - displayTooltip(event, - '
    ' + - '
    ' + - '
    ' + $translate.instant(ruleNodeType.name) + '
    ' + - '
    ' + $translate.instant(ruleNodeType.details) + '
    ' + - '
    ' + - '
    ' - ); - }; - - vm.destroyTooltips = destroyTooltips; - - function helpLinkIdForRuleNodeType() { - return helpLinks.getRuleNodeLink(vm.editingRuleNode); - } - - function destroyTooltips() { - if (vm.tooltipTimeout) { - $timeout.cancel(vm.tooltipTimeout); - vm.tooltipTimeout = null; - } - var instances = angular.element.tooltipster.instances(); - instances.forEach((instance) => { - if (!instance.isErrorTooltip) { - instance.destroy(); - } - }); - } - - function displayLibNodeDescriptionTooltip(event, node) { - displayTooltip(event, - '
    ' + - '
    ' + - '
    ' + node.component.name + '
    ' + - '
    ' + node.component.configurationDescriptor.nodeDefinition.description + '
    ' + - '
    ' + node.component.configurationDescriptor.nodeDefinition.details + '
    ' + - '
    ' + - '
    ' - ); - } - - function displayNodeDescriptionTooltip(event, node) { - if (!vm.errorTooltips[node.id]) { - var name, desc, details; - if (node.component.type == vm.types.ruleNodeType.INPUT.value) { - name = $translate.instant(vm.types.ruleNodeType.INPUT.name) + ''; - desc = $translate.instant(vm.types.ruleNodeType.INPUT.details) + ''; - } else { - name = node.name; - desc = $translate.instant(vm.types.ruleNodeType[node.component.type].name) + ' - ' + node.component.name; - if (node.additionalInfo) { - details = node.additionalInfo.description; - } - } - var tooltipContent = '
    ' + - '
    ' + - '
    ' + name + '
    ' + - '
    ' + desc + '
    '; - if (details) { - tooltipContent += '
    ' + details + '
    '; - } - tooltipContent += '
    ' + - '
    '; - displayTooltip(event, tooltipContent); - } - } - - function displayTooltip(event, content) { - destroyTooltips(); - vm.tooltipTimeout = $timeout(() => { - var element = angular.element(event.target); - element.tooltipster( - { - theme: 'tooltipster-shadow', - delay: 100, - trigger: 'custom', - triggerOpen: { - click: false, - tap: false - }, - triggerClose: { - click: true, - tap: true, - scroll: true - }, - side: 'right', - trackOrigin: true - } - ); - var contentElement = angular.element(content); - $compile(contentElement)($scope); - var tooltip = element.tooltipster('instance'); - tooltip.content(contentElement); - tooltip.open(); - }, 500); - } - - function updateNodeErrorTooltip(node) { - if (node.error) { - var element = angular.element('#' + node.id); - var tooltip = vm.errorTooltips[node.id]; - if (!tooltip || !element.hasClass("tooltipstered")) { - element.tooltipster( - { - theme: 'tooltipster-shadow', - delay: 0, - animationDuration: 0, - trigger: 'custom', - triggerOpen: { - click: false, - tap: false - }, - triggerClose: { - click: false, - tap: false, - scroll: false - }, - side: 'top', - trackOrigin: true - } - ); - var content = '
    ' + - '
    ' + - '
    ' + node.error + '
    ' + - '
    ' + - '
    '; - var contentElement = angular.element(content); - $compile(contentElement)($scope); - tooltip = element.tooltipster('instance'); - tooltip.isErrorTooltip = true; - tooltip.content(contentElement); - vm.errorTooltips[node.id] = tooltip; - } - $mdUtil.nextTick(() => { - tooltip.open(); - }); - } else { - if (vm.errorTooltips[node.id]) { - tooltip = vm.errorTooltips[node.id]; - tooltip.destroy(); - delete vm.errorTooltips[node.id]; - } - } - } - - function updateErrorTooltips(hide) { - for (var nodeId in vm.errorTooltips) { - var tooltip = vm.errorTooltips[nodeId]; - if (hide) { - tooltip.close(); - } else { - tooltip.open(); - } - } - } - - $scope.$watch(function() { - return vm.isEditingRuleNode || vm.isEditingRuleNodeLink; - }, (val) => { - vm.enableHotKeys = !val; - updateErrorTooltips(val); - }); - - vm.editCallbacks = { - edgeDoubleClick: function (event, edge) { - openLinkDetails(edge); - }, - edgeEdit: function(event, edge) { - openLinkDetails(edge); - }, - nodeCallbacks: { - 'doubleClick': function (event, node) { - openNodeDetails(node); - }, - 'nodeEdit': function (event, node) { - openNodeDetails(node); - }, - 'mouseEnter': function (event, node) { - displayNodeDescriptionTooltip(event, node); - }, - 'mouseLeave': function () { - destroyTooltips(); - }, - 'mouseDown': function () { - destroyTooltips(); - } - }, - isValidEdge: function (source, destination) { - return source.type === flowchartConstants.rightConnectorType && destination.type === flowchartConstants.leftConnectorType; - }, - createEdge: function (event, edge) { - var deferred = $q.defer(); - var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source); - if (sourceNode.component.type == types.ruleNodeType.INPUT.value) { - var destNode = vm.modelservice.nodes.getNodeByConnectorId(edge.destination); - if (destNode.component.type == types.ruleNodeType.RULE_CHAIN.value) { - deferred.reject(); - } else { - var res = $filter('filter')(vm.ruleChainModel.edges, {source: vm.inputConnectorId}, true); - if (res && res.length) { - vm.modelservice.edges.delete(res[0]); - } - deferred.resolve(edge); - } - } else { - if (edge.label) { - if (!edge.labels) { - edge.labels = edge.label.split(' / '); - } - deferred.resolve(edge); - } else { - var labels = ruleChainService.getRuleNodeSupportedLinks(sourceNode.component); - var allowCustomLabels = ruleChainService.ruleNodeAllowCustomLinks(sourceNode.component); - vm.enableHotKeys = false; - addRuleNodeLink(event, edge, labels, allowCustomLabels).then( - (link) => { - deferred.resolve(link); - vm.enableHotKeys = true; - }, - () => { - deferred.reject(); - vm.enableHotKeys = true; - } - ); - } - } - return deferred.promise; - }, - dropNode: function (event, node) { - addRuleNode(event, node); - } - }; - - function openNodeDetails(node) { - if (node.component.type != types.ruleNodeType.INPUT.value) { - vm.isEditingRuleNodeLink = false; - vm.editingRuleNodeLink = null; - vm.isEditingRuleNode = true; - vm.editingRuleNodeIndex = vm.ruleChainModel.nodes.indexOf(node); - vm.editingRuleNode = angular.copy(node); - $mdUtil.nextTick(() => { - if (vm.ruleNodeForm) { - vm.ruleNodeForm.$setPristine(); - } - }); - } - } - - function openLinkDetails(edge) { - var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source); - if (sourceNode.component.type != types.ruleNodeType.INPUT.value) { - vm.isEditingRuleNode = false; - vm.editingRuleNode = null; - vm.editingRuleNodeLinkLabels = ruleChainService.getRuleNodeSupportedLinks(sourceNode.component); - vm.editingRuleNodeAllowCustomLabels = ruleChainService.ruleNodeAllowCustomLinks(sourceNode.component); - vm.isEditingRuleNodeLink = true; - vm.editingRuleNodeLinkIndex = vm.ruleChainModel.edges.indexOf(edge); - vm.editingRuleNodeLink = angular.copy(edge); - $mdUtil.nextTick(() => { - if (vm.ruleNodeLinkForm) { - vm.ruleNodeLinkForm.$setPristine(); - } - }); - } - } - - function copyNode(node) { - itembuffer.copyRuleNodes([node], []); - } - - function copyRuleNodes() { - var nodes = vm.modelservice.nodes.getSelectedNodes(); - var edges = vm.modelservice.edges.getSelectedEdges(); - var connections = []; - for (var i=0;i -1) && toIndex > -1 ) { - var connection = { - isInputSource: isInputSource, - fromIndex: fromIndex, - toIndex: toIndex, - label: edge.label, - labels: edge.labels - }; - connections.push(connection); - } - } - itembuffer.copyRuleNodes(nodes, connections); - } - - function pasteRuleNodes(event) { - var canvas = angular.element(vm.canvasControl.modelservice.getCanvasHtmlElement()); - var x,y; - if (event) { - var offset = canvas.offset(); - x = Math.round(event.clientX - offset.left); - y = Math.round(event.clientY - offset.top); - } else { - var scrollParent = canvas.parent(); - var scrollTop = scrollParent.scrollTop(); - var scrollLeft = scrollParent.scrollLeft(); - x = scrollLeft + scrollParent.width()/2; - y = scrollTop + scrollParent.height()/2; - } - var ruleNodes = itembuffer.pasteRuleNodes(x, y, event); - if (ruleNodes) { - vm.modelservice.deselectAll(); - var nodes = []; - for (var i=0;i { - for (componentType in vm.ruleNodeTypesCanvasControl) { - if (vm.ruleNodeTypesCanvasControl[componentType].adjustCanvasSize) { - vm.ruleNodeTypesCanvasControl[componentType].adjustCanvasSize(true); - } - } - for (componentType in vm.ruleNodeTypesModel) { - var panel = vm.$mdExpansionPanel(componentType); - if (panel) { - if (!vm.ruleNodeTypesModel[componentType].model.nodes.length) { - panel.collapse(); - } else { - panel.expand(); - } - } - } - }); - } - - function prepareRuleChain() { - - if (vm.ruleChainWatch) { - vm.ruleChainWatch(); - vm.ruleChainWatch = null; - } - - vm.nextNodeID = 1; - vm.nextConnectorID = 1; - - vm.selectedObjects.length = 0; - vm.ruleChainModel.nodes.length = 0; - vm.ruleChainModel.edges.length = 0; - - vm.inputConnectorId = vm.nextConnectorID++; - - vm.ruleChainModel.nodes.push( - { - id: 'rule-chain-node-' + vm.nextNodeID++, - component: types.inputNodeComponent, - name: "", - nodeClass: types.ruleNodeType.INPUT.nodeClass, - icon: types.ruleNodeType.INPUT.icon, - readonly: true, - x: 50, - y: 150, - connectors: [ - { - type: flowchartConstants.rightConnectorType, - id: vm.inputConnectorId - }, - ] - - } - ); - ruleChainService.resolveTargetRuleChains(vm.ruleChainMetaData.ruleChainConnections) - .then((ruleChainsMap) => { - createRuleChainModel(ruleChainsMap); - } - ); - } - - function createRuleChainModel(ruleChainsMap) { - var nodes = []; - for (var i=0;i -1) { - var destNode = nodes[vm.ruleChainMetaData.firstNodeIndex]; - if (destNode) { - var connectors = vm.modelservice.nodes.getConnectorsByType(destNode, flowchartConstants.leftConnectorType); - if (connectors && connectors.length) { - var edge = { - source: vm.inputConnectorId, - destination: connectors[0].id - }; - vm.ruleChainModel.edges.push(edge); - } - } - } - - if (vm.ruleChainMetaData.connections) { - var edgeMap = {}; - for (i = 0; i < vm.ruleChainMetaData.connections.length; i++) { - var connection = vm.ruleChainMetaData.connections[i]; - var sourceNode = nodes[connection.fromIndex]; - destNode = nodes[connection.toIndex]; - if (sourceNode && destNode) { - var sourceConnectors = vm.modelservice.nodes.getConnectorsByType(sourceNode, flowchartConstants.rightConnectorType); - var destConnectors = vm.modelservice.nodes.getConnectorsByType(destNode, flowchartConstants.leftConnectorType); - if (sourceConnectors && sourceConnectors.length && destConnectors && destConnectors.length) { - var sourceId = sourceConnectors[0].id; - var destId = destConnectors[0].id; - var edgeKey = sourceId + '_' + destId; - edge = edgeMap[edgeKey]; - if (!edge) { - edge = { - source: sourceId, - destination: destId, - label: connection.type, - labels: [connection.type] - }; - edgeMap[edgeKey] = edge; - vm.ruleChainModel.edges.push(edge); - } else { - edge.label += ' / ' +connection.type; - edge.labels.push(connection.type); - } - } - } - } - } - - if (vm.ruleChainMetaData.ruleChainConnections) { - var ruleChainNodesMap = {}; - var ruleChainEdgeMap = {}; - for (i = 0; i < vm.ruleChainMetaData.ruleChainConnections.length; i++) { - var ruleChainConnection = vm.ruleChainMetaData.ruleChainConnections[i]; - var ruleChain = ruleChainsMap[ruleChainConnection.targetRuleChainId.id]; - if (ruleChainConnection.additionalInfo && ruleChainConnection.additionalInfo.ruleChainNodeId) { - var ruleChainNode = ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId]; - if (!ruleChainNode) { - ruleChainNode = { - id: 'rule-chain-node-' + vm.nextNodeID++, - additionalInfo: ruleChainConnection.additionalInfo, - x: Math.round(ruleChainConnection.additionalInfo.layoutX), - y: Math.round(ruleChainConnection.additionalInfo.layoutY), - component: types.ruleChainNodeComponent, - nodeClass: vm.types.ruleNodeType.RULE_CHAIN.nodeClass, - icon: vm.types.ruleNodeType.RULE_CHAIN.icon, - connectors: [ - { - type: flowchartConstants.leftConnectorType, - id: vm.nextConnectorID++ - } - ] - }; - if (ruleChain.name) { - ruleChainNode.name = ruleChain.name; - ruleChainNode.targetRuleChainId = ruleChainConnection.targetRuleChainId.id; - } else { - ruleChainNode.name = "Unresolved"; - ruleChainNode.targetRuleChainId = null; - ruleChainNode.error = $translate.instant('rulenode.invalid-target-rulechain'); - } - ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId] = ruleChainNode; - vm.ruleChainModel.nodes.push(ruleChainNode); - } - sourceNode = nodes[ruleChainConnection.fromIndex]; - if (sourceNode) { - connectors = vm.modelservice.nodes.getConnectorsByType(sourceNode, flowchartConstants.rightConnectorType); - if (connectors && connectors.length) { - sourceId = connectors[0].id; - destId = ruleChainNode.connectors[0].id; - edgeKey = sourceId + '_' + destId; - var ruleChainEdge = ruleChainEdgeMap[edgeKey]; - if (!ruleChainEdge) { - ruleChainEdge = { - source: sourceId, - destination: destId, - label: ruleChainConnection.type, - labels: [ruleChainConnection.type] - }; - ruleChainEdgeMap[edgeKey] = ruleChainEdge; - vm.ruleChainModel.edges.push(ruleChainEdge); - } else { - ruleChainEdge.label += ' / ' +ruleChainConnection.type; - ruleChainEdge.labels.push(ruleChainConnection.type); - } - } - } - } - } - } - - if (vm.canvasControl.adjustCanvasSize) { - vm.canvasControl.adjustCanvasSize(true); - } - - vm.isDirty = false; - - updateRuleNodesHighlight(); - - validate(); - - $mdUtil.nextTick(() => { - vm.ruleChainWatch = $scope.$watch('vm.ruleChainModel', - function (newVal, oldVal) { - if (!angular.equals(newVal, oldVal)) { - validate(); - if (!vm.isDirty) { - vm.isDirty = true; - } - } - }, true - ); - }); - } - - function updateRuleNodesHighlight() { - for (var i = 0; i < vm.ruleChainModel.nodes.length; i++) { - vm.ruleChainModel.nodes[i].highlighted = false; - } - if ($scope.searchConfig.searchText) { - var res = $filter('filter')(vm.ruleChainModel.nodes, {name: $scope.searchConfig.searchText}); - if (res) { - for (i = 0; i < res.length; i++) { - res[i].highlighted = true; - } - } - } - } - - function validate() { - $mdUtil.nextTick(() => { - vm.isInvalid = false; - for (var i = 0; i < vm.ruleChainModel.nodes.length; i++) { - if (vm.ruleChainModel.nodes[i].error) { - vm.isInvalid = true; - } - updateNodeErrorTooltip(vm.ruleChainModel.nodes[i]); - } - }); - } - - function saveRuleChain() { - var saveRuleChainPromise; - if (vm.isImport) { - saveRuleChainPromise = ruleChainService.saveRuleChain(vm.ruleChain); - } else { - saveRuleChainPromise = $q.when(vm.ruleChain); - } - saveRuleChainPromise.then( - (ruleChain) => { - vm.ruleChain = ruleChain; - var ruleChainMetaData = { - ruleChainId: vm.ruleChain.id, - nodes: [], - connections: [], - ruleChainConnections: [] - }; - - var nodes = []; - - for (var i=0;i { - vm.ruleChainMetaData = ruleChainMetaData; - if (vm.isImport) { - vm.isDirty = false; - vm.isImport = false; - $mdUtil.nextTick(() => { - $state.go('home.ruleChains.ruleChain', {ruleChainId: vm.ruleChain.id.id}); - }); - } else { - prepareRuleChain(); - } - } - ); - } - ); - } - - function revertRuleChain() { - prepareRuleChain(); - } - - function addRuleNode($event, ruleNode) { - - ruleNode.configuration = angular.copy(ruleNode.component.configurationDescriptor.nodeDefinition.defaultConfiguration); - - var ruleChainId = vm.ruleChain.id ? vm.ruleChain.id.id : null; - - vm.enableHotKeys = false; - - $mdDialog.show({ - controller: 'AddRuleNodeController', - controllerAs: 'vm', - templateUrl: addRuleNodeTemplate, - parent: angular.element($document[0].body), - locals: {ruleNode: ruleNode, ruleChainId: ruleChainId}, - fullscreen: true, - targetEvent: $event - }).then(function (ruleNode) { - ruleNode.id = 'rule-chain-node-' + vm.nextNodeID++; - ruleNode.connectors = []; - if (ruleNode.component.configurationDescriptor.nodeDefinition.inEnabled) { - ruleNode.connectors.push( - { - id: vm.nextConnectorID++, - type: flowchartConstants.leftConnectorType - } - ); - } - if (ruleNode.component.configurationDescriptor.nodeDefinition.outEnabled) { - ruleNode.connectors.push( - { - id: vm.nextConnectorID++, - type: flowchartConstants.rightConnectorType - } - ); - } - vm.ruleChainModel.nodes.push(ruleNode); - updateRuleNodesHighlight(); - vm.enableHotKeys = true; - }, function () { - vm.enableHotKeys = true; - }); - } - - function addRuleNodeLink($event, link, labels, allowCustomLabels) { - return $mdDialog.show({ - controller: 'AddRuleNodeLinkController', - controllerAs: 'vm', - templateUrl: addRuleNodeLinkTemplate, - parent: angular.element($document[0].body), - locals: {link: link, labels: labels, allowCustomLabels: allowCustomLabels}, - fullscreen: true, - targetEvent: $event - }); - } - - function objectsSelected() { - return vm.modelservice.nodes.getSelectedNodes().length > 0 || - vm.modelservice.edges.getSelectedEdges().length > 0 - } - - function deleteSelected() { - vm.modelservice.deleteSelected(); - } - - function isDebugModeEnabled() { - var res = $filter('filter')(vm.ruleChainModel.nodes, {debugMode: true}); - return (res && res.length); - } - - function resetDebugModeInAllNodes() { - vm.ruleChainModel.nodes.forEach((node) => { - if (node.component.type != types.ruleNodeType.INPUT.value && node.component.type != types.ruleNodeType.RULE_CHAIN.value) { - node.debugMode = false; - } - }); - } - - function triggerResize() { - var w = angular.element($window); - w.triggerHandler('resize'); - } -} - -/*@ngInject*/ -export function AddRuleNodeController($scope, $mdDialog, ruleNode, ruleChainId, helpLinks) { - - var vm = this; - - vm.helpLinks = helpLinks; - vm.ruleNode = ruleNode; - vm.ruleChainId = ruleChainId; - - vm.add = add; - vm.cancel = cancel; - - function cancel() { - $mdDialog.cancel(); - } - - function add() { - $scope.theForm.$setPristine(); - $mdDialog.hide(vm.ruleNode); - } -} - -/*@ngInject*/ -export function AddRuleNodeLinkController($scope, $mdDialog, link, labels, allowCustomLabels, helpLinks) { - - var vm = this; - - vm.helpLinks = helpLinks; - vm.link = link; - vm.labels = labels; - vm.allowCustomLabels = allowCustomLabels; - - vm.add = add; - vm.cancel = cancel; - - function cancel() { - $mdDialog.cancel(); - } - - function add() { - $scope.theForm.$setPristine(); - $mdDialog.hide(vm.link); - } -} diff --git a/ui/src/app/rulechain/rulechain.directive.js b/ui/src/app/rulechain/rulechain.directive.js deleted file mode 100644 index a4951c2f8b..0000000000 --- a/ui/src/app/rulechain/rulechain.directive.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import ruleChainFieldsetTemplate from './rulechain-fieldset.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function RuleChainDirective($compile, $templateCache, $mdDialog, $document, $q, $translate, types, toast) { - var linker = function (scope, element) { - var template = $templateCache.get(ruleChainFieldsetTemplate); - element.html(template); - - scope.onRuleChainIdCopied = function() { - toast.showSuccess($translate.instant('rulechain.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left'); - }; - - $compile(element.contents())(scope); - } - return { - restrict: "E", - link: linker, - scope: { - ruleChain: '=', - isEdit: '=', - isReadOnly: '=', - theForm: '=', - onSetRootRuleChain: '&', - onExportRuleChain: '&', - onDeleteRuleChain: '&' - } - }; -} diff --git a/ui/src/app/rulechain/rulechain.routes.js b/ui/src/app/rulechain/rulechain.routes.js deleted file mode 100644 index 7bad80ebaa..0000000000 --- a/ui/src/app/rulechain/rulechain.routes.js +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import ruleNodeTemplate from './rulenode.tpl.html'; -import ruleChainsTemplate from './rulechains.tpl.html'; -import ruleChainTemplate from './rulechain.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function RuleChainRoutes($stateProvider, NodeTemplatePathProvider) { - - NodeTemplatePathProvider.setTemplatePath(ruleNodeTemplate); - - $stateProvider - .state('home.ruleChains', { - url: '/ruleChains', - params: {'topIndex': 0}, - module: 'private', - auth: ['SYS_ADMIN', 'TENANT_ADMIN'], - views: { - "content@home": { - templateUrl: ruleChainsTemplate, - controllerAs: 'vm', - controller: 'RuleChainsController' - } - }, - data: { - searchEnabled: true, - pageTitle: 'rulechain.rulechains' - }, - ncyBreadcrumb: { - label: '{"icon": "settings_ethernet", "label": "rulechain.rulechains"}' - } - }).state('home.ruleChains.ruleChain', { - url: '/:ruleChainId', - reloadOnSearch: false, - module: 'private', - auth: ['SYS_ADMIN', 'TENANT_ADMIN'], - views: { - "content@home": { - templateUrl: ruleChainTemplate, - controller: 'RuleChainController', - controllerAs: 'vm' - } - }, - resolve: { - ruleChain: - /*@ngInject*/ - function($stateParams, ruleChainService) { - return ruleChainService.getRuleChain($stateParams.ruleChainId); - }, - ruleChainMetaData: - /*@ngInject*/ - function($stateParams, ruleChainService) { - return ruleChainService.getRuleChainMetaData($stateParams.ruleChainId); - }, - ruleNodeComponents: - /*@ngInject*/ - function($stateParams, ruleChainService) { - return ruleChainService.getRuleNodeComponents(); - } - }, - data: { - import: false, - searchEnabled: true, - pageTitle: 'rulechain.rulechain' - }, - ncyBreadcrumb: { - label: '{"icon": "settings_ethernet", "label": "{{ vm.ruleChain.name + (vm.ruleChain.root ? (\' (\' + (\'rulechain.root\' | translate) + \')\') : \'\') }}", "translate": "false"}' - } - }).state('home.ruleChains.importRuleChain', { - url: '/ruleChain/import', - reloadOnSearch: false, - module: 'private', - auth: ['SYS_ADMIN', 'TENANT_ADMIN'], - views: { - "content@home": { - templateUrl: ruleChainTemplate, - controller: 'RuleChainController', - controllerAs: 'vm' - } - }, - params: { - ruleChainImport: {} - }, - resolve: { - ruleChain: - /*@ngInject*/ - function($stateParams) { - return $stateParams.ruleChainImport.ruleChain; - }, - ruleChainMetaData: - /*@ngInject*/ - function($stateParams) { - return $stateParams.ruleChainImport.metadata; - }, - ruleNodeComponents: - /*@ngInject*/ - function($stateParams, ruleChainService) { - return ruleChainService.getRuleNodeComponents(); - } - }, - data: { - import: true, - searchEnabled: true, - pageTitle: 'rulechain.rulechain' - }, - ncyBreadcrumb: { - label: '{"icon": "settings_ethernet", "label": "{{ (\'rulechain.import\' | translate) + \': \'+ vm.ruleChain.name }}", "translate": "false"}' - } - }); -} diff --git a/ui/src/app/rulechain/rulechain.scss b/ui/src/app/rulechain/rulechain.scss deleted file mode 100644 index 21c417cbb5..0000000000 --- a/ui/src/app/rulechain/rulechain.scss +++ /dev/null @@ -1,561 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-rulechain { - .tb-fullscreen-button-style { - z-index: 1; - } - - section.tb-header-buttons.tb-library-open { - position: absolute; - top: 0; - left: 0; - z-index: 1; - pointer-events: none; - - .md-button.tb-btn-open-library { - top: 0; - left: 0; - width: 36px; - height: 36px; - margin: 4px 0 0 4px; - line-height: 36px; - opacity: .5; - } - } - - .tb-rulechain-library { - z-index: 1; - width: 250px; - min-width: 250px; - - md-toolbar { - height: 48px; - min-height: 48px; - - .md-toolbar-tools > .md-button:last-child { - margin-right: 0; - } - - .md-toolbar-tools { - height: 48px; - padding: 0 6px; - font-size: 14px; - - .md-button.md-icon-button { - margin: 0; - - &.tb-small { - width: 32px; - height: 32px; - min-height: 32px; - padding: 6px; - line-height: 20px; - - md-icon { - width: 20px; - min-width: 20px; - height: 20px; - min-height: 20px; - font-size: 20px; - line-height: 20px; - } - } - } - } - } - - .tb-rulechain-library-panel-group { - overflow-x: hidden; - overflow-y: auto; - - .tb-panel-title { - min-width: 150px; - user-select: none; - } - - .fc-canvas { - background: #f9f9f9; - } - - md-icon.md-expansion-panel-icon { - margin-right: 0; - } - - md-expansion-panel-collapsed, - .md-expansion-panel-header-container { - position: static; - background: #e6e6e6; - border-color: #909090; - } - - md-expansion-panel { - &.md-open { - margin-top: 0; - margin-bottom: 0; - } - } - - md-expansion-panel-content { - padding: 0; - } - } - } - - .tb-rulechain-graph { - z-index: 0; - overflow: auto; - } -} - -#tb-rule-chain-context-menu { - max-height: 404px; - padding-top: 0; - border-radius: 8px; - - .tb-context-menu-header { - display: flex; - flex-direction: row; - height: 36px; - min-height: 36px; - padding: 8px 5px 5px; - font-size: 14px; - - &.tb-rulechain { - background-color: #aac7e4; - } - - &.tb-link { - background-color: #aac7e4; - } - - md-icon { - padding-right: 10px; - padding-left: 2px; - } - - .tb-context-menu-title { - font-weight: 500; - } - - .tb-context-menu-subtitle { - font-size: 12px; - } - } -} - -.fc-canvas { - min-width: 100%; - min-height: 100%; - user-select: none; - outline: none; - -webkit-touch-callout: none; - - svg { - display: block; - } -} - -.tb-rule-node, -#tb-rule-chain-context-menu .tb-context-menu-header { - &.tb-filter-type { - background-color: #f1e861; - } - - &.tb-enrichment-type { - background-color: #cdf14e; - } - - &.tb-transformation-type { - background-color: #79cef1; - } - - &.tb-action-type { - background-color: #f1928f; - } - - &.tb-external-type { - background-color: #fbc766; - } - - &.tb-rule-chain-type { - background-color: #d6c4f1; - } - - &.tb-unknown-type { - background-color: #f16c29; - } -} - -.tb-rule-node { - display: flex; - flex-direction: row; - min-width: 150px; - max-width: 150px; - height: 32px; - min-height: 32px; - max-height: 32px; - padding: 5px 10px; - font-size: 12px; - line-height: 16px; - color: #333; - pointer-events: none; - background-color: #f15b26; - border: solid 1px #777; - border-radius: 5px; - - &.tb-rule-node-highlighted:not(.tb-rule-node-invalid) { - box-shadow: 0 0 10px 6px #51cbee; - - .tb-node-title { - font-weight: 700; - text-decoration: underline; - } - } - - &.tb-rule-node-invalid { - box-shadow: 0 0 10px 6px #ff5c50; - } - - &.tb-input-type { - user-select: none; - background-color: #a3eaa9; - } - - md-icon { - width: 20px; - min-width: 20px; - height: 20px; - min-height: 20px; - padding-right: 4px; - font-size: 20px; - } - - .tb-node-title { - font-weight: 500; - } - - .tb-node-type, - .tb-node-title { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } -} - -.fc-node { - z-index: 1; - border-radius: 8px; - outline: none; - - &.fc-dragging { - z-index: 10; - } - - p { - padding: 0 15px; - text-align: center; - } - - .fc-node-overlay { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - pointer-events: none; - background-color: #000; - border-radius: 5px; - opacity: 0; - } - - &.fc-hover { - .fc-node-overlay { - opacity: .25; - } - } - - &.fc-selected { - .fc-node-overlay { - opacity: .25; - } - - &:not(.fc-edit) { - margin: -3px; - border: solid 3px #f00; - } - } -} - -.fc-leftConnectors, -.fc-rightConnectors { - position: absolute; - top: 0; - - z-index: 0; - - display: flex; - flex-direction: column; - height: 100%; - - .fc-magnet { - align-items: center; - } -} - -.fc-leftConnectors { - left: -20px; -} - -.fc-rightConnectors { - right: -20px; -} - -.fc-magnet { - display: flex; - flex-grow: 1; - justify-content: center; - height: 60px; -} - -.fc-connector { - width: 14px; - height: 14px; - margin: 10px; - pointer-events: all; - background-color: #ccc; - border: 1px solid #333; - border-radius: 5px; -} - -.fc-connector.fc-hover { - background-color: #000; -} - -.fc-arrow-marker { - polygon { - fill: #808080; - stroke: #808080; - } -} - -.fc-arrow-marker-selected { - polygon { - fill: #f00; - stroke: #f00; - } -} - -.fc-edge { - outline: none; - transition: stroke-width .2s; - fill: transparent; - stroke: #808080; - stroke-width: 4; - - &.fc-selected { - fill: transparent; - stroke: #f00; - stroke-width: 4; - } - - &.fc-active { - animation: dash 3s linear infinite; - stroke-dasharray: 20; - } - - &.fc-hover { - fill: transparent; - stroke: #808080; - stroke-width: 6; - } - - &.fc-dragging { - pointer-events: none; - } -} - -.edge-endpoint { - fill: #808080; -} - -.fc-nodedelete { - display: none; - font-size: 18px; -} - -.fc-nodeedit { - display: none; - font-size: 15px; -} - -.fc-edit { - .fc-nodedelete, - .fc-nodeedit { - position: absolute; - display: block; - width: 22px; - height: 20px; - padding-top: 2px; - font-weight: 600; - line-height: 20px; - color: #fff; - text-align: center; - vertical-align: bottom; - cursor: pointer; - background: #f83e05; - border: solid 2px #fff; - border-radius: 50%; - outline: none; - } - - .fc-nodeedit { - top: -24px; - right: 16px; - } - - .fc-nodedelete { - top: -24px; - right: -13px; - } -} - -.fc-noselect { - user-select: none; -} - -.fc-edge-label { - position: absolute; - transition: transform .2s; - // opacity: 0.8; - - &.ng-leave { - transition: 0s none; - } - - &.fc-hover { - transform: scale(1.25); - } - - &.fc-selected { - .fc-edge-label-text { - span { - font-weight: 600; - color: #fff; - background-color: #f00; - border: solid #f00; - } - } - } - - .fc-nodeedit { - top: -30px; - right: 14px; - } - - .fc-nodedelete { - top: -30px; - right: -13px; - } - - &:focus { - outline: 0; - } -} - -.fc-edge-label-text { - position: absolute; - font-size: 14px; - font-weight: 600; - text-align: center; - white-space: nowrap; - transform: translate(-50%, -50%); - - span { - padding: 3px 5px; - color: #003a79; - cursor: default; - background-color: #fff; - border: solid 2px #003a79; - border-radius: 10px; - } -} - -.fc-select-rectangle { - position: absolute; - z-index: 2; - background: rgba(20, 125, 255, .1); - border: 2px dashed #5262ff; -} - -@keyframes dash { - from { - stroke-dashoffset: 500; - } -} - -.tb-rule-node-tooltip, -.tb-rule-node-help { - color: #333; -} - -.tb-rule-node-tooltip { - max-width: 300px; - font-size: 14px; - - &.tb-lib-tooltip { - width: 300px; - } -} - -.tb-rule-node-help { - font-size: 16px; -} - -.tb-rule-node-error-tooltip { - font-size: 16px; - color: #ea0d0d; -} - -.tb-rule-node-tooltip, -.tb-rule-node-error-tooltip, -.tb-rule-node-help { - #tb-node-content { - .tb-node-title { - font-weight: 600; - } - - .tb-node-description { - font-style: italic; - color: #555; - } - - .tb-node-details { - padding-top: 10px; - padding-bottom: 10px; - } - - code { - padding: 0 3px 2px 3px; - margin: 1px; - font-size: 12px; - color: #ad1625; - white-space: nowrap; - background-color: #f7f7f9; - border: 1px solid #e1e1e8; - border-radius: 2px; - } - } -} diff --git a/ui/src/app/rulechain/rulechain.tpl.html b/ui/src/app/rulechain/rulechain.tpl.html deleted file mode 100644 index 52a7197e93..0000000000 --- a/ui/src/app/rulechain/rulechain.tpl.html +++ /dev/null @@ -1,254 +0,0 @@ - - -
    -
    -
    - - - {{ 'rulenode.open-node-library' | translate }} - - - -
    - - -
    - - search - - {{'rulenode.search' | translate}} - - -
    - - - - -
    - - close - - {{ 'action.clear-search' | translate }} - - - - chevron_left - - {{ 'action.close' | translate }} - - -
    -
    - - - - {{vm.types.ruleNodeType[typeId].icon}} -
    {{vm.types.ruleNodeType[typeId].name}}
    - -
    - - - {{vm.types.ruleNodeType[typeId].icon}} -
    {{vm.types.ruleNodeType[typeId].name}}
    - -
    - - - -
    -
    -
    -
    - -
    - - -
    - -
    - {{vm.contextInfo.icon}} - -
    -
    {{vm.contextInfo.title}}
    -
    {{vm.contextInfo.subtitle}}
    -
    -
    -
    - - - - {{ item.shortcut | keyboardShortcut }} - {{item.icon}} - {{item.value}} - - -
    -
    -
    -
    - - -
    -
    - - -
    - - -
    -
    - - - - - -
    -
    -
    {{vm.editingRuleNode.component.name}}
    -
     
    -
    {{vm.editingRuleNode.component.configurationDescriptor.nodeDefinition.description}}
    -
     
    -
    -
    -
    -
    -
    -
    - - - - -
    - - -
    -
    -
    - -
    diff --git a/ui/src/app/rulechain/rulechains.controller.js b/ui/src/app/rulechain/rulechains.controller.js deleted file mode 100644 index 4438e32f60..0000000000 --- a/ui/src/app/rulechain/rulechains.controller.js +++ /dev/null @@ -1,215 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import addRuleChainTemplate from './add-rulechain.tpl.html'; -import ruleChainCard from './rulechain-card.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function RuleChainsController(ruleChainService, userService, importExport, $state, - $stateParams, $filter, $translate, $mdDialog, types) { - - var ruleChainActionsList = [ - { - onAction: function ($event, item) { - vm.grid.openItem($event, item); - }, - name: function() { return $translate.instant('rulechain.details') }, - details: function() { return $translate.instant('rulechain.rulechain-details') }, - icon: "edit" - }, - { - onAction: function ($event, item) { - exportRuleChain($event, item); - }, - name: function() { $translate.instant('action.export') }, - details: function() { return $translate.instant('rulechain.export') }, - icon: "file_download" - }, - { - onAction: function ($event, item) { - setRootRuleChain($event, item); - }, - name: function() { return $translate.instant('rulechain.set-root') }, - details: function() { return $translate.instant('rulechain.set-root') }, - icon: "flag", - isEnabled: isNonRootRuleChain - }, - { - onAction: function ($event, item) { - vm.grid.deleteItem($event, item); - }, - name: function() { return $translate.instant('action.delete') }, - details: function() { return $translate.instant('rulechain.delete') }, - icon: "delete", - isEnabled: isNonRootRuleChain - } - ]; - - var ruleChainAddItemActionsList = [ - { - onAction: function ($event) { - vm.grid.addItem($event); - }, - name: function() { return $translate.instant('action.create') }, - details: function() { return $translate.instant('rulechain.create-new-rulechain') }, - icon: "insert_drive_file" - }, - { - onAction: function ($event) { - importExport.importRuleChain($event).then( - function(ruleChainImport) { - $state.go('home.ruleChains.importRuleChain', {ruleChainImport:ruleChainImport}); - } - ); - }, - name: function() { return $translate.instant('action.import') }, - details: function() { return $translate.instant('rulechain.import') }, - icon: "file_upload" - } - ]; - - var vm = this; - - vm.types = types; - - vm.ruleChainGridConfig = { - - refreshParamsFunc: null, - - deleteItemTitleFunc: deleteRuleChainTitle, - deleteItemContentFunc: deleteRuleChainText, - deleteItemsTitleFunc: deleteRuleChainsTitle, - deleteItemsActionTitleFunc: deleteRuleChainsActionTitle, - deleteItemsContentFunc: deleteRuleChainsText, - - fetchItemsFunc: fetchRuleChains, - saveItemFunc: saveRuleChain, - clickItemFunc: openRuleChain, - deleteItemFunc: deleteRuleChain, - - getItemTitleFunc: getRuleChainTitle, - itemCardTemplateUrl: ruleChainCard, - parentCtl: vm, - - actionsList: ruleChainActionsList, - addItemActions: ruleChainAddItemActionsList, - - onGridInited: gridInited, - - addItemTemplateUrl: addRuleChainTemplate, - - addItemText: function() { return $translate.instant('rulechain.add-rulechain-text') }, - noItemsText: function() { return $translate.instant('rulechain.no-rulechains-text') }, - itemDetailsText: function() { return $translate.instant('rulechain.rulechain-details') }, - isSelectionEnabled: isNonRootRuleChain - }; - - if (angular.isDefined($stateParams.items) && $stateParams.items !== null) { - vm.ruleChainGridConfig.items = $stateParams.items; - } - - if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) { - vm.ruleChainGridConfig.topIndex = $stateParams.topIndex; - } - - vm.isRootRuleChain = isRootRuleChain; - vm.isNonRootRuleChain = isNonRootRuleChain; - - vm.exportRuleChain = exportRuleChain; - vm.setRootRuleChain = setRootRuleChain; - - function deleteRuleChainTitle(ruleChain) { - return $translate.instant('rulechain.delete-rulechain-title', {ruleChainName: ruleChain.name}); - } - - function deleteRuleChainText() { - return $translate.instant('rulechain.delete-rulechain-text'); - } - - function deleteRuleChainsTitle(selectedCount) { - return $translate.instant('rulechain.delete-rulechains-title', {count: selectedCount}, 'messageformat'); - } - - function deleteRuleChainsActionTitle(selectedCount) { - return $translate.instant('rulechain.delete-rulechains-action-title', {count: selectedCount}, 'messageformat'); - } - - function deleteRuleChainsText() { - return $translate.instant('rulechain.delete-rulechains-text'); - } - - function gridInited(grid) { - vm.grid = grid; - } - - function fetchRuleChains(pageLink) { - return ruleChainService.getRuleChains(pageLink); - } - - function saveRuleChain(ruleChain) { - return ruleChainService.saveRuleChain(ruleChain); - } - - function openRuleChain($event, ruleChain) { - if ($event) { - $event.stopPropagation(); - } - $state.go('home.ruleChains.ruleChain', {ruleChainId: ruleChain.id.id}); - } - - function deleteRuleChain(ruleChainId) { - return ruleChainService.deleteRuleChain(ruleChainId); - } - - function getRuleChainTitle(ruleChain) { - return ruleChain ? ruleChain.name : ''; - } - - function isRootRuleChain(ruleChain) { - return ruleChain && ruleChain.root; - } - - function isNonRootRuleChain(ruleChain) { - return ruleChain && !ruleChain.root; - } - - function exportRuleChain($event, ruleChain) { - $event.stopPropagation(); - importExport.exportRuleChain(ruleChain.id.id); - } - - function setRootRuleChain($event, ruleChain) { - $event.stopPropagation(); - var confirm = $mdDialog.confirm() - .targetEvent($event) - .title($translate.instant('rulechain.set-root-rulechain-title', {ruleChainName: ruleChain.name})) - .htmlContent($translate.instant('rulechain.set-root-rulechain-text')) - .ariaLabel($translate.instant('rulechain.set-root')) - .cancel($translate.instant('action.no')) - .ok($translate.instant('action.yes')); - $mdDialog.show(confirm).then(function () { - ruleChainService.setRootRuleChain(ruleChain.id.id).then( - () => { - vm.grid.refreshList(); - } - ); - }); - - } -} diff --git a/ui/src/app/rulechain/rulechains.tpl.html b/ui/src/app/rulechain/rulechains.tpl.html deleted file mode 100644 index 9880ac0213..0000000000 --- a/ui/src/app/rulechain/rulechains.tpl.html +++ /dev/null @@ -1,78 +0,0 @@ - - - -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    diff --git a/ui/src/app/rulechain/rulenode-config.directive.js b/ui/src/app/rulechain/rulenode-config.directive.js deleted file mode 100644 index a225253d73..0000000000 --- a/ui/src/app/rulechain/rulenode-config.directive.js +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import ruleNodeConfigTemplate from './rulenode-config.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function RuleNodeConfigDirective($compile, $templateCache, $injector, $translate) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - var template = $templateCache.get(ruleNodeConfigTemplate); - element.html(template); - - scope.$watch('configuration', function (newVal, prevVal) { - if (!angular.equals(newVal, prevVal)) { - ngModelCtrl.$setViewValue(scope.configuration); - } - }); - - ngModelCtrl.$render = function () { - scope.configuration = ngModelCtrl.$viewValue; - }; - - scope.useDefinedDirective = function() { - return scope.nodeDefinition && - scope.nodeDefinition.configDirective && !scope.definedDirectiveError; - }; - - scope.$watch('nodeDefinition', () => { - if (scope.nodeDefinition) { - validateDefinedDirective(); - } - }); - - function validateDefinedDirective() { - if (scope.nodeDefinition.uiResourceLoadError && scope.nodeDefinition.uiResourceLoadError.length) { - scope.definedDirectiveError = scope.nodeDefinition.uiResourceLoadError; - } else { - var definedDirective = scope.nodeDefinition.configDirective; - if (definedDirective && definedDirective.length) { - if (!$injector.has(definedDirective + 'Directive')) { - scope.definedDirectiveError = $translate.instant('rulenode.directive-is-not-loaded', {directiveName: definedDirective}); - } - } - } - } - - $compile(element.contents())(scope); - }; - - return { - restrict: "E", - require: "^ngModel", - scope: { - ruleNodeId:'=', - nodeDefinition:'=', - required:'=ngRequired', - readonly:'=ngReadonly' - }, - link: linker - }; - -} diff --git a/ui/src/app/rulechain/rulenode-config.tpl.html b/ui/src/app/rulechain/rulenode-config.tpl.html deleted file mode 100644 index 81a2408519..0000000000 --- a/ui/src/app/rulechain/rulenode-config.tpl.html +++ /dev/null @@ -1,32 +0,0 @@ - - - -
    {{definedDirectiveError}}
    - - diff --git a/ui/src/app/rulechain/rulenode-defined-config.directive.js b/ui/src/app/rulechain/rulenode-defined-config.directive.js deleted file mode 100644 index 38072e80f5..0000000000 --- a/ui/src/app/rulechain/rulenode-defined-config.directive.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -const SNAKE_CASE_REGEXP = /[A-Z]/g; - -/*@ngInject*/ -export default function RuleNodeDefinedConfigDirective($compile) { - - var linker = function (scope, element, attrs, ngModelCtrl) { - - attrs.$observe('ruleNodeDirective', function() { - loadTemplate(); - }); - - scope.$watch('configuration', function (newVal, prevVal) { - if (!angular.equals(newVal, prevVal)) { - ngModelCtrl.$setViewValue(scope.configuration); - } - }); - - ngModelCtrl.$render = function () { - scope.configuration = ngModelCtrl.$viewValue; - }; - - function loadTemplate() { - if (scope.ruleNodeConfigScope) { - scope.ruleNodeConfigScope.$destroy(); - } - var directive = snake_case(attrs.ruleNodeDirective, '-'); - var template = `<${directive} rule-node-id="ruleNodeId" ng-model="configuration" ng-required="required" ng-readonly="readonly">`; - element.html(template); - scope.ruleNodeConfigScope = scope.$new(); - $compile(element.contents())(scope.ruleNodeConfigScope); - } - - function snake_case(name, separator) { - separator = separator || '_'; - return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) { - return (pos ? separator : '') + letter.toLowerCase(); - }); - } - }; - - return { - restrict: "E", - require: "^ngModel", - scope: { - ruleNodeId:'=', - required:'=ngRequired', - readonly:'=ngReadonly' - }, - link: linker - }; - -} diff --git a/ui/src/app/rulechain/rulenode-fieldset.tpl.html b/ui/src/app/rulechain/rulenode-fieldset.tpl.html deleted file mode 100644 index 5a6f6039a7..0000000000 --- a/ui/src/app/rulechain/rulenode-fieldset.tpl.html +++ /dev/null @@ -1,64 +0,0 @@ - -{{ 'rulenode.delete' | translate }} - - -
    -
    -
    - - - -
    -
    rulenode.name-required
    -
    -
    - - {{ 'rulenode.debug-mode' | translate }} - - -
    - - - - - - -
    -
    - - - - - - -
    -
    -
    diff --git a/ui/src/app/rulechain/rulenode.directive.js b/ui/src/app/rulechain/rulenode.directive.js deleted file mode 100644 index 54e5e830cd..0000000000 --- a/ui/src/app/rulechain/rulenode.directive.js +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './rulenode.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import ruleNodeFieldsetTemplate from './rulenode-fieldset.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function RuleNodeDirective($compile, $templateCache, ruleChainService, types) { - var linker = function (scope, element) { - var template = $templateCache.get(ruleNodeFieldsetTemplate); - element.html(template); - - scope.types = types; - - scope.params = { - targetRuleChainId: null - }; - - scope.$watch('ruleNode', function() { - if (scope.ruleNode && scope.ruleNode.component.type == types.ruleNodeType.RULE_CHAIN.value) { - scope.params.targetRuleChainId = scope.ruleNode.targetRuleChainId; - watchTargetRuleChain(); - } else { - if (scope.targetRuleChainWatch) { - scope.targetRuleChainWatch(); - scope.targetRuleChainWatch = null; - } - } - }); - - function watchTargetRuleChain() { - scope.targetRuleChainWatch = scope.$watch('params.targetRuleChainId', - function(targetRuleChainId) { - if (scope.ruleNode.targetRuleChainId != targetRuleChainId) { - scope.ruleNode.targetRuleChainId = targetRuleChainId; - if (targetRuleChainId) { - ruleChainService.getRuleChain(targetRuleChainId).then( - (ruleChain) => { - scope.ruleNode.name = ruleChain.name; - } - ); - } else { - scope.ruleNode.name = ""; - } - } - } - ); - } - $compile(element.contents())(scope); - } - return { - restrict: "E", - link: linker, - scope: { - ruleChainId: '=', - ruleNode: '=', - isEdit: '=', - isReadOnly: '=', - theForm: '=', - onDeleteRuleNode: '&' - } - }; -} diff --git a/ui/src/app/rulechain/rulenode.scss b/ui/src/app/rulechain/rulenode.scss deleted file mode 100644 index b770224a51..0000000000 --- a/ui/src/app/rulechain/rulenode.scss +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-rulenode { - tb-json-object-edit.tb-rule-node-configuration-json { - display: block; - height: 300px; - } -} - -.tb-rulenode-directive-error { - font-size: 13px; - font-weight: 400; - color: rgb(221, 44, 0); -} diff --git a/ui/src/app/rulechain/rulenode.tpl.html b/ui/src/app/rulechain/rulenode.tpl.html deleted file mode 100644 index f58bcd8af8..0000000000 --- a/ui/src/app/rulechain/rulenode.tpl.html +++ /dev/null @@ -1,54 +0,0 @@ - -
    -
    -
    - {{node.icon}} - -
    - {{ node.component.name }} - {{ node.name }} -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    - × -
    -
    diff --git a/ui/src/app/rulechain/script/node-script-test.controller.js b/ui/src/app/rulechain/script/node-script-test.controller.js deleted file mode 100644 index 1fd8a79864..0000000000 --- a/ui/src/app/rulechain/script/node-script-test.controller.js +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './node-script-test.scss'; - -import Split from 'split.js'; - -import beautify from 'js-beautify'; - -const js_beautify = beautify.js; - -/*@ngInject*/ -export default function NodeScriptTestController($scope, $mdDialog, $window, $document, $timeout, - $q, $mdUtil, $translate, toast, types, utils, - ruleChainService, onShowingCallback, msg, msgType, metadata, - functionTitle, inputParams) { - - var vm = this; - - vm.types = types; - vm.functionTitle = functionTitle; - vm.inputParams = inputParams; - vm.inputParams.msg = js_beautify(angular.toJson(msg), {indent_size: 4}); - vm.inputParams.metadata = metadata; - vm.inputParams.msgType = msgType; - - vm.output = ''; - - vm.test = test; - vm.save = save; - vm.cancel = cancel; - - $scope.$watch('theForm.metadataForm.$dirty', (newVal) => { - if (newVal) { - toast.hide(); - } - }); - - onShowingCallback.onShowed = () => { - vm.nodeScriptTestDialogElement = angular.element('.tb-node-script-test-dialog'); - var w = vm.nodeScriptTestDialogElement.width(); - if (w > 0) { - initSplitLayout(); - } else { - $scope.$watch( - function () { - return vm.nodeScriptTestDialogElement[0].offsetWidth || parseInt(vm.nodeScriptTestDialogElement.css('width'), 10); - }, - function (newSize) { - if (newSize > 0) { - initSplitLayout(); - } - } - ); - } - }; - - function onDividerDrag() { - $scope.$broadcast('update-ace-editor-size'); - } - - function initSplitLayout() { - if (!vm.layoutInited) { - Split([angular.element('#top_panel', vm.nodeScriptTestDialogElement)[0], angular.element('#bottom_panel', vm.nodeScriptTestDialogElement)[0]], { - sizes: [35, 65], - gutterSize: 8, - cursor: 'row-resize', - direction: 'vertical', - onDrag: function () { - onDividerDrag() - } - }); - - Split([angular.element('#top_left_panel', vm.nodeScriptTestDialogElement)[0], angular.element('#top_right_panel', vm.nodeScriptTestDialogElement)[0]], { - sizes: [50, 50], - gutterSize: 8, - cursor: 'col-resize', - onDrag: function () { - onDividerDrag() - } - }); - - Split([angular.element('#bottom_left_panel', vm.nodeScriptTestDialogElement)[0], angular.element('#bottom_right_panel', vm.nodeScriptTestDialogElement)[0]], { - sizes: [50, 50], - gutterSize: 8, - cursor: 'col-resize', - onDrag: function () { - onDividerDrag() - } - }); - - onDividerDrag(); - - $scope.$applyAsync(function () { - vm.layoutInited = true; - var w = angular.element($window); - $timeout(function () { - w.triggerHandler('resize') - }); - }); - - } - } - - function test() { - testNodeScript().then( - (output) => { - vm.output = js_beautify(output, {indent_size: 4}); - } - ); - } - - function checkInputParamErrors() { - $scope.theForm.metadataForm.$setPristine(); - $scope.$broadcast('form-submit', 'validatePayload'); - if (!$scope.theForm.payloadForm.$valid) { - return false; - } else if (!$scope.theForm.metadataForm.$valid) { - showMetadataError($translate.instant('rulenode.metadata-required')); - return false; - } - return true; - } - - function showMetadataError(error) { - var toastParent = angular.element('#metadata-panel', vm.nodeScriptTestDialogElement); - toast.showError(error, toastParent, 'bottom left'); - } - - function testNodeScript() { - var deferred = $q.defer(); - if (checkInputParamErrors()) { - $mdUtil.nextTick(() => { - ruleChainService.testScript(vm.inputParams).then( - (result) => { - if (result.error) { - toast.showError(result.error); - deferred.reject(); - } else { - deferred.resolve(result.output); - } - }, - () => { - deferred.reject(); - } - ); - }); - } else { - deferred.reject(); - } - return deferred.promise; - } - - function cancel() { - $mdDialog.cancel(); - } - - function save() { - testNodeScript().then(() => { - $scope.theForm.funcBodyForm.$setPristine(); - $mdDialog.hide(vm.inputParams.script); - }); - } -} diff --git a/ui/src/app/rulechain/script/node-script-test.scss b/ui/src/app/rulechain/script/node-script-test.scss deleted file mode 100644 index 58da7fbf3b..0000000000 --- a/ui/src/app/rulechain/script/node-script-test.scss +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "../../../scss/constants"; - -@import "~compass-sass-mixins/lib/compass"; - -md-dialog.tb-node-script-test-dialog { - &.md-dialog-fullscreen { - width: 100%; - min-width: 100%; - max-width: 100%; - height: 100%; - min-height: 100%; - max-height: 100%; - border-radius: 0; - } - - .tb-split { - box-sizing: border-box; - overflow-x: hidden; - overflow-y: auto; - } - - .ace_editor { - font-size: 14px !important; - } - - .tb-content { - padding-top: 5px; - padding-left: 5px; - border: 1px solid #c0c0c0; - } - - .gutter { - background-color: #eee; - - background-repeat: no-repeat; - background-position: 50%; - } - - .gutter.gutter-horizontal { - cursor: col-resize; - background-image: url("../../../../node_modules/split.js/grips/vertical.png"); - } - - .gutter.gutter-vertical { - cursor: row-resize; - background-image: url("../../../../node_modules/split.js/grips/horizontal.png"); - } - - .tb-split.tb-split-horizontal, - .gutter.gutter-horizontal { - float: left; - height: 100%; - } - - .tb-split.tb-split-vertical { - display: flex; - - .tb-split.tb-content { - height: 100%; - } - } - - div.tb-editor-area-title-panel { - position: absolute; - top: 13px; - right: 40px; - z-index: 5; - font-size: .8rem; - font-weight: 500; - - &.tb-js-function { - right: 80px; - } - - label { - padding: 4px; - color: #00acc1; - text-transform: uppercase; - background: rgba(220, 220, 220, .35); - border-radius: 5px; - } - - .md-button { - min-width: 32px; - min-height: 15px; - padding: 4px; - margin: 0; - font-size: .8rem; - line-height: 15px; - color: #7b7b7b; - background: rgba(220, 220, 220, .35); - } - } - - .tb-resize-container { - position: relative; - width: 100%; - height: 100%; - overflow-y: auto; - - .ace_editor { - height: 100%; - } - } -} diff --git a/ui/src/app/rulechain/script/node-script-test.service.js b/ui/src/app/rulechain/script/node-script-test.service.js deleted file mode 100644 index ae1f0b0568..0000000000 --- a/ui/src/app/rulechain/script/node-script-test.service.js +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import nodeScriptTestTemplate from './node-script-test.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function NodeScriptTest($q, $mdDialog, $document, ruleChainService, utils) { - - var service = { - testNodeScript: testNodeScript - }; - - return service; - - function testNodeScript($event, script, scriptType, functionTitle, functionName, argNames, ruleNodeId) { - var deferred = $q.defer(); - if ($event) { - $event.stopPropagation(); - } - - var msg, metadata, msgType; - if (ruleNodeId) { - ruleChainService.getLatestRuleNodeDebugInput(ruleNodeId).then( - (debugIn) => { - if (debugIn) { - if (debugIn.data) { - msg = angular.fromJson(debugIn.data); - } - if (debugIn.metadata) { - metadata = angular.fromJson(debugIn.metadata); - } - msgType = debugIn.msgType; - } - openTestScriptDialog($event, script, scriptType, functionTitle, - functionName, argNames, msg, metadata, msgType).then( - (script) => { - deferred.resolve(script); - }, - () => { - deferred.reject(); - } - ); - }, - () => { - deferred.reject(); - } - ); - } else { - openTestScriptDialog($event, script, scriptType, functionTitle, - functionName, argNames).then( - (script) => { - deferred.resolve(script); - }, - () => { - deferred.reject(); - } - ); - } - return deferred.promise; - } - - function openTestScriptDialog($event, script, scriptType, functionTitle, functionName, argNames, msg, metadata, msgType) { - var deferred = $q.defer(); - if (!msg) { - msg = { - temperature: 22.4, - humidity: 78 - }; - } - if (!metadata) { - metadata = { - deviceType: "default", - deviceName: "Test Device", - ts: new Date().getTime() + "" - }; - } else { - metadata = utils.sortObjectKeys(metadata); - } - if (!msgType) { - msgType = "POST_TELEMETRY_REQUEST"; - } - - var onShowingCallback = { - onShowed: () => { - } - }; - - var inputParams = { - script: script, - scriptType: scriptType, - functionName: functionName, - argNames: argNames - }; - - $mdDialog.show({ - controller: 'NodeScriptTestController', - controllerAs: 'vm', - templateUrl: nodeScriptTestTemplate, - parent: angular.element($document[0].body), - locals: { - msg: msg, - metadata: metadata, - msgType: msgType, - functionTitle: functionTitle, - inputParams: inputParams, - onShowingCallback: onShowingCallback - }, - fullscreen: true, - multiple: true, - targetEvent: $event, - onComplete: () => { - onShowingCallback.onShowed(); - } - }).then( - (script) => { - deferred.resolve(script); - }, - () => { - deferred.reject(); - } - ); - return deferred.promise; - } - -} \ No newline at end of file diff --git a/ui/src/app/rulechain/script/node-script-test.tpl.html b/ui/src/app/rulechain/script/node-script-test.tpl.html deleted file mode 100644 index 426cdb22f8..0000000000 --- a/ui/src/app/rulechain/script/node-script-test.tpl.html +++ /dev/null @@ -1,118 +0,0 @@ - - -
    - -
    -

    {{ 'rulenode.test-script-function' | translate }}

    - - - - -
    -
    - -
    -
    -
    -
    -
    - -
    - -
    -
    - - -
    - - -
    -
    -
    -
    -
    -
    -
    - -
    - - - -
    -
    -
    -
    -
    -
    -
    - -
    - - - - -
    -
    -
    -
    -
    - -
    - - -
    -
    -
    -
    -
    - - - {{ 'rulenode.test' | translate }} - - - - {{ 'action.save' | translate }} - - {{ 'action.cancel' | translate }} - -
    -
    diff --git a/ui/src/app/services/clipboard.service.js b/ui/src/app/services/clipboard.service.js deleted file mode 100644 index f44d57d3fb..0000000000 --- a/ui/src/app/services/clipboard.service.js +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default angular.module('thingsboard.clipboard', []) - .factory('clipboardService', ClipboardService) - .name; - -/*@ngInject*/ -function ClipboardService($q) { - - var fakeHandler, fakeHandlerCallback, fakeElem; - - var service = { - copyToClipboard: copyToClipboard - }; - - return service; - - /* eslint-disable */ - function copyToClipboard(trigger, text) { - var deferred = $q.defer(); - const isRTL = document.documentElement.getAttribute('dir') == 'rtl'; - removeFake(); - fakeHandlerCallback = () => removeFake(); - fakeHandler = document.body.addEventListener('click', fakeHandlerCallback) || true; - fakeElem = document.createElement('textarea'); - fakeElem.style.fontSize = '12pt'; - fakeElem.style.border = '0'; - fakeElem.style.padding = '0'; - fakeElem.style.margin = '0'; - fakeElem.style.position = 'absolute'; - fakeElem.style[ isRTL ? 'right' : 'left' ] = '-9999px'; - let yPosition = window.pageYOffset || document.documentElement.scrollTop; - fakeElem.style.top = `${yPosition}px`; - fakeElem.setAttribute('readonly', ''); - fakeElem.value = text; - document.body.appendChild(fakeElem); - var selectedText = select(fakeElem); - - let succeeded; - try { - succeeded = document.execCommand('copy'); - } - catch (err) { - succeeded = false; - } - if (trigger) { - trigger.focus(); - } - window.getSelection().removeAllRanges(); - removeFake(); - if (succeeded) { - deferred.resolve(selectedText); - } else { - deferred.reject(); - } - return deferred.promise; - } - - function removeFake() { - if (fakeHandler) { - document.body.removeEventListener('click', fakeHandlerCallback); - fakeHandler = null; - fakeHandlerCallback = null; - } - if (fakeElem) { - document.body.removeChild(fakeElem); - fakeElem = null; - } - } - - function select(element) { - var selectedText; - - if (element.nodeName === 'SELECT') { - element.focus(); - - selectedText = element.value; - } - else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') { - var isReadOnly = element.hasAttribute('readonly'); - - if (!isReadOnly) { - element.setAttribute('readonly', ''); - } - - element.select(); - element.setSelectionRange(0, element.value.length); - - if (!isReadOnly) { - element.removeAttribute('readonly'); - } - - selectedText = element.value; - } - else { - if (element.hasAttribute('contenteditable')) { - element.focus(); - } - - var selection = window.getSelection(); - var range = document.createRange(); - - range.selectNodeContents(element); - selection.removeAllRanges(); - selection.addRange(range); - - selectedText = selection.toString(); - } - - return selectedText; - } - - /* eslint-enable */ - -} \ No newline at end of file diff --git a/ui/src/app/services/error-toast.tpl.html b/ui/src/app/services/error-toast.tpl.html deleted file mode 100644 index fb9fb3a775..0000000000 --- a/ui/src/app/services/error-toast.tpl.html +++ /dev/null @@ -1,25 +0,0 @@ - - -
    -
    - - {{ 'action.close' | translate }} - -
    -
    diff --git a/ui/src/app/services/info-toast.tpl.html b/ui/src/app/services/info-toast.tpl.html deleted file mode 100644 index a1ec3693ae..0000000000 --- a/ui/src/app/services/info-toast.tpl.html +++ /dev/null @@ -1,25 +0,0 @@ - - -
    -
    - - {{ 'action.close' | translate }} - -
    -
    diff --git a/ui/src/app/services/item-buffer.service.js b/ui/src/app/services/item-buffer.service.js deleted file mode 100644 index 2bca07ef71..0000000000 --- a/ui/src/app/services/item-buffer.service.js +++ /dev/null @@ -1,412 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import angularStorage from 'angular-storage'; - -export default angular.module('thingsboard.itembuffer', [angularStorage]) - .factory('itembuffer', ItemBuffer) - .factory('bufferStore', function(store) { - var newStore = store.getNamespacedStore('tbBufferStore', null, null, false); - return newStore; - }) - .name; - -/*@ngInject*/ -function ItemBuffer($q, bufferStore, types, utils, dashboardUtils, ruleChainService) { - - const WIDGET_ITEM = "widget_item"; - const WIDGET_REFERENCE = "widget_reference"; - const RULE_NODES = "rule_nodes"; - - var service = { - prepareWidgetItem: prepareWidgetItem, - copyWidget: copyWidget, - copyWidgetReference: copyWidgetReference, - hasWidget: hasWidget, - canPasteWidgetReference: canPasteWidgetReference, - pasteWidget: pasteWidget, - pasteWidgetReference: pasteWidgetReference, - addWidgetToDashboard: addWidgetToDashboard, - copyRuleNodes: copyRuleNodes, - hasRuleNodes: hasRuleNodes, - pasteRuleNodes: pasteRuleNodes - } - - return service; - - /** - aliasesInfo { - datasourceAliases: { - datasourceIndex: { - alias: "...", - filter: "..." - } - } - targetDeviceAliases: { - targetDeviceAliasIndex: { - alias: "...", - filter: "..." - } - } - .... - } - **/ - - function prepareAliasInfo(entityAlias) { - return { - alias: entityAlias.alias, - filter: entityAlias.filter - }; - } - - function getOriginalColumns(dashboard, sourceState, sourceLayout) { - var originalColumns = 24; - var gridSettings = null; - var state = dashboard.configuration.states[sourceState]; - var layoutCount = Object.keys(state.layouts).length; - if (state) { - var layout = state.layouts[sourceLayout]; - if (layout) { - gridSettings = layout.gridSettings; - - } - } - if (gridSettings && - gridSettings.columns) { - originalColumns = gridSettings.columns; - } - originalColumns = originalColumns * layoutCount; - return originalColumns; - } - - function getOriginalSize(dashboard, sourceState, sourceLayout, widget) { - var layout = dashboard.configuration.states[sourceState].layouts[sourceLayout]; - var widgetLayout = layout.widgets[widget.id]; - return { - sizeX: widgetLayout.sizeX, - sizeY: widgetLayout.sizeY - } - } - - function prepareWidgetItem(dashboard, sourceState, sourceLayout, widget) { - var aliasesInfo = { - datasourceAliases: {}, - targetDeviceAliases: {} - }; - var originalColumns = getOriginalColumns(dashboard, sourceState, sourceLayout); - var originalSize = getOriginalSize(dashboard, sourceState, sourceLayout, widget); - if (widget.config && dashboard.configuration - && dashboard.configuration.entityAliases) { - var entityAlias; - if (widget.config.datasources) { - for (var i=0;i - -
    -
    - - {{ 'action.close' | translate }} - -
    -
    diff --git a/ui/src/app/services/toast.controller.js b/ui/src/app/services/toast.controller.js deleted file mode 100644 index c64621446b..0000000000 --- a/ui/src/app/services/toast.controller.js +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/*@ngInject*/ -export default function ToastController($mdToast, message) { - var vm = this; - vm.message = message; - - vm.closeToast = closeToast; - - function closeToast() { - $mdToast.hide(); - } - -} diff --git a/ui/src/app/services/toast.js b/ui/src/app/services/toast.js deleted file mode 100644 index 8fafc2caa4..0000000000 --- a/ui/src/app/services/toast.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './toast.scss'; - -import Toast from './toast.service'; -import ToastController from './toast.controller'; - -export default angular.module('thingsboard.toast', []) - .factory('toast', Toast) - .controller('ToastController', ToastController) - .name; diff --git a/ui/src/app/services/toast.scss b/ui/src/app/services/toast.scss deleted file mode 100644 index a4b4f807ca..0000000000 --- a/ui/src/app/services/toast.scss +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -md-toast.tb-info-toast .md-toast-content { - height: 100%; - max-height: 100%; - padding: 18px; - font-size: 18px; -} - -md-toast.tb-success-toast .md-toast-content { - height: 100%; - max-height: 100%; - font-size: 18px !important; - background-color: #008000; -} - -md-toast.tb-error-toast .md-toast-content { - height: 100%; - max-height: 100%; - font-size: 18px !important; - background-color: #800000; -} diff --git a/ui/src/app/services/toast.service.js b/ui/src/app/services/toast.service.js deleted file mode 100644 index 963298c2b1..0000000000 --- a/ui/src/app/services/toast.service.js +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import infoToast from './info-toast.tpl.html'; -import successToast from './success-toast.tpl.html'; -import errorToast from './error-toast.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function Toast($mdToast, $document) { - - var showing = false; - - var service = { - showInfo: showInfo, - showSuccess: showSuccess, - showError: showError, - hide: hide - } - - return service; - - function showInfo(infoMessage, delay, toastParent, position) { - showMessage(infoToast, infoMessage, delay, toastParent, position); - } - - function showSuccess(successMessage, delay, toastParent, position) { - showMessage(successToast, successMessage, delay, toastParent, position); - } - - function showMessage(templateUrl, message, delay, toastParent, position) { - if (!toastParent) { - toastParent = angular.element($document[0].getElementById('toast-parent')); - } - if (!position) { - position = 'top left'; - } - $mdToast.show({ - hideDelay: delay || 0, - position: position, - controller: 'ToastController', - controllerAs: 'vm', - templateUrl: templateUrl, - locals: {message: message}, - parent: toastParent - }); - } - - function showError(errorMessage, toastParent, position) { - if (!showing) { - if (!toastParent) { - toastParent = angular.element($document[0].getElementById('toast-parent')); - } - if (!position) { - position = 'top left'; - } - showing = true; - $mdToast.show({ - hideDelay: 0, - position: position, - controller: 'ToastController', - controllerAs: 'vm', - templateUrl: errorToast, - locals: {message: errorMessage}, - parent: toastParent - }).then(function hide() { - showing = false; - }, function cancel() { - showing = false; - }); - } - } - - function hide() { - if (showing) { - $mdToast.hide(); - } - } - -} \ No newline at end of file diff --git a/ui/src/app/tenant/add-tenant.tpl.html b/ui/src/app/tenant/add-tenant.tpl.html deleted file mode 100644 index 6b86bdadac..0000000000 --- a/ui/src/app/tenant/add-tenant.tpl.html +++ /dev/null @@ -1,48 +0,0 @@ - - -
    - -
    -

    tenant.add

    - -
    - - - -
    -
    - - - -
    - -
    -
    - - - - {{ 'action.add' | translate }} - - {{ 'action.cancel' | - translate }} - - -
    -
    diff --git a/ui/src/app/tenant/index.js b/ui/src/app/tenant/index.js deleted file mode 100644 index 60cd754b25..0000000000 --- a/ui/src/app/tenant/index.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import uiRouter from 'angular-ui-router'; -import thingsboardGrid from '../components/grid.directive'; -import thingsboardApiTenant from '../api/tenant.service'; -import thingsboardContact from '../components/contact.directive'; -import thingsboardContactShort from '../components/contact-short.filter'; - -import TenantRoutes from './tenant.routes'; -import TenantController from './tenant.controller'; -import TenantDirective from './tenant.directive'; - -export default angular.module('thingsboard.tenant', [ - uiRouter, - thingsboardGrid, - thingsboardApiTenant, - thingsboardContact, - thingsboardContactShort -]) - .config(TenantRoutes) - .controller('TenantController', TenantController) - .directive('tbTenant', TenantDirective) - .name; diff --git a/ui/src/app/tenant/tenant-card.tpl.html b/ui/src/app/tenant/tenant-card.tpl.html deleted file mode 100644 index 0a9a51549d..0000000000 --- a/ui/src/app/tenant/tenant-card.tpl.html +++ /dev/null @@ -1,18 +0,0 @@ - -
    {{ item | contactShort }}
    \ No newline at end of file diff --git a/ui/src/app/tenant/tenant-fieldset.tpl.html b/ui/src/app/tenant/tenant-fieldset.tpl.html deleted file mode 100644 index b75ea30f06..0000000000 --- a/ui/src/app/tenant/tenant-fieldset.tpl.html +++ /dev/null @@ -1,64 +0,0 @@ - -{{ - 'tenant.manage-tenant-admins' | translate }} - -{{ 'tenant.delete' - | translate }} - - -
    - - - tenant.copyId - -
    - - -
    - - - -
    -
    tenant.title-required
    -
    -
    - - - - - - - - {{'tenant.isolated-tb-core' | translate}}
    - {{'tenant.isolated-tb-core-details' | translate}} -
    -
    - - - {{'tenant.isolated-tb-rule-engine' | translate}}
    - {{'tenant.isolated-tb-rule-engine-details' | translate}} -
    -
    -
    -
    diff --git a/ui/src/app/tenant/tenant.controller.js b/ui/src/app/tenant/tenant.controller.js deleted file mode 100644 index 00b4fcd869..0000000000 --- a/ui/src/app/tenant/tenant.controller.js +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import addTenantTemplate from './add-tenant.tpl.html'; -import tenantCard from './tenant-card.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function TenantController(tenantService, $state, $stateParams, $translate, types) { - - var tenantActionsList = [ - { - onAction: function ($event, item) { - openTenantUsers($event, item); - }, - name: function() { return $translate.instant('tenant.admins') }, - details: function() { return $translate.instant('tenant.manage-tenant-admins') }, - icon: "account_circle" - }, - { - onAction: function ($event, item) { - vm.grid.deleteItem($event, item); - }, - name: function() { return $translate.instant('action.delete') }, - details: function() { return $translate.instant('tenant.delete') }, - icon: "delete" - } - ]; - - var vm = this; - - vm.types = types; - - vm.tenantGridConfig = { - - refreshParamsFunc: null, - - deleteItemTitleFunc: deleteTenantTitle, - deleteItemContentFunc: deleteTenantText, - deleteItemsTitleFunc: deleteTenantsTitle, - deleteItemsActionTitleFunc: deleteTenantsActionTitle, - deleteItemsContentFunc: deleteTenantsText, - - fetchItemsFunc: fetchTenants, - saveItemFunc: saveTenant, - deleteItemFunc: deleteTenant, - - getItemTitleFunc: getTenantTitle, - - itemCardTemplateUrl: tenantCard, - - actionsList: tenantActionsList, - - onGridInited: gridInited, - - addItemTemplateUrl: addTenantTemplate, - - addItemText: function() { return $translate.instant('tenant.add-tenant-text') }, - noItemsText: function() { return $translate.instant('tenant.no-tenants-text') }, - itemDetailsText: function() { return $translate.instant('tenant.tenant-details') } - }; - - if (angular.isDefined($stateParams.items) && $stateParams.items !== null) { - vm.tenantGridConfig.items = $stateParams.items; - } - - if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) { - vm.tenantGridConfig.topIndex = $stateParams.topIndex; - } - - vm.openTenantUsers = openTenantUsers; - - function deleteTenantTitle(tenant) { - return $translate.instant('tenant.delete-tenant-title', {tenantTitle: tenant.title}); - } - - function deleteTenantText() { - return $translate.instant('tenant.delete-tenant-text'); - } - - function deleteTenantsTitle(selectedCount) { - return $translate.instant('tenant.delete-tenants-title', {count: selectedCount}, 'messageformat'); - } - - function deleteTenantsActionTitle(selectedCount) { - return $translate.instant('tenant.delete-tenants-action-title', {count: selectedCount}, 'messageformat'); - } - - function deleteTenantsText() { - return $translate.instant('tenant.delete-tenants-text'); - } - - function gridInited(grid) { - vm.grid = grid; - } - - function fetchTenants(pageLink) { - return tenantService.getTenants(pageLink); - } - - function saveTenant(tenant) { - return tenantService.saveTenant(tenant); - } - - function deleteTenant(tenantId) { - return tenantService.deleteTenant(tenantId); - } - - function getTenantTitle(tenant) { - return tenant ? tenant.title : ''; - } - - function openTenantUsers($event, tenant) { - if ($event) { - $event.stopPropagation(); - } - $state.go('home.tenants.users', {tenantId: tenant.id.id}); - } -} diff --git a/ui/src/app/tenant/tenant.directive.js b/ui/src/app/tenant/tenant.directive.js deleted file mode 100644 index 2dcee6ebfe..0000000000 --- a/ui/src/app/tenant/tenant.directive.js +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import tenantFieldsetTemplate from './tenant-fieldset.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function TenantDirective($compile, $templateCache, $translate, toast) { - var linker = function (scope, element) { - var template = $templateCache.get(tenantFieldsetTemplate); - element.html(template); - - scope.onTenantIdCopied = function() { - toast.showSuccess($translate.instant('tenant.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left'); - }; - - $compile(element.contents())(scope); - } - return { - restrict: "E", - link: linker, - scope: { - tenant: '=', - isEdit: '=', - theForm: '=', - onManageUsers: '&', - onDeleteTenant: '&' - } - }; -} diff --git a/ui/src/app/tenant/tenant.routes.js b/ui/src/app/tenant/tenant.routes.js deleted file mode 100644 index c4993ac51c..0000000000 --- a/ui/src/app/tenant/tenant.routes.js +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import tenantsTemplate from './tenants.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function TenantRoutes($stateProvider) { - - $stateProvider - .state('home.tenants', { - url: '/tenants', - params: {'topIndex': 0}, - module: 'private', - auth: ['SYS_ADMIN'], - views: { - "content@home": { - templateUrl: tenantsTemplate, - controllerAs: 'vm', - controller: 'TenantController' - } - }, - data: { - searchEnabled: true, - pageTitle: 'tenant.tenants' - }, - ncyBreadcrumb: { - label: '{"icon": "supervisor_account", "label": "tenant.tenants"}' - } - }); -} diff --git a/ui/src/app/tenant/tenants.tpl.html b/ui/src/app/tenant/tenants.tpl.html deleted file mode 100644 index 563e8103ce..0000000000 --- a/ui/src/app/tenant/tenants.tpl.html +++ /dev/null @@ -1,67 +0,0 @@ - - - -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - -
    diff --git a/ui/src/app/url.handler.js b/ui/src/app/url.handler.js deleted file mode 100644 index 3451aded38..0000000000 --- a/ui/src/app/url.handler.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export default function UrlHandler($injector, $location) { - var userService = $injector.get('userService'); - if (userService.isUserLoaded() === true) { - userService.gotoDefaultPlace(); - } else { - var $rootScope = $injector.get('$rootScope'); - if ($rootScope.userLoadedHandle) { - $rootScope.userLoadedHandle(); - } - $rootScope.userLoadedHandle = $rootScope.$on('userLoaded', function () { - $rootScope.userLoadedHandle(); - UrlHandler($injector, $location); - }); - } -} \ No newline at end of file diff --git a/ui/src/app/user/activation-link.controller.js b/ui/src/app/user/activation-link.controller.js deleted file mode 100644 index 92a8893d2c..0000000000 --- a/ui/src/app/user/activation-link.controller.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/*@ngInject*/ -export default function ActivationLinkDialogController($mdDialog, $translate, toast, activationLink) { - - var vm = this; - - vm.activationLink = activationLink; - - vm.onActivationLinkCopied = onActivationLinkCopied; - vm.close = close; - - function onActivationLinkCopied(){ - toast.showSuccess($translate.instant('user.activation-link-copied-message'), 750, angular.element('#activation-link-dialog-content'), 'bottom left'); - } - - function close() { - $mdDialog.hide(); - } - -} \ No newline at end of file diff --git a/ui/src/app/user/activation-link.dialog.tpl.html b/ui/src/app/user/activation-link.dialog.tpl.html deleted file mode 100644 index e932ceffbb..0000000000 --- a/ui/src/app/user/activation-link.dialog.tpl.html +++ /dev/null @@ -1,55 +0,0 @@ - - -
    - -
    -

    - - - - -
    -
    - - - - - - {{ 'action.ok' | - translate }} - - -
    -
    diff --git a/ui/src/app/user/add-user.controller.js b/ui/src/app/user/add-user.controller.js deleted file mode 100644 index 095d765a28..0000000000 --- a/ui/src/app/user/add-user.controller.js +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import activationLinkDialogTemplate from './activation-link.dialog.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - - -/*@ngInject*/ -export default function AddUserController($scope, $mdDialog, $state, $stateParams, $document, $q, types, userService, saveItemFunction, helpLinks) { - - var vm = this; - - var tenantId = $stateParams.tenantId; - var customerId = $stateParams.customerId; - var usersType = $state.$current.data.usersType; - - vm.helpLinks = helpLinks; - vm.item = {}; - - vm.activationMethods = [ - { - value: 'displayActivationLink', - name: 'user.display-activation-link' - }, - { - value: 'sendActivationMail', - name: 'user.send-activation-mail' - } - ]; - - vm.userActivationMethod = 'displayActivationLink'; - - vm.add = add; - vm.cancel = cancel; - - function cancel() { - $mdDialog.cancel(); - } - - function add($event) { - var sendActivationMail = false; - if (vm.userActivationMethod == 'sendActivationMail') { - sendActivationMail = true; - } - if (usersType === 'tenant') { - vm.item.authority = "TENANT_ADMIN"; - vm.item.tenantId = { - entityType: types.entityType.tenant, - id: tenantId - }; - } else if (usersType === 'customer') { - vm.item.authority = "CUSTOMER_USER"; - vm.item.customerId = { - entityType: types.entityType.customer, - id: customerId - }; - } - userService.saveUser(vm.item, sendActivationMail).then(function success(item) { - vm.item = item; - $scope.theForm.$setPristine(); - if (vm.userActivationMethod == 'displayActivationLink') { - userService.getActivationLink(vm.item.id.id).then( - function success(activationLink) { - displayActivationLink($event, activationLink).then( - function() { - $mdDialog.hide(); - } - ); - } - ); - } else { - $mdDialog.hide(); - } - }); - } - - function displayActivationLink($event, activationLink) { - var deferred = $q.defer(); - $mdDialog.show({ - controller: 'ActivationLinkDialogController', - controllerAs: 'vm', - templateUrl: activationLinkDialogTemplate, - locals: { - activationLink: activationLink - }, - parent: angular.element($document[0].body), - fullscreen: true, - multiple: true, - targetEvent: $event - }).then(function () { - deferred.resolve(); - }); - return deferred.promise; - } - -} \ No newline at end of file diff --git a/ui/src/app/user/add-user.tpl.html b/ui/src/app/user/add-user.tpl.html deleted file mode 100644 index d6e9023743..0000000000 --- a/ui/src/app/user/add-user.tpl.html +++ /dev/null @@ -1,54 +0,0 @@ - - -
    - -
    -

    user.add

    - -
    - - - -
    -
    - - - -
    - - - - - - {{activationMethod.name | translate}} - - - -
    -
    - - - - {{ 'action.add' | translate }} - - {{ 'action.cancel' | translate }} - -
    -
    diff --git a/ui/src/app/user/index.js b/ui/src/app/user/index.js deleted file mode 100644 index d5c2e410ab..0000000000 --- a/ui/src/app/user/index.js +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import uiRouter from 'angular-ui-router'; -import thingsboardGrid from '../components/grid.directive'; -import thingsboardApiUser from '../api/user.service'; -import thingsboardToast from '../services/toast'; - -import UserRoutes from './user.routes'; -import UserController from './user.controller'; -import AddUserController from './add-user.controller'; -import ActivationLinkDialogController from './activation-link.controller'; -import UserDirective from './user.directive'; - -export default angular.module('thingsboard.user', [ - uiRouter, - thingsboardGrid, - thingsboardApiUser, - thingsboardToast -]) - .config(UserRoutes) - .controller('UserController', UserController) - .controller('AddUserController', AddUserController) - .controller('ActivationLinkDialogController', ActivationLinkDialogController) - .directive('tbUser', UserDirective) - .name; diff --git a/ui/src/app/user/user-card.tpl.html b/ui/src/app/user/user-card.tpl.html deleted file mode 100644 index ec406023df..0000000000 --- a/ui/src/app/user/user-card.tpl.html +++ /dev/null @@ -1,18 +0,0 @@ - -
    {{item ? ( ( item.firstName ? item.firstName : '' ) + ' ' + ( item.lastName ? item.lastName : '') ) : ''}}
    diff --git a/ui/src/app/user/user-fieldset.scss b/ui/src/app/user/user-fieldset.scss deleted file mode 100644 index 5662c0ada8..0000000000 --- a/ui/src/app/user/user-fieldset.scss +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "../../scss/constants"; - -.tb-default-dashboard { - .tb-default-dashboard-label { - padding-bottom: 8px; - } - - tb-dashboard-autocomplete { - @media (min-width: $layout-breakpoint-sm) { - padding-right: 12px; - } - - @media (max-width: $layout-breakpoint-sm) { - padding-bottom: 12px; - } - } -} diff --git a/ui/src/app/user/user-fieldset.tpl.html b/ui/src/app/user/user-fieldset.tpl.html deleted file mode 100644 index 9e3e34a409..0000000000 --- a/ui/src/app/user/user-fieldset.tpl.html +++ /dev/null @@ -1,87 +0,0 @@ - -{{ 'user.disable-account' | - translate }} - -{{ 'user.enable-account' | - translate }} - -{{ - 'user.display-activation-link' | translate }} - -{{ - 'user.resend-activation' | translate }} - -{{ - (isTenantAdmin() ? 'user.login-as-tenant-admin' : 'user.login-as-customer-user') | translate }} - -{{ 'user.delete' | - translate }} - - - -
    - - - -
    -
    user.email-required
    -
    user.invalid-email-format
    -
    -
    - - - - - - - - - - - - -
    - user.default-dashboard -
    - - - - - {{ 'user.always-fullscreen' | translate }} - -
    -
    -
    -
    diff --git a/ui/src/app/user/user.controller.js b/ui/src/app/user/user.controller.js deleted file mode 100644 index bbe424c7a9..0000000000 --- a/ui/src/app/user/user.controller.js +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import addUserTemplate from './add-user.tpl.html'; -import userCard from './user-card.tpl.html'; -import activationLinkDialogTemplate from './activation-link.dialog.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - - -/*@ngInject*/ -export default function UserController(userService, toast, $scope, $mdDialog, $document, $controller, $state, $stateParams, $translate, types) { - - var tenantId = $stateParams.tenantId; - var customerId = $stateParams.customerId; - var usersType = $state.$current.data.usersType; - - var userActionsList = [ - { - onAction: function ($event, item) { - loginAsUser(item); - }, - name: function() { return $translate.instant('login.login') }, - details: function() { return $translate.instant(usersType === 'tenant' ? 'user.login-as-tenant-admin' : 'user.login-as-customer-user') }, - icon: "login", - isEnabled: function() { - return userService.isUserTokenAccessEnabled(); - } - }, - { - onAction: function ($event, item) { - vm.grid.deleteItem($event, item); - }, - name: function() { return $translate.instant('action.delete') }, - details: function() { return $translate.instant('user.delete') }, - icon: "delete" - } - ]; - - var vm = this; - - vm.types = types; - - vm.userGridConfig = { - deleteItemTitleFunc: deleteUserTitle, - deleteItemContentFunc: deleteUserText, - deleteItemsTitleFunc: deleteUsersTitle, - deleteItemsActionTitleFunc: deleteUsersActionTitle, - deleteItemsContentFunc: deleteUsersText, - - deleteItemFunc: deleteUser, - - getItemTitleFunc: getUserTitle, - itemCardTemplateUrl: userCard, - - actionsList: userActionsList, - - onGridInited: gridInited, - - addItemTemplateUrl: addUserTemplate, - addItemController: 'AddUserController', - - addItemText: function() { return $translate.instant('user.add-user-text') }, - noItemsText: function() { return $translate.instant('user.no-users-text') }, - itemDetailsText: function() { return $translate.instant('user.user-details') } - }; - - if (angular.isDefined($stateParams.items) && $stateParams.items !== null) { - vm.userGridConfig.items = $stateParams.items; - } - - if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) { - vm.userGridConfig.topIndex = $stateParams.topIndex; - } - - vm.displayActivationLink = displayActivationLink; - vm.resendActivation = resendActivation; - vm.loginAsUser = loginAsUser; - vm.setUserCredentialsEnabled = setUserCredentialsEnabled; - - initController(); - - function initController() { - var fetchUsersFunction = null; - var saveUserFunction = null; - var refreshUsersParamsFunction = null; - - if (usersType === 'tenant') { - fetchUsersFunction = function (pageLink) { - return userService.getTenantAdmins(tenantId, pageLink); - }; - saveUserFunction = function (user) { - user.authority = "TENANT_ADMIN"; - user.tenantId = { - entityType: types.entityType.tenant, - id: tenantId - }; - return userService.saveUser(user); - }; - refreshUsersParamsFunction = function () { - return {"tenantId": tenantId, "topIndex": vm.topIndex}; - }; - - } else if (usersType === 'customer') { - fetchUsersFunction = function (pageLink) { - return userService.getCustomerUsers(customerId, pageLink); - }; - saveUserFunction = function (user) { - user.authority = "CUSTOMER_USER"; - user.customerId = { - entityType: types.entityType.customer, - id: customerId - }; - return userService.saveUser(user); - }; - refreshUsersParamsFunction = function () { - return {"customerId": customerId, "topIndex": vm.topIndex}; - }; - } - - vm.userGridConfig.refreshParamsFunc = refreshUsersParamsFunction; - vm.userGridConfig.fetchItemsFunc = fetchUsersFunction; - vm.userGridConfig.saveItemFunc = saveUserFunction; - } - - function deleteUserTitle(user) { - return $translate.instant('user.delete-user-title', {userEmail: user.email}); - } - - function deleteUserText() { - return $translate.instant('user.delete-user-text'); - } - - function deleteUsersTitle(selectedCount) { - return $translate.instant('user.delete-users-title', {count: selectedCount}, 'messageformat'); - } - - function deleteUsersActionTitle(selectedCount) { - return $translate.instant('user.delete-users-action-title', {count: selectedCount}, 'messageformat'); - } - - function deleteUsersText() { - return $translate.instant('user.delete-users-text'); - } - - function gridInited(grid) { - vm.grid = grid; - } - - function getUserTitle(user) { - return user ? user.email : ''; - } - - function deleteUser(userId) { - return userService.deleteUser(userId); - } - - function displayActivationLink(event, user) { - userService.getActivationLink(user.id.id).then( - function success(activationLink) { - openActivationLinkDialog(event, activationLink); - } - ); - } - - function setUserCredentialsEnabled(user, userCredentialsEnabled) { - userService.setUserCredentialsEnabled(user.id.id, userCredentialsEnabled).then( - () => { - if (!user.additionalInfo) { - user.additionalInfo = {}; - } - user.additionalInfo.userCredentialsEnabled = userCredentialsEnabled; - if (userCredentialsEnabled) { - toast.showSuccess($translate.instant('user.enable-account-message')); - } else { - toast.showSuccess($translate.instant('user.disable-account-message')); - } - } - ) - } - - function openActivationLinkDialog(event, activationLink) { - $mdDialog.show({ - controller: 'ActivationLinkDialogController', - controllerAs: 'vm', - templateUrl: activationLinkDialogTemplate, - locals: { - activationLink: activationLink - }, - parent: angular.element($document[0].body), - fullscreen: true, - multiple: true, - targetEvent: event - }); - } - - function resendActivation(user) { - userService.sendActivationEmail(user.email).then(function success() { - toast.showSuccess($translate.instant('user.activation-email-sent-message')); - }); - } - - function loginAsUser(user) { - userService.loginAsUser(user.id.id); - } -} diff --git a/ui/src/app/user/user.directive.js b/ui/src/app/user/user.directive.js deleted file mode 100644 index 59ed8eba30..0000000000 --- a/ui/src/app/user/user.directive.js +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './user-fieldset.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import userFieldsetTemplate from './user-fieldset.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function UserDirective($compile, $templateCache, userService) { - var linker = function (scope, element) { - var template = $templateCache.get(userFieldsetTemplate); - element.html(template); - - scope.isTenantAdmin = function() { - return scope.user && scope.user.authority === 'TENANT_ADMIN'; - }; - - scope.isCustomerUser = function() { - return scope.user && scope.user.authority === 'CUSTOMER_USER'; - }; - - scope.isUserCredentialsEnabled = function() { - if (scope.user == null || angular.isUndefined(scope.user) || - scope.user.additionalInfo == null || angular.isUndefined(scope.user.additionalInfo) || - scope.user.additionalInfo.userCredentialsEnabled == null || angular.isUndefined(scope.user.additionalInfo.userCredentialsEnabled)) { - return true; - } - return scope.user.additionalInfo.userCredentialsEnabled === true; - }; - - scope.loginAsUserEnabled = userService.isUserTokenAccessEnabled(); - - $compile(element.contents())(scope); - } - return { - restrict: "E", - link: linker, - scope: { - user: '=', - isEdit: '=', - theForm: '=', - onDisplayActivationLink: '&', - onResendActivation: '&', - onLoginAsUser: '&', - onDeleteUser: '&', - onSetUserCredentialsEnabled: '&', - } - }; -} diff --git a/ui/src/app/user/user.routes.js b/ui/src/app/user/user.routes.js deleted file mode 100644 index 893772b8bf..0000000000 --- a/ui/src/app/user/user.routes.js +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import usersTemplate from '../user/users.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function UserRoutes($stateProvider) { - - $stateProvider - .state('home.tenants.users', { - url: '/:tenantId/users', - params: {'topIndex': 0}, - module: 'private', - auth: ['SYS_ADMIN'], - views: { - "content@home": { - templateUrl: usersTemplate, - controllerAs: 'vm', - controller: 'UserController' - } - }, - data: { - usersType: 'tenant', - searchEnabled: true, - pageTitle: 'user.tenant-admins' - }, - ncyBreadcrumb: { - label: '{"icon": "account_circle", "label": "user.tenant-admins"}' - } - }) - .state('home.customers.users', { - url: '/:customerId/users', - params: {'topIndex': 0}, - module: 'private', - auth: ['TENANT_ADMIN'], - views: { - "content@home": { - templateUrl: usersTemplate, - controllerAs: 'vm', - controller: 'UserController' - } - }, - data: { - usersType: 'customer', - searchEnabled: true, - pageTitle: 'user.customer-users' - }, - ncyBreadcrumb: { - label: '{"icon": "account_circle", "label": "user.customer-users"}' - } - }); - -} diff --git a/ui/src/app/user/users.tpl.html b/ui/src/app/user/users.tpl.html deleted file mode 100644 index 9c1d5a4e63..0000000000 --- a/ui/src/app/user/users.tpl.html +++ /dev/null @@ -1,40 +0,0 @@ - - - -
    -
    - - - - - - - - - -
    diff --git a/ui/src/app/widget/add-widgets-bundle.tpl.html b/ui/src/app/widget/add-widgets-bundle.tpl.html deleted file mode 100644 index 9638bedd5a..0000000000 --- a/ui/src/app/widget/add-widgets-bundle.tpl.html +++ /dev/null @@ -1,48 +0,0 @@ - - -
    - -
    -

    widgets-bundle.add

    - -
    - - - -
    -
    - - - -
    - -
    -
    - - - - {{ 'action.add' | translate }} - - {{ 'action.cancel' | - translate }} - - -
    -
    diff --git a/ui/src/app/widget/index.js b/ui/src/app/widget/index.js deleted file mode 100644 index db0ade84fe..0000000000 --- a/ui/src/app/widget/index.js +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './widget-editor.scss'; - -import 'angular-hotkeys'; -import 'angular-ui-ace'; - -import uiRouter from 'angular-ui-router'; -import thingsboardApiUser from '../api/user.service'; -import thingsboardApiWidget from '../api/widget.service'; -import thingsboardTypes from '../common/types.constant'; -import thingsboardToast from '../services/toast'; -import thingsboardConfirmOnExit from '../components/confirm-on-exit.directive'; -import thingsboardDashboard from '../components/dashboard.directive'; -import thingsboardExpandFullscreen from '../components/expand-fullscreen.directive'; -import thingsboardCircularProgress from '../components/circular-progress.directive'; -import thingsboardMdChipDraggable from '../components/md-chip-draggable.directive'; - -import WidgetLibraryRoutes from './widget-library.routes'; -import WidgetLibraryController from './widget-library.controller'; -import SelectWidgetTypeController from './select-widget-type.controller'; -import WidgetEditorController from './widget-editor.controller'; -import WidgetsBundleController from './widgets-bundle.controller'; -import WidgetsBundleDirective from './widgets-bundle.directive'; -import SaveWidgetTypeAsController from './save-widget-type-as.controller'; - -export default angular.module('thingsboard.widget-library', [ - uiRouter, - thingsboardApiWidget, - thingsboardApiUser, - thingsboardTypes, - thingsboardToast, - thingsboardConfirmOnExit, - thingsboardDashboard, - thingsboardExpandFullscreen, - thingsboardCircularProgress, - thingsboardMdChipDraggable, - 'cfp.hotkeys', - 'ui.ace' -]) - .config(WidgetLibraryRoutes) - .controller('WidgetLibraryController', WidgetLibraryController) - .controller('SelectWidgetTypeController', SelectWidgetTypeController) - .controller('WidgetEditorController', WidgetEditorController) - .controller('WidgetsBundleController', WidgetsBundleController) - .controller('SaveWidgetTypeAsController', SaveWidgetTypeAsController) - .directive('tbWidgetsBundle', WidgetsBundleDirective) - .name; diff --git a/ui/src/app/widget/lib/CanvasDigitalGauge.js b/ui/src/app/widget/lib/CanvasDigitalGauge.js deleted file mode 100644 index 8f48292c78..0000000000 --- a/ui/src/app/widget/lib/CanvasDigitalGauge.js +++ /dev/null @@ -1,860 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import $ from 'jquery'; -import canvasGauges from 'canvas-gauges'; -import tinycolor from 'tinycolor2'; - -/* eslint-disable angular/angularelement */ - -let defaultDigitalGaugeOptions = Object.assign({}, canvasGauges.GenericOptions, { - gaugeType: 'arc', - gaugeWithScale: 0.75, - dashThickness: 0, - roundedLineCap: false, - - gaugeColor: '#777', - levelColors: ['blue'], - - symbol: '', - label: '', - hideValue: false, - hideMinMax: false, - - fontTitle: 'Roboto', - - fontValue: 'Roboto', - - fontMinMaxSize: 10, - fontMinMaxStyle: 'normal', - fontMinMaxWeight: '500', - colorMinMax: '#eee', - fontMinMax: 'Roboto', - - fontLabelSize: 8, - fontLabelStyle: 'normal', - fontLabelWeight: '500', - colorLabel: '#eee', - fontLabel: 'Roboto', - - neonGlowBrightness: 0, - - colorTicks: 'gray', - tickWidth: 4, - ticks: [], - - isMobile: false - -}); - -const round = Math.round; - -export default class CanvasDigitalGauge extends canvasGauges.BaseGauge { - - constructor(options) { - canvasGauges.performance = window.performance; // eslint-disable-line no-undef, angular/window-service - options = Object.assign({}, defaultDigitalGaugeOptions, options || {}); - super(CanvasDigitalGauge.configure(options)); - this.initValueClone(); - } - - initValueClone() { - let canvas = this.canvas; - this.elementValueClone = canvas.element.cloneNode(true); - this.contextValueClone = this.elementValueClone.getContext('2d'); - this.elementValueClone.initialized = false; - - this.contextValueClone.translate(canvas.drawX, canvas.drawY); - this.contextValueClone.save(); - - this.elementProgressClone = canvas.element.cloneNode(true); - this.contextProgressClone = this.elementProgressClone.getContext('2d'); - this.elementProgressClone.initialized = false; - - this.contextProgressClone.translate(canvas.drawX, canvas.drawY); - this.contextProgressClone.save(); - - } - - static configure(options) { - - if (options.value > options.maxValue) { - options.value = options.maxValue; - } - - if (options.value < options.minValue) { - options.value = options.minValue; - } - - if (options.gaugeType === 'donut') { - if (!options.donutStartAngle) { - options.donutStartAngle = 1.5 * Math.PI; - } - if (!options.donutEndAngle) { - options.donutEndAngle = options.donutStartAngle + 2 * Math.PI; - } - } - - var colorsCount = options.levelColors.length; - const inc = colorsCount > 1 ? (1 / (colorsCount - 1)) : 1; - var isColorProperty = angular.isString(options.levelColors[0]); - - options.colorsRange = []; - if (options.neonGlowBrightness) { - options.neonColorsRange = []; - } - - for (let i = 0; i < options.levelColors.length; i++) { - const levelColor = options.levelColors[i]; - if (levelColor !== null) { - let percentage = isColorProperty ? inc * i : CanvasDigitalGauge.normalizeValue(levelColor.value, options.minValue, options.maxValue); - let tColor = tinycolor(isColorProperty ? levelColor : levelColor.color); - options.colorsRange.push({ - pct: percentage, - color: tColor.toRgb(), - rgbString: tColor.toRgbString() - }); - if (options.neonGlowBrightness) { - tColor = tinycolor(isColorProperty ? levelColor : levelColor.color).brighten(options.neonGlowBrightness); - options.neonColorsRange.push({ - pct: percentage, - color: tColor.toRgb(), - rgbString: tColor.toRgbString() - }); - } - } - } - - options.ticksValue = []; - for(let i = 0; i < options.ticks.length; i++){ - if(options.ticks[i] !== null){ - options.ticksValue.push(CanvasDigitalGauge.normalizeValue(options.ticks[i], options.minValue, options.maxValue)) - } - } - - if (options.neonGlowBrightness) { - options.neonColorTitle = tinycolor(options.colorTitle).brighten(options.neonGlowBrightness).toHexString(); - options.neonColorLabel = tinycolor(options.colorLabel).brighten(options.neonGlowBrightness).toHexString(); - options.neonColorValue = tinycolor(options.colorValue).brighten(options.neonGlowBrightness).toHexString(); - options.neonColorMinMax = tinycolor(options.colorMinMax).brighten(options.neonGlowBrightness).toHexString(); - } - - return canvasGauges.BaseGauge.configure(options); - } - - static normalizeValue (value, min, max) { - let normalValue = (value - min) / (max - min); - if (normalValue <= 0) { - return 0; - } - if (normalValue >= 1) { - return 1; - } - return normalValue; - } - - destroy() { - this.contextValueClone = null; - this.elementValueClone = null; - this.contextProgressClone = null; - this.elementProgressClone = null; - super.destroy(); - } - - update(options) { - this.canvas.onRedraw = null; - var result = super.update(options); - this.initValueClone(); - this.canvas.onRedraw = this.draw.bind(this); - this.draw(); - return result; - } - - set timestamp(timestamp) { - this.options.timestamp = timestamp; - this.draw(); - } - - get timestamp() { - return this.options.timestamp; - } - - draw() { - try { - - let canvas = this.canvas; - - if (!canvas.drawWidth || !canvas.drawHeight) { - return this; - } - - let [x, y, w, h] = [ - -canvas.drawX, - -canvas.drawY, - canvas.drawWidth, - canvas.drawHeight - ]; - let options = this.options; - if (!canvas.elementClone.initialized) { - let context = canvas.contextClone; - - // clear the cache - context.clearRect(x, y, w, h); - context.save(); - - canvas.context.barDimensions = barDimensions(context, options, x, y, w, h); - this.contextValueClone.barDimensions = canvas.context.barDimensions; - this.contextProgressClone.barDimensions = canvas.context.barDimensions; - - drawBackground(context, options); - - drawDigitalTitle(context, options); - - if (!options.showTimestamp) { - drawDigitalLabel(context, options); - } - - drawDigitalMinMax(context, options); - - canvas.elementClone.initialized = true; - } - - var valueChanged = false; - if (!this.elementValueClone.initialized || angular.isDefined(this._value) && this.elementValueClone.renderedValue !== this._value || (options.showTimestamp && this.elementValueClone.renderedTimestamp !== this.timestamp)) { - if (angular.isDefined(this._value)) { - this.elementValueClone.renderedValue = this._value; - } - if (angular.isUndefined(this.elementValueClone.renderedValue)) { - this.elementValueClone.renderedValue = this.value; - } - let context = this.contextValueClone; - // clear the cache - context.clearRect(x, y, w, h); - context.save(); - - context.drawImage(canvas.elementClone, x, y, w, h); - context.save(); - - drawDigitalValue(context, options, this.elementValueClone.renderedValue); - - if (options.showTimestamp) { - drawDigitalLabel(context, options); - this.elementValueClone.renderedTimestamp = this.timestamp; - } - - this.elementValueClone.initialized = true; - - valueChanged = true; - } - - var progress = (canvasGauges.drawings.normalizedValue(options).normal - options.minValue) / - (options.maxValue - options.minValue); - - var fixedProgress = progress.toFixed(3); - - if (!this.elementProgressClone.initialized || this.elementProgressClone.renderedProgress !== fixedProgress || valueChanged) { - let context = this.contextProgressClone; - // clear the cache - context.clearRect(x, y, w, h); - context.save(); - - context.drawImage(this.elementValueClone, x, y, w, h); - context.save(); - - if (Number(fixedProgress) > 0) { - drawProgress(context, options, progress); - } - - this.elementProgressClone.initialized = true; - this.elementProgressClone.renderedProgress = fixedProgress; - } - - this.canvas.commit(); - - // clear the canvas - canvas.context.clearRect(x, y, w, h); - canvas.context.save(); - - canvas.context.drawImage(this.elementProgressClone, x, y, w, h); - canvas.context.save(); - - super.draw(); - - } catch (err) { - canvasGauges.drawings.verifyError(err); - } - return this; - } - - getValueColor() { - if (this.contextProgressClone) { - var color = this.contextProgressClone.currentColor; - if (!color) { - if (this.options.neonGlowBrightness) { - color = getProgressColor(0, this.options.neonColorsRange); - } else { - color = getProgressColor(0, this.options.colorsRange); - } - } - return color; - } else { - return '#000'; - } - } -} - -/* eslint-disable angular/document-service */ -/* eslint-disable no-undef */ -function determineFontHeight (options, target, baseSize) { - var fontStyleStr = 'font-style:' + options['font' + target + 'Style'] + ';font-weight:' + - options['font' + target + 'Weight'] + ';font-size:' + - options['font' + target + 'Size'] * baseSize + 'px;font-family:' + - options['font' + target]; - var result = CanvasDigitalGauge.heightCache[fontStyleStr]; - if (!result) - { - var fontStyle = { - fontFamily: options['font' + target], - fontSize: options['font' + target + 'Size'] * baseSize + 'px', - fontWeight: options['font' + target + 'Weight'], - fontStyle: options['font' + target + 'Style'] - }; - - var text = $('Hg').css(fontStyle); - var block = $('
    '); - - var div = $('
    '); - div.append(text, block); - - var body = $('body'); - body.append(div); - - try { - result = {}; - block.css({ verticalAlign: 'baseline' }); - result.ascent = block.offset().top - text.offset().top; - block.css({ verticalAlign: 'bottom' }); - result.height = block.offset().top - text.offset().top; - result.descent = result.height - result.ascent; - } finally { - div.remove(); - } - CanvasDigitalGauge.heightCache[fontStyleStr] = result; - } - return result; -} - -/* eslint-enable angular/document-service */ -/* eslint-enable no-undef */ - -function barDimensions(context, options, x, y, w, h) { - - context.barDimensions = { - baseX: x, - baseY: y, - width: w, - height: h - }; - - var bd = context.barDimensions; - - var aspect = 1; - - if (options.gaugeType === 'horizontalBar') { - aspect = options.title === '' ? 2.5 : 2; - } else if (options.gaugeType === 'verticalBar') { - aspect = options.hideMinMax ? 0.35 : 0.5; - } else if (options.gaugeType === 'arc') { - aspect = 1.5; - } - - var currentAspect = w / h; - if (currentAspect > aspect) { - bd.width = (h * aspect); - bd.height = h; - } else { - bd.width = w; - bd.height = w / aspect; - } - - bd.origBaseX = bd.baseX; - bd.origBaseY = bd.baseY; - bd.baseX += (w - bd.width) / 2; - bd.baseY += (h - bd.height) / 2; - - if (options.gaugeType === 'donut') { - bd.fontSizeFactor = Math.max(bd.width, bd.height) / 125; - } else if (options.gaugeType === 'verticalBar' || (options.gaugeType === 'arc' && options.title === '')) { - bd.fontSizeFactor = Math.max(bd.width, bd.height) / 150; - } else { - bd.fontSizeFactor = Math.max(bd.width, bd.height) / 200; - } - - var gws = options.gaugeWidthScale; - - if (options.neonGlowBrightness) { - options.fontTitleHeight = determineFontHeight(options, 'Title', bd.fontSizeFactor); - options.fontLabelHeight = determineFontHeight(options, 'Label', bd.fontSizeFactor); - options.fontValueHeight = determineFontHeight(options, 'Value', bd.fontSizeFactor); - options.fontMinMaxHeight = determineFontHeight(options, 'MinMax', bd.fontSizeFactor); - } - - if (options.gaugeType === 'donut') { - bd.Ro = bd.width / 2 - bd.width / 20; - bd.Cy = bd.baseY + bd.height / 2; - if (options.title && options.title.length > 0) { - var titleOffset = determineFontHeight(options, 'Title', bd.fontSizeFactor).height; - titleOffset += bd.fontSizeFactor * 2; - bd.titleY = bd.baseY + titleOffset; - titleOffset += bd.fontSizeFactor * 2; - bd.Cy += titleOffset/2; - bd.Ro -= titleOffset/2; - } - bd.Ri = bd.Ro - bd.width / 6.666666666666667 * gws * 1.2; - bd.Cx = bd.baseX + bd.width / 2; - - } else if (options.gaugeType === 'arc') { - if (options.title && options.title.length > 0) { - bd.Ro = bd.width / 2 - bd.width / 7; - bd.Ri = bd.Ro - bd.width / 6.666666666666667 * gws; - } else { - bd.Ro = bd.width / 2 - bd.fontSizeFactor * 4; - bd.Ri = bd.Ro - bd.width / 6.666666666666667 * gws * 1.2; - } - bd.Cx = bd.baseX + bd.width / 2; - bd.Cy = bd.baseY + bd.height / 1.25; - } else if (options.gaugeType === 'verticalBar') { - bd.Ro = bd.width / 2 - bd.width / 10; - bd.Ri = bd.Ro - bd.width / 6.666666666666667 * gws * (options.hideMinMax ? 4 : 2.5); - } else { //horizontalBar - bd.Ro = bd.width / 2 - bd.width / 10; - bd.Ri = bd.Ro - bd.width / 6.666666666666667 * gws; - } - - bd.strokeWidth = bd.Ro - bd.Ri; - bd.Rm = bd.Ri + bd.strokeWidth * 0.5; - - bd.fontValueBaseline = 'alphabetic'; - bd.fontMinMaxBaseline = 'alphabetic'; - bd.fontMinMaxAlign = 'center'; - - if (options.gaugeType === 'donut') { - bd.fontValueBaseline = 'middle'; - if (options.label && options.label.length > 0) { - var valueHeight = determineFontHeight(options, 'Value', bd.fontSizeFactor).height; - var labelHeight = determineFontHeight(options, 'Label', bd.fontSizeFactor).height; - var total = valueHeight + labelHeight; - bd.labelY = bd.Cy + total/2; - bd.valueY = bd.Cy - total/2 + valueHeight/2; - } else { - bd.valueY = bd.Cy; - } - } else if (options.gaugeType === 'arc') { - bd.titleY = bd.Cy - bd.Ro - 12 * bd.fontSizeFactor; - bd.valueY = bd.Cy; - bd.labelY = bd.Cy + (8 + options.fontLabelSize) * bd.fontSizeFactor; - bd.minY = bd.maxY = bd.labelY; - if (options.roundedLineCap) { - bd.minY += bd.strokeWidth/2; - bd.maxY += bd.strokeWidth/2; - } - bd.minX = bd.Cx - bd.Rm; - bd.maxX = bd.Cx + bd.Rm; - } else if (options.gaugeType === 'horizontalBar') { - bd.titleY = bd.baseY + 4 * bd.fontSizeFactor + - (options.title === '' ? 0 : options.fontTitleSize * bd.fontSizeFactor); - bd.titleBottom = bd.titleY + (options.title === '' ? 0 : 4) * bd.fontSizeFactor; - - bd.valueY = bd.titleBottom + - (options.hideValue ? 0 : options.fontValueSize * bd.fontSizeFactor); - - bd.barTop = bd.valueY + 8 * bd.fontSizeFactor; - bd.barBottom = bd.barTop + bd.strokeWidth; - - if (options.hideMinMax && options.label === '') { - bd.labelY = bd.barBottom; - bd.barLeft = bd.origBaseX + options.fontMinMaxSize/3 * bd.fontSizeFactor; - bd.barRight = bd.origBaseX + w + /*bd.width*/ - options.fontMinMaxSize/3 * bd.fontSizeFactor; - } else { - context.font = canvasGauges.drawings.font(options, 'MinMax', bd.fontSizeFactor); - var minTextWidth = context.measureText(options.minValue+'').width; - var maxTextWidth = context.measureText(options.maxValue+'').width; - var maxW = Math.max(minTextWidth, maxTextWidth); - bd.minX = bd.origBaseX + maxW/2 + options.fontMinMaxSize/3 * bd.fontSizeFactor; - bd.maxX = bd.origBaseX + w + /*bd.width*/ - maxW/2 - options.fontMinMaxSize/3 * bd.fontSizeFactor; - bd.barLeft = bd.minX; - bd.barRight = bd.maxX; - bd.labelY = bd.barBottom + (8 + options.fontLabelSize) * bd.fontSizeFactor; - bd.minY = bd.maxY = bd.labelY; - } - } else if (options.gaugeType === 'verticalBar') { - bd.titleY = bd.baseY + ((options.title === '' ? 0 : options.fontTitleSize) + 8) * bd.fontSizeFactor; - bd.titleBottom = bd.titleY + (options.title === '' ? 0 : 4) * bd.fontSizeFactor; - - bd.valueY = bd.titleBottom + (options.hideValue ? 0 : options.fontValueSize * bd.fontSizeFactor); - bd.barTop = bd.valueY + 8 * bd.fontSizeFactor; - - bd.labelY = bd.baseY + bd.height - 16; - if (options.label === '') { - bd.barBottom = bd.labelY; - } else { - bd.barBottom = bd.labelY - (8 + options.fontLabelSize) * bd.fontSizeFactor; - } - bd.minX = bd.maxX = - bd.baseX + bd.width/2 + bd.strokeWidth/2 + options.fontMinMaxSize/3 * bd.fontSizeFactor; - bd.minY = bd.barBottom; - bd.maxY = bd.barTop; - bd.fontMinMaxBaseline = 'middle'; - bd.fontMinMaxAlign = 'left'; - } - - if (options.dashThickness) { - var circumference; - if (options.gaugeType === 'donut') { - circumference = Math.PI * bd.Rm * 2; - } else if (options.gaugeType === 'arc') { - circumference = Math.PI * bd.Rm; - } else if (options.gaugeType === 'horizontalBar') { - circumference = bd.barRight - bd.barLeft; - } else if (options.gaugeType === 'verticalBar') { - circumference = bd.barBottom - bd.barTop; - } - var dashCount = Math.floor(circumference / (options.dashThickness * bd.fontSizeFactor)); - if (options.gaugeType === 'donut') { - dashCount = (dashCount | 1) - 1; - } else { - dashCount = (dashCount - 1) | 1; - } - bd.dashLength = Math.ceil(circumference/dashCount); - } - - return bd; -} - -function drawBackground(context, options) { - let {barLeft, barRight, barTop, barBottom, width, baseX, strokeWidth} = - context.barDimensions; - if (context.barDimensions.dashLength) { - context.setLineDash([context.barDimensions.dashLength]); - } - context.beginPath(); - context.strokeStyle = options.gaugeColor; - context.lineWidth = strokeWidth; - if (options.roundedLineCap) { - context.lineCap = 'round'; - } - if (options.gaugeType === 'donut') { - context.arc(context.barDimensions.Cx, context.barDimensions.Cy, context.barDimensions.Rm, options.donutStartAngle, options.donutEndAngle); - context.stroke(); - } else if (options.gaugeType === 'arc') { - context.arc(context.barDimensions.Cx, context.barDimensions.Cy, context.barDimensions.Rm, Math.PI, 2*Math.PI); - context.stroke(); - } else if (options.gaugeType === 'horizontalBar') { - context.moveTo(barLeft,barTop + strokeWidth/2); - context.lineTo(barRight,barTop + strokeWidth/2); - context.stroke(); - } else if (options.gaugeType === 'verticalBar') { - context.moveTo(baseX + width/2, barBottom); - context.lineTo(baseX + width/2, barTop); - context.stroke(); - } -} - -function drawText(context, options, target, text, textX, textY) { - context.fillStyle = options[(options.neonGlowBrightness ? 'neonColor' : 'color') + target]; - context.fillText(text, textX, textY); -} - -function drawDigitalTitle(context, options) { - if (!options.title) return; - - let {titleY, width, baseX, fontSizeFactor} = - context.barDimensions; - - let textX = round(baseX + width / 2); - let textY = titleY; - - context.save(); - context.textAlign = 'center'; - context.font = canvasGauges.drawings.font(options, 'Title', fontSizeFactor); - context.lineWidth = 0; - drawText(context, options, 'Title', options.title.toUpperCase(), textX, textY); -} - -function drawDigitalLabel(context, options) { - if (!options.label || options.label === '') return; - - let {labelY, baseX, width, fontSizeFactor} = - context.barDimensions; - - let textX = round(baseX + width / 2); - let textY = labelY; - - context.save(); - context.textAlign = 'center'; - context.font = canvasGauges.drawings.font(options, 'Label', fontSizeFactor); - context.lineWidth = 0; - drawText(context, options, 'Label', options.label.toUpperCase(), textX, textY); - -} - -function drawDigitalMinMax(context, options) { - if (options.hideMinMax || options.gaugeType === 'donut') return; - - let {minY, maxY, minX, maxX, fontSizeFactor, fontMinMaxAlign, fontMinMaxBaseline} = - context.barDimensions; - - context.save(); - context.textAlign = fontMinMaxAlign; - context.textBaseline = fontMinMaxBaseline; - context.font = canvasGauges.drawings.font(options, 'MinMax', fontSizeFactor); - context.lineWidth = 0; - drawText(context, options, 'MinMax', options.minValue+'', minX, minY); - drawText(context, options, 'MinMax', options.maxValue+'', maxX, maxY); -} - -function padValue(val, options) { - let dec = options.valueDec; - let strVal, n; - - val = parseFloat(val); - n = (val < 0); - val = Math.abs(val); - - if (dec > 0) { - strVal = val.toFixed(dec).toString() - } else { - strVal = round(val).toString(); - } - strVal = (n ? '-' : '') + strVal; - return strVal; -} - -function drawDigitalValue(context, options, value) { - if (options.hideValue) return; - - let {valueY, baseX, width, fontSizeFactor, fontValueBaseline} = - context.barDimensions; - - let textX = round(baseX + width / 2); - let textY = valueY; - - let text = options.valueText || padValue(value, options); - text += options.symbol; - - context.save(); - context.textAlign = 'center'; - context.textBaseline = fontValueBaseline; - context.font = canvasGauges.drawings.font(options, 'Value', fontSizeFactor); - context.lineWidth = 0; - drawText(context, options, 'Value', text, textX, textY); -} - -function getProgressColor(progress, colorsRange) { - - var lower, upper, range, rangePct, pctLower, pctUpper, color; - - if (progress === 0 || colorsRange.length === 1) { - return colorsRange[0].rgbString; - } - - for (var j = 0; j < colorsRange.length; j++) { - if (progress <= colorsRange[j].pct) { - lower = colorsRange[j - 1]; - upper = colorsRange[j]; - range = upper.pct - lower.pct; - rangePct = (progress - lower.pct) / range; - pctLower = 1 - rangePct; - pctUpper = rangePct; - color = tinycolor({ - r: Math.floor(lower.color.r * pctLower + upper.color.r * pctUpper), - g: Math.floor(lower.color.g * pctLower + upper.color.g * pctUpper), - b: Math.floor(lower.color.b * pctLower + upper.color.b * pctUpper) - }); - return color.toRgbString(); - } - } -} - -function drawArcGlow(context, Cx, Cy, Ri, Rm, Ro, color, progress, isDonut, donutStartAngle, donutEndAngle) { - context.setLineDash([]); - var strokeWidth = Ro - Ri; - var blur = 0.55; - var edge = strokeWidth*blur; - context.lineWidth = strokeWidth+edge; - var stop = blur/(2*blur+2); - var glowGradient = context.createRadialGradient(Cx,Cy,Ri-edge/2,Cx,Cy,Ro+edge/2); - var color1 = tinycolor(color).setAlpha(0.5).toRgbString(); - var color2 = tinycolor(color).setAlpha(0).toRgbString(); - glowGradient.addColorStop(0,color2); - glowGradient.addColorStop(stop,color1); - glowGradient.addColorStop(1.0-stop,color1); - glowGradient.addColorStop(1,color2); - context.strokeStyle = glowGradient; - context.beginPath(); - var e = 0.01 * Math.PI; - if (isDonut) { - context.arc(Cx, Cy, Rm, donutStartAngle - e, donutStartAngle + (donutEndAngle - donutStartAngle) * progress + e); - } else { - context.arc(Cx, Cy, Rm, Math.PI - e, Math.PI + Math.PI * progress + e); - } - context.stroke(); -} - -function drawBarGlow(context, startX, startY, endX, endY, color, strokeWidth, isVertical) { - context.setLineDash([]); - var blur = 0.55; - var edge = strokeWidth*blur; - context.lineWidth = strokeWidth+edge; - var stop = blur/(2*blur+2); - var gradientStartX = isVertical ? startX - context.lineWidth/2 : 0; - var gradientStartY = isVertical ? 0 : startY - context.lineWidth/2; - var gradientStopX = isVertical ? startX + context.lineWidth/2 : 0; - var gradientStopY = isVertical ? 0 : startY + context.lineWidth/2; - - var glowGradient = context.createLinearGradient(gradientStartX,gradientStartY,gradientStopX,gradientStopY); - var color1 = tinycolor(color).setAlpha(0.5).toRgbString(); - var color2 = tinycolor(color).setAlpha(0).toRgbString(); - glowGradient.addColorStop(0,color2); - glowGradient.addColorStop(stop,color1); - glowGradient.addColorStop(1.0-stop,color1); - glowGradient.addColorStop(1,color2); - context.strokeStyle = glowGradient; - var dx = isVertical ? 0 : 0.05 * context.lineWidth; - var dy = isVertical ? 0.05 * context.lineWidth : 0; - context.beginPath(); - context.moveTo(startX - dx, startY + dy); - context.lineTo(endX + dx, endY - dy); - context.stroke(); -} - -function drawTickArc(context, tickValues, Cx, Cy, Ri, Rm, Ro, startAngle, endAngle, color, tickWidth) { - if(!tickValues.length) { - return; - } - - const strokeWidth = Ro - Ri; - context.beginPath(); - context.lineWidth = tickWidth; - context.strokeStyle = color; - for (let i = 0; i < tickValues.length; i++) { - var angle = startAngle + tickValues[i] * endAngle; - var x1 = Cx + (Ri + strokeWidth) * Math.cos(angle); - var y1 = Cy + (Ri + strokeWidth) * Math.sin(angle); - var x2 = Cx + Ri * Math.cos(angle); - var y2 = Cy + Ri * Math.sin(angle); - context.moveTo(x1, y1); - context.lineTo(x2, y2); - } - context.stroke(); -} - -function drawTickBar(context, tickValues, startX, startY, distanceBar, strokeWidth, isVertical, color, tickWidth) { - if(!tickValues.length) { - return; - } - - context.beginPath(); - context.lineWidth = tickWidth; - context.strokeStyle = color; - for (let i = 0; i < tickValues.length; i++) { - let tickValue = tickValues[i] * distanceBar; - if (isVertical) { - context.moveTo(startX - strokeWidth / 2, startY + tickValue - distanceBar); - context.lineTo(startX + strokeWidth / 2, startY + tickValue - distanceBar); - } else { - context.moveTo(startX + tickValue, startY); - context.lineTo(startX + tickValue, startY + strokeWidth); - } - } - context.stroke(); -} - -function drawProgress(context, options, progress) { - var neonColor; - if (options.neonGlowBrightness) { - context.currentColor = neonColor = getProgressColor(progress, options.neonColorsRange); - } else { - context.currentColor = context.strokeStyle = getProgressColor(progress, options.colorsRange); - } - - let {barLeft, barRight, barTop, baseX, width, barBottom, Cx, Cy, Rm, Ro, Ri, strokeWidth} = - context.barDimensions; - - if (context.barDimensions.dashLength) { - context.setLineDash([context.barDimensions.dashLength]); - } - context.lineWidth = strokeWidth; - if (options.roundedLineCap) { - context.lineCap = 'round'; - } else { - context.lineCap = 'butt'; - } - if (options.gaugeType === 'donut') { - if (options.neonGlowBrightness) { - context.strokeStyle = neonColor; - } - context.beginPath(); - context.arc(Cx, Cy, Rm, options.donutStartAngle, options.donutStartAngle + (options.donutEndAngle - options.donutStartAngle) * progress); - context.stroke(); - if (options.neonGlowBrightness && !options.isMobile) { - drawArcGlow(context, Cx, Cy, Ri, Rm, Ro, neonColor, progress, true, options.donutStartAngle, options.donutEndAngle); - } - drawTickArc(context, options.ticksValue, Cx, Cy, Ri, Rm, Ro, options.donutStartAngle, options.donutEndAngle - options.donutStartAngle, options.colorTicks, options.tickWidth); - } else if (options.gaugeType === 'arc') { - if (options.neonGlowBrightness) { - context.strokeStyle = neonColor; - } - context.beginPath(); - context.arc(Cx, Cy, Rm, Math.PI, Math.PI + Math.PI * progress); - context.stroke(); - if (options.neonGlowBrightness && !options.isMobile) { - drawArcGlow(context, Cx, Cy, Ri, Rm, Ro, neonColor, progress, false); - } - drawTickArc(context, options.ticksValue, Cx, Cy, Ri, Rm, Ro, Math.PI, Math.PI, options.colorTicks, options.tickWidth); - } else if (options.gaugeType === 'horizontalBar') { - if (options.neonGlowBrightness) { - context.strokeStyle = neonColor; - } - context.beginPath(); - context.moveTo(barLeft,barTop + strokeWidth/2); - context.lineTo(barLeft + (barRight-barLeft)*progress, barTop + strokeWidth/2); - context.stroke(); - if (options.neonGlowBrightness && !options.isMobile) { - drawBarGlow(context, barLeft, barTop + strokeWidth/2, barLeft + (barRight-barLeft)*progress, barTop + strokeWidth/2, - neonColor, strokeWidth, false); - } - drawTickBar(context, options.ticksValue, barLeft, barTop, barRight - barLeft, strokeWidth, false, options.colorTicks, options.tickWidth); - } else if (options.gaugeType === 'verticalBar') { - if (options.neonGlowBrightness) { - context.strokeStyle = neonColor; - } - context.beginPath(); - context.moveTo(baseX + width/2, barBottom); - context.lineTo(baseX + width/2, barBottom - (barBottom-barTop)*progress); - context.stroke(); - if (options.neonGlowBrightness && !options.isMobile) { - drawBarGlow(context, baseX + width/2, barBottom, baseX + width/2, barBottom - (barBottom-barTop)*progress, - neonColor, strokeWidth, true); - } - drawTickBar(context, options.ticksValue, baseX + width / 2, barTop, barTop - barBottom, strokeWidth, true, options.colorTicks, options.tickWidth); - } -} - -CanvasDigitalGauge.heightCache = []; - -canvasGauges.BaseGauge.initialize('CanvasDigitalGauge', defaultDigitalGaugeOptions); - -/* eslint-enable angular/angularelement */ diff --git a/ui/src/app/widget/lib/add-entity-panel.scss b/ui/src/app/widget/lib/add-entity-panel.scss deleted file mode 100644 index b7573f7c1b..0000000000 --- a/ui/src/app/widget/lib/add-entity-panel.scss +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-add-entity-panel { - min-width: 150px; - max-height: 200px; - overflow: hidden; - overflow-y: auto; - background: #fff; - border-radius: 4px; - box-shadow: - 0 7px 8px -4px rgba(0, 0, 0, .2), - 0 13px 19px 2px rgba(0, 0, 0, .14), - 0 5px 24px 4px rgba(0, 0, 0, .12); -} diff --git a/ui/src/app/widget/lib/add-entity-panel.tpl.html b/ui/src/app/widget/lib/add-entity-panel.tpl.html deleted file mode 100644 index bc6b99e9b0..0000000000 --- a/ui/src/app/widget/lib/add-entity-panel.tpl.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - {{ entity.entityLabel || entity.name }} - - diff --git a/ui/src/app/widget/lib/alarm-status-filter-panel.scss b/ui/src/app/widget/lib/alarm-status-filter-panel.scss deleted file mode 100644 index bfc0b58387..0000000000 --- a/ui/src/app/widget/lib/alarm-status-filter-panel.scss +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-alarm-status-filter-panel { - min-width: 300px; - overflow: hidden; - background: #fff; - border-radius: 4px; - box-shadow: - 0 7px 8px -4px rgba(0, 0, 0, .2), - 0 13px 19px 2px rgba(0, 0, 0, .14), - 0 5px 24px 4px rgba(0, 0, 0, .12); - - md-content { - overflow: hidden; - background-color: #fff; - } -} diff --git a/ui/src/app/widget/lib/alarm-status-filter-panel.tpl.html b/ui/src/app/widget/lib/alarm-status-filter-panel.tpl.html deleted file mode 100644 index 5790ac03e9..0000000000 --- a/ui/src/app/widget/lib/alarm-status-filter-panel.tpl.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - {{ ('alarm.search-status.' + searchStatus) | translate }} - - - diff --git a/ui/src/app/widget/lib/alarms-table-widget.js b/ui/src/app/widget/lib/alarms-table-widget.js deleted file mode 100644 index 53dd43f223..0000000000 --- a/ui/src/app/widget/lib/alarms-table-widget.js +++ /dev/null @@ -1,806 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './alarms-table-widget.scss'; -import './display-columns-panel.scss'; -import './alarm-status-filter-panel.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import alarmsTableWidgetTemplate from './alarms-table-widget.tpl.html'; -import alarmDetailsDialogTemplate from '../../alarm/alarm-details-dialog.tpl.html'; -import displayColumnsPanelTemplate from './display-columns-panel.tpl.html'; -import alarmStatusFilterPanelTemplate from './alarm-status-filter-panel.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -import tinycolor from 'tinycolor2'; -import cssjs from '../../../vendor/css.js/css'; - -export default angular.module('thingsboard.widgets.alarmsTableWidget', []) - .directive('tbAlarmsTableWidget', AlarmsTableWidget) - .name; - -/*@ngInject*/ -function AlarmsTableWidget() { - return { - restrict: "E", - scope: true, - bindToController: { - tableId: '=', - ctx: '=' - }, - controller: AlarmsTableWidgetController, - controllerAs: 'vm', - templateUrl: alarmsTableWidgetTemplate - }; -} - -/*@ngInject*/ -function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDialog, $mdPanel, $document, $translate, $q, $timeout, alarmService, utils, types) { - var vm = this; - - vm.stylesInfo = {}; - vm.contentsInfo = {}; - vm.columnWidth = {}; - - vm.showData = true; - vm.hasData = false; - - vm.alarms = []; - vm.alarmsCount = 0; - vm.selectedAlarms = [] - - vm.alarmSource = null; - vm.alarmSearchStatus = null; - vm.allAlarms = []; - - vm.currentAlarm = null; - - vm.enableSelection = true; - vm.displayDetails = true; - vm.allowAcknowledgment = true; - vm.allowClear = true; - vm.actionCellDescriptors = []; - vm.displayPagination = true; - vm.defaultPageSize = 10; - vm.defaultSortOrder = '-'+types.alarmFields.createdTime.value; - - vm.query = { - order: vm.defaultSortOrder, - limit: vm.defaultPageSize, - page: 1, - search: null - }; - - vm.searchAction = { - name: 'action.search', - show: true, - onAction: function() { - vm.enterFilterMode(); - }, - icon: 'search' - }; - - let columnDisplayAction = { - name: 'entity.columns-to-display', - show: true, - onAction: function($event) { - vm.editColumnsToDisplay($event); - }, - icon: 'view_column' - }; - - let statusFilterAction = { - name: 'alarm.alarm-status-filter', - show: true, - onAction: function($event) { - vm.editAlarmStatusFilter($event); - }, - icon: 'filter_list' - }; - - vm.enterFilterMode = enterFilterMode; - vm.exitFilterMode = exitFilterMode; - vm.onReorder = onReorder; - vm.onPaginate = onPaginate; - vm.onRowClick = onRowClick; - vm.onActionButtonClick = onActionButtonClick; - vm.actionEnabled = actionEnabled; - vm.isCurrent = isCurrent; - vm.openAlarmDetails = openAlarmDetails; - vm.ackAlarms = ackAlarms; - vm.ackAlarm = ackAlarm; - vm.clearAlarms = clearAlarms; - vm.clearAlarm = clearAlarm; - - vm.cellStyle = cellStyle; - vm.cellContent = cellContent; - - vm.editAlarmStatusFilter = editAlarmStatusFilter; - vm.editColumnsToDisplay = editColumnsToDisplay; - - $scope.$watch('vm.ctx', function() { - if (vm.ctx) { - vm.settings = vm.ctx.settings; - vm.widgetConfig = vm.ctx.widgetConfig; - vm.subscription = vm.ctx.defaultSubscription; - vm.alarmSource = vm.subscription.alarmSource; - initializeConfig(); - updateAlarmSource(); - } - }); - - $scope.$watch("vm.query.search", function(newVal, prevVal) { - if (!angular.equals(newVal, prevVal) && vm.query.search != null) { - updateAlarms(); - } - }); - - $scope.$on('alarms-table-data-updated', function(event, tableId) { - if (vm.tableId == tableId) { - if (vm.subscription) { - vm.allAlarms = vm.subscription.alarms || []; - updateAlarms(true); - $scope.$digest(); - } - } - }); - - $scope.$watch(function() { return $mdMedia('gt-xs'); }, function(isGtXs) { - vm.isGtXs = isGtXs; - }); - - $scope.$watch(function() { return $mdMedia('gt-md'); }, function(isGtMd) { - vm.isGtMd = isGtMd; - if (vm.isGtMd) { - vm.limitOptions = [vm.defaultPageSize, vm.defaultPageSize*2, vm.defaultPageSize*3]; - } else { - vm.limitOptions = null; - } - }); - - $scope.$watch('vm.selectedAlarms.length', function (newLength) { - var selectionMode = newLength ? true : false; - if (vm.ctx) { - if (selectionMode) { - vm.ctx.hideTitlePanel = true; - } else if (vm.query.search == null) { - vm.ctx.hideTitlePanel = false; - } - } - }); - - function initializeConfig() { - - vm.ctx.widgetActions = [ vm.searchAction, statusFilterAction, columnDisplayAction ]; - - vm.displayDetails = angular.isDefined(vm.settings.displayDetails) ? vm.settings.displayDetails : true; - vm.allowAcknowledgment = angular.isDefined(vm.settings.allowAcknowledgment) ? vm.settings.allowAcknowledgment : true; - vm.allowClear = angular.isDefined(vm.settings.allowClear) ? vm.settings.allowClear : true; - - if (vm.displayDetails) { - vm.actionCellDescriptors.push( - { - displayName: $translate.instant('alarm.details'), - icon: 'more_horiz', - details: true - } - ); - } - - if (vm.allowAcknowledgment) { - vm.actionCellDescriptors.push( - { - displayName: $translate.instant('alarm.acknowledge'), - icon: 'done', - acknowledge: true - } - ); - } - - if (vm.allowClear) { - vm.actionCellDescriptors.push( - { - displayName: $translate.instant('alarm.clear'), - icon: 'clear', - clear: true - } - ); - } - - vm.actionCellDescriptors = vm.actionCellDescriptors.concat(vm.ctx.actionsApi.getActionDescriptors('actionCellButton')); - - if (vm.settings.alarmsTitle && vm.settings.alarmsTitle.length) { - vm.alarmsTitle = utils.customTranslation(vm.settings.alarmsTitle, vm.settings.alarmsTitle); - } else { - vm.alarmsTitle = $translate.instant('alarm.alarms'); - } - - vm.ctx.widgetTitle = vm.alarmsTitle; - - vm.enableSelection = angular.isDefined(vm.settings.enableSelection) ? vm.settings.enableSelection : true; - vm.searchAction.show = angular.isDefined(vm.settings.enableSearch) ? vm.settings.enableSearch : true; - columnDisplayAction.show = angular.isDefined(vm.settings.enableSelectColumnDisplay) ? vm.settings.enableSelectColumnDisplay : true; - statusFilterAction.show = angular.isDefined(vm.settings.enableStatusFilter) ? vm.settings.enableStatusFilter : true; - if (!vm.allowAcknowledgment && !vm.allowClear) { - vm.enableSelection = false; - } - - vm.displayPagination = angular.isDefined(vm.settings.displayPagination) ? vm.settings.displayPagination : true; - - var pageSize = vm.settings.defaultPageSize; - if (angular.isDefined(pageSize) && angular.isNumber(pageSize) && pageSize > 0) { - vm.defaultPageSize = pageSize; - } - - if (vm.settings.defaultSortOrder && vm.settings.defaultSortOrder.length) { - vm.defaultSortOrder = vm.settings.defaultSortOrder; - } - - vm.query.order = vm.defaultSortOrder; - vm.query.limit = vm.defaultPageSize; - if (vm.isGtMd) { - vm.limitOptions = [vm.defaultPageSize, vm.defaultPageSize*2, vm.defaultPageSize*3]; - } else { - vm.limitOptions = null; - } - - var origColor = vm.widgetConfig.color || 'rgba(0, 0, 0, 0.87)'; - var defaultColor = tinycolor(origColor); - var mdDark = defaultColor.setAlpha(0.87).toRgbString(); - var mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString(); - var mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString(); - //var mdDarkIcon = mdDarkSecondary; - var mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString(); - - var cssString = 'table.md-table th.md-column {\n'+ - 'color: ' + mdDarkSecondary + ';\n'+ - '}\n'+ - 'table.md-table th.md-column.md-checkbox-column md-checkbox:not(.md-checked) .md-icon {\n'+ - 'border-color: ' + mdDarkSecondary + ';\n'+ - '}\n'+ - 'table.md-table th.md-column md-icon.md-sort-icon {\n'+ - 'color: ' + mdDarkDisabled + ';\n'+ - '}\n'+ - 'table.md-table th.md-column.md-active, table.md-table th.md-column.md-active md-icon {\n'+ - 'color: ' + mdDark + ';\n'+ - '}\n'+ - 'table.md-table td.md-cell {\n'+ - 'color: ' + mdDark + ';\n'+ - 'border-top: 1px '+mdDarkDivider+' solid;\n'+ - '}\n'+ - 'table.md-table td.md-cell.md-checkbox-cell md-checkbox:not(.md-checked) .md-icon {\n'+ - 'border-color: ' + mdDarkSecondary + ';\n'+ - '}\n'+ - 'table.md-table td.md-cell.tb-action-cell button.md-icon-button md-icon {\n'+ - 'color: ' + mdDarkSecondary + ';\n'+ - '}\n'+ - 'table.md-table td.md-cell.md-placeholder {\n'+ - 'color: ' + mdDarkDisabled + ';\n'+ - '}\n'+ - 'table.md-table td.md-cell md-select > .md-select-value > span.md-select-icon {\n'+ - 'color: ' + mdDarkSecondary + ';\n'+ - '}\n'+ - '.md-table-pagination {\n'+ - 'color: ' + mdDarkSecondary + ';\n'+ - 'border-top: 1px '+mdDarkDivider+' solid;\n'+ - '}\n'+ - '.md-table-pagination .buttons md-icon {\n'+ - 'color: ' + mdDarkSecondary + ';\n'+ - '}\n'+ - '.md-table-pagination md-select:not([disabled]):focus .md-select-value {\n'+ - 'color: ' + mdDarkSecondary + ';\n'+ - '}'; - - var cssParser = new cssjs(); - cssParser.testMode = false; - var namespace = 'ts-table-' + hashCode(cssString); - cssParser.cssPreviewNamespace = namespace; - cssParser.createStyleElement(namespace, cssString); - $element.addClass(namespace); - - function hashCode(str) { - var hash = 0; - var i, char; - if (str.length === 0) return hash; - for (i = 0; i < str.length; i++) { - char = str.charCodeAt(i); - hash = ((hash << 5) - hash) + char; - hash = hash & hash; - } - return hash; - } - } - - function enterFilterMode () { - vm.query.search = ''; - vm.ctx.hideTitlePanel = true; - $timeout(()=>{ - angular.element(vm.ctx.$container).find('.searchInput').focus(); - }) - } - - function exitFilterMode () { - vm.query.search = null; - updateAlarms(); - vm.ctx.hideTitlePanel = false; - } - - function onReorder () { - updateAlarms(); - } - - function onPaginate () { - updateAlarms(); - } - - function onRowClick($event, alarm) { - if ($event) { - $event.stopPropagation(); - } - if (vm.currentAlarm != alarm) { - vm.currentAlarm = alarm; - } - var descriptors = vm.ctx.actionsApi.getActionDescriptors('rowClick'); - if (descriptors.length) { - var entityId; - var entityName; - if (vm.currentAlarm && vm.currentAlarm.originator) { - entityId = vm.currentAlarm.originator; - entityName = vm.currentAlarm.originatorName; - } - vm.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName, { alarm: alarm }); - } - } - - function onActionButtonClick($event, alarm, actionDescriptor) { - if (actionDescriptor.details) { - vm.openAlarmDetails($event, alarm); - } else if (actionDescriptor.acknowledge) { - vm.ackAlarm($event, alarm); - } else if (actionDescriptor.clear) { - vm.clearAlarm($event, alarm); - } else { - if ($event) { - $event.stopPropagation(); - } - var entityId; - var entityName; - if (alarm && alarm.originator) { - entityId = alarm.originator; - entityName = alarm.originatorName; - } - vm.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName, {alarm: alarm}); - } - } - - function actionEnabled(alarm, actionDescriptor) { - if (actionDescriptor.acknowledge) { - return (alarm.status == types.alarmStatus.activeUnack || - alarm.status == types.alarmStatus.clearedUnack); - } else if (actionDescriptor.clear) { - return (alarm.status == types.alarmStatus.activeAck || - alarm.status == types.alarmStatus.activeUnack); - } - return true; - } - - function isCurrent(alarm) { - return (vm.currentAlarm && alarm && vm.currentAlarm.id && alarm.id) && - (vm.currentAlarm.id.id === alarm.id.id); - } - - function openAlarmDetails($event, alarm) { - if ($event) { - $event.stopPropagation(); - } - if (alarm && alarm.id) { - var onShowingCallback = { - onShowing: function(){} - } - $mdDialog.show({ - controller: 'AlarmDetailsDialogController', - controllerAs: 'vm', - templateUrl: alarmDetailsDialogTemplate, - locals: { - alarmId: alarm.id.id, - allowAcknowledgment: vm.allowAcknowledgment, - allowClear: vm.allowClear, - displayDetails: true, - showingCallback: onShowingCallback - }, - parent: angular.element($document[0].body), - targetEvent: $event, - fullscreen: true, - multiple: true, - onShowing: function(scope, element) { - onShowingCallback.onShowing(scope, element); - } - }).then(function (alarm) { - if (alarm) { - vm.subscription.update(); - } - }); - - } - } - - function ackAlarms($event) { - if ($event) { - $event.stopPropagation(); - } - if (vm.selectedAlarms && vm.selectedAlarms.length > 0) { - var title = $translate.instant('alarm.aknowledge-alarms-title', {count: vm.selectedAlarms.length}, 'messageformat'); - var content = $translate.instant('alarm.aknowledge-alarms-text', {count: vm.selectedAlarms.length}, 'messageformat'); - var confirm = $mdDialog.confirm() - .targetEvent($event) - .title(title) - .htmlContent(content) - .ariaLabel(title) - .cancel($translate.instant('action.no')) - .ok($translate.instant('action.yes')); - $mdDialog.show(confirm).then(function () { - var tasks = []; - for (var i=0;i 0) { - var title = $translate.instant('alarm.clear-alarms-title', {count: vm.selectedAlarms.length}, 'messageformat'); - var content = $translate.instant('alarm.clear-alarms-text', {count: vm.selectedAlarms.length}, 'messageformat'); - var confirm = $mdDialog.confirm() - .targetEvent($event) - .title(title) - .htmlContent(content) - .ariaLabel(title) - .cancel($translate.instant('action.no')) - .ok($translate.instant('action.yes')); - $mdDialog.show(confirm).then(function () { - var tasks = []; - for (var i=0;i ( - path.split('.').reduce((acc, part) => acc && acc[part], obj) - ); - - function getAlarmValue(alarm, key) { - var alarmField = types.alarmFields[key.name]; - if (alarmField) { - return getDescendantProp(alarm, alarmField.value); - } else { - return getDescendantProp(alarm, key.name); - } - } - - function editAlarmStatusFilter($event) { - var element = angular.element($event.target); - var position = $mdPanel.newPanelPosition() - .relativeTo(element) - .addPanelPosition($mdPanel.xPosition.ALIGN_END, $mdPanel.yPosition.BELOW); - var config = { - attachTo: angular.element($document[0].body), - controller: AlarmStatusFilterPanelController, - controllerAs: 'vm', - templateUrl: alarmStatusFilterPanelTemplate, - panelClass: 'tb-alarm-status-filter-panel', - position: position, - fullscreen: false, - locals: { - 'subscription': vm.subscription - }, - openFrom: $event, - clickOutsideToClose: true, - escapeToClose: true, - focusOnOpen: false - }; - $mdPanel.open(config); - } - - function editColumnsToDisplay($event) { - var element = angular.element($event.target); - var position = $mdPanel.newPanelPosition() - .relativeTo(element) - .addPanelPosition($mdPanel.xPosition.ALIGN_END, $mdPanel.yPosition.BELOW); - var config = { - attachTo: angular.element($document[0].body), - controller: DisplayColumnsPanelController, - controllerAs: 'vm', - templateUrl: displayColumnsPanelTemplate, - panelClass: 'tb-display-columns-panel', - position: position, - fullscreen: false, - locals: { - 'columns': vm.alarmSource.dataKeys - }, - openFrom: $event, - clickOutsideToClose: true, - escapeToClose: true, - focusOnOpen: false - }; - $mdPanel.open(config); - } - - function updateAlarmSource() { - - vm.ctx.widgetTitle = utils.createLabelFromDatasource(vm.alarmSource, vm.alarmsTitle); - - vm.stylesInfo = {}; - vm.contentsInfo = {}; - vm.columnWidth = {}; - - for (var d = 0; d < vm.alarmSource.dataKeys.length; d++ ) { - var dataKey = vm.alarmSource.dataKeys[d]; - - dataKey.title = utils.customTranslation(dataKey.label, dataKey.label); - dataKey.display = true; - - var keySettings = dataKey.settings; - - var cellStyleFunction = null; - var useCellStyleFunction = false; - - if (keySettings.useCellStyleFunction === true) { - if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) { - try { - cellStyleFunction = new Function('value', keySettings.cellStyleFunction); - useCellStyleFunction = true; - } catch (e) { - cellStyleFunction = null; - useCellStyleFunction = false; - } - } - } - - vm.stylesInfo[dataKey.label] = { - useCellStyleFunction: useCellStyleFunction, - cellStyleFunction: cellStyleFunction - }; - - var cellContentFunction = null; - var useCellContentFunction = false; - - if (keySettings.useCellContentFunction === true) { - if (angular.isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) { - try { - cellContentFunction = new Function('value, alarm, filter', keySettings.cellContentFunction); - useCellContentFunction = true; - } catch (e) { - cellContentFunction = null; - useCellContentFunction = false; - } - } - } - - vm.contentsInfo[dataKey.label] = { - useCellContentFunction: useCellContentFunction, - cellContentFunction: cellContentFunction - }; - - var columnWidth = angular.isDefined(keySettings.columnWidth) ? keySettings.columnWidth : '0px'; - vm.columnWidth[dataKey.label] = columnWidth; - } - } - -} - -/*@ngInject*/ -function DisplayColumnsPanelController(columns) { //eslint-disable-line - - var vm = this; - vm.columns = columns; -} - -/*@ngInject*/ -function AlarmStatusFilterPanelController(subscription, types) { //eslint-disable-line - - var vm = this; - vm.types = types; - vm.subscription = subscription; -} diff --git a/ui/src/app/widget/lib/alarms-table-widget.scss b/ui/src/app/widget/lib/alarms-table-widget.scss deleted file mode 100644 index 427323890e..0000000000 --- a/ui/src/app/widget/lib/alarms-table-widget.scss +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-has-timewindow { - .tb-alarms-table { - md-toolbar { - min-height: 60px; - max-height: 60px; - - &.md-table-toolbar { - .md-toolbar-tools { - max-height: 60px; - } - } - } - } -} - -.tb-alarms-table { - md-toolbar { - min-height: 39px; - max-height: 39px; - - &.md-table-toolbar { - .md-toolbar-tools { - max-height: 39px; - } - } - } - - &.tb-data-table { - table.md-table, - table.md-table.md-row-select { - tbody { - .tb-action-cell { - width: 36px; - min-width: 36px; - max-width: 36px; - - .md-button[disabled] { - md-icon { - color: rgba(0, 0, 0, .38); - } - } - } - } - } - } -} diff --git a/ui/src/app/widget/lib/alarms-table-widget.tpl.html b/ui/src/app/widget/lib/alarms-table-widget.tpl.html deleted file mode 100644 index 16fdd01a63..0000000000 --- a/ui/src/app/widget/lib/alarms-table-widget.tpl.html +++ /dev/null @@ -1,108 +0,0 @@ - -
    -
    - -
    - - search - - {{'alarm.search' | translate}} - - - - - - - - close - - {{ 'action.close' | translate }} - - -
    -
    - -
    - - - - done - - {{ 'alarm.acknowledge' | translate }} - - - - clear - - {{ 'alarm.clear' | translate }} - - -
    -
    - - - - - - - - - - - - - - -
    {{ key.title }}
    - - - {{actionDescriptor.icon}} - - {{ actionDescriptor.displayName }} - - -
    - - alarm.no-alarms-prompt -
    - - -
    - alarm.no-data -
    diff --git a/ui/src/app/widget/lib/analogue-compass.js b/ui/src/app/widget/lib/analogue-compass.js deleted file mode 100644 index f7cf4eb875..0000000000 --- a/ui/src/app/widget/lib/analogue-compass.js +++ /dev/null @@ -1,444 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import $ from 'jquery'; -import canvasGauges from 'canvas-gauges'; -/*import tinycolor from 'tinycolor2';*/ - -/* eslint-disable angular/angularelement */ - -export default class TbAnalogueCompass { - constructor(ctx, canvasId) { - this.ctx = ctx; - - canvasGauges.performance = window.performance; // eslint-disable-line no-undef, angular/window-service - - var gaugeElement = $('#'+canvasId, ctx.$container); - - var settings = ctx.settings; - var majorTicks = (settings.majorTicks && settings.majorTicks.length > 0) ? angular.copy(settings.majorTicks) : ["N","NE","E","SE","S","SW","W","NW"]; - majorTicks.push(majorTicks[0]); - - function getFontFamily(fontSettings) { - var family = fontSettings && fontSettings.family ? fontSettings.family : 'Roboto'; - if (family === 'RobotoDraft') { - family = 'Roboto'; - } - return family; - } - - var gaugeData = { - - renderTo: gaugeElement[0], - - // Generic options - minValue: 0, - maxValue: 360, - majorTicks: majorTicks, - minorTicks: settings.minorTicks || 22, - ticksAngle: 360, - startAngle: 180, - strokeTicks: settings.showStrokeTicks || false, - highlights: false, - valueBox: false, - - //needle - needleCircleSize: settings.needleCircleSize || 15, - needleType: 'line', - needleStart: 75, - needleEnd: 99, - needleWidth: 3, - needleCircleOuter: false, - - //borders - borders: settings.showBorder || false, - borderInnerWidth: 0, - borderMiddleWidth: 0, - borderOuterWidth: settings.borderOuterWidth || 10, - borderShadowWidth: 0, - - //colors - colorPlate: settings.colorPlate || '#222', - colorMajorTicks: settings.colorMajorTicks || '#f5f5f5', - colorMinorTicks: settings.colorMinorTicks || '#ddd', - colorNeedle: settings.colorNeedle || '#f08080', - colorNeedleEnd: settings.colorNeedle || '#f08080', - colorNeedleCircleInner: settings.colorNeedleCircle || '#e8e8e8', - colorNeedleCircleInnerEnd: settings.colorNeedleCircle || '#e8e8e8', - colorBorderOuter: settings.colorBorder || '#ccc', - colorBorderOuterEnd: settings.colorBorder || '#ccc', - colorNeedleShadowDown: "#222", - - //fonts - fontNumbers: getFontFamily(settings.majorTickFont), - fontNumbersSize: settings.majorTickFont && settings.majorTickFont.size ? settings.majorTickFont.size : 20, - fontNumbersStyle: settings.majorTickFont && settings.majorTickFont.style ? settings.majorTickFont.style : 'normal', - fontNumbersWeight: settings.majorTickFont && settings.majorTickFont.weight ? settings.majorTickFont.weight : '500', - colorNumbers: settings.majorTickFont && settings.majorTickFont.color ? settings.majorTickFont.color : '#ccc', - - //animations - animation: settings.animation !== false && !ctx.isMobile, - animationDuration: (angular.isDefined(settings.animationDuration) && settings.animationDuration !== null) ? settings.animationDuration : 500, - animationRule: settings.animationRule || 'cycle', - animationTarget: settings.animationTarget || 'needle' - }; - this.gauge = new canvasGauges.RadialGauge(gaugeData).draw(); - } - - update() { - if (this.ctx.data.length > 0) { - var cellData = this.ctx.data[0]; - if (cellData.data.length > 0) { - var tvPair = cellData.data[cellData.data.length - - 1]; - var value = tvPair[1]; - if(value !== this.gauge.value) { - this.gauge.value = value; - } - } - } - } - - mobileModeChanged() { - var animation = this.ctx.settings.animation !== false && !this.ctx.isMobile; - this.gauge.update({animation: animation}); - } - - resize() { - this.gauge.update({width: this.ctx.width, height: this.ctx.height}); - } - - static get settingsSchema() { - return { - "schema": { - "type": "object", - "title": "Settings", - "properties": { - "majorTicks": { - "title": "Major ticks names", - "type": "array", - "items": { - "title": "Tick name", - "type": "string" - } - }, - "minorTicks": { - "title": "Minor ticks count", - "type": "number", - "default": 22 - }, - "showStrokeTicks": { - "title": "Show ticks stroke", - "type": "boolean", - "default": false - }, - "needleCircleSize": { - "title": "Needle circle size", - "type": "number", - "default": 15 - }, - "showBorder": { - "title": "Show border", - "type": "boolean", - "default": true - }, - "borderOuterWidth": { - "title": "Border width", - "type": "number", - "default": 10 - }, - "colorPlate": { - "title": "Plate color", - "type": "string", - "default": "#222" - }, - "colorMajorTicks": { - "title": "Major ticks color", - "type": "string", - "default": "#f5f5f5" - }, - "colorMinorTicks": { - "title": "Minor ticks color", - "type": "string", - "default": "#ddd" - }, - "colorNeedle": { - "title": "Needle color", - "type": "string", - "default": "#f08080" - }, - "colorNeedleCircle": { - "title": "Needle circle color", - "type": "string", - "default": "#e8e8e8" - }, - "colorBorder": { - "title": "Border color", - "type": "string", - "default": "#ccc" - }, - "majorTickFont": { - "title": "Major tick font", - "type": "object", - "properties": { - "family": { - "title": "Font family", - "type": "string", - "default": "Roboto" - }, - "size": { - "title": "Size", - "type": "number", - "default": 20 - }, - "style": { - "title": "Style", - "type": "string", - "default": "normal" - }, - "weight": { - "title": "Weight", - "type": "string", - "default": "500" - }, - "color": { - "title": "color", - "type": "string", - "default": "#ccc" - } - } - }, - "animation": { - "title": "Enable animation", - "type": "boolean", - "default": true - }, - "animationDuration": { - "title": "Animation duration", - "type": "number", - "default": 500 - }, - "animationRule": { - "title": "Animation rule", - "type": "string", - "default": "cycle" - }, - "animationTarget": { - "title": "Animation target", - "type": "string", - "default": "needle" - } - }, - "required": [] - }, - "form": [ - { - "key": "majorTicks", - "items":[ - "majorTicks[]" - ] - }, - "minorTicks", - "showStrokeTicks", - "needleCircleSize", - "showBorder", - "borderOuterWidth", - { - "key": "colorPlate", - "type": "color" - }, - { - "key": "colorMajorTicks", - "type": "color" - }, - { - "key": "colorMinorTicks", - "type": "color" - }, - { - "key": "colorNeedle", - "type": "color" - }, - { - "key": "colorNeedleCircle", - "type": "color" - }, - { - "key": "colorBorder", - "type": "color" - }, - { - "key": "majorTickFont", - "items": [ - "majorTickFont.family", - "majorTickFont.size", - { - "key": "majorTickFont.style", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "normal", - "label": "Normal" - }, - { - "value": "italic", - "label": "Italic" - }, - { - "value": "oblique", - "label": "Oblique" - } - ] - }, - { - "key": "majorTickFont.weight", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "normal", - "label": "Normal" - }, - { - "value": "bold", - "label": "Bold" - }, - { - "value": "bolder", - "label": "Bolder" - }, - { - "value": "lighter", - "label": "Lighter" - }, - { - "value": "100", - "label": "100" - }, - { - "value": "200", - "label": "200" - }, - { - "value": "300", - "label": "300" - }, - { - "value": "400", - "label": "400" - }, - { - "value": "500", - "label": "500" - }, - { - "value": "600", - "label": "600" - }, - { - "value": "700", - "label": "800" - }, - { - "value": "800", - "label": "800" - }, - { - "value": "900", - "label": "900" - } - ] - }, - { - "key": "majorTickFont.color", - "type": "color" - } - ] - }, - "animation", - "animationDuration", - { - "key": "animationRule", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "linear", - "label": "Linear" - }, - { - "value": "quad", - "label": "Quad" - }, - { - "value": "quint", - "label": "Quint" - }, - { - "value": "cycle", - "label": "Cycle" - }, - { - "value": "bounce", - "label": "Bounce" - }, - { - "value": "elastic", - "label": "Elastic" - }, - { - "value": "dequad", - "label": "Dequad" - }, - { - "value": "dequint", - "label": "Dequint" - }, - { - "value": "decycle", - "label": "Decycle" - }, - { - "value": "debounce", - "label": "Debounce" - }, - { - "value": "delastic", - "label": "Delastic" - } - ] - }, - { - "key": "animationTarget", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "needle", - "label": "Needle" - }, - { - "value": "plate", - "label": "Plate" - } - ] - } - ] - }; - } -} - -/* eslint-enable angular/angularelement */ diff --git a/ui/src/app/widget/lib/analogue-linear-gauge.js b/ui/src/app/widget/lib/analogue-linear-gauge.js deleted file mode 100644 index 4f9b39c143..0000000000 --- a/ui/src/app/widget/lib/analogue-linear-gauge.js +++ /dev/null @@ -1,1057 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import $ from 'jquery'; -import canvasGauges from 'canvas-gauges'; -import tinycolor from 'tinycolor2'; - -/* eslint-disable angular/angularelement */ -export default class TbAnalogueLinearGauge { - constructor(ctx, canvasId) { - - this.ctx = ctx; - - canvasGauges.performance = window.performance; // eslint-disable-line no-undef, angular/window-service - - var gaugeElement = $('#'+canvasId, ctx.$container); - - var settings = ctx.settings; - - var minValue = settings.minValue || 0; - var maxValue = settings.maxValue || 100; - - var dataKey = ctx.data[0].dataKey; - var keyColor = settings.defaultColor || dataKey.color; - - var majorTicksCount = settings.majorTicksCount || 10; - var total = maxValue-minValue; - var step = (total/majorTicksCount); - - var valueInt = settings.valueInt || 3; - - var valueDec = getValueDec(settings); - - step = parseFloat(parseFloat(step).toFixed(valueDec)); - - var majorTicks = []; - var highlights = []; - var tick = minValue; - - while(tick<=maxValue) { - majorTicks.push(tick); - var nextTick = tick+step; - nextTick = parseFloat(parseFloat(nextTick).toFixed(valueDec)); - if (tick 0 ? settings.units : ctx.units; - } - } - - function getValueDec(settings) { - var dataKey; - if (ctx.data && ctx.data[0]) { - dataKey = ctx.data[0].dataKey; - } - if (dataKey && angular.isDefined(dataKey.decimals)) { - return dataKey.decimals; - } else { - return (angular.isDefined(settings.valueDec) && settings.valueDec !== null) - ? settings.valueDec : ctx.decimals; - } - } - - - function getFontFamily(fontSettings) { - var family = fontSettings && fontSettings.family ? fontSettings.family : 'Roboto'; - if (family === 'RobotoDraft') { - family = 'Roboto'; - } - return family; - } - - var gaugeData = { - - renderTo: gaugeElement[0], - - /* Generic options */ - - minValue: minValue, - maxValue: maxValue, - majorTicks: majorTicks, - minorTicks: settings.minorTicks || 2, - units: getUnits(settings), - title: ((settings.showUnitTitle !== false) ? - (settings.unitTitle && settings.unitTitle.length > 0 ? - settings.unitTitle : dataKey.label) : ''), - - borders: settings.showBorder === true, - borderShadowWidth: (settings.showBorder === true) ? 3 : 0, - borderOuterWidth: (settings.showBorder === true) ? 3 : 0, - borderMiddleWidth: (settings.showBorder === true) ? 3 : 0, - borderInnerWidth: (settings.showBorder === true) ? 3 : 0, - - // borders - - // number formats - valueInt: valueInt, - valueDec: valueDec, - majorTicksInt: 1, - majorTicksDec: 0, - - valueBox: settings.valueBox !== false, - valueBoxStroke: 5, - valueBoxWidth: 0, - valueText: '', - valueTextShadow: true, - valueBoxBorderRadius: 2.5, - - //highlights - highlights: (settings.highlights && settings.highlights.length > 0) ? settings.highlights : highlights, - highlightsWidth: (angular.isDefined(settings.highlightsWidth) && settings.highlightsWidth !== null) ? settings.highlightsWidth : 10, - - //fonts - fontNumbers: getFontFamily(settings.numbersFont), - fontTitle: getFontFamily(settings.titleFont), - fontUnits: getFontFamily(settings.unitsFont), - fontValue: getFontFamily(settings.valueFont), - - fontNumbersSize: settings.numbersFont && settings.numbersFont.size ? settings.numbersFont.size : 18, - fontTitleSize: settings.titleFont && settings.titleFont.size ? settings.titleFont.size : 24, - fontUnitsSize: settings.unitsFont && settings.unitsFont.size ? settings.unitsFont.size : 22, - fontValueSize: settings.valueFont && settings.valueFont.size ? settings.valueFont.size : 40, - - fontNumbersStyle: settings.numbersFont && settings.numbersFont.style ? settings.numbersFont.style : 'normal', - fontTitleStyle: settings.titleFont && settings.titleFont.style ? settings.titleFont.style : 'normal', - fontUnitsStyle: settings.unitsFont && settings.unitsFont.style ? settings.unitsFont.style : 'normal', - fontValueStyle: settings.valueFont && settings.valueFont.style ? settings.valueFont.style : 'normal', - - fontNumbersWeight: settings.numbersFont && settings.numbersFont.weight ? settings.numbersFont.weight : '500', - fontTitleWeight: settings.titleFont && settings.titleFont.weight ? settings.titleFont.weight : '500', - fontUnitsWeight: settings.unitsFont && settings.unitsFont.weight ? settings.unitsFont.weight : '500', - fontValueWeight: settings.valueFont && settings.valueFont.weight ? settings.valueFont.weight : '500', - - colorNumbers: settings.numbersFont && settings.numbersFont.color ? settings.numbersFont.color : colorNumbers, - colorTitle: settings.titleFont && settings.titleFont.color ? settings.titleFont.color : '#888', - colorUnits: settings.unitsFont && settings.unitsFont.color ? settings.unitsFont.color : '#888', - colorValueText: settings.valueFont && settings.valueFont.color ? settings.valueFont.color : '#444', - colorValueTextShadow: settings.valueFont && settings.valueFont.shadowColor ? settings.valueFont.shadowColor : 'rgba(0,0,0,0.3)', - - //colors - colorPlate: settings.colorPlate || '#fff', - colorMajorTicks: settings.colorMajorTicks || '#444', - colorMinorTicks: settings.colorMinorTicks || '#666', - colorNeedle: settings.colorNeedle || keyColor, - colorNeedleEnd: settings.colorNeedleEnd || keyColor, - - colorValueBoxRect: settings.colorValueBoxRect || '#888', - colorValueBoxRectEnd: settings.colorValueBoxRectEnd || '#666', - colorValueBoxBackground: settings.colorValueBoxBackground || '#babab2', - colorValueBoxShadow: settings.colorValueBoxShadow || 'rgba(0,0,0,1)', - colorNeedleShadowUp: settings.colorNeedleShadowUp || 'rgba(2,255,255,0.2)', - colorNeedleShadowDown: settings.colorNeedleShadowDown || 'rgba(188,143,143,0.45)', - - // animations - animation: settings.animation !== false && !ctx.isMobile, - animationDuration: (angular.isDefined(settings.animationDuration) && settings.animationDuration !== null) ? settings.animationDuration : 500, - animationRule: settings.animationRule || 'cycle', - - /* Linear gauge specific */ - - barStrokeWidth: (angular.isDefined(settings.barStrokeWidth) && settings.barStrokeWidth !== null) ? settings.barStrokeWidth : 2.5, - colorBarStroke: settings.colorBarStroke || barStrokeColor, - colorBar: settings.colorBar || "#fff", - colorBarEnd: settings.colorBarEnd || "#ddd", - colorBarProgress: settings.colorBarProgress || progressColorStart, - colorBarProgressEnd: settings.colorBarProgressEnd || progressColorEnd - }; - this.gauge = new canvasGauges.LinearGauge(gaugeData).draw(); - } - - update() { - if (this.ctx.data.length > 0) { - var cellData = this.ctx.data[0]; - if (cellData.data.length > 0) { - var tvPair = cellData.data[cellData.data.length - - 1]; - var value = tvPair[1]; - if(value !== this.gauge.value) { - this.gauge.value = value; - } - } - } - } - - mobileModeChanged() { - var animation = this.ctx.settings.animation !== false && !this.ctx.isMobile; - this.gauge.update({animation: animation}); - } - - resize() { - this.gauge.update({width: this.ctx.width, height: this.ctx.height}); - } - - static get settingsSchema() { - return { - "schema": { - "type": "object", - "title": "Settings", - "properties": { - "minValue": { - "title": "Minimum value", - "type": "number", - "default": 0 - }, - "maxValue": { - "title": "Maximum value", - "type": "number", - "default": 100 - }, - "unitTitle": { - "title": "Unit title", - "type": "string", - "default": null - }, - "showUnitTitle": { - "title": "Show unit title", - "type": "boolean", - "default": true - }, - "majorTicksCount": { - "title": "Major ticks count", - "type": "number", - "default": null - }, - "minorTicks": { - "title": "Minor ticks count", - "type": "number", - "default": 2 - }, - "valueBox": { - "title": "Show value box", - "type": "boolean", - "default": true - }, - "valueInt": { - "title": "Digits count for integer part of value", - "type": "number", - "default": 3 - }, - "defaultColor": { - "title": "Default color", - "type": "string", - "default": null - }, - "colorPlate": { - "title": "Plate color", - "type": "string", - "default": "#fff" - }, - "colorMajorTicks": { - "title": "Major ticks color", - "type": "string", - "default": "#444" - }, - "colorMinorTicks": { - "title": "Minor ticks color", - "type": "string", - "default": "#666" - }, - "colorNeedle": { - "title": "Needle color", - "type": "string", - "default": null - }, - "colorNeedleEnd": { - "title": "Needle color - end gradient", - "type": "string", - "default": null - }, - "colorNeedleShadowUp": { - "title": "Upper half of the needle shadow color", - "type": "string", - "default": "rgba(2,255,255,0.2)" - }, - "colorNeedleShadowDown": { - "title": "Drop shadow needle color.", - "type": "string", - "default": "rgba(188,143,143,0.45)" - }, - "colorValueBoxRect": { - "title": "Value box rectangle stroke color", - "type": "string", - "default": "#888" - }, - "colorValueBoxRectEnd": { - "title": "Value box rectangle stroke color - end gradient", - "type": "string", - "default": "#666" - }, - "colorValueBoxBackground": { - "title": "Value box background color", - "type": "string", - "default": "#babab2" - }, - "colorValueBoxShadow": { - "title": "Value box shadow color", - "type": "string", - "default": "rgba(0,0,0,1)" - }, - "highlights": { - "title": "Highlights", - "type": "array", - "items": { - "title": "Highlight", - "type": "object", - "properties": { - "from": { - "title": "From", - "type": "number" - }, - "to": { - "title": "To", - "type": "number" - }, - "color": { - "title": "Color", - "type": "string" - } - } - } - }, - "highlightsWidth": { - "title": "Highlights width", - "type": "number", - "default": 15 - }, - "showBorder": { - "title": "Show border", - "type": "boolean", - "default": true - }, - "numbersFont": { - "title": "Tick numbers font", - "type": "object", - "properties": { - "family": { - "title": "Font family", - "type": "string", - "default": "Roboto" - }, - "size": { - "title": "Size", - "type": "number", - "default": 18 - }, - "style": { - "title": "Style", - "type": "string", - "default": "normal" - }, - "weight": { - "title": "Weight", - "type": "string", - "default": "500" - }, - "color": { - "title": "color", - "type": "string", - "default": null - } - } - }, - "titleFont": { - "title": "Title text font", - "type": "object", - "properties": { - "family": { - "title": "Font family", - "type": "string", - "default": "Roboto" - }, - "size": { - "title": "Size", - "type": "number", - "default": 24 - }, - "style": { - "title": "Style", - "type": "string", - "default": "normal" - }, - "weight": { - "title": "Weight", - "type": "string", - "default": "500" - }, - "color": { - "title": "color", - "type": "string", - "default": "#888" - } - } - }, - "unitsFont": { - "title": "Units text font", - "type": "object", - "properties": { - "family": { - "title": "Font family", - "type": "string", - "default": "Roboto" - }, - "size": { - "title": "Size", - "type": "number", - "default": 22 - }, - "style": { - "title": "Style", - "type": "string", - "default": "normal" - }, - "weight": { - "title": "Weight", - "type": "string", - "default": "500" - }, - "color": { - "title": "color", - "type": "string", - "default": "#888" - } - } - }, - "valueFont": { - "title": "Value text font", - "type": "object", - "properties": { - "family": { - "title": "Font family", - "type": "string", - "default": "Roboto" - }, - "size": { - "title": "Size", - "type": "number", - "default": 40 - }, - "style": { - "title": "Style", - "type": "string", - "default": "normal" - }, - "weight": { - "title": "Weight", - "type": "string", - "default": "500" - }, - "color": { - "title": "color", - "type": "string", - "default": "#444" - }, - "shadowColor": { - "title": "Shadow color", - "type": "string", - "default": "rgba(0,0,0,0.3)" - } - } - }, - "animation": { - "title": "Enable animation", - "type": "boolean", - "default": true - }, - "animationDuration": { - "title": "Animation duration", - "type": "number", - "default": 500 - }, - "animationRule": { - "title": "Animation rule", - "type": "string", - "default": "cycle" - }, - "barStrokeWidth": { - "title": "Bar stroke width", - "type": "number", - "default": 2.5 - }, - "colorBarStroke": { - "title": "Bar stroke color", - "type": "string", - "default": null - }, - "colorBar": { - "title": "Bar background color", - "type": "string", - "default": "#fff" - }, - "colorBarEnd": { - "title": "Bar background color - end gradient", - "type": "string", - "default": "#ddd" - }, - "colorBarProgress": { - "title": "Progress bar color", - "type": "string", - "default": null - }, - "colorBarProgressEnd": { - "title": "Progress bar color - end gradient", - "type": "string", - "default": null - } - - }, - "required": [] - }, - "form": [ - "barStrokeWidth", - { - "key": "colorBarStroke", - "type": "color" - }, - { - "key": "colorBar", - "type": "color" - }, - { - "key": "colorBarEnd", - "type": "color" - }, - { - "key": "colorBarProgress", - "type": "color" - }, - { - "key": "colorBarProgressEnd", - "type": "color" - }, - "minValue", - "maxValue", - "unitTitle", - "showUnitTitle", - "majorTicksCount", - "minorTicks", - "valueBox", - "valueInt", - { - "key": "defaultColor", - "type": "color" - }, - { - "key": "colorPlate", - "type": "color" - }, - { - "key": "colorMajorTicks", - "type": "color" - }, - { - "key": "colorMinorTicks", - "type": "color" - }, - { - "key": "colorNeedle", - "type": "color" - }, - { - "key": "colorNeedleEnd", - "type": "color" - }, - { - "key": "colorNeedleShadowUp", - "type": "color" - }, - { - "key": "colorNeedleShadowDown", - "type": "color" - }, - { - "key": "colorValueBoxRect", - "type": "color" - }, - { - "key": "colorValueBoxRectEnd", - "type": "color" - }, - { - "key": "colorValueBoxBackground", - "type": "color" - }, - { - "key": "colorValueBoxShadow", - "type": "color" - }, - { - "key": "highlights", - "items": [ - "highlights[].from", - "highlights[].to", - { - "key": "highlights[].color", - "type": "color" - } - ] - }, - "highlightsWidth", - "showBorder", - { - "key": "numbersFont", - "items": [ - "numbersFont.family", - "numbersFont.size", - { - "key": "numbersFont.style", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "normal", - "label": "Normal" - }, - { - "value": "italic", - "label": "Italic" - }, - { - "value": "oblique", - "label": "Oblique" - } - ] - }, - { - "key": "numbersFont.weight", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "normal", - "label": "Normal" - }, - { - "value": "bold", - "label": "Bold" - }, - { - "value": "bolder", - "label": "Bolder" - }, - { - "value": "lighter", - "label": "Lighter" - }, - { - "value": "100", - "label": "100" - }, - { - "value": "200", - "label": "200" - }, - { - "value": "300", - "label": "300" - }, - { - "value": "400", - "label": "400" - }, - { - "value": "500", - "label": "500" - }, - { - "value": "600", - "label": "600" - }, - { - "value": "700", - "label": "800" - }, - { - "value": "800", - "label": "800" - }, - { - "value": "900", - "label": "900" - } - ] - }, - { - "key": "numbersFont.color", - "type": "color" - } - ] - }, - { - "key": "titleFont", - "items": [ - "titleFont.family", - "titleFont.size", - { - "key": "titleFont.style", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "normal", - "label": "Normal" - }, - { - "value": "italic", - "label": "Italic" - }, - { - "value": "oblique", - "label": "Oblique" - } - ] - }, - { - "key": "titleFont.weight", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "normal", - "label": "Normal" - }, - { - "value": "bold", - "label": "Bold" - }, - { - "value": "bolder", - "label": "Bolder" - }, - { - "value": "lighter", - "label": "Lighter" - }, - { - "value": "100", - "label": "100" - }, - { - "value": "200", - "label": "200" - }, - { - "value": "300", - "label": "300" - }, - { - "value": "400", - "label": "400" - }, - { - "value": "500", - "label": "500" - }, - { - "value": "600", - "label": "600" - }, - { - "value": "700", - "label": "800" - }, - { - "value": "800", - "label": "800" - }, - { - "value": "900", - "label": "900" - } - ] - }, - { - "key": "titleFont.color", - "type": "color" - } - ] - }, - { - "key": "unitsFont", - "items": [ - "unitsFont.family", - "unitsFont.size", - { - "key": "unitsFont.style", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "normal", - "label": "Normal" - }, - { - "value": "italic", - "label": "Italic" - }, - { - "value": "oblique", - "label": "Oblique" - } - ] - }, - { - "key": "unitsFont.weight", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "normal", - "label": "Normal" - }, - { - "value": "bold", - "label": "Bold" - }, - { - "value": "bolder", - "label": "Bolder" - }, - { - "value": "lighter", - "label": "Lighter" - }, - { - "value": "100", - "label": "100" - }, - { - "value": "200", - "label": "200" - }, - { - "value": "300", - "label": "300" - }, - { - "value": "400", - "label": "400" - }, - { - "value": "500", - "label": "500" - }, - { - "value": "600", - "label": "600" - }, - { - "value": "700", - "label": "800" - }, - { - "value": "800", - "label": "800" - }, - { - "value": "900", - "label": "900" - } - ] - }, - { - "key": "unitsFont.color", - "type": "color" - } - ] - }, - { - "key": "valueFont", - "items": [ - "valueFont.family", - "valueFont.size", - { - "key": "valueFont.style", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "normal", - "label": "Normal" - }, - { - "value": "italic", - "label": "Italic" - }, - { - "value": "oblique", - "label": "Oblique" - } - ] - }, - { - "key": "valueFont.weight", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "normal", - "label": "Normal" - }, - { - "value": "bold", - "label": "Bold" - }, - { - "value": "bolder", - "label": "Bolder" - }, - { - "value": "lighter", - "label": "Lighter" - }, - { - "value": "100", - "label": "100" - }, - { - "value": "200", - "label": "200" - }, - { - "value": "300", - "label": "300" - }, - { - "value": "400", - "label": "400" - }, - { - "value": "500", - "label": "500" - }, - { - "value": "600", - "label": "600" - }, - { - "value": "700", - "label": "800" - }, - { - "value": "800", - "label": "800" - }, - { - "value": "900", - "label": "900" - } - ] - }, - { - "key": "valueFont.color", - "type": "color" - }, - { - "key": "valueFont.shadowColor", - "type": "color" - } - ] - }, - "animation", - "animationDuration", - { - "key": "animationRule", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "linear", - "label": "Linear" - }, - { - "value": "quad", - "label": "Quad" - }, - { - "value": "quint", - "label": "Quint" - }, - { - "value": "cycle", - "label": "Cycle" - }, - { - "value": "bounce", - "label": "Bounce" - }, - { - "value": "elastic", - "label": "Elastic" - }, - { - "value": "dequad", - "label": "Dequad" - }, - { - "value": "dequint", - "label": "Dequint" - }, - { - "value": "decycle", - "label": "Decycle" - }, - { - "value": "debounce", - "label": "Debounce" - }, - { - "value": "delastic", - "label": "Delastic" - } - ] - } - ] - }; - } - -} - -/* eslint-enable angular/angularelement */ diff --git a/ui/src/app/widget/lib/analogue-radial-gauge.js b/ui/src/app/widget/lib/analogue-radial-gauge.js deleted file mode 100644 index cb89d9ddd2..0000000000 --- a/ui/src/app/widget/lib/analogue-radial-gauge.js +++ /dev/null @@ -1,1033 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import $ from 'jquery'; -import canvasGauges from 'canvas-gauges'; -import tinycolor from 'tinycolor2'; - -/* eslint-disable angular/angularelement */ - -export default class TbAnalogueRadialGauge { - constructor(ctx, canvasId) { - - this.ctx = ctx; - - canvasGauges.performance = window.performance; // eslint-disable-line no-undef, angular/window-service - - var gaugeElement = $('#'+canvasId, ctx.$container); - - var settings = ctx.settings; - - var minValue = settings.minValue || 0; - var maxValue = settings.maxValue || 100; - - var dataKey = ctx.data[0].dataKey; - var keyColor = settings.defaultColor || dataKey.color; - - var majorTicksCount = settings.majorTicksCount || 10; - var total = maxValue-minValue; - var step = (total/majorTicksCount); - - var valueInt = settings.valueInt || 3; - - var valueDec = getValueDec(settings); - - step = parseFloat(parseFloat(step).toFixed(valueDec)); - - var majorTicks = []; - var highlights = []; - var tick = minValue; - - while(tick<=maxValue) { - majorTicks.push(tick); - var nextTick = tick+step; - nextTick = parseFloat(parseFloat(nextTick).toFixed(valueDec)); - if (tick 0 ? settings.units : ctx.units; - } - } - - function getValueDec(settings) { - var dataKey; - if (ctx.data && ctx.data[0]) { - dataKey = ctx.data[0].dataKey; - } - if (dataKey && angular.isDefined(dataKey.decimals)) { - return dataKey.decimals; - } else { - return (angular.isDefined(settings.valueDec) && settings.valueDec !== null) - ? settings.valueDec : ctx.decimals; - } - } - - function getFontFamily(fontSettings) { - var family = fontSettings && fontSettings.family ? fontSettings.family : 'Roboto'; - if (family === 'RobotoDraft') { - family = 'Roboto'; - } - return family; - } - - var gaugeData = { - - renderTo: gaugeElement[0], - - /* Generic options */ - - minValue: minValue, - maxValue: maxValue, - majorTicks: majorTicks, - minorTicks: settings.minorTicks || 2, - units: getUnits(settings), - title: ((settings.showUnitTitle !== false) ? - (settings.unitTitle && settings.unitTitle.length > 0 ? - settings.unitTitle : dataKey.label) : ''), - - borders: settings.showBorder !== false, - borderShadowWidth: (settings.showBorder !== false) ? 3 : 0, - - // borders - //borderOuterWidth: (settings.showBorder !== false) ? 3 : 0, - //borderMiddleWidth: (settings.showBorder !== false) ? 3 : 0, - //borderInnerWidth: (settings.showBorder !== false) ? 3 : 0, - //borderShadowWidth: (settings.showBorder !== false) ? 3 : 0, - - // number formats - valueInt: valueInt, - valueDec: valueDec, - majorTicksInt: 1, - majorTicksDec: 0, - - valueBox: settings.valueBox !== false, - valueBoxStroke: 5, - valueBoxWidth: 0, - valueText: '', - valueTextShadow: true, - valueBoxBorderRadius: 2.5, - - //highlights - highlights: (settings.highlights && settings.highlights.length > 0) ? settings.highlights : highlights, - highlightsWidth: (angular.isDefined(settings.highlightsWidth) && settings.highlightsWidth !== null) ? settings.highlightsWidth : 15, - - //fonts - fontNumbers: getFontFamily(settings.numbersFont), - fontTitle: getFontFamily(settings.titleFont), - fontUnits: getFontFamily(settings.unitsFont), - fontValue: getFontFamily(settings.valueFont), - - fontNumbersSize: settings.numbersFont && settings.numbersFont.size ? settings.numbersFont.size : 18, - fontTitleSize: settings.titleFont && settings.titleFont.size ? settings.titleFont.size : 24, - fontUnitsSize: settings.unitsFont && settings.unitsFont.size ? settings.unitsFont.size : 22, - fontValueSize: settings.valueFont && settings.valueFont.size ? settings.valueFont.size : 40, - - fontNumbersStyle: settings.numbersFont && settings.numbersFont.style ? settings.numbersFont.style : 'normal', - fontTitleStyle: settings.titleFont && settings.titleFont.style ? settings.titleFont.style : 'normal', - fontUnitsStyle: settings.unitsFont && settings.unitsFont.style ? settings.unitsFont.style : 'normal', - fontValueStyle: settings.valueFont && settings.valueFont.style ? settings.valueFont.style : 'normal', - - fontNumbersWeight: settings.numbersFont && settings.numbersFont.weight ? settings.numbersFont.weight : '500', - fontTitleWeight: settings.titleFont && settings.titleFont.weight ? settings.titleFont.weight : '500', - fontUnitsWeight: settings.unitsFont && settings.unitsFont.weight ? settings.unitsFont.weight : '500', - fontValueWeight: settings.valueFont && settings.valueFont.weight ? settings.valueFont.weight : '500', - - colorNumbers: settings.numbersFont && settings.numbersFont.color ? settings.numbersFont.color : colorNumbers, - colorTitle: settings.titleFont && settings.titleFont.color ? settings.titleFont.color : '#888', - colorUnits: settings.unitsFont && settings.unitsFont.color ? settings.unitsFont.color : '#888', - colorValueText: settings.valueFont && settings.valueFont.color ? settings.valueFont.color : '#444', - colorValueTextShadow: settings.valueFont && settings.valueFont.shadowColor ? settings.valueFont.shadowColor : 'rgba(0,0,0,0.3)', - - //colors - colorPlate: settings.colorPlate || '#fff', - colorMajorTicks: settings.colorMajorTicks || '#444', - colorMinorTicks: settings.colorMinorTicks || '#666', - colorNeedle: settings.colorNeedle || keyColor, - colorNeedleEnd: settings.colorNeedleEnd || keyColor, - - colorValueBoxRect: settings.colorValueBoxRect || '#888', - colorValueBoxRectEnd: settings.colorValueBoxRectEnd || '#666', - colorValueBoxBackground: settings.colorValueBoxBackground || '#babab2', - colorValueBoxShadow: settings.colorValueBoxShadow || 'rgba(0,0,0,1)', - colorNeedleShadowUp: settings.colorNeedleShadowUp || 'rgba(2,255,255,0.2)', - colorNeedleShadowDown: settings.colorNeedleShadowDown || 'rgba(188,143,143,0.45)', - - // animations - animation: settings.animation !== false && !ctx.isMobile, - animationDuration: (angular.isDefined(settings.animationDuration) && settings.animationDuration !== null) ? settings.animationDuration : 500, - animationRule: settings.animationRule || 'cycle', - - /* Radial gauge specific */ - - ticksAngle: settings.ticksAngle || 270, - startAngle: settings.startAngle || 45, - - // colors - - colorNeedleCircleOuter: '#f0f0f0', - colorNeedleCircleOuterEnd: '#ccc', - colorNeedleCircleInner: '#e8e8e8', //tinycolor(keyColor).lighten(30).toRgbString(),//'#e8e8e8', - colorNeedleCircleInnerEnd: '#f5f5f5', - - // needle - needleCircleSize: settings.needleCircleSize || 10, - needleCircleInner: true, - needleCircleOuter: true, - - // custom animations - animationTarget: 'needle' // 'needle' or 'plate' - - }; - this.gauge = new canvasGauges.RadialGauge(gaugeData).draw(); - } - - update() { - if (this.ctx.data.length > 0) { - var cellData = this.ctx.data[0]; - if (cellData.data.length > 0) { - var tvPair = cellData.data[cellData.data.length - - 1]; - var value = tvPair[1]; - if(value !== this.gauge.value) { - this.gauge.value = value; - } - } - } - - } - - mobileModeChanged() { - var animation = this.ctx.settings.animation !== false && !this.ctx.isMobile; - this.gauge.update({animation: animation}); - } - - resize() { - this.gauge.update({width: this.ctx.width, height: this.ctx.height}); - } - - static get settingsSchema() { - return { - "schema": { - "type": "object", - "title": "Settings", - "properties": { - "minValue": { - "title": "Minimum value", - "type": "number", - "default": 0 - }, - "maxValue": { - "title": "Maximum value", - "type": "number", - "default": 100 - }, - "unitTitle": { - "title": "Unit title", - "type": "string", - "default": null - }, - "showUnitTitle": { - "title": "Show unit title", - "type": "boolean", - "default": true - }, - "majorTicksCount": { - "title": "Major ticks count", - "type": "number", - "default": null - }, - "minorTicks": { - "title": "Minor ticks count", - "type": "number", - "default": 2 - }, - "valueBox": { - "title": "Show value box", - "type": "boolean", - "default": true - }, - "valueInt": { - "title": "Digits count for integer part of value", - "type": "number", - "default": 3 - }, - "defaultColor": { - "title": "Default color", - "type": "string", - "default": null - }, - "colorPlate": { - "title": "Plate color", - "type": "string", - "default": "#fff" - }, - "colorMajorTicks": { - "title": "Major ticks color", - "type": "string", - "default": "#444" - }, - "colorMinorTicks": { - "title": "Minor ticks color", - "type": "string", - "default": "#666" - }, - "colorNeedle": { - "title": "Needle color", - "type": "string", - "default": null - }, - "colorNeedleEnd": { - "title": "Needle color - end gradient", - "type": "string", - "default": null - }, - "colorNeedleShadowUp": { - "title": "Upper half of the needle shadow color", - "type": "string", - "default": "rgba(2,255,255,0.2)" - }, - "colorNeedleShadowDown": { - "title": "Drop shadow needle color.", - "type": "string", - "default": "rgba(188,143,143,0.45)" - }, - "colorValueBoxRect": { - "title": "Value box rectangle stroke color", - "type": "string", - "default": "#888" - }, - "colorValueBoxRectEnd": { - "title": "Value box rectangle stroke color - end gradient", - "type": "string", - "default": "#666" - }, - "colorValueBoxBackground": { - "title": "Value box background color", - "type": "string", - "default": "#babab2" - }, - "colorValueBoxShadow": { - "title": "Value box shadow color", - "type": "string", - "default": "rgba(0,0,0,1)" - }, - "highlights": { - "title": "Highlights", - "type": "array", - "items": { - "title": "Highlight", - "type": "object", - "properties": { - "from": { - "title": "From", - "type": "number" - }, - "to": { - "title": "To", - "type": "number" - }, - "color": { - "title": "Color", - "type": "string" - } - } - } - }, - "highlightsWidth": { - "title": "Highlights width", - "type": "number", - "default": 15 - }, - "showBorder": { - "title": "Show border", - "type": "boolean", - "default": true - }, - "numbersFont": { - "title": "Tick numbers font", - "type": "object", - "properties": { - "family": { - "title": "Font family", - "type": "string", - "default": "Roboto" - }, - "size": { - "title": "Size", - "type": "number", - "default": 18 - }, - "style": { - "title": "Style", - "type": "string", - "default": "normal" - }, - "weight": { - "title": "Weight", - "type": "string", - "default": "500" - }, - "color": { - "title": "color", - "type": "string", - "default": null - } - } - }, - "titleFont": { - "title": "Title text font", - "type": "object", - "properties": { - "family": { - "title": "Font family", - "type": "string", - "default": "Roboto" - }, - "size": { - "title": "Size", - "type": "number", - "default": 24 - }, - "style": { - "title": "Style", - "type": "string", - "default": "normal" - }, - "weight": { - "title": "Weight", - "type": "string", - "default": "500" - }, - "color": { - "title": "color", - "type": "string", - "default": "#888" - } - } - }, - "unitsFont": { - "title": "Units text font", - "type": "object", - "properties": { - "family": { - "title": "Font family", - "type": "string", - "default": "Roboto" - }, - "size": { - "title": "Size", - "type": "number", - "default": 22 - }, - "style": { - "title": "Style", - "type": "string", - "default": "normal" - }, - "weight": { - "title": "Weight", - "type": "string", - "default": "500" - }, - "color": { - "title": "color", - "type": "string", - "default": "#888" - } - } - }, - "valueFont": { - "title": "Value text font", - "type": "object", - "properties": { - "family": { - "title": "Font family", - "type": "string", - "default": "Roboto" - }, - "size": { - "title": "Size", - "type": "number", - "default": 40 - }, - "style": { - "title": "Style", - "type": "string", - "default": "normal" - }, - "weight": { - "title": "Weight", - "type": "string", - "default": "500" - }, - "color": { - "title": "color", - "type": "string", - "default": "#444" - }, - "shadowColor": { - "title": "Shadow color", - "type": "string", - "default": "rgba(0,0,0,0.3)" - } - } - }, - "animation": { - "title": "Enable animation", - "type": "boolean", - "default": true - }, - "animationDuration": { - "title": "Animation duration", - "type": "number", - "default": 500 - }, - "animationRule": { - "title": "Animation rule", - "type": "string", - "default": "cycle" - }, - "startAngle": { - "title": "Start ticks angle", - "type": "number", - "default": 45 - }, - "ticksAngle": { - "title": "Ticks angle", - "type": "number", - "default": 270 - }, - "needleCircleSize": { - "title": "Needle circle size", - "type": "number", - "default": 10 - } - }, - "required": [] - }, - "form": [ - "startAngle", - "ticksAngle", - "needleCircleSize", - "minValue", - "maxValue", - "unitTitle", - "showUnitTitle", - "majorTicksCount", - "minorTicks", - "valueBox", - "valueInt", - { - "key": "defaultColor", - "type": "color" - }, - { - "key": "colorPlate", - "type": "color" - }, - { - "key": "colorMajorTicks", - "type": "color" - }, - { - "key": "colorMinorTicks", - "type": "color" - }, - { - "key": "colorNeedle", - "type": "color" - }, - { - "key": "colorNeedleEnd", - "type": "color" - }, - { - "key": "colorNeedleShadowUp", - "type": "color" - }, - { - "key": "colorNeedleShadowDown", - "type": "color" - }, - { - "key": "colorValueBoxRect", - "type": "color" - }, - { - "key": "colorValueBoxRectEnd", - "type": "color" - }, - { - "key": "colorValueBoxBackground", - "type": "color" - }, - { - "key": "colorValueBoxShadow", - "type": "color" - }, - { - "key": "highlights", - "items": [ - "highlights[].from", - "highlights[].to", - { - "key": "highlights[].color", - "type": "color" - } - ] - }, - "highlightsWidth", - "showBorder", - { - "key": "numbersFont", - "items": [ - "numbersFont.family", - "numbersFont.size", - { - "key": "numbersFont.style", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "normal", - "label": "Normal" - }, - { - "value": "italic", - "label": "Italic" - }, - { - "value": "oblique", - "label": "Oblique" - } - ] - }, - { - "key": "numbersFont.weight", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "normal", - "label": "Normal" - }, - { - "value": "bold", - "label": "Bold" - }, - { - "value": "bolder", - "label": "Bolder" - }, - { - "value": "lighter", - "label": "Lighter" - }, - { - "value": "100", - "label": "100" - }, - { - "value": "200", - "label": "200" - }, - { - "value": "300", - "label": "300" - }, - { - "value": "400", - "label": "400" - }, - { - "value": "500", - "label": "500" - }, - { - "value": "600", - "label": "600" - }, - { - "value": "700", - "label": "800" - }, - { - "value": "800", - "label": "800" - }, - { - "value": "900", - "label": "900" - } - ] - }, - { - "key": "numbersFont.color", - "type": "color" - } - ] - }, - { - "key": "titleFont", - "items": [ - "titleFont.family", - "titleFont.size", - { - "key": "titleFont.style", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "normal", - "label": "Normal" - }, - { - "value": "italic", - "label": "Italic" - }, - { - "value": "oblique", - "label": "Oblique" - } - ] - }, - { - "key": "titleFont.weight", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "normal", - "label": "Normal" - }, - { - "value": "bold", - "label": "Bold" - }, - { - "value": "bolder", - "label": "Bolder" - }, - { - "value": "lighter", - "label": "Lighter" - }, - { - "value": "100", - "label": "100" - }, - { - "value": "200", - "label": "200" - }, - { - "value": "300", - "label": "300" - }, - { - "value": "400", - "label": "400" - }, - { - "value": "500", - "label": "500" - }, - { - "value": "600", - "label": "600" - }, - { - "value": "700", - "label": "800" - }, - { - "value": "800", - "label": "800" - }, - { - "value": "900", - "label": "900" - } - ] - }, - { - "key": "titleFont.color", - "type": "color" - } - ] - }, - { - "key": "unitsFont", - "items": [ - "unitsFont.family", - "unitsFont.size", - { - "key": "unitsFont.style", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "normal", - "label": "Normal" - }, - { - "value": "italic", - "label": "Italic" - }, - { - "value": "oblique", - "label": "Oblique" - } - ] - }, - { - "key": "unitsFont.weight", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "normal", - "label": "Normal" - }, - { - "value": "bold", - "label": "Bold" - }, - { - "value": "bolder", - "label": "Bolder" - }, - { - "value": "lighter", - "label": "Lighter" - }, - { - "value": "100", - "label": "100" - }, - { - "value": "200", - "label": "200" - }, - { - "value": "300", - "label": "300" - }, - { - "value": "400", - "label": "400" - }, - { - "value": "500", - "label": "500" - }, - { - "value": "600", - "label": "600" - }, - { - "value": "700", - "label": "800" - }, - { - "value": "800", - "label": "800" - }, - { - "value": "900", - "label": "900" - } - ] - }, - { - "key": "unitsFont.color", - "type": "color" - } - ] - }, - { - "key": "valueFont", - "items": [ - "valueFont.family", - "valueFont.size", - { - "key": "valueFont.style", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "normal", - "label": "Normal" - }, - { - "value": "italic", - "label": "Italic" - }, - { - "value": "oblique", - "label": "Oblique" - } - ] - }, - { - "key": "valueFont.weight", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "normal", - "label": "Normal" - }, - { - "value": "bold", - "label": "Bold" - }, - { - "value": "bolder", - "label": "Bolder" - }, - { - "value": "lighter", - "label": "Lighter" - }, - { - "value": "100", - "label": "100" - }, - { - "value": "200", - "label": "200" - }, - { - "value": "300", - "label": "300" - }, - { - "value": "400", - "label": "400" - }, - { - "value": "500", - "label": "500" - }, - { - "value": "600", - "label": "600" - }, - { - "value": "700", - "label": "800" - }, - { - "value": "800", - "label": "800" - }, - { - "value": "900", - "label": "900" - } - ] - }, - { - "key": "valueFont.color", - "type": "color" - }, - { - "key": "valueFont.shadowColor", - "type": "color" - } - ] - }, - "animation", - "animationDuration", - { - "key": "animationRule", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "linear", - "label": "Linear" - }, - { - "value": "quad", - "label": "Quad" - }, - { - "value": "quint", - "label": "Quint" - }, - { - "value": "cycle", - "label": "Cycle" - }, - { - "value": "bounce", - "label": "Bounce" - }, - { - "value": "elastic", - "label": "Elastic" - }, - { - "value": "dequad", - "label": "Dequad" - }, - { - "value": "dequint", - "label": "Dequint" - }, - { - "value": "decycle", - "label": "Decycle" - }, - { - "value": "debounce", - "label": "Debounce" - }, - { - "value": "delastic", - "label": "Delastic" - } - ] - } - ] - }; - } - -} - -/* eslint-enable angular/angularelement */ diff --git a/ui/src/app/widget/lib/canvas-digital-gauge.js b/ui/src/app/widget/lib/canvas-digital-gauge.js deleted file mode 100644 index e4daa052f9..0000000000 --- a/ui/src/app/widget/lib/canvas-digital-gauge.js +++ /dev/null @@ -1,1323 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import $ from 'jquery'; -import tinycolor from 'tinycolor2'; -import canvasGauges from 'canvas-gauges'; -import CanvasDigitalGauge from './CanvasDigitalGauge'; - -/* eslint-disable angular/angularelement */ -export default class TbCanvasDigitalGauge { - - constructor(ctx, canvasId) { - this.ctx = ctx; - - canvasGauges.performance = window.performance; // eslint-disable-line no-undef, angular/window-service - - var gaugeElement = $('#'+canvasId, ctx.$container); - var settings = ctx.settings; - - this.localSettings = {}; - this.localSettings.minValue = settings.minValue || 0; - this.localSettings.maxValue = settings.maxValue || 100; - this.localSettings.gaugeType = settings.gaugeType || 'arc'; - this.localSettings.neonGlowBrightness = settings.neonGlowBrightness || 0; - this.localSettings.dashThickness = settings.dashThickness || 0; - this.localSettings.roundedLineCap = settings.roundedLineCap === true; - - var dataKey = ctx.data[0].dataKey; - var keyColor = settings.defaultColor || dataKey.color; - - this.localSettings.unitTitle = ((settings.showUnitTitle === true) ? - (settings.unitTitle && settings.unitTitle.length > 0 ? - settings.unitTitle : dataKey.label) : ''); - - this.localSettings.showTimestamp = settings.showTimestamp == true ? true : false; - this.localSettings.timestampFormat = settings.timestampFormat && settings.timestampFormat.length ? settings.timestampFormat : 'yyyy-MM-dd HH:mm:ss'; - - this.localSettings.gaugeWidthScale = settings.gaugeWidthScale || 0.75; - this.localSettings.gaugeColor = settings.gaugeColor || tinycolor(keyColor).setAlpha(0.2).toRgbString(); - - this.localSettings.useFixedLevelColor = settings.useFixedLevelColor || false; - if (!settings.useFixedLevelColor) { - if (!settings.levelColors || settings.levelColors.length <= 0) { - this.localSettings.levelColors = [keyColor]; - } else { - this.localSettings.levelColors = settings.levelColors.slice(); - } - } else { - this.localSettings.levelColors = [keyColor]; - this.localSettings.fixedLevelColors = settings.fixedLevelColors || []; - } - - this.localSettings.showTicks = settings.showTicks || false; - this.localSettings.ticks = []; - this.localSettings.ticksValue = settings.ticksValue || []; - this.localSettings.tickWidth = settings.tickWidth || 4; - this.localSettings.colorTicks = settings.colorTicks || '#666'; - - this.localSettings.decimals = angular.isDefined(dataKey.decimals) ? dataKey.decimals : - ((angular.isDefined(settings.decimals) && settings.decimals !== null) - ? settings.decimals : ctx.decimals); - - this.localSettings.units = dataKey.units && dataKey.units.length ? dataKey.units : - (angular.isDefined(settings.units) && settings.units.length > 0 ? settings.units : ctx.units); - - this.localSettings.hideValue = settings.showValue !== true; - this.localSettings.hideMinMax = settings.showMinMax !== true; - - this.localSettings.title = ((settings.showTitle === true) ? - (settings.title && settings.title.length > 0 ? - settings.title : dataKey.label) : ''); - - if (!this.localSettings.unitTitle && this.localSettings.showTimestamp) { - this.localSettings.unitTitle = ' '; - } - - this.localSettings.titleFont = {}; - var settingsTitleFont = settings.titleFont; - if (!settingsTitleFont) { - settingsTitleFont = {}; - } - - function getFontFamily(fontSettings) { - var family = fontSettings && fontSettings.family ? fontSettings.family : 'Roboto'; - if (family === 'RobotoDraft') { - family = 'Roboto'; - } - return family; - } - - this.localSettings.titleFont.family = getFontFamily(settingsTitleFont); - this.localSettings.titleFont.size = settingsTitleFont.size ? settingsTitleFont.size : 12; - this.localSettings.titleFont.style = settingsTitleFont.style ? settingsTitleFont.style : 'normal'; - this.localSettings.titleFont.weight = settingsTitleFont.weight ? settingsTitleFont.weight : '500'; - this.localSettings.titleFont.color = settingsTitleFont.color ? settingsTitleFont.color : keyColor; - - this.localSettings.valueFont = {}; - var settingsValueFont = settings.valueFont; - if (!settingsValueFont) { - settingsValueFont = {}; - } - - this.localSettings.valueFont.family = getFontFamily(settingsValueFont); - this.localSettings.valueFont.size = settingsValueFont.size ? settingsValueFont.size : 18; - this.localSettings.valueFont.style = settingsValueFont.style ? settingsValueFont.style : 'normal'; - this.localSettings.valueFont.weight = settingsValueFont.weight ? settingsValueFont.weight : '500'; - this.localSettings.valueFont.color = settingsValueFont.color ? settingsValueFont.color : keyColor; - - this.localSettings.minMaxFont = {}; - var settingsMinMaxFont = settings.minMaxFont; - if (!settingsMinMaxFont) { - settingsMinMaxFont = {}; - } - - this.localSettings.minMaxFont.family = getFontFamily(settingsMinMaxFont); - this.localSettings.minMaxFont.size = settingsMinMaxFont.size ? settingsMinMaxFont.size : 10; - this.localSettings.minMaxFont.style = settingsMinMaxFont.style ? settingsMinMaxFont.style : 'normal'; - this.localSettings.minMaxFont.weight = settingsMinMaxFont.weight ? settingsMinMaxFont.weight : '500'; - this.localSettings.minMaxFont.color = settingsMinMaxFont.color ? settingsMinMaxFont.color : keyColor; - - this.localSettings.labelFont = {}; - var settingsLabelFont = settings.labelFont; - if (!settingsLabelFont) { - settingsLabelFont = {}; - } - - this.localSettings.labelFont.family = getFontFamily(settingsLabelFont); - this.localSettings.labelFont.size = settingsLabelFont.size ? settingsLabelFont.size : 8; - this.localSettings.labelFont.style = settingsLabelFont.style ? settingsLabelFont.style : 'normal'; - this.localSettings.labelFont.weight = settingsLabelFont.weight ? settingsLabelFont.weight : '500'; - this.localSettings.labelFont.color = settingsLabelFont.color ? settingsLabelFont.color : keyColor; - - - var gaugeData = { - - renderTo: gaugeElement[0], - - gaugeWidthScale: this.localSettings.gaugeWidthScale, - gaugeColor: this.localSettings.gaugeColor, - levelColors: this.localSettings.levelColors, - - colorTicks: this.localSettings.colorTicks, - tickWidth: this.localSettings.tickWidth, - ticks: this.localSettings.ticks, - - title: this.localSettings.title, - - fontTitleSize: this.localSettings.titleFont.size, - fontTitleStyle: this.localSettings.titleFont.style, - fontTitleWeight: this.localSettings.titleFont.weight, - colorTitle: this.localSettings.titleFont.color, - fontTitle: this.localSettings.titleFont.family, - - fontValueSize: this.localSettings.valueFont.size, - fontValueStyle: this.localSettings.valueFont.style, - fontValueWeight: this.localSettings.valueFont.weight, - colorValue: this.localSettings.valueFont.color, - fontValue: this.localSettings.valueFont.family, - - fontMinMaxSize: this.localSettings.minMaxFont.size, - fontMinMaxStyle: this.localSettings.minMaxFont.style, - fontMinMaxWeight: this.localSettings.minMaxFont.weight, - colorMinMax: this.localSettings.minMaxFont.color, - fontMinMax: this.localSettings.minMaxFont.family, - - fontLabelSize: this.localSettings.labelFont.size, - fontLabelStyle: this.localSettings.labelFont.style, - fontLabelWeight: this.localSettings.labelFont.weight, - colorLabel: this.localSettings.labelFont.color, - fontLabel: this.localSettings.labelFont.family, - - minValue: this.localSettings.minValue, - maxValue: this.localSettings.maxValue, - gaugeType: this.localSettings.gaugeType, - dashThickness: this.localSettings.dashThickness, - roundedLineCap: this.localSettings.roundedLineCap, - - symbol: this.localSettings.units, - label: this.localSettings.unitTitle, - showTimestamp: this.localSettings.showTimestamp, - hideValue: this.localSettings.hideValue, - hideMinMax: this.localSettings.hideMinMax, - - valueDec: this.localSettings.decimals, - - neonGlowBrightness: this.localSettings.neonGlowBrightness, - - // animations - animation: settings.animation !== false && !ctx.isMobile, - animationDuration: (angular.isDefined(settings.animationDuration) && settings.animationDuration !== null) ? settings.animationDuration : 500, - animationRule: settings.animationRule || 'linear', - - isMobile: ctx.isMobile - - }; - - this.gauge = new CanvasDigitalGauge(gaugeData).draw(); - this.init(); - } - - init() { - if (this.localSettings.useFixedLevelColor) { - if (this.localSettings.fixedLevelColors && this.localSettings.fixedLevelColors.length > 0) { - this.localSettings.levelColors = this.settingLevelColorsSubscribe(this.localSettings.fixedLevelColors); - } - } - if (this.localSettings.showTicks) { - if (this.localSettings.ticksValue && this.localSettings.ticksValue.length) { - this.localSettings.ticks = this.settingTicksSubscribe(this.localSettings.ticksValue); - } - } - this.updateSetting(); - } - - static generateDatasorce(ctx, datasources, entityAlias, attribute, settings){ - let entityAliasId = ctx.aliasController.getEntityAliasId(entityAlias); - if (!entityAliasId) { - throw new Error('Not valid entity aliase name ' + entityAlias); - } - - let datasource = datasources.filter((datasource) => { - return datasource.entityAliasId === entityAliasId; - })[0]; - - let dataKey = { - type: ctx.$scope.$injector.get('types').dataKeyType.attribute, - name: attribute, - label: attribute, - settings: [settings], - _hash: Math.random() - }; - - if (datasource) { - let findDataKey = datasource.dataKeys.filter((dataKey) => { - return dataKey.name === attribute; - })[0]; - - if (findDataKey) { - findDataKey.settings.push(settings); - } else { - datasource.dataKeys.push(dataKey) - } - } else { - datasource = { - type: ctx.$scope.$injector.get('types').datasourceType.entity, - name: entityAlias, - aliasName: entityAlias, - entityAliasId: entityAliasId, - dataKeys: [dataKey] - }; - datasources.push(datasource); - } - - return datasources; - } - - settingTicksSubscribe(options) { - let ticksDatasource = []; - let predefineTicks = []; - - for (let i = 0; i < options.length; i++) { - let tick = options[i]; - if (tick.valueSource === 'predefinedValue' && isFinite(tick.value)) { - predefineTicks.push(tick.value) - } else if (tick.entityAlias && tick.attribute) { - try { - ticksDatasource = TbCanvasDigitalGauge.generateDatasorce(this.ctx, ticksDatasource, tick.entityAlias, tick.attribute, predefineTicks.length); - } catch (e) { - continue; - } - predefineTicks.push(null); - } - } - - this.subscribeAttributes(ticksDatasource, 'ticks').then((subscription) => { - this.ticksSourcesSubscription = subscription; - }); - - return predefineTicks; - } - - settingLevelColorsSubscribe(options) { - let levelColorsDatasource = []; - let predefineLevelColors = []; - - function setLevelColor(levelSetting, color) { - if (levelSetting.valueSource === 'predefinedValue' && isFinite(levelSetting.value)) { - predefineLevelColors.push({ - value: levelSetting.value, - color: color - }) - } else if (levelSetting.entityAlias && levelSetting.attribute) { - try { - levelColorsDatasource = TbCanvasDigitalGauge.generateDatasorce(this.ctx, levelColorsDatasource, levelSetting.entityAlias, levelSetting.attribute, { - color: color, - index: predefineLevelColors.length - }); - } catch (e) { - return; - } - predefineLevelColors.push(null); - } - } - - for (let i = 0; i < options.length; i++) { - let levelColor = options[i]; - if (levelColor.from) { - setLevelColor.call(this, levelColor.from, levelColor.color); - } - if (levelColor.to) { - setLevelColor.call(this, levelColor.to, levelColor.color); - } - } - - this.subscribeAttributes(levelColorsDatasource, 'levelColors').then((subscription) => { - this.levelColorSourcesSubscription = subscription; - }); - - return predefineLevelColors; - } - - subscribeAttributes(datasources, typeAttributes) { - if (!datasources.length) { - return this.ctx.$scope.$injector.get('$q').when(null); - } - - let levelColorsSourcesSubscriptionOptions = { - datasources: datasources, - useDashboardTimewindow: false, - type: this.ctx.$scope.$injector.get('types').widgetType.latest.value, - callbacks: { - onDataUpdated: (subscription) => { - this.updateAttribute(subscription.data, typeAttributes); - } - } - }; - - return this.ctx.subscriptionApi.createSubscription(levelColorsSourcesSubscriptionOptions, true); - } - - updateAttribute(data, typeAttributes) { - for (let i = 0; i < data.length; i++) { - let keyData = data[i]; - if (keyData && keyData.data && keyData.data[0]) { - let attrValue = keyData.data[0][1]; - if (isFinite(attrValue)) { - for (let i = 0; i < keyData.dataKey.settings.length; i++) { - let setting = keyData.dataKey.settings[i]; - switch (typeAttributes) { - case 'levelColors': - this.localSettings.levelColors[setting.index] = { - value: attrValue, - color: setting.color - }; - break; - case 'ticks': - this.localSettings.ticks[setting] = attrValue; - break; - } - } - } - } - } - this.updateSetting(); - } - - updateSetting() { - this.gauge.options.ticks = this.localSettings.ticks; - this.gauge.options.levelColors = this.localSettings.levelColors; - this.gauge.options = CanvasDigitalGauge.configure(this.gauge.options); - this.gauge.update(); - } - - update() { - if (this.ctx.data.length > 0) { - var cellData = this.ctx.data[0]; - if (cellData.data.length > 0) { - var tvPair = cellData.data[cellData.data.length - 1]; - var timestamp; - if (this.localSettings.showTimestamp) { - timestamp = tvPair[0]; - var filter= this.ctx.$scope.$injector.get('$filter'); - var timestampDisplayValue = filter('date')(timestamp, this.localSettings.timestampFormat); - this.gauge.options.label = timestampDisplayValue; - } - var value = tvPair[1]; - if(value !== this.gauge.value) { - if (!this.ctx.settings.animation || this.ctx.isMobile) { - this.gauge._value = value; - } - this.gauge.value = value; - } else if (this.localSettings.showTimestamp && this.gauge.timestamp != timestamp) { - this.gauge.timestamp = timestamp; - } - } - } - } - - mobileModeChanged() { - var animation = this.ctx.settings.animation !== false && !this.ctx.isMobile; - this.gauge.update({animation: animation, isMobile: this.ctx.isMobile}); - } - - resize() { - this.gauge.update({width: this.ctx.width, height: this.ctx.height}); - } - - static get settingsSchema() { - return { - "schema": { - "type": "object", - "title": "Settings", - "properties": { - "minValue": { - "title": "Minimum value", - "type": "number", - "default": 0 - }, - "maxValue": { - "title": "Maximum value", - "type": "number", - "default": 100 - }, - "gaugeType": { - "title": "Gauge type", - "type": "string", - "default": "arc" - }, - "donutStartAngle": { - "title": "Angle to start from when in donut mode", - "type": "number", - "default": 90 - }, - "neonGlowBrightness": { - "title": "Neon glow effect brightness, (0-100), 0 - disable effect", - "type": "number", - "default": 0 - }, - "dashThickness": { - "title": "Thickness of the stripes, 0 - no stripes", - "type": "number", - "default": 0 - }, - "roundedLineCap": { - "title": "Display rounded line cap", - "type": "boolean", - "default": false - }, - "title": { - "title": "Gauge title", - "type": "string", - "default": null - }, - "showTitle": { - "title": "Show gauge title", - "type": "boolean", - "default": false - }, - "unitTitle": { - "title": "Unit title", - "type": "string", - "default": null - }, - "showUnitTitle": { - "title": "Show unit title", - "type": "boolean", - "default": false - }, - "showTimestamp": { - "title": "Show value timestamp", - "type": "boolean", - "default": false - }, - "timestampFormat": { - "title": "Timestamp format", - "type": "string", - "default": "yyyy-MM-dd HH:mm:ss" - }, - "showValue": { - "title": "Show value text", - "type": "boolean", - "default": true - }, - "showMinMax": { - "title": "Show min and max values", - "type": "boolean", - "default": true - }, - "gaugeWidthScale": { - "title": "Width of the gauge element", - "type": "number", - "default": 0.75 - }, - "defaultColor": { - "title": "Default color", - "type": "string", - "default": null - }, - "gaugeColor": { - "title": "Background color of the gauge element", - "type": "string", - "default": null - }, - "useFixedLevelColor": { - "title": "Use precise value for the color indicator", - "type": "boolean", - "default": false - }, - "levelColors": { - "title": "Colors of indicator, from lower to upper", - "type": "array", - "items": { - "title": "Color", - "type": "string" - } - }, - "fixedLevelColors": { - "title": "The colors for the indicator using boundary values", - "type": "array", - "items": { - "title": "levelColor", - "type": "object", - "properties": { - "from": { - "title": "From", - "type": "object", - "properties": { - "valueSource": { - "title": "[From] Value source", - "type": "string", - "default": "predefinedValue" - }, - "entityAlias": { - "title": "[From] Source entity alias", - "type": "string" - }, - "attribute": { - "title": "[From] Source entity attribute", - "type": "string" - }, - "value": { - "title": "[From] Value (if predefined value is selected)", - "type": "number" - } - } - }, - "to": { - "title": "To", - "type": "object", - "properties": { - "valueSource": { - "title": "[To] Value source", - "type": "string", - "default": "predefinedValue" - }, - "entityAlias": { - "title": "[To] Source entity alias", - "type": "string" - }, - "attribute": { - "title": "[To] Source entity attribute", - "type": "string" - }, - "value": { - "title": "[To] Value (if predefined value is selected)", - "type": "number" - } - } - }, - "color": { - "title": "Color", - "type": "string" - } - } - } - }, - "showTicks": { - "title": "Show ticks", - "type": "boolean", - "default": false - }, - "tickWidth": { - "title": "Width ticks", - "type": "number", - "default": 4 - }, - "colorTicks": { - "title": "Color ticks", - "type": "string", - "default": "#666" - }, - "ticksValue": { - "title": "The ticks predefined value", - "type": "array", - "items": { - "title": "tickValue", - "type": "object", - "properties": { - "valueSource": { - "title": "Value source", - "type": "string", - "default": "predefinedValue" - }, - "entityAlias": { - "title": "Source entity alias", - "type": "string" - }, - "attribute": { - "title": "Source entity attribute", - "type": "string" - }, - "value": { - "title": "Value (if predefined value is selected)", - "type": "number" - } - } - } - }, - "animation": { - "title": "Enable animation", - "type": "boolean", - "default": true - }, - "animationDuration": { - "title": "Animation duration", - "type": "number", - "default": 500 - }, - "animationRule": { - "title": "Animation rule", - "type": "string", - "default": "linear" - }, - "titleFont": { - "title": "Gauge title font", - "type": "object", - "properties": { - "family": { - "title": "Font family", - "type": "string", - "default": "Roboto" - }, - "size": { - "title": "Size", - "type": "number", - "default": 12 - }, - "style": { - "title": "Style", - "type": "string", - "default": "normal" - }, - "weight": { - "title": "Weight", - "type": "string", - "default": "500" - }, - "color": { - "title": "color", - "type": "string", - "default": null - } - } - }, - "labelFont": { - "title": "Font of label showing under value", - "type": "object", - "properties": { - "family": { - "title": "Font family", - "type": "string", - "default": "Roboto" - }, - "size": { - "title": "Size", - "type": "number", - "default": 8 - }, - "style": { - "title": "Style", - "type": "string", - "default": "normal" - }, - "weight": { - "title": "Weight", - "type": "string", - "default": "500" - }, - "color": { - "title": "color", - "type": "string", - "default": null - } - } - }, - "valueFont": { - "title": "Font of label showing current value", - "type": "object", - "properties": { - "family": { - "title": "Font family", - "type": "string", - "default": "Roboto" - }, - "size": { - "title": "Size", - "type": "number", - "default": 18 - }, - "style": { - "title": "Style", - "type": "string", - "default": "normal" - }, - "weight": { - "title": "Weight", - "type": "string", - "default": "500" - }, - "color": { - "title": "color", - "type": "string", - "default": null - } - } - }, - "minMaxFont": { - "title": "Font of minimum and maximum labels", - "type": "object", - "properties": { - "family": { - "title": "Font family", - "type": "string", - "default": "Roboto" - }, - "size": { - "title": "Size", - "type": "number", - "default": 10 - }, - "style": { - "title": "Style", - "type": "string", - "default": "normal" - }, - "weight": { - "title": "Weight", - "type": "string", - "default": "500" - }, - "color": { - "title": "color", - "type": "string", - "default": null - } - } - } - } - }, - "form": [ - "minValue", - "maxValue", - { - "key": "gaugeType", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "arc", - "label": "Arc" - }, - { - "value": "donut", - "label": "Donut" - }, - { - "value": "horizontalBar", - "label": "Horizontal bar" - }, - { - "value": "verticalBar", - "label": "Vertical bar" - } - ] - }, - "donutStartAngle", - "neonGlowBrightness", - "dashThickness", - "roundedLineCap", - "title", - "showTitle", - "unitTitle", - "showUnitTitle", - "showTimestamp", - "timestampFormat", - "showValue", - "showMinMax", - "gaugeWidthScale", - { - "key": "defaultColor", - "type": "color" - }, - { - "key": "gaugeColor", - "type": "color" - }, - "useFixedLevelColor", - { - "key": "levelColors", - "condition": "model.useFixedLevelColor !== true", - "items": [ - { - "key": "levelColors[]", - "type": "color" - } - ] - }, - { - "key": "fixedLevelColors", - "condition": "model.useFixedLevelColor === true", - "items": [ - { - "key": "fixedLevelColors[].from.valueSource", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "predefinedValue", - "label": "Predefined value (Default)" - }, - { - "value": "entityAttribute", - "label": "Value taken from entity attribute" - } - ] - }, - "fixedLevelColors[].from.value", - "fixedLevelColors[].from.entityAlias", - "fixedLevelColors[].from.attribute", - { - "key": "fixedLevelColors[].to.valueSource", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "predefinedValue", - "label": "Predefined value (Default)" - }, - { - "value": "entityAttribute", - "label": "Value taken from entity attribute" - } - ] - }, - "fixedLevelColors[].to.value", - "fixedLevelColors[].to.entityAlias", - "fixedLevelColors[].to.attribute", - { - "key": "fixedLevelColors[].color", - "type": "color" - } - ] - }, - "showTicks", - { - "key": "tickWidth", - "condition": "model.showTicks === true" - }, - { - "key": "colorTicks", - "condition": "model.showTicks === true", - "type": "color" - }, - { - "key": "ticksValue", - "condition": "model.showTicks === true", - "items": [ - { - "key": "ticksValue[].valueSource", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "predefinedValue", - "label": "Predefined value (Default)" - }, - { - "value": "entityAttribute", - "label": "Value taken from entity attribute" - } - ] - }, - "ticksValue[].value", - "ticksValue[].entityAlias", - "ticksValue[].attribute" - ] - }, - "animation", - "animationDuration", - { - "key": "animationRule", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "linear", - "label": "Linear" - }, - { - "value": "quad", - "label": "Quad" - }, - { - "value": "quint", - "label": "Quint" - }, - { - "value": "cycle", - "label": "Cycle" - }, - { - "value": "bounce", - "label": "Bounce" - }, - { - "value": "elastic", - "label": "Elastic" - }, - { - "value": "dequad", - "label": "Dequad" - }, - { - "value": "dequint", - "label": "Dequint" - }, - { - "value": "decycle", - "label": "Decycle" - }, - { - "value": "debounce", - "label": "Debounce" - }, - { - "value": "delastic", - "label": "Delastic" - } - ] - }, - { - "key": "titleFont", - "items": [ - "titleFont.family", - "titleFont.size", - { - "key": "titleFont.style", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "normal", - "label": "Normal" - }, - { - "value": "italic", - "label": "Italic" - }, - { - "value": "oblique", - "label": "Oblique" - } - ] - }, - { - "key": "titleFont.weight", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "normal", - "label": "Normal" - }, - { - "value": "bold", - "label": "Bold" - }, - { - "value": "bolder", - "label": "Bolder" - }, - { - "value": "lighter", - "label": "Lighter" - }, - { - "value": "100", - "label": "100" - }, - { - "value": "200", - "label": "200" - }, - { - "value": "300", - "label": "300" - }, - { - "value": "400", - "label": "400" - }, - { - "value": "500", - "label": "500" - }, - { - "value": "600", - "label": "600" - }, - { - "value": "700", - "label": "800" - }, - { - "value": "800", - "label": "800" - }, - { - "value": "900", - "label": "900" - } - ] - }, - { - "key": "titleFont.color", - "type": "color" - } - ] - }, - { - "key": "labelFont", - "items": [ - "labelFont.family", - "labelFont.size", - { - "key": "labelFont.style", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "normal", - "label": "Normal" - }, - { - "value": "italic", - "label": "Italic" - }, - { - "value": "oblique", - "label": "Oblique" - } - ] - }, - { - "key": "labelFont.weight", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "normal", - "label": "Normal" - }, - { - "value": "bold", - "label": "Bold" - }, - { - "value": "bolder", - "label": "Bolder" - }, - { - "value": "lighter", - "label": "Lighter" - }, - { - "value": "100", - "label": "100" - }, - { - "value": "200", - "label": "200" - }, - { - "value": "300", - "label": "300" - }, - { - "value": "400", - "label": "400" - }, - { - "value": "500", - "label": "500" - }, - { - "value": "600", - "label": "600" - }, - { - "value": "700", - "label": "800" - }, - { - "value": "800", - "label": "800" - }, - { - "value": "900", - "label": "900" - } - ] - }, - { - "key": "labelFont.color", - "type": "color" - } - ] - }, - { - "key": "valueFont", - "items": [ - "valueFont.family", - "valueFont.size", - { - "key": "valueFont.style", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "normal", - "label": "Normal" - }, - { - "value": "italic", - "label": "Italic" - }, - { - "value": "oblique", - "label": "Oblique" - } - ] - }, - { - "key": "valueFont.weight", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "normal", - "label": "Normal" - }, - { - "value": "bold", - "label": "Bold" - }, - { - "value": "bolder", - "label": "Bolder" - }, - { - "value": "lighter", - "label": "Lighter" - }, - { - "value": "100", - "label": "100" - }, - { - "value": "200", - "label": "200" - }, - { - "value": "300", - "label": "300" - }, - { - "value": "400", - "label": "400" - }, - { - "value": "500", - "label": "500" - }, - { - "value": "600", - "label": "600" - }, - { - "value": "700", - "label": "800" - }, - { - "value": "800", - "label": "800" - }, - { - "value": "900", - "label": "900" - } - ] - }, - { - "key": "valueFont.color", - "type": "color" - } - ] - }, - { - "key": "minMaxFont", - "items": [ - "minMaxFont.family", - "minMaxFont.size", - { - "key": "minMaxFont.style", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "normal", - "label": "Normal" - }, - { - "value": "italic", - "label": "Italic" - }, - { - "value": "oblique", - "label": "Oblique" - } - ] - }, - { - "key": "minMaxFont.weight", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "normal", - "label": "Normal" - }, - { - "value": "bold", - "label": "Bold" - }, - { - "value": "bolder", - "label": "Bolder" - }, - { - "value": "lighter", - "label": "Lighter" - }, - { - "value": "100", - "label": "100" - }, - { - "value": "200", - "label": "200" - }, - { - "value": "300", - "label": "300" - }, - { - "value": "400", - "label": "400" - }, - { - "value": "500", - "label": "500" - }, - { - "value": "600", - "label": "600" - }, - { - "value": "700", - "label": "800" - }, - { - "value": "800", - "label": "800" - }, - { - "value": "900", - "label": "900" - } - ] - }, - { - "key": "minMaxFont.color", - "type": "color" - } - ] - } - ] - }; - } -} -/* eslint-enable angular/angularelement */ diff --git a/ui/src/app/widget/lib/date-range-navigator/date-range-navigator.js b/ui/src/app/widget/lib/date-range-navigator/date-range-navigator.js deleted file mode 100644 index a159badc68..0000000000 --- a/ui/src/app/widget/lib/date-range-navigator/date-range-navigator.js +++ /dev/null @@ -1,301 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ -import widgetTpl from './date-range-navigator.tpl.html'; -import './date-range-navigator.scss'; -/* eslint-enable import/no-unresolved, import/default */ - -export default angular.module('thingsboard.widgets.dateRangeNavigator', []) - .directive('dateRangeNavigatorWidget', DateRangeNavigatorWidget) - .name; - -/*@ngInject*/ -function DateRangeNavigatorWidget() { - return { - restrict: "E", - scope: true, - bindToController: { - tableId: '=', - ctx: '=' - }, - controller: DateRangeNavigatorWidgetController, - controllerAs: 'vm', - templateUrl: widgetTpl - }; -} - -/*@ngInject*/ -function DateRangeNavigatorWidgetController($scope, $window, $filter) { - - let vm = this, - hour = 3600000, - day = 86400000, - week = 604800000, - month = 2629743000, - words = [ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", - "Ok", - 'Custom Date Range', - 'Date Range Template', - 'Today', - 'Yesterday', - 'This Week', - 'Last Week', - 'Month', - 'This Month', - 'Last Month', - 'Year', - 'This Year', - 'Last Year' - ], - firstUpdate = true; - - $scope.datesMap = { - hour: { - ts: hour, - label: "Hour" - }, - day: { - ts: day, - label: "Day" - }, - week: { - ts: week, - label: "Week" - }, - twoWeeks: { - ts: week * 2, - label: "2 weeks" - }, - month: { - ts: month, - label: "Month" - }, - threeMonths: { - ts: month * 3, - label: "3 months" - }, - sixMonths: { - ts: month * 6, - label: "6 months" - } - }; - $scope.advancedModel = {}; - $scope.endRestrictionDate = Date.now(); - $scope.localizationMap = getLocalizationMap(); - - $scope.changeInterval = changeInterval; - $scope.goForth = goForth; - $scope.goBack = goBack; - $scope.triggerChange = triggerChange; - - $scope.$watch('vm.ctx', function () { - if (vm.ctx && vm.ctx.dashboard.dashboardTimewindow) { - $scope.settings = vm.ctx.widgetConfig.settings; - let selection; - if ($scope.settings.useSessionStorage) { - selection = readFromStorage('date-range'); - } - if (selection) { - $scope.advancedModel = { - selectedTemplateName: selection.name, - dateStart: selection.start, - dateEnd: selection.end - }; - } else { - let end = new Date(); - end.setHours(23, 59, 59, 999); - - let formattedDate = getFormattedDate( - (end.getTime() + 1) - $scope.datesMap[$scope.settings.initialInterval || "week"].ts, - end.getTime() - ); - $scope.advancedModel = formattedDate; - } - $scope.selectedStepSize = $scope.datesMap[$scope.settings.stepSize || "day"].ts; - - widgetContextTimewindowSync(); - } - }); - - $scope.$on('dashboardTimewindowChanged', function () { - $scope.dashboardTimewindowChanged = true; - widgetContextTimewindowSync(); - }); - - function getLocalizationMap() { - let result = {}; - - words.forEach(function (key) { - result[key] = $filter('translate')('widgets.date-range-navigator.localizationMap.' + key); - }); - - return result; - } - - function triggerChange() { - updateTimewindow($scope.advancedModel.dateStart.getTime(), $scope.advancedModel.dateEnd.getTime() + day - 1); - } - - function widgetContextTimewindowSync() { - if (vm.ctx && vm.ctx.dashboardTimewindow && $scope.dashboardTimewindowChanged && - vm.ctx.dashboard.dashboardTimewindow.history && - vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow) { - - - if (!firstUpdate) { - updateAdvancedModel(); - } - updateDateInterval(); - if ($scope.settings.useSessionStorage) { - updateStorageDate(); - } - if (firstUpdate) { - updateTimewindow($scope.advancedModel.dateStart.getTime(), $scope.advancedModel.dateEnd.getTime()); - firstUpdate = false; - } - } - } - - function getFormattedDate(startTime, endTime) { - var template; - - let startDate = new Date(startTime); - let endDate = new Date(endTime); - - if (getDateDiff(startDate, endDate) === 0) { - template = $filter('date')(startDate, 'dd MMM yyyy'); - } else { - template = $filter('date')( - startDate, - 'dd' + (startDate.getMonth() !== endDate.getMonth() || startDate.getFullYear() !== endDate.getFullYear() ? ' MMM' : '') + (startDate.getFullYear() !== endDate.getFullYear() ? ' yyyy' : '') - ) - + ' - ' + - $filter('date')( - endDate, - 'dd MMM yyyy' - ); - } - - return { - selectedTemplateName: template, - dateStart: startDate, - dateEnd: endDate - }; - } - - function readFromStorage(itemKey) { - if ($window.sessionStorage.getItem(itemKey)) { - let selection = angular.fromJson($window.sessionStorage.getItem(itemKey)); - selection.start = new Date(parseInt(selection.start)); - selection.end = new Date(parseInt(selection.end)); - return selection; - } - - return undefined; - } - - function goForth() { - let startTime = vm.ctx.dashboard.dashboardTimewindow.history ? - vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.startTimeMs : - $scope.advancedModel.dateStart.getTime(); - let endTime = vm.ctx.dashboard.dashboardTimewindow.history ? - vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.endTimeMs : - $scope.advancedModel.dateEnd.getTime(); - updateTimewindow(startTime + $scope.selectedStepSize, endTime + $scope.selectedStepSize); - } - - function goBack() { - let startTime = vm.ctx.dashboard.dashboardTimewindow.history ? - vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.startTimeMs : - $scope.advancedModel.dateStart.getTime(); - let endTime = vm.ctx.dashboard.dashboardTimewindow.history ? - vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.endTimeMs : - $scope.advancedModel.dateEnd.getTime(); - updateTimewindow(startTime - $scope.selectedStepSize, endTime - $scope.selectedStepSize); - } - - function changeInterval() { - let endTime = vm.ctx.dashboard.dashboardTimewindow.history ? - vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.endTimeMs : - $scope.advancedModel.dateEnd.getTime(); - updateTimewindow(endTime - $scope.selectedDateInterval / 2, endTime + $scope.selectedDateInterval / 2); - } - - function getDateDiff(date1, date2) { - if (!date1 || !date2) return; - var _d1 = new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()), - _d2 = new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()); - return _d2 - _d1; - } - - function updateTimewindow(startTime, endTime) { - vm.ctx.dashboard.dashboardTimewindowApi.onUpdateTimewindow(startTime, endTime, 10); - } - - function updateDateInterval() { - let interval = $scope.advancedModel.dateEnd.getTime() - $scope.advancedModel.dateStart.getTime(); - - for (let i in $scope.datesMap) { - if (Object.prototype.hasOwnProperty.call($scope.datesMap, i)) { - if ($scope.datesMap[i].ts === interval || $scope.datesMap[i].ts === interval + 1 || $scope.datesMap[i].ts === interval - 1) { - $scope.selectedDateInterval = $scope.datesMap[i].ts; - $scope.customInterval = false; - return; - } - } - } - - $scope.selectedDateInterval = interval; - $scope.customInterval = {ts: interval, label: "Custom interval"}; - } - - function updateAdvancedModel() { - $scope.advancedModel = getFormattedDate(vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.startTimeMs, vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.endTimeMs); - } - - function updateStorageDate() { - saveIntoStorage('date-range', { - start: $scope.advancedModel.dateStart.getTime(), - end: $scope.advancedModel.dateEnd.getTime(), - name: $scope.advancedModel.selectedTemplateName - }); - } - - function saveIntoStorage(keyName, selection) { - if (selection) { - $window.sessionStorage.setItem(keyName, angular.toJson(selection)); - } - } -} \ No newline at end of file diff --git a/ui/src/app/widget/lib/date-range-navigator/date-range-navigator.scss b/ui/src/app/widget/lib/date-range-navigator/date-range-navigator.scss deleted file mode 100644 index e85c627504..0000000000 --- a/ui/src/app/widget/lib/date-range-navigator/date-range-navigator.scss +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.date-range-navigator-widget { - display: flex; - height: 100%; -} - -.date-range-navigator { - display: flex; - flex-wrap: wrap; - align-items: center; - justify-content: space-evenly; - width: 100%; - margin: auto; - - .drn__element { - display: flex; - flex-direction: row; - align-items: center; - max-width: 100%; - height: 60px; - margin: 4px 0; - } - - .navigation { - md-input-container { - margin: 0; - } - } - - .picker { - .picker__wrapper { - position: relative; - max-width: 100%; - padding: 2px; - - > label { - position: absolute; - right: -3px; - bottom: 100%; - left: 0; - padding-left: 3px; - color: #787878; - transform: scale(.75); - transform-origin: left bottom; - } - } - - .md-select-value { - min-width: 225px; - border-color: #e1e1e1; - - .md-select-icon { - color: #757575; - } - } - } - - &.short-mode { - display: block; - width: 90%; - - .drn__element { - width: 100%; - - md-input-container { - flex: 1; - } - } - - .picker { - .picker__wrapper { - width: 100%; - } - - .md-select-value { - min-width: initial; - } - } - - &.labels-hidden { - .drn__element { - margin: 0; - } - } - } - - &.long-mode { - &.labels-hidden { - .drn__element { - height: 36px; - } - } - } -} diff --git a/ui/src/app/widget/lib/date-range-navigator/date-range-navigator.tpl.html b/ui/src/app/widget/lib/date-range-navigator/date-range-navigator.tpl.html deleted file mode 100644 index 714a40b6f5..0000000000 --- a/ui/src/app/widget/lib/date-range-navigator/date-range-navigator.tpl.html +++ /dev/null @@ -1,72 +0,0 @@ - -
    -
    -
    - - -
    -
    - - - -
    - - keyboard_arrow_left - - - - - - {{'widgets.date-range-navigator.localizationMap.'+dateValue.label | translate}} - - - - - keyboard_arrow_right - -
    -
    diff --git a/ui/src/app/widget/lib/display-columns-panel.scss b/ui/src/app/widget/lib/display-columns-panel.scss deleted file mode 100644 index 7fbe98975e..0000000000 --- a/ui/src/app/widget/lib/display-columns-panel.scss +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-display-columns-panel { - min-width: 300px; - overflow: hidden; - background: #fff; - border-radius: 4px; - box-shadow: - 0 7px 8px -4px rgba(0, 0, 0, .2), - 0 13px 19px 2px rgba(0, 0, 0, .14), - 0 5px 24px 4px rgba(0, 0, 0, .12); - - md-content { - overflow: hidden; - background-color: #fff; - } -} diff --git a/ui/src/app/widget/lib/display-columns-panel.tpl.html b/ui/src/app/widget/lib/display-columns-panel.tpl.html deleted file mode 100644 index e77a6fb131..0000000000 --- a/ui/src/app/widget/lib/display-columns-panel.tpl.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - {{ column.title }} - - diff --git a/ui/src/app/widget/lib/entities-hierarchy-widget.js b/ui/src/app/widget/lib/entities-hierarchy-widget.js deleted file mode 100644 index 977bd7cea6..0000000000 --- a/ui/src/app/widget/lib/entities-hierarchy-widget.js +++ /dev/null @@ -1,529 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './entities-hierarchy-widget.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import entitiesHierarchyWidgetTemplate from './entities-hierarchy-widget.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -export default angular.module('thingsboard.widgets.entitiesHierarchyWidget', []) - .directive('tbEntitiesHierarchyWidget', EntitiesHierarchyWidget) - .name; - -/*@ngInject*/ -function EntitiesHierarchyWidget() { - return { - restrict: "E", - scope: true, - bindToController: { - hierarchyId: '=', - ctx: '=' - }, - controller: EntitiesHierarchyWidgetController, - controllerAs: 'vm', - templateUrl: entitiesHierarchyWidgetTemplate - }; -} - -/*@ngInject*/ -function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast, types, entityService, entityRelationService /*$filter, $mdMedia, $mdPanel, $document, $translate, $timeout, utils, types*/) { - var vm = this; - - vm.showData = true; - - vm.nodeEditCallbacks = {}; - - vm.nodeIdCounter = 0; - - vm.nodesMap = {}; - vm.pendingUpdateNodeTasks = {}; - - vm.query = { - search: null - }; - - vm.searchAction = { - name: 'action.search', - show: true, - onAction: function() { - vm.enterFilterMode(); - }, - icon: 'search' - }; - - vm.onNodesInserted = onNodesInserted; - vm.onNodeSelected = onNodeSelected; - vm.enterFilterMode = enterFilterMode; - vm.exitFilterMode = exitFilterMode; - vm.searchCallback = searchCallback; - - $scope.$watch('vm.ctx', function() { - if (vm.ctx && vm.ctx.defaultSubscription) { - vm.settings = vm.ctx.settings; - vm.widgetConfig = vm.ctx.widgetConfig; - vm.subscription = vm.ctx.defaultSubscription; - vm.datasources = vm.subscription.datasources; - initializeConfig(); - updateDatasources(); - } - }); - - $scope.$watch("vm.query.search", function(newVal, prevVal) { - if (!angular.equals(newVal, prevVal) && vm.query.search != null) { - updateSearchNodes(); - } - }); - - $scope.$on('entities-hierarchy-data-updated', function(event, hierarchyId) { - if (vm.hierarchyId == hierarchyId) { - if (vm.subscription) { - updateNodeData(vm.subscription.data); - } - } - }); - - function initializeConfig() { - - vm.ctx.widgetActions = [ vm.searchAction ]; - - var testNodeCtx = { - entity: { - id: { - entityType: 'DEVICE', - id: '123' - }, - name: 'TEST DEV1' - }, - data: {}, - level: 2 - }; - var parentNodeCtx = angular.copy(testNodeCtx); - parentNodeCtx.level = 1; - testNodeCtx.parentNodeCtx = parentNodeCtx; - - var nodeRelationQueryFunction = loadNodeCtxFunction(vm.settings.nodeRelationQueryFunction, 'nodeCtx', testNodeCtx); - var nodeIconFunction = loadNodeCtxFunction(vm.settings.nodeIconFunction, 'nodeCtx', testNodeCtx); - var nodeTextFunction = loadNodeCtxFunction(vm.settings.nodeTextFunction, 'nodeCtx', testNodeCtx); - var nodeDisabledFunction = loadNodeCtxFunction(vm.settings.nodeDisabledFunction, 'nodeCtx', testNodeCtx); - var nodeOpenedFunction = loadNodeCtxFunction(vm.settings.nodeOpenedFunction, 'nodeCtx', testNodeCtx); - var nodeHasChildrenFunction = loadNodeCtxFunction(vm.settings.nodeHasChildrenFunction, 'nodeCtx', testNodeCtx); - - var testNodeCtx2 = angular.copy(testNodeCtx); - testNodeCtx2.entity.name = 'TEST DEV2'; - - var nodesSortFunction = loadNodeCtxFunction(vm.settings.nodesSortFunction, 'nodeCtx1,nodeCtx2', testNodeCtx, testNodeCtx2); - - vm.nodeRelationQueryFunction = nodeRelationQueryFunction || defaultNodeRelationQueryFunction; - vm.nodeIconFunction = nodeIconFunction || defaultNodeIconFunction; - vm.nodeTextFunction = nodeTextFunction || ((nodeCtx) => nodeCtx.entity.name); - vm.nodeDisabledFunction = nodeDisabledFunction || (() => false); - vm.nodeOpenedFunction = nodeOpenedFunction || defaultNodeOpenedFunction; - vm.nodeHasChildrenFunction = nodeHasChildrenFunction || (() => true); - vm.nodesSortFunction = nodesSortFunction || defaultSortFunction; - } - - function loadNodeCtxFunction(functionBody, argNames, ...args) { - var nodeCtxFunction = null; - if (angular.isDefined(functionBody) && functionBody.length) { - try { - nodeCtxFunction = new Function(argNames, functionBody); - var res = nodeCtxFunction.apply(null, args); - if (angular.isUndefined(res)) { - nodeCtxFunction = null; - } - } catch (e) { - nodeCtxFunction = null; - } - } - return nodeCtxFunction; - } - - function enterFilterMode () { - vm.query.search = ''; - vm.ctx.hideTitlePanel = true; - $timeout(()=>{ - angular.element(vm.ctx.$container).find('.searchInput').focus(); - }) - } - - function exitFilterMode () { - vm.query.search = null; - updateSearchNodes(); - vm.ctx.hideTitlePanel = false; - } - - function searchCallback (searchText, node) { - var theNode = vm.nodesMap[node.id]; - if (theNode && theNode.data.searchText) { - return theNode.data.searchText.includes(searchText.toLowerCase()); - } - return false; - } - - function updateDatasources() { - vm.loadNodes = loadNodes; - } - - function updateSearchNodes() { - if (vm.query.search != null) { - vm.nodeEditCallbacks.search(vm.query.search); - } else { - vm.nodeEditCallbacks.clearSearch(); - } - } - - function onNodesInserted(nodes/*, parent*/) { - if (nodes) { - nodes.forEach((nodeId) => { - var task = vm.pendingUpdateNodeTasks[nodeId]; - if (task) { - task(); - delete vm.pendingUpdateNodeTasks[nodeId]; - } - }); - } - } - - function onNodeSelected(node, event) { - var nodeId; - if (!node) { - nodeId = -1; - } else { - nodeId = node.id; - } - if (nodeId !== -1) { - var selectedNode = vm.nodesMap[nodeId]; - if (selectedNode) { - var descriptors = vm.ctx.actionsApi.getActionDescriptors('nodeSelected'); - if (descriptors.length) { - var entity = selectedNode.data.nodeCtx.entity; - vm.ctx.actionsApi.handleWidgetAction(event, descriptors[0], entity.id, entity.name, { nodeCtx: selectedNode.data.nodeCtx }); - } - } - } - } - - function updateNodeData(subscriptionData) { - var affectedNodes = []; - if (subscriptionData) { - for (var i=0;i { - var node = vm.nodeEditCallbacks.getNode(nodeId); - if (node) { - updateNodeStyle(vm.nodesMap[nodeId]); - } else { - vm.pendingUpdateNodeTasks[nodeId] = () => { - updateNodeStyle(vm.nodesMap[nodeId]); - }; - } - }); - } - - function updateNodeStyle(node) { - var newText = prepareNodeText(node); - if (!angular.equals(node.text, newText)) { - node.text = newText; - vm.nodeEditCallbacks.updateNode(node.id, node.text); - } - var newDisabled = vm.nodeDisabledFunction(node.data.nodeCtx); - if (!angular.equals(node.state.disabled, newDisabled)) { - node.state.disabled = newDisabled; - if (node.state.disabled) { - vm.nodeEditCallbacks.disableNode(node.id); - } else { - vm.nodeEditCallbacks.enableNode(node.id); - } - } - var newHasChildren = vm.nodeHasChildrenFunction(node.data.nodeCtx); - if (!angular.equals(node.children, newHasChildren)) { - node.children = newHasChildren; - vm.nodeEditCallbacks.setNodeHasChildren(node.id, node.children); - } - } - - function prepareNodeText(node) { - var nodeIcon = prepareNodeIcon(node.data.nodeCtx); - var nodeText = vm.nodeTextFunction(node.data.nodeCtx); - node.data.searchText = nodeText ? nodeText.replace(/<[^>]+>/g, '').toLowerCase() : ""; - return nodeIcon + nodeText; - } - - function loadNodes(node, cb) { - if (node.id === '#') { - var tasks = []; - for (var i=0;i { - cb(prepareNodes(nodes)); - updateNodeData(vm.subscription.data); - }); - } else { - if (node.data && node.data.nodeCtx.entity && node.data.nodeCtx.entity.id && node.data.nodeCtx.entity.id.entityType !== 'function') { - var relationQuery = prepareNodeRelationQuery(node.data.nodeCtx); - entityRelationService.findByQuery(relationQuery, {ignoreErrors: true, ignoreLoading: true}).then( - (entityRelations) => { - var tasks = []; - for (var i=0;i { - cb(prepareNodes(nodes)); - }); - }, - (error) => { - var errorText = "Failed to get relations!"; - if (error && error.status === 400) { - errorText = "Invalid relations query returned by 'Node relations query function'! Please check widget configuration!"; - } - showError(errorText); - } - ); - } else { - cb([]); - } - } - } - - function showError(errorText) { - var toastParent = angular.element('.tb-entities-hierarchy', $element); - toast.showError(errorText, toastParent, 'bottom left'); - } - - function prepareNodes(nodes) { - nodes = nodes.filter((node) => node !== null); - nodes.sort((node1, node2) => vm.nodesSortFunction(node1.data.nodeCtx, node2.data.nodeCtx)); - return nodes; - } - - function datasourceToNode(datasource, parentNodeCtx) { - var deferred = $q.defer(); - resolveEntity(datasource).then( - (entity) => { - if (entity != null) { - var node = { - id: ++vm.nodeIdCounter - }; - vm.nodesMap[node.id] = node; - datasource.nodeId = node.id; - node.icon = false; - var nodeCtx = { - parentNodeCtx: parentNodeCtx, - entity: entity, - data: {} - }; - nodeCtx.level = parentNodeCtx ? parentNodeCtx.level + 1 : 1; - node.data = { - datasource: datasource, - nodeCtx: nodeCtx - }; - node.state = { - disabled: vm.nodeDisabledFunction(node.data.nodeCtx), - opened: vm.nodeOpenedFunction(node.data.nodeCtx) - }; - node.text = prepareNodeText(node); - node.children = vm.nodeHasChildrenFunction(node.data.nodeCtx); - deferred.resolve(node); - } else { - deferred.resolve(null); - } - } - ); - return deferred.promise; - } - - function entityIdToNode(entityType, entityId, parentDatasource, parentNodeCtx) { - var deferred = $q.defer(); - var datasource = { - dataKeys: parentDatasource.dataKeys, - type: types.datasourceType.entity, - entityType: entityType, - entityId: entityId - }; - datasourceToNode(datasource, parentNodeCtx).then( - (node) => { - if (node != null) { - var subscriptionOptions = { - type: types.widgetType.latest.value, - datasources: [datasource], - callbacks: { - onDataUpdated: (subscription) => { - updateNodeData(subscription.data); - } - } - }; - vm.ctx.subscriptionApi.createSubscription(subscriptionOptions, true).then( - (/*subscription*/) => { - deferred.resolve(node); - } - ); - } else { - deferred.resolve(node); - } - } - ); - return deferred.promise; - } - - function resolveEntity(datasource) { - var deferred = $q.defer(); - if (datasource.type === types.datasourceType.function) { - var entity = { - id: { - entityType: "function" - }, - name: datasource.name - } - deferred.resolve(entity); - } else { - entityService.getEntity(datasource.entityType, datasource.entityId, {ignoreLoading: true}).then( - (entity) => { - deferred.resolve(entity); - }, - () => { - deferred.resolve(null); - } - ); - } - return deferred.promise; - } - - - function prepareNodeRelationQuery(nodeCtx) { - var relationQuery = vm.nodeRelationQueryFunction(nodeCtx); - if (relationQuery && relationQuery === 'default') { - relationQuery = defaultNodeRelationQueryFunction(nodeCtx); - } - return relationQuery; - } - - function defaultNodeRelationQueryFunction(nodeCtx) { - var entity = nodeCtx.entity; - var query = { - parameters: { - rootId: entity.id.id, - rootType: entity.id.entityType, - direction: types.entitySearchDirection.from, - relationTypeGroup: "COMMON", - maxLevel: 1 - }, - filters: [ - { - relationType: "Contains", - entityTypes: [] - } - ] - }; - return query; - } - - function prepareNodeIcon(nodeCtx) { - var iconInfo = vm.nodeIconFunction(nodeCtx); - if (iconInfo && iconInfo === 'default') { - iconInfo = defaultNodeIconFunction(nodeCtx); - } - if (iconInfo && (iconInfo.iconUrl || iconInfo.materialIcon)) { - if (iconInfo.materialIcon) { - return materialIconHtml(iconInfo.materialIcon); - } else { - return iconUrlHtml(iconInfo.iconUrl); - } - } else { - return ""; - } - } - - function materialIconHtml(materialIcon) { - return ''+materialIcon+''; - } - - function iconUrlHtml(iconUrl) { - return '
     
    '; - } - - function defaultNodeIconFunction(nodeCtx) { - var materialIcon = 'insert_drive_file'; - var entity = nodeCtx.entity; - if (entity && entity.id && entity.id.entityType) { - switch (entity.id.entityType) { - case 'function': - materialIcon = 'functions'; - break; - case types.entityType.device: - materialIcon = 'devices_other'; - break; - case types.entityType.asset: - materialIcon = 'domain'; - break; - case types.entityType.tenant: - materialIcon = 'supervisor_account'; - break; - case types.entityType.customer: - materialIcon = 'supervisor_account'; - break; - case types.entityType.user: - materialIcon = 'account_circle'; - break; - case types.entityType.dashboard: - materialIcon = 'dashboards'; - break; - case types.entityType.alarm: - materialIcon = 'notifications_active'; - break; - case types.entityType.entityView: - materialIcon = 'view_quilt'; - break; - } - } - return { - materialIcon: materialIcon - }; - } - - function defaultNodeOpenedFunction(nodeCtx) { - return nodeCtx.level <= 4; - } - - function defaultSortFunction(nodeCtx1, nodeCtx2) { - var result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType); - if (result === 0) { - result = nodeCtx1.entity.name.localeCompare(nodeCtx2.entity.name); - } - return result; - } -} diff --git a/ui/src/app/widget/lib/entities-hierarchy-widget.scss b/ui/src/app/widget/lib/entities-hierarchy-widget.scss deleted file mode 100644 index d6b0c81724..0000000000 --- a/ui/src/app/widget/lib/entities-hierarchy-widget.scss +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-has-timewindow { - .tb-entities-hierarchy { - md-toolbar { - min-height: 60px; - max-height: 60px; - } - } -} - -.tb-entities-hierarchy { - md-toolbar { - min-height: 39px; - max-height: 39px; - } - - .tb-entities-nav-tree-panel { - overflow-x: auto; - overflow-y: auto; - - .tb-nav-tree-container { - &.jstree-proton { - .jstree-anchor { - div.node-icon { - display: inline-block; - width: 22px; - height: 22px; - margin-right: 2px; - margin-bottom: 2px; - background-color: transparent; - background-repeat: no-repeat; - background-attachment: scroll; - background-position: center center; - background-size: 18px 18px; - } - - md-icon.node-icon { - width: 22px; - min-width: 22px; - height: 22px; - min-height: 22px; - margin-right: 2px; - margin-bottom: 2px; - color: inherit; - - &.material-icons { /* stylelint-disable-line selector-max-class */ - font-size: 18px; - line-height: 22px; - text-align: center; - } - } - - &.jstree-hovered:not(.jstree-clicked), - &.jstree-disabled { - div.node-icon { /* stylelint-disable-line selector-max-class */ - opacity: .5; - } - } - } - } - } - } -} - -@media (max-width: 768px) { - .tb-entities-hierarchy { - .tb-entities-nav-tree-panel { - .tb-nav-tree-container { - &.jstree-proton-responsive { - .jstree-anchor { - div.node-icon { - width: 40px; - height: 40px; - margin: 0; - background-size: 24px 24px; - } - - md-icon.node-icon { - width: 40px; - min-width: 40px; - height: 40px; - min-height: 40px; - margin: 0; - - &.material-icons { /* stylelint-disable-line selector-max-class */ - font-size: 24px; - line-height: 40px; - } - } - } - } - } - } - } -} diff --git a/ui/src/app/widget/lib/entities-hierarchy-widget.tpl.html b/ui/src/app/widget/lib/entities-hierarchy-widget.tpl.html deleted file mode 100644 index 1ae4dbba6a..0000000000 --- a/ui/src/app/widget/lib/entities-hierarchy-widget.tpl.html +++ /dev/null @@ -1,51 +0,0 @@ - -
    -
    - -
    - - search - - {{'entity.search' | translate}} - - - - - - - - close - - {{ 'action.close' | translate }} - - -
    -
    -
    - -
    -
    -
    diff --git a/ui/src/app/widget/lib/entities-table-widget.js b/ui/src/app/widget/lib/entities-table-widget.js deleted file mode 100644 index e8cedd640e..0000000000 --- a/ui/src/app/widget/lib/entities-table-widget.js +++ /dev/null @@ -1,633 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './entities-table-widget.scss'; -import './display-columns-panel.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import entitiesTableWidgetTemplate from './entities-table-widget.tpl.html'; -//import entityDetailsDialogTemplate from './entitiy-details-dialog.tpl.html'; -import displayColumnsPanelTemplate from './display-columns-panel.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -import tinycolor from 'tinycolor2'; -import cssjs from '../../../vendor/css.js/css'; - -export default angular.module('thingsboard.widgets.entitiesTableWidget', []) - .directive('tbEntitiesTableWidget', EntitiesTableWidget) - .name; - -/*@ngInject*/ -function EntitiesTableWidget() { - return { - restrict: "E", - scope: true, - bindToController: { - tableId: '=', - ctx: '=' - }, - controller: EntitiesTableWidgetController, - controllerAs: 'vm', - templateUrl: entitiesTableWidgetTemplate - }; -} - -/*@ngInject*/ -function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $mdPanel, $document, $translate, $timeout, utils, types) { - var vm = this; - - vm.stylesInfo = {}; - vm.contentsInfo = {}; - vm.columnWidth = {}; - - vm.showData = true; - vm.hasData = false; - - vm.entities = []; - vm.entitiesCount = 0; - - vm.datasources = null; - vm.allEntities = null; - - vm.currentEntity = null; - - vm.displayEntityName = true; - vm.entityNameColumnTitle = ''; - vm.displayEntityLabel = false; - vm.entityLabelColumnTitle = ''; - vm.displayEntityType = true; - vm.actionCellDescriptors = []; - vm.displayPagination = true; - vm.defaultPageSize = 10; - vm.defaultSortOrder = "'entityName'"; - - vm.query = { - order: vm.defaultSortOrder, - limit: vm.defaultPageSize, - page: 1, - search: null - }; - - vm.searchAction = { - name: 'action.search', - show: true, - onAction: function() { - vm.enterFilterMode(); - }, - icon: 'search' - }; - - let columnDisplayAction = { - name: 'entity.columns-to-display', - show: true, - onAction: function($event) { - vm.editColumnsToDisplay($event); - }, - icon: 'view_column' - }; - - vm.enterFilterMode = enterFilterMode; - vm.exitFilterMode = exitFilterMode; - vm.onReorder = onReorder; - vm.onPaginate = onPaginate; - vm.onRowClick = onRowClick; - vm.onActionButtonClick = onActionButtonClick; - vm.isCurrent = isCurrent; - - vm.cellStyle = cellStyle; - vm.cellContent = cellContent; - - vm.editColumnsToDisplay = editColumnsToDisplay; - - $scope.$watch('vm.ctx', function() { - if (vm.ctx && vm.ctx.defaultSubscription) { - vm.settings = vm.ctx.settings; - vm.widgetConfig = vm.ctx.widgetConfig; - vm.subscription = vm.ctx.defaultSubscription; - vm.datasources = vm.subscription.datasources; - initializeConfig(); - updateDatasources(); - updateEntities(); - } - }); - - $scope.$watch("vm.query.search", function(newVal, prevVal) { - if (!angular.equals(newVal, prevVal) && vm.query.search != null) { - updateEntities(); - } - }); - - $scope.$on('entities-table-data-updated', function(event, tableId) { - if (vm.tableId == tableId) { - if (vm.subscription) { - updateEntitiesData(vm.subscription.data); - updateEntities(); - $scope.$digest(); - } - } - }); - - $scope.$watch(function() { return $mdMedia('gt-xs'); }, function(isGtXs) { - vm.isGtXs = isGtXs; - }); - - $scope.$watch(function() { return $mdMedia('gt-md'); }, function(isGtMd) { - vm.isGtMd = isGtMd; - if (vm.isGtMd) { - vm.limitOptions = [vm.defaultPageSize, vm.defaultPageSize*2, vm.defaultPageSize*3]; - } else { - vm.limitOptions = null; - } - }); - - function initializeConfig() { - - vm.ctx.widgetActions = [ vm.searchAction, columnDisplayAction ]; - - vm.actionCellDescriptors = vm.ctx.actionsApi.getActionDescriptors('actionCellButton'); - - if (vm.settings.entitiesTitle && vm.settings.entitiesTitle.length) { - vm.entitiesTitle = utils.customTranslation(vm.settings.entitiesTitle, vm.settings.entitiesTitle); - } else { - vm.entitiesTitle = $translate.instant('entity.entities'); - } - - vm.ctx.widgetTitle = vm.entitiesTitle; - - vm.searchAction.show = angular.isDefined(vm.settings.enableSearch) ? vm.settings.enableSearch : true; - vm.displayEntityName = angular.isDefined(vm.settings.displayEntityName) ? vm.settings.displayEntityName : true; - vm.displayEntityLabel = angular.isDefined(vm.settings.displayEntityLabel) ? vm.settings.displayEntityLabel : false; - columnDisplayAction.show = angular.isDefined(vm.settings.enableSelectColumnDisplay) ? vm.settings.enableSelectColumnDisplay : true; - - if (vm.settings.entityNameColumnTitle && vm.settings.entityNameColumnTitle.length) { - vm.entityNameColumnTitle = utils.customTranslation(vm.settings.entityNameColumnTitle, vm.settings.entityNameColumnTitle); - } else { - vm.entityNameColumnTitle = $translate.instant('entity.entity-name'); - } - - if (vm.settings.entityLabelColumnTitle && vm.settings.entityLabelColumnTitle.length) { - vm.entityLabelColumnTitle = utils.customTranslation(vm.settings.entityLabelColumnTitle, vm.settings.entityLabelColumnTitle); - } else { - vm.entityLabelColumnTitle = $translate.instant('entity.entity-label'); - } - - vm.displayEntityType = angular.isDefined(vm.settings.displayEntityType) ? vm.settings.displayEntityType : true; - vm.displayPagination = angular.isDefined(vm.settings.displayPagination) ? vm.settings.displayPagination : true; - - var pageSize = vm.settings.defaultPageSize; - if (angular.isDefined(pageSize) && angular.isNumber(pageSize) && pageSize > 0) { - vm.defaultPageSize = pageSize; - } - - if (vm.settings.defaultSortOrder && vm.settings.defaultSortOrder.length) { - vm.defaultSortOrder = vm.settings.defaultSortOrder; - if (vm.settings.defaultSortOrder.charAt(0) === "-") { - vm.defaultSortOrder = '-"' + utils.customTranslation(vm.settings.defaultSortOrder.substring(1), vm.settings.defaultSortOrder.substring(1)) + '"'; - } else { - vm.defaultSortOrder = '"' + utils.customTranslation(vm.settings.defaultSortOrder, vm.settings.defaultSortOrder) + '"'; - } - } - - vm.query.order = vm.defaultSortOrder; - vm.query.limit = vm.defaultPageSize; - if (vm.isGtMd) { - vm.limitOptions = [vm.defaultPageSize, vm.defaultPageSize*2, vm.defaultPageSize*3]; - } else { - vm.limitOptions = null; - } - - var origColor = vm.widgetConfig.color || 'rgba(0, 0, 0, 0.87)'; - var defaultColor = tinycolor(origColor); - var mdDark = defaultColor.setAlpha(0.87).toRgbString(); - var mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString(); - var mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString(); - //var mdDarkIcon = mdDarkSecondary; - var mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString(); - - //md-icon.md-default-theme, md-icon { - - var cssString = 'table.md-table th.md-column {\n'+ - 'color: ' + mdDarkSecondary + ';\n'+ - '}\n'+ - 'table.md-table th.md-column.md-checkbox-column md-checkbox:not(.md-checked) .md-icon {\n'+ - 'border-color: ' + mdDarkSecondary + ';\n'+ - '}\n'+ - 'table.md-table th.md-column md-icon.md-sort-icon {\n'+ - 'color: ' + mdDarkDisabled + ';\n'+ - '}\n'+ - 'table.md-table th.md-column.md-active, table.md-table th.md-column.md-active md-icon {\n'+ - 'color: ' + mdDark + ';\n'+ - '}\n'+ - 'table.md-table td.md-cell {\n'+ - 'color: ' + mdDark + ';\n'+ - 'border-top: 1px '+mdDarkDivider+' solid;\n'+ - '}\n'+ - 'table.md-table td.md-cell.md-checkbox-cell md-checkbox:not(.md-checked) .md-icon {\n'+ - 'border-color: ' + mdDarkSecondary + ';\n'+ - '}\n'+ - 'table.md-table td.md-cell.tb-action-cell button.md-icon-button md-icon {\n'+ - 'color: ' + mdDarkSecondary + ';\n'+ - '}\n'+ - 'table.md-table td.md-cell.md-placeholder {\n'+ - 'color: ' + mdDarkDisabled + ';\n'+ - '}\n'+ - 'table.md-table td.md-cell md-select > .md-select-value > span.md-select-icon {\n'+ - 'color: ' + mdDarkSecondary + ';\n'+ - '}\n'+ - '.md-table-pagination {\n'+ - 'color: ' + mdDarkSecondary + ';\n'+ - 'border-top: 1px '+mdDarkDivider+' solid;\n'+ - '}\n'+ - '.md-table-pagination .buttons md-icon {\n'+ - 'color: ' + mdDarkSecondary + ';\n'+ - '}\n'+ - '.md-table-pagination md-select:not([disabled]):focus .md-select-value {\n'+ - 'color: ' + mdDarkSecondary + ';\n'+ - '}'; - - var cssParser = new cssjs(); - cssParser.testMode = false; - var namespace = 'entities-table-' + hashCode(cssString); - cssParser.cssPreviewNamespace = namespace; - cssParser.createStyleElement(namespace, cssString); - $element.addClass(namespace); - - function hashCode(str) { - var hash = 0; - var i, char; - if (str.length === 0) return hash; - for (i = 0; i < str.length; i++) { - char = str.charCodeAt(i); - hash = ((hash << 5) - hash) + char; - hash = hash & hash; - } - return hash; - } - } - - function enterFilterMode () { - vm.query.search = ''; - vm.ctx.hideTitlePanel = true; - $timeout(()=>{ - angular.element(vm.ctx.$container).find('.searchInput').focus(); - }) - } - - function exitFilterMode () { - vm.query.search = null; - updateEntities(); - vm.ctx.hideTitlePanel = false; - } - - function onReorder () { - updateEntities(); - } - - function onPaginate () { - updateEntities(); - } - - function onRowClick($event, entity, isDouble) { - if ($event) { - $event.stopPropagation(); - } - if (vm.currentEntity != entity) { - vm.currentEntity = entity; - } - var actionSourceId = isDouble ? 'rowDoubleClick' : 'rowClick'; - var descriptors = vm.ctx.actionsApi.getActionDescriptors(actionSourceId); - if (descriptors.length) { - var entityId, entityName, entityLabel; - if (vm.currentEntity) { - entityId = vm.currentEntity.id; - entityName = vm.currentEntity.entityName; - entityLabel = vm.currentEntity.entityLabel; - } - vm.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName, null, entityLabel); - } - } - - function onActionButtonClick($event, entity, actionDescriptor) { - if ($event) { - $event.stopPropagation(); - } - var entityId, entityName, entityLabel; - if (entity) { - entityId = entity.id; - entityName = entity.entityName; - entityLabel = entity.entityLabel; - } - vm.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName, null, entityLabel); - } - - function isCurrent(entity) { - return (vm.currentEntity && entity && vm.currentEntity.id && entity.id) && - (vm.currentEntity.id.id === entity.id.id); - } - - function updateEntities() { - var result = $filter('orderBy')(vm.allEntities, vm.query.order); - if (vm.query.search != null) { - result = $filter('filter')(result, {$: vm.query.search}); - } - vm.entitiesCount = result.length; - - if (vm.displayPagination) { - var startIndex = vm.query.limit * (vm.query.page - 1); - vm.entities = result.slice(startIndex, startIndex + vm.query.limit); - } else { - vm.entities = result; - } - } - - function cellStyle(entity, key) { - var style = {}; - if (entity && key) { - var styleInfo = vm.stylesInfo[key.label]; - var value = getEntityValue(entity, key); - if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { - try { - style = styleInfo.cellStyleFunction(value); - } catch (e) { - style = {}; - } - } else { - style = defaultStyle(key, value); - } - } - if (!style.width) { - var columnWidth = vm.columnWidth[key.label]; - if(columnWidth !== "0px") { - style.width = columnWidth; - } else { - style.width = "auto"; - } - } - return style; - } - - function cellContent(entity, key) { - var strContent = ''; - if (entity && key) { - var contentInfo = vm.contentsInfo[key.label]; - var value = getEntityValue(entity, key); - if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { - if (angular.isDefined(value)) { - strContent = '' + value; - } - var content = strContent; - try { - content = contentInfo.cellContentFunction(value, entity, $filter); - } catch (e) { - content = strContent; - } - } else { - var decimals = (contentInfo.decimals || contentInfo.decimals === 0) ? contentInfo.decimals : vm.widgetConfig.decimals; - var units = contentInfo.units || vm.widgetConfig.units; - content = vm.ctx.utils.formatValue(value, decimals, units, true); - } - return content; - } else { - return strContent; - } - } - - function defaultStyle(/*key, value*/) { - return {}; - } - - const getDescendantProp = (obj, path) => ( - path.split('.').reduce((acc, part) => acc && acc[part], obj) - ); - - function getEntityValue(entity, key) { - return getDescendantProp(entity, key.label); - } - - function updateEntitiesData(data) { - if (vm.allEntities) { - for (var i=0;i 1) { - var value = keyData[0][1]; - entity[dataKey.label] = value; - } else { - entity[dataKey.label] = ''; - } - } - } - } - } - - function editColumnsToDisplay($event) { - var element = angular.element($event.target); - var position = $mdPanel.newPanelPosition() - .relativeTo(element) - .addPanelPosition($mdPanel.xPosition.ALIGN_END, $mdPanel.yPosition.BELOW); - var config = { - attachTo: angular.element($document[0].body), - controller: DisplayColumnsPanelController, - controllerAs: 'vm', - templateUrl: displayColumnsPanelTemplate, - panelClass: 'tb-display-columns-panel', - position: position, - fullscreen: false, - locals: { - 'columns': vm.columns - }, - openFrom: $event, - clickOutsideToClose: true, - escapeToClose: true, - focusOnOpen: false - }; - $mdPanel.open(config); - } - - function updateDatasources() { - - vm.stylesInfo = {}; - vm.contentsInfo = {}; - vm.columnWidth = {}; - vm.dataKeys = []; - vm.columns = []; - vm.allEntities = []; - - var datasource; - var dataKey; - - datasource = vm.datasources[0]; - - vm.ctx.widgetTitle = utils.createLabelFromDatasource(datasource, vm.entitiesTitle); - - if (vm.displayEntityName) { - vm.columns.push( - { - name: 'entityName', - label: 'entityName', - title: vm.entityNameColumnTitle, - display: true - } - ); - vm.contentsInfo['entityName'] = { - useCellContentFunction: false - }; - vm.stylesInfo['entityName'] = { - useCellStyleFunction: false - }; - vm.columnWidth['entityName'] = '0px'; - } - - if (vm.displayEntityLabel) { - vm.columns.push( - { - name: 'entityLabel', - label: 'entityLabel', - title: vm.entityLabelColumnTitle, - display: true - } - ); - vm.contentsInfo['entityLabel'] = { - useCellContentFunction: false - }; - vm.stylesInfo['entityLabel'] = { - useCellStyleFunction: false - }; - vm.columnWidth['entityLabel'] = '0px'; - } - - if (vm.displayEntityType) { - vm.columns.push( - { - name: 'entityType', - label: 'entityType', - title: $translate.instant('entity.entity-type'), - display: true - } - ); - vm.contentsInfo['entityType'] = { - useCellContentFunction: false - }; - vm.stylesInfo['entityType'] = { - useCellStyleFunction: false - }; - vm.columnWidth['entityType'] = '0px'; - } - - for (var d = 0; d < datasource.dataKeys.length; d++ ) { - dataKey = angular.copy(datasource.dataKeys[d]); - if (dataKey.type == types.dataKeyType.function) { - dataKey.name = dataKey.label; - } - vm.dataKeys.push(dataKey); - - dataKey.title = utils.customTranslation(dataKey.label, dataKey.label); - - var keySettings = dataKey.settings; - - var cellStyleFunction = null; - var useCellStyleFunction = false; - - if (keySettings.useCellStyleFunction === true) { - if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) { - try { - cellStyleFunction = new Function('value', keySettings.cellStyleFunction); - useCellStyleFunction = true; - } catch (e) { - cellStyleFunction = null; - useCellStyleFunction = false; - } - } - } - - vm.stylesInfo[dataKey.label] = { - useCellStyleFunction: useCellStyleFunction, - cellStyleFunction: cellStyleFunction - }; - - var cellContentFunction = null; - var useCellContentFunction = false; - - if (keySettings.useCellContentFunction === true) { - if (angular.isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) { - try { - cellContentFunction = new Function('value, entity, filter', keySettings.cellContentFunction); - useCellContentFunction = true; - } catch (e) { - cellContentFunction = null; - useCellContentFunction = false; - } - } - } - - vm.contentsInfo[dataKey.label] = { - useCellContentFunction: useCellContentFunction, - cellContentFunction: cellContentFunction, - units: dataKey.units, - decimals: dataKey.decimals - }; - - var columnWidth = angular.isDefined(keySettings.columnWidth) ? keySettings.columnWidth : '0px'; - vm.columnWidth[dataKey.label] = columnWidth; - - dataKey.display = true; - vm.columns.push(dataKey); - } - - for (var i=0;i -
    -
    - -
    - - search - - {{'entity.search' | translate}} - - - - - - - - close - - {{ 'action.close' | translate }} - - -
    -
    - - - - - - - - - - - - - - -
    {{ column.title }}
    - - - {{actionDescriptor.icon}} - - {{ actionDescriptor.displayName }} - - -
    - - entity.no-entities-prompt -
    - - -
    - entity.no-data -
    diff --git a/ui/src/app/widget/lib/extensions-table-widget.js b/ui/src/app/widget/lib/extensions-table-widget.js deleted file mode 100644 index b8c093f066..0000000000 --- a/ui/src/app/widget/lib/extensions-table-widget.js +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './extensions-table-widget.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import extensionsTableWidgetTemplate from './extensions-table-widget.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -export default angular.module('thingsboard.widgets.extensionsTableWidget', []) - .directive('tbExtensionsTableWidget', ExtensionsTableWidget) - .name; - -/*@ngInject*/ -function ExtensionsTableWidget() { - return { - restrict: "E", - scope: true, - bindToController: { - ctx: '=' - }, - controller: ExtensionsTableWidgetController, - controllerAs: 'vm', - templateUrl: extensionsTableWidgetTemplate - }; -} - -/*@ngInject*/ -function ExtensionsTableWidgetController($scope, $translate, utils) { - var vm = this; - - vm.datasources = null; - vm.tabsHidden = false; - - $scope.$watch('vm.ctx', function() { - if (vm.ctx && vm.ctx.defaultSubscription) { - vm.settings = vm.ctx.settings; - vm.subscription = vm.ctx.defaultSubscription; - vm.datasources = vm.subscription.datasources; - initializeConfig(); - updateDatasources(); - } - }); - - function initializeConfig() { - - if (vm.settings.extensionsTitle && vm.settings.extensionsTitle.length) { - vm.extensionsTitle = utils.customTranslation(vm.settings.extensionsTitle, vm.settings.extensionsTitle); - } else { - vm.extensionsTitle = $translate.instant('extension.extensions'); - } - vm.ctx.widgetTitle = vm.extensionsTitle; - - vm.ctx.widgetActions = [vm.importExtensionsAction, vm.exportExtensionsAction, vm.addAction, vm.searchAction, vm.refreshAction]; - } - - function updateDatasources() { - - var datasource = vm.datasources[0]; - vm.selectedSource = vm.datasources[0]; - vm.ctx.widgetTitle = utils.createLabelFromDatasource(datasource, vm.extensionsTitle); - } - - vm.changeSelectedSource = function(source) { - vm.selectedSource = source; - }; - - vm.searchAction = { - name: "action.search", - show: true, - onAction: function() { - $scope.$broadcast("showSearch", vm.selectedSource); - }, - icon: "search" - }; - - vm.refreshAction = { - name: "action.refresh", - show: true, - onAction: function() { - $scope.$broadcast("refreshExtensions", vm.selectedSource); - }, - icon: "refresh" - }; - - vm.addAction = { - name: "action.add", - show: true, - onAction: function() { - $scope.$broadcast("addExtension", vm.selectedSource); - }, - icon: "add" - }; - - vm.exportExtensionsAction = { - name: "extension.export-extensions-configuration", - show: true, - onAction: function() { - $scope.$broadcast("exportExtensions", vm.selectedSource); - }, - icon: "file_download" - }; - - vm.importExtensionsAction = { - name: "extension.import-extensions-configuration", - show: true, - onAction: function() { - $scope.$broadcast("importExtensions", vm.selectedSource); - }, - icon: "file_upload" - }; - - $scope.$on("filterMode", function($event, mode) { - vm.tabsHidden = mode; - }); - - $scope.$on("selectedExtensions", function($event, mode) { - vm.tabsHidden = mode; - }); -} \ No newline at end of file diff --git a/ui/src/app/widget/lib/extensions-table-widget.scss b/ui/src/app/widget/lib/extensions-table-widget.scss deleted file mode 100644 index ec0f41dd17..0000000000 --- a/ui/src/app/widget/lib/extensions-table-widget.scss +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -tb-extension-table { - md-content { - background-color: #fff; - } -} - -md-tabs.hide-tabs-menu { - md-tabs-wrapper { - display: none; - } - - md-tabs-content-wrapper { - top: 0 !important; - } -} diff --git a/ui/src/app/widget/lib/extensions-table-widget.tpl.html b/ui/src/app/widget/lib/extensions-table-widget.tpl.html deleted file mode 100644 index 7a16f8af06..0000000000 --- a/ui/src/app/widget/lib/extensions-table-widget.tpl.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/ui/src/app/widget/lib/flot-widget.js b/ui/src/app/widget/lib/flot-widget.js deleted file mode 100644 index 3be077ca6d..0000000000 --- a/ui/src/app/widget/lib/flot-widget.js +++ /dev/null @@ -1,2151 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import $ from 'jquery'; -import tinycolor from 'tinycolor2'; -import moment from 'moment'; -import 'flot/lib/jquery.colorhelpers'; -import 'flot/src/jquery.flot'; -import 'flot/src/plugins/jquery.flot.time'; -import 'flot/src/plugins/jquery.flot.selection'; -import 'flot/src/plugins/jquery.flot.pie'; -import 'flot/src/plugins/jquery.flot.crosshair'; -import 'flot/src/plugins/jquery.flot.stack'; -import 'flot/src/plugins/jquery.flot.symbol'; -import 'flot.curvedlines/curvedLines'; - -/* eslint-disable angular/angularelement */ -export default class TbFlot { - constructor(ctx, chartType) { - - this.ctx = ctx; - this.chartType = chartType || 'line'; - var settings = ctx.settings; - this.utils = this.ctx.$scope.$injector.get('utils'); - this.types = this.ctx.$scope.$injector.get('types'); - - ctx.tooltip = $('#flot-series-tooltip'); - if (ctx.tooltip.length === 0) { - ctx.tooltip = $("
    "); - ctx.tooltip.css({ - fontSize: "12px", - fontFamily: "Roboto", - fontWeight: "300", - lineHeight: "18px", - opacity: "1", - backgroundColor: "rgba(0,0,0,0.7)", - color: "#D9DADB", - position: "absolute", - display: "none", - zIndex: "100", - padding: "4px 10px", - borderRadius: "4px" - }).appendTo("body"); - } - - var tbFlot = this; - - function seriesInfoDiv(label, color, value, units, trackDecimals, active, percent, valueFormatFunction) { - var divElement = $('
    '); - divElement.css({ - display: "flex", - alignItems: "center", - justifyContent: "space-between" - }); - var lineSpan = $(''); - lineSpan.css({ - backgroundColor: color, - width: "20px", - height: "3px", - display: "inline-block", - verticalAlign: "middle", - marginRight: "5px" - }); - divElement.append(lineSpan); - var labelSpan = $('' + label + ':'); - labelSpan.css({ - marginRight: "10px" - }); - if (active) { - labelSpan.css({ - color: "#FFF", - fontWeight: "700" - }); - } - divElement.append(labelSpan); - var valueContent; - if (valueFormatFunction) { - valueContent = valueFormatFunction(value); - } else { - valueContent = tbFlot.ctx.utils.formatValue(value, trackDecimals, units); - } - if (angular.isNumber(percent)) { - valueContent += ' (' + Math.round(percent) + ' %)'; - } - var valueSpan = $('' + valueContent + ''); - valueSpan.css({ - marginLeft: "auto", - fontWeight: "700" - }); - if (active) { - valueSpan.css({ - color: "#FFF" - }); - } - divElement.append(valueSpan); - - return divElement; - } - - function seriesInfoDivFromInfo(seriesHoverInfo, seriesIndex) { - var units = seriesHoverInfo.units && seriesHoverInfo.units.length ? seriesHoverInfo.units : tbFlot.ctx.trackUnits; - var decimals = angular.isDefined(seriesHoverInfo.decimals) ? seriesHoverInfo.decimals : tbFlot.ctx.trackDecimals; - var divElement = seriesInfoDiv(seriesHoverInfo.label, seriesHoverInfo.color, - seriesHoverInfo.value, units, decimals, seriesHoverInfo.index === seriesIndex, null, seriesHoverInfo.tooltipValueFormatFunction); - return divElement.prop('outerHTML'); - } - - if (this.chartType === 'pie') { - ctx.tooltipFormatter = function(item) { - var units = item.series.dataKey.units && item.series.dataKey.units.length ? item.series.dataKey.units : tbFlot.ctx.trackUnits; - var decimals = angular.isDefined(item.series.dataKey.decimals) ? item.series.dataKey.decimals : tbFlot.ctx.trackDecimals; - var divElement = seriesInfoDiv(item.series.dataKey.label, item.series.dataKey.color, - item.datapoint[1][0][1], units, decimals, true, item.series.percent, item.series.dataKey.tooltipValueFormatFunction); - return divElement.prop('outerHTML'); - }; - } else { - ctx.tooltipFormatter = function(hoverInfo, seriesIndex) { - var content = ''; - - if (tbFlot.ctx.tooltipIndividual) { - var seriesHoverArray; - if (hoverInfo[1] && hoverInfo[1].seriesHover.length) { - seriesHoverArray = hoverInfo[0].seriesHover.concat(hoverInfo[1].seriesHover); - } else { - seriesHoverArray = hoverInfo[0].seriesHover; - } - var found = seriesHoverArray.filter((seriesHover) => { - return seriesHover.index === seriesIndex; - }); - if (found && found.length) { - let timestamp = parseInt(found[0].time); - let date = moment(timestamp).format('YYYY-MM-DD HH:mm:ss'); - let dateDiv = $('
    ' + date + '
    '); - dateDiv.css({ - display: "flex", - alignItems: "center", - justifyContent: "center", - padding: "4px", - fontWeight: "700" - }); - content += dateDiv.prop('outerHTML'); - content += seriesInfoDivFromInfo(found[0], seriesIndex); - } - } else { - var maxRows; - if (hoverInfo[1] && hoverInfo[1].seriesHover.length) { - maxRows = 5; - } else { - maxRows = 15; - } - var columns = 0; - if (hoverInfo[1] && (hoverInfo[1].seriesHover.length > hoverInfo[0].seriesHover.length)) { - columns = Math.ceil(hoverInfo[1].seriesHover.length / maxRows); - } else { - columns = Math.ceil(hoverInfo[0].seriesHover.length / maxRows); - } - - for (var j = 0; j < hoverInfo.length; j++) { - var hoverData = hoverInfo[j]; - if (angular.isNumber(hoverData.time)) { - var columnsContent = ''; - let timestamp = parseInt(hoverData.time); - let date = moment(timestamp).format('YYYY-MM-DD HH:mm:ss'); - let dateDiv = $('
    ' + date + '
    '); - dateDiv.css({ - display: "flex", - alignItems: "center", - justifyContent: "center", - padding: "4px", - fontWeight: "700" - }); - content += dateDiv.prop('outerHTML'); - - var seriesDiv = $('
    '); - seriesDiv.css({ - display: "flex", - flexDirection: "row" - }); - for (var c = 0; c < columns; c++) { - var columnDiv = $('
    '); - columnDiv.css({ - display: "flex", - flexDirection: "column", - flex: "1" - }); - var columnContent = ''; - for (var i = c*maxRows; i < (c+1)*maxRows; i++) { - if (i >= hoverData.seriesHover.length) { - break; - } - var seriesHoverInfo = hoverData.seriesHover[i]; - columnContent += seriesInfoDivFromInfo(seriesHoverInfo, seriesIndex); - } - columnDiv.html(columnContent); - - if (columnContent) { - if (c > 0) { - columnsContent += ''; - } - columnsContent += columnDiv.prop('outerHTML'); - } - } - seriesDiv.html(columnsContent); - content += seriesDiv.prop('outerHTML'); - } - } - } - return content; - }; - } - - ctx.trackDecimals = ctx.decimals; - - ctx.trackUnits = ctx.units; - - ctx.tooltipIndividual = this.chartType === 'pie' || (angular.isDefined(settings.tooltipIndividual) ? settings.tooltipIndividual : false); - ctx.tooltipCumulative = angular.isDefined(settings.tooltipCumulative) ? settings.tooltipCumulative : false; - ctx.hideZeros = angular.isDefined(settings.hideZeros) ? settings.hideZeros : false; - - var font = { - color: settings.fontColor || "#545454", - size: settings.fontSize || 10, - family: "Roboto" - }; - - var options = { - title: null, - subtitle: null, - shadowSize: angular.isDefined(settings.shadowSize) ? settings.shadowSize : 4, - HtmlText: false, - grid: { - hoverable: true, - mouseActiveRadius: 10, - autoHighlight: ctx.tooltipIndividual === true, - markings: [] - }, - selection : { mode : ctx.isMobile ? null : 'x' }, - legend : { - show: false - } - }; - - if (this.chartType === 'line' || this.chartType === 'bar' || this.chartType === 'state') { - options.xaxes = []; - this.xaxis = { - mode: 'time', - timezone: 'browser', - font: angular.copy(font), - labelFont: angular.copy(font) - }; - this.yaxis = { - font: angular.copy(font), - labelFont: angular.copy(font) - }; - if (settings.xaxis) { - this.xaxis.font.color = settings.xaxis.color || this.xaxis.font.color; - this.xaxis.label = this.utils.customTranslation(settings.xaxis.title, settings.xaxis.title) || null; - this.xaxis.labelFont.color = this.xaxis.font.color; - this.xaxis.labelFont.size = this.xaxis.font.size+2; - this.xaxis.labelFont.weight = "bold"; - } - - ctx.yAxisTickFormatter = function(value/*, axis*/) { - if (settings.yaxis && settings.yaxis.showLabels === false) { - return ''; - } - if (this.ticksFormatterFunction) { - return this.ticksFormatterFunction(value); - } - - var factor = this.tickDecimals ? Math.pow(10, this.tickDecimals) : 1, - formatted = "" + Math.round(value * factor) / factor; - if (this.tickDecimals != null) { - var decimal = formatted.indexOf("."), - precision = decimal === -1 ? 0 : formatted.length - decimal - 1; - - if (precision < this.tickDecimals) { - formatted = (precision ? formatted : formatted + ".") + ("" + factor).substr(1, this.tickDecimals - precision); - } - } - if (this.tickUnits) { - formatted += ' ' + this.tickUnits; - } - - return formatted; - }; - - this.yaxis.tickFormatter = ctx.yAxisTickFormatter; - - if (settings.yaxis) { - this.yaxis.font.color = settings.yaxis.color || this.yaxis.font.color; - this.yaxis.min = angular.isDefined(settings.yaxis.min) ? settings.yaxis.min : null; - this.yaxis.max = angular.isDefined(settings.yaxis.max) ? settings.yaxis.max : null; - this.yaxis.label = this.utils.customTranslation(settings.yaxis.title, settings.yaxis.title) || null; - this.yaxis.labelFont.color = this.yaxis.font.color; - this.yaxis.labelFont.size = this.yaxis.font.size+2; - this.yaxis.labelFont.weight = "bold"; - if (angular.isNumber(settings.yaxis.tickSize)) { - this.yaxis.tickSize = settings.yaxis.tickSize; - } else { - this.yaxis.tickSize = null; - } - if (angular.isNumber(settings.yaxis.tickDecimals)) { - this.yaxis.tickDecimals = settings.yaxis.tickDecimals - } else { - this.yaxis.tickDecimals = null; - } - if (settings.yaxis.ticksFormatter && settings.yaxis.ticksFormatter.length) { - try { - this.yaxis.ticksFormatterFunction = new Function('value', settings.yaxis.ticksFormatter); - } catch (e) { - this.yaxis.ticksFormatterFunction = null; - } - } - } - - options.grid.borderWidth = 1; - options.grid.color = settings.fontColor || "#545454"; - - if (settings.grid) { - options.grid.color = settings.grid.color || "#545454"; - options.grid.backgroundColor = settings.grid.backgroundColor || null; - options.grid.tickColor = settings.grid.tickColor || "#DDDDDD"; - options.grid.borderWidth = angular.isDefined(settings.grid.outlineWidth) ? - settings.grid.outlineWidth : 1; - if (settings.grid.verticalLines === false) { - this.xaxis.tickLength = 0; - } - if (settings.grid.horizontalLines === false) { - this.yaxis.tickLength = 0; - } - if (angular.isDefined(settings.grid.margin)) { - options.grid.margin = settings.grid.margin; - } - if (angular.isDefined(settings.grid.minBorderMargin)) { - options.grid.minBorderMargin = settings.grid.minBorderMargin; - } - } - - options.xaxes[0] = angular.copy(this.xaxis); - - if (settings.xaxis && settings.xaxis.showLabels === false) { - options.xaxes[0].tickFormatter = function() { - return ''; - }; - } - - if (settings.comparisonEnabled) { - var xaxis = angular.copy(this.xaxis); - xaxis.position = 'top'; - if (settings.xaxisSecond) { - if (settings.xaxisSecond.showLabels === false) { - xaxis.tickFormatter = function() { - return ''; - }; - } - xaxis.label = this.utils.customTranslation(settings.xaxisSecond.title, settings.xaxisSecond.title) || null; - xaxis.position = settings.xaxisSecond.axisPosition; - } - xaxis.tickLength = 0; - options.xaxes.push(xaxis); - - options.series = { - stack: false - }; - } else { - options.series = { - stack: settings.stack === true - }; - } - - options.crosshair = { - mode: 'x' - }; - - if (this.chartType === 'line' && settings.smoothLines) { - options.series.curvedLines = { - active: true, - monotonicFit: true - } - } - - if (this.chartType === 'line' && isFinite(settings.thresholdsLineWidth)) { - options.grid.markingsLineWidth = settings.thresholdsLineWidth; - } - - if (this.chartType === 'bar') { - options.series.lines = { - show: false, - fill: false, - steps: false - } - options.series.bars ={ - show: true, - lineWidth: 0, - fill: 0.9, - align: settings.barAlignment || "left" - } - ctx.defaultBarWidth = settings.defaultBarWidth || 600; - } - - if (this.chartType === 'state') { - options.series.lines = { - steps: true, - show: true - } - } - - } else if (this.chartType === 'pie') { - options.series = { - pie: { - show: true, - label: { - show: settings.showLabels || settings.showPercentages - }, - radius: settings.radius || 1, - innerRadius: settings.innerRadius || 0, - stroke: { - color: '#fff', - width: 0 - }, - tilt: settings.tilt || 1, - shadow: { - left: 5, - top: 15, - alpha: 0.02 - } - } - } - options.grid.clickable = true; - - if (settings.stroke) { - options.series.pie.stroke.color = settings.stroke.color || '#fff'; - options.series.pie.stroke.width = settings.stroke.width || 0; - } - - if (options.series.pie.label.show) { - options.series.pie.label.formatter = function (label, series) { - return "
    " + - (settings.showLabels ? series.dataKey.label : "") + - (settings.showLabels && settings.showPercentages ? "
    " : "") + - (settings.showPercentages ? Math.round(series.percent) + "%" : "") + - "
    "; - } - options.series.pie.label.radius = 3/4; - options.series.pie.label.background = { - opacity: 0.8 - }; - } - } - - //Experimental - this.ctx.animatedPie = settings.animatedPie === true; - - this.options = options; - - if (this.ctx.defaultSubscription) { - this.init(this.ctx.$container, this.ctx.defaultSubscription); - } - } - - init($element, subscription) { - this.subscription = subscription; - this.$element = $element; - var colors = []; - this.yaxes = []; - var yaxesMap = {}; - let predefinedThresholds = [], thresholdsDatasources = []; - - var tooltipValueFormatFunction = null; - if (this.ctx.settings.tooltipValueFormatter && this.ctx.settings.tooltipValueFormatter.length) { - try { - tooltipValueFormatFunction = new Function('value', this.ctx.settings.tooltipValueFormatter); - } catch (e) { - tooltipValueFormatFunction = null; - } - } - - for (var i = 0; i < this.subscription.data.length; i++) { - var series = this.subscription.data[i]; - colors.push(series.dataKey.color); - var keySettings = series.dataKey.settings; - - series.dataKey.tooltipValueFormatFunction = tooltipValueFormatFunction; - if (keySettings.tooltipValueFormatter && keySettings.tooltipValueFormatter.length) { - try { - series.dataKey.tooltipValueFormatFunction = new Function('value', keySettings.tooltipValueFormatter); - } catch (e) { - series.dataKey.tooltipValueFormatFunction = tooltipValueFormatFunction; - } - } - series.lines = { - fill: keySettings.fillLines === true - }; - - if (this.ctx.settings.stack && !this.ctx.settings.comparisonEnabled) { - series.stack = !keySettings.excludeFromStacking; - } else { - series.stack = false; - } - - if (this.chartType === 'line' || this.chartType === 'state') { - series.lines.show = keySettings.showLines !== false - } else { - series.lines.show = keySettings.showLines === true; - } - - if (angular.isDefined(keySettings.lineWidth)) { - series.lines.lineWidth = keySettings.lineWidth; - } - - series.points = { - show: false, - radius: 8 - }; - if (keySettings.showPoints === true) { - series.points.show = true; - series.points.lineWidth = angular.isDefined(keySettings.showPointsLineWidth) ? keySettings.showPointsLineWidth : 5; - series.points.radius = angular.isDefined(keySettings.showPointsRadius) ? keySettings.showPointsRadius : 3; - series.points.symbol = angular.isDefined(keySettings.showPointShape) ? keySettings.showPointShape : 'circle'; - if (series.points.symbol == 'custom' && keySettings.pointShapeFormatter) { - try { - series.points.symbol = new Function('ctx, x, y, radius, shadow', keySettings.pointShapeFormatter); - } catch (e) { - series.points.symbol = 'circle'; - } - } - - } - - if (this.chartType === 'line' && this.ctx.settings.smoothLines && !series.points.show) { - series.curvedLines = { - apply: true - }; - } - - var lineColor = tinycolor(series.dataKey.color); - lineColor.setAlpha(.75); - - series.highlightColor = lineColor.toRgbString(); - - if (series.datasource.isAdditional) { - series.xaxisIndex = 1; - series.xaxis = 2; - } else { - series.xaxisIndex = 0; - series.xaxis = 1; - } - - - if (this.yaxis) { - var units = series.dataKey.units && series.dataKey.units.length ? series.dataKey.units : this.ctx.trackUnits; - var yaxis; - if (keySettings.showSeparateAxis) { - yaxis = this.createYAxis(keySettings, units); - this.yaxes.push(yaxis); - } else { - yaxis = yaxesMap[units]; - if (!yaxis) { - yaxis = this.createYAxis(keySettings, units); - yaxesMap[units] = yaxis; - this.yaxes.push(yaxis); - } - } - series.yaxisIndex = this.yaxes.indexOf(yaxis); - series.yaxis = series.yaxisIndex+1; - yaxis.keysInfo[i] = {hidden: false}; - yaxis.show = true; - - if (keySettings.thresholds && keySettings.thresholds.length) { - for (let j = 0; j < keySettings.thresholds.length; j++) { - let threshold = keySettings.thresholds[j]; - if (threshold.thresholdValueSource === 'predefinedValue' && isFinite(threshold.thresholdValue)) { - let colorIndex = this.subscription.data.length + predefinedThresholds.length; - this.generateThreshold(predefinedThresholds, series.yaxis, threshold.lineWidth, threshold.color, colorIndex, threshold.thresholdValue); - } else if (threshold.thresholdEntityAlias && threshold.thresholdAttribute) { - let entityAliasId = this.ctx.aliasController.getEntityAliasId(threshold.thresholdEntityAlias); - if (!entityAliasId) { - continue; - } - - let datasource = thresholdsDatasources.filter((datasource) => { - return datasource.entityAliasId === entityAliasId; - })[0]; - - let dataKey = { - type: this.types.dataKeyType.attribute, - name: threshold.thresholdAttribute, - label: threshold.thresholdAttribute, - settings: { - yaxis: series.yaxis, - lineWidth: threshold.lineWidth, - color: threshold.color - }, - _hash: Math.random() - }; - - if (datasource) { - datasource.dataKeys.push(dataKey); - } else { - datasource = { - type: this.types.datasourceType.entity, - name: threshold.thresholdEntityAlias, - aliasName: threshold.thresholdEntityAlias, - entityAliasId: entityAliasId, - dataKeys: [ dataKey ] - }; - thresholdsDatasources.push(datasource); - } - } - } - } - } - } - - this.subscribeForThresholdsAttributes(thresholdsDatasources); - - this.options.grid.markings = predefinedThresholds; - this.predefinedThresholds = predefinedThresholds; - - this.options.colors = colors; - this.options.yaxes = angular.copy(this.yaxes); - if (this.chartType === 'line' || this.chartType === 'bar' || this.chartType === 'state') { - if (this.chartType === 'bar') { - if (this.subscription.timeWindowConfig.aggregation && this.subscription.timeWindowConfig.aggregation.type === 'NONE') { - this.options.series.bars.barWidth = this.ctx.defaultBarWidth; - } else { - this.options.series.bars.barWidth = this.subscription.timeWindow.interval * 0.6; - } - } - this.options.xaxes[0].min = this.subscription.timeWindow.minTime; - this.options.xaxes[0].max = this.subscription.timeWindow.maxTime; - if (this.ctx.settings.comparisonEnabled) { - this.options.xaxes[1].min = this.subscription.comparisonTimeWindow.minTime; - this.options.xaxes[1].max = this.subscription.comparisonTimeWindow.maxTime; - } - } - - this.checkMouseEvents(); - - if (this.ctx.plot) { - this.ctx.plot.destroy(); - } - - if (this.chartType === 'pie' && this.ctx.animatedPie) { - this.ctx.pieDataAnimationDuration = 250; - this.pieData = angular.copy(this.subscription.data); - this.ctx.pieRenderedData = []; - this.ctx.pieTargetData = []; - for (i = 0; i < this.subscription.data.length; i++) { - this.ctx.pieTargetData[i] = (this.subscription.data[i].data && this.subscription.data[i].data[0]) - ? this.subscription.data[i].data[0][1] : 0; - } - this.pieDataRendered(); - } - this.ctx.plotInited = true; - this.createPlot(); - } - - createYAxis(keySettings, units) { - var yaxis = angular.copy(this.yaxis); - var tickDecimals, tickSize; - - var label = keySettings.axisTitle && keySettings.axisTitle.length ? keySettings.axisTitle : yaxis.label; - if (angular.isNumber(keySettings.axisTickDecimals)) { - tickDecimals = keySettings.axisTickDecimals; - } else { - tickDecimals = yaxis.tickDecimals; - } - if (angular.isNumber(keySettings.axisTickSize)) { - tickSize = keySettings.axisTickSize; - } else { - tickSize = yaxis.tickSize; - } - var position = keySettings.axisPosition && keySettings.axisPosition.length ? keySettings.axisPosition : "left"; - - var min = angular.isDefined(keySettings.axisMin) ? keySettings.axisMin : yaxis.min; - var max = angular.isDefined(keySettings.axisMax) ? keySettings.axisMax : yaxis.max; - - yaxis.label = label; - yaxis.min = min; - yaxis.max = max; - yaxis.tickUnits = units; - yaxis.tickDecimals = tickDecimals; - yaxis.tickSize = tickSize; - if (position === "right" && tickSize === null) { - yaxis.alignTicksWithAxis = 1; - } else { - yaxis.alignTicksWithAxis = null; - } - yaxis.position = position; - - yaxis.keysInfo = []; - - if (keySettings.axisTicksFormatter && keySettings.axisTicksFormatter.length) { - try { - yaxis.ticksFormatterFunction = new Function('value', keySettings.axisTicksFormatter); - } catch (e) { - yaxis.ticksFormatterFunction = this.yaxis.ticksFormatterFunction; - } - } - return yaxis; - } - - subscribeForThresholdsAttributes(datasources) { - let tbFlot = this; - let thresholdsSourcesSubscriptionOptions = { - datasources: datasources, - useDashboardTimewindow: false, - type: this.types.widgetType.latest.value, - callbacks: { - onDataUpdated: (subscription) => {tbFlot.thresholdsSourcesDataUpdated(subscription.data)} - } - }; - this.ctx.subscriptionApi.createSubscription(thresholdsSourcesSubscriptionOptions, true).then( - (subscription) => { - tbFlot.thresholdsSourcesSubscription = subscription; - } - ); - } - - thresholdsSourcesDataUpdated(data) { - let allThresholds = angular.copy(this.predefinedThresholds); - for (let i = 0; i < data.length; i++) { - let keyData = data[i]; - if (keyData && keyData.data && keyData.data[0]) { - let attrValue = keyData.data[0][1]; - if (isFinite(attrValue)) { - let settings = keyData.dataKey.settings; - let colorIndex = this.subscription.data.length + allThresholds.length; - this.generateThreshold(allThresholds, settings.yaxis, settings.lineWidth, settings.color, colorIndex, attrValue); - } - } - } - - this.options.grid.markings = allThresholds; - this.redrawPlot(); - } - - generateThreshold(existingThresholds, yaxis, lineWidth, color, defaultColorIndex, thresholdValue) { - let marking = {}; - let markingYAxis; - - if (yaxis !== 1) { - markingYAxis = 'y' + yaxis + 'axis'; - } else { - markingYAxis = 'yaxis'; - } - - if (isFinite(lineWidth)) { - marking.lineWidth = lineWidth; - } - - if (angular.isDefined(color)) { - marking.color = color; - } else { - marking.color = this.utils.getMaterialColor(defaultColorIndex); - } - - marking[markingYAxis] = { - from: thresholdValue, - to: thresholdValue - }; - - let similarMarkings = existingThresholds.filter((existingMarking) => { - return angular.equals(existingMarking[markingYAxis], marking[markingYAxis]); - }); - if (!similarMarkings.length) { - existingThresholds.push(marking); - } - } - - update() { - if (this.updateTimeoutHandle) { - this.ctx.$scope.$timeout.cancel(this.updateTimeoutHandle); - this.updateTimeoutHandle = null; - } - if (this.subscription) { - if (!this.isMouseInteraction && this.ctx.plot) { - if (this.chartType === 'line' || this.chartType === 'bar' || this.chartType === 'state') { - - var axisVisibilityChanged = false; - if (this.yaxis) { - for (var i = 0; i < this.subscription.data.length; i++) { - var series = this.subscription.data[i]; - var yaxisIndex = series.yaxisIndex; - if (this.yaxes[yaxisIndex].keysInfo[i].hidden != series.dataKey.hidden) { - this.yaxes[yaxisIndex].keysInfo[i].hidden = series.dataKey.hidden; - axisVisibilityChanged = true; - } - } - if (axisVisibilityChanged) { - this.options.yaxes.length = 0; - for (var y = 0; y < this.yaxes.length; y++) { - var yaxis = this.yaxes[y]; - var hidden = true; - for (var k = 0; k < yaxis.keysInfo.length; k++) { - if (yaxis.keysInfo[k]) { - hidden = hidden && yaxis.keysInfo[k].hidden; - } - } - yaxis.hidden = hidden; - var newIndex = 1; - if (!yaxis.hidden) { - this.options.yaxes.push(yaxis); - newIndex = this.options.yaxes.length; - } - for (k = 0; k < yaxis.keysInfo.length; k++) { - if (yaxis.keysInfo[k]) { - this.subscription.data[k].yaxis = newIndex; - } - } - - } - this.options.yaxis = { - show: this.options.yaxes.length ? true : false - }; - } - } - - this.options.xaxes[0].min = this.subscription.timeWindow.minTime; - this.options.xaxes[0].max = this.subscription.timeWindow.maxTime; - if (this.ctx.settings.comparisonEnabled) { - this.options.xaxes[1].min = this.subscription.comparisonTimeWindow.minTime; - this.options.xaxes[1].max = this.subscription.comparisonTimeWindow.maxTime; - } - if (this.chartType === 'bar') { - if (this.subscription.timeWindowConfig.aggregation && this.subscription.timeWindowConfig.aggregation.type === 'NONE') { - this.options.series.bars.barWidth = this.ctx.defaultBarWidth; - } else { - this.options.series.bars.barWidth = this.subscription.timeWindow.interval * 0.6; - } - } - - if (axisVisibilityChanged) { - this.redrawPlot(); - } else { - this.ctx.plot.getOptions().xaxes[0].min = this.subscription.timeWindow.minTime; - this.ctx.plot.getOptions().xaxes[0].max = this.subscription.timeWindow.maxTime; - if (this.ctx.settings.comparisonEnabled) { - this.ctx.plot.getOptions().xaxes[1].min = this.subscription.comparisonTimeWindow.minTime; - this.ctx.plot.getOptions().xaxes[1].max = this.subscription.comparisonTimeWindow.maxTime; - } - if (this.chartType === 'bar') { - if (this.subscription.timeWindowConfig.aggregation && this.subscription.timeWindowConfig.aggregation.type === 'NONE') { - this.ctx.plot.getOptions().series.bars.barWidth = this.ctx.defaultBarWidth; - } else { - this.ctx.plot.getOptions().series.bars.barWidth = this.subscription.timeWindow.interval * 0.6; - } - } - this.updateData(); - } - } else if (this.chartType === 'pie') { - if (this.ctx.animatedPie) { - this.nextPieDataAnimation(true); - } else { - this.updateData(); - } - } - } else if (this.isMouseInteraction && this.ctx.plot){ - var tbFlot = this; - this.updateTimeoutHandle = this.ctx.$scope.$timeout(function() { - tbFlot.update(); - }, 30, false); - } - } - } - - updateData() { - this.ctx.plot.setData(this.subscription.data); - if (this.chartType !== 'pie') { - this.ctx.plot.setupGrid(); - } - this.ctx.plot.draw(); - } - - resize() { - if (this.resizeTimeoutHandle) { - this.ctx.$scope.$timeout.cancel(this.resizeTimeoutHandle); - this.resizeTimeoutHandle = null; - } - if (this.ctx.plot && this.ctx.plotInited) { - var width = this.$element.width(); - var height = this.$element.height(); - if (width && height) { - this.ctx.plot.resize(); - if (this.chartType !== 'pie') { - this.ctx.plot.setupGrid(); - } - this.ctx.plot.draw(); - } else { - var tbFlot = this; - this.resizeTimeoutHandle = this.ctx.$scope.$timeout(function() { - tbFlot.resize(); - }, 30, false); - } - } - } - - - redrawPlot() { - if (this.ctx.plot && this.ctx.plotInited) { - this.ctx.plot.destroy(); - this.ctx.plot = null; - this.createPlot(); - } - } - - createPlot() { - if (this.createPlotTimeoutHandle) { - this.ctx.$scope.$timeout.cancel(this.createPlotTimeoutHandle); - this.createPlotTimeoutHandle = null; - } - if (this.ctx.plotInited && !this.ctx.plot) { - var width = this.$element.width(); - var height = this.$element.height(); - if (width && height) { - if (this.chartType === 'pie' && this.ctx.animatedPie) { - this.ctx.plot = $.plot(this.$element, this.pieData, this.options); - } else { - this.ctx.plot = $.plot(this.$element, this.subscription.data, this.options); - } - } else { - var tbFlot = this; - this.createPlotTimeoutHandle = this.ctx.$scope.$timeout(function() { - tbFlot.createPlot(); - }, 30, false); - } - } - } - - destroy() { - this.cleanup(); - if (this.ctx.plot) { - this.ctx.plot.destroy(); - this.ctx.plot = null; - this.ctx.plotInited = false; - } - } - - cleanup() { - if (this.updateTimeoutHandle) { - this.ctx.$scope.$timeout.cancel(this.updateTimeoutHandle); - this.updateTimeoutHandle = null; - } - if (this.createPlotTimeoutHandle) { - this.ctx.$scope.$timeout.cancel(this.createPlotTimeoutHandle); - this.createPlotTimeoutHandle = null; - } - if (this.resizeTimeoutHandle) { - this.ctx.$scope.$timeout.cancel(this.resizeTimeoutHandle); - this.resizeTimeoutHandle = null; - } - } - - checkMouseEvents() { - var enabled = !this.ctx.isMobile && !this.ctx.isEdit; - if (angular.isUndefined(this.mouseEventsEnabled) || this.mouseEventsEnabled != enabled) { - this.mouseEventsEnabled = enabled; - if (this.$element) { - if (enabled) { - this.enableMouseEvents(); - } else { - this.disableMouseEvents(); - } - this.redrawPlot(); - } - } - } - - static get pieSettingsSchema() { - return { - "schema": { - "type": "object", - "title": "Settings", - "properties": { - "radius": { - "title": "Radius", - "type": "number", - "default": 1 - }, - "innerRadius": { - "title": "Inner radius", - "type": "number", - "default": 0 - }, - "tilt": { - "title": "Tilt", - "type": "number", - "default": 1 - }, - "animatedPie": { - "title": "Enable pie animation (experimental)", - "type": "boolean", - "default": false - }, - "stroke": { - "title": "Stroke", - "type": "object", - "properties": { - "color": { - "title": "Color", - "type": "string", - "default": "" - }, - "width": { - "title": "Width (pixels)", - "type": "number", - "default": 0 - } - } - }, - "showLabels": { - "title": "Show labels", - "type": "boolean", - "default": false - }, - "showPercentages": { - "title": "Show percentages", - "type": "boolean", - "default": false - }, - "fontColor": { - "title": "Font color", - "type": "string", - "default": "#545454" - }, - "fontSize": { - "title": "Font size", - "type": "number", - "default": 10 - } - }, - "required": [] - }, - "form": [ - "radius", - "innerRadius", - "animatedPie", - "tilt", - { - "key": "stroke", - "items": [ - { - "key": "stroke.color", - "type": "color" - }, - "stroke.width" - ] - }, - "showLabels", - "showPercentages", - { - "key": "fontColor", - "type": "color" - }, - "fontSize" - ] - } - } - - static settingsSchema(chartType) { - - var schema = { - "schema": { - "type": "object", - "title": "Settings", - "properties": { - } - } - }; - - var properties = schema["schema"]["properties"]; - properties["stack"] = { - "title": "Stacking", - "type": "boolean", - "default": false - }; - if (chartType === 'graph') { - properties["smoothLines"] = { - "title": "Display smooth (curved) lines", - "type": "boolean", - "default": false - }; - } - if (chartType === 'bar') { - properties["defaultBarWidth"] = { - "title": "Default bar width for non-aggregated data (milliseconds)", - "type": "number", - "default": 600 - }; - properties["barAlignment"] = { - "title": "Bar alignment", - "type": "string", - "default": "left" - }; - } - if (chartType === 'graph' || chartType === 'bar') { - properties["thresholdsLineWidth"] = { - "title": "Default line width for all thresholds", - "type": "number" - }; - } - properties["shadowSize"] = { - "title": "Shadow size", - "type": "number", - "default": 4 - }; - properties["fontColor"] = { - "title": "Font color", - "type": "string", - "default": "#545454" - }; - properties["fontSize"] = { - "title": "Font size", - "type": "number", - "default": 10 - }; - properties["tooltipIndividual"] = { - "title": "Hover individual points", - "type": "boolean", - "default": false - }; - properties["tooltipCumulative"] = { - "title": "Show cumulative values in stacking mode", - "type": "boolean", - "default": false - }; - properties["tooltipValueFormatter"] = { - "title": "Tooltip value format function, f(value)", - "type": "string", - "default": "" - }; - properties["hideZeros"] = { - "title": "Hide zero/false values from tooltip", - "type": "boolean", - "default": false - }; - - properties["grid"] = { - "title": "Grid settings", - "type": "object", - "properties": { - "color": { - "title": "Primary color", - "type": "string", - "default": "#545454" - }, - "backgroundColor": { - "title": "Background color", - "type": "string", - "default": null - }, - "tickColor": { - "title": "Ticks color", - "type": "string", - "default": "#DDDDDD" - }, - "outlineWidth": { - "title": "Grid outline/border width (px)", - "type": "number", - "default": 1 - }, - "verticalLines": { - "title": "Show vertical lines", - "type": "boolean", - "default": true - }, - "horizontalLines": { - "title": "Show horizontal lines", - "type": "boolean", - "default": true - } - } - }; - - properties["xaxis"] = { - "title": "X axis settings", - "type": "object", - "properties": { - "showLabels": { - "title": "Show labels", - "type": "boolean", - "default": true - }, - "title": { - "title": "Axis title", - "type": "string", - "default": null - }, - "color": { - "title": "Ticks color", - "type": "string", - "default": null - } - } - }; - - properties["yaxis"] = { - "title": "Y axis settings", - "type": "object", - "properties": { - "min": { - "title": "Minimum value on the scale", - "type": "number", - "default": null - }, - "max": { - "title": "Maximum value on the scale", - "type": "number", - "default": null - }, - "showLabels": { - "title": "Show labels", - "type": "boolean", - "default": true - }, - "title": { - "title": "Axis title", - "type": "string", - "default": null - }, - "color": { - "title": "Ticks color", - "type": "string", - "default": null - }, - "ticksFormatter": { - "title": "Ticks formatter function, f(value)", - "type": "string", - "default": "" - }, - "tickDecimals": { - "title": "The number of decimals to display", - "type": "number", - "default": 0 - }, - "tickSize": { - "title": "Step size between ticks", - "type": "number", - "default": null - } - } - }; - - schema["schema"]["required"] = []; - schema["form"] = ["stack"]; - if (chartType === 'graph') { - schema["form"].push("smoothLines"); - } - if (chartType === 'bar') { - schema["form"].push("defaultBarWidth"); - schema["form"].push({ - "key": "barAlignment", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "left", - "label": "Left" - }, - { - "value": "right", - "label": "Right" - }, - { - "value": "center", - "label": "Center" - } - ] - }); - } - if (chartType === 'graph' || chartType === 'bar') { - schema["form"].push("thresholdsLineWidth"); - } - schema["form"].push("shadowSize"); - schema["form"].push({ - "key": "fontColor", - "type": "color" - }); - schema["form"].push("fontSize"); - schema["form"].push("tooltipIndividual"); - schema["form"].push("tooltipCumulative"); - schema["form"].push({ - "key": "tooltipValueFormatter", - "type": "javascript" - }); - schema["form"].push("hideZeros"); - schema["form"].push({ - "key": "grid", - "items": [ - { - "key": "grid.color", - "type": "color" - }, - { - "key": "grid.backgroundColor", - "type": "color" - }, - { - "key": "grid.tickColor", - "type": "color" - }, - "grid.outlineWidth", - "grid.verticalLines", - "grid.horizontalLines" - ] - }); - schema["form"].push({ - "key": "xaxis", - "items": [ - "xaxis.showLabels", - "xaxis.title", - { - "key": "xaxis.color", - "type": "color" - } - ] - }); - schema["form"].push({ - "key": "yaxis", - "items": [ - "yaxis.min", - "yaxis.max", - "yaxis.tickDecimals", - "yaxis.tickSize", - "yaxis.showLabels", - "yaxis.title", - { - "key": "yaxis.color", - "type": "color" - }, - { - "key": "yaxis.ticksFormatter", - "type": "javascript" - } - ] - }); - if (chartType === 'graph' || chartType === 'bar') { - schema.groupInfoes = [{ - "formIndex":0, - "GroupTitle":"Common Settings" - }]; - schema.form = [schema.form]; - angular.merge(schema.schema.properties, chartSettingsSchemaForComparison.schema.properties); - schema.schema.required = schema.schema.required.concat(chartSettingsSchemaForComparison.schema.required); - schema.form.push(chartSettingsSchemaForComparison.form); - schema.groupInfoes.push({ - "formIndex":schema.groupInfoes.length, - "GroupTitle":"Comparison Settings" - }); - } - - return schema; - } - - static get pieDatakeySettingsSchema() { - return { - "schema": { - "type": "object", - "title": "DataKeySettings", - "properties": { - "hideDataByDefault": { - "title": "Data is hidden by default", - "type": "boolean", - "default": false - }, - "disableDataHiding": { - "title": "Disable data hiding", - "type": "boolean", - "default": false - }, - "removeFromLegend": { - "title": "Remove datakey from legend", - "type": "boolean", - "default": false - } - }, - "required": [] - }, - "form": [ - "hideDataByDefault", - "disableDataHiding", - "removeFromLegend" - ] - }; - } - - static datakeySettingsSchema(defaultShowLines, chartType) { - - var schema = { - "schema": { - "type": "object", - "title": "DataKeySettings", - "properties": { - "excludeFromStacking": { - "title": "Exclude from stacking(available in \"Stacking\" mode)", - "type": "boolean", - "default": false - }, - "hideDataByDefault": { - "title": "Data is hidden by default", - "type": "boolean", - "default": false - }, - "disableDataHiding": { - "title": "Disable data hiding", - "type": "boolean", - "default": false - }, - "removeFromLegend": { - "title": "Remove datakey from legend", - "type": "boolean", - "default": false - }, - "showLines": { - "title": "Show lines", - "type": "boolean", - "default": defaultShowLines - }, - "fillLines": { - "title": "Fill lines", - "type": "boolean", - "default": false - }, - "showPoints": { - "title": "Show points", - "type": "boolean", - "default": false - }, - "showPointShape": { - "title": "Select point shape:", - "type": "string", - "default": "circle" - }, - "pointShapeFormatter": { - "title": "Point shape format function, f(ctx, x, y, radius, shadow)", - "type": "string", - "default": "var size = radius * Math.sqrt(Math.PI) / 2;\n" + - "ctx.moveTo(x - size, y - size);\n" + - "ctx.lineTo(x + size, y + size);\n" + - "ctx.moveTo(x - size, y + size);\n" + - "ctx.lineTo(x + size, y - size);" - }, - "showPointsLineWidth": { - "title": "Line width of points", - "type": "number", - "default": 5 - }, - "showPointsRadius": { - "title": "Radius of points", - "type": "number", - "default": 3 - }, - "tooltipValueFormatter": { - "title": "Tooltip value format function, f(value)", - "type": "string", - "default": "" - }, - "showSeparateAxis": { - "title": "Show separate axis", - "type": "boolean", - "default": false - }, - "axisMin": { - "title": "Minimum value on the axis scale", - "type": "number", - "default": null - }, - "axisMax": { - "title": "Maximum value on the axis scale", - "type": "number", - "default": null - }, - "axisTitle": { - "title": "Axis title", - "type": "string", - "default": "" - }, - "axisTickDecimals": { - "title": "Axis tick number of digits after floating point", - "type": "number", - "default": null - }, - "axisTickSize": { - "title": "Axis step size between ticks", - "type": "number", - "default": null - }, - "axisPosition": { - "title": "Axis position", - "type": "string", - "default": "left" - }, - "axisTicksFormatter": { - "title": "Ticks formatter function, f(value)", - "type": "string", - "default": "" - } - }, - "required": ["showLines", "fillLines", "showPoints"] - }, - "form": [ - "hideDataByDefault", - "disableDataHiding", - "removeFromLegend", - "excludeFromStacking", - "showLines", - "fillLines", - "showPoints", - { - "key": "showPointShape", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "circle", - "label": "Circle" - }, - { - "value": "cross", - "label": "Cross" - }, - { - "value": "diamond", - "label": "Diamond" - }, - { - "value": "square", - "label": "Square" - }, - { - "value": "triangle", - "label": "Triangle" - }, - { - "value": "custom", - "label": "Custom function" - } - ] - }, - { - "key": "pointShapeFormatter", - "type": "javascript" - }, - "showPointsLineWidth", - "showPointsRadius", - { - "key": "tooltipValueFormatter", - "type": "javascript" - }, - "showSeparateAxis", - "axisMin", - "axisMax", - "axisTitle", - "axisTickDecimals", - "axisTickSize", - { - "key": "axisPosition", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "left", - "label": "Left" - }, - { - "value": "right", - "label": "Right" - } - ] - }, - { - "key": "axisTicksFormatter", - "type": "javascript" - } - ] - }; - - var properties = schema["schema"]["properties"]; - - if (chartType === 'graph' || chartType === 'bar') { - properties["thresholds"] = { - "title": "Thresholds", - "type": "array", - "items": { - "title": "Threshold", - "type": "object", - "properties": { - "thresholdValueSource": { - "title": "Threshold value source", - "type": "string", - "default": "predefinedValue" - }, - "thresholdEntityAlias": { - "title": "Thresholds source entity alias", - "type": "string" - }, - "thresholdAttribute": { - "title": "Threshold source entity attribute", - "type": "string" - }, - "thresholdValue": { - "title": "Threshold value (if predefined value is selected)", - "type": "number" - }, - "lineWidth": { - "title": "Line width", - "type": "number" - }, - "color": { - "title": "Color", - "type": "string" - } - } - }, - "required": [] - }; - schema["form"].push({ - "key": "thresholds", - "items": [ - { - "key": "thresholds[].thresholdValueSource", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "predefinedValue", - "label": "Predefined value (Default)" - }, - { - "value": "entityAttribute", - "label": "Value taken from entity attribute" - } - ] - }, - "thresholds[].thresholdValue", - "thresholds[].thresholdEntityAlias", - "thresholds[].thresholdAttribute", - { - "key": "thresholds[].color", - "type": "color" - }, - "thresholds[].lineWidth" - ] - }); - - properties["comparisonSettings"] = { - "title": "Comparison Settings", - "type": "object", - "properties": { - "showValuesForComparison": { - "title": "Show historical values for comparison", - "type": "boolean", - "default": true - }, - "comparisonValuesLabel": { - "title": "Historical values label", - "type": "string", - "default": "" - }, - "color": { - "title": "Color", - "type": "string", - "default": "" - } - }, - "required": ["showValuesForComparison"] - }; - schema["form"].push({ - "key": "comparisonSettings", - "items": [ - "comparisonSettings.showValuesForComparison", - "comparisonSettings.comparisonValuesLabel", - { - "key": "comparisonSettings.color", - "type": "color" - } - ] - }); - - } - - return schema; - } - - enableMouseEvents() { - this.$element.css('pointer-events',''); - this.$element.addClass('mouse-events'); - this.options.selection = { mode : 'x' }; - - var tbFlot = this; - - if (!this.flotHoverHandler) { - this.flotHoverHandler = function (event, pos, item) { - if (!tbFlot.ctx.plot) { - return; - } - if (!tbFlot.ctx.tooltipIndividual || item) { - - var multipleModeTooltip = !tbFlot.ctx.tooltipIndividual; - - if (multipleModeTooltip) { - tbFlot.ctx.plot.unhighlight(); - } - - var pageX = pos.pageX; - var pageY = pos.pageY; - - var tooltipHtml; - - if (tbFlot.chartType === 'pie') { - tooltipHtml = tbFlot.ctx.tooltipFormatter(item); - } else { - var hoverInfo = tbFlot.getHoverInfo(tbFlot.ctx.plot.getData(), pos); - if (angular.isNumber(hoverInfo[0].time) || (hoverInfo[1] && angular.isNumber(hoverInfo[1].time))) { - hoverInfo[0].seriesHover.sort(function (a, b) { - return b.value - a.value; - }); - if (hoverInfo[1] && hoverInfo[1].seriesHover.length) { - hoverInfo[1].seriesHover.sort(function (a, b) { - return b.value - a.value; - }); - } - tooltipHtml = tbFlot.ctx.tooltipFormatter(hoverInfo, item ? item.seriesIndex : -1); - } - } - - if (tooltipHtml) { - tbFlot.ctx.tooltip.html(tooltipHtml) - .css({top: 0, left: 0}) - .fadeIn(200); - - var windowWidth = $( window ).width(); //eslint-disable-line - var windowHeight = $( window ).height(); //eslint-disable-line - var tooltipWidth = tbFlot.ctx.tooltip.width(); - var tooltipHeight = tbFlot.ctx.tooltip.height(); - var left = pageX+5; - var top = pageY+5; - if (windowWidth - pageX < tooltipWidth + 50) { - left = pageX - tooltipWidth - 10; - } - if (windowHeight - pageY < tooltipHeight + 20) { - top = pageY - tooltipHeight - 10; - } - tbFlot.ctx.tooltip.css({ - top: top, - left: left - }); - - if (multipleModeTooltip) { - for (var j = 0; j < hoverInfo.length; j++) { - for (var i = 0; i < hoverInfo[j].seriesHover.length; i++) { - var seriesHoverInfo = hoverInfo[j].seriesHover[i]; - tbFlot.ctx.plot.highlight(seriesHoverInfo.index, seriesHoverInfo.hoverIndex); - } - } - } - } - - } else { - tbFlot.ctx.tooltip.stop(true); - tbFlot.ctx.tooltip.hide(); - tbFlot.ctx.plot.unhighlight(); - } - }; - this.$element.bind('plothover', this.flotHoverHandler); - } - - if (!this.flotSelectHandler) { - this.flotSelectHandler = function (event, ranges) { - if (!tbFlot.ctx.plot) { - return; - } - tbFlot.ctx.plot.clearSelection(); - tbFlot.subscription.onUpdateTimewindow(ranges.xaxis.from, ranges.xaxis.to); - }; - this.$element.bind('plotselected', this.flotSelectHandler); - } - if (!this.dblclickHandler) { - this.dblclickHandler = function () { - tbFlot.subscription.onResetTimewindow(); - }; - this.$element.bind('dblclick', this.dblclickHandler); - } - if (!this.mousedownHandler) { - this.mousedownHandler = function () { - tbFlot.isMouseInteraction = true; - }; - this.$element.bind('mousedown', this.mousedownHandler); - } - if (!this.mouseupHandler) { - this.mouseupHandler = function () { - tbFlot.isMouseInteraction = false; - }; - this.$element.bind('mouseup', this.mouseupHandler); - } - if (!this.mouseleaveHandler) { - this.mouseleaveHandler = function () { - if (!tbFlot.ctx.plot) { - return; - } - tbFlot.ctx.tooltip.stop(true); - tbFlot.ctx.tooltip.hide(); - tbFlot.ctx.plot.unhighlight(); - tbFlot.isMouseInteraction = false; - }; - this.$element.bind('mouseleave', this.mouseleaveHandler); - } - - if (!this.flotClickHandler) { - this.flotClickHandler = function (event, pos, item) { - if (!tbFlot.ctx.plot) { - return; - } - tbFlot.onPieSliceClick(event, item); - }; - this.$element.bind('plotclick', this.flotClickHandler); - } - - } - - disableMouseEvents() { - this.$element.css('pointer-events','none'); - this.$element.removeClass('mouse-events'); - this.options.selection = { mode : null }; - - if (this.flotHoverHandler) { - this.$element.unbind('plothover', this.flotHoverHandler); - this.flotHoverHandler = null; - } - - if (this.flotSelectHandler) { - this.$element.unbind('plotselected', this.flotSelectHandler); - this.flotSelectHandler = null; - } - if (this.dblclickHandler) { - this.$element.unbind('dblclick', this.dblclickHandler); - this.dblclickHandler = null; - } - if (this.mousedownHandler) { - this.$element.unbind('mousedown', this.mousedownHandler); - this.mousedownHandler = null; - } - if (this.mouseupHandler) { - this.$element.unbind('mouseup', this.mouseupHandler); - this.mouseupHandler = null; - } - if (this.mouseleaveHandler) { - this.$element.unbind('mouseleave', this.mouseleaveHandler); - this.mouseleaveHandler = null; - } - if (this.flotClickHandler) { - this.$element.unbind('plotclick', this.flotClickHandler); - this.flotClickHandler = null; - } - } - - - findHoverIndexFromData (posX, series) { - var lower = 0; - var upper = series.data.length - 1; - var middle; - var index = null; - while (index === null) { - if (lower > upper) { - return Math.max(upper, 0); - } - middle = Math.floor((lower + upper) / 2); - if (series.data[middle][0] === posX) { - return middle; - } else if (series.data[middle][0] < posX) { - lower = middle + 1; - } else { - upper = middle - 1; - } - } - } - - findHoverIndexFromDataPoints (posX, series, last) { - var ps = series.datapoints.pointsize; - var initial = last*ps; - var len = series.datapoints.points.length; - for (var j = initial; j < len; j += ps) { - if ((!series.lines.steps && series.datapoints.points[initial] != null && series.datapoints.points[j] == null) - || series.datapoints.points[j] > posX) { - return Math.max(j - ps, 0)/ps; - } - } - return j/ps - 1; - } - - - getHoverInfo (seriesList, pos) { - var i, series, value, hoverIndex, hoverDistance, pointTime, minDistance, minTime, hoverData; - var last_value = 0; - var results = [{ - seriesHover: [] - }]; - if (this.ctx.settings.comparisonEnabled) { - results.push({ - seriesHover: [] - }); - var minDistanceHistorical, minTimeHistorical; - } - for (i = 0; i < seriesList.length; i++) { - series = seriesList[i]; - var posx; - if (series.datasource.isAdditional) { - posx = pos.x2; - } else { - posx = pos.x; - } - hoverIndex = this.findHoverIndexFromData(posx, series); - if (series.data[hoverIndex] && series.data[hoverIndex][0]) { - hoverDistance = posx - series.data[hoverIndex][0]; - pointTime = series.data[hoverIndex][0]; - - if (series.datasource.isAdditional) { - if (!minDistanceHistorical - || (hoverDistance >= 0 && (hoverDistance < minDistanceHistorical || minDistanceHistorical < 0)) - || (hoverDistance < 0 && hoverDistance > minDistanceHistorical)) { - minDistanceHistorical = hoverDistance; - minTimeHistorical = pointTime; - } - } else if (!minDistance - || (hoverDistance >= 0 && (hoverDistance < minDistance || minDistance < 0)) - || (hoverDistance < 0 && hoverDistance > minDistance)) { - minDistance = hoverDistance; - minTime = pointTime; - } - if (series.stack) { - if (this.ctx.tooltipIndividual || !this.ctx.tooltipCumulative) { - value = series.data[hoverIndex][1]; - } else { - last_value += series.data[hoverIndex][1]; - value = last_value; - } - } else { - value = series.data[hoverIndex][1]; - } - - if (series.stack || (series.curvedLines && series.curvedLines.apply)) { - hoverIndex = this.findHoverIndexFromDataPoints(posx, series, hoverIndex); - } - if (!this.ctx.hideZeros || value) { - hoverData = { - value: value, - hoverIndex: hoverIndex, - color: series.dataKey.color, - label: series.dataKey.label, - units: series.dataKey.units, - decimals: series.dataKey.decimals, - tooltipValueFormatFunction: series.dataKey.tooltipValueFormatFunction, - time: pointTime, - distance: hoverDistance, - index: i - }; - if (series.datasource.isAdditional) { - results[1].seriesHover.push(hoverData); - } else { - results[0].seriesHover.push(hoverData); - } - } - } - } - if (results[1] && results[1].seriesHover.length) { - results[1].time = minTimeHistorical; - } - results[0].time = minTime; - return results; - } - - pieDataRendered() { - for (var i = 0; i < this.ctx.pieTargetData.length; i++) { - var value = this.ctx.pieTargetData[i] ? this.ctx.pieTargetData[i] : 0; - this.ctx.pieRenderedData[i] = value; - if (!this.pieData[i].data[0]) { - this.pieData[i].data[0] = [0,0]; - } - this.pieData[i].data[0][1] = value; - } - } - - nextPieDataAnimation(start) { - if (start) { - this.finishPieDataAnimation(); - this.ctx.pieAnimationStartTime = this.ctx.pieAnimationLastTime = Date.now(); - for (var i = 0; i < this.subscription.data.length; i++) { - this.ctx.pieTargetData[i] = (this.subscription.data[i].data && this.subscription.data[i].data[0]) - ? this.subscription.data[i].data[0][1] : 0; - } - } - if (this.ctx.pieAnimationCaf) { - this.ctx.pieAnimationCaf(); - this.ctx.pieAnimationCaf = null; - } - var self = this; - this.ctx.pieAnimationCaf = this.ctx.$scope.tbRaf( - function () { - self.onPieDataAnimation(); - } - ); - } - - onPieDataAnimation() { - var time = Date.now(); - var elapsed = time - this.ctx.pieAnimationLastTime;//this.ctx.pieAnimationStartTime; - var progress = (time - this.ctx.pieAnimationStartTime) / this.ctx.pieDataAnimationDuration; - if (progress >= 1) { - this.finishPieDataAnimation(); - } else { - if (elapsed >= 40) { - for (var i = 0; i < this.ctx.pieTargetData.length; i++) { - var prevValue = this.ctx.pieRenderedData[i]; - var targetValue = this.ctx.pieTargetData[i]; - var value = prevValue + (targetValue - prevValue) * progress; - if (!this.pieData[i].data[0]) { - this.pieData[i].data[0] = [0,0]; - } - this.pieData[i].data[0][1] = value; - } - this.ctx.plot.setData(this.pieData); - this.ctx.plot.draw(); - this.ctx.pieAnimationLastTime = time; - } - this.nextPieDataAnimation(false); - } - } - - finishPieDataAnimation() { - this.pieDataRendered(); - this.ctx.plot.setData(this.pieData); - this.ctx.plot.draw(); - } - - onPieSliceClick($event, item) { - var descriptors = this.ctx.actionsApi.getActionDescriptors('sliceClick'); - if ($event && descriptors.length) { - $event.stopPropagation(); - var entityInfo = this.ctx.actionsApi.getActiveEntityInfo(); - var entityId = entityInfo ? entityInfo.entityId : null; - var entityName = entityInfo ? entityInfo.entityName : null; - var entityLabel = entityInfo && entityInfo.entityLabel ? entityInfo.entityLabel : null; - this.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName, item, entityLabel); - } - } -} - -const chartSettingsSchemaForComparison = { - "schema": { - "title": "Comparison Settings", - "type": "object", - "properties": { - "comparisonEnabled": { - "title": "Enable comparison", - "type": "boolean", - "default": false - }, - "timeForComparison": { - "title": "Time to show historical data", - "type": "string", - "default": "months" - }, - "xaxisSecond": { - "title": "Second X axis", - "type": "object", - "properties": { - "axisPosition": { - "title": "Axis position", - "type": "string", - "default": "top" - }, - "showLabels": { - "title": "Show labels", - "type": "boolean", - "default": true - }, - "title": { - "title": "Axis title", - "type": "string", - "default": null - } - } - } - }, - "required": [] - }, - "form": [ - "comparisonEnabled", - { - "key": "timeForComparison", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "days", - "label": "Day ago" - }, - { - "value": "weeks", - "label": "Week ago" - }, - { - "value": "months", - "label": "Month ago (default)" - }, - { - "value": "years", - "label": "Year ago" - } - ] - }, - { - "key": "xaxisSecond", - "items": [ - { - "key": "xaxisSecond.axisPosition", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "top", - "label": "Top (default)" - }, - { - "value": "bottom", - "label": "Bottom" - } - ] - }, - "xaxisSecond.showLabels", - "xaxisSecond.title", - ] - } - ] -}; - -/* eslint-enable angular/angularelement */ diff --git a/ui/src/app/widget/lib/google-map.js b/ui/src/app/widget/lib/google-map.js deleted file mode 100644 index dd39efb048..0000000000 --- a/ui/src/app/widget/lib/google-map.js +++ /dev/null @@ -1,489 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -var gmGlobals = { - loadingGmId: null, - gmApiKeys: {} -} - -export default class TbGoogleMap { - constructor($containerElement, utils, initCallback, defaultZoomLevel, dontFitMapBounds, disableScrollZooming, minZoomLevel, gmApiKey, gmDefaultMapType, defaultCenterPosition, markerClusteringSetting) { - - var tbMap = this; - this.utils = utils; - this.defaultZoomLevel = defaultZoomLevel; - this.dontFitMapBounds = dontFitMapBounds; - this.minZoomLevel = minZoomLevel; - this.tooltips = []; - this.defaultMapType = gmDefaultMapType; - this.defaultCenterPosition = defaultCenterPosition; - this.isMarketCluster = markerClusteringSetting && markerClusteringSetting.isMarketCluster; - - function clearGlobalId() { - if (gmGlobals.loadingGmId && gmGlobals.loadingGmId === tbMap.mapId) { - gmGlobals.loadingGmId = null; - } - } - - function displayError(message) { - $containerElement.html( // eslint-disable-line angular/angularelement - "
    "+ message + "
    " - ); - } - - function initGoogleMap() { - tbMap.map = new google.maps.Map($containerElement[0], { // eslint-disable-line no-undef - scrollwheel: !disableScrollZooming, - mapTypeId: getGoogleMapTypeId(tbMap.defaultMapType), - zoom: tbMap.defaultZoomLevel || 8, - center: new google.maps.LatLng(tbMap.defaultCenterPosition[0], tbMap.defaultCenterPosition[1]) // eslint-disable-line no-undef - }); - if (tbMap.isMarketCluster){ - tbMap.markersCluster = new MarkerClusterer(tbMap.map, [], // eslint-disable-line no-undef - angular.merge({imagePath: 'https://developers.google.com/maps/documentation/javascript/examples/markerclusterer/m'}, markerClusteringSetting)); - } - if (initCallback) { - setTimeout(initCallback, 0);// eslint-disable-line - } - - } - - /* eslint-disable no-undef */ - - function getGoogleMapTypeId(mapType) { - var mapTypeId = google.maps.MapTypeId.ROADMAP; - if (mapType) { - if (mapType === 'hybrid') { - mapTypeId = google.maps.MapTypeId.HYBRID; - } else if (mapType === 'satellite') { - mapTypeId = google.maps.MapTypeId.SATELLITE; - } else if (mapType === 'terrain') { - mapTypeId = google.maps.MapTypeId.TERRAIN; - } - } - return mapTypeId; - } - - /* eslint-enable no-undef */ - - this.mapId = '' + Math.random().toString(36).substr(2, 9); - this.apiKey = gmApiKey || ''; - - window.gm_authFailure = function() { // eslint-disable-line no-undef, angular/window-service - if (gmGlobals.loadingGmId && gmGlobals.loadingGmId === tbMap.mapId) { - gmGlobals.loadingGmId = null; - gmGlobals.gmApiKeys[tbMap.apiKey].error = 'Unable to authentificate for Google Map API.
    Please check your API key.'; - displayError(gmGlobals.gmApiKeys[tbMap.apiKey].error); - } - }; - - this.initMapFunctionName = 'initGoogleMap_' + this.mapId; - - window[this.initMapFunctionName] = function() { // eslint-disable-line no-undef, angular/window-service - lazyLoad.load([ // eslint-disable-line no-undef - { type: 'js', path: 'https://unpkg.com/@google/markerwithlabel@1.2.3/src/markerwithlabel.js' }, - { type: 'js', path: 'https://unpkg.com/@google/markerclustererplus@4.0.1/dist/markerclustererplus.min.js' } - ]).then( - function success() { - gmGlobals.gmApiKeys[tbMap.apiKey].loaded = true; - initGoogleMap(); - for (var p = 0; p < gmGlobals.gmApiKeys[tbMap.apiKey].pendingInits.length; p++) { - var pendingInit = gmGlobals.gmApiKeys[tbMap.apiKey].pendingInits[p]; - pendingInit(); - } - gmGlobals.gmApiKeys[tbMap.apiKey].pendingInits = []; - }, - function fail(e) { - clearGlobalId(); - gmGlobals.gmApiKeys[tbMap.apiKey].error = 'Google map api load failed!
    '+e; - displayError(gmGlobals.gmApiKeys[tbMap.apiKey].error); - } - ); - - }; - /* eslint-enable no-undef */ - - if (this.apiKey && this.apiKey.length > 0) { - if (gmGlobals.gmApiKeys[this.apiKey]) { - if (gmGlobals.gmApiKeys[this.apiKey].error) { - displayError(gmGlobals.gmApiKeys[this.apiKey].error); - } else if (gmGlobals.gmApiKeys[this.apiKey].loaded) { - initGoogleMap(); - } else { - gmGlobals.gmApiKeys[this.apiKey].pendingInits.push(initGoogleMap); - } - } else { - gmGlobals.gmApiKeys[this.apiKey] = { - loaded: false, - pendingInits: [] - }; - var googleMapScriptRes = 'https://maps.googleapis.com/maps/api/js?key='+this.apiKey+'&callback='+this.initMapFunctionName; - - gmGlobals.loadingGmId = this.mapId; - lazyLoad.load({ type: 'js', path: googleMapScriptRes }).then( // eslint-disable-line no-undef - function success() { - setTimeout(clearGlobalId, 2000); // eslint-disable-line no-undef, angular/timeout-service - }, - function fail(e) { - clearGlobalId(); - gmGlobals.gmApiKeys[tbMap.apiKey].error = 'Google map api load failed!
    '+e; - displayError(gmGlobals.gmApiKeys[tbMap.apiKey].error); - } - ); - } - } else { - displayError('No Google Map Api Key provided!'); - } - } - - inited() { - return angular.isDefined(this.map); - } - - getContainer() { - return this.isMarketCluster ? this.markersCluster : this.map; - } - - /* eslint-disable no-undef */ - updateMarkerLabel(marker, settings) { - marker.set('labelContent', '
    '+settings.labelText+'
    '); - } - /* eslint-enable no-undef */ - - /* eslint-disable no-undef */ - updateMarkerColor(marker, color) { - this.createDefaultMarkerIcon(marker, color, (iconInfo) => { - marker.setIcon(iconInfo.icon); - }); - } - /* eslint-enable no-undef */ - - /* eslint-disable no-undef */ - updateMarkerIcon(marker, settings) { - this.createMarkerIcon(marker, settings, (iconInfo) => { - marker.setIcon(iconInfo.icon); - if (settings.showLabel) { - marker.set('labelAnchor', new google.maps.Point(100, iconInfo.size[1] + 20)); - } - }); - } - /* eslint-disable no-undef */ - - /* eslint-disable no-undef */ - createMarkerIcon(marker, settings, onMarkerIconReady) { - var currentImage = settings.currentImage; - var gMap = this; - if (currentImage && currentImage.url) { - this.utils.loadImageAspect(currentImage.url).then( - (aspect) => { - if (aspect) { - var width; - var height; - if (aspect > 1) { - width = currentImage.size; - height = currentImage.size / aspect; - } else { - width = currentImage.size * aspect; - height = currentImage.size; - } - var icon = { - url: currentImage.url, - scaledSize : new google.maps.Size(width, height) - }; - var iconInfo = { - size: [width, height], - icon: icon - }; - onMarkerIconReady(iconInfo); - } else { - gMap.createDefaultMarkerIcon(marker, settings.color, onMarkerIconReady); - } - } - ); - } else { - this.createDefaultMarkerIcon(marker, settings.color, onMarkerIconReady); - } - } - /* eslint-enable no-undef */ - - /* eslint-disable no-undef */ - createDefaultMarkerIcon(marker, color, onMarkerIconReady) { - var pinColor = color.substr(1); - var icon = new google.maps.MarkerImage("https://chart.apis.google.com/chart?chst=d_map_pin_letter_withshadow&chld=%E2%80%A2|" + pinColor, - new google.maps.Size(40, 37), - new google.maps.Point(0,0), - new google.maps.Point(10, 37)); - var iconInfo = { - size: [40, 37], - icon: icon - }; - onMarkerIconReady(iconInfo); - } - /* eslint-enable no-undef */ - - /* eslint-disable no-undef */ - createMarker(location, dsIndex, settings, onClickListener, markerArgs, onDragendListener) { - var marker; - if (settings.showLabel) { - marker = new MarkerWithLabel({ - position: location, - labelContent: '
    '+settings.labelText+'
    ', - labelClass: "tb-labels", - draggable: settings.drraggable, - }); - } else { - marker = new google.maps.Marker({ - position: location, - draggable: settings.drraggable, - }); - } - var gMap = this; - this.createMarkerIcon(marker, settings, (iconInfo) => { - marker.setIcon(iconInfo.icon); - if (settings.showLabel) { - marker.set('labelAnchor', new google.maps.Point(100, iconInfo.size[1] + 20)); - } - if(gMap.isMarketCluster) { - gMap.getContainer().addMarker(marker); - } else { - marker.setMap(gMap.getContainer()); - } - }); - - if (settings.displayTooltip) { - this.createTooltip(marker, dsIndex, settings, markerArgs); - } - - if (onClickListener) { - marker.addListener('click', onClickListener); - } - - if (onDragendListener) { - marker.addListener('dragend', onDragendListener); - } - - return marker; - } - - removeMarker(marker) { - marker.setMap(null); - } - - /* eslint-enable no-undef */ - - /* eslint-disable no-undef */ - createTooltip(marker, dsIndex, settings, markerArgs) { - var popup = new google.maps.InfoWindow({ - content: '' - }); - var map = this; - if (settings.displayTooltipAction == 'hover') { - marker.addListener('mouseover', function () { - popup.open(this.map, marker); - }); - marker.addListener('mouseout', function () { - popup.close(); - }); - } else { - marker.addListener('click', function() { - if (settings.autocloseTooltip) { - map.tooltips.forEach((tooltip) => { - tooltip.popup.close(); - }); - } - popup.open(this.map, marker); - }); - } - this.tooltips.push( { - markerArgs: markerArgs, - popup: popup, - locationSettings: settings, - dsIndex: dsIndex - }); - } - /* eslint-enable no-undef */ - - /* eslint-disable no-undef */ - updatePolylineColor(polyline, settings, color) { - var options = { - path: polyline.getPath(), - strokeColor: color, - strokeOpacity: settings.strokeOpacity, - strokeWeight: settings.strokeWeight, - map: this.map - }; - polyline.setOptions(options); - } - /* eslint-enable no-undef */ - - /* eslint-disable no-undef */ - createPolyline(locations, settings) { - var polyline = new google.maps.Polyline({ - path: locations, - strokeColor: settings.color, - strokeOpacity: settings.strokeOpacity, - strokeWeight: settings.strokeWeight, - map: this.map - }); - - return polyline; - } - /* eslint-enable no-undef */ - - removePolyline(polyline) { - polyline.setMap(null); - } - - - createPolygon(latLangs, settings, location, onClickListener, markerArgs) { - let polygon = new google.maps.Polygon({ // eslint-disable-line no-undef - map: this.map, - paths: latLangs, - strokeColor: settings.polygonStrokeColor, - strokeOpacity: settings.polygonStrokeColor, - fillColor: settings.polygonColor, - fillOpacity: settings.polygonOpacity, - strokeWeight: settings.polygonStrokeWeight - }); - - //initialize-tooltip - - let popup = new google.maps.InfoWindow({ // eslint-disable-line no-undef - content: '' - }); - if (!this.tooltips) this.tooltips = []; - this.tooltips.push({ - markerArgs: markerArgs, - popup: popup, - locationSettings: settings, - dsIndex: location.dsIndex - }); - let map = this; - if (onClickListener) { - google.maps.event.addListener(polygon, 'click', function (event) { // eslint-disable-line no-undef - if (settings.displayTooltip ) { - if (settings.autocloseTooltip) { - map.tooltips.forEach((tooltip) => { - tooltip.popup.close(); - }); - } - if (!polygon.anchor) { - polygon.anchor = new google.maps.MVCObject(); // eslint-disable-line no-undef - } - polygon.anchor.set("position", event.latLng); - popup.open(this.map, polygon.anchor); - - } - onClickListener(); - }); - } - return polygon; - } - /* eslint-disable no-undef */ - - removePolygon (polygon) { - polygon.setMap(null); - } - - /* eslint-disable no-undef,no-unused-vars */ - updatePolygonColor (polygon, settings, color) { - let options = { - paths: polygon.getPaths(), - map: this.map, - strokeColor: color, - fillColor: color, - strokeWeight: settings.polygonStrokeWeight - }; - polygon.setOptions(options); - } - /* eslint-disable no-undef ,no-unused-vars*/ - - - getPolygonLatLngs(polygon) { - return polygon.getPaths().getArray(); - } - - setPolygonLatLngs(polygon, latLngs) { - polygon.setPaths(latLngs); - } - - - /* eslint-disable no-undef */ - fitBounds(bounds, useDefaultZoom) { - if ((this.dontFitMapBounds || useDefaultZoom) && this.defaultZoomLevel) { - this.map.setZoom(this.defaultZoomLevel); - this.map.setCenter(bounds.getCenter()); - } else { - var tbMap = this; - google.maps.event.addListenerOnce(this.map, 'bounds_changed', function() { // eslint-disable-line no-undef - if (!tbMap.defaultZoomLevel && tbMap.map.getZoom() > tbMap.minZoomLevel) { - tbMap.map.setZoom(tbMap.minZoomLevel); - } - }); - this.map.fitBounds(bounds); - } - } - /* eslint-enable no-undef */ - - createLatLng(lat, lng) { - return new google.maps.LatLng(lat, lng); // eslint-disable-line no-undef - } - - extendBoundsWithMarker(bounds, marker) { - bounds.extend(marker.getPosition()); - } - - getMarkerPosition(marker) { - return marker.getPosition(); - } - - setMarkerPosition(marker, latLng) { - marker.setPosition(latLng); - } - - getPolylineLatLngs(polyline) { - return polyline.getPath().getArray(); - } - - setPolylineLatLngs(polyline, latLngs) { - polyline.setPath(latLngs); - } - - createBounds() { - return new google.maps.LatLngBounds(); // eslint-disable-line no-undef - } - - extendBounds(bounds, polyline) { - if (polyline && polyline.getPath()) { - var locations = polyline.getPath(); - for (var i = 0; i < locations.getLength(); i++) { - bounds.extend(locations.getAt(i)); - } - } - } - - invalidateSize() { - google.maps.event.trigger(this.map, "resize"); // eslint-disable-line no-undef - } - - getTooltips() { - return this.tooltips; - } - - getCenter() { - return this.map.getCenter().toJSON(); - } - -} diff --git a/ui/src/app/widget/lib/image-map.js b/ui/src/app/widget/lib/image-map.js deleted file mode 100644 index 33eb6a5176..0000000000 --- a/ui/src/app/widget/lib/image-map.js +++ /dev/null @@ -1,479 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import 'leaflet/dist/leaflet.css'; -import * as L from 'leaflet'; - -const maxZoom = 4; - -export default class TbImageMap { - - constructor(ctx, $containerElement, utils, initCallback, imageUrl, disableScrollZooming, posFunction, imageEntityAlias, imageUrlAttribute) { - - this.ctx = ctx; - this.utils = utils; - this.tooltips = []; - - this.$containerElement = $containerElement; - this.$containerElement.css('background', '#fff'); - - this.aspect = 0; - this.width = 0; - this.height = 0; - this.markers = []; - this.initCallback = initCallback; - this.disableScrollZooming = disableScrollZooming; - - if (angular.isDefined(posFunction) && posFunction.length > 0) { - try { - this.posFunction = new Function('origXPos, origYPos', posFunction); - } catch (e) { - this.posFunction = null; - } - } - if (!this.posFunction) { - this.posFunction = (origXPos, origYPos) => {return {x: origXPos, y: origYPos}}; - } - - if (!this.subscribeForImageAttribute(imageEntityAlias, imageUrlAttribute)) { - this.loadImage(imageUrl, initCallback); - } - } - - subscribeForImageAttribute(imageEntityAlias, imageUrlAttribute) { - if (!imageEntityAlias || !imageEntityAlias.length || - !imageUrlAttribute || !imageUrlAttribute.length) { - return false; - } - var entityAliasId = this.ctx.aliasController.getEntityAliasId(imageEntityAlias); - if (!entityAliasId) { - return false; - } - var types = this.ctx.$scope.$injector.get('types'); - var datasources = [ - { - type: types.datasourceType.entity, - name: imageEntityAlias, - aliasName: imageEntityAlias, - entityAliasId: entityAliasId, - dataKeys: [ - { - type: types.dataKeyType.attribute, - name: imageUrlAttribute, - label: imageUrlAttribute, - settings: {}, - _hash: Math.random() - } - ] - } - ]; - var imageMap = this; - var imageUrlSubscriptionOptions = { - datasources: datasources, - useDashboardTimewindow: false, - type: types.widgetType.latest.value, - callbacks: { - onDataUpdated: (subscription, apply) => {imageMap.imageUrlDataUpdated(subscription, apply)} - } - }; - this.ctx.subscriptionApi.createSubscription(imageUrlSubscriptionOptions, true).then( - (subscription) => { - imageMap.imageUrlSubscription = subscription; - } - ); - return true; - } - - imageUrlDataUpdated(subscription, apply) { - var data = subscription.data; - if (data.length) { - var keyData = data[0]; - if (keyData && keyData.data && keyData.data[0]) { - var attrValue = keyData.data[0][1]; - if (attrValue && attrValue.length) { - this.loadImage(attrValue, this.aspect > 0 ? null : this.initCallback, true); - } - } - } - if (apply) { - this.ctx.$scope.$digest(); - } - } - - loadImage(imageUrl, initCallback, updateImage) { - if (!imageUrl) { - imageUrl = 'data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg=='; - } - this.imageUrl = imageUrl; - var imageMap = this; - this.utils.loadImageAspect(imageUrl).then( - (aspect) => { - imageMap.aspect = aspect; - imageMap.onresize(updateImage); - if (initCallback) { - setTimeout(initCallback, 0); //eslint-disable-line - } - } - ); - } - - onresize(updateImage) { - if (this.aspect > 0) { - var width = this.$containerElement.width(); - if (width > 0) { - var height = width / this.aspect; - var imageMapHeight = this.$containerElement.height(); - if (imageMapHeight > 0 && height > imageMapHeight) { - height = imageMapHeight; - width = height * this.aspect; - } - width *= maxZoom; - var prevWidth = this.width; - var prevHeight = this.height; - if (this.width !== width) { - this.width = width; - this.height = width / this.aspect; - if (!this.map) { - this.initMap(updateImage); - } else { - var lastCenterPos = this.latLngToPoint(this.map.getCenter()); - lastCenterPos.x /= prevWidth; - lastCenterPos.y /= prevHeight; - this.updateBounds(updateImage, lastCenterPos); - this.map.invalidateSize(true); - this.updateMarkers(); - } - } - } - } - } - - initMap(updateImage) { - if (!this.map && this.aspect > 0) { - var center = this.pointToLatLng(this.width/2, this.height/2); - this.map = L.map(this.$containerElement[0], { - minZoom: 1, - maxZoom: maxZoom, - scrollWheelZoom: !this.disableScrollZooming, - center: center, - zoom: 1, - crs: L.CRS.Simple, - attributionControl: false - }); - this.updateBounds(updateImage); - this.updateMarkers(); - } - } - - pointToLatLng(x, y) { - return L.CRS.Simple.pointToLatLng({x:x, y:y}, maxZoom-1); - } - - latLngToPoint(latLng) { - return L.CRS.Simple.latLngToPoint(latLng, maxZoom-1); - } - - inited() { - return angular.isDefined(this.map); - } - - updateBounds(updateImage, lastCenterPos) { - var w = this.width; - var h = this.height; - var southWest = this.pointToLatLng(0, h); - var northEast = this.pointToLatLng(w, 0); - var bounds = new L.LatLngBounds(southWest, northEast); - - if (updateImage && this.imageOverlay) { - this.imageOverlay.remove(); - this.imageOverlay = null; - } - - if (this.imageOverlay) { - this.imageOverlay.setBounds(bounds); - } else { - this.imageOverlay = L.imageOverlay(this.imageUrl, bounds).addTo(this.map); - } - var padding = 200 * maxZoom; - southWest = this.pointToLatLng(-padding, h + padding); - northEast = this.pointToLatLng(w+padding, -padding); - var maxBounds = new L.LatLngBounds(southWest, northEast); - this.map.setMaxBounds(maxBounds); - if (lastCenterPos) { - lastCenterPos.x *= w; - lastCenterPos.y *= h; - var center = this.pointToLatLng(lastCenterPos.x, lastCenterPos.y); - this.ctx.$scope.$injector.get('$mdUtil').nextTick(() => { - this.map.panTo(center, {animate: false}); - }); - } - } - - updateMarkerLabel(marker, settings) { - marker.unbindTooltip(); - marker.bindTooltip('
    '+settings.labelText+'
    ', - { className: 'tb-marker-label', permanent: true, direction: 'top', offset: marker.tooltipOffset }); - } - - updateMarkerColor(marker, color) { - this.createDefaultMarkerIcon(marker, color, (iconInfo) => { - marker.setIcon(iconInfo.icon); - }); - } - - updateMarkerIcon(marker, settings) { - this.createMarkerIcon(marker, settings, (iconInfo) => { - marker.setIcon(iconInfo.icon); - if (settings.showLabel) { - marker.unbindTooltip(); - marker.tooltipOffset = [0, -iconInfo.size[1] * marker.offsetY + 10]; - marker.bindTooltip('
    '+settings.labelText+'
    ', - { className: 'tb-marker-label', permanent: true, direction: 'top', offset: marker.tooltipOffset }); - } - }); - } - - createMarkerIcon(marker, settings, onMarkerIconReady) { - var currentImage = settings.currentImage; - var opMap = this; - if (currentImage && currentImage.url) { - this.utils.loadImageAspect(currentImage.url).then( - (aspect) => { - if (aspect) { - var width; - var height; - if (aspect > 1) { - width = currentImage.size; - height = currentImage.size / aspect; - } else { - width = currentImage.size * aspect; - height = currentImage.size; - } - var icon = L.icon({ - iconUrl: currentImage.url, - iconSize: [width, height], - iconAnchor: [marker.offsetX * width, marker.offsetY * height], - popupAnchor: [0, -height] - }); - var iconInfo = { - size: [width, height], - icon: icon - }; - onMarkerIconReady(iconInfo); - } else { - opMap.createDefaultMarkerIcon(marker, settings.color, onMarkerIconReady); - } - } - ); - } else { - this.createDefaultMarkerIcon(marker, settings.color, onMarkerIconReady); - } - } - - createDefaultMarkerIcon(marker, color, onMarkerIconReady) { - var pinColor = color.substr(1); - var icon = L.icon({ - iconUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|' + pinColor, - iconSize: [21, 34], - iconAnchor: [21 * marker.offsetX, 34 * marker.offsetY], - popupAnchor: [0, -34], - shadowUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_shadow', - shadowSize: [40, 37], - shadowAnchor: [12, 35] - }); - var iconInfo = { - size: [21, 34], - icon: icon - }; - onMarkerIconReady(iconInfo); - } - - createMarker(position, dsIndex, settings, onClickListener, markerArgs, onDragendListener) { - var pos = this.posFunction(position.x, position.y); - var x = pos.x * this.width; - var y = pos.y * this.height; - var location = this.pointToLatLng(x, y); - var marker = L.marker(location, { - draggable: settings.drraggable - });//.addTo(this.map); - marker.position = position; - marker.offsetX = settings.markerOffsetX; - marker.offsetY = settings.markerOffsetY; - var opMap = this; - this.createMarkerIcon(marker, settings, (iconInfo) => { - marker.setIcon(iconInfo.icon); - if (settings.showLabel) { - marker.tooltipOffset = [0, -iconInfo.size[1] * marker.offsetY + 10]; - marker.bindTooltip('
    '+settings.labelText+'
    ', - { className: 'tb-marker-label', permanent: true, direction: 'top', offset: marker.tooltipOffset }); - } - marker.addTo(opMap.map); - }); - - if (settings.displayTooltip) { - this.createTooltip(marker, dsIndex, settings, markerArgs); - } - - if (onClickListener) { - marker.on('click', onClickListener); - } - if (onDragendListener) { - marker.on('dragend', ($event) => { - let newMarkerPosition = this.latLngToPoint(marker.getLatLng()); - marker.position.x = this.constructor.calculateNewPosition(newMarkerPosition.x, this.width); - marker.position.y = this.constructor.calculateNewPosition(newMarkerPosition.y, this.height); - this.setMarkerPosition(marker, marker.position); - onDragendListener($event); - }); - } - this.markers.push(marker); - return marker; - } - - static calculateNewPosition(positon, imageSize) { - let newPosition = positon / imageSize; - if (newPosition < 0) { - newPosition = 0; - } else if (newPosition > 1) { - newPosition = 1; - } - return newPosition; - } - - - updateMarkers() { - this.markers.forEach((marker) => { - this.updateMarkerLocation(marker); - }); - } - - updateMarkerLocation(marker) { - this.setMarkerPosition(marker, marker.position); - } - - removeMarker(marker) { - this.map.removeLayer(marker); - var index = this.markers.indexOf(marker); - if (index > -1) { - marker.pinElement.remove(); - this.markers.splice(index, 1); - } - } - - createTooltip(marker, dsIndex, settings, markerArgs) { - var popup = L.popup(); - popup.setContent(''); - marker.bindPopup(popup, {autoClose: settings.autocloseTooltip, closeOnClick: false}); - if (settings.displayTooltipAction == 'hover') { - marker.off('click'); - marker.on('mouseover', function () { - this.openPopup(); - }); - marker.on('mouseout', function () { - this.closePopup(); - }); - } - this.tooltips.push( { - markerArgs: markerArgs, - popup: popup, - locationSettings: settings, - dsIndex: dsIndex - }); - } - - updatePolylineColor(/*polyline, settings, color*/) { - } - - createPolyline(/*locations, settings*/) { - } - - removePolyline(/*polyline*/) { - } - - createPolygon(/*latLangs, settings*/) { - } - - removePolygon(/*latLangs, settings*/) { - } - - updatePolygonColor(/*latLangs, settings*/) { - } - - getPolygonLatLngs(/*latLangs, settings*/) { - } - - setPolygonLatLngs(/*latLangs, settings*/) { - } - - fitBounds() { - } - - createLatLng(x, y) { - return new Position(x, y); - } - - extendBoundsWithMarker() { - } - - getMarkerPosition(marker) { - return marker.position; - } - - setMarkerPosition(marker, position) { - marker.position = position; - var pos = this.posFunction(position.x, position.y); - var x = pos.x * this.width; - var y = pos.y * this.height; - var location = this.pointToLatLng(x, y); - marker.setLatLng(location); - } - - getPolylineLatLngs(/*polyline*/) { - } - - setPolylineLatLngs(/*polyline, latLngs*/) { - } - - createBounds() { - return {}; - } - - extendBounds() { - } - - invalidateSize() { - this.onresize(); - } - - getTooltips() { - return this.tooltips; - } - - getCenter() { - return this.map.getCenter(); - } - -} - -class Position { - constructor(x, y) { - this.x = x; - this.y = y; - } - - equals(loc) { - return loc && loc.x == this.x && loc.y == this.y; - } -} diff --git a/ui/src/app/widget/lib/map-widget.js b/ui/src/app/widget/lib/map-widget.js deleted file mode 100644 index 3080074521..0000000000 --- a/ui/src/app/widget/lib/map-widget.js +++ /dev/null @@ -1,609 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import tinycolor from 'tinycolor2'; - -import TbGoogleMap from './google-map'; -import TbOpenStreetMap from './openstreet-map'; - -function procesTooltipPattern(tbMap, pattern, datasources, dsIndex) { - var match = tbMap.varsRegex.exec(pattern); - var replaceInfo = {}; - replaceInfo.variables = []; - while (match !== null) { - var variableInfo = {}; - variableInfo.dataKeyIndex = -1; - var variable = match[0]; - var label = match[1]; - var valDec = 2; - var splitVals = label.split(':'); - if (splitVals.length > 1) { - label = splitVals[0]; - valDec = parseFloat(splitVals[1]); - } - variableInfo.variable = variable; - variableInfo.valDec = valDec; - - if (label.startsWith('#')) { - var keyIndexStr = label.substring(1); - var n = Math.floor(Number(keyIndexStr)); - if (String(n) === keyIndexStr && n >= 0) { - variableInfo.dataKeyIndex = n; - } - } - if (variableInfo.dataKeyIndex === -1) { - var offset = 0; - for (var i=0;i 0 && settings.defaultZoomLevel < 21) { - this.defaultZoomLevel = Math.floor(settings.defaultZoomLevel); - } - } - - this.dontFitMapBounds = settings.fitMapBounds === false; - - if (!useDynamicLocations) { - this.subscription = this.ctx.defaultSubscription; - this.configureLocationsFromSettings(); - } - - var minZoomLevel = this.drawRoutes ? 18 : 15; - - var initCallback = function() { - tbMap.update(); - tbMap.resize(); - }; - - if (mapProvider === 'google-map') { - this.map = new TbGoogleMap($element, this.utils, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel, settings.gmApiKey, settings.gmDefaultMapType); - } else if (mapProvider === 'openstreet-map') { - this.map = new TbOpenStreetMap($element, this.utils, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel); - } - - } - - setCallbacks(callbacks) { - Object.assign(this.callbacks, callbacks); - } - - clearLocations() { - if (this.locations) { - var tbMap = this; - this.locations.forEach(function(location) { - if (location.marker) { - tbMap.map.removeMarker(location.marker); - } - if (location.polyline) { - tbMap.map.removePolyline(location.polyline); - } - }); - this.locations = null; - this.markers = []; - if (this.drawRoutes) { - this.polylines = []; - } - } - } - - configureLocationsFromSubscription(subscription, subscriptionLocationSettings) { - this.subscription = subscription; - this.clearLocations(); - this.locationsSettings = []; - var latKeyName = subscriptionLocationSettings.latKeyName; - var lngKeyName = subscriptionLocationSettings.lngKeyName; - var index = 0; - for (var i=0;i -1 && lngKeyIndex > -1) { - var locationsSettings = { - latKeyName: localLatKeyName, - lngKeyName: localLngKeyName, - showLabel: subscriptionLocationSettings.showLabel !== false, - displayTooltip: subscriptionLocationSettings.displayTooltip !== false, - label: datasource.name, - labelText: datasource.name, - labelColor: subscriptionLocationSettings.labelColor || this.ctx.widgetConfig.color || '#000000', - color: "#FE7569", - useColorFunction: false, - colorFunction: null, - markerImage: null, - markerImageSize: 34, - useMarkerImage: false, - useMarkerImageFunction: false, - markerImageFunction: null, - markerImages: [], - tooltipPattern: subscriptionLocationSettings.tooltipPattern || "Latitude: ${latitude:7}
    Longitude: ${longitude:7}" - }; - - locationsSettings.tooltipReplaceInfo = procesTooltipPattern(this, locationsSettings.tooltipPattern, this.subscription.datasources, i); - - locationsSettings.useColorFunction = subscriptionLocationSettings.useColorFunction === true; - if (angular.isDefined(subscriptionLocationSettings.colorFunction) && subscriptionLocationSettings.colorFunction.length > 0) { - try { - locationsSettings.colorFunction = new Function('data, dsData, dsIndex', subscriptionLocationSettings.colorFunction); - } catch (e) { - locationsSettings.colorFunction = null; - } - } - - this.locationsSettings.push(locationsSettings); - latKeyIndex = -1; - lngKeyIndex = -1; - } - index++; - } - } - } - - configureLocationsFromSettings() { - var configuredLocationsSettings = this.drawRoutes ? this.ctx.settings.routesSettings : this.ctx.settings.markersSettings; - if (!configuredLocationsSettings) { - configuredLocationsSettings = []; - } - - for (var i=0;iLatitude:
    ${lat:7}
    Longitude: ${lng:7}" - }; - - if (this.drawRoutes) { - this.locationsSettings[i].strokeWeight = 2; - this.locationsSettings[i].strokeOpacity = 1.0; - } - - if (configuredLocationsSettings[i]) { - this.locationsSettings[i].latKeyName = configuredLocationsSettings[i].latKeyName || this.locationsSettings[i].latKeyName; - this.locationsSettings[i].lngKeyName = configuredLocationsSettings[i].lngKeyName || this.locationsSettings[i].lngKeyName; - - this.locationsSettings[i].tooltipPattern = configuredLocationsSettings[i].tooltipPattern || "Latitude: ${"+this.locationsSettings[i].latKeyName+":7}
    Longitude: ${"+this.locationsSettings[i].lngKeyName+":7}"; - - this.locationsSettings[i].tooltipReplaceInfo = procesTooltipPattern(this, this.locationsSettings[i].tooltipPattern, this.subscription.datasources); - - this.locationsSettings[i].showLabel = configuredLocationsSettings[i].showLabel !== false; - this.locationsSettings[i].label = configuredLocationsSettings[i].label || this.locationsSettings[i].label; - this.locationsSettings[i].labelText = this.locationsSettings[i].label; - this.locationsSettings[i].color = configuredLocationsSettings[i].color ? tinycolor(configuredLocationsSettings[i].color).toHexString() : this.locationsSettings[i].color; - - this.locationsSettings[i].useColorFunction = configuredLocationsSettings[i].useColorFunction === true; - if (angular.isDefined(configuredLocationsSettings[i].colorFunction) && configuredLocationsSettings[i].colorFunction.length > 0) { - try { - this.locationsSettings[i].colorFunction = new Function('data, dsData, dsIndex', configuredLocationsSettings[i].colorFunction); - } catch (e) { - this.locationsSettings[i].colorFunction = null; - } - } - - this.locationsSettings[i].useMarkerImageFunction = configuredLocationsSettings[i].useMarkerImageFunction === true; - if (angular.isDefined(configuredLocationsSettings[i].markerImageFunction) && configuredLocationsSettings[i].markerImageFunction.length > 0) { - try { - this.locationsSettings[i].markerImageFunction = new Function('data, images, dsData, dsIndex', configuredLocationsSettings[i].markerImageFunction); - } catch (e) { - this.locationsSettings[i].markerImageFunction = null; - } - } - - this.locationsSettings[i].markerImages = configuredLocationsSettings[i].markerImages || []; - - if (!this.locationsSettings[i].useMarkerImageFunction && - angular.isDefined(configuredLocationsSettings[i].markerImage) && - configuredLocationsSettings[i].markerImage.length > 0) { - this.locationsSettings[i].useMarkerImage = true; - var url = this.ctx.settings.markerImage; - var size = this.ctx.settings.markerImageSize || 34; - this.locationsSettings[i].currentImage = { - url: url, - size: size - }; - } - - if (this.drawRoutes) { - this.locationsSettings[i].strokeWeight = configuredLocationsSettings[i].strokeWeight || this.locationsSettings[i].strokeWeight; - this.locationsSettings[i].strokeOpacity = angular.isDefined(configuredLocationsSettings[i].strokeOpacity) ? configuredLocationsSettings[i].strokeOpacity : this.locationsSettings[i].strokeOpacity; - } - } - } - } - - update() { - - var tbMap = this; - - function isNumber(n) { - return !isNaN(parseFloat(n)) && isFinite(n); - } - - function padValue(val, dec, int) { - var i = 0; - var s, strVal, n; - - val = parseFloat(val); - n = (val < 0); - val = Math.abs(val); - - if (dec > 0) { - strVal = val.toFixed(dec).toString().split('.'); - s = int - strVal[0].length; - - for (; i < s; ++i) { - strVal[0] = '0' + strVal[0]; - } - - strVal = (n ? '-' : '') + strVal[0] + '.' + strVal[1]; - } - - else { - strVal = Math.round(val).toString(); - s = int - strVal.length; - - for (; i < s; ++i) { - strVal = '0' + strVal; - } - - strVal = (n ? '-' : '') + strVal; - } - - return strVal; - } - - function arraysEqual(a, b) { - if (a === b) return true; - if (a === null || b === null) return false; - if (a.length != b.length) return false; - - for (var i = 0; i < a.length; ++i) { - if (!a[i].equals(b[i])) return false; - } - return true; - } - - function calculateLocationColor(location, dataMap) { - if (location.settings.useColorFunction && location.settings.colorFunction) { - var color = '#FE7569'; - try { - color = location.settings.colorFunction(dataMap.dataMap, dataMap.dsDataMap, location.dsIndex); - } catch (e) { - color = '#FE7569'; - } - return tinycolor(color).toHexString(); - } else { - return location.settings.color; - } - } - - function updateLocationColor(location, dataMap) { - var color = calculateLocationColor(location, dataMap); - if (!location.settings.calculatedColor || location.settings.calculatedColor !== color) { - if (!location.settings.useMarkerImage && !location.settings.useMarkerImageFunction) { - tbMap.map.updateMarkerColor(location.marker, color); - } - if (location.polyline) { - tbMap.map.updatePolylineColor(location.polyline, location.settings, color); - } - location.settings.calculatedColor = color; - } - } - - function calculateLocationMarkerImage(location, dataMap) { - if (location.settings.useMarkerImageFunction && location.settings.markerImageFunction) { - var image = null; - try { - image = location.settings.markerImageFunction(dataMap.dataMap, location.settings.markerImages, dataMap.dsDataMap, location.dsIndex); - } catch (e) { - image = null; - } - return image; - } else { - return null; - } - } - - function updateLocationMarkerIcon(location, dataMap) { - var image = calculateLocationMarkerImage(location, dataMap); - if (image && (!location.settings.currentImage || !angular.equals(location.settings.currentImage, image))) { - location.settings.currentImage = image; - tbMap.map.updateMarkerIcon(location.marker, location.settings); - } - } - - function updateLocationStyle(location, dataMap) { - updateLocationColor(location, dataMap); - updateLocationMarkerIcon(location, dataMap); - } - - function createOrUpdateLocationMarker(location, markerLocation, dataMap) { - var changed = false; - if (!location.marker) { - var image = calculateLocationMarkerImage(location, dataMap); - if (image && (!location.settings.currentImage || !angular.equals(location.settings.currentImage, image))) { - location.settings.currentImage = image; - } - location.marker = tbMap.map.createMarker(markerLocation, location.dsIndex, location.settings, - function() { - tbMap.callbacks.onLocationClick(location); - } - ); - tbMap.markers.push(location.marker); - changed = true; - } else { - var prevPosition = tbMap.map.getMarkerPosition(location.marker); - if (!prevPosition.equals(markerLocation)) { - tbMap.map.setMarkerPosition(location.marker, markerLocation); - changed = true; - } - } - return changed; - } - - function updateLocation(location, data, dataMap) { - var locationChanged = false; - if (location.latIndex > -1 && location.lngIndex > -1) { - var latData = data[location.latIndex].data; - var lngData = data[location.lngIndex].data; - var lat, lng, latLng; - if (latData.length > 0 && lngData.length > 0) { - if (tbMap.drawRoutes) { - // Create or update route - var latLngs = []; - for (var i = 0; i < latData.length; i++) { - lat = latData[i][1]; - lng = lngData[i][1]; - latLng = tbMap.map.createLatLng(lat, lng); - if (i == 0 || !latLngs[latLngs.length-1].equals(latLng)) { - latLngs.push(latLng); - } - } - if (latLngs.length > 0) { - var markerLocation = latLngs[latLngs.length-1]; - createOrUpdateLocationMarker(location, markerLocation, dataMap); - } - if (!location.polyline) { - location.polyline = tbMap.map.createPolyline(latLngs, location.settings); - tbMap.polylines.push(location.polyline); - locationChanged = true; - } else { - var prevPath = tbMap.map.getPolylineLatLngs(location.polyline); - if (!prevPath || !arraysEqual(prevPath, latLngs)) { - tbMap.map.setPolylineLatLngs(location.polyline, latLngs); - locationChanged = true; - } - } - } else { - // Create or update marker - lat = latData[latData.length-1][1]; - lng = lngData[lngData.length-1][1]; - latLng = tbMap.map.createLatLng(lat, lng); - if (createOrUpdateLocationMarker(location, latLng, dataMap)) { - locationChanged = true; - } - } - updateLocationStyle(location, dataMap); - } - } - return locationChanged; - } - - function toLabelValueMap(data, datasources) { - var dataMap = {}; - var dsDataMap = []; - for (var d=0;d 0) { - val = keyData[keyData.length-1][1]; - } - dataMap[label] = val; - var dsIndex = datasources.indexOf(data[i].datasource); - dsDataMap[dsIndex][label] = val; - } - return { - dataMap: dataMap, - dsDataMap: dsDataMap - }; - } - - function loadLocations(data, datasources) { - var bounds = tbMap.map.createBounds(); - tbMap.locations = []; - var dataMap = toLabelValueMap(data, datasources); - for (var l=0; l < tbMap.locationsSettings.length; l++) { - var locationSettings = tbMap.locationsSettings[l]; - var latIndex = -1; - var lngIndex = -1; - for (var i = 0; i < data.length; i++) { - var dataKey = data[i].dataKey; - var nameToCheck; - if (dataKey.locationAttrName) { - nameToCheck = dataKey.locationAttrName; - } else { - nameToCheck = dataKey.label; - } - if (nameToCheck === locationSettings.latKeyName) { - latIndex = i; - } else if (nameToCheck === locationSettings.lngKeyName) { - lngIndex = i; - } - } - if (latIndex > -1 && lngIndex > -1) { - var ds = data[latIndex].datasource; - var dsIndex = datasources.indexOf(ds); - var location = { - latIndex: latIndex, - lngIndex: lngIndex, - dsIndex: dsIndex, - settings: locationSettings - }; - tbMap.locations.push(location); - updateLocation(location, data, dataMap); - if (location.polyline) { - tbMap.map.extendBounds(bounds, location.polyline); - } else if (location.marker) { - tbMap.map.extendBoundsWithMarker(bounds, location.marker); - } - } - } - tbMap.map.fitBounds(bounds); - } - - function updateLocations(data, datasources) { - var locationsChanged = false; - var bounds = tbMap.map.createBounds(); - var dataMap = toLabelValueMap(data, datasources); - for (var p = 0; p < tbMap.locations.length; p++) { - var location = tbMap.locations[p]; - locationsChanged |= updateLocation(location, data, dataMap); - if (location.polyline) { - tbMap.map.extendBounds(bounds, location.polyline); - } else if (location.marker) { - tbMap.map.extendBoundsWithMarker(bounds, location.marker); - } - } - if (locationsChanged) { - tbMap.map.fitBounds(bounds); - } - } - - if (this.map && this.map.inited() && this.subscription) { - if (this.subscription.data) { - if (!this.locations) { - loadLocations(this.subscription.data, this.subscription.datasources); - } else { - updateLocations(this.subscription.data, this.subscription.datasources); - } - var tooltips = this.map.getTooltips(); - for (var t=0; t < tooltips.length; t++) { - var tooltip = tooltips[t]; - var settings = tooltip.locationSettings; - var text = settings.tooltipPattern; - var replaceInfo = settings.tooltipReplaceInfo; - for (var v = 0; v < replaceInfo.variables.length; v++) { - var variableInfo = replaceInfo.variables[v]; - var txtVal = ''; - if (variableInfo.dataKeyIndex > -1 && this.subscription.data[variableInfo.dataKeyIndex]) { - var varData = this.subscription.data[variableInfo.dataKeyIndex].data; - if (varData.length > 0) { - var val = varData[varData.length - 1][1]; - if (isNumber(val)) { - txtVal = padValue(val, variableInfo.valDec, 0); - } else { - txtVal = val; - } - } - } - text = text.split(variableInfo.variable).join(txtVal); - } - tooltip.popup.setContent(text); - } - } - } - } - - resize() { - if (this.map && this.map.inited()) { - this.map.invalidateSize(); - if (this.locations && this.locations.length > 0) { - var bounds = this.map.createBounds(); - for (var m = 0; m < this.markers.length; m++) { - this.map.extendBoundsWithMarker(bounds, this.markers[m]); - } - if (this.polylines) { - for (var p = 0; p < this.polylines.length; p++) { - this.map.extendBounds(bounds, this.polylines[p]); - } - } - this.map.fitBounds(bounds); - } - } - } - -} diff --git a/ui/src/app/widget/lib/map-widget2.js b/ui/src/app/widget/lib/map-widget2.js deleted file mode 100644 index d1d6af490e..0000000000 --- a/ui/src/app/widget/lib/map-widget2.js +++ /dev/null @@ -1,1782 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import tinycolor from 'tinycolor2'; - -import TbGoogleMap from './google-map'; -import TbOpenStreetMap from './openstreet-map'; -import TbImageMap from './image-map'; -import TbTencentMap from './tencent-map'; - -import {processPattern, arraysEqual, toLabelValueMap, fillPattern, fillPatternWithActions} from './widget-utils'; -import addEntityPanelTemplate from './add-entity-panel.tpl.html'; -import './add-entity-panel.scss'; - -export default class TbMapWidgetV2 { - - constructor(mapProvider, drawRoutes, ctx, useDynamicLocations, $element, isEdit) { - var tbMap = this; - this.ctx = ctx; - this.mapProvider = mapProvider; - if (!$element) { - $element = ctx.$container; - } - this.utils = ctx.$scope.$injector.get('utils'); - this.drawRoutes = drawRoutes; - this.isEdit = isEdit ? isEdit : false; - this.markers = []; - this.polygons = []; - if (this.drawRoutes) { - this.polylines = []; - } - - this.locationSettings = {}; - - var settings = ctx.settings; - - this.callbacks = {}; - this.callbacks.onLocationClick = function () { - }; - - if (settings.defaultZoomLevel) { - if (settings.defaultZoomLevel >= 0 && settings.defaultZoomLevel < 21) { - this.defaultZoomLevel = Math.floor(settings.defaultZoomLevel); - } - } - - if (angular.isUndefined(settings.defaultCenterPosition)) { - settings.defaultCenterPosition = [0,0]; - } else if (angular.isString(settings.defaultCenterPosition)) { - settings.defaultCenterPosition = settings.defaultCenterPosition.split(',').map(x => +x); - } - - this.dontFitMapBounds = settings.fitMapBounds === false; - - if (!useDynamicLocations) { - this.subscription = this.ctx.defaultSubscription; - } - - this.configureLocationsSettings(); - - var minZoomLevel = this.drawRoutes ? 18 : 15; - - let markerClusteringSetting = { - isMarketCluster: false - }; - - if (settings.useClusterMarkers === true){ - if (mapProvider === 'google-map' || mapProvider === 'tencent-map') { - markerClusteringSetting = { - isMarketCluster: true, - zoomOnClick: settings.zoomOnClick, - averageCenter: true - }; - if(angular.isDefined(settings.maxZoom) && settings.maxZoom >= 0 && settings.maxZoom < 19){ - markerClusteringSetting.maxZoom = Math.floor(settings.maxZoom); - } - if(angular.isDefined(settings.gridSize) && settings.gridSize > 0){ - markerClusteringSetting.gridSize = Math.floor(settings.gridSize); - } - if(angular.isDefined(settings.minimumClusterSize) && settings.minimumClusterSize > 1){ - markerClusteringSetting.minimumClusterSize = Math.ceil(settings.minimumClusterSize); - } - } else if(mapProvider === 'openstreet-map' || mapProvider === 'here') { - markerClusteringSetting = { - isMarketCluster: true, - zoomToBoundsOnClick: settings.zoomOnClick, - showCoverageOnHover: settings.showCoverageOnHover, - removeOutsideVisibleBounds: settings.removeOutsideVisibleBounds, - animate: settings.animate, - chunkedLoading: settings.chunkedLoading - }; - if(angular.isDefined(settings.maxClusterRadius) && settings.maxClusterRadius > 0){ - markerClusteringSetting.maxClusterRadius = Math.floor(settings.maxClusterRadius); - } - if(angular.isDefined(settings.maxZoom) && settings.maxZoom >= 0 && settings.maxZoom < 19){ - markerClusteringSetting.disableClusteringAtZoom = Math.floor(settings.maxZoom); - } - } - } - - - - - var initCallback = function () { - tbMap.update(); - tbMap.resize(); - }; - - this.ctx.$scope.onTooltipAction = function (event, actionName, dsIndex) { - tbMap.onTooltipAction(event, actionName, dsIndex); - }; - this.tooltipActionsMap = {}; - var descriptors = this.ctx.actionsApi.getActionDescriptors('tooltipAction'); - descriptors.forEach(function (descriptor) { - tbMap.tooltipActionsMap[descriptor.name] = descriptor; - }); - - let openStreetMapProvider = {}; - if (mapProvider === 'google-map') { - this.map = new TbGoogleMap($element, this.utils, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, settings.disableScrollZooming, minZoomLevel, settings.gmApiKey, settings.gmDefaultMapType, settings.defaultCenterPosition, markerClusteringSetting); - } else if (mapProvider === 'openstreet-map') { - if (settings.useCustomProvider && settings.customProviderTileUrl) { - openStreetMapProvider.name = settings.customProviderTileUrl; - openStreetMapProvider.isCustom = true; - } else { - openStreetMapProvider.name = settings.mapProvider; - } - this.map = new TbOpenStreetMap($element, this.utils, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, settings.disableScrollZooming, minZoomLevel, openStreetMapProvider, null,settings.defaultCenterPosition, markerClusteringSetting); - } else if (mapProvider === 'here') { - openStreetMapProvider.name = settings.mapProvider; - this.map = new TbOpenStreetMap($element, this.utils, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, settings.disableScrollZooming, minZoomLevel, openStreetMapProvider, settings.credentials, settings.defaultCenterPosition, markerClusteringSetting); - } else if (mapProvider === 'image-map') { - this.map = new TbImageMap(this.ctx, $element, this.utils, initCallback, - settings.mapImageUrl, - settings.disableScrollZooming, - settings.posFunction, - settings.imageEntityAlias, - settings.imageUrlAttribute, - settings.useDefaultCenterPosition ? settings.defaultCenterPosition: null); - } else if (mapProvider === 'tencent-map') { - this.map = new TbTencentMap($element, this.utils, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, settings.disableScrollZooming, minZoomLevel, settings.tmApiKey, settings.tmDefaultMapType, settings.defaultCenterPosition, markerClusteringSetting); - } - - - tbMap.initBounds = true; - } - - setCallbacks(callbacks) { - Object.assign(this.callbacks, callbacks); - } - - clearLocations() { - if (this.locations) { - var tbMap = this; - this.locations.forEach(function (location) { - if (location.marker) { - tbMap.map.removeMarker(location.marker); - } - if (location.polyline) { - tbMap.map.removePolyline(location.polyline); - } - if (location.polygon) { - tbMap.map.removePolygon(location.polygon); - } - }); - this.locations = null; - this.markers = []; - this.polygons = []; - if (this.drawRoutes) { - this.polylines = []; - } - } - } - - setSubscription(subscription) { - this.subscription = subscription; - this.clearLocations(); - } - - configureLocationsSettings() { - - if (this.mapProvider == 'image-map') { - this.locationSettings.latKeyName = this.ctx.settings.xPosKeyName || 'xPos'; - this.locationSettings.lngKeyName = this.ctx.settings.yPosKeyName || 'yPos'; - this.locationSettings.markerOffsetX = angular.isDefined(this.ctx.settings.markerOffsetX) ? this.ctx.settings.markerOffsetX : 0.5; - this.locationSettings.markerOffsetY = angular.isDefined(this.ctx.settings.markerOffsetY) ? this.ctx.settings.markerOffsetY : 1; - } else { - this.locationSettings.latKeyName = this.ctx.settings.latKeyName || 'latitude'; - this.locationSettings.lngKeyName = this.ctx.settings.lngKeyName || 'longitude'; - this.locationSettings.polygonKeyName = this.ctx.settings.polygonKeyName || 'coordinates'; - } - - this.locationSettings.tooltipPattern = this.ctx.settings.tooltipPattern || "${entityName}

    Latitude: ${" + this.locationSettings.latKeyName + ":7}
    Longitude: ${" + this.locationSettings.lngKeyName + ":7}"; - - this.locationSettings.showLabel = this.ctx.settings.showLabel !== false; - this.locationSettings.displayTooltip = this.ctx.settings.showTooltip !== false; - this.locationSettings.useDefaultCenterPosition = this.ctx.settings.useDefaultCenterPosition === true; - this.locationSettings.displayTooltipAction = this.ctx.settings.showTooltipAction && this.ctx.settings.showTooltipAction.length ? this.ctx.settings.showTooltipAction : "click"; - this.locationSettings.autocloseTooltip = this.ctx.settings.autocloseTooltip !== false; - this.locationSettings.showPolygon = this.ctx.settings.showPolygon === true; - this.locationSettings.labelColor = this.ctx.widgetConfig.color || '#000000'; - this.locationSettings.label = this.ctx.settings.label || "${entityName}"; - this.locationSettings.color = this.ctx.settings.color ? tinycolor(this.ctx.settings.color).toHexString() : "#FE7569"; - this.locationSettings.polygonColor = this.ctx.settings.polygonColor ? tinycolor(this.ctx.settings.polygonColor).toHexString() : "#0000ff"; - this.locationSettings.polygonStrokeColor = this.ctx.settings.polygonStrokeColor ? tinycolor(this.ctx.settings.polygonStrokeColor).toHexString() : "#fe0001"; - this.locationSettings.polygonOpacity = angular.isDefined(this.ctx.settings.polygonOpacity) ? this.ctx.settings.polygonOpacity : 0.5; - this.locationSettings.polygonStrokeOpacity = angular.isDefined(this.ctx.settings.polygonStrokeOpacity) ? this.ctx.settings.polygonStrokeOpacity : 1; - this.locationSettings.polygonStrokeWeight = angular.isDefined(this.ctx.settings.polygonStrokeWeight) ? this.ctx.settings.polygonStrokeWeight : 1; - - this.locationSettings.useLabelFunction = this.ctx.settings.useLabelFunction === true; - if (angular.isDefined(this.ctx.settings.labelFunction) && this.ctx.settings.labelFunction.length > 0) { - try { - this.locationSettings.labelFunction = new Function('data, dsData, dsIndex', this.ctx.settings.labelFunction); - } catch (e) { - this.locationSettings.labelFunction = null; - } - } - - this.locationSettings.useTooltipFunction = this.ctx.settings.useTooltipFunction === true; - if (angular.isDefined(this.ctx.settings.tooltipFunction) && this.ctx.settings.tooltipFunction.length > 0) { - try { - this.locationSettings.tooltipFunction = new Function('data, dsData, dsIndex', this.ctx.settings.tooltipFunction); - } catch (e) { - this.locationSettings.tooltipFunction = null; - } - } - - this.locationSettings.useColorFunction = this.ctx.settings.useColorFunction === true; - if (angular.isDefined(this.ctx.settings.colorFunction) && this.ctx.settings.colorFunction.length > 0) { - try { - this.locationSettings.colorFunction = new Function('data, dsData, dsIndex', this.ctx.settings.colorFunction); - } catch (e) { - this.locationSettings.colorFunction = null; - } - } - this.locationSettings.usePolygonColorFunction = this.ctx.settings.usePolygonColorFunction === true; - if (angular.isDefined(this.ctx.settings.polygonColorFunction) && this.ctx.settings.polygonColorFunction.length > 0) { - try { - this.locationSettings.polygonColorFunction = new Function('data, dsData, dsIndex', this.ctx.settings.polygonColorFunction); - } catch (e) { - this.locationSettings.polygonColorFunction = null; - } - } - - this.locationSettings.useMarkerImageFunction = this.ctx.settings.useMarkerImageFunction === true; - if (angular.isDefined(this.ctx.settings.markerImageFunction) && this.ctx.settings.markerImageFunction.length > 0) { - try { - this.locationSettings.markerImageFunction = new Function('data, images, dsData, dsIndex', this.ctx.settings.markerImageFunction); - } catch (e) { - this.locationSettings.markerImageFunction = null; - } - } - - this.locationSettings.markerImages = this.ctx.settings.markerImages || []; - - if (!this.locationSettings.useMarkerImageFunction && - angular.isDefined(this.ctx.settings.markerImage) && - this.ctx.settings.markerImage.length > 0) { - this.locationSettings.useMarkerImage = true; - var url = this.ctx.settings.markerImage; - var size = this.ctx.settings.markerImageSize || 34; - this.locationSettings.currentImage = { - url: url, - size: size - }; - } - - if (this.drawRoutes) { - this.locationSettings.strokeWeight = this.ctx.settings.strokeWeight || 2; - this.locationSettings.strokeOpacity = this.ctx.settings.strokeOpacity || 1.0; - } - } - - onTooltipAction(event, actionName, dsIndex) { - var descriptor = this.tooltipActionsMap[actionName]; - if (descriptor) { - var datasource = this.subscription.datasources[dsIndex]; - var entityId = {}; - entityId.id = datasource.entityId; - entityId.entityType = datasource.entityType; - var entityName = datasource.entityName; - var entityLabel = datasource.entityLabel; - this.ctx.actionsApi.handleWidgetAction(event, descriptor, entityId, entityName, null, entityLabel); - } - } - - selectEntity($event) { - var tbMap = this; - - function setDefaultPosition(entity) { - let position = tbMap.map.getCenter(); - if (tbMap.mapProvider === "image-map") { - position = tbMap.map.latLngToPoint(position); - position.lat = position.x / tbMap.map.width; - position.lng = position.y / tbMap.map.height; - } - - tbMap.saveMarkerLocation( - entity, - locationsWithoutMarker[entitiesWithoutPosition.indexOf(entity)], - position - ); - } - - const element = angular.element($event.target); - const $mdPanel = this.ctx.$scope.$injector.get('$mdPanel'); - const $document = this.ctx.$scope.$injector.get('$document'); - let position = $mdPanel.newPanelPosition() - .relativeTo(element) - .addPanelPosition($mdPanel.xPosition.ALIGN_END, $mdPanel.yPosition.BELOW); - - let locationsWithoutMarker = this.locations.filter((location) => !location.marker); - let entitiesWithoutPosition = []; - for (let i = 0; i < locationsWithoutMarker.length; i++) { - entitiesWithoutPosition.push(this.subscription.datasources[locationsWithoutMarker[i].dsIndex]); - } - - if(entitiesWithoutPosition.length === 1){ - setDefaultPosition(entitiesWithoutPosition[0]); - } else { - let config = { - attachTo: angular.element($document[0].body), - controller: addEntityPanelController, - controllerAs: 'vm', - templateUrl: addEntityPanelTemplate, - panelClass: 'tb-add-entity-panel', - position: position, - fullscreen: false, - locals: { - 'entities': entitiesWithoutPosition, - 'onClose': setDefaultPosition - }, - openFrom: $event, - clickOutsideToClose: true, - escapeToClose: true, - focusOnOpen: false - }; - $mdPanel.open(config); - } - } - - saveMarkerLocation(datasource, location, coordinate) { - var tbMap = this; - - const types = tbMap.ctx.$scope.$injector.get('types'); - const $q = tbMap.ctx.$scope.$injector.get('$q'); - const attributeService = tbMap.ctx.$scope.$injector.get('attributeService'); - - let attributesLocation = []; - let timeseriesLocation = []; - let promises = []; - - let dataKeys = datasource.dataKeys; - for (let i = 0; i < dataKeys.length; i++) { - if (dataKeys[i].name === location.settings.latKeyName || dataKeys[i].name === location.settings.lngKeyName) { - let newLocation = { - key: dataKeys[i].name, - value: dataKeys[i].name === location.settings.latKeyName ? coordinate.lat : coordinate.lng - }; - if (dataKeys[i].type === types.dataKeyType.attribute) { - attributesLocation.push(newLocation); - } else if (dataKeys[i].type === types.dataKeyType.timeseries) { - timeseriesLocation.push(newLocation); - } - } - } - - if (attributesLocation.length > 0) { - promises.push(attributeService.saveEntityAttributes( - datasource.entityType, - datasource.entityId, - types.attributesScope.server.value, - attributesLocation, - { - ignoreLoading: true - } - )) - } - if (timeseriesLocation.length > 0) { - promises.push(attributeService.saveEntityTimeseries( - datasource.entityType, - datasource.entityId, - "scope", - timeseriesLocation, - { - ignoreLoading: true - } - )) - } - return $q.all([promises]); - } - - update() { - var tbMap = this; - - - function updateLocationLabel(location, dataMap) { - if (location.settings.showLabel) { - if (location.settings.useLabelFunction && location.settings.labelFunction) { - try { - location.settings.label = location.settings.labelFunction(dataMap.dataMap, dataMap.dsDataMap, location.dsIndex); - } catch (e) { - location.settings.label = null; - } - if (location.settings.label) { - var datasources = tbMap.subscription.datasources; - location.settings.label = tbMap.utils.createLabelFromDatasource(datasources[location.dsIndex], location.settings.label); - location.settings.labelReplaceInfo = processPattern(location.settings.label, datasources, location.dsIndex); - location.settings.labelText = location.settings.label; - } - } - if (location.settings.labelReplaceInfo.variables.length) { - location.settings.labelText = fillPattern(location.settings.label, - location.settings.labelReplaceInfo, tbMap.subscription.data); - } - tbMap.map.updateMarkerLabel(location.marker, location.settings); - } - } - - - function calculateLocationColor(location, dataMap) { - if (location.settings.useColorFunction && location.settings.colorFunction) { - var color; - try { - color = location.settings.colorFunction(dataMap.dataMap, dataMap.dsDataMap, location.dsIndex); - } catch (e) {/**/ - } - if (!color) { - color = '#FE7569'; - } - return tinycolor(color).toHexString(); - } else { - return location.settings.color; - } - } - - function calculateLocationPolygonColor(location, dataMap) { - if (location.settings.usePolygonColorFunction && location.settings.polygonColorFunction) { - var color; - try { - color = location.settings.polygonColorFunction(dataMap.dataMap, dataMap.dsDataMap, location.dsIndex); - } catch (e) {/**/ - } - if (!color) { - color = '#007800'; - } - return tinycolor(color).toHexString(); - } else { - return location.settings.polygonColor; - } - } - - function updateLocationColor(location, color, image) { - if (!location.settings.calculatedColor || location.settings.calculatedColor !== color) { - if (!location.settings.useMarkerImage && !image) { - tbMap.map.updateMarkerColor(location.marker, color); - } - if (location.polyline) { - tbMap.map.updatePolylineColor(location.polyline, location.settings, color); - } - location.settings.calculatedColor = color; - } - } - - function updateLocationPolygonColor(location, color) { - if (location.polygon && color) { - location.settings.calculatedPolygonColor = color; - tbMap.map.updatePolygonColor(location.polygon, location.settings, color); - } - } - - function calculateLocationMarkerImage(location, dataMap) { - if (location.settings.useMarkerImageFunction && location.settings.markerImageFunction) { - var image = null; - try { - image = location.settings.markerImageFunction(dataMap.dataMap, location.settings.markerImages, dataMap.dsDataMap, location.dsIndex); - } catch (e) { - image = null; - } - return image; - } else { - return null; - } - } - - function updateLocationMarkerIcon(location, image) { - if (image && (!location.settings.currentImage || !angular.equals(location.settings.currentImage, image))) { - location.settings.currentImage = image; - tbMap.map.updateMarkerIcon(location.marker, location.settings); - } - } - - function updateLocationStyle(location, dataMap) { - updateLocationLabel(location, dataMap); - var color = calculateLocationColor(location, dataMap); - var image = calculateLocationMarkerImage(location, dataMap); - updateLocationColor(location, color, image); - updateLocationMarkerIcon(location, image); - } - - function createOrUpdateLocationMarker(location, markerLocation, dataMap) { - var changed = false; - if (!location.marker) { - var image = calculateLocationMarkerImage(location, dataMap); - if (image && (!location.settings.currentImage || !angular.equals(location.settings.currentImage, image))) { - location.settings.currentImage = image; - } - location.marker = tbMap.map.createMarker(markerLocation, location.dsIndex, location.settings, - function (event) { - tbMap.callbacks.onLocationClick(location); - locationRowClick(event, location); - }, [location.dsIndex], - function (event) { - markerDragend(event, location) - }); - tbMap.markers.push(location.marker); - changed = true; - } else { - var prevPosition = tbMap.map.getMarkerPosition(location.marker); - if (!prevPosition.equals(markerLocation)) { - tbMap.map.setMarkerPosition(location.marker, markerLocation); - changed = true; - } - } - return changed; - } - - function markerDragend($event, location) { - if (location.settings.drraggable) { - let position = tbMap.map.getMarkerPosition(location.marker); - if (tbMap.mapProvider === "image-map") { - position.lat = position.x; - position.lng = position.y; - delete position.x; - delete position.y; - } else if (tbMap.mapProvider === "google-map") { - position = position.toJSON(); - } - - tbMap.saveMarkerLocation(tbMap.subscription.datasources[location.dsIndex], location, position); - } - } - - function locationRowClick($event, location) { - var descriptors = tbMap.ctx.actionsApi.getActionDescriptors('markerClick'); - if (descriptors.length) { - var datasource = tbMap.subscription.datasources[location.dsIndex]; - var entityId = {}; - entityId.id = datasource.entityId; - entityId.entityType = datasource.entityType; - var entityName = datasource.entityName; - var entityLabel = datasource.entityLabel; - tbMap.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName, null, entityLabel); - } - } - function locationPolygonClick($event, location) { - var descriptors = tbMap.ctx.actionsApi.getActionDescriptors('polygonClick'); - if (descriptors.length) { - var datasource = tbMap.subscription.datasources[location.dsIndex]; - var entityId = {}; - entityId.id = datasource.entityId; - entityId.entityType = datasource.entityType; - var entityName = datasource.entityName; - var entityLabel = datasource.entityLabel; - tbMap.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName, null, entityLabel); - } - } - - function updateLocation(location, data, dataMap) { - var locationChanged = false; - if (location.latIndex > -1 && location.lngIndex > -1) { - var latData = data[location.latIndex].data; - var lngData = data[location.lngIndex].data; - var lat, lng, latLng; - - if (tbMap.drawRoutes) { - // Create or update route - var latLngs = []; - for (var i = 0; i < latData.length; i++) { - lat = latData[i][1]; - lng = lngData[i][1]; - if (angular.isDefined(lat) && lat != null && angular.isDefined(lng) && lng != null) { - latLng = tbMap.map.createLatLng(lat, lng); - if (i == 0 || !latLngs[latLngs.length - 1].equals(latLng)) { - latLngs.push(latLng); - } - } - } - if (latLngs.length > 0) { - var markerLocation = latLngs[latLngs.length - 1]; - createOrUpdateLocationMarker(location, markerLocation, dataMap); - } else if (location.marker) { - tbMap.map.removeMarker(location.marker); - delete location.marker; - } - if (!location.polyline) { - location.polyline = tbMap.map.createPolyline(latLngs, location.settings); - tbMap.polylines.push(location.polyline); - locationChanged = true; - } else { - var prevPath = tbMap.map.getPolylineLatLngs(location.polyline); - if (!prevPath || !arraysEqual(prevPath, latLngs)) { - tbMap.map.setPolylineLatLngs(location.polyline, latLngs); - locationChanged = true; - } - } - } else if (latData.length > 0 && lngData.length > 0) { - // Create or update marker - lat = latData[latData.length - 1][1]; - lng = lngData[lngData.length - 1][1]; - if (angular.isDefined(lat) && lat != null && angular.isDefined(lng) && lng != null) { - latLng = tbMap.map.createLatLng(lat, lng); - if (createOrUpdateLocationMarker(location, latLng, dataMap)) { - locationChanged = true; - } - } - } - if (location.marker) { - updateLocationStyle(location, dataMap); - } - - } - return locationChanged; - } - - function createUpdatePolygon(location, dataMap) { - if (location.settings.showPolygon && dataMap.dsDataMap[location.dsIndex][location.settings.polygonKeyName] !== null) { - let polygonLatLngsRaw = angular.fromJson(dataMap.dsDataMap[location.dsIndex][location.settings.polygonKeyName]); - let polygonLatLngs = !polygonLatLngsRaw || mapPolygonArray(polygonLatLngsRaw); - if (!location.polygon && polygonLatLngs.length > 0) { - location.polygon = tbMap.map.createPolygon(polygonLatLngs, location.settings, location, function (event) { - tbMap.callbacks.onLocationClick(location); - locationPolygonClick(event, location); - }, [location.dsIndex]); - tbMap.polygons.push(location.polygon); - if (location.settings.usePolygonColorFunction) updateLocationPolygonColor(location, calculateLocationPolygonColor(location, dataMap)); - } else if (polygonLatLngs.length > 0) { - let prevPolygonArr = tbMap.map.getPolygonLatLngs(location.polygon); - if (!prevPolygonArr || !arraysEqual(prevPolygonArr, polygonLatLngs)) { - tbMap.map.setPolygonLatLngs(location.polygon, polygonLatLngs); - } - if (location.settings.usePolygonColorFunction) updateLocationPolygonColor(location, calculateLocationPolygonColor(location, dataMap)); - } - } - } - - function loadLocations(data, datasources) { - var bounds = tbMap.locationSettings.useDefaultCenterPosition ? tbMap.map.createBounds().extend(tbMap.map.map.getCenter()) : tbMap.map.createBounds(); - tbMap.locations = []; - var dataMap = toLabelValueMap(data, datasources); - var currentDatasource = null; - var currentDatasourceIndex = -1; - var latIndex = -1; - var lngIndex = -1; - for (var i = 0; i < data.length; i++) { - var dataKeyData = data[i]; - var dataKey = dataKeyData.dataKey; - if (dataKeyData.datasource != currentDatasource) { - currentDatasource = dataKeyData.datasource; - currentDatasourceIndex++; - latIndex = -1; - lngIndex = -1; - } - var nameToCheck; - if (dataKey.locationAttrName) { - nameToCheck = dataKey.locationAttrName; - } else { - nameToCheck = dataKey.label; - } - if (nameToCheck === tbMap.locationSettings.latKeyName) { - latIndex = i; - } else if (nameToCheck === tbMap.locationSettings.lngKeyName) { - lngIndex = i; - } - if (latIndex > -1 && lngIndex > -1) { - var location = { - latIndex: latIndex, - lngIndex: lngIndex, - dsIndex: currentDatasourceIndex, - settings: angular.copy(tbMap.locationSettings) - }; - if (location.settings.showLabel) { - location.settings.label = tbMap.utils.createLabelFromDatasource(currentDatasource, location.settings.label); - location.settings.labelReplaceInfo = processPattern(location.settings.label, datasources, currentDatasourceIndex); - location.settings.labelText = location.settings.label; - } - if (location.settings.displayTooltip) { - location.settings.tooltipPattern = tbMap.utils.createLabelFromDatasource(currentDatasource, location.settings.tooltipPattern); - location.settings.tooltipReplaceInfo = processPattern(location.settings.tooltipPattern, datasources, currentDatasourceIndex); - } - location.settings.drraggable = tbMap.isEdit; - tbMap.locations.push(location); - updateLocation(location, data, dataMap); - if (!tbMap.locationSettings.useDefaultCenterPosition) { - if (location.polyline) { - tbMap.map.extendBounds(bounds, location.polyline); - } else if (location.marker) { - tbMap.map.extendBoundsWithMarker(bounds, location.marker); - } - } - latIndex = -1; - lngIndex = -1; - } - } - data.forEach(function (dataEl, index) { - let nameToCheck; - if (dataEl.dataKey.locationAttrName) { - nameToCheck = dataEl.dataKey.locationAttrName; - } else { - nameToCheck = dataEl.dataKey.label; - } - if (nameToCheck === tbMap.locationSettings.polygonKeyName) { - let location = { - polIndex: index, - settings: angular.copy(tbMap.locationSettings) - }; - location.dsIndex = datasources.findIndex(function (ds) { - return dataEl.datasource.entityId === ds.entityId; - }); - tbMap.locations.push(location); - createUpdatePolygon(location, dataMap); - if (!tbMap.locationSettings.useDefaultCenterPosition) { - tbMap.map.extendBounds(bounds, location.polygon); - } - } - }); - - tbMap.map.fitBounds(bounds, tbMap.isEdit && tbMap.markers.length === 1); - } - - function mapPolygonArray (rawArray) { - let latLngArray = rawArray.map(function (el) { - if (el.length === 2) { - if (!angular.isNumber(el[0]) && !angular.isNumber(el[1])) { - return el.map(function (subEl) { - return mapPolygonArray(subEl); - }) - } else { - return tbMap.map.createLatLng(el[0], el[1]); - } - } else if (el.length > 2) { - return mapPolygonArray(el); - } else { - return tbMap.map.createLatLng(false); - } - }); - return latLngArray; - } - - function updateLocations(data, datasources) { - var locationsChanged = false; - var bounds = tbMap.locationSettings.useDefaultCenterPosition ? tbMap.map.createBounds().extend(tbMap.map.map.getCenter()) : tbMap.map.createBounds(); - var dataMap = toLabelValueMap(data, datasources); - for (var p = 0; p < tbMap.locations.length; p++) { - var location = tbMap.locations[p]; - locationsChanged |= updateLocation(location, data, dataMap); - createUpdatePolygon(location, dataMap); - if (!tbMap.locationSettings.useDefaultCenterPosition) { - if (location.polyline) { - tbMap.map.extendBounds(bounds, location.polyline); - } else if (location.marker) { - tbMap.map.extendBoundsWithMarker(bounds, location.marker); - } else if (location.polygon) { - tbMap.map.extendBounds(bounds, location.polygon); - } - } - } - if (locationsChanged && tbMap.initBounds) { - let dataReceived = datasources.every( - function (ds) { - return ds.dataReceived === true; - }); - let dataValid = dataReceived && dataMap.dsDataMap.every(function (ds) { - return !(!ds[tbMap.locationSettings.latKeyName] && !ds[tbMap.locationSettings.lngKeyName]); - }); - tbMap.initBounds = !dataValid; - - if(!tbMap.isEdit && tbMap.markers.length !== 1 || tbMap.polylines || tbMap.polygons) { - tbMap.map.fitBounds(bounds); - } - } - } - - function createTooltipContent(tooltip, data, datasources) { - var content; - var settings = tooltip.locationSettings; - if (settings.useTooltipFunction && settings.tooltipFunction) { - var dataMap = toLabelValueMap(data, datasources); - try { - settings.tooltipPattern = settings.tooltipFunction(dataMap.dataMap, dataMap.dsDataMap, tooltip.dsIndex); - } catch (e) { - settings.tooltipPattern = null; - } - if (settings.tooltipPattern) { - settings.tooltipPattern = tbMap.utils.createLabelFromDatasource(datasources[tooltip.dsIndex], settings.tooltipPattern); - settings.tooltipReplaceInfo = processPattern(settings.tooltipPattern, datasources, tooltip.dsIndex); - } - } - content = fillPattern(settings.tooltipPattern, settings.tooltipReplaceInfo, data); - return fillPatternWithActions(content, 'onTooltipAction', tooltip.markerArgs); - } - - if (this.map && this.map.inited() && this.subscription) { - if (this.subscription.data) { - if (!this.locations) { - loadLocations(this.subscription.data, this.subscription.datasources); - - } else { - updateLocations(this.subscription.data, this.subscription.datasources); - } - var tooltips = this.map.getTooltips(); - for (var t = 0; t < tooltips.length; t++) { - var tooltip = tooltips[t]; - var text = createTooltipContent(tooltip, this.subscription.data, this.subscription.datasources); - tooltip.popup.setContent(text); - } - } - } - } - - resize() { - if (this.map && this.map.inited()) { - let map = this.map; - if (this.locations && this.locations.length > 0) { - map.invalidateSize(); - var bounds = this.locationSettings.useDefaultCenterPosition ? map.createBounds().extend(map.map.getCenter()) : map.createBounds(); - if (!this.locationSettings.useDefaultCenterPosition) { - if (this.markers && this.markers.length > 0) { - this.markers.forEach(function (marker) { - map.extendBoundsWithMarker(bounds, marker); - }); - } - if (this.polylines && this.polylines.length > 0) { - this.polylines.forEach(function (polyline) { - map.extendBounds(bounds, polyline); - }) - } - if (this.polygons && this.polygons.length > 0) { - this.polygons.forEach(function (polygon) { - map.extendBounds(bounds, polygon); - }) - } - } - if((!map.isEdit && map.markers && map.markers.length !== 1) || - (this.polylines && this.polylines.length > 0) || - (this.polygons && this.polygons.length > 0)) { - map.fitBounds(bounds); - } - } - } - } - - static settingsSchema(mapProvider, drawRoutes) { - var schema; - if (mapProvider === 'google-map') { - schema = angular.copy(googleMapSettingsSchema); - schema.groupInfoes=[{ - "formIndex":0, - "GroupTitle":"Google Map Settings" - }]; - } else if (mapProvider === 'openstreet-map') { - schema = angular.copy(openstreetMapSettingsSchema); - schema.groupInfoes=[{ - "formIndex":0, - "GroupTitle":"Openstreet Map Settings" - }]; - } else if (mapProvider === 'image-map') { - return imageMapSettingsSchema; - } else if (mapProvider === 'tencent-map') { - schema = angular.copy(tencentMapSettingsSchema); - schema.groupInfoes=[{ - "formIndex":0, - "GroupTitle":"Tencent Map Settings" - }]; - } else if (mapProvider === 'here') { - schema = angular.copy(hereMapSettingsSchema); - schema.groupInfoes=[{ - "formIndex":0, - "GroupTitle":"Here Map Settings" - }]; - } - if(!schema.groupInfoes)schema.groupInfoes=[]; - schema.form = [schema.form]; - - angular.merge(schema.schema.properties, commonMapSettingsSchema.schema.properties); - schema.schema.required = schema.schema.required.concat(commonMapSettingsSchema.schema.required); - schema.form.push(commonMapSettingsSchema.form);//schema.form.concat(commonMapSettingsSchema.form); - schema.groupInfoes.push({ - "formIndex":schema.groupInfoes.length, - "GroupTitle":"Common Map Settings" - }); - if (drawRoutes) { - angular.merge(schema.schema.properties, routeMapSettingsSchema.schema.properties); - schema.schema.required = schema.schema.required.concat(routeMapSettingsSchema.schema.required); - schema.form.push(routeMapSettingsSchema.form);//schema.form = schema.form.concat(routeMapSettingsSchema.form); - schema.groupInfoes.push({ - "formIndex":schema.groupInfoes.length, - "GroupTitle":"Route Map Settings" - }); - } else if (mapProvider !== 'image-map'){ - angular.merge(schema.schema.properties, markerClusteringSettingsSchema.schema.properties); - schema.schema.required = schema.schema.required.concat(markerClusteringSettingsSchema.schema.required); - schema.form.push(markerClusteringSettingsSchema.form); - if (mapProvider === 'google-map' || mapProvider === 'tencent-map') { - angular.merge(schema.schema.properties, markerClusteringSettingsSchemaGoogle.schema.properties); - schema.schema.required = schema.schema.required.concat(markerClusteringSettingsSchemaGoogle.schema.required); - schema.form[schema.form.length -1] = schema.form[schema.form.length -1].concat(markerClusteringSettingsSchemaGoogle.form); - } - if (mapProvider === 'openstreet-map' || mapProvider === 'here') { - angular.merge(schema.schema.properties, markerClusteringSettingsSchemaLeaflet.schema.properties); - schema.schema.required = schema.schema.required.concat(markerClusteringSettingsSchemaLeaflet.schema.required); - schema.form[schema.form.length -1] = schema.form[schema.form.length -1].concat(markerClusteringSettingsSchemaLeaflet.form); - } - schema.groupInfoes.push({ - "formIndex":schema.groupInfoes.length, - "GroupTitle":"Markers Clustering Settings" - }); - } - return schema; - } - - static dataKeySettingsSchema(/*mapProvider*/) { - return {}; - } - - static actionSources() { - return { - 'markerClick': { - name: 'widget-action.marker-click', - multiple: false - }, - 'polygonClick': { - name: 'widget-action.polygon-click', - multiple: false - }, - 'tooltipAction': { - name: 'widget-action.tooltip-tag-action', - multiple: true - } - }; - } - -} - -const googleMapSettingsSchema = - { - "schema": { - "title": "Google Map Configuration", - "type": "object", - "properties": { - "gmApiKey": { - "title": "Google Maps API Key", - "type": "string" - }, - "gmDefaultMapType": { - "title": "Default map type", - "type": "string", - "default": "roadmap" - } - }, - "required": [ - "gmApiKey" - ] - }, - "form": [ - "gmApiKey", - { - "key": "gmDefaultMapType", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "roadmap", - "label": "Roadmap" - }, - { - "value": "satellite", - "label": "Satellite" - }, - { - "value": "hybrid", - "label": "Hybrid" - }, - { - "value": "terrain", - "label": "Terrain" - } - ] - } - ] - }; - -const tencentMapSettingsSchema = - { - "schema": { - "title": "Tencent Map Configuration", - "type": "object", - "properties": { - "tmApiKey": { - "title": "Tencent Maps API Key", - "type": "string" - }, - "tmDefaultMapType": { - "title": "Default map type", - "type": "string", - "default": "roadmap" - } - }, - "required": [ - "tmApiKey" - ] - }, - "form": [ - "tmApiKey", - { - "key": "tmDefaultMapType", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "roadmap", - "label": "Roadmap" - }, - { - "value": "satellite", - "label": "Satellite" - }, - { - "value": "hybrid", - "label": "Hybrid" - }, - ] - } - ] - }; - -const hereMapSettingsSchema = - { - "schema": { - "title": "HERE Map Configuration", - "type": "object", - "properties": { - "mapProvider": { - "title": "Map layer", - "type": "string", - "default": "HERE.normalDay" - }, - "credentials":{ - "type": "object", - "properties": { - "app_id": { - "title": "HERE app id", - "type": "string" - }, - "app_code": { - "title": "HERE app code", - "type": "string" - } - }, - "required": ["app_id", "app_code"] - } - }, - "required": [] - }, - "form": [ - { - "key": "mapProvider", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "HERE.normalDay", - "label": "HERE.normalDay (Default)" - }, - { - "value": "HERE.normalNight", - "label": "HERE.normalNight" - }, - { - "value": "HERE.hybridDay", - "label": "HERE.hybridDay" - }, - { - "value": "HERE.terrainDay", - "label": "HERE.terrainDay" - } - ] - }, - "credentials" - ] - }; - -const openstreetMapSettingsSchema = - { - "schema": { - "title": "Openstreet Map Configuration", - "type": "object", - "properties": { - "mapProvider": { - "title": "Map provider", - "type": "string", - "default": "OpenStreetMap.Mapnik" - }, - "useCustomProvider": { - "title": "Use custom provider", - "type": "boolean", - "default": false - }, - "customProviderTileUrl": { - "title": "Custom provider tile URL", - "type": "string", - "default": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" - } - }, - "required": [] - }, - "form": [ - { - "key": "mapProvider", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "OpenStreetMap.Mapnik", - "label": "OpenStreetMap.Mapnik (Default)" - }, - { - "value": "OpenStreetMap.BlackAndWhite", - "label": "OpenStreetMap.BlackAndWhite" - }, - { - "value": "OpenStreetMap.HOT", - "label": "OpenStreetMap.HOT" - }, - { - "value": "Esri.WorldStreetMap", - "label": "Esri.WorldStreetMap" - }, - { - "value": "Esri.WorldTopoMap", - "label": "Esri.WorldTopoMap" - }, - { - "value": "CartoDB.Positron", - "label": "CartoDB.Positron" - }, - { - "value": "CartoDB.DarkMatter", - "label": "CartoDB.DarkMatter" - } - ] - }, - "useCustomProvider", - "customProviderTileUrl" - ] - }; - -const commonMapSettingsSchema = - { - "schema": { - "title": "Map Configuration", - "type": "object", - "properties": { - "defaultZoomLevel": { - "title": "Default map zoom level (0 - 20)", - "type": "number" - }, - "useDefaultCenterPosition": { - "title": "Use default map center position", - "type": "boolean", - "default": false - }, - "defaultCenterPosition": { - "title": "Default map center position (0,0)", - "type": "string", - "default" : "0,0" - }, - "fitMapBounds": { - "title": "Fit map bounds to cover all markers", - "type": "boolean", - "default": true - }, - "disableScrollZooming": { - "title": "Disable scroll zooming", - "type": "boolean", - "default": false - }, - "latKeyName": { - "title": "Latitude key name", - "type": "string", - "default": "latitude" - }, - "lngKeyName": { - "title": "Longitude key name", - "type": "string", - "default": "longitude" - }, - "showLabel": { - "title": "Show label", - "type": "boolean", - "default": true - }, - "label": { - "title": "Label (pattern examples: '${entityName}', '${entityName}: (Text ${keyName} units.)' )", - "type": "string", - "default": "${entityName}" - }, - "useLabelFunction": { - "title": "Use label function", - "type": "boolean", - "default": false - }, - "labelFunction": { - "title": "Label function: f(data, dsData, dsIndex)", - "type": "string" - }, - "showTooltip": { - "title": "Show tooltip", - "type": "boolean", - "default": true - }, - "showTooltipAction": { - "title": "Action for displaying the tooltip", - "type": "string", - "default": "click" - }, - "autocloseTooltip": { - "title": "Auto-close tooltips", - "type": "boolean", - "default": true - }, - "tooltipPattern": { - "title": "Tooltip (for ex. 'Text ${keyName} units.' or Link text')", - "type": "string", - "default": "${entityName}

    Latitude: ${latitude:7}
    Longitude: ${longitude:7}" - }, - "useTooltipFunction": { - "title": "Use tooltip function", - "type": "boolean", - "default": false - }, - "tooltipFunction": { - "title": "Tooltip function: f(data, dsData, dsIndex)", - "type": "string" - }, - "color": { - "title": "Color", - "type": "string" - }, - "useColorFunction": { - "title": "Use color function", - "type": "boolean", - "default": false - }, - "colorFunction": { - "title": "Color function: f(data, dsData, dsIndex)", - "type": "string" - }, - "showPolygon": { - "title": "Show polygon", - "type": "boolean", - "default": false - }, - "polygonKeyName": { - "title": "Polygon key name", - "type": "string", - "default": "coordinates" - }, - "polygonColor": { - "title": "Polygon color", - "type": "string" - }, - "polygonOpacity": { - "title": "Polygon opacity", - "type": "number", - "default": 0.5 - }, - "polygonStrokeColor": { - "title": "Stroke color", - "type": "string" - }, - "polygonStrokeOpacity": { - "title": "Stroke opacity", - "type": "number", - "default": 1 - }, - "polygonStrokeWeight": { - "title": "Stroke weight", - "type": "number", - "default": 1 - }, - "usePolygonColorFunction": { - "title": "Use polygon color function", - "type": "boolean", - "default": false - }, - "polygonColorFunction": { - "title": "Polygon Color function: f(data, dsData, dsIndex)", - "type": "string" - }, - "markerImage": { - "title": "Custom marker image", - "type": "string" - }, - "markerImageSize": { - "title": "Custom marker image size (px)", - "type": "number", - "default": 34 - }, - "useMarkerImageFunction": { - "title": "Use marker image function", - "type": "boolean", - "default": false - }, - "markerImageFunction": { - "title": "Marker image function: f(data, images, dsData, dsIndex)", - "type": "string" - }, - "markerImages": { - "title": "Marker images", - "type": "array", - "items": { - "title": "Marker image", - "type": "string" - } - } - }, - "required": [] - }, - "form": [ - "defaultZoomLevel", - "useDefaultCenterPosition", - "defaultCenterPosition", - "fitMapBounds", - "disableScrollZooming", - "latKeyName", - "lngKeyName", - "showLabel", - "label", - "useLabelFunction", - { - "key": "labelFunction", - "type": "javascript" - }, - "showTooltip", - { - "key": "showTooltipAction", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "click", - "label": "Show tooltip on click (Default)" - }, - { - "value": "hover", - "label": "Show tooltip on hover" - } - ] - }, - "autocloseTooltip", - { - "key": "tooltipPattern", - "type": "textarea" - }, - "useTooltipFunction", - { - "key": "tooltipFunction", - "type": "javascript" - }, - { - "key": "color", - "type": "color" - }, - "useColorFunction", - { - "key": "colorFunction", - "type": "javascript" - }, "showPolygon", "polygonKeyName", - { - "key": "polygonColor", - "type": "color" - }, - "polygonOpacity", - { - "key": "polygonStrokeColor", - "type": "color" - }, - "polygonStrokeOpacity","polygonStrokeWeight","usePolygonColorFunction", - { - "key": "polygonColorFunction", - "type": "javascript" - }, - { - "key": "markerImage", - "type": "image" - }, - "markerImageSize", - "useMarkerImageFunction", - { - "key": "markerImageFunction", - "type": "javascript" - }, - { - "key": "markerImages", - "items": [ - { - "key": "markerImages[]", - "type": "image" - } - ] - } - ] - }; - -const routeMapSettingsSchema = - { - "schema": { - "title": "Route Map Configuration", - "type": "object", - "properties": { - "strokeWeight": { - "title": "Stroke weight", - "type": "number", - "default": 2 - }, - "strokeOpacity": { - "title": "Stroke opacity", - "type": "number", - "default": 1.0 - } - }, - "required": [] - }, - "form": [ - "strokeWeight", - "strokeOpacity" - ] - }; - -const markerClusteringSettingsSchema = - { - "schema": { - "title": "Markers Clustering Configuration", - "type": "object", - "properties": { - "useClusterMarkers": { - "title": "Use map markers clustering", - "type": "boolean", - "default": false - }, - "zoomOnClick": { - "title": "Zoom when clicking on a cluster", - "type": "boolean", - "default": true - }, - "maxZoom": { - "title": "The maximum zoom level when a marker can be part of a cluster (0 - 18)", - "type": "number" - } - }, - "required": [] - }, - "form": [ - "useClusterMarkers", - "zoomOnClick", - "maxZoom" - ] - }; - -const markerClusteringSettingsSchemaGoogle = - { - "schema": { - "title": "Marker Clustering Configuration Google", - "type": "object", - "properties": { - "gridSize": { - "title": "Maximum radius that a cluster will cover in pixels", - "type": "number", - "default": 60 - }, - "minimumClusterSize": { - "title": "The minimum number of markers in a cluster", - "type": "number" - } - }, - "required": [] - }, - "form": [ - "gridSize", - "minimumClusterSize" - ] - }; - -const markerClusteringSettingsSchemaLeaflet = - { - "schema": { - "title": "Markers Clustering Configuration Leaflet", - "type": "object", - "properties": { - "showCoverageOnHover": { - "title": "Show the bounds of markers when mouse over a cluster", - "type": "boolean", - "default": true - }, - "animate": { - "title": "Show animation on markers when zooming", - "type": "boolean", - "default": true - }, - "maxClusterRadius": { - "title": "Maximum radius that a cluster will cover in pixels", - "type": "number", - "default": 80 - }, - "chunkedLoading": { - "title": "Use chunks for adding markers so that the page does not freeze", - "type": "boolean", - "default": false - }, - "removeOutsideVisibleBounds": { - "title": "Use lazy load for adding markers", - "type": "boolean", - "default": true - } - }, - "required": [] - }, - "form": [ - "showCoverageOnHover", - "animate", - "maxClusterRadius", - "chunkedLoading", - "removeOutsideVisibleBounds" - ] - }; - -const imageMapSettingsSchema = - { - "schema": { - "title": "Image Map Configuration", - "type": "object", - "properties": { - "mapImageUrl": { - "title": "Image map background", - "type": "string", - "default": "data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==" - }, - "imageEntityAlias": { - "title": "Image URL source entity alias", - "type": "string", - "default": "" - }, - "imageUrlAttribute": { - "title": "Image URL source entity attribute", - "type": "string", - "default": "" - }, - "disableScrollZooming": { - "title": "Disable scroll zooming", - "type": "boolean", - "default": false - }, - "xPosKeyName": { - "title": "X position key name", - "type": "string", - "default": "xPos" - }, - "yPosKeyName": { - "title": "Y position key name", - "type": "string", - "default": "yPos" - }, - "showLabel": { - "title": "Show label", - "type": "boolean", - "default": true - }, - "label": { - "title": "Label (pattern examples: '${entityName}', '${entityName}: (Text ${keyName} units.)' )", - "type": "string", - "default": "${entityName}" - }, - "useLabelFunction": { - "title": "Use label function", - "type": "boolean", - "default": false - }, - "labelFunction": { - "title": "Label function: f(data, dsData, dsIndex)", - "type": "string" - }, - "showTooltip": { - "title": "Show tooltip", - "type": "boolean", - "default": true - }, - "showTooltipAction": { - "title": "Action for displaying the tooltip", - "type": "string", - "default": "click" - }, - "autocloseTooltip": { - "title": "Auto-close tooltips", - "type": "boolean", - "default": true - }, - "tooltipPattern": { - "title": "Tooltip (for ex. 'Text ${keyName} units.' or Link text')", - "type": "string", - "default": "${entityName}

    X Pos: ${xPos:2}
    Y Pos: ${yPos:2}" - }, - "useTooltipFunction": { - "title": "Use tooltip function", - "type": "boolean", - "default": false - }, - "tooltipFunction": { - "title": "Tooltip function: f(data, dsData, dsIndex)", - "type": "string" - }, - "color": { - "title": "Color", - "type": "string" - }, - "posFunction": { - "title": "Position conversion function: f(origXPos, origYPos), should return x,y coordinates as double from 0 to 1 each", - "type": "string", - "default": "return {x: origXPos, y: origYPos};" - }, - "markerOffsetX": { - "title": "Marker X offset relative to position", - "type": "number", - "default": 0.5 - }, - "markerOffsetY": { - "title": "Marker Y offset relative to position", - "type": "number", - "default": 1 - }, - "useColorFunction": { - "title": "Use color function", - "type": "boolean", - "default": false - }, - "colorFunction": { - "title": "Color function: f(data, dsData, dsIndex)", - "type": "string" - }, - "markerImage": { - "title": "Custom marker image", - "type": "string" - }, - "markerImageSize": { - "title": "Custom marker image size (px)", - "type": "number", - "default": 34 - }, - "useMarkerImageFunction": { - "title": "Use marker image function", - "type": "boolean", - "default": false - }, - "markerImageFunction": { - "title": "Marker image function: f(data, images, dsData, dsIndex)", - "type": "string" - }, - "markerImages": { - "title": "Marker images", - "type": "array", - "items": { - "title": "Marker image", - "type": "string" - } - } - }, - "required": [] - }, - "form": [ - { - "key": "mapImageUrl", - "type": "image" - }, - "imageEntityAlias", - "imageUrlAttribute", - "disableScrollZooming", - "xPosKeyName", - "yPosKeyName", - "showLabel", - "label", - "useLabelFunction", - { - "key": "labelFunction", - "type": "javascript" - }, - "showTooltip", - { - "key": "showTooltipAction", - "type": "rc-select", - "multiple": false, - "items": [ - { - "value": "click", - "label": "Show tooltip on click (Default)" - }, - { - "value": "hover", - "label": "Show tooltip on hover" - } - ] - }, - "autocloseTooltip", - { - "key": "tooltipPattern", - "type": "textarea" - }, - "useTooltipFunction", - { - "key": "tooltipFunction", - "type": "javascript" - }, - { - "key": "color", - "type": "color" - }, - { - "key": "posFunction", - "type": "javascript" - }, - "markerOffsetX", - "markerOffsetY", - "useColorFunction", - { - "key": "colorFunction", - "type": "javascript" - }, - { - "key": "markerImage", - "type": "image" - }, - "markerImageSize", - "useMarkerImageFunction", - { - "key": "markerImageFunction", - "type": "javascript" - }, - { - "key": "markerImages", - "items": [ - { - "key": "markerImages[]", - "type": "image" - } - ] - } - ] - }; - -/*@ngInject*/ -function addEntityPanelController(mdPanelRef, entities) { - var vm = this; - vm.entities = entities; - vm.selectEntity = selectEntity; - - function selectEntity(entity) { - mdPanelRef.close().then(() => { - this.onClose(entity); - }); - } -} diff --git a/ui/src/app/widget/lib/multiple-input-widget.js b/ui/src/app/widget/lib/multiple-input-widget.js deleted file mode 100644 index 9d93b8f993..0000000000 --- a/ui/src/app/widget/lib/multiple-input-widget.js +++ /dev/null @@ -1,344 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './multiple-input-widget.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import multipleInputWidgetTemplate from './multiple-input-widget.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -export default angular.module('thingsboard.widgets.multipleInputWidget', []) - .directive('tbMultipleInputWidget', MultipleInputWidget) - .name; - -/*@ngInject*/ -function MultipleInputWidget() { - return { - restrict: "E", - scope: true, - bindToController: { - formId: '=', - ctx: '=' - }, - controller: MultipleInputWidgetController, - controllerAs: 'vm', - templateUrl: multipleInputWidgetTemplate - }; -} - -/*@ngInject*/ -function MultipleInputWidgetController($q, $scope, $translate, attributeService, toast, types, utils) { - var vm = this; - - vm.entityDetected = false; - vm.isAllParametersValid = true; - - vm.sources = []; - vm.datasources = null; - - vm.discardAll = discardAll; - vm.inputChanged = inputChanged; - vm.save = save; - vm.getGroupTitle = getGroupTitle; - - $scope.$watch('vm.ctx', function () { - if (vm.ctx && vm.ctx.defaultSubscription) { - vm.settings = vm.ctx.settings; - vm.widgetConfig = vm.ctx.widgetConfig; - vm.subscription = vm.ctx.defaultSubscription; - vm.datasources = vm.subscription.datasources; - initializeConfig(); - updateDatasources(); - } - }); - - $scope.$on('multiple-input-data-updated', function (event, formId) { - if (vm.formId == formId) { - updateWidgetData(vm.subscription.data); - $scope.$digest(); - } - }); - - $scope.$on('multiple-input-resize', function (event, formId) { - if (vm.formId == formId) { - updateWidgetDisplaying(); - } - }); - - function discardAll() { - for (var i = 0; i < vm.sources.length; i++) { - for (var j = 0; j < vm.sources[i].keys.length; j++) { - vm.sources[i].keys[j].data.currentValue = vm.sources[i].keys[j].data.originalValue; - } - } - $scope.multipleInputForm.$setPristine(); - } - - function inputChanged(source, key) { - if (!vm.settings.showActionButtons) { - if (!key.settings.required || (key.settings.required && key.data && angular.isDefined(key.data.currentValue))) { - var dataToSave = { - datasource: source.datasource, - keys: [key] - }; - vm.save(dataToSave); - } - } - } - - function save(dataToSave) { - var tasks = []; - var config = { - ignoreLoading: !vm.settings.showActionButtons - }; - var data; - if (dataToSave) { - data = [dataToSave]; - } else { - data = vm.sources; - } - for (let i = 0; i < data.length; i++) { - var serverAttributes = [], sharedAttributes = [], telemetry = []; - for (let j = 0; j < data[i].keys.length; j++) { - var key = data[i].keys[j]; - if ((key.data.currentValue !== key.data.originalValue) || vm.settings.updateAllValues) { - var attribute = { - key: key.name - }; - if (key.data.currentValue) { - switch (key.settings.dataKeyValueType) { - case 'dateTime': - case 'date': - attribute.value = key.data.currentValue.getTime(); - break; - case 'time': - attribute.value = key.data.currentValue.getTime() - moment().startOf('day').valueOf();//eslint-disable-line - break; - default: - attribute.value = key.data.currentValue; - } - } else { - if (key.data.currentValue === '') { - attribute.value = null; - } else { - attribute.value = key.data.currentValue; - } - } - - switch (key.settings.dataKeyType) { - case 'shared': - sharedAttributes.push(attribute); - break; - case 'timeseries': - telemetry.push(attribute); - break; - default: - serverAttributes.push(attribute); - } - } - } - if (serverAttributes.length) { - tasks.push(attributeService.saveEntityAttributes( - data[i].datasource.entityType, - data[i].datasource.entityId, - types.attributesScope.server.value, - serverAttributes, - config)); - } - if (sharedAttributes.length) { - tasks.push(attributeService.saveEntityAttributes( - data[i].datasource.entityType, - data[i].datasource.entityId, - types.attributesScope.shared.value, - sharedAttributes, - config)); - } - if (telemetry.length) { - tasks.push(attributeService.saveEntityTimeseries( - data[i].datasource.entityType, - data[i].datasource.entityId, - types.latestTelemetry.value, - telemetry, - config)); - } - } - - if (tasks.length) { - $q.all(tasks).then( - function success() { - $scope.multipleInputForm.$setPristine(); - if (vm.settings.showResultMessage) { - toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(vm.ctx.$container), 'bottom left'); - } - }, - function fail() { - if (vm.settings.showResultMessage) { - toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(vm.ctx.$container), 'bottom left'); - } - } - ); - } else { - $scope.multipleInputForm.$setPristine(); - } - } - - function initializeConfig() { - - if (vm.settings.widgetTitle && vm.settings.widgetTitle.length) { - vm.widgetTitle = utils.customTranslation(vm.settings.widgetTitle, vm.settings.widgetTitle); - } else { - vm.widgetTitle = vm.ctx.widgetConfig.title; - } - - vm.ctx.widgetTitle = vm.widgetTitle; - - vm.settings.groupTitle = vm.settings.groupTitle || "${entityName}"; - - //For backward compatibility - if (angular.isUndefined(vm.settings.showActionButtons)) { - vm.settings.showActionButtons = true; - } - if (angular.isUndefined(vm.settings.fieldsAlignment)) { - vm.settings.fieldsAlignment = 'row'; - } - if (angular.isUndefined(vm.settings.fieldsInRow)) { - vm.settings.fieldsInRow = 2; - } - //For backward compatibility - - vm.isVerticalAlignment = !(vm.settings.fieldsAlignment === 'row'); - - if (!vm.isVerticalAlignment && vm.settings.fieldsInRow) { - vm.inputWidthSettings = 100 / vm.settings.fieldsInRow + '%'; - } - updateWidgetDisplaying(); - } - - function updateDatasources() { - if (vm.datasources && vm.datasources.length) { - vm.entityDetected = true; - for (var i = 0; i < vm.datasources.length; i++) { - var datasource = vm.datasources[i]; - var source = { - datasource: datasource, - keys: [] - }; - if (datasource.type === types.datasourceType.entity) { - for (var j = 0; j < datasource.dataKeys.length; j++) { - if ((datasource.entityType !== types.entityType.device) && (datasource.dataKeys[j].settings.dataKeyType == 'shared')) { - vm.isAllParametersValid = false; - } - source.keys.push(datasource.dataKeys[j]); - if (source.keys[j].units) { - source.keys[j].label += ' (' + source.keys[j].units + ')'; - } - source.keys[j].data = {}; - - //For backward compatibility - if (angular.isUndefined(source.keys[j].settings.dataKeyType)) { - if (vm.settings.attributesShared) { - source.keys[j].settings.dataKeyType = 'shared'; - } else { - source.keys[j].settings.dataKeyType = 'server'; - } - } - - if (angular.isUndefined(source.keys[j].settings.dataKeyValueType)) { - if (source.keys[j].settings.inputTypeNumber) { - source.keys[j].settings.dataKeyValueType = 'double'; - } else { - source.keys[j].settings.dataKeyValueType = 'string'; - } - } - - if (angular.isUndefined(source.keys[j].settings.isEditable)) { - if (source.keys[j].settings.readOnly) { - source.keys[j].settings.isEditable = 'readonly'; - } else { - source.keys[j].settings.isEditable = 'editable'; - } - } - //For backward compatibility - - } - } else { - vm.entityDetected = false; - } - vm.sources.push(source); - } - } - } - - function updateWidgetData(data) { - var dataIndex = 0; - for (var i = 0; i < vm.sources.length; i++) { - var source = vm.sources[i]; - for (var j = 0; j < source.keys.length; j++) { - var keyData = data[dataIndex].data; - var key = source.keys[j]; - if (keyData && keyData.length) { - var value; - switch (key.settings.dataKeyValueType) { - case 'dateTime': - case 'date': - value = moment(keyData[0][1]).toDate(); // eslint-disable-line - break; - case 'time': - value = moment().startOf('day').add(keyData[0][1], 'ms').toDate(); // eslint-disable-line - break; - case 'booleanCheckbox': - case 'booleanSwitch': - value = (keyData[0][1] === 'true'); - break; - default: - value = keyData[0][1]; - } - - key.data = { - currentValue: value, - originalValue: value - }; - } - - if (key.settings.isEditable === 'editable' && key.settings.disabledOnDataKey) { - var conditions = data.filter((item) => { - return item.dataKey.name === key.settings.disabledOnDataKey; - }); - if (conditions && conditions.length) { - if (conditions[0].data.length) { - if (conditions[0].data[0][1] === 'false') { - key.settings.disabledOnCondition = true; - } else { - key.settings.disabledOnCondition = !conditions[0].data[0][1]; - } - } - } - } - dataIndex++; - } - } - } - - function updateWidgetDisplaying() { - vm.changeAlignment = (vm.ctx.$container[0].offsetWidth < 620); - vm.smallWidthContainer = (vm.ctx.$container[0].offsetWidth < 420); - } - - function getGroupTitle(datasource) { - return utils.createLabelFromDatasource(datasource, vm.settings.groupTitle); - } -} diff --git a/ui/src/app/widget/lib/multiple-input-widget.scss b/ui/src/app/widget/lib/multiple-input-widget.scss deleted file mode 100644 index e16922b103..0000000000 --- a/ui/src/app/widget/lib/multiple-input-widget.scss +++ /dev/null @@ -1,115 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-multiple-input { - height: 100%; - overflow-x: hidden; - overflow-y: auto; - - .fields-group { - padding: 0 8px; - margin: 10px 0; - border: 1px groove rgba(0, 0, 0, .25); - - legend { - color: rgba(0, 0, 0, .7); - } - } - - .input-field { - padding-right: 10px; - - md-input-container, - md-checkbox, - md-switch { - margin-bottom: 5px; - } - } - - md-checkbox { - margin-top: 22px; - } - - md-switch { - margin-top: 20px; - white-space: normal; - } - - .md-icon-button md-icon { - width: 20px; - min-width: 20px; - height: 20px; - min-height: 20px; - font-size: 20px; - } - - .date-time-input { - &__label { - margin-left: 36px; - font-size: 12px; - color: rgba(0, 0, 0, .54); - } - - mdp-date-picker, - mdp-time-picker { - width: 100%; - - .md-button.md-icon-button { - margin: 5px 0 0; - } - - md-input-container { - width: 100%; - margin: 2px 0; - - label { - display: none; - } - } - } - } - - .vertical-alignment { - flex-direction: column; - - md-checkbox, - md-switch { - margin-top: 18px; - } - - md-switch { - display: flex; - justify-content: space-between; - } - - .date-time-input { - &__label { - margin-top: 10px; - } - } - } - - .vertically-aligned { - flex-direction: column; - } -} - -md-toast { - min-width: 0; - - .md-toast-content { - font-size: 14px !important; - } -} diff --git a/ui/src/app/widget/lib/multiple-input-widget.tpl.html b/ui/src/app/widget/lib/multiple-input-widget.tpl.html deleted file mode 100644 index a1c903108b..0000000000 --- a/ui/src/app/widget/lib/multiple-input-widget.tpl.html +++ /dev/null @@ -1,162 +0,0 @@ - -
    -
    -
    - {{ vm.getGroupTitle(source.datasource) }} - -
    -
    -
    - - - - {{key.settings.icon}} - - -
    -
    {{ key.settings.requiredErrorMessage }}
    -
    -
    -
    -
    - - - - {{key.settings.icon}} - - -
    -
    {{ key.settings.requiredErrorMessage }}
    -
    -
    -
    -
    - - - - {{key.settings.icon}} - - -
    -
    {{ key.settings.requiredErrorMessage }}
    -
    value.invalid-integer-value
    -
    -
    -
    -
    - - {{key.label}} - -
    -
    - - {{key.label}} - -
    -
    - -
    - -
    -
    {{ key.settings.requiredErrorMessage }}
    -
    -
    - -
    -
    {{ key.settings.requiredErrorMessage }}
    -
    -
    -
    -
    -
    -
    -
    -
    - - {{ 'action.undo' | translate }} - - - {{ 'action.save' | translate }} - -
    -
    -
    -
    - {{ 'widgets.input-widgets.no-entity-selected' | translate }} -
    -
    - {{ 'widgets.input-widgets.not-allowed-entity' | translate }} -
    -
    -
    diff --git a/ui/src/app/widget/lib/openstreet-map.js b/ui/src/app/widget/lib/openstreet-map.js deleted file mode 100644 index ad14071df3..0000000000 --- a/ui/src/app/widget/lib/openstreet-map.js +++ /dev/null @@ -1,339 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import 'leaflet/dist/leaflet.css'; -import 'leaflet.markercluster/dist/MarkerCluster.css' -import 'leaflet.markercluster/dist/MarkerCluster.Default.css' -import * as L from 'leaflet'; -import 'leaflet-providers'; -import 'leaflet.markercluster/dist/leaflet.markercluster' - -export default class TbOpenStreetMap { - - constructor($containerElement, utils, initCallback, defaultZoomLevel, dontFitMapBounds, disableScrollZooming, minZoomLevel, mapProvider, credentials, defaultCenterPosition, markerClusteringSetting) { - - this.utils = utils; - this.defaultZoomLevel = defaultZoomLevel; - this.dontFitMapBounds = dontFitMapBounds; - this.minZoomLevel = minZoomLevel; - this.tooltips = []; - this.isMarketCluster = markerClusteringSetting && markerClusteringSetting.isMarketCluster; - - if (!mapProvider) { - mapProvider = { - name: "OpenStreetMap.Mapnik" - }; - } - - if (mapProvider.name.startsWith("HERE.")) { - credentials.app_id = credentials.app_id || "AhM6TzD9ThyK78CT3ptx"; - credentials.app_code = credentials.app_code || "p6NPiITB3Vv0GMUFnkLOOg"; - } - - defaultCenterPosition = defaultCenterPosition || [0,0]; - this.map = L.map($containerElement[0]).setView(defaultCenterPosition, this.defaultZoomLevel || 8); - - if (disableScrollZooming) { - this.map.scrollWheelZoom.disable(); - } - - var tileLayer = mapProvider.isCustom ? L.tileLayer(mapProvider.name) : L.tileLayer.provider(mapProvider.name, credentials); - tileLayer.addTo(this.map); - - if (this.isMarketCluster) { - this.markersCluster = L.markerClusterGroup(markerClusteringSetting); - this.map.addLayer(this.markersCluster); - } - - if (initCallback) { - setTimeout(initCallback, 0); //eslint-disable-line - } - - } - - inited() { - return angular.isDefined(this.map); - } - - getContainer() { - return this.isMarketCluster ? this.markersCluster : this.map; - } - - updateMarkerLabel(marker, settings) { - marker.unbindTooltip(); - marker.bindTooltip('
    ' + settings.labelText + '
    ', - {className: 'tb-marker-label', permanent: true, direction: 'top', offset: marker.tooltipOffset}); - } - - updateMarkerColor(marker, color) { - this.createDefaultMarkerIcon(marker, color, (iconInfo) => { - marker.setIcon(iconInfo.icon); - }); - } - - updateMarkerIcon(marker, settings) { - this.createMarkerIcon(marker, settings, (iconInfo) => { - marker.setIcon(iconInfo.icon); - if (settings.showLabel) { - marker.unbindTooltip(); - marker.tooltipOffset = [0, -iconInfo.size[1] + 10]; - marker.bindTooltip('
    ' + settings.labelText + '
    ', - {className: 'tb-marker-label', permanent: true, direction: 'top', offset: marker.tooltipOffset}); - } - }); - } - - createMarkerIcon(marker, settings, onMarkerIconReady) { - var currentImage = settings.currentImage; - var opMap = this; - if (currentImage && currentImage.url) { - this.utils.loadImageAspect(currentImage.url).then( - (aspect) => { - if (aspect) { - var width; - var height; - if (aspect > 1) { - width = currentImage.size; - height = currentImage.size / aspect; - } else { - width = currentImage.size * aspect; - height = currentImage.size; - } - var icon = L.icon({ - iconUrl: currentImage.url, - iconSize: [width, height], - iconAnchor: [width / 2, height], - popupAnchor: [0, -height] - }); - var iconInfo = { - size: [width, height], - icon: icon - }; - onMarkerIconReady(iconInfo); - } else { - opMap.createDefaultMarkerIcon(marker, settings.color, onMarkerIconReady); - } - } - ); - } else { - this.createDefaultMarkerIcon(marker, settings.color, onMarkerIconReady); - } - } - - createDefaultMarkerIcon(marker, color, onMarkerIconReady) { - var pinColor = color.substr(1); - var icon = L.icon({ - iconUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|' + pinColor, - iconSize: [21, 34], - iconAnchor: [10, 34], - popupAnchor: [0, -34], - shadowUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_shadow', - shadowSize: [40, 37], - shadowAnchor: [12, 35] - }); - var iconInfo = { - size: [21, 34], - icon: icon - }; - onMarkerIconReady(iconInfo); - } - - createMarker(location, dsIndex, settings, onClickListener, markerArgs, onDragendListener) { - var marker = L.marker(location, { - draggable: settings.drraggable - }); - var opMap = this; - this.createMarkerIcon(marker, settings, (iconInfo) => { - marker.setIcon(iconInfo.icon); - if (settings.showLabel) { - marker.tooltipOffset = [0, -iconInfo.size[1] + 10]; - marker.bindTooltip('
    ' + settings.labelText + '
    ', - {className: 'tb-marker-label', permanent: true, direction: 'top', offset: marker.tooltipOffset}); - } - marker.addTo(opMap.getContainer()); - }); - - if (settings.displayTooltip) { - this.createTooltip(marker, dsIndex, settings, markerArgs); - } - - if (onClickListener) { - marker.on('click', onClickListener); - } - - if (onDragendListener) { - marker.on('dragend', onDragendListener); - } - - return marker; - } - - removeMarker(marker) { - this.getContainer().removeLayer(marker); - } - - createTooltip(marker, dsIndex, settings, markerArgs) { - var popup = L.popup(); - popup.setContent(''); - marker.bindPopup(popup, {autoClose: settings.autocloseTooltip, closeOnClick: false}); - if (settings.displayTooltipAction == 'hover') { - marker.off('click'); - marker.on('mouseover', function () { - this.openPopup(); - }); - marker.on('mouseout', function () { - this.closePopup(); - }); - } - this.tooltips.push({ - markerArgs: markerArgs, - popup: popup, - locationSettings: settings, - dsIndex: dsIndex - }); - } - - updatePolylineColor(polyline, settings, color) { - var style = { - color: color, - opacity: settings.strokeOpacity, - weight: settings.strokeWeight - }; - polyline.setStyle(style); - } - - createPolyline(locations, settings) { - var polyline = L.polyline(locations, - { - color: settings.color, - opacity: settings.strokeOpacity, - weight: settings.strokeWeight - } - ).addTo(this.map); - return polyline; - } - - removePolyline(polyline) { - this.map.removeLayer(polyline); - } - - createPolygon(latLangs, settings, location, onClickListener, markerArgs) { - let polygon = L.polygon(latLangs, { - fill: true, - fillColor: settings.polygonColor, - color: settings.polygonStrokeColor, - weight: settings.polygonStrokeWeight, - fillOpacity: settings.polygonOpacity, - opacity: settings.polygonStrokeOpacity - }).addTo(this.map); - - if (settings.displayTooltip) { - this.createTooltip(polygon, location.dsIndex, settings, markerArgs); - } - if (onClickListener) { - polygon.on('click', onClickListener); - } - return polygon; - } - - removePolygon(polygon) { - this.map.removeLayer(polygon); - } - - updatePolygonColor(polygon, settings, color) { - let style = { - fill: true, - fillColor: color, - color: color, - weight: settings.polygonStrokeWeight, - fillOpacity: settings.polygonOpacity, - opacity: settings.polygonStrokeOpacity - }; - polygon.setStyle(style); - } - - getPolygonLatLngs(polygon) { - return polygon.getLatLngs(); - } - - setPolygonLatLngs(polygon, latLngs) { - polygon.setLatLngs(latLngs); - polygon.redraw(); - } - - fitBounds(bounds, useDefaultZoom) { - if (bounds.isValid()) { - if ((this.dontFitMapBounds || useDefaultZoom) && this.defaultZoomLevel) { - this.map.setZoom(this.defaultZoomLevel, {animate: false}); - this.map.panTo(bounds.getCenter(), {animate: false}); - } else { - var tbMap = this; - this.map.once('zoomend', function () { - if (!tbMap.defaultZoomLevel && tbMap.map.getZoom() > tbMap.minZoomLevel) { - tbMap.map.setZoom(tbMap.minZoomLevel, {animate: false}); - } - }); - this.map.fitBounds(bounds, {padding: [50, 50], animate: false}); - } - } - } - - createLatLng(lat, lng) { - return L.latLng(lat, lng); - } - - extendBoundsWithMarker(bounds, marker) { - bounds.extend(marker.getLatLng()); - } - - getMarkerPosition(marker) { - return marker.getLatLng(); - } - - setMarkerPosition(marker, latLng) { - marker.setLatLng(latLng); - } - - getPolylineLatLngs(polyline) { - return polyline.getLatLngs(); - } - - setPolylineLatLngs(polyline, latLngs) { - polyline.setLatLngs(latLngs); - } - - createBounds() { - return L.latLngBounds(); - } - - extendBounds(bounds, polyline) { - if (polyline && polyline.getLatLngs() && polyline.getBounds()) { - bounds.extend(polyline.getBounds()); - } - } - - invalidateSize() { - this.map.invalidateSize(true); - } - - getTooltips() { - return this.tooltips; - } - - getCenter() { - return this.map.getCenter(); - } - -} diff --git a/ui/src/app/widget/lib/rpc/index.js b/ui/src/app/widget/lib/rpc/index.js deleted file mode 100644 index 788e014f50..0000000000 --- a/ui/src/app/widget/lib/rpc/index.js +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import tbKnob from './knob.directive'; -import tbSwitch from './switch.directive'; -import tbRoundSwitch from './round-switch.directive'; -import tbLedIndicator from './led-indicator.directive'; - -export default angular.module('thingsboard.widgets.rpc', [ - tbKnob, - tbSwitch, - tbRoundSwitch, - tbLedIndicator -]).name; diff --git a/ui/src/app/widget/lib/rpc/knob.directive.js b/ui/src/app/widget/lib/rpc/knob.directive.js deleted file mode 100644 index e56eb89438..0000000000 --- a/ui/src/app/widget/lib/rpc/knob.directive.js +++ /dev/null @@ -1,393 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './knob.scss'; - -import CanvasDigitalGauge from './../CanvasDigitalGauge'; -import tinycolor from 'tinycolor2'; - -import { isNumber } from '../widget-utils'; - -/* eslint-disable import/no-unresolved, import/default */ - -import knobTemplate from './knob.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -export default angular.module('thingsboard.widgets.rpc.knob', []) - .directive('tbKnob', Knob) - .name; - -/*@ngInject*/ -function Knob() { - return { - restrict: "E", - scope: true, - bindToController: { - ctx: '=' - }, - controller: KnobController, - controllerAs: 'vm', - templateUrl: knobTemplate - }; -} - -/*@ngInject*/ -function KnobController($element, $scope, $document) { - let vm = this; - - vm.value = 0; - vm.error = ''; - vm.newValue = 0; - - var knob = angular.element('.knob', $element), - knobContainer = angular.element('#knob-container', $element), - knobTopPointerContainer = knob.find('.top-pointer-container'), - knobTopPointer = knob.find('.top-pointer'), - knobValueContainer = knob.find('.value-container'), - knobValue = knob.find('.knob-value'), - knobErrorContainer = knob.find('.error-container'), - knobError = knob.find('.knob-error'), - knobTitleContainer = knob.find('.title-container'), - knobTitle = knob.find('.knob-title'), - knobMinmaxContainer = knob.find('.minmax-container'), - minmaxLabel = knob.find('.minmax-label'), - textMeasure = knob.find('#text-measure'), - startDeg = -1, - currentDeg = 0, - rotation = 0, - lastDeg = 0, - moving = false; - - var minDeg = -45; - var maxDeg = 225; - - var canvasBarElement = angular.element('#canvasBar', $element); - - var levelColors = ['#19ff4b', '#ffff19', '#ff3232']; - - var canvasBar; - - $scope.$watch('vm.ctx', () => { - if (vm.ctx) { - init(); - } - }); - - function init() { - - vm.minValue = angular.isDefined(vm.ctx.settings.minValue) ? vm.ctx.settings.minValue : 0; - vm.maxValue = angular.isDefined(vm.ctx.settings.maxValue) ? vm.ctx.settings.maxValue : 100; - vm.title = angular.isDefined(vm.ctx.settings.title) ? vm.ctx.settings.title : ''; - - var canvasBarData = { - renderTo: canvasBarElement[0], - hideValue: true, - neonGlowBrightness: 0, - gaugeWidthScale: 0.4, - gaugeColor: 'rgba(0, 0, 0, 0)', - levelColors: levelColors, - minValue: vm.minValue, - maxValue: vm.maxValue, - gaugeType: 'donut', - dashThickness: 2, - donutStartAngle: 3/4*Math.PI, - donutEndAngle: 9/4*Math.PI, - animation: false - }; - - canvasBar = new CanvasDigitalGauge(canvasBarData).draw(); - - knob.on('click', (e) => { - if (moving) { - moving = false; - return false; - } - e.preventDefault(); - - var offset = knob.offset(); - var center = { - y : offset.top + knob.height()/2, - x: offset.left + knob.width()/2 - }; - var a, b, deg, - rad2deg = 180/Math.PI; - - e = (e.originalEvent.touches) ? e.originalEvent.touches[0] : e; - - a = center.y - e.pageY; - b = center.x - e.pageX; - deg = Math.atan2(a,b)*rad2deg; - if(deg < 0){ - deg = 360 + deg; - } - if (deg > maxDeg) { - if (deg - 360 > minDeg) { - deg = deg - 360; - } else { - return false; - } - } - currentDeg = deg; - lastDeg = deg; - knobTopPointerContainer.css('transform','rotate('+(currentDeg)+'deg)'); - turn(degreeToRatio(currentDeg)); - rotation = currentDeg; - startDeg = -1; - rpcUpdateValue(vm.newValue); - }); - - knob.on('mousedown touchstart', (e) => { - moving = false; - e.preventDefault(); - var offset = knob.offset(); - var center = { - y : offset.top + knob.height()/2, - x: offset.left + knob.width()/2 - }; - - var a, b, deg, tmp, - rad2deg = 180/Math.PI; - - $document.on('mousemove.rem touchmove.rem', (e) => { - moving = true; - e = (e.originalEvent.touches) ? e.originalEvent.touches[0] : e; - - a = center.y - e.pageY; - b = center.x - e.pageX; - deg = Math.atan2(a,b)*rad2deg; - if(deg < 0){ - deg = 360 + deg; - } - - if(startDeg == -1){ - startDeg = deg; - } - - tmp = Math.floor((deg-startDeg) + rotation); - - if(tmp < 0){ - tmp = 360 + tmp; - } - else if(tmp > 359){ - tmp = tmp % 360; - } - - if (tmp > maxDeg) { - if (tmp - 360 > minDeg) { - tmp = tmp - 360; - } else { - var deltaMax = Math.abs(maxDeg - lastDeg); - var deltaMin = Math.abs(minDeg - lastDeg); - if (deltaMax < deltaMin) { - tmp = maxDeg; - } else { - tmp = minDeg; - } - } - } - if(Math.abs(tmp - lastDeg) > 180){ - startDeg = deg; - rotation = currentDeg; - return false; - } - - currentDeg = tmp; - lastDeg = tmp; - - knobTopPointerContainer.css('transform','rotate('+(currentDeg)+'deg)'); - turn(degreeToRatio(currentDeg)); - }); - - $document.on('mouseup.rem touchend.rem',() => { - if(moving) { - rpcUpdateValue(vm.newValue); - } - knob.off('.rem'); - $document.off('.rem'); - rotation = currentDeg; - startDeg = -1; - }); - - }); - vm.ctx.resize = resize; - resize(); - var initialValue = angular.isDefined(vm.ctx.settings.initialValue) ? vm.ctx.settings.initialValue : vm.minValue; - setValue(initialValue); - - var subscription = vm.ctx.defaultSubscription; - var rpcEnabled = subscription.rpcEnabled; - - vm.isSimulated = $scope.widgetEditMode; - - vm.requestTimeout = 500; - if (vm.ctx.settings.requestTimeout) { - vm.requestTimeout = vm.ctx.settings.requestTimeout; - } - vm.getValueMethod = 'getValue'; - if (vm.ctx.settings.getValueMethod && vm.ctx.settings.getValueMethod.length) { - vm.getValueMethod = vm.ctx.settings.getValueMethod; - } - vm.setValueMethod = 'setValue'; - if (vm.ctx.settings.setValueMethod && vm.ctx.settings.setValueMethod.length) { - vm.setValueMethod = vm.ctx.settings.setValueMethod; - } - if (!rpcEnabled) { - onError('Target device is not set!'); - } else { - if (!vm.isSimulated) { - rpcRequestValue(); - } - } - } - - function ratioToDegree(ratio) { - return minDeg + ratio*(maxDeg-minDeg); - } - - function degreeToRatio(degree) { - return (degree-minDeg)/(maxDeg-minDeg); - } - - function resize() { - var width = knobContainer.width(); - var height = knobContainer.height(); - var size = Math.min(width, height); - knob.css({width: size, height: size}); - canvasBar.update({width: size, height: size}); - setFontSize(knobTitle, vm.title, knobTitleContainer.height(), knobTitleContainer.width()); - setFontSize(knobError, vm.error, knobErrorContainer.height(), knobErrorContainer.width()); - var minmaxHeight = knobMinmaxContainer.height(); - minmaxLabel.css({'fontSize': minmaxHeight+'px', 'lineHeight': minmaxHeight+'px'}); - checkValueSize(); - } - - function turn(ratio) { - vm.newValue = (vm.minValue + (vm.maxValue - vm.minValue)*ratio).toFixed(vm.ctx.decimals); - if (canvasBar.value != vm.newValue) { - canvasBar.value = vm.newValue; - } - updateColor(canvasBar.getValueColor()); - onValue(vm.newValue); - } - - function setValue(value) { - var ratio = (value-vm.minValue) / (vm.maxValue - vm.minValue); - rotation = lastDeg = currentDeg = ratioToDegree(ratio); - knobTopPointerContainer.css('transform','rotate('+(currentDeg)+'deg)'); - if (canvasBar.value != value) { - canvasBar.value = value; - } - updateColor(canvasBar.getValueColor()); - vm.value = formatValue(value); - checkValueSize(); - } - - function updateColor(color) { - var glowColor = tinycolor(color).brighten(30).toHexString(); - knobValue.css({'color': glowColor}); - var textShadow = `${color} 1px 1px 10px, ${glowColor} 1px 1px 10px`; - knobValue.css({'textShadow': textShadow}); - knobTopPointer.css({'backgroundColor': glowColor}); - var boxShadow = `inset 1px 0 2px #040404, 1px 1px 8px 2px ${glowColor}`; - knobTopPointer.css({'boxShadow': boxShadow}); - } - - function onValue(value) { - $scope.$applyAsync(() => { - vm.value = formatValue(value); - checkValueSize(); - }); - } - - function onError(error) { - $scope.$applyAsync(() => { - vm.error = error; - setFontSize(knobError, vm.error, knobErrorContainer.height(), knobErrorContainer.width()); - }); - } - - function formatValue(value) { - return vm.ctx.utils.formatValue(value, vm.ctx.decimals, vm.ctx.units, true); - } - - function checkValueSize() { - var fontSize = knobValueContainer.height()/3.3; - var containerWidth = knobValueContainer.width(); - setFontSize(knobValue, vm.value, fontSize, containerWidth); - } - - function setFontSize(element, text, fontSize, maxWidth) { - var textWidth = measureTextWidth(text, fontSize); - while (textWidth > maxWidth) { - fontSize--; - if (fontSize < 0) { - break; - } - textWidth = measureTextWidth(text, fontSize); - } - element.css({'fontSize': fontSize+'px', 'lineHeight': fontSize+'px'}); - } - - function measureTextWidth(text, fontSize) { - textMeasure.css({'fontSize': fontSize+'px', 'lineHeight': fontSize+'px'}); - textMeasure.html(text); - return textMeasure.width(); - } - - function rpcRequestValue() { - vm.error = ''; - vm.ctx.controlApi.sendTwoWayCommand(vm.getValueMethod, null, vm.requestTimeout).then( - (responseBody) => { - if (isNumber(responseBody)) { - var numValue = Number(responseBody).toFixed(vm.ctx.decimals); - setValue(numValue); - } else { - var errorText = `Unable to parse response: ${responseBody}`; - onError(errorText); - } - }, - () => { - var errorText = vm.ctx.defaultSubscription.rpcErrorText; - onError(errorText); - } - ); - } - - function rpcUpdateValue(value) { - if (vm.executingUpdateValue) { - vm.scheduledValue = value; - return; - } else { - vm.scheduledValue = null; - vm.rpcValue = value; - vm.executingUpdateValue = true; - } - vm.error = ''; - vm.ctx.controlApi.sendOneWayCommand(vm.setValueMethod, value, vm.requestTimeout).then( - () => { - vm.executingUpdateValue = false; - if (vm.scheduledValue != null && vm.scheduledValue != vm.rpcValue) { - rpcUpdateValue(vm.scheduledValue); - } - }, - () => { - vm.executingUpdateValue = false; - var errorText = vm.ctx.defaultSubscription.rpcErrorText; - onError(errorText); - } - ); - } -} diff --git a/ui/src/app/widget/lib/rpc/knob.scss b/ui/src/app/widget/lib/rpc/knob.scss deleted file mode 100644 index 2188511a95..0000000000 --- a/ui/src/app/widget/lib/rpc/knob.scss +++ /dev/null @@ -1,163 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -$knob-img: url("./svg/knob.svg") !default; - -$bars-margin-pct: percentage(.033) !default; -$background-margin-pct: percentage(.05) !default; -$value-container-margin-pct: percentage(.35) !default; -$error-height: percentage(.05) !default; -$title-height: percentage(.066) !default; -$title-container-margin-pct: percentage(.2) !default; -$title-container-margin-bottom-pct: percentage(.05) !default; -$minmax-height: percentage(.04) !default; -$minmax-container-margin-pct: percentage(.18) !default; -$minmax-container-margin-bottom-pct: percentage(.12) !default; - -.tb-knob { - width: 100%; - height: 100%; - - .knob { - position: relative; - - &[draggable] { - user-select: none; - } - - #canvasBar { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 2; - } - - .canvas-background { - position: absolute; - top: $background-margin-pct; - right: $background-margin-pct; - bottom: $background-margin-pct; - left: $background-margin-pct; - z-index: 2; - background: #3f4346; - border-radius: 50%; - } - - .value-container { - position: absolute; - top: $value-container-margin-pct; - right: $value-container-margin-pct; - bottom: $value-container-margin-pct; - left: $value-container-margin-pct; - z-index: 4; - - .knob-value { - font-weight: 500; - color: #fff; - white-space: nowrap; - } - } - - .error-container { - position: absolute; - top: 1%; - right: 0; - left: 0; - z-index: 4; - height: $error-height; - - .knob-error { - color: #ff3315; - white-space: nowrap; - } - } - - .title-container { - position: absolute; - right: $title-container-margin-pct; - bottom: $title-container-margin-bottom-pct; - left: $title-container-margin-pct; - z-index: 4; - height: $title-height; - - .knob-title { - font-weight: 500; - color: #757575; - white-space: nowrap; - } - } - - .minmax-container { - position: absolute; - right: $minmax-container-margin-pct; - bottom: $minmax-container-margin-bottom-pct; - left: $minmax-container-margin-pct; - z-index: 4; - height: $minmax-height; - - .minmax-label { - font-weight: 500; - color: #757575; - white-space: nowrap; - } - } - - .top-pointer-container { - position: absolute; - top: $bars-margin-pct; - right: $bars-margin-pct; - bottom: $bars-margin-pct; - left: $bars-margin-pct; - z-index: 3; - cursor: pointer !important; - - .top-pointer { - position: absolute; - top: 50%; - left: 22%; - width: 5%; - height: 5%; - margin-top: -2.5%; - cursor: pointer !important; - content: ""; - background-color: #b5b5b5; - border-radius: 50%; - box-shadow: 1px 0 2px #040404; - } - } - - .top{ - position: absolute; - top: $bars-margin-pct; - right: $bars-margin-pct; - bottom: $bars-margin-pct; - left: $bars-margin-pct; - z-index: 2; - cursor: pointer !important; - background: $knob-img no-repeat; - background-size: contain; - } - - #text-measure { - position: absolute; - width: auto; - height: auto; - white-space: nowrap; - visibility: hidden; - } - } -} diff --git a/ui/src/app/widget/lib/rpc/knob.tpl.html b/ui/src/app/widget/lib/rpc/knob.tpl.html deleted file mode 100644 index 3ff858293e..0000000000 --- a/ui/src/app/widget/lib/rpc/knob.tpl.html +++ /dev/null @@ -1,45 +0,0 @@ - -
    -
    -
    -
    - {{ vm.value }} -
    -
    - -
    -
    -
    -
    -
    - -
    -
    - {{ vm.title }} -
    -
    - min - - max -
    -
    -
    -
    -
    diff --git a/ui/src/app/widget/lib/rpc/led-indicator.directive.js b/ui/src/app/widget/lib/rpc/led-indicator.directive.js deleted file mode 100644 index cb64d5cff9..0000000000 --- a/ui/src/app/widget/lib/rpc/led-indicator.directive.js +++ /dev/null @@ -1,297 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './led-indicator.scss'; - -import tinycolor from 'tinycolor2'; - -/* eslint-disable import/no-unresolved, import/default */ - -import ledIndicatorTemplate from './led-indicator.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -export default angular.module('thingsboard.widgets.rpc.ledIndicator', []) - .directive('tbLedIndicator', LedIndicator) - .name; - -/*@ngInject*/ -function LedIndicator() { - return { - restrict: "E", - scope: true, - bindToController: { - ctx: '=' - }, - controller: LedIndicatorController, - controllerAs: 'vm', - templateUrl: ledIndicatorTemplate - }; -} - -/*@ngInject*/ -function LedIndicatorController($element, $scope, $timeout, utils, types) { - let vm = this; - - vm.showTitle = false; - vm.value = false; - vm.error = ''; - - const checkStatusPollingInterval = 10000; - - vm.subscriptionOptions = { - callbacks: { - onDataUpdated: onDataUpdated, - onDataUpdateError: onDataUpdateError, - dataLoading: () => {} - } - }; - - var led = angular.element('.led', $element), - ledContainer = angular.element('#led-container', $element), - textMeasure = angular.element('#text-measure', $element), - ledTitleContainer = angular.element('.title-container', $element), - ledTitle = angular.element('.led-title', $element), - ledErrorContainer = angular.element('.error-container', $element), - ledError = angular.element('.led-error', $element); - - $scope.$watch('vm.ctx', () => { - if (vm.ctx) { - init(); - } - }); - - $scope.$on('$destroy', () => { - vm.destroyed = true; - if (vm.checkStatusTimeoutHandle) { - $timeout.cancel(vm.checkStatusTimeoutHandle); - } - if (vm.subscription) { - vm.ctx.subscriptionApi.removeSubscription(vm.subscription.id); - } - }); - - resize(); - - function init() { - - vm.title = angular.isDefined(vm.ctx.settings.title) ? vm.ctx.settings.title : ''; - vm.showTitle = vm.title && vm.title.length ? true : false; - - var origColor = angular.isDefined(vm.ctx.settings.ledColor) ? vm.ctx.settings.ledColor : 'green'; - - vm.valueAttribute = angular.isDefined(vm.ctx.settings.valueAttribute) ? vm.ctx.settings.valueAttribute : 'value'; - - vm.ledColor = tinycolor(origColor).brighten(30).toHexString(); - vm.ledMiddleColor = tinycolor(origColor).toHexString(); - vm.disabledColor = tinycolor(origColor).darken(40).toHexString(); - vm.disabledMiddleColor = tinycolor(origColor).darken(60).toHexString(); - - vm.ctx.resize = resize; - $scope.$applyAsync(() => { - resize(); - }); - var initialValue = angular.isDefined(vm.ctx.settings.initialValue) ? vm.ctx.settings.initialValue : false; - setValue(initialValue, true); - - var subscription = vm.ctx.defaultSubscription; - var rpcEnabled = subscription.rpcEnabled; - - vm.isSimulated = $scope.widgetEditMode; - - vm.requestTimeout = 500; - if (vm.ctx.settings.requestTimeout) { - vm.requestTimeout = vm.ctx.settings.requestTimeout; - } - vm.retrieveValueMethod = 'attribute'; - if (vm.ctx.settings.retrieveValueMethod && vm.ctx.settings.retrieveValueMethod.length) { - vm.retrieveValueMethod = vm.ctx.settings.retrieveValueMethod; - } - - vm.parseValueFunction = (data) => data ? true : false; - if (vm.ctx.settings.parseValueFunction && vm.ctx.settings.parseValueFunction.length) { - try { - vm.parseValueFunction = new Function('data', vm.ctx.settings.parseValueFunction); - } catch (e) { - vm.parseValueFunction = (data) => data ? true : false; - } - } - - vm.performCheckStatus = vm.ctx.settings.performCheckStatus != false; - if (vm.performCheckStatus) { - vm.checkStatusMethod = 'checkStatus'; - if (vm.ctx.settings.checkStatusMethod && vm.ctx.settings.checkStatusMethod.length) { - vm.checkStatusMethod = vm.ctx.settings.checkStatusMethod; - } - } - if (!rpcEnabled) { - onError('Target device is not set!'); - } else { - if (!vm.isSimulated) { - if (vm.performCheckStatus) { - rpcCheckStatus(); - } else { - subscribeForValue(); - } - } - } - } - - function resize() { - var width = ledContainer.width(); - var height = ledContainer.height(); - var size = Math.min(width, height); - - led.css({width: size, height: size}); - - if (vm.showTitle) { - setFontSize(ledTitle, vm.title, ledTitleContainer.height() * 2 / 3, ledTitleContainer.width()); - } - setFontSize(ledError, vm.error, ledErrorContainer.height(), ledErrorContainer.width()); - } - - function setValue(value, forceUpdate) { - if (vm.value != value || forceUpdate) { - vm.value = value; - updateColor(); - } - } - - function updateColor() { - var color = vm.value ? vm.ledColor : vm.disabledColor; - var middleColor = vm.value ? vm.ledMiddleColor : vm.disabledMiddleColor; - var boxShadow = `#000 0 -1px 6px 1px, inset ${middleColor} 0 -1px 8px, ${color} 0 3px 11px`; - led.css({'backgroundColor': color}); - led.css({'boxShadow': boxShadow}); - if (vm.value) { - led.removeClass( 'disabled' ); - } else { - led.addClass( 'disabled' ); - } - } - - function onError(error) { - $scope.$applyAsync(() => { - vm.error = error; - setFontSize(ledError, vm.error, ledErrorContainer.height(), ledErrorContainer.width()); - }); - } - - function setFontSize(element, text, fontSize, maxWidth) { - var textWidth = measureTextWidth(text, fontSize); - while (textWidth > maxWidth) { - fontSize--; - textWidth = measureTextWidth(text, fontSize); - } - element.css({'fontSize': fontSize+'px', 'lineHeight': fontSize+'px'}); - } - - function measureTextWidth(text, fontSize) { - textMeasure.css({'fontSize': fontSize+'px', 'lineHeight': fontSize+'px'}); - textMeasure.text(text); - return textMeasure.width(); - } - - function rpcCheckStatus() { - if (vm.destroyed) { - return; - } - vm.error = ''; - vm.ctx.controlApi.sendTwoWayCommand(vm.checkStatusMethod, null, vm.requestTimeout).then( - (responseBody) => { - var status = responseBody ? true : false; - if (status) { - if (vm.checkStatusTimeoutHandle) { - $timeout.cancel(vm.checkStatusTimeoutHandle); - vm.checkStatusTimeoutHandle = null; - } - subscribeForValue(); - } else { - var errorText = 'Unknown device status!'; - onError(errorText); - if (vm.checkStatusTimeoutHandle) { - $timeout.cancel(vm.checkStatusTimeoutHandle); - } - vm.checkStatusTimeoutHandle = $timeout(rpcCheckStatus, checkStatusPollingInterval); - } - }, - () => { - var errorText = vm.ctx.defaultSubscription.rpcErrorText; - onError(errorText); - if (vm.checkStatusTimeoutHandle) { - $timeout.cancel(vm.checkStatusTimeoutHandle); - } - vm.checkStatusTimeoutHandle = $timeout(rpcCheckStatus, checkStatusPollingInterval); - } - ); - } - - function subscribeForValue() { - var subscriptionsInfo = [{ - type: types.datasourceType.entity, - entityType: types.entityType.device, - entityId: vm.ctx.defaultSubscription.targetDeviceId - }]; - - if (vm.retrieveValueMethod == 'attribute') { - subscriptionsInfo[0].attributes = [ - {name: vm.valueAttribute} - ]; - } else { - subscriptionsInfo[0].timeseries = [ - {name: vm.valueAttribute} - ]; - } - - vm.ctx.subscriptionApi.createSubscriptionFromInfo ( - types.widgetType.latest.value, subscriptionsInfo, vm.subscriptionOptions, false, true).then( - function(subscription) { - vm.subscription = subscription; - } - ); - } - - function onDataUpdated(subscription, apply) { - var value = false; - var data = subscription.data; - if (data.length) { - var keyData = data[0]; - if (keyData && keyData.data && keyData.data[0]) { - var attrValue = keyData.data[0][1]; - if (attrValue) { - var parsed = null; - try { - parsed = vm.parseValueFunction(angular.fromJson(attrValue)); - } catch (e){/**/} - value = parsed ? true : false; - } - } - } - setValue(value); - if (apply) { - $scope.$digest(); - } - } - - function onDataUpdateError(subscription, e) { - var exceptionData = utils.parseException(e); - var errorText = exceptionData.name; - if (exceptionData.message) { - errorText += ': ' + exceptionData.message; - } - onError(errorText); - } - -} diff --git a/ui/src/app/widget/lib/rpc/led-indicator.scss b/ui/src/app/widget/lib/rpc/led-indicator.scss deleted file mode 100644 index 454128a28f..0000000000 --- a/ui/src/app/widget/lib/rpc/led-indicator.scss +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "~compass-sass-mixins/lib/compass"; - -$error-height: 14px !default; - -.tb-led-indicator { - width: 100%; - height: 100%; - - .title-container { - .led-title { - font-weight: 500; - color: #757575; - white-space: nowrap; - } - } - - .error-container { - position: absolute; - top: 1%; - right: 0; - left: 0; - z-index: 4; - height: $error-height; - - .led-error { - color: #ff3315; - white-space: nowrap; - } - } - - #text-measure { - position: absolute; - width: auto; - height: auto; - white-space: nowrap; - visibility: hidden; - } - - #led-container { - padding: 10px; - - .led { - position: relative; - cursor: pointer; - background-image: radial-gradient(50% 50%, circle closest-corner, transparent, rgba(0, 0, 0, .25)); - border-radius: 50%; - transition: background-color .5s, box-shadow .5s; - - &.disabled { - background-image: radial-gradient(50% 50%, circle closest-corner, rgba(255, 255, 255, .5), rgba(0, 0, 0, .1)); - } - } - } -} - diff --git a/ui/src/app/widget/lib/rpc/led-indicator.tpl.html b/ui/src/app/widget/lib/rpc/led-indicator.tpl.html deleted file mode 100644 index 1d2c674ab9..0000000000 --- a/ui/src/app/widget/lib/rpc/led-indicator.tpl.html +++ /dev/null @@ -1,32 +0,0 @@ - -
    -
    - {{vm.title}} -
    -
    -
    -
    -
    -
    - {{ vm.error }} -
    -
    -
    diff --git a/ui/src/app/widget/lib/rpc/round-switch.directive.js b/ui/src/app/widget/lib/rpc/round-switch.directive.js deleted file mode 100644 index feb9f96ab7..0000000000 --- a/ui/src/app/widget/lib/rpc/round-switch.directive.js +++ /dev/null @@ -1,298 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './round-switch.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import roundSwitchTemplate from './round-switch.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -export default angular.module('thingsboard.widgets.rpc.roundSwitch', []) - .directive('tbRoundSwitch', RoundSwitch) - .name; - -/*@ngInject*/ -function RoundSwitch() { - return { - restrict: "E", - scope: true, - bindToController: { - ctx: '=' - }, - controller: RoundSwitchController, - controllerAs: 'vm', - templateUrl: roundSwitchTemplate - }; -} - -/*@ngInject*/ -function RoundSwitchController($element, $scope, utils, types) { - let vm = this; - - vm.showTitle = false; - vm.value = false; - vm.error = ''; - - vm.checkboxId = 'onoff-' + utils.guid(); - - var switchElement = angular.element('.switch', $element), - switchContainer = angular.element('#switch-container', $element), - onoff = angular.element('input', $element), - textMeasure = angular.element('#text-measure', $element), - switchTitleContainer = angular.element('.title-container', $element), - switchTitle = angular.element('.switch-title', $element), - switchErrorContainer = angular.element('.error-container', $element), - switchError = angular.element('.switch-error', $element); - - onoff.bind('change', () => { - vm.value = onoff.prop('checked') === false; - onValue(); - }); - - vm.valueSubscription = null; - - $scope.$watch('vm.ctx', () => { - if (vm.ctx) { - init(); - } - }); - - $scope.$on('$destroy', () => { - if (vm.valueSubscription) { - vm.ctx.subscriptionApi.removeSubscription(vm.valueSubscription.id); - } - }); - - resize(); - - function init() { - - vm.title = angular.isDefined(vm.ctx.settings.title) ? vm.ctx.settings.title : ''; - vm.showTitle = vm.title && vm.title.length ? true : false; - vm.ctx.resize = resize; - $scope.$applyAsync(() => { - resize(); - }); - var initialValue = angular.isDefined(vm.ctx.settings.initialValue) ? vm.ctx.settings.initialValue : false; - setValue(initialValue); - - var subscription = vm.ctx.defaultSubscription; - var rpcEnabled = subscription.rpcEnabled; - - vm.isSimulated = $scope.widgetEditMode; - - vm.requestTimeout = 500; - if (vm.ctx.settings.requestTimeout) { - vm.requestTimeout = vm.ctx.settings.requestTimeout; - } - - vm.retrieveValueMethod = 'rpc'; - if (vm.ctx.settings.retrieveValueMethod && vm.ctx.settings.retrieveValueMethod.length) { - vm.retrieveValueMethod = vm.ctx.settings.retrieveValueMethod; - } - - vm.valueKey = 'value'; - if (vm.ctx.settings.valueKey && vm.ctx.settings.valueKey.length) { - vm.valueKey = vm.ctx.settings.valueKey; - } - - vm.parseValueFunction = (data) => data ? true : false; - if (vm.ctx.settings.parseValueFunction && vm.ctx.settings.parseValueFunction.length) { - try { - vm.parseValueFunction = new Function('data', vm.ctx.settings.parseValueFunction); - } catch (e) { - vm.parseValueFunction = (data) => data ? true : false; - } - } - - vm.convertValueFunction = (value) => value; - if (vm.ctx.settings.convertValueFunction && vm.ctx.settings.convertValueFunction.length) { - try { - vm.convertValueFunction = new Function('value', vm.ctx.settings.convertValueFunction); - } catch (e) { - vm.convertValueFunction = (value) => value; - } - } - - vm.getValueMethod = 'getValue'; - if (vm.ctx.settings.getValueMethod && vm.ctx.settings.getValueMethod.length) { - vm.getValueMethod = vm.ctx.settings.getValueMethod; - } - vm.setValueMethod = 'setValue'; - if (vm.ctx.settings.setValueMethod && vm.ctx.settings.setValueMethod.length) { - vm.setValueMethod = vm.ctx.settings.setValueMethod; - } - if (!rpcEnabled) { - onError('Target device is not set!'); - } else { - if (!vm.isSimulated) { - if (vm.retrieveValueMethod == 'rpc') { - rpcRequestValue(); - } else if (vm.retrieveValueMethod == 'attribute' || vm.retrieveValueMethod == 'timeseries') { - subscribeForValue(); - } - } - } - } - - function resize() { - var width = switchContainer.width(); - var height = switchContainer.height(); - var size = Math.min(width, height); - var scale = size/260; - switchElement.css({ - '-webkit-transform': `scale(${scale})`, - '-moz-transform': `scale(${scale})`, - '-ms-transform': `scale(${scale})`, - '-o-transform': `scale(${scale})`, - transform: `scale(${scale})` - }); - if (vm.showTitle) { - setFontSize(switchTitle, vm.title, switchTitleContainer.height() * 2 / 3, switchTitleContainer.width()); - } - setFontSize(switchError, vm.error, switchErrorContainer.height(), switchErrorContainer.width()); - } - - function subscribeForValue() { - var valueSubscriptionInfo = [{ - type: types.datasourceType.entity, - entityType: types.entityType.device, - entityId: vm.ctx.defaultSubscription.targetDeviceId - }]; - if (vm.retrieveValueMethod == 'attribute') { - valueSubscriptionInfo[0].attributes = [ - {name: vm.valueKey} - ]; - } else { - valueSubscriptionInfo[0].timeseries = [ - {name: vm.valueKey} - ]; - } - var subscriptionOptions = { - callbacks: { - onDataUpdated: onDataUpdated, - onDataUpdateError: onDataUpdateError - } - }; - vm.ctx.subscriptionApi.createSubscriptionFromInfo ( - types.widgetType.latest.value, valueSubscriptionInfo, subscriptionOptions, false, true).then( - (subscription) => { - vm.valueSubscription = subscription; - } - ); - } - - function onDataUpdated(subscription, apply) { - var value = false; - var data = subscription.data; - if (data.length) { - var keyData = data[0]; - if (keyData && keyData.data && keyData.data[0]) { - var attrValue = keyData.data[0][1]; - if (attrValue) { - var parsed = null; - try { - parsed = vm.parseValueFunction(angular.fromJson(attrValue)); - } catch (e){/**/} - value = parsed ? true : false; - } - } - } - setValue(value); - if (apply) { - $scope.$digest(); - } - } - - function onDataUpdateError(subscription, e) { - var exceptionData = utils.parseException(e); - var errorText = exceptionData.name; - if (exceptionData.message) { - errorText += ': ' + exceptionData.message; - } - onError(errorText); - } - - function setValue(value) { - vm.value = value ? true : false; - onoff.prop('checked', !vm.value); - } - - function onValue() { - rpcUpdateValue(vm.value); - } - - function onError(error) { - $scope.$applyAsync(() => { - vm.error = error; - setFontSize(switchError, vm.error, switchErrorContainer.height(), switchErrorContainer.width()); - }); - } - - function setFontSize(element, text, fontSize, maxWidth) { - var textWidth = measureTextWidth(text, fontSize); - while (textWidth > maxWidth) { - fontSize--; - textWidth = measureTextWidth(text, fontSize); - } - element.css({'fontSize': fontSize+'px', 'lineHeight': fontSize+'px'}); - } - - function measureTextWidth(text, fontSize) { - textMeasure.css({'fontSize': fontSize+'px', 'lineHeight': fontSize+'px'}); - textMeasure.text(text); - return textMeasure.width(); - } - - function rpcRequestValue() { - vm.error = ''; - vm.ctx.controlApi.sendTwoWayCommand(vm.getValueMethod, null, vm.requestTimeout).then( - (responseBody) => { - setValue(vm.parseValueFunction(responseBody)); - }, - () => { - var errorText = vm.ctx.defaultSubscription.rpcErrorText; - onError(errorText); - } - ); - } - - function rpcUpdateValue(value) { - if (vm.executingUpdateValue) { - vm.scheduledValue = value; - return; - } else { - vm.scheduledValue = null; - vm.rpcValue = value; - vm.executingUpdateValue = true; - } - vm.error = ''; - vm.ctx.controlApi.sendOneWayCommand(vm.setValueMethod, vm.convertValueFunction(value), vm.requestTimeout).then( - () => { - vm.executingUpdateValue = false; - if (vm.scheduledValue != null && vm.scheduledValue != vm.rpcValue) { - rpcUpdateValue(vm.scheduledValue); - } - }, - () => { - vm.executingUpdateValue = false; - var errorText = vm.ctx.defaultSubscription.rpcErrorText; - onError(errorText); - } - ); - } -} diff --git a/ui/src/app/widget/lib/rpc/round-switch.scss b/ui/src/app/widget/lib/rpc/round-switch.scss deleted file mode 100644 index ead8a7d7da..0000000000 --- a/ui/src/app/widget/lib/rpc/round-switch.scss +++ /dev/null @@ -1,200 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "~compass-sass-mixins/lib/compass"; - -$error-height: 14px !default; - -.tb-round-switch { - width: 100%; - height: 100%; - - .title-container { - .switch-title { - font-weight: 500; - color: #757575; - white-space: nowrap; - } - } - - .error-container { - position: absolute; - top: 1%; - right: 0; - left: 0; - z-index: 4; - height: $error-height; - - .switch-error { - color: #ff3315; - white-space: nowrap; - } - } - - #text-measure { - position: absolute; - width: auto; - height: auto; - white-space: nowrap; - visibility: hidden; - } - - #switch-container { - padding: 10px; - - .switch { - position: relative; - - box-sizing: border-box; - width: 260px; - min-width: 260px; - height: 260px; - min-height: 260px; - padding: 25px; - font-family: sans-serif; - font-size: 48px; - - color: #424242; - cursor: pointer; - background: #ddd; - background: linear-gradient(180deg, #bbb, #ddd); - border-radius: 130px; - - box-shadow: - 0 0 0 8px rgba(0, 0, 0, .1), - 0 0 3px 1px rgba(0, 0, 0, .1), - inset 0 8px 3px -8px rgba(255, 255, 255, .4); - - input { - display: none; - } - - .on, - .off { - position: absolute; - width: 100%; - text-align: center; - - text-shadow: 1px 1px 4px #4a4a4a; - } - - .on { - top: 10px; - font-family: sans-serif; - color: #444; - - transition: all .1s; - } - - .off { - bottom: 5px; - - transition: all .1s; - - transform: scaleY(.85); - } - - .but { - position: relative; - display: block; - width: 200px; - height: 178px; - font-size: 48px; - cursor: pointer; - background-color: #d8d8d8; - border-bottom-width: 0; - border-radius: 400px 400px 400px 400px / 400px 400px 300px 300px; - - box-shadow: - inset 8px 6px 5px -7px #a2a2a2, - inset -8px 6px 5px -7px #a2a2a2, - inset 0 -3px 2px -2px rgba(200, 200, 200, .5), - 0 3px 3px -2px #fff, - inset 0 -230px 60px -200px rgba(255, 255, 255, .2), - inset 0 220px 40px -200px rgba(0, 0, 0, .3); - - transition: all .2s; - } - - .back { - - box-sizing: border-box; - width: 210px; - height: 210px; - padding: 4px 4px; - cursor: pointer; - background-color: #888787; - background-image: linear-gradient(-90deg, transparent 30%, transparent 70%), linear-gradient(0deg, rgba(150, 150, 150, 0) 30%, rgba(150, 150, 150, .2) 50%, rgba(150, 150, 150, 0) 70%); - border-radius: 105px; - - box-shadow: - 30px 30px 30px -20px rgba(58, 58, 58, .3), - -30px 30px 30px -20px rgba(58, 58, 58, .3), - 0 30px 30px 0 rgba(16, 16, 16, .3), - inset 0 -1px 0 0 #484848; - - transition: all .2s; - } - - - input:checked + .back .on, - input:checked + .back .off{ - text-shadow: 1px 1px 4px #4a4a4a; - } - - input:checked + .back .on{ - top: 10px; - color: #4c4c4c; - - transform: scaleY(.85); - } - - input:checked + .back .off{ - bottom: 5px; - color: #444; - - transform: scaleY(1); - } - - input:checked + .back .but{ - margin-top: 20px; - background: #dcdcdc; - background-image: radial-gradient(50% 15%, circle closest-corner, rgba(0, 0, 0, .3), transparent); - border-radius: 400px 400px 400px 400px / 300px 300px 400px 400px; - - box-shadow: - inset 8px -4px 5px -7px #a9a9a9, - inset -8px -4px 5px -7px #808080, - 0 -3px 8px -4px rgba(50, 50, 50, .4), - inset 0 3px 4px -2px #9c9c9c, - inset 0 280px 40px -200px rgba(0, 0, 0, .2), - inset 0 -200px 40px -200px rgba(180, 180, 180, .2); - } - - input:checked + .back{ - padding: 2px 4px; - - background-image: linear-gradient(0deg, #868686 30%, transparent 70%), linear-gradient(90deg, rgba(115, 115, 115, 0) 0%, rgba(255, 255, 255, .74) 50%, rgba(105, 105, 105, 0) 100%); - - box-shadow: - 30px 30px 30px -20px rgba(49, 49, 49, .1), - -30px 30px 30px -20px rgba(111, 111, 111, .1), - 0 30px 30px 0 rgba(0, 0, 0, .2), - inset 0 1px 2px 0 rgba(167, 167, 167, .6); - } - } - } -} - diff --git a/ui/src/app/widget/lib/rpc/round-switch.tpl.html b/ui/src/app/widget/lib/rpc/round-switch.tpl.html deleted file mode 100644 index fcecbef0f3..0000000000 --- a/ui/src/app/widget/lib/rpc/round-switch.tpl.html +++ /dev/null @@ -1,38 +0,0 @@ - -
    -
    - {{vm.title}} -
    -
    -
    - -
    - -
    -
    -
    -
    - {{ vm.error }} -
    -
    -
    diff --git a/ui/src/app/widget/lib/rpc/switch.directive.js b/ui/src/app/widget/lib/rpc/switch.directive.js deleted file mode 100644 index 0105056dab..0000000000 --- a/ui/src/app/widget/lib/rpc/switch.directive.js +++ /dev/null @@ -1,314 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './switch.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import switchTemplate from './switch.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -export default angular.module('thingsboard.widgets.rpc.switch', []) - .directive('tbSwitch', Switch) - .name; - -/*@ngInject*/ -function Switch() { - return { - restrict: "E", - scope: true, - bindToController: { - ctx: '=' - }, - controller: SwitchController, - controllerAs: 'vm', - templateUrl: switchTemplate - }; -} - -/*@ngInject*/ -function SwitchController($element, $scope, types, utils) { - let vm = this; - - vm.showTitle = false; - vm.value = false; - vm.error = ''; - - var switchElement = angular.element('.switch', $element), - switchContainer = angular.element('#switch-container', $element), - mdSwitch = angular.element('md-switch', switchElement), - onoffContainer = angular.element('.onoff-container', $element), - onLabel = angular.element('.on-label', $element), - offLabel = angular.element('.off-label', $element), - switchTitleContainer = angular.element('.title-container', $element), - switchTitle = angular.element('.switch-title', $element), - textMeasure = angular.element('#text-measure', $element), - switchErrorContainer = angular.element('.error-container', $element), - switchError = angular.element('.switch-error', $element); - - - vm.onValue = onValue; - - vm.valueSubscription = null; - - $scope.$watch('vm.ctx', () => { - if (vm.ctx) { - init(); - } - }); - - $scope.$on('$destroy', () => { - if (vm.valueSubscription) { - vm.ctx.subscriptionApi.removeSubscription(vm.valueSubscription.id); - } - }); - - function init() { - - vm.title = angular.isDefined(vm.ctx.settings.title) ? vm.ctx.settings.title : ''; - vm.showTitle = vm.title && vm.title.length ? true : false; - vm.showOnOffLabels = angular.isDefined(vm.ctx.settings.showOnOffLabels) ? vm.ctx.settings.showOnOffLabels : true; - vm.ctx.resize = resize; - $scope.$applyAsync(() => { - resize(); - }); - var initialValue = angular.isDefined(vm.ctx.settings.initialValue) ? vm.ctx.settings.initialValue : false; - setValue(initialValue); - - var subscription = vm.ctx.defaultSubscription; - var rpcEnabled = subscription.rpcEnabled; - - vm.isSimulated = $scope.widgetEditMode; - - vm.requestTimeout = 500; - if (vm.ctx.settings.requestTimeout) { - vm.requestTimeout = vm.ctx.settings.requestTimeout; - } - - vm.retrieveValueMethod = 'rpc'; - if (vm.ctx.settings.retrieveValueMethod && vm.ctx.settings.retrieveValueMethod.length) { - vm.retrieveValueMethod = vm.ctx.settings.retrieveValueMethod; - } - - vm.valueKey = 'value'; - if (vm.ctx.settings.valueKey && vm.ctx.settings.valueKey.length) { - vm.valueKey = vm.ctx.settings.valueKey; - } - - vm.parseValueFunction = (data) => data ? true : false; - if (vm.ctx.settings.parseValueFunction && vm.ctx.settings.parseValueFunction.length) { - try { - vm.parseValueFunction = new Function('data', vm.ctx.settings.parseValueFunction); - } catch (e) { - vm.parseValueFunction = (data) => data ? true : false; - } - } - - vm.convertValueFunction = (value) => value; - if (vm.ctx.settings.convertValueFunction && vm.ctx.settings.convertValueFunction.length) { - try { - vm.convertValueFunction = new Function('value', vm.ctx.settings.convertValueFunction); - } catch (e) { - vm.convertValueFunction = (value) => value; - } - } - - vm.getValueMethod = 'getValue'; - if (vm.ctx.settings.getValueMethod && vm.ctx.settings.getValueMethod.length) { - vm.getValueMethod = vm.ctx.settings.getValueMethod; - } - vm.setValueMethod = 'setValue'; - if (vm.ctx.settings.setValueMethod && vm.ctx.settings.setValueMethod.length) { - vm.setValueMethod = vm.ctx.settings.setValueMethod; - } - if (!rpcEnabled) { - onError('Target device is not set!'); - } else { - if (!vm.isSimulated) { - if (vm.retrieveValueMethod == 'rpc') { - rpcRequestValue(); - } else if (vm.retrieveValueMethod == 'attribute' || vm.retrieveValueMethod == 'timeseries') { - subscribeForValue(); - } - } - } - } - - const switchAspectRation = 2.7893; - - function resize() { - var width = switchContainer.width(); - var height; - if (vm.showOnOffLabels) { - height = switchContainer.height()*2/3; - } else { - height = switchContainer.height(); - } - var ratio = width/height; - if (ratio > switchAspectRation) { - width = height*switchAspectRation; - } else { - height = width/switchAspectRation; - } - switchElement.css({width: width, height: height}); - mdSwitch.css('height', height+'px'); - mdSwitch.css('width', width+'px'); - mdSwitch.css('min-width', width+'px'); - angular.element('.md-container', mdSwitch).css('height', height+'px'); - angular.element('.md-container', mdSwitch).css('width', width+'px'); - - - if (vm.showTitle) { - setFontSize(switchTitle, vm.title, switchTitleContainer.height() * 2 / 3, switchTitleContainer.width()); - } - - if (vm.showOnOffLabels) { - onoffContainer.css({width: width, height: switchContainer.height() / 3}); - setFontSize(onLabel, 'OFF', onoffContainer.height(), onoffContainer.width() / 2); - setFontSize(offLabel, 'OFF', onoffContainer.height(), onoffContainer.width() / 2); - } - - setFontSize(switchError, vm.error, switchErrorContainer.height(), switchErrorContainer.width()); - } - - function subscribeForValue() { - var valueSubscriptionInfo = [{ - type: types.datasourceType.entity, - entityType: types.entityType.device, - entityId: vm.ctx.defaultSubscription.targetDeviceId - }]; - if (vm.retrieveValueMethod == 'attribute') { - valueSubscriptionInfo[0].attributes = [ - {name: vm.valueKey} - ]; - } else { - valueSubscriptionInfo[0].timeseries = [ - {name: vm.valueKey} - ]; - } - var subscriptionOptions = { - callbacks: { - onDataUpdated: onDataUpdated, - onDataUpdateError: onDataUpdateError - } - }; - vm.ctx.subscriptionApi.createSubscriptionFromInfo ( - types.widgetType.latest.value, valueSubscriptionInfo, subscriptionOptions, false, true).then( - (subscription) => { - vm.valueSubscription = subscription; - } - ); - } - - function onDataUpdated(subscription, apply) { - var value = false; - var data = subscription.data; - if (data.length) { - var keyData = data[0]; - if (keyData && keyData.data && keyData.data[0]) { - var attrValue = keyData.data[0][1]; - if (attrValue) { - var parsed = null; - try { - parsed = vm.parseValueFunction(angular.fromJson(attrValue)); - } catch (e){/**/} - value = parsed ? true : false; - } - } - } - setValue(value); - if (apply) { - $scope.$digest(); - } - } - - function onDataUpdateError(subscription, e) { - var exceptionData = utils.parseException(e); - var errorText = exceptionData.name; - if (exceptionData.message) { - errorText += ': ' + exceptionData.message; - } - onError(errorText); - } - - function setValue(value) { - vm.value = value ? true : false; - } - - function onValue() { - rpcUpdateValue(vm.value); - } - - function onError(error) { - $scope.$applyAsync(() => { - vm.error = error; - setFontSize(switchError, vm.error, switchErrorContainer.height(), switchErrorContainer.width()); - }); - } - - function setFontSize(element, text, fontSize, maxWidth) { - var textWidth = measureTextWidth(text, fontSize); - while (textWidth > maxWidth) { - fontSize--; - textWidth = measureTextWidth(text, fontSize); - } - element.css({'fontSize': fontSize+'px', 'lineHeight': fontSize+'px'}); - } - - function measureTextWidth(text, fontSize) { - textMeasure.css({'fontSize': fontSize+'px', 'lineHeight': fontSize+'px'}); - textMeasure.text(text); - return textMeasure.width(); - } - - function rpcRequestValue() { - vm.error = ''; - vm.ctx.controlApi.sendTwoWayCommand(vm.getValueMethod, null, vm.requestTimeout).then( - (responseBody) => { - setValue(vm.parseValueFunction(responseBody)); - }, - () => { - var errorText = vm.ctx.defaultSubscription.rpcErrorText; - onError(errorText); - } - ); - } - - function rpcUpdateValue(value) { - if (vm.executingUpdateValue) { - vm.scheduledValue = value; - return; - } else { - vm.scheduledValue = null; - vm.rpcValue = value; - vm.executingUpdateValue = true; - } - vm.error = ''; - vm.ctx.controlApi.sendOneWayCommand(vm.setValueMethod, vm.convertValueFunction(value), vm.requestTimeout).then( - () => { - vm.executingUpdateValue = false; - if (vm.scheduledValue != null && vm.scheduledValue != vm.rpcValue) { - rpcUpdateValue(vm.scheduledValue); - } - }, - () => { - vm.executingUpdateValue = false; - var errorText = vm.ctx.defaultSubscription.rpcErrorText; - onError(errorText); - } - ); - } -} diff --git a/ui/src/app/widget/lib/rpc/switch.scss b/ui/src/app/widget/lib/rpc/switch.scss deleted file mode 100644 index 83c9c4e952..0000000000 --- a/ui/src/app/widget/lib/rpc/switch.scss +++ /dev/null @@ -1,134 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -$thumb-img: url("./svg/thumb.svg") !default; -$thumb-checked-img: url("./svg/thumb-checked.svg") !default; -$thumb-bar-img: url("./svg/thumb-bar.svg") !default; -$thumb-bar-checked-img: url("./svg/thumb-bar-checked.svg") !default; - -$error-height: 14px !default; - -.tb-switch { - width: 100%; - height: 100%; - - .error-container { - position: absolute; - top: 1%; - right: 0; - left: 0; - z-index: 4; - height: $error-height; - - .switch-error { - color: #ff3315; - white-space: nowrap; - } - } - - .onoff-container { - height: 100%; - font-weight: 500; - color: #757575; - white-space: nowrap; - - .off-label { - color: #b7b5b5; - } - - .on-label { - color: #ff7e57; - text-shadow: #ff6e4a 1px 1px 10px, #ffd1c3 1px 1px 10px; - } - } - - .title-container { - .switch-title { - font-weight: 500; - color: #757575; - white-space: nowrap; - } - } - - #switch-container { - padding-right: 10px; - padding-left: 10px; - } - - .switch { - position: relative; - - md-switch { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - margin: 0; - - .md-container { - margin: 0; - } - - .md-bar { - top: 0; - left: 0; - width: 100%; - height: 100%; - background: $thumb-bar-img no-repeat; - background-size: contain; - border-radius: 0; - } - - .md-thumb-container { - top: 5%; - left: .25%; - width: 50%; - height: 90%; - } - - .md-thumb { - top: 0; - left: 0; - width: 100%; - height: 100%; - background: $thumb-img no-repeat; - background-size: contain; - border-radius: 0; - box-shadow: none; - } - - &.md-checked { - .md-bar { - background: $thumb-bar-checked-img no-repeat; - background-size: contain; - } - - .md-thumb { - background: $thumb-checked-img no-repeat; - background-size: contain; - } - } - } - } - - #text-measure { - position: absolute; - width: auto; - height: auto; - white-space: nowrap; - visibility: hidden; - } -} diff --git a/ui/src/app/widget/lib/rpc/switch.tpl.html b/ui/src/app/widget/lib/rpc/switch.tpl.html deleted file mode 100644 index 8b63bbd48e..0000000000 --- a/ui/src/app/widget/lib/rpc/switch.tpl.html +++ /dev/null @@ -1,39 +0,0 @@ - -
    -
    - {{ vm.error }} -
    -
    - {{vm.title}} -
    -
    -
    - - -
    -
    - OFF - - ON - -
    -
    -
    -
    diff --git a/ui/src/app/widget/lib/tencent-map.js b/ui/src/app/widget/lib/tencent-map.js deleted file mode 100644 index ac6b4a8ea1..0000000000 --- a/ui/src/app/widget/lib/tencent-map.js +++ /dev/null @@ -1,499 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -var tmGlobals = { - loadingTmId: null, - tmApiKeys: {} -} - -export default class TbTencentMap { - constructor($containerElement, utils, initCallback, defaultZoomLevel, dontFitMapBounds, disableScrollZooming, minZoomLevel, tmApiKey, tmDefaultMapType, defaultCenterPosition, markerClusteringSetting) { - var tbMap = this; - this.utils = utils; - this.defaultZoomLevel = defaultZoomLevel; - this.dontFitMapBounds = dontFitMapBounds; - this.minZoomLevel = minZoomLevel; - this.tooltips = []; - this.defaultMapType = tmDefaultMapType; - this.defaultCenterPosition =defaultCenterPosition; - this.isMarketCluster = markerClusteringSetting && markerClusteringSetting.isMarketCluster; - - function clearGlobalId() { - if (tmGlobals.loadingTmId && tmGlobals.loadingTmId === tbMap.mapId) { - tmGlobals.loadingTmId = null; - } - } - - function displayError(message) { - $containerElement.html( // eslint-disable-line angular/angularelement - "
    " + message + "
    " - ); - } - - function initTencentMap() { - tbMap.map = new qq.maps.Map($containerElement[0], { // eslint-disable-line no-undef - scrollwheel: !disableScrollZooming, - mapTypeId: getTencentMapTypeId(tbMap.defaultMapType), - zoom: tbMap.defaultZoomLevel || 8, - center: new qq.maps.LatLng(tbMap.defaultCenterPosition[0],tbMap.defaultCenterPosition[1]) // eslint-disable-line no-undef - }); - - if (tbMap.isMarketCluster){ - tbMap.markersCluster = new qq.maps.MarkerCluster( // eslint-disable-line no-undef - angular.merge({map:tbMap.map}, markerClusteringSetting) - ); - } - - - if (initCallback) { - setTimeout(initCallback, 0);// eslint-disable-line - } - } - - /* eslint-disable no-undef */ - - function getTencentMapTypeId(mapType) { - var mapTypeId = qq.maps.MapTypeId.ROADMAP; - if (mapType) { - if (mapType === 'hybrid') { - mapTypeId = qq.maps.MapTypeId.HYBRID; - } else if (mapType === 'satellite') { - mapTypeId = qq.maps.MapTypeId.SATELLITE; - } else if (mapType === 'terrain') { - mapTypeId = qq.maps.MapTypeId.ROADMAP; - } - } - return mapTypeId; - } - - /* eslint-enable no-undef */ - - this.mapId = '' + Math.random().toString(36).substr(2, 9); - this.apiKey = tmApiKey || '84d6d83e0e51e481e50454ccbe8986b'; - - window.tm_authFailure = function () { // eslint-disable-line no-undef, angular/window-service - if (tmGlobals.loadingTmId && tmGlobals.loadingTmId === tbMap.mapId) { - tmGlobals.loadingTmId = null; - tmGlobals.tmApiKeys[tbMap.apiKey].error = 'Unable to authentificate for tencent Map API.
    Please check your API key.'; - displayError(tmGlobals.tmApiKeys[tbMap.apiKey].error); - } - }; - - this.initMapFunctionName = 'initTencentMap_' + this.mapId; - - window[this.initMapFunctionName] = function () { // eslint-disable-line no-undef, angular/window-service - tmGlobals.tmApiKeys[tbMap.apiKey].loaded = true; - initTencentMap(); - for (var p = 0; p < tmGlobals.tmApiKeys[tbMap.apiKey].pendingInits.length; p++) { - var pendingInit = tmGlobals.tmApiKeys[tbMap.apiKey].pendingInits[p]; - pendingInit(); - } - tmGlobals.tmApiKeys[tbMap.apiKey].pendingInits = []; - }; - if (this.apiKey && this.apiKey.length > 0) { - if (tmGlobals.tmApiKeys[this.apiKey]) { - if (tmGlobals.tmApiKeys[this.apiKey].error) { - displayError(tmGlobals.tmApiKeys[this.apiKey].error); - } else if (tmGlobals.tmApiKeys[this.apiKey].loaded) { - initTencentMap(); - } else { - tmGlobals.tmApiKeys[this.apiKey].pendingInits.push(initTencentMap); - } - } else { - tmGlobals.tmApiKeys[this.apiKey] = { - loaded: false, - pendingInits: [] - }; - var tencentMapScriptRes = 'https://map.qq.com/api/js?v=2.exp&key=' + this.apiKey + '&callback=' + this.initMapFunctionName; - - tmGlobals.loadingTmId = this.mapId; - lazyLoad.load({type: 'js', path: tencentMapScriptRes}).then( // eslint-disable-line no-undef - function success() { - setTimeout(clearGlobalId, 2000); // eslint-disable-line no-undef, angular/timeout-service - }, - function fail(e) { - clearGlobalId(); - tmGlobals.tmApiKeys[tbMap.apiKey].error = 'tencent map api load failed!
    ' + e; - displayError(tmGlobals.tmApiKeys[tbMap.apiKey].error); - } - ); - } - } else { - displayError('No tencent Map Api Key provided!'); - } - } - - inited() { - return angular.isDefined(this.map); - } - - createMarkerLabelStyle(settings) { - return { - width: "200px", - textAlign: "center", - color: settings.labelColor, - background: "none", - border: "none", - fontSize: "12px", - fontFamily: "\"Helvetica Neue\", Arial, Helvetica, sans-serif", - fontWeight: "bold" - }; - } - - /* eslint-disable no-undef,no-unused-vars*/ - updateMarkerLabel(marker, settings) { - if (marker.label) { - marker.label.setContent(settings.labelText); - marker.label.setStyle(this.createMarkerLabelStyle(settings)); - } - } - - /* eslint-enable no-undef,no-unused-vars */ - - /* eslint-disable no-undef,no-unused-vars */ - updateMarkerColor(marker, color) { - this.createDefaultMarkerIcon(marker, color, (iconInfo) => { - marker.setIcon(iconInfo.icon); - }); - } - - /* eslint-enable no-undef,,no-unused-vars */ - - /* eslint-disable no-undef */ - updateMarkerIcon(marker, settings) { - this.createMarkerIcon(marker, settings, (iconInfo) => { - marker.setIcon(iconInfo.icon); - if (marker.label) { - marker.label.setOffset(new qq.maps.Size(-100, -iconInfo.size[1] - 20)); - } - }); - } - - /* eslint-disable no-undef */ - - /* eslint-disable no-undef */ - createMarkerIcon(marker, settings, onMarkerIconReady) { - var currentImage = settings.currentImage; - var tMap = this; - if (currentImage && currentImage.url) { - this.utils.loadImageAspect(currentImage.url).then( - (aspect) => { - if (aspect) { - var width; - var height; - if (aspect > 1) { - width = currentImage.size; - height = currentImage.size / aspect; - } else { - width = currentImage.size * aspect; - height = currentImage.size; - } - var icon = new qq.maps.MarkerImage(currentImage.url, - new qq.maps.Size(width, height), - new qq.maps.Point(0, 0), - new qq.maps.Point(width / 2, height), - new qq.maps.Size(width, height)); - var iconInfo = { - size: [width, height], - icon: icon - }; - onMarkerIconReady(iconInfo); - } else { - tMap.createDefaultMarkerIcon(marker, settings.color, onMarkerIconReady); - } - } - ); - } else { - this.createDefaultMarkerIcon(marker, settings.color, onMarkerIconReady); - } - } - - /* eslint-enable no-undef */ - - /* eslint-disable no-undef */ - createDefaultMarkerIcon(marker, color, onMarkerIconReady) { - var pinColor = color.substr(1); - var icon = new qq.maps.MarkerImage("https://chart.apis.google.com/chart?chst=d_map_pin_letter_withshadow&chld=%E2%80%A2|" + pinColor, - new qq.maps.Size(40, 37), - new qq.maps.Point(0, 0), - new qq.maps.Point(10, 37)); - var iconInfo = { - size: [40, 37], - icon: icon - }; - onMarkerIconReady(iconInfo); - } - - /* eslint-enable no-undef */ - - /* eslint-disable no-undef */ - createMarker(location, dsIndex, settings, onClickListener, markerArgs, onDragendListener) { - var marker = new qq.maps.Marker({ - position: location - }); - var tMap = this; - this.createMarkerIcon(marker, settings, (iconInfo) => { - marker.setIcon(iconInfo.icon); - if(tMap.isMarketCluster) { - tMap.markersCluster.addMarker(marker); - } else { - marker.setMap(tMap.map); - } - if (settings.showLabel) { - marker.label = new qq.maps.Label({ - clickable: false, - content: settings.labelText, - offset: new qq.maps.Size(-100, -iconInfo.size[1] - 20), - style: tMap.createMarkerLabelStyle(settings), - visible: true, - position: location, - map: tMap.map, - zIndex: 1000, - draggable: settings.drraggable - }); - } - }); - - if (settings.displayTooltip) { - this.createTooltip(marker, dsIndex, settings, markerArgs); - } - - if (onClickListener) { - qq.maps.event.addListener(marker, 'click', onClickListener); - } - - if (onDragendListener) { - qq.maps.event.addListener(marker, 'dragend', onDragendListener); - } - - return marker; - } - - /* eslint-disable no-undef */ - removeMarker(marker) { - marker.setMap(null); - if (marker.label) { - marker.label.setMap(null); - } - } - - /* eslint-enable no-undef */ - - /* eslint-disable no-undef */ - createTooltip(marker, dsIndex, settings, markerArgs) { - var popup = new qq.maps.InfoWindow({ - map: this.map - }); - var map = this; - if (settings.displayTooltipAction == 'hover') { - qq.maps.event.addListener(marker, 'mouseover', function () { - popup.open(); - popup.setPosition(marker); - }); - qq.maps.event.addListener(marker, 'mouseout', function () { - popup.close(); - }); - } else { - qq.maps.event.addListener(marker, 'click', function () { - if (settings.autocloseTooltip) { - map.tooltips.forEach((tooltip) => { - tooltip.popup.close(); - }); - } - popup.open(); - popup.setPosition(marker); - }); - } - map.tooltips.push({ - markerArgs: markerArgs, - popup: popup, - locationSettings: settings, - dsIndex: dsIndex - }); - } - - /* eslint-enable no-undef */ - - /* eslint-disable no-undef */ - updatePolylineColor(polyline, settings, color) { - var options = { - path: polyline.getPath(), - strokeColor: color, - strokeOpacity: settings.strokeOpacity, - strokeWeight: settings.strokeWeight, - map: this.map - }; - polyline.setOptions(options); - } - - /* eslint-enable no-undef */ - - /* eslint-disable no-undef */ - createPolyline(locations, settings) { - var polyline = new qq.maps.Polyline({ - path: locations, - strokeColor: settings.color, - strokeOpacity: settings.strokeOpacity, - strokeWeight: settings.strokeWeight, - map: this.map - }); - - return polyline; - } - - /* eslint-enable no-undef */ - - removePolyline(polyline) { - polyline.setMap(null); - } - - /* eslint-disable no-undef */ - createPolygon(latLangs, settings, location, onClickListener, markerArgs) { - let polygon = new qq.maps.Polygon({ - map: this.map, - path: latLangs, - strokeColor: settings.polygonStrokeColor, - fillColor: qq.maps.Color.fromHex(settings.polygonColor, settings.polygonOpacity), - strokeWeight: settings.polygonStrokeWeight - }); - //initialize-tooltip - let popup = new qq.maps.InfoWindow({ - content: '' - }); - if (!this.tooltips) this.tooltips = []; - this.tooltips.push({ - markerArgs: markerArgs, - popup: popup, - locationSettings: settings, - dsIndex: location.dsIndex - }); - - if (onClickListener) { - qq.maps.event.addListener(polygon, 'click', function (event) { - if (settings.autocloseTooltip) { - map.tooltips.forEach((tooltip) => { - tooltip.popup.close(); - }); - } - if (settings.displayTooltip) { - popup.setMap(this.map); - popup.setPosition(event.latLng); - popup.open(); - } - onClickListener(); - }); - } - return polygon; - } - - /* eslint-disable no-undef */ - - removePolygon (polygon) { - polygon.setMap(null); - } - - /* eslint-disable no-undef,no-unused-vars */ - updatePolygonColor (polygon, settings, color) { - let options = { - path: polygon.getPath(), - map: this.map, - strokeColor: color, - fillColor: qq.maps.Color.fromHex(color, settings.polygonOpacity), - strokeWeight: settings.polygonStrokeWeight - } - polygon.setOptions(options); - } - /* eslint-disable no-undef ,no-unused-vars*/ - - - getPolygonLatLngs(polygon) { - return polygon.getPath().getArray(); - } - - setPolygonLatLngs(polygon, latLngs) { - polygon.setPath(latLngs); - } - - /* eslint-disable no-undef ,no-unused-vars*/ - fitBounds(bounds, useDefaultZoom) { - if ((this.dontFitMapBounds || useDefaultZoom) && this.defaultZoomLevel) { - this.map.setZoom(this.defaultZoomLevel); - this.map.setCenter(bounds.getCenter()); - } else { - var tbMap = this; - qq.maps.event.addListenerOnce(this.map, 'bounds_changed', function () { // eslint-disable-line no-undef - if (!tbMap.defaultZoomLevel && tbMap.map.getZoom() > tbMap.minZoomLevel) { - tbMap.map.setZoom(tbMap.minZoomLevel); - } - }); - this.map.fitBounds(bounds); - } - } - - /* eslint-enable no-undef,no-unused-vars */ - - createLatLng(lat, lng) { - return new qq.maps.LatLng(lat, lng); // eslint-disable-line no-undef - } - - extendBoundsWithMarker(bounds, marker) { - bounds.extend(marker.getPosition()); - } - - getMarkerPosition(marker) { - return marker.getPosition(); - } - - setMarkerPosition(marker, latLng) { - marker.setPosition(latLng); - if (marker.label) { - marker.label.setPosition(latLng); - } - } - - getPolylineLatLngs(polyline) { - return polyline.getPath().getArray(); - } - - setPolylineLatLngs(polyline, latLngs) { - polyline.setPath(latLngs); - } - - createBounds() { - return new qq.maps.LatLngBounds(); // eslint-disable-line no-undef - } - - extendBounds(bounds, polyline) { - if (polyline && polyline.getPath()) { - var locations = polyline.getPath(); - for (var i = 0; i < locations.getLength(); i++) { - bounds.extend(locations.getAt(i)); - } - } - } - - invalidateSize() { - qq.maps.event.trigger(this.map, "resize"); // eslint-disable-line no-undef - } - - getTooltips() { - return this.tooltips; - } - - getCenter() { - return this.map.getCenter(); - } - -} diff --git a/ui/src/app/widget/lib/timeseries-table-widget.js b/ui/src/app/widget/lib/timeseries-table-widget.js deleted file mode 100644 index 5922288ad7..0000000000 --- a/ui/src/app/widget/lib/timeseries-table-widget.js +++ /dev/null @@ -1,445 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './timeseries-table-widget.scss'; - -/* eslint-disable import/no-unresolved, import/default */ - -import timeseriesTableWidgetTemplate from './timeseries-table-widget.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -import tinycolor from 'tinycolor2'; -import cssjs from '../../../vendor/css.js/css'; - -export default angular.module('thingsboard.widgets.timeseriesTableWidget', []) - .directive('tbTimeseriesTableWidget', TimeseriesTableWidget) - .name; - -/*@ngInject*/ -function TimeseriesTableWidget() { - return { - restrict: "E", - scope: true, - bindToController: { - tableId: '=', - ctx: '=' - }, - controller: TimeseriesTableWidgetController, - controllerAs: 'vm', - templateUrl: timeseriesTableWidgetTemplate - }; -} - -/*@ngInject*/ -function TimeseriesTableWidgetController($element, $scope, $filter, $timeout, types) { - var vm = this; - let dateFormatFilter; - - vm.sources = []; - vm.sourceIndex = 0; - vm.defaultPageSize = 10; - vm.defaultSortOrder = '-0'; - vm.query = { - "search": null - }; - - vm.enterFilterMode = enterFilterMode; - vm.exitFilterMode = exitFilterMode; - vm.onRowClick = onRowClick; - vm.onActionButtonClick = onActionButtonClick; - vm.actionCellDescriptors = []; - - function enterFilterMode () { - vm.query.search = ''; - vm.ctx.hideTitlePanel = true; - $timeout(()=>{ - angular.element(vm.ctx.$container).find('.searchInput').focus(); - }) - } - - function exitFilterMode () { - vm.query.search = null; - vm.ctx.hideTitlePanel = false; - } - - vm.searchAction = { - name: 'action.search', - show: true, - onAction: function() { - vm.enterFilterMode(); - }, - icon: 'search' - }; - - $scope.$watch('vm.ctx', function() { - if (vm.ctx) { - vm.settings = vm.ctx.settings; - vm.widgetConfig = vm.ctx.widgetConfig; - vm.data = vm.ctx.data; - vm.datasources = vm.ctx.datasources; - initialize(); - } - }); - - function initialize() { - vm.ctx.widgetActions = [ vm.searchAction ]; - vm.actionCellDescriptors = vm.ctx.actionsApi.getActionDescriptors('actionCellButton'); - vm.showTimestamp = vm.settings.showTimestamp !== false; - dateFormatFilter = (vm.settings.showMilliseconds !== true) ? 'yyyy-MM-dd HH:mm:ss' : 'yyyy-MM-dd HH:mm:ss.sss'; - var origColor = vm.widgetConfig.color || 'rgba(0, 0, 0, 0.87)'; - var defaultColor = tinycolor(origColor); - var mdDark = defaultColor.setAlpha(0.87).toRgbString(); - var mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString(); - var mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString(); - //var mdDarkIcon = mdDarkSecondary; - var mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString(); - - var cssString = 'table.md-table th.md-column {\n'+ - 'color: ' + mdDarkSecondary + ';\n'+ - '}\n'+ - 'table.md-table th.md-column md-icon.md-sort-icon {\n'+ - 'color: ' + mdDarkDisabled + ';\n'+ - '}\n'+ - 'table.md-table th.md-column.md-active, table.md-table th.md-column.md-active md-icon {\n'+ - 'color: ' + mdDark + ';\n'+ - '}\n'+ - 'table.md-table td.md-cell {\n'+ - 'color: ' + mdDark + ';\n'+ - 'border-top: 1px '+mdDarkDivider+' solid;\n'+ - '}\n'+ - 'table.md-table td.md-cell.md-placeholder {\n'+ - 'color: ' + mdDarkDisabled + ';\n'+ - '}\n'+ - 'table.md-table td.md-cell md-select > .md-select-value > span.md-select-icon {\n'+ - 'color: ' + mdDarkSecondary + ';\n'+ - '}\n'+ - '.md-table-pagination {\n'+ - 'color: ' + mdDarkSecondary + ';\n'+ - 'border-top: 1px '+mdDarkDivider+' solid;\n'+ - '}\n'+ - '.md-table-pagination .buttons md-icon {\n'+ - 'color: ' + mdDarkSecondary + ';\n'+ - '}\n'+ - '.md-table-pagination md-select:not([disabled]):focus .md-select-value {\n'+ - 'color: ' + mdDarkSecondary + ';\n'+ - '}'; - - var cssParser = new cssjs(); - cssParser.testMode = false; - var namespace = 'ts-table-' + hashCode(cssString); - cssParser.cssPreviewNamespace = namespace; - cssParser.createStyleElement(namespace, cssString); - $element.addClass(namespace); - - vm.displayPagination = angular.isDefined(vm.settings.displayPagination) ? vm.settings.displayPagination : true; - - function hashCode(str) { - var hash = 0; - var i, char; - if (str.length === 0) return hash; - for (i = 0; i < str.length; i++) { - char = str.charCodeAt(i); - hash = ((hash << 5) - hash) + char; - hash = hash & hash; - } - return hash; - } - updateDatasources(); - } - - $scope.$on('timeseries-table-data-updated', function(event, tableId) { - if (vm.tableId == tableId) { - dataUpdated(); - } - }); - - function dataUpdated() { - for (var s=0; s < vm.sources.length; s++) { - var source = vm.sources[s]; - source.rawData = vm.data.slice(source.keyStartIndex, source.keyEndIndex); - } - updateSourceData(vm.sources[vm.sourceIndex]); - $scope.$digest(); - } - - vm.onPaginate = function(source) { - updatePage(source); - } - - vm.onReorder = function(source) { - reorder(source); - updatePage(source); - } - - function onRowClick($event, row) { - if ($event) { - $event.stopPropagation(); - } - var descriptors = vm.ctx.actionsApi.getActionDescriptors('rowClick'); - if (descriptors.length) { - var entityId = vm.ctx.activeEntityInfo.entityId; - var entityName = vm.ctx.activeEntityInfo.entityName; - var entityLabel = vm.ctx.activeEntityInfo.entityLabel; - vm.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName, row, entityLabel); - } - } - - function onActionButtonClick($event, row, actionDescriptor) { - if ($event) { - $event.stopPropagation(); - } - var entityId = vm.ctx.activeEntityInfo.entityId; - var entityName = vm.ctx.activeEntityInfo.entityName; - var entityLabel = vm.ctx.activeEntityInfo.entityLabel; - vm.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName, row, entityLabel); - } - - - vm.cellStyle = function(source, index, value) { - var style = {}; - if (index > 0) { - var styleInfo = source.ts.stylesInfo[index-1]; - if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { - try { - style = styleInfo.cellStyleFunction(value); - } catch (e) { - style = {}; - } - } - } - return style; - } - - vm.cellContent = function(source, index, row, value) { - if (index === 0) { - return $filter('date')(value, dateFormatFilter); - } else { - var strContent = ''; - if (angular.isDefined(value)) { - strContent = ''+value; - } - var content = strContent; - var contentInfo = source.ts.contentsInfo[index-1]; - if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { - try { - var rowData = source.ts.rowDataTemplate; - rowData['Timestamp'] = row[0]; - for (var h=0; h < source.ts.header.length; h++) { - var headerInfo = source.ts.header[h]; - rowData[headerInfo.dataKey.name] = row[headerInfo.index]; - } - content = contentInfo.cellContentFunction(value, rowData, $filter); - } catch (e) { - content = strContent; - } - } else { - var decimals = (contentInfo.decimals || contentInfo.decimals === 0) ? contentInfo.decimals : vm.widgetConfig.decimals; - var units = contentInfo.units || vm.widgetConfig.units; - content = vm.ctx.utils.formatValue(value, decimals, units, true); - } - return content; - } - } - - $scope.$watch('vm.sourceIndex', function(newIndex, oldIndex) { - if (newIndex != oldIndex) { - updateSourceData(vm.sources[vm.sourceIndex]); - updateActiveEntityInfo(); - } - }); - - function updateActiveEntityInfo() { - var source = vm.sources[vm.sourceIndex]; - var activeEntityInfo = null; - if (source) { - var datasource = source.datasource; - if (datasource.type === types.datasourceType.entity && - datasource.entityType && datasource.entityId) { - activeEntityInfo = { - entityId: { - entityType: datasource.entityType, - id: datasource.entityId - }, - entityName: datasource.entityName - }; - } - } - vm.ctx.activeEntityInfo = activeEntityInfo; - } - - function updateDatasources() { - vm.sources = []; - vm.sourceIndex = 0; - var keyOffset = 0; - if (vm.datasources) { - for (var ds = 0; ds < vm.datasources.length; ds++) { - var source = {}; - var datasource = vm.datasources[ds]; - source.keyStartIndex = keyOffset; - keyOffset += datasource.dataKeys.length; - source.keyEndIndex = keyOffset; - source.datasource = datasource; - source.data = []; - source.rawData = []; - source.query = { - limit: vm.settings.defaultPageSize || 10, - page: 1, - order: '-0' - } - source.ts = { - header: [], - count: 0, - data: [], - stylesInfo: [], - contentsInfo: [], - rowDataTemplate: {} - } - source.ts.rowDataTemplate['Timestamp'] = null; - for (var a = 0; a < datasource.dataKeys.length; a++ ) { - var dataKey = datasource.dataKeys[a]; - var keySettings = dataKey.settings; - source.ts.header.push({ - index: a+1, - dataKey: dataKey - }); - source.ts.rowDataTemplate[dataKey.label] = null; - - var cellStyleFunction = null; - var useCellStyleFunction = false; - - if (keySettings.useCellStyleFunction === true) { - if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) { - try { - cellStyleFunction = new Function('value', keySettings.cellStyleFunction); - useCellStyleFunction = true; - } catch (e) { - cellStyleFunction = null; - useCellStyleFunction = false; - } - } - } - - source.ts.stylesInfo.push({ - useCellStyleFunction: useCellStyleFunction, - cellStyleFunction: cellStyleFunction - }); - - var cellContentFunction = null; - var useCellContentFunction = false; - - if (keySettings.useCellContentFunction === true) { - if (angular.isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) { - try { - cellContentFunction = new Function('value, rowData, filter', keySettings.cellContentFunction); - useCellContentFunction = true; - } catch (e) { - cellContentFunction = null; - useCellContentFunction = false; - } - } - } - - source.ts.contentsInfo.push({ - useCellContentFunction: useCellContentFunction, - cellContentFunction: cellContentFunction, - units: dataKey.units, - decimals: dataKey.decimals - }); - - } - vm.sources.push(source); - } - } - updateActiveEntityInfo(); - } - - function updatePage(source) { - var startIndex = source.query.limit * (source.query.page - 1); - source.ts.data = source.data.slice(startIndex, startIndex + source.query.limit); - } - - function reorder(source) { - let searchRegExp = new RegExp(vm.query.search); - - source.data = $filter('orderBy')(source.data, source.query.order); - if (vm.query.search !== null) { - source.data = source.data.filter(function(item){ - for (let i = 0; i < item.length; i++) { - if (vm.showTimestamp) { - if (i === 0) { - if (searchRegExp.test($filter('date')(item[i], dateFormatFilter))) { - return true; - } - } else { - if (searchRegExp.test(item[i])) { - return true; - } - } - } else { - if (searchRegExp.test(item[i])) { - return true; - } - } - } - }); - } - } - - function convertData(data) { - var rowsMap = {}; - for (var d = 0; d < data.length; d++) { - var columnData = data[d].data; - for (var i = 0; i < columnData.length; i++) { - var cellData = columnData[i]; - var timestamp = cellData[0]; - var row = rowsMap[timestamp]; - if (!row) { - row = []; - row[0] = timestamp; - for (var c = 0; c < data.length; c++) { - row[c+1] = undefined; - } - rowsMap[timestamp] = row; - } - row[d+1] = cellData[1]; - } - } - var rows = []; - for (var t in rowsMap) { - if (vm.settings.hideEmptyLines) - { - var hideLine = true; - for (var _c = 0; (_c < data.length) && hideLine; _c++) { - if (rowsMap[t][_c+1]) - hideLine = false; - } - if (!hideLine) - rows.push(rowsMap[t]); - } - else - rows.push(rowsMap[t]); - } - return rows; - } - - function updateSourceData(source) { - source.data = convertData(source.rawData); - source.ts.count = source.data.length; - reorder(source); - updatePage(source); - } - -} diff --git a/ui/src/app/widget/lib/timeseries-table-widget.scss b/ui/src/app/widget/lib/timeseries-table-widget.scss deleted file mode 100644 index 9e8ed9ea3a..0000000000 --- a/ui/src/app/widget/lib/timeseries-table-widget.scss +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -tb-timeseries-table-widget { - table.md-table thead.md-head > tr.md-row { - height: 40px; - } - - table.md-table tbody.md-body > tr.md-row, - table.md-table tfoot.md-foot > tr.md-row { - height: 38px; - } - - .md-table-pagination > * { - height: 46px; - } - - .tb-data-table md-toolbar { - z-index: 10; - } - - md-tabs:not(.md-no-tab-content):not(.md-dynamic-height) { - min-height: 0; - } -} diff --git a/ui/src/app/widget/lib/timeseries-table-widget.tpl.html b/ui/src/app/widget/lib/timeseries-table-widget.tpl.html deleted file mode 100644 index 1a7194b3f9..0000000000 --- a/ui/src/app/widget/lib/timeseries-table-widget.tpl.html +++ /dev/null @@ -1,104 +0,0 @@ - -
    -
    - -
    - - search - - {{'entity.search' | translate}} - - - - - - - - close - - {{ 'action.close' | translate }} - - -
    -
    - - - -
    - - - - - - - - - - - - - - - -
    - Timestamp - - {{ h.dataKey.label }} -
    - - {{actionDescriptor.icon}} - - {{ actionDescriptor.displayName }} - - -
    - - widget.no-data-found -
    -
    -
    -
    - - -
    -
    diff --git a/ui/src/app/widget/lib/tripAnimation/trip-animation-widget.js b/ui/src/app/widget/lib/tripAnimation/trip-animation-widget.js deleted file mode 100644 index b8aeda735b..0000000000 --- a/ui/src/app/widget/lib/tripAnimation/trip-animation-widget.js +++ /dev/null @@ -1,985 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './trip-animation-widget.scss'; -import template from "./trip-animation-widget.tpl.html"; -import TbOpenStreetMap from '../openstreet-map'; -import L from 'leaflet'; -import 'leaflet-polylinedecorator' -import tinycolor from "tinycolor2"; -import {fillPatternWithActions, isNumber, padValue, processPattern} from "../widget-utils"; - -(function () { - // save these original methods before they are overwritten - var proto_initIcon = L.Marker.prototype._initIcon; - var proto_setPos = L.Marker.prototype._setPos; - var oldIE = (L.DomUtil.TRANSFORM === 'msTransform'); - - L.Marker.addInitHook(function () { - var iconOptions = this.options.icon && this.options.icon.options; - var iconAnchor = iconOptions && this.options.icon.options.iconAnchor; - if (iconAnchor) { - iconAnchor = (iconAnchor[0] + 'px ' + iconAnchor[1] + 'px'); - } - this.options.rotationOrigin = this.options.rotationOrigin || iconAnchor || 'center bottom'; - this.options.rotationAngle = this.options.rotationAngle || 0; - - // Ensure marker keeps rotated during dragging - this.on('drag', function (e) { - e.target._applyRotation(); - }); - }); - - L.Marker.include({ - _initIcon: function () { - proto_initIcon.call(this); - }, - - _setPos: function (pos) { - proto_setPos.call(this, pos); - this._applyRotation(); - }, - - _applyRotation: function () { - if (this.options.rotationAngle) { - this._icon.style[L.DomUtil.TRANSFORM + 'Origin'] = this.options.rotationOrigin; - - if (oldIE) { - // for IE 9, use the 2D rotation - this._icon.style[L.DomUtil.TRANSFORM] = 'rotate(' + this.options.rotationAngle + 'deg)'; - } else { - // for modern browsers, prefer the 3D accelerated version - let rotation = ' rotateZ(' + this.options.rotationAngle + 'deg)'; - if (!this._icon.style[L.DomUtil.TRANSFORM].includes(rotation)) { - this._icon.style[L.DomUtil.TRANSFORM] += rotation; - } - } - } - }, - - setRotationAngle: function (angle) { - this.options.rotationAngle = angle; - this.update(); - return this; - }, - - setRotationOrigin: function (origin) { - this.options.rotationOrigin = origin; - this.update(); - return this; - } - }); -})(); - - -export default angular.module('thingsboard.widgets.tripAnimation', []) - .directive('tripAnimation', tripAnimationWidget) - .filter('tripAnimation', function ($filter) { - return function (label) { - label = label.toString(); - - let translateSelector = "widgets.tripAnimation." + label; - let translation = $filter('translate')(translateSelector); - - if (translation !== translateSelector) { - return translation; - } - - return label; - } - }) - .name; - - -/*@ngInject*/ -function tripAnimationWidget() { - return { - restrict: "E", - scope: true, - bindToController: { - ctx: '=', - self: '=' - }, - controller: tripAnimationController, - controllerAs: 'vm', - templateUrl: template - }; -} - -/*@ngInject*/ -function tripAnimationController($document, $scope, $log, $http, $timeout, $filter, $sce) { - let vm = this; - - vm.initBounds = true; - - vm.markers = []; - vm.index = 0; - vm.dsIndex = 0; - vm.minTime = 0; - vm.minTimeIndex = 0; - vm.maxTimeIndex = 0; - vm.isPlaying = false; - vm.trackingLine = { - "type": "FeatureCollection", - features: [] - }; - vm.speeds = [1, 5, 10, 25]; - vm.speed = 1; - vm.trips = []; - vm.activeTripIndex = 0; - - vm.showHideTooltip = showHideTooltip; - vm.recalculateTrips = recalculateTrips; - - $scope.$watch('vm.ctx', function () { - if (vm.ctx) { - vm.utils = vm.ctx.$scope.$injector.get('utils'); - vm.settings = vm.ctx.settings; - vm.widgetConfig = vm.ctx.widgetConfig; - vm.data = vm.ctx.data; - vm.datasources = vm.ctx.datasources; - configureStaticSettings(); - initialize(); - initializeCallbacks(); - } - }); - - - function initializeCallbacks() { - vm.self.onDataUpdated = function () { - createUpdatePath(true); - }; - - vm.self.onResize = function () { - resize(); - }; - - vm.self.typeParameters = function () { - return { - maxDatasources: 1, // Maximum allowed datasources for this widget, -1 - unlimited - maxDataKeys: -1 //Maximum allowed data keys for this widget, -1 - unlimited - } - }; - return true; - } - - - function resize() { - if (vm.map) { - vm.map.invalidateSize(); - } - } - - function initCallback() { - //createUpdatePath(); - //resize(); - } - - vm.playMove = function (play) { - if (play && vm.isPlaying) return; - if (play || vm.isPlaying) vm.isPlaying = true; - if (vm.isPlaying) { - moveInc(1); - vm.timeout = $timeout(function () { - vm.playMove(); - }, 1000 / vm.speed) - } - }; - - vm.moveNext = function () { - vm.stopPlay(); - if (vm.staticSettings.usePointAsAnchor) { - let newIndex = vm.maxTimeIndex; - for (let index = vm.index + 1; index < vm.maxTimeIndex; index++) { - if (vm.trips.some(function (trip) { - return calculateCurrentDate(trip.timeRange, index).hasAnchor; - })) { - newIndex = index; - break; - } - } - moveToIndex(newIndex); - } else moveInc(1); - }; - - vm.movePrev = function () { - vm.stopPlay(); - if (vm.staticSettings.usePointAsAnchor) { - let newIndex = vm.minTimeIndex; - for (let index = vm.index - 1; index > vm.minTimeIndex; index--) { - if (vm.trips.some(function (trip) { - return calculateCurrentDate(trip.timeRange, index).hasAnchor; - })) { - newIndex = index; - break; - } - } - moveToIndex(newIndex); - } else moveInc(-1); - }; - - vm.moveStart = function () { - vm.stopPlay(); - moveToIndex(vm.minTimeIndex); - }; - - vm.moveEnd = function () { - vm.stopPlay(); - moveToIndex(vm.maxTimeIndex); - }; - - vm.stopPlay = function () { - if (vm.isPlaying) { - vm.isPlaying = false; - $timeout.cancel(vm.timeout); - } - }; - - function moveInc(inc) { - let newIndex = vm.index + inc; - moveToIndex(newIndex); - } - - function moveToIndex(newIndex) { - if (newIndex > vm.maxTimeIndex || newIndex < vm.minTimeIndex) return; - vm.index = newIndex; - vm.animationTime = vm.minTime + vm.index * vm.staticSettings.normalizationStep; - recalculateTrips(); - } - - function recalculateTrips() { - vm.trips.forEach(function (value) { - moveMarker(value); - }) - } - - function initialize() { - $scope.currentDate = $filter('date')(0, "yyyy.MM.dd HH:mm:ss"); - - vm.self.actionSources = [vm.searchAction]; - vm.endpoint = vm.ctx.settings.endpointUrl; - $scope.title = vm.ctx.widgetConfig.title; - vm.utils = vm.self.ctx.$scope.$injector.get('utils'); - - vm.showTimestamp = vm.settings.showTimestamp !== false; - vm.ctx.$element = angular.element("#trip-animation-map", vm.ctx.$container); - vm.defaultZoomLevel = 2; - if (vm.ctx.settings.defaultZoomLevel) { - if (vm.ctx.settings.defaultZoomLevel > 0 && vm.ctx.settings.defaultZoomLevel < 21) { - vm.defaultZoomLevel = Math.floor(vm.ctx.settings.defaultZoomLevel); - } - } - vm.dontFitMapBounds = vm.ctx.settings.fitMapBounds === false; - vm.map = new TbOpenStreetMap(vm.ctx.$element, vm.utils, initCallback, vm.defaultZoomLevel, vm.dontFitMapBounds, vm.staticSettings.disableScrollZooming, null, vm.staticSettings.mapProvider); - vm.map.bounds = vm.map.createBounds(); - vm.map.invalidateSize(true); - vm.map.bounds = vm.map.createBounds(); - - vm.tooltipActionsMap = {}; - var descriptors = vm.ctx.actionsApi.getActionDescriptors('tooltipAction'); - descriptors.forEach(function (descriptor) { - if (descriptor) vm.tooltipActionsMap[descriptor.name] = descriptor; - }); - } - - function configureStaticSettings() { - let staticSettings = {}; - vm.staticSettings = staticSettings; - //Calculate General Settings - staticSettings.normalizationStep = vm.ctx.settings.normalizationStep || 1000; - staticSettings.buttonColor = tinycolor(vm.widgetConfig.color).setAlpha(0.54).toRgbString(); - staticSettings.disabledButtonColor = tinycolor(vm.widgetConfig.color).setAlpha(0.3).toRgbString(); - staticSettings.polygonColor = tinycolor(vm.ctx.settings.polygonColor).toHexString(); - staticSettings.polygonStrokeColor = tinycolor(vm.ctx.settings.polygonStrokeColor).toHexString(); - staticSettings.mapProvider = vm.ctx.settings.mapProvider ? {name: vm.ctx.settings.mapProvider} : {name: "OpenStreetMap.Mapnik"}; - staticSettings.disableScrollZooming = vm.ctx.settings.disableScrollZooming || false; - staticSettings.latKeyName = vm.ctx.settings.latKeyName || "latitude"; - staticSettings.lngKeyName = vm.ctx.settings.lngKeyName || "longitude"; - staticSettings.polKeyName = vm.ctx.settings.polKeyName || "coordinates"; - staticSettings.rotationAngle = vm.ctx.settings.rotationAngle || 0; - staticSettings.polygonOpacity = vm.ctx.settings.polygonOpacity || 0.5; - staticSettings.polygonStrokeOpacity = vm.ctx.settings.polygonStrokeOpacity || 1; - staticSettings.polygonStrokeWeight = vm.ctx.settings.polygonStrokeWeight || 1; - staticSettings.showPolygon = vm.ctx.settings.showPolygon || false; - staticSettings.usePolygonColorFunction = vm.ctx.settings.usePolygonColorFunction || false; - staticSettings.usePolygonTooltipFunction = vm.ctx.settings.usePolygonTooltipFunction || false; - staticSettings.displayTooltip = vm.ctx.settings.showTooltip || false; - staticSettings.defaultZoomLevel = vm.ctx.settings.defaultZoomLevel || true; - staticSettings.showTooltip = false; - staticSettings.label = vm.ctx.settings.label || "${entityName}"; - staticSettings.useLabelFunction = vm.ctx.settings.useLabelFunction || false; - staticSettings.autocloseTooltip = vm.ctx.settings.autocloseTooltip || false; - staticSettings.pointTooltipOnRightPanel = vm.ctx.settings.pointTooltipOnRightPanel || false; - staticSettings.usePointAsAnchor = vm.ctx.settings.usePointAsAnchor || false; - staticSettings.showLabel = vm.ctx.settings.showLabel || false; - staticSettings.useTooltipFunction = vm.ctx.settings.useTooltipFunction || false; - staticSettings.usePolylineDecorator = vm.ctx.settings.usePolylineDecorator || false; - staticSettings.useDecoratorCustomColor = vm.ctx.settings.useDecoratorCustomColor || false; - staticSettings.decoratorCustomColor = tinycolor(vm.ctx.settings.decoratorCustomColor).toHexString(); - staticSettings.decoratorSymbol = vm.ctx.settings.decoratorSymbol || "arrowHead"; - staticSettings.decoratorSymbolSize = vm.ctx.settings.decoratorSymbolSize || 10; - staticSettings.decoratorOffset = vm.ctx.settings.decoratorOffset || "20px"; - staticSettings.endDecoratorOffset = vm.ctx.settings.endDecoratorOffset || "20px"; - staticSettings.decoratorRepeat = vm.ctx.settings.decoratorRepeat || "20px"; - staticSettings.tooltipPattern = vm.ctx.settings.tooltipPattern || "${entityName}\n" + - "
    \n" + - "Time: ${formattedTs}\n" + - "Latitude: ${latitude:7}\n" + - "Longitude: ${longitude:7}"; - staticSettings.polygonTooltipPattern = vm.ctx.settings.polygonTooltipPattern || "${entityName}\n" + - "
    \n" + - "Time: ${formattedTs}\n"; - staticSettings.tooltipOpacity = angular.isNumber(vm.ctx.settings.tooltipOpacity) ? vm.ctx.settings.tooltipOpacity : 1; - staticSettings.tooltipColor = vm.ctx.settings.tooltipColor ? tinycolor(vm.ctx.settings.tooltipColor).toRgbString() : "#ffffff"; - staticSettings.tooltipFontColor = vm.ctx.settings.tooltipFontColor ? tinycolor(vm.ctx.settings.tooltipFontColor).toRgbString() : "#000000"; - staticSettings.pathColor = vm.ctx.settings.color ? tinycolor(vm.ctx.settings.color).toHexString() : "#ff6300"; - staticSettings.pathWeight = vm.ctx.settings.strokeWeight || 1; - staticSettings.pathOpacity = vm.ctx.settings.strokeOpacity || 1; - staticSettings.usePathColorFunction = vm.ctx.settings.useColorFunction || false; - staticSettings.showPoints = vm.ctx.settings.showPoints || false; - staticSettings.pointSize = vm.ctx.settings.pointSize || 1; - staticSettings.markerImageSize = vm.ctx.settings.markerImageSize || 20; - staticSettings.useMarkerImageFunction = vm.ctx.settings.useMarkerImageFunction || false; - staticSettings.pointColor = vm.ctx.settings.pointColor ? tinycolor(vm.ctx.settings.pointColor).toHexString() : "#ff6300"; - staticSettings.markerImages = vm.ctx.settings.markerImages || []; - staticSettings.icon = L.icon({ - iconUrl: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKoAAACqCAYAAAA9dtSCAAAAhnpUWHRSYXcgcHJvZmlsZSB0eXBlIGV4aWYAAHjadY7LDcAwCEPvTNERCBA+40RVI3WDjl+iNMqp7wCWBZbheu4Ox6AggVRzDVVMJCSopXCcMGIhLGPnnHybSyraNjBNoeGGsg/l8xeV1bWbmGnVU0/KdLqY2HPmH4xUHDVih7S2Gv34q8ULVzos2Vmq5r4AAAoGaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/Pgo8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA0LjQuMC1FeGl2MiI+CiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICB4bWxuczpleGlmPSJodHRwOi8vbnMuYWRvYmUuY29tL2V4aWYvMS4wLyIKICAgIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIgogICBleGlmOlBpeGVsWERpbWVuc2lvbj0iMTcwIgogICBleGlmOlBpeGVsWURpbWVuc2lvbj0iMTcwIgogICB0aWZmOkltYWdlV2lkdGg9IjE3MCIKICAgdGlmZjpJbWFnZUhlaWdodD0iMTcwIgogICB0aWZmOk9yaWVudGF0aW9uPSIxIi8+CiA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz7hlLlNAAAABHNCSVQICAgIfAhkiAAAIABJREFUeNrtnXmcXFWZ97/Pubeq1+wJSQghhHQ2FUFlFVdUlEXAAVFBQZh3cAy4ESGbCBHIQiTKxHUW4FVwGcAFFMRhkWEZfZkBZJQkTRASIGHJQpbequ49z/vHre7cqrpVXd1da3edz6c+XX1rv+d3f+f3/M5zniPUW87WulAnTnN5i8IBYpnsttKoSZoVpitMFssoIIbSqIILxFIvVVGsQjdCQoSEKvtweMkIrwh0+gm6fMtWA7s2XicP1s92/ib1U7C/TfmCThrXyvm+5TgnzgwRWtUyDksL0GJigIJq8LevaWFnWMz+YzYJCHsRusSwHej2etiA8HT7Klld7406UAFou1yPdVyOclxmWZ8TRDnMaQT1AQtqU4AsBIxFOPsigAnAbBzwulAV/kdiPGK7+RuWJzaukUfrQB0hbfYSXRlz+TDKAWo50LiIpoBZEjAOoWckBVybRMXwCg6bvSSPta+UhXWgDrM293I912niSJvgTLeJ6eoFrJk1fNdAT4mAOCAueF28JA6/tsp/bbxWbq0DtUbb/EV6tTRwuvWY5bg0q59izeHScSYArfXpFHjB8/hd++rhy7TDCqjzlupFOJxshNNFQL0aZM7BNAPGDS5Ea7kf5ZcbVsh360CtsjZniV7qOFyIMscYYtYbwdGxC+qhEuOvtosbN6yRb9WBWsE26/M6PT6Bi9VykRNnnCaH19BeDGlg4uD3sAfhRr+Lm9vXyp/rQC1ngLRElxuHCwSmo3WA5u3glO0FvOz73LZxpXylDtTSa9ArUL7gNDDJJkaA9ixyT5sY2CR71GXdhm/I1+pALXKbvUSPcR1+KMrhUGfQoQZeKZZ9xvNY3r5C/r0O1CG2Q7+ih8UbucaJc5r6qVmjeiuuhk3wB7+Hq9u/KQ/UgTqYSH6xLnDiLBHlIPXqwCoZABwAdvvKDzdeK4vqQC00ml+o74g3scqJ8UG/p65Dy6pffdYrfG3DNfKLOlDzB0vnAd82hnH1Yb4CYAgmDfZ6Pt97dpUsrgM1CqSL9U4nzketV2fRSiPCaQCb5DGviy+3Xy+P14EKzPyqvrWxkR8JHF5n0erSrtbnDQsXtK+UX1WBWVFBFl2klzY28l91kFZfUx/EMNZ1+eX8ZfqvI5ZR5y3V60S5VAxO3RetbikgDvge921cKR8aMUCdgTY2f50bjeFT9dml2mmp7KxNPQk+8dwaeWJYA3XM+Tp22sHcCby77o3WpiuA8LyX5JL2lXL3sATqmy/Vw/0mfua4zLOJeqfXcpClSg8+X16/Sn4wrIA6c6HOaGjgYWOYXg+ahgFYDSh0+B5fb18ta4dF1D97iR7TGOePxqmDdNg4AhZEaXHjXD9vsZZl2rWkjDpzoc5ojPNHcZhSB+nwdASApK98vX2lrKpJRp19mR7REOcRcesgHb7UCkDMjbFy9mJdWHNAPfgcHefG+Llx6plPIwGs6oFruHruIv1C7QD1Q9rScih3mRhz6kw6csAqQpOJsXbOUj27JoA67zhuEuH4ugU1IgMs13G5fvYifU9VA3XuIr3ecfh4fbgf0WA9yI1zc9UCde5Svcw4fNl21zusigKdsk9RqwcCM+cv0UeKbzAMNcL/qh7jNvKQKA31BJMKAbIiPZ/n7V2wPndsWCFnVQmjaizWxL8IdZCWDZjhWz9PK+h9SsSsjsuZcxbr/6kKRp23VO82DifVdWllmFMHwaoiZWJYAYR9fpIz2q+T+yvGqLOX6medWB2kZWHQ8CHdf2Mgt4zXD4yCB/fdBVqdGEMu2DZooM5ZrEe5wlqbrGOpHABNA2cuCdAPQCPfs1iaN48TYGLMnb9kaKsEBg1Ux+VbIoyrJz6XB6D5gKi6v5R7rltOAOdi1yI22wMI581dop8vK1DnLtbLnBjH12eeShAgZQK0AHAWMuynsWeEdaUlJhwRYo7DFWUD6vzL9VjjstDvqeOrlPozckhnP0D7HrMFANWmAzYnWEssAcQwdd5SvbUsQCXGchEm14f8EutPMtgwDNDe4zbErDbi/xyyoY9doy6IEjabBGM4c95i/VRJgTp7kZ5iYpxYj/KLBFAigGQzwJl53EaDtk9v2pAzpMGNXolgye2fankkANDAICRA4e7ZRRqbP4mnBebVjf1BADTz3xxDsBId7IjsB6HmYUBV6PRhaweQTPVwHKa3QJOb8lBDtz5PVUJokBAwSuCvOo2Q6GJp+ypZWXRGnT+eq0XqIC16gNQLRiJYLxQsaYhpsxjWBgUjXu+EriScMEO5/0seO7/Xw+vfSfDgFz2Omqbs7AEbEWSRJ8gqRfN7wHG5+NBFeljRGXX+Mt2DMqquTQfBoLkAEAaepMAXAm7acBwGV+gxa2FnAt7ohCXvt5zxLp+j59v9gVaIjq7+qcsNDztMaKBvw7U+Zs1g1TSmLYULEGyK8eP1K+W8ogF13lL9thPjS/Uc00GAMwdb5Quc0t5Dyd5/VQOAvtQF05vg9MMtXz7d46DJGoDPy9HTDpx7fZyHXhBaYukyQDKH/hIDtfdCSexmzKZ1smfIQJ37FT3MaeVe9ZlaZ9Mh6E+JAGcOzam5hmcLnoUXOgKWXHuKz6nv9Jl9UIpm+/O1XfjLc4bDvhFj7vhU75t04JQNqClWxfLrZ66VM4asUZ1WLhTqIC04gs8VJNmQtrTpGjOsR9M2De7do9VCTwJe7IAJMeWGM3xev6GHr5ztMXuaBgxayOSLD9MnW2jcL4V7v59IDm1awn5XHxTe/+ZF+lH6v8b6eTPL57UO0jRmzDmjk3G/71+bgzEz9ahNj/pVIeHDi7vgiCnK6jN9Pni0z6RxClaih/j+WEygwQn9ltDvCGdV9T0spT2fxmG0H+PTwF2DBurcpbrKuDTUtWkejRkB0KzhPAKgaWC12c/fm4BOD46aqPzgfJ/3HuHT0pwCvTdI9BjYuVvo2QNmUuozK1wh13pgXM4GPjGooX/Ml3SsMZw9orOj+kuRy9SSmWZ8hknfZ0PZ0FAf2oLd+tDZA5tehaMPVH55scc9KxKc/E6floZUkDQUe1DgX37vMra1/xG9nPgVgbmL9ZZBAXVKMwtQZo5YbZprDr6QKD4jcaRXo6pNacmI+zu6oCsBRx6kPLkyyS++nuADR/nEnRRAh9oPDjzzvPCTxw0T4tV1qm2wGuB98y/Vtw146BfhNJERVr40XwRfqMWUaStFBVSpm29hdwJ27oUFx1s++xGfo+bZvsAHr7i/7aZ7Xd5IwiQ3B4NKRc/7NJo5GXiycKCepA1uA8f43SMYpPkAGiELMhlVyW0x+Qovd8DsFjj5cMuiT3ocNCmPBzrU5sJT7YZvPmRomxj6nqHxVCWVF2Aqev4vAK4tGKhzjuDGEbGVeCEeaC6AQpYRr1HmfeiW9GFzF8QsXPNhnzNP8Jk1zQYo8YeoP/OJTYVrfuYydUzEHL5UAaOmRhuniVkzLtW3bV4rT/YL1IkX6VTX5ehhnRSdI5ljwBF85nNs9tAvCj0ebOmEd05SLn6v5eIzPJqbU8/xSowOB+58xOHeF4QpzaQnn0Qwa0W7JQmNjVwJnNEvUCeM4yz1aBsR4ByIBo2ymMiO+sPP7/HhxT3QNhZuOsfntHf5jB+txdefedjU82D1r1zGxDIAGpo2lYgovOyhP4Hr4bgc3vYFnbVpnTyXF6gmzvtNKhIbUUN8Ln80l0kf9kAJ2VHAvgS8koCTpiurzrJ88gMexqQeL+d5deDWe1we2wazx6a+s4kAYoWAGdVP6nNIbBR/B6zJC1T1ONXKCARoLv0p0UN6VnRvoSMJr+yAjx6mXPABn1OO84nHGbr/OZhmYMdu4Ya7DYeMCmRweIiXzGE/pWWlwjLAGLDKu/ICdc4iPcltJFbz0X4hETwR0XkOiykzMNKMpJLXuiAmcNocy4Vf9HnXYX5AW0Uf4m3hgtLALb9z2LBHmNYaETBJP4xaIbKyQWx0Wl6NKvC5mh3yhxIghSLPXGDNnPK0Fnb1wK7XYeHJljOO93nX4ak3KHqA1GvAFvi+Dry2Q/jyXQ4zR2Xo0ah0vioKqNBgBcCcJXpyeIugNKA6DcytufVQ+QDaX4AEheeBhkz75zpgZhP8wzGWBad5zDgw9WDRz53N+IEFokmUq2+NMTYGTig5Oi0puj9GrWCzHhjDRUA2UOdermepz4HDRn/2asvQWqO0/id/Hmjmfd8GFpPtgG9+3Ocjx1jePDMVSZUcoAMAqQtPPevwyz8bxjdGsKeka1IM6V5qFQBWfTAucyN/vWniMOMwuurnTAvNA7URw7nNON6bWmez80B7n5v04Lm9MDEOaz7qs/OmHhZ+0uPNh9jC80AL/mF+6qbZPlMh6EldlNff4dKtQWACQYAUNfSH0/qkygJo9Tho9mX7l1X3MaomOUqdGg+QNOOp/eWBRllMqeNJH7bshrGNcOunPd7zdstBk+yg80D7v/I0PwIL1KaPP2O45XGhbXIGADOCqKzhv0rYtPeUSIzWuOHgLKAKvBO/RgHajwYtKA80BerOBLyWgBOmKDec63PysSmLaSh5oDl/WKGeVWFsmkjAFTe7HDSB/ctMQktMcrGmSHX2u6+8F1idBlSniTF+Z40yaH9ZTOTIaArJgG4PXt4Fxx6s/PhTHke/2dLaTAkspoFW0C1Qmzpw76MOj74qTGkJsabJZtaqZtPwaGh5exqjzlusl1RNtD8UBu0HoH0gDUmCnSnP+H2HKEsu9ThyfonS7AbEoAPXpns74Iu3ukxozLG6NAc4RaoQpCnycJuZnHa5qnJkxQtLDCaTPqoMTub9zIVyPvg+vNEFm16Bk2db7v5qktu/lghA6lGcROU0ahhsalShbKrccp/LC3sJEq0zI/ywNiU3aKuu+dB2uZ7Vx6hOA2+pmD4daKrdQBk0dN8qbEnlgZ5yhGXhxz0OmZpKsyv6PLwdItoLD6Ce32ZY9zuHmaPJLtcTWvosUal9VC9g1YIjnADc7o4/V0cDLRVZadpfRZFCE0U0O2rvS7MDEh5s7YZkJyw/2XL2CR7zZqQAWvQAqVg1xk3BeP75Ay7rO6CtdT8wpR9zX2ogn0N9MHHeCuCOO5B3qs8BVQ/SXACNiu5TN8+HF/bB8ZODPNAFZ/i0NGsJ8kCLvcVI4Wy6eZuw5HbDrCnsz4yKAKfkGuqrGLAaJMmMAnAdw0QsLdUG0pzLPfph0L480L2BsLn5HJ8Tj/GZOlFLMLyXag+cwtn0+p+5jB8XXZVPckX2NZQdpz4T5izUo1wnxlRjaCjbsuhCQNrfco8IDSopD/TlLjjlEGXFmZZPn+jtl4sVj+CLDFIXHnzCcMvTqalSs7/omeRi0xrRphmnehQuB7omRpwKRfwDAmnofuaxTg+27YB3HKx873yP971NGd2i0bORVcmgg6E7Zd2dLrEQQMPaU2rMjspzukepMNm1SeaUTVhr4SDNXNaRWSdUCNbCJy2cNVf57AKP9xzhBz1QFR5oadn0rkddfvlXoW0S6eZ+ZqHeGhzu085IHIkrY1yxTCt3LmLO3TkiQBoubqsEeaB7ErD9NTjvnZYFp/q8Y57FdSlhHmg5mhT8tI4u+KffOEwfF/3SnKxZi2ANCKrBtTDKKBVCKv3P0YfK4mzphMkNcMHbLQvO8DikNw+0qAxa6uF96JH+bQ+43LdFaBtN3jS+rOK8NQjY1MTNdFdgTCWCqCytmiOC9yxs74K9b8DXTrF8+oM+cw8uZx5ouUBa2FRpMgGX/bvDjJZU0Yio1L2It6opbZqBGwvTXFUmVpDSs4EcKvm9aR8cNU656F2Wi0/zGdVcSxZTCbSpAyt/FGO7hbEZEb5kAFpqZZq0kLOjtLrAmHL2keZi01Amfo8Hm/fC9//O49TjKpkHWl0g/dvLws//ZDikaT+bZpFyPjat0abQ6EoBxXxLyqQZx5MWxjTA35YmmDlVK5wHWl1B1Lf/3WVzB0xpyWPw51MStRpMBTsPVc2XQYDnd8FDlyaZOdVW+TRnee2o514S1v3BcOiUbOBlDvsow2LID3VdY0WB2hfpp4b9Tg8+dZhyzJss+FJEgFbr5liFr4O69Icxpo7P/Qsj1z9JDQdRGZeqW94LI6KfQlOg3RbmTbU4Eiyqq80IvrhDvuvAfY8bHnhOmNQcKg2Zq2iEpvQrGfdrlUwDfLhuNX0pA3T7sGNfEPXXd2KBmAvPbRMSNg+2q6DCSckptdwcErnVdmoPpgYDL7welLl2JFUvaUiwr0ZdmnN8yckoJx/v85M/OLywT4i7RFt7IW06nLAqAQ48U+kv0XuiRaDZhZ/+1fDU84bGeLEuDRN4O1VTs2ZgYLUKY5ph6bkeL+6KYE4NFTzrHe5DBDBMQOtVR+/J/m47ZBR8/TaX13ZDU0MxPUCpQsAWxqrdCTh6nuXEucreRIaJkUv7Z4K2lmWU0G20vBU7c2eap05w3IFtHcJblzdwx8MO+7qFxpIBtgoKgg5AAnztk14wFObYoSVqNUQaRrUmQQrQZYDdlewvidg7vsmBqY3wj7e5/P0/xbjlPgcxSlPD/jI1xZMFlWbYwqwzz4fDZvmcNM+yJ5ED5/1tE1mjOkCg24iwvSJTbPlmTySoQjejBTbvES75lcsBFzfyvbtcXtkZMKxTVMBWmmELozrPE5adm2R7935WTUviyShhhA7qY6oRqfucie++6jMiTCv5j5ACgioy7EEBIzAhDuMa4M71hgefMOzYYXjTDGVsaxBsFGcFbSWzOAorUKoKo5sh3i38/lnD6Fjwst5zJVFZ/qGTWovmvwT7tv7BGNhb9i8e5ftF/BX2E50xML0Vunzh+48Y2i6Js+ymGO0vGmKxwG8sriRwytyjhUmApA+fOdHnTeMUL1RsI22TYO0nkKohZpWg7190Jr73qg+IcHg5Zxmj9jqSHI+Hk38l9X+zC+Nb4bEtwq8fd3h5i2H6ZGXaxFAZypLqk1K1wlh1bCvYhPCzpw3j4xmsmqH9s5Kna4xVJSCg3xkTo71sGwzkmI8mPHT1nmCTflzCx1O3A5qh0YVfPmM4bmmc89bE+X/PGHylSD5suRm2MLbo7oHPnOhx+ISguFvmVpbkWFZei80m0ESC3cYmSVTqR+QFK+kFaMMgFbP/vnFgVAO0TQ0Y9uwfxPjSd2Lc96RDU6MScwOdWzzGK6UXWxiiep9x+WkeryTS91jVPHsU5DtetdaUYa8orxo/yTabpKci+rQAZs1i1NCKSwkvFTYwuhEmN8FjLxrO+o7LsYsauPuPgRfb3FgrXmzhdtWJR/ucMcvSldoeKLy9eppWzbgGaspXFfbisdVY5XUMHeXdoL0wsPYBtpdJQ/9LmGGdEHgdaIrD7Amwr0f4zI9d/uGGGDff62IMNMRrwYstgFVT+RALTvPZuivipREbZQyAtKsp6t/Rfr08buzz/FEcXqsIrecCa56s9TQ5EAoacNLBqwYaYtA2Cp7dJXzxDodJX2jgxrtdXn9DaKpqL7YwVk0k4W1tlgXvsWzvZH+Jd0vezYVrxQGQIBd3L4D5222yG+iomOkfBdYMwPaB02QcM+mPp/0f0rFxF9pGB1vuLLzT4fxvxbjuZzH2dAtNcXCcUjCslJxVIaj1es77fcbEU6mRNkKrRthTWgOsKgasx9N9fojfw18qNpMo2W6A5NhdTsgB2FygDoEVBxwX2sbA9h5h3SOGWV+Kc+WPYjz3shCPQ6yqAFu4Vj1ituWcYyzbutLBmetv1qVQpaAVB/B5oI/T5i3WS5wG1tlEFXy7ge5+EqHDBlLY9/VOOKAJ3jPLcvHpHrOnKUkv2FequKwzmExwKUj/GgN7O2HW4gZmNgcXZNaoE958ArLyK6rRV3Wa4S9XSN+8DxtWyXekWrbuiWBYovRrmGEzCCxNBmR4r2nWloHJqUIO92wwHLkszvnXx3ny2QAcDbFi9t9grK3CgG0tjBsF3z/T47WuCLtKcwdS1cqqYsDr4tXw2QuGkG72VNX674xSNJGSIGO4TwNtoYB1Ai+2tRHaJsGjm4VPfC/GBWvj/GmDwXUpshc70LzYwiXAh4/2ees4xfOJLjqXY6q1KlvQb09kARXhUapxQ7QBaNi0gCyDYbPA64Qsr9BzxzQGCTD/+6rwkTUxPnRFnAeeMHT0CM0VyYstnFUnjoavfMxn8xukZVRpf/mq1TgJIOA4PJQFVHF5vKorahQC2DDDEu27Skin9bkDmY85wbA/eyK81il89qYY562JccfDDr4GLoJTVi+2MFbtSsB73+rzoVlKRyKHl1oLCSsC6rPPT7AlC6i2h7+oz56qT1bIBdh8myqEY5IMSZCpW8OTB2qCnIEpLbBln3DBj1wOvbSBnz/osG2n0NqkRZ48yMWwBU6tarC8euEZHj02VL7TZgeVmcFptdlV4vLShlXy0yygblwlt+GwlVppmYAlArDh4Z50cGZpWzIAm+HHxlxoGwtTGuHLd7icc32ctbfH2NspxGMBQErLsAWmAXpw9HzLBzNZlYj7uRi0wqAVB2yS9kzJut88TrBRHGqr5QMsRG5WK1FgzvRmnQgv1gQlO6a3QoeFlfc5zPpinHW/cNmwxdDSVGzAOqRnbRWuV5d+MskrIQdAbcQkQD4rsJJxlAt+kn9Js6rC/0x691X7nDjnVM12k0UAbL+TBxkyIW1yLCoZJvV8IzA6DuOb4Z52w73/47DlJcOMKcqksYH1ZW2xf1xhRaWsBnaV2Sc8/LyhOR4aMcgIOsnI/620r5oqYbRxlZybeQbS2rylmhCI1XyVEo3WcFmP5UmLy9pHgBz+pA1AuTcJr++Az73PctqxPse+xWIUepLl//mOA6/uEs5aHadHg0kACedEZASa4dTK3OgoD5tay13rr5XTcg79qS93t3Go/Sa5Ay+J2pQhc8MGMnIIInRreLrWuIG11XYg3Pa04bx/jnHxt2M8/BdDQzzQuOV0VXwfDj5A+ex7fJ7vzNakCumrVntr1FaYoGwgUR6NslUzWed+6zO8Wq5tbCJuWfkEmZWdw5o2c/Ig9fzxTTC2Af5zs+G0tTHOXhXnnj85eJYgL7ZMP7srtRJg3mhI+BEGQrVZU8F53ZzYxy+yRojMA+OOuGprrJnTVRkPwxuw+QKvKI2btcqT7H1HwytCY06gYbfuFm59yvDE0w7WE950iO0LukrNYA0xmNgIv/hfQ2ssu5R62grWCq+tMg5Yy8PPflO+2y+jblonL3k9/Lc4DN8mEfjNMXkQmcQdISH6ZrucdJsLA80NcEgLbNol/ONPHY5b2sAt/+GyuyNYeWBKCAbPh1OO8zl2qtLjkb9oRaVtqTgkLcsL6LJUu1Ib32Lo8rsZGW2ogVfETI+GCkJkmu0JD7Z0wzvGKae+zXLhSR4tTfRtNFwKVn3sGcMp62K0jUvX1mlBVYi6ys2oYkCV59evkEMj2TbyVcul2+vh8WHNqgPVsOQJvDLzYMnOiQ0zbDwWbFm+o1tY9aDD/MsauOF2l82vCs2NWsTp2aD1JOGYeZbzjwiVA8oo/yNK7pJA5dKnws05ZUHO1/ncqSOtkG7E5raRU7S5Aq+MGbC0mS+TvfLAjcH0liDV8Nv/6fCptXEW/iDO1p2C4xSzqEbQLviQT4MTrYuVyoIUYWuPz29z2m25Hph4xFV/Nc2cjTKWkdhyMSwMaPIgHLykJSyHcg+MQGsMPIU/bxPW/s5hx6uGyWNg2qSAYYc6eWAVZkyxbNlqeHKb9O3ekFWkrgIJ1RID3+O3z66SdQV2R3qbt0zXOC5frYrM/2rVsGTr1SwdG7F+KefKA4KJht0JcAXecaCy8GMebzrEEneDufzBjnSuA8+/Ihy1Is7c8Snr1EQDNW0lQBmA+sw3MCkBMrChH2DDtXKZeiSGa134oWrYnDkFOVyEyERuZ78sCN8f2wgtMXhqm3DCiiCR+8EngjTDwRY49i1MHa9MHg1+lWzxY1xQy+35QNovUFMn+ofGod4iNGx/1lbm6lgyVsmmadrwzFcqD6WvRsEkeOoV4e9vdvnEtXF+80eHhCeDyotVpLDXaHnOp1X2+Elu6RfQ/T0h0clNVnl1xLNqPyybL5Eb6T8vVjKcg8y82JY4TGgK8mLP/VeXj14T4/b/dNi1D1qbtSAv1jGwfTds3R0Ur0DyXIBSesCmSko+1L5afj1koG5aK09iud3E6tgcEGBzDf1EMGzGsu++93Ci82Jnj4Od3cLCO1w+fl2cdb+Ksacz0KD50gwb4sq/3e/2LWgMa9C+DStkIFHM0FvHTs4fRGybu81fpvtQWup7Pw1s2MyqsicEEwG9mxSHvctQtpYSHXhpaheU3mO+wvNd0OrCkvf7fPhIn/kHK109gSbtZdLGuHLrH1yu+I3L2DjpeQu56n+VEKgSZEn9dMO1ck5BgWDh/gbfFYfL1a/jsKDLP7S/a3jXl75xTFNACWcthf6XEEh7mVY1dbz3mAbx1+yWALCrHnC48TGHk95s+cz7fQ4cb1GE7Xvgxvtj/PzPhjHxDCAWEDiW6Pxs007WDtItzN0OvUjHxCfx3wba1NaxOBR7K9/CuvC28FkMG7K7sgz61H2rsMeD1ztgythAi768ByY3kw7SKL80qrByCUDrNEJPJ8ufXS1XFR2oAPMW65lOI7fbnjr2hgrWNMBGgC6rAG+uWlI58kjDOO/dBVEiAieRfqy1UgRQlk3PrJDZA7KxBvLkDavkDj/B/eLWcVcUayvPCtqo6dlwjYK0fIPQe4Xf06T2PtAcn5MXpKW7YBOe8o2BvmzA6Q++shxle92uKqJTQP/5BGnLu6McA0NkGXnppzByOSL7PrDFwCp3ta+UHw/mlA24zVmsV8Sb+MaISQOslIbtTxbkkRW5ejmytGeux4p5bQbM/tr6a2TyoEA+mBe1r5KrveQISgMsN8NGrHoNPzdrZYGJZuPMYV4KMfhLdS0qvk2yYtBsPNgX2i4uVa0OvmekAAAFN0lEQVTs9pTDHbBpOjIHGDOH/MxJg3xpijlBWuQ+NQ2A8JONq+WGoZyaQbf5y3SBcfnuiM+uqrQsKDThOV+F71IO+coL61fIzCGBfSgvXn+tfM9P8EDdBSgxyxItC6Sf+XrILQHKAVICW6zTT3LxkFl5qG+wewefUFhf16tlkAQRQIoEbmjZS+SK2gIkR7GifJRl7Wvk7iJfr4Nrcy7XD7hxfq8WU88FqKw0KF+v9/MRLqjH3etXyilFAX0x3qT9Ornf97myjpoKs22eNMNybp6dAunjxQJp0YAKsHGlXKPwfaexjp1q0rRl//hAAr68ew8nFVVGFPPNNqyQBdbj7npwNUKvkZSpn0jw+a3flR1VC1SA5D4uUsvTUk+0HpESxPos2bRa7goyDKoYqM9+S17u6OAj1uOluhMwouSG53ks3rhCbuRKNf0t1qs4UAG2fFu2+cpZ1rK9DtbhD1ITA2u5qn2VrC42k5ZFerddprNcl8eNYVw92Xr4Bm5quWrDKlnOlWq4CkVEawqoAHOX6PEi/MYYxtaXsQw/JvW6Wb6xL1NfpdhDflnNjLbLdFbM5U9imFBn1mGAUQELSfVYvvE6ubYcn1mWPaU3rZHnSHKKtWytW1c1DtLUigG1LCsXSMvGqL1t+iV6YOsY7hXDW2py55WRDtKggssryR4WP3ud/N9yfrYp54e9+B3Z2rOXUxTurc9g1RhIXVDhRT/JBeUGadkZNdzmL9MfolwUaPA6EKq5GRd8jz9teIr3co9UZA1yRWeG5y3WK43DVSmLo96qMbIPdtG7c8NKOb2iF0slP3zDKlnueZyMQ3s9yKpKPdrhWRZUGqQVZ9Te1vYFPcht5SdOnHfbZF0KVHyobwCboN3zuOTZ6+Q/qoTcq6fNXarLHMNioLU+OVABMATrm5IKP96wQv6+ylRIdbW2y/REt4Hr3BiH+z11di0bi8ZAfbb5HldvXC3fr0K5XJ1t7hJdaQyfM4Zxtu65llSLKnSrzx0bVsqnq/ZCqtYvtnGlLFHlE1Z51DRU8zet3YjeaQSrPOMluaCaQVrVjBpusxfpZ43LVY4wQ21dDgy1x1Ms+hIJ1q1fLdfVyNeujTZpgU6ZNJ6volwiDg3WqwN2wAANdiBBfW5KdnP9c9+Sv9bQ16+9Nm+Z3miEUxEmqV8HbEEMannDWh7ojnPu5uXSXYM/ozbbrK/qcfEGLgQuMA5OnWGze9a4QQkg9fmZ7/Oj9tVyTw3/nNpvc5fqdxzDqcAMgJHswfYu/VF40U9yX/tquXCYXHfDo02/XA9sjfE54GNOA4fZZAqwI4FlBYwTaFC/h3Yc/i2xj9/WkgYdMUBNcwm+otPcBtaJy9vVMkOcYQjaFDitD+LwgrU8Tg/f2PBN+cswvRaHb5t5qc5pbOIjwIeBE00cV72QNNDa66k+5uzGF5c7reWRnje44/nvyuZhPmiMnDZ7sX7MgQtNjNlqOdA4jFIF/MHv2FzSzknt3oeAeuwTl5d8j/We5Z+fWyW/G2Gx4Qhsp2rznLfw0VgDb7IeR6rleLeJMWqBwGfMvbVOCXsgbdNeB7wutiP8t+PwB7+LLRvWyE9HsIlRb72tbbH+o1jeEWvgbUCjWg5AaRZDS2qD2bTtHiHP5mahs5tZO79vJz4PrNJhDHsx7ETZ7Sd4Rg0PDWbnkDpQR2ibc7l+QAzjxXCQG8OxPgep5WCECWppQhiD0gK0oDQBjgiuQhLFqtAp0IWwG9iH0iGGXeKwSYTtNkmPtWy1Pq88u0Yerp/x3O3/A6qXxURxUsm4AAAAAElFTkSuQmCC", - iconSize: [30, 30], - iconAnchor: [15, 15] - }); - - if (vm.ctx.settings.useCustomProvider && vm.ctx.settings.customProviderTileUrl) { - staticSettings.mapProvider.name = vm.ctx.settings.customProviderTileUrl; - staticSettings.mapProvider.isCustom = true; - } - - if (angular.isDefined(vm.ctx.settings.markerImage)) { - staticSettings.icon = L.icon({ - iconUrl: vm.ctx.settings.markerImage, - iconSize: [staticSettings.markerImageSize, staticSettings.markerImageSize], - iconAnchor: [(staticSettings.markerImageSize / 2), (staticSettings.markerImageSize / 2)] - }) - } - - if (staticSettings.usePathColorFunction && angular.isDefined(vm.ctx.settings.colorFunction)) { - staticSettings.colorFunction = new Function('data, dsData, dsIndex', vm.ctx.settings.colorFunction); - } - - if (staticSettings.usePointAsAnchor && angular.isDefined(vm.ctx.settings.pointAsAnchorFunction)) { - staticSettings.pointAsAnchorFunction = new Function('data, dsData, dsIndex', vm.ctx.settings.pointAsAnchorFunction); - } - - if (staticSettings.usePolygonTooltipFunction && angular.isDefined(vm.ctx.settings.polygonTooltipFunction)) { - staticSettings.polygonTooltipFunction = new Function('data, dsData, dsIndex', vm.ctx.settings.polygonTooltipFunction); - } - - if (staticSettings.useLabelFunction && angular.isDefined(vm.ctx.settings.labelFunction)) { - staticSettings.labelFunction = new Function('data, dsData, dsIndex', vm.ctx.settings.labelFunction); - } - - if (staticSettings.useTooltipFunction && angular.isDefined(vm.ctx.settings.tooltipFunction)) { - staticSettings.tooltipFunction = new Function('data, dsData, dsIndex', vm.ctx.settings.tooltipFunction); - } - - if (staticSettings.usePolygonColorFunction && angular.isDefined(vm.ctx.settings.polygonColorFunction)) { - staticSettings.polygonColorFunction = new Function('data, dsData, dsIndex', vm.ctx.settings.polygonColorFunction); - } - - if (staticSettings.useMarkerImageFunction && angular.isDefined(vm.ctx.settings.markerImageFunction)) { - staticSettings.markerImageFunction = new Function('data, images, dsData, dsIndex', vm.ctx.settings.markerImageFunction); - } - - if (!staticSettings.useMarkerImageFunction && - angular.isDefined(vm.ctx.settings.markerImage) && - vm.ctx.settings.markerImage.length > 0) { - staticSettings.useMarkerImage = true; - let url = vm.ctx.settings.markerImage; - let size = staticSettings.markerImageSize || 20; - staticSettings.currentImage = { - url: url, - size: size - }; - vm.utils.loadImageAspect(staticSettings.currentImage.url).then( - (aspect) => { - if (aspect) { - let width; - let height; - if (aspect > 1) { - width = staticSettings.currentImage.size; - height = staticSettings.currentImage.size / aspect; - } else { - width = staticSettings.currentImage.size * aspect; - height = staticSettings.currentImage.size; - } - staticSettings.icon = L.icon({ - iconUrl: staticSettings.currentImage.url, - iconSize: [width, height], - iconAnchor: [width / 2, height / 2] - }); - } - if (vm.trips) { - vm.trips.forEach(function (trip) { - if (trip.marker) { - trip.marker.setIcon(staticSettings.icon); - } - }); - } - } - ) - } - } - - function configureTripSettings(trip, apply) { - trip.settings = {}; - trip.settings.color = calculateColor(trip); - trip.settings.polygonColor = calculatePolygonColor(trip); - trip.settings.strokeWeight = vm.staticSettings.pathWeight; - trip.settings.strokeOpacity = vm.staticSettings.pathOpacity; - trip.settings.pointColor = vm.staticSettings.pointColor; - trip.settings.polygonStrokeColor = vm.staticSettings.polygonStrokeColor; - trip.settings.polygonStrokeOpacity = vm.staticSettings.polygonStrokeOpacity; - trip.settings.polygonOpacity = vm.staticSettings.polygonOpacity; - trip.settings.polygonStrokeWeight = vm.staticSettings.polygonStrokeWeight; - trip.settings.pointSize = vm.staticSettings.pointSize; - trip.settings.icon = calculateIcon(trip); - if (apply) { - $timeout(() => { - trip.settings.labelText = calculateLabel(trip); - trip.settings.tooltipText = $sce.trustAsHtml(calculateTooltip(trip)); - trip.settings.polygonTooltipText = $sce.trustAsHtml(calculatePolygonTooltip(trip)); - }, 0, true); - } else { - trip.settings.labelText = calculateLabel(trip); - trip.settings.tooltipText = $sce.trustAsHtml(calculateTooltip(trip)); - trip.settings.polygonTooltipText = $sce.trustAsHtml(calculatePolygonTooltip(trip)); - } - } - - function calculateLabel(trip) { - let label = ''; - if (vm.staticSettings.showLabel) { - let labelReplaceInfo; - let labelText = vm.staticSettings.label; - if (vm.staticSettings.useLabelFunction && angular.isDefined(vm.staticSettings.labelFunction)) { - try { - labelText = vm.staticSettings.labelFunction(vm.ctx.data, calculateCurrentDate(trip.timeRange, vm.index), trip.dsIndex); - } catch (e) { - labelText = null; - } - } - labelText = vm.utils.createLabelFromDatasource(trip.dataSource, labelText); - labelReplaceInfo = processPattern(labelText, vm.ctx.datasources, trip.dSIndex); - label = fillPattern(labelText, labelReplaceInfo, calculateCurrentDate(trip.timeRange, vm.index)); - if (vm.staticSettings.useLabelFunction && angular.isDefined(vm.staticSettings.labelFunction)) { - try { - labelText = vm.staticSettings.labelFunction(vm.ctx.data, calculateCurrentDate(trip.timeRange, vm.index), trip.dSIndex); - } catch (e) { - labelText = null; - } - } - } - return label; - } - - function calculateTooltip(trip) { - let tooltip = ''; - if (vm.staticSettings.displayTooltip) { - let tooltipReplaceInfo; - let tooltipText = vm.staticSettings.tooltipPattern; - if (vm.staticSettings.useTooltipFunction && angular.isDefined(vm.staticSettings.tooltipFunction)) { - try { - tooltipText = vm.staticSettings.tooltipFunction(vm.ctx.data, calculateCurrentDate(trip.timeRange, vm.index), trip.dSIndex); - } catch (e) { - tooltipText = null; - } - } - tooltipText = vm.utils.createLabelFromDatasource(trip.dataSource, tooltipText); - tooltipReplaceInfo = processPattern(tooltipText, vm.ctx.datasources, trip.dSIndex); - tooltip = fillPattern(tooltipText, tooltipReplaceInfo, calculateCurrentDate(trip.timeRange, vm.index)); - tooltip = fillPatternWithActions(tooltip, 'onTooltipAction', null); - - } - return tooltip; - } - - function calculatePolygonTooltip(trip) { - let tooltip = ''; - if (vm.staticSettings.displayTooltip) { - let tooltipReplaceInfo; - let tooltipText = vm.staticSettings.polygonTooltipPattern; - if (vm.staticSettings.usePolygonTooltipFunction && angular.isDefined(vm.staticSettings.polygonTooltipFunction)) { - try { - tooltipText = vm.staticSettings.polygonTooltipFunction(vm.ctx.data, calculateCurrentDate(trip.timeRange, vm.index), trip.dSIndex); - } catch (e) { - tooltipText = null; - } - } - tooltipText = vm.utils.createLabelFromDatasource(trip.dataSource, tooltipText); - tooltipReplaceInfo = processPattern(tooltipText, vm.ctx.datasources, trip.dSIndex); - tooltip = fillPattern(tooltipText, tooltipReplaceInfo, calculateCurrentDate(trip.timeRange, vm.index)); - tooltip = fillPatternWithActions(tooltip, 'onTooltipAction', null); - - } - return tooltip; - } - - function calculatePointTooltip(trip, index) { - let tooltip = ''; - if (vm.staticSettings.displayTooltip) { - let tooltipReplaceInfo; - let tooltipText = vm.staticSettings.tooltipPattern; - if (vm.staticSettings.useTooltipFunction && angular.isDefined(vm.staticSettings.tooltipFunction)) { - try { - tooltipText = vm.staticSettings.tooltipFunction(vm.ctx.data, calculateCurrentDate(trip.timeRange, index), trip.dSIndex); - } catch (e) { - tooltipText = null; - } - } - tooltipText = vm.utils.createLabelFromDatasource(trip.dataSource, tooltipText); - tooltipReplaceInfo = processPattern(tooltipText, vm.ctx.datasources, trip.dSIndex); - tooltip = fillPattern(tooltipText, tooltipReplaceInfo, calculateCurrentDate(trip.timeRange, index)); - tooltip = fillPatternWithActions(tooltip, 'onTooltipAction', null); - - } - return tooltip; - } - - function calculateColor(trip) { - let color = vm.staticSettings.pathColor; - let colorFn; - if (vm.staticSettings.usePathColorFunction && angular.isDefined(vm.staticSettings.colorFunction)) { - try { - colorFn = vm.staticSettings.colorFunction(vm.ctx.data, calculateCurrentDate(trip.timeRange, vm.index), trip.dSIndex); - } catch (e) { - colorFn = null; - } - } - if (colorFn && colorFn !== color && trip.polyline) { - trip.polyline.setStyle({color: colorFn}); - } - return colorFn || color; - } - - function calculatePolygonColor(trip) { - let color = vm.staticSettings.polygonColor; - let colorFn; - if (vm.staticSettings.usePolygonColorFunction && angular.isDefined(vm.staticSettings.polygonColorFunction)) { - try { - colorFn = vm.staticSettings.polygonColorFunction(vm.ctx.data, calculateCurrentDate(trip.timeRange, vm.index), trip.dSIndex); - } catch (e) { - colorFn = null; - } - } - if (colorFn && colorFn !== color && trip.polygon) { - trip.polygon.setStyle({fillColor: colorFn}); - } - return colorFn || color; - } - - function calculateIcon(trip) { - let icon = vm.staticSettings.icon; - if (vm.staticSettings.useMarkerImageFunction && angular.isDefined(vm.staticSettings.markerImageFunction)) { - let rawIcon; - try { - rawIcon = vm.staticSettings.markerImageFunction(vm.ctx.data, vm.staticSettings.markerImages, calculateCurrentDate(trip.timeRange, vm.index), trip.dSIndex); - } catch (e) { - rawIcon = null; - } - if (rawIcon) { - vm.utils.loadImageAspect(rawIcon).then( - (aspect) => { - if (aspect) { - let width; - let height; - if (aspect > 1) { - width = rawIcon.size; - height = rawIcon.size / aspect; - } else { - width = rawIcon.size * aspect; - height = rawIcon.size; - } - icon = L.icon({ - iconUrl: rawIcon, - iconSize: [width, height], - iconAnchor: [width / 2, height / 2] - }); - } - if (trip.marker) { - trip.marker.setIcon(icon); - } - } - ) - } - } - return icon; - } - - function createUpdatePath(apply) { - if (vm.trips && vm.map) { - vm.trips.forEach(function (trip) { - if (trip.marker) { - trip.marker.remove(); - delete trip.marker; - } - if (trip.polyline) { - trip.polyline.remove(); - delete trip.polyline; - } - if (trip.polylineDecorator) { - trip.polylineDecorator.remove(); - delete trip.polylineDecorator; - } - if (trip.polygon) { - trip.polygon.remove(); - delete trip.polygon; - } - if (trip.points && trip.points.length) { - trip.points.forEach(function (point) { - point.remove(); - }); - delete trip.points; - } - }); - vm.initBounds = true; - } - createNormalizedTime(vm.data, vm.staticSettings.normalizationStep); - createNormalizedTrips(vm.datasources, vm.data, vm.staticSettings.normalizationStep); - createTripsOnMap(apply); - if (vm.initBounds && !vm.initTrips) { - vm.trips.forEach(function (trip) { - vm.map.extendBounds(vm.map.bounds, trip.polyline); - vm.initBounds = !vm.datasources.every( - function (ds) { - return ds.dataReceived === true; - }); - vm.initTrips = vm.trips.every(function (trip) { - return angular.isDefined(trip.marker) && angular.isDefined(trip.polyline); - }); - }); - - vm.map.fitBounds(vm.map.bounds); - } - } - - function fillPattern(pattern, replaceInfo, currentNormalizedValue) { - let text = angular.copy(pattern); - let reg = /\$\{([^}]*)\}/g; - if (replaceInfo) { - for (let v = 0; v < replaceInfo.variables.length; v++) { - let variableInfo = replaceInfo.variables[v]; - let label = reg.exec(pattern)[1].split(":")[0]; - let txtVal = ''; - if (label.length > -1 && angular.isDefined(currentNormalizedValue[label])) { - let varData = currentNormalizedValue[label]; - if (isNumber(varData)) { - txtVal = padValue(varData, variableInfo.valDec, 0); - } else { - txtVal = varData; - } - } - text = text.split(variableInfo.variable).join(txtVal); - } - } - return text; - } - - function createNormalizedTime(data, step) { - if (!step) step = 1000; - let max_time = -Infinity; - let min_time = Infinity; - if (data) { - for (let i = 0; i < data.length; i++) { - for (let j = 0; j < data[i].data.length; j++) { - if (max_time < data[i].data[j][0]) { - max_time = data[i].data[j][0] - } - if (min_time > data[i].data[j][0]) { - min_time = data[i].data[j][0]; - } - } - } - } - vm.minTime = vm.animationTime = min_time; - if(min_time === Infinity){ - vm.animationTime = null; - } else { - vm.animationTime = min_time - } - vm.maxTimeIndex = Math.ceil((max_time - min_time) / step); - if (vm.index < vm.minTimeIndex) { - vm.index = vm.minTimeIndex; - } else if (vm.index > vm.maxTimeIndex) { - vm.index = vm.maxTimeIndex; - } - } - - function createNormalizedTrips(dataSources, data, step) { - vm.trips = []; - step = step || 1000; - if (dataSources && data) { - for (let i = 0; i < dataSources.length; i++) { - vm.trips.push({ - dataSource: dataSources[i], - dSIndex: i, - timeRange: {} - }) - } - - for (let i = 0; i < data.length; i++) { - let ds = data[i].datasource; - let tripIndex = vm.trips.findIndex(function (el) { - return el.dataSource.entityId === ds.entityId; - }); - if (tripIndex > -1) { - createNormalizedValue(data[i].data, data[i].dataKey.label, vm.trips[tripIndex].timeRange, step); - } - } - } - - createNormalizedLatLngs(); - } - - function createNormalizedValue(dataArray, dataKey, timeRange, step) { - for (let i = 0; i < dataArray.length; i++) { - let normalizeTime = vm.minTime + Math.ceil((dataArray[i][0] - vm.minTime) / step) * step; - timeRange[normalizeTime] = timeRange[normalizeTime] || {}; - timeRange[normalizeTime][dataKey] = dataArray[i][1]; - } - } - - function createNormalizedLatLngs() { - vm.trips.forEach(function (item) { - item.latLngs = []; - for (let timestamp in item.timeRange) { - if(Object.prototype.hasOwnProperty.call(item.timeRange, timestamp)) { - let lat = item.timeRange[timestamp][vm.staticSettings.latKeyName]; - let lng = item.timeRange[timestamp][vm.staticSettings.lngKeyName]; - if (lat && lng && vm.map) { - item.timeRange[timestamp].latLng = vm.map.createLatLng(lat, lng); - } - item.latLngs.push(item.timeRange[timestamp].latLng); - } - } - }); - } - - function createPointPopup(point, index, trip) { - let popup = L.popup(); - popup.setContent(calculatePointTooltip(trip, index)); - point.bindPopup(popup, {autoClose: vm.staticSettings.autocloseTooltip, closeOnClick: false}); - return popup; - } - - function createTripsOnMap(apply) { - if (vm.trips.length > 0) { - vm.trips.forEach(function (trip) { - configureTripSettings(trip, apply); - if (Object.keys(trip.timeRange).length > 0 && trip.latLngs.every(el => angular.isDefined(el))) { - if (vm.staticSettings.showPoints) { - trip.points = []; - Object.keys(trip.timeRange).forEach(function (tRange, index) { - if (trip.timeRange[tRange] && trip.timeRange[tRange].latLng - && (!vm.staticSettings.usePointAsAnchor || vm.staticSettings.pointAsAnchorFunction(vm.ctx.data, trip.timeRange[tRange], trip.dSIndex))) { - let point = L.circleMarker(trip.timeRange[tRange].latLng, { - color: trip.settings.pointColor, - radius: trip.settings.pointSize - }).addTo(vm.map.map); - if (vm.staticSettings.pointTooltipOnRightPanel) { - point.popup = createPointPopup(point, index, trip); - } else { - point.on('click', function () { - showHidePointTooltip(calculatePointTooltip(trip, index), index); - }); - } - if (vm.staticSettings.usePointAsAnchor) trip.timeRange[tRange].hasAnchor = true; - trip.points.push(point); - } - }); - } - - if (angular.isUndefined(trip.marker)) { - trip.polyline = vm.map.createPolyline(trip.latLngs, trip.settings); - if (vm.staticSettings.usePolylineDecorator) { - trip.polylineDecorator = L.polylineDecorator(trip.polyline, { - patterns: [ - { - offset: vm.staticSettings.decoratorOffset, - endOffset: vm.staticSettings.endDecoratorOffset, - repeat: vm.staticSettings.decoratorRepeat, - symbol: L.Symbol[vm.staticSettings.decoratorSymbol]({ - pixelSize: vm.staticSettings.decoratorSymbolSize, - polygon: false, - pathOptions: { - color: vm.staticSettings.useDecoratorCustomColor ? vm.staticSettings.decoratorCustomColor : trip.settings.color, - stroke: true - } - }) - } - ], - interactive: false, - }).addTo(vm.map.map); - } - } - - - if (trip.timeRange && Object.keys(trip.timeRange).length && angular.isUndefined(trip.marker)) { - trip.marker = L.marker(calculateCurrentDate(trip.timeRange, vm.index).latLng); - trip.marker.setZIndexOffset(1000); - trip.marker.setIcon(vm.staticSettings.icon); - trip.marker.setRotationOrigin('center center'); - trip.marker.on('click', function () { - showHideTooltip(trip); - }); - trip.marker.addTo(vm.map.map); - moveMarker(trip); - } - } - - if (vm.staticSettings.showPolygon && angular.isDefined(calculateCurrentDate(trip.timeRange, vm.index)[vm.staticSettings.polKeyName])) { - let polygonSettings = { - fill: true, - fillColor: trip.settings.polygonColor, - color: trip.settings.polygonStrokeColor, - weight: trip.settings.polygonStrokeWeight, - fillOpacity: trip.settings.polygonOpacity, - opacity: trip.settings.polygonStrokeOpacity - }; - let polygonLatLngsRaw = mapPolygonArray(angular.fromJson(calculateCurrentDate(trip.timeRange, vm.index)[vm.staticSettings.polKeyName])); - trip.polygon = L.polygon(polygonLatLngsRaw, polygonSettings).addTo(vm.map.map); - trip.polygon.on('click',function(){showHidePolygonTooltip(trip)}); - } - }); - } - } - - function calculateCurrentDate(tripTimeRange, index) { - let time = vm.minTime + index * vm.staticSettings.normalizationStep; - if (Object.hasOwnProperty.call(tripTimeRange, time)) { - return tripTimeRange[time]; - } else { - let timeInterval = Object.keys(tripTimeRange); - for (let i = 1; i < timeInterval.length; i++) { - if (timeInterval[i - 1] < time && timeInterval[i] > time) { - let calcPosition = angular.copy(tripTimeRange[timeInterval[i - 1]]); - let startLatLng = tripTimeRange[timeInterval[i - 1]].latLng; - let finishLatLng = tripTimeRange[timeInterval[i]].latLng; - let percentRouteComplete = (time - timeInterval[i - 1]) / (timeInterval[i] - timeInterval[i - 1]); - calcPosition[vm.staticSettings.latKeyName] = startLatLng.lat + (finishLatLng.lat - startLatLng.lat) * percentRouteComplete; - calcPosition[vm.staticSettings.lngKeyName] = startLatLng.lng + (finishLatLng.lng - startLatLng.lng) * percentRouteComplete; - calcPosition.latLng = vm.map.createLatLng(calcPosition[vm.staticSettings.latKeyName], calcPosition[vm.staticSettings.lngKeyName]); - calcPosition.angle = vm.staticSettings.rotationAngle + findAngle(startLatLng, finishLatLng); - return calcPosition; - } - } - } - return {}; - } - - function findAngle(startPoint, endPoint) { - let angle = -Math.atan2(endPoint.lat - startPoint.lat, endPoint.lng - startPoint.lng); - angle = angle * 180 / Math.PI; - return parseInt(angle.toFixed(2)); - } - - function mapPolygonArray(rawArray) { - return rawArray.map(function (el) { - if (el.length === 2) { - if (!angular.isNumber(el[0]) && !angular.isNumber(el[1])) { - return el.map(function (subEl) { - return mapPolygonArray(subEl); - }) - } else { - return vm.map.createLatLng(el[0], el[1]); - } - } else if (el.length > 2) { - return mapPolygonArray(el); - } else { - return vm.map.createLatLng(false); - } - }); - } - - function moveMarker(trip) { - let postionMarker = calculateCurrentDate(trip.timeRange, vm.index); - if (angular.isDefined(postionMarker)) { - if (angular.isDefined(trip.marker)) { - trip.markerAngleIsSet = true; - trip.marker.setLatLng(postionMarker.latLng); - trip.marker.setRotationAngle(postionMarker.angle); - trip.marker.update(); - } else { - if (trip.timeRange && trip.timeRange.length) { - trip.marker = L.marker(postionMarker.latLng); - trip.marker.setZIndexOffset(1000); - trip.marker.setIcon(vm.staticSettings.icon); - trip.marker.setRotationOrigin('center center'); - trip.marker.on('click', function () { - showHideTooltip(trip); - }); - trip.marker.addTo(vm.map.map); - trip.marker.update(); - } - } - } - configureTripSettings(trip); - } - - - function showHideTooltip(trip) { - if (vm.staticSettings.displayTooltip) { - if (vm.staticSettings.showTooltip && trip && (vm.activeTripIndex !== trip.dSIndex || vm.staticSettings.tooltipMarker !== 'marker')) { - vm.staticSettings.showTooltip = true; - vm.staticSettings.tooltipMarker = 'marker'; - } else { - vm.staticSettings.showTooltip = !vm.staticSettings.showTooltip; - } - } - if (trip && vm.activeTripIndex !== trip.dSIndex) vm.activeTripIndex = trip.dSIndex; - if (vm.trips.length) { - vm.mainTooltip = vm.trips[vm.activeTripIndex].settings.tooltipText; - } - } - - function showHidePointTooltip(text, index) { - if (vm.staticSettings.displayTooltip) { - if (vm.staticSettings.tooltipMarker && vm.staticSettings.tooltipMarker.includes('point')) { - if (vm.staticSettings.tooltipMarker === 'point' + index) { - vm.staticSettings.showTooltip = !vm.staticSettings.showTooltip; - } else { - vm.staticSettings.showTooltip = true; - vm.mainTooltip = $sce.trustAsHtml(text); - vm.staticSettings.tooltipMarker = 'point' + index; - } - } else { - vm.staticSettings.showTooltip = true; - vm.mainTooltip = $sce.trustAsHtml(text); - vm.staticSettings.tooltipMarker = 'point' + index; - } - } - } - - function showHidePolygonTooltip(trip) { - if (vm.staticSettings.displayTooltip) { - if (vm.staticSettings.showTooltip && trip && (vm.activeTripIndex !== trip.dSIndex || vm.staticSettings.tooltipMarker !== 'polygon')) { - vm.staticSettings.showTooltip = true; - vm.staticSettings.tooltipMarker = 'polygon'; - } else { - vm.staticSettings.showTooltip = !vm.staticSettings.showTooltip; - } - } - if (trip && vm.activeTripIndex !== trip.dSIndex) vm.activeTripIndex = trip.dSIndex; - vm.mainTooltip = vm.trips[vm.activeTripIndex].settings.polygonTooltipText; - } -} diff --git a/ui/src/app/widget/lib/tripAnimation/trip-animation-widget.scss b/ui/src/app/widget/lib/tripAnimation/trip-animation-widget.scss deleted file mode 100644 index c38a8ebf2c..0000000000 --- a/ui/src/app/widget/lib/tripAnimation/trip-animation-widget.scss +++ /dev/null @@ -1,136 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.trip-animation-widget { - - position: relative; - width: 100%; - height: 100%; - font-size: 16px; - line-height: 24px; - - .trip-animation-label-container { - height: 24px; - } - - .trip-animation-container { - position: relative; - z-index: 1; - flex: 1; - width: 100%; - - #trip-animation-map { - z-index: 1; - width: 100%; - height: 100%; - - .pointsLayerMarkerIcon { - border-radius: 50%; - } - } - - .trip-animation-info-panel { - position: absolute; - top: 0; - right: 0; - z-index: 2; - pointer-events: none; - - .md-button { - top: 0; - left: 0; - width: 32px; - min-width: 32px; - height: 32px; - min-height: 32px; - padding: 0 0 2px; - margin: 2px; - line-height: 24px; - - ng-md-icon { - width: 24px; - height: 24px; - - svg { - width: inherit; - height: inherit; - } - } - } - } - - .trip-animation-tooltip { - position: absolute; - top: 38px; - right: 0; - z-index: 2; - padding: 10px; - background-color: #fff; - transition: .3s ease-in-out; - - &-hidden { - transform: translateX(110%); - } - } - } - - .trip-animation-control-panel { - position: relative; - box-sizing: border-box; - width: 100%; - padding-bottom: 16px; - padding-left: 10px; - - md-slider-container { - md-slider { - min-width: 80px; - } - - button.md-button.md-icon-button { - width: 44px; - min-width: 44px; - height: 44px; - min-height: 44px; - margin: 0; - line-height: 28px; - - md-icon { - width: 28px; - height: 28px; - font-size: 28px; - - svg { - width: inherit; - height: inherit; - } - } - } - - md-select { - margin: 0; - } - } - - .panel-timer { - max-width: none; - padding-right: 250px; - padding-left: 90px; - margin-top: -20px; - font-size: 12px; - font-weight: 500; - text-align: center; - } - } -} diff --git a/ui/src/app/widget/lib/tripAnimation/trip-animation-widget.tpl.html b/ui/src/app/widget/lib/tripAnimation/trip-animation-widget.tpl.html deleted file mode 100644 index 70ef09a59f..0000000000 --- a/ui/src/app/widget/lib/tripAnimation/trip-animation-widget.tpl.html +++ /dev/null @@ -1,71 +0,0 @@ - -
    -
    - {{vm.trips[vm.activeTripIndex].settings.labelText}} -
    -
    -
    -
    - - - -
    -
    -
    -
    -
    - - - fast_rewind - - - skip_previous - - - - skip_next - - - fast_forward - - - - play_circle_outline - - - - {{ speed}} - - - - - pause_circle_outline - - - -
    - {{ vm.animationTime | date:'medium'}} - {{ "widget.no-data-found" | translate}} -
    -
    -
    diff --git a/ui/src/app/widget/lib/web-camera-input-widget.js b/ui/src/app/widget/lib/web-camera-input-widget.js deleted file mode 100644 index e66c88b5f7..0000000000 --- a/ui/src/app/widget/lib/web-camera-input-widget.js +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import './web-camera-input-widget.scss'; - -/* eslint-disable import/no-unresolved, import/default */ -import webCameraWidgetTemplate from './web-camera-input-widget.tpl.html'; -/* eslint-enable import/no-unresolved, import/default */ - -export default angular.module('thingsboard.widgets.webCameraWidget', []) - .directive('tbWebCameraWidget', webCameraWidget) - .name; - -/*@ngInject*/ -function webCameraWidget() { - return { - restrict: "E", - scope: true, - bindToController: { - ctx: '=' - }, - controller: WebCameraWidgetController, - controllerAs: 'vm', - templateUrl: webCameraWidgetTemplate - }; -} - -function WebCameraWidgetController($element, $scope, $window, types, utils, attributeService, Fullscreen) { - let vm = this; - - vm.videoInput = []; - vm.videoDevice = ""; - vm.previewPhoto = ""; - vm.isShowCamera = false; - vm.isPreviewPhoto = false; - - let streamDevice = null; - let indexWebCamera = 0; - let videoElement = null; - let canvas = null; - let photoCamera = null; - let dataKeyType = ""; - let width = 640; - let height = 480; - - const DEFAULT_IMAGE_TYPE = 'image/jpeg'; - const DEFAULT_IMAGE_QUALITY = 0.92; - - vm.getStream = getStream; - vm.createPhoto = createPhoto; - vm.takePhoto = takePhoto; - vm.switchWebCamera = switchWebCamera; - vm.cancelPhoto = cancelPhoto; - vm.closeCamera = closeCamera; - vm.savePhoto = savePhoto; - - vm.isEntityDetected = false; - vm.dataKeyDetected = false; - vm.isCameraSupport = false; - vm.isDeviceDetect = false; - - $scope.$watch('vm.ctx', function () { - if (vm.ctx && vm.ctx.datasources && vm.ctx.datasources.length) { - let datasource = vm.ctx.datasources[0]; - if (datasource.type === types.datasourceType.entity) { - if (datasource.entityType && datasource.entityId) { - if (vm.ctx.settings.widgetTitle && vm.ctx.settings.widgetTitle.length) { - $scope.titleTemplate = utils.customTranslation(vm.ctx.settings.widgetTitle, vm.ctx.settings.widgetTitle); - } else { - $scope.titleTemplate = vm.ctx.widgetConfig.title; - } - vm.isEntityDetected = true; - } - } - width = vm.ctx.settings.maxWidth ? vm.ctx.settings.maxWidth : 640; - height = vm.ctx.settings.maxHeight ? vm.ctx.settings.maxWidth : 480; - if (datasource.dataKeys.length) { - $scope.currentKey = datasource.dataKeys[0].name; - dataKeyType = datasource.dataKeys[0].type; - vm.dataKeyDetected = true; - } - if (hasGetUserMedia()) { - vm.isCameraSupport = true; - getDevices().then(gotDevices).then(() => { - vm.isDeviceDetect = !!vm.videoInput.length; - }); - } - } - }); - - function getVideoAspectRatio() { - if (videoElement.videoWidth && videoElement.videoWidth > 0 && - videoElement.videoHeight && videoElement.videoHeight > 0) { - return videoElement.videoWidth / videoElement.videoHeight; - } - return width / height; - } - - vm.videoWidth = function() { - const videoRatio = getVideoAspectRatio(); - return Math.min(width, height * videoRatio); - } - - vm.videoHeight = function() { - const videoRatio = getVideoAspectRatio(); - return Math.min(height, width / videoRatio); - } - - function hasGetUserMedia() { - return !!($window.navigator.mediaDevices && $window.navigator.mediaDevices.getUserMedia); - } - - function takePhoto(){ - vm.isShowCamera = true; - videoElement = $element[0].querySelector('#videoStream'); - photoCamera = $element[0].querySelector('#photoCamera'); - canvas = $element[0].querySelector('canvas'); - Fullscreen.enable(photoCamera); - getStream(); - } - - function cancelPhoto() { - vm.isPreviewPhoto = false; - vm.previewPhoto = ""; - } - - function switchWebCamera() { - indexWebCamera = (indexWebCamera+1)%vm.videoInput.length; - vm.videoDevice = vm.videoInput[indexWebCamera].deviceId; - getStream(); - } - - function getDevices() { - return $window.navigator.mediaDevices.enumerateDevices(); - } - - function gotDevices(deviceInfos) { - for (const deviceInfo of deviceInfos) { - let device = { - deviceId: deviceInfo.deviceId, - label: "" - }; - if (deviceInfo.kind === 'videoinput') { - device.label = deviceInfo.label || `Camera ${vm.videoInput.length + 1}`; - vm.videoInput.push(device); - } - } - } - - function getStream() { - if (streamDevice !== null) { - streamDevice.getTracks().forEach(track => { - track.stop(); - }); - } - const constraints = { - video: {deviceId: vm.videoDevice !== "" ? {exact: vm.videoDevice} : undefined} - }; - return $window.navigator.mediaDevices.getUserMedia(constraints).then(gotStream); - } - - function gotStream(stream) { - streamDevice = stream; - if(vm.videoDevice === ""){ - indexWebCamera = vm.videoInput.findIndex(option => option.label === stream.getVideoTracks()[0].label); - indexWebCamera = indexWebCamera === -1 ? 0 : indexWebCamera; - vm.videoDevice = vm.videoInput[indexWebCamera].deviceId; - } - videoElement.srcObject = stream; - } - - function createPhoto() { - canvas.width = vm.videoWidth(); - canvas.height = vm.videoHeight(); - canvas.getContext('2d').drawImage(videoElement, 0, 0, vm.videoWidth(), vm.videoHeight()); - const mimeType = vm.ctx.settings.imageFormat ? vm.ctx.settings.imageFormat : DEFAULT_IMAGE_TYPE; - const quality = vm.ctx.settings.imageQuality ? vm.ctx.settings.imageQuality : DEFAULT_IMAGE_QUALITY; - vm.previewPhoto = canvas.toDataURL(mimeType, quality); - vm.isPreviewPhoto = true; - } - - function closeCamera(){ - Fullscreen.cancel(photoCamera); - vm.isShowCamera = false; - if (streamDevice !== null) { - streamDevice.getTracks().forEach(track => { - track.stop(); - }); - } - streamDevice = null; - videoElement.srcObject = null; - } - - function savePhoto(){ - let promiseData = null; - let datasource = vm.ctx.datasources[0]; - let saveData = [{ - key: datasource.dataKeys[0].name, - value: vm.previewPhoto - }]; - if(dataKeyType === types.dataKeyType.attribute){ - promiseData = attributeService.saveEntityAttributes(datasource.entityType, datasource.entityId, types.attributesScope.server.value, saveData); - } else if(dataKeyType === types.dataKeyType.timeseries){ - promiseData = attributeService.saveEntityTimeseries(datasource.entityType, datasource.entityId, "scope", saveData); - } - promiseData.then(()=>{ - vm.isPreviewPhoto = false; - closeCamera(); - }) - } -} diff --git a/ui/src/app/widget/lib/web-camera-input-widget.scss b/ui/src/app/widget/lib/web-camera-input-widget.scss deleted file mode 100644 index 7d2d8747ec..0000000000 --- a/ui/src/app/widget/lib/web-camera-input-widget.scss +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-web-camera { - height: 100%; - - &__last-photo{ - width: 100%; - margin: 5px 0; - text-align: center; - border: solid 1px; - - &_text{ - position: absolute; - top: 50%; - left: 50%; - margin-top: -.625em; - transform: translate(-50%, -50%); - } - - &_img{ - width: 100%; - height: 100%; - object-fit: contain; - } - } - - .camera { - position: relative; - width: 100%; - height: 100%; - overflow: hidden; - - .camera-stream{ - display: block; - width: 100%; - height: 100%; - object-fit: contain; - } - - .camera-controls { - position: absolute; - bottom: 0; - width: 100%; - padding: 0 5px 5px; - } - } - - .message-text{ - font-size: 18px; - color: #a0a0a0; - text-align: center; - } -} diff --git a/ui/src/app/widget/lib/web-camera-input-widget.tpl.html b/ui/src/app/widget/lib/web-camera-input-widget.tpl.html deleted file mode 100644 index c6923cf11e..0000000000 --- a/ui/src/app/widget/lib/web-camera-input-widget.tpl.html +++ /dev/null @@ -1,73 +0,0 @@ - -
    -
    -
    -
    - widgets.input-widgets.no-image - -
    - {{ "widgets.input-widgets.take-photo" | translate }} -
    -
    -
    - -
    -
    - - switch_camera - - - photo_camera - - - close - -
    -
    -
    -
    - - -
    -
    - - close - - - check - -
    -
    -
    -
    -
    -
    - {{ 'widgets.input-widgets.no-entity-selected' | translate }} -
    -
    - {{ 'widgets.input-widgets.no-datakey-selected' | translate }} -
    -
    - {{ 'widgets.input-widgets.no-support-web-camera' | translate }} -
    -
    - {{ 'widgets.input-widgets.no-support-web-camera' | translate }} -
    -
    diff --git a/ui/src/app/widget/lib/widget-utils.js b/ui/src/app/widget/lib/widget-utils.js deleted file mode 100644 index a8d87a84d8..0000000000 --- a/ui/src/app/widget/lib/widget-utils.js +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -const varsRegex = /\$\{([^}]*)\}/g; - -const linkActionRegex = /([^<]*)<\/link-act>/g; -const buttonActionRegex = /([^<]*)<\/button-act>/g; - -export function processPattern(pattern, datasources, dsIndex) { - var match = varsRegex.exec(pattern); - var replaceInfo = {}; - replaceInfo.variables = []; - while (match !== null) { - var variableInfo = {}; - variableInfo.dataKeyIndex = -1; - var variable = match[0]; - var label = match[1]; - var valDec = 2; - var splitVals = label.split(':'); - if (splitVals.length > 1) { - label = splitVals[0]; - valDec = parseFloat(splitVals[1]); - } - variableInfo.variable = variable; - variableInfo.valDec = valDec; - - var keyIndex = -1; - - if (label.startsWith('#')) { - var keyIndexStr = label.substring(1); - var n = Math.floor(Number(keyIndexStr)); - if (String(n) === keyIndexStr && n >= 0) { - keyIndex = n; - } - } - var offset = 0; - for (var i=0;i -1) { - if (keyIndex < datasource.dataKeys.length) { - variableInfo.dataKeyIndex = offset + keyIndex; - } - } else { - for (var k = 0; k < datasource.dataKeys.length; k++) { - var dataKey = datasource.dataKeys[k]; - if (dataKey.label === label) { - variableInfo.dataKeyIndex = offset + k; - break; - } - } - } - } - offset += datasource.dataKeys.length; - } - replaceInfo.variables.push(variableInfo); - match = varsRegex.exec(pattern); - } - return replaceInfo; -} - -export function fillPattern(pattern, replaceInfo, data) { - var text = angular.copy(pattern); - if (replaceInfo) { - for (var v = 0; v < replaceInfo.variables.length; v++) { - var variableInfo = replaceInfo.variables[v]; - var txtVal = ''; - if (variableInfo.dataKeyIndex > -1 && data[variableInfo.dataKeyIndex]) { - var varData = data[variableInfo.dataKeyIndex].data; - if (varData.length > 0) { - var val = varData[varData.length - 1][1]; - if (isNumber(val)) { - txtVal = padValue(val, variableInfo.valDec, 0); - } else { - txtVal = val; - } - } - } - text = text.split(variableInfo.variable).join(txtVal); - } - } - return text; -} - -function createLink(actionName, actionText, actionCallbackName, additionalArgs) { - var args = 'event,\''+actionName+'\''; - if (additionalArgs && additionalArgs.length) { - args += ','+additionalArgs.join(); - } - return ''+actionText+''; -} - -function createButton(actionName, actionText, actionCallbackName, additionalArgs) { - var args = 'event,\''+actionName+'\''; - if (additionalArgs && additionalArgs.length) { - args += ','+additionalArgs.join(); - } - return ''; -} - -export function fillPatternWithActions(pattern, actionCallbackName, additionalArgs) { - var text = angular.copy(pattern); - var match = linkActionRegex.exec(pattern); - var actionTags; - var actionName; - var actionText; - var actionHtml; - while (match !== null) { - actionTags = match[0]; - actionName = match[1]; - actionText = match[2]; - actionHtml = createLink(actionName, actionText, actionCallbackName, additionalArgs); - text = text.split(actionTags).join(actionHtml); - match = linkActionRegex.exec(pattern); - } - match = buttonActionRegex.exec(pattern); - while (match !== null) { - actionTags = match[0]; - actionName = match[1]; - actionText = match[2]; - actionHtml = createButton(actionName, actionText, actionCallbackName, additionalArgs); - text = text.split(actionTags).join(actionHtml); - match = buttonActionRegex.exec(pattern); - } - return text; -} - -export function toLabelValueMap(data, datasources) { - var dataMap = {}; - var dsDataMap = []; - for (var d=0;d 0) { - ts = keyData[keyData.length-1][0]; - val = keyData[keyData.length-1][1]; - } - dataMap[label+'|ts'] = ts; - dataMap[label] = val; - var dsIndex = datasources.indexOf(data[i].datasource); - dsDataMap[dsIndex][label+'|ts'] = ts; - dsDataMap[dsIndex][label] = val; - } - return { - dataMap: dataMap, - dsDataMap: dsDataMap - }; -} - -export function isNumber(n) { - return !isNaN(parseFloat(n)) && isFinite(n); -} - -export function padValue(val, dec, int) { - var i = 0; - var s, strVal, n; - - val = parseFloat(val); - n = (val < 0); - val = Math.abs(val); - - if (dec > 0) { - strVal = val.toFixed(dec).toString().split('.'); - s = int - strVal[0].length; - - for (; i < s; ++i) { - strVal[0] = '0' + strVal[0]; - } - - strVal = (n ? '-' : '') + strVal[0] + '.' + strVal[1]; - } - - else { - strVal = Math.round(val).toString(); - s = int - strVal.length; - - for (; i < s; ++i) { - strVal = '0' + strVal; - } - - strVal = (n ? '-' : '') + strVal; - } - - return strVal; -} - -export function arraysEqual(a, b) { - if (a === b) return true; - if (a === null || b === null) return false; - if (a.length != b.length) return false; - - for (var i = 0; i < a.length; ++i) { - if (!arraysEqual(a[i],b[i])) return false; - } - return true; -} diff --git a/ui/src/app/widget/save-widget-type-as.controller.js b/ui/src/app/widget/save-widget-type-as.controller.js deleted file mode 100644 index b6feb444cf..0000000000 --- a/ui/src/app/widget/save-widget-type-as.controller.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/*@ngInject*/ -export default function SaveWidgetTypeAsController($mdDialog, $q, userService) { - - var vm = this; - - vm.saveAs = saveAs; - vm.cancel = cancel; - - vm.widgetName; - vm.widgetsBundle; - - vm.bundlesScope = 'system'; - - if (userService.getAuthority() === 'TENANT_ADMIN') { - vm.bundlesScope = 'tenant'; - } - - function cancel () { - $mdDialog.cancel(); - } - - function saveAs () { - $mdDialog.hide({widgetName: vm.widgetName, bundleId: vm.widgetsBundle.id.id, bundleAlias: vm.widgetsBundle.alias}); - } - -} diff --git a/ui/src/app/widget/save-widget-type-as.tpl.html b/ui/src/app/widget/save-widget-type-as.tpl.html deleted file mode 100644 index b41903d344..0000000000 --- a/ui/src/app/widget/save-widget-type-as.tpl.html +++ /dev/null @@ -1,60 +0,0 @@ - - -
    - -
    -

    widget.save-widget-type-as

    - - - - -
    -
    - - - -
    -
    - widget.save-widget-type-as-text - - - -
    -
    widget.title-required
    -
    -
    - - -
    -
    -
    - - - - {{ 'action.saveAs' | translate }} - - {{ 'action.cancel' | - translate }} - - -
    -
    diff --git a/ui/src/app/widget/select-widget-type.controller.js b/ui/src/app/widget/select-widget-type.controller.js deleted file mode 100644 index c0530ae486..0000000000 --- a/ui/src/app/widget/select-widget-type.controller.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/*@ngInject*/ -export default function SelectWidgetTypeController($mdDialog, types) { - - var vm = this; - - vm.types = types; - - vm.cancel = cancel; - vm.typeSelected = typeSelected; - - function cancel() { - $mdDialog.cancel(); - } - - function typeSelected(widgetType) { - $mdDialog.hide(widgetType); - } - -} diff --git a/ui/src/app/widget/select-widget-type.tpl.html b/ui/src/app/widget/select-widget-type.tpl.html deleted file mode 100644 index c19bfffae8..0000000000 --- a/ui/src/app/widget/select-widget-type.tpl.html +++ /dev/null @@ -1,81 +0,0 @@ - - -
    - -
    -

    widget.select-widget-type

    - - - - -
    -
    - - - -
    -
    -
    - - - timeline - - {{vm.types.widgetType.timeseries.name}} - - - track_changes - - {{vm.types.widgetType.latest.name}} - - - - - {{vm.types.widgetType.rpc.name}} - - - error - - {{vm.types.widgetType.alarm.name}} - - - font_download - - {{vm.types.widgetType.static.name}} - -
    -
    -
    -
    - - - {{ 'action.cancel' | - translate }} - - -
    -
    diff --git a/ui/src/app/widget/widget-editor.controller.js b/ui/src/app/widget/widget-editor.controller.js deleted file mode 100644 index 1fa6838b42..0000000000 --- a/ui/src/app/widget/widget-editor.controller.js +++ /dev/null @@ -1,666 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import $ from 'jquery'; -import ace from 'brace'; -import 'brace/ext/language_tools'; -import 'brace/ext/searchbox'; -import 'brace/mode/javascript'; -import 'brace/mode/html'; -import 'brace/mode/css'; -import 'brace/mode/json'; -import 'brace/snippets/javascript'; -import 'brace/snippets/text'; -import 'brace/snippets/html'; -import 'brace/snippets/css'; -import 'brace/snippets/json'; - -/* eslint-disable import/no-unresolved, import/default */ - -import saveWidgetTypeAsTemplate from './save-widget-type-as.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -import Split from 'split.js'; -import beautify from 'js-beautify'; - -const js_beautify = beautify.js; -const html_beautify = beautify.html; -const css_beautify = beautify.css; - -/* eslint-disable angular/angularelement */ - -/*@ngInject*/ -export default function WidgetEditorController(widgetService, userService, types, toast, hotkeys, - $element, $rootScope, $scope, $state, $stateParams, $timeout, - $window, $document, $translate, $mdDialog) { - - var Range = ace.acequire("ace/range").Range; - var ace_editors = []; - var js_editor; - var iframe = $('iframe', $element); - var gotError = false; - var errorMarkers = []; - var errorAnnotationId = -1; - var elem = $($element); - - var widgetsBundleId = $stateParams.widgetsBundleId; - - var vm = this; - - vm.widgetsBundle; - vm.isDirty = false; - vm.fullscreen = false; - vm.widgetType = null; - vm.widget = null; - vm.origWidget = null; - vm.widgetTypes = types.widgetType; - vm.iframeWidgetEditModeInited = false; - vm.layoutInited = false; - vm.htmlEditorOptions = { - useWrapMode: true, - mode: 'html', - advanced: { - enableSnippets: true, - enableBasicAutocompletion: true, - enableLiveAutocompletion: true - }, - onLoad: function (_ace) { - ace_editors.push(_ace); - } - }; - vm.cssEditorOptions = { - useWrapMode: true, - mode: 'css', - advanced: { - enableSnippets: true, - enableBasicAutocompletion: true, - enableLiveAutocompletion: true - }, - onLoad: function (_ace) { - ace_editors.push(_ace); - } - }; - vm.jsonSettingsEditorOptions = { - useWrapMode: true, - mode: 'json', - advanced: { - enableSnippets: true, - enableBasicAutocompletion: true, - enableLiveAutocompletion: true - }, - onLoad: function (_ace) { - ace_editors.push(_ace); - } - }; - vm.dataKeyJsonSettingsEditorOptions = { - useWrapMode: true, - mode: 'json', - advanced: { - enableSnippets: true, - enableBasicAutocompletion: true, - enableLiveAutocompletion: true - }, - onLoad: function (_ace) { - ace_editors.push(_ace); - } - }; - vm.jsEditorOptions = { - useWrapMode: true, - mode: 'javascript', - advanced: { - enableSnippets: true, - enableBasicAutocompletion: true, - enableLiveAutocompletion: true - }, - onLoad: function (_ace) { - ace_editors.push(_ace); - js_editor = _ace; - js_editor.session.on("change", function () { - cleanupJsErrors(); - }); - } - }; - - vm.addResource = addResource; - vm.applyWidgetScript = applyWidgetScript; - vm.beautifyCss = beautifyCss; - vm.beautifyDataKeyJson = beautifyDataKeyJson; - vm.beautifyHtml = beautifyHtml; - vm.beautifyJs = beautifyJs; - vm.beautifyJson = beautifyJson; - vm.removeResource = removeResource; - vm.undoDisabled = undoDisabled; - vm.undoWidget = undoWidget; - vm.saveDisabled = saveDisabled; - vm.saveWidget = saveWidget; - vm.saveAsDisabled = saveAsDisabled; - vm.saveWidgetAs = saveWidgetAs; - vm.toggleFullscreen = toggleFullscreen; - vm.isReadOnly = isReadOnly; - - initWidgetEditor(); - - function initWidgetEditor() { - - $rootScope.loading = true; - - widgetService.getWidgetsBundle(widgetsBundleId).then( - function success(widgetsBundle) { - vm.widgetsBundle = widgetsBundle; - if ($stateParams.widgetTypeId) { - widgetService.getWidgetTypeById($stateParams.widgetTypeId).then( - function success(widgetType) { - setWidgetType(widgetType) - widgetTypeLoaded(); - }, - function fail() { - toast.showError($translate.instant('widget.widget-type-load-failed-error')); - widgetTypeLoaded(); - } - ); - } else { - var type = $stateParams.widgetType; - if (!type) { - type = types.widgetType.timeseries.value; - } - widgetService.getWidgetTemplate(type).then( - function success(widgetTemplate) { - vm.widget = angular.copy(widgetTemplate); - vm.widget.widgetName = null; - vm.origWidget = angular.copy(vm.widget); - vm.isDirty = true; - widgetTypeLoaded(); - }, - function fail() { - toast.showError($translate.instant('widget.widget-template-load-failed-error')); - widgetTypeLoaded(); - } - ); - } - }, - function fail() { - toast.showError($translate.instant('widget.widget-type-load-failed-error')); - widgetTypeLoaded(); - } - ); - - } - - function setWidgetType(widgetType) { - vm.widgetType = widgetType; - vm.widget = widgetService.toWidgetInfo(vm.widgetType); - var config = angular.fromJson(vm.widget.defaultConfig); - vm.widget.defaultConfig = angular.toJson(config) - vm.origWidget = angular.copy(vm.widget); - vm.isDirty = false; - } - - function widgetTypeLoaded() { - initHotKeys(); - - initWatchers(); - - angular.element($document[0]).ready(function () { - var w = elem.width(); - if (w > 0) { - initSplitLayout(); - } else { - $scope.$watch( - function () { - return elem[0].offsetWidth || parseInt(elem.css('width'), 10); - }, - function (newSize) { - if (newSize > 0) { - initSplitLayout(); - } - } - ); - } - }); - - iframe.attr('data-widget', angular.toJson(vm.widget)); - iframe.attr('src', '/widget-editor'); - } - - function undoDisabled() { - return $scope.loading - || !vm.isDirty - || !vm.iframeWidgetEditModeInited - || vm.saveWidgetPending - || vm.saveWidgetAsPending; - } - - function saveDisabled() { - return vm.isReadOnly() - || $scope.loading - || !vm.isDirty - || !vm.iframeWidgetEditModeInited - || vm.saveWidgetPending - || vm.saveWidgetAsPending; - } - - function saveAsDisabled() { - return $scope.loading - || !vm.iframeWidgetEditModeInited - || vm.saveWidgetPending - || vm.saveWidgetAsPending; - } - - function initHotKeys() { - $translate(['widget.undo', 'widget.save', 'widget.saveAs', 'widget.toggle-fullscreen', 'widget.run']).then(function (translations) { - hotkeys.bindTo($scope) - .add({ - combo: 'ctrl+q', - description: translations['widget.undo'], - allowIn: ['INPUT', 'SELECT', 'TEXTAREA'], - callback: function (event) { - if (!undoDisabled()) { - event.preventDefault(); - undoWidget(); - } - } - }) - .add({ - combo: 'ctrl+s', - description: translations['widget.save'], - allowIn: ['INPUT', 'SELECT', 'TEXTAREA'], - callback: function (event) { - if (!saveDisabled()) { - event.preventDefault(); - saveWidget(); - } - } - }) - .add({ - combo: 'shift+ctrl+s', - description: translations['widget.saveAs'], - allowIn: ['INPUT', 'SELECT', 'TEXTAREA'], - callback: function (event) { - if (!saveAsDisabled()) { - event.preventDefault(); - saveWidgetAs(); - } - } - }) - .add({ - combo: 'shift+ctrl+f', - description: translations['widget.toggle-fullscreen'], - allowIn: ['INPUT', 'SELECT', 'TEXTAREA'], - callback: function (event) { - event.preventDefault(); - toggleFullscreen(); - } - }) - .add({ - combo: 'ctrl+enter', - description: translations['widget.run'], - allowIn: ['INPUT', 'SELECT', 'TEXTAREA'], - callback: function (event) { - event.preventDefault(); - applyWidgetScript(); - } - }); - }); - } - - function initWatchWidget() { - $scope.widgetWatcher = $scope.$watch('vm.widget', function (newVal, oldVal) { - if (!angular.equals(newVal, oldVal)) { - vm.isDirty = true; - } - }, true); - } - - function initWatchers() { - initWatchWidget(); - - $scope.$watch('vm.widget.type', function (newVal, oldVal) { - if (!angular.equals(newVal, oldVal)) { - var config = angular.fromJson(vm.widget.defaultConfig); - if (vm.widget.type !== types.widgetType.rpc.value - && vm.widget.type !== types.widgetType.alarm.value) { - if (config.targetDeviceAliases) { - delete config.targetDeviceAliases; - } - if (config.alarmSource) { - delete config.alarmSource; - } - if (!config.datasources) { - config.datasources = []; - } - if (!config.timewindow) { - config.timewindow = { - realtime: { - timewindowMs: 60000 - } - }; - } - for (var i = 0; i < config.datasources.length; i++) { - var datasource = config.datasources[i]; - datasource.type = vm.widget.type; - } - } else if (vm.widget.type == types.widgetType.rpc.value) { - if (config.datasources) { - delete config.datasources; - } - if (config.alarmSource) { - delete config.alarmSource; - } - if (config.timewindow) { - delete config.timewindow; - } - if (!config.targetDeviceAliases) { - config.targetDeviceAliases = []; - } - } else { // alarm - if (config.datasources) { - delete config.datasources; - } - if (config.targetDeviceAliases) { - delete config.targetDeviceAliases; - } - if (!config.alarmSource) { - config.alarmSource = {}; - config.alarmSource.type = vm.widget.type - } - if (!config.timewindow) { - config.timewindow = { - realtime: { - timewindowMs: 24 * 60 * 60 * 1000 - } - }; - } - } - vm.widget.defaultConfig = angular.toJson(config); - } - }); - - $scope.$on('widgetEditModeInited', function () { - vm.iframeWidgetEditModeInited = true; - if (vm.saveWidgetPending || vm.saveWidgetAsPending) { - if (!vm.saveWidgetTimeout) { - vm.saveWidgetTimeout = $timeout(function () { - if (!gotError) { - if (vm.saveWidgetPending) { - commitSaveWidget(); - } else if (vm.saveWidgetAsPending) { - commitSaveWidgetAs(); - } - } else { - toast.showError($translate.instant('widget.unable-to-save-widget-error')); - vm.saveWidgetPending = false; - vm.saveWidgetAsPending = false; - initWatchWidget(); - } - vm.saveWidgetTimeout = undefined; - }, 1500); - } - } - }); - - $scope.$on('widgetEditUpdated', function (event, widget) { - vm.widget.sizeX = widget.sizeX / 2; - vm.widget.sizeY = widget.sizeY / 2; - vm.widget.defaultConfig = angular.toJson(widget.config); - iframe.attr('data-widget', angular.toJson(vm.widget)); - }); - - $scope.$on('widgetException', function (event, details) { - if (!gotError) { - gotError = true; - var errorInfo = 'Error:'; - if (details.name) { - errorInfo += ' ' + details.name + ':'; - } - if (details.message) { - errorInfo += ' ' + details.message; - } - if (details.lineNumber) { - errorInfo += '
    Line ' + details.lineNumber; - if (details.columnNumber) { - errorInfo += ' column ' + details.columnNumber; - } - errorInfo += ' of script.'; - } - if (!vm.saveWidgetPending && !vm.saveWidgetAsPending) { - toast.showError(errorInfo, $('#javascript_panel', $element)[0]); - } - if (js_editor && details.lineNumber) { - var line = details.lineNumber - 1; - var column = 0; - if (details.columnNumber) { - column = details.columnNumber; - } - - var errorMarkerId = js_editor.session.addMarker(new Range(line, 0, line, Infinity), "ace_active-line", "screenLine"); - errorMarkers.push(errorMarkerId); - var annotations = js_editor.session.getAnnotations(); - var errorAnnotation = { - row: line, - column: column, - text: details.message, - type: "error" - }; - errorAnnotationId = annotations.push(errorAnnotation) - 1; - js_editor.session.setAnnotations(annotations); - } - } - }); - } - - function cleanupJsErrors() { - toast.hide(); - for (var i = 0; i < errorMarkers.length; i++) { - js_editor.session.removeMarker(errorMarkers[i]); - } - errorMarkers = []; - if (errorAnnotationId && errorAnnotationId > -1) { - var annotations = js_editor.session.getAnnotations(); - annotations.splice(errorAnnotationId, 1); - js_editor.session.setAnnotations(annotations); - errorAnnotationId = -1; - } - } - - function onDividerDrag() { - for (var i = 0; i < ace_editors.length; i++) { - var ace = ace_editors[i]; - ace.resize(); - ace.renderer.updateFull(); - } - } - - function initSplitLayout() { - if (!vm.layoutInited) { - Split([$('#top_panel', $element)[0], $('#bottom_panel', $element)[0]], { - sizes: [35, 65], - gutterSize: 8, - cursor: 'row-resize', - direction: 'vertical', - onDrag: function () { - onDividerDrag() - } - }); - - Split([$('#top_left_panel', $element)[0], $('#top_right_panel', $element)[0]], { - sizes: [50, 50], - gutterSize: 8, - cursor: 'col-resize', - onDrag: function () { - onDividerDrag() - } - }); - - Split([$('#javascript_panel', $element)[0], $('#frame_panel', $element)[0]], { - sizes: [50, 50], - gutterSize: 8, - cursor: 'col-resize', - onDrag: function () { - onDividerDrag() - } - }); - - onDividerDrag(); - - $scope.$applyAsync(function () { - vm.layoutInited = true; - $rootScope.loading = false; - var w = angular.element($window); - $timeout(function () { - w.triggerHandler('resize') - }); - }); - - } - } - - function removeResource(index) { - if (index > -1) { - vm.widget.resources.splice(index, 1); - } - } - - function addResource() { - vm.widget.resources.push({url: ''}); - } - - function applyWidgetScript() { - cleanupJsErrors(); - gotError = false; - vm.iframeWidgetEditModeInited = false; - var config = angular.fromJson(vm.widget.defaultConfig); - config.title = vm.widget.widgetName; - vm.widget.defaultConfig = angular.toJson(config); - iframe.attr('data-widget', angular.toJson(vm.widget)); - iframe[0].contentWindow.location.reload(true); - } - - function toggleFullscreen() { - vm.fullscreen = !vm.fullscreen; - } - - function isReadOnly() { - if (userService.getAuthority() === 'TENANT_ADMIN') { - return !vm.widgetsBundle || vm.widgetsBundle.tenantId.id === types.id.nullUid; - } else { - return userService.getAuthority() != 'SYS_ADMIN'; - } - } - - function undoWidget() { - if ($scope.widgetWatcher) { - $scope.widgetWatcher(); - } - vm.widget = angular.copy(vm.origWidget); - vm.isDirty = false; - initWatchWidget(); - applyWidgetScript(); - } - - function saveWidget() { - if (!vm.widget.widgetName) { - toast.showError($translate.instant('widget.missing-widget-title-error')); - } else { - $scope.widgetWatcher(); - vm.saveWidgetPending = true; - applyWidgetScript(); - } - } - - function saveWidgetAs($event) { - $scope.widgetWatcher(); - vm.saveWidgetAsPending = true; - vm.saveWidgetAsEvent = $event; - applyWidgetScript(); - } - - function commitSaveWidget() { - var id = (vm.widgetType && vm.widgetType.id) ? vm.widgetType.id : undefined; - widgetService.saveWidgetType(vm.widget, id, vm.widgetsBundle.alias).then( - function success(widgetType) { - setWidgetType(widgetType) - vm.saveWidgetPending = false; - initWatchWidget(); - toast.showSuccess($translate.instant('widget.widget-saved'), 500); - }, function fail() { - vm.saveWidgetPending = false; - initWatchWidget(); - } - ); - } - - function commitSaveWidgetAs() { - $mdDialog.show({ - controller: 'SaveWidgetTypeAsController', - controllerAs: 'vm', - templateUrl: saveWidgetTypeAsTemplate, - parent: angular.element($document[0].body), - fullscreen: true, - targetEvent: vm.saveWidgetAsEvent - }).then(function (saveWidgetAsData) { - vm.widget.widgetName = saveWidgetAsData.widgetName; - vm.widget.alias = undefined; - var config = angular.fromJson(vm.widget.defaultConfig); - config.title = vm.widget.widgetName; - vm.widget.defaultConfig = angular.toJson(config); - - vm.saveWidgetAsPending = false; - vm.isDirty = false; - initWatchWidget(); - widgetService.saveWidgetType(vm.widget, undefined, saveWidgetAsData.bundleAlias).then( - function success(widgetType) { - $state.go('home.widgets-bundles.widget-types.widget-type', - {widgetsBundleId: saveWidgetAsData.bundleId, widgetTypeId: widgetType.id.id}); - }, - function fail() { - vm.saveWidgetAsPending = false; - initWatchWidget(); - } - ); - }, function () { - vm.saveWidgetAsPending = false; - initWatchWidget(); - }); - } - - function beautifyJs() { - var res = js_beautify(vm.widget.controllerScript, {indent_size: 4, wrap_line_length: 60}); - vm.widget.controllerScript = res; - } - - function beautifyHtml() { - var res = html_beautify(vm.widget.templateHtml, {indent_size: 4, wrap_line_length: 60}); - vm.widget.templateHtml = res; - } - - function beautifyCss() { - var res = css_beautify(vm.widget.templateCss, {indent_size: 4}); - vm.widget.templateCss = res; - } - - function beautifyJson() { - var res = js_beautify(vm.widget.settingsSchema, {indent_size: 4}); - vm.widget.settingsSchema = res; - } - - function beautifyDataKeyJson() { - var res = js_beautify(vm.widget.dataKeySettingsSchema, {indent_size: 4}); - vm.widget.dataKeySettingsSchema = res; - } - -} - -/* eslint-enable angular/angularelement */ diff --git a/ui/src/app/widget/widget-editor.scss b/ui/src/app/widget/widget-editor.scss deleted file mode 100644 index 9cd804803d..0000000000 --- a/ui/src/app/widget/widget-editor.scss +++ /dev/null @@ -1,160 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "~compass-sass-mixins/lib/compass"; - -$edit-toolbar-height: 40px !default; - -.tb-editor { - .tb-split { - box-sizing: border-box; - overflow-x: hidden; - overflow-y: auto; - } - - .ace_editor { - font-size: 14px !important; - } - - .tb-content { - border: 1px solid #c0c0c0; - } - - .gutter { - background-color: transparent; - - background-repeat: no-repeat; - background-position: 50%; - } - - .gutter.gutter-horizontal { - cursor: col-resize; - background-image: url("../../../node_modules/split.js/grips/vertical.png"); - } - - .gutter.gutter-vertical { - cursor: row-resize; - background-image: url("../../../node_modules/split.js/grips/horizontal.png"); - } - - .tb-split.tb-split-horizontal, - .gutter.gutter-horizontal { - float: left; - height: 100%; - } - - .tb-split.tb-split-vertical { - display: flex; - - .tb-split.tb-content { - height: 100%; - } - } - - .container{ - width: 100%; - height: 100%; - } -} - -.tb-split-vertical { - md-tabs { - md-tabs-content-wrapper { - height: calc(100% - 49px); - - md-tab-content { - height: 100%; - - & > div { - height: 100%; - } - } - } - } -} - -div.tb-editor-area-title-panel { - position: absolute; - top: 5px; - right: 20px; - z-index: 5; - font-size: .8rem; - font-weight: 500; - - label { - padding: 4px; - color: #00acc1; - text-transform: uppercase; - background: rgba(220, 220, 220, .35); - border-radius: 5px; - } - - .md-button { - min-width: 32px; - min-height: 15px; - padding: 4px; - margin: 0; - font-size: .8rem; - line-height: 15px; - color: #7b7b7b; - background: rgba(220, 220, 220, .35); - } -} - -.tb-resize-container { - position: relative; - width: 100%; - height: 100%; - overflow-y: auto; - - .ace_editor { - height: 100%; - } -} - -md-toolbar.tb-edit-toolbar { - - min-height: $edit-toolbar-height !important; - max-height: $edit-toolbar-height !important; - - .md-toolbar-tools { - min-height: $edit-toolbar-height !important; - max-height: $edit-toolbar-height !important; - - .md-button { - min-width: 65px; - min-height: 30px; - font-size: 12px; - line-height: 30px; - - md-icon { - font-size: 20px; - } - - span { - padding-right: 6px; - } - } - - md-input-container { - input { - height: 28px; - font-size: 1.2rem; - font-weight: 400; - letter-spacing: .005em; - } - } - } -} diff --git a/ui/src/app/widget/widget-editor.tpl.html b/ui/src/app/widget/widget-editor.tpl.html deleted file mode 100644 index 7e532504f2..0000000000 --- a/ui/src/app/widget/widget-editor.tpl.html +++ /dev/null @@ -1,256 +0,0 @@ - -
    -
    - -
    - - - - - - - - {{ type.name | translate }} - - - - - - - {{ 'widget.run' | translate }} (CTRL + Return) - - play_arrow - action.run - - - - {{ 'widget.undo' | translate }} (CTRL + Q) - - undo - action.undo - - - - {{ 'widget.save' | translate }} (CTRL + S) - - save - action.save - - - - {{ 'widget.saveAs' | translate }} (Shift + CTRL + S) - - save - action.saveAs - - - - {{ 'widget.toggle-fullscreen' | translate }} (Shift + CTRL + F) - - - fullscreen - - - fullscreen_exit - - widget.toggle-fullscreen - - - - more_vert - - - - - play_arrow - action.run - - - - - undo - action.undo - - - - - save - action.save - - - - - save - action.saveAs - - - - -
    -
    -
    -
    -
    -
    - - -
    -
    -
    - - - - - - {{ 'widget.remove-resource' | translate }} - - - close - - -
    -
    - - - {{ 'widget.add-resource' | translate }} - - action.add - -
    -
    -
    -
    - -
    -
    - {{ 'widget.tidy' | translate }} - - -
    -
    -
    -
    - -
    -
    - {{ 'widget.tidy' | translate }} - - -
    -
    -
    -
    -
    -
    -
    - - -
    -
    - {{ 'widget.tidy' | translate }} - - -
    -
    -
    -
    - -
    -
    - {{ 'widget.tidy' | translate }} - - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - - {{ - 'widget.tidy' | translate }} - - -
    -
    -
    -
    -
    - - - -
    - - -
    -
    -
    -
    -
    -
    - - - -
    diff --git a/ui/src/app/widget/widget-library.controller.js b/ui/src/app/widget/widget-library.controller.js deleted file mode 100644 index 913cb587d1..0000000000 --- a/ui/src/app/widget/widget-library.controller.js +++ /dev/null @@ -1,221 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import AliasController from '../api/alias-controller'; - -/* eslint-disable import/no-unresolved, import/default */ - -import selectWidgetTypeTemplate from './select-widget-type.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function WidgetLibraryController($scope, $rootScope, $q, widgetService, userService, importExport, - $state, $stateParams, $document, $mdDialog, $translate, $filter, - utils, types, entityService) { - - var vm = this; - - var widgetsBundleId = $stateParams.widgetsBundleId; - - vm.widgetsBundle; - vm.widgetTypes = []; - vm.dashboardInitComplete = false; - - var stateController = { - getStateParams: function() { - return {}; - } - }; - vm.aliasController = new AliasController($scope, $q, $filter, utils, - types, entityService, stateController, {}); - - vm.noData = noData; - vm.dashboardInited = dashboardInited; - vm.dashboardInitFailed = dashboardInitFailed; - vm.addWidgetType = addWidgetType; - vm.openWidgetType = openWidgetType; - vm.exportWidgetType = exportWidgetType; - vm.importWidgetType = importWidgetType; - vm.removeWidgetType = removeWidgetType; - vm.loadWidgetLibrary = loadWidgetLibrary; - vm.addWidgetType = addWidgetType; - vm.isReadOnly = isReadOnly; - - function loadWidgetLibrary() { - var deferred = $q.defer(); - $rootScope.loading = true; - widgetService.getWidgetsBundle(widgetsBundleId).then( - function success(widgetsBundle) { - vm.widgetsBundle = widgetsBundle; - if (vm.widgetsBundle) { - var bundleAlias = vm.widgetsBundle.alias; - var isSystem = vm.widgetsBundle.tenantId.id === types.id.nullUid; - - widgetService.getBundleWidgetTypes(bundleAlias, isSystem).then( - function (widgetTypes) { - - widgetTypes = $filter('orderBy')(widgetTypes, ['-descriptor.type','-createdTime']); - - var top = 0; - var lastTop = [0, 0, 0]; - var col = 0; - var column = 0; - - if (widgetTypes.length > 0) { - loadNext(0); - } else { - $rootScope.loading = false; - deferred.resolve(); - } - - function loadNextOrComplete(i) { - i++; - if (i < widgetTypes.length) { - loadNext(i); - } else { - $rootScope.loading = false; - deferred.resolve(); - } - } - - function loadNext(i) { - var widgetType = widgetTypes[i]; - $scope.$applyAsync(function() { - var widgetTypeInfo = widgetService.toWidgetInfo(widgetType); - var sizeX = 8; - var sizeY = Math.floor(widgetTypeInfo.sizeY); - var widget = { - typeId: widgetType.id, - isSystemType: isSystem, - bundleAlias: bundleAlias, - typeAlias: widgetTypeInfo.alias, - type: widgetTypeInfo.type, - title: widgetTypeInfo.widgetName, - sizeX: sizeX, - sizeY: sizeY, - row: top, - col: col, - config: angular.fromJson(widgetTypeInfo.defaultConfig) - }; - widget.config.title = widgetTypeInfo.widgetName; - vm.widgetTypes.push(widget); - top+=sizeY; - if (top > lastTop[column] + 10) { - lastTop[column] = top; - column++; - if (column > 2) { - column = 0; - } - top = lastTop[column]; - col = column * 8; - } - loadNextOrComplete(i); - }); - } - } - ); - } else { - $rootScope.loading = false; - deferred.resolve(); - } - }, function fail() { - $rootScope.loading = false; - deferred.reject(); - } - ); - return deferred.promise; - } - - function noData() { - return vm.dashboardInitComplete && vm.widgetTypes.length == 0; - } - - function dashboardInitFailed() { - vm.dashboardInitComplete = true; - } - - function dashboardInited() { - vm.dashboardInitComplete = true; - } - - function addWidgetType($event) { - vm.openWidgetType($event); - } - - function isReadOnly() { - if (userService.getAuthority() === 'TENANT_ADMIN') { - return !vm.widgetsBundle || vm.widgetsBundle.tenantId.id === types.id.nullUid; - } else { - return userService.getAuthority() != 'SYS_ADMIN'; - } - } - - function openWidgetType(event, widget) { - if (event) { - event.stopPropagation(); - } - if (widget) { - $state.go('home.widgets-bundles.widget-types.widget-type', - {widgetTypeId: widget.typeId.id}); - } else { - $mdDialog.show({ - controller: 'SelectWidgetTypeController', - controllerAs: 'vm', - templateUrl: selectWidgetTypeTemplate, - parent: angular.element($document[0].body), - fullscreen: true, - targetEvent: event - }).then(function (widgetType) { - $state.go('home.widgets-bundles.widget-types.widget-type', - {widgetType: widgetType}); - }, function () { - }); - } - } - - function exportWidgetType(event, widget) { - event.stopPropagation(); - importExport.exportWidgetType(widget.typeId.id); - } - - function importWidgetType($event) { - $event.stopPropagation(); - importExport.importWidgetType($event, vm.widgetsBundle.alias).then( - function success() { - $state.go($state.current, $state.params, {reload: true}); - }, - function fail() {} - ); - } - - function removeWidgetType(event, widget) { - var confirm = $mdDialog.confirm() - .targetEvent(event) - .title($translate.instant('widget.remove-widget-type-title', {widgetName: widget.config.title})) - .htmlContent($translate.instant('widget.remove-widget-type-text')) - .ariaLabel($translate.instant('widget.remove-widget-type')) - .cancel($translate.instant('action.no')) - .ok($translate.instant('action.yes')); - $mdDialog.show(confirm).then(function () { - widgetService.deleteWidgetType(widget.bundleAlias, widget.typeAlias, widget.isSystemType).then( - function success() { - vm.widgetTypes.splice(vm.widgetTypes.indexOf(widget), 1); - }, - function fail() {} - ); - }); - } -} diff --git a/ui/src/app/widget/widget-library.routes.js b/ui/src/app/widget/widget-library.routes.js deleted file mode 100644 index 28217ac7da..0000000000 --- a/ui/src/app/widget/widget-library.routes.js +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import widgetLibraryTemplate from './widget-library.tpl.html'; -import widgetEditorTemplate from './widget-editor.tpl.html'; -import dashboardTemplate from '../dashboard/dashboard.tpl.html'; -import widgetsBundlesTemplate from './widgets-bundles.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function WidgetLibraryRoutes($stateProvider) { - $stateProvider - .state('home.widgets-bundles', { - url: '/widgets-bundles', - params: {'topIndex': 0}, - module: 'private', - auth: ['SYS_ADMIN', 'TENANT_ADMIN'], - views: { - "content@home": { - templateUrl: widgetsBundlesTemplate, - controller: 'WidgetsBundleController', - controllerAs: 'vm' - } - }, - data: { - searchEnabled: true, - pageTitle: 'widgets-bundle.widgets-bundles' - }, - ncyBreadcrumb: { - label: '{"icon": "now_widgets", "label": "widgets-bundle.widgets-bundles"}' - } - }) - .state('home.widgets-bundles.widget-types', { - url: '/:widgetsBundleId/widgetTypes', - params: {'topIndex': 0}, - module: 'private', - auth: ['SYS_ADMIN', 'TENANT_ADMIN'], - views: { - "content@home": { - templateUrl: widgetLibraryTemplate, - controller: 'WidgetLibraryController', - controllerAs: 'vm' - } - }, - data: { - searchEnabled: false, - pageTitle: 'widget.widget-library' - }, - ncyBreadcrumb: { - label: '{"icon": "now_widgets", "label": "{{ vm.widgetsBundle.title }}", "translate": "false"}' - } - }) - .state('home.widgets-bundles.widget-types.widget-type', { - url: '/:widgetTypeId', - module: 'private', - auth: ['SYS_ADMIN', 'TENANT_ADMIN'], - views: { - "content@home": { - templateUrl: widgetEditorTemplate, - controller: 'WidgetEditorController', - controllerAs: 'vm' - } - }, - params: { - widgetType: null - }, - data: { - searchEnabled: false, - pageTitle: 'widget.editor' - }, - ncyBreadcrumb: { - label: '{"icon": "insert_chart", "label": "{{ vm.widget.widgetName }}", "translate": "false"}' - } - }) - .state('widgetEditor', { - url: '/widget-editor', - module: 'private', - auth: ['SYS_ADMIN', 'TENANT_ADMIN'], - views: { - "@": { - templateUrl: dashboardTemplate, - controller: 'DashboardController', - controllerAs: 'vm' - } - }, - data: { - widgetEditMode: true, - searchEnabled: false, - pageTitle: 'widget.editor' - } - }) -} diff --git a/ui/src/app/widget/widget-library.tpl.html b/ui/src/app/widget/widget-library.tpl.html deleted file mode 100644 index c496430282..0000000000 --- a/ui/src/app/widget/widget-library.tpl.html +++ /dev/null @@ -1,76 +0,0 @@ - -
    - - add - {{ 'widget.add-widget-type' | translate }} - - widgets-bundle.empty -
    - - - diff --git a/ui/src/app/widget/widgets-bundle-card.tpl.html b/ui/src/app/widget/widgets-bundle-card.tpl.html deleted file mode 100644 index 20d7d12c88..0000000000 --- a/ui/src/app/widget/widgets-bundle-card.tpl.html +++ /dev/null @@ -1,18 +0,0 @@ - -
    widgets-bundle.system
    diff --git a/ui/src/app/widget/widgets-bundle-fieldset.tpl.html b/ui/src/app/widget/widgets-bundle-fieldset.tpl.html deleted file mode 100644 index c88a1e8d45..0000000000 --- a/ui/src/app/widget/widgets-bundle-fieldset.tpl.html +++ /dev/null @@ -1,35 +0,0 @@ - -{{ 'widgets-bundle.export' | translate }} -{{ 'widgets-bundle.delete' | translate }} - - -
    - - - -
    -
    widgets-bundle.title-required
    -
    -
    -
    -
    diff --git a/ui/src/app/widget/widgets-bundle.controller.js b/ui/src/app/widget/widgets-bundle.controller.js deleted file mode 100644 index da9a8b37f2..0000000000 --- a/ui/src/app/widget/widgets-bundle.controller.js +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import addWidgetsBundleTemplate from './add-widgets-bundle.tpl.html'; -import widgetsBundleCard from './widgets-bundle-card.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function WidgetsBundleController(widgetService, userService, importExport, $state, $stateParams, $filter, $translate, types) { - - var widgetsBundleActionsList = [ - { - onAction: function ($event, item) { - exportWidgetsBundle($event, item); - }, - name: function() { $translate.instant('action.export') }, - details: function() { return $translate.instant('widgets-bundle.export') }, - icon: "file_download" - }, - { - onAction: function ($event, item) { - vm.grid.openItem($event, item); - }, - name: function() { return $translate.instant('widgets-bundle.details') }, - details: function() { return $translate.instant('widgets-bundle.widgets-bundle-details') }, - icon: "edit" - }, - { - onAction: function ($event, item) { - vm.grid.deleteItem($event, item); - }, - name: function() { return $translate.instant('action.delete') }, - details: function() { return $translate.instant('widgets-bundle.delete') }, - icon: "delete", - isEnabled: isWidgetsBundleEditable - } - ]; - - var widgetsBundleAddItemActionsList = [ - { - onAction: function ($event) { - vm.grid.addItem($event); - }, - name: function() { return $translate.instant('action.create') }, - details: function() { return $translate.instant('widgets-bundle.create-new-widgets-bundle') }, - icon: "insert_drive_file" - }, - { - onAction: function ($event) { - importExport.importWidgetsBundle($event).then( - function() { - vm.grid.refreshList(); - } - ); - }, - name: function() { return $translate.instant('action.import') }, - details: function() { return $translate.instant('widgets-bundle.import') }, - icon: "file_upload" - } - ]; - - var vm = this; - - vm.types = types; - - vm.widgetsBundleGridConfig = { - - refreshParamsFunc: null, - - deleteItemTitleFunc: deleteWidgetsBundleTitle, - deleteItemContentFunc: deleteWidgetsBundleText, - deleteItemsTitleFunc: deleteWidgetsBundlesTitle, - deleteItemsActionTitleFunc: deleteWidgetsBundlesActionTitle, - deleteItemsContentFunc: deleteWidgetsBundlesText, - - fetchItemsFunc: fetchWidgetsBundles, - saveItemFunc: saveWidgetsBundle, - clickItemFunc: openWidgetsBundle, - deleteItemFunc: deleteWidgetsBundle, - - getItemTitleFunc: getWidgetsBundleTitle, - itemCardTemplateUrl: widgetsBundleCard, - parentCtl: vm, - - actionsList: widgetsBundleActionsList, - addItemActions: widgetsBundleAddItemActionsList, - - onGridInited: gridInited, - - addItemTemplateUrl: addWidgetsBundleTemplate, - - addItemText: function() { return $translate.instant('widgets-bundle.add-widgets-bundle-text') }, - noItemsText: function() { return $translate.instant('widgets-bundle.no-widgets-bundles-text') }, - itemDetailsText: function() { return $translate.instant('widgets-bundle.widgets-bundle-details') }, - isSelectionEnabled: isWidgetsBundleEditable, - isDetailsReadOnly: function(widgetsBundle) { - return !isWidgetsBundleEditable(widgetsBundle); - } - - }; - - if (angular.isDefined($stateParams.items) && $stateParams.items !== null) { - vm.widgetsBundleGridConfig.items = $stateParams.items; - } - - if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) { - vm.widgetsBundleGridConfig.topIndex = $stateParams.topIndex; - } - - vm.exportWidgetsBundle = exportWidgetsBundle; - - function deleteWidgetsBundleTitle(widgetsBundle) { - return $translate.instant('widgets-bundle.delete-widgets-bundle-title', {widgetsBundleTitle: widgetsBundle.title}); - } - - function deleteWidgetsBundleText() { - return $translate.instant('widgets-bundle.delete-widgets-bundle-text'); - } - - function deleteWidgetsBundlesTitle(selectedCount) { - return $translate.instant('widgets-bundle.delete-widgets-bundles-title', {count: selectedCount}, 'messageformat'); - } - - function deleteWidgetsBundlesActionTitle(selectedCount) { - return $translate.instant('widgets-bundle.delete-widgets-bundles-action-title', {count: selectedCount}, 'messageformat'); - } - - function deleteWidgetsBundlesText() { - return $translate.instant('widgets-bundle.delete-widgets-bundles-text'); - } - - function gridInited(grid) { - vm.grid = grid; - } - - function fetchWidgetsBundles(pageLink) { - return widgetService.getAllWidgetsBundlesByPageLink(pageLink); - } - - function saveWidgetsBundle(widgetsBundle) { - return widgetService.saveWidgetsBundle(widgetsBundle); - } - - function deleteWidgetsBundle(widgetsBundleId) { - return widgetService.deleteWidgetsBundle(widgetsBundleId); - } - - function getWidgetsBundleTitle(widgetsBundle) { - return widgetsBundle ? widgetsBundle.title : ''; - } - - function isWidgetsBundleEditable(widgetsBundle) { - if (userService.getAuthority() === 'TENANT_ADMIN') { - return widgetsBundle && widgetsBundle.tenantId.id != types.id.nullUid; - } else { - return userService.getAuthority() === 'SYS_ADMIN'; - } - } - - function exportWidgetsBundle($event, widgetsBundle) { - $event.stopPropagation(); - importExport.exportWidgetsBundle(widgetsBundle.id.id); - } - - function openWidgetsBundle($event, widgetsBundle) { - if ($event) { - $event.stopPropagation(); - } - $state.go('home.widgets-bundles.widget-types', {widgetsBundleId: widgetsBundle.id.id}); - } - -} diff --git a/ui/src/app/widget/widgets-bundle.directive.js b/ui/src/app/widget/widgets-bundle.directive.js deleted file mode 100644 index 24dd72f8e4..0000000000 --- a/ui/src/app/widget/widgets-bundle.directive.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable import/no-unresolved, import/default */ - -import widgetsBundleFieldsetTemplate from './widgets-bundle-fieldset.tpl.html'; - -/* eslint-enable import/no-unresolved, import/default */ - -/*@ngInject*/ -export default function WidgetsBundleDirective($compile, $templateCache) { - var linker = function (scope, element) { - var template = $templateCache.get(widgetsBundleFieldsetTemplate); - element.html(template); - $compile(element.contents())(scope); - } - return { - restrict: "E", - link: linker, - scope: { - widgetsBundle: '=', - isEdit: '=', - isReadOnly: '=', - theForm: '=', - onExportWidgetsBundle: '&', - onDeleteWidgetsBundle: '&' - } - }; -} diff --git a/ui/src/app/widget/widgets-bundles.tpl.html b/ui/src/app/widget/widgets-bundles.tpl.html deleted file mode 100644 index 2aa56f3211..0000000000 --- a/ui/src/app/widget/widgets-bundles.tpl.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - diff --git a/ui/src/index.html b/ui/src/index.html deleted file mode 100644 index 070de5d17a..0000000000 --- a/ui/src/index.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/ui/src/scss/constants.scss b/ui/src/scss/constants.scss deleted file mode 100644 index 98865623c1..0000000000 --- a/ui/src/scss/constants.scss +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "~sass-material-colors/sass/sass-material-colors"; - -// Colors - -$gray: #eee !default; - -$primary-palette-color: "indigo" !default; -$default: "500" !default; -$hue-1: "300" !default; -$hue-2: "800" !default; -$hue-3: "a100" !default; - -$primary-default: #305680 !default; //material-color($primary-palette-color, $default); -$primary-hue-1: material-color($primary-palette-color, $hue-1) !default; -$primary-hue-2: material-color($primary-palette-color, $hue-2) !default; -$primary-hue-3: rgb(207, 216, 220) !default; - -// Layout - -$layout-breakpoint-xs: 600px !default; -$layout-breakpoint-sm: 960px !default; -$layout-breakpoint-md: 1280px !default; -$layout-breakpoint-xmd: 1600px !default; -$layout-breakpoint-lg: 1920px !default; - -$layout-breakpoint-gt-xs: 601px !default; -$layout-breakpoint-gt-sm: 961px !default; -$layout-breakpoint-gt-md: 1281px !default; -$layout-breakpoint-gt-xmd: 1601px !default; -$layout-breakpoint-gt-lg: 1921px !default; diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss deleted file mode 100644 index 835df73894..0000000000 --- a/ui/src/scss/main.scss +++ /dev/null @@ -1,749 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "~compass-sass-mixins/lib/compass"; - -@import "constants"; - -@import "animations"; - -@import "mixins"; - -@import "fonts"; - -/*************** - * TYPE DEFAULTS - ***************/ - -button, -html, -input, -select, -textarea { - font-family: Roboto, "Helvetica Neue", sans-serif; -} - -.mdi-set { - line-height: 1; - text-transform: none; - letter-spacing: normal; - word-wrap: normal; - white-space: nowrap; - direction: ltr; - -webkit-font-feature-settings: "liga"; /* stylelint-disable-line property-no-vendor-prefix */ -} - -a { - font-weight: 400; - color: #106cc8; - text-decoration: none; - border-bottom: 1px solid rgba(64, 84, 178, .25); - - transition: border-bottom .35s; -} - -a:hover, -a:focus { - border-bottom: 1px solid #4054b2; -} - -h1, -h2, -h3, -h4, -h5, -h6 { - margin-top: 1rem; - margin-bottom: 1rem; -} - -h1 { - font-size: 3.4rem; - font-weight: 400; - line-height: 4rem; -} - -h2 { - font-size: 2.4rem; - font-weight: 400; - line-height: 3.2rem; -} - -h3 { - font-size: 2rem; - font-weight: 500; - letter-spacing: .005em; -} - -h4 { - font-size: 1.6rem; - font-weight: 400; - line-height: 2.4rem; - letter-spacing: .01em; -} - -p { - margin: .8em 0 1.6em; - font-size: 1.6rem; - font-weight: 400; - line-height: 1.6em; - letter-spacing: .01em; -} - -strong { - font-weight: 500; -} - -blockquote { - padding-left: 16px; - margin-left: 0; - font-style: italic; - border-left: 3px solid rgba(0, 0, 0, .12); -} - -fieldset { - padding: 0; - margin: 0; - border: none; -} - -/********************************* - * MATERIAL DESIGN CUSTOMIZATIONS - ********************************/ - -a.md-button { - border-bottom: none; -} - -form { - md-content { - background-color: #fff; - } -} - -md-bottom-sheet .md-subheader { - font-family: Roboto, "Helvetica Neue", sans-serif; -} - -.md-chips { - font-family: Roboto, "Helvetica Neue", sans-serif; -} - -md-content.md-default-theme, -md-content { - background-color: $gray; -} - -md-card { - background-color: #fff; - - h2:first-of-type { - margin-top: 0; - } -} - -.md-button:not([disabled]).md-icon-button:hover { - background-color: rgba(158, 158, 158, .2); -} - -md-toolbar:not(.md-hue-1), -.md-fab { - fill: #fff; -} - -md-toolbar md-input-container .md-errors-spacer { - min-height: 0; -} - -md-toolbar { - md-select.md-default-theme:not([disabled]):focus .md-select-value, - md-select:not([disabled]):focus .md-select-value { - color: #fff; - } -} - -md-menu-item { - overflow: hidden; - fill: #737373; - - .md-button { - display: block; - - .tb-alt-text { - float: right; - } - } -} - -div.md-toast-text { - width: 100%; - max-width: 500px; - max-height: inherit; - word-wrap: break-word; -} - -md-toast .md-button { - min-width: 88px; -} - -md-sidenav { - overflow: hidden; - fill: #737373; -} - -.md-panel-outer-wrapper { - overflow-y: auto; -} - -.md-radio-interactive input, -button { - pointer-events: all; -} - -.md-color-picker-input-container { - md-input-container { - margin-bottom: 0; - } -} - -/*********************** - * THINGSBOARD SPECIFIC - ***********************/ - -$swift-ease-out-duration: .4s !default; -$swift-ease-out-timing-function: cubic-bezier(.25, .8, .25, 1) !default; - -$input-label-float-offset: 6px !default; -$input-label-float-scale: .75 !default; - -label { - &.tb-title { - padding-bottom: 15px; - font-size: 13px; - font-weight: 400; - color: #666; - pointer-events: none; - - &.no-padding { - padding-bottom: 0; - } - - &.tb-required::after { - font-size: 13px; - color: rgba(0, 0, 0, .54); - vertical-align: top; - content: " *"; - } - - &.tb-error { - color: rgb(221, 44, 0); - - &.tb-required::after { - color: rgb(221, 44, 0); - } - } - } -} - -.tb-noselect { - user-select: none; -} - -.tb-readonly-label { - color: rgba(0, 0, 0, .54); -} - -.tb-disabled-label { - color: rgba(0, 0, 0, .44); -} - -/* stylelint-disable-next-line no-duplicate-selectors */ -label { - &.tb-small { - font-size: 12px; - color: rgba(0, 0, 0, .54); - pointer-events: none; - } -} - -div { - &.tb-small { - font-size: 14px; - color: rgba(0, 0, 0, .54); - } -} - -.tb-hint { - padding-bottom: 15px; - font-size: 12px; - line-height: 14px; - color: #808080; - transition: all .3s cubic-bezier(.55, 0, .55, .2); - - &.ng-hide, - &.ng-enter, - &.ng-leave.ng-leave-active { - bottom: 26px; - opacity: 0; - } - - &.ng-leave, - &.ng-enter.ng-enter-active { - bottom: 7px; - opacity: 1; - } -} - -.md-caption { - &.tb-required::after { - font-size: 10px; - color: rgba(0, 0, 0, .54); - vertical-align: top; - content: " *"; - } -} - -pre.tb-highlight { - display: block; - padding: 15px; - margin: 20px 0; - overflow-x: auto; - background-color: #f7f7f7; - - code { - box-sizing: border-box; - display: inline-block; - padding: 0; - font-family: monospace; - font-size: 16px; - font-weight: 700; - color: #303030; - vertical-align: bottom; - } -} - -.tb-notice { - padding: 15px; - font-size: 16px; - background-color: #f7f7f7; - border: 1px solid #ccc; -} - -.tb-data-table { - md-toolbar { - z-index: 0; - } - - md-toolbar.md-table-toolbar.md-default-theme:not(.md-menu-toolbar).md-default .md-button[disabled], - md-toolbar.md-table-toolbar:not(.md-menu-toolbar).md-default .md-button[disabled] { - color: rgba(0, 0, 0, .38); - } - - md-toolbar.md-default-theme:not(.md-menu-toolbar) .md-button[disabled] md-icon, - md-toolbar:not(.md-menu-toolbar) .md-button[disabled] md-icon { - color: rgba(0, 0, 0, .28); - } - - span.no-data-found { - position: relative; - display: flex; - height: calc(100% - 57px); - text-transform: uppercase; - } - - table.md-table { - &.md-row-select td.md-cell, - &.md-row-select th.md-column { - &:first-child { - width: 20px; - padding: 0 0 0 12px; - } - - &:nth-child(2) { - padding: 0 12px; - } - - &:nth-child(n+3):nth-last-child(n+2) { - padding: 0 28px 0 0; - } - } - - &:not(.md-row-select) td.md-cell, - &:not(.md-row-select) th.md-column { - &:first-child { - padding: 0 12px; - } - - &:nth-child(n+2):nth-last-child(n+2) { - padding: 0 28px 0 0; - } - } - - td.md-cell, - th.md-column { - &:last-child { - padding: 0 12px 0 0; - } - } - } - - table.md-table, - table.md-table.md-row-select { - tbody { - &.md-body { - tr { - &.md-row:not([disabled]) { - outline: none; - - &:hover { - background-color: rgba(221, 221, 221, .3) !important; - } - - &.md-selected { - background-color: rgba(221, 221, 221, .5) !important; - } - - &.tb-current, - &.tb-current:hover{ - background-color: rgba(221, 221, 221, .65) !important; - } - } - } - } - - tr { - td { - &.tb-action-cell { - width: 72px; - min-width: 72px; - max-width: 72px; - white-space: nowrap; - - .md-button { - &.md-icon-button { - width: 36px; - height: 36px; - padding: 6px; - margin: 0; - } - } - - .tb-spacer { - padding-left: 38px; - } - } - } - } - } - } -} - -.tb-severity { - font-weight: 700; - - &.tb-critical { - color: #f00 !important; - } - - &.tb-major { - color: #ffa500 !important; - } - - &.tb-minor { - color: #ffca3d !important; - } - - &.tb-warning { - color: #abab00 !important; - } - - &.tb-indeterminate { - color: #008000 !important; - } -} - -.tb-card-description { - font-size: 13px; - color: rgba(0, 0, 0, .54); - - @include line-clamp(2, 1.1); -} - -.tb-tooltip-multiline { - max-width: 400px; - height: auto !important; - padding-top: 6px; - padding-bottom: 6px; - line-height: 1.5; - white-space: normal !important; -} - -/*********************** - * Flow - ***********************/ -$previewSize: 100px !default; - -.file-input { - display: none; -} - -.tb-flow-drop { - position: relative; - height: $previewSize; - overflow: hidden; - border: dashed 2px; - - label { - display: flex; - flex-direction: column; - justify-content: center; - width: 100%; - height: 100%; - font-size: 16px; - text-align: center; - - @media (min-width: $layout-breakpoint-sm) { - font-size: 24px; - } - } -} - -.tb-container { - position: relative; - padding: 10px 0; - margin-top: 32px; -} - -/*********************** - * Prompt - ***********************/ - -.tb-prompt { - display: flex; - font-size: 18px; - font-weight: 400; - line-height: 18px; - color: rgba(0, 0, 0, .38); - text-transform: uppercase; -} - -/*********************** - * Errors - ***********************/ - -.tb-error-messages { - height: 24px; //30px -} - -.tb-error-message { - padding: 10px 0 0 10px; - margin-top: -6px; - overflow: hidden; - font-size: 12px; - line-height: 14px; - color: rgb(221, 44, 0); -} - -.tb-error-message.ng-animate { - transition: all .3s cubic-bezier(.55, 0, .55, .2); -} - -.tb-error-message.ng-enter-prepare, -.tb-error-message.ng-enter { - margin-top: -24px; - opacity: 0; -} - -.tb-error-message.ng-enter.ng-enter-active { - margin-top: -6px; - opacity: 1; -} - -.tb-error-message.ng-leave { - margin-top: -6px; - opacity: 1; -} - -.tb-error-message.ng-leave.ng-leave-active { - margin-top: -24px; - opacity: 0; -} - -/*********************** - * Tabs - ***********************/ - -md-tabs.tb-headless { - margin-top: -50px; -} - -/*********************** - * Buttons - ***********************/ - -.md-button.tb-card-button { - width: 100%; - max-width: 240px; - height: 100%; - - span { - height: 18px; - min-height: 18px; - max-height: 18px; - padding: 0 0 20px 0; - margin: auto; - font-size: 18px; - font-weight: 400; - line-height: 18px; - white-space: normal; - } -} - -.md-button.tb-layout-button { - width: 100%; - max-width: 240px; - height: 100%; - - span { - padding: 40px; - font-size: 18px; - font-weight: 400; - line-height: 18px; - white-space: normal; - } -} - -.md-button.tb-add-new-widget { - padding-right: 12px; - font-size: 24px; - border-style: dashed; - border-width: 2px; -} - -/*********************** - * Header buttons - ***********************/ - -section.tb-header-buttons { - position: absolute; - top: 86px; - right: 0; - z-index: 3; - pointer-events: none; - - @media (min-width: $layout-breakpoint-sm) { - top: 86px; - } -} - -section.tb-top-header-buttons { - top: 23px; -} - -.tb-header-buttons .tb-btn-header { - position: relative !important; - display: inline-block; - animation: tbMoveFromTopFade .3s ease both; -} - -.tb-header-buttons .tb-btn-header.ng-hide { - animation: tbMoveToTopFade .3s ease both; -} - -/*********************** - * Footer buttons - ***********************/ - -section.tb-footer-buttons { - position: fixed; - right: 20px; - bottom: 20px; - z-index: 30; - pointer-events: none; -} - -.tb-footer-buttons .tb-btn-footer { - position: relative !important; - display: inline-block !important; - animation: tbMoveFromBottomFade .3s ease both; -} - -.tb-footer-buttons .tb-btn-footer.ng-hide { - animation: tbMoveToBottomFade .3s ease both; -} - -._md-toast-open-bottom .tb-footer-buttons { - transition: all .4s cubic-bezier(.25, .8, .25, 1); - - transform: translate3d(0, -42px, 0); -} - -/*********************** - * Icons - ***********************/ - -.md-icon-button.tb-md-32 { - width: 32px; - min-width: 32px; - height: 32px; - min-height: 32px; - padding: 0 !important; - margin: 0 !important; - vertical-align: middle; -} - -.material-icons.tb-md-20 { - width: 20px; - min-width: 20px; - height: 20px; - min-height: 20px; - font-size: 20px; -} - -.material-icons.tb-md-96 { - width: 96px; - height: 96px; - font-size: 96px; -} - -/*********************** - * Layout - ***********************/ - -.tb-absolute-fill { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; -} - -.tb-progress-cover { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 6; - opacity: 1; -} - -/*********************** - * ACE - ***********************/ - -.ace_editor { - font-size: 16px !important; -} diff --git a/ui/src/scss/mixins.scss b/ui/src/scss/mixins.scss deleted file mode 100644 index 59728593b0..0000000000 --- a/ui/src/scss/mixins.scss +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import "~compass-sass-mixins/lib/compass"; - -/* stylelint-disable selector-no-vendor-prefix */ -@mixin input-placeholder { - // replaces compass/css/user-interface/input-placeholder() - - &::-webkit-input-placeholder { - @content; - } - - &:-moz-placeholder { - @content; - opacity: 1; - } - - &::-moz-placeholder { - @content; - opacity: 1; - } - - &:-ms-input-placeholder { - @content; - } -} -/* stylelint-enable selector-no-vendor-prefix */ - -@mixin line-clamp($numLines: 1, $lineHeight: 1.412) { - position: relative; - max-height: ($numLines * $lineHeight * 1em); - padding-right: 2em; - margin-right: -1em; - overflow: hidden; - line-height: $lineHeight; - text-align: justify; - - &::before { - position: absolute; - right: 1em; - bottom: 0; - content: "..."; - } - - &::after { - position: absolute; - right: 1em; - width: 1em; - height: 1em; - margin-top: .2em; - content: ""; - background: #fff; - } -} diff --git a/ui/src/svg/mdi.svg b/ui/src/svg/mdi.svg deleted file mode 100644 index bb8fa1c08b..0000000000 --- a/ui/src/svg/mdi.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/src/vendor/css.js/css.js b/ui/src/vendor/css.js/css.js deleted file mode 100644 index d8ee7160ba..0000000000 --- a/ui/src/vendor/css.js/css.js +++ /dev/null @@ -1,673 +0,0 @@ -/* eslint-disable */ - -/* jshint unused:false */ -/* global base64_decode, CSSWizardView, window, console, jQuery */ -var fi = function () { - - this.cssImportStatements = []; - this.cssKeyframeStatements = []; - - this.cssRegex = new RegExp('([\\s\\S]*?){([\\s\\S]*?)}', 'gi'); - this.cssMediaQueryRegex = '((@media [\\s\\S]*?){([\\s\\S]*?}\\s*?)})'; - this.cssKeyframeRegex = '((@.*?keyframes [\\s\\S]*?){([\\s\\S]*?}\\s*?)})'; - this.combinedCSSRegex = '((\\s*?@media[\\s\\S]*?){([\\s\\S]*?)}\\s*?})|(([\\s\\S]*?){([\\s\\S]*?)})'; //to match css & media queries together - this.cssCommentsRegex = '(\\/\\*[\\s\\S]*?\\*\\/)'; - this.cssImportStatementRegex = new RegExp('@import .*?;', 'gi'); -}; - -/* - Strip outs css comments and returns cleaned css string - - @param css, the original css string to be stipped out of comments - - @return cleanedCSS contains no css comments - */ -fi.prototype.stripComments = function (cssString) { - var regex = new RegExp(this.cssCommentsRegex, 'gi'); - - return cssString.replace(regex, ''); -}; - -/* - Parses given css string, and returns css object - keys as selectors and values are css rules - eliminates all css comments before parsing - - @param source css string to be parsed - - @return object css - */ -fi.prototype.parseCSS = function (source) { - - if (source === undefined) { - return []; - } - - var css = []; - //strip out comments - //source = this.stripComments(source); - - //get import statements - - while (true) { - var imports = this.cssImportStatementRegex.exec(source); - if (imports !== null) { - this.cssImportStatements.push(imports[0]); - css.push({ - selector: '@imports', - type: 'imports', - styles: imports[0] - }); - } else { - break; - } - } - source = source.replace(this.cssImportStatementRegex, ''); - //get keyframe statements - var keyframesRegex = new RegExp(this.cssKeyframeRegex, 'gi'); - var arr; - while (true) { - arr = keyframesRegex.exec(source); - if (arr === null) { - break; - } - css.push({ - selector: '@keyframes', - type: 'keyframes', - styles: arr[0] - }); - } - source = source.replace(keyframesRegex, ''); - - //unified regex - var unified = new RegExp(this.combinedCSSRegex, 'gi'); - - while (true) { - arr = unified.exec(source); - if (arr === null) { - break; - } - var selector = ''; - if (arr[2] === undefined) { - selector = arr[5].split('\r\n').join('\n').trim(); - } else { - selector = arr[2].split('\r\n').join('\n').trim(); - } - - /* - fetch comments and associate it with current selector - */ - var commentsRegex = new RegExp(this.cssCommentsRegex, 'gi'); - var comments = commentsRegex.exec(selector); - if (comments !== null) { - selector = selector.replace(commentsRegex, '').trim(); - } - - //determine the type - if (selector.indexOf('@media') !== -1) { - //we have a media query - var cssObject = { - selector: selector, - type: 'media', - subStyles: this.parseCSS(arr[3] + '\n}') //recursively parse media query inner css - }; - if (comments !== null) { - cssObject.comments = comments[0]; - } - css.push(cssObject); - } else { - //we have standart css - var rules = this.parseRules(arr[6]); - var style = { - selector: selector, - rules: rules - }; - if (selector === '@font-face') { - style.type = 'font-face'; - } - if (comments !== null) { - style.comments = comments[0]; - } - css.push(style); - } - } - - return css; -}; - -/* - parses given string containing css directives - and returns an array of objects containing ruleName:ruleValue pairs - - @param rules, css directive string example - \n\ncolor:white;\n font-size:18px;\n - */ -fi.prototype.parseRules = function (rules) { - //convert all windows style line endings to unix style line endings - rules = rules.split('\r\n').join('\n'); - var ret = []; - - // Split all rules but keep semicolon for base64 url data - rules = rules.split(/;(?![^\(]*\))/); - - //proccess rules line by line - for (var i = 0; i < rules.length; i++) { - var line = rules[i]; - - //determine if line is a valid css directive, ie color:white; - line = line.trim(); - if (line.indexOf(':') !== -1) { - //line contains : - line = line.split(':'); - var cssDirective = line[0].trim(); - var cssValue = line.slice(1).join(':').trim(); - - //more checks - if (cssDirective.length < 1 || cssValue.length < 1) { - continue; //there is no css directive or value that is of length 1 or 0 - // PLAIN WRONG WHAT ABOUT margin:0; ? - } - - //push rule - ret.push({ - directive: cssDirective, - value: cssValue - }); - } else { - //if there is no ':', but what if it was mis splitted value which starts with base64 - if (line.trim().substr(0, 7) == 'base64,') { //hack :) - ret[ret.length - 1].value += line.trim(); - } else { - //add rule, even if it is defective - if (line.length > 0) { - ret.push({ - directive: '', - value: line, - defective: true - }); - } - } - } - } - - return ret; //we are done! -}; -/* - just returns the rule having given directive - if not found returns false; - */ -fi.prototype.findCorrespondingRule = function (rules, directive, value) { - if (value === undefined) { - value = false; - } - var ret = false; - for (var i = 0; i < rules.length; i++) { - if (rules[i].directive == directive) { - ret = rules[i]; - if (value === rules[i].value) { - break; - } - } - } - return ret; -}; - -/* - Finds styles that have given selector, compress them, - and returns them - */ -fi.prototype.findBySelector = function (cssObjectArray, selector, contains) { - if (contains === undefined) { - contains = false; - } - - var found = []; - for (var i = 0; i < cssObjectArray.length; i++) { - if (contains === false) { - if (cssObjectArray[i].selector === selector) { - found.push(cssObjectArray[i]); - } - } else { - if (cssObjectArray[i].selector.indexOf(selector) !== -1) { - found.push(cssObjectArray[i]); - } - } - - } - if (found.length < 2) { - return found; - } else { - var base = found[0]; - for (i = 1; i < found.length; i++) { - this.intelligentCSSPush([base], found[i]); - } - return [base]; //we are done!! all properties merged into base! - } -}; - -/* - deletes cssObjects having given selector, and returns new array - */ -fi.prototype.deleteBySelector = function (cssObjectArray, selector) { - var ret = []; - for (var i = 0; i < cssObjectArray.length; i++) { - if (cssObjectArray[i].selector !== selector) { - ret.push(cssObjectArray[i]); - } - } - return ret; -}; - -/* - Compresses given cssObjectArray and tries to minimize - selector redundence. - */ -fi.prototype.compressCSS = function (cssObjectArray) { - var compressed = []; - var done = {}; - for (var i = 0; i < cssObjectArray.length; i++) { - var obj = cssObjectArray[i]; - if (done[obj.selector] === true) { - continue; - } - - var found = this.findBySelector(cssObjectArray, obj.selector); //found compressed - if (found.length !== 0) { - compressed.push(found[0]); - done[obj.selector] = true; - } - } - return compressed; -}; - -/* - Received 2 css objects with following structure - { - rules : [{directive:"", value:""}, {directive:"", value:""}, ...] - selector : "SOMESELECTOR" - } - - returns the changed(new,removed,updated) values on css1 parameter, on same structure - - if two css objects are the same, then returns false - - if a css directive exists in css1 and css2, and its value is different, it is included in diff - if a css directive exists in css1 and not css2, it is then included in diff - if a css directive exists in css2 but not css1, then it is deleted in css1, it would be included in diff but will be marked as type='DELETED' - - @object css1 css object - @object css2 css object - - @return diff css object contains changed values in css1 in regards to css2 see test input output in /test/data/css.js - */ -fi.prototype.cssDiff = function (css1, css2) { - if (css1.selector !== css2.selector) { - return false; - } - - //if one of them is media query return false, because diff function can not operate on media queries - if ((css1.type === 'media' || css2.type === 'media')) { - return false; - } - - var diff = { - selector: css1.selector, - rules: [] - }; - var rule1, rule2; - for (var i = 0; i < css1.rules.length; i++) { - rule1 = css1.rules[i]; - //find rule2 which has the same directive as rule1 - rule2 = this.findCorrespondingRule(css2.rules, rule1.directive, rule1.value); - if (rule2 === false) { - //rule1 is a new rule in css1 - diff.rules.push(rule1); - } else { - //rule2 was found only push if its value is different too - if (rule1.value !== rule2.value) { - diff.rules.push(rule1); - } - } - } - - //now for rules exists in css2 but not in css1, which means deleted rules - for (var ii = 0; ii < css2.rules.length; ii++) { - rule2 = css2.rules[ii]; - //find rule2 which has the same directive as rule1 - rule1 = this.findCorrespondingRule(css1.rules, rule2.directive); - if (rule1 === false) { - //rule1 is a new rule - rule2.type = 'DELETED'; //mark it as a deleted rule, so that other merge operations could be true - diff.rules.push(rule2); - } - } - - - if (diff.rules.length === 0) { - return false; - } - return diff; -}; - -/* - Merges 2 different css objects together - using intelligentCSSPush, - - @param cssObjectArray, target css object array - @param newArray, source array that will be pushed into cssObjectArray parameter - @param reverse, [optional], if given true, first parameter will be traversed on reversed order - effectively giving priority to the styles in newArray - */ -fi.prototype.intelligentMerge = function (cssObjectArray, newArray, reverse) { - if (reverse === undefined) { - reverse = false; - } - - - for (var i = 0; i < newArray.length; i++) { - this.intelligentCSSPush(cssObjectArray, newArray[i], reverse); - } - for (i = 0; i < cssObjectArray.length; i++) { - var cobj = cssObjectArray[i]; - if (cobj.type === 'media' || (cobj.type === 'keyframes')) { - continue; - } - cobj.rules = this.compactRules(cobj.rules); - } -}; - -/* - inserts new css objects into a bigger css object - with same selectors groupped together - - @param cssObjectArray, array of bigger css object to be pushed into - @param minimalObject, single css object - @param reverse [optional] default is false, if given, cssObjectArray will be reversly traversed - resulting more priority in minimalObject's styles - */ -fi.prototype.intelligentCSSPush = function (cssObjectArray, minimalObject, reverse) { - var pushSelector = minimalObject.selector; - //find correct selector if not found just push minimalObject into cssObject - var cssObject = false; - - if (reverse === undefined) { - reverse = false; - } - - if (reverse === false) { - for (var i = 0; i < cssObjectArray.length; i++) { - if (cssObjectArray[i].selector === minimalObject.selector) { - cssObject = cssObjectArray[i]; - break; - } - } - } else { - for (var j = cssObjectArray.length - 1; j > -1; j--) { - if (cssObjectArray[j].selector === minimalObject.selector) { - cssObject = cssObjectArray[j]; - break; - } - } - } - - if (cssObject === false) { - cssObjectArray.push(minimalObject); //just push, because cssSelector is new - } else { - if (minimalObject.type !== 'media') { - for (var ii = 0; ii < minimalObject.rules.length; ii++) { - var rule = minimalObject.rules[ii]; - //find rule inside cssObject - var oldRule = this.findCorrespondingRule(cssObject.rules, rule.directive); - if (oldRule === false) { - cssObject.rules.push(rule); - } else if (rule.type == 'DELETED') { - oldRule.type = 'DELETED'; - } else { - //rule found just update value - - oldRule.value = rule.value; - } - } - } else { - cssObject.subStyles = minimalObject.subStyles; //TODO, make this intelligent too - } - - } -}; - -/* - filter outs rule objects whose type param equal to DELETED - - @param rules, array of rules - - @returns rules array, compacted by deleting all unneccessary rules - */ -fi.prototype.compactRules = function (rules) { - var newRules = []; - for (var i = 0; i < rules.length; i++) { - if (rules[i].type !== 'DELETED') { - newRules.push(rules[i]); - } - } - return newRules; -}; -/* - computes string for ace editor using this.css or given cssBase optional parameter - - @param [optional] cssBase, if given computes cssString from cssObject array - */ -fi.prototype.getCSSForEditor = function (cssBase, depth) { - if (depth === undefined) { - depth = 0; - } - var ret = ''; - if (cssBase === undefined) { - cssBase = this.css; - } - //append imports - for (var i = 0; i < cssBase.length; i++) { - if (cssBase[i].type == 'imports') { - ret += cssBase[i].styles + '\n\n'; - } - } - for (i = 0; i < cssBase.length; i++) { - var tmp = cssBase[i]; - if (tmp.selector === undefined) { //temporarily omit media queries - continue; - } - var comments = ""; - if (tmp.comments !== undefined) { - comments = tmp.comments + '\n'; - } - - if (tmp.type == 'media') { //also put media queries to output - ret += comments + tmp.selector + '{\n'; - ret += this.getCSSForEditor(tmp.subStyles, depth + 1); - ret += '}\n\n'; - } else if (tmp.type !== 'keyframes' && tmp.type !== 'imports') { - ret += this.getSpaces(depth) + comments + tmp.selector + ' {\n'; - ret += this.getCSSOfRules(tmp.rules, depth + 1); - ret += this.getSpaces(depth) + '}\n\n'; - } - } - - //append keyFrames - for (i = 0; i < cssBase.length; i++) { - if (cssBase[i].type == 'keyframes') { - ret += cssBase[i].styles + '\n\n'; - } - } - - return ret; -}; - -fi.prototype.getImports = function (cssObjectArray) { - var imps = []; - for (var i = 0; i < cssObjectArray.length; i++) { - if (cssObjectArray[i].type == 'imports') { - imps.push(cssObjectArray[i].styles); - } - } - return imps; -}; -/* - given rules array, returns visually formatted css string - to be used inside editor - */ -fi.prototype.getCSSOfRules = function (rules, depth) { - var ret = ''; - for (var i = 0; i < rules.length; i++) { - if (rules[i] === undefined) { - continue; - } - if (rules[i].defective === undefined) { - ret += this.getSpaces(depth) + rules[i].directive + ' : ' + rules[i].value + ';\n'; - } else { - ret += this.getSpaces(depth) + rules[i].value + ';\n'; - } - - } - return ret || '\n'; -}; - -/* - A very simple helper function returns number of spaces appended in a single string, - the number depends input parameter, namely input*2 - */ -fi.prototype.getSpaces = function (num) { - var ret = ''; - for (var i = 0; i < num * 4; i++) { - ret += ' '; - } - return ret; -}; - -/* - Given css string or objectArray, parses it and then for every selector, - prepends this.cssPreviewNamespace to prevent css collision issues - - @returns css string in which this.cssPreviewNamespace prepended - */ -fi.prototype.applyNamespacing = function (css, forcedNamespace) { - var cssObjectArray = css; - var namespaceClass = '.' + this.cssPreviewNamespace; - if (forcedNamespace !== undefined) { - namespaceClass = forcedNamespace; - } - - if (typeof css === 'string') { - cssObjectArray = this.parseCSS(css); - } - - for (var i = 0; i < cssObjectArray.length; i++) { - var obj = cssObjectArray[i]; - - //bypass namespacing for @font-face @keyframes @import - if (obj.selector.indexOf('@font-face') > -1 || obj.selector.indexOf('keyframes') > -1 || obj.selector.indexOf('@import') > -1 || obj.selector.indexOf('.form-all') > -1 || obj.selector.indexOf('#stage') > -1) { - continue; - } - - if (obj.type !== 'media') { - var selector = obj.selector.split(','); - var newSelector = []; - for (var j = 0; j < selector.length; j++) { - if (selector[j].indexOf('.supernova') === -1) { //do not apply namespacing to selectors including supernova - newSelector.push(namespaceClass + ' ' + selector[j]); - } else { - newSelector.push(selector[j]); - } - } - obj.selector = newSelector.join(','); - } else { - obj.subStyles = this.applyNamespacing(obj.subStyles, forcedNamespace); //handle media queries as well - } - } - - return cssObjectArray; -}; - -/* - given css string or object array, clears possible namespacing from - all of the selectors inside the css - */ -fi.prototype.clearNamespacing = function (css, returnObj) { - if (returnObj === undefined) { - returnObj = false; - } - var cssObjectArray = css; - var namespaceClass = '.' + this.cssPreviewNamespace; - if (typeof css === 'string') { - cssObjectArray = this.parseCSS(css); - } - - for (var i = 0; i < cssObjectArray.length; i++) { - var obj = cssObjectArray[i]; - - if (obj.type !== 'media') { - var selector = obj.selector.split(','); - var newSelector = []; - for (var j = 0; j < selector.length; j++) { - newSelector.push(selector[j].split(namespaceClass + ' ').join('')); - } - obj.selector = newSelector.join(','); - } else { - obj.subStyles = this.clearNamespacing(obj.subStyles, true); //handle media queries as well - } - } - if (returnObj === false) { - return this.getCSSForEditor(cssObjectArray); - } else { - return cssObjectArray; - } - -}; - -/* - creates a new style tag (also destroys the previous one) - and injects given css string into that css tag - */ -fi.prototype.createStyleElement = function (id, css, format) { - if (format === undefined) { - format = false; - } - - if (this.testMode === false && format !== 'nonamespace') { - //apply namespacing classes - css = this.applyNamespacing(css); - } - - if (typeof css != 'string') { - css = this.getCSSForEditor(css); - } - //apply formatting for css - if (format === true) { - css = this.getCSSForEditor(this.parseCSS(css)); - } - - if (this.testMode !== false) { - return this.testMode('create style #' + id, css); //if test mode, just pass result to callback - } - - var __el = document.getElementById(id); - if (__el) { - __el.parentNode.removeChild(__el); - } - - var head = document.head || document.getElementsByTagName('head')[0], - style = document.createElement('style'); - - style.id = id; - style.type = 'text/css'; - - head.appendChild(style); - - if (style.styleSheet && !style.sheet) { - style.styleSheet.cssText = css; - } else { - style.appendChild(document.createTextNode(css)); - } -}; - -export default fi; - -/* eslint-enable */ \ No newline at end of file diff --git a/ui/src/vendor/css.js/css.min.js b/ui/src/vendor/css.js/css.min.js deleted file mode 100644 index b0e02da6f2..0000000000 --- a/ui/src/vendor/css.js/css.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! css.js 19-04-2016 */ -!function(a){"use strict";var b=function(){this.cssImportStatements=[],this.cssKeyframeStatements=[],this.cssRegex=new RegExp("([\\s\\S]*?){([\\s\\S]*?)}","gi"),this.cssMediaQueryRegex="((@media [\\s\\S]*?){([\\s\\S]*?}\\s*?)})",this.cssKeyframeRegex="((@.*?keyframes [\\s\\S]*?){([\\s\\S]*?}\\s*?)})",this.combinedCSSRegex="((\\s*?(?:\\/\\*[\\s\\S]*?\\*\\/)?\\s*?@media[\\s\\S]*?){([\\s\\S]*?)}\\s*?})|(([\\s\\S]*?){([\\s\\S]*?)})",this.cssCommentsRegex="(\\/\\*[\\s\\S]*?\\*\\/)",this.cssImportStatementRegex=new RegExp("@import .*?;","gi")};b.prototype.stripComments=function(a){var b=new RegExp(this.cssCommentsRegex,"gi");return a.replace(b,"")},b.prototype.parseCSS=function(a){if(void 0===a)return[];for(var b=[];;){var c=this.cssImportStatementRegex.exec(a);if(null===c)break;this.cssImportStatements.push(c[0]),b.push({selector:"@imports",type:"imports",styles:c[0]})}a=a.replace(this.cssImportStatementRegex,"");for(var d,e=new RegExp(this.cssKeyframeRegex,"gi");;){if(d=e.exec(a),null===d)break;b.push({selector:"@keyframes",type:"keyframes",styles:d[0]})}a=a.replace(e,"");for(var f=new RegExp(this.combinedCSSRegex,"gi");;){if(d=f.exec(a),null===d)break;var g="";g=void 0===d[2]?d[5].split("\r\n").join("\n").trim():d[2].split("\r\n").join("\n").trim();var h=new RegExp(this.cssCommentsRegex,"gi"),i=h.exec(g);if(null!==i&&(g=g.replace(h,"").trim()),g=g.replace(/\n+/,"\n"),-1!==g.indexOf("@media")){var j={selector:g,type:"media",subStyles:this.parseCSS(d[3]+"\n}")};null!==i&&(j.comments=i[0]),b.push(j)}else{var k=this.parseRules(d[6]),l={selector:g,rules:k};"@font-face"===g&&(l.type="font-face"),null!==i&&(l.comments=i[0]),b.push(l)}}return b},b.prototype.parseRules=function(a){a=a.split("\r\n").join("\n");var b=[];a=a.split(";");for(var c=0;c0&&b.push({directive:"",value:d,defective:!0})}return b},b.prototype.findCorrespondingRule=function(a,b,c){void 0===c&&(c=!1);for(var d=!1,e=0;e-1;f--)if(a[f].selector===b.selector){d=a[f];break}if(d===!1)a.push(b);else if("media"!==b.type)for(var g=0;gc;c++)b+=" ";return b},b.prototype.applyNamespacing=function(a,b){var c=a,d="."+this.cssPreviewNamespace;void 0!==b&&(d=b),"string"==typeof a&&(c=this.parseCSS(a));for(var e=0;e-1||f.selector.indexOf("keyframes")>-1||f.selector.indexOf("@import")>-1||f.selector.indexOf(".form-all")>-1||f.selector.indexOf("#stage")>-1))if("media"!==f.type){for(var g=f.selector.split(","),h=[],i=0;i { - /* It is expected what the name of a locale file has the following format: */ - /* 'locale.constant-LANG_CODE[_REGION_CODE].json', e.g. locale.constant-es.json or locale.constant-zh_CN.json*/ - langs.push(item.name.slice(item.name.lastIndexOf('-') + 1, -5)); -}); - -/* devtool: 'cheap-module-eval-source-map', */ - -module.exports = { - mode: 'development', - devtool: 'source-map', - entry: [ - './src/app/app.js', - 'webpack-hot-middleware/client?reload=true', - 'webpack-material-design-icons' - ], - output: { - path: path.resolve(__dirname, 'target/generated-resources/public/static'), - publicPath: PUBLIC_RESOURCE_PATH, - filename: 'bundle.js', - }, - plugins: [ - new webpack.ProvidePlugin({ - $: "jquery", - jQuery: "jquery", - "window.jQuery": "jquery", - tinycolor: "tinycolor2", - tv4: "tv4", - moment: "moment" - }), - new CopyWebpackPlugin([ - { - from: './src/thingsboard.ico', - to: 'thingsboard.ico' - }, - { - from: './src/app/locale', - to: 'locale', - ignore: [ '*.js' ], - transform: function(content, path) { - return Buffer.from(jsonminify(content.toString())); - } - } - ]), - new webpack.HotModuleReplacementPlugin(), - new HtmlWebpackPlugin({ - template: './src/index.html', - filename: 'index.html', - title: 'ThingsBoard', - inject: 'body', - }), - new StyleLintPlugin(), - new MiniCssExtractPlugin({ - filename: 'style.[contentHash].css' - }), - new webpack.DefinePlugin({ - THINGSBOARD_VERSION: JSON.stringify(require('./package.json').version), - '__DEVTOOLS__': false, - PUBLIC_PATH: JSON.stringify(PUBLIC_RESOURCE_PATH), - SUPPORTED_LANGS: JSON.stringify(langs) - }) - ], - node: { - tls: "empty", - fs: "empty" - }, - module: { - rules: [ - { - test: /\.jsx$/, - use: [ - { - loader: 'babel-loader', - options: { - cacheDirectory: true - } - } - ], - exclude: /node_modules/, - include: __dirname, - }, - { - test: /\.js$/, - use: [ - { - loader: 'ng-annotate-loader', - options: { - ngAnnotate: 'ng-annotate-patched', - es6: true, - explicitOnly: false - } - }, - { - loader: 'babel-loader', - options: { - cacheDirectory: true - } - } - ], - exclude: /node_modules/, - include: __dirname, - }, - { - test: /\.js$/, - use: [ - { - loader: 'eslint-loader', - options: { - parser: 'babel-eslint' - } - } - ], - exclude: /node_modules|vendor/, - include: __dirname, - }, - { - test: /\.css$/, - use: [ - MiniCssExtractPlugin.loader, - 'css-loader' - ] - }, - { - test: /\.scss$/, - use: [ - MiniCssExtractPlugin.loader, - 'css-loader', - 'postcss-loader', - 'sass-loader' - ] - }, - { - test: /\.less$/, - use: [ - MiniCssExtractPlugin.loader, - 'css-loader', - 'postcss-loader', - 'less-loader' - ] - }, - { - test: /\.tpl\.html$/, - use: [ - { - loader: 'ngtemplate-loader', - options: { - relativeTo: path.resolve(__dirname, './src/app') - } - }, - { - loader: 'html-loader' - }, - { - loader: 'html-minifier-loader', - options: { - caseSensitive: true, - removeComments: true, - collapseWhitespace: false, - preventAttributesEscaping: true, - removeEmptyAttributes: false - } - } - ] - }, - { - test: /\.(svg)(\?v=[0-9]+\.[0-9]+\.[0-9]+)?$/, - use: [ - { - loader: 'url-loader', - options: { - limit: 8192 - } - } - ] - }, - { - test: /\.(png|jpe?g|gif|woff|woff2|ttf|otf|eot|ico)(\?v=[0-9]+\.[0-9]+\.[0-9]+)?$/, - use: [ - { - loader: 'url-loader', - options: { - limit: 8192 - } - }, - { - loader: 'img-loader', - options: { - minimize: true - } - } - ] - } - ], - } -}; diff --git a/ui/webpack.config.js b/ui/webpack.config.js deleted file mode 100644 index 848712b9ad..0000000000 --- a/ui/webpack.config.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable */ - -if (process.env.NODE_ENV === 'production') { - module.exports = require('./webpack.config.prod'); -} else { - module.exports = require('./webpack.config.dev'); -} diff --git a/ui/webpack.config.prod.js b/ui/webpack.config.prod.js deleted file mode 100644 index 5985ba1876..0000000000 --- a/ui/webpack.config.prod.js +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright © 2016-2020 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* eslint-disable */ - -const HtmlWebpackPlugin = require('html-webpack-plugin'); -const MiniCssExtractPlugin = require('mini-css-extract-plugin'); -const CopyWebpackPlugin = require('copy-webpack-plugin'); -const CompressionPlugin = require('compression-webpack-plugin'); - -const webpack = require('webpack'); -const path = require('path'); -const dirTree = require('directory-tree'); -const jsonminify = require("jsonminify"); - -const PUBLIC_RESOURCE_PATH = '/static/'; - -var langs = []; -dirTree('./src/app/locale/', {extensions:/\.json$/}, (item) => { - /* It is expected what the name of a locale file has the following format: */ - /* 'locale.constant-LANG_CODE[_REGION_CODE].json', e.g. locale.constant-es.json or locale.constant-zh_CN.json*/ - langs.push(item.name.slice(item.name.lastIndexOf('-') + 1, -5)); -}); - -module.exports = { - mode: 'production', - entry: [ - './src/app/app.js', - 'webpack-material-design-icons' - ], - output: { - path: path.resolve(__dirname, 'target/generated-resources/public/static'), - publicPath: PUBLIC_RESOURCE_PATH, - filename: 'bundle.[hash].js', - pathinfo: false - }, - plugins: [ - new webpack.ProvidePlugin({ - $: "jquery", - jQuery: "jquery", - "window.jQuery": "jquery", - tinycolor: "tinycolor2", - tv4: "tv4", - moment: "moment" - }), - new CopyWebpackPlugin([ - { - from: './src/thingsboard.ico', - to: 'thingsboard.ico' - }, - { - from: './src/app/locale', - to: 'locale', - ignore: [ '*.js' ], - transform: function(content, path) { - return Buffer.from(jsonminify(content.toString())); - } - } - ]), - new HtmlWebpackPlugin({ - template: './src/index.html', - filename: '../index.html', - title: 'ThingsBoard', - inject: 'body', - }), - new MiniCssExtractPlugin({ - filename: 'style.[contentHash].css' - }), - new webpack.DefinePlugin({ - THINGSBOARD_VERSION: JSON.stringify(require('./package.json').version), - '__DEVTOOLS__': false, - PUBLIC_PATH: PUBLIC_RESOURCE_PATH, - SUPPORTED_LANGS: JSON.stringify(langs) - }), - new CompressionPlugin({ - filename: "[path].gz[query]", - algorithm: "gzip", - test: /\.js$|\.css$|\.svg$|\.ttf$|\.woff$|\.woff2$|\.eot$|\.json$/, - threshold: 10240, - minRatio: 0.8 - }) - ], - node: { - tls: "empty", - fs: "empty" - }, - module: { - rules: [ - { - test: /\.jsx$/, - use: [ - { - loader: 'babel-loader', - options: { - cacheDirectory: true - } - } - ], - exclude: /node_modules/, - include: __dirname, - }, - { - test: /\.js$/, - use: [ - { - loader: 'ng-annotate-loader', - options: { - ngAnnotate: 'ng-annotate-patched', - es6: true, - explicitOnly: false - } - }, - { - loader: 'babel-loader', - options: { - cacheDirectory: true - } - } - ], - exclude: /node_modules/, - include: __dirname, - }, - { - test: /\.js$/, - use: [ - { - loader: 'eslint-loader', - options: { - parser: 'babel-eslint' - } - } - ], - exclude: /node_modules|vendor/, - include: __dirname, - }, - { - test: /\.css$/, - use: [ - MiniCssExtractPlugin.loader, - 'css-loader' - ] - }, - { - test: /\.scss$/, - use: [ - MiniCssExtractPlugin.loader, - 'css-loader', - 'postcss-loader', - 'sass-loader' - ] - }, - { - test: /\.less$/, - use: [ - MiniCssExtractPlugin.loader, - 'css-loader', - 'postcss-loader', - 'less-loader' - ] - }, - { - test: /\.tpl\.html$/, - use: [ - { - loader: 'ngtemplate-loader', - options: { - relativeTo: path.resolve(__dirname, './src/app') - } - }, - { - loader: 'html-loader' - }, - { - loader: 'html-minifier-loader', - options: { - caseSensitive: true, - removeComments: true, - collapseWhitespace: false, - preventAttributesEscaping: true, - removeEmptyAttributes: false - } - } - ] - }, - { - test: /\.(svg)(\?v=[0-9]+\.[0-9]+\.[0-9]+)?$/, - use: [ - { - loader: 'url-loader', - options: { - limit: 8192 - } - } - ] - }, - { - test: /\.(png|jpe?g|gif|woff|woff2|ttf|otf|eot|ico)(\?v=[0-9]+\.[0-9]+\.[0-9]+)?$/, - use: [ - { - loader: 'url-loader', - options: { - limit: 8192 - } - }, - { - loader: 'img-loader', - options: { - minimize: true - } - } - ] - } - ], - } -};